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
48 changes: 29 additions & 19 deletions masterBitgoExpress.json
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,16 @@
}
}
},
"202": {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious, what is this file for?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenAPI Spec!

"description": "Accepted",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AsyncJobResponseCodec"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
Expand Down Expand Up @@ -1939,6 +1949,25 @@
"txid"
]
},
"AsyncJobResponseCodec": {
"title": "AsyncJobResponseCodec",
"type": "object",
"properties": {
"jobId": {
"type": "string"
},
"status": {
"type": "string",
"enum": [
"pending"
]
}
},
"required": [
"jobId",
"status"
]
},
"AccelerateResponseCodec": {
"title": "AccelerateResponseCodec",
"type": "object",
Expand Down Expand Up @@ -1968,25 +1997,6 @@
"txHex"
]
},
"AsyncJobResponseCodec": {
"title": "AsyncJobResponseCodec",
"type": "object",
"properties": {
"jobId": {
"type": "string"
},
"status": {
"type": "string",
"enum": [
"pending"
]
}
},
"required": [
"jobId",
"status"
]
},
"GenerateWalletResponseCodec": {
"title": "GenerateWalletResponseCodec",
"type": "object",
Expand Down
161 changes: 141 additions & 20 deletions src/__tests__/api/master/accelerate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ import nock from 'nock';
import * as utxolib from '@bitgo-beta/utxo-lib';
import { Tbtc } from '@bitgo-beta/sdk-coin-btc';
import { app as expressApp } from '../../../masterBitGoExpressApp';
import { AppMode, MasterExpressConfig, TlsMode } from '../../../shared/types';
import { Environments, Wallet } from '@bitgo-beta/sdk-core';
import { BitGoAPITestHarness, DEFAULT_ASYNC_MODE_CONFIG } from './testUtils';
import {
BitGoAPITestHarness,
makeMasterExpressTestConfig,
nockAsyncMultisigSignJob,
} from './testUtils';
import assert from 'assert';

const TBTC_PREBUILD_PSBT_HEX = utxolib.bitgo
.createPsbtForNetwork({ network: utxolib.networks.testnet })
Expand Down Expand Up @@ -51,24 +55,8 @@ describe('POST /api/v1/:coin/advancedwallet/:walletId/accelerate', () => {
nock.disableNetConnect();
nock.enableNetConnect('127.0.0.1');

const config: MasterExpressConfig = {
appMode: AppMode.MASTER_EXPRESS,
port: 0, // Let OS assign a free port
bind: 'localhost',
timeout: 30000,
httpLoggerFile: '',
env: 'test',
disableEnvCheck: true,
authVersion: 2,
advancedWalletManagerUrl: advancedWalletManagerUrl,
awmServerCaCert: 'test-cert',
tlsMode: TlsMode.DISABLED,
clientCertAllowSelfSigned: true,
asyncModeConfig: DEFAULT_ASYNC_MODE_CONFIG,
};

const app = expressApp(config);
agent = request.agent(app);
const config = makeMasterExpressTestConfig(advancedWalletManagerUrl);
agent = request.agent(expressApp(config));
});

afterEach(() => {
Expand Down Expand Up @@ -578,4 +566,137 @@ describe('POST /api/v1/:coin/advancedwallet/:walletId/accelerate', () => {
awmSignNock.done();
capturedSignBody.should.not.have.property('walletPubs');
});

it('should return 202 with jobId when async mode is enabled for onchain multisig accelerate', async () => {
const jobId = 'test-accelerate-job-id';
const cpfpTxIds = ['b8a828b98dbf32d9fd1875cbace9640ceb8c82626716b4a64203fdc79bb46d26'];
const asyncConfig = makeMasterExpressTestConfig(advancedWalletManagerUrl, {
asyncEnabled: true,
});
const asyncAgent = request.agent(expressApp(asyncConfig));

nockWalletAndKeychains();

let capturedBuildBody: Record<string, unknown> | undefined;
nock(bitgoApiUrl)
.post(`/api/v2/${coin}/wallet/${walletId}/tx/build`, (body) => {
capturedBuildBody = body;
return true;
})
.reply(200, {
txHex: TBTC_PREBUILD_PSBT_HEX,
txInfo: { nP2SHInputs: 1, nSegwitInputs: 0, nOutputs: 2 },
});
nock(bitgoApiUrl).get(`/api/v2/${coin}/public/block/latest`).reply(200, { height: 800000 });
sinon.stub(Tbtc.prototype, 'verifyTransaction').resolves(true);

let capturedJobBody: Record<string, unknown> | undefined;
const { bridgeNock, awmSignNock } = nockAsyncMultisigSignJob({
coin,
advancedWalletManagerUrl,
jobId,
captureJobBody: (body) => {
capturedJobBody = body;
},
});

const response = await asyncAgent
.post(`/api/v1/${coin}/advancedwallet/${walletId}/accelerate`)
.set('Authorization', `Bearer ${accessToken}`)
.send({
pubkey: mockUserKeychain.pub,
source: 'user' as const,
cpfpTxIds,
cpfpFeeRate: 50,
maxFee: 10000,
});

response.status.should.equal(202);
response.body.should.have.property('jobId', jobId);
response.body.should.have.property('status', 'pending');

assert(capturedBuildBody, 'capturedBuildBody is undefined');
capturedBuildBody.should.have.property('cpfpTxIds').which.deepEqual(cpfpTxIds);
capturedBuildBody.should.have.property('recipients').which.deepEqual([]);

assert(capturedJobBody, 'capturedJobBody is undefined');
capturedJobBody.should.have.property('wpSubmitKind', 'accelerate');
(capturedJobBody.wpSubmitParams as Record<string, unknown>).should.have
.property('cpfpTxIds')
.which.deepEqual(cpfpTxIds);

bridgeNock.done();
awmSignNock.isDone().should.be.false();
});

it('should fail when async mode is enabled for TSS accelerate', async () => {
const tssCoin = 'tsol';
const asyncConfig = makeMasterExpressTestConfig(advancedWalletManagerUrl, {
asyncEnabled: true,
});
const asyncAgent = request.agent(expressApp(asyncConfig));

nock(bitgoApiUrl)
.get(`/api/v2/${tssCoin}/wallet/${walletId}`)
.matchHeader('authorization', `Bearer ${accessToken}`)
.reply(200, {
id: walletId,
type: 'advanced',
keys: ['user-key-id', 'backup-key-id', 'bitgo-key-id'],
multisigType: 'tss',
});

nock(bitgoApiUrl)
.get(`/api/v2/${tssCoin}/key/user-key-id`)
.matchHeader('authorization', `Bearer ${accessToken}`)
.reply(200, mockUserKeychain);

const response = await asyncAgent
.post(`/api/v1/${tssCoin}/advancedwallet/${walletId}/accelerate`)
.set('Authorization', `Bearer ${accessToken}`)
.send({
pubkey: mockUserKeychain.pub,
source: 'user',
cpfpTxIds: ['b8a828b98dbf32d9fd1875cbace9640ceb8c82626716b4a64203fdc79bb46d26'],
cpfpFeeRate: 50,
});

response.status.should.equal(400);
response.body.details.should.containEql('Async mode is not yet supported for TSS accelerate');
});

it('should fail when async transaction verification returns false', async () => {
const asyncConfig = makeMasterExpressTestConfig(advancedWalletManagerUrl, {
asyncEnabled: true,
});
const asyncAgent = request.agent(expressApp(asyncConfig));

nockWalletAndKeychains();

const buildNock = nock(bitgoApiUrl)
.post(`/api/v2/${coin}/wallet/${walletId}/tx/build`)
.reply(200, {
txHex: TBTC_PREBUILD_PSBT_HEX,
txInfo: { nP2SHInputs: 1, nSegwitInputs: 0, nOutputs: 2 },
});
nock(bitgoApiUrl).get(`/api/v2/${coin}/public/block/latest`).reply(200, { height: 800000 });

const verifyStub = sinon.stub(Tbtc.prototype, 'verifyTransaction').resolves(false);

const response = await asyncAgent
.post(`/api/v1/${coin}/advancedwallet/${walletId}/accelerate`)
.set('Authorization', `Bearer ${accessToken}`)
.send({
pubkey: mockUserKeychain.pub,
source: 'user',
cpfpTxIds: ['b8a828b98dbf32d9fd1875cbace9640ceb8c82626716b4a64203fdc79bb46d26'],
cpfpFeeRate: 50,
});

response.status.should.equal(400);
response.body.details.should.containEql('Transaction prebuild failed local validation');

buildNock.done();
sinon.assert.calledOnce(verifyStub);
});
});
51 changes: 51 additions & 0 deletions src/__tests__/api/master/asyncJobWorker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,40 @@ function nockTxSend(walletId: string, txid: string) {
.reply(200, { txid, status: 'signed' });
}

function makeAccelerateSignJob(overrides: Partial<BridgeJobResponse> = {}): BridgeJobResponse {
const cpfpTxId = 'b8a828b98dbf32d9fd1875cbace9640ceb8c82626716b4a64203fdc79bb46d26';
return makeSignJob({
request: {
endpoint: `/api/${COIN}/multisig/sign`,
method: 'POST',
body: {
source: 'user',
pub: 'xpub_user',
txPrebuild: { txHex: '70736274ff' },
walletId: 'test-wallet-id',
wpSubmitKind: 'accelerate',
wpSubmitParams: {
cpfpTxIds: [cpfpTxId],
cpfpFeeRate: 50,
recipients: [],
},
},
},
...overrides,
});
}

function nockAccelerateTxSend(walletId: string, txid: string, cpfpTxId: string) {
return nock(BITGO_API_URL)
.post(`/api/v2/${COIN}/wallet/${walletId}/tx/send`, (body) => {
body.should.have.property('cpfpTxIds').which.deepEqual([cpfpTxId]);
body.should.have.property('txHex', 'signed-tx-hex');
return true;
})
.matchHeader('any', () => true)
.reply(200, { txid, status: 'signed' });
}

function nockUpdateSignJobComplete(jobId: string, txid: string) {
return nock(BRIDGE_URL)
.patch(`/job/${jobId}`, (body) => body.status === 'complete' && body.result?.txid === txid)
Expand Down Expand Up @@ -489,6 +523,23 @@ describe('asyncJobWorker', () => {
updateNock.done();
});

it('submits signed accelerate tx to WP with cpfp params and PATCHes job complete', async () => {
const job = makeAccelerateSignJob();
const walletId = 'test-wallet-id';
const txid = 'accelerated-tx-id';
const cpfpTxId = 'b8a828b98dbf32d9fd1875cbace9640ceb8c82626716b4a64203fdc79bb46d26';

const walletGetNock = nockWalletGet(walletId);
const sendNock = nockAccelerateTxSend(walletId, txid, cpfpTxId);
const updateNock = nockUpdateSignJobComplete(job.jobId, txid);

await handleMultisigSignOperation(job, bridge, bitgo);

walletGetNock.done();
sendNock.done();
updateNock.done();
});

it('throws when awmResponse is missing', async () => {
const job = makeSignJob({ awmResponse: undefined });

Expand Down
16 changes: 14 additions & 2 deletions src/__tests__/api/master/multisigSignUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,18 @@ describe('multisigSignUtils', () => {
});
});

it('parses accelerate wpSubmitKind', () => {
parseMultisigSignJobContext({
walletId: 'test-wallet-id',
wpSubmitKind: 'accelerate',
wpSubmitParams: { cpfpTxIds: ['tx-id'], cpfpFeeRate: 50 },
}).should.eql({
walletId: 'test-wallet-id',
wpSubmitKind: 'accelerate',
wpSubmitParams: { cpfpTxIds: ['tx-id'], cpfpFeeRate: 50 },
});
});

it('throws when wpSubmitKind is missing or unsupported', () => {
(() =>
parseMultisigSignJobContext({
Expand All @@ -208,9 +220,9 @@ describe('multisigSignUtils', () => {
(() =>
parseMultisigSignJobContext({
walletId: 'test-wallet-id',
wpSubmitKind: 'accelerate',
wpSubmitKind: 'consolidate',
wpSubmitParams: { recipients: [] },
})).should.throw(/unsupported wpSubmitKind: accelerate/);
})).should.throw(/unsupported wpSubmitKind: consolidate/);
});
});
});
Loading
Loading