-
Notifications
You must be signed in to change notification settings - Fork 58
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
229 additions
and
157 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.