From 726895d15523844b07843e2b85180e31fcca96ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Thu, 29 May 2025 15:38:35 -0300 Subject: [PATCH 01/11] feat: accept multiple URLs for networks --- Makefile | 2 +- README.md | 4 +-- config/example-config.json | 14 ++++++-- config/index.ts | 6 ++-- services/chronikService.ts | 65 ++++++++++++++++++++------------------ 5 files changed, 53 insertions(+), 38 deletions(-) diff --git a/Makefile b/Makefile index 1cfa9363d..ba5950759 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ git_hook_setup = cp .githooks/pre-commit .git/hooks/pre-commit git_diff_to_master = git diff --name-only --diff-filter=ACMRTUXB origin/master > DIFF -create_test_paybutton_json = echo { \"priceAPIURL\": \"foo\", \"networkBlockchainClients\": { \"ecash\": \"chronik\", \"bitcoincash\": \"chronik\" }, \"networkBlockchainURLs\": { \"ecash\": \"https://xec.paybutton.io\", \"bitcoincash\": \"https://bch.paybutton.io\" }, \"wsBaseURL\": \"http://localhost:5000\", \"apiDomain\": \"http://localhost:3000\" } > paybutton-config.json +create_test_paybutton_json = echo { \"priceAPIURL\": \"foo\", \"networkBlockchainClients\": { \"ecash\": \"chronik\", \"bitcoincash\": \"chronik\" }, \"networkBlockchainURLs\": { \"ecash\": [\"https://xec.paybutton.io\"], \"bitcoincash\": [\"https://bch.paybutton.io\"] }, \"wsBaseURL\": \"http://localhost:5000\", \"apiDomain\": \"http://localhost:3000\" } > paybutton-config.json touch_local_env = touch .env.local prod: diff --git a/README.md b/README.md index f394d323a..93fd14a6a 100644 --- a/README.md +++ b/README.md @@ -122,8 +122,8 @@ default: false, #### networkBlockchainURLs ``` type: { - "ecash": "https://chronik.fabien.cash", - "bitcoincash": "https://chronik.pay2stay.com/bch" + "ecash": ["https://xec.paybutton.org", "https://chronik.fabien.cash"], + "bitcoincash": ["https://chronik.pay2stay.com/bch"] } ``` diff --git a/config/example-config.json b/config/example-config.json index 04ae96b36..46e49d575 100644 --- a/config/example-config.json +++ b/config/example-config.json @@ -12,8 +12,18 @@ "bitcoincash": "chronik" }, "networkBlockchainURLs": { - "ecash": "https://xec.paybutton.io", - "bitcoincash": "https://bch.paybutton.io" + "ecash": [ + "https://xec.paybutton.org", + "https://chronik1.alitayin.com", + "https://chronik2.alitayin.com", + "https://chronik.e.cash", + "https://chronik-native1.fabien.cash", + "https://chronik-native2.fabien.cash", + "https://chronik-native3.fabien.cash", + "https://chronik.pay2stay.com/xec", + "https://chronik.pay2stay.com/xec2" + ], + "bitcoincash": ["https://bch.paybutton.io"] }, "networksUnderMaintenance": { diff --git a/config/index.ts b/config/index.ts index 24d80b1ea..b67055e95 100644 --- a/config/index.ts +++ b/config/index.ts @@ -15,7 +15,7 @@ interface Config { priceAPIURL: string redisURL: string networkBlockchainClients: KeyValueT - networkBlockchainURLs: KeyValueT + networkBlockchainURLs: KeyValueT networksUnderMaintenance: KeyValueT triggerPOSTTimeout: number sideshiftAffiliateId: string @@ -35,7 +35,7 @@ const readConfig = (): Config => { if ( ( config.networkBlockchainURLs.ecash === undefined || - config.networkBlockchainURLs.ecash === '' + config.networkBlockchainURLs.ecash.length === 0 ) && !config.networksUnderMaintenance.ecash ) { @@ -43,7 +43,7 @@ const readConfig = (): Config => { } else if ( ( config.networkBlockchainURLs.bitcoincash === undefined || - config.networkBlockchainURLs.bitcoincash === '' + config.networkBlockchainURLs.bitcoincash.length === 0 ) && !config.networksUnderMaintenance.bitcoincash ) { diff --git a/services/chronikService.ts b/services/chronikService.ts index 894019ac0..f0a6c0157 100644 --- a/services/chronikService.ts +++ b/services/chronikService.ts @@ -1,4 +1,4 @@ -import { BlockInfo, ChronikClient, ScriptType, ScriptUtxo, Tx, WsConfig, WsEndpoint, WsMsgClient, WsSubScriptClient } from 'chronik-client-cashtokens' +import { BlockInfo, ChronikClient, ConnectionStrategy, ScriptType, ScriptUtxo, Tx, WsConfig, WsEndpoint, WsMsgClient, WsSubScriptClient } from 'chronik-client-cashtokens' import { encode, decode } from 'ecashaddrjs' import bs58 from 'bs58' import { AddressWithTransaction, BlockchainInfo, TransactionDetails, ProcessedMessages, SubbedAddressesLog, SyncAndSubscriptionReturn, SubscriptionReturn, SimpleBlockInfo } from 'types/chronikTypes' @@ -107,38 +107,43 @@ export function getNullDataScriptData (outputScript: string): OpReturnData | nul } export class ChronikBlockchainClient { - chronik: ChronikClient - networkId: number - networkSlug: string - chronikWSEndpoint: WsEndpoint - confirmedTxsHashesFromLastBlock: string[] - wsEndpoint: Socket - CHRONIK_MSG_PREFIX: string - lastProcessedMessages: ProcessedMessages - initializing: boolean - mempoolTxsBeingProcessed: number + chronik!: ChronikClient + networkId!: number + networkSlug!: string + chronikWSEndpoint!: WsEndpoint + confirmedTxsHashesFromLastBlock!: string[] + wsEndpoint!: Socket + CHRONIK_MSG_PREFIX!: string + lastProcessedMessages!: ProcessedMessages + initializing!: boolean + mempoolTxsBeingProcessed!: number constructor (networkSlug: string) { - if (process.env.WS_AUTH_KEY === '' || process.env.WS_AUTH_KEY === undefined) { - throw new Error(RESPONSE_MESSAGES.MISSING_WS_AUTH_KEY_400.message) - } - - this.initializing = true - this.mempoolTxsBeingProcessed = 0 - this.networkSlug = networkSlug - this.networkId = NETWORK_IDS_FROM_SLUGS[networkSlug] - this.chronik = new ChronikClient([config.networkBlockchainURLs[networkSlug]]) - this.chronikWSEndpoint = this.chronik.ws(this.getWsConfig()) - this.confirmedTxsHashesFromLastBlock = [] - void this.chronikWSEndpoint.waitForOpen() - this.chronikWSEndpoint.subscribeToBlocks() - this.lastProcessedMessages = { confirmed: {}, unconfirmed: {} } - this.CHRONIK_MSG_PREFIX = `[CHRONIK — ${networkSlug}]` - this.wsEndpoint = io(`${config.wsBaseURL}/broadcast`, { - query: { - key: process.env.WS_AUTH_KEY + void (async () => { + if (process.env.WS_AUTH_KEY === '' || process.env.WS_AUTH_KEY === undefined) { + throw new Error(RESPONSE_MESSAGES.MISSING_WS_AUTH_KEY_400.message) } - }) + + this.initializing = true + this.mempoolTxsBeingProcessed = 0 + this.networkSlug = networkSlug + this.networkId = NETWORK_IDS_FROM_SLUGS[networkSlug] + this.chronik = await ChronikClient.useStrategy( + ConnectionStrategy.ClosestFirst, + config.networkBlockchainURLs[networkSlug] + ) + this.chronikWSEndpoint = this.chronik.ws(this.getWsConfig()) + this.confirmedTxsHashesFromLastBlock = [] + void this.chronikWSEndpoint.waitForOpen() + this.chronikWSEndpoint.subscribeToBlocks() + this.lastProcessedMessages = { confirmed: {}, unconfirmed: {} } + this.CHRONIK_MSG_PREFIX = `[CHRONIK — ${networkSlug}]` + this.wsEndpoint = io(`${config.wsBaseURL}/broadcast`, { + query: { + key: process.env.WS_AUTH_KEY + } + }) + })() } public setInitialized (): void { From 581dcd0ba71d17316be74dee329c2cceca1454b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Thu, 29 May 2025 16:22:26 -0300 Subject: [PATCH 02/11] fix: wait for latency test --- constants/index.ts | 3 +++ services/chronikService.ts | 16 +++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/constants/index.ts b/constants/index.ts index c646db747..1a97bab4d 100644 --- a/constants/index.ts +++ b/constants/index.ts @@ -146,6 +146,9 @@ export const FETCH_N_TIMEOUT = 120000 // When fetching some address transactions, delay (in ms) between each fetch. export const FETCH_DELAY = 100 +// Delay to check if latency test has finished, when the app starts. +export const LATENCY_TEST_CHECK_DELAY = 200 + // Wait time (in ms) to see if there are new unsynced addresses export const SYNC_NEW_ADDRESSES_DELAY = 10000 diff --git a/services/chronikService.ts b/services/chronikService.ts index f0a6c0157..4b412f82d 100644 --- a/services/chronikService.ts +++ b/services/chronikService.ts @@ -2,7 +2,7 @@ import { BlockInfo, ChronikClient, ConnectionStrategy, ScriptType, ScriptUtxo, T import { encode, decode } from 'ecashaddrjs' import bs58 from 'bs58' import { AddressWithTransaction, BlockchainInfo, TransactionDetails, ProcessedMessages, SubbedAddressesLog, SyncAndSubscriptionReturn, SubscriptionReturn, SimpleBlockInfo } from 'types/chronikTypes' -import { CHRONIK_MESSAGE_CACHE_DELAY, RESPONSE_MESSAGES, XEC_TIMESTAMP_THRESHOLD, XEC_NETWORK_ID, BCH_NETWORK_ID, BCH_TIMESTAMP_THRESHOLD, FETCH_DELAY, FETCH_N, KeyValueT, NETWORK_IDS_FROM_SLUGS, SOCKET_MESSAGES, NETWORK_IDS, NETWORK_TICKERS, MainNetworkSlugsType, MAX_MEMPOOL_TXS_TO_PROCESS_AT_A_TIME, MEMPOOL_PROCESS_DELAY, CHRONIK_INITIALIZATION_DELAY } from 'constants/index' +import { CHRONIK_MESSAGE_CACHE_DELAY, RESPONSE_MESSAGES, XEC_TIMESTAMP_THRESHOLD, XEC_NETWORK_ID, BCH_NETWORK_ID, BCH_TIMESTAMP_THRESHOLD, FETCH_DELAY, FETCH_N, KeyValueT, NETWORK_IDS_FROM_SLUGS, SOCKET_MESSAGES, NETWORK_IDS, NETWORK_TICKERS, MainNetworkSlugsType, MAX_MEMPOOL_TXS_TO_PROCESS_AT_A_TIME, MEMPOOL_PROCESS_DELAY, CHRONIK_INITIALIZATION_DELAY, LATENCY_TEST_CHECK_DELAY } from 'constants/index' import { productionAddresses } from 'prisma/seeds/addresses' import { TransactionWithAddressAndPrices, @@ -118,7 +118,10 @@ export class ChronikBlockchainClient { initializing!: boolean mempoolTxsBeingProcessed!: number + private latencyTestFinished: boolean + constructor (networkSlug: string) { + this.latencyTestFinished = false void (async () => { if (process.env.WS_AUTH_KEY === '' || process.env.WS_AUTH_KEY === undefined) { throw new Error(RESPONSE_MESSAGES.MISSING_WS_AUTH_KEY_400.message) @@ -132,6 +135,7 @@ export class ChronikBlockchainClient { ConnectionStrategy.ClosestFirst, config.networkBlockchainURLs[networkSlug] ) + this.latencyTestFinished = true this.chronikWSEndpoint = this.chronik.ws(this.getWsConfig()) this.confirmedTxsHashesFromLastBlock = [] void this.chronikWSEndpoint.waitForOpen() @@ -146,6 +150,15 @@ export class ChronikBlockchainClient { })() } + public async waitForLatencyTest (): Promise { + while (true) { + if (this.latencyTestFinished) { + return + } + await new Promise(resolve => setTimeout(resolve, LATENCY_TEST_CHECK_DELAY)) + } + } + public setInitialized (): void { this.initializing = false } @@ -726,6 +739,7 @@ class MultiBlockchainClient { if (this.isRunningApp()) { asyncOperations.push( (async () => { + await newClient.waitForLatencyTest() console.log(`[CHRONIK — ${networkSlug}] Subscribing addresses in database...`) await newClient.subscribeInitialAddresses() console.log(`[CHRONIK — ${networkSlug}] Syncing missed transactions...`) From fe5e15ed59565c716c6510392dfe1863fe90bd82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Fri, 30 May 2025 21:24:44 -0300 Subject: [PATCH 03/11] chore: update ecashaddrjs --- components/ButtonGenerator/index.tsx | 6 +++--- package.json | 4 ++-- services/chronikService.ts | 6 +++--- types/ecashaddrjs/index.d.ts | 4 ++-- yarn.lock | 24 ++++-------------------- 5 files changed, 14 insertions(+), 30 deletions(-) diff --git a/components/ButtonGenerator/index.tsx b/components/ButtonGenerator/index.tsx index 45592f33a..b0db31424 100644 --- a/components/ButtonGenerator/index.tsx +++ b/components/ButtonGenerator/index.tsx @@ -4,7 +4,7 @@ import style from '../../styles/landing.module.css' import { PayButton, Widget as PayButtonWidget } from '@paybutton/react' import { ChromePicker } from 'react-color' import CodeBlock from './CodeBlock' -import { decode } from 'ecashaddrjs' +import { decodeCashAddress } from 'ecashaddrjs' import { generatorFormFields } from './data.js' import { PRIMARY_XEC_COLOR, @@ -83,7 +83,7 @@ export const initialButtonState: ButtonState = { onOpen: '', onClose: '', wsBaseURL: '', - apiBaseURL: '', + apiBaseURL: '' } export default function ButtonGenerator (): JSX.Element { @@ -93,7 +93,7 @@ export default function ButtonGenerator (): JSX.Element { const isValidAddress = (address: string): string => { try { - return decode(address).prefix + return decodeCashAddress(address).prefix } catch (err) { return 'not valid' } diff --git a/package.json b/package.json index 6c13c7217..905524440 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "cors": "^2.8.5", "cross-env": "^7.0.2", "dotenv-cli": "^5.1.0", - "ecashaddrjs": "^1.5.8", + "ecashaddrjs": "^2.0.0", "express": "^4.17.1", "graphql": "^16.3.0", "helmet": "^4.4.1", @@ -97,7 +97,7 @@ "typescript": "^5.7.2" }, "resolutions": { - "chronik-client-cashtokens/ecashaddrjs": "^1.5.8" + "chronik-client-cashtokens/ecashaddrjs": "^2.0.0" }, "lint-staged": { "*.ts?(x)": [ diff --git a/services/chronikService.ts b/services/chronikService.ts index 4b412f82d..ef01c14a8 100644 --- a/services/chronikService.ts +++ b/services/chronikService.ts @@ -1,5 +1,5 @@ import { BlockInfo, ChronikClient, ConnectionStrategy, ScriptType, ScriptUtxo, Tx, WsConfig, WsEndpoint, WsMsgClient, WsSubScriptClient } from 'chronik-client-cashtokens' -import { encode, decode } from 'ecashaddrjs' +import { encodeCashAddress, decodeCashAddress } from 'ecashaddrjs' import bs58 from 'bs58' import { AddressWithTransaction, BlockchainInfo, TransactionDetails, ProcessedMessages, SubbedAddressesLog, SyncAndSubscriptionReturn, SubscriptionReturn, SimpleBlockInfo } from 'types/chronikTypes' import { CHRONIK_MESSAGE_CACHE_DELAY, RESPONSE_MESSAGES, XEC_TIMESTAMP_THRESHOLD, XEC_NETWORK_ID, BCH_NETWORK_ID, BCH_TIMESTAMP_THRESHOLD, FETCH_DELAY, FETCH_N, KeyValueT, NETWORK_IDS_FROM_SLUGS, SOCKET_MESSAGES, NETWORK_IDS, NETWORK_TICKERS, MainNetworkSlugsType, MAX_MEMPOOL_TXS_TO_PROCESS_AT_A_TIME, MEMPOOL_PROCESS_DELAY, CHRONIK_INITIALIZATION_DELAY, LATENCY_TEST_CHECK_DELAY } from 'constants/index' @@ -639,7 +639,7 @@ export function fromHash160 (networkSlug: string, type: string, hash160: string) hash160Uint8Array[i] = buffer[i] } - return encode( + return encodeCashAddress( networkSlug, type.toUpperCase(), hash160Uint8Array @@ -648,7 +648,7 @@ export function fromHash160 (networkSlug: string, type: string, hash160: string) export function toHash160 (address: string): {type: ScriptType, hash160: string} { try { - const { type, hash } = decode(address) + const { type, hash } = decodeCashAddress(address) const legacyAdress = bs58.encode(hash) const addrHash160 = Buffer.from(bs58.decode(legacyAdress)).toString( 'hex' diff --git a/types/ecashaddrjs/index.d.ts b/types/ecashaddrjs/index.d.ts index b770cca6f..6e5de02a1 100644 --- a/types/ecashaddrjs/index.d.ts +++ b/types/ecashaddrjs/index.d.ts @@ -1,4 +1,4 @@ declare module 'ecashaddrjs' { - export function decode (address: string): { prefix: string, type: string, hash: Uint8Array } - export function encode (prefix: string, type: string, hash: Uint8Array): string + export function decodeCashAddress (address: string): { prefix: string, type: string, hash: Uint8Array } + export function encodeCashAddress (prefix: string, type: string, hash: Uint8Array): string } diff --git a/yarn.lock b/yarn.lock index d188e07db..d4ddc8cbe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1528,11 +1528,6 @@ resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.2.3.tgz#cd432f280beb8d8de5b7cd2501e9f502e9f3dd72" integrity sha512-aLG2MaFs4y7IwaMTosz2r4mVbqRyCnMoFqOcmfTi7/mAS+G4IMH0vJp4oLdbshqiVoiVuKrAfqtXj55/m7Qu1Q== -"@noble/hashes@^1.2.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" - integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== - "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" @@ -2668,14 +2663,6 @@ bs58check@2.1.2, bs58check@<3.0.0, bs58check@^2.1.2: create-hash "^1.1.0" safe-buffer "^5.1.2" -bs58check@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/bs58check/-/bs58check-3.0.1.tgz#2094d13720a28593de1cba1d8c4e48602fdd841c" - integrity sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ== - dependencies: - "@noble/hashes" "^1.2.0" - bs58 "^5.0.0" - bser@2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz" @@ -3281,13 +3268,10 @@ ecashaddrjs@^1.0.7: dependencies: big-integer "1.6.36" -ecashaddrjs@^1.5.8, "ecashaddrjs@file:../.cache/yarn/v6/npm-chronik-client-cashtokens-3.1.1-rc0-e5e4a3538e8010b70623974a731bf2712506c5e3-integrity/node_modules/ecashaddrjs": - version "1.5.8" - resolved "https://registry.yarnpkg.com/ecashaddrjs/-/ecashaddrjs-1.5.8.tgz#fff684d14a944cfd4bca56dd8dc3adbfce8016f9" - integrity sha512-EBL052a1Hd1+wwaBQr3Q+jXY4XfH0jXR1tK360P8GQ9YJUZ5zLG+0I7yee8/QgHY04zdoCzDOY0fRlwqtqDhRg== - dependencies: - big-integer "1.6.36" - bs58check "^3.0.1" +ecashaddrjs@^2.0.0, "ecashaddrjs@file:../.cache/yarn/v6/npm-chronik-client-cashtokens-3.1.1-rc0-e5e4a3538e8010b70623974a731bf2712506c5e3-integrity/node_modules/ecashaddrjs": + version "2.0.0" + resolved "https://registry.yarnpkg.com/ecashaddrjs/-/ecashaddrjs-2.0.0.tgz#d45ede7fb6168815dbcf664b8e0a6872e485d874" + integrity sha512-EvK1V4D3+nIEoD0ggy/b0F4lW39/72R9aOs/scm6kxMVuXu16btc+H74eQv7okNfXaQWKgolEekZkQ6wfcMMLw== ecdsa-sig-formatter@1.0.11: version "1.0.11" From f214b9ac24cb84177323023225f059bde10f8742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Fri, 30 May 2025 22:09:48 -0300 Subject: [PATCH 04/11] fix: ecashaddrjs breaking changes --- services/chronikService.ts | 24 ++++++++++-------------- types/ecashaddrjs/index.d.ts | 4 ---- 2 files changed, 10 insertions(+), 18 deletions(-) delete mode 100644 types/ecashaddrjs/index.d.ts diff --git a/services/chronikService.ts b/services/chronikService.ts index ef01c14a8..0e1ca49b0 100644 --- a/services/chronikService.ts +++ b/services/chronikService.ts @@ -1,6 +1,5 @@ -import { BlockInfo, ChronikClient, ConnectionStrategy, ScriptType, ScriptUtxo, Tx, WsConfig, WsEndpoint, WsMsgClient, WsSubScriptClient } from 'chronik-client-cashtokens' +import { BlockInfo, ChronikClient, ConnectionStrategy, ScriptUtxo, Tx, WsConfig, WsEndpoint, WsMsgClient, WsSubScriptClient } from 'chronik-client-cashtokens' import { encodeCashAddress, decodeCashAddress } from 'ecashaddrjs' -import bs58 from 'bs58' import { AddressWithTransaction, BlockchainInfo, TransactionDetails, ProcessedMessages, SubbedAddressesLog, SyncAndSubscriptionReturn, SubscriptionReturn, SimpleBlockInfo } from 'types/chronikTypes' import { CHRONIK_MESSAGE_CACHE_DELAY, RESPONSE_MESSAGES, XEC_TIMESTAMP_THRESHOLD, XEC_NETWORK_ID, BCH_NETWORK_ID, BCH_TIMESTAMP_THRESHOLD, FETCH_DELAY, FETCH_N, KeyValueT, NETWORK_IDS_FROM_SLUGS, SOCKET_MESSAGES, NETWORK_IDS, NETWORK_TICKERS, MainNetworkSlugsType, MAX_MEMPOOL_TXS_TO_PROCESS_AT_A_TIME, MEMPOOL_PROCESS_DELAY, CHRONIK_INITIALIZATION_DELAY, LATENCY_TEST_CHECK_DELAY } from 'constants/index' import { productionAddresses } from 'prisma/seeds/addresses' @@ -28,6 +27,7 @@ import { executeAddressTriggers } from './triggerService' import { appendTxsToFile } from 'prisma/seeds/transactions' import { PHASE_PRODUCTION_BUILD } from 'next/dist/shared/lib/constants' import { syncPastDaysNewerPrices } from './priceService' +import { AddressType } from 'ecashaddrjs/dist/types' const decoder = new TextDecoder() @@ -182,7 +182,7 @@ export class ChronikBlockchainClient { } public getSubscribedAddresses (): string[] { - const ret = this.chronikWSEndpoint.subs.scripts.map((script: WsSubScriptClient) => fromHash160(this.networkSlug, script.scriptType, script.payload)) + const ret = this.chronikWSEndpoint.subs.scripts.map((script: WsSubScriptClient) => fromHash160(this.networkSlug, script.scriptType as AddressType, script.payload)) return [...new Set(ret)] } @@ -629,7 +629,7 @@ export class ChronikBlockchainClient { } } -export function fromHash160 (networkSlug: string, type: string, hash160: string): string { +export function fromHash160 (networkSlug: string, type: AddressType, hash160: string): string { const buffer = Buffer.from(hash160, 'hex') // Because ecashaddrjs only accepts Uint8Array as input type, convert @@ -641,19 +641,15 @@ export function fromHash160 (networkSlug: string, type: string, hash160: string) return encodeCashAddress( networkSlug, - type.toUpperCase(), + type, hash160Uint8Array ) } -export function toHash160 (address: string): {type: ScriptType, hash160: string} { +export function toHash160 (address: string): {type: AddressType, hash160: string} { try { const { type, hash } = decodeCashAddress(address) - const legacyAdress = bs58.encode(hash) - const addrHash160 = Buffer.from(bs58.decode(legacyAdress)).toString( - 'hex' - ) - return { type: type.toLowerCase() as ScriptType, hash160: addrHash160 } + return { type, hash160: hash } } catch (err) { console.log('[CHRONIK]: Error converting address to hash160') throw err @@ -669,14 +665,14 @@ export function outputScriptToAddress (networkSlug: string, outputScript: string let hash160 switch (typeTestSlice) { case '76a9': - addressType = 'P2PKH' + addressType = 'p2pkh' hash160 = outputScript.substring( outputScript.indexOf('76a914') + '76a914'.length, outputScript.lastIndexOf('88ac') ) break case 'a914': - addressType = 'P2SH' + addressType = 'p2sh' hash160 = outputScript.substring( outputScript.indexOf('a914') + 'a914'.length, outputScript.lastIndexOf('87') @@ -688,7 +684,7 @@ export function outputScriptToAddress (networkSlug: string, outputScript: string if (hash160.length !== 40) return undefined - return fromHash160(networkSlug, addressType, hash160) + return fromHash160(networkSlug, addressType as AddressType, hash160) } class MultiBlockchainClient { diff --git a/types/ecashaddrjs/index.d.ts b/types/ecashaddrjs/index.d.ts deleted file mode 100644 index 6e5de02a1..000000000 --- a/types/ecashaddrjs/index.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module 'ecashaddrjs' { - export function decodeCashAddress (address: string): { prefix: string, type: string, hash: Uint8Array } - export function encodeCashAddress (prefix: string, type: string, hash: Uint8Array): string -} From 8ec0655a213013b4dfb11ef8603a4f71bfeb42d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Mon, 2 Jun 2025 19:37:54 -0300 Subject: [PATCH 05/11] fix: wait for multichronik on api tests --- services/chronikService.ts | 15 +++++++++++++++ tests/integration-tests/api.test.ts | 12 ++++++++---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/services/chronikService.ts b/services/chronikService.ts index 0e1ca49b0..fe8982d32 100644 --- a/services/chronikService.ts +++ b/services/chronikService.ts @@ -689,9 +689,11 @@ export function outputScriptToAddress (networkSlug: string, outputScript: string class MultiBlockchainClient { private clients!: Record + public initializing: boolean constructor () { console.log('Initializing MultiBlockchainClient...') + this.initializing = true void (async () => { if (this.isRunningApp()) { await syncPastDaysNewerPrices() @@ -713,9 +715,16 @@ class MultiBlockchainClient { } await Promise.all(asyncOperations) } + this.initializing = false })() } + public async waitForStart (): Promise { + while (this.initializing) { + await new Promise(resolve => setTimeout(resolve, LATENCY_TEST_CHECK_DELAY)) + } + } + private isRunningApp (): boolean { if ( process.env.NEXT_PHASE !== PHASE_PRODUCTION_BUILD && @@ -743,6 +752,12 @@ class MultiBlockchainClient { console.log(`[CHRONIK — ${networkSlug}] Finished instantiating client.`) })() ) + } else if (process.env.NODE_ENV === 'test') { + asyncOperations.push( + (async () => { + await newClient.waitForLatencyTest() + })() + ) } return newClient diff --git a/tests/integration-tests/api.test.ts b/tests/integration-tests/api.test.ts index d06d0b68e..bca72febd 100644 --- a/tests/integration-tests/api.test.ts +++ b/tests/integration-tests/api.test.ts @@ -30,6 +30,7 @@ import { import { RESPONSE_MESSAGES, NETWORK_SLUGS } from 'constants/index' import { Prisma, Transaction } from '@prisma/client' +import { multiBlockchainClient } from 'services/chronikService' const setUpUsers = async (): Promise => { await createUserProfile('test-u-id') await createUserProfile('test-u-id2') @@ -46,10 +47,13 @@ jest.mock('../../utils/setSession', () => { }) beforeAll(async () => { - await setUpUsers() - jest.spyOn(console, 'log').mockImplementation(() => {}) - jest.spyOn(console, 'warn').mockImplementation(() => {}) - jest.spyOn(console, 'error').mockImplementation(() => {}) + return await (async () => { + await setUpUsers() + jest.spyOn(console, 'log').mockImplementation(() => {}) + jest.spyOn(console, 'warn').mockImplementation(() => {}) + jest.spyOn(console, 'error').mockImplementation(() => {}) + await multiBlockchainClient.waitForStart() + })() }) afterAll(async () => { From 7a7f962479c684061124d5d85f927978d07e5a9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Tue, 3 Jun 2025 10:12:40 -0300 Subject: [PATCH 06/11] fix: removed extra space --- config/example-config.json | 1 - 1 file changed, 1 deletion(-) diff --git a/config/example-config.json b/config/example-config.json index 46e49d575..09719a021 100644 --- a/config/example-config.json +++ b/config/example-config.json @@ -24,7 +24,6 @@ "https://chronik.pay2stay.com/xec2" ], "bitcoincash": ["https://bch.paybutton.io"] - }, "networksUnderMaintenance": { "bitcoincash": true From c8475574689ba6635d4fe257daeaf8ac75fc9579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Tue, 3 Jun 2025 11:02:13 -0300 Subject: [PATCH 07/11] feat: add URLs to admin props --- pages/admin/index.tsx | 8 +++++++- services/chronikService.ts | 11 +++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/pages/admin/index.tsx b/pages/admin/index.tsx index ad7890c46..84f47d17a 100644 --- a/pages/admin/index.tsx +++ b/pages/admin/index.tsx @@ -12,6 +12,8 @@ import TableContainer from '../../components/TableContainer/TableContainer' import EyeIcon from 'assets/eye-icon.png' import Image from 'next/image' import { removeUnserializableFields } from 'utils' +import { multiBlockchainClient } from 'services/chronikService' +import { MainNetworkSlugsType } from 'constants/index' export const getServerSideProps: GetServerSideProps = async (context) => { // this runs on the backend, so we must call init on supertokens-node SDK @@ -32,13 +34,15 @@ export const getServerSideProps: GetServerSideProps = async (context) => { const userId = session?.getUserId() const user = await fetchUserWithSupertokens(userId) removeUnserializableFields(user.userProfile) + const chronikUrls = multiBlockchainClient.getUrls() const isAdmin = await isUserAdmin(userId) return { props: { userId, user, - isAdmin + isAdmin, + chronikUrls } } } @@ -47,6 +51,8 @@ interface IProps { userId: string isAdmin: boolean user: supertokensNode.User | undefined + chronikUrls: Record + } export default function Admin ({ user, isAdmin }: IProps): JSX.Element { diff --git a/services/chronikService.ts b/services/chronikService.ts index fe8982d32..a67cc96f3 100644 --- a/services/chronikService.ts +++ b/services/chronikService.ts @@ -159,6 +159,10 @@ export class ChronikBlockchainClient { } } + public getUrls (): string[] { + return this.chronik.proxyInterface().getEndpointArray().map(e => e.url) + } + public setInitialized (): void { this.initializing = false } @@ -725,6 +729,13 @@ class MultiBlockchainClient { } } + public getUrls (): Record { + return { + ecash: this.clients.ecash.getUrls(), + bitcoincash: this.clients.bitcoincash.getUrls() + } + } + private isRunningApp (): boolean { if ( process.env.NEXT_PHASE !== PHASE_PRODUCTION_BUILD && From 92b037ad4833771583d42b83af1ceb3e7be94a4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Tue, 3 Jun 2025 11:14:09 -0300 Subject: [PATCH 08/11] refactor: dedicated component to subscribed addresses --- components/Admin/SubscribedAddresses.tsx | 50 ++++++++++++++++++++++++ pages/admin/index.tsx | 44 ++------------------- 2 files changed, 53 insertions(+), 41 deletions(-) create mode 100644 components/Admin/SubscribedAddresses.tsx diff --git a/components/Admin/SubscribedAddresses.tsx b/components/Admin/SubscribedAddresses.tsx new file mode 100644 index 000000000..27c75bcc4 --- /dev/null +++ b/components/Admin/SubscribedAddresses.tsx @@ -0,0 +1,50 @@ +import { useEffect, useMemo, useState } from 'react' +import Image from 'next/image' +import EyeIcon from 'assets/eye-icon.png' +import TableContainer from 'components/TableContainer/TableContainer' + +export default function SubscribedAddresses (): JSX.Element { + const [ecashSubscribedAddresses, setEcashSubscribedAddresses] = useState([]) + const [bitcoincashSubscribedAddresses, setBitcoincashSubscribedAddresses] = useState([]) + + useEffect(() => { + void (async () => { + const ok = await (await fetch('chronikStatus')).json() + const subscribedEcashAddresses = ok.ecash?.map((value: string) => ({ address: value })) + const subscribedBitcoincashAddresses = ok.bitcoincash?.map((value: string) => ({ address: value })) + setEcashSubscribedAddresses(subscribedEcashAddresses) + setBitcoincashSubscribedAddresses(subscribedBitcoincashAddresses) + })() + }, []) + + const columns = useMemo( + () => [ + { + Header: 'Subscribed addresses', + accessor: 'address', + Cell: (cellProps: any) => { + return
{cellProps.cell.value}
+ } + }, + { + Header: 'View', + accessor: 'view', + Cell: (cellProps: any) => { + return +
+ View on explorer +
+
+ } + } + ], + [] + ) + + return <> +

eCash

+ +

Bitcoin Cash

+ + +} diff --git a/pages/admin/index.tsx b/pages/admin/index.tsx index 84f47d17a..dacfb4684 100644 --- a/pages/admin/index.tsx +++ b/pages/admin/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useMemo } from 'react' +import React, { useEffect, useState } from 'react' import supertokensNode from 'supertokens-node' import * as SuperTokensConfig from '../../config/backendConfig' import Session from 'supertokens-node/recipe/session' @@ -8,9 +8,6 @@ import style from './admin.module.css' import { fetchUserWithSupertokens, isUserAdmin, UserWithSupertokens } from 'services/userService' import { useRouter } from 'next/router' import RegisteredUsers from 'components/Admin/RegisteredUsers' -import TableContainer from '../../components/TableContainer/TableContainer' -import EyeIcon from 'assets/eye-icon.png' -import Image from 'next/image' import { removeUnserializableFields } from 'utils' import { multiBlockchainClient } from 'services/chronikService' import { MainNetworkSlugsType } from 'constants/index' @@ -57,8 +54,6 @@ interface IProps { export default function Admin ({ user, isAdmin }: IProps): JSX.Element { const router = useRouter() - const [ecashSubscribedAddresses, setEcashSubscribedAddresses] = useState([]) - const [bitcoincashSubscribedAddresses, setBitcoincashSubscribedAddresses] = useState([]) const [users, setUsers] = useState([]) useEffect(() => { @@ -69,48 +64,15 @@ export default function Admin ({ user, isAdmin }: IProps): JSX.Element { useEffect(() => { void (async () => { - const ok = await (await fetch('chronikStatus')).json() - const subscribedEcashAddresses = ok.ecash?.map((value: string) => ({ address: value })) - const subscribedBitcoincashAddresses = ok.bitcoincash?.map((value: string) => ({ address: value })) - setEcashSubscribedAddresses(subscribedEcashAddresses) - setBitcoincashSubscribedAddresses(subscribedBitcoincashAddresses) - const ok2 = await (await fetch('/api/users')).json() - setUsers(ok2) + const usersJSON = await (await fetch('/api/users')).json() + setUsers(usersJSON) })() }, []) - const columns = useMemo( - () => [ - { - Header: 'Subscribed addresses', - accessor: 'address', - Cell: (cellProps: any) => { - return
{cellProps.cell.value}
- } - }, - { - Header: 'View', - accessor: 'view', - Cell: (cellProps: any) => { - return -
- View on explorer -
-
- } - } - ], - [] - ) - if (user !== null && isAdmin) { return <>

Admin Dashboard

-

eCash

- -

Bitcoin Cash

- Date: Tue, 3 Jun 2025 12:01:14 -0300 Subject: [PATCH 09/11] feat: add chronik URLs to admin dashboard --- components/Admin/ChronikURLs.tsx | 24 ++++++++++++++++++++++++ components/Admin/RegisteredUsers.tsx | 20 ++++++++++++-------- components/Admin/SubscribedAddresses.tsx | 1 + pages/admin/admin.module.css | 4 ++++ pages/admin/index.tsx | 13 +++++++++---- styles/global.css | 9 +++++++++ 6 files changed, 59 insertions(+), 12 deletions(-) create mode 100644 components/Admin/ChronikURLs.tsx diff --git a/components/Admin/ChronikURLs.tsx b/components/Admin/ChronikURLs.tsx new file mode 100644 index 000000000..fa0c1aa43 --- /dev/null +++ b/components/Admin/ChronikURLs.tsx @@ -0,0 +1,24 @@ +import { MainNetworkSlugsType } from 'constants/index' + +interface IProps { + chronikUrls: Record +} +export default function ChronikURLs ({ chronikUrls }: IProps): JSX.Element { + return <> +

Chronik URLs

+
+
+

eCash

+
    + {chronikUrls.ecash.map((url, idx) =>
  1. {url}
  2. )} +
+
+
+

Bitcoin Cash

+
    + {chronikUrls.bitcoincash.map((url, idx) =>
  1. {url}
  2. )} +
+
+
+ +} diff --git a/components/Admin/RegisteredUsers.tsx b/components/Admin/RegisteredUsers.tsx index 6e3eb7d6f..54dc0c223 100644 --- a/components/Admin/RegisteredUsers.tsx +++ b/components/Admin/RegisteredUsers.tsx @@ -2,26 +2,27 @@ import { UserWithSupertokens } from 'services/userService' import style from './admin.module.css' import moment from 'moment' import TableContainer from '../../components/TableContainer/TableContainer' +import { Cell, CellProps } from 'react-table' interface IProps { users: UserWithSupertokens[] } -export default function RegisteredUsers({ users }: IProps): JSX.Element { +export default function RegisteredUsers ({ users }: IProps): JSX.Element { if (users === undefined) return <> const columns = [ { Header: 'Registered', accessor: 'registered', - Cell: ({ cell }: any) => {cell.value} + Cell: ({ cell }: CellProps) => {cell.value} }, { Header: 'Email', accessor: 'email', - Cell: ({ cell }: any) => ( + Cell: ({ cell }: Cell) => (
@@ -32,18 +33,21 @@ export default function RegisteredUsers({ users }: IProps): JSX.Element { { Header: 'Admin', accessor: 'isAdmin', - Cell: ({ cell }: any) => ( + Cell: ({ cell }: CellProps) => ( cell.value === true ? Yes : 'No' ) } ] - console.log({users}) + console.log({ users }) const data = users.map(user => ({ id: (user.stUser?.id === undefined || user.stUser?.id === '') ? user.userProfile?.id : user.stUser?.id, - registered: user.stUser ? moment(user.stUser.timeJoined).fromNow() : 'NO ST USER FOUND', + registered: (user.stUser != null) ? moment(user.stUser.timeJoined).fromNow() : 'NO ST USER FOUND', email: (user.stUser?.email === undefined || user.stUser?.email === '') ? user.userProfile?.id : user.stUser?.email, isAdmin: user.userProfile?.isAdmin })) - return + return <> +

Registered Users

+ + } diff --git a/components/Admin/SubscribedAddresses.tsx b/components/Admin/SubscribedAddresses.tsx index 27c75bcc4..7d47e1fc3 100644 --- a/components/Admin/SubscribedAddresses.tsx +++ b/components/Admin/SubscribedAddresses.tsx @@ -42,6 +42,7 @@ export default function SubscribedAddresses (): JSX.Element { ) return <> +

Subscribed Addresses

eCash

Bitcoin Cash

diff --git a/pages/admin/admin.module.css b/pages/admin/admin.module.css index b97c2bc45..1c1832546 100644 --- a/pages/admin/admin.module.css +++ b/pages/admin/admin.module.css @@ -21,3 +21,7 @@ border-radius: 5px; max-width: 600px; } + +.divisor { + margin: 3rem; +} diff --git a/pages/admin/index.tsx b/pages/admin/index.tsx index dacfb4684..384eb217b 100644 --- a/pages/admin/index.tsx +++ b/pages/admin/index.tsx @@ -11,6 +11,8 @@ import RegisteredUsers from 'components/Admin/RegisteredUsers' import { removeUnserializableFields } from 'utils' import { multiBlockchainClient } from 'services/chronikService' import { MainNetworkSlugsType } from 'constants/index' +import SubscribedAddresses from 'components/Admin/SubscribedAddresses' +import ChronikURLs from 'components/Admin/ChronikURLs' export const getServerSideProps: GetServerSideProps = async (context) => { // this runs on the backend, so we must call init on supertokens-node SDK @@ -52,7 +54,7 @@ interface IProps { } -export default function Admin ({ user, isAdmin }: IProps): JSX.Element { +export default function Admin ({ user, isAdmin, chronikUrls }: IProps): JSX.Element { const router = useRouter() const [users, setUsers] = useState([]) @@ -71,8 +73,8 @@ export default function Admin ({ user, isAdmin }: IProps): JSX.Element { if (user !== null && isAdmin) { return <> -

Admin Dashboard

+ +
+ +
+ } else { return diff --git a/styles/global.css b/styles/global.css index 4ed7ce91e..b5012b77f 100644 --- a/styles/global.css +++ b/styles/global.css @@ -470,3 +470,12 @@ body[data-theme='dark'] .button_outline:hover { .export_btn:hover { cursor: pointer; } + +.columns { + display: flex; +} + +.paybutton-table-ctn.columns > div { + flex: 1; +} + From fd6d6350c33f7e338f3e747e4f0f58fafb96b35e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Tue, 3 Jun 2025 13:16:23 -0300 Subject: [PATCH 10/11] fix: consistent heading hierarchy --- components/Admin/ChronikURLs.tsx | 2 +- components/Admin/RegisteredUsers.tsx | 2 +- components/Admin/SubscribedAddresses.tsx | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/components/Admin/ChronikURLs.tsx b/components/Admin/ChronikURLs.tsx index fa0c1aa43..c474461b6 100644 --- a/components/Admin/ChronikURLs.tsx +++ b/components/Admin/ChronikURLs.tsx @@ -5,7 +5,7 @@ interface IProps { } export default function ChronikURLs ({ chronikUrls }: IProps): JSX.Element { return <> -

Chronik URLs

+

Chronik URLs

eCash

diff --git a/components/Admin/RegisteredUsers.tsx b/components/Admin/RegisteredUsers.tsx index 54dc0c223..0e1c5b9fa 100644 --- a/components/Admin/RegisteredUsers.tsx +++ b/components/Admin/RegisteredUsers.tsx @@ -47,7 +47,7 @@ export default function RegisteredUsers ({ users }: IProps): JSX.Element { })) return <> -

Registered Users

+

Registered Users

} diff --git a/components/Admin/SubscribedAddresses.tsx b/components/Admin/SubscribedAddresses.tsx index 7d47e1fc3..803799a9c 100644 --- a/components/Admin/SubscribedAddresses.tsx +++ b/components/Admin/SubscribedAddresses.tsx @@ -42,10 +42,10 @@ export default function SubscribedAddresses (): JSX.Element { ) return <> -

Subscribed Addresses

-

eCash

+

Subscribed Addresses

+

eCash

-

Bitcoin Cash

+

Bitcoin Cash

} From a51ab12beaaf2a073427ee81b5410192881f52e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Tue, 3 Jun 2025 20:30:51 -0300 Subject: [PATCH 11/11] chore: org -> io --- config/example-config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/example-config.json b/config/example-config.json index 09719a021..f77182ff3 100644 --- a/config/example-config.json +++ b/config/example-config.json @@ -13,7 +13,7 @@ }, "networkBlockchainURLs": { "ecash": [ - "https://xec.paybutton.org", + "https://xec.paybutton.io", "https://chronik1.alitayin.com", "https://chronik2.alitayin.com", "https://chronik.e.cash",