From 1339b98281b235b6e4a5a15db041df835b37a686 Mon Sep 17 00:00:00 2001 From: junderw Date: Sat, 27 Aug 2022 16:35:20 +0900 Subject: [PATCH 01/61] Feature: Readable RegExp constructor --- frontend/src/app/shared/common.utils.ts | 69 ++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/shared/common.utils.ts b/frontend/src/app/shared/common.utils.ts index 7d206f4b5..6cbe35386 100644 --- a/frontend/src/app/shared/common.utils.ts +++ b/frontend/src/app/shared/common.utils.ts @@ -119,6 +119,7 @@ export function convertRegion(input, to: 'name' | 'abbreviated'): string { } } + export function haversineDistance(lat1: number, lon1: number, lat2: number, lon2: number): number { const rlat1 = lat1 * Math.PI / 180; const rlon1 = lon1 * Math.PI / 180; @@ -135,4 +136,70 @@ export function haversineDistance(lat1: number, lon1: number, lat2: number, lon2 export function kmToMiles(km: number): number { return km * 0.62137119; -} \ No newline at end of file +} + +// all base58 characters +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]'; +// Hex characters +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 + switch (type) { + // Match a block height number + // [Testing Order]: any order is fine + 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 + 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': + regex += `${HEX_CHARS}{64}`; // Exactly 64 hex letters/numbers + 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 + break; + case 'address': + // TODO + switch (network) { + case 'mainnet': + break; + case 'testnet': + break; + case 'signet': + break; + case 'liquid': + break; + case 'bisq': + break; + default: + throw new Error('Invalid Network (Unreachable error in TypeScript)'); + } + break; + default: + throw new Error('Invalid RegexType (Unreachable error in TypeScript)'); + } + regex += '$'; // $ = End of string + return new RegExp(regex); +} From c0d3f295eec2f483e8006b6f06de6fd8b3650a5b Mon Sep 17 00:00:00 2001 From: junderw Date: Sun, 28 Aug 2022 00:07:13 +0900 Subject: [PATCH 02/61] 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); } From 0a51b752e62d82b6986c8177cc63a48991383fcb Mon Sep 17 00:00:00 2001 From: junderw Date: Sun, 28 Aug 2022 16:07:46 +0900 Subject: [PATCH 03/61] Improve types and add liquidtestnet for regex --- frontend/src/app/shared/common.utils.ts | 38 +++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/shared/common.utils.ts b/frontend/src/app/shared/common.utils.ts index bbc9143c0..288fc0362 100644 --- a/frontend/src/app/shared/common.utils.ts +++ b/frontend/src/app/shared/common.utils.ts @@ -160,7 +160,12 @@ const ZERO_INDEX_NUMBER_CHARS = `(?:0|[1-9]\d*)`; // We should ignore formatting it with automated formatting tools like prettier. // // prettier-ignore -const ADDRESS_CHARS = { +const ADDRESS_CHARS: { + [k in Network]: { + base58: string; + bech32: string; + }; +} = { mainnet: { base58: `[13]` // Starts with a single 1 or 3 + BASE58_CHARS @@ -227,6 +232,28 @@ const ADDRESS_CHARS = { + `{6,100}` + `)`, }, + liquidtestnet: { + base58: `[89]` // ???(TODO: find version) is P2PKH, 8|9 is P2SH + + BASE58_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? + + `|` + + `tlq` // 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` + + `|` + + `TLQ` + + `)` + + BECH32_CHARS_UP + + `{6,100}` + + `)`, + }, bisq: { base58: `B1` // bisq base58 addrs start with B1 + BASE58_CHARS @@ -245,10 +272,10 @@ const ADDRESS_CHARS = { type RegexTypeNoAddr = `blockhash` | `transaction` | `blockheight`; export type RegexType = `address` | RegexTypeNoAddr; -export const NETWORKS = [`testnet`, `signet`, `liquid`, `bisq`, `mainnet`] as const; +export const NETWORKS = [`testnet`, `signet`, `liquid`, `liquidtestnet`, `bisq`, `mainnet`] as const; export type Network = typeof NETWORKS[number]; // Turn const array into union type -export const ADDRESS_REGEXES: [RegExp, string][] = NETWORKS +export const ADDRESS_REGEXES: [RegExp, Network][] = NETWORKS .map(network => [getRegex('address', network), network]) export function getRegex(type: RegexTypeNoAddr): RegExp; @@ -306,6 +333,11 @@ export function getRegex(type: RegexType, network?: Network): RegExp { regex += `|`; // OR regex += ADDRESS_CHARS.liquid.bech32; break; + case `liquidtestnet`: + regex += ADDRESS_CHARS.liquidtestnet.base58; + regex += `|`; // OR + regex += ADDRESS_CHARS.liquidtestnet.bech32; + break; case `bisq`: regex += ADDRESS_CHARS.bisq.base58; regex += `|`; // OR From 3d900a38497d7d6b588abba8d06c2af2c8c5d663 Mon Sep 17 00:00:00 2001 From: junderw Date: Sun, 28 Aug 2022 16:28:42 +0900 Subject: [PATCH 04/61] Fix: Prevent regex clash with channel IDs --- frontend/src/app/shared/common.utils.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/shared/common.utils.ts b/frontend/src/app/shared/common.utils.ts index 288fc0362..8b914beda 100644 --- a/frontend/src/app/shared/common.utils.ts +++ b/frontend/src/app/shared/common.utils.ts @@ -149,12 +149,13 @@ 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" -// (?: // 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*)`; +// Capped at 13 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 +// ) // End the non-capturing group. +const ZERO_INDEX_NUMBER_CHARS = `(?:0|[1-9][0-9]{0,12})`; // Formatting of the address regex is for readability, // We should ignore formatting it with automated formatting tools like prettier. From d825143b3562c4ab493f56ae67a4eadcfde7a469 Mon Sep 17 00:00:00 2001 From: junderw Date: Sun, 4 Sep 2022 21:31:02 +0900 Subject: [PATCH 05/61] Search for full address in separate network if matches --- .../search-form/search-form.component.ts | 26 +- frontend/src/app/shared/common.utils.ts | 1 - .../pipes/relative-url/relative-url.pipe.ts | 4 +- frontend/src/app/shared/regex.utils.ts | 224 ++++++++++++++++++ 4 files changed, 235 insertions(+), 20 deletions(-) create mode 100644 frontend/src/app/shared/regex.utils.ts 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 8031195f0..a9e31221a 100644 --- a/frontend/src/app/components/search-form/search-form.component.ts +++ b/frontend/src/app/components/search-form/search-form.component.ts @@ -9,7 +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'; +import { findOtherNetworks, getRegex } from '../../shared/regex.utils'; @Component({ selector: 'app-search-form', @@ -208,22 +208,13 @@ export class SearchFormComponent implements OnInit { const searchText = result || this.searchForm.value.searchText.trim(); if (searchText) { this.isSearching = true; + + const otherNetworks = findOtherNetworks(searchText, this.network as any); 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 (otherNetworks.length > 0) { + // Change the network to the first match + this.navigate('/address/', searchText, undefined, otherNetworks[0]); } else if (this.regexBlockhash.test(searchText) || this.regexBlockheight.test(searchText)) { this.navigate('/block/', searchText); } else if (this.regexTransaction.test(searchText)) { @@ -252,8 +243,9 @@ export class SearchFormComponent implements OnInit { } } - navigate(url: string, searchText: string, extras?: any): void { - this.router.navigate([this.relativeUrlPipe.transform(url), searchText], extras); + + navigate(url: string, searchText: string, extras?: any, swapNetwork?: string) { + this.router.navigate([this.relativeUrlPipe.transform(url, swapNetwork), searchText], extras); this.searchTriggered.emit(); this.searchForm.setValue({ searchText: '', diff --git a/frontend/src/app/shared/common.utils.ts b/frontend/src/app/shared/common.utils.ts index 8b914beda..e50ba13b7 100644 --- a/frontend/src/app/shared/common.utils.ts +++ b/frontend/src/app/shared/common.utils.ts @@ -119,7 +119,6 @@ export function convertRegion(input, to: 'name' | 'abbreviated'): string { } } - export function haversineDistance(lat1: number, lon1: number, lat2: number, lon2: number): number { const rlat1 = lat1 * Math.PI / 180; const rlon1 = lon1 * Math.PI / 180; 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 d7fe612fe..83f5f20df 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 @@ -10,8 +10,8 @@ export class RelativeUrlPipe implements PipeTransform { private stateService: StateService, ) { } - transform(value: string): string { - let network = this.stateService.network; + transform(value: string, swapNetwork?: string): string { + let network = swapNetwork || this.stateService.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 new file mode 100644 index 000000000..bac256c8d --- /dev/null +++ b/frontend/src/app/shared/regex.utils.ts @@ -0,0 +1,224 @@ +// all base58 characters +const BASE58_CHARS = `[a-km-zA-HJ-NP-Z1-9]`; + +// all bech32 characters (after the separator) +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]`; + +// 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) +// (?: // 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 +// ) // End the non-capturing group. +const ZERO_INDEX_NUMBER_CHARS = `(?:0|[1-9][0-9]{0,12})`; + +// Formatting of the address regex is for readability, +// We should ignore formatting it with automated formatting tools like prettier. +// +// prettier-ignore +const ADDRESS_CHARS: { + [k in Network]: { + base58: string; + bech32: string; + }; +} = { + 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}` + + `)`, + }, + liquidtestnet: { + base58: `[89]` // ???(TODO: find version) is P2PKH, 8|9 is P2SH + + BASE58_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? + + `|` + + `tlq` // 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` + + `|` + + `TLQ` + + `)` + + 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`, `liquidtestnet`, `bisq`, `mainnet`] as const; +export type Network = typeof NETWORKS[number]; // Turn const array into union type + +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 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`: + 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 + 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`: + regex += `${HEX_CHARS}{64}`; // Exactly 64 hex letters/numbers + 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 + break; + // 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`: + regex += ADDRESS_CHARS.mainnet.base58; + regex += `|`; // OR + regex += ADDRESS_CHARS.mainnet.bech32; + break; + case `testnet`: + regex += ADDRESS_CHARS.testnet.base58; + regex += `|`; // OR + regex += ADDRESS_CHARS.testnet.bech32; + break; + case `signet`: + regex += ADDRESS_CHARS.signet.base58; + regex += `|`; // OR + regex += ADDRESS_CHARS.signet.bech32; + break; + case `liquid`: + regex += ADDRESS_CHARS.liquid.base58; + regex += `|`; // OR + regex += ADDRESS_CHARS.liquid.bech32; + break; + case `liquidtestnet`: + regex += ADDRESS_CHARS.liquidtestnet.base58; + regex += `|`; // OR + regex += ADDRESS_CHARS.liquidtestnet.bech32; + break; + case `bisq`: + regex += ADDRESS_CHARS.bisq.base58; + regex += `|`; // OR + regex += ADDRESS_CHARS.bisq.bech32; + break; + default: + throw new Error(`Invalid Network ${network} (Unreachable error in TypeScript)`); + } + regex += `)`; // End the non-capturing group + break; + default: + throw new Error(`Invalid RegexType ${type} (Unreachable error in TypeScript)`); + } + regex += `$`; // $ = End of string + return new RegExp(regex); +} From 2c59992d3fbc28f64312802c622564b97a946cd6 Mon Sep 17 00:00:00 2001 From: junderw Date: Sun, 4 Sep 2022 21:53:52 +0900 Subject: [PATCH 06/61] Fix E2E error --- .../search-form/search-form.component.ts | 2 +- frontend/src/app/shared/common.utils.ts | 218 ------------------ 2 files changed, 1 insertion(+), 219 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 a9e31221a..18b4048ef 100644 --- a/frontend/src/app/components/search-form/search-form.component.ts +++ b/frontend/src/app/components/search-form/search-form.component.ts @@ -65,7 +65,7 @@ export class SearchFormComponent implements OnInit { 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.regexAddress = getRegex('address', network as any || 'mainnet'); }); this.searchForm = this.formBuilder.group({ diff --git a/frontend/src/app/shared/common.utils.ts b/frontend/src/app/shared/common.utils.ts index e50ba13b7..87c952c31 100644 --- a/frontend/src/app/shared/common.utils.ts +++ b/frontend/src/app/shared/common.utils.ts @@ -136,221 +136,3 @@ export function haversineDistance(lat1: number, lon1: number, lat2: number, lon2 export function kmToMiles(km: number): number { return km * 0.62137119; } - -// all base58 characters -const BASE58_CHARS = `[a-km-zA-HJ-NP-Z1-9]`; - -// all bech32 characters (after the separator) -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]`; - -// 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) -// (?: // 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 -// ) // End the non-capturing group. -const ZERO_INDEX_NUMBER_CHARS = `(?:0|[1-9][0-9]{0,12})`; - -// Formatting of the address regex is for readability, -// We should ignore formatting it with automated formatting tools like prettier. -// -// prettier-ignore -const ADDRESS_CHARS: { - [k in Network]: { - base58: string; - bech32: string; - }; -} = { - 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}` - + `)`, - }, - liquidtestnet: { - base58: `[89]` // ???(TODO: find version) is P2PKH, 8|9 is P2SH - + BASE58_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? - + `|` - + `tlq` // 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` - + `|` - + `TLQ` - + `)` - + 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`, `liquidtestnet`, `bisq`, `mainnet`] as const; -export type Network = typeof NETWORKS[number]; // Turn const array into union type - -export const ADDRESS_REGEXES: [RegExp, Network][] = 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`: - 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 - 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`: - regex += `${HEX_CHARS}{64}`; // Exactly 64 hex letters/numbers - 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 - break; - // 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`: - regex += ADDRESS_CHARS.mainnet.base58; - regex += `|`; // OR - regex += ADDRESS_CHARS.mainnet.bech32; - break; - case `testnet`: - regex += ADDRESS_CHARS.testnet.base58; - regex += `|`; // OR - regex += ADDRESS_CHARS.testnet.bech32; - break; - case `signet`: - regex += ADDRESS_CHARS.signet.base58; - regex += `|`; // OR - regex += ADDRESS_CHARS.signet.bech32; - break; - case `liquid`: - regex += ADDRESS_CHARS.liquid.base58; - regex += `|`; // OR - regex += ADDRESS_CHARS.liquid.bech32; - break; - case `liquidtestnet`: - regex += ADDRESS_CHARS.liquidtestnet.base58; - regex += `|`; // OR - regex += ADDRESS_CHARS.liquidtestnet.bech32; - break; - case `bisq`: - regex += ADDRESS_CHARS.bisq.base58; - regex += `|`; // OR - regex += ADDRESS_CHARS.bisq.bech32; - break; - default: - throw new Error(`Invalid Network ${network} (Unreachable error in TypeScript)`); - } - regex += `)`; // End the non-capturing group - break; - default: - throw new Error(`Invalid RegexType ${type} (Unreachable error in TypeScript)`); - } - regex += `$`; // $ = End of string - return new RegExp(regex); -} From 213800f563a2731a991cfbe2ce76b3ec419a92ad Mon Sep 17 00:00:00 2001 From: softsimon Date: Wed, 19 Jul 2023 16:46:02 +0900 Subject: [PATCH 07/61] Merge error fix --- .../app/components/search-form/search-form.component.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 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 18b4048ef..eb8adbd83 100644 --- a/frontend/src/app/components/search-form/search-form.component.ts +++ b/frontend/src/app/components/search-form/search-form.component.ts @@ -35,10 +35,10 @@ export class SearchFormComponent implements OnInit { } } - regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[A-z]{2,5}1[a-zA-HJ-NP-Z0-9]{39,59})$/; - regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/; - regexTransaction = /^([a-fA-F0-9]{64})(:\d+)?$/; - regexBlockheight = /^[0-9]{1,9}$/; + regexAddress = getRegex('address', 'mainnet'); // Default to mainnet + regexBlockhash = getRegex('blockhash'); + regexTransaction = getRegex('transaction'); + regexBlockheight = getRegex('blockheight'); focus$ = new Subject(); click$ = new Subject(); From e5df1571da468afcaea57c53241ee233baadf37d Mon Sep 17 00:00:00 2001 From: softsimon Date: Sat, 9 Dec 2023 22:33:02 +0700 Subject: [PATCH 08/61] Acceleration tab --- .../src/app/components/master-page/master-page.component.html | 4 ++++ frontend/src/app/shared/shared.module.ts | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/components/master-page/master-page.component.html b/frontend/src/app/components/master-page/master-page.component.html index f31e262ce..c6af2c282 100644 --- a/frontend/src/app/components/master-page/master-page.component.html +++ b/frontend/src/app/components/master-page/master-page.component.html @@ -48,6 +48,10 @@ @@ -77,7 +77,7 @@   - + diff --git a/frontend/src/app/lightning/nodes-ranking/nodes-ranking.component.html b/frontend/src/app/lightning/nodes-ranking/nodes-ranking.component.html index 5bd03941e..51836880d 100644 --- a/frontend/src/app/lightning/nodes-ranking/nodes-ranking.component.html +++ b/frontend/src/app/lightning/nodes-ranking/nodes-ranking.component.html @@ -1,7 +1,7 @@ - + - + diff --git a/frontend/src/app/lightning/nodes-ranking/nodes-ranking.component.ts b/frontend/src/app/lightning/nodes-ranking/nodes-ranking.component.ts index 373751be9..e59e89786 100644 --- a/frontend/src/app/lightning/nodes-ranking/nodes-ranking.component.ts +++ b/frontend/src/app/lightning/nodes-ranking/nodes-ranking.component.ts @@ -1,5 +1,8 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; +import { LightningApiService } from '../lightning-api.service'; +import { share } from 'rxjs/operators'; +import { Observable } from 'rxjs'; @Component({ selector: 'app-nodes-ranking', @@ -9,10 +12,15 @@ import { ActivatedRoute } from '@angular/router'; }) export class NodesRanking implements OnInit { type: string; + statistics$: Observable; - constructor(private route: ActivatedRoute) {} + constructor( + private route: ActivatedRoute, + private lightningApiService: LightningApiService, + ) {} ngOnInit(): void { + this.statistics$ = this.lightningApiService.getLatestStatistics$().pipe(share()); this.route.data.subscribe(data => { this.type = data.type; }); diff --git a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.html b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.html index 3efbc8594..56fb091ee 100644 --- a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.html +++ b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.html @@ -27,12 +27,14 @@ +  ({{ (node.capacity / totalCapacity * 100) | number:'1.1-1' }}%) {{ node.channels | number }} +  ({{ (node?.channels / totalChannels * 100) | number:'1.1-1' }}%) diff --git a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.scss b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.scss index 3547c447f..89f144135 100644 --- a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.scss +++ b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.scss @@ -41,6 +41,11 @@ tr, td, th { } } +.capacity-ratio { + font-size: 12px; + color: darkgrey; +} + .fiat { width: 15%; @media (min-width: 768px) and (max-width: 991px) { diff --git a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts index 054fa2f3c..780b9d1cd 100644 --- a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts +++ b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts @@ -14,11 +14,14 @@ import { LightningApiService } from '../../lightning-api.service'; }) export class TopNodesPerCapacity implements OnInit { @Input() nodes$: Observable; + @Input() statistics$: Observable; @Input() widget: boolean = false; topNodesPerCapacity$: Observable; skeletonRows: number[] = []; currency$: Observable; + totalCapacity: number; + totalChannels: number; constructor( private apiService: LightningApiService, @@ -59,6 +62,11 @@ export class TopNodesPerCapacity implements OnInit { }) ); } + + this.statistics$?.subscribe((data) => { + this.totalCapacity = data.latest.total_capacity; + this.totalChannels = data.latest.channel_count; + }); } } diff --git a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.html b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.html index 94a887bb3..76b032552 100644 --- a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.html +++ b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.html @@ -27,9 +27,11 @@ {{ node.channels ? (node.channels | number) : '~' }} +  ({{ (node?.channels / totalChannels * 100) | number:'1.1-1' }}%) +  ({{ (node.capacity / totalCapacity * 100) | number:'1.1-1' }}%) diff --git a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.scss b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.scss index a42599d69..63d65bcf8 100644 --- a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.scss +++ b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.scss @@ -44,6 +44,11 @@ tr, td, th { } } +.capacity-ratio { + font-size: 12px; + color: darkgrey; +} + .geolocation { @media (min-width: 768px) and (max-width: 991px) { display: none !important; diff --git a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts index 3de177cc7..f2d00f6f0 100644 --- a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts +++ b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts @@ -14,11 +14,14 @@ import { LightningApiService } from '../../lightning-api.service'; }) export class TopNodesPerChannels implements OnInit { @Input() nodes$: Observable; + @Input() statistics$: Observable; @Input() widget: boolean = false; topNodesPerChannels$: Observable; skeletonRows: number[] = []; currency$: Observable; + totalChannels: number; + totalCapacity: number; constructor( private apiService: LightningApiService, @@ -65,6 +68,11 @@ export class TopNodesPerChannels implements OnInit { }) ); } + + this.statistics$?.subscribe((data) => { + this.totalChannels = data.latest.channel_count; + this.totalCapacity = data.latest.total_capacity; + }); } } From 97053ab5cfe4b68961bfa9ee4cefb976e6f7cb7e Mon Sep 17 00:00:00 2001 From: natsee Date: Wed, 13 Dec 2023 15:13:01 +0100 Subject: [PATCH 10/61] Update contributor agreement file --- contributors/{ncois.txt => natsee.txt} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename contributors/{ncois.txt => natsee.txt} (91%) diff --git a/contributors/ncois.txt b/contributors/natsee.txt similarity index 91% rename from contributors/ncois.txt rename to contributors/natsee.txt index 04a436613..c391ce823 100644 --- a/contributors/ncois.txt +++ b/contributors/natsee.txt @@ -1,3 +1,3 @@ I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of November 16, 2023. -Signed: ncois +Signed: natsee From 464cffb5c1c5be14d4f88ae55cef0ff5c41192c3 Mon Sep 17 00:00:00 2001 From: natsee Date: Thu, 14 Dec 2023 12:22:00 +0100 Subject: [PATCH 11/61] Add LN statistics interface --- .../src/app/interfaces/node-api.interface.ts | 23 +++++++++++++++++++ .../channels-statistics.component.ts | 3 ++- .../lightning-dashboard.component.ts | 4 ++-- .../node-statistics.component.ts | 3 ++- .../nodes-ranking/nodes-ranking.component.ts | 3 ++- .../top-nodes-per-capacity.component.ts | 4 ++-- .../top-nodes-per-channels.component.ts | 4 ++-- 7 files changed, 35 insertions(+), 9 deletions(-) diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index 862272330..37d2f29ed 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -254,6 +254,29 @@ export interface INodesRanking { topByChannels: ITopNodesPerChannels[]; } +export interface INodesStatisticsEntry { + added: string; + avg_base_fee_mtokens: number; + avg_capacity: number; + avg_fee_rate: number; + channel_count: number; + clearnet_nodes: number; + clearnet_tor_nodes: number; + id: number; + med_base_fee_mtokens: number; + med_capacity: number; + med_fee_rate: number; + node_count: number; + tor_nodes: number; + total_capacity: number; + unannounced_nodes: number; +} + +export interface INodesStatistics { + latest: INodesStatisticsEntry; + previous: INodesStatisticsEntry; +} + export interface IOldestNodes { publicKey: string, alias: string, diff --git a/frontend/src/app/lightning/channels-statistics/channels-statistics.component.ts b/frontend/src/app/lightning/channels-statistics/channels-statistics.component.ts index 4059e9420..f2b78f53c 100644 --- a/frontend/src/app/lightning/channels-statistics/channels-statistics.component.ts +++ b/frontend/src/app/lightning/channels-statistics/channels-statistics.component.ts @@ -1,5 +1,6 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; +import { INodesStatistics } from '../../interfaces/node-api.interface'; @Component({ selector: 'app-channels-statistics', @@ -8,7 +9,7 @@ import { Observable } from 'rxjs'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class ChannelsStatisticsComponent implements OnInit { - @Input() statistics$: Observable; + @Input() statistics$: Observable; mode: string = 'avg'; constructor() { } diff --git a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts index 26a67cee6..dece98ddb 100644 --- a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts +++ b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts @@ -1,7 +1,7 @@ import { AfterViewInit, ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { share } from 'rxjs/operators'; -import { INodesRanking } from '../../interfaces/node-api.interface'; +import { INodesRanking, INodesStatistics } from '../../interfaces/node-api.interface'; import { SeoService } from '../../services/seo.service'; import { StateService } from '../../services/state.service'; import { LightningApiService } from '../lightning-api.service'; @@ -13,7 +13,7 @@ import { LightningApiService } from '../lightning-api.service'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class LightningDashboardComponent implements OnInit, AfterViewInit { - statistics$: Observable; + statistics$: Observable; nodesRanking$: Observable; officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE; diff --git a/frontend/src/app/lightning/node-statistics/node-statistics.component.ts b/frontend/src/app/lightning/node-statistics/node-statistics.component.ts index c42720427..338e17ab8 100644 --- a/frontend/src/app/lightning/node-statistics/node-statistics.component.ts +++ b/frontend/src/app/lightning/node-statistics/node-statistics.component.ts @@ -1,5 +1,6 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; +import { INodesStatistics } from '../../interfaces/node-api.interface'; @Component({ selector: 'app-node-statistics', @@ -8,7 +9,7 @@ import { Observable } from 'rxjs'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class NodeStatisticsComponent implements OnInit { - @Input() statistics$: Observable; + @Input() statistics$: Observable; constructor() { } diff --git a/frontend/src/app/lightning/nodes-ranking/nodes-ranking.component.ts b/frontend/src/app/lightning/nodes-ranking/nodes-ranking.component.ts index e59e89786..8a1eae3dc 100644 --- a/frontend/src/app/lightning/nodes-ranking/nodes-ranking.component.ts +++ b/frontend/src/app/lightning/nodes-ranking/nodes-ranking.component.ts @@ -3,6 +3,7 @@ import { ActivatedRoute } from '@angular/router'; import { LightningApiService } from '../lightning-api.service'; import { share } from 'rxjs/operators'; import { Observable } from 'rxjs'; +import { INodesStatistics } from '../../interfaces/node-api.interface'; @Component({ selector: 'app-nodes-ranking', @@ -12,7 +13,7 @@ import { Observable } from 'rxjs'; }) export class NodesRanking implements OnInit { type: string; - statistics$: Observable; + statistics$: Observable; constructor( private route: ActivatedRoute, diff --git a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts index 780b9d1cd..a52ba9398 100644 --- a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts +++ b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { map, Observable } from 'rxjs'; -import { INodesRanking, ITopNodesPerCapacity } from '../../../interfaces/node-api.interface'; +import { INodesRanking, INodesStatistics, ITopNodesPerCapacity } from '../../../interfaces/node-api.interface'; import { SeoService } from '../../../services/seo.service'; import { StateService } from '../../../services/state.service'; import { GeolocationData } from '../../../shared/components/geolocation/geolocation.component'; @@ -14,7 +14,7 @@ import { LightningApiService } from '../../lightning-api.service'; }) export class TopNodesPerCapacity implements OnInit { @Input() nodes$: Observable; - @Input() statistics$: Observable; + @Input() statistics$: Observable; @Input() widget: boolean = false; topNodesPerCapacity$: Observable; diff --git a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts index f2d00f6f0..ca1b6d6a4 100644 --- a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts +++ b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { map, Observable } from 'rxjs'; -import { INodesRanking, ITopNodesPerChannels } from '../../../interfaces/node-api.interface'; +import { INodesRanking, INodesStatistics, ITopNodesPerChannels } from '../../../interfaces/node-api.interface'; import { SeoService } from '../../../services/seo.service'; import { StateService } from '../../../services/state.service'; import { GeolocationData } from '../../../shared/components/geolocation/geolocation.component'; @@ -14,7 +14,7 @@ import { LightningApiService } from '../../lightning-api.service'; }) export class TopNodesPerChannels implements OnInit { @Input() nodes$: Observable; - @Input() statistics$: Observable; + @Input() statistics$: Observable; @Input() widget: boolean = false; topNodesPerChannels$: Observable; From 3eb6241c987a25bf320e06cb1ed70d007ef4bc33 Mon Sep 17 00:00:00 2001 From: natsee Date: Thu, 14 Dec 2023 12:29:21 +0100 Subject: [PATCH 12/61] Delete variables and use observables in top nodes components --- .../top-nodes-per-capacity.component.html | 8 ++-- .../top-nodes-per-capacity.component.ts | 42 +++++++++++------- .../top-nodes-per-channels.component.html | 8 ++-- .../top-nodes-per-channels.component.ts | 44 ++++++++++++------- 4 files changed, 63 insertions(+), 39 deletions(-) diff --git a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.html b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.html index 56fb091ee..27af80564 100644 --- a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.html +++ b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.html @@ -16,8 +16,8 @@ Last update Location - - + +
@@ -27,14 +27,14 @@ -  ({{ (node.capacity / totalCapacity * 100) | number:'1.1-1' }}%) +  ({{ (node?.capacity / data.statistics.totalCapacity * 100) | number:'1.1-1' }}%) {{ node.channels | number }} -  ({{ (node?.channels / totalChannels * 100) | number:'1.1-1' }}%) +  ({{ (node?.channels / data.statistics.totalChannels * 100) | number:'1.1-1' }}%) diff --git a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts index a52ba9398..0b8c03bbd 100644 --- a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts +++ b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts @@ -1,5 +1,5 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; -import { map, Observable } from 'rxjs'; +import { combineLatest, map, Observable } from 'rxjs'; import { INodesRanking, INodesStatistics, ITopNodesPerCapacity } from '../../../interfaces/node-api.interface'; import { SeoService } from '../../../services/seo.service'; import { StateService } from '../../../services/state.service'; @@ -17,11 +17,9 @@ export class TopNodesPerCapacity implements OnInit { @Input() statistics$: Observable; @Input() widget: boolean = false; - topNodesPerCapacity$: Observable; + topNodesPerCapacity$: Observable<{ nodes: ITopNodesPerCapacity[]; statistics: { totalCapacity: number; totalChannels?: number; } }>; skeletonRows: number[] = []; currency$: Observable; - totalCapacity: number; - totalChannels: number; constructor( private apiService: LightningApiService, @@ -42,8 +40,12 @@ export class TopNodesPerCapacity implements OnInit { } if (this.widget === false) { - this.topNodesPerCapacity$ = this.apiService.getTopNodesByCapacity$().pipe( - map((ranking) => { + this.topNodesPerCapacity$ = combineLatest([ + this.apiService.getTopNodesByCapacity$(), + this.statistics$ + ]) + .pipe( + map(([ranking, statistics]) => { for (const i in ranking) { ranking[i].geolocation = { country: ranking[i].country?.en, @@ -52,21 +54,31 @@ export class TopNodesPerCapacity implements OnInit { iso: ranking[i].iso_code, }; } - return ranking; + return { + nodes: ranking, + statistics: { + totalCapacity: statistics.latest.total_capacity, + totalChannels: statistics.latest.channel_count, + } + } }) ); } else { - this.topNodesPerCapacity$ = this.nodes$.pipe( - map((ranking) => { - return ranking.topByCapacity.slice(0, 6); + this.topNodesPerCapacity$ = combineLatest([ + this.nodes$, + this.statistics$ + ]) + .pipe( + map(([ranking, statistics]) => { + return { + nodes: ranking.topByCapacity.slice(0, 6), + statistics: { + totalCapacity: statistics.latest.total_capacity, + } + } }) ); } - - this.statistics$?.subscribe((data) => { - this.totalCapacity = data.latest.total_capacity; - this.totalChannels = data.latest.channel_count; - }); } } diff --git a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.html b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.html index 76b032552..87f7f1ad2 100644 --- a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.html +++ b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.html @@ -16,8 +16,8 @@ Last update Location - - + +
@@ -27,11 +27,11 @@ {{ node.channels ? (node.channels | number) : '~' }} -  ({{ (node?.channels / totalChannels * 100) | number:'1.1-1' }}%) +  ({{ (node?.channels / data.statistics.totalChannels * 100) | number:'1.1-1' }}%) -  ({{ (node.capacity / totalCapacity * 100) | number:'1.1-1' }}%) +  ({{ (node.capacity / data.statistics.totalCapacity * 100) | number:'1.1-1' }}%) diff --git a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts index ca1b6d6a4..56d55a5d3 100644 --- a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts +++ b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts @@ -1,5 +1,5 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; -import { map, Observable } from 'rxjs'; +import { combineLatest, map, Observable } from 'rxjs'; import { INodesRanking, INodesStatistics, ITopNodesPerChannels } from '../../../interfaces/node-api.interface'; import { SeoService } from '../../../services/seo.service'; import { StateService } from '../../../services/state.service'; @@ -17,12 +17,10 @@ export class TopNodesPerChannels implements OnInit { @Input() statistics$: Observable; @Input() widget: boolean = false; - topNodesPerChannels$: Observable; + topNodesPerChannels$: Observable<{ nodes: ITopNodesPerChannels[]; statistics: { totalChannels: number; totalCapacity?: number; } }>; skeletonRows: number[] = []; currency$: Observable; - totalChannels: number; - totalCapacity: number; - + constructor( private apiService: LightningApiService, private stateService: StateService, @@ -40,8 +38,12 @@ export class TopNodesPerChannels implements OnInit { this.seoService.setTitle($localize`:@@c50bf442cf99f6fc5f8b687c460f33234b879869:Connectivity Ranking`); this.seoService.setDescription($localize`:@@meta.description.lightning.ranking.channels:See Lightning nodes with the most channels open along with high-level stats like total node capacity, node age, and more.`); - this.topNodesPerChannels$ = this.apiService.getTopNodesByChannels$().pipe( - map((ranking) => { + this.topNodesPerChannels$ = combineLatest([ + this.apiService.getTopNodesByChannels$(), + this.statistics$ + ]) + .pipe( + map(([ranking, statistics]) => { for (const i in ranking) { ranking[i].geolocation = { country: ranking[i].country?.en, @@ -50,12 +52,22 @@ export class TopNodesPerChannels implements OnInit { iso: ranking[i].iso_code, }; } - return ranking; + return { + nodes: ranking, + statistics: { + totalChannels: statistics.latest.channel_count, + totalCapacity: statistics.latest.total_capacity, + } + } }) ); } else { - this.topNodesPerChannels$ = this.nodes$.pipe( - map((ranking) => { + this.topNodesPerChannels$ = combineLatest([ + this.nodes$, + this.statistics$ + ]) + .pipe( + map(([ranking, statistics]) => { for (const i in ranking.topByChannels) { ranking.topByChannels[i].geolocation = { country: ranking.topByChannels[i].country?.en, @@ -64,15 +76,15 @@ export class TopNodesPerChannels implements OnInit { iso: ranking.topByChannels[i].iso_code, }; } - return ranking.topByChannels.slice(0, 6); + return { + nodes: ranking.topByChannels.slice(0, 6), + statistics: { + totalChannels: statistics.latest.channel_count, + } + } }) ); } - - this.statistics$?.subscribe((data) => { - this.totalChannels = data.latest.channel_count; - this.totalCapacity = data.latest.total_capacity; - }); } } From 93b074edf09b44c56c0b30cc21bc6d338d7f1c0b Mon Sep 17 00:00:00 2001 From: natsee Date: Wed, 27 Dec 2023 18:50:41 +0100 Subject: [PATCH 13/61] Add endpoints to docs api --- .../src/app/docs/api-docs/api-docs-data.ts | 1181 ++++++++++++++++- 1 file changed, 1172 insertions(+), 9 deletions(-) diff --git a/frontend/src/app/docs/api-docs/api-docs-data.ts b/frontend/src/app/docs/api-docs/api-docs-data.ts index 97be0c2b1..c5319bf2a 100644 --- a/frontend/src/app/docs/api-docs/api-docs-data.ts +++ b/frontend/src/app/docs/api-docs/api-docs-data.ts @@ -209,6 +209,114 @@ export const restApiDocsData = [ } } }, + { + type: "endpoint", + category: "general", + httpRequestMethod: "GET", + fragment: "get-price", + title: "GET Price", + description: { + default: "Returns bitcoin latest price denominated in main currencies." + }, + urlString: "/v1/prices", + showConditions: [""], + showJsExamples: showJsExamplesDefaultFalse, + codeExample: { + default: { + codeTemplate: { + commonJS: ``, + esModule: ``, + curl: `/api/v1/prices`, + }, + codeSampleMainnet: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + time: 1703252411, + USD: 43753, + EUR: 40545, + GBP: 37528, + CAD: 58123, + CHF: 37438, + AUD: 64499, + JPY: 6218915 +}` + }, + codeSampleTestnet: emptyCodeSample, + codeSampleSignet: emptyCodeSample, + codeSampleLiquid: emptyCodeSample, + codeSampleLiquidTestnet: emptyCodeSample, + codeSampleBisq: emptyCodeSample, + } + } + }, + { + type: "endpoint", + category: "general", + httpRequestMethod: "GET", + fragment: "get-historical-price", + title: "GET Historical Price", + description: { + default: "Returns bitcoin historical price denominated in main currencies." + }, + urlString: "/v1/historical-price", + showConditions: [""], + showJsExamples: showJsExamplesDefaultFalse, + codeExample: { + default: { + codeTemplate: { + commonJS: ``, + esModule: ``, + curl: `/api/v1/historical-price`, + }, + codeSampleMainnet: { + esModule: [], + commonJS: [], + curl: [], + response: `{ + prices: [ + { + time: 1703692800, + USD: 42972, + EUR: 39590, + GBP: 36803, + CAD: 56883, + CHF: 36486, + AUD: 63006, + JPY: 6124530 + }, + ... + { + time: 1279497600, + USD: 0.08584, + EUR: -1, + GBP: -1, + CAD: -1, + CHF: -1, + AUD: -1, + JPY: -1 + } + ], + exchangeRates: { + USDEUR: 0.92, + USDGBP: 0.86, + USDCAD: 1.32, + USDCHF: 0.85, + USDAUD: 1.47, + USDJPY: 142.52 + } +} +` + }, + codeSampleTestnet: emptyCodeSample, + codeSampleSignet: emptyCodeSample, + codeSampleLiquid: emptyCodeSample, + codeSampleLiquidTestnet: emptyCodeSample, + codeSampleBisq: emptyCodeSample, + } + } + }, { type: "endpoint", category: "general", @@ -1409,20 +1517,20 @@ export const restApiDocsData = [ ]` }, codeSampleSignet: { - esModule: [`1wiz18xYmhRX6xStj2b9t1rwWX4GKUgpv`], - commonJS: [`1wiz18xYmhRX6xStj2b9t1rwWX4GKUgpv`], - curl: [`1wiz18xYmhRX6xStj2b9t1rwWX4GKUgpv`], + esModule: [`tb1pu8ysre22dcl6qy5m5w7mjwutw73w4u24slcdh4myq06uhr6q29dqwc3ckt`], + commonJS: [`tb1pu8ysre22dcl6qy5m5w7mjwutw73w4u24slcdh4myq06uhr6q29dqwc3ckt`], + curl: [`tb1pu8ysre22dcl6qy5m5w7mjwutw73w4u24slcdh4myq06uhr6q29dqwc3ckt`], response: `[ { - txid: "e58b47f657b496a083ad9a4fb10c744d5e993028efd9cfba149885334d98bdf5", + txid: "c56a054302df8f8f80c5ac6b86b24ed52bf41d64de640659837c56bc33d10c9e", vout: 0, status: { confirmed: true, - block_height: 698571, - block_hash: "00000000000000000007536c0a664a7d2a01c31569623183eba0768d9a0c163d", - block_time: 1630520708 + block_height: 174923, + block_hash: "000000750e335ff355be2e3754fdada30d107d7d916aef07e2f5d014bec845e5", + block_time: 1703321003 }, - value: 642070789 + value: 546 }, ... ]` @@ -1470,6 +1578,65 @@ export const restApiDocsData = [ } } }, + { + type: "endpoint", + category: "addresses", + httpRequestMethod: "GET", + fragment: "get-address-validate", + title: "GET Address Validation", + description: { + default: "Returns whether an address is valid or not. Available fields: isvalid (boolean), address (string), scriptPubKey (string), isscript (boolean), iswitness (boolean), witness_version (numeric, optional), and witness_program (string, optional).", + }, + urlString: "/v1/validate-address/:address", + showConditions: bitcoinNetworks, + showJsExamples: showJsExamplesDefaultFalse, + codeExample: { + default: { + codeTemplate: { + curl: `/api/v1/validate-address/%{1}`, + commonJS: ``, + esModule: ``, + }, + codeSampleMainnet: { + curl: [`1KFHE7w8BhaENAswwryaoccDb6qcT6DbYY`], + response: `{ + isvalid: true, + address: "1KFHE7w8BhaENAswwryaoccDb6qcT6DbYY", + scriptPubKey: "76a914c825a1ecf2a6830c4401620c3a16f1995057c2ab88ac", + isscript: false, + iswitness: false +}` + }, + codeSampleTestnet: { + curl: [`tb1q4kgratttzjvkxfmgd95z54qcq7y6hekdm3w56u`], + response: `{ + isvalid: true, + address: "tb1q4kgratttzjvkxfmgd95z54qcq7y6hekdm3w56u", + scriptPubKey: "0014ad903ead6b149963276869682a54180789abe6cd", + isscript: false, + iswitness: true, + witness_version: 0, + witness_program: "ad903ead6b149963276869682a54180789abe6cd" +}` + }, + codeSampleSignet: { + curl: [`tb1pu8ysre22dcl6qy5m5w7mjwutw73w4u24slcdh4myq06uhr6q29dqwc3ckt`], + response: `{ + isvalid: true, + address: "tb1pu8ysre22dcl6qy5m5w7mjwutw73w4u24slcdh4myq06uhr6q29dqwc3ckt", + scriptPubKey: "5120e1c901e54a6e3fa0129ba3bdb93b8b77a2eaf15587f0dbd76403f5cb8f40515a", + isscript: true, + iswitness: true, + witness_version: 1, + witness_program: "e1c901e54a6e3fa0129ba3bdb93b8b77a2eaf15587f0dbd76403f5cb8f40515a" +}` + }, + codeSampleLiquid: emptyCodeSample, + codeSampleLiquidTestnet: emptyCodeSample, + codeSampleBisq: emptyCodeSample, + } + } + }, { type: "category", category: "assets", @@ -2109,6 +2276,61 @@ export const restApiDocsData = [ } } }, + { + type: "endpoint", + category: "blocks", + httpRequestMethod: "GET", + fragment: "get-block-timestamp", + title: "GET Block Timestamp", + description: { + default: "Returns the height and the hash of the block closest to the given :timestamp." + }, + urlString: "/v1/mining/blocks/timestamp/:timestamp", + showConditions: bitcoinNetworks, + showJsExamples: showJsExamplesDefaultFalse, + codeExample: { + default: { + codeTemplate: { + curl: `/api/v1/mining/blocks/timestamp/%{1}`, + commonJS: ``, + esModule: `` + }, + codeSampleMainnet: { + esModule: [], + commonJS: [], + curl: ['1672531200'], + response: `{ + height: 769786, + hash: "000000000000000000017f6405c2382de84944eb21be9cec0379a735813f137b", + timestamp: "2022-12-31T23:30:31.000Z" +}` + }, + codeSampleTestnet: { + esModule: [], + commonJS: [], + curl: ['1672531200'], + response: `{ + height: 2413838, + hash: "00000000000000082888e2353ea4baaea04d2e0e88f2ee054ad2bbcc1d6a5469", + timestamp: "2022-12-31T23:57:26.000Z" +}` + }, + codeSampleSignet: { + esModule: [], + commonJS: [], + curl: ['1672531200'], + response: `{ + height: 123713, + hash: "0000010c6df8ffe1684ab9d7cfac69836a4538c057fab4571b809120fe486c96", + timestamp: "2022-12-31T23:55:56.000Z" +}` + }, + codeSampleLiquid: emptyCodeSample, + codeSampleLiquidTestnet: emptyCodeSample, + codeSampleBisq: emptyCodeSample, + } + } + }, { type: "endpoint", category: "blocks", @@ -4042,6 +4264,101 @@ export const restApiDocsData = [ } } }, + { + type: "endpoint", + category: "mining", + httpRequestMethod: "GET", + fragment: "get-difficulty-adjustments", + title: "GET Difficulty Adjustments", + description: { + default: "

Returns the record of difficulty adjustments over the specified trailing :interval:

  • Block timestamp
  • Block height
  • Difficulty
  • Difficulty change

If no time interval is specified, all available data is returned." + }, + urlString: "/v1/mining/difficulty-adjustments/[:interval]", + showConditions: bitcoinNetworks, + showJsExamples: showJsExamplesDefaultFalse, + codeExample: { + default: { + codeTemplate: { + curl: `/api/v1/mining/difficulty-adjustments/1m`, + commonJS: ``, + esModule: `` + }, + codeSampleMainnet: { + esModule: [], + commonJS: [], + curl: [], + response: `[ + [ + 1703311464, + 822528, + 72006146478567.1, + 1.06983 + ], + [ + 1702180644, + 820512, + 67305906902031.39, + 0.990408 + ], + [ + 1700957763, + 818496, + 67957790298897.88, + 1.0507 + ] +]` + }, + codeSampleTestnet: { + esModule: [], + commonJS: [], + curl: [], + response: `[ + [ + 1703429523, + 2544008, + 105074715.9955905, + 105075000 + ], + [ + 1703426009, + 2544005, + 1, + 0 + ], + [ + 1703422944, + 2544000, + 105074715.9955905, + 105075000 + ], + ... +]` + }, + codeSampleSignet: { + esModule: [], + commonJS: [], + curl: [], + response: `[ + [ + 1702402252, + 173376, + 0.002967416960321784, + 1.01893 + ], + [ + 1701214807, + 171360, + 0.002912289751655253, + 0.9652 + ] +]` + }, + codeSampleLiquid: emptyCodeSample, + codeSampleLiquidTestnet: emptyCodeSample, + codeSampleBisq: emptyCodeSample, + } + } + }, { type: "endpoint", category: "mining", @@ -4559,6 +4876,407 @@ export const restApiDocsData = [ }, ... ] +}` + }, + codeSampleLiquid: emptyCodeSample, + codeSampleLiquidTestnet: emptyCodeSample, + codeSampleBisq: emptyCodeSample, + } + } + }, + { + type: "endpoint", + category: "mining", + httpRequestMethod: "GET", + fragment: "get-block-predictions", + title: "GET Block Predictions", + description: { + default: "

Returns average block health in the specified :timePeriod, ordered oldest to newest. :timePeriod can be any of the following: " + miningTimeIntervals + ".

For 24h and 3d time periods, every block is included and figures are exact (not averages). For the 1w time period, figures may be averages depending on how fast blocks were found around a particular timestamp. For other time periods, figures are averages.

" + }, + urlString: ["/v1/mining/blocks/predictions/:timePeriod"], + showConditions: bitcoinNetworks, + showJsExamples: showJsExamplesDefaultFalse, + codeExample: { + default: { + codeTemplate: { + curl: `/api/v1/mining/blocks/predictions/%{1}`, + commonJS: ``, + esModule: `` + }, + codeSampleMainnet: { + esModule: [], + commonJS: [], + curl: [`3y`], + response: `[ + [ + 1687247274, + 777625, + 100 + ], + [ + 1687066238, + 788621, + 99.85 + ], + [ + 1687263518, + 795182, + 99.46 + ], + [ + 1687312271, + 795260, + 100 + ], + ... +]` + }, + codeSampleTestnet: { + esModule: [], + commonJS: [], + curl: [`3y`], + response: `[ + [ + 1687246773, + 2429248, + 100 + ], + [ + 1687285500, + 2438380, + 100 + ], + [ + 1687342820, + 2438467, + 100 + ], + [ + 1687372143, + 2438522, + 100 + ], + ... +]` + }, + codeSampleSignet: { + esModule: [], + commonJS: [], + curl: [`3y`], + response: `[ + [ + 1687246696, + 129639, + 0 + ], + [ + 1687303289, + 148191, + 0 + ], + [ + 1687315093, + 148218, + 0 + ], + [ + 1687368211, + 148312, + 0 + ], + ... +]` + }, + codeSampleLiquid: emptyCodeSample, + codeSampleLiquidTestnet: emptyCodeSample, + codeSampleBisq: emptyCodeSample, + } + } + }, + { + type: "endpoint", + category: "mining", + httpRequestMethod: "GET", + fragment: "get-block-audit-score", + title: "GET Block Audit Score", + description: { + default: "Returns the block audit score for the specified :blockHash. Available fields: hash, matchRate, expectedFees, and expectedWeight." + }, + urlString: ["/v1/mining/blocks/audit/score/:blockHash"], + showConditions: bitcoinNetworks, + showJsExamples: showJsExamplesDefaultFalse, + codeExample: { + default: { + codeTemplate: { + curl: `/api/v1/mining/blocks/audit/score/%{1}`, + commonJS: ``, + esModule: `` + }, + codeSampleMainnet: { + esModule: [], + commonJS: [], + curl: [`000000000000000000032535698c5b0c48283b792cf86c1c6e36ff84464de785`], + response: `{ + hash: "000000000000000000032535698c5b0c48283b792cf86c1c6e36ff84464de785", + matchRate: 99.66, + expectedFees: 12090955, + expectedWeight: 3991988 +}` + }, + codeSampleTestnet: { + esModule: [], + commonJS: [], + curl: [`000000000000025a66f30a181e438b9f65ef33cec3014b7a4ff4c7578289cd6e`], + response: `{ + hash: "000000000000025a66f30a181e438b9f65ef33cec3014b7a4ff4c7578289cd6e", + matchRate: 100, + expectedFees: 579169, + expectedWeight: 12997 +}` + }, + codeSampleSignet: { + esModule: [], + commonJS: [], + curl: [`000000c1491d7d4229d4bf07e0dcaa7e396767b45be388e1174c7439a9490121`], + response: `{ + hash: "000000c1491d7d4229d4bf07e0dcaa7e396767b45be388e1174c7439a9490121", + matchRate: 100, + expectedFees: 80520, + expectedWeight: 16487 +}` + }, + codeSampleLiquid: emptyCodeSample, + codeSampleLiquidTestnet: emptyCodeSample, + codeSampleBisq: emptyCodeSample, + } + } + }, + { + type: "endpoint", + category: "mining", + httpRequestMethod: "GET", + fragment: "get-blocks-audit-scores", + title: "GET Blocks Audit Scores", + description: { + default: "Returns blocks audit score for the past 16 blocks. If :startHeight is specified, the past 15 blocks before (and including) :startHeight are returned. Available fields: hash, matchRate, expectedFees, and expectedWeight." + }, + urlString: ["/v1/mining/blocks/audit/scores/:startHeight"], + showConditions: bitcoinNetworks, + showJsExamples: showJsExamplesDefaultFalse, + codeExample: { + default: { + codeTemplate: { + curl: `/api/v1/mining/blocks/audit/scores/%{1}`, + commonJS: ``, + esModule: `` + }, + codeSampleMainnet: { + esModule: [], + commonJS: [], + curl: [`820000`], + response: `[ + { + hash: "000000000000000000034cd3689507da0386d3d1790dd56f2e6945e650e02c74", + matchRate: 100, + expectedFees: 225828975, + expectedWeight: 3991756 + }, + { + hash: "00000000000000000000b3ad97907e99c54e6b9145a8f77842e59d9c0c8377cf", + matchRate: 100, + expectedFees: 295107022, + expectedWeight: 3991752 + }, + ... +]` + }, + codeSampleTestnet: { + esModule: [], + commonJS: [], + curl: [`2566570`], + response: `[ + { + hash: "00000000000002e7e96e7b5ee04a5fbb3ef9575a9f4a99effb32a8a89d9d2f19", + matchRate: 100, + expectedFees: 964677, + expectedWeight: 24959 + }, + { + hash: "00000000000003bd3962806d0e06d9982eb2e06aeba912687b2bac3668db32aa", + matchRate: 100, + expectedFees: 631200, + expectedWeight: 15516 + }, + ... +]` + }, + codeSampleSignet: { + esModule: [], + commonJS: [], + curl: [`175504`], + response: `[ + { + hash: "00000012d54289925efc151f2e111e0775e80c3b6bb4b0dcd3ff01dec4bbc5d0", + matchRate: 100, + expectedFees: 4767, + expectedWeight: 2524 + }, + { + hash: "00000031e269cf0b567260b01ae11453175f4598fdb4f1908c5e2f4265b9d93a", + matchRate: 100, + expectedFees: 9090, + expectedWeight: 1851 + }, + ... +]` + }, + codeSampleLiquid: emptyCodeSample, + codeSampleLiquidTestnet: emptyCodeSample, + codeSampleBisq: emptyCodeSample, + } + } + }, + { + type: "endpoint", + category: "mining", + httpRequestMethod: "GET", + fragment: "get-block-audit-summary", + title: "GET Block Audit Summary", + description: { + default: "Returns the block audit summary for the specified :blockHash. Available fields: height, id, timestamp, template, missingTxs, addedTxs, freshTxs, sigopTxs, fullrbfTxs, acceleratedTxs, matchRate, expectedFees, and expectedWeight." + }, + urlString: ["/v1/block/:blockHash/audit-summary"], + showConditions: bitcoinNetworks, + showJsExamples: showJsExamplesDefaultFalse, + codeExample: { + default: { + codeTemplate: { + curl: `/api/v1/block/%{1}/audit-summary`, + commonJS: ``, + esModule: `` + }, + codeSampleMainnet: { + esModule: [], + commonJS: [], + curl: [`00000000000000000000f218ceda7a5d9c289040b9c3f05ef9f7c2f4930e0123`], + response: `{ + height: 822418, + id: "00000000000000000000f218ceda7a5d9c289040b9c3f05ef9f7c2f4930e0123", + timestamp: 1703262962, + template: [ + { + txid: "1de119e4fe0fb92378de74a59fec337c39d505bbc0d04d20d151cc3fb7a91bf0", + fee: 92000, + vsize: 140.25, + value: 354245800, + rate: 655.9714795008913, + flags: 1099511631881 + }, + ... + ], + missingTxs: [], + addedTxs: [ + "3036565d1af6c5b14876a255cdf06214aa350e62154d1ce8619c8e933d0526f8", + "aaa9d8e8f1de712574182a618b4d608f96f39bfc55e296d2e5904561cdef2e77", + ... + ], + freshTxs: [ + "8ede292d8f0319cbe79fff9fd47564cd7f78fad74d7c506d2b157399ff41d904" + ], + sigopTxs: [], + fullrbfTxs: [ + "271e7792910a4ea134c02c03c9d7477b32a8531a5dd92fbc4dbf3ca70614fcce", + "634a5b2de393f0f5b4eeb335bee75c1779b1f2308a07e86cafb95894aa4734d0", + ... + ], + acceleratedTxs: [], + matchRate: 100, + expectedFees: 169464627, + expectedWeight: 3991702 +}` + }, + codeSampleTestnet: { + esModule: [], + commonJS: [], + curl: [`000000000000007cfba94e051326b3546c968a188a7e12e340a78cefc586bfe3`], + response: `{ + height: 2566708, + id: "000000000000007cfba94e051326b3546c968a188a7e12e340a78cefc586bfe3", + timestamp: 1703684826, + template: [ + { + txid: "6556caa3c6bff537f04837a6f7182dd7a253f31a46de4f21dec9584720156d35", + fee: 109707, + vsize: 264.75, + value: 456855, + rate: 414.37960339943345, + flags: 9895621445642 + }, + { + txid: "53b7743b8cfa0108dbcdc7c2f5e661b9d8f56216845a439449d7f9dfc466b147", + fee: 74640, + vsize: 215.5, + value: 19063915, + rate: 348.5338491295938, + flags: 1099528491017 + }, + ... + ], + missingTxs: [ + "8f2eae756119e43054ce1014a06e81d612113794d8b519e6ff393d7e0023396a", + "012b44b0fc0fddc549a056c85850f03a83446c843504c588cd5829873b30f5a9", + ... + ], + addedTxs: [], + freshTxs: [ + "af36a8b88f6c19f997614dfc8a41395190eaf496a49e8db393dacb770999abd5", + "fdfa272c8fe069573b964ddad605d748d8c737e94dfcd09bddaae0ee0a2445df", + ... + ], + sigopTxs: [], + fullrbfTxs: [], + acceleratedTxs: [], + matchRate: 86.96, + expectedFees: 1541639, + expectedWeight: 26425 +}` + }, + codeSampleSignet: { + esModule: [], + commonJS: [], + curl: [`0000008acf5177d07f1d648f4d54f26095936a5d29a0a6145dd75a0415e63c0f`], + response: `{ + height: 175519, + id: "0000008acf5177d07f1d648f4d54f26095936a5d29a0a6145dd75a0415e63c0f", + timestamp: 1703682844, + template: [ + { + txid: "f95b38742c483b81dc4ff49a803bae7625f1596ec5756c944d7586dfe8b38250", + fee: 3766, + vsize: 172.25, + value: 115117171776, + rate: 21.86357039187228, + flags: 1099528425481 + }, + { + txid: "8665c4d05732c930c2037bc0220e4ab9b1b64ce3302363ff7d118827c7347b52", + fee: 3766, + vsize: 172.25, + value: 115116509429, + rate: 21.86357039187228, + flags: 1099528425481 + }, + ... + ], + missingTxs: [], + addedTxs: [], + freshTxs: [], + sigopTxs: [], + fullrbfTxs: [], + acceleratedTxs: [], + matchRate: 100, + expectedFees: 10494, + expectedWeight: 6582 }` }, codeSampleLiquid: emptyCodeSample, @@ -5127,6 +5845,267 @@ export const restApiDocsData = [ } } }, + { + type: "endpoint", + category: "mempool", + httpRequestMethod: "GET", + fragment: "get-mempool-rbf", + title: "GET Mempool RBF Transactions", + description: { + default: "Returns the list of mempool transactions that are part of a RBF chain." + }, + urlString: "/v1/replacements", + showConditions: bitcoinNetworks, + showJsExamples: showJsExamplesDefaultFalse, + codeExample: { + default: { + codeTemplate: { + curl: `/api/v1/replacements`, + commonJS: ``, + esModule: ``, + }, + codeSampleMainnet: { + curl: [], + response: `[ + { + tx: { + txid: "1ca4b22006e57b1b13f5cc89a41cf7c9e99fe225aabf407251e4fe0268f22d93", + fee: 14983, + vsize: 141.5, + value: 343934, + rate: 105.886925795053, + rbf: true, + fullRbf: false + }, + time: 1703331467, + fullRbf: false, + replaces: [ + { + tx: { + txid: "9f8e30674af641bb153a35254d539468e1d847b16bbdc13ce23b5a970b0b11cf", + fee: 13664, + vsize: 141.25, + value: 345253, + rate: 96.7362831858407, + rbf: true + }, + time: 1703331398, + interval: 69, + fullRbf: false, + replaces: [] + } + ] + }, + ... +]` + }, + codeSampleTestnet: { + curl: [], + response: `[ + { + tx: { + txid: "7766e3f008011b776905f96fcad9d4a7b75d1b368d1e77db2901254f1fa8357d", + fee: 9101, + vsize: 317, + value: 147706698, + rate: 28.709779179810724, + rbf: true, + fullRbf: false + }, + time: 1703331325, + fullRbf: false, + replaces: [ + { + tx: { + txid: "43055f6e5750c6aa0c2214e59e99f367398d96bde935e7666c3e648d249a4e40", + fee: 7000, + vsize: 317, + value: 147708799, + rate: 22.082018927444796, + rbf: true + }, + time: 1703331154, + interval: 171, + fullRbf: false, + replaces: [] + } + ] + }, + ... +]` + }, + codeSampleSignet: { + curl: [], + response: `[ + { + tx: { + txid: "13985a5717a1ea54ce720cd6b70421b1667061be491a6799acf6dea01c551248", + fee: 5040, + vsize: 215.5, + value: 762745, + rate: 23.387470997679813, + rbf: true, + fullRbf: false, + mined: true + }, + time: 1703316271, + fullRbf: false, + replaces: [ + { + tx: { + txid: "eac5ec8487414c955f4a5d3b2e516c351aec5299f1335f9019a00907962386ce", + fee: 4560, + vsize: 215.25, + value: 763225, + rate: 21.18466898954704, + rbf: true + }, + time: 1703316270, + interval: 1, + fullRbf: false, + replaces: [] + } + ], + mined: true + } +]` + }, + codeSampleLiquid: emptyCodeSample, + codeSampleLiquidTestnet: emptyCodeSample, + codeSampleBisq: emptyCodeSample, + } + } + }, + { + type: "endpoint", + category: "mempool", + httpRequestMethod: "GET", + fragment: "get-mempool-fullrbf", + title: "GET Mempool Full RBF Transactions", + description: { + default: "Returns the list of mempool transactions that are part of a Full-RBF chain." + }, + urlString: "/v1/fullrbf/replacements", + showConditions: bitcoinNetworks, + showJsExamples: showJsExamplesDefaultFalse, + codeExample: { + default: { + codeTemplate: { + curl: `/api/v1/fullrbf/replacements`, + commonJS: ``, + esModule: ``, + }, + codeSampleMainnet: { + curl: [], + response: `[ + { + tx: { + txid: "25e2bfaf0e0821e5cb71f11e460b2f71e1d5a3755015de42544afa5fbad6d443", + fee: 24436, + vsize: 297.75, + value: 273418, + rate: 82.0688497061293, + rbf: false, + fullRbf: true + }, + time: 1703409882, + fullRbf: true, + replaces: [ + { + tx: { + txid: "07d501e8ad4a25f07f3ced0a6102741720f710765e6fdb2eb966ba0df657997a", + fee: 24138, + vsize: 297.75, + value: 273716, + rate: 81.06801007556675, + rbf: false + }, + time: 1703409853, + interval: 29, + fullRbf: true, + replaces: [] + } + ] + }, + ... +]` + }, + codeSampleTestnet: { + curl: [], + response: `[ + { + tx: { + txid: "25e2bfaf0e0821e5cb71f11e460b2f71e1d5a3755015de42544afa5fbad6d443", + fee: 24436, + vsize: 297.75, + value: 273418, + rate: 82.0688497061293, + rbf: false, + fullRbf: true + }, + time: 1703409882, + fullRbf: true, + replaces: [ + { + tx: { + txid: "07d501e8ad4a25f07f3ced0a6102741720f710765e6fdb2eb966ba0df657997a", + fee: 24138, + vsize: 297.75, + value: 273716, + rate: 81.06801007556675, + rbf: false + }, + time: 1703409853, + interval: 29, + fullRbf: true, + replaces: [] + } + ] + }, + ... +]` + }, + codeSampleSignet: { + curl: [], + response: `[ + { + tx: { + txid: "25e2bfaf0e0821e5cb71f11e460b2f71e1d5a3755015de42544afa5fbad6d443", + fee: 24436, + vsize: 297.75, + value: 273418, + rate: 82.0688497061293, + rbf: false, + fullRbf: true + }, + time: 1703409882, + fullRbf: true, + replaces: [ + { + tx: { + txid: "07d501e8ad4a25f07f3ced0a6102741720f710765e6fdb2eb966ba0df657997a", + fee: 24138, + vsize: 297.75, + value: 273716, + rate: 81.06801007556675, + rbf: false + }, + time: 1703409853, + interval: 29, + fullRbf: true, + replaces: [] + } + ] + }, + ... +]` + }, + codeSampleLiquid: emptyCodeSample, + codeSampleLiquidTestnet: emptyCodeSample, + codeSampleBisq: emptyCodeSample, + } + } + }, { type: "category", category: "transactions", @@ -5143,7 +6122,7 @@ export const restApiDocsData = [ description: { default: "Returns the ancestors and the best descendant fees for a transaction." }, - urlString: "/v1/fees/cpfp", + urlString: "/v1/cpfp", showConditions: bitcoinNetworks.concat(liquidNetworks), showJsExamples: showJsExamplesDefault, codeExample: { @@ -6000,6 +6979,148 @@ export const restApiDocsData = [ } } }, + { + type: "endpoint", + category: "transactions", + httpRequestMethod: "GET", + fragment: "get-transaction-rbf-history", + title: "GET Transaction RBF History", + description: { + default: "Returns the RBF tree history of a transaction." + }, + urlString: "v1/tx/:txId/rbf", + showConditions: bitcoinNetworks, + showJsExamples: showJsExamplesDefaultFalse, + codeExample: { + default: { + codeTemplate: { + curl: `/api/v1/tx/%{1}/rbf`, + commonJS: ``, + esModule: ``, + }, + codeSampleMainnet: { + curl: [`2e95ff9094df9f3650e3f2abc189250760162be89a88f9f2f23301c7cb14b8b4`], + response: `{ + replacements: { + tx: { + txid: "2e95ff9094df9f3650e3f2abc189250760162be89a88f9f2f23301c7cb14b8b4", + fee: 1668, + vsize: 276.75, + value: 14849, + rate: 4.824207492795389, + rbf: false, + fullRbf: true + }, + time: 1703240261, + fullRbf: true, + replaces: [ + { + tx: { + txid: "3f4670463daadffed07d7a1060071b07f7e81a2566eca21d78bb513cbf21c82a", + fee: 420, + vsize: 208.25, + value: 4856, + rate: 2.0168067226890756, + rbf: false + }, + time: 1702870898, + interval: 369363, + fullRbf: true, + replaces: [] + } + ... + ] + }, + replaces: [ + "3f4670463daadffed07d7a1060071b07f7e81a2566eca21d78bb513cbf21c82a", + "92f9b4f719d0ffc9035d3a9767d80c940cecbc656df2243bafd33f52b583ee92" + ] +}` + }, + codeSampleTestnet: { + curl: [`5faaa30530bee55de8cc896bdf48f803c2274a94bffc2842386bec2a8bf7a813`], + response: `{ + replacements: { + tx: { + txid: "5faaa30530bee55de8cc896bdf48f803c2274a94bffc2842386bec2a8bf7a813", + fee: 9101, + vsize: 318, + value: 148022607, + rate: 28.61949685534591, + rbf: true, + fullRbf: false, + mined: true + }, + time: 1703322610, + fullRbf: false, + replaces: [ + { + tx: { + txid: "06e69641fa889fe9148669ac2904929004e7140087bedaec8c8e4e05aabded52", + fee: 7000, + vsize: 318, + value: 148024708, + rate: 22.0125786163522, + rbf: true + }, + time: 1703322602, + interval: 8, + fullRbf: false, + replaces: [] + } + ], + mined: true + }, + replaces: [ + "06e69641fa889fe9148669ac2904929004e7140087bedaec8c8e4e05aabded52" + ] +}` + }, + codeSampleSignet: { + curl: [`13985a5717a1ea54ce720cd6b70421b1667061be491a6799acf6dea01c551248`], + response: `{ + replacements: { + tx: { + txid: "13985a5717a1ea54ce720cd6b70421b1667061be491a6799acf6dea01c551248", + fee: 5040, + vsize: 215.5, + value: 762745, + rate: 23.387470997679813, + rbf: true, + fullRbf: false, + mined: true + }, + time: 1703316272, + fullRbf: false, + replaces: [ + { + tx: { + txid: "eac5ec8487414c955f4a5d3b2e516c351aec5299f1335f9019a00907962386ce", + fee: 4560, + vsize: 215.25, + value: 763225, + rate: 21.18466898954704, + rbf: true + }, + time: 1703316270, + interval: 2, + fullRbf: false, + replaces: [] + } + ], + mined: true + }, + replaces: [ + "eac5ec8487414c955f4a5d3b2e516c351aec5299f1335f9019a00907962386ce" + ] +}` + }, + codeSampleLiquid: emptyCodeSample, + codeSampleLiquidTestnet: emptyCodeSample, + codeSampleBisq: emptyCodeSample, + } + } + }, { type: "endpoint", category: "transactions", @@ -6098,6 +7219,48 @@ export const restApiDocsData = [ } } }, + { + type: "endpoint", + category: "transactions", + httpRequestMethod: "GET", + fragment: "get-transaction-times", + title: "GET Transaction Times", + description: { + default: "Returns the timestamps when a list of unconfirmed transactions was initially observed in the mempool. If a transaction is not found in the mempool or has been mined, the timestamp will be 0." + }, + urlString: "/v1/transaction-times", + showConditions: bitcoinNetworks.concat(liquidNetworks), + showJsExamples: showJsExamplesDefaultFalse, + codeExample: { + default: { + codeTemplate: { + curl: `/api/v1/transaction-times?txId[]=%{1}&txId[]=%{2}`, + commonJS: ``, + esModule: ``, + }, + codeSampleMainnet: { + curl: ['51545ef0ec7f09196e60693b59369a134870985c8a90e5d42655b191de06285e', '6086089bd1c56a9c42a39d470cdfa7c12d4b52bf209608b390dfc4943f2d3851'], + response: `[1703082129,1702325558]` + }, + codeSampleTestnet: { + curl: ['25e7a95ebf10ed192ee91741653d8d970ac88f8e0cd6fb14cc6c7145116d3964', '1e158327e52acae35de94962e60e53fc70f6b175b0cfc3e2058bed4b895203b4'], + response: `[1703267563,1703267322]` + }, + codeSampleSignet: { + curl: ['8af0c5199acd89621244f2f61107fe5a9c7c7aad54928e8400651d03ca949aeb', '08f840f7b0c33c5b0fdadf1666e8a8c206836993d95fc1eeeef39b5ef9de03d0'], + response: `[1703267652,1703267696]` + }, + codeSampleLiquid: { + curl: ['6091498f06a3054f82a0c3e5be0a23030185c658dc3568684b0bccc4e759be11', '631212a073aa4ca392e3aeb469d1366ec2ee288988b106e4a6fc8dae8c4d7a9a'], + response: `[1703267652,1703267696]`, + }, + codeSampleLiquidTestnet: { + curl: ['fa8d43e47b2c4bbee12fd8bc1c7440028be2da6ac0f1df6ac77c983938c503fb', '26b12cd450f8fa8b6a527578db218bf212a60b2d5eb65c168f8eb3be6f5fd991'], + response: `[1703268185,1703268209]`, + }, + } + } + }, { type: "endpoint", category: "transactions", From d2a5adbd9d234aa5023cd43d7c73eb8b35902944 Mon Sep 17 00:00:00 2001 From: natsee Date: Thu, 28 Dec 2023 15:34:36 +0100 Subject: [PATCH 14/61] Fix duplicate address field in search results --- .../search-form/search-results/search-results.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/components/search-form/search-results/search-results.component.html b/frontend/src/app/components/search-form/search-results/search-results.component.html index d4f68edbd..6c096fc4e 100644 --- a/frontend/src/app/components/search-form/search-results/search-results.component.html +++ b/frontend/src/app/components/search-form/search-results/search-results.component.html @@ -35,7 +35,7 @@ - +
Bitcoin Addresses
+ +
Other Networks Address
+ + + +
Bitcoin Addresses
- @@ -46,7 +54,7 @@
Lightning Nodes
- @@ -54,7 +62,7 @@
Lightning Channels
- diff --git a/frontend/src/app/components/search-form/search-results/search-results.component.scss b/frontend/src/app/components/search-form/search-results/search-results.component.scss index 7cf9fe3c2..496e718d8 100644 --- a/frontend/src/app/components/search-form/search-results/search-results.component.scss +++ b/frontend/src/app/components/search-form/search-results/search-results.component.scss @@ -7,6 +7,10 @@ margin-left: 10px; } +.danger { + color: #dc3545; +} + .dropdown-menu { position: absolute; top: 42px; diff --git a/frontend/src/app/components/search-form/search-results/search-results.component.ts b/frontend/src/app/components/search-form/search-results/search-results.component.ts index fa9611529..84c541a70 100644 --- a/frontend/src/app/components/search-form/search-results/search-results.component.ts +++ b/frontend/src/app/components/search-form/search-results/search-results.component.ts @@ -22,7 +22,7 @@ export class SearchResultsComponent implements OnChanges { ngOnChanges() { this.activeIdx = 0; if (this.results) { - this.resultsFlattened = [...(this.results.hashQuickMatch ? [this.results.searchText] : []), ...this.results.addresses, ...this.results.nodes, ...this.results.channels]; + this.resultsFlattened = [...(this.results.hashQuickMatch ? [this.results.searchText] : []), ...this.results.otherNetworks, ...this.results.addresses, ...this.results.nodes, ...this.results.channels]; } } diff --git a/frontend/src/app/shared/regex.utils.ts b/frontend/src/app/shared/regex.utils.ts index d0cd08f24..4bb5373ef 100644 --- a/frontend/src/app/shared/regex.utils.ts +++ b/frontend/src/app/shared/regex.utils.ts @@ -1,3 +1,5 @@ +import { Env } from '../services/state.service'; + // all base58 characters const BASE58_CHARS = `[a-km-zA-HJ-NP-Z1-9]`; @@ -148,6 +150,41 @@ export function findOtherNetworks(address: string, skipNetwork: Network): {netwo .map(([, network]) => ({ network, address })); } +export function needBaseModuleChange(fromBaseModule: 'mempool' | 'liquid' | 'bisq', toNetwork: Network): boolean { + if (!toNetwork) return false; // No target network means no change needed + if (fromBaseModule === 'mempool') { + return toNetwork !== 'mainnet' && toNetwork !== 'testnet' && toNetwork !== 'signet'; + } + if (fromBaseModule === 'liquid') { + return toNetwork !== 'liquid' && toNetwork !== 'liquidtestnet'; + } + if (fromBaseModule === 'bisq') { + return toNetwork !== 'bisq'; + } +} + +export function getTargetUrl(toNetwork: Network, address: string, env: Env): string { + let targetUrl = ''; + if (toNetwork === 'liquid' || toNetwork === 'liquidtestnet') { + targetUrl = env.LIQUID_WEBSITE_URL; + targetUrl += (toNetwork === 'liquidtestnet' ? '/testnet' : ''); + targetUrl += '/address/'; + targetUrl += address; + } + if (toNetwork === 'bisq') { + targetUrl = env.BISQ_WEBSITE_URL; + targetUrl += '/address/'; + targetUrl += address; + } + if (toNetwork === 'mainnet' || toNetwork === 'testnet' || toNetwork === 'signet') { + targetUrl = env.MEMPOOL_WEBSITE_URL; + targetUrl += (toNetwork === 'mainnet' ? '' : `/${toNetwork}`); + targetUrl += '/address/'; + targetUrl += address; + } + return targetUrl; +} + export function getRegex(type: RegexTypeNoAddrNoBlockHash): RegExp; export function getRegex(type: 'address', network: Network): RegExp; export function getRegex(type: 'blockhash', network: Network): RegExp; From dd31d5a665cacc549c2355396e1330d241940d9b Mon Sep 17 00:00:00 2001 From: natsee Date: Mon, 1 Jan 2024 14:49:02 +0100 Subject: [PATCH 18/61] Fix E2E error: change regex address minimum length --- frontend/src/app/shared/regex.utils.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/frontend/src/app/shared/regex.utils.ts b/frontend/src/app/shared/regex.utils.ts index 4bb5373ef..607bd704a 100644 --- a/frontend/src/app/shared/regex.utils.ts +++ b/frontend/src/app/shared/regex.utils.ts @@ -41,11 +41,11 @@ const ADDRESS_CHARS: { bech32: `(?:` + `bc1` // Starts with bc1 + BECH32_CHARS_LW - + `{6,100}` // As per bech32, 6 char checksum is minimum + + `{20,100}` // As per bech32, 6 char checksum is minimum + `|` + `BC1` // All upper case version + BECH32_CHARS_UP - + `{6,100}` + + `{20,100}` + `)`, }, testnet: { @@ -55,11 +55,11 @@ const ADDRESS_CHARS: { bech32: `(?:` + `tb1` // Starts with tb1 + BECH32_CHARS_LW - + `{6,100}` // As per bech32, 6 char checksum is minimum + + `{20,100}` // As per bech32, 6 char checksum is minimum + `|` + `TB1` // All upper case version + BECH32_CHARS_UP - + `{6,100}` + + `{20,100}` + `)`, }, signet: { @@ -69,11 +69,11 @@ const ADDRESS_CHARS: { bech32: `(?:` + `tb1` // Starts with tb1 + BECH32_CHARS_LW - + `{6,100}` + + `{20,100}` + `|` + `TB1` // All upper case version + BECH32_CHARS_UP - + `{6,100}` + + `{20,100}` + `)`, }, liquid: { @@ -87,7 +87,7 @@ const ADDRESS_CHARS: { + `lq1` + `)` + BECH32_CHARS_LW // blech32 and bech32 are the same alphabet and protocol, different checksums. - + `{6,100}` + + `{20,100}` + `|` + `(?:` // Same as above but all upper case + `EX1` @@ -95,7 +95,7 @@ const ADDRESS_CHARS: { + `LQ1` + `)` + BECH32_CHARS_UP - + `{6,100}` + + `{20,100}` + `)`, }, liquidtestnet: { @@ -109,7 +109,7 @@ const ADDRESS_CHARS: { + `tlq1` // TODO: does this exist? + `)` + BECH32_CHARS_LW // blech32 and bech32 are the same alphabet and protocol, different checksums. - + `{6,100}` + + `{20,100}` + `|` + `(?:` // Same as above but all upper case + `TEX1` @@ -117,7 +117,7 @@ const ADDRESS_CHARS: { + `TLQ1` + `)` + BECH32_CHARS_UP - + `{6,100}` + + `{20,100}` + `)`, }, bisq: { @@ -127,11 +127,11 @@ const ADDRESS_CHARS: { bech32: `(?:` + `[bB]bc1` // b or B at the start, followed by bc1 + BECH32_CHARS_LW - + `{6,100}` + + `{20,100}` + `|` + `[bB]BC1` // b or B at the start, followed by BC1 + BECH32_CHARS_UP - + `{6,100}` + + `{20,100}` + `)`, }, } From c1785993a8e364756f2e8cdbc7c2a2ca2d099589 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 2 Jan 2024 16:26:57 +0000 Subject: [PATCH 19/61] Fix websocket subscription logging --- backend/src/api/websocket-handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index 34d4682d2..937d4a7c5 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -968,7 +968,7 @@ class WebsocketHandler { if (client['track-tx']) { numTxSubs++; } - if (client['track-mempool-block'] >= 0) { + if (client['track-mempool-block'] != null && client['track-mempool-block'] >= 0) { numProjectedSubs++; } if (client['track-rbf']) { From b9776263dc13db21624613eb7c7c36e4556eeea8 Mon Sep 17 00:00:00 2001 From: natsee Date: Wed, 3 Jan 2024 13:23:25 +0100 Subject: [PATCH 20/61] Exclude date and timestamp results for non-bitcoin networks --- .../app/components/search-form/search-form.component.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 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 ed46f1e88..c00537f5a 100644 --- a/frontend/src/app/components/search-form/search-form.component.ts +++ b/frontend/src/app/components/search-form/search-form.component.ts @@ -177,9 +177,12 @@ export class SearchFormComponent implements OnInit { const addressPrefixSearchResults = result[0]; const lightningResults = result[1]; + // Do not show date and timestamp results for liquid and bisq + const isNetworkBitcoin = this.network === '' || this.network === 'testnet' || this.network === 'signet'; + const matchesBlockHeight = this.regexBlockheight.test(searchText) && parseInt(searchText) <= this.stateService.latestBlockHeight; - 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 matchesDateTime = this.regexDate.test(searchText) && new Date(searchText).toString() !== 'Invalid Date' && new Date(searchText).getTime() <= Date.now() && isNetworkBitcoin; + const matchesUnixTimestamp = this.regexUnixTimestamp.test(searchText) && parseInt(searchText) <= Math.floor(Date.now() / 1000) && isNetworkBitcoin; const matchesTxId = this.regexTransaction.test(searchText) && !this.regexBlockhash.test(searchText); const matchesBlockHash = this.regexBlockhash.test(searchText); let matchesAddress = !matchesTxId && this.regexAddress.test(searchText); From f73350f7d641505597ce53b060e3cc359ccec278 Mon Sep 17 00:00:00 2001 From: natsee Date: Wed, 3 Jan 2024 14:21:11 +0100 Subject: [PATCH 21/61] [Liquid] Fix missing assets/featured route --- frontend/src/app/liquid/liquid-master-page.module.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/src/app/liquid/liquid-master-page.module.ts b/frontend/src/app/liquid/liquid-master-page.module.ts index 10d87bc4b..17fa6b8e5 100644 --- a/frontend/src/app/liquid/liquid-master-page.module.ts +++ b/frontend/src/app/liquid/liquid-master-page.module.ts @@ -10,6 +10,7 @@ import { PushTransactionComponent } from '../components/push-transaction/push-tr import { BlocksList } from '../components/blocks-list/blocks-list.component'; import { AssetGroupComponent } from '../components/assets/asset-group/asset-group.component'; import { AssetsComponent } from '../components/assets/assets.component'; +import { AssetsFeaturedComponent } from '../components/assets/assets-featured/assets-featured.component' import { AssetComponent } from '../components/asset/asset.component'; import { AssetsNavComponent } from '../components/assets/assets-nav/assets-nav.component'; @@ -73,6 +74,11 @@ const routes: Routes = [ data: { networks: ['liquid'] }, component: AssetsComponent, }, + { + path: 'featured', + data: { networks: ['liquid'] }, + component: AssetsFeaturedComponent, + }, { path: 'asset/:id', data: { networkSpecific: true }, From d665d2a12cb5bfc48c20a78e162b3e1365d74210 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sat, 6 Jan 2024 03:42:45 +0000 Subject: [PATCH 22/61] Handle unmineable transactions in GBT implementations --- backend/rust-gbt/src/gbt.rs | 11 +++++++---- backend/src/api/tx-selection-worker.ts | 3 +++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/backend/rust-gbt/src/gbt.rs b/backend/rust-gbt/src/gbt.rs index fb28dc299..412d4e5e9 100644 --- a/backend/rust-gbt/src/gbt.rs +++ b/backend/rust-gbt/src/gbt.rs @@ -127,7 +127,7 @@ pub fn gbt(mempool: &mut ThreadTransactionsMap, accelerations: &[ThreadAccelerat let next_from_stack = next_valid_from_stack(&mut mempool_stack, &audit_pool); let next_from_queue = next_valid_from_queue(&mut modified, &audit_pool); if next_from_stack.is_none() && next_from_queue.is_none() { - continue; + break; } let (next_tx, from_stack) = match (next_from_stack, next_from_queue) { (Some(stack_tx), Some(queue_tx)) => match queue_tx.cmp(stack_tx) { @@ -203,10 +203,13 @@ pub fn gbt(mempool: &mut ThreadTransactionsMap, accelerations: &[ThreadAccelerat let queue_is_empty = mempool_stack.is_empty() && modified.is_empty(); if (exceeded_package_tries || queue_is_empty) && blocks.len() < (MAX_BLOCKS - 1) { // finalize this block - if !transactions.is_empty() { - blocks.push(transactions); - block_weights.push(block_weight); + if transactions.is_empty() { + break; } + + blocks.push(transactions); + block_weights.push(block_weight); + // reset for the next block transactions = Vec::with_capacity(initial_txes_per_block); block_weight = BLOCK_RESERVED_WEIGHT; diff --git a/backend/src/api/tx-selection-worker.ts b/backend/src/api/tx-selection-worker.ts index 0acc2f65e..8ac7328fe 100644 --- a/backend/src/api/tx-selection-worker.ts +++ b/backend/src/api/tx-selection-worker.ts @@ -173,10 +173,13 @@ function makeBlockTemplates(mempool: Map) // this block is full const exceededPackageTries = failures > 1000 && blockWeight > (config.MEMPOOL.BLOCK_WEIGHT_UNITS - 4000); const queueEmpty = top >= mempoolArray.length && modified.isEmpty(); + if ((exceededPackageTries || queueEmpty) && blocks.length < 7) { // construct this block if (transactions.length) { blocks.push(transactions.map(t => t.uid)); + } else { + break; } // reset for the next block transactions = []; From 5f66a954023eddf805dd0022d53ee2763d2dafb1 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 7 Jan 2024 18:02:11 +0000 Subject: [PATCH 23/61] Smooth out irregular mempool block updates --- .../block-overview-graph/block-scene.ts | 2 +- .../mempool-block-overview.component.ts | 43 +++++++++++++------ 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/frontend/src/app/components/block-overview-graph/block-scene.ts b/frontend/src/app/components/block-overview-graph/block-scene.ts index cb589527d..adcf736fc 100644 --- a/frontend/src/app/components/block-overview-graph/block-scene.ts +++ b/frontend/src/app/components/block-overview-graph/block-scene.ts @@ -11,7 +11,7 @@ export default class BlockScene { getColor: ((tx: TxView) => Color) = defaultColorFunction; orientation: string; flip: boolean; - animationDuration: number = 1000; + animationDuration: number = 900; configAnimationOffset: number | null; animationOffset: number; highlightingEnabled: boolean; diff --git a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts index f09b8f5ea..84c9a3416 100644 --- a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts +++ b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts @@ -3,8 +3,8 @@ import { Component, ComponentRef, ViewChild, HostListener, Input, Output, EventE import { StateService } from '../../services/state.service'; import { MempoolBlockDelta, TransactionStripped } from '../../interfaces/websocket.interface'; import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component'; -import { Subscription, BehaviorSubject, merge, of } from 'rxjs'; -import { switchMap, filter } from 'rxjs/operators'; +import { Subscription, BehaviorSubject, merge, of, timer } from 'rxjs'; +import { switchMap, filter, concatMap, map } from 'rxjs/operators'; import { WebsocketService } from '../../services/websocket.service'; import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; import { Router } from '@angular/router'; @@ -33,7 +33,8 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang poolDirection: string = 'left'; blockSub: Subscription; - deltaSub: Subscription; + rateLimit = 1000; + private lastEventTime = Date.now() - this.rateLimit; firstLoad: boolean = true; @@ -55,11 +56,32 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang ngAfterViewInit(): void { this.blockSub = merge( - of(true), - this.stateService.connectionState$.pipe(filter((state) => state === 2)) - ) - .pipe(switchMap(() => this.stateService.mempoolBlockTransactions$)) - .subscribe((transactionsStripped) => { + this.stateService.mempoolBlockTransactions$, + this.stateService.mempoolBlockDelta$, + ).pipe( + concatMap(event => { + const now = Date.now(); + const timeSinceLastEvent = now - this.lastEventTime; + this.lastEventTime = Math.max(now, this.lastEventTime + this.rateLimit); + + // If time since last event is less than X seconds, delay this event + if (timeSinceLastEvent < this.rateLimit) { + return timer(this.rateLimit - timeSinceLastEvent).pipe( + // Emit the event after the timer + map(() => event) + ); + } else { + // If enough time has passed, emit the event immediately + return of(event); + } + }) + ).subscribe((update) => { + if (update['added']) { + // delta + this.updateBlock(update as MempoolBlockDelta); + } else { + const transactionsStripped = update as TransactionStripped[]; + // new transactions if (this.firstLoad) { this.replaceBlock(transactionsStripped); } else { @@ -94,9 +116,7 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang added }); } - }); - this.deltaSub = this.stateService.mempoolBlockDelta$.subscribe((delta) => { - this.updateBlock(delta); + } }); } @@ -113,7 +133,6 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang ngOnDestroy(): void { this.blockSub.unsubscribe(); - this.deltaSub.unsubscribe(); this.timeLtrSubscription.unsubscribe(); this.websocketService.stopTrackMempoolBlock(); } From 9485dfe2ce7d69d6375ddba1d2b1e9b7e6b667de Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jan 2024 02:48:43 +0000 Subject: [PATCH 24/61] Bump cypress-fail-on-console-error from 5.0.0 to 5.1.0 in /frontend Bumps [cypress-fail-on-console-error](https://github.com/nils-hoyer/cypress-fail-on-console-error) from 5.0.0 to 5.1.0. - [Release notes](https://github.com/nils-hoyer/cypress-fail-on-console-error/releases) - [Commits](https://github.com/nils-hoyer/cypress-fail-on-console-error/compare/5.0.0...5.1.0) --- updated-dependencies: - dependency-name: cypress-fail-on-console-error dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- frontend/package-lock.json | 187 +++++++++++++++++++++++++------------ frontend/package.json | 2 +- 2 files changed, 127 insertions(+), 62 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b02ee1c50..dfd5b1f72 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -31,6 +31,7 @@ "bootstrap": "~4.6.2", "browserify": "^17.0.0", "clipboard": "^2.0.11", + "cypress-fail-on-console-error": "~5.1.0", "domino": "^2.1.6", "echarts": "~5.4.3", "lightweight-charts": "~3.8.0", @@ -59,7 +60,7 @@ "@cypress/schematic": "^2.5.0", "@types/cypress": "^1.1.3", "cypress": "^13.6.0", - "cypress-fail-on-console-error": "~5.0.0", + "cypress-fail-on-console-error": "~5.1.0", "cypress-wait-until": "^2.0.1", "mock-socket": "~9.3.1", "start-server-and-test": "~2.0.0" @@ -4068,9 +4069,9 @@ } }, "node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", "optional": true, "dependencies": { "@sinonjs/commons": "^3.0.0" @@ -6242,17 +6243,18 @@ "optional": true }, "node_modules/chai": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz", - "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.0.tgz", + "integrity": "sha512-x9cHNq1uvkCdU+5xTkNh5WtgD4e4yDFCsp9jVc7N7qVeKeftv3gO/ZrviX5d+3ZfxdYnZXZYujjRInu1RogU6A==", "optional": true, "dependencies": { "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^3.0.1", - "get-func-name": "^2.0.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", "pathval": "^1.1.1", - "type-detect": "^4.0.5" + "type-detect": "^4.0.8" }, "engines": { "node": ">=4" @@ -6277,10 +6279,13 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" }, "node_modules/check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", "optional": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, "engines": { "node": "*" } @@ -7137,13 +7142,13 @@ } }, "node_modules/cypress-fail-on-console-error": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cypress-fail-on-console-error/-/cypress-fail-on-console-error-5.0.0.tgz", - "integrity": "sha512-xui/aSu8rmExZjZNgId3iX0MsGZih6ZoFH+54vNHrK3HaqIZZX5hUuNhAcmfSoM1rIDc2DeITeVaMn/hiQ9IWQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cypress-fail-on-console-error/-/cypress-fail-on-console-error-5.1.0.tgz", + "integrity": "sha512-u/AXLE9obLd9KcGHkGJluJVZeOj1EEOFOs0URxxca4FrftUDJQ3u+IoNfjRUjsrBKmJxgM4vKd0G10D+ZT1uIA==", "optional": true, "dependencies": { - "chai": "^4.3.4", - "sinon": "^15.0.0", + "chai": "^4.3.10", + "sinon": "^17.0.0", "sinon-chai": "^3.7.0", "type-detect": "^4.0.8" } @@ -7403,15 +7408,15 @@ "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=" }, "node_modules/deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", "optional": true, "dependencies": { "type-detect": "^4.0.0" }, "engines": { - "node": ">=0.12" + "node": ">=6" } }, "node_modules/deep-equal": { @@ -11759,6 +11764,15 @@ "node": ">=8.0" } }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "optional": true, + "dependencies": { + "get-func-name": "^2.0.1" + } + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -12537,9 +12551,9 @@ } }, "node_modules/nise": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.4.tgz", - "integrity": "sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.5.tgz", + "integrity": "sha512-VJuPIfUFaXNRzETTQEEItTOP8Y171ijr+JLq42wHes3DiryR8vT+1TXQW/Rx8JNUhyYYWyIvjXTU6dOhJcs9Nw==", "optional": true, "dependencies": { "@sinonjs/commons": "^2.0.0", @@ -12558,6 +12572,24 @@ "type-detect": "4.0.8" } }, + "node_modules/nise/node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "optional": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/nise/node_modules/@sinonjs/fake-timers/node_modules/@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "optional": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, "node_modules/nise/node_modules/isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", @@ -14842,16 +14874,16 @@ ] }, "node_modules/sinon": { - "version": "15.2.0", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-15.2.0.tgz", - "integrity": "sha512-nPS85arNqwBXaIsFCkolHjGIkFo+Oxu9vbgmBJizLAhqe6P2o3Qmj3KCUoRkfhHtvgDhZdWD3risLHAUJ8npjw==", + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz", + "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==", "optional": true, "dependencies": { "@sinonjs/commons": "^3.0.0", - "@sinonjs/fake-timers": "^10.3.0", + "@sinonjs/fake-timers": "^11.2.2", "@sinonjs/samsam": "^8.0.0", "diff": "^5.1.0", - "nise": "^5.1.4", + "nise": "^5.1.5", "supports-color": "^7.2.0" }, "funding": { @@ -19882,9 +19914,9 @@ } }, "@sinonjs/fake-timers": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", "optional": true, "requires": { "@sinonjs/commons": "^3.0.0" @@ -21594,17 +21626,18 @@ "optional": true }, "chai": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz", - "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.0.tgz", + "integrity": "sha512-x9cHNq1uvkCdU+5xTkNh5WtgD4e4yDFCsp9jVc7N7qVeKeftv3gO/ZrviX5d+3ZfxdYnZXZYujjRInu1RogU6A==", "optional": true, "requires": { "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^3.0.1", - "get-func-name": "^2.0.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", "pathval": "^1.1.1", - "type-detect": "^4.0.5" + "type-detect": "^4.0.8" } }, "chalk": { @@ -21623,10 +21656,13 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" }, "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", - "optional": true + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "optional": true, + "requires": { + "get-func-name": "^2.0.2" + } }, "check-more-types": { "version": "2.24.0", @@ -22403,13 +22439,13 @@ } }, "cypress-fail-on-console-error": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cypress-fail-on-console-error/-/cypress-fail-on-console-error-5.0.0.tgz", - "integrity": "sha512-xui/aSu8rmExZjZNgId3iX0MsGZih6ZoFH+54vNHrK3HaqIZZX5hUuNhAcmfSoM1rIDc2DeITeVaMn/hiQ9IWQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cypress-fail-on-console-error/-/cypress-fail-on-console-error-5.1.0.tgz", + "integrity": "sha512-u/AXLE9obLd9KcGHkGJluJVZeOj1EEOFOs0URxxca4FrftUDJQ3u+IoNfjRUjsrBKmJxgM4vKd0G10D+ZT1uIA==", "optional": true, "requires": { - "chai": "^4.3.4", - "sinon": "^15.0.0", + "chai": "^4.3.10", + "sinon": "^17.0.0", "sinon-chai": "^3.7.0", "type-detect": "^4.0.8" } @@ -22490,9 +22526,9 @@ "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=" }, "deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", "optional": true, "requires": { "type-detect": "^4.0.0" @@ -25754,6 +25790,15 @@ "streamroller": "^3.0.2" } }, + "loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "optional": true, + "requires": { + "get-func-name": "^2.0.1" + } + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -26361,9 +26406,9 @@ } }, "nise": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.4.tgz", - "integrity": "sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.5.tgz", + "integrity": "sha512-VJuPIfUFaXNRzETTQEEItTOP8Y171ijr+JLq42wHes3DiryR8vT+1TXQW/Rx8JNUhyYYWyIvjXTU6dOhJcs9Nw==", "optional": true, "requires": { "@sinonjs/commons": "^2.0.0", @@ -26382,6 +26427,26 @@ "type-detect": "4.0.8" } }, + "@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "optional": true, + "requires": { + "@sinonjs/commons": "^3.0.0" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "optional": true, + "requires": { + "type-detect": "4.0.8" + } + } + } + }, "isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", @@ -28036,16 +28101,16 @@ "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" }, "sinon": { - "version": "15.2.0", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-15.2.0.tgz", - "integrity": "sha512-nPS85arNqwBXaIsFCkolHjGIkFo+Oxu9vbgmBJizLAhqe6P2o3Qmj3KCUoRkfhHtvgDhZdWD3risLHAUJ8npjw==", + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz", + "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==", "optional": true, "requires": { "@sinonjs/commons": "^3.0.0", - "@sinonjs/fake-timers": "^10.3.0", + "@sinonjs/fake-timers": "^11.2.2", "@sinonjs/samsam": "^8.0.0", "diff": "^5.1.0", - "nise": "^5.1.4", + "nise": "^5.1.5", "supports-color": "^7.2.0" }, "dependencies": { diff --git a/frontend/package.json b/frontend/package.json index a4a4ac462..8dbcbcf3e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -111,7 +111,7 @@ "@cypress/schematic": "^2.5.0", "@types/cypress": "^1.1.3", "cypress": "^13.6.0", - "cypress-fail-on-console-error": "~5.0.0", + "cypress-fail-on-console-error": "~5.1.0", "cypress-wait-until": "^2.0.1", "mock-socket": "~9.3.1", "start-server-and-test": "~2.0.0" From fc8eca4c26c38b2e57ce86c62f854f44e072e71d Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 8 Jan 2024 14:42:00 +0000 Subject: [PATCH 25/61] Handle stale smoothed mempool block updates --- .../mempool-block-overview.component.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts index 84c9a3416..8dad6a9c1 100644 --- a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts +++ b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts @@ -35,6 +35,7 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang blockSub: Subscription; rateLimit = 1000; private lastEventTime = Date.now() - this.rateLimit; + private subId = 0; firstLoad: boolean = true; @@ -59,23 +60,30 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang this.stateService.mempoolBlockTransactions$, this.stateService.mempoolBlockDelta$, ).pipe( - concatMap(event => { + concatMap(update => { const now = Date.now(); const timeSinceLastEvent = now - this.lastEventTime; this.lastEventTime = Math.max(now, this.lastEventTime + this.rateLimit); + const subId = this.subId; + // If time since last event is less than X seconds, delay this event if (timeSinceLastEvent < this.rateLimit) { return timer(this.rateLimit - timeSinceLastEvent).pipe( // Emit the event after the timer - map(() => event) + map(() => ({ update, subId })) ); } else { // If enough time has passed, emit the event immediately - return of(event); + return of({ update, subId }); } }) - ).subscribe((update) => { + ).subscribe(({ update, subId }) => { + // discard stale updates after a block transition + if (subId !== this.subId) { + return; + } + // process update if (update['added']) { // delta this.updateBlock(update as MempoolBlockDelta); @@ -122,6 +130,7 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang ngOnChanges(changes): void { if (changes.index) { + this.subId++; this.firstLoad = true; if (this.blockGraph) { this.blockGraph.clear(changes.index.currentValue > changes.index.previousValue ? this.chainDirection : this.poolDirection); From d5c5ae0e090adda89872f2e21f75dbecab937da0 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 8 Jan 2024 16:33:19 +0000 Subject: [PATCH 26/61] Additional fee recommendation sanity checks --- backend/src/api/fee-api.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/backend/src/api/fee-api.ts b/backend/src/api/fee-api.ts index 0cab5a295..24fd25a4b 100644 --- a/backend/src/api/fee-api.ts +++ b/backend/src/api/fee-api.ts @@ -39,15 +39,25 @@ class FeeApi { const secondMedianFee = pBlocks[1] ? this.optimizeMedianFee(pBlocks[1], pBlocks[2], firstMedianFee) : this.defaultFee; const thirdMedianFee = pBlocks[2] ? this.optimizeMedianFee(pBlocks[2], pBlocks[3], secondMedianFee) : this.defaultFee; + let fastestFee = Math.max(minimumFee, firstMedianFee); + let halfHourFee = Math.max(minimumFee, secondMedianFee); + let hourFee = Math.max(minimumFee, thirdMedianFee); + const economyFee = Math.max(minimumFee, Math.min(2 * minimumFee, thirdMedianFee)); + + // ensure recommendations always increase w/ priority + fastestFee = Math.max(fastestFee, halfHourFee, hourFee, economyFee); + halfHourFee = Math.max(halfHourFee, hourFee, economyFee); + hourFee = Math.max(hourFee, economyFee); + // explicitly enforce a minimum of ceil(mempoolminfee) on all recommendations. // simply rounding up recommended rates is insufficient, as the purging rate // can exceed the median rate of projected blocks in some extreme scenarios // (see https://bitcoin.stackexchange.com/a/120024) return { - 'fastestFee': Math.max(minimumFee, firstMedianFee), - 'halfHourFee': Math.max(minimumFee, secondMedianFee), - 'hourFee': Math.max(minimumFee, thirdMedianFee), - 'economyFee': Math.max(minimumFee, Math.min(2 * minimumFee, thirdMedianFee)), + 'fastestFee': fastestFee, + 'halfHourFee': halfHourFee, + 'hourFee': hourFee, + 'economyFee': economyFee, 'minimumFee': minimumFee, }; } From 0230f95001875039995c6f68a3adc80f85ce0eea Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 8 Jan 2024 22:33:11 +0000 Subject: [PATCH 27/61] Fix bad coinbase price url, switch to median prices --- backend/src/tasks/price-feeds/coinbase-api.ts | 4 ++-- backend/src/tasks/price-updater.ts | 14 ++++++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/backend/src/tasks/price-feeds/coinbase-api.ts b/backend/src/tasks/price-feeds/coinbase-api.ts index 424ac8867..d2c6d063a 100644 --- a/backend/src/tasks/price-feeds/coinbase-api.ts +++ b/backend/src/tasks/price-feeds/coinbase-api.ts @@ -5,14 +5,14 @@ class CoinbaseApi implements PriceFeed { public name: string = 'Coinbase'; public currencies: string[] = ['USD', 'EUR', 'GBP']; - public url: string = 'https://api.coinbase.com/v2/prices/spot?currency='; + public url: string = 'https://api.coinbase.com/v2/prices/BTC-{CURRENCY}/spot'; public urlHist: string = 'https://api.exchange.coinbase.com/products/BTC-{CURRENCY}/candles?granularity={GRANULARITY}'; constructor() { } public async $fetchPrice(currency): Promise { - const response = await query(this.url + currency); + const response = await query(this.url.replace('{CURRENCY}', currency)); if (response && response['data'] && response['data']['amount']) { return parseInt(response['data']['amount'], 10); } else { diff --git a/backend/src/tasks/price-updater.ts b/backend/src/tasks/price-updater.ts index fd799fb87..0d5ca5958 100644 --- a/backend/src/tasks/price-updater.ts +++ b/backend/src/tasks/price-updater.ts @@ -23,6 +23,14 @@ export interface PriceHistory { [timestamp: number]: ApiPrice; } +function getMedian(arr: number[]): number { + const sortedArr = arr.slice().sort((a, b) => a - b); + const mid = Math.floor(sortedArr.length / 2); + return sortedArr.length % 2 !== 0 + ? sortedArr[mid] + : (sortedArr[mid - 1] + sortedArr[mid]) / 2; +} + class PriceUpdater { public historyInserted = false; private timeBetweenUpdatesMs = 360_0000 / config.MEMPOOL.PRICE_UPDATES_PER_HOUR; @@ -173,7 +181,7 @@ class PriceUpdater { if (prices.length === 0) { this.latestPrices[currency] = -1; } else { - this.latestPrices[currency] = Math.round((prices.reduce((partialSum, a) => partialSum + a, 0)) / prices.length); + this.latestPrices[currency] = Math.round(getMedian(prices)); } } @@ -300,9 +308,7 @@ class PriceUpdater { if (grouped[time][currency].length === 0) { continue; } - prices[currency] = Math.round((grouped[time][currency].reduce( - (partialSum, a) => partialSum + a, 0) - ) / grouped[time][currency].length); + prices[currency] = Math.round(getMedian(grouped[time][currency])); } await PricesRepository.$savePrices(parseInt(time, 10), prices); ++totalInserted; From 89d37f00580eb033dcca81dd27c33b12fa178da5 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 9 Jan 2024 16:18:04 +0000 Subject: [PATCH 28/61] Fix unmineable tx handling --- backend/rust-gbt/src/gbt.rs | 129 +++++++++++++++--------------- backend/src/api/mempool-blocks.ts | 11 +-- 2 files changed, 72 insertions(+), 68 deletions(-) diff --git a/backend/rust-gbt/src/gbt.rs b/backend/rust-gbt/src/gbt.rs index 412d4e5e9..e1ceeefb6 100644 --- a/backend/rust-gbt/src/gbt.rs +++ b/backend/rust-gbt/src/gbt.rs @@ -60,6 +60,7 @@ pub fn gbt(mempool: &mut ThreadTransactionsMap, accelerations: &[ThreadAccelerat indexed_accelerations[acceleration.uid as usize] = Some(acceleration); } + info!("Initializing working vecs with uid capacity for {}", max_uid + 1); let mempool_len = mempool.len(); let mut audit_pool: AuditPool = Vec::with_capacity(max_uid + 1); audit_pool.resize(max_uid + 1, None); @@ -127,74 +128,75 @@ pub fn gbt(mempool: &mut ThreadTransactionsMap, accelerations: &[ThreadAccelerat let next_from_stack = next_valid_from_stack(&mut mempool_stack, &audit_pool); let next_from_queue = next_valid_from_queue(&mut modified, &audit_pool); if next_from_stack.is_none() && next_from_queue.is_none() { - break; - } - let (next_tx, from_stack) = match (next_from_stack, next_from_queue) { - (Some(stack_tx), Some(queue_tx)) => match queue_tx.cmp(stack_tx) { - std::cmp::Ordering::Less => (stack_tx, true), - _ => (queue_tx, false), - }, - (Some(stack_tx), None) => (stack_tx, true), - (None, Some(queue_tx)) => (queue_tx, false), - (None, None) => unreachable!(), - }; - - if from_stack { - mempool_stack.pop(); + info!("No transactions left! {:#?} in overflow", overflow.len()); } else { - modified.pop(); - } + let (next_tx, from_stack) = match (next_from_stack, next_from_queue) { + (Some(stack_tx), Some(queue_tx)) => match queue_tx.cmp(stack_tx) { + std::cmp::Ordering::Less => (stack_tx, true), + _ => (queue_tx, false), + }, + (Some(stack_tx), None) => (stack_tx, true), + (None, Some(queue_tx)) => (queue_tx, false), + (None, None) => unreachable!(), + }; - if blocks.len() < (MAX_BLOCKS - 1) - && ((block_weight + (4 * next_tx.ancestor_sigop_adjusted_vsize()) - >= MAX_BLOCK_WEIGHT_UNITS) - || (block_sigops + next_tx.ancestor_sigops() > BLOCK_SIGOPS)) - { - // hold this package in an overflow list while we check for smaller options - overflow.push(next_tx.uid); - failures += 1; - } else { - let mut package: Vec<(u32, u32, usize)> = Vec::new(); - let mut cluster: Vec = Vec::new(); - let is_cluster: bool = !next_tx.ancestors.is_empty(); - for ancestor_id in &next_tx.ancestors { - if let Some(Some(ancestor)) = audit_pool.get(*ancestor_id as usize) { - package.push((*ancestor_id, ancestor.order(), ancestor.ancestors.len())); - } - } - package.sort_unstable_by(|a, b| -> Ordering { - if a.2 != b.2 { - // order by ascending ancestor count - a.2.cmp(&b.2) - } else if a.1 != b.1 { - // tie-break by ascending partial txid - a.1.cmp(&b.1) - } else { - // tie-break partial txid collisions by ascending uid - a.0.cmp(&b.0) - } - }); - package.push((next_tx.uid, next_tx.order(), next_tx.ancestors.len())); - - let cluster_rate = next_tx.cluster_rate(); - - for (txid, _, _) in &package { - cluster.push(*txid); - if let Some(Some(tx)) = audit_pool.get_mut(*txid as usize) { - tx.used = true; - tx.set_dirty_if_different(cluster_rate); - transactions.push(tx.uid); - block_weight += tx.weight; - block_sigops += tx.sigops; - } - update_descendants(*txid, &mut audit_pool, &mut modified, cluster_rate); + if from_stack { + mempool_stack.pop(); + } else { + modified.pop(); } - if is_cluster { - clusters.push(cluster); - } + if blocks.len() < (MAX_BLOCKS - 1) + && ((block_weight + (4 * next_tx.ancestor_sigop_adjusted_vsize()) + >= MAX_BLOCK_WEIGHT_UNITS) + || (block_sigops + next_tx.ancestor_sigops() > BLOCK_SIGOPS)) + { + // hold this package in an overflow list while we check for smaller options + overflow.push(next_tx.uid); + failures += 1; + } else { + let mut package: Vec<(u32, u32, usize)> = Vec::new(); + let mut cluster: Vec = Vec::new(); + let is_cluster: bool = !next_tx.ancestors.is_empty(); + for ancestor_id in &next_tx.ancestors { + if let Some(Some(ancestor)) = audit_pool.get(*ancestor_id as usize) { + package.push((*ancestor_id, ancestor.order(), ancestor.ancestors.len())); + } + } + package.sort_unstable_by(|a, b| -> Ordering { + if a.2 != b.2 { + // order by ascending ancestor count + a.2.cmp(&b.2) + } else if a.1 != b.1 { + // tie-break by ascending partial txid + a.1.cmp(&b.1) + } else { + // tie-break partial txid collisions by ascending uid + a.0.cmp(&b.0) + } + }); + package.push((next_tx.uid, next_tx.order(), next_tx.ancestors.len())); - failures = 0; + let cluster_rate = next_tx.cluster_rate(); + + for (txid, _, _) in &package { + cluster.push(*txid); + if let Some(Some(tx)) = audit_pool.get_mut(*txid as usize) { + tx.used = true; + tx.set_dirty_if_different(cluster_rate); + transactions.push(tx.uid); + block_weight += tx.weight; + block_sigops += tx.sigops; + } + update_descendants(*txid, &mut audit_pool, &mut modified, cluster_rate); + } + + if is_cluster { + clusters.push(cluster); + } + + failures = 0; + } } // this block is full @@ -204,6 +206,7 @@ pub fn gbt(mempool: &mut ThreadTransactionsMap, accelerations: &[ThreadAccelerat if (exceeded_package_tries || queue_is_empty) && blocks.len() < (MAX_BLOCKS - 1) { // finalize this block if transactions.is_empty() { + info!("trying to push an empty block! breaking loop! mempool {:#?} | modified {:#?} | overflow {:#?}", mempool_stack.len(), modified.len(), overflow.len()); break; } diff --git a/backend/src/api/mempool-blocks.ts b/backend/src/api/mempool-blocks.ts index a7f00f6e8..2097acd4b 100644 --- a/backend/src/api/mempool-blocks.ts +++ b/backend/src/api/mempool-blocks.ts @@ -432,15 +432,16 @@ class MempoolBlocks { this.nextUid, ), ); - const resultMempoolSize = blocks.reduce((total, block) => total + block.length, 0); - if (mempoolSize !== resultMempoolSize) { - throw new Error('GBT returned wrong number of transactions, cache is probably out of sync'); - } else { + //// different number of transactions is now expected, if any were unmineable + // const resultMempoolSize = blocks.reduce((total, block) => total + block.length, 0); + // if (mempoolSize !== resultMempoolSize) { + // throw new Error('GBT returned wrong number of transactions, cache is probably out of sync'); + // } else { const processed = this.processBlockTemplates(newMempool, blocks, blockWeights, rates, clusters, accelerations, accelerationPool, true); this.removeUids(removedUids); logger.debug(`RUST updateBlockTemplates completed in ${(Date.now() - start)/1000} seconds`); return processed; - } + // } } catch (e) { logger.err('RUST updateBlockTemplates failed. ' + (e instanceof Error ? e.message : e)); this.resetRustGbt(); From 30d58d9971bd73c4ba19d016d4004653d3b40f1a Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 9 Jan 2024 17:08:25 +0000 Subject: [PATCH 29/61] Restore GBT result size sanity check --- backend/rust-gbt/index.d.ts | 3 ++- backend/rust-gbt/src/gbt.rs | 1 + backend/rust-gbt/src/lib.rs | 1 + backend/src/api/mempool-blocks.ts | 33 ++++++++++++++++++++----------- 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/backend/rust-gbt/index.d.ts b/backend/rust-gbt/index.d.ts index 2bd8a620a..d1cb85b92 100644 --- a/backend/rust-gbt/index.d.ts +++ b/backend/rust-gbt/index.d.ts @@ -45,5 +45,6 @@ export class GbtResult { blockWeights: Array clusters: Array> rates: Array> - constructor(blocks: Array>, blockWeights: Array, clusters: Array>, rates: Array>) + overflow: Array + constructor(blocks: Array>, blockWeights: Array, clusters: Array>, rates: Array>, overflow: Array) } diff --git a/backend/rust-gbt/src/gbt.rs b/backend/rust-gbt/src/gbt.rs index e1ceeefb6..38bf826a6 100644 --- a/backend/rust-gbt/src/gbt.rs +++ b/backend/rust-gbt/src/gbt.rs @@ -271,6 +271,7 @@ pub fn gbt(mempool: &mut ThreadTransactionsMap, accelerations: &[ThreadAccelerat block_weights, clusters, rates, + overflow, } } diff --git a/backend/rust-gbt/src/lib.rs b/backend/rust-gbt/src/lib.rs index 53db0ba21..edc9714ee 100644 --- a/backend/rust-gbt/src/lib.rs +++ b/backend/rust-gbt/src/lib.rs @@ -133,6 +133,7 @@ pub struct GbtResult { pub block_weights: Vec, pub clusters: Vec>, pub rates: Vec>, // Tuples not supported. u32 fits inside f64 + pub overflow: Vec, } /// All on another thread, this runs an arbitrary task in between diff --git a/backend/src/api/mempool-blocks.ts b/backend/src/api/mempool-blocks.ts index 2097acd4b..0ca550f4c 100644 --- a/backend/src/api/mempool-blocks.ts +++ b/backend/src/api/mempool-blocks.ts @@ -368,12 +368,15 @@ class MempoolBlocks { // run the block construction algorithm in a separate thread, and wait for a result const rustGbt = saveResults ? this.rustGbtGenerator : new GbtGenerator(); try { - const { blocks, blockWeights, rates, clusters } = this.convertNapiResultTxids( + const { blocks, blockWeights, rates, clusters, overflow } = this.convertNapiResultTxids( await rustGbt.make(Object.values(newMempool) as RustThreadTransaction[], convertedAccelerations as RustThreadAcceleration[], this.nextUid), ); if (saveResults) { this.rustInitialized = true; } + const mempoolSize = Object.keys(newMempool).length; + const resultMempoolSize = blocks.reduce((total, block) => total + block.length, 0) + overflow.length; + logger.debug(`RUST updateBlockTemplates returned ${resultMempoolSize} txs out of ${mempoolSize} in the mempool, ${overflow.length} were unmineable`); const processed = this.processBlockTemplates(newMempool, blocks, blockWeights, rates, clusters, accelerations, accelerationPool, saveResults); logger.debug(`RUST makeBlockTemplates completed in ${(Date.now() - start)/1000} seconds`); return processed; @@ -424,7 +427,7 @@ class MempoolBlocks { // run the block construction algorithm in a separate thread, and wait for a result try { - const { blocks, blockWeights, rates, clusters } = this.convertNapiResultTxids( + const { blocks, blockWeights, rates, clusters, overflow } = this.convertNapiResultTxids( await this.rustGbtGenerator.update( added as RustThreadTransaction[], removedUids, @@ -432,16 +435,16 @@ class MempoolBlocks { this.nextUid, ), ); - //// different number of transactions is now expected, if any were unmineable - // const resultMempoolSize = blocks.reduce((total, block) => total + block.length, 0); - // if (mempoolSize !== resultMempoolSize) { - // throw new Error('GBT returned wrong number of transactions, cache is probably out of sync'); - // } else { + const resultMempoolSize = blocks.reduce((total, block) => total + block.length, 0) + overflow.length; + logger.debug(`RUST updateBlockTemplates returned ${resultMempoolSize} txs out of ${mempoolSize} in the mempool, ${overflow.length} were unmineable`); + if (mempoolSize !== resultMempoolSize) { + throw new Error('GBT returned wrong number of transactions , cache is probably out of sync'); + } else { const processed = this.processBlockTemplates(newMempool, blocks, blockWeights, rates, clusters, accelerations, accelerationPool, true); this.removeUids(removedUids); logger.debug(`RUST updateBlockTemplates completed in ${(Date.now() - start)/1000} seconds`); return processed; - // } + } } catch (e) { logger.err('RUST updateBlockTemplates failed. ' + (e instanceof Error ? e.message : e)); this.resetRustGbt(); @@ -659,8 +662,8 @@ class MempoolBlocks { return { blocks: convertedBlocks, rates: convertedRates, clusters: convertedClusters } as { blocks: string[][], rates: { [root: string]: number }, clusters: { [root: string]: string[] }}; } - private convertNapiResultTxids({ blocks, blockWeights, rates, clusters }: GbtResult) - : { blocks: string[][], blockWeights: number[], rates: [string, number][], clusters: string[][] } { + private convertNapiResultTxids({ blocks, blockWeights, rates, clusters, overflow }: GbtResult) + : { blocks: string[][], blockWeights: number[], rates: [string, number][], clusters: string[][], overflow: string[] } { const convertedBlocks: string[][] = blocks.map(block => block.map(uid => { const txid = this.uidMap.get(uid); if (txid !== undefined) { @@ -678,7 +681,15 @@ class MempoolBlocks { for (const cluster of clusters) { convertedClusters.push(cluster.map(uid => this.uidMap.get(uid)) as string[]); } - return { blocks: convertedBlocks, blockWeights, rates: convertedRates, clusters: convertedClusters }; + const convertedOverflow: string[] = overflow.map(uid => { + const txid = this.uidMap.get(uid); + if (txid !== undefined) { + return txid; + } else { + throw new Error('GBT returned an unmineable transaction with unknown uid'); + } + }); + return { blocks: convertedBlocks, blockWeights, rates: convertedRates, clusters: convertedClusters, overflow: convertedOverflow }; } } From 8336a00aead1ad32dbf3606374c4c1420dc1e712 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 10 Jan 2024 14:32:37 +0000 Subject: [PATCH 30/61] Clean rust-gbt directory before build --- backend/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/package.json b/backend/package.json index 0c1d3cc4a..e2417524b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -35,7 +35,8 @@ "lint": "./node_modules/.bin/eslint . --ext .ts", "lint:fix": "./node_modules/.bin/eslint . --ext .ts --fix", "prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\"", - "rust-build": "cd rust-gbt && npm run build-release" + "rust-clean": "cd rust-gbt && rm -f *.node index.d.ts index.js && rm -rf target && cd ../", + "rust-build": "npm run rust-clean && cd rust-gbt && npm run build-release" }, "dependencies": { "@babel/core": "^7.23.2", From 367f70c3b3fb7a88546c6fce76e153ab8478046e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jan 2024 14:40:20 +0000 Subject: [PATCH 31/61] Bump mysql2 from 3.6.0 to 3.7.0 in /backend Bumps [mysql2](https://github.com/sidorares/node-mysql2) from 3.6.0 to 3.7.0. - [Release notes](https://github.com/sidorares/node-mysql2/releases) - [Changelog](https://github.com/sidorares/node-mysql2/blob/master/Changelog.md) - [Commits](https://github.com/sidorares/node-mysql2/compare/v3.6.0...v3.7.0) --- updated-dependencies: - dependency-name: mysql2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- backend/package-lock.json | 14 +++++++------- backend/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 3e9e31988..b3b659459 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -17,7 +17,7 @@ "crypto-js": "~4.2.0", "express": "~4.18.2", "maxmind": "~4.3.11", - "mysql2": "~3.6.0", + "mysql2": "~3.7.0", "redis": "^4.6.6", "rust-gbt": "file:./rust-gbt", "socks-proxy-agent": "~7.0.0", @@ -6110,9 +6110,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/mysql2": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.6.0.tgz", - "integrity": "sha512-EWUGAhv6SphezurlfI2Fpt0uJEWLmirrtQR7SkbTHFC+4/mJBrPiSzHESHKAWKG7ALVD6xaG/NBjjd1DGJGQQQ==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.7.0.tgz", + "integrity": "sha512-c45jA3Jc1X8yJKzrWu1GpplBKGwv/wIV6ITZTlCSY7npF2YfJR+6nMP5e+NTQhUeJPSyOQAbGDCGEHbAl8HN9w==", "dependencies": { "denque": "^2.1.0", "generate-function": "^2.3.1", @@ -12230,9 +12230,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "mysql2": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.6.0.tgz", - "integrity": "sha512-EWUGAhv6SphezurlfI2Fpt0uJEWLmirrtQR7SkbTHFC+4/mJBrPiSzHESHKAWKG7ALVD6xaG/NBjjd1DGJGQQQ==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.7.0.tgz", + "integrity": "sha512-c45jA3Jc1X8yJKzrWu1GpplBKGwv/wIV6ITZTlCSY7npF2YfJR+6nMP5e+NTQhUeJPSyOQAbGDCGEHbAl8HN9w==", "requires": { "denque": "^2.1.0", "generate-function": "^2.3.1", diff --git a/backend/package.json b/backend/package.json index e2417524b..cd1255392 100644 --- a/backend/package.json +++ b/backend/package.json @@ -47,7 +47,7 @@ "crypto-js": "~4.2.0", "express": "~4.18.2", "maxmind": "~4.3.11", - "mysql2": "~3.6.0", + "mysql2": "~3.7.0", "rust-gbt": "file:./rust-gbt", "redis": "^4.6.6", "socks-proxy-agent": "~7.0.0", From 439177a78f1b7024531bb198bc9f547bbd7117dc Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 10 Jan 2024 22:07:01 +0000 Subject: [PATCH 32/61] Add sigop related FAQ entries --- .../src/app/docs/api-docs/api-docs-data.ts | 21 ++++++++++++++++++ .../app/docs/api-docs/api-docs.component.html | 22 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/frontend/src/app/docs/api-docs/api-docs-data.ts b/frontend/src/app/docs/api-docs/api-docs-data.ts index c5319bf2a..86a63e513 100644 --- a/frontend/src/app/docs/api-docs/api-docs-data.ts +++ b/frontend/src/app/docs/api-docs/api-docs-data.ts @@ -10081,6 +10081,27 @@ export const faqData = [ fragment: "how-do-mempool-goggles-work", title: "How do Mempool Goggles work?", }, + { + type: "endpoint", + category: "advanced", + showConditions: bitcoinNetworks, + fragment: "what-are-sigops", + title: "What are sigops?", + }, + { + type: "endpoint", + category: "advanced", + showConditions: bitcoinNetworks, + fragment: "what-is-adjusted-vsize", + title: "What is adjusted vsize?", + }, + { + type: "endpoint", + category: "advanced", + showConditions: bitcoinNetworks, + fragment: "why-do-the-projected-block-fee-ranges-overlap", + title: "Why do the projected block fee ranges overlap?", + }, { type: "category", category: "self-hosting", diff --git a/frontend/src/app/docs/api-docs/api-docs.component.html b/frontend/src/app/docs/api-docs/api-docs.component.html index 77cf01326..c3a260995 100644 --- a/frontend/src/app/docs/api-docs/api-docs.component.html +++ b/frontend/src/app/docs/api-docs/api-docs.component.html @@ -368,6 +368,28 @@
+ +

A "sigop" is a way of accounting for the cost of "signature operations" in Bitcoin script, like OP_CHECKSIG, OP_CHECKSIGVERIFY, OP_CHECKMULTISIG and OP_CHECKMULTISIGVERIFY

+

These signature operations incur different costs depending on whether they are single or multi-sig operations, and on where they appear in a Bitcoin transaction.

+

By consensus, each Bitcoin block is permitted to include a maximum of 80,000 sigops.

+
+ + +

Bitcoin blocks have two independent consensus-enforced resource constraints - a 4MWU weight limit, and the 80,000 sigop limit.

+

Most transactions use a more of the weight limit than the sigop limit. However, some transactions use a disproportionate number of sigops compared to their weight.

+

To account for this, Bitcoin Core calculates and uses an "adjusted vsize" equal 5 times the number of sigops, or the unadjusted vsize, whichever is larger.

+

Then, during block template construction, Bitcoin Core selects transactions in descending order of fee rate measured in satoshis per adjusted vsize

+

On mempool.space, effective fee rates for unconfirmed transactions are also measured in terms of satoshis per adjusted vsize, after accounting for CPFP relationships and other dependencies.

+
+ + +

The projected mempool blocks represent what we expect the next blocks would look like if they were mined right now, and so each projected block follows all of the same rules and constraints as real mined blocks.

+

Those constraints can sometimes cause transactions with lower fee rates to be included "ahead" of transactions with higher rates.

+

For example, if one projected block has a very small amount of space left, it might be able to fit one more tiny low fee rate transaction, while larger higher fee rate transactions have to wait for the following block.

+

A similar effect can occur when there are a large number of transactions with very many sigops. In that scenario, each projected block can only include up to 80,000 sigops worth of transactions, after which the remaining space can only be filled by potentially much lower fee transactions with zero sigops.

+

In extreme cases this can produce several projected blocks in a row with overlapping fee ranges, as a result of each projected block containing both high-feerate high-sigop transactions and lower feerate zero-sigop transactions.

+
+ The official mempool.space website is operated by The Mempool Open Source Project. See more information on our
About page. There are also many unofficial instances of this website operated by individual members of the Bitcoin community. From 5389d198502a3d16678fcc12d58f6885db5af2ba Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Thu, 11 Jan 2024 16:26:09 +0100 Subject: [PATCH 33/61] [footer] fix css --- .../global-footer.component.html | 6 ++--- .../global-footer.component.scss | 22 +++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/shared/components/global-footer/global-footer.component.html b/frontend/src/app/shared/components/global-footer/global-footer.component.html index fba04e605..ad153293d 100644 --- a/frontend/src/app/shared/components/global-footer/global-footer.component.html +++ b/frontend/src/app/shared/components/global-footer/global-footer.component.html @@ -7,11 +7,11 @@
-

+

Explore the full Bitcoin ecosystem

-
+
@@ -30,7 +30,7 @@ My Account Sign In -

+

Explore the full Bitcoin ecosystem

diff --git a/frontend/src/app/shared/components/global-footer/global-footer.component.scss b/frontend/src/app/shared/components/global-footer/global-footer.component.scss index 79c7dcfce..5cb992c28 100644 --- a/frontend/src/app/shared/components/global-footer/global-footer.component.scss +++ b/frontend/src/app/shared/components/global-footer/global-footer.component.scss @@ -136,6 +136,28 @@ footer .sponsor { max-width: 160px; } +.explore-tagline-desktop { + display: none; +} + +.explore-tagline-mobile { + display: block; +} + +@media (min-width: 901px) { + .language-selector { + float: right !important; + } + + .explore-tagline-desktop { + display: block; + } + + .explore-tagline-mobile { + display: none; + } +} + @media (max-width: 1200px) { .main-logo { From e9a67adf4fb0648e5ccb01c2f20a63bbb4ae70f7 Mon Sep 17 00:00:00 2001 From: softsimon Date: Fri, 12 Jan 2024 09:50:33 +0700 Subject: [PATCH 34/61] Adding links to FAQ sections from TX page --- .../src/app/components/block/block.component.html | 2 +- .../src/app/components/block/block.component.scss | 5 ----- .../transaction/transaction.component.html | 12 ++++++++++-- frontend/src/styles.scss | 4 ++++ 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/components/block/block.component.html b/frontend/src/app/components/block/block.component.html index e908d5b24..b34b39c8c 100644 --- a/frontend/src/app/components/block/block.component.html +++ b/frontend/src/app/components/block/block.component.html @@ -59,7 +59,7 @@ - Health + Health  - Adjusted vsize + Adjusted vsize + + + + @@ -321,7 +325,11 @@ - Sigops + Sigops + + + + diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index be8cec328..8a4fe3c9a 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -1191,3 +1191,7 @@ app-global-footer { line-height: 0.5; border-radius: 0.2rem; } + +.info-link fa-icon { + color: rgba(255, 255, 255, 0.4); +} From 7f62a0f9b5109c33d3f04fd46a5277d6c43ad10e Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Fri, 12 Jan 2024 10:50:41 +0100 Subject: [PATCH 35/61] [footer] fix css RTL issues --- .../global-footer/global-footer.component.html | 2 +- .../global-footer/global-footer.component.scss | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/shared/components/global-footer/global-footer.component.html b/frontend/src/app/shared/components/global-footer/global-footer.component.html index ad153293d..232bedafd 100644 --- a/frontend/src/app/shared/components/global-footer/global-footer.component.html +++ b/frontend/src/app/shared/components/global-footer/global-footer.component.html @@ -26,7 +26,7 @@ Sign In
- diff --git a/frontend/src/app/shared/components/global-footer/global-footer.component.scss b/frontend/src/app/shared/components/global-footer/global-footer.component.scss index 5cb992c28..c2bfe71dd 100644 --- a/frontend/src/app/shared/components/global-footer/global-footer.component.scss +++ b/frontend/src/app/shared/components/global-footer/global-footer.component.scss @@ -132,6 +132,7 @@ footer .row.version p a { footer .sponsor { height: 31px; align-items: center; + margin-right: 5px; margin-left: 5px; max-width: 160px; } @@ -145,9 +146,12 @@ footer .sponsor { } @media (min-width: 901px) { - .language-selector { + :host-context(.ltr-layout) .language-selector { float: right !important; } + :host-context(.rtl-layout) .language-selector { + float: left !important; + } .explore-tagline-desktop { display: block; @@ -217,10 +221,6 @@ footer .sponsor { float: none; margin-top: 15px; } - - footer .selector:not(:last-child) { - margin-right: 10px; - } } @media (max-width: 1147px) { From a23088458bd638cdfe0cda5d73da0c4180653cbe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Jan 2024 10:21:43 +0000 Subject: [PATCH 36/61] Bump follow-redirects from 1.15.3 to 1.15.5 in /frontend Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.3 to 1.15.5. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.3...v1.15.5) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] --- frontend/package-lock.json | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index dfd5b1f72..59bdac54a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -31,7 +31,6 @@ "bootstrap": "~4.6.2", "browserify": "^17.0.0", "clipboard": "^2.0.11", - "cypress-fail-on-console-error": "~5.1.0", "domino": "^2.1.6", "echarts": "~5.4.3", "lightweight-charts": "~3.8.0", @@ -9273,9 +9272,9 @@ "devOptional": true }, "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", "funding": [ { "type": "individual", @@ -23993,9 +23992,9 @@ "devOptional": true }, "follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==" + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==" }, "foreach": { "version": "2.0.5", From 46fdb5b30d8fc9c127c1ab292940c358e58d4faf Mon Sep 17 00:00:00 2001 From: softsimon Date: Fri, 12 Jan 2024 18:08:57 +0700 Subject: [PATCH 37/61] Adding dashboard skeleton loaders --- .../app/components/block/block.component.html | 27 +++++++++++++++-- .../app/dashboard/dashboard.component.html | 30 ++++++++++++++++--- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/components/block/block.component.html b/frontend/src/app/components/block/block.component.html index b34b39c8c..0c92de7af 100644 --- a/frontend/src/app/components/block/block.component.html +++ b/frontend/src/app/components/block/block.component.html @@ -233,7 +233,9 @@
- + + +
@@ -245,7 +247,9 @@
- + + + @@ -452,5 +456,24 @@ + + + + + + + + + + + + + + + + +
Total fees
Weight
Transactions
+
+

diff --git a/frontend/src/app/dashboard/dashboard.component.html b/frontend/src/app/dashboard/dashboard.component.html index 4025a45d0..12ce14512 100644 --- a/frontend/src/app/dashboard/dashboard.component.html +++ b/frontend/src/app/dashboard/dashboard.component.html @@ -87,8 +87,8 @@ New fee Status - - + + @@ -158,8 +158,8 @@ {{ currency }} Fee - - + + @@ -199,6 +199,28 @@ + + + +
+
+
+
+ + +
+ + + + +
+
+
+
+ + +
+
From 0ec40eafb401629095573342fb2989f594620365 Mon Sep 17 00:00:00 2001 From: natsee Date: Fri, 12 Jan 2024 17:21:07 +0100 Subject: [PATCH 38/61] Remove hard-coded timestamp for regexes --- .../src/app/components/search-form/search-form.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c00537f5a..745efce21 100644 --- a/frontend/src/app/components/search-form/search-form.component.ts +++ b/frontend/src/app/components/search-form/search-form.component.ts @@ -272,7 +272,7 @@ export class SearchFormComponent implements OnInit { 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) { + if (timestamp > Math.floor(Date.now() / 1000)) { this.isSearching = false; return; } From 3b8de5057cc0eea4fdef84ede0a9d13ff10ec007 Mon Sep 17 00:00:00 2001 From: natsee Date: Fri, 12 Jan 2024 18:04:14 +0100 Subject: [PATCH 39/61] Check env flags before cross network address search --- .../search-form/search-form.component.ts | 11 +++++++-- .../search-results.component.html | 2 +- frontend/src/app/shared/regex.utils.ts | 23 +++++++++++++++++-- 3 files changed, 31 insertions(+), 5 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 745efce21..2a9983a24 100644 --- a/frontend/src/app/components/search-form/search-form.component.ts +++ b/frontend/src/app/components/search-form/search-form.component.ts @@ -186,7 +186,7 @@ export class SearchFormComponent implements OnInit { const matchesTxId = this.regexTransaction.test(searchText) && !this.regexBlockhash.test(searchText); const matchesBlockHash = this.regexBlockhash.test(searchText); let matchesAddress = !matchesTxId && this.regexAddress.test(searchText); - const otherNetworks = findOtherNetworks(searchText, this.network as any || 'mainnet'); + const otherNetworks = findOtherNetworks(searchText, this.network as any || 'mainnet', this.env); // Add B prefix to addresses in Bisq network if (!matchesAddress && this.network === 'bisq' && getRegex('address', 'mainnet').test(searchText)) { @@ -234,7 +234,14 @@ export class SearchFormComponent implements OnInit { } else if (result.short_id) { this.navigate('/lightning/channel/', result.id); } else if (result.network) { - this.navigate('/address/', result.address, undefined, result.network); + if (result.isNetworkAvailable) { + this.navigate('/address/', result.address, undefined, result.network); + } else { + this.searchForm.setValue({ + searchText: '', + }); + this.isSearching = false; + } } } diff --git a/frontend/src/app/components/search-form/search-results/search-results.component.html b/frontend/src/app/components/search-form/search-results/search-results.component.html index adc92b0bf..f83df6b93 100644 --- a/frontend/src/app/components/search-form/search-results/search-results.component.html +++ b/frontend/src/app/components/search-form/search-results/search-results.component.html @@ -38,7 +38,7 @@
Other Networks Address
- diff --git a/frontend/src/app/shared/regex.utils.ts b/frontend/src/app/shared/regex.utils.ts index 607bd704a..128f7566e 100644 --- a/frontend/src/app/shared/regex.utils.ts +++ b/frontend/src/app/shared/regex.utils.ts @@ -144,10 +144,29 @@ 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: Network, address: string}[] { +export function findOtherNetworks(address: string, skipNetwork: Network, env: Env): { network: Network, address: string, isNetworkAvailable: boolean }[] { return ADDRESS_REGEXES .filter(([regex, network]) => network !== skipNetwork && regex.test(address)) - .map(([, network]) => ({ network, address })); + .map(([, network]) => ({ network, address, isNetworkAvailable: isNetworkAvailable(network, env) })); +} + +function isNetworkAvailable(network: Network, env: Env): boolean { + switch (network) { + case 'testnet': + return env.TESTNET_ENABLED === true; + case 'signet': + return env.SIGNET_ENABLED === true; + case 'liquid': + return env.LIQUID_ENABLED === true; + case 'liquidtestnet': + return env.LIQUID_TESTNET_ENABLED === true; + case 'bisq': + return env.BISQ_ENABLED === true; + case 'mainnet': + return true; // There is no "MAINNET_ENABLED" flag + default: + return false; + } } export function needBaseModuleChange(fromBaseModule: 'mempool' | 'liquid' | 'bisq', toNetwork: Network): boolean { From a06bf86b95c43118b13bceba2caa556e9a9db1a8 Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Fri, 12 Jan 2024 13:06:11 -0800 Subject: [PATCH 40/61] Update Cypress to v13.6.2 --- frontend/package-lock.json | 14 +++++++------- frontend/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 59bdac54a..f49fd614a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -58,7 +58,7 @@ "optionalDependencies": { "@cypress/schematic": "^2.5.0", "@types/cypress": "^1.1.3", - "cypress": "^13.6.0", + "cypress": "^13.6.2", "cypress-fail-on-console-error": "~5.1.0", "cypress-wait-until": "^2.0.1", "mock-socket": "~9.3.1", @@ -7083,9 +7083,9 @@ "peer": true }, "node_modules/cypress": { - "version": "13.6.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.0.tgz", - "integrity": "sha512-quIsnFmtj4dBUEJYU4OH0H12bABJpSujvWexC24Ju1gTlKMJbeT6tTO0vh7WNfiBPPjoIXLN+OUqVtiKFs6SGw==", + "version": "13.6.2", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.2.tgz", + "integrity": "sha512-TW3bGdPU4BrfvMQYv1z3oMqj71YI4AlgJgnrycicmPZAXtvywVFZW9DAToshO65D97rCWfG/kqMFsYB6Kp91gQ==", "hasInstallScript": true, "optional": true, "dependencies": { @@ -22272,9 +22272,9 @@ "peer": true }, "cypress": { - "version": "13.6.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.0.tgz", - "integrity": "sha512-quIsnFmtj4dBUEJYU4OH0H12bABJpSujvWexC24Ju1gTlKMJbeT6tTO0vh7WNfiBPPjoIXLN+OUqVtiKFs6SGw==", + "version": "13.6.2", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.2.tgz", + "integrity": "sha512-TW3bGdPU4BrfvMQYv1z3oMqj71YI4AlgJgnrycicmPZAXtvywVFZW9DAToshO65D97rCWfG/kqMFsYB6Kp91gQ==", "optional": true, "requires": { "@cypress/request": "^3.0.0", diff --git a/frontend/package.json b/frontend/package.json index 8dbcbcf3e..330250871 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -110,7 +110,7 @@ "optionalDependencies": { "@cypress/schematic": "^2.5.0", "@types/cypress": "^1.1.3", - "cypress": "^13.6.0", + "cypress": "^13.6.2", "cypress-fail-on-console-error": "~5.1.0", "cypress-wait-until": "^2.0.1", "mock-socket": "~9.3.1", From fdd14fd6dcb4440b600062e725692a7532c248c4 Mon Sep 17 00:00:00 2001 From: wiz Date: Sat, 13 Jan 2024 12:22:23 +0900 Subject: [PATCH 41/61] ops: Disable disk cache for production mainnet --- production/mempool-config.mainnet.json | 1 + 1 file changed, 1 insertion(+) diff --git a/production/mempool-config.mainnet.json b/production/mempool-config.mainnet.json index 36310e59d..e3ce58ceb 100644 --- a/production/mempool-config.mainnet.json +++ b/production/mempool-config.mainnet.json @@ -3,6 +3,7 @@ "NETWORK": "mainnet", "BACKEND": "esplora", "HTTP_PORT": 8999, + "CACHE_ENABLED": false, "MINED_BLOCKS_CACHE": 144, "SPAWN_CLUSTER_PROCS": 0, "API_URL_PREFIX": "/api/v1/", From 2ca6dc51da5c50e24753660a8d478b921b6b25af Mon Sep 17 00:00:00 2001 From: wiz Date: Sat, 13 Jan 2024 12:22:38 +0900 Subject: [PATCH 42/61] ops: Increase nginx proxy_read_timeout to 2m --- production/nginx/http-basic.conf | 2 ++ 1 file changed, 2 insertions(+) diff --git a/production/nginx/http-basic.conf b/production/nginx/http-basic.conf index dc880cad5..fd5cc4b94 100644 --- a/production/nginx/http-basic.conf +++ b/production/nginx/http-basic.conf @@ -19,6 +19,8 @@ client_header_timeout 10s; keepalive_timeout 69s; # maximum time between packets nginx is allowed to pause when sending the client data send_timeout 69s; +# maximum time to wait for response from upstream backends +proxy_read_timeout 120s; # number of requests per connection, does not affect SPDY keepalive_requests 1337; From e4d7034d81b52ffc39e7ea25327d5241f9682c5d Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Sat, 13 Jan 2024 16:57:03 +0000 Subject: [PATCH 43/61] Translate frontend/src/locale/messages.xlf in nb 100% reviewed source file: 'frontend/src/locale/messages.xlf' on 'nb'. --- frontend/src/locale/messages.nb.xlf | 158 ++++++++++++++++++++++++++-- 1 file changed, 148 insertions(+), 10 deletions(-) diff --git a/frontend/src/locale/messages.nb.xlf b/frontend/src/locale/messages.nb.xlf index a035d3765..7870acc32 100644 --- a/frontend/src/locale/messages.nb.xlf +++ b/frontend/src/locale/messages.nb.xlf @@ -59,6 +59,7 @@ + node_modules/src/ngb-config.ts 13 @@ -66,6 +67,7 @@ Slide of + Lysbilde av node_modules/src/ngb-config.ts 13 @@ -404,6 +406,7 @@ See current balance, pending transactions, and history of confirmed transactions for BSQ address . + Se gjeldende saldo, ventende transaksjoner og historikk for bekreftede transaksjoner for BSQ-adresse . src/app/bisq/bisq-address/bisq-address.component.ts 44 @@ -500,6 +503,7 @@ See all BSQ transactions in Bitcoin block (block hash ). + Se alle BSQ-transaksjoner i Bitcoin-blokk (blokkhash ). src/app/bisq/bisq-block/bisq-block.component.ts 92 @@ -627,6 +631,7 @@ See a list of recent Bitcoin blocks with BSQ transactions, total BSQ sent per block, and more. + Se en liste over nylige Bitcoin-blokker med BSQ-transaksjoner, totalt BSQ sendt per blokk og mer. src/app/bisq/bisq-blocks/bisq-blocks.component.ts 39 @@ -756,6 +761,7 @@ Markets + Markeder src/app/bisq/bisq-dashboard/bisq-dashboard.component.ts 32 @@ -763,6 +769,7 @@ Explore the full Bitcoin ecosystem with The Mempool Open Source Project™. See Bisq market prices, trading activity, and more. + Utforsk hele Bitcoin-økosystemet med The Mempool Open Source Project™. Se Bisq markedspriser, handelsaktivitet og mer. src/app/bisq/bisq-dashboard/bisq-dashboard.component.ts 33 @@ -848,6 +855,7 @@ Bisq market: + Bisq-markedet: src/app/bisq/bisq-market/bisq-market.component.ts 51 @@ -855,6 +863,7 @@ See price history, current buy/sell offers, and latest trades for the market on Bisq. + Se prishistorikk, gjeldende kjøps-/salgstilbud og siste handler for -markedet på Bisq. src/app/bisq/bisq-market/bisq-market.component.ts 52 @@ -990,6 +999,7 @@ See high-level stats on the BSQ economy: supply metrics, number of addresses, BSQ price, market cap, and more. + Se statistikk om BSQ-økonomien: forsyningsstatistikk, antall adresser, BSQ-pris, markedsverdi og mer. src/app/bisq/bisq-stats/bisq-stats.component.ts 29 @@ -1169,6 +1179,7 @@ Fee per weight unit + Avgift per vektenhet src/app/bisq/bisq-transaction/bisq-transaction.component.html 68 @@ -1237,6 +1248,7 @@ See inputs, outputs, transaction type, burnt amount, and more for transaction with txid . + Se innganger, utganger, transaksjonstype, brent beløp og mer for transaksjon med txid . src/app/bisq/bisq-transaction/bisq-transaction.component.ts 51 @@ -1426,6 +1438,7 @@ See recent BSQ transactions: amount, txid, associated Bitcoin block, transaction type, and more. + Se nylige BSQ-transaksjoner: beløp, txid, tilhørende Bitcoin-blokk, transaksjonstype og mer. src/app/bisq/bisq-transactions/bisq-transactions.component.ts 82 @@ -1466,6 +1479,7 @@ Become a Community Sponsor + Bli en samfunnssponsor src/app/components/about/about.component.html 39 @@ -1474,6 +1488,7 @@ Become an Enterprise Sponsor + Bli en bedriftssponsor src/app/components/about/about.component.html 46 @@ -1491,6 +1506,7 @@ Whale Sponsors + Hvalsponsorer src/app/components/about/about.component.html 202 @@ -1499,6 +1515,7 @@ Chad Sponsors + Chadsponsorer src/app/components/about/about.component.html 215 @@ -1507,6 +1524,7 @@ OG Sponsors ❤️ + OG Sponsorer ❤️ src/app/components/about/about.component.html 228 @@ -1589,6 +1607,7 @@ Learn more about The Mempool Open Source Project®: enterprise sponsors, individual sponsors, integrations, who contributes, FOSS licensing, and more. + Lær mer om The Mempool Open Source Project®: bedriftssponsorer, individuelle sponsorer, integrasjoner, hvem som bidrar, FOSS-lisensiering og mer. src/app/components/about/about.component.ts 46 @@ -1818,7 +1837,8 @@ - See mempool transactions, confirmed transactions, balance, and more for address . + See mempool transactions, confirmed transactions, balance, and more for address . + Se mempool-transaksjoner, bekreftede transaksjoner, saldo og mer for adresse . src/app/components/address/address-preview.component.ts 72 @@ -2114,6 +2134,7 @@ Explore all the assets issued on the Liquid network like L-BTC, L-CAD, USDT, and more. + Utforsk alle eiendelene utstedt på Liquid-nettverket som L-BTC, L-CAD, USDT og mer. src/app/components/assets/assets-nav/assets-nav.component.ts 43 @@ -2276,6 +2297,7 @@ See Bitcoin feerates visualized over time, including minimum and maximum feerates per block along with feerates at various percentiles. + Se Bitcoin-gebyrer visualisert over tid, inkludert minimums- og maksimumsgebyrer per blokk sammen med gebyrer på forskjellige persentiler. src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.ts 67 @@ -2332,6 +2354,7 @@ See the average mining fees earned per Bitcoin block visualized in BTC and USD over time. + Se gjennomsnittlig utvinningsavgift tjent per Bitcoin-blokk visualisert i BTC og USD over tid. src/app/components/block-fees-graph/block-fees-graph.component.ts 68 @@ -2375,6 +2398,7 @@ Block Health + Blokkhelse src/app/components/block-health-graph/block-health-graph.component.html 6 @@ -2387,6 +2411,7 @@ Block Health + Blokkhelse src/app/components/block-health-graph/block-health-graph.component.ts 63 @@ -2394,6 +2419,7 @@ See Bitcoin block health visualized over time. Block health is a measure of how many expected transactions were included in an actual mined block. Expected transactions are determined using Mempool's re-implementation of Bitcoin Core's transaction selection algorithm. + Se Bitcoin-blokkhelse visualisert over tid. Blokkhelse er et mål på hvor mange forventede transaksjoner som ble inkludert i en faktisk utvunnet blokk. Forventede transaksjoner bestemmes ved hjelp av Mempools re-implementering av Bitcoin Cores transaksjonsvalgalgoritme. src/app/components/block-health-graph/block-health-graph.component.ts 64 @@ -2416,7 +2442,7 @@ src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts - 215 + 216 src/app/lightning/nodes-map/nodes-map.component.ts @@ -2425,6 +2451,7 @@ Health + Helse src/app/components/block-health-graph/block-health-graph.component.ts 190 @@ -2515,6 +2542,7 @@ Accelerated fee rate + Akselerert gebyrsats src/app/components/block-overview-tooltip/block-overview-tooltip.component.html 33 @@ -2546,6 +2574,7 @@ Weight + Vekt src/app/components/block-overview-tooltip/block-overview-tooltip.component.html 43 @@ -2603,6 +2632,7 @@ High sigop count + Høyt antall sigops src/app/components/block-overview-tooltip/block-overview-tooltip.component.html 52 @@ -2620,6 +2650,7 @@ Recently CPFP'd + Nylig CPFPet src/app/components/block-overview-tooltip/block-overview-tooltip.component.html 54 @@ -2637,6 +2668,7 @@ Conflicting + Motstridende src/app/components/block-overview-tooltip/block-overview-tooltip.component.html 57 @@ -2645,6 +2677,7 @@ Accelerated + Akselerert src/app/components/block-overview-tooltip/block-overview-tooltip.component.html 58 @@ -2670,6 +2703,7 @@ See Bitcoin block rewards in BTC and USD visualized over time. Block rewards are the total funds miners earn from the block subsidy and fees. + Se Bitcoin-blokkbelønninger i BTC og USD visualisert over tid. Blokkbelønninger er de totale midlene utvinnerene tjener fra blokksubsidier og gebyr. src/app/components/block-rewards-graph/block-rewards-graph.component.ts 66 @@ -2694,6 +2728,7 @@ See Bitcoin block sizes (MB) and block weights (weight units) visualized over time. + Se Bitcoin-blokkstørrelser (MB) og blokkvekter (vektenheter) visualisert over tid. src/app/components/block-sizes-weights-graph/block-sizes-weights-graph.component.ts 63 @@ -2809,6 +2844,7 @@ See size, weight, fee range, included transactions, and more for Liquid block (). + Se størrelse, vekt, gebyrområde, inkluderte transaksjoner og mer for Liquid blokk (). src/app/components/block-view/block-view.component.ts 112 @@ -2824,6 +2860,7 @@ See size, weight, fee range, included transactions, audit (expected v actual), and more for Bitcoin block (). + Se størrelse, vekt, gebyrområde, inkluderte transaksjoner, revisjon (forventet v faktisk) og mer for Bitcoin blokk ( ). src/app/components/block-view/block-view.component.ts 114 @@ -2912,6 +2949,7 @@ This block does not belong to the main chain, it has been replaced by: + Denne blokken tilhører ikke hovedkjeden, den er erstattet av: src/app/components/block/block.component.html 5 @@ -2947,6 +2985,7 @@ Stale + Foreldet src/app/components/block/block.component.html 30 @@ -3412,6 +3451,7 @@ Blocks + Blokker src/app/components/blocks-list/blocks-list.component.ts 59 @@ -3419,6 +3459,7 @@ See the most recent Liquid blocks along with basic stats such as block height, block size, and more. + Se de nyeste Liquid-blokkene sammen med grunnleggende statistikk som blokkhøyde, blokkstørrelse og mer. src/app/components/blocks-list/blocks-list.component.ts 62 @@ -3426,6 +3467,7 @@ See the most recent Bitcoin blocks along with basic stats such as block height, block reward, block size, and more. + Se de nyeste Bitcoin-blokkene sammen med grunnleggende statistikk som blokkhøyde, blokkbelønning, blokkstørrelse og mer. src/app/components/blocks-list/blocks-list.component.ts 64 @@ -3433,6 +3475,7 @@ Calculator + Kalkulator src/app/components/calculator/calculator.component.html 3 @@ -3470,6 +3513,7 @@ Memory Usage + Minnebruk src/app/components/clock/clock.component.html 65 @@ -3549,6 +3593,7 @@ blocks + blokker src/app/components/difficulty-mining/difficulty-mining.component.html 10,11 @@ -3852,6 +3897,7 @@ WU/s + WU/s src/app/components/footer/footer.component.html 14 @@ -4024,7 +4070,7 @@ src/app/lightning/nodes-channels-map/nodes-channels-map.component.html - 6 + 19 lightning.nodes-channels-world-map @@ -4072,6 +4118,7 @@ See hashrate and difficulty for the Bitcoin network visualized over time. + Se hashrate og vanskelighetsgrad for Bitcoin-nettverket visualisert over tid. src/app/components/hashrate-chart/hashrate-chart.component.ts 75 @@ -4179,6 +4226,7 @@ See stats for transactions in the mempool: fee range, aggregate size, and more. Mempool blocks are updated in real-time as the network receives new transactions. + Se statistikk for transaksjoner i mempoolen: gebyrområde, samlet størrelse og mer. Mempool-blokker oppdateres i sanntid etter hvert som nettverket mottar nye transaksjoner. src/app/components/mempool-block/mempool-block.component.ts 58 @@ -4202,6 +4250,7 @@ Count + Antall src/app/components/mempool-graph/mempool-graph.component.ts 325 @@ -4229,6 +4278,7 @@ Sign in + Logg inn src/app/components/menu/menu.component.html 10 @@ -4255,6 +4305,7 @@ Recent Blocks + Nylige blokker src/app/components/mining-dashboard/mining-dashboard.component.html 52 @@ -4280,6 +4331,7 @@ Get real-time Bitcoin mining stats like hashrate, difficulty adjustment, block rewards, pool dominance, and more. + Få sanntids Bitcoin-utvinningsstatistikk som hashrate, vanskeliggradsjustering, blokkbelønninger, gruppedominans og mer. src/app/components/mining-dashboard/mining-dashboard.component.ts 21 @@ -4296,6 +4348,7 @@ Pools Luck + Gruppeflaks src/app/components/pool-ranking/pool-ranking.component.html 9 @@ -4326,6 +4379,7 @@ Pools Count + Antall grupper src/app/components/pool-ranking/pool-ranking.component.html 17 @@ -4440,6 +4494,7 @@ Empty Blocks + Tomme blokker src/app/components/pool-ranking/pool-ranking.component.html 98 @@ -4465,6 +4520,7 @@ See the top Bitcoin mining pools ranked by number of blocks mined, over your desired timeframe. + Se de beste utvinningsgruppene rangert etter antall blokker utvunnet, over ønsket tidsramme. src/app/components/pool-ranking/pool-ranking.component.ts 59 @@ -4554,6 +4610,7 @@ See mining pool stats for : most recent mined blocks, hashrate over time, total block reward to date, known coinbase addresses, and more. + Se utvinningsgruppe-statistikk for : siste utvunnede blokker, hashrate over tid, total blokkbelønning hittil, kjente coinbaseadresser og mer. src/app/components/pool/pool-preview.component.ts 86 @@ -4620,6 +4677,7 @@ Blocks (24h) + Blokker (24 timer) src/app/components/pool/pool.component.html 146 @@ -4734,6 +4792,7 @@ Broadcast Transaction + Kringkast transaksjon src/app/components/push-transaction/push-transaction.component.ts 31 @@ -4741,6 +4800,7 @@ Broadcast a transaction to the network using the transaction's hash. + Kringkast en transaksjon til -nettverket ved å bruke transaksjonens hash. src/app/components/push-transaction/push-transaction.component.ts 32 @@ -4748,6 +4808,7 @@ RBF Replacements + RBF-erstatninger src/app/components/rbf-list/rbf-list.component.html 2 @@ -4760,6 +4821,7 @@ Full RBF + Full RBF src/app/components/rbf-list/rbf-list.component.html 24 @@ -4776,6 +4838,7 @@ There are no replacements in the mempool yet! + Det er ingen erstatninger i mempoolen ennå! src/app/components/rbf-list/rbf-list.component.html 34 @@ -4784,6 +4847,7 @@ See the most recent RBF replacements on the Bitcoin network, updated in real-time. + Se de siste RBF-erstatningene på Bitcoin-nettverket, oppdatert i sanntid. src/app/components/rbf-list/rbf-list.component.ts 59 @@ -4829,6 +4893,7 @@ Status + Status src/app/components/rbf-timeline/rbf-timeline-tooltip.component.html 33 @@ -4846,6 +4911,7 @@ RBF + RBF src/app/components/rbf-timeline/rbf-timeline-tooltip.component.html 36 @@ -5065,6 +5131,7 @@ Clock (Mempool) + Klokke (Mempool) src/app/components/statistics/statistics.component.html 17 @@ -5117,6 +5184,7 @@ Cap outliers + Fjern avvikere src/app/components/statistics/statistics.component.html 122 @@ -5125,6 +5193,7 @@ See mempool size (in MvB) and transactions per second (in vB/s) visualized over time. + Se mempoolstørrelse (i MvB) og transaksjoner per sekund (i vB/s) visualisert over tid. src/app/components/statistics/statistics.component.ts 67 @@ -5132,6 +5201,7 @@ See Bitcoin blocks and mempool congestion in real-time in a simplified format perfect for a TV. + Se Bitcoin-blokker og mempool i sanntid i et forenklet format perfekt for en TV. src/app/components/television/television.component.ts 40 @@ -5343,7 +5413,8 @@ transactions-list.coinbase - Get real-time status, addresses, fees, script info, and more for transaction with txid {txid}. + Get real-time status, addresses, fees, script info, and more for transaction with txid . + Få sanntidsstatus, adresser, gebyrer, skriptinformasjon og mer for transaksjon med txid . src/app/components/transaction/transaction-preview.component.ts 91 @@ -5385,6 +5456,7 @@ Accelerate + Akselerer src/app/components/transaction/transaction.component.html 123 @@ -5422,6 +5494,7 @@ RBF History + RBF historie src/app/components/transaction/transaction.component.html 218 @@ -5493,6 +5566,7 @@ Adjusted vsize + Justert vsize src/app/components/transaction/transaction.component.html 298 @@ -5511,6 +5585,7 @@ Sigops + Sigops src/app/components/transaction/transaction.component.html 320 @@ -5538,6 +5613,7 @@ Accelerated fee rate + Akselerert gebyrsats src/app/components/transaction/transaction.component.html 516 @@ -5893,6 +5969,7 @@ Recent Replacements + Nylige erstatninger src/app/dashboard/dashboard.component.html 79 @@ -5901,6 +5978,7 @@ Previous fee + Tidligere avgift src/app/dashboard/dashboard.component.html 86 @@ -5909,6 +5987,7 @@ New fee + Ny avgift src/app/dashboard/dashboard.component.html 87 @@ -5917,6 +5996,7 @@ Recent Transactions + Nylige transaksjoner src/app/dashboard/dashboard.component.html 153 @@ -5954,6 +6034,7 @@ Incoming Transactions + Innkommende transaksjoner src/app/dashboard/dashboard.component.html 259 @@ -5962,6 +6043,7 @@ mempool.space merely provides data about the Bitcoin network. It cannot help you with retrieving funds, wallet issues, etc.For any such requests, you need to get in touch with the entity that helped make the transaction (wallet software, exchange company, etc). + mempool.space gir bare data om Bitcoin-nettverket. Det kan ikke hjelpe deg med å hente midler, lommebokproblemer osv.For slike forespørsler må du ta kontakt med enheten som bidro til transaksjonen (lommebokprogramvare, børsselskap osv.). src/app/docs/api-docs/api-docs.component.html 15,16 @@ -6056,6 +6138,7 @@ FAQ + FAQ src/app/docs/docs/docs.component.ts 45 @@ -6063,6 +6146,7 @@ Get answers to common questions like: What is a mempool? Why isn't my transaction confirming? How can I run my own instance of The Mempool Open Source Project? And more. + Få svar på vanlige spørsmål som: Hva er en mempool? Hvorfor bekreftes ikke transaksjonen min? Hvordan kan jeg kjøre min egen instans av Mempool Open Source Project? Og mer. src/app/docs/docs/docs.component.ts 46 @@ -6070,6 +6154,7 @@ REST API + REST API src/app/docs/docs/docs.component.ts 49 @@ -6077,6 +6162,7 @@ Documentation for the liquid.network REST API service: get info on addresses, transactions, assets, blocks, and more. + Dokumentasjon for liquid.network REST API-tjenesten: få informasjon om adresser, transaksjoner, eiendeler, blokker og mer. src/app/docs/docs/docs.component.ts 51 @@ -6084,6 +6170,7 @@ Documentation for the bisq.markets REST API service: get info on recent trades, current offers, transactions, network state, and more. + Dokumentasjon for bisq.markets REST API-tjenesten: få informasjon om nylige handler, nåværende tilbud, transaksjoner, nettverkstilstand og mer. src/app/docs/docs/docs.component.ts 53 @@ -6091,6 +6178,7 @@ Documentation for the mempool.space REST API service: get info on addresses, transactions, blocks, fees, mining, the Lightning network, and more. + Dokumentasjon for mempool.space REST API-tjenesten: få informasjon om adresser, transaksjoner, blokker, avgifter, utvinning, Lightning-nettverket og mer. src/app/docs/docs/docs.component.ts 55 @@ -6098,6 +6186,7 @@ WebSocket API + WebSocket API src/app/docs/docs/docs.component.ts 59 @@ -6105,6 +6194,7 @@ Documentation for the liquid.network WebSocket API service: get real-time info on blocks, mempools, transactions, addresses, and more. + Dokumentasjon for liquid.network WebSocket API-tjenesten: få sanntidsinformasjon om blokker, mempooler, transaksjoner, adresser og mer. src/app/docs/docs/docs.component.ts 61 @@ -6112,6 +6202,7 @@ Documentation for the mempool.space WebSocket API service: get real-time info on blocks, mempools, transactions, addresses, and more. + Dokumentasjon for mempool.space WebSocket API-tjenesten: få sanntidsinformasjon om blokker, mempooler, transaksjoner, adresser og mer. src/app/docs/docs/docs.component.ts 63 @@ -6119,6 +6210,7 @@ Electrum RPC + Electrum RPC src/app/docs/docs/docs.component.ts 67 @@ -6126,6 +6218,7 @@ Documentation for our Electrum RPC interface: get instant, convenient, and reliable access to an Esplora instance. + Dokumentasjon for vårt Electrum RPC-grensesnitt: få umiddelbar, praktisk og pålitelig tilgang til en Esplora-instans. src/app/docs/docs/docs.component.ts 68 @@ -6438,6 +6531,7 @@ Overview for Lightning channel . See channel capacity, the Lightning nodes involved, related on-chain transactions, and more. + Oversikt for Lightning-kanalen . Se kanalkapasitet, Lightning-nodene som er involvert, relaterte transaksjoner i kjeden og mer. src/app/lightning/channel/channel-preview.component.ts 37 @@ -6936,6 +7030,7 @@ Connect + Koble til src/app/lightning/group/group.component.html 73 @@ -6986,6 +7081,7 @@ Penalties + Straffer src/app/lightning/justice-list/justice-list.component.html 4 @@ -7062,7 +7158,8 @@ lightning.connectivity-ranking - Get stats on the Lightning network (aggregate capacity, connectivity, etc) and Lightning nodes (channels, liquidity, etc) and Lightning channels (status, fees, etc). + Get stats on the Lightning network (aggregate capacity, connectivity, etc), Lightning nodes (channels, liquidity, etc) and Lightning channels (status, fees, etc). + Få statistikk om Lightning-nettverket (samlet kapasitet, tilkobling osv.), Lightning-noder (kanaler, likviditet osv.) og Lightning-kanaler (status, avgifter osv.). src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts 28 @@ -7172,6 +7269,7 @@ Overview for the Lightning network node named . See channels, capacity, location, fee stats, and more. + Oversikt for Lightning-nettverksnoden . Se kanaler, kapasitet, plassering, gebyrstatistikk og mer. src/app/lightning/node/node-preview.component.ts 52 @@ -7232,6 +7330,7 @@ Decoded + Dekodet src/app/lightning/node/node.component.html 136 @@ -7344,7 +7443,7 @@ (Tor-noder ekskludert) src/app/lightning/nodes-channels-map/nodes-channels-map.component.html - 8 + 21 src/app/lightning/nodes-map/nodes-map.component.html @@ -7365,14 +7464,15 @@ Lightning-kanaler verdenskart src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts - 68 + 69 See the channels of non-Tor Lightning network nodes visualized on a world map. Hover/tap on points on the map for node names and details. + Se kanalene til ikke-Tor Lightning-nettverksnoder visualisert på et verdenskart. Hold musepekeren/trykk på punkter på kartet for nodenavn og detaljer. src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts - 69 + 70 @@ -7380,7 +7480,7 @@ Ingen geolokaliseringsdata tilgjengelig src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts - 227 + 228 @@ -7394,6 +7494,7 @@ See the locations of non-Tor Lightning network nodes visualized on a world map. Hover/tap on points on the map for node names and details. + Se plasseringen til ikke-Tor Lightning-nettverksnoder visualisert på et verdenskart. Hold musepekeren/trykk på punkter på kartet for nodenavn og detaljer. src/app/lightning/nodes-map/nodes-map.component.ts 51 @@ -7401,6 +7502,7 @@ See the number of Lightning network nodes visualized over time by network: clearnet only (IPv4, IPv6), darknet (Tor, I2p, cjdns), and both. + Se antall Lightning-nettverksnoder visualisert over tid etter nettverk: bare clearnet (IPv4, IPv6), darknet (Tor, I2p, cjdns) og begge deler. src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts 68 @@ -7469,6 +7571,7 @@ See a geographical breakdown of the Lightning network: how many Lightning nodes are hosted in countries around the world, aggregate BTC capacity for each country, and more. + Se en geografisk inndeling av Lightning-nettverket: hvor mange Lightning-noder er det i land rundt om i verden, samlet BTC-kapasitet for hvert land, og mer. src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.ts 47 @@ -7539,6 +7642,7 @@ Explore all the Lightning nodes hosted in and see an overview of each node's capacity, number of open channels, and more. + Utforsk alle Lightning-nodene som er i og se en oversikt over hver nodes kapasitet, antall åpne kanaler og mer. src/app/lightning/nodes-per-country/nodes-per-country.component.ts 36 @@ -7689,6 +7793,7 @@ Browse all Bitcoin Lightning nodes using the [AS] ISP and see aggregate stats like total number of nodes, total capacity, and more for the ISP. + Bla gjennom alle Bitcoin Lightning-noder ved å bruke [AS] ISP og se samlet statistikk som totalt antall noder, total kapasitet, og mer for Internett-leverandøren. src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.ts 45 @@ -7744,6 +7849,7 @@ See the oldest nodes on the Lightning network along with their capacity, number of channels, location, etc. + Se de eldste nodene på Lightning-nettverket sammen med deres kapasitet, antall kanaler, plassering osv. src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.ts 28 @@ -7751,6 +7857,7 @@ See Lightning nodes with the most BTC liquidity deployed along with high-level stats like number of open channels, location, node age, and more. + Se Lightning-noder med mest BTC-likviditet utplassert sammen med statistikk som antall åpne kanaler, plassering, nodens alder og mer. src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts 34 @@ -7758,6 +7865,7 @@ See Lightning nodes with the most channels open along with high-level stats like total node capacity, node age, and more. + Se Lightning-noder med flest åpne kanaler sammen med statistikk som total nodekapasitet, nodealder og mer. src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts 38 @@ -7781,7 +7889,8 @@ - See top the Lightning network nodes ranked by liquidity, connectivity, and age. + See the top Lightning network nodes ranked by liquidity, connectivity, and age. + Se de beste Lightning-nettverksnodene rangert etter likviditet, tilkobling og alder. src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.ts 23 @@ -7789,6 +7898,7 @@ See the capacity of the Lightning network visualized over time in terms of the number of open channels and total bitcoin capacity. + Se kapasiteten til Lightning-nettverket visualisert over tid i form av antall åpne kanaler og total bitcoin-kapasitet. src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts 67 @@ -7796,6 +7906,7 @@ confirmation + bekreftelse src/app/shared/components/confirmations/confirmations.component.html 4 @@ -7805,6 +7916,7 @@ confirmations + bekreftelser src/app/shared/components/confirmations/confirmations.component.html 5 @@ -7814,6 +7926,7 @@ Replaced + Erstattet src/app/shared/components/confirmations/confirmations.component.html 12 @@ -7823,6 +7936,7 @@ Removed + Fjernet src/app/shared/components/confirmations/confirmations.component.html 15 @@ -7842,6 +7956,7 @@ sat/WU + sat/WU src/app/shared/components/fee-rate/fee-rate.component.html 3 @@ -7851,6 +7966,7 @@ My Account + Min konto src/app/shared/components/global-footer/global-footer.component.html 25 @@ -7863,6 +7979,7 @@ Sign In + Logg inn src/app/shared/components/global-footer/global-footer.component.html 26 @@ -7875,6 +7992,7 @@ Explore + Utforsk src/app/shared/components/global-footer/global-footer.component.html 41 @@ -7883,6 +8001,7 @@ Connect to our Nodes + Koble til nodene våre src/app/shared/components/global-footer/global-footer.component.html 46 @@ -7891,6 +8010,7 @@ API Documentation + API-dokumentasjon src/app/shared/components/global-footer/global-footer.component.html 47 @@ -7899,6 +8019,7 @@ Learn + Lær src/app/shared/components/global-footer/global-footer.component.html 50 @@ -7907,6 +8028,7 @@ What is a mempool? + Hva er en mempool? src/app/shared/components/global-footer/global-footer.component.html 51 @@ -7915,6 +8037,7 @@ What is a block explorer? + Hva er en blokkutforsker? src/app/shared/components/global-footer/global-footer.component.html 52 @@ -7923,6 +8046,7 @@ What is a mempool explorer? + Hva er en mempool utforsker? src/app/shared/components/global-footer/global-footer.component.html 53 @@ -7931,6 +8055,7 @@ Why isn't my transaction confirming? + Hvorfor bekreftes ikke transaksjonen min? src/app/shared/components/global-footer/global-footer.component.html 54 @@ -7939,6 +8064,7 @@ More FAQs » + Flere vanlige spørsmål » src/app/shared/components/global-footer/global-footer.component.html 55 @@ -7947,6 +8073,7 @@ Networks + Nettverk src/app/shared/components/global-footer/global-footer.component.html 59 @@ -7955,6 +8082,7 @@ Mainnet Explorer + Mainnet utforsker src/app/shared/components/global-footer/global-footer.component.html 60 @@ -7963,6 +8091,7 @@ Testnet Explorer + Testnet utforsker src/app/shared/components/global-footer/global-footer.component.html 61 @@ -7971,6 +8100,7 @@ Signet Explorer + Signet utforsker src/app/shared/components/global-footer/global-footer.component.html 62 @@ -7979,6 +8109,7 @@ Liquid Testnet Explorer + Liquid testnet utforsker src/app/shared/components/global-footer/global-footer.component.html 63 @@ -7987,6 +8118,7 @@ Liquid Explorer + Liquid utforsker src/app/shared/components/global-footer/global-footer.component.html 64 @@ -7995,6 +8127,7 @@ Bisq Explorer + Bisq utforsker src/app/shared/components/global-footer/global-footer.component.html 65 @@ -8003,6 +8136,7 @@ Tools + Verktøy src/app/shared/components/global-footer/global-footer.component.html 69 @@ -8011,6 +8145,7 @@ Clock (Mined) + Klokke (utvunnet) src/app/shared/components/global-footer/global-footer.component.html 71 @@ -8019,6 +8154,7 @@ Legal + Lovlig src/app/shared/components/global-footer/global-footer.component.html 76 @@ -8047,6 +8183,7 @@ Trademark Policy + Varemerkepolitikk src/app/shared/components/global-footer/global-footer.component.html 79 @@ -8056,6 +8193,7 @@ This is a test network. Coins have no value. + Dette er et testnettverk. Mynter har ingen verdi. src/app/shared/components/testnet-alert/testnet-alert.component.html 3 From 65236c7b08c63498f849a695767fccdecdf4a074 Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Sat, 13 Jan 2024 13:55:12 -0800 Subject: [PATCH 44/61] Add seconds to the block view timestamp --- frontend/src/app/components/block/block.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/components/block/block.component.html b/frontend/src/app/components/block/block.component.html index b34b39c8c..885f8e1ac 100644 --- a/frontend/src/app/components/block/block.component.html +++ b/frontend/src/app/components/block/block.component.html @@ -47,7 +47,7 @@ Timestamp - + From a8442a3467d808d63406a38a90680db9f8e1d70b Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Sat, 13 Jan 2024 18:11:13 -0800 Subject: [PATCH 45/61] Add seconds to block timestamp in the mining pool view and blocks list --- .../src/app/components/blocks-list/blocks-list.component.html | 2 +- frontend/src/app/components/pool/pool.component.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/blocks-list/blocks-list.component.html b/frontend/src/app/components/blocks-list/blocks-list.component.html index 85e2ea17f..838c7cb4e 100644 --- a/frontend/src/app/components/blocks-list/blocks-list.component.html +++ b/frontend/src/app/components/blocks-list/blocks-list.component.html @@ -46,7 +46,7 @@ - ‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }} + ‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm:ss' }}
{{ block.height }} - ‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }} + ‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm:ss' }} From 0722c221c78729f3954e5d2e2f620c7fa52e4200 Mon Sep 17 00:00:00 2001 From: softsimon Date: Sun, 14 Jan 2024 11:24:56 +0700 Subject: [PATCH 46/61] Minor adjustments --- .../search-form/search-results/search-results.component.html | 4 ++-- .../search-form/search-results/search-results.component.ts | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/search-form/search-results/search-results.component.html b/frontend/src/app/components/search-form/search-results/search-results.component.html index f83df6b93..5d64b7495 100644 --- a/frontend/src/app/components/search-form/search-results/search-results.component.html +++ b/frontend/src/app/components/search-form/search-results/search-results.component.html @@ -36,10 +36,10 @@ -
Other Networks Address
+
Other Network Address
diff --git a/frontend/src/app/components/search-form/search-results/search-results.component.ts b/frontend/src/app/components/search-form/search-results/search-results.component.ts index 84c541a70..ade01bf89 100644 --- a/frontend/src/app/components/search-form/search-results/search-results.component.ts +++ b/frontend/src/app/components/search-form/search-results/search-results.component.ts @@ -45,6 +45,9 @@ export class SearchResultsComponent implements OnChanges { break; case 'Enter': event.preventDefault(); + if (this.resultsFlattened[this.activeIdx]?.isNetworkAvailable === false) { + return; + } if (this.resultsFlattened[this.activeIdx]) { this.selectedResult.emit(this.resultsFlattened[this.activeIdx]); } else { From 00b8f001b0d9f37458f6979fb209c7e39b7cb5ca Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 15 Jan 2024 02:47:29 +0000 Subject: [PATCH 47/61] Fix goggles inscription detection bug --- backend/src/api/common.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index 358a98c98..af93b9622 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -263,8 +263,13 @@ export class Common { case 'v0_p2wsh': flags |= TransactionFlags.p2wsh; break; case 'v1_p2tr': { flags |= TransactionFlags.p2tr; - if (vin.witness.length > 2) { - const asm = vin.inner_witnessscript_asm || transactionUtils.convertScriptSigAsm(vin.witness[vin.witness.length - 2]); + // in taproot, if the last witness item begins with 0x50, it's an annex + const hasAnnex = vin.witness?.[vin.witness.length - 1].startsWith('50'); + // script spends have more than one witness item, not counting the annex (if present) + if (vin.witness.length > (hasAnnex ? 2 : 1)) { + // the script itself is the second-to-last witness item, not counting the annex + const asm = vin.inner_witnessscript_asm || transactionUtils.convertScriptSigAsm(vin.witness[vin.witness.length - (hasAnnex ? 3 : 2)]); + // inscriptions smuggle data within an 'OP_0 OP_IF ... OP_ENDIF' envelope if (asm?.includes('OP_0 OP_IF')) { flags |= TransactionFlags.inscription; } From f095913538a56fc32eddd1ae84d10c27fa8afe13 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Mon, 8 Jan 2024 11:48:55 +0100 Subject: [PATCH 48/61] [bitcoin core] add internal routes to bitcoin core rpc --- .../src/api/bitcoin/bitcoin-core.routes.ts | 221 ++++++++++++++++++ backend/src/index.ts | 2 + backend/src/rpc-api/commands.ts | 1 + 3 files changed, 224 insertions(+) create mode 100644 backend/src/api/bitcoin/bitcoin-core.routes.ts diff --git a/backend/src/api/bitcoin/bitcoin-core.routes.ts b/backend/src/api/bitcoin/bitcoin-core.routes.ts new file mode 100644 index 000000000..dbdcced1f --- /dev/null +++ b/backend/src/api/bitcoin/bitcoin-core.routes.ts @@ -0,0 +1,221 @@ +import { Application, NextFunction, Request, Response } from 'express'; +import logger from '../../logger'; +import bitcoinClient from './bitcoin-client'; + +/** + * Define a set of routes used by the accelerator server + * Those routes are not designed to be public + */ +class BitcoinBackendRoutes { + private static tag = 'BitcoinBackendRoutes'; + + public initRoutes(app: Application) { + app + .get('/api/internal/bitcoinCore/' + 'getMempoolEntry', this.disableCache, this.$getMempoolEntry) + .post('/api/internal/bitcoinCore/' + 'decodeRawTransaction', this.disableCache, this.$decodeRawTransaction) + .get('/api/internal/bitcoinCore/' + 'getRawTransaction', this.disableCache, this.$getRawTransaction) + .post('/api/internal/bitcoinCore/' + 'sendRawTransaction', this.disableCache, this.$sendRawTransaction) + .post('/api/internal/bitcoinCore/' + 'testMempoolAccept', this.disableCache, this.$testMempoolAccept) + .get('/api/internal/bitcoinCore/' + 'getMempoolAncestors', this.disableCache, this.$getMempoolAncestors) + .get('/api/internal/bitcoinCore/' + 'getBlock', this.disableCache, this.$getBlock) + .get('/api/internal/bitcoinCore/' + 'getBlockHash', this.disableCache, this.$getBlockHash) + .get('/api/internal/bitcoinCore/' + 'getBlockCount', this.disableCache, this.$getBlockCount) + ; + } + + /** + * Disable caching for bitcoin core routes + * + * @param req + * @param res + * @param next + */ + private disableCache(req: Request, res: Response, next: NextFunction): void { + res.setHeader('Pragma', 'no-cache'); + res.setHeader('Cache-control', 'private, no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0'); + res.setHeader('expires', -1); + next(); + } + + /** + * Exeption handler to return proper details to the accelerator server + * + * @param e + * @param fnName + * @param res + */ + private static handleException(e: any, fnName: string, res: Response): void { + if (typeof(e.code) === 'number') { + res.status(400).send(JSON.stringify(e, ['code', 'message'])); + } else { + const err = `exception in ${fnName}. ${e}. Details: ${JSON.stringify(e, ['code', 'message'])}`; + logger.err(err, BitcoinBackendRoutes.tag); + res.status(500).send(err); + } + } + + private async $getMempoolEntry(req: Request, res: Response): Promise { + const txid = req.query.txid; + try { + if (typeof(txid) !== 'string' || txid.length !== 64) { + res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`); + return; + } + const mempoolEntry = await bitcoinClient.getMempoolEntry(txid); + if (!mempoolEntry) { + res.status(404).send(`no mempool entry found for txid ${txid}`); + return; + } + res.status(200).send(mempoolEntry); + } catch (e: any) { + BitcoinBackendRoutes.handleException(e, 'getMempoolEntry', res); + } + } + + private async $decodeRawTransaction(req: Request, res: Response): Promise { + const rawTx = req.body.rawTx; + try { + if (typeof(rawTx) !== 'string') { + res.status(400).send(`invalid param rawTx ${rawTx}. must be a string`); + return; + } + const decodedTx = await bitcoinClient.decodeRawTransaction(rawTx); + if (!decodedTx) { + res.status(400).send(`unable to decode rawTx ${rawTx}`); + return; + } + res.status(200).send(decodedTx); + } catch (e: any) { + BitcoinBackendRoutes.handleException(e, 'decodeRawTransaction', res); + } + } + + private async $getRawTransaction(req: Request, res: Response): Promise { + const txid = req.query.txid; + try { + if (typeof(txid) !== 'string' || txid.length !== 64) { + res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`); + return; + } + const decodedTx = await bitcoinClient.getRawTransaction(txid); + if (!decodedTx) { + res.status(400).send(`unable to get raw transaction for txid ${txid}`); + return; + } + res.status(200).send(decodedTx); + } catch (e: any) { + BitcoinBackendRoutes.handleException(e, 'decodeRawTransaction', res); + } + } + + private async $sendRawTransaction(req: Request, res: Response): Promise { + const rawTx = req.body.rawTx; + try { + if (typeof(rawTx) !== 'string') { + res.status(400).send(`invalid param rawTx ${rawTx}. must be a string`); + return; + } + const txHex = await bitcoinClient.sendRawTransaction(rawTx); + if (!txHex) { + res.status(400).send(`unable to send rawTx ${rawTx}`); + return; + } + res.status(200).send(txHex); + } catch (e: any) { + BitcoinBackendRoutes.handleException(e, 'sendRawTransaction', res); + } + } + + private async $testMempoolAccept(req: Request, res: Response): Promise { + const rawTx = req.body.rawTx; + try { + if (typeof(rawTx) !== 'string') { + res.status(400).send(`invalid param rawTx ${rawTx}. must be a string`); + return; + } + const txHex = await bitcoinClient.testMempoolAccept([rawTx]); + if (typeof(txHex) !== 'object' || txHex.length === 0) { + res.status(400).send(`testmempoolaccept failed for raw tx ${rawTx}, got an empty result`); + return; + } + res.status(200).send(txHex); + } catch (e: any) { + BitcoinBackendRoutes.handleException(e, 'testMempoolAccept', res); + } + } + + private async $getMempoolAncestors(req: Request, res: Response): Promise { + const txid = req.query.txid; + try { + if (typeof(txid) !== 'string' || txid.length !== 64) { + res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`); + return; + } + const decodedTx = await bitcoinClient.getMempoolAncestors(txid); + if (!decodedTx) { + res.status(400).send(`unable to get mempool ancestors for txid ${txid}`); + return; + } + res.status(200).send(decodedTx); + } catch (e: any) { + BitcoinBackendRoutes.handleException(e, 'getMempoolAncestors', res); + } + } + + private async $getBlock(req: Request, res: Response): Promise { + const blockHash = req.query.hash; + try { + if (typeof(blockHash) !== 'string' || blockHash.length !== 64) { + res.status(400).send(`invalid param blockHash ${blockHash}. must be a string of 64 char`); + return; + } + const block = await bitcoinClient.getBlock(blockHash); + if (!block) { + res.status(400).send(`unable to get block for block hash ${blockHash}`); + return; + } + res.status(200).send(block); + } catch (e: any) { + BitcoinBackendRoutes.handleException(e, 'getBlock', res); + } + } + + private async $getBlockHash(req: Request, res: Response): Promise { + const blockHeight = req.query.height; + try { + if (typeof(blockHeight) !== 'string') { + res.status(400).send(`invalid param blockHeight ${blockHeight}, must be a string representing an integer`); + return; + } + const blockHeightNumber = parseInt(blockHeight, 10); + if (!blockHeightNumber) { + res.status(400).send(`invalid param blockHeight ${blockHeight}. must be a valid integer`); + return; + } + + const block = await bitcoinClient.getBlockHash(blockHeightNumber); + if (!block) { + res.status(400).send(`unable to get block hash for block height ${blockHeightNumber}`); + return; + } + res.status(200).send(block); + } catch (e: any) { + BitcoinBackendRoutes.handleException(e, 'getBlockHash', res); + } + } + + private async $getBlockCount(req: Request, res: Response): Promise { + try { + const count = await bitcoinClient.getBlockCount(); + if (!count) { + res.status(400).send(`unable to get block count`); + return; + } + res.status(200).send(`${count}`); + } catch (e: any) { + BitcoinBackendRoutes.handleException(e, 'getBlockCount', res); + } + } +} + +export default new BitcoinBackendRoutes \ No newline at end of file diff --git a/backend/src/index.ts b/backend/src/index.ts index 44fe87e3a..a7b2ad4df 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -44,6 +44,7 @@ import v8 from 'v8'; import { formatBytes, getBytesUnit } from './utils/format'; import redisCache from './api/redis-cache'; import accelerationApi from './api/services/acceleration'; +import bitcoinCoreRoutes from './api/bitcoin/bitcoin-core.routes'; class Server { private wss: WebSocket.Server | undefined; @@ -282,6 +283,7 @@ class Server { setUpHttpApiRoutes(): void { bitcoinRoutes.initRoutes(this.app); + bitcoinCoreRoutes.initRoutes(this.app); pricesRoutes.initRoutes(this.app); if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && config.MEMPOOL.ENABLED) { statisticsRoutes.initRoutes(this.app); diff --git a/backend/src/rpc-api/commands.ts b/backend/src/rpc-api/commands.ts index 78f5e12f4..ecfb2ed7c 100644 --- a/backend/src/rpc-api/commands.ts +++ b/backend/src/rpc-api/commands.ts @@ -91,4 +91,5 @@ module.exports = { walletPassphraseChange: 'walletpassphrasechange', getTxoutSetinfo: 'gettxoutsetinfo', getIndexInfo: 'getindexinfo', + testMempoolAccept: 'testmempoolaccept', }; From b38bbb95138f689de373823db3e45bbdbfa4fb3e Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Tue, 9 Jan 2024 11:47:59 +0100 Subject: [PATCH 49/61] [bitcoin core] add missing verbose params to bitcoin core internal routes --- .../src/api/bitcoin/bitcoin-core.routes.ts | 51 +++++++++++++++---- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/backend/src/api/bitcoin/bitcoin-core.routes.ts b/backend/src/api/bitcoin/bitcoin-core.routes.ts index dbdcced1f..d0aa9092a 100644 --- a/backend/src/api/bitcoin/bitcoin-core.routes.ts +++ b/backend/src/api/bitcoin/bitcoin-core.routes.ts @@ -92,12 +92,23 @@ class BitcoinBackendRoutes { private async $getRawTransaction(req: Request, res: Response): Promise { const txid = req.query.txid; + const verbose = req.query.verbose; try { if (typeof(txid) !== 'string' || txid.length !== 64) { res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`); return; } - const decodedTx = await bitcoinClient.getRawTransaction(txid); + if (typeof(verbose) !== 'string') { + res.status(400).send(`invalid param verbose ${verbose}. must be a string representing an integer`); + return; + } + const verboseNumber = parseInt(verbose, 10); + if (typeof(verboseNumber) !== 'number') { + res.status(400).send(`invalid param verbose ${verbose}. must be a valid integer`); + return; + } + + const decodedTx = await bitcoinClient.getRawTransaction(txid, verboseNumber); if (!decodedTx) { res.status(400).send(`unable to get raw transaction for txid ${txid}`); return; @@ -127,15 +138,15 @@ class BitcoinBackendRoutes { } private async $testMempoolAccept(req: Request, res: Response): Promise { - const rawTx = req.body.rawTx; + const rawTxs = req.body.rawTxs; try { - if (typeof(rawTx) !== 'string') { - res.status(400).send(`invalid param rawTx ${rawTx}. must be a string`); + if (typeof(rawTxs) !== 'object') { + res.status(400).send(`invalid param rawTxs ${JSON.stringify(rawTxs)}. must be an array of string`); return; } - const txHex = await bitcoinClient.testMempoolAccept([rawTx]); + const txHex = await bitcoinClient.testMempoolAccept(rawTxs); if (typeof(txHex) !== 'object' || txHex.length === 0) { - res.status(400).send(`testmempoolaccept failed for raw tx ${rawTx}, got an empty result`); + res.status(400).send(`testmempoolaccept failed for raw txs ${JSON.stringify(rawTxs)}, got an empty result`); return; } res.status(200).send(txHex); @@ -146,12 +157,23 @@ class BitcoinBackendRoutes { private async $getMempoolAncestors(req: Request, res: Response): Promise { const txid = req.query.txid; + const verbose = req.query.verbose; try { if (typeof(txid) !== 'string' || txid.length !== 64) { res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`); return; } - const decodedTx = await bitcoinClient.getMempoolAncestors(txid); + if (typeof(verbose) !== 'string') { + res.status(400).send(`invalid param verbose ${verbose}. must be a string representing an integer`); + return; + } + const verboseNumber = parseInt(verbose, 10); + if (typeof(verboseNumber) !== 'number') { + res.status(400).send(`invalid param verbose ${verbose}. must be a valid integer`); + return; + } + + const decodedTx = await bitcoinClient.getMempoolAncestors(txid, verboseNumber); if (!decodedTx) { res.status(400).send(`unable to get mempool ancestors for txid ${txid}`); return; @@ -164,12 +186,23 @@ class BitcoinBackendRoutes { private async $getBlock(req: Request, res: Response): Promise { const blockHash = req.query.hash; + const verbosity = req.query.verbosity; try { if (typeof(blockHash) !== 'string' || blockHash.length !== 64) { res.status(400).send(`invalid param blockHash ${blockHash}. must be a string of 64 char`); return; } - const block = await bitcoinClient.getBlock(blockHash); + if (typeof(verbosity) !== 'string') { + res.status(400).send(`invalid param verbosity ${verbosity}. must be a string representing an integer`); + return; + } + const verbosityNumber = parseInt(verbosity, 10); + if (typeof(verbosityNumber) !== 'number') { + res.status(400).send(`invalid param verbosity ${verbosity}. must be a valid integer`); + return; + } + + const block = await bitcoinClient.getBlock(blockHash, verbosityNumber); if (!block) { res.status(400).send(`unable to get block for block hash ${blockHash}`); return; @@ -188,7 +221,7 @@ class BitcoinBackendRoutes { return; } const blockHeightNumber = parseInt(blockHeight, 10); - if (!blockHeightNumber) { + if (typeof(blockHeightNumber) !== 'number') { res.status(400).send(`invalid param blockHeight ${blockHeight}. must be a valid integer`); return; } From f161f7e8e271e92ce29cf2b10e60a7e44172b6ef Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Fri, 12 Jan 2024 16:38:11 +0100 Subject: [PATCH 50/61] [bitcoin core] fix getMempoolAncestors verbose param --- backend/src/api/bitcoin/bitcoin-core.routes.ts | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/backend/src/api/bitcoin/bitcoin-core.routes.ts b/backend/src/api/bitcoin/bitcoin-core.routes.ts index d0aa9092a..edc32d0fe 100644 --- a/backend/src/api/bitcoin/bitcoin-core.routes.ts +++ b/backend/src/api/bitcoin/bitcoin-core.routes.ts @@ -163,22 +163,17 @@ class BitcoinBackendRoutes { res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`); return; } - if (typeof(verbose) !== 'string') { - res.status(400).send(`invalid param verbose ${verbose}. must be a string representing an integer`); + if (typeof(verbose) !== 'string' || (verbose !== 'true' && verbose !== 'false')) { + res.status(400).send(`invalid param verbose ${verbose}. must be a string ('true' | 'false')`); return; } - const verboseNumber = parseInt(verbose, 10); - if (typeof(verboseNumber) !== 'number') { - res.status(400).send(`invalid param verbose ${verbose}. must be a valid integer`); - return; - } - - const decodedTx = await bitcoinClient.getMempoolAncestors(txid, verboseNumber); - if (!decodedTx) { + + const ancestors = await bitcoinClient.getMempoolAncestors(txid, verbose === 'true' ? true : false); + if (!ancestors) { res.status(400).send(`unable to get mempool ancestors for txid ${txid}`); return; } - res.status(200).send(decodedTx); + res.status(200).send(ancestors); } catch (e: any) { BitcoinBackendRoutes.handleException(e, 'getMempoolAncestors', res); } From ed9826b2d890a4e5dc58f8cf20f7435212d88bb4 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Mon, 15 Jan 2024 16:12:58 +0100 Subject: [PATCH 51/61] [bitcoin core] internal core api from camel case to kebab case --- backend/src/api/bitcoin/bitcoin-core.routes.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/backend/src/api/bitcoin/bitcoin-core.routes.ts b/backend/src/api/bitcoin/bitcoin-core.routes.ts index edc32d0fe..7933dc17b 100644 --- a/backend/src/api/bitcoin/bitcoin-core.routes.ts +++ b/backend/src/api/bitcoin/bitcoin-core.routes.ts @@ -11,15 +11,15 @@ class BitcoinBackendRoutes { public initRoutes(app: Application) { app - .get('/api/internal/bitcoinCore/' + 'getMempoolEntry', this.disableCache, this.$getMempoolEntry) - .post('/api/internal/bitcoinCore/' + 'decodeRawTransaction', this.disableCache, this.$decodeRawTransaction) - .get('/api/internal/bitcoinCore/' + 'getRawTransaction', this.disableCache, this.$getRawTransaction) - .post('/api/internal/bitcoinCore/' + 'sendRawTransaction', this.disableCache, this.$sendRawTransaction) - .post('/api/internal/bitcoinCore/' + 'testMempoolAccept', this.disableCache, this.$testMempoolAccept) - .get('/api/internal/bitcoinCore/' + 'getMempoolAncestors', this.disableCache, this.$getMempoolAncestors) - .get('/api/internal/bitcoinCore/' + 'getBlock', this.disableCache, this.$getBlock) - .get('/api/internal/bitcoinCore/' + 'getBlockHash', this.disableCache, this.$getBlockHash) - .get('/api/internal/bitcoinCore/' + 'getBlockCount', this.disableCache, this.$getBlockCount) + .get('/api/internal/bitcoin-core/' + 'get-mempool-entry', this.disableCache, this.$getMempoolEntry) + .post('/api/internal/bitcoin-core/' + 'decode-raw-transaction', this.disableCache, this.$decodeRawTransaction) + .get('/api/internal/bitcoin-core/' + 'get-raw-transaction', this.disableCache, this.$getRawTransaction) + .post('/api/internal/bitcoin-core/' + 'send-raw-transaction', this.disableCache, this.$sendRawTransaction) + .post('/api/internal/bitcoin-core/' + 'test-mempool-accept', this.disableCache, this.$testMempoolAccept) + .get('/api/internal/bitcoin-core/' + 'get-mempool-ancestors', this.disableCache, this.$getMempoolAncestors) + .get('/api/internal/bitcoin-core/' + 'get-block', this.disableCache, this.$getBlock) + .get('/api/internal/bitcoin-core/' + 'get-block-hash', this.disableCache, this.$getBlockHash) + .get('/api/internal/bitcoin-core/' + 'get-block-count', this.disableCache, this.$getBlockCount) ; } From 6a189c484a7ee7e7591860fcaf9ff9ce92b6c479 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 15 Jan 2024 22:34:12 +0000 Subject: [PATCH 52/61] AccDash: accelerated -> pending --- .../accelerations-list/accelerations-list.component.html | 2 +- .../acceleration/pending-stats/pending-stats.component.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html index f2265282f..45709a47e 100644 --- a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html +++ b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html @@ -48,7 +48,7 @@ {{ acceleration.blockHeight }} - Pending + Pending Mined Canceled diff --git a/frontend/src/app/components/acceleration/pending-stats/pending-stats.component.html b/frontend/src/app/components/acceleration/pending-stats/pending-stats.component.html index c94bbf43a..bd062cd81 100644 --- a/frontend/src/app/components/acceleration/pending-stats/pending-stats.component.html +++ b/frontend/src/app/components/acceleration/pending-stats/pending-stats.component.html @@ -4,7 +4,7 @@
Transactions
{{ stats.count }}
-
accelerated
+
pending
From 5009ca909cd81aa7d2f2636c649e3aa09814c1dd Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 15 Jan 2024 22:52:42 +0000 Subject: [PATCH 53/61] AccDash: out-of-band fees -> bid boost --- .../acceleration-fees-graph.component.html | 2 +- .../acceleration-fees-graph.component.ts | 10 +++++----- .../acceleration-stats.component.html | 4 ++-- .../acceleration-stats/acceleration-stats.component.ts | 8 ++++---- .../accelerations-list.component.html | 4 ++-- .../accelerations-list/accelerations-list.component.ts | 3 +++ .../components/transaction/transaction.component.html | 2 +- .../components/transaction/transaction.component.ts | 2 +- frontend/src/app/interfaces/node-api.interface.ts | 3 ++- 9 files changed, 21 insertions(+), 17 deletions(-) diff --git a/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.html b/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.html index 9ae0ddade..98095aa07 100644 --- a/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.html +++ b/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.html @@ -29,7 +29,7 @@
-
Out-of-band Fees Per Block
+
Total Bid Boost
diff --git a/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts b/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts index d27b10690..4460bffe4 100644 --- a/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts +++ b/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts @@ -81,7 +81,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy { }), map(([accelerations, blockFeesResponse]) => { return { - avgFeesPaid: accelerations.filter(acc => acc.status === 'completed').reduce((total, acc) => total + acc.feePaid, 0) / accelerations.length + avgFeesPaid: accelerations.filter(acc => acc.status === 'completed').reduce((total, acc) => total + (acc.feePaid - acc.baseFee - acc.vsizeFee), 0) / accelerations.length }; }), ); @@ -151,7 +151,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy { while (last <= val.avgHeight) { blockCount++; totalFeeDelta += (blockAccelerations[last] || []).reduce((total, acc) => total + acc.feeDelta, 0); - totalFeePaid += (blockAccelerations[last] || []).reduce((total, acc) => total + acc.feePaid, 0); + totalFeePaid += (blockAccelerations[last] || []).reduce((total, acc) => total + (acc.feePaid - acc.baseFee - acc.vsizeFee), 0); totalCount += (blockAccelerations[last] || []).length; last++; } @@ -246,7 +246,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy { icon: 'roundRect', }, { - name: 'Out-of-band fees per block', + name: 'Total bid boost per block', inactiveColor: 'rgb(110, 112, 121)', textStyle: { color: 'white', @@ -256,7 +256,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy { ], selected: { 'In-band fees per block': false, - 'Out-of-band fees per block': true, + 'Total bid boost per block': true, }, show: !this.widget, }, @@ -299,7 +299,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy { { legendHoverLink: false, zlevel: 1, - name: 'Out-of-band fees per block', + name: 'Total bid boost per block', data: data.map(block => [block.timestamp * 1000, block.avgFeePaid, block.avgHeight]), stack: 'Total', type: 'bar', diff --git a/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.html b/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.html index 21cd57ae0..0f4421ac9 100644 --- a/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.html +++ b/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.html @@ -8,7 +8,7 @@
-
Out-of-band Fees
+
Total Bid Boost
{{ stats.totalFeesPaid / 100_000_000 | amountShortener: 4 }} BTC
@@ -36,7 +36,7 @@
-
Out-of-band Fees
+
Total Bid Boost
diff --git a/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.ts b/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.ts index d83303619..0a6ef065c 100644 --- a/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.ts +++ b/frontend/src/app/components/acceleration/acceleration-stats/acceleration-stats.component.ts @@ -27,11 +27,11 @@ export class AccelerationStatsComponent implements OnInit { let totalFeesPaid = 0; let totalSucceeded = 0; let totalCanceled = 0; - for (const acceleration of accelerations) { - if (acceleration.status === 'completed') { + for (const acc of accelerations) { + if (acc.status === 'completed') { totalSucceeded++; - totalFeesPaid += acceleration.feePaid || 0; - } else if (acceleration.status === 'failed') { + totalFeesPaid += (acc.feePaid - acc.baseFee - acc.vsizeFee) || 0; + } else if (acc.status === 'failed') { totalCanceled++; } } diff --git a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html index 45709a47e..9a919ca54 100644 --- a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html +++ b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html @@ -14,7 +14,7 @@ Requested - Out-of-band Fee + Bid Boost Block Status @@ -39,7 +39,7 @@ - {{ (acceleration.feePaid) | number }} sat + {{ (acceleration.boost) | number }} sat ~ diff --git a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.ts b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.ts index ddd89d31c..69af8b966 100644 --- a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.ts +++ b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.ts @@ -49,6 +49,9 @@ export class AccelerationsListComponent implements OnInit { acceleration.status = acceleration.status || 'accelerating'; } } + for (const acc of accelerations) { + acc.boost = acc.feePaid - acc.baseFee - acc.vsizeFee; + } if (this.widget) { return of(accelerations.slice(-6).reverse()); } else { diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index 8ac4cf919..9ad939deb 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -529,7 +529,7 @@ Effective fee rate
- + diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index 409ba33ff..883a3fa7f 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -255,7 +255,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { ).subscribe((accelerationHistory) => { for (const acceleration of accelerationHistory) { if (acceleration.txid === this.txId && (acceleration.status === 'completed' || acceleration.status === 'mined') && acceleration.feePaid > 0) { - acceleration.actualFeeDelta = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + acceleration.feePaid - acceleration.baseFee - acceleration.vsizeFee); + acceleration.acceleratedFee = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + acceleration.feePaid - acceleration.baseFee - acceleration.vsizeFee); this.accelerationInfo = acceleration; } } diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index e225eb758..1ca0d7725 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -319,7 +319,8 @@ export interface Acceleration { blockHash: string; blockHeight: number; - actualFeeDelta?: number; + acceleratedFee?: number; + boost?: number; } export interface AccelerationHistoryParams { From d2270ffc262ab9f497f0813616a32b36aaaf8e4e Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 15 Jan 2024 23:39:51 +0000 Subject: [PATCH 54/61] Move acceleration tab icon, fix responsive issues --- .../master-page/master-page.component.html | 6 +++--- .../master-page/master-page.component.scss | 15 ++++++++++++++- .../search-form/search-form.component.scss | 2 +- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/components/master-page/master-page.component.html b/frontend/src/app/components/master-page/master-page.component.html index c6af2c282..54a608869 100644 --- a/frontend/src/app/components/master-page/master-page.component.html +++ b/frontend/src/app/components/master-page/master-page.component.html @@ -49,12 +49,12 @@