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 }