diff --git a/chainio/clients/fireblocks/cancel_transaction.go b/chainio/clients/fireblocks/cancel_transaction.go new file mode 100644 index 00000000..b59c23ee --- /dev/null +++ b/chainio/clients/fireblocks/cancel_transaction.go @@ -0,0 +1,28 @@ +package fireblocks + +import ( + "context" + "encoding/json" + "fmt" + "strings" +) + +type CancelTransactionResponse struct { + Success bool `json:"success"` +} + +func (f *client) CancelTransaction(ctx context.Context, txID string) (bool, error) { + f.logger.Debug("Fireblocks cancel transaction", "txID", txID) + path := fmt.Sprintf("/v1/transactions/%s/cancel", txID) + res, err := f.makeRequest(ctx, "POST", path, nil) + if err != nil { + return false, fmt.Errorf("error making request: %w", err) + } + var response CancelTransactionResponse + err = json.NewDecoder(strings.NewReader(string(res))).Decode(&response) + if err != nil { + return false, fmt.Errorf("error parsing response body: %w", err) + } + + return response.Success, nil +} diff --git a/chainio/clients/fireblocks/client.go b/chainio/clients/fireblocks/client.go index 67f424c4..fc96e840 100644 --- a/chainio/clients/fireblocks/client.go +++ b/chainio/clients/fireblocks/client.go @@ -32,13 +32,16 @@ var AssetIDByChain = map[uint64]AssetID{ 17000: AssetIDHolETH, // holesky } -type FireblocksTxID string - 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 ContractCall(ctx context.Context, body *ContractCallRequest) (*ContractCallResponse, error) + // CancelTransaction makes a CancelTransaction request to the Fireblocks API + // It cancels a transaction by its transaction ID. + // It returns true if the transaction was successfully canceled. + // ref: https://developers.fireblocks.com/reference/post_transactions-txid-cancel + CancelTransaction(ctx context.Context, txID string) (bool, error) // ListContracts makes a ListContracts request to the Fireblocks API // It returns a list of whitelisted contracts and their assets for the account. // 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 diff --git a/chainio/clients/fireblocks/client_test.go b/chainio/clients/fireblocks/client_test.go index 8805a84f..742ce987 100644 --- a/chainio/clients/fireblocks/client_test.go +++ b/chainio/clients/fireblocks/client_test.go @@ -66,6 +66,16 @@ func TestContractCall(t *testing.T) { t.Logf("txID: %s, status: %s", resp.ID, resp.Status) } +func TestCancelTransaction(t *testing.T) { + t.Skip("skipping test as it's meant for manual runs only") + + c := newFireblocksClient(t) + txID := "FILL_ME_IN" + success, err := c.CancelTransaction(context.Background(), txID) + assert.NoError(t, err) + t.Logf("txID: %s, success: %t", txID, success) +} + func TestListVaultAccounts(t *testing.T) { t.Skip("skipping test as it's meant for manual runs only") diff --git a/chainio/clients/fireblocks/get_asset_addresses.go b/chainio/clients/fireblocks/get_asset_addresses.go index cc530033..26c961cc 100644 --- a/chainio/clients/fireblocks/get_asset_addresses.go +++ b/chainio/clients/fireblocks/get_asset_addresses.go @@ -20,8 +20,8 @@ type AssetAddress struct { func (f *client) GetAssetAddresses(ctx context.Context, vaultID string, assetID AssetID) ([]AssetAddress, error) { f.logger.Debug("Fireblocks get asset addressees", "vaultID", vaultID, "assetID", assetID) - url := fmt.Sprintf("/v1/vault/accounts/%s/%s/addresses", vaultID, assetID) - res, err := f.makeRequest(ctx, "GET", url, nil) + path := fmt.Sprintf("/v1/vault/accounts/%s/%s/addresses", vaultID, assetID) + res, err := f.makeRequest(ctx, "GET", path, nil) if err != nil { return nil, fmt.Errorf("error making request: %w", err) } diff --git a/chainio/clients/fireblocks/get_transaction.go b/chainio/clients/fireblocks/get_transaction.go index e3c61491..cd2221a6 100644 --- a/chainio/clients/fireblocks/get_transaction.go +++ b/chainio/clients/fireblocks/get_transaction.go @@ -49,8 +49,8 @@ type Transaction struct { 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) + path := fmt.Sprintf("/v1/transactions/%s", txID) + res, err := f.makeRequest(ctx, "GET", path, nil) if err != nil { return nil, fmt.Errorf("error making request: %w", err) } diff --git a/chainio/clients/mocks/fireblocks.go b/chainio/clients/mocks/fireblocks.go index ea3f13fd..39da09f4 100644 --- a/chainio/clients/mocks/fireblocks.go +++ b/chainio/clients/mocks/fireblocks.go @@ -40,6 +40,21 @@ func (m *MockFireblocksClient) EXPECT() *MockFireblocksClientMockRecorder { return m.recorder } +// CancelTransaction mocks base method. +func (m *MockFireblocksClient) CancelTransaction(arg0 context.Context, arg1 string) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CancelTransaction", arg0, arg1) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CancelTransaction indicates an expected call of CancelTransaction. +func (mr *MockFireblocksClientMockRecorder) CancelTransaction(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CancelTransaction", reflect.TypeOf((*MockFireblocksClient)(nil).CancelTransaction), arg0, arg1) +} + // ContractCall mocks base method. func (m *MockFireblocksClient) ContractCall(arg0 context.Context, arg1 *fireblocks.ContractCallRequest) (*fireblocks.ContractCallResponse, error) { m.ctrl.T.Helper() diff --git a/chainio/clients/wallet/fireblocks_wallet.go b/chainio/clients/wallet/fireblocks_wallet.go index 35b41e39..4fbee2d3 100644 --- a/chainio/clients/wallet/fireblocks_wallet.go +++ b/chainio/clients/wallet/fireblocks_wallet.go @@ -206,6 +206,10 @@ func (t *fireblocksWallet) SendTransaction(ctx context.Context, tx *types.Transa return res.ID, nil } +func (t *fireblocksWallet) CancelTransactionBroadcast(ctx context.Context, txID TxID) (bool, error) { + return t.fireblocksClient.CancelTransaction(ctx, string(txID)) +} + func (t *fireblocksWallet) GetTransactionReceipt(ctx context.Context, txID TxID) (*types.Receipt, error) { fireblockTx, err := t.fireblocksClient.GetTransaction(ctx, txID) if err != nil {