From e343f7c0f363fcdbe712c26235c7675b263e2190 Mon Sep 17 00:00:00 2001 From: rohan-agarwal-coinbase Date: Wed, 4 Dec 2024 17:12:14 -0500 Subject: [PATCH] Update fund and quoteFund interfaces to take objects and added listFundOperations (#330) --- CHANGELOG.md | 3 + jest.config.js | 2 +- src/coinbase/address/wallet_address.ts | 100 +++++++++++++++++-------- src/coinbase/types.ts | 13 ++++ src/coinbase/wallet.ts | 41 ++++++++-- src/tests/wallet_address_fund_test.ts | 27 +++++-- src/tests/wallet_fund_test.ts | 49 ++++++++---- src/tests/wallet_test.ts | 8 +- 8 files changed, 178 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43c24f40..d58ddb6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Add `listFundOperations` method to `Wallet` and `WalletAddress` to list the fund operations associated with the wallet or address. +- Updated `fund` and `quoteFund` methods to take `CreateFundOptions` object instead of individual parameters. + ## [0.11.1] - 2024-11-29 ### Fixed diff --git a/jest.config.js b/jest.config.js index 2642a5f3..40a4c63a 100644 --- a/jest.config.js +++ b/jest.config.js @@ -10,7 +10,7 @@ module.exports = { maxWorkers: 1, coverageThreshold: { "./src/coinbase/**": { - branches: 77, + branches: 75, functions: 85, statements: 85, lines: 85, diff --git a/src/coinbase/address/wallet_address.ts b/src/coinbase/address/wallet_address.ts index 31c15181..a3b773a1 100644 --- a/src/coinbase/address/wallet_address.ts +++ b/src/coinbase/address/wallet_address.ts @@ -20,6 +20,8 @@ import { CreateERC1155Options, PaginationOptions, PaginationResponse, + CreateFundOptions, + CreateQuoteOptions, } from "../types"; import { delay } from "../utils"; import { Wallet as WalletClass } from "../wallet"; @@ -311,19 +313,24 @@ export class WalletAddress extends Address { * @throws {Error} if the address cannot sign. * @throws {ArgumentError} if the address does not have sufficient balance. */ - public async invokeContract( - options: CreateContractInvocationOptions, - ): Promise { + public async invokeContract({ + contractAddress, + method, + abi, + args, + amount, + assetId, + }: CreateContractInvocationOptions): Promise { if (!Coinbase.useServerSigner && !this.key) { throw new Error("Cannot invoke contract from address without private key loaded"); } let atomicAmount: string | undefined; - if (options.assetId && options.amount) { - const asset = await Asset.fetch(this.getNetworkId(), options.assetId); - const normalizedAmount = new Decimal(options.amount.toString()); - const currentBalance = await this.getBalance(options.assetId); + if (assetId && amount) { + const asset = await Asset.fetch(this.getNetworkId(), assetId); + const normalizedAmount = new Decimal(amount.toString()); + const currentBalance = await this.getBalance(assetId); if (currentBalance.lessThan(normalizedAmount)) { throw new ArgumentError( `Insufficient funds: ${normalizedAmount} requested, but only ${currentBalance} available`, @@ -333,10 +340,10 @@ export class WalletAddress extends Address { } const contractInvocation = await this.createContractInvocation( - options.contractAddress, - options.method, - options.abi!, - options.args, + contractAddress, + method, + abi!, + args, atomicAmount, ); @@ -360,12 +367,12 @@ export class WalletAddress extends Address { * @returns A Promise that resolves to the deployed SmartContract object. * @throws {APIError} If the API request to create a smart contract fails. */ - public async deployToken(options: CreateERC20Options): Promise { + public async deployToken({ name, symbol, totalSupply }: CreateERC20Options): Promise { if (!Coinbase.useServerSigner && !this.key) { throw new Error("Cannot deploy ERC20 without private key loaded"); } - const smartContract = await this.createERC20(options); + const smartContract = await this.createERC20({ name, symbol, totalSupply }); if (Coinbase.useServerSigner) { return smartContract; @@ -387,12 +394,12 @@ export class WalletAddress extends Address { * @returns A Promise that resolves to the deployed SmartContract object. * @throws {APIError} If the API request to create a smart contract fails. */ - public async deployNFT(options: CreateERC721Options): Promise { + public async deployNFT({ name, symbol, baseURI }: CreateERC721Options): Promise { if (!Coinbase.useServerSigner && !this.key) { throw new Error("Cannot deploy ERC721 without private key loaded"); } - const smartContract = await this.createERC721(options); + const smartContract = await this.createERC721({ name, symbol, baseURI }); if (Coinbase.useServerSigner) { return smartContract; @@ -412,12 +419,12 @@ export class WalletAddress extends Address { * @returns A Promise that resolves to the deployed SmartContract object. * @throws {APIError} If the API request to create a smart contract fails. */ - public async deployMultiToken(options: CreateERC1155Options): Promise { + public async deployMultiToken({ uri }: CreateERC1155Options): Promise { if (!Coinbase.useServerSigner && !this.key) { throw new Error("Cannot deploy ERC1155 without private key loaded"); } - const smartContract = await this.createERC1155(options); + const smartContract = await this.createERC1155({ uri }); if (Coinbase.useServerSigner) { return smartContract; @@ -440,16 +447,16 @@ export class WalletAddress extends Address { * @returns {Promise} A Promise that resolves to the created SmartContract. * @throws {APIError} If the API request to create a smart contract fails. */ - private async createERC20(options: CreateERC20Options): Promise { + private async createERC20({ name, symbol, totalSupply }: CreateERC20Options): Promise { const resp = await Coinbase.apiClients.smartContract!.createSmartContract( this.getWalletId(), this.getId(), { type: SmartContractType.Erc20, options: { - name: options.name, - symbol: options.symbol, - total_supply: options.totalSupply.toString(), + name, + symbol, + total_supply: totalSupply.toString(), }, }, ); @@ -466,16 +473,16 @@ export class WalletAddress extends Address { * @returns A Promise that resolves to the deployed SmartContract object. * @throws {APIError} If the private key is not loaded when not using server signer. */ - private async createERC721(options: CreateERC721Options): Promise { + private async createERC721({ name, symbol, baseURI }: CreateERC721Options): Promise { const resp = await Coinbase.apiClients.smartContract!.createSmartContract( this.getWalletId(), this.getId(), { type: SmartContractType.Erc721, options: { - name: options.name, - symbol: options.symbol, - base_uri: options.baseURI, + name, + symbol, + base_uri: baseURI, }, }, ); @@ -491,14 +498,14 @@ export class WalletAddress extends Address { * @returns {Promise} A Promise that resolves to the created SmartContract. * @throws {APIError} If the API request to create a smart contract fails. */ - private async createERC1155(options: CreateERC1155Options): Promise { + private async createERC1155({ uri }: CreateERC1155Options): Promise { const resp = await Coinbase.apiClients.smartContract!.createSmartContract( this.getWalletId(), this.getId(), { type: SmartContractType.Erc1155, options: { - uri: options.uri, + uri, }, }, ); @@ -752,11 +759,15 @@ export class WalletAddress extends Address { /** * Fund the address from your account on the Coinbase Platform. * - * @param amount - The amount of the Asset to fund the wallet with - * @param assetId - The ID of the Asset to fund with. For Ether, eth, gwei, and wei are supported. + * @param options - The options to create the fund operation + * @param options.amount - The amount of the Asset to fund the wallet with + * @param options.assetId - The ID of the Asset to fund with. For Ether, eth, gwei, and wei are supported. * @returns The created fund operation object */ - public async fund(amount: Amount, assetId: string): Promise { + public async fund({ + amount, + assetId, + }: CreateFundOptions): Promise { const normalizedAmount = new Decimal(amount.toString()); return FundOperation.create( @@ -771,11 +782,15 @@ export class WalletAddress extends Address { /** * Get a quote for funding the address from your Coinbase platform account. * - * @param amount - The amount to fund - * @param assetId - The ID of the Asset to fund with. For Ether, eth, gwei, and wei are supported. + * @param options - The options to create the fund quote + * @param options.amount - The amount to fund + * @param options.assetId - The ID of the Asset to fund with. For Ether, eth, gwei, and wei are supported. * @returns The fund quote object */ - public async quoteFund(amount: Amount, assetId: string): Promise { + public async quoteFund({ + amount, + assetId, + }: CreateQuoteOptions): Promise { const normalizedAmount = new Decimal(amount.toString()); return FundQuote.create( @@ -787,6 +802,25 @@ export class WalletAddress extends Address { ); } + /** + * Returns all the fund operations associated with the address. + * + * @param options - The pagination options. + * @param options.limit - The maximum number of Fund Operations to return. Limit can range between 1 and 100. + * @param options.page - The cursor for pagination across multiple pages of Fund Operations. Don't include this parameter on the first call. Use the next page value returned in a previous response to request subsequent results. + * + * @returns The paginated list response of fund operations. + */ + public async listFundOperations({ + limit = Coinbase.defaultPageLimit, + page = undefined, + }: PaginationOptions = {}): Promise> { + return FundOperation.listFundOperations(this.model.wallet_id, this.model.address_id, { + limit, + page, + }); + } + /** * Returns the address and network ID of the given destination. * diff --git a/src/coinbase/types.ts b/src/coinbase/types.ts index 099d7870..90dc36fe 100644 --- a/src/coinbase/types.ts +++ b/src/coinbase/types.ts @@ -1052,6 +1052,19 @@ export type CreateERC1155Options = { uri: string; }; +/** + * Options for creating a fund operation. + */ +export type CreateFundOptions = { + amount: Amount; + assetId: string; +}; + +/** + * Options for creating a quote for a fund operation. + */ +export type CreateQuoteOptions = CreateFundOptions; + /** * Options for listing historical balances of an address. */ diff --git a/src/coinbase/wallet.ts b/src/coinbase/wallet.ts index bfa9ab5c..b8b16e97 100644 --- a/src/coinbase/wallet.ts +++ b/src/coinbase/wallet.ts @@ -31,6 +31,8 @@ import { CreateERC1155Options, PaginationOptions, PaginationResponse, + CreateFundOptions, + CreateQuoteOptions, } from "./types"; import { convertStringToHex, delay, formatDate, getWeekBackDate } from "./utils"; import { StakingOperation } from "./staking_operation"; @@ -841,35 +843,58 @@ export class Wallet { /** * Fund the wallet from your account on the Coinbase Platform. * - * @param amount - The amount of the Asset to fund the wallet with - * @param assetId - The ID of the Asset to fund with. For Ether, eth, gwei, and wei are supported. + * @param options - The options to create the fund operation + * @param options.amount - The amount of the Asset to fund the wallet with + * @param options.assetId - The ID of the Asset to fund with. For Ether, eth, gwei, and wei are supported. * @returns The created fund operation object * @throws {Error} If the default address does not exist */ - public async fund(amount: Amount, assetId: string): Promise { + public async fund(options: CreateFundOptions): Promise { const defaultAddress = await this.getDefaultAddress(); if (!defaultAddress) { throw new Error("Default address does not exist"); } - return defaultAddress.fund(amount, assetId); + return defaultAddress.fund(options); } /** * Get a quote for funding the wallet from your Coinbase platform account. * - * @param amount - The amount to fund - * @param assetId - The ID of the Asset to fund with. For Ether, eth, gwei, and wei are supported. + * @param options - The options to create the fund quote + * @param options.amount - The amount to fund + * @param options.assetId - The ID of the Asset to fund with. For Ether, eth, gwei, and wei are supported. * @returns The fund quote object * @throws {Error} If the default address does not exist */ - public async quoteFund(amount: Amount, assetId: string): Promise { + public async quoteFund(options: CreateQuoteOptions): Promise { const defaultAddress = await this.getDefaultAddress(); if (!defaultAddress) { throw new Error("Default address does not exist"); } - return defaultAddress.quoteFund(amount, assetId); + return defaultAddress.quoteFund(options); + } + + /** + * Returns all the fund operations associated with the wallet's default address. + * + * @param options - The pagination options. + * @param options.limit - The maximum number of fund operations to return. Limit can range between 1 and 100. + * @param options.page - The cursor for pagination across multiple pages of fund operations. Don't include this parameter on the first call. Use the next page value returned in a previous response to request subsequent results. + * @returns The paginated list response of fund operations. + * @throws {Error} If the default address does not exist + */ + public async listFundOperations({ + limit = Coinbase.defaultPageLimit, + page = undefined, + }: PaginationOptions = {}): Promise> { + const defaultAddress = await this.getDefaultAddress(); + if (!defaultAddress) { + throw new Error("Default address does not exist"); + } + + return defaultAddress.listFundOperations({ limit, page }); } /** diff --git a/src/tests/wallet_address_fund_test.ts b/src/tests/wallet_address_fund_test.ts index 4050d096..b7db396e 100644 --- a/src/tests/wallet_address_fund_test.ts +++ b/src/tests/wallet_address_fund_test.ts @@ -1,6 +1,7 @@ import { WalletAddress } from "../coinbase/address/wallet_address"; import { FundOperation } from "../coinbase/fund_operation"; import { FundQuote } from "../coinbase/fund_quote"; +import { PaginationResponse } from "../coinbase/types"; import { newAddressModel } from "./utils"; import { Decimal } from "decimal.js"; @@ -14,6 +15,9 @@ describe("WalletAddress Fund", () => { jest.spyOn(FundOperation, "create").mockResolvedValue({} as FundOperation); jest.spyOn(FundQuote, "create").mockResolvedValue({} as FundQuote); + jest + .spyOn(FundOperation, "listFundOperations") + .mockResolvedValue({} as PaginationResponse); }); afterEach(() => { @@ -25,7 +29,7 @@ describe("WalletAddress Fund", () => { const amount = new Decimal("1.0"); const assetId = "eth"; - await walletAddress.fund(amount, assetId); + await walletAddress.fund({ amount, assetId }); expect(FundOperation.create).toHaveBeenCalledWith( walletId, @@ -39,7 +43,7 @@ describe("WalletAddress Fund", () => { const amount = 1; const assetId = "eth"; - await walletAddress.fund(amount, assetId); + await walletAddress.fund({ amount, assetId }); expect(FundOperation.create).toHaveBeenCalledWith( walletId, @@ -53,7 +57,7 @@ describe("WalletAddress Fund", () => { const amount = BigInt(1); const assetId = "eth"; - await walletAddress.fund(amount, assetId); + await walletAddress.fund({ amount, assetId }); expect(FundOperation.create).toHaveBeenCalledWith( walletId, @@ -70,7 +74,7 @@ describe("WalletAddress Fund", () => { const amount = new Decimal("1.0"); const assetId = "eth"; - await walletAddress.quoteFund(amount, assetId); + await walletAddress.quoteFund({ amount, assetId }); expect(FundQuote.create).toHaveBeenCalledWith( walletId, @@ -84,7 +88,7 @@ describe("WalletAddress Fund", () => { const amount = 1; const assetId = "eth"; - await walletAddress.quoteFund(amount, assetId); + await walletAddress.quoteFund({ amount, assetId }); expect(FundQuote.create).toHaveBeenCalledWith( walletId, @@ -98,7 +102,7 @@ describe("WalletAddress Fund", () => { const amount = BigInt(1); const assetId = "eth"; - await walletAddress.quoteFund(amount, assetId); + await walletAddress.quoteFund({ amount, assetId }); expect(FundQuote.create).toHaveBeenCalledWith( walletId, @@ -109,4 +113,15 @@ describe("WalletAddress Fund", () => { ); }); }); + + describe("#listFundOperations", () => { + it("should call listFundOperations with correct parameters", async () => { + await walletAddress.listFundOperations({ limit: 10, page: "test-page" }); + + expect(FundOperation.listFundOperations).toHaveBeenCalledWith(walletId, addressId, { + limit: 10, + page: "test-page", + }); + }); + }); }); diff --git a/src/tests/wallet_fund_test.ts b/src/tests/wallet_fund_test.ts index 4c5caa2f..5c715a14 100644 --- a/src/tests/wallet_fund_test.ts +++ b/src/tests/wallet_fund_test.ts @@ -4,7 +4,7 @@ import { FundOperation } from "../coinbase/fund_operation"; import { FundQuote } from "../coinbase/fund_quote"; import { newAddressModel } from "./utils"; import { Decimal } from "decimal.js"; -import { Coinbase } from ".."; +import { Coinbase, PaginationResponse } from ".."; import { FeatureSet, Wallet as WalletModel } from "../client/api"; describe("Wallet Fund", () => { @@ -33,6 +33,9 @@ describe("Wallet Fund", () => { // Mock the fund and quoteFund methods on the default address jest.spyOn(defaultAddress, "fund").mockResolvedValue({} as FundOperation); jest.spyOn(defaultAddress, "quoteFund").mockResolvedValue({} as FundQuote); + jest + .spyOn(defaultAddress, "listFundOperations") + .mockResolvedValue({} as PaginationResponse); }); afterEach(() => { @@ -44,27 +47,27 @@ describe("Wallet Fund", () => { const amount = new Decimal("1.0"); const assetId = "eth"; - await wallet.fund(amount, assetId); + await wallet.fund({ amount, assetId }); - expect(defaultAddress.fund).toHaveBeenCalledWith(amount, assetId); + expect(defaultAddress.fund).toHaveBeenCalledWith({ amount, assetId }); }); it("should call defaultAddress.fund with correct parameters when passing in number amount", async () => { const amount = 1; const assetId = "eth"; - await wallet.fund(amount, assetId); + await wallet.fund({ amount, assetId }); - expect(defaultAddress.fund).toHaveBeenCalledWith(amount, assetId); + expect(defaultAddress.fund).toHaveBeenCalledWith({ amount, assetId }); }); it("should call defaultAddress.fund with correct parameters when passing in bigint amount", async () => { const amount = BigInt(1); const assetId = "eth"; - await wallet.fund(amount, assetId); + await wallet.fund({ amount, assetId }); - expect(defaultAddress.fund).toHaveBeenCalledWith(amount, assetId); + expect(defaultAddress.fund).toHaveBeenCalledWith({ amount, assetId }); }); it("should throw error if default address does not exist", async () => { @@ -75,7 +78,9 @@ describe("Wallet Fund", () => { const amount = new Decimal("1.0"); const assetId = "eth"; - await expect(wallet.fund(amount, assetId)).rejects.toThrow("Default address does not exist"); + await expect(wallet.fund({ amount, assetId })).rejects.toThrow( + "Default address does not exist", + ); }); }); @@ -84,27 +89,27 @@ describe("Wallet Fund", () => { const amount = new Decimal("1.0"); const assetId = "eth"; - await wallet.quoteFund(amount, assetId); + await wallet.quoteFund({ amount, assetId }); - expect(defaultAddress.quoteFund).toHaveBeenCalledWith(amount, assetId); + expect(defaultAddress.quoteFund).toHaveBeenCalledWith({ amount, assetId }); }); it("should call defaultAddress.quoteFund with correct parameters when passing in number amount", async () => { const amount = 1; const assetId = "eth"; - await wallet.quoteFund(amount, assetId); + await wallet.quoteFund({ amount, assetId }); - expect(defaultAddress.quoteFund).toHaveBeenCalledWith(amount, assetId); + expect(defaultAddress.quoteFund).toHaveBeenCalledWith({ amount, assetId }); }); it("should call defaultAddress.quoteFund with correct parameters when passing in bigint amount", async () => { const amount = BigInt(1); const assetId = "eth"; - await wallet.quoteFund(amount, assetId); + await wallet.quoteFund({ amount, assetId }); - expect(defaultAddress.quoteFund).toHaveBeenCalledWith(amount, assetId); + expect(defaultAddress.quoteFund).toHaveBeenCalledWith({ amount, assetId }); }); it("should throw error if default address does not exist", async () => { @@ -115,9 +120,23 @@ describe("Wallet Fund", () => { const amount = new Decimal("1.0"); const assetId = "eth"; - await expect(wallet.quoteFund(amount, assetId)).rejects.toThrow( + await expect(wallet.quoteFund({ amount, assetId })).rejects.toThrow( "Default address does not exist", ); }); }); + + describe("#listFundOperations", () => { + it("should call defaultAddress.listFundOperations with correct parameters", async () => { + await wallet.listFundOperations({ + limit: 10, + page: "test-page", + }); + + expect(defaultAddress.listFundOperations).toHaveBeenCalledWith({ + limit: 10, + page: "test-page", + }); + }); + }); }); diff --git a/src/tests/wallet_test.ts b/src/tests/wallet_test.ts index e4a84338..ba37aef3 100644 --- a/src/tests/wallet_test.ts +++ b/src/tests/wallet_test.ts @@ -1441,8 +1441,12 @@ describe("Wallet Class", () => { jest.spyOn(Wallet.prototype, "createWebhook").mockReturnValue(wh); const result = await wallet.createWebhook("https://example.com/callback"); expect(result).toBeInstanceOf(Webhook); - expect((result.getEventTypeFilter() as WebhookWalletActivityFilter)?.wallet_id).toBe(walletModel.id); - expect((result.getEventTypeFilter() as WebhookWalletActivityFilter)?.addresses).toStrictEqual([address1]); + expect((result.getEventTypeFilter() as WebhookWalletActivityFilter)?.wallet_id).toBe( + walletModel.id, + ); + expect((result.getEventTypeFilter() as WebhookWalletActivityFilter)?.addresses).toStrictEqual( + [address1], + ); expect(result.getEventType()).toBe("wallet_activity"); }); });