graphql: Updates to graphql support to match EIP1767 (#19238)

Updates to match EIP1767
This commit is contained in:
Nick Johnson 2019-03-18 19:24:43 +13:00 committed by Guillaume Ballet
parent 6e401792ce
commit acebccc3bf
2 changed files with 285 additions and 141 deletions

View File

@ -19,6 +19,7 @@ package graphql
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
@ -43,6 +44,9 @@ import (
"github.com/graph-gophers/graphql-go/relay" "github.com/graph-gophers/graphql-go/relay"
) )
var OnlyOnMainChainError = errors.New("This operation is only available for blocks on the canonical chain.")
var BlockInvariantError = errors.New("Block objects must be instantiated with at least one of num or hash.")
// Account represents an Ethereum account at a particular block. // Account represents an Ethereum account at a particular block.
type Account struct { type Account struct {
backend *eth.EthAPIBackend backend *eth.EthAPIBackend
@ -144,8 +148,9 @@ func (t *Transaction) resolve(ctx context.Context) (*types.Transaction, error) {
if tx != nil { if tx != nil {
t.tx = tx t.tx = tx
t.block = &Block{ t.block = &Block{
backend: t.backend, backend: t.backend,
hash: blockHash, hash: blockHash,
canonical: unknown,
} }
t.index = index t.index = index
} else { } else {
@ -332,16 +337,47 @@ func (t *Transaction) Logs(ctx context.Context) (*[]*Log, error) {
return &ret, nil return &ret, nil
} }
// Block represennts an Ethereum block. type BlockType int
const (
unknown BlockType = iota
isCanonical
notCanonical
)
// Block represents an Ethereum block.
// backend, and either num or hash are mandatory. All other fields are lazily fetched // backend, and either num or hash are mandatory. All other fields are lazily fetched
// when required. // when required.
type Block struct { type Block struct {
backend *eth.EthAPIBackend backend *eth.EthAPIBackend
num *rpc.BlockNumber num *rpc.BlockNumber
hash common.Hash hash common.Hash
header *types.Header header *types.Header
block *types.Block block *types.Block
receipts []*types.Receipt receipts []*types.Receipt
canonical BlockType // Indicates if this block is on the main chain or not.
}
func (b *Block) onMainChain(ctx context.Context) error {
if b.canonical == unknown {
header, err := b.resolveHeader(ctx)
if err != nil {
return err
}
canonHeader, err := b.backend.HeaderByNumber(ctx, rpc.BlockNumber(header.Number.Uint64()))
if err != nil {
return err
}
if header.Hash() == canonHeader.Hash() {
b.canonical = isCanonical
} else {
b.canonical = notCanonical
}
}
if b.canonical != isCanonical {
return OnlyOnMainChainError
}
return nil
} }
// resolve returns the internal Block object representing this block, fetching // resolve returns the internal Block object representing this block, fetching
@ -367,6 +403,10 @@ func (b *Block) resolve(ctx context.Context) (*types.Block, error) {
// if necessary. Call this function instead of `resolve` unless you need the // if necessary. Call this function instead of `resolve` unless you need the
// additional data (transactions and uncles). // additional data (transactions and uncles).
func (b *Block) resolveHeader(ctx context.Context) (*types.Header, error) { func (b *Block) resolveHeader(ctx context.Context) (*types.Header, error) {
if b.num == nil && b.hash == (common.Hash{}) {
return nil, BlockInvariantError
}
if b.header == nil { if b.header == nil {
if _, err := b.resolve(ctx); err != nil { if _, err := b.resolve(ctx); err != nil {
return nil, err return nil, err
@ -447,15 +487,18 @@ func (b *Block) Parent(ctx context.Context) (*Block, error) {
if b.header != nil && b.block.NumberU64() > 0 { if b.header != nil && b.block.NumberU64() > 0 {
num := rpc.BlockNumber(b.header.Number.Uint64() - 1) num := rpc.BlockNumber(b.header.Number.Uint64() - 1)
return &Block{ return &Block{
backend: b.backend, backend: b.backend,
num: &num, num: &num,
hash: b.header.ParentHash, hash: b.header.ParentHash,
canonical: unknown,
}, nil }, nil
} else if b.num != nil && *b.num != 0 { }
if b.num != nil && *b.num != 0 {
num := *b.num - 1 num := *b.num - 1
return &Block{ return &Block{
backend: b.backend, backend: b.backend,
num: &num, num: &num,
canonical: isCanonical,
}, nil }, nil
} }
return nil, nil return nil, nil
@ -544,10 +587,11 @@ func (b *Block) Ommers(ctx context.Context) (*[]*Block, error) {
for _, uncle := range block.Uncles() { for _, uncle := range block.Uncles() {
blockNumber := rpc.BlockNumber(uncle.Number.Uint64()) blockNumber := rpc.BlockNumber(uncle.Number.Uint64())
ret = append(ret, &Block{ ret = append(ret, &Block{
backend: b.backend, backend: b.backend,
num: &blockNumber, num: &blockNumber,
hash: uncle.Hash(), hash: uncle.Hash(),
header: uncle, header: uncle,
canonical: notCanonical,
}) })
} }
return &ret, nil return &ret, nil
@ -672,10 +716,11 @@ func (b *Block) OmmerAt(ctx context.Context, args struct{ Index int32 }) (*Block
uncle := uncles[args.Index] uncle := uncles[args.Index]
blockNumber := rpc.BlockNumber(uncle.Number.Uint64()) blockNumber := rpc.BlockNumber(uncle.Number.Uint64())
return &Block{ return &Block{
backend: b.backend, backend: b.backend,
num: &blockNumber, num: &blockNumber,
hash: uncle.Hash(), hash: uncle.Hash(),
header: uncle, header: uncle,
canonical: notCanonical,
}, nil }, nil
} }
@ -744,6 +789,162 @@ func (b *Block) Logs(ctx context.Context, args struct{ Filter BlockFilterCriteri
return runFilter(ctx, b.backend, filter) return runFilter(ctx, b.backend, filter)
} }
func (b *Block) Account(ctx context.Context, args struct {
Address common.Address
}) (*Account, error) {
err := b.onMainChain(ctx)
if err != nil {
return nil, err
}
if b.num == nil {
_, err := b.resolveHeader(ctx)
if err != nil {
return nil, err
}
}
return &Account{
backend: b.backend,
address: args.Address,
blockNumber: *b.num,
}, nil
}
// CallData encapsulates arguments to `call` or `estimateGas`.
// All arguments are optional.
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.
GasPrice *hexutil.Big // The price of each unit of gas, in wei.
Value *hexutil.Big // The value sent along with the call.
Data *hexutil.Bytes // Any data sent with the call.
}
// CallResult encapsulates the result of an invocation of the `call` accessor.
type CallResult struct {
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() hexutil.Uint64 {
return c.gasUsed
}
func (c *CallResult) Status() hexutil.Uint64 {
return c.status
}
func (b *Block) Call(ctx context.Context, args struct {
Data ethapi.CallArgs
}) (*CallResult, error) {
err := b.onMainChain(ctx)
if err != nil {
return nil, err
}
if b.num == nil {
_, err := b.resolveHeader(ctx)
if err != nil {
return nil, err
}
}
result, gas, failed, err := ethapi.DoCall(ctx, b.backend, args.Data, *b.num, vm.Config{}, 5*time.Second)
status := hexutil.Uint64(1)
if failed {
status = 0
}
return &CallResult{
data: hexutil.Bytes(result),
gasUsed: hexutil.Uint64(gas),
status: status,
}, err
}
func (b *Block) EstimateGas(ctx context.Context, args struct {
Data ethapi.CallArgs
}) (hexutil.Uint64, error) {
err := b.onMainChain(ctx)
if err != nil {
return hexutil.Uint64(0), err
}
if b.num == nil {
_, err := b.resolveHeader(ctx)
if err != nil {
return hexutil.Uint64(0), err
}
}
gas, err := ethapi.DoEstimateGas(ctx, b.backend, args.Data, *b.num)
return gas, err
}
type Pending struct {
backend *eth.EthAPIBackend
}
func (p *Pending) TransactionCount(ctx context.Context) (int32, error) {
txs, err := p.backend.GetPoolTransactions()
return int32(len(txs)), err
}
func (p *Pending) Transactions(ctx context.Context) (*[]*Transaction, error) {
txs, err := p.backend.GetPoolTransactions()
if err != nil {
return nil, err
}
ret := make([]*Transaction, 0, len(txs))
for i, tx := range txs {
ret = append(ret, &Transaction{
backend: p.backend,
hash: tx.Hash(),
tx: tx,
index: uint64(i),
})
}
return &ret, nil
}
func (p *Pending) Account(ctx context.Context, args struct {
Address common.Address
}) *Account {
return &Account{
backend: p.backend,
address: args.Address,
blockNumber: rpc.PendingBlockNumber,
}
}
func (p *Pending) Call(ctx context.Context, args struct {
Data ethapi.CallArgs
}) (*CallResult, error) {
result, gas, failed, err := ethapi.DoCall(ctx, p.backend, args.Data, rpc.PendingBlockNumber, vm.Config{}, 5*time.Second)
status := hexutil.Uint64(1)
if failed {
status = 0
}
return &CallResult{
data: hexutil.Bytes(result),
gasUsed: hexutil.Uint64(gas),
status: status,
}, err
}
func (p *Pending) EstimateGas(ctx context.Context, args struct {
Data ethapi.CallArgs
}) (hexutil.Uint64, error) {
return ethapi.DoEstimateGas(ctx, p.backend, args.Data, rpc.PendingBlockNumber)
}
// Resolver is the top-level object in the GraphQL hierarchy. // Resolver is the top-level object in the GraphQL hierarchy.
type Resolver struct { type Resolver struct {
backend *eth.EthAPIBackend backend *eth.EthAPIBackend
@ -757,19 +958,22 @@ func (r *Resolver) Block(ctx context.Context, args struct {
if args.Number != nil { if args.Number != nil {
num := rpc.BlockNumber(uint64(*args.Number)) num := rpc.BlockNumber(uint64(*args.Number))
block = &Block{ block = &Block{
backend: r.backend, backend: r.backend,
num: &num, num: &num,
canonical: isCanonical,
} }
} else if args.Hash != nil { } else if args.Hash != nil {
block = &Block{ block = &Block{
backend: r.backend, backend: r.backend,
hash: *args.Hash, hash: *args.Hash,
canonical: unknown,
} }
} else { } else {
num := rpc.LatestBlockNumber num := rpc.LatestBlockNumber
block = &Block{ block = &Block{
backend: r.backend, backend: r.backend,
num: &num, num: &num,
canonical: isCanonical,
} }
} }
@ -804,27 +1008,16 @@ func (r *Resolver) Blocks(ctx context.Context, args struct {
for i := from; i <= to; i++ { for i := from; i <= to; i++ {
num := i num := i
ret = append(ret, &Block{ ret = append(ret, &Block{
backend: r.backend, backend: r.backend,
num: &num, num: &num,
canonical: isCanonical,
}) })
} }
return ret, nil return ret, nil
} }
func (r *Resolver) Account(ctx context.Context, args struct { func (r *Resolver) Pending(ctx context.Context) *Pending {
Address common.Address return &Pending{r.backend}
BlockNumber *hexutil.Uint64
}) *Account {
blockNumber := rpc.LatestBlockNumber
if args.BlockNumber != nil {
blockNumber = rpc.BlockNumber(*args.BlockNumber)
}
return &Account{
backend: r.backend,
address: args.Address,
blockNumber: blockNumber,
}
} }
func (r *Resolver) Transaction(ctx context.Context, args struct{ Hash common.Hash }) (*Transaction, error) { func (r *Resolver) Transaction(ctx context.Context, args struct{ Hash common.Hash }) (*Transaction, error) {
@ -852,70 +1045,6 @@ func (r *Resolver) SendRawTransaction(ctx context.Context, args struct{ Data hex
return hash, err return hash, err
} }
// CallData encapsulates arguments to `call` or `estimateGas`.
// All arguments are optional.
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.
GasPrice *hexutil.Big // The price of each unit of gas, in wei.
Value *hexutil.Big // The value sent along with the call.
Data *hexutil.Bytes // Any data sent with the call.
}
// CallResult encapsulates the result of an invocation of the `call` accessor.
type CallResult struct {
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() hexutil.Uint64 {
return c.gasUsed
}
func (c *CallResult) Status() hexutil.Uint64 {
return c.status
}
func (r *Resolver) Call(ctx context.Context, args struct {
Data ethapi.CallArgs
BlockNumber *hexutil.Uint64
}) (*CallResult, error) {
blockNumber := rpc.LatestBlockNumber
if args.BlockNumber != nil {
blockNumber = rpc.BlockNumber(*args.BlockNumber)
}
result, gas, failed, err := ethapi.DoCall(ctx, r.backend, args.Data, blockNumber, vm.Config{}, 5*time.Second)
status := hexutil.Uint64(1)
if failed {
status = 0
}
return &CallResult{
data: hexutil.Bytes(result),
gasUsed: hexutil.Uint64(gas),
status: status,
}, err
}
func (r *Resolver) EstimateGas(ctx context.Context, args struct {
Data ethapi.CallArgs
BlockNumber *hexutil.Uint64
}) (hexutil.Uint64, error) {
blockNumber := rpc.LatestBlockNumber
if args.BlockNumber != nil {
blockNumber = rpc.BlockNumber(*args.BlockNumber)
}
gas, err := ethapi.DoEstimateGas(ctx, r.backend, args.Data, blockNumber)
return gas, err
}
// FilterCriteria encapsulates the arguments to `logs` on the root resolver object. // FilterCriteria encapsulates the arguments to `logs` on the root resolver object.
type FilterCriteria struct { type FilterCriteria struct {
FromBlock *hexutil.Uint64 // beginning of the queried range, nil means genesis block FromBlock *hexutil.Uint64 // beginning of the queried range, nil means genesis block

View File

@ -22,6 +22,7 @@ const schema string = `
# Address is a 20 byte Ethereum address, represented as 0x-prefixed hexadecimal. # Address is a 20 byte Ethereum address, represented as 0x-prefixed hexadecimal.
scalar Address scalar Address
# Bytes is an arbitrary length binary string, represented as 0x-prefixed hexadecimal. # Bytes is an arbitrary length binary string, represented as 0x-prefixed hexadecimal.
# An empty byte string is represented as '0x'. Byte strings must have an even number of hexadecimal nybbles.
scalar Bytes scalar Bytes
# BigInt is a large integer. Input is accepted as either a JSON number or as a string. # BigInt is a large 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 # Strings may be either decimal or 0x-prefixed hexadecimal. Output values are all
@ -75,7 +76,7 @@ const schema string = `
# Nonce is the nonce of the account this transaction was generated with. # Nonce is the nonce of the account this transaction was generated with.
nonce: Long! nonce: Long!
# Index is the index of this transaction in the parent block. This will # Index is the index of this transaction in the parent block. This will
# be null if the transaction has not yet beenn mined. # be null if the transaction has not yet been mined.
index: Int index: Int
# From is the account that sent this transaction - this will always be # From is the account that sent this transaction - this will always be
# an externally owned account. # an externally owned account.
@ -123,16 +124,16 @@ const schema string = `
# empty, results will not be filtered by address. # empty, results will not be filtered by address.
addresses: [Address!] addresses: [Address!]
# Topics list restricts matches to particular event topics. Each event has a list # Topics list restricts matches to particular event topics. Each event has a list
# of topics. Topics matches a prefix of that list. An empty element array matches any # of topics. Topics matches a prefix of that list. An empty element array matches any
# topic. Non-empty elements represent an alternative that matches any of the # topic. Non-empty elements represent an alternative that matches any of the
# contained topics. # contained topics.
# #
# Examples: # Examples:
# - [] or nil matches any topic list # - [] or nil matches any topic list
# - [[A]] matches topic A in first position # - [[A]] matches topic A in first position
# - [[], [B]] matches any topic in first position, B in second position # - [[], [B]] matches any topic in first position, B in second position
# - [[A], [B]] matches topic A in first position, B in second position # - [[A], [B]] matches topic A in first position, B in second position
# - [[A, B]], [C, D]] matches topic (A OR B) in first position, (C OR D) in second position # - [[A, B]], [C, D]] matches topic (A OR B) in first position, (C OR D) in second position
topics: [[Bytes32!]!] topics: [[Bytes32!]!]
} }
@ -198,6 +199,13 @@ const schema string = `
transactionAt(index: Int!): Transaction transactionAt(index: Int!): Transaction
# Logs returns a filtered set of logs from this block. # Logs returns a filtered set of logs from this block.
logs(filter: BlockFilterCriteria!): [Log!]! logs(filter: BlockFilterCriteria!): [Log!]!
# Account fetches an Ethereum account at the current block's state.
account(address: Address!): Account!
# 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
# successful execution of a transaction at the current block's state.
estimateGas(data: CallData!): Long!
} }
# CallData represents the data associated with a local contract call. # CallData represents the data associated with a local contract call.
@ -217,7 +225,7 @@ const schema string = `
data: Bytes data: Bytes
} }
# CallResult is the result of a local call operationn. # CallResult is the result of a local call operation.
type CallResult { type CallResult {
# Data is the return data of the called contract. # Data is the return data of the called contract.
data: Bytes! data: Bytes!
@ -239,16 +247,16 @@ const schema string = `
# empty, results will not be filtered by address. # empty, results will not be filtered by address.
addresses: [Address!] addresses: [Address!]
# Topics list restricts matches to particular event topics. Each event has a list # Topics list restricts matches to particular event topics. Each event has a list
# of topics. Topics matches a prefix of that list. An empty element array matches any # of topics. Topics matches a prefix of that list. An empty element array matches any
# topic. Non-empty elements represent an alternative that matches any of the # topic. Non-empty elements represent an alternative that matches any of the
# contained topics. # contained topics.
# #
# Examples: # Examples:
# - [] or nil matches any topic list # - [] or nil matches any topic list
# - [[A]] matches topic A in first position # - [[A]] matches topic A in first position
# - [[], [B]] matches any topic in first position, B in second position # - [[], [B]] matches any topic in first position, B in second position
# - [[A], [B]] matches topic A in first position, B in second position # - [[A], [B]] matches topic A in first position, B in second position
# - [[A, B]], [C, D]] matches topic (A OR B) in first position, (C OR D) in second position # - [[A, B]], [C, D]] matches topic (A OR B) in first position, (C OR D) in second position
topics: [[Bytes32!]!] topics: [[Bytes32!]!]
} }
@ -268,25 +276,32 @@ const schema string = `
knownStates: Long knownStates: Long
} }
# Pending represents the current pending state.
type Pending {
# TransactionCount is the number of transactions in the pending state.
transactionCount: Int!
# Transactions is a list of transactions in the current pending state.
transactions: [Transaction!]
# Account fetches an Ethereum account for the pending state.
account(address: Address!): Account!
# Call executes a local call operation for the pending state.
call(data: CallData!): CallResult
# EstimateGas estimates the amount of gas that will be required for
# successful execution of a transaction for the pending state.
estimateGas(data: CallData!): Long!
}
type Query { type Query {
# Account fetches an Ethereum account at the specified block number.
# If blockNumber is not provided, it defaults to the most recent block.
account(address: Address!, blockNumber: Long): Account!
# Block fetches an Ethereum block by number or by hash. If neither is # Block fetches an Ethereum block by number or by hash. If neither is
# supplied, the most recent known block is returned. # supplied, the most recent known block is returned.
block(number: Long, hash: Bytes32): Block block(number: Long, hash: Bytes32): Block
# Blocks returns all the blocks between two numbers, inclusive. If # Blocks returns all the blocks between two numbers, inclusive. If
# to is not supplied, it defaults to the most recent known block. # to is not supplied, it defaults to the most recent known block.
blocks(from: Long!, to: Long): [Block!]! blocks(from: Long!, to: Long): [Block!]!
# Pending returns the current pending state.
pending: Pending!
# Transaction returns a transaction specified by its hash. # Transaction returns a transaction specified by its hash.
transaction(hash: Bytes32!): Transaction transaction(hash: Bytes32!): Transaction
# Call executes a local call operation. If blockNumber is not specified,
# it defaults to the most recent known block.
call(data: CallData!, blockNumber: Long): CallResult
# EstimateGas estimates the amount of gas that will be required for
# successful execution of a transaction. If blockNumber is not specified,
# it defaults ot the most recent known block.
estimateGas(data: CallData!, blockNumber: Long): Long!
# Logs returns log entries matching the provided filter. # Logs returns log entries matching the provided filter.
logs(filter: FilterCriteria!): [Log!]! logs(filter: FilterCriteria!): [Log!]!
# GasPrice returns the node's estimate of a gas price sufficient to # GasPrice returns the node's estimate of a gas price sufficient to