Skip to content

Commit

Permalink
feat(bitcoinAddress): multiple bitcoin address types and testnet (#2922)
Browse files Browse the repository at this point in the history
  • Loading branch information
madoke authored Jun 5, 2024
1 parent badaa6d commit 3ae9393
Show file tree
Hide file tree
Showing 5 changed files with 235 additions and 22 deletions.
8 changes: 8 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
72 changes: 72 additions & 0 deletions src/modules/finance/bitcoin.ts
Original file line number Diff line number Diff line change
@@ -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',
},
};
53 changes: 42 additions & 11 deletions src/modules/finance/index.ts
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand Down Expand Up @@ -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;
}

/**
Expand Down
18 changes: 15 additions & 3 deletions test/modules/__snapshots__/finance.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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"`;

Expand Down Expand Up @@ -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"`;

Expand Down Expand Up @@ -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"`;

Expand Down
106 changes: 98 additions & 8 deletions test/modules/finance.spec.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -21,7 +24,6 @@ describe('finance', () => {
'currencyCode',
'currencyName',
'currencySymbol',
'bitcoinAddress',
'litecoinAddress',
'creditCardCVV',
'ethereumAddress',
Expand Down Expand Up @@ -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()))(
Expand Down Expand Up @@ -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()', () => {
Expand Down

0 comments on commit 3ae9393

Please sign in to comment.