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 cb64df7f4..4ed6b1d64 100644 --- a/frontend/src/app/components/search-form/search-form.component.ts +++ b/frontend/src/app/components/search-form/search-form.component.ts @@ -38,11 +38,11 @@ export class SearchFormComponent implements OnInit { } regexAddress = getRegex('address', 'mainnet'); // Default to mainnet - regexBlockhash = getRegex('blockhash'); + regexBlockhash = getRegex('blockhash', 'mainnet'); regexTransaction = getRegex('transaction'); regexBlockheight = getRegex('blockheight'); - regexDate = /^(?:\d{4}[-/]\d{1,2}[-/]\d{1,2}(?: \d{1,2}:\d{2})?)$/; - regexUnixTimestamp = /^\d{10}$/; + regexDate = getRegex('date'); + regexUnixTimestamp = getRegex('timestamp'); focus$ = new Subject(); click$ = new Subject(); @@ -72,6 +72,7 @@ export class SearchFormComponent implements OnInit { this.network = network; // TODO: Eventually change network type here from string to enum of consts this.regexAddress = getRegex('address', network as any || 'mainnet'); + this.regexBlockhash = getRegex('blockhash', network as any || 'mainnet'); }); this.router.events.subscribe((e: NavigationStart) => { // Reset search focus when changing page @@ -181,8 +182,8 @@ export class SearchFormComponent implements OnInit { const lightningResults = result[1]; const matchesBlockHeight = this.regexBlockheight.test(searchText) && parseInt(searchText) <= this.stateService.latestBlockHeight; - const matchesDateTime = this.regexDate.test(searchText) && new Date(searchText).toString() !== 'Invalid Date'; - const matchesUnixTimestamp = this.regexUnixTimestamp.test(searchText); + const matchesDateTime = this.regexDate.test(searchText) && new Date(searchText).toString() !== 'Invalid Date' && new Date(searchText).getTime() <= Date.now() && new Date(searchText).getTime() >= 1231006505000; + const matchesUnixTimestamp = this.regexUnixTimestamp.test(searchText) && parseInt(searchText) <= Math.floor(Date.now() / 1000) && parseInt(searchText) >= 1231006505; // 1231006505 is the timestamp of the genesis block const matchesTxId = this.regexTransaction.test(searchText) && !this.regexBlockhash.test(searchText); const matchesBlockHash = this.regexBlockhash.test(searchText); const matchesAddress = !matchesTxId && this.regexAddress.test(searchText); @@ -237,7 +238,7 @@ export class SearchFormComponent implements OnInit { if (searchText) { this.isSearching = true; - const otherNetworks = findOtherNetworks(searchText, this.network as any); + const otherNetworks = findOtherNetworks(searchText, this.network as any || 'mainnet'); if (!this.regexTransaction.test(searchText) && this.regexAddress.test(searchText)) { this.navigate('/address/', searchText); } else if (otherNetworks.length > 0) { @@ -269,6 +270,11 @@ export class SearchFormComponent implements OnInit { } else if (this.regexDate.test(searchText) || this.regexUnixTimestamp.test(searchText)) { let timestamp: number; this.regexDate.test(searchText) ? timestamp = Math.floor(new Date(searchText).getTime() / 1000) : timestamp = searchText; + // Check if timestamp is too far in the future or before the genesis block + if (timestamp > Math.floor(Date.now() / 1000) || timestamp < 1231006505) { + this.isSearching = false; + return; + } this.apiService.getBlockDataFromTimestamp$(timestamp).subscribe( (data) => { this.navigate('/block/', data.hash); }, (error) => { console.log(error); this.isSearching = false; } diff --git a/frontend/src/app/shared/pipes/relative-url/relative-url.pipe.ts b/frontend/src/app/shared/pipes/relative-url/relative-url.pipe.ts index 83f5f20df..4211765df 100644 --- a/frontend/src/app/shared/pipes/relative-url/relative-url.pipe.ts +++ b/frontend/src/app/shared/pipes/relative-url/relative-url.pipe.ts @@ -12,6 +12,7 @@ export class RelativeUrlPipe implements PipeTransform { transform(value: string, swapNetwork?: string): string { let network = swapNetwork || this.stateService.network; + if (network === 'mainnet') network = ''; if (this.stateService.env.BASE_MODULE === 'liquid' && network === 'liquidtestnet') { network = 'testnet'; } else if (this.stateService.env.BASE_MODULE !== 'mempool') { diff --git a/frontend/src/app/shared/regex.utils.ts b/frontend/src/app/shared/regex.utils.ts index bac256c8d..d0cd08f24 100644 --- a/frontend/src/app/shared/regex.utils.ts +++ b/frontend/src/app/shared/regex.utils.ts @@ -9,13 +9,16 @@ const BECH32_CHARS_UP = `[AC-HJ-NP-Z02-9]`; const HEX_CHARS = `[a-fA-F0-9]`; // A regex to say "A single 0 OR any number with no leading zeroes" -// Capped at 13 digits so as to not be confused with lightning channel IDs (which are around 17 digits) +// Capped at 9 digits so as to not be confused with lightning channel IDs (which are around 17 digits) // (?: // Start a non-capturing group // 0 // A single 0 // | // OR -// [1-9][0-9]{0,12} // Any succession of numbers up to 13 digits starting with 1-9 +// [1-9][0-9]{0,8} // Any succession of numbers up to 9 digits starting with 1-9 // ) // End the non-capturing group. -const ZERO_INDEX_NUMBER_CHARS = `(?:0|[1-9][0-9]{0,12})`; +const ZERO_INDEX_NUMBER_CHARS = `(?:0|[1-9][0-9]{0,8})`; + +// Simple digits only regex +const NUMBER_CHARS = `[0-9]`; // Formatting of the address regex is for readability, // We should ignore formatting it with automated formatting tools like prettier. @@ -48,7 +51,7 @@ const ADDRESS_CHARS: { + BASE58_CHARS + `{33,34}`, // m|n is 34 length, 2 is 35 length (We match the first letter separately) bech32: `(?:` - + `tb1` // Starts with bc1 + + `tb1` // Starts with tb1 + BECH32_CHARS_LW + `{6,100}` // As per bech32, 6 char checksum is minimum + `|` @@ -76,18 +79,18 @@ const ADDRESS_CHARS: { + BASE58_CHARS + `{33}`, // All min-max lengths are 34 bech32: `(?:` - + `(?:` // bech32 liquid starts with ex or lq - + `ex` + + `(?:` // bech32 liquid starts with ex1 or lq1 + + `ex1` + `|` - + `lq` + + `lq1` + `)` + BECH32_CHARS_LW // blech32 and bech32 are the same alphabet and protocol, different checksums. + `{6,100}` + `|` + `(?:` // Same as above but all upper case - + `EX` + + `EX1` + `|` - + `LQ` + + `LQ1` + `)` + BECH32_CHARS_UP + `{6,100}` @@ -99,39 +102,39 @@ const ADDRESS_CHARS: { + `{33}`, // P2PKH is ???(TODO: find size), P2SH is 34 bech32: `(?:` + `(?:` // bech32 liquid testnet starts with tex or tlq - + `tex` // TODO: Why does mempool use this and not ert|el like in the elements source? + + `tex1` // TODO: Why does mempool use this and not ert|el like in the elements source? + `|` - + `tlq` // TODO: does this exist? + + `tlq1` // TODO: does this exist? + `)` + BECH32_CHARS_LW // blech32 and bech32 are the same alphabet and protocol, different checksums. + `{6,100}` + `|` + `(?:` // Same as above but all upper case - + `TEX` + + `TEX1` + `|` - + `TLQ` + + `TLQ1` + `)` + BECH32_CHARS_UP + `{6,100}` + `)`, }, bisq: { - base58: `B1` // bisq base58 addrs start with B1 + base58: `(?:[bB][13]` // b or B at the start, followed by a single 1 or 3 + BASE58_CHARS - + `{33}`, // always length 35 + + `{26,33})`, bech32: `(?:` - + `bbc1` // Starts with bbc1 + + `[bB]bc1` // b or B at the start, followed by bc1 + BECH32_CHARS_LW - + `{6,100}` + + `{6,100}` + `|` - + `BBC1` // All upper case version + + `[bB]BC1` // b or B at the start, followed by BC1 + BECH32_CHARS_UP + `{6,100}` + `)`, }, } -type RegexTypeNoAddr = `blockhash` | `transaction` | `blockheight`; -export type RegexType = `address` | RegexTypeNoAddr; +type RegexTypeNoAddrNoBlockHash = | `transaction` | `blockheight` | `date` | `timestamp`; +export type RegexType = `address` | `blockhash` | RegexTypeNoAddrNoBlockHash; export const NETWORKS = [`testnet`, `signet`, `liquid`, `liquidtestnet`, `bisq`, `mainnet`] as const; export type Network = typeof NETWORKS[number]; // Turn const array into union type @@ -139,15 +142,15 @@ export type Network = typeof NETWORKS[number]; // Turn const array into union ty export const ADDRESS_REGEXES: [RegExp, Network][] = NETWORKS .map(network => [getRegex('address', network), network]) -export function findOtherNetworks(address: string, skipNetwork: Network): Network[] { - return ADDRESS_REGEXES.filter(([regex, network]) => - network !== skipNetwork && - regex.test(address) - ).map(([, network]) => network); +export function findOtherNetworks(address: string, skipNetwork: Network): {network: Network, address: string}[] { + return ADDRESS_REGEXES + .filter(([regex, network]) => network !== skipNetwork && regex.test(address)) + .map(([, network]) => ({ network, address })); } -export function getRegex(type: RegexTypeNoAddr): RegExp; +export function getRegex(type: RegexTypeNoAddrNoBlockHash): RegExp; export function getRegex(type: 'address', network: Network): RegExp; +export function getRegex(type: 'blockhash', network: Network): RegExp; export function getRegex(type: RegexType, network?: Network): RegExp { let regex = `^`; // ^ = Start of string switch (type) { @@ -156,11 +159,37 @@ export function getRegex(type: RegexType, network?: Network): RegExp { 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. + // Match a 32 byte block hash in hex. // [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 + if (!network) { + throw new Error(`Must pass network when type is blockhash`); + } + let leadingZeroes: number; + switch (network) { + case `mainnet`: + leadingZeroes = 8; // Assumes at least 32 bits of difficulty + break; + case `testnet`: + leadingZeroes = 8; // Assumes at least 32 bits of difficulty + break; + case `signet`: + leadingZeroes = 5; + break; + case `liquid`: + leadingZeroes = 8; // We are not interested in Liquid block hashes + break; + case `liquidtestnet`: + leadingZeroes = 8; // We are not interested in Liquid block hashes + break; + case `bisq`: + leadingZeroes = 8; // Assumes at least 32 bits of difficulty + break; + default: + throw new Error(`Invalid Network ${network} (Unreachable error in TypeScript)`); + } + regex += `0{${leadingZeroes}}`; + regex += `${HEX_CHARS}{${64 - leadingZeroes}}`; // Exactly 64 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` @@ -185,16 +214,28 @@ export function getRegex(type: RegexType, network?: Network): RegExp { regex += ADDRESS_CHARS.mainnet.base58; regex += `|`; // OR regex += ADDRESS_CHARS.mainnet.bech32; + regex += `|`; // OR + regex += `04${HEX_CHARS}{128}`; // Uncompressed pubkey + regex += `|`; // OR + regex += `(?:02|03)${HEX_CHARS}{64}`; // Compressed pubkey break; case `testnet`: regex += ADDRESS_CHARS.testnet.base58; regex += `|`; // OR regex += ADDRESS_CHARS.testnet.bech32; + regex += `|`; // OR + regex += `04${HEX_CHARS}{128}`; // Uncompressed pubkey + regex += `|`; // OR + regex += `(?:02|03)${HEX_CHARS}{64}`; // Compressed pubkey break; case `signet`: regex += ADDRESS_CHARS.signet.base58; regex += `|`; // OR regex += ADDRESS_CHARS.signet.bech32; + regex += `|`; // OR + regex += `04${HEX_CHARS}{128}`; // Uncompressed pubkey + regex += `|`; // OR + regex += `(?:02|03)${HEX_CHARS}{64}`; // Compressed pubkey break; case `liquid`: regex += ADDRESS_CHARS.liquid.base58; @@ -216,6 +257,28 @@ export function getRegex(type: RegexType, network?: Network): RegExp { } regex += `)`; // End the non-capturing group break; + // Match a date in the format YYYY-MM-DD (optional: HH:MM) + // [Testing Order]: any order is fine + case `date`: + regex += `(?:`; // Start a non-capturing group + regex += `${NUMBER_CHARS}{4}`; // Exactly 4 digits + regex += `[-/]`; // 1 instance of the symbol "-" or "/" + regex += `${NUMBER_CHARS}{1,2}`; // Exactly 4 digits + regex += `[-/]`; // 1 instance of the symbol "-" or "/" + regex += `${NUMBER_CHARS}{1,2}`; // Exactly 4 digits + regex += `(?:`; // Start a non-capturing group + regex += ` `; // 1 instance of the symbol " " + regex += `${NUMBER_CHARS}{1,2}`; // Exactly 4 digits + regex += `:`; // 1 instance of the symbol ":" + regex += `${NUMBER_CHARS}{1,2}`; // Exactly 4 digits + regex += `)?`; // End the non-capturing group. This group appears 0 or 1 times + regex += `)`; // End the non-capturing group + break; + // Match a unix timestamp + // [Testing Order]: any order is fine + case `timestamp`: + regex += `${NUMBER_CHARS}{10}`; // Exactly 10 digits + break; default: throw new Error(`Invalid RegexType ${type} (Unreachable error in TypeScript)`); }