Finished Regex portion

This commit is contained in:
junderw 2022-08-28 00:07:13 +09:00 committed by softsimon
parent 1339b98281
commit c0d3f295ee
No known key found for this signature in database
GPG key ID: 488D7DCFB5A430D7
2 changed files with 178 additions and 38 deletions

View file

@ -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<string>();
click$ = new Subject<string>();
@ -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)) {

View file

@ -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);
}