diff --git a/package.json b/package.json index 6ee4598f..4a6c6ea0 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "text" ], "verbose": true, - "testRegex": ".test.ts$" + "testRegex": ".test.ts$", + "maxWorkers": 1 } } diff --git a/src/coinbase/coinbase.ts b/src/coinbase/coinbase.ts index 6f25e92b..e85eec2b 100644 --- a/src/coinbase/coinbase.ts +++ b/src/coinbase/coinbase.ts @@ -1,6 +1,7 @@ import globalAxios from "axios"; import fs from "fs"; -import { User as UserModel, UsersApiFactory } from "../client"; +import { User as UserModel, UsersApiFactory, TransfersApiFactory } from "../client"; +import { ethers } from "ethers"; import { BASE_PATH } from "./../client/base"; import { Configuration } from "./../client/configuration"; import { CoinbaseAuthenticator } from "./authenticator"; @@ -22,8 +23,28 @@ export class Coinbase { BaseSepolia: "base-sepolia", }; + /** + * The list of supported assets. + * + * @constant + */ + static assetList = { + Eth: "eth", + Wei: "wei", + Gwei: "gwei", + Usdc: "usdc", + Weth: "weth", + }; + apiClients: ApiClients = {}; + /** + * Represents the number of Wei per Ether. + * + * @constant + */ + static readonly WEI_PER_ETHER: bigint = BigInt("1000000000000000000"); + /** * Initializes the Coinbase SDK. * @@ -59,6 +80,8 @@ export class Coinbase { ); this.apiClients.user = UsersApiFactory(config, BASE_PATH, axiosInstance); + this.apiClients.transfer = TransfersApiFactory(config, BASE_PATH, axiosInstance); + this.apiClients.baseSepoliaProvider = new ethers.JsonRpcProvider("https://sepolia.base.org"); } /** @@ -67,7 +90,7 @@ export class Coinbase { * @param filePath - The path to the JSON file containing the API key and private key. * @param debugging - If true, logs API requests and responses to the console. * @param basePath - The base path for the API. - * @returns {Coinbase} A new instance of the Coinbase SDK. + * @returns A new instance of the Coinbase SDK. * @throws {InvalidAPIKeyFormat} If the file does not exist or the configuration values are missing/invalid. * @throws {InvalidConfiguration} If the configuration is invalid. * @throws {InvalidAPIKeyFormat} If not able to create JWT token. @@ -102,7 +125,7 @@ export class Coinbase { /** * Returns User object for the default user. * - * @returns {User} The default user. + * @returns The default user. * @throws {APIError} If the request fails. */ async getDefaultUser(): Promise { diff --git a/src/coinbase/errors.ts b/src/coinbase/errors.ts index af0431e6..46ea85b9 100644 --- a/src/coinbase/errors.ts +++ b/src/coinbase/errors.ts @@ -77,3 +77,23 @@ export class InvalidConfiguration extends Error { } } } + +/** + * InvalidUnsignedPayload error is thrown when the unsigned payload is invalid. + */ +export class InvalidUnsignedPayload extends Error { + static DEFAULT_MESSAGE = "Invalid unsigned payload"; + + /** + * Initializes a new InvalidUnsignedPayload instance. + * + * @param message - The error message. + */ + constructor(message: string = InvalidUnsignedPayload.DEFAULT_MESSAGE) { + super(message); + this.name = "InvalidUnsignedPayload"; + if (Error.captureStackTrace) { + Error.captureStackTrace(this, InvalidUnsignedPayload); + } + } +} diff --git a/src/coinbase/tests/transfer_test.ts b/src/coinbase/tests/transfer_test.ts new file mode 100644 index 00000000..3d57161b --- /dev/null +++ b/src/coinbase/tests/transfer_test.ts @@ -0,0 +1,277 @@ +import { ethers } from "ethers"; +import { Transfer as TransferModel, TransferStatusEnum } from "../../client/api"; +import { TransferAPIClient, TransferStatus } from "../types"; +import { Transfer, TransferClients } from "../transfer"; +import { Coinbase } from "../coinbase"; + +const fromKey = ethers.Wallet.createRandom(); + +const networkId = "base_sepolia"; +const walletId = "12345"; +const fromAddressId = fromKey.address; +const amount = ethers.parseUnits("100", 18); +const ethAmount = amount / BigInt(Coinbase.WEI_PER_ETHER); +const toAddressId = "0x4D9E4F3f4D1A8B5F4f7b1F5b5C7b8d6b2B3b1b0b"; +const transferId = "67890"; + +const unsignedPayload = + "7b2274797065223a22307832222c22636861696e4964223a2230783134613334222c226e6f6e63" + + "65223a22307830222c22746f223a22307834643965346633663464316138623566346637623166" + + "356235633762386436623262336231623062222c22676173223a22307835323038222c22676173" + + "5072696365223a6e756c6c2c226d61785072696f72697479466565506572476173223a223078" + + "3539363832663030222c226d6178466565506572476173223a2230783539363832663030222c22" + + "76616c7565223a2230783536626337356532643633313030303030222c22696e707574223a22" + + "3078222c226163636573734c697374223a5b5d2c2276223a22307830222c2272223a2230783022" + + "2c2273223a22307830222c2279506172697479223a22307830222c2268617368223a2230783664" + + "633334306534643663323633653363396561396135656438646561346332383966613861363966" + + "3031653635393462333732386230386138323335333433227d"; + +const signedPayload = + "02f86b83014a3401830f4240830f4350825208946cd01c0f55ce9e0bf78f5e90f72b4345b" + + "16d515d0280c001a0566afb8ab09129b3f5b666c3a1e4a7e92ae12bbee8c75b4c6e0c46f6" + + "6dd10094a02115d1b52c49b39b6cb520077161c9bf636730b1b40e749250743f4524e9e4ba"; + +const transactionHash = "0x6c087c1676e8269dd81e0777244584d0cbfd39b6997b3477242a008fa9349e11"; + +const mockProvider = new ethers.JsonRpcProvider( + "https://sepolia.base.org", +) as jest.Mocked; +mockProvider.getTransaction = jest.fn(); +mockProvider.getTransactionReceipt = jest.fn(); + +describe("Transfer Class", () => { + let transferModel: TransferModel; + let mockApiClients: TransferClients; + let transfer: Transfer; + + beforeEach(() => { + transferModel = { + transfer_id: transferId, + network_id: networkId, + wallet_id: walletId, + address_id: fromAddressId, + destination: toAddressId, + asset_id: "eth", + amount: amount.toString(), + unsigned_payload: unsignedPayload, + status: TransferStatusEnum.Pending, + } as TransferModel; + + mockApiClients = { + transfer: {} as TransferAPIClient, + baseSepoliaProvider: mockProvider, + } as TransferClients; + + transfer = new Transfer(transferModel, mockApiClients); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe("constructor", () => { + it("should initialize a new Transfer", () => { + expect(transfer).toBeInstanceOf(Transfer); + }); + }); + + describe("getId", () => { + it("should return the transfer ID", () => { + expect(transfer.getId()).toEqual(transferId); + }); + }); + + describe("getNetworkId", () => { + it("should return the network ID", () => { + expect(transfer.getNetworkId()).toEqual(networkId); + }); + }); + + describe("getWalletId", () => { + it("should return the wallet ID", () => { + expect(transfer.getWalletId()).toEqual(walletId); + }); + }); + + describe("getFromAddressId", () => { + it("should return the source address ID", () => { + expect(transfer.getFromAddressId()).toEqual(fromAddressId); + }); + }); + + describe("getDestinationAddressId", () => { + it("should return the destination address ID", () => { + expect(transfer.getDestinationAddressId()).toEqual(toAddressId); + }); + }); + + describe("getAssetId", () => { + it("should return the asset ID", () => { + expect(transfer.getAssetId()).toEqual("eth"); + }); + }); + + describe("getAmount", () => { + it("should return the amount", () => { + transferModel.asset_id = "usdc"; + transfer = new Transfer(transferModel, mockApiClients); + expect(transfer.getAmount()).toEqual(amount); + }); + + it("should return the ETH amount when the asset ID is eth", () => { + expect(transfer.getAmount()).toEqual(BigInt(ethAmount)); + }); + }); + + describe("getUnsignedPayload", () => { + it("should return the unsigned payload", () => { + expect(transfer.getUnsignedPayload()).toEqual(unsignedPayload); + }); + }); + + describe("getSignedPayload", () => { + it("should return undefined when the transfer has not been broadcast on chain", () => { + expect(transfer.getSignedPayload()).toBeUndefined(); + }); + + it("should return the signed payload when the transfer has been broadcast on chain", () => { + transferModel.signed_payload = signedPayload; + transfer = new Transfer(transferModel, mockApiClients); + expect(transfer.getSignedPayload()).toEqual(signedPayload); + }); + }); + + describe("getTransactionHash", () => { + it("should return undefined when the transfer has not been broadcast on chain", () => { + expect(transfer.getTransactionHash()).toBeUndefined(); + }); + + it("should return the transaction hash when the transfer has been broadcast on chain", () => { + transferModel.transaction_hash = transactionHash; + transfer = new Transfer(transferModel, mockApiClients); + expect(transfer.getTransactionHash()).toEqual(transactionHash); + }); + }); + + describe("getTransaction", () => { + it("should return the Transfer transaction", () => { + const transaction = transfer.getTransaction(); + expect(transaction).toBeInstanceOf(ethers.Transaction); + expect(transaction.chainId).toEqual(BigInt("0x14a34")); + expect(transaction.nonce).toEqual(Number("0x0")); + expect(transaction.maxPriorityFeePerGas).toEqual(BigInt("0x59682f00")); + expect(transaction.maxFeePerGas).toEqual(BigInt("0x59682f00")); + expect(transaction.gasLimit).toEqual(BigInt("0x5208")); + expect(transaction.to).toEqual(toAddressId); + expect(transaction.value).toEqual(amount); + expect(transaction.data).toEqual("0x"); + }); + }); + + describe("getStatus", () => { + it("should return PENDING when the transaction has not been created", async () => { + const status = await transfer.getStatus(); + expect(status).toEqual(TransferStatus.PENDING); + }); + + it("should return PENDING when the transaction has been created but not broadcast", async () => { + transferModel.transaction_hash = transactionHash; + transfer = new Transfer(transferModel, mockApiClients); + mockProvider.getTransaction.mockResolvedValueOnce(null); + const status = await transfer.getStatus(); + expect(status).toEqual(TransferStatus.PENDING); + }); + + it("should return BROADCAST when the transaction has been broadcast but not included in a block", async () => { + transferModel.transaction_hash = transactionHash; + transfer = new Transfer(transferModel, mockApiClients); + mockProvider.getTransaction.mockResolvedValueOnce({ + blockHash: null, + } as ethers.TransactionResponse); + const status = await transfer.getStatus(); + expect(status).toEqual(TransferStatus.BROADCAST); + }); + + it("should return COMPLETE when the transaction has confirmed", async () => { + transferModel.transaction_hash = transactionHash; + transfer = new Transfer(transferModel, mockApiClients); + mockProvider.getTransaction.mockResolvedValueOnce({ + blockHash: "0xdeadbeef", + } as ethers.TransactionResponse); + mockProvider.getTransactionReceipt.mockResolvedValueOnce({ + status: 1, + } as ethers.TransactionReceipt); + const status = await transfer.getStatus(); + expect(status).toEqual(TransferStatus.COMPLETE); + }); + + it("should return FAILED when the transaction has failed", async () => { + transferModel.transaction_hash = transactionHash; + transfer = new Transfer(transferModel, mockApiClients); + mockProvider.getTransaction.mockResolvedValueOnce({ + blockHash: "0xdeadbeef", + } as ethers.TransactionResponse); + mockProvider.getTransactionReceipt.mockResolvedValueOnce({ + status: 0, + } as ethers.TransactionReceipt); + const status = await transfer.getStatus(); + expect(status).toEqual(TransferStatus.FAILED); + }); + }); + + describe("wait", () => { + it("should return the completed Transfer when the transfer is completed", async () => { + transferModel.transaction_hash = transactionHash; + transfer = new Transfer(transferModel, mockApiClients); + mockProvider.getTransaction.mockResolvedValueOnce({ + blockHash: "0xdeadbeef", + } as ethers.TransactionResponse); + mockProvider.getTransactionReceipt.mockResolvedValueOnce({ + status: 1, + } as ethers.TransactionReceipt); + mockProvider.getTransaction.mockResolvedValueOnce({ + blockHash: "0xdeadbeef", + } as ethers.TransactionResponse); + mockProvider.getTransactionReceipt.mockResolvedValueOnce({ + status: 1, + } as ethers.TransactionReceipt); + + const promise = transfer.wait(0.2, 10); + + const result = await promise; + expect(result).toBe(transfer); + const status = await transfer.getStatus(); + expect(status).toEqual(TransferStatus.COMPLETE); + }); + + it("should return the failed Transfer when the transfer is failed", async () => { + transferModel.transaction_hash = transactionHash; + transfer = new Transfer(transferModel, mockApiClients); + mockProvider.getTransaction.mockResolvedValueOnce({ + blockHash: "0xdeadbeef", + } as ethers.TransactionResponse); + mockProvider.getTransactionReceipt.mockResolvedValueOnce({ + status: 0, + } as ethers.TransactionReceipt); + mockProvider.getTransaction.mockResolvedValueOnce({ + blockHash: "0xdeadbeef", + } as ethers.TransactionResponse); + mockProvider.getTransactionReceipt.mockResolvedValueOnce({ + status: 0, + } as ethers.TransactionReceipt); + + const promise = transfer.wait(0.2, 10); + + const result = await promise; + expect(result).toBe(transfer); + const status = await transfer.getStatus(); + expect(status).toEqual(TransferStatus.FAILED); + }); + + it("should throw an error when the transfer times out", async () => { + const promise = transfer.wait(0.2, 0.00001); + + await expect(promise).rejects.toThrow("Transfer timed out"); + }); + }); +}); diff --git a/src/coinbase/transfer.ts b/src/coinbase/transfer.ts new file mode 100644 index 00000000..79e4687d --- /dev/null +++ b/src/coinbase/transfer.ts @@ -0,0 +1,250 @@ +import { TransferAPIClient, TransferStatus } from "./types"; +import { Coinbase } from "./coinbase"; +import { Transfer as TransferModel } from "../client/api"; +import { ethers } from "ethers"; +import { InternalError, InvalidUnsignedPayload } from "./errors"; +import { delay } from "./utils"; + +/** + * The Transfer API client types. + */ +export type TransferClients = { + transfer: TransferAPIClient; + baseSepoliaProvider: ethers.Provider; +}; + +/** + * A representation of a Transfer, which moves an Amount of an Asset from + * a user-controlled Wallet to another Address. The fee is assumed to be paid + * in the native Asset of the Network. + */ +export class Transfer { + private model: TransferModel; + private client: TransferClients; + private transaction?: ethers.Transaction; + + /** + * Initializes a new Transfer instance. + * + * @param transferModel - The Transfer model. + * @param client - The API clients. + */ + constructor(transferModel: TransferModel, client: TransferClients) { + if (!transferModel) { + throw new InternalError("Transfer model cannot be empty"); + } + this.model = transferModel; + + if (!client) { + throw new InternalError("API clients cannot be empty"); + } + this.client = client; + } + + /** + * Returns the ID of the Transfer. + * + * @returns The Transfer ID. + */ + public getId(): string { + return this.model.transfer_id; + } + + /** + * Returns the Network ID of the Transfer. + * + * @returns The Network ID. + */ + public getNetworkId(): string { + return this.model.network_id; + } + + /** + * Returns the Wallet ID of the Transfer. + * + * @returns The Wallet ID. + */ + public getWalletId(): string { + return this.model.wallet_id; + } + + /** + * Returns the From Address ID of the Transfer. + * + * @returns The From Address ID. + */ + public getFromAddressId(): string { + return this.model.address_id; + } + + /** + * Returns the Destination Address ID of the Transfer. + * + * @returns The Destination Address ID. + */ + public getDestinationAddressId(): string { + return this.model.destination; + } + + /** + * Returns the Asset ID of the Transfer. + * + * @returns The Asset ID. + */ + public getAssetId(): string { + return this.model.asset_id; + } + + /** + * Returns the Amount of the Transfer. + * + * @returns The Amount of the Asset. + */ + public getAmount(): bigint { + const amount = BigInt(this.model.amount); + + if (this.getAssetId() === Coinbase.assetList.Eth) { + return amount / BigInt(Coinbase.WEI_PER_ETHER); + } + return BigInt(this.model.amount); + } + + /** + * Returns the Unsigned Payload of the Transfer. + * + * @returns The Unsigned Payload as a Hex string. + */ + public getUnsignedPayload(): string { + return this.model.unsigned_payload; + } + + /** + * Returns the Signed Payload of the Transfer. + * + * @returns The Signed Payload as a Hex string, or undefined if not yet available. + */ + public getSignedPayload(): string | undefined { + return this.model.signed_payload; + } + + /** + * Returns the Transaction Hash of the Transfer. + * + * @returns The Transaction Hash as a Hex string, or undefined if not yet available. + */ + public getTransactionHash(): string | undefined { + return this.model.transaction_hash; + } + + /** + * Returns the Transaction of the Transfer. + * + * @returns The ethers.js Transaction object. + * @throws (InvalidUnsignedPayload) If the Unsigned Payload is invalid. + */ + public getTransaction(): ethers.Transaction { + if (this.transaction) return this.transaction; + + const transaction = new ethers.Transaction(); + + const rawPayload = this.getUnsignedPayload() + .match(/../g) + ?.map(byte => parseInt(byte, 16)); + if (!rawPayload) { + throw new InvalidUnsignedPayload("Unable to parse unsigned payload"); + } + + let parsedPayload; + try { + const rawPayloadBytes = new Uint8Array(rawPayload); + const decoder = new TextDecoder(); + parsedPayload = JSON.parse(decoder.decode(rawPayloadBytes)); + } catch (error) { + throw new InvalidUnsignedPayload("Unable to decode unsigned payload JSON"); + } + + transaction.chainId = BigInt(parsedPayload.chainId); + transaction.nonce = BigInt(parsedPayload.nonce); + transaction.maxPriorityFeePerGas = BigInt(parsedPayload.maxPriorityFeePerGas); + transaction.maxFeePerGas = BigInt(parsedPayload.maxFeePerGas); + transaction.gasLimit = BigInt(parsedPayload.gas); + transaction.to = parsedPayload.to; + transaction.value = BigInt(parsedPayload.value); + transaction.data = parsedPayload.input; + + this.transaction = transaction; + return transaction; + } + + /** + * Sets the Signed Transaction of the Transfer. + * + * @param transaction - The Signed Transaction. + */ + public setSignedTransaction(transaction: ethers.Transaction): void { + this.transaction = transaction; + } + + /** + * Returns the Status of the Transfer. + * + * @returns The Status of the Transfer. + */ + public async getStatus(): Promise { + const transactionHash = this.getTransactionHash(); + if (!transactionHash) return TransferStatus.PENDING; + + const onchainTransaction = + await this.client.baseSepoliaProvider!.getTransaction(transactionHash); + if (!onchainTransaction) return TransferStatus.PENDING; + if (!onchainTransaction.blockHash) return TransferStatus.BROADCAST; + + const transactionReceipt = + await this.client.baseSepoliaProvider!.getTransactionReceipt(transactionHash); + return transactionReceipt?.status ? TransferStatus.COMPLETE : TransferStatus.FAILED; + } + + /** + * Waits until the Transfer is completed or failed by polling the Network at the given interval. + * + * @param intervalSeconds - The interval at which to poll the Network, in seconds. + * @param timeoutSeconds - The maximum amount of time to wait for the Transfer to complete, in seconds. + * @returns The completed Transfer object. + * @throws {Error} if the Transfer takes longer than the given timeout. + */ + public async wait(intervalSeconds = 0.2, timeoutSeconds = 10): Promise { + const startTime = Date.now(); + while (Date.now() - startTime < timeoutSeconds * 1000) { + const status = await this.getStatus(); + if (status === TransferStatus.COMPLETE || status === TransferStatus.FAILED) { + return this; + } + await delay(intervalSeconds); + } + throw new Error("Transfer timed out"); + } + + /** + * Returns the link to the Transaction on the blockchain explorer. + * + * @returns The link to the Transaction on the blockchain explorer. + */ + public getTransactionLink(): string { + return `https://sepolia.basescan.org/tx/${this.getTransactionHash()}`; + } + + /** + * Returns a string representation of the Transfer. + * + * @returns The string representation of the Transfer. + */ + public async toString(): Promise { + const status = await this.getStatus(); + return ( + `Transfer{transferId: '${this.getId()}', networkId: '${this.getNetworkId()}', ` + + `fromAddressId: '${this.getFromAddressId()}', destinationAddressId: '${this.getDestinationAddressId()}', ` + + `assetId: '${this.getAssetId()}', amount: '${this.getAmount()}', transactionHash: '${this.getTransactionHash()}', ` + + `transactionLink: '${this.getTransactionLink()}', status: '${status}'}` + ); + } +} diff --git a/src/coinbase/types.ts b/src/coinbase/types.ts index 123ce3ba..0ce6957f 100644 --- a/src/coinbase/types.ts +++ b/src/coinbase/types.ts @@ -1,9 +1,14 @@ import { AxiosPromise, AxiosRequestConfig, RawAxiosRequestConfig } from "axios"; +import { ethers } from "ethers"; import { - Address, CreateWalletRequest, + BroadcastTransferRequest, + CreateTransferRequest, + TransferList, User as UserModel, Wallet as WalletModel, + Address as AddressModel, + Transfer as TransferModel, } from "./../client/api"; /** @@ -53,7 +58,7 @@ export type AddressAPIClient = { walletId: string, addressId: string, options?: AxiosRequestConfig, - ): AxiosPromise
; + ): AxiosPromise; }; /** @@ -70,12 +75,101 @@ export type UserAPIClient = { getCurrentUser(options?: AxiosRequestConfig): AxiosPromise; }; +/** + * TransferAPI client type definition. + */ +export type TransferAPIClient = { + /** + * Broadcasts a transfer. + * + * @param walletId - The ID of the wallet the address belongs to. + * @param addressId - The ID of the address the transfer belongs to. + * @param transferId - The ID of the transfer to broadcast. + * @param broadcastTransferRequest - The request body. + * @param options - Axios request options. + * @returns - A promise resolving to the Transfer model. + * @throws {APIError} If the request fails. + */ + broadcastTransfer( + walletId: string, + addressId: string, + transferId: string, + broadcastTransferRequest: BroadcastTransferRequest, + options?: AxiosRequestConfig, + ): AxiosPromise; + + /** + * Creates a Transfer. + * + * @param walletId - The ID of the wallet the address belongs to. + * @param addressId - The ID of the address the transfer belongs to. + * @param createTransferRequest - The request body. + * @param options - Axios request options. + * @returns - A promise resolving to the Transfer model. + * @throws {APIError} If the request fails. + */ + createTransfer( + walletId: string, + addressId: string, + createTransferRequest: CreateTransferRequest, + options?: AxiosRequestConfig, + ): AxiosPromise; + + /** + * Retrieves a Transfer. + * + * @param walletId - The ID of the wallet the address belongs to. + * @param addressId - The ID of the address the transfer belongs to. + * @param transferId - The ID of the transfer to retrieve. + * @param options - Axios request options. + * @returns - A promise resolving to the Transfer model. + * @throws {APIError} If the request fails. + */ + getTransfer( + walletId: string, + addressId: string, + transferId: string, + options?: AxiosRequestConfig, + ): AxiosPromise; + + /** + * Lists Transfers. + * + * @param walletId - The ID of the wallet the address belongs to. + * @param addressId - The ID of the address the transfers belong to. + * @param limit - The maximum number of transfers to return. + * @param page - The cursor for pagination across multiple pages of Transfers. + * @param options - Axios request options. + * @returns - A promise resolving to the Transfer list. + * @throws {APIError} If the request fails. + */ + listTransfers( + walletId: string, + addressId: string, + limit?: number, + page?: string, + options?: AxiosRequestConfig, + ): AxiosPromise; +}; + /** * API clients type definition for the Coinbase SDK. * Represents the set of API clients available in the SDK. */ export type ApiClients = { user?: UserAPIClient; - address?: AddressAPIClient; wallet?: WalletAPIClient; + address?: AddressAPIClient; + transfer?: TransferAPIClient; + baseSepoliaProvider?: ethers.Provider; }; + +/** + * Transfer status type definition. + */ +export enum TransferStatus { + PENDING = "PENDING", + BROADCAST = "BROADCAST", + COMPLETE = "COMPLETE", + FAILED = "FAILED", +} diff --git a/src/coinbase/user.ts b/src/coinbase/user.ts index c5fab06c..347a2990 100644 --- a/src/coinbase/user.ts +++ b/src/coinbase/user.ts @@ -1,5 +1,6 @@ import { ApiClients } from "./types"; import { User as UserModel } from "./../client/api"; + /** * A representation of a User. * Users have Wallets, which can hold balances of Assets. diff --git a/src/coinbase/utils.ts b/src/coinbase/utils.ts index b45ab341..f647b8c1 100644 --- a/src/coinbase/utils.ts +++ b/src/coinbase/utils.ts @@ -70,3 +70,13 @@ export const registerAxiosInterceptors = ( export const convertStringToHex = (key: Uint8Array): string => { return Buffer.from(key).toString("hex"); }; + +/** + * Delays the execution of the function by the specified number of seconds. + * + * @param seconds - The number of seconds to delay the execution. + * @returns A promise that resolves after the specified number of seconds. + */ +export async function delay(seconds: number): Promise { + return new Promise(resolve => setTimeout(resolve, seconds * 1000)); +}