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
145 changes: 145 additions & 0 deletions src/__tests__/integration/accelerate.integ.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import 'should';
import { startServices, IntegServices } from './helpers/setup';
import { LOCALHOST } from './helpers/servers';
import { SigningMode } from '../../shared/types';

/**
* Deterministic test keypair derived from Buffer.alloc(64, 0x42) — a public, reproducible seed.
* Not a secret. Never funded. Matches getKeychain.user.json and prebuildTx.tbtc.json.
*/
const USER_XPUB =
'xpub661MyMwAqRbcEvJQx6spkkHLRgtjxmVdyDSvbDt2m9NFpbkHdcu5WJsHHHqFxNATbNHnhMWJiwckoMqF75EpcNhU9xeVM4oDS7urM3os4BH';
const USER_XPRV =
'xprv9s21ZrQH143K2SDwr5LpPcLbsf4FZJmnbzXKnqURCoqGwoR965apxWYoS2DKu2ivcMTB9uTK6XhZDEPfTeNXGf7mmACuMN6cFS5ttmrpZ3i';

const WALLET_ID = 'test-wallet-id';
const CPFP_TX_ID = 'b8a828b98dbf32d9fd1875cbace9640ceb8c82626716b4a64203fdc79bb46d26';

const accelerateRequestBody = {
pubkey: USER_XPUB,
source: 'user' as const,
cpfpTxIds: [CPFP_TX_ID],
cpfpFeeRate: 50,
maxFee: 10000,
};

describe('Accelerate: EXTERNAL signing', () => {
let services: IntegServices;

before(async () => {
services = await startServices({ signingMode: SigningMode.EXTERNAL });
});

after(async () => {
await services.teardown();
});

beforeEach(() => {
services.keyProvider.calls.length = 0;
services.bitgo.calls.length = 0;
});

it('accelerates a tbtc transaction via CPFP using external key provider', async () => {
const res = await fetch(
`http://${LOCALHOST}:${services.mbePort}/api/v1/tbtc/advancedwallet/${WALLET_ID}/accelerate`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer test-token' },
body: JSON.stringify(accelerateRequestBody),
},
);

res.status.should.equal(200);
const body = (await res.json()) as { txid: string; tx: string; status: string };
body.should.have.property('txid', 'test-tx-id');
body.should.have.property('tx', '01000000000101030a0000');
body.should.have.property('status', 'signed');

/**
* In external mode, AWM delegates signing to the key provider.
* POST /sign must be called — not POST /key (no local key retrieval for signing).
*/
services.keyProvider.calls.filter((c) => c.path === '/sign').should.have.length(1);
services.keyProvider.calls.filter((c) => c.path === '/key').should.have.length(0);

/** BitGo must receive tx/build with the correct cpfpTxIds, block/latest, and tx/send */
const buildCalls = services.bitgo.calls.filter((c) => c.path.endsWith('/tx/build'));
buildCalls.should.have.length(1);
const buildBody = buildCalls[0].body as { cpfpTxIds?: string[] };
buildBody.should.have.property('cpfpTxIds').which.deepEqual([CPFP_TX_ID]);

services.bitgo.calls
.filter((c) => c.path.endsWith('/public/block/latest'))
.should.have.length(1);
services.bitgo.calls.filter((c) => c.path.endsWith('/tx/send')).should.have.length(1);
});
});

describe('Accelerate: LOCAL signing', () => {
let services: IntegServices;

before(async () => {
services = await startServices({ signingMode: SigningMode.LOCAL });

/**
* Seed the mock key provider with a known xprv so AWM can retrieve it
* via GET /key/:pub and sign the PSBT locally. The xpub must match
* getKeychain.user.json and prebuildTx.accelerate.tbtc.json.
*/
await fetch(`http://127.0.0.1:${services.keyProvider.port}/key`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
pub: USER_XPUB,
prv: USER_XPRV,
coin: 'tbtc',
source: 'user',
type: 'independent',
}),
});
});

after(async () => {
await services.teardown();
});

beforeEach(() => {
services.keyProvider.calls.length = 0;
services.bitgo.calls.length = 0;
});

it('accelerates a tbtc transaction via CPFP using locally stored xprv', async () => {
const res = await fetch(
`http://${LOCALHOST}:${services.mbePort}/api/v1/tbtc/advancedwallet/${WALLET_ID}/accelerate`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer test-token' },
body: JSON.stringify(accelerateRequestBody),
},
);

res.status.should.equal(200);
const body = (await res.json()) as { txid: string; tx: string; status: string };
body.should.have.property('txid', 'test-tx-id');
body.should.have.property('tx', '01000000000101030a0000');
body.should.have.property('status', 'signed');

/**
* In local mode, AWM retrieves the xprv via GET /key/:pub and signs internally.
* POST /sign must NOT be called — signing happens inside AWM, not in the key provider.
*/
services.keyProvider.calls.filter((c) => c.path === '/sign').should.have.length(0);
services.keyProvider.calls.filter((c) => c.path.startsWith('/key/')).length.should.be.above(0);

/** BitGo must receive tx/build with the correct cpfpTxIds, block/latest, and tx/send */
const buildCalls = services.bitgo.calls.filter((c) => c.path.endsWith('/tx/build'));
buildCalls.should.have.length(1);
const buildBody = buildCalls[0].body as { cpfpTxIds?: string[] };
buildBody.should.have.property('cpfpTxIds').which.deepEqual([CPFP_TX_ID]);

services.bitgo.calls
.filter((c) => c.path.endsWith('/public/block/latest'))
.should.have.length(1);
services.bitgo.calls.filter((c) => c.path.endsWith('/tx/send')).should.have.length(1);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"txHex": "70736274ff01000a01000000000000000000000000",
"txInfo": { "nP2SHInputs": 0, "nSegwitInputs": 0, "nOutputs": 0 }
}
23 changes: 19 additions & 4 deletions src/__tests__/integration/helpers/mockBitgoServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,22 @@ type SendManyFixtureMethod = 'getWallet' | 'prebuildTx' | 'sendTx';
type SupportedCoin = 'hteth' | 'tbtc';
type CoinToFixtures<C extends SupportedCoin> = {
[K in SendManyFixtureMethod]: `${K}.${C}`;
};
} & { acceleratePrebuildTx: `prebuildTx.accelerate.${C}` | `prebuildTx.${C}` };

/** Registry — add a new coin here to support it across all sendMany integ test routes */
const COIN_FIXTURES: { [C in SupportedCoin]: CoinToFixtures<C> } = {
hteth: { getWallet: 'getWallet.hteth', prebuildTx: 'prebuildTx.hteth', sendTx: 'sendTx.hteth' },
tbtc: { getWallet: 'getWallet.tbtc', prebuildTx: 'prebuildTx.tbtc', sendTx: 'sendTx.tbtc' },
hteth: {
getWallet: 'getWallet.hteth',
prebuildTx: 'prebuildTx.hteth',
sendTx: 'sendTx.hteth',
acceleratePrebuildTx: 'prebuildTx.hteth', // CPFP/RBF not applicable to EVM; reuses standard prebuild
},
tbtc: {
getWallet: 'getWallet.tbtc',
prebuildTx: 'prebuildTx.tbtc',
sendTx: 'sendTx.tbtc',
acceleratePrebuildTx: 'prebuildTx.accelerate.tbtc',
},
};

function coinFixtures(coin: string): CoinToFixtures<SupportedCoin> {
Expand Down Expand Up @@ -99,7 +109,12 @@ export async function startMockBitgoServer(): Promise<MockBitgoServer> {

/** Transaction prebuild — coin-specific fixture */
app.post('/api/v2/:coin/wallet/:walletId/tx/build', (req, res) => {
res.json(loadFixture(coinFixtures(req.params.coin).prebuildTx));
const { coin } = req.params;
const isAccelerate = req.body?.cpfpTxIds?.length || req.body?.rbfTxIds?.length;
const fixtureName = isAccelerate
? coinFixtures(coin).acceleratePrebuildTx
: coinFixtures(coin).prebuildTx;
res.json(loadFixture(fixtureName));
});

/** Transaction submit — coin-specific fixture */
Expand Down
Loading