From c0d3f295eec2f483e8006b6f06de6fd8b3650a5b Mon Sep 17 00:00:00 2001 From: junderw Date: Sun, 28 Aug 2022 00:07:13 +0900 Subject: [PATCH] Finished Regex portion --- .../search-form/search-form.component.ts | 25 ++- frontend/src/app/shared/common.utils.ts | 191 ++++++++++++++---- 2 files changed, 178 insertions(+), 38 deletions(-) diff --git a/frontend/src/app/components/search-form/search-form.component.ts b/frontend/src/app/components/search-form/search-form.component.ts index ab42fe1f7..8031195f0 100644 --- a/frontend/src/app/components/search-form/search-form.component.ts +++ b/frontend/src/app/components/search-form/search-form.component.ts @@ -9,6 +9,7 @@ import { ElectrsApiService } from '../../services/electrs-api.service'; import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; import { ApiService } from '../../services/api.service'; import { SearchResultsComponent } from './search-results/search-results.component'; +import { ADDRESS_REGEXES, getRegex } from '../../shared/common.utils'; @Component({ selector: 'app-search-form', @@ -38,6 +39,7 @@ export class SearchFormComponent implements OnInit { regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/; regexTransaction = /^([a-fA-F0-9]{64})(:\d+)?$/; regexBlockheight = /^[0-9]{1,9}$/; + focus$ = new Subject(); click$ = new Subject(); @@ -58,8 +60,13 @@ export class SearchFormComponent implements OnInit { private elementRef: ElementRef, ) { } - ngOnInit(): void { - this.stateService.networkChanged$.subscribe((network) => this.network = network); + + ngOnInit() { + this.stateService.networkChanged$.subscribe((network) => { + this.network = network; + // TODO: Eventually change network type here from string to enum of consts + this.regexAddress = getRegex('address', network as any); + }); this.searchForm = this.formBuilder.group({ searchText: ['', Validators.required], @@ -203,6 +210,20 @@ export class SearchFormComponent implements OnInit { this.isSearching = true; if (!this.regexTransaction.test(searchText) && this.regexAddress.test(searchText)) { this.navigate('/address/', searchText); + } else if ( + // If the search text matches any other network besides this one + ADDRESS_REGEXES + .filter(([, network]) => network !== this.network) + .some(([regex]) => regex.test(searchText)) + ) { + // Gather all network matches as string[] + const networks = ADDRESS_REGEXES.filter(([regex, network]) => + network !== this.network && + regex.test(searchText) + ).map(([, network]) => network); + // ############################################### + // TODO: Create the search items for the drop down + // ############################################### } else if (this.regexBlockhash.test(searchText) || this.regexBlockheight.test(searchText)) { this.navigate('/block/', searchText); } else if (this.regexTransaction.test(searchText)) { diff --git a/frontend/src/app/shared/common.utils.ts b/frontend/src/app/shared/common.utils.ts index 6cbe35386..bbc9143c0 100644 --- a/frontend/src/app/shared/common.utils.ts +++ b/frontend/src/app/shared/common.utils.ts @@ -139,67 +139,186 @@ export function kmToMiles(km: number): number { } // all base58 characters -const BASE58_CHARS = '[a-km-zA-HJ-NP-Z1-9]'; +const BASE58_CHARS = `[a-km-zA-HJ-NP-Z1-9]`; + // all bech32 characters (after the separator) -const BECH32_CHARS = '[ac-hj-np-z02-9]'; -// All characters usable in bech32 human readable portion (before the 1 separator) -// Note: Technically the spec says "all US ASCII characters" but in practice only alphabet is used. -// Note: If HRP contains the separator (1) then the separator is "the last instance of separator" -const BECH32_HRP_CHARS = '[a-zA-Z0-9]'; +const BECH32_CHARS_LW = `[ac-hj-np-z02-9]`; +const BECH32_CHARS_UP = `[AC-HJ-NP-Z02-9]`; + // Hex characters -const HEX_CHARS = '[a-fA-F0-9]'; +const HEX_CHARS = `[a-fA-F0-9]`; + // A regex to say "A single 0 OR any number with no leading zeroes" -// (?: // Start a non-capturing group -// 0 // A single 0 -// | // OR -// [1-9][0-9]* // Any succession of numbers starting with 1-9 -// ) // End the non-capturing group. -const ZERO_INDEX_NUMBER_CHARS = '(?:0|[1-9][0-9]*)'; -export type RegexType = 'address' | 'blockhash' | 'transaction' | 'blockheight'; -export type Network = 'testnet' | 'signet' | 'liquid' | 'bisq' | 'mainnet'; -export function getRegex(type: RegexType, network: Network): RegExp { - let regex = '^'; // ^ = Start of string +// (?: // Start a non-capturing group +// 0 // A single 0 +// | // OR +// [1-9]\d* // Any succession of numbers starting with 1-9 +// ) // End the non-capturing group. +const ZERO_INDEX_NUMBER_CHARS = `(?:0|[1-9]\d*)`; + +// Formatting of the address regex is for readability, +// We should ignore formatting it with automated formatting tools like prettier. +// +// prettier-ignore +const ADDRESS_CHARS = { + mainnet: { + base58: `[13]` // Starts with a single 1 or 3 + + BASE58_CHARS + + `{26,33}`, // Repeat the previous char 26-33 times. + // Version byte 0x00 (P2PKH) can be as short as 27 characters, up to 34 length + // P2SH must be 34 length + bech32: `(?:` + + `bc1` // Starts with bc1 + + BECH32_CHARS_LW + + `{6,100}` // As per bech32, 6 char checksum is minimum + + `|` + + `BC1` // All upper case version + + BECH32_CHARS_UP + + `{6,100}` + + `)`, + }, + testnet: { + base58: `[mn2]` // Starts with a single m, n, or 2 (P2PKH is m or n, 2 is P2SH) + + BASE58_CHARS + + `{33,34}`, // m|n is 34 length, 2 is 35 length (We match the first letter separately) + bech32: `(?:` + + `tb1` // Starts with bc1 + + BECH32_CHARS_LW + + `{6,100}` // As per bech32, 6 char checksum is minimum + + `|` + + `TB1` // All upper case version + + BECH32_CHARS_UP + + `{6,100}` + + `)`, + }, + signet: { + base58: `[mn2]` + + BASE58_CHARS + + `{33,34}`, + bech32: `(?:` + + `tb1` // Starts with tb1 + + BECH32_CHARS_LW + + `{6,100}` + + `|` + + `TB1` // All upper case version + + BECH32_CHARS_UP + + `{6,100}` + + `)`, + }, + liquid: { + base58: `[GHPQ]` // G|H is P2PKH, P|Q is P2SH + + BASE58_CHARS + + `{33}`, // All min-max lengths are 34 + bech32: `(?:` + + `(?:` // bech32 liquid starts with ex or lq + + `ex` + + `|` + + `lq` + + `)` + + BECH32_CHARS_LW // blech32 and bech32 are the same alphabet and protocol, different checksums. + + `{6,100}` + + `|` + + `(?:` // Same as above but all upper case + + `EX` + + `|` + + `LQ` + + `)` + + BECH32_CHARS_UP + + `{6,100}` + + `)`, + }, + bisq: { + base58: `B1` // bisq base58 addrs start with B1 + + BASE58_CHARS + + `{33}`, // always length 35 + bech32: `(?:` + + `bbc1` // Starts with bbc1 + + BECH32_CHARS_LW + + `{6,100}` + + `|` + + `BBC1` // All upper case version + + BECH32_CHARS_UP + + `{6,100}` + + `)`, + }, +} +type RegexTypeNoAddr = `blockhash` | `transaction` | `blockheight`; +export type RegexType = `address` | RegexTypeNoAddr; + +export const NETWORKS = [`testnet`, `signet`, `liquid`, `bisq`, `mainnet`] as const; +export type Network = typeof NETWORKS[number]; // Turn const array into union type + +export const ADDRESS_REGEXES: [RegExp, string][] = NETWORKS + .map(network => [getRegex('address', network), network]) + +export function getRegex(type: RegexTypeNoAddr): RegExp; +export function getRegex(type: 'address', network: Network): RegExp; +export function getRegex(type: RegexType, network?: Network): RegExp { + let regex = `^`; // ^ = Start of string switch (type) { // Match a block height number // [Testing Order]: any order is fine - case 'blockheight': + case `blockheight`: regex += ZERO_INDEX_NUMBER_CHARS; // block height is a 0 indexed number break; // Match a 32 byte block hash in hex. Assumes at least 32 bits of difficulty. - // [Testing Order]: Must always be tested before 'transaction' - case 'blockhash': - regex += '0{8}'; // Starts with exactly 8 zeroes in a row + // [Testing Order]: Must always be tested before `transaction` + case `blockhash`: + regex += `0{8}`; // Starts with exactly 8 zeroes in a row regex += `${HEX_CHARS}{56}`; // Continues with exactly 56 hex letters/numbers break; // Match a 32 byte tx hash in hex. Contains optional output index specifier. - // [Testing Order]: Must always be tested after 'blockhash' - case 'transaction': + // [Testing Order]: Must always be tested after `blockhash` + case `transaction`: regex += `${HEX_CHARS}{64}`; // Exactly 64 hex letters/numbers - regex += '(?:'; // Start a non-capturing group - regex += ':'; // 1 instances of the symbol ":" + regex += `(?:`; // Start a non-capturing group + regex += `:`; // 1 instances of the symbol ":" regex += ZERO_INDEX_NUMBER_CHARS; // A zero indexed number - regex += ')?'; // End the non-capturing group. This group appears 0 or 1 times + regex += `)?`; // End the non-capturing group. This group appears 0 or 1 times break; - case 'address': - // TODO + // Match any one of the many address types + // [Testing Order]: While possible that a bech32 address happens to be 64 hex + // characters in the future (current lengths are not 64), it is highly unlikely + // Order therefore, does not matter. + case `address`: + if (!network) { + throw new Error(`Must pass network when type is address`); + } + regex += `(?:`; // Start a non-capturing group (each network has multiple options) switch (network) { - case 'mainnet': + case `mainnet`: + regex += ADDRESS_CHARS.mainnet.base58; + regex += `|`; // OR + regex += ADDRESS_CHARS.mainnet.bech32; break; - case 'testnet': + case `testnet`: + regex += ADDRESS_CHARS.testnet.base58; + regex += `|`; // OR + regex += ADDRESS_CHARS.testnet.bech32; break; - case 'signet': + case `signet`: + regex += ADDRESS_CHARS.signet.base58; + regex += `|`; // OR + regex += ADDRESS_CHARS.signet.bech32; break; - case 'liquid': + case `liquid`: + regex += ADDRESS_CHARS.liquid.base58; + regex += `|`; // OR + regex += ADDRESS_CHARS.liquid.bech32; break; - case 'bisq': + case `bisq`: + regex += ADDRESS_CHARS.bisq.base58; + regex += `|`; // OR + regex += ADDRESS_CHARS.bisq.bech32; break; default: - throw new Error('Invalid Network (Unreachable error in TypeScript)'); + throw new Error(`Invalid Network ${network} (Unreachable error in TypeScript)`); } + regex += `)`; // End the non-capturing group break; default: - throw new Error('Invalid RegexType (Unreachable error in TypeScript)'); + throw new Error(`Invalid RegexType ${type} (Unreachable error in TypeScript)`); } - regex += '$'; // $ = End of string + regex += `$`; // $ = End of string return new RegExp(regex); }