Skip to content

Commit

Permalink
more methods in fireblocks client
Browse files Browse the repository at this point in the history
  • Loading branch information
ian-shim committed Feb 27, 2024
1 parent 3b23266 commit 021a26a
Show file tree
Hide file tree
Showing 17 changed files with 298 additions and 228 deletions.
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
44 changes: 32 additions & 12 deletions chainio/clients/fireblocks/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,21 @@ const (
apiKey = "FILL_ME_IN"
)

func TestListContracts(t *testing.T) {
t.Skip("skipping test as it's meant for manual runs only")

func newFireblocksClient(t *testing.T) fireblocks.Client {
secretKey, err := os.ReadFile(secretKeyPath)
assert.NoError(t, err)
logger, err := logging.NewZapLogger(logging.Development)
assert.NoError(t, err)
c, err := fireblocks.NewFireblocksClient(apiKey, secretKey, sandboxAPI, 5*time.Second, logger)
c, err := fireblocks.NewClient(apiKey, secretKey, sandboxAPI, 5*time.Second, logger)
assert.NoError(t, err)
return c

}

func TestListContracts(t *testing.T) {
t.Skip("skipping test as it's meant for manual runs only")

c := newFireblocksClient(t)
contracts, err := c.ListContracts(context.Background())
assert.NoError(t, err)
for _, contract := range contracts {
Expand All @@ -36,14 +42,7 @@ func TestListContracts(t *testing.T) {
func TestContractCall(t *testing.T) {
t.Skip("skipping test as it's meant for manual runs only")

secretKey, err := os.ReadFile(secretKeyPath)
assert.NoError(t, err)

logger, err := logging.NewZapLogger(logging.Development)
assert.NoError(t, err)

c, err := fireblocks.NewFireblocksClient(apiKey, secretKey, sandboxAPI, 5*time.Second, logger)
assert.NoError(t, err)
c := newFireblocksClient(t)
destinationAccountID := "FILL_ME_IN"
idempotenceKey := "FILL_ME_IN"
// Tests the contract call against this contract:
Expand All @@ -60,3 +59,24 @@ func TestContractCall(t *testing.T) {
assert.NoError(t, err)
t.Logf("txID: %s, status: %s", resp.ID, resp.Status)
}

func TestListVaultAccounts(t *testing.T) {
t.Skip("skipping test as it's meant for manual runs only")

c := newFireblocksClient(t)
accounts, err := c.ListVaultAccounts(context.Background())
assert.NoError(t, err)
for _, account := range accounts {
t.Logf("Account: %+v", account)
}
}

func TestGetTransaction(t *testing.T) {
t.Skip("skipping test as it's meant for manual runs only")

c := newFireblocksClient(t)
txID := "FILL_ME_IN"
tx, err := c.GetTransaction(context.Background(), txID)
assert.NoError(t, err)
t.Logf("Transaction: %+v", tx)
}
33 changes: 22 additions & 11 deletions chainio/clients/fireblocks/contract_call.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ import (
"strings"
)

type TransactionOperation string

const (
ContractCall TransactionOperation = "CONTRACT_CALL"
Transfer TransactionOperation = "TRANSFER"
Mint TransactionOperation = "MINT"
Burn TransactionOperation = "BURN"
TypedMessage TransactionOperation = "TYPED_MESSAGE"
Raw TransactionOperation = "RAW"
)

type account struct {
Type string `json:"type"`
ID string `json:"id"`
Expand All @@ -17,30 +28,30 @@ type extraParams struct {
}

type ContractCallRequest struct {
Operation string `json:"operation"`
ExternalTxID string `json:"externalTxId"`
AssetID string `json:"assetId"`
Source account `json:"source"`
Destination account `json:"destination"`
Amount string `json:"amount"`
ExtraParameters extraParams `json:"extraParameters"`
Operation TransactionOperation `json:"operation"`
ExternalTxID string `json:"externalTxId"`
AssetID AssetID `json:"assetId"`
Source account `json:"source"`
Destination account `json:"destination"`
Amount string `json:"amount"`
ExtraParameters extraParams `json:"extraParameters"`
}

type ContractCallResponse struct {
ID string `json:"id"`
Status string `json:"status"`
Status Status `json:"status"`
}

func NewContractCallRequest(
externalTxID string,
assetID string,
assetID AssetID,
sourceAccountID string,
destinationAccountID string,
amount string,
calldata string,
) *ContractCallRequest {
return &ContractCallRequest{
Operation: "CONTRACT_CALL",
Operation: ContractCall,
ExternalTxID: externalTxID,
AssetID: assetID,
Source: account{
Expand All @@ -59,7 +70,7 @@ func NewContractCallRequest(
}
}

func (f *fireblocksClient) ContractCall(ctx context.Context, req *ContractCallRequest) (*ContractCallResponse, error) {
func (f *client) ContractCall(ctx context.Context, req *ContractCallRequest) (*ContractCallResponse, error) {
f.logger.Debug("Fireblocks call contract", "req", req)
res, err := f.makeRequest(ctx, "POST", "/v1/transactions", req)
if err != nil {
Expand Down
64 changes: 64 additions & 0 deletions chainio/clients/fireblocks/get_transaction.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package fireblocks

import (
"context"
"encoding/json"
"fmt"
"strings"
)

// Transaction is a type for the transaction response from Fireblocks
type Transaction struct {
ID string `json:"id"`
ExternalID string `json:"externalId"`
Status Status `json:"status"`
SubStatus string `json:"subStatus"`
TxHash string `json:"txHash"`
Operation TransactionOperation `json:"operation"`
CreatedAt int64 `json:"createdAt"`
LastUpdated int64 `json:"lastUpdated"`
AssetID AssetID `json:"assetId"`
Source account `json:"source"`
SourceAddress string `json:"sourceAddress"`
Destination account `json:"destination"`
DestinationAddress string `json:"destinationAddress"`
DestinationAddressDescription string `json:"destinationAddressDescription"`
DestinationTag string `json:"destinationTag"`
AmountInfo struct {
Amount string `json:"amount"`
RequestedAmount string `json:"requestedAmount"`
NetAmount string `json:"netAmount"`
AmountUSD string `json:"amountUSD"`
} `json:"amountInfo"`
FeeInfo struct {
NetworkFee string `json:"networkFee"`
ServiceFee string `json:"serviceFee"`
GasPrice string `json:"gasPrice"`
} `json:"feeInfo"`
FeeCurrency string `json:"feeCurrency"`
ExtraParameters struct {
ContractCallData string `json:"contractCallData"`
} `json:"extraParameters"`
NumOfConfirmations int `json:"numOfConfirmations"`
// The block hash and height of the block that this transaction was mined in.
BlockInfo struct {
BlockHeight string `json:"blockHeight"`
BlockHash string `json:"blockHash"`
} `json:"blockInfo"`
}

func (f *client) GetTransaction(ctx context.Context, txID string) (*Transaction, error) {
f.logger.Debug("Fireblocks get transaction", "txID", txID)
url := fmt.Sprintf("/v1/transactions/%s", txID)
res, err := f.makeRequest(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("error making request: %w", err)
}
var tx Transaction
err = json.NewDecoder(strings.NewReader(string(res))).Decode(&tx)
if err != nil {
return nil, fmt.Errorf("error parsing response body: %w", err)
}

return &tx, nil
}
15 changes: 14 additions & 1 deletion chainio/clients/fireblocks/list_contracts.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,22 @@ import (
"encoding/json"
"fmt"
"strings"

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

func (f *fireblocksClient) ListContracts(ctx context.Context) ([]WhitelistedContract, error) {
type WhitelistedContract struct {
ID string `json:"id"`
Name string `json:"name"`
Assets []struct {
ID AssetID `json:"id"`
Status string `json:"status"`
Address common.Address `json:"address"`
Tag string `json:"tag"`
} `json:"assets"`
}

func (f *client) ListContracts(ctx context.Context) ([]WhitelistedContract, error) {
var contracts []WhitelistedContract
res, err := f.makeRequest(ctx, "GET", "/v1/contracts", nil)
if err != nil {
Expand Down
Loading

0 comments on commit 021a26a

Please sign in to comment.