Skip to content
Open
232 changes: 173 additions & 59 deletions app.js
Original file line number Diff line number Diff line change
@@ -1,61 +1,175 @@
// ===== app.js (final faucet backend) =====
const fetch = global.fetch || ((...a) => import('node-fetch').then(m => m.default(...a)));
const express = require("express");
const xrpl = require("xrpl");
const app = express();
const port = process.env.PORT || 3001;

app.get("/", (req, res) => res.type('html').send(html));

const server = app.listen(port, () => console.log(`Example app listening on port ${port}!`));

server.keepAliveTimeout = 120 * 1000;
server.headersTimeout = 120 * 1000;

const html = `
<!DOCTYPE html>
<html>
<head>
<title>Hello from Render!</title>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/confetti.browser.min.js"></script>
<script>
setTimeout(() => {
confetti({
particleCount: 100,
spread: 70,
origin: { y: 0.6 },
disableForReducedMotion: true
});
}, 500);
</script>
<style>
@import url("https://p.typekit.net/p.css?s=1&k=vnd5zic&ht=tk&f=39475.39476.39477.39478.39479.39480.39481.39482&a=18673890&app=typekit&e=css");
@font-face {
font-family: "neo-sans";
src: url("https://use.typekit.net/af/00ac0a/00000000000000003b9b2033/27/l?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n7&v=3") format("woff2"), url("https://use.typekit.net/af/00ac0a/00000000000000003b9b2033/27/d?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n7&v=3") format("woff"), url("https://use.typekit.net/af/00ac0a/00000000000000003b9b2033/27/a?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n7&v=3") format("opentype");
font-style: normal;
font-weight: 700;
}
html {
font-family: neo-sans;
font-weight: 700;
font-size: calc(62rem / 16);
}
body {
background: white;
}
section {
border-radius: 1em;
padding: 1em;
position: absolute;
top: 50%;
left: 50%;
margin-right: -50%;
transform: translate(-50%, -50%);
}
</style>
</head>
<body>
<section>
Hello from Render!
</section>
</body>
</html>
`

app.use(express.json());

/* ---------- CORS ---------- */
const EXACT_IPFS_ORIGIN = process.env.EXACT_IPFS_ORIGIN || "";
const UD_ORIGIN = process.env.UD_ORIGIN || "";
const ALLOW_ANY_IPFS_SUBDOMAIN = true;

function isAllowedOrigin(origin) {
if (!origin) return false;
if (origin === EXACT_IPFS_ORIGIN) return true;
if (UD_ORIGIN && origin === UD_ORIGIN) return true;
if (ALLOW_ANY_IPFS_SUBDOMAIN) {
try { return new URL(origin).hostname.endsWith(".ipfs.dweb.link"); } catch {}
}
return false;
}

function setCorsHeaders(res, origin) {
res.setHeader("Access-Control-Allow-Origin", origin);
res.setHeader("Vary", "Origin");
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS, GET");
res.setHeader("Access-Control-Allow-Headers", "content-type");
}

app.use((req, res, next) => {
const origin = req.headers.origin;
if (isAllowedOrigin(origin)) setCorsHeaders(res, origin);
next();
});

/* ---------- HEALTH ---------- */
app.get('/health', (_req, res) => res.json({ ok: true }));

/* ---------- SDK serve ---------- */
app.get('/sdk/xumm.min.js', async (_req, res) => {
const r = await fetch('https://xumm.app/assets/cdn/xumm.min.js');
res.type('application/javascript').send(await r.text());
});

app.get('/sdk/xrpl-latest-min.js', async (_req, res) => {
const r = await fetch('https://cdnjs.cloudflare.com/ajax/libs/xrpl/3.2.0/xrpl-latest-min.js');
res.type('application/javascript').send(await r.text());
});

/* ---------- PAY (unchanged stubs) ---------- */
app.get('/api/pay-cfc', async (_req, res) => res.json({ ok: true }));
app.get('/api/pay-xrp', async (_req, res) => res.json({ ok: true }));

/* ---------- BALANCES ---------- */
app.get('/api/balances', async (req, res) => {
const account = (req.query.account || '').trim();
if (!account) return res.status(400).json({ ok: false, error: 'Missing account' });
try {
const client = new xrpl.Client('wss://xrplcluster.com');
await client.connect();
let xrp = null, cfc = null, hasTrust = false;
try {
const ai = await client.request({ command: 'account_info', account, ledger_index: 'validated' });
xrp = ai.result?.account_data?.Balance ? ai.result.account_data.Balance / 1_000_000 : null;
} catch {}
const issuer = process.env.CFC_ISSUER || process.env.ISSUER_CLASSIC;
const currency = process.env.CFC_CURRENCY || 'CFC';
if (issuer) {
try {
const al = await client.request({ command: 'account_lines', account, ledger_index: 'validated', peer: issuer });
const line = (al.result?.lines || []).find(l => l.currency === currency);
cfc = line ? line.balance : null;
hasTrust = !!line;
} catch {}
}
await client.disconnect();
return res.json({ ok: true, xrp, cfc, hasTrust });
} catch (e) {
console.error('balances error:', e);
return res.status(500).json({ ok: false, error: 'XRPL error' });
}
});

/* ---------- JOIN ---------- */
app.post('/api/join', (req, res) => {
const email = (req.body && req.body.email || '').trim();
if (!email) return res.status(400).json({ ok: false, error: 'Missing email' });
console.log('JOIN email:', email);
return res.json({ ok: true });
});

/* ---------- FAUCET (real CFC send) ---------- */
const grants = new Map();

app.post('/api/faucet', async (req, res) => {
try {
const { account, captcha_ok } = req.body || {};
if (!captcha_ok) return res.status(400).json({ ok: false, error: 'Captcha required' });
if (!account || !/^r[1-9A-HJ-NP-Za-km-z]{25,34}$/.test(account)) {
return res.status(400).json({ ok: false, error: 'Invalid account' });
}

// 24h rate limit
const last = grants.get(account) || 0;
const now = Date.now();
if (now - last < 24 * 60 * 60 * 1000) {
return res.status(429).json({ ok: false, error: 'Faucet already claimed (24h limit)' });
}

const issuer = process.env.ISSUER_CLASSIC || process.env.CFC_ISSUER;
const seed = process.env.ISSUER_SEED || process.env.FAUCET_SEED;
const currency = process.env.CFC_CURRENCY || 'CFC';
const value = String(process.env.AMOUNT_CFC || '10');
if (!issuer || !seed) {
return res.status(500).json({ ok: false, error: 'Server faucet not configured' });
}

const client = new xrpl.Client(process.env.RIPPLED_URL || 'wss://s1.ripple.com');
await client.connect();

// Trustline check
const al = await client.request({ command: 'account_lines', account, ledger_index: 'validated', peer: issuer });
const hasLine = (al.result?.lines || []).some(l => l.currency === currency);
if (!hasLine) {
await client.disconnect();
return res.status(400).json({ ok: false, error: 'No CFC trustline. Please add trustline first.' });
}

// Build tx
const wallet = xrpl.Wallet.fromSeed(seed);
const tx = {
TransactionType: "Payment",
Account: wallet.address,
Destination: account,
Amount: { currency, issuer, value }
};

// Use autofill + extended ledger window
const filled = await client.autofill(tx);
const { result: { ledger_index } } = await client.request({ command: "ledger", ledger_index: "validated" });
filled.LastLedgerSequence = ledger_index + 20;

const signed = wallet.sign(filled);
const result = await client.submitAndWait(signed.tx_blob);

await client.disconnect();

if (result.result?.meta?.TransactionResult === 'tesSUCCESS') {
grants.set(account, now);
return res.json({ ok: true, hash: result.result?.tx_json?.hash });
} else {
return res.status(500).json({
ok: false,
error: result.result?.meta?.TransactionResult || 'Submit failed'
});
}
} catch (e) {
console.error('faucet error:', e);
return res.status(500).json({ ok: false, error: String(e.message || e) });
}
});

/* ---------- ROOT ---------- */
app.get("/", (_req, res) =>
res.type('html').send(`<!doctype html><html><body><h1>Hello from Render</h1></body></html>`)
);

/* ---------- START ---------- */
const port = process.env.PORT || 10000;
const server = app.listen(port, () => console.log(`Server listening on ${port}`));
server.keepAliveTimeout = 120000;
server.headersTimeout = 120000;

// ===== end app.js =====
22 changes: 9 additions & 13 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
{
"name": "express-hello-world",
"version": "1.0.0",
"description": "Express Hello World on Render",
"main": "app.js",
"repository": "https://github.com/render-examples/express-hello-world",
"author": "Render Developers",
"license": "MIT",
"private": false,
"scripts": {
"start": "node app.js"
},
"name": "cfc-backend",
"private": true,
"engines": { "node": ">=18" },
"dependencies": {
"express": "^5.0.0"
}
"express": "^4.19.2",
"xrpl": "^2.11.0",
"node-fetch": "^3.3.2"
},
"scripts": { "start": "node app.js" }
}