Skip to content

Commit 13217cf

Browse files
test: add unit tests for bitcoin common module (#1823)
* test: add unit tests for bitcoin address module * test: add unit test for info module * test: add unit test for key derivation modules
1 parent 1fc3c99 commit 13217cf

File tree

14 files changed

+479
-148
lines changed

14 files changed

+479
-148
lines changed

packages/bitcoin/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"prepare": "ts-patch install -s",
3535
"prestart": "yarn build",
3636
"start": "node dist/index.js",
37-
"test": "echo \"@lace/bitcoin: no test command specified\"",
37+
"test": "NODE_ENV=test run -T jest -c ./test/jest.config.js --silent",
3838
"tsc:declarationOnly": "tsc --project ./src/tsconfig.declarationOnly.json",
3939
"type-check": "echo \"@lace/bitcoin: no type-check command specified\"",
4040
"watch": "yarn build --watch"

packages/bitcoin/src/wallet/README.md

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,33 @@
1-
# Light Wallet | Packages | Cardano | Wallet
1+
# **Bitcoin Observable Wallet Proof of Concept**
22

3-
A thin wrapper around the `cardano-js-sdk` to define a blockchain wallet module conforming to
4-
a basic interface exposed by the core package.
3+
This project is a implementation of a simple Bitcoin wallet, built using **TypeScript** and leveraging:
4+
- **BitcoinJS** for address/key derivation and transaction building/signing.
5+
- **Maestro API** to interact with the Bitcoin network (Testnet or Mainnet) for transaction history, UTXO retrieval, and broadcasting transactions.
6+
- **RxJS** for reactive programming to observe wallet state, transaction history, and UTXO updates.
7+
8+
---
9+
10+
## **Features**
11+
12+
1. **Address and Key Derivation**:
13+
- Supports **BIP32** and **Electrum-compatible** derivation paths.
14+
- Generates:
15+
- Legacy (P2PKH)
16+
- SegWit (P2SH-P2WPKH)
17+
- Native SegWit (P2WPKH)
18+
- Taproot (P2TR) addresses.
19+
20+
2. **Transaction History and UTXOs**:
21+
- Fetches transaction history for specified addresses.
22+
- Retrieves **UTXOs** (Unspent Transaction Outputs) for address spending.
23+
24+
3. **Balance Tracking**:
25+
- Continuously tracks and updates wallet balance based on UTXOs.
26+
27+
4. **Transaction Building and Signing**:
28+
- Constructs a raw transaction from available UTXOs.
29+
- Signs the transaction using the derived private key.
30+
- Submits the transaction to the Bitcoin network.
31+
32+
5. **Transaction Tracking**:
33+
- Tracks the status of a transaction (e.g., Pending, Confirmed, or Dropped) until it is included in a block.

packages/bitcoin/src/wallet/types.ts

Lines changed: 0 additions & 83 deletions
This file was deleted.

packages/bitcoin/test/.eslintrc.js

Lines changed: 0 additions & 7 deletions
This file was deleted.

packages/bitcoin/test/__mocks__/fileMock.js

Lines changed: 0 additions & 1 deletion
This file was deleted.

packages/bitcoin/test/__mocks__/styleMock.js

Lines changed: 0 additions & 1 deletion
This file was deleted.

packages/bitcoin/test/__mocks__/svgMock.js

Lines changed: 0 additions & 1 deletion
This file was deleted.

packages/bitcoin/test/address.test.ts

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/* eslint-disable no-magic-numbers, no-loop-func */
2+
import * as bitcoin from 'bitcoinjs-lib';
3+
import * as ecc from '@bitcoinerlab/secp256k1';
4+
import { Network } from '../src/wallet/lib/common/network';
5+
import {
6+
AddressType,
7+
deriveAddressByType,
8+
validateBitcoinAddress,
9+
AddressValidationResult,
10+
isP2trAddress
11+
} from '../src/wallet/lib/common/address';
12+
13+
bitcoin.initEccLib(ecc);
14+
15+
const networkMap: Record<string, bitcoin.Network> = {
16+
mainnet: bitcoin.networks.bitcoin,
17+
testnet: bitcoin.networks.testnet
18+
};
19+
20+
const addressTypeMap: Record<string, AddressType> = {
21+
p2pkh: AddressType.Legacy,
22+
'p2sh-p2wpkh': AddressType.SegWit,
23+
p2wpkh: AddressType.NativeSegWit,
24+
p2tr: AddressType.Taproot
25+
};
26+
27+
type Vector = {
28+
privateKey?: string;
29+
publicKey: string;
30+
address: string;
31+
network: string;
32+
format: string;
33+
};
34+
35+
// Test vectors taken from https://github.com/hirosystems/stacks-blockchain-api/blob/ae0eb65c4d6901db172f2b4ea751817d8ef8c05c/src/tests/helpers-tests.ts#L193-L530
36+
const TEST_VECTORS: Vector[] = [
37+
{
38+
publicKey: '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798',
39+
address: '1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH',
40+
network: 'mainnet',
41+
format: 'p2pkh'
42+
},
43+
{
44+
publicKey: '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798',
45+
address: '3JvL6Ymt8MVWiCNHC7oWU6nLeHNJKLZGLN',
46+
network: 'mainnet',
47+
format: 'p2sh-p2wpkh'
48+
},
49+
{
50+
publicKey: '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798',
51+
address: 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4',
52+
network: 'mainnet',
53+
format: 'p2wpkh'
54+
},
55+
{
56+
publicKey: '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798',
57+
address: 'bc1pmfr3p9j00pfxjh0zmgp99y8zftmd3s5pmedqhyptwy6lm87hf5sspknck9',
58+
network: 'mainnet',
59+
format: 'p2tr'
60+
},
61+
{
62+
publicKey: '03797dd653040d344fd048c1ad05d4cbcb2178b30c6a0c4276994795f3e833da41',
63+
address: '2NEb2fNbJXdwi7EC6vKCjWUTA12PABNniQM',
64+
network: 'testnet',
65+
format: 'p2sh-p2wpkh'
66+
},
67+
{
68+
publicKey: '03797dd653040d344fd048c1ad05d4cbcb2178b30c6a0c4276994795f3e833da41',
69+
address: 'tb1qzepy04hjksj6c4m3ggawdjqvw48hzu4swvwmvt',
70+
network: 'testnet',
71+
format: 'p2wpkh'
72+
},
73+
{
74+
publicKey: '03797dd653040d344fd048c1ad05d4cbcb2178b30c6a0c4276994795f3e833da41',
75+
address: 'tb1p8dlmzllfah294ntwatr8j5uuvcj7yg0dete94ck2krrk0ka2c9qqex96hv',
76+
network: 'testnet',
77+
format: 'p2tr'
78+
}
79+
];
80+
81+
describe('Addresses', () => {
82+
describe('Bitcoin address derivation (from test vectors)', () => {
83+
for (const vector of TEST_VECTORS) {
84+
const { publicKey, address, network, format } = vector;
85+
86+
const testName = `${format.toUpperCase()} | ${network} | ${publicKey.slice(0, 10)}...`;
87+
const addressType = addressTypeMap[format];
88+
89+
it(`produces correct address: ${testName}`, () => {
90+
const derived = deriveAddressByType(Buffer.from(publicKey, 'hex'), addressType, networkMap[network]);
91+
expect(derived).toBe(address);
92+
});
93+
}
94+
});
95+
96+
describe('validateBitcoinAddress', () => {
97+
it('validates a correct legacy address (Mainnet)', () => {
98+
expect(validateBitcoinAddress('1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa', Network.Mainnet)).toBe(
99+
AddressValidationResult.Valid
100+
);
101+
});
102+
103+
it('validates a correct native segwit address (Mainnet)', () => {
104+
expect(validateBitcoinAddress('bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq', Network.Mainnet)).toBe(
105+
AddressValidationResult.Valid
106+
);
107+
});
108+
109+
it('validates a correct taproot address (Mainnet)', () => {
110+
expect(
111+
validateBitcoinAddress('bc1pemwrzunpf5tj70s6vkxysf2v8njg60kpc2mvuccath5kfw3zd6jqv9lks9', Network.Mainnet)
112+
).toBe(AddressValidationResult.Valid);
113+
});
114+
115+
it('returns InvalidNetwork for valid testnet address on mainnet', () => {
116+
expect(
117+
validateBitcoinAddress('tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c', Network.Mainnet)
118+
).toBe(AddressValidationResult.InvalidNetwork);
119+
});
120+
121+
it('validates a correct legacy address (Testnet)', () => {
122+
expect(validateBitcoinAddress('mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn', Network.Testnet)).toBe(
123+
AddressValidationResult.Valid
124+
);
125+
});
126+
127+
it('validates a correct native segwit address (Testnet)', () => {
128+
expect(validateBitcoinAddress('tb1q597d0yvt3mg3k9p5qtkz8lh3j53nsssr57wnfr', Network.Testnet)).toBe(
129+
AddressValidationResult.Valid
130+
);
131+
});
132+
133+
it('validates a correct taproot address (Testnet)', () => {
134+
expect(
135+
validateBitcoinAddress('tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c', Network.Testnet)
136+
).toBe(AddressValidationResult.Valid);
137+
});
138+
139+
it('returns InvalidNetwork for valid mainnet address on testnet', () => {
140+
expect(
141+
validateBitcoinAddress('bc1pemwrzunpf5tj70s6vkxysf2v8njg60kpc2mvuccath5kfw3zd6jqv9lks9', Network.Testnet)
142+
).toBe(AddressValidationResult.InvalidNetwork);
143+
});
144+
145+
it('returns InvalidAddress for malformed address', () => {
146+
expect(validateBitcoinAddress('notARealAddress', Network.Mainnet)).toBe(AddressValidationResult.InvalidAddress);
147+
});
148+
});
149+
150+
describe('isP2trAddress', () => {
151+
it('identifies P2TR address correctly', () => {
152+
expect(isP2trAddress('bc1pmyrcn4jl9x8gtz8wjyqchghzq39kech5xg99snm4a3p2l3thf4esfp20h9')).toBe(true);
153+
expect(isP2trAddress('tb1pmyrcn4jl9x8gtz8wjyqchghzq39kech5xg99snm4a3p2l3thf4esfp20h9')).toBe(true);
154+
});
155+
156+
it('rejects non-taproot address', () => {
157+
expect(isP2trAddress('bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq')).toBe(false);
158+
});
159+
});
160+
});

packages/bitcoin/test/info.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { getNetworkKeys, BitcoinWalletInfo, ExtendedAccountPublicKeys } from '../src/wallet/lib/common/info';
2+
import { Network } from '../src/wallet/lib/common/network';
3+
4+
describe('getNetworkKeys', () => {
5+
const mainnetKeys: ExtendedAccountPublicKeys = {
6+
legacy: 'xpub-mainnet-legacy',
7+
segWit: 'xpub-mainnet-segwit',
8+
nativeSegWit: 'xpub-mainnet-nativesegwit',
9+
taproot: 'xpub-mainnet-taproot',
10+
electrumNativeSegWit: 'xpub-mainnet-electrum'
11+
};
12+
13+
const testnetKeys: ExtendedAccountPublicKeys = {
14+
legacy: 'tpub-testnet-legacy',
15+
segWit: 'tpub-testnet-segwit',
16+
nativeSegWit: 'tpub-testnet-nativesegwit',
17+
taproot: 'tpub-testnet-taproot',
18+
electrumNativeSegWit: 'tpub-testnet-electrum'
19+
};
20+
21+
const walletInfo: BitcoinWalletInfo = {
22+
walletName: 'Test Wallet',
23+
accountIndex: 0,
24+
encryptedSecrets: {
25+
mnemonics: 'encrypted-mnemonic-placeholder',
26+
seed: 'encrypted-seed-placeholder'
27+
},
28+
extendedAccountPublicKeys: {
29+
mainnet: mainnetKeys,
30+
testnet: testnetKeys
31+
}
32+
};
33+
34+
it('returns mainnet keys for Network.Mainnet', () => {
35+
const keys = getNetworkKeys(walletInfo, Network.Mainnet);
36+
expect(keys).toEqual(mainnetKeys);
37+
});
38+
39+
it('returns testnet keys for Network.Testnet', () => {
40+
const keys = getNetworkKeys(walletInfo, Network.Testnet);
41+
expect(keys).toEqual(testnetKeys);
42+
});
43+
});

packages/bitcoin/test/jest.config.js

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,13 @@
1-
const { pathsToModuleNameMapper } = require('ts-jest');
2-
const { compilerOptions } = require('../src/tsconfig');
3-
const { createJestConfig } = require('../../../test/createJestConfig');
4-
5-
const rootDir = process.cwd();
6-
7-
module.exports = createJestConfig({
8-
moduleNameMapper: {
9-
'.*\\.(scss|sass|css|less)$': '<rootDir>/test/__mocks__/styleMock.js',
10-
'.*\\.(jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2)$': '<rootDir>/test/__mocks__/fileMock.js',
11-
'^[.]*(?!.*\\.component\\.svg$).*\\.svg*$': '<rootDir>/test/__mocks__/fileMock.js',
12-
'component\\.svg(\\?v=\\d+\\.d+\\.\\d+)?$': '<rootDir>/test/__mocks__/svgMock.js',
13-
'^lodash-es$': 'lodash',
14-
'^webextension-polyfill': '<rootDir>/test/__mocks__/fileMock.js',
15-
// https://github.com/LedgerHQ/ledger-live/issues/763
16-
'@ledgerhq/devices/hid-framing': '@ledgerhq/devices/lib/hid-framing',
17-
// TODO update uuid and retest; https://github.com/uuidjs/uuid/issues/451
18-
uuid: require.resolve('uuid'),
19-
...pathsToModuleNameMapper(compilerOptions.paths, { prefix: '<rootDir>/src' })
20-
},
21-
roots: ['<rootDir>/src'],
22-
testTimeout: 60000,
23-
testEnvironment: 'jsdom',
24-
setupFilesAfterEnv: ['./test/jest.setup.js', 'jest-canvas-mock'],
25-
workerThreads: true
26-
});
1+
module.exports = {
2+
preset: 'ts-jest/presets/default',
3+
testEnvironment: 'node',
4+
testMatch: ['**/__tests__/**/*.test.ts', '**/*.test.ts'],
5+
transform: {
6+
'^.+\\.tsx?$': [
7+
'ts-jest',
8+
{
9+
tsconfig: './test/tsconfig.json'
10+
}
11+
]
12+
}
13+
};

0 commit comments

Comments
 (0)