diff --git a/CHANGELOG.md b/CHANGELOG.md index 53f55c2c5..dcd70e29c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,14 @@ Ref: https://keepachangelog.com/en/1.0.0/ * (crypto) [\#559](https://github.com/cosmos/ethermint/pull/559) Refactored crypto package in preparation for the SDK's Stargate release: * `crypto.PubKeySecp256k1` and `crypto.PrivKeySecp256k1` are now `ethsecp256k1.PubKey` and `ethsecp256k1.PrivKey`, respectively * Moved SDK `SigningAlgo` implementation for Ethermint's Secp256k1 key to `crypto/hd` package. +* (rpc) [\#588](https://github.com/cosmos/ethermint/pull/588) The `rpc` package has been refactored to account for the separation of each +corresponding Ethereum API namespace: + * `rpc/namespaces/eth`: `eth` namespace. Exposes the `PublicEthereumAPI` and the `PublicFilterAPI`. + * `rpc/namespaces/personal`: `personal` namespace. Exposes the `PrivateAccountAPI`. + * `rpc/namespaces/net`: `net` namespace. Exposes the `PublicNetAPI`. + * `rpc/namespaces/web3`: `web3` namespace. Exposes the `PublicWeb3API`. + +* (evm) [\#588](https://github.com/cosmos/ethermint/pull/588) The EVM transaction CLI has been removed in favor of the JSON-RPC. ### Bug Fixes diff --git a/cmd/ethermintcli/main.go b/cmd/ethermintcli/main.go index cded7573a..8a92cee4c 100644 --- a/cmd/ethermintcli/main.go +++ b/cmd/ethermintcli/main.go @@ -67,7 +67,7 @@ func main() { queryCmd(cdc), txCmd(cdc), client.ValidateChainID( - rpc.EmintServeCmd(cdc), + rpc.ServeCmd(cdc), ), flags.LineBreak, client.KeyCommands(), diff --git a/rpc/apis.go b/rpc/apis.go index 378844280..b38dcaa43 100644 --- a/rpc/apis.go +++ b/rpc/apis.go @@ -1,5 +1,3 @@ -// Package rpc contains RPC handler methods and utilities to start -// Ethermint's Web3-compatibly JSON-RPC server. package rpc import ( @@ -8,6 +6,13 @@ import ( "github.com/cosmos/cosmos-sdk/client/context" "github.com/cosmos/ethermint/crypto/ethsecp256k1" + "github.com/cosmos/ethermint/rpc/backend" + "github.com/cosmos/ethermint/rpc/namespaces/eth" + "github.com/cosmos/ethermint/rpc/namespaces/eth/filters" + "github.com/cosmos/ethermint/rpc/namespaces/net" + "github.com/cosmos/ethermint/rpc/namespaces/personal" + "github.com/cosmos/ethermint/rpc/namespaces/web3" + rpctypes "github.com/cosmos/ethermint/rpc/types" ) // RPC namespaces and API version @@ -20,17 +25,17 @@ const ( apiVersion = "1.0" ) -// GetRPCAPIs returns the list of all APIs -func GetRPCAPIs(cliCtx context.CLIContext, keys []ethsecp256k1.PrivKey) []rpc.API { - nonceLock := new(AddrLocker) - backend := NewEthermintBackend(cliCtx) - ethAPI := NewPublicEthAPI(cliCtx, backend, nonceLock, keys) +// GetAPIs returns the list of all APIs from the Ethereum namespaces +func GetAPIs(clientCtx context.CLIContext, keys ...ethsecp256k1.PrivKey) []rpc.API { + nonceLock := new(rpctypes.AddrLocker) + backend := backend.New(clientCtx) + ethAPI := eth.NewAPI(clientCtx, backend, nonceLock, keys...) return []rpc.API{ { Namespace: Web3Namespace, Version: apiVersion, - Service: NewPublicWeb3API(), + Service: web3.NewAPI(), Public: true, }, { @@ -40,21 +45,21 @@ func GetRPCAPIs(cliCtx context.CLIContext, keys []ethsecp256k1.PrivKey) []rpc.AP Public: true, }, { - Namespace: PersonalNamespace, + Namespace: EthNamespace, Version: apiVersion, - Service: NewPersonalEthAPI(ethAPI), - Public: false, + Service: filters.NewAPI(clientCtx, backend), + Public: true, }, { - Namespace: EthNamespace, + Namespace: PersonalNamespace, Version: apiVersion, - Service: NewPublicFilterAPI(cliCtx, backend), - Public: true, + Service: personal.NewAPI(ethAPI), + Public: false, }, { Namespace: NetNamespace, Version: apiVersion, - Service: NewPublicNetAPI(cliCtx), + Service: net.NewAPI(clientCtx), Public: true, }, } diff --git a/rpc/args/send_tx.go b/rpc/args/send_tx.go deleted file mode 100644 index 7515dbb56..000000000 --- a/rpc/args/send_tx.go +++ /dev/null @@ -1,22 +0,0 @@ -package args - -import ( - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" -) - -// SendTxArgs represents the arguments to submit a new transaction into the transaction pool. -// Duplicate struct definition since geth struct is in internal package -// Ref: https://github.com/ethereum/go-ethereum/blob/release/1.9/internal/ethapi/api.go#L1346 -type SendTxArgs struct { - From common.Address `json:"from"` - To *common.Address `json:"to"` - Gas *hexutil.Uint64 `json:"gas"` - GasPrice *hexutil.Big `json:"gasPrice"` - Value *hexutil.Big `json:"value"` - Nonce *hexutil.Uint64 `json:"nonce"` - // We accept "data" and "input" for backwards-compatibility reasons. "input" is the - // newer name and should be preferred by clients. - Data *hexutil.Bytes `json:"data"` - Input *hexutil.Bytes `json:"input"` -} diff --git a/rpc/backend.go b/rpc/backend.go deleted file mode 100644 index 1c5800a1e..000000000 --- a/rpc/backend.go +++ /dev/null @@ -1,328 +0,0 @@ -package rpc - -import ( - "fmt" - "math/big" - "os" - "strconv" - - "github.com/tendermint/tendermint/libs/log" - tmtypes "github.com/tendermint/tendermint/types" - - evmtypes "github.com/cosmos/ethermint/x/evm/types" - - "github.com/cosmos/cosmos-sdk/client/context" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - ethtypes "github.com/ethereum/go-ethereum/core/types" -) - -// Backend implements the functionality needed to filter changes. -// Implemented by EthermintBackend. -type Backend interface { - // Used by block filter; also used for polling - BlockNumber() (hexutil.Uint64, error) - HeaderByNumber(blockNum BlockNumber) (*ethtypes.Header, error) - HeaderByHash(blockHash common.Hash) (*ethtypes.Header, error) - GetBlockByNumber(blockNum BlockNumber, fullTx bool) (map[string]interface{}, error) - GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) - getEthBlockByNumber(height int64, fullTx bool) (map[string]interface{}, error) - getGasLimit() (int64, error) - // returns the logs of a given block - GetLogs(blockHash common.Hash) ([][]*ethtypes.Log, error) - - // Used by pending transaction filter - PendingTransactions() ([]*Transaction, error) - - // Used by log filter - GetTransactionLogs(txHash common.Hash) ([]*ethtypes.Log, error) - BloomStatus() (uint64, uint64) -} - -var _ Backend = (*EthermintBackend)(nil) - -// EthermintBackend implements the Backend interface -type EthermintBackend struct { - cliCtx context.CLIContext - logger log.Logger - gasLimit int64 -} - -// NewEthermintBackend creates a new EthermintBackend instance -func NewEthermintBackend(cliCtx context.CLIContext) *EthermintBackend { - return &EthermintBackend{ - cliCtx: cliCtx, - logger: log.NewTMLogger(log.NewSyncWriter(os.Stdout)).With("module", "json-rpc"), - gasLimit: int64(^uint32(0)), - } -} - -// BlockNumber returns the current block number. -func (e *EthermintBackend) BlockNumber() (hexutil.Uint64, error) { - res, height, err := e.cliCtx.QueryWithData(fmt.Sprintf("custom/%s/blockNumber", evmtypes.ModuleName), nil) - if err != nil { - return hexutil.Uint64(0), err - } - - var out evmtypes.QueryResBlockNumber - e.cliCtx.Codec.MustUnmarshalJSON(res, &out) - - e.cliCtx.WithHeight(height) - return hexutil.Uint64(out.Number), nil -} - -// GetBlockByNumber returns the block identified by number. -func (e *EthermintBackend) GetBlockByNumber(blockNum BlockNumber, fullTx bool) (map[string]interface{}, error) { - value := blockNum.Int64() - return e.getEthBlockByNumber(value, fullTx) -} - -// GetBlockByHash returns the block identified by hash. -func (e *EthermintBackend) GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) { - res, height, err := e.cliCtx.Query(fmt.Sprintf("custom/%s/%s/%s", evmtypes.ModuleName, evmtypes.QueryHashToHeight, hash.Hex())) - if err != nil { - return nil, err - } - - var out evmtypes.QueryResBlockNumber - if err := e.cliCtx.Codec.UnmarshalJSON(res, &out); err != nil { - return nil, err - } - - e.cliCtx = e.cliCtx.WithHeight(height) - return e.getEthBlockByNumber(out.Number, fullTx) -} - -// HeaderByNumber returns the block header identified by height. -func (e *EthermintBackend) HeaderByNumber(blockNum BlockNumber) (*ethtypes.Header, error) { - return e.getBlockHeader(blockNum.Int64()) -} - -// HeaderByHash returns the block header identified by hash. -func (e *EthermintBackend) HeaderByHash(blockHash common.Hash) (*ethtypes.Header, error) { - res, height, err := e.cliCtx.Query(fmt.Sprintf("custom/%s/%s/%s", evmtypes.ModuleName, evmtypes.QueryHashToHeight, blockHash.Hex())) - if err != nil { - return nil, err - } - var out evmtypes.QueryResBlockNumber - if err := e.cliCtx.Codec.UnmarshalJSON(res, &out); err != nil { - return nil, err - } - - e.cliCtx = e.cliCtx.WithHeight(height) - return e.getBlockHeader(out.Number) -} - -func (e *EthermintBackend) getBlockHeader(height int64) (*ethtypes.Header, error) { - if height <= 0 { - // get latest block height - num, err := e.BlockNumber() - if err != nil { - return nil, err - } - - height = int64(num) - } - - block, err := e.cliCtx.Client.Block(&height) - if err != nil { - return nil, err - } - - res, _, err := e.cliCtx.Query(fmt.Sprintf("custom/%s/%s/%s", evmtypes.ModuleName, evmtypes.QueryBloom, strconv.FormatInt(height, 10))) - if err != nil { - return nil, err - } - - var bloomRes evmtypes.QueryBloomFilter - e.cliCtx.Codec.MustUnmarshalJSON(res, &bloomRes) - - ethHeader := EthHeaderFromTendermint(block.Block.Header) - ethHeader.Bloom = bloomRes.Bloom - - return ethHeader, nil -} - -func (e *EthermintBackend) getEthBlockByNumber(height int64, fullTx bool) (map[string]interface{}, error) { - // Remove this check when 0 query is fixed ref: (https://github.com/tendermint/tendermint/issues/4014) - var blkNumPtr *int64 - if height != 0 { - blkNumPtr = &height - } - - block, err := e.cliCtx.Client.Block(blkNumPtr) - if err != nil { - return nil, err - } - header := block.Block.Header - - gasLimit, err := e.getGasLimit() - if err != nil { - return nil, err - } - - var ( - gasUsed *big.Int - transactions []common.Hash - ) - - if fullTx { - // Populate full transaction data - transactions, gasUsed, err = convertTransactionsToRPC( - e.cliCtx, block.Block.Txs, common.BytesToHash(header.Hash()), uint64(header.Height), - ) - if err != nil { - return nil, err - } - } else { - // TODO: Gas used not saved and cannot be calculated by hashes - // Return slice of transaction hashes - transactions = make([]common.Hash, len(block.Block.Txs)) - for i, tx := range block.Block.Txs { - transactions[i] = common.BytesToHash(tx.Hash()) - } - } - - res, _, err := e.cliCtx.Query(fmt.Sprintf("custom/%s/%s/%s", evmtypes.ModuleName, evmtypes.QueryBloom, strconv.FormatInt(block.Block.Height, 10))) - if err != nil { - return nil, err - } - - var out evmtypes.QueryBloomFilter - e.cliCtx.Codec.MustUnmarshalJSON(res, &out) - return formatBlock(header, block.Block.Size(), gasLimit, gasUsed, transactions, out.Bloom), nil -} - -// getGasLimit returns the gas limit per block set in genesis -func (e *EthermintBackend) getGasLimit() (int64, error) { - // Retrieve from gasLimit variable cache - if e.gasLimit != -1 { - return e.gasLimit, nil - } - - // Query genesis block if hasn't been retrieved yet - genesis, err := e.cliCtx.Client.Genesis() - if err != nil { - return 0, err - } - - // Save value to gasLimit cached value - gasLimit := genesis.Genesis.ConsensusParams.Block.MaxGas - if gasLimit == -1 { - // Sets gas limit to max uint32 to not error with javascript dev tooling - // This -1 value indicating no block gas limit is set to max uint64 with geth hexutils - // which errors certain javascript dev tooling which only supports up to 53 bits - gasLimit = int64(^uint32(0)) - } - e.gasLimit = gasLimit - return gasLimit, nil -} - -// GetTransactionLogs returns the logs given a transaction hash. -// It returns an error if there's an encoding error. -// If no logs are found for the tx hash, the error is nil. -func (e *EthermintBackend) GetTransactionLogs(txHash common.Hash) ([]*ethtypes.Log, error) { - ctx := e.cliCtx - - res, height, err := ctx.QueryWithData(fmt.Sprintf("custom/%s/%s/%s", evmtypes.ModuleName, evmtypes.QueryTransactionLogs, txHash.Hex()), nil) - if err != nil { - return nil, err - } - - out := new(evmtypes.QueryETHLogs) - if err := e.cliCtx.Codec.UnmarshalJSON(res, &out); err != nil { - return nil, err - } - - e.cliCtx = e.cliCtx.WithHeight(height) - return out.Logs, nil -} - -// PendingTransactions returns the transactions that are in the transaction pool -// and have a from address that is one of the accounts this node manages. -func (e *EthermintBackend) PendingTransactions() ([]*Transaction, error) { - pendingTxs, err := e.cliCtx.Client.UnconfirmedTxs(100) - if err != nil { - return nil, err - } - - transactions := make([]*Transaction, pendingTxs.Count) - for _, tx := range pendingTxs.Txs { - ethTx, err := bytesToEthTx(e.cliCtx, tx) - if err != nil { - return nil, err - } - - // * Should check signer and reference against accounts the node manages in future - rpcTx, err := newRPCTransaction(*ethTx, common.BytesToHash(tx.Hash()), common.Hash{}, nil, 0) - if err != nil { - return nil, err - } - - transactions = append(transactions, rpcTx) - } - - return transactions, nil -} - -// GetLogs returns all the logs from all the ethreum transactions in a block. -func (e *EthermintBackend) GetLogs(blockHash common.Hash) ([][]*ethtypes.Log, error) { - res, _, err := e.cliCtx.Query(fmt.Sprintf("custom/%s/%s/%s", evmtypes.ModuleName, evmtypes.QueryHashToHeight, blockHash.Hex())) - if err != nil { - return nil, err - } - - var out evmtypes.QueryResBlockNumber - if err := e.cliCtx.Codec.UnmarshalJSON(res, &out); err != nil { - return nil, err - } - - block, err := e.cliCtx.Client.Block(&out.Number) - if err != nil { - return nil, err - } - - var blockLogs = [][]*ethtypes.Log{} - for _, tx := range block.Block.Txs { - // NOTE: we query the state in case the tx result logs are not persisted after an upgrade. - res, _, err := e.cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s/%s", evmtypes.ModuleName, evmtypes.QueryTransactionLogs, common.BytesToHash(tx.Hash()).Hex()), nil) - if err != nil { - continue - } - - out := new(evmtypes.QueryETHLogs) - if err := e.cliCtx.Codec.UnmarshalJSON(res, &out); err != nil { - return nil, err - } - - blockLogs = append(blockLogs, out.Logs) - } - - return blockLogs, nil -} - -// BloomStatus returns the BloomBitsBlocks and the number of processed sections maintained -// by the chain indexer. -func (e *EthermintBackend) BloomStatus() (uint64, uint64) { - return 4096, 0 -} - -// EthHeaderFromTendermint is an util function that returns an Ethereum Header -// from a tendermint Header. -func EthHeaderFromTendermint(header tmtypes.Header) *ethtypes.Header { - return ðtypes.Header{ - ParentHash: common.BytesToHash(header.LastBlockID.Hash.Bytes()), - UncleHash: common.Hash{}, - Coinbase: common.Address{}, - Root: common.BytesToHash(header.AppHash), - TxHash: common.BytesToHash(header.DataHash), - ReceiptHash: common.Hash{}, - Difficulty: nil, - Number: big.NewInt(header.Height), - Time: uint64(header.Time.Unix()), - Extra: nil, - MixDigest: common.Hash{}, - Nonce: ethtypes.BlockNonce{}, - } -} diff --git a/rpc/backend/backend.go b/rpc/backend/backend.go new file mode 100644 index 000000000..99d711747 --- /dev/null +++ b/rpc/backend/backend.go @@ -0,0 +1,259 @@ +package backend + +import ( + "context" + "fmt" + "os" + + "github.com/tendermint/tendermint/libs/log" + + rpctypes "github.com/cosmos/ethermint/rpc/types" + evmtypes "github.com/cosmos/ethermint/x/evm/types" + + clientcontext "github.com/cosmos/cosmos-sdk/client/context" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + ethtypes "github.com/ethereum/go-ethereum/core/types" +) + +// Backend implements the functionality needed to filter changes. +// Implemented by EthermintBackend. +type Backend interface { + // Used by block filter; also used for polling + BlockNumber() (hexutil.Uint64, error) + HeaderByNumber(blockNum rpctypes.BlockNumber) (*ethtypes.Header, error) + HeaderByHash(blockHash common.Hash) (*ethtypes.Header, error) + GetBlockByNumber(blockNum rpctypes.BlockNumber, fullTx bool) (map[string]interface{}, error) + GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) + + // returns the logs of a given block + GetLogs(blockHash common.Hash) ([][]*ethtypes.Log, error) + + // Used by pending transaction filter + PendingTransactions() ([]*rpctypes.Transaction, error) + + // Used by log filter + GetTransactionLogs(txHash common.Hash) ([]*ethtypes.Log, error) + BloomStatus() (uint64, uint64) +} + +var _ Backend = (*EthermintBackend)(nil) + +// EthermintBackend implements the Backend interface +type EthermintBackend struct { + ctx context.Context + clientCtx clientcontext.CLIContext + logger log.Logger + gasLimit int64 +} + +// New creates a new EthermintBackend instance +func New(clientCtx clientcontext.CLIContext) *EthermintBackend { + return &EthermintBackend{ + ctx: context.Background(), + clientCtx: clientCtx, + logger: log.NewTMLogger(log.NewSyncWriter(os.Stdout)).With("module", "json-rpc"), + gasLimit: int64(^uint32(0)), + } +} + +// BlockNumber returns the current block number. +func (b *EthermintBackend) BlockNumber() (hexutil.Uint64, error) { + // NOTE: using 0 as min and max height returns the blockchain info up to the latest block. + info, err := b.clientCtx.Client.BlockchainInfo(0, 0) + if err != nil { + return hexutil.Uint64(0), err + } + + return hexutil.Uint64(info.LastHeight), nil +} + +// GetBlockByNumber returns the block identified by number. +func (b *EthermintBackend) GetBlockByNumber(blockNum rpctypes.BlockNumber, fullTx bool) (map[string]interface{}, error) { + height := blockNum.Int64() + if height <= 0 { + // get latest block height + num, err := b.BlockNumber() + if err != nil { + return nil, err + } + + height = int64(num) + } + + resBlock, err := b.clientCtx.Client.Block(&height) + if err != nil { + return nil, err + } + + return rpctypes.EthBlockFromTendermint(b.clientCtx, resBlock.Block) +} + +// GetBlockByHash returns the block identified by hash. +func (b *EthermintBackend) GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) { + res, _, err := b.clientCtx.Query(fmt.Sprintf("custom/%s/%s/%s", evmtypes.ModuleName, evmtypes.QueryHashToHeight, hash.Hex())) + if err != nil { + return nil, err + } + + var out evmtypes.QueryResBlockNumber + if err := b.clientCtx.Codec.UnmarshalJSON(res, &out); err != nil { + return nil, err + } + + resBlock, err := b.clientCtx.Client.Block(&out.Number) + if err != nil { + return nil, err + } + + return rpctypes.EthBlockFromTendermint(b.clientCtx, resBlock.Block) +} + +// HeaderByNumber returns the block header identified by height. +func (b *EthermintBackend) HeaderByNumber(blockNum rpctypes.BlockNumber) (*ethtypes.Header, error) { + height := blockNum.Int64() + if height <= 0 { + // get latest block height + num, err := b.BlockNumber() + if err != nil { + return nil, err + } + + height = int64(num) + } + + resBlock, err := b.clientCtx.Client.Block(&height) + if err != nil { + return nil, err + } + + res, _, err := b.clientCtx.Query(fmt.Sprintf("custom/%s/%s/%d", evmtypes.ModuleName, evmtypes.QueryBloom, resBlock.Block.Height)) + if err != nil { + return nil, err + } + + var bloomRes evmtypes.QueryBloomFilter + b.clientCtx.Codec.MustUnmarshalJSON(res, &bloomRes) + + ethHeader := rpctypes.EthHeaderFromTendermint(resBlock.Block.Header) + ethHeader.Bloom = bloomRes.Bloom + return ethHeader, nil +} + +// HeaderByHash returns the block header identified by hash. +func (b *EthermintBackend) HeaderByHash(blockHash common.Hash) (*ethtypes.Header, error) { + res, _, err := b.clientCtx.Query(fmt.Sprintf("custom/%s/%s/%s", evmtypes.ModuleName, evmtypes.QueryHashToHeight, blockHash.Hex())) + if err != nil { + return nil, err + } + + var out evmtypes.QueryResBlockNumber + if err := b.clientCtx.Codec.UnmarshalJSON(res, &out); err != nil { + return nil, err + } + + resBlock, err := b.clientCtx.Client.Block(&out.Number) + if err != nil { + return nil, err + } + + res, _, err = b.clientCtx.Query(fmt.Sprintf("custom/%s/%s/%d", evmtypes.ModuleName, evmtypes.QueryBloom, resBlock.Block.Height)) + if err != nil { + return nil, err + } + + var bloomRes evmtypes.QueryBloomFilter + b.clientCtx.Codec.MustUnmarshalJSON(res, &bloomRes) + + ethHeader := rpctypes.EthHeaderFromTendermint(resBlock.Block.Header) + ethHeader.Bloom = bloomRes.Bloom + return ethHeader, nil +} + +// GetTransactionLogs returns the logs given a transaction hash. +// It returns an error if there's an encoding error. +// If no logs are found for the tx hash, the error is nil. +func (b *EthermintBackend) GetTransactionLogs(txHash common.Hash) ([]*ethtypes.Log, error) { + res, _, err := b.clientCtx.QueryWithData(fmt.Sprintf("custom/%s/%s/%s", evmtypes.ModuleName, evmtypes.QueryTransactionLogs, txHash.String()), nil) + if err != nil { + return nil, err + } + + out := new(evmtypes.QueryETHLogs) + if err := b.clientCtx.Codec.UnmarshalJSON(res, &out); err != nil { + return nil, err + } + + return out.Logs, nil +} + +// PendingTransactions returns the transactions that are in the transaction pool +// and have a from address that is one of the accounts this node manages. +func (b *EthermintBackend) PendingTransactions() ([]*rpctypes.Transaction, error) { + pendingTxs, err := b.clientCtx.Client.UnconfirmedTxs(1000) + if err != nil { + return nil, err + } + + transactions := make([]*rpctypes.Transaction, pendingTxs.Count) + for _, tx := range pendingTxs.Txs { + ethTx, err := rpctypes.RawTxToEthTx(b.clientCtx, tx) + if err != nil { + // ignore non Ethermint EVM transactions + continue + } + + // TODO: check signer and reference against accounts the node manages + rpcTx, err := rpctypes.NewTransaction(ethTx, common.BytesToHash(tx.Hash()), common.Hash{}, 0, 0) + if err != nil { + return nil, err + } + + transactions = append(transactions, rpcTx) + } + + return transactions, nil +} + +// GetLogs returns all the logs from all the ethereum transactions in a block. +func (b *EthermintBackend) GetLogs(blockHash common.Hash) ([][]*ethtypes.Log, error) { + res, _, err := b.clientCtx.Query(fmt.Sprintf("custom/%s/%s/%s", evmtypes.ModuleName, evmtypes.QueryHashToHeight, blockHash.Hex())) + if err != nil { + return nil, err + } + + var out evmtypes.QueryResBlockNumber + if err := b.clientCtx.Codec.UnmarshalJSON(res, &out); err != nil { + return nil, err + } + + block, err := b.clientCtx.Client.Block(&out.Number) + if err != nil { + return nil, err + } + + var blockLogs = [][]*ethtypes.Log{} + for _, tx := range block.Block.Txs { + // NOTE: we query the state in case the tx result logs are not persisted after an upgrade. + res, _, err := b.clientCtx.QueryWithData(fmt.Sprintf("custom/%s/%s/%s", evmtypes.ModuleName, evmtypes.QueryTransactionLogs, common.BytesToHash(tx.Hash()).String()), nil) + if err != nil { + continue + } + + out := new(evmtypes.QueryETHLogs) + if err := b.clientCtx.Codec.UnmarshalJSON(res, &out); err != nil { + return nil, err + } + + blockLogs = append(blockLogs, out.Logs) + } + + return blockLogs, nil +} + +// BloomStatus returns the BloomBitsBlocks and the number of processed sections maintained +// by the chain indexer. +func (b *EthermintBackend) BloomStatus() (uint64, uint64) { + return 4096, 0 +} diff --git a/rpc/cmd.go b/rpc/cmd.go new file mode 100644 index 000000000..bfd4663b3 --- /dev/null +++ b/rpc/cmd.go @@ -0,0 +1,18 @@ +package rpc + +import ( + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/client/lcd" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/spf13/cobra" +) + +// ServeCmd creates a CLI command to start Cosmos REST server with web3 RPC API and +// Cosmos rest-server endpoints +func ServeCmd(cdc *codec.Codec) *cobra.Command { + cmd := lcd.ServeCommand(cdc, RegisterRoutes) + cmd.Flags().String(flagUnlockKey, "", "Select a key to unlock on the RPC server") + cmd.Flags().String(flagWebsocket, "8546", "websocket port to listen to") + cmd.Flags().StringP(flags.FlagBroadcastMode, "b", flags.BroadcastSync, "Transaction broadcasting mode (sync|async|block)") + return cmd +} diff --git a/rpc/config.go b/rpc/config.go index 7133c15dd..12125449e 100644 --- a/rpc/config.go +++ b/rpc/config.go @@ -6,14 +6,12 @@ import ( "os" "strings" - "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/client/input" "github.com/cosmos/cosmos-sdk/client/lcd" - "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/crypto/keys" sdk "github.com/cosmos/cosmos-sdk/types" authrest "github.com/cosmos/cosmos-sdk/x/auth/client/rest" @@ -21,6 +19,7 @@ import ( "github.com/cosmos/ethermint/app" "github.com/cosmos/ethermint/crypto/ethsecp256k1" "github.com/cosmos/ethermint/crypto/hd" + "github.com/cosmos/ethermint/rpc/websockets" "github.com/ethereum/go-ethereum/rpc" ) @@ -29,20 +28,10 @@ const ( flagWebsocket = "wsport" ) -// EmintServeCmd creates a CLI command to start Cosmos REST server with web3 RPC API and -// Cosmos rest-server endpoints -func EmintServeCmd(cdc *codec.Codec) *cobra.Command { - cmd := lcd.ServeCommand(cdc, registerRoutes) - cmd.Flags().String(flagUnlockKey, "", "Select a key to unlock on the RPC server") - cmd.Flags().String(flagWebsocket, "8546", "websocket port to listen to") - cmd.Flags().StringP(flags.FlagBroadcastMode, "b", flags.BroadcastSync, "Transaction broadcasting mode (sync|async|block)") - return cmd -} - -// registerRoutes creates a new server and registers the `/rpc` endpoint. +// RegisterRoutes creates a new server and registers the `/rpc` endpoint. // Rpc calls are enabled based on their associated module (eg. "eth"). -func registerRoutes(rs *lcd.RestServer) { - s := rpc.NewServer() +func RegisterRoutes(rs *lcd.RestServer) { + server := rpc.NewServer() accountName := viper.GetString(flagUnlockKey) accountNames := strings.Split(accountName, ",") @@ -71,26 +60,18 @@ func registerRoutes(rs *lcd.RestServer) { } } - apis := GetRPCAPIs(rs.CliCtx, privkeys) + apis := GetAPIs(rs.CliCtx, privkeys...) - // TODO: Allow cli to configure modules https://github.com/cosmos/ethermint/issues/74 - whitelist := make(map[string]bool) - - // Register all the APIs exposed by the services + // Register all the APIs exposed by the namespace services + // TODO: handle allowlist and private APIs for _, api := range apis { - if whitelist[api.Namespace] || (len(whitelist) == 0 && api.Public) { - if err := s.RegisterName(api.Namespace, api.Service); err != nil { - panic(err) - } - } else if !api.Public { // TODO: how to handle private apis? should only accept local calls - if err := s.RegisterName(api.Namespace, api.Service); err != nil { - panic(err) - } + if err := server.RegisterName(api.Namespace, api.Service); err != nil { + panic(err) } } // Web3 RPC API route - rs.Mux.HandleFunc("/", s.ServeHTTP).Methods("POST", "OPTIONS") + rs.Mux.HandleFunc("/", server.ServeHTTP).Methods("POST", "OPTIONS") // Register all other Cosmos routes client.RegisterRoutes(rs.CliCtx, rs.Mux) @@ -99,8 +80,8 @@ func registerRoutes(rs *lcd.RestServer) { // start websockets server websocketAddr := viper.GetString(flagWebsocket) - ws := newWebsocketsServer(rs.CliCtx, websocketAddr) - ws.start() + ws := websockets.NewServer(rs.CliCtx, websocketAddr) + ws.Start() } func unlockKeyFromNameAndPassphrase(accountNames []string, passphrase string) ([]ethsecp256k1.PrivKey, error) { diff --git a/rpc/doc.go b/rpc/doc.go new file mode 100644 index 000000000..bea7dea7a --- /dev/null +++ b/rpc/doc.go @@ -0,0 +1,10 @@ +// Package rpc contains RPC handler methods, namespaces and utilities to start +// Ethermint's Web3-compatible JSON-RPC server. +// +// The list of available namespaces are: +// +// * `rpc/namespaces/eth`: `eth` namespace. Exposes the `PublicEthereumAPI` and the `PublicFilterAPI`. +// * `rpc/namespaces/personal`: `personal` namespace. Exposes the `PrivateAccountAPI`. +// * `rpc/namespaces/net`: `net` namespace. Exposes the `PublicNetAPI`. +// * `rpc/namespaces/web3`: `web3` namespace. Exposes the `PublicWeb3API` +package rpc diff --git a/rpc/eth_api.go b/rpc/eth_api.go deleted file mode 100644 index 6c602df82..000000000 --- a/rpc/eth_api.go +++ /dev/null @@ -1,1070 +0,0 @@ -package rpc - -import ( - "bytes" - "errors" - "fmt" - "math/big" - "os" - "sync" - - "github.com/spf13/viper" - - "github.com/cosmos/ethermint/crypto/ethsecp256k1" - "github.com/cosmos/ethermint/crypto/hd" - params "github.com/cosmos/ethermint/rpc/args" - ethermint "github.com/cosmos/ethermint/types" - "github.com/cosmos/ethermint/utils" - "github.com/cosmos/ethermint/version" - evmtypes "github.com/cosmos/ethermint/x/evm/types" - - abci "github.com/tendermint/tendermint/abci/types" - "github.com/tendermint/tendermint/crypto/merkle" - tmbytes "github.com/tendermint/tendermint/libs/bytes" - "github.com/tendermint/tendermint/libs/log" - "github.com/tendermint/tendermint/rpc/client" - tmtypes "github.com/tendermint/tendermint/types" - - "github.com/ethereum/go-ethereum/accounts/keystore" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - ethtypes "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/rlp" - - "github.com/cosmos/cosmos-sdk/client/context" - "github.com/cosmos/cosmos-sdk/client/flags" - "github.com/cosmos/cosmos-sdk/crypto/keys" - sdk "github.com/cosmos/cosmos-sdk/types" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - "github.com/cosmos/cosmos-sdk/x/auth" - authclient "github.com/cosmos/cosmos-sdk/x/auth/client/utils" - authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" -) - -// PublicEthAPI is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec. -type PublicEthAPI struct { - cliCtx context.CLIContext - chainIDEpoch *big.Int - logger log.Logger - backend Backend - keys []ethsecp256k1.PrivKey // unlocked keys - nonceLock *AddrLocker - keybaseLock sync.Mutex -} - -// NewPublicEthAPI creates an instance of the public ETH Web3 API. -func NewPublicEthAPI(cliCtx context.CLIContext, backend Backend, nonceLock *AddrLocker, - key []ethsecp256k1.PrivKey) *PublicEthAPI { - - epoch, err := ethermint.ParseChainID(cliCtx.ChainID) - if err != nil { - panic(err) - } - - api := &PublicEthAPI{ - cliCtx: cliCtx, - chainIDEpoch: epoch, - logger: log.NewTMLogger(log.NewSyncWriter(os.Stdout)).With("module", "json-rpc"), - backend: backend, - keys: key, - nonceLock: nonceLock, - } - - if err := api.getKeybaseInfo(); err != nil { - api.logger.Error("failed to get keybase info", "error", err) - } - - return api -} - -func (e *PublicEthAPI) getKeybaseInfo() error { - e.keybaseLock.Lock() - defer e.keybaseLock.Unlock() - - if e.cliCtx.Keybase == nil { - keybase, err := keys.NewKeyring( - sdk.KeyringServiceName(), - viper.GetString(flags.FlagKeyringBackend), - viper.GetString(flags.FlagHome), - e.cliCtx.Input, - hd.EthSecp256k1Options()..., - ) - if err != nil { - return err - } - - e.cliCtx.Keybase = keybase - } - - return nil -} - -// ProtocolVersion returns the supported Ethereum protocol version. -func (e *PublicEthAPI) ProtocolVersion() hexutil.Uint { - e.logger.Debug("eth_protocolVersion") - return hexutil.Uint(version.ProtocolVersion) -} - -// ChainId returns the chain's identifier in hex format -func (e *PublicEthAPI) ChainId() (hexutil.Uint, error) { // nolint - e.logger.Debug("eth_chainId") - return hexutil.Uint(uint(e.chainIDEpoch.Uint64())), nil -} - -// Syncing returns whether or not the current node is syncing with other peers. Returns false if not, or a struct -// outlining the state of the sync if it is. -func (e *PublicEthAPI) Syncing() (interface{}, error) { - e.logger.Debug("eth_syncing") - - status, err := e.cliCtx.Client.Status() - if err != nil { - return false, err - } - - if !status.SyncInfo.CatchingUp { - return false, nil - } - - return map[string]interface{}{ - // "startingBlock": nil, // NA - "currentBlock": hexutil.Uint64(status.SyncInfo.LatestBlockHeight), - // "highestBlock": nil, // NA - // "pulledStates": nil, // NA - // "knownStates": nil, // NA - }, nil -} - -// Coinbase is the address that staking rewards will be send to (alias for Etherbase). -func (e *PublicEthAPI) Coinbase() (common.Address, error) { - e.logger.Debug("eth_coinbase") - - node, err := e.cliCtx.GetNode() - if err != nil { - return common.Address{}, err - } - - status, err := node.Status() - if err != nil { - return common.Address{}, err - } - - return common.BytesToAddress(status.ValidatorInfo.Address.Bytes()), nil -} - -// Mining returns whether or not this node is currently mining. Always false. -func (e *PublicEthAPI) Mining() bool { - e.logger.Debug("eth_mining") - return false -} - -// Hashrate returns the current node's hashrate. Always 0. -func (e *PublicEthAPI) Hashrate() hexutil.Uint64 { - e.logger.Debug("eth_hashrate") - return 0 -} - -// GasPrice returns the current gas price based on Ethermint's gas price oracle. -func (e *PublicEthAPI) GasPrice() *hexutil.Big { - e.logger.Debug("eth_gasPrice") - out := big.NewInt(0) - return (*hexutil.Big)(out) -} - -// Accounts returns the list of accounts available to this node. -func (e *PublicEthAPI) Accounts() ([]common.Address, error) { - e.logger.Debug("eth_accounts") - e.keybaseLock.Lock() - - addresses := make([]common.Address, 0) // return [] instead of nil if empty - - keybase, err := keys.NewKeyring( - sdk.KeyringServiceName(), - viper.GetString(flags.FlagKeyringBackend), - viper.GetString(flags.FlagHome), - e.cliCtx.Input, - hd.EthSecp256k1Options()..., - ) - if err != nil { - return addresses, err - } - - infos, err := keybase.List() - if err != nil { - return addresses, err - } - - e.keybaseLock.Unlock() - - for _, info := range infos { - addressBytes := info.GetPubKey().Address().Bytes() - addresses = append(addresses, common.BytesToAddress(addressBytes)) - } - - return addresses, nil -} - -// BlockNumber returns the current block number. -func (e *PublicEthAPI) BlockNumber() (hexutil.Uint64, error) { - e.logger.Debug("eth_blockNumber") - return e.backend.BlockNumber() -} - -// GetBalance returns the provided account's balance up to the provided block number. -func (e *PublicEthAPI) GetBalance(address common.Address, blockNum BlockNumber) (*hexutil.Big, error) { - e.logger.Debug("eth_getBalance", "address", address, "block number", blockNum) - ctx := e.cliCtx.WithHeight(blockNum.Int64()) - res, _, err := ctx.QueryWithData(fmt.Sprintf("custom/%s/balance/%s", evmtypes.ModuleName, address.Hex()), nil) - if err != nil { - return nil, err - } - - var out evmtypes.QueryResBalance - e.cliCtx.Codec.MustUnmarshalJSON(res, &out) - val, err := utils.UnmarshalBigInt(out.Balance) - if err != nil { - return nil, err - } - - return (*hexutil.Big)(val), nil -} - -// GetStorageAt returns the contract storage at the given address, block number, and key. -func (e *PublicEthAPI) GetStorageAt(address common.Address, key string, blockNum BlockNumber) (hexutil.Bytes, error) { - e.logger.Debug("eth_getStorageAt", "address", address, "key", key, "block number", blockNum) - ctx := e.cliCtx.WithHeight(blockNum.Int64()) - res, _, err := ctx.QueryWithData(fmt.Sprintf("custom/%s/storage/%s/%s", evmtypes.ModuleName, address.Hex(), key), nil) - if err != nil { - return nil, err - } - - var out evmtypes.QueryResStorage - e.cliCtx.Codec.MustUnmarshalJSON(res, &out) - return out.Value, nil -} - -// GetTransactionCount returns the number of transactions at the given address up to the given block number. -func (e *PublicEthAPI) GetTransactionCount(address common.Address, blockNum BlockNumber) (*hexutil.Uint64, error) { - e.logger.Debug("eth_getTransactionCount", "address", address, "block number", blockNum) - ctx := e.cliCtx.WithHeight(blockNum.Int64()) - - // Get nonce (sequence) from account - from := sdk.AccAddress(address.Bytes()) - accRet := authtypes.NewAccountRetriever(ctx) - - err := accRet.EnsureExists(from) - if err != nil { - // account doesn't exist yet, return 0 - n := hexutil.Uint64(0) - return &n, nil - } - - _, nonce, err := accRet.GetAccountNumberSequence(from) - if err != nil { - return nil, err - } - - n := hexutil.Uint64(nonce) - return &n, nil -} - -// GetBlockTransactionCountByHash returns the number of transactions in the block identified by hash. -func (e *PublicEthAPI) GetBlockTransactionCountByHash(hash common.Hash) *hexutil.Uint { - e.logger.Debug("eth_getBlockTransactionCountByHash", "hash", hash) - res, _, err := e.cliCtx.Query(fmt.Sprintf("custom/%s/%s/%s", evmtypes.ModuleName, evmtypes.QueryHashToHeight, hash.Hex())) - if err != nil { - // Return nil if block does not exist - return nil - } - - var out evmtypes.QueryResBlockNumber - e.cliCtx.Codec.MustUnmarshalJSON(res, &out) - return e.getBlockTransactionCountByNumber(out.Number) -} - -// GetBlockTransactionCountByNumber returns the number of transactions in the block identified by number. -func (e *PublicEthAPI) GetBlockTransactionCountByNumber(blockNum BlockNumber) *hexutil.Uint { - e.logger.Debug("eth_getBlockTransactionCountByNumber", "block number", blockNum) - height := blockNum.Int64() - return e.getBlockTransactionCountByNumber(height) -} - -func (e *PublicEthAPI) getBlockTransactionCountByNumber(number int64) *hexutil.Uint { - block, err := e.cliCtx.Client.Block(&number) - if err != nil { - // Return nil if block doesn't exist - return nil - } - - n := hexutil.Uint(len(block.Block.Txs)) - return &n -} - -// GetUncleCountByBlockHash returns the number of uncles in the block idenfied by hash. Always zero. -func (e *PublicEthAPI) GetUncleCountByBlockHash(hash common.Hash) hexutil.Uint { - return 0 -} - -// GetUncleCountByBlockNumber returns the number of uncles in the block idenfied by number. Always zero. -func (e *PublicEthAPI) GetUncleCountByBlockNumber(blockNum BlockNumber) hexutil.Uint { - return 0 -} - -// GetCode returns the contract code at the given address and block number. -func (e *PublicEthAPI) GetCode(address common.Address, blockNumber BlockNumber) (hexutil.Bytes, error) { - e.logger.Debug("eth_getCode", "address", address, "block number", blockNumber) - ctx := e.cliCtx.WithHeight(blockNumber.Int64()) - res, _, err := ctx.QueryWithData(fmt.Sprintf("custom/%s/%s/%s", evmtypes.ModuleName, evmtypes.QueryCode, address.Hex()), nil) - if err != nil { - return nil, err - } - - var out evmtypes.QueryResCode - e.cliCtx.Codec.MustUnmarshalJSON(res, &out) - return out.Code, nil -} - -// GetTransactionLogs returns the logs given a transaction hash. -func (e *PublicEthAPI) GetTransactionLogs(txHash common.Hash) ([]*ethtypes.Log, error) { - e.logger.Debug("eth_getTransactionLogs", "hash", txHash) - return e.backend.GetTransactionLogs(txHash) -} - -// ExportAccount exports an account's balance, code, and storage at the given block number -// TODO: deprecate this once the export genesis command works -func (e *PublicEthAPI) ExportAccount(address common.Address, blockNumber BlockNumber) (string, error) { - e.logger.Debug("eth_exportAccount", "address", address, "block number", blockNumber) - ctx := e.cliCtx.WithHeight(blockNumber.Int64()) - - res, _, err := ctx.QueryWithData(fmt.Sprintf("custom/%s/%s/%s", evmtypes.ModuleName, evmtypes.QueryExportAccount, address.Hex()), nil) - if err != nil { - return "", err - } - - return string(res), nil -} - -func checkKeyInKeyring(keys []ethsecp256k1.PrivKey, address common.Address) (key ethsecp256k1.PrivKey, exist bool) { - if len(keys) > 0 { - for _, key := range keys { - if bytes.Equal(key.PubKey().Address().Bytes(), address.Bytes()) { - return key, true - } - } - } - return nil, false -} - -// Sign signs the provided data using the private key of address via Geth's signature standard. -func (e *PublicEthAPI) Sign(address common.Address, data hexutil.Bytes) (hexutil.Bytes, error) { - e.logger.Debug("eth_sign", "address", address, "data", data) - // TODO: Change this functionality to find an unlocked account by address - - key, exist := checkKeyInKeyring(e.keys, address) - if !exist { - return nil, keystore.ErrLocked - } - - // Sign the requested hash with the wallet - signature, err := key.Sign(data) - if err == nil { - signature[64] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper - } - - return signature, err -} - -// SendTransaction sends an Ethereum transaction. -func (e *PublicEthAPI) SendTransaction(args params.SendTxArgs) (common.Hash, error) { - e.logger.Debug("eth_sendTransaction", "args", args) - // TODO: Change this functionality to find an unlocked account by address - - for _, key := range e.keys { - e.logger.Debug("eth_sendTransaction", "key", fmt.Sprintf("0x%x", key.PubKey().Address().Bytes())) - } - - key, exist := checkKeyInKeyring(e.keys, args.From) - if !exist { - e.logger.Debug("failed to find key in keyring", "key", args.From) - return common.Hash{}, keystore.ErrLocked - } - - // Mutex lock the address' nonce to avoid assigning it to multiple requests - if args.Nonce == nil { - e.nonceLock.LockAddr(args.From) - defer e.nonceLock.UnlockAddr(args.From) - } - - // Assemble transaction from fields - tx, err := e.generateFromArgs(args) - if err != nil { - e.logger.Debug("failed to generate tx", "error", err) - return common.Hash{}, err - } - - // ChainID must be set as flag to send transaction - chainID := viper.GetString(flags.FlagChainID) - // parse the chainID from a string to a base-10 integer - chainIDEpoch, err := ethermint.ParseChainID(chainID) - if err != nil { - return common.Hash{}, err - } - - // Sign transaction - if err := tx.Sign(chainIDEpoch, key.ToECDSA()); err != nil { - e.logger.Debug("failed to sign tx", "error", err) - return common.Hash{}, err - } - - // Encode transaction by default Tx encoder - txEncoder := authclient.GetTxEncoder(e.cliCtx.Codec) - txBytes, err := txEncoder(tx) - if err != nil { - return common.Hash{}, err - } - - // Broadcast transaction in sync mode (default) - res, err := e.cliCtx.BroadcastTx(txBytes) - // If error is encountered on the node, the broadcast will not return an error - if err != nil { - return common.Hash{}, err - } - - // Return transaction hash - return common.HexToHash(res.TxHash), nil -} - -// SendRawTransaction send a raw Ethereum transaction. -func (e *PublicEthAPI) SendRawTransaction(data hexutil.Bytes) (common.Hash, error) { - e.logger.Debug("eth_sendRawTransaction", "data", data) - tx := new(evmtypes.MsgEthereumTx) - - // RLP decode raw transaction bytes - if err := rlp.DecodeBytes(data, tx); err != nil { - // Return nil is for when gasLimit overflows uint64 - return common.Hash{}, nil - } - - // Encode transaction by default Tx encoder - txEncoder := authclient.GetTxEncoder(e.cliCtx.Codec) - txBytes, err := txEncoder(tx) - if err != nil { - return common.Hash{}, err - } - - // TODO: Possibly log the contract creation address (if recipient address is nil) or tx data - // If error is encountered on the node, the broadcast will not return an error - res, err := e.cliCtx.BroadcastTx(txBytes) - if err != nil { - return common.Hash{}, err - } - - // Return transaction hash - return common.HexToHash(res.TxHash), nil -} - -// CallArgs represents the arguments for a call. -type CallArgs struct { - From *common.Address `json:"from"` - To *common.Address `json:"to"` - Gas *hexutil.Uint64 `json:"gas"` - GasPrice *hexutil.Big `json:"gasPrice"` - Value *hexutil.Big `json:"value"` - Data *hexutil.Bytes `json:"data"` -} - -// Call performs a raw contract call. -func (e *PublicEthAPI) Call(args CallArgs, blockNr BlockNumber, _ *map[common.Address]account) (hexutil.Bytes, error) { - e.logger.Debug("eth_call", "args", args, "block number", blockNr) - simRes, err := e.doCall(args, blockNr, big.NewInt(ethermint.DefaultRPCGasLimit)) - if err != nil { - return []byte{}, err - } - - data, err := evmtypes.DecodeResultData(simRes.Result.Data) - if err != nil { - return []byte{}, err - } - - return (hexutil.Bytes)(data.Ret), nil -} - -// account indicates the overriding fields of account during the execution of -// a message call. -// Note, state and stateDiff can't be specified at the same time. If state is -// set, message execution will only use the data in the given state. Otherwise -// if statDiff is set, all diff will be applied first and then execute the call -// message. -type account struct { - Nonce *hexutil.Uint64 `json:"nonce"` - Code *hexutil.Bytes `json:"code"` - Balance **hexutil.Big `json:"balance"` - State *map[common.Hash]common.Hash `json:"state"` - StateDiff *map[common.Hash]common.Hash `json:"stateDiff"` -} - -// DoCall performs a simulated call operation through the evmtypes. It returns the -// estimated gas used on the operation or an error if fails. -func (e *PublicEthAPI) doCall( - args CallArgs, blockNr BlockNumber, globalGasCap *big.Int, -) (*sdk.SimulationResponse, error) { - // Set height for historical queries - ctx := e.cliCtx - - if blockNr.Int64() != 0 { - ctx = e.cliCtx.WithHeight(blockNr.Int64()) - } - - // Set sender address or use a default if none specified - var addr common.Address - - if args.From == nil { - addrs, err := e.Accounts() - if err == nil && len(addrs) > 0 { - addr = addrs[0] - } - } else { - addr = *args.From - } - - // Set default gas & gas price if none were set - // Change this to uint64(math.MaxUint64 / 2) if gas cap can be configured - gas := uint64(ethermint.DefaultRPCGasLimit) - if args.Gas != nil { - gas = uint64(*args.Gas) - } - if globalGasCap != nil && globalGasCap.Uint64() < gas { - e.logger.Debug("Caller gas above allowance, capping", "requested", gas, "cap", globalGasCap) - gas = globalGasCap.Uint64() - } - - // Set gas price using default or parameter if passed in - gasPrice := new(big.Int).SetUint64(ethermint.DefaultGasPrice) - if args.GasPrice != nil { - gasPrice = args.GasPrice.ToInt() - } - - // Set value for transaction - value := new(big.Int) - if args.Value != nil { - value = args.Value.ToInt() - } - - // Set Data if provided - var data []byte - if args.Data != nil { - data = []byte(*args.Data) - } - - // Set destination address for call - var toAddr sdk.AccAddress - if args.To != nil { - toAddr = sdk.AccAddress(args.To.Bytes()) - } - - // Create new call message - msg := evmtypes.NewMsgEthermint(0, &toAddr, sdk.NewIntFromBigInt(value), gas, - sdk.NewIntFromBigInt(gasPrice), data, sdk.AccAddress(addr.Bytes())) - - if err := msg.ValidateBasic(); err != nil { - return nil, err - } - - // Generate tx to be used to simulate (signature isn't needed) - var stdSig authtypes.StdSignature - tx := authtypes.NewStdTx([]sdk.Msg{msg}, authtypes.StdFee{}, []authtypes.StdSignature{stdSig}, "") - - txEncoder := authclient.GetTxEncoder(ctx.Codec) - txBytes, err := txEncoder(tx) - if err != nil { - return nil, err - } - - // Transaction simulation through query - res, _, err := ctx.QueryWithData("app/simulate", txBytes) - if err != nil { - return nil, err - } - - var simResponse sdk.SimulationResponse - if err := ctx.Codec.UnmarshalBinaryBare(res, &simResponse); err != nil { - return nil, err - } - - return &simResponse, nil -} - -// EstimateGas returns an estimate of gas usage for the given smart contract call. -// It adds 1,000 gas to the returned value instead of using the gas adjustment -// param from the SDK. -func (e *PublicEthAPI) EstimateGas(args CallArgs) (hexutil.Uint64, error) { - e.logger.Debug("eth_estimateGas", "args", args) - simResponse, err := e.doCall(args, 0, big.NewInt(ethermint.DefaultRPCGasLimit)) - if err != nil { - return 0, err - } - - // TODO: change 1000 buffer for more accurate buffer (eg: SDK's gasAdjusted) - estimatedGas := simResponse.GasInfo.GasUsed - gas := estimatedGas + 1000 - - return hexutil.Uint64(gas), nil -} - -// GetBlockByHash returns the block identified by hash. -func (e *PublicEthAPI) GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) { - e.logger.Debug("eth_getBlockByHash", "hash", hash, "full", fullTx) - return e.backend.GetBlockByHash(hash, fullTx) -} - -// GetBlockByNumber returns the block identified by number. -func (e *PublicEthAPI) GetBlockByNumber(blockNum BlockNumber, fullTx bool) (map[string]interface{}, error) { - e.logger.Debug("eth_getBlockByNumber", "number", blockNum, "full", fullTx) - return e.backend.GetBlockByNumber(blockNum, fullTx) -} - -func formatBlock( - header tmtypes.Header, size int, gasLimit int64, - gasUsed *big.Int, transactions interface{}, bloom ethtypes.Bloom, -) map[string]interface{} { - if bytes.Equal(header.DataHash, []byte{}) { - header.DataHash = tmbytes.HexBytes(common.Hash{}.Bytes()) - } - - return map[string]interface{}{ - "number": hexutil.Uint64(header.Height), - "hash": hexutil.Bytes(header.Hash()), - "parentHash": hexutil.Bytes(header.LastBlockID.Hash), - "nonce": hexutil.Uint64(0), // PoW specific - "sha3Uncles": common.Hash{}, // No uncles in Tendermint - "logsBloom": bloom, - "transactionsRoot": hexutil.Bytes(header.DataHash), - "stateRoot": hexutil.Bytes(header.AppHash), - "miner": common.Address{}, - "mixHash": common.Hash{}, - "difficulty": 0, - "totalDifficulty": 0, - "extraData": hexutil.Uint64(0), - "size": hexutil.Uint64(size), - "gasLimit": hexutil.Uint64(gasLimit), // Static gas limit - "gasUsed": (*hexutil.Big)(gasUsed), - "timestamp": hexutil.Uint64(header.Time.Unix()), - "transactions": transactions.([]common.Hash), - "uncles": []string{}, - "receiptsRoot": common.Hash{}, - } -} - -func convertTransactionsToRPC(cliCtx context.CLIContext, txs []tmtypes.Tx, blockHash common.Hash, height uint64) ([]common.Hash, *big.Int, error) { - transactions := make([]common.Hash, len(txs)) - gasUsed := big.NewInt(0) - - for i, tx := range txs { - ethTx, err := bytesToEthTx(cliCtx, tx) - if err != nil { - // continue to next transaction in case it's not a MsgEthereumTx - continue - } - // TODO: Remove gas usage calculation if saving gasUsed per block - gasUsed.Add(gasUsed, ethTx.Fee()) - tx, err := newRPCTransaction(*ethTx, common.BytesToHash(tx.Hash()), blockHash, &height, uint64(i)) - if err != nil { - return nil, nil, err - } - transactions[i] = tx.Hash - } - - return transactions, gasUsed, nil -} - -// Transaction represents a transaction returned to RPC clients. -type Transaction 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"` -} - -func bytesToEthTx(cliCtx context.CLIContext, bz []byte) (*evmtypes.MsgEthereumTx, error) { - var stdTx sdk.Tx - // TODO: switch to UnmarshalBinaryBare on SDK v0.40.0 - err := cliCtx.Codec.UnmarshalBinaryLengthPrefixed(bz, &stdTx) - if err != nil { - return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) - } - - ethTx, ok := stdTx.(evmtypes.MsgEthereumTx) - if !ok { - return nil, fmt.Errorf("invalid transaction type %T, expected MsgEthereumTx", stdTx) - } - return ðTx, nil -} - -// newRPCTransaction returns a transaction that will serialize to the RPC -// representation, with the given location metadata set (if available). -func newRPCTransaction(tx evmtypes.MsgEthereumTx, txHash, blockHash common.Hash, blockNumber *uint64, index uint64) (*Transaction, error) { - // Verify signature and retrieve sender address - from, err := tx.VerifySig(tx.ChainID()) - if err != nil { - return nil, err - } - - result := Transaction{ - From: from, - Gas: hexutil.Uint64(tx.Data.GasLimit), - GasPrice: (*hexutil.Big)(tx.Data.Price), - Hash: txHash, - Input: hexutil.Bytes(tx.Data.Payload), - Nonce: hexutil.Uint64(tx.Data.AccountNonce), - To: tx.To(), - Value: (*hexutil.Big)(tx.Data.Amount), - V: (*hexutil.Big)(tx.Data.V), - R: (*hexutil.Big)(tx.Data.R), - S: (*hexutil.Big)(tx.Data.S), - } - - if blockHash != (common.Hash{}) { - result.BlockHash = &blockHash - result.BlockNumber = (*hexutil.Big)(new(big.Int).SetUint64(*blockNumber)) - result.TransactionIndex = (*hexutil.Uint64)(&index) - } - - return &result, nil -} - -// GetTransactionByHash returns the transaction identified by hash. -func (e *PublicEthAPI) GetTransactionByHash(hash common.Hash) (*Transaction, error) { - e.logger.Debug("eth_getTransactionByHash", "hash", hash) - tx, err := e.cliCtx.Client.Tx(hash.Bytes(), false) - if err != nil { - // Return nil for transaction when not found - return nil, nil - } - - // Can either cache or just leave this out if not necessary - block, err := e.cliCtx.Client.Block(&tx.Height) - if err != nil { - return nil, err - } - blockHash := common.BytesToHash(block.Block.Header.Hash()) - - ethTx, err := bytesToEthTx(e.cliCtx, tx.Tx) - if err != nil { - return nil, err - } - - height := uint64(tx.Height) - return newRPCTransaction(*ethTx, common.BytesToHash(tx.Tx.Hash()), blockHash, &height, uint64(tx.Index)) -} - -// GetTransactionByBlockHashAndIndex returns the transaction identified by hash and index. -func (e *PublicEthAPI) GetTransactionByBlockHashAndIndex(hash common.Hash, idx hexutil.Uint) (*Transaction, error) { - e.logger.Debug("eth_getTransactionByHashAndIndex", "hash", hash, "index", idx) - res, _, err := e.cliCtx.Query(fmt.Sprintf("custom/%s/%s/%s", evmtypes.ModuleName, evmtypes.QueryHashToHeight, hash.Hex())) - if err != nil { - return nil, err - } - - var out evmtypes.QueryResBlockNumber - e.cliCtx.Codec.MustUnmarshalJSON(res, &out) - return e.getTransactionByBlockNumberAndIndex(out.Number, idx) -} - -// GetTransactionByBlockNumberAndIndex returns the transaction identified by number and index. -func (e *PublicEthAPI) GetTransactionByBlockNumberAndIndex(blockNum BlockNumber, idx hexutil.Uint) (*Transaction, error) { - e.logger.Debug("eth_getTransactionByBlockNumberAndIndex", "number", blockNum, "index", idx) - value := blockNum.Int64() - return e.getTransactionByBlockNumberAndIndex(value, idx) -} - -func (e *PublicEthAPI) getTransactionByBlockNumberAndIndex(number int64, idx hexutil.Uint) (*Transaction, error) { - block, err := e.cliCtx.Client.Block(&number) - if err != nil { - return nil, err - } - header := block.Block.Header - - txs := block.Block.Txs - if uint64(idx) >= uint64(len(txs)) { - return nil, nil - } - ethTx, err := bytesToEthTx(e.cliCtx, txs[idx]) - if err != nil { - return nil, err - } - - height := uint64(header.Height) - return newRPCTransaction(*ethTx, common.BytesToHash(txs[idx].Hash()), common.BytesToHash(header.Hash()), &height, uint64(idx)) -} - -// GetTransactionReceipt returns the transaction receipt identified by hash. -func (e *PublicEthAPI) GetTransactionReceipt(hash common.Hash) (map[string]interface{}, error) { - e.logger.Debug("eth_getTransactionReceipt", "hash", hash) - tx, err := e.cliCtx.Client.Tx(hash.Bytes(), false) - if err != nil { - // Return nil for transaction when not found - return nil, nil - } - - // Query block for consensus hash - block, err := e.cliCtx.Client.Block(&tx.Height) - if err != nil { - return nil, err - } - blockHash := common.BytesToHash(block.Block.Header.Hash()) - - // Convert tx bytes to eth transaction - ethTx, err := bytesToEthTx(e.cliCtx, tx.Tx) - if err != nil { - return nil, err - } - - from, err := ethTx.VerifySig(ethTx.ChainID()) - if err != nil { - return nil, err - } - - // Set status codes based on tx result - var status hexutil.Uint - if tx.TxResult.IsOK() { - status = hexutil.Uint(1) - } else { - status = hexutil.Uint(0) - } - - txData := tx.TxResult.GetData() - - data, err := evmtypes.DecodeResultData(txData) - if err != nil { - status = 0 // transaction failed - } - - if data.Logs == nil { - data.Logs = []*ethtypes.Log{} - } - - receipt := map[string]interface{}{ - // Consensus fields: These fields are defined by the Yellow Paper - "status": status, - "cumulativeGasUsed": nil, // ignore until needed - "logsBloom": data.Bloom, - "logs": data.Logs, - - // Implementation fields: These fields are added by geth when processing a transaction. - // They are stored in the chain database. - "transactionHash": hash, - "contractAddress": data.ContractAddress, - "gasUsed": hexutil.Uint64(tx.TxResult.GasUsed), - - // Inclusion information: These fields provide information about the inclusion of the - // transaction corresponding to this receipt. - "blockHash": blockHash, - "blockNumber": hexutil.Uint64(tx.Height), - "transactionIndex": hexutil.Uint64(tx.Index), - - // sender and receiver (contract or EOA) addreses - "from": from, - "to": ethTx.To(), - } - - return receipt, nil -} - -// PendingTransactions returns the transactions that are in the transaction pool -// and have a from address that is one of the accounts this node manages. -func (e *PublicEthAPI) PendingTransactions() ([]*Transaction, error) { - e.logger.Debug("eth_getPendingTransactions") - return e.backend.PendingTransactions() -} - -// GetUncleByBlockHashAndIndex returns the uncle identified by hash and index. Always returns nil. -func (e *PublicEthAPI) GetUncleByBlockHashAndIndex(hash common.Hash, idx hexutil.Uint) map[string]interface{} { - return nil -} - -// GetUncleByBlockNumberAndIndex returns the uncle identified by number and index. Always returns nil. -func (e *PublicEthAPI) GetUncleByBlockNumberAndIndex(number hexutil.Uint, idx hexutil.Uint) map[string]interface{} { - return nil -} - -// Copied the Account and StorageResult types since they are registered under an -// internal pkg on geth. - -// AccountResult struct for account proof -type AccountResult struct { - Address common.Address `json:"address"` - AccountProof []string `json:"accountProof"` - Balance *hexutil.Big `json:"balance"` - CodeHash common.Hash `json:"codeHash"` - Nonce hexutil.Uint64 `json:"nonce"` - StorageHash common.Hash `json:"storageHash"` - StorageProof []StorageResult `json:"storageProof"` -} - -// StorageResult defines the format for storage proof return -type StorageResult struct { - Key string `json:"key"` - Value *hexutil.Big `json:"value"` - Proof []string `json:"proof"` -} - -// GetProof returns an account object with proof and any storage proofs -func (e *PublicEthAPI) GetProof(address common.Address, storageKeys []string, block BlockNumber) (*AccountResult, error) { - e.logger.Debug("eth_getProof", "address", address, "keys", storageKeys, "number", block) - e.cliCtx = e.cliCtx.WithHeight(int64(block)) - path := fmt.Sprintf("custom/%s/%s/%s", evmtypes.ModuleName, evmtypes.QueryAccount, address.Hex()) - - // query eth account at block height - resBz, _, err := e.cliCtx.Query(path) - if err != nil { - return nil, err - } - - var account evmtypes.QueryResAccount - e.cliCtx.Codec.MustUnmarshalJSON(resBz, &account) - - storageProofs := make([]StorageResult, len(storageKeys)) - opts := client.ABCIQueryOptions{Height: int64(block), Prove: true} - for i, k := range storageKeys { - // Get value for key - vPath := fmt.Sprintf("custom/%s/%s/%s/%s", evmtypes.ModuleName, evmtypes.QueryStorage, address, k) - vRes, err := e.cliCtx.Client.ABCIQueryWithOptions(vPath, nil, opts) - if err != nil { - return nil, err - } - - var value evmtypes.QueryResStorage - e.cliCtx.Codec.MustUnmarshalJSON(vRes.Response.GetValue(), &value) - - // check for proof - proof := vRes.Response.GetProof() - proofStr := new(merkle.Proof).String() - if proof != nil { - proofStr = proof.String() - } - - storageProofs[i] = StorageResult{ - Key: k, - Value: (*hexutil.Big)(common.BytesToHash(value.Value).Big()), - Proof: []string{proofStr}, - } - } - - req := abci.RequestQuery{ - Path: fmt.Sprintf("store/%s/key", auth.StoreKey), - Data: auth.AddressStoreKey(sdk.AccAddress(address.Bytes())), - Height: int64(block), - Prove: true, - } - - res, err := e.cliCtx.QueryABCI(req) - if err != nil { - return nil, err - } - - // check for proof - accountProof := res.GetProof() - accProofStr := new(merkle.Proof).String() - if accountProof != nil { - accProofStr = accountProof.String() - } - - return &AccountResult{ - Address: address, - AccountProof: []string{accProofStr}, - Balance: (*hexutil.Big)(utils.MustUnmarshalBigInt(account.Balance)), - CodeHash: common.BytesToHash(account.CodeHash), - Nonce: hexutil.Uint64(account.Nonce), - StorageHash: common.Hash{}, // Ethermint doesn't have a storage hash - StorageProof: storageProofs, - }, nil -} - -// generateFromArgs populates tx message with args (used in RPC API) -func (e *PublicEthAPI) generateFromArgs(args params.SendTxArgs) (*evmtypes.MsgEthereumTx, error) { - var ( - nonce uint64 - gasLimit uint64 - err error - ) - - amount := (*big.Int)(args.Value) - gasPrice := (*big.Int)(args.GasPrice) - - if args.GasPrice == nil { - - // Set default gas price - // TODO: Change to min gas price from context once available through server/daemon - gasPrice = big.NewInt(ethermint.DefaultGasPrice) - } - - if args.Nonce == nil { - // Get nonce (sequence) from account - from := sdk.AccAddress(args.From.Bytes()) - accRet := authtypes.NewAccountRetriever(e.cliCtx) - - if e.cliCtx.Keybase == nil { - return nil, fmt.Errorf("cliCtx.Keybase is nil") - } - - err = accRet.EnsureExists(from) - if err != nil { - // account doesn't exist - return nil, fmt.Errorf("nonexistent account %s: %s", args.From.Hex(), err) - } - - _, nonce, err = accRet.GetAccountNumberSequence(from) - if err != nil { - return nil, err - } - } else { - nonce = (uint64)(*args.Nonce) - } - - if args.Data != nil && args.Input != nil && !bytes.Equal(*args.Data, *args.Input) { - return nil, errors.New(`both "data" and "input" are set and not equal. Please use "input" to pass transaction call data`) - } - - // Sets input to either Input or Data, if both are set and not equal error above returns - var input []byte - if args.Input != nil { - input = *args.Input - } else if args.Data != nil { - input = *args.Data - } - - if args.To == nil && len(input) == 0 { - // Contract creation - return nil, fmt.Errorf("contract creation without any data provided") - } - - if args.Gas == nil { - callArgs := CallArgs{ - From: &args.From, - To: args.To, - Gas: args.Gas, - GasPrice: args.GasPrice, - Value: args.Value, - Data: args.Data, - } - gl, err := e.EstimateGas(callArgs) - if err != nil { - return nil, err - } - gasLimit = uint64(gl) - } else { - gasLimit = (uint64)(*args.Gas) - } - msg := evmtypes.NewMsgEthereumTx(nonce, args.To, amount, gasLimit, gasPrice, input) - - return &msg, nil -} diff --git a/rpc/namespaces/eth/api.go b/rpc/namespaces/eth/api.go new file mode 100644 index 000000000..71af7b6c3 --- /dev/null +++ b/rpc/namespaces/eth/api.go @@ -0,0 +1,890 @@ +package eth + +import ( + "bytes" + "context" + "errors" + "fmt" + "math/big" + "os" + "sync" + + "github.com/spf13/viper" + + "github.com/cosmos/ethermint/crypto/ethsecp256k1" + "github.com/cosmos/ethermint/crypto/hd" + "github.com/cosmos/ethermint/rpc/backend" + rpctypes "github.com/cosmos/ethermint/rpc/types" + ethermint "github.com/cosmos/ethermint/types" + "github.com/cosmos/ethermint/utils" + "github.com/cosmos/ethermint/version" + evmtypes "github.com/cosmos/ethermint/x/evm/types" + + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/crypto/merkle" + "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/rpc/client" + tmtypes "github.com/tendermint/tendermint/types" + + "github.com/ethereum/go-ethereum/accounts/keystore" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rlp" + + clientcontext "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/crypto/keys" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + authclient "github.com/cosmos/cosmos-sdk/x/auth/client/utils" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" +) + +// PublicEthereumAPI is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec. +type PublicEthereumAPI struct { + ctx context.Context + clientCtx clientcontext.CLIContext + chainIDEpoch *big.Int + logger log.Logger + backend backend.Backend + keys []ethsecp256k1.PrivKey // unlocked keys + nonceLock *rpctypes.AddrLocker + keyringLock sync.Mutex +} + +// NewAPI creates an instance of the public ETH Web3 API. +func NewAPI( + clientCtx clientcontext.CLIContext, backend backend.Backend, nonceLock *rpctypes.AddrLocker, + keys ...ethsecp256k1.PrivKey, +) *PublicEthereumAPI { + + epoch, err := ethermint.ParseChainID(clientCtx.ChainID) + if err != nil { + panic(err) + } + + api := &PublicEthereumAPI{ + ctx: context.Background(), + clientCtx: clientCtx, + chainIDEpoch: epoch, + logger: log.NewTMLogger(log.NewSyncWriter(os.Stdout)).With("module", "json-rpc", "namespace", "eth"), + backend: backend, + keys: keys, + nonceLock: nonceLock, + } + + if err := api.GetKeyringInfo(); err != nil { + api.logger.Error("failed to get keybase info", "error", err) + } + + return api +} + +// GetKeyringInfo checks if the keyring is present on the client context. If not, it creates a new +// instance and sets it to the client context for later usage. +func (api *PublicEthereumAPI) GetKeyringInfo() error { + api.keyringLock.Lock() + defer api.keyringLock.Unlock() + + if api.clientCtx.Keybase != nil { + return nil + } + + keybase, err := keys.NewKeyring( + sdk.KeyringServiceName(), + viper.GetString(flags.FlagKeyringBackend), + viper.GetString(flags.FlagHome), + api.clientCtx.Input, + hd.EthSecp256k1Options()..., + ) + if err != nil { + return err + } + + api.clientCtx.Keybase = keybase + return nil +} + +// ClientCtx returns the Cosmos SDK client context. +func (api *PublicEthereumAPI) ClientCtx() clientcontext.CLIContext { + return api.clientCtx +} + +// GetKeys returns the Cosmos SDK client context. +func (api *PublicEthereumAPI) GetKeys() []ethsecp256k1.PrivKey { + return api.keys +} + +// SetKeys sets the given key slice to the set of private keys +func (api *PublicEthereumAPI) SetKeys(keys []ethsecp256k1.PrivKey) { + api.keys = keys +} + +// ProtocolVersion returns the supported Ethereum protocol version. +func (api *PublicEthereumAPI) ProtocolVersion() hexutil.Uint { + api.logger.Debug("eth_protocolVersion") + return hexutil.Uint(version.ProtocolVersion) +} + +// ChainId returns the chain's identifier in hex format +func (api *PublicEthereumAPI) ChainId() (hexutil.Uint, error) { // nolint + api.logger.Debug("eth_chainId") + return hexutil.Uint(uint(api.chainIDEpoch.Uint64())), nil +} + +// Syncing returns whether or not the current node is syncing with other peers. Returns false if not, or a struct +// outlining the state of the sync if it is. +func (api *PublicEthereumAPI) Syncing() (interface{}, error) { + api.logger.Debug("eth_syncing") + + status, err := api.clientCtx.Client.Status() + if err != nil { + return false, err + } + + if !status.SyncInfo.CatchingUp { + return false, nil + } + + return map[string]interface{}{ + // "startingBlock": nil, // NA + "currentBlock": hexutil.Uint64(status.SyncInfo.LatestBlockHeight), + // "highestBlock": nil, // NA + // "pulledStates": nil, // NA + // "knownStates": nil, // NA + }, nil +} + +// Coinbase is the address that staking rewards will be send to (alias for Etherbase). +func (api *PublicEthereumAPI) Coinbase() (common.Address, error) { + api.logger.Debug("eth_coinbase") + + node, err := api.clientCtx.GetNode() + if err != nil { + return common.Address{}, err + } + + status, err := node.Status() + if err != nil { + return common.Address{}, err + } + + return common.BytesToAddress(status.ValidatorInfo.Address.Bytes()), nil +} + +// Mining returns whether or not this node is currently mining. Always false. +func (api *PublicEthereumAPI) Mining() bool { + api.logger.Debug("eth_mining") + return false +} + +// Hashrate returns the current node's hashrate. Always 0. +func (api *PublicEthereumAPI) Hashrate() hexutil.Uint64 { + api.logger.Debug("eth_hashrate") + return 0 +} + +// GasPrice returns the current gas price based on Ethermint's gas price oracle. +func (api *PublicEthereumAPI) GasPrice() *hexutil.Big { + api.logger.Debug("eth_gasPrice") + out := big.NewInt(0) + return (*hexutil.Big)(out) +} + +// Accounts returns the list of accounts available to this node. +func (api *PublicEthereumAPI) Accounts() ([]common.Address, error) { + api.logger.Debug("eth_accounts") + api.keyringLock.Lock() + + addresses := make([]common.Address, 0) // return [] instead of nil if empty + + infos, err := api.clientCtx.Keybase.List() + if err != nil { + return addresses, err + } + + api.keyringLock.Unlock() + + for _, info := range infos { + addressBytes := info.GetPubKey().Address().Bytes() + addresses = append(addresses, common.BytesToAddress(addressBytes)) + } + + return addresses, nil +} + +// rpctypes.BlockNumber returns the current block number. +func (api *PublicEthereumAPI) BlockNumber() (hexutil.Uint64, error) { + api.logger.Debug("eth_blockNumber") + return api.backend.BlockNumber() +} + +// GetBalance returns the provided account's balance up to the provided block number. +func (api *PublicEthereumAPI) GetBalance(address common.Address, blockNum rpctypes.BlockNumber) (*hexutil.Big, error) { + api.logger.Debug("eth_getBalance", "address", address, "block number", blockNum) + clientCtx := api.clientCtx.WithHeight(blockNum.Int64()) + res, _, err := clientCtx.QueryWithData(fmt.Sprintf("custom/%s/balance/%s", evmtypes.ModuleName, address.Hex()), nil) + if err != nil { + return nil, err + } + + var out evmtypes.QueryResBalance + api.clientCtx.Codec.MustUnmarshalJSON(res, &out) + val, err := utils.UnmarshalBigInt(out.Balance) + if err != nil { + return nil, err + } + + return (*hexutil.Big)(val), nil +} + +// GetStorageAt returns the contract storage at the given address, block number, and key. +func (api *PublicEthereumAPI) GetStorageAt(address common.Address, key string, blockNum rpctypes.BlockNumber) (hexutil.Bytes, error) { + api.logger.Debug("eth_getStorageAt", "address", address, "key", key, "block number", blockNum) + clientCtx := api.clientCtx.WithHeight(blockNum.Int64()) + res, _, err := clientCtx.QueryWithData(fmt.Sprintf("custom/%s/storage/%s/%s", evmtypes.ModuleName, address.Hex(), key), nil) + if err != nil { + return nil, err + } + + var out evmtypes.QueryResStorage + api.clientCtx.Codec.MustUnmarshalJSON(res, &out) + return out.Value, nil +} + +// GetTransactionCount returns the number of transactions at the given address up to the given block number. +func (api *PublicEthereumAPI) GetTransactionCount(address common.Address, blockNum rpctypes.BlockNumber) (*hexutil.Uint64, error) { + api.logger.Debug("eth_getTransactionCount", "address", address, "block number", blockNum) + clientCtx := api.clientCtx.WithHeight(blockNum.Int64()) + + // Get nonce (sequence) from account + from := sdk.AccAddress(address.Bytes()) + accRet := authtypes.NewAccountRetriever(clientCtx) + + err := accRet.EnsureExists(from) + if err != nil { + // account doesn't exist yet, return 0 + n := hexutil.Uint64(0) + return &n, nil + } + + _, nonce, err := accRet.GetAccountNumberSequence(from) + if err != nil { + return nil, err + } + + n := hexutil.Uint64(nonce) + return &n, nil +} + +// GetBlockTransactionCountByHash returns the number of transactions in the block identified by hash. +func (api *PublicEthereumAPI) GetBlockTransactionCountByHash(hash common.Hash) *hexutil.Uint { + api.logger.Debug("eth_getBlockTransactionCountByHash", "hash", hash) + res, _, err := api.clientCtx.Query(fmt.Sprintf("custom/%s/%s/%s", evmtypes.ModuleName, evmtypes.QueryHashToHeight, hash.Hex())) + if err != nil { + return nil + } + + var out evmtypes.QueryResBlockNumber + if err := api.clientCtx.Codec.UnmarshalJSON(res, &out); err != nil { + return nil + } + + resBlock, err := api.clientCtx.Client.Block(&out.Number) + if err != nil { + return nil + } + + n := hexutil.Uint(len(resBlock.Block.Txs)) + return &n +} + +// GetBlockTransactionCountByNumber returns the number of transactions in the block identified by number. +func (api *PublicEthereumAPI) GetBlockTransactionCountByNumber(blockNum rpctypes.BlockNumber) *hexutil.Uint { + api.logger.Debug("eth_getBlockTransactionCountByNumber", "block number", blockNum) + height := blockNum.Int64() + resBlock, err := api.clientCtx.Client.Block(&height) + if err != nil { + return nil + } + + n := hexutil.Uint(len(resBlock.Block.Txs)) + return &n +} + +// GetUncleCountByBlockHash returns the number of uncles in the block idenfied by hash. Always zero. +func (api *PublicEthereumAPI) GetUncleCountByBlockHash(_ common.Hash) hexutil.Uint { + return 0 +} + +// GetUncleCountByBlockNumber returns the number of uncles in the block idenfied by number. Always zero. +func (api *PublicEthereumAPI) GetUncleCountByBlockNumber(_ rpctypes.BlockNumber) hexutil.Uint { + return 0 +} + +// GetCode returns the contract code at the given address and block number. +func (api *PublicEthereumAPI) GetCode(address common.Address, blockNumber rpctypes.BlockNumber) (hexutil.Bytes, error) { + api.logger.Debug("eth_getCode", "address", address, "block number", blockNumber) + clientCtx := api.clientCtx.WithHeight(blockNumber.Int64()) + res, _, err := clientCtx.QueryWithData(fmt.Sprintf("custom/%s/%s/%s", evmtypes.ModuleName, evmtypes.QueryCode, address.Hex()), nil) + if err != nil { + return nil, err + } + + var out evmtypes.QueryResCode + api.clientCtx.Codec.MustUnmarshalJSON(res, &out) + return out.Code, nil +} + +// GetTransactionLogs returns the logs given a transaction hash. +func (api *PublicEthereumAPI) GetTransactionLogs(txHash common.Hash) ([]*ethtypes.Log, error) { + api.logger.Debug("eth_getTransactionLogs", "hash", txHash) + return api.backend.GetTransactionLogs(txHash) +} + +// Sign signs the provided data using the private key of address via Geth's signature standard. +func (api *PublicEthereumAPI) Sign(address common.Address, data hexutil.Bytes) (hexutil.Bytes, error) { + api.logger.Debug("eth_sign", "address", address, "data", data) + // TODO: Change this functionality to find an unlocked account by address + + key, exist := rpctypes.GetKeyByAddress(api.keys, address) + if !exist { + return nil, keystore.ErrLocked + } + + // Sign the requested hash with the wallet + signature, err := key.Sign(data) + if err != nil { + return nil, err + } + + signature[64] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper + return signature, nil +} + +// SendTransaction sends an Ethereum transaction. +func (api *PublicEthereumAPI) SendTransaction(args rpctypes.SendTxArgs) (common.Hash, error) { + api.logger.Debug("eth_sendTransaction", "args", args) + // TODO: Change this functionality to find an unlocked account by address + + key, exist := rpctypes.GetKeyByAddress(api.keys, args.From) + if !exist { + api.logger.Debug("failed to find key in keyring", "key", args.From) + return common.Hash{}, keystore.ErrLocked + } + + // Mutex lock the address' nonce to avoid assigning it to multiple requests + if args.Nonce == nil { + api.nonceLock.LockAddr(args.From) + defer api.nonceLock.UnlockAddr(args.From) + } + + // Assemble transaction from fields + tx, err := api.generateFromArgs(args) + if err != nil { + api.logger.Debug("failed to generate tx", "error", err) + return common.Hash{}, err + } + + // Sign transaction + if err := tx.Sign(api.chainIDEpoch, key.ToECDSA()); err != nil { + api.logger.Debug("failed to sign tx", "error", err) + return common.Hash{}, err + } + + // Encode transaction by default Tx encoder + txEncoder := authclient.GetTxEncoder(api.clientCtx.Codec) + txBytes, err := txEncoder(tx) + if err != nil { + return common.Hash{}, err + } + + // Broadcast transaction in sync mode (default) + // NOTE: If error is encountered on the node, the broadcast will not return an error + res, err := api.clientCtx.BroadcastTx(txBytes) + if err != nil { + return common.Hash{}, err + } + + // Return transaction hash + return common.HexToHash(res.TxHash), nil +} + +// SendRawTransaction send a raw Ethereum transaction. +func (api *PublicEthereumAPI) SendRawTransaction(data hexutil.Bytes) (common.Hash, error) { + api.logger.Debug("eth_sendRawTransaction", "data", data) + tx := new(evmtypes.MsgEthereumTx) + + // RLP decode raw transaction bytes + if err := rlp.DecodeBytes(data, tx); err != nil { + // Return nil is for when gasLimit overflows uint64 + return common.Hash{}, nil + } + + // Encode transaction by default Tx encoder + txEncoder := authclient.GetTxEncoder(api.clientCtx.Codec) + txBytes, err := txEncoder(tx) + if err != nil { + return common.Hash{}, err + } + + // TODO: Possibly log the contract creation address (if recipient address is nil) or tx data + // If error is encountered on the node, the broadcast will not return an error + res, err := api.clientCtx.BroadcastTx(txBytes) + if err != nil { + return common.Hash{}, err + } + + // Return transaction hash + return common.HexToHash(res.TxHash), nil +} + +// Call performs a raw contract call. +func (api *PublicEthereumAPI) Call(args rpctypes.CallArgs, blockNr rpctypes.BlockNumber, _ *map[common.Address]rpctypes.Account) (hexutil.Bytes, error) { + api.logger.Debug("eth_call", "args", args, "block number", blockNr) + simRes, err := api.doCall(args, blockNr, big.NewInt(ethermint.DefaultRPCGasLimit)) + if err != nil { + return []byte{}, err + } + + data, err := evmtypes.DecodeResultData(simRes.Result.Data) + if err != nil { + return []byte{}, err + } + + return (hexutil.Bytes)(data.Ret), nil +} + +// DoCall performs a simulated call operation through the evmtypes. It returns the +// estimated gas used on the operation or an error if fails. +func (api *PublicEthereumAPI) doCall( + args rpctypes.CallArgs, blockNr rpctypes.BlockNumber, globalGasCap *big.Int, +) (*sdk.SimulationResponse, error) { + // Set height for historical queries + clientCtx := api.clientCtx + + if blockNr.Int64() != 0 { + clientCtx = api.clientCtx.WithHeight(blockNr.Int64()) + } + + // Set sender address or use a default if none specified + var addr common.Address + + if args.From == nil { + addrs, err := api.Accounts() + if err == nil && len(addrs) > 0 { + addr = addrs[0] + } + } else { + addr = *args.From + } + + // Set default gas & gas price if none were set + // Change this to uint64(math.MaxUint64 / 2) if gas cap can be configured + gas := uint64(ethermint.DefaultRPCGasLimit) + if args.Gas != nil { + gas = uint64(*args.Gas) + } + if globalGasCap != nil && globalGasCap.Uint64() < gas { + api.logger.Debug("Caller gas above allowance, capping", "requested", gas, "cap", globalGasCap) + gas = globalGasCap.Uint64() + } + + // Set gas price using default or parameter if passed in + gasPrice := new(big.Int).SetUint64(ethermint.DefaultGasPrice) + if args.GasPrice != nil { + gasPrice = args.GasPrice.ToInt() + } + + // Set value for transaction + value := new(big.Int) + if args.Value != nil { + value = args.Value.ToInt() + } + + // Set Data if provided + var data []byte + if args.Data != nil { + data = []byte(*args.Data) + } + + // Set destination address for call + var toAddr sdk.AccAddress + if args.To != nil { + toAddr = sdk.AccAddress(args.To.Bytes()) + } + + // Create new call message + msg := evmtypes.NewMsgEthermint(0, &toAddr, sdk.NewIntFromBigInt(value), gas, + sdk.NewIntFromBigInt(gasPrice), data, sdk.AccAddress(addr.Bytes())) + + if err := msg.ValidateBasic(); err != nil { + return nil, err + } + + // Generate tx to be used to simulate (signature isn't needed) + var stdSig authtypes.StdSignature + tx := authtypes.NewStdTx([]sdk.Msg{msg}, authtypes.StdFee{}, []authtypes.StdSignature{stdSig}, "") + + txEncoder := authclient.GetTxEncoder(clientCtx.Codec) + txBytes, err := txEncoder(tx) + if err != nil { + return nil, err + } + + // Transaction simulation through query + res, _, err := clientCtx.QueryWithData("app/simulate", txBytes) + if err != nil { + return nil, err + } + + var simResponse sdk.SimulationResponse + if err := clientCtx.Codec.UnmarshalBinaryBare(res, &simResponse); err != nil { + return nil, err + } + + return &simResponse, nil +} + +// EstimateGas returns an estimate of gas usage for the given smart contract call. +// It adds 1,000 gas to the returned value instead of using the gas adjustment +// param from the SDK. +func (api *PublicEthereumAPI) EstimateGas(args rpctypes.CallArgs) (hexutil.Uint64, error) { + api.logger.Debug("eth_estimateGas", "args", args) + simResponse, err := api.doCall(args, 0, big.NewInt(ethermint.DefaultRPCGasLimit)) + if err != nil { + return 0, err + } + + // TODO: change 1000 buffer for more accurate buffer (eg: SDK's gasAdjusted) + estimatedGas := simResponse.GasInfo.GasUsed + gas := estimatedGas + 1000 + + return hexutil.Uint64(gas), nil +} + +// GetBlockByHash returns the block identified by hash. +func (api *PublicEthereumAPI) GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) { + api.logger.Debug("eth_getBlockByHash", "hash", hash, "full", fullTx) + return api.backend.GetBlockByHash(hash, fullTx) +} + +// GetBlockByNumber returns the block identified by number. +func (api *PublicEthereumAPI) GetBlockByNumber(blockNum rpctypes.BlockNumber, fullTx bool) (map[string]interface{}, error) { + api.logger.Debug("eth_getBlockByNumber", "number", blockNum, "full", fullTx) + return api.backend.GetBlockByNumber(blockNum, fullTx) +} + +// GetTransactionByHash returns the transaction identified by hash. +func (api *PublicEthereumAPI) GetTransactionByHash(hash common.Hash) (*rpctypes.Transaction, error) { + api.logger.Debug("eth_getTransactionByHash", "hash", hash) + tx, err := api.clientCtx.Client.Tx(hash.Bytes(), false) + if err != nil { + // Return nil for transaction when not found + return nil, nil + } + + // Can either cache or just leave this out if not necessary + block, err := api.clientCtx.Client.Block(&tx.Height) + if err != nil { + return nil, err + } + + blockHash := common.BytesToHash(block.Block.Header.Hash()) + + ethTx, err := rpctypes.RawTxToEthTx(api.clientCtx, tx.Tx) + if err != nil { + return nil, err + } + + height := uint64(tx.Height) + return rpctypes.NewTransaction(ethTx, common.BytesToHash(tx.Tx.Hash()), blockHash, height, uint64(tx.Index)) +} + +// GetTransactionByBlockHashAndIndex returns the transaction identified by hash and index. +func (api *PublicEthereumAPI) GetTransactionByBlockHashAndIndex(hash common.Hash, idx hexutil.Uint) (*rpctypes.Transaction, error) { + api.logger.Debug("eth_getTransactionByHashAndIndex", "hash", hash, "index", idx) + res, _, err := api.clientCtx.Query(fmt.Sprintf("custom/%s/%s/%s", evmtypes.ModuleName, evmtypes.QueryHashToHeight, hash.Hex())) + if err != nil { + return nil, err + } + + var out evmtypes.QueryResBlockNumber + api.clientCtx.Codec.MustUnmarshalJSON(res, &out) + + resBlock, err := api.clientCtx.Client.Block(&out.Number) + if err != nil { + return nil, err + } + + return api.getTransactionByBlockAndIndex(resBlock.Block, idx) +} + +// GetTransactionByBlockNumberAndIndex returns the transaction identified by number and index. +func (api *PublicEthereumAPI) GetTransactionByBlockNumberAndIndex(blockNum rpctypes.BlockNumber, idx hexutil.Uint) (*rpctypes.Transaction, error) { + api.logger.Debug("eth_getTransactionByBlockNumberAndIndex", "number", blockNum, "index", idx) + height := blockNum.Int64() + resBlock, err := api.clientCtx.Client.Block(&height) + if err != nil { + return nil, err + } + + return api.getTransactionByBlockAndIndex(resBlock.Block, idx) +} + +func (api *PublicEthereumAPI) getTransactionByBlockAndIndex(block *tmtypes.Block, idx hexutil.Uint) (*rpctypes.Transaction, error) { + // return if index out of bounds + if uint64(idx) >= uint64(len(block.Txs)) { + return nil, nil + } + + ethTx, err := rpctypes.RawTxToEthTx(api.clientCtx, block.Txs[idx]) + if err != nil { + // return nil error if the transaction is not a MsgEthereumTx + return nil, nil + } + + height := uint64(block.Height) + txHash := common.BytesToHash(block.Txs[idx].Hash()) + blockHash := common.BytesToHash(block.Header.Hash()) + return rpctypes.NewTransaction(ethTx, txHash, blockHash, height, uint64(idx)) +} + +// GetTransactionReceipt returns the transaction receipt identified by hash. +func (api *PublicEthereumAPI) GetTransactionReceipt(hash common.Hash) (map[string]interface{}, error) { + api.logger.Debug("eth_getTransactionReceipt", "hash", hash) + tx, err := api.clientCtx.Client.Tx(hash.Bytes(), false) + if err != nil { + // Return nil for transaction when not found + return nil, nil + } + + // Query block for consensus hash + block, err := api.clientCtx.Client.Block(&tx.Height) + if err != nil { + return nil, err + } + + blockHash := common.BytesToHash(block.Block.Header.Hash()) + + // Convert tx bytes to eth transaction + ethTx, err := rpctypes.RawTxToEthTx(api.clientCtx, tx.Tx) + if err != nil { + return nil, err + } + + from, err := ethTx.VerifySig(ethTx.ChainID()) + if err != nil { + return nil, err + } + + // Set status codes based on tx result + var status hexutil.Uint + if tx.TxResult.IsOK() { + status = hexutil.Uint(1) + } else { + status = hexutil.Uint(0) + } + + txData := tx.TxResult.GetData() + + data, err := evmtypes.DecodeResultData(txData) + if err != nil { + status = 0 // transaction failed + } + + if len(data.Logs) == 0 { + data.Logs = []*ethtypes.Log{} + } + + receipt := map[string]interface{}{ + // Consensus fields: These fields are defined by the Yellow Paper + "status": status, + "cumulativeGasUsed": nil, // ignore until needed + "logsBloom": data.Bloom, + "logs": data.Logs, + + // Implementation fields: These fields are added by geth when processing a transaction. + // They are stored in the chain database. + "transactionHash": hash, + "contractAddress": data.ContractAddress, + "gasUsed": hexutil.Uint64(tx.TxResult.GasUsed), + + // Inclusion information: These fields provide information about the inclusion of the + // transaction corresponding to this receipt. + "blockHash": blockHash, + "blockNumber": hexutil.Uint64(tx.Height), + "transactionIndex": hexutil.Uint64(tx.Index), + + // sender and receiver (contract or EOA) addreses + "from": from, + "to": ethTx.To(), + } + + return receipt, nil +} + +// PendingTransactions returns the transactions that are in the transaction pool +// and have a from address that is one of the accounts this node manages. +func (api *PublicEthereumAPI) PendingTransactions() ([]*rpctypes.Transaction, error) { + api.logger.Debug("eth_getPendingTransactions") + return api.backend.PendingTransactions() +} + +// GetUncleByBlockHashAndIndex returns the uncle identified by hash and index. Always returns nil. +func (api *PublicEthereumAPI) GetUncleByBlockHashAndIndex(hash common.Hash, idx hexutil.Uint) map[string]interface{} { + return nil +} + +// GetUncleByBlockNumberAndIndex returns the uncle identified by number and index. Always returns nil. +func (api *PublicEthereumAPI) GetUncleByBlockNumberAndIndex(number hexutil.Uint, idx hexutil.Uint) map[string]interface{} { + return nil +} + +// GetProof returns an account object with proof and any storage proofs +func (api *PublicEthereumAPI) GetProof(address common.Address, storageKeys []string, block rpctypes.BlockNumber) (*rpctypes.AccountResult, error) { + api.logger.Debug("eth_getProof", "address", address, "keys", storageKeys, "number", block) + + clientCtx := api.clientCtx.WithHeight(int64(block)) + path := fmt.Sprintf("custom/%s/%s/%s", evmtypes.ModuleName, evmtypes.QueryAccount, address.Hex()) + + // query eth account at block height + resBz, _, err := clientCtx.Query(path) + if err != nil { + return nil, err + } + + var account evmtypes.QueryResAccount + clientCtx.Codec.MustUnmarshalJSON(resBz, &account) + + storageProofs := make([]rpctypes.StorageResult, len(storageKeys)) + opts := client.ABCIQueryOptions{Height: int64(block), Prove: true} + for i, k := range storageKeys { + // Get value for key + vPath := fmt.Sprintf("custom/%s/%s/%s/%s", evmtypes.ModuleName, evmtypes.QueryStorage, address, k) + vRes, err := api.clientCtx.Client.ABCIQueryWithOptions(vPath, nil, opts) + if err != nil { + return nil, err + } + + var value evmtypes.QueryResStorage + clientCtx.Codec.MustUnmarshalJSON(vRes.Response.GetValue(), &value) + + // check for proof + proof := vRes.Response.GetProof() + proofStr := new(merkle.Proof).String() + if proof != nil { + proofStr = proof.String() + } + + storageProofs[i] = rpctypes.StorageResult{ + Key: k, + Value: (*hexutil.Big)(common.BytesToHash(value.Value).Big()), + Proof: []string{proofStr}, + } + } + + req := abci.RequestQuery{ + Path: fmt.Sprintf("store/%s/key", auth.StoreKey), + Data: auth.AddressStoreKey(sdk.AccAddress(address.Bytes())), + Height: int64(block), + Prove: true, + } + + res, err := clientCtx.QueryABCI(req) + if err != nil { + return nil, err + } + + // check for proof + accountProof := res.GetProof() + accProofStr := new(merkle.Proof).String() + if accountProof != nil { + accProofStr = accountProof.String() + } + + return &rpctypes.AccountResult{ + Address: address, + AccountProof: []string{accProofStr}, + Balance: (*hexutil.Big)(utils.MustUnmarshalBigInt(account.Balance)), + CodeHash: common.BytesToHash(account.CodeHash), + Nonce: hexutil.Uint64(account.Nonce), + StorageHash: common.Hash{}, // Ethermint doesn't have a storage hash + StorageProof: storageProofs, + }, nil +} + +// generateFromArgs populates tx message with args (used in RPC API) +func (api *PublicEthereumAPI) generateFromArgs(args rpctypes.SendTxArgs) (*evmtypes.MsgEthereumTx, error) { + var ( + nonce uint64 + gasLimit uint64 + err error + ) + + amount := (*big.Int)(args.Value) + gasPrice := (*big.Int)(args.GasPrice) + + if args.GasPrice == nil { + + // Set default gas price + // TODO: Change to min gas price from context once available through server/daemon + gasPrice = big.NewInt(ethermint.DefaultGasPrice) + } + + if args.Nonce == nil { + // Get nonce (sequence) from account + from := sdk.AccAddress(args.From.Bytes()) + accRet := authtypes.NewAccountRetriever(api.clientCtx) + + if api.clientCtx.Keybase == nil { + return nil, fmt.Errorf("clientCtx.Keybase is nil") + } + + _, nonce, err = accRet.GetAccountNumberSequence(from) + if err != nil { + return nil, err + } + } else { + nonce = (uint64)(*args.Nonce) + } + + if args.Data != nil && args.Input != nil && !bytes.Equal(*args.Data, *args.Input) { + return nil, errors.New(`both "data" and "input" are set and not equal. Please use "input" to pass transaction call data`) + } + + // Sets input to either Input or Data, if both are set and not equal error above returns + var input []byte + if args.Input != nil { + input = *args.Input + } else if args.Data != nil { + input = *args.Data + } + + if args.To == nil && len(input) == 0 { + // Contract creation + return nil, fmt.Errorf("contract creation without any data provided") + } + + if args.Gas == nil { + callArgs := rpctypes.CallArgs{ + From: &args.From, + To: args.To, + Gas: args.Gas, + GasPrice: args.GasPrice, + Value: args.Value, + Data: args.Data, + } + gl, err := api.EstimateGas(callArgs) + if err != nil { + return nil, err + } + gasLimit = uint64(gl) + } else { + gasLimit = (uint64)(*args.Gas) + } + msg := evmtypes.NewMsgEthereumTx(nonce, args.To, amount, gasLimit, gasPrice, input) + + return &msg, nil +} diff --git a/rpc/filter_api.go b/rpc/namespaces/eth/filters/api.go similarity index 90% rename from rpc/filter_api.go rename to rpc/namespaces/eth/filters/api.go index 6d2fdc845..9e7017634 100644 --- a/rpc/filter_api.go +++ b/rpc/namespaces/eth/filters/api.go @@ -1,4 +1,4 @@ -package rpc +package filters import ( "context" @@ -16,13 +16,14 @@ import ( clientcontext "github.com/cosmos/cosmos-sdk/client/context" + rpctypes "github.com/cosmos/ethermint/rpc/types" evmtypes "github.com/cosmos/ethermint/x/evm/types" ) -// FiltersBackend defines the methods requided by the PublicFilterAPI backend -type FiltersBackend interface { - GetBlockByNumber(blockNum BlockNumber, fullTx bool) (map[string]interface{}, error) - HeaderByNumber(blockNr BlockNumber) (*ethtypes.Header, error) +// Backend defines the methods requided by the PublicFilterAPI backend +type Backend interface { + GetBlockByNumber(blockNum rpctypes.BlockNumber, fullTx bool) (map[string]interface{}, error) + HeaderByNumber(blockNr rpctypes.BlockNumber) (*ethtypes.Header, error) HeaderByHash(blockHash common.Hash) (*ethtypes.Header, error) GetLogs(blockHash common.Hash) ([][]*ethtypes.Log, error) @@ -47,26 +48,26 @@ type filter struct { // PublicFilterAPI offers support to create and manage filters. This will allow external clients to retrieve various // information related to the Ethereum protocol such as blocks, transactions and logs. type PublicFilterAPI struct { - cliCtx clientcontext.CLIContext - backend FiltersBackend + clientCtx clientcontext.CLIContext + backend Backend events *EventSystem filtersMu sync.Mutex filters map[rpc.ID]*filter } -// NewPublicFilterAPI returns a new PublicFilterAPI instance. -func NewPublicFilterAPI(cliCtx clientcontext.CLIContext, backend FiltersBackend) *PublicFilterAPI { +// NewAPI returns a new PublicFilterAPI instance. +func NewAPI(clientCtx clientcontext.CLIContext, backend Backend) *PublicFilterAPI { // start the client to subscribe to Tendermint events - err := cliCtx.Client.Start() + err := clientCtx.Client.Start() if err != nil { panic(err) } api := &PublicFilterAPI{ - cliCtx: cliCtx, - backend: backend, - filters: make(map[rpc.ID]*filter), - events: NewEventSystem(cliCtx.Client), + clientCtx: clientCtx, + backend: backend, + filters: make(map[rpc.ID]*filter), + events: NewEventSystem(clientCtx.Client), } go api.timeoutLoop() @@ -209,7 +210,7 @@ func (api *PublicFilterAPI) NewBlockFilter() rpc.ID { select { case ev := <-headersCh: data, _ := ev.Data.(tmtypes.EventDataNewBlockHeader) - header := EthHeaderFromTendermint(data.Header) + header := rpctypes.EthHeaderFromTendermint(data.Header) api.filtersMu.Lock() if f, found := api.filters[headerSub.ID()]; found { f.hashes = append(f.hashes, header.Hash()) @@ -255,7 +256,7 @@ func (api *PublicFilterAPI) NewHeads(ctx context.Context) (*rpc.Subscription, er return } - header := EthHeaderFromTendermint(data.Header) + header := rpctypes.EthHeaderFromTendermint(data.Header) err = notifier.Notify(rpcSub.ID, header) if err != nil { headersSub.err <- err @@ -317,7 +318,7 @@ func (api *PublicFilterAPI) Logs(ctx context.Context, crit filters.FilterCriteri return } - logs := filterLogs(resultData.Logs, crit.FromBlock, crit.ToBlock, crit.Addresses, crit.Topics) + logs := FilterLogs(resultData.Logs, crit.FromBlock, crit.ToBlock, crit.Addresses, crit.Topics) for _, log := range logs { err = notifier.Notify(rpcSub.ID, log) @@ -386,7 +387,7 @@ func (api *PublicFilterAPI) NewFilter(criteria filters.FilterCriteria) (rpc.ID, return } - logs := filterLogs(resultData.Logs, criteria.FromBlock, criteria.ToBlock, criteria.Addresses, criteria.Topics) + logs := FilterLogs(resultData.Logs, criteria.FromBlock, criteria.ToBlock, criteria.Addresses, criteria.Topics) api.filtersMu.Lock() if f, found := api.filters[filterID]; found { @@ -407,7 +408,7 @@ func (api *PublicFilterAPI) NewFilter(criteria filters.FilterCriteria) (rpc.ID, // GetLogs returns logs matching the given argument that are stored within the state. // -// https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getlogs +// https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getLogs func (api *PublicFilterAPI) GetLogs(ctx context.Context, crit filters.FilterCriteria) ([]*ethtypes.Log, error) { var filter *Filter if crit.BlockHash != nil { @@ -433,7 +434,7 @@ func (api *PublicFilterAPI) GetLogs(ctx context.Context, crit filters.FilterCrit return nil, err } - return returnLogs(logs), err + return returnLogs(logs), nil } // UninstallFilter removes the filter with the given filter id. @@ -533,21 +534,3 @@ func (api *PublicFilterAPI) GetFilterChanges(id rpc.ID) (interface{}, error) { return nil, fmt.Errorf("invalid filter %s type %d", id, f.typ) } } - -// returnHashes is a helper that will return an empty hash array case the given hash array is nil, -// otherwise the given hashes array is returned. -func returnHashes(hashes []common.Hash) []common.Hash { - if hashes == nil { - return []common.Hash{} - } - return hashes -} - -// returnLogs is a helper that will return an empty log array in case the given logs array is nil, -// otherwise the given logs array is returned. -func returnLogs(logs []*ethtypes.Log) []*ethtypes.Log { - if logs == nil { - return []*ethtypes.Log{} - } - return logs -} diff --git a/rpc/filter_system.go b/rpc/namespaces/eth/filters/filter_system.go similarity index 88% rename from rpc/filter_system.go rename to rpc/namespaces/eth/filters/filter_system.go index 00e7aa96b..4f308e786 100644 --- a/rpc/filter_system.go +++ b/rpc/namespaces/eth/filters/filter_system.go @@ -1,9 +1,8 @@ -package rpc +package filters import ( "context" "fmt" - "log" "time" tmquery "github.com/tendermint/tendermint/libs/pubsub/query" @@ -18,6 +17,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" + rpctypes "github.com/cosmos/ethermint/rpc/types" evmtypes "github.com/cosmos/ethermint/x/evm/types" ) @@ -262,7 +262,7 @@ func (es *EventSystem) handleLogs(ev coretypes.ResultEvent) { return } for _, f := range es.index[filters.LogsSubscription] { - matchedLogs := filterLogs(resultData.Logs, f.logsCrit.FromBlock, f.logsCrit.ToBlock, f.logsCrit.Addresses, f.logsCrit.Topics) + matchedLogs := FilterLogs(resultData.Logs, f.logsCrit.FromBlock, f.logsCrit.ToBlock, f.logsCrit.Addresses, f.logsCrit.Topics) if len(matchedLogs) > 0 { f.logs <- matchedLogs } @@ -279,7 +279,7 @@ func (es *EventSystem) handleTxsEvent(ev coretypes.ResultEvent) { func (es *EventSystem) handleChainEvent(ev coretypes.ResultEvent) { data, _ := ev.Data.(tmtypes.EventDataNewBlockHeader) for _, f := range es.index[filters.BlocksSubscription] { - f.headers <- EthHeaderFromTendermint(data.Header) + f.headers <- rpctypes.EthHeaderFromTendermint(data.Header) } // TODO: light client } @@ -377,57 +377,3 @@ func (es *EventSystem) eventLoop() { } // }() } - -// Subscription defines a wrapper for the private subscription -type Subscription struct { - id rpc.ID - typ filters.Type - event string - created time.Time - logsCrit filters.FilterCriteria - logs chan []*ethtypes.Log - hashes chan []common.Hash - headers chan *ethtypes.Header - installed chan struct{} // closed when the filter is installed - eventCh <-chan coretypes.ResultEvent - err chan error -} - -// ID returns the underlying subscription RPC identifier. -func (s Subscription) ID() rpc.ID { - return s.id -} - -// Unsubscribe to the current subscription from Tendermint Websocket. It sends an error to the -// subscription error channel if unsubscription fails. -func (s *Subscription) Unsubscribe(es *EventSystem) { - if err := es.client.Unsubscribe(es.ctx, string(s.ID()), s.event); err != nil { - s.err <- err - } - - go func() { - defer func() { - log.Println("successfully unsubscribed to event", s.event) - }() - - uninstallLoop: - for { - // write uninstall request and consume logs/hashes. This prevents - // the eventLoop broadcast method to deadlock when writing to the - // filter event channel while the subscription loop is waiting for - // this method to return (and thus not reading these events). - select { - case es.uninstall <- s: - break uninstallLoop - case <-s.logs: - case <-s.hashes: - case <-s.headers: - } - } - }() -} - -// Err returns the error channel -func (s *Subscription) Err() <-chan error { - return s.err -} diff --git a/rpc/filters.go b/rpc/namespaces/eth/filters/filters.go similarity index 63% rename from rpc/filters.go rename to rpc/namespaces/eth/filters/filters.go index 2a94040dd..c361ef63a 100644 --- a/rpc/filters.go +++ b/rpc/namespaces/eth/filters/filters.go @@ -1,4 +1,4 @@ -package rpc +package filters import ( "context" @@ -9,25 +9,27 @@ import ( "github.com/ethereum/go-ethereum/core/bloombits" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/eth/filters" + + rpctypes "github.com/cosmos/ethermint/rpc/types" ) // Filter can be used to retrieve and filter logs. type Filter struct { - backend FiltersBackend + backend Backend criteria filters.FilterCriteria matcher *bloombits.Matcher } // NewBlockFilter creates a new filter which directly inspects the contents of // a block to figure out whether it is interesting or not. -func NewBlockFilter(backend FiltersBackend, criteria filters.FilterCriteria) *Filter { +func NewBlockFilter(backend Backend, criteria filters.FilterCriteria) *Filter { // Create a generic filter and convert it into a block filter return newFilter(backend, criteria, nil) } // NewRangeFilter creates a new filter which uses a bloom filter on blocks to // figure out whether a particular block is interesting or not. -func NewRangeFilter(backend FiltersBackend, begin, end int64, addresses []common.Address, topics [][]common.Hash) *Filter { +func NewRangeFilter(backend Backend, begin, end int64, addresses []common.Address, topics [][]common.Hash) *Filter { // Flatten the address and topic filter clauses into a single bloombits filter // system. Since the bloombits are not positional, nil topics are permitted, // which get flattened into a nil byte slice. @@ -62,7 +64,7 @@ func NewRangeFilter(backend FiltersBackend, begin, end int64, addresses []common } // newFilter returns a new Filter -func newFilter(backend FiltersBackend, criteria filters.FilterCriteria, matcher *bloombits.Matcher) *Filter { +func newFilter(backend Backend, criteria filters.FilterCriteria, matcher *bloombits.Matcher) *Filter { return &Filter{ backend: backend, criteria: criteria, @@ -89,7 +91,7 @@ func (f *Filter) Logs(_ context.Context) ([]*ethtypes.Log, error) { } // Figure out the limits of the filter range - header, err := f.backend.HeaderByNumber(LatestBlockNumber) + header, err := f.backend.HeaderByNumber(rpctypes.LatestBlockNumber) if err != nil { return nil, err } @@ -107,7 +109,7 @@ func (f *Filter) Logs(_ context.Context) ([]*ethtypes.Log, error) { } for i := f.criteria.FromBlock.Int64(); i <= f.criteria.ToBlock.Int64(); i++ { - block, err := f.backend.GetBlockByNumber(BlockNumber(i), true) + block, err := f.backend.GetBlockByNumber(rpctypes.BlockNumber(i), true) if err != nil { return logs, err } @@ -139,7 +141,7 @@ func (f *Filter) blockLogs(header *ethtypes.Header) ([]*ethtypes.Log, error) { for _, logs := range logsList { unfiltered = append(unfiltered, logs...) } - logs := filterLogs(unfiltered, nil, nil, f.criteria.Addresses, f.criteria.Topics) + logs := FilterLogs(unfiltered, nil, nil, f.criteria.Addresses, f.criteria.Topics) if len(logs) == 0 { return []*ethtypes.Log{}, nil } @@ -162,81 +164,5 @@ func (f *Filter) checkMatches(transactions []common.Hash) []*ethtypes.Log { unfiltered = append(unfiltered, logs...) } - return filterLogs(unfiltered, f.criteria.FromBlock, f.criteria.ToBlock, f.criteria.Addresses, f.criteria.Topics) -} - -// filterLogs creates a slice of logs matching the given criteria. -// [] -> anything -// [A] -> A in first position of log topics, anything after -// [null, B] -> anything in first position, B in second position -// [A, B] -> A in first position and B in second position -// [[A, B], [A, B]] -> A or B in first position, A or B in second position -func filterLogs(logs []*ethtypes.Log, fromBlock, toBlock *big.Int, addresses []common.Address, topics [][]common.Hash) []*ethtypes.Log { - var ret []*ethtypes.Log -Logs: - for _, log := range logs { - if fromBlock != nil && fromBlock.Int64() >= 0 && fromBlock.Uint64() > log.BlockNumber { - continue - } - if toBlock != nil && toBlock.Int64() >= 0 && toBlock.Uint64() < log.BlockNumber { - continue - } - if len(addresses) > 0 && !includes(addresses, log.Address) { - continue - } - // If the to filtered topics is greater than the amount of topics in logs, skip. - if len(topics) > len(log.Topics) { - continue - } - for i, sub := range topics { - match := len(sub) == 0 // empty rule set == wildcard - for _, topic := range sub { - if log.Topics[i] == topic { - match = true - break - } - } - if !match { - continue Logs - } - } - ret = append(ret, log) - } - return ret -} - -func includes(addresses []common.Address, a common.Address) bool { - for _, addr := range addresses { - if addr == a { - return true - } - } - - return false -} - -func bloomFilter(bloom ethtypes.Bloom, addresses []common.Address, topics [][]common.Hash) bool { - var included bool - if len(addresses) > 0 { - for _, addr := range addresses { - if ethtypes.BloomLookup(bloom, addr) { - included = true - break - } - } - if !included { - return false - } - } - - for _, sub := range topics { - included = len(sub) == 0 // empty rule set == wildcard - for _, topic := range sub { - if ethtypes.BloomLookup(bloom, topic) { - included = true - break - } - } - } - return included + return FilterLogs(unfiltered, f.criteria.FromBlock, f.criteria.ToBlock, f.criteria.Addresses, f.criteria.Topics) } diff --git a/rpc/namespaces/eth/filters/subscription.go b/rpc/namespaces/eth/filters/subscription.go new file mode 100644 index 000000000..ee09f6ebe --- /dev/null +++ b/rpc/namespaces/eth/filters/subscription.go @@ -0,0 +1,71 @@ +package filters + +import ( + "log" + "time" + + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/eth/filters" + "github.com/ethereum/go-ethereum/rpc" + coretypes "github.com/tendermint/tendermint/rpc/core/types" +) + +// Subscription defines a wrapper for the private subscription +type Subscription struct { + id rpc.ID + typ filters.Type + event string + created time.Time + logsCrit filters.FilterCriteria + logs chan []*ethtypes.Log + hashes chan []common.Hash + headers chan *ethtypes.Header + installed chan struct{} // closed when the filter is installed + eventCh <-chan coretypes.ResultEvent + err chan error +} + +// ID returns the underlying subscription RPC identifier. +func (s Subscription) ID() rpc.ID { + return s.id +} + +// Unsubscribe to the current subscription from Tendermint Websocket. It sends an error to the +// subscription error channel if unsubscription fails. +func (s *Subscription) Unsubscribe(es *EventSystem) { + if err := es.client.Unsubscribe(es.ctx, string(s.ID()), s.event); err != nil { + s.err <- err + } + + go func() { + defer func() { + log.Println("successfully unsubscribed to event", s.event) + }() + + uninstallLoop: + for { + // write uninstall request and consume logs/hashes. This prevents + // the eventLoop broadcast method to deadlock when writing to the + // filter event channel while the subscription loop is waiting for + // this method to return (and thus not reading these events). + select { + case es.uninstall <- s: + break uninstallLoop + case <-s.logs: + case <-s.hashes: + case <-s.headers: + } + } + }() +} + +// Err returns the error channel +func (s *Subscription) Err() <-chan error { + return s.err +} + +// Event returns the tendermint result event channel +func (s *Subscription) Event() <-chan coretypes.ResultEvent { + return s.eventCh +} diff --git a/rpc/namespaces/eth/filters/utils.go b/rpc/namespaces/eth/filters/utils.go new file mode 100644 index 000000000..fd65181ee --- /dev/null +++ b/rpc/namespaces/eth/filters/utils.go @@ -0,0 +1,102 @@ +package filters + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" +) + +// filterLogs creates a slice of logs matching the given criteria. +// [] -> anything +// [A] -> A in first position of log topics, anything after +// [null, B] -> anything in first position, B in second position +// [A, B] -> A in first position and B in second position +// [[A, B], [A, B]] -> A or B in first position, A or B in second position +func FilterLogs(logs []*ethtypes.Log, fromBlock, toBlock *big.Int, addresses []common.Address, topics [][]common.Hash) []*ethtypes.Log { + var ret []*ethtypes.Log +Logs: + for _, log := range logs { + if fromBlock != nil && fromBlock.Int64() >= 0 && fromBlock.Uint64() > log.BlockNumber { + continue + } + if toBlock != nil && toBlock.Int64() >= 0 && toBlock.Uint64() < log.BlockNumber { + continue + } + if len(addresses) > 0 && !includes(addresses, log.Address) { + continue + } + // If the to filtered topics is greater than the amount of topics in logs, skip. + if len(topics) > len(log.Topics) { + continue + } + for i, sub := range topics { + match := len(sub) == 0 // empty rule set == wildcard + for _, topic := range sub { + if log.Topics[i] == topic { + match = true + break + } + } + if !match { + continue Logs + } + } + ret = append(ret, log) + } + return ret +} + +func includes(addresses []common.Address, a common.Address) bool { + for _, addr := range addresses { + if addr == a { + return true + } + } + + return false +} + +func bloomFilter(bloom ethtypes.Bloom, addresses []common.Address, topics [][]common.Hash) bool { + var included bool + if len(addresses) > 0 { + for _, addr := range addresses { + if ethtypes.BloomLookup(bloom, addr) { + included = true + break + } + } + if !included { + return false + } + } + + for _, sub := range topics { + included = len(sub) == 0 // empty rule set == wildcard + for _, topic := range sub { + if ethtypes.BloomLookup(bloom, topic) { + included = true + break + } + } + } + return included +} + +// returnHashes is a helper that will return an empty hash array case the given hash array is nil, +// otherwise the given hashes array is returned. +func returnHashes(hashes []common.Hash) []common.Hash { + if hashes == nil { + return []common.Hash{} + } + return hashes +} + +// returnLogs is a helper that will return an empty log array in case the given logs array is nil, +// otherwise the given logs array is returned. +func returnLogs(logs []*ethtypes.Log) []*ethtypes.Log { + if logs == nil { + return []*ethtypes.Log{} + } + return logs +} diff --git a/rpc/net_api.go b/rpc/namespaces/net/api.go similarity index 53% rename from rpc/net_api.go rename to rpc/namespaces/net/api.go index 2d0659305..64d4c5289 100644 --- a/rpc/net_api.go +++ b/rpc/namespaces/net/api.go @@ -1,12 +1,9 @@ -package rpc +package net import ( "fmt" - "github.com/spf13/viper" - "github.com/cosmos/cosmos-sdk/client/context" - "github.com/cosmos/cosmos-sdk/client/flags" ethermint "github.com/cosmos/ethermint/types" ) @@ -15,11 +12,10 @@ type PublicNetAPI struct { networkVersion uint64 } -// NewPublicNetAPI creates an instance of the public Net Web3 API. -func NewPublicNetAPI(_ context.CLIContext) *PublicNetAPI { - chainID := viper.GetString(flags.FlagChainID) +// NewAPI creates an instance of the public Net Web3 API. +func NewAPI(clientCtx context.CLIContext) *PublicNetAPI { // parse the chainID from a integer string - chainIDEpoch, err := ethermint.ParseChainID(chainID) + chainIDEpoch, err := ethermint.ParseChainID(clientCtx.ChainID) if err != nil { panic(err) } @@ -30,6 +26,6 @@ func NewPublicNetAPI(_ context.CLIContext) *PublicNetAPI { } // Version returns the current ethereum protocol version. -func (s *PublicNetAPI) Version() string { - return fmt.Sprintf("%d", s.networkVersion) +func (api *PublicNetAPI) Version() string { + return fmt.Sprintf("%d", api.networkVersion) } diff --git a/rpc/personal_api.go b/rpc/namespaces/personal/api.go similarity index 56% rename from rpc/personal_api.go rename to rpc/namespaces/personal/api.go index d3bcfcb3d..f78d74092 100644 --- a/rpc/personal_api.go +++ b/rpc/namespaces/personal/api.go @@ -1,4 +1,4 @@ -package rpc +package personal import ( "bytes" @@ -7,12 +7,10 @@ import ( "os" "time" - "github.com/spf13/viper" + "github.com/tendermint/tendermint/libs/log" - "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/crypto/keys" "github.com/cosmos/cosmos-sdk/crypto/keys/mintkey" - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" @@ -21,58 +19,43 @@ import ( "github.com/cosmos/ethermint/crypto/ethsecp256k1" "github.com/cosmos/ethermint/crypto/hd" - params "github.com/cosmos/ethermint/rpc/args" + "github.com/cosmos/ethermint/rpc/namespaces/eth" + rpctypes "github.com/cosmos/ethermint/rpc/types" ) -// PersonalEthAPI is the personal_ prefixed set of APIs in the Web3 JSON-RPC spec. -type PersonalEthAPI struct { - ethAPI *PublicEthAPI +// PrivateAccountAPI is the personal_ prefixed set of APIs in the Web3 JSON-RPC spec. +type PrivateAccountAPI struct { + ethAPI *eth.PublicEthereumAPI + logger log.Logger keyInfos []keys.Info // all keys, both locked and unlocked. unlocked keys are stored in ethAPI.keys } -// NewPersonalEthAPI creates an instance of the public Personal Eth API. -func NewPersonalEthAPI(ethAPI *PublicEthAPI) *PersonalEthAPI { - api := &PersonalEthAPI{ +// NewAPI creates an instance of the public Personal Eth API. +func NewAPI(ethAPI *eth.PublicEthereumAPI) *PrivateAccountAPI { + api := &PrivateAccountAPI{ ethAPI: ethAPI, + logger: log.NewTMLogger(log.NewSyncWriter(os.Stdout)).With("module", "json-rpc", "namespace", "personal"), } - infos, err := api.getKeybaseInfo() + err := api.ethAPI.GetKeyringInfo() if err != nil { return api } - api.keyInfos = infos - return api -} - -func (e *PersonalEthAPI) getKeybaseInfo() ([]keys.Info, error) { - e.ethAPI.keybaseLock.Lock() - defer e.ethAPI.keybaseLock.Unlock() - - if e.ethAPI.cliCtx.Keybase == nil { - keybase, err := keys.NewKeyring( - sdk.KeyringServiceName(), - viper.GetString(flags.FlagKeyringBackend), - viper.GetString(flags.FlagHome), - e.ethAPI.cliCtx.Input, - hd.EthSecp256k1Options()..., - ) - if err != nil { - return nil, err - } - - e.ethAPI.cliCtx.Keybase = keybase + api.keyInfos, err = api.ethAPI.ClientCtx().Keybase.List() + if err != nil { + return api } - return e.ethAPI.cliCtx.Keybase.List() + return api } // ImportRawKey armors and encrypts a given raw hex encoded ECDSA key and stores it into the key directory. // The name of the key will have the format "personal_", where is the total number of // keys stored on the keyring. // NOTE: The key will be both armored and encrypted using the same passphrase. -func (e *PersonalEthAPI) ImportRawKey(privkey, password string) (common.Address, error) { - e.ethAPI.logger.Debug("personal_importRawKey") +func (api *PrivateAccountAPI) ImportRawKey(privkey, password string) (common.Address, error) { + api.logger.Debug("personal_importRawKey") priv, err := crypto.HexToECDSA(privkey) if err != nil { return common.Address{}, err @@ -83,33 +66,33 @@ func (e *PersonalEthAPI) ImportRawKey(privkey, password string) (common.Address, armor := mintkey.EncryptArmorPrivKey(privKey, password, ethsecp256k1.KeyType) // ignore error as we only care about the length of the list - list, _ := e.ethAPI.cliCtx.Keybase.List() + list, _ := api.ethAPI.ClientCtx().Keybase.List() privKeyName := fmt.Sprintf("personal_%d", len(list)) - if err := e.ethAPI.cliCtx.Keybase.ImportPrivKey(privKeyName, armor, password); err != nil { + if err := api.ethAPI.ClientCtx().Keybase.ImportPrivKey(privKeyName, armor, password); err != nil { return common.Address{}, err } addr := common.BytesToAddress(privKey.PubKey().Address().Bytes()) - info, err := e.ethAPI.cliCtx.Keybase.Get(privKeyName) + info, err := api.ethAPI.ClientCtx().Keybase.Get(privKeyName) if err != nil { return common.Address{}, err } // append key and info to be able to lock and list the account - //e.ethAPI.keys = append(e.ethAPI.keys, privKey) - e.keyInfos = append(e.keyInfos, info) - e.ethAPI.logger.Info("key successfully imported", "name", privKeyName, "address", addr.String()) + //api.ethAPI.keys = append(api.ethAPI.keys, privKey) + api.keyInfos = append(api.keyInfos, info) + api.logger.Info("key successfully imported", "name", privKeyName, "address", addr.String()) return addr, nil } // ListAccounts will return a list of addresses for accounts this node manages. -func (e *PersonalEthAPI) ListAccounts() ([]common.Address, error) { - e.ethAPI.logger.Debug("personal_listAccounts") +func (api *PrivateAccountAPI) ListAccounts() ([]common.Address, error) { + api.logger.Debug("personal_listAccounts") addrs := []common.Address{} - for _, info := range e.keyInfos { + for _, info := range api.keyInfos { addressBytes := info.GetPubKey().Address().Bytes() addrs = append(addrs, common.BytesToAddress(addressBytes)) } @@ -119,20 +102,21 @@ func (e *PersonalEthAPI) ListAccounts() ([]common.Address, error) { // LockAccount will lock the account associated with the given address when it's unlocked. // It removes the key corresponding to the given address from the API's local keys. -func (e *PersonalEthAPI) LockAccount(address common.Address) bool { - e.ethAPI.logger.Debug("personal_lockAccount", "address", address.String()) +func (api *PrivateAccountAPI) LockAccount(address common.Address) bool { + api.logger.Debug("personal_lockAccount", "address", address.String()) - for i, key := range e.ethAPI.keys { + keys := api.ethAPI.GetKeys() + for i, key := range keys { if !bytes.Equal(key.PubKey().Address().Bytes(), address.Bytes()) { continue } - tmp := make([]ethsecp256k1.PrivKey, len(e.ethAPI.keys)-1) - copy(tmp[:i], e.ethAPI.keys[:i]) - copy(tmp[i:], e.ethAPI.keys[i+1:]) - e.ethAPI.keys = tmp + tmp := make([]ethsecp256k1.PrivKey, len(keys)-1) + copy(tmp[:i], keys[:i]) + copy(tmp[i:], keys[i+1:]) + api.ethAPI.SetKeys(tmp) - e.ethAPI.logger.Debug("account unlocked", "address", address.String()) + api.logger.Debug("account unlocked", "address", address.String()) return true } @@ -140,25 +124,21 @@ func (e *PersonalEthAPI) LockAccount(address common.Address) bool { } // NewAccount will create a new account and returns the address for the new account. -func (e *PersonalEthAPI) NewAccount(password string) (common.Address, error) { - e.ethAPI.logger.Debug("personal_newAccount") - _, err := e.getKeybaseInfo() - if err != nil { - return common.Address{}, err - } +func (api *PrivateAccountAPI) NewAccount(password string) (common.Address, error) { + api.logger.Debug("personal_newAccount") name := "key_" + time.Now().UTC().Format(time.RFC3339) - info, _, err := e.ethAPI.cliCtx.Keybase.CreateMnemonic(name, keys.English, password, hd.EthSecp256k1) + info, _, err := api.ethAPI.ClientCtx().Keybase.CreateMnemonic(name, keys.English, password, hd.EthSecp256k1) if err != nil { return common.Address{}, err } - e.keyInfos = append(e.keyInfos, info) + api.keyInfos = append(api.keyInfos, info) addr := common.BytesToAddress(info.GetPubKey().Address().Bytes()) - e.ethAPI.logger.Info("Your new key was generated", "address", addr.String()) - e.ethAPI.logger.Info("Please backup your key file!", "path", os.Getenv("HOME")+"/.ethermintcli/"+name) - e.ethAPI.logger.Info("Please remember your password!") + api.logger.Info("Your new key was generated", "address", addr.String()) + api.logger.Info("Please backup your key file!", "path", os.Getenv("HOME")+"/.ethermintd/"+name) + api.logger.Info("Please remember your password!") return addr, nil } @@ -166,13 +146,13 @@ func (e *PersonalEthAPI) NewAccount(password string) (common.Address, error) { // the given password for duration seconds. If duration is nil it will use a // default of 300 seconds. It returns an indication if the account was unlocked. // It exports the private key corresponding to the given address from the keyring and stores it in the API's local keys. -func (e *PersonalEthAPI) UnlockAccount(_ context.Context, addr common.Address, password string, _ *uint64) (bool, error) { // nolint: interfacer - e.ethAPI.logger.Debug("personal_unlockAccount", "address", addr.String()) +func (api *PrivateAccountAPI) UnlockAccount(_ context.Context, addr common.Address, password string, _ *uint64) (bool, error) { // nolint: interfacer + api.logger.Debug("personal_unlockAccount", "address", addr.String()) // TODO: use duration var keyInfo keys.Info - for _, info := range e.keyInfos { + for _, info := range api.keyInfos { addressBytes := info.GetPubKey().Address().Bytes() if bytes.Equal(addressBytes, addr[:]) { keyInfo = info @@ -184,31 +164,26 @@ func (e *PersonalEthAPI) UnlockAccount(_ context.Context, addr common.Address, p return false, fmt.Errorf("cannot find key with given address %s", addr.String()) } - // exporting private key only works on local keys - if keyInfo.GetType() != keys.TypeLocal { - return false, fmt.Errorf("key type must be %s, got %s", keys.TypeLedger.String(), keyInfo.GetType().String()) - } - - privKey, err := e.ethAPI.cliCtx.Keybase.ExportPrivateKeyObject(keyInfo.GetName(), password) + privKey, err := api.ethAPI.ClientCtx().Keybase.ExportPrivateKeyObject(keyInfo.GetName(), password) if err != nil { return false, err } - emintKey, ok := privKey.(ethsecp256k1.PrivKey) + ethermintPrivKey, ok := privKey.(ethsecp256k1.PrivKey) if !ok { - return false, fmt.Errorf("invalid private key type: %T", privKey) + return false, fmt.Errorf("invalid private key type %T, expected %T", privKey, ðsecp256k1.PrivKey{}) } - e.ethAPI.keys = append(e.ethAPI.keys, emintKey) - e.ethAPI.logger.Debug("account unlocked", "address", addr.String()) + api.ethAPI.SetKeys(append(api.ethAPI.GetKeys(), ethermintPrivKey)) + api.logger.Debug("account unlocked", "address", addr.String()) return true, nil } // SendTransaction will create a transaction from the given arguments and // tries to sign it with the key associated with args.To. If the given password isn't // able to decrypt the key it fails. -func (e *PersonalEthAPI) SendTransaction(_ context.Context, args params.SendTxArgs, _ string) (common.Hash, error) { - return e.ethAPI.SendTransaction(args) +func (api *PrivateAccountAPI) SendTransaction(_ context.Context, args rpctypes.SendTxArgs, _ string) (common.Hash, error) { + return api.ethAPI.SendTransaction(args) } // Sign calculates an Ethereum ECDSA signature for: @@ -220,10 +195,10 @@ func (e *PersonalEthAPI) SendTransaction(_ context.Context, args params.SendTxAr // The key used to calculate the signature is decrypted with the given password. // // https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_sign -func (e *PersonalEthAPI) Sign(_ context.Context, data hexutil.Bytes, addr common.Address, _ string) (hexutil.Bytes, error) { - e.ethAPI.logger.Debug("personal_sign", "data", data, "address", addr.String()) +func (api *PrivateAccountAPI) Sign(_ context.Context, data hexutil.Bytes, addr common.Address, _ string) (hexutil.Bytes, error) { + api.logger.Debug("personal_sign", "data", data, "address", addr.String()) - key, ok := checkKeyInKeyring(e.ethAPI.keys, addr) + key, ok := rpctypes.GetKeyByAddress(api.ethAPI.GetKeys(), addr) if !ok { return nil, fmt.Errorf("cannot find key with address %s", addr.String()) } @@ -247,8 +222,8 @@ func (e *PersonalEthAPI) Sign(_ context.Context, data hexutil.Bytes, addr common // the V value must be 27 or 28 for legacy reasons. // // https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_ecRecove -func (e *PersonalEthAPI) EcRecover(_ context.Context, data, sig hexutil.Bytes) (common.Address, error) { - e.ethAPI.logger.Debug("personal_ecRecover", "data", data, "sig", sig) +func (api *PrivateAccountAPI) EcRecover(_ context.Context, data, sig hexutil.Bytes) (common.Address, error) { + api.logger.Debug("personal_ecRecover", "data", data, "sig", sig) if len(sig) != crypto.SignatureLength { return common.Address{}, fmt.Errorf("signature must be %d bytes long", crypto.SignatureLength) diff --git a/rpc/web3_api.go b/rpc/namespaces/web3/api.go similarity index 68% rename from rpc/web3_api.go rename to rpc/namespaces/web3/api.go index edff98fdf..24885692c 100644 --- a/rpc/web3_api.go +++ b/rpc/namespaces/web3/api.go @@ -1,4 +1,4 @@ -package rpc +package web3 import ( "github.com/cosmos/ethermint/version" @@ -10,17 +10,17 @@ import ( // PublicWeb3API is the web3_ prefixed set of APIs in the Web3 JSON-RPC spec. type PublicWeb3API struct{} -// NewPublicWeb3API creates an instance of the Web3 API. -func NewPublicWeb3API() *PublicWeb3API { +// New creates an instance of the Web3 API. +func NewAPI() *PublicWeb3API { return &PublicWeb3API{} } // ClientVersion returns the client version in the Web3 user agent format. -func (a *PublicWeb3API) ClientVersion() string { +func (PublicWeb3API) ClientVersion() string { return version.ClientVersion() } // Sha3 returns the keccak-256 hash of the passed-in input. -func (a *PublicWeb3API) Sha3(input hexutil.Bytes) hexutil.Bytes { +func (PublicWeb3API) Sha3(input hexutil.Bytes) hexutil.Bytes { return crypto.Keccak256(input) } diff --git a/rpc/addrlock.go b/rpc/types/addrlock.go similarity index 98% rename from rpc/addrlock.go rename to rpc/types/addrlock.go index 51f6b9c33..e512c03c5 100644 --- a/rpc/addrlock.go +++ b/rpc/types/addrlock.go @@ -1,4 +1,4 @@ -package rpc +package types import ( "sync" diff --git a/rpc/types.go b/rpc/types/block.go similarity index 82% rename from rpc/types.go rename to rpc/types/block.go index 935947a29..0951da25a 100644 --- a/rpc/types.go +++ b/rpc/types/block.go @@ -1,4 +1,4 @@ -package rpc +package types import ( "fmt" @@ -63,5 +63,16 @@ func (bn *BlockNumber) UnmarshalJSON(data []byte) error { // Int64 converts block number to primitive type func (bn BlockNumber) Int64() int64 { - return (int64)(bn) + return int64(bn) +} + +// TmHeight is a util function used for the Tendermint RPC client. It returns +// nil if the block number is "latest". Otherwise, it returns the pointer of the +// int64 value of the height. +func (bn BlockNumber) TmHeight() *int64 { + if bn == LatestBlockNumber { + return nil + } + height := bn.Int64() + return &height } diff --git a/rpc/types/types.go b/rpc/types/types.go new file mode 100644 index 000000000..72bf22d79 --- /dev/null +++ b/rpc/types/types.go @@ -0,0 +1,85 @@ +package types + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +// Copied the Account and StorageResult types since they are registered under an +// internal pkg on geth. + +// AccountResult struct for account proof +type AccountResult struct { + Address common.Address `json:"address"` + AccountProof []string `json:"accountProof"` + Balance *hexutil.Big `json:"balance"` + CodeHash common.Hash `json:"codeHash"` + Nonce hexutil.Uint64 `json:"nonce"` + StorageHash common.Hash `json:"storageHash"` + StorageProof []StorageResult `json:"storageProof"` +} + +// StorageResult defines the format for storage proof return +type StorageResult struct { + Key string `json:"key"` + Value *hexutil.Big `json:"value"` + Proof []string `json:"proof"` +} + +// Transaction represents a transaction returned to RPC clients. +type Transaction 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"` +} + +// SendTxArgs represents the arguments to submit a new transaction into the transaction pool. +// Duplicate struct definition since geth struct is in internal package +// Ref: https://github.com/ethereum/go-ethereum/blob/release/1.9/internal/ethapi/api.go#L1346 +type SendTxArgs struct { + From common.Address `json:"from"` + To *common.Address `json:"to"` + Gas *hexutil.Uint64 `json:"gas"` + GasPrice *hexutil.Big `json:"gasPrice"` + Value *hexutil.Big `json:"value"` + Nonce *hexutil.Uint64 `json:"nonce"` + // We accept "data" and "input" for backwards-compatibility reasons. "input" is the + // newer name and should be preferred by clients. + Data *hexutil.Bytes `json:"data"` + Input *hexutil.Bytes `json:"input"` +} + +// CallArgs represents the arguments for a call. +type CallArgs struct { + From *common.Address `json:"from"` + To *common.Address `json:"to"` + Gas *hexutil.Uint64 `json:"gas"` + GasPrice *hexutil.Big `json:"gasPrice"` + Value *hexutil.Big `json:"value"` + Data *hexutil.Bytes `json:"data"` +} + +// Account indicates the overriding fields of account during the execution of +// a message call. +// NOTE: state and stateDiff can't be specified at the same time. If state is +// set, message execution will only use the data in the given state. Otherwise +// if statDiff is set, all diff will be applied first and then execute the call +// message. +type Account struct { + Nonce *hexutil.Uint64 `json:"nonce"` + Code *hexutil.Bytes `json:"code"` + Balance **hexutil.Big `json:"balance"` + State *map[common.Hash]common.Hash `json:"state"` + StateDiff *map[common.Hash]common.Hash `json:"stateDiff"` +} diff --git a/rpc/types/utils.go b/rpc/types/utils.go new file mode 100644 index 000000000..e1bbaec7b --- /dev/null +++ b/rpc/types/utils.go @@ -0,0 +1,191 @@ +package types + +import ( + "bytes" + "context" + "fmt" + "math/big" + + tmbytes "github.com/tendermint/tendermint/libs/bytes" + tmtypes "github.com/tendermint/tendermint/types" + + clientcontext "github.com/cosmos/cosmos-sdk/client/context" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + + "github.com/cosmos/ethermint/crypto/ethsecp256k1" + evmtypes "github.com/cosmos/ethermint/x/evm/types" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + ethtypes "github.com/ethereum/go-ethereum/core/types" +) + +// RawTxToEthTx returns a evm MsgEthereum transaction from raw tx bytes. +func RawTxToEthTx(clientCtx clientcontext.CLIContext, bz []byte) (*evmtypes.MsgEthereumTx, error) { + tx, err := evmtypes.TxDecoder(clientCtx.Codec)(bz) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + + ethTx, ok := tx.(evmtypes.MsgEthereumTx) + if !ok { + return nil, fmt.Errorf("invalid transaction type %T, expected %T", tx, &evmtypes.MsgEthereumTx{}) + } + return ðTx, nil +} + +// NewTransaction returns a transaction that will serialize to the RPC +// representation, with the given location metadata set (if available). +func NewTransaction(tx *evmtypes.MsgEthereumTx, txHash, blockHash common.Hash, blockNumber, index uint64) (*Transaction, error) { + // Verify signature and retrieve sender address + from, err := tx.VerifySig(tx.ChainID()) + if err != nil { + return nil, err + } + + rpcTx := &Transaction{ + From: from, + Gas: hexutil.Uint64(tx.Data.GasLimit), + GasPrice: (*hexutil.Big)(tx.Data.Price), + Hash: txHash, + Input: hexutil.Bytes(tx.Data.Payload), + Nonce: hexutil.Uint64(tx.Data.AccountNonce), + To: tx.To(), + Value: (*hexutil.Big)(tx.Data.Amount), + V: (*hexutil.Big)(tx.Data.V), + R: (*hexutil.Big)(tx.Data.R), + S: (*hexutil.Big)(tx.Data.S), + } + + if blockHash != (common.Hash{}) { + rpcTx.BlockHash = &blockHash + rpcTx.BlockNumber = (*hexutil.Big)(new(big.Int).SetUint64(blockNumber)) + rpcTx.TransactionIndex = (*hexutil.Uint64)(&index) + } + + return rpcTx, nil +} + +// EthBlockFromTendermint returns a JSON-RPC compatible Ethereum blockfrom a given Tendermint block. +func EthBlockFromTendermint(clientCtx clientcontext.CLIContext, block *tmtypes.Block) (map[string]interface{}, error) { + gasLimit, err := BlockMaxGasFromConsensusParams(context.Background(), clientCtx) + if err != nil { + return nil, err + } + + transactions, gasUsed, err := EthTransactionsFromTendermint(clientCtx, block.Txs) + if err != nil { + return nil, err + } + + res, _, err := clientCtx.Query(fmt.Sprintf("custom/%s/%s/%d", evmtypes.ModuleName, evmtypes.QueryBloom, block.Height)) + if err != nil { + return nil, err + } + + var bloomRes evmtypes.QueryBloomFilter + clientCtx.Codec.MustUnmarshalJSON(res, &bloomRes) + + bloom := bloomRes.Bloom + + return formatBlock(block.Header, block.Size(), gasLimit, gasUsed, transactions, bloom), nil +} + +// EthHeaderFromTendermint is an util function that returns an Ethereum Header +// from a tendermint Header. +func EthHeaderFromTendermint(header tmtypes.Header) *ethtypes.Header { + return ðtypes.Header{ + ParentHash: common.BytesToHash(header.LastBlockID.Hash.Bytes()), + UncleHash: common.Hash{}, + Coinbase: common.Address{}, + Root: common.BytesToHash(header.AppHash), + TxHash: common.BytesToHash(header.DataHash), + ReceiptHash: common.Hash{}, + Difficulty: nil, + Number: big.NewInt(header.Height), + Time: uint64(header.Time.Unix()), + Extra: nil, + MixDigest: common.Hash{}, + Nonce: ethtypes.BlockNonce{}, + } +} + +// EthTransactionsFromTendermint returns a slice of ethereum transaction hashes and the total gas usage from a set of +// tendermint block transactions. +func EthTransactionsFromTendermint(clientCtx clientcontext.CLIContext, txs []tmtypes.Tx) ([]common.Hash, *big.Int, error) { + transactionHashes := []common.Hash{} + gasUsed := big.NewInt(0) + + for _, tx := range txs { + ethTx, err := RawTxToEthTx(clientCtx, tx) + if err != nil { + // continue to next transaction in case it's not a MsgEthereumTx + continue + } + // TODO: Remove gas usage calculation if saving gasUsed per block + gasUsed.Add(gasUsed, ethTx.Fee()) + transactionHashes = append(transactionHashes, common.BytesToHash(tx.Hash())) + } + + return transactionHashes, gasUsed, nil +} + +// BlockMaxGasFromConsensusParams returns the gas limit for the latest block from the chain consensus params. +func BlockMaxGasFromConsensusParams(_ context.Context, clientCtx clientcontext.CLIContext) (int64, error) { + resConsParams, err := clientCtx.Client.ConsensusParams(nil) + if err != nil { + return 0, err + } + + gasLimit := resConsParams.ConsensusParams.Block.MaxGas + if gasLimit == -1 { + // Sets gas limit to max uint32 to not error with javascript dev tooling + // This -1 value indicating no block gas limit is set to max uint64 with geth hexutils + // which errors certain javascript dev tooling which only supports up to 53 bits + gasLimit = int64(^uint32(0)) + } + + return gasLimit, nil +} + +func formatBlock( + header tmtypes.Header, size int, gasLimit int64, + gasUsed *big.Int, transactions interface{}, bloom ethtypes.Bloom, +) map[string]interface{} { + if len(header.DataHash) == 0 { + header.DataHash = tmbytes.HexBytes(common.Hash{}.Bytes()) + } + + return map[string]interface{}{ + "number": hexutil.Uint64(header.Height), + "hash": hexutil.Bytes(header.Hash()), + "parentHash": hexutil.Bytes(header.LastBlockID.Hash), + "nonce": hexutil.Uint64(0), // PoW specific + "sha3Uncles": common.Hash{}, // No uncles in Tendermint + "logsBloom": bloom, + "transactionsRoot": hexutil.Bytes(header.DataHash), + "stateRoot": hexutil.Bytes(header.AppHash), + "miner": common.Address{}, + "mixHash": common.Hash{}, + "difficulty": 0, + "totalDifficulty": 0, + "extraData": hexutil.Uint64(0), + "size": hexutil.Uint64(size), + "gasLimit": hexutil.Uint64(gasLimit), // Static gas limit + "gasUsed": (*hexutil.Big)(gasUsed), + "timestamp": hexutil.Uint64(header.Time.Unix()), + "transactions": transactions.([]common.Hash), + "uncles": []string{}, + "receiptsRoot": common.Hash{}, + } +} + +// GetKeyByAddress returns the private key matching the given address. If not found it returns false. +func GetKeyByAddress(keys []ethsecp256k1.PrivKey, address common.Address) (key *ethsecp256k1.PrivKey, exist bool) { + for _, key := range keys { + if bytes.Equal(key.PubKey().Address().Bytes(), address.Bytes()) { + return &key, true + } + } + return nil, false +} diff --git a/rpc/websockets.go b/rpc/websockets.go deleted file mode 100644 index 26117824a..000000000 --- a/rpc/websockets.go +++ /dev/null @@ -1,542 +0,0 @@ -package rpc - -import ( - "bufio" - "bytes" - "encoding/json" - "fmt" - "io/ioutil" - "math/big" - "net" - "net/http" - "os" - "strings" - "sync" - - "github.com/gorilla/mux" - "github.com/gorilla/websocket" - "github.com/spf13/viper" - - "github.com/tendermint/tendermint/libs/log" - coretypes "github.com/tendermint/tendermint/rpc/core/types" - tmtypes "github.com/tendermint/tendermint/types" - - evmtypes "github.com/cosmos/ethermint/x/evm/types" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/eth/filters" - "github.com/ethereum/go-ethereum/rpc" - - context "github.com/cosmos/cosmos-sdk/client/context" -) - -type SubscriptionResponseJSON struct { - Jsonrpc string `json:"jsonrpc"` - Result interface{} `json:"result"` - ID float64 `json:"id"` -} - -type SubscriptionNotification struct { - Jsonrpc string `json:"jsonrpc"` - Method string `json:"method"` - Params *SubscriptionResult `json:"params"` -} - -type SubscriptionResult struct { - Subscription rpc.ID `json:"subscription"` - Result interface{} `json:"result"` -} - -type ErrorResponseJSON struct { - Jsonrpc string `json:"jsonrpc"` - Error *ErrorMessageJSON `json:"error"` - ID *big.Int `json:"id"` -} - -type ErrorMessageJSON struct { - Code *big.Int `json:"code"` - Message string `json:"message"` -} - -type websocketsServer struct { - rpcAddr string // listen address of rest-server - wsAddr string // listen address of ws server - api *pubSubAPI - logger log.Logger -} - -func newWebsocketsServer(cliCtx context.CLIContext, wsAddr string) *websocketsServer { - return &websocketsServer{ - rpcAddr: viper.GetString("laddr"), - wsAddr: wsAddr, - api: newPubSubAPI(cliCtx), - logger: log.NewTMLogger(log.NewSyncWriter(os.Stdout)).With("module", "websocket-server"), - } -} - -func (s *websocketsServer) start() { - ws := mux.NewRouter() - ws.Handle("/", s) - - go func() { - err := http.ListenAndServe(fmt.Sprintf(":%s", s.wsAddr), ws) - if err != nil { - s.logger.Error("http error:", err) - } - }() -} - -func (s *websocketsServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { - var upgrader = websocket.Upgrader{ - CheckOrigin: func(r *http.Request) bool { - return true - }, - } - - wsConn, err := upgrader.Upgrade(w, r, nil) - if err != nil { - s.logger.Error("websocket upgrade failed; error:", err) - return - } - - s.readLoop(wsConn) -} - -func (s *websocketsServer) sendErrResponse(conn *websocket.Conn, msg string) { - res := &ErrorResponseJSON{ - Jsonrpc: "2.0", - Error: &ErrorMessageJSON{ - Code: big.NewInt(-32600), - Message: msg, - }, - ID: nil, - } - err := conn.WriteJSON(res) - if err != nil { - s.logger.Error("websocket failed write message", "error", err) - } -} - -func (s *websocketsServer) readLoop(wsConn *websocket.Conn) { - for { - _, mb, err := wsConn.ReadMessage() - if err != nil { - _ = wsConn.Close() - s.logger.Error("failed to read message; error", err) - return - } - - var msg map[string]interface{} - err = json.Unmarshal(mb, &msg) - if err != nil { - s.sendErrResponse(wsConn, "invalid request") - continue - } - - // check if method == eth_subscribe or eth_unsubscribe - method := msg["method"] - if method.(string) == "eth_subscribe" { - params := msg["params"].([]interface{}) - if len(params) == 0 { - s.sendErrResponse(wsConn, "invalid parameters") - continue - } - - id, err := s.api.subscribe(wsConn, params) - if err != nil { - s.sendErrResponse(wsConn, err.Error()) - continue - } - - res := &SubscriptionResponseJSON{ - Jsonrpc: "2.0", - ID: 1, - Result: id, - } - - err = wsConn.WriteJSON(res) - if err != nil { - s.logger.Error("failed to write json response", err) - continue - } - - continue - } else if method.(string) == "eth_unsubscribe" { - ids, ok := msg["params"].([]interface{}) - if _, idok := ids[0].(string); !ok || !idok { - s.sendErrResponse(wsConn, "invalid parameters") - continue - } - - ok = s.api.unsubscribe(rpc.ID(ids[0].(string))) - res := &SubscriptionResponseJSON{ - Jsonrpc: "2.0", - ID: 1, - Result: ok, - } - - err = wsConn.WriteJSON(res) - if err != nil { - s.logger.Error("failed to write json response", err) - continue - } - - continue - } - - // otherwise, call the usual rpc server to respond - err = s.tcpGetAndSendResponse(wsConn, mb) - if err != nil { - s.sendErrResponse(wsConn, err.Error()) - } - } -} - -// tcpGetAndSendResponse connects to the rest-server over tcp, posts a JSON-RPC request, and sends the response -// to the client over websockets -func (s *websocketsServer) tcpGetAndSendResponse(conn *websocket.Conn, mb []byte) error { - addr := strings.Split(s.rpcAddr, "tcp://") - if len(addr) != 2 { - return fmt.Errorf("invalid laddr %s", s.rpcAddr) - } - - tcpConn, err := net.Dial("tcp", addr[1]) - if err != nil { - return fmt.Errorf("cannot connect to %s; %s", s.rpcAddr, err) - } - - buf := &bytes.Buffer{} - _, err = buf.Write(mb) - if err != nil { - return fmt.Errorf("failed to write message; %s", err) - } - - req, err := http.NewRequest("POST", s.rpcAddr, buf) - if err != nil { - return fmt.Errorf("failed to request; %s", err) - } - - req.Header.Set("Content-Type", "application/json;") - err = req.Write(tcpConn) - if err != nil { - return fmt.Errorf("failed to write to rest-server; %s", err) - } - - respBytes, err := ioutil.ReadAll(tcpConn) - if err != nil { - return fmt.Errorf("error reading response from rest-server; %s", err) - } - - respbuf := &bytes.Buffer{} - respbuf.Write(respBytes) - resp, err := http.ReadResponse(bufio.NewReader(respbuf), req) - if err != nil { - return fmt.Errorf("could not read response; %s", err) - } - - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return fmt.Errorf("could not read body from response; %s", err) - } - - var wsSend interface{} - err = json.Unmarshal(body, &wsSend) - if err != nil { - return fmt.Errorf("failed to unmarshal rest-server response; %s", err) - } - - return conn.WriteJSON(wsSend) -} - -type wsSubscription struct { - sub *Subscription - unsubscribed chan struct{} // closed when unsubscribing - conn *websocket.Conn -} - -// pubSubAPI is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec -type pubSubAPI struct { - cliCtx context.CLIContext - events *EventSystem - filtersMu sync.Mutex - filters map[rpc.ID]*wsSubscription - logger log.Logger -} - -// newPubSubAPI creates an instance of the ethereum PubSub API. -func newPubSubAPI(cliCtx context.CLIContext) *pubSubAPI { - return &pubSubAPI{ - cliCtx: cliCtx, - events: NewEventSystem(cliCtx.Client), - filters: make(map[rpc.ID]*wsSubscription), - logger: log.NewTMLogger(log.NewSyncWriter(os.Stdout)).With("module", "websocket-client"), - } -} - -func (api *pubSubAPI) subscribe(conn *websocket.Conn, params []interface{}) (rpc.ID, error) { - method, ok := params[0].(string) - if !ok { - return "0", fmt.Errorf("invalid parameters") - } - - switch method { - case "newHeads": - // TODO: handle extra params - return api.subscribeNewHeads(conn) - case "logs": - if len(params) > 1 { - return api.subscribeLogs(conn, params[1]) - } - - return api.subscribeLogs(conn, nil) - case "newPendingTransactions": - return api.subscribePendingTransactions(conn) - case "syncing": - return api.subscribeSyncing(conn) - default: - return "0", fmt.Errorf("unsupported method %s", method) - } -} - -func (api *pubSubAPI) unsubscribe(id rpc.ID) bool { - api.filtersMu.Lock() - defer api.filtersMu.Unlock() - - if api.filters[id] == nil { - return false - } - - close(api.filters[id].unsubscribed) - delete(api.filters, id) - return true -} - -func (api *pubSubAPI) subscribeNewHeads(conn *websocket.Conn) (rpc.ID, error) { - sub, _, err := api.events.SubscribeNewHeads() - if err != nil { - return "", fmt.Errorf("error creating block filter: %s", err.Error()) - } - - unsubscribed := make(chan struct{}) - api.filtersMu.Lock() - api.filters[sub.ID()] = &wsSubscription{ - sub: sub, - conn: conn, - unsubscribed: unsubscribed, - } - api.filtersMu.Unlock() - - go func(headersCh <-chan coretypes.ResultEvent, errCh <-chan error) { - for { - select { - case event := <-headersCh: - data, _ := event.Data.(tmtypes.EventDataNewBlockHeader) - header := EthHeaderFromTendermint(data.Header) - - api.filtersMu.Lock() - if f, found := api.filters[sub.ID()]; found { - // write to ws conn - res := &SubscriptionNotification{ - Jsonrpc: "2.0", - Method: "eth_subscription", - Params: &SubscriptionResult{ - Subscription: sub.ID(), - Result: header, - }, - } - - err = f.conn.WriteJSON(res) - if err != nil { - api.logger.Error("error writing header") - } - } - api.filtersMu.Unlock() - case <-errCh: - api.filtersMu.Lock() - delete(api.filters, sub.ID()) - api.filtersMu.Unlock() - return - case <-unsubscribed: - return - } - } - }(sub.eventCh, sub.Err()) - - return sub.ID(), nil -} - -func (api *pubSubAPI) subscribeLogs(conn *websocket.Conn, extra interface{}) (rpc.ID, error) { - crit := filters.FilterCriteria{} - - if extra != nil { - params, ok := extra.(map[string]interface{}) - if !ok { - return "", fmt.Errorf("invalid criteria") - } - - if params["address"] != nil { - address, ok := params["address"].(string) - addresses, sok := params["address"].([]interface{}) - if !ok && !sok { - return "", fmt.Errorf("invalid address; must be address or array of addresses") - } - - if ok { - crit.Addresses = []common.Address{common.HexToAddress(address)} - } - - if sok { - crit.Addresses = []common.Address{} - for _, addr := range addresses { - address, ok := addr.(string) - if !ok { - return "", fmt.Errorf("invalid address") - } - - crit.Addresses = append(crit.Addresses, common.HexToAddress(address)) - } - } - } - - if params["topics"] != nil { - topics, ok := params["topics"].([]interface{}) - if !ok { - return "", fmt.Errorf("invalid topics") - } - - crit.Topics = [][]common.Hash{} - for _, topic := range topics { - tstr, ok := topic.(string) - if !ok { - return "", fmt.Errorf("invalid topics") - } - - h := common.HexToHash(tstr) - crit.Topics = append(crit.Topics, []common.Hash{h}) - } - } - } - - sub, _, err := api.events.SubscribeLogs(crit) - if err != nil { - return rpc.ID(""), err - } - - unsubscribed := make(chan struct{}) - api.filtersMu.Lock() - api.filters[sub.ID()] = &wsSubscription{ - sub: sub, - conn: conn, - unsubscribed: unsubscribed, - } - api.filtersMu.Unlock() - - go func(ch <-chan coretypes.ResultEvent, errCh <-chan error) { - for { - select { - case event := <-ch: - dataTx, ok := event.Data.(tmtypes.EventDataTx) - if !ok { - err = fmt.Errorf("invalid event data %T, expected EventDataTx", event.Data) - return - } - - var resultData evmtypes.ResultData - resultData, err = evmtypes.DecodeResultData(dataTx.TxResult.Result.Data) - if err != nil { - return - } - - logs := filterLogs(resultData.Logs, crit.FromBlock, crit.ToBlock, crit.Addresses, crit.Topics) - - api.filtersMu.Lock() - if f, found := api.filters[sub.ID()]; found { - // write to ws conn - res := &SubscriptionNotification{ - Jsonrpc: "2.0", - Method: "eth_subscription", - Params: &SubscriptionResult{ - Subscription: sub.ID(), - Result: logs, - }, - } - - err = f.conn.WriteJSON(res) - } - api.filtersMu.Unlock() - - if err != nil { - err = fmt.Errorf("failed to write header: %w", err) - return - } - case <-errCh: - api.filtersMu.Lock() - delete(api.filters, sub.ID()) - api.filtersMu.Unlock() - return - case <-unsubscribed: - return - } - } - }(sub.eventCh, sub.Err()) - - return sub.ID(), nil -} - -func (api *pubSubAPI) subscribePendingTransactions(conn *websocket.Conn) (rpc.ID, error) { - sub, _, err := api.events.SubscribePendingTxs() - if err != nil { - return "", fmt.Errorf("error creating block filter: %s", err.Error()) - } - - unsubscribed := make(chan struct{}) - api.filtersMu.Lock() - api.filters[sub.ID()] = &wsSubscription{ - sub: sub, - conn: conn, - unsubscribed: unsubscribed, - } - api.filtersMu.Unlock() - - go func(txsCh <-chan coretypes.ResultEvent, errCh <-chan error) { - for { - select { - case ev := <-txsCh: - data, _ := ev.Data.(tmtypes.EventDataTx) - txHash := common.BytesToHash(data.Tx.Hash()) - - api.filtersMu.Lock() - if f, found := api.filters[sub.ID()]; found { - // write to ws conn - res := &SubscriptionNotification{ - Jsonrpc: "2.0", - Method: "eth_subscription", - Params: &SubscriptionResult{ - Subscription: sub.ID(), - Result: txHash, - }, - } - - err = f.conn.WriteJSON(res) - } - api.filtersMu.Unlock() - - if err != nil { - err = fmt.Errorf("failed to write header: %w", err) - return - } - case <-errCh: - api.filtersMu.Lock() - delete(api.filters, sub.ID()) - api.filtersMu.Unlock() - } - } - }(sub.eventCh, sub.Err()) - - return sub.ID(), nil -} - -func (api *pubSubAPI) subscribeSyncing(conn *websocket.Conn) (rpc.ID, error) { - return "", nil -} diff --git a/rpc/websockets/pubsub_api.go b/rpc/websockets/pubsub_api.go new file mode 100644 index 000000000..266fce40f --- /dev/null +++ b/rpc/websockets/pubsub_api.go @@ -0,0 +1,309 @@ +package websockets + +import ( + "fmt" + "os" + "sync" + + "github.com/gorilla/websocket" + + "github.com/tendermint/tendermint/libs/log" + coretypes "github.com/tendermint/tendermint/rpc/core/types" + tmtypes "github.com/tendermint/tendermint/types" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/eth/filters" + "github.com/ethereum/go-ethereum/rpc" + + context "github.com/cosmos/cosmos-sdk/client/context" + + rpcfilters "github.com/cosmos/ethermint/rpc/namespaces/eth/filters" + rpctypes "github.com/cosmos/ethermint/rpc/types" + evmtypes "github.com/cosmos/ethermint/x/evm/types" +) + +// PubSubAPI is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec +type PubSubAPI struct { + clientCtx context.CLIContext + events *rpcfilters.EventSystem + filtersMu sync.Mutex + filters map[rpc.ID]*wsSubscription + logger log.Logger +} + +// NewAPI creates an instance of the ethereum PubSub API. +func NewAPI(clientCtx context.CLIContext) *PubSubAPI { + return &PubSubAPI{ + clientCtx: clientCtx, + events: rpcfilters.NewEventSystem(clientCtx.Client), + filters: make(map[rpc.ID]*wsSubscription), + logger: log.NewTMLogger(log.NewSyncWriter(os.Stdout)).With("module", "websocket-client"), + } +} + +func (api *PubSubAPI) subscribe(conn *websocket.Conn, params []interface{}) (rpc.ID, error) { + method, ok := params[0].(string) + if !ok { + return "0", fmt.Errorf("invalid parameters") + } + + switch method { + case "newHeads": + // TODO: handle extra params + return api.subscribeNewHeads(conn) + case "logs": + if len(params) > 1 { + return api.subscribeLogs(conn, params[1]) + } + + return api.subscribeLogs(conn, nil) + case "newPendingTransactions": + return api.subscribePendingTransactions(conn) + case "syncing": + return api.subscribeSyncing(conn) + default: + return "0", fmt.Errorf("unsupported method %s", method) + } +} + +func (api *PubSubAPI) unsubscribe(id rpc.ID) bool { + api.filtersMu.Lock() + defer api.filtersMu.Unlock() + + if api.filters[id] == nil { + return false + } + + close(api.filters[id].unsubscribed) + delete(api.filters, id) + return true +} + +func (api *PubSubAPI) subscribeNewHeads(conn *websocket.Conn) (rpc.ID, error) { + sub, _, err := api.events.SubscribeNewHeads() + if err != nil { + return "", fmt.Errorf("error creating block filter: %s", err.Error()) + } + + unsubscribed := make(chan struct{}) + api.filtersMu.Lock() + api.filters[sub.ID()] = &wsSubscription{ + sub: sub, + conn: conn, + unsubscribed: unsubscribed, + } + api.filtersMu.Unlock() + + go func(headersCh <-chan coretypes.ResultEvent, errCh <-chan error) { + for { + select { + case event := <-headersCh: + data, _ := event.Data.(tmtypes.EventDataNewBlockHeader) + header := rpctypes.EthHeaderFromTendermint(data.Header) + + api.filtersMu.Lock() + if f, found := api.filters[sub.ID()]; found { + // write to ws conn + res := &SubscriptionNotification{ + Jsonrpc: "2.0", + Method: "eth_subscription", + Params: &SubscriptionResult{ + Subscription: sub.ID(), + Result: header, + }, + } + + err = f.conn.WriteJSON(res) + if err != nil { + api.logger.Error("error writing header") + } + } + api.filtersMu.Unlock() + case <-errCh: + api.filtersMu.Lock() + delete(api.filters, sub.ID()) + api.filtersMu.Unlock() + return + case <-unsubscribed: + return + } + } + }(sub.Event(), sub.Err()) + + return sub.ID(), nil +} + +func (api *PubSubAPI) subscribeLogs(conn *websocket.Conn, extra interface{}) (rpc.ID, error) { + crit := filters.FilterCriteria{} + + if extra != nil { + params, ok := extra.(map[string]interface{}) + if !ok { + return "", fmt.Errorf("invalid criteria") + } + + if params["address"] != nil { + address, ok := params["address"].(string) + addresses, sok := params["address"].([]interface{}) + if !ok && !sok { + return "", fmt.Errorf("invalid address; must be address or array of addresses") + } + + if ok { + crit.Addresses = []common.Address{common.HexToAddress(address)} + } + + if sok { + crit.Addresses = []common.Address{} + for _, addr := range addresses { + address, ok := addr.(string) + if !ok { + return "", fmt.Errorf("invalid address") + } + + crit.Addresses = append(crit.Addresses, common.HexToAddress(address)) + } + } + } + + if params["topics"] != nil { + topics, ok := params["topics"].([]interface{}) + if !ok { + return "", fmt.Errorf("invalid topics") + } + + crit.Topics = [][]common.Hash{} + for _, topic := range topics { + tstr, ok := topic.(string) + if !ok { + return "", fmt.Errorf("invalid topics") + } + + h := common.HexToHash(tstr) + crit.Topics = append(crit.Topics, []common.Hash{h}) + } + } + } + + sub, _, err := api.events.SubscribeLogs(crit) + if err != nil { + return rpc.ID(""), err + } + + unsubscribed := make(chan struct{}) + api.filtersMu.Lock() + api.filters[sub.ID()] = &wsSubscription{ + sub: sub, + conn: conn, + unsubscribed: unsubscribed, + } + api.filtersMu.Unlock() + + go func(ch <-chan coretypes.ResultEvent, errCh <-chan error) { + for { + select { + case event := <-ch: + dataTx, ok := event.Data.(tmtypes.EventDataTx) + if !ok { + err = fmt.Errorf("invalid event data %T, expected EventDataTx", event.Data) + return + } + + var resultData evmtypes.ResultData + resultData, err = evmtypes.DecodeResultData(dataTx.TxResult.Result.Data) + if err != nil { + return + } + + logs := rpcfilters.FilterLogs(resultData.Logs, crit.FromBlock, crit.ToBlock, crit.Addresses, crit.Topics) + + api.filtersMu.Lock() + if f, found := api.filters[sub.ID()]; found { + // write to ws conn + res := &SubscriptionNotification{ + Jsonrpc: "2.0", + Method: "eth_subscription", + Params: &SubscriptionResult{ + Subscription: sub.ID(), + Result: logs, + }, + } + + err = f.conn.WriteJSON(res) + } + api.filtersMu.Unlock() + + if err != nil { + err = fmt.Errorf("failed to write header: %w", err) + return + } + case <-errCh: + api.filtersMu.Lock() + delete(api.filters, sub.ID()) + api.filtersMu.Unlock() + return + case <-unsubscribed: + return + } + } + }(sub.Event(), sub.Err()) + + return sub.ID(), nil +} + +func (api *PubSubAPI) subscribePendingTransactions(conn *websocket.Conn) (rpc.ID, error) { + sub, _, err := api.events.SubscribePendingTxs() + if err != nil { + return "", fmt.Errorf("error creating block filter: %s", err.Error()) + } + + unsubscribed := make(chan struct{}) + api.filtersMu.Lock() + api.filters[sub.ID()] = &wsSubscription{ + sub: sub, + conn: conn, + unsubscribed: unsubscribed, + } + api.filtersMu.Unlock() + + go func(txsCh <-chan coretypes.ResultEvent, errCh <-chan error) { + for { + select { + case ev := <-txsCh: + data, _ := ev.Data.(tmtypes.EventDataTx) + txHash := common.BytesToHash(data.Tx.Hash()) + + api.filtersMu.Lock() + if f, found := api.filters[sub.ID()]; found { + // write to ws conn + res := &SubscriptionNotification{ + Jsonrpc: "2.0", + Method: "eth_subscription", + Params: &SubscriptionResult{ + Subscription: sub.ID(), + Result: txHash, + }, + } + + err = f.conn.WriteJSON(res) + } + api.filtersMu.Unlock() + + if err != nil { + err = fmt.Errorf("failed to write header: %w", err) + return + } + case <-errCh: + api.filtersMu.Lock() + delete(api.filters, sub.ID()) + api.filtersMu.Unlock() + } + } + }(sub.Event(), sub.Err()) + + return sub.ID(), nil +} + +func (api *PubSubAPI) subscribeSyncing(conn *websocket.Conn) (rpc.ID, error) { + return "", nil +} diff --git a/rpc/websockets/server.go b/rpc/websockets/server.go new file mode 100644 index 000000000..25102afb2 --- /dev/null +++ b/rpc/websockets/server.go @@ -0,0 +1,219 @@ +package websockets + +import ( + "bufio" + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "math/big" + "net" + "net/http" + "os" + "strings" + + "github.com/gorilla/mux" + "github.com/gorilla/websocket" + "github.com/spf13/viper" + + "github.com/tendermint/tendermint/libs/log" + + "github.com/ethereum/go-ethereum/rpc" + + context "github.com/cosmos/cosmos-sdk/client/context" +) + +// Server defines a server that handles Ethereum websockets. +type Server struct { + rpcAddr string // listen address of rest-server + wsAddr string // listen address of ws server + api *PubSubAPI + logger log.Logger +} + +// NewServer creates a new websocket server instance. +func NewServer(clientCtx context.CLIContext, wsAddr string) *Server { + return &Server{ + rpcAddr: viper.GetString("laddr"), + wsAddr: wsAddr, + api: NewAPI(clientCtx), + logger: log.NewTMLogger(log.NewSyncWriter(os.Stdout)).With("module", "websocket-server"), + } +} + +// Start runs the websocket server +func (s *Server) Start() { + ws := mux.NewRouter() + ws.Handle("/", s) + + go func() { + err := http.ListenAndServe(fmt.Sprintf(":%s", s.wsAddr), ws) + if err != nil { + s.logger.Error("http error:", err) + } + }() +} + +func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + var upgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true + }, + } + + wsConn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + s.logger.Error("websocket upgrade failed; error:", err) + return + } + + s.readLoop(wsConn) +} + +func (s *Server) sendErrResponse(conn *websocket.Conn, msg string) { + res := &ErrorResponseJSON{ + Jsonrpc: "2.0", + Error: &ErrorMessageJSON{ + Code: big.NewInt(-32600), + Message: msg, + }, + ID: nil, + } + err := conn.WriteJSON(res) + if err != nil { + s.logger.Error("websocket failed write message", "error", err) + } +} + +func (s *Server) readLoop(wsConn *websocket.Conn) { + for { + _, mb, err := wsConn.ReadMessage() + if err != nil { + _ = wsConn.Close() + s.logger.Error("failed to read message; error", err) + return + } + + var msg map[string]interface{} + err = json.Unmarshal(mb, &msg) + if err != nil { + s.sendErrResponse(wsConn, "invalid request") + continue + } + + // check if method == eth_subscribe or eth_unsubscribe + method := msg["method"] + if method.(string) == "eth_subscribe" { + params := msg["params"].([]interface{}) + if len(params) == 0 { + s.sendErrResponse(wsConn, "invalid parameters") + continue + } + + id, err := s.api.subscribe(wsConn, params) + if err != nil { + s.sendErrResponse(wsConn, err.Error()) + continue + } + + res := &SubscriptionResponseJSON{ + Jsonrpc: "2.0", + ID: 1, + Result: id, + } + + err = wsConn.WriteJSON(res) + if err != nil { + s.logger.Error("failed to write json response", err) + continue + } + + continue + } else if method.(string) == "eth_unsubscribe" { + ids, ok := msg["params"].([]interface{}) + if _, idok := ids[0].(string); !ok || !idok { + s.sendErrResponse(wsConn, "invalid parameters") + continue + } + + ok = s.api.unsubscribe(rpc.ID(ids[0].(string))) + res := &SubscriptionResponseJSON{ + Jsonrpc: "2.0", + ID: 1, + Result: ok, + } + + err = wsConn.WriteJSON(res) + if err != nil { + s.logger.Error("failed to write json response", err) + continue + } + + continue + } + + // otherwise, call the usual rpc server to respond + err = s.tcpGetAndSendResponse(wsConn, mb) + if err != nil { + s.sendErrResponse(wsConn, err.Error()) + } + } +} + +// tcpGetAndSendResponse connects to the rest-server over tcp, posts a JSON-RPC request, and sends the response +// to the client over websockets +func (s *Server) tcpGetAndSendResponse(conn *websocket.Conn, mb []byte) error { + addr := strings.Split(s.rpcAddr, "tcp://") + if len(addr) != 2 { + return fmt.Errorf("invalid laddr %s", s.rpcAddr) + } + + tcpConn, err := net.Dial("tcp", addr[1]) + if err != nil { + return fmt.Errorf("cannot connect to %s; %s", s.rpcAddr, err) + } + + buf := &bytes.Buffer{} + _, err = buf.Write(mb) + if err != nil { + return fmt.Errorf("failed to write message; %s", err) + } + + req, err := http.NewRequest("POST", s.rpcAddr, buf) + if err != nil { + return fmt.Errorf("failed to request; %s", err) + } + + req.Header.Set("Content-Type", "application/json;") + err = req.Write(tcpConn) + if err != nil { + return fmt.Errorf("failed to write to rest-server; %s", err) + } + + respBytes, err := ioutil.ReadAll(tcpConn) + if err != nil { + return fmt.Errorf("error reading response from rest-server; %s", err) + } + + respbuf := &bytes.Buffer{} + respbuf.Write(respBytes) + resp, err := http.ReadResponse(bufio.NewReader(respbuf), req) + if err != nil { + return fmt.Errorf("could not read response; %s", err) + } + + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("could not read body from response; %s", err) + } + + var wsSend interface{} + err = json.Unmarshal(body, &wsSend) + if err != nil { + return fmt.Errorf("failed to unmarshal rest-server response; %s", err) + } + + return conn.WriteJSON(wsSend) +} diff --git a/rpc/websockets/types.go b/rpc/websockets/types.go new file mode 100644 index 000000000..ca8266415 --- /dev/null +++ b/rpc/websockets/types.go @@ -0,0 +1,45 @@ +package websockets + +import ( + "math/big" + + "github.com/gorilla/websocket" + + "github.com/ethereum/go-ethereum/rpc" + + rpcfilters "github.com/cosmos/ethermint/rpc/namespaces/eth/filters" +) + +type SubscriptionResponseJSON struct { + Jsonrpc string `json:"jsonrpc"` + Result interface{} `json:"result"` + ID float64 `json:"id"` +} + +type SubscriptionNotification struct { + Jsonrpc string `json:"jsonrpc"` + Method string `json:"method"` + Params *SubscriptionResult `json:"params"` +} + +type SubscriptionResult struct { + Subscription rpc.ID `json:"subscription"` + Result interface{} `json:"result"` +} + +type ErrorResponseJSON struct { + Jsonrpc string `json:"jsonrpc"` + Error *ErrorMessageJSON `json:"error"` + ID *big.Int `json:"id"` +} + +type ErrorMessageJSON struct { + Code *big.Int `json:"code"` + Message string `json:"message"` +} + +type wsSubscription struct { + sub *rpcfilters.Subscription + unsubscribed chan struct{} // closed when unsubscribing + conn *websocket.Conn +} diff --git a/tests/rpc_test.go b/tests/rpc_test.go index 28bf81d61..fe3ae2906 100644 --- a/tests/rpc_test.go +++ b/tests/rpc_test.go @@ -14,7 +14,6 @@ import ( "math/big" "net/http" "os" - "strings" "testing" "time" @@ -24,9 +23,8 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" ethtypes "github.com/ethereum/go-ethereum/core/types" - "github.com/cosmos/ethermint/rpc" + rpctypes "github.com/cosmos/ethermint/rpc/types" "github.com/cosmos/ethermint/version" - "github.com/cosmos/ethermint/x/evm/types" ) const ( @@ -191,7 +189,14 @@ func TestEth_GetLogs_NoLogs(t *testing.T) { param := make([]map[string][]string, 1) param[0] = make(map[string][]string) param[0]["topics"] = []string{} - call(t, "eth_getLogs", param) + rpcRes := call(t, "eth_getLogs", param) + require.NotNil(t, rpcRes) + require.Nil(t, rpcRes.Error) + + var logs []*ethtypes.Log + err := json.Unmarshal(rpcRes.Result, &logs) + require.NoError(t, err) + require.NotEmpty(t, logs) } func TestEth_GetLogs_Topics_AB(t *testing.T) { @@ -332,7 +337,7 @@ func TestEth_GetProof(t *testing.T) { rpcRes := call(t, "eth_getProof", params) require.NotNil(t, rpcRes) - var accRes rpc.AccountResult + var accRes rpctypes.AccountResult err := json.Unmarshal(rpcRes.Result, &accRes) require.NoError(t, err) require.NotEmpty(t, accRes.AccountProof) @@ -779,68 +784,6 @@ func TestEth_EstimateGas_ContractDeployment(t *testing.T) { require.Equal(t, "0x1c2c4", gas.String()) } -func TestEth_ExportAccount(t *testing.T) { - param := []string{} - param = append(param, "0x1122334455667788990011223344556677889901") - param = append(param, "latest") - rpcRes := call(t, "eth_exportAccount", param) - - var res string - err := json.Unmarshal(rpcRes.Result, &res) - require.NoError(t, err) - - var account types.GenesisAccount - err = json.Unmarshal([]byte(res), &account) - require.NoError(t, err) - - require.Equal(t, "0x1122334455667788990011223344556677889901", account.Address.Hex()) - require.Equal(t, big.NewInt(0), account.Balance) - require.Equal(t, hexutil.Bytes(nil), account.Code) - require.Equal(t, types.Storage(nil), account.Storage) -} - -func TestEth_ExportAccount_WithStorage(t *testing.T) { - hash := deployTestContractWithFunction(t) - receipt := waitForReceipt(t, hash) - addr := receipt["contractAddress"].(string) - - // call function to set storage - calldata := "0xeb8ac92100000000000000000000000000000000000000000000000000000000000000630000000000000000000000000000000000000000000000000000000000000000" - - param := make([]map[string]string, 1) - param[0] = make(map[string]string) - param[0]["from"] = "0x" + fmt.Sprintf("%x", from) - param[0]["to"] = addr - param[0]["data"] = calldata - rpcRes := call(t, "eth_sendTransaction", param) - - var txhash hexutil.Bytes - err := json.Unmarshal(rpcRes.Result, &txhash) - require.NoError(t, err) - waitForReceipt(t, txhash) - - // get exported account - eap := []string{} - eap = append(eap, addr) - eap = append(eap, "latest") - rpcRes = call(t, "eth_exportAccount", eap) - - var res string - err = json.Unmarshal(rpcRes.Result, &res) - require.NoError(t, err) - - var account types.GenesisAccount - err = json.Unmarshal([]byte(res), &account) - require.NoError(t, err) - - // deployed bytecode - bytecode := "0x6080604052348015600f57600080fd5b506004361060285760003560e01c8063eb8ac92114602d575b600080fd5b606060048036036040811015604157600080fd5b8101908080359060200190929190803590602001909291905050506062565b005b8160008190555080827ff3ca124a697ba07e8c5e80bebcfcc48991fc16a63170e8a9206e30508960d00360405160405180910390a3505056fea265627a7a723158201d94d2187aaf3a6790527b615fcc40970febf0385fa6d72a2344848ebd0df3e964736f6c63430005110032" - require.Equal(t, addr, strings.ToLower(account.Address.Hex())) - require.Equal(t, big.NewInt(0), account.Balance) - require.Equal(t, bytecode, account.Code.String()) - require.NotEqual(t, types.Storage(nil), account.Storage) -} - func TestEth_GetBlockByNumber(t *testing.T) { param := []interface{}{"0x1", false} rpcRes := call(t, "eth_getBlockByNumber", param) diff --git a/x/evm/client/cli/query.go b/x/evm/client/cli/query.go index f274a20ea..0a73e9930 100644 --- a/x/evm/client/cli/query.go +++ b/x/evm/client/cli/query.go @@ -37,7 +37,7 @@ func GetCmdGetStorageAt(queryRoute string, cdc *codec.Codec) *cobra.Command { Short: "Gets storage for an account at a given key", Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { - cliCtx := context.NewCLIContext().WithCodec(cdc) + clientCtx := context.NewCLIContext().WithCodec(cdc) account, err := accountToHex(args[0]) if err != nil { @@ -46,7 +46,7 @@ func GetCmdGetStorageAt(queryRoute string, cdc *codec.Codec) *cobra.Command { key := formatKeyToHash(args[1]) - res, _, err := cliCtx.Query( + res, _, err := clientCtx.Query( fmt.Sprintf("custom/%s/storage/%s/%s", queryRoute, account, key)) if err != nil { @@ -54,7 +54,7 @@ func GetCmdGetStorageAt(queryRoute string, cdc *codec.Codec) *cobra.Command { } var out types.QueryResStorage cdc.MustUnmarshalJSON(res, &out) - return cliCtx.PrintOutput(out) + return clientCtx.PrintOutput(out) }, } } @@ -66,14 +66,14 @@ func GetCmdGetCode(queryRoute string, cdc *codec.Codec) *cobra.Command { Short: "Gets code from an account", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - cliCtx := context.NewCLIContext().WithCodec(cdc) + clientCtx := context.NewCLIContext().WithCodec(cdc) account, err := accountToHex(args[0]) if err != nil { return errors.Wrap(err, "could not parse account address") } - res, _, err := cliCtx.Query( + res, _, err := clientCtx.Query( fmt.Sprintf("custom/%s/code/%s", queryRoute, account)) if err != nil { @@ -82,7 +82,7 @@ func GetCmdGetCode(queryRoute string, cdc *codec.Codec) *cobra.Command { var out types.QueryResCode cdc.MustUnmarshalJSON(res, &out) - return cliCtx.PrintOutput(out) + return clientCtx.PrintOutput(out) }, } } diff --git a/x/evm/client/cli/tx.go b/x/evm/client/cli/tx.go deleted file mode 100644 index 84258ac68..000000000 --- a/x/evm/client/cli/tx.go +++ /dev/null @@ -1,164 +0,0 @@ -package cli - -import ( - "bufio" - "fmt" - "strconv" - "strings" - - "github.com/pkg/errors" - "github.com/spf13/cobra" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - ethcrypto "github.com/ethereum/go-ethereum/crypto" - - "github.com/cosmos/cosmos-sdk/client" - "github.com/cosmos/cosmos-sdk/client/context" - "github.com/cosmos/cosmos-sdk/client/flags" - "github.com/cosmos/cosmos-sdk/codec" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/auth" - authclient "github.com/cosmos/cosmos-sdk/x/auth/client/utils" - authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" - - emint "github.com/cosmos/ethermint/types" - "github.com/cosmos/ethermint/x/evm/types" -) - -// GetTxCmd defines the CLI commands regarding evm module transactions -func GetTxCmd(cdc *codec.Codec) *cobra.Command { - evmTxCmd := &cobra.Command{ - Use: types.ModuleName, - Short: "EVM transaction subcommands", - DisableFlagParsing: true, - SuggestionsMinimumDistance: 2, - RunE: client.ValidateCmd, - } - - evmTxCmd.AddCommand(flags.PostCommands( - GetCmdSendTx(cdc), - GetCmdGenCreateTx(cdc), - )...) - - return evmTxCmd -} - -// GetCmdSendTx generates an Ethermint transaction (excludes create operations) -func GetCmdSendTx(cdc *codec.Codec) *cobra.Command { - return &cobra.Command{ - Use: "send [to_address] [amount (in aphotons)] []", - Short: "send transaction to address (call operations included)", - Args: cobra.RangeArgs(2, 3), - RunE: func(cmd *cobra.Command, args []string) error { - cliCtx := context.NewCLIContext().WithCodec(cdc) - inBuf := bufio.NewReader(cmd.InOrStdin()) - - txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(authclient.GetTxEncoder(cdc)) - - toAddr, err := cosmosAddressFromArg(args[0]) - if err != nil { - return errors.Wrap(err, "must provide a valid Bech32 address for to_address") - } - - // Ambiguously decode amount from any base - amount, err := strconv.ParseInt(args[1], 0, 64) - if err != nil { - return err - } - - var data []byte - if len(args) > 2 { - payload := args[2] - if !strings.HasPrefix(payload, "0x") { - payload = "0x" + payload - } - - data, err = hexutil.Decode(payload) - if err != nil { - return err - } - } - - from := cliCtx.GetFromAddress() - - _, seq, err := authtypes.NewAccountRetriever(cliCtx).GetAccountNumberSequence(from) - if err != nil { - return errors.Wrap(err, "Could not retrieve account sequence") - } - - // TODO: Potentially allow overriding of gas price and gas limit - msg := types.NewMsgEthermint(seq, &toAddr, sdk.NewInt(amount), txBldr.Gas(), - sdk.NewInt(emint.DefaultGasPrice), data, from) - - err = msg.ValidateBasic() - if err != nil { - return err - } - - return authclient.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) - }, - } -} - -// GetCmdGenCreateTx generates an Ethermint transaction (excludes create operations) -func GetCmdGenCreateTx(cdc *codec.Codec) *cobra.Command { - return &cobra.Command{ - Use: "create [contract bytecode] []", - Short: "create contract through the evm using compiled bytecode", - Args: cobra.RangeArgs(1, 2), - RunE: func(cmd *cobra.Command, args []string) error { - cliCtx := context.NewCLIContext().WithCodec(cdc) - inBuf := bufio.NewReader(cmd.InOrStdin()) - - txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(authclient.GetTxEncoder(cdc)) - - payload := args[0] - if !strings.HasPrefix(payload, "0x") { - payload = "0x" + payload - } - - data, err := hexutil.Decode(payload) - if err != nil { - return err - } - - var amount int64 - if len(args) > 1 { - // Ambiguously decode amount from any base - amount, err = strconv.ParseInt(args[1], 0, 64) - if err != nil { - return errors.Wrap(err, "invalid amount") - } - } - - from := cliCtx.GetFromAddress() - - _, seq, err := authtypes.NewAccountRetriever(cliCtx).GetAccountNumberSequence(from) - if err != nil { - return errors.Wrap(err, "Could not retrieve account sequence") - } - - // TODO: Potentially allow overriding of gas price and gas limit - msg := types.NewMsgEthermint(seq, nil, sdk.NewInt(amount), txBldr.Gas(), - sdk.NewInt(emint.DefaultGasPrice), data, from) - - err = msg.ValidateBasic() - if err != nil { - return err - } - - if err = authclient.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}); err != nil { - return err - } - - contractAddr := ethcrypto.CreateAddress(common.BytesToAddress(from.Bytes()), seq) - fmt.Printf( - "Contract will be deployed to: \nHex: %s\nCosmos Address: %s\n", - contractAddr.Hex(), - sdk.AccAddress(contractAddr.Bytes()), - ) - return nil - }, - } -} diff --git a/x/evm/client/cli/utils.go b/x/evm/client/cli/utils.go index 6114246ca..7277cdae8 100644 --- a/x/evm/client/cli/utils.go +++ b/x/evm/client/cli/utils.go @@ -45,19 +45,3 @@ func formatKeyToHash(key string) string { return ethkey.Hex() } - -func cosmosAddressFromArg(addr string) (sdk.AccAddress, error) { - if strings.HasPrefix(addr, sdk.GetConfig().GetBech32AccountAddrPrefix()) { - // Check to see if address is Cosmos bech32 formatted - toAddr, err := sdk.AccAddressFromBech32(addr) - if err != nil { - return nil, errors.Wrap(err, "invalid bech32 formatted address") - } - return toAddr, nil - } - - // Strip 0x prefix if exists - addr = strings.TrimPrefix(addr, "0x") - - return sdk.AccAddressFromHex(addr) -} diff --git a/x/evm/client/cli/utils_test.go b/x/evm/client/cli/utils_test.go index 8ddb31028..ffe18a5a1 100644 --- a/x/evm/client/cli/utils_test.go +++ b/x/evm/client/cli/utils_test.go @@ -61,24 +61,3 @@ func TestCosmosToEthereumTypes(t *testing.T) { require.NoError(t, err) require.Equal(t, hexString, ethDecoded) } - -func TestAddressToCosmosAddress(t *testing.T) { - baseAddr, err := sdk.AccAddressFromHex("6A98D72760f7bbA69d62Ed6F48278451251948E7") - require.NoError(t, err) - - // Test cosmos string back to address - cosmosFormatted, err := cosmosAddressFromArg(baseAddr.String()) - require.NoError(t, err) - require.Equal(t, baseAddr, cosmosFormatted) - - // Test account address from Ethereum address - ethAddr := common.BytesToAddress(baseAddr.Bytes()) - ethFormatted, err := cosmosAddressFromArg(ethAddr.Hex()) - require.NoError(t, err) - require.Equal(t, baseAddr, ethFormatted) - - // Test encoding without the 0x prefix - ethFormatted, err = cosmosAddressFromArg(ethAddr.Hex()[2:]) - require.NoError(t, err) - require.Equal(t, baseAddr, ethFormatted) -} diff --git a/x/evm/module.go b/x/evm/module.go index 6b5de639c..28d09bc90 100644 --- a/x/evm/module.go +++ b/x/evm/module.go @@ -52,7 +52,6 @@ func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error { // RegisterRESTRoutes Registers rest routes func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router) { - //rpc.RegisterRoutes(ctx, rtr, StoreKey) } // GetQueryCmd Gets the root query command of this module @@ -62,7 +61,7 @@ func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command { // GetTxCmd Gets the root tx command of this module func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command { - return cli.GetTxCmd(cdc) + return nil } //____________________________________________________________________________ diff --git a/x/faucet/client/cli/query.go b/x/faucet/client/cli/query.go index 0a928c7cf..65725ecb3 100644 --- a/x/faucet/client/cli/query.go +++ b/x/faucet/client/cli/query.go @@ -37,17 +37,17 @@ func GetCmdFunded(cdc *codec.Codec) *cobra.Command { Short: "Gets storage for an account at a given key", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - cliCtx := context.NewCLIContext().WithCodec(cdc) + clientCtx := context.NewCLIContext().WithCodec(cdc) - res, height, err := cliCtx.Query(fmt.Sprintf("custom/%s/%s", types.QuerierRoute, types.QueryFunded)) + res, height, err := clientCtx.Query(fmt.Sprintf("custom/%s/%s", types.QuerierRoute, types.QueryFunded)) if err != nil { return err } var out sdk.Coins cdc.MustUnmarshalJSON(res, &out) - cliCtx = cliCtx.WithHeight(height) - return cliCtx.PrintOutput(out) + clientCtx = clientCtx.WithHeight(height) + return clientCtx.PrintOutput(out) }, } } diff --git a/x/faucet/client/cli/tx.go b/x/faucet/client/cli/tx.go index 2faf648c9..d33820053 100644 --- a/x/faucet/client/cli/tx.go +++ b/x/faucet/client/cli/tx.go @@ -41,7 +41,7 @@ func GetCmdRequest(cdc *codec.Codec) *cobra.Command { Args: cobra.RangeArgs(1, 2), RunE: func(cmd *cobra.Command, args []string) error { inBuf := bufio.NewReader(cmd.InOrStdin()) - cliCtx := context.NewCLIContext().WithCodec(cdc) + clientCtx := context.NewCLIContext().WithCodec(cdc) txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(authclient.GetTxEncoder(cdc)) amount, err := sdk.ParseCoins(args[0]) @@ -51,7 +51,7 @@ func GetCmdRequest(cdc *codec.Codec) *cobra.Command { var recipient sdk.AccAddress if len(args) == 1 { - recipient = cliCtx.GetFromAddress() + recipient = clientCtx.GetFromAddress() } else { recipient, err = sdk.AccAddressFromBech32(args[1]) } @@ -60,12 +60,12 @@ func GetCmdRequest(cdc *codec.Codec) *cobra.Command { return err } - msg := types.NewMsgFund(amount, cliCtx.GetFromAddress(), recipient) + msg := types.NewMsgFund(amount, clientCtx.GetFromAddress(), recipient) if err := msg.ValidateBasic(); err != nil { return err } - return authclient.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + return authclient.GenerateOrBroadcastMsgs(clientCtx, txBldr, []sdk.Msg{msg}) }, } } diff --git a/x/faucet/client/rest/tx.go b/x/faucet/client/rest/tx.go index f475bf963..dd634694a 100644 --- a/x/faucet/client/rest/tx.go +++ b/x/faucet/client/rest/tx.go @@ -15,9 +15,9 @@ import ( ) // RegisterRoutes register REST endpoints for the faucet module -func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) { - r.HandleFunc(fmt.Sprintf("/%s/request", types.ModuleName), requestHandler(cliCtx)).Methods("POST") - r.HandleFunc(fmt.Sprintf("/%s/funded", types.ModuleName), fundedHandlerFn(cliCtx)).Methods("GET") +func RegisterRoutes(clientCtx context.CLIContext, r *mux.Router) { + r.HandleFunc(fmt.Sprintf("/%s/request", types.ModuleName), requestHandler(clientCtx)).Methods("POST") + r.HandleFunc(fmt.Sprintf("/%s/funded", types.ModuleName), fundedHandlerFn(clientCtx)).Methods("GET") } // PostRequestBody defines fund request's body. @@ -27,10 +27,10 @@ type PostRequestBody struct { Recipient string `json:"receipient" yaml:"receipient"` } -func requestHandler(cliCtx context.CLIContext) http.HandlerFunc { +func requestHandler(clientCtx context.CLIContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req PostRequestBody - if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) { + if !rest.ReadRESTReq(w, r, clientCtx.Codec, &req) { rest.WriteErrorResponse(w, http.StatusBadRequest, "failed to parse request") return } @@ -65,19 +65,19 @@ func requestHandler(cliCtx context.CLIContext) http.HandlerFunc { return } - authclient.WriteGenerateStdTxResponse(w, cliCtx, baseReq, []sdk.Msg{msg}) + authclient.WriteGenerateStdTxResponse(w, clientCtx, baseReq, []sdk.Msg{msg}) } } -func fundedHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { +func fundedHandlerFn(clientCtx context.CLIContext) http.HandlerFunc { return func(w http.ResponseWriter, _ *http.Request) { - res, height, err := cliCtx.Query(fmt.Sprintf("custom/%s/%s", types.QuerierRoute, types.QueryFunded)) + res, height, err := clientCtx.Query(fmt.Sprintf("custom/%s/%s", types.QuerierRoute, types.QueryFunded)) if err != nil { rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return } - cliCtx = cliCtx.WithHeight(height) - rest.PostProcessResponse(w, cliCtx, res) + clientCtx = clientCtx.WithHeight(height) + rest.PostProcessResponse(w, clientCtx, res) } }