Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 36 additions & 6 deletions modules/sdk-core/src/bitgo/safe/safes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -40,6 +41,17 @@ const ROOT_COIN_BY_NETWORK: Record<'mainnet' | 'testnet', Record<RootKeyType, st
},
};

/**
* Wire shape of the paginated `GET /enterprise/:eId/safes` response. WP paginates with the v2
* `prevId`/`nextBatchPrevId` convention; the SDK re-exposes it as an opaque `cursor`/`nextCursor`
* (see `list`).
* @experimental
*/
const ListSafesResponse = t.intersection([
t.type({ safes: t.array(SafeData) }),
t.partial({ nextBatchPrevId: t.string }),
]);

/**
* Collection accessor for a single enterprise's safes, mirroring Wallets / Enterprises.
* Safe routes are enterprise-scoped (/api/v2/enterprise/:eId/safes), so the accessor is
Expand Down Expand Up @@ -248,20 +260,38 @@ export class Safes implements ISafes {
}

/**
* List the enterprise's safes (cursor pagination).
* Implemented in WCN-1192 Phase 3 (blocked on WCN-1177).
* List the enterprise's safes the caller is a member of (cursor pagination).
* GET /api/v2/enterprise/:eId/safes?limit&prevId
*
* `cursor` is the opaque `nextCursor` returned by a previous call; page forward until
* `nextCursor` is absent.
* @experimental
*/
async list(params: ListSafesOptions = {}): Promise<{ safes: Safe[]; nextCursor?: string }> {
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<Safe> {
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);
}
}
43 changes: 38 additions & 5 deletions modules/sdk-core/test/unit/bitgo/safe/safes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
});
});

Expand Down
Loading