diff --git a/cmd/rpcdaemon/graphql/gqlgen.yml b/cmd/rpcdaemon/graphql/gqlgen.yml
index b5d15a9afee..ae89d8f1d0a 100644
--- a/cmd/rpcdaemon/graphql/gqlgen.yml
+++ b/cmd/rpcdaemon/graphql/gqlgen.yml
@@ -60,7 +60,7 @@ models:
- github.com/99designs/gqlgen/graphql.Int32
Long:
model:
- - github.com/99designs/gqlgen/graphql.Uint64
+ - github.com/erigontech/erigon/cmd/rpcdaemon/graphql/graph/scalar.Uint64
BigInt:
model:
- github.com/99designs/gqlgen/graphql.String
@@ -77,13 +77,17 @@ models:
model:
- github.com/99designs/gqlgen/graphql.String
- github.com/99designs/gqlgen/graphql.Uint64
-# Block:
-# fields:
-# logs:
-# resolver: true # force a resolver to be generated
-# ommers:
-# resolver: true # force a resolver to be generated
-# transactions:
-# resolver: true # force a resolver to be generated
+ Block:
+ fields:
+ transactionAt:
+ resolver: true
+ account:
+ resolver: true
+ Transaction:
+ fields:
+ from:
+ resolver: true
+ to:
+ resolver: true
omit_getters: true
diff --git a/cmd/rpcdaemon/graphql/graph/generated.go b/cmd/rpcdaemon/graphql/graph/generated.go
index 446c25807d8..d9ff004480a 100644
--- a/cmd/rpcdaemon/graphql/graph/generated.go
+++ b/cmd/rpcdaemon/graphql/graph/generated.go
@@ -14,6 +14,7 @@ import (
"github.com/99designs/gqlgen/graphql"
"github.com/99designs/gqlgen/graphql/introspection"
"github.com/erigontech/erigon/cmd/rpcdaemon/graphql/graph/model"
+ "github.com/erigontech/erigon/cmd/rpcdaemon/graphql/graph/scalar"
gqlparser "github.com/vektah/gqlparser/v2"
"github.com/vektah/gqlparser/v2/ast"
)
@@ -28,18 +29,10 @@ func NewExecutableSchema(cfg Config) graphql.ExecutableSchema {
type Config = graphql.Config[ResolverRoot, DirectiveRoot, ComplexityRoot]
type ResolverRoot interface {
- Account() AccountResolver
Block() BlockResolver
Mutation() MutationResolver
Query() QueryResolver
-}
-
-type AccountResolver interface {
- Storage(ctx context.Context, obj *model.Account, slot string) (string, error)
-}
-
-type BlockResolver interface {
- Account(ctx context.Context, obj *model.Block, address string) (*model.Account, error)
+ Transaction() TransactionResolver
}
type DirectiveRoot struct {
@@ -174,6 +167,11 @@ type ComplexityRoot struct {
}
}
+type BlockResolver interface {
+ TransactionAt(ctx context.Context, obj *model.Block, index int) (*model.Transaction, error)
+
+ Account(ctx context.Context, obj *model.Block, address string) (*model.Account, error)
+}
type MutationResolver interface {
SendRawTransaction(ctx context.Context, data string) (string, error)
}
@@ -188,6 +186,10 @@ type QueryResolver interface {
Syncing(ctx context.Context) (*model.SyncState, error)
ChainID(ctx context.Context) (string, error)
}
+type TransactionResolver interface {
+ 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 executableSchema graphql.ExecutableSchemaState[ResolverRoot, DirectiveRoot, ComplexityRoot]
@@ -1458,8 +1460,7 @@ func (ec *executionContext) _Account_storage(ctx context.Context, field graphql.
field,
ec.fieldContext_Account_storage,
func(ctx context.Context) (any, error) {
- fc := graphql.GetFieldContext(ctx)
- return ec.Resolvers.Account().Storage(ctx, obj, fc.Args["slot"].(string))
+ return obj.Storage, nil
},
nil,
ec.marshalNBytes322string,
@@ -1472,8 +1473,8 @@ func (ec *executionContext) fieldContext_Account_storage(ctx context.Context, fi
fc = &graphql.FieldContext{
Object: "Account",
Field: field,
- IsMethod: true,
- IsResolver: true,
+ IsMethod: false,
+ IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type Bytes32 does not have child fields")
},
@@ -2446,7 +2447,8 @@ func (ec *executionContext) _Block_transactionAt(ctx context.Context, field grap
field,
ec.fieldContext_Block_transactionAt,
func(ctx context.Context) (any, error) {
- return obj.TransactionAt, nil
+ fc := graphql.GetFieldContext(ctx)
+ return ec.Resolvers.Block().TransactionAt(ctx, obj, fc.Args["index"].(int))
},
nil,
ec.marshalOTransaction2ᚖgithubᚗcomᚋerigontechᚋerigonᚋcmdᚋrpcdaemonᚋgraphqlᚋgraphᚋmodelᚐTransaction,
@@ -2459,8 +2461,8 @@ func (ec *executionContext) fieldContext_Block_transactionAt(ctx context.Context
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 "hash":
@@ -4237,7 +4239,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 +4253,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 +4292,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 +4306,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":
@@ -4936,7 +4940,7 @@ func (ec *executionContext) _Transaction_type(ctx context.Context, field graphql
return obj.Type, nil
},
nil,
- ec.marshalOInt2ᚖint,
+ ec.marshalOLong2ᚖuint64,
true,
false,
)
@@ -4949,7 +4953,7 @@ func (ec *executionContext) fieldContext_Transaction_type(_ context.Context, fie
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
- return nil, errors.New("field of type Int does not have child fields")
+ return nil, errors.New("field of type Long does not have child fields")
},
}
return fc, nil
@@ -6902,56 +6906,56 @@ func (ec *executionContext) _Block(ctx context.Context, sel ast.SelectionSet, ob
case "number":
out.Values[i] = ec._Block_number(ctx, field, obj)
if out.Values[i] == graphql.Null {
- out.Invalids++
+ atomic.AddUint32(&out.Invalids, 1)
}
case "hash":
out.Values[i] = ec._Block_hash(ctx, field, obj)
if out.Values[i] == graphql.Null {
- out.Invalids++
+ atomic.AddUint32(&out.Invalids, 1)
}
case "parent":
out.Values[i] = ec._Block_parent(ctx, field, obj)
case "nonce":
out.Values[i] = ec._Block_nonce(ctx, field, obj)
if out.Values[i] == graphql.Null {
- out.Invalids++
+ atomic.AddUint32(&out.Invalids, 1)
}
case "transactionsRoot":
out.Values[i] = ec._Block_transactionsRoot(ctx, field, obj)
if out.Values[i] == graphql.Null {
- out.Invalids++
+ atomic.AddUint32(&out.Invalids, 1)
}
case "transactionCount":
out.Values[i] = ec._Block_transactionCount(ctx, field, obj)
case "stateRoot":
out.Values[i] = ec._Block_stateRoot(ctx, field, obj)
if out.Values[i] == graphql.Null {
- out.Invalids++
+ atomic.AddUint32(&out.Invalids, 1)
}
case "receiptsRoot":
out.Values[i] = ec._Block_receiptsRoot(ctx, field, obj)
if out.Values[i] == graphql.Null {
- out.Invalids++
+ atomic.AddUint32(&out.Invalids, 1)
}
case "miner":
out.Values[i] = ec._Block_miner(ctx, field, obj)
if out.Values[i] == graphql.Null {
- out.Invalids++
+ atomic.AddUint32(&out.Invalids, 1)
}
case "extraData":
out.Values[i] = ec._Block_extraData(ctx, field, obj)
if out.Values[i] == graphql.Null {
- out.Invalids++
+ atomic.AddUint32(&out.Invalids, 1)
}
case "gasLimit":
out.Values[i] = ec._Block_gasLimit(ctx, field, obj)
if out.Values[i] == graphql.Null {
- out.Invalids++
+ atomic.AddUint32(&out.Invalids, 1)
}
case "gasUsed":
out.Values[i] = ec._Block_gasUsed(ctx, field, obj)
if out.Values[i] == graphql.Null {
- out.Invalids++
+ atomic.AddUint32(&out.Invalids, 1)
}
case "baseFeePerGas":
out.Values[i] = ec._Block_baseFeePerGas(ctx, field, obj)
@@ -6960,22 +6964,22 @@ func (ec *executionContext) _Block(ctx context.Context, sel ast.SelectionSet, ob
case "timestamp":
out.Values[i] = ec._Block_timestamp(ctx, field, obj)
if out.Values[i] == graphql.Null {
- out.Invalids++
+ atomic.AddUint32(&out.Invalids, 1)
}
case "logsBloom":
out.Values[i] = ec._Block_logsBloom(ctx, field, obj)
if out.Values[i] == graphql.Null {
- out.Invalids++
+ atomic.AddUint32(&out.Invalids, 1)
}
case "mixHash":
out.Values[i] = ec._Block_mixHash(ctx, field, obj)
if out.Values[i] == graphql.Null {
- out.Invalids++
+ atomic.AddUint32(&out.Invalids, 1)
}
case "difficulty":
out.Values[i] = ec._Block_difficulty(ctx, field, obj)
if out.Values[i] == graphql.Null {
- out.Invalids++
+ atomic.AddUint32(&out.Invalids, 1)
}
case "ommerCount":
out.Values[i] = ec._Block_ommerCount(ctx, field, obj)
@@ -6986,38 +6990,100 @@ func (ec *executionContext) _Block(ctx context.Context, sel ast.SelectionSet, ob
case "ommerHash":
out.Values[i] = ec._Block_ommerHash(ctx, field, obj)
if out.Values[i] == graphql.Null {
- out.Invalids++
+ atomic.AddUint32(&out.Invalids, 1)
}
case "transactions":
out.Values[i] = ec._Block_transactions(ctx, field, obj)
case "transactionAt":
- out.Values[i] = ec._Block_transactionAt(ctx, field, obj)
+ field := field
+
+ innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) {
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ }
+ }()
+ res = ec._Block_transactionAt(ctx, field, obj)
+ return res
+ }
+
+ if field.Deferrable != nil {
+ dfs, ok := deferred[field.Deferrable.Label]
+ di := 0
+ if ok {
+ dfs.AddField(field)
+ di = len(dfs.Values) - 1
+ } else {
+ dfs = graphql.NewFieldSet([]graphql.CollectedField{field})
+ deferred[field.Deferrable.Label] = dfs
+ }
+ dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler {
+ return innerFunc(ctx, dfs)
+ })
+
+ // don't run the out.Concurrently() call below
+ out.Values[i] = graphql.Null
+ continue
+ }
+
+ out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
case "logs":
out.Values[i] = ec._Block_logs(ctx, field, obj)
if out.Values[i] == graphql.Null {
- out.Invalids++
+ atomic.AddUint32(&out.Invalids, 1)
}
case "account":
- out.Values[i] = ec._Block_account(ctx, field, obj)
- if out.Values[i] == graphql.Null {
- out.Invalids++
+ field := field
+
+ innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ }
+ }()
+ res = ec._Block_account(ctx, field, obj)
+ if res == graphql.Null {
+ atomic.AddUint32(&fs.Invalids, 1)
+ }
+ return res
}
+
+ if field.Deferrable != nil {
+ dfs, ok := deferred[field.Deferrable.Label]
+ di := 0
+ if ok {
+ dfs.AddField(field)
+ di = len(dfs.Values) - 1
+ } else {
+ dfs = graphql.NewFieldSet([]graphql.CollectedField{field})
+ deferred[field.Deferrable.Label] = dfs
+ }
+ dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler {
+ return innerFunc(ctx, dfs)
+ })
+
+ // don't run the out.Concurrently() call below
+ out.Values[i] = graphql.Null
+ continue
+ }
+
+ out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
case "call":
out.Values[i] = ec._Block_call(ctx, field, obj)
case "estimateGas":
out.Values[i] = ec._Block_estimateGas(ctx, field, obj)
if out.Values[i] == graphql.Null {
- out.Invalids++
+ atomic.AddUint32(&out.Invalids, 1)
}
case "rawHeader":
out.Values[i] = ec._Block_rawHeader(ctx, field, obj)
if out.Values[i] == graphql.Null {
- out.Invalids++
+ atomic.AddUint32(&out.Invalids, 1)
}
case "raw":
out.Values[i] = ec._Block_raw(ctx, field, obj)
if out.Values[i] == graphql.Null {
- out.Invalids++
+ atomic.AddUint32(&out.Invalids, 1)
}
case "withdrawals":
out.Values[i] = ec._Block_withdrawals(ctx, field, obj)
@@ -7556,31 +7622,93 @@ func (ec *executionContext) _Transaction(ctx context.Context, sel ast.SelectionS
case "hash":
out.Values[i] = ec._Transaction_hash(ctx, field, obj)
if out.Values[i] == graphql.Null {
- out.Invalids++
+ atomic.AddUint32(&out.Invalids, 1)
}
case "nonce":
out.Values[i] = ec._Transaction_nonce(ctx, field, obj)
if out.Values[i] == graphql.Null {
- out.Invalids++
+ atomic.AddUint32(&out.Invalids, 1)
}
case "index":
out.Values[i] = ec._Transaction_index(ctx, field, obj)
case "from":
- out.Values[i] = ec._Transaction_from(ctx, field, obj)
- if out.Values[i] == graphql.Null {
- out.Invalids++
+ field := field
+
+ innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ }
+ }()
+ res = ec._Transaction_from(ctx, field, obj)
+ if res == graphql.Null {
+ atomic.AddUint32(&fs.Invalids, 1)
+ }
+ return res
}
+
+ if field.Deferrable != nil {
+ dfs, ok := deferred[field.Deferrable.Label]
+ di := 0
+ if ok {
+ dfs.AddField(field)
+ di = len(dfs.Values) - 1
+ } else {
+ dfs = graphql.NewFieldSet([]graphql.CollectedField{field})
+ deferred[field.Deferrable.Label] = dfs
+ }
+ dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler {
+ return innerFunc(ctx, dfs)
+ })
+
+ // don't run the out.Concurrently() call below
+ out.Values[i] = graphql.Null
+ continue
+ }
+
+ out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
case "to":
- out.Values[i] = ec._Transaction_to(ctx, field, obj)
+ field := field
+
+ innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) {
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ }
+ }()
+ res = ec._Transaction_to(ctx, field, obj)
+ return res
+ }
+
+ if field.Deferrable != nil {
+ dfs, ok := deferred[field.Deferrable.Label]
+ di := 0
+ if ok {
+ dfs.AddField(field)
+ di = len(dfs.Values) - 1
+ } else {
+ dfs = graphql.NewFieldSet([]graphql.CollectedField{field})
+ deferred[field.Deferrable.Label] = dfs
+ }
+ dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler {
+ return innerFunc(ctx, dfs)
+ })
+
+ // don't run the out.Concurrently() call below
+ out.Values[i] = graphql.Null
+ continue
+ }
+
+ out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
case "value":
out.Values[i] = ec._Transaction_value(ctx, field, obj)
if out.Values[i] == graphql.Null {
- out.Invalids++
+ atomic.AddUint32(&out.Invalids, 1)
}
case "gasPrice":
out.Values[i] = ec._Transaction_gasPrice(ctx, field, obj)
if out.Values[i] == graphql.Null {
- out.Invalids++
+ atomic.AddUint32(&out.Invalids, 1)
}
case "maxFeePerGas":
out.Values[i] = ec._Transaction_maxFeePerGas(ctx, field, obj)
@@ -7591,12 +7719,12 @@ func (ec *executionContext) _Transaction(ctx context.Context, sel ast.SelectionS
case "gas":
out.Values[i] = ec._Transaction_gas(ctx, field, obj)
if out.Values[i] == graphql.Null {
- out.Invalids++
+ atomic.AddUint32(&out.Invalids, 1)
}
case "inputData":
out.Values[i] = ec._Transaction_inputData(ctx, field, obj)
if out.Values[i] == graphql.Null {
- out.Invalids++
+ atomic.AddUint32(&out.Invalids, 1)
}
case "block":
out.Values[i] = ec._Transaction_block(ctx, field, obj)
@@ -7615,17 +7743,17 @@ func (ec *executionContext) _Transaction(ctx context.Context, sel ast.SelectionS
case "r":
out.Values[i] = ec._Transaction_r(ctx, field, obj)
if out.Values[i] == graphql.Null {
- out.Invalids++
+ atomic.AddUint32(&out.Invalids, 1)
}
case "s":
out.Values[i] = ec._Transaction_s(ctx, field, obj)
if out.Values[i] == graphql.Null {
- out.Invalids++
+ atomic.AddUint32(&out.Invalids, 1)
}
case "v":
out.Values[i] = ec._Transaction_v(ctx, field, obj)
if out.Values[i] == graphql.Null {
- out.Invalids++
+ atomic.AddUint32(&out.Invalids, 1)
}
case "type":
out.Values[i] = ec._Transaction_type(ctx, field, obj)
@@ -7634,12 +7762,12 @@ func (ec *executionContext) _Transaction(ctx context.Context, sel ast.SelectionS
case "raw":
out.Values[i] = ec._Transaction_raw(ctx, field, obj)
if out.Values[i] == graphql.Null {
- out.Invalids++
+ atomic.AddUint32(&out.Invalids, 1)
}
case "rawReceipt":
out.Values[i] = ec._Transaction_rawReceipt(ctx, field, obj)
if out.Values[i] == graphql.Null {
- out.Invalids++
+ atomic.AddUint32(&out.Invalids, 1)
}
default:
panic("unknown field " + strconv.Quote(field.Name))
@@ -8063,6 +8191,10 @@ func (ec *executionContext) marshalNAccessTuple2ᚖgithubᚗcomᚋerigontechᚋe
return ec._AccessTuple(ctx, sel, v)
}
+func (ec *executionContext) marshalNAccount2githubᚗcomᚋerigontechᚋerigonᚋcmdᚋrpcdaemonᚋgraphqlᚋgraphᚋmodelᚐAccount(ctx context.Context, sel ast.SelectionSet, v model.Account) graphql.Marshaler {
+ return ec._Account(ctx, sel, &v)
+}
+
func (ec *executionContext) marshalNAccount2ᚖgithubᚗcomᚋerigontechᚋerigonᚋcmdᚋrpcdaemonᚋgraphqlᚋgraphᚋmodelᚐAccount(ctx context.Context, sel ast.SelectionSet, v *model.Account) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
@@ -8267,13 +8399,13 @@ func (ec *executionContext) marshalNLog2ᚖgithubᚗcomᚋerigontechᚋerigonᚋ
}
func (ec *executionContext) unmarshalNLong2uint64(ctx context.Context, v any) (uint64, error) {
- res, err := graphql.UnmarshalUint64(v)
+ res, err := scalar.UnmarshalUint64(v)
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalNLong2uint64(ctx context.Context, sel ast.SelectionSet, v uint64) graphql.Marshaler {
_ = sel
- res := graphql.MarshalUint64(v)
+ res := scalar.MarshalUint64(v)
if res == graphql.Null {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow")
@@ -8759,7 +8891,7 @@ func (ec *executionContext) unmarshalOLong2ᚖuint64(ctx context.Context, v any)
if v == nil {
return nil, nil
}
- res, err := graphql.UnmarshalUint64(v)
+ res, err := scalar.UnmarshalUint64(v)
return &res, graphql.ErrorOnPath(ctx, err)
}
@@ -8769,7 +8901,7 @@ func (ec *executionContext) marshalOLong2ᚖuint64(ctx context.Context, sel ast.
}
_ = sel
_ = ctx
- res := graphql.MarshalUint64(*v)
+ res := scalar.MarshalUint64(*v)
return res
}
diff --git a/cmd/rpcdaemon/graphql/graph/helpers.go b/cmd/rpcdaemon/graphql/graph/helpers.go
index 12f3dc8fc90..2991de3fd8f 100644
--- a/cmd/rpcdaemon/graphql/graph/helpers.go
+++ b/cmd/rpcdaemon/graphql/graph/helpers.go
@@ -15,6 +15,9 @@ import (
)
func convertDataToStringP(abstractMap map[string]any, field string) *string {
+ if abstractMap[field] == nil {
+ return nil
+ }
var result string
switch v := abstractMap[field].(type) {
@@ -64,6 +67,9 @@ func convertDataToStringP(abstractMap map[string]any, field string) *string {
}
func convertDataToIntP(abstractMap map[string]any, field string) *int {
+ if abstractMap[field] == nil {
+ return nil
+ }
var result int
switch v := abstractMap[field].(type) {
@@ -92,6 +98,9 @@ func convertDataToIntP(abstractMap map[string]any, field string) *int {
}
func convertDataToUint64P(abstractMap map[string]any, field string) *uint64 {
+ if abstractMap[field] == nil {
+ return nil
+ }
var result uint64
switch v := abstractMap[field].(type) {
diff --git a/cmd/rpcdaemon/graphql/graph/helpers_test.go b/cmd/rpcdaemon/graphql/graph/helpers_test.go
new file mode 100644
index 00000000000..8eb49f0842b
--- /dev/null
+++ b/cmd/rpcdaemon/graphql/graph/helpers_test.go
@@ -0,0 +1,65 @@
+// Copyright 2024 The Erigon Authors
+// This file is part of Erigon.
+//
+// Erigon is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Erigon is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with Erigon. If not, see .
+
+package graph
+
+import (
+ "testing"
+
+ "github.com/erigontech/erigon/common/hexutil"
+)
+
+func TestConvertDataToStringP_Nil(t *testing.T) {
+ m := map[string]any{"null": nil, "value": hexutil.Uint64(42)}
+
+ if got := convertDataToStringP(m, "null"); got != nil {
+ t.Errorf("nil value: expected nil, got %q", *got)
+ }
+ if got := convertDataToStringP(m, "missing"); got != nil {
+ t.Errorf("missing key: expected nil, got %q", *got)
+ }
+ if got := convertDataToStringP(m, "value"); got == nil {
+ t.Error("present value: expected non-nil, got nil")
+ }
+}
+
+func TestConvertDataToIntP_Nil(t *testing.T) {
+ m := map[string]any{"null": nil, "value": hexutil.Uint64(5)}
+
+ if got := convertDataToIntP(m, "null"); got != nil {
+ t.Errorf("nil value: expected nil, got %d", *got)
+ }
+ if got := convertDataToIntP(m, "missing"); got != nil {
+ t.Errorf("missing key: expected nil, got %d", *got)
+ }
+ if got := convertDataToIntP(m, "value"); got == nil {
+ t.Error("present value: expected non-nil, got nil")
+ }
+}
+
+func TestConvertDataToUint64P_Nil(t *testing.T) {
+ m := map[string]any{"null": nil, "value": hexutil.Uint64(7)}
+
+ if got := convertDataToUint64P(m, "null"); got != nil {
+ t.Errorf("nil value: expected nil, got %d", *got)
+ }
+ if got := convertDataToUint64P(m, "missing"); got != nil {
+ t.Errorf("missing key: expected nil, got %d", *got)
+ }
+ if got := convertDataToUint64P(m, "value"); got == nil {
+ t.Error("present value: expected non-nil, got nil")
+ }
+}
diff --git a/cmd/rpcdaemon/graphql/graph/model/models_gen.go b/cmd/rpcdaemon/graphql/graph/model/models_gen.go
index e691cd4b462..c675c8b42ce 100644
--- a/cmd/rpcdaemon/graphql/graph/model/models_gen.go
+++ b/cmd/rpcdaemon/graphql/graph/model/models_gen.go
@@ -121,7 +121,7 @@ type Transaction struct {
R string `json:"r"`
S string `json:"s"`
V string `json:"v"`
- Type *int `json:"type,omitempty"`
+ Type *uint64 `json:"type,omitempty"`
AccessList []*AccessTuple `json:"accessList,omitempty"`
Raw string `json:"raw"`
RawReceipt string `json:"rawReceipt"`
diff --git a/cmd/rpcdaemon/graphql/graph/resolver_helpers.go b/cmd/rpcdaemon/graphql/graph/resolver_helpers.go
new file mode 100644
index 00000000000..badca836dc1
--- /dev/null
+++ b/cmd/rpcdaemon/graphql/graph/resolver_helpers.go
@@ -0,0 +1,165 @@
+package graph
+
+import (
+ "context"
+ "strings"
+
+ "github.com/erigontech/erigon/cmd/rpcdaemon/graphql/graph/model"
+ "github.com/erigontech/erigon/common"
+ "github.com/erigontech/erigon/common/hexutil"
+ "github.com/erigontech/erigon/execution/types"
+ "github.com/erigontech/erigon/rpc"
+)
+
+func (r *Resolver) resolveAccountAtBlock(ctx context.Context, address string, defaultBlock uint64, override *uint64) (*model.Account, error) {
+ blockNum := rpc.BlockNumber(defaultBlock)
+ if override != nil {
+ blockNum = rpc.BlockNumber(*override)
+ }
+ addr := common.HexToAddress(address)
+ balance, nonce, code, err := r.GraphQLAPI.GetAccountInfo(ctx, addr, blockNum)
+ if err != nil {
+ return nil, err
+ }
+ return &model.Account{
+ Address: strings.ToLower(address),
+ Balance: balance,
+ TransactionCount: nonce,
+ Code: code,
+ BlockNum: uint64(blockNum),
+ }, nil
+}
+
+func (r *queryResolver) buildBlock(res map[string]any) (*model.Block, error) {
+ block := &model.Block{}
+ absBlk := res["block"]
+ if absBlk == nil {
+ return block, nil
+ }
+
+ blk := absBlk.(map[string]any)
+
+ block.Difficulty = *convertDataToStringP(blk, "difficulty")
+ block.ExtraData = *convertDataToStringP(blk, "extraData")
+ block.GasLimit = uint64(*convertDataToUint64P(blk, "gasLimit"))
+ block.GasUsed = *convertDataToUint64P(blk, "gasUsed")
+ block.Hash = *convertDataToStringP(blk, "hash")
+ block.Miner = &model.Account{}
+ if address := convertDataToStringP(blk, "miner"); address != nil {
+ block.Miner.Address = strings.ToLower(*address)
+ }
+ if mixHash := convertDataToStringP(blk, "mixHash"); mixHash != nil {
+ block.MixHash = *mixHash
+ }
+ if blockNonce := convertDataToStringP(blk, "nonce"); blockNonce != nil {
+ block.Nonce = *blockNonce
+ }
+ block.Number = *convertDataToUint64P(blk, "number")
+ block.Miner.BlockNum = block.Number
+ block.Parent = &model.Block{}
+ block.Parent.Hash = *convertDataToStringP(blk, "parentHash")
+ block.ReceiptsRoot = *convertDataToStringP(blk, "receiptsRoot")
+ block.StateRoot = *convertDataToStringP(blk, "stateRoot")
+ block.Timestamp = *convertDataToStringP(blk, "timestamp")
+ block.TransactionCount = convertDataToIntP(blk, "transactionCount")
+ block.TransactionsRoot = *convertDataToStringP(blk, "transactionsRoot")
+ block.BaseFeePerGas = convertDataToStringP(blk, "baseFeePerGas")
+ block.LogsBloom = "0x" + *convertDataToStringP(blk, "logsBloom")
+ block.OmmerHash = *convertDataToStringP(blk, "sha3Uncles")
+
+ uncles := blk["uncles"].([]common.Hash)
+ block.Ommers = make([]*model.Block, 0, len(uncles))
+ for _, ommerHash := range uncles {
+ block.Ommers = append(block.Ommers, &model.Block{Hash: ommerHash.String()})
+ }
+ ommerCount := len(block.Ommers)
+ block.OmmerCount = &ommerCount
+
+ rcp := res["receipts"].([]map[string]any)
+ block.Transactions = make([]*model.Transaction, 0, len(rcp))
+ for _, transReceipt := range rcp {
+ trans := r.buildTransaction(block, transReceipt)
+ block.Transactions = append(block.Transactions, trans)
+ }
+
+ withdrawals := res["withdrawals"].([]map[string]any)
+ block.Withdrawals = make([]*model.Withdrawal, 0, len(withdrawals))
+ for _, withdrawal := range withdrawals {
+ w := &model.Withdrawal{}
+ w.Index = *convertDataToIntP(withdrawal, "index")
+ w.Validator = *convertDataToIntP(withdrawal, "validator")
+ w.Address = *convertDataToStringP(withdrawal, "address")
+ w.Amount = *convertDataToStringP(withdrawal, "amount")
+ block.Withdrawals = append(block.Withdrawals, w)
+ }
+
+ return block, nil
+}
+
+func (r *queryResolver) buildTransaction(block *model.Block, transReceipt map[string]any) *model.Transaction {
+ trans := &model.Transaction{}
+ trans.Block = block
+ trans.CumulativeGasUsed = convertDataToUint64P(transReceipt, "cumulativeGasUsed")
+ trans.Gas = *convertDataToUint64P(transReceipt, "gas")
+ trans.InputData = *convertDataToStringP(transReceipt, "data")
+ trans.EffectiveGasPrice = convertDataToStringP(transReceipt, "effectiveGasPrice")
+ if trans.EffectiveGasPrice != nil {
+ trans.GasPrice = *trans.EffectiveGasPrice
+ }
+ trans.GasUsed = convertDataToUint64P(transReceipt, "gasUsed")
+ trans.Hash = *convertDataToStringP(transReceipt, "transactionHash")
+ trans.Index = convertDataToIntP(transReceipt, "transactionIndex")
+ trans.MaxFeePerGas = convertDataToStringP(transReceipt, "maxFeePerGas")
+ trans.MaxPriorityFeePerGas = convertDataToStringP(transReceipt, "maxPriorityFeePerGas")
+ if transNonce := convertDataToStringP(transReceipt, "nonce"); transNonce != nil {
+ trans.Nonce = *transNonce
+ }
+ trans.Status = convertDataToUint64P(transReceipt, "status")
+ trans.Type = convertDataToUint64P(transReceipt, "type")
+ trans.Value = *convertDataToStringP(transReceipt, "value")
+
+ logs := transReceipt["logs"].(types.Logs)
+ trans.Logs = make([]*model.Log, 0, len(logs))
+ for _, rlog := range logs {
+ tlog := model.Log{
+ Index: int(rlog.Index),
+ Data: hexutil.Encode(rlog.Data),
+ }
+ tlog.Account = model.NewAccountAtBlock(block.Number)
+ tlog.Account.Address = strings.ToLower(rlog.Address.String())
+ tlog.Topics = make([]string, 0, len(rlog.Topics))
+ for _, rtopic := range rlog.Topics {
+ tlog.Topics = append(tlog.Topics, rtopic.String())
+ }
+ trans.Logs = append(trans.Logs, &tlog)
+ }
+
+ trans.From = model.NewAccountAtBlock(block.Number)
+ trans.From.Address = strings.ToLower(*convertDataToStringP(transReceipt, "from"))
+
+ if toAddress := convertDataToStringP(transReceipt, "to"); toAddress != nil {
+ trans.To = model.NewAccountAtBlock(block.Number)
+ trans.To.Address = strings.ToLower(*toAddress)
+ }
+
+ if contractAddr := convertDataToStringP(transReceipt, "contractAddress"); contractAddr != nil {
+ trans.CreatedContract = model.NewAccountAtBlock(block.Number)
+ trans.CreatedContract.Address = strings.ToLower(*contractAddr)
+ }
+
+ if al, ok := transReceipt["accessList"].(types.AccessList); ok {
+ trans.AccessList = make([]*model.AccessTuple, len(al))
+ for i, entry := range al {
+ keys := make([]string, len(entry.StorageKeys))
+ for j, k := range entry.StorageKeys {
+ keys[j] = k.Hex()
+ }
+ trans.AccessList[i] = &model.AccessTuple{
+ Address: strings.ToLower(entry.Address.String()),
+ StorageKeys: keys,
+ }
+ }
+ }
+
+ return trans
+}
diff --git a/cmd/rpcdaemon/graphql/graph/scalar/scalar.go b/cmd/rpcdaemon/graphql/graph/scalar/scalar.go
new file mode 100644
index 00000000000..25e487a43fd
--- /dev/null
+++ b/cmd/rpcdaemon/graphql/graph/scalar/scalar.go
@@ -0,0 +1,36 @@
+package scalar
+
+import (
+ "fmt"
+ "io"
+ "strconv"
+
+ "github.com/99designs/gqlgen/graphql"
+)
+
+type Uint64 = uint64
+
+func MarshalUint64(v uint64) graphql.Marshaler {
+ return graphql.WriterFunc(func(w io.Writer) {
+ fmt.Fprintf(w, `"0x%x"`, v)
+ })
+}
+
+func UnmarshalUint64(v any) (uint64, error) {
+ switch val := v.(type) {
+ case string:
+ if len(val) > 2 && val[0] == '0' && (val[1] == 'x' || val[1] == 'X') {
+ return strconv.ParseUint(val[2:], 16, 64)
+ }
+ return strconv.ParseUint(val, 10, 64)
+ case int64:
+ if val < 0 {
+ return 0, fmt.Errorf("Long cannot be negative: %d", val)
+ }
+ return uint64(val), nil
+ case float64:
+ return uint64(val), nil
+ default:
+ return graphql.UnmarshalUint64(v)
+ }
+}
diff --git a/cmd/rpcdaemon/graphql/graph/scalar/scalar_test.go b/cmd/rpcdaemon/graphql/graph/scalar/scalar_test.go
new file mode 100644
index 00000000000..361034e0c3b
--- /dev/null
+++ b/cmd/rpcdaemon/graphql/graph/scalar/scalar_test.go
@@ -0,0 +1,76 @@
+// Copyright 2024 The Erigon Authors
+// This file is part of Erigon.
+//
+// Erigon is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Erigon is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with Erigon. If not, see .
+
+package scalar_test
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/erigontech/erigon/cmd/rpcdaemon/graphql/graph/scalar"
+)
+
+func TestMarshalUint64(t *testing.T) {
+ tests := []struct {
+ input uint64
+ want string
+ }{
+ {0, `"0x0"`},
+ {2, `"0x2"`},
+ {255, `"0xff"`},
+ {1000, `"0x3e8"`},
+ {^uint64(0), `"0xffffffffffffffff"`},
+ }
+ for _, tt := range tests {
+ var buf bytes.Buffer
+ scalar.MarshalUint64(tt.input).MarshalGQL(&buf)
+ if got := buf.String(); got != tt.want {
+ t.Errorf("MarshalUint64(%d) = %s, want %s", tt.input, got, tt.want)
+ }
+ }
+}
+
+func TestUnmarshalUint64(t *testing.T) {
+ tests := []struct {
+ name string
+ input any
+ want uint64
+ wantErr bool
+ }{
+ {"hex string lowercase", "0x2", 2, false},
+ {"hex string uppercase", "0xFF", 255, false},
+ {"decimal string", "42", 42, false},
+ {"int64 positive", int64(10), 10, false},
+ {"float64", float64(7), 7, false},
+ {"zero hex", "0x0", 0, false},
+ {"zero decimal", "0", 0, false},
+ {"negative int64", int64(-1), 0, true},
+ {"empty string", "", 0, true},
+ {"invalid hex chars", "0xGG", 0, true},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := scalar.UnmarshalUint64(tt.input)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("UnmarshalUint64(%v) error = %v, wantErr %v", tt.input, err, tt.wantErr)
+ return
+ }
+ if !tt.wantErr && got != tt.want {
+ t.Errorf("UnmarshalUint64(%v) = %d, want %d", tt.input, got, tt.want)
+ }
+ })
+ }
+}
diff --git a/cmd/rpcdaemon/graphql/graph/schema.graphqls b/cmd/rpcdaemon/graphql/graph/schema.graphqls
index ff0bbb9800c..14233388363 100644
--- a/cmd/rpcdaemon/graphql/graph/schema.graphqls
+++ b/cmd/rpcdaemon/graphql/graph/schema.graphqls
@@ -121,7 +121,7 @@ type Transaction {
s: BigInt!
v: BigInt!
# Envelope transaction support
- type: Int
+ type: Long
accessList: [AccessTuple!]
# Raw is the canonical encoding of the transaction.
# For legacy transactions, it returns the RLP encoding.
diff --git a/cmd/rpcdaemon/graphql/graph/schema.resolvers.go b/cmd/rpcdaemon/graphql/graph/schema.resolvers.go
index 74f65f87f60..1418ee771b0 100644
--- a/cmd/rpcdaemon/graphql/graph/schema.resolvers.go
+++ b/cmd/rpcdaemon/graphql/graph/schema.resolvers.go
@@ -7,18 +7,31 @@ package graph
import (
"context"
- "encoding/hex"
"fmt"
"strconv"
- "strings"
"github.com/erigontech/erigon/cmd/rpcdaemon/graphql/graph/model"
"github.com/erigontech/erigon/common"
"github.com/erigontech/erigon/common/hexutil"
- "github.com/erigontech/erigon/execution/types"
"github.com/erigontech/erigon/rpc"
)
+// TransactionAt is the resolver for the transactionAt field.
+func (r *blockResolver) TransactionAt(ctx context.Context, obj *model.Block, index int) (*model.Transaction, error) {
+ if index < 0 || index >= len(obj.Transactions) {
+ return nil, nil
+ }
+ return obj.Transactions[index], nil
+}
+
+// Account is the resolver for the account field.
+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.resolveAccountAtBlock(ctx, address, obj.Number, nil)
+}
+
// SendRawTransaction is the resolver for the sendRawTransaction field.
func (r *mutationResolver) SendRawTransaction(ctx context.Context, data string) (string, error) {
panic("not implemented: SendRawTransaction - sendRawTransaction")
@@ -26,41 +39,30 @@ func (r *mutationResolver) SendRawTransaction(ctx context.Context, data string)
// Block is the resolver for the block field.
func (r *queryResolver) Block(ctx context.Context, number *string, hash *string) (*model.Block, error) {
+ if hash != nil && number == nil {
+ blockHash := common.HexToHash(*hash)
+ res, err := r.GraphQLAPI.GetBlockDetailsByHash(ctx, blockHash)
+ if err != nil {
+ return nil, err
+ }
+ return r.buildBlock(res)
+ }
+
var blockNumber rpc.BlockNumber
if number != nil {
- // Block number is not null, test for a positive long integer
bNum, err := strconv.ParseUint(*number, 10, 64)
if err == nil {
- // Positive integer, go ahead
blockNumber = rpc.BlockNumber(bNum)
} else {
bNum, err := hexutil.DecodeUint64(*number)
if err == nil {
- // Hexadecimal, 0x prefixed
blockNumber = rpc.BlockNumber(bNum)
} else {
- var err error
- return nil, err
+ return nil, fmt.Errorf("invalid block number: %s", *number)
}
}
} else {
- if hash != nil {
- blockHash, _ := hexutil.DecodeBig(*hash)
- fmt.Println("TODO/GraphQL/Implement me, get Block by hash=", blockHash)
- hash = nil
- }
- }
-
- if number == nil && hash == nil {
- // If neither number or hash is specified (nil), we should deliver "latest" block
- /*
- rpc.LatestExecutedBlockNumber = BlockNumber(-5)
- rpc.FinalizedBlockNumber = BlockNumber(-4)
- rpc.SafeBlockNumber = BlockNumber(-3)
- rpc.PendingBlockNumber = BlockNumber(-2)
- rpc.LatestBlockNumber = BlockNumber(-1)
- */
blockNumber = rpc.LatestBlockNumber
}
@@ -68,120 +70,7 @@ func (r *queryResolver) Block(ctx context.Context, number *string, hash *string)
if err != nil {
return nil, err
}
-
- block := &model.Block{}
- absBlk := res["block"]
-
- if absBlk != nil {
- blk := absBlk.(map[string]any)
-
- block.Difficulty = *convertDataToStringP(blk, "difficulty")
- block.ExtraData = *convertDataToStringP(blk, "extraData")
- block.GasLimit = uint64(*convertDataToUint64P(blk, "gasLimit"))
- block.GasUsed = *convertDataToUint64P(blk, "gasUsed")
- block.Hash = *convertDataToStringP(blk, "hash")
- block.Miner = &model.Account{}
- address := convertDataToStringP(blk, "miner")
- if address != nil {
- block.Miner.Address = strings.ToLower(*address)
- }
- mixHash := convertDataToStringP(blk, "mixHash")
- if mixHash != nil {
- block.MixHash = *mixHash
- }
- blockNonce := convertDataToStringP(blk, "nonce")
- if blockNonce != nil {
- block.Nonce = *blockNonce
- }
- block.Number = *convertDataToUint64P(blk, "number")
- block.Miner.BlockNum = block.Number
- block.Parent = &model.Block{}
- block.Parent.Hash = *convertDataToStringP(blk, "parentHash")
- block.ReceiptsRoot = *convertDataToStringP(blk, "receiptsRoot")
- block.StateRoot = *convertDataToStringP(blk, "stateRoot")
- block.Timestamp = *convertDataToStringP(blk, "timestamp")
- block.TransactionCount = convertDataToIntP(blk, "transactionCount")
- block.TransactionsRoot = *convertDataToStringP(blk, "transactionsRoot")
- block.BaseFeePerGas = convertDataToStringP(blk, "baseFeePerGas")
- block.Transactions = []*model.Transaction{}
-
- block.LogsBloom = "0x" + *convertDataToStringP(blk, "logsBloom")
- block.OmmerHash = *convertDataToStringP(blk, "sha3Uncles")
-
- // Ommers
- block.Ommers = []*model.Block{}
- for _, ommerHash := range blk["uncles"].([]common.Hash) {
- block.Ommers = append(block.Ommers, &model.Block{Hash: ommerHash.String()})
- }
-
- ommerCount := len(block.Ommers)
- block.OmmerCount = &ommerCount
-
- // Transactions
- absRcp := res["receipts"]
- rcp := absRcp.([]map[string]any)
- for _, transReceipt := range rcp {
- trans := &model.Transaction{}
- trans.CumulativeGasUsed = convertDataToUint64P(transReceipt, "cumulativeGasUsed")
- trans.InputData = *convertDataToStringP(transReceipt, "data")
- trans.EffectiveGasPrice = convertDataToStringP(transReceipt, "effectiveGasPrice")
- trans.GasPrice = *convertDataToStringP(transReceipt, "effectiveGasPrice")
- trans.GasUsed = convertDataToUint64P(transReceipt, "gasUsed")
- trans.Hash = *convertDataToStringP(transReceipt, "transactionHash")
- trans.Index = convertDataToIntP(transReceipt, "transactionIndex")
- transNonce := convertDataToStringP(transReceipt, "nonce")
- if transNonce != nil {
- trans.Nonce = *transNonce
- }
- trans.Status = convertDataToUint64P(transReceipt, "status")
- trans.Type = convertDataToIntP(transReceipt, "type")
- trans.Value = *convertDataToStringP(transReceipt, "value")
-
- trans.Logs = make([]*model.Log, 0)
- for _, rlog := range transReceipt["logs"].(types.Logs) {
- tlog := model.Log{
- Index: int(rlog.Index),
- Data: "0x" + hex.EncodeToString(rlog.Data),
- }
- tlog.Account = model.NewAccountAtBlock(block.Number)
- tlog.Account.Address = strings.ToLower(rlog.Address.String())
-
- for _, rtopic := range rlog.Topics {
- tlog.Topics = append(tlog.Topics, rtopic.String())
- }
-
- trans.Logs = append(trans.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"))
- }
-
- block.Transactions = append(block.Transactions, trans)
- }
-
- // Withdrawals
- block.Withdrawals = []*model.Withdrawal{}
- absWthd := res["withdrawals"]
- wthd := absWthd.([]map[string]any)
- for _, withdrawal := range wthd {
- wthd := &model.Withdrawal{}
- wthd.Index = *convertDataToIntP(withdrawal, "index")
- wthd.Validator = *convertDataToIntP(withdrawal, "validator")
- wthd.Address = *convertDataToStringP(withdrawal, "address")
- wthd.Amount = *convertDataToStringP(withdrawal, "amount")
-
- block.Withdrawals = append(block.Withdrawals, wthd)
- }
- }
-
- return block, ctx.Err()
+ return r.buildBlock(res)
}
// Blocks is the resolver for the blocks field.
@@ -194,7 +83,7 @@ func (r *queryResolver) Blocks(ctx context.Context, from *uint64, to *uint64) ([
toBlockNumber := *to
if toBlockNumber >= fromBlockNumber && (toBlockNumber-fromBlockNumber+1) < maxBlocks {
-
+ blocks = make([]*model.Block, 0, toBlockNumber-fromBlockNumber+1)
for i := fromBlockNumber; i <= toBlockNumber; i++ {
blockNumberStr := strconv.FormatUint(i, 10)
block, _ := r.Block(ctx, &blockNumberStr, nil)
@@ -214,7 +103,36 @@ func (r *queryResolver) Pending(ctx context.Context) (*model.Pending, error) {
// Transaction is the resolver for the transaction field.
func (r *queryResolver) Transaction(ctx context.Context, hash string) (*model.Transaction, error) {
- panic("not implemented: Transaction - transaction")
+ hashBytes, decErr := hexutil.Decode(hash)
+ if decErr != nil {
+ return nil, fmt.Errorf("invalid transaction hash: %w", decErr)
+ }
+ txHash := common.BytesToHash(hashBytes)
+
+ blockNum, ok, err := r.GraphQLAPI.GetBlockNumberForTx(ctx, txHash)
+ if err != nil {
+ return nil, err
+ }
+ if !ok {
+ return nil, nil
+ }
+
+ blockNumberStr := strconv.FormatUint(blockNum, 10)
+ block, err := r.Block(ctx, &blockNumberStr, nil)
+ if err != nil {
+ return nil, err
+ }
+ if block == nil {
+ return nil, nil
+ }
+
+ hashStr := txHash.String()
+ for _, trans := range block.Transactions {
+ if trans.Hash == hashStr {
+ return trans, nil
+ }
+ }
+ return nil, nil
}
// Logs is the resolver for the logs field.
@@ -244,52 +162,35 @@ func (r *queryResolver) ChainID(ctx context.Context) (string, error) {
return "0x" + strconv.FormatUint(chainID.Uint64(), 16), err
}
+// From is the resolver for the from field.
+func (r *transactionResolver) From(ctx context.Context, obj *model.Transaction, block *uint64) (*model.Account, error) {
+ if obj.From == nil {
+ return nil, nil
+ }
+ return r.resolveAccountAtBlock(ctx, obj.From.Address, obj.Block.Number, block)
+}
+
+// To is the resolver for the to field.
+func (r *transactionResolver) To(ctx context.Context, obj *model.Transaction, block *uint64) (*model.Account, error) {
+ if obj.To == nil {
+ return nil, nil
+ }
+ return r.resolveAccountAtBlock(ctx, obj.To.Address, obj.Block.Number, block)
+}
+
+// Block returns BlockResolver implementation.
+func (r *Resolver) Block() BlockResolver { return &blockResolver{r} }
+
// Mutation returns MutationResolver implementation.
func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} }
// Query returns QueryResolver implementation.
func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }
-// Block returns BlockResolver implementation.
-func (r *Resolver) Block() BlockResolver { return &blockResolver{r} }
-
-// Account returns AccountResolver implementation.
-func (r *Resolver) Account() AccountResolver { return &accountResolver{r} }
+// Transaction returns TransactionResolver implementation.
+func (r *Resolver) Transaction() TransactionResolver { return &transactionResolver{r} }
+type blockResolver struct{ *Resolver }
type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
-type blockResolver struct{ *Resolver }
-type accountResolver 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)
- }
- addr := common.HexToAddress(address)
- blockNumber := rpc.BlockNumber(obj.Number)
-
- balance, nonce, code, err := r.GraphQLAPI.GetAccountInfo(ctx, addr, blockNumber)
- if err != nil {
- return nil, err
- }
-
- return &model.Account{
- Address: strings.ToLower(address),
- Balance: balance,
- TransactionCount: nonce,
- Code: code,
- BlockNum: obj.Number,
- }, nil
-}
-
-// Storage is the resolver for the storage field on Account.
-// TODO: each storage(slot) call opens a new DB transaction and rebuilds the state reader,
-// causing an N+1 pattern when a query resolves multiple slots. Fix by caching the state
-// reader in the request context so all slots for the same account/block share one tx.
-func (r *accountResolver) Storage(ctx context.Context, obj *model.Account, slot string) (string, error) {
- addr := common.HexToAddress(obj.Address)
- blockNumber := rpc.BlockNumber(obj.BlockNum)
-
- return r.GraphQLAPI.GetAccountStorage(ctx, addr, slot, blockNumber)
-}
+type transactionResolver struct{ *Resolver }
diff --git a/cmd/rpcdaemon/graphql/graph/schema_resolvers_test.go b/cmd/rpcdaemon/graphql/graph/schema_resolvers_test.go
index 63e9531de76..0ac1715fe59 100644
--- a/cmd/rpcdaemon/graphql/graph/schema_resolvers_test.go
+++ b/cmd/rpcdaemon/graphql/graph/schema_resolvers_test.go
@@ -58,3 +58,54 @@ func TestNewAccountAtBlock(t *testing.T) {
t.Errorf("expected BlockNum=42, got %d", acc.BlockNum)
}
}
+
+func TestBlockResolver_TransactionAt(t *testing.T) {
+ r := &blockResolver{&Resolver{}}
+
+ tx0 := &model.Transaction{Hash: "0xaaa"}
+ tx1 := &model.Transaction{Hash: "0xbbb"}
+ block := &model.Block{Transactions: []*model.Transaction{tx0, tx1}}
+
+ tests := []struct {
+ name string
+ index int
+ want *model.Transaction
+ }{
+ {"first", 0, tx0},
+ {"second", 1, tx1},
+ {"out of range", 2, nil},
+ {"negative", -1, nil},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := r.TransactionAt(context.Background(), block, tt.index)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if got != tt.want {
+ t.Errorf("TransactionAt(%d) = %v, want %v", tt.index, got, tt.want)
+ }
+ })
+ }
+}
+
+func TestQueryResolver_Transaction_InvalidHash(t *testing.T) {
+ r := &queryResolver{&Resolver{}} // GraphQLAPI is nil; error fires before it is called
+
+ tests := []struct {
+ name string
+ hash string
+ }{
+ {"empty", ""},
+ {"no 0x prefix", "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"},
+ {"invalid hex chars", "0xGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG"},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ _, err := r.Transaction(context.Background(), tt.hash)
+ if err == nil {
+ t.Errorf("hash %q: expected error, got nil", tt.hash)
+ }
+ })
+ }
+}
diff --git a/rpc/jsonrpc/graphql_api.go b/rpc/jsonrpc/graphql_api.go
index 9442672a151..6dd28b08a36 100644
--- a/rpc/jsonrpc/graphql_api.go
+++ b/rpc/jsonrpc/graphql_api.go
@@ -33,9 +33,11 @@ import (
type GraphQLAPI interface {
GetBlockDetails(ctx context.Context, number rpc.BlockNumber) (map[string]any, error)
+ GetBlockDetailsByHash(ctx context.Context, hash common.Hash) (map[string]any, error)
GetChainID(ctx context.Context) (*big.Int, error)
GetAccountInfo(ctx context.Context, address common.Address, blockNumber rpc.BlockNumber) (balance string, nonce uint64, code string, err error)
GetAccountStorage(ctx context.Context, address common.Address, slot string, blockNumber rpc.BlockNumber) (string, error)
+ GetBlockNumberForTx(ctx context.Context, hash common.Hash) (blockNum uint64, ok bool, err error)
}
type GraphQLAPIImpl struct {
@@ -50,6 +52,17 @@ func NewGraphQLAPI(base *BaseAPI, db kv.TemporalRoDB) *GraphQLAPIImpl {
}
}
+func (api *GraphQLAPIImpl) GetBlockNumberForTx(ctx context.Context, hash common.Hash) (uint64, bool, error) {
+ tx, err := api.db.BeginTemporalRo(ctx)
+ if err != nil {
+ return 0, false, err
+ }
+ defer tx.Rollback()
+
+ blockNum, _, ok, err := api.txnLookup(ctx, tx, hash)
+ return blockNum, ok, err
+}
+
func (api *GraphQLAPIImpl) GetChainID(ctx context.Context) (*big.Int, error) {
tx, err := api.db.BeginTemporalRo(ctx)
if err != nil {
@@ -85,6 +98,33 @@ func (api *GraphQLAPIImpl) GetBlockDetails(ctx context.Context, blockNumber rpc.
return nil, err
}
+ return api.buildBlockDetailsResponse(ctx, tx, block, getBlockRes)
+}
+
+func (api *GraphQLAPIImpl) GetBlockDetailsByHash(ctx context.Context, hash common.Hash) (map[string]any, error) {
+ tx, err := api.db.BeginTemporalRo(ctx)
+ if err != nil {
+ return nil, err
+ }
+ defer tx.Rollback()
+
+ block, err := api.blockByHashWithSenders(ctx, tx, hash)
+ if err != nil {
+ return nil, err
+ }
+ if block == nil {
+ return nil, nil
+ }
+
+ getBlockRes, err := api.delegateGetBlockByNumber(tx, block, rpc.BlockNumber(block.NumberU64()), false)
+ if err != nil {
+ return nil, err
+ }
+
+ return api.buildBlockDetailsResponse(ctx, tx, block, getBlockRes)
+}
+
+func (api *GraphQLAPIImpl) buildBlockDetailsResponse(ctx context.Context, tx kv.TemporalTx, block *types.Block, getBlockRes map[string]any) (map[string]any, error) {
chainConfig, err := api.chainConfig(ctx, tx)
if err != nil {
return nil, err
@@ -104,6 +144,17 @@ func (api *GraphQLAPIImpl) GetBlockDetails(ctx context.Context, blockNumber rpc.
transaction["value"] = txn.GetValue()
transaction["data"] = txn.GetData()
transaction["logs"] = receipt.Logs
+ transaction["gas"] = txn.GetGasLimit()
+ txType := txn.Type()
+ if txType == types.DynamicFeeTxType || txType == types.SetCodeTxType {
+ transaction["maxFeePerGas"] = txn.GetFeeCap()
+ transaction["maxPriorityFeePerGas"] = txn.GetTipCap()
+ }
+ transaction["accessList"] = txn.GetAccessList()
+ // Pre-Byzantium receipts have PostState instead of Status; default status to 0.
+ if _, hasStatus := transaction["status"]; !hasStatus {
+ transaction["status"] = hexutil.Uint64(0)
+ }
result = append(result, transaction)
}
@@ -111,7 +162,6 @@ func (api *GraphQLAPIImpl) GetBlockDetails(ctx context.Context, blockNumber rpc.
response["block"] = getBlockRes
response["receipts"] = result
- // Withdrawals
wresult := make([]map[string]any, 0, len(block.Withdrawals()))
for _, withdrawal := range block.Withdrawals() {
wmap := make(map[string]any)