Skip to content

Commit 3ae9393

Browse files
authored
feat(bitcoinAddress): multiple bitcoin address types and testnet (#2922)
1 parent badaa6d commit 3ae9393

File tree

5 files changed

+235
-22
lines changed

5 files changed

+235
-22
lines changed

src/index.ts

+8
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@ export type { DatabaseModule } from './modules/database';
5555
export type { DatatypeModule } from './modules/datatype';
5656
export type { DateModule, SimpleDateModule } from './modules/date';
5757
export type { Currency, FinanceModule } from './modules/finance';
58+
export {
59+
BitcoinAddressFamily,
60+
BitcoinNetwork,
61+
} from './modules/finance/bitcoin';
62+
export type {
63+
BitcoinAddressFamilyType,
64+
BitcoinNetworkType,
65+
} from './modules/finance/bitcoin';
5866
export type { FoodModule } from './modules/food';
5967
export type { GitModule } from './modules/git';
6068
export type { HackerModule } from './modules/hacker';

src/modules/finance/bitcoin.ts

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import type { Casing } from '../string';
2+
3+
/**
4+
* The bitcoin address families.
5+
*/
6+
export enum BitcoinAddressFamily {
7+
Legacy = 'legacy',
8+
Segwit = 'segwit',
9+
Bech32 = 'bech32',
10+
Taproot = 'taproot',
11+
}
12+
13+
/**
14+
* The bitcoin address families.
15+
*/
16+
export type BitcoinAddressFamilyType = `${BitcoinAddressFamily}`;
17+
18+
/**
19+
* The different bitcoin networks.
20+
*/
21+
export enum BitcoinNetwork {
22+
Mainnet = 'mainnet',
23+
Testnet = 'testnet',
24+
}
25+
26+
/**
27+
* The different bitcoin networks.
28+
*/
29+
export type BitcoinNetworkType = `${BitcoinNetwork}`;
30+
31+
type BitcoinAddressOptions = {
32+
prefix: Record<BitcoinNetworkType, string>;
33+
length: { min: number; max: number };
34+
casing: Casing;
35+
exclude: string;
36+
};
37+
38+
export const BitcoinAddressSpecs: Record<
39+
BitcoinAddressFamilyType,
40+
BitcoinAddressOptions
41+
> = {
42+
[BitcoinAddressFamily.Legacy]: {
43+
prefix: { [BitcoinNetwork.Mainnet]: '1', [BitcoinNetwork.Testnet]: 'm' },
44+
length: { min: 26, max: 34 },
45+
casing: 'mixed',
46+
exclude: '0OIl',
47+
},
48+
[BitcoinAddressFamily.Segwit]: {
49+
prefix: { [BitcoinNetwork.Mainnet]: '3', [BitcoinNetwork.Testnet]: '2' },
50+
length: { min: 26, max: 34 },
51+
casing: 'mixed',
52+
exclude: '0OIl',
53+
},
54+
[BitcoinAddressFamily.Bech32]: {
55+
prefix: {
56+
[BitcoinNetwork.Mainnet]: 'bc1',
57+
[BitcoinNetwork.Testnet]: 'tb1',
58+
},
59+
length: { min: 42, max: 42 },
60+
casing: 'lower',
61+
exclude: '1bBiIoO',
62+
},
63+
[BitcoinAddressFamily.Taproot]: {
64+
prefix: {
65+
[BitcoinNetwork.Mainnet]: 'bc1p',
66+
[BitcoinNetwork.Testnet]: 'tb1p',
67+
},
68+
length: { min: 62, max: 62 },
69+
casing: 'lower',
70+
exclude: '1bBiIoO',
71+
},
72+
};

src/modules/finance/index.ts

+42-11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { FakerError } from '../../errors/faker-error';
22
import { ModuleBase } from '../../internal/module-base';
3+
import type { BitcoinAddressFamilyType, BitcoinNetworkType } from './bitcoin';
4+
import {
5+
BitcoinAddressFamily,
6+
BitcoinAddressSpecs,
7+
BitcoinNetwork,
8+
} from './bitcoin';
39
import iban from './iban';
410

511
/**
@@ -486,23 +492,48 @@ export class FinanceModule extends ModuleBase {
486492
/**
487493
* Generates a random Bitcoin address.
488494
*
495+
* @param options An optional options object.
496+
* @param options.type The bitcoin address type (`'legacy'`, `'sewgit'`, `'bech32'` or `'taproot'`). Defaults to a random address type.
497+
* @param options.network The bitcoin network (`'mainnet'` or `'testnet'`). Defaults to `'mainnet'`.
498+
*
489499
* @example
490-
* faker.finance.bitcoinAddress() // '3ySdvCkTLVy7gKD4j6JfSaf5d'
500+
* faker.finance.bitcoinAddress() // '1TeZEFLmGPLEQrSRdAcnZLoWwYeiHwmRog'
501+
* faker.finance.bitcoinAddress({ type: 'bech32' }) // 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'
502+
* faker.finance.bitcoinAddress({ type: 'bech32', network: 'testnet' }) // 'tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx'
491503
*
492504
* @since 3.1.0
493505
*/
494-
bitcoinAddress(): string {
495-
const addressLength = this.faker.number.int({ min: 25, max: 39 });
496-
497-
let address = this.faker.helpers.arrayElement(['1', '3']);
498-
499-
address += this.faker.string.alphanumeric({
500-
length: addressLength,
501-
casing: 'mixed',
502-
exclude: '0OIl',
506+
bitcoinAddress(
507+
options: {
508+
/**
509+
* The bitcoin address type (`'legacy'`, `'sewgit'`, `'bech32'` or `'taproot'`).
510+
*
511+
* @default faker.helpers.arrayElement(['legacy','sewgit','bech32','taproot'])
512+
*/
513+
type?: BitcoinAddressFamilyType;
514+
/**
515+
* The bitcoin network (`'mainnet'` or `'testnet'`).
516+
*
517+
* @default 'mainnet'
518+
*/
519+
network?: BitcoinNetworkType;
520+
} = {}
521+
): string {
522+
const {
523+
type = this.faker.helpers.enumValue(BitcoinAddressFamily),
524+
network = BitcoinNetwork.Mainnet,
525+
} = options;
526+
const addressSpec = BitcoinAddressSpecs[type];
527+
const addressPrefix = addressSpec.prefix[network];
528+
const addressLength = this.faker.number.int(addressSpec.length);
529+
530+
const address = this.faker.string.alphanumeric({
531+
length: addressLength - addressPrefix.length,
532+
casing: addressSpec.casing,
533+
exclude: addressSpec.exclude,
503534
});
504535

505-
return address;
536+
return addressPrefix + address;
506537
}
507538

508539
/**

test/modules/__snapshots__/finance.spec.ts.snap

+15-3
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ exports[`finance > 42 > bic > noArgs 1`] = `"YTPECC2VXXX"`;
2424

2525
exports[`finance > 42 > bic > with branch code 1`] = `"JYTPCD52XXX"`;
2626

27-
exports[`finance > 42 > bitcoinAddress 1`] = `"3JAaa4SAH2YQdbbiwrhB9hnsMcvA3Ba"`;
27+
exports[`finance > 42 > bitcoinAddress > noArgs 1`] = `"3JAaa4SAH2YQdbbiwrhB9hnsMcvA3Ba4XY"`;
28+
29+
exports[`finance > 42 > bitcoinAddress > with type and network option 1`] = `"1XJAaa4SAH2YQdbbiwrhB9hnsMcvA"`;
30+
31+
exports[`finance > 42 > bitcoinAddress > with type option 1`] = `"1XJAaa4SAH2YQdbbiwrhB9hnsMcvA"`;
2832

2933
exports[`finance > 42 > creditCardCVV 1`] = `"397"`;
3034

@@ -108,7 +112,11 @@ exports[`finance > 1211 > bic > noArgs 1`] = `"XFZROMRC"`;
108112

109113
exports[`finance > 1211 > bic > with branch code 1`] = `"YXFZNPOROTR"`;
110114

111-
exports[`finance > 1211 > bitcoinAddress 1`] = `"3eZEFLmGPLEQrSRdAcnZLoWwYeiHwmRogjbyG9G"`;
115+
exports[`finance > 1211 > bitcoinAddress > noArgs 1`] = `"bc1pw8zppsdqusnufvv7l7dzsexkz8aqjdve9a6kq5qh8f7vlh2q6q9sjg7mv4"`;
116+
117+
exports[`finance > 1211 > bitcoinAddress > with type and network option 1`] = `"1TeZEFLmGPLEQrSRdAcnZLoWwYeiHwmRog"`;
118+
119+
exports[`finance > 1211 > bitcoinAddress > with type option 1`] = `"1TeZEFLmGPLEQrSRdAcnZLoWwYeiHwmRog"`;
112120

113121
exports[`finance > 1211 > creditCardCVV 1`] = `"982"`;
114122

@@ -192,7 +200,11 @@ exports[`finance > 1337 > bic > noArgs 1`] = `"EHLILK9ZXXX"`;
192200

193201
exports[`finance > 1337 > bic > with branch code 1`] = `"GEHLGGI9XXX"`;
194202

195-
exports[`finance > 1337 > bitcoinAddress 1`] = `"1hsjwgYJ7oC8ZrMNmqzLbhEubpcwQ"`;
203+
exports[`finance > 1337 > bitcoinAddress > noArgs 1`] = `"3hsjwgYJ7oC8ZrMNmqzLbhEubpc"`;
204+
205+
exports[`finance > 1337 > bitcoinAddress > with type and network option 1`] = `"1ahsjwgYJ7oC8ZrMNmqzLbhEubpc"`;
206+
207+
exports[`finance > 1337 > bitcoinAddress > with type option 1`] = `"1ahsjwgYJ7oC8ZrMNmqzLbhEubpc"`;
196208

197209
exports[`finance > 1337 > creditCardCVV 1`] = `"212"`;
198210

test/modules/finance.spec.ts

+98-8
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
import isValidBtcAddress from 'validator/lib/isBtcAddress';
21
import isCreditCard from 'validator/lib/isCreditCard';
32
import { describe, expect, it } from 'vitest';
43
import { faker, fakerZH_CN } from '../../src';
54
import { FakerError } from '../../src/errors/faker-error';
5+
import {
6+
BitcoinAddressFamily,
7+
BitcoinNetwork,
8+
} from '../../src/modules/finance/bitcoin';
69
import ibanLib from '../../src/modules/finance/iban';
710
import { luhnCheck } from '../../src/modules/helpers/luhn-check';
811
import { seededTests } from '../support/seeded-runs';
@@ -21,7 +24,6 @@ describe('finance', () => {
2124
'currencyCode',
2225
'currencyName',
2326
'currencySymbol',
24-
'bitcoinAddress',
2527
'litecoinAddress',
2628
'creditCardCVV',
2729
'ethereumAddress',
@@ -91,6 +93,15 @@ describe('finance', () => {
9193
ellipsis: true,
9294
});
9395
});
96+
97+
t.describe('bitcoinAddress', (t) => {
98+
t.it('noArgs')
99+
.it('with type option', { type: BitcoinAddressFamily.Legacy })
100+
.it('with type and network option', {
101+
type: BitcoinAddressFamily.Legacy,
102+
network: BitcoinNetwork.Mainnet,
103+
});
104+
});
94105
});
95106

96107
describe.each(times(NON_SEEDED_BASED_RUN).map(() => faker.seed()))(
@@ -313,17 +324,96 @@ describe('finance', () => {
313324
});
314325

315326
describe('bitcoinAddress()', () => {
327+
const m_legacy = /^1[A-HJ-NP-Za-km-z1-9]{25,39}$/;
328+
const t_legacy = /^m[A-HJ-NP-Za-km-z1-9]{25,39}$/;
329+
const m_segwit = /^3[A-HJ-NP-Za-km-z1-9]{25,39}$/;
330+
const t_segwit = /^2[A-HJ-NP-Za-km-z1-9]{25,39}$/;
331+
const m_bech32 = /^bc1[ac-hj-np-z02-9]{39,39}$/;
332+
const t_bech32 = /^tb1[ac-hj-np-z02-9]{39,39}$/;
333+
const m_taproot = /^bc1p[ac-hj-np-z02-9]{58,58}$/;
334+
const t_taproot = /^tb1p[ac-hj-np-z02-9]{58,58}$/;
335+
336+
const isBtcAddress = (address: string) =>
337+
[
338+
m_legacy,
339+
t_legacy,
340+
m_segwit,
341+
t_segwit,
342+
m_bech32,
343+
t_bech32,
344+
m_taproot,
345+
t_taproot,
346+
].some((r) => r.test(address));
347+
316348
it('should return a valid bitcoin address', () => {
317349
const bitcoinAddress = faker.finance.bitcoinAddress();
318-
/**
319-
* Note: Although the total length of a Bitcoin address can be 25-33 characters, regex quantifiers only check the preceding token
320-
* Therefore we take one from the total length of the address not including the first character ([13])
321-
*/
322350

323351
expect(bitcoinAddress).toBeTruthy();
324352
expect(bitcoinAddress).toBeTypeOf('string');
325-
expect(bitcoinAddress).toSatisfy(isValidBtcAddress);
326-
});
353+
expect(bitcoinAddress).toSatisfy(isBtcAddress);
354+
});
355+
356+
it.each([
357+
[BitcoinAddressFamily.Legacy, m_legacy],
358+
[BitcoinAddressFamily.Segwit, m_segwit],
359+
[BitcoinAddressFamily.Bech32, m_bech32],
360+
[BitcoinAddressFamily.Taproot, m_taproot],
361+
] as const)(
362+
'should handle the type = $type argument',
363+
(type, regex) => {
364+
const bitcoinAddress = faker.finance.bitcoinAddress({
365+
type,
366+
});
367+
368+
expect(bitcoinAddress).toBeTruthy();
369+
expect(bitcoinAddress).toBeTypeOf('string');
370+
expect(bitcoinAddress).toSatisfy(isBtcAddress);
371+
expect(bitcoinAddress).toMatch(regex);
372+
}
373+
);
374+
375+
it.each([
376+
[BitcoinNetwork.Mainnet, [m_legacy, m_segwit, m_bech32, m_taproot]],
377+
[BitcoinNetwork.Testnet, [t_legacy, t_segwit, t_bech32, t_taproot]],
378+
] as const)(
379+
'should handle the network = $network argument',
380+
(network, regexes) => {
381+
const bitcoinAddress = faker.finance.bitcoinAddress({
382+
network,
383+
});
384+
385+
expect(bitcoinAddress).toBeTruthy();
386+
expect(bitcoinAddress).toBeTypeOf('string');
387+
expect(bitcoinAddress).toSatisfy(isBtcAddress);
388+
expect(bitcoinAddress).toSatisfy<string>((v) =>
389+
regexes.some((r) => r.test(v))
390+
);
391+
}
392+
);
393+
394+
it.each([
395+
[BitcoinAddressFamily.Legacy, BitcoinNetwork.Mainnet, m_legacy],
396+
[BitcoinAddressFamily.Legacy, BitcoinNetwork.Testnet, t_legacy],
397+
[BitcoinAddressFamily.Segwit, BitcoinNetwork.Mainnet, m_segwit],
398+
[BitcoinAddressFamily.Segwit, BitcoinNetwork.Testnet, t_segwit],
399+
[BitcoinAddressFamily.Bech32, BitcoinNetwork.Mainnet, m_bech32],
400+
[BitcoinAddressFamily.Bech32, BitcoinNetwork.Testnet, t_bech32],
401+
[BitcoinAddressFamily.Taproot, BitcoinNetwork.Mainnet, m_taproot],
402+
[BitcoinAddressFamily.Taproot, BitcoinNetwork.Testnet, t_taproot],
403+
] as const)(
404+
'should handle the type = $type and network = $network arguments',
405+
(type, network, regex) => {
406+
const bitcoinAddress = faker.finance.bitcoinAddress({
407+
type,
408+
network,
409+
});
410+
411+
expect(bitcoinAddress).toBeTruthy();
412+
expect(bitcoinAddress).toBeTypeOf('string');
413+
expect(bitcoinAddress).toSatisfy(isBtcAddress);
414+
expect(bitcoinAddress).toMatch(regex);
415+
}
416+
);
327417
});
328418

329419
describe('litecoinAddress()', () => {

0 commit comments

Comments
 (0)