diff --git a/README.md b/README.md index 056a1655..cca12140 100644 --- a/README.md +++ b/README.md @@ -66,15 +66,23 @@ The corresponding CLI flags can be found with the `./ipld-eth-server serve --hel ipcPath = "~/.vulcanize/vulcanize.ipc" # $SERVER_IPC_PATH wsPath = "127.0.0.1:8081" # $SERVER_WS_PATH httpPath = "127.0.0.1:8082" # $SERVER_HTTP_PATH + graphql = true # $SERVER_GRAPHQL + graphqlEndpoint = "" # $SERVER_GRAPHQL_ENDPOINT [ethereum] chainID = "1" # $ETH_CHAIN_ID defaultSender = "" # $ETH_DEFAULT_SENDER_ADDR + rpcGasCap = "1000000000000" # $ETH_RPC_GAS_CAP + httpPath = "127.0.0.1:8545" # $ETH_HTTP_PATH + nodeID = "arch1" # $ETH_NODE_ID + clientName = "Geth" # $ETH_CLIENT_NAME + genesisBlock = "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" # $ETH_GENESIS_BLOCK + networkID = "1" # $ETH_NETWORK_ID ``` -The `database` fields are for connecting to a Postgres database that has been/is being populated by [ipld-eth-indexer](https://github.com/vulcanize/ipld-eth-indexer). -The `server` fields set the paths for exposing the ipld-eth-server endpoints -The `ethereum` fields set the chainID and default sender address to use for EVM simulation +The `database` fields are for connecting to a Postgres database that has been/is being populated by [ipld-eth-indexer](https://github.com/vulcanize/ipld-eth-indexer) +The `server` fields set the paths for exposing the ipld-eth-server endpoints +The `ethereum` fields set the chainID and default sender address to use for EVM simulation, and can optionally be used to configure a remote eth node to forward cache misses to ### Endpoints diff --git a/cmd/serve.go b/cmd/serve.go index 8ccc63c5..eb4b23e4 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -20,6 +20,8 @@ import ( "os/signal" "sync" + "github.com/vulcanize/ipld-eth-server/pkg/graphql" + "github.com/ethereum/go-ethereum/rpc" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -28,7 +30,6 @@ import ( "github.com/vulcanize/ipld-eth-indexer/pkg/eth" srpc "github.com/vulcanize/ipld-eth-server/pkg/rpc" - s "github.com/vulcanize/ipld-eth-server/pkg/serve" v "github.com/vulcanize/ipld-eth-server/version" ) @@ -70,10 +71,17 @@ func serve() { if err := startServers(server, serverConfig); err != nil { logWithCommand.Fatal(err) } + graphQL, err := startGraphQL(server) + if err != nil { + logWithCommand.Fatal(err) + } shutdown := make(chan os.Signal) signal.Notify(shutdown, os.Interrupt) <-shutdown + if graphQL != nil { + graphQL.Stop() + } server.Stop() wg.Wait() } @@ -91,14 +99,32 @@ func startServers(server s.Server, settings *s.Config) error { } logWithCommand.Info("starting up HTTP server") _, _, err = srpc.StartHTTPEndpoint(settings.HTTPEndpoint, server.APIs(), []string{"eth"}, nil, []string{"*"}, rpc.HTTPTimeouts{}) - return err } +func startGraphQL(server s.Server) (graphQLServer *graphql.Service, err error) { + viper.BindEnv("server.graphql", "SERVER_GRAPHQL") + if viper.GetBool("server.graphql") { + logWithCommand.Info("starting up GraphQL server") + viper.BindEnv("server.graphqlEndpoint", "SERVER_GRAPHQL_ENDPOINT") + endPoint := viper.GetString("server.graphqlEndpoint") + if endPoint != "" { + graphQLServer, err = graphql.New(server.Backend(), endPoint, nil, []string{"*"}, rpc.HTTPTimeouts{}) + if err != nil { + return + } + err = graphQLServer.Start(nil) + } + } + return +} + func init() { rootCmd.AddCommand(serveCmd) // flags for all config variables + serveCmd.PersistentFlags().Bool("server-graphql", false, "turn on the graphql server") + serveCmd.PersistentFlags().String("server-graphql-endpoint", "", "endpoint url for graphql server") serveCmd.PersistentFlags().String("server-ws-path", "", "vdb server ws path") serveCmd.PersistentFlags().String("server-http-path", "", "vdb server http path") serveCmd.PersistentFlags().String("server-ipc-path", "", "vdb server ipc path") @@ -113,6 +139,8 @@ func init() { serveCmd.PersistentFlags().String("eth-rpc-gas-cap", "", "rpc gas cap (for eth_Call execution)") // and their bindings + viper.BindPFlag("server.graphql", serveCmd.PersistentFlags().Lookup("server-graphql")) + viper.BindPFlag("server.graphqlEndpoint", serveCmd.PersistentFlags().Lookup("server-graphql-endpoint")) viper.BindPFlag("server.wsPath", serveCmd.PersistentFlags().Lookup("server-ws-path")) viper.BindPFlag("server.httpPath", serveCmd.PersistentFlags().Lookup("server-http-path")) viper.BindPFlag("server.ipcPath", serveCmd.PersistentFlags().Lookup("server-ipc-path")) diff --git a/environments/example.toml b/environments/example.toml index 187bf1b6..380540ac 100644 --- a/environments/example.toml +++ b/environments/example.toml @@ -12,6 +12,8 @@ ipcPath = "~/.vulcanize/vulcanize.ipc" # $SERVER_IPC_PATH wsPath = "127.0.0.1:8081" # $SERVER_WS_PATH httpPath = "127.0.0.1:8082" # $SERVER_HTTP_PATH + graphql = true # $SERVER_GRAPHQL + graphqlEndpoint = "127.0.0.1:8083" # $SERVER_GRAPHQL_ENDPOINT [ethereum] chainID = "1" # $ETH_CHAIN_ID diff --git a/go.mod b/go.mod index 1a68847d..b8759647 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.13 require ( github.com/ethereum/go-ethereum v1.9.11 + github.com/graph-gophers/graphql-go v0.0.0-20201003130358-c5bdf3b1108e github.com/ipfs/go-block-format v0.0.2 github.com/ipfs/go-cid v0.0.5 github.com/ipfs/go-ipfs-blockstore v1.0.0 diff --git a/go.sum b/go.sum index 956ef686..797d8fcd 100644 --- a/go.sum +++ b/go.sum @@ -224,6 +224,8 @@ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/graph-gophers/graphql-go v0.0.0-20191115155744-f33e81362277/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= +github.com/graph-gophers/graphql-go v0.0.0-20201003130358-c5bdf3b1108e h1:IpssFbpfPSx/3c7x601Npx+UOQ4tqd0Rk4sObCQ+zlQ= +github.com/graph-gophers/graphql-go v0.0.0-20201003130358-c5bdf3b1108e/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= diff --git a/pkg/eth/api.go b/pkg/eth/api.go index f61888e5..9a792c0b 100644 --- a/pkg/eth/api.go +++ b/pkg/eth/api.go @@ -145,11 +145,7 @@ func (pea *PublicEthAPI) getLogs(ctx context.Context, crit ethereum.FilterQuery) startingBlock := crit.FromBlock endingBlock := crit.ToBlock if startingBlock == nil { - startingBlockInt, err := pea.B.Retriever.RetrieveFirstBlockNumber() - if err != nil { - return nil, err - } - startingBlock = big.NewInt(startingBlockInt) + startingBlock = common.Big0 } if endingBlock == nil { endingBlockInt, err := pea.B.Retriever.RetrieveLastBlockNumber() diff --git a/pkg/eth/backend.go b/pkg/eth/backend.go index ba51dd54..4e5ff088 100644 --- a/pkg/eth/backend.go +++ b/pkg/eth/backend.go @@ -21,6 +21,9 @@ import ( "errors" "math/big" + "github.com/ethereum/go-ethereum/core/bloombits" + "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/math" @@ -36,7 +39,6 @@ import ( "github.com/ethereum/go-ethereum/rpc" ipfsethdb "github.com/vulcanize/pg-ipfs-ethdb" - "github.com/vulcanize/ipld-eth-indexer/pkg/ipfs" "github.com/vulcanize/ipld-eth-indexer/pkg/postgres" "github.com/vulcanize/ipld-eth-server/pkg/shared" ) @@ -53,6 +55,12 @@ const ( RetrieveCanonicalHeaderByNumber = `SELECT cid, data FROM eth.header_cids INNER JOIN public.blocks ON (header_cids.mh_key = blocks.key) WHERE id = (SELECT canonical_header($1))` + RetrieveTD = `SELECT td FROM eth.header_cids + WHERE header_cids.block_hash = $1` + RetrieveRPCTransaction = `SELECT blocks.data, block_hash, block_number, index FROM public.blocks, eth.transaction_cids, eth.header_cids + WHERE blocks.key = transaction_cids.mh_key + AND transaction_cids.header_id = header_cids.id + AND transaction_cids.tx_hash = $1` ) type Backend struct { @@ -92,6 +100,11 @@ func NewEthBackend(db *postgres.DB, c *Config) (*Backend, error) { }, nil } +// ChainDb returns the backend's underlying chain database +func (b *Backend) ChainDb() ethdb.Database { + return b.EthDB +} + // HeaderByNumber gets the canonical header for the provided block number func (b *Backend) HeaderByNumber(ctx context.Context, blockNumber rpc.BlockNumber) (*types.Header, error) { var err error @@ -133,12 +146,43 @@ func (b *Backend) HeaderByHash(ctx context.Context, hash common.Hash) (*types.He return header, rlp.DecodeBytes(headerRLP, header) } +// HeaderByNumberOrHash gets the header for the provided block hash or number +func (b *Backend) HeaderByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*types.Header, error) { + if blockNr, ok := blockNrOrHash.Number(); ok { + return b.HeaderByNumber(ctx, blockNr) + } + if hash, ok := blockNrOrHash.Hash(); ok { + header, err := b.HeaderByHash(ctx, hash) + if err != nil { + return nil, err + } + if header == nil { + return nil, errors.New("header for hash not found") + } + if blockNrOrHash.RequireCanonical && b.GetCanonicalHash(header.Number.Uint64()) != hash { + return nil, errors.New("hash is not currently canonical") + } + return header, nil + } + return nil, errors.New("invalid arguments; neither block nor hash specified") +} + +// rpcMarshalHeader uses the generalized output filler, then adds the total difficulty field, which requires +// a `PublicEthAPI`. +func (pea *PublicEthAPI) rpcMarshalHeader(header *types.Header) (map[string]interface{}, error) { + fields := RPCMarshalHeader(header) + td, err := pea.B.GetTd(header.Hash()) + if err != nil { + return nil, err + } + fields["totalDifficulty"] = (*hexutil.Big)(td) + return fields, nil +} + // GetTd gets the total difficulty at the given block hash func (b *Backend) GetTd(blockHash common.Hash) (*big.Int, error) { - pgStr := `SELECT td FROM eth.header_cids - WHERE header_cids.block_hash = $1` var tdStr string - err := b.DB.Get(&tdStr, pgStr, blockHash.String()) + err := b.DB.Get(&tdStr, RetrieveTD, blockHash.String()) if err != nil { return nil, err } @@ -149,21 +193,38 @@ func (b *Backend) GetTd(blockHash common.Hash) (*big.Int, error) { return td, nil } -// GetLogs returns all the logs for the given block hash -func (b *Backend) GetLogs(ctx context.Context, hash common.Hash) ([][]*types.Log, error) { - _, receiptBytes, err := b.IPLDRetriever.RetrieveReceiptsByBlockHash(hash) - if err != nil { - return nil, err +// CurrentBlock returns the current block +func (b *Backend) CurrentBlock() *types.Block { + block, _ := b.BlockByNumber(context.Background(), rpc.LatestBlockNumber) + return block +} + +// BlockByNumberOrHash returns block by number or hash +func (b *Backend) BlockByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*types.Block, error) { + if blockNr, ok := blockNrOrHash.Number(); ok { + return b.BlockByNumber(ctx, blockNr) } - logs := make([][]*types.Log, len(receiptBytes)) - for i, rctBytes := range receiptBytes { - var rct types.Receipt - if err := rlp.DecodeBytes(rctBytes, &rct); err != nil { + if hash, ok := blockNrOrHash.Hash(); ok { + header, err := b.HeaderByHash(ctx, hash) + if err != nil { return nil, err } - logs[i] = rct.Logs + if header == nil { + return nil, errors.New("header for hash not found") + } + if blockNrOrHash.RequireCanonical && b.GetCanonicalHash(header.Number.Uint64()) != hash { + return nil, errors.New("hash is not currently canonical") + } + block, err := b.BlockByHash(ctx, hash) + if err != nil { + return nil, err + } + if block == nil { + return nil, errors.New("header found, but block body is missing") + } + return block, nil } - return logs, nil + return nil, errors.New("invalid arguments; neither block nor hash specified") } // BlockByNumber returns the requested canonical block. @@ -354,11 +415,7 @@ func (b *Backend) GetTransaction(ctx context.Context, txHash common.Hash) (*type BlockNumber uint64 `db:"block_number"` Index uint64 `db:"index"` } - pgStr := `SELECT blocks.data, block_hash, block_number, index FROM public.blocks, eth.transaction_cids, eth.header_cids - WHERE blocks.key = transaction_cids.mh_key - AND transaction_cids.header_id = header_cids.id - AND transaction_cids.tx_hash = $1` - if err := b.DB.Get(&tempTxStruct, pgStr, txHash.String()); err != nil { + if err := b.DB.Get(&tempTxStruct, RetrieveRPCTransaction, txHash.String()); err != nil { return nil, common.Hash{}, 0, 0, err } var transaction types.Transaction @@ -368,88 +425,38 @@ func (b *Backend) GetTransaction(ctx context.Context, txHash common.Hash) (*type return &transaction, common.HexToHash(tempTxStruct.BlockHash), tempTxStruct.BlockNumber, tempTxStruct.Index, nil } -// extractLogsOfInterest returns logs from the receipt IPLD -func extractLogsOfInterest(rctIPLDs []ipfs.BlockModel, wantedTopics [][]string) ([]*types.Log, error) { - var logs []*types.Log - for _, rctIPLD := range rctIPLDs { - rctRLP := rctIPLD - var rct types.Receipt - if err := rlp.DecodeBytes(rctRLP.Data, &rct); err != nil { - return nil, err - } - for _, log := range rct.Logs { - if wanted := wantedLog(wantedTopics, log.Topics); wanted == true { - logs = append(logs, log) - } - } - } - return logs, nil -} - -// returns true if the log matches on the filter -func wantedLog(wantedTopics [][]string, actualTopics []common.Hash) bool { - // actualTopics will always have length <= 4 - // wantedTopics will always have length 4 - matches := 0 - for i, actualTopic := range actualTopics { - // If we have topics in this filter slot, count as a match if the actualTopic matches one of the ones in this filter slot - if len(wantedTopics[i]) > 0 { - matches += sliceContainsHash(wantedTopics[i], actualTopic) - } else { - // Filter slot is empty, not matching any topics at this slot => counts as a match - matches++ - } - } - if matches == len(actualTopics) { - return true - } - return false -} - -// returns 1 if the slice contains the hash, 0 if it does not -func sliceContainsHash(slice []string, hash common.Hash) int { - for _, str := range slice { - if str == hash.String() { - return 1 - } - } - return 0 -} - -// rpcMarshalHeader uses the generalized output filler, then adds the total difficulty field, which requires -// a `PublicEthAPI`. -func (pea *PublicEthAPI) rpcMarshalHeader(header *types.Header) (map[string]interface{}, error) { - fields := RPCMarshalHeader(header) - td, err := pea.B.GetTd(header.Hash()) +// GetReceipts retrieves receipts for provided block hash +func (b *Backend) GetReceipts(ctx context.Context, hash common.Hash) (types.Receipts, error) { + _, receiptBytes, err := b.IPLDRetriever.RetrieveReceiptsByBlockHash(hash) if err != nil { return nil, err } - fields["totalDifficulty"] = (*hexutil.Big)(td) - return fields, nil + rcts := make(types.Receipts, 0, len(receiptBytes)) + for i, rctBytes := range receiptBytes { + rct := new(types.Receipt) + if err := rlp.DecodeBytes(rctBytes, rct); err != nil { + return nil, err + } + rcts[i] = rct + } + return rcts, nil } -// RPCMarshalHeader converts the given header to the RPC output. -// This function is eth/internal so we have to make our own version here... -func RPCMarshalHeader(head *types.Header) map[string]interface{} { - return map[string]interface{}{ - "number": (*hexutil.Big)(head.Number), - "hash": head.Hash(), - "parentHash": head.ParentHash, - "nonce": head.Nonce, - "mixHash": head.MixDigest, - "sha3Uncles": head.UncleHash, - "logsBloom": head.Bloom, - "stateRoot": head.Root, - "miner": head.Coinbase, - "difficulty": (*hexutil.Big)(head.Difficulty), - "extraData": hexutil.Bytes(head.Extra), - "size": hexutil.Uint64(head.Size()), - "gasLimit": hexutil.Uint64(head.GasLimit), - "gasUsed": hexutil.Uint64(head.GasUsed), - "timestamp": hexutil.Uint64(head.Time), - "transactionsRoot": head.TxHash, - "receiptsRoot": head.ReceiptHash, +// GetLogs returns all the logs for the given block hash +func (b *Backend) GetLogs(ctx context.Context, hash common.Hash) ([][]*types.Log, error) { + _, receiptBytes, err := b.IPLDRetriever.RetrieveReceiptsByBlockHash(hash) + if err != nil { + return nil, err } + logs := make([][]*types.Log, len(receiptBytes)) + for i, rctBytes := range receiptBytes { + var rct types.Receipt + if err := rlp.DecodeBytes(rctBytes, &rct); err != nil { + return nil, err + } + logs[i] = rct.Logs + } + return logs, nil } // rpcMarshalBlock uses the generalized output filler, then adds the total difficulty field, which requires @@ -467,110 +474,6 @@ func (pea *PublicEthAPI) rpcMarshalBlock(b *types.Block, inclTx bool, fullTx boo return fields, err } -// RPCMarshalBlock converts the given block to the RPC output which depends on fullTx. If inclTx is true transactions are -// returned. When fullTx is true the returned block contains full transaction details, otherwise it will only contain -// transaction hashes. -func RPCMarshalBlock(block *types.Block, inclTx bool, fullTx bool) (map[string]interface{}, error) { - fields := RPCMarshalHeader(block.Header()) - fields["size"] = hexutil.Uint64(block.Size()) - - if inclTx { - formatTx := func(tx *types.Transaction) (interface{}, error) { - return tx.Hash(), nil - } - if fullTx { - formatTx = func(tx *types.Transaction) (interface{}, error) { - return NewRPCTransactionFromBlockHash(block, tx.Hash()), nil - } - } - txs := block.Transactions() - transactions := make([]interface{}, len(txs)) - var err error - for i, tx := range txs { - if transactions[i], err = formatTx(tx); err != nil { - return nil, err - } - } - fields["transactions"] = transactions - } - uncles := block.Uncles() - uncleHashes := make([]common.Hash, len(uncles)) - for i, uncle := range uncles { - uncleHashes[i] = uncle.Hash() - } - fields["uncles"] = uncleHashes - - return fields, nil -} - -// NewRPCTransactionFromBlockHash returns a transaction that will serialize to the RPC representation. -func NewRPCTransactionFromBlockHash(b *types.Block, hash common.Hash) *RPCTransaction { - for idx, tx := range b.Transactions() { - if tx.Hash() == hash { - return newRPCTransactionFromBlockIndex(b, uint64(idx)) - } - } - return nil -} - -// newRPCTransactionFromBlockIndex returns a transaction that will serialize to the RPC representation. -func newRPCTransactionFromBlockIndex(b *types.Block, index uint64) *RPCTransaction { - txs := b.Transactions() - if index >= uint64(len(txs)) { - return nil - } - return NewRPCTransaction(txs[index], b.Hash(), b.NumberU64(), index) -} - -// RPCTransaction represents a transaction that will serialize to the RPC representation of a transaction -type RPCTransaction struct { - BlockHash *common.Hash `json:"blockHash"` - BlockNumber *hexutil.Big `json:"blockNumber"` - From common.Address `json:"from"` - Gas hexutil.Uint64 `json:"gas"` - GasPrice *hexutil.Big `json:"gasPrice"` - Hash common.Hash `json:"hash"` - Input hexutil.Bytes `json:"input"` - Nonce hexutil.Uint64 `json:"nonce"` - To *common.Address `json:"to"` - TransactionIndex *hexutil.Uint64 `json:"transactionIndex"` - Value *hexutil.Big `json:"value"` - V *hexutil.Big `json:"v"` - R *hexutil.Big `json:"r"` - S *hexutil.Big `json:"s"` -} - -// NewRPCTransaction returns a transaction that will serialize to the RPC -// representation, with the given location metadata set (if available). -func NewRPCTransaction(tx *types.Transaction, blockHash common.Hash, blockNumber uint64, index uint64) *RPCTransaction { - var signer types.Signer = types.FrontierSigner{} - if tx.Protected() { - signer = types.NewEIP155Signer(tx.ChainId()) - } - from, _ := types.Sender(signer, tx) - v, r, s := tx.RawSignatureValues() - - result := &RPCTransaction{ - From: from, - Gas: hexutil.Uint64(tx.Gas()), - GasPrice: (*hexutil.Big)(tx.GasPrice()), - Hash: tx.Hash(), - Input: hexutil.Bytes(tx.Data()), // somehow this is ending up `nil` - Nonce: hexutil.Uint64(tx.Nonce()), - To: tx.To(), - Value: (*hexutil.Big)(tx.Value()), - V: (*hexutil.Big)(v), - R: (*hexutil.Big)(r), - S: (*hexutil.Big)(s), - } - if blockHash != (common.Hash{}) { - result.BlockHash = &blockHash - result.BlockNumber = (*hexutil.Big)(new(big.Int).SetUint64(blockNumber)) - result.TransactionIndex = (*hexutil.Uint64)(&index) - } - return result -} - // StateAndHeaderByNumberOrHash returns the statedb and header for the provided block number or hash func (b *Backend) StateAndHeaderByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*state.StateDB, *types.Header, error) { if blockNr, ok := blockNrOrHash.Number(); ok { @@ -652,3 +555,36 @@ func (b *Backend) GetHeader(hash common.Hash, height uint64) *types.Header { } return header } + +// RPCGasCap returns the configured gas cap for the rpc server +func (b *Backend) RPCGasCap() *big.Int { + return b.Config.RPCGasCap +} + +func (b *Backend) SubscribeNewTxsEvent(chan<- core.NewTxsEvent) event.Subscription { + panic("implement me") +} + +func (b *Backend) SubscribeChainEvent(ch chan<- core.ChainEvent) event.Subscription { + panic("implement me") +} + +func (b *Backend) SubscribeRemovedLogsEvent(ch chan<- core.RemovedLogsEvent) event.Subscription { + panic("implement me") +} + +func (b *Backend) SubscribeLogsEvent(ch chan<- []*types.Log) event.Subscription { + panic("implement me") +} + +func (b *Backend) SubscribePendingLogsEvent(ch chan<- []*types.Log) event.Subscription { + panic("implement me") +} + +func (b *Backend) BloomStatus() (uint64, uint64) { + panic("implement me") +} + +func (b *Backend) ServiceFilter(ctx context.Context, session *bloombits.MatcherSession) { + panic("implement me") +} diff --git a/pkg/eth/backend_utils.go b/pkg/eth/backend_utils.go new file mode 100644 index 00000000..9d7277eb --- /dev/null +++ b/pkg/eth/backend_utils.go @@ -0,0 +1,185 @@ +// VulcanizeDB +// Copyright © 2019 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program 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 Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package eth + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rlp" + "github.com/vulcanize/ipld-eth-indexer/pkg/ipfs" +) + +// RPCMarshalHeader converts the given header to the RPC output. +// This function is eth/internal so we have to make our own version here... +func RPCMarshalHeader(head *types.Header) map[string]interface{} { + return map[string]interface{}{ + "number": (*hexutil.Big)(head.Number), + "hash": head.Hash(), + "parentHash": head.ParentHash, + "nonce": head.Nonce, + "mixHash": head.MixDigest, + "sha3Uncles": head.UncleHash, + "logsBloom": head.Bloom, + "stateRoot": head.Root, + "miner": head.Coinbase, + "difficulty": (*hexutil.Big)(head.Difficulty), + "extraData": hexutil.Bytes(head.Extra), + "size": hexutil.Uint64(head.Size()), + "gasLimit": hexutil.Uint64(head.GasLimit), + "gasUsed": hexutil.Uint64(head.GasUsed), + "timestamp": hexutil.Uint64(head.Time), + "transactionsRoot": head.TxHash, + "receiptsRoot": head.ReceiptHash, + } +} + +// RPCMarshalBlock converts the given block to the RPC output which depends on fullTx. If inclTx is true transactions are +// returned. When fullTx is true the returned block contains full transaction details, otherwise it will only contain +// transaction hashes. +func RPCMarshalBlock(block *types.Block, inclTx bool, fullTx bool) (map[string]interface{}, error) { + fields := RPCMarshalHeader(block.Header()) + fields["size"] = hexutil.Uint64(block.Size()) + + if inclTx { + formatTx := func(tx *types.Transaction) (interface{}, error) { + return tx.Hash(), nil + } + if fullTx { + formatTx = func(tx *types.Transaction) (interface{}, error) { + return NewRPCTransactionFromBlockHash(block, tx.Hash()), nil + } + } + txs := block.Transactions() + transactions := make([]interface{}, len(txs)) + var err error + for i, tx := range txs { + if transactions[i], err = formatTx(tx); err != nil { + return nil, err + } + } + fields["transactions"] = transactions + } + uncles := block.Uncles() + uncleHashes := make([]common.Hash, len(uncles)) + for i, uncle := range uncles { + uncleHashes[i] = uncle.Hash() + } + fields["uncles"] = uncleHashes + + return fields, nil +} + +// NewRPCTransactionFromBlockHash returns a transaction that will serialize to the RPC representation. +func NewRPCTransactionFromBlockHash(b *types.Block, hash common.Hash) *RPCTransaction { + for idx, tx := range b.Transactions() { + if tx.Hash() == hash { + return newRPCTransactionFromBlockIndex(b, uint64(idx)) + } + } + return nil +} + +// NewRPCTransaction returns a transaction that will serialize to the RPC +// representation, with the given location metadata set (if available). +func NewRPCTransaction(tx *types.Transaction, blockHash common.Hash, blockNumber uint64, index uint64) *RPCTransaction { + var signer types.Signer = types.FrontierSigner{} + if tx.Protected() { + signer = types.NewEIP155Signer(tx.ChainId()) + } + from, _ := types.Sender(signer, tx) + v, r, s := tx.RawSignatureValues() + + result := &RPCTransaction{ + From: from, + Gas: hexutil.Uint64(tx.Gas()), + GasPrice: (*hexutil.Big)(tx.GasPrice()), + Hash: tx.Hash(), + Input: hexutil.Bytes(tx.Data()), // somehow this is ending up `nil` + Nonce: hexutil.Uint64(tx.Nonce()), + To: tx.To(), + Value: (*hexutil.Big)(tx.Value()), + V: (*hexutil.Big)(v), + R: (*hexutil.Big)(r), + S: (*hexutil.Big)(s), + } + if blockHash != (common.Hash{}) { + result.BlockHash = &blockHash + result.BlockNumber = (*hexutil.Big)(new(big.Int).SetUint64(blockNumber)) + result.TransactionIndex = (*hexutil.Uint64)(&index) + } + return result +} + +// newRPCTransactionFromBlockIndex returns a transaction that will serialize to the RPC representation. +func newRPCTransactionFromBlockIndex(b *types.Block, index uint64) *RPCTransaction { + txs := b.Transactions() + if index >= uint64(len(txs)) { + return nil + } + return NewRPCTransaction(txs[index], b.Hash(), b.NumberU64(), index) +} + +// extractLogsOfInterest returns logs from the receipt IPLD +func extractLogsOfInterest(rctIPLDs []ipfs.BlockModel, wantedTopics [][]string) ([]*types.Log, error) { + var logs []*types.Log + for _, rctIPLD := range rctIPLDs { + rctRLP := rctIPLD + var rct types.Receipt + if err := rlp.DecodeBytes(rctRLP.Data, &rct); err != nil { + return nil, err + } + for _, log := range rct.Logs { + if wanted := wantedLog(wantedTopics, log.Topics); wanted == true { + logs = append(logs, log) + } + } + } + return logs, nil +} + +// returns true if the log matches on the filter +func wantedLog(wantedTopics [][]string, actualTopics []common.Hash) bool { + // actualTopics will always have length <= 4 + // wantedTopics will always have length 4 + matches := 0 + for i, actualTopic := range actualTopics { + // If we have topics in this filter slot, count as a match if the actualTopic matches one of the ones in this filter slot + if len(wantedTopics[i]) > 0 { + matches += sliceContainsHash(wantedTopics[i], actualTopic) + } else { + // Filter slot is empty, not matching any topics at this slot => counts as a match + matches++ + } + } + if matches == len(actualTopics) { + return true + } + return false +} + +// returns 1 if the slice contains the hash, 0 if it does not +func sliceContainsHash(slice []string, hash common.Hash) int { + for _, str := range slice { + if str == hash.String() { + return 1 + } + } + return 0 +} diff --git a/pkg/eth/interfaces.go b/pkg/eth/interfaces.go new file mode 100644 index 00000000..9e2dcae3 --- /dev/null +++ b/pkg/eth/interfaces.go @@ -0,0 +1,47 @@ +// VulcanizeDB +// Copyright © 2019 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program 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 Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package eth + +import ( + "context" + + "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/types" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/rpc" +) + +// FilterBackend is the geth interface we need to satisfy to use their filters +type FilterBackend interface { + ChainDb() ethdb.Database + HeaderByNumber(ctx context.Context, blockNr rpc.BlockNumber) (*types.Header, error) + HeaderByHash(ctx context.Context, blockHash common.Hash) (*types.Header, error) + GetReceipts(ctx context.Context, blockHash common.Hash) (types.Receipts, error) + GetLogs(ctx context.Context, blockHash common.Hash) ([][]*types.Log, error) + + SubscribeNewTxsEvent(chan<- core.NewTxsEvent) event.Subscription + SubscribeChainEvent(ch chan<- core.ChainEvent) event.Subscription + SubscribeRemovedLogsEvent(ch chan<- core.RemovedLogsEvent) event.Subscription + SubscribeLogsEvent(ch chan<- []*types.Log) event.Subscription + SubscribePendingLogsEvent(ch chan<- []*types.Log) event.Subscription + + BloomStatus() (uint64, uint64) + ServiceFilter(ctx context.Context, session *bloombits.MatcherSession) +} diff --git a/pkg/eth/ipld_retriever.go b/pkg/eth/ipld_retriever.go index c3e3aa08..b2800300 100644 --- a/pkg/eth/ipld_retriever.go +++ b/pkg/eth/ipld_retriever.go @@ -96,8 +96,8 @@ const ( ) type ipldResult struct { - cid string `db:"cid"` - data []byte `db:"data"` + CID string `db:"cid"` + Data []byte `db:"data"` } type IPLDRetriever struct { db *postgres.DB @@ -122,8 +122,8 @@ func (r *IPLDRetriever) RetrieveHeadersByHashes(hashes []common.Hash) ([]string, cids := make([]string, len(headerResults)) headers := make([][]byte, len(headerResults)) for i, res := range headerResults { - cids[i] = res.cid - headers[i] = res.data + cids[i] = res.CID + headers[i] = res.Data } return cids, headers, nil } @@ -138,8 +138,8 @@ func (r *IPLDRetriever) RetrieveHeadersByBlockNumber(number uint64) ([]string, [ cids := make([]string, len(headerResults)) headers := make([][]byte, len(headerResults)) for i, res := range headerResults { - cids[i] = res.cid - headers[i] = res.data + cids[i] = res.CID + headers[i] = res.Data } return cids, headers, nil } @@ -147,7 +147,7 @@ func (r *IPLDRetriever) RetrieveHeadersByBlockNumber(number uint64) ([]string, [ // RetrieveHeaderByHash returns the cid and rlp bytes for the header corresponding to the provided block hash func (r *IPLDRetriever) RetrieveHeaderByHash(hash common.Hash) (string, []byte, error) { headerResult := new(ipldResult) - return headerResult.cid, headerResult.data, r.db.Get(headerResult, RetrieveHeaderByHashPgStr, hash.Hex()) + return headerResult.CID, headerResult.Data, r.db.Get(headerResult, RetrieveHeaderByHashPgStr, hash.Hex()) } // RetrieveUnclesByHashes returns the cids and rlp bytes for the uncles corresponding to the provided uncle hashes @@ -163,8 +163,8 @@ func (r *IPLDRetriever) RetrieveUnclesByHashes(hashes []common.Hash) ([]string, cids := make([]string, len(uncleResults)) uncles := make([][]byte, len(uncleResults)) for i, res := range uncleResults { - cids[i] = res.cid - uncles[i] = res.data + cids[i] = res.CID + uncles[i] = res.Data } return cids, uncles, nil } @@ -178,8 +178,8 @@ func (r *IPLDRetriever) RetrieveUnclesByBlockHash(hash common.Hash) ([]string, [ cids := make([]string, len(uncleResults)) uncles := make([][]byte, len(uncleResults)) for i, res := range uncleResults { - cids[i] = res.cid - uncles[i] = res.data + cids[i] = res.CID + uncles[i] = res.Data } return cids, uncles, nil } @@ -193,8 +193,8 @@ func (r *IPLDRetriever) RetrieveUnclesByBlockNumber(number uint64) ([]string, [] cids := make([]string, len(uncleResults)) uncles := make([][]byte, len(uncleResults)) for i, res := range uncleResults { - cids[i] = res.cid - uncles[i] = res.data + cids[i] = res.CID + uncles[i] = res.Data } return cids, uncles, nil } @@ -202,7 +202,7 @@ func (r *IPLDRetriever) RetrieveUnclesByBlockNumber(number uint64) ([]string, [] // RetrieveUncleByHash returns the cid and rlp bytes for the uncle corresponding to the provided uncle hash func (r *IPLDRetriever) RetrieveUncleByHash(hash common.Hash) (string, []byte, error) { uncleResult := new(ipldResult) - return uncleResult.cid, uncleResult.data, r.db.Get(uncleResult, RetrieveUncleByHashPgStr, hash.Hex()) + return uncleResult.CID, uncleResult.Data, r.db.Get(uncleResult, RetrieveUncleByHashPgStr, hash.Hex()) } // RetrieveTransactionsByHashes returns the cids and rlp bytes for the transactions corresponding to the provided tx hashes @@ -218,8 +218,8 @@ func (r *IPLDRetriever) RetrieveTransactionsByHashes(hashes []common.Hash) ([]st cids := make([]string, len(txResults)) txs := make([][]byte, len(txResults)) for i, res := range txResults { - cids[i] = res.cid - txs[i] = res.data + cids[i] = res.CID + txs[i] = res.Data } return cids, txs, nil } @@ -233,8 +233,8 @@ func (r *IPLDRetriever) RetrieveTransactionsByBlockHash(hash common.Hash) ([]str cids := make([]string, len(txResults)) txs := make([][]byte, len(txResults)) for i, res := range txResults { - cids[i] = res.cid - txs[i] = res.data + cids[i] = res.CID + txs[i] = res.Data } return cids, txs, nil } @@ -248,8 +248,8 @@ func (r *IPLDRetriever) RetrieveTransactionsByBlockNumber(number uint64) ([]stri cids := make([]string, len(txResults)) txs := make([][]byte, len(txResults)) for i, res := range txResults { - cids[i] = res.cid - txs[i] = res.data + cids[i] = res.CID + txs[i] = res.Data } return cids, txs, nil } @@ -257,7 +257,7 @@ func (r *IPLDRetriever) RetrieveTransactionsByBlockNumber(number uint64) ([]stri // RetrieveTransactionByTxHash returns the cid and rlp bytes for the transaction corresponding to the provided tx hash func (r *IPLDRetriever) RetrieveTransactionByTxHash(hash common.Hash) (string, []byte, error) { txResult := new(ipldResult) - return txResult.cid, txResult.data, r.db.Get(txResult, RetrieveTransactionByHashPgStr, hash.Hex()) + return txResult.CID, txResult.Data, r.db.Get(txResult, RetrieveTransactionByHashPgStr, hash.Hex()) } // RetrieveReceiptsByTxHashes returns the cids and rlp bytes for the receipts corresponding to the provided tx hashes @@ -273,8 +273,8 @@ func (r *IPLDRetriever) RetrieveReceiptsByTxHashes(hashes []common.Hash) ([]stri cids := make([]string, len(rctResults)) rcts := make([][]byte, len(rctResults)) for i, res := range rctResults { - cids[i] = res.cid - rcts[i] = res.data + cids[i] = res.CID + rcts[i] = res.Data } return cids, rcts, nil } @@ -288,8 +288,8 @@ func (r *IPLDRetriever) RetrieveReceiptsByBlockHash(hash common.Hash) ([]string, cids := make([]string, len(rctResults)) rcts := make([][]byte, len(rctResults)) for i, res := range rctResults { - cids[i] = res.cid - rcts[i] = res.data + cids[i] = res.CID + rcts[i] = res.Data } return cids, rcts, nil } @@ -303,8 +303,8 @@ func (r *IPLDRetriever) RetrieveReceiptsByBlockNumber(number uint64) ([]string, cids := make([]string, len(rctResults)) rcts := make([][]byte, len(rctResults)) for i, res := range rctResults { - cids[i] = res.cid - rcts[i] = res.data + cids[i] = res.CID + rcts[i] = res.Data } return cids, rcts, nil } @@ -312,7 +312,7 @@ func (r *IPLDRetriever) RetrieveReceiptsByBlockNumber(number uint64) ([]string, // RetrieveReceiptByHash returns the cid and rlp bytes for the receipt corresponding to the provided tx hash func (r *IPLDRetriever) RetrieveReceiptByHash(hash common.Hash) (string, []byte, error) { rctResult := new(ipldResult) - return rctResult.cid, rctResult.data, r.db.Get(rctResult, RetrieveReceiptByTxHashPgStr, hash.Hex()) + return rctResult.CID, rctResult.Data, r.db.Get(rctResult, RetrieveReceiptByTxHashPgStr, hash.Hex()) } // RetrieveAccountByAddressAndBlockHash returns the cid and rlp bytes for the account corresponding to the provided address and block hash @@ -323,13 +323,13 @@ func (r *IPLDRetriever) RetrieveAccountByAddressAndBlockHash(address common.Addr return "", nil, err } var i []interface{} - if err := rlp.DecodeBytes(accountResult.data, &i); err != nil { + if err := rlp.DecodeBytes(accountResult.Data, &i); err != nil { return "", nil, fmt.Errorf("error decoding state leaf node rlp: %s", err.Error()) } if len(i) != 2 { return "", nil, fmt.Errorf("eth IPLDRetriever expected state leaf node rlp to decode into two elements") } - return accountResult.cid, i[1].([]byte), nil + return accountResult.CID, i[1].([]byte), nil } // RetrieveAccountByAddressAndBlockNumber returns the cid and rlp bytes for the account corresponding to the provided address and block number @@ -343,9 +343,9 @@ func (r *IPLDRetriever) RetrieveAccountByAddressAndBlockNumber(address common.Ad cids := make([]string, len(accountResults)) accounts := make([][]byte, len(accountResults)) for i, res := range accountResults { - cids[i] = res.cid + cids[i] = res.CID var iface []interface{} - if err := rlp.DecodeBytes(res.data, &iface); err != nil { + if err := rlp.DecodeBytes(res.Data, &iface); err != nil { return nil, nil, fmt.Errorf("error decoding state leaf node rlp: %s", err.Error()) } if len(iface) != 2 { diff --git a/pkg/eth/types.go b/pkg/eth/types.go index 353c15eb..44a2f7e2 100644 --- a/pkg/eth/types.go +++ b/pkg/eth/types.go @@ -20,11 +20,30 @@ import ( "math/big" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/statediff" "github.com/vulcanize/ipld-eth-indexer/pkg/eth" "github.com/vulcanize/ipld-eth-indexer/pkg/ipfs" ) +// RPCTransaction represents a transaction that will serialize to the RPC representation of a transaction +type RPCTransaction struct { + BlockHash *common.Hash `json:"blockHash"` + BlockNumber *hexutil.Big `json:"blockNumber"` + From common.Address `json:"from"` + Gas hexutil.Uint64 `json:"gas"` + GasPrice *hexutil.Big `json:"gasPrice"` + Hash common.Hash `json:"hash"` + Input hexutil.Bytes `json:"input"` + Nonce hexutil.Uint64 `json:"nonce"` + To *common.Address `json:"to"` + TransactionIndex *hexutil.Uint64 `json:"transactionIndex"` + Value *hexutil.Big `json:"value"` + V *hexutil.Big `json:"v"` + R *hexutil.Big `json:"r"` + S *hexutil.Big `json:"s"` +} + // IPLDs is used to package raw IPLD block data fetched from IPFS and returned by the server // Returned by IPLDFetcher and ResponseFilterer type IPLDs struct { diff --git a/pkg/graphql/graphiql.go b/pkg/graphql/graphiql.go new file mode 100644 index 00000000..e57cb2a0 --- /dev/null +++ b/pkg/graphql/graphiql.go @@ -0,0 +1,114 @@ +// VulcanizeDB +// Copyright © 2020 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program 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 Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package graphql + +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.Header().Set("Content-Type", "text/html") + w.Write(graphiql) +} + +var graphiql = []byte(` + + + + + + + + + + + +
Loading...
+ + + +`) diff --git a/pkg/graphql/graphql.go b/pkg/graphql/graphql.go new file mode 100644 index 00000000..a4f4a923 --- /dev/null +++ b/pkg/graphql/graphql.go @@ -0,0 +1,942 @@ +// VulcanizeDB +// Copyright © 2020 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program 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 Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// Package graphql provides a GraphQL interface to Ethereum node data. +package graphql + +import ( + "context" + "errors" + "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/eth/filters" + "github.com/ethereum/go-ethereum/rpc" + + "github.com/vulcanize/ipld-eth-server/pkg/eth" +) + +var ( + errBlockInvariant = errors.New("block objects must be instantiated with at least one of num or hash") +) + +// Account represents an Ethereum account at a particular block. +type Account struct { + backend *eth.Backend + address common.Address + blockNrOrHash rpc.BlockNumberOrHash +} + +// getState fetches the StateDB object for an account. +func (a *Account) getState(ctx context.Context) (*state.StateDB, error) { + state, _, err := a.backend.StateAndHeaderByNumberOrHash(ctx, a.blockNrOrHash) + return state, err +} + +func (a *Account) Address(ctx context.Context) (common.Address, error) { + return a.address, nil +} + +func (a *Account) Balance(ctx context.Context) (hexutil.Big, error) { + state, err := a.getState(ctx) + if err != nil { + return hexutil.Big{}, err + } + return hexutil.Big(*state.GetBalance(a.address)), nil +} + +func (a *Account) TransactionCount(ctx context.Context) (hexutil.Uint64, error) { + state, err := a.getState(ctx) + if err != nil { + return 0, err + } + return hexutil.Uint64(state.GetNonce(a.address)), nil +} + +func (a *Account) Code(ctx context.Context) (hexutil.Bytes, error) { + state, err := a.getState(ctx) + if err != nil { + return hexutil.Bytes{}, err + } + return hexutil.Bytes(state.GetCode(a.address)), nil +} + +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 + } + return state.GetState(a.address, args.Slot), nil +} + +// Log represents an individual log message. All arguments are mandatory. +type Log struct { + backend *eth.Backend + 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{ + backend: l.backend, + address: l.log.Address, + blockNrOrHash: args.NumberOrLatest(), + } +} + +func (l *Log) Index(ctx context.Context) int32 { + return int32(l.log.Index) +} + +func (l *Log) Topics(ctx context.Context) []common.Hash { + return l.log.Topics +} + +func (l *Log) Data(ctx context.Context) hexutil.Bytes { + return hexutil.Bytes(l.log.Data) +} + +// Transaction represents an Ethereum transaction. +// backend and hash are mandatory; all others will be fetched when required. +type Transaction struct { + backend *eth.Backend + 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 { + tx, blockHash, _, index := rawdb.ReadTransaction(t.backend.ChainDb(), t.hash) + if tx != nil { + t.tx = tx + blockNrOrHash := rpc.BlockNumberOrHashWithHash(blockHash, false) + t.block = &Block{ + backend: t.backend, + numberOrHash: &blockNrOrHash, + } + t.index = index + } + } + return t.tx, nil +} + +func (t *Transaction) Hash(ctx context.Context) common.Hash { + return t.hash +} + +func (t *Transaction) InputData(ctx context.Context) (hexutil.Bytes, error) { + tx, err := t.resolve(ctx) + if err != nil || tx == nil { + return hexutil.Bytes{}, err + } + return hexutil.Bytes(tx.Data()), nil +} + +func (t *Transaction) Gas(ctx context.Context) (hexutil.Uint64, error) { + tx, err := t.resolve(ctx) + if err != nil || tx == nil { + return 0, err + } + return hexutil.Uint64(tx.Gas()), nil +} + +func (t *Transaction) GasPrice(ctx context.Context) (hexutil.Big, error) { + tx, err := t.resolve(ctx) + if err != nil || tx == nil { + return hexutil.Big{}, err + } + return hexutil.Big(*tx.GasPrice()), nil +} + +func (t *Transaction) Value(ctx context.Context) (hexutil.Big, error) { + tx, err := t.resolve(ctx) + if err != nil || tx == nil { + return hexutil.Big{}, err + } + return hexutil.Big(*tx.Value()), nil +} + +func (t *Transaction) Nonce(ctx context.Context) (hexutil.Uint64, error) { + tx, err := t.resolve(ctx) + if err != nil || tx == nil { + return 0, err + } + return hexutil.Uint64(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 + } + return &Account{ + backend: t.backend, + address: *to, + blockNrOrHash: args.NumberOrLatest(), + }, 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 + } + var signer types.Signer = types.HomesteadSigner{} + if tx.Protected() { + signer = types.NewEIP155Signer(tx.ChainId()) + } + from, _ := types.Sender(signer, tx) + + return &Account{ + backend: t.backend, + address: from, + blockNrOrHash: args.NumberOrLatest(), + }, 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 +} + +// 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 + } + if t.block == nil { + return nil, nil + } + receipts, err := t.block.resolveReceipts(ctx) + if err != nil { + return nil, err + } + return receipts[t.index], nil +} + +func (t *Transaction) Status(ctx context.Context) (*hexutil.Uint64, error) { + receipt, err := t.getReceipt(ctx) + if err != nil || receipt == nil { + return nil, err + } + ret := hexutil.Uint64(receipt.Status) + return &ret, nil +} + +func (t *Transaction) GasUsed(ctx context.Context) (*hexutil.Uint64, error) { + receipt, err := t.getReceipt(ctx) + if err != nil || receipt == nil { + return nil, err + } + ret := hexutil.Uint64(receipt.GasUsed) + return &ret, nil +} + +func (t *Transaction) CumulativeGasUsed(ctx context.Context) (*hexutil.Uint64, error) { + receipt, err := t.getReceipt(ctx) + if err != nil || receipt == nil { + return nil, err + } + ret := hexutil.Uint64(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{ + backend: t.backend, + address: receipt.ContractAddress, + blockNrOrHash: args.NumberOrLatest(), + }, 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{ + backend: t.backend, + transaction: t, + log: log, + }) + } + return &ret, nil +} + +func (t *Transaction) R(ctx context.Context) (hexutil.Big, error) { + tx, err := t.resolve(ctx) + if err != nil || tx == nil { + return hexutil.Big{}, err + } + _, r, _ := tx.RawSignatureValues() + return hexutil.Big(*r), nil +} + +func (t *Transaction) S(ctx context.Context) (hexutil.Big, error) { + tx, err := t.resolve(ctx) + if err != nil || tx == nil { + return hexutil.Big{}, err + } + _, _, s := tx.RawSignatureValues() + return hexutil.Big(*s), nil +} + +func (t *Transaction) V(ctx context.Context) (hexutil.Big, error) { + tx, err := t.resolve(ctx) + if err != nil || tx == nil { + return hexutil.Big{}, err + } + v, _, _ := tx.RawSignatureValues() + return hexutil.Big(*v), nil +} + +type BlockType int + +// Block represents an Ethereum block. +// backend, and numberOrHash are mandatory. All other fields are lazily fetched +// when required. +type Block struct { + backend *eth.Backend + numberOrHash *rpc.BlockNumberOrHash + hash common.Hash + header *types.Header + block *types.Block + receipts []*types.Receipt +} + +// 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 { + return b.block, nil + } + if b.numberOrHash == nil { + latest := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber) + b.numberOrHash = &latest + } + var err error + b.block, err = b.backend.BlockByNumberOrHash(ctx, *b.numberOrHash) + if b.block != nil && b.header == nil { + b.header = b.block.Header() + if hash, ok := b.numberOrHash.Hash(); ok { + b.hash = hash + } + } + 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.numberOrHash == nil && b.hash == (common.Hash{}) { + return nil, errBlockInvariant + } + var err error + if b.header == nil { + if b.hash != (common.Hash{}) { + b.header, err = b.backend.HeaderByHash(ctx, b.hash) + } else { + b.header, err = b.backend.HeaderByNumberOrHash(ctx, *b.numberOrHash) + } + } + return b.header, err +} + +// 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 { + hash := b.hash + if hash == (common.Hash{}) { + header, err := b.resolveHeader(ctx) + if err != nil { + return nil, err + } + hash = header.Hash() + } + receipts, err := b.backend.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) (hexutil.Uint64, error) { + header, err := b.resolveHeader(ctx) + if err != nil { + return 0, err + } + + return hexutil.Uint64(header.Number.Uint64()), nil +} + +func (b *Block) Hash(ctx context.Context) (common.Hash, error) { + if b.hash == (common.Hash{}) { + header, err := b.resolveHeader(ctx) + if err != nil { + return common.Hash{}, err + } + b.hash = header.Hash() + } + return b.hash, nil +} + +func (b *Block) GasLimit(ctx context.Context) (hexutil.Uint64, error) { + header, err := b.resolveHeader(ctx) + if err != nil { + return 0, err + } + return hexutil.Uint64(header.GasLimit), nil +} + +func (b *Block) GasUsed(ctx context.Context) (hexutil.Uint64, error) { + header, err := b.resolveHeader(ctx) + if err != nil { + return 0, err + } + return hexutil.Uint64(header.GasUsed), nil +} + +func (b *Block) Parent(ctx context.Context) (*Block, error) { + // If the block header hasn't been fetched, and we'll need it, fetch it. + if b.numberOrHash == nil && b.header == nil { + if _, err := b.resolveHeader(ctx); err != nil { + return nil, err + } + } + if b.header != nil && b.header.Number.Uint64() > 0 { + num := rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(b.header.Number.Uint64() - 1)) + return &Block{ + backend: b.backend, + numberOrHash: &num, + hash: b.header.ParentHash, + }, nil + } + return nil, nil +} + +func (b *Block) Difficulty(ctx context.Context) (hexutil.Big, error) { + header, err := b.resolveHeader(ctx) + if err != nil { + return hexutil.Big{}, err + } + return hexutil.Big(*header.Difficulty), nil +} + +func (b *Block) Timestamp(ctx context.Context) (hexutil.Uint64, error) { + header, err := b.resolveHeader(ctx) + if err != nil { + return 0, err + } + return hexutil.Uint64(header.Time), nil +} + +func (b *Block) Nonce(ctx context.Context) (hexutil.Bytes, error) { + header, err := b.resolveHeader(ctx) + if err != nil { + return hexutil.Bytes{}, err + } + return hexutil.Bytes(header.Nonce[:]), nil +} + +func (b *Block) MixHash(ctx context.Context) (common.Hash, error) { + header, err := b.resolveHeader(ctx) + if err != nil { + return common.Hash{}, err + } + return header.MixDigest, nil +} + +func (b *Block) TransactionsRoot(ctx context.Context) (common.Hash, error) { + header, err := b.resolveHeader(ctx) + if err != nil { + return common.Hash{}, err + } + return header.TxHash, nil +} + +func (b *Block) StateRoot(ctx context.Context) (common.Hash, error) { + header, err := b.resolveHeader(ctx) + if err != nil { + return common.Hash{}, err + } + return header.Root, nil +} + +func (b *Block) ReceiptsRoot(ctx context.Context) (common.Hash, error) { + header, err := b.resolveHeader(ctx) + if err != nil { + return common.Hash{}, err + } + return header.ReceiptHash, nil +} + +func (b *Block) OmmerHash(ctx context.Context) (common.Hash, error) { + header, err := b.resolveHeader(ctx) + if err != nil { + return common.Hash{}, err + } + return header.UncleHash, nil +} + +func (b *Block) OmmerCount(ctx context.Context) (*int32, error) { + block, err := b.resolve(ctx) + if err != nil || block == nil { + return nil, err + } + count := int32(len(block.Uncles())) + return &count, err +} + +func (b *Block) Ommers(ctx context.Context) (*[]*Block, error) { + block, err := b.resolve(ctx) + if err != nil || block == nil { + return nil, err + } + ret := make([]*Block, 0, len(block.Uncles())) + for _, uncle := range block.Uncles() { + blockNumberOrHash := rpc.BlockNumberOrHashWithHash(uncle.Hash(), false) + ret = append(ret, &Block{ + backend: b.backend, + numberOrHash: &blockNumberOrHash, + header: uncle, + }) + } + return &ret, nil +} + +func (b *Block) ExtraData(ctx context.Context) (hexutil.Bytes, error) { + header, err := b.resolveHeader(ctx) + if err != nil { + return hexutil.Bytes{}, err + } + return hexutil.Bytes(header.Extra), nil +} + +func (b *Block) LogsBloom(ctx context.Context) (hexutil.Bytes, error) { + header, err := b.resolveHeader(ctx) + if err != nil { + return hexutil.Bytes{}, err + } + return hexutil.Bytes(header.Bloom.Bytes()), nil +} + +func (b *Block) TotalDifficulty(ctx context.Context) (hexutil.Big, error) { + h := b.hash + if h == (common.Hash{}) { + header, err := b.resolveHeader(ctx) + if err != nil { + return hexutil.Big{}, err + } + h = header.Hash() + } + td, err := b.backend.GetTd(h) + if err != nil { + return hexutil.Big{}, err + } + return hexutil.Big(*td), nil +} + +// BlockNumberArgs encapsulates arguments to accessors that specify a block number. +type BlockNumberArgs struct { + // TODO: Ideally we could use input unions to allow the query to specify the + // block parameter by hash, block number, or tag but input unions aren't part of the + // standard GraphQL schema SDL yet, see: https://github.com/graphql/graphql-spec/issues/488 + Block *hexutil.Uint64 +} + +// NumberOr returns the provided block number argument, or the "current" block number or hash if none +// was provided. +func (a BlockNumberArgs) NumberOr(current rpc.BlockNumberOrHash) rpc.BlockNumberOrHash { + if a.Block != nil { + blockNr := rpc.BlockNumber(*a.Block) + return rpc.BlockNumberOrHashWithNumber(blockNr) + } + return current +} + +// NumberOrLatest returns the provided block number argument, or the "latest" block number if none +// was provided. +func (a BlockNumberArgs) NumberOrLatest() rpc.BlockNumberOrHash { + return a.NumberOr(rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber)) +} + +func (b *Block) Miner(ctx context.Context, args BlockNumberArgs) (*Account, error) { + header, err := b.resolveHeader(ctx) + if err != nil { + return nil, err + } + return &Account{ + backend: b.backend, + address: header.Coinbase, + blockNrOrHash: args.NumberOrLatest(), + }, nil +} + +func (b *Block) TransactionCount(ctx context.Context) (*int32, error) { + block, err := b.resolve(ctx) + if err != nil || block == nil { + return nil, err + } + count := int32(len(block.Transactions())) + return &count, err +} + +func (b *Block) Transactions(ctx context.Context) (*[]*Transaction, error) { + block, err := b.resolve(ctx) + if err != nil || block == nil { + return nil, err + } + ret := make([]*Transaction, 0, len(block.Transactions())) + for i, tx := range block.Transactions() { + ret = append(ret, &Transaction{ + backend: b.backend, + hash: tx.Hash(), + tx: tx, + block: b, + index: uint64(i), + }) + } + return &ret, nil +} + +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 + } + txs := block.Transactions() + if args.Index < 0 || int(args.Index) >= len(txs) { + return nil, nil + } + tx := txs[args.Index] + return &Transaction{ + backend: b.backend, + hash: tx.Hash(), + tx: tx, + block: b, + index: uint64(args.Index), + }, nil +} + +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 + } + uncles := block.Uncles() + if args.Index < 0 || int(args.Index) >= len(uncles) { + return nil, nil + } + uncle := uncles[args.Index] + blockNumberOrHash := rpc.BlockNumberOrHashWithHash(uncle.Hash(), false) + return &Block{ + backend: b.backend, + numberOrHash: &blockNumberOrHash, + header: uncle, + }, 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 + + // 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 +} + +// runFilter accepts a filter and executes it, returning all its results as +// `Log` objects. +func runFilter(ctx context.Context, be *eth.Backend, 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{ + backend: be, + transaction: &Transaction{backend: be, hash: log.TxHash}, + log: log, + }) + } + return ret, nil +} + +func (b *Block) Logs(ctx context.Context, args struct{ Filter BlockFilterCriteria }) ([]*Log, error) { + 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{}) { + header, err := b.resolveHeader(ctx) + if err != nil { + return nil, err + } + hash = header.Hash() + } + // Construct the range filter + filter := filters.NewBlockFilter(b.backend, hash, addresses, topics) + + // Run the filter and return all the logs + return runFilter(ctx, b.backend, filter) +} + +func (b *Block) Account(ctx context.Context, args struct { + Address common.Address +}) (*Account, error) { + if b.numberOrHash == nil { + _, err := b.resolveHeader(ctx) + if err != nil { + return nil, err + } + } + return &Account{ + backend: b.backend, + address: args.Address, + blockNrOrHash: *b.numberOrHash, + }, nil +} + +// CallData encapsulates arguments to `call` or `estimateGas`. +// All arguments are optional. +type CallData struct { + From *common.Address // The Ethereum address the call is from. + To *common.Address // The Ethereum address the call is to. + Gas *hexutil.Uint64 // The amount of gas provided for the call. + GasPrice *hexutil.Big // The price of each unit of gas, in wei. + Value *hexutil.Big // The value sent along with the call. + Data *hexutil.Bytes // Any data sent with the call. +} + +// CallResult encapsulates the result of an invocation of the `call` accessor. +type CallResult struct { + data hexutil.Bytes // The return data from the call + gasUsed hexutil.Uint64 // The amount of gas used + status hexutil.Uint64 // The return status of the call - 0 for failure or 1 for success. +} + +func (c *CallResult) Data() hexutil.Bytes { + return c.data +} + +func (c *CallResult) GasUsed() hexutil.Uint64 { + return c.gasUsed +} + +func (c *CallResult) Status() hexutil.Uint64 { + return c.status +} + +func (b *Block) Call(ctx context.Context, args struct { + Data eth.CallArgs +}) (*CallResult, error) { + if b.numberOrHash == nil { + _, err := b.resolve(ctx) + if err != nil { + return nil, err + } + } + result, gas, failed, err := eth.DoCall(ctx, b.backend, args.Data, *b.numberOrHash, nil, 5*time.Second, b.backend.RPCGasCap()) + status := hexutil.Uint64(1) + if failed { + status = 0 + } + return &CallResult{ + data: hexutil.Bytes(result), + gasUsed: hexutil.Uint64(gas), + status: status, + }, err +} + +// Resolver is the top-level object in the GraphQL hierarchy. +type Resolver struct { + backend *eth.Backend +} + +func (r *Resolver) Block(ctx context.Context, args struct { + Number *hexutil.Uint64 + Hash *common.Hash +}) (*Block, error) { + var block *Block + if args.Number != nil { + number := rpc.BlockNumber(uint64(*args.Number)) + numberOrHash := rpc.BlockNumberOrHashWithNumber(number) + block = &Block{ + backend: r.backend, + numberOrHash: &numberOrHash, + } + } else if args.Hash != nil { + numberOrHash := rpc.BlockNumberOrHashWithHash(*args.Hash, false) + block = &Block{ + backend: r.backend, + numberOrHash: &numberOrHash, + } + } else { + numberOrHash := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber) + block = &Block{ + backend: r.backend, + numberOrHash: &numberOrHash, + } + } + // Resolve the header, return nil if it doesn't exist. + // Note we don't resolve block directly here since it will require an + // additional network request for light client. + h, err := block.resolveHeader(ctx) + if err != nil { + return nil, err + } else if h == nil { + return nil, nil + } + return block, nil +} + +func (r *Resolver) Blocks(ctx context.Context, args struct { + From hexutil.Uint64 + To *hexutil.Uint64 +}) ([]*Block, error) { + from := rpc.BlockNumber(args.From) + + var to rpc.BlockNumber + if args.To != nil { + to = rpc.BlockNumber(*args.To) + } else { + to = rpc.BlockNumber(r.backend.CurrentBlock().Number().Int64()) + } + if to < from { + return []*Block{}, nil + } + ret := make([]*Block, 0, to-from+1) + for i := from; i <= to; i++ { + numberOrHash := rpc.BlockNumberOrHashWithNumber(i) + ret = append(ret, &Block{ + backend: r.backend, + numberOrHash: &numberOrHash, + }) + } + return ret, nil +} + +func (r *Resolver) Transaction(ctx context.Context, args struct{ Hash common.Hash }) (*Transaction, error) { + tx := &Transaction{ + backend: r.backend, + hash: args.Hash, + } + // 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 +} + +// FilterCriteria encapsulates the arguments to `logs` on the root resolver object. +type FilterCriteria struct { + FromBlock *hexutil.Uint64 // beginning of the queried range, nil means genesis block + ToBlock *hexutil.Uint64 // end of the range, nil means latest block + 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) { + // 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(r.backend), begin, end, addresses, topics) + return runFilter(ctx, r.backend, filter) +} diff --git a/pkg/graphql/graphql_suite_test.go b/pkg/graphql/graphql_suite_test.go new file mode 100644 index 00000000..cdfd5329 --- /dev/null +++ b/pkg/graphql/graphql_suite_test.go @@ -0,0 +1,35 @@ +// VulcanizeDB +// Copyright © 2020 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program 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 Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package graphql_test + +import ( + "io/ioutil" + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/sirupsen/logrus" +) + +func TestGraphQL(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "graphql test suite") +} + +var _ = BeforeSuite(func() { + logrus.SetOutput(ioutil.Discard) +}) diff --git a/pkg/graphql/graphql_test.go b/pkg/graphql/graphql_test.go new file mode 100644 index 00000000..7315f530 --- /dev/null +++ b/pkg/graphql/graphql_test.go @@ -0,0 +1,31 @@ +// VulcanizeDB +// Copyright © 2020 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program 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 Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package graphql_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/vulcanize/ipld-eth-server/pkg/graphql" +) + +var _ = Describe("GraphQL", func() { + It("Builds the schema and creates a new handler", func() { + _, err := graphql.NewHandler(nil) + Expect(err).ToNot(HaveOccurred()) + }) +}) diff --git a/pkg/graphql/schema.go b/pkg/graphql/schema.go new file mode 100644 index 00000000..8057eac9 --- /dev/null +++ b/pkg/graphql/schema.go @@ -0,0 +1,274 @@ +// VulcanizeDB +// Copyright © 2020 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program 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 Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package graphql + +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. + # An empty byte string is represented as '0x'. Byte strings must have an even number of hexadecimal nybbles. + scalar Bytes + # BigInt is a large integer. Input is accepted as either a JSON number or as a string. + # Strings may be either decimal or 0x-prefixed hexadecimal. Output values are all + # 0x-prefixed hexadecimal. + scalar BigInt + # Long is a 64 bit unsigned integer. + scalar Long + + schema { + query: Query + } + + # 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 been 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!] + r: BigInt! + s: BigInt! + v: BigInt! + } + + # 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: Long! + # 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!]! + # Account fetches an Ethereum account at the current block's state. + account(address: Address!): Account! + # Call executes a local call operation at the current block's state. + call(data: CallData!): CallResult + } + + # 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 operation. + 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!]!] + } + + type Query { + # 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 + # Logs returns log entries matching the provided filter. + logs(filter: FilterCriteria!): [Log!]! + } +` diff --git a/pkg/graphql/service.go b/pkg/graphql/service.go new file mode 100644 index 00000000..04c69af4 --- /dev/null +++ b/pkg/graphql/service.go @@ -0,0 +1,104 @@ +// VulcanizeDB +// Copyright © 2020 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program 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 Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package graphql + +import ( + "fmt" + "net" + "net/http" + + "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" + "github.com/sirupsen/logrus" + + "github.com/vulcanize/ipld-eth-server/pkg/eth" +) + +// 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.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 *eth.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) + logrus.Debugf("graphQL endpoint opened for url %s", 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 *eth.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 + logrus.Debugf("graphQL endpoint closed for url %s", fmt.Sprintf("http://%s", s.endpoint)) + } + return nil +} diff --git a/pkg/serve/service.go b/pkg/serve/service.go index 6c005631..790a7f2b 100644 --- a/pkg/serve/service.go +++ b/pkg/serve/service.go @@ -52,6 +52,8 @@ type Server interface { Subscribe(id rpc.ID, sub chan<- SubscriptionPayload, quitChan chan<- bool, params eth.SubscriptionSettings) // Method to unsubscribe from the service Unsubscribe(id rpc.ID) + // Backend exposes the server's backend + Backend() *eth.Backend } // Service is the underlying struct for the watcher @@ -74,29 +76,30 @@ type Service struct { db *postgres.DB // wg for syncing serve processes serveWg *sync.WaitGroup - // config for backend - config *eth.Config // rpc client for forwarding cache misses client *rpc.Client + // backend for the server + backend *eth.Backend } // NewServer creates a new Server using an underlying Service struct func NewServer(settings *Config) (Server, error) { - sn := new(Service) - sn.Retriever = eth.NewCIDRetriever(settings.DB) - sn.IPLDFetcher = eth.NewIPLDFetcher(settings.DB) - sn.Filterer = eth.NewResponseFilterer() - sn.db = settings.DB - sn.QuitChan = make(chan bool) - sn.Subscriptions = make(map[common.Hash]map[rpc.ID]Subscription) - sn.SubscriptionTypes = make(map[common.Hash]eth.SubscriptionSettings) - sn.config = ð.Config{ + sap := new(Service) + sap.Retriever = eth.NewCIDRetriever(settings.DB) + sap.IPLDFetcher = eth.NewIPLDFetcher(settings.DB) + sap.Filterer = eth.NewResponseFilterer() + sap.db = settings.DB + sap.QuitChan = make(chan bool) + sap.Subscriptions = make(map[common.Hash]map[rpc.ID]Subscription) + sap.SubscriptionTypes = make(map[common.Hash]eth.SubscriptionSettings) + var err error + sap.backend, err = eth.NewEthBackend(sap.db, ð.Config{ ChainConfig: settings.ChainConfig, VmConfig: vm.Config{}, DefaultSender: settings.DefaultSender, RPCGasCap: settings.RPCGasCap, - } - return sn, nil + }) + return sap, err } // Protocols exports the services p2p protocols, this service has none @@ -133,15 +136,10 @@ func (sap *Service) APIs() []rpc.API { Public: true, }, } - backend, err := eth.NewEthBackend(sap.db, sap.config) - if err != nil { - log.Error(err) - return nil - } return append(apis, rpc.API{ Namespace: eth.APIName, Version: eth.APIVersion, - Service: eth.NewPublicEthAPI(backend, sap.client), + Service: eth.NewPublicEthAPI(sap.backend, sap.client), Public: true, }) } @@ -358,6 +356,11 @@ func (sap *Service) Stop() error { return nil } +// Backend exposes the server's backend +func (sap *Service) Backend() *eth.Backend { + return sap.backend +} + // close is used to close all listening subscriptions // close needs to be called with subscription access locked func (sap *Service) close() {