Skip to content

Commit 7d89528

Browse files
feat: implement Safes.list and Safes.get
Wire up the safe read APIs now that the backend (WCN-1177) has merged. - get: GET /enterprise/:eId/safes/:safeId, decodes SafeData - list: cursor-paginated GET /enterprise/:eId/safes, mapping the SDK's opaque cursor/nextCursor onto WP's prevId/nextBatchPrevId convention WCN-1192
1 parent c68fba5 commit 7d89528

2 files changed

Lines changed: 74 additions & 11 deletions

File tree

modules/sdk-core/src/bitgo/safe/safes.ts

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* @experimental The safe client surface is experimental and may change (including breaking
55
* changes) before the public release.
66
*/
7+
import * as t from 'io-ts';
78
import { FinalizeSafeBody, InitializeSafeBody, RootKeyTriplet, RootKeyType, SafeData } from '@bitgo/public-types';
89
import { Environments } from '../../common';
910
import { IBaseCoin } from '../baseCoin';
@@ -40,6 +41,17 @@ const ROOT_COIN_BY_NETWORK: Record<'mainnet' | 'testnet', Record<RootKeyType, st
4041
},
4142
};
4243

44+
/**
45+
* Wire shape of the paginated `GET /enterprise/:eId/safes` response. WP paginates with the v2
46+
* `prevId`/`nextBatchPrevId` convention; the SDK re-exposes it as an opaque `cursor`/`nextCursor`
47+
* (see `list`).
48+
* @experimental
49+
*/
50+
const ListSafesResponse = t.intersection([
51+
t.type({ safes: t.array(SafeData) }),
52+
t.partial({ nextBatchPrevId: t.string }),
53+
]);
54+
4355
/**
4456
* Collection accessor for a single enterprise's safes, mirroring Wallets / Enterprises.
4557
* Safe routes are enterprise-scoped (/api/v2/enterprise/:eId/safes), so the accessor is
@@ -248,20 +260,38 @@ export class Safes implements ISafes {
248260
}
249261

250262
/**
251-
* List the enterprise's safes (cursor pagination).
252-
* Implemented in WCN-1192 Phase 3 (blocked on WCN-1177).
263+
* List the enterprise's safes the caller is a member of (cursor pagination).
264+
* GET /api/v2/enterprise/:eId/safes?limit&prevId
265+
*
266+
* `cursor` is the opaque `nextCursor` returned by a previous call; page forward until
267+
* `nextCursor` is absent.
253268
* @experimental
254269
*/
255270
async list(params: ListSafesOptions = {}): Promise<{ safes: Safe[]; nextCursor?: string }> {
256-
throw new Error('Safes.list is not yet implemented (WCN-1192 Phase 3)');
271+
// SDK exposes an opaque cursor; WP speaks prevId/nextBatchPrevId (v2 list convention).
272+
const query: { limit?: number; prevId?: string } = {};
273+
if (params.limit !== undefined) {
274+
query.limit = params.limit;
275+
}
276+
if (params.cursor !== undefined) {
277+
query.prevId = params.cursor;
278+
}
279+
const response = await this.bitgo.get(this.url()).query(query).result();
280+
const { safes, nextBatchPrevId } = decodeWithCodec(ListSafesResponse, response, 'ListSafesResponse');
281+
return {
282+
safes: safes.map((safeData) => new Safe(this.bitgo, safeData)),
283+
nextCursor: nextBatchPrevId,
284+
};
257285
}
258286

259287
/**
260-
* Fetch a single safe by id.
261-
* Implemented in WCN-1192 Phase 3 (blocked on WCN-1177).
288+
* Fetch a single safe by id. Non-members get a 404 (existence is not leaked).
289+
* GET /api/v2/enterprise/:eId/safes/:safeId
262290
* @experimental
263291
*/
264292
async get(params: GetSafeOptions): Promise<Safe> {
265-
throw new Error('Safes.get is not yet implemented (WCN-1192 Phase 3)');
293+
const response = await this.bitgo.get(this.url(`/${params.id}`)).result();
294+
const safeData = decodeWithCodec(SafeData, response, 'SafeData');
295+
return new Safe(this.bitgo, safeData);
266296
}
267297
}

modules/sdk-core/test/unit/bitgo/safe/safes.ts

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -253,13 +253,46 @@ describe('Safes', function () {
253253
});
254254
});
255255

256-
describe('unimplemented lifecycle methods', function () {
257-
it('list throws (Phase 3)', async function () {
258-
await safes.list().should.be.rejectedWith(/not yet implemented .*Phase 3/);
256+
describe('get', function () {
257+
it('GETs the safe URL and returns a Safe', async function () {
258+
mockBitGo.get = sinon.stub().returns({ result: sinon.stub().resolves(safeDataWire) });
259+
260+
const result = await safes.get({ id: 'test-safe-id' });
261+
262+
result.should.be.instanceof(Safe);
263+
result.id().should.equal('test-safe-id');
264+
sinon.assert.calledWith(mockBitGo.get, '/enterprise/test-enterprise-id/safes/test-safe-id');
259265
});
266+
});
267+
268+
describe('list', function () {
269+
it('GETs the safes collection and maps the response to Safes', async function () {
270+
const query = sinon.stub().returnsThis();
271+
const result = sinon.stub().resolves({ safes: [safeDataWire], nextBatchPrevId: 'next-page-id' });
272+
mockBitGo.get = sinon.stub().returns({ query, result });
273+
274+
const page = await safes.list();
275+
276+
page.safes.should.have.length(1);
277+
page.safes[0].should.be.instanceof(Safe);
278+
page.safes[0].id().should.equal('test-safe-id');
279+
page.should.have.property('nextCursor', 'next-page-id');
280+
sinon.assert.calledWith(mockBitGo.get, '/enterprise/test-enterprise-id/safes');
281+
// no cursor/limit passed → empty query
282+
sinon.assert.calledWith(query, {});
283+
});
284+
285+
it('maps cursor→prevId and limit onto the query', async function () {
286+
const query = sinon.stub().returnsThis();
287+
const result = sinon.stub().resolves({ safes: [] });
288+
mockBitGo.get = sinon.stub().returns({ query, result });
289+
290+
const page = await safes.list({ cursor: 'prev-page-id', limit: 50 });
260291

261-
it('get throws (Phase 3)', async function () {
262-
await safes.get({ id: 'vid' }).should.be.rejectedWith(/not yet implemented .*Phase 3/);
292+
page.safes.should.have.length(0);
293+
// absent nextBatchPrevId → undefined nextCursor (last page)
294+
(page.nextCursor === undefined).should.be.true();
295+
sinon.assert.calledWith(query, { limit: 50, prevId: 'prev-page-id' });
263296
});
264297
});
265298

0 commit comments

Comments
 (0)