Skip to content

Commit

Permalink
[HotWallet] Add txsender (#124)
Browse files Browse the repository at this point in the history
* add txsender

* [HotWallet] Fireblock client more methods (#130)

* add txsender

* more methods in fireblocks client

* [HotWallet] Implement Fireblocks TxSender (#131)

* more methods in fireblocks client

* add fireblocks txsender

* address feedback
  • Loading branch information
ian-shim authored Feb 27, 2024
1 parent d145eb2 commit 65cc80e
Show file tree
Hide file tree
Showing 32 changed files with 927 additions and 241 deletions.
6 changes: 3 additions & 3 deletions chainio/clients/avsregistry/reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ type AvsRegistryChainReader struct {
registryCoordinator *regcoord.ContractRegistryCoordinator
operatorStateRetriever *opstateretriever.ContractOperatorStateRetriever
stakeRegistry *stakeregistry.ContractStakeRegistry
ethClient eth.EthClient
ethClient eth.Client
}

// forces AvsReader to implement the clients.ReaderInterface interface
Expand All @@ -99,7 +99,7 @@ func NewAvsRegistryChainReader(
operatorStateRetriever *opstateretriever.ContractOperatorStateRetriever,
stakeRegistry *stakeregistry.ContractStakeRegistry,
logger logging.Logger,
ethClient eth.EthClient,
ethClient eth.Client,
) *AvsRegistryChainReader {
return &AvsRegistryChainReader{
blsApkRegistryAddr: blsApkRegistryAddr,
Expand All @@ -115,7 +115,7 @@ func NewAvsRegistryChainReader(
func BuildAvsRegistryChainReader(
registryCoordinatorAddr gethcommon.Address,
operatorStateRetrieverAddr gethcommon.Address,
ethClient eth.EthClient,
ethClient eth.Client,
logger logging.Logger,
) (*AvsRegistryChainReader, error) {
contractRegistryCoordinator, err := regcoord.NewContractRegistryCoordinator(registryCoordinatorAddr, ethClient)
Expand Down
2 changes: 1 addition & 1 deletion chainio/clients/avsregistry/subscriber.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func NewAvsRegistryChainSubscriber(

func BuildAvsRegistryChainSubscriber(
blsApkRegistryAddr common.Address,
ethWsClient eth.EthClient,
ethWsClient eth.Client,
logger logging.Logger,
) (*AvsRegistryChainSubscriber, error) {
blsapkreg, err := blsapkreg.NewContractBLSApkRegistry(blsApkRegistryAddr, ethWsClient)
Expand Down
6 changes: 3 additions & 3 deletions chainio/clients/avsregistry/writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ type AvsRegistryChainWriter struct {
blsApkRegistry *blsapkregistry.ContractBLSApkRegistry
elReader elcontracts.ELReader
logger logging.Logger
ethClient eth.EthClient
ethClient eth.Client
txMgr txmgr.TxManager
}

Expand All @@ -93,7 +93,7 @@ func NewAvsRegistryChainWriter(
blsApkRegistry *blsapkregistry.ContractBLSApkRegistry,
elReader elcontracts.ELReader,
logger logging.Logger,
ethClient eth.EthClient,
ethClient eth.Client,
txMgr txmgr.TxManager,
) (*AvsRegistryChainWriter, error) {
return &AvsRegistryChainWriter{
Expand All @@ -113,7 +113,7 @@ func BuildAvsRegistryChainWriter(
registryCoordinatorAddr gethcommon.Address,
operatorStateRetrieverAddr gethcommon.Address,
logger logging.Logger,
ethClient eth.EthClient,
ethClient eth.Client,
txMgr txmgr.TxManager,
) (*AvsRegistryChainWriter, error) {
registryCoordinator, err := regcoord.NewContractRegistryCoordinator(registryCoordinatorAddr, ethClient)
Expand Down
27 changes: 19 additions & 8 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 @@ -37,16 +40,15 @@ type Clients struct {
AvsRegistryChainWriter *avsregistry.AvsRegistryChainWriter
ElChainReader *elcontracts.ELChainReader
ElChainWriter *elcontracts.ELChainWriter
EthHttpClient *eth.Client
EthWsClient *eth.Client
EthHttpClient eth.Client
EthWsClient eth.Client
Metrics *metrics.EigenMetrics // exposes main avs node spec metrics that need to be incremented by avs code and used to start the metrics server
PrometheusRegistry *prometheus.Registry // Used if avs teams need to register avs-specific metrics
}

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,16 @@ func BuildAll(
return nil, types.WrapError(errors.New("Failed to create Eth WS client"), err)
}

txMgr := txmgr.NewSimpleTxManager(ethHttpClient, logger, signerFn, signerAddr)
signerV2, addr, err := signerv2.SignerFromConfig(signerv2.Config{PrivateKey: ecdsaPrivateKey}, big.NewInt(1))
if err != nil {
panic(err)
}

txSender, err := txsender.NewPrivateKeyTxSender(ethHttpClient, signerV2, addr, logger)
if err != nil {
return nil, types.WrapError(errors.New("Failed to create transaction sender"), err)
}
txMgr := txmgr.NewSimpleTxManager(txSender, ethHttpClient, logger, signerV2, addr)
// creating EL clients: Reader, Writer and Subscriber
elChainReader, elChainWriter, err := config.buildElClients(
ethHttpClient,
Expand Down Expand Up @@ -105,7 +116,7 @@ func BuildAll(
}

func (config *BuildAllConfig) buildElClients(
ethHttpClient eth.EthClient,
ethHttpClient eth.Client,
txMgr txmgr.TxManager,
logger logging.Logger,
eigenMetrics *metrics.EigenMetrics,
Expand Down Expand Up @@ -170,8 +181,8 @@ func (config *BuildAllConfig) buildElClients(

func (config *BuildAllConfig) buildAvsClients(
elReader elcontracts.ELReader,
ethHttpClient eth.EthClient,
ethWsClient eth.EthClient,
ethHttpClient eth.Client,
ethWsClient eth.Client,
txMgr txmgr.TxManager,
logger logging.Logger,
) (*avsregistry.AvsRegistryChainReader, *avsregistry.AvsRegistryChainSubscriber, *avsregistry.AvsRegistryChainWriter, error) {
Expand Down
6 changes: 3 additions & 3 deletions chainio/clients/elcontracts/reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ type ELChainReader struct {
delegationManager delegationmanager.ContractDelegationManagerCalls
strategyManager strategymanager.ContractStrategyManagerCalls
avsDirectory avsdirectory.ContractAVSDirectoryCalls
ethClient eth.EthClient
ethClient eth.Client
}

// forces EthReader to implement the chainio.Reader interface
Expand All @@ -79,7 +79,7 @@ func NewELChainReader(
strategyManager strategymanager.ContractStrategyManagerCalls,
avsDirectory avsdirectory.ContractAVSDirectoryCalls,
logger logging.Logger,
ethClient eth.EthClient,
ethClient eth.Client,
) *ELChainReader {
return &ELChainReader{
slasher: slasher,
Expand All @@ -94,7 +94,7 @@ func NewELChainReader(
func BuildELChainReader(
delegationManagerAddr gethcommon.Address,
avsDirectoryAddr gethcommon.Address,
ethClient eth.EthClient,
ethClient eth.Client,
logger logging.Logger,
) (*ELChainReader, error) {
elContractBindings, err := chainioutils.NewEigenlayerContractBindings(
Expand Down
6 changes: 3 additions & 3 deletions chainio/clients/elcontracts/writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ type ELChainWriter struct {
strategyManager strategymanager.ContractStrategyManagerTransacts
strategyManagerAddr gethcommon.Address
elChainReader ELReader
ethClient eth.EthClient
ethClient eth.Client
logger logging.Logger
txMgr txmgr.TxManager
}
Expand All @@ -53,7 +53,7 @@ func NewELChainWriter(
strategyManager strategymanager.ContractStrategyManagerTransacts,
strategyManagerAddr gethcommon.Address,
elChainReader ELReader,
ethClient eth.EthClient,
ethClient eth.Client,
logger logging.Logger,
eigenMetrics metrics.Metrics,
txMgr txmgr.TxManager,
Expand All @@ -73,7 +73,7 @@ func NewELChainWriter(
func BuildELChainWriter(
delegationManagerAddr gethcommon.Address,
avsDirectoryAddr gethcommon.Address,
ethClient eth.EthClient,
ethClient eth.Client,
logger logging.Logger,
eigenMetrics metrics.Metrics,
txMgr txmgr.TxManager,
Expand Down
43 changes: 3 additions & 40 deletions chainio/clients/eth/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@ package eth
import (
"context"
"math/big"
"time"

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

type gethClient interface {
type Client interface {
ChainID(ctx context.Context) (*big.Int, error)
BalanceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error)
BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error)
Expand Down Expand Up @@ -53,42 +52,6 @@ type gethClient interface {
TransactionSender(ctx context.Context, tx *types.Transaction, block common.Hash, index uint) (common.Address, error)
}

// EthClient is modified interface with additional custom methods
type EthClient interface {
gethClient

WaitForTransactionReceipt(
ctx context.Context,
txHash common.Hash,
) *types.Receipt
}

// Client is a wrapper around geth's ethclient.Client struct, that adds a WaitForTransactionReceipt convenience method.
type Client struct {
*ethclient.Client
}

var _ EthClient = (*Client)(nil)

func NewClient(rpcAddress string) (*Client, error) {
client, err := ethclient.Dial(rpcAddress)
if err != nil {
return nil, err
}
return &Client{client}, nil
}

func (e *Client) WaitForTransactionReceipt(
ctx context.Context,
txHash common.Hash,
) *types.Receipt {
for {
// verifying transaction receipt
receipt, err := e.Client.TransactionReceipt(ctx, txHash)
if err != nil {
time.Sleep(2 * time.Second)
} else {
return receipt
}
}
func NewClient(rpcAddress string) (Client, error) {
return ethclient.Dial(rpcAddress)
}
2 changes: 1 addition & 1 deletion chainio/clients/eth/instrumented_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ type InstrumentedClient struct {
clientAndVersion string
}

var _ EthClient = (*InstrumentedClient)(nil)
var _ Client = (*InstrumentedClient)(nil)

func NewInstrumentedClient(rpcAddress string, rpcCallsCollector *rpccalls.Collector) (*InstrumentedClient, error) {
client, err := ethclient.Dial(rpcAddress)
Expand Down
63 changes: 40 additions & 23 deletions chainio/clients/fireblocks/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,25 @@ import (
"time"

"github.com/Layr-Labs/eigensdk-go/logging"
"github.com/ethereum/go-ethereum/common"
"github.com/golang-jwt/jwt"
"github.com/google/uuid"
)

type AssetID string

const (
AssetIDETH AssetID = "ETH"
AssetIDGoerliETH AssetID = "ETH_TEST3"
)

var AssetIDByChain = map[uint64]AssetID{
1: AssetIDETH, // mainnet
5: AssetIDGoerliETH, // goerli
}

type FireblocksTxID string

type FireblocksClient interface {
type Client interface {
// ContractCall makes a ContractCall request to the Fireblocks API.
// It signs and broadcasts a transaction and returns the transaction ID and status.
// ref: https://developers.fireblocks.com/reference/post_transactions
Expand All @@ -31,9 +42,15 @@ type FireblocksClient interface {
// This call is used to get the contract ID for a whitelisted contract, which is needed as destination account ID by NewContractCallRequest in a ContractCall
// ref: https://developers.fireblocks.com/reference/get_contracts
ListContracts(ctx context.Context) ([]WhitelistedContract, error)
// ListVaultAccounts makes a ListVaultAccounts request to the Fireblocks API
// It returns a list of vault accounts for the account.
ListVaultAccounts(ctx context.Context) ([]VaultAccount, error)
// GetTransaction makes a GetTransaction request to the Fireblocks API
// It returns the transaction details for the given transaction ID.
GetTransaction(ctx context.Context, txID string) (*Transaction, error)
}

type fireblocksClient struct {
type client struct {
apiKey string
privateKey *rsa.PrivateKey
baseURL string
Expand All @@ -42,32 +59,19 @@ type fireblocksClient struct {
logger logging.Logger
}

type Asset struct {
ID string `json:"id"`
Status string `json:"status"`
Address common.Address `json:"address"`
Tag string `json:"tag"`
}

type ErrorResponse struct {
Message string `json:"message"`
Code int `json:"code"`
}

type WhitelistedContract struct {
ID string `json:"id"`
Name string `json:"name"`
Assets []Asset `json:"assets"`
}

func NewFireblocksClient(apiKey string, secretKey []byte, baseURL string, timeout time.Duration, logger logging.Logger) (FireblocksClient, error) {
func NewClient(apiKey string, secretKey []byte, baseURL string, timeout time.Duration, logger logging.Logger) (Client, error) {
c := http.Client{Timeout: timeout}
privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(secretKey)
if err != nil {
return nil, fmt.Errorf("error parsing RSA private key: %w", err)
}

return &fireblocksClient{
return &client{
apiKey: apiKey,
privateKey: privateKey,
baseURL: baseURL,
Expand All @@ -79,7 +83,7 @@ func NewFireblocksClient(apiKey string, secretKey []byte, baseURL string, timeou

// signJwt signs a JWT token for the Fireblocks API
// mostly copied from the Fireblocks example: https://github.com/fireblocks/developers-hub/blob/main/authentication_examples/go/test.go
func (f *fireblocksClient) signJwt(path string, bodyJson interface{}, durationSeconds int64) (string, error) {
func (f *client) signJwt(path string, bodyJson interface{}, durationSeconds int64) (string, error) {
nonce := uuid.New().String()
now := time.Now().Unix()
expiration := now + durationSeconds
Expand Down Expand Up @@ -113,12 +117,25 @@ func (f *fireblocksClient) signJwt(path string, bodyJson interface{}, durationSe

// makeRequest makes a request to the Fireblocks API
// mostly copied from the Fireblocks example: https://github.com/fireblocks/developers-hub/blob/main/authentication_examples/go/test.go
func (f *fireblocksClient) makeRequest(ctx context.Context, method, path string, body interface{}) ([]byte, error) {
url, err := url.JoinPath(f.baseURL, path)
func (f *client) makeRequest(ctx context.Context, method, path string, body interface{}) ([]byte, error) {
// remove query parameters from path and join with baseURL
pathURI, err := url.Parse(path)
if err != nil {
return nil, fmt.Errorf("error parsing URL: %w", err)
}
query := pathURI.Query()
pathURI.RawQuery = ""
urlStr, err := url.JoinPath(f.baseURL, pathURI.String())
if err != nil {
return nil, fmt.Errorf("error joining URL path with %s and %s: %w", f.baseURL, path, err)
}
f.logger.Debug("making request to Fireblocks", "method", method, "url", url)
url, err := url.Parse(urlStr)
if err != nil {
return nil, fmt.Errorf("error parsing URL: %w", err)
}
// add query parameters back to path
url.RawQuery = query.Encode()
f.logger.Debug("making request to Fireblocks", "method", method, "url", url.String())
var reqBodyBytes []byte
if body != nil {
var err error
Expand All @@ -133,7 +150,7 @@ func (f *fireblocksClient) makeRequest(ctx context.Context, method, path string,
return nil, fmt.Errorf("error signing JWT: %w", err)
}

req, err := http.NewRequest(method, url, bytes.NewBuffer(reqBodyBytes))
req, err := http.NewRequest(method, url.String(), bytes.NewBuffer(reqBodyBytes))
if err != nil {
return nil, fmt.Errorf("error creating HTTP request: %w", err)
}
Expand Down
Loading

0 comments on commit 65cc80e

Please sign in to comment.