diff --git a/cmd/geth/config.go b/cmd/geth/config.go index 8f0bae8225..3b63d6913f 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -30,7 +30,6 @@ 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/graphql" "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/params" whisper "github.com/ethereum/go-ethereum/whisper/whisperv6" @@ -177,14 +176,10 @@ func makeFullNode(ctx *cli.Context) *node.Node { } utils.RegisterShhService(stack, &cfg.Shh) } - - // Configure GraphQL if required + // Configure GraphQL if requested if ctx.GlobalIsSet(utils.GraphQLEnabledFlag.Name) { - 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) - } + utils.RegisterGraphQLService(stack, cfg.Node.GraphQLEndpoint(), cfg.Node.GraphQLCors, cfg.Node.GraphQLVirtualHosts, cfg.Node.HTTPTimeouts) } - // Add the Ethereum Stats daemon if requested. if cfg.Ethstats.URL != "" { utils.RegisterEthStatsService(stack, cfg.Ethstats.URL) diff --git a/cmd/geth/usage.go b/cmd/geth/usage.go index 4cc77b9128..58a3b87a46 100644 --- a/cmd/geth/usage.go +++ b/cmd/geth/usage.go @@ -156,20 +156,25 @@ var AppHelpFlagGroups = []flagGroup{ { Name: "API AND CONSOLE", Flags: []cli.Flag{ + utils.IPCDisabledFlag, + utils.IPCPathFlag, utils.RPCEnabledFlag, utils.RPCListenAddrFlag, utils.RPCPortFlag, utils.RPCApiFlag, utils.RPCGlobalGasCap, + utils.RPCCORSDomainFlag, + utils.RPCVirtualHostsFlag, utils.WSEnabledFlag, utils.WSListenAddrFlag, utils.WSPortFlag, utils.WSApiFlag, utils.WSAllowedOriginsFlag, - utils.IPCDisabledFlag, - utils.IPCPathFlag, - utils.RPCCORSDomainFlag, - utils.RPCVirtualHostsFlag, + utils.GraphQLEnabledFlag, + utils.GraphQLListenAddrFlag, + utils.GraphQLPortFlag, + utils.GraphQLCORSDomainFlag, + utils.GraphQLVirtualHostsFlag, utils.JSpathFlag, utils.ExecFlag, utils.PreloadJSFlag, diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 973e47ea0c..8c5a0c99a0 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -20,6 +20,7 @@ package utils import ( "crypto/ecdsa" "encoding/json" + "errors" "fmt" "io/ioutil" "math/big" @@ -45,6 +46,7 @@ import ( "github.com/ethereum/go-ethereum/eth/gasprice" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethstats" + "github.com/ethereum/go-ethereum/graphql" "github.com/ethereum/go-ethereum/les" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" @@ -57,6 +59,7 @@ import ( "github.com/ethereum/go-ethereum/p2p/nat" "github.com/ethereum/go-ethereum/p2p/netutil" "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rpc" whisper "github.com/ethereum/go-ethereum/whisper/whisperv6" pcsclite "github.com/gballet/go-libpcsclite" cli "gopkg.in/urfave/cli.v1" @@ -474,6 +477,14 @@ var ( Usage: "Disables db compaction after import", } // RPC settings + IPCDisabledFlag = cli.BoolFlag{ + Name: "ipcdisable", + Usage: "Disable the IPC-RPC server", + } + IPCPathFlag = DirectoryFlag{ + Name: "ipcpath", + Usage: "Filename for IPC socket/pipe within the datadir (explicit paths escape it)", + } RPCEnabledFlag = cli.BoolFlag{ Name: "rpc", Usage: "Enable the HTTP-RPC server", @@ -488,30 +499,6 @@ var ( Usage: "HTTP-RPC server listening port", Value: node.DefaultHTTPPort, } - GraphQLEnabledFlag = cli.BoolFlag{ - Name: "graphql", - Usage: "Enable the GraphQL server", - } - GraphQLListenAddrFlag = cli.StringFlag{ - Name: "graphql.addr", - Usage: "GraphQL server listening interface", - Value: node.DefaultGraphQLHost, - } - GraphQLPortFlag = cli.IntFlag{ - 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)", @@ -527,14 +514,6 @@ var ( Usage: "API's offered over the HTTP-RPC interface", Value: "", } - IPCDisabledFlag = cli.BoolFlag{ - Name: "ipcdisable", - Usage: "Disable the IPC-RPC server", - } - IPCPathFlag = DirectoryFlag{ - Name: "ipcpath", - Usage: "Filename for IPC socket/pipe within the datadir (explicit paths escape it)", - } WSEnabledFlag = cli.BoolFlag{ Name: "ws", Usage: "Enable the WS-RPC server", @@ -559,6 +538,30 @@ var ( Usage: "Origins from which to accept websockets requests", Value: "", } + GraphQLEnabledFlag = cli.BoolFlag{ + Name: "graphql", + Usage: "Enable the GraphQL server", + } + GraphQLListenAddrFlag = cli.StringFlag{ + Name: "graphql.addr", + Usage: "GraphQL server listening interface", + Value: node.DefaultGraphQLHost, + } + GraphQLPortFlag = cli.IntFlag{ + Name: "graphql.port", + Usage: "GraphQL server listening port", + Value: node.DefaultGraphQLPort, + } + GraphQLCORSDomainFlag = cli.StringFlag{ + Name: "graphql.corsdomain", + Usage: "Comma separated list of domains from which to accept cross origin requests (browser enforced)", + Value: "", + } + GraphQLVirtualHostsFlag = cli.StringFlag{ + Name: "graphql.vhosts", + Usage: "Comma separated list of virtual hostnames from which to accept requests (server enforced). Accepts '*' wildcard.", + Value: strings.Join(node.DefaultConfig.GraphQLVirtualHosts, ","), + } ExecFlag = cli.StringFlag{ Name: "exec", Usage: "Execute JavaScript statement", @@ -874,7 +877,6 @@ func setHTTP(ctx *cli.Context, cfg *node.Config) { cfg.HTTPHost = ctx.GlobalString(RPCListenAddrFlag.Name) } } - if ctx.GlobalIsSet(RPCPortFlag.Name) { cfg.HTTPPort = ctx.GlobalInt(RPCPortFlag.Name) } @@ -916,7 +918,6 @@ func setWS(ctx *cli.Context, cfg *node.Config) { cfg.WSHost = ctx.GlobalString(WSListenAddrFlag.Name) } } - if ctx.GlobalIsSet(WSPortFlag.Name) { cfg.WSPort = ctx.GlobalInt(WSPortFlag.Name) } @@ -1546,12 +1547,33 @@ func RegisterEthStatsService(stack *node.Node, url string) { var lesServ *les.LightEthereum ctx.Service(&lesServ) + // Let ethstats use whichever is not nil return ethstats.New(url, ethServ, lesServ) }); err != nil { Fatalf("Failed to register the Ethereum Stats service: %v", err) } } +// 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) { + if err := stack.Register(func(ctx *node.ServiceContext) (node.Service, error) { + // Try to construct the GraphQL service backed by a full node + var ethServ *eth.Ethereum + if err := ctx.Service(ðServ); err == nil { + return graphql.New(ethServ.APIBackend, endpoint, cors, vhosts, timeouts) + } + // Try to construct the GraphQL service backed by a light node + var lesServ *les.LightEthereum + if err := ctx.Service(&lesServ); err == nil { + return graphql.New(lesServ.ApiBackend, endpoint, cors, vhosts, timeouts) + } + // Well, this should not have happened, bail out + return nil, errors.New("no Ethereum service") + }); err != nil { + Fatalf("Failed to register the GraphQL service: %v", err) + } +} + func SetupMetrics(ctx *cli.Context) { if metrics.Enabled { log.Info("Enabling metrics collection") diff --git a/graphql/graphiql.go b/graphql/graphiql.go index 6d9dda3e8c..e72609254e 100644 --- a/graphql/graphiql.go +++ b/graphql/graphiql.go @@ -60,11 +60,11 @@ var graphiql = []byte(` - - - - - + + + + +
Loading...
diff --git a/graphql/graphql.go b/graphql/graphql.go index d22a3afb6f..3f6bcab992 100644 --- a/graphql/graphql.go +++ b/graphql/graphql.go @@ -20,9 +20,6 @@ package graphql import ( "context" "errors" - "fmt" - "net" - "net/http" "time" "github.com/ethereum/go-ethereum" @@ -32,16 +29,10 @@ import ( "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/eth/filters" "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/rlp" "github.com/ethereum/go-ethereum/rpc" - graphqlgo "github.com/graph-gophers/graphql-go" - "github.com/graph-gophers/graphql-go/relay" ) var OnlyOnMainChainError = errors.New("This operation is only available for blocks on the canonical chain.") @@ -49,7 +40,7 @@ var BlockInvariantError = errors.New("Block objects must be instantiated with at // Account represents an Ethereum account at a particular block. type Account struct { - backend *eth.EthAPIBackend + backend ethapi.Backend address common.Address blockNumber rpc.BlockNumber } @@ -102,7 +93,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 { - backend *eth.EthAPIBackend + backend ethapi.Backend transaction *Transaction log *types.Log } @@ -134,7 +125,7 @@ func (l *Log) Data(ctx context.Context) hexutil.Bytes { // Transaction represents an Ethereum transaction. // backend and hash are mandatory; all others will be fetched when required. type Transaction struct { - backend *eth.EthAPIBackend + backend ethapi.Backend hash common.Hash tx *types.Transaction block *Block @@ -349,7 +340,7 @@ const ( // backend, and either num or hash are mandatory. All other fields are lazily fetched // when required. type Block struct { - backend *eth.EthAPIBackend + backend ethapi.Backend num *rpc.BlockNumber hash common.Hash header *types.Header @@ -745,7 +736,7 @@ type BlockFilterCriteria struct { // runFilter accepts a filter and executes it, returning all its results as // `Log` objects. -func runFilter(ctx context.Context, be *eth.EthAPIBackend, filter *filters.Filter) ([]*Log, error) { +func runFilter(ctx context.Context, be ethapi.Backend, filter *filters.Filter) ([]*Log, error) { logs, err := filter.Logs(ctx) if err != nil || logs == nil { return nil, err @@ -888,7 +879,7 @@ func (b *Block) EstimateGas(ctx context.Context, args struct { } type Pending struct { - backend *eth.EthAPIBackend + backend ethapi.Backend } func (p *Pending) TransactionCount(ctx context.Context) (int32, error) { @@ -947,7 +938,7 @@ func (p *Pending) EstimateGas(ctx context.Context, args struct { // Resolver is the top-level object in the GraphQL hierarchy. type Resolver struct { - backend *eth.EthAPIBackend + backend ethapi.Backend } func (r *Resolver) Block(ctx context.Context, args struct { @@ -1088,7 +1079,6 @@ func (r *Resolver) Logs(ctx context.Context, args struct{ Filter FilterCriteria // Construct the range filter filter := filters.NewRangeFilter(filters.Backend(r.backend), begin, end, addresses, topics) - return runFilter(ctx, r.backend, filter) } @@ -1145,89 +1135,3 @@ func (r *Resolver) Syncing() (*SyncState, error) { // Otherwise gather the block sync stats 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(be *eth.EthAPIBackend) (http.Handler, error) { - q := Resolver{be} - - s, err := graphqlgo.ParseSchema(schema, &q) - if err != nil { - return nil, err - } - h := &relay.Handler{Schema: s} - - mux := http.NewServeMux() - mux.Handle("/", GraphiQL{}) - mux.Handle("/graphql", h) - mux.Handle("/graphql/", h) - return mux, nil -} - -// Service encapsulates a GraphQL 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. - 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. -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 -// 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.backend) - 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 -} - -// NewService constructs a new service instance. -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, - 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) { - 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/graphql/graphql_test.go b/graphql/graphql_test.go index d63418398a..22182833bf 100644 --- a/graphql/graphql_test.go +++ b/graphql/graphql_test.go @@ -22,8 +22,7 @@ import ( 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 { + if _, err := newHandler(nil); err != nil { t.Errorf("Could not construct GraphQL handler: %v", err) } } diff --git a/graphql/service.go b/graphql/service.go new file mode 100644 index 0000000000..f640756806 --- /dev/null +++ b/graphql/service.go @@ -0,0 +1,103 @@ +// Copyright 2019 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 ( + "fmt" + "net" + "net/http" + + "github.com/ethereum/go-ethereum/internal/ethapi" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/rpc" + "github.com/graph-gophers/graphql-go" + "github.com/graph-gophers/graphql-go/relay" +) + +// Service encapsulates a GraphQL 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. + backend ethapi.Backend // The backend that queries will operate onn. + handler http.Handler // The `http.Handler` used to answer queries. + listener net.Listener // The listening socket. +} + +// New constructs a new GraphQL service instance. +func New(backend ethapi.Backend, endpoint string, cors, vhosts []string, timeouts rpc.HTTPTimeouts) (*Service, error) { + return &Service{ + endpoint: endpoint, + cors: cors, + vhosts: vhosts, + timeouts: timeouts, + backend: backend, + }, nil +} + +// 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 +// 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.backend) + 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 +} + +// newHandler returns a new `http.Handler` that will answer GraphQL queries. +// It additionally exports an interactive query browser on the / endpoint. +func newHandler(backend ethapi.Backend) (http.Handler, error) { + q := Resolver{backend} + + s, err := graphql.ParseSchema(schema, &q) + if err != nil { + return nil, err + } + h := &relay.Handler{Schema: s} + + mux := http.NewServeMux() + mux.Handle("/", GraphiQL{}) + mux.Handle("/graphql", h) + mux.Handle("/graphql/", h) + return mux, 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 +} diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go index 9229ccfb64..28ec698972 100644 --- a/internal/ethapi/backend.go +++ b/internal/ethapi/backend.go @@ -24,6 +24,7 @@ import ( "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/bloombits" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" @@ -47,9 +48,10 @@ type Backend interface { ExtRPCEnabled() bool RPCGasCap() *big.Int // global gas cap for eth_call over rpc: DoS protection - // BlockChain API + // Blockchain API SetHead(number uint64) HeaderByNumber(ctx context.Context, blockNr rpc.BlockNumber) (*types.Header, error) + HeaderByHash(ctx context.Context, blockHash common.Hash) (*types.Header, error) BlockByNumber(ctx context.Context, blockNr rpc.BlockNumber) (*types.Block, error) StateAndHeaderByNumber(ctx context.Context, blockNr rpc.BlockNumber) (*state.StateDB, *types.Header, error) GetBlock(ctx context.Context, blockHash common.Hash) (*types.Block, error) @@ -60,7 +62,7 @@ type Backend interface { SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription SubscribeChainSideEvent(ch chan<- core.ChainSideEvent) event.Subscription - // TxPool API + // Transaction pool API SendTx(ctx context.Context, signedTx *types.Transaction) error GetTransaction(ctx context.Context, txHash common.Hash) (*types.Transaction, common.Hash, uint64, uint64, error) GetPoolTransactions() (types.Transactions, error) @@ -70,6 +72,13 @@ type Backend interface { TxPoolContent() (map[common.Address]types.Transactions, map[common.Address]types.Transactions) SubscribeNewTxsEvent(chan<- core.NewTxsEvent) event.Subscription + // Filter API + BloomStatus() (uint64, uint64) + GetLogs(ctx context.Context, blockHash common.Hash) ([][]*types.Log, error) + ServiceFilter(ctx context.Context, session *bloombits.MatcherSession) + SubscribeLogsEvent(ch chan<- []*types.Log) event.Subscription + SubscribeRemovedLogsEvent(ch chan<- core.RemovedLogsEvent) event.Subscription + ChainConfig() *params.ChainConfig CurrentBlock() *types.Block } diff --git a/node/defaults.go b/node/defaults.go index 564bb35b49..f84a5d5479 100644 --- a/node/defaults.go +++ b/node/defaults.go @@ -38,13 +38,15 @@ const ( // DefaultConfig contains reasonable default settings. var DefaultConfig = Config{ - DataDir: DefaultDataDir(), - HTTPPort: DefaultHTTPPort, - HTTPModules: []string{"net", "web3"}, - HTTPVirtualHosts: []string{"localhost"}, - HTTPTimeouts: rpc.DefaultHTTPTimeouts, - WSPort: DefaultWSPort, - WSModules: []string{"net", "web3"}, + DataDir: DefaultDataDir(), + HTTPPort: DefaultHTTPPort, + HTTPModules: []string{"net", "web3"}, + HTTPVirtualHosts: []string{"localhost"}, + HTTPTimeouts: rpc.DefaultHTTPTimeouts, + WSPort: DefaultWSPort, + WSModules: []string{"net", "web3"}, + GraphQLPort: DefaultGraphQLPort, + GraphQLVirtualHosts: []string{"localhost"}, P2P: p2p.Config{ ListenAddr: ":30303", MaxPeers: 50,