From fb6e4c8903848701e312a7bb69c0c6e7f64857db Mon Sep 17 00:00:00 2001 From: beer-1 <147697694+beer-1@users.noreply.github.com> Date: Fri, 12 Jul 2024 18:19:24 +0900 Subject: [PATCH] add more json rpc support (#35) --- jsonrpc/README.md | 19 +++-- jsonrpc/backend/eth.go | 34 +++++++++ jsonrpc/backend/net.go | 21 ++++++ jsonrpc/backend/txpool.go | 119 +++++++++++++++++++++++++++++++ jsonrpc/backend/web3.go | 11 +++ jsonrpc/jsonrpc.go | 31 ++++---- jsonrpc/namespaces/eth/api.go | 31 ++++---- jsonrpc/namespaces/net/api.go | 16 ++--- jsonrpc/namespaces/txpool/api.go | 48 ++++++------- jsonrpc/namespaces/web3/api.go | 45 ++++++++++++ 10 files changed, 303 insertions(+), 72 deletions(-) create mode 100644 jsonrpc/backend/txpool.go create mode 100644 jsonrpc/backend/web3.go create mode 100644 jsonrpc/namespaces/web3/api.go diff --git a/jsonrpc/README.md b/jsonrpc/README.md index d96225c..9005cab 100644 --- a/jsonrpc/README.md +++ b/jsonrpc/README.md @@ -6,13 +6,12 @@ The ETH JSON-RPC (Remote Procedure Call) is a protocol that allows clients to in | namespace | api | supported | description | | --------- | ------------------------------------------ | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| web3 | web3_clientVersion | 🚫 | Returns the current client version. | -| web3 | web3_sha3 | 🚫 | Returns the Keccak-256 (not the standardized SHA3-256) of the given data. | +| web3 | web3_clientVersion | ✅ | Returns the current client version. | +| web3 | web3_sha3 | ✅ | Returns the Keccak-256 (not the standardized SHA3-256) of the given data. | | net | net_version | ✅ | Returns the current network ID. | -| net | net_peerCount | 🚫 | Returns the number of peers currently connected to the client. | -| net | net_listening | 🚫 | Returns true if the client is actively listening for network connections. | -| eth | eth_protocolVersion | 🚫 | Returns the current Ethereum protocol version. | -| eth | eth_syncing | 🚫 | Returns an object with data about the sync status or false if not syncing. | +| net | net_peerCount | ✅ | Returns the number of peers currently connected to the client. | +| net | net_listening | ✅ | Returns true if the client is actively listening for network connections. | +| eth | eth_syncing | ✅ | Returns an object with data about the sync status or false if not syncing. | | eth | eth_coinbase | 🚫 | Returns the client coinbase address. | | eth | eth_chainId | ✅ | Returns the chain id. | | eth | eth_mining | 🚫 | Returns true if the client is actively mining new blocks. | @@ -73,7 +72,7 @@ The ETH JSON-RPC (Remote Procedure Call) is a protocol that allows clients to in | debug | debug_traceBlockByHash | 🚫 | Returns trace of a block by hash. | | debug | debug_storageRangeAt | 🚫 | Returns a storage range at a specific position. | | debug | debug_getBadBlocks | 🚫 | Returns list of bad blocks. | -| txpool | txpool_content | 🚫 | Returns all pending and queued transactions | -| txpool | txpool_inspect | 🚫 | Returns a textual summary of all pending and queued transactions | -| txpool | txpool_contentFrom | 🚫 | Retrieves the transactions contained within the txpool, returning pending and queued transactions of this address, grouped by nonce | -| txpool | txpool_status | 🚫 | Returns the number of transactions in pending and queued states | +| txpool | txpool_content | ✅ | Returns all pending and queued transactions | +| txpool | txpool_inspect | ✅ | Returns a textual summary of all pending and queued transactions | +| txpool | txpool_contentFrom | ✅ | Retrieves the transactions contained within the txpool, returning pending and queued transactions of this address, grouped by nonce | +| txpool | txpool_status | ✅ | Returns the number of transactions in pending and queued states | diff --git a/jsonrpc/backend/eth.go b/jsonrpc/backend/eth.go index 8c39560..59d124c 100644 --- a/jsonrpc/backend/eth.go +++ b/jsonrpc/backend/eth.go @@ -157,3 +157,37 @@ func (b *JSONRPCBackend) ChainID() (*big.Int, error) { sdkCtx := sdk.UnwrapSDKContext(queryCtx) return types.ConvertCosmosChainIDToEthereumChainID(sdkCtx.ChainID()), nil } + +func (b *JSONRPCBackend) Syncing() (interface{}, error) { + status, err := b.clientCtx.Client.Status(b.ctx) + if err != nil { + return nil, err + } + + if !status.SyncInfo.CatchingUp { + return false, nil + } + + latestHeight := status.SyncInfo.LatestBlockHeight + + // Otherwise gather the block sync stats + return map[string]interface{}{ + "startingBlock": hexutil.Uint64(status.SyncInfo.EarliestBlockHeight), + "currentBlock": hexutil.Uint64(latestHeight), + "highestBlock": hexutil.Uint64(latestHeight), + "syncedAccounts": hexutil.Uint64(latestHeight), + "syncedAccountBytes": hexutil.Uint64(latestHeight), + "syncedBytecodes": hexutil.Uint64(latestHeight), + "syncedBytecodeBytes": hexutil.Uint64(latestHeight), + "syncedStorage": hexutil.Uint64(latestHeight), + "syncedStorageBytes": hexutil.Uint64(latestHeight), + "healedTrienodes": hexutil.Uint64(latestHeight), + "healedTrienodeBytes": hexutil.Uint64(latestHeight), + "healedBytecodes": hexutil.Uint64(latestHeight), + "healedBytecodeBytes": hexutil.Uint64(latestHeight), + "healingTrienodes": hexutil.Uint64(latestHeight), + "healingBytecode": hexutil.Uint64(latestHeight), + "txIndexFinishedBlocks": hexutil.Uint64(latestHeight), + "txIndexRemainingBlocks": hexutil.Uint64(latestHeight), + }, nil +} diff --git a/jsonrpc/backend/net.go b/jsonrpc/backend/net.go index d8c1a7b..4e84eee 100644 --- a/jsonrpc/backend/net.go +++ b/jsonrpc/backend/net.go @@ -4,7 +4,10 @@ import ( "strconv" "strings" + rpcclient "github.com/cometbft/cometbft/rpc/client" sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/ethereum/go-ethereum/common/hexutil" ) func (b *JSONRPCBackend) Version() (string, error) { @@ -23,3 +26,21 @@ func (b *JSONRPCBackend) Version() (string, error) { return version, err } + +func (b *JSONRPCBackend) PeerCount() (hexutil.Uint, error) { + netInfo, err := b.clientCtx.Client.(rpcclient.NetworkClient).NetInfo(b.ctx) + if err != nil { + return hexutil.Uint(0), err + } + + return hexutil.Uint(netInfo.NPeers), nil +} + +func (b *JSONRPCBackend) Listening() (bool, error) { + netInfo, err := b.clientCtx.Client.(rpcclient.NetworkClient).NetInfo(b.ctx) + if err != nil { + return false, err + } + + return netInfo.Listening, nil +} diff --git a/jsonrpc/backend/txpool.go b/jsonrpc/backend/txpool.go new file mode 100644 index 0000000..4008d4f --- /dev/null +++ b/jsonrpc/backend/txpool.go @@ -0,0 +1,119 @@ +package backend + +import ( + "fmt" + + rpcclient "github.com/cometbft/cometbft/rpc/client" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + + rpctypes "github.com/initia-labs/minievm/jsonrpc/types" + "github.com/initia-labs/minievm/x/evm/keeper" +) + +func (b *JSONRPCBackend) TxPoolContent() (map[string]map[string]map[string]*rpctypes.RPCTransaction, error) { + content := map[string]map[string]map[string]*rpctypes.RPCTransaction{ + "pending": make(map[string]map[string]*rpctypes.RPCTransaction), + "queued": make(map[string]map[string]*rpctypes.RPCTransaction), + } + + limit := int(100) + pending, err := b.clientCtx.Client.(rpcclient.MempoolClient).UnconfirmedTxs(b.ctx, &limit) + if err != nil { + return nil, err + } + + ctx, err := b.getQueryCtx() + if err != nil { + return nil, err + } + + chainID, err := b.ChainID() + if err != nil { + return nil, err + } + + txUtils := keeper.NewTxUtils(b.app.EVMKeeper) + for _, tx := range pending.Txs { + cosmosTx, err := b.app.TxDecode(tx) + if err != nil { + return nil, err + } + + ethTx, account, err := txUtils.ConvertCosmosTxToEthereumTx(ctx, cosmosTx) + if err != nil { + return nil, err + } + if ethTx == nil { + continue + } + + dump, ok := content["pending"][account.Hex()] + if !ok { + dump = make(map[string]*rpctypes.RPCTransaction) + content["pending"][account.Hex()] = dump + } + + dump[fmt.Sprintf("%d", ethTx.Nonce())] = rpctypes.NewRPCTransaction(ethTx, common.Hash{}, 0, 0, chainID) + } + + return content, nil +} + +func (b *JSONRPCBackend) TxPoolContentFrom(addr common.Address) (map[string]map[string]*rpctypes.RPCTransaction, error) { + content, err := b.TxPoolContent() + if err != nil { + return nil, err + } + + dump := content["pending"][addr.Hex()] + accountContent := make(map[string]map[string]*rpctypes.RPCTransaction, 2) + accountContent["pending"] = dump + accountContent["queued"] = make(map[string]*rpctypes.RPCTransaction) + + return accountContent, nil +} + +// Status returns the number of pending and queued transaction in the pool. +func (b *JSONRPCBackend) TxPoolStatus() (map[string]hexutil.Uint, error) { + numUnconfirmedTxs, err := b.clientCtx.Client.(rpcclient.MempoolClient).NumUnconfirmedTxs(b.ctx) + if err != nil { + return nil, err + } + + return map[string]hexutil.Uint{ + "pending": hexutil.Uint(numUnconfirmedTxs.Count), + "queued": hexutil.Uint(0), + }, nil +} + +// Inspect retrieves the content of the transaction pool and flattens it into an +// easily inspectable list. +func (b *JSONRPCBackend) TxPoolInspect() (map[string]map[string]map[string]string, error) { + inspectContent := map[string]map[string]map[string]string{ + "pending": make(map[string]map[string]string), + "queued": make(map[string]map[string]string), + } + + content, err := b.TxPoolContent() + if err != nil { + return nil, err + } + + // Define a formatter to flatten a transaction into a string + var format = func(tx *rpctypes.RPCTransaction) string { + if to := tx.To; to != nil { + return fmt.Sprintf("%s: %v wei + %v gas × %v wei", tx.To.Hex(), tx.Value, tx.Gas, tx.GasPrice) + } + return fmt.Sprintf("contract creation: %v wei + %v gas × %v wei", tx.Value, tx.Gas, tx.GasPrice) + } + // Flatten the pending transactions + for account, txs := range content["pending"] { + dump := make(map[string]string) + for _, tx := range txs { + dump[fmt.Sprintf("%d", tx.Nonce)] = format(tx) + } + inspectContent["pending"][account] = dump + } + return inspectContent, nil +} diff --git a/jsonrpc/backend/web3.go b/jsonrpc/backend/web3.go new file mode 100644 index 0000000..bd395c9 --- /dev/null +++ b/jsonrpc/backend/web3.go @@ -0,0 +1,11 @@ +package backend + +// ClientVersion returns the node name +func (b *JSONRPCBackend) ClientVersion() (string, error) { + status, err := b.clientCtx.Client.Status(b.ctx) + if err != nil { + return "", err + } + + return status.NodeInfo.Version, nil +} diff --git a/jsonrpc/jsonrpc.go b/jsonrpc/jsonrpc.go index 2943013..a6b7388 100644 --- a/jsonrpc/jsonrpc.go +++ b/jsonrpc/jsonrpc.go @@ -6,11 +6,6 @@ import ( "net/http" "github.com/gorilla/mux" - ethns "github.com/initia-labs/minievm/jsonrpc/namespaces/eth" - "github.com/initia-labs/minievm/jsonrpc/namespaces/eth/filters" - - // "github.com/initia-labs/minievm/jsonrpc/namespaces/eth/filters" - netns "github.com/initia-labs/minievm/jsonrpc/namespaces/net" "github.com/rs/cors" "golang.org/x/net/netutil" "golang.org/x/sync/errgroup" @@ -24,6 +19,11 @@ import ( "github.com/initia-labs/minievm/app" "github.com/initia-labs/minievm/jsonrpc/backend" "github.com/initia-labs/minievm/jsonrpc/config" + ethns "github.com/initia-labs/minievm/jsonrpc/namespaces/eth" + "github.com/initia-labs/minievm/jsonrpc/namespaces/eth/filters" + netns "github.com/initia-labs/minievm/jsonrpc/namespaces/net" + txpoolns "github.com/initia-labs/minievm/jsonrpc/namespaces/txpool" + web3ns "github.com/initia-labs/minievm/jsonrpc/namespaces/web3" ) // RPC namespaces and API version @@ -32,8 +32,8 @@ const ( EthNamespace = "eth" NetNamespace = "net" TxPoolNamespace = "txpool" + Web3Namespace = "web3" // TODO: support more namespaces - Web3Namespace = "web3" PersonalNamespace = "personal" DebugNamespace = "debug" MinerNamespace = "miner" @@ -73,13 +73,18 @@ func StartJSONRPC( Service: netns.NewNetAPI(svrCtx.Logger, bkd), Public: true, }, - // TODO: implement more namespaces - //{ - // Namespace: TxPoolNamespace, - // Version: apiVersion, - // Service: txpool.NewTxPoolAPI(svrCtx.Logger, bkd), - // Public: true, - //}, + { + Namespace: Web3Namespace, + Version: apiVersion, + Service: web3ns.NewWeb3API(svrCtx.Logger, bkd), + Public: true, + }, + { + Namespace: TxPoolNamespace, + Version: apiVersion, + Service: txpoolns.NewTxPoolAPI(svrCtx.Logger, bkd), + Public: true, + }, } for _, api := range apis { diff --git a/jsonrpc/namespaces/eth/api.go b/jsonrpc/namespaces/eth/api.go index 5d2945d..9e2087b 100644 --- a/jsonrpc/namespaces/eth/api.go +++ b/jsonrpc/namespaces/eth/api.go @@ -48,7 +48,7 @@ type EthEthereumAPI interface { // Allows developers to both send ETH from one address to another, write data // on-chain, and interact with smart contracts. SendRawTransaction(data hexutil.Bytes) (common.Hash, error) - //SendTransaction(args rpctypes.TransactionArgs) (common.Hash, error) + // SendTransaction(args rpctypes.TransactionArgs) (common.Hash, error) // eth_sendPrivateTransaction // eth_cancel PrivateTransaction @@ -67,7 +67,7 @@ type EthEthereumAPI interface { // smart contracts. However, no data is published to the Ethereum network. Call(args rpctypes.TransactionArgs, blockNrOrHash *rpc.BlockNumberOrHash, so *rpctypes.StateOverride, bo *rpctypes.BlockOverrides) (hexutil.Bytes, error) - // // Chain Information + // Chain Information // // // // Returns information on the Ethereum network and internal settings. // ProtocolVersion() hexutil.Uint @@ -77,8 +77,8 @@ type EthEthereumAPI interface { MaxPriorityFeePerGas() (*hexutil.Big, error) ChainId() *hexutil.Big - // // Other - // Syncing() (interface{}, error) + // Other + Syncing() (interface{}, error) // Coinbase() (string, error) // Sign(address common.Address, data hexutil.Bytes) (hexutil.Bytes, error) // GetTransactionLogs(txHash common.Hash) ([]*ethtypes.Log, error) @@ -384,18 +384,17 @@ func (api *EthAPI) Mining() bool { // * others * // ************************************* -// TODO: Implement eth_syncing -//// Syncing returns false in case the node is currently not syncing with the network. It can be up to date or has not -//// yet received the latest block headers from its pears. In case it is synchronizing: -//// - startingBlock: block number this node started to synchronize from -//// - currentBlock: block number this node is currently importing -//// - highestBlock: block number of the highest block header this node has received from peers -//// - pulledStates: number of state entries processed until now -//// - knownStates: number of known state entries that still need to be pulled -//func (e *EthAPI) Syncing() (interface{}, error) { -// e.logger.Debug("eth_syncing") -// return e.backend.Syncing() -//} +// Syncing returns false in case the node is currently not syncing with the network. It can be up to date or has not +// yet received the latest block headers from its pears. In case it is synchronizing: +// - startingBlock: block number this node started to synchronize from +// - currentBlock: block number this node is currently importing +// - highestBlock: block number of the highest block header this node has received from peers +// - pulledStates: number of state entries processed until now +// - knownStates: number of known state entries that still need to be pulled +func (e *EthAPI) Syncing() (interface{}, error) { + e.logger.Debug("eth_syncing") + return e.backend.Syncing() +} // TODO: Implement eth_coinbase //// Coinbase is the address that staking rewards will be send to (alias for Etherbase). diff --git a/jsonrpc/namespaces/net/api.go b/jsonrpc/namespaces/net/api.go index 557beaf..23c8732 100644 --- a/jsonrpc/namespaces/net/api.go +++ b/jsonrpc/namespaces/net/api.go @@ -32,15 +32,13 @@ func NewNetAPI(logger log.Logger, backend *backend.JSONRPCBackend) *NetAPI { } } -// TODO: implement net_listening -//func (api *NetAPI) Listening() bool { -// return true -//} - -// TODO: implement net_peerCount -//func (api *NetAPI) PeerCount() hexutil.Uint { -// return hexutil.Uint(0) -//} +func (api *NetAPI) Listening() (bool, error) { + return api.backend.Listening() +} + +func (api *NetAPI) PeerCount() (hexutil.Uint, error) { + return api.backend.PeerCount() +} func (api *NetAPI) Version() string { v, err := api.backend.Version() diff --git a/jsonrpc/namespaces/txpool/api.go b/jsonrpc/namespaces/txpool/api.go index f426006..b64bb69 100644 --- a/jsonrpc/namespaces/txpool/api.go +++ b/jsonrpc/namespaces/txpool/api.go @@ -4,7 +4,12 @@ import ( "context" "cosmossdk.io/log" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/initia-labs/minievm/jsonrpc/backend" + rpctypes "github.com/initia-labs/minievm/jsonrpc/types" ) var _ TxpoolEthereumAPI = (*TxPoolAPI)(nil) @@ -13,11 +18,10 @@ var _ TxpoolEthereumAPI = (*TxPoolAPI)(nil) // Current it is used for tracking what APIs should be implemented for Ethereum compatibility. // After fully implementing the Ethereum APIs, this interface can be removed. type TxpoolEthereumAPI interface { - // TODO: implement the following apis - //Content() map[string]map[string]map[string]*rpctypes.RPCTransaction - //ContentFrom() map[string]map[string]*rpctypes.RPCTransaction - //Inspect() map[string]map[string]map[string]string - //Status() map[string]hexutil.Uint + Content() (map[string]map[string]map[string]*rpctypes.RPCTransaction, error) + ContentFrom(addr common.Address) (map[string]map[string]*rpctypes.RPCTransaction, error) + Inspect() (map[string]map[string]map[string]string, error) + Status() (map[string]hexutil.Uint, error) } // TxPoolAPI is the txpool namespace for the Ethereum JSON-RPC APIs. @@ -36,22 +40,18 @@ func NewTxPoolAPI(logger log.Logger, backend *backend.JSONRPCBackend) *TxPoolAPI } } -// TODO: implement txpool_content -//func (api *TxPoolAPI) Content() map[string]map[string]map[string]*rpctypes.RPCTransaction { -// return nil -//} - -// TODO: implement txpool_contentFrom -//func (api *TxPoolAPI) ContentFrom() map[string]map[string]*rpctypes.RPCTransaction { -// return nil -//} - -// TODO: implement txpool_inspect -//func (api *TxPoolAPI) Inspect() map[string]map[string]map[string]string { -// return nil -//} - -// TODO: implement txpool_status -//func (api *TxPoolAPI) Status() map[string]hexutil.Uint { -// return nil -//} +func (api *TxPoolAPI) Content() (map[string]map[string]map[string]*rpctypes.RPCTransaction, error) { + return api.backend.TxPoolContent() +} + +func (api *TxPoolAPI) ContentFrom(addr common.Address) (map[string]map[string]*rpctypes.RPCTransaction, error) { + return api.backend.TxPoolContentFrom(addr) +} + +func (api *TxPoolAPI) Inspect() (map[string]map[string]map[string]string, error) { + return api.backend.TxPoolInspect() +} + +func (api *TxPoolAPI) Status() (map[string]hexutil.Uint, error) { + return api.backend.TxPoolStatus() +} diff --git a/jsonrpc/namespaces/web3/api.go b/jsonrpc/namespaces/web3/api.go new file mode 100644 index 0000000..115e26a --- /dev/null +++ b/jsonrpc/namespaces/web3/api.go @@ -0,0 +1,45 @@ +package web3 + +import ( + "context" + + "cosmossdk.io/log" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/initia-labs/minievm/jsonrpc/backend" + + "github.com/ethereum/go-ethereum/crypto" +) + +// Web3EthereumAPI is the web3 namespace for the Ethereum JSON-RPC APIs. +// Current it is used for tracking what APIs should be implemented for Ethereum compatibility. +// After fully implementing the Ethereum APIs, this interface can be removed. +type Web3EthereumAPI interface { + ClientVersion() string + Sha3(input hexutil.Bytes) hexutil.Bytes +} + +type Web3API struct { + ctx context.Context + logger log.Logger + backend *backend.JSONRPCBackend +} + +// NewWeb3API creates a new net API instance +func NewWeb3API(logger log.Logger, backend *backend.JSONRPCBackend) *Web3API { + return &Web3API{ + ctx: context.TODO(), + logger: logger, + backend: backend, + } +} + +// ClientVersion returns the node name +func (s *Web3API) ClientVersion() (string, error) { + return s.backend.ClientVersion() +} + +// Sha3 applies the ethereum sha3 implementation on the input. +// It assumes the input is hex encoded. +func (s *Web3API) Sha3(input hexutil.Bytes) hexutil.Bytes { + return crypto.Keccak256(input) +}