diff --git a/graphql/graphql.go b/graphql/graphql.go index 68d36f9977..b16f93e463 100644 --- a/graphql/graphql.go +++ b/graphql/graphql.go @@ -19,6 +19,7 @@ package graphql import ( "context" + "errors" "fmt" "net" "net/http" @@ -43,6 +44,9 @@ import ( "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. type Account struct { backend *eth.EthAPIBackend @@ -144,8 +148,9 @@ func (t *Transaction) resolve(ctx context.Context) (*types.Transaction, error) { if tx != nil { t.tx = tx t.block = &Block{ - backend: t.backend, - hash: blockHash, + backend: t.backend, + hash: blockHash, + canonical: unknown, } t.index = index } else { @@ -332,16 +337,47 @@ func (t *Transaction) Logs(ctx context.Context) (*[]*Log, error) { 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 // when required. type Block struct { - backend *eth.EthAPIBackend - num *rpc.BlockNumber - hash common.Hash - header *types.Header - block *types.Block - receipts []*types.Receipt + backend *eth.EthAPIBackend + num *rpc.BlockNumber + hash common.Hash + header *types.Header + block *types.Block + 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 @@ -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 // additional data (transactions and uncles). 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 _, err := b.resolve(ctx); err != nil { return nil, err @@ -447,15 +487,18 @@ func (b *Block) Parent(ctx context.Context) (*Block, error) { if b.header != nil && b.block.NumberU64() > 0 { num := rpc.BlockNumber(b.header.Number.Uint64() - 1) return &Block{ - backend: b.backend, - num: &num, - hash: b.header.ParentHash, + backend: b.backend, + num: &num, + hash: b.header.ParentHash, + canonical: unknown, }, nil - } else if b.num != nil && *b.num != 0 { + } + if b.num != nil && *b.num != 0 { num := *b.num - 1 return &Block{ - backend: b.backend, - num: &num, + backend: b.backend, + num: &num, + canonical: isCanonical, }, nil } return nil, nil @@ -544,10 +587,11 @@ func (b *Block) Ommers(ctx context.Context) (*[]*Block, error) { for _, uncle := range block.Uncles() { blockNumber := rpc.BlockNumber(uncle.Number.Uint64()) ret = append(ret, &Block{ - backend: b.backend, - num: &blockNumber, - hash: uncle.Hash(), - header: uncle, + backend: b.backend, + num: &blockNumber, + hash: uncle.Hash(), + header: uncle, + canonical: notCanonical, }) } return &ret, nil @@ -672,10 +716,11 @@ func (b *Block) OmmerAt(ctx context.Context, args struct{ Index int32 }) (*Block uncle := uncles[args.Index] blockNumber := rpc.BlockNumber(uncle.Number.Uint64()) return &Block{ - backend: b.backend, - num: &blockNumber, - hash: uncle.Hash(), - header: uncle, + backend: b.backend, + num: &blockNumber, + hash: uncle.Hash(), + header: uncle, + canonical: notCanonical, }, nil } @@ -744,6 +789,162 @@ func (b *Block) Logs(ctx context.Context, args struct{ Filter BlockFilterCriteri 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. type Resolver struct { backend *eth.EthAPIBackend @@ -757,19 +958,22 @@ func (r *Resolver) Block(ctx context.Context, args struct { if args.Number != nil { num := rpc.BlockNumber(uint64(*args.Number)) block = &Block{ - backend: r.backend, - num: &num, + backend: r.backend, + num: &num, + canonical: isCanonical, } } else if args.Hash != nil { block = &Block{ - backend: r.backend, - hash: *args.Hash, + backend: r.backend, + hash: *args.Hash, + canonical: unknown, } } else { num := rpc.LatestBlockNumber block = &Block{ - backend: r.backend, - num: &num, + backend: r.backend, + num: &num, + canonical: isCanonical, } } @@ -804,27 +1008,16 @@ func (r *Resolver) Blocks(ctx context.Context, args struct { for i := from; i <= to; i++ { num := i ret = append(ret, &Block{ - backend: r.backend, - num: &num, + backend: r.backend, + num: &num, + canonical: isCanonical, }) } return ret, nil } -func (r *Resolver) Account(ctx context.Context, args struct { - Address common.Address - 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) Pending(ctx context.Context) *Pending { + return &Pending{r.backend} } 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 } -// 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. type FilterCriteria struct { FromBlock *hexutil.Uint64 // beginning of the queried range, nil means genesis block diff --git a/graphql/schema.go b/graphql/schema.go index c1ba87d2d6..e266e429e1 100644 --- a/graphql/schema.go +++ b/graphql/schema.go @@ -22,6 +22,7 @@ const schema string = ` # Address is a 20 byte Ethereum address, represented as 0x-prefixed hexadecimal. scalar Address # 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 # 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 @@ -75,7 +76,7 @@ const schema string = ` # Nonce is the nonce of the account this transaction was generated with. nonce: Long! # 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 # From is the account that sent this transaction - this will always be # an externally owned account. @@ -123,16 +124,16 @@ const schema string = ` # empty, results will not be filtered by address. addresses: [Address!] # 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 - # topic. Non-empty elements represent an alternative that matches any of the - # contained topics. - # - # Examples: - # - [] or nil matches any topic list - # - [[A]] matches topic A in first 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]], [C, D]] matches topic (A OR B) in first position, (C OR D) in second position + # 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 + # contained topics. + # + # Examples: + # - [] or nil matches any topic list + # - [[A]] matches topic A in first 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]], [C, D]] matches topic (A OR B) in first position, (C OR D) in second position topics: [[Bytes32!]!] } @@ -198,6 +199,13 @@ const schema string = ` transactionAt(index: Int!): 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. + 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. @@ -217,7 +225,7 @@ const schema string = ` data: Bytes } - # CallResult is the result of a local call operationn. + # CallResult is the result of a local call operation. type CallResult { # Data is the return data of the called contract. data: Bytes! @@ -239,16 +247,16 @@ const schema string = ` # empty, results will not be filtered by address. addresses: [Address!] # 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 - # topic. Non-empty elements represent an alternative that matches any of the - # contained topics. - # - # Examples: - # - [] or nil matches any topic list - # - [[A]] matches topic A in first 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]], [C, D]] matches topic (A OR B) in first position, (C OR D) in second position + # 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 + # contained topics. + # + # Examples: + # - [] or nil matches any topic list + # - [[A]] matches topic A in first 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]], [C, D]] matches topic (A OR B) in first position, (C OR D) in second position topics: [[Bytes32!]!] } @@ -268,25 +276,32 @@ const schema string = ` 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 { - # 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 # supplied, the most recent known block is returned. block(number: Long, hash: Bytes32): Block # Blocks returns all the blocks between two numbers, inclusive. If # to is not supplied, it defaults to the most recent known block. blocks(from: Long!, to: Long): [Block!]! + # Pending returns the current pending state. + pending: Pending! # Transaction returns a transaction specified by its hash. 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(filter: FilterCriteria!): [Log!]! # GasPrice returns the node's estimate of a gas price sufficient to