From badaa6d60593fe41a1afa0e9a8bef6ae5bb8a352 Mon Sep 17 00:00:00 2001 From: Shinigami Date: Mon, 3 Jun 2024 21:22:19 +0200 Subject: [PATCH 1/2] refactor(locale): unfreeze arrays (#2928) --- scripts/generate-locales.ts | 3 +-- src/locales/en/science/chemical_element.ts | 4 ++-- src/locales/en/science/unit.ts | 4 ++-- src/locales/eo/science/chemical_element.ts | 4 ++-- src/locales/eo/science/unit.ts | 4 ++-- src/locales/nb_NO/science/chemical_element.ts | 4 ++-- src/locales/nb_NO/science/unit.ts | 4 ++-- src/locales/pl/science/chemical_element.ts | 4 ++-- src/locales/pl/science/unit.ts | 4 ++-- src/locales/zh_CN/science/chemical_element.ts | 4 ++-- src/locales/zh_CN/science/unit.ts | 4 ++-- 11 files changed, 21 insertions(+), 22 deletions(-) diff --git a/scripts/generate-locales.ts b/scripts/generate-locales.ts index 933215a1ed3..2d6c252ecef 100644 --- a/scripts/generate-locales.ts +++ b/scripts/generate-locales.ts @@ -335,8 +335,7 @@ async function normalizeLocaleFile(filePath: string, definitionKey: string) { const isDynamicFile = compareString.startsWith('mergeArrays'); const isNonApplicable = compareString.startsWith('null'); - const isFrozenData = compareString.startsWith('Object.freeze'); - if (isDynamicFile || isNonApplicable || isFrozenData) { + if (isDynamicFile || isNonApplicable) { return; } diff --git a/src/locales/en/science/chemical_element.ts b/src/locales/en/science/chemical_element.ts index 06d84cf1479..06a90388ecf 100644 --- a/src/locales/en/science/chemical_element.ts +++ b/src/locales/en/science/chemical_element.ts @@ -1,4 +1,4 @@ -export default Object.freeze([ +export default [ { symbol: 'H', name: 'Hydrogen', @@ -589,4 +589,4 @@ export default Object.freeze([ name: 'Oganesson', atomicNumber: 118, }, -]); +]; diff --git a/src/locales/en/science/unit.ts b/src/locales/en/science/unit.ts index 005735e9a13..08ff8dc5f7a 100644 --- a/src/locales/en/science/unit.ts +++ b/src/locales/en/science/unit.ts @@ -1,4 +1,4 @@ -export default Object.freeze([ +export default [ { name: 'meter', symbol: 'm', @@ -115,4 +115,4 @@ export default Object.freeze([ name: 'katal', symbol: 'kat', }, -]); +]; diff --git a/src/locales/eo/science/chemical_element.ts b/src/locales/eo/science/chemical_element.ts index 33736ed5893..949eda6bb9d 100644 --- a/src/locales/eo/science/chemical_element.ts +++ b/src/locales/eo/science/chemical_element.ts @@ -1,4 +1,4 @@ -export default Object.freeze([ +export default [ { symbol: 'H', name: 'hidrogeno', @@ -589,4 +589,4 @@ export default Object.freeze([ name: 'oganesono', atomicNumber: 118, }, -]); +]; diff --git a/src/locales/eo/science/unit.ts b/src/locales/eo/science/unit.ts index 25c9296cc0d..3792a4891d5 100644 --- a/src/locales/eo/science/unit.ts +++ b/src/locales/eo/science/unit.ts @@ -1,4 +1,4 @@ -export default Object.freeze([ +export default [ { name: 'metro', symbol: 'm', @@ -115,4 +115,4 @@ export default Object.freeze([ name: 'katalo', symbol: 'kat', }, -]); +]; diff --git a/src/locales/nb_NO/science/chemical_element.ts b/src/locales/nb_NO/science/chemical_element.ts index c7d903b638e..930c3a70577 100644 --- a/src/locales/nb_NO/science/chemical_element.ts +++ b/src/locales/nb_NO/science/chemical_element.ts @@ -1,4 +1,4 @@ -export default Object.freeze([ +export default [ { symbol: 'H', name: 'Hydrogen', @@ -589,4 +589,4 @@ export default Object.freeze([ name: 'Oganesson', atomicNumber: 118, }, -]); +]; diff --git a/src/locales/nb_NO/science/unit.ts b/src/locales/nb_NO/science/unit.ts index 1db4a834c0d..d217064fc68 100644 --- a/src/locales/nb_NO/science/unit.ts +++ b/src/locales/nb_NO/science/unit.ts @@ -1,4 +1,4 @@ -export default Object.freeze([ +export default [ { name: 'meter', symbol: 'm', @@ -87,4 +87,4 @@ export default Object.freeze([ name: 'sievert', symbol: 'Sv', }, -]); +]; diff --git a/src/locales/pl/science/chemical_element.ts b/src/locales/pl/science/chemical_element.ts index 82a30a4600c..c9209fb1ee3 100644 --- a/src/locales/pl/science/chemical_element.ts +++ b/src/locales/pl/science/chemical_element.ts @@ -1,4 +1,4 @@ -export default Object.freeze([ +export default [ { symbol: 'H', name: 'Wodór', @@ -589,4 +589,4 @@ export default Object.freeze([ name: 'Oganeson', atomicNumber: 118, }, -]); +]; diff --git a/src/locales/pl/science/unit.ts b/src/locales/pl/science/unit.ts index f81f4d3b105..ab9fb2ec607 100644 --- a/src/locales/pl/science/unit.ts +++ b/src/locales/pl/science/unit.ts @@ -1,4 +1,4 @@ -export default Object.freeze([ +export default [ { name: 'metr', symbol: 'm', @@ -87,4 +87,4 @@ export default Object.freeze([ name: 'siwert', symbol: 'Sv', }, -]); +]; diff --git a/src/locales/zh_CN/science/chemical_element.ts b/src/locales/zh_CN/science/chemical_element.ts index 7d653d60813..6d450d05c5b 100644 --- a/src/locales/zh_CN/science/chemical_element.ts +++ b/src/locales/zh_CN/science/chemical_element.ts @@ -1,4 +1,4 @@ -export default Object.freeze([ +export default [ { symbol: 'H', name: '氢', @@ -589,4 +589,4 @@ export default Object.freeze([ name: '鿫', atomicNumber: 118, }, -]); +]; diff --git a/src/locales/zh_CN/science/unit.ts b/src/locales/zh_CN/science/unit.ts index 98e60e23562..b9bc83351da 100644 --- a/src/locales/zh_CN/science/unit.ts +++ b/src/locales/zh_CN/science/unit.ts @@ -1,4 +1,4 @@ -export default Object.freeze([ +export default [ { name: '米', symbol: 'm', @@ -115,4 +115,4 @@ export default Object.freeze([ name: '开特', symbol: 'kat', }, -]); +]; From 3ae93934bc4cf5f6414acfa28ea727f758d18756 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Sim=C3=A3o?= Date: Wed, 5 Jun 2024 09:01:26 +0100 Subject: [PATCH 2/2] feat(bitcoinAddress): multiple bitcoin address types and testnet (#2922) --- src/index.ts | 8 ++ src/modules/finance/bitcoin.ts | 72 ++++++++++++ src/modules/finance/index.ts | 53 +++++++-- .../__snapshots__/finance.spec.ts.snap | 18 ++- test/modules/finance.spec.ts | 106 ++++++++++++++++-- 5 files changed, 235 insertions(+), 22 deletions(-) create mode 100644 src/modules/finance/bitcoin.ts diff --git a/src/index.ts b/src/index.ts index b5676f4475a..2db17030540 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 00000000000..f7cfaf70ac3 --- /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; + 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 b78b6a3f18b..4c84fe4c148 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 2f394c08bab..1312820a5ad 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 7f8a8255716..ee19b17a843 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((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()', () => {