From 6037ecc715f7a28f73bf1372ceeda922c102f064 Mon Sep 17 00:00:00 2001 From: Cal Bera Date: Wed, 17 Jan 2024 23:08:32 -0500 Subject: [PATCH] feat(txr): Allow custom gas options on tx requests, callbacks on tx result (#50) * initial * working * lint --- core/transactor/factory/factory.go | 89 ++++++++++++++++------------ core/transactor/factory/multicall.go | 30 ++++++---- core/transactor/tracker/tracker.go | 27 ++++++--- core/transactor/transactor.go | 15 +++-- core/transactor/types/packer.go | 14 +++-- core/transactor/types/types.go | 19 +++++- 6 files changed, 121 insertions(+), 73 deletions(-) diff --git a/core/transactor/factory/factory.go b/core/transactor/factory/factory.go index 0860f28..6ab9319 100644 --- a/core/transactor/factory/factory.go +++ b/core/transactor/factory/factory.go @@ -44,13 +44,13 @@ func New(noncer Noncer, signer kmstypes.TxSigner, mc3Batcher *Multicall3Batcher) func (f *Factory) BuildTransactionFromRequests( ctx context.Context, txReqs []*types.TxRequest, -) (*coretypes.Transaction, error) { +) (*coretypes.Transaction, types.ResultCallback, error) { switch len(txReqs) { case 0: - return nil, errors.New("no transaction requests provided") + return nil, nil, errors.New("no transaction requests provided") case 1: // if len(txReqs) == 1 then build a single transaction. - return f.BuildTransaction(ctx, txReqs[0].To, txReqs[0].Value, txReqs[0].Data) + return f.BuildTransaction(ctx, txReqs[0]) default: // len(txReqs) > 1 then build a multicall transaction. ar := f.mc3Batcher.BatchTxRequests(ctx, txReqs) @@ -58,63 +58,76 @@ func (f *Factory) BuildTransactionFromRequests( // Build the transaction to include the calldata. // ar.To should be the Multicall3 contract address // ar.Data should be the calldata with the batched transactions. - // ar.Value TODO: needs to be implemented (right now current is always 0). - return f.BuildTransaction(ctx, ar.To, ar.Value, ar.Data) + // ar.Value is the sum of the values of the batched transactions. + return f.BuildTransaction(ctx, ar) } } // BuildTransaction builds a transaction with the configured signer. func (f *Factory) BuildTransaction( ctx context.Context, - to common.Address, - value *big.Int, - data []byte, -) (*coretypes.Transaction, error) { - var err error - ethClient := sdk.UnwrapContext(ctx).Chain() - gasFeeCap, err := ethClient.SuggestGasPrice(ctx) - if err != nil { - return nil, err - } - - gasTipCap, err := ethClient.SuggestGasTipCap(ctx) - if err != nil { - return nil, err - } + txReq *types.TxRequest, +) (*coretypes.Transaction, types.ResultCallback, error) { + var ( + gasOpts = txReq.GasOpts + err error + ) + ethClient := sdk.UnwrapContext(ctx).Chain() if f.chainID == nil { f.chainID, err = ethClient.ChainID(ctx) if err != nil { - return nil, err + return nil, nil, err } } nonce, err := f.noncer.Acquire(ctx) if err != nil { - return nil, err + return nil, nil, err } txData := &coretypes.DynamicFeeTx{ - ChainID: f.chainID, - Nonce: nonce, - GasFeeCap: gasFeeCap, - GasTipCap: gasTipCap, - To: &to, - Value: value, - Data: data, + ChainID: f.chainID, + To: &txReq.To, + Value: txReq.Value, + Data: txReq.Data, + Nonce: nonce, } - if txData.Gas, err = ethClient.EstimateGas(ctx, ethereum.CallMsg{ - From: f.signerAddress, - To: txData.To, - GasFeeCap: txData.GasFeeCap, - Value: txData.Value, - Data: txData.Data, - }); err != nil { - return nil, err + if gasOpts != nil && gasOpts.GasFeeCap != nil { + txData.GasFeeCap = gasOpts.GasFeeCap + } else { + txData.GasFeeCap, err = ethClient.SuggestGasPrice(ctx) + if err != nil { + return nil, nil, err + } + } + + if gasOpts != nil && gasOpts.GasTipCap != nil { + txData.GasTipCap = gasOpts.GasTipCap + } else { + txData.GasTipCap, err = ethClient.SuggestGasTipCap(ctx) + if err != nil { + return nil, nil, err + } + } + + if gasOpts != nil && gasOpts.GasLimit > 0 { + txData.Gas = gasOpts.GasLimit + } else { + if txData.Gas, err = ethClient.EstimateGas(ctx, ethereum.CallMsg{ + From: f.signerAddress, + To: txData.To, + GasFeeCap: txData.GasFeeCap, + Value: txData.Value, + Data: txData.Data, + }); err != nil { + return nil, nil, err + } } - return f.SignTransaction(coretypes.NewTx(txData)) + signedTx, err := f.SignTransaction(coretypes.NewTx(txData)) + return signedTx, txReq.Resultor, err } // signTransaction signs a transaction with the configured signer. diff --git a/core/transactor/factory/multicall.go b/core/transactor/factory/multicall.go index 1e9f464..52f74b0 100644 --- a/core/transactor/factory/multicall.go +++ b/core/transactor/factory/multicall.go @@ -7,7 +7,6 @@ import ( "github.com/berachain/offchain-sdk/contracts/bindings" "github.com/berachain/offchain-sdk/core/transactor/types" - "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" ) @@ -15,12 +14,16 @@ import ( // --private-key=0xfffdbb37105441e14b0ee6330d855d8504ff39e705c3afa8f859ac9865f99306. type Multicall3Batcher struct { contractAddress common.Address + packer *types.Packer } // NewMulticall3Batcher creates a new Multicall3Batcher instance. func NewMulticall3Batcher(address common.Address) *Multicall3Batcher { return &Multicall3Batcher{ contractAddress: address, + packer: &types.Packer{ + Metadata: bindings.Multicall3MetaData, + }, } } @@ -30,7 +33,18 @@ func (mc *Multicall3Batcher) BatchTxRequests( txReqs []*types.TxRequest, ) *types.TxRequest { calls := make([]bindings.Multicall3Call, len(txReqs)) + totalValue := big.NewInt(0) + var resultor types.ResultCallback + for i, txReq := range txReqs { + // use the summed value for the batched transaction. + totalValue = totalValue.Add(totalValue, txReq.Value) + + // use the first resultor as the result callback for the batched transaction. + if resultor == nil && txReq.Resultor != nil { + resultor = txReq.Resultor + } + call := bindings.Multicall3Call{ Target: txReq.To, CallData: txReq.Data, @@ -38,16 +52,8 @@ func (mc *Multicall3Batcher) BatchTxRequests( calls[i] = call } - txRequest, err := (&types.Packer[*bind.MetaData]{Metadata: bindings.Multicall3MetaData}). - CreateTxRequest( - mc.contractAddress, - big.NewInt(0), - "aggregate", - calls, - ) - if err != nil { - return nil - } - + txRequest, _ := mc.packer.CreateTxRequest( + mc.contractAddress, totalValue, resultor, "aggregate", calls, + ) return txRequest } diff --git a/core/transactor/tracker/tracker.go b/core/transactor/tracker/tracker.go index 6055279..93f3412 100644 --- a/core/transactor/tracker/tracker.go +++ b/core/transactor/tracker/tracker.go @@ -6,6 +6,7 @@ import ( "time" "github.com/berachain/offchain-sdk/core/transactor/event" + "github.com/berachain/offchain-sdk/core/transactor/types" sdk "github.com/berachain/offchain-sdk/types" "github.com/ethereum/go-ethereum" @@ -45,17 +46,18 @@ func (t *Tracker) Unsubscribe(ch chan *InFlightTx) { } // Track adds a transaction to the in-flight list. -func (t *Tracker) Track(ctx context.Context, tx *InFlightTx, async bool) { +func (t *Tracker) Track( + ctx context.Context, tx *InFlightTx, async bool, resultor types.ResultCallback, +) { if async { - // todo: handle error - go t.track(ctx, tx) - return + go t.track(ctx, tx, resultor) + } else { + t.track(ctx, tx, resultor) } - t.track(ctx, tx) } // track adds a transaction to the in-flight list. -func (t *Tracker) track(ctx context.Context, tx *InFlightTx) { +func (t *Tracker) track(ctx context.Context, tx *InFlightTx, resultor types.ResultCallback) { // If there is already a transaction that is being tracked for this nonce. if oldTx := t.noncer.GetInFlight(tx.Nonce()); oldTx != nil { // Watch for the old transaction to be replaced. @@ -67,7 +69,7 @@ func (t *Tracker) track(ctx context.Context, tx *InFlightTx) { } t.noncer.SetInFlight(tx) - t.watchTx(ctx, tx) + t.watchTx(ctx, tx, resultor) } // watchTxForReplacement is watching for a transaction to be replaced by another. @@ -96,13 +98,20 @@ loop: return nil } -func (t *Tracker) watchTx(ctx context.Context, tx *InFlightTx) { +func (t *Tracker) watchTx(ctx context.Context, tx *InFlightTx, resultor types.ResultCallback) { sCtx := sdk.UnwrapContext(ctx) ethClient := sCtx.Chain() + var ( + receipt *coretypes.Receipt + err error + ) // We want to notify the dispatcher at the end of this function. defer t.dispatcher.Dispatch(tx) + // Call the result callback for the transaction with the receipt. + defer resultor(sCtx, receipt) + // Loop until the context is done, the transaction status is determined, // or the timeout is reached. for { @@ -116,7 +125,7 @@ func (t *Tracker) watchTx(ctx context.Context, tx *InFlightTx) { return default: // Else check for the receipt again. - receipt, err := ethClient.TransactionReceipt(ctx, tx.Hash()) + receipt, err = ethClient.TransactionReceipt(ctx, tx.Hash()) switch { case errors.Is(err, ethereum.NotFound): time.Sleep(retryPendingBackoff) diff --git a/core/transactor/transactor.go b/core/transactor/transactor.go index 403ae5c..1c8abb3 100644 --- a/core/transactor/transactor.go +++ b/core/transactor/transactor.go @@ -176,7 +176,7 @@ func (t *TxrV2) retrieveBatch(_ context.Context) ([]string, []*types.TxRequest) func (t *TxrV2) sendAndTrack( ctx context.Context, msgIDs []string, batch []*types.TxRequest, ) error { - tx, err := t.factory.BuildTransactionFromRequests(ctx, batch) + tx, resultor, err := t.factory.BuildTransactionFromRequests(ctx, batch) if err != nil { return err } @@ -189,9 +189,14 @@ func (t *TxrV2) sendAndTrack( // t.logger.Debug("📡 sent transaction", "tx-hash", tx.Hash().Hex(), "tx-reqs", len(batch)) // Spin off a goroutine to track the transaction. - t.tracker.Track(ctx, &tracker.InFlightTx{ - Transaction: tx, - MsgIDs: msgIDs, - }, true) + t.tracker.Track( + ctx, + &tracker.InFlightTx{ + Transaction: tx, + MsgIDs: msgIDs, + }, + true, + resultor, + ) return nil } diff --git a/core/transactor/types/packer.go b/core/transactor/types/packer.go index 574aa21..c36d964 100644 --- a/core/transactor/types/packer.go +++ b/core/transactor/types/packer.go @@ -13,14 +13,15 @@ type Metadata interface { } // Packer struct for packing metadata. -type Packer[T Metadata] struct { - Metadata T +type Packer struct { + Metadata } // CreateTxRequest function for creating transaction request. -func (p *Packer[T]) CreateTxRequest( +func (p *Packer) CreateTxRequest( to common.Address, // address to send transaction to value *big.Int, // value to be sent in the transaction + resultor ResultCallback, // result callback for the transaction method string, // method to be called in the transaction args ...interface{}, // arguments for the method ) (*TxRequest, error) { // returns a transaction request or an error @@ -35,8 +36,9 @@ func (p *Packer[T]) CreateTxRequest( } return &TxRequest{ - To: to, - Data: bz, - Value: value, + To: to, + Data: bz, + Value: value, + Resultor: resultor, }, nil // return a new transaction request } diff --git a/core/transactor/types/types.go b/core/transactor/types/types.go index dca0759..53ae5bd 100644 --- a/core/transactor/types/types.go +++ b/core/transactor/types/types.go @@ -4,9 +4,11 @@ import ( "encoding/json" "math/big" + sdk "github.com/berachain/offchain-sdk/types" "github.com/berachain/offchain-sdk/types/queue/types" "github.com/ethereum/go-ethereum/common" + coretypes "github.com/ethereum/go-ethereum/core/types" ) // TxResultType represents the type of error that occurred when sending a tx. @@ -35,9 +37,19 @@ const ( // Nil if the tx was successful, RevertReason nil if we have an ErrSend, ErrReceive, ErrDecode. type ( TxRequest struct { - To common.Address `json:"to"` - Value *big.Int `json:"value"` - Data []byte `json:"data"` + To common.Address `json:"to"` + Value *big.Int `json:"value"` + Data []byte `json:"data"` + GasOpts *GasOpts `json:"gasOpts"` + Resultor ResultCallback + } + + ResultCallback func(*sdk.Context, *coretypes.Receipt) + + GasOpts struct { + GasTipCap *big.Int `json:"gasTipCap"` + GasFeeCap *big.Int `json:"gasFeeCap"` + GasLimit uint64 `json:"gasLimit"` } TxResult struct { @@ -55,6 +67,7 @@ func (TxRequest) New() types.Marshallable { // NewTxResult returns a new TxResult with the given type and error. func (tx TxRequest) Marshal() ([]byte, error) { + //nolint:staticcheck,SA1026 // Resultor is not needed if marshalled. return json.Marshal(tx) }