update: .......
This commit is contained in:
parent
955b26f942
commit
e62b60e0fd
|
|
@ -33,6 +33,20 @@ export async function migrate() {
|
|||
|
||||
await sql`ALTER TABLE payments ADD COLUMN IF NOT EXISTS amount_received TEXT NOT NULL DEFAULT '0'`;
|
||||
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS payment_txs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
payment_id BIGINT NOT NULL REFERENCES payments(id) ON DELETE CASCADE,
|
||||
txid TEXT NOT NULL,
|
||||
amount TEXT NOT NULL,
|
||||
confirmed BOOLEAN NOT NULL DEFAULT false,
|
||||
detected_at TIMESTAMPTZ DEFAULT now(),
|
||||
UNIQUE(payment_id, txid)
|
||||
)
|
||||
`;
|
||||
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_payment_txs_payment ON payment_txs(payment_id)`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_payment_txs_txid ON payment_txs(txid)`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_payments_status ON payments(status)`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_payments_account ON payments(account_id)`;
|
||||
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@ import { getAddressInfo, getAddressInfoBulk } from "./freedom";
|
|||
import { COINS } from "./plans";
|
||||
|
||||
const SOCK_API = process.env.FREEDOM_SOCK ?? "https://sock-v1.freedom.st";
|
||||
const THRESHOLD = 0.995; // 0.5% tolerance for network fees
|
||||
const THRESHOLD = 0.995;
|
||||
|
||||
// ── In-memory maps ──────────────────────────────────────────────────
|
||||
let addressMap = new Map<string, any>();
|
||||
let txidLookup = new Map<string, number>(); // txid → payment.id
|
||||
let addressMap = new Map<string, any>(); // address → payment
|
||||
let txidToPayment = new Map<string, number>(); // txid → payment.id
|
||||
const seenTxids = new Set<string>();
|
||||
|
||||
async function refreshMaps() {
|
||||
|
|
@ -21,21 +21,24 @@ async function refreshMaps() {
|
|||
`;
|
||||
|
||||
const newAddr = new Map<string, any>();
|
||||
const newTxidLookup = new Map<string, number>();
|
||||
const newTxid = new Map<string, number>();
|
||||
|
||||
for (const p of active) {
|
||||
newAddr.set(p.address, p);
|
||||
if (p.txid) {
|
||||
// txid column may contain comma-separated txids
|
||||
for (const t of p.txid.split(",")) newTxidLookup.set(t, p.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Load all txids for active payments
|
||||
if (active.length > 0) {
|
||||
const ids = active.map((p: any) => p.id);
|
||||
const txs = await sql`SELECT payment_id, txid FROM payment_txs WHERE payment_id = ANY(${ids})`;
|
||||
for (const tx of txs) newTxid.set(tx.txid, tx.payment_id);
|
||||
}
|
||||
|
||||
addressMap = newAddr;
|
||||
txidLookup = newTxidLookup;
|
||||
txidToPayment = newTxid;
|
||||
|
||||
for (const txid of seenTxids) {
|
||||
if (!newTxidLookup.has(txid)) seenTxids.delete(txid);
|
||||
if (!newTxid.has(txid)) seenTxids.delete(txid);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -108,53 +111,49 @@ async function handleTxEvent(event: any) {
|
|||
const coin = COINS[payment.coin];
|
||||
if (!coin) continue;
|
||||
|
||||
// Sum output value going to our address in this tx
|
||||
// Sum outputs going to our address
|
||||
const txValue = outputs
|
||||
.filter((o: any) => o?.script?.address === addr)
|
||||
.reduce((sum: number, o: any) => sum + Number(o.value ?? 0), 0);
|
||||
|
||||
const prevReceived = parseFloat(payment.amount_received || "0");
|
||||
const newReceived = prevReceived + txValue;
|
||||
if (txValue <= 0) continue;
|
||||
|
||||
console.log(`SSE: tx ${txHash} for payment ${payment.id}: +${txValue} ${payment.coin}`);
|
||||
|
||||
// Insert into payment_txs (ignore duplicate)
|
||||
await sql`
|
||||
INSERT INTO payment_txs (payment_id, txid, amount)
|
||||
VALUES (${payment.id}, ${txHash}, ${txValue.toFixed(8)})
|
||||
ON CONFLICT (payment_id, txid) DO NOTHING
|
||||
`;
|
||||
txidToPayment.set(txHash, payment.id);
|
||||
|
||||
// Recalculate total received
|
||||
const [{ total }] = await sql`
|
||||
SELECT COALESCE(SUM(amount::numeric), 0) as total FROM payment_txs WHERE payment_id = ${payment.id}
|
||||
`;
|
||||
const totalReceived = Number(total);
|
||||
const expected = parseFloat(payment.amount_crypto);
|
||||
const threshold = expected * THRESHOLD;
|
||||
|
||||
console.log(`SSE: tx ${txHash} for payment ${payment.id}: +${txValue} ${payment.coin} (total: ${newReceived}/${expected})`);
|
||||
|
||||
// Append txid
|
||||
const txids = payment.txid ? payment.txid + "," + txHash : txHash;
|
||||
|
||||
if (coin.confirmations === 0 && newReceived >= threshold) {
|
||||
// 0-conf, full amount: activate immediately
|
||||
await sql`UPDATE payments SET amount_received = ${newReceived.toFixed(8)}, txid = ${txids}, status = 'paid', paid_at = now() WHERE id = ${payment.id} AND status != 'paid'`;
|
||||
if (coin.confirmations === 0 && totalReceived >= threshold) {
|
||||
await sql`UPDATE payments SET amount_received = ${totalReceived.toFixed(8)}, status = 'paid', paid_at = now() WHERE id = ${payment.id} AND status != 'paid'`;
|
||||
await applyPlan(payment);
|
||||
addressMap.delete(addr);
|
||||
console.log(`Payment ${payment.id} paid (0-conf)`);
|
||||
} else if (coin.confirmations === 0 && newReceived > 0) {
|
||||
// 0-conf, partial: underpaid
|
||||
await sql`UPDATE payments SET amount_received = ${newReceived.toFixed(8)}, txid = ${txids}, status = 'underpaid' WHERE id = ${payment.id}`;
|
||||
payment.amount_received = newReceived.toFixed(8);
|
||||
payment.txid = txids;
|
||||
payment.status = "underpaid";
|
||||
console.log(`Payment ${payment.id} underpaid (0-conf): ${newReceived}/${expected}`);
|
||||
} else if (newReceived >= threshold) {
|
||||
// 1+ conf, full amount: confirming
|
||||
await sql`UPDATE payments SET amount_received = ${newReceived.toFixed(8)}, txid = ${txids}, status = 'confirming' WHERE id = ${payment.id}`;
|
||||
payment.amount_received = newReceived.toFixed(8);
|
||||
payment.txid = txids;
|
||||
} else if (totalReceived >= threshold) {
|
||||
await sql`UPDATE payments SET amount_received = ${totalReceived.toFixed(8)}, status = 'confirming' WHERE id = ${payment.id}`;
|
||||
payment.amount_received = totalReceived.toFixed(8);
|
||||
payment.status = "confirming";
|
||||
for (const t of txids.split(",")) txidLookup.set(t, payment.id);
|
||||
console.log(`Payment ${payment.id} confirming: ${newReceived}/${expected}`);
|
||||
console.log(`Payment ${payment.id} confirming: ${totalReceived}/${expected}`);
|
||||
} else {
|
||||
// 1+ conf, partial: underpaid
|
||||
await sql`UPDATE payments SET amount_received = ${newReceived.toFixed(8)}, txid = ${txids}, status = 'underpaid' WHERE id = ${payment.id}`;
|
||||
payment.amount_received = newReceived.toFixed(8);
|
||||
payment.txid = txids;
|
||||
await sql`UPDATE payments SET amount_received = ${totalReceived.toFixed(8)}, status = 'underpaid' WHERE id = ${payment.id}`;
|
||||
payment.amount_received = totalReceived.toFixed(8);
|
||||
payment.status = "underpaid";
|
||||
for (const t of txids.split(",")) txidLookup.set(t, payment.id);
|
||||
console.log(`Payment ${payment.id} underpaid: ${newReceived}/${expected}`);
|
||||
console.log(`Payment ${payment.id} underpaid: ${totalReceived}/${expected}`);
|
||||
}
|
||||
|
||||
return; // Only process first matching output set per tx
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -164,43 +163,47 @@ async function handleBlockEvent(event: any) {
|
|||
const blockTxs: string[] = event.data?.tx ?? [];
|
||||
if (blockTxs.length === 0) return;
|
||||
|
||||
// Find payments with txids in this block
|
||||
// Find payment txs in this block
|
||||
const paymentIds = new Set<number>();
|
||||
for (const txid of blockTxs) {
|
||||
const pid = txidLookup.get(txid);
|
||||
const pid = txidToPayment.get(txid);
|
||||
if (pid != null) paymentIds.add(pid);
|
||||
}
|
||||
|
||||
if (paymentIds.size === 0) return;
|
||||
|
||||
// Mark matched txs as confirmed
|
||||
const matchedTxids = blockTxs.filter(t => txidToPayment.has(t));
|
||||
if (matchedTxids.length > 0) {
|
||||
await sql`UPDATE payment_txs SET confirmed = true WHERE txid = ANY(${matchedTxids})`;
|
||||
}
|
||||
|
||||
for (const pid of paymentIds) {
|
||||
// Re-fetch from DB for latest state
|
||||
const [payment] = await sql`
|
||||
SELECT * FROM payments WHERE id = ${pid} AND status IN ('underpaid', 'confirming')
|
||||
`;
|
||||
if (!payment) continue;
|
||||
|
||||
// Check confirmed amount via address API
|
||||
let info: any;
|
||||
try { info = await getAddressInfo(payment.address); } catch { continue; }
|
||||
if (!info || info.error) continue;
|
||||
// Check if all txs for this payment are confirmed
|
||||
const [{ unconfirmed }] = await sql`
|
||||
SELECT COUNT(*)::int as unconfirmed FROM payment_txs
|
||||
WHERE payment_id = ${pid} AND confirmed = false
|
||||
`;
|
||||
|
||||
const receivedConfirmed = Number(info.received_confirmed ?? 0);
|
||||
if (Number(unconfirmed) > 0) continue;
|
||||
|
||||
// All confirmed — check total meets threshold
|
||||
const [{ total }] = await sql`
|
||||
SELECT COALESCE(SUM(amount::numeric), 0) as total FROM payment_txs WHERE payment_id = ${pid}
|
||||
`;
|
||||
const totalReceived = Number(total);
|
||||
const expected = parseFloat(payment.amount_crypto);
|
||||
const threshold = expected * THRESHOLD;
|
||||
|
||||
if (receivedConfirmed >= threshold) {
|
||||
await sql`UPDATE payments SET amount_received = ${receivedConfirmed.toFixed(8)}, status = 'paid', paid_at = now() WHERE id = ${payment.id} AND status != 'paid'`;
|
||||
if (totalReceived >= expected * THRESHOLD) {
|
||||
await sql`UPDATE payments SET amount_received = ${totalReceived.toFixed(8)}, status = 'paid', paid_at = now() WHERE id = ${pid} AND status != 'paid'`;
|
||||
await applyPlan(payment);
|
||||
addressMap.delete(payment.address);
|
||||
// Clean up txid lookups
|
||||
if (payment.txid) {
|
||||
for (const t of payment.txid.split(",")) txidLookup.delete(t);
|
||||
}
|
||||
console.log(`Payment ${payment.id} paid (confirmed)`);
|
||||
} else if (receivedConfirmed > 0) {
|
||||
// Partially confirmed — update amount_received
|
||||
await sql`UPDATE payments SET amount_received = ${receivedConfirmed.toFixed(8)} WHERE id = ${payment.id}`;
|
||||
console.log(`Payment ${pid} paid (all txs confirmed)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -247,22 +250,42 @@ export async function checkPayments() {
|
|||
const receivedConfirmed = Number(info.received_confirmed ?? 0);
|
||||
const expected = parseFloat(payment.amount_crypto);
|
||||
const threshold = expected * THRESHOLD;
|
||||
const txid = payment.txid || findTxid(info);
|
||||
|
||||
// Sync txs from address info into payment_txs
|
||||
if (info.in?.length) {
|
||||
for (const tx of info.in) {
|
||||
if (!tx.txid) continue;
|
||||
await sql`
|
||||
INSERT INTO payment_txs (payment_id, txid, amount, confirmed)
|
||||
VALUES (${payment.id}, ${tx.txid}, ${String(tx.amount ?? 0)}, ${tx.block != null})
|
||||
ON CONFLICT (payment_id, txid) DO UPDATE SET confirmed = EXCLUDED.confirmed
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
if (payment.status === "pending" || payment.status === "underpaid") {
|
||||
if (coin.confirmations === 0 && received >= threshold) {
|
||||
await sql`UPDATE payments SET amount_received = ${received.toFixed(8)}, txid = ${txid}, status = 'paid', paid_at = now() WHERE id = ${payment.id} AND status != 'paid'`;
|
||||
await sql`UPDATE payments SET amount_received = ${received.toFixed(8)}, status = 'paid', paid_at = now() WHERE id = ${payment.id} AND status != 'paid'`;
|
||||
await applyPlan(payment);
|
||||
} else if (coin.confirmations > 0 && receivedConfirmed >= threshold) {
|
||||
await sql`UPDATE payments SET amount_received = ${receivedConfirmed.toFixed(8)}, txid = ${txid}, status = 'paid', paid_at = now() WHERE id = ${payment.id} AND status != 'paid'`;
|
||||
await applyPlan(payment);
|
||||
// Check all txs confirmed
|
||||
const [{ unconfirmed }] = await sql`
|
||||
SELECT COUNT(*)::int as unconfirmed FROM payment_txs WHERE payment_id = ${payment.id} AND confirmed = false
|
||||
`;
|
||||
if (Number(unconfirmed) === 0) {
|
||||
await sql`UPDATE payments SET amount_received = ${receivedConfirmed.toFixed(8)}, status = 'paid', paid_at = now() WHERE id = ${payment.id} AND status != 'paid'`;
|
||||
await applyPlan(payment);
|
||||
}
|
||||
} else if (received >= threshold) {
|
||||
await sql`UPDATE payments SET amount_received = ${received.toFixed(8)}, txid = ${txid}, status = 'confirming' WHERE id = ${payment.id}`;
|
||||
await sql`UPDATE payments SET amount_received = ${received.toFixed(8)}, status = 'confirming' WHERE id = ${payment.id}`;
|
||||
} else if (received > 0) {
|
||||
await sql`UPDATE payments SET amount_received = ${received.toFixed(8)}, txid = ${txid}, status = 'underpaid' WHERE id = ${payment.id}`;
|
||||
await sql`UPDATE payments SET amount_received = ${received.toFixed(8)}, status = 'underpaid' WHERE id = ${payment.id}`;
|
||||
}
|
||||
} else if (payment.status === "confirming") {
|
||||
if (receivedConfirmed >= threshold) {
|
||||
const [{ unconfirmed }] = await sql`
|
||||
SELECT COUNT(*)::int as unconfirmed FROM payment_txs WHERE payment_id = ${payment.id} AND confirmed = false
|
||||
`;
|
||||
if (receivedConfirmed >= threshold && Number(unconfirmed) === 0) {
|
||||
await sql`UPDATE payments SET amount_received = ${receivedConfirmed.toFixed(8)}, status = 'paid', paid_at = now() WHERE id = ${payment.id} AND status != 'paid'`;
|
||||
await applyPlan(payment);
|
||||
}
|
||||
|
|
@ -279,11 +302,6 @@ export function watchPayment(payment: any) {
|
|||
addressMap.set(payment.address, payment);
|
||||
}
|
||||
|
||||
function findTxid(info: any): string | null {
|
||||
if (info.in?.length) return info.in.map((i: any) => i.txid).filter(Boolean).join(",") || null;
|
||||
return null;
|
||||
}
|
||||
|
||||
async function applyPlan(payment: any) {
|
||||
if (payment.plan === "lifetime") {
|
||||
await sql`UPDATE accounts SET plan = 'lifetime', plan_expires_at = NULL WHERE id = ${payment.account_id}`;
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ export const routes = new Elysia()
|
|||
address: payment.address,
|
||||
status: payment.status,
|
||||
expires_at: payment.expires_at,
|
||||
txs: [],
|
||||
qr_url: getQrUrl(uri),
|
||||
coin_label: coinInfo.label,
|
||||
coin_ticker: coinInfo.ticker,
|
||||
|
|
@ -144,10 +145,16 @@ export const routes = new Elysia()
|
|||
const amountReceived = parseFloat(payment.amount_received || "0");
|
||||
const amountRemaining = Math.max(0, amountCrypto - amountReceived);
|
||||
|
||||
// QR shows remaining amount (or full amount if nothing received yet)
|
||||
const qrAmount = amountRemaining > 0 ? amountRemaining.toFixed(8) : payment.amount_crypto;
|
||||
const qrAmount = amountRemaining > 0 && amountRemaining < amountCrypto
|
||||
? amountRemaining.toFixed(8) : payment.amount_crypto;
|
||||
const uri = `${coinInfo.uri}:${payment.address}?amount=${qrAmount}`;
|
||||
|
||||
const txs = await sql`
|
||||
SELECT txid, amount, confirmed, detected_at
|
||||
FROM payment_txs WHERE payment_id = ${payment.id}
|
||||
ORDER BY detected_at ASC
|
||||
`;
|
||||
|
||||
return {
|
||||
id: payment.id,
|
||||
plan: payment.plan,
|
||||
|
|
@ -162,7 +169,7 @@ export const routes = new Elysia()
|
|||
created_at: payment.created_at,
|
||||
expires_at: payment.expires_at,
|
||||
paid_at: payment.paid_at,
|
||||
txid: payment.txid,
|
||||
txs: txs.map((t: any) => ({ txid: t.txid, amount: t.amount, confirmed: t.confirmed, detected_at: t.detected_at })),
|
||||
qr_url: getQrUrl(uri),
|
||||
coin_label: coinInfo?.label,
|
||||
coin_ticker: coinInfo?.ticker,
|
||||
|
|
|
|||
|
|
@ -281,27 +281,26 @@
|
|||
}
|
||||
}
|
||||
|
||||
function syncTxids(data) {
|
||||
if (!data?.txs) return;
|
||||
for (const tx of data.txs) {
|
||||
if (tx.txid && !watchedTxids.includes(tx.txid)) watchedTxids.push(tx.txid);
|
||||
}
|
||||
}
|
||||
|
||||
function applyStatus(status, data) {
|
||||
if (status === 'underpaid') {
|
||||
document.getElementById('pay-status').innerHTML = `
|
||||
<span class="w-2 h-2 rounded-full bg-yellow-500 animate-pulse"></span>
|
||||
<span class="text-yellow-400">Underpaid — please send the remaining amount</span>
|
||||
`;
|
||||
if (data?.txid) {
|
||||
for (const t of data.txid.split(',')) {
|
||||
if (!watchedTxids.includes(t)) watchedTxids.push(t);
|
||||
}
|
||||
}
|
||||
syncTxids(data);
|
||||
} else if (status === 'confirming') {
|
||||
document.getElementById('pay-status').innerHTML = `
|
||||
<span class="w-2 h-2 rounded-full bg-blue-500 animate-pulse"></span>
|
||||
<span class="text-blue-400">Transaction detected, waiting for confirmation...</span>
|
||||
`;
|
||||
if (data?.txid) {
|
||||
for (const t of data.txid.split(',')) {
|
||||
if (!watchedTxids.includes(t)) watchedTxids.push(t);
|
||||
}
|
||||
}
|
||||
syncTxids(data);
|
||||
} else if (status === 'paid') {
|
||||
clearInterval(pollInterval);
|
||||
clearInterval(countdownInterval);
|
||||
|
|
|
|||
Loading…
Reference in New Issue