forked from cerc-io/laconicd-deprecated
rpc: restructure JSON-RPC directory and rename server config (#612)
* Restructure ethermint/rpc repo structure and change import statements * Add #400 to changelog * fix filepath in util and json_rpc * Move #400 to unreleased section
This commit is contained in:
@@ -1,136 +0,0 @@
|
||||
// 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/cosmos-sdk/server"
|
||||
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
|
||||
"github.com/tharsis/ethermint/ethereum/rpc/backend"
|
||||
"github.com/tharsis/ethermint/ethereum/rpc/namespaces/debug"
|
||||
"github.com/tharsis/ethermint/ethereum/rpc/namespaces/eth"
|
||||
"github.com/tharsis/ethermint/ethereum/rpc/namespaces/eth/filters"
|
||||
"github.com/tharsis/ethermint/ethereum/rpc/namespaces/miner"
|
||||
"github.com/tharsis/ethermint/ethereum/rpc/namespaces/net"
|
||||
"github.com/tharsis/ethermint/ethereum/rpc/namespaces/personal"
|
||||
"github.com/tharsis/ethermint/ethereum/rpc/namespaces/txpool"
|
||||
"github.com/tharsis/ethermint/ethereum/rpc/namespaces/web3"
|
||||
"github.com/tharsis/ethermint/ethereum/rpc/types"
|
||||
|
||||
rpcclient "github.com/tendermint/tendermint/rpc/jsonrpc/client"
|
||||
)
|
||||
|
||||
// RPC namespaces and API version
|
||||
const (
|
||||
Web3Namespace = "web3"
|
||||
EthNamespace = "eth"
|
||||
PersonalNamespace = "personal"
|
||||
NetNamespace = "net"
|
||||
TxPoolNamespace = "txpool"
|
||||
DebugNamespace = "debug"
|
||||
MinerNamespace = "miner"
|
||||
|
||||
apiVersion = "1.0"
|
||||
)
|
||||
|
||||
// GetRPCAPIs returns the list of all APIs
|
||||
func GetRPCAPIs(ctx *server.Context, clientCtx client.Context, tmWSClient *rpcclient.WSClient, selectedAPIs []string) []rpc.API {
|
||||
nonceLock := new(types.AddrLocker)
|
||||
evmBackend := backend.NewEVMBackend(ctx, ctx.Logger, clientCtx)
|
||||
|
||||
var apis []rpc.API
|
||||
// remove duplicates
|
||||
selectedAPIs = unique(selectedAPIs)
|
||||
|
||||
for index := range selectedAPIs {
|
||||
switch selectedAPIs[index] {
|
||||
case EthNamespace:
|
||||
apis = append(apis,
|
||||
rpc.API{
|
||||
Namespace: EthNamespace,
|
||||
Version: apiVersion,
|
||||
Service: eth.NewPublicAPI(ctx.Logger, clientCtx, evmBackend, nonceLock),
|
||||
Public: true,
|
||||
},
|
||||
rpc.API{
|
||||
Namespace: EthNamespace,
|
||||
Version: apiVersion,
|
||||
Service: filters.NewPublicAPI(ctx.Logger, tmWSClient, evmBackend),
|
||||
Public: true,
|
||||
},
|
||||
)
|
||||
case Web3Namespace:
|
||||
apis = append(apis,
|
||||
rpc.API{
|
||||
Namespace: Web3Namespace,
|
||||
Version: apiVersion,
|
||||
Service: web3.NewPublicAPI(),
|
||||
Public: true,
|
||||
},
|
||||
)
|
||||
case NetNamespace:
|
||||
apis = append(apis,
|
||||
rpc.API{
|
||||
Namespace: NetNamespace,
|
||||
Version: apiVersion,
|
||||
Service: net.NewPublicAPI(clientCtx),
|
||||
Public: true,
|
||||
},
|
||||
)
|
||||
case PersonalNamespace:
|
||||
apis = append(apis,
|
||||
rpc.API{
|
||||
Namespace: PersonalNamespace,
|
||||
Version: apiVersion,
|
||||
Service: personal.NewAPI(ctx.Logger, clientCtx, evmBackend),
|
||||
Public: false,
|
||||
},
|
||||
)
|
||||
case TxPoolNamespace:
|
||||
apis = append(apis,
|
||||
rpc.API{
|
||||
Namespace: TxPoolNamespace,
|
||||
Version: apiVersion,
|
||||
Service: txpool.NewPublicAPI(ctx.Logger),
|
||||
Public: true,
|
||||
},
|
||||
)
|
||||
case DebugNamespace:
|
||||
apis = append(apis,
|
||||
rpc.API{
|
||||
Namespace: DebugNamespace,
|
||||
Version: apiVersion,
|
||||
Service: debug.NewAPI(ctx, evmBackend, clientCtx),
|
||||
Public: true,
|
||||
},
|
||||
)
|
||||
case MinerNamespace:
|
||||
apis = append(apis,
|
||||
rpc.API{
|
||||
Namespace: MinerNamespace,
|
||||
Version: apiVersion,
|
||||
Service: miner.NewPrivateAPI(ctx, clientCtx, evmBackend),
|
||||
Public: false,
|
||||
},
|
||||
)
|
||||
default:
|
||||
ctx.Logger.Error("invalid namespace value", "namespace", selectedAPIs[index])
|
||||
}
|
||||
}
|
||||
|
||||
return apis
|
||||
}
|
||||
|
||||
func unique(intSlice []string) []string {
|
||||
keys := make(map[string]bool)
|
||||
var list []string
|
||||
for _, entry := range intSlice {
|
||||
if _, value := keys[entry]; !value {
|
||||
keys[entry] = true
|
||||
list = append(list, entry)
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
@@ -1,822 +0,0 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strconv"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client/flags"
|
||||
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
|
||||
"github.com/cosmos/cosmos-sdk/server"
|
||||
authtx "github.com/cosmos/cosmos-sdk/x/auth/tx"
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
tmrpctypes "github.com/tendermint/tendermint/rpc/core/types"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/metadata"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
tmtypes "github.com/tendermint/tendermint/types"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
grpctypes "github.com/cosmos/cosmos-sdk/types/grpc"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
ethtypes "github.com/ethereum/go-ethereum/core/types"
|
||||
|
||||
"github.com/tharsis/ethermint/ethereum/rpc/namespaces/eth/filters"
|
||||
"github.com/tharsis/ethermint/ethereum/rpc/types"
|
||||
"github.com/tharsis/ethermint/server/config"
|
||||
ethermint "github.com/tharsis/ethermint/types"
|
||||
evmtypes "github.com/tharsis/ethermint/x/evm/types"
|
||||
)
|
||||
|
||||
// Backend implements the functionality shared within namespaces.
|
||||
// Implemented by EVMBackend.
|
||||
type Backend interface {
|
||||
BlockNumber() (hexutil.Uint64, error)
|
||||
GetBlockByNumber(blockNum types.BlockNumber, fullTx bool) (map[string]interface{}, error)
|
||||
GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error)
|
||||
GetTendermintBlockByNumber(blockNum types.BlockNumber) (*tmrpctypes.ResultBlock, error)
|
||||
CurrentHeader() *ethtypes.Header
|
||||
HeaderByNumber(blockNum types.BlockNumber) (*ethtypes.Header, error)
|
||||
HeaderByHash(blockHash common.Hash) (*ethtypes.Header, error)
|
||||
PendingTransactions() ([]*sdk.Tx, error)
|
||||
GetTransactionLogs(txHash common.Hash) ([]*ethtypes.Log, error)
|
||||
GetTransactionCount(address common.Address, blockNum types.BlockNumber) (*hexutil.Uint64, error)
|
||||
SendTransaction(args types.SendTxArgs) (common.Hash, error)
|
||||
GetLogsByHeight(height *int64) ([][]*ethtypes.Log, error)
|
||||
GetLogs(hash common.Hash) ([][]*ethtypes.Log, error)
|
||||
BloomStatus() (uint64, uint64)
|
||||
GetCoinbase() (sdk.AccAddress, error)
|
||||
GetTransactionByHash(txHash common.Hash) (*types.RPCTransaction, error)
|
||||
GetTxByEthHash(txHash common.Hash) (*tmrpctypes.ResultTx, error)
|
||||
EstimateGas(args evmtypes.CallArgs, blockNrOptional *types.BlockNumber) (hexutil.Uint64, error)
|
||||
RPCGasCap() uint64
|
||||
RPCMinGasPrice() int64
|
||||
ChainConfig() *params.ChainConfig
|
||||
SuggestGasTipCap() (*big.Int, error)
|
||||
GetFilteredBlocks(from int64, to int64, filter [][]filters.BloomIV, filterAddresses bool) ([]int64, error)
|
||||
}
|
||||
|
||||
var _ Backend = (*EVMBackend)(nil)
|
||||
|
||||
var bAttributeKeyEthereumBloom = []byte(evmtypes.AttributeKeyEthereumBloom)
|
||||
|
||||
// EVMBackend implements the Backend interface
|
||||
type EVMBackend struct {
|
||||
ctx context.Context
|
||||
clientCtx client.Context
|
||||
queryClient *types.QueryClient // gRPC query client
|
||||
logger log.Logger
|
||||
chainID *big.Int
|
||||
cfg config.Config
|
||||
}
|
||||
|
||||
// NewEVMBackend creates a new EVMBackend instance
|
||||
func NewEVMBackend(ctx *server.Context, logger log.Logger, clientCtx client.Context) *EVMBackend {
|
||||
chainID, err := ethermint.ParseChainID(clientCtx.ChainID)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
appConf := config.GetConfig(ctx.Viper)
|
||||
|
||||
return &EVMBackend{
|
||||
ctx: context.Background(),
|
||||
clientCtx: clientCtx,
|
||||
queryClient: types.NewQueryClient(clientCtx),
|
||||
logger: logger.With("module", "evm-backend"),
|
||||
chainID: chainID,
|
||||
cfg: appConf,
|
||||
}
|
||||
}
|
||||
|
||||
// BlockNumber returns the current block number in abci app state.
|
||||
// Because abci app state could lag behind from tendermint latest block, it's more stable
|
||||
// for the client to use the latest block number in abci app state than tendermint rpc.
|
||||
func (e *EVMBackend) BlockNumber() (hexutil.Uint64, error) {
|
||||
// do any grpc query, ignore the response and use the returned block height
|
||||
var header metadata.MD
|
||||
_, err := e.queryClient.Params(e.ctx, &evmtypes.QueryParamsRequest{}, grpc.Header(&header))
|
||||
if err != nil {
|
||||
return hexutil.Uint64(0), err
|
||||
}
|
||||
|
||||
blockHeightHeader := header.Get(grpctypes.GRPCBlockHeightHeader)
|
||||
if headerLen := len(blockHeightHeader); headerLen != 1 {
|
||||
return 0, fmt.Errorf("unexpected '%s' gRPC header length; got %d, expected: %d", grpctypes.GRPCBlockHeightHeader, headerLen, 1)
|
||||
}
|
||||
|
||||
height, err := strconv.ParseUint(blockHeightHeader[0], 10, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to parse block height: %w", err)
|
||||
}
|
||||
|
||||
return hexutil.Uint64(height), nil
|
||||
}
|
||||
|
||||
// GetBlockByNumber returns the block identified by number.
|
||||
func (e *EVMBackend) GetBlockByNumber(blockNum types.BlockNumber, fullTx bool) (map[string]interface{}, error) {
|
||||
resBlock, err := e.GetTendermintBlockByNumber(blockNum)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// return if requested block height is greater than the current one
|
||||
if resBlock == nil || resBlock.Block == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
res, err := e.EthBlockFromTendermint(resBlock.Block, fullTx)
|
||||
if err != nil {
|
||||
e.logger.Debug("EthBlockFromTendermint failed", "height", blockNum, "error", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// 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.Debug("BlockByHash block not found", "hash", hash.Hex(), "error", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resBlock.Block == nil {
|
||||
e.logger.Debug("BlockByHash block not found", "hash", hash.Hex())
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return e.EthBlockFromTendermint(resBlock.Block, fullTx)
|
||||
}
|
||||
|
||||
// GetTendermintBlockByNumber returns a Tendermint format block by block number
|
||||
func (e *EVMBackend) GetTendermintBlockByNumber(blockNum types.BlockNumber) (*tmrpctypes.ResultBlock, error) {
|
||||
height := blockNum.Int64()
|
||||
currentBlockNumber, _ := e.BlockNumber()
|
||||
|
||||
switch blockNum {
|
||||
case types.EthLatestBlockNumber:
|
||||
if currentBlockNumber > 0 {
|
||||
height = int64(currentBlockNumber)
|
||||
}
|
||||
case types.EthPendingBlockNumber:
|
||||
if currentBlockNumber > 0 {
|
||||
height = int64(currentBlockNumber)
|
||||
}
|
||||
case types.EthEarliestBlockNumber:
|
||||
height = 1
|
||||
default:
|
||||
if blockNum < 0 {
|
||||
return nil, errors.Errorf("cannot fetch a negative block height: %d", height)
|
||||
}
|
||||
if height > int64(currentBlockNumber) {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
resBlock, err := e.clientCtx.Client.Block(e.ctx, &height)
|
||||
if err != nil {
|
||||
if resBlock, err = e.clientCtx.Client.Block(e.ctx, nil); err != nil {
|
||||
e.logger.Debug("tendermint client failed to get latest block", "height", height, "error", err.Error())
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
if resBlock.Block == nil {
|
||||
e.logger.Debug("GetBlockByNumber block not found", "height", height)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return resBlock, nil
|
||||
}
|
||||
|
||||
// BlockBloom query block bloom filter from block results
|
||||
func (e *EVMBackend) BlockBloom(height *int64) (ethtypes.Bloom, error) {
|
||||
result, err := e.clientCtx.Client.BlockResults(e.ctx, height)
|
||||
if err != nil {
|
||||
return ethtypes.Bloom{}, err
|
||||
}
|
||||
for _, event := range result.EndBlockEvents {
|
||||
if event.Type != evmtypes.EventTypeBlockBloom {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, attr := range event.Attributes {
|
||||
if bytes.Equal(attr.Key, bAttributeKeyEthereumBloom) {
|
||||
return ethtypes.BytesToBloom(attr.Value), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return ethtypes.Bloom{}, errors.New("block bloom event is not found")
|
||||
}
|
||||
|
||||
// EthBlockFromTendermint returns a JSON-RPC compatible Ethereum block from a given Tendermint block and its block result.
|
||||
func (e *EVMBackend) EthBlockFromTendermint(
|
||||
block *tmtypes.Block,
|
||||
fullTx bool,
|
||||
) (map[string]interface{}, error) {
|
||||
ethRPCTxs := []interface{}{}
|
||||
|
||||
for i, txBz := range block.Txs {
|
||||
tx, err := e.clientCtx.TxConfig.TxDecoder()(txBz)
|
||||
if err != nil {
|
||||
e.logger.Debug("failed to decode transaction in block", "height", block.Height, "error", err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
for _, msg := range tx.GetMsgs() {
|
||||
ethMsg, ok := msg.(*evmtypes.MsgEthereumTx)
|
||||
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
hash := ethMsg.AsTransaction().Hash()
|
||||
if !fullTx {
|
||||
ethRPCTxs = append(ethRPCTxs, hash)
|
||||
continue
|
||||
}
|
||||
|
||||
// get full transaction from message data
|
||||
from, err := ethMsg.GetSender(e.chainID)
|
||||
if err != nil {
|
||||
e.logger.Debug("failed to get sender from already included transaction", "hash", hash.Hex(), "error", err.Error())
|
||||
from = common.HexToAddress(ethMsg.From)
|
||||
}
|
||||
|
||||
txData, err := evmtypes.UnpackTxData(ethMsg.Data)
|
||||
if err != nil {
|
||||
e.logger.Debug("decoding failed", "error", err.Error())
|
||||
return nil, fmt.Errorf("failed to unpack tx data: %w", err)
|
||||
}
|
||||
|
||||
ethTx, err := types.NewTransactionFromData(
|
||||
txData,
|
||||
from,
|
||||
hash,
|
||||
common.BytesToHash(block.Hash()),
|
||||
uint64(block.Height),
|
||||
uint64(i),
|
||||
)
|
||||
if err != nil {
|
||||
e.logger.Debug("NewTransactionFromData for receipt failed", "hash", hash.Hex(), "error", err.Error())
|
||||
continue
|
||||
}
|
||||
ethRPCTxs = append(ethRPCTxs, ethTx)
|
||||
}
|
||||
}
|
||||
|
||||
bloom, err := e.BlockBloom(&block.Height)
|
||||
if err != nil {
|
||||
e.logger.Debug("failed to query BlockBloom", "height", block.Height, "error", err.Error())
|
||||
}
|
||||
|
||||
req := &evmtypes.QueryValidatorAccountRequest{
|
||||
ConsAddress: sdk.ConsAddress(block.Header.ProposerAddress).String(),
|
||||
}
|
||||
|
||||
ctx := types.ContextWithHeight(block.Height)
|
||||
|
||||
res, err := e.queryClient.ValidatorAccount(ctx, req)
|
||||
if err != nil {
|
||||
e.logger.Debug(
|
||||
"failed to query validator operator address",
|
||||
"height", block.Height,
|
||||
"cons-address", req.ConsAddress,
|
||||
"error", err.Error(),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addr, err := sdk.AccAddressFromBech32(res.AccountAddress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
validatorAddr := common.BytesToAddress(addr)
|
||||
|
||||
gasLimit, err := types.BlockMaxGasFromConsensusParams(ctx, e.clientCtx)
|
||||
if err != nil {
|
||||
e.logger.Error("failed to query consensus params", "error", err.Error())
|
||||
}
|
||||
|
||||
resBlockResult, err := e.clientCtx.Client.BlockResults(e.ctx, &block.Height)
|
||||
if err != nil {
|
||||
e.logger.Debug("EthBlockFromTendermint block result not found", "height", block.Height, "error", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gasUsed := uint64(0)
|
||||
|
||||
for _, txsResult := range resBlockResult.TxsResults {
|
||||
gasUsed += uint64(txsResult.GetGasUsed())
|
||||
}
|
||||
|
||||
formattedBlock := types.FormatBlock(block.Header, block.Size(), gasLimit, new(big.Int).SetUint64(gasUsed), ethRPCTxs, bloom, validatorAddr)
|
||||
return formattedBlock, nil
|
||||
}
|
||||
|
||||
// CurrentHeader returns the latest block header
|
||||
func (e *EVMBackend) CurrentHeader() *ethtypes.Header {
|
||||
header, _ := e.HeaderByNumber(types.EthLatestBlockNumber)
|
||||
return header
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
case types.EthPendingBlockNumber:
|
||||
if currentBlockNumber > 0 {
|
||||
height = int64(currentBlockNumber)
|
||||
}
|
||||
case types.EthEarliestBlockNumber:
|
||||
height = 1
|
||||
default:
|
||||
if blockNum < 0 {
|
||||
return nil, errors.Errorf("incorrect block height: %d", height)
|
||||
}
|
||||
}
|
||||
|
||||
resBlock, err := e.clientCtx.Client.Block(e.ctx, &height)
|
||||
if err != nil {
|
||||
e.logger.Debug("HeaderByNumber failed")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bloom, err := e.BlockBloom(&resBlock.Block.Height)
|
||||
if err != nil {
|
||||
e.logger.Debug("HeaderByNumber BlockBloom failed", "height", resBlock.Block.Height)
|
||||
}
|
||||
|
||||
ethHeader := types.EthHeaderFromTendermint(resBlock.Block.Header)
|
||||
ethHeader.Bloom = 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.Debug("HeaderByHash failed", "hash", blockHash.Hex())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resBlock.Block == nil {
|
||||
return nil, errors.Errorf("block not found for hash %s", blockHash.Hex())
|
||||
}
|
||||
|
||||
bloom, err := e.BlockBloom(&resBlock.Block.Height)
|
||||
if err != nil {
|
||||
e.logger.Debug("HeaderByHash BlockBloom failed", "height", resBlock.Block.Height)
|
||||
}
|
||||
|
||||
ethHeader := types.EthHeaderFromTendermint(resBlock.Block.Header)
|
||||
ethHeader.Bloom = 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) {
|
||||
tx, err := e.GetTxByEthHash(txHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return TxLogsFromEvents(e.clientCtx.Codec, tx.TxResult.Events), 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() ([]*sdk.Tx, error) {
|
||||
res, err := e.clientCtx.Client.UnconfirmedTxs(e.ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]*sdk.Tx, 0, len(res.Txs))
|
||||
for _, txBz := range res.Txs {
|
||||
tx, err := e.clientCtx.TxConfig.TxDecoder()(txBz)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, &tx)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetLogsByHeight returns all the logs from all the ethereum transactions in a block.
|
||||
func (e *EVMBackend) GetLogsByHeight(height *int64) ([][]*ethtypes.Log, error) {
|
||||
// NOTE: we query the state in case the tx result logs are not persisted after an upgrade.
|
||||
blockRes, err := e.clientCtx.Client.BlockResults(e.ctx, height)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blockLogs := [][]*ethtypes.Log{}
|
||||
for _, txResult := range blockRes.TxsResults {
|
||||
logs := TxLogsFromEvents(e.clientCtx.Codec, txResult.Events)
|
||||
blockLogs = append(blockLogs, logs)
|
||||
}
|
||||
|
||||
return blockLogs, nil
|
||||
}
|
||||
|
||||
// GetLogs returns all the logs from all the ethereum transactions in a block.
|
||||
func (e *EVMBackend) GetLogs(hash common.Hash) ([][]*ethtypes.Log, error) {
|
||||
block, err := e.clientCtx.Client.BlockByHash(e.ctx, hash.Bytes())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return e.GetLogsByHeight(&block.Block.Header.Height)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
case types.EthPendingBlockNumber:
|
||||
if currentBlockNumber > 0 {
|
||||
height = int64(currentBlockNumber)
|
||||
}
|
||||
case types.EthEarliestBlockNumber:
|
||||
height = 1
|
||||
default:
|
||||
if blockNum < 0 {
|
||||
return nil, errors.Errorf("incorrect block height: %d", height)
|
||||
}
|
||||
}
|
||||
|
||||
return e.GetLogsByHeight(&height)
|
||||
}
|
||||
|
||||
// BloomStatus returns the BloomBitsBlocks and the number of processed sections maintained
|
||||
// by the chain indexer.
|
||||
func (e *EVMBackend) BloomStatus() (uint64, uint64) {
|
||||
return 4096, 0
|
||||
}
|
||||
|
||||
// GetCoinbase is the address that staking rewards will be send to (alias for Etherbase).
|
||||
func (e *EVMBackend) GetCoinbase() (sdk.AccAddress, error) {
|
||||
node, err := e.clientCtx.GetNode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
status, err := node.Status(e.ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := &evmtypes.QueryValidatorAccountRequest{
|
||||
ConsAddress: sdk.ConsAddress(status.ValidatorInfo.Address).String(),
|
||||
}
|
||||
|
||||
res, err := e.queryClient.ValidatorAccount(e.ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
address, _ := sdk.AccAddressFromBech32(res.AccountAddress)
|
||||
return address, nil
|
||||
}
|
||||
|
||||
// GetTransactionByHash returns the Ethereum format transaction identified by Ethereum transaction hash
|
||||
func (e *EVMBackend) GetTransactionByHash(txHash common.Hash) (*types.RPCTransaction, error) {
|
||||
res, err := e.GetTxByEthHash(txHash)
|
||||
if err != nil {
|
||||
// try to find tx in mempool
|
||||
txs, err := e.PendingTransactions()
|
||||
if err != nil {
|
||||
e.logger.Debug("tx not found", "hash", txHash.Hex(), "error", err.Error())
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
for _, tx := range txs {
|
||||
msg, err := evmtypes.UnwrapEthereumMsg(tx)
|
||||
if err != nil {
|
||||
// not ethereum tx
|
||||
continue
|
||||
}
|
||||
|
||||
if msg.Hash == txHash.Hex() {
|
||||
rpctx, err := types.NewTransactionFromMsg(
|
||||
msg,
|
||||
common.Hash{},
|
||||
uint64(0),
|
||||
uint64(0),
|
||||
e.chainID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rpctx, nil
|
||||
}
|
||||
}
|
||||
|
||||
e.logger.Debug("tx not found", "hash", txHash.Hex())
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
resBlock, err := e.clientCtx.Client.Block(e.ctx, &res.Height)
|
||||
if err != nil {
|
||||
e.logger.Debug("block not found", "height", res.Height, "error", err.Error())
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
tx, err := e.clientCtx.TxConfig.TxDecoder()(res.Tx)
|
||||
if err != nil {
|
||||
e.logger.Debug("decoding failed", "error", err.Error())
|
||||
return nil, fmt.Errorf("failed to decode tx: %w", err)
|
||||
}
|
||||
|
||||
msg, err := evmtypes.UnwrapEthereumMsg(&tx)
|
||||
if err != nil {
|
||||
e.logger.Debug("invalid tx", "error", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return types.NewTransactionFromMsg(
|
||||
msg,
|
||||
common.BytesToHash(resBlock.Block.Hash()),
|
||||
uint64(res.Height),
|
||||
uint64(res.Index),
|
||||
e.chainID,
|
||||
)
|
||||
}
|
||||
|
||||
// GetTxByEthHash uses `/tx_query` to find transaction by ethereum tx hash
|
||||
// TODO: Don't need to convert once hashing is fixed on Tendermint
|
||||
// https://github.com/tendermint/tendermint/issues/6539
|
||||
func (e *EVMBackend) GetTxByEthHash(hash common.Hash) (*tmrpctypes.ResultTx, error) {
|
||||
query := fmt.Sprintf("%s.%s='%s'", evmtypes.TypeMsgEthereumTx, evmtypes.AttributeKeyEthereumTxHash, hash.Hex())
|
||||
resTxs, err := e.clientCtx.Client.TxSearch(e.ctx, query, false, nil, nil, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(resTxs.Txs) == 0 {
|
||||
return nil, errors.Errorf("ethereum tx not found for hash %s", hash.Hex())
|
||||
}
|
||||
return resTxs.Txs[0], nil
|
||||
}
|
||||
|
||||
func (e *EVMBackend) SendTransaction(args types.SendTxArgs) (common.Hash, error) {
|
||||
// Look up the wallet containing the requested signer
|
||||
_, err := e.clientCtx.Keyring.KeyByAddress(sdk.AccAddress(args.From.Bytes()))
|
||||
if err != nil {
|
||||
e.logger.Error("failed to find key in keyring", "address", args.From, "error", err.Error())
|
||||
return common.Hash{}, fmt.Errorf("%s; %s", keystore.ErrNoMatch, err.Error())
|
||||
}
|
||||
|
||||
args, err = e.setTxDefaults(args)
|
||||
if err != nil {
|
||||
return common.Hash{}, err
|
||||
}
|
||||
|
||||
msg := args.ToTransaction()
|
||||
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
e.logger.Debug("tx failed basic validation", "error", err.Error())
|
||||
return common.Hash{}, err
|
||||
}
|
||||
|
||||
// TODO: get from chain config
|
||||
signer := ethtypes.LatestSignerForChainID(args.ChainID.ToInt())
|
||||
|
||||
// Sign transaction
|
||||
if err := msg.Sign(signer, e.clientCtx.Keyring); err != nil {
|
||||
e.logger.Debug("failed to sign tx", "error", err.Error())
|
||||
return common.Hash{}, err
|
||||
}
|
||||
|
||||
// Assemble transaction from fields
|
||||
builder, ok := e.clientCtx.TxConfig.NewTxBuilder().(authtx.ExtensionOptionsTxBuilder)
|
||||
if !ok {
|
||||
e.logger.Error("clientCtx.TxConfig.NewTxBuilder returns unsupported builder", "error", err.Error())
|
||||
}
|
||||
|
||||
option, err := codectypes.NewAnyWithValue(&evmtypes.ExtensionOptionsEthereumTx{})
|
||||
if err != nil {
|
||||
e.logger.Error("codectypes.NewAnyWithValue failed to pack an obvious value", "error", err.Error())
|
||||
return common.Hash{}, err
|
||||
}
|
||||
|
||||
builder.SetExtensionOptions(option)
|
||||
err = builder.SetMsgs(msg)
|
||||
if err != nil {
|
||||
e.logger.Error("builder.SetMsgs failed", "error", err.Error())
|
||||
}
|
||||
|
||||
// Query params to use the EVM denomination
|
||||
res, err := e.queryClient.QueryClient.Params(e.ctx, &evmtypes.QueryParamsRequest{})
|
||||
if err != nil {
|
||||
e.logger.Error("failed to query evm params", "error", err.Error())
|
||||
return common.Hash{}, err
|
||||
}
|
||||
|
||||
txData, err := evmtypes.UnpackTxData(msg.Data)
|
||||
if err != nil {
|
||||
e.logger.Error("failed to unpack tx data", "error", err.Error())
|
||||
return common.Hash{}, err
|
||||
}
|
||||
|
||||
fees := sdk.Coins{sdk.NewCoin(res.Params.EvmDenom, sdk.NewIntFromBigInt(txData.Fee()))}
|
||||
builder.SetFeeAmount(fees)
|
||||
builder.SetGasLimit(msg.GetGas())
|
||||
|
||||
// Encode transaction by default Tx encoder
|
||||
txEncoder := e.clientCtx.TxConfig.TxEncoder()
|
||||
txBytes, err := txEncoder(builder.GetTx())
|
||||
if err != nil {
|
||||
e.logger.Error("failed to encode eth tx using default encoder", "error", err.Error())
|
||||
return common.Hash{}, err
|
||||
}
|
||||
|
||||
txHash := msg.AsTransaction().Hash()
|
||||
|
||||
// Broadcast transaction in sync mode (default)
|
||||
// NOTE: If error is encountered on the node, the broadcast will not return an error
|
||||
syncCtx := e.clientCtx.WithBroadcastMode(flags.BroadcastSync)
|
||||
rsp, err := syncCtx.BroadcastTx(txBytes)
|
||||
if err != nil || rsp.Code != 0 {
|
||||
if err == nil {
|
||||
err = errors.New(rsp.RawLog)
|
||||
}
|
||||
e.logger.Error("failed to broadcast tx", "error", err.Error())
|
||||
return txHash, err
|
||||
}
|
||||
|
||||
// Return transaction hash
|
||||
return txHash, nil
|
||||
}
|
||||
|
||||
// EstimateGas returns an estimate of gas usage for the given smart contract call.
|
||||
func (e *EVMBackend) EstimateGas(args evmtypes.CallArgs, blockNrOptional *types.BlockNumber) (hexutil.Uint64, error) {
|
||||
blockNr := types.EthPendingBlockNumber
|
||||
if blockNrOptional != nil {
|
||||
blockNr = *blockNrOptional
|
||||
}
|
||||
|
||||
bz, err := json.Marshal(&args)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
req := evmtypes.EthCallRequest{Args: bz, GasCap: e.RPCGasCap()}
|
||||
|
||||
// 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.
|
||||
res, err := e.queryClient.EstimateGas(types.ContextWithHeight(blockNr.Int64()), &req)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return hexutil.Uint64(res.Gas), nil
|
||||
}
|
||||
|
||||
// GetTransactionCount returns the number of transactions at the given address up to the given block number.
|
||||
func (e *EVMBackend) GetTransactionCount(address common.Address, blockNum types.BlockNumber) (*hexutil.Uint64, error) {
|
||||
// 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
|
||||
}
|
||||
|
||||
includePending := blockNum == types.EthPendingBlockNumber
|
||||
nonce, err := e.getAccountNonce(address, includePending, blockNum.Int64(), e.logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
n := hexutil.Uint64(nonce)
|
||||
return &n, nil
|
||||
}
|
||||
|
||||
// RPCGasCap is the global gas cap for eth-call variants.
|
||||
func (e *EVMBackend) RPCGasCap() uint64 {
|
||||
return e.cfg.JSONRPC.GasCap
|
||||
}
|
||||
|
||||
// RPCMinGasPrice returns the minimum gas price for a transaction obtained from
|
||||
// the node config. If set value is 0, it will default to 20.
|
||||
|
||||
func (e *EVMBackend) RPCMinGasPrice() int64 {
|
||||
evmParams, err := e.queryClient.Params(context.Background(), &evmtypes.QueryParamsRequest{})
|
||||
if err != nil {
|
||||
return ethermint.DefaultGasPrice
|
||||
}
|
||||
|
||||
minGasPrice := e.cfg.GetMinGasPrices()
|
||||
amt := minGasPrice.AmountOf(evmParams.Params.EvmDenom).TruncateInt64()
|
||||
if amt == 0 {
|
||||
return ethermint.DefaultGasPrice
|
||||
}
|
||||
|
||||
return amt
|
||||
}
|
||||
|
||||
// ChainConfig return the ethereum chain configuration
|
||||
func (e *EVMBackend) ChainConfig() *params.ChainConfig {
|
||||
params, err := e.queryClient.Params(e.ctx, &evmtypes.QueryParamsRequest{})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return params.Params.ChainConfig.EthereumConfig(e.chainID)
|
||||
}
|
||||
|
||||
// SuggestGasTipCap returns the suggested tip cap
|
||||
func (e *EVMBackend) SuggestGasTipCap() (*big.Int, error) {
|
||||
// TODO: implement
|
||||
return big.NewInt(1), nil
|
||||
}
|
||||
|
||||
// GetFilteredBlocks returns the block height list match the given bloom filters.
|
||||
func (e *EVMBackend) GetFilteredBlocks(
|
||||
from int64,
|
||||
to int64,
|
||||
filters [][]filters.BloomIV,
|
||||
filterAddresses bool,
|
||||
) ([]int64, error) {
|
||||
matchedBlocks := make([]int64, 0)
|
||||
|
||||
BLOCKS:
|
||||
for height := from; height <= to; height++ {
|
||||
if err := e.ctx.Err(); err != nil {
|
||||
e.logger.Error("EVMBackend context error", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
h := height
|
||||
bloom, err := e.BlockBloom(&h)
|
||||
if err != nil {
|
||||
e.logger.Error("retrieve header failed", "blockHeight", height, "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i, filter := range filters {
|
||||
// filter the header bloom with the addresses
|
||||
if filterAddresses && i == 0 {
|
||||
if !checkMatches(bloom, filter) {
|
||||
continue BLOCKS
|
||||
}
|
||||
|
||||
// the filter doesn't have any topics
|
||||
if len(filters) == 1 {
|
||||
matchedBlocks = append(matchedBlocks, height)
|
||||
continue BLOCKS
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// filter the bloom with topics
|
||||
if len(filter) > 0 && !checkMatches(bloom, filter) {
|
||||
continue BLOCKS
|
||||
}
|
||||
}
|
||||
matchedBlocks = append(matchedBlocks, height)
|
||||
}
|
||||
|
||||
return matchedBlocks, nil
|
||||
}
|
||||
|
||||
// checkMatches revised the function from
|
||||
// https://github.com/ethereum/go-ethereum/blob/401354976bb44f0ad4455ca1e0b5c0dc31d9a5f5/core/types/bloom9.go#L88
|
||||
func checkMatches(bloom ethtypes.Bloom, filter []filters.BloomIV) bool {
|
||||
for _, bloomIV := range filter {
|
||||
if bloomIV.V[0] == bloomIV.V[0]&bloom[bloomIV.I[0]] &&
|
||||
bloomIV.V[1] == bloomIV.V[1]&bloom[bloomIV.I[1]] &&
|
||||
bloomIV.V[2] == bloomIV.V[2]&bloom[bloomIV.I[2]] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"math/big"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
ethtypes "github.com/ethereum/go-ethereum/core/types"
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/tharsis/ethermint/ethereum/rpc/types"
|
||||
evmtypes "github.com/tharsis/ethermint/x/evm/types"
|
||||
)
|
||||
|
||||
// setTxDefaults populates tx message with default values in case they are not
|
||||
// provided on the args
|
||||
func (e *EVMBackend) setTxDefaults(args types.SendTxArgs) (types.SendTxArgs, error) {
|
||||
if args.GasPrice == nil {
|
||||
// TODO: Suggest a gas price based on the previous included txs
|
||||
args.GasPrice = (*hexutil.Big)(new(big.Int).SetInt64(e.RPCMinGasPrice()))
|
||||
}
|
||||
|
||||
if args.Nonce == nil {
|
||||
// get the nonce from the account retriever
|
||||
// ignore error in case tge account doesn't exist yet
|
||||
nonce, _ := e.getAccountNonce(args.From, true, 0, e.logger)
|
||||
args.Nonce = (*hexutil.Uint64)(&nonce)
|
||||
}
|
||||
|
||||
if args.Data != nil && args.Input != nil && !bytes.Equal(*args.Data, *args.Input) {
|
||||
return args, errors.New("both 'data' and 'input' are set and not equal. Please use 'input' to pass transaction call data")
|
||||
}
|
||||
|
||||
if args.To == nil {
|
||||
// Contract creation
|
||||
var input []byte
|
||||
if args.Data != nil {
|
||||
input = *args.Data
|
||||
} else if args.Input != nil {
|
||||
input = *args.Input
|
||||
}
|
||||
|
||||
if len(input) == 0 {
|
||||
return args, errors.New("contract creation without any data provided")
|
||||
}
|
||||
}
|
||||
|
||||
if args.Gas == nil {
|
||||
// For backwards-compatibility reason, we try both input and data
|
||||
// but input is preferred.
|
||||
input := args.Input
|
||||
if input == nil {
|
||||
input = args.Data
|
||||
}
|
||||
|
||||
callArgs := evmtypes.CallArgs{
|
||||
From: &args.From, // From shouldn't be nil
|
||||
To: args.To,
|
||||
Gas: args.Gas,
|
||||
GasPrice: args.GasPrice,
|
||||
Value: args.Value,
|
||||
Data: input,
|
||||
AccessList: args.AccessList,
|
||||
}
|
||||
blockNr := types.NewBlockNumber(big.NewInt(0))
|
||||
estimated, err := e.EstimateGas(callArgs, &blockNr)
|
||||
if err != nil {
|
||||
return args, err
|
||||
}
|
||||
args.Gas = &estimated
|
||||
e.logger.Debug("estimate gas usage automatically", "gas", args.Gas)
|
||||
}
|
||||
|
||||
if args.ChainID == nil {
|
||||
args.ChainID = (*hexutil.Big)(e.chainID)
|
||||
}
|
||||
|
||||
return args, nil
|
||||
}
|
||||
|
||||
// getAccountNonce returns the account nonce for the given account address.
|
||||
// If the pending value is true, it will iterate over the mempool (pending)
|
||||
// txs in order to compute and return the pending tx sequence.
|
||||
// Todo: include the ability to specify a blockNumber
|
||||
func (e *EVMBackend) getAccountNonce(accAddr common.Address, pending bool, height int64, logger log.Logger) (uint64, error) {
|
||||
queryClient := authtypes.NewQueryClient(e.clientCtx)
|
||||
res, err := queryClient.Account(types.ContextWithHeight(height), &authtypes.QueryAccountRequest{Address: sdk.AccAddress(accAddr.Bytes()).String()})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var acc authtypes.AccountI
|
||||
if err := e.clientCtx.InterfaceRegistry.UnpackAny(res.Account, &acc); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
nonce := acc.GetSequence()
|
||||
|
||||
if !pending {
|
||||
return nonce, nil
|
||||
}
|
||||
|
||||
// the account retriever doesn't include the uncommitted transactions on the nonce so we need to
|
||||
// to manually add them.
|
||||
pendingTxs, err := e.PendingTransactions()
|
||||
if err != nil {
|
||||
logger.Error("failed to fetch pending transactions", "error", err.Error())
|
||||
return nonce, nil
|
||||
}
|
||||
|
||||
// add the uncommitted txs to the nonce counter
|
||||
// only supports `MsgEthereumTx` style tx
|
||||
for _, tx := range pendingTxs {
|
||||
msg, err := evmtypes.UnwrapEthereumMsg(tx)
|
||||
if err != nil {
|
||||
// not ethereum tx
|
||||
continue
|
||||
}
|
||||
|
||||
sender, err := msg.GetSender(e.chainID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if sender == accAddr {
|
||||
nonce++
|
||||
}
|
||||
}
|
||||
|
||||
return nonce, nil
|
||||
}
|
||||
|
||||
// TxLogsFromEvents parses ethereum logs from cosmos events
|
||||
func TxLogsFromEvents(codec codec.Codec, events []abci.Event) []*ethtypes.Log {
|
||||
logs := make([]*evmtypes.Log, 0)
|
||||
for _, event := range events {
|
||||
if event.Type != evmtypes.EventTypeTxLog {
|
||||
continue
|
||||
}
|
||||
for _, attr := range event.Attributes {
|
||||
if !bytes.Equal(attr.Key, []byte(evmtypes.AttributeKeyTxLog)) {
|
||||
continue
|
||||
}
|
||||
|
||||
var log evmtypes.Log
|
||||
codec.MustUnmarshal(attr.Value, &log)
|
||||
logs = append(logs, &log)
|
||||
}
|
||||
}
|
||||
return evmtypes.LogsToEthereum(logs)
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// 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
|
||||
@@ -1,384 +0,0 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"runtime/pprof"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tendermint/tendermint/types"
|
||||
|
||||
evmtypes "github.com/tharsis/ethermint/x/evm/types"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/server"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
"github.com/tharsis/ethermint/ethereum/rpc/backend"
|
||||
rpctypes "github.com/tharsis/ethermint/ethereum/rpc/types"
|
||||
)
|
||||
|
||||
// HandlerT keeps track of the cpu profiler and trace execution
|
||||
type HandlerT struct {
|
||||
cpuFilename string
|
||||
cpuFile io.WriteCloser
|
||||
mu sync.Mutex
|
||||
traceFilename string
|
||||
traceFile io.WriteCloser
|
||||
}
|
||||
|
||||
// API is the collection of tracing APIs exposed over the private debugging endpoint.
|
||||
type API struct {
|
||||
ctx *server.Context
|
||||
logger log.Logger
|
||||
backend backend.Backend
|
||||
clientCtx client.Context
|
||||
queryClient *rpctypes.QueryClient
|
||||
handler *HandlerT
|
||||
}
|
||||
|
||||
// NewAPI creates a new API definition for the tracing methods of the Ethereum service.
|
||||
func NewAPI(
|
||||
ctx *server.Context,
|
||||
backend backend.Backend,
|
||||
clientCtx client.Context,
|
||||
) *API {
|
||||
return &API{
|
||||
ctx: ctx,
|
||||
logger: ctx.Logger.With("module", "debug"),
|
||||
backend: backend,
|
||||
clientCtx: clientCtx,
|
||||
queryClient: rpctypes.NewQueryClient(clientCtx),
|
||||
handler: new(HandlerT),
|
||||
}
|
||||
}
|
||||
|
||||
// TraceTransaction returns the structured logs created during the execution of EVM
|
||||
// and returns them as a JSON object.
|
||||
func (a *API) TraceTransaction(hash common.Hash, config *evmtypes.TraceConfig) (interface{}, error) {
|
||||
a.logger.Debug("debug_traceTransaction", "hash", hash)
|
||||
// Get transaction by hash
|
||||
transaction, err := a.backend.GetTxByEthHash(hash)
|
||||
if err != nil {
|
||||
a.logger.Debug("tx not found", "hash", hash)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// check if block number is 0
|
||||
if transaction.Height == 0 {
|
||||
return nil, errors.New("genesis is not traceable")
|
||||
}
|
||||
|
||||
tx, err := a.clientCtx.TxConfig.TxDecoder()(transaction.Tx)
|
||||
if err != nil {
|
||||
a.logger.Debug("tx not found", "hash", hash)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ethMessage, ok := tx.GetMsgs()[0].(*evmtypes.MsgEthereumTx)
|
||||
if !ok {
|
||||
a.logger.Debug("invalid transaction type", "type", fmt.Sprintf("%T", tx))
|
||||
return nil, fmt.Errorf("invalid transaction type %T", tx)
|
||||
}
|
||||
|
||||
traceTxRequest := evmtypes.QueryTraceTxRequest{
|
||||
Msg: ethMessage,
|
||||
TxIndex: uint64(transaction.Index),
|
||||
}
|
||||
|
||||
if config != nil {
|
||||
traceTxRequest.TraceConfig = config
|
||||
}
|
||||
|
||||
traceResult, err := a.queryClient.TraceTx(rpctypes.ContextWithHeight(transaction.Height), &traceTxRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Response format is unknown due to custom tracer config param
|
||||
// More information can be found here https://geth.ethereum.org/docs/dapp/tracing-filtered
|
||||
var decodedResult interface{}
|
||||
err = json.Unmarshal(traceResult.Data, &decodedResult)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return decodedResult, nil
|
||||
}
|
||||
|
||||
// TraceBlockByNumber returns the structured logs created during the execution of
|
||||
// EVM and returns them as a JSON object.
|
||||
func (a *API) TraceBlockByNumber(height rpctypes.BlockNumber, config *evmtypes.TraceConfig) ([]*evmtypes.TxTraceResult, error) {
|
||||
a.logger.Debug("debug_traceBlockByNumber", "height", height)
|
||||
if height == 0 {
|
||||
return nil, errors.New("genesis is not traceable")
|
||||
}
|
||||
// Get Tendermint Block
|
||||
resBlock, err := a.backend.GetTendermintBlockByNumber(height)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return a.traceBlock(height, config, resBlock.Block.Txs)
|
||||
}
|
||||
|
||||
// traceBlock configures a new tracer according to the provided configuration, and
|
||||
// executes all the transactions contained within. The return value will be one item
|
||||
// per transaction, dependent on the requested tracer.
|
||||
func (a API) traceBlock(height rpctypes.BlockNumber, config *evmtypes.TraceConfig, txs types.Txs) ([]*evmtypes.TxTraceResult, error) {
|
||||
txsLength := len(txs)
|
||||
|
||||
if txsLength == 0 {
|
||||
// If there are no transactions return empty array
|
||||
return []*evmtypes.TxTraceResult{}, nil
|
||||
}
|
||||
|
||||
var (
|
||||
results = make([]*evmtypes.TxTraceResult, txsLength)
|
||||
wg = new(sync.WaitGroup)
|
||||
jobs = make(chan *evmtypes.TxTraceTask, txsLength)
|
||||
)
|
||||
|
||||
threads := runtime.NumCPU()
|
||||
if threads > txsLength {
|
||||
threads = txsLength
|
||||
}
|
||||
|
||||
ctxWithHeight := rpctypes.ContextWithHeight(int64(height))
|
||||
|
||||
wg.Add(threads)
|
||||
for th := 0; th < threads; th++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
txDecoder := a.clientCtx.TxConfig.TxDecoder()
|
||||
// Fetch and execute the next transaction trace tasks
|
||||
for task := range jobs {
|
||||
tx, err := txDecoder(txs[task.Index])
|
||||
if err != nil {
|
||||
a.logger.Error("failed to decode transaction", "hash", txs[task.Index].Hash(), "error", err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
messages := tx.GetMsgs()
|
||||
if len(messages) == 0 {
|
||||
continue
|
||||
}
|
||||
ethMessage, ok := messages[0].(*evmtypes.MsgEthereumTx)
|
||||
if !ok {
|
||||
// Just considers Ethereum transactions
|
||||
continue
|
||||
}
|
||||
|
||||
traceTxRequest := &evmtypes.QueryTraceTxRequest{
|
||||
Msg: ethMessage,
|
||||
TxIndex: uint64(task.Index),
|
||||
TraceConfig: config,
|
||||
}
|
||||
|
||||
res, err := a.queryClient.TraceTx(ctxWithHeight, traceTxRequest)
|
||||
if err != nil {
|
||||
results[task.Index] = &evmtypes.TxTraceResult{Error: err.Error()}
|
||||
continue
|
||||
}
|
||||
// Response format is unknown due to custom tracer config param
|
||||
// More information can be found here https://geth.ethereum.org/docs/dapp/tracing-filtered
|
||||
var decodedResult interface{}
|
||||
if err := json.Unmarshal(res.Data, &decodedResult); err != nil {
|
||||
results[task.Index] = &evmtypes.TxTraceResult{Error: err.Error()}
|
||||
continue
|
||||
}
|
||||
results[task.Index] = &evmtypes.TxTraceResult{Result: decodedResult}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
for i := range txs {
|
||||
jobs <- &evmtypes.TxTraceTask{Index: i}
|
||||
}
|
||||
|
||||
close(jobs)
|
||||
wg.Wait()
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// BlockProfile turns on goroutine profiling for nsec seconds and writes profile data to
|
||||
// file. It uses a profile rate of 1 for most accurate information. If a different rate is
|
||||
// desired, set the rate and write the profile manually.
|
||||
func (a *API) BlockProfile(file string, nsec uint) error {
|
||||
a.logger.Debug("debug_blockProfile", "file", file, "nsec", nsec)
|
||||
runtime.SetBlockProfileRate(1)
|
||||
defer runtime.SetBlockProfileRate(0)
|
||||
|
||||
time.Sleep(time.Duration(nsec) * time.Second)
|
||||
return writeProfile("block", file, a.logger)
|
||||
}
|
||||
|
||||
// CpuProfile turns on CPU profiling for nsec seconds and writes
|
||||
// profile data to file.
|
||||
func (a *API) CpuProfile(file string, nsec uint) error { // nolint: golint, stylecheck
|
||||
a.logger.Debug("debug_cpuProfile", "file", file, "nsec", nsec)
|
||||
if err := a.StartCPUProfile(file); err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(time.Duration(nsec) * time.Second)
|
||||
return a.StopCPUProfile()
|
||||
}
|
||||
|
||||
// GcStats returns GC statistics.
|
||||
func (a *API) GcStats() *debug.GCStats {
|
||||
a.logger.Debug("debug_gcStats")
|
||||
s := new(debug.GCStats)
|
||||
debug.ReadGCStats(s)
|
||||
return s
|
||||
}
|
||||
|
||||
// GoTrace turns on tracing for nsec seconds and writes
|
||||
// trace data to file.
|
||||
func (a *API) GoTrace(file string, nsec uint) error {
|
||||
a.logger.Debug("debug_goTrace", "file", file, "nsec", nsec)
|
||||
if err := a.StartGoTrace(file); err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(time.Duration(nsec) * time.Second)
|
||||
return a.StopGoTrace()
|
||||
}
|
||||
|
||||
// MemStats returns detailed runtime memory statistics.
|
||||
func (a *API) MemStats() *runtime.MemStats {
|
||||
a.logger.Debug("debug_memStats")
|
||||
s := new(runtime.MemStats)
|
||||
runtime.ReadMemStats(s)
|
||||
return s
|
||||
}
|
||||
|
||||
// SetBlockProfileRate sets the rate of goroutine block profile data collection.
|
||||
// rate 0 disables block profiling.
|
||||
func (a *API) SetBlockProfileRate(rate int) {
|
||||
a.logger.Debug("debug_setBlockProfileRate", "rate", rate)
|
||||
runtime.SetBlockProfileRate(rate)
|
||||
}
|
||||
|
||||
// Stacks returns a printed representation of the stacks of all goroutines.
|
||||
func (a *API) Stacks() string {
|
||||
a.logger.Debug("debug_stacks")
|
||||
buf := new(bytes.Buffer)
|
||||
err := pprof.Lookup("goroutine").WriteTo(buf, 2)
|
||||
if err != nil {
|
||||
a.logger.Error("Failed to create stacks", "error", err.Error())
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// StartCPUProfile turns on CPU profiling, writing to the given file.
|
||||
func (a *API) StartCPUProfile(file string) error {
|
||||
a.logger.Debug("debug_startCPUProfile", "file", file)
|
||||
a.handler.mu.Lock()
|
||||
defer a.handler.mu.Unlock()
|
||||
|
||||
switch {
|
||||
case isCPUProfileConfigurationActivated(a.ctx):
|
||||
a.logger.Debug("CPU profiling already in progress using the configuration file")
|
||||
return errors.New("CPU profiling already in progress using the configuration file")
|
||||
case a.handler.cpuFile != nil:
|
||||
a.logger.Debug("CPU profiling already in progress")
|
||||
return errors.New("CPU profiling already in progress")
|
||||
default:
|
||||
f, err := os.Create(ExpandHome(file))
|
||||
if err != nil {
|
||||
a.logger.Debug("failed to create CPU profile file", "error", err.Error())
|
||||
return err
|
||||
}
|
||||
if err := pprof.StartCPUProfile(f); err != nil {
|
||||
a.logger.Debug("cpu profiling already in use", "error", err.Error())
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
a.logger.Info("CPU profiling started", "profile", file)
|
||||
a.handler.cpuFile = f
|
||||
a.handler.cpuFilename = file
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// StopCPUProfile stops an ongoing CPU profile.
|
||||
func (a *API) StopCPUProfile() error {
|
||||
a.logger.Debug("debug_stopCPUProfile")
|
||||
a.handler.mu.Lock()
|
||||
defer a.handler.mu.Unlock()
|
||||
|
||||
switch {
|
||||
case isCPUProfileConfigurationActivated(a.ctx):
|
||||
a.logger.Debug("CPU profiling already in progress using the configuration file")
|
||||
return errors.New("CPU profiling already in progress using the configuration file")
|
||||
case a.handler.cpuFile != nil:
|
||||
a.logger.Info("Done writing CPU profile", "profile", a.handler.cpuFilename)
|
||||
pprof.StopCPUProfile()
|
||||
a.handler.cpuFile.Close()
|
||||
a.handler.cpuFile = nil
|
||||
a.handler.cpuFilename = ""
|
||||
return nil
|
||||
default:
|
||||
a.logger.Debug("CPU profiling not in progress")
|
||||
return errors.New("CPU profiling not in progress")
|
||||
}
|
||||
}
|
||||
|
||||
// WriteBlockProfile writes a goroutine blocking profile to the given file.
|
||||
func (a *API) WriteBlockProfile(file string) error {
|
||||
a.logger.Debug("debug_writeBlockProfile", "file", file)
|
||||
return writeProfile("block", file, a.logger)
|
||||
}
|
||||
|
||||
// WriteMemProfile writes an allocation profile to the given file.
|
||||
// Note that the profiling rate cannot be set through the API,
|
||||
// it must be set on the command line.
|
||||
func (a *API) WriteMemProfile(file string) error {
|
||||
a.logger.Debug("debug_writeMemProfile", "file", file)
|
||||
return writeProfile("heap", file, a.logger)
|
||||
}
|
||||
|
||||
// MutexProfile turns on mutex profiling for nsec seconds and writes profile data to file.
|
||||
// It uses a profile rate of 1 for most accurate information. If a different rate is
|
||||
// desired, set the rate and write the profile manually.
|
||||
func (a *API) MutexProfile(file string, nsec uint) error {
|
||||
a.logger.Debug("debug_mutexProfile", "file", file, "nsec", nsec)
|
||||
runtime.SetMutexProfileFraction(1)
|
||||
time.Sleep(time.Duration(nsec) * time.Second)
|
||||
defer runtime.SetMutexProfileFraction(0)
|
||||
return writeProfile("mutex", file, a.logger)
|
||||
}
|
||||
|
||||
// SetMutexProfileFraction sets the rate of mutex profiling.
|
||||
func (a *API) SetMutexProfileFraction(rate int) {
|
||||
a.logger.Debug("debug_setMutexProfileFraction", "rate", rate)
|
||||
runtime.SetMutexProfileFraction(rate)
|
||||
}
|
||||
|
||||
// WriteMutexProfile writes a goroutine blocking profile to the given file.
|
||||
func (a *API) WriteMutexProfile(file string) error {
|
||||
a.logger.Debug("debug_writeMutexProfile", "file", file)
|
||||
return writeProfile("mutex", file, a.logger)
|
||||
}
|
||||
|
||||
// FreeOSMemory forces a garbage collection.
|
||||
func (a *API) FreeOSMemory() {
|
||||
a.logger.Debug("debug_freeOSMemory")
|
||||
debug.FreeOSMemory()
|
||||
}
|
||||
|
||||
// SetGCPercent sets the garbage collection target percentage. It returns the previous
|
||||
// setting. A negative value disables GC.
|
||||
func (a *API) SetGCPercent(v int) int {
|
||||
a.logger.Debug("debug_setGCPercent", "percent", v)
|
||||
return debug.SetGCPercent(v)
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
// Copyright 2016 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build go1.5
|
||||
// +build go1.5
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"runtime/trace"
|
||||
)
|
||||
|
||||
// StartGoTrace turns on tracing, writing to the given file.
|
||||
func (a *API) StartGoTrace(file string) error {
|
||||
a.logger.Debug("debug_startGoTrace", "file", file)
|
||||
a.handler.mu.Lock()
|
||||
defer a.handler.mu.Unlock()
|
||||
|
||||
if a.handler.traceFile != nil {
|
||||
a.logger.Debug("trace already in progress")
|
||||
return errors.New("trace already in progress")
|
||||
}
|
||||
f, err := os.Create(ExpandHome(file))
|
||||
if err != nil {
|
||||
a.logger.Debug("failed to create go trace file", "error", err.Error())
|
||||
return err
|
||||
}
|
||||
if err := trace.Start(f); err != nil {
|
||||
a.logger.Debug("Go tracing already started", "error", err.Error())
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
a.handler.traceFile = f
|
||||
a.handler.traceFilename = file
|
||||
a.logger.Info("Go tracing started", "dump", a.handler.traceFilename)
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopGoTrace stops an ongoing trace.
|
||||
func (a *API) StopGoTrace() error {
|
||||
a.logger.Debug("debug_stopGoTrace")
|
||||
a.handler.mu.Lock()
|
||||
defer a.handler.mu.Unlock()
|
||||
|
||||
trace.Stop()
|
||||
if a.handler.traceFile == nil {
|
||||
a.logger.Debug("trace not in progress")
|
||||
return errors.New("trace not in progress")
|
||||
}
|
||||
a.logger.Info("Done writing Go trace", "dump", a.handler.traceFilename)
|
||||
a.handler.traceFile.Close()
|
||||
a.handler.traceFile = nil
|
||||
a.handler.traceFilename = ""
|
||||
return nil
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
// Copyright 2016 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build !go1.5
|
||||
// +build !go1.5
|
||||
|
||||
// no-op implementation of tracing methods for Go < 1.5.
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
func (*API) StartGoTrace(string file) error {
|
||||
a.logger.Debug("debug_stopGoTrace", "file", file)
|
||||
return errors.New("tracing is not supported on Go < 1.5")
|
||||
}
|
||||
|
||||
func (*API) StopGoTrace() error {
|
||||
a.logger.Debug("debug_stopGoTrace")
|
||||
return errors.New("tracing is not supported on Go < 1.5")
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime/pprof"
|
||||
"strings"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/server"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
)
|
||||
|
||||
// isCPUProfileConfigurationActivated checks if cpuprofile was configured via flag
|
||||
func isCPUProfileConfigurationActivated(ctx *server.Context) bool {
|
||||
// TODO: use same constants as server/start.go
|
||||
// constant declared in start.go cannot be imported (cyclical dependency)
|
||||
const flagCPUProfile = "cpu-profile"
|
||||
if cpuProfile := ctx.Viper.GetString(flagCPUProfile); cpuProfile != "" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ExpandHome expands home directory in file paths.
|
||||
// ~someuser/tmp will not be expanded.
|
||||
func ExpandHome(p string) string {
|
||||
if strings.HasPrefix(p, "~/") || strings.HasPrefix(p, "~\\") {
|
||||
home := os.Getenv("HOME")
|
||||
if home == "" {
|
||||
if usr, err := user.Current(); err == nil {
|
||||
home = usr.HomeDir
|
||||
}
|
||||
}
|
||||
if home != "" {
|
||||
p = home + p[1:]
|
||||
}
|
||||
}
|
||||
return filepath.Clean(p)
|
||||
}
|
||||
|
||||
// writeProfile writes the data to a file
|
||||
func writeProfile(name, file string, log log.Logger) error {
|
||||
p := pprof.Lookup(name)
|
||||
log.Info("Writing profile records", "count", p.Count(), "type", name, "dump", file)
|
||||
f, err := os.Create(ExpandHome(file))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
return p.WriteTo(f, 0)
|
||||
}
|
||||
@@ -1,827 +0,0 @@
|
||||
package eth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/core/vm"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/cosmos/cosmos-sdk/client/flags"
|
||||
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
|
||||
"github.com/cosmos/cosmos-sdk/crypto/keyring"
|
||||
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/ethereum/go-ethereum/accounts/keystore"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
ethtypes "github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
|
||||
"github.com/tharsis/ethermint/crypto/hd"
|
||||
"github.com/tharsis/ethermint/ethereum/rpc/backend"
|
||||
rpctypes "github.com/tharsis/ethermint/ethereum/rpc/types"
|
||||
ethermint "github.com/tharsis/ethermint/types"
|
||||
evmtypes "github.com/tharsis/ethermint/x/evm/types"
|
||||
)
|
||||
|
||||
// PublicAPI is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec.
|
||||
type PublicAPI struct {
|
||||
ctx context.Context
|
||||
clientCtx client.Context
|
||||
queryClient *rpctypes.QueryClient
|
||||
chainIDEpoch *big.Int
|
||||
logger log.Logger
|
||||
backend backend.Backend
|
||||
nonceLock *rpctypes.AddrLocker
|
||||
}
|
||||
|
||||
// NewPublicAPI creates an instance of the public ETH Web3 API.
|
||||
func NewPublicAPI(
|
||||
logger log.Logger,
|
||||
clientCtx client.Context,
|
||||
backend backend.Backend,
|
||||
nonceLock *rpctypes.AddrLocker,
|
||||
) *PublicAPI {
|
||||
epoch, err := ethermint.ParseChainID(clientCtx.ChainID)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
algos, _ := clientCtx.Keyring.SupportedAlgorithms()
|
||||
|
||||
if !algos.Contains(hd.EthSecp256k1) {
|
||||
kr, err := keyring.New(
|
||||
sdk.KeyringServiceName(),
|
||||
viper.GetString(flags.FlagKeyringBackend),
|
||||
clientCtx.KeyringDir,
|
||||
clientCtx.Input,
|
||||
hd.EthSecp256k1Option(),
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
clientCtx = clientCtx.WithKeyring(kr)
|
||||
}
|
||||
|
||||
api := &PublicAPI{
|
||||
ctx: context.Background(),
|
||||
clientCtx: clientCtx,
|
||||
queryClient: rpctypes.NewQueryClient(clientCtx),
|
||||
chainIDEpoch: epoch,
|
||||
logger: logger.With("client", "json-rpc"),
|
||||
backend: backend,
|
||||
nonceLock: nonceLock,
|
||||
}
|
||||
|
||||
return api
|
||||
}
|
||||
|
||||
// ClientCtx returns client context
|
||||
func (e *PublicAPI) ClientCtx() client.Context {
|
||||
return e.clientCtx
|
||||
}
|
||||
|
||||
func (e *PublicAPI) QueryClient() *rpctypes.QueryClient {
|
||||
return e.queryClient
|
||||
}
|
||||
|
||||
func (e *PublicAPI) Ctx() context.Context {
|
||||
return e.ctx
|
||||
}
|
||||
|
||||
// ProtocolVersion returns the supported Ethereum protocol version.
|
||||
func (e *PublicAPI) ProtocolVersion() hexutil.Uint {
|
||||
e.logger.Debug("eth_protocolVersion")
|
||||
return hexutil.Uint(ethermint.ProtocolVersion)
|
||||
}
|
||||
|
||||
// ChainId returns the chain's identifier in hex format
|
||||
func (e *PublicAPI) ChainId() (hexutil.Uint, error) { // nolint
|
||||
e.logger.Debug("eth_chainId")
|
||||
return hexutil.Uint(uint(e.chainIDEpoch.Uint64())), nil
|
||||
}
|
||||
|
||||
// Syncing returns whether or not the current node is syncing with other peers. Returns false if not, or a struct
|
||||
// outlining the state of the sync if it is.
|
||||
func (e *PublicAPI) Syncing() (interface{}, error) {
|
||||
e.logger.Debug("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 *PublicAPI) Coinbase() (string, error) {
|
||||
e.logger.Debug("eth_coinbase")
|
||||
|
||||
coinbase, err := e.backend.GetCoinbase()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ethAddr := common.BytesToAddress(coinbase.Bytes())
|
||||
return ethAddr.Hex(), nil
|
||||
}
|
||||
|
||||
// Mining returns whether or not this node is currently mining. Always false.
|
||||
func (e *PublicAPI) Mining() bool {
|
||||
e.logger.Debug("eth_mining")
|
||||
return false
|
||||
}
|
||||
|
||||
// Hashrate returns the current node's hashrate. Always 0.
|
||||
func (e *PublicAPI) Hashrate() hexutil.Uint64 {
|
||||
e.logger.Debug("eth_hashrate")
|
||||
return 0
|
||||
}
|
||||
|
||||
// GasPrice returns the current gas price based on Ethermint's gas price oracle.
|
||||
func (e *PublicAPI) GasPrice() *hexutil.Big {
|
||||
e.logger.Debug("eth_gasPrice")
|
||||
out := new(big.Int).SetInt64(e.backend.RPCMinGasPrice())
|
||||
return (*hexutil.Big)(out)
|
||||
}
|
||||
|
||||
// Accounts returns the list of accounts available to this node.
|
||||
func (e *PublicAPI) Accounts() ([]common.Address, error) {
|
||||
e.logger.Debug("eth_accounts")
|
||||
|
||||
addresses := make([]common.Address, 0) // return [] instead of nil if empty
|
||||
|
||||
infos, err := e.clientCtx.Keyring.List()
|
||||
if err != nil {
|
||||
return addresses, err
|
||||
}
|
||||
|
||||
for _, info := range infos {
|
||||
addressBytes := info.GetPubKey().Address().Bytes()
|
||||
addresses = append(addresses, common.BytesToAddress(addressBytes))
|
||||
}
|
||||
|
||||
return addresses, nil
|
||||
}
|
||||
|
||||
// BlockNumber returns the current block number.
|
||||
func (e *PublicAPI) BlockNumber() (hexutil.Uint64, error) {
|
||||
e.logger.Debug("eth_blockNumber")
|
||||
return e.backend.BlockNumber()
|
||||
}
|
||||
|
||||
// GetBalance returns the provided account's balance up to the provided block number.
|
||||
func (e *PublicAPI) GetBalance(address common.Address, blockNrOrHash rpctypes.BlockNumberOrHash) (*hexutil.Big, error) {
|
||||
e.logger.Debug("eth_getBalance", "address", address.String(), "block number or hash", blockNrOrHash)
|
||||
|
||||
blockNum, err := e.getBlockNumber(blockNrOrHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := &evmtypes.QueryBalanceRequest{
|
||||
Address: address.String(),
|
||||
}
|
||||
|
||||
res, err := e.queryClient.Balance(rpctypes.ContextWithHeight(blockNum.Int64()), req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
val, ok := sdk.NewIntFromString(res.Balance)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid balance")
|
||||
}
|
||||
|
||||
return (*hexutil.Big)(val.BigInt()), nil
|
||||
}
|
||||
|
||||
// GetStorageAt returns the contract storage at the given address, block number, and key.
|
||||
func (e *PublicAPI) GetStorageAt(address common.Address, key string, blockNrOrHash rpctypes.BlockNumberOrHash) (hexutil.Bytes, error) {
|
||||
e.logger.Debug("eth_getStorageAt", "address", address.Hex(), "key", key, "block number or hash", blockNrOrHash)
|
||||
|
||||
blockNum, err := e.getBlockNumber(blockNrOrHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := &evmtypes.QueryStorageRequest{
|
||||
Address: address.String(),
|
||||
Key: key,
|
||||
}
|
||||
|
||||
res, err := e.queryClient.Storage(rpctypes.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 *PublicAPI) GetTransactionCount(address common.Address, blockNrOrHash rpctypes.BlockNumberOrHash) (*hexutil.Uint64, error) {
|
||||
e.logger.Debug("eth_getTransactionCount", "address", address.Hex(), "block number or hash", blockNrOrHash)
|
||||
blockNum, err := e.getBlockNumber(blockNrOrHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return e.backend.GetTransactionCount(address, blockNum)
|
||||
}
|
||||
|
||||
// GetBlockTransactionCountByHash returns the number of transactions in the block identified by hash.
|
||||
func (e *PublicAPI) GetBlockTransactionCountByHash(hash common.Hash) *hexutil.Uint {
|
||||
e.logger.Debug("eth_getBlockTransactionCountByHash", "hash", hash.Hex())
|
||||
|
||||
resBlock, err := e.clientCtx.Client.BlockByHash(e.ctx, hash.Bytes())
|
||||
if err != nil {
|
||||
e.logger.Debug("block not found", "hash", hash.Hex(), "error", err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
if resBlock.Block == nil {
|
||||
e.logger.Debug("block not found", "hash", hash.Hex())
|
||||
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 *PublicAPI) GetBlockTransactionCountByNumber(blockNum rpctypes.BlockNumber) *hexutil.Uint {
|
||||
e.logger.Debug("eth_getBlockTransactionCountByNumber", "height", blockNum.Int64())
|
||||
resBlock, err := e.clientCtx.Client.Block(e.ctx, blockNum.TmHeight())
|
||||
if err != nil {
|
||||
e.logger.Debug("block not found", "height", blockNum.Int64(), "error", err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
if resBlock.Block == nil {
|
||||
e.logger.Debug("block not found", "height", blockNum.Int64())
|
||||
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 *PublicAPI) GetUncleCountByBlockHash(hash common.Hash) hexutil.Uint {
|
||||
return 0
|
||||
}
|
||||
|
||||
// GetUncleCountByBlockNumber returns the number of uncles in the block identified by number. Always zero.
|
||||
func (e *PublicAPI) GetUncleCountByBlockNumber(blockNum rpctypes.BlockNumber) hexutil.Uint {
|
||||
return 0
|
||||
}
|
||||
|
||||
// GetCode returns the contract code at the given address and block number.
|
||||
func (e *PublicAPI) GetCode(address common.Address, blockNrOrHash rpctypes.BlockNumberOrHash) (hexutil.Bytes, error) {
|
||||
e.logger.Debug("eth_getCode", "address", address.Hex(), "block number or hash", blockNrOrHash)
|
||||
|
||||
blockNum, err := e.getBlockNumber(blockNrOrHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := &evmtypes.QueryCodeRequest{
|
||||
Address: address.String(),
|
||||
}
|
||||
|
||||
res, err := e.queryClient.Code(rpctypes.ContextWithHeight(blockNum.Int64()), req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res.Code, nil
|
||||
}
|
||||
|
||||
// GetTransactionLogs returns the logs given a transaction hash.
|
||||
func (e *PublicAPI) GetTransactionLogs(txHash common.Hash) ([]*ethtypes.Log, error) {
|
||||
e.logger.Debug("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 *PublicAPI) Sign(address common.Address, data hexutil.Bytes) (hexutil.Bytes, error) {
|
||||
e.logger.Debug("eth_sign", "address", address.Hex(), "data", common.Bytes2Hex(data))
|
||||
|
||||
from := sdk.AccAddress(address.Bytes())
|
||||
|
||||
_, err := e.clientCtx.Keyring.KeyByAddress(from)
|
||||
if err != nil {
|
||||
e.logger.Error("failed to find key in keyring", "address", address.String())
|
||||
return nil, fmt.Errorf("%s; %s", keystore.ErrNoMatch, err.Error())
|
||||
}
|
||||
|
||||
// Sign the requested hash with the wallet
|
||||
signature, _, err := e.clientCtx.Keyring.SignByAddress(from, data)
|
||||
if err != nil {
|
||||
e.logger.Error("keyring.SignByAddress failed", "address", address.Hex())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signature[64] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper
|
||||
return signature, nil
|
||||
}
|
||||
|
||||
// SendTransaction sends an Ethereum transaction.
|
||||
func (e *PublicAPI) SendTransaction(args rpctypes.SendTxArgs) (common.Hash, error) {
|
||||
e.logger.Debug("eth_sendTransaction", "args", args.String())
|
||||
return e.backend.SendTransaction(args)
|
||||
}
|
||||
|
||||
// SendRawTransaction send a raw Ethereum transaction.
|
||||
func (e *PublicAPI) SendRawTransaction(data hexutil.Bytes) (common.Hash, error) {
|
||||
e.logger.Debug("eth_sendRawTransaction", "length", len(data))
|
||||
|
||||
// RLP decode raw transaction bytes
|
||||
tx, err := e.clientCtx.TxConfig.TxDecoder()(data)
|
||||
if err != nil {
|
||||
e.logger.Error("transaction decoding failed", "error", err.Error())
|
||||
|
||||
return common.Hash{}, err
|
||||
}
|
||||
|
||||
ethereumTx, isEthTx := tx.(*evmtypes.MsgEthereumTx)
|
||||
if !isEthTx {
|
||||
e.logger.Debug("invalid transaction type", "type", fmt.Sprintf("%T", tx))
|
||||
return common.Hash{}, fmt.Errorf("invalid transaction type %T", tx)
|
||||
}
|
||||
|
||||
if err := ethereumTx.ValidateBasic(); err != nil {
|
||||
e.logger.Debug("tx failed basic validation", "error", err.Error())
|
||||
return common.Hash{}, err
|
||||
}
|
||||
|
||||
builder, ok := e.clientCtx.TxConfig.NewTxBuilder().(authtx.ExtensionOptionsTxBuilder)
|
||||
if !ok {
|
||||
e.logger.Error("clientCtx.TxConfig.NewTxBuilder returns unsupported builder")
|
||||
}
|
||||
|
||||
option, err := codectypes.NewAnyWithValue(&evmtypes.ExtensionOptionsEthereumTx{})
|
||||
if err != nil {
|
||||
e.logger.Error("codectypes.NewAnyWithValue failed to pack an obvious value", "error", err.Error())
|
||||
}
|
||||
|
||||
builder.SetExtensionOptions(option)
|
||||
err = builder.SetMsgs(tx.GetMsgs()...)
|
||||
if err != nil {
|
||||
e.logger.Error("builder.SetMsgs failed", "error", err.Error())
|
||||
}
|
||||
|
||||
// Query params to use the EVM denomination
|
||||
res, err := e.queryClient.QueryClient.Params(e.ctx, &evmtypes.QueryParamsRequest{})
|
||||
if err != nil {
|
||||
e.logger.Error("failed to query evm params", "error", err.Error())
|
||||
return common.Hash{}, err
|
||||
}
|
||||
|
||||
txData, err := evmtypes.UnpackTxData(ethereumTx.Data)
|
||||
if err != nil {
|
||||
e.logger.Error("failed to unpack tx data", "error", err.Error())
|
||||
return common.Hash{}, err
|
||||
}
|
||||
|
||||
fees := sdk.Coins{sdk.NewCoin(res.Params.EvmDenom, sdk.NewIntFromBigInt(txData.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.Error("failed to encode eth tx using default encoder", "error", err.Error())
|
||||
return common.Hash{}, err
|
||||
}
|
||||
|
||||
txHash := ethereumTx.AsTransaction().Hash()
|
||||
|
||||
syncCtx := e.clientCtx.WithBroadcastMode(flags.BroadcastSync)
|
||||
rsp, err := syncCtx.BroadcastTx(txBytes)
|
||||
if err != nil || rsp.Code != 0 {
|
||||
if err == nil {
|
||||
err = errors.New(rsp.RawLog)
|
||||
}
|
||||
e.logger.Error("failed to broadcast tx", "error", err.Error())
|
||||
return txHash, err
|
||||
}
|
||||
|
||||
return txHash, nil
|
||||
}
|
||||
|
||||
// Call performs a raw contract call.
|
||||
func (e *PublicAPI) Call(args evmtypes.CallArgs, blockNrOrHash rpctypes.BlockNumberOrHash, _ *rpctypes.StateOverride) (hexutil.Bytes, error) {
|
||||
e.logger.Debug("eth_call", "args", args.String(), "block number or hash", blockNrOrHash)
|
||||
|
||||
blockNum, err := e.getBlockNumber(blockNrOrHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := e.doCall(args, blockNum)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
return (hexutil.Bytes)(data.Ret), nil
|
||||
}
|
||||
|
||||
// DoCall performs a simulated call operation through the evmtypes. It returns the
|
||||
// estimated gas used on the operation or an error if fails.
|
||||
func (e *PublicAPI) doCall(
|
||||
args evmtypes.CallArgs, blockNr rpctypes.BlockNumber,
|
||||
) (*evmtypes.MsgEthereumTxResponse, error) {
|
||||
bz, err := json.Marshal(&args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req := evmtypes.EthCallRequest{Args: bz, GasCap: e.backend.RPCGasCap()}
|
||||
|
||||
// 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.
|
||||
res, err := e.queryClient.EthCall(rpctypes.ContextWithHeight(blockNr.Int64()), &req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if res.Failed() {
|
||||
if res.VmError != vm.ErrExecutionReverted.Error() {
|
||||
return nil, status.Error(codes.Internal, res.VmError)
|
||||
}
|
||||
return nil, evmtypes.NewExecErrorWithReason(res.Ret)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// EstimateGas returns an estimate of gas usage for the given smart contract call.
|
||||
func (e *PublicAPI) EstimateGas(args evmtypes.CallArgs, blockNrOptional *rpctypes.BlockNumber) (hexutil.Uint64, error) {
|
||||
e.logger.Debug("eth_estimateGas")
|
||||
return e.backend.EstimateGas(args, blockNrOptional)
|
||||
}
|
||||
|
||||
// GetBlockByHash returns the block identified by hash.
|
||||
func (e *PublicAPI) GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) {
|
||||
e.logger.Debug("eth_getBlockByHash", "hash", hash.Hex(), "full", fullTx)
|
||||
return e.backend.GetBlockByHash(hash, fullTx)
|
||||
}
|
||||
|
||||
// GetBlockByNumber returns the block identified by number.
|
||||
func (e *PublicAPI) GetBlockByNumber(ethBlockNum rpctypes.BlockNumber, fullTx bool) (map[string]interface{}, error) {
|
||||
e.logger.Debug("eth_getBlockByNumber", "number", ethBlockNum, "full", fullTx)
|
||||
return e.backend.GetBlockByNumber(ethBlockNum, fullTx)
|
||||
}
|
||||
|
||||
// GetTransactionByHash returns the transaction identified by hash.
|
||||
func (e *PublicAPI) GetTransactionByHash(hash common.Hash) (*rpctypes.RPCTransaction, error) {
|
||||
e.logger.Debug("eth_getTransactionByHash", "hash", hash.Hex())
|
||||
return e.backend.GetTransactionByHash(hash)
|
||||
}
|
||||
|
||||
// GetTransactionByBlockHashAndIndex returns the transaction identified by hash and index.
|
||||
func (e *PublicAPI) GetTransactionByBlockHashAndIndex(hash common.Hash, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) {
|
||||
e.logger.Debug("eth_getTransactionByBlockHashAndIndex", "hash", hash.Hex(), "index", idx)
|
||||
|
||||
resBlock, err := e.clientCtx.Client.BlockByHash(e.ctx, hash.Bytes())
|
||||
if err != nil {
|
||||
e.logger.Debug("block not found", "hash", hash.Hex(), "error", err.Error())
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if resBlock.Block == nil {
|
||||
e.logger.Debug("block not found", "hash", hash.Hex())
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
i := int(idx)
|
||||
if i >= len(resBlock.Block.Txs) {
|
||||
e.logger.Debug("block txs index out of bound", "index", i)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
txBz := resBlock.Block.Txs[i]
|
||||
tx, err := e.clientCtx.TxConfig.TxDecoder()(txBz)
|
||||
if err != nil {
|
||||
e.logger.Debug("decoding failed", "error", err.Error())
|
||||
return nil, fmt.Errorf("failed to decode tx: %w", err)
|
||||
}
|
||||
|
||||
msg, err := evmtypes.UnwrapEthereumMsg(&tx)
|
||||
if err != nil {
|
||||
e.logger.Debug("invalid tx", "error", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rpctypes.NewTransactionFromMsg(
|
||||
msg,
|
||||
hash,
|
||||
uint64(resBlock.Block.Height),
|
||||
uint64(idx),
|
||||
e.chainIDEpoch,
|
||||
)
|
||||
}
|
||||
|
||||
// GetTransactionByBlockNumberAndIndex returns the transaction identified by number and index.
|
||||
func (e *PublicAPI) GetTransactionByBlockNumberAndIndex(blockNum rpctypes.BlockNumber, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) {
|
||||
e.logger.Debug("eth_getTransactionByBlockNumberAndIndex", "number", blockNum, "index", idx)
|
||||
|
||||
resBlock, err := e.clientCtx.Client.Block(e.ctx, blockNum.TmHeight())
|
||||
if err != nil {
|
||||
e.logger.Debug("block not found", "height", blockNum.Int64(), "error", err.Error())
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if resBlock.Block == nil {
|
||||
e.logger.Debug("block not found", "height", blockNum.Int64())
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
i := int(idx)
|
||||
if i >= len(resBlock.Block.Txs) {
|
||||
e.logger.Debug("block txs index out of bound", "index", i)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
txBz := resBlock.Block.Txs[i]
|
||||
tx, err := e.clientCtx.TxConfig.TxDecoder()(txBz)
|
||||
if err != nil {
|
||||
e.logger.Debug("decoding failed", "error", err.Error())
|
||||
return nil, fmt.Errorf("failed to decode tx: %w", err)
|
||||
}
|
||||
|
||||
msg, err := evmtypes.UnwrapEthereumMsg(&tx)
|
||||
if err != nil {
|
||||
e.logger.Debug("invalid tx", "error", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rpctypes.NewTransactionFromMsg(
|
||||
msg,
|
||||
common.BytesToHash(resBlock.Block.Hash()),
|
||||
uint64(resBlock.Block.Height),
|
||||
uint64(idx),
|
||||
e.chainIDEpoch,
|
||||
)
|
||||
}
|
||||
|
||||
// GetTransactionReceipt returns the transaction receipt identified by hash.
|
||||
func (e *PublicAPI) GetTransactionReceipt(hash common.Hash) (map[string]interface{}, error) {
|
||||
e.logger.Debug("eth_getTransactionReceipt", "hash", hash.Hex())
|
||||
|
||||
res, err := e.backend.GetTxByEthHash(hash)
|
||||
if err != nil {
|
||||
e.logger.Debug("tx not found", "hash", hash.Hex(), "error", err.Error())
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
resBlock, err := e.clientCtx.Client.Block(e.ctx, &res.Height)
|
||||
if err != nil {
|
||||
e.logger.Debug("block not found", "height", res.Height, "error", err.Error())
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
tx, err := e.clientCtx.TxConfig.TxDecoder()(res.Tx)
|
||||
if err != nil {
|
||||
e.logger.Debug("decoding failed", "error", err.Error())
|
||||
return nil, fmt.Errorf("failed to decode tx: %w", err)
|
||||
}
|
||||
|
||||
msg, err := evmtypes.UnwrapEthereumMsg(&tx)
|
||||
if err != nil {
|
||||
e.logger.Debug("invalid tx", "error", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
txData, err := evmtypes.UnpackTxData(msg.Data)
|
||||
if err != nil {
|
||||
e.logger.Error("failed to unpack tx data", "error", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cumulativeGasUsed := uint64(0)
|
||||
blockRes, err := e.clientCtx.Client.BlockResults(e.ctx, &res.Height)
|
||||
if err != nil {
|
||||
e.logger.Debug("failed to retrieve block results", "height", res.Height, "error", err.Error())
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
for i := 0; i <= int(res.Index) && i < len(blockRes.TxsResults); i++ {
|
||||
cumulativeGasUsed += uint64(blockRes.TxsResults[i].GasUsed)
|
||||
}
|
||||
|
||||
// Get the transaction result from the log
|
||||
var status hexutil.Uint
|
||||
if strings.Contains(res.TxResult.GetLog(), evmtypes.AttributeKeyEthereumTxFailed) {
|
||||
status = hexutil.Uint(ethtypes.ReceiptStatusFailed)
|
||||
} else {
|
||||
status = hexutil.Uint(ethtypes.ReceiptStatusSuccessful)
|
||||
}
|
||||
|
||||
from, err := msg.GetSender(e.chainIDEpoch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logs, err := e.backend.GetTransactionLogs(hash)
|
||||
if err != nil {
|
||||
e.logger.Debug("logs not found", "hash", hash.Hex(), "error", err.Error())
|
||||
}
|
||||
|
||||
receipt := map[string]interface{}{
|
||||
// Consensus fields: These fields are defined by the Yellow Paper
|
||||
"status": status,
|
||||
"cumulativeGasUsed": hexutil.Uint64(cumulativeGasUsed),
|
||||
"logsBloom": ethtypes.BytesToBloom(ethtypes.LogsBloom(logs)),
|
||||
"logs": logs,
|
||||
|
||||
// Implementation fields: These fields are added by geth when processing a transaction.
|
||||
// They are stored in the chain database.
|
||||
"transactionHash": hash,
|
||||
"contractAddress": nil,
|
||||
"gasUsed": hexutil.Uint64(res.TxResult.GasUsed),
|
||||
"type": hexutil.Uint(txData.TxType()),
|
||||
|
||||
// Inclusion information: These fields provide information about the inclusion of the
|
||||
// transaction corresponding to this receipt.
|
||||
"blockHash": common.BytesToHash(resBlock.Block.Header.Hash()).Hex(),
|
||||
"blockNumber": hexutil.Uint64(res.Height),
|
||||
"transactionIndex": hexutil.Uint64(res.Index),
|
||||
|
||||
// sender and receiver (contract or EOA) addreses
|
||||
"from": from,
|
||||
"to": txData.GetTo(),
|
||||
}
|
||||
|
||||
if logs == nil {
|
||||
receipt["logs"] = [][]*ethtypes.Log{}
|
||||
}
|
||||
|
||||
// If the ContractAddress is 20 0x0 bytes, assume it is not a contract creation
|
||||
if txData.GetTo() == nil {
|
||||
receipt["contractAddress"] = crypto.CreateAddress(from, txData.GetNonce())
|
||||
}
|
||||
|
||||
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 *PublicAPI) PendingTransactions() ([]*rpctypes.RPCTransaction, error) {
|
||||
e.logger.Debug("eth_getPendingTransactions")
|
||||
|
||||
txs, err := e.backend.PendingTransactions()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]*rpctypes.RPCTransaction, 0, len(txs))
|
||||
for _, tx := range txs {
|
||||
msg, err := evmtypes.UnwrapEthereumMsg(tx)
|
||||
if err != nil {
|
||||
// not valid ethereum tx
|
||||
continue
|
||||
}
|
||||
|
||||
rpctx, err := rpctypes.NewTransactionFromMsg(
|
||||
msg,
|
||||
common.Hash{},
|
||||
uint64(0),
|
||||
uint64(0),
|
||||
e.chainIDEpoch,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result = append(result, rpctx)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetUncleByBlockHashAndIndex returns the uncle identified by hash and index. Always returns nil.
|
||||
func (e *PublicAPI) 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 *PublicAPI) GetUncleByBlockNumberAndIndex(number, idx hexutil.Uint) map[string]interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetProof returns an account object with proof and any storage proofs
|
||||
func (e *PublicAPI) GetProof(address common.Address, storageKeys []string, blockNrOrHash rpctypes.BlockNumberOrHash) (*rpctypes.AccountResult, error) {
|
||||
e.logger.Debug("eth_getProof", "address", address.Hex(), "keys", storageKeys, "block number or hash", blockNrOrHash)
|
||||
|
||||
blockNum, err := e.getBlockNumber(blockNrOrHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
height := blockNum.Int64()
|
||||
|
||||
ctx := rpctypes.ContextWithHeight(height)
|
||||
clientCtx := e.clientCtx.WithHeight(height)
|
||||
|
||||
// query storage proofs
|
||||
storageProofs := make([]rpctypes.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] = rpctypes.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, ok := sdk.NewIntFromString(res.Balance)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid balance")
|
||||
}
|
||||
|
||||
return &rpctypes.AccountResult{
|
||||
Address: address,
|
||||
AccountProof: []string{accProofStr},
|
||||
Balance: (*hexutil.Big)(balance.BigInt()),
|
||||
CodeHash: common.HexToHash(res.CodeHash),
|
||||
Nonce: hexutil.Uint64(res.Nonce),
|
||||
StorageHash: common.Hash{}, // NOTE: Ethermint doesn't have a storage hash. TODO: implement?
|
||||
StorageProof: storageProofs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getBlockNumber returns the BlockNumber from BlockNumberOrHash
|
||||
func (e *PublicAPI) getBlockNumber(blockNrOrHash rpctypes.BlockNumberOrHash) (rpctypes.BlockNumber, error) {
|
||||
switch {
|
||||
case blockNrOrHash.BlockHash == nil && blockNrOrHash.BlockNumber == nil:
|
||||
return rpctypes.EthEarliestBlockNumber, fmt.Errorf("types BlockHash and BlockNumber cannot be both nil")
|
||||
case blockNrOrHash.BlockHash != nil:
|
||||
blockHeader, err := e.backend.HeaderByHash(*blockNrOrHash.BlockHash)
|
||||
if err != nil {
|
||||
return rpctypes.EthEarliestBlockNumber, err
|
||||
}
|
||||
return rpctypes.NewBlockNumber(blockHeader.Number), nil
|
||||
case blockNrOrHash.BlockNumber != nil:
|
||||
return *blockNrOrHash.BlockNumber, nil
|
||||
default:
|
||||
return rpctypes.EthEarliestBlockNumber, nil
|
||||
}
|
||||
}
|
||||
@@ -1,584 +0,0 @@
|
||||
package filters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tharsis/ethermint/ethereum/rpc/types"
|
||||
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
|
||||
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/tharsis/ethermint/x/evm/types"
|
||||
)
|
||||
|
||||
// Backend defines the methods requided by the PublicFilterAPI backend
|
||||
type Backend 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)
|
||||
|
||||
GetFilteredBlocks(from int64, to int64, bloomIndexes [][]BloomIV, filterAddresses bool) ([]int64, error)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
logger log.Logger
|
||||
backend Backend
|
||||
events *EventSystem
|
||||
filtersMu sync.Mutex
|
||||
filters map[rpc.ID]*filter
|
||||
}
|
||||
|
||||
// NewPublicAPI returns a new PublicFilterAPI instance.
|
||||
func NewPublicAPI(logger log.Logger, tmWSClient *rpcclient.WSClient, backend Backend) *PublicFilterAPI {
|
||||
logger = logger.With("api", "filter")
|
||||
api := &PublicFilterAPI{
|
||||
logger: logger,
|
||||
backend: backend,
|
||||
filters: make(map[rpc.ID]*filter),
|
||||
events: NewEventSystem(logger, 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 {
|
||||
api.logger.Debug("event data type mismatch", "type", fmt.Sprintf("%T", ev.Data))
|
||||
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 {
|
||||
api.logger.Debug("event data type mismatch", "type", fmt.Sprintf("%T", ev.Data))
|
||||
continue
|
||||
}
|
||||
|
||||
txHash := common.BytesToHash(tmtypes.Tx(data.Tx).Hash())
|
||||
|
||||
// To keep the original behavior, 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 {
|
||||
api.logger.Debug("event data type mismatch", "type", fmt.Sprintf("%T", ev.Data))
|
||||
continue
|
||||
}
|
||||
|
||||
header := types.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 {
|
||||
api.logger.Debug("event data type mismatch", "type", fmt.Sprintf("%T", ev.Data))
|
||||
continue
|
||||
}
|
||||
|
||||
header := types.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 {
|
||||
api.logger.Debug("event data type mismatch", "type", fmt.Sprintf("%T", ev.Data))
|
||||
continue
|
||||
}
|
||||
|
||||
txResponse, err := evmtypes.DecodeTxResponse(dataTx.TxResult.Result.Data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
logs := FilterLogs(evmtypes.LogsToEthereum(txResponse.Logs), crit.FromBlock, crit.ToBlock, crit.Addresses, crit.Topics)
|
||||
|
||||
for _, log := range logs {
|
||||
err = notifier.Notify(rpcSub.ID, log)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
case <-rpcSub.Err(): // client send an unsubscribe request
|
||||
logsSub.Unsubscribe(api.events)
|
||||
return
|
||||
case <-notifier.Closed(): // connection dropped
|
||||
logsSub.Unsubscribe(api.events)
|
||||
return
|
||||
}
|
||||
}
|
||||
}(logsSub.eventCh)
|
||||
|
||||
return rpcSub, err
|
||||
}
|
||||
|
||||
// NewFilter creates a new filter and returns the filter id. It can be
|
||||
// used to retrieve logs when the state changes. This method cannot be
|
||||
// used to fetch logs that are already stored in the state.
|
||||
//
|
||||
// Default criteria for the from and to block are "latest".
|
||||
// Using "latest" as block number will return logs for mined blocks.
|
||||
// Using "pending" as block number returns logs for not yet mined (pending) blocks.
|
||||
// In case logs are removed (chain reorg) previously returned logs are returned
|
||||
// again but with the removed property set to true.
|
||||
//
|
||||
// In case "fromBlock" > "toBlock" an error is returned.
|
||||
//
|
||||
// https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_newfilter
|
||||
func (api *PublicFilterAPI) NewFilter(criteria filters.FilterCriteria) (rpc.ID, error) {
|
||||
var (
|
||||
filterID = rpc.ID("")
|
||||
err error
|
||||
)
|
||||
|
||||
logsSub, cancelSubs, err := api.events.SubscribeLogs(criteria)
|
||||
if err != nil {
|
||||
return rpc.ID(""), err
|
||||
}
|
||||
|
||||
filterID = logsSub.ID()
|
||||
|
||||
api.filtersMu.Lock()
|
||||
api.filters[filterID] = &filter{typ: filters.LogsSubscription, deadline: time.NewTimer(deadline), hashes: []common.Hash{}, s: logsSub}
|
||||
api.filtersMu.Unlock()
|
||||
|
||||
go func(eventCh <-chan coretypes.ResultEvent) {
|
||||
defer cancelSubs()
|
||||
|
||||
for {
|
||||
select {
|
||||
case ev, ok := <-eventCh:
|
||||
if !ok {
|
||||
api.filtersMu.Lock()
|
||||
delete(api.filters, filterID)
|
||||
api.filtersMu.Unlock()
|
||||
return
|
||||
}
|
||||
dataTx, ok := ev.Data.(tmtypes.EventDataTx)
|
||||
if !ok {
|
||||
api.logger.Debug("event data type mismatch", "type", fmt.Sprintf("%T", ev.Data))
|
||||
continue
|
||||
}
|
||||
|
||||
txResponse, err := evmtypes.DecodeTxResponse(dataTx.TxResult.Result.Data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
logs := FilterLogs(evmtypes.LogsToEthereum(txResponse.Logs), criteria.FromBlock, criteria.ToBlock, criteria.Addresses, criteria.Topics)
|
||||
|
||||
api.filtersMu.Lock()
|
||||
if f, found := api.filters[filterID]; found {
|
||||
f.logs = append(f.logs, logs...)
|
||||
}
|
||||
api.filtersMu.Unlock()
|
||||
case <-logsSub.Err():
|
||||
api.filtersMu.Lock()
|
||||
delete(api.filters, filterID)
|
||||
api.filtersMu.Unlock()
|
||||
return
|
||||
}
|
||||
}
|
||||
}(logsSub.eventCh)
|
||||
|
||||
return filterID, err
|
||||
}
|
||||
|
||||
// GetLogs returns logs matching the given argument that are stored within the state.
|
||||
//
|
||||
// https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getlogs
|
||||
func (api *PublicFilterAPI) GetLogs(ctx context.Context, crit filters.FilterCriteria) ([]*ethtypes.Log, error) {
|
||||
var filter *Filter
|
||||
if crit.BlockHash != nil {
|
||||
// Block filter requested, construct a single-shot filter
|
||||
filter = NewBlockFilter(api.logger, 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.logger, 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.logger, 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.logger, 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)
|
||||
}
|
||||
}
|
||||
@@ -1,301 +0,0 @@
|
||||
package filters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
tmjson "github.com/tendermint/tendermint/libs/json"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
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/tharsis/ethermint/ethereum/rpc/pubsub"
|
||||
evmtypes "github.com/tharsis/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 {
|
||||
logger log.Logger
|
||||
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(logger log.Logger, tmWSClient *rpcclient.WSClient) *EventSystem {
|
||||
index := make(filterIndex)
|
||||
for i := filters.UnknownSubscription; i < filters.LastIndexSubscription; i++ {
|
||||
index[i] = make(map[rpc.ID]*Subscription)
|
||||
}
|
||||
|
||||
es := &EventSystem{
|
||||
logger: logger,
|
||||
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 {
|
||||
es.logger.Error("failed to add event topic to event bus", "topic", f.event, "error", err.Error())
|
||||
}
|
||||
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 {
|
||||
es.logger.Error("failed to unsubscribe from query", "query", f.event, "error", err.Error())
|
||||
}
|
||||
|
||||
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 {
|
||||
es.logger.Error("failed to JSON unmarshal ResponsesCh result event", "error", err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
if len(ev.Query) == 0 {
|
||||
// skip empty responses
|
||||
continue
|
||||
}
|
||||
|
||||
es.indexMux.RLock()
|
||||
ch, ok := es.topicChans[ev.Query]
|
||||
es.indexMux.RUnlock()
|
||||
if !ok {
|
||||
es.logger.Debug("channel for subscription not found", "topic", ev.Query)
|
||||
es.logger.Debug("list of available channels", "channels", es.eventBus.Topics())
|
||||
continue
|
||||
}
|
||||
|
||||
// gracefully handle lagging subscribers
|
||||
t := time.NewTimer(time.Second)
|
||||
select {
|
||||
case <-t.C:
|
||||
es.logger.Debug("dropped event during lagging subscription", "topic", ev.Query)
|
||||
case ch <- ev:
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
@@ -1,249 +0,0 @@
|
||||
package filters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"math/big"
|
||||
|
||||
"github.com/tharsis/ethermint/ethereum/rpc/types"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
ethtypes "github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/eth/filters"
|
||||
)
|
||||
|
||||
// BloomIV represents the bit indexes and value inside the bloom filter that belong
|
||||
// to some key.
|
||||
type BloomIV struct {
|
||||
I [3]uint
|
||||
V [3]byte
|
||||
}
|
||||
|
||||
// Filter can be used to retrieve and filter logs.
|
||||
type Filter struct {
|
||||
logger log.Logger
|
||||
backend Backend
|
||||
criteria filters.FilterCriteria
|
||||
|
||||
bloomFilters [][]BloomIV // Filter the system is matching for
|
||||
}
|
||||
|
||||
// NewBlockFilter creates a new filter which directly inspects the contents of
|
||||
// a block to figure out whether it is interesting or not.
|
||||
func NewBlockFilter(logger log.Logger, backend Backend, criteria filters.FilterCriteria) *Filter {
|
||||
// Create a generic filter and convert it into a block filter
|
||||
return newFilter(logger, 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(logger log.Logger, backend Backend, begin, end int64, addresses []common.Address, topics [][]common.Hash) *Filter {
|
||||
// Flatten the address and topic filter clauses into a single bloombits filter
|
||||
// system. Since the bloombits are not positional, nil topics are permitted,
|
||||
// which get flattened into a nil byte slice.
|
||||
var filtersBz [][][]byte // nolint: prealloc
|
||||
if len(addresses) > 0 {
|
||||
filter := make([][]byte, len(addresses))
|
||||
for i, address := range addresses {
|
||||
filter[i] = address.Bytes()
|
||||
}
|
||||
filtersBz = append(filtersBz, filter)
|
||||
}
|
||||
|
||||
for _, topicList := range topics {
|
||||
filter := make([][]byte, len(topicList))
|
||||
for i, topic := range topicList {
|
||||
filter[i] = topic.Bytes()
|
||||
}
|
||||
filtersBz = append(filtersBz, filter)
|
||||
}
|
||||
|
||||
// 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(logger, backend, criteria, createBloomFilters(filtersBz, logger))
|
||||
}
|
||||
|
||||
// newFilter returns a new Filter
|
||||
func newFilter(logger log.Logger, backend Backend, criteria filters.FilterCriteria, bloomFilters [][]BloomIV) *Filter {
|
||||
return &Filter{
|
||||
logger: logger,
|
||||
backend: backend,
|
||||
criteria: criteria,
|
||||
bloomFilters: bloomFilters,
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil, errors.Wrap(err, "failed to fetch header by hash")
|
||||
}
|
||||
|
||||
if header == nil {
|
||||
return nil, errors.Errorf("unknown block header %s", f.criteria.BlockHash.String())
|
||||
}
|
||||
|
||||
return f.blockLogs(header)
|
||||
}
|
||||
|
||||
// Figure out the limits of the filter range
|
||||
header, err := f.backend.HeaderByNumber(types.EthLatestBlockNumber)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to fetch header by number (latest)")
|
||||
}
|
||||
|
||||
if header == nil || header.Number == nil {
|
||||
f.logger.Debug("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 {
|
||||
return nil, errors.Errorf("maximum [from, to] blocks distance: %d", maxFilterBlocks)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
from := f.criteria.FromBlock.Int64()
|
||||
to := f.criteria.ToBlock.Int64()
|
||||
|
||||
blocks, err := f.backend.GetFilteredBlocks(from, to, f.bloomFilters, len(f.criteria.Addresses) > 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, height := range blocks {
|
||||
ethLogs, err := f.backend.GetLogsByNumber(types.BlockNumber(height))
|
||||
if err != nil {
|
||||
return logs, errors.Wrapf(err, "failed to fetch block by number %d", height)
|
||||
}
|
||||
|
||||
for _, ethLog := range ethLogs {
|
||||
filtered := FilterLogs(ethLog, f.criteria.FromBlock, f.criteria.ToBlock, f.criteria.Addresses, f.criteria.Topics)
|
||||
logs = append(logs, filtered...)
|
||||
}
|
||||
}
|
||||
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 {
|
||||
return []*ethtypes.Log{}, errors.Wrapf(err, "failed to fetch logs block number %d", header.Number.Int64())
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func createBloomFilters(filters [][][]byte, logger log.Logger) [][]BloomIV {
|
||||
bloomFilters := make([][]BloomIV, 0)
|
||||
for _, filter := range filters {
|
||||
// Gather the bit indexes of the filter rule, special casing the nil filter
|
||||
if len(filter) == 0 {
|
||||
continue
|
||||
}
|
||||
bloomIVs := make([]BloomIV, len(filter))
|
||||
|
||||
// Transform the filter rules (the addresses and topics) to the bloom index and value arrays
|
||||
// So it can be used to compare with the bloom of the block header. If the rule has any nil
|
||||
// clauses. The rule will be ignored.
|
||||
for i, clause := range filter {
|
||||
if clause == nil {
|
||||
bloomIVs = nil
|
||||
break
|
||||
}
|
||||
|
||||
iv, err := calcBloomIVs(clause)
|
||||
if err != nil {
|
||||
bloomIVs = nil
|
||||
logger.Error("calcBloomIVs error: %v", err)
|
||||
break
|
||||
}
|
||||
|
||||
bloomIVs[i] = iv
|
||||
}
|
||||
// Accumulate the filter rules if no nil rule was within
|
||||
if bloomIVs != nil {
|
||||
bloomFilters = append(bloomFilters, bloomIVs)
|
||||
}
|
||||
}
|
||||
return bloomFilters
|
||||
}
|
||||
|
||||
// calcBloomIVs returns BloomIV for the given data,
|
||||
// revised from https://github.com/ethereum/go-ethereum/blob/401354976bb44f0ad4455ca1e0b5c0dc31d9a5f5/core/types/bloom9.go#L139
|
||||
func calcBloomIVs(data []byte) (BloomIV, error) {
|
||||
hashbuf := make([]byte, 6)
|
||||
biv := BloomIV{}
|
||||
|
||||
sha := crypto.NewKeccakState()
|
||||
sha.Reset()
|
||||
if _, err := sha.Write(data); err != nil {
|
||||
return BloomIV{}, err
|
||||
}
|
||||
if _, err := sha.Read(hashbuf); err != nil {
|
||||
return BloomIV{}, err
|
||||
}
|
||||
|
||||
// The actual bits to flip
|
||||
biv.V[0] = byte(1 << (hashbuf[1] & 0x7))
|
||||
biv.V[1] = byte(1 << (hashbuf[3] & 0x7))
|
||||
biv.V[2] = byte(1 << (hashbuf[5] & 0x7))
|
||||
// The indices for the bytes to OR in
|
||||
biv.I[0] = ethtypes.BloomByteLength - uint((binary.BigEndian.Uint16(hashbuf)&0x7ff)>>3) - 1
|
||||
biv.I[1] = ethtypes.BloomByteLength - uint((binary.BigEndian.Uint16(hashbuf[2:])&0x7ff)>>3) - 1
|
||||
biv.I[2] = ethtypes.BloomByteLength - uint((binary.BigEndian.Uint16(hashbuf[4:])&0x7ff)>>3) - 1
|
||||
|
||||
return biv, nil
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package filters
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
ethtypes "github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/eth/filters"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
coretypes "github.com/tendermint/tendermint/rpc/core/types"
|
||||
)
|
||||
|
||||
// Subscription defines a wrapper for the private subscription
|
||||
type Subscription struct {
|
||||
id rpc.ID
|
||||
typ filters.Type
|
||||
event string
|
||||
created time.Time
|
||||
logsCrit filters.FilterCriteria
|
||||
logs chan []*ethtypes.Log
|
||||
hashes chan []common.Hash
|
||||
headers chan *ethtypes.Header
|
||||
installed chan struct{} // closed when the filter is installed
|
||||
eventCh <-chan coretypes.ResultEvent
|
||||
err chan error
|
||||
}
|
||||
|
||||
// ID returns the underlying subscription RPC identifier.
|
||||
func (s Subscription) ID() rpc.ID {
|
||||
return s.id
|
||||
}
|
||||
|
||||
// Unsubscribe 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
|
||||
}
|
||||
|
||||
// Event returns the tendermint result event channel
|
||||
func (s *Subscription) Event() <-chan coretypes.ResultEvent {
|
||||
return s.eventCh
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
package filters
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
ethtypes "github.com/ethereum/go-ethereum/core/types"
|
||||
)
|
||||
|
||||
// FilterLogs creates a slice of logs matching the given criteria.
|
||||
// [] -> anything
|
||||
// [A] -> A in first position of log topics, anything after
|
||||
// [null, B] -> anything in first position, B in second position
|
||||
// [A, B] -> A in first position and B in second position
|
||||
// [[A, B], [A, B]] -> A or B in first position, A or B in second position
|
||||
func FilterLogs(logs []*ethtypes.Log, fromBlock, toBlock *big.Int, addresses []common.Address, topics [][]common.Hash) []*ethtypes.Log {
|
||||
var ret []*ethtypes.Log
|
||||
Logs:
|
||||
for _, log := range logs {
|
||||
if fromBlock != nil && fromBlock.Int64() >= 0 && fromBlock.Uint64() > log.BlockNumber {
|
||||
continue
|
||||
}
|
||||
if toBlock != nil && toBlock.Int64() >= 0 && toBlock.Uint64() < log.BlockNumber {
|
||||
continue
|
||||
}
|
||||
if len(addresses) > 0 && !includes(addresses, log.Address) {
|
||||
continue
|
||||
}
|
||||
// If the to filtered topics is greater than the amount of topics in logs, skip.
|
||||
if len(topics) > len(log.Topics) {
|
||||
continue
|
||||
}
|
||||
for i, sub := range topics {
|
||||
match := len(sub) == 0 // empty rule set == wildcard
|
||||
for _, topic := range sub {
|
||||
if log.Topics[i] == topic {
|
||||
match = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
continue Logs
|
||||
}
|
||||
}
|
||||
ret = append(ret, log)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func includes(addresses []common.Address, a common.Address) bool {
|
||||
for _, addr := range addresses {
|
||||
if addr == a {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func bloomFilter(bloom ethtypes.Bloom, addresses []common.Address, topics [][]common.Hash) bool {
|
||||
var included bool
|
||||
if len(addresses) > 0 {
|
||||
for _, addr := range addresses {
|
||||
if ethtypes.BloomLookup(bloom, addr) {
|
||||
included = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !included {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for _, sub := range topics {
|
||||
included = len(sub) == 0 // empty rule set == wildcard
|
||||
for _, topic := range sub {
|
||||
if ethtypes.BloomLookup(bloom, topic) {
|
||||
included = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return included
|
||||
}
|
||||
|
||||
// returnHashes is a helper that will return an empty hash array case the given hash array is nil,
|
||||
// otherwise the given hashes array is returned.
|
||||
func returnHashes(hashes []common.Hash) []common.Hash {
|
||||
if hashes == nil {
|
||||
return []common.Hash{}
|
||||
}
|
||||
return hashes
|
||||
}
|
||||
|
||||
// returnLogs is a helper that will return an empty log array in case the given logs array is nil,
|
||||
// otherwise the given logs array is returned.
|
||||
func returnLogs(logs []*ethtypes.Log) []*ethtypes.Log {
|
||||
if logs == nil {
|
||||
return []*ethtypes.Log{}
|
||||
}
|
||||
return logs
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
package miner
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math/big"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client/flags"
|
||||
"github.com/cosmos/cosmos-sdk/client/tx"
|
||||
"github.com/cosmos/cosmos-sdk/server"
|
||||
sdkconfig "github.com/cosmos/cosmos-sdk/server/config"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
|
||||
authtx "github.com/cosmos/cosmos-sdk/x/auth/tx"
|
||||
distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
tmtypes "github.com/tendermint/tendermint/types"
|
||||
|
||||
"github.com/tharsis/ethermint/ethereum/rpc/backend"
|
||||
rpctypes "github.com/tharsis/ethermint/ethereum/rpc/types"
|
||||
"github.com/tharsis/ethermint/server/config"
|
||||
)
|
||||
|
||||
// API is the private miner prefixed set of APIs in the Miner JSON-RPC spec.
|
||||
type API struct {
|
||||
ctx *server.Context
|
||||
logger log.Logger
|
||||
clientCtx client.Context
|
||||
backend backend.Backend
|
||||
}
|
||||
|
||||
// NewPrivateAPI creates an instance of the Miner API.
|
||||
func NewPrivateAPI(
|
||||
ctx *server.Context,
|
||||
clientCtx client.Context,
|
||||
backend backend.Backend,
|
||||
) *API {
|
||||
return &API{
|
||||
ctx: ctx,
|
||||
clientCtx: clientCtx,
|
||||
logger: ctx.Logger.With("api", "miner"),
|
||||
backend: backend,
|
||||
}
|
||||
}
|
||||
|
||||
// SetEtherbase sets the etherbase of the miner
|
||||
func (api *API) SetEtherbase(etherbase common.Address) bool {
|
||||
api.logger.Debug("miner_setEtherbase")
|
||||
|
||||
delAddr, err := api.backend.GetCoinbase()
|
||||
if err != nil {
|
||||
api.logger.Debug("failed to get coinbase address", "error", err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
withdrawAddr := sdk.AccAddress(etherbase.Bytes())
|
||||
msg := distributiontypes.NewMsgSetWithdrawAddress(delAddr, withdrawAddr)
|
||||
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
api.logger.Debug("tx failed basic validation", "error", err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
// Assemble transaction from fields
|
||||
builder, ok := api.clientCtx.TxConfig.NewTxBuilder().(authtx.ExtensionOptionsTxBuilder)
|
||||
if !ok {
|
||||
api.logger.Debug("clientCtx.TxConfig.NewTxBuilder returns unsupported builder", "error", err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
err = builder.SetMsgs(msg)
|
||||
if err != nil {
|
||||
api.logger.Error("builder.SetMsgs failed", "error", err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
// Fetch minimun gas price to calculate fees using the configuration.
|
||||
appConf := config.GetConfig(api.ctx.Viper)
|
||||
|
||||
minGasPrices := appConf.GetMinGasPrices()
|
||||
if len(minGasPrices) == 0 || minGasPrices.Empty() {
|
||||
api.logger.Debug("the minimun fee is not set")
|
||||
return false
|
||||
}
|
||||
minGasPriceValue := minGasPrices[0].Amount
|
||||
denom := minGasPrices[0].Denom
|
||||
|
||||
delCommonAddr := common.BytesToAddress(delAddr.Bytes())
|
||||
nonce, err := api.backend.GetTransactionCount(delCommonAddr, rpctypes.EthPendingBlockNumber)
|
||||
if err != nil {
|
||||
api.logger.Debug("failed to get nonce", "error", err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
txFactory := tx.Factory{}
|
||||
txFactory = txFactory.
|
||||
WithChainID(api.clientCtx.ChainID).
|
||||
WithKeybase(api.clientCtx.Keyring).
|
||||
WithTxConfig(api.clientCtx.TxConfig).
|
||||
WithSequence(uint64(*nonce)).
|
||||
WithGasAdjustment(1.25)
|
||||
|
||||
_, gas, err := tx.CalculateGas(api.clientCtx, txFactory, msg)
|
||||
if err != nil {
|
||||
api.logger.Debug("failed to calculate gas", "error", err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
txFactory = txFactory.WithGas(gas)
|
||||
|
||||
value := new(big.Int).SetUint64(gas * minGasPriceValue.Ceil().TruncateInt().Uint64())
|
||||
fees := sdk.Coins{sdk.NewCoin(denom, sdk.NewIntFromBigInt(value))}
|
||||
builder.SetFeeAmount(fees)
|
||||
builder.SetGasLimit(gas)
|
||||
|
||||
keyInfo, err := api.clientCtx.Keyring.KeyByAddress(delAddr)
|
||||
if err != nil {
|
||||
api.logger.Debug("failed to get the wallet address using the keyring", "error", err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
if err := tx.Sign(txFactory, keyInfo.GetName(), builder, false); err != nil {
|
||||
api.logger.Debug("failed to sign tx", "error", err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
// Encode transaction by default Tx encoder
|
||||
txEncoder := api.clientCtx.TxConfig.TxEncoder()
|
||||
txBytes, err := txEncoder(builder.GetTx())
|
||||
if err != nil {
|
||||
api.logger.Debug("failed to encode eth tx using default encoder", "error", err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
tmHash := common.BytesToHash(tmtypes.Tx(txBytes).Hash())
|
||||
|
||||
// Broadcast transaction in sync mode (default)
|
||||
// NOTE: If error is encountered on the node, the broadcast will not return an error
|
||||
syncCtx := api.clientCtx.WithBroadcastMode(flags.BroadcastSync)
|
||||
rsp, err := syncCtx.BroadcastTx(txBytes)
|
||||
if err != nil || rsp.Code != 0 {
|
||||
if err == nil {
|
||||
err = errors.New(rsp.RawLog)
|
||||
}
|
||||
api.logger.Debug("failed to broadcast tx", "error", err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
api.logger.Debug("broadcasted tx to set miner withdraw address (etherbase)", "hash", tmHash.String())
|
||||
return true
|
||||
}
|
||||
|
||||
// SetGasPrice sets the minimum accepted gas price for the miner.
|
||||
// NOTE: this function accepts only integers to have the same interface than go-eth
|
||||
// to use float values, the gas prices must be configured using the configuration file
|
||||
func (api *API) SetGasPrice(gasPrice hexutil.Big) bool {
|
||||
api.logger.Info(api.ctx.Viper.ConfigFileUsed())
|
||||
appConf := config.GetConfig(api.ctx.Viper)
|
||||
|
||||
var unit string
|
||||
minGasPrices := appConf.GetMinGasPrices()
|
||||
|
||||
// fetch the base denom from the sdk Config in case it's not currently defined on the node config
|
||||
if len(minGasPrices) == 0 || minGasPrices.Empty() {
|
||||
var err error
|
||||
unit, err = sdk.GetBaseDenom()
|
||||
if err != nil {
|
||||
api.logger.Debug("could not get the denom of smallest unit registered", "error", err.Error())
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
unit = minGasPrices[0].Denom
|
||||
}
|
||||
|
||||
c := sdk.NewDecCoin(unit, sdk.NewIntFromBigInt(gasPrice.ToInt()))
|
||||
|
||||
appConf.SetMinGasPrices(sdk.DecCoins{c})
|
||||
sdkconfig.WriteConfigFile(api.ctx.Viper.ConfigFileUsed(), appConf)
|
||||
api.logger.Info("Your configuration file was modified. Please RESTART your node.", "gas-price", c.String())
|
||||
return true
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package miner
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
)
|
||||
|
||||
// GetHashrate returns the current hashrate for local CPU miner and remote miner.
|
||||
// Unsupported in Ethermint
|
||||
func (api *API) GetHashrate() uint64 {
|
||||
api.logger.Debug("miner_getHashrate")
|
||||
api.logger.Debug("Unsupported rpc function: miner_getHashrate")
|
||||
return 0
|
||||
}
|
||||
|
||||
// SetExtra sets the extra data string that is included when this miner mines a block.
|
||||
// Unsupported in Ethermint
|
||||
func (api *API) SetExtra(extra string) (bool, error) {
|
||||
api.logger.Debug("miner_setExtra")
|
||||
api.logger.Debug("Unsupported rpc function: miner_setExtra")
|
||||
return false, errors.New("unsupported rpc function: miner_setExtra")
|
||||
}
|
||||
|
||||
// SetGasLimit sets the gaslimit to target towards during mining.
|
||||
// Unsupported in Ethermint
|
||||
func (api *API) SetGasLimit(gasLimit hexutil.Uint64) bool {
|
||||
api.logger.Debug("miner_setGasLimit")
|
||||
api.logger.Debug("Unsupported rpc function: miner_setGasLimit")
|
||||
return false
|
||||
}
|
||||
|
||||
// Start starts the miner with the given number of threads. If threads is nil,
|
||||
// the number of workers started is equal to the number of logical CPUs that are
|
||||
// usable by this process. If mining is already running, this method adjust the
|
||||
// number of threads allowed to use and updates the minimum price required by the
|
||||
// transaction pool.
|
||||
// Unsupported in Ethermint
|
||||
func (api *API) Start(threads *int) error {
|
||||
api.logger.Debug("miner_start")
|
||||
api.logger.Debug("Unsupported rpc function: miner_start")
|
||||
return errors.New("unsupported rpc function: miner_start")
|
||||
}
|
||||
|
||||
// Stop terminates the miner, both at the consensus engine level as well as at
|
||||
// the block creation level.
|
||||
// Unsupported in Ethermint
|
||||
func (api *API) Stop() {
|
||||
api.logger.Debug("miner_stop")
|
||||
api.logger.Debug("Unsupported rpc function: miner_stop")
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package net
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
rpcclient "github.com/tendermint/tendermint/rpc/client"
|
||||
ethermint "github.com/tharsis/ethermint/types"
|
||||
)
|
||||
|
||||
// PublicAPI is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec.
|
||||
type PublicAPI struct {
|
||||
networkVersion uint64
|
||||
tmClient rpcclient.Client
|
||||
}
|
||||
|
||||
// NewPublicAPI creates an instance of the public Net Web3 API.
|
||||
func NewPublicAPI(clientCtx client.Context) *PublicAPI {
|
||||
// parse the chainID from a integer string
|
||||
chainIDEpoch, err := ethermint.ParseChainID(clientCtx.ChainID)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &PublicAPI{
|
||||
networkVersion: chainIDEpoch.Uint64(),
|
||||
tmClient: clientCtx.Client,
|
||||
}
|
||||
}
|
||||
|
||||
// Version returns the current ethereum protocol version.
|
||||
func (s *PublicAPI) Version() string {
|
||||
return fmt.Sprintf("%d", s.networkVersion)
|
||||
}
|
||||
|
||||
// Listening returns if client is actively listening for network connections.
|
||||
func (s *PublicAPI) Listening() bool {
|
||||
ctx := context.Background()
|
||||
netInfo, err := s.tmClient.NetInfo(ctx)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return netInfo.Listening
|
||||
}
|
||||
|
||||
// PeerCount returns the number of peers currently connected to the client.
|
||||
func (s *PublicAPI) PeerCount() int {
|
||||
ctx := context.Background()
|
||||
netInfo, err := s.tmClient.NetInfo(ctx)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return len(netInfo.Peers)
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
package personal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/tharsis/ethermint/ethereum/rpc/backend"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
|
||||
"github.com/tharsis/ethermint/crypto/hd"
|
||||
ethermint "github.com/tharsis/ethermint/types"
|
||||
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
|
||||
sdkcrypto "github.com/cosmos/cosmos-sdk/crypto"
|
||||
"github.com/cosmos/cosmos-sdk/crypto/keyring"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
|
||||
"github.com/tharsis/ethermint/crypto/ethsecp256k1"
|
||||
rpctypes "github.com/tharsis/ethermint/ethereum/rpc/types"
|
||||
)
|
||||
|
||||
// PrivateAccountAPI is the personal_ prefixed set of APIs in the Web3 JSON-RPC spec.
|
||||
type PrivateAccountAPI struct {
|
||||
clientCtx client.Context
|
||||
backend backend.Backend
|
||||
logger log.Logger
|
||||
hdPathIter ethermint.HDPathIterator
|
||||
}
|
||||
|
||||
// NewAPI creates an instance of the public Personal Eth API.
|
||||
func NewAPI(logger log.Logger, clientCtx client.Context, backend backend.Backend) *PrivateAccountAPI {
|
||||
cfg := sdk.GetConfig()
|
||||
basePath := cfg.GetFullBIP44Path()
|
||||
|
||||
iterator, err := ethermint.NewHDPathIterator(basePath, true)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &PrivateAccountAPI{
|
||||
clientCtx: clientCtx,
|
||||
logger: logger.With("api", "personal"),
|
||||
hdPathIter: iterator,
|
||||
backend: backend,
|
||||
}
|
||||
}
|
||||
|
||||
// ImportRawKey armors and encrypts a given raw hex encoded ECDSA key and stores it into the key directory.
|
||||
// The name of the key will have the format "personal_<length-keys>", where <length-keys> is the total number of
|
||||
// keys stored on the keyring.
|
||||
//
|
||||
// NOTE: The key will be both armored and encrypted using the same passphrase.
|
||||
func (api *PrivateAccountAPI) ImportRawKey(privkey, password string) (common.Address, error) {
|
||||
api.logger.Debug("personal_importRawKey")
|
||||
priv, err := crypto.HexToECDSA(privkey)
|
||||
if err != nil {
|
||||
return common.Address{}, err
|
||||
}
|
||||
|
||||
privKey := ðsecp256k1.PrivKey{Key: crypto.FromECDSA(priv)}
|
||||
|
||||
addr := sdk.AccAddress(privKey.PubKey().Address().Bytes())
|
||||
ethereumAddr := common.BytesToAddress(addr)
|
||||
|
||||
// return if the key has already been imported
|
||||
if _, err := api.clientCtx.Keyring.KeyByAddress(addr); err == nil {
|
||||
return ethereumAddr, nil
|
||||
}
|
||||
|
||||
// ignore error as we only care about the length of the list
|
||||
list, _ := api.clientCtx.Keyring.List()
|
||||
privKeyName := fmt.Sprintf("personal_%d", len(list))
|
||||
|
||||
armor := sdkcrypto.EncryptArmorPrivKey(privKey, password, ethsecp256k1.KeyType)
|
||||
|
||||
if err := api.clientCtx.Keyring.ImportPrivKey(privKeyName, armor, password); err != nil {
|
||||
return common.Address{}, err
|
||||
}
|
||||
|
||||
api.logger.Info("key successfully imported", "name", privKeyName, "address", ethereumAddr.String())
|
||||
|
||||
return ethereumAddr, nil
|
||||
}
|
||||
|
||||
// ListAccounts will return a list of addresses for accounts this node manages.
|
||||
func (api *PrivateAccountAPI) ListAccounts() ([]common.Address, error) {
|
||||
api.logger.Debug("personal_listAccounts")
|
||||
addrs := []common.Address{}
|
||||
|
||||
list, err := api.clientCtx.Keyring.List()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, info := range list {
|
||||
addrs = append(addrs, common.BytesToAddress(info.GetPubKey().Address()))
|
||||
}
|
||||
|
||||
return addrs, nil
|
||||
}
|
||||
|
||||
// LockAccount will lock the account associated with the given address when it's unlocked.
|
||||
// It removes the key corresponding to the given address from the API's local keys.
|
||||
func (api *PrivateAccountAPI) LockAccount(address common.Address) bool {
|
||||
api.logger.Debug("personal_lockAccount", "address", address.String())
|
||||
api.logger.Info("personal_lockAccount not supported")
|
||||
// TODO: Not supported. See underlying issue https://github.com/99designs/keyring/issues/85
|
||||
return false
|
||||
}
|
||||
|
||||
// NewAccount will create a new account and returns the address for the new account.
|
||||
func (api *PrivateAccountAPI) NewAccount(password string) (common.Address, error) {
|
||||
api.logger.Debug("personal_newAccount")
|
||||
|
||||
name := "key_" + time.Now().UTC().Format(time.RFC3339)
|
||||
|
||||
// create the mnemonic and save the account
|
||||
hdPath := api.hdPathIter()
|
||||
|
||||
info, _, err := api.clientCtx.Keyring.NewMnemonic(name, keyring.English, hdPath.String(), password, hd.EthSecp256k1)
|
||||
if err != nil {
|
||||
return common.Address{}, err
|
||||
}
|
||||
|
||||
addr := common.BytesToAddress(info.GetPubKey().Address().Bytes())
|
||||
api.logger.Info("Your new key was generated", "address", addr.String())
|
||||
api.logger.Info("Please backup your key file!", "path", os.Getenv("HOME")+"/.ethermint/"+name) // TODO: pass the correct binary
|
||||
api.logger.Info("Please remember your password!")
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
// UnlockAccount will unlock the account associated with the given address with
|
||||
// the given password for duration seconds. If duration is nil it will use a
|
||||
// default of 300 seconds. It returns an indication if the account was unlocked.
|
||||
func (api *PrivateAccountAPI) UnlockAccount(_ context.Context, addr common.Address, _ string, _ *uint64) (bool, error) {
|
||||
api.logger.Debug("personal_unlockAccount", "address", addr.String())
|
||||
// TODO: Not supported. See underlying issue https://github.com/99designs/keyring/issues/85
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// SendTransaction will create a transaction from the given arguments and
|
||||
// tries to sign it with the key associated with args.To. If the given password isn't
|
||||
// able to decrypt the key it fails.
|
||||
func (api *PrivateAccountAPI) SendTransaction(_ context.Context, args rpctypes.SendTxArgs, pwrd string) (common.Hash, error) {
|
||||
api.logger.Debug("personal_sendTransaction", "address", args.To.String())
|
||||
|
||||
addr := sdk.AccAddress(args.From.Bytes())
|
||||
|
||||
// check if the key is on the keyring
|
||||
_, err := api.clientCtx.Keyring.KeyByAddress(addr)
|
||||
if err != nil {
|
||||
return common.Hash{}, err
|
||||
}
|
||||
|
||||
return api.backend.SendTransaction(args)
|
||||
}
|
||||
|
||||
// Sign calculates an Ethereum ECDSA signature for:
|
||||
// keccak256("\x19Ethereum Signed Message:\n" + len(message) + message))
|
||||
//
|
||||
// Note, the produced signature conforms to the secp256k1 curve R, S and V values,
|
||||
// where the V value will be 27 or 28 for legacy reasons.
|
||||
//
|
||||
// The key used to calculate the signature is decrypted with the given password.
|
||||
//
|
||||
// https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_sign
|
||||
func (api *PrivateAccountAPI) Sign(_ context.Context, data hexutil.Bytes, addr common.Address, pwrd string) (hexutil.Bytes, error) {
|
||||
api.logger.Debug("personal_sign", "data", data, "address", addr.String())
|
||||
|
||||
cosmosAddr := sdk.AccAddress(addr.Bytes())
|
||||
|
||||
sig, _, err := api.clientCtx.Keyring.SignByAddress(cosmosAddr, accounts.TextHash(data))
|
||||
if err != nil {
|
||||
api.logger.Error("failed to sign with key", "data", data, "address", addr.String(), "error", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sig[crypto.RecoveryIDOffset] += 27 // transform V from 0/1 to 27/28
|
||||
return sig, nil
|
||||
}
|
||||
|
||||
// EcRecover returns the address for the account that was used to create the signature.
|
||||
// Note, this function is compatible with eth_sign and personal_sign. As such it recovers
|
||||
// the address of:
|
||||
// hash = keccak256("\x19Ethereum Signed Message:\n"${message length}${message})
|
||||
// addr = ecrecover(hash, signature)
|
||||
//
|
||||
// Note, the signature must conform to the secp256k1 curve R, S and V values, where
|
||||
// the V value must be 27 or 28 for legacy reasons.
|
||||
//
|
||||
// https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_ecRecove
|
||||
func (api *PrivateAccountAPI) EcRecover(_ context.Context, data, sig hexutil.Bytes) (common.Address, error) {
|
||||
api.logger.Debug("personal_ecRecover", "data", data, "sig", sig)
|
||||
|
||||
if len(sig) != crypto.SignatureLength {
|
||||
return common.Address{}, fmt.Errorf("signature must be %d bytes long", crypto.SignatureLength)
|
||||
}
|
||||
|
||||
if sig[crypto.RecoveryIDOffset] != 27 && sig[crypto.RecoveryIDOffset] != 28 {
|
||||
return common.Address{}, fmt.Errorf("invalid Ethereum signature (V is not 27 or 28)")
|
||||
}
|
||||
|
||||
sig[crypto.RecoveryIDOffset] -= 27 // Transform yellow paper V from 27/28 to 0/1
|
||||
|
||||
pubkey, err := crypto.SigToPub(accounts.TextHash(data), sig)
|
||||
if err != nil {
|
||||
return common.Address{}, err
|
||||
}
|
||||
|
||||
return crypto.PubkeyToAddress(*pubkey), nil
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package txpool
|
||||
|
||||
import (
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
|
||||
"github.com/tharsis/ethermint/ethereum/rpc/types"
|
||||
)
|
||||
|
||||
// PublicAPI offers and API for the transaction pool. It only operates on data that is non-confidential.
|
||||
// NOTE: For more info about the current status of this endpoints see https://github.com/tharsis/ethermint/issues/124
|
||||
type PublicAPI struct {
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
// NewPublicAPI creates a new tx pool service that gives information about the transaction pool.
|
||||
func NewPublicAPI(logger log.Logger) *PublicAPI {
|
||||
return &PublicAPI{
|
||||
logger: logger.With("module", "txpool"),
|
||||
}
|
||||
}
|
||||
|
||||
// Content returns the transactions contained within the transaction pool
|
||||
func (api *PublicAPI) Content() (map[string]map[string]map[string]*types.RPCTransaction, error) {
|
||||
api.logger.Debug("txpool_content")
|
||||
content := map[string]map[string]map[string]*types.RPCTransaction{
|
||||
"pending": make(map[string]map[string]*types.RPCTransaction),
|
||||
"queued": make(map[string]map[string]*types.RPCTransaction),
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
|
||||
// Inspect returns the content of the transaction pool and flattens it into an
|
||||
func (api *PublicAPI) Inspect() (map[string]map[string]map[string]string, error) {
|
||||
api.logger.Debug("txpool_inspect")
|
||||
content := map[string]map[string]map[string]string{
|
||||
"pending": make(map[string]map[string]string),
|
||||
"queued": make(map[string]map[string]string),
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
|
||||
// Status returns the number of pending and queued transaction in the pool.
|
||||
func (api *PublicAPI) Status() map[string]hexutil.Uint {
|
||||
api.logger.Debug("txpool_status")
|
||||
return map[string]hexutil.Uint{
|
||||
"pending": hexutil.Uint(0),
|
||||
"queued": hexutil.Uint(0),
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package web3
|
||||
|
||||
import (
|
||||
"github.com/tharsis/ethermint/version"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
)
|
||||
|
||||
// PublicAPI is the web3_ prefixed set of APIs in the Web3 JSON-RPC spec.
|
||||
type PublicAPI struct{}
|
||||
|
||||
// NewPublicAPI creates an instance of the Web3 API.
|
||||
func NewPublicAPI() *PublicAPI {
|
||||
return &PublicAPI{}
|
||||
}
|
||||
|
||||
// ClientVersion returns the client version in the Web3 user agent format.
|
||||
func (a *PublicAPI) ClientVersion() string {
|
||||
return version.Version()
|
||||
}
|
||||
|
||||
// Sha3 returns the keccak-256 hash of the passed-in input.
|
||||
func (a *PublicAPI) Sha3(input hexutil.Bytes) hexutil.Bytes {
|
||||
return crypto.Keccak256(input)
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
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 {
|
||||
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:
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package pubsub
|
||||
|
||||
// func TestAddTopic(t *testing.T) {
|
||||
// q := NewEventBus()
|
||||
// err := q.AddTopic("kek", make(<-chan coretypes.ResultEvent))
|
||||
// require.NoError(t, err)
|
||||
|
||||
// err = q.AddTopic("lol", make(<-chan coretypes.ResultEvent))
|
||||
// require.NoError(t, err)
|
||||
|
||||
// err = q.AddTopic("lol", make(<-chan coretypes.ResultEvent))
|
||||
// require.Error(t, err)
|
||||
|
||||
// require.EqualValues(t, []string{"kek", "lol"}, q.Topics())
|
||||
// }
|
||||
|
||||
// func TestSubscribe(t *testing.T) {
|
||||
// q := NewEventBus()
|
||||
// kekSrc := make(chan coretypes.ResultEvent)
|
||||
// q.AddTopic("kek", kekSrc)
|
||||
|
||||
// lolSrc := make(chan coretypes.ResultEvent)
|
||||
// q.AddTopic("lol", lolSrc)
|
||||
|
||||
// kekSubC, err := q.Subscribe("kek")
|
||||
// require.NoError(t, err)
|
||||
|
||||
// lolSubC, err := q.Subscribe("lol")
|
||||
// require.NoError(t, err)
|
||||
|
||||
// lol2SubC, err := q.Subscribe("lol")
|
||||
// require.NoError(t, err)
|
||||
|
||||
// wg := new(sync.WaitGroup)
|
||||
// wg.Add(4)
|
||||
|
||||
// go func() {
|
||||
// defer wg.Done()
|
||||
// msg := <-kekSubC
|
||||
// log.Println("kek:", msg)
|
||||
// require.EqualValues(t, 1, msg)
|
||||
// }()
|
||||
|
||||
// go func() {
|
||||
// defer wg.Done()
|
||||
// msg := <-lolSubC
|
||||
// log.Println("lol:", msg)
|
||||
// require.EqualValues(t, 1, msg)
|
||||
// }()
|
||||
|
||||
// go func() {
|
||||
// defer wg.Done()
|
||||
// msg := <-lol2SubC
|
||||
// log.Println("lol2:", msg)
|
||||
// require.EqualValues(t, 1, msg)
|
||||
// }()
|
||||
|
||||
// go func() {
|
||||
// defer wg.Done()
|
||||
|
||||
// time.Sleep(time.Second)
|
||||
|
||||
// close(kekSrc)
|
||||
// close(lolSrc)
|
||||
// }()
|
||||
|
||||
// wg.Wait()
|
||||
// time.Sleep(time.Second)
|
||||
// }
|
||||
@@ -1,38 +0,0 @@
|
||||
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()
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/big"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
"google.golang.org/grpc/metadata"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
|
||||
grpctypes "github.com/cosmos/cosmos-sdk/types/grpc"
|
||||
|
||||
ethermint "github.com/tharsis/ethermint/types"
|
||||
)
|
||||
|
||||
// BlockNumber represents decoding hex string to block values
|
||||
type BlockNumber int64
|
||||
|
||||
const (
|
||||
EthPendingBlockNumber = BlockNumber(-2)
|
||||
EthLatestBlockNumber = BlockNumber(-1)
|
||||
EthEarliestBlockNumber = BlockNumber(0)
|
||||
)
|
||||
|
||||
const (
|
||||
BlockParamEarliest = "earliest"
|
||||
BlockParamLatest = "latest"
|
||||
BlockParamPending = "pending"
|
||||
)
|
||||
|
||||
// NewBlockNumber creates a new BlockNumber instance.
|
||||
func NewBlockNumber(n *big.Int) BlockNumber {
|
||||
if !n.IsInt64() {
|
||||
// default to latest block if it overflows
|
||||
return EthLatestBlockNumber
|
||||
}
|
||||
|
||||
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.
|
||||
// Note that all metadata are processed and removed by tendermint layer, so it wont be accessible at gRPC server level.
|
||||
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 BlockParamEarliest:
|
||||
*bn = EthEarliestBlockNumber
|
||||
return nil
|
||||
case BlockParamLatest:
|
||||
*bn = EthLatestBlockNumber
|
||||
return nil
|
||||
case BlockParamPending:
|
||||
*bn = EthPendingBlockNumber
|
||||
return nil
|
||||
}
|
||||
|
||||
blckNum, err := hexutil.DecodeUint64(input)
|
||||
if errors.Is(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
|
||||
}
|
||||
|
||||
// BlockNumberOrHash represents a block number or a block hash.
|
||||
type BlockNumberOrHash struct {
|
||||
BlockNumber *BlockNumber `json:"blockNumber,omitempty"`
|
||||
BlockHash *common.Hash `json:"blockHash,omitempty"`
|
||||
}
|
||||
|
||||
func (bnh *BlockNumberOrHash) UnmarshalJSON(data []byte) error {
|
||||
type erased BlockNumberOrHash
|
||||
e := erased{}
|
||||
err := json.Unmarshal(data, &e)
|
||||
if err == nil {
|
||||
return bnh.checkUnmarshal(BlockNumberOrHash(e))
|
||||
}
|
||||
var input string
|
||||
err = json.Unmarshal(data, &input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = bnh.decodeFromString(input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bnh *BlockNumberOrHash) checkUnmarshal(e BlockNumberOrHash) error {
|
||||
if e.BlockNumber != nil && e.BlockHash != nil {
|
||||
return fmt.Errorf("cannot specify both BlockHash and BlockNumber, choose one or the other")
|
||||
}
|
||||
bnh.BlockNumber = e.BlockNumber
|
||||
bnh.BlockHash = e.BlockHash
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bnh *BlockNumberOrHash) decodeFromString(input string) error {
|
||||
switch input {
|
||||
case BlockParamEarliest:
|
||||
bn := EthEarliestBlockNumber
|
||||
bnh.BlockNumber = &bn
|
||||
case BlockParamLatest:
|
||||
bn := EthLatestBlockNumber
|
||||
bnh.BlockNumber = &bn
|
||||
case BlockParamPending:
|
||||
bn := EthPendingBlockNumber
|
||||
bnh.BlockNumber = &bn
|
||||
default:
|
||||
// check if the input is a block hash
|
||||
if len(input) == 66 {
|
||||
hash := common.Hash{}
|
||||
err := hash.UnmarshalText([]byte(input))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bnh.BlockHash = &hash
|
||||
break
|
||||
}
|
||||
// otherwise take the hex string has int64 value
|
||||
blockNumber, err := hexutil.DecodeUint64(input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bnInt, err := ethermint.SafeInt64(blockNumber)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bn := BlockNumber(bnInt)
|
||||
bnh.BlockNumber = &bn
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUnmarshalBlockNumberOrHash(t *testing.T) {
|
||||
bnh := new(BlockNumberOrHash)
|
||||
|
||||
testCases := []struct {
|
||||
msg string
|
||||
input []byte
|
||||
malleate func()
|
||||
expPass bool
|
||||
}{
|
||||
{
|
||||
"JSON input with block hash",
|
||||
[]byte("{\"blockHash\": \"0x579917054e325746fda5c3ee431d73d26255bc4e10b51163862368629ae19739\"}"),
|
||||
func() {
|
||||
require.Equal(t, *bnh.BlockHash, common.HexToHash("0x579917054e325746fda5c3ee431d73d26255bc4e10b51163862368629ae19739"))
|
||||
require.Nil(t, bnh.BlockNumber)
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"JSON input with block number",
|
||||
[]byte("{\"blockNumber\": \"0x35\"}"),
|
||||
func() {
|
||||
require.Equal(t, *bnh.BlockNumber, BlockNumber(0x35))
|
||||
require.Nil(t, bnh.BlockHash)
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"JSON input with block number latest",
|
||||
[]byte("{\"blockNumber\": \"latest\"}"),
|
||||
func() {
|
||||
require.Equal(t, *bnh.BlockNumber, EthLatestBlockNumber)
|
||||
require.Nil(t, bnh.BlockHash)
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"JSON input with both block hash and block number",
|
||||
[]byte("{\"blockHash\": \"0x579917054e325746fda5c3ee431d73d26255bc4e10b51163862368629ae19739\", \"blockNumber\": \"0x35\"}"),
|
||||
func() {
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"String input with block hash",
|
||||
[]byte("\"0x579917054e325746fda5c3ee431d73d26255bc4e10b51163862368629ae19739\""),
|
||||
func() {
|
||||
require.Equal(t, *bnh.BlockHash, common.HexToHash("0x579917054e325746fda5c3ee431d73d26255bc4e10b51163862368629ae19739"))
|
||||
require.Nil(t, bnh.BlockNumber)
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"String input with block number",
|
||||
[]byte("\"0x35\""),
|
||||
func() {
|
||||
require.Equal(t, *bnh.BlockNumber, BlockNumber(0x35))
|
||||
require.Nil(t, bnh.BlockHash)
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"String input with block number latest",
|
||||
[]byte("\"latest\""),
|
||||
func() {
|
||||
require.Equal(t, *bnh.BlockNumber, EthLatestBlockNumber)
|
||||
require.Nil(t, bnh.BlockHash)
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"String input with block number overflow",
|
||||
[]byte("\"0xffffffffffffffffffffffffffffffffffffff\""),
|
||||
func() {
|
||||
},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
fmt.Sprintf("Case %s", tc.msg)
|
||||
// reset input
|
||||
bnh = new(BlockNumberOrHash)
|
||||
err := bnh.UnmarshalJSON(tc.input)
|
||||
tc.malleate()
|
||||
if tc.expPass {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
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/tharsis/ethermint/x/evm/types"
|
||||
feemarkettypes "github.com/tharsis/ethermint/x/feemarket/types"
|
||||
)
|
||||
|
||||
// QueryClient defines a gRPC Client used for:
|
||||
// - Transaction simulation
|
||||
// - EVM module queries
|
||||
type QueryClient struct {
|
||||
tx.ServiceClient
|
||||
evmtypes.QueryClient
|
||||
FeeMarket feemarkettypes.QueryClient
|
||||
}
|
||||
|
||||
// NewQueryClient creates a new gRPC query client
|
||||
func NewQueryClient(clientCtx client.Context) *QueryClient {
|
||||
return &QueryClient{
|
||||
ServiceClient: tx.NewServiceClient(clientCtx),
|
||||
QueryClient: evmtypes.NewQueryClient(clientCtx),
|
||||
FeeMarket: feemarkettypes.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
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
ethtypes "github.com/ethereum/go-ethereum/core/types"
|
||||
|
||||
evmtypes "github.com/tharsis/ethermint/x/evm/types"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// RPCTransaction represents a transaction that will serialize to the RPC representation of a transaction
|
||||
type RPCTransaction struct {
|
||||
BlockHash *common.Hash `json:"blockHash"`
|
||||
BlockNumber *hexutil.Big `json:"blockNumber"`
|
||||
From common.Address `json:"from"`
|
||||
Gas hexutil.Uint64 `json:"gas"`
|
||||
GasPrice *hexutil.Big `json:"gasPrice"`
|
||||
Hash common.Hash `json:"hash"`
|
||||
Input hexutil.Bytes `json:"input"`
|
||||
Nonce hexutil.Uint64 `json:"nonce"`
|
||||
To *common.Address `json:"to"`
|
||||
TransactionIndex *hexutil.Uint64 `json:"transactionIndex"`
|
||||
Value *hexutil.Big `json:"value"`
|
||||
Type hexutil.Uint64 `json:"type"`
|
||||
Accesses *ethtypes.AccessList `json:"accessList,omitempty"`
|
||||
ChainID *hexutil.Big `json:"chainId,omitempty"`
|
||||
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"`
|
||||
// For non-legacy transactions
|
||||
AccessList *ethtypes.AccessList `json:"accessList,omitempty"`
|
||||
ChainID *hexutil.Big `json:"chainId,omitempty"`
|
||||
}
|
||||
|
||||
// String return the struct in a string format
|
||||
func (args *SendTxArgs) String() string {
|
||||
// Todo: There is currently a bug with hexutil.Big when the value its nil, printing would trigger an exception
|
||||
return fmt.Sprintf("SendTxArgs{From:%v, To:%v, Gas:%v,"+
|
||||
" Nonce:%v, Data:%v, Input:%v, AccessList:%v}",
|
||||
args.From,
|
||||
args.To,
|
||||
args.Gas,
|
||||
args.Nonce,
|
||||
args.Data,
|
||||
args.Input,
|
||||
args.AccessList)
|
||||
}
|
||||
|
||||
// ToTransaction converts the arguments to an ethereum transaction.
|
||||
// This assumes that setTxDefaults has been called.
|
||||
func (args *SendTxArgs) ToTransaction() *evmtypes.MsgEthereumTx {
|
||||
var (
|
||||
input []byte
|
||||
chainID, value, gasPrice *big.Int
|
||||
gas, nonce uint64
|
||||
)
|
||||
|
||||
if args.Input != nil {
|
||||
input = *args.Input
|
||||
} else if args.Data != nil {
|
||||
input = *args.Data
|
||||
}
|
||||
|
||||
if args.ChainID != nil {
|
||||
chainID = args.ChainID.ToInt()
|
||||
}
|
||||
|
||||
if args.Nonce != nil {
|
||||
nonce = uint64(*args.Nonce)
|
||||
}
|
||||
|
||||
if args.Gas != nil {
|
||||
gas = uint64(*args.Gas)
|
||||
}
|
||||
|
||||
if args.GasPrice != nil {
|
||||
gasPrice = args.GasPrice.ToInt()
|
||||
}
|
||||
|
||||
if args.Value != nil {
|
||||
value = args.Value.ToInt()
|
||||
}
|
||||
|
||||
tx := evmtypes.NewTx(chainID, nonce, args.To, value, gas, gasPrice, input, args.AccessList)
|
||||
tx.From = args.From.Hex()
|
||||
|
||||
return tx
|
||||
}
|
||||
|
||||
// StateOverride is the collection of overridden accounts.
|
||||
type StateOverride map[common.Address]OverrideAccount
|
||||
|
||||
// OverrideAccount 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 OverrideAccount 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"`
|
||||
}
|
||||
@@ -1,283 +0,0 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
tmbytes "github.com/tendermint/tendermint/libs/bytes"
|
||||
tmtypes "github.com/tendermint/tendermint/types"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
|
||||
evmtypes "github.com/tharsis/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, txBz tmtypes.Tx) (*evmtypes.MsgEthereumTx, error) {
|
||||
tx, err := clientCtx.TxConfig.TxDecoder()(txBz)
|
||||
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 *ethtypes.Transaction, blockHash common.Hash, blockNumber, index uint64) *RPCTransaction {
|
||||
// Determine the signer. For replay-protected transactions, use the most permissive
|
||||
// signer, because we assume that signers are backwards-compatible with old
|
||||
// transactions. For non-protected transactions, the homestead signer signer is used
|
||||
// because the return value of ChainId is zero for those transactions.
|
||||
var signer ethtypes.Signer
|
||||
if tx.Protected() {
|
||||
signer = ethtypes.LatestSignerForChainID(tx.ChainId())
|
||||
} else {
|
||||
signer = ethtypes.HomesteadSigner{}
|
||||
}
|
||||
|
||||
from, _ := ethtypes.Sender(signer, tx)
|
||||
v, r, s := tx.RawSignatureValues()
|
||||
result := &RPCTransaction{
|
||||
Type: hexutil.Uint64(tx.Type()),
|
||||
From: from,
|
||||
Gas: hexutil.Uint64(tx.Gas()),
|
||||
GasPrice: (*hexutil.Big)(tx.GasPrice()),
|
||||
Hash: tx.Hash(), // NOTE: transaction hash here uses the ethereum format for compatibility
|
||||
Input: hexutil.Bytes(tx.Data()),
|
||||
Nonce: hexutil.Uint64(tx.Nonce()),
|
||||
To: tx.To(),
|
||||
Value: (*hexutil.Big)(tx.Value()),
|
||||
V: (*hexutil.Big)(v),
|
||||
R: (*hexutil.Big)(r),
|
||||
S: (*hexutil.Big)(s),
|
||||
}
|
||||
if blockHash != (common.Hash{}) {
|
||||
result.BlockHash = &blockHash
|
||||
result.BlockNumber = (*hexutil.Big)(new(big.Int).SetUint64(blockNumber))
|
||||
result.TransactionIndex = (*hexutil.Uint64)(&index)
|
||||
}
|
||||
if tx.Type() == ethtypes.AccessListTxType {
|
||||
al := tx.AccessList()
|
||||
result.Accesses = &al
|
||||
result.ChainID = (*hexutil.Big)(tx.ChainId())
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// EthHeaderFromTendermint is an util function that returns an Ethereum Header
|
||||
// from a tendermint Header.
|
||||
func EthHeaderFromTendermint(header tmtypes.Header) *ethtypes.Header {
|
||||
txHash := ethtypes.EmptyRootHash
|
||||
if len(header.DataHash) == 0 {
|
||||
txHash = common.BytesToHash(header.DataHash)
|
||||
}
|
||||
return ðtypes.Header{
|
||||
ParentHash: common.BytesToHash(header.LastBlockID.Hash.Bytes()),
|
||||
UncleHash: ethtypes.EmptyUncleHash,
|
||||
Coinbase: common.Address{},
|
||||
Root: common.BytesToHash(header.AppHash),
|
||||
TxHash: txHash,
|
||||
ReceiptHash: ethtypes.EmptyRootHash,
|
||||
Bloom: ethtypes.Bloom{},
|
||||
Difficulty: big.NewInt(0),
|
||||
Number: big.NewInt(header.Height),
|
||||
GasLimit: 0,
|
||||
GasUsed: 0,
|
||||
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
|
||||
}
|
||||
|
||||
data, err := evmtypes.UnpackTxData(ethTx.Data)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to unpack tx data: %w", err)
|
||||
}
|
||||
|
||||
// TODO: Remove gas usage calculation if saving gasUsed per block
|
||||
gasUsed.Add(gasUsed, data.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 int64(^uint32(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,
|
||||
validatorAddr common.Address,
|
||||
) 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": common.BytesToHash(header.LastBlockID.Hash.Bytes()),
|
||||
"nonce": ethtypes.BlockNonce{}, // PoW specific
|
||||
"sha3Uncles": ethtypes.EmptyUncleHash, // No uncles in Tendermint
|
||||
"logsBloom": bloom,
|
||||
"stateRoot": hexutil.Bytes(header.AppHash),
|
||||
"miner": validatorAddr,
|
||||
"mixHash": common.Hash{},
|
||||
"difficulty": (*hexutil.Big)(big.NewInt(0)),
|
||||
"extraData": "0x",
|
||||
"size": hexutil.Uint64(size),
|
||||
"gasLimit": hexutil.Uint64(gasLimit), // Static gas limit
|
||||
"gasUsed": (*hexutil.Big)(gasUsed),
|
||||
"timestamp": hexutil.Uint64(header.Time.Unix()),
|
||||
"transactionsRoot": hexutil.Bytes(header.DataHash),
|
||||
"receiptsRoot": ethtypes.EmptyRootHash,
|
||||
|
||||
"uncles": []common.Hash{},
|
||||
"transactions": transactions,
|
||||
"totalDifficulty": (*hexutil.Big)(big.NewInt(0)),
|
||||
}
|
||||
}
|
||||
|
||||
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)),
|
||||
}
|
||||
}
|
||||
|
||||
// NewTransactionFromMsg returns a transaction that will serialize to the RPC
|
||||
// representation, with the given location metadata set (if available).
|
||||
func NewTransactionFromMsg(
|
||||
msg *evmtypes.MsgEthereumTx,
|
||||
blockHash common.Hash,
|
||||
blockNumber, index uint64,
|
||||
chainID *big.Int,
|
||||
) (*RPCTransaction, error) {
|
||||
from, err := msg.GetSender(chainID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := evmtypes.UnpackTxData(msg.Data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unpack tx data: %w", err)
|
||||
}
|
||||
|
||||
return NewTransactionFromData(data, from, msg.AsTransaction().Hash(), blockHash, blockNumber, index)
|
||||
}
|
||||
|
||||
// NewTransactionFromData 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,
|
||||
) (*RPCTransaction, error) {
|
||||
if txHash == (common.Hash{}) {
|
||||
txHash = ethtypes.EmptyRootHash
|
||||
}
|
||||
|
||||
v, r, s := txData.GetRawSignatureValues()
|
||||
|
||||
rpcTx := &RPCTransaction{
|
||||
Type: hexutil.Uint64(txData.TxType()),
|
||||
From: from,
|
||||
Gas: hexutil.Uint64(txData.GetGas()),
|
||||
GasPrice: (*hexutil.Big)(txData.GetGasPrice()),
|
||||
Hash: txHash,
|
||||
Input: hexutil.Bytes(txData.GetData()),
|
||||
Nonce: hexutil.Uint64(txData.GetNonce()),
|
||||
To: txData.GetTo(),
|
||||
Value: (*hexutil.Big)(txData.GetValue()),
|
||||
V: (*hexutil.Big)(v),
|
||||
R: (*hexutil.Big)(r),
|
||||
S: (*hexutil.Big)(s),
|
||||
}
|
||||
if rpcTx.To == nil {
|
||||
addr := common.Address{}
|
||||
rpcTx.To = &addr
|
||||
}
|
||||
|
||||
if blockHash != (common.Hash{}) {
|
||||
rpcTx.BlockHash = &blockHash
|
||||
rpcTx.BlockNumber = (*hexutil.Big)(new(big.Int).SetUint64(blockNumber))
|
||||
rpcTx.TransactionIndex = (*hexutil.Uint64)(&index)
|
||||
}
|
||||
|
||||
if txData.TxType() == ethtypes.AccessListTxType {
|
||||
accesses := txData.GetAccessList()
|
||||
rpcTx.Accesses = &accesses
|
||||
rpcTx.ChainID = (*hexutil.Big)(txData.GetChainID())
|
||||
}
|
||||
|
||||
return rpcTx, nil
|
||||
}
|
||||
@@ -1,733 +0,0 @@
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/eth/filters"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
coretypes "github.com/tendermint/tendermint/rpc/core/types"
|
||||
rpcclient "github.com/tendermint/tendermint/rpc/jsonrpc/client"
|
||||
tmtypes "github.com/tendermint/tendermint/types"
|
||||
|
||||
rpcfilters "github.com/tharsis/ethermint/ethereum/rpc/namespaces/eth/filters"
|
||||
"github.com/tharsis/ethermint/ethereum/rpc/types"
|
||||
"github.com/tharsis/ethermint/server/config"
|
||||
evmtypes "github.com/tharsis/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
|
||||
certFile string
|
||||
keyFile string
|
||||
api *pubSubAPI
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
func NewWebsocketsServer(logger log.Logger, tmWSClient *rpcclient.WSClient, cfg config.Config) WebsocketsServer {
|
||||
logger = logger.With("api", "websocket-server")
|
||||
_, port, _ := net.SplitHostPort(cfg.JSONRPC.Address)
|
||||
|
||||
return &websocketsServer{
|
||||
rpcAddr: "localhost:" + port, // FIXME: this shouldn't be hardcoded to localhost
|
||||
wsAddr: cfg.JSONRPC.WsAddress,
|
||||
certFile: cfg.TLS.CertificatePath,
|
||||
keyFile: cfg.TLS.KeyPath,
|
||||
api: newPubSubAPI(logger, tmWSClient),
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *websocketsServer) Start() {
|
||||
ws := mux.NewRouter()
|
||||
ws.Handle("/", s)
|
||||
|
||||
go func() {
|
||||
var err error
|
||||
if s.certFile == "" || s.keyFile == "" {
|
||||
err = http.ListenAndServe(s.wsAddr, ws)
|
||||
} else {
|
||||
err = http.ListenAndServeTLS(s.wsAddr, s.certFile, s.keyFile, ws)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if err == http.ErrServerClosed {
|
||||
return
|
||||
}
|
||||
|
||||
s.logger.Error("failed to start HTTP server for WS", "error", err.Error())
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *websocketsServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
upgrader := websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
s.logger.Debug("websocket upgrade failed", "error", err.Error())
|
||||
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.NewRequestWithContext(context.Background(), "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 *rpcfilters.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 *rpcfilters.EventSystem
|
||||
filtersMu *sync.RWMutex
|
||||
filters map[rpc.ID]*wsSubscription
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
// newPubSubAPI creates an instance of the ethereum PubSub API.
|
||||
func newPubSubAPI(logger log.Logger, tmWSClient *rpcclient.WSClient) *pubSubAPI {
|
||||
logger = logger.With("module", "websocket-client")
|
||||
return &pubSubAPI{
|
||||
events: rpcfilters.NewEventSystem(logger, tmWSClient),
|
||||
filtersMu: new(sync.RWMutex),
|
||||
filters: make(map[rpc.ID]*wsSubscription),
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
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) subscribeNewHeads(wsConn *wsConn) (rpc.ID, error) {
|
||||
query := "subscribeNewHeads"
|
||||
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 {
|
||||
api.logger.Debug("event data type mismatch", "type", fmt.Sprintf("%T", event.Data))
|
||||
continue
|
||||
}
|
||||
|
||||
header := types.EthHeaderFromTendermint(data.Header)
|
||||
|
||||
api.filtersMu.RLock()
|
||||
for subID, wsSub := range api.filters {
|
||||
subID := subID
|
||||
wsSub := wsSub
|
||||
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.Error("error writing header, will drop peer", "error", err.Error())
|
||||
|
||||
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
|
||||
}
|
||||
api.logger.Debug("dropping NewHeads WebSocket subscription", "subscription-id", subID, "error", err.Error())
|
||||
api.unsubscribe(subID)
|
||||
case <-unsubscribed:
|
||||
return
|
||||
}
|
||||
}
|
||||
}(sub.Event(), sub.Err())
|
||||
|
||||
return subID, nil
|
||||
}
|
||||
|
||||
func try(fn func(), l log.Logger, desc string) {
|
||||
defer func() {
|
||||
if x := recover(); x != nil {
|
||||
if err, ok := x.(error); ok {
|
||||
// debug.PrintStack()
|
||||
l.Debug("panic during "+desc, "error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
l.Debug(fmt.Sprintf("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")
|
||||
api.logger.Debug("invalid criteria", "type", fmt.Sprintf("%T", extra))
|
||||
return "", err
|
||||
}
|
||||
|
||||
if params["address"] != nil {
|
||||
address, ok := params["address"].(string)
|
||||
addresses, sok := params["address"].([]interface{})
|
||||
if !ok && !sok {
|
||||
err := errors.New("invalid addresses; must be address or array of addresses")
|
||||
api.logger.Debug("invalid addresses", "type", fmt.Sprintf("%T", params["address"]))
|
||||
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")
|
||||
api.logger.Debug("invalid address", "type", fmt.Sprintf("%T", addr))
|
||||
return "", err
|
||||
}
|
||||
|
||||
crit.Addresses = append(crit.Addresses, common.HexToAddress(address))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if params["topics"] != nil {
|
||||
topics, ok := params["topics"].([]interface{})
|
||||
if !ok {
|
||||
err := errors.Errorf("invalid topics: %s", topics)
|
||||
api.logger.Error("invalid topics", "type", fmt.Sprintf("%T", topics))
|
||||
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)
|
||||
api.logger.Error("invalid topic", "type", fmt.Sprintf("%T", topic))
|
||||
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")
|
||||
api.logger.Error("invalid subtopic", "type", fmt.Sprintf("%T", subtopics))
|
||||
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)
|
||||
api.logger.Error("invalid subtopic", "type", fmt.Sprintf("%T", subtopic))
|
||||
return "", err
|
||||
}
|
||||
|
||||
subtopicsCollect[idx] = common.HexToHash(tstr)
|
||||
}
|
||||
|
||||
crit.Topics[topicIdx] = subtopicsCollect
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
critBz, err := json.Marshal(crit)
|
||||
if err != nil {
|
||||
api.logger.Error("failed to JSON marshal criteria", "error", err.Error())
|
||||
return rpc.ID(""), err
|
||||
}
|
||||
|
||||
query := "subscribeLogs" + string(critBz)
|
||||
subID := rpc.NewID()
|
||||
|
||||
sub, _, err := api.events.SubscribeLogs(crit)
|
||||
if err != nil {
|
||||
api.logger.Error("failed to subscribe logs", "error", err.Error())
|
||||
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 {
|
||||
api.logger.Debug("event data type mismatch", "type", fmt.Sprintf("%T", event.Data))
|
||||
continue
|
||||
}
|
||||
|
||||
txResponse, err := evmtypes.DecodeTxResponse(dataTx.TxResult.Result.Data)
|
||||
if err != nil {
|
||||
api.logger.Error("failed to decode tx response", "error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
logs := rpcfilters.FilterLogs(evmtypes.LogsToEthereum(txResponse.Logs), crit.FromBlock, crit.ToBlock, crit.Addresses, crit.Topics)
|
||||
if len(logs) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
api.filtersMu.RLock()
|
||||
wsSub, ok := api.filters[subID]
|
||||
if !ok {
|
||||
api.logger.Debug("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
|
||||
}
|
||||
api.logger.Debug("dropping Logs WebSocket subscription", "subscription-id", subID, "error", err.Error())
|
||||
api.unsubscribe(subID)
|
||||
case <-unsubscribed:
|
||||
return
|
||||
}
|
||||
}
|
||||
}(sub.Event(), sub.Err(), subID)
|
||||
|
||||
return subID, nil
|
||||
}
|
||||
|
||||
func (api *pubSubAPI) subscribePendingTransactions(wsConn *wsConn) (rpc.ID, error) {
|
||||
query := "subscribePendingTransactions"
|
||||
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 {
|
||||
subID := subID
|
||||
wsSub := wsSub
|
||||
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.Debug("error writing header, will drop peer", "error", err.Error())
|
||||
|
||||
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
|
||||
}
|
||||
api.logger.Debug("dropping PendingTransactions WebSocket subscription", subID, "error", err.Error())
|
||||
api.unsubscribe(subID)
|
||||
case <-unsubscribed:
|
||||
return
|
||||
}
|
||||
}
|
||||
}(sub.Event(), sub.Err())
|
||||
|
||||
return subID, nil
|
||||
}
|
||||
|
||||
func (api *pubSubAPI) subscribeSyncing(wsConn *wsConn) (rpc.ID, error) {
|
||||
return "", nil
|
||||
}
|
||||
Reference in New Issue
Block a user