diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 56478007..da0521ed 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: - node-version: [12.x, 14.x, 16.x] + node-version: [12.x, 14.x, 16.x, 18.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: diff --git a/README.md b/README.md index d66c7339..f76c1869 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,15 @@ This repository contains the official Javascript & Typescript SDK for Fireblocks API. For the complete API reference, go to [API reference](https://docs.fireblocks.com/api/swagger-ui/). +## V4 Migration +Please read the [following](./docs/V4-MIGRATION.md) guide for migration + ## Usage #### Before You Begin Make sure you have the credentials for Fireblocks API Services. Otherwise, please contact Fireblocks support for further instructions on how to obtain your API credentials. #### Requirements -- [node.js](https://nodejs.org) v6.3.1 or newer +- [node.js](https://nodejs.org) v12 or newer #### Installation `npm install fireblocks-sdk --save` @@ -54,3 +57,39 @@ interface SDKOptions { userAgent?: string; } ``` + +#### Axios Interceptor +You can provide the sdk options with an [axios response interceptor](https://axios-http.com/docs/interceptors): +```ts +new FireblocksSDK(privateKey, userId, serverAddress, undefined, { + customAxiosOptions: { + interceptors: { + response: { + onFulfilled: (response) => { + console.log(`Request ID: ${response.headers["x-request-id"]}`); + return response; + }, + onRejected: (error) => { + console.log(`Request ID: ${error.response.headers["x-request-id"]}`); + throw error; + } + } + } + } +}); +``` + +#### Error Handling +The SDK throws `AxiosError` upon http errors for API requests. + +You can read more about axios error handling [here](https://axios-http.com/docs/handling_errors). + +You can get more data on the Fireblocks error using the following fields: + +- `error.response.data.code`: The Fireblocks error code, should be provided on support tickets +- `error.response.data.message`: Explanation of the Fireblocks error +- `error.response.headers['x-request-id']`: The request ID correlated to the API request, should be provided on support tickets / Github issues + + + + diff --git a/docs/V4-MIGRATION.md b/docs/V4-MIGRATION.md new file mode 100644 index 00000000..90b0c661 --- /dev/null +++ b/docs/V4-MIGRATION.md @@ -0,0 +1,40 @@ +## V4 Migration +### `X-REQUEST-ID` Response Header +Using v4 you can now use the response header `x-request-id` which correlates the request to the Fireblocks operation. + +You can provide the value of this header in case you have support tickets related to an API operation, or a Github issue. + +In case of API request failures the SDK throws an AxiosError that contains the following fields: +```ts + error.response.data; // the error body + error.response.status; // the error status code + error.response.headers; // the error headers +``` + +- You can get the request-id by using the `error.response.headers['x-request-id']` field +- Another way of getting the request-id for successful operations as well, will be to provide an axios response interceptor + +For example, you can provide the sdk options with an axios response interceptor: +```ts +new FireblocksSDK(privateKey, userId, serverAddress, undefined, { + customAxiosOptions: { + interceptors: { + response: { + onFulfilled: (response) => { + console.log(`Request ID: ${response.headers["x-request-id"]}`); + return response; + }, + onRejected: (error) => { + console.log(`Request ID: ${error.response.headers["x-request-id"]}`); + throw error; + } + } + } + } + }); +``` + +### Removed deprecated methods +- `getVaultAccounts` method was removed. It is replaced by the `getVaultAccountsWithPageInfo` method +- `getVaultAccount` method was removed. It is replaced by the `getVaultAccountById` method +- `getExchangeAccount` was removed in favour of `getExchangeAccountById` diff --git a/src/api-client.ts b/src/api-client.ts index 6dfc78cd..c32608a5 100644 --- a/src/api-client.ts +++ b/src/api-client.ts @@ -1,7 +1,7 @@ import os from "os"; import platform from "platform"; import { IAuthProvider } from "./iauth-provider"; -import { RequestOptions } from "./types"; +import { RequestOptions, TransactionPageResponse } from "./types"; import { SDKOptions } from "./fireblocks-sdk"; import axios, { AxiosInstance } from "axios"; import { version as SDK_VERSION } from "../package.json"; @@ -19,6 +19,10 @@ export class ApiClient { "User-Agent": this.getUserAgent() } }); + + if (options.customAxiosOptions?.interceptors?.response) { + this.axiosInstance.interceptors.response.use(options.customAxiosOptions.interceptors.response.onFulfilled, options.customAxiosOptions.interceptors.response.onRejected); + } } private getUserAgent(): string { @@ -32,51 +36,60 @@ export class ApiClient { return userAgent; } - public async issueGetRequest(path: string, pageMode: boolean = false) { + public async issueGetRequestForTransactionPages(path: string): Promise { + const token = this.authProvider.signJwt(path); + const res = await this.axiosInstance.get(path, { + headers: {"Authorization": `Bearer ${token}`} + }); + return { + transactions: res.data, + pageDetails: { + prevPage: res.headers["prev-page"] ? res.headers["prev-page"].toString() : "", + nextPage: res.headers["next-page"] ? res.headers["next-page"].toString() : "", + } + }; + } + + public async issueGetRequest(path: string): Promise { const token = this.authProvider.signJwt(path); const res = await this.axiosInstance.get(path, { headers: {"Authorization": `Bearer ${token}`} }); - if (pageMode) { - return { - transactions: res.data, - pageDetails: { - prevPage: res.headers["prev-page"] ? res.headers["prev-page"].toString() : "", - nextPage: res.headers["next-page"] ? res.headers["next-page"].toString() : "", - } - }; - } return res.data; } - public async issuePostRequest(path: string, body: any, requestOptions?: RequestOptions) { + public async issuePostRequest(path: string, body: any, requestOptions?: RequestOptions): Promise { const token = this.authProvider.signJwt(path, body); const headers: any = {"Authorization": `Bearer ${token}`}; const idempotencyKey = requestOptions?.idempotencyKey; if (idempotencyKey) { headers["Idempotency-Key"] = idempotencyKey; } - return (await this.axiosInstance.post(path, body, {headers})).data; + const response = await this.axiosInstance.post(path, body, {headers}); + return response.data; } - public async issuePutRequest(path: string, body: any) { + public async issuePutRequest(path: string, body: any): Promise { const token = this.authProvider.signJwt(path, body); - return (await this.axiosInstance.put(path, body, { + const res = (await this.axiosInstance.put(path, body, { headers: {"Authorization": `Bearer ${token}`} - })).data; + })); + return res.data; } - public async issuePatchRequest(path: string, body: any) { + public async issuePatchRequest(path: string, body: any): Promise { const token = this.authProvider.signJwt(path, body); - return (await this.axiosInstance.patch(path, body, { + const res = (await this.axiosInstance.patch(path, body, { headers: {"Authorization": `Bearer ${token}`} - })).data; + })); + return res.data; } - public async issueDeleteRequest(path: string) { + public async issueDeleteRequest(path: string): Promise { const token = this.authProvider.signJwt(path); - return (await this.axiosInstance.delete(path, { + const res = (await this.axiosInstance.delete(path, { headers: {"Authorization": `Bearer ${token}`} - })).data; + })); + return res.data; } } diff --git a/src/fireblocks-sdk.ts b/src/fireblocks-sdk.ts index 3b65dbb4..ee65965f 100644 --- a/src/fireblocks-sdk.ts +++ b/src/fireblocks-sdk.ts @@ -10,14 +10,11 @@ import { CancelTransactionResponse, ConvertExchangeAssetResponse, CreateTransactionResponse, - CreateTransferTicketArgs, - CreateTransferTicketResponse, DeallocateFundsRequest, DepositAddressResponse, EstimateFeeResponse, EstimateTransactionFeeResponse, ExchangeResponse, - ExecuteTermArgs, ExternalWalletAsset, FiatAccountResponse, GasStationInfo, @@ -33,17 +30,14 @@ import { PublicKeyInfoForVaultAccountArgs, RequestOptions, ResendWebhooksResponse, - TermResponse, TransactionArguments, TransactionFilter, TransactionPageFilter, TransactionPageResponse, TransactionResponse, - TransferTicketResponse, User, ValidateAddressResponse, VaultAccountResponse, - VaultAccountsFilter, VaultAssetResponse, VaultBalancesFilter, WalletContainerResponse, @@ -60,16 +54,16 @@ import { NFTOwnershipFilter, Token, TokenWithBalance, - APIPagedResponse, + Web3PagedResponse, CreateWalletConnectPayload, Web3ConnectionType, GetWeb3ConnectionsPayload, PublicKeyResponse, AllocateFundsResponse, - SettleOffExchangeAccountResponse, GetNFTsFilter, + SettleOffExchangeAccountResponse, PublicKeyInformation, DropTransactionResponse, } from "./types"; -import { AxiosProxyConfig } from "axios"; +import { AxiosInterceptorOptions, AxiosProxyConfig, AxiosResponse } from "axios"; export * from "./types"; @@ -85,11 +79,23 @@ export interface SDKOptions { /** Additional product identifier to be prepended to the User-Agent header */ userAgent?: string; + + /** + * Providing custom axios options including a response interceptor (https://axios-http.com/docs/interceptors) + */ + customAxiosOptions?: { + interceptors?: { + response?: { + onFulfilled: (value: AxiosResponse) => AxiosResponse | Promise>; + onRejected: (error: any) => any; + }; + } + }; } export class FireblocksSDK { - private authProvider: IAuthProvider; - private apiBaseUrl: string; + private readonly authProvider: IAuthProvider; + private readonly apiBaseUrl: string; private apiClient: ApiClient; /** @@ -116,16 +122,6 @@ export class FireblocksSDK { public async getSupportedAssets(): Promise { return await this.apiClient.issueGetRequest("/v1/supported_assets"); } - - /** - * @deprecated this method is deprecated and will be removed in the future. Please use getVaultAccountsWithPageInfo instead. - * Gets all vault accounts for your tenant - */ - public async getVaultAccounts(filter?: VaultAccountsFilter): Promise { - const url = `/v1/vault/accounts?${queryString.stringify(filter)}`; - return await this.apiClient.issueGetRequest(url); - } - /** * Gets a list of vault accounts per page matching the given filter or path * @param pagedVaultAccountsRequestFilters Filters for the first request @@ -133,16 +129,6 @@ export class FireblocksSDK { public async getVaultAccountsWithPageInfo(pagedVaultAccountsRequestFilters: PagedVaultAccountsRequestFilters): Promise { return await this.apiClient.issueGetRequest(`/v1/vault/accounts_paged?${queryString.stringify(pagedVaultAccountsRequestFilters)}`); } - - /** - * @deprecated Replaced by getVaultAccountById. - * Gets a single vault account - * @param vaultAccountId The vault account ID - */ - public async getVaultAccount(vaultAccountId: string): Promise { - return await this.getVaultAccountById(vaultAccountId); - } - /** * Gets a single vault account * @param vaultAccountId The vault account ID @@ -265,7 +251,7 @@ export class FireblocksSDK { * @param connectionId The network connection's id * @param routingPolicy The desired routing policy */ - public async setNetworkConnectionRoutingPolicy(connectionId: string, routingPolicy: NetworkConnectionRoutingPolicy) { + public async setNetworkConnectionRoutingPolicy(connectionId: string, routingPolicy: NetworkConnectionRoutingPolicy): Promise { const body = { routingPolicy }; return await this.apiClient.issuePatchRequest(`/v1/network_connections/${connectionId}/set_routing_policy`, body); } @@ -315,7 +301,7 @@ export class FireblocksSDK { * @param routingPolicy The desired routing policy * @returns OperationSuccessResponse */ - public async setNetworkIdRoutingPolicy(networkId: string, routingPolicy: NetworkIdRoutingPolicy) { + public async setNetworkIdRoutingPolicy(networkId: string, routingPolicy: NetworkIdRoutingPolicy): Promise { const body = { routingPolicy }; return await this.apiClient.issuePatchRequest(`/v1/network_ids/${networkId}/set_routing_policy`, body); } @@ -327,14 +313,6 @@ export class FireblocksSDK { return await this.apiClient.issueGetRequest("/v1/exchange_accounts"); } - /** - * @deprecated Replaced by getExchangeAccountById - * Gets a single exchange account by ID - * @param exchangeAccountId The exchange account ID - */ - public async getExchangeAccount(exchangeAccountId: string): Promise { - return await this.getExchangeAccount(exchangeAccountId); - } /** * Gets a single exchange account by ID @@ -462,14 +440,16 @@ export class FireblocksSDK { */ public async getTransactionsWithPageInfo(pageFilter?: TransactionPageFilter, nextOrPreviousPath?: string): Promise { if (pageFilter) { - return await this.apiClient.issueGetRequest(`/v1/transactions?${queryString.stringify(pageFilter)}`, true); + return await this.apiClient.issueGetRequestForTransactionPages(`/v1/transactions?${queryString.stringify(pageFilter)}`); } else if (nextOrPreviousPath) { const index = nextOrPreviousPath.indexOf("/v1/"); const path = nextOrPreviousPath.substring(index, nextOrPreviousPath.length); - return await this.apiClient.issueGetRequest(path, true); + return await this.apiClient.issueGetRequestForTransactionPages(path); } - return {transactions: [], pageDetails: { prevPage: "", nextPage: "" }}; + return { + transactions: [], pageDetails: { prevPage: "", nextPage: "" }, + }; } /** @@ -767,58 +747,6 @@ export class FireblocksSDK { return await this.apiClient.issueGetRequest(`/v1/estimate_network_fee?assetId=${asset}`); } - /** - * Creates a new transfer ticket - */ - public async createTransferTicket(options: CreateTransferTicketArgs, requestOptions?: RequestOptions): Promise { - return await this.apiClient.issuePostRequest("/v1/transfer_tickets", options, requestOptions); - } - - /** - * Gets all transfer tickets - */ - public async getTransferTickets(): Promise { - return await this.apiClient.issueGetRequest("/v1/transfer_tickets"); - } - - /** - * Get a transfer ticket by ticket ID - * @param ticketId - */ - public async getTransferTicketById(ticketId: string): Promise { - return await this.apiClient.issueGetRequest(`/v1/transfer_tickets/${ticketId}`); - } - - /** - * Get a term of transfer ticket - * @param ticketId - * @param termId - */ - public async getTransferTicketTerm(ticketId: string, termId: string): Promise { - return await this.apiClient.issueGetRequest(`/v1/transfer_tickets/${ticketId}/${termId}`); - } - - /** - * Cancel the transfer ticket - * @param ticketId - * @param requestOptions - */ - public async cancelTransferTicket(ticketId: string, requestOptions?: RequestOptions) { - return await this.apiClient.issuePostRequest(`/v1/transfer_tickets/${ticketId}/cancel`, {}, requestOptions); - } - - /** - * Executes a transaction for a single term of a transfer ticket - * @param ticketId - * @param termId - * @param options - * @param requestOptions - */ - public async executeTransferTicketTerm(ticketId: string, termId: string, options: ExecuteTermArgs, requestOptions?: RequestOptions) { - return await this.apiClient.issuePostRequest(`/v1/transfer_tickets/${ticketId}/${termId}/transfer`, - options, requestOptions); - } - /** * Deletes a single internal wallet * @param walletId The internal wallet ID @@ -942,7 +870,7 @@ export class FireblocksSDK { * Get the public key information * @param args */ - public async getPublicKeyInfo(args: PublicKeyInfoArgs) { + public async getPublicKeyInfo(args: PublicKeyInfoArgs): Promise { let url = `/v1/vault/public_key_info`; if (args.algorithm) { url += `?algorithm=${args.algorithm}`; @@ -1025,7 +953,7 @@ export class FireblocksSDK { /** * Drop an ETH based transaction */ - public async dropTransaction(txId: string, feeLevel?: string, requestedFee?: string, requestOptions?: RequestOptions) { + public async dropTransaction(txId: string, feeLevel?: string, requestedFee?: string, requestOptions?: RequestOptions): Promise { const url = `/v1/transactions/${txId}/drop`; const body = {feeLevel, requestedFee}; @@ -1165,7 +1093,7 @@ export class FireblocksSDK { return await this.apiClient.issueDeleteRequest(`/v1/fee_payer/${baseAsset}`); } - private getWeb3ConnectionPath(type: Web3ConnectionType) { + private getWeb3ConnectionPath(type: Web3ConnectionType): string { const basePath = `/v1/connections`; switch (type) { @@ -1195,7 +1123,7 @@ export class FireblocksSDK { sort, filter, order - }: GetWeb3ConnectionsPayload = {}): Promise> { + }: GetWeb3ConnectionsPayload = {}): Promise> { const params = new URLSearchParams({ ...(pageCursor && { next: pageCursor }), ...(pageSize && { pageSize: pageSize.toString() }), @@ -1211,6 +1139,7 @@ export class FireblocksSDK { * Initiate a new web3 connection * @param type The type of the connection * @param payload The payload for creating a new web3 connection + * @param requestOptions * @param payload.vaultAccountId The vault account to link with the dapp * @param payload.feeLevel The fee level for the connection * @param payload.uri The WalletConnect URI, as provided by the dapp @@ -1284,7 +1213,7 @@ export class FireblocksSDK { * @param filter.ids * @param filter.order */ - public async getNFTs(filter: GetNFTsFilter): Promise> { + public async getNFTs(filter: GetNFTsFilter): Promise> { const { pageCursor, pageSize, ids, order } = filter; const queryParams = { pageCursor, @@ -1306,7 +1235,7 @@ export class FireblocksSDK { * @param filter.sort Sort by value * @param filter.order Order value */ - public async getOwnedNFTs(filter?: NFTOwnershipFilter): Promise> { + public async getOwnedNFTs(filter?: NFTOwnershipFilter): Promise> { let url = "/v1/nfts/ownership/tokens"; if (filter) { const { blockchainDescriptor, vaultAccountIds, collectionIds, ids, pageCursor, pageSize, sort, order } = filter; diff --git a/src/types.ts b/src/types.ts index 9ff94c13..b7c1ba12 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,12 @@ +import { AxiosResponseHeaders } from "axios"; + +export interface Web3PagedResponse { + data: T[]; + paging?: Paging; +} + +export type APIResponseHeaders = AxiosResponseHeaders & {"x-request-id"?: string}; + export interface VaultAccountResponse { id: string; name: string; @@ -497,7 +506,6 @@ export interface GetNFTsFilter { order?: OrderValues; } - class MediaEntity { url: string; contentType: string; @@ -513,11 +521,6 @@ export interface Paging { next: string; } -export interface APIPagedResponse { - data: T[]; - paging?: Paging; -} - export interface Token { id: string; tokenId: string; @@ -726,6 +729,17 @@ export interface PublicKeyResponse { publicKey: string; } +export interface PublicKeyInformation { + algorithm: string; + derivationPath: number[]; + publicKey: String; +} + +export interface DropTransactionResponse { + success: boolean; + transactions?: string[]; +} + export interface MaxSpendableAmountResponse { maxSpendableAmount: string; }