Spaces:
Running
Running
| import { | |
| Keypair, | |
| TransactionBuilder, | |
| Operation, | |
| Asset, | |
| Horizon, | |
| Memo, | |
| } from "@stellar/stellar-sdk"; | |
| import { NETWORK_PASSPHRASE, HORIZON_URL, USDC_ISSUER } from "./network-config.js"; | |
| const USDC = new Asset("USDC", USDC_ISSUER); | |
| // Per-payer refund rate limit. Throws off attrition attacks (paying $0.001 | |
| // repeatedly to drain XLM via tx fees) without blocking legitimate users. | |
| // In-memory only — fine for single-instance HF Space; revisit if we scale. | |
| const REFUND_WINDOW_MS = 60 * 60 * 1000; // 1 hour | |
| const REFUND_MAX_PER_WINDOW = 10; | |
| const refundLog = new Map(); // payerAddress -> [timestamp, ...] | |
| function isRefundRateLimited(payerAddress) { | |
| const now = Date.now(); | |
| const cutoff = now - REFUND_WINDOW_MS; | |
| const history = (refundLog.get(payerAddress) || []).filter((t) => t > cutoff); | |
| if (history.length >= REFUND_MAX_PER_WINDOW) { | |
| refundLog.set(payerAddress, history); | |
| return true; | |
| } | |
| history.push(now); | |
| refundLog.set(payerAddress, history); | |
| return false; | |
| } | |
| // Periodic prune so the map doesn't grow unbounded. | |
| setInterval(() => { | |
| const cutoff = Date.now() - REFUND_WINDOW_MS; | |
| for (const [addr, history] of refundLog) { | |
| const kept = history.filter((t) => t > cutoff); | |
| if (kept.length === 0) refundLog.delete(addr); | |
| else refundLog.set(addr, kept); | |
| } | |
| }, REFUND_WINDOW_MS).unref(); | |
| const BLOCKED_PATTERNS = [ | |
| "you've been blocked", | |
| "you have been blocked", | |
| "blocked by network security", | |
| "access denied", | |
| "please log in", | |
| "log in to", | |
| "sign in to", | |
| "enable javascript", | |
| "just a moment...", | |
| "checking your browser", | |
| "performing security verification", | |
| "ray id:", | |
| "cloudflare", | |
| "attention required", | |
| "please verify you are a human", | |
| "this page doesn't exist", | |
| ]; | |
| export function isFailedRender(content, title) { | |
| if (!content || content.length < 100) return "empty_content"; | |
| const lower = content.toLowerCase(); | |
| const titleLower = (title || "").toLowerCase(); | |
| // Check title for block signals | |
| if (titleLower === "just a moment..." || titleLower === "blocked") { | |
| return "blocked_page"; | |
| } | |
| // Check content for block/login patterns | |
| for (const pattern of BLOCKED_PATTERNS) { | |
| // Only flag if the blocked pattern is a large portion of the content | |
| // (avoid false positives on pages that mention "log in" in a nav bar) | |
| if (lower.includes(pattern) && content.length < 500) { | |
| return "blocked_page"; | |
| } | |
| } | |
| return null; // render is good | |
| } | |
| export async function sendRefund(payerAddress, amount, memo) { | |
| const serverSecret = process.env.SERVER_SECRET; | |
| if (!serverSecret) { | |
| console.error("SERVER_SECRET not set — cannot send refund"); | |
| return null; | |
| } | |
| if (isRefundRateLimited(payerAddress)) { | |
| console.warn( | |
| `Refund rate-limited for ${payerAddress} (>${REFUND_MAX_PER_WINDOW}/hr) — skipping`, | |
| ); | |
| return null; | |
| } | |
| try { | |
| const server = new Horizon.Server(HORIZON_URL); | |
| const keypair = Keypair.fromSecret(serverSecret); | |
| const account = await server.loadAccount(keypair.publicKey()); | |
| const tx = new TransactionBuilder(account, { | |
| fee: "100", | |
| networkPassphrase: NETWORK_PASSPHRASE, | |
| }) | |
| .addOperation( | |
| Operation.payment({ | |
| destination: payerAddress, | |
| asset: USDC, | |
| amount: amount, | |
| }), | |
| ) | |
| .addMemo(new Memo("text", (memo || "RenderGate refund").slice(0, 28))) | |
| .setTimeout(30) | |
| .build(); | |
| tx.sign(keypair); | |
| const result = await server.submitTransaction(tx); | |
| console.log(`Refund sent to ${payerAddress}: ${result.hash}`); | |
| return result.hash; | |
| } catch (err) { | |
| console.error(`Refund failed for ${payerAddress}:`, err.message); | |
| return null; | |
| } | |
| } | |