package backend import ( "bytes" "context" "encoding/json" "fmt" "math/big" errorsmod "cosmossdk.io/errors" rpctypes "github.com/cerc-io/laconicd/rpc/types" ethermint "github.com/cerc-io/laconicd/types" evmtypes "github.com/cerc-io/laconicd/x/evm/types" "github.com/cosmos/cosmos-sdk/client/flags" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/pkg/errors" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) // Resend accepts an existing transaction and a new gas price and limit. It will remove // the given transaction from the pool and reinsert it with the new gas price and limit. func (b *Backend) Resend(args evmtypes.TransactionArgs, gasPrice *hexutil.Big, gasLimit *hexutil.Uint64) (common.Hash, error) { if args.Nonce == nil { return common.Hash{}, fmt.Errorf("missing transaction nonce in transaction spec") } args, err := b.SetTxDefaults(args) if err != nil { return common.Hash{}, err } // The signer used should always be the 'latest' known one because we expect // signers to be backwards-compatible with old transactions. eip155ChainID, err := ethermint.ParseChainID(b.clientCtx.ChainID) if err != nil { return common.Hash{}, err } cfg := b.ChainConfig() if cfg == nil { cfg = evmtypes.DefaultChainConfig().EthereumConfig(eip155ChainID) } signer := ethtypes.LatestSigner(cfg) matchTx := args.ToTransaction().AsTransaction() // Before replacing the old transaction, ensure the _new_ transaction fee is reasonable. price := matchTx.GasPrice() if gasPrice != nil { price = gasPrice.ToInt() } gas := matchTx.Gas() if gasLimit != nil { gas = uint64(*gasLimit) } if err := rpctypes.CheckTxFee(price, gas, b.RPCTxFeeCap()); err != nil { return common.Hash{}, err } pending, err := b.PendingTransactions() if err != nil { return common.Hash{}, err } for _, tx := range pending { // FIXME does Resend api possible at all? https://github.com/evmos/ethermint/issues/905 p, err := evmtypes.UnwrapEthereumMsg(tx, common.Hash{}) if err != nil { // not valid ethereum tx continue } pTx := p.AsTransaction() wantSigHash := signer.Hash(matchTx) pFrom, err := ethtypes.Sender(signer, pTx) if err != nil { continue } if pFrom == *args.From && signer.Hash(pTx) == wantSigHash { // Match. Re-sign and send the transaction. if gasPrice != nil && (*big.Int)(gasPrice).Sign() != 0 { args.GasPrice = gasPrice } if gasLimit != nil && *gasLimit != 0 { args.Gas = gasLimit } return b.SendTransaction(args) // TODO: this calls SetTxDefaults again, refactor to avoid calling it twice } } return common.Hash{}, fmt.Errorf("transaction %#x not found", matchTx.Hash()) } // SendRawTransaction send a raw Ethereum transaction. func (b *Backend) SendRawTransaction(data hexutil.Bytes) (common.Hash, error) { // RLP decode raw transaction bytes tx := ðtypes.Transaction{} if err := tx.UnmarshalBinary(data); err != nil { b.logger.Error("transaction decoding failed", "error", err.Error()) return common.Hash{}, err } // check the local node config in case unprotected txs are disabled if !b.UnprotectedAllowed() && !tx.Protected() { // Ensure only eip155 signed transactions are submitted if EIP155Required is set. return common.Hash{}, errors.New("only replay-protected (EIP-155) transactions allowed over RPC") } ethereumTx := &evmtypes.MsgEthereumTx{} if err := ethereumTx.FromEthereumTx(tx); err != nil { b.logger.Error("transaction converting failed", "error", err.Error()) return common.Hash{}, err } if err := ethereumTx.ValidateBasic(); err != nil { b.logger.Debug("tx failed basic validation", "error", err.Error()) return common.Hash{}, err } // Query params to use the EVM denomination res, err := b.queryClient.QueryClient.Params(b.ctx, &evmtypes.QueryParamsRequest{}) if err != nil { b.logger.Error("failed to query evm params", "error", err.Error()) return common.Hash{}, err } cosmosTx, err := ethereumTx.BuildTx(b.clientCtx.TxConfig.NewTxBuilder(), res.Params.EvmDenom) if err != nil { b.logger.Error("failed to build cosmos tx", "error", err.Error()) return common.Hash{}, err } // Encode transaction by default Tx encoder txBytes, err := b.clientCtx.TxConfig.TxEncoder()(cosmosTx) if err != nil { b.logger.Error("failed to encode eth tx using default encoder", "error", err.Error()) return common.Hash{}, err } txHash := ethereumTx.AsTransaction().Hash() syncCtx := b.clientCtx.WithBroadcastMode(flags.BroadcastSync) rsp, err := syncCtx.BroadcastTx(txBytes) if rsp != nil && rsp.Code != 0 { err = errorsmod.ABCIError(rsp.Codespace, rsp.Code, rsp.RawLog) } if err != nil { b.logger.Error("failed to broadcast tx", "error", err.Error()) return txHash, err } return txHash, nil } // SetTxDefaults populates tx message with default values in case they are not // provided on the args func (b *Backend) SetTxDefaults(args evmtypes.TransactionArgs) (evmtypes.TransactionArgs, error) { if args.GasPrice != nil && (args.MaxFeePerGas != nil || args.MaxPriorityFeePerGas != nil) { return args, errors.New("both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified") } head := b.CurrentHeader() if head == nil { return args, errors.New("latest header is nil") } // If user specifies both maxPriorityfee and maxFee, then we do not // need to consult the chain for defaults. It's definitely a London tx. if args.MaxPriorityFeePerGas == nil || args.MaxFeePerGas == nil { // In this clause, user left some fields unspecified. if head.BaseFee != nil && args.GasPrice == nil { if args.MaxPriorityFeePerGas == nil { tip, err := b.SuggestGasTipCap(head.BaseFee) if err != nil { return args, err } args.MaxPriorityFeePerGas = (*hexutil.Big)(tip) } if args.MaxFeePerGas == nil { gasFeeCap := new(big.Int).Add( (*big.Int)(args.MaxPriorityFeePerGas), new(big.Int).Mul(head.BaseFee, big.NewInt(2)), ) args.MaxFeePerGas = (*hexutil.Big)(gasFeeCap) } if args.MaxFeePerGas.ToInt().Cmp(args.MaxPriorityFeePerGas.ToInt()) < 0 { return args, fmt.Errorf("maxFeePerGas (%v) < maxPriorityFeePerGas (%v)", args.MaxFeePerGas, args.MaxPriorityFeePerGas) } } else { if args.MaxFeePerGas != nil || args.MaxPriorityFeePerGas != nil { return args, errors.New("maxFeePerGas or maxPriorityFeePerGas specified but london is not active yet") } if args.GasPrice == nil { price, err := b.SuggestGasTipCap(head.BaseFee) if err != nil { return args, err } if head.BaseFee != nil { // The legacy tx gas price suggestion should not add 2x base fee // because all fees are consumed, so it would result in a spiral // upwards. price.Add(price, head.BaseFee) } args.GasPrice = (*hexutil.Big)(price) } } } else { // Both maxPriorityfee and maxFee set by caller. Sanity-check their internal relation if args.MaxFeePerGas.ToInt().Cmp(args.MaxPriorityFeePerGas.ToInt()) < 0 { return args, fmt.Errorf("maxFeePerGas (%v) < maxPriorityFeePerGas (%v)", args.MaxFeePerGas, args.MaxPriorityFeePerGas) } } if args.Value == nil { args.Value = new(hexutil.Big) } if args.Nonce == nil { // get the nonce from the account retriever // ignore error in case tge account doesn't exist yet nonce, _ := b.getAccountNonce(*args.From, true, 0, b.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.TransactionArgs{ From: args.From, To: args.To, Gas: args.Gas, GasPrice: args.GasPrice, MaxFeePerGas: args.MaxFeePerGas, MaxPriorityFeePerGas: args.MaxPriorityFeePerGas, Value: args.Value, Data: input, AccessList: args.AccessList, ChainID: args.ChainID, Nonce: args.Nonce, } blockNr := rpctypes.NewBlockNumber(big.NewInt(0)) estimated, err := b.EstimateGas(callArgs, &blockNr) if err != nil { return args, err } args.Gas = &estimated b.logger.Debug("estimate gas usage automatically", "gas", args.Gas) } if args.ChainID == nil { args.ChainID = (*hexutil.Big)(b.chainID) } return args, nil } // EstimateGas returns an estimate of gas usage for the given smart contract call. func (b *Backend) EstimateGas(args evmtypes.TransactionArgs, blockNrOptional *rpctypes.BlockNumber) (hexutil.Uint64, error) { blockNr := rpctypes.EthPendingBlockNumber if blockNrOptional != nil { blockNr = *blockNrOptional } bz, err := json.Marshal(&args) if err != nil { return 0, err } header, err := b.TendermintBlockByNumber(blockNr) if err != nil { // the error message imitates geth behavior return 0, errors.New("header not found") } req := evmtypes.EthCallRequest{ Args: bz, GasCap: b.RPCGasCap(), ProposerAddress: sdk.ConsAddress(header.Block.ProposerAddress), ChainId: b.chainID.Int64(), } // 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 := b.queryClient.EstimateGas(rpctypes.ContextWithHeight(blockNr.Int64()), &req) if err != nil { return 0, err } return hexutil.Uint64(res.Gas), 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 (b *Backend) DoCall( args evmtypes.TransactionArgs, blockNr rpctypes.BlockNumber, ) (*evmtypes.MsgEthereumTxResponse, error) { bz, err := json.Marshal(&args) if err != nil { return nil, err } header, err := b.TendermintBlockByNumber(blockNr) if err != nil { // the error message imitates geth behavior return nil, errors.New("header not found") } req := evmtypes.EthCallRequest{ Args: bz, GasCap: b.RPCGasCap(), ProposerAddress: sdk.ConsAddress(header.Block.ProposerAddress), ChainId: b.chainID.Int64(), } // 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. ctx := rpctypes.ContextWithHeight(blockNr.Int64()) timeout := b.RPCEVMTimeout() // Setup context so it may be canceled the call has completed // or, in case of unmetered gas, setup a context with a timeout. var cancel context.CancelFunc if timeout > 0 { ctx, cancel = context.WithTimeout(ctx, timeout) } else { ctx, cancel = context.WithCancel(ctx) } // Make sure the context is canceled when the call has completed // this makes sure resources are cleaned up. defer cancel() res, err := b.queryClient.EthCall(ctx, &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 } // GasPrice returns the current gas price based on Ethermint's gas price oracle. func (b *Backend) GasPrice() (*hexutil.Big, error) { var ( result *big.Int err error ) if head := b.CurrentHeader(); head.BaseFee != nil { result, err = b.SuggestGasTipCap(head.BaseFee) if err != nil { return nil, err } result = result.Add(result, head.BaseFee) } else { result = big.NewInt(b.RPCMinGasPrice()) } // return at least GlobalMinGasPrice from FeeMarket module minGasPrice, err := b.GlobalMinGasPrice() if err != nil { return nil, err } minGasPriceInt := minGasPrice.TruncateInt().BigInt() if result.Cmp(minGasPriceInt) < 0 { result = minGasPriceInt } return (*hexutil.Big)(result), nil }