forked from cerc-io/laconicd-deprecated
rpc, evm: refactor (#588)
* stargate: refactor * remove evm CLI * rpc: refactor * more fixes * fixes fixes fixes * changelog * refactor according to namespaces * fix * lint * remove export logic * fix rpc test * godoc
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,536 @@
|
||||
package filters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
coretypes "github.com/tendermint/tendermint/rpc/core/types"
|
||||
tmtypes "github.com/tendermint/tendermint/types"
|
||||
|
||||
"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"
|
||||
|
||||
clientcontext "github.com/cosmos/cosmos-sdk/client/context"
|
||||
|
||||
rpctypes "github.com/cosmos/ethermint/rpc/types"
|
||||
evmtypes "github.com/cosmos/ethermint/x/evm/types"
|
||||
)
|
||||
|
||||
// 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)
|
||||
|
||||
GetTransactionLogs(txHash common.Hash) ([]*ethtypes.Log, error)
|
||||
BloomStatus() (uint64, uint64)
|
||||
}
|
||||
|
||||
// consider a filter inactive if it has not been polled for within deadline
|
||||
var deadline = 5 * time.Minute
|
||||
|
||||
// filter is a helper struct that holds meta information over the filter type
|
||||
// and associated subscription in the event system.
|
||||
type filter struct {
|
||||
typ filters.Type
|
||||
deadline *time.Timer // filter is inactive when deadline triggers
|
||||
hashes []common.Hash
|
||||
crit filters.FilterCriteria
|
||||
logs []*ethtypes.Log
|
||||
s *Subscription // associated subscription in event system
|
||||
}
|
||||
|
||||
// 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 {
|
||||
clientCtx clientcontext.CLIContext
|
||||
backend Backend
|
||||
events *EventSystem
|
||||
filtersMu sync.Mutex
|
||||
filters map[rpc.ID]*filter
|
||||
}
|
||||
|
||||
// NewAPI returns a new PublicFilterAPI instance.
|
||||
func NewAPI(clientCtx clientcontext.CLIContext, backend Backend) *PublicFilterAPI {
|
||||
// start the client to subscribe to Tendermint events
|
||||
err := clientCtx.Client.Start()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
api := &PublicFilterAPI{
|
||||
clientCtx: clientCtx,
|
||||
backend: backend,
|
||||
filters: make(map[rpc.ID]*filter),
|
||||
events: NewEventSystem(clientCtx.Client),
|
||||
}
|
||||
|
||||
go api.timeoutLoop()
|
||||
|
||||
return api
|
||||
}
|
||||
|
||||
// timeoutLoop runs every 5 minutes and deletes filters that have not been recently used.
|
||||
// Tt is started when the api is created.
|
||||
func (api *PublicFilterAPI) timeoutLoop() {
|
||||
ticker := time.NewTicker(deadline)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
<-ticker.C
|
||||
api.filtersMu.Lock()
|
||||
for id, f := range api.filters {
|
||||
select {
|
||||
case <-f.deadline.C:
|
||||
f.s.Unsubscribe(api.events)
|
||||
delete(api.filters, id)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
api.filtersMu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// NewPendingTransactionFilter creates a filter that fetches pending transaction hashes
|
||||
// as transactions enter the pending state.
|
||||
//
|
||||
// It is part of the filter package because this filter can be used through the
|
||||
// `eth_getFilterChanges` polling method that is also used for log filters.
|
||||
//
|
||||
// https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_newPendingTransactionFilter
|
||||
func (api *PublicFilterAPI) NewPendingTransactionFilter() rpc.ID {
|
||||
pendingTxSub, cancelSubs, err := api.events.SubscribePendingTxs()
|
||||
if err != nil {
|
||||
// wrap error on the ID
|
||||
return rpc.ID(fmt.Sprintf("error creating pending tx filter: %s", err.Error()))
|
||||
}
|
||||
|
||||
api.filtersMu.Lock()
|
||||
api.filters[pendingTxSub.ID()] = &filter{typ: filters.PendingTransactionsSubscription, deadline: time.NewTimer(deadline), hashes: make([]common.Hash, 0), s: pendingTxSub}
|
||||
api.filtersMu.Unlock()
|
||||
|
||||
go func(txsCh <-chan coretypes.ResultEvent, errCh <-chan error) {
|
||||
defer cancelSubs()
|
||||
|
||||
for {
|
||||
select {
|
||||
case ev := <-txsCh:
|
||||
data, _ := ev.Data.(tmtypes.EventDataTx)
|
||||
txHash := common.BytesToHash(data.Tx.Hash())
|
||||
|
||||
api.filtersMu.Lock()
|
||||
if f, found := api.filters[pendingTxSub.ID()]; found {
|
||||
f.hashes = append(f.hashes, txHash)
|
||||
}
|
||||
api.filtersMu.Unlock()
|
||||
case <-errCh:
|
||||
api.filtersMu.Lock()
|
||||
delete(api.filters, pendingTxSub.ID())
|
||||
api.filtersMu.Unlock()
|
||||
}
|
||||
}
|
||||
}(pendingTxSub.eventCh, pendingTxSub.Err())
|
||||
|
||||
return pendingTxSub.ID()
|
||||
}
|
||||
|
||||
// NewPendingTransactions creates a subscription that is triggered each time a transaction
|
||||
// enters the transaction pool and was signed from one of the transactions this nodes manages.
|
||||
func (api *PublicFilterAPI) NewPendingTransactions(ctx context.Context) (*rpc.Subscription, error) {
|
||||
notifier, supported := rpc.NotifierFromContext(ctx)
|
||||
if !supported {
|
||||
return &rpc.Subscription{}, rpc.ErrNotificationsUnsupported
|
||||
}
|
||||
|
||||
rpcSub := notifier.CreateSubscription()
|
||||
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), deadline)
|
||||
defer cancelFn()
|
||||
|
||||
api.events.WithContext(ctx)
|
||||
|
||||
pendingTxSub, cancelSubs, err := api.events.SubscribePendingTxs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go func(txsCh <-chan coretypes.ResultEvent) {
|
||||
defer cancelSubs()
|
||||
|
||||
for {
|
||||
select {
|
||||
case ev := <-txsCh:
|
||||
data, _ := ev.Data.(tmtypes.EventDataTx)
|
||||
txHash := common.BytesToHash(data.Tx.Hash())
|
||||
|
||||
// To keep the original behaviour, send a single tx hash in one notification.
|
||||
// TODO(rjl493456442) Send a batch of tx hashes in one notification
|
||||
err = notifier.Notify(rpcSub.ID, txHash)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
case <-rpcSub.Err():
|
||||
pendingTxSub.Unsubscribe(api.events)
|
||||
return
|
||||
case <-notifier.Closed():
|
||||
pendingTxSub.Unsubscribe(api.events)
|
||||
return
|
||||
}
|
||||
}
|
||||
}(pendingTxSub.eventCh)
|
||||
|
||||
return rpcSub, err
|
||||
}
|
||||
|
||||
// NewBlockFilter creates a filter that fetches blocks that are imported into the chain.
|
||||
// It is part of the filter package since polling goes with eth_getFilterChanges.
|
||||
//
|
||||
// https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_newblockfilter
|
||||
func (api *PublicFilterAPI) NewBlockFilter() rpc.ID {
|
||||
headerSub, cancelSubs, err := api.events.SubscribeNewHeads()
|
||||
if err != nil {
|
||||
// wrap error on the ID
|
||||
return rpc.ID(fmt.Sprintf("error creating block filter: %s", err.Error()))
|
||||
}
|
||||
|
||||
api.filtersMu.Lock()
|
||||
api.filters[headerSub.ID()] = &filter{typ: filters.BlocksSubscription, deadline: time.NewTimer(deadline), hashes: []common.Hash{}, s: headerSub}
|
||||
api.filtersMu.Unlock()
|
||||
|
||||
go func(headersCh <-chan coretypes.ResultEvent, errCh <-chan error) {
|
||||
defer cancelSubs()
|
||||
|
||||
for {
|
||||
select {
|
||||
case ev := <-headersCh:
|
||||
data, _ := ev.Data.(tmtypes.EventDataNewBlockHeader)
|
||||
header := rpctypes.EthHeaderFromTendermint(data.Header)
|
||||
api.filtersMu.Lock()
|
||||
if f, found := api.filters[headerSub.ID()]; found {
|
||||
f.hashes = append(f.hashes, header.Hash())
|
||||
}
|
||||
api.filtersMu.Unlock()
|
||||
case <-errCh:
|
||||
api.filtersMu.Lock()
|
||||
delete(api.filters, headerSub.ID())
|
||||
api.filtersMu.Unlock()
|
||||
return
|
||||
}
|
||||
}
|
||||
}(headerSub.eventCh, headerSub.Err())
|
||||
|
||||
return headerSub.ID()
|
||||
}
|
||||
|
||||
// NewHeads send a notification each time a new (header) block is appended to the chain.
|
||||
func (api *PublicFilterAPI) NewHeads(ctx context.Context) (*rpc.Subscription, error) {
|
||||
notifier, supported := rpc.NotifierFromContext(ctx)
|
||||
if !supported {
|
||||
return &rpc.Subscription{}, rpc.ErrNotificationsUnsupported
|
||||
}
|
||||
|
||||
api.events.WithContext(ctx)
|
||||
rpcSub := notifier.CreateSubscription()
|
||||
|
||||
headersSub, cancelSubs, err := api.events.SubscribeNewHeads()
|
||||
if err != nil {
|
||||
return &rpc.Subscription{}, err
|
||||
}
|
||||
|
||||
go func(headersCh <-chan coretypes.ResultEvent) {
|
||||
defer cancelSubs()
|
||||
|
||||
for {
|
||||
select {
|
||||
case ev := <-headersCh:
|
||||
data, ok := ev.Data.(tmtypes.EventDataNewBlockHeader)
|
||||
if !ok {
|
||||
err = fmt.Errorf("invalid event data %T, expected %s", ev.Data, tmtypes.EventNewBlockHeader)
|
||||
headersSub.err <- err
|
||||
return
|
||||
}
|
||||
|
||||
header := rpctypes.EthHeaderFromTendermint(data.Header)
|
||||
err = notifier.Notify(rpcSub.ID, header)
|
||||
if err != nil {
|
||||
headersSub.err <- err
|
||||
return
|
||||
}
|
||||
case <-rpcSub.Err():
|
||||
headersSub.Unsubscribe(api.events)
|
||||
return
|
||||
case <-notifier.Closed():
|
||||
headersSub.Unsubscribe(api.events)
|
||||
return
|
||||
}
|
||||
}
|
||||
}(headersSub.eventCh)
|
||||
|
||||
return rpcSub, err
|
||||
}
|
||||
|
||||
// Logs creates a subscription that fires for all new log that match the given filter criteria.
|
||||
func (api *PublicFilterAPI) Logs(ctx context.Context, crit filters.FilterCriteria) (*rpc.Subscription, error) {
|
||||
notifier, supported := rpc.NotifierFromContext(ctx)
|
||||
if !supported {
|
||||
return &rpc.Subscription{}, rpc.ErrNotificationsUnsupported
|
||||
}
|
||||
|
||||
api.events.WithContext(ctx)
|
||||
rpcSub := notifier.CreateSubscription()
|
||||
|
||||
logsSub, cancelSubs, err := api.events.SubscribeLogs(crit)
|
||||
if err != nil {
|
||||
return &rpc.Subscription{}, err
|
||||
}
|
||||
|
||||
go func(logsCh <-chan coretypes.ResultEvent) {
|
||||
defer cancelSubs()
|
||||
|
||||
for {
|
||||
select {
|
||||
case event := <-logsCh:
|
||||
// filter only events from EVM module txs
|
||||
_, isMsgEthermint := event.Events[evmtypes.TypeMsgEthermint]
|
||||
_, isMsgEthereumTx := event.Events[evmtypes.TypeMsgEthereumTx]
|
||||
|
||||
if !(isMsgEthermint || isMsgEthereumTx) {
|
||||
// ignore transaction as it's not from the evm module
|
||||
return
|
||||
}
|
||||
|
||||
// get transaction result data
|
||||
dataTx, ok := event.Data.(tmtypes.EventDataTx)
|
||||
if !ok {
|
||||
err = fmt.Errorf("invalid event data %T, expected %s", event.Data, tmtypes.EventTx)
|
||||
logsSub.err <- err
|
||||
return
|
||||
}
|
||||
|
||||
resultData, err := evmtypes.DecodeResultData(dataTx.TxResult.Result.Data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
logs := FilterLogs(resultData.Logs, crit.FromBlock, crit.ToBlock, crit.Addresses, crit.Topics)
|
||||
|
||||
for _, log := range logs {
|
||||
err = notifier.Notify(rpcSub.ID, log)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
case <-rpcSub.Err(): // client send an unsubscribe request
|
||||
logsSub.Unsubscribe(api.events)
|
||||
return
|
||||
case <-notifier.Closed(): // connection dropped
|
||||
logsSub.Unsubscribe(api.events)
|
||||
return
|
||||
}
|
||||
}
|
||||
}(logsSub.eventCh)
|
||||
|
||||
return rpcSub, err
|
||||
}
|
||||
|
||||
// NewFilter creates a new filter and returns the filter id. It can be
|
||||
// used to retrieve logs when the state changes. This method cannot be
|
||||
// used to fetch logs that are already stored in the state.
|
||||
//
|
||||
// Default criteria for the from and to block are "latest".
|
||||
// Using "latest" as block number will return logs for mined blocks.
|
||||
// Using "pending" as block number returns logs for not yet mined (pending) blocks.
|
||||
// In case logs are removed (chain reorg) previously returned logs are returned
|
||||
// again but with the removed property set to true.
|
||||
//
|
||||
// In case "fromBlock" > "toBlock" an error is returned.
|
||||
//
|
||||
// https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_newfilter
|
||||
func (api *PublicFilterAPI) NewFilter(criteria filters.FilterCriteria) (rpc.ID, error) {
|
||||
var (
|
||||
filterID = rpc.ID("")
|
||||
err error
|
||||
)
|
||||
|
||||
logsSub, cancelSubs, err := api.events.SubscribeLogs(criteria)
|
||||
if err != nil {
|
||||
return rpc.ID(""), err
|
||||
}
|
||||
|
||||
filterID = logsSub.ID()
|
||||
|
||||
api.filtersMu.Lock()
|
||||
api.filters[filterID] = &filter{typ: filters.LogsSubscription, deadline: time.NewTimer(deadline), hashes: []common.Hash{}, s: logsSub}
|
||||
api.filtersMu.Unlock()
|
||||
|
||||
go func(eventCh <-chan coretypes.ResultEvent) {
|
||||
defer cancelSubs()
|
||||
|
||||
for {
|
||||
select {
|
||||
case event := <-eventCh:
|
||||
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, criteria.FromBlock, criteria.ToBlock, criteria.Addresses, criteria.Topics)
|
||||
|
||||
api.filtersMu.Lock()
|
||||
if f, found := api.filters[filterID]; found {
|
||||
f.logs = append(f.logs, logs...)
|
||||
}
|
||||
api.filtersMu.Unlock()
|
||||
case <-logsSub.Err():
|
||||
api.filtersMu.Lock()
|
||||
delete(api.filters, filterID)
|
||||
api.filtersMu.Unlock()
|
||||
return
|
||||
}
|
||||
}
|
||||
}(logsSub.eventCh)
|
||||
|
||||
return filterID, err
|
||||
}
|
||||
|
||||
// GetLogs returns logs matching the given argument that are stored within the state.
|
||||
//
|
||||
// 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 {
|
||||
// Block filter requested, construct a single-shot filter
|
||||
filter = NewBlockFilter(api.backend, crit)
|
||||
} else {
|
||||
// Convert the RPC block numbers into internal representations
|
||||
begin := rpc.LatestBlockNumber.Int64()
|
||||
if crit.FromBlock != nil {
|
||||
begin = crit.FromBlock.Int64()
|
||||
}
|
||||
end := rpc.LatestBlockNumber.Int64()
|
||||
if crit.ToBlock != nil {
|
||||
end = crit.ToBlock.Int64()
|
||||
}
|
||||
// Construct the range filter
|
||||
filter = NewRangeFilter(api.backend, begin, end, crit.Addresses, crit.Topics)
|
||||
}
|
||||
|
||||
// Run the filter and return all the logs
|
||||
logs, err := filter.Logs(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return returnLogs(logs), nil
|
||||
}
|
||||
|
||||
// UninstallFilter removes the filter with the given filter id.
|
||||
//
|
||||
// https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_uninstallfilter
|
||||
func (api *PublicFilterAPI) UninstallFilter(id rpc.ID) bool {
|
||||
api.filtersMu.Lock()
|
||||
f, found := api.filters[id]
|
||||
if found {
|
||||
delete(api.filters, id)
|
||||
}
|
||||
api.filtersMu.Unlock()
|
||||
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
f.s.Unsubscribe(api.events)
|
||||
return true
|
||||
}
|
||||
|
||||
// GetFilterLogs returns the logs for the filter with the given id.
|
||||
// If the filter could not be found an empty array of logs is returned.
|
||||
//
|
||||
// https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getfilterlogs
|
||||
func (api *PublicFilterAPI) GetFilterLogs(ctx context.Context, id rpc.ID) ([]*ethtypes.Log, error) {
|
||||
api.filtersMu.Lock()
|
||||
f, found := api.filters[id]
|
||||
api.filtersMu.Unlock()
|
||||
|
||||
if !found {
|
||||
return returnLogs(nil), fmt.Errorf("filter %s not found", id)
|
||||
}
|
||||
|
||||
if f.typ != filters.LogsSubscription {
|
||||
return returnLogs(nil), fmt.Errorf("filter %s doesn't have a LogsSubscription type: got %d", id, f.typ)
|
||||
}
|
||||
|
||||
var filter *Filter
|
||||
if f.crit.BlockHash != nil {
|
||||
// Block filter requested, construct a single-shot filter
|
||||
filter = NewBlockFilter(api.backend, f.crit)
|
||||
} else {
|
||||
// Convert the RPC block numbers into internal representations
|
||||
begin := rpc.LatestBlockNumber.Int64()
|
||||
if f.crit.FromBlock != nil {
|
||||
begin = f.crit.FromBlock.Int64()
|
||||
}
|
||||
end := rpc.LatestBlockNumber.Int64()
|
||||
if f.crit.ToBlock != nil {
|
||||
end = f.crit.ToBlock.Int64()
|
||||
}
|
||||
// Construct the range filter
|
||||
filter = NewRangeFilter(api.backend, begin, end, f.crit.Addresses, f.crit.Topics)
|
||||
}
|
||||
// Run the filter and return all the logs
|
||||
logs, err := filter.Logs(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return returnLogs(logs), nil
|
||||
}
|
||||
|
||||
// GetFilterChanges returns the logs for the filter with the given id since
|
||||
// last time it was called. This can be used for polling.
|
||||
//
|
||||
// For pending transaction and block filters the result is []common.Hash.
|
||||
// (pending)Log filters return []Log.
|
||||
//
|
||||
// https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getfilterchanges
|
||||
func (api *PublicFilterAPI) GetFilterChanges(id rpc.ID) (interface{}, error) {
|
||||
api.filtersMu.Lock()
|
||||
defer api.filtersMu.Unlock()
|
||||
|
||||
f, found := api.filters[id]
|
||||
if !found {
|
||||
return nil, fmt.Errorf("filter %s not found", id)
|
||||
}
|
||||
|
||||
if !f.deadline.Stop() {
|
||||
// timer expired but filter is not yet removed in timeout loop
|
||||
// receive timer value and reset timer
|
||||
<-f.deadline.C
|
||||
}
|
||||
f.deadline.Reset(deadline)
|
||||
|
||||
switch f.typ {
|
||||
case filters.PendingTransactionsSubscription, filters.BlocksSubscription:
|
||||
hashes := f.hashes
|
||||
f.hashes = nil
|
||||
return returnHashes(hashes), nil
|
||||
case filters.LogsSubscription, filters.MinedAndPendingLogsSubscription:
|
||||
logs := make([]*ethtypes.Log, len(f.logs))
|
||||
copy(logs, f.logs)
|
||||
f.logs = []*ethtypes.Log{}
|
||||
return returnLogs(logs), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid filter %s type %d", id, f.typ)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
package filters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
tmquery "github.com/tendermint/tendermint/libs/pubsub/query"
|
||||
rpcclient "github.com/tendermint/tendermint/rpc/client"
|
||||
coretypes "github.com/tendermint/tendermint/rpc/core/types"
|
||||
tmtypes "github.com/tendermint/tendermint/types"
|
||||
|
||||
"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"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
|
||||
rpctypes "github.com/cosmos/ethermint/rpc/types"
|
||||
evmtypes "github.com/cosmos/ethermint/x/evm/types"
|
||||
)
|
||||
|
||||
var (
|
||||
txEvents = tmtypes.QueryForEvent(tmtypes.EventTx).String()
|
||||
evmEvents = tmquery.MustParse(fmt.Sprintf("%s='%s' AND %s.%s='%s'", tmtypes.EventTypeKey, tmtypes.EventTx, sdk.EventTypeMessage, sdk.AttributeKeyModule, evmtypes.ModuleName)).String()
|
||||
headerEvents = tmtypes.QueryForEvent(tmtypes.EventNewBlockHeader).String()
|
||||
)
|
||||
|
||||
// EventSystem creates subscriptions, processes events and broadcasts them to the
|
||||
// subscription which match the subscription criteria using the Tendermint's RPC client.
|
||||
type EventSystem struct {
|
||||
ctx context.Context
|
||||
client rpcclient.Client
|
||||
|
||||
// light client mode
|
||||
lightMode bool
|
||||
|
||||
index filterIndex
|
||||
|
||||
// Subscriptions
|
||||
txsSub *Subscription // Subscription for new transaction event
|
||||
logsSub *Subscription // Subscription for new log event
|
||||
// rmLogsSub *Subscription // Subscription for removed log event
|
||||
|
||||
pendingLogsSub *Subscription // Subscription for pending log event
|
||||
chainSub *Subscription // Subscription for new chain event
|
||||
|
||||
// Channels
|
||||
install chan *Subscription // install filter for event notification
|
||||
uninstall chan *Subscription // remove filter for event notification
|
||||
|
||||
// Unidirectional channels to receive Tendermint ResultEvents
|
||||
txsCh <-chan coretypes.ResultEvent // Channel to receive new pending transactions event
|
||||
logsCh <-chan coretypes.ResultEvent // Channel to receive new log event
|
||||
pendingLogsCh <-chan coretypes.ResultEvent // Channel to receive new pending log event
|
||||
// rmLogsCh <-chan coretypes.ResultEvent // Channel to receive removed log event
|
||||
|
||||
chainCh <-chan coretypes.ResultEvent // Channel to receive new chain event
|
||||
}
|
||||
|
||||
// NewEventSystem creates a new manager that listens for event on the given mux,
|
||||
// parses and filters them. It uses the all map to retrieve filter changes. The
|
||||
// work loop holds its own index that is used to forward events to filters.
|
||||
//
|
||||
// The returned manager has a loop that needs to be stopped with the Stop function
|
||||
// or by stopping the given mux.
|
||||
func NewEventSystem(client rpcclient.Client) *EventSystem {
|
||||
index := make(filterIndex)
|
||||
for i := filters.UnknownSubscription; i < filters.LastIndexSubscription; i++ {
|
||||
index[i] = make(map[rpc.ID]*Subscription)
|
||||
}
|
||||
|
||||
es := &EventSystem{
|
||||
ctx: context.Background(),
|
||||
client: client,
|
||||
lightMode: false,
|
||||
index: index,
|
||||
install: make(chan *Subscription),
|
||||
uninstall: make(chan *Subscription),
|
||||
txsCh: make(<-chan coretypes.ResultEvent),
|
||||
logsCh: make(<-chan coretypes.ResultEvent),
|
||||
pendingLogsCh: make(<-chan coretypes.ResultEvent),
|
||||
// rmLogsCh: make(<-chan coretypes.ResultEvent),
|
||||
chainCh: make(<-chan coretypes.ResultEvent),
|
||||
}
|
||||
|
||||
go es.eventLoop()
|
||||
return es
|
||||
}
|
||||
|
||||
// WithContext sets a new context to the EventSystem. This is required to set a timeout context when
|
||||
// a new filter is intantiated.
|
||||
func (es *EventSystem) WithContext(ctx context.Context) {
|
||||
es.ctx = ctx
|
||||
}
|
||||
|
||||
// subscribe performs a new event subscription to a given Tendermint event.
|
||||
// The subscription creates a unidirectional receive event channel to receive the ResultEvent. By
|
||||
// default, the subscription timeouts (i.e is canceled) after 5 minutes. This function returns an
|
||||
// error if the subscription fails (eg: if the identifier is already subscribed) or if the filter
|
||||
// type is invalid.
|
||||
func (es *EventSystem) subscribe(sub *Subscription) (*Subscription, context.CancelFunc, error) {
|
||||
var (
|
||||
err error
|
||||
cancelFn context.CancelFunc
|
||||
eventCh <-chan coretypes.ResultEvent
|
||||
)
|
||||
|
||||
es.ctx, cancelFn = context.WithTimeout(context.Background(), deadline)
|
||||
|
||||
switch sub.typ {
|
||||
case filters.PendingTransactionsSubscription:
|
||||
eventCh, err = es.client.Subscribe(es.ctx, string(sub.id), sub.event)
|
||||
case filters.PendingLogsSubscription, filters.MinedAndPendingLogsSubscription:
|
||||
eventCh, err = es.client.Subscribe(es.ctx, string(sub.id), sub.event)
|
||||
case filters.LogsSubscription:
|
||||
eventCh, err = es.client.Subscribe(es.ctx, string(sub.id), sub.event)
|
||||
case filters.BlocksSubscription:
|
||||
eventCh, err = es.client.Subscribe(es.ctx, string(sub.id), sub.event)
|
||||
default:
|
||||
err = fmt.Errorf("invalid filter subscription type %d", sub.typ)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
sub.err <- err
|
||||
return nil, cancelFn, err
|
||||
}
|
||||
|
||||
// wrap events in a go routine to prevent blocking
|
||||
go func() {
|
||||
es.install <- sub
|
||||
<-sub.installed
|
||||
}()
|
||||
|
||||
sub.eventCh = eventCh
|
||||
return sub, cancelFn, nil
|
||||
}
|
||||
|
||||
// SubscribeLogs creates a subscription that will write all logs matching the
|
||||
// given criteria to the given logs channel. Default value for the from and to
|
||||
// block is "latest". If the fromBlock > toBlock an error is returned.
|
||||
func (es *EventSystem) SubscribeLogs(crit filters.FilterCriteria) (*Subscription, context.CancelFunc, error) {
|
||||
var from, to rpc.BlockNumber
|
||||
if crit.FromBlock == nil {
|
||||
from = rpc.LatestBlockNumber
|
||||
} else {
|
||||
from = rpc.BlockNumber(crit.FromBlock.Int64())
|
||||
}
|
||||
if crit.ToBlock == nil {
|
||||
to = rpc.LatestBlockNumber
|
||||
} else {
|
||||
to = rpc.BlockNumber(crit.ToBlock.Int64())
|
||||
}
|
||||
|
||||
switch {
|
||||
// only interested in pending logs
|
||||
case from == rpc.PendingBlockNumber && to == rpc.PendingBlockNumber:
|
||||
return es.subscribePendingLogs(crit)
|
||||
|
||||
// only interested in new mined logs, mined logs within a specific block range, or
|
||||
// logs from a specific block number to new mined blocks
|
||||
case (from == rpc.LatestBlockNumber && to == rpc.LatestBlockNumber),
|
||||
(from >= 0 && to >= 0 && to >= from):
|
||||
return es.subscribeLogs(crit)
|
||||
|
||||
// interested in mined logs from a specific block number, new logs and pending logs
|
||||
case from >= rpc.LatestBlockNumber && (to == rpc.PendingBlockNumber || to == rpc.LatestBlockNumber):
|
||||
return es.subscribeMinedPendingLogs(crit)
|
||||
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("invalid from and to block combination: from > to (%d > %d)", from, to)
|
||||
}
|
||||
}
|
||||
|
||||
// subscribeMinedPendingLogs creates a subscription that returned mined and
|
||||
// pending logs that match the given criteria.
|
||||
func (es *EventSystem) subscribeMinedPendingLogs(crit filters.FilterCriteria) (*Subscription, context.CancelFunc, error) {
|
||||
sub := &Subscription{
|
||||
id: rpc.NewID(),
|
||||
typ: filters.MinedAndPendingLogsSubscription,
|
||||
event: evmEvents,
|
||||
logsCrit: crit,
|
||||
created: time.Now().UTC(),
|
||||
logs: make(chan []*ethtypes.Log),
|
||||
installed: make(chan struct{}, 1),
|
||||
err: make(chan error, 1),
|
||||
}
|
||||
return es.subscribe(sub)
|
||||
}
|
||||
|
||||
// subscribeLogs creates a subscription that will write all logs matching the
|
||||
// given criteria to the given logs channel.
|
||||
func (es *EventSystem) subscribeLogs(crit filters.FilterCriteria) (*Subscription, context.CancelFunc, error) {
|
||||
sub := &Subscription{
|
||||
id: rpc.NewID(),
|
||||
typ: filters.LogsSubscription,
|
||||
event: evmEvents,
|
||||
logsCrit: crit,
|
||||
created: time.Now().UTC(),
|
||||
logs: make(chan []*ethtypes.Log),
|
||||
installed: make(chan struct{}, 1),
|
||||
err: make(chan error, 1),
|
||||
}
|
||||
return es.subscribe(sub)
|
||||
}
|
||||
|
||||
// subscribePendingLogs creates a subscription that writes transaction hashes for
|
||||
// transactions that enter the transaction pool.
|
||||
func (es *EventSystem) subscribePendingLogs(crit filters.FilterCriteria) (*Subscription, context.CancelFunc, error) {
|
||||
sub := &Subscription{
|
||||
id: rpc.NewID(),
|
||||
typ: filters.PendingLogsSubscription,
|
||||
event: evmEvents,
|
||||
logsCrit: crit,
|
||||
created: time.Now().UTC(),
|
||||
logs: make(chan []*ethtypes.Log),
|
||||
installed: make(chan struct{}, 1),
|
||||
err: make(chan error, 1),
|
||||
}
|
||||
return es.subscribe(sub)
|
||||
}
|
||||
|
||||
// SubscribeNewHeads subscribes to new block headers events.
|
||||
func (es EventSystem) SubscribeNewHeads() (*Subscription, context.CancelFunc, error) {
|
||||
sub := &Subscription{
|
||||
id: rpc.NewID(),
|
||||
typ: filters.BlocksSubscription,
|
||||
event: headerEvents,
|
||||
created: time.Now().UTC(),
|
||||
headers: make(chan *ethtypes.Header),
|
||||
installed: make(chan struct{}, 1),
|
||||
err: make(chan error, 1),
|
||||
}
|
||||
return es.subscribe(sub)
|
||||
}
|
||||
|
||||
// SubscribePendingTxs subscribes to new pending transactions events from the mempool.
|
||||
func (es EventSystem) SubscribePendingTxs() (*Subscription, context.CancelFunc, error) {
|
||||
sub := &Subscription{
|
||||
id: rpc.NewID(),
|
||||
typ: filters.PendingTransactionsSubscription,
|
||||
event: txEvents,
|
||||
created: time.Now().UTC(),
|
||||
hashes: make(chan []common.Hash),
|
||||
installed: make(chan struct{}, 1),
|
||||
err: make(chan error, 1),
|
||||
}
|
||||
return es.subscribe(sub)
|
||||
}
|
||||
|
||||
type filterIndex map[filters.Type]map[rpc.ID]*Subscription
|
||||
|
||||
func (es *EventSystem) handleLogs(ev coretypes.ResultEvent) {
|
||||
data, _ := ev.Data.(tmtypes.EventDataTx)
|
||||
resultData, err := evmtypes.DecodeResultData(data.TxResult.Result.Data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(resultData.Logs) == 0 {
|
||||
return
|
||||
}
|
||||
for _, f := range es.index[filters.LogsSubscription] {
|
||||
matchedLogs := FilterLogs(resultData.Logs, f.logsCrit.FromBlock, f.logsCrit.ToBlock, f.logsCrit.Addresses, f.logsCrit.Topics)
|
||||
if len(matchedLogs) > 0 {
|
||||
f.logs <- matchedLogs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (es *EventSystem) handleTxsEvent(ev coretypes.ResultEvent) {
|
||||
data, _ := ev.Data.(tmtypes.EventDataTx)
|
||||
for _, f := range es.index[filters.PendingTransactionsSubscription] {
|
||||
f.hashes <- []common.Hash{common.BytesToHash(data.Tx.Hash())}
|
||||
}
|
||||
}
|
||||
|
||||
func (es *EventSystem) handleChainEvent(ev coretypes.ResultEvent) {
|
||||
data, _ := ev.Data.(tmtypes.EventDataNewBlockHeader)
|
||||
for _, f := range es.index[filters.BlocksSubscription] {
|
||||
f.headers <- rpctypes.EthHeaderFromTendermint(data.Header)
|
||||
}
|
||||
// TODO: light client
|
||||
}
|
||||
|
||||
// eventLoop (un)installs filters and processes mux events.
|
||||
func (es *EventSystem) eventLoop() {
|
||||
var (
|
||||
err error
|
||||
cancelPendingTxsSubs, cancelLogsSubs, cancelPendingLogsSubs, cancelHeaderSubs context.CancelFunc
|
||||
)
|
||||
|
||||
// Subscribe events
|
||||
es.txsSub, cancelPendingTxsSubs, err = es.SubscribePendingTxs()
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to subscribe pending txs: %w", err))
|
||||
}
|
||||
|
||||
defer cancelPendingTxsSubs()
|
||||
|
||||
es.logsSub, cancelLogsSubs, err = es.SubscribeLogs(filters.FilterCriteria{})
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to subscribe logs: %w", err))
|
||||
}
|
||||
|
||||
defer cancelLogsSubs()
|
||||
|
||||
es.pendingLogsSub, cancelPendingLogsSubs, err = es.subscribePendingLogs(filters.FilterCriteria{})
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to subscribe pending logs: %w", err))
|
||||
}
|
||||
|
||||
defer cancelPendingLogsSubs()
|
||||
|
||||
es.chainSub, cancelHeaderSubs, err = es.SubscribeNewHeads()
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to subscribe headers: %w", err))
|
||||
}
|
||||
|
||||
defer cancelHeaderSubs()
|
||||
|
||||
// Ensure all subscriptions get cleaned up
|
||||
defer func() {
|
||||
es.txsSub.Unsubscribe(es)
|
||||
es.logsSub.Unsubscribe(es)
|
||||
// es.rmLogsSub.Unsubscribe(es)
|
||||
es.pendingLogsSub.Unsubscribe(es)
|
||||
es.chainSub.Unsubscribe(es)
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case txEvent := <-es.txsSub.eventCh:
|
||||
es.handleTxsEvent(txEvent)
|
||||
case headerEv := <-es.chainSub.eventCh:
|
||||
es.handleChainEvent(headerEv)
|
||||
case logsEv := <-es.logsSub.eventCh:
|
||||
es.handleLogs(logsEv)
|
||||
// TODO: figure out how to handle removed logs
|
||||
// case logsEv := <-es.rmLogsSub.eventCh:
|
||||
// es.handleLogs(logsEv)
|
||||
case logsEv := <-es.pendingLogsSub.eventCh:
|
||||
es.handleLogs(logsEv)
|
||||
|
||||
case f := <-es.install:
|
||||
if f.typ == filters.MinedAndPendingLogsSubscription {
|
||||
// the type are logs and pending logs subscriptions
|
||||
es.index[filters.LogsSubscription][f.id] = f
|
||||
es.index[filters.PendingLogsSubscription][f.id] = f
|
||||
} else {
|
||||
es.index[f.typ][f.id] = f
|
||||
}
|
||||
close(f.installed)
|
||||
|
||||
case f := <-es.uninstall:
|
||||
if f.typ == filters.MinedAndPendingLogsSubscription {
|
||||
// the type are logs and pending logs subscriptions
|
||||
delete(es.index[filters.LogsSubscription], f.id)
|
||||
delete(es.index[filters.PendingLogsSubscription], f.id)
|
||||
} else {
|
||||
delete(es.index[f.typ], f.id)
|
||||
}
|
||||
close(f.err)
|
||||
// System stopped
|
||||
case <-es.txsSub.Err():
|
||||
return
|
||||
case <-es.logsSub.Err():
|
||||
return
|
||||
// case <-es.rmLogsSub.Err():
|
||||
// return
|
||||
case <-es.pendingLogsSub.Err():
|
||||
return
|
||||
case <-es.chainSub.Err():
|
||||
return
|
||||
}
|
||||
}
|
||||
// }()
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package filters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"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 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 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 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.
|
||||
var filtersBz [][][]byte // nolint: prealloc
|
||||
if len(addresses) > 0 {
|
||||
filter := make([][]byte, len(addresses))
|
||||
for i, address := range addresses {
|
||||
filter[i] = address.Bytes()
|
||||
}
|
||||
filtersBz = append(filtersBz, filter)
|
||||
}
|
||||
|
||||
for _, topicList := range topics {
|
||||
filter := make([][]byte, len(topicList))
|
||||
for i, topic := range topicList {
|
||||
filter[i] = topic.Bytes()
|
||||
}
|
||||
filtersBz = append(filtersBz, filter)
|
||||
}
|
||||
|
||||
size, _ := backend.BloomStatus()
|
||||
|
||||
// Create a generic filter and convert it into a range filter
|
||||
criteria := filters.FilterCriteria{
|
||||
FromBlock: big.NewInt(begin),
|
||||
ToBlock: big.NewInt(end),
|
||||
Addresses: addresses,
|
||||
Topics: topics,
|
||||
}
|
||||
|
||||
return newFilter(backend, criteria, bloombits.NewMatcher(size, filtersBz))
|
||||
}
|
||||
|
||||
// newFilter returns a new Filter
|
||||
func newFilter(backend Backend, criteria filters.FilterCriteria, matcher *bloombits.Matcher) *Filter {
|
||||
return &Filter{
|
||||
backend: backend,
|
||||
criteria: criteria,
|
||||
matcher: matcher,
|
||||
}
|
||||
}
|
||||
|
||||
// Logs searches the blockchain for matching log entries, returning all from the
|
||||
// first block that contains matches, updating the start of the filter accordingly.
|
||||
func (f *Filter) Logs(_ context.Context) ([]*ethtypes.Log, error) {
|
||||
logs := []*ethtypes.Log{}
|
||||
var err error
|
||||
|
||||
// If we're doing singleton block filtering, execute and return
|
||||
if f.criteria.BlockHash != nil && f.criteria.BlockHash != (&common.Hash{}) {
|
||||
header, err := f.backend.HeaderByHash(*f.criteria.BlockHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if header == nil {
|
||||
return nil, fmt.Errorf("unknown block header %s", f.criteria.BlockHash.String())
|
||||
}
|
||||
return f.blockLogs(header)
|
||||
}
|
||||
|
||||
// Figure out the limits of the filter range
|
||||
header, err := f.backend.HeaderByNumber(rpctypes.LatestBlockNumber)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if header == nil || header.Number == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
head := header.Number.Int64()
|
||||
if f.criteria.FromBlock.Int64() == -1 {
|
||||
f.criteria.FromBlock = big.NewInt(head)
|
||||
}
|
||||
if f.criteria.ToBlock.Int64() == -1 {
|
||||
f.criteria.ToBlock = big.NewInt(head)
|
||||
}
|
||||
|
||||
for i := f.criteria.FromBlock.Int64(); i <= f.criteria.ToBlock.Int64(); i++ {
|
||||
block, err := f.backend.GetBlockByNumber(rpctypes.BlockNumber(i), true)
|
||||
if err != nil {
|
||||
return logs, err
|
||||
}
|
||||
|
||||
txs, ok := block["transactions"].([]common.Hash)
|
||||
if !ok || len(txs) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
logsMatched := f.checkMatches(txs)
|
||||
logs = append(logs, logsMatched...)
|
||||
}
|
||||
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
// blockLogs returns the logs matching the filter criteria within a single block.
|
||||
func (f *Filter) blockLogs(header *ethtypes.Header) ([]*ethtypes.Log, error) {
|
||||
if !bloomFilter(header.Bloom, f.criteria.Addresses, f.criteria.Topics) {
|
||||
return []*ethtypes.Log{}, nil
|
||||
}
|
||||
|
||||
logsList, err := f.backend.GetLogs(header.Hash())
|
||||
if err != nil {
|
||||
return []*ethtypes.Log{}, err
|
||||
}
|
||||
|
||||
var unfiltered []*ethtypes.Log // nolint: prealloc
|
||||
for _, logs := range logsList {
|
||||
unfiltered = append(unfiltered, logs...)
|
||||
}
|
||||
logs := FilterLogs(unfiltered, nil, nil, f.criteria.Addresses, f.criteria.Topics)
|
||||
if len(logs) == 0 {
|
||||
return []*ethtypes.Log{}, nil
|
||||
}
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
// checkMatches checks if the logs from the a list of transactions transaction
|
||||
// contain any log events that match the filter criteria. This function is
|
||||
// called when the bloom filter signals a potential match.
|
||||
func (f *Filter) checkMatches(transactions []common.Hash) []*ethtypes.Log {
|
||||
unfiltered := []*ethtypes.Log{}
|
||||
for _, tx := range transactions {
|
||||
logs, err := f.backend.GetTransactionLogs(tx)
|
||||
if err != nil {
|
||||
// ignore error if transaction didn't set any logs (eg: when tx type is not
|
||||
// MsgEthereumTx or MsgEthermint)
|
||||
continue
|
||||
}
|
||||
|
||||
unfiltered = append(unfiltered, logs...)
|
||||
}
|
||||
|
||||
return FilterLogs(unfiltered, f.criteria.FromBlock, f.criteria.ToBlock, f.criteria.Addresses, f.criteria.Topics)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package net
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client/context"
|
||||
ethermint "github.com/cosmos/ethermint/types"
|
||||
)
|
||||
|
||||
// PublicNetAPI is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec.
|
||||
type PublicNetAPI struct {
|
||||
networkVersion uint64
|
||||
}
|
||||
|
||||
// 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(clientCtx.ChainID)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &PublicNetAPI{
|
||||
networkVersion: chainIDEpoch.Uint64(),
|
||||
}
|
||||
}
|
||||
|
||||
// Version returns the current ethereum protocol version.
|
||||
func (api *PublicNetAPI) Version() string {
|
||||
return fmt.Sprintf("%d", api.networkVersion)
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
package personal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/crypto/keys"
|
||||
"github.com/cosmos/cosmos-sdk/crypto/keys/mintkey"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
|
||||
"github.com/cosmos/ethermint/crypto/ethsecp256k1"
|
||||
"github.com/cosmos/ethermint/crypto/hd"
|
||||
"github.com/cosmos/ethermint/rpc/namespaces/eth"
|
||||
rpctypes "github.com/cosmos/ethermint/rpc/types"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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"),
|
||||
}
|
||||
|
||||
err := api.ethAPI.GetKeyringInfo()
|
||||
if err != nil {
|
||||
return api
|
||||
}
|
||||
|
||||
api.keyInfos, err = api.ethAPI.ClientCtx().Keybase.List()
|
||||
if err != nil {
|
||||
return api
|
||||
}
|
||||
|
||||
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_<length-keys>", where <length-keys> is the total number of
|
||||
// keys stored on the keyring.
|
||||
// NOTE: The key will be both armored and encrypted using the same passphrase.
|
||||
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
|
||||
}
|
||||
|
||||
privKey := ethsecp256k1.PrivKey(crypto.FromECDSA(priv))
|
||||
|
||||
armor := mintkey.EncryptArmorPrivKey(privKey, password, ethsecp256k1.KeyType)
|
||||
|
||||
// ignore error as we only care about the length of the list
|
||||
list, _ := api.ethAPI.ClientCtx().Keybase.List()
|
||||
privKeyName := fmt.Sprintf("personal_%d", len(list))
|
||||
|
||||
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 := 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
|
||||
//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 (api *PrivateAccountAPI) ListAccounts() ([]common.Address, error) {
|
||||
api.logger.Debug("personal_listAccounts")
|
||||
addrs := []common.Address{}
|
||||
for _, info := range api.keyInfos {
|
||||
addressBytes := info.GetPubKey().Address().Bytes()
|
||||
addrs = append(addrs, common.BytesToAddress(addressBytes))
|
||||
}
|
||||
|
||||
return addrs, nil
|
||||
}
|
||||
|
||||
// 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 (api *PrivateAccountAPI) LockAccount(address common.Address) bool {
|
||||
api.logger.Debug("personal_lockAccount", "address", address.String())
|
||||
|
||||
keys := api.ethAPI.GetKeys()
|
||||
for i, key := range keys {
|
||||
if !bytes.Equal(key.PubKey().Address().Bytes(), address.Bytes()) {
|
||||
continue
|
||||
}
|
||||
|
||||
tmp := make([]ethsecp256k1.PrivKey, len(keys)-1)
|
||||
copy(tmp[:i], keys[:i])
|
||||
copy(tmp[i:], keys[i+1:])
|
||||
api.ethAPI.SetKeys(tmp)
|
||||
|
||||
api.logger.Debug("account unlocked", "address", address.String())
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// NewAccount will create a new account and returns the address for the new account.
|
||||
func (api *PrivateAccountAPI) NewAccount(password string) (common.Address, error) {
|
||||
api.logger.Debug("personal_newAccount")
|
||||
|
||||
name := "key_" + time.Now().UTC().Format(time.RFC3339)
|
||||
info, _, err := api.ethAPI.ClientCtx().Keybase.CreateMnemonic(name, keys.English, password, hd.EthSecp256k1)
|
||||
if err != nil {
|
||||
return common.Address{}, err
|
||||
}
|
||||
|
||||
api.keyInfos = append(api.keyInfos, info)
|
||||
|
||||
addr := common.BytesToAddress(info.GetPubKey().Address().Bytes())
|
||||
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
|
||||
}
|
||||
|
||||
// UnlockAccount will unlock the account associated with the given address with
|
||||
// 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 (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 api.keyInfos {
|
||||
addressBytes := info.GetPubKey().Address().Bytes()
|
||||
if bytes.Equal(addressBytes, addr[:]) {
|
||||
keyInfo = info
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if keyInfo == nil {
|
||||
return false, fmt.Errorf("cannot find key with given address %s", addr.String())
|
||||
}
|
||||
|
||||
privKey, err := api.ethAPI.ClientCtx().Keybase.ExportPrivateKeyObject(keyInfo.GetName(), password)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
ethermintPrivKey, ok := privKey.(ethsecp256k1.PrivKey)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("invalid private key type %T, expected %T", privKey, ðsecp256k1.PrivKey{})
|
||||
}
|
||||
|
||||
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 (api *PrivateAccountAPI) SendTransaction(_ context.Context, args rpctypes.SendTxArgs, _ string) (common.Hash, error) {
|
||||
return api.ethAPI.SendTransaction(args)
|
||||
}
|
||||
|
||||
// Sign calculates an Ethereum ECDSA signature for:
|
||||
// keccak256("\x19Ethereum Signed Message:\n" + len(message) + message))
|
||||
//
|
||||
// Note, the produced signature conforms to the secp256k1 curve R, S and V values,
|
||||
// where the V value will be 27 or 28 for legacy reasons.
|
||||
//
|
||||
// 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 (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 := rpctypes.GetKeyByAddress(api.ethAPI.GetKeys(), addr)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("cannot find key with address %s", addr.String())
|
||||
}
|
||||
|
||||
sig, err := crypto.Sign(accounts.TextHash(data), key.ToECDSA())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sig[crypto.RecoveryIDOffset] += 27 // transform V from 0/1 to 27/28
|
||||
return sig, nil
|
||||
}
|
||||
|
||||
// EcRecover returns the address for the account that was used to create the signature.
|
||||
// Note, this function is compatible with eth_sign and personal_sign. As such it recovers
|
||||
// the address of:
|
||||
// hash = keccak256("\x19Ethereum Signed Message:\n"${message length}${message})
|
||||
// addr = ecrecover(hash, signature)
|
||||
//
|
||||
// Note, the signature must conform to the secp256k1 curve R, S and V values, where
|
||||
// the V value must be 27 or 28 for legacy reasons.
|
||||
//
|
||||
// https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_ecRecove
|
||||
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)
|
||||
}
|
||||
if sig[crypto.RecoveryIDOffset] != 27 && sig[crypto.RecoveryIDOffset] != 28 {
|
||||
return common.Address{}, fmt.Errorf("invalid Ethereum signature (V is not 27 or 28)")
|
||||
}
|
||||
sig[crypto.RecoveryIDOffset] -= 27 // Transform yellow paper V from 27/28 to 0/1
|
||||
|
||||
pubkey, err := crypto.SigToPub(accounts.TextHash(data), sig)
|
||||
if err != nil {
|
||||
return common.Address{}, err
|
||||
}
|
||||
return crypto.PubkeyToAddress(*pubkey), nil
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package web3
|
||||
|
||||
import (
|
||||
"github.com/cosmos/ethermint/version"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
)
|
||||
|
||||
// PublicWeb3API is the web3_ prefixed set of APIs in the Web3 JSON-RPC spec.
|
||||
type PublicWeb3API struct{}
|
||||
|
||||
// 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 (PublicWeb3API) ClientVersion() string {
|
||||
return version.ClientVersion()
|
||||
}
|
||||
|
||||
// Sha3 returns the keccak-256 hash of the passed-in input.
|
||||
func (PublicWeb3API) Sha3(input hexutil.Bytes) hexutil.Bytes {
|
||||
return crypto.Keccak256(input)
|
||||
}
|
||||
Reference in New Issue
Block a user