Skip to content

Commit

Permalink
feat(txr): Allow custom gas options on tx requests, callbacks on tx r…
Browse files Browse the repository at this point in the history
…esult (#50)

* initial

* working

* lint
  • Loading branch information
calbera authored Jan 18, 2024
1 parent 41a3d9b commit 6037ecc
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 73 deletions.
89 changes: 51 additions & 38 deletions core/transactor/factory/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,77 +44,90 @@ 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)

// 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.
Expand Down
30 changes: 18 additions & 12 deletions core/transactor/factory/multicall.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,23 @@ 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"
)

// forge create Multicall3 --rpc-url=http://devnet.beraswillmakeit.com:8545
// --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,
},
}
}

Expand All @@ -30,24 +33,27 @@ 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,
}
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
}
27 changes: 18 additions & 9 deletions core/transactor/tracker/tracker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
Expand Down
15 changes: 10 additions & 5 deletions core/transactor/transactor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
}
14 changes: 8 additions & 6 deletions core/transactor/types/packer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
19 changes: 16 additions & 3 deletions core/transactor/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
}

Expand Down

0 comments on commit 6037ecc

Please sign in to comment.