diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9bedc01c55..41106fcb2b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -39,16 +39,12 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: - version: nightly-f625d0fa7c51e65b4bf1e8f7931cd1c6e2e285e9 + version: nightly-143abd6a768eeb52a5785240b763d72a56987b4a - name: Run e2e tests local shell: bash run: e2e_test/run_all_tests.sh - - name: Run e2e tests alfajores - shell: bash - run: NETWORK=alfajores e2e_test/run_all_tests.sh - Lint: runs-on: ["8-cpu","self-hosted","org"] steps: diff --git a/.github/workflows/e2e-test-deployed-network.yaml b/.github/workflows/e2e-test-deployed-network.yaml new file mode 100644 index 0000000000..6bf14d0127 --- /dev/null +++ b/.github/workflows/e2e-test-deployed-network.yaml @@ -0,0 +1,42 @@ +name: e2e-test-deployed-network + +on: + schedule: + - cron: "0 14 * * *" + pull_request: + branches: + - master + - celo* + paths: + - 'e2e_test/**' + + workflow_dispatch: + +permissions: + contents: read + +jobs: + e2e-tests: + runs-on: ["8-cpu","self-hosted","org"] + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: '1.21' + + - name: Build + run: make all + + - uses: actions/setup-node@v4 + with: + node-version: 18 + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly-143abd6a768eeb52a5785240b763d72a56987b4a + + - name: Run e2e tests alfajores + shell: bash + run: NETWORK=alfajores e2e_test/run_all_tests.sh diff --git a/core/types/celo_transaction_signing_tx_funcs.go b/core/types/celo_transaction_signing_tx_funcs.go index 8d53abff34..a0e55c72e9 100644 --- a/core/types/celo_transaction_signing_tx_funcs.go +++ b/core/types/celo_transaction_signing_tx_funcs.go @@ -18,6 +18,9 @@ var ( return nil, nil, nil, fmt.Errorf("%w %v", ErrDeprecatedTxType, tx.Type()) }, sender: func(tx *Transaction, hashFunc func(tx *Transaction, chainID *big.Int) common.Hash, signerChainID *big.Int) (common.Address, error) { + if tx.IsCeloLegacy() { + return common.Address{}, fmt.Errorf("%w %v %v", ErrDeprecatedTxType, tx.Type(), "(celo legacy)") + } return common.Address{}, fmt.Errorf("%w %v", ErrDeprecatedTxType, tx.Type()) }, } diff --git a/e2e_test/js-tests/send_tx.mjs b/e2e_test/js-tests/send_tx.mjs index 90b3bd7a68..0e61228104 100755 --- a/e2e_test/js-tests/send_tx.mjs +++ b/e2e_test/js-tests/send_tx.mjs @@ -1,50 +1,10 @@ #!/usr/bin/env node import { - createWalletClient, - createPublicClient, - http, - defineChain, TransactionReceiptNotFoundError, } from "viem"; -import { celoAlfajores } from "viem/chains"; -import { privateKeyToAccount } from "viem/accounts"; +import { publicClient, walletClient, account } from "./viem_setup.mjs" -const [chainId, privateKey, feeCurrency, waitBlocks, replaceTxAfterWait, celoValue] = - process.argv.slice(2); -const devChain = defineChain({ - ...celoAlfajores, - id: parseInt(chainId, 10), - name: "local dev chain", - network: "dev", - rpcUrls: { - default: { - http: ["http://127.0.0.1:8545"], - }, - }, -}); - -var chain; -switch(process.env.NETWORK) { - case "alfajores": - chain = celoAlfajores; - break; - default: - chain = devChain; - // Code to run if no cases match -}; - -const account = privateKeyToAccount(privateKey); - -const publicClient = createPublicClient({ - account, - chain: chain, - transport: http(), -}); -const walletClient = createWalletClient({ - account, - chain: chain, - transport: http(), -}); +const [chainId, privateKey, feeCurrency, waitBlocks, replaceTxAfterWait, celoValue] = process.argv.slice(2); function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/e2e_test/js-tests/test_viem_smoketest.mjs b/e2e_test/js-tests/test_viem_smoketest.mjs new file mode 100644 index 0000000000..50b2d1a810 --- /dev/null +++ b/e2e_test/js-tests/test_viem_smoketest.mjs @@ -0,0 +1,76 @@ +import { assert } from "chai"; +import "mocha"; +import { + parseAbi, +} from "viem"; +import fs from "fs"; +import { publicClient, walletClient } from "./viem_setup.mjs" + +// Load compiled contract +const testContractJSON = JSON.parse(fs.readFileSync(process.env.COMPILED_TEST_CONTRACT, 'utf8')); + +// check checks that the receipt has status success and that the transaction +// type matches the expected type, since viem sometimes mangles the type when +// building txs. +async function check(txHash, type) { + const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash }); + assert.equal(receipt.status, "success", "receipt status 'failure'"); + const transaction = await publicClient.getTransaction({ hash: txHash }); + assert.equal(transaction.type, type, "transaction type does not match"); +} + +// sendTypedTransaction sends a transaction with the given type and an optional +// feeCurrency. +async function sendTypedTransaction(type, feeCurrency) { + return await walletClient.sendTransaction({ + to: "0x00000000000000000000000000000000DeaDBeef", + value: 1, + type: type, + feeCurrency: feeCurrency, + }); +} + +// sendTypedSmartContractTransaction initiates a token transfer with the given type +// and an optional feeCurrency. +async function sendTypedSmartContractTransaction(type, feeCurrency) { + const abi = parseAbi(['function transfer(address to, uint256 value) external returns (bool)']); + return await walletClient.writeContract({ + abi: abi, + address: process.env.TOKEN_ADDR, + functionName: 'transfer', + args: ['0x00000000000000000000000000000000DeaDBeef', 1n], + type: type, + feeCurrency: feeCurrency, + }); +} + +// sendTypedCreateTransaction sends a create transaction with the given type +// and an optional feeCurrency. +async function sendTypedCreateTransaction(type, feeCurrency) { + return await walletClient.deployContract({ + type: type, + feeCurrency: feeCurrency, + bytecode: testContractJSON.bytecode.object, + abi: testContractJSON.abi, + // The constructor args for the test contract at ../debug-fee-currency/DebugFeeCurrency.sol + args: [1n, true, true, true], + }); +} + +["legacy", "eip2930", "eip1559", "cip64"].forEach(function (type) { + describe("viem smoke test, tx type " + type, () => { + const feeCurrency = type == "cip64" ? process.env.FEE_CURRENCY : undefined; + it("send tx", async () => { + const send = await sendTypedTransaction(type, feeCurrency); + await check(send, type); + }); + it("send create tx", async () => { + const create = await sendTypedCreateTransaction(type, feeCurrency); + await check(create, type); + }); + it("send contract interaction tx", async () => { + const contract = await sendTypedSmartContractTransaction(type, feeCurrency); + await check(contract, type); + }); + }); +}); diff --git a/e2e_test/js-tests/test_viem_tx.mjs b/e2e_test/js-tests/test_viem_tx.mjs index c3e5240aac..07832fe8ac 100644 --- a/e2e_test/js-tests/test_viem_tx.mjs +++ b/e2e_test/js-tests/test_viem_tx.mjs @@ -1,48 +1,9 @@ import { assert } from "chai"; import "mocha"; import { - createPublicClient, - createWalletClient, - http, - defineChain, parseAbi, } from "viem"; -import { celoAlfajores } from "viem/chains"; -import { privateKeyToAccount } from "viem/accounts"; - -// Setup up chain -const devChain = defineChain({ - ...celoAlfajores, - id: 1337, - name: "local dev chain", - network: "dev", - rpcUrls: { - default: { - http: [process.env.ETH_RPC_URL], - }, - }, -}); - -const chain = (() => { - switch (process.env.NETWORK) { - case 'alfajores': - return celoAlfajores - default: - return devChain - }; -})(); - -// Set up clients/wallet -const publicClient = createPublicClient({ - chain: chain, - transport: http(), -}); -const account = privateKeyToAccount(process.env.ACC_PRIVKEY); -const walletClient = createWalletClient({ - account, - chain: chain, - transport: http(), -}); +import { publicClient, walletClient } from "./viem_setup.mjs" // Returns the base fee per gas for the current block multiplied by 2 to account for any increase in the subsequent block. async function getGasFees(publicClient, tip, feeCurrency) { @@ -60,13 +21,12 @@ const testNonceBump = async ( shouldReplace, ) => { const syncBarrierRequest = await walletClient.prepareTransactionRequest({ - account, + to: "0x00000000000000000000000000000000DeaDBeef", value: 2, gas: 22000, }); const firstTxHash = await walletClient.sendTransaction({ - account, to: "0x00000000000000000000000000000000DeaDBeef", value: 2, gas: 171000, @@ -78,7 +38,6 @@ const testNonceBump = async ( var secondTxHash; try { secondTxHash = await walletClient.sendTransaction({ - account, to: "0x00000000000000000000000000000000DeaDBeef", value: 3, gas: 171000, @@ -95,7 +54,7 @@ const testNonceBump = async ( shouldReplace ) { throw err; // Only throw if unexpected error. - } + } } const syncBarrierSignature = await walletClient.signTransaction(syncBarrierRequest); @@ -115,7 +74,6 @@ const testNonceBump = async ( describe("viem send tx", () => { it("send basic tx and check receipt", async () => { const request = await walletClient.prepareTransactionRequest({ - account, to: "0x00000000000000000000000000000000DeaDBeef", value: 1, gas: 21000, @@ -130,7 +88,6 @@ describe("viem send tx", () => { it("send basic tx using viem gas estimation and check receipt", async () => { const request = await walletClient.prepareTransactionRequest({ - account, to: "0x00000000000000000000000000000000DeaDBeef", value: 1, }); @@ -145,7 +102,6 @@ describe("viem send tx", () => { it("send fee currency tx with explicit gas fields and check receipt", async () => { const [maxFeePerGas, tip] = await getGasFees(publicClient, 2n, process.env.FEE_CURRENCY); const request = await walletClient.prepareTransactionRequest({ - account, to: "0x00000000000000000000000000000000DeaDBeef", value: 2, gas: 171000, @@ -163,7 +119,6 @@ describe("viem send tx", () => { it("test gas price difference for fee currency", async () => { const request = await walletClient.prepareTransactionRequest({ - account, to: "0x00000000000000000000000000000000DeaDBeef", value: 2, gas: 171000, @@ -208,8 +163,8 @@ describe("viem send tx", () => { // The expected value for the max fee should be the (baseFeePerGas * multiplier) + maxPriorityFeePerGas // Instead what is currently returned is (maxFeePerGas * multiplier) + maxPriorityFeePerGas const maxPriorityFeeInFeeCurrency = (maxPriorityFeePerGasNative * numerator) / denominator; - const maxFeeInFeeCurrency = ((block.baseFeePerGas +maxPriorityFeePerGasNative)*numerator) /denominator; - assert.equal(fees.maxFeePerGas, ((maxFeeInFeeCurrency*12n)/10n) + maxPriorityFeeInFeeCurrency); + const maxFeeInFeeCurrency = ((block.baseFeePerGas + maxPriorityFeePerGasNative) * numerator) / denominator; + assert.equal(fees.maxFeePerGas, ((maxFeeInFeeCurrency * 12n) / 10n) + maxPriorityFeeInFeeCurrency); assert.equal(fees.maxPriorityFeePerGas, maxPriorityFeeInFeeCurrency); // check that the prepared transaction request uses the @@ -220,7 +175,6 @@ describe("viem send tx", () => { it("send fee currency with gas estimation tx and check receipt", async () => { const request = await walletClient.prepareTransactionRequest({ - account, to: "0x00000000000000000000000000000000DeaDBeef", value: 2, feeCurrency: process.env.FEE_CURRENCY, @@ -286,7 +240,6 @@ describe("viem send tx", () => { it("send tx with unregistered fee currency", async () => { const request = await walletClient.prepareTransactionRequest({ - account, to: "0x00000000000000000000000000000000DeaDBeef", value: 2, gas: 171000, @@ -334,11 +287,11 @@ describe("viem send tx", () => { assert.fail(`Converted base fee (${convertedBaseFee}) not less than native base fee (${block.baseFeePerGas})`); } const request = await walletClient.prepareTransactionRequest({ - account, to: "0x00000000000000000000000000000000DeaDBeef", value: 2, gas: 171000, feeCurrency: process.env.FEE_CURRENCY, + feeCurrency: fc, maxFeePerGas: convertedBaseFee +2n, maxPriorityFeePerGas: 2n, }); @@ -352,13 +305,13 @@ describe("viem send tx", () => { }); async function getRate(feeCurrencyAddress) { - const abi = parseAbi(['function getExchangeRate(address token) public view returns (uint256 numerator, uint256 denominator)']); - const [numerator, denominator] = await publicClient.readContract({ - address: process.env.FEE_CURRENCY_DIRECTORY_ADDR, - abi: abi, - functionName: 'getExchangeRate', - args: [feeCurrencyAddress], - }); + const abi = parseAbi(['function getExchangeRate(address token) public view returns (uint256 numerator, uint256 denominator)']); + const [numerator, denominator] = await publicClient.readContract({ + address: process.env.FEE_CURRENCY_DIRECTORY_ADDR, + abi: abi, + functionName: 'getExchangeRate', + args: [feeCurrencyAddress], + }); return { toFeeCurrency: (v) => (v * numerator) / denominator, toNative: (v) => (v * denominator) / numerator, diff --git a/e2e_test/js-tests/viem_setup.mjs b/e2e_test/js-tests/viem_setup.mjs new file mode 100644 index 0000000000..664f1f75f2 --- /dev/null +++ b/e2e_test/js-tests/viem_setup.mjs @@ -0,0 +1,44 @@ +import { assert } from "chai"; +import "mocha"; +import { + createPublicClient, + createWalletClient, + http, + defineChain, +} from "viem"; +import { celoAlfajores } from "viem/chains"; +import { privateKeyToAccount } from "viem/accounts"; + +// Setup up chain +const devChain = defineChain({ + ...celoAlfajores, + id: 1337, + name: "local dev chain", + network: "dev", + rpcUrls: { + default: { + http: [process.env.ETH_RPC_URL], + }, + }, +}); + +const chain = (() => { + switch (process.env.NETWORK) { + case 'alfajores': + return celoAlfajores + default: + return devChain + }; +})(); + +// Set up clients/wallet +export const publicClient = createPublicClient({ + chain: chain, + transport: http(), +}); +export const account = privateKeyToAccount(process.env.ACC_PRIVKEY); +export const walletClient = createWalletClient({ + account, + chain: chain, + transport: http(), +}); \ No newline at end of file diff --git a/e2e_test/smoketest_unsupported_txs/unsupported_txs_test.go b/e2e_test/smoketest_unsupported_txs/unsupported_txs_test.go new file mode 100644 index 0000000000..781e520b15 --- /dev/null +++ b/e2e_test/smoketest_unsupported_txs/unsupported_txs_test.go @@ -0,0 +1,105 @@ +//go:build smoketest + +package smoketestunsupportedtxs + +import ( + "context" + "crypto/ecdsa" + "flag" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/params" + "github.com/stretchr/testify/require" +) + +var ( + privateKey string + opGethRpcURL string + feeCurrency string + deadAddr = common.HexToAddress("0x00000000000000000000000000000000DeaDBeef") +) + +func init() { + // Define your custom flag + flag.StringVar(&privateKey, "private-key", "", "private key of transaction sender") + flag.StringVar(&opGethRpcURL, "op-geth-url", "", "op-geth rpc url") + flag.StringVar(&feeCurrency, "fee-currency", "", "address of the fee currency to use") +} + +func TestTxSendingFails(t *testing.T) { + key, err := parsePrivateKey(privateKey) + require.NoError(t, err) + + feeCurrencyAddr := common.HexToAddress(feeCurrency) + + client, err := ethclient.Dial(opGethRpcURL) + require.NoError(t, err) + + chainId, err := client.ChainID(context.Background()) + require.NoError(t, err) + + // Get a signer that can sign deprecated txs, we need cel2 configured but not active yet. + cel2Time := uint64(1) + signer := types.MakeSigner(¶ms.ChainConfig{Cel2Time: &cel2Time, ChainID: chainId}, big.NewInt(0), 0) + + t.Run("CeloLegacy", func(t *testing.T) { + gasPrice, err := client.SuggestGasPriceForCurrency(context.Background(), &feeCurrencyAddr) + require.NoError(t, err) + + nonce, err := client.PendingNonceAt(context.Background(), crypto.PubkeyToAddress(key.PublicKey)) + require.NoError(t, err) + + txdata := &types.LegacyTx{ + Nonce: nonce, + To: &deadAddr, + Gas: 100_000, + GasPrice: gasPrice, + FeeCurrency: &feeCurrencyAddr, + Value: big.NewInt(1), + CeloLegacy: true, + } + + tx, err := types.SignNewTx(key, signer, txdata) + require.NoError(t, err) + + // we expect this to fail because the tx is not supported. + err = client.SendTransaction(context.Background(), tx) + require.Error(t, err) + }) + + t.Run("CIP42", func(t *testing.T) { + gasFeeCap, err := client.SuggestGasPriceForCurrency(context.Background(), &feeCurrencyAddr) + require.NoError(t, err) + + nonce, err := client.PendingNonceAt(context.Background(), crypto.PubkeyToAddress(key.PublicKey)) + require.NoError(t, err) + + txdata := &types.CeloDynamicFeeTx{ + Nonce: nonce, + To: &deadAddr, + Gas: 100_000, + GasFeeCap: gasFeeCap, + FeeCurrency: &feeCurrencyAddr, + Value: big.NewInt(1), + } + + tx, err := types.SignNewTx(key, signer, txdata) + require.NoError(t, err) + + // we expect this to fail because the tx is not supported. + err = client.SendTransaction(context.Background(), tx) + require.Error(t, err) + }) +} + +func parsePrivateKey(privateKey string) (*ecdsa.PrivateKey, error) { + if len(privateKey) >= 2 && privateKey[0] == '0' && (privateKey[1] == 'x' || privateKey[1] == 'X') { + privateKey = privateKey[2:] + } + return crypto.HexToECDSA(privateKey) +} diff --git a/e2e_test/test_smoketest.sh b/e2e_test/test_smoketest.sh new file mode 100755 index 0000000000..7358d314a4 --- /dev/null +++ b/e2e_test/test_smoketest.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -eo pipefail + +source shared.sh +prepare_node + +(cd debug-fee-currency && forge build --out $PWD/out $PWD) +export COMPILED_TEST_CONTRACT=../debug-fee-currency/out/DebugFeeCurrency.sol/DebugFeeCurrency.json +(cd js-tests && ./node_modules/mocha/bin/mocha.js test_viem_smoketest.mjs --timeout 25000 --exit) +echo go test -v ./smoketest_unsupported -op-geth-url $ETH_RPC_URL -private-key $ACC_PRIVKEY -fee-currency $FEE_CURRENCY +go test -v ./smoketest_unsupported_txs -tags smoketest -op-geth-url $ETH_RPC_URL -private-key $ACC_PRIVKEY -fee-currency $FEE_CURRENCY