From 2f98dd3838bc5e6de462d128b7cce776a2abcf83 Mon Sep 17 00:00:00 2001 From: Sina Mahmoodi <1591639+s1na@users.noreply.github.com> Date: Tue, 25 Apr 2023 14:02:54 +0200 Subject: [PATCH] graphql: encode Long values as hex (#26894) This is a breaking GraphQL API change. All numeric values are now encoded as hex strings. The motivation for this change is matching JSON-RPC outputs more closely. Numbers in query parameters are accepted as both decimal integers and hex strings. --- graphql/graphql.go | 109 ++++++++++++++++++++-------------------- graphql/graphql_test.go | 30 +++++------ graphql/schema.go | 20 ++++---- 3 files changed, 78 insertions(+), 81 deletions(-) diff --git a/graphql/graphql.go b/graphql/graphql.go index 45a1f7a2aa..ce3ca171cf 100644 --- a/graphql/graphql.go +++ b/graphql/graphql.go @@ -24,6 +24,7 @@ import ( "math/big" "sort" "strconv" + "strings" "sync" "github.com/ethereum/go-ethereum" @@ -54,16 +55,16 @@ func (b *Long) UnmarshalGraphQL(input interface{}) error { switch input := input.(type) { case string: // uncomment to support hex values - //if strings.HasPrefix(input, "0x") { - // // apply leniency and support hex representations of longs. - // value, err := hexutil.DecodeUint64(input) - // *b = Long(value) - // return err - //} else { - value, err := strconv.ParseInt(input, 10, 64) - *b = Long(value) - return err - //} + if strings.HasPrefix(input, "0x") { + // apply leniency and support hex representations of longs. + value, err := hexutil.DecodeUint64(input) + *b = Long(value) + return err + } else { + value, err := strconv.ParseInt(input, 10, 64) + *b = Long(value) + return err + } case int32: *b = Long(input) case int64: @@ -156,8 +157,8 @@ func (l *Log) Account(ctx context.Context, args BlockNumberArgs) *Account { } } -func (l *Log) Index(ctx context.Context) int32 { - return int32(l.log.Index) +func (l *Log) Index(ctx context.Context) hexutil.Uint64 { + return hexutil.Uint64(l.log.Index) } func (l *Log) Topics(ctx context.Context) []common.Hash { @@ -391,7 +392,7 @@ func (t *Transaction) Block(ctx context.Context) (*Block, error) { return block, nil } -func (t *Transaction) Index(ctx context.Context) (*int32, error) { +func (t *Transaction) Index(ctx context.Context) (*hexutil.Uint64, error) { _, block, err := t.resolve(ctx) if err != nil { return nil, err @@ -400,7 +401,7 @@ func (t *Transaction) Index(ctx context.Context) (*int32, error) { if block == nil { return nil, nil } - index := int32(t.index) + index := hexutil.Uint64(t.index) return &index, nil } @@ -421,7 +422,7 @@ func (t *Transaction) getReceipt(ctx context.Context) (*types.Receipt, error) { return receipts[t.index], nil } -func (t *Transaction) Status(ctx context.Context) (*Long, error) { +func (t *Transaction) Status(ctx context.Context) (*hexutil.Uint64, error) { receipt, err := t.getReceipt(ctx) if err != nil || receipt == nil { return nil, err @@ -429,25 +430,25 @@ func (t *Transaction) Status(ctx context.Context) (*Long, error) { if len(receipt.PostState) != 0 { return nil, nil } - ret := Long(receipt.Status) + ret := hexutil.Uint64(receipt.Status) return &ret, nil } -func (t *Transaction) GasUsed(ctx context.Context) (*Long, error) { +func (t *Transaction) GasUsed(ctx context.Context) (*hexutil.Uint64, error) { receipt, err := t.getReceipt(ctx) if err != nil || receipt == nil { return nil, err } - ret := Long(receipt.GasUsed) + ret := hexutil.Uint64(receipt.GasUsed) return &ret, nil } -func (t *Transaction) CumulativeGasUsed(ctx context.Context) (*Long, error) { +func (t *Transaction) CumulativeGasUsed(ctx context.Context) (*hexutil.Uint64, error) { receipt, err := t.getReceipt(ctx) if err != nil || receipt == nil { return nil, err } - ret := Long(receipt.CumulativeGasUsed) + ret := hexutil.Uint64(receipt.CumulativeGasUsed) return &ret, nil } @@ -503,12 +504,12 @@ func (t *Transaction) getLogs(ctx context.Context, hash common.Hash) (*[]*Log, e return &ret, nil } -func (t *Transaction) Type(ctx context.Context) (*int32, error) { +func (t *Transaction) Type(ctx context.Context) (*hexutil.Uint64, error) { tx, _, err := t.resolve(ctx) if err != nil { return nil, err } - txType := int32(tx.Type()) + txType := hexutil.Uint64(tx.Type()) return &txType, nil } @@ -649,13 +650,13 @@ func (b *Block) resolveReceipts(ctx context.Context) ([]*types.Receipt, error) { return receipts, nil } -func (b *Block) Number(ctx context.Context) (Long, error) { +func (b *Block) Number(ctx context.Context) (hexutil.Uint64, error) { header, err := b.resolveHeader(ctx) if err != nil { return 0, err } - return Long(header.Number.Uint64()), nil + return hexutil.Uint64(header.Number.Uint64()), nil } func (b *Block) Hash(ctx context.Context) (common.Hash, error) { @@ -664,20 +665,20 @@ func (b *Block) Hash(ctx context.Context) (common.Hash, error) { return b.hash, nil } -func (b *Block) GasLimit(ctx context.Context) (Long, error) { +func (b *Block) GasLimit(ctx context.Context) (hexutil.Uint64, error) { header, err := b.resolveHeader(ctx) if err != nil { return 0, err } - return Long(header.GasLimit), nil + return hexutil.Uint64(header.GasLimit), nil } -func (b *Block) GasUsed(ctx context.Context) (Long, error) { +func (b *Block) GasUsed(ctx context.Context) (hexutil.Uint64, error) { header, err := b.resolveHeader(ctx) if err != nil { return 0, err } - return Long(header.GasUsed), nil + return hexutil.Uint64(header.GasUsed), nil } func (b *Block) BaseFeePerGas(ctx context.Context) (*hexutil.Big, error) { @@ -793,12 +794,12 @@ func (b *Block) OmmerHash(ctx context.Context) (common.Hash, error) { return header.UncleHash, nil } -func (b *Block) OmmerCount(ctx context.Context) (*int32, error) { +func (b *Block) OmmerCount(ctx context.Context) (*hexutil.Uint64, error) { block, err := b.resolve(ctx) if err != nil || block == nil { return nil, err } - count := int32(len(block.Uncles())) + count := hexutil.Uint64(len(block.Uncles())) return &count, err } @@ -869,7 +870,7 @@ type BlockNumberArgs struct { // TODO: Ideally we could use input unions to allow the query to specify the // block parameter by hash, block number, or tag but input unions aren't part of the // standard GraphQL schema SDL yet, see: https://github.com/graphql/graphql-spec/issues/488 - Block *hexutil.Uint64 + Block *Long } // NumberOr returns the provided block number argument, or the "current" block number or hash if none @@ -900,12 +901,12 @@ func (b *Block) Miner(ctx context.Context, args BlockNumberArgs) (*Account, erro }, nil } -func (b *Block) TransactionCount(ctx context.Context) (*int32, error) { +func (b *Block) TransactionCount(ctx context.Context) (*hexutil.Uint64, error) { block, err := b.resolve(ctx) if err != nil || block == nil { return nil, err } - count := int32(len(block.Transactions())) + count := hexutil.Uint64(len(block.Transactions())) return &count, err } @@ -927,7 +928,7 @@ func (b *Block) Transactions(ctx context.Context) (*[]*Transaction, error) { return &ret, nil } -func (b *Block) TransactionAt(ctx context.Context, args struct{ Index int32 }) (*Transaction, error) { +func (b *Block) TransactionAt(ctx context.Context, args struct{ Index Long }) (*Transaction, error) { block, err := b.resolve(ctx) if err != nil || block == nil { return nil, err @@ -946,7 +947,7 @@ func (b *Block) TransactionAt(ctx context.Context, args struct{ Index int32 }) ( }, nil } -func (b *Block) OmmerAt(ctx context.Context, args struct{ Index int32 }) (*Block, error) { +func (b *Block) OmmerAt(ctx context.Context, args struct{ Index Long }) (*Block, error) { block, err := b.resolve(ctx) if err != nil || block == nil { return nil, err @@ -1037,7 +1038,7 @@ func (b *Block) Account(ctx context.Context, args struct { type CallData struct { From *common.Address // The Ethereum address the call is from. To *common.Address // The Ethereum address the call is to. - Gas *hexutil.Uint64 // The amount of gas provided for the call. + Gas *Long // The amount of gas provided for the call. GasPrice *hexutil.Big // The price of each unit of gas, in wei. MaxFeePerGas *hexutil.Big // The max price of each unit of gas, in wei (1559). MaxPriorityFeePerGas *hexutil.Big // The max tip of each unit of gas, in wei (1559). @@ -1047,20 +1048,20 @@ type CallData struct { // CallResult encapsulates the result of an invocation of the `call` accessor. type CallResult struct { - data hexutil.Bytes // The return data from the call - gasUsed Long // The amount of gas used - status Long // The return status of the call - 0 for failure or 1 for success. + data hexutil.Bytes // The return data from the call + gasUsed hexutil.Uint64 // The amount of gas used + status hexutil.Uint64 // The return status of the call - 0 for failure or 1 for success. } func (c *CallResult) Data() hexutil.Bytes { return c.data } -func (c *CallResult) GasUsed() Long { +func (c *CallResult) GasUsed() hexutil.Uint64 { return c.gasUsed } -func (c *CallResult) Status() Long { +func (c *CallResult) Status() hexutil.Uint64 { return c.status } @@ -1071,32 +1072,31 @@ func (b *Block) Call(ctx context.Context, args struct { if err != nil { return nil, err } - status := Long(1) + status := hexutil.Uint64(1) if result.Failed() { status = 0 } return &CallResult{ data: result.ReturnData, - gasUsed: Long(result.UsedGas), + gasUsed: hexutil.Uint64(result.UsedGas), status: status, }, nil } func (b *Block) EstimateGas(ctx context.Context, args struct { Data ethapi.TransactionArgs -}) (Long, error) { - gas, err := ethapi.DoEstimateGas(ctx, b.r.backend, args.Data, *b.numberOrHash, b.r.backend.RPCGasCap()) - return Long(gas), err +}) (hexutil.Uint64, error) { + return ethapi.DoEstimateGas(ctx, b.r.backend, args.Data, *b.numberOrHash, b.r.backend.RPCGasCap()) } type Pending struct { r *Resolver } -func (p *Pending) TransactionCount(ctx context.Context) (int32, error) { +func (p *Pending) TransactionCount(ctx context.Context) (hexutil.Uint64, error) { txs, err := p.r.backend.GetPoolTransactions() - return int32(len(txs)), err + return hexutil.Uint64(len(txs)), err } func (p *Pending) Transactions(ctx context.Context) (*[]*Transaction, error) { @@ -1135,24 +1135,23 @@ func (p *Pending) Call(ctx context.Context, args struct { if err != nil { return nil, err } - status := Long(1) + status := hexutil.Uint64(1) if result.Failed() { status = 0 } return &CallResult{ data: result.ReturnData, - gasUsed: Long(result.UsedGas), + gasUsed: hexutil.Uint64(result.UsedGas), status: status, }, nil } func (p *Pending) EstimateGas(ctx context.Context, args struct { Data ethapi.TransactionArgs -}) (Long, error) { +}) (hexutil.Uint64, error) { pendingBlockNr := rpc.BlockNumberOrHashWithNumber(rpc.PendingBlockNumber) - gas, err := ethapi.DoEstimateGas(ctx, p.r.backend, args.Data, pendingBlockNr, p.r.backend.RPCGasCap()) - return Long(gas), err + return ethapi.DoEstimateGas(ctx, p.r.backend, args.Data, pendingBlockNr, p.r.backend.RPCGasCap()) } // Resolver is the top-level object in the GraphQL hierarchy. @@ -1260,8 +1259,8 @@ func (r *Resolver) SendRawTransaction(ctx context.Context, args struct{ Data hex // FilterCriteria encapsulates the arguments to `logs` on the root resolver object. type FilterCriteria struct { - FromBlock *hexutil.Uint64 // beginning of the queried range, nil means genesis block - ToBlock *hexutil.Uint64 // end of the range, nil means latest block + FromBlock *Long // beginning of the queried range, nil means genesis block + ToBlock *Long // end of the range, nil means latest block Addresses *[]common.Address // restricts matches to events created by specific contracts // The Topic list restricts matches to particular event topics. Each event has a list diff --git a/graphql/graphql_test.go b/graphql/graphql_test.go index 9354eac0f3..00ad4f0211 100644 --- a/graphql/graphql_test.go +++ b/graphql/graphql_test.go @@ -80,17 +80,17 @@ func TestGraphQLBlockSerialization(t *testing.T) { }{ { // Should return latest block body: `{"query": "{block{number}}","variables": null}`, - want: `{"data":{"block":{"number":10}}}`, + want: `{"data":{"block":{"number":"0xa"}}}`, code: 200, }, { // Should return info about latest block body: `{"query": "{block{number,gasUsed,gasLimit}}","variables": null}`, - want: `{"data":{"block":{"number":10,"gasUsed":0,"gasLimit":11500000}}}`, + want: `{"data":{"block":{"number":"0xa","gasUsed":"0x0","gasLimit":"0xaf79e0"}}}`, code: 200, }, { body: `{"query": "{block(number:0){number,gasUsed,gasLimit}}","variables": null}`, - want: `{"data":{"block":{"number":0,"gasUsed":0,"gasLimit":11500000}}}`, + want: `{"data":{"block":{"number":"0x0","gasUsed":"0x0","gasLimit":"0xaf79e0"}}}`, code: 200, }, { @@ -105,7 +105,7 @@ func TestGraphQLBlockSerialization(t *testing.T) { }, { body: `{"query": "{block(number:\"0\"){number,gasUsed,gasLimit}}","variables": null}`, - want: `{"data":{"block":{"number":0,"gasUsed":0,"gasLimit":11500000}}}`, + want: `{"data":{"block":{"number":"0x0","gasUsed":"0x0","gasLimit":"0xaf79e0"}}}`, code: 200, }, { @@ -119,14 +119,10 @@ func TestGraphQLBlockSerialization(t *testing.T) { code: 200, }, { - body: `{"query": "{block(number:\"0xbad\"){number,gasUsed,gasLimit}}","variables": null}`, - want: `{"errors":[{"message":"strconv.ParseInt: parsing \"0xbad\": invalid syntax"}],"data":{}}`, - code: 400, - }, - { // hex strings are currently not supported. If that's added to the spec, this test will need to change body: `{"query": "{block(number:\"0x0\"){number,gasUsed,gasLimit}}","variables": null}`, - want: `{"errors":[{"message":"strconv.ParseInt: parsing \"0x0\": invalid syntax"}],"data":{}}`, - code: 400, + want: `{"data":{"block":{"number":"0x0","gasUsed":"0x0","gasLimit":"0xaf79e0"}}}`, + //want: `{"errors":[{"message":"strconv.ParseInt: parsing \"0x0\": invalid syntax"}],"data":{}}`, + code: 200, }, { body: `{"query": "{block(number:\"a\"){number,gasUsed,gasLimit}}","variables": null}`, @@ -141,13 +137,13 @@ func TestGraphQLBlockSerialization(t *testing.T) { // should return `estimateGas` as decimal { body: `{"query": "{block{ estimateGas(data:{}) }}"}`, - want: `{"data":{"block":{"estimateGas":53000}}}`, + want: `{"data":{"block":{"estimateGas":"0xcf08"}}}`, code: 200, }, // should return `status` as decimal { body: `{"query": "{block {number call (data : {from : \"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b\", to: \"0x6295ee1b4f6dd65047762f924ecd367c17eabf8f\", data :\"0x12a7b914\"}){data status}}}"}`, - want: `{"data":{"block":{"number":10,"call":{"data":"0x","status":1}}}}`, + want: `{"data":{"block":{"number":"0xa","call":{"data":"0x","status":"0x1"}}}}`, code: 200, }, } { @@ -231,7 +227,7 @@ func TestGraphQLBlockSerializationEIP2718(t *testing.T) { }{ { body: `{"query": "{block {number transactions { from { address } to { address } value hash type accessList { address storageKeys } index}}}"}`, - want: `{"data":{"block":{"number":1,"transactions":[{"from":{"address":"0x71562b71999873db5b286df957af199ec94617f7"},"to":{"address":"0x0000000000000000000000000000000000000dad"},"value":"0x64","hash":"0xd864c9d7d37fade6b70164740540c06dd58bb9c3f6b46101908d6339db6a6a7b","type":0,"accessList":[],"index":0},{"from":{"address":"0x71562b71999873db5b286df957af199ec94617f7"},"to":{"address":"0x0000000000000000000000000000000000000dad"},"value":"0x32","hash":"0x19b35f8187b4e15fb59a9af469dca5dfa3cd363c11d372058c12f6482477b474","type":1,"accessList":[{"address":"0x0000000000000000000000000000000000000dad","storageKeys":["0x0000000000000000000000000000000000000000000000000000000000000000"]}],"index":1}]}}}`, + want: `{"data":{"block":{"number":"0x1","transactions":[{"from":{"address":"0x71562b71999873db5b286df957af199ec94617f7"},"to":{"address":"0x0000000000000000000000000000000000000dad"},"value":"0x64","hash":"0xd864c9d7d37fade6b70164740540c06dd58bb9c3f6b46101908d6339db6a6a7b","type":"0x0","accessList":[],"index":"0x0"},{"from":{"address":"0x71562b71999873db5b286df957af199ec94617f7"},"to":{"address":"0x0000000000000000000000000000000000000dad"},"value":"0x32","hash":"0x19b35f8187b4e15fb59a9af469dca5dfa3cd363c11d372058c12f6482477b474","type":"0x1","accessList":[{"address":"0x0000000000000000000000000000000000000dad","storageKeys":["0x0000000000000000000000000000000000000000000000000000000000000000"]}],"index":"0x1"}]}}}`, code: 200, }, } { @@ -327,17 +323,17 @@ func TestGraphQLConcurrentResolvers(t *testing.T) { // Multiple txes of a block race to set/retrieve receipts of a block. { body: "{block { transactions { status gasUsed } } }", - want: `{"block":{"transactions":[{"status":1,"gasUsed":21768},{"status":1,"gasUsed":21768},{"status":1,"gasUsed":21768}]}}`, + want: `{"block":{"transactions":[{"status":"0x1","gasUsed":"0x5508"},{"status":"0x1","gasUsed":"0x5508"},{"status":"0x1","gasUsed":"0x5508"}]}}`, }, // Multiple fields of block race to resolve header and body. { body: "{ block { number hash gasLimit ommerCount transactionCount totalDifficulty } }", - want: fmt.Sprintf(`{"block":{"number":1,"hash":"%s","gasLimit":11500000,"ommerCount":0,"transactionCount":3,"totalDifficulty":"0x200000"}}`, chain[len(chain)-1].Hash()), + want: fmt.Sprintf(`{"block":{"number":"0x1","hash":"%s","gasLimit":"0xaf79e0","ommerCount":"0x0","transactionCount":"0x3","totalDifficulty":"0x200000"}}`, chain[len(chain)-1].Hash()), }, // Multiple fields of a block race to resolve the header and body. { body: fmt.Sprintf(`{ transaction(hash: "%s") { block { number hash gasLimit ommerCount transactionCount } } }`, tx.Hash()), - want: fmt.Sprintf(`{"transaction":{"block":{"number":1,"hash":"%s","gasLimit":11500000,"ommerCount":0,"transactionCount":3}}}`, chain[len(chain)-1].Hash()), + want: fmt.Sprintf(`{"transaction":{"block":{"number":"0x1","hash":"%s","gasLimit":"0xaf79e0","ommerCount":"0x0","transactionCount":"0x3"}}}`, chain[len(chain)-1].Hash()), }, // Account fields race the resolve the state object. { diff --git a/graphql/schema.go b/graphql/schema.go index ff3919be74..e4ce7b9afc 100644 --- a/graphql/schema.go +++ b/graphql/schema.go @@ -28,7 +28,9 @@ const schema string = ` # Strings may be either decimal or 0x-prefixed hexadecimal. Output values are all # 0x-prefixed hexadecimal. scalar BigInt - # Long is a 64 bit unsigned integer. + # Long is a 64 bit unsigned integer. Input is accepted as either a JSON number or as a string. + # Strings may be either decimal or 0x-prefixed hexadecimal. Output values are all + # 0x-prefixed hexadecimal. scalar Long schema { @@ -57,7 +59,7 @@ const schema string = ` # Log is an Ethereum event log. type Log { # Index is the index of this log in the block. - index: Int! + index: Long! # Account is the account which generated this log - this will always # be a contract account. account(block: Long): Account! @@ -83,7 +85,7 @@ const schema string = ` nonce: Long! # Index is the index of this transaction in the parent block. This will # be null if the transaction has not yet been mined. - index: Int + index: Long # From is the account that sent this transaction - this will always be # an externally owned account. from(block: Long): Account! @@ -138,7 +140,7 @@ const schema string = ` 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. @@ -183,7 +185,7 @@ const schema string = ` transactionsRoot: Bytes32! # TransactionCount is the number of transactions in this block. if # transactions are not available for this block, this field will be null. - transactionCount: Int + transactionCount: Long # StateRoot is the keccak256 hash of the state trie after this block was processed. stateRoot: Bytes32! # ReceiptsRoot is the keccak256 hash of the trie of transaction receipts in this block. @@ -214,7 +216,7 @@ const schema string = ` totalDifficulty: BigInt! # OmmerCount is the number of ommers (AKA uncles) associated with this # block. If ommers are unavailable, this field will be null. - ommerCount: Int + ommerCount: Long # Ommers is a list of ommer (AKA uncle) blocks associated with this block. # If ommers are unavailable, this field will be null. Depending on your # node, the transactions, transactionAt, transactionCount, ommers, @@ -222,7 +224,7 @@ const schema string = ` ommers: [Block] # OmmerAt returns the ommer (AKA uncle) at the specified index. If ommers # are unavailable, or the index is out of bounds, this field will be null. - ommerAt(index: Int!): Block + ommerAt(index: Long!): Block # OmmerHash is the keccak256 hash of all the ommers (AKA uncles) # associated with this block. ommerHash: Bytes32! @@ -232,7 +234,7 @@ const schema string = ` # TransactionAt returns the transaction at the specified index. If # transactions are unavailable for this block, or if the index is out of # bounds, this field will be null. - transactionAt(index: Int!): Transaction + transactionAt(index: Long!): Transaction # Logs returns a filtered set of logs from this block. logs(filter: BlockFilterCriteria!): [Log!]! # Account fetches an Ethereum account at the current block's state. @@ -317,7 +319,7 @@ const schema string = ` # Pending represents the current pending state. type Pending { # TransactionCount is the number of transactions in the pending state. - transactionCount: Int! + transactionCount: Long! # Transactions is a list of transactions in the current pending state. transactions: [Transaction!] # Account fetches an Ethereum account for the pending state.