From 3498f8424072e1402c57919c79893f0ac30f0a52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Thu, 7 May 2026 15:02:16 -0300 Subject: [PATCH 1/5] fix: server down due to infinite pagination bug --- services/chronikService.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/services/chronikService.ts b/services/chronikService.ts index b3e96938..a0661801 100644 --- a/services/chronikService.ts +++ b/services/chronikService.ts @@ -714,6 +714,7 @@ export class ChronikBlockchainClient { } } } else if (msg.msgType === 'TX_CONFIRMED') { + if (this.isAlreadyBeingProcessed(msg.txid, true)) return try { const transaction = await this.fetchTxWithRetry(msg.txid) const addressesWithTransactions = await this.getAddressesForTransaction(transaction) @@ -803,8 +804,9 @@ export class ChronikBlockchainClient { const pageSize = 200 let blockPageTxs = (await this.chronik.blockTxs(blockHash, page, pageSize)).txs let blockTxsToSync: Tx[] = [] - while (blockPageTxs.length > 0 && blockTxsToSync.length !== this.confirmedTxsHashesFromLastBlock.length) { - const thisBlockTxsToSync = blockPageTxs.filter(tx => this.confirmedTxsHashesFromLastBlock.includes(tx.txid)) + const confirmedTxHashes = new Set(this.confirmedTxsHashesFromLastBlock) + while (blockPageTxs.length > 0 && blockTxsToSync.length < confirmedTxHashes.size) { + const thisBlockTxsToSync = blockPageTxs.filter(tx => confirmedTxHashes.has(tx.txid)) blockTxsToSync = [...blockTxsToSync, ...thisBlockTxsToSync] page += 1 blockPageTxs = (await this.chronik.blockTxs(blockHash, page, pageSize)).txs From b9c072fbecd85610d1dfae9a821bf26d06e84c95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Thu, 7 May 2026 15:19:08 -0300 Subject: [PATCH 2/5] fix: handle error case when fetching confirmed tx --- services/chronikService.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/chronikService.ts b/services/chronikService.ts index a0661801..97738e17 100644 --- a/services/chronikService.ts +++ b/services/chronikService.ts @@ -732,6 +732,8 @@ export class ChronikBlockchainClient { await markTransactionsOrphaned(msg.txid) } else { console.error(`${this.CHRONIK_MSG_PREFIX}: confirmed tx handler failed for ${msg.txid}`, e) + const { [msg.txid]: _, ...rest } = this.lastProcessedMessages.confirmed + this.lastProcessedMessages.confirmed = rest } } } else if (msg.msgType === 'TX_ADDED_TO_MEMPOOL') { From f286514e289df0a5e53ac14eabeb9edd4a29f233 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Fri, 8 May 2026 01:23:38 -0300 Subject: [PATCH 3/5] fix: detect and fill internal price gaps on startup sync --- services/priceService.ts | 67 +++++++++++++++++++++++++++------------- 1 file changed, 46 insertions(+), 21 deletions(-) diff --git a/services/priceService.ts b/services/priceService.ts index b015121c..b0403e52 100644 --- a/services/priceService.ts +++ b/services/priceService.ts @@ -2,7 +2,7 @@ import axios from 'axios' import { Prisma, Price } from '@prisma/client' import config from 'config' import prisma from 'prisma-local/clientInstance' -import { PRICE_API_TIMEOUT, PRICE_API_MAX_RETRIES, PRICE_API_DATE_FORMAT, RESPONSE_MESSAGES, NETWORK_TICKERS, XEC_NETWORK_ID, BCH_NETWORK_ID, USD_QUOTE_ID, CAD_QUOTE_ID, N_OF_QUOTES, HUMAN_READABLE_DATE_FORMAT } from 'constants/index' +import { PRICE_API_TIMEOUT, PRICE_API_MAX_RETRIES, PRICE_API_DATE_FORMAT, RESPONSE_MESSAGES, NETWORK_TICKERS, XEC_NETWORK_ID, BCH_NETWORK_ID, USD_QUOTE_ID, CAD_QUOTE_ID, N_OF_QUOTES } from 'constants/index' import { validatePriceAPIUrlAndToken, validateNetworkTicker } from 'utils/validators' import moment from 'moment' @@ -154,44 +154,69 @@ export async function getAllPricesByNetworkTicker ( } export async function syncPastDaysNewerPrices (): Promise { - console.log('[PRICES] Syncing prices...') - const lastPrice = await prisma.price.findFirst({ - orderBy: [{ timestamp: 'desc' }], - select: { timestamp: true } + console.log('[PRICES] Syncing missing prices (gap-aware)...') + + const today = moment.utc().startOf('day') + const windowStart = moment.utc().subtract(365, 'days').startOf('day') + + const existingPrices = await prisma.price.findMany({ + where: { + timestamp: { + gte: windowStart.unix(), + lte: today.unix() + }, + quoteId: USD_QUOTE_ID + }, + select: { timestamp: true, networkId: true } }) - if (lastPrice === null) throw new Error('No prices found, initial database seed did not complete successfully') - const lastDateInDB = moment.unix(lastPrice.timestamp) - const date = moment().startOf('day') - const daysToRetrieve: string[] = [] + const xecTimestamps = new Set( + existingPrices.filter(p => p.networkId === XEC_NETWORK_ID).map(p => p.timestamp) + ) + const bchTimestamps = new Set( + existingPrices.filter(p => p.networkId === BCH_NETWORK_ID).map(p => p.timestamp) + ) + + const expectedDays: Array<{ formatted: string, timestamp: number }> = [] + const cursor = today.clone() + while (cursor.isSameOrAfter(windowStart)) { + expectedDays.push({ formatted: cursor.format(PRICE_API_DATE_FORMAT), timestamp: cursor.unix() }) + cursor.subtract(1, 'day') + } + + const missingXECDays = expectedDays.filter(d => !xecTimestamps.has(d.timestamp)) + const missingBCHDays = expectedDays.filter(d => !bchTimestamps.has(d.timestamp)) - console.log(`[PRICES] Last price found is for ${lastDateInDB.format(HUMAN_READABLE_DATE_FORMAT)}.`) - while (date.isAfter(lastDateInDB)) { - daysToRetrieve.push(date.format(PRICE_API_DATE_FORMAT)) - date.add(-1, 'day') + const totalMissing = missingXECDays.length + missingBCHDays.length + if (totalMissing === 0) { + console.log('[PRICES] No missing prices found in the last 365 days.') + return } - console.log(`[PRICES] Will try to retrieve ${daysToRetrieve.length} prices.`) - const allXECPrices = await getAllPricesByNetworkTicker(NETWORK_TICKERS.ecash, false) - const allBCHPrices = await getAllPricesByNetworkTicker(NETWORK_TICKERS.bitcoincash, false) + console.log(`[PRICES] Found ${missingXECDays.length} missing XEC days and ${missingBCHDays.length} missing BCH days. Fetching from API...`) + + const allXECPrices = missingXECDays.length > 0 ? await getAllPricesByNetworkTicker(NETWORK_TICKERS.ecash, false) : null + const allBCHPrices = missingBCHDays.length > 0 ? await getAllPricesByNetworkTicker(NETWORK_TICKERS.bitcoincash, false) : null if (allXECPrices !== null) { + const missingDaySet = new Set(missingXECDays.map(d => d.formatted)) await Promise.all( allXECPrices - .filter(p => daysToRetrieve.includes(p.day)) - .map(async price => await upsertPricesForNetworkId(price, XEC_NETWORK_ID, moment(price.day).unix())) + .filter(p => missingDaySet.has(p.day)) + .map(async price => await upsertPricesForNetworkId(price, XEC_NETWORK_ID, moment.utc(price.day).unix())) ) } if (allBCHPrices !== null) { + const missingDaySet = new Set(missingBCHDays.map(d => d.formatted)) await Promise.all( allBCHPrices - .filter(p => daysToRetrieve.includes(p.day)) - .map(async price => await upsertPricesForNetworkId(price, BCH_NETWORK_ID, moment(price.day).unix())) + .filter(p => missingDaySet.has(p.day)) + .map(async price => await upsertPricesForNetworkId(price, BCH_NETWORK_ID, moment.utc(price.day).unix())) ) } - console.log('[PRICES] All past prices have been synced.') + console.log('[PRICES] All missing prices have been synced.') } export async function syncCurrentPrices (): Promise { From ca8e93cf8746520d172d5a8bd1bcb37c736d220d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Fri, 8 May 2026 01:26:21 -0300 Subject: [PATCH 4/5] fix: hardcoded constant --- constants/index.ts | 3 +++ services/priceService.ts | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/constants/index.ts b/constants/index.ts index d835c275..42e3d553 100644 --- a/constants/index.ts +++ b/constants/index.ts @@ -295,3 +295,6 @@ export const CLIENT_PAYMENT_EXPIRATION_TIME = (7) * (24 * 60 * 60 * 1000) // (nu // Enough for eCash IFP when created export const MAX_TXS_PER_ADDRESS = 250000 + +// Will look for this many days before to check if there are gaps in prices +export const N_DAYS_LOOK_FOR_PRICE_GAPS = 30 diff --git a/services/priceService.ts b/services/priceService.ts index b0403e52..29efbce1 100644 --- a/services/priceService.ts +++ b/services/priceService.ts @@ -2,7 +2,7 @@ import axios from 'axios' import { Prisma, Price } from '@prisma/client' import config from 'config' import prisma from 'prisma-local/clientInstance' -import { PRICE_API_TIMEOUT, PRICE_API_MAX_RETRIES, PRICE_API_DATE_FORMAT, RESPONSE_MESSAGES, NETWORK_TICKERS, XEC_NETWORK_ID, BCH_NETWORK_ID, USD_QUOTE_ID, CAD_QUOTE_ID, N_OF_QUOTES } from 'constants/index' +import { PRICE_API_TIMEOUT, PRICE_API_MAX_RETRIES, PRICE_API_DATE_FORMAT, RESPONSE_MESSAGES, NETWORK_TICKERS, XEC_NETWORK_ID, BCH_NETWORK_ID, USD_QUOTE_ID, CAD_QUOTE_ID, N_OF_QUOTES, N_DAYS_LOOK_FOR_PRICE_GAPS } from 'constants/index' import { validatePriceAPIUrlAndToken, validateNetworkTicker } from 'utils/validators' import moment from 'moment' @@ -154,10 +154,10 @@ export async function getAllPricesByNetworkTicker ( } export async function syncPastDaysNewerPrices (): Promise { - console.log('[PRICES] Syncing missing prices (gap-aware)...') + console.log(`[PRICES] Syncing missing prices, including gaps on the last ${N_DAYS_LOOK_FOR_PRICE_GAPS} days...`) const today = moment.utc().startOf('day') - const windowStart = moment.utc().subtract(365, 'days').startOf('day') + const windowStart = moment.utc().subtract(N_DAYS_LOOK_FOR_PRICE_GAPS, 'days').startOf('day') const existingPrices = await prisma.price.findMany({ where: { @@ -189,7 +189,7 @@ export async function syncPastDaysNewerPrices (): Promise { const totalMissing = missingXECDays.length + missingBCHDays.length if (totalMissing === 0) { - console.log('[PRICES] No missing prices found in the last 365 days.') + console.log(`[PRICES] No missing prices found in the last ${N_DAYS_LOOK_FOR_PRICE_GAPS} days.`) return } From ca970eb74e95d8da4255b850a094a978c91a6468 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o?= Date: Fri, 8 May 2026 01:45:05 -0300 Subject: [PATCH 5/5] fix: fallback to per-day API for dates missing from bulk endpoint --- services/priceService.ts | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/services/priceService.ts b/services/priceService.ts index 29efbce1..f9cf319d 100644 --- a/services/priceService.ts +++ b/services/priceService.ts @@ -195,9 +195,14 @@ export async function syncPastDaysNewerPrices (): Promise { console.log(`[PRICES] Found ${missingXECDays.length} missing XEC days and ${missingBCHDays.length} missing BCH days. Fetching from API...`) + const failedDays: string[] = [] + const allXECPrices = missingXECDays.length > 0 ? await getAllPricesByNetworkTicker(NETWORK_TICKERS.ecash, false) : null const allBCHPrices = missingBCHDays.length > 0 ? await getAllPricesByNetworkTicker(NETWORK_TICKERS.bitcoincash, false) : null + const xecBulkDays = new Set(allXECPrices?.map(p => p.day) ?? []) + const bchBulkDays = new Set(allBCHPrices?.map(p => p.day) ?? []) + if (allXECPrices !== null) { const missingDaySet = new Set(missingXECDays.map(d => d.formatted)) await Promise.all( @@ -216,7 +221,38 @@ export async function syncPastDaysNewerPrices (): Promise { ) } - console.log('[PRICES] All missing prices have been synced.') + const xecStillMissing = missingXECDays.filter(d => !xecBulkDays.has(d.formatted)) + const bchStillMissing = missingBCHDays.filter(d => !bchBulkDays.has(d.formatted)) + + for (const day of xecStillMissing) { + const price = await withRetries( + async () => await getPriceForDayAndNetworkTicker(moment.utc(day.formatted), NETWORK_TICKERS.ecash), + { throwOnFailure: false, context: { day: day.formatted, network: 'XEC' } } + ) + if (price !== null) { + await upsertPricesForNetworkId(price, XEC_NETWORK_ID, day.timestamp) + } else { + failedDays.push(`XEC ${day.formatted}`) + } + } + + for (const day of bchStillMissing) { + const price = await withRetries( + async () => await getPriceForDayAndNetworkTicker(moment.utc(day.formatted), NETWORK_TICKERS.bitcoincash), + { throwOnFailure: false, context: { day: day.formatted, network: 'BCH' } } + ) + if (price !== null) { + await upsertPricesForNetworkId(price, BCH_NETWORK_ID, day.timestamp) + } else { + failedDays.push(`BCH ${day.formatted}`) + } + } + + if (failedDays.length > 0) { + console.warn(`[PRICES] Could not fetch prices for: ${failedDays.join(', ')}`) + } else { + console.log('[PRICES] All missing prices have been synced.') + } } export async function syncCurrentPrices (): Promise {