Skip to content

Commit

Permalink
add txsender
Browse files Browse the repository at this point in the history
  • Loading branch information
ian-shim committed Feb 23, 2024
1 parent 4d7d39f commit 3afcc62
Show file tree
Hide file tree
Showing 5 changed files with 229 additions and 157 deletions.
18 changes: 15 additions & 3 deletions chainio/clients/builder.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package clients

import (
"crypto/ecdsa"
"errors"
"math/big"

"github.com/Layr-Labs/eigensdk-go/chainio/clients/avsregistry"
"github.com/Layr-Labs/eigensdk-go/chainio/clients/elcontracts"
"github.com/Layr-Labs/eigensdk-go/chainio/clients/eth"
"github.com/Layr-Labs/eigensdk-go/chainio/clients/txsender"
"github.com/Layr-Labs/eigensdk-go/chainio/txmgr"
chainioutils "github.com/Layr-Labs/eigensdk-go/chainio/utils"
"github.com/Layr-Labs/eigensdk-go/logging"
Expand Down Expand Up @@ -45,8 +48,7 @@ type Clients struct {

func BuildAll(
config BuildAllConfig,
signerAddr gethcommon.Address,
signerFn signerv2.SignerFn,
ecdsaPrivateKey *ecdsa.PrivateKey,
logger logging.Logger,
) (*Clients, error) {
config.validate(logger)
Expand All @@ -66,7 +68,17 @@ func BuildAll(
return nil, types.WrapError(errors.New("Failed to create Eth WS client"), err)
}

txMgr := txmgr.NewSimpleTxManager(ethHttpClient, logger, signerFn, signerAddr)
txSender, err := txsender.NewPrivateKeyTxSender(config.EthHttpUrl, big.NewInt(1), ecdsaPrivateKey, logger)
if err != nil {
return nil, types.WrapError(errors.New("Failed to create transaction sender"), err)
}

signerV2, addr, err := signerv2.SignerFromConfig(signerv2.Config{PrivateKey: ecdsaPrivateKey}, big.NewInt(1))
if err != nil {
panic(err)
}

txMgr := txmgr.NewSimpleTxManager(txSender, ethHttpClient, logger, signerV2, addr)
// creating EL clients: Reader, Writer and Subscriber
elChainReader, elChainWriter, err := config.buildElClients(
ethHttpClient,
Expand Down
181 changes: 181 additions & 0 deletions chainio/clients/txsender/privateKeyTxSender.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package txsender

import (
"context"
"crypto/ecdsa"
"errors"
"fmt"
"math/big"
"time"

"github.com/Layr-Labs/eigensdk-go/chainio/clients/eth"
"github.com/Layr-Labs/eigensdk-go/logging"
"github.com/Layr-Labs/eigensdk-go/signerv2"
sdktypes "github.com/Layr-Labs/eigensdk-go/types"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
)

var (
FallbackGasTipCap = big.NewInt(15_000_000_000)
)

var _ TxSender = (*privateKeyTxSender)(nil)

type privateKeyTxSender struct {
ethClient eth.EthClient
address common.Address
signerFn signerv2.SignerFn
logger logging.Logger

// cache
contracts map[common.Address]*bind.BoundContract
}

func NewPrivateKeyTxSender(ethRPCURL string, chainID *big.Int, ecdsaPrivateKey *ecdsa.PrivateKey, logger logging.Logger) (TxSender, error) {
ethClient, err := eth.NewClient(ethRPCURL)
if err != nil {
return nil, err
}
signer, addr, err := signerv2.SignerFromConfig(signerv2.Config{PrivateKey: ecdsaPrivateKey}, chainID)
if err != nil {
return nil, err
}

return &privateKeyTxSender{
ethClient: ethClient,
address: addr,
signerFn: signer,
logger: logger,
}, nil
}

func (t *privateKeyTxSender) SendTransaction(ctx context.Context, tx *types.Transaction) (TxID, error) {
// Estimate gas and nonce
// can't print tx hash in logs because the tx changes below when we complete and sign it
// so the txHash is meaningless at this point
t.logger.Debug("Estimating gas and nonce")
tx, err := t.estimateGasAndNonce(ctx, tx)
if err != nil {
return "", err
}

t.logger.Debug("Getting signer for tx")
signer, err := t.signerFn(ctx, t.address)
if err != nil {
return "", err
}

t.logger.Debug("Sending transaction")
opts := &bind.TransactOpts{
From: t.address,
Nonce: new(big.Int).SetUint64(tx.Nonce()),
Signer: signer,
Value: tx.Value(),
GasFeeCap: tx.GasFeeCap(),
GasTipCap: tx.GasTipCap(),
GasLimit: tx.Gas(),
Context: ctx,
}

contract := t.contracts[*tx.To()]
// if the contract has not been cached
if contract == nil {
// create a dummy bound contract tied to the `to` address of the transaction
contract = bind.NewBoundContract(*tx.To(), abi.ABI{}, t.ethClient, t.ethClient, t.ethClient)
// cache the contract for later use
t.contracts[*tx.To()] = contract
}

tx, err = contract.RawTransact(opts, tx.Data())
if err != nil {
return "", sdktypes.WrapError(fmt.Errorf("send: tx %v failed.", tx.Hash().String()), err)
}

return tx.Hash().Hex(), nil
}

func (t *privateKeyTxSender) WaitForTransactionReceipt(ctx context.Context, txID TxID) (*types.Receipt, error) {
txHash := common.HexToHash(txID)
// TODO: make this ticker adjustable
queryTicker := time.NewTicker(2 * time.Second)
defer queryTicker.Stop()
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-queryTicker.C:
if receipt := t.queryReceipt(ctx, txHash); receipt != nil {
return receipt, nil
}
}
}
}

func (t *privateKeyTxSender) queryReceipt(ctx context.Context, txHash common.Hash) *types.Receipt {
receipt, err := t.ethClient.TransactionReceipt(ctx, txHash)
if errors.Is(err, ethereum.NotFound) {
t.logger.Info("Transaction not yet mined", "hash", txHash)
return nil
} else if err != nil {
t.logger.Info("Receipt retrieval failed", "hash", txHash, "err", err)
return nil
} else if receipt == nil {
t.logger.Warn("Receipt and error are both nil", "hash", txHash)
return nil
}

return receipt
}

func (t *privateKeyTxSender) SenderAddress() common.Address {
return t.address
}

// estimateGasAndNonce we are explicitly implementing this because
// * We want to support legacy transactions (i.e. not dynamic fee)
// * We want to support gas management, i.e. add buffer to gas limit
func (t *privateKeyTxSender) estimateGasAndNonce(ctx context.Context, tx *types.Transaction) (*types.Transaction, error) {
gasTipCap, err := t.ethClient.SuggestGasTipCap(ctx)
if err != nil {
// If the transaction failed because the backend does not support
// eth_maxPriorityFeePerGas, fallback to using the default constant.
t.logger.Info("eth_maxPriorityFeePerGas is unsupported by current backend, using fallback gasTipCap")
gasTipCap = FallbackGasTipCap
}

header, err := t.ethClient.HeaderByNumber(ctx, nil)
if err != nil {
return nil, err
}

gasFeeCap := new(big.Int).Add(header.BaseFee, gasTipCap)

gasLimit, err := t.ethClient.EstimateGas(ctx, ethereum.CallMsg{
From: t.address,
To: tx.To(),
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
Value: tx.Value(),
Data: tx.Data(),
})
if err != nil {
return nil, err
}

rawTx := &types.DynamicFeeTx{
ChainID: tx.ChainId(),
To: tx.To(),
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
Data: tx.Data(),
Value: tx.Value(),
Gas: gasLimit, // TODO(add buffer)
Nonce: tx.Nonce(), // We are not doing any nonce management for now but we probably should later for more robustness
}

return types.NewTx(rawTx), nil
}
17 changes: 17 additions & 0 deletions chainio/clients/txsender/txsender.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package txsender

import (
"context"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
)

type TxID = string

// TxSender is an interface for signing and sending transactions to the network
type TxSender interface {
SendTransaction(ctx context.Context, tx *types.Transaction) (TxID, error)
WaitForTransactionReceipt(ctx context.Context, txID TxID) (*types.Receipt, error)
SenderAddress() common.Address
}
Loading

0 comments on commit 3afcc62

Please sign in to comment.