From 64beba78220c22e8d45c9aa58a85898342758a64 Mon Sep 17 00:00:00 2001 From: Unique-Divine Date: Wed, 11 Sep 2024 11:55:23 +0900 Subject: [PATCH 01/16] chore(github): Automate labeleing and add-to-project for GH issues/tickets --- .github/issue-labeler-config.yml | 3 +++ .github/workflows/e2e-wasm.yml | 1 - .github/workflows/gh-issues.yml | 36 ++++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 .github/issue-labeler-config.yml create mode 100644 .github/workflows/gh-issues.yml diff --git a/.github/issue-labeler-config.yml b/.github/issue-labeler-config.yml new file mode 100644 index 000000000..1d7046b6a --- /dev/null +++ b/.github/issue-labeler-config.yml @@ -0,0 +1,3 @@ +# Adds the "S-triage" label ot any issue that gets opened. +S-triage: + - '/.*/' diff --git a/.github/workflows/e2e-wasm.yml b/.github/workflows/e2e-wasm.yml index 1a38958ce..7d5ab19be 100644 --- a/.github/workflows/e2e-wasm.yml +++ b/.github/workflows/e2e-wasm.yml @@ -1,4 +1,3 @@ ---- name: CosmWasm e2e tests on: diff --git a/.github/workflows/gh-issues.yml b/.github/workflows/gh-issues.yml new file mode 100644 index 000000000..95154e494 --- /dev/null +++ b/.github/workflows/gh-issues.yml @@ -0,0 +1,36 @@ +name: "Auto-add GH issues to project" +# Add all issues opened to the issue board for triage and assignment + +on: + issues: + types: ["opened", "labeled"] + +permissions: + issues: write + contents: read + +jobs: + # https://github.com/actions/add-to-project + add-to-project: + name: "Add GH ticket to project" + runs-on: ubuntu-latest + steps: + - uses: actions/add-to-project@v1.0.2 + with: + project-url: https://github.com/orgs/NibiruChain/projects/8 + github-token: ${{ secrets.NIBIRU_PM }} + + label-triage: + name: "Add GH ticket to project" + runs-on: ubuntu-latest + # The action comes from the "Activty types" for the "issues" webhook event + # https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#issues + if: "github.event.action == 'opened'" + steps: + - uses: github/issue-labeler@v3.4 + if: join(github.event.issue.labels) == '' + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + configuration-path: ".github/issue-labeler-config.yml" + enable-versioned-regex: 0 + not-before: "2024-05-01T00:00:00Z" From 6ba79bbfd890dbc3cdf7e99781612e65c9d63173 Mon Sep 17 00:00:00 2001 From: Oleg Nikonychev Date: Thu, 12 Sep 2024 04:45:03 +0400 Subject: [PATCH 02/16] fix(evm): debug calls with custom tracer and tracer options (#2031) --- CHANGELOG.md | 1 + app/app.go | 3 + e2e/evm/test/debug_queries.test.ts | 37 ++- proto/eth/evm/v1/evm.proto | 9 +- x/evm/evm.pb.go | 354 +++++++++++++++++++++-------- x/evm/keeper/grpc_query.go | 15 +- 6 files changed, 314 insertions(+), 105 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9df469730..ba5b6877a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -117,6 +117,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#2020](https://github.com/NibiruChain/nibiru/pull/2020) - test(evm): e2e tests for debug namespace - [#2022](https://github.com/NibiruChain/nibiru/pull/2022) - feat(evm): debug_traceCall method implemented - [#2023](https://github.com/NibiruChain/nibiru/pull/2023) - fix(evm)!: adjusted generation and parsing of the block bloom events +- [#2031](https://github.com/NibiruChain/nibiru/pull/2031) - fix(evm): debug calls with custom tracer and tracer options #### Dapp modules: perp, spot, oracle, etc diff --git a/app/app.go b/app/app.go index 72e728927..9230a3553 100644 --- a/app/app.go +++ b/app/app.go @@ -52,6 +52,9 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/rakyll/statik/fs" "github.com/spf13/cast" + + // force call init() of the geth tracers + _ "github.com/ethereum/go-ethereum/eth/tracers/native" ) const ( diff --git a/e2e/evm/test/debug_queries.test.ts b/e2e/evm/test/debug_queries.test.ts index 934767b2c..c01723d90 100644 --- a/e2e/evm/test/debug_queries.test.ts +++ b/e2e/evm/test/debug_queries.test.ts @@ -32,6 +32,11 @@ describe("debug queries", () => { it("debug_traceBlockByNumber", async () => { const traceResult = await provider.send("debug_traceBlockByNumber", [ blockNumber, + { + tracer: "callTracer", + timeout: "3000s", + tracerConfig: { onlyTopCall: false }, + }, ]) expectTrace(traceResult) }) @@ -40,13 +45,24 @@ describe("debug queries", () => { it("debug_traceBlockByHash", async () => { const traceResult = await provider.send("debug_traceBlockByHash", [ blockHash, + { + tracer: "callTracer", + timeout: "3000s", + tracerConfig: { onlyTopCall: false }, + }, ]) expectTrace(traceResult) }) - // TODO: impl in EVM: remove skip - it.skip("debug_traceTransaction", async () => { - const traceResult = await provider.send("debug_traceTransaction", [txHash]) + it("debug_traceTransaction", async () => { + const traceResult = await provider.send("debug_traceTransaction", [ + txHash, + { + tracer: "callTracer", + timeout: "3000s", + tracerConfig: { onlyTopCall: false }, + }, + ]) expectTrace([{ result: traceResult }]) }) @@ -61,7 +77,11 @@ describe("debug queries", () => { const traceResult = await provider.send("debug_traceCall", [ tx, "latest", - {}, + { + tracer: "callTracer", + timeout: "3000s", + tracerConfig: { onlyTopCall: false }, + }, ]) expectTrace([{ result: traceResult }]) }) @@ -90,8 +110,11 @@ const expectTrace = (traceResult: any[]) => { expect(traceResult.length).toBeGreaterThan(0) const trace = traceResult[0]["result"] - expect(trace).toHaveProperty("failed", false) + expect(trace).toHaveProperty("from") + expect(trace).toHaveProperty("to") expect(trace).toHaveProperty("gas") - expect(trace).toHaveProperty("returnValue") - expect(trace).toHaveProperty("structLogs") + expect(trace).toHaveProperty("gasUsed") + expect(trace).toHaveProperty("input") + expect(trace).toHaveProperty("output") + expect(trace).toHaveProperty("type", "CALL") } diff --git a/proto/eth/evm/v1/evm.proto b/proto/eth/evm/v1/evm.proto index ce50798a7..7be577a1c 100644 --- a/proto/eth/evm/v1/evm.proto +++ b/proto/eth/evm/v1/evm.proto @@ -145,6 +145,11 @@ message AccessTuple { repeated string storage_keys = 2 [ (gogoproto.jsontag) = "storageKeys" ]; } +// TracerConfig stores additional tracer args. For geth it's only one attr: onlyTopCall +message TracerConfig { + bool only_top_call = 1 [ (gogoproto.jsontag) = "onlyTopCall" ]; +} + // TraceConfig holds extra parameters to trace functions. message TraceConfig { // DEPRECATED: DisableMemory and DisableReturnData have been renamed to @@ -173,6 +178,6 @@ message TraceConfig { bool enable_memory = 11 [ (gogoproto.jsontag) = "enableMemory" ]; // enable_return_data switches the capture of return data bool enable_return_data = 12 [ (gogoproto.jsontag) = "enableReturnData" ]; - // tracer_json_config configures the tracer using a JSON string - string tracer_json_config = 13 [ (gogoproto.jsontag) = "tracerConfig" ]; + // tracer_config configures the tracer options + TracerConfig tracer_config = 13 [ (gogoproto.jsontag) = "tracerConfig" ]; } diff --git a/x/evm/evm.pb.go b/x/evm/evm.pb.go index c8e6bebf3..13c357492 100644 --- a/x/evm/evm.pb.go +++ b/x/evm/evm.pb.go @@ -495,6 +495,51 @@ func (m *AccessTuple) XXX_DiscardUnknown() { var xxx_messageInfo_AccessTuple proto.InternalMessageInfo +// TracerConfig stores additional tracer args. For geth it's only one attr: onlyTopCall +type TracerConfig struct { + OnlyTopCall bool `protobuf:"varint,1,opt,name=only_top_call,json=onlyTopCall,proto3" json:"onlyTopCall"` +} + +func (m *TracerConfig) Reset() { *m = TracerConfig{} } +func (m *TracerConfig) String() string { return proto.CompactTextString(m) } +func (*TracerConfig) ProtoMessage() {} +func (*TracerConfig) Descriptor() ([]byte, []int) { + return fileDescriptor_98abbdadb327b7d0, []int{7} +} +func (m *TracerConfig) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *TracerConfig) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_TracerConfig.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *TracerConfig) XXX_Merge(src proto.Message) { + xxx_messageInfo_TracerConfig.Merge(m, src) +} +func (m *TracerConfig) XXX_Size() int { + return m.Size() +} +func (m *TracerConfig) XXX_DiscardUnknown() { + xxx_messageInfo_TracerConfig.DiscardUnknown(m) +} + +var xxx_messageInfo_TracerConfig proto.InternalMessageInfo + +func (m *TracerConfig) GetOnlyTopCall() bool { + if m != nil { + return m.OnlyTopCall + } + return false +} + // TraceConfig holds extra parameters to trace functions. type TraceConfig struct { // tracer is a custom javascript tracer @@ -516,15 +561,15 @@ type TraceConfig struct { EnableMemory bool `protobuf:"varint,11,opt,name=enable_memory,json=enableMemory,proto3" json:"enableMemory"` // enable_return_data switches the capture of return data EnableReturnData bool `protobuf:"varint,12,opt,name=enable_return_data,json=enableReturnData,proto3" json:"enableReturnData"` - // tracer_json_config configures the tracer using a JSON string - TracerJsonConfig string `protobuf:"bytes,13,opt,name=tracer_json_config,json=tracerJsonConfig,proto3" json:"tracerConfig"` + // tracer_config configures the tracer options + TracerConfig *TracerConfig `protobuf:"bytes,13,opt,name=tracer_config,json=tracerConfig,proto3" json:"tracerConfig"` } func (m *TraceConfig) Reset() { *m = TraceConfig{} } func (m *TraceConfig) String() string { return proto.CompactTextString(m) } func (*TraceConfig) ProtoMessage() {} func (*TraceConfig) Descriptor() ([]byte, []int) { - return fileDescriptor_98abbdadb327b7d0, []int{7} + return fileDescriptor_98abbdadb327b7d0, []int{8} } func (m *TraceConfig) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -616,11 +661,11 @@ func (m *TraceConfig) GetEnableReturnData() bool { return false } -func (m *TraceConfig) GetTracerJsonConfig() string { +func (m *TraceConfig) GetTracerConfig() *TracerConfig { if m != nil { - return m.TracerJsonConfig + return m.TracerConfig } - return "" + return nil } func init() { @@ -631,84 +676,87 @@ func init() { proto.RegisterType((*Log)(nil), "eth.evm.v1.Log") proto.RegisterType((*TxResult)(nil), "eth.evm.v1.TxResult") proto.RegisterType((*AccessTuple)(nil), "eth.evm.v1.AccessTuple") + proto.RegisterType((*TracerConfig)(nil), "eth.evm.v1.TracerConfig") proto.RegisterType((*TraceConfig)(nil), "eth.evm.v1.TraceConfig") } func init() { proto.RegisterFile("eth/evm/v1/evm.proto", fileDescriptor_98abbdadb327b7d0) } var fileDescriptor_98abbdadb327b7d0 = []byte{ - // 1123 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x55, 0xcd, 0x6e, 0xdb, 0x46, - 0x10, 0xb6, 0x2c, 0xca, 0xa6, 0x56, 0x4a, 0xc4, 0xac, 0x9d, 0x94, 0x4d, 0x10, 0x51, 0x60, 0x81, - 0x42, 0x05, 0x02, 0xa9, 0x51, 0x91, 0x1e, 0x52, 0xa0, 0xa8, 0xe5, 0xd8, 0xa8, 0x55, 0x3b, 0x08, - 0x36, 0x4a, 0x0f, 0xbd, 0x10, 0x2b, 0x72, 0x4c, 0xb1, 0x22, 0xb9, 0x06, 0x77, 0xa9, 0xca, 0x0f, - 0x50, 0xa0, 0xc7, 0x3e, 0x42, 0xee, 0x7d, 0x91, 0xa0, 0xa7, 0x00, 0xbd, 0x14, 0x3d, 0x10, 0x85, - 0x73, 0x69, 0x75, 0xf4, 0x13, 0x14, 0xbb, 0x4b, 0x59, 0x8e, 0x0b, 0xb4, 0x27, 0xce, 0xf7, 0xcd, - 0xce, 0x0f, 0x67, 0xbe, 0x25, 0xd1, 0x2e, 0x88, 0x69, 0x1f, 0xe6, 0x49, 0x7f, 0xfe, 0x58, 0x3e, - 0x7a, 0x67, 0x19, 0x13, 0x0c, 0x23, 0x10, 0xd3, 0x9e, 0x84, 0xf3, 0xc7, 0xf7, 0x77, 0x43, 0x16, - 0x32, 0x45, 0xf7, 0xa5, 0xa5, 0x4f, 0xb8, 0xbf, 0x54, 0x90, 0x79, 0x98, 0xa7, 0x63, 0x36, 0x83, - 0x14, 0xbf, 0x42, 0x08, 0x32, 0x7f, 0xf0, 0xa9, 0x47, 0x83, 0x20, 0xb3, 0x2b, 0x9d, 0x4a, 0xb7, - 0x3e, 0xfc, 0xfc, 0x4d, 0xe1, 0x6c, 0xfc, 0x51, 0x38, 0xbd, 0x30, 0x12, 0xd3, 0x7c, 0xd2, 0xf3, - 0x59, 0xd2, 0x7f, 0x1e, 0x4d, 0xa2, 0x2c, 0xdf, 0x9f, 0xd2, 0x28, 0xed, 0xa7, 0xca, 0xee, 0xcf, - 0x07, 0x7d, 0x59, 0xeb, 0xe0, 0xe8, 0xc5, 0x93, 0x27, 0x7b, 0x41, 0x90, 0x91, 0xba, 0xca, 0x24, - 0x4d, 0xfc, 0x10, 0xa1, 0x09, 0x4d, 0x67, 0x5e, 0x00, 0x29, 0x4b, 0xec, 0x4d, 0x99, 0x96, 0xd4, - 0x25, 0xf3, 0x4c, 0x12, 0xf8, 0x13, 0x74, 0x27, 0xe2, 0x5e, 0x42, 0x03, 0xf0, 0x4e, 0x33, 0x96, - 0x78, 0x3e, 0x8b, 0x52, 0xbb, 0xda, 0xa9, 0x74, 0x4d, 0x72, 0x3b, 0xe2, 0x27, 0x34, 0x80, 0xc3, - 0x8c, 0x25, 0xfb, 0x2c, 0x4a, 0xdd, 0xdf, 0x36, 0xd1, 0xd6, 0x0b, 0x9a, 0xd1, 0x84, 0xe3, 0xc7, - 0xa8, 0x0e, 0xf3, 0xa4, 0xcc, 0xa9, 0x5b, 0xdd, 0xbd, 0x2c, 0x1c, 0xeb, 0x9c, 0x26, 0xf1, 0x53, - 0xf7, 0xca, 0xe5, 0x12, 0x13, 0xe6, 0x89, 0x2e, 0xb4, 0x87, 0x10, 0x2c, 0x44, 0x46, 0x3d, 0x88, - 0xce, 0xb8, 0x6d, 0x74, 0xaa, 0xdd, 0xea, 0xd0, 0xbd, 0x28, 0x9c, 0xfa, 0x81, 0x64, 0x0f, 0x8e, - 0x5e, 0xf0, 0xcb, 0xc2, 0xb9, 0x53, 0x26, 0xb8, 0x3a, 0xe8, 0x92, 0xba, 0x02, 0x07, 0xd1, 0x19, - 0xc7, 0x03, 0x74, 0x97, 0xc6, 0x31, 0xfb, 0xc1, 0xcb, 0x53, 0x39, 0x3f, 0xf0, 0x05, 0x04, 0x9e, - 0x58, 0x70, 0x7b, 0x4b, 0xf5, 0xbb, 0xa3, 0x9c, 0xaf, 0xd6, 0xbe, 0xf1, 0x42, 0xc6, 0x34, 0x65, - 0x3b, 0xfe, 0x94, 0xa6, 0x29, 0xc4, 0xdc, 0x36, 0x3b, 0xd5, 0x6e, 0x7d, 0xd8, 0xba, 0x28, 0x9c, - 0xc6, 0xc1, 0xb7, 0x27, 0xfb, 0x25, 0x4d, 0x1a, 0x30, 0x4f, 0x56, 0x00, 0x9f, 0xa0, 0x1d, 0x3f, - 0x03, 0x2a, 0xc0, 0x3b, 0xcd, 0x53, 0x21, 0x97, 0xe3, 0x9d, 0x02, 0xd8, 0x75, 0xf5, 0x9e, 0x0f, - 0xcb, 0x95, 0xdc, 0xf5, 0x19, 0x4f, 0x18, 0xe7, 0xc1, 0xac, 0x17, 0xb1, 0x7e, 0x42, 0xc5, 0xb4, - 0x77, 0x94, 0x0a, 0x72, 0x47, 0x47, 0x1e, 0x96, 0x81, 0x87, 0x00, 0x4f, 0x8d, 0xbf, 0x5e, 0x3b, - 0x95, 0x91, 0x61, 0x6e, 0x5a, 0xd5, 0x91, 0x61, 0x56, 0x2d, 0x63, 0x64, 0x98, 0x35, 0x6b, 0x6b, - 0x64, 0x98, 0xdb, 0x96, 0xe9, 0xf6, 0x51, 0xed, 0xa5, 0xa0, 0x02, 0xb0, 0x85, 0xaa, 0x33, 0x38, - 0xd7, 0xd3, 0x24, 0xd2, 0xc4, 0xbb, 0xa8, 0x36, 0xa7, 0x71, 0x0e, 0xe5, 0xd6, 0x34, 0x70, 0x47, - 0xa8, 0x35, 0xce, 0x68, 0xca, 0xa9, 0x2f, 0x22, 0x96, 0x1e, 0xb3, 0x90, 0x63, 0x8c, 0x8c, 0x29, - 0xe5, 0xd3, 0x32, 0x56, 0xd9, 0xf8, 0x23, 0x64, 0xc4, 0x2c, 0xe4, 0xf6, 0x66, 0xa7, 0xda, 0x6d, - 0x0c, 0x5a, 0xbd, 0xb5, 0x18, 0x7b, 0xc7, 0x2c, 0x24, 0xca, 0xe9, 0xfe, 0xba, 0x89, 0xaa, 0xc7, - 0x2c, 0xc4, 0x36, 0xda, 0x96, 0xaa, 0x03, 0xce, 0xcb, 0x1c, 0x2b, 0x88, 0xef, 0xa1, 0x2d, 0xc1, - 0xce, 0x22, 0x5f, 0x27, 0xaa, 0x93, 0x12, 0xc9, 0x92, 0x01, 0x15, 0x54, 0x49, 0xa5, 0x49, 0x94, - 0x2d, 0x67, 0x3d, 0x89, 0x99, 0x3f, 0xf3, 0xd2, 0x3c, 0x99, 0x40, 0x66, 0x1b, 0x9d, 0x4a, 0xd7, - 0x18, 0xb6, 0x96, 0x85, 0xd3, 0x50, 0xfc, 0x73, 0x45, 0x93, 0xeb, 0x00, 0x3f, 0x42, 0xdb, 0x62, - 0xe1, 0xa9, 0xee, 0x6b, 0x6a, 0xbe, 0x3b, 0xcb, 0xc2, 0x69, 0x89, 0xf5, 0x0b, 0x7e, 0x4d, 0xf9, - 0x94, 0x6c, 0x89, 0x85, 0x7c, 0xe2, 0x3e, 0x32, 0xc5, 0xc2, 0x8b, 0xd2, 0x00, 0x16, 0x6a, 0xe9, - 0xc6, 0x70, 0x77, 0x59, 0x38, 0xd6, 0xb5, 0xe3, 0x47, 0xd2, 0x47, 0xb6, 0xc5, 0x42, 0x19, 0xf8, - 0x11, 0x42, 0xba, 0x25, 0x55, 0x61, 0x5b, 0x55, 0xb8, 0xb5, 0x2c, 0x9c, 0xba, 0x62, 0x55, 0xee, - 0xb5, 0x89, 0x5d, 0x54, 0xd3, 0xb9, 0x4d, 0x95, 0xbb, 0xb9, 0x2c, 0x1c, 0x33, 0x66, 0xa1, 0xce, - 0xa9, 0x5d, 0x72, 0x54, 0x19, 0x24, 0x6c, 0x0e, 0x81, 0x12, 0x84, 0x49, 0x56, 0xd0, 0xfd, 0x71, - 0x13, 0x99, 0xe3, 0x05, 0x01, 0x9e, 0xc7, 0x02, 0x1f, 0x22, 0xcb, 0x67, 0xa9, 0xc8, 0xa8, 0x2f, - 0xbc, 0xf7, 0x46, 0x3b, 0x7c, 0x70, 0x59, 0x38, 0x1f, 0x68, 0x9d, 0xdf, 0x3c, 0xe1, 0x92, 0xd6, - 0x8a, 0xda, 0x2b, 0xe7, 0xbf, 0x8b, 0x6a, 0x93, 0x98, 0x95, 0x37, 0xb7, 0x49, 0x34, 0xc0, 0xc7, - 0x6a, 0x6a, 0x6a, 0xbf, 0x72, 0x01, 0x8d, 0xc1, 0x83, 0xeb, 0xfb, 0xbd, 0x21, 0x8f, 0xe1, 0x3d, - 0x29, 0xd9, 0xcb, 0xc2, 0xb9, 0xad, 0xab, 0x96, 0x91, 0xae, 0x9c, 0xaa, 0x92, 0x8f, 0x85, 0xaa, - 0x19, 0x08, 0xb5, 0xae, 0x26, 0x91, 0x26, 0xbe, 0x8f, 0xcc, 0x0c, 0xe6, 0x90, 0x09, 0x08, 0xd4, - 0x5a, 0x4c, 0x72, 0x85, 0xf1, 0x87, 0xc8, 0x0c, 0x29, 0xf7, 0x72, 0x0e, 0x81, 0xde, 0x01, 0xd9, - 0x0e, 0x29, 0x7f, 0xc5, 0x21, 0x78, 0x6a, 0xfc, 0xf4, 0xda, 0xd9, 0x70, 0x29, 0x6a, 0xec, 0xf9, - 0x3e, 0x70, 0x3e, 0xce, 0xcf, 0x62, 0xf8, 0x0f, 0x6d, 0x0d, 0x50, 0x93, 0x0b, 0x96, 0xd1, 0x10, - 0xbc, 0x19, 0x9c, 0x97, 0x0a, 0xd3, 0x7a, 0x29, 0xf9, 0x6f, 0xe0, 0x9c, 0x93, 0xeb, 0xa0, 0x2c, - 0xf1, 0x77, 0x15, 0x35, 0xc6, 0x19, 0xf5, 0x61, 0x9f, 0xa5, 0xa7, 0x51, 0xa8, 0x54, 0x2a, 0x61, - 0xf9, 0xdd, 0x24, 0x25, 0x92, 0xb5, 0x45, 0x94, 0x00, 0xcb, 0x45, 0x79, 0x87, 0x56, 0x50, 0x46, - 0x64, 0x00, 0x0b, 0xf0, 0xd5, 0x00, 0x0d, 0x52, 0x22, 0xfc, 0x04, 0xdd, 0x0a, 0x22, 0x4e, 0x27, - 0x31, 0x78, 0x5c, 0x50, 0x7f, 0xa6, 0x5f, 0x7f, 0x68, 0x2d, 0x0b, 0xa7, 0x59, 0x3a, 0x5e, 0x4a, - 0x9e, 0xbc, 0x87, 0xf0, 0x17, 0xa8, 0xb5, 0x0e, 0x53, 0xdd, 0xea, 0x8f, 0xd2, 0x10, 0x2f, 0x0b, - 0xe7, 0xf6, 0xd5, 0x51, 0xe5, 0x21, 0x37, 0xb0, 0xdc, 0x71, 0x00, 0x93, 0x3c, 0x54, 0xb2, 0x33, - 0x89, 0x06, 0x92, 0x8d, 0xa3, 0x24, 0x12, 0x4a, 0x66, 0x35, 0xa2, 0x81, 0xec, 0x0f, 0x52, 0x55, - 0x27, 0x81, 0x84, 0x65, 0xe7, 0x76, 0x63, 0xdd, 0x9f, 0x76, 0x9c, 0x28, 0x9e, 0xbc, 0x87, 0xf0, - 0x10, 0xe1, 0x32, 0x2c, 0x03, 0x91, 0x67, 0xa9, 0xa7, 0x2e, 0x6f, 0x53, 0xc5, 0xaa, 0x2b, 0xa4, - 0xbd, 0x44, 0x39, 0x9f, 0x51, 0x41, 0xc9, 0xbf, 0x18, 0xfc, 0x25, 0xc2, 0x7a, 0xac, 0xde, 0xf7, - 0x9c, 0xa5, 0x9e, 0xaf, 0x46, 0x6f, 0xdf, 0x52, 0xa2, 0x56, 0xf5, 0xb5, 0x57, 0xaf, 0x84, 0x58, - 0x1a, 0x8d, 0x38, 0x4b, 0x35, 0x33, 0x32, 0x4c, 0xc3, 0xaa, 0xe9, 0xaf, 0xde, 0xc8, 0x30, 0x91, - 0xd5, 0xb8, 0x1a, 0x44, 0xf9, 0x2e, 0x64, 0x67, 0x85, 0xaf, 0x35, 0x39, 0xfc, 0xea, 0xcd, 0x45, - 0xbb, 0xf2, 0xf6, 0xa2, 0x5d, 0xf9, 0xf3, 0xa2, 0x5d, 0xf9, 0xf9, 0x5d, 0x7b, 0xe3, 0xed, 0xbb, - 0xf6, 0xc6, 0xef, 0xef, 0xda, 0x1b, 0xdf, 0x7d, 0xfc, 0xbf, 0x7f, 0xc5, 0x85, 0xfc, 0x1d, 0x4f, - 0xb6, 0xd4, 0xdf, 0xf6, 0xb3, 0x7f, 0x02, 0x00, 0x00, 0xff, 0xff, 0xa2, 0xa8, 0xe3, 0xf2, 0xa7, - 0x07, 0x00, 0x00, + // 1153 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x55, 0x3f, 0x6f, 0xdb, 0x46, + 0x14, 0xb7, 0x2c, 0xca, 0xa6, 0x4e, 0x72, 0xa4, 0x9c, 0x9d, 0x94, 0x4d, 0x10, 0xd3, 0x60, 0x81, + 0xc2, 0x05, 0x02, 0xa9, 0x71, 0x90, 0x0e, 0xe9, 0x52, 0xcb, 0xb1, 0x51, 0xab, 0x76, 0x1a, 0x5c, + 0x94, 0x0e, 0x5d, 0x88, 0x13, 0xf9, 0x4c, 0x11, 0x22, 0x79, 0xc2, 0xdd, 0x51, 0x95, 0x3f, 0x40, + 0x81, 0x8e, 0xfd, 0x08, 0xd9, 0xfb, 0x45, 0x82, 0x4e, 0x01, 0xba, 0x14, 0x1d, 0x88, 0xc2, 0x59, + 0x0a, 0x8d, 0x1e, 0x3b, 0x15, 0x77, 0x47, 0x5b, 0xb2, 0x0b, 0xb4, 0x13, 0xdf, 0xef, 0xf7, 0xee, + 0xfd, 0xb9, 0xf7, 0x7e, 0x24, 0xd1, 0x16, 0xc8, 0x51, 0x17, 0xa6, 0x69, 0x77, 0xfa, 0x44, 0x3d, + 0x3a, 0x13, 0xce, 0x24, 0xc3, 0x08, 0xe4, 0xa8, 0xa3, 0xe0, 0xf4, 0xc9, 0x83, 0xad, 0x88, 0x45, + 0x4c, 0xd3, 0x5d, 0x65, 0x99, 0x13, 0xde, 0x2f, 0x15, 0x64, 0x1f, 0xe5, 0xd9, 0x80, 0x8d, 0x21, + 0xc3, 0x6f, 0x10, 0x02, 0x1e, 0xec, 0x7d, 0xee, 0xd3, 0x30, 0xe4, 0x4e, 0x65, 0xa7, 0xb2, 0x5b, + 0xef, 0x7d, 0xf1, 0xae, 0x70, 0x57, 0xfe, 0x28, 0xdc, 0x4e, 0x14, 0xcb, 0x51, 0x3e, 0xec, 0x04, + 0x2c, 0xed, 0xbe, 0x8c, 0x87, 0x31, 0xcf, 0x0f, 0x46, 0x34, 0xce, 0xba, 0x99, 0xb6, 0xbb, 0xd3, + 0xbd, 0xae, 0xaa, 0x75, 0x78, 0xfc, 0xea, 0xd9, 0xb3, 0xfd, 0x30, 0xe4, 0xa4, 0xae, 0x33, 0x29, + 0x13, 0x3f, 0x42, 0x68, 0x48, 0xb3, 0xb1, 0x1f, 0x42, 0xc6, 0x52, 0x67, 0x55, 0xa5, 0x25, 0x75, + 0xc5, 0xbc, 0x50, 0x04, 0xfe, 0x0c, 0xdd, 0x8d, 0x85, 0x9f, 0xd2, 0x10, 0xfc, 0x33, 0xce, 0x52, + 0x3f, 0x60, 0x71, 0xe6, 0x54, 0x77, 0x2a, 0xbb, 0x36, 0xb9, 0x13, 0x8b, 0x53, 0x1a, 0xc2, 0x11, + 0x67, 0xe9, 0x01, 0x8b, 0x33, 0xef, 0xb7, 0x55, 0xb4, 0xf6, 0x8a, 0x72, 0x9a, 0x0a, 0xfc, 0x04, + 0xd5, 0x61, 0x9a, 0x96, 0x39, 0x4d, 0xab, 0x5b, 0x97, 0x85, 0xdb, 0x3e, 0xa7, 0x69, 0xf2, 0xdc, + 0xbb, 0x76, 0x79, 0xc4, 0x86, 0x69, 0x6a, 0x0a, 0xed, 0x23, 0x04, 0x33, 0xc9, 0xa9, 0x0f, 0xf1, + 0x44, 0x38, 0xd6, 0x4e, 0x75, 0xb7, 0xda, 0xf3, 0x2e, 0x0a, 0xb7, 0x7e, 0xa8, 0xd8, 0xc3, 0xe3, + 0x57, 0xe2, 0xb2, 0x70, 0xef, 0x96, 0x09, 0xae, 0x0f, 0x7a, 0xa4, 0xae, 0xc1, 0x61, 0x3c, 0x11, + 0x78, 0x0f, 0xdd, 0xa3, 0x49, 0xc2, 0x7e, 0xf0, 0xf3, 0x4c, 0xcd, 0x0f, 0x02, 0x09, 0xa1, 0x2f, + 0x67, 0xc2, 0x59, 0xd3, 0xfd, 0x6e, 0x6a, 0xe7, 0x9b, 0x85, 0x6f, 0x30, 0x53, 0x31, 0x4d, 0xd5, + 0x4e, 0x30, 0xa2, 0x59, 0x06, 0x89, 0x70, 0xec, 0x9d, 0xea, 0x6e, 0xbd, 0xd7, 0xba, 0x28, 0xdc, + 0xc6, 0xe1, 0x77, 0xa7, 0x07, 0x25, 0x4d, 0x1a, 0x30, 0x4d, 0xaf, 0x00, 0x3e, 0x45, 0x9b, 0x01, + 0x07, 0x2a, 0xc1, 0x3f, 0xcb, 0x33, 0xa9, 0x96, 0xe3, 0x9f, 0x01, 0x38, 0x75, 0x7d, 0xcf, 0x47, + 0xe5, 0x4a, 0xee, 0x05, 0x4c, 0xa4, 0x4c, 0x88, 0x70, 0xdc, 0x89, 0x59, 0x37, 0xa5, 0x72, 0xd4, + 0x39, 0xce, 0x24, 0xb9, 0x6b, 0x22, 0x8f, 0xca, 0xc0, 0x23, 0x80, 0xe7, 0xd6, 0x5f, 0x6f, 0xdd, + 0x4a, 0xdf, 0xb2, 0x57, 0xdb, 0xd5, 0xbe, 0x65, 0x57, 0xdb, 0x56, 0xdf, 0xb2, 0x6b, 0xed, 0xb5, + 0xbe, 0x65, 0xaf, 0xb7, 0x6d, 0xaf, 0x8b, 0x6a, 0xaf, 0x25, 0x95, 0x80, 0xdb, 0xa8, 0x3a, 0x86, + 0x73, 0x33, 0x4d, 0xa2, 0x4c, 0xbc, 0x85, 0x6a, 0x53, 0x9a, 0xe4, 0x50, 0x6e, 0xcd, 0x00, 0xaf, + 0x8f, 0x5a, 0x03, 0x4e, 0x33, 0x41, 0x03, 0x19, 0xb3, 0xec, 0x84, 0x45, 0x02, 0x63, 0x64, 0x8d, + 0xa8, 0x18, 0x95, 0xb1, 0xda, 0xc6, 0x9f, 0x20, 0x2b, 0x61, 0x91, 0x70, 0x56, 0x77, 0xaa, 0xbb, + 0x8d, 0xbd, 0x56, 0x67, 0x21, 0xc6, 0xce, 0x09, 0x8b, 0x88, 0x76, 0x7a, 0xbf, 0xae, 0xa2, 0xea, + 0x09, 0x8b, 0xb0, 0x83, 0xd6, 0x95, 0xea, 0x40, 0x88, 0x32, 0xc7, 0x15, 0xc4, 0xf7, 0xd1, 0x9a, + 0x64, 0x93, 0x38, 0x30, 0x89, 0xea, 0xa4, 0x44, 0xaa, 0x64, 0x48, 0x25, 0xd5, 0x52, 0x69, 0x12, + 0x6d, 0xab, 0x59, 0x0f, 0x13, 0x16, 0x8c, 0xfd, 0x2c, 0x4f, 0x87, 0xc0, 0x1d, 0x6b, 0xa7, 0xb2, + 0x6b, 0xf5, 0x5a, 0xf3, 0xc2, 0x6d, 0x68, 0xfe, 0xa5, 0xa6, 0xc9, 0x32, 0xc0, 0x8f, 0xd1, 0xba, + 0x9c, 0xf9, 0xba, 0xfb, 0x9a, 0x9e, 0xef, 0xe6, 0xbc, 0x70, 0x5b, 0x72, 0x71, 0xc1, 0xaf, 0xa9, + 0x18, 0x91, 0x35, 0x39, 0x53, 0x4f, 0xdc, 0x45, 0xb6, 0x9c, 0xf9, 0x71, 0x16, 0xc2, 0x4c, 0x2f, + 0xdd, 0xea, 0x6d, 0xcd, 0x0b, 0xb7, 0xbd, 0x74, 0xfc, 0x58, 0xf9, 0xc8, 0xba, 0x9c, 0x69, 0x03, + 0x3f, 0x46, 0xc8, 0xb4, 0xa4, 0x2b, 0xac, 0xeb, 0x0a, 0x1b, 0xf3, 0xc2, 0xad, 0x6b, 0x56, 0xe7, + 0x5e, 0x98, 0xd8, 0x43, 0x35, 0x93, 0xdb, 0xd6, 0xb9, 0x9b, 0xf3, 0xc2, 0xb5, 0x13, 0x16, 0x99, + 0x9c, 0xc6, 0xa5, 0x46, 0xc5, 0x21, 0x65, 0x53, 0x08, 0xb5, 0x20, 0x6c, 0x72, 0x05, 0xbd, 0x1f, + 0x57, 0x91, 0x3d, 0x98, 0x11, 0x10, 0x79, 0x22, 0xf1, 0x11, 0x6a, 0x07, 0x2c, 0x93, 0x9c, 0x06, + 0xd2, 0xbf, 0x31, 0xda, 0xde, 0xc3, 0xcb, 0xc2, 0xfd, 0xc8, 0xe8, 0xfc, 0xf6, 0x09, 0x8f, 0xb4, + 0xae, 0xa8, 0xfd, 0x72, 0xfe, 0x5b, 0xa8, 0x36, 0x4c, 0x58, 0xf9, 0xe6, 0x36, 0x89, 0x01, 0xf8, + 0x44, 0x4f, 0x4d, 0xef, 0x57, 0x2d, 0xa0, 0xb1, 0xf7, 0x70, 0x79, 0xbf, 0xb7, 0xe4, 0xd1, 0xbb, + 0xaf, 0x24, 0x7b, 0x59, 0xb8, 0x77, 0x4c, 0xd5, 0x32, 0xd2, 0x53, 0x53, 0xd5, 0xf2, 0x69, 0xa3, + 0x2a, 0x07, 0xa9, 0xd7, 0xd5, 0x24, 0xca, 0xc4, 0x0f, 0x90, 0xcd, 0x61, 0x0a, 0x5c, 0x42, 0xa8, + 0xd7, 0x62, 0x93, 0x6b, 0x8c, 0x3f, 0x46, 0x76, 0x44, 0x85, 0x9f, 0x0b, 0x08, 0xcd, 0x0e, 0xc8, + 0x7a, 0x44, 0xc5, 0x1b, 0x01, 0xe1, 0x73, 0xeb, 0xa7, 0xb7, 0xee, 0x8a, 0x47, 0x51, 0x63, 0x3f, + 0x08, 0x40, 0x88, 0x41, 0x3e, 0x49, 0xe0, 0x3f, 0xb4, 0xb5, 0x87, 0x9a, 0x42, 0x32, 0x4e, 0x23, + 0xf0, 0xc7, 0x70, 0x5e, 0x2a, 0xcc, 0xe8, 0xa5, 0xe4, 0xbf, 0x81, 0x73, 0x41, 0x96, 0x41, 0x59, + 0xe2, 0x00, 0x35, 0x07, 0x9c, 0x06, 0xc0, 0x0f, 0x58, 0x76, 0x16, 0x47, 0xf8, 0x29, 0xda, 0x60, + 0x59, 0x72, 0xee, 0x4b, 0x36, 0xf1, 0x03, 0x9a, 0x24, 0xba, 0x92, 0x6d, 0x52, 0x29, 0xc7, 0x80, + 0x4d, 0x0e, 0x68, 0x92, 0x90, 0x65, 0xe0, 0xfd, 0x5d, 0x45, 0x0d, 0x9d, 0xa5, 0x4c, 0xa2, 0xa4, + 0xae, 0x93, 0x96, 0x7d, 0x96, 0x48, 0x5d, 0x40, 0xc6, 0x29, 0xb0, 0x5c, 0x96, 0x2f, 0xe2, 0x15, + 0x54, 0x11, 0x1c, 0x60, 0x06, 0x81, 0xde, 0x82, 0x45, 0x4a, 0x84, 0x9f, 0xa1, 0x8d, 0x30, 0x16, + 0x74, 0x98, 0x80, 0x2f, 0x24, 0x0d, 0xc6, 0x66, 0x86, 0xbd, 0xf6, 0xbc, 0x70, 0x9b, 0xa5, 0xe3, + 0xb5, 0xe2, 0xc9, 0x0d, 0x84, 0xbf, 0x44, 0xad, 0x45, 0x98, 0xbe, 0xb2, 0xf9, 0xb2, 0xf5, 0xf0, + 0xbc, 0x70, 0xef, 0x5c, 0x1f, 0xd5, 0x1e, 0x72, 0x0b, 0x2b, 0xa1, 0x84, 0x30, 0xcc, 0x23, 0xad, + 0x5d, 0x9b, 0x18, 0xa0, 0xd8, 0x24, 0x4e, 0x63, 0xa9, 0xb5, 0x5a, 0x23, 0x06, 0xa8, 0xfe, 0x20, + 0xd3, 0x75, 0x52, 0x48, 0x19, 0x3f, 0x77, 0x1a, 0x8b, 0xfe, 0x8c, 0xe3, 0x54, 0xf3, 0xe4, 0x06, + 0xc2, 0x3d, 0x84, 0xcb, 0x30, 0x0e, 0x32, 0xe7, 0x99, 0xaf, 0xbf, 0x00, 0x4d, 0x1d, 0xab, 0xdf, + 0x43, 0xe3, 0x25, 0xda, 0xf9, 0x82, 0x4a, 0x4a, 0xfe, 0xc5, 0xe0, 0x6f, 0xd1, 0x86, 0x19, 0xab, + 0x1f, 0xe8, 0xa9, 0x3b, 0x1b, 0x5a, 0xbf, 0xce, 0x2d, 0xfd, 0x5e, 0xaf, 0xd6, 0x34, 0x25, 0x97, + 0x18, 0x72, 0x03, 0xf5, 0x2d, 0xdb, 0x6a, 0xd7, 0xcc, 0xb7, 0xb4, 0x6f, 0xd9, 0xa8, 0xdd, 0xb8, + 0x9e, 0x4c, 0x79, 0x39, 0xb2, 0x79, 0x85, 0x97, 0xba, 0xee, 0x7d, 0xf5, 0xee, 0x62, 0xbb, 0xf2, + 0xfe, 0x62, 0xbb, 0xf2, 0xe7, 0xc5, 0x76, 0xe5, 0xe7, 0x0f, 0xdb, 0x2b, 0xef, 0x3f, 0x6c, 0xaf, + 0xfc, 0xfe, 0x61, 0x7b, 0xe5, 0xfb, 0x4f, 0xff, 0xf7, 0x5f, 0x3b, 0x53, 0x3f, 0xf9, 0xe1, 0x9a, + 0xfe, 0x87, 0x3f, 0xfd, 0x27, 0x00, 0x00, 0xff, 0xff, 0xad, 0xf4, 0x1f, 0x30, 0xfd, 0x07, 0x00, + 0x00, } func (this *Params) Equal(that interface{}) bool { @@ -1159,6 +1207,39 @@ func (m *AccessTuple) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *TracerConfig) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *TracerConfig) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *TracerConfig) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.OnlyTopCall { + i-- + if m.OnlyTopCall { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + func (m *TraceConfig) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) @@ -1179,10 +1260,15 @@ func (m *TraceConfig) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l - if len(m.TracerJsonConfig) > 0 { - i -= len(m.TracerJsonConfig) - copy(dAtA[i:], m.TracerJsonConfig) - i = encodeVarintEvm(dAtA, i, uint64(len(m.TracerJsonConfig))) + if m.TracerConfig != nil { + { + size, err := m.TracerConfig.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintEvm(dAtA, i, uint64(size)) + } i-- dAtA[i] = 0x6a } @@ -1450,6 +1536,18 @@ func (m *AccessTuple) Size() (n int) { return n } +func (m *TracerConfig) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.OnlyTopCall { + n += 2 + } + return n +} + func (m *TraceConfig) Size() (n int) { if m == nil { return 0 @@ -1485,8 +1583,8 @@ func (m *TraceConfig) Size() (n int) { if m.EnableReturnData { n += 2 } - l = len(m.TracerJsonConfig) - if l > 0 { + if m.TracerConfig != nil { + l = m.TracerConfig.Size() n += 1 + l + sovEvm(uint64(l)) } return n @@ -2733,6 +2831,76 @@ func (m *AccessTuple) Unmarshal(dAtA []byte) error { } return nil } +func (m *TracerConfig) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowEvm + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: TracerConfig: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: TracerConfig: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field OnlyTopCall", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowEvm + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.OnlyTopCall = bool(v != 0) + default: + iNdEx = preIndex + skippy, err := skipEvm(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthEvm + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func (m *TraceConfig) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 @@ -2966,9 +3134,9 @@ func (m *TraceConfig) Unmarshal(dAtA []byte) error { m.EnableReturnData = bool(v != 0) case 13: if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field TracerJsonConfig", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field TracerConfig", wireType) } - var stringLen uint64 + var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowEvm @@ -2978,23 +3146,27 @@ func (m *TraceConfig) Unmarshal(dAtA []byte) error { } b := dAtA[iNdEx] iNdEx++ - stringLen |= uint64(b&0x7F) << shift + msglen |= int(b&0x7F) << shift if b < 0x80 { break } } - intStringLen := int(stringLen) - if intStringLen < 0 { + if msglen < 0 { return ErrInvalidLengthEvm } - postIndex := iNdEx + intStringLen + postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLengthEvm } if postIndex > l { return io.ErrUnexpectedEOF } - m.TracerJsonConfig = string(dAtA[iNdEx:postIndex]) + if m.TracerConfig == nil { + m.TracerConfig = &TracerConfig{} + } + if err := m.TracerConfig.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } iNdEx = postIndex default: iNdEx = preIndex diff --git a/x/evm/keeper/grpc_query.go b/x/evm/keeper/grpc_query.go index 4509e5ed2..c2c051be3 100644 --- a/x/evm/keeper/grpc_query.go +++ b/x/evm/keeper/grpc_query.go @@ -528,9 +528,9 @@ func (k Keeper) TraceTx( } var tracerConfig json.RawMessage - if req.TraceConfig != nil && req.TraceConfig.TracerJsonConfig != "" { + if req.TraceConfig != nil && req.TraceConfig.TracerConfig != nil { // ignore error. default to no traceConfig - _ = json.Unmarshal([]byte(req.TraceConfig.TracerJsonConfig), &tracerConfig) + tracerConfig, _ = json.Marshal(req.TraceConfig.TracerConfig) } msg, err := tx.AsMessage(signer, cfg.BaseFee) @@ -596,9 +596,9 @@ func (k Keeper) TraceCall( txConfig := statedb.NewEmptyTxConfig(gethcommon.BytesToHash(ctx.HeaderHash().Bytes())) var tracerConfig json.RawMessage - if req.TraceConfig != nil && req.TraceConfig.TracerJsonConfig != "" { + if req.TraceConfig != nil && req.TraceConfig.TracerConfig != nil { // ignore error. default to no traceConfig - _ = json.Unmarshal([]byte(req.TraceConfig.TracerJsonConfig), &tracerConfig) + tracerConfig, _ = json.Marshal(req.TraceConfig.TracerConfig) } // req.Msg is not signed, so to gethcore.Message because it's not signed and will fail on getting @@ -681,6 +681,11 @@ func (k Keeper) TraceBlock( if baseFee != nil { cfg.BaseFee = baseFee } + var tracerConfig json.RawMessage + if req.TraceConfig != nil && req.TraceConfig.TracerConfig != nil { + // ignore error. default to no traceConfig + tracerConfig, _ = json.Marshal(req.TraceConfig.TracerConfig) + } signer := gethcore.MakeSigner(cfg.ChainConfig, big.NewInt(ctx.BlockHeight())) txsLength := len(req.Txs) @@ -698,7 +703,7 @@ func (k Keeper) TraceBlock( result.Error = err.Error() continue } - traceResult, logIndex, err := k.TraceEthTxMsg(ctx, cfg, txConfig, msg, req.TraceConfig, true, nil) + traceResult, logIndex, err := k.TraceEthTxMsg(ctx, cfg, txConfig, msg, req.TraceConfig, true, tracerConfig) if err != nil { result.Error = err.Error() } else { From 60167cafd835f32a228dedea9d1ed7b29ef092bb Mon Sep 17 00:00:00 2001 From: Unique Divine <51418232+Unique-Divine@users.noreply.github.com> Date: Fri, 13 Sep 2024 03:07:36 +0900 Subject: [PATCH 03/16] refactor(eth-rpc): Delete unused code and improve logging in the eth and debug namespaces (#2030) * wip!: checkpoint * remove dead code * test(e2e): remove unnecessary skips * pending chekcpoint * chore(github): Automate labeleing and add-to-project for GH issues/tickets * chore: changelog * chore: linter and changelog * wip!: save * chore: stopping point * chore: linter --- CHANGELOG.md | 1 + app/evmante/evmante_emit_event.go | 3 + e2e/evm/test/basic_queries.test.ts | 397 ------------------ e2e/evm/test/debug_queries.test.ts | 41 +- e2e/evm/test/eth_queries.test.ts | 35 +- eth/indexer/kv_indexer.go | 8 +- eth/rpc/backend/blocks.go | 10 +- eth/rpc/backend/call_tx.go | 7 +- eth/rpc/backend/filters.go | 10 +- eth/rpc/backend/tx_info.go | 15 +- eth/rpc/rpc.go | 13 +- eth/rpc/rpcapi/apis.go | 3 +- eth/rpc/rpcapi/eth_api_test.go | 96 +++-- .../{filtersapi/api.go => eth_filters_api.go} | 134 +++--- eth/rpc/rpcapi/event_subscriber.go | 327 +++++++++++++++ eth/rpc/rpcapi/event_subscriber_test.go | 127 ++++++ .../{filtersapi/utils.go => filter_utils.go} | 35 +- eth/rpc/rpcapi/{filtersapi => }/filters.go | 19 +- eth/rpc/rpcapi/filtersapi/filter_system.go | 311 -------------- .../rpcapi/filtersapi/filter_system_test.go | 73 ---- eth/rpc/rpcapi/net_api_test.go | 2 +- .../rpcapi/{filtersapi => }/subscription.go | 46 +- eth/rpc/rpcapi/websockets.go | 19 +- eth/stringify.go | 35 ++ eth/stringify_test.go | 23 + x/common/address.go | 1 + x/evm/cli/query.go | 2 - x/evm/evmtest/tx.go | 10 +- x/evm/keeper/hooks.go | 2 +- x/evm/keeper/msg_server.go | 3 + 30 files changed, 815 insertions(+), 993 deletions(-) delete mode 100644 e2e/evm/test/basic_queries.test.ts rename eth/rpc/rpcapi/{filtersapi/api.go => eth_filters_api.go} (86%) create mode 100644 eth/rpc/rpcapi/event_subscriber.go create mode 100644 eth/rpc/rpcapi/event_subscriber_test.go rename eth/rpc/rpcapi/{filtersapi/utils.go => filter_utils.go} (73%) rename eth/rpc/rpcapi/{filtersapi => }/filters.go (93%) delete mode 100644 eth/rpc/rpcapi/filtersapi/filter_system.go delete mode 100644 eth/rpc/rpcapi/filtersapi/filter_system_test.go rename eth/rpc/rpcapi/{filtersapi => }/subscription.go (59%) create mode 100644 eth/stringify.go create mode 100644 eth/stringify_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index ba5b6877a..96dae1703 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -117,6 +117,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#2020](https://github.com/NibiruChain/nibiru/pull/2020) - test(evm): e2e tests for debug namespace - [#2022](https://github.com/NibiruChain/nibiru/pull/2022) - feat(evm): debug_traceCall method implemented - [#2023](https://github.com/NibiruChain/nibiru/pull/2023) - fix(evm)!: adjusted generation and parsing of the block bloom events +- [#2030](https://github.com/NibiruChain/nibiru/pull/2030) - refactor(eth/rpc): Delete unused code and improve logging in the eth and debug namespaces - [#2031](https://github.com/NibiruChain/nibiru/pull/2031) - fix(evm): debug calls with custom tracer and tracer options #### Dapp modules: perp, spot, oracle, etc diff --git a/app/evmante/evmante_emit_event.go b/app/evmante/evmante_emit_event.go index 95b85b648..01e178d6f 100644 --- a/app/evmante/evmante_emit_event.go +++ b/app/evmante/evmante_emit_event.go @@ -54,6 +54,9 @@ func (eeed EthEmitEventDecorator) AnteHandle( 10, ), ), // #nosec G701 + // TODO: fix: It's odd that each event is emitted twice. Migrate to typed + // events and change EVM indexer to align. + // sdk.NewAttribute("emitted_from", "EthEmitEventDecorator"), )) } diff --git a/e2e/evm/test/basic_queries.test.ts b/e2e/evm/test/basic_queries.test.ts deleted file mode 100644 index 8caed4032..000000000 --- a/e2e/evm/test/basic_queries.test.ts +++ /dev/null @@ -1,397 +0,0 @@ -import { describe, expect, it, jest } from "@jest/globals" -import { - toBigInt, - parseEther, - keccak256, - AbiCoder, - TransactionRequest, - Block, - TransactionResponse, -} from "ethers" -import { account, provider } from "./setup" -import { - INTRINSIC_TX_GAS, - alice, - deployContractTestERC20, - deployContractSendNibi, - hexify, - sendTestNibi, -} from "./utils" - -describe("Basic Queries", () => { - jest.setTimeout(15e3) - - it("Simple transfer, balance check", async () => { - const amountToSend = toBigInt(5e12) * toBigInt(1e6) // unibi - const senderBalanceBefore = await provider.getBalance(account) - const recipientBalanceBefore = await provider.getBalance(alice) - expect(senderBalanceBefore).toBeGreaterThan(0) - expect(recipientBalanceBefore).toEqual(BigInt(0)) - - const tenPow12 = toBigInt(1e12) - - // Execute EVM transfer - const transaction: TransactionRequest = { - gasLimit: toBigInt(100e3), - to: alice, - value: amountToSend, - } - const txResponse = await account.sendTransaction(transaction) - await txResponse.wait(1, 10e3) - expect(txResponse).toHaveProperty("blockHash") - - const senderBalanceAfter = await provider.getBalance(account) - const recipientBalanceAfter = await provider.getBalance(alice) - - // Assert balances with logging - const gasUsed = 50000n // 50k gas for the transaction - const txCostMicronibi = amountToSend / tenPow12 + gasUsed - const txCostWei = txCostMicronibi * tenPow12 - const expectedSenderWei = senderBalanceBefore - txCostWei - console.debug("DEBUG should send via transfer method %o:", { - senderBalanceBefore, - amountToSend, - expectedSenderWei, - senderBalanceAfter, - }) - expect(senderBalanceAfter).toEqual(expectedSenderWei) - expect(recipientBalanceAfter).toEqual(amountToSend) - }) - - it("eth_accounts", async () => { - const accounts = await provider.listAccounts() - expect(accounts).not.toHaveLength(0) - }) - - it("eth_estimateGas", async () => { - const tx = { - from: account.address, - to: alice, - value: parseEther("0.01"), // Sending 0.01 Ether - } - const estimatedGas = await provider.estimateGas(tx) - expect(estimatedGas).toBeGreaterThan(BigInt(0)) - expect(estimatedGas).toEqual(INTRINSIC_TX_GAS) - }) - - it("eth_feeHistory", async () => { - const blockCount = 5 // Number of blocks in the requested history - const newestBlock = "latest" // Can be a block number or 'latest' - const rewardPercentiles = [25, 50, 75] // Example percentiles for priority fees - - const feeHistory = await provider.send("eth_feeHistory", [ - blockCount, - newestBlock, - rewardPercentiles, - ]) - expect(feeHistory).toBeDefined() - expect(feeHistory).toHaveProperty("baseFeePerGas") - expect(feeHistory).toHaveProperty("gasUsedRatio") - expect(feeHistory).toHaveProperty("oldestBlock") - expect(feeHistory).toHaveProperty("reward") - }) - - it("eth_gasPrice", async () => { - const gasPrice = await provider.send("eth_gasPrice", []) - expect(gasPrice).toBeDefined() - expect(gasPrice).toEqual(hexify(1)) - }) - - it("eth_getBalance", async () => { - const balance = await provider.getBalance(account.address) - expect(balance).toBeGreaterThan(0) - }) - - it("eth_getBlockByNumber, eth_getBlockByHash", async () => { - const blockNumber = 1 - const blockByNumber = await provider.send("eth_getBlockByNumber", [ - blockNumber, - false, - ]) - expect(blockByNumber).toBeDefined() - expect(blockByNumber).toHaveProperty("hash") - - const blockByHash = await provider.send("eth_getBlockByHash", [ - blockByNumber.hash, - false, - ]) - expect(blockByHash).toBeDefined() - expect(blockByHash.hash).toEqual(blockByNumber.hash) - expect(blockByHash.number).toEqual(blockByNumber.number) - }) - - it("eth_getBlockTransactionCountByHash", async () => { - const blockNumber = 1 - const block = await provider.send("eth_getBlockByNumber", [ - blockNumber, - false, - ]) - const txCount = await provider.send("eth_getBlockTransactionCountByHash", [ - block.hash, - ]) - expect(parseInt(txCount)).toBeGreaterThanOrEqual(0) - }) - - it("eth_getBlockTransactionCountByNumber", async () => { - const blockNumber = 1 - const txCount = await provider.send( - "eth_getBlockTransactionCountByNumber", - [blockNumber], - ) - expect(parseInt(txCount)).toBeGreaterThanOrEqual(0) - }) - - it("eth_getCode", async () => { - const contract = await deployContractSendNibi() - const contractAddr = await contract.getAddress() - const code = await provider.send("eth_getCode", [contractAddr, "latest"]) - expect(code).toBeDefined() - }) - - it("eth_getFilterChanges", async () => { - // Deploy ERC-20 contract - const contract = await deployContractTestERC20() - const contractAddr = await contract.getAddress() - const filter = { - fromBlock: "latest", - address: contractAddr, - } - // Create the filter for a contract - const filterId = await provider.send("eth_newFilter", [filter]) - expect(filterId).toBeDefined() - - // Execute some contract TX - const tx = await contract.transfer(alice, parseEther("0.01")) - await tx.wait(1, 5e3) - await new Promise((resolve) => setTimeout(resolve, 3000)) - - // Assert logs - const changes = await provider.send("eth_getFilterChanges", [filterId]) - expect(changes.length).toBeGreaterThan(0) - expect(changes[0]).toHaveProperty("address") - expect(changes[0]).toHaveProperty("data") - expect(changes[0]).toHaveProperty("topics") - - const success = await provider.send("eth_uninstallFilter", [filterId]) - expect(success).toBeTruthy() - }) - - // Skipping as the method is not implemented - it.skip("eth_getFilterLogs", async () => { - // Deploy ERC-20 contract - const contract = await deployContractTestERC20() - const contractAddr = await contract.getAddress() - const filter = { - fromBlock: "latest", - address: contractAddr, - } - // Execute some contract TX - const tx = await contract.transfer(alice, parseEther("0.01")) - await tx.wait(1, 5e3) - - // Create the filter for a contract - const filterId = await provider.send("eth_newFilter", [filter]) - expect(filterId).toBeDefined() - - // Assert logs - const changes = await provider.send("eth_getFilterLogs", [filterId]) - expect(changes.length).toBeGreaterThan(0) - expect(changes[0]).toHaveProperty("address") - expect(changes[0]).toHaveProperty("data") - expect(changes[0]).toHaveProperty("topics") - }) - - // Skipping as the method is not implemented - it.skip("eth_getLogs", async () => { - // Deploy ERC-20 contract - const contract = await deployContractTestERC20() - const contractAddr = await contract.getAddress() - const filter = { - fromBlock: "latest", - address: contractAddr, - } - // Execute some contract TX - const tx = await contract.transfer(alice, parseEther("0.01")) - - // Assert logs - const changes = await provider.send("eth_getLogs", [filter]) - expect(changes.length).toBeGreaterThan(0) - expect(changes[0]).toHaveProperty("address") - expect(changes[0]).toHaveProperty("data") - expect(changes[0]).toHaveProperty("topics") - }) - - it("eth_getProof", async () => { - // Deploy ERC-20 contract - const contract = await deployContractTestERC20() - const contractAddr = await contract.getAddress() - - const slot = 1 // Assuming balanceOf is at slot 1 - const storageKey = keccak256( - AbiCoder.defaultAbiCoder().encode( - ["address", "uint256"], - [account.address, slot], - ), - ) - const proof = await provider.send("eth_getProof", [ - contractAddr, - [storageKey], - "latest", - ]) - // Assert proof structure - expect(proof).toHaveProperty("address") - expect(proof).toHaveProperty("balance") - expect(proof).toHaveProperty("codeHash") - expect(proof).toHaveProperty("nonce") - expect(proof).toHaveProperty("storageProof") - - if (proof.storageProof.length > 0) { - expect(proof.storageProof[0]).toHaveProperty("key", storageKey) - expect(proof.storageProof[0]).toHaveProperty("value") - expect(proof.storageProof[0]).toHaveProperty("proof") - } - }) - - // Skipping as the method is not implemented - it.skip("eth_getLogs", async () => { - // Deploy ERC-20 contract - const contract = await deployContractTestERC20() - const contractAddr = await contract.getAddress() - const filter = { - fromBlock: "latest", - address: contractAddr, - } - // Execute some contract TX - const tx = await contract.transfer(alice, parseEther("0.01")) - await tx.wait(1, 5e3) - - // Assert logs - const logs = await provider.send("eth_getLogs", [filter]) - expect(logs.length).toBeGreaterThan(0) - expect(logs[0]).toHaveProperty("address") - expect(logs[0]).toHaveProperty("data") - expect(logs[0]).toHaveProperty("topics") - }) - - it("eth_getProof", async () => { - const contract = await deployContractTestERC20() - const contractAddr = await contract.getAddress() - - const slot = 1 // Assuming balanceOf is at slot 1 - const storageKey = keccak256( - AbiCoder.defaultAbiCoder().encode( - ["address", "uint256"], - [account.address, slot], - ), - ) - const proof = await provider.send("eth_getProof", [ - contractAddr, - [storageKey], - "latest", - ]) - // Assert proof structure - expect(proof).toHaveProperty("address") - expect(proof).toHaveProperty("balance") - expect(proof).toHaveProperty("codeHash") - expect(proof).toHaveProperty("nonce") - expect(proof).toHaveProperty("storageProof") - - if (proof.storageProof.length > 0) { - expect(proof.storageProof[0]).toHaveProperty("key", storageKey) - expect(proof.storageProof[0]).toHaveProperty("value") - expect(proof.storageProof[0]).toHaveProperty("proof") - } - }) - - it("eth_getStorageAt", async () => { - const contract = await deployContractTestERC20() - const contractAddr = await contract.getAddress() - - const value = await provider.getStorage(contractAddr, 1) - expect(value).toBeDefined() - }) - - it("eth_getTransactionByBlockHashAndIndex, eth_getTransactionByBlockNumberAndIndex", async () => { - // Execute EVM transfer - const txResponse: TransactionResponse = await sendTestNibi() - const block: Block = (await txResponse.getBlock()) as Block - expect(block).toBeTruthy() - - const txByBlockHash = await provider.send( - "eth_getTransactionByBlockHashAndIndex", - [block.hash, "0x0"], - ) - expect(txByBlockHash).toBeDefined() - expect(txByBlockHash).toHaveProperty("from") - expect(txByBlockHash).toHaveProperty("to") - expect(txByBlockHash).toHaveProperty("blockHash") - expect(txByBlockHash).toHaveProperty("blockNumber") - expect(txByBlockHash).toHaveProperty("value") - - const txByBlockNumber = await provider.send( - "eth_getTransactionByBlockNumberAndIndex", - [block.number, "0x0"], - ) - - expect(txByBlockNumber).toBeDefined() - expect(txByBlockNumber["from"]).toEqual(txByBlockHash["from"]) - expect(txByBlockNumber["to"]).toEqual(txByBlockHash["to"]) - expect(txByBlockNumber["value"]).toEqual(txByBlockHash["value"]) - }) - - it("eth_getTransactionByHash", async () => { - const txResponse = await sendTestNibi() - const txByHash = await provider.getTransaction(txResponse.hash) - expect(txByHash).toBeDefined() - expect(txByHash.hash).toEqual(txResponse.hash) - }) - - it("eth_getTransactionCount", async () => { - const txCount = await provider.getTransactionCount(account.address) - expect(txCount).toBeGreaterThanOrEqual(0) - }) - - it("eth_getTransactionReceipt", async () => { - const txResponse = await sendTestNibi() - const txReceipt = await provider.getTransactionReceipt(txResponse.hash) - expect(txReceipt).toBeDefined() - expect(txReceipt.hash).toEqual(txResponse.hash) - }) - - it("eth_getUncleCountByBlockHash", async () => { - const latestBlock = await provider.getBlockNumber() - const block = await provider.getBlock(latestBlock) - const uncleCount = await provider.send("eth_getUncleCountByBlockHash", [ - block.hash, - ]) - expect(parseInt(uncleCount)).toBeGreaterThanOrEqual(0) - }) - - it("eth_getUncleCountByBlockNumber", async () => { - const latestBlock = await provider.getBlockNumber() - const uncleCount = await provider.send("eth_getUncleCountByBlockNumber", [ - latestBlock, - ]) - expect(parseInt(uncleCount)).toBeGreaterThanOrEqual(0) - }) - - it("eth_maxPriorityFeePerGas", async () => { - const maxPriorityGas = await provider.send("eth_maxPriorityFeePerGas", []) - expect(parseInt(maxPriorityGas)).toBeGreaterThanOrEqual(0) - }) - - it("eth_newBlockFilter", async () => { - const filterId = await provider.send("eth_newBlockFilter", []) - expect(filterId).toBeDefined() - }) - - it("eth_newPendingTransactionFilter", async () => { - const filterId = await provider.send("eth_newPendingTransactionFilter", []) - expect(filterId).toBeDefined() - }) - - it("eth_syncing", async () => { - const syncing = await provider.send("eth_syncing", []) - expect(syncing).toBeFalsy() - }) -}) diff --git a/e2e/evm/test/debug_queries.test.ts b/e2e/evm/test/debug_queries.test.ts index c01723d90..180ef2ab6 100644 --- a/e2e/evm/test/debug_queries.test.ts +++ b/e2e/evm/test/debug_queries.test.ts @@ -41,7 +41,6 @@ describe("debug queries", () => { expectTrace(traceResult) }) - // TODO: impl in EVM: remove skip it("debug_traceBlockByHash", async () => { const traceResult = await provider.send("debug_traceBlockByHash", [ blockHash, @@ -86,22 +85,34 @@ describe("debug queries", () => { expectTrace([{ result: traceResult }]) }) - // TODO: impl in EVM: remove skip - it.skip("debug_getBadBlocks", async () => { - const traceResult = await provider.send("debug_getBadBlocks", [txHash]) - expect(traceResult).toBeDefined() + // TODO: impl in EVM + it("debug_getBadBlocks", async () => { + try { + const traceResult = await provider.send("debug_getBadBlocks", [txHash]) + expect(traceResult).toBeDefined() + } catch (err) { + expect(err.message).toContain( + "the method debug_getBadBlocks does not exist", + ) + } }) - // TODO: impl in EVM: remove skip - it.skip("debug_storageRangeAt", async () => { - const traceResult = await provider.send("debug_storageRangeAt", [ - blockNumber, - txIndex, - contractAddress, - "0x0", - 100, - ]) - expect(traceResult).toBeDefined() + // TODO: impl in EVM + it("debug_storageRangeAt", async () => { + try { + const traceResult = await provider.send("debug_storageRangeAt", [ + blockNumber, + txIndex, + contractAddress, + "0x0", + 100, + ]) + expect(traceResult).toBeDefined() + } catch (err) { + expect(err.message).toContain( + "the method debug_storageRangeAt does not exist", + ) + } }) }) diff --git a/e2e/evm/test/eth_queries.test.ts b/e2e/evm/test/eth_queries.test.ts index ac648e274..3189ede3c 100644 --- a/e2e/evm/test/eth_queries.test.ts +++ b/e2e/evm/test/eth_queries.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, jest } from "@jest/globals" -import { parseEther, keccak256, AbiCoder, TransactionRequest } from "ethers" +import { parseEther, keccak256, AbiCoder } from "ethers" import { account, provider } from "./setup" import { INTRINSIC_TX_GAS, @@ -131,7 +131,6 @@ describe("eth queries", () => { expect(success).toBeTruthy() }) - // Skipping as the method is not implemented it("eth_getFilterLogs", async () => { // Deploy ERC-20 contract const contract = await deployContractTestERC20() @@ -157,12 +156,10 @@ describe("eth queries", () => { expect(changes[0]).toHaveProperty("topics") }) - // Skipping as the method is not implemented it("eth_getLogs", async () => { // Deploy ERC-20 contract const contract = await deployContractTestERC20() const contractAddr = await contract.getAddress() - console.log(contractAddr) const filter = { fromBlock: "0x1", address: contractAddr, @@ -211,36 +208,6 @@ describe("eth queries", () => { } }) - it("eth_getProof", async () => { - const contract = await deployContractTestERC20() - const contractAddr = await contract.getAddress() - - const slot = 1 // Assuming balanceOf is at slot 1 - const storageKey = keccak256( - AbiCoder.defaultAbiCoder().encode( - ["address", "uint256"], - [account.address, slot], - ), - ) - const proof = await provider.send("eth_getProof", [ - contractAddr, - [storageKey], - "latest", - ]) - // Assert proof structure - expect(proof).toHaveProperty("address") - expect(proof).toHaveProperty("balance") - expect(proof).toHaveProperty("codeHash") - expect(proof).toHaveProperty("nonce") - expect(proof).toHaveProperty("storageProof") - - if (proof.storageProof.length > 0) { - expect(proof.storageProof[0]).toHaveProperty("key", storageKey) - expect(proof.storageProof[0]).toHaveProperty("value") - expect(proof.storageProof[0]).toHaveProperty("proof") - } - }) - it("eth_getStorageAt", async () => { const contract = await deployContractTestERC20() const contractAddr = await contract.getAddress() diff --git a/eth/indexer/kv_indexer.go b/eth/indexer/kv_indexer.go index 7f3384a4a..30696d472 100644 --- a/eth/indexer/kv_indexer.go +++ b/eth/indexer/kv_indexer.go @@ -57,7 +57,13 @@ func (kv *KVIndexer) IndexBlock(block *tmtypes.Block, txResults []*abci.Response var ethTxIndex int32 for txIndex, tx := range block.Txs { result := txResults[txIndex] - if !rpc.TxSuccessOrExpectedFailure(result) { + isValidEnough, reason := rpc.TxIsValidEnough(result) + if !isValidEnough { + kv.logger.Debug( + "Skipped indexing of tx", + "reason", reason, + "tm_tx_hash", eth.TmTxHashToString(tx.Hash()), + ) continue } diff --git a/eth/rpc/backend/blocks.go b/eth/rpc/backend/blocks.go index a56201c4e..ea3e38f95 100644 --- a/eth/rpc/backend/blocks.go +++ b/eth/rpc/backend/blocks.go @@ -22,6 +22,7 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/metadata" + "github.com/NibiruChain/nibiru/v2/eth" "github.com/NibiruChain/nibiru/v2/eth/rpc" "github.com/NibiruChain/nibiru/v2/x/evm" ) @@ -267,8 +268,13 @@ func (b *Backend) EthMsgsFromTendermintBlock( // - Include unsuccessful tx that exceeds block gas limit // - Include unsuccessful tx that failed when committing changes to stateDB // - Exclude unsuccessful tx with any other error but ExceedBlockGasLimit - if !rpc.TxSuccessOrExpectedFailure(txResults[i]) { - b.logger.Debug("invalid tx result code", "cosmos-hash", hexutil.Encode(tx.Hash())) + isValidEnough, reason := rpc.TxIsValidEnough(txResults[i]) + if !isValidEnough { + b.logger.Debug( + "invalid tx result code", + "tm_tx_hash", eth.TmTxHashToString(tx.Hash()), + "reason", reason, + ) continue } diff --git a/eth/rpc/backend/call_tx.go b/eth/rpc/backend/call_tx.go index d7cff6eb4..12f7ce378 100644 --- a/eth/rpc/backend/call_tx.go +++ b/eth/rpc/backend/call_tx.go @@ -148,7 +148,9 @@ func (b *Backend) SendRawTransaction(data hexutil.Bytes) (common.Hash, error) { } txHash := ethereumTx.AsTransaction().Hash() - b.logger.Debug("eth_sendRawTransaction", "txHash", txHash.Hex()) + b.logger.Debug("eth_sendRawTransaction", + "txHash", txHash.Hex(), + ) syncCtx := b.clientCtx.WithBroadcastMode(flags.BroadcastSync) rsp, err := syncCtx.BroadcastTx(txBytes) @@ -159,6 +161,9 @@ func (b *Backend) SendRawTransaction(data hexutil.Bytes) (common.Hash, error) { b.logger.Error("failed to broadcast tx", "error", err.Error()) return txHash, err } + b.logger.Debug("eth_sendRawTransaction", + "blockHeight", fmt.Sprintf("%d", rsp.Height), + ) return txHash, nil } diff --git a/eth/rpc/backend/filters.go b/eth/rpc/backend/filters.go index 244cda0e5..02fa6c11f 100644 --- a/eth/rpc/backend/filters.go +++ b/eth/rpc/backend/filters.go @@ -30,8 +30,12 @@ func (b *Backend) GetLogsByHeight(height *int64) ([][]*gethcore.Log, error) { return GetLogsFromBlockResults(blockRes) } -// BloomStatus returns the BloomBitsBlocks and the number of processed sections maintained -// by the chain indexer. -func (b *Backend) BloomStatus() (uint64, uint64) { +// BloomStatus returns: +// - bloomBitsBlocks: The number of blocks a single bloom bit section vector +// contains on the server side. +// - bloomSections: The number of processed sections maintained by the indexer. +func (b *Backend) BloomStatus() ( + bloomBitBlocks, bloomSections uint64, +) { return 4096, 0 } diff --git a/eth/rpc/backend/tx_info.go b/eth/rpc/backend/tx_info.go index 48bbbf2bf..2f5003d0b 100644 --- a/eth/rpc/backend/tx_info.go +++ b/eth/rpc/backend/tx_info.go @@ -22,11 +22,11 @@ import ( "github.com/NibiruChain/nibiru/v2/x/evm" ) -// GetTransactionByHash returns the Ethereum format transaction identified by Ethereum transaction hash +// GetTransactionByHash returns the Ethereum format transaction identified by +// Ethereum transaction hash. If the transaction is not found or has been +// discarded from a pruning node, this resolves to nil. func (b *Backend) GetTransactionByHash(txHash common.Hash) (*rpc.EthTxJsonRPC, error) { res, err := b.GetTxByEthHash(txHash) - hexTx := txHash.Hex() - if err != nil { return b.getTransactionByHashPending(txHash) } @@ -57,7 +57,7 @@ func (b *Backend) GetTransactionByHash(txHash common.Hash) (*rpc.EthTxJsonRPC, e // Fallback to find tx index by iterating all valid eth transactions msgs := b.EthMsgsFromTendermintBlock(block, blockRes) for i := range msgs { - if msgs[i].Hash == hexTx { + if msgs[i].Hash == eth.EthTxHashToString(txHash) { if i > math.MaxInt32 { return nil, errors.New("tx index overflow") } @@ -316,7 +316,7 @@ func (b *Backend) GetTxByEthHash(hash common.Hash) (*eth.TxResult, error) { return txs.GetTxByHash(hash) }) if err != nil { - return nil, errorsmod.Wrapf(err, "GetTxByEthHash %s", hash.Hex()) + return nil, errorsmod.Wrapf(err, "GetTxByEthHash(%s)", hash.Hex()) } return txResult, nil } @@ -352,8 +352,9 @@ func (b *Backend) queryTendermintTxIndexer(query string, txGetter func(*rpc.Pars return nil, errors.New("ethereum tx not found") } txResult := resTxs.Txs[0] - if !rpc.TxSuccessOrExpectedFailure(&txResult.TxResult) { - return nil, errors.New("invalid ethereum tx") + isValidEnough, reason := rpc.TxIsValidEnough(&txResult.TxResult) + if !isValidEnough { + return nil, errors.Errorf("invalid ethereum tx: %s", reason) } var tx sdk.Tx diff --git a/eth/rpc/rpc.go b/eth/rpc/rpc.go index 8bd080c61..ec211a121 100644 --- a/eth/rpc/rpc.go +++ b/eth/rpc/rpc.go @@ -263,8 +263,15 @@ func TxStateDBCommitError(res *abci.ResponseDeliverTx) bool { return strings.Contains(res.Log, ErrStateDBCommit) } -// TxSuccessOrExpectedFailure returns true if the transaction was successful +// TxIsValidEnough returns true if the transaction was successful // or if it failed with an ExceedBlockGasLimit error or TxStateDBCommitError error -func TxSuccessOrExpectedFailure(res *abci.ResponseDeliverTx) bool { - return res.Code == 0 || TxExceedBlockGasLimit(res) || TxStateDBCommitError(res) +func TxIsValidEnough(res *abci.ResponseDeliverTx) (condition bool, reason string) { + if res.Code == 0 { + return true, "tx succeeded" + } else if TxExceedBlockGasLimit(res) { + return true, "tx exceeded block gas limit" + } else if TxStateDBCommitError(res) { + return true, "tx state db commit error" + } + return false, "unexpected failure" } diff --git a/eth/rpc/rpcapi/apis.go b/eth/rpc/rpcapi/apis.go index f3f33ae76..6607f92d2 100644 --- a/eth/rpc/rpcapi/apis.go +++ b/eth/rpc/rpcapi/apis.go @@ -10,7 +10,6 @@ import ( "github.com/NibiruChain/nibiru/v2/eth" "github.com/NibiruChain/nibiru/v2/eth/rpc/backend" "github.com/NibiruChain/nibiru/v2/eth/rpc/rpcapi/debugapi" - "github.com/NibiruChain/nibiru/v2/eth/rpc/rpcapi/filtersapi" rpcclient "github.com/cometbft/cometbft/rpc/jsonrpc/client" ) @@ -61,7 +60,7 @@ func init() { { Namespace: NamespaceEth, Version: apiVersion, - Service: filtersapi.NewImplFiltersAPI(ctx.Logger, clientCtx, tmWSClient, evmBackend), + Service: NewImplFiltersAPI(ctx.Logger, clientCtx, tmWSClient, evmBackend), Public: true, }, } diff --git a/eth/rpc/rpcapi/eth_api_test.go b/eth/rpc/rpcapi/eth_api_test.go index 39e995e87..74f5a8af5 100644 --- a/eth/rpc/rpcapi/eth_api_test.go +++ b/eth/rpc/rpcapi/eth_api_test.go @@ -3,6 +3,7 @@ package rpcapi_test import ( "context" "crypto/ecdsa" + "encoding/json" "fmt" "math/big" "strings" @@ -36,11 +37,11 @@ import ( ) var ( - _ suite.TearDownAllSuite = (*TestSuite)(nil) - _ suite.SetupAllSuite = (*TestSuite)(nil) + _ suite.TearDownAllSuite = (*NodeSuite)(nil) + _ suite.SetupAllSuite = (*NodeSuite)(nil) ) -type TestSuite struct { +type NodeSuite struct { suite.Suite cfg testnetwork.Config network *testnetwork.Network @@ -57,12 +58,13 @@ type TestSuite struct { } func TestSuite_RunAll(t *testing.T) { - suite.Run(t, new(TestSuite)) + suite.Run(t, new(Suite)) + suite.Run(t, new(NodeSuite)) } // SetupSuite runs before every test in the suite. Implements the // "suite.SetupAllSuite" interface. -func (s *TestSuite) SetupSuite() { +func (s *NodeSuite) SetupSuite() { testutil.BeforeIntegrationSuite(s.T()) testapp.EnsureNibiruPrefix() @@ -89,14 +91,14 @@ func (s *TestSuite) SetupSuite() { } // Test_ChainID EVM method: eth_chainId -func (s *TestSuite) Test_ChainID() { +func (s *NodeSuite) Test_ChainID() { ethChainID, err := s.ethClient.ChainID(context.Background()) s.NoError(err) s.Equal(appconst.ETH_CHAIN_ID_DEFAULT, ethChainID.Int64()) } // Test_BlockNumber EVM method: eth_blockNumber -func (s *TestSuite) Test_BlockNumber() { +func (s *NodeSuite) Test_BlockNumber() { networkBlockNumber, err := s.network.LatestHeight() s.NoError(err) @@ -106,7 +108,7 @@ func (s *TestSuite) Test_BlockNumber() { } // Test_BlockByNumber EVM method: eth_getBlockByNumber -func (s *TestSuite) Test_BlockByNumber() { +func (s *NodeSuite) Test_BlockByNumber() { networkBlockNumber, err := s.network.LatestHeight() s.NoError(err) @@ -116,7 +118,7 @@ func (s *TestSuite) Test_BlockByNumber() { } // Test_BalanceAt EVM method: eth_getBalance -func (s *TestSuite) Test_BalanceAt() { +func (s *NodeSuite) Test_BalanceAt() { testAccEthAddr := gethcommon.BytesToAddress(testnetwork.NewAccount(s.network, "new-user")) // New user balance should be 0 @@ -133,7 +135,7 @@ func (s *TestSuite) Test_BalanceAt() { } // Test_StorageAt EVM method: eth_getStorageAt -func (s *TestSuite) Test_StorageAt() { +func (s *NodeSuite) Test_StorageAt() { storage, err := s.ethClient.StorageAt( context.Background(), s.fundedAccEthAddr, gethcommon.Hash{}, nil, ) @@ -143,7 +145,7 @@ func (s *TestSuite) Test_StorageAt() { } // Test_PendingStorageAt EVM method: eth_getStorageAt | pending -func (s *TestSuite) Test_PendingStorageAt() { +func (s *NodeSuite) Test_PendingStorageAt() { storage, err := s.ethClient.PendingStorageAt( context.Background(), s.fundedAccEthAddr, gethcommon.Hash{}, ) @@ -154,7 +156,7 @@ func (s *TestSuite) Test_PendingStorageAt() { } // Test_CodeAt EVM method: eth_getCode -func (s *TestSuite) Test_CodeAt() { +func (s *NodeSuite) Test_CodeAt() { code, err := s.ethClient.CodeAt(context.Background(), s.fundedAccEthAddr, nil) s.NoError(err) @@ -163,7 +165,7 @@ func (s *TestSuite) Test_CodeAt() { } // Test_PendingCodeAt EVM method: eth_getCode -func (s *TestSuite) Test_PendingCodeAt() { +func (s *NodeSuite) Test_PendingCodeAt() { code, err := s.ethClient.PendingCodeAt(context.Background(), s.fundedAccEthAddr) s.NoError(err) @@ -172,7 +174,7 @@ func (s *TestSuite) Test_PendingCodeAt() { } // Test_EstimateGas EVM method: eth_estimateGas -func (s *TestSuite) Test_EstimateGas() { +func (s *NodeSuite) Test_EstimateGas() { testAccEthAddr := gethcommon.BytesToAddress(testnetwork.NewAccount(s.network, "new-user")) gasLimit := uint64(21000) msg := geth.CallMsg{ @@ -196,14 +198,14 @@ func (s *TestSuite) Test_EstimateGas() { } // Test_SuggestGasPrice EVM method: eth_gasPrice -func (s *TestSuite) Test_SuggestGasPrice() { +func (s *NodeSuite) Test_SuggestGasPrice() { // TODO: the backend method is stubbed to 0 _, err := s.ethClient.SuggestGasPrice(context.Background()) s.NoError(err) } // Test_SimpleTransferTransaction EVM method: eth_sendRawTransaction -func (s *TestSuite) Test_SimpleTransferTransaction() { +func (s *NodeSuite) Test_SimpleTransferTransaction() { chainID, err := s.ethClient.ChainID(context.Background()) s.NoError(err) nonce, err := s.ethClient.PendingNonceAt(context.Background(), s.fundedAccEthAddr) @@ -231,13 +233,15 @@ func (s *TestSuite) Test_SimpleTransferTransaction() { grpcConn, err := gosdk.GetGRPCConnection(grpcUrl, true, 5) s.NoError(err) - querier := bank.NewQueryClient(grpcConn) - resp, err := querier.Balance(context.Background(), &bank.QueryBalanceRequest{ - Address: s.fundedAccNibiAddr.String(), - Denom: eth.EthBaseDenom, - }) - s.Require().NoError(err) - s.Equal("105"+strings.Repeat("0", 6), resp.Balance.Amount.String()) + { + querier := bank.NewQueryClient(grpcConn) + resp, err := querier.Balance(context.Background(), &bank.QueryBalanceRequest{ + Address: s.fundedAccNibiAddr.String(), + Denom: eth.EthBaseDenom, + }) + s.Require().NoError(err) + s.Equal("105"+strings.Repeat("0", 6), resp.Balance.Amount.String()) + } s.T().Logf("Sending %d wei to %s", weiToSend, recipientAddr.Hex()) signer := gethcore.LatestSignerForChainID(chainID) @@ -257,7 +261,36 @@ func (s *TestSuite) Test_SimpleTransferTransaction() { s.Require().NoError(err) s.NoError(s.network.WaitForNextBlock()) - senderAmountAfterWei, err := s.ethClient.BalanceAt(context.Background(), s.fundedAccEthAddr, nil) + s.NoError(s.network.WaitForNextBlock()) + s.NoError(s.network.WaitForNextBlock()) + + txReceipt, err := s.ethClient.TransactionReceipt(blankCtx, tx.Hash()) + s.NoError(err) + + s.T().Log("Assert event expectations - successful eth tx") + { + blockHeightOfTx := int64(txReceipt.BlockNumber.Uint64()) + blockOfTx, err := s.val.RPCClient.BlockResults(blankCtx, &blockHeightOfTx) + s.NoError(err) + ethTxEvents := []sdk.Event{} + events := blockOfTx.TxsResults[0].Events + for _, event := range events { + if event.Type == "ethereum_tx" { + ethTxEvents = append(ethTxEvents, + sdk.Event{Type: event.Type, Attributes: event.Attributes}, + ) + } + } + + eventsJson, _ := json.Marshal(events) + s.Require().Equal(len(ethTxEvents), 2, "events: ", eventsJson) + hash0, _ := ethTxEvents[0].GetAttribute(evm.AttributeKeyEthereumTxHash) + hash1, _ := ethTxEvents[1].GetAttribute(evm.AttributeKeyEthereumTxHash) + s.Require().Equal(hash0, hash1) + } + + s.T().Log("Assert balances") + senderBalanceAfterWei, err := s.ethClient.BalanceAt(context.Background(), s.fundedAccEthAddr, nil) s.NoError(err) costOfTx := new(big.Int).Add( @@ -265,7 +298,7 @@ func (s *TestSuite) Test_SimpleTransferTransaction() { new(big.Int).Mul((new(big.Int).SetUint64(params.TxGas)), gasPrice), ) wantSenderBalWei := new(big.Int).Sub(senderBalanceBeforeWei, costOfTx) - s.Equal(wantSenderBalWei.String(), senderAmountAfterWei.String(), "surpising sender balance") + s.Equal(wantSenderBalWei.String(), senderBalanceAfterWei.String(), "surpising sender balance") recipientBalanceAfter, err := s.ethClient.BalanceAt(context.Background(), recipientAddr, nil) s.NoError(err) @@ -275,7 +308,7 @@ func (s *TestSuite) Test_SimpleTransferTransaction() { var blankCtx = context.Background() // Test_SmartContract includes contract deployment, query, execution -func (s *TestSuite) Test_SmartContract() { +func (s *NodeSuite) Test_SmartContract() { chainID, err := s.ethClient.ChainID(context.Background()) s.NoError(err) nonce, err := s.ethClient.NonceAt(context.Background(), s.fundedAccEthAddr, nil) @@ -376,7 +409,6 @@ func (s *TestSuite) Test_SmartContract() { txBz, err := tx.MarshalBinary() s.NoError(err) txHash, err := s.ethAPI.SendRawTransaction(txBz) - // err = s.ethClient.SendTransaction(blankCtx, tx) s.Require().NoError(err) _ = s.network.WaitForNextBlock() @@ -384,12 +416,18 @@ func (s *TestSuite) Test_SmartContract() { s.Require().NoError(err) s.NotNil(txReceipt) - _, err = s.ethAPI.GetTransactionLogs(txHash) + txHashFromReceipt := txReceipt.TxHash + s.Equal(txHash, txHashFromReceipt) + + // TODO: Test eth_getTransactionByHash using a JSON-RPC request at the + // endpoint directly. + tx, _, err = s.ethClient.TransactionByHash(blankCtx, txHash) s.NoError(err) + s.NotNil(tx) } } -func (s *TestSuite) TearDownSuite() { +func (s *NodeSuite) TearDownSuite() { s.T().Log("tearing down integration test suite") s.network.Cleanup() } diff --git a/eth/rpc/rpcapi/filtersapi/api.go b/eth/rpc/rpcapi/eth_filters_api.go similarity index 86% rename from eth/rpc/rpcapi/filtersapi/api.go rename to eth/rpc/rpcapi/eth_filters_api.go index 7d0985433..9a72a5042 100644 --- a/eth/rpc/rpcapi/filtersapi/api.go +++ b/eth/rpc/rpcapi/eth_filters_api.go @@ -1,5 +1,5 @@ // Copyright (c) 2023-2024 Nibi, Inc. -package filtersapi +package rpcapi import ( "context" @@ -11,6 +11,7 @@ import ( "github.com/cosmos/cosmos-sdk/client" "github.com/NibiruChain/nibiru/v2/eth/rpc" + rpcbackend "github.com/NibiruChain/nibiru/v2/eth/rpc/backend" "github.com/cometbft/cometbft/libs/log" @@ -26,33 +27,16 @@ import ( "github.com/NibiruChain/nibiru/v2/x/evm" ) -// IFilterAPI -type IFilterAPI interface { - NewPendingTransactionFilter() gethrpc.ID - NewBlockFilter() gethrpc.ID - NewFilter(criteria filters.FilterCriteria) (gethrpc.ID, error) - GetFilterChanges(id gethrpc.ID) (interface{}, error) - GetFilterLogs(ctx context.Context, id gethrpc.ID) ([]*gethcore.Log, error) - UninstallFilter(id gethrpc.ID) bool - GetLogs(ctx context.Context, crit filters.FilterCriteria) ([]*gethcore.Log, error) -} - -// IFilterEthBackend defines the methods requided by the PublicFilterAPI backend -type IFilterEthBackend interface { - GetBlockByNumber(blockNum rpc.BlockNumber, fullTx bool) (map[string]interface{}, error) - HeaderByNumber(blockNum rpc.BlockNumber) (*gethcore.Header, error) - HeaderByHash(blockHash common.Hash) (*gethcore.Header, error) - TendermintBlockByHash(hash common.Hash) (*coretypes.ResultBlock, error) - TendermintBlockResultByNumber(height *int64) (*coretypes.ResultBlockResults, error) - GetLogs(blockHash common.Hash) ([][]*gethcore.Log, error) - GetLogsByHeight(*int64) ([][]*gethcore.Log, error) - BlockBloom(blockRes *coretypes.ResultBlockResults) (gethcore.Bloom, error) - - BloomStatus() (uint64, uint64) - - RPCFilterCap() int32 - RPCLogsCap() int32 - RPCBlockRangeCap() int32 +// FiltersAPI offers support to create and manage filters. This will allow +// external clients to retrieve various information related to the Ethereum +// protocol such as blocks, transactions and logs. +type FiltersAPI struct { + logger log.Logger + clientCtx client.Context + backend *rpcbackend.Backend + events *EventSubscriber + filtersMu sync.Mutex + filters map[gethrpc.ID]*filter } // consider a filter inactive if it has not been polled for within deadlineForInactivity @@ -69,27 +53,20 @@ type filter struct { s *Subscription // associated subscription in event system } -// FiltersAPI offers support to create and manage filters. This will allow -// external clients to retrieve various information related to the Ethereum -// protocol such as blocks, transactions and logs. -type FiltersAPI struct { - logger log.Logger - clientCtx client.Context - backend IFilterEthBackend - events *EventSystem - filtersMu sync.Mutex - filters map[gethrpc.ID]*filter -} - -// NewImplFiltersAPI returns a new PublicFilterAPI instance. -func NewImplFiltersAPI(logger log.Logger, clientCtx client.Context, tmWSClient *rpcclient.WSClient, backend IFilterEthBackend) *FiltersAPI { +// NewImplFiltersAPI returns a new FiltersAPI instance. +func NewImplFiltersAPI( + logger log.Logger, + clientCtx client.Context, + tmWSClient *rpcclient.WSClient, + backend *rpcbackend.Backend, +) *FiltersAPI { logger = logger.With("api", "filter") api := &FiltersAPI{ logger: logger, clientCtx: clientCtx, backend: backend, filters: make(map[gethrpc.ID]*filter), - events: NewEventSystem(logger, tmWSClient), + events: NewEventSubscriber(logger, tmWSClient), } go api.timeoutLoop() @@ -189,7 +166,7 @@ func (api *FiltersAPI) NewPendingTransactionFilter() gethrpc.ID { api.filtersMu.Unlock() } } - }(pendingTxSub.eventCh, pendingTxSub.Err()) + }(pendingTxSub.EventCh, pendingTxSub.Error()) return pendingTxSub.ID() } @@ -198,6 +175,7 @@ func (api *FiltersAPI) NewPendingTransactionFilter() gethrpc.ID { // transaction enters the transaction pool and was signed from one of the // transactions this nodes manages. func (api *FiltersAPI) NewPendingTransactions(ctx context.Context) (*gethrpc.Subscription, error) { + api.logger.Debug("eth_newPendingTransactions") notifier, supported := gethrpc.NotifierFromContext(ctx) if !supported { return &gethrpc.Subscription{}, gethrpc.ErrNotificationsUnsupported @@ -254,7 +232,7 @@ func (api *FiltersAPI) NewPendingTransactions(ctx context.Context) (*gethrpc.Sub return } } - }(pendingTxSub.eventCh) + }(pendingTxSub.EventCh) return rpcSub, err } @@ -311,14 +289,15 @@ func (api *FiltersAPI) NewBlockFilter() gethrpc.ID { return } } - }(headerSub.eventCh, headerSub.Err()) + }(headerSub.EventCh, headerSub.Error()) return headerSub.ID() } -// NewHeads send a notification each time a new (header) block is appended to the -// chain. +// NewHeads send a notification each time a new block (and thus, block header) is +// added to the chain. func (api *FiltersAPI) NewHeads(ctx context.Context) (*gethrpc.Subscription, error) { + api.logger.Debug("eth_newHeads") notifier, supported := gethrpc.NotifierFromContext(ctx) if !supported { return &gethrpc.Subscription{}, gethrpc.ErrNotificationsUnsupported @@ -332,9 +311,17 @@ func (api *FiltersAPI) NewHeads(ctx context.Context) (*gethrpc.Subscription, err return &gethrpc.Subscription{}, err } + // Start go routine to continue executing without blocking while the + // goroutine handles incoming events. + // The routine receives a channel that receives block header events as an + // argument. go func(headersCh <-chan coretypes.ResultEvent) { + // This defer statement ensures the subscription is canceled + // when the goroutine exits. defer cancelSubs() + // Listen for block header events and handle them based on the + // type of event received. for { select { case ev, ok := <-headersCh: @@ -350,8 +337,12 @@ func (api *FiltersAPI) NewHeads(ctx context.Context) (*gethrpc.Subscription, err } var baseFee *big.Int = nil - // TODO: fetch bloom from events - header := rpc.EthHeaderFromTendermint(data.Header, gethcore.Bloom{}, baseFee) + bloom, err := ParseBloomFromEvents(data.ResultEndBlock.Events) + if err != nil { + api.logger.Error("failed to parse bloom from end block events") + return + } + header := rpc.EthHeaderFromTendermint(data.Header, bloom, baseFee) _ = notifier.Notify(rpcSub.ID, header) // #nosec G703 case <-rpcSub.Err(): headersSub.Unsubscribe(api.events) @@ -361,14 +352,18 @@ func (api *FiltersAPI) NewHeads(ctx context.Context) (*gethrpc.Subscription, err return } } - }(headersSub.eventCh) + }(headersSub.EventCh) return rpcSub, err } // Logs creates a subscription that fires for all new log that match the given // filter criteria. -func (api *FiltersAPI) Logs(ctx context.Context, crit filters.FilterCriteria) (*gethrpc.Subscription, error) { +// Implements "eth_logs". +func (api *FiltersAPI) Logs( + ctx context.Context, crit filters.FilterCriteria, +) (*gethrpc.Subscription, error) { + api.logger.Debug("eth_logs") notifier, supported := gethrpc.NotifierFromContext(ctx) if !supported { return &gethrpc.Subscription{}, gethrpc.ErrNotificationsUnsupported @@ -427,7 +422,7 @@ func (api *FiltersAPI) Logs(ctx context.Context, crit filters.FilterCriteria) (* return } } - }(logsSub.eventCh) + }(logsSub.EventCh) return rpcSub, err } @@ -446,6 +441,7 @@ func (api *FiltersAPI) Logs(ctx context.Context, crit filters.FilterCriteria) (* // // https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_newfilter func (api *FiltersAPI) NewFilter(criteria filters.FilterCriteria) (gethrpc.ID, error) { + api.logger.Debug("eth_newFilter") api.filtersMu.Lock() defer api.filtersMu.Unlock() @@ -504,26 +500,28 @@ func (api *FiltersAPI) NewFilter(criteria filters.FilterCriteria) (gethrpc.ID, e f.logs = append(f.logs, logs...) } api.filtersMu.Unlock() - case <-logsSub.Err(): + case <-logsSub.Error(): api.filtersMu.Lock() delete(api.filters, filterID) api.filtersMu.Unlock() return } } - }(logsSub.eventCh) + }(logsSub.EventCh) return filterID, err } // GetLogs returns logs matching the given argument that are stored within the state. -// +// This function implements the "eth_getLogs" JSON-RPC service method. // https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getlogs -func (api *FiltersAPI) GetLogs(ctx context.Context, crit filters.FilterCriteria) ([]*gethcore.Log, error) { +func (api *FiltersAPI) GetLogs( + ctx context.Context, crit filters.FilterCriteria, +) ([]*gethcore.Log, error) { var filter *Filter if crit.BlockHash != nil { // Block filter requested, construct a single-shot filter - filter = NewBlockFilter(api.logger, api.backend, crit) + filter = NewBlockFilter(api.logger, *api.backend, crit) } else { // Convert the RPC block numbers into internal representations begin := gethrpc.LatestBlockNumber.Int64() @@ -535,9 +533,15 @@ func (api *FiltersAPI) GetLogs(ctx context.Context, crit filters.FilterCriteria) end = crit.ToBlock.Int64() } // Construct the range filter - filter = NewRangeFilter(api.logger, api.backend, begin, end, crit.Addresses, crit.Topics) + filter = NewRangeFilter(api.logger, *api.backend, begin, end, crit.Addresses, crit.Topics) } + api.logger.Debug("eth_getLogs", + "from_block", filter.criteria.FromBlock.String(), + "to_block", filter.criteria.ToBlock.String(), + "time", time.Now().UTC(), + ) + // Run the filter and return all the logs logs, err := filter.Logs(ctx, int(api.backend.RPCLogsCap()), int64(api.backend.RPCBlockRangeCap())) if err != nil { @@ -568,8 +572,10 @@ func (api *FiltersAPI) UninstallFilter(id gethrpc.ID) bool { // GetFilterLogs returns the logs for the filter with the given id. // If the filter could not be found an empty array of logs is returned. // +// This function implements the "eth_getFilterLogs" JSON-RPC service method. // https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getfilterlogs func (api *FiltersAPI) GetFilterLogs(ctx context.Context, id gethrpc.ID) ([]*gethcore.Log, error) { + api.logger.Debug("eth_getFilterLogs") api.filtersMu.Lock() f, found := api.filters[id] api.filtersMu.Unlock() @@ -585,7 +591,7 @@ func (api *FiltersAPI) GetFilterLogs(ctx context.Context, id gethrpc.ID) ([]*get var filter *Filter if f.crit.BlockHash != nil { // Block filter requested, construct a single-shot filter - filter = NewBlockFilter(api.logger, api.backend, f.crit) + filter = NewBlockFilter(api.logger, *api.backend, f.crit) } else { // Convert the RPC block numbers into internal representations begin := gethrpc.LatestBlockNumber.Int64() @@ -597,7 +603,7 @@ func (api *FiltersAPI) GetFilterLogs(ctx context.Context, id gethrpc.ID) ([]*get end = f.crit.ToBlock.Int64() } // Construct the range filter - filter = NewRangeFilter(api.logger, api.backend, begin, end, f.crit.Addresses, f.crit.Topics) + filter = NewRangeFilter(api.logger, *api.backend, begin, end, f.crit.Addresses, f.crit.Topics) } // Run the filter and return all the logs logs, err := filter.Logs(ctx, int(api.backend.RPCLogsCap()), int64(api.backend.RPCBlockRangeCap())) @@ -607,14 +613,16 @@ func (api *FiltersAPI) GetFilterLogs(ctx context.Context, id gethrpc.ID) ([]*get return returnLogs(logs), nil } -// GetFilterChanges returns the logs for the filter with the given id since -// last time it was called. This can be used for polling. +// GetFilterChanges returns the logs for the filter with the given id since last +// time it was called. This can be used for polling. // // For pending transaction and block filters the result is []common.Hash. // (pending)Log filters return []Log. // +// This function implements the "eth_getFilterChanges" JSON-RPC service method. // https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getfilterchanges func (api *FiltersAPI) GetFilterChanges(id gethrpc.ID) (interface{}, error) { + api.logger.Debug("eth_getFilterChanges") api.filtersMu.Lock() defer api.filtersMu.Unlock() diff --git a/eth/rpc/rpcapi/event_subscriber.go b/eth/rpc/rpcapi/event_subscriber.go new file mode 100644 index 000000000..7f038dbdc --- /dev/null +++ b/eth/rpc/rpcapi/event_subscriber.go @@ -0,0 +1,327 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package rpcapi + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/pkg/errors" + + tmjson "github.com/cometbft/cometbft/libs/json" + "github.com/cometbft/cometbft/libs/log" + tmquery "github.com/cometbft/cometbft/libs/pubsub/query" + coretypes "github.com/cometbft/cometbft/rpc/core/types" + rpcclient "github.com/cometbft/cometbft/rpc/jsonrpc/client" + tmtypes "github.com/cometbft/cometbft/types" + + "github.com/ethereum/go-ethereum/common" + gethcore "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/eth/filters" + gethrpc "github.com/ethereum/go-ethereum/rpc" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/NibiruChain/nibiru/v2/eth/rpc/pubsub" + "github.com/NibiruChain/nibiru/v2/x/evm" +) + +var ( + txEventsQuery = tmtypes.QueryForEvent(tmtypes.EventTx).String() + evmEventsQuery = tmquery.MustParse(fmt.Sprintf("%s='%s' AND %s.%s='%s'", + tmtypes.EventTypeKey, + tmtypes.EventTx, + sdk.EventTypeMessage, + sdk.AttributeKeyModule, evm.ModuleName)).String() + headerEventsQuery = tmtypes.QueryForEvent(tmtypes.EventNewBlockHeader).String() +) + +// EventSubscriber creates subscriptions, processes events and broadcasts them to the +// subscription which match the subscription criteria using the Tendermint's RPC +// client. +type EventSubscriber struct { + Logger log.Logger + Ctx context.Context + TmWSClient *rpcclient.WSClient + + // light client mode + LightMode bool + + Index FilterIndex + TopicChans map[string]chan<- coretypes.ResultEvent + IndexMux *sync.RWMutex + + // Channels + Install chan *Subscription // install filter for event notification + Uninstall chan *Subscription // remove filter for event notification + EventBus pubsub.EventBus +} + +// NewEventSubscriber creates a new manager that listens for event on the given mux, +// parses and filters them. It uses the all map to retrieve filter changes. The +// work loop holds its own index that is used to forward events to filters. +// +// The returned manager has a loop that needs to be stopped with the Stop function +// or by stopping the given mux. +func NewEventSubscriber( + logger log.Logger, + tmWSClient *rpcclient.WSClient, +) *EventSubscriber { + index := make(FilterIndex) + for i := filters.UnknownSubscription; i < filters.LastIndexSubscription; i++ { + index[i] = make(map[gethrpc.ID]*Subscription) + } + + es := &EventSubscriber{ + Logger: logger, + Ctx: context.Background(), + TmWSClient: tmWSClient, + LightMode: false, + Index: index, + TopicChans: make(map[string]chan<- coretypes.ResultEvent, len(index)), + IndexMux: new(sync.RWMutex), + Install: make(chan *Subscription), + Uninstall: make(chan *Subscription), + EventBus: pubsub.NewEventBus(), + } + + go es.EventLoop() + go es.consumeEvents() + return es +} + +// WithContext sets a new context to the EventSystem. This is required to set a timeout context when +// a new filter is intantiated. +func (es *EventSubscriber) WithContext(ctx context.Context) { + es.Ctx = ctx +} + +// subscribe performs a new event subscription to a given Tendermint event. +// The subscription creates a unidirectional receive event channel to receive the ResultEvent. +func (es *EventSubscriber) subscribe(sub *Subscription) (*Subscription, pubsub.UnsubscribeFunc, error) { + var ( + err error + cancelFn context.CancelFunc + ) + + ctx, cancelFn := context.WithCancel(context.Background()) + defer cancelFn() + + existingSubs := es.EventBus.Topics() + for _, topic := range existingSubs { + if topic == sub.Event { + eventCh, unsubFn, err := es.EventBus.Subscribe(sub.Event) + if err != nil { + err := errors.Wrapf(err, "failed to subscribe to topic: %s", sub.Event) + return nil, nil, err + } + + sub.EventCh = eventCh + return sub, unsubFn, nil + } + } + + switch sub.Typ { + case filters.LogsSubscription: + err = es.TmWSClient.Subscribe(ctx, sub.Event) + case filters.BlocksSubscription: + err = es.TmWSClient.Subscribe(ctx, sub.Event) + case filters.PendingTransactionsSubscription: + err = es.TmWSClient.Subscribe(ctx, sub.Event) + default: + err = fmt.Errorf("invalid filter subscription type %d", sub.Typ) + } + + if err != nil { + sub.ErrCh <- err + return nil, nil, err + } + + // wrap events in a go routine to prevent blocking + es.Install <- sub + <-sub.Installed + + eventCh, unsubFn, err := es.EventBus.Subscribe(sub.Event) + if err != nil { + return nil, nil, errors.Wrapf(err, "failed to subscribe to topic after installed: %s", sub.Event) + } + + sub.EventCh = eventCh + return sub, unsubFn, nil +} + +// SubscribeLogs creates a subscription that will write all logs matching the +// given criteria to the given logs channel. Default value for the from and to +// block is "latest". If the fromBlock > toBlock an error is returned. +func (es *EventSubscriber) SubscribeLogs(crit filters.FilterCriteria) (*Subscription, pubsub.UnsubscribeFunc, error) { + var from, to gethrpc.BlockNumber + if crit.FromBlock == nil { + from = gethrpc.LatestBlockNumber + } else { + from = gethrpc.BlockNumber(crit.FromBlock.Int64()) + } + if crit.ToBlock == nil { + to = gethrpc.LatestBlockNumber + } else { + to = gethrpc.BlockNumber(crit.ToBlock.Int64()) + } + + switch { + // only interested in new mined logs, mined logs within a specific block range, or + // logs from a specific block number to new mined blocks + case (from == gethrpc.LatestBlockNumber && to == gethrpc.LatestBlockNumber), + (from >= 0 && to >= 0 && to >= from), + (from >= 0 && to == gethrpc.LatestBlockNumber): + + // Create a subscription that will write all logs matching the + // given criteria to the given logs channel. + sub := &Subscription{ + Id: gethrpc.NewID(), + Typ: filters.LogsSubscription, + Event: evmEventsQuery, + logsCrit: crit, + Created: time.Now().UTC(), + Logs: make(chan []*gethcore.Log), + Installed: make(chan struct{}, 1), + ErrCh: make(chan error, 1), + } + return es.subscribe(sub) + + default: + return nil, nil, fmt.Errorf("invalid from and to block combination: from > to (%d > %d)", from, to) + } +} + +// SubscribeNewHeads subscribes to new block headers events. +func (es EventSubscriber) SubscribeNewHeads() (*Subscription, pubsub.UnsubscribeFunc, error) { + sub := &Subscription{ + Id: gethrpc.NewID(), + Typ: filters.BlocksSubscription, + Event: headerEventsQuery, + Created: time.Now().UTC(), + Headers: make(chan *gethcore.Header), + Installed: make(chan struct{}, 1), + ErrCh: make(chan error, 1), + } + return es.subscribe(sub) +} + +// SubscribePendingTxs subscribes to new pending transactions events from the mempool. +func (es EventSubscriber) SubscribePendingTxs() (*Subscription, pubsub.UnsubscribeFunc, error) { + sub := &Subscription{ + Id: gethrpc.NewID(), + Typ: filters.PendingTransactionsSubscription, + Event: txEventsQuery, + Created: time.Now().UTC(), + Hashes: make(chan []common.Hash), + Installed: make(chan struct{}, 1), + ErrCh: make(chan error, 1), + } + return es.subscribe(sub) +} + +type FilterIndex map[filters.Type]map[gethrpc.ID]*Subscription + +// EventLoop (un)installs filters and processes mux events. +func (es *EventSubscriber) EventLoop() { + for { + select { + case f := <-es.Install: + es.IndexMux.Lock() + es.Index[f.Typ][f.Id] = f + ch := make(chan coretypes.ResultEvent) + if err := es.EventBus.AddTopic(f.Event, ch); err != nil { + es.Logger.Error("failed to add event topic to event bus", "topic", f.Event, "error", err.Error()) + } else { + es.TopicChans[f.Event] = ch + } + es.IndexMux.Unlock() + close(f.Installed) + case f := <-es.Uninstall: + es.IndexMux.Lock() + delete(es.Index[f.Typ], f.Id) + + var channelInUse bool + // #nosec G705 + for _, sub := range es.Index[f.Typ] { + if sub.Event == f.Event { + channelInUse = true + break + } + } + + // remove topic only when channel is not used by other subscriptions + if !channelInUse { + if err := es.TmWSClient.Unsubscribe(es.Ctx, f.Event); err != nil { + es.Logger.Error("failed to unsubscribe from query", "query", f.Event, "error", err.Error()) + } + + ch, ok := es.TopicChans[f.Event] + if ok { + es.EventBus.RemoveTopic(f.Event) + close(ch) + delete(es.TopicChans, f.Event) + } + } + + es.IndexMux.Unlock() + close(f.ErrCh) + } + } +} + +func (es *EventSubscriber) consumeEvents() { + for { + for rpcResp := range es.TmWSClient.ResponsesCh { + var ev coretypes.ResultEvent + + if rpcResp.Error != nil { + time.Sleep(5 * time.Second) + continue + } else if err := tmjson.Unmarshal(rpcResp.Result, &ev); err != nil { + es.Logger.Error("failed to JSON unmarshal ResponsesCh result event", "error", err.Error()) + continue + } + + if len(ev.Query) == 0 { + // skip empty responses + continue + } + + es.IndexMux.RLock() + ch, ok := es.TopicChans[ev.Query] + es.IndexMux.RUnlock() + if !ok { + es.Logger.Debug("channel for subscription not found", "topic", ev.Query) + es.Logger.Debug("list of available channels", "channels", es.EventBus.Topics()) + continue + } + + // gracefully handle lagging subscribers + t := time.NewTimer(time.Second) + select { + case <-t.C: + es.Logger.Debug("dropped event during lagging subscription", "topic", ev.Query) + case ch <- ev: + } + } + + time.Sleep(time.Second) + } +} + +func MakeSubscription(id, event string) *Subscription { + return &Subscription{ + Id: gethrpc.ID(id), + Typ: filters.LogsSubscription, + Event: event, + Created: time.Now(), + Logs: make(chan []*gethcore.Log), + Hashes: make(chan []common.Hash), + Headers: make(chan *gethcore.Header), + Installed: make(chan struct{}), + EventCh: make(chan coretypes.ResultEvent), + ErrCh: make(chan error), + } +} diff --git a/eth/rpc/rpcapi/event_subscriber_test.go b/eth/rpc/rpcapi/event_subscriber_test.go new file mode 100644 index 000000000..dc14b092e --- /dev/null +++ b/eth/rpc/rpcapi/event_subscriber_test.go @@ -0,0 +1,127 @@ +package rpcapi_test + +import ( + "context" + "os" + "sync" + "testing" + + abci "github.com/cometbft/cometbft/abci/types" + gogoproto "github.com/cosmos/gogoproto/proto" + "github.com/stretchr/testify/suite" + + "github.com/cometbft/cometbft/libs/log" + coretypes "github.com/cometbft/cometbft/rpc/core/types" + gethcore "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/eth/filters" + "github.com/ethereum/go-ethereum/rpc" + + "github.com/NibiruChain/nibiru/v2/eth" + "github.com/NibiruChain/nibiru/v2/eth/rpc/pubsub" + "github.com/NibiruChain/nibiru/v2/eth/rpc/rpcapi" + "github.com/NibiruChain/nibiru/v2/x/common/testutil" + "github.com/NibiruChain/nibiru/v2/x/evm" + "github.com/NibiruChain/nibiru/v2/x/evm/evmtest" +) + +type Suite struct { + suite.Suite +} + +func TestEventSubscriber(t *testing.T) { + index := make(rpcapi.FilterIndex) + for i := filters.UnknownSubscription; i < filters.LastIndexSubscription; i++ { + index[i] = make(map[rpc.ID]*rpcapi.Subscription) + } + es := &rpcapi.EventSubscriber{ + Logger: log.NewTMLogger(log.NewSyncWriter(os.Stdout)), + Ctx: context.Background(), + LightMode: false, + Index: index, + TopicChans: make(map[string]chan<- coretypes.ResultEvent, len(index)), + IndexMux: new(sync.RWMutex), + Install: make(chan *rpcapi.Subscription), + Uninstall: make(chan *rpcapi.Subscription), + EventBus: pubsub.NewEventBus(), + } + go es.EventLoop() + + event := "event" + sub := rpcapi.MakeSubscription("1", event) + es.Install <- sub + <-sub.Installed + ch, ok := es.TopicChans[sub.Event] + if !ok { + t.Error("expect topic channel exist") + } + + sub = rpcapi.MakeSubscription("2", event) + es.Install <- sub + <-sub.Installed + newCh, ok := es.TopicChans[sub.Event] + if !ok { + t.Error("expect topic channel exist") + } + + if newCh != ch { + t.Error("expect topic channel unchanged") + } +} + +func (s *Suite) TestParseBloomFromEvents() { + for _, tc := range []struct { + name string + endBlockEvents func() (gethcore.Bloom, []abci.Event) + wantErr string + }{ + { + name: "happy: empty events", + endBlockEvents: func() (gethcore.Bloom, []abci.Event) { + return *new(gethcore.Bloom), []abci.Event{} + }, + wantErr: "", + }, + { + name: "happy: events with bloom included", + endBlockEvents: func() (gethcore.Bloom, []abci.Event) { + deps := evmtest.NewTestDeps() + + // populate valid bloom + bloom := gethcore.Bloom{} + dummyBz := []byte("dummybloom") + copy(bloom[:], dummyBz) + + err := deps.Ctx.EventManager().EmitTypedEvents( + &evm.EventTransfer{}, + &evm.EventBlockBloom{ + Bloom: eth.BloomToHex(bloom), + }, + ) + s.NoError(err, "emitting bloom event failed") + + abciEvents := deps.Ctx.EventManager().ABCIEvents() + + bloomEvent := new(evm.EventBlockBloom) + bloomEventType := gogoproto.MessageName(bloomEvent) + + err = testutil.AssertEventPresent(deps.Ctx.EventManager().Events(), bloomEventType) + s.Require().NoError(err) + + return bloom, abciEvents + }, + wantErr: "", + }, + } { + s.Run(tc.name, func() { + wantBloom, events := tc.endBlockEvents() + bloom, err := rpcapi.ParseBloomFromEvents(events) + + if tc.wantErr != "" { + s.Require().ErrorContains(err, tc.wantErr) + return + } + + s.Require().Equal(wantBloom, bloom) + }) + } +} diff --git a/eth/rpc/rpcapi/filtersapi/utils.go b/eth/rpc/rpcapi/filter_utils.go similarity index 73% rename from eth/rpc/rpcapi/filtersapi/utils.go rename to eth/rpc/rpcapi/filter_utils.go index 47d27de2e..53e7c609d 100644 --- a/eth/rpc/rpcapi/filtersapi/utils.go +++ b/eth/rpc/rpcapi/filter_utils.go @@ -1,11 +1,20 @@ // Copyright (c) 2023-2024 Nibi, Inc. -package filtersapi +package rpcapi import ( "math/big" + "cosmossdk.io/errors" + abci "github.com/cometbft/cometbft/abci/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" gethcore "github.com/ethereum/go-ethereum/core/types" + + gogoproto "github.com/cosmos/gogoproto/proto" + + "github.com/NibiruChain/nibiru/v2/eth" + "github.com/NibiruChain/nibiru/v2/x/evm" ) // FilterLogs creates a slice of logs matching the given criteria. @@ -105,3 +114,27 @@ func returnLogs(logs []*gethcore.Log) []*gethcore.Log { } return logs } + +// ParseBloomFromEvents iterates through the slice of events +func ParseBloomFromEvents(events []abci.Event) (bloom gethcore.Bloom, err error) { + bloomEvent := new(evm.EventBlockBloom) + bloomEventType := gogoproto.MessageName(bloomEvent) + for _, event := range events { + if event.Type != bloomEventType { + continue + } + typedProtoEvent, err := sdk.ParseTypedEvent(event) + if err != nil { + return bloom, errors.Wrapf( + err, "failed to parse event of type %s", bloomEventType) + } + bloomEvent, ok := (typedProtoEvent).(*evm.EventBlockBloom) + if !ok { + return bloom, errors.Wrapf( + err, "failed to parse event of type %s", bloomEventType) + } + + return eth.BloomFromHex(bloomEvent.Bloom) + } + return bloom, err +} diff --git a/eth/rpc/rpcapi/filtersapi/filters.go b/eth/rpc/rpcapi/filters.go similarity index 93% rename from eth/rpc/rpcapi/filtersapi/filters.go rename to eth/rpc/rpcapi/filters.go index e49d61e57..850e7fdfd 100644 --- a/eth/rpc/rpcapi/filtersapi/filters.go +++ b/eth/rpc/rpcapi/filters.go @@ -1,5 +1,5 @@ // Copyright (c) 2023-2024 Nibi, Inc. -package filtersapi +package rpcapi import ( "context" @@ -8,7 +8,7 @@ import ( "math/big" "github.com/NibiruChain/nibiru/v2/eth/rpc" - "github.com/NibiruChain/nibiru/v2/eth/rpc/backend" + rpcbackend "github.com/NibiruChain/nibiru/v2/eth/rpc/backend" "github.com/cometbft/cometbft/libs/log" tmrpctypes "github.com/cometbft/cometbft/rpc/core/types" @@ -30,7 +30,7 @@ type BloomIV struct { // Filter can be used to retrieve and filter logs. type Filter struct { logger log.Logger - backend IFilterEthBackend + backend rpcbackend.Backend criteria filters.FilterCriteria bloomFilters [][]BloomIV // Filter the system is matching for @@ -38,14 +38,14 @@ type Filter struct { // NewBlockFilter creates a new filter which directly inspects the contents of // a block to figure out whether it is interesting or not. -func NewBlockFilter(logger log.Logger, backend IFilterEthBackend, criteria filters.FilterCriteria) *Filter { +func NewBlockFilter(logger log.Logger, backend rpcbackend.Backend, criteria filters.FilterCriteria) *Filter { // Create a generic filter and convert it into a block filter return newFilter(logger, backend, criteria, nil) } // NewRangeFilter creates a new filter which uses a bloom filter on blocks to // figure out whether a particular block is interesting or not. -func NewRangeFilter(logger log.Logger, backend IFilterEthBackend, begin, end int64, addresses []common.Address, topics [][]common.Hash) *Filter { +func NewRangeFilter(logger log.Logger, backend rpcbackend.Backend, begin, end int64, addresses []common.Address, topics [][]common.Hash) *Filter { // Flatten the address and topic filter clauses into a single bloombits filter // system. Since the bloombits are not positional, nil topics are permitted, // which get flattened into a nil byte slice. @@ -78,7 +78,12 @@ func NewRangeFilter(logger log.Logger, backend IFilterEthBackend, begin, end int } // newFilter returns a new Filter -func newFilter(logger log.Logger, backend IFilterEthBackend, criteria filters.FilterCriteria, bloomFilters [][]BloomIV) *Filter { +func newFilter( + logger log.Logger, + backend rpcbackend.Backend, + criteria filters.FilterCriteria, + bloomFilters [][]BloomIV, +) *Filter { return &Filter{ logger: logger, backend: backend, @@ -187,7 +192,7 @@ func (f *Filter) blockLogs(blockRes *tmrpctypes.ResultBlockResults, bloom gethco return []*gethcore.Log{}, nil } - logsList, err := backend.GetLogsFromBlockResults(blockRes) + logsList, err := rpcbackend.GetLogsFromBlockResults(blockRes) if err != nil { return []*gethcore.Log{}, errors.Wrapf(err, "failed to fetch logs block number %d", blockRes.Height) } diff --git a/eth/rpc/rpcapi/filtersapi/filter_system.go b/eth/rpc/rpcapi/filtersapi/filter_system.go deleted file mode 100644 index d54e1afb7..000000000 --- a/eth/rpc/rpcapi/filtersapi/filter_system.go +++ /dev/null @@ -1,311 +0,0 @@ -// Copyright (c) 2023-2024 Nibi, Inc. -package filtersapi - -import ( - "context" - "fmt" - "sync" - "time" - - "github.com/pkg/errors" - - tmjson "github.com/cometbft/cometbft/libs/json" - "github.com/cometbft/cometbft/libs/log" - tmquery "github.com/cometbft/cometbft/libs/pubsub/query" - coretypes "github.com/cometbft/cometbft/rpc/core/types" - rpcclient "github.com/cometbft/cometbft/rpc/jsonrpc/client" - tmtypes "github.com/cometbft/cometbft/types" - - "github.com/ethereum/go-ethereum/common" - gethcore "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/eth/filters" - gethrpc "github.com/ethereum/go-ethereum/rpc" - - sdk "github.com/cosmos/cosmos-sdk/types" - - "github.com/NibiruChain/nibiru/v2/eth/rpc/pubsub" - "github.com/NibiruChain/nibiru/v2/x/evm" -) - -var ( - txEvents = tmtypes.QueryForEvent(tmtypes.EventTx).String() - evmEvents = tmquery.MustParse(fmt.Sprintf("%s='%s' AND %s.%s='%s'", - tmtypes.EventTypeKey, - tmtypes.EventTx, - sdk.EventTypeMessage, - sdk.AttributeKeyModule, evm.ModuleName)).String() - headerEvents = tmtypes.QueryForEvent(tmtypes.EventNewBlockHeader).String() -) - -// EventSystem creates subscriptions, processes events and broadcasts them to the -// subscription which match the subscription criteria using the Tendermint's RPC client. -type EventSystem struct { - logger log.Logger - ctx context.Context - tmWSClient *rpcclient.WSClient - - // light client mode - lightMode bool - - index filterIndex - topicChans map[string]chan<- coretypes.ResultEvent - indexMux *sync.RWMutex - - // Channels - install chan *Subscription // install filter for event notification - uninstall chan *Subscription // remove filter for event notification - eventBus pubsub.EventBus -} - -// NewEventSystem creates a new manager that listens for event on the given mux, -// parses and filters them. It uses the all map to retrieve filter changes. The -// work loop holds its own index that is used to forward events to filters. -// -// The returned manager has a loop that needs to be stopped with the Stop function -// or by stopping the given mux. -func NewEventSystem(logger log.Logger, tmWSClient *rpcclient.WSClient) *EventSystem { - index := make(filterIndex) - for i := filters.UnknownSubscription; i < filters.LastIndexSubscription; i++ { - index[i] = make(map[gethrpc.ID]*Subscription) - } - - es := &EventSystem{ - logger: logger, - ctx: context.Background(), - tmWSClient: tmWSClient, - lightMode: false, - index: index, - topicChans: make(map[string]chan<- coretypes.ResultEvent, len(index)), - indexMux: new(sync.RWMutex), - install: make(chan *Subscription), - uninstall: make(chan *Subscription), - eventBus: pubsub.NewEventBus(), - } - - go es.eventLoop() - go es.consumeEvents() - return es -} - -// WithContext sets a new context to the EventSystem. This is required to set a timeout context when -// a new filter is intantiated. -func (es *EventSystem) WithContext(ctx context.Context) { - es.ctx = ctx -} - -// subscribe performs a new event subscription to a given Tendermint event. -// The subscription creates a unidirectional receive event channel to receive the ResultEvent. -func (es *EventSystem) subscribe(sub *Subscription) (*Subscription, pubsub.UnsubscribeFunc, error) { - var ( - err error - cancelFn context.CancelFunc - ) - - ctx, cancelFn := context.WithCancel(context.Background()) - defer cancelFn() - - existingSubs := es.eventBus.Topics() - for _, topic := range existingSubs { - if topic == sub.event { - eventCh, unsubFn, err := es.eventBus.Subscribe(sub.event) - if err != nil { - err := errors.Wrapf(err, "failed to subscribe to topic: %s", sub.event) - return nil, nil, err - } - - sub.eventCh = eventCh - return sub, unsubFn, nil - } - } - - switch sub.typ { - case filters.LogsSubscription: - err = es.tmWSClient.Subscribe(ctx, sub.event) - case filters.BlocksSubscription: - err = es.tmWSClient.Subscribe(ctx, sub.event) - case filters.PendingTransactionsSubscription: - err = es.tmWSClient.Subscribe(ctx, sub.event) - default: - err = fmt.Errorf("invalid filter subscription type %d", sub.typ) - } - - if err != nil { - sub.err <- err - return nil, nil, err - } - - // wrap events in a go routine to prevent blocking - es.install <- sub - <-sub.installed - - eventCh, unsubFn, err := es.eventBus.Subscribe(sub.event) - if err != nil { - return nil, nil, errors.Wrapf(err, "failed to subscribe to topic after installed: %s", sub.event) - } - - sub.eventCh = eventCh - return sub, unsubFn, nil -} - -// SubscribeLogs creates a subscription that will write all logs matching the -// given criteria to the given logs channel. Default value for the from and to -// block is "latest". If the fromBlock > toBlock an error is returned. -func (es *EventSystem) SubscribeLogs(crit filters.FilterCriteria) (*Subscription, pubsub.UnsubscribeFunc, error) { - var from, to gethrpc.BlockNumber - if crit.FromBlock == nil { - from = gethrpc.LatestBlockNumber - } else { - from = gethrpc.BlockNumber(crit.FromBlock.Int64()) - } - if crit.ToBlock == nil { - to = gethrpc.LatestBlockNumber - } else { - to = gethrpc.BlockNumber(crit.ToBlock.Int64()) - } - - switch { - // only interested in new mined logs, mined logs within a specific block range, or - // logs from a specific block number to new mined blocks - case (from == gethrpc.LatestBlockNumber && to == gethrpc.LatestBlockNumber), - (from >= 0 && to >= 0 && to >= from), - (from >= 0 && to == gethrpc.LatestBlockNumber): - return es.subscribeLogs(crit) - - default: - return nil, nil, fmt.Errorf("invalid from and to block combination: from > to (%d > %d)", from, to) - } -} - -// subscribeLogs creates a subscription that will write all logs matching the -// given criteria to the given logs channel. -func (es *EventSystem) subscribeLogs(crit filters.FilterCriteria) (*Subscription, pubsub.UnsubscribeFunc, error) { - sub := &Subscription{ - id: gethrpc.NewID(), - typ: filters.LogsSubscription, - event: evmEvents, - logsCrit: crit, - created: time.Now().UTC(), - logs: make(chan []*gethcore.Log), - installed: make(chan struct{}, 1), - err: make(chan error, 1), - } - return es.subscribe(sub) -} - -// SubscribeNewHeads subscribes to new block headers events. -func (es EventSystem) SubscribeNewHeads() (*Subscription, pubsub.UnsubscribeFunc, error) { - sub := &Subscription{ - id: gethrpc.NewID(), - typ: filters.BlocksSubscription, - event: headerEvents, - created: time.Now().UTC(), - headers: make(chan *gethcore.Header), - installed: make(chan struct{}, 1), - err: make(chan error, 1), - } - return es.subscribe(sub) -} - -// SubscribePendingTxs subscribes to new pending transactions events from the mempool. -func (es EventSystem) SubscribePendingTxs() (*Subscription, pubsub.UnsubscribeFunc, error) { - sub := &Subscription{ - id: gethrpc.NewID(), - typ: filters.PendingTransactionsSubscription, - event: txEvents, - created: time.Now().UTC(), - hashes: make(chan []common.Hash), - installed: make(chan struct{}, 1), - err: make(chan error, 1), - } - return es.subscribe(sub) -} - -type filterIndex map[filters.Type]map[gethrpc.ID]*Subscription - -// eventLoop (un)installs filters and processes mux events. -func (es *EventSystem) eventLoop() { - for { - select { - case f := <-es.install: - es.indexMux.Lock() - es.index[f.typ][f.id] = f - ch := make(chan coretypes.ResultEvent) - if err := es.eventBus.AddTopic(f.event, ch); err != nil { - es.logger.Error("failed to add event topic to event bus", "topic", f.event, "error", err.Error()) - } else { - es.topicChans[f.event] = ch - } - es.indexMux.Unlock() - close(f.installed) - case f := <-es.uninstall: - es.indexMux.Lock() - delete(es.index[f.typ], f.id) - - var channelInUse bool - // #nosec G705 - for _, sub := range es.index[f.typ] { - if sub.event == f.event { - channelInUse = true - break - } - } - - // remove topic only when channel is not used by other subscriptions - if !channelInUse { - if err := es.tmWSClient.Unsubscribe(es.ctx, f.event); err != nil { - es.logger.Error("failed to unsubscribe from query", "query", f.event, "error", err.Error()) - } - - ch, ok := es.topicChans[f.event] - if ok { - es.eventBus.RemoveTopic(f.event) - close(ch) - delete(es.topicChans, f.event) - } - } - - es.indexMux.Unlock() - close(f.err) - } - } -} - -func (es *EventSystem) consumeEvents() { - for { - for rpcResp := range es.tmWSClient.ResponsesCh { - var ev coretypes.ResultEvent - - if rpcResp.Error != nil { - time.Sleep(5 * time.Second) - continue - } else if err := tmjson.Unmarshal(rpcResp.Result, &ev); err != nil { - es.logger.Error("failed to JSON unmarshal ResponsesCh result event", "error", err.Error()) - continue - } - - if len(ev.Query) == 0 { - // skip empty responses - continue - } - - es.indexMux.RLock() - ch, ok := es.topicChans[ev.Query] - es.indexMux.RUnlock() - if !ok { - es.logger.Debug("channel for subscription not found", "topic", ev.Query) - es.logger.Debug("list of available channels", "channels", es.eventBus.Topics()) - continue - } - - // gracefully handle lagging subscribers - t := time.NewTimer(time.Second) - select { - case <-t.C: - es.logger.Debug("dropped event during lagging subscription", "topic", ev.Query) - case ch <- ev: - } - } - - time.Sleep(time.Second) - } -} diff --git a/eth/rpc/rpcapi/filtersapi/filter_system_test.go b/eth/rpc/rpcapi/filtersapi/filter_system_test.go deleted file mode 100644 index cd887c897..000000000 --- a/eth/rpc/rpcapi/filtersapi/filter_system_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package filtersapi - -import ( - "context" - "os" - "sync" - "testing" - "time" - - "github.com/cometbft/cometbft/libs/log" - coretypes "github.com/cometbft/cometbft/rpc/core/types" - "github.com/ethereum/go-ethereum/common" - gethcore "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/eth/filters" - "github.com/ethereum/go-ethereum/rpc" - - "github.com/NibiruChain/nibiru/v2/eth/rpc/pubsub" -) - -func makeSubscription(id, event string) *Subscription { - return &Subscription{ - id: rpc.ID(id), - typ: filters.LogsSubscription, - event: event, - created: time.Now(), - logs: make(chan []*gethcore.Log), - hashes: make(chan []common.Hash), - headers: make(chan *gethcore.Header), - installed: make(chan struct{}), - eventCh: make(chan coretypes.ResultEvent), - err: make(chan error), - } -} - -func TestFilterSystem(t *testing.T) { - index := make(filterIndex) - for i := filters.UnknownSubscription; i < filters.LastIndexSubscription; i++ { - index[i] = make(map[rpc.ID]*Subscription) - } - es := &EventSystem{ - logger: log.NewTMLogger(log.NewSyncWriter(os.Stdout)), - ctx: context.Background(), - lightMode: false, - index: index, - topicChans: make(map[string]chan<- coretypes.ResultEvent, len(index)), - indexMux: new(sync.RWMutex), - install: make(chan *Subscription), - uninstall: make(chan *Subscription), - eventBus: pubsub.NewEventBus(), - } - go es.eventLoop() - - event := "event" - sub := makeSubscription("1", event) - es.install <- sub - <-sub.installed - ch, ok := es.topicChans[sub.event] - if !ok { - t.Error("expect topic channel exist") - } - - sub = makeSubscription("2", event) - es.install <- sub - <-sub.installed - newCh, ok := es.topicChans[sub.event] - if !ok { - t.Error("expect topic channel exist") - } - - if newCh != ch { - t.Error("expect topic channel unchanged") - } -} diff --git a/eth/rpc/rpcapi/net_api_test.go b/eth/rpc/rpcapi/net_api_test.go index 2803a7e15..6b54f5094 100644 --- a/eth/rpc/rpcapi/net_api_test.go +++ b/eth/rpc/rpcapi/net_api_test.go @@ -4,7 +4,7 @@ import ( "github.com/NibiruChain/nibiru/v2/app/appconst" ) -func (s *TestSuite) TestNetNamespace() { +func (s *NodeSuite) TestNetNamespace() { api := s.val.EthRpc_NET s.Require().True(api.Listening()) s.EqualValues( diff --git a/eth/rpc/rpcapi/filtersapi/subscription.go b/eth/rpc/rpcapi/subscription.go similarity index 59% rename from eth/rpc/rpcapi/filtersapi/subscription.go rename to eth/rpc/rpcapi/subscription.go index fc2ac0f91..3912216ff 100644 --- a/eth/rpc/rpcapi/filtersapi/subscription.go +++ b/eth/rpc/rpcapi/subscription.go @@ -1,5 +1,5 @@ // Copyright (c) 2023-2024 Nibi, Inc. -package filtersapi +package rpcapi import ( "time" @@ -13,27 +13,28 @@ import ( // Subscription defines a wrapper for the private subscription type Subscription struct { - id rpc.ID - typ filters.Type - event string - created time.Time + Id rpc.ID + Typ filters.Type + Event string + Created time.Time logsCrit filters.FilterCriteria - logs chan []*gethcore.Log - hashes chan []common.Hash - headers chan *gethcore.Header - installed chan struct{} // closed when the filter is installed - eventCh <-chan coretypes.ResultEvent - err chan error + Logs chan []*gethcore.Log + Hashes chan []common.Hash + Headers chan *gethcore.Header + Installed chan struct{} // closed when the filter is installed + // Consensus result event channel + EventCh <-chan coretypes.ResultEvent + ErrCh chan error } // ID returns the underlying subscription RPC identifier. func (s Subscription) ID() rpc.ID { - return s.id + return s.Id } // Unsubscribe from the current subscription to Tendermint Websocket. It sends an error to the // subscription error channel if unsubscribe fails. -func (s *Subscription) Unsubscribe(es *EventSystem) { +func (s *Subscription) Unsubscribe(es *EventSubscriber) { go func() { uninstallLoop: for { @@ -42,22 +43,17 @@ func (s *Subscription) Unsubscribe(es *EventSystem) { // filter event channel while the subscription loop is waiting for // this method to return (and thus not reading these events). select { - case es.uninstall <- s: + case es.Uninstall <- s: break uninstallLoop - case <-s.logs: - case <-s.hashes: - case <-s.headers: + case <-s.Logs: + case <-s.Hashes: + case <-s.Headers: } } }() } -// Err returns the error channel -func (s *Subscription) Err() <-chan error { - return s.err -} - -// Event returns the tendermint result event channel -func (s *Subscription) Event() <-chan coretypes.ResultEvent { - return s.eventCh +// Error returns the error channel +func (s *Subscription) Error() <-chan error { + return s.ErrCh } diff --git a/eth/rpc/rpcapi/websockets.go b/eth/rpc/rpcapi/websockets.go index e9447acd8..a1af04dd4 100644 --- a/eth/rpc/rpcapi/websockets.go +++ b/eth/rpc/rpcapi/websockets.go @@ -33,7 +33,6 @@ import ( "github.com/NibiruChain/nibiru/v2/app/server/config" "github.com/NibiruChain/nibiru/v2/eth/rpc" "github.com/NibiruChain/nibiru/v2/eth/rpc/pubsub" - rpcfilters "github.com/NibiruChain/nibiru/v2/eth/rpc/rpcapi/filtersapi" "github.com/NibiruChain/nibiru/v2/x/evm" ) @@ -348,7 +347,7 @@ func (s *websocketsServer) tcpGetAndSendResponse(wsConn *wsConn, mb []byte) erro // pubSubAPI is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec type pubSubAPI struct { - events *rpcfilters.EventSystem + events *EventSubscriber logger log.Logger clientCtx client.Context } @@ -357,7 +356,7 @@ type pubSubAPI struct { func newPubSubAPI(clientCtx client.Context, logger log.Logger, tmWSClient *rpcclient.WSClient) *pubSubAPI { logger = logger.With("module", "websocket-client") return &pubSubAPI{ - events: rpcfilters.NewEventSystem(logger, tmWSClient), + events: NewEventSubscriber(logger, tmWSClient), logger: logger, clientCtx: clientCtx, } @@ -397,8 +396,8 @@ func (api *pubSubAPI) subscribeNewHeads(wsConn *wsConn, subID gethrpc.ID) (pubsu baseFee := big.NewInt(params.InitialBaseFee) go func() { - headersCh := sub.Event() - errCh := sub.Err() + headersCh := sub.EventCh + errCh := sub.Error() for { select { case event, ok := <-headersCh: @@ -570,8 +569,8 @@ func (api *pubSubAPI) subscribeLogs(wsConn *wsConn, subID gethrpc.ID, extra inte } go func() { - ch := sub.Event() - errCh := sub.Err() + ch := sub.EventCh + errCh := sub.Error() for { select { case event, ok := <-ch: @@ -591,7 +590,7 @@ func (api *pubSubAPI) subscribeLogs(wsConn *wsConn, subID gethrpc.ID, extra inte return } - logs := rpcfilters.FilterLogs(evm.LogsToEthereum(txResponse.Logs), crit.FromBlock, crit.ToBlock, crit.Addresses, crit.Topics) + logs := FilterLogs(evm.LogsToEthereum(txResponse.Logs), crit.FromBlock, crit.ToBlock, crit.Addresses, crit.Topics) if len(logs) == 0 { continue } @@ -634,8 +633,8 @@ func (api *pubSubAPI) subscribePendingTransactions(wsConn *wsConn, subID gethrpc } go func() { - txsCh := sub.Event() - errCh := sub.Err() + txsCh := sub.EventCh + errCh := sub.Error() for { select { case ev := <-txsCh: diff --git a/eth/stringify.go b/eth/stringify.go new file mode 100644 index 000000000..2a2775213 --- /dev/null +++ b/eth/stringify.go @@ -0,0 +1,35 @@ +package eth + +import ( + "encoding/hex" + fmt "fmt" + + gethcommon "github.com/ethereum/go-ethereum/common" + gethcore "github.com/ethereum/go-ethereum/core/types" +) + +// TmTxHashToString returns the consensus transaction hash as a string. +// Transactions are hex-encoded and capitlized. +// Reference: Tx.String function from comet-bft/types/tx.go +func TmTxHashToString(tmTxHash []byte) string { + return fmt.Sprintf("%X", tmTxHash) +} + +// EthTxHashToString returns the EVM transaction hash as a string. +func EthTxHashToString(hash gethcommon.Hash) string { + return hash.Hex() +} + +// BloomToHex returns the bloom filter as a string. +func BloomToHex(bloom gethcore.Bloom) string { + return BytesToHex(bloom.Bytes()) +} + +// BloomFromHex converts a hex-encoded bloom filter to a gethcore.Bloom. +func BloomFromHex(bloomHex string) (gethcore.Bloom, error) { + bloomBz, err := hex.DecodeString(bloomHex) + if err != nil { + return gethcore.Bloom{}, fmt.Errorf("could not construct bloom: %w", err) + } + return gethcore.BytesToBloom(bloomBz), nil +} diff --git a/eth/stringify_test.go b/eth/stringify_test.go new file mode 100644 index 000000000..4e826126b --- /dev/null +++ b/eth/stringify_test.go @@ -0,0 +1,23 @@ +package eth_test + +import ( + gethcommon "github.com/ethereum/go-ethereum/common" + gethcore "github.com/ethereum/go-ethereum/core/types" + + "github.com/NibiruChain/nibiru/v2/eth" +) + +func (s *Suite) TestStringify() { + testCases := []gethcore.Bloom{ + gethcore.BytesToBloom([]byte("alphanumeric123")), + gethcore.BytesToBloom([]byte{}), + gethcore.BytesToBloom(gethcommon.Big0.Bytes()), + gethcore.BytesToBloom(gethcommon.Big1.Bytes()), + } + for tcIdx, bloom := range testCases { + gotStr := eth.BloomToHex(bloom) + gotBloom, err := eth.BloomFromHex(gotStr) + s.NoError(err) + s.Equalf(bloom, gotBloom, "test case: %d", tcIdx) + } +} diff --git a/x/common/address.go b/x/common/address.go index 67bf644d5..61ca416d3 100644 --- a/x/common/address.go +++ b/x/common/address.go @@ -19,6 +19,7 @@ func StringsToAddrs(strs ...string) []sdk.AccAddress { addr := sdk.MustAccAddressFromBech32(str) addrs = append(addrs, addr) } + return addrs } diff --git a/x/evm/cli/query.go b/x/evm/cli/query.go index 349ee7bb2..545b683d2 100644 --- a/x/evm/cli/query.go +++ b/x/evm/cli/query.go @@ -93,8 +93,6 @@ func CmdQueryAccount() *cobra.Command { } isBech32, err := req.Validate() - fmt.Printf("TODO: UD-DEBUG: req.String(): %v\n", req.String()) - fmt.Printf("TODO: UD-DEBUG: err: %v\n", err) if err != nil { return err } diff --git a/x/evm/evmtest/tx.go b/x/evm/evmtest/tx.go index d75efc860..107a15593 100644 --- a/x/evm/evmtest/tx.go +++ b/x/evm/evmtest/tx.go @@ -184,7 +184,7 @@ func DeployContract( // DeployAndExecuteERC20Transfer deploys contract, executes transfer and returns tx hash func DeployAndExecuteERC20Transfer( deps *TestDeps, t *testing.T, -) (*evm.MsgEthereumTx, []*evm.MsgEthereumTx) { +) (erc20Transfer *evm.MsgEthereumTx, predecessors []*evm.MsgEthereumTx) { // TX 1: Deploy ERC-20 contract deployResp, err := DeployContract(deps, embeds.SmartContract_TestERC20) require.NoError(t, err) @@ -194,7 +194,7 @@ func DeployAndExecuteERC20Transfer( // Contract address is deterministic contractAddress := crypto.CreateAddress(deps.Sender.EthAddr, nonce) deps.App.Commit() - predecessors := []*evm.MsgEthereumTx{ + predecessors = []*evm.MsgEthereumTx{ deployResp.EthTxMsg, } @@ -210,14 +210,14 @@ func DeployAndExecuteERC20Transfer( Nonce: (*hexutil.Uint64)(&nonce), Data: (*hexutil.Bytes)(&input), } - ethTxMsg, err := GenerateAndSignEthTxMsg(txArgs, deps) + erc20Transfer, err = GenerateAndSignEthTxMsg(txArgs, deps) require.NoError(t, err) - resp, err := deps.App.EvmKeeper.EthereumTx(sdk.WrapSDKContext(deps.Ctx), ethTxMsg) + resp, err := deps.App.EvmKeeper.EthereumTx(sdk.WrapSDKContext(deps.Ctx), erc20Transfer) require.NoError(t, err) require.Empty(t, resp.VmError) - return ethTxMsg, predecessors + return erc20Transfer, predecessors } // GenerateAndSignEthTxMsg estimates gas, sets gas limit and sings the tx diff --git a/x/evm/keeper/hooks.go b/x/evm/keeper/hooks.go index 177708d64..f42c5ae1e 100644 --- a/x/evm/keeper/hooks.go +++ b/x/evm/keeper/hooks.go @@ -20,7 +20,7 @@ func (k *Keeper) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.Valida ctx = ctx.WithGasMeter(sdk.NewInfiniteGasMeter()) bloom := gethcoretypes.BytesToBloom(k.EvmState.GetBlockBloomTransient(ctx).Bytes()) _ = ctx.EventManager().EmitTypedEvent(&evm.EventBlockBloom{ - Bloom: eth.BytesToHex(bloom.Bytes()), + Bloom: eth.BloomToHex(bloom), }) // The bloom logic doesn't update the validator set. return []abci.ValidatorUpdate{} diff --git a/x/evm/keeper/msg_server.go b/x/evm/keeper/msg_server.go index f2cb75e95..10fdfb131 100644 --- a/x/evm/keeper/msg_server.go +++ b/x/evm/keeper/msg_server.go @@ -50,6 +50,9 @@ func (k *Keeper) EthereumTx( sdk.NewAttribute(evm.AttributeKeyTxIndex, strconv.FormatUint(k.EvmState.BlockTxIndex.GetOr(ctx, 0), 10)), // add event for eth tx gas used, we can't get it from cosmos tx result when it contains multiple eth tx msgs. sdk.NewAttribute(evm.AttributeKeyTxGasUsed, strconv.FormatUint(resp.GasUsed, 10)), + // TODO: fix: It's odd that each event is emitted twice. Migrate to typed + // events and change EVM indexer to align. + // sdk.NewAttribute("emitted_from", "EthereumTx"), } if len(ctx.TxBytes()) > 0 { From 5085dad7e03a879fa04e8f73045682aed0265f97 Mon Sep 17 00:00:00 2001 From: Unique Divine <51418232+Unique-Divine@users.noreply.github.com> Date: Sat, 14 Sep 2024 12:51:20 +0900 Subject: [PATCH 04/16] refactor(rpc-backend): remove unnecessary interface code (#2039) --- CHANGELOG.md | 1 + eth/rpc/backend/backend.go | 132 ++++----------------------------- eth/rpc/rpcapi/debugapi/api.go | 4 +- eth/rpc/rpcapi/eth_api.go | 4 +- 4 files changed, 19 insertions(+), 122 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96dae1703..26d1f96b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -119,6 +119,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#2023](https://github.com/NibiruChain/nibiru/pull/2023) - fix(evm)!: adjusted generation and parsing of the block bloom events - [#2030](https://github.com/NibiruChain/nibiru/pull/2030) - refactor(eth/rpc): Delete unused code and improve logging in the eth and debug namespaces - [#2031](https://github.com/NibiruChain/nibiru/pull/2031) - fix(evm): debug calls with custom tracer and tracer options +- [#2039](https://github.com/NibiruChain/nibiru/pull/2039) - refactor(rpc-backend): remove unnecessary interface code #### Dapp modules: perp, spot, oracle, etc diff --git a/eth/rpc/backend/backend.go b/eth/rpc/backend/backend.go index aecadec75..9f3ad79ba 100644 --- a/eth/rpc/backend/backend.go +++ b/eth/rpc/backend/backend.go @@ -4,134 +4,20 @@ package backend import ( "context" "math/big" - "time" "github.com/cometbft/cometbft/libs/log" - tmrpctypes "github.com/cometbft/cometbft/rpc/core/types" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/server" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - gethcore "github.com/ethereum/go-ethereum/core/types" - params "github.com/ethereum/go-ethereum/params" - gethrpc "github.com/ethereum/go-ethereum/rpc" - "github.com/ethereum/go-ethereum/signer/core/apitypes" "github.com/NibiruChain/nibiru/v2/app/server/config" "github.com/NibiruChain/nibiru/v2/eth" "github.com/NibiruChain/nibiru/v2/eth/rpc" - "github.com/NibiruChain/nibiru/v2/x/evm" ) -// BackendI implements the Cosmos and EVM backend. -type BackendI interface { //nolint: revive - CosmosBackend - EVMBackend -} - -// CosmosBackend: Currently unused. Backend functionality for the shared -// "cosmos" RPC namespace. Implements [BackendI] in combination with [EVMBackend]. -// TODO: feat(eth): Implement the cosmos JSON-RPC defined by Wallet Connect V2: -// https://docs.walletconnect.com/2.0/json-rpc/cosmos. -type CosmosBackend interface { - // TODO: GetAccounts() - // TODO: SignDirect() - // TODO: SignAmino() -} - -// EVMBackend implements the functionality shared within ethereum namespaces -// as defined by EIP-1474: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1474.md -// Implemented by Backend. -type EVMBackend interface { - // Node specific queries - Accounts() ([]common.Address, error) - Syncing() (interface{}, error) - RPCGasCap() uint64 // global gas cap for eth_call over rpc: DoS protection - RPCEVMTimeout() time.Duration // global timeout for eth_call over rpc: DoS protection - RPCTxFeeCap() float64 // RPCTxFeeCap is the global transaction fee(price * gaslimit) cap for send-transaction variants. The unit is ether. - RPCMinGasPrice() int64 - - // Sign Tx - Sign(address common.Address, data hexutil.Bytes) (hexutil.Bytes, error) - SendTransaction(args evm.JsonTxArgs) (common.Hash, error) - SignTypedData(address common.Address, typedData apitypes.TypedData) (hexutil.Bytes, error) - - // Blocks Info - BlockNumber() (hexutil.Uint64, error) - GetBlockByNumber(blockNum rpc.BlockNumber, fullTx bool) (map[string]interface{}, error) - GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) - GetBlockTransactionCountByHash(hash common.Hash) *hexutil.Uint - GetBlockTransactionCountByNumber(blockNum rpc.BlockNumber) *hexutil.Uint - TendermintBlockByNumber(blockNum rpc.BlockNumber) (*tmrpctypes.ResultBlock, error) - TendermintBlockResultByNumber(height *int64) (*tmrpctypes.ResultBlockResults, error) - TendermintBlockByHash(blockHash common.Hash) (*tmrpctypes.ResultBlock, error) - BlockNumberFromTendermint(blockNrOrHash rpc.BlockNumberOrHash) (rpc.BlockNumber, error) - BlockNumberFromTendermintByHash(blockHash common.Hash) (*big.Int, error) - EthMsgsFromTendermintBlock(block *tmrpctypes.ResultBlock, blockRes *tmrpctypes.ResultBlockResults) []*evm.MsgEthereumTx - BlockBloom(blockRes *tmrpctypes.ResultBlockResults) (gethcore.Bloom, error) - HeaderByNumber(blockNum rpc.BlockNumber) (*gethcore.Header, error) - HeaderByHash(blockHash common.Hash) (*gethcore.Header, error) - RPCBlockFromTendermintBlock(resBlock *tmrpctypes.ResultBlock, blockRes *tmrpctypes.ResultBlockResults, fullTx bool) (map[string]interface{}, error) - EthBlockByNumber(blockNum rpc.BlockNumber) (*gethcore.Block, error) - EthBlockFromTendermintBlock(resBlock *tmrpctypes.ResultBlock, blockRes *tmrpctypes.ResultBlockResults) (*gethcore.Block, error) - - // Account Info - GetCode(address common.Address, blockNrOrHash rpc.BlockNumberOrHash) (hexutil.Bytes, error) - GetBalance(address common.Address, blockNrOrHash rpc.BlockNumberOrHash) (*hexutil.Big, error) - GetStorageAt(address common.Address, key string, blockNrOrHash rpc.BlockNumberOrHash) (hexutil.Bytes, error) - GetProof(address common.Address, storageKeys []string, blockNrOrHash rpc.BlockNumberOrHash) (*rpc.AccountResult, error) - GetTransactionCount(address common.Address, blockNum rpc.BlockNumber) (*hexutil.Uint64, error) - - // Chain Info - ChainID() (*hexutil.Big, error) - ChainConfig() *params.ChainConfig - // TODO: feat: Dynamic fees - BaseFee(blockRes *tmrpctypes.ResultBlockResults) (*big.Int, error) - CurrentHeader() (*gethcore.Header, error) - PendingTransactions() ([]*sdk.Tx, error) - FeeHistory(blockCount gethrpc.DecimalOrHex, lastBlock gethrpc.BlockNumber, rewardPercentiles []float64) (*rpc.FeeHistoryResult, error) - SuggestGasTipCap(baseFee *big.Int) (*big.Int, error) - - // Tx Info - GetTransactionByHash(txHash common.Hash) (*rpc.EthTxJsonRPC, error) - GetTxByEthHash(txHash common.Hash) (*eth.TxResult, error) - GetTxByTxIndex(height int64, txIndex uint) (*eth.TxResult, error) - GetTransactionByBlockAndIndex(block *tmrpctypes.ResultBlock, idx hexutil.Uint) (*rpc.EthTxJsonRPC, error) - GetTransactionReceipt(hash common.Hash) (map[string]interface{}, error) - GetTransactionByBlockHashAndIndex(hash common.Hash, idx hexutil.Uint) (*rpc.EthTxJsonRPC, error) - GetTransactionByBlockNumberAndIndex(blockNum rpc.BlockNumber, idx hexutil.Uint) (*rpc.EthTxJsonRPC, error) - - // Send Transaction - Resend(args evm.JsonTxArgs, gasPrice *hexutil.Big, gasLimit *hexutil.Uint64) (common.Hash, error) - SendRawTransaction(data hexutil.Bytes) (common.Hash, error) - SetTxDefaults(args evm.JsonTxArgs) (evm.JsonTxArgs, error) - EstimateGas(args evm.JsonTxArgs, blockNrOptional *rpc.BlockNumber) (hexutil.Uint64, error) - DoCall(args evm.JsonTxArgs, blockNr rpc.BlockNumber) (*evm.MsgEthereumTxResponse, error) - GasPrice() (*hexutil.Big, error) - - // Filter API - GetLogs(hash common.Hash) ([][]*gethcore.Log, error) - GetLogsByHeight(height *int64) ([][]*gethcore.Log, error) - BloomStatus() (uint64, uint64) - - // Tracing - TraceTransaction(hash common.Hash, config *evm.TraceConfig) (interface{}, error) - TraceBlock( - height rpc.BlockNumber, - config *evm.TraceConfig, - block *tmrpctypes.ResultBlock, - ) ([]*evm.TxTraceResult, error) - TraceCall( - txArgs evm.JsonTxArgs, - contextHeight rpc.BlockNumber, - config *evm.TraceConfig, - ) (interface{}, error) -} - -var _ BackendI = (*Backend)(nil) - -// Backend implements the BackendI interface +// Backend implements implements the functionality shared within ethereum namespaces +// as defined by [EIP-1474]. +// +// [EIP-1474]: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1474.md type Backend struct { ctx context.Context clientCtx client.Context @@ -172,3 +58,13 @@ func NewBackend( indexer: indexer, } } + +// CosmosBackend: Currently unused. Backend functionality for the shared +// "cosmos" RPC namespace. Implements [BackendI] in combination with [EVMBackend]. +// TODO: feat(eth): Implement the cosmos JSON-RPC defined by Wallet Connect V2: +// https://docs.walletconnect.com/2.0/json-rpc/cosmos. +type CosmosBackend interface { + // TODO: GetAccounts() + // TODO: SignDirect() + // TODO: SignAmino() +} diff --git a/eth/rpc/rpcapi/debugapi/api.go b/eth/rpc/rpcapi/debugapi/api.go index ebb2cebaf..c70673273 100644 --- a/eth/rpc/rpcapi/debugapi/api.go +++ b/eth/rpc/rpcapi/debugapi/api.go @@ -45,7 +45,7 @@ type HandlerT struct { type DebugAPI struct { ctx *server.Context logger log.Logger - backend backend.EVMBackend + backend *backend.Backend handler *HandlerT } @@ -53,7 +53,7 @@ type DebugAPI struct { // Ethereum service. func NewImplDebugAPI( ctx *server.Context, - backend backend.EVMBackend, + backend *backend.Backend, ) *DebugAPI { return &DebugAPI{ ctx: ctx, diff --git a/eth/rpc/rpcapi/eth_api.go b/eth/rpc/rpcapi/eth_api.go index 75f7ffc85..9695832df 100644 --- a/eth/rpc/rpcapi/eth_api.go +++ b/eth/rpc/rpcapi/eth_api.go @@ -131,11 +131,11 @@ var _ IEthAPI = (*EthAPI)(nil) type EthAPI struct { ctx context.Context logger log.Logger - backend backend.EVMBackend + backend *backend.Backend } // NewImplEthAPI creates an instance of the public ETH Web3 API. -func NewImplEthAPI(logger log.Logger, backend backend.EVMBackend) *EthAPI { +func NewImplEthAPI(logger log.Logger, backend *backend.Backend) *EthAPI { api := &EthAPI{ ctx: context.Background(), logger: logger.With("client", "json-rpc"), From 8fa26b16cd4c501ef2aee197be9f277cf5240bce Mon Sep 17 00:00:00 2001 From: Unique Divine <51418232+Unique-Divine@users.noreply.github.com> Date: Wed, 18 Sep 2024 03:29:13 +0800 Subject: [PATCH 05/16] refactor(rpc-backend): Remove mocks from eth/rpc/backend, partially completing #2037 (#2041) * refactor: partial commit from #2037. Remove mocks * chore: changelog --- CHANGELOG.md | 1 + eth/rpc/backend/account_info_test.go | 413 +---- eth/rpc/backend/backend.go | 2 +- eth/rpc/backend/backend_suite_test.go | 243 +-- eth/rpc/backend/blocks_test.go | 1641 +---------------- eth/rpc/backend/call_tx_test.go | 503 +---- eth/rpc/backend/chain_info_test.go | 295 +-- eth/rpc/backend/client_test.go | 260 +-- eth/rpc/backend/evm_query_client_test.go | 346 +--- eth/rpc/backend/filters_test.go | 124 +- eth/rpc/backend/mocks/README.md | 13 - eth/rpc/backend/mocks/client.go | 887 --------- eth/rpc/backend/mocks/evm_query_client.go | 512 ----- eth/rpc/backend/node_info_test.go | 144 +- eth/rpc/backend/sign_tx_test.go | 274 +-- eth/rpc/backend/tracing.go | 2 +- eth/rpc/backend/tracing_test.go | 359 +--- eth/rpc/backend/tx_info.go | 25 +- eth/rpc/backend/tx_info_test.go | 692 +------ eth/rpc/backend/utils.go | 5 +- eth/rpc/backend/utils_test.go | 53 +- eth/rpc/rpcapi/eth_api_test.go | 15 +- x/common/testutil/testnetwork/start_node.go | 3 +- x/common/testutil/testnetwork/tx_test.go | 5 +- x/common/testutil/testnetwork/util.go | 10 +- .../testutil/testnetwork/validator_node.go | 13 + x/sudo/cli/cli_test.go | 6 +- 27 files changed, 350 insertions(+), 6496 deletions(-) delete mode 100644 eth/rpc/backend/mocks/README.md delete mode 100644 eth/rpc/backend/mocks/client.go delete mode 100644 eth/rpc/backend/mocks/evm_query_client.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 26d1f96b6..49d5d9dfe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -120,6 +120,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#2030](https://github.com/NibiruChain/nibiru/pull/2030) - refactor(eth/rpc): Delete unused code and improve logging in the eth and debug namespaces - [#2031](https://github.com/NibiruChain/nibiru/pull/2031) - fix(evm): debug calls with custom tracer and tracer options - [#2039](https://github.com/NibiruChain/nibiru/pull/2039) - refactor(rpc-backend): remove unnecessary interface code +- [#2039](https://github.com/NibiruChain/nibiru/pull/2039) - refactor(rpc-backend): Remove mocks from eth/rpc/backend, partially completing [nibiru#2037](https://github.com/NibiruChain/nibiru/issue/2037). #### Dapp modules: perp, spot, oracle, etc diff --git a/eth/rpc/backend/account_info_test.go b/eth/rpc/backend/account_info_test.go index 15650c0f4..f0dcea536 100644 --- a/eth/rpc/backend/account_info_test.go +++ b/eth/rpc/backend/account_info_test.go @@ -1,412 +1 @@ -package backend - -import ( - "fmt" - "math/big" - - tmrpcclient "github.com/cometbft/cometbft/rpc/client" - sdk "github.com/cosmos/cosmos-sdk/types" - authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - "google.golang.org/grpc/metadata" - - "github.com/NibiruChain/nibiru/v2/eth/rpc" - "github.com/NibiruChain/nibiru/v2/eth/rpc/backend/mocks" - "github.com/NibiruChain/nibiru/v2/x/evm" - evmtest "github.com/NibiruChain/nibiru/v2/x/evm/evmtest" -) - -func (s *BackendSuite) TestGetCode() { - blockNr := rpc.NewBlockNumber(big.NewInt(1)) - contractCode := []byte( - "0xef616c92f3cfc9e92dc270d6acff9cea213cecc7020a76ee4395af09bdceb4837a1ebdb5735e11e7d3adb6104e0c3ac55180b4ddf5e54d022cc5e8837f6a4f971b", - ) - - testCases := []struct { - name string - addr common.Address - blockNrOrHash rpc.BlockNumberOrHash - registerMock func(common.Address) - expPass bool - expCode hexutil.Bytes - }{ - { - "fail - BlockHash and BlockNumber are both nil ", - evmtest.NewEthPrivAcc().EthAddr, - rpc.BlockNumberOrHash{}, - func(addr common.Address) {}, - false, - nil, - }, - { - "fail - query client errors on getting Code", - evmtest.NewEthPrivAcc().EthAddr, - rpc.BlockNumberOrHash{BlockNumber: &blockNr}, - func(addr common.Address) { - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterCodeError(queryClient, addr) - }, - false, - nil, - }, - { - "pass", - evmtest.NewEthPrivAcc().EthAddr, - rpc.BlockNumberOrHash{BlockNumber: &blockNr}, - func(addr common.Address) { - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterCode(queryClient, addr, contractCode) - }, - true, - contractCode, - }, - } - for _, tc := range testCases { - s.Run(fmt.Sprintf("Case %s", tc.name), func() { - s.SetupTest() // reset - tc.registerMock(tc.addr) - - code, err := s.backend.GetCode(tc.addr, tc.blockNrOrHash) - if tc.expPass { - s.Require().NoError(err) - s.Require().Equal(tc.expCode, code) - } else { - s.Require().Error(err) - } - }) - } -} - -func (s *BackendSuite) TestGetProof() { - blockNrInvalid := rpc.NewBlockNumber(big.NewInt(1)) - blockNr := rpc.NewBlockNumber(big.NewInt(4)) - address1 := evmtest.NewEthPrivAcc().EthAddr - - testCases := []struct { - name string - addr common.Address - storageKeys []string - blockNrOrHash rpc.BlockNumberOrHash - registerMock func(rpc.BlockNumber, common.Address) - expPass bool - expAccRes *rpc.AccountResult - }{ - { - "fail - BlockNumeber = 1 (invalidBlockNumber)", - address1, - []string{}, - rpc.BlockNumberOrHash{BlockNumber: &blockNrInvalid}, - func(bn rpc.BlockNumber, addr common.Address) { - client := s.backend.clientCtx.Client.(*mocks.Client) - _, err := RegisterBlock(client, bn.Int64(), nil) - s.Require().NoError(err) - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterAccount(queryClient, addr, blockNrInvalid.Int64()) - }, - false, - &rpc.AccountResult{}, - }, - { - "fail - Block doesn't exist", - address1, - []string{}, - rpc.BlockNumberOrHash{BlockNumber: &blockNrInvalid}, - func(bn rpc.BlockNumber, addr common.Address) { - client := s.backend.clientCtx.Client.(*mocks.Client) - RegisterBlockError(client, bn.Int64()) - }, - false, - &rpc.AccountResult{}, - }, - { - "pass", - address1, - []string{"0x0"}, - rpc.BlockNumberOrHash{BlockNumber: &blockNr}, - func(bn rpc.BlockNumber, addr common.Address) { - s.backend.ctx = rpc.NewContextWithHeight(bn.Int64()) - - client := s.backend.clientCtx.Client.(*mocks.Client) - _, err := RegisterBlock(client, bn.Int64(), nil) - s.Require().NoError(err) - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterAccount(queryClient, addr, bn.Int64()) - - // Use the IAVL height if a valid tendermint height is passed in. - iavlHeight := bn.Int64() - RegisterABCIQueryWithOptions( - client, - bn.Int64(), - "store/evm/key", - evm.StateKey(address1, common.HexToHash("0x0").Bytes()), - tmrpcclient.ABCIQueryOptions{Height: iavlHeight, Prove: true}, - ) - RegisterABCIQueryWithOptions( - client, - bn.Int64(), - "store/acc/key", - authtypes.AddressStoreKey(sdk.AccAddress(address1.Bytes())), - tmrpcclient.ABCIQueryOptions{Height: iavlHeight, Prove: true}, - ) - }, - true, - &rpc.AccountResult{ - Address: address1, - AccountProof: []string{""}, - Balance: (*hexutil.Big)(big.NewInt(0)), - CodeHash: common.HexToHash(""), - Nonce: 0x0, - StorageHash: common.Hash{}, - StorageProof: []rpc.StorageResult{ - { - Key: "0x0", - Value: (*hexutil.Big)(big.NewInt(2)), - Proof: []string{""}, - }, - }, - }, - }, - } - for _, tc := range testCases { - s.Run(fmt.Sprintf("Case %s", tc.name), func() { - s.SetupTest() - tc.registerMock(*tc.blockNrOrHash.BlockNumber, tc.addr) - - accRes, err := s.backend.GetProof(tc.addr, tc.storageKeys, tc.blockNrOrHash) - - if tc.expPass { - s.Require().NoError(err) - s.Require().Equal(tc.expAccRes, accRes) - } else { - s.Require().Error(err) - } - }) - } -} - -func (s *BackendSuite) TestGetStorageAt() { - blockNr := rpc.NewBlockNumber(big.NewInt(1)) - - testCases := []struct { - name string - addr common.Address - key string - blockNrOrHash rpc.BlockNumberOrHash - registerMock func(common.Address, string, string) - expPass bool - expStorage hexutil.Bytes - }{ - { - "fail - BlockHash and BlockNumber are both nil", - evmtest.NewEthPrivAcc().EthAddr, - "0x0", - rpc.BlockNumberOrHash{}, - func(addr common.Address, key string, storage string) {}, - false, - nil, - }, - { - "fail - query client errors on getting Storage", - evmtest.NewEthPrivAcc().EthAddr, - "0x0", - rpc.BlockNumberOrHash{BlockNumber: &blockNr}, - func(addr common.Address, key string, storage string) { - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterStorageAtError(queryClient, addr, key) - }, - false, - nil, - }, - { - "pass", - evmtest.NewEthPrivAcc().EthAddr, - "0x0", - rpc.BlockNumberOrHash{BlockNumber: &blockNr}, - func(addr common.Address, key string, storage string) { - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterStorageAt(queryClient, addr, key, storage) - }, - true, - hexutil.Bytes{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, - }, - } - for _, tc := range testCases { - s.Run(fmt.Sprintf("Case %s", tc.name), func() { - s.SetupTest() - tc.registerMock(tc.addr, tc.key, tc.expStorage.String()) - - storage, err := s.backend.GetStorageAt(tc.addr, tc.key, tc.blockNrOrHash) - if tc.expPass { - s.Require().NoError(err) - s.Require().Equal(tc.expStorage, storage) - } else { - s.Require().Error(err) - } - }) - } -} - -func (s *BackendSuite) TestGetEvmGasBalance() { - blockNr := rpc.NewBlockNumber(big.NewInt(1)) - - testCases := []struct { - name string - addr common.Address - blockNrOrHash rpc.BlockNumberOrHash - registerMock func(rpc.BlockNumber, common.Address) - expPass bool - expBalance *hexutil.Big - }{ - { - "fail - BlockHash and BlockNumber are both nil", - evmtest.NewEthPrivAcc().EthAddr, - rpc.BlockNumberOrHash{}, - func(bn rpc.BlockNumber, addr common.Address) { - }, - false, - nil, - }, - { - "fail - tendermint client failed to get block", - evmtest.NewEthPrivAcc().EthAddr, - rpc.BlockNumberOrHash{BlockNumber: &blockNr}, - func(bn rpc.BlockNumber, addr common.Address) { - client := s.backend.clientCtx.Client.(*mocks.Client) - RegisterBlockError(client, bn.Int64()) - }, - false, - nil, - }, - { - "fail - query client failed to get balance", - evmtest.NewEthPrivAcc().EthAddr, - rpc.BlockNumberOrHash{BlockNumber: &blockNr}, - func(bn rpc.BlockNumber, addr common.Address) { - client := s.backend.clientCtx.Client.(*mocks.Client) - _, err := RegisterBlock(client, bn.Int64(), nil) - s.Require().NoError(err) - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterBalanceError(queryClient, addr, bn.Int64()) - }, - false, - nil, - }, - { - "fail - invalid balance", - evmtest.NewEthPrivAcc().EthAddr, - rpc.BlockNumberOrHash{BlockNumber: &blockNr}, - func(bn rpc.BlockNumber, addr common.Address) { - client := s.backend.clientCtx.Client.(*mocks.Client) - _, err := RegisterBlock(client, bn.Int64(), nil) - s.Require().NoError(err) - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterBalanceInvalid(queryClient, addr, bn.Int64()) - }, - false, - nil, - }, - { - "fail - pruned node state", - evmtest.NewEthPrivAcc().EthAddr, - rpc.BlockNumberOrHash{BlockNumber: &blockNr}, - func(bn rpc.BlockNumber, addr common.Address) { - client := s.backend.clientCtx.Client.(*mocks.Client) - _, err := RegisterBlock(client, bn.Int64(), nil) - s.Require().NoError(err) - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterBalanceNegative(queryClient, addr, bn.Int64()) - }, - false, - nil, - }, - { - "pass", - evmtest.NewEthPrivAcc().EthAddr, - rpc.BlockNumberOrHash{BlockNumber: &blockNr}, - func(bn rpc.BlockNumber, addr common.Address) { - client := s.backend.clientCtx.Client.(*mocks.Client) - _, err := RegisterBlock(client, bn.Int64(), nil) - s.Require().NoError(err) - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterBalance(queryClient, addr, bn.Int64()) - }, - true, - (*hexutil.Big)(big.NewInt(1)), - }, - } - for _, tc := range testCases { - s.Run(fmt.Sprintf("Case %s", tc.name), func() { - s.SetupTest() - - // avoid nil pointer reference - if tc.blockNrOrHash.BlockNumber != nil { - tc.registerMock(*tc.blockNrOrHash.BlockNumber, tc.addr) - } - - balance, err := s.backend.GetBalance(tc.addr, tc.blockNrOrHash) - if tc.expPass { - s.Require().NoError(err) - s.Require().Equal(tc.expBalance, balance) - } else { - s.Require().Error(err) - } - }) - } -} - -func (s *BackendSuite) TestGetTransactionCount() { - testCases := []struct { - name string - accExists bool - blockNum rpc.BlockNumber - registerMock func(common.Address, rpc.BlockNumber) - expPass bool - expTxCount hexutil.Uint64 - }{ - { - "pass - account doesn't exist", - false, - rpc.NewBlockNumber(big.NewInt(1)), - func(addr common.Address, bn rpc.BlockNumber) { - var header metadata.MD - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterParams(queryClient, &header, 1) - }, - true, - hexutil.Uint64(0), - }, - { - "fail - block height is in the future", - false, - rpc.NewBlockNumber(big.NewInt(10000)), - func(addr common.Address, bn rpc.BlockNumber) { - var header metadata.MD - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterParams(queryClient, &header, 1) - }, - false, - hexutil.Uint64(0), - }, - } - for _, tc := range testCases { - s.Run(fmt.Sprintf("Case %s", tc.name), func() { - s.SetupTest() - - addr := evmtest.NewEthPrivAcc().EthAddr - if tc.accExists { - addr = common.BytesToAddress(s.acc.Bytes()) - } - - tc.registerMock(addr, tc.blockNum) - - txCount, err := s.backend.GetTransactionCount(addr, tc.blockNum) - if tc.expPass { - s.Require().NoError(err) - s.Require().Equal(tc.expTxCount, *txCount) - } else { - s.Require().Error(err) - } - }) - } -} +package backend_test diff --git a/eth/rpc/backend/backend.go b/eth/rpc/backend/backend.go index 9f3ad79ba..2f569125f 100644 --- a/eth/rpc/backend/backend.go +++ b/eth/rpc/backend/backend.go @@ -60,7 +60,7 @@ func NewBackend( } // CosmosBackend: Currently unused. Backend functionality for the shared -// "cosmos" RPC namespace. Implements [BackendI] in combination with [EVMBackend]. +// "cosmos" RPC namespace. Implements [BackendI] in combination with [Backend]. // TODO: feat(eth): Implement the cosmos JSON-RPC defined by Wallet Connect V2: // https://docs.walletconnect.com/2.0/json-rpc/cosmos. type CosmosBackend interface { diff --git a/eth/rpc/backend/backend_suite_test.go b/eth/rpc/backend/backend_suite_test.go index e019c2fcc..1914bf865 100644 --- a/eth/rpc/backend/backend_suite_test.go +++ b/eth/rpc/backend/backend_suite_test.go @@ -1,203 +1,110 @@ -package backend +package backend_test import ( - "bufio" "math/big" - "os" - "path/filepath" "testing" - dbm "github.com/cometbft/cometbft-db" + "crypto/ecdsa" - tmrpctypes "github.com/cometbft/cometbft/rpc/core/types" - "github.com/cosmos/cosmos-sdk/client" - "github.com/cosmos/cosmos-sdk/crypto/keyring" - "github.com/cosmos/cosmos-sdk/server" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/ethereum/go-ethereum/common" + gethcommon "github.com/ethereum/go-ethereum/common" gethcore "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" "github.com/stretchr/testify/suite" - "github.com/NibiruChain/nibiru/v2/app" - "github.com/NibiruChain/nibiru/v2/eth" - "github.com/NibiruChain/nibiru/v2/eth/crypto/hd" - "github.com/NibiruChain/nibiru/v2/eth/encoding" - "github.com/NibiruChain/nibiru/v2/eth/indexer" + "github.com/NibiruChain/nibiru/v2/x/evm/evmtest" + "github.com/NibiruChain/nibiru/v2/eth/rpc" - "github.com/NibiruChain/nibiru/v2/eth/rpc/backend/mocks" "github.com/NibiruChain/nibiru/v2/x/evm" - evmtest "github.com/NibiruChain/nibiru/v2/x/evm/evmtest" + + "github.com/NibiruChain/nibiru/v2/app" + "github.com/NibiruChain/nibiru/v2/app/appconst" + "github.com/NibiruChain/nibiru/v2/eth" + "github.com/NibiruChain/nibiru/v2/eth/rpc/backend" + + "github.com/NibiruChain/nibiru/v2/x/common/testutil/genesis" + "github.com/NibiruChain/nibiru/v2/x/common/testutil/testapp" + "github.com/NibiruChain/nibiru/v2/x/common/testutil/testnetwork" ) +var recipient = evmtest.NewEthPrivAcc().EthAddr +var amountToSend = evm.NativeToWei(big.NewInt(1)) +var transferTxBlockNumber rpc.BlockNumber +var transferTxHash gethcommon.Hash + type BackendSuite struct { suite.Suite - - backend *Backend - from common.Address - acc sdk.AccAddress - signer keyring.Signer + cfg testnetwork.Config + network *testnetwork.Network + node *testnetwork.Validator + fundedAccPrivateKey *ecdsa.PrivateKey + fundedAccEthAddr gethcommon.Address + fundedAccNibiAddr sdk.AccAddress + backend *backend.Backend + ethChainID *big.Int } func TestBackendSuite(t *testing.T) { suite.Run(t, new(BackendSuite)) } -const ChainID = eth.EIP155ChainID_Testnet - -// SetupTest is executed before every BackendTestSuite test -func (s *BackendSuite) SetupTest() { - ctx := server.NewDefaultContext() - ctx.Viper.Set("telemetry.global-labels", []interface{}{}) - - baseDir := s.T().TempDir() - nodeDirName := "node" - clientDir := filepath.Join(baseDir, nodeDirName, "nibirucli") - keyRing, err := s.generateTestKeyring(clientDir) - if err != nil { - panic(err) - } - - // Create Account with set sequence - s.acc = sdk.AccAddress(evmtest.NewEthPrivAcc().EthAddr.Bytes()) - accounts := map[string]client.TestAccount{} - accounts[s.acc.String()] = client.TestAccount{ - Address: s.acc, - Num: uint64(1), - Seq: uint64(1), - } - - ethAcc := evmtest.NewEthPrivAcc() - from, priv := ethAcc.EthAddr, ethAcc.PrivKey - s.from = from - s.signer = evmtest.NewSigner(priv) - s.Require().NoError(err) - - encCfg := encoding.MakeConfig(app.ModuleBasics) - evm.RegisterInterfaces(encCfg.InterfaceRegistry) - eth.RegisterInterfaces(encCfg.InterfaceRegistry) - clientCtx := client.Context{}.WithChainID(ChainID). - WithHeight(1). - WithTxConfig(encCfg.TxConfig). - WithKeyringDir(clientDir). - WithKeyring(keyRing). - WithAccountRetriever(client.TestAccountRetriever{Accounts: accounts}) - - allowUnprotectedTxs := false - idxer := indexer.NewKVIndexer(dbm.NewMemDB(), ctx.Logger, clientCtx) - - s.backend = NewBackend(ctx, ctx.Logger, clientCtx, allowUnprotectedTxs, idxer) - s.backend.cfg.JSONRPC.GasCap = 0 - s.backend.cfg.JSONRPC.EVMTimeout = 0 - s.backend.queryClient.QueryClient = mocks.NewEVMQueryClient(s.T()) - s.backend.clientCtx.Client = mocks.NewClient(s.T()) - s.backend.ctx = rpc.NewContextWithHeight(1) - - s.backend.clientCtx.Codec = encCfg.Codec -} +func (s *BackendSuite) SetupSuite() { + testapp.EnsureNibiruPrefix() -// buildEthereumTx returns an example legacy Ethereum transaction -func (s *BackendSuite) buildEthereumTx() (*evm.MsgEthereumTx, []byte) { - ethTxParams := evm.EvmTxArgs{ - ChainID: s.backend.chainID, - Nonce: uint64(0), - To: &common.Address{}, - Amount: big.NewInt(0), - GasLimit: 100000, - GasPrice: big.NewInt(1), - } - msgEthereumTx := evm.NewTx(ðTxParams) - - // A valid msg should have empty `From` - msgEthereumTx.From = s.from.Hex() - - txBuilder := s.backend.clientCtx.TxConfig.NewTxBuilder() - err := txBuilder.SetMsgs(msgEthereumTx) + genState := genesis.NewTestGenesisState(app.MakeEncodingConfig()) + homeDir := s.T().TempDir() + s.cfg = testnetwork.BuildNetworkConfig(genState) + network, err := testnetwork.New(s.T(), homeDir, s.cfg) s.Require().NoError(err) + s.network = network + s.node = network.Validators[0] + s.ethChainID = appconst.GetEthChainID(s.node.ClientCtx.ChainID) + s.backend = s.node.EthRpcBackend - bz, err := s.backend.clientCtx.TxConfig.TxEncoder()(txBuilder.GetTx()) - s.Require().NoError(err) - return msgEthereumTx, bz -} + testAccPrivateKey, _ := crypto.GenerateKey() + s.fundedAccPrivateKey = testAccPrivateKey + s.fundedAccEthAddr = crypto.PubkeyToAddress(testAccPrivateKey.PublicKey) + s.fundedAccNibiAddr = eth.EthAddrToNibiruAddr(s.fundedAccEthAddr) -// buildFormattedBlock returns a formatted block for testing -func (s *BackendSuite) buildFormattedBlock( - blockRes *tmrpctypes.ResultBlockResults, - resBlock *tmrpctypes.ResultBlock, - fullTx bool, - tx *evm.MsgEthereumTx, - validator sdk.AccAddress, - baseFee *big.Int, -) map[string]interface{} { - header := resBlock.Block.Header - gasLimit := int64(^uint32(0)) // for `MaxGas = -1` (DefaultConsensusParams) - gasUsed := new(big.Int).SetUint64(uint64(blockRes.TxsResults[0].GasUsed)) - - root := common.Hash{}.Bytes() - receipt := gethcore.NewReceipt(root, false, gasUsed.Uint64()) - bloom := gethcore.CreateBloom(gethcore.Receipts{receipt}) - - ethRPCTxs := []interface{}{} - if tx != nil { - if fullTx { - rpcTx, err := rpc.NewRPCTxFromEthTx( - tx.AsTransaction(), - common.BytesToHash(header.Hash()), - uint64(header.Height), - uint64(0), - baseFee, - s.backend.chainID, - ) - s.Require().NoError(err) - ethRPCTxs = []interface{}{rpcTx} - } else { - ethRPCTxs = []interface{}{common.HexToHash(tx.Hash)} - } - } - - return rpc.FormatBlock( - header, - resBlock.Block.Size(), - gasLimit, - gasUsed, - ethRPCTxs, - bloom, - common.BytesToAddress(validator.Bytes()), - baseFee, - ) -} + funds := sdk.NewCoins(sdk.NewInt64Coin(eth.EthBaseDenom, 100_000_000)) -func (s *BackendSuite) generateTestKeyring(clientDir string) (keyring.Keyring, error) { - buf := bufio.NewReader(os.Stdin) - encCfg := encoding.MakeConfig(app.ModuleBasics) - return keyring.New( - sdk.KeyringServiceName(), // appName - keyring.BackendTest, // backend - clientDir, // rootDir - buf, // userInput - encCfg.Codec, // codec - []keyring.Option{hd.EthSecp256k1Option()}..., + txResp, err := testnetwork.FillWalletFromValidator( + s.fundedAccNibiAddr, funds, s.node, eth.EthBaseDenom, ) -} + s.Require().NoError(err, txResp.TxHash) + s.NoError(s.network.WaitForNextBlock()) -func (s *BackendSuite) signAndEncodeEthTx(msgEthereumTx *evm.MsgEthereumTx) []byte { - ethAcc := evmtest.NewEthPrivAcc() - from, priv := ethAcc.EthAddr, ethAcc.PrivKey - signer := evmtest.NewSigner(priv) - - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterParamsWithoutHeader(queryClient, 1) + // Send 1 Transfer TX and use the results in the tests + transferTxBlockNumber, transferTxHash = s.sendNibiViaEthTransfer(recipient, amountToSend) +} - ethSigner := gethcore.LatestSigner(s.backend.ChainConfig()) - msgEthereumTx.From = from.String() - err := msgEthereumTx.Sign(ethSigner, signer) +// SendNibiViaEthTransfer sends nibi using the eth rpc backend +func (s *BackendSuite) sendNibiViaEthTransfer( + to gethcommon.Address, + amount *big.Int, +) (rpc.BlockNumber, gethcommon.Hash) { + block, err := s.backend.BlockNumber() s.Require().NoError(err) - - tx, err := msgEthereumTx.BuildTx(s.backend.clientCtx.TxConfig.NewTxBuilder(), eth.EthBaseDenom) + s.NoError(err) + + signer := gethcore.LatestSignerForChainID(s.ethChainID) + gasPrice := evm.NativeToWei(big.NewInt(1)) + tx, err := gethcore.SignNewTx( + s.fundedAccPrivateKey, + signer, + &gethcore.LegacyTx{ + To: &to, + Value: amount, + Gas: params.TxGas, + GasPrice: gasPrice, + }) s.Require().NoError(err) - - txEncoder := s.backend.clientCtx.TxConfig.TxEncoder() - txBz, err := txEncoder(tx) + txBz, err := tx.MarshalBinary() + s.Require().NoError(err) + txHash, err := s.backend.SendRawTransaction(txBz) s.Require().NoError(err) + s.Require().NoError(s.network.WaitForNextBlock()) - return txBz + return rpc.NewBlockNumber(big.NewInt(int64(block) + 1)), txHash } diff --git a/eth/rpc/backend/blocks_test.go b/eth/rpc/backend/blocks_test.go index 2cefc8748..8d04c814f 100644 --- a/eth/rpc/backend/blocks_test.go +++ b/eth/rpc/backend/blocks_test.go @@ -1,1630 +1,39 @@ -package backend +package backend_test import ( - "fmt" + "context" "math/big" - "cosmossdk.io/math" - "github.com/cosmos/gogoproto/proto" - - "github.com/cometbft/cometbft/abci/types" - cmtrpc "github.com/cometbft/cometbft/rpc/core/types" - cmt "github.com/cometbft/cometbft/types" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - gethcore "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/trie" - "google.golang.org/grpc/metadata" "github.com/NibiruChain/nibiru/v2/eth/rpc" - "github.com/NibiruChain/nibiru/v2/eth/rpc/backend/mocks" - "github.com/NibiruChain/nibiru/v2/x/evm" - evmtest "github.com/NibiruChain/nibiru/v2/x/evm/evmtest" ) func (s *BackendSuite) TestBlockNumber() { - testCases := []struct { - name string - registerMock func() - wantBlockNum hexutil.Uint64 - wantPass bool - }{ - { - name: "fail - invalid block header height", - registerMock: func() { - var header metadata.MD - height := int64(1) - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterParamsInvalidHeight(queryClient, &header, height) - }, - wantBlockNum: 0x0, - wantPass: false, - }, - { - name: "fail - invalid block header", - registerMock: func() { - var header metadata.MD - height := int64(1) - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterParamsInvalidHeader(queryClient, &header, height) - }, - wantBlockNum: 0x0, - wantPass: false, - }, - { - name: "pass - app state header height 1", - registerMock: func() { - var header metadata.MD - height := int64(1) - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterParams(queryClient, &header, height) - }, - wantBlockNum: 0x1, - wantPass: true, - }, - } - for _, tc := range testCases { - s.Run(fmt.Sprintf("Case %s", tc.name), func() { - s.SetupTest() // reset test and queries - tc.registerMock() - - blockNumber, err := s.backend.BlockNumber() - - if tc.wantPass { - s.Require().NoError(err) - s.Require().Equal(tc.wantBlockNum, blockNumber) - } else { - s.Require().Error(err) - } - }) - } -} - -func (s *BackendSuite) TestGetBlockByNumber() { - var ( - blockRes *cmtrpc.ResultBlockResults - resBlock *cmtrpc.ResultBlock + blockHeight, err := s.backend.BlockNumber() + s.Require().NoError(err) + blockHeightU64, err := hexutil.DecodeUint64(blockHeight.String()) + s.NoError(err) + s.Greater(blockHeightU64, uint64(1)) + + latestHeight, _ := s.network.LatestHeight() + wantFullTx := true + resp, err := s.backend.GetBlockByNumber( + rpc.NewBlockNumber(big.NewInt(latestHeight)), + wantFullTx, ) - msgEthereumTx, bz := s.buildEthereumTx() - - testCases := []struct { - name string - blockNumber rpc.BlockNumber - fullTx bool - baseFee *big.Int - validator sdk.AccAddress - ethTx *evm.MsgEthereumTx - ethTxBz []byte - registerMock func(rpc.BlockNumber, math.Int, sdk.AccAddress, []byte) - wantNoop bool - wantPass bool - }{ - { - name: "pass - tendermint block not found", - blockNumber: rpc.BlockNumber(1), - fullTx: true, - baseFee: math.NewInt(1).BigInt(), - validator: sdk.AccAddress(evmtest.NewEthPrivAcc().EthAddr.Bytes()), - ethTx: nil, - ethTxBz: nil, - registerMock: func(blockNum rpc.BlockNumber, _ math.Int, _ sdk.AccAddress, _ []byte) { - height := blockNum.Int64() - client := s.backend.clientCtx.Client.(*mocks.Client) - RegisterBlockError(client, height) - }, - wantNoop: true, - wantPass: true, - }, - { - name: "pass - block not found (e.g. request block height that is greater than current one)", - blockNumber: rpc.BlockNumber(1), - fullTx: true, - baseFee: math.NewInt(1).BigInt(), - validator: sdk.AccAddress(evmtest.NewEthPrivAcc().EthAddr.Bytes()), - ethTx: nil, - ethTxBz: nil, - registerMock: func(blockNum rpc.BlockNumber, baseFee math.Int, validator sdk.AccAddress, txBz []byte) { - height := blockNum.Int64() - client := s.backend.clientCtx.Client.(*mocks.Client) - resBlock, _ = RegisterBlockNotFound(client, height) - }, - wantNoop: true, - wantPass: true, - }, - { - name: "pass - block results error", - blockNumber: rpc.BlockNumber(1), - fullTx: true, - baseFee: math.NewInt(1).BigInt(), - validator: sdk.AccAddress(evmtest.NewEthPrivAcc().EthAddr.Bytes()), - ethTx: nil, - ethTxBz: nil, - registerMock: func(blockNum rpc.BlockNumber, baseFee math.Int, validator sdk.AccAddress, txBz []byte) { - height := blockNum.Int64() - client := s.backend.clientCtx.Client.(*mocks.Client) - resBlock, _ = RegisterBlock(client, height, txBz) - RegisterBlockResultsError(client, blockNum.Int64()) - }, - wantNoop: true, - wantPass: true, - }, - { - name: "pass - without tx", - blockNumber: rpc.BlockNumber(1), - fullTx: true, - baseFee: math.NewInt(1).BigInt(), - validator: sdk.AccAddress(evmtest.NewEthPrivAcc().EthAddr.Bytes()), - ethTx: nil, - ethTxBz: nil, - registerMock: func(blockNum rpc.BlockNumber, baseFee math.Int, validator sdk.AccAddress, txBz []byte) { - height := blockNum.Int64() - client := s.backend.clientCtx.Client.(*mocks.Client) - resBlock, _ = RegisterBlock(client, height, txBz) - blockRes, _ = RegisterBlockResults(client, blockNum.Int64()) - RegisterConsensusParams(client, height) - - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterBaseFee(queryClient, baseFee) - RegisterValidatorAccount(queryClient, validator) - }, - wantNoop: false, - wantPass: true, - }, - { - name: "pass - with tx", - blockNumber: rpc.BlockNumber(1), - fullTx: true, - baseFee: math.NewInt(1).BigInt(), - validator: sdk.AccAddress(evmtest.NewEthPrivAcc().EthAddr.Bytes()), - ethTx: msgEthereumTx, - ethTxBz: bz, - registerMock: func(blockNum rpc.BlockNumber, baseFee math.Int, validator sdk.AccAddress, txBz []byte) { - height := blockNum.Int64() - client := s.backend.clientCtx.Client.(*mocks.Client) - resBlock, _ = RegisterBlock(client, height, txBz) - blockRes, _ = RegisterBlockResults(client, blockNum.Int64()) - RegisterConsensusParams(client, height) + s.Require().NoError(err, resp) - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterBaseFee(queryClient, baseFee) - RegisterValidatorAccount(queryClient, validator) - }, - wantNoop: false, - wantPass: true, - }, - } - for _, tc := range testCases { - s.Run(fmt.Sprintf("Case %s", tc.name), func() { - s.SetupTest() // reset test and queries - tc.registerMock(tc.blockNumber, math.NewIntFromBigInt(tc.baseFee), tc.validator, tc.ethTxBz) - - block, err := s.backend.GetBlockByNumber(tc.blockNumber, tc.fullTx) - - if tc.wantPass { - if tc.wantNoop { - s.Require().Nil(block) - } else { - expBlock := s.buildFormattedBlock( - blockRes, - resBlock, - tc.fullTx, - tc.ethTx, - tc.validator, - tc.baseFee, - ) - s.Require().Equal(expBlock, block) - } - s.Require().NoError(err) - } else { - s.Require().Error(err) - } - }) - } -} - -func (s *BackendSuite) TestGetBlockByHash() { - var ( - blockRes *cmtrpc.ResultBlockResults - resBlock *cmtrpc.ResultBlock + // TODO: test backend.GetBlockByHash + // s.backend.GetBlockByHash() + block, err := s.node.RPCClient.Block( + context.Background(), + &latestHeight, ) - msgEthereumTx, bz := s.buildEthereumTx() - - block := cmt.MakeBlock(1, []cmt.Tx{bz}, nil, nil) - - testCases := []struct { - name string - hash common.Hash - fullTx bool - baseFee *big.Int - validator sdk.AccAddress - tx *evm.MsgEthereumTx - txBz []byte - registerMock func( - common.Hash, math.Int, sdk.AccAddress, []byte) - wantNoop bool - wantPass bool - }{ - { - name: "fail - tendermint failed to get block", - hash: common.BytesToHash(block.Hash()), - fullTx: true, - baseFee: math.NewInt(1).BigInt(), - validator: sdk.AccAddress(evmtest.NewEthPrivAcc().EthAddr.Bytes()), - tx: nil, - txBz: nil, - registerMock: func(hash common.Hash, baseFee math.Int, validator sdk.AccAddress, txBz []byte) { - client := s.backend.clientCtx.Client.(*mocks.Client) - RegisterBlockByHashError(client, hash, txBz) - }, - wantNoop: false, - wantPass: false, - }, - { - name: "noop - tendermint blockres not found", - hash: common.BytesToHash(block.Hash()), - fullTx: true, - baseFee: math.NewInt(1).BigInt(), - validator: sdk.AccAddress(evmtest.NewEthPrivAcc().EthAddr.Bytes()), - tx: nil, - txBz: nil, - registerMock: func(hash common.Hash, baseFee math.Int, validator sdk.AccAddress, txBz []byte) { - client := s.backend.clientCtx.Client.(*mocks.Client) - RegisterBlockByHashNotFound(client, hash, txBz) - }, - wantNoop: true, - wantPass: true, - }, - { - name: "noop - tendermint failed to fetch block result", - hash: common.BytesToHash(block.Hash()), - fullTx: true, - baseFee: math.NewInt(1).BigInt(), - validator: sdk.AccAddress(evmtest.NewEthPrivAcc().EthAddr.Bytes()), - tx: nil, - txBz: nil, - registerMock: func(hash common.Hash, baseFee math.Int, validator sdk.AccAddress, txBz []byte) { - height := int64(1) - client := s.backend.clientCtx.Client.(*mocks.Client) - resBlock, _ = RegisterBlockByHash(client, hash, txBz) - - RegisterBlockResultsError(client, height) - }, - wantNoop: true, - wantPass: true, - }, - { - name: "pass - without tx", - hash: common.BytesToHash(block.Hash()), - fullTx: true, - baseFee: math.NewInt(1).BigInt(), - validator: sdk.AccAddress(evmtest.NewEthPrivAcc().EthAddr.Bytes()), - tx: nil, - txBz: nil, - registerMock: func(hash common.Hash, baseFee math.Int, validator sdk.AccAddress, txBz []byte) { - height := int64(1) - client := s.backend.clientCtx.Client.(*mocks.Client) - resBlock, _ = RegisterBlockByHash(client, hash, txBz) - - blockRes, _ = RegisterBlockResults(client, height) - RegisterConsensusParams(client, height) - - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterBaseFee(queryClient, baseFee) - RegisterValidatorAccount(queryClient, validator) - }, - wantNoop: false, - wantPass: true, - }, - { - name: "pass - with tx", - hash: common.BytesToHash(block.Hash()), - fullTx: true, - baseFee: math.NewInt(1).BigInt(), - validator: sdk.AccAddress(evmtest.NewEthPrivAcc().EthAddr.Bytes()), - tx: msgEthereumTx, - txBz: bz, - registerMock: func(hash common.Hash, baseFee math.Int, validator sdk.AccAddress, txBz []byte) { - height := int64(1) - client := s.backend.clientCtx.Client.(*mocks.Client) - resBlock, _ = RegisterBlockByHash(client, hash, txBz) - - blockRes, _ = RegisterBlockResults(client, height) - RegisterConsensusParams(client, height) - - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterBaseFee(queryClient, baseFee) - RegisterValidatorAccount(queryClient, validator) - }, - wantNoop: false, - wantPass: true, - }, - } - for _, tc := range testCases { - s.Run(fmt.Sprintf("Case %s", tc.name), func() { - s.SetupTest() // reset test and queries - tc.registerMock(tc.hash, math.NewIntFromBigInt(tc.baseFee), tc.validator, tc.txBz) - - block, err := s.backend.GetBlockByHash(tc.hash, tc.fullTx) - - if tc.wantPass { - if tc.wantNoop { - s.Require().Nil(block) - } else { - expBlock := s.buildFormattedBlock( - blockRes, - resBlock, - tc.fullTx, - tc.tx, - tc.validator, - tc.baseFee, - ) - s.Require().Equal(expBlock, block) - } - s.Require().NoError(err) - } else { - s.Require().Error(err) - } - }) - } -} - -func (s *BackendSuite) TestGetBlockTransactionCountByHash() { - _, bz := s.buildEthereumTx() - block := cmt.MakeBlock(1, []cmt.Tx{bz}, nil, nil) - emptyBlock := cmt.MakeBlock(1, []cmt.Tx{}, nil, nil) - - testCases := []struct { - name string - hash common.Hash - registerMock func(common.Hash) - wantCount hexutil.Uint - wantPass bool - }{ - { - name: "fail - block not found", - hash: common.BytesToHash(emptyBlock.Hash()), - registerMock: func(hash common.Hash) { - client := s.backend.clientCtx.Client.(*mocks.Client) - RegisterBlockByHashError(client, hash, nil) - }, - wantCount: hexutil.Uint(0), - wantPass: false, - }, - { - name: "fail - tendermint client failed to get block result", - hash: common.BytesToHash(emptyBlock.Hash()), - registerMock: func(hash common.Hash) { - height := int64(1) - client := s.backend.clientCtx.Client.(*mocks.Client) - _, err := RegisterBlockByHash(client, hash, nil) - s.Require().NoError(err) - RegisterBlockResultsError(client, height) - }, - wantCount: hexutil.Uint(0), - wantPass: false, - }, - { - name: "pass - block without tx", - hash: common.BytesToHash(emptyBlock.Hash()), - registerMock: func(hash common.Hash) { - height := int64(1) - client := s.backend.clientCtx.Client.(*mocks.Client) - _, err := RegisterBlockByHash(client, hash, nil) - s.Require().NoError(err) - _, err = RegisterBlockResults(client, height) - s.Require().NoError(err) - }, - wantCount: hexutil.Uint(0), - wantPass: true, - }, - { - name: "pass - block with tx", - hash: common.BytesToHash(block.Hash()), - registerMock: func(hash common.Hash) { - height := int64(1) - client := s.backend.clientCtx.Client.(*mocks.Client) - _, err := RegisterBlockByHash(client, hash, bz) - s.Require().NoError(err) - _, err = RegisterBlockResults(client, height) - s.Require().NoError(err) - }, - wantCount: hexutil.Uint(1), - wantPass: true, - }, - } - for _, tc := range testCases { - s.Run(fmt.Sprintf("Case %s", tc.name), func() { - s.SetupTest() // reset test and queries - - tc.registerMock(tc.hash) - count := s.backend.GetBlockTransactionCountByHash(tc.hash) - if tc.wantPass { - s.Require().Equal(tc.wantCount, *count) - } else { - s.Require().Nil(count) - } - }) - } -} - -func (s *BackendSuite) TestGetBlockTransactionCountByNumber() { - _, bz := s.buildEthereumTx() - block := cmt.MakeBlock(1, []cmt.Tx{bz}, nil, nil) - emptyBlock := cmt.MakeBlock(1, []cmt.Tx{}, nil, nil) - - testCases := []struct { - name string - blockNum rpc.BlockNumber - registerMock func(rpc.BlockNumber) - wantCount hexutil.Uint - wantPass bool - }{ - { - name: "fail - block not found", - blockNum: rpc.BlockNumber(emptyBlock.Height), - registerMock: func(blockNum rpc.BlockNumber) { - height := blockNum.Int64() - client := s.backend.clientCtx.Client.(*mocks.Client) - RegisterBlockError(client, height) - }, - wantCount: hexutil.Uint(0), - wantPass: false, - }, - { - name: "fail - tendermint client failed to get block result", - blockNum: rpc.BlockNumber(emptyBlock.Height), - registerMock: func(blockNum rpc.BlockNumber) { - height := blockNum.Int64() - client := s.backend.clientCtx.Client.(*mocks.Client) - _, err := RegisterBlock(client, height, nil) - s.Require().NoError(err) - RegisterBlockResultsError(client, height) - }, - wantCount: hexutil.Uint(0), - wantPass: false, - }, - { - name: "pass - block without tx", - blockNum: rpc.BlockNumber(emptyBlock.Height), - registerMock: func(blockNum rpc.BlockNumber) { - height := blockNum.Int64() - client := s.backend.clientCtx.Client.(*mocks.Client) - _, err := RegisterBlock(client, height, nil) - s.Require().NoError(err) - _, err = RegisterBlockResults(client, height) - s.Require().NoError(err) - }, - wantCount: hexutil.Uint(0), - wantPass: true, - }, - { - name: "pass - block with tx", - blockNum: rpc.BlockNumber(block.Height), - registerMock: func(blockNum rpc.BlockNumber) { - height := blockNum.Int64() - client := s.backend.clientCtx.Client.(*mocks.Client) - _, err := RegisterBlock(client, height, bz) - s.Require().NoError(err) - _, err = RegisterBlockResults(client, height) - s.Require().NoError(err) - }, - wantCount: hexutil.Uint(1), - wantPass: true, - }, - } - for _, tc := range testCases { - s.Run(fmt.Sprintf("Case %s", tc.name), func() { - s.SetupTest() // reset test and queries - - tc.registerMock(tc.blockNum) - count := s.backend.GetBlockTransactionCountByNumber(tc.blockNum) - if tc.wantPass { - s.Require().Equal(tc.wantCount, *count) - } else { - s.Require().Nil(count) - } - }) - } -} - -func (s *BackendSuite) TestTendermintBlockByNumber() { - var expResultBlock *cmtrpc.ResultBlock - - testCases := []struct { - name string - blockNumber rpc.BlockNumber - registerMock func(rpc.BlockNumber) - wantBlockFound bool - wantPass bool - }{ - { - name: "fail - client error", - blockNumber: rpc.BlockNumber(1), - registerMock: func(blockNum rpc.BlockNumber) { - height := blockNum.Int64() - client := s.backend.clientCtx.Client.(*mocks.Client) - RegisterBlockError(client, height) - }, - wantBlockFound: false, - wantPass: false, - }, - { - name: "noop - block not found", - blockNumber: rpc.BlockNumber(1), - registerMock: func(blockNum rpc.BlockNumber) { - height := blockNum.Int64() - client := s.backend.clientCtx.Client.(*mocks.Client) - _, err := RegisterBlockNotFound(client, height) - s.Require().NoError(err) - }, - wantBlockFound: false, - wantPass: true, - }, - { - name: "fail - blockNum < 0 with app state height error", - blockNumber: rpc.BlockNumber(-1), - registerMock: func(_ rpc.BlockNumber) { - var header metadata.MD - appHeight := int64(1) - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterParamsError(queryClient, &header, appHeight) - }, - wantBlockFound: false, - wantPass: false, - }, - { - name: "pass - blockNum < 0 with app state height >= 1", - blockNumber: rpc.BlockNumber(-1), - registerMock: func(blockNum rpc.BlockNumber) { - var header metadata.MD - appHeight := int64(1) - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterParams(queryClient, &header, appHeight) - - tmHeight := appHeight - client := s.backend.clientCtx.Client.(*mocks.Client) - expResultBlock, _ = RegisterBlock(client, tmHeight, nil) - }, - wantBlockFound: true, - wantPass: true, - }, - { - name: "pass - blockNum = 0 (defaults to blockNum = 1 due to a difference between tendermint heights and geth heights)", - blockNumber: rpc.BlockNumber(0), - registerMock: func(blockNum rpc.BlockNumber) { - height := blockNum.Int64() - client := s.backend.clientCtx.Client.(*mocks.Client) - expResultBlock, _ = RegisterBlock(client, height, nil) - }, - wantBlockFound: true, - wantPass: true, - }, - { - name: "pass - blockNum = 1", - blockNumber: rpc.BlockNumber(1), - registerMock: func(blockNum rpc.BlockNumber) { - height := blockNum.Int64() - client := s.backend.clientCtx.Client.(*mocks.Client) - expResultBlock, _ = RegisterBlock(client, height, nil) - }, - wantBlockFound: true, - wantPass: true, - }, - } - for _, tc := range testCases { - s.Run(fmt.Sprintf("Case %s", tc.name), func() { - s.SetupTest() // reset test and queries - - tc.registerMock(tc.blockNumber) - resultBlock, err := s.backend.TendermintBlockByNumber(tc.blockNumber) - - if tc.wantPass { - s.Require().NoError(err) - - if !tc.wantBlockFound { - s.Require().Nil(resultBlock) - } else { - s.Require().Equal(expResultBlock, resultBlock) - s.Require().Equal(expResultBlock.Block.Header.Height, resultBlock.Block.Header.Height) - } - } else { - s.Require().Error(err) - } - }) - } -} - -func (s *BackendSuite) TestTendermintBlockResultByNumber() { - var expBlockRes *cmtrpc.ResultBlockResults - - testCases := []struct { - name string - blockNumber int64 - registerMock func(int64) - wantPass bool - }{ - { - name: "fail", - blockNumber: 1, - registerMock: func(blockNum int64) { - client := s.backend.clientCtx.Client.(*mocks.Client) - RegisterBlockResultsError(client, blockNum) - }, - wantPass: false, - }, - { - name: "pass", - blockNumber: 1, - registerMock: func(blockNum int64) { - client := s.backend.clientCtx.Client.(*mocks.Client) - _, err := RegisterBlockResults(client, blockNum) - s.Require().NoError(err) - expBlockRes = &cmtrpc.ResultBlockResults{ - Height: blockNum, - TxsResults: []*types.ResponseDeliverTx{{Code: 0, GasUsed: 0}}, - } - }, - wantPass: true, - }, - } - for _, tc := range testCases { - s.Run(fmt.Sprintf("Case %s", tc.name), func() { - s.SetupTest() // reset test and queries - tc.registerMock(tc.blockNumber) - - blockRes, err := s.backend.TendermintBlockResultByNumber(&tc.blockNumber) //#nosec G601 -- fine for tests - - if tc.wantPass { - s.Require().NoError(err) - s.Require().Equal(expBlockRes, blockRes) - } else { - s.Require().Error(err) - } - }) - } -} - -func (s *BackendSuite) TestBlockNumberFromTendermint() { - var resBlock *cmtrpc.ResultBlock - - _, bz := s.buildEthereumTx() - block := cmt.MakeBlock(1, []cmt.Tx{bz}, nil, nil) - blockNum := rpc.NewBlockNumber(big.NewInt(block.Height)) - blockHash := common.BytesToHash(block.Hash()) - - testCases := []struct { - name string - blockNum *rpc.BlockNumber - hash *common.Hash - registerMock func(*common.Hash) - wantPass bool - }{ - { - "error - without blockHash or blockNum", - nil, - nil, - func(hash *common.Hash) {}, - false, - }, - { - "error - with blockHash, tendermint client failed to get block", - nil, - &blockHash, - func(hash *common.Hash) { - client := s.backend.clientCtx.Client.(*mocks.Client) - RegisterBlockByHashError(client, *hash, bz) - }, - false, - }, - { - "pass - with blockHash", - nil, - &blockHash, - func(hash *common.Hash) { - client := s.backend.clientCtx.Client.(*mocks.Client) - resBlock, _ = RegisterBlockByHash(client, *hash, bz) - }, - true, - }, - { - "pass - without blockHash & with blockNumber", - &blockNum, - nil, - func(hash *common.Hash) {}, - true, - }, - } - for _, tc := range testCases { - s.Run(fmt.Sprintf("Case %s", tc.name), func() { - s.SetupTest() // reset test and queries - - blockNrOrHash := rpc.BlockNumberOrHash{ - BlockNumber: tc.blockNum, - BlockHash: tc.hash, - } - - tc.registerMock(tc.hash) - blockNum, err := s.backend.BlockNumberFromTendermint(blockNrOrHash) - - if tc.wantPass { - s.Require().NoError(err) - if tc.hash == nil { - s.Require().Equal(*tc.blockNum, blockNum) - } else { - expHeight := rpc.NewBlockNumber(big.NewInt(resBlock.Block.Height)) - s.Require().Equal(expHeight, blockNum) - } - } else { - s.Require().Error(err) - } - }) - } -} - -func (s *BackendSuite) TestBlockNumberFromTendermintByHash() { - var resBlock *cmtrpc.ResultBlock - - _, bz := s.buildEthereumTx() - block := cmt.MakeBlock(1, []cmt.Tx{bz}, nil, nil) - emptyBlock := cmt.MakeBlock(1, []cmt.Tx{}, nil, nil) - - testCases := []struct { - name string - hash common.Hash - registerMock func(common.Hash) - wantPass bool - }{ - { - "fail - tendermint client failed to get block", - common.BytesToHash(block.Hash()), - func(hash common.Hash) { - client := s.backend.clientCtx.Client.(*mocks.Client) - RegisterBlockByHashError(client, hash, bz) - }, - false, - }, - { - "pass - block without tx", - common.BytesToHash(emptyBlock.Hash()), - func(hash common.Hash) { - client := s.backend.clientCtx.Client.(*mocks.Client) - resBlock, _ = RegisterBlockByHash(client, hash, bz) - }, - true, - }, - { - "pass - block with tx", - common.BytesToHash(block.Hash()), - func(hash common.Hash) { - client := s.backend.clientCtx.Client.(*mocks.Client) - resBlock, _ = RegisterBlockByHash(client, hash, bz) - }, - true, - }, - } - for _, tc := range testCases { - s.Run(fmt.Sprintf("Case %s", tc.name), func() { - s.SetupTest() // reset test and queries - - tc.registerMock(tc.hash) - blockNum, err := s.backend.BlockNumberFromTendermintByHash(tc.hash) - if tc.wantPass { - expHeight := big.NewInt(resBlock.Block.Height) - s.Require().NoError(err) - s.Require().Equal(expHeight, blockNum) - } else { - s.Require().Error(err) - } - }) - } -} - -func (s *BackendSuite) TestBlockBloom() { - testCases := []struct { - name string - blockRes *cmtrpc.ResultBlockResults - wantBlockBloom gethcore.Bloom - wantPass bool - }{ - { - "fail - empty block result", - &cmtrpc.ResultBlockResults{}, - gethcore.Bloom{}, - false, - }, - { - "fail - non block bloom event type", - &cmtrpc.ResultBlockResults{ - EndBlockEvents: []types.Event{{Type: evm.EventTypeEthereumTx}}, - }, - gethcore.Bloom{}, - false, - }, - { - "fail - nonblock bloom attribute key", - &cmtrpc.ResultBlockResults{ - EndBlockEvents: []types.Event{ - { - Type: proto.MessageName((*evm.EventBlockBloom)(nil)), - Attributes: []types.EventAttribute{ - {Key: evm.AttributeKeyEthereumTxHash}, - }, - }, - }, - }, - gethcore.Bloom{}, - false, - }, - { - "pass - block bloom attribute key", - &cmtrpc.ResultBlockResults{ - EndBlockEvents: []types.Event{ - { - Type: proto.MessageName((*evm.EventBlockBloom)(nil)), - Attributes: []types.EventAttribute{ - {Key: evm.AttributeKeyEthereumBloom}, - }, - }, - }, - }, - gethcore.Bloom{}, - true, - }, - } - for _, tc := range testCases { - s.Run(fmt.Sprintf("Case %s", tc.name), func() { - blockBloom, err := s.backend.BlockBloom(tc.blockRes) - - if tc.wantPass { - s.Require().NoError(err) - s.Require().Equal(tc.wantBlockBloom, blockBloom) - } else { - s.Require().Error(err) - } - }) - } -} - -func (s *BackendSuite) TestGetEthBlockFromTendermint() { - msgEthereumTx, bz := s.buildEthereumTx() - emptyBlock := cmt.MakeBlock(1, []cmt.Tx{}, nil, nil) - - testCases := []struct { - name string - baseFee *big.Int - validator sdk.AccAddress - height int64 - resBlock *cmtrpc.ResultBlock - blockRes *cmtrpc.ResultBlockResults - fullTx bool - registerMock func(math.Int, sdk.AccAddress, int64) - wantTxs bool - wantPass bool - }{ - { - name: "pass - block without tx", - baseFee: math.NewInt(1).BigInt(), - validator: sdk.AccAddress(common.Address{}.Bytes()), - height: int64(1), - resBlock: &cmtrpc.ResultBlock{Block: emptyBlock}, - blockRes: &cmtrpc.ResultBlockResults{ - Height: 1, - TxsResults: []*types.ResponseDeliverTx{{Code: 0, GasUsed: 0}}, - }, - fullTx: false, - registerMock: func(baseFee math.Int, validator sdk.AccAddress, height int64) { - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterBaseFee(queryClient, baseFee) - RegisterValidatorAccount(queryClient, validator) - - client := s.backend.clientCtx.Client.(*mocks.Client) - RegisterConsensusParams(client, height) - }, - wantTxs: false, - wantPass: true, - }, - { - name: "pass - block with tx - with BaseFee error", - baseFee: nil, - validator: sdk.AccAddress(evmtest.NewEthPrivAcc().EthAddr.Bytes()), - height: int64(1), - resBlock: &cmtrpc.ResultBlock{ - Block: cmt.MakeBlock(1, []cmt.Tx{bz}, nil, nil), - }, - blockRes: &cmtrpc.ResultBlockResults{ - Height: 1, - TxsResults: []*types.ResponseDeliverTx{{Code: 0, GasUsed: 0}}, - }, - fullTx: true, - registerMock: func(baseFee math.Int, validator sdk.AccAddress, height int64) { - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterBaseFeeError(queryClient) - RegisterValidatorAccount(queryClient, validator) - - client := s.backend.clientCtx.Client.(*mocks.Client) - RegisterConsensusParams(client, height) - }, - wantTxs: true, - wantPass: true, - }, - { - name: "pass - block with tx - with ValidatorAccount error", - baseFee: math.NewInt(1).BigInt(), - validator: sdk.AccAddress(common.Address{}.Bytes()), - height: int64(1), - resBlock: &cmtrpc.ResultBlock{ - Block: cmt.MakeBlock(1, []cmt.Tx{bz}, nil, nil), - }, - blockRes: &cmtrpc.ResultBlockResults{ - Height: 1, - TxsResults: []*types.ResponseDeliverTx{{Code: 0, GasUsed: 0}}, - }, - fullTx: true, - registerMock: func(baseFee math.Int, validator sdk.AccAddress, height int64) { - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterBaseFee(queryClient, baseFee) - RegisterValidatorAccountError(queryClient) - - client := s.backend.clientCtx.Client.(*mocks.Client) - RegisterConsensusParams(client, height) - }, - wantTxs: true, - wantPass: true, - }, - { - name: "pass - block with tx - with ConsensusParams error - BlockMaxGas defaults to max uint32", - baseFee: math.NewInt(1).BigInt(), - validator: sdk.AccAddress(evmtest.NewEthPrivAcc().EthAddr.Bytes()), - height: int64(1), - resBlock: &cmtrpc.ResultBlock{ - Block: cmt.MakeBlock(1, []cmt.Tx{bz}, nil, nil), - }, - blockRes: &cmtrpc.ResultBlockResults{ - Height: 1, - TxsResults: []*types.ResponseDeliverTx{{Code: 0, GasUsed: 0}}, - }, - fullTx: true, - registerMock: func(baseFee math.Int, validator sdk.AccAddress, height int64) { - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterBaseFee(queryClient, baseFee) - RegisterValidatorAccount(queryClient, validator) - - client := s.backend.clientCtx.Client.(*mocks.Client) - RegisterConsensusParamsError(client, height) - }, - wantTxs: true, - wantPass: true, - }, - { - name: "pass - block with tx - with ShouldIgnoreGasUsed - empty txs", - baseFee: math.NewInt(1).BigInt(), - validator: sdk.AccAddress(evmtest.NewEthPrivAcc().EthAddr.Bytes()), - height: int64(1), - resBlock: &cmtrpc.ResultBlock{ - Block: cmt.MakeBlock(1, []cmt.Tx{bz}, nil, nil), - }, - blockRes: &cmtrpc.ResultBlockResults{ - Height: 1, - TxsResults: []*types.ResponseDeliverTx{ - { - Code: 11, - GasUsed: 0, - Log: "no block gas left to run tx: out of gas", - }, - }, - }, - fullTx: true, - registerMock: func(baseFee math.Int, validator sdk.AccAddress, height int64) { - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterBaseFee(queryClient, baseFee) - RegisterValidatorAccount(queryClient, validator) - - client := s.backend.clientCtx.Client.(*mocks.Client) - RegisterConsensusParams(client, height) - }, - wantTxs: false, - wantPass: true, - }, - { - name: "pass - block with tx - non fullTx", - baseFee: math.NewInt(1).BigInt(), - validator: sdk.AccAddress(evmtest.NewEthPrivAcc().EthAddr.Bytes()), - height: int64(1), - resBlock: &cmtrpc.ResultBlock{ - Block: cmt.MakeBlock(1, []cmt.Tx{bz}, nil, nil), - }, - blockRes: &cmtrpc.ResultBlockResults{ - Height: 1, - TxsResults: []*types.ResponseDeliverTx{{Code: 0, GasUsed: 0}}, - }, - fullTx: false, - registerMock: func(baseFee math.Int, validator sdk.AccAddress, height int64) { - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterBaseFee(queryClient, baseFee) - RegisterValidatorAccount(queryClient, validator) - - client := s.backend.clientCtx.Client.(*mocks.Client) - RegisterConsensusParams(client, height) - }, - wantTxs: true, - wantPass: true, - }, - { - name: "pass - block with tx", - baseFee: math.NewInt(1).BigInt(), - validator: sdk.AccAddress(evmtest.NewEthPrivAcc().EthAddr.Bytes()), - height: int64(1), - resBlock: &cmtrpc.ResultBlock{ - Block: cmt.MakeBlock(1, []cmt.Tx{bz}, nil, nil), - }, - blockRes: &cmtrpc.ResultBlockResults{ - Height: 1, - TxsResults: []*types.ResponseDeliverTx{{Code: 0, GasUsed: 0}}, - }, - fullTx: true, - registerMock: func(baseFee math.Int, validator sdk.AccAddress, height int64) { - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterBaseFee(queryClient, baseFee) - RegisterValidatorAccount(queryClient, validator) - - client := s.backend.clientCtx.Client.(*mocks.Client) - RegisterConsensusParams(client, height) - }, - wantTxs: true, - wantPass: true, - }, - } - for _, tc := range testCases { - s.Run(fmt.Sprintf("Case %s", tc.name), func() { - s.SetupTest() // reset test and queries - tc.registerMock(math.NewIntFromBigInt(tc.baseFee), tc.validator, tc.height) - - block, err := s.backend.RPCBlockFromTendermintBlock(tc.resBlock, tc.blockRes, tc.fullTx) - - var expBlock map[string]interface{} - header := tc.resBlock.Block.Header - gasLimit := int64(^uint32(0)) // for `MaxGas = -1` (DefaultConsensusParams) - gasUsed := new(big.Int).SetUint64(uint64(tc.blockRes.TxsResults[0].GasUsed)) - - root := common.Hash{}.Bytes() - receipt := gethcore.NewReceipt(root, false, gasUsed.Uint64()) - bloom := gethcore.CreateBloom(gethcore.Receipts{receipt}) - - ethRPCTxs := []interface{}{} - - if tc.wantTxs { - if tc.fullTx { - rpcTx, err := rpc.NewRPCTxFromEthTx( - msgEthereumTx.AsTransaction(), - common.BytesToHash(header.Hash()), - uint64(header.Height), - uint64(0), - tc.baseFee, - s.backend.chainID, - ) - s.Require().NoError(err) - ethRPCTxs = []interface{}{rpcTx} - } else { - ethRPCTxs = []interface{}{common.HexToHash(msgEthereumTx.Hash)} - } - } - - expBlock = rpc.FormatBlock( - header, - tc.resBlock.Block.Size(), - gasLimit, - gasUsed, - ethRPCTxs, - bloom, - common.BytesToAddress(tc.validator.Bytes()), - tc.baseFee, - ) - - if tc.wantPass { - s.Require().Equal(expBlock, block) - s.Require().NoError(err) - } else { - s.Require().Error(err) - } - }) - } -} - -func (s *BackendSuite) TestEthMsgsFromTendermintBlock() { - msgEthereumTx, bz := s.buildEthereumTx() - - testCases := []struct { - name string - resBlock *cmtrpc.ResultBlock - blockRes *cmtrpc.ResultBlockResults - wantMsgs []*evm.MsgEthereumTx - }{ - { - "tx in not included in block - unsuccessful tx without ExceedBlockGasLimit error", - &cmtrpc.ResultBlock{ - Block: cmt.MakeBlock(1, []cmt.Tx{bz}, nil, nil), - }, - &cmtrpc.ResultBlockResults{ - TxsResults: []*types.ResponseDeliverTx{ - { - Code: 1, - }, - }, - }, - []*evm.MsgEthereumTx(nil), - }, - { - "tx included in block - unsuccessful tx with ExceedBlockGasLimit error", - &cmtrpc.ResultBlock{ - Block: cmt.MakeBlock(1, []cmt.Tx{bz}, nil, nil), - }, - &cmtrpc.ResultBlockResults{ - TxsResults: []*types.ResponseDeliverTx{ - { - Code: 1, - Log: rpc.ErrExceedBlockGasLimit, - }, - }, - }, - []*evm.MsgEthereumTx{msgEthereumTx}, - }, - { - "pass", - &cmtrpc.ResultBlock{ - Block: cmt.MakeBlock(1, []cmt.Tx{bz}, nil, nil), - }, - &cmtrpc.ResultBlockResults{ - TxsResults: []*types.ResponseDeliverTx{ - { - Code: 0, - Log: rpc.ErrExceedBlockGasLimit, - }, - }, - }, - []*evm.MsgEthereumTx{msgEthereumTx}, - }, - } - for _, tc := range testCases { - s.Run(fmt.Sprintf("Case %s", tc.name), func() { - s.SetupTest() // reset test and queries - - msgs := s.backend.EthMsgsFromTendermintBlock(tc.resBlock, tc.blockRes) - s.Require().Equal(tc.wantMsgs, msgs) - }) - } -} - -func (s *BackendSuite) TestHeaderByNumber() { - var expResultBlock *cmtrpc.ResultBlock - - _, bz := s.buildEthereumTx() - - testCases := []struct { - name string - blockNumber rpc.BlockNumber - baseFee *big.Int - registerMock func(rpc.BlockNumber, math.Int) - wantPass bool - }{ - { - name: "fail - tendermint client failed to get block", - blockNumber: rpc.BlockNumber(1), - baseFee: math.NewInt(1).BigInt(), - registerMock: func(blockNum rpc.BlockNumber, baseFee math.Int) { - height := blockNum.Int64() - client := s.backend.clientCtx.Client.(*mocks.Client) - RegisterBlockError(client, height) - }, - wantPass: false, - }, - { - name: "fail - block not found for height", - blockNumber: rpc.BlockNumber(1), - baseFee: math.NewInt(1).BigInt(), - registerMock: func(blockNum rpc.BlockNumber, baseFee math.Int) { - height := blockNum.Int64() - client := s.backend.clientCtx.Client.(*mocks.Client) - _, err := RegisterBlockNotFound(client, height) - s.Require().NoError(err) - }, - wantPass: false, - }, - { - name: "fail - block not found for height", - blockNumber: rpc.BlockNumber(1), - baseFee: math.NewInt(1).BigInt(), - registerMock: func(blockNum rpc.BlockNumber, baseFee math.Int) { - height := blockNum.Int64() - client := s.backend.clientCtx.Client.(*mocks.Client) - _, err := RegisterBlock(client, height, nil) - s.Require().NoError(err) - RegisterBlockResultsError(client, height) - }, - wantPass: false, - }, - { - name: "pass - without Base Fee, failed to fetch from prunned block", - blockNumber: rpc.BlockNumber(1), - baseFee: nil, - registerMock: func(blockNum rpc.BlockNumber, baseFee math.Int) { - height := blockNum.Int64() - client := s.backend.clientCtx.Client.(*mocks.Client) - expResultBlock, _ = RegisterBlock(client, height, nil) - _, err := RegisterBlockResults(client, height) - s.Require().NoError(err) - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterBaseFeeError(queryClient) - }, - wantPass: true, - }, - { - name: "pass - blockNum = 1, without tx", - blockNumber: rpc.BlockNumber(1), - baseFee: math.NewInt(1).BigInt(), - registerMock: func(blockNum rpc.BlockNumber, baseFee math.Int) { - height := blockNum.Int64() - client := s.backend.clientCtx.Client.(*mocks.Client) - expResultBlock, _ = RegisterBlock(client, height, nil) - _, err := RegisterBlockResults(client, height) - s.Require().NoError(err) - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterBaseFee(queryClient, baseFee) - }, - wantPass: true, - }, - { - name: "pass - blockNum = 1, with tx", - blockNumber: rpc.BlockNumber(1), - baseFee: math.NewInt(1).BigInt(), - registerMock: func(blockNum rpc.BlockNumber, baseFee math.Int) { - height := blockNum.Int64() - client := s.backend.clientCtx.Client.(*mocks.Client) - expResultBlock, _ = RegisterBlock(client, height, bz) - _, err := RegisterBlockResults(client, height) - s.Require().NoError(err) - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterBaseFee(queryClient, baseFee) - }, - wantPass: true, - }, - } - for _, tc := range testCases { - s.Run(fmt.Sprintf("Case %s", tc.name), func() { - s.SetupTest() // reset test and queries - - tc.registerMock(tc.blockNumber, math.NewIntFromBigInt(tc.baseFee)) - header, err := s.backend.HeaderByNumber(tc.blockNumber) - - if tc.wantPass { - expHeader := rpc.EthHeaderFromTendermint(expResultBlock.Block.Header, gethcore.Bloom{}, tc.baseFee) - s.Require().NoError(err) - s.Require().Equal(expHeader, header) - } else { - s.Require().Error(err) - } - }) - } -} - -func (s *BackendSuite) TestHeaderByHash() { - var expResultBlock *cmtrpc.ResultBlock - - _, bz := s.buildEthereumTx() - block := cmt.MakeBlock(1, []cmt.Tx{bz}, nil, nil) - emptyBlock := cmt.MakeBlock(1, []cmt.Tx{}, nil, nil) - - testCases := []struct { - name string - hash common.Hash - baseFee *big.Int - registerMock func(common.Hash, math.Int) - wantPass bool - }{ - { - name: "fail - tendermint client failed to get block", - hash: common.BytesToHash(block.Hash()), - baseFee: math.NewInt(1).BigInt(), - registerMock: func(hash common.Hash, baseFee math.Int) { - client := s.backend.clientCtx.Client.(*mocks.Client) - RegisterBlockByHashError(client, hash, bz) - }, - wantPass: false, - }, - { - name: "fail - block not found for height", - hash: common.BytesToHash(block.Hash()), - baseFee: math.NewInt(1).BigInt(), - registerMock: func(hash common.Hash, baseFee math.Int) { - client := s.backend.clientCtx.Client.(*mocks.Client) - RegisterBlockByHashNotFound(client, hash, bz) - }, - wantPass: false, - }, - { - name: "fail - block not found for height", - hash: common.BytesToHash(block.Hash()), - baseFee: math.NewInt(1).BigInt(), - registerMock: func(hash common.Hash, baseFee math.Int) { - height := int64(1) - client := s.backend.clientCtx.Client.(*mocks.Client) - _, err := RegisterBlockByHash(client, hash, bz) - s.Require().NoError(err) - RegisterBlockResultsError(client, height) - }, - wantPass: false, - }, - { - name: "pass - without Base Fee, failed to fetch from prunned block", - hash: common.BytesToHash(block.Hash()), - baseFee: nil, - registerMock: func(hash common.Hash, baseFee math.Int) { - height := int64(1) - client := s.backend.clientCtx.Client.(*mocks.Client) - expResultBlock, _ = RegisterBlockByHash(client, hash, bz) - _, err := RegisterBlockResults(client, height) - s.Require().NoError(err) - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterBaseFeeError(queryClient) - }, - wantPass: true, - }, - { - name: "pass - blockNum = 1, without tx", - hash: common.BytesToHash(emptyBlock.Hash()), - baseFee: math.NewInt(1).BigInt(), - registerMock: func(hash common.Hash, baseFee math.Int) { - height := int64(1) - client := s.backend.clientCtx.Client.(*mocks.Client) - expResultBlock, _ = RegisterBlockByHash(client, hash, nil) - _, err := RegisterBlockResults(client, height) - s.Require().NoError(err) - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterBaseFee(queryClient, baseFee) - }, - wantPass: true, - }, - { - name: "pass - with tx", - hash: common.BytesToHash(block.Hash()), - baseFee: math.NewInt(1).BigInt(), - registerMock: func(hash common.Hash, baseFee math.Int) { - height := int64(1) - client := s.backend.clientCtx.Client.(*mocks.Client) - expResultBlock, _ = RegisterBlockByHash(client, hash, bz) - _, err := RegisterBlockResults(client, height) - s.Require().NoError(err) - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterBaseFee(queryClient, baseFee) - }, - wantPass: true, - }, - } - - for _, tc := range testCases { - s.Run(fmt.Sprintf("Case %s", tc.name), func() { - s.SetupTest() // reset test and queries - - tc.registerMock(tc.hash, math.NewIntFromBigInt(tc.baseFee)) - header, err := s.backend.HeaderByHash(tc.hash) - - if tc.wantPass { - expHeader := rpc.EthHeaderFromTendermint(expResultBlock.Block.Header, gethcore.Bloom{}, tc.baseFee) - s.Require().NoError(err) - s.Require().Equal(expHeader, header) - } else { - s.Require().Error(err) - } - }) - } -} - -func (s *BackendSuite) TestEthBlockByNumber() { - msgEthereumTx, bz := s.buildEthereumTx() - emptyBlock := cmt.MakeBlock(1, []cmt.Tx{}, nil, nil) - - testCases := []struct { - name string - blockNumber rpc.BlockNumber - registerMock func(rpc.BlockNumber) - expEthBlock *gethcore.Block - wantPass bool - }{ - { - name: "fail - tendermint client failed to get block", - blockNumber: rpc.BlockNumber(1), - registerMock: func(blockNum rpc.BlockNumber) { - height := blockNum.Int64() - client := s.backend.clientCtx.Client.(*mocks.Client) - RegisterBlockError(client, height) - }, - expEthBlock: nil, - wantPass: false, - }, - { - name: "fail - block result not found for height", - blockNumber: rpc.BlockNumber(1), - registerMock: func(blockNum rpc.BlockNumber) { - height := blockNum.Int64() - client := s.backend.clientCtx.Client.(*mocks.Client) - _, err := RegisterBlock(client, height, nil) - s.Require().NoError(err) - RegisterBlockResultsError(client, blockNum.Int64()) - }, - expEthBlock: nil, - wantPass: false, - }, - { - name: "pass - block without tx", - blockNumber: rpc.BlockNumber(1), - registerMock: func(blockNum rpc.BlockNumber) { - height := blockNum.Int64() - client := s.backend.clientCtx.Client.(*mocks.Client) - _, err := RegisterBlock(client, height, nil) - s.Require().NoError(err) - _, err = RegisterBlockResults(client, blockNum.Int64()) - s.Require().NoError(err) - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - baseFee := math.NewInt(1) - RegisterBaseFee(queryClient, baseFee) - }, - expEthBlock: gethcore.NewBlock( - rpc.EthHeaderFromTendermint( - emptyBlock.Header, - gethcore.Bloom{}, - math.NewInt(1).BigInt(), - ), - []*gethcore.Transaction{}, - nil, - nil, - nil, - ), - wantPass: true, - }, - { - name: "pass - block with tx", - blockNumber: rpc.BlockNumber(1), - registerMock: func(blockNum rpc.BlockNumber) { - height := blockNum.Int64() - client := s.backend.clientCtx.Client.(*mocks.Client) - _, err := RegisterBlock(client, height, bz) - s.Require().NoError(err) - _, err = RegisterBlockResults(client, blockNum.Int64()) - s.Require().NoError(err) - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - baseFee := math.NewInt(1) - RegisterBaseFee(queryClient, baseFee) - }, - expEthBlock: gethcore.NewBlock( - rpc.EthHeaderFromTendermint( - emptyBlock.Header, - gethcore.Bloom{}, - math.NewInt(1).BigInt(), - ), - []*gethcore.Transaction{msgEthereumTx.AsTransaction()}, - nil, - nil, - trie.NewStackTrie(nil), - ), - wantPass: true, - }, - } - for _, tc := range testCases { - s.Run(fmt.Sprintf("Case %s", tc.name), func() { - s.SetupTest() // reset test and queries - tc.registerMock(tc.blockNumber) - - ethBlock, err := s.backend.EthBlockByNumber(tc.blockNumber) - - if tc.wantPass { - s.Require().NoError(err) - s.Require().Equal(tc.expEthBlock.Header(), ethBlock.Header()) - s.Require().Equal(tc.expEthBlock.Uncles(), ethBlock.Uncles()) - s.Require().Equal(tc.expEthBlock.ReceiptHash(), ethBlock.ReceiptHash()) - for i, tx := range tc.expEthBlock.Transactions() { - s.Require().Equal(tx.Data(), ethBlock.Transactions()[i].Data()) - } - } else { - s.Require().Error(err) - } - }) - } -} - -func (s *BackendSuite) TestEthBlockFromTendermintBlock() { - msgEthereumTx, bz := s.buildEthereumTx() - emptyBlock := cmt.MakeBlock(1, []cmt.Tx{}, nil, nil) - - testCases := []struct { - name string - baseFee *big.Int - resBlock *cmtrpc.ResultBlock - blockRes *cmtrpc.ResultBlockResults - registerMock func(math.Int, int64) - expEthBlock *gethcore.Block - wantPass bool - }{ - { - name: "pass - block without tx", - baseFee: math.NewInt(1).BigInt(), - resBlock: &cmtrpc.ResultBlock{ - Block: emptyBlock, - }, - blockRes: &cmtrpc.ResultBlockResults{ - Height: 1, - TxsResults: []*types.ResponseDeliverTx{{Code: 0, GasUsed: 0}}, - }, - registerMock: func(baseFee math.Int, blockNum int64) { - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterBaseFee(queryClient, baseFee) - }, - expEthBlock: gethcore.NewBlock( - rpc.EthHeaderFromTendermint( - emptyBlock.Header, - gethcore.Bloom{}, - math.NewInt(1).BigInt(), - ), - []*gethcore.Transaction{}, - nil, - nil, - nil, - ), - wantPass: true, - }, - { - name: "pass - block with tx", - baseFee: math.NewInt(1).BigInt(), - resBlock: &cmtrpc.ResultBlock{ - Block: cmt.MakeBlock(1, []cmt.Tx{bz}, nil, nil), - }, - blockRes: &cmtrpc.ResultBlockResults{ - Height: 1, - TxsResults: []*types.ResponseDeliverTx{{Code: 0, GasUsed: 0}}, - EndBlockEvents: []types.Event{ - { - Type: evm.EventTypeBlockBloom, - Attributes: []types.EventAttribute{ - {Key: evm.AttributeKeyEthereumBloom}, - }, - }, - }, - }, - registerMock: func(baseFee math.Int, blockNum int64) { - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterBaseFee(queryClient, baseFee) - }, - expEthBlock: gethcore.NewBlock( - rpc.EthHeaderFromTendermint( - emptyBlock.Header, - gethcore.Bloom{}, - math.NewInt(1).BigInt(), - ), - []*gethcore.Transaction{msgEthereumTx.AsTransaction()}, - nil, - nil, - trie.NewStackTrie(nil), - ), - wantPass: true, - }, - } - for _, tc := range testCases { - s.Run(fmt.Sprintf("Case %s", tc.name), func() { - s.SetupTest() // reset test and queries - tc.registerMock(math.NewIntFromBigInt(tc.baseFee), tc.blockRes.Height) - - ethBlock, err := s.backend.EthBlockFromTendermintBlock(tc.resBlock, tc.blockRes) - - if tc.wantPass { - s.Require().NoError(err) - s.Require().Equal(tc.expEthBlock.Header(), ethBlock.Header()) - s.Require().Equal(tc.expEthBlock.Uncles(), ethBlock.Uncles()) - s.Require().Equal(tc.expEthBlock.ReceiptHash(), ethBlock.ReceiptHash()) - for i, tx := range tc.expEthBlock.Transactions() { - s.Require().Equal(tx.Data(), ethBlock.Transactions()[i].Data()) - } - } else { - s.Require().Error(err) - } - }) - } + s.NoError(err, block) + blockResults, err := s.node.RPCClient.BlockResults( + context.Background(), + &latestHeight, + ) + s.NoError(err, blockResults) } diff --git a/eth/rpc/backend/call_tx_test.go b/eth/rpc/backend/call_tx_test.go index 879438d51..f0dcea536 100644 --- a/eth/rpc/backend/call_tx_test.go +++ b/eth/rpc/backend/call_tx_test.go @@ -1,502 +1 @@ -package backend - -import ( - "encoding/json" - "fmt" - "math/big" - - "cosmossdk.io/math" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - gethcore "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/rlp" - "google.golang.org/grpc/metadata" - - "github.com/NibiruChain/nibiru/v2/eth/rpc" - "github.com/NibiruChain/nibiru/v2/eth/rpc/backend/mocks" - "github.com/NibiruChain/nibiru/v2/x/evm" - evmtest "github.com/NibiruChain/nibiru/v2/x/evm/evmtest" -) - -func (s *BackendSuite) TestResend() { - txNonce := (hexutil.Uint64)(1) - baseFee := math.NewInt(1) - gasPrice := new(hexutil.Big) - toAddr := evmtest.NewEthPrivAcc().EthAddr - chainID := (*hexutil.Big)(s.backend.chainID) - callArgs := evm.JsonTxArgs{ - From: nil, - To: &toAddr, - Gas: nil, - GasPrice: nil, - MaxFeePerGas: gasPrice, - MaxPriorityFeePerGas: gasPrice, - Value: gasPrice, - Nonce: &txNonce, - Input: nil, - Data: nil, - AccessList: nil, - ChainID: chainID, - } - - testCases := []struct { - name string - registerMock func() - args evm.JsonTxArgs - gasPrice *hexutil.Big - gasLimit *hexutil.Uint64 - expHash common.Hash - expPass bool - }{ - { - "fail - Missing transaction nonce", - func() {}, - evm.JsonTxArgs{ - Nonce: nil, - }, - nil, - nil, - common.Hash{}, - false, - }, - { - "pass - Can't set Tx defaults BaseFee disabled", - func() { - var header metadata.MD - client := s.backend.clientCtx.Client.(*mocks.Client) - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterParams(queryClient, &header, 1) - _, err := RegisterBlock(client, 1, nil) - s.Require().NoError(err) - _, err = RegisterBlockResults(client, 1) - s.Require().NoError(err) - RegisterBaseFeeDisabled(queryClient) - }, - evm.JsonTxArgs{ - Nonce: &txNonce, - ChainID: callArgs.ChainID, - }, - nil, - nil, - common.Hash{}, - true, - }, - { - "pass - Can't set Tx defaults", - func() { - var header metadata.MD - client := s.backend.clientCtx.Client.(*mocks.Client) - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterParams(queryClient, &header, 1) - _, err := RegisterBlock(client, 1, nil) - s.Require().NoError(err) - _, err = RegisterBlockResults(client, 1) - s.Require().NoError(err) - RegisterBaseFee(queryClient, baseFee) - }, - evm.JsonTxArgs{ - Nonce: &txNonce, - }, - nil, - nil, - common.Hash{}, - true, - }, - { - "pass - MaxFeePerGas is nil", - func() { - var header metadata.MD - client := s.backend.clientCtx.Client.(*mocks.Client) - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterParams(queryClient, &header, 1) - _, err := RegisterBlock(client, 1, nil) - s.Require().NoError(err) - _, err = RegisterBlockResults(client, 1) - s.Require().NoError(err) - RegisterBaseFeeDisabled(queryClient) - }, - evm.JsonTxArgs{ - Nonce: &txNonce, - MaxPriorityFeePerGas: nil, - GasPrice: nil, - MaxFeePerGas: nil, - }, - nil, - nil, - common.Hash{}, - true, - }, - { - "fail - GasPrice and (MaxFeePerGas or MaxPriorityPerGas specified)", - func() {}, - evm.JsonTxArgs{ - Nonce: &txNonce, - MaxPriorityFeePerGas: nil, - GasPrice: gasPrice, - MaxFeePerGas: gasPrice, - }, - nil, - nil, - common.Hash{}, - false, - }, - { - "fail - Block error", - func() { - var header metadata.MD - client := s.backend.clientCtx.Client.(*mocks.Client) - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterParams(queryClient, &header, 1) - RegisterBlockError(client, 1) - }, - evm.JsonTxArgs{ - Nonce: &txNonce, - }, - nil, - nil, - common.Hash{}, - false, - }, - { - "pass - MaxFeePerGas is nil", - func() { - var header metadata.MD - client := s.backend.clientCtx.Client.(*mocks.Client) - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterParams(queryClient, &header, 1) - _, err := RegisterBlock(client, 1, nil) - s.Require().NoError(err) - _, err = RegisterBlockResults(client, 1) - s.Require().NoError(err) - RegisterBaseFee(queryClient, baseFee) - }, - evm.JsonTxArgs{ - Nonce: &txNonce, - GasPrice: nil, - MaxPriorityFeePerGas: gasPrice, - MaxFeePerGas: gasPrice, - ChainID: callArgs.ChainID, - }, - nil, - nil, - common.Hash{}, - true, - }, - { - "pass - Chain Id is nil", - func() { - var header metadata.MD - client := s.backend.clientCtx.Client.(*mocks.Client) - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterParams(queryClient, &header, 1) - _, err := RegisterBlock(client, 1, nil) - s.Require().NoError(err) - _, err = RegisterBlockResults(client, 1) - s.Require().NoError(err) - RegisterBaseFee(queryClient, baseFee) - }, - evm.JsonTxArgs{ - Nonce: &txNonce, - MaxPriorityFeePerGas: gasPrice, - ChainID: nil, - }, - nil, - nil, - common.Hash{}, - true, - }, - { - "fail - Pending transactions error", - func() { - var header metadata.MD - client := s.backend.clientCtx.Client.(*mocks.Client) - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - _, err := RegisterBlock(client, 1, nil) - s.Require().NoError(err) - _, err = RegisterBlockResults(client, 1) - s.Require().NoError(err) - RegisterBaseFee(queryClient, baseFee) - RegisterEstimateGas(queryClient, callArgs) - RegisterParams(queryClient, &header, 1) - RegisterParamsWithoutHeader(queryClient, 1) - RegisterUnconfirmedTxsError(client, nil) - }, - evm.JsonTxArgs{ - Nonce: &txNonce, - To: &toAddr, - MaxFeePerGas: gasPrice, - MaxPriorityFeePerGas: gasPrice, - Value: gasPrice, - Gas: nil, - ChainID: callArgs.ChainID, - }, - gasPrice, - nil, - common.Hash{}, - false, - }, - { - "fail - Not Ethereum txs", - func() { - var header metadata.MD - client := s.backend.clientCtx.Client.(*mocks.Client) - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - _, err := RegisterBlock(client, 1, nil) - s.Require().NoError(err) - _, err = RegisterBlockResults(client, 1) - s.Require().NoError(err) - RegisterBaseFee(queryClient, baseFee) - RegisterEstimateGas(queryClient, callArgs) - RegisterParams(queryClient, &header, 1) - RegisterParamsWithoutHeader(queryClient, 1) - RegisterUnconfirmedTxsEmpty(client, nil) - }, - evm.JsonTxArgs{ - Nonce: &txNonce, - To: &toAddr, - MaxFeePerGas: gasPrice, - MaxPriorityFeePerGas: gasPrice, - Value: gasPrice, - Gas: nil, - ChainID: callArgs.ChainID, - }, - gasPrice, - nil, - common.Hash{}, - false, - }, - } - - for _, tc := range testCases { - s.Run(fmt.Sprintf("case %s", tc.name), func() { - s.SetupTest() // reset test and queries - tc.registerMock() - - hash, err := s.backend.Resend(tc.args, tc.gasPrice, tc.gasLimit) - - if tc.expPass { - s.Require().Equal(tc.expHash, hash) - } else { - s.Require().Error(err) - } - }) - } -} - -func (s *BackendSuite) TestSendRawTransaction() { - ethTx, bz := s.buildEthereumTx() - - // Sign the ethTx - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterParamsWithoutHeader(queryClient, 1) - ethSigner := gethcore.LatestSigner(s.backend.ChainConfig()) - err := ethTx.Sign(ethSigner, s.signer) - s.Require().NoError(err) - - rlpEncodedBz, _ := rlp.EncodeToBytes(ethTx.AsTransaction()) - cosmosTx, _ := ethTx.BuildTx(s.backend.clientCtx.TxConfig.NewTxBuilder(), evm.DefaultEVMDenom) - txBytes, _ := s.backend.clientCtx.TxConfig.TxEncoder()(cosmosTx) - - testCases := []struct { - name string - registerMock func() - rawTx []byte - expHash common.Hash - expPass bool - }{ - { - "sad - empty bytes", - func() {}, - []byte{}, - common.Hash{}, - false, - }, - { - "sad - no RLP encoded bytes", - func() {}, - bz, - common.Hash{}, - false, - }, - { - "sad - unprotected transactions", - func() { - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - s.backend.allowUnprotectedTxs = false - RegisterParamsWithoutHeaderError(queryClient, 1) - }, - rlpEncodedBz, - common.Hash{}, - false, - }, - { - "sad - failed to get evm params", - func() { - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - s.backend.allowUnprotectedTxs = true - RegisterParamsWithoutHeaderError(queryClient, 1) - }, - rlpEncodedBz, - common.Hash{}, - false, - }, - { - "sad - failed to broadcast transaction", - func() { - client := s.backend.clientCtx.Client.(*mocks.Client) - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - s.backend.allowUnprotectedTxs = true - RegisterParamsWithoutHeader(queryClient, 1) - RegisterBroadcastTxError(client, txBytes) - }, - rlpEncodedBz, - common.HexToHash(ethTx.Hash), - false, - }, - { - "pass - Gets the correct transaction hash of the eth transaction", - func() { - client := s.backend.clientCtx.Client.(*mocks.Client) - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - s.backend.allowUnprotectedTxs = true - RegisterParamsWithoutHeader(queryClient, 1) - RegisterBroadcastTx(client, txBytes) - }, - rlpEncodedBz, - common.HexToHash(ethTx.Hash), - true, - }, - } - - for _, tc := range testCases { - s.Run(fmt.Sprintf("case %s", tc.name), func() { - s.SetupTest() // reset test and queries - tc.registerMock() - - hash, err := s.backend.SendRawTransaction(tc.rawTx) - - if tc.expPass { - s.Require().Equal(tc.expHash, hash) - } else { - s.Require().Error(err) - } - }) - } -} - -func (s *BackendSuite) TestDoCall() { - _, bz := s.buildEthereumTx() - gasPrice := (*hexutil.Big)(big.NewInt(1)) - toAddr := evmtest.NewEthPrivAcc().EthAddr - chainID := (*hexutil.Big)(s.backend.chainID) - callArgs := evm.JsonTxArgs{ - From: nil, - To: &toAddr, - Gas: nil, - GasPrice: nil, - MaxFeePerGas: gasPrice, - MaxPriorityFeePerGas: gasPrice, - Value: gasPrice, - Input: nil, - Data: nil, - AccessList: nil, - ChainID: chainID, - } - argsBz, err := json.Marshal(callArgs) - s.Require().NoError(err) - - testCases := []struct { - name string - registerMock func() - blockNum rpc.BlockNumber - callArgs evm.JsonTxArgs - expEthTx *evm.MsgEthereumTxResponse - expPass bool - }{ - { - "fail - Invalid request", - func() { - client := s.backend.clientCtx.Client.(*mocks.Client) - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - _, err := RegisterBlock(client, 1, bz) - s.Require().NoError(err) - RegisterEthCallError(queryClient, &evm.EthCallRequest{Args: argsBz, ChainId: s.backend.chainID.Int64()}) - }, - rpc.BlockNumber(1), - callArgs, - &evm.MsgEthereumTxResponse{}, - false, - }, - { - "pass - Returned transaction response", - func() { - client := s.backend.clientCtx.Client.(*mocks.Client) - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - _, err := RegisterBlock(client, 1, bz) - s.Require().NoError(err) - RegisterEthCall(queryClient, &evm.EthCallRequest{Args: argsBz, ChainId: s.backend.chainID.Int64()}) - }, - rpc.BlockNumber(1), - callArgs, - &evm.MsgEthereumTxResponse{}, - true, - }, - } - - for _, tc := range testCases { - s.Run(fmt.Sprintf("case %s", tc.name), func() { - s.SetupTest() // reset test and queries - tc.registerMock() - - msgEthTx, err := s.backend.DoCall(tc.callArgs, tc.blockNum) - - if tc.expPass { - s.Require().Equal(tc.expEthTx, msgEthTx) - } else { - s.Require().Error(err) - } - }) - } -} - -func (s *BackendSuite) TestGasPrice() { - defaultGasPrice := (*hexutil.Big)(big.NewInt(1)) - - testCases := []struct { - name string - registerMock func() - expGas *hexutil.Big - expPass bool - }{ - { - "pass - get the default gas price", - func() { - var header metadata.MD - client := s.backend.clientCtx.Client.(*mocks.Client) - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterParams(queryClient, &header, 1) - _, err := RegisterBlock(client, 1, nil) - s.Require().NoError(err) - _, err = RegisterBlockResults(client, 1) - s.Require().NoError(err) - RegisterBaseFee(queryClient, math.NewInt(1)) - }, - defaultGasPrice, - true, - }, - } - - for _, tc := range testCases { - s.Run(fmt.Sprintf("case %s", tc.name), func() { - s.SetupTest() // reset test and queries - tc.registerMock() - - gasPrice, err := s.backend.GasPrice() - if tc.expPass { - s.Require().Equal(tc.expGas, gasPrice) - } else { - s.Require().Error(err) - } - }) - } -} +package backend_test diff --git a/eth/rpc/backend/chain_info_test.go b/eth/rpc/backend/chain_info_test.go index b97783103..f0dcea536 100644 --- a/eth/rpc/backend/chain_info_test.go +++ b/eth/rpc/backend/chain_info_test.go @@ -1,294 +1 @@ -package backend - -import ( - "fmt" - "math/big" - - sdkmath "cosmossdk.io/math" - "github.com/ethereum/go-ethereum/common/hexutil" - ethrpc "github.com/ethereum/go-ethereum/rpc" - - "google.golang.org/grpc/metadata" - - sdk "github.com/cosmos/cosmos-sdk/types" - - "github.com/cometbft/cometbft/abci/types" - tmrpctypes "github.com/cometbft/cometbft/rpc/core/types" - - "github.com/NibiruChain/nibiru/v2/eth" - "github.com/NibiruChain/nibiru/v2/eth/rpc" - "github.com/NibiruChain/nibiru/v2/eth/rpc/backend/mocks" - "github.com/NibiruChain/nibiru/v2/x/evm" - evmtest "github.com/NibiruChain/nibiru/v2/x/evm/evmtest" -) - -func (s *BackendSuite) TestBaseFee() { - baseFee := sdkmath.NewInt(1) - - testCases := []struct { - name string - blockRes *tmrpctypes.ResultBlockResults - registerMock func() - expBaseFee *big.Int - expPass bool - }{ - // TODO: test(eth): Test base fee query after it's enabled. - // { - // "fail - grpc BaseFee error", - // &tmrpctypes.ResultBlockResults{Height: 1}, - // func() { - // queryClient := suite.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - // RegisterBaseFeeError(queryClient) - // }, - // nil, - // false, - // }, - { - name: "pass - grpc BaseFee error - with non feemarket block event", - blockRes: &tmrpctypes.ResultBlockResults{ - Height: 1, - BeginBlockEvents: []types.Event{ - { - Type: evm.EventTypeBlockBloom, - }, - }, - }, - registerMock: func() { - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterBaseFeeDisabled(queryClient) - }, - expBaseFee: nil, - expPass: true, - }, - { - name: "pass - base fee not enabled", - blockRes: &tmrpctypes.ResultBlockResults{Height: 1}, - registerMock: func() { - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterBaseFeeDisabled(queryClient) - }, - expBaseFee: nil, - expPass: true, - }, - { - "pass", - &tmrpctypes.ResultBlockResults{Height: 1}, - func() { - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterBaseFee(queryClient, baseFee) - }, - baseFee.BigInt(), - true, - }, - } - for _, tc := range testCases { - s.Run(fmt.Sprintf("Case %s", tc.name), func() { - s.SetupTest() // reset test and queries - tc.registerMock() - - baseFee, err := s.backend.BaseFee(tc.blockRes) - - if tc.expPass { - s.Require().NoError(err) - s.Require().Equal(tc.expBaseFee, baseFee) - } else { - s.Require().Error(err) - } - }) - } -} - -func (s *BackendSuite) TestChainId() { - expChainIDNumber, err := eth.ParseEthChainID(eth.EIP155ChainID_Testnet) - s.Require().NoError(err) - expChainID := (*hexutil.Big)(expChainIDNumber) - testCases := []struct { - name string - registerMock func() - expChainID *hexutil.Big - expPass bool - }{ - { - "pass - block is at or past the EIP-155 replay-protection fork block, return chainID from config ", - func() { - var header metadata.MD - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterParamsInvalidHeight(queryClient, &header, int64(1)) - }, - expChainID, - true, - }, - } - - for _, tc := range testCases { - s.Run(fmt.Sprintf("case %s", tc.name), func() { - s.SetupTest() // reset test and queries - tc.registerMock() - - chainID, err := s.backend.ChainID() - if tc.expPass { - s.Require().NoError(err) - s.Require().Equal(tc.expChainID, chainID) - } else { - s.Require().Error(err) - } - }) - } -} - -func (s *BackendSuite) TestSuggestGasTipCap() { - testCases := []struct { - name string - registerMock func() - baseFee *big.Int - expGasTipCap *big.Int - expPass bool - }{ - { - "pass - Feemarket not enabled ", - func() {}, - nil, - big.NewInt(0), - true, - }, - { - "pass - Gets the suggest gas tip cap ", - func() {}, - nil, - big.NewInt(0), - true, - }, - } - - for _, tc := range testCases { - s.Run(fmt.Sprintf("case %s", tc.name), func() { - s.SetupTest() // reset test and queries - tc.registerMock() - - maxDelta, err := s.backend.SuggestGasTipCap(tc.baseFee) - - if tc.expPass { - s.Require().Equal(tc.expGasTipCap, maxDelta) - } else { - s.Require().Error(err) - } - }) - } -} - -func (s *BackendSuite) TestFeeHistory() { - testCases := []struct { - name string - registerMock func(validator sdk.AccAddress) - userBlockCount ethrpc.DecimalOrHex - latestBlock ethrpc.BlockNumber - expFeeHistory *rpc.FeeHistoryResult - validator sdk.AccAddress - expPass bool - }{ - { - "fail - can't get params ", - func(validator sdk.AccAddress) { - var header metadata.MD - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - s.backend.cfg.JSONRPC.FeeHistoryCap = 0 - RegisterParamsError(queryClient, &header, ethrpc.BlockNumber(1).Int64()) - }, - 1, - -1, - nil, - nil, - false, - }, - { - "fail - user block count higher than max block count ", - func(validator sdk.AccAddress) { - var header metadata.MD - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - s.backend.cfg.JSONRPC.FeeHistoryCap = 0 - RegisterParams(queryClient, &header, ethrpc.BlockNumber(1).Int64()) - }, - 1, - -1, - nil, - nil, - false, - }, - { - "fail - Tendermint block fetching error ", - func(validator sdk.AccAddress) { - client := s.backend.clientCtx.Client.(*mocks.Client) - s.backend.cfg.JSONRPC.FeeHistoryCap = 2 - RegisterBlockError(client, ethrpc.BlockNumber(1).Int64()) - }, - 1, - 1, - nil, - nil, - false, - }, - { - "fail - Eth block fetching error", - func(validator sdk.AccAddress) { - client := s.backend.clientCtx.Client.(*mocks.Client) - s.backend.cfg.JSONRPC.FeeHistoryCap = 2 - _, err := RegisterBlock(client, ethrpc.BlockNumber(1).Int64(), nil) - s.Require().NoError(err) - RegisterBlockResultsError(client, 1) - }, - 1, - 1, - nil, - nil, - true, - }, - { - name: "pass - Valid FeeHistoryResults object", - registerMock: func(validator sdk.AccAddress) { - baseFee := sdkmath.NewInt(1) - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - client := s.backend.clientCtx.Client.(*mocks.Client) - s.backend.cfg.JSONRPC.FeeHistoryCap = 2 - blockHeight := int64(1) - _, err := RegisterBlock(client, blockHeight, nil) - s.Require().NoError(err) - - _, err = RegisterBlockResults(client, blockHeight) - s.Require().NoError(err) - - RegisterBaseFee(queryClient, baseFee) - RegisterValidatorAccount(queryClient, validator) - RegisterConsensusParams(client, blockHeight) - - header := new(metadata.MD) - RegisterParams(queryClient, header, blockHeight) - RegisterParamsWithoutHeader(queryClient, blockHeight) - }, - userBlockCount: 1, - latestBlock: 1, - expFeeHistory: &rpc.FeeHistoryResult{ - OldestBlock: (*hexutil.Big)(big.NewInt(1)), - BaseFee: []*hexutil.Big{(*hexutil.Big)(big.NewInt(1)), (*hexutil.Big)(big.NewInt(1))}, - GasUsedRatio: []float64{0}, - Reward: [][]*hexutil.Big{{(*hexutil.Big)(big.NewInt(0)), (*hexutil.Big)(big.NewInt(0)), (*hexutil.Big)(big.NewInt(0)), (*hexutil.Big)(big.NewInt(0))}}, - }, - validator: sdk.AccAddress(evmtest.NewEthPrivAcc().EthAddr.Bytes()), - expPass: true, - }, - } - - for _, tc := range testCases { - s.Run(fmt.Sprintf("case %s", tc.name), func() { - s.SetupTest() // reset test and queries - tc.registerMock(tc.validator) - - feeHistory, err := s.backend.FeeHistory(tc.userBlockCount, tc.latestBlock, []float64{25, 50, 75, 100}) - if tc.expPass { - s.Require().NoError(err) - s.Require().Equal(feeHistory, tc.expFeeHistory) - } else { - s.Require().Error(err) - } - }) - } -} +package backend_test diff --git a/eth/rpc/backend/client_test.go b/eth/rpc/backend/client_test.go index 53a34e799..f0dcea536 100644 --- a/eth/rpc/backend/client_test.go +++ b/eth/rpc/backend/client_test.go @@ -1,259 +1 @@ -package backend - -import ( - "context" - "testing" - - errortypes "github.com/cosmos/cosmos-sdk/types/errors" - - abci "github.com/cometbft/cometbft/abci/types" - "github.com/cometbft/cometbft/libs/bytes" - tmrpcclient "github.com/cometbft/cometbft/rpc/client" - tmrpctypes "github.com/cometbft/cometbft/rpc/core/types" - "github.com/cometbft/cometbft/types" - "github.com/ethereum/go-ethereum/common" - mock "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - - "github.com/NibiruChain/nibiru/v2/eth/rpc" - "github.com/NibiruChain/nibiru/v2/eth/rpc/backend/mocks" - "github.com/NibiruChain/nibiru/v2/x/evm" -) - -// Client defines a mocked object that implements the Tendermint JSON-RPC Client -// interface. It allows for performing Client queries without having to run a -// Tendermint RPC Client server. -// -// To use a mock method it has to be registered in a given test. -var _ tmrpcclient.Client = &mocks.Client{} - -// Tx Search -func RegisterTxSearch(client *mocks.Client, query string, txBz []byte) { - resulTxs := []*tmrpctypes.ResultTx{{Tx: txBz}} - client.On("TxSearch", rpc.NewContextWithHeight(1), query, false, (*int)(nil), (*int)(nil), ""). - Return(&tmrpctypes.ResultTxSearch{Txs: resulTxs, TotalCount: 1}, nil) -} - -func RegisterTxSearchEmpty(client *mocks.Client, query string) { - client.On("TxSearch", rpc.NewContextWithHeight(1), query, false, (*int)(nil), (*int)(nil), ""). - Return(&tmrpctypes.ResultTxSearch{}, nil) -} - -// Broadcast Tx -func RegisterBroadcastTx(client *mocks.Client, tx types.Tx) { - client.On("BroadcastTxSync", context.Background(), tx). - Return(&tmrpctypes.ResultBroadcastTx{}, nil) -} - -func RegisterBroadcastTxError(client *mocks.Client, tx types.Tx) { - client.On("BroadcastTxSync", context.Background(), tx). - Return(nil, errortypes.ErrInvalidRequest) -} - -// Unconfirmed Transactions -func RegisterUnconfirmedTxs(client *mocks.Client, limit *int, txs []types.Tx) { - client.On("UnconfirmedTxs", rpc.NewContextWithHeight(1), limit). - Return(&tmrpctypes.ResultUnconfirmedTxs{Txs: txs}, nil) -} - -func RegisterUnconfirmedTxsEmpty(client *mocks.Client, limit *int) { - client.On("UnconfirmedTxs", rpc.NewContextWithHeight(1), limit). - Return(&tmrpctypes.ResultUnconfirmedTxs{ - Txs: make([]types.Tx, 2), - }, nil) -} - -func RegisterUnconfirmedTxsError(client *mocks.Client, limit *int) { - client.On("UnconfirmedTxs", rpc.NewContextWithHeight(1), limit). - Return(nil, errortypes.ErrInvalidRequest) -} - -// Status -func RegisterStatus(client *mocks.Client) { - client.On("Status", rpc.NewContextWithHeight(1)). - Return(&tmrpctypes.ResultStatus{}, nil) -} - -func RegisterStatusError(client *mocks.Client) { - client.On("Status", rpc.NewContextWithHeight(1)). - Return(nil, errortypes.ErrInvalidRequest) -} - -// Block -func RegisterBlockMultipleTxs( - client *mocks.Client, - height int64, - txs []types.Tx, -) (*tmrpctypes.ResultBlock, error) { - block := types.MakeBlock(height, txs, nil, nil) - block.ChainID = ChainID - resBlock := &tmrpctypes.ResultBlock{Block: block} - client.On("Block", rpc.NewContextWithHeight(height), mock.AnythingOfType("*int64")).Return(resBlock, nil) - return resBlock, nil -} - -func RegisterBlock( - client *mocks.Client, - height int64, - tx []byte, -) (*tmrpctypes.ResultBlock, error) { - // without tx - if tx == nil { - emptyBlock := types.MakeBlock(height, []types.Tx{}, nil, nil) - emptyBlock.ChainID = ChainID - resBlock := &tmrpctypes.ResultBlock{Block: emptyBlock} - client.On("Block", rpc.NewContextWithHeight(height), mock.AnythingOfType("*int64")).Return(resBlock, nil) - return resBlock, nil - } - - // with tx - block := types.MakeBlock(height, []types.Tx{tx}, nil, nil) - block.ChainID = ChainID - resBlock := &tmrpctypes.ResultBlock{Block: block} - client.On("Block", rpc.NewContextWithHeight(height), mock.AnythingOfType("*int64")).Return(resBlock, nil) - return resBlock, nil -} - -// Block returns error -func RegisterBlockError(client *mocks.Client, height int64) { - client.On("Block", rpc.NewContextWithHeight(height), mock.AnythingOfType("*int64")). - Return(nil, errortypes.ErrInvalidRequest) -} - -// Block not found -func RegisterBlockNotFound( - client *mocks.Client, - height int64, -) (*tmrpctypes.ResultBlock, error) { - client.On("Block", rpc.NewContextWithHeight(height), mock.AnythingOfType("*int64")). - Return(&tmrpctypes.ResultBlock{Block: nil}, nil) - - return &tmrpctypes.ResultBlock{Block: nil}, nil -} - -func TestRegisterBlock(t *testing.T) { - client := mocks.NewClient(t) - height := rpc.BlockNumber(1).Int64() - _, err := RegisterBlock(client, height, nil) - require.NoError(t, err) - - res, err := client.Block(rpc.NewContextWithHeight(height), &height) - - emptyBlock := types.MakeBlock(height, []types.Tx{}, nil, nil) - emptyBlock.ChainID = ChainID - resBlock := &tmrpctypes.ResultBlock{Block: emptyBlock} - require.Equal(t, resBlock, res) - require.NoError(t, err) -} - -// ConsensusParams -func RegisterConsensusParams(client *mocks.Client, height int64) { - consensusParams := types.DefaultConsensusParams() - client.On("ConsensusParams", rpc.NewContextWithHeight(height), mock.AnythingOfType("*int64")). - Return(&tmrpctypes.ResultConsensusParams{ConsensusParams: *consensusParams}, nil) -} - -func RegisterConsensusParamsError(client *mocks.Client, height int64) { - client.On("ConsensusParams", rpc.NewContextWithHeight(height), mock.AnythingOfType("*int64")). - Return(nil, errortypes.ErrInvalidRequest) -} - -func TestRegisterConsensusParams(t *testing.T) { - client := mocks.NewClient(t) - height := int64(1) - RegisterConsensusParams(client, height) - - res, err := client.ConsensusParams(rpc.NewContextWithHeight(height), &height) - consensusParams := types.DefaultConsensusParams() - require.Equal(t, &tmrpctypes.ResultConsensusParams{ConsensusParams: *consensusParams}, res) - require.NoError(t, err) -} - -// BlockResults - -func RegisterBlockResultsWithEventLog(client *mocks.Client, height int64) (*tmrpctypes.ResultBlockResults, error) { - res := &tmrpctypes.ResultBlockResults{ - Height: height, - TxsResults: []*abci.ResponseDeliverTx{ - {Code: 0, GasUsed: 0, Events: []abci.Event{{ - Type: evm.EventTypeTxLog, - Attributes: []abci.EventAttribute{{ - Key: evm.AttributeKeyTxLog, - Value: "{\"test\": \"hello\"}", // TODO refactor the value to unmarshall to a evmtypes.Log struct successfully - Index: true, - }}, - }}}, - }, - } - client.On("BlockResults", rpc.NewContextWithHeight(height), mock.AnythingOfType("*int64")). - Return(res, nil) - return res, nil -} - -func RegisterBlockResults( - client *mocks.Client, - height int64, -) (*tmrpctypes.ResultBlockResults, error) { - res := &tmrpctypes.ResultBlockResults{ - Height: height, - TxsResults: []*abci.ResponseDeliverTx{{Code: 0, GasUsed: 0}}, - } - - client.On("BlockResults", rpc.NewContextWithHeight(height), mock.AnythingOfType("*int64")). - Return(res, nil) - return res, nil -} - -func RegisterBlockResultsError(client *mocks.Client, height int64) { - client.On("BlockResults", rpc.NewContextWithHeight(height), mock.AnythingOfType("*int64")). - Return(nil, errortypes.ErrInvalidRequest) -} - -func TestRegisterBlockResults(t *testing.T) { - client := mocks.NewClient(t) - height := int64(1) - _, err := RegisterBlockResults(client, height) - require.NoError(t, err) - - res, err := client.BlockResults(rpc.NewContextWithHeight(height), &height) - expRes := &tmrpctypes.ResultBlockResults{ - Height: height, - TxsResults: []*abci.ResponseDeliverTx{{Code: 0, GasUsed: 0}}, - } - require.Equal(t, expRes, res) - require.NoError(t, err) -} - -// BlockByHash -func RegisterBlockByHash( - client *mocks.Client, - _ common.Hash, - tx []byte, -) (*tmrpctypes.ResultBlock, error) { - block := types.MakeBlock(1, []types.Tx{tx}, nil, nil) - resBlock := &tmrpctypes.ResultBlock{Block: block} - - client.On("BlockByHash", rpc.NewContextWithHeight(1), []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}). - Return(resBlock, nil) - return resBlock, nil -} - -func RegisterBlockByHashError(client *mocks.Client, _ common.Hash, _ []byte) { - client.On("BlockByHash", rpc.NewContextWithHeight(1), []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}). - Return(nil, errortypes.ErrInvalidRequest) -} - -func RegisterBlockByHashNotFound(client *mocks.Client, _ common.Hash, _ []byte) { - client.On("BlockByHash", rpc.NewContextWithHeight(1), []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}). - Return(nil, nil) -} - -func RegisterABCIQueryWithOptions(client *mocks.Client, height int64, path string, data bytes.HexBytes, opts tmrpcclient.ABCIQueryOptions) { - client.On("ABCIQueryWithOptions", context.Background(), path, data, opts). - Return(&tmrpctypes.ResultABCIQuery{ - Response: abci.ResponseQuery{ - Value: []byte{2}, // TODO replace with data.Bytes(), - Height: height, - }, - }, nil) -} +package backend_test diff --git a/eth/rpc/backend/evm_query_client_test.go b/eth/rpc/backend/evm_query_client_test.go index ee05fe7c2..f0dcea536 100644 --- a/eth/rpc/backend/evm_query_client_test.go +++ b/eth/rpc/backend/evm_query_client_test.go @@ -1,345 +1 @@ -package backend - -import ( - "context" - "encoding/json" - "fmt" - "strconv" - "testing" - - "cosmossdk.io/math" - - sdk "github.com/cosmos/cosmos-sdk/types" - errortypes "github.com/cosmos/cosmos-sdk/types/errors" - grpctypes "github.com/cosmos/cosmos-sdk/types/grpc" - "github.com/ethereum/go-ethereum/common" - mock "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" - - "github.com/NibiruChain/nibiru/v2/eth" - "github.com/NibiruChain/nibiru/v2/eth/rpc" - "github.com/NibiruChain/nibiru/v2/eth/rpc/backend/mocks" - "github.com/NibiruChain/nibiru/v2/x/evm" - evmtest "github.com/NibiruChain/nibiru/v2/x/evm/evmtest" -) - -// QueryClient defines a mocked object that implements the ethermint GRPC -// QueryClient interface. It allows for performing QueryClient queries without having -// to run a ethermint GRPC server. -// -// To use a mock method it has to be registered in a given test. -var _ evm.QueryClient = &mocks.EVMQueryClient{} - -func TEST_CHAIN_ID_NUMBER() int64 { - n, _ := eth.ParseEthChainID(eth.EIP155ChainID_Testnet) - return n.Int64() -} - -// TraceTransaction -func RegisterTraceTransactionWithPredecessors( - queryClient *mocks.EVMQueryClient, msgEthTx *evm.MsgEthereumTx, predecessors []*evm.MsgEthereumTx, -) { - data := []byte{0x7b, 0x22, 0x74, 0x65, 0x73, 0x74, 0x22, 0x3a, 0x20, 0x22, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x22, 0x7d} - queryClient.On("TraceTx", rpc.NewContextWithHeight(1), - &evm.QueryTraceTxRequest{Msg: msgEthTx, BlockNumber: 1, Predecessors: predecessors, ChainId: TEST_CHAIN_ID_NUMBER(), BlockMaxGas: -1}). - Return(&evm.QueryTraceTxResponse{Data: data}, nil) -} - -func RegisterTraceTransaction( - queryClient *mocks.EVMQueryClient, msgEthTx *evm.MsgEthereumTx, -) { - data := []byte{0x7b, 0x22, 0x74, 0x65, 0x73, 0x74, 0x22, 0x3a, 0x20, 0x22, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x22, 0x7d} - queryClient.On("TraceTx", rpc.NewContextWithHeight(1), &evm.QueryTraceTxRequest{Msg: msgEthTx, BlockNumber: 1, ChainId: TEST_CHAIN_ID_NUMBER(), BlockMaxGas: -1}). - Return(&evm.QueryTraceTxResponse{Data: data}, nil) -} - -func RegisterTraceCall( - queryClient *mocks.EVMQueryClient, msgEthTx *evm.MsgEthereumTx, -) { - data := []byte{0x7b, 0x22, 0x74, 0x65, 0x73, 0x74, 0x22, 0x3a, 0x20, 0x22, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x22, 0x7d} - queryClient.On( - "TraceCall", - rpc.NewContextWithHeight(1), - &evm.QueryTraceTxRequest{ - Msg: msgEthTx, - BlockNumber: 1, - ChainId: TEST_CHAIN_ID_NUMBER(), - BlockMaxGas: -1, - TraceConfig: &evm.TraceConfig{}, - }, - ). - Return(&evm.QueryTraceTxResponse{Data: data}, nil) -} - -func RegisterTraceTransactionError( - queryClient *mocks.EVMQueryClient, msgEthTx *evm.MsgEthereumTx, -) { - queryClient.On("TraceTx", rpc.NewContextWithHeight(1), &evm.QueryTraceTxRequest{Msg: msgEthTx, BlockNumber: 1, ChainId: TEST_CHAIN_ID_NUMBER()}). - Return(nil, errortypes.ErrInvalidRequest) -} - -// TraceBlock -func RegisterTraceBlock( - queryClient *mocks.EVMQueryClient, txs []*evm.MsgEthereumTx, -) { - data := []byte{0x7b, 0x22, 0x74, 0x65, 0x73, 0x74, 0x22, 0x3a, 0x20, 0x22, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x22, 0x7d} - queryClient.On("TraceBlock", rpc.NewContextWithHeight(1), - &evm.QueryTraceBlockRequest{Txs: txs, BlockNumber: 1, TraceConfig: &evm.TraceConfig{}, ChainId: TEST_CHAIN_ID_NUMBER(), BlockMaxGas: -1}). - Return(&evm.QueryTraceBlockResponse{Data: data}, nil) -} - -func RegisterTraceBlockError(queryClient *mocks.EVMQueryClient) { - queryClient.On("TraceBlock", rpc.NewContextWithHeight(1), &evm.QueryTraceBlockRequest{}). - Return(nil, errortypes.ErrInvalidRequest) -} - -// Params -func RegisterParams( - queryClient *mocks.EVMQueryClient, header *metadata.MD, height int64, -) { - queryClient.On("Params", rpc.NewContextWithHeight(height), &evm.QueryParamsRequest{}, grpc.Header(header)). - Return(&evm.QueryParamsResponse{}, nil). - Run(func(args mock.Arguments) { - // If Params call is successful, also update the header height - arg := args.Get(2).(grpc.HeaderCallOption) - h := metadata.MD{} - h.Set(grpctypes.GRPCBlockHeightHeader, fmt.Sprint(height)) - *arg.HeaderAddr = h - }) -} - -func RegisterParamsWithoutHeader( - queryClient *mocks.EVMQueryClient, height int64, -) { - queryClient.On("Params", rpc.NewContextWithHeight(height), &evm.QueryParamsRequest{}). - Return(&evm.QueryParamsResponse{Params: evm.DefaultParams()}, nil) -} - -func RegisterParamsInvalidHeader( - queryClient *mocks.EVMQueryClient, header *metadata.MD, height int64, -) { - queryClient.On("Params", rpc.NewContextWithHeight(height), &evm.QueryParamsRequest{}, grpc.Header(header)). - Return(&evm.QueryParamsResponse{}, nil). - Run(func(args mock.Arguments) { - // If Params call is successful, also update the header height - arg := args.Get(2).(grpc.HeaderCallOption) - h := metadata.MD{} - *arg.HeaderAddr = h - }) -} - -func RegisterParamsInvalidHeight(queryClient *mocks.EVMQueryClient, header *metadata.MD, height int64) { - queryClient.On("Params", rpc.NewContextWithHeight(height), &evm.QueryParamsRequest{}, grpc.Header(header)). - Return(&evm.QueryParamsResponse{}, nil). - Run(func(args mock.Arguments) { - // If Params call is successful, also update the header height - arg := args.Get(2).(grpc.HeaderCallOption) - h := metadata.MD{} - h.Set(grpctypes.GRPCBlockHeightHeader, "invalid") - *arg.HeaderAddr = h - }) -} - -func RegisterParamsWithoutHeaderError(queryClient *mocks.EVMQueryClient, height int64) { - queryClient.On("Params", rpc.NewContextWithHeight(height), &evm.QueryParamsRequest{}). - Return(nil, errortypes.ErrInvalidRequest) -} - -// Params returns error -func RegisterParamsError( - queryClient *mocks.EVMQueryClient, header *metadata.MD, height int64, -) { - queryClient.On("Params", rpc.NewContextWithHeight(height), &evm.QueryParamsRequest{}, grpc.Header(header)). - Return(nil, errortypes.ErrInvalidRequest) -} - -func TestRegisterParams(t *testing.T) { - var header metadata.MD - queryClient := mocks.NewEVMQueryClient(t) - - height := int64(1) - RegisterParams(queryClient, &header, height) - - _, err := queryClient.Params(rpc.NewContextWithHeight(height), &evm.QueryParamsRequest{}, grpc.Header(&header)) - require.NoError(t, err) - blockHeightHeader := header.Get(grpctypes.GRPCBlockHeightHeader) - headerHeight, err := strconv.ParseInt(blockHeightHeader[0], 10, 64) - require.NoError(t, err) - require.Equal(t, height, headerHeight) -} - -func TestRegisterParamsError(t *testing.T) { - queryClient := mocks.NewEVMQueryClient(t) - RegisterBaseFeeError(queryClient) - _, err := queryClient.BaseFee(rpc.NewContextWithHeight(1), &evm.QueryBaseFeeRequest{}) - require.Error(t, err) -} - -// ETH Call -func RegisterEthCall( - queryClient *mocks.EVMQueryClient, request *evm.EthCallRequest, -) { - ctx, _ := context.WithCancel(rpc.NewContextWithHeight(1)) //nolint - queryClient.On("EthCall", ctx, request). - Return(&evm.MsgEthereumTxResponse{}, nil) -} - -func RegisterEthCallError( - queryClient *mocks.EVMQueryClient, request *evm.EthCallRequest, -) { - ctx, _ := context.WithCancel(rpc.NewContextWithHeight(1)) //nolint - queryClient.On("EthCall", ctx, request). - Return(nil, errortypes.ErrInvalidRequest) -} - -// Estimate Gas -func RegisterEstimateGas( - queryClient *mocks.EVMQueryClient, args evm.JsonTxArgs, -) { - bz, _ := json.Marshal(args) - queryClient.On("EstimateGas", rpc.NewContextWithHeight(1), &evm.EthCallRequest{Args: bz, ChainId: args.ChainID.ToInt().Int64()}). - Return(&evm.EstimateGasResponse{}, nil) -} - -// BaseFee -func RegisterBaseFee( - queryClient *mocks.EVMQueryClient, baseFee math.Int, -) { - queryClient.On("BaseFee", rpc.NewContextWithHeight(1), &evm.QueryBaseFeeRequest{}). - Return(&evm.QueryBaseFeeResponse{BaseFee: &baseFee}, nil) -} - -// Base fee returns error -func RegisterBaseFeeError(queryClient *mocks.EVMQueryClient) { - queryClient.On("BaseFee", rpc.NewContextWithHeight(1), &evm.QueryBaseFeeRequest{}). - Return(&evm.QueryBaseFeeResponse{}, evm.ErrInvalidBaseFee) -} - -// Base fee not enabled -func RegisterBaseFeeDisabled(queryClient *mocks.EVMQueryClient) { - queryClient.On("BaseFee", rpc.NewContextWithHeight(1), &evm.QueryBaseFeeRequest{}). - Return(&evm.QueryBaseFeeResponse{}, nil) -} - -func TestRegisterBaseFee(t *testing.T) { - baseFee := math.NewInt(1) - queryClient := mocks.NewEVMQueryClient(t) - RegisterBaseFee(queryClient, baseFee) - res, err := queryClient.BaseFee(rpc.NewContextWithHeight(1), &evm.QueryBaseFeeRequest{}) - require.Equal(t, &evm.QueryBaseFeeResponse{BaseFee: &baseFee}, res) - require.NoError(t, err) -} - -func TestRegisterBaseFeeError(t *testing.T) { - queryClient := mocks.NewEVMQueryClient(t) - RegisterBaseFeeError(queryClient) - res, err := queryClient.BaseFee(rpc.NewContextWithHeight(1), &evm.QueryBaseFeeRequest{}) - require.Equal(t, &evm.QueryBaseFeeResponse{}, res) - require.Error(t, err) -} - -func TestRegisterBaseFeeDisabled(t *testing.T) { - queryClient := mocks.NewEVMQueryClient(t) - RegisterBaseFeeDisabled(queryClient) - res, err := queryClient.BaseFee(rpc.NewContextWithHeight(1), &evm.QueryBaseFeeRequest{}) - require.Equal(t, &evm.QueryBaseFeeResponse{}, res) - require.NoError(t, err) -} - -// ValidatorAccount -func RegisterValidatorAccount( - queryClient *mocks.EVMQueryClient, validator sdk.AccAddress, -) { - queryClient.On("ValidatorAccount", rpc.NewContextWithHeight(1), &evm.QueryValidatorAccountRequest{}). - Return(&evm.QueryValidatorAccountResponse{AccountAddress: validator.String()}, nil) -} - -func RegisterValidatorAccountError(queryClient *mocks.EVMQueryClient) { - queryClient.On("ValidatorAccount", rpc.NewContextWithHeight(1), &evm.QueryValidatorAccountRequest{}). - Return(nil, status.Error(codes.InvalidArgument, "empty request")) -} - -func TestRegisterValidatorAccount(t *testing.T) { - queryClient := mocks.NewEVMQueryClient(t) - - validator := sdk.AccAddress(evmtest.NewEthPrivAcc().EthAddr.Bytes()) - RegisterValidatorAccount(queryClient, validator) - res, err := queryClient.ValidatorAccount(rpc.NewContextWithHeight(1), &evm.QueryValidatorAccountRequest{}) - require.Equal(t, &evm.QueryValidatorAccountResponse{AccountAddress: validator.String()}, res) - require.NoError(t, err) -} - -// Code -func RegisterCode( - queryClient *mocks.EVMQueryClient, addr common.Address, code []byte, -) { - queryClient.On("Code", rpc.NewContextWithHeight(1), &evm.QueryCodeRequest{Address: addr.String()}). - Return(&evm.QueryCodeResponse{Code: code}, nil) -} - -func RegisterCodeError(queryClient *mocks.EVMQueryClient, addr common.Address) { - queryClient.On("Code", rpc.NewContextWithHeight(1), &evm.QueryCodeRequest{Address: addr.String()}). - Return(nil, errortypes.ErrInvalidRequest) -} - -// Storage -func RegisterStorageAt( - queryClient *mocks.EVMQueryClient, addr common.Address, - key string, storage string, -) { - queryClient.On("Storage", rpc.NewContextWithHeight(1), &evm.QueryStorageRequest{Address: addr.String(), Key: key}). - Return(&evm.QueryStorageResponse{Value: storage}, nil) -} - -func RegisterStorageAtError( - queryClient *mocks.EVMQueryClient, addr common.Address, key string, -) { - queryClient.On("Storage", rpc.NewContextWithHeight(1), &evm.QueryStorageRequest{Address: addr.String(), Key: key}). - Return(nil, errortypes.ErrInvalidRequest) -} - -func RegisterAccount( - queryClient *mocks.EVMQueryClient, addr common.Address, height int64, -) { - queryClient.On("EthAccount", rpc.NewContextWithHeight(height), &evm.QueryEthAccountRequest{Address: addr.String()}). - Return(&evm.QueryEthAccountResponse{ - BalanceWei: "0", - CodeHash: "", - Nonce: 0, - }, - nil, - ) -} - -// Balance -func RegisterBalance( - queryClient *mocks.EVMQueryClient, addr common.Address, height int64, -) { - queryClient.On("Balance", rpc.NewContextWithHeight(height), &evm.QueryBalanceRequest{Address: addr.String()}). - Return(&evm.QueryBalanceResponse{BalanceWei: "1"}, nil) -} - -func RegisterBalanceInvalid( - queryClient *mocks.EVMQueryClient, addr common.Address, height int64, -) { - queryClient.On("Balance", rpc.NewContextWithHeight(height), &evm.QueryBalanceRequest{Address: addr.String()}). - Return(&evm.QueryBalanceResponse{BalanceWei: "invalid"}, nil) -} - -func RegisterBalanceNegative( - queryClient *mocks.EVMQueryClient, addr common.Address, height int64, -) { - queryClient.On("Balance", rpc.NewContextWithHeight(height), &evm.QueryBalanceRequest{Address: addr.String()}). - Return(&evm.QueryBalanceResponse{BalanceWei: "-1"}, nil) -} - -func RegisterBalanceError( - queryClient *mocks.EVMQueryClient, addr common.Address, height int64, -) { - queryClient.On("Balance", rpc.NewContextWithHeight(height), &evm.QueryBalanceRequest{Address: addr.String()}). - Return(nil, errortypes.ErrInvalidRequest) -} +package backend_test diff --git a/eth/rpc/backend/filters_test.go b/eth/rpc/backend/filters_test.go index e9c414648..f0dcea536 100644 --- a/eth/rpc/backend/filters_test.go +++ b/eth/rpc/backend/filters_test.go @@ -1,123 +1 @@ -package backend - -import ( - "encoding/json" - - tmtypes "github.com/cometbft/cometbft/types" - "github.com/ethereum/go-ethereum/common" - gethcore "github.com/ethereum/go-ethereum/core/types" - - "github.com/NibiruChain/nibiru/v2/eth/rpc" - "github.com/NibiruChain/nibiru/v2/eth/rpc/backend/mocks" - "github.com/NibiruChain/nibiru/v2/x/evm" -) - -func (s *BackendSuite) TestGetLogs() { - _, bz := s.buildEthereumTx() - block := tmtypes.MakeBlock(1, []tmtypes.Tx{bz}, nil, nil) - logs := make([]*evm.Log, 0, 1) - var log evm.Log - err := json.Unmarshal([]byte("{\"test\": \"hello\"}"), &log) // TODO refactor this to unmarshall to a log struct successfully - s.Require().NoError(err) - - logs = append(logs, &log) - - testCases := []struct { - name string - registerMock func(hash common.Hash) - blockHash common.Hash - expLogs [][]*gethcore.Log - expPass bool - }{ - { - "fail - no block with that hash", - func(hash common.Hash) { - client := s.backend.clientCtx.Client.(*mocks.Client) - RegisterBlockByHashNotFound(client, hash, bz) - }, - common.Hash{}, - nil, - false, - }, - { - "fail - error fetching block by hash", - func(hash common.Hash) { - client := s.backend.clientCtx.Client.(*mocks.Client) - RegisterBlockByHashError(client, hash, bz) - }, - common.Hash{}, - nil, - false, - }, - { - "fail - error getting block results", - func(hash common.Hash) { - client := s.backend.clientCtx.Client.(*mocks.Client) - _, err := RegisterBlockByHash(client, hash, bz) - s.Require().NoError(err) - RegisterBlockResultsError(client, 1) - }, - common.Hash{}, - nil, - false, - }, - { - "success - getting logs with block hash", - func(hash common.Hash) { - client := s.backend.clientCtx.Client.(*mocks.Client) - _, err := RegisterBlockByHash(client, hash, bz) - s.Require().NoError(err) - _, err = RegisterBlockResultsWithEventLog(client, rpc.BlockNumber(1).Int64()) - s.Require().NoError(err) - }, - common.BytesToHash(block.Hash()), - [][]*gethcore.Log{evm.LogsToEthereum(logs)}, - true, - }, - } - - for _, tc := range testCases { - s.Run(tc.name, func() { - s.SetupTest() - - tc.registerMock(tc.blockHash) - logs, err := s.backend.GetLogs(tc.blockHash) - - if tc.expPass { - s.Require().NoError(err) - s.Require().Equal(tc.expLogs, logs) - } else { - s.Require().Error(err) - } - }) - } -} - -func (s *BackendSuite) TestBloomStatus() { - testCases := []struct { - name string - registerMock func() - expResult uint64 - expPass bool - }{ - { - "pass - returns the BloomBitsBlocks and the number of processed sections maintained", - func() {}, - 4096, - true, - }, - } - - for _, tc := range testCases { - s.Run(tc.name, func() { - s.SetupTest() - - tc.registerMock() - bloom, _ := s.backend.BloomStatus() - - if tc.expPass { - s.Require().Equal(tc.expResult, bloom) - } - }) - } -} +package backend_test diff --git a/eth/rpc/backend/mocks/README.md b/eth/rpc/backend/mocks/README.md deleted file mode 100644 index 376dbe6de..000000000 --- a/eth/rpc/backend/mocks/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Mocks Generation - -To generate mocks, install `mockery` tool: -https://vektra.github.io/mockery/latest/installation/ - -```bash -cd x/evm -mockery \ - --name QueryClient \ - --filename evm_query_client.go \ - --output ../../eth/rpc/backend/mocks \ - --structname EVMQueryClient -``` diff --git a/eth/rpc/backend/mocks/client.go b/eth/rpc/backend/mocks/client.go deleted file mode 100644 index 2fabaa113..000000000 --- a/eth/rpc/backend/mocks/client.go +++ /dev/null @@ -1,887 +0,0 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. - -package mocks - -import ( - bytes "github.com/cometbft/cometbft/libs/bytes" - client "github.com/cometbft/cometbft/rpc/client" - - context "context" - - coretypes "github.com/cometbft/cometbft/rpc/core/types" - - log "github.com/cometbft/cometbft/libs/log" - - mock "github.com/stretchr/testify/mock" - - types "github.com/cometbft/cometbft/types" -) - -// Client is an autogenerated mock type for the Client type -type Client struct { - mock.Mock -} - -// ABCIInfo provides a mock function with given fields: _a0 -func (_m *Client) ABCIInfo(_a0 context.Context) (*coretypes.ResultABCIInfo, error) { - ret := _m.Called(_a0) - - var r0 *coretypes.ResultABCIInfo - if rf, ok := ret.Get(0).(func(context.Context) *coretypes.ResultABCIInfo); ok { - r0 = rf(_a0) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*coretypes.ResultABCIInfo) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(_a0) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// ABCIQuery provides a mock function with given fields: ctx, path, data -func (_m *Client) ABCIQuery(ctx context.Context, path string, data bytes.HexBytes) (*coretypes.ResultABCIQuery, error) { - ret := _m.Called(ctx, path, data) - - var r0 *coretypes.ResultABCIQuery - if rf, ok := ret.Get(0).(func(context.Context, string, bytes.HexBytes) *coretypes.ResultABCIQuery); ok { - r0 = rf(ctx, path, data) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*coretypes.ResultABCIQuery) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, string, bytes.HexBytes) error); ok { - r1 = rf(ctx, path, data) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// ABCIQueryWithOptions provides a mock function with given fields: ctx, path, data, opts -func (_m *Client) ABCIQueryWithOptions(ctx context.Context, path string, data bytes.HexBytes, opts client.ABCIQueryOptions) (*coretypes.ResultABCIQuery, error) { - ret := _m.Called(ctx, path, data, opts) - - var r0 *coretypes.ResultABCIQuery - if rf, ok := ret.Get(0).(func(context.Context, string, bytes.HexBytes, client.ABCIQueryOptions) *coretypes.ResultABCIQuery); ok { - r0 = rf(ctx, path, data, opts) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*coretypes.ResultABCIQuery) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, string, bytes.HexBytes, client.ABCIQueryOptions) error); ok { - r1 = rf(ctx, path, data, opts) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Block provides a mock function with given fields: ctx, height -func (_m *Client) Block(ctx context.Context, height *int64) (*coretypes.ResultBlock, error) { - ret := _m.Called(ctx, height) - - var r0 *coretypes.ResultBlock - if rf, ok := ret.Get(0).(func(context.Context, *int64) *coretypes.ResultBlock); ok { - r0 = rf(ctx, height) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*coretypes.ResultBlock) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *int64) error); ok { - r1 = rf(ctx, height) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// BlockByHash provides a mock function with given fields: ctx, hash -func (_m *Client) BlockByHash(ctx context.Context, hash []byte) (*coretypes.ResultBlock, error) { - ret := _m.Called(ctx, hash) - - var r0 *coretypes.ResultBlock - if rf, ok := ret.Get(0).(func(context.Context, []byte) *coretypes.ResultBlock); ok { - r0 = rf(ctx, hash) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*coretypes.ResultBlock) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, []byte) error); ok { - r1 = rf(ctx, hash) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// BlockResults provides a mock function with given fields: ctx, height -func (_m *Client) BlockResults(ctx context.Context, height *int64) (*coretypes.ResultBlockResults, error) { - ret := _m.Called(ctx, height) - - var r0 *coretypes.ResultBlockResults - if rf, ok := ret.Get(0).(func(context.Context, *int64) *coretypes.ResultBlockResults); ok { - r0 = rf(ctx, height) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*coretypes.ResultBlockResults) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *int64) error); ok { - r1 = rf(ctx, height) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// BlockSearch provides a mock function with given fields: ctx, query, page, perPage, orderBy -func (_m *Client) BlockSearch(ctx context.Context, query string, page *int, perPage *int, orderBy string) (*coretypes.ResultBlockSearch, error) { - ret := _m.Called(ctx, query, page, perPage, orderBy) - - var r0 *coretypes.ResultBlockSearch - if rf, ok := ret.Get(0).(func(context.Context, string, *int, *int, string) *coretypes.ResultBlockSearch); ok { - r0 = rf(ctx, query, page, perPage, orderBy) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*coretypes.ResultBlockSearch) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, string, *int, *int, string) error); ok { - r1 = rf(ctx, query, page, perPage, orderBy) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// BlockchainInfo provides a mock function with given fields: ctx, minHeight, maxHeight -func (_m *Client) BlockchainInfo(ctx context.Context, minHeight int64, maxHeight int64) (*coretypes.ResultBlockchainInfo, error) { - ret := _m.Called(ctx, minHeight, maxHeight) - - var r0 *coretypes.ResultBlockchainInfo - if rf, ok := ret.Get(0).(func(context.Context, int64, int64) *coretypes.ResultBlockchainInfo); ok { - r0 = rf(ctx, minHeight, maxHeight) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*coretypes.ResultBlockchainInfo) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, int64, int64) error); ok { - r1 = rf(ctx, minHeight, maxHeight) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// BroadcastEvidence provides a mock function with given fields: _a0, _a1 -func (_m *Client) BroadcastEvidence(_a0 context.Context, _a1 types.Evidence) (*coretypes.ResultBroadcastEvidence, error) { - ret := _m.Called(_a0, _a1) - - var r0 *coretypes.ResultBroadcastEvidence - if rf, ok := ret.Get(0).(func(context.Context, types.Evidence) *coretypes.ResultBroadcastEvidence); ok { - r0 = rf(_a0, _a1) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*coretypes.ResultBroadcastEvidence) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, types.Evidence) error); ok { - r1 = rf(_a0, _a1) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// BroadcastTxAsync provides a mock function with given fields: _a0, _a1 -func (_m *Client) BroadcastTxAsync(_a0 context.Context, _a1 types.Tx) (*coretypes.ResultBroadcastTx, error) { - ret := _m.Called(_a0, _a1) - - var r0 *coretypes.ResultBroadcastTx - if rf, ok := ret.Get(0).(func(context.Context, types.Tx) *coretypes.ResultBroadcastTx); ok { - r0 = rf(_a0, _a1) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*coretypes.ResultBroadcastTx) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, types.Tx) error); ok { - r1 = rf(_a0, _a1) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// BroadcastTxCommit provides a mock function with given fields: _a0, _a1 -func (_m *Client) BroadcastTxCommit(_a0 context.Context, _a1 types.Tx) (*coretypes.ResultBroadcastTxCommit, error) { - ret := _m.Called(_a0, _a1) - - var r0 *coretypes.ResultBroadcastTxCommit - if rf, ok := ret.Get(0).(func(context.Context, types.Tx) *coretypes.ResultBroadcastTxCommit); ok { - r0 = rf(_a0, _a1) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*coretypes.ResultBroadcastTxCommit) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, types.Tx) error); ok { - r1 = rf(_a0, _a1) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// BroadcastTxSync provides a mock function with given fields: _a0, _a1 -func (_m *Client) BroadcastTxSync(_a0 context.Context, _a1 types.Tx) (*coretypes.ResultBroadcastTx, error) { - ret := _m.Called(_a0, _a1) - - var r0 *coretypes.ResultBroadcastTx - if rf, ok := ret.Get(0).(func(context.Context, types.Tx) *coretypes.ResultBroadcastTx); ok { - r0 = rf(_a0, _a1) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*coretypes.ResultBroadcastTx) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, types.Tx) error); ok { - r1 = rf(_a0, _a1) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// CheckTx provides a mock function with given fields: _a0, _a1 -func (_m *Client) CheckTx(_a0 context.Context, _a1 types.Tx) (*coretypes.ResultCheckTx, error) { - ret := _m.Called(_a0, _a1) - - var r0 *coretypes.ResultCheckTx - if rf, ok := ret.Get(0).(func(context.Context, types.Tx) *coretypes.ResultCheckTx); ok { - r0 = rf(_a0, _a1) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*coretypes.ResultCheckTx) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, types.Tx) error); ok { - r1 = rf(_a0, _a1) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Commit provides a mock function with given fields: ctx, height -func (_m *Client) Commit(ctx context.Context, height *int64) (*coretypes.ResultCommit, error) { - ret := _m.Called(ctx, height) - - var r0 *coretypes.ResultCommit - if rf, ok := ret.Get(0).(func(context.Context, *int64) *coretypes.ResultCommit); ok { - r0 = rf(ctx, height) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*coretypes.ResultCommit) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *int64) error); ok { - r1 = rf(ctx, height) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// ConsensusParams provides a mock function with given fields: ctx, height -func (_m *Client) ConsensusParams(ctx context.Context, height *int64) (*coretypes.ResultConsensusParams, error) { - ret := _m.Called(ctx, height) - - var r0 *coretypes.ResultConsensusParams - if rf, ok := ret.Get(0).(func(context.Context, *int64) *coretypes.ResultConsensusParams); ok { - r0 = rf(ctx, height) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*coretypes.ResultConsensusParams) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *int64) error); ok { - r1 = rf(ctx, height) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// ConsensusState provides a mock function with given fields: _a0 -func (_m *Client) ConsensusState(_a0 context.Context) (*coretypes.ResultConsensusState, error) { - ret := _m.Called(_a0) - - var r0 *coretypes.ResultConsensusState - if rf, ok := ret.Get(0).(func(context.Context) *coretypes.ResultConsensusState); ok { - r0 = rf(_a0) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*coretypes.ResultConsensusState) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(_a0) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// DumpConsensusState provides a mock function with given fields: _a0 -func (_m *Client) DumpConsensusState(_a0 context.Context) (*coretypes.ResultDumpConsensusState, error) { - ret := _m.Called(_a0) - - var r0 *coretypes.ResultDumpConsensusState - if rf, ok := ret.Get(0).(func(context.Context) *coretypes.ResultDumpConsensusState); ok { - r0 = rf(_a0) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*coretypes.ResultDumpConsensusState) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(_a0) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Genesis provides a mock function with given fields: _a0 -func (_m *Client) Genesis(_a0 context.Context) (*coretypes.ResultGenesis, error) { - ret := _m.Called(_a0) - - var r0 *coretypes.ResultGenesis - if rf, ok := ret.Get(0).(func(context.Context) *coretypes.ResultGenesis); ok { - r0 = rf(_a0) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*coretypes.ResultGenesis) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(_a0) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GenesisChunked provides a mock function with given fields: _a0, _a1 -func (_m *Client) GenesisChunked(_a0 context.Context, _a1 uint) (*coretypes.ResultGenesisChunk, error) { - ret := _m.Called(_a0, _a1) - - var r0 *coretypes.ResultGenesisChunk - if rf, ok := ret.Get(0).(func(context.Context, uint) *coretypes.ResultGenesisChunk); ok { - r0 = rf(_a0, _a1) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*coretypes.ResultGenesisChunk) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, uint) error); ok { - r1 = rf(_a0, _a1) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Header provides a mock function with given fields: ctx, height -func (_m *Client) Header(ctx context.Context, height *int64) (*coretypes.ResultHeader, error) { - ret := _m.Called(ctx, height) - - var r0 *coretypes.ResultHeader - if rf, ok := ret.Get(0).(func(context.Context, *int64) *coretypes.ResultHeader); ok { - r0 = rf(ctx, height) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*coretypes.ResultHeader) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *int64) error); ok { - r1 = rf(ctx, height) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// HeaderByHash provides a mock function with given fields: ctx, hash -func (_m *Client) HeaderByHash(ctx context.Context, hash bytes.HexBytes) (*coretypes.ResultHeader, error) { - ret := _m.Called(ctx, hash) - - var r0 *coretypes.ResultHeader - if rf, ok := ret.Get(0).(func(context.Context, bytes.HexBytes) *coretypes.ResultHeader); ok { - r0 = rf(ctx, hash) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*coretypes.ResultHeader) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, bytes.HexBytes) error); ok { - r1 = rf(ctx, hash) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Health provides a mock function with given fields: _a0 -func (_m *Client) Health(_a0 context.Context) (*coretypes.ResultHealth, error) { - ret := _m.Called(_a0) - - var r0 *coretypes.ResultHealth - if rf, ok := ret.Get(0).(func(context.Context) *coretypes.ResultHealth); ok { - r0 = rf(_a0) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*coretypes.ResultHealth) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(_a0) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// IsRunning provides a mock function with given fields: -func (_m *Client) IsRunning() bool { - ret := _m.Called() - - var r0 bool - if rf, ok := ret.Get(0).(func() bool); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(bool) - } - - return r0 -} - -// NetInfo provides a mock function with given fields: _a0 -func (_m *Client) NetInfo(_a0 context.Context) (*coretypes.ResultNetInfo, error) { - ret := _m.Called(_a0) - - var r0 *coretypes.ResultNetInfo - if rf, ok := ret.Get(0).(func(context.Context) *coretypes.ResultNetInfo); ok { - r0 = rf(_a0) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*coretypes.ResultNetInfo) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(_a0) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// NumUnconfirmedTxs provides a mock function with given fields: _a0 -func (_m *Client) NumUnconfirmedTxs(_a0 context.Context) (*coretypes.ResultUnconfirmedTxs, error) { - ret := _m.Called(_a0) - - var r0 *coretypes.ResultUnconfirmedTxs - if rf, ok := ret.Get(0).(func(context.Context) *coretypes.ResultUnconfirmedTxs); ok { - r0 = rf(_a0) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*coretypes.ResultUnconfirmedTxs) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(_a0) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// OnReset provides a mock function with given fields: -func (_m *Client) OnReset() error { - ret := _m.Called() - - var r0 error - if rf, ok := ret.Get(0).(func() error); ok { - r0 = rf() - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// OnStart provides a mock function with given fields: -func (_m *Client) OnStart() error { - ret := _m.Called() - - var r0 error - if rf, ok := ret.Get(0).(func() error); ok { - r0 = rf() - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// OnStop provides a mock function with given fields: -func (_m *Client) OnStop() { - _m.Called() -} - -// Quit provides a mock function with given fields: -func (_m *Client) Quit() <-chan struct{} { - ret := _m.Called() - - var r0 <-chan struct{} - if rf, ok := ret.Get(0).(func() <-chan struct{}); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(<-chan struct{}) - } - } - - return r0 -} - -// Reset provides a mock function with given fields: -func (_m *Client) Reset() error { - ret := _m.Called() - - var r0 error - if rf, ok := ret.Get(0).(func() error); ok { - r0 = rf() - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// SetLogger provides a mock function with given fields: _a0 -func (_m *Client) SetLogger(_a0 log.Logger) { - _m.Called(_a0) -} - -// Start provides a mock function with given fields: -func (_m *Client) Start() error { - ret := _m.Called() - - var r0 error - if rf, ok := ret.Get(0).(func() error); ok { - r0 = rf() - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// Status provides a mock function with given fields: _a0 -func (_m *Client) Status(_a0 context.Context) (*coretypes.ResultStatus, error) { - ret := _m.Called(_a0) - - var r0 *coretypes.ResultStatus - if rf, ok := ret.Get(0).(func(context.Context) *coretypes.ResultStatus); ok { - r0 = rf(_a0) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*coretypes.ResultStatus) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(_a0) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Stop provides a mock function with given fields: -func (_m *Client) Stop() error { - ret := _m.Called() - - var r0 error - if rf, ok := ret.Get(0).(func() error); ok { - r0 = rf() - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// String provides a mock function with given fields: -func (_m *Client) String() string { - ret := _m.Called() - - var r0 string - if rf, ok := ret.Get(0).(func() string); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(string) - } - - return r0 -} - -// Subscribe provides a mock function with given fields: ctx, subscriber, query, outCapacity -func (_m *Client) Subscribe(ctx context.Context, subscriber string, query string, outCapacity ...int) (<-chan coretypes.ResultEvent, error) { - _va := make([]interface{}, len(outCapacity)) - for _i := range outCapacity { - _va[_i] = outCapacity[_i] - } - var _ca []interface{} - _ca = append(_ca, ctx, subscriber, query) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - var r0 <-chan coretypes.ResultEvent - if rf, ok := ret.Get(0).(func(context.Context, string, string, ...int) <-chan coretypes.ResultEvent); ok { - r0 = rf(ctx, subscriber, query, outCapacity...) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(<-chan coretypes.ResultEvent) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, string, string, ...int) error); ok { - r1 = rf(ctx, subscriber, query, outCapacity...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Tx provides a mock function with given fields: ctx, hash, prove -func (_m *Client) Tx(ctx context.Context, hash []byte, prove bool) (*coretypes.ResultTx, error) { - ret := _m.Called(ctx, hash, prove) - - var r0 *coretypes.ResultTx - if rf, ok := ret.Get(0).(func(context.Context, []byte, bool) *coretypes.ResultTx); ok { - r0 = rf(ctx, hash, prove) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*coretypes.ResultTx) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, []byte, bool) error); ok { - r1 = rf(ctx, hash, prove) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// TxSearch provides a mock function with given fields: ctx, query, prove, page, perPage, orderBy -func (_m *Client) TxSearch(ctx context.Context, query string, prove bool, page *int, perPage *int, orderBy string) (*coretypes.ResultTxSearch, error) { - ret := _m.Called(ctx, query, prove, page, perPage, orderBy) - - var r0 *coretypes.ResultTxSearch - if rf, ok := ret.Get(0).(func(context.Context, string, bool, *int, *int, string) *coretypes.ResultTxSearch); ok { - r0 = rf(ctx, query, prove, page, perPage, orderBy) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*coretypes.ResultTxSearch) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, string, bool, *int, *int, string) error); ok { - r1 = rf(ctx, query, prove, page, perPage, orderBy) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// UnconfirmedTxs provides a mock function with given fields: ctx, limit -func (_m *Client) UnconfirmedTxs(ctx context.Context, limit *int) (*coretypes.ResultUnconfirmedTxs, error) { - ret := _m.Called(ctx, limit) - - var r0 *coretypes.ResultUnconfirmedTxs - if rf, ok := ret.Get(0).(func(context.Context, *int) *coretypes.ResultUnconfirmedTxs); ok { - r0 = rf(ctx, limit) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*coretypes.ResultUnconfirmedTxs) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *int) error); ok { - r1 = rf(ctx, limit) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Unsubscribe provides a mock function with given fields: ctx, subscriber, query -func (_m *Client) Unsubscribe(ctx context.Context, subscriber string, query string) error { - ret := _m.Called(ctx, subscriber, query) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { - r0 = rf(ctx, subscriber, query) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// UnsubscribeAll provides a mock function with given fields: ctx, subscriber -func (_m *Client) UnsubscribeAll(ctx context.Context, subscriber string) error { - ret := _m.Called(ctx, subscriber) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { - r0 = rf(ctx, subscriber) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// Validators provides a mock function with given fields: ctx, height, page, perPage -func (_m *Client) Validators(ctx context.Context, height *int64, page *int, perPage *int) (*coretypes.ResultValidators, error) { - ret := _m.Called(ctx, height, page, perPage) - - var r0 *coretypes.ResultValidators - if rf, ok := ret.Get(0).(func(context.Context, *int64, *int, *int) *coretypes.ResultValidators); ok { - r0 = rf(ctx, height, page, perPage) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*coretypes.ResultValidators) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *int64, *int, *int) error); ok { - r1 = rf(ctx, height, page, perPage) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -type mockConstructorTestingTNewClient interface { - mock.TestingT - Cleanup(func()) -} - -// NewClient creates a new instance of Client. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewClient(t mockConstructorTestingTNewClient) *Client { - mock := &Client{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/eth/rpc/backend/mocks/evm_query_client.go b/eth/rpc/backend/mocks/evm_query_client.go deleted file mode 100644 index 301e84995..000000000 --- a/eth/rpc/backend/mocks/evm_query_client.go +++ /dev/null @@ -1,512 +0,0 @@ -// Code generated by mockery v2.45.0. DO NOT EDIT. - -package mocks - -import ( - context "context" - - evm "github.com/NibiruChain/nibiru/v2/x/evm" - grpc "google.golang.org/grpc" - - mock "github.com/stretchr/testify/mock" -) - -// EVMQueryClient is an autogenerated mock type for the QueryClient type -type EVMQueryClient struct { - mock.Mock -} - -// Balance provides a mock function with given fields: ctx, in, opts -func (_m *EVMQueryClient) Balance(ctx context.Context, in *evm.QueryBalanceRequest, opts ...grpc.CallOption) (*evm.QueryBalanceResponse, error) { - _va := make([]interface{}, len(opts)) - for _i := range opts { - _va[_i] = opts[_i] - } - var _ca []interface{} - _ca = append(_ca, ctx, in) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for Balance") - } - - var r0 *evm.QueryBalanceResponse - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *evm.QueryBalanceRequest, ...grpc.CallOption) (*evm.QueryBalanceResponse, error)); ok { - return rf(ctx, in, opts...) - } - if rf, ok := ret.Get(0).(func(context.Context, *evm.QueryBalanceRequest, ...grpc.CallOption) *evm.QueryBalanceResponse); ok { - r0 = rf(ctx, in, opts...) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*evm.QueryBalanceResponse) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *evm.QueryBalanceRequest, ...grpc.CallOption) error); ok { - r1 = rf(ctx, in, opts...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// BaseFee provides a mock function with given fields: ctx, in, opts -func (_m *EVMQueryClient) BaseFee(ctx context.Context, in *evm.QueryBaseFeeRequest, opts ...grpc.CallOption) (*evm.QueryBaseFeeResponse, error) { - _va := make([]interface{}, len(opts)) - for _i := range opts { - _va[_i] = opts[_i] - } - var _ca []interface{} - _ca = append(_ca, ctx, in) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for BaseFee") - } - - var r0 *evm.QueryBaseFeeResponse - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *evm.QueryBaseFeeRequest, ...grpc.CallOption) (*evm.QueryBaseFeeResponse, error)); ok { - return rf(ctx, in, opts...) - } - if rf, ok := ret.Get(0).(func(context.Context, *evm.QueryBaseFeeRequest, ...grpc.CallOption) *evm.QueryBaseFeeResponse); ok { - r0 = rf(ctx, in, opts...) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*evm.QueryBaseFeeResponse) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *evm.QueryBaseFeeRequest, ...grpc.CallOption) error); ok { - r1 = rf(ctx, in, opts...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Code provides a mock function with given fields: ctx, in, opts -func (_m *EVMQueryClient) Code(ctx context.Context, in *evm.QueryCodeRequest, opts ...grpc.CallOption) (*evm.QueryCodeResponse, error) { - _va := make([]interface{}, len(opts)) - for _i := range opts { - _va[_i] = opts[_i] - } - var _ca []interface{} - _ca = append(_ca, ctx, in) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for Code") - } - - var r0 *evm.QueryCodeResponse - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *evm.QueryCodeRequest, ...grpc.CallOption) (*evm.QueryCodeResponse, error)); ok { - return rf(ctx, in, opts...) - } - if rf, ok := ret.Get(0).(func(context.Context, *evm.QueryCodeRequest, ...grpc.CallOption) *evm.QueryCodeResponse); ok { - r0 = rf(ctx, in, opts...) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*evm.QueryCodeResponse) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *evm.QueryCodeRequest, ...grpc.CallOption) error); ok { - r1 = rf(ctx, in, opts...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// EstimateGas provides a mock function with given fields: ctx, in, opts -func (_m *EVMQueryClient) EstimateGas(ctx context.Context, in *evm.EthCallRequest, opts ...grpc.CallOption) (*evm.EstimateGasResponse, error) { - _va := make([]interface{}, len(opts)) - for _i := range opts { - _va[_i] = opts[_i] - } - var _ca []interface{} - _ca = append(_ca, ctx, in) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for EstimateGas") - } - - var r0 *evm.EstimateGasResponse - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *evm.EthCallRequest, ...grpc.CallOption) (*evm.EstimateGasResponse, error)); ok { - return rf(ctx, in, opts...) - } - if rf, ok := ret.Get(0).(func(context.Context, *evm.EthCallRequest, ...grpc.CallOption) *evm.EstimateGasResponse); ok { - r0 = rf(ctx, in, opts...) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*evm.EstimateGasResponse) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *evm.EthCallRequest, ...grpc.CallOption) error); ok { - r1 = rf(ctx, in, opts...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// EthAccount provides a mock function with given fields: ctx, in, opts -func (_m *EVMQueryClient) EthAccount(ctx context.Context, in *evm.QueryEthAccountRequest, opts ...grpc.CallOption) (*evm.QueryEthAccountResponse, error) { - _va := make([]interface{}, len(opts)) - for _i := range opts { - _va[_i] = opts[_i] - } - var _ca []interface{} - _ca = append(_ca, ctx, in) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for EthAccount") - } - - var r0 *evm.QueryEthAccountResponse - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *evm.QueryEthAccountRequest, ...grpc.CallOption) (*evm.QueryEthAccountResponse, error)); ok { - return rf(ctx, in, opts...) - } - if rf, ok := ret.Get(0).(func(context.Context, *evm.QueryEthAccountRequest, ...grpc.CallOption) *evm.QueryEthAccountResponse); ok { - r0 = rf(ctx, in, opts...) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*evm.QueryEthAccountResponse) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *evm.QueryEthAccountRequest, ...grpc.CallOption) error); ok { - r1 = rf(ctx, in, opts...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// EthCall provides a mock function with given fields: ctx, in, opts -func (_m *EVMQueryClient) EthCall(ctx context.Context, in *evm.EthCallRequest, opts ...grpc.CallOption) (*evm.MsgEthereumTxResponse, error) { - _va := make([]interface{}, len(opts)) - for _i := range opts { - _va[_i] = opts[_i] - } - var _ca []interface{} - _ca = append(_ca, ctx, in) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for EthCall") - } - - var r0 *evm.MsgEthereumTxResponse - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *evm.EthCallRequest, ...grpc.CallOption) (*evm.MsgEthereumTxResponse, error)); ok { - return rf(ctx, in, opts...) - } - if rf, ok := ret.Get(0).(func(context.Context, *evm.EthCallRequest, ...grpc.CallOption) *evm.MsgEthereumTxResponse); ok { - r0 = rf(ctx, in, opts...) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*evm.MsgEthereumTxResponse) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *evm.EthCallRequest, ...grpc.CallOption) error); ok { - r1 = rf(ctx, in, opts...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// FunTokenMapping provides a mock function with given fields: ctx, in, opts -func (_m *EVMQueryClient) FunTokenMapping(ctx context.Context, in *evm.QueryFunTokenMappingRequest, opts ...grpc.CallOption) (*evm.QueryFunTokenMappingResponse, error) { - _va := make([]interface{}, len(opts)) - for _i := range opts { - _va[_i] = opts[_i] - } - var _ca []interface{} - _ca = append(_ca, ctx, in) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for FunTokenMapping") - } - - var r0 *evm.QueryFunTokenMappingResponse - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *evm.QueryFunTokenMappingRequest, ...grpc.CallOption) (*evm.QueryFunTokenMappingResponse, error)); ok { - return rf(ctx, in, opts...) - } - if rf, ok := ret.Get(0).(func(context.Context, *evm.QueryFunTokenMappingRequest, ...grpc.CallOption) *evm.QueryFunTokenMappingResponse); ok { - r0 = rf(ctx, in, opts...) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*evm.QueryFunTokenMappingResponse) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *evm.QueryFunTokenMappingRequest, ...grpc.CallOption) error); ok { - r1 = rf(ctx, in, opts...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Params provides a mock function with given fields: ctx, in, opts -func (_m *EVMQueryClient) Params(ctx context.Context, in *evm.QueryParamsRequest, opts ...grpc.CallOption) (*evm.QueryParamsResponse, error) { - _va := make([]interface{}, len(opts)) - for _i := range opts { - _va[_i] = opts[_i] - } - var _ca []interface{} - _ca = append(_ca, ctx, in) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for Params") - } - - var r0 *evm.QueryParamsResponse - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *evm.QueryParamsRequest, ...grpc.CallOption) (*evm.QueryParamsResponse, error)); ok { - return rf(ctx, in, opts...) - } - if rf, ok := ret.Get(0).(func(context.Context, *evm.QueryParamsRequest, ...grpc.CallOption) *evm.QueryParamsResponse); ok { - r0 = rf(ctx, in, opts...) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*evm.QueryParamsResponse) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *evm.QueryParamsRequest, ...grpc.CallOption) error); ok { - r1 = rf(ctx, in, opts...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Storage provides a mock function with given fields: ctx, in, opts -func (_m *EVMQueryClient) Storage(ctx context.Context, in *evm.QueryStorageRequest, opts ...grpc.CallOption) (*evm.QueryStorageResponse, error) { - _va := make([]interface{}, len(opts)) - for _i := range opts { - _va[_i] = opts[_i] - } - var _ca []interface{} - _ca = append(_ca, ctx, in) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for Storage") - } - - var r0 *evm.QueryStorageResponse - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *evm.QueryStorageRequest, ...grpc.CallOption) (*evm.QueryStorageResponse, error)); ok { - return rf(ctx, in, opts...) - } - if rf, ok := ret.Get(0).(func(context.Context, *evm.QueryStorageRequest, ...grpc.CallOption) *evm.QueryStorageResponse); ok { - r0 = rf(ctx, in, opts...) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*evm.QueryStorageResponse) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *evm.QueryStorageRequest, ...grpc.CallOption) error); ok { - r1 = rf(ctx, in, opts...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// TraceBlock provides a mock function with given fields: ctx, in, opts -func (_m *EVMQueryClient) TraceBlock(ctx context.Context, in *evm.QueryTraceBlockRequest, opts ...grpc.CallOption) (*evm.QueryTraceBlockResponse, error) { - _va := make([]interface{}, len(opts)) - for _i := range opts { - _va[_i] = opts[_i] - } - var _ca []interface{} - _ca = append(_ca, ctx, in) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for TraceBlock") - } - - var r0 *evm.QueryTraceBlockResponse - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *evm.QueryTraceBlockRequest, ...grpc.CallOption) (*evm.QueryTraceBlockResponse, error)); ok { - return rf(ctx, in, opts...) - } - if rf, ok := ret.Get(0).(func(context.Context, *evm.QueryTraceBlockRequest, ...grpc.CallOption) *evm.QueryTraceBlockResponse); ok { - r0 = rf(ctx, in, opts...) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*evm.QueryTraceBlockResponse) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *evm.QueryTraceBlockRequest, ...grpc.CallOption) error); ok { - r1 = rf(ctx, in, opts...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// TraceCall provides a mock function with given fields: ctx, in, opts -func (_m *EVMQueryClient) TraceCall(ctx context.Context, in *evm.QueryTraceTxRequest, opts ...grpc.CallOption) (*evm.QueryTraceTxResponse, error) { - _va := make([]interface{}, len(opts)) - for _i := range opts { - _va[_i] = opts[_i] - } - var _ca []interface{} - _ca = append(_ca, ctx, in) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for TraceCall") - } - - var r0 *evm.QueryTraceTxResponse - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *evm.QueryTraceTxRequest, ...grpc.CallOption) (*evm.QueryTraceTxResponse, error)); ok { - return rf(ctx, in, opts...) - } - if rf, ok := ret.Get(0).(func(context.Context, *evm.QueryTraceTxRequest, ...grpc.CallOption) *evm.QueryTraceTxResponse); ok { - r0 = rf(ctx, in, opts...) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*evm.QueryTraceTxResponse) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *evm.QueryTraceTxRequest, ...grpc.CallOption) error); ok { - r1 = rf(ctx, in, opts...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// TraceTx provides a mock function with given fields: ctx, in, opts -func (_m *EVMQueryClient) TraceTx(ctx context.Context, in *evm.QueryTraceTxRequest, opts ...grpc.CallOption) (*evm.QueryTraceTxResponse, error) { - _va := make([]interface{}, len(opts)) - for _i := range opts { - _va[_i] = opts[_i] - } - var _ca []interface{} - _ca = append(_ca, ctx, in) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for TraceTx") - } - - var r0 *evm.QueryTraceTxResponse - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *evm.QueryTraceTxRequest, ...grpc.CallOption) (*evm.QueryTraceTxResponse, error)); ok { - return rf(ctx, in, opts...) - } - if rf, ok := ret.Get(0).(func(context.Context, *evm.QueryTraceTxRequest, ...grpc.CallOption) *evm.QueryTraceTxResponse); ok { - r0 = rf(ctx, in, opts...) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*evm.QueryTraceTxResponse) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *evm.QueryTraceTxRequest, ...grpc.CallOption) error); ok { - r1 = rf(ctx, in, opts...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// ValidatorAccount provides a mock function with given fields: ctx, in, opts -func (_m *EVMQueryClient) ValidatorAccount(ctx context.Context, in *evm.QueryValidatorAccountRequest, opts ...grpc.CallOption) (*evm.QueryValidatorAccountResponse, error) { - _va := make([]interface{}, len(opts)) - for _i := range opts { - _va[_i] = opts[_i] - } - var _ca []interface{} - _ca = append(_ca, ctx, in) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for ValidatorAccount") - } - - var r0 *evm.QueryValidatorAccountResponse - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *evm.QueryValidatorAccountRequest, ...grpc.CallOption) (*evm.QueryValidatorAccountResponse, error)); ok { - return rf(ctx, in, opts...) - } - if rf, ok := ret.Get(0).(func(context.Context, *evm.QueryValidatorAccountRequest, ...grpc.CallOption) *evm.QueryValidatorAccountResponse); ok { - r0 = rf(ctx, in, opts...) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*evm.QueryValidatorAccountResponse) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *evm.QueryValidatorAccountRequest, ...grpc.CallOption) error); ok { - r1 = rf(ctx, in, opts...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// NewEVMQueryClient creates a new instance of EVMQueryClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewEVMQueryClient(t interface { - mock.TestingT - Cleanup(func()) -}) *EVMQueryClient { - mock := &EVMQueryClient{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/eth/rpc/backend/node_info_test.go b/eth/rpc/backend/node_info_test.go index 9949defb1..f0dcea536 100644 --- a/eth/rpc/backend/node_info_test.go +++ b/eth/rpc/backend/node_info_test.go @@ -1,143 +1 @@ -package backend - -import ( - "fmt" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - - "github.com/NibiruChain/nibiru/v2/eth" - "github.com/NibiruChain/nibiru/v2/eth/rpc/backend/mocks" -) - -func (s *BackendSuite) TestRPCMinGasPrice() { - testCases := []struct { - name string - registerMock func() - expMinGasPrice int64 - expPass bool - }{ - { - "pass - default gas price", - func() { - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterParamsWithoutHeaderError(queryClient, 1) - }, - eth.DefaultGasPrice, - true, - }, - { - "pass - min gas price is 0", - func() { - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterParamsWithoutHeader(queryClient, 1) - }, - eth.DefaultGasPrice, - true, - }, - } - - for _, tc := range testCases { - s.Run(fmt.Sprintf("case %s", tc.name), func() { - s.SetupTest() // reset test and queries - tc.registerMock() - - minPrice := s.backend.RPCMinGasPrice() - if tc.expPass { - s.Require().Equal(tc.expMinGasPrice, minPrice) - } else { - s.Require().NotEqual(tc.expMinGasPrice, minPrice) - } - }) - } -} - -func (s *BackendSuite) TestAccounts() { - testCases := []struct { - name string - registerMock func() - expAddr []common.Address - expPass bool - }{ - { - "pass - returns empty address", - func() {}, - []common.Address{}, - true, - }, - } - - for _, tc := range testCases { - s.Run(fmt.Sprintf("case %s", tc.name), func() { - s.SetupTest() // reset test and queries - tc.registerMock() - - output, err := s.backend.Accounts() - - if tc.expPass { - s.Require().NoError(err) - s.Require().Equal(tc.expAddr, output) - } else { - s.Require().Error(err) - } - }) - } -} - -func (s *BackendSuite) TestSyncing() { - testCases := []struct { - name string - registerMock func() - expResponse interface{} - expPass bool - }{ - { - "fail - Can't get status", - func() { - client := s.backend.clientCtx.Client.(*mocks.Client) - RegisterStatusError(client) - }, - false, - false, - }, - { - "pass - Node not catching up", - func() { - client := s.backend.clientCtx.Client.(*mocks.Client) - RegisterStatus(client) - }, - false, - true, - }, - { - "pass - Node is catching up", - func() { - client := s.backend.clientCtx.Client.(*mocks.Client) - RegisterStatus(client) - status, _ := client.Status(s.backend.ctx) - status.SyncInfo.CatchingUp = true - }, - map[string]interface{}{ - "startingBlock": hexutil.Uint64(0), - "currentBlock": hexutil.Uint64(0), - }, - true, - }, - } - - for _, tc := range testCases { - s.Run(fmt.Sprintf("case %s", tc.name), func() { - s.SetupTest() // reset test and queries - tc.registerMock() - - output, err := s.backend.Syncing() - - if tc.expPass { - s.Require().NoError(err) - s.Require().Equal(tc.expResponse, output) - } else { - s.Require().Error(err) - } - }) - } -} +package backend_test diff --git a/eth/rpc/backend/sign_tx_test.go b/eth/rpc/backend/sign_tx_test.go index 559289d3f..f0dcea536 100644 --- a/eth/rpc/backend/sign_tx_test.go +++ b/eth/rpc/backend/sign_tx_test.go @@ -1,273 +1 @@ -package backend - -import ( - "fmt" - - "cosmossdk.io/math" - - "github.com/cosmos/cosmos-sdk/crypto" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - gethcore "github.com/ethereum/go-ethereum/core/types" - goethcrypto "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/signer/core/apitypes" - "google.golang.org/grpc/metadata" - - "github.com/NibiruChain/nibiru/v2/eth/crypto/ethsecp256k1" - "github.com/NibiruChain/nibiru/v2/eth/rpc/backend/mocks" - "github.com/NibiruChain/nibiru/v2/x/evm" - evmtest "github.com/NibiruChain/nibiru/v2/x/evm/evmtest" -) - -func (s *BackendSuite) TestSendTransaction() { - gasPrice := new(hexutil.Big) - gas := hexutil.Uint64(1) - zeroGas := hexutil.Uint64(0) - toAddr := evmtest.NewEthPrivAcc().EthAddr - priv, _ := ethsecp256k1.GenerateKey() - from := common.BytesToAddress(priv.PubKey().Address().Bytes()) - nonce := hexutil.Uint64(1) - baseFee := math.NewInt(1) - callArgsDefault := evm.JsonTxArgs{ - From: &from, - To: &toAddr, - GasPrice: gasPrice, - Gas: &gas, - Nonce: &nonce, - } - - hash := common.Hash{} - - testCases := []struct { - name string - registerMock func() - args evm.JsonTxArgs - expHash common.Hash - expPass bool - }{ - { - "fail - Can't find account in Keyring", - func() {}, - evm.JsonTxArgs{}, - hash, - false, - }, - { - "fail - Block error can't set Tx defaults", - func() { - var header metadata.MD - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - client := s.backend.clientCtx.Client.(*mocks.Client) - armor := crypto.EncryptArmorPrivKey(priv, "", "eth_secp256k1") - err := s.backend.clientCtx.Keyring.ImportPrivKey("test_key", armor, "") - s.Require().NoError(err) - RegisterParams(queryClient, &header, 1) - RegisterBlockError(client, 1) - }, - callArgsDefault, - hash, - false, - }, - { - "fail - Cannot validate transaction gas set to 0", - func() { - var header metadata.MD - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - client := s.backend.clientCtx.Client.(*mocks.Client) - armor := crypto.EncryptArmorPrivKey(priv, "", "eth_secp256k1") - err := s.backend.clientCtx.Keyring.ImportPrivKey("test_key", armor, "") - s.Require().NoError(err) - RegisterParams(queryClient, &header, 1) - _, err = RegisterBlock(client, 1, nil) - s.Require().NoError(err) - _, err = RegisterBlockResults(client, 1) - s.Require().NoError(err) - RegisterBaseFee(queryClient, baseFee) - RegisterParamsWithoutHeader(queryClient, 1) - }, - evm.JsonTxArgs{ - From: &from, - To: &toAddr, - GasPrice: gasPrice, - Gas: &zeroGas, - Nonce: &nonce, - }, - hash, - false, - }, - { - "fail - Cannot broadcast transaction", - func() { - client, txBytes := broadcastTx(s, priv, baseFee, callArgsDefault) - RegisterBroadcastTxError(client, txBytes) - }, - callArgsDefault, - common.Hash{}, - false, - }, - { - "pass - Return the transaction hash", - func() { - client, txBytes := broadcastTx(s, priv, baseFee, callArgsDefault) - RegisterBroadcastTx(client, txBytes) - }, - callArgsDefault, - hash, - true, - }, - } - - for _, tc := range testCases { - s.Run(fmt.Sprintf("case %s", tc.name), func() { - s.SetupTest() // reset test and queries - tc.registerMock() - - if tc.expPass { - // Sign the transaction and get the hash - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterParamsWithoutHeader(queryClient, 1) - ethSigner := gethcore.LatestSigner(s.backend.ChainConfig()) - msg := callArgsDefault.ToMsgEthTx() - err := msg.Sign(ethSigner, s.backend.clientCtx.Keyring) - s.Require().NoError(err) - tc.expHash = msg.AsTransaction().Hash() - } - responseHash, err := s.backend.SendTransaction(tc.args) - if tc.expPass { - s.Require().NoError(err) - s.Require().Equal(tc.expHash, responseHash) - } else { - s.Require().Error(err) - } - }) - } -} - -func (s *BackendSuite) TestSign() { - ethAcc := evmtest.NewEthPrivAcc() - from, priv := ethAcc.EthAddr, ethAcc.PrivKey - - testCases := []struct { - name string - registerMock func() - fromAddr common.Address - inputBz hexutil.Bytes - expPass bool - }{ - { - "fail - can't find key in Keyring", - func() {}, - from, - nil, - false, - }, - { - "pass - sign nil data", - func() { - armor := crypto.EncryptArmorPrivKey(priv, "", "eth_secp256k1") - err := s.backend.clientCtx.Keyring.ImportPrivKey("test_key", armor, "") - s.Require().NoError(err) - }, - from, - nil, - true, - }, - } - - for _, tc := range testCases { - s.Run(fmt.Sprintf("case %s", tc.name), func() { - s.SetupTest() // reset test and queries - tc.registerMock() - - responseBz, err := s.backend.Sign(tc.fromAddr, tc.inputBz) - if tc.expPass { - signature, _, err := s.backend.clientCtx.Keyring.SignByAddress((sdk.AccAddress)(from.Bytes()), tc.inputBz) - signature[goethcrypto.RecoveryIDOffset] += 27 - s.Require().NoError(err) - s.Require().Equal((hexutil.Bytes)(signature), responseBz) - } else { - s.Require().Error(err) - } - }) - } -} - -func (s *BackendSuite) TestSignTypedData() { - ethAcc := evmtest.NewEthPrivAcc() - from, priv := ethAcc.EthAddr, ethAcc.PrivKey - testCases := []struct { - name string - registerMock func() - fromAddr common.Address - inputTypedData apitypes.TypedData - expPass bool - }{ - { - "fail - can't find key in Keyring", - func() {}, - from, - apitypes.TypedData{}, - false, - }, - { - "fail - empty TypeData", - func() { - armor := crypto.EncryptArmorPrivKey(priv, "", "eth_secp256k1") - err := s.backend.clientCtx.Keyring.ImportPrivKey("test_key", armor, "") - s.Require().NoError(err) - }, - from, - apitypes.TypedData{}, - false, - }, - // TODO: Generate a TypedData msg - } - - for _, tc := range testCases { - s.Run(fmt.Sprintf("case %s", tc.name), func() { - s.SetupTest() // reset test and queries - tc.registerMock() - - responseBz, err := s.backend.SignTypedData(tc.fromAddr, tc.inputTypedData) - - if tc.expPass { - sigHash, _, _ := apitypes.TypedDataAndHash(tc.inputTypedData) - signature, _, err := s.backend.clientCtx.Keyring.SignByAddress((sdk.AccAddress)(from.Bytes()), sigHash) - signature[goethcrypto.RecoveryIDOffset] += 27 - s.Require().NoError(err) - s.Require().Equal((hexutil.Bytes)(signature), responseBz) - } else { - s.Require().Error(err) - } - }) - } -} - -func broadcastTx( - s *BackendSuite, - priv *ethsecp256k1.PrivKey, - baseFee math.Int, - callArgsDefault evm.JsonTxArgs, -) (client *mocks.Client, txBytes []byte) { - var header metadata.MD - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - client = s.backend.clientCtx.Client.(*mocks.Client) - armor := crypto.EncryptArmorPrivKey(priv, "", "eth_secp256k1") - _ = s.backend.clientCtx.Keyring.ImportPrivKey("test_key", armor, "") - RegisterParams(queryClient, &header, 1) - _, err := RegisterBlock(client, 1, nil) - s.Require().NoError(err) - _, err = RegisterBlockResults(client, 1) - s.Require().NoError(err) - RegisterBaseFee(queryClient, baseFee) - RegisterParamsWithoutHeader(queryClient, 1) - ethSigner := gethcore.LatestSigner(s.backend.ChainConfig()) - msg := callArgsDefault.ToMsgEthTx() - err = msg.Sign(ethSigner, s.backend.clientCtx.Keyring) - s.Require().NoError(err) - tx, _ := msg.BuildTx(s.backend.clientCtx.TxConfig.NewTxBuilder(), evm.DefaultEVMDenom) - txEncoder := s.backend.clientCtx.TxConfig.TxEncoder() - txBytes, _ = txEncoder(tx) - return client, txBytes -} +package backend_test diff --git a/eth/rpc/backend/tracing.go b/eth/rpc/backend/tracing.go index 1362aab4f..425e3d00e 100644 --- a/eth/rpc/backend/tracing.go +++ b/eth/rpc/backend/tracing.go @@ -39,7 +39,7 @@ func (b *Backend) TraceTransaction(hash common.Hash, config *evm.TraceConfig) (i // check tx index is not out of bound if len(blk.Block.Txs) > math.MaxUint32 { - return nil, fmt.Errorf("tx count %d is overfloing", len(blk.Block.Txs)) + return nil, fmt.Errorf("tx count %d is overflowing", len(blk.Block.Txs)) } txsLen := uint32(len(blk.Block.Txs)) // #nosec G701 -- checked for int overflow already if txsLen < transaction.TxIndex { diff --git a/eth/rpc/backend/tracing_test.go b/eth/rpc/backend/tracing_test.go index 914524f55..e8c9a090c 100644 --- a/eth/rpc/backend/tracing_test.go +++ b/eth/rpc/backend/tracing_test.go @@ -1,321 +1,134 @@ -package backend +package backend_test import ( - "fmt" "math/big" + "strings" - dbm "github.com/cometbft/cometbft-db" - abci "github.com/cometbft/cometbft/abci/types" - tmlog "github.com/cometbft/cometbft/libs/log" tmrpctypes "github.com/cometbft/cometbft/rpc/core/types" - "github.com/cometbft/cometbft/types" - "github.com/cosmos/cosmos-sdk/crypto" - "github.com/ethereum/go-ethereum/common" + gethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - gethcore "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" - "github.com/NibiruChain/nibiru/v2/eth/crypto/ethsecp256k1" - "github.com/NibiruChain/nibiru/v2/eth/indexer" "github.com/NibiruChain/nibiru/v2/eth/rpc" - "github.com/NibiruChain/nibiru/v2/eth/rpc/backend/mocks" "github.com/NibiruChain/nibiru/v2/x/evm" ) -func (s *BackendSuite) TestTraceTransaction() { - msgEthereumTx, _ := s.buildEthereumTx() - msgEthereumTx2, _ := s.buildEthereumTx() - - txHash := msgEthereumTx.AsTransaction().Hash() - txHash2 := msgEthereumTx2.AsTransaction().Hash() - - priv, _ := ethsecp256k1.GenerateKey() - from := common.BytesToAddress(priv.PubKey().Address().Bytes()) - - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - RegisterParamsWithoutHeader(queryClient, 1) - - armor := crypto.EncryptArmorPrivKey(priv, "", "eth_secp256k1") - _ = s.backend.clientCtx.Keyring.ImportPrivKey("test_key", armor, "") - - ethSigner := gethcore.LatestSigner(s.backend.ChainConfig()) - - txEncoder := s.backend.clientCtx.TxConfig.TxEncoder() - - msgEthereumTx.From = from.String() - _ = msgEthereumTx.Sign(ethSigner, s.signer) - - tx, _ := msgEthereumTx.BuildTx(s.backend.clientCtx.TxConfig.NewTxBuilder(), evm.DefaultEVMDenom) - txBz, _ := txEncoder(tx) - - msgEthereumTx2.From = from.String() - _ = msgEthereumTx2.Sign(ethSigner, s.signer) - - tx2, _ := msgEthereumTx.BuildTx(s.backend.clientCtx.TxConfig.NewTxBuilder(), evm.DefaultEVMDenom) - txBz2, _ := txEncoder(tx2) +var traceConfig = &evm.TraceConfig{ + Tracer: "callTracer", + TracerConfig: &evm.TracerConfig{ + OnlyTopCall: true, + }, +} +func (s *BackendSuite) TestTraceTransaction() { testCases := []struct { - name string - registerMock func() - block *types.Block - responseBlock []*abci.ResponseDeliverTx - expResult interface{} - expPass bool + name string + txHash gethcommon.Hash + wantErr string }{ { - "fail - tx not found", - func() {}, - &types.Block{Header: types.Header{Height: 1}, Data: types.Data{Txs: []types.Tx{}}}, - []*abci.ResponseDeliverTx{ - { - Code: 0, - Events: []abci.Event{ - {Type: evm.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ - {Key: "ethereumTxHash", Value: txHash.Hex()}, - {Key: "txIndex", Value: "0"}, - {Key: "amount", Value: "1000"}, - {Key: "txGasUsed", Value: "21000"}, - {Key: "txHash", Value: ""}, - {Key: "recipient", Value: "0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7"}, - }}, - }, - }, - }, - nil, - false, - }, - { - "fail - block not found", - func() { - // var header metadata.MD - client := s.backend.clientCtx.Client.(*mocks.Client) - RegisterBlockError(client, 1) - }, - &types.Block{Header: types.Header{Height: 1}, Data: types.Data{Txs: []types.Tx{txBz}}}, - []*abci.ResponseDeliverTx{ - { - Code: 0, - Events: []abci.Event{ - {Type: evm.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ - {Key: "ethereumTxHash", Value: txHash.Hex()}, - {Key: "txIndex", Value: "0"}, - {Key: "amount", Value: "1000"}, - {Key: "txGasUsed", Value: "21000"}, - {Key: "txHash", Value: ""}, - {Key: "recipient", Value: "0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7"}, - }}, - }, - }, - }, - map[string]interface{}{"test": "hello"}, - false, - }, - { - "pass - transaction found in a block with multiple transactions", - func() { - var ( - queryClient = s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - client = s.backend.clientCtx.Client.(*mocks.Client) - height int64 = 1 - ) - _, err := RegisterBlockMultipleTxs(client, height, []types.Tx{txBz, txBz2}) - s.Require().NoError(err) - RegisterTraceTransactionWithPredecessors(queryClient, msgEthereumTx, []*evm.MsgEthereumTx{msgEthereumTx}) - RegisterConsensusParams(client, height) - }, - &types.Block{Header: types.Header{Height: 1, ChainID: ChainID}, Data: types.Data{Txs: []types.Tx{txBz, txBz2}}}, - []*abci.ResponseDeliverTx{ - { - Code: 0, - Events: []abci.Event{ - {Type: evm.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ - {Key: "ethereumTxHash", Value: txHash.Hex()}, - {Key: "txIndex", Value: "0"}, - {Key: "amount", Value: "1000"}, - {Key: "txGasUsed", Value: "21000"}, - {Key: "txHash", Value: ""}, - {Key: "recipient", Value: "0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7"}, - }}, - }, - }, - { - Code: 0, - Events: []abci.Event{ - {Type: evm.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ - {Key: "ethereumTxHash", Value: txHash2.Hex()}, - {Key: "txIndex", Value: "1"}, - {Key: "amount", Value: "1000"}, - {Key: "txGasUsed", Value: "21000"}, - {Key: "txHash", Value: ""}, - {Key: "recipient", Value: "0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7"}, - }}, - }, - }, - }, - map[string]interface{}{"test": "hello"}, - true, + name: "sad: tx not found", + txHash: gethcommon.BytesToHash([]byte("0x0")), + wantErr: "not found", }, { - "pass - transaction found", - func() { - var ( - queryClient = s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - client = s.backend.clientCtx.Client.(*mocks.Client) - height int64 = 1 - ) - _, err := RegisterBlock(client, height, txBz) - s.Require().NoError(err) - RegisterTraceTransaction(queryClient, msgEthereumTx) - RegisterConsensusParams(client, height) - }, - &types.Block{Header: types.Header{Height: 1}, Data: types.Data{Txs: []types.Tx{txBz}}}, - []*abci.ResponseDeliverTx{ - { - Code: 0, - Events: []abci.Event{ - {Type: evm.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ - {Key: "ethereumTxHash", Value: txHash.Hex()}, - {Key: "txIndex", Value: "0"}, - {Key: "amount", Value: "1000"}, - {Key: "txGasUsed", Value: "21000"}, - {Key: "txHash", Value: ""}, - {Key: "recipient", Value: "0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7"}, - }}, - }, - }, - }, - map[string]interface{}{"test": "hello"}, - true, + name: "happy: tx found", + txHash: transferTxHash, + wantErr: "", }, } for _, tc := range testCases { - s.Run(fmt.Sprintf("case %s", tc.name), func() { - s.SetupTest() // reset test and queries - tc.registerMock() - - db := dbm.NewMemDB() - s.backend.indexer = indexer.NewKVIndexer(db, tmlog.NewNopLogger(), s.backend.clientCtx) - - err := s.backend.indexer.IndexBlock(tc.block, tc.responseBlock) - s.Require().NoError(err) - txResult, err := s.backend.TraceTransaction(txHash, nil) - - if tc.expPass { - s.Require().NoError(err) - s.Require().Equal(tc.expResult, txResult) - } else { - s.Require().Error(err) + s.Run(tc.name, func() { + res, err := s.backend.TraceTransaction( + tc.txHash, + traceConfig, + ) + if tc.wantErr != "" { + s.ErrorContains(err, tc.wantErr) + return } + s.Require().NoError(err) + s.Require().NotNil(res) + AssertTraceCall(s, res.(map[string]interface{})) }) } } func (s *BackendSuite) TestTraceBlock() { - msgEthTx, bz := s.buildEthereumTx() - emptyBlock := types.MakeBlock(1, []types.Tx{}, nil, nil) - emptyBlock.ChainID = ChainID - filledBlock := types.MakeBlock(1, []types.Tx{bz}, nil, nil) - filledBlock.ChainID = ChainID - resBlockEmpty := tmrpctypes.ResultBlock{Block: emptyBlock, BlockID: emptyBlock.LastBlockID} - resBlockFilled := tmrpctypes.ResultBlock{Block: filledBlock, BlockID: filledBlock.LastBlockID} + tmBlockWithTx, err := s.backend.TendermintBlockByNumber(transferTxBlockNumber) + s.Require().NoError(err) + + blockNumberWithoutTx := rpc.NewBlockNumber(big.NewInt(1)) + tmBlockWithoutTx, err := s.backend.TendermintBlockByNumber(1) + s.Require().NoError(err) testCases := []struct { - name string - registerMock func() - expTraceResults []*evm.TxTraceResult - resBlock *tmrpctypes.ResultBlock - config *evm.TraceConfig - expPass bool + name string + blockNumber rpc.BlockNumber + tmBlock *tmrpctypes.ResultBlock + txCount int }{ { - "pass - no transaction returning empty array", - func() {}, - []*evm.TxTraceResult{}, - &resBlockEmpty, - &evm.TraceConfig{}, - true, + name: "happy: block without txs", + blockNumber: blockNumberWithoutTx, + tmBlock: tmBlockWithoutTx, + txCount: 0, }, { - "fail - cannot unmarshal data", - func() { - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - client := s.backend.clientCtx.Client.(*mocks.Client) - RegisterTraceBlock(queryClient, []*evm.MsgEthereumTx{msgEthTx}) - RegisterConsensusParams(client, 1) - }, - []*evm.TxTraceResult{}, - &resBlockFilled, - &evm.TraceConfig{}, - false, + name: "happy: block with txs", + blockNumber: transferTxBlockNumber, + tmBlock: tmBlockWithTx, + txCount: 1, }, } for _, tc := range testCases { - s.Run(fmt.Sprintf("case %s", tc.name), func() { - s.SetupTest() // reset test and queries - tc.registerMock() - - traceResults, err := s.backend.TraceBlock(1, tc.config, tc.resBlock) - - if tc.expPass { - s.Require().NoError(err) - s.Require().Equal(tc.expTraceResults, traceResults) - } else { - s.Require().Error(err) + s.Run(tc.name, func() { + res, err := s.backend.TraceBlock( + tc.blockNumber, + traceConfig, + tc.tmBlock, + ) + s.Require().NoError(err) + s.Require().Equal(tc.txCount, len(res)) + if tc.txCount > 0 { + AssertTraceCall(s, res[0].Result.(map[string]interface{})) } }) } } func (s *BackendSuite) TestTraceCall() { - priv, _ := ethsecp256k1.GenerateKey() - from := common.BytesToAddress(priv.PubKey().Address().Bytes()) + block, err := s.backend.BlockNumber() + s.Require().NoError(err) + nonce, err := s.backend.GetTransactionCount(s.fundedAccEthAddr, rpc.BlockNumber(block)) + s.NoError(err) + gas := hexutil.Uint64(evm.NativeToWei(big.NewInt(int64(params.TxGas))).Uint64()) + amountToSendHex := hexutil.Big(*amountToSend) txArgs := evm.JsonTxArgs{ - From: &from, - Value: (*hexutil.Big)(big.NewInt(1e12)), - } - - testCases := []struct { - name string - registerMock func() - expResult interface{} - expPass bool - }{ - { - "pass - valid call", - func() { - var ( - queryClient = s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - client = s.backend.clientCtx.Client.(*mocks.Client) - height int64 = 1 - ) - - msgEthereumTx := txArgs.ToMsgEthTx() - - _, err := RegisterBlock(client, height, nil) - s.Require().NoError(err) - RegisterTraceCall(queryClient, msgEthereumTx) - RegisterConsensusParams(client, height) - }, - map[string]interface{}{"test": "hello"}, - true, - }, + Nonce: nonce, + From: &s.fundedAccEthAddr, + To: &recipient, + Value: &amountToSendHex, + Gas: &gas, } + s.Require().NoError(err) + + res, err := s.backend.TraceCall( + txArgs, + rpc.BlockNumber(block), + traceConfig, + ) + s.Require().NoError(err) + s.Require().NotNil(res) + AssertTraceCall(s, res.(map[string]interface{})) +} - for _, tc := range testCases { - s.Run(fmt.Sprintf("case %s", tc.name), func() { - s.SetupTest() // reset test and queries - tc.registerMock() - - txResult, err := s.backend.TraceCall(txArgs, rpc.NewBlockNumber(big.NewInt(1)), &evm.TraceConfig{}) - - if tc.expPass { - s.Require().NoError(err) - s.Require().Equal(tc.expResult, txResult) - } else { - s.Require().Error(err) - } - }) - } +func AssertTraceCall(s *BackendSuite, trace map[string]interface{}) { + s.Require().Equal("CALL", trace["type"]) + s.Require().Equal(strings.ToLower(s.fundedAccEthAddr.Hex()), trace["from"]) + s.Require().Equal(strings.ToLower(recipient.Hex()), trace["to"]) + s.Require().Equal("0x"+gethcommon.Bytes2Hex(amountToSend.Bytes()), trace["value"]) } diff --git a/eth/rpc/backend/tx_info.go b/eth/rpc/backend/tx_info.go index 2f5003d0b..9783ca747 100644 --- a/eth/rpc/backend/tx_info.go +++ b/eth/rpc/backend/tx_info.go @@ -11,7 +11,7 @@ import ( tmrpcclient "github.com/cometbft/cometbft/rpc/client" tmrpctypes "github.com/cometbft/cometbft/rpc/core/types" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/ethereum/go-ethereum/common" + gethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" gethcore "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" @@ -25,7 +25,7 @@ import ( // GetTransactionByHash returns the Ethereum format transaction identified by // Ethereum transaction hash. If the transaction is not found or has been // discarded from a pruning node, this resolves to nil. -func (b *Backend) GetTransactionByHash(txHash common.Hash) (*rpc.EthTxJsonRPC, error) { +func (b *Backend) GetTransactionByHash(txHash gethcommon.Hash) (*rpc.EthTxJsonRPC, error) { res, err := b.GetTxByEthHash(txHash) if err != nil { return b.getTransactionByHashPending(txHash) @@ -81,7 +81,7 @@ func (b *Backend) GetTransactionByHash(txHash common.Hash) (*rpc.EthTxJsonRPC, e index := uint64(res.EthTxIndex) //#nosec G701 -- checked for int overflow already return rpc.NewRPCTxFromMsg( msg, - common.BytesToHash(block.BlockID.Hash.Bytes()), + gethcommon.BytesToHash(block.BlockID.Hash.Bytes()), height, index, baseFee, @@ -90,7 +90,7 @@ func (b *Backend) GetTransactionByHash(txHash common.Hash) (*rpc.EthTxJsonRPC, e } // getTransactionByHashPending find pending tx from mempool -func (b *Backend) getTransactionByHashPending(txHash common.Hash) (*rpc.EthTxJsonRPC, error) { +func (b *Backend) getTransactionByHashPending(txHash gethcommon.Hash) (*rpc.EthTxJsonRPC, error) { hexTx := txHash.Hex() // try to find tx in mempool txs, err := b.PendingTransactions() @@ -110,7 +110,7 @@ func (b *Backend) getTransactionByHashPending(txHash common.Hash) (*rpc.EthTxJso // use zero block values since it's not included in a block yet rpctx, err := rpc.NewRPCTxFromMsg( msg, - common.Hash{}, + gethcommon.Hash{}, uint64(0), uint64(0), nil, @@ -137,7 +137,7 @@ func (b *Backend) GetGasUsed(res *eth.TxResult, price *big.Int, gas uint64) uint } // GetTransactionReceipt returns the transaction receipt identified by hash. -func (b *Backend) GetTransactionReceipt(hash common.Hash) (map[string]interface{}, error) { +func (b *Backend) GetTransactionReceipt(hash gethcommon.Hash) (map[string]interface{}, error) { hexTx := hash.Hex() b.logger.Debug("eth_getTransactionReceipt", "hash", hexTx) @@ -230,7 +230,7 @@ func (b *Backend) GetTransactionReceipt(hash common.Hash) (map[string]interface{ // Inclusion information: These fields provide information about the inclusion of the // transaction corresponding to this receipt. - "blockHash": common.BytesToHash(resBlock.Block.Header.Hash()).Hex(), + "blockHash": gethcommon.BytesToHash(resBlock.Block.Header.Hash()).Hex(), "blockNumber": hexutil.Uint64(res.Height), "transactionIndex": hexutil.Uint64(res.EthTxIndex), @@ -263,7 +263,7 @@ func (b *Backend) GetTransactionReceipt(hash common.Hash) (map[string]interface{ } // GetTransactionByBlockHashAndIndex returns the transaction identified by hash and index. -func (b *Backend) GetTransactionByBlockHashAndIndex(hash common.Hash, idx hexutil.Uint) (*rpc.EthTxJsonRPC, error) { +func (b *Backend) GetTransactionByBlockHashAndIndex(hash gethcommon.Hash, idx hexutil.Uint) (*rpc.EthTxJsonRPC, error) { b.logger.Debug("eth_getTransactionByBlockHashAndIndex", "hash", hash.Hex(), "index", idx) sc, ok := b.clientCtx.Client.(tmrpcclient.SignClient) if !ok { @@ -303,9 +303,10 @@ func (b *Backend) GetTransactionByBlockNumberAndIndex(blockNum rpc.BlockNumber, } // GetTxByEthHash uses `/tx_query` to find transaction by ethereum tx hash -// TODO: Don't need to convert once hashing is fixed on Tendermint -// https://github.com/cometbft/cometbft/issues/6539 -func (b *Backend) GetTxByEthHash(hash common.Hash) (*eth.TxResult, error) { +func (b *Backend) GetTxByEthHash(hash gethcommon.Hash) (*eth.TxResult, error) { + // NOTE: The Tendermint hash is not the same as the gethcommon.Hash. + // https://github.com/cometbft/cometbft/issues/342#issuecomment-1428836340 + // https://github.com/tendermint/tendermint/issues/6539 if b.indexer != nil { return b.indexer.GetByTxHash(hash) } @@ -414,7 +415,7 @@ func (b *Backend) GetTransactionByBlockAndIndex(block *tmrpctypes.ResultBlock, i index := uint64(idx) // #nosec G701 -- checked for int overflow already return rpc.NewRPCTxFromMsg( msg, - common.BytesToHash(block.Block.Hash()), + gethcommon.BytesToHash(block.Block.Hash()), height, index, baseFee, diff --git a/eth/rpc/backend/tx_info_test.go b/eth/rpc/backend/tx_info_test.go index 28f345eba..d203b2bc4 100644 --- a/eth/rpc/backend/tx_info_test.go +++ b/eth/rpc/backend/tx_info_test.go @@ -1,671 +1,179 @@ -package backend +package backend_test import ( - "fmt" "math/big" - "cosmossdk.io/math" - dbm "github.com/cometbft/cometbft-db" - abci "github.com/cometbft/cometbft/abci/types" - tmlog "github.com/cometbft/cometbft/libs/log" - tmrpctypes "github.com/cometbft/cometbft/rpc/core/types" - "github.com/cometbft/cometbft/types" - "github.com/ethereum/go-ethereum/common" + gethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - "google.golang.org/grpc/metadata" - "github.com/NibiruChain/nibiru/v2/eth" - "github.com/NibiruChain/nibiru/v2/eth/indexer" "github.com/NibiruChain/nibiru/v2/eth/rpc" - "github.com/NibiruChain/nibiru/v2/eth/rpc/backend/mocks" - "github.com/NibiruChain/nibiru/v2/x/evm" ) func (s *BackendSuite) TestGetTransactionByHash() { - msgEthereumTx, _ := s.buildEthereumTx() - txHash := msgEthereumTx.AsTransaction().Hash() - - txBz := s.signAndEncodeEthTx(msgEthereumTx) - block := &types.Block{Header: types.Header{Height: 1, ChainID: "test"}, Data: types.Data{Txs: []types.Tx{txBz}}} - responseDeliver := []*abci.ResponseDeliverTx{ - { - Code: 0, - Events: []abci.Event{ - {Type: evm.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ - {Key: "ethereumTxHash", Value: txHash.Hex()}, - {Key: "txIndex", Value: "0"}, - {Key: "amount", Value: "1000"}, - {Key: "txGasUsed", Value: "21000"}, - {Key: "txHash", Value: ""}, - {Key: "recipient", Value: ""}, - }}, - }, - }, - } - - rpcTransaction, _ := rpc.NewRPCTxFromEthTx(msgEthereumTx.AsTransaction(), common.Hash{}, 0, 0, big.NewInt(1), s.backend.chainID) - testCases := []struct { - name string - registerMock func() - tx *evm.MsgEthereumTx - expRPCTx *rpc.EthTxJsonRPC - expPass bool + name string + txHash gethcommon.Hash + wantTxFound bool }{ { - "fail - Block error", - func() { - client := s.backend.clientCtx.Client.(*mocks.Client) - RegisterBlockError(client, 1) - }, - msgEthereumTx, - rpcTransaction, - false, - }, - { - "fail - Block Result error", - func() { - client := s.backend.clientCtx.Client.(*mocks.Client) - _, err := RegisterBlock(client, 1, txBz) - s.Require().NoError(err) - RegisterBlockResultsError(client, 1) - }, - msgEthereumTx, - nil, - true, + name: "happy: tx found", + txHash: transferTxHash, + wantTxFound: true, }, { - "pass - Base fee error", - func() { - client := s.backend.clientCtx.Client.(*mocks.Client) - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - _, err := RegisterBlock(client, 1, txBz) - s.Require().NoError(err) - _, err = RegisterBlockResults(client, 1) - s.Require().NoError(err) - RegisterBaseFeeError(queryClient) - }, - msgEthereumTx, - rpcTransaction, - true, - }, - { - "pass - Transaction found and returned", - func() { - client := s.backend.clientCtx.Client.(*mocks.Client) - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - _, err := RegisterBlock(client, 1, txBz) - s.Require().NoError(err) - _, err = RegisterBlockResults(client, 1) - s.Require().NoError(err) - RegisterBaseFee(queryClient, math.NewInt(1)) - }, - msgEthereumTx, - rpcTransaction, - true, + name: "sad: tx not found", + txHash: gethcommon.BytesToHash([]byte("0x0")), + wantTxFound: false, }, } for _, tc := range testCases { s.Run(tc.name, func() { - s.SetupTest() // reset - tc.registerMock() - - db := dbm.NewMemDB() - s.backend.indexer = indexer.NewKVIndexer(db, tmlog.NewNopLogger(), s.backend.clientCtx) - err := s.backend.indexer.IndexBlock(block, responseDeliver) - s.Require().NoError(err) - - rpcTx, err := s.backend.GetTransactionByHash(common.HexToHash(tc.tx.Hash)) - - if tc.expPass { - s.Require().NoError(err) - s.Require().Equal(rpcTx, tc.expRPCTx) - } else { - s.Require().Error(err) + txResponse, err := s.backend.GetTransactionByHash(tc.txHash) + if !tc.wantTxFound { + s.Require().Nil(txResponse) + return } + s.Require().NoError(err) + s.Require().NotNil(txResponse) + s.Require().Equal(tc.txHash, txResponse.Hash) + s.Require().Equal(s.fundedAccEthAddr, txResponse.From) + s.Require().Equal(&recipient, txResponse.To) + s.Require().Equal(amountToSend, txResponse.Value.ToInt()) }) } } -func (s *BackendSuite) TestGetTransactionsByHashPending() { - msgEthereumTx, bz := s.buildEthereumTx() - rpcTransaction, _ := rpc.NewRPCTxFromEthTx(msgEthereumTx.AsTransaction(), common.Hash{}, 0, 0, big.NewInt(1), s.backend.chainID) - +func (s *BackendSuite) TestGetTransactionReceipt() { testCases := []struct { - name string - registerMock func() - tx *evm.MsgEthereumTx - expRPCTx *rpc.EthTxJsonRPC - expPass bool + name string + txHash gethcommon.Hash + wantTxFound bool }{ { - "fail - Pending transactions returns error", - func() { - client := s.backend.clientCtx.Client.(*mocks.Client) - RegisterUnconfirmedTxsError(client, nil) - }, - msgEthereumTx, - nil, - true, - }, - { - "fail - Tx not found return nil", - func() { - client := s.backend.clientCtx.Client.(*mocks.Client) - RegisterUnconfirmedTxs(client, nil, nil) - }, - msgEthereumTx, - nil, - true, + name: "happy: tx found", + txHash: transferTxHash, + wantTxFound: true, }, { - "pass - Tx found and returned", - func() { - client := s.backend.clientCtx.Client.(*mocks.Client) - RegisterUnconfirmedTxs(client, nil, types.Txs{bz}) - }, - msgEthereumTx, - rpcTransaction, - true, + name: "sad: tx not found", + txHash: gethcommon.BytesToHash([]byte("0x0")), + wantTxFound: false, }, } for _, tc := range testCases { s.Run(tc.name, func() { - s.SetupTest() // reset - tc.registerMock() - - rpcTx, err := s.backend.getTransactionByHashPending(common.HexToHash(tc.tx.Hash)) - - if tc.expPass { - s.Require().NoError(err) - s.Require().Equal(rpcTx, tc.expRPCTx) - } else { - s.Require().Error(err) - } - }) - } -} - -func (s *BackendSuite) TestGetTxByEthHash() { - msgEthereumTx, bz := s.buildEthereumTx() - rpcTransaction, _ := rpc.NewRPCTxFromEthTx(msgEthereumTx.AsTransaction(), common.Hash{}, 0, 0, big.NewInt(1), s.backend.chainID) - - testCases := []struct { - name string - registerMock func() - tx *evm.MsgEthereumTx - expRPCTx *rpc.EthTxJsonRPC - expPass bool - }{ - { - "fail - Indexer disabled can't find transaction", - func() { - s.backend.indexer = nil - client := s.backend.clientCtx.Client.(*mocks.Client) - query := fmt.Sprintf("%s.%s='%s'", evm.TypeMsgEthereumTx, evm.AttributeKeyEthereumTxHash, common.HexToHash(msgEthereumTx.Hash).Hex()) - RegisterTxSearch(client, query, bz) - }, - msgEthereumTx, - rpcTransaction, - false, - }, - } - - for _, tc := range testCases { - s.Run(tc.name, func() { - s.SetupTest() // reset - tc.registerMock() - - rpcTx, err := s.backend.GetTxByEthHash(common.HexToHash(tc.tx.Hash)) - - if tc.expPass { - s.Require().NoError(err) - s.Require().Equal(rpcTx, tc.expRPCTx) - } else { - s.Require().Error(err) + receipt, err := s.backend.GetTransactionReceipt(tc.txHash) + if !tc.wantTxFound { + s.Require().Nil(receipt) + return } + s.Require().NoError(err) + s.Require().NotNil(receipt) + + // Check fields + // s.Equal(s.fundedAccEthAddr, receipt.From) + // s.Equal(&recipient, receipt.To) + // s.Greater(receipt.GasUsed, uint64(0)) + // s.Equal(receipt.GasUsed, receipt.CumulativeGasUsed) + // s.Equal(tc.txHash, receipt.TxHash) + // s.Nil(receipt.ContractAddress) + // s.Require().Equal(gethcore.ReceiptStatusSuccessful, receipt.Status) }) } } func (s *BackendSuite) TestGetTransactionByBlockHashAndIndex() { - _, bz := s.buildEthereumTx() + blockWithTx, err := s.backend.GetBlockByNumber(transferTxBlockNumber, false) + s.Require().NoError(err) + blockHash := gethcommon.BytesToHash(blockWithTx["hash"].(hexutil.Bytes)) testCases := []struct { - name string - registerMock func() - blockHash common.Hash - expRPCTx *rpc.EthTxJsonRPC - expPass bool + name string + blockHash gethcommon.Hash + txIndex uint + wantTxFound bool }{ { - "pass - block not found", - func() { - client := s.backend.clientCtx.Client.(*mocks.Client) - RegisterBlockByHashError(client, common.Hash{}, bz) - }, - common.Hash{}, - nil, - true, + name: "happy: tx found", + blockHash: blockHash, + txIndex: 1, + wantTxFound: true, }, { - "pass - Block results error", - func() { - client := s.backend.clientCtx.Client.(*mocks.Client) - _, err := RegisterBlockByHash(client, common.Hash{}, bz) - s.Require().NoError(err) - RegisterBlockResultsError(client, 1) - }, - common.Hash{}, - nil, - true, + name: "sad: block not found", + blockHash: gethcommon.BytesToHash([]byte("0x0")), + txIndex: 1, + wantTxFound: false, }, - } - - for _, tc := range testCases { - s.Run(tc.name, func() { - s.SetupTest() // reset - tc.registerMock() - - rpcTx, err := s.backend.GetTransactionByBlockHashAndIndex(tc.blockHash, 1) - - if tc.expPass { - s.Require().NoError(err) - s.Require().Equal(rpcTx, tc.expRPCTx) - } else { - s.Require().Error(err) - } - }) - } -} - -func (s *BackendSuite) TestGetTransactionByBlockAndIndex() { - msgEthTx, bz := s.buildEthereumTx() - - defaultBlock := types.MakeBlock(1, []types.Tx{bz}, nil, nil) - defaultResponseDeliverTx := []*abci.ResponseDeliverTx{ { - Code: 0, - Events: []abci.Event{ - {Type: evm.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ - {Key: "ethereumTxHash", Value: common.HexToHash(msgEthTx.Hash).Hex()}, - {Key: "txIndex", Value: "0"}, - {Key: "amount", Value: "1000"}, - {Key: "txGasUsed", Value: "21000"}, - {Key: "txHash", Value: ""}, - {Key: "recipient", Value: ""}, - }}, - }, - }, - } - - txFromMsg, _ := rpc.NewRPCTxFromMsg( - msgEthTx, - common.BytesToHash(defaultBlock.Hash().Bytes()), - 1, - 0, - big.NewInt(1), - s.backend.chainID, - ) - testCases := []struct { - name string - registerMock func() - block *tmrpctypes.ResultBlock - idx hexutil.Uint - expRPCTx *rpc.EthTxJsonRPC - expPass bool - }{ - { - "pass - block txs index out of bound", - func() { - client := s.backend.clientCtx.Client.(*mocks.Client) - _, err := RegisterBlockResults(client, 1) - s.Require().NoError(err) - }, - &tmrpctypes.ResultBlock{Block: types.MakeBlock(1, []types.Tx{bz}, nil, nil)}, - 1, - nil, - true, - }, - { - "pass - Can't fetch base fee", - func() { - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - client := s.backend.clientCtx.Client.(*mocks.Client) - _, err := RegisterBlockResults(client, 1) - s.Require().NoError(err) - RegisterBaseFeeError(queryClient) - }, - &tmrpctypes.ResultBlock{Block: defaultBlock}, - 0, - txFromMsg, - true, - }, - { - "pass - Gets Tx by transaction index", - func() { - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - client := s.backend.clientCtx.Client.(*mocks.Client) - db := dbm.NewMemDB() - s.backend.indexer = indexer.NewKVIndexer(db, tmlog.NewNopLogger(), s.backend.clientCtx) - txBz := s.signAndEncodeEthTx(msgEthTx) - block := &types.Block{Header: types.Header{Height: 1, ChainID: "test"}, Data: types.Data{Txs: []types.Tx{txBz}}} - err := s.backend.indexer.IndexBlock(block, defaultResponseDeliverTx) - s.Require().NoError(err) - _, err = RegisterBlockResults(client, 1) - s.Require().NoError(err) - RegisterBaseFee(queryClient, math.NewInt(1)) - }, - &tmrpctypes.ResultBlock{Block: defaultBlock}, - 0, - txFromMsg, - true, - }, - { - "pass - returns the Ethereum format transaction by the Ethereum hash", - func() { - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - client := s.backend.clientCtx.Client.(*mocks.Client) - _, err := RegisterBlockResults(client, 1) - s.Require().NoError(err) - RegisterBaseFee(queryClient, math.NewInt(1)) - }, - &tmrpctypes.ResultBlock{Block: defaultBlock}, - 0, - txFromMsg, - true, + name: "sad: tx not found", + blockHash: blockHash, + txIndex: 9999, + wantTxFound: false, }, } for _, tc := range testCases { s.Run(tc.name, func() { - s.SetupTest() // reset - tc.registerMock() - - rpcTx, err := s.backend.GetTransactionByBlockAndIndex(tc.block, tc.idx) - - if tc.expPass { - s.Require().NoError(err) - s.Require().Equal(rpcTx, tc.expRPCTx) - } else { - s.Require().Error(err) + tx, err := s.backend.GetTransactionByBlockHashAndIndex(tc.blockHash, hexutil.Uint(tc.txIndex)) + if !tc.wantTxFound { + s.Require().Nil(tx) + return } + s.Require().NoError(err) + s.Require().NotNil(tx) + AssertTxResults(s, tx, transferTxHash) }) } } func (s *BackendSuite) TestGetTransactionByBlockNumberAndIndex() { - msgEthTx, bz := s.buildEthereumTx() - defaultBlock := types.MakeBlock(1, []types.Tx{bz}, nil, nil) - txFromMsg, _ := rpc.NewRPCTxFromMsg( - msgEthTx, - common.BytesToHash(defaultBlock.Hash().Bytes()), - 1, - 0, - big.NewInt(1), - s.backend.chainID, - ) testCases := []struct { - name string - registerMock func() - blockNum rpc.BlockNumber - idx hexutil.Uint - expRPCTx *rpc.EthTxJsonRPC - expPass bool + name string + blockNumber rpc.BlockNumber + txIndex uint + wantTxFound bool }{ { - "fail - block not found return nil", - func() { - client := s.backend.clientCtx.Client.(*mocks.Client) - RegisterBlockError(client, 1) - }, - 0, - 0, - nil, - true, + name: "happy: tx found", + blockNumber: transferTxBlockNumber, + txIndex: 1, + wantTxFound: true, }, { - "pass - returns the transaction identified by block number and index", - func() { - client := s.backend.clientCtx.Client.(*mocks.Client) - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - _, err := RegisterBlock(client, 1, bz) - s.Require().NoError(err) - _, err = RegisterBlockResults(client, 1) - s.Require().NoError(err) - RegisterBaseFee(queryClient, math.NewInt(1)) - }, - 0, - 0, - txFromMsg, - true, + name: "sad: block not found", + blockNumber: rpc.NewBlockNumber(big.NewInt(9999999)), + txIndex: 1, + wantTxFound: false, }, - } - - for _, tc := range testCases { - s.Run(tc.name, func() { - s.SetupTest() // reset - tc.registerMock() - - rpcTx, err := s.backend.GetTransactionByBlockNumberAndIndex(tc.blockNum, tc.idx) - if tc.expPass { - s.Require().NoError(err) - s.Require().Equal(rpcTx, tc.expRPCTx) - } else { - s.Require().Error(err) - } - }) - } -} - -func (s *BackendSuite) TestGetTransactionByTxIndex() { - _, bz := s.buildEthereumTx() - - testCases := []struct { - name string - registerMock func() - height int64 - index uint - expTxResult *eth.TxResult - expPass bool - }{ { - "fail - Ethereum tx with query not found", - func() { - client := s.backend.clientCtx.Client.(*mocks.Client) - s.backend.indexer = nil - RegisterTxSearch(client, "tx.height=0 AND ethereum_tx.txIndex=0", bz) - }, - 0, - 0, - ð.TxResult{}, - false, + name: "sad: tx not found", + blockNumber: transferTxBlockNumber, + txIndex: 9999, + wantTxFound: false, }, } for _, tc := range testCases { s.Run(tc.name, func() { - s.SetupTest() // reset - tc.registerMock() - - txResults, err := s.backend.GetTxByTxIndex(tc.height, tc.index) - - if tc.expPass { - s.Require().NoError(err) - s.Require().Equal(txResults, tc.expTxResult) - } else { - s.Require().Error(err) - } - }) - } -} - -func (s *BackendSuite) TestQueryTendermintTxIndexer() { - testCases := []struct { - name string - registerMock func() - txGetter func(*rpc.ParsedTxs) *rpc.ParsedTx - query string - expTxResult *eth.TxResult - expPass bool - }{ - { - "fail - Ethereum tx with query not found", - func() { - client := s.backend.clientCtx.Client.(*mocks.Client) - RegisterTxSearchEmpty(client, "") - }, - func(txs *rpc.ParsedTxs) *rpc.ParsedTx { - return &rpc.ParsedTx{} - }, - "", - ð.TxResult{}, - false, - }, - } - - for _, tc := range testCases { - s.Run(tc.name, func() { - s.SetupTest() // reset - tc.registerMock() - - txResults, err := s.backend.queryTendermintTxIndexer(tc.query, tc.txGetter) - - if tc.expPass { - s.Require().NoError(err) - s.Require().Equal(txResults, tc.expTxResult) - } else { - s.Require().Error(err) + tx, err := s.backend.GetTransactionByBlockNumberAndIndex(tc.blockNumber, hexutil.Uint(tc.txIndex)) + if !tc.wantTxFound { + s.Require().Nil(tx) + return } - }) - } -} - -func (s *BackendSuite) TestGetTransactionReceipt() { - msgEthereumTx, _ := s.buildEthereumTx() - txHash := msgEthereumTx.AsTransaction().Hash() - - txBz := s.signAndEncodeEthTx(msgEthereumTx) - - testCases := []struct { - name string - registerMock func() - tx *evm.MsgEthereumTx - block *types.Block - blockResult []*abci.ResponseDeliverTx - expTxReceipt map[string]interface{} - expPass bool - }{ - { - "fail - Receipts do not match", - func() { - var header metadata.MD - queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) - client := s.backend.clientCtx.Client.(*mocks.Client) - RegisterParams(queryClient, &header, 1) - RegisterParamsWithoutHeader(queryClient, 1) - _, err := RegisterBlock(client, 1, txBz) - s.Require().NoError(err) - _, err = RegisterBlockResults(client, 1) - s.Require().NoError(err) - }, - msgEthereumTx, - &types.Block{Header: types.Header{Height: 1}, Data: types.Data{Txs: []types.Tx{txBz}}}, - []*abci.ResponseDeliverTx{ - { - Code: 0, - Events: []abci.Event{ - {Type: evm.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ - {Key: "ethereumTxHash", Value: txHash.Hex()}, - {Key: "txIndex", Value: "0"}, - {Key: "amount", Value: "1000"}, - {Key: "txGasUsed", Value: "21000"}, - {Key: "txHash", Value: ""}, - {Key: "recipient", Value: "0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7"}, - }}, - }, - }, - }, - map[string]interface{}(nil), - false, - }, - } - - for _, tc := range testCases { - s.Run(tc.name, func() { - s.SetupTest() // reset - tc.registerMock() - - db := dbm.NewMemDB() - s.backend.indexer = indexer.NewKVIndexer(db, tmlog.NewNopLogger(), s.backend.clientCtx) - err := s.backend.indexer.IndexBlock(tc.block, tc.blockResult) s.Require().NoError(err) - - txReceipt, err := s.backend.GetTransactionReceipt(common.HexToHash(tc.tx.Hash)) - if tc.expPass { - s.Require().NoError(err) - s.Require().Equal(txReceipt, tc.expTxReceipt) - } else { - s.Require().NotEqual(txReceipt, tc.expTxReceipt) - } + s.Require().NotNil(tx) + AssertTxResults(s, tx, transferTxHash) }) } } -func (s *BackendSuite) TestGetGasUsed() { - origin := s.backend.cfg.JSONRPC.FixRevertGasRefundHeight - testCases := []struct { - name string - fixRevertGasRefundHeight int64 - txResult *eth.TxResult - price *big.Int - gas uint64 - exp uint64 - }{ - { - "success txResult", - 1, - ð.TxResult{ - Height: 1, - Failed: false, - GasUsed: 53026, - }, - new(big.Int).SetUint64(0), - 0, - 53026, - }, - { - "fail txResult before cap", - 2, - ð.TxResult{ - Height: 1, - Failed: true, - GasUsed: 53026, - }, - new(big.Int).SetUint64(200000), - 5000000000000, - 1000000000000000000, - }, - { - "fail txResult after cap", - 2, - ð.TxResult{ - Height: 3, - Failed: true, - GasUsed: 53026, - }, - new(big.Int).SetUint64(200000), - 5000000000000, - 53026, - }, - } - for _, tc := range testCases { - s.Run(fmt.Sprintf("Case %s", tc.name), func() { - s.backend.cfg.JSONRPC.FixRevertGasRefundHeight = tc.fixRevertGasRefundHeight - s.Require().Equal(tc.exp, s.backend.GetGasUsed(tc.txResult, tc.price, tc.gas)) - s.backend.cfg.JSONRPC.FixRevertGasRefundHeight = origin - }) - } +func AssertTxResults(s *BackendSuite, tx *rpc.EthTxJsonRPC, expectedTxHash gethcommon.Hash) { + s.Require().Equal(s.fundedAccEthAddr, tx.From) + s.Require().Equal(&recipient, tx.To) + s.Require().Greater(tx.Gas, uint64(0)) + s.Require().Equal(expectedTxHash, tx.Hash) + s.Require().Equal(uint64(1), uint64(*tx.TransactionIndex)) } diff --git a/eth/rpc/backend/utils.go b/eth/rpc/backend/utils.go index e436297bd..499bf107e 100644 --- a/eth/rpc/backend/utils.go +++ b/eth/rpc/backend/utils.go @@ -47,7 +47,6 @@ func (s sortGasAndReward) Less(i, j int) bool { // getAccountNonce returns the account nonce for the given account address. // If the pending value is true, it will iterate over the mempool (pending) // txs in order to compute and return the pending tx sequence. -// Todo: include the ability to specify a blockNumber func (b *Backend) getAccountNonce(accAddr common.Address, pending bool, height int64, logger log.Logger) (uint64, error) { queryClient := authtypes.NewQueryClient(b.clientCtx) adr := sdk.AccAddress(accAddr.Bytes()).String() @@ -72,8 +71,8 @@ func (b *Backend) getAccountNonce(accAddr common.Address, pending bool, height i return nonce, nil } - // the account retriever doesn't include the uncommitted transactions on the nonce so we need to - // to manually add them. + // the account retriever doesn't include the uncommitted transactions on the + // nonce so we need to to manually add them. pendingTxs, err := b.PendingTransactions() if err != nil { logger.Error("failed to fetch pending transactions", "error", err.Error()) diff --git a/eth/rpc/backend/utils_test.go b/eth/rpc/backend/utils_test.go index 4c7c14a74..f0dcea536 100644 --- a/eth/rpc/backend/utils_test.go +++ b/eth/rpc/backend/utils_test.go @@ -1,52 +1 @@ -package backend - -import ( - "fmt" - - "github.com/cometbft/cometbft/proto/tendermint/crypto" -) - -func mookProofs(num int, withData bool) *crypto.ProofOps { - var proofOps *crypto.ProofOps - if num > 0 { - proofOps = new(crypto.ProofOps) - for i := 0; i < num; i++ { - proof := crypto.ProofOp{} - if withData { - proof.Data = []byte("\n\031\n\003KEY\022\005VALUE\032\013\010\001\030\001 \001*\003\000\002\002") - } - proofOps.Ops = append(proofOps.Ops, proof) - } - } - return proofOps -} - -func (s *BackendSuite) TestGetHexProofs() { - defaultRes := []string{""} - testCases := []struct { - name string - proof *crypto.ProofOps - exp []string - }{ - { - "no proof provided", - mookProofs(0, false), - defaultRes, - }, - { - "no proof data provided", - mookProofs(1, false), - defaultRes, - }, - { - "valid proof provided", - mookProofs(1, true), - []string{"0x0a190a034b4559120556414c55451a0b0801180120012a03000202"}, - }, - } - for _, tc := range testCases { - s.Run(fmt.Sprintf("Case %s", tc.name), func() { - s.Require().Equal(tc.exp, GetHexProofs(tc.proof)) - }) - } -} +package backend_test diff --git a/eth/rpc/rpcapi/eth_api_test.go b/eth/rpc/rpcapi/eth_api_test.go index 74f5a8af5..54e992462 100644 --- a/eth/rpc/rpcapi/eth_api_test.go +++ b/eth/rpc/rpcapi/eth_api_test.go @@ -86,7 +86,10 @@ func (s *NodeSuite) SetupSuite() { s.fundedAccNibiAddr = eth.EthAddrToNibiruAddr(s.fundedAccEthAddr) funds := sdk.NewCoins(sdk.NewInt64Coin(eth.EthBaseDenom, 100_000_000)) // 10 NIBI - s.NoError(testnetwork.FillWalletFromValidator(s.fundedAccNibiAddr, funds, s.val, eth.EthBaseDenom)) + _, err = testnetwork.FillWalletFromValidator( + s.fundedAccNibiAddr, funds, s.val, eth.EthBaseDenom, + ) + s.Require().NoError(err) s.NoError(s.network.WaitForNextBlock()) } @@ -219,9 +222,10 @@ func (s *NodeSuite) Test_SimpleTransferTransaction() { s.T().Log("make sure the sender has enough funds") weiToSend := evm.NativeToWei(big.NewInt(1)) // 1 unibi funds := sdk.NewCoins(sdk.NewInt64Coin(eth.EthBaseDenom, 5_000_000)) // 5 * 10^6 unibi - s.Require().NoError(testnetwork.FillWalletFromValidator( - s.fundedAccNibiAddr, funds, s.network.Validators[0], eth.EthBaseDenom), + _, err = testnetwork.FillWalletFromValidator( + s.fundedAccNibiAddr, funds, s.network.Validators[0], eth.EthBaseDenom, ) + s.Require().NoError(err) s.NoError(s.network.WaitForNextBlock()) senderBalanceBeforeWei, err := s.ethClient.BalanceAt( @@ -317,9 +321,10 @@ func (s *NodeSuite) Test_SmartContract() { s.T().Log("Make sure the account has funds.") funds := sdk.NewCoins(sdk.NewInt64Coin(eth.EthBaseDenom, 1_000_000_000)) - s.Require().NoError(testnetwork.FillWalletFromValidator( - s.fundedAccNibiAddr, funds, s.network.Validators[0], eth.EthBaseDenom), + _, err = testnetwork.FillWalletFromValidator( + s.fundedAccNibiAddr, funds, s.network.Validators[0], eth.EthBaseDenom, ) + s.Require().NoError(err) s.NoError(s.network.WaitForNextBlock()) grpcUrl := s.network.Validators[0].AppConfig.GRPC.Address diff --git a/x/common/testutil/testnetwork/start_node.go b/x/common/testutil/testnetwork/start_node.go index 6753ffdc5..a12317c10 100644 --- a/x/common/testutil/testnetwork/start_node.go +++ b/x/common/testutil/testnetwork/start_node.go @@ -160,7 +160,8 @@ func startNodeAndServers(cfg Config, val *Validator) error { val.Ctx.Logger, val.ClientCtx, val.AppConfig.JSONRPC.AllowUnprotectedTxs, - val.EthTxIndexer, + // TODO: reenable indexer when we have indexer service (process which does IndexBlock) implemented + nil, //val.EthTxIndexer ) val.Logger.Log("Expose typed methods for each namespace") diff --git a/x/common/testutil/testnetwork/tx_test.go b/x/common/testutil/testnetwork/tx_test.go index c3fdf9a46..94a9db6bd 100644 --- a/x/common/testutil/testnetwork/tx_test.go +++ b/x/common/testutil/testnetwork/tx_test.go @@ -69,7 +69,8 @@ func (s *TestSuite) TestFillWalletFromValidator() { sdk.NewInt64Coin(denoms.NIBI, 420), ) feeDenom := denoms.NIBI - s.NoError(testnetwork.FillWalletFromValidator( + _, err := testnetwork.FillWalletFromValidator( toAddr, funds, val, feeDenom, - )) + ) + s.Require().NoError(err) } diff --git a/x/common/testutil/testnetwork/util.go b/x/common/testutil/testnetwork/util.go index 4ba1e082d..6ecb36bba 100644 --- a/x/common/testutil/testnetwork/util.go +++ b/x/common/testutil/testnetwork/util.go @@ -126,7 +126,7 @@ func writeFile(name string, dir string, contents []byte) error { // validator. func FillWalletFromValidator( addr sdk.AccAddress, balance sdk.Coins, val *Validator, feesDenom string, -) error { +) (*sdk.TxResponse, error) { rawResp, err := clitestutil.MsgSendExec( val.ClientCtx, val.Address, @@ -137,19 +137,19 @@ func FillWalletFromValidator( fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewInt64Coin(feesDenom, 10000)), ) if err != nil { - return err + return nil, err } return txOK(val.ClientCtx.Codec, rawResp.Bytes()) } -func txOK(jsonCodec sdkcodec.JSONCodec, txBytes []byte) error { +func txOK(jsonCodec sdkcodec.JSONCodec, txBytes []byte) (*sdk.TxResponse, error) { resp := new(sdk.TxResponse) jsonCodec.MustUnmarshalJSON(txBytes, resp) if resp.Code != tmtypes.CodeTypeOK { - return fmt.Errorf("%s", resp.RawLog) + return resp, fmt.Errorf("%s", resp.RawLog) } - return nil + return resp, nil } /* diff --git a/x/common/testutil/testnetwork/validator_node.go b/x/common/testutil/testnetwork/validator_node.go index bbdb53395..131d74aa2 100644 --- a/x/common/testutil/testnetwork/validator_node.go +++ b/x/common/testutil/testnetwork/validator_node.go @@ -20,6 +20,7 @@ import ( "github.com/cometbft/cometbft/node" tmclient "github.com/cometbft/cometbft/rpc/client" + cmtcore "github.com/cometbft/cometbft/rpc/core/types" "github.com/cosmos/cosmos-sdk/client" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" "github.com/cosmos/cosmos-sdk/server" @@ -265,3 +266,15 @@ func (val *Validator) AssertERC20Balance( balance := new(big.Int).SetBytes(recipientBalanceBeforeBytes) s.Equal(expectedBalance.String(), balance.String()) } + +func (node *Validator) BlockByEthTx( + ethTxHash gethcommon.Hash, +) (*cmtcore.ResultBlockResults, error) { + blankCtx := context.Background() + txReceipt, err := node.JSONRPCClient.TransactionReceipt(blankCtx, ethTxHash) + if err != nil { + return nil, err + } + blockHeightOfTx := txReceipt.BlockNumber.Int64() + return node.RPCClient.BlockResults(blankCtx, &blockHeightOfTx) +} diff --git a/x/sudo/cli/cli_test.go b/x/sudo/cli/cli_test.go index 2150f1c19..96baa5064 100644 --- a/x/sudo/cli/cli_test.go +++ b/x/sudo/cli/cli_test.go @@ -130,9 +130,11 @@ func (s *TestSuite) FundRoot(root Account) { sdk.NewInt64Coin(denoms.NIBI, 420*common.TO_MICRO), ) feeDenom := denoms.NIBI - s.NoError(testnetwork.FillWalletFromValidator( + + _, err := testnetwork.FillWalletFromValidator( root.addr, funds, val, feeDenom, - )) + ) + s.NoError(err) } func (s *TestSuite) AddRootToKeyring(root Account) { From 0929266a425432d1b17eab513beca3c2d29bf688 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Sep 2024 02:01:05 +0800 Subject: [PATCH 06/16] chore(deps-dev): bump axios from 1.7.3 to 1.7.4 in /x/evm/embeds (#2016) * chore(deps-dev): bump axios from 1.7.3 to 1.7.4 in /x/evm/embeds Bumps [axios](https://github.com/axios/axios) from 1.7.3 to 1.7.4. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.7.3...v1.7.4) --- updated-dependencies: - dependency-name: axios dependency-type: indirect ... Signed-off-by: dependabot[bot] * Updated changelog - dependabot --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Unique Divine <51418232+Unique-Divine@users.noreply.github.com> Co-authored-by: Unique-Divine --- CHANGELOG.md | 1 + x/evm/embeds/package-lock.json | 7 +++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49d5d9dfe..555fa5992 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -173,6 +173,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump `github.com/btcsuite/btcd` from 0.23.3 to 0.24.0 ([#1862](https://github.com/NibiruChain/nibiru/pull/1862)) - Bump `pozetroninc/github-action-get-latest-release` from 0.7.0 to 0.8.0 ([#1863](https://github.com/NibiruChain/nibiru/pull/1863)) - Bump `bufbuild/buf-setup-action` from 1.30.1 to 1.36.0 ([#1891](https://github.com/NibiruChain/nibiru/pull/1891), [#1900](https://github.com/NibiruChain/nibiru/pull/1900), [#1923](https://github.com/NibiruChain/nibiru/pull/1923), [#1972](https://github.com/NibiruChain/nibiru/pull/1972), [#1974](https://github.com/NibiruChain/nibiru/pull/1974), [#1988](https://github.com/NibiruChain/nibiru/pull/1988)) +- Bump `axios` from 1.7.3 to 1.7.4 ([#2016](https://github.com/NibiruChain/nibiru/pull/2016)) ## [v1.5.0](https://github.com/NibiruChain/nibiru/releases/tag/v1.5.0) - 2024-06-21 diff --git a/x/evm/embeds/package-lock.json b/x/evm/embeds/package-lock.json index f08e691bd..257cdaa23 100644 --- a/x/evm/embeds/package-lock.json +++ b/x/evm/embeds/package-lock.json @@ -2471,11 +2471,10 @@ } }, "node_modules/axios": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.3.tgz", - "integrity": "sha512-Ar7ND9pU99eJ9GpoGQKhKf58GpUOgnzuaB7ueNQ5BMi0p+LZ5oaEnfF999fAArcTIBwXTCHAmGcHOZJaWPq9Nw==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", + "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", "dev": true, - "license": "MIT", "peer": true, "dependencies": { "follow-redirects": "^1.15.6", From 5214349712f42547bc986a4ed4d224252de7407d Mon Sep 17 00:00:00 2001 From: Oleg Nikonychev Date: Sun, 22 Sep 2024 00:07:05 +0400 Subject: [PATCH 07/16] test(evm): backend tests with test network and real txs (#2045) * test(evm): backend tests with test network and real txs * chore: changelog update --- CHANGELOG.md | 1 + eth/chain_id.go | 8 +- eth/eip712/encoding.go | 10 +- eth/eip712/encoding_legacy.go | 11 +- eth/rpc/backend/account_info.go | 22 +-- eth/rpc/backend/account_info_test.go | 201 +++++++++++++++++++++++ eth/rpc/backend/backend.go | 6 +- eth/rpc/backend/backend_suite_test.go | 108 ++++++++++-- eth/rpc/backend/blocks.go | 52 ++---- eth/rpc/backend/blocks_test.go | 110 ++++++++++--- eth/rpc/backend/call_tx.go | 88 +--------- eth/rpc/backend/call_tx_test.go | 89 ++++++++++ eth/rpc/backend/chain_info.go | 39 ++--- eth/rpc/backend/chain_info_test.go | 92 +++++++++++ eth/rpc/backend/evm_query_client_test.go | 1 - eth/rpc/backend/filters.go | 41 ----- eth/rpc/backend/filters_test.go | 1 - eth/rpc/backend/node_info.go | 27 +-- eth/rpc/backend/node_info_test.go | 44 +++++ eth/rpc/backend/sign_tx.go | 156 ------------------ eth/rpc/backend/sign_tx_test.go | 1 - eth/rpc/backend/tracing.go | 10 +- eth/rpc/backend/tx_info.go | 84 +++++----- eth/rpc/backend/tx_info_test.go | 53 +++++- eth/rpc/backend/utils.go | 16 +- eth/rpc/backend/utils_test.go | 67 ++++++++ eth/rpc/rpcapi/eth_api.go | 48 +----- eth/rpc/rpcapi/net_api.go | 8 +- 28 files changed, 830 insertions(+), 564 deletions(-) delete mode 100644 eth/rpc/backend/evm_query_client_test.go delete mode 100644 eth/rpc/backend/filters.go delete mode 100644 eth/rpc/backend/filters_test.go delete mode 100644 eth/rpc/backend/sign_tx.go delete mode 100644 eth/rpc/backend/sign_tx_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 555fa5992..4aa34212e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -121,6 +121,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#2031](https://github.com/NibiruChain/nibiru/pull/2031) - fix(evm): debug calls with custom tracer and tracer options - [#2039](https://github.com/NibiruChain/nibiru/pull/2039) - refactor(rpc-backend): remove unnecessary interface code - [#2039](https://github.com/NibiruChain/nibiru/pull/2039) - refactor(rpc-backend): Remove mocks from eth/rpc/backend, partially completing [nibiru#2037](https://github.com/NibiruChain/nibiru/issue/2037). +- [#2045](https://github.com/NibiruChain/nibiru/pull/2045) - test(evm): backend tests with test network and real txs #### Dapp modules: perp, spot, oracle, etc diff --git a/eth/chain_id.go b/eth/chain_id.go index 3c5491594..541e8fa0b 100644 --- a/eth/chain_id.go +++ b/eth/chain_id.go @@ -38,14 +38,14 @@ func IsValidChainID(chainID string) bool { return nibiruEvmChainId.MatchString(chainID) } -// ParseEthChainID parses a string chain identifier's epoch to an -// Ethereum-compatible chain-id in *big.Int format. +// ParseEthChainID parses a string chain identifier's +// to an Ethereum-compatible chain-id in *big.Int format. // // This function uses Nibiru's map of chain IDs defined in Nibiru/app/appconst // rather than the regex of EIP155, which is implemented by // ParseEthChainIDStrict. -func ParseEthChainID(chainID string) (*big.Int, error) { - return appconst.GetEthChainID(chainID), nil +func ParseEthChainID(chainID string) *big.Int { + return appconst.GetEthChainID(chainID) } // ParseEthChainIDStrict parses a string chain identifier's epoch to an diff --git a/eth/eip712/encoding.go b/eth/eip712/encoding.go index 2c2b8ea0b..2950d68b1 100644 --- a/eth/eip712/encoding.go +++ b/eth/eip712/encoding.go @@ -103,10 +103,7 @@ func decodeAminoSignDoc(signDocBytes []byte) (apitypes.TypedData, error) { return apitypes.TypedData{}, err } - chainID, err := eth.ParseEthChainID(aminoDoc.ChainID) - if err != nil { - return apitypes.TypedData{}, errors.New("invalid chain ID passed as argument") - } + chainID := eth.ParseEthChainID(aminoDoc.ChainID) typedData, err := WrapTxToTypedData( chainID.Uint64(), @@ -167,10 +164,7 @@ func decodeProtobufSignDoc(signDocBytes []byte) (apitypes.TypedData, error) { signerInfo := authInfo.SignerInfos[0] - chainID, err := eth.ParseEthChainID(signDoc.ChainId) - if err != nil { - return apitypes.TypedData{}, fmt.Errorf("invalid chain ID passed as argument: %w", err) - } + chainID := eth.ParseEthChainID(signDoc.ChainId) stdFee := &legacytx.StdFee{ Amount: authInfo.Fee.Amount, diff --git a/eth/eip712/encoding_legacy.go b/eth/eip712/encoding_legacy.go index 751be9a4b..805eb48dd 100644 --- a/eth/eip712/encoding_legacy.go +++ b/eth/eip712/encoding_legacy.go @@ -94,11 +94,7 @@ func legacyDecodeAminoSignDoc(signDocBytes []byte) (apitypes.TypedData, error) { feeDelegation := &FeeDelegationOptions{ FeePayer: feePayer, } - - chainID, err := eth.ParseEthChainID(aminoDoc.ChainID) - if err != nil { - return apitypes.TypedData{}, errors.New("invalid chain ID passed as argument") - } + chainID := eth.ParseEthChainID(aminoDoc.ChainID) typedData, err := LegacyWrapTxToTypedData( protoCodec, @@ -165,10 +161,7 @@ func legacyDecodeProtobufSignDoc(signDocBytes []byte) (apitypes.TypedData, error signerInfo := authInfo.SignerInfos[0] - chainID, err := eth.ParseEthChainID(signDoc.ChainId) - if err != nil { - return apitypes.TypedData{}, fmt.Errorf("invalid chain ID passed as argument: %w", err) - } + chainID := eth.ParseEthChainID(signDoc.ChainId) stdFee := &legacytx.StdFee{ Amount: authInfo.Fee.Amount, diff --git a/eth/rpc/backend/account_info.go b/eth/rpc/backend/account_info.go index 94478a259..53fcd222e 100644 --- a/eth/rpc/backend/account_info.go +++ b/eth/rpc/backend/account_info.go @@ -12,7 +12,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" - "github.com/ethereum/go-ethereum/common" + gethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/pkg/errors" @@ -22,7 +22,7 @@ import ( // GetCode returns the contract code at the given address and block number. func (b *Backend) GetCode( - address common.Address, blockNrOrHash rpc.BlockNumberOrHash, + address gethcommon.Address, blockNrOrHash rpc.BlockNumberOrHash, ) (hexutil.Bytes, error) { blockNum, err := b.BlockNumberFromTendermint(blockNrOrHash) if err != nil { @@ -43,7 +43,7 @@ func (b *Backend) GetCode( // GetProof returns an account object with proof and any storage proofs func (b *Backend) GetProof( - address common.Address, + address gethcommon.Address, storageKeys []string, blockNrOrHash rpc.BlockNumberOrHash, ) (*rpc.AccountResult, error) { @@ -81,7 +81,7 @@ func (b *Backend) GetProof( storageProofs := make([]rpc.StorageResult, len(storageKeys)) for i, key := range storageKeys { - hexKey := common.HexToHash(key) + hexKey := gethcommon.HexToHash(key) valueBz, proof, err := b.queryClient.GetProof(clientCtx, evm.StoreKey, evm.StateKey(address, hexKey.Bytes())) if err != nil { return nil, err @@ -105,7 +105,7 @@ func (b *Backend) GetProof( } // query account proofs - accountKey := authtypes.AddressStoreKey(sdk.AccAddress(address.Bytes())) + accountKey := authtypes.AddressStoreKey(address.Bytes()) _, proof, err := b.queryClient.GetProof(clientCtx, authtypes.StoreKey, accountKey) if err != nil { return nil, err @@ -120,18 +120,18 @@ func (b *Backend) GetProof( Address: address, AccountProof: GetHexProofs(proof), Balance: (*hexutil.Big)(balance.BigInt()), - CodeHash: common.HexToHash(res.CodeHash), + CodeHash: gethcommon.HexToHash(res.CodeHash), Nonce: hexutil.Uint64(res.Nonce), // NOTE: The StorageHash is blank. Consider whether this is useful in the // future. Currently, all storage is handles by persistent and transient // `sdk.KVStore` objects. - StorageHash: common.Hash{}, + StorageHash: gethcommon.Hash{}, StorageProof: storageProofs, }, nil } // GetStorageAt returns the contract storage at the given address, block number, and key. -func (b *Backend) GetStorageAt(address common.Address, key string, blockNrOrHash rpc.BlockNumberOrHash) (hexutil.Bytes, error) { +func (b *Backend) GetStorageAt(address gethcommon.Address, key string, blockNrOrHash rpc.BlockNumberOrHash) (hexutil.Bytes, error) { blockNum, err := b.BlockNumberFromTendermint(blockNrOrHash) if err != nil { return nil, err @@ -147,13 +147,13 @@ func (b *Backend) GetStorageAt(address common.Address, key string, blockNrOrHash return nil, err } - value := common.HexToHash(res.Value) + value := gethcommon.HexToHash(res.Value) return value.Bytes(), nil } // GetBalance returns the provided account's balance up to the provided block number. func (b *Backend) GetBalance( - address common.Address, + address gethcommon.Address, blockNrOrHash rpc.BlockNumberOrHash, ) (*hexutil.Big, error) { blockNum, err := b.BlockNumberFromTendermint(blockNrOrHash) @@ -189,7 +189,7 @@ func (b *Backend) GetBalance( } // GetTransactionCount returns the number of transactions at the given address up to the given block number. -func (b *Backend) GetTransactionCount(address common.Address, blockNum rpc.BlockNumber) (*hexutil.Uint64, error) { +func (b *Backend) GetTransactionCount(address gethcommon.Address, blockNum rpc.BlockNumber) (*hexutil.Uint64, error) { n := hexutil.Uint64(0) bn, err := b.BlockNumber() if err != nil { diff --git a/eth/rpc/backend/account_info_test.go b/eth/rpc/backend/account_info_test.go index f0dcea536..109cc3d49 100644 --- a/eth/rpc/backend/account_info_test.go +++ b/eth/rpc/backend/account_info_test.go @@ -1 +1,202 @@ package backend_test + +import ( + "math/big" + + gethcommon "github.com/ethereum/go-ethereum/common" + "golang.org/x/crypto/sha3" + + "github.com/NibiruChain/nibiru/v2/x/evm/evmtest" + + rpc "github.com/NibiruChain/nibiru/v2/eth/rpc" +) + +func (s *BackendSuite) TestGetCode() { + testCases := []struct { + name string + contractAddr gethcommon.Address + blockNumber rpc.BlockNumber + codeFound bool + }{ + { + name: "happy: valid contract address", + contractAddr: testContractAddress, + blockNumber: deployContractBlockNumber, + codeFound: true, + }, + { + name: "sad: not a contract address", + contractAddr: s.fundedAccEthAddr, + blockNumber: deployContractBlockNumber, + codeFound: false, + }, + } + for _, tc := range testCases { + s.Run(tc.name, func() { + code, err := s.backend.GetCode( + tc.contractAddr, + rpc.BlockNumberOrHash{ + BlockNumber: &tc.blockNumber, + }, + ) + if !tc.codeFound { + s.Require().Nil(code) + return + } + s.Require().NoError(err) + s.Require().NotNil(code) + }) + } +} + +func (s *BackendSuite) TestGetProof() { + testCases := []struct { + name string + contractAddr gethcommon.Address + blockNumber rpc.BlockNumber + address gethcommon.Address + slot uint64 + wantValue string + }{ + { + name: "happy: balance of the contract deployer", + contractAddr: testContractAddress, + address: s.fundedAccEthAddr, + blockNumber: deployContractBlockNumber, + slot: 0, // _balances is the first slot in ERC20 + wantValue: "0xd3c21bcecceda1000000", // = 1000000 * (10**18), initial supply + }, + { + name: "sad: address which is not in contract storage", + contractAddr: s.fundedAccEthAddr, + address: recipient, + blockNumber: deployContractBlockNumber, + slot: 0, + wantValue: "0x0", + }, + } + for _, tc := range testCases { + s.Run(tc.name, func() { + proof, err := s.backend.GetProof( + tc.contractAddr, + []string{generateStorageKey(tc.address, tc.slot)}, + rpc.BlockNumberOrHash{ + BlockNumber: &tc.blockNumber, + }, + ) + s.Require().NoError(err) + s.Require().NotNil(proof) + s.Require().Equal(tc.wantValue, proof.StorageProof[0].Value.String()) + }) + } +} + +func (s *BackendSuite) TestGetStorageAt() { + testCases := []struct { + name string + contractAddr gethcommon.Address + blockNumber rpc.BlockNumber + address gethcommon.Address + slot uint64 + wantValue string + }{ + { + name: "happy: balance of the contract deployer", + contractAddr: testContractAddress, + address: s.fundedAccEthAddr, + blockNumber: deployContractBlockNumber, + // _balances is the first slot in ERC20 + slot: 0, + // = 1000000 * (10**18), initial supply + wantValue: "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000", + }, + { + name: "sad: address which is not in contract storage", + contractAddr: s.fundedAccEthAddr, + address: recipient, + blockNumber: deployContractBlockNumber, + slot: 0, + wantValue: "0x0000000000000000000000000000000000000000000000000000000000000000", + }, + } + for _, tc := range testCases { + s.Run(tc.name, func() { + value, err := s.backend.GetStorageAt( + tc.contractAddr, + generateStorageKey(tc.address, tc.slot), + rpc.BlockNumberOrHash{ + BlockNumber: &tc.blockNumber, + }, + ) + s.Require().NoError(err) + s.Require().NotNil(value) + s.Require().Equal(tc.wantValue, value.String()) + }) + } +} + +func (s *BackendSuite) TestGetBalance() { + testCases := []struct { + name string + blockNumber rpc.BlockNumber + address gethcommon.Address + wantPositiveBalance bool + }{ + { + name: "happy: funded account balance", + address: s.fundedAccEthAddr, + blockNumber: transferTxBlockNumber, + wantPositiveBalance: true, + }, + { + name: "happy: recipient balance at block 1", + address: recipient, + blockNumber: rpc.NewBlockNumber(big.NewInt(1)), + wantPositiveBalance: false, + }, + { + name: "happy: recipient balance after transfer", + address: recipient, + blockNumber: transferTxBlockNumber, + wantPositiveBalance: true, + }, + { + name: "sad: not existing account", + address: evmtest.NewEthPrivAcc().EthAddr, + blockNumber: transferTxBlockNumber, + wantPositiveBalance: false, + }, + } + for _, tc := range testCases { + s.Run(tc.name, func() { + balance, err := s.backend.GetBalance( + tc.address, + rpc.BlockNumberOrHash{ + BlockNumber: &tc.blockNumber, + }, + ) + s.Require().NoError(err) + s.Require().NotNil(balance) + if tc.wantPositiveBalance { + s.Require().Greater(balance.ToInt().Int64(), int64(0)) + } else { + s.Require().Equal(balance.ToInt().Int64(), int64(0)) + } + }) + } +} + +// generateStorageKey produces the storage key from address and slot (order of the variable in solidity declaration) +func generateStorageKey(key gethcommon.Address, slot uint64) string { + // Prepare the key and slot as 32-byte values + keyBytes := gethcommon.LeftPadBytes(key.Bytes(), 32) + slotBytes := gethcommon.LeftPadBytes(new(big.Int).SetUint64(slot).Bytes(), 32) + + // Concatenate key and slot + data := append(keyBytes, slotBytes...) + + // Hash the data using Keccak256 + hash := sha3.NewLegacyKeccak256() + hash.Write(data) + return gethcommon.BytesToHash(hash.Sum(nil)).Hex() +} diff --git a/eth/rpc/backend/backend.go b/eth/rpc/backend/backend.go index 2f569125f..7383fe600 100644 --- a/eth/rpc/backend/backend.go +++ b/eth/rpc/backend/backend.go @@ -37,11 +37,7 @@ func NewBackend( allowUnprotectedTxs bool, indexer eth.EVMTxIndexer, ) *Backend { - chainID, err := eth.ParseEthChainID(clientCtx.ChainID) - if err != nil { - panic(err) - } - + chainID := eth.ParseEthChainID(clientCtx.ChainID) appConf, err := config.GetConfig(ctx.Viper) if err != nil { panic(err) diff --git a/eth/rpc/backend/backend_suite_test.go b/eth/rpc/backend/backend_suite_test.go index 1914bf865..540bdddb4 100644 --- a/eth/rpc/backend/backend_suite_test.go +++ b/eth/rpc/backend/backend_suite_test.go @@ -1,8 +1,11 @@ package backend_test import ( + "context" + "fmt" "math/big" "testing" + "time" "crypto/ecdsa" @@ -13,6 +16,8 @@ import ( "github.com/ethereum/go-ethereum/params" "github.com/stretchr/testify/suite" + "github.com/NibiruChain/nibiru/v2/x/evm/embeds" + "github.com/NibiruChain/nibiru/v2/x/evm/evmtest" "github.com/NibiruChain/nibiru/v2/eth/rpc" @@ -30,9 +35,14 @@ import ( var recipient = evmtest.NewEthPrivAcc().EthAddr var amountToSend = evm.NativeToWei(big.NewInt(1)) + var transferTxBlockNumber rpc.BlockNumber +var transferTxBlockHash gethcommon.Hash var transferTxHash gethcommon.Hash +var testContractAddress gethcommon.Address +var deployContractBlockNumber rpc.BlockNumber + type BackendSuite struct { suite.Suite cfg testnetwork.Config @@ -72,39 +82,101 @@ func (s *BackendSuite) SetupSuite() { txResp, err := testnetwork.FillWalletFromValidator( s.fundedAccNibiAddr, funds, s.node, eth.EthBaseDenom, ) - s.Require().NoError(err, txResp.TxHash) + s.Require().NoError(err) + s.Require().NotNil(txResp.TxHash) s.NoError(s.network.WaitForNextBlock()) - // Send 1 Transfer TX and use the results in the tests - transferTxBlockNumber, transferTxHash = s.sendNibiViaEthTransfer(recipient, amountToSend) + // Send Transfer TX and use the results in the tests + s.Require().NoError(err) + transferTxHash = s.SendNibiViaEthTransfer(recipient, amountToSend, true) + blockNumber, blockHash := WaitForReceipt(s, transferTxHash) + s.Require().NotNil(blockNumber) + s.Require().NotNil(blockHash) + transferTxBlockNumber = rpc.NewBlockNumber(blockNumber) + transferTxBlockHash = *blockHash + + // Deploy test erc20 contract + deployContractTxHash, contractAddress := s.DeployTestContract(true) + testContractAddress = contractAddress + blockNumber, blockHash = WaitForReceipt(s, deployContractTxHash) + s.Require().NotNil(blockNumber) + s.Require().NotNil(blockHash) + deployContractBlockNumber = rpc.NewBlockNumber(blockNumber) } // SendNibiViaEthTransfer sends nibi using the eth rpc backend -func (s *BackendSuite) sendNibiViaEthTransfer( +func (s *BackendSuite) SendNibiViaEthTransfer( to gethcommon.Address, amount *big.Int, -) (rpc.BlockNumber, gethcommon.Hash) { - block, err := s.backend.BlockNumber() + waitForNextBlock bool, +) gethcommon.Hash { + nonce, err := s.backend.GetTransactionCount(s.fundedAccEthAddr, rpc.EthPendingBlockNumber) s.Require().NoError(err) - s.NoError(err) - - signer := gethcore.LatestSignerForChainID(s.ethChainID) - gasPrice := evm.NativeToWei(big.NewInt(1)) - tx, err := gethcore.SignNewTx( - s.fundedAccPrivateKey, - signer, + return SendTransaction( + s, &gethcore.LegacyTx{ To: &to, + Nonce: uint64(*nonce), Value: amount, Gas: params.TxGas, - GasPrice: gasPrice, - }) + GasPrice: big.NewInt(1), + }, + waitForNextBlock, + ) +} + +// DeployTestContract deploys test erc20 contract +func (s *BackendSuite) DeployTestContract(waitForNextBlock bool) (gethcommon.Hash, gethcommon.Address) { + packedArgs, err := embeds.SmartContract_TestERC20.ABI.Pack("") s.Require().NoError(err) - txBz, err := tx.MarshalBinary() + bytecodeForCall := append(embeds.SmartContract_TestERC20.Bytecode, packedArgs...) + nonce, err := s.backend.GetTransactionCount(s.fundedAccEthAddr, rpc.EthPendingBlockNumber) + s.Require().NoError(err) + + txHash := SendTransaction( + s, + &gethcore.LegacyTx{ + Nonce: uint64(*nonce), + Data: bytecodeForCall, + Gas: 1500_000, + GasPrice: big.NewInt(1), + }, + waitForNextBlock, + ) + contractAddr := crypto.CreateAddress(s.fundedAccEthAddr, (uint64)(*nonce)) + return txHash, contractAddr +} + +// SendTransaction signs and sends raw ethereum transaction +func SendTransaction(s *BackendSuite, tx *gethcore.LegacyTx, waitForNextBlock bool) gethcommon.Hash { + signer := gethcore.LatestSignerForChainID(s.ethChainID) + signedTx, err := gethcore.SignNewTx(s.fundedAccPrivateKey, signer, tx) + s.Require().NoError(err) + txBz, err := signedTx.MarshalBinary() s.Require().NoError(err) txHash, err := s.backend.SendRawTransaction(txBz) s.Require().NoError(err) - s.Require().NoError(s.network.WaitForNextBlock()) + if waitForNextBlock { + s.Require().NoError(s.network.WaitForNextBlock()) + } + return txHash +} - return rpc.NewBlockNumber(big.NewInt(int64(block) + 1)), txHash +func WaitForReceipt(s *BackendSuite, txHash gethcommon.Hash) (*big.Int, *gethcommon.Hash) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + for { + receipt, err := s.backend.GetTransactionReceipt(txHash) + if err == nil { + return receipt.BlockNumber, &receipt.BlockHash + } + select { + case <-ctx.Done(): + fmt.Println("Timeout reached, transaction not included in a block yet.") + return nil, nil + default: + time.Sleep(1 * time.Second) + } + } } diff --git a/eth/rpc/backend/blocks.go b/eth/rpc/backend/blocks.go index ea3e38f95..2b90c55c6 100644 --- a/eth/rpc/backend/blocks.go +++ b/eth/rpc/backend/blocks.go @@ -13,7 +13,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" grpctypes "github.com/cosmos/cosmos-sdk/types/grpc" "github.com/cosmos/gogoproto/proto" - "github.com/ethereum/go-ethereum/common" + gethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" gethcore "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/trie" @@ -29,8 +29,7 @@ import ( // BlockNumber returns the current block number in abci app state. Because abci // app state could lag behind from tendermint latest block, it's more stable for -// the client to use the latest block number in abci app state than tendermint -// rpc. +// the client to use the latest block number in abci app state than tendermint rpc. func (b *Backend) BlockNumber() (hexutil.Uint64, error) { // do any grpc query, ignore the response and use the returned block height var header metadata.MD @@ -87,7 +86,7 @@ func (b *Backend) GetBlockByNumber(blockNum rpc.BlockNumber, fullTx bool) (map[s // GetBlockByHash returns the JSON-RPC compatible Ethereum block identified by // hash. -func (b *Backend) GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) { +func (b *Backend) GetBlockByHash(hash gethcommon.Hash, fullTx bool) (map[string]interface{}, error) { resBlock, err := b.TendermintBlockByHash(hash) if err != nil { return nil, err @@ -115,7 +114,7 @@ func (b *Backend) GetBlockByHash(hash common.Hash, fullTx bool) (map[string]inte // GetBlockTransactionCountByHash returns the number of Ethereum transactions in // the block identified by hash. -func (b *Backend) GetBlockTransactionCountByHash(hash common.Hash) *hexutil.Uint { +func (b *Backend) GetBlockTransactionCountByHash(hash gethcommon.Hash) *hexutil.Uint { sc, ok := b.clientCtx.Client.(tmrpcclient.SignClient) if !ok { b.logger.Error("invalid rpc client") @@ -202,7 +201,7 @@ func (b *Backend) TendermintBlockResultByNumber(height *int64) (*tmrpctypes.Resu } // TendermintBlockByHash returns a Tendermint-formatted block by block number -func (b *Backend) TendermintBlockByHash(blockHash common.Hash) (*tmrpctypes.ResultBlock, error) { +func (b *Backend) TendermintBlockByHash(blockHash gethcommon.Hash) (*tmrpctypes.ResultBlock, error) { sc, ok := b.clientCtx.Client.(tmrpcclient.SignClient) if !ok { return nil, errors.New("invalid rpc client") @@ -240,7 +239,7 @@ func (b *Backend) BlockNumberFromTendermint(blockNrOrHash rpc.BlockNumberOrHash) } // BlockNumberFromTendermintByHash returns the block height of given block hash -func (b *Backend) BlockNumberFromTendermintByHash(blockHash common.Hash) (*big.Int, error) { +func (b *Backend) BlockNumberFromTendermintByHash(blockHash gethcommon.Hash) (*big.Int, error) { resBlock, err := b.TendermintBlockByHash(blockHash) if err != nil { return nil, err @@ -294,7 +293,6 @@ func (b *Backend) EthMsgsFromTendermintBlock( result = append(result, ethMsg) } } - return result } @@ -329,36 +327,6 @@ func (b *Backend) HeaderByNumber(blockNum rpc.BlockNumber) (*gethcore.Header, er return ethHeader, nil } -// HeaderByHash returns the block header identified by hash. -func (b *Backend) HeaderByHash(blockHash common.Hash) (*gethcore.Header, error) { - resBlock, err := b.TendermintBlockByHash(blockHash) - if err != nil { - return nil, err - } - if resBlock == nil { - return nil, errors.Errorf("block not found for hash %s", blockHash.Hex()) - } - - blockRes, err := b.TendermintBlockResultByNumber(&resBlock.Block.Height) - if err != nil { - return nil, errors.Errorf("block result not found for height %d", resBlock.Block.Height) - } - - bloom, err := b.BlockBloom(blockRes) - if err != nil { - b.logger.Debug("HeaderByHash BlockBloom failed", "height", resBlock.Block.Height) - } - - baseFee, err := b.BaseFee(blockRes) - if err != nil { - // handle the error for pruned node. - b.logger.Error("failed to fetch Base Fee from prunned block. Check node prunning configuration", "height", resBlock.Block.Height, "error", err) - } - - ethHeader := rpc.EthHeaderFromTendermint(resBlock.Block.Header, bloom, baseFee) - return ethHeader, nil -} - // BlockBloom query block bloom filter from block results func (b *Backend) BlockBloom(blockRes *tmrpctypes.ResultBlockResults) (gethcore.Bloom, error) { msgType := proto.MessageName((*evm.EventBlockBloom)(nil)) @@ -399,7 +367,7 @@ func (b *Backend) RPCBlockFromTendermintBlock( msgs := b.EthMsgsFromTendermintBlock(resBlock, blockRes) for txIndex, ethMsg := range msgs { if !fullTx { - hash := common.HexToHash(ethMsg.Hash) + hash := gethcommon.HexToHash(ethMsg.Hash) ethRPCTxs = append(ethRPCTxs, hash) continue } @@ -409,7 +377,7 @@ func (b *Backend) RPCBlockFromTendermintBlock( index := uint64(txIndex) //#nosec G701 -- checked for int overflow already rpcTx, err := rpc.NewRPCTxFromEthTx( tx, - common.BytesToHash(block.Hash()), + gethcommon.BytesToHash(block.Hash()), height, index, baseFee, @@ -443,7 +411,7 @@ func (b *Backend) RPCBlockFromTendermintBlock( "error", err.Error(), ) // use zero address as the validator operator address - validatorAccAddr = sdk.AccAddress(common.Address{}.Bytes()) + validatorAccAddr = sdk.AccAddress(gethcommon.Address{}.Bytes()) } else { validatorAccAddr, err = sdk.AccAddressFromBech32(res.AccountAddress) if err != nil { @@ -451,7 +419,7 @@ func (b *Backend) RPCBlockFromTendermintBlock( } } - validatorAddr := common.BytesToAddress(validatorAccAddr) + validatorAddr := gethcommon.BytesToAddress(validatorAccAddr) gasLimit, err := rpc.BlockMaxGasFromConsensusParams(ctx, b.clientCtx, block.Height) if err != nil { diff --git a/eth/rpc/backend/blocks_test.go b/eth/rpc/backend/blocks_test.go index 8d04c814f..4bb5d6511 100644 --- a/eth/rpc/backend/blocks_test.go +++ b/eth/rpc/backend/blocks_test.go @@ -1,9 +1,6 @@ package backend_test import ( - "context" - "math/big" - "github.com/ethereum/go-ethereum/common/hexutil" "github.com/NibiruChain/nibiru/v2/eth/rpc" @@ -17,23 +14,96 @@ func (s *BackendSuite) TestBlockNumber() { s.Greater(blockHeightU64, uint64(1)) latestHeight, _ := s.network.LatestHeight() - wantFullTx := true - resp, err := s.backend.GetBlockByNumber( - rpc.NewBlockNumber(big.NewInt(latestHeight)), - wantFullTx, - ) + resp, err := s.backend.BlockNumber() s.Require().NoError(err, resp) + s.Require().Equal(uint64(latestHeight), uint64(blockHeight)) +} + +func (s *BackendSuite) TestGetBlockByNumberr() { + block, err := s.backend.GetBlockByNumber(transferTxBlockNumber, true) + s.Require().NoError(err) + s.Require().NotNil(block) + s.Require().Greater(len(block["transactions"].([]interface{})), 0) + s.Require().NotNil(block["size"]) + s.Require().NotNil(block["nonce"]) + s.Require().Equal(int64(block["number"].(hexutil.Uint64)), transferTxBlockNumber.Int64()) +} + +func (s *BackendSuite) TestGetBlockByHash() { + blockMap, err := s.backend.GetBlockByHash(transferTxBlockHash, true) + s.Require().NoError(err) + AssertBlockContents(s, blockMap) +} + +func (s *BackendSuite) TestBlockNumberFromTendermint() { + testCases := []struct { + name string + blockNrOrHash rpc.BlockNumberOrHash + wantBlockNumber rpc.BlockNumber + wantErr string + }{ + { + name: "happy: block number specified", + blockNrOrHash: rpc.BlockNumberOrHash{ + BlockNumber: &transferTxBlockNumber, + }, + wantBlockNumber: transferTxBlockNumber, + wantErr: "", + }, + { + name: "happy: block hash specified", + blockNrOrHash: rpc.BlockNumberOrHash{ + BlockHash: &transferTxBlockHash, + }, + wantBlockNumber: transferTxBlockNumber, + wantErr: "", + }, + { + name: "sad: neither block number nor hash specified", + blockNrOrHash: rpc.BlockNumberOrHash{}, + wantBlockNumber: 0, + wantErr: "BlockHash and BlockNumber cannot be both nil", + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + blockNumber, err := s.backend.BlockNumberFromTendermint(tc.blockNrOrHash) + + if tc.wantErr != "" { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().Equal(tc.wantBlockNumber, blockNumber) + }) + } +} + +func (s *BackendSuite) TestEthBlockByNumber() { + block, err := s.backend.EthBlockByNumber(transferTxBlockNumber) + s.Require().NoError(err) + s.Require().NotNil(block) + s.Require().Equal(transferTxBlockNumber.Int64(), block.Number().Int64()) + s.Require().Greater(block.Transactions().Len(), 0) + s.Require().NotNil(block.ParentHash()) + s.Require().NotNil(block.UncleHash()) +} + +func (s *BackendSuite) TestGetBlockTransactionCountByHash() { + txCount := s.backend.GetBlockTransactionCountByHash(transferTxBlockHash) + s.Require().Greater((uint64)(*txCount), uint64(0)) +} + +func (s *BackendSuite) TestGetBlockTransactionCountByNumber() { + txCount := s.backend.GetBlockTransactionCountByNumber(transferTxBlockNumber) + s.Require().Greater((uint64)(*txCount), uint64(0)) +} - // TODO: test backend.GetBlockByHash - // s.backend.GetBlockByHash() - block, err := s.node.RPCClient.Block( - context.Background(), - &latestHeight, - ) - s.NoError(err, block) - blockResults, err := s.node.RPCClient.BlockResults( - context.Background(), - &latestHeight, - ) - s.NoError(err, blockResults) +func AssertBlockContents(s *BackendSuite, blockMap map[string]interface{}) { + s.Require().NotNil(blockMap) + s.Require().Greater(len(blockMap["transactions"].([]interface{})), 0) + s.Require().NotNil(blockMap["size"]) + s.Require().NotNil(blockMap["nonce"]) + s.Require().Equal(int64(blockMap["number"].(hexutil.Uint64)), transferTxBlockNumber.Int64()) } diff --git a/eth/rpc/backend/call_tx.go b/eth/rpc/backend/call_tx.go index 12f7ce378..4b64997e2 100644 --- a/eth/rpc/backend/call_tx.go +++ b/eth/rpc/backend/call_tx.go @@ -19,88 +19,10 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "github.com/NibiruChain/nibiru/v2/eth" "github.com/NibiruChain/nibiru/v2/eth/rpc" "github.com/NibiruChain/nibiru/v2/x/evm" ) -// Resend accepts an existing transaction and a new gas price and limit. It will remove -// the given transaction from the pool and reinsert it with the new gas price and limit. -func (b *Backend) Resend(args evm.JsonTxArgs, gasPrice *hexutil.Big, gasLimit *hexutil.Uint64) (common.Hash, error) { - if args.Nonce == nil { - return common.Hash{}, fmt.Errorf("missing transaction nonce in transaction spec") - } - - args, err := b.SetTxDefaults(args) - if err != nil { - return common.Hash{}, err - } - - // The signer used should always be the 'latest' known one because we expect - // signers to be backwards-compatible with old transactions. - eip155ChainID, err := eth.ParseEthChainID(b.clientCtx.ChainID) - if err != nil { - return common.Hash{}, err - } - - cfg := b.ChainConfig() - if cfg == nil { - cfg = evm.EthereumConfig(eip155ChainID) - } - - signer := gethcore.LatestSigner(cfg) - - matchTx := args.ToMsgEthTx().AsTransaction() - - // Before replacing the old transaction, ensure the _new_ transaction fee is reasonable. - price := matchTx.GasPrice() - if gasPrice != nil { - price = gasPrice.ToInt() - } - gas := matchTx.Gas() - if gasLimit != nil { - gas = uint64(*gasLimit) - } - if err := rpc.CheckTxFee(price, gas, b.RPCTxFeeCap()); err != nil { - return common.Hash{}, err - } - - pending, err := b.PendingTransactions() - if err != nil { - return common.Hash{}, err - } - - for _, tx := range pending { - p, err := evm.UnwrapEthereumMsg(tx, common.Hash{}) - if err != nil { - // not valid ethereum tx - continue - } - - pTx := p.AsTransaction() - - wantSigHash := signer.Hash(matchTx) - pFrom, err := gethcore.Sender(signer, pTx) - if err != nil { - continue - } - - if pFrom == *args.From && signer.Hash(pTx) == wantSigHash { - // Match. Re-sign and send the transaction. - if gasPrice != nil && (*big.Int)(gasPrice).Sign() != 0 { - args.GasPrice = gasPrice - } - if gasLimit != nil && *gasLimit != 0 { - args.Gas = gasLimit - } - - return b.SendTransaction(args) // TODO: this calls SetTxDefaults again, refactor to avoid calling it twice - } - } - - return common.Hash{}, fmt.Errorf("transaction %#x not found", matchTx.Hash()) -} - // SendRawTransaction send a raw Ethereum transaction. func (b *Backend) SendRawTransaction(data hexutil.Bytes) (common.Hash, error) { // RLP decode raw transaction bytes @@ -111,7 +33,7 @@ func (b *Backend) SendRawTransaction(data hexutil.Bytes) (common.Hash, error) { } // check the local node config in case unprotected txs are disabled - if !b.UnprotectedAllowed() && !tx.Protected() { + if !b.allowUnprotectedTxs && !tx.Protected() { // Ensure only eip155 signed transactions are submitted if EIP155Required is set. return common.Hash{}, errors.New("only replay-protected (EIP-155) transactions allowed over RPC") } @@ -233,9 +155,9 @@ func (b *Backend) SetTxDefaults(args evm.JsonTxArgs) (evm.JsonTxArgs, error) { if args.Value == nil { args.Value = new(hexutil.Big) } - if args.Nonce == nil { + if args.Nonce == nil && args.From != nil { // get the nonce from the account retriever - // ignore error in case tge account doesn't exist yet + // ignore error in case the account doesn't exist yet nonce, _ := b.getAccountNonce(*args.From, true, 0, b.logger) // #nosec G703s args.Nonce = (*hexutil.Uint64)(&nonce) } @@ -280,7 +202,7 @@ func (b *Backend) SetTxDefaults(args evm.JsonTxArgs) (evm.JsonTxArgs, error) { Nonce: args.Nonce, } - blockNr := rpc.NewBlockNumber(big.NewInt(0)) + blockNr := rpc.EthPendingBlockNumber estimated, err := b.EstimateGas(callArgs, &blockNr) if err != nil { return args, err @@ -416,7 +338,7 @@ func (b *Backend) GasPrice() (*hexutil.Big, error) { if err != nil { return nil, err } - minGasPriceInt := minGasPrice.TruncateInt().BigInt() + minGasPriceInt := minGasPrice if result.Cmp(minGasPriceInt) < 0 { result = minGasPriceInt } diff --git a/eth/rpc/backend/call_tx_test.go b/eth/rpc/backend/call_tx_test.go index f0dcea536..3267d8fd9 100644 --- a/eth/rpc/backend/call_tx_test.go +++ b/eth/rpc/backend/call_tx_test.go @@ -1 +1,90 @@ package backend_test + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common/hexutil" + + "github.com/NibiruChain/nibiru/v2/app/appconst" + "github.com/NibiruChain/nibiru/v2/eth/rpc" + "github.com/NibiruChain/nibiru/v2/x/evm" +) + +func (s *BackendSuite) TestSetTxDefaults() { + testCases := []struct { + name string + jsonTxArgs evm.JsonTxArgs + wantErr string + }{ + { + name: "happy: minimal args set", + jsonTxArgs: evm.JsonTxArgs{ + From: &s.fundedAccEthAddr, + To: &recipient, + Value: (*hexutil.Big)(evm.NativeToWei(big.NewInt(1))), + }, + wantErr: "", + }, + { + name: "happy: gas price set", + jsonTxArgs: evm.JsonTxArgs{ + From: &s.fundedAccEthAddr, + To: &recipient, + GasPrice: (*hexutil.Big)(evm.NativeToWei(big.NewInt(1))), + Value: (*hexutil.Big)(evm.NativeToWei(big.NewInt(1))), + }, + wantErr: "", + }, + { + name: "sad: no to (contract creation) and no data", + jsonTxArgs: evm.JsonTxArgs{ + From: &s.fundedAccEthAddr, + }, + wantErr: "contract creation without any data provided", + }, + { + name: "sad: transfer without from specified generates new empty account", + jsonTxArgs: evm.JsonTxArgs{ + To: &recipient, + Value: (*hexutil.Big)(evm.NativeToWei(big.NewInt(1))), + }, + wantErr: "insufficient balance", + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + jsonTxArgs, err := s.backend.SetTxDefaults(tc.jsonTxArgs) + + if tc.wantErr != "" { + s.Require().Error(err) + return + } + s.Require().NoError(err) + s.Require().NotNil(jsonTxArgs.Nonce) + s.Require().NotNil(jsonTxArgs.Gas) + s.Require().Greater(*jsonTxArgs.Nonce, hexutil.Uint64(0)) + s.Require().Greater(*jsonTxArgs.Gas, hexutil.Uint64(0)) + s.Require().Equal(jsonTxArgs.ChainID.ToInt().Int64(), appconst.ETH_CHAIN_ID_DEFAULT) + }) + } +} + +func (s *BackendSuite) TestDoCall() { + jsonTxArgs := evm.JsonTxArgs{ + From: &s.fundedAccEthAddr, + To: &recipient, + Value: (*hexutil.Big)(evm.NativeToWei(big.NewInt(1))), + } + txResponse, err := s.backend.DoCall(jsonTxArgs, rpc.EthPendingBlockNumber) + s.Require().NoError(err) + s.Require().NotNil(txResponse) + s.Require().Greater(txResponse.GasUsed, uint64(0)) +} + +func (s *BackendSuite) TestGasPrice() { + gasPrice, err := s.backend.GasPrice() + s.Require().NoError(err) + s.Require().NotNil(gasPrice) + s.Require().Greater(gasPrice.ToInt().Int64(), int64(0)) +} diff --git a/eth/rpc/backend/chain_info.go b/eth/rpc/backend/chain_info.go index 6b478a577..efeededb8 100644 --- a/eth/rpc/backend/chain_info.go +++ b/eth/rpc/backend/chain_info.go @@ -5,7 +5,6 @@ import ( "fmt" "math/big" - sdkmath "cosmossdk.io/math" tmrpcclient "github.com/cometbft/cometbft/rpc/client" tmrpctypes "github.com/cometbft/cometbft/rpc/core/types" sdk "github.com/cosmos/cosmos-sdk/types" @@ -21,23 +20,8 @@ import ( ) // ChainID is the EIP-155 replay-protection chain id for the current ethereum chain config. -func (b *Backend) ChainID() (*hexutil.Big, error) { - eip155ChainID, err := eth.ParseEthChainID(b.clientCtx.ChainID) - if err != nil { - panic(err) - } - // if current block is at or past the EIP-155 replay-protection fork block, return chainID from config - bn, err := b.BlockNumber() - if err != nil { - b.logger.Debug("failed to fetch latest block number", "error", err.Error()) - return (*hexutil.Big)(eip155ChainID), nil - } - - if config := b.ChainConfig(); config.IsEIP155(new(big.Int).SetUint64(uint64(bn))) { - return (*hexutil.Big)(config.ChainID), nil - } - - return nil, fmt.Errorf("chain not synced beyond EIP-155 replay-protection fork block") +func (b *Backend) ChainID() *hexutil.Big { + return (*hexutil.Big)(eth.ParseEthChainID(b.clientCtx.ChainID)) } // ChainConfig returns the latest ethereum chain configuration @@ -46,7 +30,6 @@ func (b *Backend) ChainConfig() *params.ChainConfig { if err != nil { return nil } - return evm.EthereumConfig(b.chainID) } @@ -100,7 +83,7 @@ func (b *Backend) PendingTransactions() ([]*sdk.Tx, error) { // FeeHistory returns data relevant for fee estimation based on the specified range of blocks. func (b *Backend) FeeHistory( userBlockCount gethrpc.DecimalOrHex, // number blocks to fetch, maximum is 100 - lastBlock gethrpc.BlockNumber, // the block to start search , to oldest + lastBlock gethrpc.BlockNumber, // the block to start search, to oldest rewardPercentiles []float64, // percentiles to fetch reward ) (*rpc.FeeHistoryResult, error) { blockEnd := int64(lastBlock) //#nosec G701 -- checked for int overflow already @@ -162,7 +145,7 @@ func (b *Backend) FeeHistory( } oneFeeHistory := rpc.OneFeeHistory{} - err = b.processBlock(tendermintblock, ðBlock, rewardPercentiles, tendermintBlockResult, &oneFeeHistory) + err = b.retrieveEVMTxFeesFromBlock(tendermintblock, ðBlock, rewardPercentiles, tendermintBlockResult, &oneFeeHistory) if err != nil { return nil, err } @@ -194,18 +177,16 @@ func (b *Backend) FeeHistory( return &feeHistory, nil } -// SuggestGasTipCap: Not yet supported. Returns 0 as the suggested tip cap. After -// implementing tx prioritization, this function can come to life. +// SuggestGasTipCap Not yet supported. Returns 0 as the suggested tip cap. +// After implementing tx prioritization, this function can come to life. func (b *Backend) SuggestGasTipCap(baseFee *big.Int) (*big.Int, error) { maxDelta := big.NewInt(0) return maxDelta, nil } -func DefaultMinGasPrice() sdkmath.LegacyDec { return sdkmath.LegacyZeroDec() } - -// GlobalMinGasPrice returns the minimum gas price for all nodes. This is -// distinct from the individual configuration set by the validator set. -func (b *Backend) GlobalMinGasPrice() (sdkmath.LegacyDec, error) { +// GlobalMinGasPrice returns the minimum gas price for all nodes. +// This is distinct from the individual configuration set by the validator set. +func (b *Backend) GlobalMinGasPrice() (*big.Int, error) { // TODO: feat(eth): dynamic fees - return DefaultMinGasPrice(), nil + return big.NewInt(0), nil } diff --git a/eth/rpc/backend/chain_info_test.go b/eth/rpc/backend/chain_info_test.go index f0dcea536..923764888 100644 --- a/eth/rpc/backend/chain_info_test.go +++ b/eth/rpc/backend/chain_info_test.go @@ -1 +1,93 @@ package backend_test + +import ( + "math/big" + + gethrpc "github.com/ethereum/go-ethereum/rpc" + + "github.com/NibiruChain/nibiru/v2/app/appconst" + "github.com/NibiruChain/nibiru/v2/x/evm" + "github.com/NibiruChain/nibiru/v2/x/evm/evmtest" +) + +func (s *BackendSuite) TestChainID() { + s.Require().Equal(appconst.ETH_CHAIN_ID_DEFAULT, s.backend.ChainID().ToInt().Int64()) +} + +func (s *BackendSuite) TestChainConfig() { + config := s.backend.ChainConfig() + s.Require().Equal(appconst.ETH_CHAIN_ID_DEFAULT, config.ChainID.Int64()) + s.Require().Equal(int64(0), config.LondonBlock.Int64()) +} + +func (s *BackendSuite) TestBaseFee() { + resBlock, err := s.backend.TendermintBlockResultByNumber(transferTxBlockNumber.TmHeight()) + s.Require().NoError(err) + baseFee, err := s.backend.BaseFee(resBlock) + s.Require().NoError(err) + s.Require().Equal(evm.BASE_FEE_MICRONIBI, baseFee) +} + +func (s *BackendSuite) TestCurrentHeader() { + currentHeader, err := s.backend.CurrentHeader() + s.Require().NoError(err) + s.Require().NotNil(currentHeader) + s.Require().GreaterOrEqual(currentHeader.Number.Int64(), transferTxBlockNumber.Int64()) +} + +func (s *BackendSuite) TestPendingTransactions() { + // Create pending tx: don't wait for next block + randomEthAddr := evmtest.NewEthPrivAcc().EthAddr + txHash := s.SendNibiViaEthTransfer(randomEthAddr, big.NewInt(123), false) + txs, err := s.backend.PendingTransactions() + s.Require().NoError(err) + s.Require().NotNil(txs) + s.Require().NotNil(txHash) + s.Require().Greater(len(txs), 0) + txFound := false + for _, tx := range txs { + msg, err := evm.UnwrapEthereumMsg(tx, txHash) + if err != nil { + // not ethereum tx + continue + } + if msg.Hash == txHash.String() { + txFound = true + } + } + s.Require().True(txFound, "pending tx not found") +} + +func (s *BackendSuite) TestFeeHistory() { + currentBlock, err := s.backend.BlockNumber() + s.Require().NoError(err) + blockCount := 2 // blocks to search backwards from the current block + percentiles := []float64{50, 100} + + res, err := s.backend.FeeHistory( + (gethrpc.DecimalOrHex)(blockCount), + gethrpc.BlockNumber(int64(currentBlock)), + percentiles, + ) + s.Require().NoError(err) + s.Require().NotNil(res) + s.Require().Len(res.Reward, blockCount) + s.Require().Len(res.BaseFee, blockCount+1) + s.Require().Len(res.GasUsedRatio, len(percentiles)) + + for _, gasUsed := range res.GasUsedRatio { + s.Require().LessOrEqual(gasUsed, float64(1)) + } +} + +func (s *BackendSuite) TestSuggestGasTipCap() { + tipCap, err := s.backend.SuggestGasTipCap(big.NewInt(1)) + s.Require().NoError(err) + s.Require().Equal(big.NewInt(0), tipCap) +} + +func (s *BackendSuite) TestGlobalMinGasPrice() { + gasPrice, err := s.backend.GlobalMinGasPrice() + s.Require().NoError(err) + s.Require().Equal(big.NewInt(0), gasPrice) +} diff --git a/eth/rpc/backend/evm_query_client_test.go b/eth/rpc/backend/evm_query_client_test.go deleted file mode 100644 index f0dcea536..000000000 --- a/eth/rpc/backend/evm_query_client_test.go +++ /dev/null @@ -1 +0,0 @@ -package backend_test diff --git a/eth/rpc/backend/filters.go b/eth/rpc/backend/filters.go deleted file mode 100644 index 02fa6c11f..000000000 --- a/eth/rpc/backend/filters.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2023-2024 Nibi, Inc. -package backend - -import ( - "github.com/ethereum/go-ethereum/common" - gethcore "github.com/ethereum/go-ethereum/core/types" - "github.com/pkg/errors" -) - -// GetLogs returns all the logs from all the ethereum transactions in a block. -func (b *Backend) GetLogs(hash common.Hash) ([][]*gethcore.Log, error) { - resBlock, err := b.TendermintBlockByHash(hash) - if err != nil { - return nil, err - } - if resBlock == nil { - return nil, errors.Errorf("block not found for hash %s", hash) - } - return b.GetLogsByHeight(&resBlock.Block.Header.Height) -} - -// GetLogsByHeight returns all the logs from all the ethereum transactions in a block. -func (b *Backend) GetLogsByHeight(height *int64) ([][]*gethcore.Log, error) { - // NOTE: we query the state in case the tx result logs are not persisted after an upgrade. - blockRes, err := b.TendermintBlockResultByNumber(height) - if err != nil { - return nil, err - } - - return GetLogsFromBlockResults(blockRes) -} - -// BloomStatus returns: -// - bloomBitsBlocks: The number of blocks a single bloom bit section vector -// contains on the server side. -// - bloomSections: The number of processed sections maintained by the indexer. -func (b *Backend) BloomStatus() ( - bloomBitBlocks, bloomSections uint64, -) { - return 4096, 0 -} diff --git a/eth/rpc/backend/filters_test.go b/eth/rpc/backend/filters_test.go deleted file mode 100644 index f0dcea536..000000000 --- a/eth/rpc/backend/filters_test.go +++ /dev/null @@ -1 +0,0 @@ -package backend_test diff --git a/eth/rpc/backend/node_info.go b/eth/rpc/backend/node_info.go index 93f3cc32b..66d56d76c 100644 --- a/eth/rpc/backend/node_info.go +++ b/eth/rpc/backend/node_info.go @@ -4,7 +4,7 @@ package backend import ( "time" - "github.com/ethereum/go-ethereum/common" + gethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/NibiruChain/nibiru/v2/eth" @@ -12,8 +12,8 @@ import ( ) // Accounts returns the list of accounts available to this node. -func (b *Backend) Accounts() ([]common.Address, error) { - addresses := make([]common.Address, 0) // return [] instead of nil if empty +func (b *Backend) Accounts() ([]gethcommon.Address, error) { + addresses := make([]gethcommon.Address, 0) // return [] instead of nil if empty infos, err := b.clientCtx.Keyring.List() if err != nil { @@ -26,14 +26,14 @@ func (b *Backend) Accounts() ([]common.Address, error) { return nil, err } addressBytes := pubKey.Address().Bytes() - addresses = append(addresses, common.BytesToAddress(addressBytes)) + addresses = append(addresses, gethcommon.BytesToAddress(addressBytes)) } return addresses, nil } // Syncing returns false in case the node is currently not syncing with the network. It can be up to date or has not -// yet received the latest block headers from its pears. In case it is synchronizing: +// yet received the latest block headers from its peers. In case it is synchronizing: // - startingBlock: block number this node started to synchronize from // - currentBlock: block number this node is currently importing // - highestBlock: block number of the highest block header this node has received from peers @@ -58,12 +58,6 @@ func (b *Backend) Syncing() (interface{}, error) { }, nil } -// UnprotectedAllowed returns the node configuration value for allowing -// unprotected transactions (i.e not replay-protected) -func (b Backend) UnprotectedAllowed() bool { - return b.allowUnprotectedTxs -} - // RPCGasCap is the global gas cap for eth-call variants. func (b *Backend) RPCGasCap() uint64 { return b.cfg.JSONRPC.GasCap @@ -74,21 +68,11 @@ func (b *Backend) RPCEVMTimeout() time.Duration { return b.cfg.JSONRPC.EVMTimeout } -// RPCGasCap is the global gas cap for eth-call variants. -func (b *Backend) RPCTxFeeCap() float64 { - return b.cfg.JSONRPC.TxFeeCap -} - // RPCFilterCap is the limit for total number of filters that can be created func (b *Backend) RPCFilterCap() int32 { return b.cfg.JSONRPC.FilterCap } -// RPCFeeHistoryCap is the limit for total number of blocks that can be fetched -func (b *Backend) RPCFeeHistoryCap() int32 { - return b.cfg.JSONRPC.FeeHistoryCap -} - // RPCLogsCap defines the max number of results can be returned from single `eth_getLogs` query. func (b *Backend) RPCLogsCap() int32 { return b.cfg.JSONRPC.LogsCap @@ -101,7 +85,6 @@ func (b *Backend) RPCBlockRangeCap() int32 { // RPCMinGasPrice returns the minimum gas price for a transaction obtained from // the node config. If set value is 0, it will default to 20. - func (b *Backend) RPCMinGasPrice() int64 { evmParams, err := b.queryClient.Params(b.ctx, &evm.QueryParamsRequest{}) if err != nil { diff --git a/eth/rpc/backend/node_info_test.go b/eth/rpc/backend/node_info_test.go index f0dcea536..a41d2f66d 100644 --- a/eth/rpc/backend/node_info_test.go +++ b/eth/rpc/backend/node_info_test.go @@ -1 +1,45 @@ package backend_test + +import ( + gethcommon "github.com/ethereum/go-ethereum/common" + + "github.com/NibiruChain/nibiru/v2/app/server/config" + "github.com/NibiruChain/nibiru/v2/eth" +) + +func (s *BackendSuite) TestAccounts() { + accounts, err := s.backend.Accounts() + s.Require().NoError(err) + s.Require().Greater(len(accounts), 0) + s.Require().Contains(accounts, gethcommon.BytesToAddress(s.node.ValAddress.Bytes())) +} + +func (s *BackendSuite) TestSyncing() { + syncing, err := s.backend.Syncing() + s.Require().NoError(err) + s.Require().False(syncing.(bool)) +} + +func (s *BackendSuite) TestRPCGasCap() { + s.Require().Equal(config.DefaultConfig().JSONRPC.GasCap, s.backend.RPCGasCap()) +} + +func (s *BackendSuite) TestRPCEVMTimeout() { + s.Require().Equal(config.DefaultConfig().JSONRPC.EVMTimeout, s.backend.RPCEVMTimeout()) +} + +func (s *BackendSuite) TestRPCFilterCap() { + s.Require().Equal(config.DefaultConfig().JSONRPC.FilterCap, s.backend.RPCFilterCap()) +} + +func (s *BackendSuite) TestRPCLogsCap() { + s.Require().Equal(config.DefaultConfig().JSONRPC.LogsCap, s.backend.RPCLogsCap()) +} + +func (s *BackendSuite) TestRPCBlockRangeCap() { + s.Require().Equal(config.DefaultConfig().JSONRPC.BlockRangeCap, s.backend.RPCBlockRangeCap()) +} + +func (s *BackendSuite) TestRPCMinGasPrice() { + s.Require().Equal(int64(eth.DefaultGasPrice), s.backend.RPCMinGasPrice()) +} diff --git a/eth/rpc/backend/sign_tx.go b/eth/rpc/backend/sign_tx.go deleted file mode 100644 index 7d3032ea3..000000000 --- a/eth/rpc/backend/sign_tx.go +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright (c) 2023-2024 Nibi, Inc. -package backend - -import ( - "errors" - "fmt" - "math/big" - - errorsmod "cosmossdk.io/errors" - "github.com/cosmos/cosmos-sdk/client/flags" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/ethereum/go-ethereum/accounts/keystore" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - gethcore "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/signer/core/apitypes" - - "github.com/NibiruChain/nibiru/v2/x/evm" -) - -// SendTransaction sends transaction based on received args using Node's key to sign it -func (b *Backend) SendTransaction(args evm.JsonTxArgs) (common.Hash, error) { - // Look up the wallet containing the requested signer - _, err := b.clientCtx.Keyring.KeyByAddress(sdk.AccAddress(args.GetFrom().Bytes())) - if err != nil { - b.logger.Error("failed to find key in keyring", "address", args.GetFrom(), "error", err.Error()) - return common.Hash{}, fmt.Errorf("failed to find key in the node's keyring; %s; %s", keystore.ErrNoMatch, err.Error()) - } - - if args.ChainID != nil && (b.chainID).Cmp((*big.Int)(args.ChainID)) != 0 { - return common.Hash{}, fmt.Errorf("chainId does not match node's (have=%v, want=%v)", args.ChainID, (*hexutil.Big)(b.chainID)) - } - - args, err = b.SetTxDefaults(args) - if err != nil { - return common.Hash{}, err - } - - bn, err := b.BlockNumber() - if err != nil { - b.logger.Debug("failed to fetch latest block number", "error", err.Error()) - return common.Hash{}, err - } - - signer := gethcore.MakeSigner(b.ChainConfig(), new(big.Int).SetUint64(uint64(bn))) - - // LegacyTx derives chainID from the signature. To make sure the msg.ValidateBasic makes - // the corresponding chainID validation, we need to sign the transaction before calling it - - // Sign transaction - msg := args.ToMsgEthTx() - if err := msg.Sign(signer, b.clientCtx.Keyring); err != nil { - b.logger.Debug("failed to sign tx", "error", err.Error()) - return common.Hash{}, err - } - - if err := msg.ValidateBasic(); err != nil { - b.logger.Debug("tx failed basic validation", "error", err.Error()) - return common.Hash{}, err - } - - // Query params to use the EVM denomination - res, err := b.queryClient.QueryClient.Params(b.ctx, &evm.QueryParamsRequest{}) - if err != nil { - b.logger.Error("failed to query evm params", "error", err.Error()) - return common.Hash{}, err - } - - // Assemble transaction from fields - tx, err := msg.BuildTx(b.clientCtx.TxConfig.NewTxBuilder(), res.Params.EvmDenom) - if err != nil { - b.logger.Error("build cosmos tx failed", "error", err.Error()) - return common.Hash{}, err - } - - // Encode transaction by default Tx encoder - txEncoder := b.clientCtx.TxConfig.TxEncoder() - txBytes, err := txEncoder(tx) - if err != nil { - b.logger.Error("failed to encode eth tx using default encoder", "error", err.Error()) - return common.Hash{}, err - } - - ethTx := msg.AsTransaction() - - // check the local node config in case unprotected txs are disabled - if !b.UnprotectedAllowed() && !ethTx.Protected() { - // Ensure only eip155 signed transactions are submitted if EIP155Required is set. - return common.Hash{}, errors.New("only replay-protected (EIP-155) transactions allowed over RPC") - } - - txHash := ethTx.Hash() - - // Broadcast transaction in sync mode (default) - // NOTE: If error is encountered on the node, the broadcast will not return an error - syncCtx := b.clientCtx.WithBroadcastMode(flags.BroadcastSync) - rsp, err := syncCtx.BroadcastTx(txBytes) - if rsp != nil && rsp.Code != 0 { - err = errorsmod.ABCIError(rsp.Codespace, rsp.Code, rsp.RawLog) - } - if err != nil { - b.logger.Error("failed to broadcast tx", "error", err.Error()) - return txHash, err - } - - // Return transaction hash - return txHash, nil -} - -// Sign signs the provided data using the private key of address via Geth's signature standard. -func (b *Backend) Sign(address common.Address, data hexutil.Bytes) (hexutil.Bytes, error) { - from := sdk.AccAddress(address.Bytes()) - - _, err := b.clientCtx.Keyring.KeyByAddress(from) - if err != nil { - b.logger.Error("failed to find key in keyring", "address", address.String()) - return nil, fmt.Errorf("%s; %s", keystore.ErrNoMatch, err.Error()) - } - - // Sign the requested hash with the wallet - signature, _, err := b.clientCtx.Keyring.SignByAddress(from, data) - if err != nil { - b.logger.Error("keyring.SignByAddress failed", "address", address.Hex()) - return nil, err - } - - signature[crypto.RecoveryIDOffset] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper - return signature, nil -} - -// SignTypedData signs EIP-712 conformant typed data -func (b *Backend) SignTypedData(address common.Address, typedData apitypes.TypedData) (hexutil.Bytes, error) { - from := sdk.AccAddress(address.Bytes()) - - _, err := b.clientCtx.Keyring.KeyByAddress(from) - if err != nil { - b.logger.Error("failed to find key in keyring", "address", address.String()) - return nil, fmt.Errorf("%s; %s", keystore.ErrNoMatch, err.Error()) - } - - sigHash, _, err := apitypes.TypedDataAndHash(typedData) - if err != nil { - return nil, err - } - - // Sign the requested hash with the wallet - signature, _, err := b.clientCtx.Keyring.SignByAddress(from, sigHash) - if err != nil { - b.logger.Error("keyring.SignByAddress failed", "address", address.Hex()) - return nil, err - } - - signature[crypto.RecoveryIDOffset] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper - return signature, nil -} diff --git a/eth/rpc/backend/sign_tx_test.go b/eth/rpc/backend/sign_tx_test.go deleted file mode 100644 index f0dcea536..000000000 --- a/eth/rpc/backend/sign_tx_test.go +++ /dev/null @@ -1 +0,0 @@ -package backend_test diff --git a/eth/rpc/backend/tracing.go b/eth/rpc/backend/tracing.go index 425e3d00e..4a20557cd 100644 --- a/eth/rpc/backend/tracing.go +++ b/eth/rpc/backend/tracing.go @@ -9,7 +9,7 @@ import ( tmrpcclient "github.com/cometbft/cometbft/rpc/client" tmrpctypes "github.com/cometbft/cometbft/rpc/core/types" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/ethereum/go-ethereum/common" + gethcommon "github.com/ethereum/go-ethereum/common" "github.com/pkg/errors" "github.com/NibiruChain/nibiru/v2/eth/rpc" @@ -18,7 +18,7 @@ import ( // TraceTransaction returns the structured logs created during the execution of EVM // and returns them as a JSON object. -func (b *Backend) TraceTransaction(hash common.Hash, config *evm.TraceConfig) (interface{}, error) { +func (b *Backend) TraceTransaction(hash gethcommon.Hash, config *evm.TraceConfig) (interface{}, error) { // Get transaction by hash transaction, err := b.GetTxByEthHash(hash) if err != nil { @@ -101,7 +101,7 @@ func (b *Backend) TraceTransaction(hash common.Hash, config *evm.TraceConfig) (i Predecessors: predecessors, BlockNumber: blk.Block.Height, BlockTime: blk.Block.Time, - BlockHash: common.Bytes2Hex(blk.BlockID.Hash), + BlockHash: gethcommon.Bytes2Hex(blk.BlockID.Hash), ProposerAddress: sdk.ConsAddress(blk.Block.ProposerAddress), ChainId: b.chainID.Int64(), BlockMaxGas: cp.ConsensusParams.Block.MaxGas, @@ -191,7 +191,7 @@ func (b *Backend) TraceBlock(height rpc.BlockNumber, TraceConfig: config, BlockNumber: block.Block.Height, BlockTime: block.Block.Time, - BlockHash: common.Bytes2Hex(block.BlockID.Hash), + BlockHash: gethcommon.Bytes2Hex(block.BlockID.Hash), ProposerAddress: sdk.ConsAddress(block.Block.ProposerAddress), ChainId: b.chainID.Int64(), BlockMaxGas: cp.ConsensusParams.Block.MaxGas, @@ -240,7 +240,7 @@ func (b *Backend) TraceCall( Predecessors: nil, BlockNumber: blk.Block.Height, BlockTime: blk.Block.Time, - BlockHash: common.Bytes2Hex(blk.BlockID.Hash), + BlockHash: gethcommon.Bytes2Hex(blk.BlockID.Hash), ProposerAddress: sdk.ConsAddress(blk.Block.ProposerAddress), ChainId: b.chainID.Int64(), BlockMaxGas: cp.ConsensusParams.Block.MaxGas, diff --git a/eth/rpc/backend/tx_info.go b/eth/rpc/backend/tx_info.go index 9783ca747..c4c7f0de7 100644 --- a/eth/rpc/backend/tx_info.go +++ b/eth/rpc/backend/tx_info.go @@ -127,17 +127,22 @@ func (b *Backend) getTransactionByHashPending(txHash gethcommon.Hash) (*rpc.EthT return nil, nil } -// GetGasUsed returns gasUsed from transaction, patching to -// price * gas in the event the tx is reverted. -func (b *Backend) GetGasUsed(res *eth.TxResult, price *big.Int, gas uint64) uint64 { - if res.Failed && res.Height < b.cfg.JSONRPC.FixRevertGasRefundHeight { - return new(big.Int).Mul(price, new(big.Int).SetUint64(gas)).Uint64() - } - return res.GasUsed +// TransactionReceipt represents the results of a transaction. TransactionReceipt +// is an extension of gethcore.Receipt, the response type for the +// "eth_getTransactionReceipt" JSON-RPC method. +// Reason being, the gethcore.Receipt struct has an incorrect JSON struct tag on one +// field and doesn't marshal JSON as expected, so we embed and extend it here. +type TransactionReceipt struct { + gethcore.Receipt + + ContractAddress *gethcommon.Address `json:"contractAddress,omitempty"` + From gethcommon.Address + To *gethcommon.Address + EffectiveGasPrice *big.Int } // GetTransactionReceipt returns the transaction receipt identified by hash. -func (b *Backend) GetTransactionReceipt(hash gethcommon.Hash) (map[string]interface{}, error) { +func (b *Backend) GetTransactionReceipt(hash gethcommon.Hash) (*TransactionReceipt, error) { hexTx := hash.Hex() b.logger.Debug("eth_getTransactionReceipt", "hash", hexTx) @@ -175,17 +180,13 @@ func (b *Backend) GetTransactionReceipt(hash gethcommon.Hash) (map[string]interf } cumulativeGasUsed += res.CumulativeGasUsed - var status hexutil.Uint + var status uint64 = gethcore.ReceiptStatusSuccessful if res.Failed { - status = hexutil.Uint(gethcore.ReceiptStatusFailed) - } else { - status = hexutil.Uint(gethcore.ReceiptStatusSuccessful) - } - chainID, err := b.ChainID() - if err != nil { - return nil, err + status = gethcore.ReceiptStatusFailed } + chainID := b.ChainID() + from, err := ethMsg.GetSender(chainID.ToInt()) if err != nil { return nil, err @@ -213,40 +214,38 @@ func (b *Backend) GetTransactionReceipt(hash gethcommon.Hash) (map[string]interf return nil, errors.New("can't find index of ethereum tx") } - // TODO: refactor(evm-rpc-backend): Replace interface with gethcore.Receipt - // in eth_getTransactionReceipt - receipt := map[string]interface{}{ - // Consensus fields: These fields are defined by the Yellow Paper - "status": status, - "cumulativeGasUsed": hexutil.Uint64(cumulativeGasUsed), - "logsBloom": gethcore.BytesToBloom(gethcore.LogsBloom(logs)), - "logs": logs, + receipt := TransactionReceipt{ + Receipt: gethcore.Receipt{ + Type: ethMsg.AsTransaction().Type(), - // Implementation fields: These fields are added by geth when processing a transaction. - // They are stored in the chain database. - "transactionHash": hash, - "contractAddress": nil, - "gasUsed": hexutil.Uint64(b.GetGasUsed(res, txData.GetGasPrice(), txData.GetGas())), + // Consensus fields: These fields are defined by the Etheruem Yellow Paper + Status: status, + CumulativeGasUsed: cumulativeGasUsed, + Bloom: gethcore.BytesToBloom(gethcore.LogsBloom(logs)), + Logs: logs, - // Inclusion information: These fields provide information about the inclusion of the - // transaction corresponding to this receipt. - "blockHash": gethcommon.BytesToHash(resBlock.Block.Header.Hash()).Hex(), - "blockNumber": hexutil.Uint64(res.Height), - "transactionIndex": hexutil.Uint64(res.EthTxIndex), + // Implementation fields: These fields are added by geth when processing a transaction. + // They are stored in the chain database. + TxHash: hash, + GasUsed: res.GasUsed, - // sender and receiver (contract or EOA) addreses - "from": from, - "to": txData.GetTo(), - "type": hexutil.Uint(ethMsg.AsTransaction().Type()), + BlockHash: gethcommon.BytesToHash(resBlock.Block.Header.Hash()), + BlockNumber: big.NewInt(res.Height), + TransactionIndex: uint(res.EthTxIndex), + }, + ContractAddress: nil, + From: from, + To: txData.GetTo(), } if logs == nil { - receipt["logs"] = [][]*gethcore.Log{} + receipt.Logs = []*gethcore.Log{} } // If the ContractAddress is 20 0x0 bytes, assume it is not a contract creation if txData.GetTo() == nil { - receipt["contractAddress"] = crypto.CreateAddress(from, txData.GetNonce()) + addr := crypto.CreateAddress(from, txData.GetNonce()) + receipt.ContractAddress = &addr } if dynamicTx, ok := txData.(*evm.DynamicFeeTx); ok { @@ -255,11 +254,10 @@ func (b *Backend) GetTransactionReceipt(hash gethcommon.Hash) (map[string]interf // tolerate the error for pruned node. b.logger.Error("fetch basefee failed, node is pruned?", "height", res.Height, "error", err) } else { - receipt["effectiveGasPrice"] = hexutil.Big(*dynamicTx.EffectiveGasPriceWei(baseFee)) + receipt.EffectiveGasPrice = dynamicTx.EffectiveGasPriceWei(baseFee) } } - - return receipt, nil + return &receipt, nil } // GetTransactionByBlockHashAndIndex returns the transaction identified by hash and index. diff --git a/eth/rpc/backend/tx_info_test.go b/eth/rpc/backend/tx_info_test.go index d203b2bc4..3d14d60bd 100644 --- a/eth/rpc/backend/tx_info_test.go +++ b/eth/rpc/backend/tx_info_test.go @@ -1,12 +1,16 @@ package backend_test import ( + "encoding/json" "math/big" gethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + gethcore "github.com/ethereum/go-ethereum/core/types" "github.com/NibiruChain/nibiru/v2/eth/rpc" + "github.com/NibiruChain/nibiru/v2/eth/rpc/backend" + "github.com/NibiruChain/nibiru/v2/x/evm/evmtest" ) func (s *BackendSuite) TestGetTransactionByHash() { @@ -73,13 +77,13 @@ func (s *BackendSuite) TestGetTransactionReceipt() { s.Require().NotNil(receipt) // Check fields - // s.Equal(s.fundedAccEthAddr, receipt.From) - // s.Equal(&recipient, receipt.To) - // s.Greater(receipt.GasUsed, uint64(0)) - // s.Equal(receipt.GasUsed, receipt.CumulativeGasUsed) - // s.Equal(tc.txHash, receipt.TxHash) - // s.Nil(receipt.ContractAddress) - // s.Require().Equal(gethcore.ReceiptStatusSuccessful, receipt.Status) + s.Equal(s.fundedAccEthAddr, receipt.From) + s.Equal(&recipient, receipt.To) + s.Greater(receipt.GasUsed, uint64(0)) + s.Equal(receipt.GasUsed, receipt.CumulativeGasUsed) + s.Equal(tc.txHash, receipt.TxHash) + s.Nil(receipt.ContractAddress) + s.Require().Equal(gethcore.ReceiptStatusSuccessful, receipt.Status) }) } } @@ -177,3 +181,38 @@ func AssertTxResults(s *BackendSuite, tx *rpc.EthTxJsonRPC, expectedTxHash gethc s.Require().Equal(expectedTxHash, tx.Hash) s.Require().Equal(uint64(1), uint64(*tx.TransactionIndex)) } + +func (s *BackendSuite) TestReceiptMarshalJson() { + toAddr := evmtest.NewEthPrivAcc().EthAddr + tr := backend.TransactionReceipt{ + Receipt: gethcore.Receipt{ + Type: 0, + PostState: []byte{}, + Status: 0, + CumulativeGasUsed: 0, + Bloom: [256]byte{}, + Logs: []*gethcore.Log{}, + TxHash: [32]byte{}, + ContractAddress: [20]byte{}, + GasUsed: 0, + BlockHash: [32]byte{}, + BlockNumber: &big.Int{}, + TransactionIndex: 0, + }, + ContractAddress: nil, + From: evmtest.NewEthPrivAcc().EthAddr, + To: &toAddr, + EffectiveGasPrice: big.NewInt(1), + } + + jsonBz, err := json.Marshal(tr) + s.Require().NoError(err) + + gethReceipt := new(gethcore.Receipt) + err = json.Unmarshal(jsonBz, gethReceipt) + s.Require().NoError(err) + + receipt := new(backend.TransactionReceipt) + err = json.Unmarshal(jsonBz, receipt) + s.Require().NoError(err) +} diff --git a/eth/rpc/backend/utils.go b/eth/rpc/backend/utils.go index 499bf107e..7083354c7 100644 --- a/eth/rpc/backend/utils.go +++ b/eth/rpc/backend/utils.go @@ -71,8 +71,8 @@ func (b *Backend) getAccountNonce(accAddr common.Address, pending bool, height i return nonce, nil } - // the account retriever doesn't include the uncommitted transactions on the - // nonce so we need to to manually add them. + // the account retriever doesn't include the uncommitted transactions on the nonce, + // so we need to manually add them. pendingTxs, err := b.PendingTransactions() if err != nil { logger.Error("failed to fetch pending transactions", "error", err.Error()) @@ -102,8 +102,10 @@ func (b *Backend) getAccountNonce(accAddr common.Address, pending bool, height i return nonce, nil } -// output: targetOneFeeHistory -func (b *Backend) processBlock( +// retrieveEVMTxFeesFromBlock goes through evm txs of the block, +// retrieves the gas fees and puts them into an object `targetOneFeeHistory` +// See eth_feeHistory method for more details of the return format. +func (b *Backend) retrieveEVMTxFeesFromBlock( tendermintBlock *tmrpctypes.ResultBlock, ethBlock *map[string]interface{}, rewardPercentiles []float64, @@ -281,13 +283,13 @@ func GetLogsFromBlockResults(blockRes *tmrpctypes.ResultBlockResults) ([][]*geth } // GetHexProofs returns list of hex data of proof op -func GetHexProofs(proof *crypto.ProofOps) []string { - if proof == nil { +func GetHexProofs(proofOps *crypto.ProofOps) []string { + if proofOps == nil { return []string{""} } proofs := []string{} // check for proof - for _, p := range proof.Ops { + for _, p := range proofOps.Ops { proof := "" if len(p.Data) > 0 { proof = hexutil.Encode(p.Data) diff --git a/eth/rpc/backend/utils_test.go b/eth/rpc/backend/utils_test.go index f0dcea536..94fc90fe4 100644 --- a/eth/rpc/backend/utils_test.go +++ b/eth/rpc/backend/utils_test.go @@ -1 +1,68 @@ package backend_test + +import ( + "fmt" + + "github.com/cometbft/cometbft/proto/tendermint/crypto" + + "github.com/NibiruChain/nibiru/v2/eth/rpc/backend" +) + +func (s *BackendSuite) TestGetLogsFromBlockResults() { + blockWithTx := transferTxBlockNumber.Int64() + blockResults, err := s.backend.TendermintBlockResultByNumber(&blockWithTx) + s.Require().NoError(err) + s.Require().NotNil(blockResults) + + logs, err := backend.GetLogsFromBlockResults(blockResults) + s.Require().NoError(err) + s.Require().NotNil(logs) + + // TODO: ON: the structured event eth.evm.v1.EventTxLog is not emitted properly, so the logs are not retrieved + // Add proper checks after implementing +} + +func (s *BackendSuite) TestGetHexProofs() { + defaultRes := []string{""} + testCases := []struct { + name string + proof *crypto.ProofOps + exp []string + }{ + { + "no proof provided", + mockProofs(0, false), + defaultRes, + }, + { + "no proof data provided", + mockProofs(1, false), + defaultRes, + }, + { + "valid proof provided", + mockProofs(1, true), + []string{"0x0a190a034b4559120556414c55451a0b0801180120012a03000202"}, + }, + } + for _, tc := range testCases { + s.Run(fmt.Sprintf("Case %s", tc.name), func() { + s.Require().Equal(tc.exp, backend.GetHexProofs(tc.proof)) + }) + } +} + +func mockProofs(num int, withData bool) *crypto.ProofOps { + var proofOps *crypto.ProofOps + if num > 0 { + proofOps = new(crypto.ProofOps) + for i := 0; i < num; i++ { + proof := crypto.ProofOp{} + if withData { + proof.Data = []byte("\n\031\n\003KEY\022\005VALUE\032\013\010\001\030\001 \001*\003\000\002\002") + } + proofOps.Ops = append(proofOps.Ops, proof) + } + } + return proofOps +} diff --git a/eth/rpc/rpcapi/eth_api.go b/eth/rpc/rpcapi/eth_api.go index 9695832df..62590347f 100644 --- a/eth/rpc/rpcapi/eth_api.go +++ b/eth/rpc/rpcapi/eth_api.go @@ -4,8 +4,6 @@ package rpcapi import ( "context" - "github.com/ethereum/go-ethereum/signer/core/apitypes" - gethrpc "github.com/ethereum/go-ethereum/rpc" "github.com/cometbft/cometbft/libs/log" @@ -43,7 +41,7 @@ type IEthAPI interface { // it is a user or a smart contract. GetTransactionByHash(hash common.Hash) (*rpc.EthTxJsonRPC, error) GetTransactionCount(address common.Address, blockNrOrHash rpc.BlockNumberOrHash) (*hexutil.Uint64, error) - GetTransactionReceipt(hash common.Hash) (map[string]interface{}, error) + GetTransactionReceipt(hash common.Hash) (*backend.TransactionReceipt, error) GetTransactionByBlockHashAndIndex(hash common.Hash, idx hexutil.Uint) (*rpc.EthTxJsonRPC, error) GetTransactionByBlockNumberAndIndex(blockNum rpc.BlockNumber, idx hexutil.Uint) (*rpc.EthTxJsonRPC, error) // eth_getBlockReceipts @@ -53,9 +51,6 @@ type IEthAPI interface { // Allows developers to both send ETH from one address to another, write data // on-chain, and interact with smart contracts. SendRawTransaction(data hexutil.Bytes) (common.Hash, error) - SendTransaction(args evm.JsonTxArgs) (common.Hash, error) - // eth_sendPrivateTransaction - // eth_cancel PrivateTransaction // Account Information // @@ -112,16 +107,9 @@ type IEthAPI interface { // Other Syncing() (interface{}, error) GetTransactionLogs(txHash common.Hash) ([]*gethcore.Log, error) - SignTypedData( - address common.Address, typedData apitypes.TypedData, - ) (hexutil.Bytes, error) FillTransaction( args evm.JsonTxArgs, ) (*rpc.SignTransactionResult, error) - Resend( - ctx context.Context, args evm.JsonTxArgs, - gasPrice *hexutil.Big, gasLimit *hexutil.Uint64, - ) (common.Hash, error) GetPendingTransactions() ([]*rpc.EthTxJsonRPC, error) } @@ -192,7 +180,7 @@ func (e *EthAPI) GetTransactionCount( // GetTransactionReceipt returns the transaction receipt identified by hash. func (e *EthAPI) GetTransactionReceipt( hash common.Hash, -) (map[string]interface{}, error) { +) (*backend.TransactionReceipt, error) { hexTx := hash.Hex() e.logger.Debug("eth_getTransactionReceipt", "hash", hexTx) return e.backend.GetTransactionReceipt(hash) @@ -238,14 +226,6 @@ func (e *EthAPI) SendRawTransaction(data hexutil.Bytes) (common.Hash, error) { return e.backend.SendRawTransaction(data) } -// SendTransaction sends an Ethereum transaction. -func (e *EthAPI) SendTransaction( - txArgs evm.JsonTxArgs, -) (common.Hash, error) { - e.logger.Debug("eth_sendTransaction", "args", txArgs.String()) - return e.backend.SendTransaction(txArgs) -} - // -------------------------------------------------------------------------- // Account Information // -------------------------------------------------------------------------- @@ -368,7 +348,7 @@ func (e *EthAPI) MaxPriorityFeePerGas() (*hexutil.Big, error) { // chain config. func (e *EthAPI) ChainId() (*hexutil.Big, error) { //nolint e.logger.Debug("eth_chainId") - return e.backend.ChainID() + return e.backend.ChainID(), nil } // -------------------------------------------------------------------------- @@ -448,16 +428,6 @@ func (e *EthAPI) GetTransactionLogs(txHash common.Hash) ([]*gethcore.Log, error) return backend.TxLogsFromEvents(resBlockResult.TxsResults[res.TxIndex].Events, index) } -// SignTypedData signs EIP-712 conformant typed data -func (e *EthAPI) SignTypedData( - address common.Address, typedData apitypes.TypedData, -) (hexutil.Bytes, error) { - e.logger.Debug( - "eth_signTypedData", "address", address.Hex(), "data", typedData, - ) - return e.backend.SignTypedData(address, typedData) -} - // FillTransaction fills the defaults (nonce, gas, gasPrice or 1559 fields) // on a given unsigned transaction, and returns it to the caller for further // processing (signing + broadcast). @@ -484,18 +454,6 @@ func (e *EthAPI) FillTransaction( }, nil } -// Resend accepts an existing transaction and a new gas price and limit. It will -// remove the given transaction from the pool and reinsert it with the new gas -// price and limit. -func (e *EthAPI) Resend(_ context.Context, - args evm.JsonTxArgs, - gasPrice *hexutil.Big, - gasLimit *hexutil.Uint64, -) (common.Hash, error) { - e.logger.Debug("eth_resend", "args", args.String()) - return e.backend.Resend(args, gasPrice, gasLimit) -} - // GetPendingTransactions returns the transactions that are in the transaction // pool and have a from address that is one of the accounts this node manages. func (e *EthAPI) GetPendingTransactions() ([]*rpc.EthTxJsonRPC, error) { diff --git a/eth/rpc/rpcapi/net_api.go b/eth/rpc/rpcapi/net_api.go index cc63bd303..029aacc38 100644 --- a/eth/rpc/rpcapi/net_api.go +++ b/eth/rpc/rpcapi/net_api.go @@ -22,14 +22,10 @@ type NetAPI struct { // NewImplNetAPI creates an instance of the public Net Web3 API. func NewImplNetAPI(clientCtx client.Context) *NetAPI { - // parse the chainID from a integer string - chainIDEpoch, err := eth.ParseEthChainID(clientCtx.ChainID) - if err != nil { - panic(err) - } + chainID := eth.ParseEthChainID(clientCtx.ChainID) return &NetAPI{ - networkVersion: chainIDEpoch.Uint64(), + networkVersion: chainID.Uint64(), tmClient: clientCtx.Client.(rpcclient.Client), } } From 81ea61df43b049f8e70f2ec93a6463a0165df9dc Mon Sep 17 00:00:00 2001 From: Oleg Nikonychev Date: Mon, 23 Sep 2024 08:08:32 +0400 Subject: [PATCH 08/16] feat(evm): evm tx indexer service implemented (#2044) * feat(evm): evm tx indexer service implemented * feat(evm): evm-indexer cli command to fill the indexing gaps * chore: lint * chore: changelog update * fix: graceful stopping of the evm indexer * fix: race conditions on evm indexer service * fix: race condition within evm indexer --- CHANGELOG.md | 2 +- app/server/evm_tx_indexer_cli.go | 133 +++++++++++++++++ app/server/evm_tx_indexer_service.go | 135 ++++++++++++++++++ app/server/json_rpc.go | 11 +- app/server/start.go | 52 ++++--- app/server/util.go | 4 +- cmd/nibid/cmd/root.go | 3 + contrib/scripts/localnet.sh | 3 + e2e/evm/test/utils.ts | 1 + .../{kv_indexer.go => evm_tx_indexer.go} | 67 +++++---- ...indexer_test.go => evm_tx_indexer_test.go} | 5 +- eth/rpc/backend/backend.go | 6 +- eth/rpc/backend/tx_info.go | 19 ++- eth/rpc/backend/tx_info_test.go | 8 +- x/common/testutil/testnetwork/start_node.go | 17 +-- .../testutil/testnetwork/validator_node.go | 20 ++- 16 files changed, 400 insertions(+), 86 deletions(-) create mode 100644 app/server/evm_tx_indexer_cli.go create mode 100644 app/server/evm_tx_indexer_service.go rename eth/indexer/{kv_indexer.go => evm_tx_indexer.go} (74%) rename eth/indexer/{kv_indexer_test.go => evm_tx_indexer_test.go} (97%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4aa34212e..57d38bc11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -120,7 +120,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#2030](https://github.com/NibiruChain/nibiru/pull/2030) - refactor(eth/rpc): Delete unused code and improve logging in the eth and debug namespaces - [#2031](https://github.com/NibiruChain/nibiru/pull/2031) - fix(evm): debug calls with custom tracer and tracer options - [#2039](https://github.com/NibiruChain/nibiru/pull/2039) - refactor(rpc-backend): remove unnecessary interface code -- [#2039](https://github.com/NibiruChain/nibiru/pull/2039) - refactor(rpc-backend): Remove mocks from eth/rpc/backend, partially completing [nibiru#2037](https://github.com/NibiruChain/nibiru/issue/2037). +- [#2044](https://github.com/NibiruChain/nibiru/pull/2044) - feat(evm): evm tx indexer service implemented - [#2045](https://github.com/NibiruChain/nibiru/pull/2045) - test(evm): backend tests with test network and real txs #### Dapp modules: perp, spot, oracle, etc diff --git a/app/server/evm_tx_indexer_cli.go b/app/server/evm_tx_indexer_cli.go new file mode 100644 index 000000000..74b9b8fd4 --- /dev/null +++ b/app/server/evm_tx_indexer_cli.go @@ -0,0 +1,133 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package server + +import ( + "fmt" + "strconv" + + "github.com/spf13/cobra" + + "github.com/NibiruChain/nibiru/v2/eth/indexer" + + tmnode "github.com/cometbft/cometbft/node" + sm "github.com/cometbft/cometbft/state" + tmstore "github.com/cometbft/cometbft/store" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/server" +) + +func NewEVMTxIndexCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "evm-tx-index [minBlockNumber|last-indexed] [maxBlockNumber|latest]", + Short: "Index historical evm blocks and transactions", + Long: `Command is useful for catching up if the node experienced a period +with EVMTxIndexer turned off or was stopped without proper closing/flushing EVMIndexerDB. +Processes blocks from minBlockNumber to maxBlockNumber, indexes evm txs. + +- minBlockNumber: min block to start indexing. Supply "last-indexed" to start with the latest block available in EVMIndexerDB. +- maxBlockNumber: max block, could be a number or "latest". + +Default run before the full node/archive node start should be: + +nibid evm-tx-index last-indexed latest + `, + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + serverCtx := server.GetServerContextFromCmd(cmd) + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + cfg := serverCtx.Config + logger := serverCtx.Logger + evmIndexerDB, err := OpenIndexerDB(cfg.RootDir, server.GetAppDBBackend(serverCtx.Viper)) + if err != nil { + logger.Error("failed to open evm indexer DB", "error", err.Error()) + return err + } + + evmTxIndexer := indexer.NewEVMTxIndexer(evmIndexerDB, logger.With("module", "evmindex"), clientCtx) + + tmdb, err := tmnode.DefaultDBProvider(&tmnode.DBContext{ID: "blockstore", Config: cfg}) + if err != nil { + return err + } + blockStore := tmstore.NewBlockStore(tmdb) + minAvailableHeight := blockStore.Base() + maxAvailableHeight := blockStore.Height() + fmt.Printf("Block range available on the node: %d - %d\n", minAvailableHeight, maxAvailableHeight) + + var fromBlock int64 + var toBlock int64 + + // FROM block could be one of two: + // - int64 number - replaced with minAvailableHeight if too low + // - last-indexed - latest available block in EVMIndexerDB, 0 if nothing is indexed + if args[0] == "last-indexed" { + fromBlock, err = evmTxIndexer.LastIndexedBlock() + if err != nil || fromBlock < 0 { + fromBlock = 0 + } + } else { + fromBlock, err = strconv.ParseInt(args[1], 10, 64) + if err != nil { + return fmt.Errorf("cannot parse min block number: %s", args[1]) + } + if fromBlock > maxAvailableHeight { + return fmt.Errorf("maximum available block is: %d", maxAvailableHeight) + } + } + if fromBlock < minAvailableHeight { + fromBlock = minAvailableHeight + } + + // TO block could be one of two: + // - int64 number - replaced with maxAvailableHeight if too high + // - latest - latest available block in the node + if args[1] == "latest" { + toBlock = maxAvailableHeight + } else { + toBlock, err = strconv.ParseInt(args[1], 10, 64) + if err != nil { + return fmt.Errorf("cannot parse max block number: %s", args[1]) + } + if toBlock > maxAvailableHeight { + toBlock = maxAvailableHeight + } + } + if fromBlock > toBlock { + return fmt.Errorf("minBlockNumber must be less or equal to maxBlockNumber") + } + stateDB, err := tmnode.DefaultDBProvider(&tmnode.DBContext{ID: "state", Config: cfg}) + if err != nil { + return err + } + stateStore := sm.NewStore(stateDB, sm.StoreOptions{ + DiscardABCIResponses: cfg.Storage.DiscardABCIResponses, + }) + + fmt.Printf("Indexing blocks from %d to %d\n", fromBlock, toBlock) + for height := fromBlock; height <= toBlock; height++ { + block := blockStore.LoadBlock(height) + if block == nil { + return fmt.Errorf("block not found %d", height) + } + blockResults, err := stateStore.LoadABCIResponses(height) + if err != nil { + return err + } + if err := evmTxIndexer.IndexBlock(block, blockResults.DeliverTxs); err != nil { + return err + } + fmt.Println(height) + } + err = evmTxIndexer.CloseDBAndExit() + if err != nil { + return err + } + fmt.Println("Indexing complete") + return nil + }, + } + return cmd +} diff --git a/app/server/evm_tx_indexer_service.go b/app/server/evm_tx_indexer_service.go new file mode 100644 index 000000000..32e1709c5 --- /dev/null +++ b/app/server/evm_tx_indexer_service.go @@ -0,0 +1,135 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package server + +import ( + "context" + "sync/atomic" + "time" + + "github.com/cometbft/cometbft/libs/service" + rpcclient "github.com/cometbft/cometbft/rpc/client" + "github.com/cometbft/cometbft/types" + + "github.com/NibiruChain/nibiru/v2/eth/indexer" +) + +const ( + EVMTxIndexerServiceName = "EVMTxIndexerService" + + NewBlockWaitTimeout = 60 * time.Second +) + +// EVMTxIndexerService indexes transactions for json-rpc service. +type EVMTxIndexerService struct { + service.BaseService + + evmTxIndexer *indexer.EVMTxIndexer + rpcClient rpcclient.Client + cancelFunc context.CancelFunc +} + +// NewEVMIndexerService returns a new service instance. +func NewEVMIndexerService(evmTxIndexer *indexer.EVMTxIndexer, rpcClient rpcclient.Client) *EVMTxIndexerService { + indexerService := &EVMTxIndexerService{evmTxIndexer: evmTxIndexer, rpcClient: rpcClient} + indexerService.BaseService = *service.NewBaseService(nil, EVMTxIndexerServiceName, indexerService) + return indexerService +} + +// OnStart implements service.Service by subscribing for new blocks +// and indexing them by events. +func (service *EVMTxIndexerService) OnStart() error { + ctx, cancel := context.WithCancel(context.Background()) + service.cancelFunc = cancel + + status, err := service.rpcClient.Status(ctx) + if err != nil { + return err + } + + // chainHeightStorage is used within goroutine and the indexer loop so, using atomic for read/write + var chainHeightStorage int64 + atomic.StoreInt64(&chainHeightStorage, status.SyncInfo.LatestBlockHeight) + + newBlockSignal := make(chan struct{}, 1) + blockHeadersChan, err := service.rpcClient.Subscribe( + ctx, + EVMTxIndexerServiceName, + types.QueryForEvent(types.EventNewBlockHeader).String(), + 0, + ) + if err != nil { + return err + } + + // Goroutine listening for new blocks + go func(ctx context.Context) { + for { + select { + case <-ctx.Done(): + service.Logger.Info("Stopping indexer goroutine") + err := service.evmTxIndexer.CloseDBAndExit() + if err != nil { + service.Logger.Error("Error closing indexer DB", "err", err) + } + return + case msg := <-blockHeadersChan: + eventDataHeader := msg.Data.(types.EventDataNewBlockHeader) + currentChainHeight := eventDataHeader.Header.Height + chainHeight := atomic.LoadInt64(&chainHeightStorage) + if currentChainHeight > chainHeight { + atomic.StoreInt64(&chainHeightStorage, currentChainHeight) + // notify + select { + case newBlockSignal <- struct{}{}: + default: + } + } + } + } + }(ctx) + + lastIndexedHeight, err := service.evmTxIndexer.LastIndexedBlock() + if err != nil { + return err + } + if lastIndexedHeight == -1 { + lastIndexedHeight = atomic.LoadInt64(&chainHeightStorage) + } + + // Indexer loop + for { + chainHeight := atomic.LoadInt64(&chainHeightStorage) + if chainHeight <= lastIndexedHeight { + // nothing to index. wait for signal of new block + select { + case <-newBlockSignal: + case <-time.After(NewBlockWaitTimeout): + } + continue + } + for i := lastIndexedHeight + 1; i <= chainHeight; i++ { + block, err := service.rpcClient.Block(ctx, &i) + if err != nil { + service.Logger.Error("failed to fetch block", "height", i, "err", err) + break + } + blockResult, err := service.rpcClient.BlockResults(ctx, &i) + if err != nil { + service.Logger.Error("failed to fetch block result", "height", i, "err", err) + break + } + if err := service.evmTxIndexer.IndexBlock(block.Block, blockResult.TxsResults); err != nil { + service.Logger.Error("failed to index block", "height", i, "err", err) + } + lastIndexedHeight = blockResult.Height + } + } +} + +func (service *EVMTxIndexerService) OnStop() { + service.Logger.Info("Stopping EVMTxIndexerService") + if service.cancelFunc != nil { + service.Logger.Info("Calling EVMIndexerService CancelFunc") + service.cancelFunc() + } +} diff --git a/app/server/json_rpc.go b/app/server/json_rpc.go index 7baa9cc9d..44b946719 100644 --- a/app/server/json_rpc.go +++ b/app/server/json_rpc.go @@ -21,14 +21,15 @@ import ( ) // StartJSONRPC starts the JSON-RPC server -func StartJSONRPC(ctx *server.Context, +func StartJSONRPC( + ctx *server.Context, clientCtx client.Context, tmRPCAddr, tmEndpoint string, config *srvconfig.Config, indexer eth.EVMTxIndexer, ) (*http.Server, chan struct{}, error) { - tmWsClient := ConnectTmWS(tmRPCAddr, tmEndpoint, ctx.Logger) + tmWsClientForRPCApi := ConnectTmWS(tmRPCAddr, tmEndpoint, ctx.Logger) logger := ctx.Logger.With("module", "geth") ethlog.Root().SetHandler(ethlog.FuncHandler(func(r *ethlog.Record) error { @@ -48,7 +49,7 @@ func StartJSONRPC(ctx *server.Context, allowUnprotectedTxs := config.JSONRPC.AllowUnprotectedTxs rpcAPIArr := config.JSONRPC.API - apis := rpcapi.GetRPCAPIs(ctx, clientCtx, tmWsClient, allowUnprotectedTxs, indexer, rpcAPIArr) + apis := rpcapi.GetRPCAPIs(ctx, clientCtx, tmWsClientForRPCApi, allowUnprotectedTxs, indexer, rpcAPIArr) for _, api := range apis { if err := rpcServer.RegisterName(api.Namespace, api.Service); err != nil { @@ -108,8 +109,8 @@ func StartJSONRPC(ctx *server.Context, ctx.Logger.Info("Starting JSON WebSocket server", "address", config.JSONRPC.WsAddress) // allocate separate WS connection to Tendermint - tmWsClient = ConnectTmWS(tmRPCAddr, tmEndpoint, ctx.Logger) - wsSrv := rpcapi.NewWebsocketsServer(clientCtx, ctx.Logger, tmWsClient, config) + tmWsClientForRPCWs := ConnectTmWS(tmRPCAddr, tmEndpoint, ctx.Logger) + wsSrv := rpcapi.NewWebsocketsServer(clientCtx, ctx.Logger, tmWsClientForRPCWs, config) wsSrv.Start() return httpSrv, httpSrvDone, nil } diff --git a/app/server/start.go b/app/server/start.go index f18c0101f..44a1d2d82 100644 --- a/app/server/start.go +++ b/app/server/start.go @@ -17,6 +17,7 @@ import ( "github.com/NibiruChain/nibiru/v2/eth" "github.com/NibiruChain/nibiru/v2/eth/indexer" + rpcclient "github.com/cometbft/cometbft/rpc/client" "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/crypto/keyring" "github.com/cosmos/cosmos-sdk/telemetry" @@ -29,7 +30,6 @@ import ( dbm "github.com/cometbft/cometbft-db" abciserver "github.com/cometbft/cometbft/abci/server" tcmd "github.com/cometbft/cometbft/cmd/cometbft/commands" - "github.com/cometbft/cometbft/libs/log" tmos "github.com/cometbft/cometbft/libs/os" "github.com/cometbft/cometbft/node" "github.com/cometbft/cometbft/p2p" @@ -362,7 +362,7 @@ func startInProcess(ctx *sdkserver.Context, clientCtx client.Context, opts Start // Add the tx service to the gRPC router. We only need to register this // service if API or gRPC or JSONRPC is enabled, and avoid doing so in the general - // case, because it spawns a new local tendermint RPC client. + // case, because it spawns a new local tendermint RPC rpcClient. if (conf.API.Enable || conf.GRPC.Enable || conf.JSONRPC.Enable || conf.JSONRPC.EnableIndexer) && tmNode != nil { clientCtx = clientCtx.WithClient(local.New(tmNode)) @@ -384,12 +384,17 @@ func startInProcess(ctx *sdkserver.Context, clientCtx client.Context, opts Start var evmIdxer eth.EVMTxIndexer if conf.JSONRPC.EnableIndexer { - idxer, err := OpenEVMIndexer(ctx, ctx.Logger, clientCtx, home) + idxDB, err := OpenIndexerDB(home, sdkserver.GetAppDBBackend(ctx.Viper)) if err != nil { logger.Error("failed to open evm indexer DB", "error", err.Error()) return err } - evmIdxer = idxer + evmTxIndexer, _, err := OpenEVMIndexer(ctx, idxDB, clientCtx) + if err != nil { + logger.Error("failed starting evm indexer service", "error", err.Error()) + return err + } + evmIdxer = evmTxIndexer } if conf.API.Enable || conf.JSONRPC.Enable { @@ -422,7 +427,7 @@ func startInProcess(ctx *sdkserver.Context, clientCtx client.Context, opts Start grpcAddress := fmt.Sprintf("127.0.0.1:%s", port) - // If grpc is enabled, configure grpc client for grpc gateway and json-rpc. + // If grpc is enabled, configure grpc rpcClient for grpc gateway and json-rpc. grpcClient, err := grpc.Dial( grpcAddress, grpc.WithTransportCredentials(insecure.NewCredentials()), @@ -437,7 +442,7 @@ func startInProcess(ctx *sdkserver.Context, clientCtx client.Context, opts Start } clientCtx = clientCtx.WithGRPCClient(grpcClient) - ctx.Logger.Debug("gRPC client assigned to client context", "address", grpcAddress) + ctx.Logger.Debug("gRPC rpcClient assigned to rpcClient context", "address", grpcAddress) } } @@ -507,7 +512,9 @@ func startInProcess(ctx *sdkserver.Context, clientCtx client.Context, opts Start tmEndpoint := "/websocket" tmRPCAddr := cfg.RPC.ListenAddress - httpSrv, httpSrvDone, err = StartJSONRPC(ctx, clientCtx, tmRPCAddr, tmEndpoint, &conf, evmIdxer) + httpSrv, httpSrvDone, err = StartJSONRPC( + ctx, clientCtx, tmRPCAddr, tmEndpoint, &conf, evmIdxer, + ) if err != nil { return err } @@ -593,19 +600,26 @@ func OpenIndexerDB(rootDir string, backendType dbm.BackendType) (dbm.DB, error) } func OpenEVMIndexer( - ctx *sdkserver.Context, - logger log.Logger, - clientCtx client.Context, - homeDir string, -) (eth.EVMTxIndexer, error) { - idxDB, err := OpenIndexerDB(homeDir, sdkserver.GetAppDBBackend(ctx.Viper)) - if err != nil { - logger.Error("failed to open evm indexer DB", "error", err.Error()) - return nil, err - } - + ctx *sdkserver.Context, indexerDb dbm.DB, clientCtx client.Context, +) (eth.EVMTxIndexer, *EVMTxIndexerService, error) { idxLogger := ctx.Logger.With("indexer", "evm") - return indexer.NewKVIndexer(idxDB, idxLogger, clientCtx), nil + evmIndexer := indexer.NewEVMTxIndexer(indexerDb, idxLogger, clientCtx) + + evmIndexerService := NewEVMIndexerService(evmIndexer, clientCtx.Client.(rpcclient.Client)) + evmIndexerService.SetLogger(idxLogger) + + errCh := make(chan error) + go func() { + if err := evmIndexerService.Start(); err != nil { + errCh <- err + } + }() + select { + case err := <-errCh: + return nil, nil, err + case <-time.After(types.ServerStartTime): // assume server started successfully + } + return evmIndexer, evmIndexerService, nil } func openTraceWriter(traceWriterFile string) (w io.Writer, err error) { diff --git a/app/server/util.go b/app/server/util.go index 8e223abd7..9e45fe28b 100644 --- a/app/server/util.go +++ b/app/server/util.go @@ -68,13 +68,13 @@ func ConnectTmWS(tmRPCAddr, tmEndpoint string, logger tmlog.Logger) *rpcclient.W if err != nil { logger.Error( - "Tendermint WS client could not be created", + "Tendermint WS rpcClient could not be created", "address", tmRPCAddr+tmEndpoint, "error", err, ) } else if err := tmWsClient.OnStart(); err != nil { logger.Error( - "Tendermint WS client could not start", + "Tendermint WS rpcClient could not start", "address", tmRPCAddr+tmEndpoint, "error", err, ) diff --git a/cmd/nibid/cmd/root.go b/cmd/nibid/cmd/root.go index 7c8dbea5b..bf1f8c89c 100644 --- a/cmd/nibid/cmd/root.go +++ b/cmd/nibid/cmd/root.go @@ -136,6 +136,9 @@ func initRootCmd(rootCmd *cobra.Command, encodingConfig app.EncodingConfig) { queryCommand(), txCommand(), keys.Commands(app.DefaultNodeHome), + + // EVM Tx Indexer force catch up command + server.NewEVMTxIndexCmd(), ) // TODO add rosettaj diff --git a/contrib/scripts/localnet.sh b/contrib/scripts/localnet.sh index a33e7a3fd..8266cf44e 100755 --- a/contrib/scripts/localnet.sh +++ b/contrib/scripts/localnet.sh @@ -165,6 +165,9 @@ sed -i $SEDOPTION '/\[json\-rpc\]/,+3 s/enable = false/enable = true/' $CHAIN_DI echo_info "config/app.toml: Enabling debug evm api" sed -i $SEDOPTION '/\[json\-rpc\]/,+13 s/api = "eth,net,web3"/api = "eth,net,web3,debug"/' $CHAIN_DIR/config/app.toml +echo_info "config/app.toml: Enabling evm indexer" +sed -i $SEDOPTION '/\[json\-rpc\]/,+51 s/enable-indexer = false/enable-indexer = true/' $CHAIN_DIR/config/app.toml + # Enable Swagger Docs echo_info "config/app.toml: Enabling Swagger Docs" sed -i $SEDOPTION 's/swagger = false/swagger = true/' $CHAIN_DIR/config/app.toml diff --git a/e2e/evm/test/utils.ts b/e2e/evm/test/utils.ts index e034fb489..c2676c27b 100644 --- a/e2e/evm/test/utils.ts +++ b/e2e/evm/test/utils.ts @@ -47,5 +47,6 @@ export const sendTestNibi = async () => { } const txResponse = await account.sendTransaction(transaction) await txResponse.wait(1, 10e3) + console.log(txResponse) return txResponse } diff --git a/eth/indexer/kv_indexer.go b/eth/indexer/evm_tx_indexer.go similarity index 74% rename from eth/indexer/kv_indexer.go rename to eth/indexer/evm_tx_indexer.go index 30696d472..83f5c8213 100644 --- a/eth/indexer/kv_indexer.go +++ b/eth/indexer/evm_tx_indexer.go @@ -28,29 +28,29 @@ const ( TxIndexKeyLength = 1 + 8 + 8 ) -var _ eth.EVMTxIndexer = &KVIndexer{} +var _ eth.EVMTxIndexer = &EVMTxIndexer{} -// KVIndexer implements a eth tx indexer on a KV db. -type KVIndexer struct { +// EVMTxIndexer implements a eth tx indexer on a KV db. +type EVMTxIndexer struct { db dbm.DB logger log.Logger clientCtx client.Context } -// NewKVIndexer creates the KVIndexer -func NewKVIndexer(db dbm.DB, logger log.Logger, clientCtx client.Context) *KVIndexer { - return &KVIndexer{db, logger, clientCtx} +// NewEVMTxIndexer creates the EVMTxIndexer +func NewEVMTxIndexer(db dbm.DB, logger log.Logger, clientCtx client.Context) *EVMTxIndexer { + return &EVMTxIndexer{db, logger, clientCtx} } // IndexBlock index all the eth txs in a block through the following steps: -// - Iterates over all of the Txs in Block +// - Iterates over all the Txs in Block // - Parses eth Tx infos from cosmos-sdk events for every TxResult // - Iterates over all the messages of the Tx // - Builds and stores a indexer.TxResult based on parsed events for every message -func (kv *KVIndexer) IndexBlock(block *tmtypes.Block, txResults []*abci.ResponseDeliverTx) error { +func (indexer *EVMTxIndexer) IndexBlock(block *tmtypes.Block, txResults []*abci.ResponseDeliverTx) error { height := block.Header.Height - batch := kv.db.NewBatch() + batch := indexer.db.NewBatch() defer batch.Close() // record index of valid eth tx during the iteration @@ -59,7 +59,7 @@ func (kv *KVIndexer) IndexBlock(block *tmtypes.Block, txResults []*abci.Response result := txResults[txIndex] isValidEnough, reason := rpc.TxIsValidEnough(result) if !isValidEnough { - kv.logger.Debug( + indexer.logger.Debug( "Skipped indexing of tx", "reason", reason, "tm_tx_hash", eth.TmTxHashToString(tx.Hash()), @@ -67,9 +67,9 @@ func (kv *KVIndexer) IndexBlock(block *tmtypes.Block, txResults []*abci.Response continue } - tx, err := kv.clientCtx.TxConfig.TxDecoder()(tx) + tx, err := indexer.clientCtx.TxConfig.TxDecoder()(tx) if err != nil { - kv.logger.Error("Fail to decode tx", "err", err, "block", height, "txIndex", txIndex) + indexer.logger.Error("Fail to decode tx", "err", err, "block", height, "txIndex", txIndex) continue } @@ -79,7 +79,7 @@ func (kv *KVIndexer) IndexBlock(block *tmtypes.Block, txResults []*abci.Response txs, err := rpc.ParseTxResult(result, tx) if err != nil { - kv.logger.Error("Fail to parse event", "err", err, "block", height, "txIndex", txIndex) + indexer.logger.Error("Fail to parse event", "err", err, "block", height, "txIndex", txIndex) continue } @@ -102,11 +102,16 @@ func (kv *KVIndexer) IndexBlock(block *tmtypes.Block, txResults []*abci.Response } else { parsedTx := txs.GetTxByMsgIndex(msgIndex) if parsedTx == nil { - kv.logger.Error("msg index not found in events", "msgIndex", msgIndex) + indexer.logger.Error("msg index not found in events", "msgIndex", msgIndex) continue } if parsedTx.EthTxIndex >= 0 && parsedTx.EthTxIndex != ethTxIndex { - kv.logger.Error("eth tx index don't match", "expect", ethTxIndex, "found", parsedTx.EthTxIndex) + indexer.logger.Error( + "eth tx index don't match", + "expect", ethTxIndex, + "found", parsedTx.EthTxIndex, + "height", height, + ) } txResult.GasUsed = parsedTx.GasUsed txResult.Failed = parsedTx.Failed @@ -116,7 +121,7 @@ func (kv *KVIndexer) IndexBlock(block *tmtypes.Block, txResults []*abci.Response txResult.CumulativeGasUsed = cumulativeGasUsed ethTxIndex++ - if err := saveTxResult(kv.clientCtx.Codec, batch, txHash, &txResult); err != nil { + if err := saveTxResult(indexer.clientCtx.Codec, batch, txHash, &txResult); err != nil { return errorsmod.Wrapf(err, "IndexBlock %d", height) } } @@ -128,18 +133,18 @@ func (kv *KVIndexer) IndexBlock(block *tmtypes.Block, txResults []*abci.Response } // LastIndexedBlock returns the latest indexed block number, returns -1 if db is empty -func (kv *KVIndexer) LastIndexedBlock() (int64, error) { - return LoadLastBlock(kv.db) +func (indexer *EVMTxIndexer) LastIndexedBlock() (int64, error) { + return LoadLastBlock(indexer.db) } // FirstIndexedBlock returns the first indexed block number, returns -1 if db is empty -func (kv *KVIndexer) FirstIndexedBlock() (int64, error) { - return LoadFirstBlock(kv.db) +func (indexer *EVMTxIndexer) FirstIndexedBlock() (int64, error) { + return LoadFirstBlock(indexer.db) } // GetByTxHash finds eth tx by eth tx hash -func (kv *KVIndexer) GetByTxHash(hash common.Hash) (*eth.TxResult, error) { - bz, err := kv.db.Get(TxHashKey(hash)) +func (indexer *EVMTxIndexer) GetByTxHash(hash common.Hash) (*eth.TxResult, error) { + bz, err := indexer.db.Get(TxHashKey(hash)) if err != nil { return nil, errorsmod.Wrapf(err, "GetByTxHash %s", hash.Hex()) } @@ -147,22 +152,22 @@ func (kv *KVIndexer) GetByTxHash(hash common.Hash) (*eth.TxResult, error) { return nil, fmt.Errorf("tx not found, hash: %s", hash.Hex()) } var txKey eth.TxResult - if err := kv.clientCtx.Codec.Unmarshal(bz, &txKey); err != nil { + if err := indexer.clientCtx.Codec.Unmarshal(bz, &txKey); err != nil { return nil, errorsmod.Wrapf(err, "GetByTxHash %s", hash.Hex()) } return &txKey, nil } // GetByBlockAndIndex finds eth tx by block number and eth tx index -func (kv *KVIndexer) GetByBlockAndIndex(blockNumber int64, txIndex int32) (*eth.TxResult, error) { - bz, err := kv.db.Get(TxIndexKey(blockNumber, txIndex)) +func (indexer *EVMTxIndexer) GetByBlockAndIndex(blockNumber int64, txIndex int32) (*eth.TxResult, error) { + bz, err := indexer.db.Get(TxIndexKey(blockNumber, txIndex)) if err != nil { return nil, errorsmod.Wrapf(err, "GetByBlockAndIndex %d %d", blockNumber, txIndex) } if len(bz) == 0 { return nil, fmt.Errorf("tx not found, block: %d, eth-index: %d", blockNumber, txIndex) } - return kv.GetByTxHash(common.BytesToHash(bz)) + return indexer.GetByTxHash(common.BytesToHash(bz)) } // TxHashKey returns the key for db entry: `tx hash -> tx result struct` @@ -203,6 +208,16 @@ func LoadFirstBlock(db dbm.DB) (int64, error) { return parseBlockNumberFromKey(it.Key()) } +// CloseDBAndExit should be called upon stopping the indexer +func (indexer *EVMTxIndexer) CloseDBAndExit() error { + indexer.logger.Info("Closing EVMTxIndexer DB") + err := indexer.db.Close() + if err != nil { + return errorsmod.Wrap(err, "CloseDBAndExit") + } + return nil +} + // isEthTx check if the tx is an eth tx func isEthTx(tx sdk.Tx) bool { extTx, ok := tx.(authante.HasExtensionOptionsTx) diff --git a/eth/indexer/kv_indexer_test.go b/eth/indexer/evm_tx_indexer_test.go similarity index 97% rename from eth/indexer/kv_indexer_test.go rename to eth/indexer/evm_tx_indexer_test.go index b1b222027..83931dcba 100644 --- a/eth/indexer/kv_indexer_test.go +++ b/eth/indexer/evm_tx_indexer_test.go @@ -23,7 +23,7 @@ import ( evmtest "github.com/NibiruChain/nibiru/v2/x/evm/evmtest" ) -func TestKVIndexer(t *testing.T) { +func TestEVMTxIndexer(t *testing.T) { priv, err := ethsecp256k1.GenerateKey() require.NoError(t, err) from := common.BytesToAddress(priv.PubKey().Address().Bytes()) @@ -42,7 +42,6 @@ func TestKVIndexer(t *testing.T) { require.NoError(t, tx.Sign(ethSigner, signer)) txHash := tx.AsTransaction().Hash() - // encCfg := MakeEncodingConfig() encCfg := app.MakeEncodingConfig() eth.RegisterInterfaces(encCfg.InterfaceRegistry) evm.RegisterInterfaces(encCfg.InterfaceRegistry) @@ -162,7 +161,7 @@ func TestKVIndexer(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { db := dbm.NewMemDB() - idxer := indexer.NewKVIndexer(db, tmlog.NewNopLogger(), clientCtx) + idxer := indexer.NewEVMTxIndexer(db, tmlog.NewNopLogger(), clientCtx) err = idxer.IndexBlock(tc.block, tc.blockResult) require.NoError(t, err) diff --git a/eth/rpc/backend/backend.go b/eth/rpc/backend/backend.go index 7383fe600..c8bebb610 100644 --- a/eth/rpc/backend/backend.go +++ b/eth/rpc/backend/backend.go @@ -26,7 +26,7 @@ type Backend struct { chainID *big.Int cfg config.Config allowUnprotectedTxs bool - indexer eth.EVMTxIndexer + evmTxIndexer eth.EVMTxIndexer } // NewBackend creates a new Backend instance for cosmos and ethereum namespaces @@ -35,7 +35,7 @@ func NewBackend( logger log.Logger, clientCtx client.Context, allowUnprotectedTxs bool, - indexer eth.EVMTxIndexer, + evmTxIndexer eth.EVMTxIndexer, ) *Backend { chainID := eth.ParseEthChainID(clientCtx.ChainID) appConf, err := config.GetConfig(ctx.Viper) @@ -51,7 +51,7 @@ func NewBackend( chainID: chainID, cfg: appConf, allowUnprotectedTxs: allowUnprotectedTxs, - indexer: indexer, + evmTxIndexer: evmTxIndexer, } } diff --git a/eth/rpc/backend/tx_info.go b/eth/rpc/backend/tx_info.go index c4c7f0de7..9a5c85e09 100644 --- a/eth/rpc/backend/tx_info.go +++ b/eth/rpc/backend/tx_info.go @@ -302,14 +302,11 @@ func (b *Backend) GetTransactionByBlockNumberAndIndex(blockNum rpc.BlockNumber, // GetTxByEthHash uses `/tx_query` to find transaction by ethereum tx hash func (b *Backend) GetTxByEthHash(hash gethcommon.Hash) (*eth.TxResult, error) { - // NOTE: The Tendermint hash is not the same as the gethcommon.Hash. - // https://github.com/cometbft/cometbft/issues/342#issuecomment-1428836340 - // https://github.com/tendermint/tendermint/issues/6539 - if b.indexer != nil { - return b.indexer.GetByTxHash(hash) + if b.evmTxIndexer != nil { + return b.evmTxIndexer.GetByTxHash(hash) } - // fallback to tendermint tx indexer + // fallback to tendermint tx evmTxIndexer query := fmt.Sprintf("%s.%s='%s'", evm.TypeMsgEthereumTx, evm.AttributeKeyEthereumTxHash, hash.Hex()) txResult, err := b.queryTendermintTxIndexer(query, func(txs *rpc.ParsedTxs) *rpc.ParsedTx { return txs.GetTxByHash(hash) @@ -323,11 +320,11 @@ func (b *Backend) GetTxByEthHash(hash gethcommon.Hash) (*eth.TxResult, error) { // GetTxByTxIndex uses `/tx_query` to find transaction by tx index of valid ethereum txs func (b *Backend) GetTxByTxIndex(height int64, index uint) (*eth.TxResult, error) { int32Index := int32(index) // #nosec G701 -- checked for int overflow already - if b.indexer != nil { - return b.indexer.GetByBlockAndIndex(height, int32Index) + if b.evmTxIndexer != nil { + return b.evmTxIndexer.GetByBlockAndIndex(height, int32Index) } - // fallback to tendermint tx indexer + // fallback to tendermint tx evmTxIndexer query := fmt.Sprintf("tx.height=%d AND %s.%s=%d", height, evm.TypeMsgEthereumTx, evm.AttributeKeyTxIndex, index, @@ -341,7 +338,7 @@ func (b *Backend) GetTxByTxIndex(height int64, index uint) (*eth.TxResult, error return txResult, nil } -// queryTendermintTxIndexer query tx in tendermint tx indexer +// queryTendermintTxIndexer query tx in tendermint tx evmTxIndexer func (b *Backend) queryTendermintTxIndexer(query string, txGetter func(*rpc.ParsedTxs) *rpc.ParsedTx) (*eth.TxResult, error) { resTxs, err := b.clientCtx.Client.TxSearch(b.ctx, query, false, nil, nil, "") if err != nil { @@ -376,7 +373,7 @@ func (b *Backend) GetTransactionByBlockAndIndex(block *tmrpctypes.ResultBlock, i } var msg *evm.MsgEthereumTx - // find in tx indexer + // find in tx evmTxIndexer res, err := b.GetTxByTxIndex(block.Block.Height, uint(idx)) if err == nil { tx, err := b.clientCtx.TxConfig.TxDecoder()(block.Block.Txs[res.TxIndex]) diff --git a/eth/rpc/backend/tx_info_test.go b/eth/rpc/backend/tx_info_test.go index 3d14d60bd..b9cdda3a6 100644 --- a/eth/rpc/backend/tx_info_test.go +++ b/eth/rpc/backend/tx_info_test.go @@ -102,7 +102,7 @@ func (s *BackendSuite) TestGetTransactionByBlockHashAndIndex() { { name: "happy: tx found", blockHash: blockHash, - txIndex: 1, + txIndex: 0, wantTxFound: true, }, { @@ -143,13 +143,13 @@ func (s *BackendSuite) TestGetTransactionByBlockNumberAndIndex() { { name: "happy: tx found", blockNumber: transferTxBlockNumber, - txIndex: 1, + txIndex: 0, wantTxFound: true, }, { name: "sad: block not found", blockNumber: rpc.NewBlockNumber(big.NewInt(9999999)), - txIndex: 1, + txIndex: 0, wantTxFound: false, }, { @@ -179,7 +179,7 @@ func AssertTxResults(s *BackendSuite, tx *rpc.EthTxJsonRPC, expectedTxHash gethc s.Require().Equal(&recipient, tx.To) s.Require().Greater(tx.Gas, uint64(0)) s.Require().Equal(expectedTxHash, tx.Hash) - s.Require().Equal(uint64(1), uint64(*tx.TransactionIndex)) + s.Require().Equal(uint64(0), uint64(*tx.TransactionIndex)) } func (s *BackendSuite) TestReceiptMarshalJson() { diff --git a/x/common/testutil/testnetwork/start_node.go b/x/common/testutil/testnetwork/start_node.go index a12317c10..a9dcaf010 100644 --- a/x/common/testutil/testnetwork/start_node.go +++ b/x/common/testutil/testnetwork/start_node.go @@ -6,6 +6,7 @@ import ( "time" "cosmossdk.io/errors" + db "github.com/cometbft/cometbft-db" "github.com/ethereum/go-ethereum/ethclient" "github.com/NibiruChain/nibiru/v2/app/server" @@ -132,16 +133,17 @@ func startNodeAndServers(cfg Config, val *Validator) error { val.Logger.Log("Set EVM indexer") - homeDir := val.Ctx.Config.RootDir - evmTxIndexer, err := server.OpenEVMIndexer( - val.Ctx, evmServerCtxLogger, val.ClientCtx, homeDir, - ) + evmTxIndexer, evmTxIndexerService, err := server.OpenEVMIndexer(val.Ctx, db.NewMemDB(), val.ClientCtx) if err != nil { - return err + { + return fmt.Errorf("failed starting evm indexer service: %w", err) + } } val.EthTxIndexer = evmTxIndexer + val.EthTxIndexerService = evmTxIndexerService - val.jsonrpc, val.jsonrpcDone, err = server.StartJSONRPC(val.Ctx, val.ClientCtx, tmRPCAddr, tmEndpoint, val.AppConfig, nil) + val.jsonrpc, val.jsonrpcDone, err = + server.StartJSONRPC(val.Ctx, val.ClientCtx, tmRPCAddr, tmEndpoint, val.AppConfig, val.EthTxIndexer) if err != nil { return errors.Wrap(err, "failed to start JSON-RPC server") } @@ -160,8 +162,7 @@ func startNodeAndServers(cfg Config, val *Validator) error { val.Ctx.Logger, val.ClientCtx, val.AppConfig.JSONRPC.AllowUnprotectedTxs, - // TODO: reenable indexer when we have indexer service (process which does IndexBlock) implemented - nil, //val.EthTxIndexer + val.EthTxIndexer, ) val.Logger.Log("Expose typed methods for each namespace") diff --git a/x/common/testutil/testnetwork/validator_node.go b/x/common/testutil/testnetwork/validator_node.go index 131d74aa2..f4dc3f016 100644 --- a/x/common/testutil/testnetwork/validator_node.go +++ b/x/common/testutil/testnetwork/validator_node.go @@ -12,6 +12,8 @@ import ( "github.com/ethereum/go-ethereum/ethclient" "github.com/stretchr/testify/suite" + appserver "github.com/NibiruChain/nibiru/v2/app/server" + serverconfig "github.com/NibiruChain/nibiru/v2/app/server/config" "github.com/NibiruChain/nibiru/v2/eth" ethrpc "github.com/NibiruChain/nibiru/v2/eth/rpc" @@ -85,10 +87,11 @@ type Validator struct { // - rpc.Local RPCClient tmclient.Client - JSONRPCClient *ethclient.Client - EthRpcQueryClient *ethrpc.QueryClient - EthRpcBackend *backend.Backend - EthTxIndexer eth.EVMTxIndexer + JSONRPCClient *ethclient.Client + EthRpcQueryClient *ethrpc.QueryClient + EthRpcBackend *backend.Backend + EthTxIndexer eth.EVMTxIndexer + EthTxIndexerService *appserver.EVMTxIndexerService EthRPC_ETH *rpcapi.EthAPI EthRpc_WEB3 *rpcapi.APIWeb3 @@ -161,6 +164,7 @@ func stopValidatorNode(v *Validator) { // _ = v.jsonrpc.Close() ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() + if err := v.jsonrpc.Shutdown(ctx); err != nil { // Log the error or handle it as appropriate for your application v.Logger.Logf("❌ Error shutting down JSON-RPC server: %w", err) @@ -168,6 +172,14 @@ func stopValidatorNode(v *Validator) { v.Logger.Log("✅ Successfully shut down JSON-RPC server") v.jsonrpc = nil } + if v.EthTxIndexerService != nil { + err := v.EthTxIndexerService.Stop() + if err != nil { + v.Logger.Logf("❌ Error shutting down EVMTxIndexerService: %w", err) + } else { + v.Logger.Log("✅ Successfully shut down EVMTxIndexerService") + } + } } if v.tmNode != nil { From 1829bc42cab97e706126a9883ea9a1c468fd0391 Mon Sep 17 00:00:00 2001 From: Oleg Nikonychev Date: Tue, 1 Oct 2024 18:05:18 +0400 Subject: [PATCH 09/16] feat(evm): ante handler to prohibit authz grant evm messages (#2032) * feat(evm): ante handler to prohibit authz grant evm messages * feat(authz): rejecting authz exec messages with MsgEthereumTx inside * chore: lint * chore: typo fix --------- Co-authored-by: Unique Divine <51418232+Unique-Divine@users.noreply.github.com> Co-authored-by: Kevin Yang <5478483+k-yang@users.noreply.github.com> --- CHANGELOG.md | 1 + app/ante.go | 1 + app/ante/auth_grard_test.go | 138 ++++++++++++++++++++++++++++++++++++ app/ante/authz_guard.go | 72 +++++++++++++++++-- 4 files changed, 205 insertions(+), 7 deletions(-) create mode 100644 app/ante/auth_grard_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 57d38bc11..d0590d8a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -119,6 +119,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#2023](https://github.com/NibiruChain/nibiru/pull/2023) - fix(evm)!: adjusted generation and parsing of the block bloom events - [#2030](https://github.com/NibiruChain/nibiru/pull/2030) - refactor(eth/rpc): Delete unused code and improve logging in the eth and debug namespaces - [#2031](https://github.com/NibiruChain/nibiru/pull/2031) - fix(evm): debug calls with custom tracer and tracer options +- [#2032](https://github.com/NibiruChain/nibiru/pull/2032) - feat(evm): ante handler to prohibit authz grant evm messages - [#2039](https://github.com/NibiruChain/nibiru/pull/2039) - refactor(rpc-backend): remove unnecessary interface code - [#2044](https://github.com/NibiruChain/nibiru/pull/2044) - feat(evm): evm tx indexer service implemented - [#2045](https://github.com/NibiruChain/nibiru/pull/2045) - test(evm): backend tests with test network and real txs diff --git a/app/ante.go b/app/ante.go index f8c661163..79c2d5adf 100644 --- a/app/ante.go +++ b/app/ante.go @@ -62,6 +62,7 @@ func NewAnteHandlerNonEVM( ) sdk.AnteHandler { return sdk.ChainAnteDecorators( ante.AnteDecoratorPreventEtheruemTxMsgs{}, // reject MsgEthereumTxs + ante.AnteDecoratorAuthzGuard{}, // disable certain messages in authz grant "generic" authante.NewSetUpContextDecorator(), wasmkeeper.NewLimitSimulationGasDecorator(opts.WasmConfig.SimulationGasLimit), wasmkeeper.NewCountTXDecorator(opts.TxCounterStoreKey), diff --git a/app/ante/auth_grard_test.go b/app/ante/auth_grard_test.go new file mode 100644 index 000000000..838ed804e --- /dev/null +++ b/app/ante/auth_grard_test.go @@ -0,0 +1,138 @@ +package ante_test + +import ( + "time" + + sdkclienttx "github.com/cosmos/cosmos-sdk/client/tx" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/authz" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + "github.com/NibiruChain/nibiru/v2/app" + "github.com/NibiruChain/nibiru/v2/app/ante" + "github.com/NibiruChain/nibiru/v2/x/evm" + "github.com/NibiruChain/nibiru/v2/x/evm/evmtest" +) + +func (s *AnteTestSuite) TestAnteDecoratorAuthzGuard() { + testCases := []struct { + name string + txMsg func() sdk.Msg + wantErr string + }{ + { + name: "sad: authz generic grant with evm message", + txMsg: func() sdk.Msg { + someTime := time.Now() + expiryTime := someTime.Add(time.Hour) + genericGrant, err := authz.NewGrant( + someTime, + authz.NewGenericAuthorization(sdk.MsgTypeURL(&evm.MsgEthereumTx{})), &expiryTime, + ) + s.Require().NoError(err) + return &authz.MsgGrant{Grant: genericGrant} + }, + wantErr: "not allowed", + }, + { + name: "happy: authz generic grant with non evm message", + txMsg: func() sdk.Msg { + someTime := time.Now() + expiryTime := someTime.Add(time.Hour) + genericGrant, err := authz.NewGrant( + someTime, + authz.NewGenericAuthorization(sdk.MsgTypeURL(&stakingtypes.MsgCreateValidator{})), &expiryTime, + ) + s.Require().NoError(err) + return &authz.MsgGrant{Grant: genericGrant} + }, + wantErr: "", + }, + { + name: "happy: authz non generic grant", + txMsg: func() sdk.Msg { + someTime := time.Now() + expiryTime := someTime.Add(time.Hour) + genericGrant, err := authz.NewGrant( + someTime, + &banktypes.SendAuthorization{}, + &expiryTime, + ) + s.Require().NoError(err) + return &authz.MsgGrant{Grant: genericGrant} + }, + wantErr: "", + }, + { + name: "happy: non authz message", + txMsg: func() sdk.Msg { + return &evm.MsgEthereumTx{} + }, + wantErr: "", + }, + { + name: "sad: authz exec with a single evm message", + txMsg: func() sdk.Msg { + msgExec := authz.NewMsgExec( + sdk.AccAddress("nibiuser"), + []sdk.Msg{ + &evm.MsgEthereumTx{}, + }, + ) + return &msgExec + }, + wantErr: "ExtensionOptionsEthereumTx", + }, + { + name: "sad: authz exec with evm message and non evm message", + txMsg: func() sdk.Msg { + msgExec := authz.NewMsgExec( + sdk.AccAddress("nibiuser"), + []sdk.Msg{ + &banktypes.MsgSend{}, + &evm.MsgEthereumTx{}, + }, + ) + return &msgExec + }, + wantErr: "ExtensionOptionsEthereumTx", + }, + { + name: "happy: authz exec without evm messages", + txMsg: func() sdk.Msg { + msgExec := authz.NewMsgExec( + sdk.AccAddress("nibiuser"), + []sdk.Msg{ + &banktypes.MsgSend{}, + }, + ) + return &msgExec + }, + wantErr: "", + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + deps := evmtest.NewTestDeps() + anteDec := ante.AnteDecoratorAuthzGuard{} + + encCfg := app.MakeEncodingConfig() + txBuilder, err := sdkclienttx.Factory{}. + WithChainID(s.ctx.ChainID()). + WithTxConfig(encCfg.TxConfig). + BuildUnsignedTx(tc.txMsg()) + s.Require().NoError(err) + + _, err = anteDec.AnteHandle( + deps.Ctx, txBuilder.GetTx(), false, evmtest.NextNoOpAnteHandler, + ) + if tc.wantErr != "" { + s.Require().ErrorContains(err, tc.wantErr) + return + } + s.Require().NoError(err) + }) + } +} diff --git a/app/ante/authz_guard.go b/app/ante/authz_guard.go index 9ceb8b240..c1cbb8f8d 100644 --- a/app/ante/authz_guard.go +++ b/app/ante/authz_guard.go @@ -1,9 +1,67 @@ +// Copyright (c) 2023-2024 Nibi, Inc. package ante -// TODO: https://github.com/NibiruChain/nibiru/issues/1915 -// feat(ante): Add an authz guard to disable authz Ethereum txs and provide -// additional security around the default functionality exposed by the module. -// -// Implemenetation Notes -// UD-NOTE - IsAuthzMessage fn. Use authz import with module name -// UD-NOTE - Define set of disabled txMsgs +import ( + "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/authz" + + "github.com/NibiruChain/nibiru/v2/x/evm" +) + +// AnteDecoratorAuthzGuard filters autz messages +type AnteDecoratorAuthzGuard struct{} + +// AnteHandle rejects "authz grant generic --msg-type '/eth.evm.v1.MsgEthereumTx'" +// Also rejects authz exec tx.json with any MsgEthereumTx inside +func (rmd AnteDecoratorAuthzGuard) AnteHandle( + ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler, +) (newCtx sdk.Context, err error) { + for _, msg := range tx.GetMsgs() { + // Do not allow grant for MsgEthereumTx + if msgGrant, ok := msg.(*authz.MsgGrant); ok { + if msgGrant.Grant.Authorization == nil { + return ctx, errors.Wrapf( + errortypes.ErrInvalidType, + "grant authorization is missing", + ) + } + authorization, err := msgGrant.Grant.GetAuthorization() + if err != nil { + return ctx, errors.Wrapf( + errortypes.ErrInvalidType, + "failed unmarshaling generic authorization %s", err, + ) + } + if genericAuth, ok := authorization.(*authz.GenericAuthorization); ok { + if genericAuth.MsgTypeURL() == sdk.MsgTypeURL(&evm.MsgEthereumTx{}) { + return ctx, errors.Wrapf( + errortypes.ErrNotSupported, + "authz grant generic for msg type %s is not allowed", + genericAuth.MsgTypeURL(), + ) + } + } + } + // Also reject MsgEthereumTx in exec + if msgExec, ok := msg.(*authz.MsgExec); ok { + msgsInExec, err := msgExec.GetMessages() + if err != nil { + return ctx, errors.Wrapf( + errortypes.ErrInvalidType, + "failed getting exec messages %s", err, + ) + } + for _, msgInExec := range msgsInExec { + if _, ok := msgInExec.(*evm.MsgEthereumTx); ok { + return ctx, errors.Wrapf( + errortypes.ErrInvalidType, + "MsgEthereumTx needs to be contained within a tx with 'ExtensionOptionsEthereumTx' option", + ) + } + } + } + } + return next(ctx, tx, simulate) +} From 48c8146cb1a4b25c219445a46d2e949b6acf8bd2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 14:07:22 +0000 Subject: [PATCH 10/16] chore(deps): bump bufbuild/buf-setup-action from 1.36.0 to 1.42.0 (#2043) * chore(deps): bump bufbuild/buf-setup-action from 1.36.0 to 1.42.0 Bumps [bufbuild/buf-setup-action](https://github.com/bufbuild/buf-setup-action) from 1.36.0 to 1.42.0. - [Release notes](https://github.com/bufbuild/buf-setup-action/releases) - [Commits](https://github.com/bufbuild/buf-setup-action/compare/v1.36.0...v1.42.0) --- updated-dependencies: - dependency-name: bufbuild/buf-setup-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Updated changelog - dependabot --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Unique Divine <51418232+Unique-Divine@users.noreply.github.com> Co-authored-by: Unique-Divine --- .github/workflows/proto-lint.yml | 4 ++-- CHANGELOG.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/proto-lint.yml b/.github/workflows/proto-lint.yml index 1151f2994..7140276e5 100644 --- a/.github/workflows/proto-lint.yml +++ b/.github/workflows/proto-lint.yml @@ -22,7 +22,7 @@ jobs: # timeout-minutes: 5 # steps: # - uses: actions/checkout@v4 - # - uses: bufbuild/buf-setup-action@v1.36.0 + # - uses: bufbuild/buf-setup-action@v1.42.0 # - uses: bufbuild/buf-lint-action@v1 # with: # input: "proto" @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: bufbuild/buf-setup-action@v1.36.0 + - uses: bufbuild/buf-setup-action@v1.42.0 with: github_token: ${{ github.token }} - uses: bufbuild/buf-breaking-action@v1 diff --git a/CHANGELOG.md b/CHANGELOG.md index d0590d8a4..6791ff05d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -174,7 +174,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump `github.com/hashicorp/go-getter` from 1.7.1 to 1.7.5 ([#1858](https://github.com/NibiruChain/nibiru/pull/1858), [#1938](https://github.com/NibiruChain/nibiru/pull/1938)) - Bump `github.com/btcsuite/btcd` from 0.23.3 to 0.24.0 ([#1862](https://github.com/NibiruChain/nibiru/pull/1862)) - Bump `pozetroninc/github-action-get-latest-release` from 0.7.0 to 0.8.0 ([#1863](https://github.com/NibiruChain/nibiru/pull/1863)) -- Bump `bufbuild/buf-setup-action` from 1.30.1 to 1.36.0 ([#1891](https://github.com/NibiruChain/nibiru/pull/1891), [#1900](https://github.com/NibiruChain/nibiru/pull/1900), [#1923](https://github.com/NibiruChain/nibiru/pull/1923), [#1972](https://github.com/NibiruChain/nibiru/pull/1972), [#1974](https://github.com/NibiruChain/nibiru/pull/1974), [#1988](https://github.com/NibiruChain/nibiru/pull/1988)) +- Bump `bufbuild/buf-setup-action` from 1.30.1 to 1.42.0 ([#1891](https://github.com/NibiruChain/nibiru/pull/1891), [#1900](https://github.com/NibiruChain/nibiru/pull/1900), [#1923](https://github.com/NibiruChain/nibiru/pull/1923), [#1972](https://github.com/NibiruChain/nibiru/pull/1972), [#1974](https://github.com/NibiruChain/nibiru/pull/1974), [#1988](https://github.com/NibiruChain/nibiru/pull/1988), [#2043](https://github.com/NibiruChain/nibiru/pull/2043)) - Bump `axios` from 1.7.3 to 1.7.4 ([#2016](https://github.com/NibiruChain/nibiru/pull/2016)) ## [v1.5.0](https://github.com/NibiruChain/nibiru/releases/tag/v1.5.0) - 2024-06-21 From 36533cd633e3280cfef1bab0a4abb06cc5f9bf39 Mon Sep 17 00:00:00 2001 From: Unique Divine <51418232+Unique-Divine@users.noreply.github.com> Date: Tue, 1 Oct 2024 11:01:23 -0500 Subject: [PATCH 11/16] refactor(oracle): remove unused code and collapse empty client/cli directory (#2050) * gitignore and issue comment * refactor(oracle): remove unused code and collapse empty client/cli directory * chore: changelog * fix last few import paths --- .github/workflows/gh-issues.yml | 2 + .gitignore | 1 + CHANGELOG.md | 1 + cmd/nibid/cmd/root.go | 2 +- x/common/testutil/testnetwork/query.go | 2 +- .../cli/gen_pricefeeder_delegation.go | 0 .../cli/gen_pricefeeder_delegation_test.go | 2 +- x/oracle/{client => }/cli/query.go | 0 x/oracle/{client => }/cli/tx.go | 0 x/oracle/integration/action/price.go | 56 ------------------- x/oracle/module.go | 2 +- 11 files changed, 8 insertions(+), 60 deletions(-) rename x/oracle/{client => }/cli/gen_pricefeeder_delegation.go (100%) rename x/oracle/{client => }/cli/gen_pricefeeder_delegation_test.go (97%) rename x/oracle/{client => }/cli/query.go (100%) rename x/oracle/{client => }/cli/tx.go (100%) delete mode 100644 x/oracle/integration/action/price.go diff --git a/.github/workflows/gh-issues.yml b/.github/workflows/gh-issues.yml index 95154e494..69054648e 100644 --- a/.github/workflows/gh-issues.yml +++ b/.github/workflows/gh-issues.yml @@ -1,5 +1,7 @@ name: "Auto-add GH issues to project" # Add all issues opened to the issue board for triage and assignment +# GitHub Org and Project Automation +# https://www.notion.so/nibiru/GitHub-Org-and-Project-Automation-c771d671109849ee9fda7c8b741cd66a?pvs=4 on: issues: diff --git a/.gitignore b/.gitignore index 548b7e4b3..8e690b405 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ temp* txout.json vote.json **__pycache** +scratch-paper.md ### TypeScript and Friends diff --git a/CHANGELOG.md b/CHANGELOG.md index 6791ff05d..2d15cfb76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -152,6 +152,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#1913](https://github.com/NibiruChain/nibiru/pull/1913) - fix(tests): race condition from heavy Network tests - [#1992](https://github.com/NibiruChain/nibiru/pull/1992) - chore: enabled grpc for localnet - [#1999](https://github.com/NibiruChain/nibiru/pull/1999) - chore: update nibi go package version to v2 +- [#2050](https://github.com/NibiruChain/nibiru/pull/2050) - refactor(oracle): remove unused code and collapse empty client/cli directory ### Dependencies diff --git a/cmd/nibid/cmd/root.go b/cmd/nibid/cmd/root.go index bf1f8c89c..d52d70401 100644 --- a/cmd/nibid/cmd/root.go +++ b/cmd/nibid/cmd/root.go @@ -31,7 +31,7 @@ import ( "github.com/spf13/cobra" "github.com/NibiruChain/nibiru/v2/app" - oraclecli "github.com/NibiruChain/nibiru/v2/x/oracle/client/cli" + oraclecli "github.com/NibiruChain/nibiru/v2/x/oracle/cli" ) // NewRootCmd creates a new root command for nibid. It is called once in the diff --git a/x/common/testutil/testnetwork/query.go b/x/common/testutil/testnetwork/query.go index 3599ac2e4..ff5ff189f 100644 --- a/x/common/testutil/testnetwork/query.go +++ b/x/common/testutil/testnetwork/query.go @@ -14,7 +14,7 @@ import ( "github.com/spf13/cobra" "github.com/NibiruChain/nibiru/v2/x/common/asset" - oraclecli "github.com/NibiruChain/nibiru/v2/x/oracle/client/cli" + oraclecli "github.com/NibiruChain/nibiru/v2/x/oracle/cli" oracletypes "github.com/NibiruChain/nibiru/v2/x/oracle/types" sudocli "github.com/NibiruChain/nibiru/v2/x/sudo/cli" sudotypes "github.com/NibiruChain/nibiru/v2/x/sudo/types" diff --git a/x/oracle/client/cli/gen_pricefeeder_delegation.go b/x/oracle/cli/gen_pricefeeder_delegation.go similarity index 100% rename from x/oracle/client/cli/gen_pricefeeder_delegation.go rename to x/oracle/cli/gen_pricefeeder_delegation.go diff --git a/x/oracle/client/cli/gen_pricefeeder_delegation_test.go b/x/oracle/cli/gen_pricefeeder_delegation_test.go similarity index 97% rename from x/oracle/client/cli/gen_pricefeeder_delegation_test.go rename to x/oracle/cli/gen_pricefeeder_delegation_test.go index a2bde2e6b..cd1aa881c 100644 --- a/x/oracle/client/cli/gen_pricefeeder_delegation_test.go +++ b/x/oracle/cli/gen_pricefeeder_delegation_test.go @@ -7,7 +7,7 @@ import ( "github.com/NibiruChain/nibiru/v2/app" "github.com/NibiruChain/nibiru/v2/app/appconst" "github.com/NibiruChain/nibiru/v2/x/common/testutil" - "github.com/NibiruChain/nibiru/v2/x/oracle/client/cli" + "github.com/NibiruChain/nibiru/v2/x/oracle/cli" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/stretchr/testify/require" diff --git a/x/oracle/client/cli/query.go b/x/oracle/cli/query.go similarity index 100% rename from x/oracle/client/cli/query.go rename to x/oracle/cli/query.go diff --git a/x/oracle/client/cli/tx.go b/x/oracle/cli/tx.go similarity index 100% rename from x/oracle/client/cli/tx.go rename to x/oracle/cli/tx.go diff --git a/x/oracle/integration/action/price.go b/x/oracle/integration/action/price.go deleted file mode 100644 index 2cd9114bc..000000000 --- a/x/oracle/integration/action/price.go +++ /dev/null @@ -1,56 +0,0 @@ -package action - -import ( - "time" - - sdk "github.com/cosmos/cosmos-sdk/types" - - "github.com/NibiruChain/collections" - - "github.com/NibiruChain/nibiru/v2/app" - "github.com/NibiruChain/nibiru/v2/x/common/asset" - "github.com/NibiruChain/nibiru/v2/x/common/testutil/action" - "github.com/NibiruChain/nibiru/v2/x/oracle/types" -) - -func SetOraclePrice(pair asset.Pair, price sdk.Dec) action.Action { - return &setPairPrice{ - Pair: pair, - Price: price, - } -} - -type setPairPrice struct { - Pair asset.Pair - Price sdk.Dec -} - -func (s setPairPrice) Do(app *app.NibiruApp, ctx sdk.Context) (sdk.Context, error) { - app.OracleKeeper.SetPrice(ctx, s.Pair, s.Price) - - return ctx, nil -} - -func InsertOraclePriceSnapshot(pair asset.Pair, time time.Time, price sdk.Dec) action.Action { - return &insertOraclePriceSnapshot{ - Pair: pair, - Time: time, - Price: price, - } -} - -type insertOraclePriceSnapshot struct { - Pair asset.Pair - Time time.Time - Price sdk.Dec -} - -func (s insertOraclePriceSnapshot) Do(app *app.NibiruApp, ctx sdk.Context) (sdk.Context, error) { - app.OracleKeeper.PriceSnapshots.Insert(ctx, collections.Join(s.Pair, s.Time), types.PriceSnapshot{ - Pair: s.Pair, - Price: s.Price, - TimestampMs: s.Time.UnixMilli(), - }) - - return ctx, nil -} diff --git a/x/oracle/module.go b/x/oracle/module.go index d3d5fd4ec..55ba76ea5 100644 --- a/x/oracle/module.go +++ b/x/oracle/module.go @@ -18,7 +18,7 @@ import ( "github.com/cosmos/cosmos-sdk/types/module" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" - "github.com/NibiruChain/nibiru/v2/x/oracle/client/cli" + "github.com/NibiruChain/nibiru/v2/x/oracle/cli" "github.com/NibiruChain/nibiru/v2/x/oracle/keeper" "github.com/NibiruChain/nibiru/v2/x/oracle/simulation" "github.com/NibiruChain/nibiru/v2/x/oracle/types" From 2a46f17f98327788ae6ac2b4e0de1dca4de9892a Mon Sep 17 00:00:00 2001 From: Unique Divine <51418232+Unique-Divine@users.noreply.github.com> Date: Tue, 1 Oct 2024 22:54:04 -0500 Subject: [PATCH 12/16] feat(evm-precompile): Precompile for one-way EVM calls to invoke/execute Wasm contracts. (#2054) * Implement wasm precompile in evm/embeds * feat(evm-precompile): Precompile for one-way EVM calls to invoke/execute Wasm contracts. * impl remaining Wasm.sol methods and document thoroughly * more tests and docs --- CHANGELOG.md | 1 + README.md | 2 +- eth/rpc/backend/backend_suite_test.go | 23 +- go.mod | 4 +- x/common/testutil/testnetwork/start_node.go | 3 +- x/evm/embeds/README.md | 48 ++ .../IFunToken.json | 4 +- .../artifacts/contracts/Wasm.sol/IWasm.json | 204 ++++++ .../contracts/{IFunToken.sol => FunToken.sol} | 2 +- x/evm/embeds/contracts/Wasm.sol | 73 +++ x/evm/embeds/embeds.go | 19 +- x/evm/evmtest/erc20.go | 13 + x/evm/precompile/errors.go | 45 ++ x/evm/precompile/funtoken.go | 57 +- x/evm/precompile/funtoken_test.go | 23 +- x/evm/precompile/hello_world_counter.wasm | Bin 0 -> 203903 bytes x/evm/precompile/precompile.go | 31 +- x/evm/precompile/wasm.go | 378 +++++++++++ x/evm/precompile/wasm_parse.go | 242 ++++++++ x/evm/precompile/wasm_test.go | 587 ++++++++++++++++++ x/tokenfactory/fixture/fixture.go | 3 +- 21 files changed, 1696 insertions(+), 66 deletions(-) rename x/evm/embeds/artifacts/contracts/{IFunToken.sol => FunToken.sol}/IFunToken.json (89%) create mode 100644 x/evm/embeds/artifacts/contracts/Wasm.sol/IWasm.json rename x/evm/embeds/contracts/{IFunToken.sol => FunToken.sol} (89%) create mode 100644 x/evm/embeds/contracts/Wasm.sol create mode 100644 x/evm/precompile/errors.go create mode 100644 x/evm/precompile/hello_world_counter.wasm create mode 100644 x/evm/precompile/wasm.go create mode 100644 x/evm/precompile/wasm_parse.go create mode 100644 x/evm/precompile/wasm_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d15cfb76..c04678690 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -123,6 +123,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#2039](https://github.com/NibiruChain/nibiru/pull/2039) - refactor(rpc-backend): remove unnecessary interface code - [#2044](https://github.com/NibiruChain/nibiru/pull/2044) - feat(evm): evm tx indexer service implemented - [#2045](https://github.com/NibiruChain/nibiru/pull/2045) - test(evm): backend tests with test network and real txs +- [#2054](https://github.com/NibiruChain/nibiru/pull/2054) - feat(evm-precompile): Precompile for one-way EVM calls to invoke/execute Wasm contracts. #### Dapp modules: perp, spot, oracle, etc diff --git a/README.md b/README.md index acf559738..4b1b13efc 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ If you have questions or concerns, feel free to connect with a developer or comm | Module | Description | | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [Wasm][code-x-wasm] | Implements the execution environment for WebAssembly (WASM) smart contracts. CosmWasm smart contracts are Rust-based, Wasm smart contracts built for enhanced security, performance, and interoperability. See our [CosmWasm sandbox monorepo (cw-nibiru)](https://github.com/NibiruChain/cw-nibiru/tree/main) for the protocol's core smart contracts. | +| [Wasm][code-x-wasm] | Implements the execution environment for WebAssembly (WASM) smart contracts. CosmWasm smart contracts are Rust-based, Wasm smart contracts built for enhanced security, performance, and interoperability. See our [CosmWasm sandbox monorepo (nibiru-wasm)](https://github.com/NibiruChain/nibiru-wasm/tree/main) for the protocol's core smart contracts. | | [EVM][code-x-evm] | Implements Nibiru EVM, which manages an Ethereum Virtual Machine (EVM) state database and enables the execution of Ethereum smart contracts. Nibiru EVM is an extension of "[geth](https://github.com/ethereum/go-ethereum)" along with "web3" and "eth" JSON-RPC methods. | | [Devgas][code-x-devgas] | The `devgas` module of Nibiru Chain shares contract execution fees with smart contract developers. This aims to increase the adoption of Nibiru by offering CosmWasm smart contract developers a direct source of income based on usage. | | [Epochs][code-x-epochs] | The `epochs` module allows other modules to set hooks to be called to execute code automatically on a period basis. For example, "once a week, starting at UTC-time = x". `epochs` creates a generalized epoch interface. | diff --git a/eth/rpc/backend/backend_suite_test.go b/eth/rpc/backend/backend_suite_test.go index 540bdddb4..0623557a5 100644 --- a/eth/rpc/backend/backend_suite_test.go +++ b/eth/rpc/backend/backend_suite_test.go @@ -2,13 +2,12 @@ package backend_test import ( "context" + "crypto/ecdsa" "fmt" "math/big" "testing" "time" - "crypto/ecdsa" - sdk "github.com/cosmos/cosmos-sdk/types" gethcommon "github.com/ethereum/go-ethereum/common" gethcore "github.com/ethereum/go-ethereum/core/types" @@ -33,15 +32,21 @@ import ( "github.com/NibiruChain/nibiru/v2/x/common/testutil/testnetwork" ) -var recipient = evmtest.NewEthPrivAcc().EthAddr -var amountToSend = evm.NativeToWei(big.NewInt(1)) +var ( + recipient = evmtest.NewEthPrivAcc().EthAddr + amountToSend = evm.NativeToWei(big.NewInt(1)) +) -var transferTxBlockNumber rpc.BlockNumber -var transferTxBlockHash gethcommon.Hash -var transferTxHash gethcommon.Hash +var ( + transferTxBlockNumber rpc.BlockNumber + transferTxBlockHash gethcommon.Hash + transferTxHash gethcommon.Hash +) -var testContractAddress gethcommon.Address -var deployContractBlockNumber rpc.BlockNumber +var ( + testContractAddress gethcommon.Address + deployContractBlockNumber rpc.BlockNumber +) type BackendSuite struct { suite.Suite diff --git a/go.mod b/go.mod index 3476119b5..59bf0e292 100644 --- a/go.mod +++ b/go.mod @@ -60,6 +60,8 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/gorilla/websocket v1.5.0 github.com/rs/cors v1.8.3 + github.com/status-im/keycard-go v0.0.0-20190316090335-8537d3370df4 + golang.org/x/crypto v0.21.0 golang.org/x/exp v0.0.0-20231006140011-7918f672742d golang.org/x/net v0.23.0 golang.org/x/text v0.14.0 @@ -201,7 +203,6 @@ require ( github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect - github.com/status-im/keycard-go v0.0.0-20190316090335-8537d3370df4 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect @@ -223,7 +224,6 @@ require ( go.opentelemetry.io/otel/trace v1.21.0 // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.9.0 // indirect - golang.org/x/crypto v0.21.0 // indirect golang.org/x/oauth2 v0.16.0 // indirect golang.org/x/sync v0.6.0 // indirect golang.org/x/sys v0.18.0 // indirect diff --git a/x/common/testutil/testnetwork/start_node.go b/x/common/testutil/testnetwork/start_node.go index a9dcaf010..93c7381ac 100644 --- a/x/common/testutil/testnetwork/start_node.go +++ b/x/common/testutil/testnetwork/start_node.go @@ -142,8 +142,7 @@ func startNodeAndServers(cfg Config, val *Validator) error { val.EthTxIndexer = evmTxIndexer val.EthTxIndexerService = evmTxIndexerService - val.jsonrpc, val.jsonrpcDone, err = - server.StartJSONRPC(val.Ctx, val.ClientCtx, tmRPCAddr, tmEndpoint, val.AppConfig, val.EthTxIndexer) + val.jsonrpc, val.jsonrpcDone, err = server.StartJSONRPC(val.Ctx, val.ClientCtx, tmRPCAddr, tmEndpoint, val.AppConfig, val.EthTxIndexer) if err != nil { return errors.Wrap(err, "failed to start JSON-RPC server") } diff --git a/x/evm/embeds/README.md b/x/evm/embeds/README.md index 6d4e207ff..e4cc8dad9 100644 --- a/x/evm/embeds/README.md +++ b/x/evm/embeds/README.md @@ -1,6 +1,54 @@ # Nibiru Contract Embeds +## Hacking + ```shell npm install npx hardhat compile ``` + +## Precompile Solidity Documentation + +Example of a well-documented contract: [[Uniswap/v4-core/.../IHooks.sol](https://github.com/Uniswap/v4-core/blob/3407bce4b39869fe41ad5ec724b2df308c34900f/src/interfaces/IHooks.sol)] + +- `@notice`: Used to explain to end users what the function does. Should be written in plain English and focus on the function's purpose. + Best practice: Include for all public and external functions. +- `@param`: Describes a function parameter. Should explain what the parameter is used for. + Best practice: Include for all function parameters, especially in interfaces. +- `@dev`: Provides additional details for developers. Used for implementation details, notes, or warnings for developers. + Best practice: Use when there's important information that doesn't fit in `@notice` but is crucial for developers. +- `@return`: Describes what a function returns. + Best practice: Use for all functions that return values, explaining each return value. + +Example from IHooks.sol: +```solidity +/@notice The hook called before liquidity is removed +/// @param sender The initial msg.sender for the remove liquidity call +/// @param key The key for the pool +/// @param params The parameters for removing liquidity +/// @param hookData Arbitrary data handed into the PoolManager by the liquidity provider to be be passed on to the hook +/// @return bytes4 The function selector for the hook +function beforeRemoveLiquidity( + address sender, + PoolKey calldata key, + IPoolManager.ModifyLiquidityParams calldata params, + bytes calldata hookData +) external returns (bytes4); +``` + +@inheritdoc: + +Used to inherit documentation from a parent contract or interface. +Best practice: Use when you want to reuse documentation from a base contract. + + +@title: + +Provides a title for the contract or interface. +Best practice: Include at the top of each contract or interface file. + + +@author: + +States the author of the contract. +Best practice: Optional, but can be useful in larger projects. diff --git a/x/evm/embeds/artifacts/contracts/IFunToken.sol/IFunToken.json b/x/evm/embeds/artifacts/contracts/FunToken.sol/IFunToken.json similarity index 89% rename from x/evm/embeds/artifacts/contracts/IFunToken.sol/IFunToken.json rename to x/evm/embeds/artifacts/contracts/FunToken.sol/IFunToken.json index 6fe6e838a..a2bebf939 100644 --- a/x/evm/embeds/artifacts/contracts/IFunToken.sol/IFunToken.json +++ b/x/evm/embeds/artifacts/contracts/FunToken.sol/IFunToken.json @@ -1,7 +1,7 @@ { "_format": "hh-sol-artifact-1", - "contractName": "IFunToken", - "sourceName": "contracts/IFunToken.sol", + "contractName": "FunToken", + "sourceName": "contracts/FunToken.sol", "abi": [ { "inputs": [ diff --git a/x/evm/embeds/artifacts/contracts/Wasm.sol/IWasm.json b/x/evm/embeds/artifacts/contracts/Wasm.sol/IWasm.json new file mode 100644 index 000000000..61e38453f --- /dev/null +++ b/x/evm/embeds/artifacts/contracts/Wasm.sol/IWasm.json @@ -0,0 +1,204 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "IWasm", + "sourceName": "contracts/Wasm.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "string", + "name": "contractAddr", + "type": "string" + }, + { + "internalType": "bytes", + "name": "msgArgs", + "type": "bytes" + }, + { + "components": [ + { + "internalType": "string", + "name": "denom", + "type": "string" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "internalType": "struct IWasm.BankCoin[]", + "name": "funds", + "type": "tuple[]" + } + ], + "name": "execute", + "outputs": [ + { + "internalType": "bytes", + "name": "response", + "type": "bytes" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "string", + "name": "contractAddr", + "type": "string" + }, + { + "internalType": "bytes", + "name": "msgArgs", + "type": "bytes" + }, + { + "components": [ + { + "internalType": "string", + "name": "denom", + "type": "string" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "internalType": "struct IWasm.BankCoin[]", + "name": "funds", + "type": "tuple[]" + } + ], + "internalType": "struct IWasm.WasmExecuteMsg[]", + "name": "executeMsgs", + "type": "tuple[]" + } + ], + "name": "executeMulti", + "outputs": [ + { + "internalType": "bytes[]", + "name": "responses", + "type": "bytes[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "admin", + "type": "string" + }, + { + "internalType": "uint64", + "name": "codeID", + "type": "uint64" + }, + { + "internalType": "bytes", + "name": "msgArgs", + "type": "bytes" + }, + { + "internalType": "string", + "name": "label", + "type": "string" + }, + { + "components": [ + { + "internalType": "string", + "name": "denom", + "type": "string" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "internalType": "struct IWasm.BankCoin[]", + "name": "funds", + "type": "tuple[]" + } + ], + "name": "instantiate", + "outputs": [ + { + "internalType": "string", + "name": "contractAddr", + "type": "string" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "contractAddr", + "type": "string" + }, + { + "internalType": "bytes", + "name": "req", + "type": "bytes" + } + ], + "name": "query", + "outputs": [ + { + "internalType": "bytes", + "name": "response", + "type": "bytes" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "contractAddr", + "type": "string" + }, + { + "internalType": "bytes", + "name": "key", + "type": "bytes" + } + ], + "name": "queryRaw", + "outputs": [ + { + "internalType": "bytes", + "name": "response", + "type": "bytes" + } + ], + "stateMutability": "view", + "type": "function" + } + ], + "bytecode": "0x", + "deployedBytecode": "0x", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/x/evm/embeds/contracts/IFunToken.sol b/x/evm/embeds/contracts/FunToken.sol similarity index 89% rename from x/evm/embeds/contracts/IFunToken.sol rename to x/evm/embeds/contracts/FunToken.sol index db1757f1b..73fb0ed7f 100644 --- a/x/evm/embeds/contracts/IFunToken.sol +++ b/x/evm/embeds/contracts/FunToken.sol @@ -14,4 +14,4 @@ interface IFunToken { address constant FUNTOKEN_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000800; -IFunToken constant FUNTOKEN_GATEWAY = IFunToken(FUNTOKEN_PRECOMPILE_ADDRESS); +IFunToken constant FUNTOKEN_PRECOMPILE = IFunToken(FUNTOKEN_PRECOMPILE_ADDRESS); diff --git a/x/evm/embeds/contracts/Wasm.sol b/x/evm/embeds/contracts/Wasm.sol new file mode 100644 index 000000000..8a5842062 --- /dev/null +++ b/x/evm/embeds/contracts/Wasm.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.19; + +address constant WASM_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000802; + +IWasm constant WASM_PRECOMPILE = IWasm(WASM_PRECOMPILE_ADDRESS); + +interface IWasm { + struct BankCoin { + string denom; + uint256 amount; + } + + /// @notice Invoke a contract's "ExecuteMsg", which corresponds to + /// "wasm/types/MsgExecuteContract". This enables arbitrary smart contract + /// execution using the Wasm VM from the EVM. + /// @param contractAddr nibi-prefixed Bech32 address of the wasm contract + /// @param msgArgs JSON encoded wasm execute invocation + /// @param funds Optional funds to supply during the execute call. It's + /// uncommon to use this field, so you'll pass an empty array most of the time. + /// @dev The three non-struct arguments are more gas efficient than encoding a + /// single argument as a WasmExecuteMsg. + function execute( + string memory contractAddr, + bytes memory msgArgs, + BankCoin[] memory funds + ) payable external returns (bytes memory response); + + struct WasmExecuteMsg { + string contractAddr; + bytes msgArgs; + BankCoin[] funds; + } + + /// @notice Identical to "execute", except for multiple contract calls. + function executeMulti( + WasmExecuteMsg[] memory executeMsgs + ) payable external returns (bytes[] memory responses); + + /// @notice Query the public API of another contract at a known address (with + /// known ABI). + /// Implements smart query, the "WasmQuery::Smart" variant from "cosmwas_std". + /// @param contractAddr nibi-prefixed Bech32 address of the wasm contract + /// @param req JSON encoded query request + /// @return response Returns whatever type the contract returns (caller should + /// know), wrapped in a JSON encoded contract result. + function query( + string memory contractAddr, + bytes memory req + ) external view returns (bytes memory response); + + /// @notice Query the raw kv-store of the contract. + /// Implements raw query, the "WasmQuery::Raw" variant from "cosmwas_std". + /// @param contractAddr nibi-prefixed Bech32 address of the wasm contract + /// @param key contract state key. For example, a `cw_storage_plus::Item` of + /// value `Item::new("state")` creates prefix store with key, "state". + /// @return response JSON encoded, raw data stored at that key. + function queryRaw( + string memory contractAddr, + bytes memory key + ) external view returns (bytes memory response); + + /// @notice InstantiateContract creates a new smart contract instance for the + /// given code id. + function instantiate( + string memory admin, + uint64 codeID, + bytes memory msgArgs, + string memory label, + BankCoin[] memory funds + ) payable external returns (string memory contractAddr, bytes memory data); + +} diff --git a/x/evm/embeds/embeds.go b/x/evm/embeds/embeds.go index 5c358c723..0103b78c1 100644 --- a/x/evm/embeds/embeds.go +++ b/x/evm/embeds/embeds.go @@ -17,8 +17,10 @@ import ( var ( //go:embed artifacts/contracts/ERC20Minter.sol/ERC20Minter.json erc20MinterContractJSON []byte - //go:embed artifacts/contracts/IFunToken.sol/IFunToken.json - funtokenContractJSON []byte + //go:embed artifacts/contracts/FunToken.sol/IFunToken.json + funtokenPrecompileJSON []byte + //go:embed artifacts/contracts/Wasm.sol/IWasm.json + wasmPrecompileJSON []byte //go:embed artifacts/contracts/TestERC20.sol/TestERC20.json testErc20Json []byte ) @@ -32,11 +34,19 @@ var ( } // SmartContract_Funtoken: Precompile contract interface for - // "IFunToken.sol". This precompile enables transfers of ERC20 tokens + // "FunToken.sol". This precompile enables transfers of ERC20 tokens // to non-EVM accounts. Only the ABI is used. SmartContract_FunToken = CompiledEvmContract{ Name: "FunToken.sol", - EmbedJSON: funtokenContractJSON, + EmbedJSON: funtokenPrecompileJSON, + } + + // SmartContract_Funtoken: Precompile contract interface for + // "Wasm.sol". This precompile enables contract invocations in the Wasm VM + // from EVM accounts. Only the ABI is used. + SmartContract_Wasm = CompiledEvmContract{ + Name: "Wasm.sol", + EmbedJSON: wasmPrecompileJSON, } SmartContract_TestERC20 = CompiledEvmContract{ @@ -48,6 +58,7 @@ var ( func init() { SmartContract_ERC20Minter.MustLoad() SmartContract_FunToken.MustLoad() + SmartContract_Wasm.MustLoad() SmartContract_TestERC20.MustLoad() } diff --git a/x/evm/evmtest/erc20.go b/x/evm/evmtest/erc20.go index c7f20cfce..ce020036f 100644 --- a/x/evm/evmtest/erc20.go +++ b/x/evm/evmtest/erc20.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" + "github.com/NibiruChain/nibiru/v2/eth" "github.com/NibiruChain/nibiru/v2/x/common/testutil/testapp" "github.com/NibiruChain/nibiru/v2/x/evm" ) @@ -86,3 +87,15 @@ func CreateFunTokenForBankCoin( return funtoken } + +func AssertBankBalanceEqual( + t *testing.T, + deps TestDeps, + denom string, + account gethcommon.Address, + expectedBalance *big.Int, +) { + bech32Addr := eth.EthAddrToNibiruAddr(account) + actualBalance := deps.App.BankKeeper.GetBalance(deps.Ctx, bech32Addr, denom).Amount.BigInt() + assert.Zero(t, expectedBalance.Cmp(actualBalance), "expected %s, got %s", expectedBalance, actualBalance) +} diff --git a/x/evm/precompile/errors.go b/x/evm/precompile/errors.go new file mode 100644 index 000000000..5f4ee88da --- /dev/null +++ b/x/evm/precompile/errors.go @@ -0,0 +1,45 @@ +package precompile + +import ( + "errors" + "fmt" + + gethabi "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/core/vm" +) + +// Error short-hand for type validation +func ErrArgTypeValidation(solidityHint string, arg any) error { + return fmt.Errorf("type validation failed for (%s) argument: %s", solidityHint, arg) +} + +// Error when parsing method arguments +func ErrInvalidArgs(err error) error { + return fmt.Errorf("invalid method args: %w", err) +} + +func ErrMethodCalled(method *gethabi.Method, wrapped error) error { + return fmt.Errorf("%s method called: %w", method.Name, wrapped) +} + +// Check required for transactions but not needed for queries +func assertNotReadonlyTx(readOnly bool, isTx bool) error { + if readOnly && isTx { + return errors.New("cannot write state from staticcall (a read-only call)") + } + return nil +} + +// assertContractQuery checks if a contract call is a valid query operation. This +// function verifies that no funds (wei) are being sent with the query, as query +// operations should be read-only and not involve any value transfer. +func assertContractQuery(contract *vm.Contract) error { + weiValue := contract.Value() + if weiValue != nil && weiValue.Sign() != 0 { + return fmt.Errorf( + "funds (wei value) must not be expended calling a query function; received wei value %s", weiValue, + ) + } + + return nil +} diff --git a/x/evm/precompile/funtoken.go b/x/evm/precompile/funtoken.go index 365cac8e1..6eaf1bbff 100644 --- a/x/evm/precompile/funtoken.go +++ b/x/evm/precompile/funtoken.go @@ -32,23 +32,24 @@ func (p precompileFunToken) Address() gethcommon.Address { return PrecompileAddr_FunToken } -func (p precompileFunToken) RequiredGas(input []byte) (gasPrice uint64) { +// RequiredGas calculates the cost of calling the precompile in gas units. +func (p precompileFunToken) RequiredGas(input []byte) (gasCost uint64) { // Since [gethparams.TxGas] is the cost per (Ethereum) transaction that does not create // a contract, it's value can be used to derive an appropriate value for the - // precompile call. The FunToken precompile performs 3 operations, labeld 1-3 + // precompile call. The FunToken precompile performs 3 operations, labeled 1-3 // below: // 0 | Call the precompile (already counted in gas calculation) // 1 | Send ERC20 to EVM. // 2 | Convert ERC20 to coin // 3 | Send coin to recipient. - return gethparams.TxGas * 3 + return gethparams.TxGas * 2 } const ( - FunTokenMethod_BankSend FunTokenMethod = "bankSend" + FunTokenMethod_BankSend PrecompileMethod = "bankSend" ) -type FunTokenMethod string +type PrecompileMethod string // Run runs the precompiled contract func (p precompileFunToken) Run( @@ -76,13 +77,12 @@ func (p precompileFunToken) Run( return nil, err } - switch FunTokenMethod(method.Name) { + switch PrecompileMethod(method.Name) { case FunTokenMethod_BankSend: - // TODO: UD-DEBUG: Test that calling non-method on the right address does - // nothing. bz, err = p.bankSend(ctx, contract.CallerAddress, method, args, readonly) default: - // TODO: UD-DEBUG: test invalid method called + // Note that this code path should be impossible to reach since + // "DecomposeInput" parses methods directly from the ABI. err = fmt.Errorf("invalid method called with name \"%s\"", method.Name) return } @@ -104,28 +104,27 @@ type precompileFunToken struct { var executionGuard sync.Mutex -/* -bankSend: Implements "IFunToken.bankSend" - -The "args" populate the following function signature in Solidity: -```solidity -/// @dev bankSend sends ERC20 tokens as coins to a Nibiru base account -/// @param erc20 the address of the ERC20 token contract -/// @param amount the amount of tokens to send -/// @param to the receiving Nibiru base account address as a string -function bankSend(address erc20, uint256 amount, string memory to) external; -``` -*/ +// bankSend: Implements "IFunToken.bankSend" +// +// The "args" populate the following function signature in Solidity: +// +// ```solidity +// /// @dev bankSend sends ERC20 tokens as coins to a Nibiru base account +// /// @param erc20 the address of the ERC20 token contract +// /// @param amount the amount of tokens to send +// /// @param to the receiving Nibiru base account address as a string +// function bankSend(address erc20, uint256 amount, string memory to) external; +// ``` func (p precompileFunToken) bankSend( ctx sdk.Context, caller gethcommon.Address, method *gethabi.Method, - args []interface{}, + args []any, readOnly bool, ) (bz []byte, err error) { - if readOnly { - // Check required for transactions but not needed for queries - return nil, fmt.Errorf("cannot write state from staticcall (a read-only call)") + if e := assertNotReadonlyTx(readOnly, true); e != nil { + err = e + return } if !executionGuard.TryLock() { return nil, fmt.Errorf("bankSend is already in progress") @@ -196,7 +195,6 @@ func (p precompileFunToken) bankSend( } // TODO: UD-DEBUG: feat: Emit EVM events - // TODO: UD-DEBUG: feat: Emit ABCI events return method.Outputs.Pack() } @@ -207,10 +205,9 @@ func (p precompileFunToken) decomposeBankSendArgs(args []any) ( to string, err error, ) { - if len(args) != 3 { - err = fmt.Errorf("expected 3 arguments but got %d", len(args)) - return - } + // Note: The number of arguments is valiated before this function is called + // during "DecomposeInput". DecomposeInput calls "method.Inputs.Unpack", + // which validates against the the structure of the precompile's ABI. erc20, ok := args[0].(gethcommon.Address) if !ok { diff --git a/x/evm/precompile/funtoken_test.go b/x/evm/precompile/funtoken_test.go index 31f9413af..64be0360f 100644 --- a/x/evm/precompile/funtoken_test.go +++ b/x/evm/precompile/funtoken_test.go @@ -17,7 +17,17 @@ import ( "github.com/NibiruChain/nibiru/v2/x/evm/precompile" ) -func (s *Suite) TestFailToPackABI() { +// TestSuite: Runs all the tests in the suite. +func TestSuite(t *testing.T) { + suite.Run(t, new(FuntokenSuite)) + suite.Run(t, new(WasmSuite)) +} + +type FuntokenSuite struct { + suite.Suite +} + +func (s *FuntokenSuite) TestFailToPackABI() { testcases := []struct { name string methodName string @@ -67,7 +77,7 @@ func (s *Suite) TestFailToPackABI() { } } -func (s *Suite) TestHappyPath() { +func (s *FuntokenSuite) TestHappyPath() { deps := evmtest.NewTestDeps() s.T().Log("Create FunToken mapping and ERC20") @@ -127,12 +137,3 @@ func (s *Suite) TestHappyPath() { deps.App.BankKeeper.GetBalance(deps.Ctx, randomAcc, funtoken.BankDenom).Amount, ) } - -type Suite struct { - suite.Suite -} - -// TestPrecompileSuite: Runs all the tests in the suite. -func TestSuite(t *testing.T) { - suite.Run(t, new(Suite)) -} diff --git a/x/evm/precompile/hello_world_counter.wasm b/x/evm/precompile/hello_world_counter.wasm new file mode 100644 index 0000000000000000000000000000000000000000..9a532ca445d02a86d802a63f2683d66ff2df08e0 GIT binary patch literal 203903 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 0 { + adminAddr = sdk.MustAccAddressFromBech32(txMsg.Admin) // validated in parse + } + contractAddr, data, err := p.Wasm.Instantiate( + ctx, txMsg.CodeID, callerBech32, adminAddr, txMsg.Msg, txMsg.Label, txMsg.Funds, + ) + if err != nil { + return + } + + return method.Outputs.Pack(contractAddr.String(), data) +} + +// executeMulti allows executing multiple Wasm contract calls in a single transaction. +// It corresponds to the "executeMulti" method in the IWasm interface. +// +// Implements "executeMulti" from evm/embeds/contracts/Wasm.sol: +// +// ```solidity +// /// @notice Identical to "execute", except for multiple contract calls. +// /// @param executeMsgs An array of WasmExecuteMsg structs, each containing: +// /// - contractAddr: nibi-prefixed Bech32 address of the wasm contract +// /// - msgArgs: JSON encoded wasm execute invocation +// /// - funds: Optional funds to supply during the execute call +// function executeMulti( +// WasmExecuteMsg[] memory executeMsgs +// ) payable external returns (bytes[] memory responses); +// ``` +func (p precompileWasm) executeMulti( + ctx sdk.Context, + caller gethcommon.Address, + method *gethabi.Method, + args []any, + readOnly bool, +) (bz []byte, err error) { + defer func() { + if err != nil { + err = ErrMethodCalled(method, err) + } + }() + if err := assertNotReadonlyTx(readOnly, true); err != nil { + return bz, err + } + + wasmExecMsgs, err := p.parseExecuteMultiArgs(args) + if err != nil { + err = ErrInvalidArgs(err) + return + } + callerBech32 := eth.EthAddrToNibiruAddr(caller) + + var responses [][]byte + for _, m := range wasmExecMsgs { + wasmContract, e := sdk.AccAddressFromBech32(m.ContractAddr) + if e != nil { + err = fmt.Errorf("Execute failed: %w", e) + return + } + var funds sdk.Coins + for _, fund := range m.Funds { + funds = append(funds, sdk.Coin{ + Denom: fund.Denom, + Amount: sdk.NewIntFromBigInt(fund.Amount), + }) + } + respBz, e := p.Wasm.Execute(ctx, wasmContract, callerBech32, m.MsgArgs, funds) + if e != nil { + err = e + return + } + responses = append(responses, respBz) + } + return method.Outputs.Pack(responses) +} + +// queryRaw queries the raw key-value store of a Wasm contract. This implements +// the 'queryRaw' method of Wasm.sol: +// +// ```solidity +// function queryRaw( +// string memory contractAddr, +// bytes memory key +// ) external view returns (bytes memory response); +// ``` +// +// Parameters: +// - ctx: The SDK context for the query +// - method: The ABI method being called +// - args: The arguments passed to the method +// - contract: The EVM contract context +// +// Returns: +// - bz: The encoded raw data stored at the queried key +// - err: Any error that occurred during the query +func (p precompileWasm) queryRaw( + ctx sdk.Context, + method *gethabi.Method, + args []any, + contract *vm.Contract, +) (bz []byte, err error) { + defer func() { + if err != nil { + err = ErrMethodCalled(method, err) + } + }() + if err := assertContractQuery(contract); err != nil { + return bz, err + } + + // Note: The number of arguments is valiated before this function is called + // during "DecomposeInput". DecomposeInput calls "method.Inputs.Unpack", + // which validates against the the structure of the precompile's ABI. + + argIdx := 0 + wasmContract, e := parseContractAddrArg(args[argIdx]) + if e != nil { + err = e + return + } + + argIdx++ + key, ok := args[argIdx].([]byte) + if !ok { + err = ErrArgTypeValidation("bytes req", args[argIdx]) + return + } + + respBz := p.Wasm.QueryRaw(ctx, wasmContract, []byte(key)) + return method.Outputs.Pack(respBz) +} diff --git a/x/evm/precompile/wasm_parse.go b/x/evm/precompile/wasm_parse.go new file mode 100644 index 000000000..2f447c340 --- /dev/null +++ b/x/evm/precompile/wasm_parse.go @@ -0,0 +1,242 @@ +package precompile + +import ( + "fmt" + "math/big" + + sdk "github.com/cosmos/cosmos-sdk/types" + + wasm "github.com/CosmWasm/wasmd/x/wasm/types" +) + +// WasmBankCoin is a naked struct for the "BankCoin" type from Wasm.sol. +// The ABI parser requires an unnamed strict, so this type is only used in tests. +type WasmBankCoin struct { + Denom string `json:"denom"` + Amount *big.Int `json:"amount"` +} + +func parseSdkCoins(unparsed []struct { + Denom string `json:"denom"` + Amount *big.Int `json:"amount"` +}, +) sdk.Coins { + parsed := sdk.Coins{} + for _, coin := range unparsed { + parsed = append( + parsed, + // Favor the sdk.Coin constructor over sdk.NewCoin because sdk.NewCoin + // is not panic-safe. Validation will be handled when the coin is used + // as an argument during the execution of a transaction. + sdk.Coin{ + Denom: coin.Denom, + Amount: sdk.NewIntFromBigInt(coin.Amount), + }, + ) + } + return parsed +} + +// Parses [sdk.Coins] from a "BankCoin[]" solidity argument: +// +// ```solidity +// BankCoin[] memory funds +// ``` +func parseFundsArg(arg any) (funds sdk.Coins, err error) { + bankCoinsUnparsed, ok := arg.([]struct { + Denom string `json:"denom"` + Amount *big.Int `json:"amount"` + }) + switch { + case arg == nil: + bankCoinsUnparsed = []struct { + Denom string `json:"denom"` + Amount *big.Int `json:"amount"` + }{} + case !ok: + err = ErrArgTypeValidation("BankCoin[] funds", arg) + return + case ok: + // Type assertion succeeded + } + funds = parseSdkCoins(bankCoinsUnparsed) + return +} + +// Parses [sdk.AccAddress] from a "string" solidity argument: +func parseContractAddrArg(arg any) (addr sdk.AccAddress, err error) { + addrStr, ok := arg.(string) + if !ok { + err = ErrArgTypeValidation("string contractAddr", arg) + return + } + addr, err = sdk.AccAddressFromBech32(addrStr) + if err != nil { + err = fmt.Errorf("%w: %s", + ErrArgTypeValidation("string contractAddr", arg), err, + ) + return + } + return addr, nil +} + +func (p precompileWasm) parseInstantiateArgs(args []any, sender string) ( + txMsg wasm.MsgInstantiateContract, + err error, +) { + // Note: The number of arguments is valiated before this function is called + // during "DecomposeInput". DecomposeInput calls "method.Inputs.Unpack", + // which validates against the the structure of the precompile's ABI. + + argIdx := 0 + admin, ok := args[argIdx].(string) + if !ok { + err = ErrArgTypeValidation("string admin", args[argIdx]) + return + } + + argIdx++ + codeID, ok := args[argIdx].(uint64) + if !ok { + err = ErrArgTypeValidation("uint64 codeID", args[argIdx]) + return + } + + argIdx++ + msgArgs, ok := args[argIdx].([]byte) + if !ok { + err = ErrArgTypeValidation("bytes msgArgs", args[argIdx]) + return + } + + argIdx++ + label, ok := args[argIdx].(string) + if !ok { + err = ErrArgTypeValidation("string label", args[argIdx]) + return + } + + argIdx++ + funds, e := parseFundsArg(args[argIdx]) + if e != nil { + err = e + return + } + + txMsg = wasm.MsgInstantiateContract{ + Sender: sender, + CodeID: codeID, + Label: label, + Msg: msgArgs, + Funds: funds, + } + if len(admin) > 0 { + txMsg.Admin = admin + } + return txMsg, txMsg.ValidateBasic() +} + +func (p precompileWasm) parseExecuteArgs(args []any) ( + wasmContract sdk.AccAddress, + msgArgs []byte, + funds sdk.Coins, + err error, +) { + // Note: The number of arguments is valiated before this function is called + // during "DecomposeInput". DecomposeInput calls "method.Inputs.Unpack", + // which validates against the the structure of the precompile's ABI. + + argIdx := 0 + contractAddrStr, ok := args[argIdx].(string) + if !ok { + err = ErrArgTypeValidation("string contractAddr", args[argIdx]) + return + } + contractAddr, err := sdk.AccAddressFromBech32(contractAddrStr) + if err != nil { + err = fmt.Errorf("%w: %s", + ErrArgTypeValidation("string contractAddr", args[argIdx]), err, + ) + return + } + + argIdx++ + msgArgs, ok = args[argIdx].([]byte) + if !ok { + err = ErrArgTypeValidation("bytes msgArgs", args[argIdx]) + return + } + msgArgsCopy := wasm.RawContractMessage(msgArgs) + if e := msgArgsCopy.ValidateBasic(); e != nil { + err = ErrArgTypeValidation(e.Error(), args[argIdx]) + return + } + + argIdx++ + funds, e := parseFundsArg(args[argIdx]) + if e != nil { + err = ErrArgTypeValidation(e.Error(), args[argIdx]) + return + } + + return contractAddr, msgArgs, funds, nil +} + +func (p precompileWasm) parseQueryArgs(args []any) ( + wasmContract sdk.AccAddress, + req wasm.RawContractMessage, + err error, +) { + // Note: The number of arguments is valiated before this function is called + // during "DecomposeInput". DecomposeInput calls "method.Inputs.Unpack", + // which validates against the the structure of the precompile's ABI. + + argsIdx := 0 + wasmContract, e := parseContractAddrArg(args[argsIdx]) + if e != nil { + err = e + return + } + + argsIdx++ + reqBz := args[argsIdx].([]byte) + req = wasm.RawContractMessage(reqBz) + if e := req.ValidateBasic(); e != nil { + err = e + return + } + + return wasmContract, req, nil +} + +func (p precompileWasm) parseExecuteMultiArgs(args []any) ( + wasmExecMsgs []struct { + ContractAddr string `json:"contractAddr"` + MsgArgs []byte `json:"msgArgs"` + Funds []struct { + Denom string `json:"denom"` + Amount *big.Int `json:"amount"` + } `json:"funds"` + }, + err error, +) { + // Note: The number of arguments is valiated before this function is called + // during "DecomposeInput". DecomposeInput calls "method.Inputs.Unpack", + // which validates against the the structure of the precompile's ABI. + + arg := args[0] + execMsgs, ok := arg.([]struct { + ContractAddr string `json:"contractAddr"` + MsgArgs []byte `json:"msgArgs"` + Funds []struct { + Denom string `json:"denom"` + Amount *big.Int `json:"amount"` + } `json:"funds"` + }) + if !ok { + err = ErrArgTypeValidation("BankCoin[] funds", arg) + return + } + + return execMsgs, nil +} diff --git a/x/evm/precompile/wasm_test.go b/x/evm/precompile/wasm_test.go new file mode 100644 index 000000000..6db1d7642 --- /dev/null +++ b/x/evm/precompile/wasm_test.go @@ -0,0 +1,587 @@ +package precompile_test + +import ( + "encoding/json" + "fmt" + "math/big" + "os" + + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + wasm "github.com/CosmWasm/wasmd/x/wasm/types" + + "github.com/NibiruChain/nibiru/v2/app" + "github.com/NibiruChain/nibiru/v2/x/common/testutil" + "github.com/NibiruChain/nibiru/v2/x/evm/embeds" + "github.com/NibiruChain/nibiru/v2/x/evm/evmtest" + "github.com/NibiruChain/nibiru/v2/x/evm/precompile" + tokenfactory "github.com/NibiruChain/nibiru/v2/x/tokenfactory/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/suite" +) + +type WasmSuite struct { + suite.Suite +} + +// SetupWasmContracts stores all Wasm bytecode and has the "deps.Sender" +// instantiate each Wasm contract using the precompile. +func SetupWasmContracts(deps *evmtest.TestDeps, s *suite.Suite) ( + contracts []sdk.AccAddress, +) { + wasmCodes := DeployWasmBytecode(s, deps.Ctx, deps.Sender.NibiruAddr, deps.App) + + otherArgs := []struct { + InstMsg []byte + Label string + }{ + { + InstMsg: []byte("{}"), + Label: "https://github.com/NibiruChain/nibiru-wasm/blob/main/contracts/nibi-stargate/src/contract.rs", + }, + { + InstMsg: []byte(`{"count": 0}`), + Label: "https://github.com/NibiruChain/nibiru-wasm/tree/ec3ab9f09587a11fbdfbd4021c7617eca3912044/contracts/00-hello-world-counter", + }, + } + + for wasmCodeIdx, wasmCode := range wasmCodes { + s.T().Logf("Instantiate using Wasm precompile: %s", wasmCode.binPath) + codeId := wasmCode.codeId + + m := wasm.MsgInstantiateContract{ + Admin: "", + CodeID: codeId, + Label: otherArgs[wasmCodeIdx].Label, + Msg: otherArgs[wasmCodeIdx].InstMsg, + Funds: []sdk.Coin{}, + } + + msgArgsBz, err := json.Marshal(m.Msg) + s.NoError(err) + + var funds []precompile.WasmBankCoin + fundsJson, err := m.Funds.MarshalJSON() + s.NoErrorf(err, "fundsJson: %s", fundsJson) + err = json.Unmarshal(fundsJson, &funds) + s.Require().NoError(err) + + callArgs := []any{m.Admin, m.CodeID, msgArgsBz, m.Label, funds} + input, err := embeds.SmartContract_Wasm.ABI.Pack( + string(precompile.WasmMethod_instantiate), + callArgs..., + ) + s.Require().NoError(err) + + ethTxResp, err := deps.EvmKeeper.CallContractWithInput( + deps.Ctx, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, true, input, + ) + s.Require().NoError(err) + s.Require().NotEmpty(ethTxResp.Ret) + + s.T().Log("Parse the response contract addr and response bytes") + var contractAddrStr string + var data []byte + err = embeds.SmartContract_Wasm.ABI.UnpackIntoInterface( + &[]any{&contractAddrStr, &data}, + string(precompile.WasmMethod_instantiate), + ethTxResp.Ret, + ) + s.Require().NoError(err) + contractAddr, err := sdk.AccAddressFromBech32(contractAddrStr) + s.NoError(err) + contracts = append(contracts, contractAddr) + } + + return contracts +} + +// DeployWasmBytecode is a setup function that stores all Wasm bytecode used in +// the test suite. +func DeployWasmBytecode( + s *suite.Suite, + ctx sdk.Context, + sender sdk.AccAddress, + nibiru *app.NibiruApp, +) (codeIds []struct { + codeId uint64 + binPath string +}, +) { + for _, pathToWasmBin := range []string{ + // nibi_stargate.wasm is a compiled version of: + // https://github.com/NibiruChain/nibiru-wasm/blob/main/contracts/nibi-stargate/src/contract.rs + "../../tokenfactory/fixture/nibi_stargate.wasm", + + // hello_world_counter.wasm is a compiled version of: + // https://github.com/NibiruChain/nibiru-wasm/tree/ec3ab9f09587a11fbdfbd4021c7617eca3912044/contracts/00-hello-world-counter + "./hello_world_counter.wasm", + + // Add other wasm bytecode here if needed... + } { + wasmBytecode, err := os.ReadFile(pathToWasmBin) + s.Require().NoError(err) + + // The "Create" fn is private on the nibiru.WasmKeeper. By placing it as the + // decorated keeper in PermissionedKeeper type, we can access "Create" as a + // public fn. + wasmPermissionedKeeper := wasmkeeper.NewDefaultPermissionKeeper(nibiru.WasmKeeper) + instantiateAccess := &wasm.AccessConfig{ + Permission: wasm.AccessTypeEverybody, + } + codeId, _, err := wasmPermissionedKeeper.Create( + ctx, sender, wasmBytecode, instantiateAccess, + ) + s.Require().NoError(err) + codeIds = append(codeIds, struct { + codeId uint64 + binPath string + }{codeId, pathToWasmBin}) + } + + return codeIds +} + +func (s *WasmSuite) TestExecuteHappy() { + deps := evmtest.NewTestDeps() + wasmContracts := SetupWasmContracts(&deps, &s.Suite) + wasmContract := wasmContracts[0] // nibi_stargate.wasm + + s.T().Log("Execute: create denom") + msgArgsBz := []byte(` + { "create_denom": { + "subdenom": "ETH" + } + } + `) + + var funds []precompile.WasmBankCoin + fundsJson, err := json.Marshal(funds) + s.NoErrorf(err, "fundsJson: %s", fundsJson) + err = json.Unmarshal(fundsJson, &funds) + s.Require().NoError(err, "fundsJson %s, funds %s", fundsJson, funds) + + callArgs := []any{ + wasmContract.String(), + msgArgsBz, + funds, + } + input, err := embeds.SmartContract_Wasm.ABI.Pack( + string(precompile.WasmMethod_execute), + callArgs..., + ) + s.Require().NoError(err) + + ethTxResp, err := deps.EvmKeeper.CallContractWithInput( + deps.Ctx, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, true, input, + ) + s.Require().NoError(err) + s.Require().NotEmpty(ethTxResp.Ret) + + s.T().Log("Execute: mint tokens") + coinDenom := tokenfactory.TFDenom{ + Creator: wasmContract.String(), + Subdenom: "ETH", + }.Denom().String() + msgArgsBz = []byte(fmt.Sprintf(` + { + "mint": { + "coin": { "amount": "69420", "denom": "%s" }, + "mint_to": "%s" + } + } + `, coinDenom, deps.Sender.NibiruAddr)) + callArgs = []any{ + wasmContract.String(), + msgArgsBz, + funds, + } + input, err = embeds.SmartContract_Wasm.ABI.Pack( + string(precompile.WasmMethod_execute), + callArgs..., + ) + s.Require().NoError(err) + ethTxResp, err = deps.EvmKeeper.CallContractWithInput( + deps.Ctx, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, true, input, + ) + s.Require().NoError(err) + s.Require().NotEmpty(ethTxResp.Ret) + evmtest.AssertBankBalanceEqual( + s.T(), deps, coinDenom, deps.Sender.EthAddr, big.NewInt(69420), + ) +} + +// Result of QueryMsg::Count from the [hello_world_counter] Wasm contract: +// +// ```rust +// #[cw_serde] +// pub struct State { +// pub count: i64, +// pub owner: Addr, +// } +// ``` +// +// [hello_world_counter]: https://github.com/NibiruChain/nibiru-wasm/tree/ec3ab9f09587a11fbdfbd4021c7617eca3912044/contracts/00-hello-world-counter +type QueryMsgCountResp struct { + Count int64 `json:"count"` + Owner string `json:"owner"` +} + +func (s *WasmSuite) TestExecuteMultiHappy() { + deps := evmtest.NewTestDeps() + wasmContracts := SetupWasmContracts(&deps, &s.Suite) + wasmContract := wasmContracts[1] // hello_world_counter.wasm + + s.assertWasmCounterState(deps, wasmContract, 0) // count = 0 + s.incrementWasmCounterWithExecuteMulti(&deps, wasmContract, 2) // count += 2 + s.assertWasmCounterState(deps, wasmContract, 2) // count = 2 + s.assertWasmCounterStateRaw(deps, wasmContract, 2) + s.incrementWasmCounterWithExecuteMulti(&deps, wasmContract, 67) // count += 67 + s.assertWasmCounterState(deps, wasmContract, 69) // count = 69 + s.assertWasmCounterStateRaw(deps, wasmContract, 69) +} + +// From IWasm.query of Wasm.sol: +// +// ```solidity +// function query( +// string memory contractAddr, +// bytes memory req +// ) external view returns (bytes memory response); +// ``` +func (s *WasmSuite) assertWasmCounterState( + deps evmtest.TestDeps, + wasmContract sdk.AccAddress, + wantCount int64, +) { + msgArgsBz := []byte(` + { + "count": {} + } + `) + + callArgs := []any{ + // string memory contractAddr + wasmContract.String(), + // bytes memory req + msgArgsBz, + } + input, err := embeds.SmartContract_Wasm.ABI.Pack( + string(precompile.WasmMethod_query), + callArgs..., + ) + s.Require().NoError(err) + + ethTxResp, err := deps.EvmKeeper.CallContractWithInput( + deps.Ctx, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, true, input, + ) + s.Require().NoError(err) + s.Require().NotEmpty(ethTxResp.Ret) + + s.T().Log("Parse the response contract addr and response bytes") + s.T().Logf("ethTxResp.Ret: %s", ethTxResp.Ret) + var queryResp []byte + err = embeds.SmartContract_Wasm.ABI.UnpackIntoInterface( + // Since there's only one return value, don't unpack as a slice. + // If there were two or more return values, we'd use + // &[]any{...} + &queryResp, + string(precompile.WasmMethod_query), + ethTxResp.Ret, + ) + s.Require().NoError(err) + s.T().Logf("queryResp: %s", queryResp) + + s.T().Log("Response is a JSON-encoded struct from the Wasm contract") + var wasmMsg wasm.RawContractMessage + err = json.Unmarshal(queryResp, &wasmMsg) + s.NoError(err) + s.NoError(wasmMsg.ValidateBasic()) + var typedResp QueryMsgCountResp + err = json.Unmarshal(wasmMsg, &typedResp) + s.NoError(err) + + s.EqualValues(wantCount, typedResp.Count) + s.EqualValues(deps.Sender.NibiruAddr.String(), typedResp.Owner) +} + +// From evm/embeds/contracts/Wasm.sol: +// +// ```solidity +// struct WasmExecuteMsg { +// string contractAddr; +// bytes msgArgs; +// BankCoin[] funds; +// } +// +// /// @notice Identical to "execute", except for multiple contract calls. +// function executeMulti( +// WasmExecuteMsg[] memory executeMsgs +// ) payable external returns (bytes[] memory responses); +// ``` +// +// The increment call corresponds to the ExecuteMsg from +// the [hello_world_counter] Wasm contract: +// +// ```rust +// #[cw_serde] +// pub enum ExecuteMsg { +// Increment {}, // Increase count by 1 +// Reset { count: i64 }, // Reset to any i64 value +// } +// ``` +// +// [hello_world_counter]: https://github.com/NibiruChain/nibiru-wasm/tree/ec3ab9f09587a11fbdfbd4021c7617eca3912044/contracts/00-hello-world-counter +func (s *WasmSuite) incrementWasmCounterWithExecuteMulti( + deps *evmtest.TestDeps, + wasmContract sdk.AccAddress, + times uint, +) { + msgArgsBz := []byte(` + { + "increment": {} + } + `) + + // Parse funds argument. + var funds []precompile.WasmBankCoin // blank funds + fundsJson, err := json.Marshal(funds) + s.NoErrorf(err, "fundsJson: %s", fundsJson) + err = json.Unmarshal(fundsJson, &funds) + s.Require().NoError(err, "fundsJson %s, funds %s", fundsJson, funds) + + // The "times" arg determines the number of messages in the executeMsgs slice + executeMsgs := []struct { + ContractAddr string `json:"contractAddr"` + MsgArgs []byte `json:"msgArgs"` + Funds []precompile.WasmBankCoin `json:"funds"` + }{ + {wasmContract.String(), msgArgsBz, funds}, + } + if times == 0 { + executeMsgs = executeMsgs[:0] // force empty + } else { + for i := uint(1); i < times; i++ { + executeMsgs = append(executeMsgs, executeMsgs[0]) + } + } + s.Require().Len(executeMsgs, int(times)) // sanity check assertion + + callArgs := []any{ + executeMsgs, + } + input, err := embeds.SmartContract_Wasm.ABI.Pack( + string(precompile.WasmMethod_executeMulti), + callArgs..., + ) + s.Require().NoError(err) + + ethTxResp, err := deps.EvmKeeper.CallContractWithInput( + deps.Ctx, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, true, input, + ) + s.Require().NoError(err) + s.Require().NotEmpty(ethTxResp.Ret) +} + +// From IWasm.query of Wasm.sol: +// +// ```solidity +// function queryRaw( +// string memory contractAddr, +// bytes memory key +// ) external view returns (bytes memory response); +// ``` +func (s *WasmSuite) assertWasmCounterStateRaw( + deps evmtest.TestDeps, + wasmContract sdk.AccAddress, + wantCount int64, +) { + keyBz := []byte(`state`) + callArgs := []any{ + wasmContract.String(), + keyBz, + } + input, err := embeds.SmartContract_Wasm.ABI.Pack( + string(precompile.WasmMethod_queryRaw), + callArgs..., + ) + s.Require().NoError(err) + + ethTxResp, err := deps.EvmKeeper.CallContractWithInput( + deps.Ctx, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, true, input, + ) + s.Require().NoError(err) + s.Require().NotEmpty(ethTxResp.Ret) + + s.T().Log("Parse the response contract addr and response bytes") + s.T().Logf("ethTxResp.Ret: %s", ethTxResp.Ret) + + var queryResp []byte + err = embeds.SmartContract_Wasm.ABI.UnpackIntoInterface( + &queryResp, + string(precompile.WasmMethod_queryRaw), + ethTxResp.Ret, + ) + s.Require().NoError(err) + s.T().Logf("queryResp: %s", queryResp) + + var wasmMsg wasm.RawContractMessage + s.NoError(wasmMsg.UnmarshalJSON(queryResp), queryResp) + s.T().Logf("wasmMsg: %s", wasmMsg) + s.NoError(wasmMsg.ValidateBasic()) + + var typedResp QueryMsgCountResp + s.NoError(json.Unmarshal(wasmMsg, &typedResp)) + s.EqualValues(wantCount, typedResp.Count) + s.EqualValues(deps.Sender.NibiruAddr.String(), typedResp.Owner) +} + +func (s *WasmSuite) TestSadArgsCount() { + nonsenseArgs := []any{"nonsense", "args here", "to see if", "precompile is", "called"} + testcases := []struct { + name string + methodName precompile.PrecompileMethod + callArgs []any + wantError string + }{ + { + name: "execute", + methodName: precompile.WasmMethod_execute, + callArgs: nonsenseArgs, + wantError: "argument count mismatch: got 5 for 3", + }, + { + name: "executeMulti", + methodName: precompile.WasmMethod_executeMulti, + callArgs: nonsenseArgs, + wantError: "argument count mismatch: got 5 for 1", + }, + { + name: "query", + methodName: precompile.WasmMethod_query, + callArgs: nonsenseArgs, + wantError: "argument count mismatch: got 5 for 2", + }, + { + name: "queryRaw", + methodName: precompile.WasmMethod_queryRaw, + callArgs: nonsenseArgs, + wantError: "argument count mismatch: got 5 for 2", + }, + { + name: "instantiate", + methodName: precompile.WasmMethod_instantiate, + callArgs: nonsenseArgs[:4], + wantError: "argument count mismatch: got 4 for 5", + }, + { + name: "invalid method name", + methodName: "not_a_method", + callArgs: nonsenseArgs, + wantError: "method 'not_a_method' not found", + }, + } + + abi := embeds.SmartContract_Wasm.ABI + for _, tc := range testcases { + s.Run(tc.name, func() { + callArgs := tc.callArgs + _, err := abi.Pack( + string(tc.methodName), + callArgs..., + ) + s.Require().ErrorContains(err, tc.wantError) + }) + } +} + +func (s *WasmSuite) TestSadArgsExecute() { + methodName := precompile.WasmMethod_execute + contractAddr := testutil.AccAddress().String() + wasmContractMsg := []byte(` + { "create_denom": { + "subdenom": "ETH" + } + } + `) + { + wasmMsg := wasm.RawContractMessage(wasmContractMsg) + s.Require().NoError(wasmMsg.ValidateBasic()) + } + + testcases := []struct { + name string + methodName precompile.PrecompileMethod + callArgs []any + wantError string + }{ + { + name: "valid arg types, should get VM error", + methodName: methodName, + callArgs: []any{ + // contractAddr + contractAddr, + // msgArgBz + wasmContractMsg, + // funds + []precompile.WasmBankCoin{}, + }, + wantError: "execute method called", + }, + { + name: "contractAddr", + methodName: methodName, + callArgs: []any{ + // contractAddr + contractAddr + "malformed", // mess up bech32 + // msgArgBz + wasmContractMsg, + // funds + []precompile.WasmBankCoin{}, + }, + wantError: "decoding bech32 failed", + }, + { + name: "funds populated", + methodName: methodName, + callArgs: []any{ + // contractAddr + contractAddr, + // msgArgBz + []byte(`[]`), + // funds + []precompile.WasmBankCoin{ + { + Denom: "x-123a!$", + Amount: big.NewInt(123), + }, + { + Denom: "xyz", + Amount: big.NewInt(456), + }, + }, + }, + wantError: "no such contract", + }, + } + + abi := embeds.SmartContract_Wasm.ABI + for _, tc := range testcases { + s.Run(tc.name, func() { + deps := evmtest.NewTestDeps() + + callArgs := tc.callArgs + input, err := abi.Pack( + string(tc.methodName), + callArgs..., + ) + s.Require().NoError(err) + + ethTxResp, err := deps.EvmKeeper.CallContractWithInput( + deps.Ctx, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, true, input, + ) + s.ErrorContains(err, tc.wantError) + s.Require().Nil(ethTxResp) + }) + } +} diff --git a/x/tokenfactory/fixture/fixture.go b/x/tokenfactory/fixture/fixture.go index 5a16f682c..cdaca74d2 100644 --- a/x/tokenfactory/fixture/fixture.go +++ b/x/tokenfactory/fixture/fixture.go @@ -1,6 +1,7 @@ package fixture const ( - // WASM_NIBI_STARGATE is a compiled version of: https://github.com/NibiruChain/cw-nibiru/blob/main/contracts/nibi-stargate/src/contract.rs + // WASM_NIBI_STARGATE is a compiled version of: + // https://github.com/NibiruChain/nibiru-wasm/blob/main/contracts/nibi-stargate/src/contract.rs WASM_NIBI_STARGATE = "nibi_stargate.wasm" ) From fa62d3550fad58d68eda149204c2463255e552c8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 22:57:00 -0500 Subject: [PATCH 13/16] chore(deps): bump bufbuild/buf-setup-action from 1.42.0 to 1.43.0 (#2057) Bumps [bufbuild/buf-setup-action](https://github.com/bufbuild/buf-setup-action) from 1.42.0 to 1.43.0. - [Release notes](https://github.com/bufbuild/buf-setup-action/releases) - [Commits](https://github.com/bufbuild/buf-setup-action/compare/v1.42.0...v1.43.0) --- updated-dependencies: - dependency-name: bufbuild/buf-setup-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Unique Divine <51418232+Unique-Divine@users.noreply.github.com> --- .github/workflows/proto-lint.yml | 4 ++-- CHANGELOG.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/proto-lint.yml b/.github/workflows/proto-lint.yml index 7140276e5..11b1b9e4a 100644 --- a/.github/workflows/proto-lint.yml +++ b/.github/workflows/proto-lint.yml @@ -22,7 +22,7 @@ jobs: # timeout-minutes: 5 # steps: # - uses: actions/checkout@v4 - # - uses: bufbuild/buf-setup-action@v1.42.0 + # - uses: bufbuild/buf-setup-action@v1.43.0 # - uses: bufbuild/buf-lint-action@v1 # with: # input: "proto" @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: bufbuild/buf-setup-action@v1.42.0 + - uses: bufbuild/buf-setup-action@v1.43.0 with: github_token: ${{ github.token }} - uses: bufbuild/buf-breaking-action@v1 diff --git a/CHANGELOG.md b/CHANGELOG.md index c04678690..194df558a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -176,7 +176,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump `github.com/hashicorp/go-getter` from 1.7.1 to 1.7.5 ([#1858](https://github.com/NibiruChain/nibiru/pull/1858), [#1938](https://github.com/NibiruChain/nibiru/pull/1938)) - Bump `github.com/btcsuite/btcd` from 0.23.3 to 0.24.0 ([#1862](https://github.com/NibiruChain/nibiru/pull/1862)) - Bump `pozetroninc/github-action-get-latest-release` from 0.7.0 to 0.8.0 ([#1863](https://github.com/NibiruChain/nibiru/pull/1863)) -- Bump `bufbuild/buf-setup-action` from 1.30.1 to 1.42.0 ([#1891](https://github.com/NibiruChain/nibiru/pull/1891), [#1900](https://github.com/NibiruChain/nibiru/pull/1900), [#1923](https://github.com/NibiruChain/nibiru/pull/1923), [#1972](https://github.com/NibiruChain/nibiru/pull/1972), [#1974](https://github.com/NibiruChain/nibiru/pull/1974), [#1988](https://github.com/NibiruChain/nibiru/pull/1988), [#2043](https://github.com/NibiruChain/nibiru/pull/2043)) +- Bump `bufbuild/buf-setup-action` from 1.30.1 to 1.43.0 ([#1891](https://github.com/NibiruChain/nibiru/pull/1891), [#1900](https://github.com/NibiruChain/nibiru/pull/1900), [#1923](https://github.com/NibiruChain/nibiru/pull/1923), [#1972](https://github.com/NibiruChain/nibiru/pull/1972), [#1974](https://github.com/NibiruChain/nibiru/pull/1974), [#1988](https://github.com/NibiruChain/nibiru/pull/1988), [#2043](https://github.com/NibiruChain/nibiru/pull/2043), [#2057](https://github.com/NibiruChain/nibiru/pull/2057)) - Bump `axios` from 1.7.3 to 1.7.4 ([#2016](https://github.com/NibiruChain/nibiru/pull/2016)) ## [v1.5.0](https://github.com/NibiruChain/nibiru/releases/tag/v1.5.0) - 2024-06-21 From 629aea39d5527dcaeafd4b6f6472da3b8649c8aa Mon Sep 17 00:00:00 2001 From: Oleg Nikonychev Date: Thu, 3 Oct 2024 16:31:50 +0400 Subject: [PATCH 14/16] refactor(evm): converted untyped event to typed and cleaned up (#2053) * refactor(evm): made all eth events typed * refactor(evm): tests for evm events upgraded * chore: cleanup * chore: cleanup * chore: cleanup * chore: cosmetic fixes --------- Co-authored-by: Unique Divine <51418232+Unique-Divine@users.noreply.github.com> --- CHANGELOG.md | 1 + app/evmante/evmante_emit_event.go | 25 +- app/evmante/evmante_emit_event_test.go | 6 +- eth/indexer/evm_tx_indexer.go | 2 +- eth/indexer/evm_tx_indexer_test.go | 83 +++--- eth/rpc/backend/account_info_test.go | 2 +- eth/rpc/backend/backend_suite_test.go | 5 +- eth/rpc/backend/blocks.go | 16 +- eth/rpc/backend/tx_info.go | 10 +- eth/rpc/backend/utils.go | 29 +- eth/rpc/events.go | 265 ------------------ eth/rpc/events_parser.go | 176 ++++++++++++ eth/rpc/events_parser_test.go | 201 ++++++++++++++ eth/rpc/events_test.go | 193 ------------- eth/rpc/rpcapi/eth_api_test.go | 32 ++- eth/rpc/rpcapi/eth_filters_api.go | 2 +- eth/rpc/rpcapi/filter_utils.go | 15 +- proto/eth/evm/v1/events.proto | 10 - x/evm/events.go | 115 +++++++- x/evm/events.pb.go | 371 +++---------------------- x/evm/keeper/msg_server.go | 182 ++++++------ x/evm/msg.go | 11 +- x/evm/msg_test.go | 1 - 23 files changed, 730 insertions(+), 1023 deletions(-) delete mode 100644 eth/rpc/events.go create mode 100644 eth/rpc/events_parser.go create mode 100644 eth/rpc/events_parser_test.go delete mode 100644 eth/rpc/events_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 194df558a..737577fb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -123,6 +123,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#2039](https://github.com/NibiruChain/nibiru/pull/2039) - refactor(rpc-backend): remove unnecessary interface code - [#2044](https://github.com/NibiruChain/nibiru/pull/2044) - feat(evm): evm tx indexer service implemented - [#2045](https://github.com/NibiruChain/nibiru/pull/2045) - test(evm): backend tests with test network and real txs +- [#2053](https://github.com/NibiruChain/nibiru/pull/2053) - refactor(evm): converted untyped event to typed and cleaned up - [#2054](https://github.com/NibiruChain/nibiru/pull/2054) - feat(evm-precompile): Precompile for one-way EVM calls to invoke/execute Wasm contracts. #### Dapp modules: perp, spot, oracle, etc diff --git a/app/evmante/evmante_emit_event.go b/app/evmante/evmante_emit_event.go index 01e178d6f..a9c656284 100644 --- a/app/evmante/evmante_emit_event.go +++ b/app/evmante/evmante_emit_event.go @@ -41,24 +41,17 @@ func (eeed EthEmitEventDecorator) AnteHandle( msg, (*evm.MsgEthereumTx)(nil), ) } - - // emit ethereum tx hash as an event so that it can be indexed by - // Tendermint for query purposes it's emitted in ante handler, so we can - // query failed transaction (out of block gas limit). + // Untyped event "pending_ethereum_tx" is emitted for then indexing purposes. + // Tendermint tx_search can only search the untyped events. + // TxHash and TxIndex values are exposed in the ante handler (before the actual tx execution) + // to allow searching for txs which are failed due to "out of block gas limit" error. ctx.EventManager().EmitEvent( sdk.NewEvent( - evm.EventTypeEthereumTx, - sdk.NewAttribute(evm.AttributeKeyEthereumTxHash, msgEthTx.Hash), - sdk.NewAttribute( - evm.AttributeKeyTxIndex, strconv.FormatUint(txIndex+uint64(i), - 10, - ), - ), // #nosec G701 - // TODO: fix: It's odd that each event is emitted twice. Migrate to typed - // events and change EVM indexer to align. - // sdk.NewAttribute("emitted_from", "EthEmitEventDecorator"), - )) + evm.PendingEthereumTxEvent, + sdk.NewAttribute(evm.PendingEthereumTxEventAttrEthHash, msgEthTx.Hash), + sdk.NewAttribute(evm.PendingEthereumTxEventAttrIndex, strconv.FormatUint(txIndex+uint64(i), 10)), + ), + ) } - return next(ctx, tx, simulate) } diff --git a/app/evmante/evmante_emit_event_test.go b/app/evmante/evmante_emit_event_test.go index ff501e5ec..855165450 100644 --- a/app/evmante/evmante_emit_event_test.go +++ b/app/evmante/evmante_emit_event_test.go @@ -59,19 +59,19 @@ func (s *TestSuite) TestEthEmitEventDecorator() { s.Require().Greater(len(events), 0) event := events[len(events)-1] - s.Require().Equal(evm.EventTypeEthereumTx, event.Type) + s.Require().Equal(evm.PendingEthereumTxEvent, event.Type) // Convert tx to msg to get hash txMsg, ok := tx.GetMsgs()[0].(*evm.MsgEthereumTx) s.Require().True(ok) // TX hash attr must present - attr, ok := event.GetAttribute(evm.AttributeKeyEthereumTxHash) + attr, ok := event.GetAttribute(evm.PendingEthereumTxEventAttrEthHash) s.Require().True(ok, "tx hash attribute not found") s.Require().Equal(txMsg.Hash, attr.Value) // TX index attr must present - attr, ok = event.GetAttribute(evm.AttributeKeyTxIndex) + attr, ok = event.GetAttribute(evm.PendingEthereumTxEventAttrIndex) s.Require().True(ok, "tx index attribute not found") s.Require().Equal("0", attr.Value) }) diff --git a/eth/indexer/evm_tx_indexer.go b/eth/indexer/evm_tx_indexer.go index 83f5c8213..0b1ba8966 100644 --- a/eth/indexer/evm_tx_indexer.go +++ b/eth/indexer/evm_tx_indexer.go @@ -46,7 +46,7 @@ func NewEVMTxIndexer(db dbm.DB, logger log.Logger, clientCtx client.Context) *EV // - Iterates over all the Txs in Block // - Parses eth Tx infos from cosmos-sdk events for every TxResult // - Iterates over all the messages of the Tx -// - Builds and stores a indexer.TxResult based on parsed events for every message +// - Builds and stores indexer.TxResult based on parsed events for every message func (indexer *EVMTxIndexer) IndexBlock(block *tmtypes.Block, txResults []*abci.ResponseDeliverTx) error { height := block.Header.Height diff --git a/eth/indexer/evm_tx_indexer_test.go b/eth/indexer/evm_tx_indexer_test.go index 83931dcba..17e46bf2c 100644 --- a/eth/indexer/evm_tx_indexer_test.go +++ b/eth/indexer/evm_tx_indexer_test.go @@ -4,7 +4,6 @@ import ( "math/big" "testing" - "cosmossdk.io/simapp/params" dbm "github.com/cometbft/cometbft-db" abci "github.com/cometbft/cometbft/abci/types" tmlog "github.com/cometbft/cometbft/libs/log" @@ -17,7 +16,6 @@ import ( "github.com/NibiruChain/nibiru/v2/app" "github.com/NibiruChain/nibiru/v2/eth" "github.com/NibiruChain/nibiru/v2/eth/crypto/ethsecp256k1" - evmenc "github.com/NibiruChain/nibiru/v2/eth/encoding" "github.com/NibiruChain/nibiru/v2/eth/indexer" "github.com/NibiruChain/nibiru/v2/x/evm" evmtest "github.com/NibiruChain/nibiru/v2/x/evm/evmtest" @@ -50,16 +48,16 @@ func TestEVMTxIndexer(t *testing.T) { WithCodec(encCfg.Codec) // build cosmos-sdk wrapper tx - tmTx, err := tx.BuildTx(clientCtx.TxConfig.NewTxBuilder(), eth.EthBaseDenom) + validEVMTx, err := tx.BuildTx(clientCtx.TxConfig.NewTxBuilder(), eth.EthBaseDenom) require.NoError(t, err) - txBz, err := clientCtx.TxConfig.TxEncoder()(tmTx) + validEVMTxBz, err := clientCtx.TxConfig.TxEncoder()(validEVMTx) require.NoError(t, err) // build an invalid wrapper tx builder := clientCtx.TxConfig.NewTxBuilder() require.NoError(t, builder.SetMsgs(tx)) - tmTx2 := builder.GetTx() - txBz2, err := clientCtx.TxConfig.TxEncoder()(tmTx2) + invalidTx := builder.GetTx() + invalidTxBz, err := clientCtx.TxConfig.TxEncoder()(invalidTx) require.NoError(t, err) testCases := []struct { @@ -69,50 +67,58 @@ func TestEVMTxIndexer(t *testing.T) { expSuccess bool }{ { - "success, format 1", - &tmtypes.Block{Header: tmtypes.Header{Height: 1}, Data: tmtypes.Data{Txs: []tmtypes.Tx{txBz}}}, + "happy, only pending_ethereum_tx presents", + &tmtypes.Block{ + Header: tmtypes.Header{Height: 1}, + Data: tmtypes.Data{Txs: []tmtypes.Tx{validEVMTxBz}}, + }, []*abci.ResponseDeliverTx{ { Code: 0, Events: []abci.Event{ - {Type: evm.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ - {Key: "ethereumTxHash", Value: txHash.Hex()}, - {Key: "txIndex", Value: "0"}, - {Key: "amount", Value: "1000"}, - {Key: "txGasUsed", Value: "21000"}, - {Key: "txHash", Value: ""}, - {Key: "recipient", Value: "0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7"}, - }}, + { + Type: evm.PendingEthereumTxEvent, + Attributes: []abci.EventAttribute{ + {Key: evm.PendingEthereumTxEventAttrEthHash, Value: txHash.Hex()}, + {Key: evm.PendingEthereumTxEventAttrIndex, Value: "0"}, + }, + }, }, }, }, true, }, { - "success, format 2", - &tmtypes.Block{Header: tmtypes.Header{Height: 1}, Data: tmtypes.Data{Txs: []tmtypes.Tx{txBz}}}, + "happy: code 0, pending_ethereum_tx and typed EventEthereumTx present", + &tmtypes.Block{Header: tmtypes.Header{Height: 1}, Data: tmtypes.Data{Txs: []tmtypes.Tx{validEVMTxBz}}}, []*abci.ResponseDeliverTx{ { Code: 0, Events: []abci.Event{ - {Type: evm.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ - {Key: "ethereumTxHash", Value: txHash.Hex()}, - {Key: "txIndex", Value: "0"}, - }}, - {Type: evm.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ - {Key: "amount", Value: "1000"}, - {Key: "txGasUsed", Value: "21000"}, - {Key: "txHash", Value: "14A84ED06282645EFBF080E0B7ED80D8D8D6A36337668A12B5F229F81CDD3F57"}, - {Key: "recipient", Value: "0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7"}, - }}, + { + Type: evm.PendingEthereumTxEvent, + Attributes: []abci.EventAttribute{ + {Key: evm.PendingEthereumTxEventAttrEthHash, Value: txHash.Hex()}, + {Key: evm.PendingEthereumTxEventAttrIndex, Value: "0"}, + }, + }, + { + Type: evm.TypeUrlEventEthereumTx, + Attributes: []abci.EventAttribute{ + {Key: "amount", Value: `"1000"`}, + {Key: "gas_used", Value: `"21000"`}, + {Key: "index", Value: `"0"`}, + {Key: "hash", Value: `"14A84ED06282645EFBF080E0B7ED80D8D8D6A36337668A12B5F229F81CDD3F57"`}, + }, + }, }, }, }, true, }, { - "success, exceed block gas limit", - &tmtypes.Block{Header: tmtypes.Header{Height: 1}, Data: tmtypes.Data{Txs: []tmtypes.Tx{txBz}}}, + "happy: code 11, exceed block gas limit", + &tmtypes.Block{Header: tmtypes.Header{Height: 1}, Data: tmtypes.Data{Txs: []tmtypes.Tx{validEVMTxBz}}}, []*abci.ResponseDeliverTx{ { Code: 11, @@ -123,8 +129,8 @@ func TestEVMTxIndexer(t *testing.T) { true, }, { - "fail, failed eth tx", - &tmtypes.Block{Header: tmtypes.Header{Height: 1}, Data: tmtypes.Data{Txs: []tmtypes.Tx{txBz}}}, + "sad: failed eth tx", + &tmtypes.Block{Header: tmtypes.Header{Height: 1}, Data: tmtypes.Data{Txs: []tmtypes.Tx{validEVMTxBz}}}, []*abci.ResponseDeliverTx{ { Code: 15, @@ -135,8 +141,8 @@ func TestEVMTxIndexer(t *testing.T) { false, }, { - "fail, invalid events", - &tmtypes.Block{Header: tmtypes.Header{Height: 1}, Data: tmtypes.Data{Txs: []tmtypes.Tx{txBz}}}, + "sad: invalid events", + &tmtypes.Block{Header: tmtypes.Header{Height: 1}, Data: tmtypes.Data{Txs: []tmtypes.Tx{validEVMTxBz}}}, []*abci.ResponseDeliverTx{ { Code: 0, @@ -146,8 +152,8 @@ func TestEVMTxIndexer(t *testing.T) { false, }, { - "fail, not eth tx", - &tmtypes.Block{Header: tmtypes.Header{Height: 1}, Data: tmtypes.Data{Txs: []tmtypes.Tx{txBz2}}}, + "sad: not eth tx", + &tmtypes.Block{Header: tmtypes.Header{Height: 1}, Data: tmtypes.Data{Txs: []tmtypes.Tx{invalidTxBz}}}, []*abci.ResponseDeliverTx{ { Code: 0, @@ -192,8 +198,3 @@ func TestEVMTxIndexer(t *testing.T) { }) } } - -// MakeEncodingConfig creates the EncodingConfig -func MakeEncodingConfig() params.EncodingConfig { - return evmenc.MakeConfig(app.ModuleBasics) -} diff --git a/eth/rpc/backend/account_info_test.go b/eth/rpc/backend/account_info_test.go index 109cc3d49..66491320a 100644 --- a/eth/rpc/backend/account_info_test.go +++ b/eth/rpc/backend/account_info_test.go @@ -195,7 +195,7 @@ func generateStorageKey(key gethcommon.Address, slot uint64) string { // Concatenate key and slot data := append(keyBytes, slotBytes...) - // Hash the data using Keccak256 + // Compute the data hash using Keccak256 hash := sha3.NewLegacyKeccak256() hash.Write(data) return gethcommon.BytesToHash(hash.Sum(nil)).Hex() diff --git a/eth/rpc/backend/backend_suite_test.go b/eth/rpc/backend/backend_suite_test.go index 0623557a5..1ba9bd03e 100644 --- a/eth/rpc/backend/backend_suite_test.go +++ b/eth/rpc/backend/backend_suite_test.go @@ -173,7 +173,10 @@ func WaitForReceipt(s *BackendSuite, txHash gethcommon.Hash) (*big.Int, *gethcom for { receipt, err := s.backend.GetTransactionReceipt(txHash) - if err == nil { + if err != nil { + return nil, nil + } + if receipt != nil { return receipt.BlockNumber, &receipt.BlockHash } select { diff --git a/eth/rpc/backend/blocks.go b/eth/rpc/backend/blocks.go index 2b90c55c6..d688415d0 100644 --- a/eth/rpc/backend/blocks.go +++ b/eth/rpc/backend/blocks.go @@ -6,7 +6,6 @@ import ( "math" "math/big" "strconv" - "strings" tmrpcclient "github.com/cometbft/cometbft/rpc/client" tmrpctypes "github.com/cometbft/cometbft/rpc/core/types" @@ -334,18 +333,13 @@ func (b *Backend) BlockBloom(blockRes *tmrpctypes.ResultBlockResults) (gethcore. if event.Type != msgType { continue } - - for _, attr := range event.Attributes { - if attr.Key == evm.AttributeKeyEthereumBloom { - return gethcore.BytesToBloom( - hexutils.HexToBytes( // Bloom stores hex bytes - strings.ReplaceAll(attr.Value, `"`, ""), // Unquote typed event - ), - ), nil - } + blockBloomEvent, err := evm.EventBlockBloomFromABCIEvent(event) + if err != nil { + continue } + return gethcore.BytesToBloom(hexutils.HexToBytes(blockBloomEvent.Bloom)), nil } - return gethcore.Bloom{}, errors.New("block bloom event is not found") + return gethcore.Bloom{}, errors.New(msgType + " not found in end block results") } // RPCBlockFromTendermintBlock returns a JSON-RPC compatible Ethereum block from a diff --git a/eth/rpc/backend/tx_info.go b/eth/rpc/backend/tx_info.go index 9a5c85e09..8d4569d16 100644 --- a/eth/rpc/backend/tx_info.go +++ b/eth/rpc/backend/tx_info.go @@ -7,7 +7,6 @@ import ( "math/big" errorsmod "cosmossdk.io/errors" - tmrpcclient "github.com/cometbft/cometbft/rpc/client" tmrpctypes "github.com/cometbft/cometbft/rpc/core/types" sdk "github.com/cosmos/cosmos-sdk/types" @@ -307,7 +306,8 @@ func (b *Backend) GetTxByEthHash(hash gethcommon.Hash) (*eth.TxResult, error) { } // fallback to tendermint tx evmTxIndexer - query := fmt.Sprintf("%s.%s='%s'", evm.TypeMsgEthereumTx, evm.AttributeKeyEthereumTxHash, hash.Hex()) + query := fmt.Sprintf("%s.%s='%s'", evm.PendingEthereumTxEvent, evm.PendingEthereumTxEventAttrEthHash, hash.Hex()) + txResult, err := b.queryTendermintTxIndexer(query, func(txs *rpc.ParsedTxs) *rpc.ParsedTx { return txs.GetTxByHash(hash) }) @@ -326,8 +326,10 @@ func (b *Backend) GetTxByTxIndex(height int64, index uint) (*eth.TxResult, error // fallback to tendermint tx evmTxIndexer query := fmt.Sprintf("tx.height=%d AND %s.%s=%d", - height, evm.TypeMsgEthereumTx, - evm.AttributeKeyTxIndex, index, + height, + evm.PendingEthereumTxEvent, + evm.PendingEthereumTxEventAttrIndex, + index, ) txResult, err := b.queryTendermintTxIndexer(query, func(txs *rpc.ParsedTxs) *rpc.ParsedTx { return txs.GetTxByTxIndex(int(index)) // #nosec G701 -- checked for int overflow already diff --git a/eth/rpc/backend/utils.go b/eth/rpc/backend/utils.go index 7083354c7..0f8506abd 100644 --- a/eth/rpc/backend/utils.go +++ b/eth/rpc/backend/utils.go @@ -8,8 +8,10 @@ import ( "sort" "strings" + "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/gogoproto/proto" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -212,7 +214,7 @@ func (b *Backend) retrieveEVMTxFeesFromBlock( func AllTxLogsFromEvents(events []abci.Event) ([][]*gethcore.Log, error) { allLogs := make([][]*gethcore.Log, 0, 4) for _, event := range events { - if event.Type != evm.EventTypeTxLog { + if event.Type != proto.MessageName(new(evm.EventTxLog)) { continue } @@ -229,7 +231,7 @@ func AllTxLogsFromEvents(events []abci.Event) ([][]*gethcore.Log, error) { // TxLogsFromEvents parses ethereum logs from cosmos events for specific msg index func TxLogsFromEvents(events []abci.Event, msgIndex int) ([]*gethcore.Log, error) { for _, event := range events { - if event.Type != evm.EventTypeTxLog { + if event.Type != proto.MessageName(new(evm.EventTxLog)) { continue } @@ -246,20 +248,19 @@ func TxLogsFromEvents(events []abci.Event, msgIndex int) ([]*gethcore.Log, error // ParseTxLogsFromEvent parse tx logs from one event func ParseTxLogsFromEvent(event abci.Event) ([]*gethcore.Log, error) { - logs := make([]*evm.Log, 0, len(event.Attributes)) - for _, attr := range event.Attributes { - if attr.Key != evm.AttributeKeyTxLog { - continue - } - - var log evm.Log - if err := json.Unmarshal([]byte(attr.Value), &log); err != nil { - return nil, err + eventTxLog, err := evm.EventTxLogFromABCIEvent(event) + if err != nil { + return nil, errors.Wrapf(err, "failed to parse event tx log") + } + var evmLogs []*evm.Log + for _, logString := range eventTxLog.TxLogs { + var evmLog evm.Log + if err = json.Unmarshal([]byte(logString), &evmLog); err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal event tx log") } - - logs = append(logs, &log) + evmLogs = append(evmLogs, &evmLog) } - return evm.LogsToEthereum(logs), nil + return evm.LogsToEthereum(evmLogs), nil } // ShouldIgnoreGasUsed returns true if the gasUsed in result should be ignored diff --git a/eth/rpc/events.go b/eth/rpc/events.go deleted file mode 100644 index 8c0d8cacf..000000000 --- a/eth/rpc/events.go +++ /dev/null @@ -1,265 +0,0 @@ -// Copyright (c) 2023-2024 Nibi, Inc. -package rpc - -import ( - "fmt" - "strconv" - - abci "github.com/cometbft/cometbft/abci/types" - tmrpctypes "github.com/cometbft/cometbft/rpc/core/types" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/ethereum/go-ethereum/common" - - "github.com/NibiruChain/nibiru/v2/eth" - "github.com/NibiruChain/nibiru/v2/x/evm" -) - -// EventFormat is an enum type for an ethereum tx event. Each event format -// variant has json-rpc logic to make it so that clients using either format will -// be compatible, meaning nodes will able to sync without restarting from the -// first block. -type EventFormat int - -const ( - eventFormatUnknown EventFormat = iota - - // Event Format 1 - // ``` - // ethereum_tx(amount, ethereumTxHash, [txIndex, txGasUsed], txHash, [receipient], ethereumTxFailed) - // tx_log(txLog, txLog, ...) - // ethereum_tx(amount, ethereumTxHash, [txIndex, txGasUsed], txHash, [receipient], ethereumTxFailed) - // tx_log(txLog, txLog, ...) - // ... - // ``` - eventFormat1 - - // Event Format 2 - // ``` - // ethereum_tx(ethereumTxHash, txIndex) - // ethereum_tx(ethereumTxHash, txIndex) - // ... - // ethereum_tx(amount, ethereumTxHash, txIndex, txGasUsed, txHash, [receipient], ethereumTxFailed) - // tx_log(txLog, txLog, ...) - // ethereum_tx(amount, ethereumTxHash, txIndex, txGasUsed, txHash, [receipient], ethereumTxFailed) - // tx_log(txLog, txLog, ...) - // ... - // ``` - // If the transaction exceeds block gas limit, it only emits the first part. - eventFormat2 -) - -// ParsedTx is eth tx info parsed from ABCI events. Each `ParsedTx` corresponds -// to one eth tx msg ([evm.MsgEthereumTx]). -type ParsedTx struct { - MsgIndex int - - // the following fields are parsed from events - - Hash common.Hash - // -1 means uninitialized - EthTxIndex int32 - GasUsed uint64 - Failed bool -} - -// NewParsedTx initialize a ParsedTx -func NewParsedTx(msgIndex int) ParsedTx { - return ParsedTx{MsgIndex: msgIndex, EthTxIndex: -1} -} - -// ParsedTxs is the tx infos parsed from eth tx events. -type ParsedTxs struct { - // one item per message - Txs []ParsedTx - // map tx hash to msg index - TxHashes map[common.Hash]int -} - -// ParseTxResult: parses eth tx info from the ABCI events of Eth tx msgs -// ([evm.MsgEthereumTx]). It supports each [EventFormat]. -func ParseTxResult( - result *abci.ResponseDeliverTx, tx sdk.Tx, -) (*ParsedTxs, error) { - format := eventFormatUnknown - // the index of current ethereum_tx event in format 1 or the second part of format 2 - eventIndex := -1 - - p := &ParsedTxs{ - TxHashes: make(map[common.Hash]int), - } - for _, event := range result.Events { - if event.Type != evm.EventTypeEthereumTx { - continue - } - - if format == eventFormatUnknown { - // discover the format version by inspect the first ethereum_tx event. - if len(event.Attributes) > 2 { - format = eventFormat1 - } else { - format = eventFormat2 - } - } - - if len(event.Attributes) == 2 { - // the first part of format 2 - if err := p.newTx(event.Attributes); err != nil { - return nil, err - } - } else { - // format 1 or second part of format 2 - eventIndex++ - if format == eventFormat1 { - // append tx - if err := p.newTx(event.Attributes); err != nil { - return nil, err - } - } else { - // the second part of format 2, update tx fields - if err := p.updateTx(eventIndex, event.Attributes); err != nil { - return nil, err - } - } - } - } - - // some old versions miss some events, fill it with tx result - gasUsed := uint64(result.GasUsed) // #nosec G701 - if len(p.Txs) == 1 { - p.Txs[0].GasUsed = gasUsed - } - - // this could only happen if tx exceeds block gas limit - if result.Code != 0 && tx != nil { - for i := 0; i < len(p.Txs); i++ { - p.Txs[i].Failed = true - - // replace gasUsed with gasLimit because that's what's actually deducted. - gasLimit := tx.GetMsgs()[i].(*evm.MsgEthereumTx).GetGas() - p.Txs[i].GasUsed = gasLimit - } - } - return p, nil -} - -// ParseTxIndexerResult parse tm tx result to a format compatible with the custom tx indexer. -func ParseTxIndexerResult( - txResult *tmrpctypes.ResultTx, tx sdk.Tx, getter func(*ParsedTxs) *ParsedTx, -) (*eth.TxResult, error) { - txs, err := ParseTxResult(&txResult.TxResult, tx) - if err != nil { - return nil, fmt.Errorf("failed to parse tx events: block %d, index %d, %v", txResult.Height, txResult.Index, err) - } - - parsedTx := getter(txs) - if parsedTx == nil { - return nil, fmt.Errorf("ethereum tx not found in msgs: block %d, index %d", txResult.Height, txResult.Index) - } - index := uint32(parsedTx.MsgIndex) // #nosec G701 - return ð.TxResult{ - Height: txResult.Height, - TxIndex: txResult.Index, - MsgIndex: index, - EthTxIndex: parsedTx.EthTxIndex, - Failed: parsedTx.Failed, - GasUsed: parsedTx.GasUsed, - CumulativeGasUsed: txs.AccumulativeGasUsed(parsedTx.MsgIndex), - }, nil -} - -// newTx parse a new tx from events, called during parsing. -func (p *ParsedTxs) newTx(attrs []abci.EventAttribute) error { - msgIndex := len(p.Txs) - tx := NewParsedTx(msgIndex) - if err := fillTxAttributes(&tx, attrs); err != nil { - return err - } - p.Txs = append(p.Txs, tx) - p.TxHashes[tx.Hash] = msgIndex - return nil -} - -// updateTx updates an exiting tx from events, called during parsing. -// In event format 2, we update the tx with the attributes of the second `ethereum_tx` event, -func (p *ParsedTxs) updateTx(eventIndex int, attrs []abci.EventAttribute) error { - tx := NewParsedTx(eventIndex) - if err := fillTxAttributes(&tx, attrs); err != nil { - return err - } - if tx.Hash != p.Txs[eventIndex].Hash { - // if hash is different, index the new one too - p.TxHashes[tx.Hash] = eventIndex - } - // override the tx because the second event is more trustworthy - p.Txs[eventIndex] = tx - return nil -} - -// GetTxByHash find ParsedTx by tx hash, returns nil if not exists. -func (p *ParsedTxs) GetTxByHash(hash common.Hash) *ParsedTx { - if idx, ok := p.TxHashes[hash]; ok { - return &p.Txs[idx] - } - return nil -} - -// GetTxByMsgIndex returns ParsedTx by msg index -func (p *ParsedTxs) GetTxByMsgIndex(i int) *ParsedTx { - if i < 0 || i >= len(p.Txs) { - return nil - } - return &p.Txs[i] -} - -// GetTxByTxIndex returns ParsedTx by tx index -func (p *ParsedTxs) GetTxByTxIndex(txIndex int) *ParsedTx { - if len(p.Txs) == 0 { - return nil - } - // assuming the `EthTxIndex` increase continuously, - // convert TxIndex to MsgIndex by subtract the begin TxIndex. - msgIndex := txIndex - int(p.Txs[0].EthTxIndex) - // GetTxByMsgIndex will check the bound - return p.GetTxByMsgIndex(msgIndex) -} - -// AccumulativeGasUsed calculates the accumulated gas used within the batch of txs -func (p *ParsedTxs) AccumulativeGasUsed(msgIndex int) (result uint64) { - for i := 0; i <= msgIndex; i++ { - result += p.Txs[i].GasUsed - } - return result -} - -// fillTxAttribute parse attributes by name, less efficient than hardcode the -// index, but more stable against event format changes. -func fillTxAttribute(tx *ParsedTx, key string, value string) error { - switch key { - case evm.AttributeKeyEthereumTxHash: - tx.Hash = common.HexToHash(value) - case evm.AttributeKeyTxIndex: - txIndex, err := strconv.ParseUint(value, 10, 31) // #nosec G701 - if err != nil { - return err - } - tx.EthTxIndex = int32(txIndex) // #nosec G701 - case evm.AttributeKeyTxGasUsed: - gasUsed, err := strconv.ParseUint(value, 10, 64) - if err != nil { - return err - } - tx.GasUsed = gasUsed - case evm.AttributeKeyEthereumTxFailed: - tx.Failed = len(value) > 0 - } - return nil -} - -func fillTxAttributes(tx *ParsedTx, attrs []abci.EventAttribute) error { - for _, attr := range attrs { - if err := fillTxAttribute(tx, attr.Key, attr.Value); err != nil { - return err - } - } - return nil -} diff --git a/eth/rpc/events_parser.go b/eth/rpc/events_parser.go new file mode 100644 index 000000000..2c334ef7b --- /dev/null +++ b/eth/rpc/events_parser.go @@ -0,0 +1,176 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package rpc + +import ( + "errors" + "fmt" + "strconv" + + abci "github.com/cometbft/cometbft/abci/types" + tmrpctypes "github.com/cometbft/cometbft/rpc/core/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/gogoproto/proto" + gethcommon "github.com/ethereum/go-ethereum/common" + + "github.com/NibiruChain/nibiru/v2/eth" + "github.com/NibiruChain/nibiru/v2/x/evm" +) + +// ParsedTx is eth tx info parsed from ABCI events. Each `ParsedTx` corresponds +// to one eth tx msg ([evm.MsgEthereumTx]). +type ParsedTx struct { + MsgIndex int + + // the following fields are parsed from events + EthHash gethcommon.Hash + + EthTxIndex int32 // -1 means uninitialized + GasUsed uint64 + Failed bool +} + +// ParsedTxs is the tx infos parsed from eth tx events. +type ParsedTxs struct { + // one item per message + Txs []ParsedTx + // map tx hash to msg index + TxHashes map[gethcommon.Hash]int +} + +// ParseTxResult parses eth tx info from the ABCI events of Eth tx msgs +func ParseTxResult(result *abci.ResponseDeliverTx, tx sdk.Tx) (*ParsedTxs, error) { + eventTypePendingEthereumTx := evm.PendingEthereumTxEvent + eventTypeEthereumTx := proto.MessageName((*evm.EventEthereumTx)(nil)) + + // Parsed txs is the structure being populated from the events + // So far (until we allow ethereum_txs as cosmos tx messages) it'll have single tx + parsedTxs := &ParsedTxs{ + Txs: make([]ParsedTx, 0), + TxHashes: make(map[gethcommon.Hash]int), + } + + // msgIndex counts only ethereum tx messages. + msgIndex := -1 + for _, event := range result.Events { + // Pending tx event could be single if tx didn't succeed + if event.Type == eventTypePendingEthereumTx { + msgIndex++ + ethHash, txIndex, err := evm.GetEthHashAndIndexFromPendingEthereumTxEvent(event) + if err != nil { + return nil, err + } + pendingTx := ParsedTx{ + MsgIndex: msgIndex, + EthTxIndex: txIndex, + EthHash: ethHash, + } + parsedTxs.Txs = append(parsedTxs.Txs, pendingTx) + parsedTxs.TxHashes[ethHash] = msgIndex + } else if event.Type == eventTypeEthereumTx { // Full event replaces the pending tx + eventEthereumTx, err := evm.EventEthereumTxFromABCIEvent(event) + if err != nil { + return nil, err + } + ethTxIndexFromEvent, err := strconv.ParseUint(eventEthereumTx.Index, 10, 31) + if err != nil { + return nil, fmt.Errorf("failed to parse EthTxIndex from event: %w", err) + } + gasUsed, err := strconv.ParseUint(eventEthereumTx.GasUsed, 10, 64) + if err != nil { + return nil, fmt.Errorf("failed to parse GasUsed from event: %w", err) + } + committedTx := ParsedTx{ + MsgIndex: msgIndex, + EthTxIndex: int32(ethTxIndexFromEvent), + EthHash: gethcommon.HexToHash(eventEthereumTx.EthHash), + GasUsed: gasUsed, + Failed: len(eventEthereumTx.EthTxFailed) > 0, + } + // replace pending tx with committed tx + if msgIndex >= 0 && len(parsedTxs.Txs) == msgIndex+1 { + parsedTxs.Txs[msgIndex] = committedTx + } else { + // EventEthereumTx without EventPendingEthereumTx + return nil, errors.New("EventEthereumTx without pending_ethereum_tx event") + } + } + } + + // this could only happen if tx exceeds block gas limit + if result.Code != 0 && tx != nil { + for i := 0; i < len(parsedTxs.Txs); i++ { + parsedTxs.Txs[i].Failed = true + + // replace gasUsed with gasLimit because that's what's actually deducted. + msgEthereumTx, ok := tx.GetMsgs()[i].(*evm.MsgEthereumTx) + if !ok { + return nil, fmt.Errorf("unexpected message type at index %d", i) + } + gasLimit := msgEthereumTx.GetGas() + parsedTxs.Txs[i].GasUsed = gasLimit + } + } + return parsedTxs, nil +} + +// ParseTxIndexerResult parse tm tx result to a format compatible with the custom tx indexer. +func ParseTxIndexerResult(txResult *tmrpctypes.ResultTx, tx sdk.Tx, getter func(*ParsedTxs) *ParsedTx) (*eth.TxResult, error) { + txs, err := ParseTxResult(&txResult.TxResult, tx) + if err != nil { + return nil, fmt.Errorf("failed to parse tx events: block %d, index %d, %v", txResult.Height, txResult.Index, err) + } + + parsedTx := getter(txs) + if parsedTx == nil { + return nil, fmt.Errorf("ethereum tx not found in msgs: block %d, index %d", txResult.Height, txResult.Index) + } + index := uint32(parsedTx.MsgIndex) // #nosec G701 + return ð.TxResult{ + Height: txResult.Height, + TxIndex: txResult.Index, + MsgIndex: index, + EthTxIndex: parsedTx.EthTxIndex, + Failed: parsedTx.Failed, + GasUsed: parsedTx.GasUsed, + CumulativeGasUsed: txs.AccumulativeGasUsed(parsedTx.MsgIndex), + }, nil +} + +// GetTxByHash find ParsedTx by tx hash, returns nil if not exists. +func (p *ParsedTxs) GetTxByHash(hash gethcommon.Hash) *ParsedTx { + if idx, ok := p.TxHashes[hash]; ok { + return &p.Txs[idx] + } + return nil +} + +// GetTxByMsgIndex returns ParsedTx by msg index +func (p *ParsedTxs) GetTxByMsgIndex(i int) *ParsedTx { + if i < 0 || i >= len(p.Txs) { + return nil + } + return &p.Txs[i] +} + +// GetTxByTxIndex returns ParsedTx by tx index +func (p *ParsedTxs) GetTxByTxIndex(txIndex int) *ParsedTx { + if len(p.Txs) == 0 { + return nil + } + // assuming the `EthTxIndex` increase continuously, + // convert TxIndex to MsgIndex by subtract the begin TxIndex. + msgIndex := txIndex - int(p.Txs[0].EthTxIndex) + // GetTxByMsgIndex will check the bound + return p.GetTxByMsgIndex(msgIndex) +} + +// AccumulativeGasUsed calculates the accumulated gas used within the batch of txs +func (p *ParsedTxs) AccumulativeGasUsed(msgIndex int) (result uint64) { + if msgIndex < 0 || msgIndex >= len(p.Txs) { + return 0 + } + for i := 0; i <= msgIndex; i++ { + result += p.Txs[i].GasUsed + } + return result +} diff --git a/eth/rpc/events_parser_test.go b/eth/rpc/events_parser_test.go new file mode 100644 index 000000000..e4a0c6b73 --- /dev/null +++ b/eth/rpc/events_parser_test.go @@ -0,0 +1,201 @@ +package rpc + +import ( + "math/big" + "strconv" + "testing" + + abci "github.com/cometbft/cometbft/abci/types" + sdk "github.com/cosmos/cosmos-sdk/types" + gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + + "github.com/NibiruChain/nibiru/v2/x/evm" +) + +func TestParseTxResult(t *testing.T) { + txHashOne := gethcommon.BigToHash(big.NewInt(1)) + txHashTwo := gethcommon.BigToHash(big.NewInt(2)) + + type TestCase struct { + name string + txResp abci.ResponseDeliverTx + wantEthTxs []*ParsedTx + wantErr bool + } + + testCases := []TestCase{ + { + name: "happy: valid single pending_ethereum_tx event", + txResp: abci.ResponseDeliverTx{ + Events: []abci.Event{ + pendingEthereumTxEvent(txHashOne.Hex(), 0), + }, + }, + wantEthTxs: []*ParsedTx{ + { + MsgIndex: 0, + EthHash: txHashOne, + EthTxIndex: 0, + GasUsed: 0, + Failed: false, + }, + }, + }, + { + name: "happy: two valid pending_ethereum_tx events", + txResp: abci.ResponseDeliverTx{ + Events: []abci.Event{ + pendingEthereumTxEvent(txHashOne.Hex(), 0), + pendingEthereumTxEvent(txHashTwo.Hex(), 1), + }, + }, + wantEthTxs: []*ParsedTx{ + { + MsgIndex: 0, + EthHash: txHashOne, + EthTxIndex: 0, + GasUsed: 0, + Failed: false, + }, + { + MsgIndex: 1, + EthHash: txHashTwo, + EthTxIndex: 1, + Failed: false, + }, + }, + }, + { + name: "happy: one pending_ethereum_tx and one EventEthereumTx", + txResp: abci.ResponseDeliverTx{ + Events: []abci.Event{ + pendingEthereumTxEvent(txHashOne.Hex(), 0), + ethereumTxEvent(txHashOne.Hex(), 0, 21000, false), + }, + }, + wantEthTxs: []*ParsedTx{ + { + MsgIndex: 0, + EthHash: txHashOne, + EthTxIndex: 0, + GasUsed: 21000, + Failed: false, + }, + }, + }, + { + name: "happy: two pending_ethereum_tx and one EventEthereumTx", + txResp: abci.ResponseDeliverTx{ + Events: []abci.Event{ + pendingEthereumTxEvent(txHashOne.Hex(), 0), + pendingEthereumTxEvent(txHashTwo.Hex(), 1), + ethereumTxEvent(txHashTwo.Hex(), 1, 21000, false), + }, + }, + wantEthTxs: []*ParsedTx{ + { + MsgIndex: 0, + EthHash: txHashOne, + EthTxIndex: 0, + GasUsed: 0, + Failed: false, + }, + { + MsgIndex: 1, + EthHash: txHashTwo, + EthTxIndex: 1, + GasUsed: 21000, + Failed: false, + }, + }, + }, + { + name: "happy: one pending_ethereum_tx and one EventEthereumTx failed", + txResp: abci.ResponseDeliverTx{ + Events: []abci.Event{ + pendingEthereumTxEvent(txHashOne.Hex(), 0), + ethereumTxEvent(txHashOne.Hex(), 0, 21000, true), + }, + }, + wantEthTxs: []*ParsedTx{ + { + MsgIndex: 0, + EthHash: txHashOne, + EthTxIndex: 0, + GasUsed: 21000, + Failed: true, + }, + }, + }, + { + name: "sad: EventEthereumTx without pending_ethereum_tx", + txResp: abci.ResponseDeliverTx{ + Events: []abci.Event{ + ethereumTxEvent(txHashOne.Hex(), 0, 21000, false), + }, + }, + wantEthTxs: nil, + wantErr: true, + }, + { + name: "sad: no events", + txResp: abci.ResponseDeliverTx{ + Events: []abci.Event{}, + }, + wantEthTxs: []*ParsedTx{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + parsed, err := ParseTxResult(&tc.txResp, nil) //#nosec G601 -- fine for tests + if tc.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + for msgIndex, expTx := range tc.wantEthTxs { + require.Equal(t, expTx, parsed.GetTxByMsgIndex(msgIndex)) + require.Equal(t, expTx, parsed.GetTxByHash(expTx.EthHash)) + require.Equal(t, expTx, parsed.GetTxByTxIndex(int(expTx.EthTxIndex))) + require.Equal(t, expTx.GasUsed, parsed.GetTxByHash(expTx.EthHash).GasUsed) + require.Equal(t, expTx.Failed, parsed.GetTxByHash(expTx.EthHash).Failed) + } + // non-exists tx hash + require.Nil(t, parsed.GetTxByHash(gethcommon.Hash{})) + // out of range + require.Nil(t, parsed.GetTxByMsgIndex(len(tc.wantEthTxs))) + require.Nil(t, parsed.GetTxByTxIndex(99999999)) + }) + } +} + +func pendingEthereumTxEvent(txHash string, txIndex int) abci.Event { + return abci.Event{ + Type: evm.PendingEthereumTxEvent, + Attributes: []abci.EventAttribute{ + {Key: evm.PendingEthereumTxEventAttrEthHash, Value: txHash}, + {Key: evm.PendingEthereumTxEventAttrIndex, Value: strconv.Itoa(txIndex)}, + }, + } +} + +func ethereumTxEvent(txHash string, txIndex int, gasUsed int, failed bool) abci.Event { + failure := "" + if failed { + failure = "failed" + } + event, err := sdk.TypedEventToEvent( + &evm.EventEthereumTx{ + EthHash: txHash, + Index: strconv.Itoa(txIndex), + GasUsed: strconv.Itoa(gasUsed), + EthTxFailed: failure, + }, + ) + if err != nil { + panic(err) + } + return (abci.Event)(event) +} diff --git a/eth/rpc/events_test.go b/eth/rpc/events_test.go deleted file mode 100644 index c9d48e84e..000000000 --- a/eth/rpc/events_test.go +++ /dev/null @@ -1,193 +0,0 @@ -package rpc - -import ( - "math/big" - "testing" - - abci "github.com/cometbft/cometbft/abci/types" - "github.com/ethereum/go-ethereum/common" - "github.com/stretchr/testify/require" - - "github.com/NibiruChain/nibiru/v2/x/evm" -) - -func TestParseTxResult(t *testing.T) { - address := "0x57f96e6B86CdeFdB3d412547816a82E3E0EbF9D2" - txHash := common.BigToHash(big.NewInt(1)) - txHash2 := common.BigToHash(big.NewInt(2)) - - type TestCase struct { - name string - txResp abci.ResponseDeliverTx - wantEthTxs []*ParsedTx // expected parse result, nil means expect error. - } - - testCases := []TestCase{ - { - name: "format 1 events", - txResp: abci.ResponseDeliverTx{ - GasUsed: 21000, - Events: []abci.Event{ - {Type: "coin_received", Attributes: []abci.EventAttribute{ - {Key: "receiver", Value: "ethm12luku6uxehhak02py4rcz65zu0swh7wjun6msa"}, - {Key: "amount", Value: "1252860basetcro"}, - }}, - {Type: "coin_spent", Attributes: []abci.EventAttribute{ - {Key: "spender", Value: "ethm17xpfvakm2amg962yls6f84z3kell8c5lthdzgl"}, - {Key: "amount", Value: "1252860basetcro"}, - }}, - {Type: evm.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ - {Key: "ethereumTxHash", Value: txHash.Hex()}, - {Key: "txIndex", Value: "10"}, - {Key: "amount", Value: "1000"}, - {Key: "txGasUsed", Value: "21000"}, - {Key: "txHash", Value: "14A84ED06282645EFBF080E0B7ED80D8D8D6A36337668A12B5F229F81CDD3F57"}, - {Key: "recipient", Value: "0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7"}, - }}, - {Type: "message", Attributes: []abci.EventAttribute{ - {Key: "action", Value: "/ehermint.evm.v1.MsgEthereumTx"}, - {Key: "key", Value: "ethm17xpfvakm2amg962yls6f84z3kell8c5lthdzgl"}, - {Key: "module", Value: "evm"}, - {Key: "sender", Value: address}, - }}, - {Type: evm.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ - {Key: "ethereumTxHash", Value: txHash2.Hex()}, - {Key: "txIndex", Value: "11"}, - {Key: "amount", Value: "1000"}, - {Key: "txGasUsed", Value: "21000"}, - {Key: "txHash", Value: "14A84ED06282645EFBF080E0B7ED80D8D8D6A36337668A12B5F229F81CDD3F57"}, - {Key: "recipient", Value: "0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7"}, - {Key: "ethereumTxFailed", Value: "contract everted"}, - }}, - {Type: evm.EventTypeTxLog, Attributes: []abci.EventAttribute{}}, - }, - }, - wantEthTxs: []*ParsedTx{ - { - MsgIndex: 0, - Hash: txHash, - EthTxIndex: 10, - GasUsed: 21000, - Failed: false, - }, - { - MsgIndex: 1, - Hash: txHash2, - EthTxIndex: 11, - GasUsed: 21000, - Failed: true, - }, - }, - }, - { - name: "format 2 events", - txResp: abci.ResponseDeliverTx{ - GasUsed: 21000, - Events: []abci.Event{ - {Type: "coin_received", Attributes: []abci.EventAttribute{ - {Key: "receiver", Value: "ethm12luku6uxehhak02py4rcz65zu0swh7wjun6msa"}, - {Key: "amount", Value: "1252860basetcro"}, - }}, - {Type: "coin_spent", Attributes: []abci.EventAttribute{ - {Key: "spender", Value: "ethm17xpfvakm2amg962yls6f84z3kell8c5lthdzgl"}, - {Key: "amount", Value: "1252860basetcro"}, - }}, - {Type: evm.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ - {Key: "ethereumTxHash", Value: txHash.Hex()}, - {Key: "txIndex", Value: "0"}, - }}, - {Type: evm.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ - {Key: "amount", Value: "1000"}, - {Key: "ethereumTxHash", Value: txHash.Hex()}, - {Key: "txIndex", Value: "0"}, - {Key: "txGasUsed", Value: "21000"}, - {Key: "txHash", Value: "14A84ED06282645EFBF080E0B7ED80D8D8D6A36337668A12B5F229F81CDD3F57"}, - {Key: "recipient", Value: "0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7"}, - }}, - {Type: "message", Attributes: []abci.EventAttribute{ - {Key: "action", Value: "/ehermint.evm.v1.MsgEthereumTx"}, - {Key: "key", Value: "ethm17xpfvakm2amg962yls6f84z3kell8c5lthdzgl"}, - {Key: "module", Value: "evm"}, - {Key: "sender", Value: address}, - }}, - }, - }, - wantEthTxs: []*ParsedTx{ - { - MsgIndex: 0, - Hash: txHash, - EthTxIndex: 0, - GasUsed: 21000, - Failed: false, - }, - }, - }, - { - "format 1 events, failed", - abci.ResponseDeliverTx{ - GasUsed: 21000, - Events: []abci.Event{ - {Type: evm.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ - {Key: "ethereumTxHash", Value: txHash.Hex()}, - {Key: "txIndex", Value: "10"}, - {Key: "amount", Value: "1000"}, - {Key: "txGasUsed", Value: "21000"}, - {Key: "txHash", Value: "14A84ED06282645EFBF080E0B7ED80D8D8D6A36337668A12B5F229F81CDD3F57"}, - {Key: "recipient", Value: "0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7"}, - }}, - {Type: evm.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ - {Key: "ethereumTxHash", Value: txHash2.Hex()}, - {Key: "txIndex", Value: "10"}, - {Key: "amount", Value: "1000"}, - {Key: "txGasUsed", Value: "0x01"}, - {Key: "txHash", Value: "14A84ED06282645EFBF080E0B7ED80D8D8D6A36337668A12B5F229F81CDD3F57"}, - {Key: "recipient", Value: "0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7"}, - {Key: "ethereumTxFailed", Value: "contract everted"}, - }}, - {Type: evm.EventTypeTxLog, Attributes: []abci.EventAttribute{}}, - }, - }, - nil, - }, - { - name: "format 2 events failed", - txResp: abci.ResponseDeliverTx{ - GasUsed: 21000, - Events: []abci.Event{ - {Type: evm.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ - {Key: "ethereumTxHash", Value: txHash.Hex()}, - {Key: "txIndex", Value: "10"}, - }}, - {Type: evm.EventTypeEthereumTx, Attributes: []abci.EventAttribute{ - {Key: "amount", Value: "1000"}, - {Key: "txGasUsed", Value: "0x01"}, - {Key: "txHash", Value: "14A84ED06282645EFBF080E0B7ED80D8D8D6A36337668A12B5F229F81CDD3F57"}, - {Key: "recipient", Value: "0x775b87ef5D82ca211811C1a02CE0fE0CA3a455d7"}, - }}, - }, - }, - wantEthTxs: nil, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - parsed, err := ParseTxResult(&tc.txResp, nil) //#nosec G601 -- fine for tests - if tc.wantEthTxs == nil { - require.Error(t, err) - return - } - require.NoError(t, err) - for msgIndex, expTx := range tc.wantEthTxs { - require.Equal(t, expTx, parsed.GetTxByMsgIndex(msgIndex)) - require.Equal(t, expTx, parsed.GetTxByHash(expTx.Hash)) - require.Equal(t, expTx, parsed.GetTxByTxIndex(int(expTx.EthTxIndex))) - } - // non-exists tx hash - require.Nil(t, parsed.GetTxByHash(common.Hash{})) - // out of range - require.Nil(t, parsed.GetTxByMsgIndex(len(tc.wantEthTxs))) - require.Nil(t, parsed.GetTxByTxIndex(99999999)) - }) - } -} diff --git a/eth/rpc/rpcapi/eth_api_test.go b/eth/rpc/rpcapi/eth_api_test.go index 54e992462..7ba8552f1 100644 --- a/eth/rpc/rpcapi/eth_api_test.go +++ b/eth/rpc/rpcapi/eth_api_test.go @@ -3,7 +3,6 @@ package rpcapi_test import ( "context" "crypto/ecdsa" - "encoding/json" "fmt" "math/big" "strings" @@ -171,8 +170,6 @@ func (s *NodeSuite) Test_CodeAt() { func (s *NodeSuite) Test_PendingCodeAt() { code, err := s.ethClient.PendingCodeAt(context.Background(), s.fundedAccEthAddr) s.NoError(err) - - // TODO: add more checks s.NotNil(code) } @@ -276,21 +273,30 @@ func (s *NodeSuite) Test_SimpleTransferTransaction() { blockHeightOfTx := int64(txReceipt.BlockNumber.Uint64()) blockOfTx, err := s.val.RPCClient.BlockResults(blankCtx, &blockHeightOfTx) s.NoError(err) - ethTxEvents := []sdk.Event{} events := blockOfTx.TxsResults[0].Events + pendingEthTxEventHash := gethcommon.Hash{} + pendingEthTxEventIndex := int32(-1) for _, event := range events { - if event.Type == "ethereum_tx" { - ethTxEvents = append(ethTxEvents, - sdk.Event{Type: event.Type, Attributes: event.Attributes}, + if event.Type == evm.PendingEthereumTxEvent { + pendingEthTxEventHash, pendingEthTxEventIndex, err = + evm.GetEthHashAndIndexFromPendingEthereumTxEvent(event) + s.Require().NoError(err) + } + if event.Type == evm.TypeUrlEventEthereumTx { + ethereumTx, err := evm.EventEthereumTxFromABCIEvent(event) + s.Require().NoError(err) + s.Require().Equal( + pendingEthTxEventHash.Hex(), + ethereumTx.EthHash, + "hash of pending ethereum tx event and ethereum tx event must be equal", + ) + s.Require().Equal( + fmt.Sprintf("%d", pendingEthTxEventIndex), + ethereumTx.Index, + "index of pending ethereum tx event and ethereum tx event must be equal", ) } } - - eventsJson, _ := json.Marshal(events) - s.Require().Equal(len(ethTxEvents), 2, "events: ", eventsJson) - hash0, _ := ethTxEvents[0].GetAttribute(evm.AttributeKeyEthereumTxHash) - hash1, _ := ethTxEvents[1].GetAttribute(evm.AttributeKeyEthereumTxHash) - s.Require().Equal(hash0, hash1) } s.T().Log("Assert balances") diff --git a/eth/rpc/rpcapi/eth_filters_api.go b/eth/rpc/rpcapi/eth_filters_api.go index 9a72a5042..782014bb5 100644 --- a/eth/rpc/rpcapi/eth_filters_api.go +++ b/eth/rpc/rpcapi/eth_filters_api.go @@ -389,7 +389,7 @@ func (api *FiltersAPI) Logs( } // filter only events from EVM module txs - _, isMsgEthereumTx := ev.Events[evm.TypeMsgEthereumTx] + _, isMsgEthereumTx := ev.Events[evm.TypeUrlEventEthereumTx] if !isMsgEthereumTx { // ignore transaction as it's not from the evm module diff --git a/eth/rpc/rpcapi/filter_utils.go b/eth/rpc/rpcapi/filter_utils.go index 53e7c609d..41983b84d 100644 --- a/eth/rpc/rpcapi/filter_utils.go +++ b/eth/rpc/rpcapi/filter_utils.go @@ -6,8 +6,6 @@ import ( "cosmossdk.io/errors" abci "github.com/cometbft/cometbft/abci/types" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/ethereum/go-ethereum/common" gethcore "github.com/ethereum/go-ethereum/core/types" @@ -117,24 +115,17 @@ func returnLogs(logs []*gethcore.Log) []*gethcore.Log { // ParseBloomFromEvents iterates through the slice of events func ParseBloomFromEvents(events []abci.Event) (bloom gethcore.Bloom, err error) { - bloomEvent := new(evm.EventBlockBloom) - bloomEventType := gogoproto.MessageName(bloomEvent) + bloomEventType := gogoproto.MessageName(new(evm.EventBlockBloom)) for _, event := range events { if event.Type != bloomEventType { continue } - typedProtoEvent, err := sdk.ParseTypedEvent(event) + bloomTypedEvent, err := evm.EventBlockBloomFromABCIEvent(event) if err != nil { return bloom, errors.Wrapf( err, "failed to parse event of type %s", bloomEventType) } - bloomEvent, ok := (typedProtoEvent).(*evm.EventBlockBloom) - if !ok { - return bloom, errors.Wrapf( - err, "failed to parse event of type %s", bloomEventType) - } - - return eth.BloomFromHex(bloomEvent.Bloom) + return eth.BloomFromHex(bloomTypedEvent.Bloom) } return bloom, err } diff --git a/proto/eth/evm/v1/events.proto b/proto/eth/evm/v1/events.proto index 039566fa0..826c8bf6d 100644 --- a/proto/eth/evm/v1/events.proto +++ b/proto/eth/evm/v1/events.proto @@ -31,16 +31,6 @@ message EventTxLog { repeated string tx_logs = 1; } -// EventMessage -message EventMessage { - // module which emits the event - string module = 1; - // sender of the message - string sender = 2; - // tx_type is the type of the message - string tx_type = 3; -} - // EventBlockBloom defines an Ethereum block bloom filter event message EventBlockBloom { // bloom is the bloom filter of the block diff --git a/x/evm/events.go b/x/evm/events.go index 3bf3dad0a..e6d0b217d 100644 --- a/x/evm/events.go +++ b/x/evm/events.go @@ -1,21 +1,106 @@ // Copyright (c) 2023-2024 Nibi, Inc. package evm +import ( + "fmt" + "strconv" + + "cosmossdk.io/errors" + abci "github.com/cometbft/cometbft/abci/types" + sdk "github.com/cosmos/cosmos-sdk/types" + gethcommon "github.com/ethereum/go-ethereum/common" +) + // Evm module events const ( - EventTypeEthereumTx = TypeMsgEthereumTx - EventTypeBlockBloom = "block_bloom" - EventTypeTxLog = "tx_log" - - AttributeKeyRecipient = "recipient" - AttributeKeyTxHash = "txHash" - AttributeKeyEthereumTxHash = "ethereumTxHash" - AttributeKeyTxIndex = "txIndex" - AttributeKeyTxGasUsed = "txGasUsed" - AttributeKeyTxType = "txType" - AttributeKeyTxLog = "txLog" - // tx failed in eth vm execution - AttributeKeyEthereumTxFailed = "ethereumTxFailed" - AttributeValueCategory = ModuleName - AttributeKeyEthereumBloom = "bloom" + // proto.MessageName(new(evm.EventBlockBloom)) + TypeUrlEventBlockBloom = "eth.evm.v1.EventBlockBloom" + + // proto.MessageName(new(evm.EventTxLog)) + TypeUrlEventTxLog = "eth.evm.v1.EventTxLog" + + // proto.MessageName(new(evm.TypeUrlEventEthereumTx)) + TypeUrlEventEthereumTx = "eth.evm.v1.EventEthereumTx" + + // Untyped events and attribuges + + // Used in non-typed event "message" + MessageEventAttrTxType = "tx_type" + + // Used in non-typed event "pending_ethereum_tx" + PendingEthereumTxEvent = "pending_ethereum_tx" + PendingEthereumTxEventAttrEthHash = "eth_hash" + PendingEthereumTxEventAttrIndex = "index" ) + +func EventTxLogFromABCIEvent(event abci.Event) (*EventTxLog, error) { + typeUrl := TypeUrlEventTxLog + typedProtoEvent, err := sdk.ParseTypedEvent(event) + if err != nil { + return nil, errors.Wrapf( + err, "failed to parse event of type %s", typeUrl) + } + typedEvent, ok := (typedProtoEvent).(*EventTxLog) + if !ok { + return nil, errors.Wrapf( + err, "failed to parse event of type %s", typeUrl) + } + return typedEvent, nil +} + +func EventBlockBloomFromABCIEvent(event abci.Event) (*EventBlockBloom, error) { + typeUrl := TypeUrlEventBlockBloom + typedProtoEvent, err := sdk.ParseTypedEvent(event) + if err != nil { + return nil, errors.Wrapf( + err, "failed to parse event of type %s", typeUrl) + } + typedEvent, ok := (typedProtoEvent).(*EventBlockBloom) + if !ok { + return nil, errors.Wrapf( + err, "failed to parse event of type %s", typeUrl) + } + return typedEvent, nil +} + +func EventEthereumTxFromABCIEvent(event abci.Event) (*EventEthereumTx, error) { + typeUrl := TypeUrlEventEthereumTx + typedProtoEvent, err := sdk.ParseTypedEvent(event) + if err != nil { + return nil, errors.Wrapf( + err, "failed to parse event of type %s", typeUrl) + } + typedEvent, ok := (typedProtoEvent).(*EventEthereumTx) + if !ok { + return nil, errors.Wrapf( + err, "failed to parse event of type %s", typeUrl) + } + return typedEvent, nil +} + +func GetEthHashAndIndexFromPendingEthereumTxEvent(event abci.Event) (gethcommon.Hash, int32, error) { + ethHash := gethcommon.Hash{} + txIndex := int32(-1) + + for _, attr := range event.Attributes { + if attr.Key == PendingEthereumTxEventAttrEthHash { + ethHash = gethcommon.HexToHash(attr.Value) + } + if attr.Key == PendingEthereumTxEventAttrIndex { + parsedIndex, err := strconv.ParseInt(attr.Value, 10, 32) + if err != nil { + return ethHash, -1, fmt.Errorf( + "failed to parse tx index from pending_ethereum_tx event, %s", attr.Value, + ) + } + txIndex = int32(parsedIndex) + } + } + if txIndex == -1 { + return ethHash, -1, fmt.Errorf("tx index not found in pending_ethereum_tx") + } + if ethHash.String() == "" { + return ethHash, -1, fmt.Errorf("eth hash not found in pending_ethereum_tx") + } + return ethHash, txIndex, nil +} diff --git a/x/evm/events.pb.go b/x/evm/events.pb.go index e9f40dbd1..918018491 100644 --- a/x/evm/events.pb.go +++ b/x/evm/events.pb.go @@ -170,70 +170,6 @@ func (m *EventTxLog) GetTxLogs() []string { return nil } -// EventMessage -type EventMessage struct { - // module which emits the event - Module string `protobuf:"bytes,1,opt,name=module,proto3" json:"module,omitempty"` - // sender of the message - Sender string `protobuf:"bytes,2,opt,name=sender,proto3" json:"sender,omitempty"` - // tx_type is the type of the message - TxType string `protobuf:"bytes,3,opt,name=tx_type,json=txType,proto3" json:"tx_type,omitempty"` -} - -func (m *EventMessage) Reset() { *m = EventMessage{} } -func (m *EventMessage) String() string { return proto.CompactTextString(m) } -func (*EventMessage) ProtoMessage() {} -func (*EventMessage) Descriptor() ([]byte, []int) { - return fileDescriptor_f8bc26b53c788f17, []int{2} -} -func (m *EventMessage) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *EventMessage) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - if deterministic { - return xxx_messageInfo_EventMessage.Marshal(b, m, deterministic) - } else { - b = b[:cap(b)] - n, err := m.MarshalToSizedBuffer(b) - if err != nil { - return nil, err - } - return b[:n], nil - } -} -func (m *EventMessage) XXX_Merge(src proto.Message) { - xxx_messageInfo_EventMessage.Merge(m, src) -} -func (m *EventMessage) XXX_Size() int { - return m.Size() -} -func (m *EventMessage) XXX_DiscardUnknown() { - xxx_messageInfo_EventMessage.DiscardUnknown(m) -} - -var xxx_messageInfo_EventMessage proto.InternalMessageInfo - -func (m *EventMessage) GetModule() string { - if m != nil { - return m.Module - } - return "" -} - -func (m *EventMessage) GetSender() string { - if m != nil { - return m.Sender - } - return "" -} - -func (m *EventMessage) GetTxType() string { - if m != nil { - return m.TxType - } - return "" -} - // EventBlockBloom defines an Ethereum block bloom filter event type EventBlockBloom struct { // bloom is the bloom filter of the block @@ -244,7 +180,7 @@ func (m *EventBlockBloom) Reset() { *m = EventBlockBloom{} } func (m *EventBlockBloom) String() string { return proto.CompactTextString(m) } func (*EventBlockBloom) ProtoMessage() {} func (*EventBlockBloom) Descriptor() ([]byte, []int) { - return fileDescriptor_f8bc26b53c788f17, []int{3} + return fileDescriptor_f8bc26b53c788f17, []int{2} } func (m *EventBlockBloom) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -292,7 +228,7 @@ func (m *EventFunTokenCreated) Reset() { *m = EventFunTokenCreated{} } func (m *EventFunTokenCreated) String() string { return proto.CompactTextString(m) } func (*EventFunTokenCreated) ProtoMessage() {} func (*EventFunTokenCreated) Descriptor() ([]byte, []int) { - return fileDescriptor_f8bc26b53c788f17, []int{4} + return fileDescriptor_f8bc26b53c788f17, []int{3} } func (m *EventFunTokenCreated) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -361,7 +297,7 @@ func (m *EventConvertCoinToEvm) Reset() { *m = EventConvertCoinToEvm{} } func (m *EventConvertCoinToEvm) String() string { return proto.CompactTextString(m) } func (*EventConvertCoinToEvm) ProtoMessage() {} func (*EventConvertCoinToEvm) Descriptor() ([]byte, []int) { - return fileDescriptor_f8bc26b53c788f17, []int{5} + return fileDescriptor_f8bc26b53c788f17, []int{4} } func (m *EventConvertCoinToEvm) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -429,7 +365,7 @@ func (m *EventTransfer) Reset() { *m = EventTransfer{} } func (m *EventTransfer) String() string { return proto.CompactTextString(m) } func (*EventTransfer) ProtoMessage() {} func (*EventTransfer) Descriptor() ([]byte, []int) { - return fileDescriptor_f8bc26b53c788f17, []int{6} + return fileDescriptor_f8bc26b53c788f17, []int{5} } func (m *EventTransfer) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -489,7 +425,7 @@ func (m *EventContractDeployed) Reset() { *m = EventContractDeployed{} } func (m *EventContractDeployed) String() string { return proto.CompactTextString(m) } func (*EventContractDeployed) ProtoMessage() {} func (*EventContractDeployed) Descriptor() ([]byte, []int) { - return fileDescriptor_f8bc26b53c788f17, []int{7} + return fileDescriptor_f8bc26b53c788f17, []int{6} } func (m *EventContractDeployed) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -542,7 +478,7 @@ func (m *EventContractExecuted) Reset() { *m = EventContractExecuted{} } func (m *EventContractExecuted) String() string { return proto.CompactTextString(m) } func (*EventContractExecuted) ProtoMessage() {} func (*EventContractExecuted) Descriptor() ([]byte, []int) { - return fileDescriptor_f8bc26b53c788f17, []int{8} + return fileDescriptor_f8bc26b53c788f17, []int{7} } func (m *EventContractExecuted) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -588,7 +524,6 @@ func (m *EventContractExecuted) GetContractAddr() string { func init() { proto.RegisterType((*EventEthereumTx)(nil), "eth.evm.v1.EventEthereumTx") proto.RegisterType((*EventTxLog)(nil), "eth.evm.v1.EventTxLog") - proto.RegisterType((*EventMessage)(nil), "eth.evm.v1.EventMessage") proto.RegisterType((*EventBlockBloom)(nil), "eth.evm.v1.EventBlockBloom") proto.RegisterType((*EventFunTokenCreated)(nil), "eth.evm.v1.EventFunTokenCreated") proto.RegisterType((*EventConvertCoinToEvm)(nil), "eth.evm.v1.EventConvertCoinToEvm") @@ -600,49 +535,46 @@ func init() { func init() { proto.RegisterFile("eth/evm/v1/events.proto", fileDescriptor_f8bc26b53c788f17) } var fileDescriptor_f8bc26b53c788f17 = []byte{ - // 658 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x54, 0xcb, 0x6e, 0xd4, 0x3c, - 0x14, 0x9e, 0xf4, 0x32, 0xd3, 0x71, 0xdb, 0xff, 0x07, 0x6b, 0x68, 0xd3, 0x0a, 0xd2, 0x2a, 0x88, - 0xdb, 0x26, 0x61, 0x0a, 0x2b, 0x56, 0x30, 0xd3, 0xa9, 0x58, 0x50, 0x84, 0xaa, 0x41, 0x48, 0x48, - 0x28, 0x72, 0x92, 0xd3, 0x24, 0x6a, 0x62, 0x8f, 0x6c, 0x27, 0xca, 0xbc, 0x05, 0x8f, 0xc2, 0x63, - 0x54, 0x62, 0xd3, 0x1d, 0xac, 0x2a, 0xd4, 0xbe, 0x01, 0x4f, 0x80, 0xec, 0xb8, 0x9d, 0x16, 0xd4, - 0x0d, 0xec, 0xce, 0xf9, 0xce, 0xc5, 0xe7, 0xfb, 0x7c, 0x6c, 0xb4, 0x0e, 0x32, 0xf5, 0xa1, 0x2a, - 0xfc, 0xaa, 0xef, 0x43, 0x05, 0x54, 0x0a, 0x6f, 0xc2, 0x99, 0x64, 0x18, 0x81, 0x4c, 0x3d, 0xa8, - 0x0a, 0xaf, 0xea, 0x6f, 0x3a, 0x11, 0x13, 0x05, 0x13, 0x7e, 0x48, 0x04, 0xf8, 0x55, 0x3f, 0x04, - 0x49, 0xfa, 0x7e, 0xc4, 0x32, 0xda, 0xe4, 0x6e, 0xf6, 0x12, 0x96, 0x30, 0x6d, 0xfa, 0xca, 0x6a, - 0x50, 0xf7, 0xab, 0x85, 0xfe, 0x1f, 0xa9, 0x96, 0x23, 0x99, 0x02, 0x87, 0xb2, 0x18, 0xd7, 0x78, - 0x0d, 0xb5, 0x49, 0xc1, 0x4a, 0x2a, 0x6d, 0x6b, 0xdb, 0x7a, 0xdc, 0x3d, 0x30, 0x1e, 0xde, 0x40, - 0x4b, 0x20, 0xd3, 0x20, 0x25, 0x22, 0xb5, 0xe7, 0x74, 0xa4, 0x03, 0x32, 0x7d, 0x4d, 0x44, 0x8a, - 0x7b, 0x68, 0x31, 0xa3, 0x31, 0xd4, 0xf6, 0xbc, 0xc6, 0x1b, 0x47, 0x15, 0x24, 0x44, 0x04, 0xa5, - 0x80, 0xd8, 0x5e, 0x68, 0x0a, 0x12, 0x22, 0xde, 0x0b, 0x88, 0x31, 0x46, 0x0b, 0xba, 0xcf, 0xa2, - 0x86, 0xb5, 0x8d, 0xef, 0xa2, 0x2e, 0x87, 0x28, 0x9b, 0x64, 0x40, 0xa5, 0xdd, 0xd6, 0x81, 0x19, - 0x80, 0x5d, 0xb4, 0xaa, 0x4e, 0x97, 0x75, 0x70, 0x48, 0xb2, 0x1c, 0x62, 0xbb, 0xa3, 0x33, 0x96, - 0x41, 0xa6, 0xe3, 0x7a, 0x4f, 0x43, 0xee, 0x03, 0x84, 0x34, 0x99, 0x71, 0xfd, 0x86, 0x25, 0x78, - 0x1d, 0x75, 0x64, 0x1d, 0xe4, 0x2c, 0x11, 0xb6, 0xb5, 0x3d, 0xaf, 0x88, 0x48, 0x85, 0x0b, 0xf7, - 0x03, 0x5a, 0xd1, 0x69, 0xfb, 0x20, 0x04, 0x49, 0x40, 0x11, 0x2e, 0x58, 0x5c, 0xe6, 0x70, 0x41, - 0xb8, 0xf1, 0x14, 0x2e, 0x80, 0xc6, 0xc0, 0x0d, 0x5d, 0xe3, 0x99, 0xc6, 0x72, 0x3a, 0x01, 0xc3, - 0xb7, 0x2d, 0xeb, 0xf1, 0x74, 0x02, 0xee, 0x23, 0x23, 0xe6, 0x20, 0x67, 0xd1, 0xd1, 0x20, 0x67, - 0xac, 0x50, 0xca, 0x84, 0xca, 0x30, 0xad, 0x1b, 0xc7, 0xfd, 0x62, 0xa1, 0x9e, 0xce, 0xdc, 0x2b, - 0xe9, 0x98, 0x1d, 0x01, 0x1d, 0x72, 0x20, 0x12, 0x62, 0x7c, 0x0f, 0xa1, 0x90, 0xd0, 0xa3, 0x20, - 0x06, 0x7a, 0x59, 0xd3, 0x55, 0xc8, 0xae, 0x02, 0xf0, 0x73, 0xb4, 0x06, 0x3c, 0xda, 0x79, 0x1a, - 0x44, 0x8c, 0x4a, 0x4e, 0x22, 0x19, 0x90, 0x38, 0xe6, 0x20, 0x84, 0x99, 0xb0, 0xa7, 0xa3, 0x43, - 0x13, 0x7c, 0xd5, 0xc4, 0xb0, 0x8d, 0x3a, 0x91, 0xea, 0xcf, 0xb8, 0x99, 0xf7, 0xc2, 0xc5, 0x4f, - 0xd0, 0xed, 0x4c, 0x04, 0x05, 0x89, 0x21, 0x38, 0xe4, 0xac, 0x08, 0xd4, 0xbe, 0xe8, 0xab, 0x5a, - 0x3a, 0xf8, 0x2f, 0x13, 0xfb, 0x24, 0x86, 0x3d, 0xce, 0x8a, 0x21, 0xcb, 0xa8, 0xfb, 0xcd, 0x42, - 0x77, 0xf4, 0xc8, 0x43, 0x46, 0x2b, 0xe0, 0x52, 0x81, 0x63, 0x36, 0xaa, 0x8a, 0x2b, 0x32, 0x59, - 0xd7, 0x64, 0xfa, 0xbb, 0x61, 0x1d, 0xb4, 0x2c, 0x59, 0xa0, 0xae, 0x5a, 0x65, 0x9b, 0x81, 0xbb, - 0x92, 0x8d, 0x64, 0xaa, 0x52, 0xf0, 0x3b, 0xa4, 0xf5, 0x98, 0x8d, 0xba, 0xbc, 0xb3, 0xe1, 0x35, - 0xbb, 0xef, 0xa9, 0xdd, 0xf7, 0xcc, 0xee, 0x7b, 0x6a, 0xc0, 0x81, 0x7d, 0x7c, 0xba, 0xd5, 0xfa, - 0x79, 0xba, 0x75, 0x6b, 0x4a, 0x8a, 0xfc, 0x85, 0x7b, 0x59, 0xe9, 0x1e, 0x2c, 0x29, 0x5b, 0x33, - 0xfb, 0x84, 0x56, 0x9b, 0xad, 0xe1, 0x84, 0x8a, 0x43, 0xe0, 0x37, 0x12, 0xba, 0xb6, 0xa0, 0x73, - 0xbf, 0x2f, 0xe8, 0xec, 0xd9, 0xcc, 0x5f, 0x7d, 0x36, 0xee, 0x78, 0xa6, 0x9b, 0x26, 0xba, 0x0b, - 0x93, 0x9c, 0x4d, 0x21, 0xbe, 0xf1, 0x98, 0xfb, 0x68, 0xf5, 0x9a, 0x62, 0xe6, 0xa8, 0x95, 0xe8, - 0x8a, 0x52, 0x7f, 0x74, 0x1d, 0xd5, 0x10, 0x95, 0xf2, 0x1f, 0xbb, 0x0e, 0x5e, 0x1e, 0x9f, 0x39, - 0xd6, 0xc9, 0x99, 0x63, 0xfd, 0x38, 0x73, 0xac, 0xcf, 0xe7, 0x4e, 0xeb, 0xe4, 0xdc, 0x69, 0x7d, - 0x3f, 0x77, 0x5a, 0x1f, 0x1f, 0x26, 0x99, 0x4c, 0xcb, 0xd0, 0x8b, 0x58, 0xe1, 0xbf, 0xcd, 0xc2, - 0x8c, 0x97, 0xc3, 0x94, 0x64, 0xd4, 0xa7, 0xda, 0xf6, 0xab, 0x1d, 0xbf, 0x56, 0x5f, 0x54, 0xd8, - 0xd6, 0xff, 0xca, 0xb3, 0x5f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x18, 0xd0, 0xf1, 0x1b, 0xb4, 0x04, - 0x00, 0x00, + // 618 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x54, 0x4b, 0x6e, 0xd4, 0x40, + 0x10, 0x1d, 0xe7, 0x37, 0x99, 0x0e, 0xe1, 0xd3, 0x1a, 0x12, 0x27, 0x02, 0x27, 0x32, 0xe2, 0xb7, + 0xb1, 0x99, 0xc0, 0x8a, 0x15, 0xcc, 0x64, 0x22, 0x16, 0x80, 0x50, 0x34, 0x6c, 0x90, 0x90, 0xd5, + 0xb6, 0x2b, 0xb6, 0x15, 0xbb, 0x2b, 0xea, 0x6e, 0x5b, 0xce, 0x2d, 0x38, 0x0a, 0xc7, 0x88, 0xc4, + 0x26, 0x3b, 0x58, 0x45, 0x28, 0xb9, 0x01, 0x27, 0x40, 0xdd, 0x76, 0x32, 0x09, 0x28, 0x1b, 0xd8, + 0x55, 0xbd, 0xfa, 0x74, 0xbd, 0xaa, 0x67, 0x93, 0x55, 0x50, 0xa9, 0x0f, 0x55, 0xe1, 0x57, 0x03, + 0x1f, 0x2a, 0xe0, 0x4a, 0x7a, 0x07, 0x02, 0x15, 0x52, 0x02, 0x2a, 0xf5, 0xa0, 0x2a, 0xbc, 0x6a, + 0xb0, 0xee, 0x44, 0x28, 0x0b, 0x94, 0x7e, 0xc8, 0x24, 0xf8, 0xd5, 0x20, 0x04, 0xc5, 0x06, 0x7e, + 0x84, 0x19, 0x6f, 0x72, 0xd7, 0xfb, 0x09, 0x26, 0x68, 0x4c, 0x5f, 0x5b, 0x0d, 0xea, 0x7e, 0xb3, + 0xc8, 0xad, 0xb1, 0x6e, 0x39, 0x56, 0x29, 0x08, 0x28, 0x8b, 0x49, 0x4d, 0x57, 0xc8, 0x02, 0x2b, + 0xb0, 0xe4, 0xca, 0xb6, 0x36, 0xad, 0x27, 0xbd, 0xdd, 0xd6, 0xa3, 0x6b, 0x64, 0x11, 0x54, 0x1a, + 0xa4, 0x4c, 0xa6, 0xf6, 0x8c, 0x89, 0x74, 0x41, 0xa5, 0x6f, 0x98, 0x4c, 0x69, 0x9f, 0xcc, 0x67, + 0x3c, 0x86, 0xda, 0x9e, 0x35, 0x78, 0xe3, 0xe8, 0x82, 0x84, 0xc9, 0xa0, 0x94, 0x10, 0xdb, 0x73, + 0x4d, 0x41, 0xc2, 0xe4, 0x47, 0x09, 0x31, 0xa5, 0x64, 0xce, 0xf4, 0x99, 0x37, 0xb0, 0xb1, 0xe9, + 0x3d, 0xd2, 0x13, 0x10, 0x65, 0x07, 0x19, 0x70, 0x65, 0x2f, 0x98, 0xc0, 0x14, 0xa0, 0x2e, 0x59, + 0xd6, 0xaf, 0xab, 0x3a, 0xd8, 0x63, 0x59, 0x0e, 0xb1, 0xdd, 0x35, 0x19, 0x4b, 0xa0, 0xd2, 0x49, + 0xbd, 0x63, 0x20, 0xf7, 0x21, 0x21, 0x86, 0xcc, 0xa4, 0x7e, 0x8b, 0x09, 0x5d, 0x25, 0x5d, 0x55, + 0x07, 0x39, 0x26, 0xd2, 0xb6, 0x36, 0x67, 0x35, 0x11, 0xa5, 0x71, 0xe9, 0x3e, 0x6e, 0x39, 0x0f, + 0x73, 0x8c, 0xf6, 0x87, 0x39, 0x62, 0xa1, 0x09, 0x84, 0xda, 0x68, 0x29, 0x37, 0x8e, 0xfb, 0xd5, + 0x22, 0x7d, 0x93, 0xb9, 0x53, 0xf2, 0x09, 0xee, 0x03, 0x1f, 0x09, 0x60, 0x0a, 0x62, 0x7a, 0x9f, + 0x90, 0x90, 0xf1, 0xfd, 0x20, 0x06, 0x7e, 0x51, 0xd3, 0xd3, 0xc8, 0xb6, 0x06, 0xe8, 0x0b, 0xb2, + 0x02, 0x22, 0xda, 0x7a, 0x16, 0x44, 0xc8, 0x95, 0x60, 0x91, 0x0a, 0x58, 0x1c, 0x0b, 0x90, 0xb2, + 0xdd, 0x5b, 0xdf, 0x44, 0x47, 0x6d, 0xf0, 0x75, 0x13, 0xa3, 0x36, 0xe9, 0x46, 0xba, 0x3f, 0x8a, + 0x76, 0x8d, 0xe7, 0x2e, 0x7d, 0x4a, 0xee, 0x64, 0x32, 0x28, 0x58, 0x0c, 0xc1, 0x9e, 0xc0, 0x22, + 0xd0, 0x67, 0x35, 0x1b, 0x5d, 0xdc, 0xbd, 0x99, 0xc9, 0x77, 0x2c, 0x86, 0x1d, 0x81, 0xc5, 0x08, + 0x33, 0xee, 0x7e, 0xb7, 0xc8, 0x5d, 0x33, 0xf2, 0x08, 0x79, 0x05, 0x42, 0x69, 0x70, 0x82, 0xe3, + 0xaa, 0xd0, 0x67, 0x95, 0xc0, 0x63, 0x10, 0xe7, 0x67, 0x6d, 0xbc, 0x7f, 0x1c, 0xd6, 0x21, 0x4b, + 0x0a, 0x03, 0x7d, 0x11, 0x9d, 0xdd, 0x0e, 0xdc, 0x53, 0x38, 0x56, 0xa9, 0x4e, 0xa1, 0x1f, 0x88, + 0xd9, 0xc7, 0x74, 0xd4, 0xa5, 0xad, 0x35, 0xaf, 0x91, 0xa8, 0xa7, 0x25, 0xea, 0xb5, 0x12, 0xf5, + 0xf4, 0x80, 0x43, 0xfb, 0xe8, 0x64, 0xa3, 0xf3, 0xeb, 0x64, 0xe3, 0xf6, 0x21, 0x2b, 0xf2, 0x97, + 0xee, 0x45, 0xa5, 0xbb, 0xbb, 0xa8, 0x6d, 0xc3, 0xec, 0x33, 0x59, 0x6e, 0x8e, 0x2b, 0x18, 0x97, + 0x7b, 0x20, 0xae, 0x25, 0x74, 0x45, 0x47, 0x33, 0x7f, 0xea, 0x68, 0xaa, 0xee, 0xd9, 0xcb, 0xea, + 0x76, 0x27, 0xd3, 0xbd, 0x19, 0xa2, 0xdb, 0x70, 0x90, 0xe3, 0x21, 0xc4, 0xd7, 0x3e, 0xf3, 0x80, + 0x2c, 0x5f, 0xd9, 0x58, 0xfb, 0xd4, 0x8d, 0xe8, 0xd2, 0xa6, 0xfe, 0xea, 0x3a, 0xae, 0x21, 0x2a, + 0xd5, 0x7f, 0x76, 0x1d, 0xbe, 0x3a, 0x3a, 0x75, 0xac, 0xe3, 0x53, 0xc7, 0xfa, 0x79, 0xea, 0x58, + 0x5f, 0xce, 0x9c, 0xce, 0xf1, 0x99, 0xd3, 0xf9, 0x71, 0xe6, 0x74, 0x3e, 0x3d, 0x4a, 0x32, 0x95, + 0x96, 0xa1, 0x17, 0x61, 0xe1, 0xbf, 0xcf, 0xc2, 0x4c, 0x94, 0xa3, 0x94, 0x65, 0xdc, 0xe7, 0xc6, + 0xf6, 0xab, 0x2d, 0xbf, 0xd6, 0x7f, 0x92, 0x70, 0xc1, 0x7c, 0xfe, 0xcf, 0x7f, 0x07, 0x00, 0x00, + 0xff, 0xff, 0x1c, 0xa0, 0xad, 0x46, 0x5b, 0x04, 0x00, 0x00, } func (m *EventEthereumTx) Marshal() (dAtA []byte, err error) { @@ -749,50 +681,6 @@ func (m *EventTxLog) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } -func (m *EventMessage) Marshal() (dAtA []byte, err error) { - size := m.Size() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBuffer(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *EventMessage) MarshalTo(dAtA []byte) (int, error) { - size := m.Size() - return m.MarshalToSizedBuffer(dAtA[:size]) -} - -func (m *EventMessage) MarshalToSizedBuffer(dAtA []byte) (int, error) { - i := len(dAtA) - _ = i - var l int - _ = l - if len(m.TxType) > 0 { - i -= len(m.TxType) - copy(dAtA[i:], m.TxType) - i = encodeVarintEvents(dAtA, i, uint64(len(m.TxType))) - i-- - dAtA[i] = 0x1a - } - if len(m.Sender) > 0 { - i -= len(m.Sender) - copy(dAtA[i:], m.Sender) - i = encodeVarintEvents(dAtA, i, uint64(len(m.Sender))) - i-- - dAtA[i] = 0x12 - } - if len(m.Module) > 0 { - i -= len(m.Module) - copy(dAtA[i:], m.Module) - i = encodeVarintEvents(dAtA, i, uint64(len(m.Module))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - func (m *EventBlockBloom) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) @@ -1112,27 +1000,6 @@ func (m *EventTxLog) Size() (n int) { return n } -func (m *EventMessage) Size() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Module) - if l > 0 { - n += 1 + l + sovEvents(uint64(l)) - } - l = len(m.Sender) - if l > 0 { - n += 1 + l + sovEvents(uint64(l)) - } - l = len(m.TxType) - if l > 0 { - n += 1 + l + sovEvents(uint64(l)) - } - return n -} - func (m *EventBlockBloom) Size() (n int) { if m == nil { return 0 @@ -1610,152 +1477,6 @@ func (m *EventTxLog) Unmarshal(dAtA []byte) error { } return nil } -func (m *EventMessage) Unmarshal(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowEvents - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: EventMessage: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: EventMessage: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Module", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowEvents - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthEvents - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLengthEvents - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Module = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Sender", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowEvents - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthEvents - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLengthEvents - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Sender = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 3: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field TxType", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowEvents - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthEvents - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLengthEvents - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.TxType = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skipEvents(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLengthEvents - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} func (m *EventBlockBloom) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 diff --git a/x/evm/keeper/msg_server.go b/x/evm/keeper/msg_server.go index 10fdfb131..36f2d2d9b 100644 --- a/x/evm/keeper/msg_server.go +++ b/x/evm/keeper/msg_server.go @@ -41,61 +41,6 @@ func (k *Keeper) EthereumTx( if err != nil { return nil, errors.Wrap(err, "failed to apply transaction") } - - attrs := []sdk.Attribute{ - sdk.NewAttribute(sdk.AttributeKeyAmount, tx.Value().String()), - // add event for ethereum transaction hash format - sdk.NewAttribute(evm.AttributeKeyEthereumTxHash, resp.Hash), - // add event for index of valid ethereum tx - sdk.NewAttribute(evm.AttributeKeyTxIndex, strconv.FormatUint(k.EvmState.BlockTxIndex.GetOr(ctx, 0), 10)), - // add event for eth tx gas used, we can't get it from cosmos tx result when it contains multiple eth tx msgs. - sdk.NewAttribute(evm.AttributeKeyTxGasUsed, strconv.FormatUint(resp.GasUsed, 10)), - // TODO: fix: It's odd that each event is emitted twice. Migrate to typed - // events and change EVM indexer to align. - // sdk.NewAttribute("emitted_from", "EthereumTx"), - } - - if len(ctx.TxBytes()) > 0 { - // add event for tendermint transaction hash format - hash := tmbytes.HexBytes(tmtypes.Tx(ctx.TxBytes()).Hash()) - attrs = append(attrs, sdk.NewAttribute(evm.AttributeKeyTxHash, hash.String())) - } - - if to := tx.To(); to != nil { - attrs = append(attrs, sdk.NewAttribute(evm.AttributeKeyRecipient, to.Hex())) - } - - if resp.Failed() { - attrs = append(attrs, sdk.NewAttribute(evm.AttributeKeyEthereumTxFailed, resp.VmError)) - } - - txLogAttrs := make([]sdk.Attribute, len(resp.Logs)) - for i, log := range resp.Logs { - value, err := json.Marshal(log) - if err != nil { - return nil, errors.Wrap(err, "failed to encode log") - } - txLogAttrs[i] = sdk.NewAttribute(evm.AttributeKeyTxLog, string(value)) - } - - // emit events - ctx.EventManager().EmitEvents(sdk.Events{ - sdk.NewEvent( - evm.EventTypeEthereumTx, - attrs..., - ), - sdk.NewEvent( - evm.EventTypeTxLog, - txLogAttrs..., - ), - sdk.NewEvent( - sdk.EventTypeMessage, - sdk.NewAttribute(sdk.AttributeKeyModule, evm.AttributeValueCategory), - sdk.NewAttribute(sdk.AttributeKeySender, msg.From), - sdk.NewAttribute(evm.AttributeKeyTxType, fmt.Sprintf("%d", tx.Type())), - ), - }) - return resp, nil } @@ -118,7 +63,7 @@ func (k *Keeper) ApplyEvmTx( tmpCtx, commit := ctx.CacheContext() // pass true to commit the StateDB - res, err := k.ApplyEvmMsg(tmpCtx, msg, nil, true, evmConfig, txConfig) + evmResp, err := k.ApplyEvmMsg(tmpCtx, msg, nil, true, evmConfig, txConfig) if err != nil { // when a transaction contains multiple msg, as long as one of the msg fails // all gas will be deducted. so is not msg.Gas() @@ -126,9 +71,9 @@ func (k *Keeper) ApplyEvmTx( return nil, errors.Wrap(err, "failed to apply ethereum core message") } - logs := evm.LogsToEthereum(res.Logs) + logs := evm.LogsToEthereum(evmResp.Logs) - cumulativeGasUsed := res.GasUsed + cumulativeGasUsed := evmResp.GasUsed if ctx.BlockGasMeter() != nil { limit := ctx.BlockGasMeter().Limit() cumulativeGasUsed += ctx.BlockGasMeter().GasConsumed() @@ -150,40 +95,24 @@ func (k *Keeper) ApplyEvmTx( Logs: logs, TxHash: txConfig.TxHash, ContractAddress: contractAddr, - GasUsed: res.GasUsed, + GasUsed: evmResp.GasUsed, BlockHash: txConfig.BlockHash, BlockNumber: big.NewInt(ctx.BlockHeight()), TransactionIndex: txConfig.TxIndex, } - if !res.Failed() { + if !evmResp.Failed() { receipt.Status = gethcore.ReceiptStatusSuccessful commit() - ctx.EventManager().EmitEvents(tmpCtx.EventManager().Events()) - - // Emit typed events - if msg.To() == nil { // contract creation - _ = ctx.EventManager().EmitTypedEvent(&evm.EventContractDeployed{ - Sender: msg.From().String(), - ContractAddr: contractAddr.String(), - }) - } else if len(msg.Data()) > 0 { // contract executed - _ = ctx.EventManager().EmitTypedEvent(&evm.EventContractExecuted{ - Sender: msg.From().String(), - ContractAddr: msg.To().String(), - }) - } else if msg.Value().Cmp(big.NewInt(0)) > 0 { // evm transfer - _ = ctx.EventManager().EmitTypedEvent(&evm.EventTransfer{ - Sender: msg.From().String(), - Recipient: msg.To().String(), - Amount: msg.Value().String(), - }) - } } // refund gas in order to match the Ethereum gas consumption instead of the default SDK one. - if err = k.RefundGas(ctx, msg, msg.Gas()-res.GasUsed, evmConfig.Params.EvmDenom); err != nil { - return nil, errors.Wrapf(err, "failed to refund gas leftover gas to sender %s", msg.From()) + refundGas := uint64(0) + if msg.Gas() > evmResp.GasUsed { + refundGas = msg.Gas() - evmResp.GasUsed + } + if err = k.RefundGas(ctx, msg, refundGas, evmConfig.Params.EvmDenom); err != nil { + return nil, errors.Wrapf(err, "failed to refund leftover gas to sender %s", msg.From()) } if len(receipt.Logs) > 0 { @@ -193,17 +122,23 @@ func (k *Keeper) ApplyEvmTx( k.EvmState.BlockLogSize.Set(ctx, blockLogSize) } - blockTxIdx := uint64(txConfig.TxIndex) + 1 - k.EvmState.BlockTxIndex.Set(ctx, blockTxIdx) - - totalGasUsed, err := k.AddToBlockGasUsed(ctx, res.GasUsed) + totalGasUsed, err := k.AddToBlockGasUsed(ctx, evmResp.GasUsed) if err != nil { return nil, errors.Wrap(err, "failed to add transient gas used") } // reset the gas meter for current cosmos transaction k.ResetGasMeterAndConsumeGas(ctx, totalGasUsed) - return res, nil + + err = k.EmitEthereumTxEvents(ctx, tx, msg, evmResp, contractAddr) + if err != nil { + return nil, errors.Wrap(err, "failed to emit ethereum tx events") + } + + blockTxIdx := uint64(txConfig.TxIndex) + 1 + k.EvmState.BlockTxIndex.Set(ctx, blockTxIdx) + + return evmResp, nil } // NewEVM generates a go-ethereum VM. @@ -734,3 +669,76 @@ func (k Keeper) convertCoinNativeERC20( return &evm.MsgConvertCoinToEvmResponse{}, nil } + +// EmitEthereumTxEvents emits all types of EVM events applicable to a particular execution case +func (k *Keeper) EmitEthereumTxEvents( + ctx sdk.Context, + tx *gethcore.Transaction, + msg gethcore.Message, + evmResp *evm.MsgEthereumTxResponse, + contractAddr gethcommon.Address, +) error { + // Typed event: eth.evm.v1.EventEthereumTx + eventEthereumTx := &evm.EventEthereumTx{ + EthHash: evmResp.Hash, + Index: strconv.FormatUint(k.EvmState.BlockTxIndex.GetOr(ctx, 0), 10), + GasUsed: strconv.FormatUint(evmResp.GasUsed, 10), + } + if len(ctx.TxBytes()) > 0 { + eventEthereumTx.Hash = tmbytes.HexBytes(tmtypes.Tx(ctx.TxBytes()).Hash()).String() + } + if to := tx.To(); to != nil { + eventEthereumTx.Recipient = to.Hex() + } + if evmResp.Failed() { + eventEthereumTx.EthTxFailed = evmResp.VmError + } + err := ctx.EventManager().EmitTypedEvent(eventEthereumTx) + if err != nil { + return errors.Wrap(err, "failed to emit event ethereum tx") + } + + // Typed event: eth.evm.v1.EventTxLog + txLogs := make([]string, len(evmResp.Logs)) + for i, log := range evmResp.Logs { + value, err := json.Marshal(log) + if err != nil { + return errors.Wrap(err, "failed to encode log") + } + txLogs[i] = string(value) + } + _ = ctx.EventManager().EmitTypedEvent(&evm.EventTxLog{TxLogs: txLogs}) + + // Untyped event: "message", used for tendermint subscription + ctx.EventManager().EmitEvent( + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, evm.ModuleName), + sdk.NewAttribute(sdk.AttributeKeySender, msg.From().Hex()), + sdk.NewAttribute(evm.MessageEventAttrTxType, fmt.Sprintf("%d", tx.Type())), + ), + ) + + // Emit typed events + if !evmResp.Failed() { + if tx.To() == nil { // contract creation + _ = ctx.EventManager().EmitTypedEvent(&evm.EventContractDeployed{ + Sender: msg.From().Hex(), + ContractAddr: contractAddr.String(), + }) + } else if len(msg.Data()) > 0 { // contract executed + _ = ctx.EventManager().EmitTypedEvent(&evm.EventContractExecuted{ + Sender: msg.From().Hex(), + ContractAddr: msg.To().String(), + }) + } else if msg.Value().Cmp(big.NewInt(0)) > 0 { // evm transfer + _ = ctx.EventManager().EmitTypedEvent(&evm.EventTransfer{ + Sender: msg.From().Hex(), + Recipient: msg.To().Hex(), + Amount: msg.Value().String(), + }) + } + } + + return nil +} diff --git a/x/evm/msg.go b/x/evm/msg.go index dc15727c5..f27cade13 100644 --- a/x/evm/msg.go +++ b/x/evm/msg.go @@ -38,12 +38,6 @@ var ( _ codectypes.UnpackInterfacesMessage = MsgEthereumTx{} ) -// message type and route constants -const ( - // TypeMsgEthereumTx defines the type string of an Ethereum transaction - TypeMsgEthereumTx = "ethereum_tx" -) - // NewTx returns a reference to a new Ethereum transaction message. func NewTx( tx *EvmTxArgs, @@ -147,8 +141,7 @@ func (msg *MsgEthereumTx) FromEthereumTx(tx *gethcore.Transaction) error { // Route returns the route value of an MsgEthereumTx. func (msg MsgEthereumTx) Route() string { return RouterKey } -// Type returns the type value of an MsgEthereumTx. -func (msg MsgEthereumTx) Type() string { return TypeMsgEthereumTx } +func (msg MsgEthereumTx) Type() string { return proto.MessageName(new(MsgEthereumTx)) } // ValidateBasic implements the sdk.Msg interface. It performs basic validation // checks of a Transaction. If returns an error if validation fails. @@ -185,7 +178,7 @@ func (msg MsgEthereumTx) ValidateBasic() error { return errorsmod.Wrap(err, "failed \"TxData.Validate\"") } - // Validate Hash field after validated txData to avoid panic + // Validate EthHash field after validated txData to avoid panic txHash := msg.AsTransaction().Hash().Hex() if msg.Hash != txHash { return errorsmod.Wrapf(errortypes.ErrInvalidRequest, "invalid tx hash %s, expected: %s", msg.Hash, txHash) diff --git a/x/evm/msg_test.go b/x/evm/msg_test.go index 98beaafdc..0dd7d8fb9 100644 --- a/x/evm/msg_test.go +++ b/x/evm/msg_test.go @@ -66,7 +66,6 @@ func (s *MsgsSuite) TestMsgEthereumTx_Constructor() { // suite.Require().Equal(msg.Data.To, suite.to.Hex()) s.Require().Equal(msg.Route(), evm.RouterKey) - s.Require().Equal(msg.Type(), evm.TypeMsgEthereumTx) // suite.Require().NotNil(msg.To()) s.Require().Equal(msg.GetMsgs(), []sdk.Msg{msg}) s.Require().Panics(func() { msg.GetSigners() }) From 158874497044d1e32e58ac44a54852da7778f982 Mon Sep 17 00:00:00 2001 From: Unique Divine <51418232+Unique-Divine@users.noreply.github.com> Date: Thu, 3 Oct 2024 07:32:25 -0500 Subject: [PATCH 15/16] fix(evm-precompiles): add assertNumArgs validation (#2060) --- CHANGELOG.md | 1 + x/evm/precompile/errors.go | 10 ++++++++++ x/evm/precompile/funtoken.go | 7 ++++--- x/evm/precompile/wasm.go | 7 ++++--- x/evm/precompile/wasm_parse.go | 28 ++++++++++++++++------------ 5 files changed, 35 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 737577fb3..50af1cb18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -125,6 +125,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#2045](https://github.com/NibiruChain/nibiru/pull/2045) - test(evm): backend tests with test network and real txs - [#2053](https://github.com/NibiruChain/nibiru/pull/2053) - refactor(evm): converted untyped event to typed and cleaned up - [#2054](https://github.com/NibiruChain/nibiru/pull/2054) - feat(evm-precompile): Precompile for one-way EVM calls to invoke/execute Wasm contracts. +- [#2060](https://github.com/NibiruChain/nibiru/pull/2060) - fix(evm-precompiles): add assertNumArgs validation #### Dapp modules: perp, spot, oracle, etc diff --git a/x/evm/precompile/errors.go b/x/evm/precompile/errors.go index 5f4ee88da..f22ed9f7e 100644 --- a/x/evm/precompile/errors.go +++ b/x/evm/precompile/errors.go @@ -43,3 +43,13 @@ func assertContractQuery(contract *vm.Contract) error { return nil } + +// assertNumArgs checks if the number of provided arguments matches the expected +// count. If lenArgs does not equal wantArgsLen, it returns an error describing +// the mismatch between expected and actual argument counts. +func assertNumArgs(lenArgs, wantArgsLen int) error { + if lenArgs != wantArgsLen { + return fmt.Errorf("expected %d arguments but got %d", wantArgsLen, lenArgs) + } + return nil +} diff --git a/x/evm/precompile/funtoken.go b/x/evm/precompile/funtoken.go index 6eaf1bbff..042544269 100644 --- a/x/evm/precompile/funtoken.go +++ b/x/evm/precompile/funtoken.go @@ -205,9 +205,10 @@ func (p precompileFunToken) decomposeBankSendArgs(args []any) ( to string, err error, ) { - // Note: The number of arguments is valiated before this function is called - // during "DecomposeInput". DecomposeInput calls "method.Inputs.Unpack", - // which validates against the the structure of the precompile's ABI. + if e := assertNumArgs(len(args), 3); e != nil { + err = e + return + } erc20, ok := args[0].(gethcommon.Address) if !ok { diff --git a/x/evm/precompile/wasm.go b/x/evm/precompile/wasm.go index 8e8c446dc..091999ee3 100644 --- a/x/evm/precompile/wasm.go +++ b/x/evm/precompile/wasm.go @@ -355,9 +355,10 @@ func (p precompileWasm) queryRaw( return bz, err } - // Note: The number of arguments is valiated before this function is called - // during "DecomposeInput". DecomposeInput calls "method.Inputs.Unpack", - // which validates against the the structure of the precompile's ABI. + if e := assertNumArgs(len(args), 2); e != nil { + err = e + return + } argIdx := 0 wasmContract, e := parseContractAddrArg(args[argIdx]) diff --git a/x/evm/precompile/wasm_parse.go b/x/evm/precompile/wasm_parse.go index 2f447c340..80d950622 100644 --- a/x/evm/precompile/wasm_parse.go +++ b/x/evm/precompile/wasm_parse.go @@ -84,9 +84,10 @@ func (p precompileWasm) parseInstantiateArgs(args []any, sender string) ( txMsg wasm.MsgInstantiateContract, err error, ) { - // Note: The number of arguments is valiated before this function is called - // during "DecomposeInput". DecomposeInput calls "method.Inputs.Unpack", - // which validates against the the structure of the precompile's ABI. + if e := assertNumArgs(len(args), 5); e != nil { + err = e + return + } argIdx := 0 admin, ok := args[argIdx].(string) @@ -142,9 +143,10 @@ func (p precompileWasm) parseExecuteArgs(args []any) ( funds sdk.Coins, err error, ) { - // Note: The number of arguments is valiated before this function is called - // during "DecomposeInput". DecomposeInput calls "method.Inputs.Unpack", - // which validates against the the structure of the precompile's ABI. + if e := assertNumArgs(len(args), 3); e != nil { + err = e + return + } argIdx := 0 contractAddrStr, ok := args[argIdx].(string) @@ -187,9 +189,10 @@ func (p precompileWasm) parseQueryArgs(args []any) ( req wasm.RawContractMessage, err error, ) { - // Note: The number of arguments is valiated before this function is called - // during "DecomposeInput". DecomposeInput calls "method.Inputs.Unpack", - // which validates against the the structure of the precompile's ABI. + if e := assertNumArgs(len(args), 2); e != nil { + err = e + return + } argsIdx := 0 wasmContract, e := parseContractAddrArg(args[argsIdx]) @@ -220,9 +223,10 @@ func (p precompileWasm) parseExecuteMultiArgs(args []any) ( }, err error, ) { - // Note: The number of arguments is valiated before this function is called - // during "DecomposeInput". DecomposeInput calls "method.Inputs.Unpack", - // which validates against the the structure of the precompile's ABI. + if e := assertNumArgs(len(args), 1); e != nil { + err = e + return + } arg := args[0] execMsgs, ok := arg.([]struct { From 585ebe70900b70ac9aca3add9580a15d283048d5 Mon Sep 17 00:00:00 2001 From: Kevin Yang <5478483+k-yang@users.noreply.github.com> Date: Thu, 3 Oct 2024 08:40:45 -0400 Subject: [PATCH 16/16] feat(evm): add oracle precompile (#2056) * feat(oracle): add solidity interface * feat(oracle): fix solidity IOracle.sol interface * feat(evm): add oracle precompile * chore: update changelog * Update x/evm/embeds/contracts/IOracle.sol Co-authored-by: Unique Divine <51418232+Unique-Divine@users.noreply.github.com> * Update x/evm/embeds/contracts/IOracle.sol Co-authored-by: Unique Divine <51418232+Unique-Divine@users.noreply.github.com> * Update x/evm/precompile/oracle.go Co-authored-by: Unique Divine <51418232+Unique-Divine@users.noreply.github.com> * Update x/evm/precompile/oracle.go Co-authored-by: Unique Divine <51418232+Unique-Divine@users.noreply.github.com> * Update IOracle.json * chore: linter --- Co-authored-by: Unique Divine <51418232+Unique-Divine@users.noreply.github.com> Co-authored-by: Unique Divine <51418232+Unique-Divine@users.noreply.github.com> Co-authored-by: Oleg Nikonychev --- CHANGELOG.md | 5 +- .../contracts/IOracle.sol/IOracle.json | 30 +++++ x/evm/embeds/contracts/IOracle.sol | 16 +++ x/evm/embeds/embeds.go | 8 +- x/evm/precompile/oracle.go | 127 ++++++++++++++++++ x/evm/precompile/oracle_test.go | 82 +++++++++++ x/evm/precompile/precompile.go | 1 + 7 files changed, 266 insertions(+), 3 deletions(-) create mode 100644 x/evm/embeds/artifacts/contracts/IOracle.sol/IOracle.json create mode 100644 x/evm/embeds/contracts/IOracle.sol create mode 100644 x/evm/precompile/oracle.go create mode 100644 x/evm/precompile/oracle_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 50af1cb18..5f754fc93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -109,7 +109,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#2003](https://github.com/NibiruChain/nibiru/pull/2003) - fix(evm): fix FunToken conversions between Cosmos and EVM - [#2004](https://github.com/NibiruChain/nibiru/pull/2004) - refactor(evm)!: replace `HexAddr` with `EIP55Addr` - [#2006](https://github.com/NibiruChain/nibiru/pull/2006) - test(evm): e2e tests for eth_* endpoints -- [#2008](https://github.com/NibiruChain/nibiru/pull/2008) - refactor(evm): clean up precompile setups +- [#2008](https://github.com/NibiruChain/nibiru/pull/2008) - refactor(evm): clean up precompile setups - [#2013](https://github.com/NibiruChain/nibiru/pull/2013) - chore(evm): Set appropriate gas value for the required gas of the "IFunToken.sol" precompile. - [#2014](https://github.com/NibiruChain/nibiru/pull/2014) - feat(evm): Emit block bloom event in EndBlock hook. - [#2017](https://github.com/NibiruChain/nibiru/pull/2017) - fix(evm): Fix DynamicFeeTx gas cap parameters @@ -119,13 +119,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#2023](https://github.com/NibiruChain/nibiru/pull/2023) - fix(evm)!: adjusted generation and parsing of the block bloom events - [#2030](https://github.com/NibiruChain/nibiru/pull/2030) - refactor(eth/rpc): Delete unused code and improve logging in the eth and debug namespaces - [#2031](https://github.com/NibiruChain/nibiru/pull/2031) - fix(evm): debug calls with custom tracer and tracer options -- [#2032](https://github.com/NibiruChain/nibiru/pull/2032) - feat(evm): ante handler to prohibit authz grant evm messages +- [#2032](https://github.com/NibiruChain/nibiru/pull/2032) - feat(evm): ante handler to prohibit authz grant evm messages - [#2039](https://github.com/NibiruChain/nibiru/pull/2039) - refactor(rpc-backend): remove unnecessary interface code - [#2044](https://github.com/NibiruChain/nibiru/pull/2044) - feat(evm): evm tx indexer service implemented - [#2045](https://github.com/NibiruChain/nibiru/pull/2045) - test(evm): backend tests with test network and real txs - [#2053](https://github.com/NibiruChain/nibiru/pull/2053) - refactor(evm): converted untyped event to typed and cleaned up - [#2054](https://github.com/NibiruChain/nibiru/pull/2054) - feat(evm-precompile): Precompile for one-way EVM calls to invoke/execute Wasm contracts. - [#2060](https://github.com/NibiruChain/nibiru/pull/2060) - fix(evm-precompiles): add assertNumArgs validation +- [#2056](https://github.com/NibiruChain/nibiru/pull/2056) - feat(evm): add oracle precompile #### Dapp modules: perp, spot, oracle, etc diff --git a/x/evm/embeds/artifacts/contracts/IOracle.sol/IOracle.json b/x/evm/embeds/artifacts/contracts/IOracle.sol/IOracle.json new file mode 100644 index 000000000..39638bdae --- /dev/null +++ b/x/evm/embeds/artifacts/contracts/IOracle.sol/IOracle.json @@ -0,0 +1,30 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "IOracle", + "sourceName": "contracts/IOracle.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "string", + "name": "pair", + "type": "string" + } + ], + "name": "queryExchangeRate", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + } + ], + "bytecode": "0x", + "deployedBytecode": "0x", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/x/evm/embeds/contracts/IOracle.sol b/x/evm/embeds/contracts/IOracle.sol new file mode 100644 index 000000000..7cb0a820a --- /dev/null +++ b/x/evm/embeds/contracts/IOracle.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.19; + +/// @notice Oracle interface for querying exchange rates +interface IOracle { + /// @notice Queries the exchange rate for a given pair + /// @param pair The asset pair to query. For example, "ubtc:uusd" is the + /// USD price of BTC and "unibi:uusd" is the USD price of NIBI. + /// @return The exchange rate (a decimal value) as a string. + /// @dev This function is view-only and does not modify state. + function queryExchangeRate(string memory pair) external view returns (string memory); +} + +address constant ORACLE_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000801; + +IOracle constant ORACLE_GATEWAY = IOracle(ORACLE_PRECOMPILE_ADDRESS); diff --git a/x/evm/embeds/embeds.go b/x/evm/embeds/embeds.go index 0103b78c1..b2535040c 100644 --- a/x/evm/embeds/embeds.go +++ b/x/evm/embeds/embeds.go @@ -17,6 +17,8 @@ import ( var ( //go:embed artifacts/contracts/ERC20Minter.sol/ERC20Minter.json erc20MinterContractJSON []byte + //go:embed artifacts/contracts/IOracle.sol/IOracle.json + oracleContractJSON []byte //go:embed artifacts/contracts/FunToken.sol/IFunToken.json funtokenPrecompileJSON []byte //go:embed artifacts/contracts/Wasm.sol/IWasm.json @@ -48,7 +50,10 @@ var ( Name: "Wasm.sol", EmbedJSON: wasmPrecompileJSON, } - + SmartContract_Oracle = CompiledEvmContract{ + Name: "Oracle.sol", + EmbedJSON: oracleContractJSON, + } SmartContract_TestERC20 = CompiledEvmContract{ Name: "TestERC20.sol", EmbedJSON: testErc20Json, @@ -59,6 +64,7 @@ func init() { SmartContract_ERC20Minter.MustLoad() SmartContract_FunToken.MustLoad() SmartContract_Wasm.MustLoad() + SmartContract_Oracle.MustLoad() SmartContract_TestERC20.MustLoad() } diff --git a/x/evm/precompile/oracle.go b/x/evm/precompile/oracle.go new file mode 100644 index 000000000..7c3a57af5 --- /dev/null +++ b/x/evm/precompile/oracle.go @@ -0,0 +1,127 @@ +package precompile + +import ( + "fmt" + "reflect" + + sdk "github.com/cosmos/cosmos-sdk/types" + gethabi "github.com/ethereum/go-ethereum/accounts/abi" + gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + gethparams "github.com/ethereum/go-ethereum/params" + + "github.com/NibiruChain/nibiru/v2/app/keepers" + "github.com/NibiruChain/nibiru/v2/x/common/asset" + "github.com/NibiruChain/nibiru/v2/x/evm/embeds" + "github.com/NibiruChain/nibiru/v2/x/evm/statedb" + oraclekeeper "github.com/NibiruChain/nibiru/v2/x/oracle/keeper" +) + +var _ vm.PrecompiledContract = (*precompileOracle)(nil) + +// Precompile address for "Oracle.sol", the contract that enables queries for exchange rates +var PrecompileAddr_Oracle = gethcommon.HexToAddress("0x0000000000000000000000000000000000000801") + +func (p precompileOracle) Address() gethcommon.Address { + return PrecompileAddr_Oracle +} + +func (p precompileOracle) RequiredGas(input []byte) (gasPrice uint64) { + // Since [gethparams.TxGas] is the cost per (Ethereum) transaction that does not create + // a contract, it's value can be used to derive an appropriate value for the precompile call. + return gethparams.TxGas +} + +const ( + OracleMethod_QueryExchangeRate OracleMethod = "queryExchangeRate" +) + +type OracleMethod string + +// Run runs the precompiled contract +func (p precompileOracle) Run( + evm *vm.EVM, contract *vm.Contract, readonly bool, +) (bz []byte, err error) { + // This is a `defer` pattern to add behavior that runs in the case that the error is + // non-nil, creating a concise way to add extra information. + defer func() { + if err != nil { + precompileType := reflect.TypeOf(p).Name() + err = fmt.Errorf("precompile error: failed to run %s: %w", precompileType, err) + } + }() + + // 1 | Get context from StateDB + stateDB, ok := evm.StateDB.(*statedb.StateDB) + if !ok { + err = fmt.Errorf("failed to load the sdk.Context from the EVM StateDB") + return + } + ctx := stateDB.GetContext() + + method, args, err := DecomposeInput(embeds.SmartContract_Oracle.ABI, contract.Input) + if err != nil { + return nil, err + } + + switch OracleMethod(method.Name) { + case OracleMethod_QueryExchangeRate: + bz, err = p.queryExchangeRate(ctx, method, args, readonly) + default: + err = fmt.Errorf("invalid method called with name \"%s\"", method.Name) + return + } + + return +} + +func PrecompileOracle(keepers keepers.PublicKeepers) vm.PrecompiledContract { + return precompileOracle{ + oracleKeeper: keepers.OracleKeeper, + } +} + +type precompileOracle struct { + oracleKeeper oraclekeeper.Keeper +} + +func (p precompileOracle) queryExchangeRate( + ctx sdk.Context, + method *gethabi.Method, + args []interface{}, + readOnly bool, +) (bz []byte, err error) { + pair, err := p.decomposeQueryExchangeRateArgs(args) + if err != nil { + return nil, err + } + assetPair, err := asset.TryNewPair(pair) + if err != nil { + return nil, err + } + + price, err := p.oracleKeeper.GetExchangeRate(ctx, assetPair) + if err != nil { + return nil, err + } + + return method.Outputs.Pack(price.String()) +} + +func (p precompileOracle) decomposeQueryExchangeRateArgs(args []any) ( + pair string, + err error, +) { + if len(args) != 1 { + err = fmt.Errorf("expected 3 arguments but got %d", len(args)) + return + } + + pair, ok := args[0].(string) + if !ok { + err = ErrArgTypeValidation("string pair", args[0]) + return + } + + return pair, nil +} diff --git a/x/evm/precompile/oracle_test.go b/x/evm/precompile/oracle_test.go new file mode 100644 index 000000000..4d8d0116e --- /dev/null +++ b/x/evm/precompile/oracle_test.go @@ -0,0 +1,82 @@ +package precompile_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/suite" + + "github.com/NibiruChain/nibiru/v2/x/evm/embeds" + "github.com/NibiruChain/nibiru/v2/x/evm/evmtest" + "github.com/NibiruChain/nibiru/v2/x/evm/precompile" +) + +func (s *OracleSuite) TestOracle_FailToPackABI() { + testcases := []struct { + name string + methodName string + callArgs []any + wantError string + }{ + { + name: "wrong amount of call args", + methodName: string(precompile.OracleMethod_QueryExchangeRate), + callArgs: []any{"nonsense", "args here", "to see if", "precompile is", "called"}, + wantError: "argument count mismatch: got 5 for 1", + }, + { + name: "wrong type for pair", + methodName: string(precompile.OracleMethod_QueryExchangeRate), + callArgs: []any{common.HexToAddress("0x7D4B7B8CA7E1a24928Bb96D59249c7a5bd1DfBe6")}, + wantError: "abi: cannot use array as type string as argument", + }, + { + name: "invalid method name", + methodName: "foo", + callArgs: []any{"ubtc:uusdc"}, + wantError: "method 'foo' not found", + }, + } + + abi := embeds.SmartContract_Oracle.ABI + + for _, tc := range testcases { + s.Run(tc.name, func() { + input, err := abi.Pack(tc.methodName, tc.callArgs...) + s.ErrorContains(err, tc.wantError) + s.Nil(input) + }) + } +} + +func (s *OracleSuite) TestOracle_HappyPath() { + deps := evmtest.NewTestDeps() + + s.T().Log("Query exchange rate") + { + deps.App.OracleKeeper.SetPrice(deps.Ctx, "unibi:uusd", sdk.MustNewDecFromStr("0.067")) + input, err := embeds.SmartContract_Oracle.ABI.Pack("queryExchangeRate", "unibi:uusd") + s.NoError(err) + resp, err := deps.EvmKeeper.CallContractWithInput( + deps.Ctx, deps.Sender.EthAddr, &precompile.PrecompileAddr_Oracle, true, input, + ) + s.NoError(err) + + // Check the response + out, err := embeds.SmartContract_Oracle.ABI.Unpack(string(precompile.OracleMethod_QueryExchangeRate), resp.Ret) + s.NoError(err) + + // Check the response + s.Equal("0.067000000000000000", out[0].(string)) + } +} + +type OracleSuite struct { + suite.Suite +} + +// TestPrecompileSuite: Runs all the tests in the suite. +func TestOracleSuite(t *testing.T) { + suite.Run(t, new(OracleSuite)) +} diff --git a/x/evm/precompile/precompile.go b/x/evm/precompile/precompile.go index 6bba3145e..38a8744c1 100644 --- a/x/evm/precompile/precompile.go +++ b/x/evm/precompile/precompile.go @@ -49,6 +49,7 @@ func InitPrecompiles( for _, precompileSetupFn := range []func(k keepers.PublicKeepers) vm.PrecompiledContract{ PrecompileFunToken, PrecompileWasm, + PrecompileOracle, } { pc := precompileSetupFn(k) precompiles[pc.Address()] = pc