fet: reduce LOC by reducing comments
This commit is contained in:
@@ -1,6 +1,3 @@
|
||||
/// HD address derivation using bitcore-lib family.
|
||||
/// Derives child addresses at m/0/{index} (external receive chain).
|
||||
|
||||
// @ts-ignore
|
||||
import bitcore from "bitcore-lib";
|
||||
import bs58check from "bs58check";
|
||||
|
||||
@@ -13,7 +13,6 @@ const app = new Elysia()
|
||||
.onAfterHandle(({ set }) => {
|
||||
Object.assign(set.headers, SECURITY_HEADERS);
|
||||
})
|
||||
// CORS for web app
|
||||
.onRequest(({ request, set }) => {
|
||||
const origin = request.headers.get("origin") ?? "";
|
||||
if (CORS_ORIGIN.includes(origin)) {
|
||||
@@ -42,13 +41,11 @@ const app = new Elysia()
|
||||
|
||||
console.log(`PingQL Pay running at http://localhost:${app.server?.port}`);
|
||||
|
||||
// Run immediately on startup, then every 30 seconds
|
||||
checkPayments().catch((err) => console.error("Payment check failed:", err));
|
||||
setInterval(() => {
|
||||
checkPayments().catch((err) => console.error("Payment check failed:", err));
|
||||
}, 30_000);
|
||||
|
||||
// Expire pro plans every hour
|
||||
setInterval(() => {
|
||||
expireProPlans().catch((err) => console.error("Plan expiry check failed:", err));
|
||||
}, 60 * 60_000);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/// Payment monitor: raw SSE + polling fallback.
|
||||
/// States: pending → underpaid → confirming → paid | expired
|
||||
import sql from "./db";
|
||||
import { getAddressInfo, getAddressInfoBulk } from "./freedom";
|
||||
import { COINS, planTier } from "../../shared/plans";
|
||||
@@ -8,7 +6,6 @@ import { generateReceipt } from "./receipt";
|
||||
const SOCK_API = process.env.FREEDOM_SOCK ?? "https://sock-v1.freedom.st";
|
||||
const THRESHOLD = 0.95;
|
||||
|
||||
// ── In-memory lookups for SSE matching ──────────────────────────────
|
||||
let addressMap = new Map<string, any>(); // address → payment
|
||||
let txidToPayment = new Map<string, number>(); // txid → payment.id
|
||||
const seenTxids = new Set<string>();
|
||||
@@ -35,8 +32,6 @@ async function refreshMaps() {
|
||||
for (const t of seenTxids) { if (!newTxid.has(t)) seenTxids.delete(t); }
|
||||
}
|
||||
|
||||
// ── Core logic: one place for all state transitions ─────────────────
|
||||
|
||||
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
|
||||
@@ -52,7 +47,6 @@ async function recordTx(paymentId: number, address: string, txid: string, amount
|
||||
ON CONFLICT (payment_id, txid) DO UPDATE SET confirmed = EXCLUDED.confirmed OR payment_txs.confirmed
|
||||
RETURNING (xmax = 0) as is_new
|
||||
`;
|
||||
// Extend expiry to 24h on new tx
|
||||
if (ins?.is_new) {
|
||||
const exp = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
|
||||
await sql`UPDATE payments SET expires_at = ${exp} WHERE id = ${paymentId} AND expires_at < ${exp}`;
|
||||
@@ -100,8 +94,6 @@ async function evaluatePayment(paymentId: number) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── SSE ─────────────────────────────────────────────────────────────
|
||||
|
||||
async function handleTxEvent(event: any) {
|
||||
const txHash = event.data?.tx?.hash;
|
||||
if (!txHash || seenTxids.has(txHash)) return;
|
||||
@@ -179,8 +171,6 @@ async function connectSSE(url: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Polling fallback ────────────────────────────────────────────────
|
||||
|
||||
export async function checkPayments() {
|
||||
await sql`
|
||||
UPDATE payments SET status = 'expired'
|
||||
@@ -202,7 +192,6 @@ export async function checkPayments() {
|
||||
if (!info) try { info = await getAddressInfo(payment.address); } catch { continue; }
|
||||
if (!info || info.error) continue;
|
||||
|
||||
// Sync txs from address API
|
||||
for (const tx of info.in ?? []) {
|
||||
if (!tx.txid) continue;
|
||||
await recordTx(payment.id, payment.address, tx.txid, Number(tx.amount ?? 0), tx.block != null);
|
||||
@@ -214,21 +203,16 @@ export async function checkPayments() {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
export function watchPayment(payment: any) {
|
||||
addressMap.set(payment.address, payment);
|
||||
}
|
||||
|
||||
// ── Plan stacking logic (pure, testable) ─────────────────────────
|
||||
|
||||
interface StackEntry { plan: string; remaining_days: number | null }
|
||||
interface AccountState { plan: string; plan_expires_at: Date | null; plan_stack: StackEntry[] }
|
||||
interface AccountUpdate { plan: string; plan_expires_at: Date | null; plan_stack: StackEntry[] }
|
||||
|
||||
export function insertIntoStack(stack: StackEntry[], entry: StackEntry): StackEntry[] {
|
||||
const result = stack.slice();
|
||||
// Merge if same plan already exists
|
||||
const existing = result.findIndex(e => e.plan === entry.plan);
|
||||
if (existing !== -1) {
|
||||
const old = result[existing];
|
||||
@@ -237,11 +221,9 @@ export function insertIntoStack(stack: StackEntry[], entry: StackEntry): StackEn
|
||||
} else {
|
||||
result[existing] = { plan: entry.plan, remaining_days: old.remaining_days + entry.remaining_days };
|
||||
}
|
||||
// Re-sort after merge
|
||||
result.sort((a, b) => planTier(b.plan) - planTier(a.plan));
|
||||
return result;
|
||||
}
|
||||
// Insert at correct position (tier descending)
|
||||
const tier = planTier(entry.plan);
|
||||
let i = 0;
|
||||
while (i < result.length && planTier(result[i].plan) >= tier) i++;
|
||||
@@ -262,19 +244,16 @@ export function computeApplyPlan(
|
||||
const currentIsActive = acc.plan === "lifetime"
|
||||
|| (acc.plan !== "free" && currentExpiry && currentExpiry > now);
|
||||
|
||||
// No active plan worth saving — just activate the new one
|
||||
if (!currentIsActive || acc.plan === "free") {
|
||||
const expiresAt = newDays != null ? new Date(now.getTime() + newDays * 86400000) : null;
|
||||
return { plan: newPlan, plan_expires_at: expiresAt, plan_stack: stack };
|
||||
}
|
||||
|
||||
// Same plan renewal — extend from current expiry
|
||||
if (newPlan === acc.plan && newDays != null && currentExpiry) {
|
||||
const extended = new Date(currentExpiry.getTime() + newDays * 86400000);
|
||||
return { plan: acc.plan, plan_expires_at: extended, plan_stack: stack };
|
||||
}
|
||||
|
||||
// Upgrade: new plan takes over, current gets frozen onto stack
|
||||
if (planTier(newPlan) > planTier(acc.plan)) {
|
||||
const remainingDays = acc.plan === "lifetime"
|
||||
? null
|
||||
@@ -284,38 +263,30 @@ export function computeApplyPlan(
|
||||
return { plan: newPlan, plan_expires_at: expiresAt, plan_stack: newStack };
|
||||
}
|
||||
|
||||
// Downgrade/side-grade: purchased plan goes onto stack, current stays active
|
||||
const newStack = insertIntoStack(stack, { plan: newPlan, remaining_days: newDays });
|
||||
return { plan: acc.plan, plan_expires_at: currentExpiry, plan_stack: newStack };
|
||||
}
|
||||
|
||||
export function computeExpiry(acc: AccountState, now: Date): AccountUpdate | null {
|
||||
// Only expire timed pro plans
|
||||
if (!["pro", "pro2x", "pro4x"].includes(acc.plan)) return null;
|
||||
if (!acc.plan_expires_at || new Date(acc.plan_expires_at) >= now) return null;
|
||||
|
||||
const stack = (acc.plan_stack || []).slice();
|
||||
|
||||
// Pop layers until we find a valid one or exhaust the stack
|
||||
while (stack.length > 0) {
|
||||
const next = stack.shift()!;
|
||||
if (next.remaining_days === null) {
|
||||
// Permanent plan (lifetime)
|
||||
return { plan: next.plan, plan_expires_at: null, plan_stack: stack };
|
||||
}
|
||||
if (next.remaining_days > 0) {
|
||||
const expiresAt = new Date(now.getTime() + next.remaining_days * 86400000);
|
||||
return { plan: next.plan, plan_expires_at: expiresAt, plan_stack: stack };
|
||||
}
|
||||
// 0 days — skip, try next
|
||||
}
|
||||
|
||||
// Stack exhausted — fall to free
|
||||
return { plan: "free", plan_expires_at: null, plan_stack: [] };
|
||||
}
|
||||
|
||||
// ── DB wrappers ──────────────────────────────────────────────────
|
||||
|
||||
async function applyPlan(payment: any) {
|
||||
try {
|
||||
await sql.begin(async (tx) => {
|
||||
|
||||
@@ -5,7 +5,6 @@ export async function generateReceipt(paymentId: number): Promise<string> {
|
||||
const [payment] = await sql`SELECT * FROM payments WHERE id = ${paymentId}`;
|
||||
if (!payment) throw new Error("Payment not found");
|
||||
|
||||
// Already locked — return as-is
|
||||
if (payment.receipt_html) return payment.receipt_html;
|
||||
|
||||
const coinInfo = COINS[payment.coin];
|
||||
@@ -126,7 +125,6 @@ export async function generateReceipt(paymentId: number): Promise<string> {
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
// Minify and lock it
|
||||
const minified = html.replace(/\n\s*/g, "").replace(/>\s+</g, "><").replace(/\s{2,}/g, " ");
|
||||
await sql`UPDATE payments SET receipt_html = ${minified} WHERE id = ${paymentId}`;
|
||||
return minified;
|
||||
|
||||
@@ -30,7 +30,6 @@ function requireAuth(app: Elysia) {
|
||||
|
||||
export const routes = new Elysia()
|
||||
|
||||
// Public: available coins and rates
|
||||
.get("/coins", async () => {
|
||||
const [available, rates] = await Promise.all([getAvailableCoins(), getExchangeRates()]);
|
||||
const coins = Object.entries(COINS)
|
||||
@@ -41,13 +40,11 @@ export const routes = new Elysia()
|
||||
|
||||
.use(requireAuth)
|
||||
|
||||
// Create a checkout
|
||||
.post("/checkout", async ({ accountId, keyId, body, set }) => {
|
||||
if (keyId) { set.status = 403; return { error: "Sub-keys cannot create checkouts" }; }
|
||||
|
||||
const { plan, months, coin } = body;
|
||||
|
||||
// Validate plan — block duplicate lifetime
|
||||
if (plan === "lifetime") {
|
||||
const [acc] = await sql`SELECT plan, plan_stack FROM accounts WHERE id = ${accountId}`;
|
||||
const stack = typeof acc.plan_stack === "string" ? JSON.parse(acc.plan_stack) : (acc.plan_stack || []);
|
||||
@@ -55,17 +52,14 @@ export const routes = new Elysia()
|
||||
if (hasLifetime) { set.status = 400; return { error: "You already have a lifetime plan" }; }
|
||||
}
|
||||
|
||||
// Validate coin
|
||||
if (!COINS[coin]) { set.status = 400; return { error: `Unknown coin: ${coin}` }; }
|
||||
const available = await getAvailableCoins();
|
||||
if (!available.includes(coin)) { set.status = 400; return { error: `${coin} is temporarily unavailable` }; }
|
||||
|
||||
// Calculate amount
|
||||
const planDef = PLANS[plan];
|
||||
if (!planDef) { set.status = 400; return { error: `Unknown plan: ${plan}` }; }
|
||||
let amountUsd = planDef.priceUsd ?? (planDef.monthlyUsd! * (months ?? 1));
|
||||
|
||||
// Lifetime discount: credit up to 50% of lifetime price from previous payments
|
||||
if (plan === "lifetime" && planDef.priceUsd) {
|
||||
const [{ total }] = await sql`SELECT COALESCE(SUM(amount_usd), 0)::numeric as total FROM payments WHERE account_id = ${accountId} AND status = 'paid'`;
|
||||
const credit = Math.min(Number(total), planDef.priceUsd * 0.75);
|
||||
@@ -75,15 +69,12 @@ export const routes = new Elysia()
|
||||
const rate = rates[coin];
|
||||
if (!rate) { set.status = 500; return { error: "Could not fetch exchange rate" }; }
|
||||
|
||||
// Crypto amount with 8 decimal precision
|
||||
const amountCrypto = (amountUsd / rate).toFixed(8);
|
||||
|
||||
// Get next derivation index for this coin
|
||||
const [{ next_index }] = await sql`
|
||||
SELECT COALESCE(MAX(derivation_index), -1) + 1 as next_index FROM payments WHERE coin = ${coin}
|
||||
`;
|
||||
|
||||
// Derive address
|
||||
let address: string;
|
||||
try {
|
||||
address = derive(coin, next_index);
|
||||
@@ -100,10 +91,8 @@ export const routes = new Elysia()
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
// Start watching this address immediately via SSE
|
||||
watchPayment(payment);
|
||||
|
||||
// Build payment URI for QR code
|
||||
const coinInfo = COINS[coin];
|
||||
const uri = `${coinInfo.uri}:${address.replace(/^.*:/, '')}?amount=${amountCrypto}`;
|
||||
|
||||
@@ -133,7 +122,6 @@ export const routes = new Elysia()
|
||||
}),
|
||||
})
|
||||
|
||||
// Get checkout details
|
||||
.get("/checkout/:id", async ({ accountId, params, set }) => {
|
||||
const [payment] = await sql`
|
||||
SELECT * FROM payments WHERE id = ${params.id} AND account_id = ${accountId}
|
||||
@@ -178,7 +166,6 @@ export const routes = new Elysia()
|
||||
};
|
||||
})
|
||||
|
||||
// Serve locked receipt for a paid invoice
|
||||
.get("/checkout/:id/receipt", async ({ accountId, params, set }) => {
|
||||
const [payment] = await sql`
|
||||
SELECT * FROM payments WHERE id = ${params.id} AND account_id = ${accountId}
|
||||
|
||||
Reference in New Issue
Block a user