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
17 changes: 17 additions & 0 deletions modules/bitgo/test/v2/unit/pendingApproval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,23 @@ describe('Pending Approvals:', () => {
paScope.isDone().should.be.true();
});

it('should cancel a pending approval without approving or rejecting it', async () => {
const pendingApproval = new PendingApproval(bitgo, basecoin, pendingApprovalData, wallet);
const paScope = nock(bgUrl)
.put(`/api/v2/${coin}/pendingapprovals/${pendingApprovalData.id}`, {
state: 'canceled',
})
.reply(200, {
...pendingApprovalData,
state: State.CANCELED,
});

const response = await pendingApproval.cancel();

response.state.should.equal(State.CANCELED);
paScope.isDone().should.be.true();
});

function testRecreateTransaction(coinName: string, recreateTransaction: boolean, type: Type) {
it(`[${coinName}] should ${
recreateTransaction ? 'not ' : ''
Expand Down
6 changes: 6 additions & 0 deletions modules/express/src/clientRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,9 @@ function handleApproveTransaction(req: ExpressApiRouteRequest<'express.v1.pendin
if (params.state === 'approved') {
return pendingApproval.approve(params);
}
if (params.state === 'canceled') {
return pendingApproval.cancel(params);
}
return pendingApproval.reject(params);
});
}
Expand Down Expand Up @@ -833,6 +836,9 @@ async function handleV2PendingApproval(req: ExpressApiRouteRequest<'express.pend
if (params.state === 'approved') {
return pendingApproval.approve(params);
}
if (params.state === 'canceled') {
return pendingApproval.cancel(params);
}
return pendingApproval.reject(params);
}

Expand Down
8 changes: 4 additions & 4 deletions modules/express/src/typedRoutes/api/v1/pendingApproval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const pendingApprovalRequestParams = {
};

export const pendingApprovalRequestBody = {
/** State of the approval: 'approved' to approve, any other value or omit to reject (defaults to rejection) */
/** State of the approval: 'approved' to approve, 'canceled' to cancel, any other value or omit to reject */
state: optional(t.string),
/** Wallet passphrase for transaction signing (required for transactionRequest approvals unless tx or xprv provided) */
walletPassphrase: optional(t.string),
Expand All @@ -30,9 +30,9 @@ export const pendingApprovalRequestBody = {
/**
* Approve or reject a pending approval
*
* Updates the state of a pending approval to either 'approved' or 'rejected' based on the
* state parameter. If state is 'approved', the approval is approved; otherwise, it is rejected.
* Default behavior (when state is omitted) is rejection.
* Updates the state of a pending approval to 'approved', 'canceled', or 'rejected' based on
* the state parameter. If state is 'approved', the approval is approved; if state is 'canceled',
* it is canceled; otherwise, it is rejected. Default behavior is rejection.
*
* **Approval Types:**
* - **transactionRequest**: Transaction approvals (may require walletPassphrase for signing)
Expand Down
3 changes: 2 additions & 1 deletion modules/express/src/typedRoutes/api/v2/pendingApproval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const PendingApprovalParams = {
* Request body for approving or rejecting a pending approval
*/
export const PendingApprovalRequestBody = {
/** New state for the pending approval: 'approved' to approve, omit or 'rejected' to reject */
/** New state: 'approved' to approve, 'canceled' to cancel, omit or 'rejected' to reject */
state: optional(t.string),
/** Passphrase to decrypt the user key on the wallet */
walletPassphrase: optional(t.string),
Expand Down Expand Up @@ -48,6 +48,7 @@ export const PendingApprovalState = t.union([
t.literal('approved'),
t.literal('processing'),
t.literal('rejected'),
t.literal('canceled'),
]);

/**
Expand Down
33 changes: 33 additions & 0 deletions modules/express/test/unit/typedRoutes/pendingApproval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,13 @@ describe('PendingApproval codec tests', function () {
enterprise: 'enterprise123',
};

const mockCanceledResponse = {
id: 'approval123',
state: 'canceled',
wallet: 'wallet123',
enterprise: 'enterprise123',
};

afterEach(function () {
sinon.restore();
});
Expand Down Expand Up @@ -387,6 +394,32 @@ describe('PendingApproval codec tests', function () {
assert.strictEqual(result.body.enterprise, mockRejectedResponse.enterprise);
});

it('should successfully cancel pending approval', async function () {
const approvalId = '123456789abcdef';
const requestBody = { state: 'canceled' };
const cancel = sinon.stub().resolves(mockCanceledResponse);

const mockPendingApprovalObject = {
approve: sinon.stub().resolves(mockApprovedResponse),
reject: sinon.stub().resolves(mockRejectedResponse),
cancel,
};

sinon.stub(BitGo.prototype, 'pendingApprovals').returns({
get: sinon.stub().resolves(mockPendingApprovalObject),
} as any);

const result = await agent
.put(`/api/v1/pendingapprovals/${approvalId}/express`)
.set('Authorization', 'Bearer test_access_token_12345')
.set('Content-Type', 'application/json')
.send(requestBody);

assert.strictEqual(result.status, 200);
assert.strictEqual(result.body.state, 'canceled');
sinon.assert.calledOnce(cancel);
});

it('should successfully approve with otp', async function () {
const approvalId = '123456789abcdef';
const requestBody = {
Expand Down
38 changes: 38 additions & 0 deletions modules/express/test/unit/typedRoutes/pendingApprovalV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,14 @@ describe('V2 PendingApproval API Tests', function () {
wallet: 'wallet123',
};

const mockCanceledResponse = {
id: 'approval123',
state: 'canceled',
creator: 'user123',
info: { type: 'transactionRequest' },
wallet: 'wallet123',
};

afterEach(function () {
sinon.restore();
});
Expand Down Expand Up @@ -389,6 +397,36 @@ describe('V2 PendingApproval API Tests', function () {
sinon.assert.calledOnce(mockPendingApprovalObject.reject);
});

it('should successfully cancel pending approval', async function () {
const coin = 'tbtc';
const approvalId = '123456789abcdef';
const cancel = sinon.stub().resolves(mockCanceledResponse);

const mockPendingApprovalObject = {
approve: sinon.stub().resolves(mockApprovedResponse),
reject: sinon.stub().resolves(mockRejectedResponse),
cancel,
};

const mockCoin = {
pendingApprovals: sinon.stub().returns({
get: sinon.stub().resolves(mockPendingApprovalObject),
}),
};

sinon.stub(BitGo.prototype, 'coin').returns(mockCoin as any);

const result = await agent
.put(`/api/v2/${coin}/pendingapprovals/${approvalId}`)
.set('Authorization', 'Bearer test_access_token_12345')
.set('Content-Type', 'application/json')
.send({ state: 'canceled' });

assert.strictEqual(result.status, 200);
assert.strictEqual(result.body.state, 'canceled');
sinon.assert.calledOnce(cancel);
});

it('should default to rejection with empty body', async function () {
const coin = 'tbtc';
const approvalId = '123456789abcdef';
Expand Down
6 changes: 4 additions & 2 deletions modules/sdk-api/src/v1/pendingapproval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,10 +359,12 @@ PendingApproval.prototype.reject = function (params, callback) {

//
// cancel
// rejects the pending approval
// cancels the pending approval
//
PendingApproval.prototype.cancel = function (params, callback) {
return this.reject(params, callback);
return Promise.resolve(this.bitgo.put(this.url()).send({ state: 'canceled' }).result())
.then(callback)
.catch(callback);
};

export = PendingApproval;
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export enum State {
APPROVED = 'approved',
PROCESSING = 'processing',
REJECTED = 'rejected',
CANCELED = 'canceled',
}

export enum Type {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,13 +225,11 @@ export class PendingApproval implements IPendingApproval {
}

/**
* Alias for PendingApproval.reject()
*
* @deprecated
* Sets this PendingApproval to a canceled state.
* @param params
*/
async cancel(params: Record<string, never> = {}): Promise<any> {
return await this.reject(params);
return await this.bitgo.put(this.url()).send({ state: 'canceled' }).result();
}

/**
Expand Down
Loading