Skip to content
Draft
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
28 changes: 28 additions & 0 deletions modules/bitgo/test/v2/unit/wallets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4440,6 +4440,34 @@ describe('V2 Wallets:', function () {
});
});

describe('cancelShare', function () {
it('should send DELETE to /walletshare/:id and return the result', async function () {
const bitgo = TestBitGo.decorate(BitGo, { env: 'mock' });
bitgo.initializeTestVars();
const basecoin = bitgo.coin('tbtc');
const wallets = basecoin.wallets();
const bgUrl = common.Environments[bitgo.getEnv()].uri;
const shareId = 'abc123shareId';

nock(bgUrl)
.delete(`/api/v2/tbtc/walletshare/${shareId}`)
.reply(200, { changed: true, state: 'canceled' });

const result = await wallets.cancelShare({ walletShareId: shareId });
result.should.have.property('changed', true);
result.should.have.property('state', 'canceled');
});

it('should throw if walletShareId is missing', async function () {
const bitgo = TestBitGo.decorate(BitGo, { env: 'mock' });
bitgo.initializeTestVars();
const basecoin = bitgo.coin('tbtc');
const wallets = basecoin.wallets();

await wallets.cancelShare({}).should.be.rejectedWith('walletShareId must be a string');
});
});

describe('List Wallets:', function () {
it('should list wallets with skipReceiveAddress = true', async function () {
const bitgo = TestBitGo.decorate(BitGo, { env: 'mock' });
Expand Down
10 changes: 10 additions & 0 deletions modules/express/src/clientRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -862,6 +862,15 @@ async function handleV2AcceptWalletShare(req: express.Request) {
return coin.wallets().acceptShare(params);
}

/**
* handle cancel wallet share
*/
export async function handleV2CancelWalletShare(req: ExpressApiRouteRequest<'express.wallet.cancelShare', 'delete'>) {
const bitgo = req.bitgo;
const coin = bitgo.coin(req.decoded.coin);
return coin.wallets().cancelShare({ walletShareId: req.decoded.id });
}

/**
* handle wallet sign transaction
*/
Expand Down Expand Up @@ -2060,6 +2069,7 @@ export function setupAPIRoutes(app: express.Application, config: Config): void {
router.post('express.v2.address.derive', [prepareBitGo(config), typedPromiseWrapper(handleV2DeriveAddress)]);

router.post('express.wallet.share', [prepareBitGo(config), typedPromiseWrapper(handleV2ShareWallet)]);
router.delete('express.wallet.cancelShare', [prepareBitGo(config), typedPromiseWrapper(handleV2CancelWalletShare)]);
app.post(
'/api/v2/:coin/walletshare/:id/acceptshare',
parseBody,
Expand Down
4 changes: 4 additions & 0 deletions modules/express/src/typedRoutes/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { PostCoinSignTx } from './v2/coinSignTx';
import { PostWalletSignTx } from './v2/walletSignTx';
import { PostWalletTxSignTSS } from './v2/walletTxSignTSS';
import { PostShareWallet } from './v2/shareWallet';
import { DeleteCancelWalletShare } from './v2/cancelWalletShare';
import { PutExpressWalletUpdate } from './v2/expressWalletUpdate';
import { PostFanoutUnspents } from './v2/fanoutUnspents';
import { PostSendMany } from './v2/sendmany';
Expand Down Expand Up @@ -345,6 +346,9 @@ export const ExpressWalletManagementApiSpec = apiSpec({
'express.wallet.share': {
post: PostShareWallet,
},
'express.wallet.cancelShare': {
delete: DeleteCancelWalletShare,
},
'express.wallet.update': {
put: PutExpressWalletUpdate,
},
Expand Down
47 changes: 47 additions & 0 deletions modules/express/src/typedRoutes/api/v2/cancelWalletShare.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import * as t from 'io-ts';
import { httpRoute, httpRequest } from '@api-ts/io-ts-http';
import { BitgoExpressError } from '../../schemas/error';
import { ShareState } from '../../schemas/wallet';

/**
* Path parameters for canceling a wallet share
*/
export const CancelWalletShareParams = {
/** A cryptocurrency or token ticker symbol. */
coin: t.string,
/** The wallet share ID to cancel. */
id: t.string,
} as const;

/**
* Response for canceling a wallet share
*/
export const CancelWalletShareResponse200 = t.type({
/** Whether the share state was changed by this operation. */
changed: t.boolean,
/** Current state of the wallet share after the operation. */
state: ShareState,
});

export const CancelWalletShareResponse = {
200: CancelWalletShareResponse200,
400: BitgoExpressError,
} as const;

/**
* Cancel a pending wallet share invitation
*
* Cancels an outgoing wallet share that has not yet been accepted.
* Only the user who created the share can cancel it.
*
* @operationId express.wallet.cancelShare
* @tag Express
*/
export const DeleteCancelWalletShare = httpRoute({
path: '/api/v2/{coin}/walletshare/{id}',
method: 'DELETE',
request: httpRequest({
params: CancelWalletShareParams,
}),
response: CancelWalletShareResponse,
});
91 changes: 91 additions & 0 deletions modules/express/test/unit/clientRoutes/cancelWalletShare.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import * as sinon from 'sinon';
import 'should-http';
import 'should-sinon';
import '../../lib/asserts';
import nock from 'nock';
import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test';
import { BitGo } from 'bitgo';
import { BaseCoin, Wallets, decodeOrElse, common } from '@bitgo/sdk-core';
import { ExpressApiRouteRequest } from '../../../src/typedRoutes/api';
import { handleV2CancelWalletShare } from '../../../src/clientRoutes';
import { CancelWalletShareResponse } from '../../../src/typedRoutes/api/v2/cancelWalletShare';

describe('Cancel Wallet Share (typed handler)', () => {
let bitgo: TestBitGoAPI;

before(async function () {
if (!nock.isActive()) {
nock.activate();
}
bitgo = TestBitGo.decorate(BitGo, { env: 'test' });
bitgo.initializeTestVars();
nock.disableNetConnect();
nock.enableNetConnect('127.0.0.1');
});

after(() => {
if (nock.isActive()) {
nock.restore();
}
});

it('should call cancelShare and return a typed response', async () => {
const coin = 'tbtc';
const shareId = 'abc123shareId';

const cancelResponse = {
changed: true,
state: 'canceled',
};

const cancelShareStub = sinon.stub().resolves(cancelResponse);
const coinStub = sinon.createStubInstance(BaseCoin, {
wallets: sinon.stub<[], Wallets>().returns({
cancelShare: cancelShareStub,
} as any),
});

const stubBitgo = sinon.createStubInstance(BitGo, { coin: sinon.stub<[string]>().returns(coinStub) });

const req = {
bitgo: stubBitgo,
decoded: {
coin,
id: shareId,
},
} as unknown as ExpressApiRouteRequest<'express.wallet.cancelShare', 'delete'>;

const res = await handleV2CancelWalletShare(req);

cancelShareStub.calledOnceWith({ walletShareId: shareId }).should.be.true();
decodeOrElse('CancelWalletShareResponse200', CancelWalletShareResponse[200], res, (errors) => {
throw new Error(`Response did not match expected codec: ${errors}`);
});
});

it('should pass the walletShareId from the route param to cancelShare', async () => {
const coin = 'tbtc';
const shareId = 'someOtherShareId';

const cancelShareStub = sinon.stub().resolves({ changed: false, state: 'canceled' });
const coinStub = sinon.createStubInstance(BaseCoin, {
wallets: sinon.stub<[], Wallets>().returns({
cancelShare: cancelShareStub,
} as any),
});

const stubBitgo = sinon.createStubInstance(BitGo, { coin: sinon.stub<[string]>().returns(coinStub) });

const req = {
bitgo: stubBitgo,
decoded: {
coin,
id: shareId,
},
} as unknown as ExpressApiRouteRequest<'express.wallet.cancelShare', 'delete'>;

await handleV2CancelWalletShare(req);

cancelShareStub.calledOnceWith({ walletShareId: shareId }).should.be.true();
});
});
Loading