Skip to content

Commit 2b6aa37

Browse files
refactor(abstract-utxo): reduce test boilerplate
- util/address.ts: Extract getWalletAddress helper from psbt.ts for generating addresses from RootWalletKeys, enabling dynamic address generation in tests instead of hardcoded values - util/nockBitGo.ts: Add nockWalletKeys helper to encapsulate the repeated pattern of mocking wallet key fetch endpoints, reducing 10+ lines of boilerplate per test to a single function call - util/index.ts: Export nockBitGo and address utilities so they're accessible from the central util import - postProcessPrebuild.ts: Use getUtxoCoin('tbtc') instead of manual TestBitGo setup, and generate output address dynamically instead of hardcoding - testSpoofTransaction.ts: Replace hardcoded addresses with dynamically generated ones using wallet keys vs attacker keys, making the test intent clearer. Use nockBitGo() and nockWalletKeys() to eliminate URL extraction and key mocking boilerplate Co-authored-by: Cursor <cursoragent@cursor.com> TICKET: BTC-2650
1 parent 3732a6f commit 2b6aa37

File tree

10 files changed

+90
-147
lines changed

10 files changed

+90
-147
lines changed

modules/abstract-utxo/test/unit/postProcessPrebuild.ts

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,23 @@
11
import 'should';
22

3-
import { type TestBitGoAPI, TestBitGo } from '@bitgo/sdk-test';
4-
import { BitGoAPI } from '@bitgo/sdk-api';
53
import { fixedScriptWallet } from '@bitgo/wasm-utxo';
64
import * as testutils from '@bitgo/wasm-utxo/testutils';
75

8-
import { Tbtc } from '../../src/impl/btc';
9-
10-
import { constructPsbt } from './util';
6+
import { constructPsbt, getWalletAddress, getUtxoCoin } from './util';
117

128
const { BitGoPsbt } = fixedScriptWallet;
139

1410
describe('Post Build Validation', function () {
15-
let bitgo: TestBitGoAPI;
16-
let coin: Tbtc;
17-
18-
before(function () {
19-
bitgo = TestBitGo.decorate(BitGoAPI, { env: 'test' });
20-
bitgo.safeRegister('tbtc', Tbtc.createInstance);
21-
bitgo.initializeTestVars();
22-
coin = bitgo.coin('tbtc') as Tbtc;
23-
});
11+
const coin = getUtxoCoin('tbtc');
2412

2513
it('should not modify locktime on postProcessPrebuild', async function () {
2614
const walletKeys = testutils.getDefaultWalletKeys();
15+
const walletAddress = getWalletAddress('tbtc', walletKeys);
2716

2817
// Create a PSBT with lockTime=0 and sequence=0xffffffff
2918
const psbt = constructPsbt(
3019
[{ scriptType: 'p2wsh' as const, value: BigInt(100000) }],
31-
[{ address: 'tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7', value: BigInt(90000) }],
20+
[{ address: walletAddress, value: BigInt(90000) }],
3221
'tbtc',
3322
walletKeys,
3423
{ lockTime: 0, sequence: 0xffffffff }

modules/abstract-utxo/test/unit/recovery/backupKeyRecovery.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import { BIP32, ECPair, fixedScriptWallet } from '@bitgo/wasm-utxo';
88
import { AbstractUtxoCoin, backupKeyRecoveryWithWalletUnspents } from '../../../src';
99
import type { WalletUnspent } from '../../../src/unspent';
1010
import {
11+
createWasmWalletKeys,
1112
getDefaultWasmWalletKeys,
1213
getFixture,
1314
getNormalTestnetCoin,
1415
getWalletAddress,
15-
getWalletKeys,
1616
toUnspent,
1717
utxoCoins,
1818
} from '../util';
@@ -48,7 +48,7 @@ function run(
4848
const defaultFeeRateSatB = 100;
4949

5050
describe(`Backup Key Recovery PSBT [${[coin.getChain(), ...tags].join(',')}]`, function () {
51-
const externalWallet = getWalletKeys('external');
51+
const { walletKeys: externalWallet } = createWasmWalletKeys('external');
5252
const recoveryDestination = getWalletAddress(coin.name, externalWallet);
5353
const fixtureCoin = getNormalTestnetCoin(coin);
5454
// Get xpubs from wallet keys

modules/abstract-utxo/test/unit/recovery/backupKeyRecoveryUnspentGathering.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ import {
1313
} from '../../../src';
1414
import type { Unspent } from '../../../src/unspent';
1515
import {
16+
createWasmWalletKeys,
1617
defaultBitGo,
1718
encryptKeychain,
1819
getDefaultWasmWalletKeys,
1920
getMinUtxoCoins,
2021
getWalletAddress,
21-
getWalletKeys,
2222
keychainsBase58,
2323
toUnspentWithPrevTx,
2424
WalletUnspentWithPrevTx,
@@ -79,7 +79,7 @@ describe('Backup Key Recovery - Unspent Gathering', function () {
7979

8080
getMinUtxoCoins().forEach((coin) => {
8181
describe(`Unspent Gathering [${coin.getChain()}]`, function () {
82-
const externalWallet = getWalletKeys('external');
82+
const { walletKeys: externalWallet } = createWasmWalletKeys('external');
8383
const recoveryDestination = getWalletAddress(coin.name, externalWallet);
8484

8585
before('mock', function () {

modules/abstract-utxo/test/unit/recovery/formatBackupKeyRecoveryResult.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ import {
1010
} from '../../../src';
1111
import type { WalletUnspent } from '../../../src/unspent';
1212
import {
13+
createWasmWalletKeys,
1314
getDefaultWasmWalletKeys,
1415
getFixture,
1516
getWalletAddress,
16-
getWalletKeys,
1717
shouldEqualJSON,
1818
toUnspent,
1919
utxoCoins,
@@ -52,7 +52,7 @@ function clonePsbt(psbt: fixedScriptWallet.BitGoPsbt): fixedScriptWallet.BitGoPs
5252
}
5353

5454
describe('formatBackupKeyRecoveryResult', function () {
55-
const externalWallet = getWalletKeys('external');
55+
const { walletKeys: externalWallet } = createWasmWalletKeys('external');
5656
const recoveryDestination = getWalletAddress(coin.name, externalWallet);
5757
const feeRateSatVB = 100;
5858

modules/abstract-utxo/test/unit/testSpoofTransaction.ts

Lines changed: 25 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,40 @@
11
import assert from 'assert';
22

3-
import { type TestBitGoAPI, TestBitGo } from '@bitgo/sdk-test';
4-
import { BitGoAPI, encrypt } from '@bitgo/sdk-api';
53
import * as testutils from '@bitgo/wasm-utxo/testutils';
64
import { Wallet } from '@bitgo/sdk-core';
75

8-
import { Tbtc } from '../../src/impl/btc';
9-
10-
import { constructPsbt } from './util';
6+
import { constructPsbt, getWalletAddress, getUtxoCoin, defaultBitGo, nockBitGo, nockWalletKeys } from './util';
117

128
describe('Transaction Spoofability Tests', function () {
13-
describe('Unspent management spoofability - Consolidation (BUILD_SIGN_SEND)', function () {
14-
let coin: Tbtc;
15-
let bitgoTest: TestBitGoAPI;
16-
17-
before(function () {
18-
bitgoTest = TestBitGo.decorate(BitGoAPI, { env: 'test' });
19-
bitgoTest.safeRegister('tbtc', Tbtc.createInstance);
20-
bitgoTest.initializeTestVars();
21-
coin = bitgoTest.coin('tbtc') as Tbtc;
22-
});
9+
const coin = getUtxoCoin('tbtc');
2310

24-
it('should detect hex spoofing in BUILD_SIGN_SEND', async function (): Promise<void> {
11+
describe('Unspent management spoofability - Consolidation (BUILD_SIGN_SEND)', function () {
12+
it('should detect spoofed consolidation to attacker address', async function (): Promise<void> {
2513
const keyTriple = testutils.getKeyTriple('default');
26-
const rootWalletKey = testutils.getDefaultWalletKeys();
27-
const [user] = keyTriple;
14+
const walletKeys = testutils.getDefaultWalletKeys();
15+
const attackerKeys = testutils.getWalletKeysForSeed('attacker');
2816

29-
const wallet = new Wallet(bitgoTest, coin, {
17+
const wallet = new Wallet(defaultBitGo, coin, {
3018
id: '5b34252f1bf349930e34020a',
3119
coin: 'tbtc',
3220
keys: ['user', 'backup', 'bitgo'],
3321
});
3422

35-
// originalPsbt is created to show what the legitimate transaction would look like
36-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
37-
const originalPsbt = constructPsbt(
38-
[{ scriptType: 'p2wsh' as const, value: BigInt(10000) }],
39-
[{ address: 'tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7', value: BigInt(9000) }],
40-
'tbtc',
41-
rootWalletKey
42-
);
23+
// The attacker replaces the legitimate wallet address with their own
24+
const attackerAddress = getWalletAddress('tbtc', attackerKeys);
4325
const spoofedPsbt = constructPsbt(
4426
[{ scriptType: 'p2wsh' as const, value: BigInt(10000) }],
45-
[{ address: 'tb1pjgg9ty3s2ztp60v6lhgrw76f7hxydzuk9t9mjsndh3p2gf2ah7gs4850kn', value: BigInt(9000) }],
27+
[{ address: attackerAddress, value: BigInt(9000) }],
4628
'tbtc',
47-
rootWalletKey
29+
walletKeys // Input uses wallet keys (the funds being stolen)
4830
);
4931
const spoofedHex: string = Buffer.from(spoofedPsbt.serialize()).toString('hex');
5032

51-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
52-
const bgUrl: string = (bitgoTest as any)._baseUrl;
53-
const nock = require('nock');
54-
55-
nock(bgUrl)
33+
nockBitGo()
5634
.post(`/api/v2/${wallet.coin()}/wallet/${wallet.id()}/consolidateUnspents`)
5735
.reply(200, { txHex: spoofedHex, consolidateId: 'test' });
5836

59-
nock(bgUrl)
37+
nockBitGo()
6038
.post(`/api/v2/${wallet.coin()}/wallet/${wallet.id()}/tx/send`)
6139
// eslint-disable-next-line @typescript-eslint/no-explicit-any
6240
.reply((requestBody: any) => {
@@ -66,15 +44,7 @@ describe('Transaction Spoofability Tests', function () {
6644
return [200, { txid: 'test-txid-123', status: 'signed' }];
6745
});
6846

69-
const pubs = keyTriple.map((k) => k.neutered().toBase58());
70-
const responses = [
71-
{ pub: pubs[0], encryptedPrv: encrypt('pass', user.toBase58()) },
72-
{ pub: pubs[1] },
73-
{ pub: pubs[2] },
74-
];
75-
wallet
76-
.keyIds()
77-
.forEach((id, i) => nock(bgUrl).get(`/api/v2/${wallet.coin()}/key/${id}`).reply(200, responses[i]));
47+
nockWalletKeys(wallet, keyTriple, 'pass');
7848

7949
await assert.rejects(
8050
wallet.consolidateUnspents({ walletPassphrase: 'pass' }),
@@ -87,53 +57,32 @@ describe('Transaction Spoofability Tests', function () {
8757
});
8858

8959
describe('Unspent management spoofability - Fanout (BUILD_SIGN_SEND)', function () {
90-
let coin: Tbtc;
91-
let bitgoTest: TestBitGoAPI;
92-
93-
before(function () {
94-
bitgoTest = TestBitGo.decorate(BitGoAPI, { env: 'test' });
95-
bitgoTest.safeRegister('tbtc', Tbtc.createInstance);
96-
bitgoTest.initializeTestVars();
97-
coin = bitgoTest.coin('tbtc') as Tbtc;
98-
});
99-
100-
it('should detect hex spoofing in fanout BUILD_SIGN_SEND', async function (): Promise<void> {
60+
it('should detect spoofed fanout to attacker address', async function (): Promise<void> {
10161
const keyTriple = testutils.getKeyTriple('default');
102-
const rootWalletKey = testutils.getDefaultWalletKeys();
103-
const [user] = keyTriple;
62+
const walletKeys = testutils.getDefaultWalletKeys();
63+
const attackerKeys = testutils.getWalletKeysForSeed('attacker');
10464

105-
const wallet = new Wallet(bitgoTest, coin, {
65+
const wallet = new Wallet(defaultBitGo, coin, {
10666
id: '5b34252f1bf349930e34020a',
10767
coin: 'tbtc',
10868
keys: ['user', 'backup', 'bitgo'],
10969
});
11070

111-
// originalPsbt is created to show what the legitimate transaction would look like
112-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
113-
const originalPsbt = constructPsbt(
114-
[{ scriptType: 'p2wsh' as const, value: BigInt(10000) }],
115-
[{ address: 'tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7', value: BigInt(9000) }],
116-
'tbtc',
117-
rootWalletKey
118-
);
119-
71+
// The attacker replaces the legitimate wallet address with their own
72+
const attackerAddress = getWalletAddress('tbtc', attackerKeys);
12073
const spoofedPsbt = constructPsbt(
12174
[{ scriptType: 'p2wsh' as const, value: BigInt(10000) }],
122-
[{ address: 'tb1pjgg9ty3s2ztp60v6lhgrw76f7hxydzuk9t9mjsndh3p2gf2ah7gs4850kn', value: BigInt(9000) }],
75+
[{ address: attackerAddress, value: BigInt(9000) }],
12376
'tbtc',
124-
rootWalletKey
77+
walletKeys // Input uses wallet keys (the funds being stolen)
12578
);
12679
const spoofedHex: string = Buffer.from(spoofedPsbt.serialize()).toString('hex');
12780

128-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
129-
const bgUrl: string = (bitgoTest as any)._baseUrl;
130-
const nock = require('nock');
131-
132-
nock(bgUrl)
81+
nockBitGo()
13382
.post(`/api/v2/${wallet.coin()}/wallet/${wallet.id()}/fanoutUnspents`)
13483
.reply(200, { txHex: spoofedHex, fanoutId: 'test' });
13584

136-
nock(bgUrl)
85+
nockBitGo()
13786
.post(`/api/v2/${wallet.coin()}/wallet/${wallet.id()}/tx/send`)
13887
// eslint-disable-next-line @typescript-eslint/no-explicit-any
13988
.reply((requestBody: any) => {
@@ -143,15 +92,7 @@ describe('Transaction Spoofability Tests', function () {
14392
return [200, { txid: 'test-txid-123', status: 'signed' }];
14493
});
14594

146-
const pubs = keyTriple.map((k) => k.neutered().toBase58());
147-
const responses = [
148-
{ pub: pubs[0], encryptedPrv: encrypt('pass', user.toBase58()) },
149-
{ pub: pubs[1] },
150-
{ pub: pubs[2] },
151-
];
152-
wallet
153-
.keyIds()
154-
.forEach((id, i) => nock(bgUrl).get(`/api/v2/${wallet.coin()}/key/${id}`).reply(200, responses[i]));
95+
nockWalletKeys(wallet, keyTriple, 'pass');
15596

15697
await assert.rejects(
15798
wallet.fanoutUnspents({ walletPassphrase: 'pass' }),
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import * as utxolib from '@bitgo/utxo-lib';
2+
import { fixedScriptWallet, type CoinName } from '@bitgo/wasm-utxo';
3+
4+
const { ChainCode } = fixedScriptWallet;
5+
6+
type UtxolibRootWalletKeys = utxolib.bitgo.RootWalletKeys;
7+
type WasmRootWalletKeys = fixedScriptWallet.RootWalletKeys;
8+
type RootWalletKeys = UtxolibRootWalletKeys | WasmRootWalletKeys;
9+
10+
const defaultChain = ChainCode.value('p2sh', 'external');
11+
12+
/**
13+
* Generate a wallet address from RootWalletKeys.
14+
* Supports both utxolib and wasm-utxo RootWalletKeys.
15+
* Utxolib keys are converted to wasm-utxo keys for address generation.
16+
*/
17+
export function getWalletAddress(
18+
coinName: CoinName,
19+
walletKeys: RootWalletKeys,
20+
chain = defaultChain,
21+
index = 0
22+
): string {
23+
return fixedScriptWallet.address(walletKeys, chain, index, coinName);
24+
}

modules/abstract-utxo/test/unit/util/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ export * from './wallet';
55
export * from './unspents';
66
export * from './transaction';
77
export * from './psbt';
8+
export * from './address';
9+
export * from './nockBitGo';

modules/abstract-utxo/test/unit/util/keychains.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Triple } from '@bitgo/sdk-core';
22
import { encrypt } from '@bitgo/sdk-api';
33
import { getSeed } from '@bitgo/sdk-test';
4-
import { bip32, BIP32Interface, bitgo, testutil } from '@bitgo/utxo-lib';
4+
import { bip32, BIP32Interface, bitgo } from '@bitgo/utxo-lib';
55
import { BIP32 as WasmBIP32, fixedScriptWallet } from '@bitgo/wasm-utxo';
66

77
type RootWalletKeys = bitgo.RootWalletKeys;
@@ -93,13 +93,16 @@ export function getWalletKeys(seed: string): RootWalletKeys {
9393

9494
/**
9595
* Create test wallet keys from a seed string.
96-
* Returns xpubs and xprivs as base58 strings.
96+
* Uses the same seed generation as getWalletKeys (getSeed('seed/i'))
97+
* to ensure compatibility with existing test fixtures.
9798
*/
9899
export function createTestWalletKeys(seed: string): {
99100
xpubs: Triple<string>;
100101
xprivs: Triple<string>;
101102
} {
102-
const keys = testutil.getKeyTriple(seed);
103+
const keys = Array.from({ length: 3 }).map((_, i) =>
104+
bip32.fromSeed(getSeed(`${seed}/${i}`))
105+
) as Triple<BIP32Interface>;
103106
return {
104107
xpubs: keys.map((k) => k.neutered().toBase58()) as Triple<string>,
105108
xprivs: keys.map((k) => k.toBase58()) as Triple<string>,
Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,26 @@
11
import nock = require('nock');
2-
import { Environment, Environments } from '@bitgo/sdk-core';
2+
import { encrypt } from '@bitgo/sdk-api';
3+
import { Environment, Environments, Wallet } from '@bitgo/sdk-core';
4+
import { BIP32, Triple } from '@bitgo/wasm-utxo';
35

46
import { defaultBitGo } from './utxoCoins';
57

68
export function nockBitGo(bitgo = defaultBitGo): nock.Scope {
79
const env = Environments[bitgo.getEnv()] as Environment;
810
return nock(env.uri);
911
}
12+
13+
/**
14+
* Mock the key fetching endpoints for a wallet.
15+
* Sets up nock to return the key triple with the user key encrypted.
16+
*/
17+
export function nockWalletKeys(wallet: Wallet, keyTriple: Triple<BIP32>, userPassphrase: string): void {
18+
const [user] = keyTriple;
19+
const pubs = keyTriple.map((k) => k.neutered().toBase58());
20+
const responses = [
21+
{ pub: pubs[0], encryptedPrv: encrypt(userPassphrase, user.toBase58()) },
22+
{ pub: pubs[1] },
23+
{ pub: pubs[2] },
24+
];
25+
wallet.keyIds().forEach((id, i) => nockBitGo().get(`/api/v2/${wallet.coin()}/key/${id}`).reply(200, responses[i]));
26+
}

0 commit comments

Comments
 (0)