diff --git a/examples/ts/reserve-unspents.ts b/examples/ts/reserve-unspents.ts index 02a82d38dc..014809d788 100644 --- a/examples/ts/reserve-unspents.ts +++ b/examples/ts/reserve-unspents.ts @@ -52,6 +52,17 @@ async function releaseUnspentReservation() { console.log('released ' + JSON.stringify(reserveResult, null, 2)); } +async function listUnspentReservations() { + bitgo.authenticateWithAccessToken({ accessToken }); + const wallet = await bitgo.coin(coin).wallets().get({ id: walletId }); + + const listResult = await wallet.manageUnspentReservations({ + list: { limit: 25 }, + }); + console.log('reserved unspents: ' + JSON.stringify(listResult, null, 2)); +} + // createUnspentReservation().catch(console.error); // modifyUnspentReservation().catch(console.error); // releaseUnspentReservation().catch(console.error); +// listUnspentReservations().catch(console.error); diff --git a/modules/bitgo/test/v2/unit/unspents.ts b/modules/bitgo/test/v2/unit/unspents.ts index 0d2ed310d6..dad1c710c2 100644 --- a/modules/bitgo/test/v2/unit/unspents.ts +++ b/modules/bitgo/test/v2/unit/unspents.ts @@ -196,9 +196,63 @@ describe('Verify string type is used for value of unspent', function () { delete: { id: unspentIds[0], dontIncludeThis: 'this' } as unknown as { id: string }, }); + // List + const listScope = nock(bgUrl) + .get(`/api/v2/wallet/${wallet.id()}/reservedunspents`) + .query({ limit: '10', prevId: 'prev-123' }) + .reply(200, { unspents: [], nextBatchPrevId: undefined }); + await wallet.manageUnspentReservations({ + list: { limit: 10, prevId: 'prev-123', dontIncludeThis: 'this' } as unknown as { + limit: number; + prevId: string; + }, + }); + createScope.done(); modifyScope.done(); deleteScope.done(); + listScope.done(); + }); + + it('should list reserved unspents with pagination', async function () { + const mockUnspents = [ + { id: 'txid:0', walletId: wallet.id(), expireTime: '2030-01-01T00:00:00.000Z' }, + { id: 'txid:1', walletId: wallet.id(), expireTime: '2030-01-01T00:00:00.000Z' }, + ]; + const mockNextBatchPrevId = 'next-page-cursor'; + + const listScope = nock(bgUrl) + .get(`/api/v2/wallet/${wallet.id()}/reservedunspents`) + .query({ limit: '2' }) + .reply(200, { unspents: mockUnspents, nextBatchPrevId: mockNextBatchPrevId }); + + const result = await wallet.manageUnspentReservations({ list: { limit: 2 } }); + + result.should.deepEqual({ unspents: mockUnspents, nextBatchPrevId: mockNextBatchPrevId }); + listScope.done(); + }); + + it('should list reserved unspents filtering by expireTimeGt', async function () { + const expireTimeGt = '2025-01-01T00:00:00.000Z'; + const mockUnspents = [ + { id: 'txid:2', walletId: wallet.id(), expireTime: '2030-06-01T00:00:00.000Z' }, + ]; + + const listScope = nock(bgUrl) + .get(`/api/v2/wallet/${wallet.id()}/reservedunspents`) + .query({ expireTimeGt }) + .reply(200, { unspents: mockUnspents }); + + const result = await wallet.manageUnspentReservations({ list: { expireTimeGt } }); + + result.should.deepEqual({ unspents: mockUnspents }); + listScope.done(); + }); + + it('should throw when no operation is provided', async function () { + await wallet + .manageUnspentReservations({}) + .should.be.rejectedWith('Did not detect a creation, modification, deletion, or list request.'); }); }); }); diff --git a/modules/sdk-core/src/bitgo/wallet/iWallet.ts b/modules/sdk-core/src/bitgo/wallet/iWallet.ts index 3681c535f3..e03ae60ab7 100644 --- a/modules/sdk-core/src/bitgo/wallet/iWallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/iWallet.ts @@ -495,6 +495,13 @@ export interface UnspentsOptions extends PaginationOptions { unspentIds?: string[]; } +export interface ReservedUnspent { + id: string; + walletId: string; + expireTime: string; + userId?: string; +} + export interface ManageUnspentReservationOptions { create?: { unspentIds: string[]; @@ -509,6 +516,16 @@ export interface ManageUnspentReservationOptions { delete?: { id: string; }; + list?: { + limit?: number; + prevId?: string; + expireTimeGt?: string; + }; +} + +export interface ListReservedUnspentsResponse { + unspents: ReservedUnspent[]; + nextBatchPrevId?: string; } export interface ConsolidateUnspentsOptions extends WalletSignTransactionOptions { @@ -1143,6 +1160,9 @@ export interface IWallet { unspents(params?: UnspentsOptions): Promise; consolidateUnspents(params?: ConsolidateUnspentsOptions): Promise; fanoutUnspents(params?: FanoutUnspentsOptions): Promise; + manageUnspentReservations( + params: ManageUnspentReservationOptions + ): Promise<{ unspents: ReservedUnspent[] } | ListReservedUnspentsResponse>; updateTokenFlushThresholds(thresholds?: any): Promise; updateForwarders(forwarderFlags?: any): Promise; deployForwarders(params: DeployForwardersOptions): Promise; diff --git a/modules/sdk-core/src/bitgo/wallet/wallet.ts b/modules/sdk-core/src/bitgo/wallet/wallet.ts index 870d8f2a9e..997d5baf62 100644 --- a/modules/sdk-core/src/bitgo/wallet/wallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/wallet.ts @@ -100,7 +100,9 @@ import { GetTransferOptions, GetUserPrvOptions, type IWallet, + ListReservedUnspentsResponse, ManageUnspentReservationOptions, + ReservedUnspent, MaximumSpendable, MaximumSpendableOptions, ModifyWebhookOptions, @@ -893,16 +895,12 @@ export class Wallet implements IWallet { * @param params.create - create a new reservation * @param params.modify - modify an existing reservation * @param params.delete - delete an existing reservation + * @param params.list - list existing reservations */ - async manageUnspentReservations(params: ManageUnspentReservationOptions): Promise<{ - unspents: { - id: string; - walletId: string; - expireTime: string; - userId?: string; - }[]; - }> { - const filteredParams = _.pick(params, ['create', 'modify', 'delete']); + async manageUnspentReservations( + params: ManageUnspentReservationOptions + ): Promise<{ unspents: ReservedUnspent[] } | ListReservedUnspentsResponse> { + const filteredParams = _.pick(params, ['create', 'modify', 'delete', 'list']); this.bitgo.setRequestTracer(new RequestTracer()); // The URL cannot contain the coinName, so we remove it from the URL const url = this.url(`/reservedunspents`).replace(`/${this.baseCoin.getChain()}`, ''); @@ -915,8 +913,11 @@ export class Wallet implements IWallet { } else if (filteredParams.delete) { const filteredDeleteParams = _.pick(params.delete, ['id']); return this.bitgo.del(url).query(filteredDeleteParams).result(); + } else if (filteredParams.list) { + const filteredListParams = _.pick(params.list, ['limit', 'prevId', 'expireTimeGt']); + return this.bitgo.get(url).query(filteredListParams).result(); } else { - throw new Error('Did not detect a creation, modification, or deletion request.'); + throw new Error('Did not detect a creation, modification, deletion, or list request.'); } }