Skip to content

Commit 09383ed

Browse files
author
Support Bot
committed
feat(express): add cancel wallet share express route
Add a typed DELETE /api/v2/{coin}/walletshare/{id} route to the BitGo Express server so callers can cancel outgoing wallet share requests. The SDK's Wallets.cancelShare() already issued the correct HTTP DELETE to the BitGo API, but BitGo Express had no handler for this endpoint. The accept-share endpoint had a similar gap that was addressed with app.post(); this change adds a first-class typed route following the same pattern as shareWallet. Changes: - modules/express/src/typedRoutes/api/v2/cancelWalletShare.ts: new typed route definition (params, response codecs, httpRoute export) - modules/express/src/typedRoutes/api/index.ts: register DeleteCancelWalletShare under 'express.wallet.cancelShare' inside ExpressWalletManagementApiSpec - modules/express/src/clientRoutes.ts: export handleV2CancelWalletShare handler and register it with router.delete - modules/express/test/unit/clientRoutes/cancelWalletShare.ts: unit tests for the new handler - modules/bitgo/test/v2/unit/wallets.ts: unit tests for Wallets.cancelShare() (DELETE /walletshare/:id) Ticket: WCI-1164 Session-Id: 39708184-a685-4405-b0cf-0c296a1bf0d3 Task-Id: 7c07317a-7a0a-4ead-8e23-6f96f60d1755
1 parent ebb4eb3 commit 09383ed

5 files changed

Lines changed: 180 additions & 0 deletions

File tree

modules/bitgo/test/v2/unit/wallets.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4440,6 +4440,34 @@ describe('V2 Wallets:', function () {
44404440
});
44414441
});
44424442

4443+
describe('cancelShare', function () {
4444+
it('should send DELETE to /walletshare/:id and return the result', async function () {
4445+
const bitgo = TestBitGo.decorate(BitGo, { env: 'mock' });
4446+
bitgo.initializeTestVars();
4447+
const basecoin = bitgo.coin('tbtc');
4448+
const wallets = basecoin.wallets();
4449+
const bgUrl = common.Environments[bitgo.getEnv()].uri;
4450+
const shareId = 'abc123shareId';
4451+
4452+
nock(bgUrl)
4453+
.delete(`/api/v2/tbtc/walletshare/${shareId}`)
4454+
.reply(200, { changed: true, state: 'canceled' });
4455+
4456+
const result = await wallets.cancelShare({ walletShareId: shareId });
4457+
result.should.have.property('changed', true);
4458+
result.should.have.property('state', 'canceled');
4459+
});
4460+
4461+
it('should throw if walletShareId is missing', async function () {
4462+
const bitgo = TestBitGo.decorate(BitGo, { env: 'mock' });
4463+
bitgo.initializeTestVars();
4464+
const basecoin = bitgo.coin('tbtc');
4465+
const wallets = basecoin.wallets();
4466+
4467+
await wallets.cancelShare({}).should.be.rejectedWith('walletShareId must be a string');
4468+
});
4469+
});
4470+
44434471
describe('List Wallets:', function () {
44444472
it('should list wallets with skipReceiveAddress = true', async function () {
44454473
const bitgo = TestBitGo.decorate(BitGo, { env: 'mock' });

modules/express/src/clientRoutes.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -862,6 +862,15 @@ async function handleV2AcceptWalletShare(req: express.Request) {
862862
return coin.wallets().acceptShare(params);
863863
}
864864

865+
/**
866+
* handle cancel wallet share
867+
*/
868+
export async function handleV2CancelWalletShare(req: ExpressApiRouteRequest<'express.wallet.cancelShare', 'delete'>) {
869+
const bitgo = req.bitgo;
870+
const coin = bitgo.coin(req.decoded.coin);
871+
return coin.wallets().cancelShare({ walletShareId: req.decoded.id });
872+
}
873+
865874
/**
866875
* handle wallet sign transaction
867876
*/
@@ -2060,6 +2069,7 @@ export function setupAPIRoutes(app: express.Application, config: Config): void {
20602069
router.post('express.v2.address.derive', [prepareBitGo(config), typedPromiseWrapper(handleV2DeriveAddress)]);
20612070

20622071
router.post('express.wallet.share', [prepareBitGo(config), typedPromiseWrapper(handleV2ShareWallet)]);
2072+
router.delete('express.wallet.cancelShare', [prepareBitGo(config), typedPromiseWrapper(handleV2CancelWalletShare)]);
20632073
app.post(
20642074
'/api/v2/:coin/walletshare/:id/acceptshare',
20652075
parseBody,

modules/express/src/typedRoutes/api/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { PostCoinSignTx } from './v2/coinSignTx';
4040
import { PostWalletSignTx } from './v2/walletSignTx';
4141
import { PostWalletTxSignTSS } from './v2/walletTxSignTSS';
4242
import { PostShareWallet } from './v2/shareWallet';
43+
import { DeleteCancelWalletShare } from './v2/cancelWalletShare';
4344
import { PutExpressWalletUpdate } from './v2/expressWalletUpdate';
4445
import { PostFanoutUnspents } from './v2/fanoutUnspents';
4546
import { PostSendMany } from './v2/sendmany';
@@ -345,6 +346,9 @@ export const ExpressWalletManagementApiSpec = apiSpec({
345346
'express.wallet.share': {
346347
post: PostShareWallet,
347348
},
349+
'express.wallet.cancelShare': {
350+
delete: DeleteCancelWalletShare,
351+
},
348352
'express.wallet.update': {
349353
put: PutExpressWalletUpdate,
350354
},
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import * as t from 'io-ts';
2+
import { httpRoute, httpRequest } from '@api-ts/io-ts-http';
3+
import { BitgoExpressError } from '../../schemas/error';
4+
import { ShareState } from '../../schemas/wallet';
5+
6+
/**
7+
* Path parameters for canceling a wallet share
8+
*/
9+
export const CancelWalletShareParams = {
10+
/** A cryptocurrency or token ticker symbol. */
11+
coin: t.string,
12+
/** The wallet share ID to cancel. */
13+
id: t.string,
14+
} as const;
15+
16+
/**
17+
* Response for canceling a wallet share
18+
*/
19+
export const CancelWalletShareResponse200 = t.type({
20+
/** Whether the share state was changed by this operation. */
21+
changed: t.boolean,
22+
/** Current state of the wallet share after the operation. */
23+
state: ShareState,
24+
});
25+
26+
export const CancelWalletShareResponse = {
27+
200: CancelWalletShareResponse200,
28+
400: BitgoExpressError,
29+
} as const;
30+
31+
/**
32+
* Cancel a pending wallet share invitation
33+
*
34+
* Cancels an outgoing wallet share that has not yet been accepted.
35+
* Only the user who created the share can cancel it.
36+
*
37+
* @operationId express.wallet.cancelShare
38+
* @tag Express
39+
*/
40+
export const DeleteCancelWalletShare = httpRoute({
41+
path: '/api/v2/{coin}/walletshare/{id}',
42+
method: 'DELETE',
43+
request: httpRequest({
44+
params: CancelWalletShareParams,
45+
}),
46+
response: CancelWalletShareResponse,
47+
});
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import * as sinon from 'sinon';
2+
import 'should-http';
3+
import 'should-sinon';
4+
import '../../lib/asserts';
5+
import nock from 'nock';
6+
import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test';
7+
import { BitGo } from 'bitgo';
8+
import { BaseCoin, Wallets, decodeOrElse, common } from '@bitgo/sdk-core';
9+
import { ExpressApiRouteRequest } from '../../../src/typedRoutes/api';
10+
import { handleV2CancelWalletShare } from '../../../src/clientRoutes';
11+
import { CancelWalletShareResponse } from '../../../src/typedRoutes/api/v2/cancelWalletShare';
12+
13+
describe('Cancel Wallet Share (typed handler)', () => {
14+
let bitgo: TestBitGoAPI;
15+
16+
before(async function () {
17+
if (!nock.isActive()) {
18+
nock.activate();
19+
}
20+
bitgo = TestBitGo.decorate(BitGo, { env: 'test' });
21+
bitgo.initializeTestVars();
22+
nock.disableNetConnect();
23+
nock.enableNetConnect('127.0.0.1');
24+
});
25+
26+
after(() => {
27+
if (nock.isActive()) {
28+
nock.restore();
29+
}
30+
});
31+
32+
it('should call cancelShare and return a typed response', async () => {
33+
const coin = 'tbtc';
34+
const shareId = 'abc123shareId';
35+
36+
const cancelResponse = {
37+
changed: true,
38+
state: 'canceled',
39+
};
40+
41+
const cancelShareStub = sinon.stub().resolves(cancelResponse);
42+
const coinStub = sinon.createStubInstance(BaseCoin, {
43+
wallets: sinon.stub<[], Wallets>().returns({
44+
cancelShare: cancelShareStub,
45+
} as any),
46+
});
47+
48+
const stubBitgo = sinon.createStubInstance(BitGo, { coin: sinon.stub<[string]>().returns(coinStub) });
49+
50+
const req = {
51+
bitgo: stubBitgo,
52+
decoded: {
53+
coin,
54+
id: shareId,
55+
},
56+
} as unknown as ExpressApiRouteRequest<'express.wallet.cancelShare', 'delete'>;
57+
58+
const res = await handleV2CancelWalletShare(req);
59+
60+
cancelShareStub.calledOnceWith({ walletShareId: shareId }).should.be.true();
61+
decodeOrElse('CancelWalletShareResponse200', CancelWalletShareResponse[200], res, (errors) => {
62+
throw new Error(`Response did not match expected codec: ${errors}`);
63+
});
64+
});
65+
66+
it('should pass the walletShareId from the route param to cancelShare', async () => {
67+
const coin = 'tbtc';
68+
const shareId = 'someOtherShareId';
69+
70+
const cancelShareStub = sinon.stub().resolves({ changed: false, state: 'canceled' });
71+
const coinStub = sinon.createStubInstance(BaseCoin, {
72+
wallets: sinon.stub<[], Wallets>().returns({
73+
cancelShare: cancelShareStub,
74+
} as any),
75+
});
76+
77+
const stubBitgo = sinon.createStubInstance(BitGo, { coin: sinon.stub<[string]>().returns(coinStub) });
78+
79+
const req = {
80+
bitgo: stubBitgo,
81+
decoded: {
82+
coin,
83+
id: shareId,
84+
},
85+
} as unknown as ExpressApiRouteRequest<'express.wallet.cancelShare', 'delete'>;
86+
87+
await handleV2CancelWalletShare(req);
88+
89+
cancelShareStub.calledOnceWith({ walletShareId: shareId }).should.be.true();
90+
});
91+
});

0 commit comments

Comments
 (0)