Skip to content

Commit b310ff9

Browse files
feat: add unit tests to bitcoin package (#1855)
1 parent 471f209 commit b310ff9

File tree

6 files changed

+614
-14
lines changed

6 files changed

+614
-14
lines changed

packages/bitcoin/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
"@types/pbkdf2": "^3",
6868
"@types/webextension-polyfill": "0.10.0",
6969
"axios": "^1.7.4",
70+
"ecpair": "^3.0.0",
7071
"rollup-plugin-polyfill-node": "^0.8.0",
7172
"ts-log": "^2.2.7",
7273
"type-fest": "^4.26.1",

packages/bitcoin/src/wallet/lib/wallet/BitcoinWallet.ts

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ export class BitcoinWallet {
322322
);
323323
const updatedPendingTxs = [...this.pendingTransactions$.value];
324324

325-
updatedPendingTxs.forEach((localTx, index) => {
325+
const filteredPendingTxs = updatedPendingTxs.filter((localTx) => {
326326
const inputUsedInHistory = this.transactionHistory.some((historyTx) =>
327327
historyTx.inputs.some((histInput) =>
328328
localTx.inputs.some(
@@ -341,24 +341,17 @@ export class BitcoinWallet {
341341
)
342342
);
343343

344-
if (inputUsedInHistory || inputUsedInRemotePending) {
345-
updatedPendingTxs.splice(index, 1);
346-
}
344+
return !(inputUsedInHistory || inputUsedInRemotePending);
347345
});
348346

349347
remotePendingTxs.forEach((remoteTx) => {
350-
const localTxIndex = updatedPendingTxs.findIndex(
351-
(localTx) => localTx.transactionHash === remoteTx.transactionHash
352-
);
353-
if (localTxIndex > -1) {
354-
updatedPendingTxs[localTxIndex] = remoteTx;
355-
} else {
356-
updatedPendingTxs.push(remoteTx);
357-
}
348+
const index = filteredPendingTxs.findIndex((t) => t.transactionHash === remoteTx.transactionHash);
349+
if (index > -1) filteredPendingTxs[index] = remoteTx;
350+
else filteredPendingTxs.push(remoteTx);
358351
});
359352

360-
if (!isEqual(updatedPendingTxs, this.pendingTransactions$.value)) {
361-
this.pendingTransactions$.next(updatedPendingTxs);
353+
if (!isEqual(filteredPendingTxs, this.pendingTransactions$.value)) {
354+
this.pendingTransactions$.next(filteredPendingTxs);
362355
}
363356
}
364357

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/* eslint-disable no-magic-numbers, @typescript-eslint/no-non-null-assertion */
2+
import {
3+
BitcoinSigner,
4+
signTx,
5+
AddressType,
6+
ChainType,
7+
KeyPair,
8+
Network,
9+
UnsignedTransaction
10+
} from '../src/wallet/lib';
11+
import * as bitcoin from 'bitcoinjs-lib';
12+
import * as ecc from '@bitcoinerlab/secp256k1';
13+
import * as ECPairFactory from 'ecpair';
14+
15+
const ECPair = ECPairFactory.ECPairFactory(ecc);
16+
bitcoin.initEccLib(ecc);
17+
18+
describe('BitcoinSigner', () => {
19+
let keyPair: KeyPair;
20+
let publicKey: Uint8Array;
21+
let privateKey: Uint8Array;
22+
23+
beforeEach(() => {
24+
const ecPair = ECPair.makeRandom();
25+
publicKey = ecPair.publicKey!;
26+
privateKey = ecPair.privateKey!;
27+
keyPair = { publicKey: Buffer.from(publicKey), privateKey: Buffer.from(privateKey) };
28+
});
29+
30+
it('initializes with a valid keyPair', () => {
31+
const signer = new BitcoinSigner(keyPair);
32+
expect(signer.getPublicKey()).toEqual(Buffer.from(publicKey));
33+
});
34+
35+
it('throws if private key is missing', () => {
36+
expect(() => new BitcoinSigner({ publicKey: Buffer.from(publicKey) } as KeyPair)).toThrow(
37+
'Private key is required to sign transactions.'
38+
);
39+
});
40+
41+
it('throws if hash is not 32 bytes', () => {
42+
const signer = new BitcoinSigner(keyPair);
43+
const badHash = Buffer.alloc(31);
44+
expect(() => signer.sign(badHash)).toThrow('Hash must be 32 bytes.');
45+
});
46+
47+
it('signs a 32-byte hash and produces a valid signature', () => {
48+
const signer = new BitcoinSigner(keyPair);
49+
const hash = Buffer.alloc(32, 0x01);
50+
const signature = signer.sign(hash);
51+
52+
const isValid = ecc.verify(new Uint8Array(hash), new Uint8Array(publicKey), new Uint8Array(signature));
53+
54+
expect(signature).toBeInstanceOf(Buffer);
55+
expect(signature.length).toBeGreaterThan(0);
56+
expect(isValid).toBe(true);
57+
});
58+
59+
it('clears secrets from memory', () => {
60+
const pair = { ...keyPair, privateKey: Buffer.from(privateKey) };
61+
const signer = new BitcoinSigner(pair);
62+
signer.clearSecrets();
63+
expect(pair.privateKey.every((b) => b === 0)).toBe(true);
64+
});
65+
});
66+
67+
describe('signTx', () => {
68+
it('signs and finalizes a PSBT, returning valid hex and signature', () => {
69+
const ecPair = ECPair.makeRandom();
70+
const publicKey = Buffer.from(ecPair.publicKey!);
71+
const privateKey = Buffer.from(ecPair.privateKey!);
72+
const keyPair = { publicKey, privateKey };
73+
74+
const network = bitcoin.networks.testnet;
75+
const p2wpkh = bitcoin.payments.p2wpkh({ pubkey: publicKey, network });
76+
77+
const psbt = new bitcoin.Psbt({ network });
78+
79+
psbt.addInput({
80+
hash: 'f'.repeat(64),
81+
index: 0,
82+
witnessUtxo: {
83+
script: p2wpkh.output!,
84+
value: 10_000
85+
}
86+
});
87+
88+
psbt.addOutput({
89+
address: p2wpkh.address!,
90+
value: 9000
91+
});
92+
93+
const unsignedTx: UnsignedTransaction = {
94+
context: psbt,
95+
toAddress: p2wpkh.address!,
96+
amount: BigInt(9000),
97+
fee: BigInt(1000),
98+
vBytes: 111,
99+
signers: [
100+
{
101+
address: p2wpkh.address!,
102+
publicKeyHex: publicKey.toString('hex'),
103+
account: 0,
104+
chain: ChainType.External,
105+
index: 0,
106+
addressType: AddressType.Legacy,
107+
network: Network.Testnet
108+
}
109+
]
110+
};
111+
112+
const signer = new BitcoinSigner(keyPair);
113+
114+
const signedTx = signTx(unsignedTx, [signer]);
115+
116+
expect(signedTx.hex.length).toBeGreaterThan(0);
117+
expect(signedTx.context).toBeInstanceOf(bitcoin.Psbt);
118+
119+
const rePsbt = new bitcoin.Psbt({ network });
120+
rePsbt.addInput({
121+
hash: 'f'.repeat(64),
122+
index: 0,
123+
witnessUtxo: {
124+
script: p2wpkh.output!,
125+
value: 10_000
126+
}
127+
});
128+
rePsbt.addOutput({
129+
address: p2wpkh.address!,
130+
value: 9000
131+
});
132+
133+
rePsbt.signInput(0, signer);
134+
135+
const isValid = rePsbt.validateSignaturesOfInput(0, (pubkey, msgHash, sig) => ecc.verify(msgHash, pubkey, sig));
136+
expect(isValid).toBe(true);
137+
});
138+
});

0 commit comments

Comments
 (0)