From 4b53623cd3507cdc2e51d8c9589201842738e353 Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Sat, 13 Oct 2018 11:27:19 +0100 Subject: [PATCH 01/40] Initial work on a graphql API --- cmd/geth/config.go | 8 + cmd/geth/main.go | 3 + cmd/utils/flags.go | 28 + ethgraphql/graphiql.go | 73 ++ ethgraphql/main.go | 551 +++++++++++ node/config.go | 18 + node/defaults.go | 10 +- rpc/http.go | 4 +- .../graph-gophers/graphql-go/Gopkg.lock | 25 + .../graph-gophers/graphql-go/Gopkg.toml | 10 + .../graph-gophers/graphql-go/LICENSE | 24 + .../graph-gophers/graphql-go/README.md | 100 ++ .../graph-gophers/graphql-go/errors/errors.go | 41 + .../graph-gophers/graphql-go/graphql.go | 205 ++++ .../github.com/graph-gophers/graphql-go/id.go | 30 + .../graphql-go/internal/common/directive.go | 32 + .../graphql-go/internal/common/lexer.go | 161 ++++ .../graphql-go/internal/common/literals.go | 206 ++++ .../graphql-go/internal/common/types.go | 80 ++ .../graphql-go/internal/common/values.go | 78 ++ .../graphql-go/internal/exec/exec.go | 305 ++++++ .../graphql-go/internal/exec/packer/packer.go | 371 +++++++ .../internal/exec/resolvable/meta.go | 58 ++ .../internal/exec/resolvable/resolvable.go | 331 +++++++ .../internal/exec/selected/selected.go | 238 +++++ .../graphql-go/internal/query/query.go | 234 +++++ .../graphql-go/internal/schema/meta.go | 190 ++++ .../graphql-go/internal/schema/schema.go | 570 +++++++++++ .../internal/validation/suggestion.go | 71 ++ .../internal/validation/validation.go | 909 ++++++++++++++++++ .../graph-gophers/graphql-go/introspection.go | 117 +++ .../graphql-go/introspection/introspection.go | 313 ++++++ .../graph-gophers/graphql-go/log/log.go | 23 + .../graph-gophers/graphql-go/relay/relay.go | 70 ++ .../graph-gophers/graphql-go/time.go | 51 + .../graph-gophers/graphql-go/trace/trace.go | 80 ++ .../graphql-go/trace/validation_trace.go | 17 + 37 files changed, 5629 insertions(+), 6 deletions(-) create mode 100644 ethgraphql/graphiql.go create mode 100644 ethgraphql/main.go create mode 100644 vendor/github.com/graph-gophers/graphql-go/Gopkg.lock create mode 100644 vendor/github.com/graph-gophers/graphql-go/Gopkg.toml create mode 100644 vendor/github.com/graph-gophers/graphql-go/LICENSE create mode 100644 vendor/github.com/graph-gophers/graphql-go/README.md create mode 100644 vendor/github.com/graph-gophers/graphql-go/errors/errors.go create mode 100644 vendor/github.com/graph-gophers/graphql-go/graphql.go create mode 100644 vendor/github.com/graph-gophers/graphql-go/id.go create mode 100644 vendor/github.com/graph-gophers/graphql-go/internal/common/directive.go create mode 100644 vendor/github.com/graph-gophers/graphql-go/internal/common/lexer.go create mode 100644 vendor/github.com/graph-gophers/graphql-go/internal/common/literals.go create mode 100644 vendor/github.com/graph-gophers/graphql-go/internal/common/types.go create mode 100644 vendor/github.com/graph-gophers/graphql-go/internal/common/values.go create mode 100644 vendor/github.com/graph-gophers/graphql-go/internal/exec/exec.go create mode 100644 vendor/github.com/graph-gophers/graphql-go/internal/exec/packer/packer.go create mode 100644 vendor/github.com/graph-gophers/graphql-go/internal/exec/resolvable/meta.go create mode 100644 vendor/github.com/graph-gophers/graphql-go/internal/exec/resolvable/resolvable.go create mode 100644 vendor/github.com/graph-gophers/graphql-go/internal/exec/selected/selected.go create mode 100644 vendor/github.com/graph-gophers/graphql-go/internal/query/query.go create mode 100644 vendor/github.com/graph-gophers/graphql-go/internal/schema/meta.go create mode 100644 vendor/github.com/graph-gophers/graphql-go/internal/schema/schema.go create mode 100644 vendor/github.com/graph-gophers/graphql-go/internal/validation/suggestion.go create mode 100644 vendor/github.com/graph-gophers/graphql-go/internal/validation/validation.go create mode 100644 vendor/github.com/graph-gophers/graphql-go/introspection.go create mode 100644 vendor/github.com/graph-gophers/graphql-go/introspection/introspection.go create mode 100644 vendor/github.com/graph-gophers/graphql-go/log/log.go create mode 100644 vendor/github.com/graph-gophers/graphql-go/relay/relay.go create mode 100644 vendor/github.com/graph-gophers/graphql-go/time.go create mode 100644 vendor/github.com/graph-gophers/graphql-go/trace/trace.go create mode 100644 vendor/github.com/graph-gophers/graphql-go/trace/validation_trace.go diff --git a/cmd/geth/config.go b/cmd/geth/config.go index b0749d23291b..aff1530ff4ef 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -30,6 +30,7 @@ import ( "github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/dashboard" "github.com/ethereum/go-ethereum/eth" + "github.com/ethereum/go-ethereum/ethgraphql" "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/params" whisper "github.com/ethereum/go-ethereum/whisper/whisperv6" @@ -174,6 +175,13 @@ func makeFullNode(ctx *cli.Context) *node.Node { utils.RegisterShhService(stack, &cfg.Shh) } + // Configure GraphQL if required + if ctx.GlobalIsSet(utils.GraphQLEnabledFlag.Name) { + if err := ethgraphql.RegisterGraphQLService(stack, cfg.Node.GraphQLEndpoint(), cfg.Node.HTTPCors, cfg.Node.HTTPVirtualHosts, cfg.Node.HTTPTimeouts); err != nil { + utils.Fatalf("Failed to register the Ethereum service: %v", err) + } + } + // Add the Ethereum Stats daemon if requested. if cfg.Ethstats.URL != "" { utils.RegisterEthStatsService(stack, cfg.Ethstats.URL) diff --git a/cmd/geth/main.go b/cmd/geth/main.go index fae4b5718191..24c80739a83d 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -139,6 +139,9 @@ var ( utils.RPCEnabledFlag, utils.RPCListenAddrFlag, utils.RPCPortFlag, + utils.GraphQLEnabledFlag, + utils.GraphQLListenAddrFlag, + utils.GraphQLPortFlag, utils.RPCApiFlag, utils.WSEnabledFlag, utils.WSListenAddrFlag, diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 429c2bbb9bba..361f8be41495 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -422,6 +422,20 @@ var ( Usage: "HTTP-RPC server listening port", Value: node.DefaultHTTPPort, } + GraphQLEnabledFlag = cli.BoolFlag{ + Name: "graphql", + Usage: "Enable the GraphQL server", + } + GraphQLListenAddrFlag = cli.StringFlag{ + Name: "graphqladdr", + Usage: "GraphQL server listening interface", + Value: node.DefaultGraphQLHost, + } + GraphQLPortFlag = cli.IntFlag{ + Name: "graphqlport", + Usage: "GraphQL server listening port", + Value: node.DefaultGraphQLPort, + } RPCCORSDomainFlag = cli.StringFlag{ Name: "rpccorsdomain", Usage: "Comma separated list of domains from which to accept cross origin requests (browser enforced)", @@ -783,6 +797,19 @@ func setHTTP(ctx *cli.Context, cfg *node.Config) { } } +// setGraphQL creates the GraphQL listener interface string from the set +// command line flags, returning empty if the GraphQL endpoint is disabled. +func setGraphQL(ctx *cli.Context, cfg *node.Config) { + if ctx.GlobalBool(GraphQLEnabledFlag.Name) && cfg.GraphQLHost == "" { + cfg.GraphQLHost = "127.0.0.1" + if ctx.GlobalIsSet(GraphQLListenAddrFlag.Name) { + cfg.GraphQLHost = ctx.GlobalString(GraphQLListenAddrFlag.Name) + } + } + + cfg.GraphQLPort = ctx.GlobalInt(GraphQLPortFlag.Name) +} + // setWS creates the WebSocket RPC listener interface string from the set // command line flags, returning empty if the HTTP endpoint is disabled. func setWS(ctx *cli.Context, cfg *node.Config) { @@ -970,6 +997,7 @@ func SetNodeConfig(ctx *cli.Context, cfg *node.Config) { SetP2PConfig(ctx, &cfg.P2P) setIPC(ctx, cfg) setHTTP(ctx, cfg) + setGraphQL(ctx, cfg) setWS(ctx, cfg) setNodeUserIdent(ctx, cfg) diff --git a/ethgraphql/graphiql.go b/ethgraphql/graphiql.go new file mode 100644 index 000000000000..42bb7920fd27 --- /dev/null +++ b/ethgraphql/graphiql.go @@ -0,0 +1,73 @@ +package ethgraphql + +import ( + "bytes" + "fmt" + "net/http" +) + +// GraphiQL is an in-browser IDE for exploring GraphiQL APIs. +// This handler returns GraphiQL when requested. +// +// For more information, see https://github.com/graphql/graphiql. +type GraphiQL struct{} + +func respond(w http.ResponseWriter, body []byte, code int) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.WriteHeader(code) + _, _ = w.Write(body) +} + +func errorJSON(msg string) []byte { + buf := bytes.Buffer{} + fmt.Fprintf(&buf, `{"error": "%s"}`, msg) + return buf.Bytes() +} + +func (h GraphiQL) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + respond(w, errorJSON("only GET requests are supported"), http.StatusMethodNotAllowed) + return + } + + w.Write(graphiql) +} + +var graphiql = []byte(` + + + + + + + + + + +
Loading...
+ + + +`) diff --git a/ethgraphql/main.go b/ethgraphql/main.go new file mode 100644 index 000000000000..e4506db9566c --- /dev/null +++ b/ethgraphql/main.go @@ -0,0 +1,551 @@ +// Copyright 2018 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library 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. +// +// The go-ethereum library 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 the go-ethereum library. If not, see . + +package ethgraphql + +import ( + "context" + "fmt" + "math/big" + "net" + "net/http" + "strconv" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/eth" + "github.com/ethereum/go-ethereum/internal/ethapi" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/rpc" + graphql "github.com/graph-gophers/graphql-go" + "github.com/graph-gophers/graphql-go/relay" +) + +func getBackend(n *node.Node) (ethapi.Backend, error) { + var ethereum *eth.Ethereum + if err := n.Service(ðereum); err != nil { + return nil, err + } + return ethereum.APIBackend, nil +} + +type HexBytes struct { + hexutil.Bytes +} + +func (h HexBytes) ImplementsGraphQLType(name string) bool { return name == "HexBytes" } + +func (h *HexBytes) UnmarshalGraphQL(input interface{}) error { + var err error + switch input := input.(type) { + case string: + data, err := hexutil.Decode(input) + if err != nil { + return err + } + *h = HexBytes{data} + default: + err = fmt.Errorf("Unexpected type for Hash: %v", input) + } + return err +} + +type BigNum struct { + *big.Int +} + +func (bn BigNum) ImplementsGraphQLType(name string) bool { return name == "BigNum" } + +func (bn *BigNum) UnmarshalGraphQL(input interface{}) error { + var err error + switch input := input.(type) { + case string: + i := big.NewInt(0) + i.SetString(input, 10) + *bn = BigNum{i} + case int32: + *bn = BigNum{big.NewInt(int64(input))} + default: + err = fmt.Errorf("Unexpected type for Hash: %v", input) + } + return err +} + +func (bn BigNum) MarshalJSON() ([]byte, error) { + return strconv.AppendQuote(nil, bn.Text(10)), nil +} + +type Account struct { + node *node.Node + address common.Address + blockNumber rpc.BlockNumber +} + +func (a *Account) getState(ctx context.Context) (*state.StateDB, error) { + be, err := getBackend(a.node) + if err != nil { + return nil, err + } + + state, _, err := be.StateAndHeaderByNumber(ctx, a.blockNumber) + return state, err +} + +func (a *Account) Address(ctx context.Context) (HexBytes, error) { + return HexBytes{a.address.Bytes()}, nil +} + +func (a *Account) Balance(ctx context.Context) (BigNum, error) { + state, err := a.getState(ctx) + if err != nil { + return BigNum{}, err + } + + return BigNum{state.GetBalance(a.address)}, nil +} + +func (a *Account) Nonce(ctx context.Context) (int32, error) { + state, err := a.getState(ctx) + if err != nil { + return 0, err + } + + return int32(state.GetNonce(a.address)), nil +} + +func (a *Account) Code(ctx context.Context) (HexBytes, error) { + state, err := a.getState(ctx) + if err != nil { + return HexBytes{}, err + } + + return HexBytes{state.GetCode(a.address)}, nil +} + +type StorageSlotArgs struct { + Slot HexBytes +} + +func (a *Account) Storage(ctx context.Context, args StorageSlotArgs) (HexBytes, error) { + state, err := a.getState(ctx) + if err != nil { + return HexBytes{}, err + } + + return HexBytes{state.GetState(a.address, common.BytesToHash(args.Slot.Bytes)).Bytes()}, nil +} + +type Transaction struct { + node *node.Node + hash common.Hash + tx *types.Transaction +} + +func (tx *Transaction) Hash(ctx context.Context) HexBytes { + return HexBytes{tx.hash.Bytes()} +} + +type Block struct { + node *node.Node + num *rpc.BlockNumber + hash common.Hash + block *types.Block +} + +func (b *Block) resolve(ctx context.Context) (*types.Block, error) { + if b.block != nil { + return b.block, nil + } + + be, err := getBackend(b.node) + if err != nil { + return nil, err + } + + if b.num != nil { + b.block, err = be.BlockByNumber(ctx, rpc.BlockNumber(*b.num)) + } else { + b.block, err = be.GetBlock(ctx, b.hash) + } + return b.block, err +} + +func (b *Block) Number(ctx context.Context) (int32, error) { + if b.num == nil || *b.num == rpc.LatestBlockNumber { + block, err := b.resolve(ctx) + if err != nil { + return 0, err + } + num := rpc.BlockNumber(block.Number().Uint64()) + b.num = &num + } + return int32(*b.num), nil +} + +func (b *Block) Hash(ctx context.Context) (HexBytes, error) { + if b.hash == (common.Hash{}) { + block, err := b.resolve(ctx) + if err != nil { + return HexBytes{}, err + } + b.hash = block.Hash() + } + return HexBytes{b.hash.Bytes()}, nil +} + +func (b *Block) GasLimit(ctx context.Context) (int32, error) { + block, err := b.resolve(ctx) + if err != nil { + return 0, err + } + return int32(block.GasLimit()), nil +} + +func (b *Block) GasUsed(ctx context.Context) (int32, error) { + block, err := b.resolve(ctx) + if err != nil { + return 0, err + } + return int32(block.GasUsed()), nil +} + +func (b *Block) Parent(ctx context.Context) (*Block, error) { + // If the block hasn't been fetched, and we'll need it, fetch it. + if b.num == nil && b.hash != (common.Hash{}) && b.block == nil { + if _, err := b.resolve(ctx); err != nil { + return nil, err + } + } + + if b.block != nil && b.block.NumberU64() > 0 { + num := rpc.BlockNumber(b.block.NumberU64() - 1) + return &Block{ + node: b.node, + num: &num, + hash: b.block.ParentHash(), + }, nil + } else if b.num != nil && *b.num != 0 { + num := *b.num - 1 + return &Block{ + node: b.node, + num: &num, + }, nil + } + return nil, nil +} + +func (b *Block) Difficulty(ctx context.Context) (BigNum, error) { + block, err := b.resolve(ctx) + if err != nil { + return BigNum{}, err + } + return BigNum{block.Difficulty()}, nil +} + +func (b *Block) Time(ctx context.Context) (BigNum, error) { + block, err := b.resolve(ctx) + if err != nil { + return BigNum{}, err + } + return BigNum{block.Time()}, nil +} + +func (b *Block) Nonce(ctx context.Context) (BigNum, error) { + block, err := b.resolve(ctx) + if err != nil { + return BigNum{}, err + } + i := new(big.Int) + i.SetUint64(block.Nonce()) + return BigNum{i}, nil +} + +func (b *Block) MixDigest(ctx context.Context) (HexBytes, error) { + block, err := b.resolve(ctx) + if err != nil { + return HexBytes{}, err + } + return HexBytes{block.MixDigest().Bytes()}, nil +} + +func (b *Block) Root(ctx context.Context) (HexBytes, error) { + block, err := b.resolve(ctx) + if err != nil { + return HexBytes{}, err + } + return HexBytes{block.Root().Bytes()}, nil +} + +func (b *Block) TxHash(ctx context.Context) (HexBytes, error) { + block, err := b.resolve(ctx) + if err != nil { + return HexBytes{}, err + } + return HexBytes{block.TxHash().Bytes()}, nil +} + +func (b *Block) ReceiptHash(ctx context.Context) (HexBytes, error) { + block, err := b.resolve(ctx) + if err != nil { + return HexBytes{}, err + } + return HexBytes{block.ReceiptHash().Bytes()}, nil +} + +func (b *Block) UncleHash(ctx context.Context) (HexBytes, error) { + block, err := b.resolve(ctx) + if err != nil { + return HexBytes{}, err + } + return HexBytes{block.UncleHash().Bytes()}, nil +} + +func (b *Block) Extra(ctx context.Context) (HexBytes, error) { + block, err := b.resolve(ctx) + if err != nil { + return HexBytes{}, err + } + return HexBytes{block.Extra()}, nil +} + +func (b *Block) TotalDifficulty(ctx context.Context) (BigNum, error) { + h := b.hash + if h == (common.Hash{}) { + block, err := b.resolve(ctx) + if err != nil { + return BigNum{}, err + } + h = block.Hash() + } + + be, err := getBackend(b.node) + if err != nil { + return BigNum{}, err + } + + return BigNum{be.GetTd(h)}, nil +} + +type BlockNumberArgs struct { + Block *int32 +} + +func (b *Block) Coinbase(ctx context.Context, args BlockNumberArgs) (*Account, error) { + block, err := b.resolve(ctx) + if err != nil { + return nil, err + } + + blockNumber := rpc.LatestBlockNumber + if args.Block != nil { + blockNumber = rpc.BlockNumber(*args.Block) + } + + return &Account{ + node: b.node, + address: block.Coinbase(), + blockNumber: blockNumber, + }, nil +} + +func (b *Block) Transactions(ctx context.Context) ([]*Transaction, error) { + block, err := b.resolve(ctx) + if err != nil { + return nil, err + } + + ret := make([]*Transaction, 0, len(block.Transactions())) + for _, tx := range block.Transactions() { + ret = append(ret, &Transaction{ + node: b.node, + hash: tx.Hash(), + tx: tx, + }) + } + return ret, nil +} + +type Query struct { + node *node.Node +} + +type BlockArgs struct { + Number *int32 + Hash *string +} + +func (q *Query) Block(ctx context.Context, args BlockArgs) *Block { + if args.Number != nil { + num := rpc.BlockNumber(uint64(*args.Number)) + return &Block{ + node: q.node, + num: &num, + } + } else if args.Hash != nil { + return &Block{ + node: q.node, + hash: common.HexToHash(*args.Hash), + } + } else { + num := rpc.LatestBlockNumber + return &Block{ + node: q.node, + num: &num, + } + } +} + +type AccountArgs struct { + Address HexBytes + BlockNumber *int32 +} + +func (q *Query) Account(ctx context.Context, args AccountArgs) *Account { + blockNumber := rpc.LatestBlockNumber + if args.BlockNumber != nil { + blockNumber = rpc.BlockNumber(*args.BlockNumber) + } + + return &Account{ + node: q.node, + address: common.BytesToAddress(args.Address.Bytes), + blockNumber: blockNumber, + } +} + +func NewHandler(n *node.Node) (http.Handler, error) { + q := Query{n} + + s := ` + scalar HexBytes + scalar BigNum + + schema { + query: Query + } + + type Account { + address: HexBytes! + balance: BigNum! + nonce: Int! + code: HexBytes! + storage(slot: HexBytes!): HexBytes! + } + + type Transaction { + hash: HexBytes! + } + + type Block { + number: Int! + hash: HexBytes! + gasLimit: Int! + gasUsed: Int! + parent: Block + difficulty: BigNum! + time: BigNum! + nonce: BigNum! + mixDigest: HexBytes! + root: HexBytes! + txHash: HexBytes! + receiptHash: HexBytes! + uncleHash: HexBytes! + extra: HexBytes! + totalDifficulty: BigNum! + coinbase(block: Int): Account! + transactions: [Transaction]! + } + + type Query { + block(number: Int, hash: String): Block + account(address: HexBytes!, blockNumber: Int): Account + } + ` + schema, err := graphql.ParseSchema(s, &q) + if err != nil { + return nil, err + } + h := &relay.Handler{Schema: schema} + + mux := http.NewServeMux() + mux.Handle("/", GraphiQL{}) + mux.Handle("/graphql", h) + mux.Handle("/graphql/", h) + return mux, nil +} + +type Service struct { + endpoint string + cors []string + vhosts []string + timeouts rpc.HTTPTimeouts + node *node.Node + handler http.Handler + listener net.Listener +} + +func (s *Service) Protocols() []p2p.Protocol { return nil } + +func (s *Service) APIs() []rpc.API { return nil } + +// Start is called after all services have been constructed and the networking +// layer was also initialized to spawn any goroutines required by the service. +func (s *Service) Start(server *p2p.Server) error { + var err error + s.handler, err = NewHandler(s.node) + if err != nil { + return err + } + + if s.listener, err = net.Listen("tcp", s.endpoint); err != nil { + return err + } + + go rpc.NewHTTPServer(s.cors, s.vhosts, s.timeouts, s.handler).Serve(s.listener) + log.Info("GraphQL endpoint opened", "url", fmt.Sprintf("http://%s", s.endpoint)) + return nil +} + +// Stop terminates all goroutines belonging to the service, blocking until they +// are all terminated. +func (s *Service) Stop() error { + if s.listener != nil { + s.listener.Close() + s.listener = nil + log.Info("GraphQL endpoint closed", "url", fmt.Sprintf("http://%s", s.endpoint)) + } + return nil +} + +func NewService(ctx *node.ServiceContext, stack *node.Node, endpoint string, cors, vhosts []string, timeouts rpc.HTTPTimeouts) (*Service, error) { + return &Service{ + endpoint: endpoint, + cors: cors, + vhosts: vhosts, + timeouts: timeouts, + node: stack, + }, nil +} + +func RegisterGraphQLService(stack *node.Node, endpoint string, cors, vhosts []string, timeouts rpc.HTTPTimeouts) error { + return stack.Register(func(ctx *node.ServiceContext) (node.Service, error) { + return NewService(ctx, stack, endpoint, cors, vhosts, timeouts) + }) +} diff --git a/node/config.go b/node/config.go index 8f10f4f613d7..a1a896df89e0 100644 --- a/node/config.go +++ b/node/config.go @@ -101,6 +101,15 @@ type Config struct { // for ephemeral nodes). HTTPPort int `toml:",omitempty"` + // GraphQLHost is the host interface on which to start the GraphQL server. If this + // field is empty, no GraphQL API endpoint will be started. + GraphQLHost string `toml:",omitempty"` + + // GraphQLPort is the TCP port number on which to start the GraphQL server. The + // default zero value is/ valid and will pick a port number randomly (useful + // for ephemeral nodes). + GraphQLPort int `toml:",omitempty"` + // HTTPCors is the Cross-Origin Resource Sharing header to send to requesting // clients. Please be aware that CORS is a browser enforced security, it's fully // useless for custom HTTP clients. @@ -208,6 +217,15 @@ func (c *Config) HTTPEndpoint() string { return fmt.Sprintf("%s:%d", c.HTTPHost, c.HTTPPort) } +// GraphQLEndpoint resolves a GraphQL endpoint based on the configured host interface +// and port parameters. +func (c *Config) GraphQLEndpoint() string { + if c.GraphQLHost == "" { + return "" + } + return fmt.Sprintf("%s:%d", c.GraphQLHost, c.GraphQLPort) +} + // DefaultHTTPEndpoint returns the HTTP endpoint used by default. func DefaultHTTPEndpoint() string { config := &Config{HTTPHost: DefaultHTTPHost, HTTPPort: DefaultHTTPPort} diff --git a/node/defaults.go b/node/defaults.go index c1376dba0f33..cea4997cb421 100644 --- a/node/defaults.go +++ b/node/defaults.go @@ -28,10 +28,12 @@ import ( ) const ( - DefaultHTTPHost = "localhost" // Default host interface for the HTTP RPC server - DefaultHTTPPort = 8545 // Default TCP port for the HTTP RPC server - DefaultWSHost = "localhost" // Default host interface for the websocket RPC server - DefaultWSPort = 8546 // Default TCP port for the websocket RPC server + DefaultHTTPHost = "localhost" // Default host interface for the HTTP RPC server + DefaultHTTPPort = 8545 // Default TCP port for the HTTP RPC server + DefaultWSHost = "localhost" // Default host interface for the websocket RPC server + DefaultWSPort = 8546 // Default TCP port for the websocket RPC server + DefaultGraphQLHost = "localhost" // Default host interface for the GraphQL server + DefaultGraphQLPort = 8547 // Default TCP port for the GraphQL server ) // DefaultConfig contains reasonable default settings. diff --git a/rpc/http.go b/rpc/http.go index af79858e2b78..844d18dced54 100644 --- a/rpc/http.go +++ b/rpc/http.go @@ -194,7 +194,7 @@ func (t *httpReadWriteNopCloser) Close() error { // NewHTTPServer creates a new HTTP RPC server around an API provider. // // Deprecated: Server implements http.Handler -func NewHTTPServer(cors []string, vhosts []string, timeouts HTTPTimeouts, srv *Server) *http.Server { +func NewHTTPServer(cors []string, vhosts []string, timeouts HTTPTimeouts, srv http.Handler) *http.Server { // Wrap the CORS-handler within a host-handler handler := newCorsHandler(srv, cors) handler = newVHostHandler(vhosts, handler) @@ -271,7 +271,7 @@ func validateRequest(r *http.Request) (int, error) { return 0, nil } -func newCorsHandler(srv *Server, allowedOrigins []string) http.Handler { +func newCorsHandler(srv http.Handler, allowedOrigins []string) http.Handler { // disable CORS support if user has not specified a custom CORS configuration if len(allowedOrigins) == 0 { return srv diff --git a/vendor/github.com/graph-gophers/graphql-go/Gopkg.lock b/vendor/github.com/graph-gophers/graphql-go/Gopkg.lock new file mode 100644 index 000000000000..4574275c5d79 --- /dev/null +++ b/vendor/github.com/graph-gophers/graphql-go/Gopkg.lock @@ -0,0 +1,25 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + name = "github.com/opentracing/opentracing-go" + packages = [ + ".", + "ext", + "log" + ] + revision = "1949ddbfd147afd4d964a9f00b24eb291e0e7c38" + version = "v1.0.2" + +[[projects]] + branch = "master" + name = "golang.org/x/net" + packages = ["context"] + revision = "f5dfe339be1d06f81b22525fe34671ee7d2c8904" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + inputs-digest = "f417062128566756a9360b1c13ada79bdeeb6bab1f53ee9147a3328d95c1653f" + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/vendor/github.com/graph-gophers/graphql-go/Gopkg.toml b/vendor/github.com/graph-gophers/graphql-go/Gopkg.toml new file mode 100644 index 000000000000..62b936799851 --- /dev/null +++ b/vendor/github.com/graph-gophers/graphql-go/Gopkg.toml @@ -0,0 +1,10 @@ +# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html +# for detailed Gopkg.toml documentation. + +[[constraint]] + name = "github.com/opentracing/opentracing-go" + version = "1.0.2" + +[prune] + go-tests = true + unused-packages = true diff --git a/vendor/github.com/graph-gophers/graphql-go/LICENSE b/vendor/github.com/graph-gophers/graphql-go/LICENSE new file mode 100644 index 000000000000..3907cecacf28 --- /dev/null +++ b/vendor/github.com/graph-gophers/graphql-go/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2016 Richard Musiol. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/graph-gophers/graphql-go/README.md b/vendor/github.com/graph-gophers/graphql-go/README.md new file mode 100644 index 000000000000..ef4b4639b522 --- /dev/null +++ b/vendor/github.com/graph-gophers/graphql-go/README.md @@ -0,0 +1,100 @@ +# graphql-go [![Sourcegraph](https://sourcegraph.com/github.com/graph-gophers/graphql-go/-/badge.svg)](https://sourcegraph.com/github.com/graph-gophers/graphql-go?badge) [![Build Status](https://semaphoreci.com/api/v1/graph-gophers/graphql-go/branches/master/badge.svg)](https://semaphoreci.com/graph-gophers/graphql-go) [![GoDoc](https://godoc.org/github.com/graph-gophers/graphql-go?status.svg)](https://godoc.org/github.com/graph-gophers/graphql-go) + +

+ +The goal of this project is to provide full support of the [GraphQL draft specification](https://facebook.github.io/graphql/draft) with a set of idiomatic, easy to use Go packages. + +While still under heavy development (`internal` APIs are almost certainly subject to change), this library is +safe for production use. + +## Features + +- minimal API +- support for `context.Context` +- support for the `OpenTracing` standard +- schema type-checking against resolvers +- resolvers are matched to the schema based on method sets (can resolve a GraphQL schema with a Go interface or Go struct). +- handles panics in resolvers +- parallel execution of resolvers + +## Roadmap + +We're trying out the GitHub Project feature to manage `graphql-go`'s [development roadmap](https://github.com/graph-gophers/graphql-go/projects/1). +Feedback is welcome and appreciated. + +## (Some) Documentation + +### Basic Sample + +```go +package main + +import ( + "log" + "net/http" + + graphql "github.com/graph-gophers/graphql-go" + "github.com/graph-gophers/graphql-go/relay" +) + +type query struct{} + +func (_ *query) Hello() string { return "Hello, world!" } + +func main() { + s := ` + schema { + query: Query + } + type Query { + hello: String! + } + ` + schema := graphql.MustParseSchema(s, &query{}) + http.Handle("/query", &relay.Handler{Schema: schema}) + log.Fatal(http.ListenAndServe(":8080", nil)) +} +``` + +To test: +```sh +$ curl -XPOST -d '{"query": "{ hello }"}' localhost:8080/query +``` + +### Resolvers + +A resolver must have one method for each field of the GraphQL type it resolves. The method name has to be [exported](https://golang.org/ref/spec#Exported_identifiers) and match the field's name in a non-case-sensitive way. + +The method has up to two arguments: + +- Optional `context.Context` argument. +- Mandatory `*struct { ... }` argument if the corresponding GraphQL field has arguments. The names of the struct fields have to be [exported](https://golang.org/ref/spec#Exported_identifiers) and have to match the names of the GraphQL arguments in a non-case-sensitive way. + +The method has up to two results: + +- The GraphQL field's value as determined by the resolver. +- Optional `error` result. + +Example for a simple resolver method: + +```go +func (r *helloWorldResolver) Hello() string { + return "Hello world!" +} +``` + +The following signature is also allowed: + +```go +func (r *helloWorldResolver) Hello(ctx context.Context) (string, error) { + return "Hello world!", nil +} +``` + +### Community Examples + +[tonyghita/graphql-go-example](https://github.com/tonyghita/graphql-go-example) - A more "productionized" version of the Star Wars API example given in this repository. + +[deltaskelta/graphql-go-pets-example](https://github.com/deltaskelta/graphql-go-pets-example) - graphql-go resolving against a sqlite database + +[OscarYuen/go-graphql-starter](https://github.com/OscarYuen/go-graphql-starter) - a starter application integrated with dataloader, psql and basic authentication diff --git a/vendor/github.com/graph-gophers/graphql-go/errors/errors.go b/vendor/github.com/graph-gophers/graphql-go/errors/errors.go new file mode 100644 index 000000000000..fdfa62024db1 --- /dev/null +++ b/vendor/github.com/graph-gophers/graphql-go/errors/errors.go @@ -0,0 +1,41 @@ +package errors + +import ( + "fmt" +) + +type QueryError struct { + Message string `json:"message"` + Locations []Location `json:"locations,omitempty"` + Path []interface{} `json:"path,omitempty"` + Rule string `json:"-"` + ResolverError error `json:"-"` +} + +type Location struct { + Line int `json:"line"` + Column int `json:"column"` +} + +func (a Location) Before(b Location) bool { + return a.Line < b.Line || (a.Line == b.Line && a.Column < b.Column) +} + +func Errorf(format string, a ...interface{}) *QueryError { + return &QueryError{ + Message: fmt.Sprintf(format, a...), + } +} + +func (err *QueryError) Error() string { + if err == nil { + return "" + } + str := fmt.Sprintf("graphql: %s", err.Message) + for _, loc := range err.Locations { + str += fmt.Sprintf(" (line %d, column %d)", loc.Line, loc.Column) + } + return str +} + +var _ error = &QueryError{} diff --git a/vendor/github.com/graph-gophers/graphql-go/graphql.go b/vendor/github.com/graph-gophers/graphql-go/graphql.go new file mode 100644 index 000000000000..06ffd4599749 --- /dev/null +++ b/vendor/github.com/graph-gophers/graphql-go/graphql.go @@ -0,0 +1,205 @@ +package graphql + +import ( + "context" + "fmt" + + "encoding/json" + + "github.com/graph-gophers/graphql-go/errors" + "github.com/graph-gophers/graphql-go/internal/common" + "github.com/graph-gophers/graphql-go/internal/exec" + "github.com/graph-gophers/graphql-go/internal/exec/resolvable" + "github.com/graph-gophers/graphql-go/internal/exec/selected" + "github.com/graph-gophers/graphql-go/internal/query" + "github.com/graph-gophers/graphql-go/internal/schema" + "github.com/graph-gophers/graphql-go/internal/validation" + "github.com/graph-gophers/graphql-go/introspection" + "github.com/graph-gophers/graphql-go/log" + "github.com/graph-gophers/graphql-go/trace" +) + +// ParseSchema parses a GraphQL schema and attaches the given root resolver. It returns an error if +// the Go type signature of the resolvers does not match the schema. If nil is passed as the +// resolver, then the schema can not be executed, but it may be inspected (e.g. with ToJSON). +func ParseSchema(schemaString string, resolver interface{}, opts ...SchemaOpt) (*Schema, error) { + s := &Schema{ + schema: schema.New(), + maxParallelism: 10, + tracer: trace.OpenTracingTracer{}, + validationTracer: trace.NoopValidationTracer{}, + logger: &log.DefaultLogger{}, + } + for _, opt := range opts { + opt(s) + } + + if err := s.schema.Parse(schemaString); err != nil { + return nil, err + } + + if resolver != nil { + r, err := resolvable.ApplyResolver(s.schema, resolver) + if err != nil { + return nil, err + } + s.res = r + } + + return s, nil +} + +// MustParseSchema calls ParseSchema and panics on error. +func MustParseSchema(schemaString string, resolver interface{}, opts ...SchemaOpt) *Schema { + s, err := ParseSchema(schemaString, resolver, opts...) + if err != nil { + panic(err) + } + return s +} + +// Schema represents a GraphQL schema with an optional resolver. +type Schema struct { + schema *schema.Schema + res *resolvable.Schema + + maxDepth int + maxParallelism int + tracer trace.Tracer + validationTracer trace.ValidationTracer + logger log.Logger +} + +// SchemaOpt is an option to pass to ParseSchema or MustParseSchema. +type SchemaOpt func(*Schema) + +// MaxDepth specifies the maximum field nesting depth in a query. The default is 0 which disables max depth checking. +func MaxDepth(n int) SchemaOpt { + return func(s *Schema) { + s.maxDepth = n + } +} + +// MaxParallelism specifies the maximum number of resolvers per request allowed to run in parallel. The default is 10. +func MaxParallelism(n int) SchemaOpt { + return func(s *Schema) { + s.maxParallelism = n + } +} + +// Tracer is used to trace queries and fields. It defaults to trace.OpenTracingTracer. +func Tracer(tracer trace.Tracer) SchemaOpt { + return func(s *Schema) { + s.tracer = tracer + } +} + +// ValidationTracer is used to trace validation errors. It defaults to trace.NoopValidationTracer. +func ValidationTracer(tracer trace.ValidationTracer) SchemaOpt { + return func(s *Schema) { + s.validationTracer = tracer + } +} + +// Logger is used to log panics during query execution. It defaults to exec.DefaultLogger. +func Logger(logger log.Logger) SchemaOpt { + return func(s *Schema) { + s.logger = logger + } +} + +// Response represents a typical response of a GraphQL server. It may be encoded to JSON directly or +// it may be further processed to a custom response type, for example to include custom error data. +// Errors are intentionally serialized first based on the advice in https://github.com/facebook/graphql/commit/7b40390d48680b15cb93e02d46ac5eb249689876#diff-757cea6edf0288677a9eea4cfc801d87R107 +type Response struct { + Errors []*errors.QueryError `json:"errors,omitempty"` + Data json.RawMessage `json:"data,omitempty"` + Extensions map[string]interface{} `json:"extensions,omitempty"` +} + +// Validate validates the given query with the schema. +func (s *Schema) Validate(queryString string) []*errors.QueryError { + doc, qErr := query.Parse(queryString) + if qErr != nil { + return []*errors.QueryError{qErr} + } + + return validation.Validate(s.schema, doc, s.maxDepth) +} + +// Exec executes the given query with the schema's resolver. It panics if the schema was created +// without a resolver. If the context get cancelled, no further resolvers will be called and a +// the context error will be returned as soon as possible (not immediately). +func (s *Schema) Exec(ctx context.Context, queryString string, operationName string, variables map[string]interface{}) *Response { + if s.res == nil { + panic("schema created without resolver, can not exec") + } + return s.exec(ctx, queryString, operationName, variables, s.res) +} + +func (s *Schema) exec(ctx context.Context, queryString string, operationName string, variables map[string]interface{}, res *resolvable.Schema) *Response { + doc, qErr := query.Parse(queryString) + if qErr != nil { + return &Response{Errors: []*errors.QueryError{qErr}} + } + + validationFinish := s.validationTracer.TraceValidation() + errs := validation.Validate(s.schema, doc, s.maxDepth) + validationFinish(errs) + if len(errs) != 0 { + return &Response{Errors: errs} + } + + op, err := getOperation(doc, operationName) + if err != nil { + return &Response{Errors: []*errors.QueryError{errors.Errorf("%s", err)}} + } + + r := &exec.Request{ + Request: selected.Request{ + Doc: doc, + Vars: variables, + Schema: s.schema, + }, + Limiter: make(chan struct{}, s.maxParallelism), + Tracer: s.tracer, + Logger: s.logger, + } + varTypes := make(map[string]*introspection.Type) + for _, v := range op.Vars { + t, err := common.ResolveType(v.Type, s.schema.Resolve) + if err != nil { + return &Response{Errors: []*errors.QueryError{err}} + } + varTypes[v.Name.Name] = introspection.WrapType(t) + } + traceCtx, finish := s.tracer.TraceQuery(ctx, queryString, operationName, variables, varTypes) + data, errs := r.Execute(traceCtx, res, op) + finish(errs) + + return &Response{ + Data: data, + Errors: errs, + } +} + +func getOperation(document *query.Document, operationName string) (*query.Operation, error) { + if len(document.Operations) == 0 { + return nil, fmt.Errorf("no operations in query document") + } + + if operationName == "" { + if len(document.Operations) > 1 { + return nil, fmt.Errorf("more than one operation in query document and no operation name given") + } + for _, op := range document.Operations { + return op, nil // return the one and only operation + } + } + + op := document.Operations.Get(operationName) + if op == nil { + return nil, fmt.Errorf("no operation with name %q", operationName) + } + return op, nil +} diff --git a/vendor/github.com/graph-gophers/graphql-go/id.go b/vendor/github.com/graph-gophers/graphql-go/id.go new file mode 100644 index 000000000000..52771c413b03 --- /dev/null +++ b/vendor/github.com/graph-gophers/graphql-go/id.go @@ -0,0 +1,30 @@ +package graphql + +import ( + "errors" + "strconv" +) + +// ID represents GraphQL's "ID" scalar type. A custom type may be used instead. +type ID string + +func (ID) ImplementsGraphQLType(name string) bool { + return name == "ID" +} + +func (id *ID) UnmarshalGraphQL(input interface{}) error { + var err error + switch input := input.(type) { + case string: + *id = ID(input) + case int32: + *id = ID(strconv.Itoa(int(input))) + default: + err = errors.New("wrong type") + } + return err +} + +func (id ID) MarshalJSON() ([]byte, error) { + return strconv.AppendQuote(nil, string(id)), nil +} diff --git a/vendor/github.com/graph-gophers/graphql-go/internal/common/directive.go b/vendor/github.com/graph-gophers/graphql-go/internal/common/directive.go new file mode 100644 index 000000000000..62dca47f8166 --- /dev/null +++ b/vendor/github.com/graph-gophers/graphql-go/internal/common/directive.go @@ -0,0 +1,32 @@ +package common + +type Directive struct { + Name Ident + Args ArgumentList +} + +func ParseDirectives(l *Lexer) DirectiveList { + var directives DirectiveList + for l.Peek() == '@' { + l.ConsumeToken('@') + d := &Directive{} + d.Name = l.ConsumeIdentWithLoc() + d.Name.Loc.Column-- + if l.Peek() == '(' { + d.Args = ParseArguments(l) + } + directives = append(directives, d) + } + return directives +} + +type DirectiveList []*Directive + +func (l DirectiveList) Get(name string) *Directive { + for _, d := range l { + if d.Name.Name == name { + return d + } + } + return nil +} diff --git a/vendor/github.com/graph-gophers/graphql-go/internal/common/lexer.go b/vendor/github.com/graph-gophers/graphql-go/internal/common/lexer.go new file mode 100644 index 000000000000..a38fcbaf7068 --- /dev/null +++ b/vendor/github.com/graph-gophers/graphql-go/internal/common/lexer.go @@ -0,0 +1,161 @@ +package common + +import ( + "fmt" + "strings" + "text/scanner" + + "github.com/graph-gophers/graphql-go/errors" +) + +type syntaxError string + +type Lexer struct { + sc *scanner.Scanner + next rune + descComment string +} + +type Ident struct { + Name string + Loc errors.Location +} + +func NewLexer(s string) *Lexer { + sc := &scanner.Scanner{ + Mode: scanner.ScanIdents | scanner.ScanInts | scanner.ScanFloats | scanner.ScanStrings, + } + sc.Init(strings.NewReader(s)) + + return &Lexer{sc: sc} +} + +func (l *Lexer) CatchSyntaxError(f func()) (errRes *errors.QueryError) { + defer func() { + if err := recover(); err != nil { + if err, ok := err.(syntaxError); ok { + errRes = errors.Errorf("syntax error: %s", err) + errRes.Locations = []errors.Location{l.Location()} + return + } + panic(err) + } + }() + + f() + return +} + +func (l *Lexer) Peek() rune { + return l.next +} + +// Consume whitespace and tokens equivalent to whitespace (e.g. commas and comments). +// +// Consumed comment characters will build the description for the next type or field encountered. +// The description is available from `DescComment()`, and will be reset every time `Consume()` is +// executed. +func (l *Lexer) Consume() { + l.descComment = "" + for { + l.next = l.sc.Scan() + + if l.next == ',' { + // Similar to white space and line terminators, commas (',') are used to improve the + // legibility of source text and separate lexical tokens but are otherwise syntactically and + // semantically insignificant within GraphQL documents. + // + // http://facebook.github.io/graphql/draft/#sec-Insignificant-Commas + continue + } + + if l.next == '#' { + // GraphQL source documents may contain single-line comments, starting with the '#' marker. + // + // A comment can contain any Unicode code point except `LineTerminator` so a comment always + // consists of all code points starting with the '#' character up to but not including the + // line terminator. + + l.consumeComment() + continue + } + + break + } +} + +func (l *Lexer) ConsumeIdent() string { + name := l.sc.TokenText() + l.ConsumeToken(scanner.Ident) + return name +} + +func (l *Lexer) ConsumeIdentWithLoc() Ident { + loc := l.Location() + name := l.sc.TokenText() + l.ConsumeToken(scanner.Ident) + return Ident{name, loc} +} + +func (l *Lexer) ConsumeKeyword(keyword string) { + if l.next != scanner.Ident || l.sc.TokenText() != keyword { + l.SyntaxError(fmt.Sprintf("unexpected %q, expecting %q", l.sc.TokenText(), keyword)) + } + l.Consume() +} + +func (l *Lexer) ConsumeLiteral() *BasicLit { + lit := &BasicLit{Type: l.next, Text: l.sc.TokenText()} + l.Consume() + return lit +} + +func (l *Lexer) ConsumeToken(expected rune) { + if l.next != expected { + l.SyntaxError(fmt.Sprintf("unexpected %q, expecting %s", l.sc.TokenText(), scanner.TokenString(expected))) + } + l.Consume() +} + +func (l *Lexer) DescComment() string { + return l.descComment +} + +func (l *Lexer) SyntaxError(message string) { + panic(syntaxError(message)) +} + +func (l *Lexer) Location() errors.Location { + return errors.Location{ + Line: l.sc.Line, + Column: l.sc.Column, + } +} + +// consumeComment consumes all characters from `#` to the first encountered line terminator. +// The characters are appended to `l.descComment`. +func (l *Lexer) consumeComment() { + if l.next != '#' { + return + } + + // TODO: count and trim whitespace so we can dedent any following lines. + if l.sc.Peek() == ' ' { + l.sc.Next() + } + + if l.descComment != "" { + // TODO: use a bytes.Buffer or strings.Builder instead of this. + l.descComment += "\n" + } + + for { + next := l.sc.Next() + if next == '\r' || next == '\n' || next == scanner.EOF { + break + } + + // TODO: use a bytes.Buffer or strings.Build instead of this. + l.descComment += string(next) + } +} diff --git a/vendor/github.com/graph-gophers/graphql-go/internal/common/literals.go b/vendor/github.com/graph-gophers/graphql-go/internal/common/literals.go new file mode 100644 index 000000000000..e7bbe2638e9f --- /dev/null +++ b/vendor/github.com/graph-gophers/graphql-go/internal/common/literals.go @@ -0,0 +1,206 @@ +package common + +import ( + "strconv" + "strings" + "text/scanner" + + "github.com/graph-gophers/graphql-go/errors" +) + +type Literal interface { + Value(vars map[string]interface{}) interface{} + String() string + Location() errors.Location +} + +type BasicLit struct { + Type rune + Text string + Loc errors.Location +} + +func (lit *BasicLit) Value(vars map[string]interface{}) interface{} { + switch lit.Type { + case scanner.Int: + value, err := strconv.ParseInt(lit.Text, 10, 32) + if err != nil { + panic(err) + } + return int32(value) + + case scanner.Float: + value, err := strconv.ParseFloat(lit.Text, 64) + if err != nil { + panic(err) + } + return value + + case scanner.String: + value, err := strconv.Unquote(lit.Text) + if err != nil { + panic(err) + } + return value + + case scanner.Ident: + switch lit.Text { + case "true": + return true + case "false": + return false + default: + return lit.Text + } + + default: + panic("invalid literal") + } +} + +func (lit *BasicLit) String() string { + return lit.Text +} + +func (lit *BasicLit) Location() errors.Location { + return lit.Loc +} + +type ListLit struct { + Entries []Literal + Loc errors.Location +} + +func (lit *ListLit) Value(vars map[string]interface{}) interface{} { + entries := make([]interface{}, len(lit.Entries)) + for i, entry := range lit.Entries { + entries[i] = entry.Value(vars) + } + return entries +} + +func (lit *ListLit) String() string { + entries := make([]string, len(lit.Entries)) + for i, entry := range lit.Entries { + entries[i] = entry.String() + } + return "[" + strings.Join(entries, ", ") + "]" +} + +func (lit *ListLit) Location() errors.Location { + return lit.Loc +} + +type ObjectLit struct { + Fields []*ObjectLitField + Loc errors.Location +} + +type ObjectLitField struct { + Name Ident + Value Literal +} + +func (lit *ObjectLit) Value(vars map[string]interface{}) interface{} { + fields := make(map[string]interface{}, len(lit.Fields)) + for _, f := range lit.Fields { + fields[f.Name.Name] = f.Value.Value(vars) + } + return fields +} + +func (lit *ObjectLit) String() string { + entries := make([]string, 0, len(lit.Fields)) + for _, f := range lit.Fields { + entries = append(entries, f.Name.Name+": "+f.Value.String()) + } + return "{" + strings.Join(entries, ", ") + "}" +} + +func (lit *ObjectLit) Location() errors.Location { + return lit.Loc +} + +type NullLit struct { + Loc errors.Location +} + +func (lit *NullLit) Value(vars map[string]interface{}) interface{} { + return nil +} + +func (lit *NullLit) String() string { + return "null" +} + +func (lit *NullLit) Location() errors.Location { + return lit.Loc +} + +type Variable struct { + Name string + Loc errors.Location +} + +func (v Variable) Value(vars map[string]interface{}) interface{} { + return vars[v.Name] +} + +func (v Variable) String() string { + return "$" + v.Name +} + +func (v *Variable) Location() errors.Location { + return v.Loc +} + +func ParseLiteral(l *Lexer, constOnly bool) Literal { + loc := l.Location() + switch l.Peek() { + case '$': + if constOnly { + l.SyntaxError("variable not allowed") + panic("unreachable") + } + l.ConsumeToken('$') + return &Variable{l.ConsumeIdent(), loc} + + case scanner.Int, scanner.Float, scanner.String, scanner.Ident: + lit := l.ConsumeLiteral() + if lit.Type == scanner.Ident && lit.Text == "null" { + return &NullLit{loc} + } + lit.Loc = loc + return lit + case '-': + l.ConsumeToken('-') + lit := l.ConsumeLiteral() + lit.Text = "-" + lit.Text + lit.Loc = loc + return lit + case '[': + l.ConsumeToken('[') + var list []Literal + for l.Peek() != ']' { + list = append(list, ParseLiteral(l, constOnly)) + } + l.ConsumeToken(']') + return &ListLit{list, loc} + + case '{': + l.ConsumeToken('{') + var fields []*ObjectLitField + for l.Peek() != '}' { + name := l.ConsumeIdentWithLoc() + l.ConsumeToken(':') + value := ParseLiteral(l, constOnly) + fields = append(fields, &ObjectLitField{name, value}) + } + l.ConsumeToken('}') + return &ObjectLit{fields, loc} + + default: + l.SyntaxError("invalid value") + panic("unreachable") + } +} diff --git a/vendor/github.com/graph-gophers/graphql-go/internal/common/types.go b/vendor/github.com/graph-gophers/graphql-go/internal/common/types.go new file mode 100644 index 000000000000..a20ca309069c --- /dev/null +++ b/vendor/github.com/graph-gophers/graphql-go/internal/common/types.go @@ -0,0 +1,80 @@ +package common + +import ( + "github.com/graph-gophers/graphql-go/errors" +) + +type Type interface { + Kind() string + String() string +} + +type List struct { + OfType Type +} + +type NonNull struct { + OfType Type +} + +type TypeName struct { + Ident +} + +func (*List) Kind() string { return "LIST" } +func (*NonNull) Kind() string { return "NON_NULL" } +func (*TypeName) Kind() string { panic("TypeName needs to be resolved to actual type") } + +func (t *List) String() string { return "[" + t.OfType.String() + "]" } +func (t *NonNull) String() string { return t.OfType.String() + "!" } +func (*TypeName) String() string { panic("TypeName needs to be resolved to actual type") } + +func ParseType(l *Lexer) Type { + t := parseNullType(l) + if l.Peek() == '!' { + l.ConsumeToken('!') + return &NonNull{OfType: t} + } + return t +} + +func parseNullType(l *Lexer) Type { + if l.Peek() == '[' { + l.ConsumeToken('[') + ofType := ParseType(l) + l.ConsumeToken(']') + return &List{OfType: ofType} + } + + return &TypeName{Ident: l.ConsumeIdentWithLoc()} +} + +type Resolver func(name string) Type + +func ResolveType(t Type, resolver Resolver) (Type, *errors.QueryError) { + switch t := t.(type) { + case *List: + ofType, err := ResolveType(t.OfType, resolver) + if err != nil { + return nil, err + } + return &List{OfType: ofType}, nil + case *NonNull: + ofType, err := ResolveType(t.OfType, resolver) + if err != nil { + return nil, err + } + return &NonNull{OfType: ofType}, nil + case *TypeName: + refT := resolver(t.Name) + if refT == nil { + err := errors.Errorf("Unknown type %q.", t.Name) + err.Rule = "KnownTypeNames" + err.Locations = []errors.Location{t.Loc} + return nil, err + } + return refT, nil + default: + return t, nil + } +} diff --git a/vendor/github.com/graph-gophers/graphql-go/internal/common/values.go b/vendor/github.com/graph-gophers/graphql-go/internal/common/values.go new file mode 100644 index 000000000000..fcd456abfa5d --- /dev/null +++ b/vendor/github.com/graph-gophers/graphql-go/internal/common/values.go @@ -0,0 +1,78 @@ +package common + +import ( + "github.com/graph-gophers/graphql-go/errors" +) + +// http://facebook.github.io/graphql/draft/#InputValueDefinition +type InputValue struct { + Name Ident + Type Type + Default Literal + Desc string + Loc errors.Location + TypeLoc errors.Location +} + +type InputValueList []*InputValue + +func (l InputValueList) Get(name string) *InputValue { + for _, v := range l { + if v.Name.Name == name { + return v + } + } + return nil +} + +func ParseInputValue(l *Lexer) *InputValue { + p := &InputValue{} + p.Loc = l.Location() + p.Desc = l.DescComment() + p.Name = l.ConsumeIdentWithLoc() + l.ConsumeToken(':') + p.TypeLoc = l.Location() + p.Type = ParseType(l) + if l.Peek() == '=' { + l.ConsumeToken('=') + p.Default = ParseLiteral(l, true) + } + return p +} + +type Argument struct { + Name Ident + Value Literal +} + +type ArgumentList []Argument + +func (l ArgumentList) Get(name string) (Literal, bool) { + for _, arg := range l { + if arg.Name.Name == name { + return arg.Value, true + } + } + return nil, false +} + +func (l ArgumentList) MustGet(name string) Literal { + value, ok := l.Get(name) + if !ok { + panic("argument not found") + } + return value +} + +func ParseArguments(l *Lexer) ArgumentList { + var args ArgumentList + l.ConsumeToken('(') + for l.Peek() != ')' { + name := l.ConsumeIdentWithLoc() + l.ConsumeToken(':') + value := ParseLiteral(l, false) + args = append(args, Argument{Name: name, Value: value}) + } + l.ConsumeToken(')') + return args +} diff --git a/vendor/github.com/graph-gophers/graphql-go/internal/exec/exec.go b/vendor/github.com/graph-gophers/graphql-go/internal/exec/exec.go new file mode 100644 index 000000000000..e6cca7448d70 --- /dev/null +++ b/vendor/github.com/graph-gophers/graphql-go/internal/exec/exec.go @@ -0,0 +1,305 @@ +package exec + +import ( + "bytes" + "context" + "encoding/json" + "reflect" + "sync" + + "github.com/graph-gophers/graphql-go/errors" + "github.com/graph-gophers/graphql-go/internal/common" + "github.com/graph-gophers/graphql-go/internal/exec/resolvable" + "github.com/graph-gophers/graphql-go/internal/exec/selected" + "github.com/graph-gophers/graphql-go/internal/query" + "github.com/graph-gophers/graphql-go/internal/schema" + "github.com/graph-gophers/graphql-go/log" + "github.com/graph-gophers/graphql-go/trace" +) + +type Request struct { + selected.Request + Limiter chan struct{} + Tracer trace.Tracer + Logger log.Logger +} + +func (r *Request) handlePanic(ctx context.Context) { + if value := recover(); value != nil { + r.Logger.LogPanic(ctx, value) + r.AddError(makePanicError(value)) + } +} + +func makePanicError(value interface{}) *errors.QueryError { + return errors.Errorf("graphql: panic occurred: %v", value) +} + +func (r *Request) Execute(ctx context.Context, s *resolvable.Schema, op *query.Operation) ([]byte, []*errors.QueryError) { + var out bytes.Buffer + func() { + defer r.handlePanic(ctx) + sels := selected.ApplyOperation(&r.Request, s, op) + r.execSelections(ctx, sels, nil, s.Resolver, &out, op.Type == query.Mutation) + }() + + if err := ctx.Err(); err != nil { + return nil, []*errors.QueryError{errors.Errorf("%s", err)} + } + + return out.Bytes(), r.Errs +} + +type fieldToExec struct { + field *selected.SchemaField + sels []selected.Selection + resolver reflect.Value + out *bytes.Buffer +} + +func (r *Request) execSelections(ctx context.Context, sels []selected.Selection, path *pathSegment, resolver reflect.Value, out *bytes.Buffer, serially bool) { + async := !serially && selected.HasAsyncSel(sels) + + var fields []*fieldToExec + collectFieldsToResolve(sels, resolver, &fields, make(map[string]*fieldToExec)) + + if async { + var wg sync.WaitGroup + wg.Add(len(fields)) + for _, f := range fields { + go func(f *fieldToExec) { + defer wg.Done() + defer r.handlePanic(ctx) + f.out = new(bytes.Buffer) + execFieldSelection(ctx, r, f, &pathSegment{path, f.field.Alias}, true) + }(f) + } + wg.Wait() + } + + out.WriteByte('{') + for i, f := range fields { + if i > 0 { + out.WriteByte(',') + } + out.WriteByte('"') + out.WriteString(f.field.Alias) + out.WriteByte('"') + out.WriteByte(':') + if async { + out.Write(f.out.Bytes()) + continue + } + f.out = out + execFieldSelection(ctx, r, f, &pathSegment{path, f.field.Alias}, false) + } + out.WriteByte('}') +} + +func collectFieldsToResolve(sels []selected.Selection, resolver reflect.Value, fields *[]*fieldToExec, fieldByAlias map[string]*fieldToExec) { + for _, sel := range sels { + switch sel := sel.(type) { + case *selected.SchemaField: + field, ok := fieldByAlias[sel.Alias] + if !ok { // validation already checked for conflict (TODO) + field = &fieldToExec{field: sel, resolver: resolver} + fieldByAlias[sel.Alias] = field + *fields = append(*fields, field) + } + field.sels = append(field.sels, sel.Sels...) + + case *selected.TypenameField: + sf := &selected.SchemaField{ + Field: resolvable.MetaFieldTypename, + Alias: sel.Alias, + FixedResult: reflect.ValueOf(typeOf(sel, resolver)), + } + *fields = append(*fields, &fieldToExec{field: sf, resolver: resolver}) + + case *selected.TypeAssertion: + out := resolver.Method(sel.MethodIndex).Call(nil) + if !out[1].Bool() { + continue + } + collectFieldsToResolve(sel.Sels, out[0], fields, fieldByAlias) + + default: + panic("unreachable") + } + } +} + +func typeOf(tf *selected.TypenameField, resolver reflect.Value) string { + if len(tf.TypeAssertions) == 0 { + return tf.Name + } + for name, a := range tf.TypeAssertions { + out := resolver.Method(a.MethodIndex).Call(nil) + if out[1].Bool() { + return name + } + } + return "" +} + +func execFieldSelection(ctx context.Context, r *Request, f *fieldToExec, path *pathSegment, applyLimiter bool) { + if applyLimiter { + r.Limiter <- struct{}{} + } + + var result reflect.Value + var err *errors.QueryError + + traceCtx, finish := r.Tracer.TraceField(ctx, f.field.TraceLabel, f.field.TypeName, f.field.Name, !f.field.Async, f.field.Args) + defer func() { + finish(err) + }() + + err = func() (err *errors.QueryError) { + defer func() { + if panicValue := recover(); panicValue != nil { + r.Logger.LogPanic(ctx, panicValue) + err = makePanicError(panicValue) + err.Path = path.toSlice() + } + }() + + if f.field.FixedResult.IsValid() { + result = f.field.FixedResult + return nil + } + + if err := traceCtx.Err(); err != nil { + return errors.Errorf("%s", err) // don't execute any more resolvers if context got cancelled + } + + var in []reflect.Value + if f.field.HasContext { + in = append(in, reflect.ValueOf(traceCtx)) + } + if f.field.ArgsPacker != nil { + in = append(in, f.field.PackedArgs) + } + callOut := f.resolver.Method(f.field.MethodIndex).Call(in) + result = callOut[0] + if f.field.HasError && !callOut[1].IsNil() { + resolverErr := callOut[1].Interface().(error) + err := errors.Errorf("%s", resolverErr) + err.Path = path.toSlice() + err.ResolverError = resolverErr + return err + } + return nil + }() + + if applyLimiter { + <-r.Limiter + } + + if err != nil { + r.AddError(err) + f.out.WriteString("null") // TODO handle non-nil + return + } + + r.execSelectionSet(traceCtx, f.sels, f.field.Type, path, result, f.out) +} + +func (r *Request) execSelectionSet(ctx context.Context, sels []selected.Selection, typ common.Type, path *pathSegment, resolver reflect.Value, out *bytes.Buffer) { + t, nonNull := unwrapNonNull(typ) + switch t := t.(type) { + case *schema.Object, *schema.Interface, *schema.Union: + // a reflect.Value of a nil interface will show up as an Invalid value + if resolver.Kind() == reflect.Invalid || ((resolver.Kind() == reflect.Ptr || resolver.Kind() == reflect.Interface) && resolver.IsNil()) { + if nonNull { + panic(errors.Errorf("got nil for non-null %q", t)) + } + out.WriteString("null") + return + } + + r.execSelections(ctx, sels, path, resolver, out, false) + return + } + + if !nonNull { + if resolver.IsNil() { + out.WriteString("null") + return + } + resolver = resolver.Elem() + } + + switch t := t.(type) { + case *common.List: + l := resolver.Len() + + if selected.HasAsyncSel(sels) { + var wg sync.WaitGroup + wg.Add(l) + entryouts := make([]bytes.Buffer, l) + for i := 0; i < l; i++ { + go func(i int) { + defer wg.Done() + defer r.handlePanic(ctx) + r.execSelectionSet(ctx, sels, t.OfType, &pathSegment{path, i}, resolver.Index(i), &entryouts[i]) + }(i) + } + wg.Wait() + + out.WriteByte('[') + for i, entryout := range entryouts { + if i > 0 { + out.WriteByte(',') + } + out.Write(entryout.Bytes()) + } + out.WriteByte(']') + return + } + + out.WriteByte('[') + for i := 0; i < l; i++ { + if i > 0 { + out.WriteByte(',') + } + r.execSelectionSet(ctx, sels, t.OfType, &pathSegment{path, i}, resolver.Index(i), out) + } + out.WriteByte(']') + + case *schema.Scalar: + v := resolver.Interface() + data, err := json.Marshal(v) + if err != nil { + panic(errors.Errorf("could not marshal %v: %s", v, err)) + } + out.Write(data) + + case *schema.Enum: + out.WriteByte('"') + out.WriteString(resolver.String()) + out.WriteByte('"') + + default: + panic("unreachable") + } +} + +func unwrapNonNull(t common.Type) (common.Type, bool) { + if nn, ok := t.(*common.NonNull); ok { + return nn.OfType, true + } + return t, false +} + +type pathSegment struct { + parent *pathSegment + value interface{} +} + +func (p *pathSegment) toSlice() []interface{} { + if p == nil { + return nil + } + return append(p.parent.toSlice(), p.value) +} diff --git a/vendor/github.com/graph-gophers/graphql-go/internal/exec/packer/packer.go b/vendor/github.com/graph-gophers/graphql-go/internal/exec/packer/packer.go new file mode 100644 index 000000000000..22706bcd1f1e --- /dev/null +++ b/vendor/github.com/graph-gophers/graphql-go/internal/exec/packer/packer.go @@ -0,0 +1,371 @@ +package packer + +import ( + "fmt" + "math" + "reflect" + "strings" + + "github.com/graph-gophers/graphql-go/errors" + "github.com/graph-gophers/graphql-go/internal/common" + "github.com/graph-gophers/graphql-go/internal/schema" +) + +type packer interface { + Pack(value interface{}) (reflect.Value, error) +} + +type Builder struct { + packerMap map[typePair]*packerMapEntry + structPackers []*StructPacker +} + +type typePair struct { + graphQLType common.Type + resolverType reflect.Type +} + +type packerMapEntry struct { + packer packer + targets []*packer +} + +func NewBuilder() *Builder { + return &Builder{ + packerMap: make(map[typePair]*packerMapEntry), + } +} + +func (b *Builder) Finish() error { + for _, entry := range b.packerMap { + for _, target := range entry.targets { + *target = entry.packer + } + } + + for _, p := range b.structPackers { + p.defaultStruct = reflect.New(p.structType).Elem() + for _, f := range p.fields { + if defaultVal := f.field.Default; defaultVal != nil { + v, err := f.fieldPacker.Pack(defaultVal.Value(nil)) + if err != nil { + return err + } + p.defaultStruct.FieldByIndex(f.fieldIndex).Set(v) + } + } + } + + return nil +} + +func (b *Builder) assignPacker(target *packer, schemaType common.Type, reflectType reflect.Type) error { + k := typePair{schemaType, reflectType} + ref, ok := b.packerMap[k] + if !ok { + ref = &packerMapEntry{} + b.packerMap[k] = ref + var err error + ref.packer, err = b.makePacker(schemaType, reflectType) + if err != nil { + return err + } + } + ref.targets = append(ref.targets, target) + return nil +} + +func (b *Builder) makePacker(schemaType common.Type, reflectType reflect.Type) (packer, error) { + t, nonNull := unwrapNonNull(schemaType) + if !nonNull { + if reflectType.Kind() != reflect.Ptr { + return nil, fmt.Errorf("%s is not a pointer", reflectType) + } + elemType := reflectType.Elem() + addPtr := true + if _, ok := t.(*schema.InputObject); ok { + elemType = reflectType // keep pointer for input objects + addPtr = false + } + elem, err := b.makeNonNullPacker(t, elemType) + if err != nil { + return nil, err + } + return &nullPacker{ + elemPacker: elem, + valueType: reflectType, + addPtr: addPtr, + }, nil + } + + return b.makeNonNullPacker(t, reflectType) +} + +func (b *Builder) makeNonNullPacker(schemaType common.Type, reflectType reflect.Type) (packer, error) { + if u, ok := reflect.New(reflectType).Interface().(Unmarshaler); ok { + if !u.ImplementsGraphQLType(schemaType.String()) { + return nil, fmt.Errorf("can not unmarshal %s into %s", schemaType, reflectType) + } + return &unmarshalerPacker{ + ValueType: reflectType, + }, nil + } + + switch t := schemaType.(type) { + case *schema.Scalar: + return &ValuePacker{ + ValueType: reflectType, + }, nil + + case *schema.Enum: + if reflectType.Kind() != reflect.String { + return nil, fmt.Errorf("wrong type, expected %s", reflect.String) + } + return &ValuePacker{ + ValueType: reflectType, + }, nil + + case *schema.InputObject: + e, err := b.MakeStructPacker(t.Values, reflectType) + if err != nil { + return nil, err + } + return e, nil + + case *common.List: + if reflectType.Kind() != reflect.Slice { + return nil, fmt.Errorf("expected slice, got %s", reflectType) + } + p := &listPacker{ + sliceType: reflectType, + } + if err := b.assignPacker(&p.elem, t.OfType, reflectType.Elem()); err != nil { + return nil, err + } + return p, nil + + case *schema.Object, *schema.Interface, *schema.Union: + return nil, fmt.Errorf("type of kind %s can not be used as input", t.Kind()) + + default: + panic("unreachable") + } +} + +func (b *Builder) MakeStructPacker(values common.InputValueList, typ reflect.Type) (*StructPacker, error) { + structType := typ + usePtr := false + if typ.Kind() == reflect.Ptr { + structType = typ.Elem() + usePtr = true + } + if structType.Kind() != reflect.Struct { + return nil, fmt.Errorf("expected struct or pointer to struct, got %s", typ) + } + + var fields []*structPackerField + for _, v := range values { + fe := &structPackerField{field: v} + fx := func(n string) bool { + return strings.EqualFold(stripUnderscore(n), stripUnderscore(v.Name.Name)) + } + + sf, ok := structType.FieldByNameFunc(fx) + if !ok { + return nil, fmt.Errorf("missing argument %q", v.Name) + } + if sf.PkgPath != "" { + return nil, fmt.Errorf("field %q must be exported", sf.Name) + } + fe.fieldIndex = sf.Index + + ft := v.Type + if v.Default != nil { + ft, _ = unwrapNonNull(ft) + ft = &common.NonNull{OfType: ft} + } + + if err := b.assignPacker(&fe.fieldPacker, ft, sf.Type); err != nil { + return nil, fmt.Errorf("field %q: %s", sf.Name, err) + } + + fields = append(fields, fe) + } + + p := &StructPacker{ + structType: structType, + usePtr: usePtr, + fields: fields, + } + b.structPackers = append(b.structPackers, p) + return p, nil +} + +type StructPacker struct { + structType reflect.Type + usePtr bool + defaultStruct reflect.Value + fields []*structPackerField +} + +type structPackerField struct { + field *common.InputValue + fieldIndex []int + fieldPacker packer +} + +func (p *StructPacker) Pack(value interface{}) (reflect.Value, error) { + if value == nil { + return reflect.Value{}, errors.Errorf("got null for non-null") + } + + values := value.(map[string]interface{}) + v := reflect.New(p.structType) + v.Elem().Set(p.defaultStruct) + for _, f := range p.fields { + if value, ok := values[f.field.Name.Name]; ok { + packed, err := f.fieldPacker.Pack(value) + if err != nil { + return reflect.Value{}, err + } + v.Elem().FieldByIndex(f.fieldIndex).Set(packed) + } + } + if !p.usePtr { + return v.Elem(), nil + } + return v, nil +} + +type listPacker struct { + sliceType reflect.Type + elem packer +} + +func (e *listPacker) Pack(value interface{}) (reflect.Value, error) { + list, ok := value.([]interface{}) + if !ok { + list = []interface{}{value} + } + + v := reflect.MakeSlice(e.sliceType, len(list), len(list)) + for i := range list { + packed, err := e.elem.Pack(list[i]) + if err != nil { + return reflect.Value{}, err + } + v.Index(i).Set(packed) + } + return v, nil +} + +type nullPacker struct { + elemPacker packer + valueType reflect.Type + addPtr bool +} + +func (p *nullPacker) Pack(value interface{}) (reflect.Value, error) { + if value == nil { + return reflect.Zero(p.valueType), nil + } + + v, err := p.elemPacker.Pack(value) + if err != nil { + return reflect.Value{}, err + } + + if p.addPtr { + ptr := reflect.New(p.valueType.Elem()) + ptr.Elem().Set(v) + return ptr, nil + } + + return v, nil +} + +type ValuePacker struct { + ValueType reflect.Type +} + +func (p *ValuePacker) Pack(value interface{}) (reflect.Value, error) { + if value == nil { + return reflect.Value{}, errors.Errorf("got null for non-null") + } + + coerced, err := unmarshalInput(p.ValueType, value) + if err != nil { + return reflect.Value{}, fmt.Errorf("could not unmarshal %#v (%T) into %s: %s", value, value, p.ValueType, err) + } + return reflect.ValueOf(coerced), nil +} + +type unmarshalerPacker struct { + ValueType reflect.Type +} + +func (p *unmarshalerPacker) Pack(value interface{}) (reflect.Value, error) { + if value == nil { + return reflect.Value{}, errors.Errorf("got null for non-null") + } + + v := reflect.New(p.ValueType) + if err := v.Interface().(Unmarshaler).UnmarshalGraphQL(value); err != nil { + return reflect.Value{}, err + } + return v.Elem(), nil +} + +type Unmarshaler interface { + ImplementsGraphQLType(name string) bool + UnmarshalGraphQL(input interface{}) error +} + +func unmarshalInput(typ reflect.Type, input interface{}) (interface{}, error) { + if reflect.TypeOf(input) == typ { + return input, nil + } + + switch typ.Kind() { + case reflect.Int32: + switch input := input.(type) { + case int: + if input < math.MinInt32 || input > math.MaxInt32 { + return nil, fmt.Errorf("not a 32-bit integer") + } + return int32(input), nil + case float64: + coerced := int32(input) + if input < math.MinInt32 || input > math.MaxInt32 || float64(coerced) != input { + return nil, fmt.Errorf("not a 32-bit integer") + } + return coerced, nil + } + + case reflect.Float64: + switch input := input.(type) { + case int32: + return float64(input), nil + case int: + return float64(input), nil + } + + case reflect.String: + if reflect.TypeOf(input).ConvertibleTo(typ) { + return reflect.ValueOf(input).Convert(typ).Interface(), nil + } + } + + return nil, fmt.Errorf("incompatible type") +} + +func unwrapNonNull(t common.Type) (common.Type, bool) { + if nn, ok := t.(*common.NonNull); ok { + return nn.OfType, true + } + return t, false +} + +func stripUnderscore(s string) string { + return strings.Replace(s, "_", "", -1) +} diff --git a/vendor/github.com/graph-gophers/graphql-go/internal/exec/resolvable/meta.go b/vendor/github.com/graph-gophers/graphql-go/internal/exec/resolvable/meta.go new file mode 100644 index 000000000000..826c8234841b --- /dev/null +++ b/vendor/github.com/graph-gophers/graphql-go/internal/exec/resolvable/meta.go @@ -0,0 +1,58 @@ +package resolvable + +import ( + "fmt" + "reflect" + + "github.com/graph-gophers/graphql-go/internal/common" + "github.com/graph-gophers/graphql-go/internal/schema" + "github.com/graph-gophers/graphql-go/introspection" +) + +var MetaSchema *Object +var MetaType *Object + +func init() { + var err error + b := newBuilder(schema.Meta) + + metaSchema := schema.Meta.Types["__Schema"].(*schema.Object) + MetaSchema, err = b.makeObjectExec(metaSchema.Name, metaSchema.Fields, nil, false, reflect.TypeOf(&introspection.Schema{})) + if err != nil { + panic(err) + } + + metaType := schema.Meta.Types["__Type"].(*schema.Object) + MetaType, err = b.makeObjectExec(metaType.Name, metaType.Fields, nil, false, reflect.TypeOf(&introspection.Type{})) + if err != nil { + panic(err) + } + + if err := b.finish(); err != nil { + panic(err) + } +} + +var MetaFieldTypename = Field{ + Field: schema.Field{ + Name: "__typename", + Type: &common.NonNull{OfType: schema.Meta.Types["String"]}, + }, + TraceLabel: fmt.Sprintf("GraphQL field: __typename"), +} + +var MetaFieldSchema = Field{ + Field: schema.Field{ + Name: "__schema", + Type: schema.Meta.Types["__Schema"], + }, + TraceLabel: fmt.Sprintf("GraphQL field: __schema"), +} + +var MetaFieldType = Field{ + Field: schema.Field{ + Name: "__type", + Type: schema.Meta.Types["__Type"], + }, + TraceLabel: fmt.Sprintf("GraphQL field: __type"), +} diff --git a/vendor/github.com/graph-gophers/graphql-go/internal/exec/resolvable/resolvable.go b/vendor/github.com/graph-gophers/graphql-go/internal/exec/resolvable/resolvable.go new file mode 100644 index 000000000000..3e5d9e44d90e --- /dev/null +++ b/vendor/github.com/graph-gophers/graphql-go/internal/exec/resolvable/resolvable.go @@ -0,0 +1,331 @@ +package resolvable + +import ( + "context" + "fmt" + "reflect" + "strings" + + "github.com/graph-gophers/graphql-go/internal/common" + "github.com/graph-gophers/graphql-go/internal/exec/packer" + "github.com/graph-gophers/graphql-go/internal/schema" +) + +type Schema struct { + schema.Schema + Query Resolvable + Mutation Resolvable + Resolver reflect.Value +} + +type Resolvable interface { + isResolvable() +} + +type Object struct { + Name string + Fields map[string]*Field + TypeAssertions map[string]*TypeAssertion +} + +type Field struct { + schema.Field + TypeName string + MethodIndex int + HasContext bool + HasError bool + ArgsPacker *packer.StructPacker + ValueExec Resolvable + TraceLabel string +} + +type TypeAssertion struct { + MethodIndex int + TypeExec Resolvable +} + +type List struct { + Elem Resolvable +} + +type Scalar struct{} + +func (*Object) isResolvable() {} +func (*List) isResolvable() {} +func (*Scalar) isResolvable() {} + +func ApplyResolver(s *schema.Schema, resolver interface{}) (*Schema, error) { + b := newBuilder(s) + + var query, mutation Resolvable + + if t, ok := s.EntryPoints["query"]; ok { + if err := b.assignExec(&query, t, reflect.TypeOf(resolver)); err != nil { + return nil, err + } + } + + if t, ok := s.EntryPoints["mutation"]; ok { + if err := b.assignExec(&mutation, t, reflect.TypeOf(resolver)); err != nil { + return nil, err + } + } + + if err := b.finish(); err != nil { + return nil, err + } + + return &Schema{ + Schema: *s, + Resolver: reflect.ValueOf(resolver), + Query: query, + Mutation: mutation, + }, nil +} + +type execBuilder struct { + schema *schema.Schema + resMap map[typePair]*resMapEntry + packerBuilder *packer.Builder +} + +type typePair struct { + graphQLType common.Type + resolverType reflect.Type +} + +type resMapEntry struct { + exec Resolvable + targets []*Resolvable +} + +func newBuilder(s *schema.Schema) *execBuilder { + return &execBuilder{ + schema: s, + resMap: make(map[typePair]*resMapEntry), + packerBuilder: packer.NewBuilder(), + } +} + +func (b *execBuilder) finish() error { + for _, entry := range b.resMap { + for _, target := range entry.targets { + *target = entry.exec + } + } + + return b.packerBuilder.Finish() +} + +func (b *execBuilder) assignExec(target *Resolvable, t common.Type, resolverType reflect.Type) error { + k := typePair{t, resolverType} + ref, ok := b.resMap[k] + if !ok { + ref = &resMapEntry{} + b.resMap[k] = ref + var err error + ref.exec, err = b.makeExec(t, resolverType) + if err != nil { + return err + } + } + ref.targets = append(ref.targets, target) + return nil +} + +func (b *execBuilder) makeExec(t common.Type, resolverType reflect.Type) (Resolvable, error) { + var nonNull bool + t, nonNull = unwrapNonNull(t) + + switch t := t.(type) { + case *schema.Object: + return b.makeObjectExec(t.Name, t.Fields, nil, nonNull, resolverType) + + case *schema.Interface: + return b.makeObjectExec(t.Name, t.Fields, t.PossibleTypes, nonNull, resolverType) + + case *schema.Union: + return b.makeObjectExec(t.Name, nil, t.PossibleTypes, nonNull, resolverType) + } + + if !nonNull { + if resolverType.Kind() != reflect.Ptr { + return nil, fmt.Errorf("%s is not a pointer", resolverType) + } + resolverType = resolverType.Elem() + } + + switch t := t.(type) { + case *schema.Scalar: + return makeScalarExec(t, resolverType) + + case *schema.Enum: + return &Scalar{}, nil + + case *common.List: + if resolverType.Kind() != reflect.Slice { + return nil, fmt.Errorf("%s is not a slice", resolverType) + } + e := &List{} + if err := b.assignExec(&e.Elem, t.OfType, resolverType.Elem()); err != nil { + return nil, err + } + return e, nil + + default: + panic("invalid type: " + t.String()) + } +} + +func makeScalarExec(t *schema.Scalar, resolverType reflect.Type) (Resolvable, error) { + implementsType := false + switch r := reflect.New(resolverType).Interface().(type) { + case *int32: + implementsType = (t.Name == "Int") + case *float64: + implementsType = (t.Name == "Float") + case *string: + implementsType = (t.Name == "String") + case *bool: + implementsType = (t.Name == "Boolean") + case packer.Unmarshaler: + implementsType = r.ImplementsGraphQLType(t.Name) + } + if !implementsType { + return nil, fmt.Errorf("can not use %s as %s", resolverType, t.Name) + } + return &Scalar{}, nil +} + +func (b *execBuilder) makeObjectExec(typeName string, fields schema.FieldList, possibleTypes []*schema.Object, nonNull bool, resolverType reflect.Type) (*Object, error) { + if !nonNull { + if resolverType.Kind() != reflect.Ptr && resolverType.Kind() != reflect.Interface { + return nil, fmt.Errorf("%s is not a pointer or interface", resolverType) + } + } + + methodHasReceiver := resolverType.Kind() != reflect.Interface + + Fields := make(map[string]*Field) + for _, f := range fields { + methodIndex := findMethod(resolverType, f.Name) + if methodIndex == -1 { + hint := "" + if findMethod(reflect.PtrTo(resolverType), f.Name) != -1 { + hint = " (hint: the method exists on the pointer type)" + } + return nil, fmt.Errorf("%s does not resolve %q: missing method for field %q%s", resolverType, typeName, f.Name, hint) + } + + m := resolverType.Method(methodIndex) + fe, err := b.makeFieldExec(typeName, f, m, methodIndex, methodHasReceiver) + if err != nil { + return nil, fmt.Errorf("%s\n\treturned by (%s).%s", err, resolverType, m.Name) + } + Fields[f.Name] = fe + } + + typeAssertions := make(map[string]*TypeAssertion) + for _, impl := range possibleTypes { + methodIndex := findMethod(resolverType, "To"+impl.Name) + if methodIndex == -1 { + return nil, fmt.Errorf("%s does not resolve %q: missing method %q to convert to %q", resolverType, typeName, "To"+impl.Name, impl.Name) + } + if resolverType.Method(methodIndex).Type.NumOut() != 2 { + return nil, fmt.Errorf("%s does not resolve %q: method %q should return a value and a bool indicating success", resolverType, typeName, "To"+impl.Name) + } + a := &TypeAssertion{ + MethodIndex: methodIndex, + } + if err := b.assignExec(&a.TypeExec, impl, resolverType.Method(methodIndex).Type.Out(0)); err != nil { + return nil, err + } + typeAssertions[impl.Name] = a + } + + return &Object{ + Name: typeName, + Fields: Fields, + TypeAssertions: typeAssertions, + }, nil +} + +var contextType = reflect.TypeOf((*context.Context)(nil)).Elem() +var errorType = reflect.TypeOf((*error)(nil)).Elem() + +func (b *execBuilder) makeFieldExec(typeName string, f *schema.Field, m reflect.Method, methodIndex int, methodHasReceiver bool) (*Field, error) { + in := make([]reflect.Type, m.Type.NumIn()) + for i := range in { + in[i] = m.Type.In(i) + } + if methodHasReceiver { + in = in[1:] // first parameter is receiver + } + + hasContext := len(in) > 0 && in[0] == contextType + if hasContext { + in = in[1:] + } + + var argsPacker *packer.StructPacker + if len(f.Args) > 0 { + if len(in) == 0 { + return nil, fmt.Errorf("must have parameter for field arguments") + } + var err error + argsPacker, err = b.packerBuilder.MakeStructPacker(f.Args, in[0]) + if err != nil { + return nil, err + } + in = in[1:] + } + + if len(in) > 0 { + return nil, fmt.Errorf("too many parameters") + } + + if m.Type.NumOut() > 2 { + return nil, fmt.Errorf("too many return values") + } + + hasError := m.Type.NumOut() == 2 + if hasError { + if m.Type.Out(1) != errorType { + return nil, fmt.Errorf(`must have "error" as its second return value`) + } + } + + fe := &Field{ + Field: *f, + TypeName: typeName, + MethodIndex: methodIndex, + HasContext: hasContext, + ArgsPacker: argsPacker, + HasError: hasError, + TraceLabel: fmt.Sprintf("GraphQL field: %s.%s", typeName, f.Name), + } + if err := b.assignExec(&fe.ValueExec, f.Type, m.Type.Out(0)); err != nil { + return nil, err + } + return fe, nil +} + +func findMethod(t reflect.Type, name string) int { + for i := 0; i < t.NumMethod(); i++ { + if strings.EqualFold(stripUnderscore(name), stripUnderscore(t.Method(i).Name)) { + return i + } + } + return -1 +} + +func unwrapNonNull(t common.Type) (common.Type, bool) { + if nn, ok := t.(*common.NonNull); ok { + return nn.OfType, true + } + return t, false +} + +func stripUnderscore(s string) string { + return strings.Replace(s, "_", "", -1) +} diff --git a/vendor/github.com/graph-gophers/graphql-go/internal/exec/selected/selected.go b/vendor/github.com/graph-gophers/graphql-go/internal/exec/selected/selected.go new file mode 100644 index 000000000000..aed079b6713a --- /dev/null +++ b/vendor/github.com/graph-gophers/graphql-go/internal/exec/selected/selected.go @@ -0,0 +1,238 @@ +package selected + +import ( + "fmt" + "reflect" + "sync" + + "github.com/graph-gophers/graphql-go/errors" + "github.com/graph-gophers/graphql-go/internal/common" + "github.com/graph-gophers/graphql-go/internal/exec/packer" + "github.com/graph-gophers/graphql-go/internal/exec/resolvable" + "github.com/graph-gophers/graphql-go/internal/query" + "github.com/graph-gophers/graphql-go/internal/schema" + "github.com/graph-gophers/graphql-go/introspection" +) + +type Request struct { + Schema *schema.Schema + Doc *query.Document + Vars map[string]interface{} + Mu sync.Mutex + Errs []*errors.QueryError +} + +func (r *Request) AddError(err *errors.QueryError) { + r.Mu.Lock() + r.Errs = append(r.Errs, err) + r.Mu.Unlock() +} + +func ApplyOperation(r *Request, s *resolvable.Schema, op *query.Operation) []Selection { + var obj *resolvable.Object + switch op.Type { + case query.Query: + obj = s.Query.(*resolvable.Object) + case query.Mutation: + obj = s.Mutation.(*resolvable.Object) + } + return applySelectionSet(r, obj, op.Selections) +} + +type Selection interface { + isSelection() +} + +type SchemaField struct { + resolvable.Field + Alias string + Args map[string]interface{} + PackedArgs reflect.Value + Sels []Selection + Async bool + FixedResult reflect.Value +} + +type TypeAssertion struct { + resolvable.TypeAssertion + Sels []Selection +} + +type TypenameField struct { + resolvable.Object + Alias string +} + +func (*SchemaField) isSelection() {} +func (*TypeAssertion) isSelection() {} +func (*TypenameField) isSelection() {} + +func applySelectionSet(r *Request, e *resolvable.Object, sels []query.Selection) (flattenedSels []Selection) { + for _, sel := range sels { + switch sel := sel.(type) { + case *query.Field: + field := sel + if skipByDirective(r, field.Directives) { + continue + } + + switch field.Name.Name { + case "__typename": + flattenedSels = append(flattenedSels, &TypenameField{ + Object: *e, + Alias: field.Alias.Name, + }) + + case "__schema": + flattenedSels = append(flattenedSels, &SchemaField{ + Field: resolvable.MetaFieldSchema, + Alias: field.Alias.Name, + Sels: applySelectionSet(r, resolvable.MetaSchema, field.Selections), + Async: true, + FixedResult: reflect.ValueOf(introspection.WrapSchema(r.Schema)), + }) + + case "__type": + p := packer.ValuePacker{ValueType: reflect.TypeOf("")} + v, err := p.Pack(field.Arguments.MustGet("name").Value(r.Vars)) + if err != nil { + r.AddError(errors.Errorf("%s", err)) + return nil + } + + t, ok := r.Schema.Types[v.String()] + if !ok { + return nil + } + + flattenedSels = append(flattenedSels, &SchemaField{ + Field: resolvable.MetaFieldType, + Alias: field.Alias.Name, + Sels: applySelectionSet(r, resolvable.MetaType, field.Selections), + Async: true, + FixedResult: reflect.ValueOf(introspection.WrapType(t)), + }) + + default: + fe := e.Fields[field.Name.Name] + + var args map[string]interface{} + var packedArgs reflect.Value + if fe.ArgsPacker != nil { + args = make(map[string]interface{}) + for _, arg := range field.Arguments { + args[arg.Name.Name] = arg.Value.Value(r.Vars) + } + var err error + packedArgs, err = fe.ArgsPacker.Pack(args) + if err != nil { + r.AddError(errors.Errorf("%s", err)) + return + } + } + + fieldSels := applyField(r, fe.ValueExec, field.Selections) + flattenedSels = append(flattenedSels, &SchemaField{ + Field: *fe, + Alias: field.Alias.Name, + Args: args, + PackedArgs: packedArgs, + Sels: fieldSels, + Async: fe.HasContext || fe.ArgsPacker != nil || fe.HasError || HasAsyncSel(fieldSels), + }) + } + + case *query.InlineFragment: + frag := sel + if skipByDirective(r, frag.Directives) { + continue + } + flattenedSels = append(flattenedSels, applyFragment(r, e, &frag.Fragment)...) + + case *query.FragmentSpread: + spread := sel + if skipByDirective(r, spread.Directives) { + continue + } + flattenedSels = append(flattenedSels, applyFragment(r, e, &r.Doc.Fragments.Get(spread.Name.Name).Fragment)...) + + default: + panic("invalid type") + } + } + return +} + +func applyFragment(r *Request, e *resolvable.Object, frag *query.Fragment) []Selection { + if frag.On.Name != "" && frag.On.Name != e.Name { + a, ok := e.TypeAssertions[frag.On.Name] + if !ok { + panic(fmt.Errorf("%q does not implement %q", frag.On, e.Name)) // TODO proper error handling + } + + return []Selection{&TypeAssertion{ + TypeAssertion: *a, + Sels: applySelectionSet(r, a.TypeExec.(*resolvable.Object), frag.Selections), + }} + } + return applySelectionSet(r, e, frag.Selections) +} + +func applyField(r *Request, e resolvable.Resolvable, sels []query.Selection) []Selection { + switch e := e.(type) { + case *resolvable.Object: + return applySelectionSet(r, e, sels) + case *resolvable.List: + return applyField(r, e.Elem, sels) + case *resolvable.Scalar: + return nil + default: + panic("unreachable") + } +} + +func skipByDirective(r *Request, directives common.DirectiveList) bool { + if d := directives.Get("skip"); d != nil { + p := packer.ValuePacker{ValueType: reflect.TypeOf(false)} + v, err := p.Pack(d.Args.MustGet("if").Value(r.Vars)) + if err != nil { + r.AddError(errors.Errorf("%s", err)) + } + if err == nil && v.Bool() { + return true + } + } + + if d := directives.Get("include"); d != nil { + p := packer.ValuePacker{ValueType: reflect.TypeOf(false)} + v, err := p.Pack(d.Args.MustGet("if").Value(r.Vars)) + if err != nil { + r.AddError(errors.Errorf("%s", err)) + } + if err == nil && !v.Bool() { + return true + } + } + + return false +} + +func HasAsyncSel(sels []Selection) bool { + for _, sel := range sels { + switch sel := sel.(type) { + case *SchemaField: + if sel.Async { + return true + } + case *TypeAssertion: + if HasAsyncSel(sel.Sels) { + return true + } + case *TypenameField: + // sync + default: + panic("unreachable") + } + } + return false +} diff --git a/vendor/github.com/graph-gophers/graphql-go/internal/query/query.go b/vendor/github.com/graph-gophers/graphql-go/internal/query/query.go new file mode 100644 index 000000000000..faba4d2adee4 --- /dev/null +++ b/vendor/github.com/graph-gophers/graphql-go/internal/query/query.go @@ -0,0 +1,234 @@ +package query + +import ( + "fmt" + "text/scanner" + + "github.com/graph-gophers/graphql-go/errors" + "github.com/graph-gophers/graphql-go/internal/common" +) + +type Document struct { + Operations OperationList + Fragments FragmentList +} + +type OperationList []*Operation + +func (l OperationList) Get(name string) *Operation { + for _, f := range l { + if f.Name.Name == name { + return f + } + } + return nil +} + +type FragmentList []*FragmentDecl + +func (l FragmentList) Get(name string) *FragmentDecl { + for _, f := range l { + if f.Name.Name == name { + return f + } + } + return nil +} + +type Operation struct { + Type OperationType + Name common.Ident + Vars common.InputValueList + Selections []Selection + Directives common.DirectiveList + Loc errors.Location +} + +type OperationType string + +const ( + Query OperationType = "QUERY" + Mutation = "MUTATION" + Subscription = "SUBSCRIPTION" +) + +type Fragment struct { + On common.TypeName + Selections []Selection +} + +type FragmentDecl struct { + Fragment + Name common.Ident + Directives common.DirectiveList + Loc errors.Location +} + +type Selection interface { + isSelection() +} + +type Field struct { + Alias common.Ident + Name common.Ident + Arguments common.ArgumentList + Directives common.DirectiveList + Selections []Selection + SelectionSetLoc errors.Location +} + +type InlineFragment struct { + Fragment + Directives common.DirectiveList + Loc errors.Location +} + +type FragmentSpread struct { + Name common.Ident + Directives common.DirectiveList + Loc errors.Location +} + +func (Field) isSelection() {} +func (InlineFragment) isSelection() {} +func (FragmentSpread) isSelection() {} + +func Parse(queryString string) (*Document, *errors.QueryError) { + l := common.NewLexer(queryString) + + var doc *Document + err := l.CatchSyntaxError(func() { doc = parseDocument(l) }) + if err != nil { + return nil, err + } + + return doc, nil +} + +func parseDocument(l *common.Lexer) *Document { + d := &Document{} + l.Consume() + for l.Peek() != scanner.EOF { + if l.Peek() == '{' { + op := &Operation{Type: Query, Loc: l.Location()} + op.Selections = parseSelectionSet(l) + d.Operations = append(d.Operations, op) + continue + } + + loc := l.Location() + switch x := l.ConsumeIdent(); x { + case "query": + op := parseOperation(l, Query) + op.Loc = loc + d.Operations = append(d.Operations, op) + + case "mutation": + d.Operations = append(d.Operations, parseOperation(l, Mutation)) + + case "subscription": + d.Operations = append(d.Operations, parseOperation(l, Subscription)) + + case "fragment": + frag := parseFragment(l) + frag.Loc = loc + d.Fragments = append(d.Fragments, frag) + + default: + l.SyntaxError(fmt.Sprintf(`unexpected %q, expecting "fragment"`, x)) + } + } + return d +} + +func parseOperation(l *common.Lexer, opType OperationType) *Operation { + op := &Operation{Type: opType} + op.Name.Loc = l.Location() + if l.Peek() == scanner.Ident { + op.Name = l.ConsumeIdentWithLoc() + } + op.Directives = common.ParseDirectives(l) + if l.Peek() == '(' { + l.ConsumeToken('(') + for l.Peek() != ')' { + loc := l.Location() + l.ConsumeToken('$') + iv := common.ParseInputValue(l) + iv.Loc = loc + op.Vars = append(op.Vars, iv) + } + l.ConsumeToken(')') + } + op.Selections = parseSelectionSet(l) + return op +} + +func parseFragment(l *common.Lexer) *FragmentDecl { + f := &FragmentDecl{} + f.Name = l.ConsumeIdentWithLoc() + l.ConsumeKeyword("on") + f.On = common.TypeName{Ident: l.ConsumeIdentWithLoc()} + f.Directives = common.ParseDirectives(l) + f.Selections = parseSelectionSet(l) + return f +} + +func parseSelectionSet(l *common.Lexer) []Selection { + var sels []Selection + l.ConsumeToken('{') + for l.Peek() != '}' { + sels = append(sels, parseSelection(l)) + } + l.ConsumeToken('}') + return sels +} + +func parseSelection(l *common.Lexer) Selection { + if l.Peek() == '.' { + return parseSpread(l) + } + return parseField(l) +} + +func parseField(l *common.Lexer) *Field { + f := &Field{} + f.Alias = l.ConsumeIdentWithLoc() + f.Name = f.Alias + if l.Peek() == ':' { + l.ConsumeToken(':') + f.Name = l.ConsumeIdentWithLoc() + } + if l.Peek() == '(' { + f.Arguments = common.ParseArguments(l) + } + f.Directives = common.ParseDirectives(l) + if l.Peek() == '{' { + f.SelectionSetLoc = l.Location() + f.Selections = parseSelectionSet(l) + } + return f +} + +func parseSpread(l *common.Lexer) Selection { + loc := l.Location() + l.ConsumeToken('.') + l.ConsumeToken('.') + l.ConsumeToken('.') + + f := &InlineFragment{Loc: loc} + if l.Peek() == scanner.Ident { + ident := l.ConsumeIdentWithLoc() + if ident.Name != "on" { + fs := &FragmentSpread{ + Name: ident, + Loc: loc, + } + fs.Directives = common.ParseDirectives(l) + return fs + } + f.On = common.TypeName{Ident: l.ConsumeIdentWithLoc()} + } + f.Directives = common.ParseDirectives(l) + f.Selections = parseSelectionSet(l) + return f +} diff --git a/vendor/github.com/graph-gophers/graphql-go/internal/schema/meta.go b/vendor/github.com/graph-gophers/graphql-go/internal/schema/meta.go new file mode 100644 index 000000000000..b48bf7acf286 --- /dev/null +++ b/vendor/github.com/graph-gophers/graphql-go/internal/schema/meta.go @@ -0,0 +1,190 @@ +package schema + +var Meta *Schema + +func init() { + Meta = &Schema{} // bootstrap + Meta = New() + if err := Meta.Parse(metaSrc); err != nil { + panic(err) + } +} + +var metaSrc = ` + # The ` + "`" + `Int` + "`" + ` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. + scalar Int + + # The ` + "`" + `Float` + "`" + ` scalar type represents signed double-precision fractional values as specified by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point). + scalar Float + + # The ` + "`" + `String` + "`" + ` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. + scalar String + + # The ` + "`" + `Boolean` + "`" + ` scalar type represents ` + "`" + `true` + "`" + ` or ` + "`" + `false` + "`" + `. + scalar Boolean + + # The ` + "`" + `ID` + "`" + ` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as ` + "`" + `"4"` + "`" + `) or integer (such as ` + "`" + `4` + "`" + `) input value will be accepted as an ID. + scalar ID + + # Directs the executor to include this field or fragment only when the ` + "`" + `if` + "`" + ` argument is true. + directive @include( + # Included when true. + if: Boolean! + ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + + # Directs the executor to skip this field or fragment when the ` + "`" + `if` + "`" + ` argument is true. + directive @skip( + # Skipped when true. + if: Boolean! + ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + + # Marks an element of a GraphQL schema as no longer supported. + directive @deprecated( + # Explains why this element was deprecated, usually also including a suggestion + # for how to access supported similar data. Formatted in + # [Markdown](https://daringfireball.net/projects/markdown/). + reason: String = "No longer supported" + ) on FIELD_DEFINITION | ENUM_VALUE + + # A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document. + # + # In some cases, you need to provide options to alter GraphQL's execution behavior + # in ways field arguments will not suffice, such as conditionally including or + # skipping a field. Directives provide this by describing additional information + # to the executor. + type __Directive { + name: String! + description: String + locations: [__DirectiveLocation!]! + args: [__InputValue!]! + } + + # A Directive can be adjacent to many parts of the GraphQL language, a + # __DirectiveLocation describes one such possible adjacencies. + enum __DirectiveLocation { + # Location adjacent to a query operation. + QUERY + # Location adjacent to a mutation operation. + MUTATION + # Location adjacent to a subscription operation. + SUBSCRIPTION + # Location adjacent to a field. + FIELD + # Location adjacent to a fragment definition. + FRAGMENT_DEFINITION + # Location adjacent to a fragment spread. + FRAGMENT_SPREAD + # Location adjacent to an inline fragment. + INLINE_FRAGMENT + # Location adjacent to a schema definition. + SCHEMA + # Location adjacent to a scalar definition. + SCALAR + # Location adjacent to an object type definition. + OBJECT + # Location adjacent to a field definition. + FIELD_DEFINITION + # Location adjacent to an argument definition. + ARGUMENT_DEFINITION + # Location adjacent to an interface definition. + INTERFACE + # Location adjacent to a union definition. + UNION + # Location adjacent to an enum definition. + ENUM + # Location adjacent to an enum value definition. + ENUM_VALUE + # Location adjacent to an input object type definition. + INPUT_OBJECT + # Location adjacent to an input object field definition. + INPUT_FIELD_DEFINITION + } + + # One possible value for a given Enum. Enum values are unique values, not a + # placeholder for a string or numeric value. However an Enum value is returned in + # a JSON response as a string. + type __EnumValue { + name: String! + description: String + isDeprecated: Boolean! + deprecationReason: String + } + + # Object and Interface types are described by a list of Fields, each of which has + # a name, potentially a list of arguments, and a return type. + type __Field { + name: String! + description: String + args: [__InputValue!]! + type: __Type! + isDeprecated: Boolean! + deprecationReason: String + } + + # Arguments provided to Fields or Directives and the input fields of an + # InputObject are represented as Input Values which describe their type and + # optionally a default value. + type __InputValue { + name: String! + description: String + type: __Type! + # A GraphQL-formatted string representing the default value for this input value. + defaultValue: String + } + + # A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all + # available types and directives on the server, as well as the entry points for + # query, mutation, and subscription operations. + type __Schema { + # A list of all types supported by this server. + types: [__Type!]! + # The type that query operations will be rooted at. + queryType: __Type! + # If this server supports mutation, the type that mutation operations will be rooted at. + mutationType: __Type + # If this server support subscription, the type that subscription operations will be rooted at. + subscriptionType: __Type + # A list of all directives supported by this server. + directives: [__Directive!]! + } + + # The fundamental unit of any GraphQL Schema is the type. There are many kinds of + # types in GraphQL as represented by the ` + "`" + `__TypeKind` + "`" + ` enum. + # + # Depending on the kind of a type, certain fields describe information about that + # type. Scalar types provide no information beyond a name and description, while + # Enum types provide their values. Object and Interface types provide the fields + # they describe. Abstract types, Union and Interface, provide the Object types + # possible at runtime. List and NonNull types compose other types. + type __Type { + kind: __TypeKind! + name: String + description: String + fields(includeDeprecated: Boolean = false): [__Field!] + interfaces: [__Type!] + possibleTypes: [__Type!] + enumValues(includeDeprecated: Boolean = false): [__EnumValue!] + inputFields: [__InputValue!] + ofType: __Type + } + + # An enum describing what kind of type a given ` + "`" + `__Type` + "`" + ` is. + enum __TypeKind { + # Indicates this type is a scalar. + SCALAR + # Indicates this type is an object. ` + "`" + `fields` + "`" + ` and ` + "`" + `interfaces` + "`" + ` are valid fields. + OBJECT + # Indicates this type is an interface. ` + "`" + `fields` + "`" + ` and ` + "`" + `possibleTypes` + "`" + ` are valid fields. + INTERFACE + # Indicates this type is a union. ` + "`" + `possibleTypes` + "`" + ` is a valid field. + UNION + # Indicates this type is an enum. ` + "`" + `enumValues` + "`" + ` is a valid field. + ENUM + # Indicates this type is an input object. ` + "`" + `inputFields` + "`" + ` is a valid field. + INPUT_OBJECT + # Indicates this type is a list. ` + "`" + `ofType` + "`" + ` is a valid field. + LIST + # Indicates this type is a non-null. ` + "`" + `ofType` + "`" + ` is a valid field. + NON_NULL + } +` diff --git a/vendor/github.com/graph-gophers/graphql-go/internal/schema/schema.go b/vendor/github.com/graph-gophers/graphql-go/internal/schema/schema.go new file mode 100644 index 000000000000..e549f17c07ed --- /dev/null +++ b/vendor/github.com/graph-gophers/graphql-go/internal/schema/schema.go @@ -0,0 +1,570 @@ +package schema + +import ( + "fmt" + "text/scanner" + + "github.com/graph-gophers/graphql-go/errors" + "github.com/graph-gophers/graphql-go/internal/common" +) + +// Schema represents a GraphQL service's collective type system capabilities. +// A schema is defined in terms of the types and directives it supports as well as the root +// operation types for each kind of operation: `query`, `mutation`, and `subscription`. +// +// For a more formal definition, read the relevant section in the specification: +// +// http://facebook.github.io/graphql/draft/#sec-Schema +type Schema struct { + // EntryPoints determines the place in the type system where `query`, `mutation`, and + // `subscription` operations begin. + // + // http://facebook.github.io/graphql/draft/#sec-Root-Operation-Types + // + // NOTE: The specification refers to this concept as "Root Operation Types". + // TODO: Rename the `EntryPoints` field to `RootOperationTypes` to align with spec terminology. + EntryPoints map[string]NamedType + + // Types are the fundamental unit of any GraphQL schema. + // There are six kinds of named types, and two wrapping types. + // + // http://facebook.github.io/graphql/draft/#sec-Types + Types map[string]NamedType + + // TODO: Type extensions? + // http://facebook.github.io/graphql/draft/#sec-Type-Extensions + + // Directives are used to annotate various parts of a GraphQL document as an indicator that they + // should be evaluated differently by a validator, executor, or client tool such as a code + // generator. + // + // http://facebook.github.io/graphql/draft/#sec-Type-System.Directives + Directives map[string]*DirectiveDecl + + entryPointNames map[string]string + objects []*Object + unions []*Union + enums []*Enum +} + +// Resolve a named type in the schema by its name. +func (s *Schema) Resolve(name string) common.Type { + return s.Types[name] +} + +// NamedType represents a type with a name. +// +// http://facebook.github.io/graphql/draft/#NamedType +type NamedType interface { + common.Type + TypeName() string + Description() string +} + +// Scalar types represent primitive leaf values (e.g. a string or an integer) in a GraphQL type +// system. +// +// GraphQL responses take the form of a hierarchical tree; the leaves on these trees are GraphQL +// scalars. +// +// http://facebook.github.io/graphql/draft/#sec-Scalars +type Scalar struct { + Name string + Desc string + // TODO: Add a list of directives? +} + +// Object types represent a list of named fields, each of which yield a value of a specific type. +// +// GraphQL queries are hierarchical and composed, describing a tree of information. +// While Scalar types describe the leaf values of these hierarchical types, Objects describe the +// intermediate levels. +// +// http://facebook.github.io/graphql/draft/#sec-Objects +type Object struct { + Name string + Interfaces []*Interface + Fields FieldList + Desc string + // TODO: Add a list of directives? + + interfaceNames []string +} + +// Interface types represent a list of named fields and their arguments. +// +// GraphQL objects can then implement these interfaces which requires that the object type will +// define all fields defined by those interfaces. +// +// http://facebook.github.io/graphql/draft/#sec-Interfaces +type Interface struct { + Name string + PossibleTypes []*Object + Fields FieldList // NOTE: the spec refers to this as `FieldsDefinition`. + Desc string + // TODO: Add a list of directives? +} + +// Union types represent objects that could be one of a list of GraphQL object types, but provides no +// guaranteed fields between those types. +// +// They also differ from interfaces in that object types declare what interfaces they implement, but +// are not aware of what unions contain them. +// +// http://facebook.github.io/graphql/draft/#sec-Unions +type Union struct { + Name string + PossibleTypes []*Object // NOTE: the spec refers to this as `UnionMemberTypes`. + Desc string + // TODO: Add a list of directives? + + typeNames []string +} + +// Enum types describe a set of possible values. +// +// Like scalar types, Enum types also represent leaf values in a GraphQL type system. +// +// http://facebook.github.io/graphql/draft/#sec-Enums +type Enum struct { + Name string + Values []*EnumValue // NOTE: the spec refers to this as `EnumValuesDefinition`. + Desc string + // TODO: Add a list of directives? +} + +// EnumValue types are unique values that may be serialized as a string: the name of the +// represented value. +// +// http://facebook.github.io/graphql/draft/#EnumValueDefinition +type EnumValue struct { + Name string + Directives common.DirectiveList + Desc string + // TODO: Add a list of directives? +} + +// InputObject types define a set of input fields; the input fields are either scalars, enums, or +// other input objects. +// +// This allows arguments to accept arbitrarily complex structs. +// +// http://facebook.github.io/graphql/draft/#sec-Input-Objects +type InputObject struct { + Name string + Desc string + Values common.InputValueList + // TODO: Add a list of directives? +} + +// FieldsList is a list of an Object's Fields. +// +// http://facebook.github.io/graphql/draft/#FieldsDefinition +type FieldList []*Field + +// Get iterates over the field list, returning a pointer-to-Field when the field name matches the +// provided `name` argument. +// Returns nil when no field was found by that name. +func (l FieldList) Get(name string) *Field { + for _, f := range l { + if f.Name == name { + return f + } + } + return nil +} + +// Names returns a string slice of the field names in the FieldList. +func (l FieldList) Names() []string { + names := make([]string, len(l)) + for i, f := range l { + names[i] = f.Name + } + return names +} + +// http://facebook.github.io/graphql/draft/#sec-Type-System.Directives +type DirectiveDecl struct { + Name string + Desc string + Locs []string + Args common.InputValueList +} + +func (*Scalar) Kind() string { return "SCALAR" } +func (*Object) Kind() string { return "OBJECT" } +func (*Interface) Kind() string { return "INTERFACE" } +func (*Union) Kind() string { return "UNION" } +func (*Enum) Kind() string { return "ENUM" } +func (*InputObject) Kind() string { return "INPUT_OBJECT" } + +func (t *Scalar) String() string { return t.Name } +func (t *Object) String() string { return t.Name } +func (t *Interface) String() string { return t.Name } +func (t *Union) String() string { return t.Name } +func (t *Enum) String() string { return t.Name } +func (t *InputObject) String() string { return t.Name } + +func (t *Scalar) TypeName() string { return t.Name } +func (t *Object) TypeName() string { return t.Name } +func (t *Interface) TypeName() string { return t.Name } +func (t *Union) TypeName() string { return t.Name } +func (t *Enum) TypeName() string { return t.Name } +func (t *InputObject) TypeName() string { return t.Name } + +func (t *Scalar) Description() string { return t.Desc } +func (t *Object) Description() string { return t.Desc } +func (t *Interface) Description() string { return t.Desc } +func (t *Union) Description() string { return t.Desc } +func (t *Enum) Description() string { return t.Desc } +func (t *InputObject) Description() string { return t.Desc } + +// Field is a conceptual function which yields values. +// http://facebook.github.io/graphql/draft/#FieldDefinition +type Field struct { + Name string + Args common.InputValueList // NOTE: the spec refers to this as `ArgumentsDefinition`. + Type common.Type + Directives common.DirectiveList + Desc string +} + +// New initializes an instance of Schema. +func New() *Schema { + s := &Schema{ + entryPointNames: make(map[string]string), + Types: make(map[string]NamedType), + Directives: make(map[string]*DirectiveDecl), + } + for n, t := range Meta.Types { + s.Types[n] = t + } + for n, d := range Meta.Directives { + s.Directives[n] = d + } + return s +} + +// Parse the schema string. +func (s *Schema) Parse(schemaString string) error { + l := common.NewLexer(schemaString) + + err := l.CatchSyntaxError(func() { parseSchema(s, l) }) + if err != nil { + return err + } + + for _, t := range s.Types { + if err := resolveNamedType(s, t); err != nil { + return err + } + } + for _, d := range s.Directives { + for _, arg := range d.Args { + t, err := common.ResolveType(arg.Type, s.Resolve) + if err != nil { + return err + } + arg.Type = t + } + } + + s.EntryPoints = make(map[string]NamedType) + for key, name := range s.entryPointNames { + t, ok := s.Types[name] + if !ok { + if !ok { + return errors.Errorf("type %q not found", name) + } + } + s.EntryPoints[key] = t + } + + for _, obj := range s.objects { + obj.Interfaces = make([]*Interface, len(obj.interfaceNames)) + for i, intfName := range obj.interfaceNames { + t, ok := s.Types[intfName] + if !ok { + return errors.Errorf("interface %q not found", intfName) + } + intf, ok := t.(*Interface) + if !ok { + return errors.Errorf("type %q is not an interface", intfName) + } + obj.Interfaces[i] = intf + intf.PossibleTypes = append(intf.PossibleTypes, obj) + } + } + + for _, union := range s.unions { + union.PossibleTypes = make([]*Object, len(union.typeNames)) + for i, name := range union.typeNames { + t, ok := s.Types[name] + if !ok { + return errors.Errorf("object type %q not found", name) + } + obj, ok := t.(*Object) + if !ok { + return errors.Errorf("type %q is not an object", name) + } + union.PossibleTypes[i] = obj + } + } + + for _, enum := range s.enums { + for _, value := range enum.Values { + if err := resolveDirectives(s, value.Directives); err != nil { + return err + } + } + } + + return nil +} + +func resolveNamedType(s *Schema, t NamedType) error { + switch t := t.(type) { + case *Object: + for _, f := range t.Fields { + if err := resolveField(s, f); err != nil { + return err + } + } + case *Interface: + for _, f := range t.Fields { + if err := resolveField(s, f); err != nil { + return err + } + } + case *InputObject: + if err := resolveInputObject(s, t.Values); err != nil { + return err + } + } + return nil +} + +func resolveField(s *Schema, f *Field) error { + t, err := common.ResolveType(f.Type, s.Resolve) + if err != nil { + return err + } + f.Type = t + if err := resolveDirectives(s, f.Directives); err != nil { + return err + } + return resolveInputObject(s, f.Args) +} + +func resolveDirectives(s *Schema, directives common.DirectiveList) error { + for _, d := range directives { + dirName := d.Name.Name + dd, ok := s.Directives[dirName] + if !ok { + return errors.Errorf("directive %q not found", dirName) + } + for _, arg := range d.Args { + if dd.Args.Get(arg.Name.Name) == nil { + return errors.Errorf("invalid argument %q for directive %q", arg.Name.Name, dirName) + } + } + for _, arg := range dd.Args { + if _, ok := d.Args.Get(arg.Name.Name); !ok { + d.Args = append(d.Args, common.Argument{Name: arg.Name, Value: arg.Default}) + } + } + } + return nil +} + +func resolveInputObject(s *Schema, values common.InputValueList) error { + for _, v := range values { + t, err := common.ResolveType(v.Type, s.Resolve) + if err != nil { + return err + } + v.Type = t + } + return nil +} + +func parseSchema(s *Schema, l *common.Lexer) { + l.Consume() + + for l.Peek() != scanner.EOF { + desc := l.DescComment() + switch x := l.ConsumeIdent(); x { + + case "schema": + l.ConsumeToken('{') + for l.Peek() != '}' { + name := l.ConsumeIdent() + l.ConsumeToken(':') + typ := l.ConsumeIdent() + s.entryPointNames[name] = typ + } + l.ConsumeToken('}') + + case "type": + obj := parseObjectDef(l) + obj.Desc = desc + s.Types[obj.Name] = obj + s.objects = append(s.objects, obj) + + case "interface": + iface := parseInterfaceDef(l) + iface.Desc = desc + s.Types[iface.Name] = iface + + case "union": + union := parseUnionDef(l) + union.Desc = desc + s.Types[union.Name] = union + s.unions = append(s.unions, union) + + case "enum": + enum := parseEnumDef(l) + enum.Desc = desc + s.Types[enum.Name] = enum + s.enums = append(s.enums, enum) + + case "input": + input := parseInputDef(l) + input.Desc = desc + s.Types[input.Name] = input + + case "scalar": + name := l.ConsumeIdent() + s.Types[name] = &Scalar{Name: name, Desc: desc} + + case "directive": + directive := parseDirectiveDef(l) + directive.Desc = desc + s.Directives[directive.Name] = directive + + default: + // TODO: Add support for type extensions. + l.SyntaxError(fmt.Sprintf(`unexpected %q, expecting "schema", "type", "enum", "interface", "union", "input", "scalar" or "directive"`, x)) + } + } +} + +func parseObjectDef(l *common.Lexer) *Object { + object := &Object{Name: l.ConsumeIdent()} + + if l.Peek() == scanner.Ident { + l.ConsumeKeyword("implements") + + for l.Peek() != '{' { + if l.Peek() == '&' { + l.ConsumeToken('&') + } + + object.interfaceNames = append(object.interfaceNames, l.ConsumeIdent()) + } + } + + l.ConsumeToken('{') + object.Fields = parseFieldsDef(l) + l.ConsumeToken('}') + + return object +} + +func parseInterfaceDef(l *common.Lexer) *Interface { + i := &Interface{Name: l.ConsumeIdent()} + + l.ConsumeToken('{') + i.Fields = parseFieldsDef(l) + l.ConsumeToken('}') + + return i +} + +func parseUnionDef(l *common.Lexer) *Union { + union := &Union{Name: l.ConsumeIdent()} + + l.ConsumeToken('=') + union.typeNames = []string{l.ConsumeIdent()} + for l.Peek() == '|' { + l.ConsumeToken('|') + union.typeNames = append(union.typeNames, l.ConsumeIdent()) + } + + return union +} + +func parseInputDef(l *common.Lexer) *InputObject { + i := &InputObject{} + i.Name = l.ConsumeIdent() + l.ConsumeToken('{') + for l.Peek() != '}' { + i.Values = append(i.Values, common.ParseInputValue(l)) + } + l.ConsumeToken('}') + return i +} + +func parseEnumDef(l *common.Lexer) *Enum { + enum := &Enum{Name: l.ConsumeIdent()} + + l.ConsumeToken('{') + for l.Peek() != '}' { + v := &EnumValue{ + Desc: l.DescComment(), + Name: l.ConsumeIdent(), + Directives: common.ParseDirectives(l), + } + + enum.Values = append(enum.Values, v) + } + l.ConsumeToken('}') + return enum +} + +func parseDirectiveDef(l *common.Lexer) *DirectiveDecl { + l.ConsumeToken('@') + d := &DirectiveDecl{Name: l.ConsumeIdent()} + + if l.Peek() == '(' { + l.ConsumeToken('(') + for l.Peek() != ')' { + v := common.ParseInputValue(l) + d.Args = append(d.Args, v) + } + l.ConsumeToken(')') + } + + l.ConsumeKeyword("on") + + for { + loc := l.ConsumeIdent() + d.Locs = append(d.Locs, loc) + if l.Peek() != '|' { + break + } + l.ConsumeToken('|') + } + return d +} + +func parseFieldsDef(l *common.Lexer) FieldList { + var fields FieldList + for l.Peek() != '}' { + f := &Field{} + f.Desc = l.DescComment() + f.Name = l.ConsumeIdent() + if l.Peek() == '(' { + l.ConsumeToken('(') + for l.Peek() != ')' { + f.Args = append(f.Args, common.ParseInputValue(l)) + } + l.ConsumeToken(')') + } + l.ConsumeToken(':') + f.Type = common.ParseType(l) + f.Directives = common.ParseDirectives(l) + fields = append(fields, f) + } + return fields +} diff --git a/vendor/github.com/graph-gophers/graphql-go/internal/validation/suggestion.go b/vendor/github.com/graph-gophers/graphql-go/internal/validation/suggestion.go new file mode 100644 index 000000000000..9702b5f52947 --- /dev/null +++ b/vendor/github.com/graph-gophers/graphql-go/internal/validation/suggestion.go @@ -0,0 +1,71 @@ +package validation + +import ( + "fmt" + "sort" + "strconv" + "strings" +) + +func makeSuggestion(prefix string, options []string, input string) string { + var selected []string + distances := make(map[string]int) + for _, opt := range options { + distance := levenshteinDistance(input, opt) + threshold := max(len(input)/2, max(len(opt)/2, 1)) + if distance < threshold { + selected = append(selected, opt) + distances[opt] = distance + } + } + + if len(selected) == 0 { + return "" + } + sort.Slice(selected, func(i, j int) bool { + return distances[selected[i]] < distances[selected[j]] + }) + + parts := make([]string, len(selected)) + for i, opt := range selected { + parts[i] = strconv.Quote(opt) + } + if len(parts) > 1 { + parts[len(parts)-1] = "or " + parts[len(parts)-1] + } + return fmt.Sprintf(" %s %s?", prefix, strings.Join(parts, ", ")) +} + +func levenshteinDistance(s1, s2 string) int { + column := make([]int, len(s1)+1) + for y := range s1 { + column[y+1] = y + 1 + } + for x, rx := range s2 { + column[0] = x + 1 + lastdiag := x + for y, ry := range s1 { + olddiag := column[y+1] + if rx != ry { + lastdiag++ + } + column[y+1] = min(column[y+1]+1, min(column[y]+1, lastdiag)) + lastdiag = olddiag + } + } + return column[len(s1)] +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} diff --git a/vendor/github.com/graph-gophers/graphql-go/internal/validation/validation.go b/vendor/github.com/graph-gophers/graphql-go/internal/validation/validation.go new file mode 100644 index 000000000000..94ad5ca7fbac --- /dev/null +++ b/vendor/github.com/graph-gophers/graphql-go/internal/validation/validation.go @@ -0,0 +1,909 @@ +package validation + +import ( + "fmt" + "math" + "reflect" + "strconv" + "strings" + "text/scanner" + + "github.com/graph-gophers/graphql-go/errors" + "github.com/graph-gophers/graphql-go/internal/common" + "github.com/graph-gophers/graphql-go/internal/query" + "github.com/graph-gophers/graphql-go/internal/schema" +) + +type varSet map[*common.InputValue]struct{} + +type selectionPair struct{ a, b query.Selection } + +type fieldInfo struct { + sf *schema.Field + parent schema.NamedType +} + +type context struct { + schema *schema.Schema + doc *query.Document + errs []*errors.QueryError + opErrs map[*query.Operation][]*errors.QueryError + usedVars map[*query.Operation]varSet + fieldMap map[*query.Field]fieldInfo + overlapValidated map[selectionPair]struct{} + maxDepth int +} + +func (c *context) addErr(loc errors.Location, rule string, format string, a ...interface{}) { + c.addErrMultiLoc([]errors.Location{loc}, rule, format, a...) +} + +func (c *context) addErrMultiLoc(locs []errors.Location, rule string, format string, a ...interface{}) { + c.errs = append(c.errs, &errors.QueryError{ + Message: fmt.Sprintf(format, a...), + Locations: locs, + Rule: rule, + }) +} + +type opContext struct { + *context + ops []*query.Operation +} + +func newContext(s *schema.Schema, doc *query.Document, maxDepth int) *context { + return &context{ + schema: s, + doc: doc, + opErrs: make(map[*query.Operation][]*errors.QueryError), + usedVars: make(map[*query.Operation]varSet), + fieldMap: make(map[*query.Field]fieldInfo), + overlapValidated: make(map[selectionPair]struct{}), + maxDepth: maxDepth, + } +} + +func Validate(s *schema.Schema, doc *query.Document, maxDepth int) []*errors.QueryError { + c := newContext(s, doc, maxDepth) + + opNames := make(nameSet) + fragUsedBy := make(map[*query.FragmentDecl][]*query.Operation) + for _, op := range doc.Operations { + c.usedVars[op] = make(varSet) + opc := &opContext{c, []*query.Operation{op}} + + // Check if max depth is exceeded, if it's set. If max depth is exceeded, + // don't continue to validate the document and exit early. + if validateMaxDepth(opc, op.Selections, 1) { + return c.errs + } + + if op.Name.Name == "" && len(doc.Operations) != 1 { + c.addErr(op.Loc, "LoneAnonymousOperation", "This anonymous operation must be the only defined operation.") + } + if op.Name.Name != "" { + validateName(c, opNames, op.Name, "UniqueOperationNames", "operation") + } + + validateDirectives(opc, string(op.Type), op.Directives) + + varNames := make(nameSet) + for _, v := range op.Vars { + validateName(c, varNames, v.Name, "UniqueVariableNames", "variable") + + t := resolveType(c, v.Type) + if !canBeInput(t) { + c.addErr(v.TypeLoc, "VariablesAreInputTypes", "Variable %q cannot be non-input type %q.", "$"+v.Name.Name, t) + } + + if v.Default != nil { + validateLiteral(opc, v.Default) + + if t != nil { + if nn, ok := t.(*common.NonNull); ok { + c.addErr(v.Default.Location(), "DefaultValuesOfCorrectType", "Variable %q of type %q is required and will not use the default value. Perhaps you meant to use type %q.", "$"+v.Name.Name, t, nn.OfType) + } + + if ok, reason := validateValueType(opc, v.Default, t); !ok { + c.addErr(v.Default.Location(), "DefaultValuesOfCorrectType", "Variable %q of type %q has invalid default value %s.\n%s", "$"+v.Name.Name, t, v.Default, reason) + } + } + } + } + + var entryPoint schema.NamedType + switch op.Type { + case query.Query: + entryPoint = s.EntryPoints["query"] + case query.Mutation: + entryPoint = s.EntryPoints["mutation"] + case query.Subscription: + entryPoint = s.EntryPoints["subscription"] + default: + panic("unreachable") + } + + validateSelectionSet(opc, op.Selections, entryPoint) + + fragUsed := make(map[*query.FragmentDecl]struct{}) + markUsedFragments(c, op.Selections, fragUsed) + for frag := range fragUsed { + fragUsedBy[frag] = append(fragUsedBy[frag], op) + } + } + + fragNames := make(nameSet) + fragVisited := make(map[*query.FragmentDecl]struct{}) + for _, frag := range doc.Fragments { + opc := &opContext{c, fragUsedBy[frag]} + + validateName(c, fragNames, frag.Name, "UniqueFragmentNames", "fragment") + validateDirectives(opc, "FRAGMENT_DEFINITION", frag.Directives) + + t := unwrapType(resolveType(c, &frag.On)) + // continue even if t is nil + if t != nil && !canBeFragment(t) { + c.addErr(frag.On.Loc, "FragmentsOnCompositeTypes", "Fragment %q cannot condition on non composite type %q.", frag.Name.Name, t) + continue + } + + validateSelectionSet(opc, frag.Selections, t) + + if _, ok := fragVisited[frag]; !ok { + detectFragmentCycle(c, frag.Selections, fragVisited, nil, map[string]int{frag.Name.Name: 0}) + } + } + + for _, frag := range doc.Fragments { + if len(fragUsedBy[frag]) == 0 { + c.addErr(frag.Loc, "NoUnusedFragments", "Fragment %q is never used.", frag.Name.Name) + } + } + + for _, op := range doc.Operations { + c.errs = append(c.errs, c.opErrs[op]...) + + opUsedVars := c.usedVars[op] + for _, v := range op.Vars { + if _, ok := opUsedVars[v]; !ok { + opSuffix := "" + if op.Name.Name != "" { + opSuffix = fmt.Sprintf(" in operation %q", op.Name.Name) + } + c.addErr(v.Loc, "NoUnusedVariables", "Variable %q is never used%s.", "$"+v.Name.Name, opSuffix) + } + } + } + + return c.errs +} + +// validates the query doesn't go deeper than maxDepth (if set). Returns whether +// or not query validated max depth to avoid excessive recursion. +func validateMaxDepth(c *opContext, sels []query.Selection, depth int) bool { + // maxDepth checking is turned off when maxDepth is 0 + if c.maxDepth == 0 { + return false + } + + exceededMaxDepth := false + + for _, sel := range sels { + switch sel := sel.(type) { + case *query.Field: + if depth > c.maxDepth { + exceededMaxDepth = true + c.addErr(sel.Alias.Loc, "MaxDepthExceeded", "Field %q has depth %d that exceeds max depth %d", sel.Name.Name, depth, c.maxDepth) + continue + } + exceededMaxDepth = exceededMaxDepth || validateMaxDepth(c, sel.Selections, depth+1) + case *query.InlineFragment: + // Depth is not checked because inline fragments resolve to other fields which are checked. + // Depth is not incremented because inline fragments have the same depth as neighboring fields + exceededMaxDepth = exceededMaxDepth || validateMaxDepth(c, sel.Selections, depth) + case *query.FragmentSpread: + // Depth is not checked because fragments resolve to other fields which are checked. + frag := c.doc.Fragments.Get(sel.Name.Name) + if frag == nil { + // In case of unknown fragment (invalid request), ignore max depth evaluation + c.addErr(sel.Loc, "MaxDepthEvaluationError", "Unknown fragment %q. Unable to evaluate depth.", sel.Name.Name) + continue + } + // Depth is not incremented because fragments have the same depth as surrounding fields + exceededMaxDepth = exceededMaxDepth || validateMaxDepth(c, frag.Selections, depth) + } + } + + return exceededMaxDepth +} + +func validateSelectionSet(c *opContext, sels []query.Selection, t schema.NamedType) { + for _, sel := range sels { + validateSelection(c, sel, t) + } + + for i, a := range sels { + for _, b := range sels[i+1:] { + c.validateOverlap(a, b, nil, nil) + } + } +} + +func validateSelection(c *opContext, sel query.Selection, t schema.NamedType) { + switch sel := sel.(type) { + case *query.Field: + validateDirectives(c, "FIELD", sel.Directives) + + fieldName := sel.Name.Name + var f *schema.Field + switch fieldName { + case "__typename": + f = &schema.Field{ + Name: "__typename", + Type: c.schema.Types["String"], + } + case "__schema": + f = &schema.Field{ + Name: "__schema", + Type: c.schema.Types["__Schema"], + } + case "__type": + f = &schema.Field{ + Name: "__type", + Args: common.InputValueList{ + &common.InputValue{ + Name: common.Ident{Name: "name"}, + Type: &common.NonNull{OfType: c.schema.Types["String"]}, + }, + }, + Type: c.schema.Types["__Type"], + } + default: + f = fields(t).Get(fieldName) + if f == nil && t != nil { + suggestion := makeSuggestion("Did you mean", fields(t).Names(), fieldName) + c.addErr(sel.Alias.Loc, "FieldsOnCorrectType", "Cannot query field %q on type %q.%s", fieldName, t, suggestion) + } + } + c.fieldMap[sel] = fieldInfo{sf: f, parent: t} + + validateArgumentLiterals(c, sel.Arguments) + if f != nil { + validateArgumentTypes(c, sel.Arguments, f.Args, sel.Alias.Loc, + func() string { return fmt.Sprintf("field %q of type %q", fieldName, t) }, + func() string { return fmt.Sprintf("Field %q", fieldName) }, + ) + } + + var ft common.Type + if f != nil { + ft = f.Type + sf := hasSubfields(ft) + if sf && sel.Selections == nil { + c.addErr(sel.Alias.Loc, "ScalarLeafs", "Field %q of type %q must have a selection of subfields. Did you mean \"%s { ... }\"?", fieldName, ft, fieldName) + } + if !sf && sel.Selections != nil { + c.addErr(sel.SelectionSetLoc, "ScalarLeafs", "Field %q must not have a selection since type %q has no subfields.", fieldName, ft) + } + } + if sel.Selections != nil { + validateSelectionSet(c, sel.Selections, unwrapType(ft)) + } + + case *query.InlineFragment: + validateDirectives(c, "INLINE_FRAGMENT", sel.Directives) + if sel.On.Name != "" { + fragTyp := unwrapType(resolveType(c.context, &sel.On)) + if fragTyp != nil && !compatible(t, fragTyp) { + c.addErr(sel.Loc, "PossibleFragmentSpreads", "Fragment cannot be spread here as objects of type %q can never be of type %q.", t, fragTyp) + } + t = fragTyp + // continue even if t is nil + } + if t != nil && !canBeFragment(t) { + c.addErr(sel.On.Loc, "FragmentsOnCompositeTypes", "Fragment cannot condition on non composite type %q.", t) + return + } + validateSelectionSet(c, sel.Selections, unwrapType(t)) + + case *query.FragmentSpread: + validateDirectives(c, "FRAGMENT_SPREAD", sel.Directives) + frag := c.doc.Fragments.Get(sel.Name.Name) + if frag == nil { + c.addErr(sel.Name.Loc, "KnownFragmentNames", "Unknown fragment %q.", sel.Name.Name) + return + } + fragTyp := c.schema.Types[frag.On.Name] + if !compatible(t, fragTyp) { + c.addErr(sel.Loc, "PossibleFragmentSpreads", "Fragment %q cannot be spread here as objects of type %q can never be of type %q.", frag.Name.Name, t, fragTyp) + } + + default: + panic("unreachable") + } +} + +func compatible(a, b common.Type) bool { + for _, pta := range possibleTypes(a) { + for _, ptb := range possibleTypes(b) { + if pta == ptb { + return true + } + } + } + return false +} + +func possibleTypes(t common.Type) []*schema.Object { + switch t := t.(type) { + case *schema.Object: + return []*schema.Object{t} + case *schema.Interface: + return t.PossibleTypes + case *schema.Union: + return t.PossibleTypes + default: + return nil + } +} + +func markUsedFragments(c *context, sels []query.Selection, fragUsed map[*query.FragmentDecl]struct{}) { + for _, sel := range sels { + switch sel := sel.(type) { + case *query.Field: + if sel.Selections != nil { + markUsedFragments(c, sel.Selections, fragUsed) + } + + case *query.InlineFragment: + markUsedFragments(c, sel.Selections, fragUsed) + + case *query.FragmentSpread: + frag := c.doc.Fragments.Get(sel.Name.Name) + if frag == nil { + return + } + + if _, ok := fragUsed[frag]; ok { + return + } + fragUsed[frag] = struct{}{} + markUsedFragments(c, frag.Selections, fragUsed) + + default: + panic("unreachable") + } + } +} + +func detectFragmentCycle(c *context, sels []query.Selection, fragVisited map[*query.FragmentDecl]struct{}, spreadPath []*query.FragmentSpread, spreadPathIndex map[string]int) { + for _, sel := range sels { + detectFragmentCycleSel(c, sel, fragVisited, spreadPath, spreadPathIndex) + } +} + +func detectFragmentCycleSel(c *context, sel query.Selection, fragVisited map[*query.FragmentDecl]struct{}, spreadPath []*query.FragmentSpread, spreadPathIndex map[string]int) { + switch sel := sel.(type) { + case *query.Field: + if sel.Selections != nil { + detectFragmentCycle(c, sel.Selections, fragVisited, spreadPath, spreadPathIndex) + } + + case *query.InlineFragment: + detectFragmentCycle(c, sel.Selections, fragVisited, spreadPath, spreadPathIndex) + + case *query.FragmentSpread: + frag := c.doc.Fragments.Get(sel.Name.Name) + if frag == nil { + return + } + + spreadPath = append(spreadPath, sel) + if i, ok := spreadPathIndex[frag.Name.Name]; ok { + cyclePath := spreadPath[i:] + via := "" + if len(cyclePath) > 1 { + names := make([]string, len(cyclePath)-1) + for i, frag := range cyclePath[:len(cyclePath)-1] { + names[i] = frag.Name.Name + } + via = " via " + strings.Join(names, ", ") + } + + locs := make([]errors.Location, len(cyclePath)) + for i, frag := range cyclePath { + locs[i] = frag.Loc + } + c.addErrMultiLoc(locs, "NoFragmentCycles", "Cannot spread fragment %q within itself%s.", frag.Name.Name, via) + return + } + + if _, ok := fragVisited[frag]; ok { + return + } + fragVisited[frag] = struct{}{} + + spreadPathIndex[frag.Name.Name] = len(spreadPath) + detectFragmentCycle(c, frag.Selections, fragVisited, spreadPath, spreadPathIndex) + delete(spreadPathIndex, frag.Name.Name) + + default: + panic("unreachable") + } +} + +func (c *context) validateOverlap(a, b query.Selection, reasons *[]string, locs *[]errors.Location) { + if a == b { + return + } + + if _, ok := c.overlapValidated[selectionPair{a, b}]; ok { + return + } + c.overlapValidated[selectionPair{a, b}] = struct{}{} + c.overlapValidated[selectionPair{b, a}] = struct{}{} + + switch a := a.(type) { + case *query.Field: + switch b := b.(type) { + case *query.Field: + if b.Alias.Loc.Before(a.Alias.Loc) { + a, b = b, a + } + if reasons2, locs2 := c.validateFieldOverlap(a, b); len(reasons2) != 0 { + locs2 = append(locs2, a.Alias.Loc, b.Alias.Loc) + if reasons == nil { + c.addErrMultiLoc(locs2, "OverlappingFieldsCanBeMerged", "Fields %q conflict because %s. Use different aliases on the fields to fetch both if this was intentional.", a.Alias.Name, strings.Join(reasons2, " and ")) + return + } + for _, r := range reasons2 { + *reasons = append(*reasons, fmt.Sprintf("subfields %q conflict because %s", a.Alias.Name, r)) + } + *locs = append(*locs, locs2...) + } + + case *query.InlineFragment: + for _, sel := range b.Selections { + c.validateOverlap(a, sel, reasons, locs) + } + + case *query.FragmentSpread: + if frag := c.doc.Fragments.Get(b.Name.Name); frag != nil { + for _, sel := range frag.Selections { + c.validateOverlap(a, sel, reasons, locs) + } + } + + default: + panic("unreachable") + } + + case *query.InlineFragment: + for _, sel := range a.Selections { + c.validateOverlap(sel, b, reasons, locs) + } + + case *query.FragmentSpread: + if frag := c.doc.Fragments.Get(a.Name.Name); frag != nil { + for _, sel := range frag.Selections { + c.validateOverlap(sel, b, reasons, locs) + } + } + + default: + panic("unreachable") + } +} + +func (c *context) validateFieldOverlap(a, b *query.Field) ([]string, []errors.Location) { + if a.Alias.Name != b.Alias.Name { + return nil, nil + } + + if asf := c.fieldMap[a].sf; asf != nil { + if bsf := c.fieldMap[b].sf; bsf != nil { + if !typesCompatible(asf.Type, bsf.Type) { + return []string{fmt.Sprintf("they return conflicting types %s and %s", asf.Type, bsf.Type)}, nil + } + } + } + + at := c.fieldMap[a].parent + bt := c.fieldMap[b].parent + if at == nil || bt == nil || at == bt { + if a.Name.Name != b.Name.Name { + return []string{fmt.Sprintf("%s and %s are different fields", a.Name.Name, b.Name.Name)}, nil + } + + if argumentsConflict(a.Arguments, b.Arguments) { + return []string{"they have differing arguments"}, nil + } + } + + var reasons []string + var locs []errors.Location + for _, a2 := range a.Selections { + for _, b2 := range b.Selections { + c.validateOverlap(a2, b2, &reasons, &locs) + } + } + return reasons, locs +} + +func argumentsConflict(a, b common.ArgumentList) bool { + if len(a) != len(b) { + return true + } + for _, argA := range a { + valB, ok := b.Get(argA.Name.Name) + if !ok || !reflect.DeepEqual(argA.Value.Value(nil), valB.Value(nil)) { + return true + } + } + return false +} + +func fields(t common.Type) schema.FieldList { + switch t := t.(type) { + case *schema.Object: + return t.Fields + case *schema.Interface: + return t.Fields + default: + return nil + } +} + +func unwrapType(t common.Type) schema.NamedType { + if t == nil { + return nil + } + for { + switch t2 := t.(type) { + case schema.NamedType: + return t2 + case *common.List: + t = t2.OfType + case *common.NonNull: + t = t2.OfType + default: + panic("unreachable") + } + } +} + +func resolveType(c *context, t common.Type) common.Type { + t2, err := common.ResolveType(t, c.schema.Resolve) + if err != nil { + c.errs = append(c.errs, err) + } + return t2 +} + +func validateDirectives(c *opContext, loc string, directives common.DirectiveList) { + directiveNames := make(nameSet) + for _, d := range directives { + dirName := d.Name.Name + validateNameCustomMsg(c.context, directiveNames, d.Name, "UniqueDirectivesPerLocation", func() string { + return fmt.Sprintf("The directive %q can only be used once at this location.", dirName) + }) + + validateArgumentLiterals(c, d.Args) + + dd, ok := c.schema.Directives[dirName] + if !ok { + c.addErr(d.Name.Loc, "KnownDirectives", "Unknown directive %q.", dirName) + continue + } + + locOK := false + for _, allowedLoc := range dd.Locs { + if loc == allowedLoc { + locOK = true + break + } + } + if !locOK { + c.addErr(d.Name.Loc, "KnownDirectives", "Directive %q may not be used on %s.", dirName, loc) + } + + validateArgumentTypes(c, d.Args, dd.Args, d.Name.Loc, + func() string { return fmt.Sprintf("directive %q", "@"+dirName) }, + func() string { return fmt.Sprintf("Directive %q", "@"+dirName) }, + ) + } +} + +type nameSet map[string]errors.Location + +func validateName(c *context, set nameSet, name common.Ident, rule string, kind string) { + validateNameCustomMsg(c, set, name, rule, func() string { + return fmt.Sprintf("There can be only one %s named %q.", kind, name.Name) + }) +} + +func validateNameCustomMsg(c *context, set nameSet, name common.Ident, rule string, msg func() string) { + if loc, ok := set[name.Name]; ok { + c.addErrMultiLoc([]errors.Location{loc, name.Loc}, rule, msg()) + return + } + set[name.Name] = name.Loc +} + +func validateArgumentTypes(c *opContext, args common.ArgumentList, argDecls common.InputValueList, loc errors.Location, owner1, owner2 func() string) { + for _, selArg := range args { + arg := argDecls.Get(selArg.Name.Name) + if arg == nil { + c.addErr(selArg.Name.Loc, "KnownArgumentNames", "Unknown argument %q on %s.", selArg.Name.Name, owner1()) + continue + } + value := selArg.Value + if ok, reason := validateValueType(c, value, arg.Type); !ok { + c.addErr(value.Location(), "ArgumentsOfCorrectType", "Argument %q has invalid value %s.\n%s", arg.Name.Name, value, reason) + } + } + for _, decl := range argDecls { + if _, ok := decl.Type.(*common.NonNull); ok { + if _, ok := args.Get(decl.Name.Name); !ok { + c.addErr(loc, "ProvidedNonNullArguments", "%s argument %q of type %q is required but not provided.", owner2(), decl.Name.Name, decl.Type) + } + } + } +} + +func validateArgumentLiterals(c *opContext, args common.ArgumentList) { + argNames := make(nameSet) + for _, arg := range args { + validateName(c.context, argNames, arg.Name, "UniqueArgumentNames", "argument") + validateLiteral(c, arg.Value) + } +} + +func validateLiteral(c *opContext, l common.Literal) { + switch l := l.(type) { + case *common.ObjectLit: + fieldNames := make(nameSet) + for _, f := range l.Fields { + validateName(c.context, fieldNames, f.Name, "UniqueInputFieldNames", "input field") + validateLiteral(c, f.Value) + } + case *common.ListLit: + for _, entry := range l.Entries { + validateLiteral(c, entry) + } + case *common.Variable: + for _, op := range c.ops { + v := op.Vars.Get(l.Name) + if v == nil { + byOp := "" + if op.Name.Name != "" { + byOp = fmt.Sprintf(" by operation %q", op.Name.Name) + } + c.opErrs[op] = append(c.opErrs[op], &errors.QueryError{ + Message: fmt.Sprintf("Variable %q is not defined%s.", "$"+l.Name, byOp), + Locations: []errors.Location{l.Loc, op.Loc}, + Rule: "NoUndefinedVariables", + }) + continue + } + c.usedVars[op][v] = struct{}{} + } + } +} + +func validateValueType(c *opContext, v common.Literal, t common.Type) (bool, string) { + if v, ok := v.(*common.Variable); ok { + for _, op := range c.ops { + if v2 := op.Vars.Get(v.Name); v2 != nil { + t2, err := common.ResolveType(v2.Type, c.schema.Resolve) + if _, ok := t2.(*common.NonNull); !ok && v2.Default != nil { + t2 = &common.NonNull{OfType: t2} + } + if err == nil && !typeCanBeUsedAs(t2, t) { + c.addErrMultiLoc([]errors.Location{v2.Loc, v.Loc}, "VariablesInAllowedPosition", "Variable %q of type %q used in position expecting type %q.", "$"+v.Name, t2, t) + } + } + } + return true, "" + } + + if nn, ok := t.(*common.NonNull); ok { + if isNull(v) { + return false, fmt.Sprintf("Expected %q, found null.", t) + } + t = nn.OfType + } + if isNull(v) { + return true, "" + } + + switch t := t.(type) { + case *schema.Scalar, *schema.Enum: + if lit, ok := v.(*common.BasicLit); ok { + if validateBasicLit(lit, t) { + return true, "" + } + } + + case *common.List: + list, ok := v.(*common.ListLit) + if !ok { + return validateValueType(c, v, t.OfType) // single value instead of list + } + for i, entry := range list.Entries { + if ok, reason := validateValueType(c, entry, t.OfType); !ok { + return false, fmt.Sprintf("In element #%d: %s", i, reason) + } + } + return true, "" + + case *schema.InputObject: + v, ok := v.(*common.ObjectLit) + if !ok { + return false, fmt.Sprintf("Expected %q, found not an object.", t) + } + for _, f := range v.Fields { + name := f.Name.Name + iv := t.Values.Get(name) + if iv == nil { + return false, fmt.Sprintf("In field %q: Unknown field.", name) + } + if ok, reason := validateValueType(c, f.Value, iv.Type); !ok { + return false, fmt.Sprintf("In field %q: %s", name, reason) + } + } + for _, iv := range t.Values { + found := false + for _, f := range v.Fields { + if f.Name.Name == iv.Name.Name { + found = true + break + } + } + if !found { + if _, ok := iv.Type.(*common.NonNull); ok && iv.Default == nil { + return false, fmt.Sprintf("In field %q: Expected %q, found null.", iv.Name.Name, iv.Type) + } + } + } + return true, "" + } + + return false, fmt.Sprintf("Expected type %q, found %s.", t, v) +} + +func validateBasicLit(v *common.BasicLit, t common.Type) bool { + switch t := t.(type) { + case *schema.Scalar: + switch t.Name { + case "Int": + if v.Type != scanner.Int { + return false + } + f, err := strconv.ParseFloat(v.Text, 64) + if err != nil { + panic(err) + } + return f >= math.MinInt32 && f <= math.MaxInt32 + case "Float": + return v.Type == scanner.Int || v.Type == scanner.Float + case "String": + return v.Type == scanner.String + case "Boolean": + return v.Type == scanner.Ident && (v.Text == "true" || v.Text == "false") + case "ID": + return v.Type == scanner.Int || v.Type == scanner.String + default: + //TODO: Type-check against expected type by Unmarshalling + return true + } + + case *schema.Enum: + if v.Type != scanner.Ident { + return false + } + for _, option := range t.Values { + if option.Name == v.Text { + return true + } + } + return false + } + + return false +} + +func canBeFragment(t common.Type) bool { + switch t.(type) { + case *schema.Object, *schema.Interface, *schema.Union: + return true + default: + return false + } +} + +func canBeInput(t common.Type) bool { + switch t := t.(type) { + case *schema.InputObject, *schema.Scalar, *schema.Enum: + return true + case *common.List: + return canBeInput(t.OfType) + case *common.NonNull: + return canBeInput(t.OfType) + default: + return false + } +} + +func hasSubfields(t common.Type) bool { + switch t := t.(type) { + case *schema.Object, *schema.Interface, *schema.Union: + return true + case *common.List: + return hasSubfields(t.OfType) + case *common.NonNull: + return hasSubfields(t.OfType) + default: + return false + } +} + +func isLeaf(t common.Type) bool { + switch t.(type) { + case *schema.Scalar, *schema.Enum: + return true + default: + return false + } +} + +func isNull(lit interface{}) bool { + _, ok := lit.(*common.NullLit) + return ok +} + +func typesCompatible(a, b common.Type) bool { + al, aIsList := a.(*common.List) + bl, bIsList := b.(*common.List) + if aIsList || bIsList { + return aIsList && bIsList && typesCompatible(al.OfType, bl.OfType) + } + + ann, aIsNN := a.(*common.NonNull) + bnn, bIsNN := b.(*common.NonNull) + if aIsNN || bIsNN { + return aIsNN && bIsNN && typesCompatible(ann.OfType, bnn.OfType) + } + + if isLeaf(a) || isLeaf(b) { + return a == b + } + + return true +} + +func typeCanBeUsedAs(t, as common.Type) bool { + nnT, okT := t.(*common.NonNull) + if okT { + t = nnT.OfType + } + + nnAs, okAs := as.(*common.NonNull) + if okAs { + as = nnAs.OfType + if !okT { + return false // nullable can not be used as non-null + } + } + + if t == as { + return true + } + + if lT, ok := t.(*common.List); ok { + if lAs, ok := as.(*common.List); ok { + return typeCanBeUsedAs(lT.OfType, lAs.OfType) + } + } + return false +} diff --git a/vendor/github.com/graph-gophers/graphql-go/introspection.go b/vendor/github.com/graph-gophers/graphql-go/introspection.go new file mode 100644 index 000000000000..7e515cf25f4b --- /dev/null +++ b/vendor/github.com/graph-gophers/graphql-go/introspection.go @@ -0,0 +1,117 @@ +package graphql + +import ( + "context" + "encoding/json" + + "github.com/graph-gophers/graphql-go/internal/exec/resolvable" + "github.com/graph-gophers/graphql-go/introspection" +) + +// Inspect allows inspection of the given schema. +func (s *Schema) Inspect() *introspection.Schema { + return introspection.WrapSchema(s.schema) +} + +// ToJSON encodes the schema in a JSON format used by tools like Relay. +func (s *Schema) ToJSON() ([]byte, error) { + result := s.exec(context.Background(), introspectionQuery, "", nil, &resolvable.Schema{ + Query: &resolvable.Object{}, + Schema: *s.schema, + }) + if len(result.Errors) != 0 { + panic(result.Errors[0]) + } + return json.MarshalIndent(result.Data, "", "\t") +} + +var introspectionQuery = ` + query { + __schema { + queryType { name } + mutationType { name } + subscriptionType { name } + types { + ...FullType + } + directives { + name + description + locations + args { + ...InputValue + } + } + } + } + fragment FullType on __Type { + kind + name + description + fields(includeDeprecated: true) { + name + description + args { + ...InputValue + } + type { + ...TypeRef + } + isDeprecated + deprecationReason + } + inputFields { + ...InputValue + } + interfaces { + ...TypeRef + } + enumValues(includeDeprecated: true) { + name + description + isDeprecated + deprecationReason + } + possibleTypes { + ...TypeRef + } + } + fragment InputValue on __InputValue { + name + description + type { ...TypeRef } + defaultValue + } + fragment TypeRef on __Type { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + } + } + } + } + } + } +` diff --git a/vendor/github.com/graph-gophers/graphql-go/introspection/introspection.go b/vendor/github.com/graph-gophers/graphql-go/introspection/introspection.go new file mode 100644 index 000000000000..2f4acad0a689 --- /dev/null +++ b/vendor/github.com/graph-gophers/graphql-go/introspection/introspection.go @@ -0,0 +1,313 @@ +package introspection + +import ( + "sort" + + "github.com/graph-gophers/graphql-go/internal/common" + "github.com/graph-gophers/graphql-go/internal/schema" +) + +type Schema struct { + schema *schema.Schema +} + +// WrapSchema is only used internally. +func WrapSchema(schema *schema.Schema) *Schema { + return &Schema{schema} +} + +func (r *Schema) Types() []*Type { + var names []string + for name := range r.schema.Types { + names = append(names, name) + } + sort.Strings(names) + + l := make([]*Type, len(names)) + for i, name := range names { + l[i] = &Type{r.schema.Types[name]} + } + return l +} + +func (r *Schema) Directives() []*Directive { + var names []string + for name := range r.schema.Directives { + names = append(names, name) + } + sort.Strings(names) + + l := make([]*Directive, len(names)) + for i, name := range names { + l[i] = &Directive{r.schema.Directives[name]} + } + return l +} + +func (r *Schema) QueryType() *Type { + t, ok := r.schema.EntryPoints["query"] + if !ok { + return nil + } + return &Type{t} +} + +func (r *Schema) MutationType() *Type { + t, ok := r.schema.EntryPoints["mutation"] + if !ok { + return nil + } + return &Type{t} +} + +func (r *Schema) SubscriptionType() *Type { + t, ok := r.schema.EntryPoints["subscription"] + if !ok { + return nil + } + return &Type{t} +} + +type Type struct { + typ common.Type +} + +// WrapType is only used internally. +func WrapType(typ common.Type) *Type { + return &Type{typ} +} + +func (r *Type) Kind() string { + return r.typ.Kind() +} + +func (r *Type) Name() *string { + if named, ok := r.typ.(schema.NamedType); ok { + name := named.TypeName() + return &name + } + return nil +} + +func (r *Type) Description() *string { + if named, ok := r.typ.(schema.NamedType); ok { + desc := named.Description() + if desc == "" { + return nil + } + return &desc + } + return nil +} + +func (r *Type) Fields(args *struct{ IncludeDeprecated bool }) *[]*Field { + var fields schema.FieldList + switch t := r.typ.(type) { + case *schema.Object: + fields = t.Fields + case *schema.Interface: + fields = t.Fields + default: + return nil + } + + var l []*Field + for _, f := range fields { + if d := f.Directives.Get("deprecated"); d == nil || args.IncludeDeprecated { + l = append(l, &Field{f}) + } + } + return &l +} + +func (r *Type) Interfaces() *[]*Type { + t, ok := r.typ.(*schema.Object) + if !ok { + return nil + } + + l := make([]*Type, len(t.Interfaces)) + for i, intf := range t.Interfaces { + l[i] = &Type{intf} + } + return &l +} + +func (r *Type) PossibleTypes() *[]*Type { + var possibleTypes []*schema.Object + switch t := r.typ.(type) { + case *schema.Interface: + possibleTypes = t.PossibleTypes + case *schema.Union: + possibleTypes = t.PossibleTypes + default: + return nil + } + + l := make([]*Type, len(possibleTypes)) + for i, intf := range possibleTypes { + l[i] = &Type{intf} + } + return &l +} + +func (r *Type) EnumValues(args *struct{ IncludeDeprecated bool }) *[]*EnumValue { + t, ok := r.typ.(*schema.Enum) + if !ok { + return nil + } + + var l []*EnumValue + for _, v := range t.Values { + if d := v.Directives.Get("deprecated"); d == nil || args.IncludeDeprecated { + l = append(l, &EnumValue{v}) + } + } + return &l +} + +func (r *Type) InputFields() *[]*InputValue { + t, ok := r.typ.(*schema.InputObject) + if !ok { + return nil + } + + l := make([]*InputValue, len(t.Values)) + for i, v := range t.Values { + l[i] = &InputValue{v} + } + return &l +} + +func (r *Type) OfType() *Type { + switch t := r.typ.(type) { + case *common.List: + return &Type{t.OfType} + case *common.NonNull: + return &Type{t.OfType} + default: + return nil + } +} + +type Field struct { + field *schema.Field +} + +func (r *Field) Name() string { + return r.field.Name +} + +func (r *Field) Description() *string { + if r.field.Desc == "" { + return nil + } + return &r.field.Desc +} + +func (r *Field) Args() []*InputValue { + l := make([]*InputValue, len(r.field.Args)) + for i, v := range r.field.Args { + l[i] = &InputValue{v} + } + return l +} + +func (r *Field) Type() *Type { + return &Type{r.field.Type} +} + +func (r *Field) IsDeprecated() bool { + return r.field.Directives.Get("deprecated") != nil +} + +func (r *Field) DeprecationReason() *string { + d := r.field.Directives.Get("deprecated") + if d == nil { + return nil + } + reason := d.Args.MustGet("reason").Value(nil).(string) + return &reason +} + +type InputValue struct { + value *common.InputValue +} + +func (r *InputValue) Name() string { + return r.value.Name.Name +} + +func (r *InputValue) Description() *string { + if r.value.Desc == "" { + return nil + } + return &r.value.Desc +} + +func (r *InputValue) Type() *Type { + return &Type{r.value.Type} +} + +func (r *InputValue) DefaultValue() *string { + if r.value.Default == nil { + return nil + } + s := r.value.Default.String() + return &s +} + +type EnumValue struct { + value *schema.EnumValue +} + +func (r *EnumValue) Name() string { + return r.value.Name +} + +func (r *EnumValue) Description() *string { + if r.value.Desc == "" { + return nil + } + return &r.value.Desc +} + +func (r *EnumValue) IsDeprecated() bool { + return r.value.Directives.Get("deprecated") != nil +} + +func (r *EnumValue) DeprecationReason() *string { + d := r.value.Directives.Get("deprecated") + if d == nil { + return nil + } + reason := d.Args.MustGet("reason").Value(nil).(string) + return &reason +} + +type Directive struct { + directive *schema.DirectiveDecl +} + +func (r *Directive) Name() string { + return r.directive.Name +} + +func (r *Directive) Description() *string { + if r.directive.Desc == "" { + return nil + } + return &r.directive.Desc +} + +func (r *Directive) Locations() []string { + return r.directive.Locs +} + +func (r *Directive) Args() []*InputValue { + l := make([]*InputValue, len(r.directive.Args)) + for i, v := range r.directive.Args { + l[i] = &InputValue{v} + } + return l +} diff --git a/vendor/github.com/graph-gophers/graphql-go/log/log.go b/vendor/github.com/graph-gophers/graphql-go/log/log.go new file mode 100644 index 000000000000..25569af7cacf --- /dev/null +++ b/vendor/github.com/graph-gophers/graphql-go/log/log.go @@ -0,0 +1,23 @@ +package log + +import ( + "context" + "log" + "runtime" +) + +// Logger is the interface used to log panics that occur during query execution. It is settable via graphql.ParseSchema +type Logger interface { + LogPanic(ctx context.Context, value interface{}) +} + +// DefaultLogger is the default logger used to log panics that occur during query execution +type DefaultLogger struct{} + +// LogPanic is used to log recovered panic values that occur during query execution +func (l *DefaultLogger) LogPanic(_ context.Context, value interface{}) { + const size = 64 << 10 + buf := make([]byte, size) + buf = buf[:runtime.Stack(buf, false)] + log.Printf("graphql: panic occurred: %v\n%s", value, buf) +} diff --git a/vendor/github.com/graph-gophers/graphql-go/relay/relay.go b/vendor/github.com/graph-gophers/graphql-go/relay/relay.go new file mode 100644 index 000000000000..78e4dfdd51d5 --- /dev/null +++ b/vendor/github.com/graph-gophers/graphql-go/relay/relay.go @@ -0,0 +1,70 @@ +package relay + +import ( + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + + graphql "github.com/graph-gophers/graphql-go" +) + +func MarshalID(kind string, spec interface{}) graphql.ID { + d, err := json.Marshal(spec) + if err != nil { + panic(fmt.Errorf("relay.MarshalID: %s", err)) + } + return graphql.ID(base64.URLEncoding.EncodeToString(append([]byte(kind+":"), d...))) +} + +func UnmarshalKind(id graphql.ID) string { + s, err := base64.URLEncoding.DecodeString(string(id)) + if err != nil { + return "" + } + i := strings.IndexByte(string(s), ':') + if i == -1 { + return "" + } + return string(s[:i]) +} + +func UnmarshalSpec(id graphql.ID, v interface{}) error { + s, err := base64.URLEncoding.DecodeString(string(id)) + if err != nil { + return err + } + i := strings.IndexByte(string(s), ':') + if i == -1 { + return errors.New("invalid graphql.ID") + } + return json.Unmarshal([]byte(s[i+1:]), v) +} + +type Handler struct { + Schema *graphql.Schema +} + +func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + var params struct { + Query string `json:"query"` + OperationName string `json:"operationName"` + Variables map[string]interface{} `json:"variables"` + } + if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + response := h.Schema.Exec(r.Context(), params.Query, params.OperationName, params.Variables) + responseJSON, err := json.Marshal(response) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.Write(responseJSON) +} diff --git a/vendor/github.com/graph-gophers/graphql-go/time.go b/vendor/github.com/graph-gophers/graphql-go/time.go new file mode 100644 index 000000000000..829c502275d7 --- /dev/null +++ b/vendor/github.com/graph-gophers/graphql-go/time.go @@ -0,0 +1,51 @@ +package graphql + +import ( + "encoding/json" + "fmt" + "time" +) + +// Time is a custom GraphQL type to represent an instant in time. It has to be added to a schema +// via "scalar Time" since it is not a predeclared GraphQL type like "ID". +type Time struct { + time.Time +} + +// ImplementsGraphQLType maps this custom Go type +// to the graphql scalar type in the schema. +func (Time) ImplementsGraphQLType(name string) bool { + return name == "Time" +} + +// UnmarshalGraphQL is a custom unmarshaler for Time +// +// This function will be called whenever you use the +// time scalar as an input +func (t *Time) UnmarshalGraphQL(input interface{}) error { + switch input := input.(type) { + case time.Time: + t.Time = input + return nil + case string: + var err error + t.Time, err = time.Parse(time.RFC3339, input) + return err + case int: + t.Time = time.Unix(int64(input), 0) + return nil + case float64: + t.Time = time.Unix(int64(input), 0) + return nil + default: + return fmt.Errorf("wrong type") + } +} + +// MarshalJSON is a custom marshaler for Time +// +// This function will be called whenever you +// query for fields that use the Time type +func (t Time) MarshalJSON() ([]byte, error) { + return json.Marshal(t.Time) +} diff --git a/vendor/github.com/graph-gophers/graphql-go/trace/trace.go b/vendor/github.com/graph-gophers/graphql-go/trace/trace.go new file mode 100644 index 000000000000..68b856ae7199 --- /dev/null +++ b/vendor/github.com/graph-gophers/graphql-go/trace/trace.go @@ -0,0 +1,80 @@ +package trace + +import ( + "context" + "fmt" + + "github.com/graph-gophers/graphql-go/errors" + "github.com/graph-gophers/graphql-go/introspection" + opentracing "github.com/opentracing/opentracing-go" + "github.com/opentracing/opentracing-go/ext" + "github.com/opentracing/opentracing-go/log" +) + +type TraceQueryFinishFunc func([]*errors.QueryError) +type TraceFieldFinishFunc func(*errors.QueryError) + +type Tracer interface { + TraceQuery(ctx context.Context, queryString string, operationName string, variables map[string]interface{}, varTypes map[string]*introspection.Type) (context.Context, TraceQueryFinishFunc) + TraceField(ctx context.Context, label, typeName, fieldName string, trivial bool, args map[string]interface{}) (context.Context, TraceFieldFinishFunc) +} + +type OpenTracingTracer struct{} + +func (OpenTracingTracer) TraceQuery(ctx context.Context, queryString string, operationName string, variables map[string]interface{}, varTypes map[string]*introspection.Type) (context.Context, TraceQueryFinishFunc) { + span, spanCtx := opentracing.StartSpanFromContext(ctx, "GraphQL request") + span.SetTag("graphql.query", queryString) + + if operationName != "" { + span.SetTag("graphql.operationName", operationName) + } + + if len(variables) != 0 { + span.LogFields(log.Object("graphql.variables", variables)) + } + + return spanCtx, func(errs []*errors.QueryError) { + if len(errs) > 0 { + msg := errs[0].Error() + if len(errs) > 1 { + msg += fmt.Sprintf(" (and %d more errors)", len(errs)-1) + } + ext.Error.Set(span, true) + span.SetTag("graphql.error", msg) + } + span.Finish() + } +} + +func (OpenTracingTracer) TraceField(ctx context.Context, label, typeName, fieldName string, trivial bool, args map[string]interface{}) (context.Context, TraceFieldFinishFunc) { + if trivial { + return ctx, noop + } + + span, spanCtx := opentracing.StartSpanFromContext(ctx, label) + span.SetTag("graphql.type", typeName) + span.SetTag("graphql.field", fieldName) + for name, value := range args { + span.SetTag("graphql.args."+name, value) + } + + return spanCtx, func(err *errors.QueryError) { + if err != nil { + ext.Error.Set(span, true) + span.SetTag("graphql.error", err.Error()) + } + span.Finish() + } +} + +func noop(*errors.QueryError) {} + +type NoopTracer struct{} + +func (NoopTracer) TraceQuery(ctx context.Context, queryString string, operationName string, variables map[string]interface{}, varTypes map[string]*introspection.Type) (context.Context, TraceQueryFinishFunc) { + return ctx, func(errs []*errors.QueryError) {} +} + +func (NoopTracer) TraceField(ctx context.Context, label, typeName, fieldName string, trivial bool, args map[string]interface{}) (context.Context, TraceFieldFinishFunc) { + return ctx, func(err *errors.QueryError) {} +} diff --git a/vendor/github.com/graph-gophers/graphql-go/trace/validation_trace.go b/vendor/github.com/graph-gophers/graphql-go/trace/validation_trace.go new file mode 100644 index 000000000000..e223f0fdbc9c --- /dev/null +++ b/vendor/github.com/graph-gophers/graphql-go/trace/validation_trace.go @@ -0,0 +1,17 @@ +package trace + +import ( + "github.com/graph-gophers/graphql-go/errors" +) + +type TraceValidationFinishFunc = TraceQueryFinishFunc + +type ValidationTracer interface { + TraceValidation() TraceValidationFinishFunc +} + +type NoopValidationTracer struct{} + +func (NoopValidationTracer) TraceValidation() TraceValidationFinishFunc { + return func(errs []*errors.QueryError) {} +} From e0f4d7e9e5e71c7c27f4125d279c96ceaea97231 Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Sun, 14 Oct 2018 11:04:29 +0100 Subject: [PATCH 02/40] Added receipts, and more transaction fields. --- ethgraphql/main.go | 282 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 263 insertions(+), 19 deletions(-) diff --git a/ethgraphql/main.go b/ethgraphql/main.go index e4506db9566c..d40053b6bee8 100644 --- a/ethgraphql/main.go +++ b/ethgraphql/main.go @@ -26,6 +26,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/eth" @@ -152,21 +153,190 @@ func (a *Account) Storage(ctx context.Context, args StorageSlotArgs) (HexBytes, return HexBytes{state.GetState(a.address, common.BytesToHash(args.Slot.Bytes)).Bytes()}, nil } +type Receipt struct { + node *node.Node + transaction *Transaction + receipt *types.Receipt +} + +func (r *Receipt) Status(ctx context.Context) int32 { + return int32(r.receipt.Status) +} + +func (r *Receipt) GasUsed(ctx context.Context) int32 { + return int32(r.receipt.GasUsed) +} + +func (r *Receipt) CumulativeGasUsed(ctx context.Context) int32 { + return int32(r.receipt.CumulativeGasUsed) +} + type Transaction struct { - node *node.Node - hash common.Hash - tx *types.Transaction + node *node.Node + hash common.Hash + tx *types.Transaction + block *Block + index uint64 +} + +func (t *Transaction) resolve(ctx context.Context) (*types.Transaction, error) { + if t.tx == nil { + be, err := getBackend(t.node) + if err != nil { + return nil, err + } + + tx, blockHash, _, index := rawdb.ReadTransaction(be.ChainDb(), t.hash) + if tx != nil { + t.tx = tx + t.block = &Block{ + node: t.node, + hash: blockHash, + } + t.index = index + } else { + t.tx = be.GetPoolTransaction(t.hash) + } + } + return t.tx, nil } func (tx *Transaction) Hash(ctx context.Context) HexBytes { return HexBytes{tx.hash.Bytes()} } +func (t *Transaction) Data(ctx context.Context) (HexBytes, error) { + tx, err := t.resolve(ctx) + if err != nil || tx == nil { + return HexBytes{}, err + } + return HexBytes{tx.Data()}, nil +} + +func (t *Transaction) Gas(ctx context.Context) (int32, error) { + tx, err := t.resolve(ctx) + if err != nil || tx == nil { + return 0, err + } + return int32(tx.Gas()), nil +} + +func (t *Transaction) GasPrice(ctx context.Context) (BigNum, error) { + tx, err := t.resolve(ctx) + if err != nil || tx == nil { + return BigNum{}, err + } + return BigNum{tx.GasPrice()}, nil +} + +func (t *Transaction) Value(ctx context.Context) (BigNum, error) { + tx, err := t.resolve(ctx) + if err != nil || tx == nil { + return BigNum{}, err + } + return BigNum{tx.Value()}, nil +} + +func (t *Transaction) Nonce(ctx context.Context) (int32, error) { + tx, err := t.resolve(ctx) + if err != nil || tx == nil { + return 0, err + } + return int32(tx.Nonce()), nil +} + +func (t *Transaction) To(ctx context.Context, args BlockNumberArgs) (*Account, error) { + tx, err := t.resolve(ctx) + if err != nil || tx == nil { + return nil, err + } + + to := tx.To() + if to == nil { + return nil, nil + } + + block := rpc.LatestBlockNumber + if args.Block != nil { + block = rpc.BlockNumber(*args.Block) + } + + return &Account{ + node: t.node, + address: *to, + blockNumber: block, + }, nil +} + +func (t *Transaction) From(ctx context.Context, args BlockNumberArgs) (*Account, error) { + tx, err := t.resolve(ctx) + if err != nil || tx == nil { + return nil, err + } + + block := rpc.LatestBlockNumber + if args.Block != nil { + block = rpc.BlockNumber(*args.Block) + } + + var signer types.Signer = types.FrontierSigner{} + if tx.Protected() { + signer = types.NewEIP155Signer(tx.ChainId()) + } + from, _ := types.Sender(signer, tx) + + return &Account{ + node: t.node, + address: from, + blockNumber: block, + }, nil +} + +func (t *Transaction) Block(ctx context.Context) (*Block, error) { + if _, err := t.resolve(ctx); err != nil { + return nil, err + } + return t.block, nil +} + +func (t *Transaction) Index(ctx context.Context) (*int32, error) { + if _, err := t.resolve(ctx); err != nil { + return nil, err + } + if t.block == nil { + return nil, nil + } + index := int32(t.index) + return &index, nil +} + +func (t *Transaction) Receipt(ctx context.Context) (*Receipt, error) { + if _, err := t.resolve(ctx); err != nil { + return nil, err + } + + if t.block == nil { + return nil, nil + } + + receipts, err := t.block.resolveReceipts(ctx) + if err != nil { + return nil, err + } + + return &Receipt{ + node: t.node, + transaction: t, + receipt: receipts[t.index], + }, nil +} + type Block struct { - node *node.Node - num *rpc.BlockNumber - hash common.Hash - block *types.Block + node *node.Node + num *rpc.BlockNumber + hash common.Hash + block *types.Block + receipts []*types.Receipt } func (b *Block) resolve(ctx context.Context) (*types.Block, error) { @@ -187,6 +357,31 @@ func (b *Block) resolve(ctx context.Context) (*types.Block, error) { return b.block, err } +func (b *Block) resolveReceipts(ctx context.Context) ([]*types.Receipt, error) { + if b.receipts == nil { + be, err := getBackend(b.node) + if err != nil { + return nil, err + } + + hash := b.hash + if hash == (common.Hash{}) { + block, err := b.resolve(ctx) + if err != nil { + return nil, err + } + hash = block.Hash() + } + + receipts, err := be.GetReceipts(ctx, hash) + if err != nil { + return nil, err + } + b.receipts = []*types.Receipt(receipts) + } + return b.receipts, nil +} + func (b *Block) Number(ctx context.Context) (int32, error) { if b.num == nil || *b.num == rpc.LatestBlockNumber { block, err := b.resolve(ctx) @@ -372,11 +567,13 @@ func (b *Block) Transactions(ctx context.Context) ([]*Transaction, error) { } ret := make([]*Transaction, 0, len(block.Transactions())) - for _, tx := range block.Transactions() { + for i, tx := range block.Transactions() { ret = append(ret, &Transaction{ - node: b.node, - hash: tx.Hash(), - tx: tx, + node: b.node, + hash: tx.Hash(), + tx: tx, + block: b, + index: uint64(i), }) } return ret, nil @@ -388,28 +585,38 @@ type Query struct { type BlockArgs struct { Number *int32 - Hash *string + Hash *HexBytes } -func (q *Query) Block(ctx context.Context, args BlockArgs) *Block { +func (q *Query) Block(ctx context.Context, args BlockArgs) (*Block, error) { + var block *Block if args.Number != nil { num := rpc.BlockNumber(uint64(*args.Number)) - return &Block{ + block = &Block{ node: q.node, num: &num, } } else if args.Hash != nil { - return &Block{ + block = &Block{ node: q.node, - hash: common.HexToHash(*args.Hash), + hash: common.BytesToHash(args.Hash.Bytes), } } else { num := rpc.LatestBlockNumber - return &Block{ + block = &Block{ node: q.node, num: &num, } } + + // Resolve the block; if it doesn't exist, return nil. + b, err := block.resolve(ctx) + if err != nil { + return nil, err + } else if b == nil { + return nil, nil + } + return block, nil } type AccountArgs struct { @@ -430,6 +637,26 @@ func (q *Query) Account(ctx context.Context, args AccountArgs) *Account { } } +type TransactionArgs struct { + Hash HexBytes +} + +func (q *Query) Transaction(ctx context.Context, args TransactionArgs) (*Transaction, error) { + tx := &Transaction{ + node: q.node, + hash: common.BytesToHash(args.Hash.Bytes), + } + + // Resolve the transaction; if it doesn't exist, return nil. + t, err := tx.resolve(ctx) + if err != nil { + return nil, err + } else if t == nil { + return nil, nil + } + return tx, nil +} + func NewHandler(n *node.Node) (http.Handler, error) { q := Query{n} @@ -449,8 +676,24 @@ func NewHandler(n *node.Node) (http.Handler, error) { storage(slot: HexBytes!): HexBytes! } + type Receipt { + status: Int! + gasUsed: Int! + cumulativeGasUsed: Int! + } + type Transaction { hash: HexBytes! + data: HexBytes! + gas: Int! + gasPrice: BigNum! + value: BigNum! + nonce: Int! + to(block: Int): Account + from(block: Int): Account! + block: Block + index: Int + receipt: Receipt } type Block { @@ -474,8 +717,9 @@ func NewHandler(n *node.Node) (http.Handler, error) { } type Query { - block(number: Int, hash: String): Block - account(address: HexBytes!, blockNumber: Int): Account + block(number: Int, hash: HexBytes): Block + account(address: HexBytes!, blockNumber: Int): Account! + transaction(hash: HexBytes!): Transaction } ` schema, err := graphql.ParseSchema(s, &q) From c46a697c26e925eb81dde3a8bfe2a947f87abec2 Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Sun, 14 Oct 2018 13:04:53 +0100 Subject: [PATCH 03/40] Finish receipts, add logs --- ethgraphql/main.go | 91 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 73 insertions(+), 18 deletions(-) diff --git a/ethgraphql/main.go b/ethgraphql/main.go index d40053b6bee8..c24e38702e44 100644 --- a/ethgraphql/main.go +++ b/ethgraphql/main.go @@ -153,6 +153,36 @@ func (a *Account) Storage(ctx context.Context, args StorageSlotArgs) (HexBytes, return HexBytes{state.GetState(a.address, common.BytesToHash(args.Slot.Bytes)).Bytes()}, nil } +type Log struct { + node *node.Node + transaction *Transaction + log *types.Log +} + +func (l *Log) Transaction(ctx context.Context) *Transaction { + return l.transaction +} + +func (l *Log) Account(ctx context.Context, args BlockNumberArgs) *Account { + return &Account{ + node: l.node, + address: l.log.Address, + blockNumber: args.Number(), + } +} + +func (l *Log) Topics(ctx context.Context) []*HexBytes { + ret := make([]*HexBytes, 0, len(l.log.Topics)) + for _, topic := range l.log.Topics { + ret = append(ret, &HexBytes{topic.Bytes()}) + } + return ret +} + +func (l *Log) Data(ctx context.Context) HexBytes { + return HexBytes{l.log.Data} +} + type Receipt struct { node *node.Node transaction *Transaction @@ -171,6 +201,30 @@ func (r *Receipt) CumulativeGasUsed(ctx context.Context) int32 { return int32(r.receipt.CumulativeGasUsed) } +func (r *Receipt) Contract(ctx context.Context, args BlockNumberArgs) *Account { + if r.receipt.ContractAddress == (common.Address{}) { + return nil + } + + return &Account{ + node: r.node, + address: r.receipt.ContractAddress, + blockNumber: args.Number(), + } +} + +func (r *Receipt) Logs(ctx context.Context) []*Log { + ret := make([]*Log, 0, len(r.receipt.Logs)) + for _, log := range r.receipt.Logs { + ret = append(ret, &Log{ + node: r.node, + transaction: r.transaction, + log: log, + }) + } + return ret +} + type Transaction struct { node *node.Node hash common.Hash @@ -256,15 +310,10 @@ func (t *Transaction) To(ctx context.Context, args BlockNumberArgs) (*Account, e return nil, nil } - block := rpc.LatestBlockNumber - if args.Block != nil { - block = rpc.BlockNumber(*args.Block) - } - return &Account{ node: t.node, address: *to, - blockNumber: block, + blockNumber: args.Number(), }, nil } @@ -274,11 +323,6 @@ func (t *Transaction) From(ctx context.Context, args BlockNumberArgs) (*Account, return nil, err } - block := rpc.LatestBlockNumber - if args.Block != nil { - block = rpc.BlockNumber(*args.Block) - } - var signer types.Signer = types.FrontierSigner{} if tx.Protected() { signer = types.NewEIP155Signer(tx.ChainId()) @@ -288,7 +332,7 @@ func (t *Transaction) From(ctx context.Context, args BlockNumberArgs) (*Account, return &Account{ node: t.node, address: from, - blockNumber: block, + blockNumber: args.Number(), }, nil } @@ -542,21 +586,23 @@ type BlockNumberArgs struct { Block *int32 } +func (a BlockNumberArgs) Number() rpc.BlockNumber { + if a.Block != nil { + return rpc.BlockNumber(*a.Block) + } + return rpc.LatestBlockNumber +} + func (b *Block) Coinbase(ctx context.Context, args BlockNumberArgs) (*Account, error) { block, err := b.resolve(ctx) if err != nil { return nil, err } - blockNumber := rpc.LatestBlockNumber - if args.Block != nil { - blockNumber = rpc.BlockNumber(*args.Block) - } - return &Account{ node: b.node, address: block.Coinbase(), - blockNumber: blockNumber, + blockNumber: args.Number(), }, nil } @@ -676,10 +722,19 @@ func NewHandler(n *node.Node) (http.Handler, error) { storage(slot: HexBytes!): HexBytes! } + type Log { + transaction: Transaction! + account(block: Int): Account! + topics: [HexBytes]! + data: HexBytes! + } + type Receipt { status: Int! gasUsed: Int! cumulativeGasUsed: Int! + contract(block: Int): Account + logs: [Log]! } type Transaction { From ed023321482a4c9401d253eb702accc131c2a13e Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Sun, 14 Oct 2018 15:33:39 +0100 Subject: [PATCH 04/40] Add transactionCount to block --- ethgraphql/main.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ethgraphql/main.go b/ethgraphql/main.go index c24e38702e44..4650ec7b963f 100644 --- a/ethgraphql/main.go +++ b/ethgraphql/main.go @@ -606,6 +606,14 @@ func (b *Block) Coinbase(ctx context.Context, args BlockNumberArgs) (*Account, e }, nil } +func (b *Block) TransactionCount(ctx context.Context) (int32, error) { + block, err := b.resolve(ctx) + if err != nil { + return 0, err + } + return int32(len(block.Transactions())), nil +} + func (b *Block) Transactions(ctx context.Context) ([]*Transaction, error) { block, err := b.resolve(ctx) if err != nil { @@ -768,6 +776,7 @@ func NewHandler(n *node.Node) (http.Handler, error) { extra: HexBytes! totalDifficulty: BigNum! coinbase(block: Int): Account! + transactionCount: Int! transactions: [Transaction]! } From cd53e6083abc4d162c10a25036aab7454a2a6eb5 Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Mon, 15 Oct 2018 13:24:58 +0100 Subject: [PATCH 05/40] Add types and . --- ethgraphql/main.go | 132 ++++++++++++++++++++++++++++----------------- 1 file changed, 84 insertions(+), 48 deletions(-) diff --git a/ethgraphql/main.go b/ethgraphql/main.go index 4650ec7b963f..19f55df228f6 100644 --- a/ethgraphql/main.go +++ b/ethgraphql/main.go @@ -62,6 +62,40 @@ func (h *HexBytes) UnmarshalGraphQL(input interface{}) error { return err } *h = HexBytes{data} + default: + err = fmt.Errorf("Unexpected type for HexBytes: %v", input) + } + return err +} + +type Bytes32 struct { + common.Hash +} + +func (_ Bytes32) ImplementsGraphQLType(name string) bool { return name == "Bytes32" } + +func (b *Bytes32) UnmarshalGraphQL(input interface{}) error { + var err error + switch input := input.(type) { + case string: + *b = Bytes32{common.HexToHash(input)} + default: + err = fmt.Errorf("Unexpected type for Hash: %v", input) + } + return err +} + +type Address struct { + common.Address +} + +func (h Address) ImplementsGraphQLType(name string) bool { return name == "Address" } + +func (h *Address) UnmarshalGraphQL(input interface{}) error { + var err error + switch input := input.(type) { + case string: + *h = Address{common.HexToAddress(input)} default: err = fmt.Errorf("Unexpected type for Hash: %v", input) } @@ -109,8 +143,8 @@ func (a *Account) getState(ctx context.Context) (*state.StateDB, error) { return state, err } -func (a *Account) Address(ctx context.Context) (HexBytes, error) { - return HexBytes{a.address.Bytes()}, nil +func (a *Account) Address(ctx context.Context) (Address, error) { + return Address{a.address}, nil } func (a *Account) Balance(ctx context.Context) (BigNum, error) { @@ -141,16 +175,16 @@ func (a *Account) Code(ctx context.Context) (HexBytes, error) { } type StorageSlotArgs struct { - Slot HexBytes + Slot Bytes32 } -func (a *Account) Storage(ctx context.Context, args StorageSlotArgs) (HexBytes, error) { +func (a *Account) Storage(ctx context.Context, args StorageSlotArgs) (Bytes32, error) { state, err := a.getState(ctx) if err != nil { - return HexBytes{}, err + return Bytes32{}, err } - return HexBytes{state.GetState(a.address, common.BytesToHash(args.Slot.Bytes)).Bytes()}, nil + return Bytes32{state.GetState(a.address, args.Slot.Hash)}, nil } type Log struct { @@ -171,10 +205,10 @@ func (l *Log) Account(ctx context.Context, args BlockNumberArgs) *Account { } } -func (l *Log) Topics(ctx context.Context) []*HexBytes { - ret := make([]*HexBytes, 0, len(l.log.Topics)) +func (l *Log) Topics(ctx context.Context) []*Bytes32 { + ret := make([]*Bytes32, 0, len(l.log.Topics)) for _, topic := range l.log.Topics { - ret = append(ret, &HexBytes{topic.Bytes()}) + ret = append(ret, &Bytes32{topic}) } return ret } @@ -255,8 +289,8 @@ func (t *Transaction) resolve(ctx context.Context) (*types.Transaction, error) { return t.tx, nil } -func (tx *Transaction) Hash(ctx context.Context) HexBytes { - return HexBytes{tx.hash.Bytes()} +func (tx *Transaction) Hash(ctx context.Context) Bytes32 { + return Bytes32{tx.hash} } func (t *Transaction) Data(ctx context.Context) (HexBytes, error) { @@ -438,15 +472,15 @@ func (b *Block) Number(ctx context.Context) (int32, error) { return int32(*b.num), nil } -func (b *Block) Hash(ctx context.Context) (HexBytes, error) { +func (b *Block) Hash(ctx context.Context) (Bytes32, error) { if b.hash == (common.Hash{}) { block, err := b.resolve(ctx) if err != nil { - return HexBytes{}, err + return Bytes32{}, err } b.hash = block.Hash() } - return HexBytes{b.hash.Bytes()}, nil + return Bytes32{b.hash}, nil } func (b *Block) GasLimit(ctx context.Context) (int32, error) { @@ -516,44 +550,44 @@ func (b *Block) Nonce(ctx context.Context) (BigNum, error) { return BigNum{i}, nil } -func (b *Block) MixDigest(ctx context.Context) (HexBytes, error) { +func (b *Block) MixDigest(ctx context.Context) (Bytes32, error) { block, err := b.resolve(ctx) if err != nil { - return HexBytes{}, err + return Bytes32{}, err } - return HexBytes{block.MixDigest().Bytes()}, nil + return Bytes32{block.MixDigest()}, nil } -func (b *Block) Root(ctx context.Context) (HexBytes, error) { +func (b *Block) Root(ctx context.Context) (Bytes32, error) { block, err := b.resolve(ctx) if err != nil { - return HexBytes{}, err + return Bytes32{}, err } - return HexBytes{block.Root().Bytes()}, nil + return Bytes32{block.Root()}, nil } -func (b *Block) TxHash(ctx context.Context) (HexBytes, error) { +func (b *Block) TxHash(ctx context.Context) (Bytes32, error) { block, err := b.resolve(ctx) if err != nil { - return HexBytes{}, err + return Bytes32{}, err } - return HexBytes{block.TxHash().Bytes()}, nil + return Bytes32{block.TxHash()}, nil } -func (b *Block) ReceiptHash(ctx context.Context) (HexBytes, error) { +func (b *Block) ReceiptHash(ctx context.Context) (Bytes32, error) { block, err := b.resolve(ctx) if err != nil { - return HexBytes{}, err + return Bytes32{}, err } - return HexBytes{block.ReceiptHash().Bytes()}, nil + return Bytes32{block.ReceiptHash()}, nil } -func (b *Block) UncleHash(ctx context.Context) (HexBytes, error) { +func (b *Block) UncleHash(ctx context.Context) (Bytes32, error) { block, err := b.resolve(ctx) if err != nil { - return HexBytes{}, err + return Bytes32{}, err } - return HexBytes{block.UncleHash().Bytes()}, nil + return Bytes32{block.UncleHash()}, nil } func (b *Block) Extra(ctx context.Context) (HexBytes, error) { @@ -639,7 +673,7 @@ type Query struct { type BlockArgs struct { Number *int32 - Hash *HexBytes + Hash *Bytes32 } func (q *Query) Block(ctx context.Context, args BlockArgs) (*Block, error) { @@ -653,7 +687,7 @@ func (q *Query) Block(ctx context.Context, args BlockArgs) (*Block, error) { } else if args.Hash != nil { block = &Block{ node: q.node, - hash: common.BytesToHash(args.Hash.Bytes), + hash: args.Hash.Hash, } } else { num := rpc.LatestBlockNumber @@ -674,7 +708,7 @@ func (q *Query) Block(ctx context.Context, args BlockArgs) (*Block, error) { } type AccountArgs struct { - Address HexBytes + Address Address BlockNumber *int32 } @@ -686,19 +720,19 @@ func (q *Query) Account(ctx context.Context, args AccountArgs) *Account { return &Account{ node: q.node, - address: common.BytesToAddress(args.Address.Bytes), + address: args.Address.Address, blockNumber: blockNumber, } } type TransactionArgs struct { - Hash HexBytes + Hash Bytes32 } func (q *Query) Transaction(ctx context.Context, args TransactionArgs) (*Transaction, error) { tx := &Transaction{ node: q.node, - hash: common.BytesToHash(args.Hash.Bytes), + hash: args.Hash.Hash, } // Resolve the transaction; if it doesn't exist, return nil. @@ -715,6 +749,8 @@ func NewHandler(n *node.Node) (http.Handler, error) { q := Query{n} s := ` + scalar Bytes32 + scalar Address scalar HexBytes scalar BigNum @@ -723,17 +759,17 @@ func NewHandler(n *node.Node) (http.Handler, error) { } type Account { - address: HexBytes! + address: Address! balance: BigNum! nonce: Int! code: HexBytes! - storage(slot: HexBytes!): HexBytes! + storage(slot: Bytes32!): Bytes32! } type Log { transaction: Transaction! account(block: Int): Account! - topics: [HexBytes]! + topics: [Bytes32]! data: HexBytes! } @@ -746,7 +782,7 @@ func NewHandler(n *node.Node) (http.Handler, error) { } type Transaction { - hash: HexBytes! + hash: Bytes32! data: HexBytes! gas: Int! gasPrice: BigNum! @@ -761,18 +797,18 @@ func NewHandler(n *node.Node) (http.Handler, error) { type Block { number: Int! - hash: HexBytes! + hash: Bytes32! gasLimit: Int! gasUsed: Int! parent: Block difficulty: BigNum! time: BigNum! nonce: BigNum! - mixDigest: HexBytes! - root: HexBytes! - txHash: HexBytes! - receiptHash: HexBytes! - uncleHash: HexBytes! + mixDigest: Bytes32! + root: Bytes32! + txHash: Bytes32! + receiptHash: Bytes32! + uncleHash: Bytes32! extra: HexBytes! totalDifficulty: BigNum! coinbase(block: Int): Account! @@ -781,9 +817,9 @@ func NewHandler(n *node.Node) (http.Handler, error) { } type Query { - block(number: Int, hash: HexBytes): Block - account(address: HexBytes!, blockNumber: Int): Account! - transaction(hash: HexBytes!): Transaction + block(number: Int, hash: Bytes32): Block + account(address: Address!, blockNumber: Int): Account! + transaction(hash: Bytes32!): Transaction } ` schema, err := graphql.ParseSchema(s, &q) From 4e3be4fe8305ed22f8d5979e262f49ebc97d91a5 Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Mon, 15 Oct 2018 13:44:19 +0100 Subject: [PATCH 06/40] Update Block type to be compatible with ethql --- ethgraphql/main.go | 70 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 49 insertions(+), 21 deletions(-) diff --git a/ethgraphql/main.go b/ethgraphql/main.go index 19f55df228f6..0302f85bfd74 100644 --- a/ethgraphql/main.go +++ b/ethgraphql/main.go @@ -532,7 +532,7 @@ func (b *Block) Difficulty(ctx context.Context) (BigNum, error) { return BigNum{block.Difficulty()}, nil } -func (b *Block) Time(ctx context.Context) (BigNum, error) { +func (b *Block) Timestamp(ctx context.Context) (BigNum, error) { block, err := b.resolve(ctx) if err != nil { return BigNum{}, err @@ -550,7 +550,7 @@ func (b *Block) Nonce(ctx context.Context) (BigNum, error) { return BigNum{i}, nil } -func (b *Block) MixDigest(ctx context.Context) (Bytes32, error) { +func (b *Block) MixHash(ctx context.Context) (Bytes32, error) { block, err := b.resolve(ctx) if err != nil { return Bytes32{}, err @@ -558,23 +558,23 @@ func (b *Block) MixDigest(ctx context.Context) (Bytes32, error) { return Bytes32{block.MixDigest()}, nil } -func (b *Block) Root(ctx context.Context) (Bytes32, error) { +func (b *Block) TransactionsRoot(ctx context.Context) (Bytes32, error) { block, err := b.resolve(ctx) if err != nil { return Bytes32{}, err } - return Bytes32{block.Root()}, nil + return Bytes32{block.TxHash()}, nil } -func (b *Block) TxHash(ctx context.Context) (Bytes32, error) { +func (b *Block) StateRoot(ctx context.Context) (Bytes32, error) { block, err := b.resolve(ctx) if err != nil { return Bytes32{}, err } - return Bytes32{block.TxHash()}, nil + return Bytes32{block.Root()}, nil } -func (b *Block) ReceiptHash(ctx context.Context) (Bytes32, error) { +func (b *Block) ReceiptsRoot(ctx context.Context) (Bytes32, error) { block, err := b.resolve(ctx) if err != nil { return Bytes32{}, err @@ -582,7 +582,7 @@ func (b *Block) ReceiptHash(ctx context.Context) (Bytes32, error) { return Bytes32{block.ReceiptHash()}, nil } -func (b *Block) UncleHash(ctx context.Context) (Bytes32, error) { +func (b *Block) OmmerHash(ctx context.Context) (Bytes32, error) { block, err := b.resolve(ctx) if err != nil { return Bytes32{}, err @@ -590,7 +590,25 @@ func (b *Block) UncleHash(ctx context.Context) (Bytes32, error) { return Bytes32{block.UncleHash()}, nil } -func (b *Block) Extra(ctx context.Context) (HexBytes, error) { +func (b *Block) Ommers(ctx context.Context) ([]*Block, error) { + block, err := b.resolve(ctx) + if err != nil { + return nil, err + } + + ret := make([]*Block, 0, len(block.Uncles())) + for _, uncle := range block.Uncles() { + blockNumber := rpc.BlockNumber(uncle.Number.Uint64()) + ret = append(ret, &Block{ + node: b.node, + num: &blockNumber, + hash: uncle.Hash(), + }) + } + return ret, nil +} + +func (b *Block) ExtraData(ctx context.Context) (HexBytes, error) { block, err := b.resolve(ctx) if err != nil { return HexBytes{}, err @@ -598,6 +616,14 @@ func (b *Block) Extra(ctx context.Context) (HexBytes, error) { return HexBytes{block.Extra()}, nil } +func (b *Block) LogsBloom(ctx context.Context) (HexBytes, error) { + block, err := b.resolve(ctx) + if err != nil { + return HexBytes{}, err + } + return HexBytes{block.Bloom().Bytes()}, nil +} + func (b *Block) TotalDifficulty(ctx context.Context) (BigNum, error) { h := b.hash if h == (common.Hash{}) { @@ -627,7 +653,7 @@ func (a BlockNumberArgs) Number() rpc.BlockNumber { return rpc.LatestBlockNumber } -func (b *Block) Coinbase(ctx context.Context, args BlockNumberArgs) (*Account, error) { +func (b *Block) Miner(ctx context.Context, args BlockNumberArgs) (*Account, error) { block, err := b.resolve(ctx) if err != nil { return nil, err @@ -798,21 +824,23 @@ func NewHandler(n *node.Node) (http.Handler, error) { type Block { number: Int! hash: Bytes32! + parent: Block + nonce: BigNum! + transactionsRoot: Bytes32! + transactionCount: Int! + stateRoot: Bytes32! + receiptsRoot: Bytes32! + miner(block: Int): Account! + extraData: HexBytes! gasLimit: Int! gasUsed: Int! - parent: Block + timestamp: BigNum! + logsBloom: HexBytes! + mixHash: Bytes32! difficulty: BigNum! - time: BigNum! - nonce: BigNum! - mixDigest: Bytes32! - root: Bytes32! - txHash: Bytes32! - receiptHash: Bytes32! - uncleHash: Bytes32! - extra: HexBytes! totalDifficulty: BigNum! - coinbase(block: Int): Account! - transactionCount: Int! + ommers: [Block]! + ommerHash: Bytes32! transactions: [Transaction]! } From 928615200aa681c5735d102b805bf82dba3fca9c Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Mon, 15 Oct 2018 13:50:05 +0100 Subject: [PATCH 07/40] Rename nonce to transactionCount in Account, to be compatible with ethql --- ethgraphql/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ethgraphql/main.go b/ethgraphql/main.go index 0302f85bfd74..237b4862ce54 100644 --- a/ethgraphql/main.go +++ b/ethgraphql/main.go @@ -156,7 +156,7 @@ func (a *Account) Balance(ctx context.Context) (BigNum, error) { return BigNum{state.GetBalance(a.address)}, nil } -func (a *Account) Nonce(ctx context.Context) (int32, error) { +func (a *Account) TransactionCount(ctx context.Context) (int32, error) { state, err := a.getState(ctx) if err != nil { return 0, err @@ -787,7 +787,7 @@ func NewHandler(n *node.Node) (http.Handler, error) { type Account { address: Address! balance: BigNum! - nonce: Int! + transactionCount: Int! code: HexBytes! storage(slot: Bytes32!): Bytes32! } From 4ecd3de5fc8f3e1ac71f53d5127febd6f1111ad9 Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Mon, 15 Oct 2018 14:01:51 +0100 Subject: [PATCH 08/40] Update transaction, receipt and log to match ethql --- ethgraphql/main.go | 152 +++++++++++++++++++++++++-------------------- 1 file changed, 84 insertions(+), 68 deletions(-) diff --git a/ethgraphql/main.go b/ethgraphql/main.go index 237b4862ce54..422592e44dde 100644 --- a/ethgraphql/main.go +++ b/ethgraphql/main.go @@ -205,10 +205,14 @@ func (l *Log) Account(ctx context.Context, args BlockNumberArgs) *Account { } } -func (l *Log) Topics(ctx context.Context) []*Bytes32 { - ret := make([]*Bytes32, 0, len(l.log.Topics)) +func (l *Log) Index(ctx context.Context) int32 { + return int32(l.log.Index) +} + +func (l *Log) Topics(ctx context.Context) []Bytes32 { + ret := make([]Bytes32, 0, len(l.log.Topics)) for _, topic := range l.log.Topics { - ret = append(ret, &Bytes32{topic}) + ret = append(ret, Bytes32{topic}) } return ret } @@ -217,48 +221,6 @@ func (l *Log) Data(ctx context.Context) HexBytes { return HexBytes{l.log.Data} } -type Receipt struct { - node *node.Node - transaction *Transaction - receipt *types.Receipt -} - -func (r *Receipt) Status(ctx context.Context) int32 { - return int32(r.receipt.Status) -} - -func (r *Receipt) GasUsed(ctx context.Context) int32 { - return int32(r.receipt.GasUsed) -} - -func (r *Receipt) CumulativeGasUsed(ctx context.Context) int32 { - return int32(r.receipt.CumulativeGasUsed) -} - -func (r *Receipt) Contract(ctx context.Context, args BlockNumberArgs) *Account { - if r.receipt.ContractAddress == (common.Address{}) { - return nil - } - - return &Account{ - node: r.node, - address: r.receipt.ContractAddress, - blockNumber: args.Number(), - } -} - -func (r *Receipt) Logs(ctx context.Context) []*Log { - ret := make([]*Log, 0, len(r.receipt.Logs)) - for _, log := range r.receipt.Logs { - ret = append(ret, &Log{ - node: r.node, - transaction: r.transaction, - log: log, - }) - } - return ret -} - type Transaction struct { node *node.Node hash common.Hash @@ -293,7 +255,7 @@ func (tx *Transaction) Hash(ctx context.Context) Bytes32 { return Bytes32{tx.hash} } -func (t *Transaction) Data(ctx context.Context) (HexBytes, error) { +func (t *Transaction) InputData(ctx context.Context) (HexBytes, error) { tx, err := t.resolve(ctx) if err != nil || tx == nil { return HexBytes{}, err @@ -388,7 +350,7 @@ func (t *Transaction) Index(ctx context.Context) (*int32, error) { return &index, nil } -func (t *Transaction) Receipt(ctx context.Context) (*Receipt, error) { +func (t *Transaction) getReceipt(ctx context.Context) (*types.Receipt, error) { if _, err := t.resolve(ctx); err != nil { return nil, err } @@ -402,13 +364,69 @@ func (t *Transaction) Receipt(ctx context.Context) (*Receipt, error) { return nil, err } - return &Receipt{ + return receipts[t.index], nil +} + +func (t *Transaction) Status(ctx context.Context) (*int32, error) { + receipt, err := t.getReceipt(ctx) + if err != nil || receipt == nil { + return nil, err + } + + ret := int32(receipt.Status) + return &ret, nil +} + +func (t *Transaction) GasUsed(ctx context.Context) (*int32, error) { + receipt, err := t.getReceipt(ctx) + if err != nil || receipt == nil { + return nil, err + } + + ret := int32(receipt.GasUsed) + return &ret, nil +} + +func (t *Transaction) CumulativeGasUsed(ctx context.Context) (*int32, error) { + receipt, err := t.getReceipt(ctx) + if err != nil || receipt == nil { + return nil, err + } + + ret := int32(receipt.CumulativeGasUsed) + return &ret, nil +} + +func (t *Transaction) CreatedContract(ctx context.Context, args BlockNumberArgs) (*Account, error) { + receipt, err := t.getReceipt(ctx) + if err != nil || receipt == nil || receipt.ContractAddress == (common.Address{}) { + return nil, err + } + + return &Account{ node: t.node, - transaction: t, - receipt: receipts[t.index], + address: receipt.ContractAddress, + blockNumber: args.Number(), }, nil } +func (t *Transaction) Logs(ctx context.Context) (*[]*Log, error) { + receipt, err := t.getReceipt(ctx) + if err != nil || receipt == nil { + return nil, err + } + + ret := make([]*Log, 0, len(receipt.Logs)) + for _, log := range receipt.Logs { + ret = append(ret, &Log{ + node: t.node, + transaction: t, + log: log, + }) + } + return &ret, nil +} + type Block struct { node *node.Node num *rpc.BlockNumber @@ -793,32 +811,30 @@ func NewHandler(n *node.Node) (http.Handler, error) { } type Log { - transaction: Transaction! + index: Int! account(block: Int): Account! - topics: [Bytes32]! + topics: [Bytes32!]! data: HexBytes! - } - - type Receipt { - status: Int! - gasUsed: Int! - cumulativeGasUsed: Int! - contract(block: Int): Account - logs: [Log]! + transaction: Transaction! } type Transaction { hash: Bytes32! - data: HexBytes! - gas: Int! - gasPrice: BigNum! - value: BigNum! nonce: Int! - to(block: Int): Account + index: Int from(block: Int): Account! + to(block: Int): Account + value: BigNum! + gasPrice: BigNum! + gas: Int! + inputData: HexBytes! block: Block - index: Int - receipt: Receipt + + status: Int + gasUsed: Int + cumulativeGasUsed: Int + createdContract(block: Int): Account + logs: [Log!] } type Block { @@ -841,7 +857,7 @@ func NewHandler(n *node.Node) (http.Handler, error) { totalDifficulty: BigNum! ommers: [Block]! ommerHash: Bytes32! - transactions: [Transaction]! + transactions: [Transaction!]! } type Query { From 76e45cf2c157fe33642ea549a5c1e31c84067a21 Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Mon, 15 Oct 2018 14:12:02 +0100 Subject: [PATCH 09/40] Add query operator, for a range of blocks --- ethgraphql/main.go | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/ethgraphql/main.go b/ethgraphql/main.go index 422592e44dde..70e76ed79409 100644 --- a/ethgraphql/main.go +++ b/ethgraphql/main.go @@ -751,6 +751,41 @@ func (q *Query) Block(ctx context.Context, args BlockArgs) (*Block, error) { return block, nil } +type BlocksArgs struct { + From int32 + To *int32 +} + +func (q *Query) Blocks(ctx context.Context, args BlocksArgs) ([]*Block, error) { + be, err := getBackend(q.node) + if err != nil { + return nil, err + } + + from := rpc.BlockNumber(args.From) + + var to rpc.BlockNumber + if args.To != nil { + to = rpc.BlockNumber(*args.To) + } else { + to = rpc.BlockNumber(be.CurrentBlock().Number().Int64()) + } + + if to < from { + return []*Block{}, nil + } + + ret := make([]*Block, 0, to-from+1) + for i := from; i <= to; i++ { + num := i + ret = append(ret, &Block{ + node: q.node, + num: &num, + }) + } + return ret, nil +} + type AccountArgs struct { Address Address BlockNumber *int32 @@ -861,8 +896,9 @@ func NewHandler(n *node.Node) (http.Handler, error) { } type Query { - block(number: Int, hash: Bytes32): Block account(address: Address!, blockNumber: Int): Account! + block(number: Int, hash: Bytes32): Block + blocks(from: Int!, to: Int): [Block!]! transaction(hash: Bytes32!): Transaction } ` From 460d1586e68716d4a3fd2f9f867679260883b2c1 Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Mon, 15 Oct 2018 14:16:36 +0100 Subject: [PATCH 10/40] Added ommerCount to Block --- ethgraphql/main.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ethgraphql/main.go b/ethgraphql/main.go index 70e76ed79409..b2dcdff185bb 100644 --- a/ethgraphql/main.go +++ b/ethgraphql/main.go @@ -608,6 +608,14 @@ func (b *Block) OmmerHash(ctx context.Context) (Bytes32, error) { return Bytes32{block.UncleHash()}, nil } +func (b *Block) OmmerCount(ctx context.Context) (int32, error) { + block, err := b.resolve(ctx) + if err != nil { + return 0, err + } + return int32(len(block.Uncles())), nil +} + func (b *Block) Ommers(ctx context.Context) ([]*Block, error) { block, err := b.resolve(ctx) if err != nil { @@ -890,6 +898,7 @@ func NewHandler(n *node.Node) (http.Handler, error) { mixHash: Bytes32! difficulty: BigNum! totalDifficulty: BigNum! + ommerCount: Int! ommers: [Block]! ommerHash: Bytes32! transactions: [Transaction!]! From 75aeee1c30e685bf68294e326df203808cceddb7 Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Mon, 15 Oct 2018 17:02:48 +0100 Subject: [PATCH 11/40] Add transactionAt and ommerAt to Block --- ethgraphql/main.go | 49 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/ethgraphql/main.go b/ethgraphql/main.go index b2dcdff185bb..659709572cee 100644 --- a/ethgraphql/main.go +++ b/ethgraphql/main.go @@ -446,7 +446,7 @@ func (b *Block) resolve(ctx context.Context) (*types.Block, error) { } if b.num != nil { - b.block, err = be.BlockByNumber(ctx, rpc.BlockNumber(*b.num)) + b.block, err = be.BlockByNumber(ctx, *b.num) } else { b.block, err = be.GetBlock(ctx, b.hash) } @@ -719,6 +719,51 @@ func (b *Block) Transactions(ctx context.Context) ([]*Transaction, error) { return ret, nil } +type ArrayIndexArgs struct { + Index int32 +} + +func (b *Block) TransactionAt(ctx context.Context, args ArrayIndexArgs) (*Transaction, error) { + block, err := b.resolve(ctx) + if err != nil { + return nil, err + } + + txes := block.Transactions() + if args.Index < 0 || int(args.Index) >= len(txes) { + return nil, nil + } + + tx := txes[args.Index] + return &Transaction{ + node: b.node, + hash: tx.Hash(), + tx: tx, + block: b, + index: uint64(args.Index), + }, nil +} + +func (b *Block) OmmerAt(ctx context.Context, args ArrayIndexArgs) (*Block, error) { + block, err := b.resolve(ctx) + if err != nil { + return nil, err + } + + uncles := block.Uncles() + if args.Index < 0 || int(args.Index) >= len(uncles) { + return nil, nil + } + + uncle := uncles[args.Index] + blockNumber := rpc.BlockNumber(uncle.Number.Uint64()) + return &Block{ + node: b.node, + num: &blockNumber, + hash: uncle.Hash(), + }, nil +} + type Query struct { node *node.Node } @@ -900,8 +945,10 @@ func NewHandler(n *node.Node) (http.Handler, error) { totalDifficulty: BigNum! ommerCount: Int! ommers: [Block]! + ommerAt(index: Int!): Block ommerHash: Bytes32! transactions: [Transaction!]! + transactionAt(index: Int!): Transaction } type Query { From 42bbab1b03ce1d426692f322ae6a0c21d6be99ec Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Tue, 16 Oct 2018 11:51:06 +0100 Subject: [PATCH 12/40] Added sendRawTransaction mutation --- ethgraphql/main.go | 46 ++++++++++++++++++++++++++++++------------ internal/ethapi/api.go | 10 ++++----- 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/ethgraphql/main.go b/ethgraphql/main.go index 659709572cee..cd36ab3a929c 100644 --- a/ethgraphql/main.go +++ b/ethgraphql/main.go @@ -34,6 +34,7 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rpc" graphql "github.com/graph-gophers/graphql-go" "github.com/graph-gophers/graphql-go/relay" @@ -764,7 +765,7 @@ func (b *Block) OmmerAt(ctx context.Context, args ArrayIndexArgs) (*Block, error }, nil } -type Query struct { +type Resolver struct { node *node.Node } @@ -773,23 +774,23 @@ type BlockArgs struct { Hash *Bytes32 } -func (q *Query) Block(ctx context.Context, args BlockArgs) (*Block, error) { +func (r *Resolver) Block(ctx context.Context, args BlockArgs) (*Block, error) { var block *Block if args.Number != nil { num := rpc.BlockNumber(uint64(*args.Number)) block = &Block{ - node: q.node, + node: r.node, num: &num, } } else if args.Hash != nil { block = &Block{ - node: q.node, + node: r.node, hash: args.Hash.Hash, } } else { num := rpc.LatestBlockNumber block = &Block{ - node: q.node, + node: r.node, num: &num, } } @@ -809,8 +810,8 @@ type BlocksArgs struct { To *int32 } -func (q *Query) Blocks(ctx context.Context, args BlocksArgs) ([]*Block, error) { - be, err := getBackend(q.node) +func (r *Resolver) Blocks(ctx context.Context, args BlocksArgs) ([]*Block, error) { + be, err := getBackend(r.node) if err != nil { return nil, err } @@ -832,7 +833,7 @@ func (q *Query) Blocks(ctx context.Context, args BlocksArgs) ([]*Block, error) { for i := from; i <= to; i++ { num := i ret = append(ret, &Block{ - node: q.node, + node: r.node, num: &num, }) } @@ -844,14 +845,14 @@ type AccountArgs struct { BlockNumber *int32 } -func (q *Query) Account(ctx context.Context, args AccountArgs) *Account { +func (r *Resolver) Account(ctx context.Context, args AccountArgs) *Account { blockNumber := rpc.LatestBlockNumber if args.BlockNumber != nil { blockNumber = rpc.BlockNumber(*args.BlockNumber) } return &Account{ - node: q.node, + node: r.node, address: args.Address.Address, blockNumber: blockNumber, } @@ -861,9 +862,9 @@ type TransactionArgs struct { Hash Bytes32 } -func (q *Query) Transaction(ctx context.Context, args TransactionArgs) (*Transaction, error) { +func (r *Resolver) Transaction(ctx context.Context, args TransactionArgs) (*Transaction, error) { tx := &Transaction{ - node: q.node, + node: r.node, hash: args.Hash.Hash, } @@ -877,8 +878,22 @@ func (q *Query) Transaction(ctx context.Context, args TransactionArgs) (*Transac return tx, nil } +func (r *Resolver) SendRawTransaction(ctx context.Context, args struct{ Data HexBytes }) (Bytes32, error) { + be, err := getBackend(r.node) + if err != nil { + return Bytes32{}, err + } + + tx := new(types.Transaction) + if err := rlp.DecodeBytes(args.Data.Bytes, tx); err != nil { + return Bytes32{}, err + } + hash, err := ethapi.SubmitTransaction(ctx, be, tx) + return Bytes32{hash}, err +} + func NewHandler(n *node.Node) (http.Handler, error) { - q := Query{n} + q := Resolver{n} s := ` scalar Bytes32 @@ -888,6 +903,7 @@ func NewHandler(n *node.Node) (http.Handler, error) { schema { query: Query + mutation: Mutation } type Account { @@ -957,6 +973,10 @@ func NewHandler(n *node.Node) (http.Handler, error) { blocks(from: Int!, to: Int): [Block!]! transaction(hash: Bytes32!): Transaction } + + type Mutation { + sendRawTransaction(data: HexBytes!): Bytes32! + } ` schema, err := graphql.ParseSchema(s, &q) if err != nil { diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 196be43e0382..f8933f1e4d34 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -378,7 +378,7 @@ func (s *PrivateAccountAPI) SendTransaction(ctx context.Context, args SendTxArgs log.Warn("Failed transaction send attempt", "from", args.From, "to", args.To, "value", args.Value.ToInt(), "err", err) return common.Hash{}, err } - return submitTransaction(ctx, s.b, signed) + return SubmitTransaction(ctx, s.b, signed) } // SignTransaction will create a transaction from the given arguments and @@ -1181,8 +1181,8 @@ func (args *SendTxArgs) toTransaction() *types.Transaction { return types.NewTransaction(uint64(*args.Nonce), *args.To, (*big.Int)(args.Value), uint64(*args.Gas), (*big.Int)(args.GasPrice), input) } -// submitTransaction is a helper function that submits tx to txPool and logs a message. -func submitTransaction(ctx context.Context, b Backend, tx *types.Transaction) (common.Hash, error) { +// SubmitTransaction is a helper function that submits tx to txPool and logs a message. +func SubmitTransaction(ctx context.Context, b Backend, tx *types.Transaction) (common.Hash, error) { if err := b.SendTx(ctx, tx); err != nil { return common.Hash{}, err } @@ -1234,7 +1234,7 @@ func (s *PublicTransactionPoolAPI) SendTransaction(ctx context.Context, args Sen if err != nil { return common.Hash{}, err } - return submitTransaction(ctx, s.b, signed) + return SubmitTransaction(ctx, s.b, signed) } // SendRawTransaction will add the signed transaction to the transaction pool. @@ -1244,7 +1244,7 @@ func (s *PublicTransactionPoolAPI) SendRawTransaction(ctx context.Context, encod if err := rlp.DecodeBytes(encodedTx, tx); err != nil { return common.Hash{}, err } - return submitTransaction(ctx, s.b, tx) + return SubmitTransaction(ctx, s.b, tx) } // Sign calculates an ECDSA signature for: From c6c6a7a68fc87f191d7603fd6c5f2f48d98fd0eb Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Tue, 16 Oct 2018 12:39:07 +0100 Subject: [PATCH 13/40] Add Call and EstimateGas to graphQL API --- ethgraphql/main.go | 110 +++++++++++++++++++++++++++++++++++++++++ internal/ethapi/api.go | 24 +++++---- 2 files changed, 124 insertions(+), 10 deletions(-) diff --git a/ethgraphql/main.go b/ethgraphql/main.go index cd36ab3a929c..8e60cb02d742 100644 --- a/ethgraphql/main.go +++ b/ethgraphql/main.go @@ -23,12 +23,14 @@ import ( "net" "net/http" "strconv" + "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/eth" "github.com/ethereum/go-ethereum/internal/ethapi" "github.com/ethereum/go-ethereum/log" @@ -892,6 +894,96 @@ func (r *Resolver) SendRawTransaction(ctx context.Context, args struct{ Data Hex return Bytes32{hash}, err } +type CallData struct { + From *Address + To *Address + Gas *int32 + GasPrice *BigNum + Value *BigNum + Data *HexBytes + BlockNumber *int32 +} + +type CallResult struct { + data HexBytes + gasUsed int32 + status int32 +} + +func (c *CallResult) Data() HexBytes { + return c.data +} + +func (c *CallResult) GasUsed() int32 { + return c.gasUsed +} + +func (c *CallResult) Status() int32 { + return c.status +} + +func convertCallData(data CallData) (ethapi.CallArgs, rpc.BlockNumber) { + callArgs := ethapi.CallArgs{} + if data.From != nil { + callArgs.From = data.From.Address + } + if data.To != nil { + addr := data.To.Address + callArgs.To = &addr + } + if data.Gas != nil { + callArgs.Gas = hexutil.Uint64(*data.Gas) + } + if data.GasPrice != nil { + callArgs.GasPrice = hexutil.Big(*data.GasPrice.Int) + } + if data.Value != nil { + callArgs.Value = hexutil.Big(*data.Value.Int) + } + if data.Data != nil { + callArgs.Data = data.Data.Bytes + } + + blockNumber := rpc.LatestBlockNumber + if data.BlockNumber != nil { + blockNumber = rpc.BlockNumber(*data.BlockNumber) + } + + return callArgs, blockNumber +} + +func (r *Resolver) Call(ctx context.Context, args struct{ Data CallData }) (*CallResult, error) { + be, err := getBackend(r.node) + if err != nil { + return nil, err + } + + callArgs, blockNumber := convertCallData(args.Data) + + result, gas, failed, err := ethapi.DoCall(ctx, be, callArgs, blockNumber, vm.Config{}, 5*time.Second) + status := int32(1) + if failed { + status = 0 + } + return &CallResult{ + data: HexBytes{result}, + gasUsed: int32(gas), + status: status, + }, err +} + +func (r *Resolver) EstimateGas(ctx context.Context, args struct{ Data CallData }) (int32, error) { + be, err := getBackend(r.node) + if err != nil { + return 0, err + } + + callArgs, blockNumber := convertCallData(args.Data) + + gas, err := ethapi.DoEstimateGas(ctx, be, callArgs, blockNumber) + return int32(gas), err +} + func NewHandler(n *node.Node) (http.Handler, error) { q := Resolver{n} @@ -967,11 +1059,29 @@ func NewHandler(n *node.Node) (http.Handler, error) { transactionAt(index: Int!): Transaction } + input CallData { + from: Address + to: Address + gas: Int + gasPrice: BigNum + value: BigNum + data: HexBytes + blockNumber: Int + } + + type CallResult { + data: HexBytes! + gasUsed: Int! + status: Int! + } + type Query { account(address: Address!, blockNumber: Int): Account! block(number: Int, hash: Bytes32): Block blocks(from: Int!, to: Int): [Block!]! transaction(hash: Bytes32!): Transaction + call(data: CallData!): CallResult + estimateGas(data: CallData!): Int! } type Mutation { diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index f8933f1e4d34..75f638d91e09 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -617,17 +617,17 @@ type CallArgs struct { Data hexutil.Bytes `json:"data"` } -func (s *PublicBlockChainAPI) doCall(ctx context.Context, args CallArgs, blockNr rpc.BlockNumber, vmCfg vm.Config, timeout time.Duration) ([]byte, uint64, bool, error) { +func DoCall(ctx context.Context, b Backend, args CallArgs, blockNr rpc.BlockNumber, vmCfg vm.Config, timeout time.Duration) ([]byte, uint64, bool, error) { defer func(start time.Time) { log.Debug("Executing EVM call finished", "runtime", time.Since(start)) }(time.Now()) - state, header, err := s.b.StateAndHeaderByNumber(ctx, blockNr) + state, header, err := b.StateAndHeaderByNumber(ctx, blockNr) if state == nil || err != nil { return nil, 0, false, err } // Set sender address or use a default if none specified addr := args.From if addr == (common.Address{}) { - if wallets := s.b.AccountManager().Wallets(); len(wallets) > 0 { + if wallets := b.AccountManager().Wallets(); len(wallets) > 0 { if accounts := wallets[0].Accounts(); len(accounts) > 0 { addr = accounts[0].Address } @@ -658,7 +658,7 @@ func (s *PublicBlockChainAPI) doCall(ctx context.Context, args CallArgs, blockNr defer cancel() // Get a new instance of the EVM. - evm, vmError, err := s.b.GetEVM(ctx, msg, state, header, vmCfg) + evm, vmError, err := b.GetEVM(ctx, msg, state, header, vmCfg) if err != nil { return nil, 0, false, err } @@ -682,13 +682,13 @@ func (s *PublicBlockChainAPI) doCall(ctx context.Context, args CallArgs, blockNr // Call executes the given transaction on the state for the given block number. // It doesn't make and changes in the state/blockchain and is useful to execute and retrieve values. func (s *PublicBlockChainAPI) Call(ctx context.Context, args CallArgs, blockNr rpc.BlockNumber) (hexutil.Bytes, error) { - result, _, _, err := s.doCall(ctx, args, blockNr, vm.Config{}, 5*time.Second) + result, _, _, err := DoCall(ctx, s.b, args, blockNr, vm.Config{}, 5*time.Second) return (hexutil.Bytes)(result), err } -// EstimateGas returns an estimate of the amount of gas needed to execute the +// DoEstimateGas returns an estimate of the amount of gas needed to execute the // given transaction against the current pending block. -func (s *PublicBlockChainAPI) EstimateGas(ctx context.Context, args CallArgs) (hexutil.Uint64, error) { +func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNr rpc.BlockNumber) (hexutil.Uint64, error) { // Binary search the gas requirement, as it may be higher than the amount used var ( lo uint64 = params.TxGas - 1 @@ -698,8 +698,8 @@ func (s *PublicBlockChainAPI) EstimateGas(ctx context.Context, args CallArgs) (h if uint64(args.Gas) >= params.TxGas { hi = uint64(args.Gas) } else { - // Retrieve the current pending block to act as the gas ceiling - block, err := s.b.BlockByNumber(ctx, rpc.PendingBlockNumber) + // Retrieve the block to act as the gas ceiling + block, err := b.BlockByNumber(ctx, blockNr) if err != nil { return 0, err } @@ -711,7 +711,7 @@ func (s *PublicBlockChainAPI) EstimateGas(ctx context.Context, args CallArgs) (h executable := func(gas uint64) bool { args.Gas = hexutil.Uint64(gas) - _, _, failed, err := s.doCall(ctx, args, rpc.PendingBlockNumber, vm.Config{}, 0) + _, _, failed, err := DoCall(ctx, b, args, blockNr, vm.Config{}, 0) if err != nil || failed { return false } @@ -735,6 +735,10 @@ func (s *PublicBlockChainAPI) EstimateGas(ctx context.Context, args CallArgs) (h return hexutil.Uint64(hi), nil } +func (s *PublicBlockChainAPI) EstimateGas(ctx context.Context, args CallArgs) (hexutil.Uint64, error) { + return DoEstimateGas(ctx, s.b, args, rpc.PendingBlockNumber) +} + // ExecutionResult groups all structured logs emitted by the EVM // while replaying a transaction in debug mode as well as transaction // execution status, the amount of gas used and the return value From 59e2a7f2c8f49a9ede3a1158a089d3cbacf5b90f Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Tue, 16 Oct 2018 13:31:36 +0100 Subject: [PATCH 14/40] Refactored to use hexutil.Bytes instead of HexBytes --- common/hexutil/json.go | 17 +++++++++ ethgraphql/main.go | 81 ++++++++++++++++-------------------------- 2 files changed, 47 insertions(+), 51 deletions(-) diff --git a/common/hexutil/json.go b/common/hexutil/json.go index fbc21241c8ad..645de788ce5f 100644 --- a/common/hexutil/json.go +++ b/common/hexutil/json.go @@ -72,6 +72,23 @@ func (b Bytes) String() string { return Encode(b) } +func (b Bytes) ImplementsGraphQLType(name string) bool { return name == "Bytes" } + +func (b *Bytes) UnmarshalGraphQL(input interface{}) error { + var err error + switch input := input.(type) { + case string: + data, err := Decode(input) + if err != nil { + return err + } + *b = data + default: + err = fmt.Errorf("Unexpected type for Bytes: %v", input) + } + return err +} + // UnmarshalFixedJSON decodes the input as a string with 0x prefix. The length of out // determines the required input length. This function is commonly used to implement the // UnmarshalJSON method for fixed-size types. diff --git a/ethgraphql/main.go b/ethgraphql/main.go index 8e60cb02d742..031451e9cbe4 100644 --- a/ethgraphql/main.go +++ b/ethgraphql/main.go @@ -50,27 +50,6 @@ func getBackend(n *node.Node) (ethapi.Backend, error) { return ethereum.APIBackend, nil } -type HexBytes struct { - hexutil.Bytes -} - -func (h HexBytes) ImplementsGraphQLType(name string) bool { return name == "HexBytes" } - -func (h *HexBytes) UnmarshalGraphQL(input interface{}) error { - var err error - switch input := input.(type) { - case string: - data, err := hexutil.Decode(input) - if err != nil { - return err - } - *h = HexBytes{data} - default: - err = fmt.Errorf("Unexpected type for HexBytes: %v", input) - } - return err -} - type Bytes32 struct { common.Hash } @@ -168,13 +147,13 @@ func (a *Account) TransactionCount(ctx context.Context) (int32, error) { return int32(state.GetNonce(a.address)), nil } -func (a *Account) Code(ctx context.Context) (HexBytes, error) { +func (a *Account) Code(ctx context.Context) (hexutil.Bytes, error) { state, err := a.getState(ctx) if err != nil { - return HexBytes{}, err + return hexutil.Bytes{}, err } - return HexBytes{state.GetCode(a.address)}, nil + return hexutil.Bytes(state.GetCode(a.address)), nil } type StorageSlotArgs struct { @@ -220,8 +199,8 @@ func (l *Log) Topics(ctx context.Context) []Bytes32 { return ret } -func (l *Log) Data(ctx context.Context) HexBytes { - return HexBytes{l.log.Data} +func (l *Log) Data(ctx context.Context) hexutil.Bytes { + return hexutil.Bytes(l.log.Data) } type Transaction struct { @@ -258,12 +237,12 @@ func (tx *Transaction) Hash(ctx context.Context) Bytes32 { return Bytes32{tx.hash} } -func (t *Transaction) InputData(ctx context.Context) (HexBytes, error) { +func (t *Transaction) InputData(ctx context.Context) (hexutil.Bytes, error) { tx, err := t.resolve(ctx) if err != nil || tx == nil { - return HexBytes{}, err + return hexutil.Bytes{}, err } - return HexBytes{tx.Data()}, nil + return hexutil.Bytes(tx.Data()), nil } func (t *Transaction) Gas(ctx context.Context) (int32, error) { @@ -637,20 +616,20 @@ func (b *Block) Ommers(ctx context.Context) ([]*Block, error) { return ret, nil } -func (b *Block) ExtraData(ctx context.Context) (HexBytes, error) { +func (b *Block) ExtraData(ctx context.Context) (hexutil.Bytes, error) { block, err := b.resolve(ctx) if err != nil { - return HexBytes{}, err + return hexutil.Bytes{}, err } - return HexBytes{block.Extra()}, nil + return hexutil.Bytes(block.Extra()), nil } -func (b *Block) LogsBloom(ctx context.Context) (HexBytes, error) { +func (b *Block) LogsBloom(ctx context.Context) (hexutil.Bytes, error) { block, err := b.resolve(ctx) if err != nil { - return HexBytes{}, err + return hexutil.Bytes{}, err } - return HexBytes{block.Bloom().Bytes()}, nil + return hexutil.Bytes(block.Bloom().Bytes()), nil } func (b *Block) TotalDifficulty(ctx context.Context) (BigNum, error) { @@ -880,14 +859,14 @@ func (r *Resolver) Transaction(ctx context.Context, args TransactionArgs) (*Tran return tx, nil } -func (r *Resolver) SendRawTransaction(ctx context.Context, args struct{ Data HexBytes }) (Bytes32, error) { +func (r *Resolver) SendRawTransaction(ctx context.Context, args struct{ Data hexutil.Bytes }) (Bytes32, error) { be, err := getBackend(r.node) if err != nil { return Bytes32{}, err } tx := new(types.Transaction) - if err := rlp.DecodeBytes(args.Data.Bytes, tx); err != nil { + if err := rlp.DecodeBytes(args.Data, tx); err != nil { return Bytes32{}, err } hash, err := ethapi.SubmitTransaction(ctx, be, tx) @@ -900,17 +879,17 @@ type CallData struct { Gas *int32 GasPrice *BigNum Value *BigNum - Data *HexBytes + Data *hexutil.Bytes BlockNumber *int32 } type CallResult struct { - data HexBytes + data hexutil.Bytes gasUsed int32 status int32 } -func (c *CallResult) Data() HexBytes { +func (c *CallResult) Data() hexutil.Bytes { return c.data } @@ -941,7 +920,7 @@ func convertCallData(data CallData) (ethapi.CallArgs, rpc.BlockNumber) { callArgs.Value = hexutil.Big(*data.Value.Int) } if data.Data != nil { - callArgs.Data = data.Data.Bytes + callArgs.Data = *data.Data } blockNumber := rpc.LatestBlockNumber @@ -966,7 +945,7 @@ func (r *Resolver) Call(ctx context.Context, args struct{ Data CallData }) (*Cal status = 0 } return &CallResult{ - data: HexBytes{result}, + data: hexutil.Bytes(result), gasUsed: int32(gas), status: status, }, err @@ -990,7 +969,7 @@ func NewHandler(n *node.Node) (http.Handler, error) { s := ` scalar Bytes32 scalar Address - scalar HexBytes + scalar Bytes scalar BigNum schema { @@ -1002,7 +981,7 @@ func NewHandler(n *node.Node) (http.Handler, error) { address: Address! balance: BigNum! transactionCount: Int! - code: HexBytes! + code: Bytes! storage(slot: Bytes32!): Bytes32! } @@ -1010,7 +989,7 @@ func NewHandler(n *node.Node) (http.Handler, error) { index: Int! account(block: Int): Account! topics: [Bytes32!]! - data: HexBytes! + data: Bytes! transaction: Transaction! } @@ -1023,7 +1002,7 @@ func NewHandler(n *node.Node) (http.Handler, error) { value: BigNum! gasPrice: BigNum! gas: Int! - inputData: HexBytes! + inputData: Bytes! block: Block status: Int @@ -1043,11 +1022,11 @@ func NewHandler(n *node.Node) (http.Handler, error) { stateRoot: Bytes32! receiptsRoot: Bytes32! miner(block: Int): Account! - extraData: HexBytes! + extraData: Bytes! gasLimit: Int! gasUsed: Int! timestamp: BigNum! - logsBloom: HexBytes! + logsBloom: Bytes! mixHash: Bytes32! difficulty: BigNum! totalDifficulty: BigNum! @@ -1065,12 +1044,12 @@ func NewHandler(n *node.Node) (http.Handler, error) { gas: Int gasPrice: BigNum value: BigNum - data: HexBytes + data: Bytes blockNumber: Int } type CallResult { - data: HexBytes! + data: Bytes! gasUsed: Int! status: Int! } @@ -1085,7 +1064,7 @@ func NewHandler(n *node.Node) (http.Handler, error) { } type Mutation { - sendRawTransaction(data: HexBytes!): Bytes32! + sendRawTransaction(data: Bytes!): Bytes32! } ` schema, err := graphql.ParseSchema(s, &q) From 8790e18ae7e1b2b679f87d296ece5729d130f4e4 Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Tue, 16 Oct 2018 13:48:23 +0100 Subject: [PATCH 15/40] Replace BigNum with hexutil.Big --- common/hexutil/json.go | 21 +++++++++ ethgraphql/main.go | 98 ++++++++++++++++-------------------------- 2 files changed, 57 insertions(+), 62 deletions(-) diff --git a/common/hexutil/json.go b/common/hexutil/json.go index 645de788ce5f..7b55fba174ab 100644 --- a/common/hexutil/json.go +++ b/common/hexutil/json.go @@ -204,6 +204,27 @@ func (b *Big) String() string { return EncodeBig(b.ToInt()) } +func (b Big) ImplementsGraphQLType(name string) bool { return name == "BigInt" } + +func (b *Big) UnmarshalGraphQL(input interface{}) error { + var err error + switch input := input.(type) { + case string: + return b.UnmarshalText([]byte(input)) + case int32: + var num big.Int + num.SetInt64(int64(input)) + *b = Big(num) + default: + err = fmt.Errorf("Unexpected type for BigInt: %v", input) + } + return err +} + +func (b Big) MarshalJSON() ([]byte, error) { + return strconv.AppendQuote(nil, (*big.Int)(&b).Text(10)), nil +} + // Uint64 marshals/unmarshals as a JSON string with 0x prefix. // The zero value marshals as "0x0". type Uint64 uint64 diff --git a/ethgraphql/main.go b/ethgraphql/main.go index 031451e9cbe4..7480282d0841 100644 --- a/ethgraphql/main.go +++ b/ethgraphql/main.go @@ -22,7 +22,6 @@ import ( "math/big" "net" "net/http" - "strconv" "time" "github.com/ethereum/go-ethereum/common" @@ -84,31 +83,6 @@ func (h *Address) UnmarshalGraphQL(input interface{}) error { return err } -type BigNum struct { - *big.Int -} - -func (bn BigNum) ImplementsGraphQLType(name string) bool { return name == "BigNum" } - -func (bn *BigNum) UnmarshalGraphQL(input interface{}) error { - var err error - switch input := input.(type) { - case string: - i := big.NewInt(0) - i.SetString(input, 10) - *bn = BigNum{i} - case int32: - *bn = BigNum{big.NewInt(int64(input))} - default: - err = fmt.Errorf("Unexpected type for Hash: %v", input) - } - return err -} - -func (bn BigNum) MarshalJSON() ([]byte, error) { - return strconv.AppendQuote(nil, bn.Text(10)), nil -} - type Account struct { node *node.Node address common.Address @@ -129,13 +103,13 @@ func (a *Account) Address(ctx context.Context) (Address, error) { return Address{a.address}, nil } -func (a *Account) Balance(ctx context.Context) (BigNum, error) { +func (a *Account) Balance(ctx context.Context) (hexutil.Big, error) { state, err := a.getState(ctx) if err != nil { - return BigNum{}, err + return hexutil.Big{}, err } - return BigNum{state.GetBalance(a.address)}, nil + return hexutil.Big(*state.GetBalance(a.address)), nil } func (a *Account) TransactionCount(ctx context.Context) (int32, error) { @@ -253,20 +227,20 @@ func (t *Transaction) Gas(ctx context.Context) (int32, error) { return int32(tx.Gas()), nil } -func (t *Transaction) GasPrice(ctx context.Context) (BigNum, error) { +func (t *Transaction) GasPrice(ctx context.Context) (hexutil.Big, error) { tx, err := t.resolve(ctx) if err != nil || tx == nil { - return BigNum{}, err + return hexutil.Big{}, err } - return BigNum{tx.GasPrice()}, nil + return hexutil.Big(*tx.GasPrice()), nil } -func (t *Transaction) Value(ctx context.Context) (BigNum, error) { +func (t *Transaction) Value(ctx context.Context) (hexutil.Big, error) { tx, err := t.resolve(ctx) if err != nil || tx == nil { - return BigNum{}, err + return hexutil.Big{}, err } - return BigNum{tx.Value()}, nil + return hexutil.Big(*tx.Value()), nil } func (t *Transaction) Nonce(ctx context.Context) (int32, error) { @@ -524,30 +498,30 @@ func (b *Block) Parent(ctx context.Context) (*Block, error) { return nil, nil } -func (b *Block) Difficulty(ctx context.Context) (BigNum, error) { +func (b *Block) Difficulty(ctx context.Context) (hexutil.Big, error) { block, err := b.resolve(ctx) if err != nil { - return BigNum{}, err + return hexutil.Big{}, err } - return BigNum{block.Difficulty()}, nil + return hexutil.Big(*block.Difficulty()), nil } -func (b *Block) Timestamp(ctx context.Context) (BigNum, error) { +func (b *Block) Timestamp(ctx context.Context) (hexutil.Big, error) { block, err := b.resolve(ctx) if err != nil { - return BigNum{}, err + return hexutil.Big{}, err } - return BigNum{block.Time()}, nil + return hexutil.Big(*block.Time()), nil } -func (b *Block) Nonce(ctx context.Context) (BigNum, error) { +func (b *Block) Nonce(ctx context.Context) (hexutil.Big, error) { block, err := b.resolve(ctx) if err != nil { - return BigNum{}, err + return hexutil.Big{}, err } i := new(big.Int) i.SetUint64(block.Nonce()) - return BigNum{i}, nil + return hexutil.Big(*i), nil } func (b *Block) MixHash(ctx context.Context) (Bytes32, error) { @@ -632,22 +606,22 @@ func (b *Block) LogsBloom(ctx context.Context) (hexutil.Bytes, error) { return hexutil.Bytes(block.Bloom().Bytes()), nil } -func (b *Block) TotalDifficulty(ctx context.Context) (BigNum, error) { +func (b *Block) TotalDifficulty(ctx context.Context) (hexutil.Big, error) { h := b.hash if h == (common.Hash{}) { block, err := b.resolve(ctx) if err != nil { - return BigNum{}, err + return hexutil.Big{}, err } h = block.Hash() } be, err := getBackend(b.node) if err != nil { - return BigNum{}, err + return hexutil.Big{}, err } - return BigNum{be.GetTd(h)}, nil + return hexutil.Big(*be.GetTd(h)), nil } type BlockNumberArgs struct { @@ -877,8 +851,8 @@ type CallData struct { From *Address To *Address Gas *int32 - GasPrice *BigNum - Value *BigNum + GasPrice *hexutil.Big + Value *hexutil.Big Data *hexutil.Bytes BlockNumber *int32 } @@ -914,10 +888,10 @@ func convertCallData(data CallData) (ethapi.CallArgs, rpc.BlockNumber) { callArgs.Gas = hexutil.Uint64(*data.Gas) } if data.GasPrice != nil { - callArgs.GasPrice = hexutil.Big(*data.GasPrice.Int) + callArgs.GasPrice = *data.GasPrice } if data.Value != nil { - callArgs.Value = hexutil.Big(*data.Value.Int) + callArgs.Value = *data.Value } if data.Data != nil { callArgs.Data = *data.Data @@ -970,7 +944,7 @@ func NewHandler(n *node.Node) (http.Handler, error) { scalar Bytes32 scalar Address scalar Bytes - scalar BigNum + scalar BigInt schema { query: Query @@ -979,7 +953,7 @@ func NewHandler(n *node.Node) (http.Handler, error) { type Account { address: Address! - balance: BigNum! + balance: BigInt! transactionCount: Int! code: Bytes! storage(slot: Bytes32!): Bytes32! @@ -999,8 +973,8 @@ func NewHandler(n *node.Node) (http.Handler, error) { index: Int from(block: Int): Account! to(block: Int): Account - value: BigNum! - gasPrice: BigNum! + value: BigInt! + gasPrice: BigInt! gas: Int! inputData: Bytes! block: Block @@ -1016,7 +990,7 @@ func NewHandler(n *node.Node) (http.Handler, error) { number: Int! hash: Bytes32! parent: Block - nonce: BigNum! + nonce: BigInt! transactionsRoot: Bytes32! transactionCount: Int! stateRoot: Bytes32! @@ -1025,11 +999,11 @@ func NewHandler(n *node.Node) (http.Handler, error) { extraData: Bytes! gasLimit: Int! gasUsed: Int! - timestamp: BigNum! + timestamp: BigInt! logsBloom: Bytes! mixHash: Bytes32! - difficulty: BigNum! - totalDifficulty: BigNum! + difficulty: BigInt! + totalDifficulty: BigInt! ommerCount: Int! ommers: [Block]! ommerAt(index: Int!): Block @@ -1042,8 +1016,8 @@ func NewHandler(n *node.Node) (http.Handler, error) { from: Address to: Address gas: Int - gasPrice: BigNum - value: BigNum + gasPrice: BigInt + value: BigInt data: Bytes blockNumber: Int } From ba6f5310276932c70be30bf4bd2543727b9d6a73 Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Tue, 16 Oct 2018 14:13:21 +0100 Subject: [PATCH 16/40] Refactor call and estimateGas to use ethapi struct type --- common/hexutil/json.go | 15 +++++++++ common/types.go | 13 ++++++++ ethgraphql/main.go | 75 ++++++++++++++++-------------------------- internal/ethapi/api.go | 45 ++++++++++++++++--------- 4 files changed, 85 insertions(+), 63 deletions(-) diff --git a/common/hexutil/json.go b/common/hexutil/json.go index 7b55fba174ab..cbe0943a2dd6 100644 --- a/common/hexutil/json.go +++ b/common/hexutil/json.go @@ -272,6 +272,21 @@ func (b Uint64) String() string { return EncodeUint64(uint64(b)) } +func (b Uint64) ImplementsGraphQLType(name string) bool { return name == "Long" } + +func (b *Uint64) UnmarshalGraphQL(input interface{}) error { + var err error + switch input := input.(type) { + case string: + return b.UnmarshalText([]byte(input)) + case int32: + *b = Uint64(input) + default: + err = fmt.Errorf("Unexpected type for BigInt: %v", input) + } + return err +} + // Uint marshals/unmarshals as a JSON string with 0x prefix. // The zero value marshals as "0x0". type Uint uint diff --git a/common/types.go b/common/types.go index a4b9995267dd..6ecef405921e 100644 --- a/common/types.go +++ b/common/types.go @@ -268,6 +268,19 @@ func (a Address) Value() (driver.Value, error) { return a[:], nil } +func (a Address) ImplementsGraphQLType(name string) bool { return name == "Address" } + +func (a *Address) UnmarshalGraphQL(input interface{}) error { + var err error + switch input := input.(type) { + case string: + *a = HexToAddress(input) + default: + err = fmt.Errorf("Unexpected type for Hash: %v", input) + } + return err +} + // UnprefixedAddress allows marshaling an Address without 0x prefix. type UnprefixedAddress Address diff --git a/ethgraphql/main.go b/ethgraphql/main.go index 7480282d0841..f700556adae3 100644 --- a/ethgraphql/main.go +++ b/ethgraphql/main.go @@ -848,13 +848,12 @@ func (r *Resolver) SendRawTransaction(ctx context.Context, args struct{ Data hex } type CallData struct { - From *Address - To *Address - Gas *int32 - GasPrice *hexutil.Big - Value *hexutil.Big - Data *hexutil.Bytes - BlockNumber *int32 + From *Address + To *Address + Gas *hexutil.Uint64 + GasPrice *hexutil.Big + Value *hexutil.Big + Data *hexutil.Bytes } type CallResult struct { @@ -875,45 +874,21 @@ func (c *CallResult) Status() int32 { return c.status } -func convertCallData(data CallData) (ethapi.CallArgs, rpc.BlockNumber) { - callArgs := ethapi.CallArgs{} - if data.From != nil { - callArgs.From = data.From.Address - } - if data.To != nil { - addr := data.To.Address - callArgs.To = &addr - } - if data.Gas != nil { - callArgs.Gas = hexutil.Uint64(*data.Gas) - } - if data.GasPrice != nil { - callArgs.GasPrice = *data.GasPrice - } - if data.Value != nil { - callArgs.Value = *data.Value - } - if data.Data != nil { - callArgs.Data = *data.Data - } - - blockNumber := rpc.LatestBlockNumber - if data.BlockNumber != nil { - blockNumber = rpc.BlockNumber(*data.BlockNumber) - } - - return callArgs, blockNumber -} - -func (r *Resolver) Call(ctx context.Context, args struct{ Data CallData }) (*CallResult, error) { +func (r *Resolver) Call(ctx context.Context, args struct { + Data ethapi.CallArgs + BlockNumber *int32 +}) (*CallResult, error) { be, err := getBackend(r.node) if err != nil { return nil, err } - callArgs, blockNumber := convertCallData(args.Data) + blockNumber := rpc.LatestBlockNumber + if args.BlockNumber != nil { + blockNumber = rpc.BlockNumber(*args.BlockNumber) + } - result, gas, failed, err := ethapi.DoCall(ctx, be, callArgs, blockNumber, vm.Config{}, 5*time.Second) + result, gas, failed, err := ethapi.DoCall(ctx, be, args.Data, blockNumber, vm.Config{}, 5*time.Second) status := int32(1) if failed { status = 0 @@ -925,15 +900,21 @@ func (r *Resolver) Call(ctx context.Context, args struct{ Data CallData }) (*Cal }, err } -func (r *Resolver) EstimateGas(ctx context.Context, args struct{ Data CallData }) (int32, error) { +func (r *Resolver) EstimateGas(ctx context.Context, args struct { + Data ethapi.CallArgs + BlockNumber *int32 +}) (int32, error) { be, err := getBackend(r.node) if err != nil { return 0, err } - callArgs, blockNumber := convertCallData(args.Data) + blockNumber := rpc.LatestBlockNumber + if args.BlockNumber != nil { + blockNumber = rpc.BlockNumber(*args.BlockNumber) + } - gas, err := ethapi.DoEstimateGas(ctx, be, callArgs, blockNumber) + gas, err := ethapi.DoEstimateGas(ctx, be, args.Data, blockNumber) return int32(gas), err } @@ -945,6 +926,7 @@ func NewHandler(n *node.Node) (http.Handler, error) { scalar Address scalar Bytes scalar BigInt + scalar Long schema { query: Query @@ -1015,11 +997,10 @@ func NewHandler(n *node.Node) (http.Handler, error) { input CallData { from: Address to: Address - gas: Int + gas: Long gasPrice: BigInt value: BigInt data: Bytes - blockNumber: Int } type CallResult { @@ -1033,8 +1014,8 @@ func NewHandler(n *node.Node) (http.Handler, error) { block(number: Int, hash: Bytes32): Block blocks(from: Int!, to: Int): [Block!]! transaction(hash: Bytes32!): Transaction - call(data: CallData!): CallResult - estimateGas(data: CallData!): Int! + call(data: CallData!, blockNumber: Int): CallResult + estimateGas(data: CallData!, blockNumber: Int): Int! } type Mutation { diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 75f638d91e09..b85350bdd724 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -609,12 +609,12 @@ func (s *PublicBlockChainAPI) GetStorageAt(ctx context.Context, address common.A // CallArgs represents the arguments for a call. type CallArgs struct { - From common.Address `json:"from"` + From *common.Address `json:"from"` To *common.Address `json:"to"` - Gas hexutil.Uint64 `json:"gas"` - GasPrice hexutil.Big `json:"gasPrice"` - Value hexutil.Big `json:"value"` - Data hexutil.Bytes `json:"data"` + Gas *hexutil.Uint64 `json:"gas"` + GasPrice *hexutil.Big `json:"gasPrice"` + Value *hexutil.Big `json:"value"` + Data *hexutil.Bytes `json:"data"` } func DoCall(ctx context.Context, b Backend, args CallArgs, blockNr rpc.BlockNumber, vmCfg vm.Config, timeout time.Duration) ([]byte, uint64, bool, error) { @@ -625,25 +625,38 @@ func DoCall(ctx context.Context, b Backend, args CallArgs, blockNr rpc.BlockNumb return nil, 0, false, err } // Set sender address or use a default if none specified - addr := args.From - if addr == (common.Address{}) { + var addr common.Address + if args.From == nil { if wallets := b.AccountManager().Wallets(); len(wallets) > 0 { if accounts := wallets[0].Accounts(); len(accounts) > 0 { addr = accounts[0].Address } } + } else { + addr = *args.From } // Set default gas & gas price if none were set - gas, gasPrice := uint64(args.Gas), args.GasPrice.ToInt() - if gas == 0 { - gas = math.MaxUint64 / 2 + gas := uint64(math.MaxUint64 / 2) + if args.Gas != nil { + gas = uint64(*args.Gas) } - if gasPrice.Sign() == 0 { - gasPrice = new(big.Int).SetUint64(defaultGasPrice) + gasPrice := new(big.Int).SetUint64(defaultGasPrice) + if args.GasPrice != nil { + gasPrice = args.GasPrice.ToInt() + } + + value := new(big.Int) + if args.Value != nil { + value = args.Value.ToInt() + } + + var data []byte + if args.Data != nil { + data = []byte(*args.Data) } // Create new call message - msg := types.NewMessage(addr, args.To, 0, args.Value.ToInt(), gas, gasPrice, args.Data, false) + msg := types.NewMessage(addr, args.To, 0, value, gas, gasPrice, data, false) // Setup context so it may be cancelled the call has completed // or, in case of unmetered gas, setup a context with a timeout. @@ -695,8 +708,8 @@ func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNr rpc.Bl hi uint64 cap uint64 ) - if uint64(args.Gas) >= params.TxGas { - hi = uint64(args.Gas) + if args.Gas != nil && uint64(*args.Gas) >= params.TxGas { + hi = uint64(*args.Gas) } else { // Retrieve the block to act as the gas ceiling block, err := b.BlockByNumber(ctx, blockNr) @@ -709,7 +722,7 @@ func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNr rpc.Bl // Create a helper to check if a gas allowance results in an executable transaction executable := func(gas uint64) bool { - args.Gas = hexutil.Uint64(gas) + args.Gas = (*hexutil.Uint64)(&gas) _, _, failed, err := DoCall(ctx, b, args, blockNr, vm.Config{}, 0) if err != nil || failed { From 268a03b1023fd6af797b02b51875deac18f63b16 Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Tue, 16 Oct 2018 14:16:20 +0100 Subject: [PATCH 17/40] Replace ethgraphql.Address with common.Address --- ethgraphql/main.go | 29 ++++++----------------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/ethgraphql/main.go b/ethgraphql/main.go index f700556adae3..f64dd4264e5b 100644 --- a/ethgraphql/main.go +++ b/ethgraphql/main.go @@ -66,23 +66,6 @@ func (b *Bytes32) UnmarshalGraphQL(input interface{}) error { return err } -type Address struct { - common.Address -} - -func (h Address) ImplementsGraphQLType(name string) bool { return name == "Address" } - -func (h *Address) UnmarshalGraphQL(input interface{}) error { - var err error - switch input := input.(type) { - case string: - *h = Address{common.HexToAddress(input)} - default: - err = fmt.Errorf("Unexpected type for Hash: %v", input) - } - return err -} - type Account struct { node *node.Node address common.Address @@ -99,8 +82,8 @@ func (a *Account) getState(ctx context.Context) (*state.StateDB, error) { return state, err } -func (a *Account) Address(ctx context.Context) (Address, error) { - return Address{a.address}, nil +func (a *Account) Address(ctx context.Context) (common.Address, error) { + return a.address, nil } func (a *Account) Balance(ctx context.Context) (hexutil.Big, error) { @@ -796,7 +779,7 @@ func (r *Resolver) Blocks(ctx context.Context, args BlocksArgs) ([]*Block, error } type AccountArgs struct { - Address Address + Address common.Address BlockNumber *int32 } @@ -808,7 +791,7 @@ func (r *Resolver) Account(ctx context.Context, args AccountArgs) *Account { return &Account{ node: r.node, - address: args.Address.Address, + address: args.Address, blockNumber: blockNumber, } } @@ -848,8 +831,8 @@ func (r *Resolver) SendRawTransaction(ctx context.Context, args struct{ Data hex } type CallData struct { - From *Address - To *Address + From *common.Address + To *common.Address Gas *hexutil.Uint64 GasPrice *hexutil.Big Value *hexutil.Big From 21cef58afb8b8e669054e39f70ec0728dbc2d688 Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Tue, 16 Oct 2018 14:22:02 +0100 Subject: [PATCH 18/40] Replace ethgraphql.Hash with common.Hash --- common/types.go | 13 +++++++ ethgraphql/main.go | 89 ++++++++++++++++++---------------------------- 2 files changed, 47 insertions(+), 55 deletions(-) diff --git a/common/types.go b/common/types.go index 6ecef405921e..251dd8459309 100644 --- a/common/types.go +++ b/common/types.go @@ -141,6 +141,19 @@ func (h Hash) Value() (driver.Value, error) { return h[:], nil } +func (_ Hash) ImplementsGraphQLType(name string) bool { return name == "Bytes32" } + +func (h *Hash) UnmarshalGraphQL(input interface{}) error { + var err error + switch input := input.(type) { + case string: + *h = HexToHash(input) + default: + err = fmt.Errorf("Unexpected type for Bytes32: %v", input) + } + return err +} + // UnprefixedHash allows marshaling a Hash without 0x prefix. type UnprefixedHash Hash diff --git a/ethgraphql/main.go b/ethgraphql/main.go index f64dd4264e5b..d42261b9782d 100644 --- a/ethgraphql/main.go +++ b/ethgraphql/main.go @@ -49,23 +49,6 @@ func getBackend(n *node.Node) (ethapi.Backend, error) { return ethereum.APIBackend, nil } -type Bytes32 struct { - common.Hash -} - -func (_ Bytes32) ImplementsGraphQLType(name string) bool { return name == "Bytes32" } - -func (b *Bytes32) UnmarshalGraphQL(input interface{}) error { - var err error - switch input := input.(type) { - case string: - *b = Bytes32{common.HexToHash(input)} - default: - err = fmt.Errorf("Unexpected type for Hash: %v", input) - } - return err -} - type Account struct { node *node.Node address common.Address @@ -114,16 +97,16 @@ func (a *Account) Code(ctx context.Context) (hexutil.Bytes, error) { } type StorageSlotArgs struct { - Slot Bytes32 + Slot common.Hash } -func (a *Account) Storage(ctx context.Context, args StorageSlotArgs) (Bytes32, error) { +func (a *Account) Storage(ctx context.Context, args StorageSlotArgs) (common.Hash, error) { state, err := a.getState(ctx) if err != nil { - return Bytes32{}, err + return common.Hash{}, err } - return Bytes32{state.GetState(a.address, args.Slot.Hash)}, nil + return state.GetState(a.address, args.Slot), nil } type Log struct { @@ -148,12 +131,8 @@ func (l *Log) Index(ctx context.Context) int32 { return int32(l.log.Index) } -func (l *Log) Topics(ctx context.Context) []Bytes32 { - ret := make([]Bytes32, 0, len(l.log.Topics)) - for _, topic := range l.log.Topics { - ret = append(ret, Bytes32{topic}) - } - return ret +func (l *Log) Topics(ctx context.Context) []common.Hash { + return l.log.Topics } func (l *Log) Data(ctx context.Context) hexutil.Bytes { @@ -190,8 +169,8 @@ func (t *Transaction) resolve(ctx context.Context) (*types.Transaction, error) { return t.tx, nil } -func (tx *Transaction) Hash(ctx context.Context) Bytes32 { - return Bytes32{tx.hash} +func (tx *Transaction) Hash(ctx context.Context) common.Hash { + return tx.hash } func (t *Transaction) InputData(ctx context.Context) (hexutil.Bytes, error) { @@ -429,15 +408,15 @@ func (b *Block) Number(ctx context.Context) (int32, error) { return int32(*b.num), nil } -func (b *Block) Hash(ctx context.Context) (Bytes32, error) { +func (b *Block) Hash(ctx context.Context) (common.Hash, error) { if b.hash == (common.Hash{}) { block, err := b.resolve(ctx) if err != nil { - return Bytes32{}, err + return common.Hash{}, err } b.hash = block.Hash() } - return Bytes32{b.hash}, nil + return b.hash, nil } func (b *Block) GasLimit(ctx context.Context) (int32, error) { @@ -507,44 +486,44 @@ func (b *Block) Nonce(ctx context.Context) (hexutil.Big, error) { return hexutil.Big(*i), nil } -func (b *Block) MixHash(ctx context.Context) (Bytes32, error) { +func (b *Block) MixHash(ctx context.Context) (common.Hash, error) { block, err := b.resolve(ctx) if err != nil { - return Bytes32{}, err + return common.Hash{}, err } - return Bytes32{block.MixDigest()}, nil + return block.MixDigest(), nil } -func (b *Block) TransactionsRoot(ctx context.Context) (Bytes32, error) { +func (b *Block) TransactionsRoot(ctx context.Context) (common.Hash, error) { block, err := b.resolve(ctx) if err != nil { - return Bytes32{}, err + return common.Hash{}, err } - return Bytes32{block.TxHash()}, nil + return block.TxHash(), nil } -func (b *Block) StateRoot(ctx context.Context) (Bytes32, error) { +func (b *Block) StateRoot(ctx context.Context) (common.Hash, error) { block, err := b.resolve(ctx) if err != nil { - return Bytes32{}, err + return common.Hash{}, err } - return Bytes32{block.Root()}, nil + return block.Root(), nil } -func (b *Block) ReceiptsRoot(ctx context.Context) (Bytes32, error) { +func (b *Block) ReceiptsRoot(ctx context.Context) (common.Hash, error) { block, err := b.resolve(ctx) if err != nil { - return Bytes32{}, err + return common.Hash{}, err } - return Bytes32{block.ReceiptHash()}, nil + return block.ReceiptHash(), nil } -func (b *Block) OmmerHash(ctx context.Context) (Bytes32, error) { +func (b *Block) OmmerHash(ctx context.Context) (common.Hash, error) { block, err := b.resolve(ctx) if err != nil { - return Bytes32{}, err + return common.Hash{}, err } - return Bytes32{block.UncleHash()}, nil + return block.UncleHash(), nil } func (b *Block) OmmerCount(ctx context.Context) (int32, error) { @@ -709,7 +688,7 @@ type Resolver struct { type BlockArgs struct { Number *int32 - Hash *Bytes32 + Hash *common.Hash } func (r *Resolver) Block(ctx context.Context, args BlockArgs) (*Block, error) { @@ -723,7 +702,7 @@ func (r *Resolver) Block(ctx context.Context, args BlockArgs) (*Block, error) { } else if args.Hash != nil { block = &Block{ node: r.node, - hash: args.Hash.Hash, + hash: *args.Hash, } } else { num := rpc.LatestBlockNumber @@ -797,13 +776,13 @@ func (r *Resolver) Account(ctx context.Context, args AccountArgs) *Account { } type TransactionArgs struct { - Hash Bytes32 + Hash common.Hash } func (r *Resolver) Transaction(ctx context.Context, args TransactionArgs) (*Transaction, error) { tx := &Transaction{ node: r.node, - hash: args.Hash.Hash, + hash: args.Hash, } // Resolve the transaction; if it doesn't exist, return nil. @@ -816,18 +795,18 @@ func (r *Resolver) Transaction(ctx context.Context, args TransactionArgs) (*Tran return tx, nil } -func (r *Resolver) SendRawTransaction(ctx context.Context, args struct{ Data hexutil.Bytes }) (Bytes32, error) { +func (r *Resolver) SendRawTransaction(ctx context.Context, args struct{ Data hexutil.Bytes }) (common.Hash, error) { be, err := getBackend(r.node) if err != nil { - return Bytes32{}, err + return common.Hash{}, err } tx := new(types.Transaction) if err := rlp.DecodeBytes(args.Data, tx); err != nil { - return Bytes32{}, err + return common.Hash{}, err } hash, err := ethapi.SubmitTransaction(ctx, be, tx) - return Bytes32{hash}, err + return hash, err } type CallData struct { From d92e3731231f318d338cb8a5566d2a2e46abfe8d Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Tue, 16 Oct 2018 14:30:13 +0100 Subject: [PATCH 19/40] Converted most quantities to Long instead of Int --- ethgraphql/main.go | 108 ++++++++++++++++++++++----------------------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/ethgraphql/main.go b/ethgraphql/main.go index d42261b9782d..c94eaeef4d25 100644 --- a/ethgraphql/main.go +++ b/ethgraphql/main.go @@ -78,13 +78,13 @@ func (a *Account) Balance(ctx context.Context) (hexutil.Big, error) { return hexutil.Big(*state.GetBalance(a.address)), nil } -func (a *Account) TransactionCount(ctx context.Context) (int32, error) { +func (a *Account) TransactionCount(ctx context.Context) (hexutil.Uint64, error) { state, err := a.getState(ctx) if err != nil { return 0, err } - return int32(state.GetNonce(a.address)), nil + return hexutil.Uint64(state.GetNonce(a.address)), nil } func (a *Account) Code(ctx context.Context) (hexutil.Bytes, error) { @@ -181,12 +181,12 @@ func (t *Transaction) InputData(ctx context.Context) (hexutil.Bytes, error) { return hexutil.Bytes(tx.Data()), nil } -func (t *Transaction) Gas(ctx context.Context) (int32, error) { +func (t *Transaction) Gas(ctx context.Context) (hexutil.Uint64, error) { tx, err := t.resolve(ctx) if err != nil || tx == nil { return 0, err } - return int32(tx.Gas()), nil + return hexutil.Uint64(tx.Gas()), nil } func (t *Transaction) GasPrice(ctx context.Context) (hexutil.Big, error) { @@ -205,12 +205,12 @@ func (t *Transaction) Value(ctx context.Context) (hexutil.Big, error) { return hexutil.Big(*tx.Value()), nil } -func (t *Transaction) Nonce(ctx context.Context) (int32, error) { +func (t *Transaction) Nonce(ctx context.Context) (hexutil.Uint64, error) { tx, err := t.resolve(ctx) if err != nil || tx == nil { return 0, err } - return int32(tx.Nonce()), nil + return hexutil.Uint64(tx.Nonce()), nil } func (t *Transaction) To(ctx context.Context, args BlockNumberArgs) (*Account, error) { @@ -285,33 +285,33 @@ func (t *Transaction) getReceipt(ctx context.Context) (*types.Receipt, error) { return receipts[t.index], nil } -func (t *Transaction) Status(ctx context.Context) (*int32, error) { +func (t *Transaction) Status(ctx context.Context) (*hexutil.Uint64, error) { receipt, err := t.getReceipt(ctx) if err != nil || receipt == nil { return nil, err } - ret := int32(receipt.Status) + ret := hexutil.Uint64(receipt.Status) return &ret, nil } -func (t *Transaction) GasUsed(ctx context.Context) (*int32, 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 := int32(receipt.GasUsed) + ret := hexutil.Uint64(receipt.GasUsed) return &ret, nil } -func (t *Transaction) CumulativeGasUsed(ctx context.Context) (*int32, 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 := int32(receipt.CumulativeGasUsed) + ret := hexutil.Uint64(receipt.CumulativeGasUsed) return &ret, nil } @@ -396,7 +396,7 @@ func (b *Block) resolveReceipts(ctx context.Context) ([]*types.Receipt, error) { return b.receipts, nil } -func (b *Block) Number(ctx context.Context) (int32, error) { +func (b *Block) Number(ctx context.Context) (hexutil.Uint64, error) { if b.num == nil || *b.num == rpc.LatestBlockNumber { block, err := b.resolve(ctx) if err != nil { @@ -405,7 +405,7 @@ func (b *Block) Number(ctx context.Context) (int32, error) { num := rpc.BlockNumber(block.Number().Uint64()) b.num = &num } - return int32(*b.num), nil + return hexutil.Uint64(*b.num), nil } func (b *Block) Hash(ctx context.Context) (common.Hash, error) { @@ -419,20 +419,20 @@ func (b *Block) Hash(ctx context.Context) (common.Hash, error) { return b.hash, nil } -func (b *Block) GasLimit(ctx context.Context) (int32, error) { +func (b *Block) GasLimit(ctx context.Context) (hexutil.Uint64, error) { block, err := b.resolve(ctx) if err != nil { return 0, err } - return int32(block.GasLimit()), nil + return hexutil.Uint64(block.GasLimit()), nil } -func (b *Block) GasUsed(ctx context.Context) (int32, error) { +func (b *Block) GasUsed(ctx context.Context) (hexutil.Uint64, error) { block, err := b.resolve(ctx) if err != nil { return 0, err } - return int32(block.GasUsed()), nil + return hexutil.Uint64(block.GasUsed()), nil } func (b *Block) Parent(ctx context.Context) (*Block, error) { @@ -587,7 +587,7 @@ func (b *Block) TotalDifficulty(ctx context.Context) (hexutil.Big, error) { } type BlockNumberArgs struct { - Block *int32 + Block *hexutil.Uint64 } func (a BlockNumberArgs) Number() rpc.BlockNumber { @@ -687,7 +687,7 @@ type Resolver struct { } type BlockArgs struct { - Number *int32 + Number *hexutil.Uint64 Hash *common.Hash } @@ -723,8 +723,8 @@ func (r *Resolver) Block(ctx context.Context, args BlockArgs) (*Block, error) { } type BlocksArgs struct { - From int32 - To *int32 + From hexutil.Uint64 + To *hexutil.Uint64 } func (r *Resolver) Blocks(ctx context.Context, args BlocksArgs) ([]*Block, error) { @@ -759,7 +759,7 @@ func (r *Resolver) Blocks(ctx context.Context, args BlocksArgs) ([]*Block, error type AccountArgs struct { Address common.Address - BlockNumber *int32 + BlockNumber *hexutil.Uint64 } func (r *Resolver) Account(ctx context.Context, args AccountArgs) *Account { @@ -820,25 +820,25 @@ type CallData struct { type CallResult struct { data hexutil.Bytes - gasUsed int32 - status int32 + gasUsed hexutil.Uint64 + status hexutil.Uint64 } func (c *CallResult) Data() hexutil.Bytes { return c.data } -func (c *CallResult) GasUsed() int32 { +func (c *CallResult) GasUsed() hexutil.Uint64 { return c.gasUsed } -func (c *CallResult) Status() int32 { +func (c *CallResult) Status() hexutil.Uint64 { return c.status } func (r *Resolver) Call(ctx context.Context, args struct { Data ethapi.CallArgs - BlockNumber *int32 + BlockNumber *hexutil.Uint64 }) (*CallResult, error) { be, err := getBackend(r.node) if err != nil { @@ -851,21 +851,21 @@ func (r *Resolver) Call(ctx context.Context, args struct { } result, gas, failed, err := ethapi.DoCall(ctx, be, args.Data, blockNumber, vm.Config{}, 5*time.Second) - status := int32(1) + status := hexutil.Uint64(1) if failed { status = 0 } return &CallResult{ data: hexutil.Bytes(result), - gasUsed: int32(gas), + gasUsed: hexutil.Uint64(gas), status: status, }, err } func (r *Resolver) EstimateGas(ctx context.Context, args struct { Data ethapi.CallArgs - BlockNumber *int32 -}) (int32, error) { + BlockNumber *hexutil.Uint64 +}) (hexutil.Uint64, error) { be, err := getBackend(r.node) if err != nil { return 0, err @@ -877,7 +877,7 @@ func (r *Resolver) EstimateGas(ctx context.Context, args struct { } gas, err := ethapi.DoEstimateGas(ctx, be, args.Data, blockNumber) - return int32(gas), err + return hexutil.Uint64(gas), err } func NewHandler(n *node.Node) (http.Handler, error) { @@ -898,14 +898,14 @@ func NewHandler(n *node.Node) (http.Handler, error) { type Account { address: Address! balance: BigInt! - transactionCount: Int! + transactionCount: Long! code: Bytes! storage(slot: Bytes32!): Bytes32! } type Log { index: Int! - account(block: Int): Account! + account(block: Long): Account! topics: [Bytes32!]! data: Bytes! transaction: Transaction! @@ -913,25 +913,25 @@ func NewHandler(n *node.Node) (http.Handler, error) { type Transaction { hash: Bytes32! - nonce: Int! + nonce: Long! index: Int - from(block: Int): Account! - to(block: Int): Account + from(block: Long): Account! + to(block: Long): Account value: BigInt! gasPrice: BigInt! - gas: Int! + gas: Long! inputData: Bytes! block: Block - status: Int - gasUsed: Int - cumulativeGasUsed: Int - createdContract(block: Int): Account + status: Long + gasUsed: Long + cumulativeGasUsed: Long + createdContract(block: Long): Account logs: [Log!] } type Block { - number: Int! + number: Long! hash: Bytes32! parent: Block nonce: BigInt! @@ -939,10 +939,10 @@ func NewHandler(n *node.Node) (http.Handler, error) { transactionCount: Int! stateRoot: Bytes32! receiptsRoot: Bytes32! - miner(block: Int): Account! + miner(block: Long): Account! extraData: Bytes! - gasLimit: Int! - gasUsed: Int! + gasLimit: Long! + gasUsed: Long! timestamp: BigInt! logsBloom: Bytes! mixHash: Bytes32! @@ -967,17 +967,17 @@ func NewHandler(n *node.Node) (http.Handler, error) { type CallResult { data: Bytes! - gasUsed: Int! - status: Int! + gasUsed: Long! + status: Long! } type Query { - account(address: Address!, blockNumber: Int): Account! - block(number: Int, hash: Bytes32): Block - blocks(from: Int!, to: Int): [Block!]! + account(address: Address!, blockNumber: Long): Account! + block(number: Long, hash: Bytes32): Block + blocks(from: Long!, to: Long): [Block!]! transaction(hash: Bytes32!): Transaction - call(data: CallData!, blockNumber: Int): CallResult - estimateGas(data: CallData!, blockNumber: Int): Int! + call(data: CallData!, blockNumber: Long): CallResult + estimateGas(data: CallData!, blockNumber: Long): Long! } type Mutation { From b235d1836753138b781d9bdaae779ae0c7cb3ec0 Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Tue, 16 Oct 2018 15:29:53 +0100 Subject: [PATCH 20/40] Add support for logs --- ethgraphql/main.go | 134 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 133 insertions(+), 1 deletion(-) diff --git a/ethgraphql/main.go b/ethgraphql/main.go index c94eaeef4d25..41753e944864 100644 --- a/ethgraphql/main.go +++ b/ethgraphql/main.go @@ -31,6 +31,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/eth" + "github.com/ethereum/go-ethereum/eth/filters" "github.com/ethereum/go-ethereum/internal/ethapi" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/node" @@ -41,7 +42,7 @@ import ( "github.com/graph-gophers/graphql-go/relay" ) -func getBackend(n *node.Node) (ethapi.Backend, error) { +func getBackend(n *node.Node) (*eth.EthAPIBackend, error) { var ethereum *eth.Ethereum if err := n.Service(ðereum); err != nil { return nil, err @@ -682,6 +683,72 @@ func (b *Block) OmmerAt(ctx context.Context, args ArrayIndexArgs) (*Block, error }, nil } +type BlockFilterCriteria struct { + 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 + // of topics. Topics matches a prefix of that list. An empty element slice 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 *[][]common.Hash +} + +func runFilter(ctx context.Context, node *node.Node, filter *filters.Filter) ([]*Log, error) { + logs, err := filter.Logs(ctx) + if err != nil || logs == nil { + return nil, err + } + + ret := make([]*Log, 0, len(logs)) + for _, log := range logs { + ret = append(ret, &Log{ + node: node, + transaction: &Transaction{hash: log.TxHash}, + log: log, + }) + } + return ret, nil +} + +func (b *Block) Logs(ctx context.Context, args struct{ Filter BlockFilterCriteria }) ([]*Log, error) { + be, err := getBackend(b.node) + if err != nil { + return nil, err + } + + var addresses []common.Address + if args.Filter.Addresses != nil { + addresses = *args.Filter.Addresses + } + + var topics [][]common.Hash + if args.Filter.Topics != nil { + topics = *args.Filter.Topics + } + + hash := b.hash + if hash == (common.Hash{}) { + block, err := b.resolve(ctx) + if err != nil { + return nil, err + } + hash = block.Hash() + } + + // Construct the range filter + filter := filters.NewBlockFilter(be, hash, addresses, topics) + + // Run the filter and return all the logs + return runFilter(ctx, b.node, filter) +} + type Resolver struct { node *node.Node } @@ -880,6 +947,57 @@ func (r *Resolver) EstimateGas(ctx context.Context, args struct { return hexutil.Uint64(gas), err } +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 + 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 + // of topics. Topics matches a prefix of that list. An empty element slice 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 *[][]common.Hash +} + +func (r *Resolver) Logs(ctx context.Context, args struct{ Filter FilterCriteria }) ([]*Log, error) { + be, err := getBackend(r.node) + if err != nil { + return nil, err + } + + // Convert the RPC block numbers into internal representations + begin := rpc.LatestBlockNumber.Int64() + if args.Filter.FromBlock != nil { + begin = int64(*args.Filter.FromBlock) + } + end := rpc.LatestBlockNumber.Int64() + if args.Filter.ToBlock != nil { + end = int64(*args.Filter.ToBlock) + } + + var addresses []common.Address + if args.Filter.Addresses != nil { + addresses = *args.Filter.Addresses + } + + var topics [][]common.Hash + if args.Filter.Topics != nil { + topics = *args.Filter.Topics + } + + // Construct the range filter + filter := filters.NewRangeFilter(filters.Backend(be), begin, end, addresses, topics) + + return runFilter(ctx, r.node, filter) +} + func NewHandler(n *node.Node) (http.Handler, error) { q := Resolver{n} @@ -930,6 +1048,11 @@ func NewHandler(n *node.Node) (http.Handler, error) { logs: [Log!] } + input BlockFilterCriteria { + addresses: [Address!] + topics: [[Bytes32!]!] + } + type Block { number: Long! hash: Bytes32! @@ -954,6 +1077,7 @@ func NewHandler(n *node.Node) (http.Handler, error) { ommerHash: Bytes32! transactions: [Transaction!]! transactionAt(index: Int!): Transaction + logs(filter: BlockFilterCriteria!): [Log!]! } input CallData { @@ -971,6 +1095,13 @@ func NewHandler(n *node.Node) (http.Handler, error) { status: Long! } + input FilterCriteria { + fromBlock: Long + toBlock: Long + addresses: [Address!] + topics: [[Bytes32!]!] + } + type Query { account(address: Address!, blockNumber: Long): Account! block(number: Long, hash: Bytes32): Block @@ -978,6 +1109,7 @@ func NewHandler(n *node.Node) (http.Handler, error) { transaction(hash: Bytes32!): Transaction call(data: CallData!, blockNumber: Long): CallResult estimateGas(data: CallData!, blockNumber: Long): Long! + logs(filter: FilterCriteria!): [Log!]! } type Mutation { From 549d64929d5bed7bf0cf0778bcde0186bd7ff346 Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Tue, 16 Oct 2018 15:40:38 +0100 Subject: [PATCH 21/40] Fix bug in runFilter --- ethgraphql/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ethgraphql/main.go b/ethgraphql/main.go index 41753e944864..5b3975ea6bcf 100644 --- a/ethgraphql/main.go +++ b/ethgraphql/main.go @@ -710,7 +710,7 @@ func runFilter(ctx context.Context, node *node.Node, filter *filters.Filter) ([] for _, log := range logs { ret = append(ret, &Log{ node: node, - transaction: &Transaction{hash: log.TxHash}, + transaction: &Transaction{node: node, hash: log.TxHash}, log: log, }) } From 19735c1d2304db05464fc5479f23b21728423d16 Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Tue, 16 Oct 2018 18:24:56 +0100 Subject: [PATCH 22/40] Restructured Transaction to work primarily with headers, so uncle data is reported properly --- ethgraphql/main.go | 156 ++++++++++++++++++++++++--------------------- 1 file changed, 85 insertions(+), 71 deletions(-) diff --git a/ethgraphql/main.go b/ethgraphql/main.go index 5b3975ea6bcf..67a89749d78e 100644 --- a/ethgraphql/main.go +++ b/ethgraphql/main.go @@ -19,7 +19,6 @@ package ethgraphql import ( "context" "fmt" - "math/big" "net" "net/http" "time" @@ -350,6 +349,7 @@ type Block struct { node *node.Node num *rpc.BlockNumber hash common.Hash + header *types.Header block *types.Block receipts []*types.Receipt } @@ -364,14 +364,26 @@ func (b *Block) resolve(ctx context.Context) (*types.Block, error) { return nil, err } - if b.num != nil { - b.block, err = be.BlockByNumber(ctx, *b.num) - } else { + if b.hash != (common.Hash{}) { b.block, err = be.GetBlock(ctx, b.hash) + } else { + b.block, err = be.BlockByNumber(ctx, *b.num) + } + if b.block != nil { + b.header = b.block.Header() } return b.block, err } +func (b *Block) resolveHeader(ctx context.Context) (*types.Header, error) { + if b.header == nil { + if _, err := b.resolve(ctx); err != nil { + return nil, err + } + } + return b.header, nil +} + func (b *Block) resolveReceipts(ctx context.Context) ([]*types.Receipt, error) { if b.receipts == nil { be, err := getBackend(b.node) @@ -381,11 +393,11 @@ func (b *Block) resolveReceipts(ctx context.Context) ([]*types.Receipt, error) { hash := b.hash if hash == (common.Hash{}) { - block, err := b.resolve(ctx) + header, err := b.resolveHeader(ctx) if err != nil { return nil, err } - hash = block.Hash() + hash = header.Hash() } receipts, err := be.GetReceipts(ctx, hash) @@ -399,11 +411,11 @@ func (b *Block) resolveReceipts(ctx context.Context) ([]*types.Receipt, error) { func (b *Block) Number(ctx context.Context) (hexutil.Uint64, error) { if b.num == nil || *b.num == rpc.LatestBlockNumber { - block, err := b.resolve(ctx) + header, err := b.resolveHeader(ctx) if err != nil { return 0, err } - num := rpc.BlockNumber(block.Number().Uint64()) + num := rpc.BlockNumber(header.Number.Uint64()) b.num = &num } return hexutil.Uint64(*b.num), nil @@ -411,45 +423,45 @@ func (b *Block) Number(ctx context.Context) (hexutil.Uint64, error) { func (b *Block) Hash(ctx context.Context) (common.Hash, error) { if b.hash == (common.Hash{}) { - block, err := b.resolve(ctx) + header, err := b.resolveHeader(ctx) if err != nil { return common.Hash{}, err } - b.hash = block.Hash() + b.hash = header.Hash() } return b.hash, nil } func (b *Block) GasLimit(ctx context.Context) (hexutil.Uint64, error) { - block, err := b.resolve(ctx) + header, err := b.resolveHeader(ctx) if err != nil { return 0, err } - return hexutil.Uint64(block.GasLimit()), nil + return hexutil.Uint64(header.GasLimit), nil } func (b *Block) GasUsed(ctx context.Context) (hexutil.Uint64, error) { - block, err := b.resolve(ctx) + header, err := b.resolveHeader(ctx) if err != nil { return 0, err } - return hexutil.Uint64(block.GasUsed()), nil + return hexutil.Uint64(header.GasUsed), nil } func (b *Block) Parent(ctx context.Context) (*Block, error) { // If the block hasn't been fetched, and we'll need it, fetch it. - if b.num == nil && b.hash != (common.Hash{}) && b.block == nil { + if b.num == nil && b.hash != (common.Hash{}) && b.header == nil { if _, err := b.resolve(ctx); err != nil { return nil, err } } - if b.block != nil && b.block.NumberU64() > 0 { - num := rpc.BlockNumber(b.block.NumberU64() - 1) + if b.header != nil && b.block.NumberU64() > 0 { + num := rpc.BlockNumber(b.header.Number.Uint64() - 1) return &Block{ node: b.node, num: &num, - hash: b.block.ParentHash(), + hash: b.header.ParentHash, }, nil } else if b.num != nil && *b.num != 0 { num := *b.num - 1 @@ -462,82 +474,81 @@ func (b *Block) Parent(ctx context.Context) (*Block, error) { } func (b *Block) Difficulty(ctx context.Context) (hexutil.Big, error) { - block, err := b.resolve(ctx) + header, err := b.resolveHeader(ctx) if err != nil { return hexutil.Big{}, err } - return hexutil.Big(*block.Difficulty()), nil + return hexutil.Big(*header.Difficulty), nil } func (b *Block) Timestamp(ctx context.Context) (hexutil.Big, error) { - block, err := b.resolve(ctx) + header, err := b.resolveHeader(ctx) if err != nil { return hexutil.Big{}, err } - return hexutil.Big(*block.Time()), nil + return hexutil.Big(*header.Time), nil } -func (b *Block) Nonce(ctx context.Context) (hexutil.Big, error) { - block, err := b.resolve(ctx) +func (b *Block) Nonce(ctx context.Context) (hexutil.Bytes, error) { + header, err := b.resolveHeader(ctx) if err != nil { - return hexutil.Big{}, err + return hexutil.Bytes{}, err } - i := new(big.Int) - i.SetUint64(block.Nonce()) - return hexutil.Big(*i), nil + return hexutil.Bytes(header.Nonce[:]), nil } func (b *Block) MixHash(ctx context.Context) (common.Hash, error) { - block, err := b.resolve(ctx) + header, err := b.resolveHeader(ctx) if err != nil { return common.Hash{}, err } - return block.MixDigest(), nil + return header.MixDigest, nil } func (b *Block) TransactionsRoot(ctx context.Context) (common.Hash, error) { - block, err := b.resolve(ctx) + header, err := b.resolveHeader(ctx) if err != nil { return common.Hash{}, err } - return block.TxHash(), nil + return header.TxHash, nil } func (b *Block) StateRoot(ctx context.Context) (common.Hash, error) { - block, err := b.resolve(ctx) + header, err := b.resolveHeader(ctx) if err != nil { return common.Hash{}, err } - return block.Root(), nil + return header.Root, nil } func (b *Block) ReceiptsRoot(ctx context.Context) (common.Hash, error) { - block, err := b.resolve(ctx) + header, err := b.resolveHeader(ctx) if err != nil { return common.Hash{}, err } - return block.ReceiptHash(), nil + return header.ReceiptHash, nil } func (b *Block) OmmerHash(ctx context.Context) (common.Hash, error) { - block, err := b.resolve(ctx) + header, err := b.resolveHeader(ctx) if err != nil { return common.Hash{}, err } - return block.UncleHash(), nil + return header.UncleHash, nil } -func (b *Block) OmmerCount(ctx context.Context) (int32, error) { +func (b *Block) OmmerCount(ctx context.Context) (*int32, error) { block, err := b.resolve(ctx) - if err != nil { - return 0, err + if err != nil || block == nil { + return nil, err } - return int32(len(block.Uncles())), nil + count := int32(len(block.Uncles())) + return &count, err } -func (b *Block) Ommers(ctx context.Context) ([]*Block, error) { +func (b *Block) Ommers(ctx context.Context) (*[]*Block, error) { block, err := b.resolve(ctx) - if err != nil { + if err != nil || block == nil { return nil, err } @@ -545,38 +556,39 @@ func (b *Block) Ommers(ctx context.Context) ([]*Block, error) { for _, uncle := range block.Uncles() { blockNumber := rpc.BlockNumber(uncle.Number.Uint64()) ret = append(ret, &Block{ - node: b.node, - num: &blockNumber, - hash: uncle.Hash(), + node: b.node, + num: &blockNumber, + hash: uncle.Hash(), + header: uncle, }) } - return ret, nil + return &ret, nil } func (b *Block) ExtraData(ctx context.Context) (hexutil.Bytes, error) { - block, err := b.resolve(ctx) + header, err := b.resolveHeader(ctx) if err != nil { return hexutil.Bytes{}, err } - return hexutil.Bytes(block.Extra()), nil + return hexutil.Bytes(header.Extra), nil } func (b *Block) LogsBloom(ctx context.Context) (hexutil.Bytes, error) { - block, err := b.resolve(ctx) + header, err := b.resolveHeader(ctx) if err != nil { return hexutil.Bytes{}, err } - return hexutil.Bytes(block.Bloom().Bytes()), nil + return hexutil.Bytes(header.Bloom.Bytes()), nil } func (b *Block) TotalDifficulty(ctx context.Context) (hexutil.Big, error) { h := b.hash if h == (common.Hash{}) { - block, err := b.resolve(ctx) + header, err := b.resolveHeader(ctx) if err != nil { return hexutil.Big{}, err } - h = block.Hash() + h = header.Hash() } be, err := getBackend(b.node) @@ -611,17 +623,18 @@ 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) (*int32, error) { block, err := b.resolve(ctx) - if err != nil { - return 0, err + if err != nil || block == nil { + return nil, err } - return int32(len(block.Transactions())), nil + count := int32(len(block.Transactions())) + return &count, err } -func (b *Block) Transactions(ctx context.Context) ([]*Transaction, error) { +func (b *Block) Transactions(ctx context.Context) (*[]*Transaction, error) { block, err := b.resolve(ctx) - if err != nil { + if err != nil || block == nil { return nil, err } @@ -635,7 +648,7 @@ func (b *Block) Transactions(ctx context.Context) ([]*Transaction, error) { index: uint64(i), }) } - return ret, nil + return &ret, nil } type ArrayIndexArgs struct { @@ -644,7 +657,7 @@ type ArrayIndexArgs struct { func (b *Block) TransactionAt(ctx context.Context, args ArrayIndexArgs) (*Transaction, error) { block, err := b.resolve(ctx) - if err != nil { + if err != nil || block == nil { return nil, err } @@ -665,7 +678,7 @@ func (b *Block) TransactionAt(ctx context.Context, args ArrayIndexArgs) (*Transa func (b *Block) OmmerAt(ctx context.Context, args ArrayIndexArgs) (*Block, error) { block, err := b.resolve(ctx) - if err != nil { + if err != nil || block == nil { return nil, err } @@ -677,9 +690,10 @@ func (b *Block) OmmerAt(ctx context.Context, args ArrayIndexArgs) (*Block, error uncle := uncles[args.Index] blockNumber := rpc.BlockNumber(uncle.Number.Uint64()) return &Block{ - node: b.node, - num: &blockNumber, - hash: uncle.Hash(), + node: b.node, + num: &blockNumber, + hash: uncle.Hash(), + header: uncle, }, nil } @@ -1057,9 +1071,9 @@ func NewHandler(n *node.Node) (http.Handler, error) { number: Long! hash: Bytes32! parent: Block - nonce: BigInt! + nonce: Bytes! transactionsRoot: Bytes32! - transactionCount: Int! + transactionCount: Int stateRoot: Bytes32! receiptsRoot: Bytes32! miner(block: Long): Account! @@ -1071,11 +1085,11 @@ func NewHandler(n *node.Node) (http.Handler, error) { mixHash: Bytes32! difficulty: BigInt! totalDifficulty: BigInt! - ommerCount: Int! - ommers: [Block]! + ommerCount: Int + ommers: [Block] ommerAt(index: Int!): Block ommerHash: Bytes32! - transactions: [Transaction!]! + transactions: [Transaction!] transactionAt(index: Int!): Transaction logs(filter: BlockFilterCriteria!): [Log!]! } From 135b7d9f5c31037728250eed21b8456ab999c8fe Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Tue, 16 Oct 2018 20:12:53 +0100 Subject: [PATCH 23/40] Add gasPrice API --- ethgraphql/main.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/ethgraphql/main.go b/ethgraphql/main.go index 67a89749d78e..89c778187dc1 100644 --- a/ethgraphql/main.go +++ b/ethgraphql/main.go @@ -1012,6 +1012,16 @@ func (r *Resolver) Logs(ctx context.Context, args struct{ Filter FilterCriteria return runFilter(ctx, r.node, filter) } +func (r *Resolver) GasPrice(ctx context.Context) (hexutil.Big, error) { + be, err := getBackend(r.node) + if err != nil { + return hexutil.Big{}, err + } + + price, err := be.SuggestPrice(ctx) + return hexutil.Big(*price), err +} + func NewHandler(n *node.Node) (http.Handler, error) { q := Resolver{n} @@ -1124,6 +1134,7 @@ func NewHandler(n *node.Node) (http.Handler, error) { call(data: CallData!, blockNumber: Long): CallResult estimateGas(data: CallData!, blockNumber: Long): Long! logs(filter: FilterCriteria!): [Log!]! + gasPrice: BigInt! } type Mutation { From 0e6fa8ef86d810cd724a7976b93049378979f03e Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Tue, 16 Oct 2018 20:15:27 +0100 Subject: [PATCH 24/40] Add protocolVersion API --- ethgraphql/main.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/ethgraphql/main.go b/ethgraphql/main.go index 89c778187dc1..cde9002f40e0 100644 --- a/ethgraphql/main.go +++ b/ethgraphql/main.go @@ -1022,6 +1022,15 @@ func (r *Resolver) GasPrice(ctx context.Context) (hexutil.Big, error) { return hexutil.Big(*price), err } +func (r *Resolver) ProtocolVersion(ctx context.Context) (int32, error) { + be, err := getBackend(r.node) + if err != nil { + return 0, err + } + + return int32(be.ProtocolVersion()), nil +} + func NewHandler(n *node.Node) (http.Handler, error) { q := Resolver{n} @@ -1135,6 +1144,7 @@ func NewHandler(n *node.Node) (http.Handler, error) { estimateGas(data: CallData!, blockNumber: Long): Long! logs(filter: FilterCriteria!): [Log!]! gasPrice: BigInt! + protocolVersion: Int! } type Mutation { From 32068e0e314263ef0bf4c5ab7fc58c4d0ed88c5b Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Tue, 16 Oct 2018 20:27:44 +0100 Subject: [PATCH 25/40] Add syncing API --- ethgraphql/main.go | 58 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/ethgraphql/main.go b/ethgraphql/main.go index cde9002f40e0..6d4dd88c1d37 100644 --- a/ethgraphql/main.go +++ b/ethgraphql/main.go @@ -23,6 +23,7 @@ import ( "net/http" "time" + "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/rawdb" @@ -1031,6 +1032,54 @@ func (r *Resolver) ProtocolVersion(ctx context.Context) (int32, error) { return int32(be.ProtocolVersion()), nil } +type SyncState struct { + progress ethereum.SyncProgress +} + +func (s *SyncState) StartingBlock() hexutil.Uint64 { + return hexutil.Uint64(s.progress.StartingBlock) +} + +func (s *SyncState) CurrentBlock() hexutil.Uint64 { + return hexutil.Uint64(s.progress.CurrentBlock) +} + +func (s *SyncState) HighestBlock() hexutil.Uint64 { + return hexutil.Uint64(s.progress.HighestBlock) +} + +func (s *SyncState) PulledStates() *hexutil.Uint64 { + ret := hexutil.Uint64(s.progress.PulledStates) + return &ret +} + +func (s *SyncState) KnownStates() *hexutil.Uint64 { + ret := hexutil.Uint64(s.progress.KnownStates) + return &ret +} + +// Syncing returns false in case the node is currently not syncing with the network. It can be up to date or has not +// yet received the latest block headers from its pears. In case it is synchronizing: +// - startingBlock: block number this node started to synchronise from +// - currentBlock: block number this node is currently importing +// - highestBlock: block number of the highest block header this node has received from peers +// - pulledStates: number of state entries processed until now +// - knownStates: number of known state entries that still need to be pulled +func (r *Resolver) Syncing() (*SyncState, error) { + be, err := getBackend(r.node) + if err != nil { + return nil, err + } + progress := be.Downloader().Progress() + + // Return not syncing if the synchronisation already completed + if progress.CurrentBlock >= progress.HighestBlock { + return nil, nil + } + // Otherwise gather the block sync stats + return &SyncState{progress}, nil +} + func NewHandler(n *node.Node) (http.Handler, error) { q := Resolver{n} @@ -1135,6 +1184,14 @@ func NewHandler(n *node.Node) (http.Handler, error) { topics: [[Bytes32!]!] } + type SyncState{ + startingBlock: Long! + currentBlock: Long! + highestBlock: Long! + pulledStates: Long + knownStates: Long + } + type Query { account(address: Address!, blockNumber: Long): Account! block(number: Long, hash: Bytes32): Block @@ -1145,6 +1202,7 @@ func NewHandler(n *node.Node) (http.Handler, error) { logs(filter: FilterCriteria!): [Log!]! gasPrice: BigInt! protocolVersion: Int! + syncing: SyncState } type Mutation { From c3387f8d0c267761f274c90120cb8b2f068499af Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Tue, 16 Oct 2018 20:38:19 +0100 Subject: [PATCH 26/40] Moved schema into its own source file --- ethgraphql/main.go | 130 +-------------------------------------------- 1 file changed, 2 insertions(+), 128 deletions(-) diff --git a/ethgraphql/main.go b/ethgraphql/main.go index 6d4dd88c1d37..67223ce28deb 100644 --- a/ethgraphql/main.go +++ b/ethgraphql/main.go @@ -1083,137 +1083,11 @@ func (r *Resolver) Syncing() (*SyncState, error) { func NewHandler(n *node.Node) (http.Handler, error) { q := Resolver{n} - s := ` - scalar Bytes32 - scalar Address - scalar Bytes - scalar BigInt - scalar Long - - schema { - query: Query - mutation: Mutation - } - - type Account { - address: Address! - balance: BigInt! - transactionCount: Long! - code: Bytes! - storage(slot: Bytes32!): Bytes32! - } - - type Log { - index: Int! - account(block: Long): Account! - topics: [Bytes32!]! - data: Bytes! - transaction: Transaction! - } - - type Transaction { - hash: Bytes32! - nonce: Long! - index: Int - from(block: Long): Account! - to(block: Long): Account - value: BigInt! - gasPrice: BigInt! - gas: Long! - inputData: Bytes! - block: Block - - status: Long - gasUsed: Long - cumulativeGasUsed: Long - createdContract(block: Long): Account - logs: [Log!] - } - - input BlockFilterCriteria { - addresses: [Address!] - topics: [[Bytes32!]!] - } - - type Block { - number: Long! - hash: Bytes32! - parent: Block - nonce: Bytes! - transactionsRoot: Bytes32! - transactionCount: Int - stateRoot: Bytes32! - receiptsRoot: Bytes32! - miner(block: Long): Account! - extraData: Bytes! - gasLimit: Long! - gasUsed: Long! - timestamp: BigInt! - logsBloom: Bytes! - mixHash: Bytes32! - difficulty: BigInt! - totalDifficulty: BigInt! - ommerCount: Int - ommers: [Block] - ommerAt(index: Int!): Block - ommerHash: Bytes32! - transactions: [Transaction!] - transactionAt(index: Int!): Transaction - logs(filter: BlockFilterCriteria!): [Log!]! - } - - input CallData { - from: Address - to: Address - gas: Long - gasPrice: BigInt - value: BigInt - data: Bytes - } - - type CallResult { - data: Bytes! - gasUsed: Long! - status: Long! - } - - input FilterCriteria { - fromBlock: Long - toBlock: Long - addresses: [Address!] - topics: [[Bytes32!]!] - } - - type SyncState{ - startingBlock: Long! - currentBlock: Long! - highestBlock: Long! - pulledStates: Long - knownStates: Long - } - - type Query { - account(address: Address!, blockNumber: Long): Account! - block(number: Long, hash: Bytes32): Block - blocks(from: Long!, to: Long): [Block!]! - transaction(hash: Bytes32!): Transaction - call(data: CallData!, blockNumber: Long): CallResult - estimateGas(data: CallData!, blockNumber: Long): Long! - logs(filter: FilterCriteria!): [Log!]! - gasPrice: BigInt! - protocolVersion: Int! - syncing: SyncState - } - - type Mutation { - sendRawTransaction(data: Bytes!): Bytes32! - } - ` - schema, err := graphql.ParseSchema(s, &q) + s, err := graphql.ParseSchema(schema, &q) if err != nil { return nil, err } - h := &relay.Handler{Schema: schema} + h := &relay.Handler{Schema: s} mux := http.NewServeMux() mux.Handle("/", GraphiQL{}) From a79bacd7e2fd3e062235eb4d378392a77a788873 Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Tue, 16 Oct 2018 20:42:01 +0100 Subject: [PATCH 27/40] Move some single use args types into anonymous structs --- ethgraphql/main.go | 38 ++++++++++---------------------------- 1 file changed, 10 insertions(+), 28 deletions(-) diff --git a/ethgraphql/main.go b/ethgraphql/main.go index 67223ce28deb..d51d99cc18e9 100644 --- a/ethgraphql/main.go +++ b/ethgraphql/main.go @@ -97,11 +97,7 @@ func (a *Account) Code(ctx context.Context) (hexutil.Bytes, error) { return hexutil.Bytes(state.GetCode(a.address)), nil } -type StorageSlotArgs struct { - Slot common.Hash -} - -func (a *Account) Storage(ctx context.Context, args StorageSlotArgs) (common.Hash, error) { +func (a *Account) Storage(ctx context.Context, args struct{ Slot common.Hash }) (common.Hash, error) { state, err := a.getState(ctx) if err != nil { return common.Hash{}, err @@ -652,11 +648,7 @@ func (b *Block) Transactions(ctx context.Context) (*[]*Transaction, error) { return &ret, nil } -type ArrayIndexArgs struct { - Index int32 -} - -func (b *Block) TransactionAt(ctx context.Context, args ArrayIndexArgs) (*Transaction, error) { +func (b *Block) TransactionAt(ctx context.Context, args struct{ Index int32 }) (*Transaction, error) { block, err := b.resolve(ctx) if err != nil || block == nil { return nil, err @@ -677,7 +669,7 @@ func (b *Block) TransactionAt(ctx context.Context, args ArrayIndexArgs) (*Transa }, nil } -func (b *Block) OmmerAt(ctx context.Context, args ArrayIndexArgs) (*Block, error) { +func (b *Block) OmmerAt(ctx context.Context, args struct{ Index int32 }) (*Block, error) { block, err := b.resolve(ctx) if err != nil || block == nil { return nil, err @@ -768,12 +760,10 @@ type Resolver struct { node *node.Node } -type BlockArgs struct { +func (r *Resolver) Block(ctx context.Context, args struct { Number *hexutil.Uint64 Hash *common.Hash -} - -func (r *Resolver) Block(ctx context.Context, args BlockArgs) (*Block, error) { +}) (*Block, error) { var block *Block if args.Number != nil { num := rpc.BlockNumber(uint64(*args.Number)) @@ -804,12 +794,10 @@ func (r *Resolver) Block(ctx context.Context, args BlockArgs) (*Block, error) { return block, nil } -type BlocksArgs struct { +func (r *Resolver) Blocks(ctx context.Context, args struct { From hexutil.Uint64 To *hexutil.Uint64 -} - -func (r *Resolver) Blocks(ctx context.Context, args BlocksArgs) ([]*Block, error) { +}) ([]*Block, error) { be, err := getBackend(r.node) if err != nil { return nil, err @@ -839,12 +827,10 @@ func (r *Resolver) Blocks(ctx context.Context, args BlocksArgs) ([]*Block, error return ret, nil } -type AccountArgs struct { +func (r *Resolver) Account(ctx context.Context, args struct { Address common.Address BlockNumber *hexutil.Uint64 -} - -func (r *Resolver) Account(ctx context.Context, args AccountArgs) *Account { +}) *Account { blockNumber := rpc.LatestBlockNumber if args.BlockNumber != nil { blockNumber = rpc.BlockNumber(*args.BlockNumber) @@ -857,11 +843,7 @@ func (r *Resolver) Account(ctx context.Context, args AccountArgs) *Account { } } -type TransactionArgs struct { - Hash common.Hash -} - -func (r *Resolver) Transaction(ctx context.Context, args TransactionArgs) (*Transaction, error) { +func (r *Resolver) Transaction(ctx context.Context, args struct{ Hash common.Hash }) (*Transaction, error) { tx := &Transaction{ node: r.node, hash: args.Hash, From c5f893d24c704868ef4a7b1c8dedaef48e8cb766 Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Wed, 17 Oct 2018 12:46:20 +0100 Subject: [PATCH 28/40] Add doc-comments --- ethgraphql/main.go | 71 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 55 insertions(+), 16 deletions(-) diff --git a/ethgraphql/main.go b/ethgraphql/main.go index d51d99cc18e9..7375d5455b1a 100644 --- a/ethgraphql/main.go +++ b/ethgraphql/main.go @@ -42,6 +42,8 @@ import ( "github.com/graph-gophers/graphql-go/relay" ) +// getBackend fetches the Ethereum instannce from the provided node, and returns +// the API backend from that. func getBackend(n *node.Node) (*eth.EthAPIBackend, error) { var ethereum *eth.Ethereum if err := n.Service(ðereum); err != nil { @@ -50,12 +52,14 @@ func getBackend(n *node.Node) (*eth.EthAPIBackend, error) { return ethereum.APIBackend, nil } +// Account represents an Ethereum account at a particular block. type Account struct { node *node.Node address common.Address blockNumber rpc.BlockNumber } +// getState fetches the StateDB object for an account. func (a *Account) getState(ctx context.Context) (*state.StateDB, error) { be, err := getBackend(a.node) if err != nil { @@ -106,6 +110,7 @@ func (a *Account) Storage(ctx context.Context, args struct{ Slot common.Hash }) return state.GetState(a.address, args.Slot), nil } +// Log represents an individual log message. All arguments are mandatory. type Log struct { node *node.Node transaction *Transaction @@ -136,6 +141,8 @@ func (l *Log) Data(ctx context.Context) hexutil.Bytes { return hexutil.Bytes(l.log.Data) } +// Transactionn represents an Ethereum transaction. +// node and hash are mandatory; all others will be fetched when required. type Transaction struct { node *node.Node hash common.Hash @@ -144,6 +151,7 @@ type Transaction struct { index uint64 } +// resolve returns the internal transaction object, fetching it if needed. func (t *Transaction) resolve(ctx context.Context) (*types.Transaction, error) { if t.tx == nil { be, err := getBackend(t.node) @@ -265,6 +273,7 @@ func (t *Transaction) Index(ctx context.Context) (*int32, error) { return &index, nil } +// getReceipt returns the receipt associated with this transaction, if any. func (t *Transaction) getReceipt(ctx context.Context) (*types.Receipt, error) { if _, err := t.resolve(ctx); err != nil { return nil, err @@ -342,6 +351,9 @@ func (t *Transaction) Logs(ctx context.Context) (*[]*Log, error) { return &ret, nil } +// Block represennts an Ethereum block. +// node, and either num or hash are mandatory. All other fields are lazily fetched +// when required. type Block struct { node *node.Node num *rpc.BlockNumber @@ -351,6 +363,8 @@ type Block struct { receipts []*types.Receipt } +// resolve returns the internal Block object represennting this block, fetching +// it if necessary. func (b *Block) resolve(ctx context.Context) (*types.Block, error) { if b.block != nil { return b.block, nil @@ -372,6 +386,9 @@ func (b *Block) resolve(ctx context.Context) (*types.Block, error) { return b.block, err } +// resolveHeader returns the internal Header object for this block, fetching it +// 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.header == nil { if _, err := b.resolve(ctx); err != nil { @@ -381,6 +398,8 @@ func (b *Block) resolveHeader(ctx context.Context) (*types.Header, error) { return b.header, nil } +// resolveReceipts returns the list of receipts for this block, fetching them +// if necessary. func (b *Block) resolveReceipts(ctx context.Context) ([]*types.Receipt, error) { if b.receipts == nil { be, err := getBackend(b.node) @@ -596,10 +615,13 @@ func (b *Block) TotalDifficulty(ctx context.Context) (hexutil.Big, error) { return hexutil.Big(*be.GetTd(h)), nil } +// BlockNumberArgs encapsulates arguments to accessors that specify a block number. type BlockNumberArgs struct { Block *hexutil.Uint64 } +// Number returns the provided block number, or rpc.LatestBlockNumber if none +// was provided. func (a BlockNumberArgs) Number() rpc.BlockNumber { if a.Block != nil { return rpc.BlockNumber(*a.Block) @@ -690,6 +712,8 @@ func (b *Block) OmmerAt(ctx context.Context, args struct{ Index int32 }) (*Block }, nil } +// BlockFilterCriteria encapsulates criteria passed to a `logs` accessor inside +// a block. type BlockFilterCriteria struct { Addresses *[]common.Address // restricts matches to events created by specific contracts @@ -707,6 +731,8 @@ type BlockFilterCriteria struct { Topics *[][]common.Hash } +// runFilter accepts a filter and executes it, returning all its results as +// `Log` objects. func runFilter(ctx context.Context, node *node.Node, filter *filters.Filter) ([]*Log, error) { logs, err := filter.Logs(ctx) if err != nil || logs == nil { @@ -756,6 +782,7 @@ func (b *Block) Logs(ctx context.Context, args struct{ Filter BlockFilterCriteri return runFilter(ctx, b.node, filter) } +// Resolver is the top-level object in the GraphQL heirarchy. type Resolver struct { node *node.Node } @@ -873,19 +900,22 @@ 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 - To *common.Address - Gas *hexutil.Uint64 - GasPrice *hexutil.Big - Value *hexutil.Big - Data *hexutil.Bytes + 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 - gasUsed hexutil.Uint64 - status hexutil.Uint64 + 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 { @@ -944,6 +974,7 @@ func (r *Resolver) EstimateGas(ctx context.Context, args struct { return hexutil.Uint64(gas), err } +// FilterCritera 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 @@ -1014,6 +1045,7 @@ func (r *Resolver) ProtocolVersion(ctx context.Context) (int32, error) { return int32(be.ProtocolVersion()), nil } +// SyncState represents the synchronisation status returned from the `syncing` accessor. type SyncState struct { progress ethereum.SyncProgress } @@ -1062,6 +1094,8 @@ func (r *Resolver) Syncing() (*SyncState, error) { return &SyncState{progress}, nil } +// NewHandler returns a new `http.Handler` that will answer GraphQL queries. +// It additionally exports an interactive query browser on the / endpoint. func NewHandler(n *node.Node) (http.Handler, error) { q := Resolver{n} @@ -1078,18 +1112,21 @@ func NewHandler(n *node.Node) (http.Handler, error) { return mux, nil } +// Service encapsulates an ETHGraphQL service. type Service struct { - endpoint string - cors []string - vhosts []string - timeouts rpc.HTTPTimeouts - node *node.Node - handler http.Handler - listener net.Listener + endpoint string // The host:port endpoint for this service. + cors []string // Allowed CORS domains + vhosts []string // Recognised vhosts + timeouts rpc.HTTPTimeouts // Timeout settings for HTTP requests. + node *node.Node // The node that queries will operate onn. + handler http.Handler // The `http.Handler` used to answer queries. + listener net.Listener // The listening socket. } +// Protocols returns the list of protocols exported by this service. func (s *Service) Protocols() []p2p.Protocol { return nil } +// APIs returns the list of APIs exported by this service. func (s *Service) APIs() []rpc.API { return nil } // Start is called after all services have been constructed and the networking @@ -1121,6 +1158,7 @@ func (s *Service) Stop() error { return nil } +// NewService constructs a new service instance. func NewService(ctx *node.ServiceContext, stack *node.Node, endpoint string, cors, vhosts []string, timeouts rpc.HTTPTimeouts) (*Service, error) { return &Service{ endpoint: endpoint, @@ -1131,6 +1169,7 @@ func NewService(ctx *node.ServiceContext, stack *node.Node, endpoint string, cor }, nil } +// RegisterGraphQLService is a utility function to construct a new service and register it against a node. func RegisterGraphQLService(stack *node.Node, endpoint string, cors, vhosts []string, timeouts rpc.HTTPTimeouts) error { return stack.Register(func(ctx *node.ServiceContext) (node.Service, error) { return NewService(ctx, stack, endpoint, cors, vhosts, timeouts) From 205f82e721156e47a260bd6ad08c77a510391edb Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Wed, 17 Oct 2018 13:30:40 +0100 Subject: [PATCH 29/40] Fixed backend fetching to use context --- ethgraphql/main.go | 258 +++++++++++++++---------------------------- ethgraphql/schema.go | 128 +++++++++++++++++++++ 2 files changed, 220 insertions(+), 166 deletions(-) create mode 100644 ethgraphql/schema.go diff --git a/ethgraphql/main.go b/ethgraphql/main.go index 7375d5455b1a..388b2a2c2d44 100644 --- a/ethgraphql/main.go +++ b/ethgraphql/main.go @@ -42,31 +42,16 @@ import ( "github.com/graph-gophers/graphql-go/relay" ) -// getBackend fetches the Ethereum instannce from the provided node, and returns -// the API backend from that. -func getBackend(n *node.Node) (*eth.EthAPIBackend, error) { - var ethereum *eth.Ethereum - if err := n.Service(ðereum); err != nil { - return nil, err - } - return ethereum.APIBackend, nil -} - // Account represents an Ethereum account at a particular block. type Account struct { - node *node.Node + backend *eth.EthAPIBackend address common.Address blockNumber rpc.BlockNumber } // getState fetches the StateDB object for an account. func (a *Account) getState(ctx context.Context) (*state.StateDB, error) { - be, err := getBackend(a.node) - if err != nil { - return nil, err - } - - state, _, err := be.StateAndHeaderByNumber(ctx, a.blockNumber) + state, _, err := a.backend.StateAndHeaderByNumber(ctx, a.blockNumber) return state, err } @@ -112,7 +97,7 @@ func (a *Account) Storage(ctx context.Context, args struct{ Slot common.Hash }) // Log represents an individual log message. All arguments are mandatory. type Log struct { - node *node.Node + backend *eth.EthAPIBackend transaction *Transaction log *types.Log } @@ -123,7 +108,7 @@ func (l *Log) Transaction(ctx context.Context) *Transaction { func (l *Log) Account(ctx context.Context, args BlockNumberArgs) *Account { return &Account{ - node: l.node, + backend: l.backend, address: l.log.Address, blockNumber: args.Number(), } @@ -142,33 +127,28 @@ func (l *Log) Data(ctx context.Context) hexutil.Bytes { } // Transactionn represents an Ethereum transaction. -// node and hash are mandatory; all others will be fetched when required. +// backend and hash are mandatory; all others will be fetched when required. type Transaction struct { - node *node.Node - hash common.Hash - tx *types.Transaction - block *Block - index uint64 + backend *eth.EthAPIBackend + hash common.Hash + tx *types.Transaction + block *Block + index uint64 } // resolve returns the internal transaction object, fetching it if needed. func (t *Transaction) resolve(ctx context.Context) (*types.Transaction, error) { if t.tx == nil { - be, err := getBackend(t.node) - if err != nil { - return nil, err - } - - tx, blockHash, _, index := rawdb.ReadTransaction(be.ChainDb(), t.hash) + tx, blockHash, _, index := rawdb.ReadTransaction(t.backend.ChainDb(), t.hash) if tx != nil { t.tx = tx t.block = &Block{ - node: t.node, - hash: blockHash, + backend: t.backend, + hash: blockHash, } t.index = index } else { - t.tx = be.GetPoolTransaction(t.hash) + t.tx = t.backend.GetPoolTransaction(t.hash) } } return t.tx, nil @@ -230,7 +210,7 @@ func (t *Transaction) To(ctx context.Context, args BlockNumberArgs) (*Account, e } return &Account{ - node: t.node, + backend: t.backend, address: *to, blockNumber: args.Number(), }, nil @@ -249,7 +229,7 @@ func (t *Transaction) From(ctx context.Context, args BlockNumberArgs) (*Account, from, _ := types.Sender(signer, tx) return &Account{ - node: t.node, + backend: t.backend, address: from, blockNumber: args.Number(), }, nil @@ -328,7 +308,7 @@ func (t *Transaction) CreatedContract(ctx context.Context, args BlockNumberArgs) } return &Account{ - node: t.node, + backend: t.backend, address: receipt.ContractAddress, blockNumber: args.Number(), }, nil @@ -343,7 +323,7 @@ func (t *Transaction) Logs(ctx context.Context) (*[]*Log, error) { ret := make([]*Log, 0, len(receipt.Logs)) for _, log := range receipt.Logs { ret = append(ret, &Log{ - node: t.node, + backend: t.backend, transaction: t, log: log, }) @@ -352,10 +332,10 @@ func (t *Transaction) Logs(ctx context.Context) (*[]*Log, error) { } // Block represennts an Ethereum block. -// node, 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. type Block struct { - node *node.Node + backend *eth.EthAPIBackend num *rpc.BlockNumber hash common.Hash header *types.Header @@ -370,15 +350,11 @@ func (b *Block) resolve(ctx context.Context) (*types.Block, error) { return b.block, nil } - be, err := getBackend(b.node) - if err != nil { - return nil, err - } - + var err error if b.hash != (common.Hash{}) { - b.block, err = be.GetBlock(ctx, b.hash) + b.block, err = b.backend.GetBlock(ctx, b.hash) } else { - b.block, err = be.BlockByNumber(ctx, *b.num) + b.block, err = b.backend.BlockByNumber(ctx, *b.num) } if b.block != nil { b.header = b.block.Header() @@ -402,11 +378,6 @@ func (b *Block) resolveHeader(ctx context.Context) (*types.Header, error) { // if necessary. func (b *Block) resolveReceipts(ctx context.Context) ([]*types.Receipt, error) { if b.receipts == nil { - be, err := getBackend(b.node) - if err != nil { - return nil, err - } - hash := b.hash if hash == (common.Hash{}) { header, err := b.resolveHeader(ctx) @@ -416,7 +387,7 @@ func (b *Block) resolveReceipts(ctx context.Context) ([]*types.Receipt, error) { hash = header.Hash() } - receipts, err := be.GetReceipts(ctx, hash) + receipts, err := b.backend.GetReceipts(ctx, hash) if err != nil { return nil, err } @@ -475,15 +446,15 @@ 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{ - node: b.node, - num: &num, - hash: b.header.ParentHash, + backend: b.backend, + num: &num, + hash: b.header.ParentHash, }, nil } else if b.num != nil && *b.num != 0 { num := *b.num - 1 return &Block{ - node: b.node, - num: &num, + backend: b.backend, + num: &num, }, nil } return nil, nil @@ -572,10 +543,10 @@ func (b *Block) Ommers(ctx context.Context) (*[]*Block, error) { for _, uncle := range block.Uncles() { blockNumber := rpc.BlockNumber(uncle.Number.Uint64()) ret = append(ret, &Block{ - node: b.node, - num: &blockNumber, - hash: uncle.Hash(), - header: uncle, + backend: b.backend, + num: &blockNumber, + hash: uncle.Hash(), + header: uncle, }) } return &ret, nil @@ -607,12 +578,7 @@ func (b *Block) TotalDifficulty(ctx context.Context) (hexutil.Big, error) { h = header.Hash() } - be, err := getBackend(b.node) - if err != nil { - return hexutil.Big{}, err - } - - return hexutil.Big(*be.GetTd(h)), nil + return hexutil.Big(*b.backend.GetTd(h)), nil } // BlockNumberArgs encapsulates arguments to accessors that specify a block number. @@ -636,7 +602,7 @@ func (b *Block) Miner(ctx context.Context, args BlockNumberArgs) (*Account, erro } return &Account{ - node: b.node, + backend: b.backend, address: block.Coinbase(), blockNumber: args.Number(), }, nil @@ -660,11 +626,11 @@ func (b *Block) Transactions(ctx context.Context) (*[]*Transaction, error) { ret := make([]*Transaction, 0, len(block.Transactions())) for i, tx := range block.Transactions() { ret = append(ret, &Transaction{ - node: b.node, - hash: tx.Hash(), - tx: tx, - block: b, - index: uint64(i), + backend: b.backend, + hash: tx.Hash(), + tx: tx, + block: b, + index: uint64(i), }) } return &ret, nil @@ -683,11 +649,11 @@ func (b *Block) TransactionAt(ctx context.Context, args struct{ Index int32 }) ( tx := txes[args.Index] return &Transaction{ - node: b.node, - hash: tx.Hash(), - tx: tx, - block: b, - index: uint64(args.Index), + backend: b.backend, + hash: tx.Hash(), + tx: tx, + block: b, + index: uint64(args.Index), }, nil } @@ -705,10 +671,10 @@ func (b *Block) OmmerAt(ctx context.Context, args struct{ Index int32 }) (*Block uncle := uncles[args.Index] blockNumber := rpc.BlockNumber(uncle.Number.Uint64()) return &Block{ - node: b.node, - num: &blockNumber, - hash: uncle.Hash(), - header: uncle, + backend: b.backend, + num: &blockNumber, + hash: uncle.Hash(), + header: uncle, }, nil } @@ -733,7 +699,7 @@ type BlockFilterCriteria struct { // runFilter accepts a filter and executes it, returning all its results as // `Log` objects. -func runFilter(ctx context.Context, node *node.Node, filter *filters.Filter) ([]*Log, error) { +func runFilter(ctx context.Context, be *eth.EthAPIBackend, filter *filters.Filter) ([]*Log, error) { logs, err := filter.Logs(ctx) if err != nil || logs == nil { return nil, err @@ -742,8 +708,8 @@ func runFilter(ctx context.Context, node *node.Node, filter *filters.Filter) ([] ret := make([]*Log, 0, len(logs)) for _, log := range logs { ret = append(ret, &Log{ - node: node, - transaction: &Transaction{node: node, hash: log.TxHash}, + backend: be, + transaction: &Transaction{backend: be, hash: log.TxHash}, log: log, }) } @@ -751,11 +717,6 @@ func runFilter(ctx context.Context, node *node.Node, filter *filters.Filter) ([] } func (b *Block) Logs(ctx context.Context, args struct{ Filter BlockFilterCriteria }) ([]*Log, error) { - be, err := getBackend(b.node) - if err != nil { - return nil, err - } - var addresses []common.Address if args.Filter.Addresses != nil { addresses = *args.Filter.Addresses @@ -776,15 +737,15 @@ func (b *Block) Logs(ctx context.Context, args struct{ Filter BlockFilterCriteri } // Construct the range filter - filter := filters.NewBlockFilter(be, hash, addresses, topics) + filter := filters.NewBlockFilter(b.backend, hash, addresses, topics) // Run the filter and return all the logs - return runFilter(ctx, b.node, filter) + return runFilter(ctx, b.backend, filter) } // Resolver is the top-level object in the GraphQL heirarchy. type Resolver struct { - node *node.Node + backend *eth.EthAPIBackend } func (r *Resolver) Block(ctx context.Context, args struct { @@ -795,19 +756,19 @@ func (r *Resolver) Block(ctx context.Context, args struct { if args.Number != nil { num := rpc.BlockNumber(uint64(*args.Number)) block = &Block{ - node: r.node, - num: &num, + backend: r.backend, + num: &num, } } else if args.Hash != nil { block = &Block{ - node: r.node, - hash: *args.Hash, + backend: r.backend, + hash: *args.Hash, } } else { num := rpc.LatestBlockNumber block = &Block{ - node: r.node, - num: &num, + backend: r.backend, + num: &num, } } @@ -825,18 +786,13 @@ func (r *Resolver) Blocks(ctx context.Context, args struct { From hexutil.Uint64 To *hexutil.Uint64 }) ([]*Block, error) { - be, err := getBackend(r.node) - if err != nil { - return nil, err - } - from := rpc.BlockNumber(args.From) var to rpc.BlockNumber if args.To != nil { to = rpc.BlockNumber(*args.To) } else { - to = rpc.BlockNumber(be.CurrentBlock().Number().Int64()) + to = rpc.BlockNumber(r.backend.CurrentBlock().Number().Int64()) } if to < from { @@ -847,8 +803,8 @@ func (r *Resolver) Blocks(ctx context.Context, args struct { for i := from; i <= to; i++ { num := i ret = append(ret, &Block{ - node: r.node, - num: &num, + backend: r.backend, + num: &num, }) } return ret, nil @@ -864,7 +820,7 @@ func (r *Resolver) Account(ctx context.Context, args struct { } return &Account{ - node: r.node, + backend: r.backend, address: args.Address, blockNumber: blockNumber, } @@ -872,8 +828,8 @@ func (r *Resolver) Account(ctx context.Context, args struct { func (r *Resolver) Transaction(ctx context.Context, args struct{ Hash common.Hash }) (*Transaction, error) { tx := &Transaction{ - node: r.node, - hash: args.Hash, + backend: r.backend, + hash: args.Hash, } // Resolve the transaction; if it doesn't exist, return nil. @@ -887,16 +843,11 @@ func (r *Resolver) Transaction(ctx context.Context, args struct{ Hash common.Has } func (r *Resolver) SendRawTransaction(ctx context.Context, args struct{ Data hexutil.Bytes }) (common.Hash, error) { - be, err := getBackend(r.node) - if err != nil { - return common.Hash{}, err - } - tx := new(types.Transaction) if err := rlp.DecodeBytes(args.Data, tx); err != nil { return common.Hash{}, err } - hash, err := ethapi.SubmitTransaction(ctx, be, tx) + hash, err := ethapi.SubmitTransaction(ctx, r.backend, tx) return hash, err } @@ -934,17 +885,12 @@ func (r *Resolver) Call(ctx context.Context, args struct { Data ethapi.CallArgs BlockNumber *hexutil.Uint64 }) (*CallResult, error) { - be, err := getBackend(r.node) - if err != nil { - return nil, err - } - blockNumber := rpc.LatestBlockNumber if args.BlockNumber != nil { blockNumber = rpc.BlockNumber(*args.BlockNumber) } - result, gas, failed, err := ethapi.DoCall(ctx, be, args.Data, blockNumber, vm.Config{}, 5*time.Second) + 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 @@ -960,17 +906,12 @@ func (r *Resolver) EstimateGas(ctx context.Context, args struct { Data ethapi.CallArgs BlockNumber *hexutil.Uint64 }) (hexutil.Uint64, error) { - be, err := getBackend(r.node) - if err != nil { - return 0, err - } - blockNumber := rpc.LatestBlockNumber if args.BlockNumber != nil { blockNumber = rpc.BlockNumber(*args.BlockNumber) } - gas, err := ethapi.DoEstimateGas(ctx, be, args.Data, blockNumber) + gas, err := ethapi.DoEstimateGas(ctx, r.backend, args.Data, blockNumber) return hexutil.Uint64(gas), err } @@ -995,11 +936,6 @@ type FilterCriteria struct { } func (r *Resolver) Logs(ctx context.Context, args struct{ Filter FilterCriteria }) ([]*Log, error) { - be, err := getBackend(r.node) - if err != nil { - return nil, err - } - // Convert the RPC block numbers into internal representations begin := rpc.LatestBlockNumber.Int64() if args.Filter.FromBlock != nil { @@ -1021,28 +957,18 @@ func (r *Resolver) Logs(ctx context.Context, args struct{ Filter FilterCriteria } // Construct the range filter - filter := filters.NewRangeFilter(filters.Backend(be), begin, end, addresses, topics) + filter := filters.NewRangeFilter(filters.Backend(r.backend), begin, end, addresses, topics) - return runFilter(ctx, r.node, filter) + return runFilter(ctx, r.backend, filter) } func (r *Resolver) GasPrice(ctx context.Context) (hexutil.Big, error) { - be, err := getBackend(r.node) - if err != nil { - return hexutil.Big{}, err - } - - price, err := be.SuggestPrice(ctx) + price, err := r.backend.SuggestPrice(ctx) return hexutil.Big(*price), err } func (r *Resolver) ProtocolVersion(ctx context.Context) (int32, error) { - be, err := getBackend(r.node) - if err != nil { - return 0, err - } - - return int32(be.ProtocolVersion()), nil + return int32(r.backend.ProtocolVersion()), nil } // SyncState represents the synchronisation status returned from the `syncing` accessor. @@ -1080,11 +1006,7 @@ func (s *SyncState) KnownStates() *hexutil.Uint64 { // - pulledStates: number of state entries processed until now // - knownStates: number of known state entries that still need to be pulled func (r *Resolver) Syncing() (*SyncState, error) { - be, err := getBackend(r.node) - if err != nil { - return nil, err - } - progress := be.Downloader().Progress() + progress := r.backend.Downloader().Progress() // Return not syncing if the synchronisation already completed if progress.CurrentBlock >= progress.HighestBlock { @@ -1096,8 +1018,8 @@ func (r *Resolver) Syncing() (*SyncState, error) { // NewHandler returns a new `http.Handler` that will answer GraphQL queries. // It additionally exports an interactive query browser on the / endpoint. -func NewHandler(n *node.Node) (http.Handler, error) { - q := Resolver{n} +func NewHandler(be *eth.EthAPIBackend) (http.Handler, error) { + q := Resolver{be} s, err := graphql.ParseSchema(schema, &q) if err != nil { @@ -1114,13 +1036,13 @@ func NewHandler(n *node.Node) (http.Handler, error) { // Service encapsulates an ETHGraphQL service. type Service struct { - endpoint string // The host:port endpoint for this service. - cors []string // Allowed CORS domains - vhosts []string // Recognised vhosts - timeouts rpc.HTTPTimeouts // Timeout settings for HTTP requests. - node *node.Node // The node that queries will operate onn. - handler http.Handler // The `http.Handler` used to answer queries. - listener net.Listener // The listening socket. + endpoint string // The host:port endpoint for this service. + cors []string // Allowed CORS domains + vhosts []string // Recognised vhosts + timeouts rpc.HTTPTimeouts // Timeout settings for HTTP requests. + backend *eth.EthAPIBackend // The backend that queries will operate onn. + handler http.Handler // The `http.Handler` used to answer queries. + listener net.Listener // The listening socket. } // Protocols returns the list of protocols exported by this service. @@ -1133,7 +1055,7 @@ func (s *Service) APIs() []rpc.API { return nil } // layer was also initialized to spawn any goroutines required by the service. func (s *Service) Start(server *p2p.Server) error { var err error - s.handler, err = NewHandler(s.node) + s.handler, err = NewHandler(s.backend) if err != nil { return err } @@ -1159,19 +1081,23 @@ func (s *Service) Stop() error { } // NewService constructs a new service instance. -func NewService(ctx *node.ServiceContext, stack *node.Node, endpoint string, cors, vhosts []string, timeouts rpc.HTTPTimeouts) (*Service, error) { +func NewService(backend *eth.EthAPIBackend, endpoint string, cors, vhosts []string, timeouts rpc.HTTPTimeouts) (*Service, error) { return &Service{ endpoint: endpoint, cors: cors, vhosts: vhosts, timeouts: timeouts, - node: stack, + backend: backend, }, nil } // RegisterGraphQLService is a utility function to construct a new service and register it against a node. func RegisterGraphQLService(stack *node.Node, endpoint string, cors, vhosts []string, timeouts rpc.HTTPTimeouts) error { return stack.Register(func(ctx *node.ServiceContext) (node.Service, error) { - return NewService(ctx, stack, endpoint, cors, vhosts, timeouts) + var ethereum *eth.Ethereum + if err := ctx.Service(ðereum); err != nil { + return nil, err + } + return NewService(ethereum.APIBackend, endpoint, cors, vhosts, timeouts) }) } diff --git a/ethgraphql/schema.go b/ethgraphql/schema.go new file mode 100644 index 000000000000..7ce994bb4d37 --- /dev/null +++ b/ethgraphql/schema.go @@ -0,0 +1,128 @@ +package ethgraphql + +const schema string = ` + scalar Bytes32 + scalar Address + scalar Bytes + scalar BigInt + scalar Long + + schema { + query: Query + mutation: Mutation + } + + type Account { + address: Address! + balance: BigInt! + transactionCount: Long! + code: Bytes! + storage(slot: Bytes32!): Bytes32! + } + + type Log { + index: Int! + account(block: Long): Account! + topics: [Bytes32!]! + data: Bytes! + transaction: Transaction! + } + + type Transaction { + hash: Bytes32! + nonce: Long! + index: Int + from(block: Long): Account! + to(block: Long): Account + value: BigInt! + gasPrice: BigInt! + gas: Long! + inputData: Bytes! + block: Block + + status: Long + gasUsed: Long + cumulativeGasUsed: Long + createdContract(block: Long): Account + logs: [Log!] + } + + input BlockFilterCriteria { + addresses: [Address!] + topics: [[Bytes32!]!] + } + + type Block { + number: Long! + hash: Bytes32! + parent: Block + nonce: Bytes! + transactionsRoot: Bytes32! + transactionCount: Int + stateRoot: Bytes32! + receiptsRoot: Bytes32! + miner(block: Long): Account! + extraData: Bytes! + gasLimit: Long! + gasUsed: Long! + timestamp: BigInt! + logsBloom: Bytes! + mixHash: Bytes32! + difficulty: BigInt! + totalDifficulty: BigInt! + ommerCount: Int + ommers: [Block] + ommerAt(index: Int!): Block + ommerHash: Bytes32! + transactions: [Transaction!] + transactionAt(index: Int!): Transaction + logs(filter: BlockFilterCriteria!): [Log!]! + } + + input CallData { + from: Address + to: Address + gas: Long + gasPrice: BigInt + value: BigInt + data: Bytes + } + + type CallResult { + data: Bytes! + gasUsed: Long! + status: Long! + } + + input FilterCriteria { + fromBlock: Long + toBlock: Long + addresses: [Address!] + topics: [[Bytes32!]!] + } + + type SyncState{ + startingBlock: Long! + currentBlock: Long! + highestBlock: Long! + pulledStates: Long + knownStates: Long + } + + type Query { + account(address: Address!, blockNumber: Long): Account! + block(number: Long, hash: Bytes32): Block + blocks(from: Long!, to: Long): [Block!]! + transaction(hash: Bytes32!): Transaction + call(data: CallData!, blockNumber: Long): CallResult + estimateGas(data: CallData!, blockNumber: Long): Long! + logs(filter: FilterCriteria!): [Log!]! + gasPrice: BigInt! + protocolVersion: Int! + syncing: SyncState + } + + type Mutation { + sendRawTransaction(data: Bytes!): Bytes32! + } +` From 844aa56f63cb33109c47eec23c821f05701a17fa Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Wed, 17 Oct 2018 13:57:49 +0100 Subject: [PATCH 30/40] Added (very) basic tests --- ethgraphql/main_test.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 ethgraphql/main_test.go diff --git a/ethgraphql/main_test.go b/ethgraphql/main_test.go new file mode 100644 index 000000000000..acdb143f7d77 --- /dev/null +++ b/ethgraphql/main_test.go @@ -0,0 +1,13 @@ +package ethgraphql + +import ( + "testing" +) + +func TestBuildSchema(t *testing.T) { + // Make sure the schema can be parsed and matched up to the object model. + _, err := NewHandler(nil) + if err != nil { + t.Errorf("Could not construct GraphQL handler: %v", err) + } +} From 5719808582f7aef225ec301462c1a52c791dd748 Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Wed, 17 Oct 2018 18:42:30 +0100 Subject: [PATCH 31/40] Add documentation to the graphql schema --- ethgraphql/schema.go | 161 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) diff --git a/ethgraphql/schema.go b/ethgraphql/schema.go index 7ce994bb4d37..9367cdc7cf7c 100644 --- a/ethgraphql/schema.go +++ b/ethgraphql/schema.go @@ -1,10 +1,17 @@ package ethgraphql const schema string = ` + # Bytes32 is a 32 byte binary string, represented as 0x-prefixed hexadecimal. scalar Bytes32 + # 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. 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 + # 0x-prefixed hexadecimal. scalar BigInt + # Long is a 64 bit unsigned integer. scalar Long schema { @@ -12,117 +19,271 @@ const schema string = ` mutation: Mutation } + # Account is an Ethereum account at a particular block. type Account { + # Address is the address owning the account. address: Address! + # Balance is the balance of the account, in wei. balance: BigInt! + # TransactionCount is the number of transactions sent from this account, + # or in the case of a contract, the number of contracts created. Otherwise + # known as the nonce. transactionCount: Long! + # Code contains the smart contract code for this account, if the account + # is a (non-self-destructed) contract. code: Bytes! + # Storage provides access to the storage of a contract account, indexed + # by its 32 byte slot identifier. storage(slot: Bytes32!): Bytes32! } + # Log is an Ethereum event log. type Log { + # Index is the index of this log in the block. index: Int! + # Account is the account which generated this log - this will always + # be a contract account. account(block: Long): Account! + # Topics is a list of 0-4 indexed topics for the log. topics: [Bytes32!]! + # Data is unindexed data for this log. data: Bytes! + # Transaction is the transaction that generated this log entry. transaction: Transaction! } + # Transaction is an Ethereum transaction. type Transaction { + # Hash is the hash of this transaction. hash: Bytes32! + # 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. index: Int + # From is the account that sent this transaction - this will always be + # an externally owned account. from(block: Long): Account! + # To is the account the transaction was sent to. This is null for + # contract-creating transactions. to(block: Long): Account + # Value is the value, in wei, sent along with this transaction. value: BigInt! + # GasPrice is the price offered to miners for gas, in wei per unit. gasPrice: BigInt! + # Gas is the maximum amount of gas this transaction can consume. gas: Long! + # InputData is the data supplied to the target of the transaction. inputData: Bytes! + # Block is the block this transaction was mined in. This will be null if + # the transaction has not yet been mined. block: Block + # Status is the return status of the transaction. This will be 1 if the + # transaction succeeded, or 0 if it failed (due to a revert, or due to + # running out of gas). If the transaction has not yet been mined, this + # field will be null. status: Long + # GasUsed is the amount of gas that was used processing this transaction. + # If the transaction has not yet been mined, this field will be null. gasUsed: Long + # CumulativeGasUsed is the total gas used in the block up to and including + # this transaction. If the transaction has not yet been mined, this field + # will be null. cumulativeGasUsed: Long + # CreatedContract is the account that was created by a contract creation + # transaction. If the transaction was not a contract creation transaction, + # or it has not yet been mined, this field will be null. createdContract(block: Long): Account + # Logs is a list of log entries emitted by this transaction. If the + # transaction has not yet been mined, this field will be null. logs: [Log!] } + # BlockFilterCriteria encapsulates log filter criteria for a filter applied + # to a single block. input BlockFilterCriteria { + # Addresses is list of addresses that are of interest. If this list is + # 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 topics: [[Bytes32!]!] } + # Block is an Ethereum block. type Block { + # Number is the number of this block, starting at 0 for the genesis block. number: Long! + # Hash is the block hash of this block. hash: Bytes32! + # Parent is the parent block of this block. parent: Block + # Nonce is the block nonce, an 8 byte sequence determined by the miner. nonce: Bytes! + # TransactionsRoot is the keccak256 hash of the root of the trie of transactions in this block. 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 + # 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. receiptsRoot: Bytes32! + # Miner is the account that mined this block. miner(block: Long): Account! + # ExtraData is an arbitrary data field supplied by the miner. extraData: Bytes! + # GasLimit is the maximum amount of gas that was available to transactions in this block. gasLimit: Long! + # GasUsed is the amount of gas that was used executing transactions in this block. gasUsed: Long! + # Timestamp is the unix timestamp at which this block was mined. timestamp: BigInt! + # LogsBloom is a bloom filter that can be used to check if a block may + # contain log entries matching a filter. logsBloom: Bytes! + # MixHash is the hash that was used as an input to the PoW process. mixHash: Bytes32! + # Difficulty is a measure of the difficulty of mining this block. difficulty: BigInt! + # TotalDifficulty is the sum of all difficulty values up to and including + # this block. 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 + # 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, + # ommerCount and ommerAt fields may not be available on any ommer blocks. 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 + # OmmerHash is the keccak256 hash of all the ommers (AKA uncles) + # associated with this block. ommerHash: Bytes32! + # Transactions is a list of transactions associated with this block. If + # transactions are unavailable for this block, this field will be null. transactions: [Transaction!] + # 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 + # Logs returns a filtered set of logs from this block. logs(filter: BlockFilterCriteria!): [Log!]! } + # CallData represents the data associated with a local contract call. + # All fields are optional. input CallData { + # From is the address making the call. from: Address + # To is the address the call is sent to. to: Address + # Gas is the amount of gas sent with the call. gas: Long + # GasPrice is the price, in wei, offered for each unit of gas. gasPrice: BigInt + # Value is the value, in wei, sent along with the call. value: BigInt + # Data is the data sent to the callee. data: Bytes } + # CallResult is the result of a local call operationn. type CallResult { + # Data is the return data of the called contract. data: Bytes! + # GasUsed is the amount of gas used by the call, after any refunds. gasUsed: Long! + # Status is the result of the call - 1 for success or 0 for failure. status: Long! } + # FilterCriteria encapsulates log filter criteria for searching log entries. input FilterCriteria { + # FromBlock is the block at which to start searching, inclusive. Defaults + # to the latest block if not supplied. fromBlock: Long + # ToBlock is the block at which to stop searching, inclusive. Defaults + # to the latest block if not supplied. toBlock: Long + # Addresses is a list of addresses that are of interest. If this list is + # 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 topics: [[Bytes32!]!] } + # SyncState contains the current synchronisation state of the client. type SyncState{ + # StartingBlock is the block number at which synchronisation started. startingBlock: Long! + # CurrentBlock is the point at which synchronisation has presently reached. currentBlock: Long! + # HighestBlock is the latest known block number. highestBlock: Long! + # PulledStates is the number of state entries fetched so far, or null + # if this is not known or not relevant. pulledStates: Long + # KnownStates is the number of states the node knows of so far, or null + # if this is not known or not relevant. knownStates: 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!]! + # 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 + # ensure a transaction is mined in a timely fashion. gasPrice: BigInt! + # ProtocolVersion returns the current wire protocol version number. protocolVersion: Int! + # Syncing returns information on the current synchronisation state. syncing: SyncState } type Mutation { + # SendRawTransaction sends an RLP-encoded transaction to the network. sendRawTransaction(data: Bytes!): Bytes32! } ` From 532ec8e2c13263126e7ed98f9ae0b97d04143084 Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Thu, 18 Oct 2018 13:02:48 +0100 Subject: [PATCH 32/40] Fix reversion for formatting of big numbers --- common/hexutil/json.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/common/hexutil/json.go b/common/hexutil/json.go index cbe0943a2dd6..dd6a50b81b44 100644 --- a/common/hexutil/json.go +++ b/common/hexutil/json.go @@ -221,10 +221,6 @@ func (b *Big) UnmarshalGraphQL(input interface{}) error { return err } -func (b Big) MarshalJSON() ([]byte, error) { - return strconv.AppendQuote(nil, (*big.Int)(&b).Text(10)), nil -} - // Uint64 marshals/unmarshals as a JSON string with 0x prefix. // The zero value marshals as "0x0". type Uint64 uint64 From c10477905794d2b6d105c5403a4f61c748ec3d9c Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Thu, 18 Oct 2018 14:19:38 +0100 Subject: [PATCH 33/40] Correct spelling error --- ethgraphql/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ethgraphql/main.go b/ethgraphql/main.go index 388b2a2c2d44..3c81c80e2356 100644 --- a/ethgraphql/main.go +++ b/ethgraphql/main.go @@ -743,7 +743,7 @@ func (b *Block) Logs(ctx context.Context, args struct{ Filter BlockFilterCriteri return runFilter(ctx, b.backend, filter) } -// Resolver is the top-level object in the GraphQL heirarchy. +// Resolver is the top-level object in the GraphQL hierarchy. type Resolver struct { backend *eth.EthAPIBackend } From 1c5ed0d5f3c4e3844a6c55676daebaf2d0c4dbf6 Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Fri, 19 Oct 2018 09:14:46 +0100 Subject: [PATCH 34/40] s/BigInt/Long/ --- common/hexutil/json.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/hexutil/json.go b/common/hexutil/json.go index dd6a50b81b44..30bfb61c9b7f 100644 --- a/common/hexutil/json.go +++ b/common/hexutil/json.go @@ -278,7 +278,7 @@ func (b *Uint64) UnmarshalGraphQL(input interface{}) error { case int32: *b = Uint64(input) default: - err = fmt.Errorf("Unexpected type for BigInt: %v", input) + err = fmt.Errorf("Unexpected type for Long: %v", input) } return err } From 84874ac2d8b6f708cdbc1b1b00b4ea74b675593f Mon Sep 17 00:00:00 2001 From: Martin Holst Swende Date: Fri, 19 Oct 2018 09:14:59 +0100 Subject: [PATCH 35/40] Update common/types.go Co-Authored-By: Arachnid --- common/types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/types.go b/common/types.go index 251dd8459309..cf61ec66c5c0 100644 --- a/common/types.go +++ b/common/types.go @@ -289,7 +289,7 @@ func (a *Address) UnmarshalGraphQL(input interface{}) error { case string: *a = HexToAddress(input) default: - err = fmt.Errorf("Unexpected type for Hash: %v", input) + err = fmt.Errorf("Unexpected type for Address: %v", input) } return err } From 202a1003eef330cd3b39aae17df128ee6b2d1af1 Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Thu, 22 Nov 2018 14:20:55 +1300 Subject: [PATCH 36/40] Fixes in response to review --- cmd/geth/config.go | 4 ++-- cmd/geth/main.go | 2 ++ cmd/utils/flags.go | 21 ++++++++++++++--- common/hexutil/json.go | 6 +++++ common/types.go | 4 ++++ ethgraphql/main_test.go | 13 ----------- ethgraphql/main.go => graphql/grahpql.go | 9 ++++---- {ethgraphql => graphql}/graphiql.go | 24 +++++++++++++++++++- graphql/graphql_test.go | 29 ++++++++++++++++++++++++ {ethgraphql => graphql}/schema.go | 18 ++++++++++++++- node/config.go | 14 ++++++++++++ 11 files changed, 120 insertions(+), 24 deletions(-) delete mode 100644 ethgraphql/main_test.go rename ethgraphql/main.go => graphql/grahpql.go (99%) rename {ethgraphql => graphql}/graphiql.go (63%) create mode 100644 graphql/graphql_test.go rename {ethgraphql => graphql}/schema.go (94%) diff --git a/cmd/geth/config.go b/cmd/geth/config.go index aff1530ff4ef..cb6cf54766a5 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -30,7 +30,7 @@ import ( "github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/dashboard" "github.com/ethereum/go-ethereum/eth" - "github.com/ethereum/go-ethereum/ethgraphql" + "github.com/ethereum/go-ethereum/graphql" "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/params" whisper "github.com/ethereum/go-ethereum/whisper/whisperv6" @@ -177,7 +177,7 @@ func makeFullNode(ctx *cli.Context) *node.Node { // Configure GraphQL if required if ctx.GlobalIsSet(utils.GraphQLEnabledFlag.Name) { - if err := ethgraphql.RegisterGraphQLService(stack, cfg.Node.GraphQLEndpoint(), cfg.Node.HTTPCors, cfg.Node.HTTPVirtualHosts, cfg.Node.HTTPTimeouts); err != nil { + if err := graphql.RegisterGraphQLService(stack, cfg.Node.GraphQLEndpoint(), cfg.Node.GraphQLCors, cfg.Node.GraphQLVirtualHosts, cfg.Node.HTTPTimeouts); err != nil { utils.Fatalf("Failed to register the Ethereum service: %v", err) } } diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 859d4d959aa2..2cc881dbc4de 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -142,6 +142,8 @@ var ( utils.GraphQLEnabledFlag, utils.GraphQLListenAddrFlag, utils.GraphQLPortFlag, + utils.GraphQLCORSDomainFlag, + utils.GraphQLVirtualHostsFlag, utils.RPCApiFlag, utils.WSEnabledFlag, utils.WSListenAddrFlag, diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index e6834442ca5c..16cae43e3d02 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -432,15 +432,25 @@ var ( Usage: "Enable the GraphQL server", } GraphQLListenAddrFlag = cli.StringFlag{ - Name: "graphqladdr", + Name: "graphql.addr", Usage: "GraphQL server listening interface", Value: node.DefaultGraphQLHost, } GraphQLPortFlag = cli.IntFlag{ - Name: "graphqlport", + Name: "graphql.port", Usage: "GraphQL server listening port", Value: node.DefaultGraphQLPort, } + GraphQLCORSDomainFlag = cli.StringFlag{ + Name: "graphql.rpccorsdomain", + Usage: "Comma separated list of domains from which to accept cross origin requests (browser enforced)", + Value: "", + } + GraphQLVirtualHostsFlag = cli.StringFlag{ + Name: "graphql.rpcvhosts", + Usage: "Comma separated list of virtual hostnames from which to accept requests (server enforced). Accepts '*' wildcard.", + Value: strings.Join(node.DefaultConfig.HTTPVirtualHosts, ","), + } RPCCORSDomainFlag = cli.StringFlag{ Name: "rpccorsdomain", Usage: "Comma separated list of domains from which to accept cross origin requests (browser enforced)", @@ -811,8 +821,13 @@ func setGraphQL(ctx *cli.Context, cfg *node.Config) { cfg.GraphQLHost = ctx.GlobalString(GraphQLListenAddrFlag.Name) } } - cfg.GraphQLPort = ctx.GlobalInt(GraphQLPortFlag.Name) + if ctx.GlobalIsSet(GraphQLCORSDomainFlag.Name) { + cfg.GraphQLCors = splitAndTrim(ctx.GlobalString(GraphQLCORSDomainFlag.Name)) + } + if ctx.GlobalIsSet(GraphQLVirtualHostsFlag.Name) { + cfg.GraphQLVirtualHosts = splitAndTrim(ctx.GlobalString(GraphQLVirtualHostsFlag.Name)) + } } // setWS creates the WebSocket RPC listener interface string from the set diff --git a/common/hexutil/json.go b/common/hexutil/json.go index 30bfb61c9b7f..777b08eca42a 100644 --- a/common/hexutil/json.go +++ b/common/hexutil/json.go @@ -72,8 +72,10 @@ func (b Bytes) String() string { return Encode(b) } +// ImplementsGraphQLType returns true if Bytes implements the specified GraphQL type. func (b Bytes) ImplementsGraphQLType(name string) bool { return name == "Bytes" } +// UnmarshalGraphQL unmarshals the provided GraphQL query data. func (b *Bytes) UnmarshalGraphQL(input interface{}) error { var err error switch input := input.(type) { @@ -204,8 +206,10 @@ func (b *Big) String() string { return EncodeBig(b.ToInt()) } +// ImplementsGraphQLType returns true if Big implements the provided GraphQL type. func (b Big) ImplementsGraphQLType(name string) bool { return name == "BigInt" } +// UnmarshalGraphQL unmarshals the provided GraphQL query data. func (b *Big) UnmarshalGraphQL(input interface{}) error { var err error switch input := input.(type) { @@ -268,8 +272,10 @@ func (b Uint64) String() string { return EncodeUint64(uint64(b)) } +// ImplementsGraphQLType returns true if Uint64 implements the provided GraphQL type. func (b Uint64) ImplementsGraphQLType(name string) bool { return name == "Long" } +// UnmarshalGraphQL unmarshals the provided GraphQL query data. func (b *Uint64) UnmarshalGraphQL(input interface{}) error { var err error switch input := input.(type) { diff --git a/common/types.go b/common/types.go index cf61ec66c5c0..cb74cbd6ff69 100644 --- a/common/types.go +++ b/common/types.go @@ -141,8 +141,10 @@ func (h Hash) Value() (driver.Value, error) { return h[:], nil } +// ImplementsGraphQLType returns true if Hash implements the specified GraphQL type. func (_ Hash) ImplementsGraphQLType(name string) bool { return name == "Bytes32" } +// UnmarshalGraphQL unmarshals the provided GraphQL query data. func (h *Hash) UnmarshalGraphQL(input interface{}) error { var err error switch input := input.(type) { @@ -281,8 +283,10 @@ func (a Address) Value() (driver.Value, error) { return a[:], nil } +// ImplementsGraphQLType returns true if Hash implements the specified GraphQL type. func (a Address) ImplementsGraphQLType(name string) bool { return name == "Address" } +// UnmarshalGraphQL unmarshals the provided GraphQL query data. func (a *Address) UnmarshalGraphQL(input interface{}) error { var err error switch input := input.(type) { diff --git a/ethgraphql/main_test.go b/ethgraphql/main_test.go deleted file mode 100644 index acdb143f7d77..000000000000 --- a/ethgraphql/main_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package ethgraphql - -import ( - "testing" -) - -func TestBuildSchema(t *testing.T) { - // Make sure the schema can be parsed and matched up to the object model. - _, err := NewHandler(nil) - if err != nil { - t.Errorf("Could not construct GraphQL handler: %v", err) - } -} diff --git a/ethgraphql/main.go b/graphql/grahpql.go similarity index 99% rename from ethgraphql/main.go rename to graphql/grahpql.go index 3c81c80e2356..b16271e6d5c2 100644 --- a/ethgraphql/main.go +++ b/graphql/grahpql.go @@ -14,7 +14,8 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -package ethgraphql +// Package graphql provides a GraphQL interface to Ethereum node data. +package graphql import ( "context" @@ -38,7 +39,7 @@ import ( "github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rpc" - graphql "github.com/graph-gophers/graphql-go" + graphqlgo "github.com/graph-gophers/graphql-go" "github.com/graph-gophers/graphql-go/relay" ) @@ -1021,7 +1022,7 @@ func (r *Resolver) Syncing() (*SyncState, error) { func NewHandler(be *eth.EthAPIBackend) (http.Handler, error) { q := Resolver{be} - s, err := graphql.ParseSchema(schema, &q) + s, err := graphqlgo.ParseSchema(schema, &q) if err != nil { return nil, err } @@ -1034,7 +1035,7 @@ func NewHandler(be *eth.EthAPIBackend) (http.Handler, error) { return mux, nil } -// Service encapsulates an ETHGraphQL service. +// Service encapsulates a GraphQL service. type Service struct { endpoint string // The host:port endpoint for this service. cors []string // Allowed CORS domains diff --git a/ethgraphql/graphiql.go b/graphql/graphiql.go similarity index 63% rename from ethgraphql/graphiql.go rename to graphql/graphiql.go index 42bb7920fd27..6d9dda3e8c16 100644 --- a/ethgraphql/graphiql.go +++ b/graphql/graphiql.go @@ -1,4 +1,26 @@ -package ethgraphql +// The MIT License (MIT) +// +// Copyright (c) 2016 Muhammed Thanish +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package graphql import ( "bytes" diff --git a/graphql/graphql_test.go b/graphql/graphql_test.go new file mode 100644 index 000000000000..d63418398a5d --- /dev/null +++ b/graphql/graphql_test.go @@ -0,0 +1,29 @@ +// Copyright 2018 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library 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. +// +// The go-ethereum library 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 the go-ethereum library. If not, see . + +package graphql + +import ( + "testing" +) + +func TestBuildSchema(t *testing.T) { + // Make sure the schema can be parsed and matched up to the object model. + _, err := NewHandler(nil) + if err != nil { + t.Errorf("Could not construct GraphQL handler: %v", err) + } +} diff --git a/ethgraphql/schema.go b/graphql/schema.go similarity index 94% rename from ethgraphql/schema.go rename to graphql/schema.go index 9367cdc7cf7c..c1ba87d2d6cb 100644 --- a/ethgraphql/schema.go +++ b/graphql/schema.go @@ -1,4 +1,20 @@ -package ethgraphql +// Copyright 2018 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library 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. +// +// The go-ethereum library 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 the go-ethereum library. If not, see . + +package graphql const schema string = ` # Bytes32 is a 32 byte binary string, represented as 0x-prefixed hexadecimal. diff --git a/node/config.go b/node/config.go index a1a896df89e0..c259223cbb49 100644 --- a/node/config.go +++ b/node/config.go @@ -110,6 +110,20 @@ type Config struct { // for ephemeral nodes). GraphQLPort int `toml:",omitempty"` + // GraphQLCors is the Cross-Origin Resource Sharing header to send to requesting + // clients. Please be aware that CORS is a browser enforced security, it's fully + // useless for custom HTTP clients. + GraphQLCors []string `toml:",omitempty"` + + // GraphQLVirtualHosts is the list of virtual hostnames which are allowed on incoming requests. + // This is by default {'localhost'}. Using this prevents attacks like + // DNS rebinding, which bypasses SOP by simply masquerading as being within the same + // origin. These attacks do not utilize CORS, since they are not cross-domain. + // By explicitly checking the Host-header, the server will not allow requests + // made against the server with a malicious host domain. + // Requests using ip address directly are not affected + GraphQLVirtualHosts []string `toml:",omitempty"` + // HTTPCors is the Cross-Origin Resource Sharing header to send to requesting // clients. Please be aware that CORS is a browser enforced security, it's fully // useless for custom HTTP clients. From c780558e08a924a9cbfa6a27aeb858a60052fd94 Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Fri, 23 Nov 2018 11:33:24 +1300 Subject: [PATCH 37/40] Fix lint error --- graphql/grahpql.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphql/grahpql.go b/graphql/grahpql.go index b16271e6d5c2..fce78a8d1b03 100644 --- a/graphql/grahpql.go +++ b/graphql/grahpql.go @@ -913,7 +913,7 @@ func (r *Resolver) EstimateGas(ctx context.Context, args struct { } gas, err := ethapi.DoEstimateGas(ctx, r.backend, args.Data, blockNumber) - return hexutil.Uint64(gas), err + return gas, err } // FilterCritera encapsulates the arguments to `logs` on the root resolver object. From 81efd4b8814f9559753bc7e6f086acab5823df91 Mon Sep 17 00:00:00 2001 From: Kris Shinn Date: Sat, 29 Dec 2018 21:48:05 -0800 Subject: [PATCH 38/40] Updated calls on private functions --- internal/ethapi/api.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 7f8476d578ea..ffe1ebe04074 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -686,14 +686,14 @@ type CallArgs struct { func (s *PublicBlockChainAPI) doCall(ctx context.Context, args CallArgs, blockNr rpc.BlockNumber, timeout time.Duration) ([]byte, uint64, bool, error) { defer func(start time.Time) { log.Debug("Executing EVM call finished", "runtime", time.Since(start)) }(time.Now()) - state, header, err := b.StateAndHeaderByNumber(ctx, blockNr) + state, header, err := s.b.StateAndHeaderByNumber(ctx, blockNr) if state == nil || err != nil { return nil, 0, false, err } // Set sender address or use a default if none specified var addr common.Address if args.From == nil { - if wallets := b.AccountManager().Wallets(); len(wallets) > 0 { + if wallets := s.b.AccountManager().Wallets(); len(wallets) > 0 { if accounts := wallets[0].Accounts(); len(accounts) > 0 { addr = accounts[0].Address } @@ -765,9 +765,7 @@ func (s *PublicBlockChainAPI) Call(ctx context.Context, args CallArgs, blockNr r return (hexutil.Bytes)(result), err } -// DoEstimateGas returns an estimate of the amount of gas needed to execute the -// given transaction against the current pending block. -func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNr rpc.BlockNumber) (hexutil.Uint64, error) { +func (s *PublicBlockChainAPI) doEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNr rpc.BlockNumber) (hexutil.Uint64, error) { // Binary search the gas requirement, as it may be higher than the amount used var ( lo uint64 = params.TxGas - 1 @@ -814,8 +812,10 @@ func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNr rpc.Bl return hexutil.Uint64(hi), nil } +// EstimateGas returns an estimate of the amount of gas needed to execute the +// given transaction against the current pending block. func (s *PublicBlockChainAPI) EstimateGas(ctx context.Context, args CallArgs) (hexutil.Uint64, error) { - return DoEstimateGas(ctx, s.b, args, rpc.PendingBlockNumber) + return s.doEstimateGas(ctx, s.b, args, rpc.PendingBlockNumber) } // ExecutionResult groups all structured logs emitted by the EVM @@ -842,7 +842,7 @@ type StructLogRes struct { Storage *map[string]string `json:"storage,omitempty"` } -// formatLogs formats EVM returned structured logs for json output +// FormatLogs formats EVM returned structured logs for json output func FormatLogs(logs []vm.StructLog) []StructLogRes { formatted := make([]StructLogRes, len(logs)) for index, trace := range logs { From 27e746580c7c1394d7de21ec51b574e371f4e07d Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Wed, 16 Jan 2019 08:54:26 +1300 Subject: [PATCH 39/40] Fix typo in graphql.go --- graphql/grahpql.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphql/grahpql.go b/graphql/grahpql.go index fce78a8d1b03..1eca7895679d 100644 --- a/graphql/grahpql.go +++ b/graphql/grahpql.go @@ -344,7 +344,7 @@ type Block struct { receipts []*types.Receipt } -// resolve returns the internal Block object represennting this block, fetching +// resolve returns the internal Block object representing this block, fetching // it if necessary. func (b *Block) resolve(ctx context.Context) (*types.Block, error) { if b.block != nil { From 0c70616f4f1e58f1c8d88b53a07b0258550a8f0b Mon Sep 17 00:00:00 2001 From: Kris Shinn Date: Sun, 20 Jan 2019 21:31:28 -0800 Subject: [PATCH 40/40] Rollback ethapi breaking changes for graphql support --- internal/ethapi/api.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index ffe1ebe04074..5f90aef086a8 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -683,17 +683,17 @@ type CallArgs struct { Data *hexutil.Bytes `json:"data"` } -func (s *PublicBlockChainAPI) doCall(ctx context.Context, args CallArgs, blockNr rpc.BlockNumber, timeout time.Duration) ([]byte, uint64, bool, error) { +func DoCall(ctx context.Context, b Backend, args CallArgs, blockNr rpc.BlockNumber, vmCfg vm.Config, timeout time.Duration) ([]byte, uint64, bool, error) { defer func(start time.Time) { log.Debug("Executing EVM call finished", "runtime", time.Since(start)) }(time.Now()) - state, header, err := s.b.StateAndHeaderByNumber(ctx, blockNr) + state, header, err := b.StateAndHeaderByNumber(ctx, blockNr) if state == nil || err != nil { return nil, 0, false, err } // Set sender address or use a default if none specified var addr common.Address if args.From == nil { - if wallets := s.b.AccountManager().Wallets(); len(wallets) > 0 { + if wallets := b.AccountManager().Wallets(); len(wallets) > 0 { if accounts := wallets[0].Accounts(); len(accounts) > 0 { addr = accounts[0].Address } @@ -737,7 +737,7 @@ func (s *PublicBlockChainAPI) doCall(ctx context.Context, args CallArgs, blockNr defer cancel() // Get a new instance of the EVM. - evm, vmError, err := s.b.GetEVM(ctx, msg, state, header) + evm, vmError, err := b.GetEVM(ctx, msg, state, header) if err != nil { return nil, 0, false, err } @@ -761,11 +761,11 @@ func (s *PublicBlockChainAPI) doCall(ctx context.Context, args CallArgs, blockNr // Call executes the given transaction on the state for the given block number. // It doesn't make and changes in the state/blockchain and is useful to execute and retrieve values. func (s *PublicBlockChainAPI) Call(ctx context.Context, args CallArgs, blockNr rpc.BlockNumber) (hexutil.Bytes, error) { - result, _, _, err := s.doCall(ctx, args, blockNr, 5*time.Second) + result, _, _, err := DoCall(ctx, s.b, args, blockNr, vm.Config{}, 5*time.Second) return (hexutil.Bytes)(result), err } -func (s *PublicBlockChainAPI) doEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNr rpc.BlockNumber) (hexutil.Uint64, error) { +func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNr rpc.BlockNumber) (hexutil.Uint64, error) { // Binary search the gas requirement, as it may be higher than the amount used var ( lo uint64 = params.TxGas - 1 @@ -788,7 +788,7 @@ func (s *PublicBlockChainAPI) doEstimateGas(ctx context.Context, b Backend, args executable := func(gas uint64) bool { args.Gas = (*hexutil.Uint64)(&gas) - _, _, failed, err := s.doCall(ctx, args, rpc.PendingBlockNumber, 0) + _, _, failed, err := DoCall(ctx, b, args, rpc.PendingBlockNumber, vm.Config{}, 0) if err != nil || failed { return false } @@ -815,7 +815,7 @@ func (s *PublicBlockChainAPI) doEstimateGas(ctx context.Context, b Backend, args // EstimateGas returns an estimate of the amount of gas needed to execute the // given transaction against the current pending block. func (s *PublicBlockChainAPI) EstimateGas(ctx context.Context, args CallArgs) (hexutil.Uint64, error) { - return s.doEstimateGas(ctx, s.b, args, rpc.PendingBlockNumber) + return DoEstimateGas(ctx, s.b, args, rpc.PendingBlockNumber) } // ExecutionResult groups all structured logs emitted by the EVM