more fixes

This commit is contained in:
Federico Kunze
2021-04-18 18:39:15 +02:00
parent 614e62fb7e
commit cb2ab3d95d
63 changed files with 83570 additions and 3393 deletions
+54
View File
@@ -0,0 +1,54 @@
// Package rpc contains RPC handler methods and utilities to start
// Ethermint's Web3-compatibly JSON-RPC server.
package rpc
import (
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/ethermint/ethereum/rpc/types"
"github.com/ethereum/go-ethereum/rpc"
rpcclient "github.com/tendermint/tendermint/rpc/jsonrpc/client"
)
// RPC namespaces and API version
const (
Web3Namespace = "web3"
EthNamespace = "eth"
PersonalNamespace = "personal"
NetNamespace = "net"
apiVersion = "1.0"
)
// GetRPCAPIs returns the list of all APIs
func GetRPCAPIs(clientCtx client.Context, tmWSClient *rpcclient.WSClient) []rpc.API {
nonceLock := new(types.AddrLocker)
backend := NewEVMBackend(clientCtx)
ethAPI := NewPublicEthAPI(clientCtx, backend, nonceLock)
return []rpc.API{
{
Namespace: Web3Namespace,
Version: apiVersion,
Service: NewPublicWeb3API(),
Public: true,
},
{
Namespace: EthNamespace,
Version: apiVersion,
Service: ethAPI,
Public: true,
},
{
Namespace: EthNamespace,
Version: apiVersion,
Service: NewPublicFilterAPI(tmWSClient, backend),
Public: true,
},
{
Namespace: NetNamespace,
Version: apiVersion,
Service: NewPublicNetAPI(clientCtx),
Public: true,
},
}
}
+338
View File
@@ -0,0 +1,338 @@
package rpc
import (
"context"
"math/big"
"regexp"
"github.com/cosmos/ethermint/ethereum/rpc/types"
"github.com/pkg/errors"
tmtypes "github.com/tendermint/tendermint/types"
log "github.com/xlab/suplog"
"github.com/cosmos/cosmos-sdk/client"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
ethtypes "github.com/ethereum/go-ethereum/core/types"
ethermint "github.com/cosmos/ethermint/types"
evmtypes "github.com/cosmos/ethermint/x/evm/types"
)
// Backend implements the functionality needed to filter changes.
// Implemented by EVMBackend.
type Backend interface {
// Used by block filter; also used for polling
BlockNumber() (hexutil.Uint64, error)
HeaderByNumber(blockNum types.BlockNumber) (*ethtypes.Header, error)
HeaderByHash(blockHash common.Hash) (*ethtypes.Header, error)
GetBlockByNumber(blockNum types.BlockNumber, fullTx bool) (map[string]interface{}, error)
GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error)
// returns the logs of a given block
GetLogs(blockHash common.Hash) ([][]*ethtypes.Log, error)
// Used by pending transaction filter
PendingTransactions() ([]*types.Transaction, error)
// Used by log filter
GetTransactionLogs(txHash common.Hash) ([]*ethtypes.Log, error)
BloomStatus() (uint64, uint64)
}
var _ Backend = (*EVMBackend)(nil)
// implements the Backend interface
type EVMBackend struct {
ctx context.Context
clientCtx client.Context
queryClient *types.QueryClient // gRPC query client
logger log.Logger
}
func NewEVMBackend(clientCtx client.Context) *EVMBackend {
return &EVMBackend{
ctx: context.Background(),
clientCtx: clientCtx,
queryClient: types.NewQueryClient(clientCtx),
logger: log.WithField("module", "evm-backend"),
}
}
// BlockNumber returns the current block number.
func (e *EVMBackend) BlockNumber() (hexutil.Uint64, error) {
// NOTE: using 0 as min and max height returns the blockchain info up to the latest block.
info, err := e.clientCtx.Client.BlockchainInfo(e.ctx, 0, 0)
if err != nil {
return hexutil.Uint64(0), err
}
return hexutil.Uint64(info.LastHeight), nil
}
// GetBlockByNumber returns the block identified by number.
func (e *EVMBackend) GetBlockByNumber(blockNum types.BlockNumber, fullTx bool) (map[string]interface{}, error) {
height := blockNum.Int64()
currentBlockNumber, _ := e.BlockNumber()
switch blockNum {
case types.EthLatestBlockNumber:
if currentBlockNumber > 0 {
height = int64(currentBlockNumber - 1)
}
case types.EthPendingBlockNumber:
if currentBlockNumber > 0 {
height = int64(currentBlockNumber)
}
case types.EthEarliestBlockNumber:
height = 1
default:
if blockNum < 0 {
err := errors.Errorf("incorrect block height: %d", height)
return nil, err
} else if height > int64(currentBlockNumber) {
return nil, nil
}
}
resBlock, err := e.clientCtx.Client.Block(e.ctx, &height)
if err != nil {
// e.logger.Debugf("GetBlockByNumber safely bumping down from %d to latest", height)
if resBlock, err = e.clientCtx.Client.Block(e.ctx, nil); err != nil {
e.logger.Warningln("GetBlockByNumber failed to get latest block")
return nil, nil
}
}
res, err := e.ethBlockFromTendermint(e.clientCtx, e.queryClient, resBlock.Block, fullTx)
if err != nil {
e.logger.WithError(err).Warningf("EthBlockFromTendermint failed with block %s", resBlock.Block.String())
}
return res, err
}
// GetBlockByHash returns the block identified by hash.
func (e *EVMBackend) GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) {
resBlock, err := e.clientCtx.Client.BlockByHash(e.ctx, hash.Bytes())
if err != nil {
e.logger.Warningf("BlockByHash failed for %s", hash.Hex())
return nil, err
}
return e.ethBlockFromTendermint(e.clientCtx, e.queryClient, resBlock.Block, fullTx)
}
// ethBlockFromTendermint returns a JSON-RPC compatible Ethereum block from a given Tendermint block.
func (e *EVMBackend) ethBlockFromTendermint(
clientCtx client.Context,
queryClient evmtypes.QueryClient,
block *tmtypes.Block,
fullTx bool,
) (map[string]interface{}, error) {
txReceiptsResp, err := queryClient.TxReceiptsByBlockHeight(types.ContextWithHeight(0), &evmtypes.QueryTxReceiptsByBlockHeightRequest{
Height: block.Height,
})
if err != nil {
e.logger.Warningf("TxReceiptsByBlockHeight fail: %s", err.Error())
return nil, err
}
gasUsed := big.NewInt(0)
ethRPCTxs := make([]interface{}, 0, len(txReceiptsResp.Receipts))
if !fullTx {
// simply hashes
for _, receipt := range txReceiptsResp.Receipts {
ethRPCTxs = append(ethRPCTxs, common.BytesToHash(receipt.Hash))
}
} else {
// full txns from receipts
for _, receipt := range txReceiptsResp.Receipts {
tx, err := NewTransactionFromData(
receipt.Data,
common.BytesToAddress(receipt.From),
common.BytesToHash(receipt.Hash),
common.BytesToHash(receipt.BlockHash),
receipt.BlockHeight,
receipt.Index,
)
if err != nil {
e.logger.Warningf("NewTransactionFromData fail: %s", err.Error())
continue
}
ethRPCTxs = append(ethRPCTxs, tx)
gasUsed.Add(gasUsed, new(big.Int).SetUint64(receipt.Result.GasUsed))
}
}
blockBloomResp, err := queryClient.BlockBloom(types.ContextWithHeight(0), &evmtypes.QueryBlockBloomRequest{
Height: block.Height,
})
if err != nil {
err = errors.Wrapf(err, "failed to query BlockBloom for height %d", block.Height)
return nil, err
}
bloom := ethtypes.BytesToBloom(blockBloomResp.Bloom)
formattedBlock := formatBlock(block.Header, block.Size(), ethermint.DefaultRPCGasLimit, gasUsed, ethRPCTxs, bloom)
return formattedBlock, nil
}
// HeaderByNumber returns the block header identified by height.
func (e *EVMBackend) HeaderByNumber(blockNum types.BlockNumber) (*ethtypes.Header, error) {
height := blockNum.Int64()
currentBlockNumber, _ := e.BlockNumber()
switch blockNum {
case types.EthLatestBlockNumber:
if currentBlockNumber > 0 {
height = int64(currentBlockNumber - 1)
}
case types.EthPendingBlockNumber:
if currentBlockNumber > 0 {
height = int64(currentBlockNumber)
}
case types.EthEarliestBlockNumber:
height = 1
default:
if blockNum < 0 {
err := errors.Errorf("incorrect block height: %d", height)
return nil, err
}
}
resBlock, err := e.clientCtx.Client.Block(e.ctx, &height)
if err != nil {
e.logger.Warningf("HeaderByNumber failed")
return nil, err
}
req := &evmtypes.QueryBlockBloomRequest{
Height: resBlock.Block.Height,
}
res, err := e.queryClient.BlockBloom(types.ContextWithHeight(resBlock.Block.Height), req)
if err != nil {
e.logger.Warningf("HeaderByNumber BlockBloom fail %d", resBlock.Block.Height)
return nil, err
}
ethHeader := EthHeaderFromTendermint(resBlock.Block.Header)
ethHeader.Bloom = ethtypes.BytesToBloom(res.Bloom)
return ethHeader, nil
}
// HeaderByHash returns the block header identified by hash.
func (e *EVMBackend) HeaderByHash(blockHash common.Hash) (*ethtypes.Header, error) {
resBlock, err := e.clientCtx.Client.BlockByHash(e.ctx, blockHash.Bytes())
if err != nil {
e.logger.Warningf("HeaderByHash fail")
return nil, err
}
req := &evmtypes.QueryBlockBloomRequest{
Height: resBlock.Block.Height,
}
res, err := e.queryClient.BlockBloom(types.ContextWithHeight(resBlock.Block.Height), req)
if err != nil {
e.logger.Warningf("HeaderByHash BlockBloom fail %d", resBlock.Block.Height)
return nil, err
}
ethHeader := EthHeaderFromTendermint(resBlock.Block.Header)
ethHeader.Bloom = ethtypes.BytesToBloom(res.Bloom)
return ethHeader, nil
}
// GetTransactionLogs returns the logs given a transaction hash.
// It returns an error if there's an encoding error.
// If no logs are found for the tx hash, the error is nil.
func (e *EVMBackend) GetTransactionLogs(txHash common.Hash) ([]*ethtypes.Log, error) {
req := &evmtypes.QueryTxLogsRequest{
Hash: txHash.String(),
}
res, err := e.queryClient.TxLogs(e.ctx, req)
if err != nil {
e.logger.Warningf("TxLogs fail")
return nil, err
}
return evmtypes.LogsToEthereum(res.Logs), nil
}
// PendingTransactions returns the transactions that are in the transaction pool
// and have a from address that is one of the accounts this node manages.
func (e *EVMBackend) PendingTransactions() ([]*types.Transaction, error) {
return []*types.Transaction{}, nil
}
// GetLogs returns all the logs from all the ethereum transactions in a block.
func (e *EVMBackend) GetLogs(blockHash common.Hash) ([][]*ethtypes.Log, error) {
// NOTE: we query the state in case the tx result logs are not persisted after an upgrade.
req := &evmtypes.QueryBlockLogsRequest{
Hash: blockHash.String(),
}
res, err := e.queryClient.BlockLogs(e.ctx, req)
if err != nil {
e.logger.Warningf("BlockLogs fail")
return nil, err
}
var blockLogs = [][]*ethtypes.Log{}
for _, txLog := range res.TxLogs {
blockLogs = append(blockLogs, txLog.EthLogs())
}
return blockLogs, nil
}
// This is very brittle, see: https://github.com/tendermint/tendermint/issues/4740
var regexpMissingHeight = regexp.MustCompile(`height \d+ (must be less than or equal to|is not available)`)
func (e *EVMBackend) GetLogsByNumber(blockNum types.BlockNumber) ([][]*ethtypes.Log, error) {
height := blockNum.Int64()
currentBlockNumber, _ := e.BlockNumber()
switch blockNum {
case types.EthLatestBlockNumber:
if currentBlockNumber > 0 {
height = int64(currentBlockNumber - 1)
}
case types.EthPendingBlockNumber:
if currentBlockNumber > 0 {
height = int64(currentBlockNumber)
}
case types.EthEarliestBlockNumber:
height = 1
default:
if blockNum < 0 {
err := errors.Errorf("incorrect block height: %d", height)
return nil, err
}
}
resBlock, err := e.clientCtx.Client.Block(e.ctx, &height)
if err != nil {
if regexpMissingHeight.MatchString(err.Error()) {
return [][]*ethtypes.Log{}, nil
}
e.logger.Warningf("failed to query block at %d", height)
return nil, err
}
return e.GetLogs(common.BytesToHash(resBlock.BlockID.Hash))
}
// BloomStatus returns the BloomBitsBlocks and the number of processed sections maintained
// by the chain indexer.
func (e *EVMBackend) BloomStatus() (uint64, uint64) {
return 4096, 0
}
+10
View File
@@ -0,0 +1,10 @@
// Package rpc contains RPC handler methods, namespaces and utilities to start
// Ethermint's Web3-compatible JSON-RPC server.
//
// The list of available namespaces are:
//
// * `rpc/namespaces/eth`: `eth` namespace. Exposes the `PublicEthereumAPI` and the `PublicFilterAPI`.
// * `rpc/namespaces/personal`: `personal` namespace. Exposes the `PrivateAccountAPI`.
// * `rpc/namespaces/net`: `net` namespace. Exposes the `PublicNetAPI`.
// * `rpc/namespaces/web3`: `web3` namespace. Exposes the `PublicWeb3API`
package rpc
+760
View File
@@ -0,0 +1,760 @@
package rpc
import (
"bytes"
"context"
"encoding/json"
"math/big"
"strings"
"sync"
"github.com/cosmos/ethermint/ethereum/rpc/types"
"github.com/gogo/protobuf/jsonpb"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/pkg/errors"
log "github.com/xlab/suplog"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
sdk "github.com/cosmos/cosmos-sdk/types"
authtx "github.com/cosmos/cosmos-sdk/x/auth/tx"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
"github.com/tendermint/tendermint/crypto/tmhash"
"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"
rpctypes "github.com/cosmos/ethermint/ethereum/rpc/types"
ethermint "github.com/cosmos/ethermint/types"
evmtypes "github.com/cosmos/ethermint/x/evm/types"
)
// PublicEthAPI is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec.
type PublicEthAPI struct {
ctx context.Context
clientCtx client.Context
queryClient *types.QueryClient
chainIDEpoch *big.Int
logger log.Logger
backend Backend
nonceLock *types.AddrLocker
keyringLock sync.Mutex
}
// NewPublicEthAPI creates an instance of the public ETH Web3 API.
func NewPublicEthAPI(
clientCtx client.Context,
backend Backend,
nonceLock *types.AddrLocker,
) *PublicEthAPI {
epoch, err := ethermint.ParseChainID(clientCtx.ChainID)
if err != nil {
panic(err)
}
api := &PublicEthAPI{
ctx: context.Background(),
clientCtx: clientCtx,
queryClient: types.NewQueryClient(clientCtx),
chainIDEpoch: epoch,
logger: log.WithField("module", "json-rpc"),
backend: backend,
nonceLock: nonceLock,
}
return api
}
// ProtocolVersion returns the supported Ethereum protocol version.
func (e *PublicEthAPI) ProtocolVersion() hexutil.Uint {
e.logger.Debugln("eth_protocolVersion")
return hexutil.Uint(evmtypes.ProtocolVersion)
}
// ChainId returns the chain's identifier in hex format
func (e *PublicEthAPI) ChainId() (hexutil.Uint, error) { // nolint
e.logger.Debugln("eth_chainId")
return hexutil.Uint(uint(e.chainIDEpoch.Uint64())), nil
}
// Syncing returns whether or not the current node is syncing with other peers. Returns false if not, or a struct
// outlining the state of the sync if it is.
func (e *PublicEthAPI) Syncing() (interface{}, error) {
e.logger.Debugln("eth_syncing")
status, err := e.clientCtx.Client.Status(e.ctx)
if err != nil {
return false, err
}
if !status.SyncInfo.CatchingUp {
return false, nil
}
return map[string]interface{}{
// "startingBlock": nil, // NA
"currentBlock": hexutil.Uint64(status.SyncInfo.LatestBlockHeight),
// "highestBlock": nil, // NA
// "pulledStates": nil, // NA
// "knownStates": nil, // NA
}, nil
}
// Coinbase is the address that staking rewards will be send to (alias for Etherbase).
func (e *PublicEthAPI) Coinbase() (common.Address, error) {
e.logger.Debugln("eth_coinbase")
node, err := e.clientCtx.GetNode()
if err != nil {
return common.Address{}, err
}
status, err := node.Status(e.ctx)
if err != nil {
return common.Address{}, err
}
return common.BytesToAddress(status.ValidatorInfo.Address.Bytes()), nil
}
// Mining returns whether or not this node is currently mining. Always false.
func (e *PublicEthAPI) Mining() bool {
e.logger.Debugln("eth_mining")
return false
}
// Hashrate returns the current node's hashrate. Always 0.
func (e *PublicEthAPI) Hashrate() hexutil.Uint64 {
e.logger.Debugln("eth_hashrate")
return 0
}
// GasPrice returns the current gas price based on Ethermint's gas price oracle.
func (e *PublicEthAPI) GasPrice() *hexutil.Big {
e.logger.Debugln("eth_gasPrice")
out := big.NewInt(0)
return (*hexutil.Big)(out)
}
// Accounts returns the list of accounts available to this node.
func (e *PublicEthAPI) Accounts() ([]common.Address, error) {
e.logger.Debugln("eth_accounts")
return []common.Address{}, nil
}
// BlockNumber returns the current block number.
func (e *PublicEthAPI) BlockNumber() (hexutil.Uint64, error) {
//e.logger.Debugln("eth_blockNumber")
return e.backend.BlockNumber()
}
// GetBalance returns the provided account's balance up to the provided block number.
func (e *PublicEthAPI) GetBalance(address common.Address, blockNum types.BlockNumber) (*hexutil.Big, error) { // nolint: interfacer
e.logger.Debugln("eth_getBalance", "address", address.Hex(), "block number", blockNum)
req := &evmtypes.QueryBalanceRequest{
Address: address.String(),
}
res, err := e.queryClient.Balance(types.ContextWithHeight(blockNum.Int64()), req)
if err != nil {
return nil, err
}
val, err := ethermint.UnmarshalBigInt(res.Balance)
if err != nil {
return nil, err
}
return (*hexutil.Big)(val), nil
}
// GetStorageAt returns the contract storage at the given address, block number, and key.
func (e *PublicEthAPI) GetStorageAt(address common.Address, key string, blockNum types.BlockNumber) (hexutil.Bytes, error) { // nolint: interfacer
e.logger.Debugln("eth_getStorageAt", "address", address.Hex(), "key", key, "block number", blockNum)
req := &evmtypes.QueryStorageRequest{
Address: address.String(),
Key: key,
}
res, err := e.queryClient.Storage(types.ContextWithHeight(blockNum.Int64()), req)
if err != nil {
return nil, err
}
value := common.HexToHash(res.Value)
return value.Bytes(), nil
}
// GetTransactionCount returns the number of transactions at the given address up to the given block number.
func (e *PublicEthAPI) GetTransactionCount(address common.Address, blockNum types.BlockNumber) (*hexutil.Uint64, error) {
e.logger.Debugln("eth_getTransactionCount", "address", address.Hex(), "block number", blockNum)
// Get nonce (sequence) from account
from := sdk.AccAddress(address.Bytes())
accRet := e.clientCtx.AccountRetriever
err := accRet.EnsureExists(e.clientCtx, from)
if err != nil {
// account doesn't exist yet, return 0
n := hexutil.Uint64(0)
return &n, nil
}
_, nonce, err := accRet.GetAccountNumberSequence(e.clientCtx, from)
if err != nil {
return nil, err
}
n := hexutil.Uint64(nonce)
return &n, nil
}
// GetBlockTransactionCountByHash returns the number of transactions in the block identified by hash.
func (e *PublicEthAPI) GetBlockTransactionCountByHash(hash common.Hash) *hexutil.Uint {
e.logger.Debugln("eth_getBlockTransactionCountByHash", "hash", hash.Hex())
resBlock, err := e.clientCtx.Client.BlockByHash(e.ctx, hash.Bytes())
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 (e *PublicEthAPI) GetBlockTransactionCountByNumber(blockNum types.BlockNumber) *hexutil.Uint {
e.logger.Debugln("eth_getBlockTransactionCountByNumber", "block number", blockNum)
resBlock, err := e.clientCtx.Client.Block(e.ctx, blockNum.TmHeight())
if err != nil {
return nil
}
n := hexutil.Uint(len(resBlock.Block.Txs))
return &n
}
// GetUncleCountByBlockHash returns the number of uncles in the block identified by hash. Always zero.
func (e *PublicEthAPI) GetUncleCountByBlockHash(hash common.Hash) hexutil.Uint {
return 0
}
// GetUncleCountByBlockNumber returns the number of uncles in the block identified by number. Always zero.
func (e *PublicEthAPI) GetUncleCountByBlockNumber(blockNum types.BlockNumber) hexutil.Uint {
return 0
}
// GetCode returns the contract code at the given address and block number.
func (e *PublicEthAPI) GetCode(address common.Address, blockNumber types.BlockNumber) (hexutil.Bytes, error) { // nolint: interfacer
e.logger.Debugln("eth_getCode", "address", address.Hex(), "block number", blockNumber)
req := &evmtypes.QueryCodeRequest{
Address: address.String(),
}
res, err := e.queryClient.Code(types.ContextWithHeight(blockNumber.Int64()), req)
if err != nil {
return nil, err
}
return res.Code, nil
}
// GetTransactionLogs returns the logs given a transaction hash.
func (e *PublicEthAPI) GetTransactionLogs(txHash common.Hash) ([]*ethtypes.Log, error) {
e.logger.Debugln("eth_getTransactionLogs", "hash", txHash)
return e.backend.GetTransactionLogs(txHash)
}
// Sign signs the provided data using the private key of address via Geth's signature standard.
func (e *PublicEthAPI) Sign(address common.Address, data hexutil.Bytes) (hexutil.Bytes, error) {
e.logger.Debugln("eth_sign", "address", address.Hex(), "data", common.Bytes2Hex(data))
return nil, errors.New("eth_sign not supported")
}
// SendTransaction sends an Ethereum transaction.
func (e *PublicEthAPI) SendTransaction(args types.SendTxArgs) (common.Hash, error) {
e.logger.Debugln("eth_sendTransaction", "args", args)
return common.Hash{}, errors.New("eth_sendTransaction not supported")
}
// SendRawTransaction send a raw Ethereum transaction.
func (e *PublicEthAPI) SendRawTransaction(data hexutil.Bytes) (common.Hash, error) {
e.logger.Debugln("eth_sendRawTransaction", "data_len", len(data))
ethereumTx := new(evmtypes.MsgEthereumTx)
// RLP decode raw transaction bytes
if err := rlp.DecodeBytes(data, ethereumTx); err != nil {
e.logger.WithError(err).Errorln("transaction RLP decode failed")
// Return nil is for when gasLimit overflows uint64
return common.Hash{}, err
}
builder, ok := e.clientCtx.TxConfig.NewTxBuilder().(authtx.ExtensionOptionsTxBuilder)
if !ok {
e.logger.Panicln("clientCtx.TxConfig.NewTxBuilder returns unsupported builder")
}
option, err := codectypes.NewAnyWithValue(&evmtypes.ExtensionOptionsEthereumTx{})
if err != nil {
e.logger.Panicln("codectypes.NewAnyWithValue failed to pack an obvious value")
}
builder.SetExtensionOptions(option)
err = builder.SetMsgs(ethereumTx.GetMsgs()...)
if err != nil {
e.logger.Panicln("builder.SetMsgs failed")
}
fees := sdk.NewCoins(ethermint.NewPhotonCoin(sdk.NewIntFromBigInt(ethereumTx.Fee())))
builder.SetFeeAmount(fees)
builder.SetGasLimit(ethereumTx.GetGas())
// Encode transaction by default Tx encoder
txBytes, err := e.clientCtx.TxConfig.TxEncoder()(builder.GetTx())
if err != nil {
e.logger.WithError(err).Errorln("failed to encode Eth tx using default encoder")
return common.Hash{}, err
}
txHash := common.BytesToHash(tmhash.Sum(txBytes))
asyncCtx := e.clientCtx.WithBroadcastMode(flags.BroadcastAsync)
if _, err := asyncCtx.BroadcastTx(txBytes); err != nil {
e.logger.WithError(err).Errorln("failed to broadcast Eth tx")
return txHash, err
}
return txHash, nil
}
// Call performs a raw contract call.
func (e *PublicEthAPI) Call(args types.CallArgs, blockNr types.BlockNumber, _ *map[common.Address]types.Account) (hexutil.Bytes, error) {
//e.logger.Debugln("eth_call", "args", args, "block number", blockNr)
simRes, err := e.doCall(args, blockNr, big.NewInt(ethermint.DefaultRPCGasLimit))
if err != nil {
return []byte{}, err
} else if len(simRes.Result.Log) > 0 {
var logs []sdkTxLogs
if err := json.Unmarshal([]byte(simRes.Result.Log), &logs); err != nil {
e.logger.WithError(err).Errorln("failed to unmarshal simRes.Result.Log")
}
if len(logs) > 0 && logs[0].Log == logRevertedFlag {
data, err := evmtypes.DecodeTxResponse(simRes.Result.Data)
if err != nil {
e.logger.WithError(err).Warningln("call result decoding failed")
return []byte{}, err
}
return []byte{}, errRevertedWith(data.Ret)
}
}
data, err := evmtypes.DecodeTxResponse(simRes.Result.Data)
if err != nil {
e.logger.WithError(err).Warningln("call result decoding failed")
return []byte{}, err
}
return (hexutil.Bytes)(data.Ret), nil
}
var zeroAddr = common.HexToAddress("0x0000000000000000000000000000000000000000")
// DoCall performs a simulated call operation through the evmtypes. It returns the
// estimated gas used on the operation or an error if fails.
func (e *PublicEthAPI) doCall(
args types.CallArgs, blockNr types.BlockNumber, globalGasCap *big.Int,
) (*sdk.SimulationResponse, error) {
// Set default gas & gas price if none were set
// Change this to uint64(math.MaxUint64 / 2) if gas cap can be configured
gas := uint64(ethermint.DefaultRPCGasLimit)
if args.Gas != nil {
gas = uint64(*args.Gas)
}
if globalGasCap != nil && globalGasCap.Uint64() < gas {
e.logger.Debugln("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 fromAddr sdk.AccAddress
if args.From != nil {
fromAddr = sdk.AccAddress(args.From.Bytes())
} else {
fromAddr = sdk.AccAddress(zeroAddr.Bytes())
}
_, seq, err := e.clientCtx.AccountRetriever.GetAccountNumberSequence(e.clientCtx, fromAddr)
if err != nil {
return nil, err
}
// Create new call message
msg := evmtypes.NewMsgEthereumTx(seq, args.To, value, gas, gasPrice, data)
if err := msg.ValidateBasic(); err != nil {
return nil, err
}
msg.From = &evmtypes.SigCache{
Address: fromAddr.Bytes(),
}
if err := msg.ValidateBasic(); err != nil {
return nil, err
}
// Create a TxBuilder
txBuilder, ok := e.clientCtx.TxConfig.NewTxBuilder().(authtx.ExtensionOptionsTxBuilder)
if !ok {
log.Panicln("clientCtx.TxConfig.NewTxBuilder returns unsupported builder")
}
option, err := codectypes.NewAnyWithValue(&evmtypes.ExtensionOptionsEthereumTx{})
if err != nil {
log.Panicln("codectypes.NewAnyWithValue failed to pack an obvious value")
}
txBuilder.SetExtensionOptions(option)
if err := txBuilder.SetMsgs(msg); err != nil {
log.Panicln("builder.SetMsgs failed")
}
fees := sdk.NewCoins(ethermint.NewPhotonCoin(sdk.NewIntFromBigInt(msg.Fee())))
txBuilder.SetFeeAmount(fees)
txBuilder.SetGasLimit(gas)
//doc about generate and transform tx into json, protobuf bytes
//https://github.com/cosmos/cosmos-sdk/blob/master/docs/run-node/txs.md
txBytes, err := e.clientCtx.TxConfig.TxEncoder()(txBuilder.GetTx())
if err != nil {
return nil, err
}
// simulate by calling ABCI Query
query := abci.RequestQuery{
Path: "/app/simulate",
Data: txBytes,
Height: blockNr.Int64(),
}
queryResult, err := e.clientCtx.QueryABCI(query)
if err != nil {
return nil, err
}
var simResponse sdk.SimulationResponse
err = jsonpb.Unmarshal(strings.NewReader(string(queryResult.Value)), &simResponse)
if err != nil {
return nil, err
}
return &simResponse, nil
}
// EstimateGas returns an estimate of gas usage for the given smart contract call.
// It adds 1,000 gas to the returned value instead of using the gas adjustment
// param from the SDK.
func (e *PublicEthAPI) EstimateGas(args types.CallArgs) (hexutil.Uint64, error) {
e.logger.Debugln("eth_estimateGas")
// From ContextWithHeight: if the provided height is 0,
// it will return an empty context and the gRPC query will use
// the latest block height for querying.
simRes, err := e.doCall(args, 0, big.NewInt(ethermint.DefaultRPCGasLimit))
if err != nil {
return 0, err
} else if len(simRes.Result.Log) > 0 {
var logs []sdkTxLogs
if err := json.Unmarshal([]byte(simRes.Result.Log), &logs); err != nil {
e.logger.WithError(err).Errorln("failed to unmarshal simRes.Result.Log")
}
if len(logs) > 0 && logs[0].Log == logRevertedFlag {
data, err := evmtypes.DecodeTxResponse(simRes.Result.Data)
if err != nil {
e.logger.WithError(err).Warningln("call result decoding failed")
return 0, err
}
return 0, errRevertedWith(data.Ret)
}
}
// TODO: change 1000 buffer for more accurate buffer (eg: SDK's gasAdjusted)
estimatedGas := simRes.GasInfo.GasUsed
gas := estimatedGas + 200000
return hexutil.Uint64(gas), nil
}
// GetBlockByHash returns the block identified by hash.
func (e *PublicEthAPI) GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) {
e.logger.Debugln("eth_getBlockByHash", "hash", hash.Hex(), "full", fullTx)
return e.backend.GetBlockByHash(hash, fullTx)
}
// GetBlockByNumber returns the block identified by number.
func (e *PublicEthAPI) GetBlockByNumber(ethBlockNum types.BlockNumber, fullTx bool) (map[string]interface{}, error) {
// e.logger.Debugln("eth_getBlockByNumber", "number", ethBlockNum, "full", fullTx)
return e.backend.GetBlockByNumber(ethBlockNum, fullTx)
}
// GetTransactionByHash returns the transaction identified by hash.
func (e *PublicEthAPI) GetTransactionByHash(hash common.Hash) (*types.Transaction, error) {
e.logger.Debugln("eth_getTransactionByHash", "hash", hash.Hex())
resp, err := e.queryClient.TxReceipt(e.ctx, &evmtypes.QueryTxReceiptRequest{
Hash: hash.Hex(),
})
if err != nil {
e.logger.Debugf("failed to get tx info for %s: %s", hash.Hex(), err.Error())
return nil, nil
}
return NewTransactionFromData(
resp.Receipt.Data,
common.BytesToAddress(resp.Receipt.From),
common.BytesToHash(resp.Receipt.Hash),
common.BytesToHash(resp.Receipt.BlockHash),
resp.Receipt.BlockHeight,
resp.Receipt.Index,
)
}
// GetTransactionByBlockHashAndIndex returns the transaction identified by hash and index.
func (e *PublicEthAPI) GetTransactionByBlockHashAndIndex(hash common.Hash, idx hexutil.Uint) (*types.Transaction, error) {
e.logger.Debugln("eth_getTransactionByHashAndIndex", "hash", hash.Hex(), "index", idx)
resp, err := e.queryClient.TxReceiptsByBlockHash(e.ctx, &evmtypes.QueryTxReceiptsByBlockHashRequest{
Hash: hash.Hex(),
})
if err != nil {
err = errors.Wrap(err, "failed to query tx receipts by block hash")
return nil, err
}
return e.getReceiptByIndex(resp.Receipts, hash, idx)
}
// GetTransactionByBlockNumberAndIndex returns the transaction identified by number and index.
func (e *PublicEthAPI) GetTransactionByBlockNumberAndIndex(blockNum types.BlockNumber, idx hexutil.Uint) (*types.Transaction, error) {
e.logger.Debugln("eth_getTransactionByBlockNumberAndIndex", "number", blockNum, "index", idx)
resp, err := e.queryClient.TxReceiptsByBlockHeight(e.ctx, &evmtypes.QueryTxReceiptsByBlockHeightRequest{
Height: blockNum.Int64(),
})
if err != nil {
err = errors.Wrap(err, "failed to query tx receipts by block height")
return nil, err
}
return e.getReceiptByIndex(resp.Receipts, common.Hash{}, idx)
}
func (e *PublicEthAPI) getReceiptByIndex(receipts []*evmtypes.TxReceipt, blockHash common.Hash, idx hexutil.Uint) (*types.Transaction, error) {
// return if index out of bounds
if uint64(idx) >= uint64(len(receipts)) {
return nil, nil
}
receipt := receipts[idx]
if (blockHash != common.Hash{}) {
if !bytes.Equal(receipt.BlockHash, blockHash.Bytes()) {
err := errors.Errorf("receipt found but block hashes don't match %s != %s",
common.Bytes2Hex(receipt.BlockHash),
blockHash.Hex(),
)
return nil, err
}
}
return NewTransactionFromData(
receipt.Data,
common.BytesToAddress(receipt.From),
common.BytesToHash(receipt.Hash),
blockHash,
receipt.BlockHeight,
uint64(idx),
)
}
// GetTransactionReceipt returns the transaction receipt identified by hash.
func (e *PublicEthAPI) GetTransactionReceipt(hash common.Hash) (map[string]interface{}, error) {
e.logger.Debugln("eth_getTransactionReceipt", "hash", hash.Hex())
ctx := types.ContextWithHeight(int64(0))
tx, err := e.queryClient.TxReceipt(ctx, &evmtypes.QueryTxReceiptRequest{
Hash: hash.Hex(),
})
if err != nil {
e.logger.Debugf("failed to get tx receipt for %s: %s", hash.Hex(), err.Error())
return nil, nil
}
// Query block for consensus hash
height := int64(tx.Receipt.BlockHeight)
block, err := e.clientCtx.Client.Block(e.ctx, &height)
if err != nil {
e.logger.Warningln("didnt find block for tx height", height)
return nil, err
}
cumulativeGasUsed := uint64(tx.Receipt.Result.GasUsed)
if tx.Receipt.Index != 0 {
cumulativeGasUsed += rpctypes.GetBlockCumulativeGas(e.clientCtx, block.Block, int(tx.Receipt.Index))
}
var status hexutil.Uint
if tx.Receipt.Result.Reverted {
status = hexutil.Uint(0)
} else {
status = hexutil.Uint(1)
}
toHex := common.Address{}
if len(tx.Receipt.Data.Recipient) > 0 {
toHex = common.BytesToAddress(tx.Receipt.Data.Recipient)
}
contractAddress := common.HexToAddress(tx.Receipt.Result.ContractAddress)
logsBloom := hexutil.Encode(ethtypes.BytesToBloom(tx.Receipt.Result.Bloom).Bytes())
receipt := map[string]interface{}{
// Consensus fields: These fields are defined by the Yellow Paper
"status": status,
"cumulativeGasUsed": hexutil.Uint64(cumulativeGasUsed),
"logsBloom": logsBloom,
"logs": tx.Receipt.Result.TxLogs.EthLogs(),
// Implementation fields: These fields are added by geth when processing a transaction.
// They are stored in the chain database.
"transactionHash": hash.Hex(),
"contractAddress": contractAddress.Hex(),
"gasUsed": hexutil.Uint64(tx.Receipt.Result.GasUsed),
// Inclusion information: These fields provide information about the inclusion of the
// transaction corresponding to this receipt.
"blockHash": common.BytesToHash(block.Block.Header.Hash()).Hex(),
"blockNumber": hexutil.Uint64(tx.Receipt.BlockHeight),
"transactionIndex": hexutil.Uint64(tx.Receipt.Index),
// sender and receiver (contract or EOA) addreses
"from": common.BytesToAddress(tx.Receipt.From),
"to": toHex,
}
return receipt, nil
}
// PendingTransactions returns the transactions that are in the transaction pool
// and have a from address that is one of the accounts this node manages.
func (e *PublicEthAPI) PendingTransactions() ([]*types.Transaction, error) {
e.logger.Debugln("eth_getPendingTransactions")
return e.backend.PendingTransactions()
}
// GetUncleByBlockHashAndIndex returns the uncle identified by hash and index. Always returns nil.
func (e *PublicEthAPI) GetUncleByBlockHashAndIndex(hash common.Hash, idx hexutil.Uint) map[string]interface{} {
return nil
}
// GetUncleByBlockNumberAndIndex returns the uncle identified by number and index. Always returns nil.
func (e *PublicEthAPI) GetUncleByBlockNumberAndIndex(number hexutil.Uint, idx hexutil.Uint) map[string]interface{} {
return nil
}
// GetProof returns an account object with proof and any storage proofs
func (e *PublicEthAPI) GetProof(address common.Address, storageKeys []string, blockNumber types.BlockNumber) (*types.AccountResult, error) {
height := blockNumber.Int64()
e.logger.Debugln("eth_getProof", "address", address.Hex(), "keys", storageKeys, "number", height)
ctx := types.ContextWithHeight(height)
clientCtx := e.clientCtx.WithHeight(height)
// query storage proofs
storageProofs := make([]types.StorageResult, len(storageKeys))
for i, key := range storageKeys {
hexKey := common.HexToHash(key)
valueBz, proof, err := e.queryClient.GetProof(clientCtx, evmtypes.StoreKey, evmtypes.StateKey(address, hexKey.Bytes()))
if err != nil {
return nil, err
}
// check for proof
var proofStr string
if proof != nil {
proofStr = proof.String()
}
storageProofs[i] = types.StorageResult{
Key: key,
Value: (*hexutil.Big)(new(big.Int).SetBytes(valueBz)),
Proof: []string{proofStr},
}
}
// query EVM account
req := &evmtypes.QueryAccountRequest{
Address: address.String(),
}
res, err := e.queryClient.Account(ctx, req)
if err != nil {
return nil, err
}
// query account proofs
accountKey := authtypes.AddressStoreKey(sdk.AccAddress(address.Bytes()))
_, proof, err := e.queryClient.GetProof(clientCtx, authtypes.StoreKey, accountKey)
if err != nil {
return nil, err
}
// check for proof
var accProofStr string
if proof != nil {
accProofStr = proof.String()
}
balance, err := ethermint.UnmarshalBigInt(res.Balance)
if err != nil {
return nil, err
}
return &types.AccountResult{
Address: address,
AccountProof: []string{accProofStr},
Balance: (*hexutil.Big)(balance),
CodeHash: common.BytesToHash(res.CodeHash),
Nonce: hexutil.Uint64(res.Nonce),
StorageHash: common.Hash{}, // NOTE: Ethermint doesn't have a storage hash. TODO: implement?
StorageProof: storageProofs,
}, nil
}
+615
View File
@@ -0,0 +1,615 @@
package rpc
import (
"context"
"fmt"
"sync"
"time"
"github.com/cosmos/ethermint/ethereum/rpc/types"
log "github.com/xlab/suplog"
coretypes "github.com/tendermint/tendermint/rpc/core/types"
rpcclient "github.com/tendermint/tendermint/rpc/jsonrpc/client"
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"
evmtypes "github.com/cosmos/ethermint/x/evm/types"
)
// FiltersBackend defines the methods requided by the PublicFilterAPI backend
type FiltersBackend interface {
GetBlockByNumber(blockNum types.BlockNumber, fullTx bool) (map[string]interface{}, error)
HeaderByNumber(blockNum types.BlockNumber) (*ethtypes.Header, error)
HeaderByHash(blockHash common.Hash) (*ethtypes.Header, error)
GetLogs(blockHash common.Hash) ([][]*ethtypes.Log, error)
GetLogsByNumber(blockNum types.BlockNumber) ([][]*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 {
backend FiltersBackend
events *EventSystem
filtersMu sync.Mutex
filters map[rpc.ID]*filter
}
// NewPublicFilterAPI returns a new PublicFilterAPI instance.
func NewPublicFilterAPI(tmWSClient *rpcclient.WSClient, backend FiltersBackend) *PublicFilterAPI {
api := &PublicFilterAPI{
backend: backend,
filters: make(map[rpc.ID]*filter),
events: NewEventSystem(tmWSClient),
}
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, ok := <-txsCh:
if !ok {
api.filtersMu.Lock()
delete(api.filters, pendingTxSub.ID())
api.filtersMu.Unlock()
return
}
data, ok := ev.Data.(tmtypes.EventDataTx)
if !ok {
log.WithFields(log.Fields{
"expected": "tmtypes.EventDataTx",
"actual": fmt.Sprintf("%T", ev.Data),
}).Warningln("event Data type mismatch")
continue
}
txHash := common.BytesToHash(tmtypes.Tx(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, ok := <-txsCh:
if !ok {
api.filtersMu.Lock()
delete(api.filters, pendingTxSub.ID())
api.filtersMu.Unlock()
return
}
data, ok := ev.Data.(tmtypes.EventDataTx)
if !ok {
log.WithFields(log.Fields{
"expected": "tmtypes.EventDataTx",
"actual": fmt.Sprintf("%T", ev.Data),
}).Warningln("event Data type mismatch")
continue
}
txHash := common.BytesToHash(tmtypes.Tx(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, ok := <-headersCh:
if !ok {
api.filtersMu.Lock()
delete(api.filters, headerSub.ID())
api.filtersMu.Unlock()
return
}
data, ok := ev.Data.(tmtypes.EventDataNewBlockHeader)
if !ok {
log.WithFields(log.Fields{
"expected": "tmtypes.EventDataNewBlockHeader",
"actual": fmt.Sprintf("%T", ev.Data),
}).Warningln("event Data type mismatch")
continue
}
header := 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, ok := <-headersCh:
if !ok {
headersSub.Unsubscribe(api.events)
return
}
data, ok := ev.Data.(tmtypes.EventDataNewBlockHeader)
if !ok {
log.WithFields(log.Fields{
"expected": "tmtypes.EventDataNewBlockHeader",
"actual": fmt.Sprintf("%T", ev.Data),
}).Warningln("event Data type mismatch")
continue
}
header := 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 ev, ok := <-logsCh:
if !ok {
logsSub.Unsubscribe(api.events)
return
}
// filter only events from EVM module txs
_, isMsgEthereumTx := ev.Events[evmtypes.TypeMsgEthereumTx]
if !isMsgEthereumTx {
// ignore transaction as it's not from the evm module
return
}
// get transaction result data
dataTx, ok := ev.Data.(tmtypes.EventDataTx)
if !ok {
log.WithFields(log.Fields{
"expected": "tmtypes.EventDataTx",
"actual": fmt.Sprintf("%T", ev.Data),
}).Warningln("event Data type mismatch")
continue
}
txResponse, err := evmtypes.DecodeTxResponse(dataTx.TxResult.Result.Data)
if err != nil {
return
}
logs := filterLogs(txResponse.TxLogs.EthLogs(), 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 ev, ok := <-eventCh:
if !ok {
api.filtersMu.Lock()
delete(api.filters, filterID)
api.filtersMu.Unlock()
return
}
dataTx, ok := ev.Data.(tmtypes.EventDataTx)
if !ok {
log.WithFields(log.Fields{
"expected": "tmtypes.EventDataTx",
"actual": fmt.Sprintf("%T", ev.Data),
}).Warningln("event Data type mismatch")
continue
}
txResponse, err := evmtypes.DecodeTxResponse(dataTx.TxResult.Result.Data)
if err != nil {
return
}
logs := filterLogs(txResponse.TxLogs.EthLogs(), 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), err
}
// 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)
}
}
// 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
}
+345
View File
@@ -0,0 +1,345 @@
package rpc
import (
"context"
"fmt"
"sync"
"time"
"github.com/pkg/errors"
log "github.com/xlab/suplog"
tmjson "github.com/tendermint/tendermint/libs/json"
tmquery "github.com/tendermint/tendermint/libs/pubsub/query"
coretypes "github.com/tendermint/tendermint/rpc/core/types"
rpcclient "github.com/tendermint/tendermint/rpc/jsonrpc/client"
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"
"github.com/cosmos/ethermint/ethereum/rpc/pubsub"
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
tmWSClient *rpcclient.WSClient
// light client mode
lightMode bool
index filterIndex
topicChans map[string]chan<- coretypes.ResultEvent
indexMux *sync.RWMutex
// Channels
install chan *Subscription // install filter for event notification
uninstall chan *Subscription // remove filter for event notification
eventBus pubsub.EventBus
}
// 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(tmWSClient *rpcclient.WSClient) *EventSystem {
index := make(filterIndex)
for i := filters.UnknownSubscription; i < filters.LastIndexSubscription; i++ {
index[i] = make(map[rpc.ID]*Subscription)
}
es := &EventSystem{
ctx: context.Background(),
tmWSClient: tmWSClient,
lightMode: false,
index: index,
topicChans: make(map[string]chan<- coretypes.ResultEvent, len(index)),
indexMux: new(sync.RWMutex),
install: make(chan *Subscription),
uninstall: make(chan *Subscription),
eventBus: pubsub.NewEventBus(),
}
go es.eventLoop()
go es.consumeEvents()
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.
func (es *EventSystem) subscribe(sub *Subscription) (*Subscription, context.CancelFunc, error) {
var (
err error
cancelFn context.CancelFunc
)
es.ctx, cancelFn = context.WithCancel(context.Background())
existingSubs := es.eventBus.Topics()
for _, topic := range existingSubs {
if topic == sub.event {
eventCh, err := es.eventBus.Subscribe(sub.event)
if err != nil {
err := errors.Wrapf(err, "failed to subscribe to topic: %s", sub.event)
return nil, cancelFn, err
}
sub.eventCh = eventCh
return sub, cancelFn, nil
}
}
switch sub.typ {
case filters.LogsSubscription:
err = es.tmWSClient.Subscribe(es.ctx, sub.event)
case filters.BlocksSubscription:
err = es.tmWSClient.Subscribe(es.ctx, 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
es.install <- sub
<-sub.installed
eventCh, err := es.eventBus.Subscribe(sub.event)
if err != nil {
err := errors.Wrapf(err, "failed to subscribe to topic after installed: %s", sub.event)
return sub, cancelFn, err
}
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 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)
default:
return nil, nil, fmt.Errorf("invalid from and to block combination: from > to (%d > %d)", from, to)
}
}
// 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)
}
// 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
// eventLoop (un)installs filters and processes mux events.
func (es *EventSystem) eventLoop() {
for {
select {
case f := <-es.install:
es.indexMux.Lock()
es.index[f.typ][f.id] = f
ch := make(chan coretypes.ResultEvent)
es.topicChans[f.event] = ch
if err := es.eventBus.AddTopic(f.event, ch); err != nil {
log.WithField("topic", f.event).WithError(err).Errorln("failed to add event topic to event bus")
}
es.indexMux.Unlock()
close(f.installed)
case f := <-es.uninstall:
es.indexMux.Lock()
delete(es.index[f.typ], f.id)
var channelInUse bool
for _, sub := range es.index[f.typ] {
if sub.event == f.event {
channelInUse = true
break
}
}
// remove topic only when channel is not used by other subscriptions
if !channelInUse {
if err := es.tmWSClient.Unsubscribe(es.ctx, f.event); err != nil {
log.WithError(err).WithField("query", f.event).Errorln("failed to unsubscribe from query")
}
ch, ok := es.topicChans[f.event]
if ok {
es.eventBus.RemoveTopic(f.event)
close(ch)
delete(es.topicChans, f.event)
}
}
es.indexMux.Unlock()
close(f.err)
}
}
}
func (es *EventSystem) consumeEvents() {
for {
for rpcResp := range es.tmWSClient.ResponsesCh {
var ev coretypes.ResultEvent
if rpcResp.Error != nil {
time.Sleep(5 * time.Second)
continue
} else if err := tmjson.Unmarshal(rpcResp.Result, &ev); err != nil {
log.WithError(err).Warningln("failed to JSON unmarshal ResponsesCh result event")
continue
}
if len(ev.Query) == 0 {
// skip empty responses
continue
}
es.indexMux.RLock()
ch, ok := es.topicChans[ev.Query]
es.indexMux.RUnlock()
if !ok {
log.WithField("topic", ev.Query).Warningln("channel for subscription not found, lol")
log.Infoln("available channels:", es.eventBus.Topics())
continue
}
// gracefully handle lagging subscribers
t := time.NewTimer(time.Second)
select {
case <-t.C:
log.WithField("topic", ev.Query).Warningln("dropped event during lagging subscription")
case ch <- ev:
}
}
time.Sleep(time.Second)
}
}
// 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 from the current subscription to Tendermint Websocket. It sends an error to the
// subscription error channel if unsubscription fails.
func (s *Subscription) Unsubscribe(es *EventSystem) {
go func() {
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
}
+302
View File
@@ -0,0 +1,302 @@
package rpc
import (
"context"
"fmt"
"math/big"
"github.com/cosmos/ethermint/ethereum/rpc/types"
"github.com/pkg/errors"
log "github.com/xlab/suplog"
"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"
)
// Filter can be used to retrieve and filter logs.
type Filter struct {
backend FiltersBackend
criteria filters.FilterCriteria
matcher *bloombits.Matcher
}
// NewBlockFilter creates a new filter which directly inspects the contents of
// a block to figure out whether it is interesting or not.
func NewBlockFilter(backend FiltersBackend, criteria filters.FilterCriteria) *Filter {
// Create a generic filter and convert it into a block filter
return newFilter(backend, criteria, nil)
}
// NewRangeFilter creates a new filter which uses a bloom filter on blocks to
// figure out whether a particular block is interesting or not.
func NewRangeFilter(backend FiltersBackend, begin, end int64, addresses []common.Address, topics [][]common.Hash) *Filter {
// 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 FiltersBackend, criteria filters.FilterCriteria, matcher *bloombits.Matcher) *Filter {
return &Filter{
backend: backend,
criteria: criteria,
matcher: matcher,
}
}
const (
maxFilterBlocks = 100000
maxToOverhang = 600
)
// 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 {
err = errors.Wrap(err, "failed to fetch header by hash")
return nil, err
}
if header == nil {
err := errors.Errorf("unknown block header %s", f.criteria.BlockHash.String())
return nil, err
}
return f.blockLogs(header)
}
// Figure out the limits of the filter range
header, err := f.backend.HeaderByNumber(types.EthLatestBlockNumber)
if err != nil {
err = errors.Wrap(err, "failed to fetch header by number (latest)")
return nil, err
}
if header == nil || header.Number == nil {
log.Warningln("header not found or has no number")
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)
}
if f.criteria.ToBlock.Int64()-f.criteria.FromBlock.Int64() > maxFilterBlocks {
err := errors.Errorf("maximum [from, to] blocks distance: %d", maxFilterBlocks)
return nil, err
}
// check bounds
if f.criteria.FromBlock.Int64() > head {
return []*ethtypes.Log{}, nil
} else if f.criteria.ToBlock.Int64() > head+maxToOverhang {
f.criteria.ToBlock = big.NewInt(head + maxToOverhang)
}
for i := f.criteria.FromBlock.Int64(); i <= f.criteria.ToBlock.Int64(); i++ {
block, err := f.backend.GetBlockByNumber(types.BlockNumber(i), false)
if err != nil {
err = errors.Wrapf(err, "failed to fetch block by number %d", i)
return logs, err
} else if block["transactions"] == nil {
continue
}
var txHashes []common.Hash
txs, ok := block["transactions"].([]interface{})
if !ok {
txHashes, ok = block["transactions"].([]common.Hash)
if !ok {
log.WithField(
"transactions",
fmt.Sprintf("%T", block["transactions"]),
).Errorln("reading transactions from block data: bad field type")
continue
}
} else if len(txs) == 0 && len(txHashes) == 0 {
continue
}
for _, tx := range txs {
txHash, ok := tx.(common.Hash)
if !ok {
log.WithField(
"tx",
fmt.Sprintf("%T", tx),
).Errorln("transactions list contains non-hash element")
} else {
txHashes = append(txHashes, txHash)
}
}
logsMatched := f.checkMatches(txHashes)
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
}
// DANGER: do not call GetLogs(header.Hash())
// eth header's hash doesn't match tm block hash
logsList, err := f.backend.GetLogsByNumber(types.BlockNumber(header.Number.Int64()))
if err != nil {
err = errors.Wrapf(err, "failed to fetch logs block number %d", header.Number.Int64())
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)
}
// 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
}
+36
View File
@@ -0,0 +1,36 @@
package rpc
import (
"fmt"
"github.com/spf13/viper"
ethermint "github.com/cosmos/ethermint/types"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
)
// PublicNetAPI is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec.
type PublicNetAPI struct {
networkVersion uint64
}
// NewPublicNetAPI creates an instance of the public Net Web3 API.
func NewPublicNetAPI(_ client.Context) *PublicNetAPI {
chainID := viper.GetString(flags.FlagChainID)
// parse the chainID from a integer string
chainIDEpoch, err := ethermint.ParseChainID(chainID)
if err != nil {
panic(err)
}
return &PublicNetAPI{
networkVersion: chainIDEpoch.Uint64(),
}
}
// Version returns the current ethereum protocol version.
func (s *PublicNetAPI) Version() string {
return fmt.Sprintf("%d", s.networkVersion)
}
+128
View File
@@ -0,0 +1,128 @@
package pubsub
import (
"sync"
"github.com/pkg/errors"
coretypes "github.com/tendermint/tendermint/rpc/core/types"
)
type EventBus interface {
AddTopic(name string, src <-chan coretypes.ResultEvent) error
RemoveTopic(name string)
Subscribe(name string) (<-chan coretypes.ResultEvent, error)
Topics() []string
}
type memEventBus struct {
topics map[string]<-chan coretypes.ResultEvent
topicsMux *sync.RWMutex
subscribers map[string][]chan<- coretypes.ResultEvent
subscribersMux *sync.RWMutex
}
func NewEventBus() EventBus {
return &memEventBus{
topics: make(map[string]<-chan coretypes.ResultEvent),
topicsMux: new(sync.RWMutex),
subscribers: make(map[string][]chan<- coretypes.ResultEvent),
subscribersMux: new(sync.RWMutex),
}
}
func (m *memEventBus) Topics() (topics []string) {
m.topicsMux.RLock()
defer m.topicsMux.RUnlock()
topics = make([]string, 0, len(m.topics))
for topicName := range m.topics {
topics = append(topics, topicName)
}
return topics
}
func (m *memEventBus) AddTopic(name string, src <-chan coretypes.ResultEvent) error {
m.topicsMux.RLock()
_, ok := m.topics[name]
m.topicsMux.RUnlock()
if ok {
return errors.New("topic already registered")
}
m.topicsMux.Lock()
m.topics[name] = src
m.topicsMux.Unlock()
go m.publishTopic(name, src)
return nil
}
func (m *memEventBus) RemoveTopic(name string) {
m.topicsMux.Lock()
delete(m.topics, name)
m.topicsMux.Unlock()
}
func (m *memEventBus) Subscribe(name string) (<-chan coretypes.ResultEvent, error) {
m.topicsMux.RLock()
_, ok := m.topics[name]
m.topicsMux.RUnlock()
if !ok {
return nil, errors.Errorf("topic not found: %s", name)
}
ch := make(chan coretypes.ResultEvent)
m.subscribersMux.Lock()
defer m.subscribersMux.Unlock()
m.subscribers[name] = append(m.subscribers[name], ch)
return ch, nil
}
func (m *memEventBus) publishTopic(name string, src <-chan coretypes.ResultEvent) {
for {
select {
case msg, ok := <-src:
if !ok {
m.closeAllSubscribers(name)
m.topicsMux.Lock()
delete(m.topics, name)
m.topicsMux.Unlock()
return
}
m.publishAllSubscribers(name, msg)
}
}
}
func (m *memEventBus) closeAllSubscribers(name string) {
m.subscribersMux.Lock()
defer m.subscribersMux.Unlock()
subsribers := m.subscribers[name]
delete(m.subscribers, name)
for _, sub := range subsribers {
close(sub)
}
}
func (m *memEventBus) publishAllSubscribers(name string, msg coretypes.ResultEvent) {
m.subscribersMux.RLock()
subsribers := m.subscribers[name]
m.subscribersMux.RUnlock()
for _, sub := range subsribers {
select {
case sub <- msg:
default:
}
}
}
+96
View File
@@ -0,0 +1,96 @@
package pubsub
import (
"log"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestAddTopic(t *testing.T) {
assert := assert.New(t)
q := NewEventBus()
err := q.AddTopic("kek", make(chan interface{}))
if !assert.NoError(err) {
return
}
err = q.AddTopic("lol", make(chan interface{}))
if !assert.NoError(err) {
return
}
err = q.AddTopic("lol", make(chan interface{}))
if !assert.Error(err) {
return
}
assert.EqualValues([]string{"kek", "lol"}, q.Topics())
}
func TestSubscribe(t *testing.T) {
assert := assert.New(t)
q := NewEventBus()
kekSrc := make(chan interface{})
q.AddTopic("kek", kekSrc)
lolSrc := make(chan interface{})
q.AddTopic("lol", lolSrc)
kekSubC, err := q.Subscribe("kek")
if !assert.NoError(err) {
return
}
lolSubC, err := q.Subscribe("lol")
if !assert.NoError(err) {
return
}
lol2SubC, err := q.Subscribe("lol")
if !assert.NoError(err) {
return
}
wg := new(sync.WaitGroup)
wg.Add(4)
go func() {
defer wg.Done()
msg := <-kekSubC
log.Println("kek:", msg)
assert.EqualValues(1, msg)
}()
go func() {
defer wg.Done()
msg := <-lolSubC
log.Println("lol:", msg)
assert.EqualValues(1, msg)
}()
go func() {
defer wg.Done()
msg := <-lol2SubC
log.Println("lol2:", msg)
assert.EqualValues(1, msg)
}()
go func() {
defer wg.Done()
time.Sleep(time.Second)
kekSrc <- 1
lolSrc <- 1
close(kekSrc)
close(lolSrc)
}()
wg.Wait()
time.Sleep(time.Second)
}
+38
View File
@@ -0,0 +1,38 @@
package types
import (
"sync"
"github.com/ethereum/go-ethereum/common"
)
// AddrLocker is a mutex structure used to avoid querying outdated account data
type AddrLocker struct {
mu sync.Mutex
locks map[common.Address]*sync.Mutex
}
// lock returns the lock of the given address.
func (l *AddrLocker) lock(address common.Address) *sync.Mutex {
l.mu.Lock()
defer l.mu.Unlock()
if l.locks == nil {
l.locks = make(map[common.Address]*sync.Mutex)
}
if _, ok := l.locks[address]; !ok {
l.locks[address] = new(sync.Mutex)
}
return l.locks[address]
}
// LockAddr locks an account's mutex. This is used to prevent another tx getting the
// same nonce until the lock is released. The mutex prevents the (an identical nonce) from
// being read again during the time that the first transaction is being signed.
func (l *AddrLocker) LockAddr(address common.Address) {
l.lock(address).Lock()
}
// UnlockAddr unlocks the mutex of the given account.
func (l *AddrLocker) UnlockAddr(address common.Address) {
l.lock(address).Unlock()
}
+104
View File
@@ -0,0 +1,104 @@
package types
import (
"context"
"fmt"
"math"
"math/big"
"strings"
grpctypes "github.com/cosmos/cosmos-sdk/types/grpc"
"github.com/spf13/cast"
"google.golang.org/grpc/metadata"
"github.com/ethereum/go-ethereum/common/hexutil"
)
// BlockNumber represents decoding hex string to block values
type BlockNumber int64
const (
EthPendingBlockNumber = BlockNumber(-2)
EthLatestBlockNumber = BlockNumber(-1)
EthEarliestBlockNumber = BlockNumber(0)
)
// NewBlockNumber creates a new BlockNumber instance.
func NewBlockNumber(n *big.Int) BlockNumber {
return BlockNumber(n.Int64())
}
// ContextWithHeight wraps a context with the a gRPC block height header. If the provided height is
// 0, it will return an empty context and the gRPC query will use the latest block height for querying.
func ContextWithHeight(height int64) context.Context {
if height == 0 {
return context.Background()
}
return metadata.AppendToOutgoingContext(context.Background(), grpctypes.GRPCBlockHeightHeader, fmt.Sprintf("%d", height))
}
// UnmarshalJSON parses the given JSON fragment into a BlockNumber. It supports:
// - "latest", "earliest" or "pending" as string arguments
// - the block number
// Returned errors:
// - an invalid block number error when the given argument isn't a known strings
// - an out of range error when the given block number is either too little or too large
func (bn *BlockNumber) UnmarshalJSON(data []byte) error {
input := strings.TrimSpace(string(data))
if len(input) >= 2 && input[0] == '"' && input[len(input)-1] == '"' {
input = input[1 : len(input)-1]
}
switch input {
case "earliest":
*bn = EthEarliestBlockNumber
return nil
case "latest":
*bn = EthLatestBlockNumber
return nil
case "pending":
*bn = EthPendingBlockNumber
return nil
}
blckNum, err := hexutil.DecodeUint64(input)
if err == hexutil.ErrMissingPrefix {
blckNum = cast.ToUint64(input)
} else if err != nil {
return err
}
if blckNum > math.MaxInt64 {
return fmt.Errorf("block number larger than int64")
}
*bn = BlockNumber(blckNum)
return nil
}
// Int64 converts block number to primitive type
func (bn BlockNumber) Int64() int64 {
if bn < 0 {
return 0
} else if bn == 0 {
return 1
}
return int64(bn)
}
// TmHeight is a util function used for the Tendermint RPC client. It returns
// nil if the block number is "latest". Otherwise, it returns the pointer of the
// int64 value of the height.
func (bn BlockNumber) TmHeight() *int64 {
if bn < 0 {
return nil
} else if bn == EthEarliestBlockNumber {
var firstHeight int64 = 0
return &firstHeight
}
height := bn.Int64()
return &height
}
+62
View File
@@ -0,0 +1,62 @@
package types
import (
"fmt"
"github.com/cosmos/cosmos-sdk/types/tx"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/proto/tendermint/crypto"
"github.com/cosmos/cosmos-sdk/client"
evmtypes "github.com/cosmos/ethermint/x/evm/types"
)
// QueryClient defines a gRPC Client used for:
// - Transaction simulation
// - EVM module queries
type QueryClient struct {
tx.ServiceClient
evmtypes.QueryClient
}
// NewQueryClient creates a new gRPC query client
func NewQueryClient(clientCtx client.Context) *QueryClient { // nolint: interfacer
return &QueryClient{
ServiceClient: tx.NewServiceClient(clientCtx),
QueryClient: evmtypes.NewQueryClient(clientCtx),
}
}
// GetProof performs an ABCI query with the given key and returns a merkle proof. The desired
// tendermint height to perform the query should be set in the client context. The query will be
// performed at one below this height (at the IAVL version) in order to obtain the correct merkle
// proof. Proof queries at height less than or equal to 2 are not supported.
// Issue: https://github.com/cosmos/cosmos-sdk/issues/6567
func (QueryClient) GetProof(clientCtx client.Context, storeKey string, key []byte) ([]byte, *crypto.ProofOps, error) {
height := clientCtx.Height
// ABCI queries at height less than or equal to 2 are not supported.
// Base app does not support queries for height less than or equal to 1.
// Therefore, a query at height 2 would be equivalent to a query at height 3
if height <= 2 {
return nil, nil, fmt.Errorf("proof queries at height <= 2 are not supported")
}
// Use the IAVL height if a valid tendermint height is passed in.
height--
abciReq := abci.RequestQuery{
Path: fmt.Sprintf("store/%s/key", storeKey),
Data: key,
Height: height,
Prove: true,
}
abciRes, err := clientCtx.QueryABCI(abciReq)
if err != nil {
return nil, nil, err
}
return abciRes.Value, abciRes.ProofOps, nil
}
+85
View File
@@ -0,0 +1,85 @@
package types
import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
)
// Copied the Account and StorageResult types since they are registered under an
// internal pkg on geth.
// AccountResult struct for account proof
type AccountResult struct {
Address common.Address `json:"address"`
AccountProof []string `json:"accountProof"`
Balance *hexutil.Big `json:"balance"`
CodeHash common.Hash `json:"codeHash"`
Nonce hexutil.Uint64 `json:"nonce"`
StorageHash common.Hash `json:"storageHash"`
StorageProof []StorageResult `json:"storageProof"`
}
// StorageResult defines the format for storage proof return
type StorageResult struct {
Key string `json:"key"`
Value *hexutil.Big `json:"value"`
Proof []string `json:"proof"`
}
// Transaction represents a transaction returned to RPC clients.
type Transaction struct {
BlockHash *common.Hash `json:"blockHash"`
BlockNumber *hexutil.Big `json:"blockNumber"`
From common.Address `json:"from"`
Gas hexutil.Uint64 `json:"gas"`
GasPrice *hexutil.Big `json:"gasPrice"`
Hash common.Hash `json:"hash"`
Input hexutil.Bytes `json:"input"`
Nonce hexutil.Uint64 `json:"nonce"`
To *common.Address `json:"to"`
TransactionIndex *hexutil.Uint64 `json:"transactionIndex"`
Value *hexutil.Big `json:"value"`
V *hexutil.Big `json:"v"`
R *hexutil.Big `json:"r"`
S *hexutil.Big `json:"s"`
}
// SendTxArgs represents the arguments to submit a new transaction into the transaction pool.
// Duplicate struct definition since geth struct is in internal package
// Ref: https://github.com/ethereum/go-ethereum/blob/release/1.9/internal/ethapi/api.go#L1346
type SendTxArgs struct {
From common.Address `json:"from"`
To *common.Address `json:"to"`
Gas *hexutil.Uint64 `json:"gas"`
GasPrice *hexutil.Big `json:"gasPrice"`
Value *hexutil.Big `json:"value"`
Nonce *hexutil.Uint64 `json:"nonce"`
// We accept "data" and "input" for backwards-compatibility reasons. "input" is the
// newer name and should be preferred by clients.
Data *hexutil.Bytes `json:"data"`
Input *hexutil.Bytes `json:"input"`
}
// CallArgs represents the arguments for a call.
type CallArgs struct {
From *common.Address `json:"from"`
To *common.Address `json:"to"`
Gas *hexutil.Uint64 `json:"gas"`
GasPrice *hexutil.Big `json:"gasPrice"`
Value *hexutil.Big `json:"value"`
Data *hexutil.Bytes `json:"data"`
}
// account indicates the overriding fields of account during the execution of
// a message call.
// Note, state and stateDiff can't be specified at the same time. If state is
// set, message execution will only use the data in the given state. Otherwise
// if statDiff is set, all diff will be applied first and then execute the call
// message.
type Account struct {
Nonce *hexutil.Uint64 `json:"nonce"`
Code *hexutil.Bytes `json:"code"`
Balance **hexutil.Big `json:"balance"`
State *map[common.Hash]common.Hash `json:"state"`
StateDiff *map[common.Hash]common.Hash `json:"stateDiff"`
}
+268
View File
@@ -0,0 +1,268 @@
package types
import (
"bytes"
"context"
"fmt"
"math/big"
tmbytes "github.com/tendermint/tendermint/libs/bytes"
tmtypes "github.com/tendermint/tendermint/types"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/tx"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing"
"github.com/cosmos/ethermint/crypto/ethsecp256k1"
ethermint "github.com/cosmos/ethermint/types"
evmtypes "github.com/cosmos/ethermint/x/evm/types"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
ethtypes "github.com/ethereum/go-ethereum/core/types"
)
// RawTxToEthTx returns a evm MsgEthereum transaction from raw tx bytes.
func RawTxToEthTx(clientCtx client.Context, bz []byte) (*evmtypes.MsgEthereumTx, error) {
tx, err := clientCtx.TxConfig.TxDecoder()(bz)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
}
ethTx, ok := tx.(*evmtypes.MsgEthereumTx)
if !ok {
return nil, fmt.Errorf("invalid transaction type %T, expected %T", tx, evmtypes.MsgEthereumTx{})
}
return ethTx, nil
}
// NewTransaction returns a transaction that will serialize to the RPC
// representation, with the given location metadata set (if available).
func NewTransaction(tx *evmtypes.MsgEthereumTx, txHash, blockHash common.Hash, blockNumber, index uint64) (*Transaction, error) {
// Verify signature and retrieve sender address
from, err := tx.VerifySig(tx.ChainID())
if err != nil {
return nil, err
}
gasPrice := new(big.Int).SetBytes(tx.Data.Price)
value := new(big.Int).SetBytes(tx.Data.Amount)
rpcTx := &Transaction{
From: from,
Gas: hexutil.Uint64(tx.Data.GasLimit),
GasPrice: (*hexutil.Big)(gasPrice),
//GasPrice: (*hexutil.Big)(tx.Data.Price.BigInt()),
Hash: txHash,
Input: hexutil.Bytes(tx.Data.Payload),
Nonce: hexutil.Uint64(tx.Data.AccountNonce),
To: tx.To(),
Value: (*hexutil.Big)(value),
//Value: (*hexutil.Big)(tx.Data.Amount.BigInt()),
V: (*hexutil.Big)(new(big.Int).SetBytes(tx.Data.V)),
R: (*hexutil.Big)(new(big.Int).SetBytes(tx.Data.R)),
S: (*hexutil.Big)(new(big.Int).SetBytes(tx.Data.S)),
}
if blockHash != (common.Hash{}) {
rpcTx.BlockHash = &blockHash
rpcTx.BlockNumber = (*hexutil.Big)(new(big.Int).SetUint64(blockNumber))
rpcTx.TransactionIndex = (*hexutil.Uint64)(&index)
}
return rpcTx, nil
}
// EthBlockFromTendermint returns a JSON-RPC compatible Ethereum blockfrom a given Tendermint block.
func EthBlockFromTendermint(clientCtx client.Context, queryClient *QueryClient, block *tmtypes.Block) (map[string]interface{}, error) {
gasLimit, err := BlockMaxGasFromConsensusParams(context.Background(), clientCtx)
if err != nil {
return nil, err
}
transactions, gasUsed, err := EthTransactionsFromTendermint(clientCtx, block.Txs)
if err != nil {
return nil, err
}
req := &evmtypes.QueryBlockBloomRequest{}
res, err := queryClient.BlockBloom(ContextWithHeight(block.Height), req)
if err != nil {
return nil, err
}
bloom := ethtypes.BytesToBloom(res.Bloom)
return FormatBlock(block.Header, block.Size(), gasLimit, gasUsed, transactions, bloom), nil
}
// EthHeaderFromTendermint is an util function that returns an Ethereum Header
// from a tendermint Header.
func EthHeaderFromTendermint(header tmtypes.Header) *ethtypes.Header {
return &ethtypes.Header{
ParentHash: common.BytesToHash(header.LastBlockID.Hash.Bytes()),
UncleHash: common.Hash{},
Coinbase: common.Address{},
Root: common.BytesToHash(header.AppHash),
TxHash: common.BytesToHash(header.DataHash),
ReceiptHash: common.Hash{},
Difficulty: nil,
Number: big.NewInt(header.Height),
Time: uint64(header.Time.Unix()),
Extra: nil,
MixDigest: common.Hash{},
Nonce: ethtypes.BlockNonce{},
}
}
// EthTransactionsFromTendermint returns a slice of ethereum transaction hashes and the total gas usage from a set of
// tendermint block transactions.
func EthTransactionsFromTendermint(clientCtx client.Context, txs []tmtypes.Tx) ([]common.Hash, *big.Int, error) {
transactionHashes := []common.Hash{}
gasUsed := big.NewInt(0)
for _, tx := range txs {
ethTx, err := RawTxToEthTx(clientCtx, tx)
if err != nil {
// continue to next transaction in case it's not a MsgEthereumTx
continue
}
// TODO: Remove gas usage calculation if saving gasUsed per block
gasUsed.Add(gasUsed, ethTx.Fee())
transactionHashes = append(transactionHashes, common.BytesToHash(tx.Hash()))
}
return transactionHashes, gasUsed, nil
}
// BlockMaxGasFromConsensusParams returns the gas limit for the latest block from the chain consensus params.
func BlockMaxGasFromConsensusParams(ctx context.Context, clientCtx client.Context) (int64, error) {
resConsParams, err := clientCtx.Client.ConsensusParams(ctx, nil)
if err != nil {
return 0, err
}
gasLimit := resConsParams.ConsensusParams.Block.MaxGas
if gasLimit == -1 {
// Sets gas limit to max uint32 to not error with javascript dev tooling
// This -1 value indicating no block gas limit is set to max uint64 with geth hexutils
// which errors certain javascript dev tooling which only supports up to 53 bits
gasLimit = int64(^uint32(0))
}
return gasLimit, nil
}
// FormatBlock creates an ethereum block from a tendermint header and ethereum-formatted
// transactions.
func FormatBlock(
header tmtypes.Header, size int, gasLimit int64,
gasUsed *big.Int, transactions interface{}, bloom ethtypes.Bloom,
) map[string]interface{} {
if len(header.DataHash) == 0 {
header.DataHash = tmbytes.HexBytes(common.Hash{}.Bytes())
}
return map[string]interface{}{
"number": hexutil.Uint64(header.Height),
"hash": hexutil.Bytes(header.Hash()),
"parentHash": hexutil.Bytes(header.LastBlockID.Hash),
"nonce": hexutil.Uint64(0), // PoW specific
"sha3Uncles": common.Hash{}, // No uncles in Tendermint
"logsBloom": bloom,
"transactionsRoot": hexutil.Bytes(header.DataHash),
"stateRoot": hexutil.Bytes(header.AppHash),
"miner": common.Address{},
"mixHash": common.Hash{},
"difficulty": 0,
"totalDifficulty": 0,
"extraData": hexutil.Uint64(0),
"size": hexutil.Uint64(size),
"gasLimit": hexutil.Uint64(gasLimit), // Static gas limit
"gasUsed": (*hexutil.Big)(gasUsed),
"timestamp": hexutil.Uint64(header.Time.Unix()),
"transactions": transactions.([]common.Hash),
"uncles": []string{},
"receiptsRoot": common.Hash{},
}
}
// GetKeyByAddress returns the private key matching the given address. If not found it returns false.
func GetKeyByAddress(keys []ethsecp256k1.PrivKey, address common.Address) (key *ethsecp256k1.PrivKey, exist bool) {
for _, key := range keys {
if bytes.Equal(key.PubKey().Address().Bytes(), address.Bytes()) {
return &key, true
}
}
return nil, false
}
// BuildEthereumTx builds and signs a Cosmos transaction from a MsgEthereumTx and returns the tx
func BuildEthereumTx(clientCtx client.Context, msg *evmtypes.MsgEthereumTx, accNumber, seq uint64, privKey cryptotypes.PrivKey) ([]byte, error) {
// TODO: user defined evm coin
fees := sdk.NewCoins(ethermint.NewPhotonCoin(sdk.NewIntFromBigInt(msg.Fee())))
signMode := clientCtx.TxConfig.SignModeHandler().DefaultMode()
signerData := authsigning.SignerData{
ChainID: clientCtx.ChainID,
AccountNumber: accNumber,
Sequence: seq,
}
// Create a TxBuilder
txBuilder := clientCtx.TxConfig.NewTxBuilder()
if err := txBuilder.SetMsgs(msg); err != nil {
return nil, err
}
txBuilder.SetFeeAmount(fees)
txBuilder.SetGasLimit(msg.GetGas())
// sign with the private key
sigV2, err := tx.SignWithPrivKey(
signMode, signerData,
txBuilder, privKey, clientCtx.TxConfig, seq,
)
if err != nil {
return nil, err
}
if err := txBuilder.SetSignatures(sigV2); err != nil {
return nil, err
}
txBytes, err := clientCtx.TxConfig.TxEncoder()(txBuilder.GetTx())
if err != nil {
return nil, err
}
return txBytes, nil
}
// GetBlockCumulativeGas returns the cumulative gas used on a block up to a given
// transaction index. The returned gas used includes the gas from both the SDK and
// EVM module transactions.
func GetBlockCumulativeGas(clientCtx client.Context, block *tmtypes.Block, idx int) uint64 {
var gasUsed uint64
txDecoder := clientCtx.TxConfig.TxDecoder()
for i := 0; i < idx && i < len(block.Txs); i++ {
txi, err := txDecoder(block.Txs[i])
if err != nil {
continue
}
switch tx := txi.(type) {
case *evmtypes.MsgEthereumTx:
gasUsed += tx.GetGas()
case sdk.FeeTx:
// TODO: add internal txns
gasUsed += tx.GetGas()
}
}
return gasUsed
}
+242
View File
@@ -0,0 +1,242 @@
package rpc
import (
"context"
"encoding/hex"
"fmt"
"math/big"
"github.com/xlab/suplog"
tmbytes "github.com/tendermint/tendermint/libs/bytes"
tmtypes "github.com/tendermint/tendermint/types"
"github.com/cosmos/cosmos-sdk/client"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
ethtypes "github.com/ethereum/go-ethereum/core/types"
rpctypes "github.com/cosmos/ethermint/ethereum/rpc/types"
ethermint "github.com/cosmos/ethermint/types"
evmtypes "github.com/cosmos/ethermint/x/evm/types"
)
type DataError interface {
Error() string // returns the message
ErrorData() interface{} // returns the error data
}
type dataError struct {
msg string
data string
}
func (d *dataError) Error() string {
return d.msg
}
func (d *dataError) ErrorData() interface{} {
return d.data
}
type sdkTxLogs struct {
Log string `json:"log"`
}
const logRevertedFlag = "transaction reverted"
func errRevertedWith(data []byte) DataError {
return &dataError{
msg: "VM execution error.",
data: fmt.Sprintf("0x%s", hex.EncodeToString(data)),
}
}
// NewTransaction returns a transaction that will serialize to the RPC
// representation, with the given location metadata set (if available).
func NewTransaction(tx *evmtypes.MsgEthereumTx, txHash, blockHash common.Hash, blockNumber, index uint64) (*rpctypes.Transaction, error) {
// Verify signature and retrieve sender address
from, err := tx.VerifySig(tx.ChainID())
if err != nil {
return nil, err
}
rpcTx := &rpctypes.Transaction{
From: from,
Gas: hexutil.Uint64(tx.Data.GasLimit),
GasPrice: (*hexutil.Big)(new(big.Int).SetBytes(tx.Data.Price)),
Hash: txHash,
Input: hexutil.Bytes(tx.Data.Payload),
Nonce: hexutil.Uint64(tx.Data.AccountNonce),
To: tx.To(),
Value: (*hexutil.Big)(new(big.Int).SetBytes(tx.Data.Amount)),
V: (*hexutil.Big)(new(big.Int).SetBytes(tx.Data.V)),
R: (*hexutil.Big)(new(big.Int).SetBytes(tx.Data.R)),
S: (*hexutil.Big)(new(big.Int).SetBytes(tx.Data.S)),
}
if rpcTx.To == nil {
addr := common.HexToAddress("0x0000000000000000000000000000000000000000")
rpcTx.To = &addr
}
if blockHash != (common.Hash{}) {
rpcTx.BlockHash = &blockHash
rpcTx.BlockNumber = (*hexutil.Big)(new(big.Int).SetUint64(blockNumber))
rpcTx.TransactionIndex = (*hexutil.Uint64)(&index)
}
return rpcTx, nil
}
// NewTransaction returns a transaction that will serialize to the RPC
// representation, with the given location metadata set (if available).
func NewTransactionFromData(
txData *evmtypes.TxData,
from common.Address,
txHash, blockHash common.Hash,
blockNumber, index uint64,
) (*rpctypes.Transaction, error) {
var to *common.Address
if len(txData.Recipient) > 0 {
recipient := common.BytesToAddress(txData.Recipient)
to = &recipient
}
rpcTx := &rpctypes.Transaction{
From: from,
Gas: hexutil.Uint64(txData.GasLimit),
GasPrice: (*hexutil.Big)(new(big.Int).SetBytes(txData.Price)),
Hash: txHash,
Input: hexutil.Bytes(txData.Payload),
Nonce: hexutil.Uint64(txData.AccountNonce),
To: to,
Value: (*hexutil.Big)(new(big.Int).SetBytes(txData.Amount)),
V: (*hexutil.Big)(new(big.Int).SetBytes(txData.V)),
R: (*hexutil.Big)(new(big.Int).SetBytes(txData.R)),
S: (*hexutil.Big)(new(big.Int).SetBytes(txData.S)),
}
if rpcTx.To == nil {
addr := common.HexToAddress("0x0000000000000000000000000000000000000000")
rpcTx.To = &addr
}
if blockHash != (common.Hash{}) {
rpcTx.BlockHash = &blockHash
rpcTx.BlockNumber = (*hexutil.Big)(new(big.Int).SetUint64(blockNumber))
rpcTx.TransactionIndex = (*hexutil.Uint64)(&index)
}
return rpcTx, nil
}
// EthHeaderFromTendermint is an util function that returns an Ethereum Header
// from a tendermint Header.
func EthHeaderFromTendermint(header tmtypes.Header) *ethtypes.Header {
return &ethtypes.Header{
ParentHash: common.BytesToHash(header.LastBlockID.Hash.Bytes()),
UncleHash: common.Hash{},
Coinbase: common.Address{},
Root: common.BytesToHash(header.AppHash),
TxHash: common.BytesToHash(header.DataHash),
ReceiptHash: common.Hash{},
Difficulty: nil,
Number: big.NewInt(header.Height),
Time: uint64(header.Time.Unix()),
Extra: nil,
MixDigest: common.Hash{},
Nonce: ethtypes.BlockNonce{},
}
}
// BlockMaxGasFromConsensusParams returns the gas limit for the latest block from the chain consensus params.
func BlockMaxGasFromConsensusParams(ctx context.Context, clientCtx client.Context, height *int64, logger suplog.Logger) (int64, error) {
return ethermint.DefaultRPCGasLimit, nil
}
var zeroHash = hexutil.Bytes(make([]byte, 32))
func hashOrZero(data []byte) hexutil.Bytes {
if len(data) == 0 {
return zeroHash
}
return hexutil.Bytes(data)
}
func bigOrZero(i *big.Int) *hexutil.Big {
if i == nil {
return new(hexutil.Big)
}
return (*hexutil.Big)(i)
}
func formatBlock(
header tmtypes.Header, size int, gasLimit int64,
gasUsed *big.Int, transactions interface{}, bloom ethtypes.Bloom,
) map[string]interface{} {
if len(header.DataHash) == 0 {
header.DataHash = tmbytes.HexBytes(common.Hash{}.Bytes())
}
var txRoot interface{}
txDescriptors, ok := transactions.([]interface{})
if !ok || len(txDescriptors) == 0 {
txRoot = ethtypes.EmptyRootHash
transactions = []common.Hash{}
} else {
txRoot = hashOrZero(header.DataHash)
}
ret := map[string]interface{}{
"parentHash": hashOrZero(header.LastBlockID.Hash),
"sha3Uncles": ethtypes.EmptyUncleHash, // No uncles in Tendermint
"miner": common.Address{},
"stateRoot": hashOrZero(header.AppHash),
"transactionsRoot": txRoot,
"receiptsRoot": zeroHash,
"logsBloom": hexutil.Encode(bloom.Bytes()),
"difficulty": new(hexutil.Big),
"number": hexutil.Uint64(header.Height),
"gasLimit": hexutil.Uint64(gasLimit), // Static gas limit
"gasUsed": bigOrZero(gasUsed),
"timestamp": hexutil.Uint64(header.Time.Unix()),
"extraData": hexutil.Bytes([]byte{}),
"mixHash": zeroHash,
"hash": hashOrZero(header.Hash()),
"nonce": ethtypes.EncodeNonce(0),
"totalDifficulty": new(hexutil.Big),
"size": hexutil.Uint64(size),
"transactions": transactions,
"uncles": []string{},
}
return ret
}
// GetBlockCumulativeGas returns the cumulative gas used on a block up to a given
// transaction index. The returned gas used includes the gas from both the SDK and
// EVM module transactions.
func GetBlockCumulativeGas(clientCtx client.Context, block *tmtypes.Block, idx int) uint64 {
var gasUsed uint64
txDecoder := clientCtx.TxConfig.TxDecoder()
for i := 0; i < idx && i < len(block.Txs); i++ {
txi, err := txDecoder(block.Txs[i])
if err != nil {
continue
}
switch tx := txi.(type) {
case *evmtypes.MsgEthereumTx:
gasUsed += tx.GetGas()
case sdk.FeeTx:
gasUsed += tx.GetGas()
}
}
return gasUsed
}
+26
View File
@@ -0,0 +1,26 @@
package rpc
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{}
// NewPublicWeb3API creates an instance of the Web3 API.
func NewPublicWeb3API() *PublicWeb3API {
return &PublicWeb3API{}
}
// ClientVersion returns the client version in the Web3 user agent format.
func (a *PublicWeb3API) ClientVersion() string {
return version.ClientVersion()
}
// Sha3 returns the keccak-256 hash of the passed-in input.
func (a *PublicWeb3API) Sha3(input hexutil.Bytes) hexutil.Bytes {
return crypto.Keccak256(input)
}
+747
View File
@@ -0,0 +1,747 @@
package rpc
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"math/big"
"net/http"
"sync"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
"github.com/pkg/errors"
log "github.com/xlab/suplog"
coretypes "github.com/tendermint/tendermint/rpc/core/types"
rpcclient "github.com/tendermint/tendermint/rpc/jsonrpc/client"
tmtypes "github.com/tendermint/tendermint/types"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/eth/filters"
"github.com/ethereum/go-ethereum/rpc"
evmtypes "github.com/cosmos/ethermint/x/evm/types"
)
type WebsocketsServer interface {
Start()
}
type SubscriptionResponseJSON struct {
Jsonrpc string `json:"jsonrpc"`
Result interface{} `json:"result"`
ID float64 `json:"id"`
}
type SubscriptionNotification struct {
Jsonrpc string `json:"jsonrpc"`
Method string `json:"method"`
Params *SubscriptionResult `json:"params"`
}
type SubscriptionResult struct {
Subscription rpc.ID `json:"subscription"`
Result interface{} `json:"result"`
}
type ErrorResponseJSON struct {
Jsonrpc string `json:"jsonrpc"`
Error *ErrorMessageJSON `json:"error"`
ID *big.Int `json:"id"`
}
type ErrorMessageJSON struct {
Code *big.Int `json:"code"`
Message string `json:"message"`
}
type websocketsServer struct {
rpcAddr string // listen address of rest-server
wsAddr string // listen address of ws server
api *pubSubAPI
logger log.Logger
}
func NewWebsocketsServer(tmWSClient *rpcclient.WSClient, rpcAddr, wsAddr string) *websocketsServer {
return &websocketsServer{
rpcAddr: rpcAddr,
wsAddr: wsAddr,
api: newPubSubAPI(tmWSClient),
logger: log.WithField("module", "websocket-server"),
}
}
func (s *websocketsServer) Start() {
ws := mux.NewRouter()
ws.Handle("/", s)
go func() {
err := http.ListenAndServe(s.wsAddr, ws)
if err != nil {
if err == http.ErrServerClosed {
return
}
s.logger.WithError(err).Errorln("failed to start HTTP server for WS")
}
}()
}
func (s *websocketsServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
s.logger.WithError(err).Warningln("websocket upgrade failed")
return
}
s.readLoop(&wsConn{
mux: new(sync.Mutex),
conn: conn,
})
}
func (s *websocketsServer) sendErrResponse(wsConn *wsConn, msg string) {
res := &ErrorResponseJSON{
Jsonrpc: "2.0",
Error: &ErrorMessageJSON{
Code: big.NewInt(-32600),
Message: msg,
},
ID: nil,
}
_ = wsConn.WriteJSON(res)
}
type wsConn struct {
conn *websocket.Conn
mux *sync.Mutex
}
func (w *wsConn) WriteJSON(v interface{}) error {
w.mux.Lock()
defer w.mux.Unlock()
return w.conn.WriteJSON(v)
}
func (w *wsConn) Close() error {
w.mux.Lock()
defer w.mux.Unlock()
return w.conn.Close()
}
func (w *wsConn) ReadMessage() (messageType int, p []byte, err error) {
// not protected by write mutex
return w.conn.ReadMessage()
}
func (s *websocketsServer) readLoop(wsConn *wsConn) {
for {
_, mb, err := wsConn.ReadMessage()
if err != nil {
_ = wsConn.Close()
return
}
var msg map[string]interface{}
err = json.Unmarshal(mb, &msg)
if err != nil {
s.sendErrResponse(wsConn, "invalid request")
continue
}
// check if method == eth_subscribe or eth_unsubscribe
method, ok := msg["method"].(string)
if !ok {
// otherwise, call the usual rpc server to respond
err = s.tcpGetAndSendResponse(wsConn, mb)
if err != nil {
s.sendErrResponse(wsConn, err.Error())
}
continue
}
connId := msg["id"].(float64)
if method == "eth_subscribe" {
params := msg["params"].([]interface{})
if len(params) == 0 {
s.sendErrResponse(wsConn, "invalid parameters")
continue
}
id, err := s.api.subscribe(wsConn, params)
if err != nil {
s.sendErrResponse(wsConn, err.Error())
continue
}
res := &SubscriptionResponseJSON{
Jsonrpc: "2.0",
ID: connId,
Result: id,
}
err = wsConn.WriteJSON(res)
if err != nil {
continue
}
continue
} else if method == "eth_unsubscribe" {
ids, ok := msg["params"].([]interface{})
if _, idok := ids[0].(string); !ok || !idok {
s.sendErrResponse(wsConn, "invalid parameters")
continue
}
ok = s.api.unsubscribe(rpc.ID(ids[0].(string)))
res := &SubscriptionResponseJSON{
Jsonrpc: "2.0",
ID: connId,
Result: ok,
}
err = wsConn.WriteJSON(res)
if err != nil {
continue
}
continue
}
// otherwise, call the usual rpc server to respond
err = s.tcpGetAndSendResponse(wsConn, mb)
if err != nil {
s.sendErrResponse(wsConn, err.Error())
}
}
}
// tcpGetAndSendResponse connects to the rest-server over tcp, posts a JSON-RPC request, and sends the response
// to the client over websockets
func (s *websocketsServer) tcpGetAndSendResponse(wsConn *wsConn, mb []byte) error {
req, err := http.NewRequest("POST", "http://"+s.rpcAddr, bytes.NewBuffer(mb))
if err != nil {
return errors.Wrap(err, "Could not build request")
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return errors.Wrap(err, "Could not perform request")
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return errors.Wrap(err, "could not read body from response")
}
var wsSend interface{}
err = json.Unmarshal(body, &wsSend)
if err != nil {
return errors.Wrap(err, "failed to unmarshal rest-server response")
}
return wsConn.WriteJSON(wsSend)
}
type wsSubscription struct {
sub *Subscription
unsubscribed chan struct{} // closed when unsubscribing
wsConn *wsConn
query string
}
// pubSubAPI is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec
type pubSubAPI struct {
events *EventSystem
filtersMu *sync.RWMutex
filters map[rpc.ID]*wsSubscription
logger log.Logger
}
// newPubSubAPI creates an instance of the ethereum PubSub API.
func newPubSubAPI(tmWSClient *rpcclient.WSClient) *pubSubAPI {
return &pubSubAPI{
events: NewEventSystem(tmWSClient),
filtersMu: new(sync.RWMutex),
filters: make(map[rpc.ID]*wsSubscription),
logger: log.WithField("module", "websocket-client"),
}
}
func (api *pubSubAPI) subscribe(wsConn *wsConn, params []interface{}) (rpc.ID, error) {
method, ok := params[0].(string)
if !ok {
return "0", errors.New("invalid parameters")
}
switch method {
case "newHeads":
// TODO: handle extra params
return api.subscribeNewHeads(wsConn)
case "logs":
if len(params) > 1 {
return api.subscribeLogs(wsConn, params[1])
}
return api.subscribeLogs(wsConn, nil)
case "newPendingTransactions":
return api.subscribePendingTransactions(wsConn)
case "syncing":
return api.subscribeSyncing(wsConn)
default:
return "0", errors.Errorf("unsupported method %s", method)
}
}
func (api *pubSubAPI) unsubscribe(id rpc.ID) bool {
api.filtersMu.Lock()
defer api.filtersMu.Unlock()
wsSub, ok := api.filters[id]
if !ok {
return false
}
wsSub.sub.Unsubscribe(api.events)
close(api.filters[id].unsubscribed)
delete(api.filters, id)
return true
}
func (api *pubSubAPI) getSubscriptionByQuery(query string) *Subscription {
api.filtersMu.Lock()
defer api.filtersMu.Unlock()
for _, wsSub := range api.filters {
if wsSub.query == query {
return wsSub.sub
}
}
return nil
}
func (api *pubSubAPI) subscribeNewHeads(wsConn *wsConn) (rpc.ID, error) {
var query = "subscribeNewHeads"
var subID = rpc.NewID()
sub, _, err := api.events.SubscribeNewHeads()
if err != nil {
return "", errors.Wrap(err, "error creating block filter")
}
unsubscribed := make(chan struct{})
api.filtersMu.Lock()
api.filters[subID] = &wsSubscription{
sub: sub,
wsConn: wsConn,
unsubscribed: unsubscribed,
query: query,
}
api.filtersMu.Unlock()
go func(headersCh <-chan coretypes.ResultEvent, errCh <-chan error) {
for {
select {
case event, ok := <-headersCh:
if !ok {
api.unsubscribe(subID)
return
}
data, ok := event.Data.(tmtypes.EventDataNewBlockHeader)
if !ok {
log.WithFields(log.Fields{
"expected": "tmtypes.EventDataNewBlockHeader",
"actual": fmt.Sprintf("%T", event.Data),
}).Warningln("event Data type mismatch")
continue
}
header := EthHeaderFromTendermint(data.Header)
api.filtersMu.RLock()
for subID, wsSub := range api.filters {
if wsSub.query != query {
continue
}
// write to ws conn
res := &SubscriptionNotification{
Jsonrpc: "2.0",
Method: "eth_subscription",
Params: &SubscriptionResult{
Subscription: subID,
Result: header,
},
}
err = wsSub.wsConn.WriteJSON(res)
if err != nil {
api.logger.WithError(err).Error("error writing header, will drop peer")
try(func() {
api.filtersMu.RUnlock()
api.filtersMu.Lock()
defer func() {
api.filtersMu.Unlock()
api.filtersMu.RLock()
}()
if err != websocket.ErrCloseSent {
_ = wsSub.wsConn.Close()
}
delete(api.filters, subID)
close(wsSub.unsubscribed)
}, api.logger, "closing websocket peer sub")
}
}
api.filtersMu.RUnlock()
case err, ok := <-errCh:
if !ok {
api.unsubscribe(subID)
return
}
log.WithError(err).Debugln("dropping NewHeads WebSocket subscription", subID)
api.unsubscribe(subID)
case <-unsubscribed:
return
}
}
}(sub.eventCh, sub.Err())
return subID, nil
}
func try(fn func(), l log.Logger, desc string) {
if l == nil {
l = log.DefaultLogger
}
defer func() {
if x := recover(); x != nil {
if err, ok := x.(error); ok {
// debug.PrintStack()
l.WithError(err).Debugln("panic during", desc)
return
}
// debug.PrintStack()
l.Debugf("panic during %s: %+v", desc, x)
return
}
}()
fn()
}
func (api *pubSubAPI) subscribeLogs(wsConn *wsConn, extra interface{}) (rpc.ID, error) {
crit := filters.FilterCriteria{}
if extra != nil {
params, ok := extra.(map[string]interface{})
if !ok {
err := errors.New("invalid criteria")
log.WithFields(log.Fields{
"criteria": fmt.Sprintf("expected: map[string]interface{}, got %T", extra),
}).Errorln(err)
return "", err
}
if params["address"] != nil {
address, ok := params["address"].(string)
addresses, sok := params["address"].([]interface{})
if !ok && !sok {
err := errors.New("invalid address; must be address or array of addresses")
log.WithFields(log.Fields{
"address": ok,
"addresses": sok,
"type": fmt.Sprintf("%T", params["address"]),
}).Errorln(err)
return "", err
}
if ok {
crit.Addresses = []common.Address{common.HexToAddress(address)}
}
if sok {
crit.Addresses = []common.Address{}
for _, addr := range addresses {
address, ok := addr.(string)
if !ok {
err := errors.New("invalid address")
log.WithField("type", fmt.Sprintf("%T", addr)).Errorln(err)
return "", err
}
crit.Addresses = append(crit.Addresses, common.HexToAddress(address))
}
}
}
if params["topics"] != nil {
topics, ok := params["topics"].([]interface{})
if !ok {
err := errors.New("invalid topics")
log.WithFields(log.Fields{
"type": fmt.Sprintf("expected []interface{}, got %T", params["topics"]),
}).Errorln(err)
return "", err
}
crit.Topics = make([][]common.Hash, len(topics))
addCritTopic := func(topicIdx int, topic interface{}) error {
tstr, ok := topic.(string)
if !ok {
err := errors.Errorf("invalid topic: %s", topic)
log.WithField("type", fmt.Sprintf("expected string, got %T", topic)).Errorln(err)
return err
}
crit.Topics[topicIdx] = []common.Hash{common.HexToHash(tstr)}
return nil
}
for topicIdx, subtopics := range topics {
if subtopics == nil {
continue
}
// in case we don't have list, but a single topic value
if topic, ok := subtopics.(string); ok {
if err := addCritTopic(topicIdx, topic); err != nil {
return "", err
}
continue
}
// in case we actually have a list of subtopics
subtopicsList, ok := subtopics.([]interface{})
if !ok {
err := errors.New("invalid subtopics")
log.WithFields(log.Fields{
"type": fmt.Sprintf("expected []interface{}, got %T", subtopics),
}).Errorln(err)
return "", err
}
subtopicsCollect := make([]common.Hash, len(subtopicsList))
for idx, subtopic := range subtopicsList {
tstr, ok := subtopic.(string)
if !ok {
err := errors.Errorf("invalid subtopic: %s", subtopic)
log.WithField("type", fmt.Sprintf("expected string, got %T", subtopic)).Errorln(err)
return "", err
}
subtopicsCollect[idx] = common.HexToHash(tstr)
}
crit.Topics[topicIdx] = subtopicsCollect
}
}
}
critBz, err := json.Marshal(crit)
if err != nil {
log.WithError(err).Errorln("failed to JSON marshal criteria")
return rpc.ID(""), err
}
var query = "subscribeLogs" + string(critBz)
var subID = rpc.NewID()
sub, _, err := api.events.SubscribeLogs(crit)
if err != nil {
log.WithError(err).Errorln("failed to subscribe logs")
return rpc.ID(""), err
}
unsubscribed := make(chan struct{})
api.filtersMu.Lock()
api.filters[subID] = &wsSubscription{
sub: sub,
wsConn: wsConn,
unsubscribed: unsubscribed,
query: query,
}
api.filtersMu.Unlock()
go func(ch <-chan coretypes.ResultEvent, errCh <-chan error, subID rpc.ID) {
for {
select {
case event, ok := <-ch:
if !ok {
api.unsubscribe(subID)
return
}
dataTx, ok := event.Data.(tmtypes.EventDataTx)
if !ok {
log.WithFields(log.Fields{
"expected": "tmtypes.EventDataTx",
"actual": fmt.Sprintf("%T", event.Data),
}).Warningln("event Data type mismatch")
continue
}
txResponse, err := evmtypes.DecodeTxResponse(dataTx.TxResult.Result.Data)
if err != nil {
log.WithError(err).Errorln("failed to decode tx response")
return
}
logs := filterLogs(txResponse.TxLogs.EthLogs(), crit.FromBlock, crit.ToBlock, crit.Addresses, crit.Topics)
if len(logs) == 0 {
continue
}
api.filtersMu.RLock()
wsSub, ok := api.filters[subID]
if !ok {
log.Warningln("subID not in filters", subID)
return
}
api.filtersMu.RUnlock()
for _, ethLog := range logs {
res := &SubscriptionNotification{
Jsonrpc: "2.0",
Method: "eth_subscription",
Params: &SubscriptionResult{
Subscription: subID,
Result: ethLog,
},
}
err = wsSub.wsConn.WriteJSON(res)
if err != nil {
try(func() {
api.filtersMu.Lock()
defer api.filtersMu.Unlock()
if err != websocket.ErrCloseSent {
_ = wsSub.wsConn.Close()
}
delete(api.filters, subID)
close(wsSub.unsubscribed)
}, api.logger, "closing websocket peer sub")
}
}
case err, ok := <-errCh:
if !ok {
api.unsubscribe(subID)
return
}
log.WithError(err).Warningln("dropping Logs WebSocket subscription", subID)
api.unsubscribe(subID)
case <-unsubscribed:
return
}
}
}(sub.eventCh, sub.Err(), subID)
return subID, nil
}
func (api *pubSubAPI) subscribePendingTransactions(wsConn *wsConn) (rpc.ID, error) {
var query = "subscribePendingTransactions"
var subID = rpc.NewID()
sub, _, err := api.events.SubscribePendingTxs()
if err != nil {
return "", errors.Wrap(err, "error creating block filter: %s")
}
unsubscribed := make(chan struct{})
api.filtersMu.Lock()
api.filters[subID] = &wsSubscription{
sub: sub,
wsConn: wsConn,
unsubscribed: unsubscribed,
query: query,
}
api.filtersMu.Unlock()
go func(txsCh <-chan coretypes.ResultEvent, errCh <-chan error) {
for {
select {
case ev := <-txsCh:
data, _ := ev.Data.(tmtypes.EventDataTx)
txHash := common.BytesToHash(tmtypes.Tx(data.Tx).Hash())
api.filtersMu.RLock()
for subID, wsSub := range api.filters {
if wsSub.query != query {
continue
}
// write to ws conn
res := &SubscriptionNotification{
Jsonrpc: "2.0",
Method: "eth_subscription",
Params: &SubscriptionResult{
Subscription: subID,
Result: txHash,
},
}
err = wsSub.wsConn.WriteJSON(res)
if err != nil {
api.logger.WithError(err).Warningln("error writing header, will drop peer")
try(func() {
api.filtersMu.Lock()
defer api.filtersMu.Unlock()
if err != websocket.ErrCloseSent {
_ = wsSub.wsConn.Close()
}
delete(api.filters, subID)
close(wsSub.unsubscribed)
}, api.logger, "closing websocket peer sub")
}
}
api.filtersMu.RUnlock()
case err, ok := <-errCh:
if !ok {
api.unsubscribe(subID)
return
}
log.WithError(err).Warningln("dropping PendingTransactions WebSocket subscription", subID)
api.unsubscribe(subID)
case <-unsubscribed:
return
}
}
}(sub.eventCh, sub.Err())
return subID, nil
}
func (api *pubSubAPI) subscribeSyncing(wsConn *wsConn) (rpc.ID, error) {
return "", nil
}