github.com/0xsequence/ethkit@v1.25.0/ethtxn/ethtxn.go (about)

     1  package ethtxn
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"math/big"
     7  
     8  	"github.com/0xsequence/ethkit/ethrpc"
     9  	"github.com/0xsequence/ethkit/go-ethereum"
    10  	"github.com/0xsequence/ethkit/go-ethereum/common"
    11  	"github.com/0xsequence/ethkit/go-ethereum/core/types"
    12  )
    13  
    14  type TransactionRequest struct {
    15  	// Ethereum account to send the transaction from. Optional, will automatically be set.
    16  	From common.Address
    17  
    18  	// To is the recipient address, can be account, contract or nil. If `to` is nil, it will assume contract creation
    19  	To *common.Address
    20  
    21  	// Nonce is the nonce of the transaction for the sender. If this value is left empty (nil), it will
    22  	// automatically be assigned.
    23  	Nonce *big.Int
    24  
    25  	// GasLimit is the total gas the transaction is expected the consume. If this value is left empty (0), it will
    26  	// automatically be estimated and assigned.
    27  	GasLimit uint64
    28  
    29  	// GasPrice (in WEI) offering to pay for per unit of gas. If this value is left empty (nil), it will
    30  	// automatically be sampled and assigned.
    31  	// Used as GasFeeCap, but name kept for compatibility reasons
    32  	GasPrice *big.Int
    33  
    34  	// GasTip (in WEI) optional offering to pay for per unit of gas to the miner.
    35  	// If this value is left empty (nil), it will be considered a pre-EIP1559 or "legacy" transaction
    36  	GasTip *big.Int
    37  
    38  	// AccessList optional key-values to pre-import
    39  	// saves cost by pre-importing storage related values before executing the tx
    40  	AccessList types.AccessList
    41  
    42  	// ETHValue (in WEI) amount of ETH currency to send with this transaction. Optional.
    43  	ETHValue *big.Int
    44  
    45  	// Data is calldata / input when calling or creating a contract. Optional.
    46  	Data []byte
    47  }
    48  
    49  type WaitReceipt func(ctx context.Context) (*types.Receipt, error)
    50  
    51  // NewTransaction prepares a transaction for delivery, however the transaction still needs to be signed
    52  // before it can be sent.
    53  func NewTransaction(ctx context.Context, provider *ethrpc.Provider, txnRequest *TransactionRequest) (*types.Transaction, error) {
    54  	if txnRequest == nil {
    55  		return nil, fmt.Errorf("ethtxn: txnRequest is required")
    56  	}
    57  	if provider == nil {
    58  		return nil, fmt.Errorf("ethtxn: provider is not set")
    59  	}
    60  
    61  	if txnRequest.Nonce == nil {
    62  		nonce, err := provider.PendingNonceAt(ctx, txnRequest.From)
    63  		if err != nil {
    64  			return nil, fmt.Errorf("ethtxn: failed to get pending nonce: %w", err)
    65  		}
    66  		txnRequest.Nonce = big.NewInt(0).SetUint64(nonce)
    67  	}
    68  
    69  	if txnRequest.GasPrice == nil {
    70  		// Get suggested gas price, the user can change this on their own too
    71  		gasPrice, err := provider.SuggestGasPrice(ctx)
    72  		if err != nil {
    73  			return nil, fmt.Errorf("ethtxn: %w", err)
    74  		}
    75  		txnRequest.GasPrice = gasPrice
    76  	}
    77  
    78  	if txnRequest.GasLimit == 0 {
    79  		callMsg := ethereum.CallMsg{
    80  			From:     txnRequest.From,
    81  			To:       txnRequest.To,
    82  			Gas:      0, // estimating this value
    83  			GasPrice: txnRequest.GasPrice,
    84  			Value:    txnRequest.ETHValue,
    85  			Data:     txnRequest.Data,
    86  		}
    87  
    88  		gasLimit, err := provider.EstimateGas(ctx, callMsg)
    89  		if err != nil {
    90  			return nil, fmt.Errorf("ethtxn: %w", err)
    91  		}
    92  		txnRequest.GasLimit = gasLimit
    93  	}
    94  
    95  	if txnRequest.To == nil && len(txnRequest.Data) == 0 {
    96  		return nil, fmt.Errorf("ethtxn: contract creation txn request requires data field")
    97  	}
    98  
    99  	var rawTx *types.Transaction
   100  	if txnRequest.GasTip != nil {
   101  		chainId, err := provider.ChainID(ctx)
   102  		if err != nil {
   103  			return nil, err
   104  		}
   105  
   106  		rawTx = types.NewTx(&types.DynamicFeeTx{
   107  			ChainID:    chainId,
   108  			To:         txnRequest.To,
   109  			Nonce:      txnRequest.Nonce.Uint64(),
   110  			Value:      txnRequest.ETHValue,
   111  			GasFeeCap:  txnRequest.GasPrice,
   112  			GasTipCap:  txnRequest.GasTip,
   113  			Data:       txnRequest.Data,
   114  			Gas:        txnRequest.GasLimit,
   115  			AccessList: txnRequest.AccessList,
   116  		})
   117  	} else if txnRequest.AccessList != nil {
   118  		chainId, err := provider.ChainID(ctx)
   119  		if err != nil {
   120  			return nil, err
   121  		}
   122  
   123  		rawTx = types.NewTx(&types.AccessListTx{
   124  			ChainID:    chainId,
   125  			To:         txnRequest.To,
   126  			Gas:        txnRequest.GasLimit,
   127  			GasPrice:   txnRequest.GasPrice,
   128  			Data:       txnRequest.Data,
   129  			Nonce:      txnRequest.Nonce.Uint64(),
   130  			Value:      txnRequest.ETHValue,
   131  			AccessList: txnRequest.AccessList,
   132  		})
   133  	} else {
   134  		rawTx = types.NewTx(&types.LegacyTx{
   135  			To:       txnRequest.To,
   136  			Gas:      txnRequest.GasLimit,
   137  			GasPrice: txnRequest.GasPrice,
   138  			Data:     txnRequest.Data,
   139  			Nonce:    txnRequest.Nonce.Uint64(),
   140  			Value:    txnRequest.ETHValue,
   141  		})
   142  	}
   143  
   144  	return rawTx, nil
   145  }
   146  
   147  func SendTransaction(ctx context.Context, provider *ethrpc.Provider, signedTx *types.Transaction) (*types.Transaction, WaitReceipt, error) {
   148  	if provider == nil {
   149  		return nil, nil, fmt.Errorf("ethtxn (SendTransaction): provider is not set")
   150  	}
   151  
   152  	waitFn := func(ctx context.Context) (*types.Receipt, error) {
   153  		return ethrpc.WaitForTxnReceipt(ctx, provider, signedTx.Hash())
   154  	}
   155  
   156  	return signedTx, waitFn, provider.SendTransaction(ctx, signedTx)
   157  }
   158  
   159  var zeroBigInt = big.NewInt(0)
   160  
   161  func AsMessage(txn *types.Transaction) (types.Message, error) {
   162  	return AsMessageWithSigner(txn, types.NewLondonSigner(txn.ChainId()), nil)
   163  }
   164  
   165  // AsMessageWithSigner decodes a transaction payload, and will check v, r, s values and skips
   166  // zero'd numbers which is the case for Polygon state sync transactions:
   167  // https://wiki.polygon.technology/docs/pos/state-sync/how-state-sync-works#state-sync-logs-and-bor-block-receipt
   168  func AsMessageWithSigner(txn *types.Transaction, signer types.Signer, baseFee *big.Int) (types.Message, error) {
   169  	v, r, s := txn.RawSignatureValues()
   170  	if v.Cmp(zeroBigInt) == 0 && r.Cmp(zeroBigInt) == 0 && s.Cmp(zeroBigInt) == 0 {
   171  		m := types.NewMessage(
   172  			common.Address{}, txn.To(), txn.Nonce(), txn.Value(),
   173  			txn.Gas(), txn.GasPrice(), txn.GasFeeCap(), txn.GasTipCap(),
   174  			txn.Data(), txn.AccessList(), true,
   175  		)
   176  		return m, nil
   177  	} else {
   178  		return txn.AsMessage(signer, baseFee)
   179  	}
   180  }