From 71c7669028cf6dec4b49ae90ff5862c48844a9ec Mon Sep 17 00:00:00 2001 From: Keemosty12 Date: Tue, 14 Apr 2026 15:17:46 +0800 Subject: [PATCH 1/2] graphql: fix parameterized account fields and block logs --- cmd/rpcdaemon/graphql/graph/generated.go | 56 ++++-- cmd/rpcdaemon/graphql/graph/schema.graphqls | 26 ++- .../graphql/graph/schema.resolvers.go | 186 ++++++++++++++++-- .../graphql/graph/schema_resolvers_test.go | 130 ++++++++++++ 4 files changed, 355 insertions(+), 43 deletions(-) diff --git a/cmd/rpcdaemon/graphql/graph/generated.go b/cmd/rpcdaemon/graphql/graph/generated.go index 446c25807d8..74d6f0320c1 100644 --- a/cmd/rpcdaemon/graphql/graph/generated.go +++ b/cmd/rpcdaemon/graphql/graph/generated.go @@ -30,8 +30,10 @@ type Config = graphql.Config[ResolverRoot, DirectiveRoot, ComplexityRoot] type ResolverRoot interface { Account() AccountResolver Block() BlockResolver + Log() LogResolver Mutation() MutationResolver Query() QueryResolver + Transaction() TransactionResolver } type AccountResolver interface { @@ -40,6 +42,18 @@ type AccountResolver interface { type BlockResolver interface { Account(ctx context.Context, obj *model.Block, address string) (*model.Account, error) + Logs(ctx context.Context, obj *model.Block, filter model.BlockFilterCriteria) ([]*model.Log, error) + Miner(ctx context.Context, obj *model.Block, block *uint64) (*model.Account, error) +} + +type LogResolver interface { + Account(ctx context.Context, obj *model.Log, block *uint64) (*model.Account, error) +} + +type TransactionResolver interface { + CreatedContract(ctx context.Context, obj *model.Transaction, block *uint64) (*model.Account, error) + From(ctx context.Context, obj *model.Transaction, block *uint64) (*model.Account, error) + To(ctx context.Context, obj *model.Transaction, block *uint64) (*model.Account, error) } type DirectiveRoot struct { @@ -1795,7 +1809,8 @@ func (ec *executionContext) _Block_miner(ctx context.Context, field graphql.Coll field, ec.fieldContext_Block_miner, func(ctx context.Context) (any, error) { - return obj.Miner, nil + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Block().Miner(ctx, obj, fc.Args["block"].(*uint64)) }, nil, ec.marshalNAccount2ᚖgithubᚗcomᚋerigontechᚋerigonᚋcmdᚋrpcdaemonᚋgraphqlᚋgraphᚋmodelᚐAccount, @@ -1808,8 +1823,8 @@ func (ec *executionContext) fieldContext_Block_miner(ctx context.Context, field fc = &graphql.FieldContext{ Object: "Block", Field: field, - IsMethod: false, - IsResolver: false, + IsMethod: true, + IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "address": @@ -2540,7 +2555,8 @@ func (ec *executionContext) _Block_logs(ctx context.Context, field graphql.Colle field, ec.fieldContext_Block_logs, func(ctx context.Context) (any, error) { - return obj.Logs, nil + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Block().Logs(ctx, obj, fc.Args["filter"].(model.BlockFilterCriteria)) }, nil, ec.marshalNLog2ᚕᚖgithubᚗcomᚋerigontechᚋerigonᚋcmdᚋrpcdaemonᚋgraphqlᚋgraphᚋmodelᚐLogᚄ, @@ -2553,8 +2569,8 @@ func (ec *executionContext) fieldContext_Block_logs(ctx context.Context, field g fc = &graphql.FieldContext{ Object: "Block", Field: field, - IsMethod: false, - IsResolver: false, + IsMethod: true, + IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "index": @@ -2946,7 +2962,8 @@ func (ec *executionContext) _Log_account(ctx context.Context, field graphql.Coll field, ec.fieldContext_Log_account, func(ctx context.Context) (any, error) { - return obj.Account, nil + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Log().Account(ctx, obj, fc.Args["block"].(*uint64)) }, nil, ec.marshalNAccount2ᚖgithubᚗcomᚋerigontechᚋerigonᚋcmdᚋrpcdaemonᚋgraphqlᚋgraphᚋmodelᚐAccount, @@ -2959,8 +2976,8 @@ func (ec *executionContext) fieldContext_Log_account(ctx context.Context, field fc = &graphql.FieldContext{ Object: "Log", Field: field, - IsMethod: false, - IsResolver: false, + IsMethod: true, + IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "address": @@ -4237,7 +4254,8 @@ func (ec *executionContext) _Transaction_from(ctx context.Context, field graphql field, ec.fieldContext_Transaction_from, func(ctx context.Context) (any, error) { - return obj.From, nil + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Transaction().From(ctx, obj, fc.Args["block"].(*uint64)) }, nil, ec.marshalNAccount2ᚖgithubᚗcomᚋerigontechᚋerigonᚋcmdᚋrpcdaemonᚋgraphqlᚋgraphᚋmodelᚐAccount, @@ -4250,8 +4268,8 @@ func (ec *executionContext) fieldContext_Transaction_from(ctx context.Context, f fc = &graphql.FieldContext{ Object: "Transaction", Field: field, - IsMethod: false, - IsResolver: false, + IsMethod: true, + IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "address": @@ -4289,7 +4307,8 @@ func (ec *executionContext) _Transaction_to(ctx context.Context, field graphql.C field, ec.fieldContext_Transaction_to, func(ctx context.Context) (any, error) { - return obj.To, nil + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Transaction().To(ctx, obj, fc.Args["block"].(*uint64)) }, nil, ec.marshalOAccount2ᚖgithubᚗcomᚋerigontechᚋerigonᚋcmdᚋrpcdaemonᚋgraphqlᚋgraphᚋmodelᚐAccount, @@ -4302,8 +4321,8 @@ func (ec *executionContext) fieldContext_Transaction_to(ctx context.Context, fie fc = &graphql.FieldContext{ Object: "Transaction", Field: field, - IsMethod: false, - IsResolver: false, + IsMethod: true, + IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "address": @@ -4753,7 +4772,8 @@ func (ec *executionContext) _Transaction_createdContract(ctx context.Context, fi field, ec.fieldContext_Transaction_createdContract, func(ctx context.Context) (any, error) { - return obj.CreatedContract, nil + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Transaction().CreatedContract(ctx, obj, fc.Args["block"].(*uint64)) }, nil, ec.marshalOAccount2ᚖgithubᚗcomᚋerigontechᚋerigonᚋcmdᚋrpcdaemonᚋgraphqlᚋgraphᚋmodelᚐAccount, @@ -4766,8 +4786,8 @@ func (ec *executionContext) fieldContext_Transaction_createdContract(ctx context fc = &graphql.FieldContext{ Object: "Transaction", Field: field, - IsMethod: false, - IsResolver: false, + IsMethod: true, + IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "address": diff --git a/cmd/rpcdaemon/graphql/graph/schema.graphqls b/cmd/rpcdaemon/graphql/graph/schema.graphqls index ff0bbb9800c..19b29ed7689 100644 --- a/cmd/rpcdaemon/graphql/graph/schema.graphqls +++ b/cmd/rpcdaemon/graphql/graph/schema.graphqls @@ -14,6 +14,16 @@ scalar Long # Either a string or an unsigned int scalar BlockNum +directive @goField( + forceResolver: Boolean + name: String + omittable: Boolean + type: String + autoBindGetterHaser: Boolean + forceGenerate: Boolean + batch: Boolean +) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION + schema { query: Query mutation: Mutation @@ -34,7 +44,7 @@ type Account { code: Bytes! # Storage provides access to the storage of a contract account, indexed # by its 32 byte slot identifier. - storage(slot: Bytes32!): Bytes32! + storage(slot: Bytes32!): Bytes32! @goField(forceResolver: true) } # Log is an Ethereum event log. @@ -43,7 +53,7 @@ type Log { index: Int! # Account is the account which generated this log - this will always # be a contract account. - account(block: Long): Account! + account(block: Long): Account! @goField(forceResolver: true) # Topics is a list of 0-4 indexed topics for the log. topics: [Bytes32!]! # Data is unindexed data for this log. @@ -69,10 +79,10 @@ type Transaction { index: Int # From is the account that sent this transaction - this will always be # an externally owned account. - from(block: Long): Account! + from(block: Long): Account! @goField(forceResolver: true) # To is the account the transaction was sent to. This is null for # contract-creating transactions. - to(block: Long): Account + to(block: Long): Account @goField(forceResolver: true) # Value is the value, in wei, sent along with this transaction. value: BigInt! # GasPrice is the price offered to miners for gas, in wei per unit. @@ -113,7 +123,7 @@ type Transaction { # CreatedContract is the account that was created by a contract creation # transaction. If the transaction was not a contract creation transaction, # or it has not yet been mined, this field will be null. - createdContract(block: Long): Account + createdContract(block: Long): Account @goField(forceResolver: true) # Logs is a list of log entries emitted by this transaction. If the # transaction has not yet been mined, this field will be null. logs: [Log!] @@ -172,7 +182,7 @@ type Block { # ReceiptsRoot is the keccak256 hash of the trie of transaction receipts in this block. receiptsRoot: Bytes32! # Miner is the account that mined this block. - miner(block: Long): Account! + miner(block: Long): Account! @goField(forceResolver: true) # ExtraData is an arbitrary data field supplied by the miner. extraData: Bytes! # GasLimit is the maximum amount of gas that was available to transactions in this block. @@ -214,9 +224,9 @@ type Block { # bounds, this field will be null. transactionAt(index: Int!): Transaction # Logs returns a filtered set of logs from this block. - logs(filter: BlockFilterCriteria!): [Log!]! + logs(filter: BlockFilterCriteria!): [Log!]! @goField(forceResolver: true) # Account fetches an Ethereum account at the current block's state. - account(address: Address!): Account! + account(address: Address!): Account! @goField(forceResolver: true) # Call executes a local call operation at the current block's state. call(data: CallData!): CallResult # EstimateGas estimates the amount of gas that will be required for diff --git a/cmd/rpcdaemon/graphql/graph/schema.resolvers.go b/cmd/rpcdaemon/graphql/graph/schema.resolvers.go index 74f65f87f60..c4f2ef55de2 100644 --- a/cmd/rpcdaemon/graphql/graph/schema.resolvers.go +++ b/cmd/rpcdaemon/graphql/graph/schema.resolvers.go @@ -19,6 +19,101 @@ import ( "github.com/erigontech/erigon/rpc" ) +func accountBlockNumber(block *uint64, fallback uint64) uint64 { + if block != nil { + return *block + } + return fallback +} + +func normalizeHex(value string) string { + return strings.ToLower(value) +} + +func (r *Resolver) accountAtBlock(ctx context.Context, address string, blockNum uint64) (*model.Account, error) { + if !common.IsHexAddress(address) { + return nil, fmt.Errorf("invalid address %q", address) + } + + addr := common.HexToAddress(address) + balance, nonce, code, err := r.GraphQLAPI.GetAccountInfo(ctx, addr, rpc.BlockNumber(blockNum)) + if err != nil { + return nil, err + } + + return &model.Account{ + Address: normalizeHex(address), + Balance: balance, + TransactionCount: nonce, + Code: code, + BlockNum: blockNum, + }, nil +} + +func (r *Resolver) resolveAccountAtRequestedBlock(ctx context.Context, account *model.Account, block *uint64) (*model.Account, error) { + if account == nil || account.Address == "" { + return nil, nil + } + return r.accountAtBlock(ctx, account.Address, accountBlockNumber(block, account.BlockNum)) +} + +func flattenBlockLogs(transactions []*model.Transaction) []*model.Log { + var logs []*model.Log + for _, tx := range transactions { + if tx == nil { + continue + } + logs = append(logs, tx.Logs...) + } + return logs +} + +func blockLogMatchesFilter(log *model.Log, filter model.BlockFilterCriteria) bool { + if log == nil { + return false + } + + if len(filter.Addresses) > 0 { + if log.Account == nil || log.Account.Address == "" { + return false + } + + match := false + for _, address := range filter.Addresses { + if normalizeHex(address) == normalizeHex(log.Account.Address) { + match = true + break + } + } + if !match { + return false + } + } + + if len(filter.Topics) > len(log.Topics) { + return false + } + + for i, alternatives := range filter.Topics { + if len(alternatives) == 0 { + continue + } + + match := false + for _, topic := range alternatives { + if normalizeHex(topic) == normalizeHex(log.Topics[i]) { + match = true + break + } + } + if !match { + return false + } + } + + return true +} + // SendRawTransaction is the resolver for the sendRawTransaction field. func (r *mutationResolver) SendRawTransaction(ctx context.Context, data string) (string, error) { panic("not implemented: SendRawTransaction - sendRawTransaction") @@ -80,7 +175,7 @@ func (r *queryResolver) Block(ctx context.Context, number *string, hash *string) block.GasLimit = uint64(*convertDataToUint64P(blk, "gasLimit")) block.GasUsed = *convertDataToUint64P(blk, "gasUsed") block.Hash = *convertDataToStringP(blk, "hash") - block.Miner = &model.Account{} + block.Miner = model.NewAccountAtBlock(block.Number) address := convertDataToStringP(blk, "miner") if address != nil { block.Miner.Address = strings.ToLower(*address) @@ -104,6 +199,7 @@ func (r *queryResolver) Block(ctx context.Context, number *string, hash *string) block.TransactionsRoot = *convertDataToStringP(blk, "transactionsRoot") block.BaseFeePerGas = convertDataToStringP(blk, "baseFeePerGas") block.Transactions = []*model.Transaction{} + block.Logs = []*model.Log{} block.LogsBloom = "0x" + *convertDataToStringP(blk, "logsBloom") block.OmmerHash = *convertDataToStringP(blk, "sha3Uncles") @@ -122,6 +218,7 @@ func (r *queryResolver) Block(ctx context.Context, number *string, hash *string) rcp := absRcp.([]map[string]any) for _, transReceipt := range rcp { trans := &model.Transaction{} + trans.Block = block trans.CumulativeGasUsed = convertDataToUint64P(transReceipt, "cumulativeGasUsed") trans.InputData = *convertDataToStringP(transReceipt, "data") trans.EffectiveGasPrice = convertDataToStringP(transReceipt, "effectiveGasPrice") @@ -145,22 +242,30 @@ func (r *queryResolver) Block(ctx context.Context, number *string, hash *string) } tlog.Account = model.NewAccountAtBlock(block.Number) tlog.Account.Address = strings.ToLower(rlog.Address.String()) + tlog.Transaction = trans for _, rtopic := range rlog.Topics { tlog.Topics = append(tlog.Topics, rtopic.String()) } trans.Logs = append(trans.Logs, &tlog) + block.Logs = append(block.Logs, &tlog) } trans.From = model.NewAccountAtBlock(block.Number) trans.From.Address = strings.ToLower(*convertDataToStringP(transReceipt, "from")) - trans.To = model.NewAccountAtBlock(block.Number) address := convertDataToStringP(transReceipt, "to") // To address could be nil in case of contract creation if address != nil { - trans.To.Address = strings.ToLower(*convertDataToStringP(transReceipt, "to")) + trans.To = model.NewAccountAtBlock(block.Number) + trans.To.Address = strings.ToLower(*address) + } + + contractAddress := convertDataToStringP(transReceipt, "contractAddress") + if contractAddress != nil { + trans.CreatedContract = model.NewAccountAtBlock(block.Number) + trans.CreatedContract.Address = strings.ToLower(*contractAddress) } block.Transactions = append(block.Transactions, trans) @@ -256,31 +361,78 @@ func (r *Resolver) Block() BlockResolver { return &blockResolver{r} } // Account returns AccountResolver implementation. func (r *Resolver) Account() AccountResolver { return &accountResolver{r} } +// Log returns LogResolver implementation. +func (r *Resolver) Log() LogResolver { return &logResolver{r} } + +// Transaction returns TransactionResolver implementation. +func (r *Resolver) Transaction() TransactionResolver { return &transactionResolver{r} } + type mutationResolver struct{ *Resolver } type queryResolver struct{ *Resolver } type blockResolver struct{ *Resolver } type accountResolver struct{ *Resolver } +type logResolver struct{ *Resolver } +type transactionResolver struct{ *Resolver } // Account is the resolver for the account field on Block. func (r *blockResolver) Account(ctx context.Context, obj *model.Block, address string) (*model.Account, error) { - if !common.IsHexAddress(address) { - return nil, fmt.Errorf("invalid address %q", address) + return r.accountAtBlock(ctx, address, obj.Number) +} + +// Miner is the resolver for the miner field on Block. +func (r *blockResolver) Miner(ctx context.Context, obj *model.Block, block *uint64) (*model.Account, error) { + account, err := r.resolveAccountAtRequestedBlock(ctx, obj.Miner, block) + if account != nil || err != nil { + return account, err } - addr := common.HexToAddress(address) - blockNumber := rpc.BlockNumber(obj.Number) + return nil, fmt.Errorf("block miner is unavailable") +} - balance, nonce, code, err := r.GraphQLAPI.GetAccountInfo(ctx, addr, blockNumber) - if err != nil { - return nil, err +// Logs is the resolver for the logs field on Block. +func (r *blockResolver) Logs(ctx context.Context, obj *model.Block, filter model.BlockFilterCriteria) ([]*model.Log, error) { + logs := obj.Logs + if logs == nil { + logs = flattenBlockLogs(obj.Transactions) + } + if len(logs) == 0 { + return []*model.Log{}, nil } - return &model.Account{ - Address: strings.ToLower(address), - Balance: balance, - TransactionCount: nonce, - Code: code, - BlockNum: obj.Number, - }, nil + filtered := make([]*model.Log, 0, len(logs)) + for _, log := range logs { + if blockLogMatchesFilter(log, filter) { + filtered = append(filtered, log) + } + } + return filtered, ctx.Err() +} + +// Account is the resolver for the account field on Log. +func (r *logResolver) Account(ctx context.Context, obj *model.Log, block *uint64) (*model.Account, error) { + account, err := r.resolveAccountAtRequestedBlock(ctx, obj.Account, block) + if account != nil || err != nil { + return account, err + } + return nil, fmt.Errorf("log account is unavailable") +} + +// From is the resolver for the from field on Transaction. +func (r *transactionResolver) From(ctx context.Context, obj *model.Transaction, block *uint64) (*model.Account, error) { + account, err := r.resolveAccountAtRequestedBlock(ctx, obj.From, block) + if account != nil || err != nil { + return account, err + } + return nil, fmt.Errorf("transaction sender is unavailable") +} + +// To is the resolver for the to field on Transaction. +func (r *transactionResolver) To(ctx context.Context, obj *model.Transaction, block *uint64) (*model.Account, error) { + return r.resolveAccountAtRequestedBlock(ctx, obj.To, block) +} + +// CreatedContract is the resolver for the createdContract field on Transaction. +func (r *transactionResolver) CreatedContract(ctx context.Context, obj *model.Transaction, block *uint64) (*model.Account, error) { + return r.resolveAccountAtRequestedBlock(ctx, obj.CreatedContract, block) } // Storage is the resolver for the storage field on Account. diff --git a/cmd/rpcdaemon/graphql/graph/schema_resolvers_test.go b/cmd/rpcdaemon/graphql/graph/schema_resolvers_test.go index 63e9531de76..59f74f22cdc 100644 --- a/cmd/rpcdaemon/graphql/graph/schema_resolvers_test.go +++ b/cmd/rpcdaemon/graphql/graph/schema_resolvers_test.go @@ -18,11 +18,37 @@ package graph import ( "context" + "math/big" "testing" "github.com/erigontech/erigon/cmd/rpcdaemon/graphql/graph/model" + "github.com/erigontech/erigon/common" + "github.com/erigontech/erigon/rpc" ) +type mockGraphQLAPI struct { + getAccountInfo func(ctx context.Context, address common.Address, blockNumber rpc.BlockNumber) (string, uint64, string, error) +} + +func (m mockGraphQLAPI) GetBlockDetails(context.Context, rpc.BlockNumber) (map[string]any, error) { + return nil, nil +} + +func (m mockGraphQLAPI) GetChainID(context.Context) (*big.Int, error) { + return big.NewInt(1), nil +} + +func (m mockGraphQLAPI) GetAccountInfo(ctx context.Context, address common.Address, blockNumber rpc.BlockNumber) (string, uint64, string, error) { + if m.getAccountInfo != nil { + return m.getAccountInfo(ctx, address, blockNumber) + } + return "0x0", 0, "0x", nil +} + +func (m mockGraphQLAPI) GetAccountStorage(context.Context, common.Address, string, rpc.BlockNumber) (string, error) { + return "0x0", nil +} + // TestBlockResolver_Account_InvalidAddress verifies that a malformed address // string is rejected before any GraphQL API call is made. func TestBlockResolver_Account_InvalidAddress(t *testing.T) { @@ -58,3 +84,107 @@ func TestNewAccountAtBlock(t *testing.T) { t.Errorf("expected BlockNum=42, got %d", acc.BlockNum) } } + +func TestBlockResolverLogsAppliesFilter(t *testing.T) { + block := &model.Block{ + Logs: []*model.Log{ + { + Index: 0, + Account: &model.Account{Address: "0x1111111111111111111111111111111111111111"}, + Topics: []string{ + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + }, + }, + { + Index: 1, + Account: &model.Account{Address: "0x2222222222222222222222222222222222222222"}, + Topics: []string{ + "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + }, + }, + }, + } + + resolver := &blockResolver{&Resolver{}} + logs, err := resolver.Logs(context.Background(), block, model.BlockFilterCriteria{ + Addresses: []string{"0x1111111111111111111111111111111111111111"}, + Topics: [][]string{ + {"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}, + }, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(logs) != 1 { + t.Fatalf("expected 1 log, got %d", len(logs)) + } + if logs[0].Index != 0 { + t.Fatalf("expected first log to match, got index %d", logs[0].Index) + } +} + +func TestTransactionResolverFromUsesRequestedBlock(t *testing.T) { + const address = "0x1111111111111111111111111111111111111111" + + var gotAddress common.Address + var gotBlock rpc.BlockNumber + + resolver := &transactionResolver{&Resolver{ + GraphQLAPI: mockGraphQLAPI{ + getAccountInfo: func(_ context.Context, addr common.Address, blockNumber rpc.BlockNumber) (string, uint64, string, error) { + gotAddress = addr + gotBlock = blockNumber + return "0x10", 7, "0x6000", nil + }, + }, + }} + + tx := &model.Transaction{From: model.NewAccountAtBlock(12)} + tx.From.Address = address + targetBlock := uint64(99) + + account, err := resolver.From(context.Background(), tx, &targetBlock) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if gotAddress != common.HexToAddress(address) { + t.Fatalf("expected address %s, got %s", address, gotAddress) + } + if gotBlock != rpc.BlockNumber(targetBlock) { + t.Fatalf("expected block %d, got %d", targetBlock, gotBlock) + } + if account.BlockNum != targetBlock { + t.Fatalf("expected result block %d, got %d", targetBlock, account.BlockNum) + } +} + +func TestLogResolverAccountFallsBackToEmbeddedBlock(t *testing.T) { + const address = "0x3333333333333333333333333333333333333333" + + var gotBlock rpc.BlockNumber + + resolver := &logResolver{&Resolver{ + GraphQLAPI: mockGraphQLAPI{ + getAccountInfo: func(_ context.Context, _ common.Address, blockNumber rpc.BlockNumber) (string, uint64, string, error) { + gotBlock = blockNumber + return "0x20", 3, "0x6001", nil + }, + }, + }} + + logEntry := &model.Log{Account: model.NewAccountAtBlock(55)} + logEntry.Account.Address = address + + account, err := resolver.Account(context.Background(), logEntry, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if gotBlock != rpc.BlockNumber(55) { + t.Fatalf("expected embedded block 55, got %d", gotBlock) + } + if account.BlockNum != 55 { + t.Fatalf("expected result block 55, got %d", account.BlockNum) + } +} From 766e3ed23d258b2798a18ddd40961eeab3115f1a Mon Sep 17 00:00:00 2001 From: Keemosty12 Date: Sun, 17 May 2026 20:21:03 +0800 Subject: [PATCH 2/2] graphql: address review feedback in parameterized resolvers --- .../graphql/graph/schema.resolvers.go | 31 ++++---- .../graphql/graph/schema_resolvers_test.go | 79 +++++++++++++++++++ 2 files changed, 94 insertions(+), 16 deletions(-) diff --git a/cmd/rpcdaemon/graphql/graph/schema.resolvers.go b/cmd/rpcdaemon/graphql/graph/schema.resolvers.go index c4f2ef55de2..b345b63e206 100644 --- a/cmd/rpcdaemon/graphql/graph/schema.resolvers.go +++ b/cmd/rpcdaemon/graphql/graph/schema.resolvers.go @@ -9,6 +9,7 @@ import ( "context" "encoding/hex" "fmt" + "math" "strconv" "strings" @@ -19,21 +20,22 @@ import ( "github.com/erigontech/erigon/rpc" ) -func accountBlockNumber(block *uint64, fallback uint64) uint64 { - if block != nil { - return *block - } - return fallback -} - func normalizeHex(value string) string { return strings.ToLower(value) } -func (r *Resolver) accountAtBlock(ctx context.Context, address string, blockNum uint64) (*model.Account, error) { +func (r *Resolver) resolveAccountAtBlock(ctx context.Context, address string, defaultBlock uint64, override *uint64) (*model.Account, error) { + blockNum := defaultBlock + if override != nil { + blockNum = *override + } + if !common.IsHexAddress(address) { return nil, fmt.Errorf("invalid address %q", address) } + if blockNum > uint64(math.MaxInt64) { + return nil, fmt.Errorf("block number %d exceeds max supported value", blockNum) + } addr := common.HexToAddress(address) balance, nonce, code, err := r.GraphQLAPI.GetAccountInfo(ctx, addr, rpc.BlockNumber(blockNum)) @@ -54,7 +56,7 @@ func (r *Resolver) resolveAccountAtRequestedBlock(ctx context.Context, account * if account == nil || account.Address == "" { return nil, nil } - return r.accountAtBlock(ctx, account.Address, accountBlockNumber(block, account.BlockNum)) + return r.resolveAccountAtBlock(ctx, account.Address, account.BlockNum, block) } func flattenBlockLogs(transactions []*model.Transaction) []*model.Log { @@ -90,14 +92,13 @@ func blockLogMatchesFilter(log *model.Log, filter model.BlockFilterCriteria) boo } } - if len(filter.Topics) > len(log.Topics) { - return false - } - for i, alternatives := range filter.Topics { if len(alternatives) == 0 { continue } + if i >= len(log.Topics) { + return false + } match := false for _, topic := range alternatives { @@ -199,8 +200,6 @@ func (r *queryResolver) Block(ctx context.Context, number *string, hash *string) block.TransactionsRoot = *convertDataToStringP(blk, "transactionsRoot") block.BaseFeePerGas = convertDataToStringP(blk, "baseFeePerGas") block.Transactions = []*model.Transaction{} - block.Logs = []*model.Log{} - block.LogsBloom = "0x" + *convertDataToStringP(blk, "logsBloom") block.OmmerHash = *convertDataToStringP(blk, "sha3Uncles") @@ -376,7 +375,7 @@ type transactionResolver struct{ *Resolver } // Account is the resolver for the account field on Block. func (r *blockResolver) Account(ctx context.Context, obj *model.Block, address string) (*model.Account, error) { - return r.accountAtBlock(ctx, address, obj.Number) + return r.resolveAccountAtBlock(ctx, address, obj.Number, nil) } // Miner is the resolver for the miner field on Block. diff --git a/cmd/rpcdaemon/graphql/graph/schema_resolvers_test.go b/cmd/rpcdaemon/graphql/graph/schema_resolvers_test.go index 59f74f22cdc..2a286faea0a 100644 --- a/cmd/rpcdaemon/graphql/graph/schema_resolvers_test.go +++ b/cmd/rpcdaemon/graphql/graph/schema_resolvers_test.go @@ -18,6 +18,7 @@ package graph import ( "context" + "math" "math/big" "testing" @@ -125,6 +126,58 @@ func TestBlockResolverLogsAppliesFilter(t *testing.T) { } } +func TestBlockResolverLogsTopicWildcardDoesNotRequireTopicPosition(t *testing.T) { + block := &model.Block{ + Logs: []*model.Log{ + { + Index: 0, + Topics: []string{ + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + }, + }, + }, + } + + resolver := &blockResolver{&Resolver{}} + logs, err := resolver.Logs(context.Background(), block, model.BlockFilterCriteria{ + Topics: [][]string{ + {"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {}, + }, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(logs) != 1 { + t.Fatalf("expected 1 log, got %d", len(logs)) + } +} + +func TestBlockResolverLogsFallsBackToFlattenTransactionsWhenLogsNil(t *testing.T) { + block := &model.Block{ + Logs: nil, + Transactions: []*model.Transaction{ + { + Logs: []*model.Log{ + {Index: 7}, + }, + }, + }, + } + + resolver := &blockResolver{&Resolver{}} + logs, err := resolver.Logs(context.Background(), block, model.BlockFilterCriteria{}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(logs) != 1 { + t.Fatalf("expected 1 flattened log, got %d", len(logs)) + } + if logs[0].Index != 7 { + t.Fatalf("expected flattened log index 7, got %d", logs[0].Index) + } +} + func TestTransactionResolverFromUsesRequestedBlock(t *testing.T) { const address = "0x1111111111111111111111111111111111111111" @@ -188,3 +241,29 @@ func TestLogResolverAccountFallsBackToEmbeddedBlock(t *testing.T) { t.Fatalf("expected result block 55, got %d", account.BlockNum) } } + +func TestTransactionResolverFromRejectsOverflowRequestedBlock(t *testing.T) { + const address = "0x1111111111111111111111111111111111111111" + + called := false + resolver := &transactionResolver{&Resolver{ + GraphQLAPI: mockGraphQLAPI{ + getAccountInfo: func(_ context.Context, _ common.Address, _ rpc.BlockNumber) (string, uint64, string, error) { + called = true + return "0x10", 7, "0x6000", nil + }, + }, + }} + + tx := &model.Transaction{From: model.NewAccountAtBlock(12)} + tx.From.Address = address + overflow := uint64(math.MaxInt64) + 1 + + _, err := resolver.From(context.Background(), tx, &overflow) + if err == nil { + t.Fatal("expected overflow error, got nil") + } + if called { + t.Fatal("expected resolver to reject overflow before GetAccountInfo call") + } +}