aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--index.js15
-rw-r--r--lib/db.js34
-rw-r--r--lib/invoices.js1
-rw-r--r--lib/lightning.js14
-rw-r--r--lib/mail.js14
-rw-r--r--lib/nostr.js88
-rw-r--r--lib/payments.js7
-rw-r--r--lib/register.js1
-rw-r--r--package.json1
-rw-r--r--pnpm-lock.yaml53
-rw-r--r--routes/lnurl.js16
-rw-r--r--routes/payments.js2
-rw-r--r--routes/users.js18
-rw-r--r--templates/payments/sent.html28
15 files changed, 269 insertions, 24 deletions
diff --git a/.gitignore b/.gitignore
index 977ad65f..0676a28b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@ compose.yml
*.flitevox
mimic
watcher.sh
+.aws
diff --git a/index.js b/index.js
index e8aa3a64..d705d59f 100644
--- a/index.js
+++ b/index.js
@@ -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"));
diff --git a/lib/db.js b/lib/db.js
index 197697a0..b790917e 100644
--- a/lib/db.js
+++ b/lib/db.js
@@ -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
+ resolution: {integrity: sha512-6YBxJDAapHSdd3bLDv6x2wRPwq4QFMUaB3HvljNBUTThDd12eSm7/3F+2lnfzx2jvM+S6Nsy0jEt9QbPqSwqRw==}
+ dev: false
+
+ resolution: {integrity: sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==}
+ dependencies:
+ '@noble/hashes': 1.3.1
+ dev: false
+
+ resolution: {integrity: sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==}
+ engines: {node: '>= 16'}
+ dev: false
+
resolution: {integrity: sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA==}
dev: false
@@ -820,6 +838,25 @@ packages:
'@redis/client': 1.5.11
dev: false
+ resolution: {integrity: sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==}
+ dev: false
+
+ resolution: {integrity: sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==}
+ dependencies:
+ '@noble/curves': 1.1.0
+ '@noble/hashes': 1.3.1
+ '@scure/base': 1.1.1
+ dev: false
+
+ 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>