Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
3ebc4a0
fix: don't sed dump
chedieck Jun 1, 2026
03e107c
chore: improve testing from dump
chedieck Jun 1, 2026
c275f8e
chore: improve testing from dump 2
chedieck Jun 1, 2026
054ccda
fix: dev-from-dump setup (named volume, prisma deploy, pv progress)
chedieck Jun 2, 2026
a7c0814
feat: SKIP_CACHE_REBUILD to prevent I/O saturation with prod data
chedieck Jun 2, 2026
18879a6
chore: ignore .forever
chedieck Jun 2, 2026
25b5016
fix: skip txs with missing prices instead of failing entire batch
chedieck May 29, 2026
8ce9149
fix: resilient chronik WebSocket with retry + bump sync concurrency t…
chedieck May 29, 2026
6fdb0f6
fix: undo 16 -> 64 bump
chedieck Jun 1, 2026
b72ce29
fix: throttle TX_CONFIRMED handler to max 6 concurrent (prevents DB c…
chedieck Jun 1, 2026
72333f3
perf: non-blocking payment cache rebuild with background backfill and…
chedieck Jun 1, 2026
4a93f89
fix: non-blocking cache rebuild with progressive UX
chedieck Jun 1, 2026
5bdf53f
perf: fix payments page blocking on cache rebuild
chedieck Jun 2, 2026
59373c9
perf: fix page loads blocking on cache rebuild I/O saturation
chedieck Jun 2, 2026
a4b6f5c
fix: revert backgroundPrisma - was starving websocket handlers
chedieck Jun 2, 2026
5392e6c
chore: remove changes from other PR
chedieck Jun 2, 2026
68ad97b
fix: revert 30s TTL cache during rebuild - don't cache partial data
chedieck Jun 2, 2026
3b198d0
test: update tests for generateAddressPaymentInfo aggregate refactor
chedieck Jun 2, 2026
678f9f4
test: expect cacheRebuilding in dashboard response during integration…
chedieck Jun 2, 2026
f83885d
refactor: extract WS retry constants (CHRONIK_WS_MAX_RETRIES, CHRONIK…
chedieck Jun 2, 2026
ff1a38f
Merge branch 'master' into fix/tx-prices-linking
chedieck Jun 2, 2026
e5eafb3
refactor: rename chronik retry constants to CHRONIK_HTTP_* and CHRONI…
chedieck Jun 2, 2026
3d9c4bd
fix: constants names
chedieck Jun 8, 2026
c77cd75
fix: resolve merge conflicts in paymentCache.ts (keep background rebu…
chedieck Jun 22, 2026
2e78112
fix: optmize search iterations
chedieck Jun 30, 2026
89d64e0
fix: deduplicate cache rebuild
chedieck Jun 22, 2026
1231945
fix: retry -> attempt
chedieck Jun 23, 2026
c616877
fix: do not block chronik connection end
chedieck Jun 30, 2026
4f38145
refactor: bump max confirmed txs to process from 6 to 20
chedieck Jun 30, 2026
f076ec9
fix: improve comment
chedieck Jun 30, 2026
9799f62
Merge branch 'master' into fix/tx-prices-linking
Klakurka Jul 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.from-dump
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
IGNORE_SEEDING=true
DONT_EXECUTE_TRIGGERS=true
FROM_DUMP=true
SKIP_CACHE_REBUILD=true
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,4 @@ prisma-local/seeds/productionTxs.csv
paybutton-config.json

dump.sql*
.forever
12 changes: 12 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ dev-from-dump:
$(touch_local_env)
docker compose -f docker-compose-from-dump.yml --env-file .env --env-file .env.local --env-file .env.from-dump up --build -d

stop-dev-from-dump:
docker compose -f docker-compose-from-dump.yml --env-file .env --env-file .env.local --env-file .env.from-dump down

reset-dev-from-dump:
make stop-dev-from-dump && make dev-from-dump

reset-dev-from-dump-keep-db:
docker compose -f docker-compose-from-dump.yml --env-file .env --env-file .env.local --env-file .env.from-dump up --build -d --force-recreate --no-deps paybutton

nuke-dev-from-dump:
docker compose -f docker-compose-from-dump.yml --env-file .env --env-file .env.local --env-file .env.from-dump down -v

logs-dev:
docker logs -f paybutton-dev

Expand Down
4 changes: 4 additions & 0 deletions docker-compose-from-dump.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ services:
- ./scripts/db/init-supertokens-db.sql:/home/mysql/raw_entrypoint/3-init-supertokens-db.sql
- ./scripts/db/prisma-shadow-db-fix.sql:/home/mysql/raw_entrypoint/4-prisma-shadow-db-fix.sql
- ./dump.sql:/home/mysql/raw_entrypoint/5-dump.sql
- paybutton-dump-db-data:/var/lib/mysql

users-service:
container_name: paybutton-users-service
Expand Down Expand Up @@ -69,3 +70,6 @@ services:
- ./redis:/data/redis:z
- ./scripts:/data/scripts
command: sh -c "sed -i 's/\\r//g' ./scripts/redis-start.sh && sh ./scripts/redis-start.sh"

volumes:
paybutton-dump-db-data:
25 changes: 17 additions & 8 deletions redis/paymentCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
import { fetchAllUserAddresses, AddressPaymentInfo } from 'services/addressService'
import { fetchPaybuttonArrayByUserId } from 'services/paybuttonService'

import { RESPONSE_MESSAGES, PAYMENT_WEEK_KEY_FORMAT, KeyValueT } from 'constants/index'
import { RESPONSE_MESSAGES, PAYMENT_WEEK_KEY_FORMAT, KeyValueT, N_OF_QUOTES } from 'constants/index'
import moment from 'moment-timezone'
import { CacheSet } from 'redis/index'
import { ButtonDisplayData, Payment } from './types'
Expand All @@ -31,9 +31,11 @@ export async function * getUserUncachedAddresses (userId: string): AsyncGenerato
}

export const getPaymentList = async (userId: string): Promise<Payment[]> => {
const uncachedAddressStream = getUserUncachedAddresses(userId)
for await (const address of uncachedAddressStream) {
void await CacheSet.addressCreation(address)
if (process.env.SKIP_CACHE_REBUILD === undefined) {
const uncachedAddressStream = getUserUncachedAddresses(userId)
for await (const address of uncachedAddressStream) {
void await CacheSet.addressCreation(address)
}
}
return await getCachedPaymentsForUser(userId)
}
Expand Down Expand Up @@ -153,6 +155,10 @@ export const generateAndCacheGroupedPaymentsAndInfoForAddress = async (address:
for (const tx of batch) {
balance = balance.plus(tx.amount)
if (tx.amount.gt(0)) {
if (tx.prices.length !== N_OF_QUOTES) {
console.warn(`[CACHE] Skipping tx ${tx.hash} — missing price links (${tx.prices.length}/${N_OF_QUOTES})`)
continue
}
const payment = generatePaymentFromTx(tx)
paymentList.push(payment)
paymentCount++
Expand Down Expand Up @@ -304,6 +310,7 @@ export const clearPaymentCacheForAddress = async (addressString: string): Promis
}

export const initPaymentCache = async (address: Address): Promise<boolean> => {
if (process.env.SKIP_CACHE_REBUILD !== undefined) return false
const cachedKeys = await getCachedWeekKeysForAddress(address.address)
if (cachedKeys.length === 0) {
await CacheSet.addressCreation(address)
Expand All @@ -313,10 +320,12 @@ export const initPaymentCache = async (address: Address): Promise<boolean> => {
}

export async function * getPaymentStream (userId: string): AsyncGenerator<Payment> {
const uncachedAddressStream = getUserUncachedAddresses(userId)
for await (const address of uncachedAddressStream) {
console.log('[CACHE]: Creating cache for address', address.address)
await CacheSet.addressCreation(address)
if (process.env.SKIP_CACHE_REBUILD === undefined) {
const uncachedAddressStream = getUserUncachedAddresses(userId)
for await (const address of uncachedAddressStream) {
console.log('[CACHE]: Creating cache for address', address.address)
await CacheSet.addressCreation(address)
}
}
const userButtonIds: string[] = (await fetchPaybuttonArrayByUserId(userId))
.map(p => p.id)
Expand Down
18 changes: 16 additions & 2 deletions scripts/db-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,25 @@ sed_vars () {
cd /home/mysql/raw_entrypoint || exit

for file in *.sql; do
sed_vars "$file"
if [[ "$file" == *dump* ]]; then
echo "Linking dump file: $file"
ln -s /home/mysql/raw_entrypoint/"$file" /home/mysql/entrypoint/"$file"
else
echo "Copying file $file after seding vars"
sed_vars "$file"
fi
done

cd ../entrypoint/ || exit

for file in *.sql; do
mariadb -u root -p"$MYSQL_ROOT_PASSWORD" < "$file"
if [[ "$file" == *dump* ]]; then
filesize=$(stat -Lc%s "$file" 2>/dev/null || stat -Lf%z "$file" 2>/dev/null)
echo "Importing $file ($(numfmt --to=iec $filesize)) ..."
pv "$file" | mariadb -u root -p"$MYSQL_ROOT_PASSWORD"
else
echo "Importing $file ..."
mariadb -u root -p"$MYSQL_ROOT_PASSWORD" < "$file"
fi
echo "Done importing $file"
done
2 changes: 1 addition & 1 deletion scripts/db/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ FROM mariadb:latest
RUN mkhomedir_helper mysql

RUN apt-get update || echo && \
apt-get install -y gettext || echo
apt-get install -y gettext pv || echo

RUN mkdir -p /home/mysql/raw_entrypoint && \
mkdir -p /home/mysql/entrypoint
Expand Down
14 changes: 8 additions & 6 deletions scripts/paybutton-server-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,19 @@ logtime=$(date +%Y-%m-%d@%H:%M)
[ -e logs/jobs.log ] && mv logs/jobs.log logs/history/jobs_"$logtime".log
[ -e logs/ws-server.log ] && mv logs/ws-server.log logs/history/ws-server_"$logtime".log

if [ "$ENVIRONMENT" = "production" ]; then
if [ "$FROM_DUMP" = "true" ] || [ "$ENVIRONMENT" = "production" ]; then
yarn prisma migrate deploy || exit 1
yarn prisma generate || exit 1
pm2 start yarn --time --interpreter ash --name jobs --output logs/jobs.log --error logs/jobs.log -- initJobs || exit 1
pm2 start yarn --time --interpreter ash --name WSServer --output logs/ws-server.log --error logs/ws-server.log -- initWSServer || exit 1
pm2 start yarn --time --interpreter ash --name next --output logs/next.log --error logs/next.log -- prod || exit 1
else
yarn prisma migrate dev || exit 1
yarn prisma db seed || exit 1
pm2 start yarn --time --interpreter ash --name jobs --output logs/jobs.log --error logs/jobs.log -- initJobs || exit 1
pm2 start yarn --time --interpreter ash --name WSServer --output logs/ws-server.log --error logs/ws-server.log -- initWSServer || exit 1
fi

pm2 start yarn --time --interpreter ash --name jobs --output logs/jobs.log --error logs/jobs.log -- initJobs || exit 1
pm2 start yarn --time --interpreter ash --name WSServer --output logs/ws-server.log --error logs/ws-server.log -- initWSServer || exit 1
if [ "$ENVIRONMENT" = "production" ]; then
pm2 start yarn --time --interpreter ash --name next --output logs/next.log --error logs/next.log -- prod || exit 1
else
pm2 start yarn --time --interpreter ash --name next --output logs/next.log --error logs/next.log -- dev || exit 1
fi
pm2 logs next
28 changes: 21 additions & 7 deletions services/transactionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -519,21 +519,22 @@ export async function connectTransactionsListToPrices (
grouped.set(p.timestamp, g)
}

// Throw on missing price pairs
for (const ts of tsArray) {
const allPrices = grouped.get(ts)
const formattedDate = moment.unix(ts).format(HUMAN_READABLE_DATE_FORMAT)

if (allPrices == null) {
throw new Error(
`[PRICES] No price record found for networkId=${networkId} at ${formattedDate}.`
console.warn(
`[PRICES] No price record found for networkId=${networkId} at ${formattedDate} — skipping affected txs.`
)
continue
}

if ((allPrices.cad == null) || (allPrices.usd == null)) {
throw new Error(
`[PRICES] Incomplete price data for networkId=${networkId} at ${formattedDate}. Partial data: ${JSON.stringify(allPrices, null, 2)}`
console.warn(
`[PRICES] Incomplete price data for networkId=${networkId} at ${formattedDate} — skipping affected txs.`
)
continue
}

priceByNetworkTs.set(`${networkId}:${ts}`, allPrices as AllPrices)
Expand All @@ -544,19 +545,32 @@ export async function connectTransactionsListToPrices (

// Build all join rows (2 per tx: USD + CAD)
const rows: Prisma.PricesOnTransactionsCreateManyInput[] = []
const skippedTxIds: string[] = []
for (const t of txList) {
const ts = flattenTimestamp(t.timestamp)
const allPrices = priceByNetworkTs.get(`${t.address.networkId}:${ts}`)
if (allPrices == null) {
throw new Error(`[PRICES] Missing price pair for networkId ${t.address.networkId} at ${moment.unix(ts).format(HUMAN_READABLE_DATE_FORMAT)}.`)
skippedTxIds.push(t.id)
continue
}
rows.push(...buildPriceTxConnectionInput(t, allPrices))
}

if (skippedTxIds.length > 0) {
console.warn(`[PRICES] Skipped ${skippedTxIds.length} txs due to missing price data.`)
}

const txIdsToConnect = txList.filter(t => !skippedTxIds.includes(t.id)).map(t => t.id)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very inefficient, you can build txIdsToConnect on the fly (instead of the skipped ones) and check if txList.length > txIdsToConnect.length to determine if there is any skipped tx. This avoids the O(n²) search and map building + saves some memory from the temporary filter vector.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, fixed.

if (txIdsToConnect.length === 0) {
console.warn('[PRICES] No txs to connect after filtering — all had missing prices.')
return
}

console.log(`[PRICES] Built ${rows.length} price links (2 per tx).`)

await prisma.$transaction(
async (tx) => {
await deletePriceTxConnectionsInChunks(tx, txList.map((t) => t.id))
await deletePriceTxConnectionsInChunks(tx, txIdsToConnect)
await createPriceTxConnectionInChunks(tx, rows)
},
{ timeout: PRICES_CONNECTION_TIMEOUT }
Expand Down