diff --git a/contracts/test/SeiEndpointsTest.js b/contracts/test/SeiEndpointsTest.js new file mode 100644 index 0000000000..825e0bffd5 --- /dev/null +++ b/contracts/test/SeiEndpointsTest.js @@ -0,0 +1,84 @@ +const { expect } = require("chai"); +const { ethers } = require('hardhat'); +const { deployWasm, ABI, WASM, executeWasm, deployErc20PointerForCw20, getAdmin, setupSigners } = require("./lib") + +describe("Sei Endpoints Tester", function () { + let accounts; + let admin; + let cw20Address; + let pointer; + + before(async function () { + accounts = await setupSigners(await hre.ethers.getSigners()); + admin = await getAdmin(); + + cw20Address = await deployWasm(WASM.CW20, accounts[0].seiAddress, "cw20", { + name: "Test", + symbol: "TEST", + decimals: 6, + initial_balances: [ + { address: admin.seiAddress, amount: "1000000" }, + { address: accounts[0].seiAddress, amount: "2000000" }, + { address: accounts[1].seiAddress, amount: "3000000" } + ], + mint: { + "minter": admin.seiAddress, "cap": "99900000000" + } + }); + // deploy a pointer + const pointerAddr = await deployErc20PointerForCw20(hre.ethers.provider, cw20Address); + const contract = new hre.ethers.Contract(pointerAddr, ABI.ERC20, hre.ethers.provider); + pointer = contract.connect(accounts[0].signer); + }); + + it("Should emit a synthetic event upon transfer", async function () { + const res = await executeWasm(cw20Address, { transfer: { recipient: accounts[1].seiAddress, amount: "1" } }); + const blockNumber = parseInt(res["height"], 10); + // look for synthetic event on evm sei_ endpoints + const filter = { + fromBlock: '0x' + blockNumber.toString(16), + toBlock: '0x' + blockNumber.toString(16), + address: pointer.address, + topics: [ethers.id("Transfer(address,address,uint256)")] + }; + const seilogs = await ethers.provider.send('sei_getLogs', [filter]); + expect(seilogs.length).to.equal(1); + }); + + it("sei_getBlockByNumberExcludeTraceFail should not have the synthetic tx", async function () { + // create a synthetic tx + const res = await executeWasm(cw20Address, { transfer: { recipient: accounts[1].seiAddress, amount: "1" } }); + const blockNumber = parseInt(res["height"], 10); + + // Query sei_getBlockByNumber - should have synthetic tx + const seiBlock = await ethers.provider.send('sei_getBlockByNumber', ['0x' + blockNumber.toString(16), false]); + expect(seiBlock.transactions.length).to.equal(1); + + // Query sei_getBlockByNumberExcludeTraceFail - should not have synthetic tx + const seiBlockExcludeTrace = await ethers.provider.send('sei_getBlockByNumberExcludeTraceFail', ['0x' + blockNumber.toString(16), false]); + expect(seiBlockExcludeTrace.transactions.length).to.equal(0); + }); + + it("sei_traceBlockByNumberExcludeTraceFail should not have synthetic tx", async function () { + // create a synthetic tx + const res = await executeWasm(cw20Address, { transfer: { recipient: accounts[1].seiAddress, amount: "1" } }); + const blockNumber = parseInt(res["height"], 10); + const seiBlockExcludeTrace = await ethers.provider.send('sei_traceBlockByNumberExcludeTraceFail', ['0x' + blockNumber.toString(16), {"tracer": "callTracer"}]); + expect(seiBlockExcludeTrace.length).to.equal(0); + }); + + it("sei_traceBlockByHashExcludeTraceFail should not have synthetic tx", async function () { + // create a synthetic tx + const res = await executeWasm(cw20Address, { transfer: { recipient: accounts[1].seiAddress, amount: "1" } }); + const blockNumber = parseInt(res["height"], 10); + // get the block hash + const block = await ethers.provider.send('eth_getBlockByNumber', ['0x' + blockNumber.toString(16), false]); + const blockHash = block.hash; + // check sei_getBlockByHash + const seiBlock = await ethers.provider.send('sei_getBlockByHash', [blockHash, false]); + expect(seiBlock.transactions.length).to.equal(1); + // check sei_traceBlockByHashExcludeTraceFail + const seiBlockExcludeTrace = await ethers.provider.send('sei_traceBlockByHashExcludeTraceFail', [blockHash, {"tracer": "callTracer"}]); + expect(seiBlockExcludeTrace.length).to.equal(0); + }); +}) diff --git a/evmrpc/block.go b/evmrpc/block.go index 28fceae322..0d5d9a7483 100644 --- a/evmrpc/block.go +++ b/evmrpc/block.go @@ -80,7 +80,13 @@ func NewSeiBlockAPI( } func (a *SeiBlockAPI) GetBlockByNumberExcludeTraceFail(ctx context.Context, number rpc.BlockNumber, fullTx bool) (result map[string]interface{}, returnErr error) { - return a.getBlockByNumber(ctx, number, fullTx, a.isPanicTx) + // exclude synthetic txs + return a.getBlockByNumber(ctx, number, fullTx, false, a.isPanicTx) +} + +func (a *SeiBlockAPI) GetBlockByHashExcludeTraceFail(ctx context.Context, blockHash common.Hash, fullTx bool) (result map[string]interface{}, returnErr error) { + // exclude synthetic txs + return a.getBlockByHash(ctx, blockHash, fullTx, false, a.isPanicTx) } func (a *BlockAPI) GetBlockTransactionCountByNumber(ctx context.Context, number rpc.BlockNumber) (result *hexutil.Uint, returnErr error) { @@ -108,18 +114,11 @@ func (a *BlockAPI) GetBlockTransactionCountByHash(ctx context.Context, blockHash } func (a *BlockAPI) GetBlockByHash(ctx context.Context, blockHash common.Hash, fullTx bool) (result map[string]interface{}, returnErr error) { - return a.getBlockByHash(ctx, blockHash, fullTx, nil) + // used for both: eth_ and sei_ namespaces + return a.getBlockByHash(ctx, blockHash, fullTx, a.includeShellReceipts, nil) } -func (a *SeiBlockAPI) GetBlockByHash(ctx context.Context, blockHash common.Hash, fullTx bool) (result map[string]interface{}, returnErr error) { - return a.getBlockByHash(ctx, blockHash, fullTx, nil) -} - -func (a *SeiBlockAPI) GetBlockByHashExcludeTraceFail(ctx context.Context, blockHash common.Hash, fullTx bool) (result map[string]interface{}, returnErr error) { - return a.getBlockByHash(ctx, blockHash, fullTx, a.isPanicTx) -} - -func (a *BlockAPI) getBlockByHash(ctx context.Context, blockHash common.Hash, fullTx bool, isPanicTx func(ctx context.Context, hash common.Hash) (bool, error)) (result map[string]interface{}, returnErr error) { +func (a *BlockAPI) getBlockByHash(ctx context.Context, blockHash common.Hash, fullTx bool, includeSyntheticTxs bool, isPanicTx func(ctx context.Context, hash common.Hash) (bool, error)) (result map[string]interface{}, returnErr error) { startTime := time.Now() defer recordMetrics(fmt.Sprintf("%s_getBlockByHash", a.namespace), a.connectionType, startTime, returnErr == nil) block, err := blockByHashWithRetry(ctx, a.tmClient, blockHash[:], 1) @@ -162,13 +161,14 @@ func (a *BlockAPI) GetBlockByNumber(ctx context.Context, number rpc.BlockNumber, "baseFeePerGas": (*hexutil.Big)(big.NewInt(0)), }, nil } - return a.getBlockByNumber(ctx, number, fullTx, nil) + return a.getBlockByNumber(ctx, number, fullTx, a.includeShellReceipts, nil) } func (a *BlockAPI) getBlockByNumber( ctx context.Context, number rpc.BlockNumber, fullTx bool, + includeSyntheticTxs bool, isPanicTx func(ctx context.Context, hash common.Hash) (bool, error), ) (result map[string]interface{}, returnErr error) { startTime := time.Now() @@ -186,7 +186,7 @@ func (a *BlockAPI) getBlockByNumber( return nil, err } blockBloom := a.keeper.GetBlockBloom(a.ctxProvider(block.Block.Height)) - return EncodeTmBlock(a.ctxProvider(block.Block.Height), block, blockRes, blockBloom, a.keeper, a.txConfig.TxDecoder(), fullTx, a.includeShellReceipts, isPanicTx) + return EncodeTmBlock(a.ctxProvider(block.Block.Height), block, blockRes, blockBloom, a.keeper, a.txConfig.TxDecoder(), fullTx, includeSyntheticTxs, isPanicTx) } func (a *BlockAPI) GetBlockReceipts(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (result []map[string]interface{}, returnErr error) { @@ -267,7 +267,7 @@ func EncodeTmBlock( txDecoder sdk.TxDecoder, fullTx bool, includeSyntheticTxs bool, - isPanicTx func(ctx context.Context, hash common.Hash) (bool, error), + isPanicOrSynthetic func(ctx context.Context, hash common.Hash) (bool, error), ) (map[string]interface{}, error) { number := big.NewInt(block.Block.Height) blockhash := common.HexToHash(block.BlockID.Hash.String()) @@ -296,12 +296,12 @@ func EncodeTmBlock( } ethtx, _ := m.AsTransaction() hash := ethtx.Hash() - if isPanicTx != nil { - isPanic, err := isPanicTx(ctx.Context(), hash) + if isPanicOrSynthetic != nil { + isPanicOrSynthetic, err := isPanicOrSynthetic(ctx.Context(), hash) if err != nil { return nil, fmt.Errorf("failed to check if tx is panic tx: %w", err) } - if isPanic { + if isPanicOrSynthetic { continue } } diff --git a/evmrpc/block_test.go b/evmrpc/block_test.go index b7da70e6b1..d273690aa4 100644 --- a/evmrpc/block_test.go +++ b/evmrpc/block_test.go @@ -36,7 +36,7 @@ func TestGetSeiBlockByHash(t *testing.T) { func TestGetSeiBlockByNumberExcludeTraceFail(t *testing.T) { resObj := sendSeiRequestGood(t, "getBlockByNumberExcludeTraceFail", "0x67", true) - // first tx is not a panic tx, second tx is a panic tx + // first tx is not a panic tx, second tx is a panic tx, third tx is a synthetic tx expectedNumTxs := 1 require.Equal(t, expectedNumTxs, len(resObj["result"].(map[string]interface{})["transactions"].([]interface{}))) } diff --git a/evmrpc/server.go b/evmrpc/server.go index c3d40a93f5..6bf207a969 100644 --- a/evmrpc/server.go +++ b/evmrpc/server.go @@ -33,7 +33,7 @@ func NewEVMHTTPServer( ctxProvider func(int64) sdk.Context, txConfig client.TxConfig, homeDir string, - isPanicTxFunc func(ctx context.Context, hash common.Hash) (bool, error), // optional - for testing + isPanicOrSyntheticTxFunc func(ctx context.Context, hash common.Hash) (bool, error), // used in *ExcludeTraceFail endpoints ) (EVMServer, error) { httpServer := NewHTTPServer(logger, rpc.HTTPTimeouts{ ReadTimeout: config.ReadTimeout, @@ -50,12 +50,12 @@ func NewEVMHTTPServer( txAPI := NewTransactionAPI(tmClient, k, ctxProvider, txConfig, homeDir, ConnectionTypeHTTP) debugAPI := NewDebugAPI(tmClient, k, ctxProvider, txConfig.TxDecoder(), simulateConfig, ConnectionTypeHTTP) - if isPanicTxFunc == nil { - isPanicTxFunc = func(ctx context.Context, hash common.Hash) (bool, error) { - return debugAPI.isPanicTx(ctx, hash) + if isPanicOrSyntheticTxFunc == nil { + isPanicOrSyntheticTxFunc = func(ctx context.Context, hash common.Hash) (bool, error) { + return debugAPI.isPanicOrSyntheticTx(ctx, hash) } } - seiTxAPI := NewSeiTransactionAPI(tmClient, k, ctxProvider, txConfig, homeDir, ConnectionTypeHTTP, isPanicTxFunc) + seiTxAPI := NewSeiTransactionAPI(tmClient, k, ctxProvider, txConfig, homeDir, ConnectionTypeHTTP, isPanicOrSyntheticTxFunc) seiDebugAPI := NewSeiDebugAPI(tmClient, k, ctxProvider, txConfig.TxDecoder(), simulateConfig, ConnectionTypeHTTP) apis := []rpc.API{ @@ -69,7 +69,7 @@ func NewEVMHTTPServer( }, { Namespace: "sei", - Service: NewSeiBlockAPI(tmClient, k, ctxProvider, txConfig, ConnectionTypeHTTP, isPanicTxFunc), + Service: NewSeiBlockAPI(tmClient, k, ctxProvider, txConfig, ConnectionTypeHTTP, isPanicOrSyntheticTxFunc), }, { Namespace: "eth", diff --git a/evmrpc/setup_test.go b/evmrpc/setup_test.go index 565c7972b8..744a9085e5 100644 --- a/evmrpc/setup_test.go +++ b/evmrpc/setup_test.go @@ -64,6 +64,7 @@ var TestEvmTxHash = "0xf02362077ac075a397344172496b28e913ce5294879d811bb0269b3be var TestNonPanicTxHash = "0x566f1c956c74b089643a1e6f880ac65745de0e5cd8cfc3c7482d20a486576219" var TestPanicTxHash = "0x0ea197de8403de9c2e8cf9ec724e43734e9dbd3a8294a09d031acd67914b73e4" +var TestSyntheticTxHash = "0x3ca69dcca32f435b642fcb13e50a1c6934b04c6f901deffa42cec6eb58c40f20" var TestBlockHash = "0x0000000000000000000000000000000000000000000000000000000000000001" var EncodingConfig = app.MakeEncodingConfig() @@ -86,6 +87,7 @@ var multiTxBlockSynthTx *ethtypes.Transaction var DebugTraceTx sdk.Tx var DebugTracePanicTx sdk.Tx var DebugTraceNonPanicTx sdk.Tx +var DebugTraceSyntheticTx sdk.Tx var TxNonEvm sdk.Tx var TxNonEvmWithSyntheticLog sdk.Tx var UnconfirmedTx sdk.Tx @@ -236,6 +238,10 @@ func (c *MockClient) mockBlock(height int64) *coretypes.ResultBlock { bz, _ := Encoder(DebugTracePanicTx) return bz }(), + func() []byte { + bz, _ := Encoder(DebugTraceSyntheticTx) + return bz + }(), } } return res @@ -662,6 +668,15 @@ func generateTxData() { Data: []byte("abc"), ChainID: chainId, }) + debugTraceSyntheticTxBuilder, _ := buildTx(ethtypes.DynamicFeeTx{ + Nonce: 0, + GasFeeCap: big.NewInt(10), + Gas: 25000, + To: &to, + Value: big.NewInt(1000), + Data: []byte("synthetic"), + ChainID: chainId, + }) Tx1 = txBuilder1.GetTx() MultiTxBlockTx1 = txBuilder1_5.GetTx() MultiTxBlockTx2 = txBuilder2.GetTx() @@ -671,6 +686,7 @@ func generateTxData() { DebugTraceTx = debugTraceTxBuilder.GetTx() DebugTracePanicTx = debugTracePanicTxBuilder.GetTx() DebugTraceNonPanicTx = debugTraceNonPanicTxBuilder.GetTx() + DebugTraceSyntheticTx = debugTraceSyntheticTxBuilder.GetTx() TxNonEvm = app.TestTx{} TxNonEvmWithSyntheticLog = app.TestTx{} bloomTx1 := ethtypes.CreateBloom(ethtypes.Receipts{ðtypes.Receipt{Logs: []*ethtypes.Log{{ @@ -876,6 +892,12 @@ func setupLogs() { }}, EffectiveGasPrice: 0, }) + EVMKeeper.MockReceipt(CtxMock, common.HexToHash(TestSyntheticTxHash), &types.Receipt{ + TxType: evmrpc.ShellEVMTxType, + BlockNumber: MockHeight103, + TransactionIndex: 2, + TxHashHex: TestSyntheticTxHash, + }) CtxDebugTrace := Ctx.WithBlockHeight(MockHeight101) EVMKeeper.MockReceipt(CtxDebugTrace, common.HexToHash(DebugTraceHashHex), &types.Receipt{ BlockNumber: MockHeight101, @@ -1114,5 +1136,6 @@ func TestEcho(t *testing.T) { } func isPanicTxFunc(ctx context.Context, hash common.Hash) (bool, error) { - return hash == common.HexToHash(TestPanicTxHash), nil + result := hash == common.HexToHash(TestPanicTxHash) || hash == common.HexToHash(TestSyntheticTxHash) + return result, nil } diff --git a/evmrpc/tracers.go b/evmrpc/tracers.go index 52d8505796..cb3b7e7dad 100644 --- a/evmrpc/tracers.go +++ b/evmrpc/tracers.go @@ -110,7 +110,8 @@ func (api *SeiDebugAPI) TraceBlockByHashExcludeTraceFail(ctx context.Context, ha return finalTraces, nil } -func (api *DebugAPI) isPanicTx(ctx context.Context, hash common.Hash) (isPanic bool, err error) { +// isPanicOrSyntheticTx returns true if the tx is a panic tx or if it is a synthetic tx. Used in the *ExcludeTraceFail endpoints. +func (api *DebugAPI) isPanicOrSyntheticTx(ctx context.Context, hash common.Hash) (isPanic bool, err error) { sdkctx := api.ctxProvider(LatestCtxHeight) receipt, err := api.keeper.GetReceipt(sdkctx, hash) if err != nil { @@ -131,17 +132,25 @@ func (api *DebugAPI) isPanicTx(ctx context.Context, hash common.Hash) (isPanic b return false, err } + found := false result := false for _, trace := range tracersResult { if trace.TxHash == hash { + found = true result = len(trace.Error) > 0 } + // for each tx, add to cache to avoid re-tracing if len(trace.Error) > 0 { api.isPanicCache.Add(trace.TxHash, true) } else { api.isPanicCache.Add(trace.TxHash, false) } } + + if !found { // likely a synthetic tx + return true, nil + } + return result, nil } diff --git a/evmrpc/tracers_test.go b/evmrpc/tracers_test.go index a0788780d7..4ec88c65c5 100644 --- a/evmrpc/tracers_test.go +++ b/evmrpc/tracers_test.go @@ -93,10 +93,10 @@ func TestTraceBlockByNumberExcludeTraceFail(t *testing.T) { blockNumber := fmt.Sprintf("%#x", MockHeight103) seiResObj := sendRequestGoodWithNamespace(t, "sei", "traceBlockByNumberExcludeTraceFail", blockNumber, args) result := seiResObj["result"].([]interface{}) - // sei_traceBlockByNumberExcludeTraceFail returns 1 trace, and removes the panic tx + // sei_traceBlockByNumberExcludeTraceFail returns 1 trace, and removes the panic tx and synthetic tx require.Equal(t, 1, len(result)) ethResObj := sendRequestGoodWithNamespace(t, "debug", "traceBlockByNumber", blockNumber, args) - // eth_traceBlockByNumber returns 2 traces, including the panic tx + // eth_traceBlockByNumber returns 2 traces, including the panic tx, but excludes the synthetic tx require.Equal(t, 2, len(ethResObj["result"].([]interface{}))) } diff --git a/integration_test/evm_module/scripts/evm_tests.sh b/integration_test/evm_module/scripts/evm_tests.sh index 32b647d495..36c3632ebf 100755 --- a/integration_test/evm_module/scripts/evm_tests.sh +++ b/integration_test/evm_module/scripts/evm_tests.sh @@ -6,4 +6,5 @@ cd contracts npm ci npx hardhat test --network seilocal test/EVMCompatabilityTest.js npx hardhat test --network seilocal test/EVMPrecompileTest.js +npx hardhat test --network seilocal test/SeiEndpointsTest.js npx hardhat test --network seilocal test/AssociateTest.js \ No newline at end of file