feat: improve payments

This commit is contained in:
nate 2026-03-24 21:17:50 +04:00
parent 371eeffd38
commit 7b8f693710
4 changed files with 19 additions and 7 deletions

View File

@ -22,7 +22,7 @@ export async function migrate() {
coin TEXT NOT NULL, coin TEXT NOT NULL,
amount_crypto TEXT NOT NULL, amount_crypto TEXT NOT NULL,
address TEXT NOT NULL, address TEXT NOT NULL,
derivation_index INTEGER NOT NULL UNIQUE, derivation_index INTEGER NOT NULL,
status TEXT NOT NULL DEFAULT 'pending', status TEXT NOT NULL DEFAULT 'pending',
created_at TIMESTAMPTZ DEFAULT now(), created_at TIMESTAMPTZ DEFAULT now(),
paid_at TIMESTAMPTZ, paid_at TIMESTAMPTZ,
@ -52,5 +52,9 @@ export async function migrate() {
await sql`ALTER TABLE payments ADD COLUMN IF NOT EXISTS receipt_html TEXT`; await sql`ALTER TABLE payments ADD COLUMN IF NOT EXISTS receipt_html TEXT`;
// Derivation index should be unique per coin, not globally
await sql`DROP INDEX IF EXISTS payments_derivation_index_key`;
await sql`CREATE UNIQUE INDEX IF NOT EXISTS idx_payments_coin_derivation ON payments(coin, derivation_index)`;
console.log("Pay DB ready"); console.log("Pay DB ready");
} }

View File

@ -37,7 +37,15 @@ async function refreshMaps() {
// ── Core logic: one place for all state transitions ───────────────── // ── Core logic: one place for all state transitions ─────────────────
async function recordTx(paymentId: number, txid: string, amount: number, confirmed: boolean) { async function recordTx(paymentId: number, address: string, txid: string, amount: number, confirmed: boolean) {
// Verify the payment exists and the address matches — prevents stale in-memory state
// from attributing transactions to the wrong payment
const [payment] = await sql`
SELECT id FROM payments WHERE id = ${paymentId} AND address = ${address}
AND status IN ('pending', 'underpaid', 'confirming')
`;
if (!payment) return;
const [ins] = await sql` const [ins] = await sql`
INSERT INTO payment_txs (payment_id, txid, amount, confirmed) INSERT INTO payment_txs (payment_id, txid, amount, confirmed)
VALUES (${paymentId}, ${txid}, ${amount.toFixed(8)}, ${confirmed}) VALUES (${paymentId}, ${txid}, ${amount.toFixed(8)}, ${confirmed})
@ -111,7 +119,7 @@ async function handleTxEvent(event: any) {
if (txValue <= 0) continue; if (txValue <= 0) continue;
console.log(`SSE: tx ${txHash} for payment ${payment.id}: +${txValue} ${payment.coin}`); console.log(`SSE: tx ${txHash} for payment ${payment.id}: +${txValue} ${payment.coin}`);
await recordTx(payment.id, txHash, txValue, false); await recordTx(payment.id, payment.address, txHash, txValue, false);
txidToPayment.set(txHash, payment.id); txidToPayment.set(txHash, payment.id);
await evaluatePayment(payment.id); await evaluatePayment(payment.id);
return; return;
@ -197,7 +205,7 @@ export async function checkPayments() {
// Sync txs from address API // Sync txs from address API
for (const tx of info.in ?? []) { for (const tx of info.in ?? []) {
if (!tx.txid) continue; if (!tx.txid) continue;
await recordTx(payment.id, tx.txid, Number(tx.amount ?? 0), tx.block != null); await recordTx(payment.id, payment.address, tx.txid, Number(tx.amount ?? 0), tx.block != null);
} }
await evaluatePayment(payment.id); await evaluatePayment(payment.id);
} catch (e) { } catch (e) {

View File

@ -89,9 +89,9 @@ export const routes = new Elysia()
// Crypto amount with 8 decimal precision // Crypto amount with 8 decimal precision
const amountCrypto = (amountUsd / rate).toFixed(8); const amountCrypto = (amountUsd / rate).toFixed(8);
// Get next derivation index // Get next derivation index for this coin
const [{ next_index }] = await sql` const [{ next_index }] = await sql`
SELECT COALESCE(MAX(derivation_index), -1) + 1 as next_index FROM payments SELECT COALESCE(MAX(derivation_index), -1) + 1 as next_index FROM payments WHERE coin = ${coin}
`; `;
// Derive address // Derive address

View File

@ -29,7 +29,7 @@ nuke_db() {
fi fi
echo "[nuke-db] Dropping all tables on database-eu-central..." echo "[nuke-db] Dropping all tables on database-eu-central..."
$SSH $DB_HOST bash << 'REMOTE' $SSH $DB_HOST bash << 'REMOTE'
sudo -u postgres psql -d pingql -c "DROP TABLE IF EXISTS payments, pings, api_keys, monitors, accounts CASCADE;" sudo -u postgres psql -d pingql -c "DROP TABLE IF EXISTS payment_txs, ping_bodies, payments, pings, api_keys, monitors, accounts CASCADE;"
echo "All tables dropped" echo "All tables dropped"
REMOTE REMOTE
echo "[nuke-db] Done. Tables will be recreated on next API/web restart." echo "[nuke-db] Done. Tables will be recreated on next API/web restart."