diff options
| author | Adam Soltys <[email protected]> | 2023-11-30 15:34:45 -0800 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2023-11-30 15:34:45 -0800 |
| commit | a7322928c45848cfb9984454bf8ece6e3439c673 (patch) | |
| tree | 9b1031c31ee1f8160306ac66749afe0802f8aa54 | |
| parent | acb93f29e83b35c0917d88ebbc62812e2dff989f (diff) | |
| download | coinos-server-a7322928c45848cfb9984454bf8ece6e3439c673.tar.gz coinos-server-a7322928c45848cfb9984454bf8ece6e3439c673.zip | |
zaps and emails
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | index.js | 15 | ||||
| -rw-r--r-- | lib/db.js | 34 | ||||
| -rw-r--r-- | lib/invoices.js | 1 | ||||
| -rw-r--r-- | lib/lightning.js | 14 | ||||
| -rw-r--r-- | lib/mail.js | 14 | ||||
| -rw-r--r-- | lib/nostr.js | 88 | ||||
| -rw-r--r-- | lib/payments.js | 7 | ||||
| -rw-r--r-- | lib/register.js | 1 | ||||
| -rw-r--r-- | package.json | 1 | ||||
| -rw-r--r-- | pnpm-lock.yaml | 53 | ||||
| -rw-r--r-- | routes/lnurl.js | 16 | ||||
| -rw-r--r-- | routes/payments.js | 2 | ||||
| -rw-r--r-- | routes/users.js | 18 | ||||
| -rw-r--r-- | templates/payments/sent.html | 28 |
15 files changed, 269 insertions, 24 deletions
@@ -7,3 +7,4 @@ compose.yml *.flitevox mimic watcher.sh +.aws @@ -6,6 +6,7 @@ import { listenForLightning } from "$lib/lightning"; import { getLocations } from "$lib/locations"; import { getRates, sendRates } from "$lib/rates"; import { sendHeartbeat } from "$lib/sockets"; +import { err } from "$lib/logging"; import email from "$routes/email"; import info from "$routes/info"; @@ -19,10 +20,13 @@ import payments from "$routes/payments"; import requests from "$routes/requests"; import shopify from "$routes/shopify"; -fillPool(); - -getLocations(); -getRates(); +try { + fillPool(); + getLocations(); + getRates(); +} catch (e) { + console.log(e); +} setTimeout(listenForLightning, 2000); @@ -84,6 +88,7 @@ app.post("/upload/:type", users.upload); app.get("/users/delete/:username", users.del); app.post("/acl", users.acl); app.post("/superuser", users.superuser); +app.post("/verify", users.verify); app.post("/login", users.login); @@ -105,3 +110,5 @@ let host = process.env.HOST || "0.0.0.0"; let port = process.env.PORT || 3119; app.listen({ host, port }); + + process.on("unhandledRejection", () => err("Unhandled error")); @@ -1,7 +1,7 @@ import config from "$config"; -import { sleep } from "$lib/utils"; +import { wait, sleep } from "$lib/utils"; import { createClient, defineScript } from "redis"; -import { warn } from "$lib/logging"; +import { warn, l, err } from "$lib/logging"; let SCRIPT = ` local balanceKey = KEYS[1] @@ -33,11 +33,35 @@ let debit = defineScript({ transformArguments: (...args) => args.map(a => a.toString()) }); -export const db = createClient({ +export let db = createClient({ url: config.db, - scripts: { debit } + scripts: { debit }, + socket: { + reconnectStrategy: retries => Math.min(retries * 50, 5000) + } +}); + + +async function connectWithRetry() { + try { + await db.connect(); + } catch (err) { + console.error('Failed to connect to Redis, retrying...', err); + setTimeout(connectWithRetry, 5000); // Retry after 5 seconds + } +} + +connectWithRetry(); + +db.on("error", e => { + if (e.message.startsWith("getaddr")) return; + err("Redis error", e.message); }); -await db.connect(); + +db.on("end", () => { + warn("Redis connection ended"); +}); + export default db; export let g = async k => { diff --git a/lib/invoices.js b/lib/invoices.js index 8ac49a79..9f2c0dc9 100644 --- a/lib/invoices.js +++ b/lib/invoices.js @@ -65,6 +65,7 @@ export let generate = async ({ invoice, user, sender, memo }) => { currency, hash, id, + memo, rate, pending: 0, received: 0, diff --git a/lib/lightning.js b/lib/lightning.js index 77a429ed..d4664458 100644 --- a/lib/lightning.js +++ b/lib/lightning.js @@ -1,4 +1,5 @@ import { emit } from "$lib/sockets"; +import { handleZap } from "$lib/nostr"; import config from "$config"; import { notify } from "$lib/notifications"; import { callWebhook } from "$lib/webhooks"; @@ -8,14 +9,15 @@ import { g, s, t, db } from "$lib/db"; import { credit, types } from "$lib/payments"; export async function listenForLightning() { - let event = await ln.waitanyinvoice((await g("pay_index")) || 0); + let inv = await ln.waitanyinvoice((await g("pay_index")) || 0); let { bolt11, + description, pay_index, status, amount_received_msat, payment_preimage: preimage - } = event; + } = inv; await s("pay_index", pay_index); setTimeout(listenForLightning); @@ -31,6 +33,14 @@ export async function listenForLightning() { let invoice = await g(`invoice:${id}`); if (!invoice) return warn("received lightning with no invoice", bolt11); + if (invoice && invoice.memo) { + try { + if (JSON.parse(description).kind === 9734) await handleZap(inv); + } catch (e) { + warn("failed to handle zap", e.message); + } + } + let p = await g(`payment:${bolt11}`); if (typeof p === "string") p = await g(`payment:${p}`); diff --git a/lib/mail.js b/lib/mail.js index b5fda14d..47ef5eac 100644 --- a/lib/mail.js +++ b/lib/mail.js @@ -14,12 +14,15 @@ export let templates = { export let mail = async (user, subject, template, params) => { try { + l("sending mail", user.username, subject); if (!user.email) return; - let source = fs.readFileSync(template, "utf8"); - let html = handlebars.compile(source, params); + let source = fs.readFileSync(templates[template], "utf8"); + let html = handlebars.compile(source)(params); let client = new SESClient({ region: "us-east-2" }); + return; + await client.send( new SendEmailCommand({ Destination: { @@ -38,3 +41,10 @@ export let mail = async (user, subject, template, params) => { err(e.message); } }; + +/* +mail({ username: "bob", email: "[email protected]" }, "Test", "paymentSent", { + username: "bob", + link: "https://coinos.io/api/verify/12345" +}); +*/ diff --git a/lib/nostr.js b/lib/nostr.js index 604d207d..3a370aec 100644 --- a/lib/nostr.js +++ b/lib/nostr.js @@ -1,9 +1,10 @@ import config from "$config"; -import { Relay, RelayPool } from "nostr"; +import { Relay, RelayPool, calculateId, signId, getPublicKey } from "nostr"; import { broadcast, emit } from "$lib/sockets"; import store from "$lib/store"; import { g, s, db } from "$lib/db"; -import { nada, wait } from "$lib/utils"; +import { fail, nada, wait } from "$lib/utils"; +import { nip19 } from "nostr-tools"; export let coinos; export let pool; @@ -178,3 +179,86 @@ let getUser = async pubkey => { } ); }; + +function send(ev, url, opts) { + let timeout = (opts && opts.timeout != null && opts.timeout) || 1000; + + return new Promise((resolve, reject) => { + let relay = Relay(url); + + function timeout_reached() { + relay.close(); + reject(new Error("Request timeout")); + } + + let timer = setTimeout(timeout_reached, timeout); + + relay.on("open", () => { + clearTimeout(timer); + timer = setTimeout(timeout_reached, timeout); + relay.send(["EVENT", ev]); + }); + + relay.on("ok", (evid, ok, msg) => { + clearTimeout(timer); + relay.close(); + resolve({ evid, ok, msg }); + }); + }); +} + +export async function handleZap(invoice) { + let { nostrKey: privkey } = config; + let pubkey = getPublicKey(privkey); + let keypair = { privkey, pubkey }; + let zapreq = JSON.parse(invoice.description); + + if (!zapreq.tags || zapreq.tags.length === 0) { + fail(`No tags found`); + } + + let ptags = zapreq.tags.filter( + t => t && t.length && t.length >= 2 && t[0] === "p" + ); + + if (ptags.length !== 1) { + fail(`None or multiple p tags found`); + } + + let etags = zapreq.tags.filter( + t => t && t.length && t.length >= 2 && t[0] === "e" + ); + + if (!(etags.length === 0 || etags.length === 1)) { + fail(`Expected none or 1 e tags`); + } + + let relays_tag = zapreq.tags.find( + t => t && t.length && t.length >= 2 && t[0] === "relays" + ); + + if (!relays_tag) { + fail(`No relays tag found`); + } + + let relays = relays_tag.slice(1).filter(r => r && r.startsWith("ws")); + let etag = etags.length > 0 && etags[0]; + let ptag = ptags[0]; + + let kind = 9735; + let created_at = invoice.paid_at; + let content = zapreq.content; + + let tags = [ptag]; + if (etag) tags.push(etag); + + tags.push(["bolt11", invoice.bolt11]); + tags.push(["description", invoice.description]); + tags.push(["preimage", invoice.payment_preimage]); + + let ev = { pubkey, kind, created_at, content, tags }; + ev.id = await calculateId(ev); + ev.sig = await signId(privkey, ev.id); + + await Promise.all(relays.map(r => send(ev, r))); +} diff --git a/lib/payments.js b/lib/payments.js index 9f32f8fb..f2fab1ba 100644 --- a/lib/payments.js +++ b/lib/payments.js @@ -113,9 +113,12 @@ export let credit = async (hash, amount, memo, ref, type = types.internal) => { if (iid && iid.hash) iid = iid.hash; let invoice = await g(`invoice:${iid}`); - if (!invoice) return warn("failed to credit", hash, ", invoice not found"); + if (!invoice) { + await db.sAdd("missing", hash); + return; + } - let { tip } = invoice; + let { tip } = invoice; tip = parseInt(tip) || 0; if (!memo) ({ memo } = invoice); diff --git a/lib/register.js b/lib/register.js index 798565b9..c52dad03 100644 --- a/lib/register.js +++ b/lib/register.js @@ -45,6 +45,7 @@ export default async (user, ip, requireChallenge) => { user.fiat = false; user.otpsecret = authenticator.generateSecret(); user.migrated = true; + user.locktime = 300; await s(`user:${id}`, user); await s(`user:${username}`, id); diff --git a/package.json b/package.json index 5e2ebe1d..e99ef1e6 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "mqtt": "^5.0.2", "nodemon": "^2.0.7", "nostr": "^0.2.7", + "nostr-tools": "^1.17.0", "otplib": "^12.0.1", "passport-jwt": "^4.0.0", "passport-local": "1.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 940bb30b..54e6e02b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,6 +74,9 @@ dependencies: nostr: specifier: ^0.2.7 version: 0.2.7 + nostr-tools: + specifier: ^1.17.0 + version: 1.17.0 otplib: specifier: ^12.0.1 version: 12.0.1 @@ -738,6 +741,21 @@ packages: - supports-color dev: false + /@noble/[email protected]: + resolution: {integrity: sha512-6YBxJDAapHSdd3bLDv6x2wRPwq4QFMUaB3HvljNBUTThDd12eSm7/3F+2lnfzx2jvM+S6Nsy0jEt9QbPqSwqRw==} + dev: false + + /@noble/[email protected]: + resolution: {integrity: sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==} + dependencies: + '@noble/hashes': 1.3.1 + dev: false + + /@noble/[email protected]: + resolution: {integrity: sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==} + engines: {node: '>= 16'} + dev: false + /@otplib/[email protected]: resolution: {integrity: sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA==} dev: false @@ -820,6 +838,25 @@ packages: '@redis/client': 1.5.11 dev: false + /@scure/[email protected]: + resolution: {integrity: sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==} + dev: false + + /@scure/[email protected]: + resolution: {integrity: sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==} + dependencies: + '@noble/curves': 1.1.0 + '@noble/hashes': 1.3.1 + '@scure/base': 1.1.1 + dev: false + + /@scure/[email protected]: + resolution: {integrity: sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==} + dependencies: + '@noble/hashes': 1.3.1 + '@scure/base': 1.1.1 + dev: false + /@sindresorhus/[email protected]: resolution: {integrity: sha512-CX6t4SYQ37lzxicAqsBtxA3OseeoVrh9cSJ5PFYam0GksYlupRfy1A+Q4aYD3zvcfECLc0zO2u+ZnR2UYKvCrw==} engines: {node: '>=14.16'} @@ -2832,6 +2869,22 @@ packages: engines: {node: '>=14.16'} dev: false + resolution: {integrity: sha512-LZmR8GEWKZeElbFV5Xte75dOeE9EFUW/QLI1Ncn3JKn0kFddDKEfBbFN8Mu4TMs+L4HR/WTPha2l+PPuRnJcMw==} + peerDependencies: + typescript: '>=5.0.0' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@noble/ciphers': 0.2.0 + '@noble/curves': 1.1.0 + '@noble/hashes': 1.3.1 + '@scure/base': 1.1.1 + '@scure/bip32': 1.3.1 + '@scure/bip39': 1.2.1 + dev: false + resolution: {integrity: sha512-Yq5tkiCTJtohs7H7Fx+Ki83bR8b3xapdwYi17HunhW5EQfZ9rI0cIXFyihj2ZRkv/0YYvVBiCp/KIaLYt5LyfQ==} dependencies: diff --git a/routes/lnurl.js b/routes/lnurl.js index 6567f0b7..0f760695 100644 --- a/routes/lnurl.js +++ b/routes/lnurl.js @@ -47,7 +47,7 @@ export default { async lnurlp({ params: { username } }, res) { try { - let { id: uid } = await getUser(username); + let { id: uid, pubkey: nostrPubkey } = await getUser(username); let metadata = JSON.stringify([ ["text/plain", `Paying ${username}@${host}`], @@ -58,9 +58,11 @@ export default { await s(`lnurl:${id}`, uid); res.send({ + allowsNostr: true, minSendable: 1000, maxSendable: 100000000000, metadata, + nostrPubkey, callback: `${URL}/api/lnurl/${id}`, tag: "payRequest" }); @@ -72,12 +74,22 @@ export default { async lnurlw({ params: { k1, pr } }, res) {}, - async lnurl({ params: { id }, query: { amount } }, res) { + async lnurl({ params: { id }, query: { amount, nostr } }, res) { let uid = await g(`lnurl:${id}`); let user = await g(`user:${uid}`); let { username } = user; username = username.replace(/\s/g, "").toLowerCase(); + if (nostr) { + try { + let event = JSON.parse(decodeURIComponent(nostr)); + // TODO: validate the event + await s(`zap:${id}`, event); + } catch (e) { + err("problem handling zap", e.message); + } + } + let metadata = JSON.stringify([ ["text/plain", `Paying ${username}@${host}`], ["text/identifier", `${username}@${host}`] diff --git a/routes/payments.js b/routes/payments.js index b4ac7dfd..84fe4e61 100644 --- a/routes/payments.js +++ b/routes/payments.js @@ -13,7 +13,7 @@ import got from "got"; import bc from "$lib/bitcoin"; import ln from "$lib/ln"; -let seen = []; +let seen = await db.sMembers("missing"); let catchUp = async () => { let txns = await bc.listTransactions("*", 50); txns = txns.filter((tx) => tx.category === "receive" && tx.confirmations > 0); diff --git a/routes/users.js b/routes/users.js index 678f0748..41c3bfd5 100644 --- a/routes/users.js +++ b/routes/users.js @@ -327,7 +327,13 @@ export default { async password({ body: { password }, user }, res) { if (!user.password) return res.send(true); - res.send(await bcrypt.compare(password, user.password)); + + try { + if (!password) fail("password not provided"); + res.send(await bcrypt.compare(password, user.password)); + } catch (e) { + bail(res, e.message); + } }, async pin({ body: { pin }, user }, res) { @@ -362,11 +368,11 @@ export default { let u = await g(`user:${ref}`); if (typeof u === "string") u = await g(`user:${ref}`); if (u) contacts.unshift(pick(u, ["id", "profile", "username"])); - } + } await s(`${id}:contacts`, contacts); - res.send(contacts); + res.send(contacts); }, async del({ params: { username }, headers: { authorization } }, res) { @@ -425,5 +431,11 @@ export default { async superuser({ body: { username } }, res) { if (username === config.mqtt2.username) res.send({ ok: true }); else bail(res, "unauthorized"); + }, + + async verify({ body: { test } }, res) { + console.log("test", test); + res.send(test); } + }; diff --git a/templates/payments/sent.html b/templates/payments/sent.html index ae142137..087577e1 100644 --- a/templates/payments/sent.html +++ b/templates/payments/sent.html @@ -1 +1,27 @@ -hello {{username}} +<style> + @import url("https://fonts.googleapis.com/css2?family=Public+Sans:wght@400;700&display=swap"); + + body { + font-family: "Public Sans", sans-serif; + } +</style> + +<html> + <body> + <div style="display: flex; width: 100%;"> + <a href="https://coinos.io" style="margin: auto"> + <img src="https://coinos.io/images/logo.svg"> + </a> + </div> + + <div style="margin-bottom: 10px">Hello {{username}},</div> + + <div style="margin-bottom: 10px"> + Please visit <a href="{{link}}">{{link}}</a> to verify your email. + </div> + + <div style="margin-bottom: 10px">Thanks!</div> + + <div>The Coinos Team</div> + </body> +</html> |
