aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Simão <[email protected]>2024-06-05 09:01:26 +0100
committerGitHub <[email protected]>2024-06-05 10:01:26 +0200
commit3ae93934bc4cf5f6414acfa28ea727f758d18756 (patch)
treede1c649f829dbcdca838c4fedcac76c3450ccff6
parentbadaa6d60593fe41a1afa0e9a8bef6ae5bb8a352 (diff)
downloadfaker-3ae93934bc4cf5f6414acfa28ea727f758d18756.tar.xz
faker-3ae93934bc4cf5f6414acfa28ea727f758d18756.zip
feat(bitcoinAddress): multiple bitcoin address types and testnet (#2922)
-rw-r--r--src/index.ts8
-rw-r--r--src/modules/finance/bitcoin.ts72
-rw-r--r--src/modules/finance/index.ts53
-rw-r--r--test/modules/__snapshots__/finance.spec.ts.snap18
-rw-r--r--test/modules/finance.spec.ts106
5 files changed, 235 insertions, 22 deletions
diff --git a/src/index.ts b/src/index.ts
index b5676f44..2db17030 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -55,6 +55,14 @@ export type { DatabaseModule } from './modules/database';
export type { DatatypeModule } from './modules/datatype';
export type { DateModule, SimpleDateModule } from './modules/date';
export type { Currency, FinanceModule } from './modules/finance';
+export {
+ BitcoinAddressFamily,
+ BitcoinNetwork,
+} from './modules/finance/bitcoin';
+export type {
+ BitcoinAddressFamilyType,
+ BitcoinNetworkType,
+} from './modules/finance/bitcoin';
export type { FoodModule } from './modules/food';
export type { GitModule } from './modules/git';
export type { HackerModule } from './modules/hacker';
diff --git a/src/modules/finance/bitcoin.ts b/src/modules/finance/bitcoin.ts
new file mode 100644
index 00000000..f7cfaf70
--- /dev/null
+++ b/src/modules/finance/bitcoin.ts
@@ -0,0 +1,72 @@
+import type { Casing } from '../string';
+
+/**
+ * The bitcoin address families.
+ */
+export enum BitcoinAddressFamily {
+ Legacy = 'legacy',
+ Segwit = 'segwit',
+ Bech32 = 'bech32',
+ Taproot = 'taproot',
+}
+
+/**
+ * The bitcoin address families.
+ */
+export type BitcoinAddressFamilyType = `${BitcoinAddressFamily}`;
+
+/**
+ * The different bitcoin networks.
+ */
+export enum BitcoinNetwork {
+ Mainnet = 'mainnet',
+ Testnet = 'testnet',
+}
+
+/**
+ * The different bitcoin networks.
+ */
+export type BitcoinNetworkType = `${BitcoinNetwork}`;
+
+type BitcoinAddressOptions = {
+ prefix: Record<BitcoinNetworkType, string>;
+ length: { min: number; max: number };
+ casing: Casing;
+ exclude: string;
+};
+
+export const BitcoinAddressSpecs: Record<
+ BitcoinAddressFamilyType,
+ BitcoinAddressOptions
+> = {
+ [BitcoinAddressFamily.Legacy]: {
+ prefix: { [BitcoinNetwork.Mainnet]: '1', [BitcoinNetwork.Testnet]: 'm' },
+ length: { min: 26, max: 34 },
+ casing: 'mixed',
+ exclude: '0OIl',
+ },
+ [BitcoinAddressFamily.Segwit]: {
+ prefix: { [BitcoinNetwork.Mainnet]: '3', [BitcoinNetwork.Testnet]: '2' },
+ length: { min: 26, max: 34 },
+ casing: 'mixed',
+ exclude: '0OIl',
+ },
+ [BitcoinAddressFamily.Bech32]: {
+ prefix: {
+ [BitcoinNetwork.Mainnet]: 'bc1',
+ [BitcoinNetwork.Testnet]: 'tb1',
+ },
+ length: { min: 42, max: 42 },
+ casing: 'lower',
+ exclude: '1bBiIoO',
+ },
+ [BitcoinAddressFamily.Taproot]: {
+ prefix: {
+ [BitcoinNetwork.Mainnet]: 'bc1p',
+ [BitcoinNetwork.Testnet]: 'tb1p',
+ },
+ length: { min: 62, max: 62 },
+ casing: 'lower',
+ exclude: '1bBiIoO',
+ },
+};
diff --git a/src/modules/finance/index.ts b/src/modules/finance/index.ts
index b78b6a3f..4c84fe4c 100644
--- a/src/modules/finance/index.ts
+++ b/src/modules/finance/index.ts
@@ -1,5 +1,11 @@
import { FakerError } from '../../errors/faker-error';
import { ModuleBase } from '../../internal/module-base';
+import type { BitcoinAddressFamilyType, BitcoinNetworkType } from './bitcoin';
+import {
+ BitcoinAddressFamily,
+ BitcoinAddressSpecs,
+ BitcoinNetwork,
+} from './bitcoin';
import iban from './iban';
/**
@@ -486,23 +492,48 @@ export class FinanceModule extends ModuleBase {
/**
* Generates a random Bitcoin address.
*
+ * @param options An optional options object.
+ * @param options.type The bitcoin address type (`'legacy'`, `'sewgit'`, `'bech32'` or `'taproot'`). Defaults to a random address type.
+ * @param options.network The bitcoin network (`'mainnet'` or `'testnet'`). Defaults to `'mainnet'`.
+ *
* @example
- * faker.finance.bitcoinAddress() // '3ySdvCkTLVy7gKD4j6JfSaf5d'
+ * faker.finance.bitcoinAddress() // '1TeZEFLmGPLEQrSRdAcnZLoWwYeiHwmRog'
+ * faker.finance.bitcoinAddress({ type: 'bech32' }) // 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'
+ * faker.finance.bitcoinAddress({ type: 'bech32', network: 'testnet' }) // 'tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx'
*
* @since 3.1.0
*/
- bitcoinAddress(): string {
- const addressLength = this.faker.number.int({ min: 25, max: 39 });
-
- let address = this.faker.helpers.arrayElement(['1', '3']);
-
- address += this.faker.string.alphanumeric({
- length: addressLength,
- casing: 'mixed',
- exclude: '0OIl',
+ bitcoinAddress(
+ options: {
+ /**
+ * The bitcoin address type (`'legacy'`, `'sewgit'`, `'bech32'` or `'taproot'`).
+ *
+ * @default faker.helpers.arrayElement(['legacy','sewgit','bech32','taproot'])
+ */
+ type?: BitcoinAddressFamilyType;
+ /**
+ * The bitcoin network (`'mainnet'` or `'testnet'`).
+ *
+ * @default 'mainnet'
+ */
+ network?: BitcoinNetworkType;
+ } = {}
+ ): string {
+ const {
+ type = this.faker.helpers.enumValue(BitcoinAddressFamily),
+ network = BitcoinNetwork.Mainnet,
+ } = options;
+ const addressSpec = BitcoinAddressSpecs[type];
+ const addressPrefix = addressSpec.prefix[network];
+ const addressLength = this.faker.number.int(addressSpec.length);
+
+ const address = this.faker.string.alphanumeric({
+ length: addressLength - addressPrefix.length,
+ casing: addressSpec.casing,
+ exclude: addressSpec.exclude,
});
- return address;
+ return addressPrefix + address;
}
/**
diff --git a/test/modules/__snapshots__/finance.spec.ts.snap b/test/modules/__snapshots__/finance.spec.ts.snap
index 2f394c08..1312820a 100644
--- a/test/modules/__snapshots__/finance.spec.ts.snap
+++ b/test/modules/__snapshots__/finance.spec.ts.snap
@@ -24,7 +24,11 @@ exports[`finance > 42 > bic > noArgs 1`] = `"YTPECC2VXXX"`;
exports[`finance > 42 > bic > with branch code 1`] = `"JYTPCD52XXX"`;
-exports[`finance > 42 > bitcoinAddress 1`] = `"3JAaa4SAH2YQdbbiwrhB9hnsMcvA3Ba"`;
+exports[`finance > 42 > bitcoinAddress > noArgs 1`] = `"3JAaa4SAH2YQdbbiwrhB9hnsMcvA3Ba4XY"`;
+
+exports[`finance > 42 > bitcoinAddress > with type and network option 1`] = `"1XJAaa4SAH2YQdbbiwrhB9hnsMcvA"`;
+
+exports[`finance > 42 > bitcoinAddress > with type option 1`] = `"1XJAaa4SAH2YQdbbiwrhB9hnsMcvA"`;
exports[`finance > 42 > creditCardCVV 1`] = `"397"`;
@@ -108,7 +112,11 @@ exports[`finance > 1211 > bic > noArgs 1`] = `"XFZROMRC"`;
exports[`finance > 1211 > bic > with branch code 1`] = `"YXFZNPOROTR"`;
-exports[`finance > 1211 > bitcoinAddress 1`] = `"3eZEFLmGPLEQrSRdAcnZLoWwYeiHwmRogjbyG9G"`;
+exports[`finance > 1211 > bitcoinAddress > noArgs 1`] = `"bc1pw8zppsdqusnufvv7l7dzsexkz8aqjdve9a6kq5qh8f7vlh2q6q9sjg7mv4"`;
+
+exports[`finance > 1211 > bitcoinAddress > with type and network option 1`] = `"1TeZEFLmGPLEQrSRdAcnZLoWwYeiHwmRog"`;
+
+exports[`finance > 1211 > bitcoinAddress > with type option 1`] = `"1TeZEFLmGPLEQrSRdAcnZLoWwYeiHwmRog"`;
exports[`finance > 1211 > creditCardCVV 1`] = `"982"`;
@@ -192,7 +200,11 @@ exports[`finance > 1337 > bic > noArgs 1`] = `"EHLILK9ZXXX"`;
exports[`finance > 1337 > bic > with branch code 1`] = `"GEHLGGI9XXX"`;
-exports[`finance > 1337 > bitcoinAddress 1`] = `"1hsjwgYJ7oC8ZrMNmqzLbhEubpcwQ"`;
+exports[`finance > 1337 > bitcoinAddress > noArgs 1`] = `"3hsjwgYJ7oC8ZrMNmqzLbhEubpc"`;
+
+exports[`finance > 1337 > bitcoinAddress > with type and network option 1`] = `"1ahsjwgYJ7oC8ZrMNmqzLbhEubpc"`;
+
+exports[`finance > 1337 > bitcoinAddress > with type option 1`] = `"1ahsjwgYJ7oC8ZrMNmqzLbhEubpc"`;
exports[`finance > 1337 > creditCardCVV 1`] = `"212"`;
diff --git a/test/modules/finance.spec.ts b/test/modules/finance.spec.ts
index 7f8a8255..ee19b17a 100644
--- a/test/modules/finance.spec.ts
+++ b/test/modules/finance.spec.ts
@@ -1,8 +1,11 @@
-import isValidBtcAddress from 'validator/lib/isBtcAddress';
import isCreditCard from 'validator/lib/isCreditCard';
import { describe, expect, it } from 'vitest';
import { faker, fakerZH_CN } from '../../src';
import { FakerError } from '../../src/errors/faker-error';
+import {
+ BitcoinAddressFamily,
+ BitcoinNetwork,
+} from '../../src/modules/finance/bitcoin';
import ibanLib from '../../src/modules/finance/iban';
import { luhnCheck } from '../../src/modules/helpers/luhn-check';
import { seededTests } from '../support/seeded-runs';
@@ -21,7 +24,6 @@ describe('finance', () => {
'currencyCode',
'currencyName',
'currencySymbol',
- 'bitcoinAddress',
'litecoinAddress',
'creditCardCVV',
'ethereumAddress',
@@ -91,6 +93,15 @@ describe('finance', () => {
ellipsis: true,
});
});
+
+ t.describe('bitcoinAddress', (t) => {
+ t.it('noArgs')
+ .it('with type option', { type: BitcoinAddressFamily.Legacy })
+ .it('with type and network option', {
+ type: BitcoinAddressFamily.Legacy,
+ network: BitcoinNetwork.Mainnet,
+ });
+ });
});
describe.each(times(NON_SEEDED_BASED_RUN).map(() => faker.seed()))(
@@ -313,17 +324,96 @@ describe('finance', () => {
});
describe('bitcoinAddress()', () => {
+ const m_legacy = /^1[A-HJ-NP-Za-km-z1-9]{25,39}$/;
+ const t_legacy = /^m[A-HJ-NP-Za-km-z1-9]{25,39}$/;
+ const m_segwit = /^3[A-HJ-NP-Za-km-z1-9]{25,39}$/;
+ const t_segwit = /^2[A-HJ-NP-Za-km-z1-9]{25,39}$/;
+ const m_bech32 = /^bc1[ac-hj-np-z02-9]{39,39}$/;
+ const t_bech32 = /^tb1[ac-hj-np-z02-9]{39,39}$/;
+ const m_taproot = /^bc1p[ac-hj-np-z02-9]{58,58}$/;
+ const t_taproot = /^tb1p[ac-hj-np-z02-9]{58,58}$/;
+
+ const isBtcAddress = (address: string) =>
+ [
+ m_legacy,
+ t_legacy,
+ m_segwit,
+ t_segwit,
+ m_bech32,
+ t_bech32,
+ m_taproot,
+ t_taproot,
+ ].some((r) => r.test(address));
+
it('should return a valid bitcoin address', () => {
const bitcoinAddress = faker.finance.bitcoinAddress();
- /**
- * Note: Although the total length of a Bitcoin address can be 25-33 characters, regex quantifiers only check the preceding token
- * Therefore we take one from the total length of the address not including the first character ([13])
- */
expect(bitcoinAddress).toBeTruthy();
expect(bitcoinAddress).toBeTypeOf('string');
- expect(bitcoinAddress).toSatisfy(isValidBtcAddress);
- });
+ expect(bitcoinAddress).toSatisfy(isBtcAddress);
+ });
+
+ it.each([
+ [BitcoinAddressFamily.Legacy, m_legacy],
+ [BitcoinAddressFamily.Segwit, m_segwit],
+ [BitcoinAddressFamily.Bech32, m_bech32],
+ [BitcoinAddressFamily.Taproot, m_taproot],
+ ] as const)(
+ 'should handle the type = $type argument',
+ (type, regex) => {
+ const bitcoinAddress = faker.finance.bitcoinAddress({
+ type,
+ });
+
+ expect(bitcoinAddress).toBeTruthy();
+ expect(bitcoinAddress).toBeTypeOf('string');
+ expect(bitcoinAddress).toSatisfy(isBtcAddress);
+ expect(bitcoinAddress).toMatch(regex);
+ }
+ );
+
+ it.each([
+ [BitcoinNetwork.Mainnet, [m_legacy, m_segwit, m_bech32, m_taproot]],
+ [BitcoinNetwork.Testnet, [t_legacy, t_segwit, t_bech32, t_taproot]],
+ ] as const)(
+ 'should handle the network = $network argument',
+ (network, regexes) => {
+ const bitcoinAddress = faker.finance.bitcoinAddress({
+ network,
+ });
+
+ expect(bitcoinAddress).toBeTruthy();
+ expect(bitcoinAddress).toBeTypeOf('string');
+ expect(bitcoinAddress).toSatisfy(isBtcAddress);
+ expect(bitcoinAddress).toSatisfy<string>((v) =>
+ regexes.some((r) => r.test(v))
+ );
+ }
+ );
+
+ it.each([
+ [BitcoinAddressFamily.Legacy, BitcoinNetwork.Mainnet, m_legacy],
+ [BitcoinAddressFamily.Legacy, BitcoinNetwork.Testnet, t_legacy],
+ [BitcoinAddressFamily.Segwit, BitcoinNetwork.Mainnet, m_segwit],
+ [BitcoinAddressFamily.Segwit, BitcoinNetwork.Testnet, t_segwit],
+ [BitcoinAddressFamily.Bech32, BitcoinNetwork.Mainnet, m_bech32],
+ [BitcoinAddressFamily.Bech32, BitcoinNetwork.Testnet, t_bech32],
+ [BitcoinAddressFamily.Taproot, BitcoinNetwork.Mainnet, m_taproot],
+ [BitcoinAddressFamily.Taproot, BitcoinNetwork.Testnet, t_taproot],
+ ] as const)(
+ 'should handle the type = $type and network = $network arguments',
+ (type, network, regex) => {
+ const bitcoinAddress = faker.finance.bitcoinAddress({
+ type,
+ network,
+ });
+
+ expect(bitcoinAddress).toBeTruthy();
+ expect(bitcoinAddress).toBeTypeOf('string');
+ expect(bitcoinAddress).toSatisfy(isBtcAddress);
+ expect(bitcoinAddress).toMatch(regex);
+ }
+ );
});
describe('litecoinAddress()', () => {