package util import ( "context" "crypto/ecdsa" "errors" "fmt" "math/big" "time" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi/bind" "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/ethclient" ) var ( ErrEvmBroadcastTimeout = errors.New("timed out waiting for tx to be committed to block") // ErrEvmTxFailed is returned when a tx is committed to a block, but the receipt status is 0. // this means the tx failed. we don't have debug_traceTransaction RPC command so the best way // to determine the problem is to attempt to make the tx manually. ErrEvmTxFailed = errors.New("transaction was committed but failed. likely an execution revert by contract code") ) type EvmTxRequest struct { Tx *ethtypes.Transaction Data interface{} } type EvmTxResponse struct { Request EvmTxRequest TxHash common.Hash Err error } type ErrEvmFailedToSign struct{ Err error } func (e ErrEvmFailedToSign) Error() string { return fmt.Sprintf("failed to sign tx: %s", e.Err) } type ErrEvmFailedToBroadcast struct{ Err error } func (e ErrEvmFailedToBroadcast) Error() string { return fmt.Sprintf("failed to broadcast tx: %s", e.Err) } // EvmSigner manages signing and broadcasting requests to transfer Erc20 tokens // Will work for calling all contracts that have func signature `transfer(address,uint256)` type EvmSigner struct { signerAddress common.Address Auth *bind.TransactOpts EvmClient *ethclient.Client } func NewEvmSigner( evmClient *ethclient.Client, privKey *ecdsa.PrivateKey, chainId *big.Int, ) (*EvmSigner, error) { auth, err := bind.NewKeyedTransactorWithChainID(privKey, chainId) if err != nil { return &EvmSigner{}, err } publicKey := privKey.Public() publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) if !ok { return &EvmSigner{}, fmt.Errorf("cannot assert type: publicKey is not of type *ecdsa.PublicKey") } return &EvmSigner{ Auth: auth, signerAddress: crypto.PubkeyToAddress(*publicKeyECDSA), EvmClient: evmClient, }, nil } func (s *EvmSigner) Run(requests <-chan EvmTxRequest) <-chan EvmTxResponse { responses := make(chan EvmTxResponse) // receive tx requests, sign & broadcast them. // Responses are sent once the tx is added to the pending tx pool. // To see result, use TransactionReceipt after tx has been included in a block. go func() { for { // wait for incoming request req := <-requests signedTx, err := s.Auth.Signer(s.signerAddress, req.Tx) if err != nil { err = ErrEvmFailedToSign{Err: err} } else { err = s.EvmClient.SendTransaction(context.Background(), signedTx) if err != nil { err = ErrEvmFailedToBroadcast{Err: err} } } responses <- EvmTxResponse{ Request: req, TxHash: signedTx.Hash(), Err: err, } } }() return responses } func (s *EvmSigner) Address() common.Address { return s.signerAddress } // WaitForEvmTxReceipt polls for a tx receipt and errors on timeout. // If the receipt comes back, but with status 0 (failed), an error is returned. func WaitForEvmTxReceipt(client *ethclient.Client, txHash common.Hash, timeout time.Duration) (*ethtypes.Receipt, error) { var receipt *ethtypes.Receipt var err error outOfTime := time.After(timeout) for { select { case <-outOfTime: err = ErrEvmBroadcastTimeout default: receipt, err = client.TransactionReceipt(context.Background(), txHash) if errors.Is(err, ethereum.NotFound) { // tx still not committed to a block. retry! time.Sleep(100 * time.Millisecond) continue } // a response status of 0 means the tx was successfully committed but failed to execute if receipt.Status == 0 { err = ErrEvmTxFailed } } break } return receipt, err }