diff --git a/modules/sdk-core/src/bitgo/safe/safes.ts b/modules/sdk-core/src/bitgo/safe/safes.ts index 32c436592a..c332bbc2fe 100644 --- a/modules/sdk-core/src/bitgo/safe/safes.ts +++ b/modules/sdk-core/src/bitgo/safe/safes.ts @@ -4,6 +4,7 @@ * @experimental The safe client surface is experimental and may change (including breaking * changes) before the public release. */ +import * as t from 'io-ts'; import { FinalizeSafeBody, InitializeSafeBody, RootKeyTriplet, RootKeyType, SafeData } from '@bitgo/public-types'; import { Environments } from '../../common'; import { IBaseCoin } from '../baseCoin'; @@ -40,6 +41,17 @@ const ROOT_COIN_BY_NETWORK: Record<'mainnet' | 'testnet', Record { - throw new Error('Safes.list is not yet implemented (WCN-1192 Phase 3)'); + // SDK exposes an opaque cursor; WP speaks prevId/nextBatchPrevId (v2 list convention). + const query: { limit?: number; prevId?: string } = {}; + if (params.limit !== undefined) { + query.limit = params.limit; + } + if (params.cursor !== undefined) { + query.prevId = params.cursor; + } + const response = await this.bitgo.get(this.url()).query(query).result(); + const { safes, nextBatchPrevId } = decodeWithCodec(ListSafesResponse, response, 'ListSafesResponse'); + return { + safes: safes.map((safeData) => new Safe(this.bitgo, safeData)), + nextCursor: nextBatchPrevId, + }; } /** - * Fetch a single safe by id. - * Implemented in WCN-1192 Phase 3 (blocked on WCN-1177). + * Fetch a single safe by id. Non-members get a 404 (existence is not leaked). + * GET /api/v2/enterprise/:eId/safes/:safeId * @experimental */ async get(params: GetSafeOptions): Promise { - throw new Error('Safes.get is not yet implemented (WCN-1192 Phase 3)'); + const response = await this.bitgo.get(this.url(`/${params.id}`)).result(); + const safeData = decodeWithCodec(SafeData, response, 'SafeData'); + return new Safe(this.bitgo, safeData); } } diff --git a/modules/sdk-core/test/unit/bitgo/safe/safes.ts b/modules/sdk-core/test/unit/bitgo/safe/safes.ts index 73d5eca6a9..0a8a7e54a2 100644 --- a/modules/sdk-core/test/unit/bitgo/safe/safes.ts +++ b/modules/sdk-core/test/unit/bitgo/safe/safes.ts @@ -253,13 +253,46 @@ describe('Safes', function () { }); }); - describe('unimplemented lifecycle methods', function () { - it('list throws (Phase 3)', async function () { - await safes.list().should.be.rejectedWith(/not yet implemented .*Phase 3/); + describe('get', function () { + it('GETs the safe URL and returns a Safe', async function () { + mockBitGo.get = sinon.stub().returns({ result: sinon.stub().resolves(safeDataWire) }); + + const result = await safes.get({ id: 'test-safe-id' }); + + result.should.be.instanceof(Safe); + result.id().should.equal('test-safe-id'); + sinon.assert.calledWith(mockBitGo.get, '/enterprise/test-enterprise-id/safes/test-safe-id'); }); + }); + + describe('list', function () { + it('GETs the safes collection and maps the response to Safes', async function () { + const query = sinon.stub().returnsThis(); + const result = sinon.stub().resolves({ safes: [safeDataWire], nextBatchPrevId: 'next-page-id' }); + mockBitGo.get = sinon.stub().returns({ query, result }); + + const page = await safes.list(); + + page.safes.should.have.length(1); + page.safes[0].should.be.instanceof(Safe); + page.safes[0].id().should.equal('test-safe-id'); + page.should.have.property('nextCursor', 'next-page-id'); + sinon.assert.calledWith(mockBitGo.get, '/enterprise/test-enterprise-id/safes'); + // no cursor/limit passed → empty query + sinon.assert.calledWith(query, {}); + }); + + it('maps cursor→prevId and limit onto the query', async function () { + const query = sinon.stub().returnsThis(); + const result = sinon.stub().resolves({ safes: [] }); + mockBitGo.get = sinon.stub().returns({ query, result }); + + const page = await safes.list({ cursor: 'prev-page-id', limit: 50 }); - it('get throws (Phase 3)', async function () { - await safes.get({ id: 'vid' }).should.be.rejectedWith(/not yet implemented .*Phase 3/); + page.safes.should.have.length(0); + // absent nextBatchPrevId → undefined nextCursor (last page) + (page.nextCursor === undefined).should.be.true(); + sinon.assert.calledWith(query, { limit: 50, prevId: 'prev-page-id' }); }); });