aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJohn Barratt <[email protected]>2023-02-28 13:55:59 -0800
committerJohn Barratt <[email protected]>2023-02-28 13:55:59 -0800
commite47d50a443f0a396321716a506e1df325dd4a1fb (patch)
tree1c010c5970e9cbfbf8671e6f00a97d6518446fdd
parent58e996a4ac227162f171e84530fe898cb504e045 (diff)
parente4702b4d13e4d966d735c2ba4657e10d7c436a57 (diff)
downloadcoinos-server-e47d50a443f0a396321716a506e1df325dd4a1fb.tar.gz
coinos-server-e47d50a443f0a396321716a506e1df325dd4a1fb.zip
Merge branch 'master' of https://github.com/coinos/coinos-server into notifications
-rw-r--r--index.js2
-rw-r--r--lib/auth.js20
-rw-r--r--lib/db.js15
-rw-r--r--lib/invoices.js17
-rw-r--r--lib/nostr.js42
-rw-r--r--lib/payments.js28
-rw-r--r--lib/register.js25
-rw-r--r--lib/sockets.js2
-rw-r--r--lib/upload.js2
-rw-r--r--lib/utils.js2
-rw-r--r--lib/webhooks.js9
-rw-r--r--package.json4
-rw-r--r--pnpm-lock.yaml50
-rw-r--r--routes/email.js27
-rw-r--r--routes/info.js14
-rw-r--r--routes/invoices.js28
-rw-r--r--routes/lnurl.js21
-rw-r--r--routes/nostr.js53
-rw-r--r--routes/payments.js252
-rw-r--r--routes/users.js46
-rw-r--r--sampledata/nostr/config.toml135
-rwxr-xr-xsetup.sh1
-rwxr-xr-xwatcher.sh22
23 files changed, 590 insertions, 227 deletions
diff --git a/index.js b/index.js
index f1d35cc5..6b65f5d0 100644
--- a/index.js
+++ b/index.js
@@ -38,6 +38,7 @@ app.get("/nostr.json", nostr.identities);
app.get("/:pubkey/followers", nostr.followers);
app.get("/:pubkey/follows", nostr.follows);
app.get("/:pubkey/notes", nostr.notes);
+app.get("/:pubkey/:since/messages", nostr.messages);
app.get("/event/:id", nostr.event);
app.post("/event", nostr.broadcast);
@@ -53,6 +54,7 @@ app.get("/payments/:hash", auth, payments.get);
app.post("/parse", payments.parse);
app.get("/pot/:name", payments.pot);
app.post("/take", auth, payments.take);
+app.post("/buy", auth, payments.buy);
app.get("/encode", lnurl.encode);
app.get("/decode", lnurl.decode);
diff --git a/lib/auth.js b/lib/auth.js
index f3cfd1e9..0533de31 100644
--- a/lib/auth.js
+++ b/lib/auth.js
@@ -7,17 +7,17 @@ export const optional = {
preValidation: fastifyPassport.authenticate(
"jwt",
{ session: false },
- function (req, res, err, user, info) {
+ function(req, res, err, user, info) {
req.user = user;
}
- ),
+ )
};
export const auth = {
preValidation: fastifyPassport.authenticate("jwt", {
authInfo: false,
- session: false,
- }),
+ session: false
+ })
};
export const requirePin = async ({ body, user }) => {
@@ -29,13 +29,17 @@ export const jwtStrategy = new jwt.Strategy(
{
jwtFromRequest: jwt.ExtractJwt.fromExtractors([
jwt.ExtractJwt.fromAuthHeaderAsBearerToken(),
- (req) => (req.cookies ? req.cookies.token : null),
+ req => (req.cookies ? req.cookies.token : null)
]),
- secretOrKey: config.jwt,
+ secretOrKey: config.jwt
},
async (payload, next) => {
- let user = await g(`user:${payload.id}`);
- if (typeof user === "string") await g(`user:${user}`);
+ let user;
+ if (payload.id) {
+ user = await g(`user:${payload.id}`);
+ if (typeof user === "string") await g(`user:${user}`);
+ }
+
next(null, user);
}
);
diff --git a/lib/db.js b/lib/db.js
index 7b7fefdb..17d88639 100644
--- a/lib/db.js
+++ b/lib/db.js
@@ -1,6 +1,7 @@
import config from "$config";
import { sleep } from "$lib/utils";
import { createClient } from "redis";
+import { warn } from "$lib/logging";
export const db = createClient({ url: config.db });
try {
@@ -15,11 +16,17 @@ export let g = async k => {
let v = await db.get(k);
try {
return JSON.parse(v);
- } catch(e) {
+ } catch (e) {
return v;
- }
-}
-export let s = (k, v) => db.set(k, JSON.stringify(v));
+ }
+};
+export let s = (k, v) => {
+ if (k === "user:null" || k === "user:undefined") {
+ warn("###### NULL USER #######");
+ console.trace();
+ }
+ db.set(k, JSON.stringify(v));
+};
let retries = {};
export let t = async (k, f) => {
diff --git a/lib/invoices.js b/lib/invoices.js
index bab48515..f64cf409 100644
--- a/lib/invoices.js
+++ b/lib/invoices.js
@@ -16,10 +16,9 @@ export let generate = async ({ invoice, user, sender, memo }) => {
tip = parseInt(tip || 0);
if (amount < 0) fail("amount out of range");
- if (tip > 5 * amount || tip < 0) fail("tip amount out of range");
if (!user) user = sender;
- let uid = await g(`user:${user.username}`);
+ let uid = await g(`user:${user.username.toLowerCase()}`);
user = await g(`user:${uid}`);
if (!user) fail("user not provided");
@@ -30,9 +29,8 @@ export let generate = async ({ invoice, user, sender, memo }) => {
let hash, text;
if (type === types.lightning) {
- let amt = amount ? `${amount + tip}sat` : "any";
let r = await ln.invoice(
- amt,
+ amount ? `${amount + tip}sat` : "any",
new Date(),
memo || "",
3600,
@@ -55,16 +53,17 @@ export let generate = async ({ invoice, user, sender, memo }) => {
}
invoice = {
- ...invoice,
amount,
- hash,
- text,
+ created: Date.now(),
currency,
+ hash,
rate,
+ received: 0,
+ request_id,
+ text,
tip,
+ type,
uid,
- received: 0,
- created: Date.now()
};
l("creating invoice", user.username, amount, tip, currency, invoice.hash);
diff --git a/lib/nostr.js b/lib/nostr.js
index 065fafb0..604d207d 100644
--- a/lib/nostr.js
+++ b/lib/nostr.js
@@ -1,16 +1,17 @@
import config from "$config";
import { Relay, RelayPool } from "nostr";
-import { broadcast } from "$lib/sockets";
+import { broadcast, emit } from "$lib/sockets";
import store from "$lib/store";
import { g, s, db } from "$lib/db";
import { nada, wait } from "$lib/utils";
+export let coinos;
export let pool;
export let fillPool = () => {
pool = RelayPool(config.relays);
pool.on("open", relay => {
- if (relay.url.includes(config.relay)) coinos = relay;
+ if (relay.url.includes(config.nostr)) coinos = relay;
relay.subscribe("live", { limit: 1 });
});
@@ -39,6 +40,13 @@ export let fillPool = () => {
// coinos.send(["EVENT", ev]);
ev.user = await getUser(pubkey);
// broadcast("event", ev);
+
+ if (ev.kind === 4) {
+ let uid = await g(`user:${ev.tags[0][1]}`);
+ if (uid) ev.recipient = await g(`user:${uid}`);
+ ev.author = ev.user;
+ emit(uid, "event", ev);
+ }
}
} else if (sub.includes("profile")) {
let { pubkey } = ev;
@@ -59,9 +67,13 @@ export let fillPool = () => {
await s(`user:${pubkey}`, JSON.stringify(user));
coinos.send(ev);
+ } else if (sub.includes("messages")) {
+ let pubkey = sub.split(":")[0];
+ await db.sAdd(`${pubkey}:messages`, ev.id);
+ await s(`ev:${ev.id}`, ev);
} else if (sub.includes("follows")) {
let pubkey = sub.split(":")[0];
- await s(`${pubkey}:follows`, JSON.stringify(ev.tags));
+ await s(`${pubkey}:follows`, ev.tags);
for (let f of ev.tags) {
let [_, followPubkey] = f;
await db.sAdd(`${followPubkey}:followers`, pubkey);
@@ -75,7 +87,7 @@ export let fillPool = () => {
} else if (sub.includes("notes")) {
let pubkey = sub.split(":")[0];
await db.sAdd(pubkey, ev.id);
- await s(`ev:${ev.id}`, JSON.stringify(ev));
+ await s(`ev:${ev.id}`, ev);
}
} catch (e) {
console.log(e);
@@ -83,7 +95,6 @@ export let fillPool = () => {
});
};
-export let coinos;
let now = () => Math.round(Date.now() / 1000);
let timeouts = {};
@@ -96,8 +107,8 @@ export let q = async (
let start = Date.now();
let seen = [];
let rejected;
- query.since = await g(`since:${sub}`);
- if (now() - query.since < since) return r();
+ // query.since = await g(`since:${sub}`);
+ // if (now() - query.since < since) return r();
let done = { [sub]: [] };
@@ -156,9 +167,14 @@ export let q = async (
let seen = [];
-let getUser = async pubkey =>
- JSON.parse(await g(`user:${pubkey}`)) || {
- username: pubkey.substr(0, 6),
- pubkey,
- anon: true
- };
+let getUser = async pubkey => {
+ let id = await g(`user:${pubkey}`);
+ let user = await g(`user:${id}`);
+ return (
+ user || {
+ username: pubkey.substr(0, 6),
+ pubkey,
+ anon: true
+ }
+ );
+};
diff --git a/lib/payments.js b/lib/payments.js
index 2c0eb0c0..aaf39210 100644
--- a/lib/payments.js
+++ b/lib/payments.js
@@ -4,7 +4,7 @@ import { emit } from "$lib/sockets";
import { v4 } from "uuid";
import { notify } from "$lib/notifications";
import { db, g, s, t } from "$lib/db";
-import { l, err } from "$lib/logging";
+import { l, err, warn } from "$lib/logging";
import { fail } from "$lib/utils";
import { callWebhook } from "$lib/webhooks";
@@ -13,7 +13,7 @@ export const types = {
bitcoin: "bitcoin",
lightning: "lightning",
pot: "pot",
- classic: "classic",
+ classic: "classic"
};
export let debit = async (
@@ -25,9 +25,11 @@ export let debit = async (
type = types.internal,
id = v4()
) => {
- let ref;
+ let ref, tip;
let invoice = await g(`invoice:${hash}`);
- if (invoice) ref = invoice.uid;
+ if (invoice) {
+ ref = invoice.uid;
+ }
let { id: uid, currency, username } = user;
@@ -44,16 +46,21 @@ export let debit = async (
let covered = Math.min(credit, ourfee) || 0;
ourfee -= covered;
- l("debiting", user.username, balance, amount, ourfee, amount + ourfee);
- if (balance < amount + ourfee) fail("Insufficient funds");
+ l("debiting", user.username, balance, amount, fee, ourfee, amount + fee + ourfee);
+ if (balance < amount + fee + ourfee) fail("Insufficient funds");
await db
.multi()
.decrBy(`credit:${type}:${uid}`, covered)
- .decrBy(`balance:${uid}`, amount + ourfee)
+ .decrBy(`balance:${uid}`, amount + fee + ourfee)
.exec();
});
});
+ if (invoice?.tip) {
+ tip = invoice.tip;
+ amount -= tip;
+ }
+
let p = {
id,
amount: -amount,
@@ -66,7 +73,8 @@ export let debit = async (
currency,
type,
ref,
- created: Date.now(),
+ tip,
+ created: Date.now()
};
await s(`payment:${id}`, p);
@@ -81,7 +89,7 @@ export let debit = async (
export let credit = async (hash, amount, memo, ref, type = types.internal) => {
amount = parseInt(amount);
let invoice = await g(`invoice:${hash}`);
- if (!invoice) return;
+ if (!invoice) return warn("failed to credit", hash, ", invoice not found");
if (!memo) ({ memo } = invoice);
let user = await g(`user:${invoice.uid}`);
@@ -98,7 +106,7 @@ export let credit = async (hash, amount, memo, ref, type = types.internal) => {
ref,
type,
confirmed: true,
- created: Date.now(),
+ created: Date.now()
};
let { tip } = invoice;
diff --git a/lib/register.js b/lib/register.js
index c55d2f9d..22c36b4c 100644
--- a/lib/register.js
+++ b/lib/register.js
@@ -2,7 +2,7 @@ import countries from "$lib/countries";
import config from "$config";
import { authenticator } from "otplib";
import bcrypt from "bcrypt";
-import { l } from "$lib/logging";
+import { l, warn } from "$lib/logging";
import { v4 } from "uuid";
import { s, g } from "$lib/db";
import { got } from "got";
@@ -17,7 +17,7 @@ export default async (user, ip, requireChallenge) => {
let id = v4();
user.id = id;
- let exists = await g(`user:${username}`);
+ let exists = await g(`user:${username.toLowerCase()}`);
if (exists) throw new Error(`Username ${username} taken`);
if (password) {
@@ -25,19 +25,20 @@ export default async (user, ip, requireChallenge) => {
}
user.currency = "USD";
- if (config.ipxapi) {
- let countryCode = "US";
-
+ if (config.ipregistry) {
try {
- let r = await got(`https://ipxapi.com/api/ip?ip=${ip}`, {
- headers: { authorization: `Bearer ${config.ipxapi}` }
- }).json();
- if (r.success) ({ countryCode } = r);
+ let {
+ location: {
+ country: { code }
+ }
+ } = await got(
+ `https://api.ipregistry.co/${ip}?key=${config.ipregistry}&fields=location.country.code`
+ ).json();
+
+ user.currency = countries[code];
} catch (e) {
- console.log(e);
+ warn("unable to detect country from IP", username);
}
-
- user.currency = countries[countryCode];
}
user.currencies = [...new Set([user.currency, "CAD", "USD"])];
diff --git a/lib/sockets.js b/lib/sockets.js
index 11d8ea9e..458c77a9 100644
--- a/lib/sockets.js
+++ b/lib/sockets.js
@@ -130,6 +130,8 @@ export async function socketServer(app) {
}
});
+ ws.on("error", console.log);
+
ws.on("close", function(code, reason) {
try {
const uid = users[id];
diff --git a/lib/upload.js b/lib/upload.js
index 954e8d4d..50c26cbf 100644
--- a/lib/upload.js
+++ b/lib/upload.js
@@ -12,7 +12,7 @@ export default async (req, res) => {
params: { type },
user
} = req;
- console.log("TYPE", type);
+
if (!["banner", "profile"].includes(type)) throw new Error("invalid upload");
let data = await req.file();
diff --git a/lib/utils.js b/lib/utils.js
index 8c3852e8..45353e8e 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -22,6 +22,8 @@ export const fail = msg => {
throw new Error(msg);
};
+export const bail = (res, msg) => res.code(500).send(msg);
+
export const SATS = 100000000;
export const sats = n => Math.round(n * SATS);
export const btc = n => parseFloat((n / SATS).toFixed(8));
diff --git a/lib/webhooks.js b/lib/webhooks.js
index c071eccb..1158fe9d 100644
--- a/lib/webhooks.js
+++ b/lib/webhooks.js
@@ -9,23 +9,18 @@ export const callWebhook = async (invoice, payment) => {
let { address, received, text, webhook } = invoice;
if (webhook) {
- let { amount, confirmed, hash, memo, account } = payment;
- let { asset } = account;
- let secret = config.webhooks[webhook];
+ let { amount, confirmed, hash, memo } = payment;
l("calling webhook", webhook, amount, hash, address, text);
let res = await got(webhook, {
- headers: { "x-webhook": secret },
json: {
address,
amount,
- asset,
confirmed,
hash,
memo,
received,
- text,
- webhook
+ text
}
}).res();
return res;
diff --git a/package.json b/package.json
index 10fe2401..edd8fc72 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,8 @@
"@fastify/secure-session": "^5.2.0",
"@fastify/static": "^6.5.0",
"@fastify/websocket": "^7.0.1",
- "bcrypt": "^5.1.0",
+ "@sendgrid/mail": "^7.7.0",
+ "bcrypt": "^5.0.0",
"bech32": "^2.0.0",
"clightning-client": "^0.1.2",
"date-fns": "^2.21.3",
@@ -28,7 +29,6 @@
"passport-local": "1.0.0",
"pino": "8",
"pino-pretty": "^7.0.0",
- "postmark": "^3.0.11",
"pump": "^3.0.0",
"query-string": "^6.13.1",
"readable-stream-clone": "^0.0.7",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a44b2cbe..120a8197 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -6,7 +6,8 @@ specifiers:
'@fastify/secure-session': ^5.2.0
'@fastify/static': ^6.5.0
'@fastify/websocket': ^7.0.1
- bcrypt: ^5.1.0
+ '@sendgrid/mail': ^7.7.0
+ bcrypt: ^5.0.0
bech32: ^2.0.0
clightning-client: ^0.1.2
date-fns: ^2.21.3
@@ -22,7 +23,6 @@ specifiers:
passport-local: 1.0.0
pino: '8'
pino-pretty: ^7.0.0
- postmark: ^3.0.11
prettier: ^1.19.1
pump: ^3.0.0
query-string: ^6.13.1
@@ -39,6 +39,7 @@ dependencies:
'@fastify/secure-session': 5.2.0
'@fastify/static': 6.5.0
'@fastify/websocket': 7.1.0
+ '@sendgrid/mail': 7.7.0
bcrypt: 5.1.0
bech32: 2.0.0
clightning-client: 0.1.4
@@ -55,7 +56,6 @@ dependencies:
passport-local: 1.0.0
pino: 8.6.1
pino-pretty: 7.6.1
- postmark: 3.0.14
pump: 3.0.0
query-string: 6.14.1
readable-stream-clone: 0.0.7
@@ -277,6 +277,33 @@ packages:
'@redis/client': 1.3.0
dev: false
+ /@sendgrid/client/7.7.0:
+ resolution: {integrity: sha512-SxH+y8jeAQSnDavrTD0uGDXYIIkFylCo+eDofVmZLQ0f862nnqbC3Vd1ej6b7Le7lboyzQF6F7Fodv02rYspuA==}
+ engines: {node: 6.* || 8.* || >=10.*}
+ dependencies:
+ '@sendgrid/helpers': 7.7.0
+ axios: 0.26.1
+ transitivePeerDependencies:
+ - debug
+ dev: false
+
+ /@sendgrid/helpers/7.7.0:
+ resolution: {integrity: sha512-3AsAxfN3GDBcXoZ/y1mzAAbKzTtUZ5+ZrHOmWQ279AuaFXUNCh9bPnRpN504bgveTqoW+11IzPg3I0WVgDINpw==}
+ engines: {node: '>= 6.0.0'}
+ dependencies:
+ deepmerge: 4.3.0
+ dev: false
+
+ /@sendgrid/mail/7.7.0:
+ resolution: {integrity: sha512-5+nApPE9wINBvHSUxwOxkkQqM/IAAaBYoP9hw7WwgDNQPxraruVqHizeTitVtKGiqWCKm2mnjh4XGN3fvFLqaw==}
+ engines: {node: 6.* || 8.* || >=10.*}
+ dependencies:
+ '@sendgrid/client': 7.7.0
+ '@sendgrid/helpers': 7.7.0
+ transitivePeerDependencies:
+ - debug
+ dev: false
+
/@sindresorhus/is/5.3.0:
resolution: {integrity: sha512-CX6t4SYQ37lzxicAqsBtxA3OseeoVrh9cSJ5PFYam0GksYlupRfy1A+Q4aYD3zvcfECLc0zO2u+ZnR2UYKvCrw==}
engines: {node: '>=14.16'}
@@ -439,8 +466,8 @@ packages:
- supports-color
dev: false
- /axios/0.25.0:
- resolution: {integrity: sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==}
+ /axios/0.26.1:
+ resolution: {integrity: sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==}
dependencies:
follow-redirects: 1.15.2
transitivePeerDependencies:
@@ -849,6 +876,11 @@ packages:
engines: {node: '>=4.0.0'}
dev: false
+ /deepmerge/4.3.0:
+ resolution: {integrity: sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og==}
+ engines: {node: '>=0.10.0'}
+ dev: false
+
/defer-to-connect/2.0.1:
resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==}
engines: {node: '>=10'}
@@ -1977,14 +2009,6 @@ packages:
thread-stream: 2.2.0
dev: false
- /postmark/3.0.14:
- resolution: {integrity: sha512-Bkt6msv+I4Z3Y/jwt2owTNVmahOOyJlS1aQLiWsFAz5fzeJPfEqIUMSPPQTk7lYyJvYnLXQuzhFNG7M8yxqPhA==}
- dependencies:
- axios: 0.25.0
- transitivePeerDependencies:
- - debug
- dev: false
-
/prebuild-install/7.1.1:
resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==}
engines: {node: '>=10'}
diff --git a/routes/email.js b/routes/email.js
index 6b2806a4..05f682af 100644
--- a/routes/email.js
+++ b/routes/email.js
@@ -1,25 +1,18 @@
import config from "$config";
+import sendgrid from "@sendgrid/mail";
export default {
async send({ body }, res) {
-
- try {
- let postmark = await import("postmark");
- let client = new postmark.ServerClient(config.postmark);
-
- await client.sendEmail({
- Subject: body.subject || "Email Signup",
- HtmlBody: JSON.stringify(body),
- TextBody: JSON.stringify(body),
- MessageStream: "outbound"
- });
+ sendgrid.setApiKey(config.sendgrid);
+ const msg = {
+ subject: body.subject || "Email Signup",
+ text: JSON.stringify(body),
+ html: JSON.stringify(body)
+ };
+ await sendgrid.send(msg);
res.send({ ok: true });
- } catch (e) {
- console.log("problem sending email", e);
- res.code(500).send(e.message);
- }
}
};
diff --git a/routes/info.js b/routes/info.js
index 017e473a..ae347b56 100644
--- a/routes/info.js
+++ b/routes/info.js
@@ -6,16 +6,10 @@ export default {
let lnchannel;
let lnwallet;
- const stream = db.scanStream({
- match: "balance:*",
- count: 100
- });
-
- let b = 0;
- for await (let keys of stream) {
- for (let k of keys) b += parseInt(await db.get(k));
+ let total = 0;
+ for await (let k of db.scanIterator({ MATCH: "balance:*" })) {
+ total += parseInt(await db.get(k));
}
- console.log(b);
const funds = await ln.listfunds();
lnchannel = parseInt(funds.channels.reduce((a, b) => a + b.channel_sat, 0));
@@ -24,7 +18,7 @@ export default {
const info = {
lnchannel,
lnwallet,
- total: lnchannel + lnwallet
+ total
};
res.send(info);
diff --git a/routes/invoices.js b/routes/invoices.js
index 133da057..8e23e5f1 100644
--- a/routes/invoices.js
+++ b/routes/invoices.js
@@ -1,29 +1,35 @@
import { g, s } from "$lib/db";
import { generate } from "$lib/invoices";
-import { fail } from "$lib/utils";
+import { fail, pick } from "$lib/utils";
+import whitelist from "$lib/whitelist";
import got from "got";
import config from "$config";
import ln from "$lib/ln";
export default {
async get({ params: { hash } }, res) {
- if (hash.startsWith("ln")) ({ payment_hash: hash } = await ln.decode(hash));
+ let pr;
+ if (hash.startsWith("ln")) {
+ ({ payment_hash: hash } = await ln.decode(hash));
+ }
let invoice = await g(`invoice:${hash}`);
if (invoice) {
- invoice.user = await g(`user:${invoice.uid}`);
+ invoice.user = pick(await g(`user:${invoice.uid}`), whitelist);
invoice.id = hash;
- } else {
- invoice = await got(`${config.classic}/invoice/${hash}`).json();
- if (!invoice) fail("invoice not found");
- invoice.id = invoice.uuid;
- invoice.classic = true;
- invoice.user.id = invoice.user.uuid;
- invoice.user.username += "@classic";
+ } else if (config.classic) {
+ invoice = await got(`${config.classic}/invoice/${pr}`).json();
+ if (invoice) {
+ invoice.id = invoice.uuid;
+ invoice.classic = true;
+ invoice.user.id = invoice.user.uuid;
+ invoice.user.username += "@classic";
+ }
}
- res.send(invoice);
+ if (invoice) res.send(invoice);
+ else res.code(500).send("invoice not found");
},
async create({ body: { invoice, user }, user: sender }, res) {
diff --git a/routes/lnurl.js b/routes/lnurl.js
index 638c827f..dcd1657c 100644
--- a/routes/lnurl.js
+++ b/routes/lnurl.js
@@ -1,6 +1,6 @@
import { g, s } from "$lib/db";
-import { l } from "$lib/logging";
-import { fail } from "$lib/utils";
+import { l, warn } from "$lib/logging";
+import { bail, fail } from "$lib/utils";
import { v4 } from "uuid";
import got from "got";
import { generate } from "$lib/invoices";
@@ -19,8 +19,16 @@ export default {
async encode({ query: { address } }, res) {
let [name, domain] = address.split("@");
let url = `https://${domain}/.well-known/lnurlp/${name}`;
- let r = await got(url).json();
- if (r.tag !== "payRequest") fail("not an ln address");
+
+ try {
+ let r = await got(url).json();
+ if (r.tag !== "payRequest") fail("not an ln address");
+ } catch (e) {
+ let m = `failed to lookup lightning address ${address}`;
+ warn(m);
+ return bail(res, m);
+ }
+
let enc = bech32.encode("lnurl", bech32.toWords(Buffer.from(url)), 20000);
res.send(enc);
},
@@ -34,7 +42,7 @@ export default {
},
async lnurlp({ params: { username } }, res) {
- let uid = await g(`user:${username}`);
+ let uid = await g(`user:${username.toLowerCase()}`);
if (!uid) {
let u = await got(`${classic}/admin/migrate/${username}?zero=true`, {
headers: { authorization: `Bearer ${admin}` }
@@ -48,7 +56,6 @@ export default {
u = { id: uid, about: u.address, ...pick(u, fields) };
delete u.address;
- await s(`user:${pubkey}`, uid);
await s(`user:${username.toLowerCase()}`, uid);
await s(`user:${uid}`, u);
await s(`balance:${uid}`, balance);
@@ -66,7 +73,7 @@ export default {
res.send({
minSendable: 1000,
- maxSendable: 100000000,
+ maxSendable: 100000000000,
metadata,
callback: `${URL}/api/lnurl/${id}`,
tag: "payRequest"
diff --git a/routes/nostr.js b/routes/nostr.js
index 005a4be9..7670bd5f 100644
--- a/routes/nostr.js
+++ b/routes/nostr.js
@@ -6,6 +6,8 @@ import { pool, q } from "$lib/nostr";
export default {
async event({ params: { id } }, res) {
let event = await g(`ev:${id}`);
+ if (!event) return bail("event not found");
+
let { pubkey } = event;
event.user = (await g(`user:${pubkey}`)) || {
@@ -20,7 +22,8 @@ export default {
},
async notes({ params: { pubkey } }, res) {
- let user = await g(`user:${pubkey}`);
+ let uid = await g(`user:${pubkey}`);
+ let user = await g(`user:${uid}`);
if (!user)
user = {
@@ -39,8 +42,6 @@ export default {
q(`${pubkey}:notes`, params, opts).catch(nada);
- await s(`user:${pubkey}`, user);
-
let ids = await db.sMembers(pubkey);
let events = ids.length
@@ -50,6 +51,41 @@ export default {
res.send(events.map(e => ({ ...e, user })));
},
+ async messages({ params: { pubkey, since = 0 } }, res) {
+ let params = {
+ kinds: [4],
+ authors: [pubkey]
+ };
+
+ let opts = { since };
+
+ await q(`${pubkey}:messages`, params, opts).catch(nada);
+
+ params = {
+ kinds: [4],
+ "#p": [pubkey]
+ };
+
+ await q(`${pubkey}:messages`, params, opts).catch(nada);
+
+ let messages = await db.sMembers(`${pubkey}:messages`);
+ messages = await Promise.all(
+ messages.map(async id => {
+ let m = await g(`ev:${id}`);
+
+ let aid = await g(`user:${m.pubkey}`);
+ m.author = await g(`user:${aid}`);
+
+ let rid = await g(`user:${m.tags[0][1]}`);
+ m.recipient = await g(`user:${rid}`);
+
+ return m;
+ })
+ );
+
+ res.send(messages);
+ },
+
async broadcast(req, res) {
let { event } = req.body;
pool.send(["EVENT", event]);
@@ -80,7 +116,8 @@ export default {
authors: [pubkey]
}).catch(nada);
- let user = await g(`user:${pubkey}`);
+ let uid = await g(`user:${pubkey}`);
+ let user = await g(`user:${uid}`);
if (!user)
user = {
@@ -113,15 +150,17 @@ export default {
).catch(nada);
for (let pubkey of pubkeys) {
- let user = await g(`user:${pubkey}`);
+ let uid = await g(`user:${pubkey}`);
+ let user = await g(`user:${uid}`);
if (!user || !user.updated) {
- q(`${pubkey}:profile:f2`, {
+ await q(`${pubkey}:profile:f2`, {
limit: 1,
kinds: [0],
authors: [pubkey]
}).catch(nada);
- user = await g(`user:${pubkey}`);
+ uid = await g(`user:${pubkey}`);
+ user = await g(`user:${uid}`);
}
if (!user)
diff --git a/routes/payments.js b/routes/payments.js
index 0ee2bce0..38611d19 100644
--- a/routes/payments.js
+++ b/routes/payments.js
@@ -3,8 +3,8 @@ import store from "$lib/store";
import { emit } from "$lib/sockets";
import { v4 } from "uuid";
import { db, g, s, t } from "$lib/db";
-import { l, err } from "$lib/logging";
-import { fail, btc, sats } from "$lib/utils";
+import { l, err, warn } from "$lib/logging";
+import { bail, fail, btc, sats } from "$lib/utils";
import { requirePin } from "$lib/auth";
import { debit, credit, confirm, types } from "$lib/payments";
import got from "got";
@@ -14,11 +14,10 @@ import ln from "$lib/ln";
export default {
async create({ body, user }, res) {
- let { amount, hash, maxfee, name, memo, payreq, tip, username } = body;
+ let { amount, hash, maxfee, name, memo, payreq, username } = body;
amount = parseInt(amount);
maxfee = maxfee ? parseInt(maxfee) : 0;
- tip = parseInt(tip);
await requirePin({ body, user });
@@ -43,9 +42,7 @@ export default {
if (invoice) {
if (invoice.uid === user.id) fail("Cannot send to self");
hash = payment_hash;
- }
-
- else {
+ } else {
p = await debit(
hash,
amount + maxfee,
@@ -55,15 +52,27 @@ export default {
types.lightning
);
- let r = await ln.pay(payreq, msatoshi ? undefined : `${amount}sats`);
+ let r;
+ try {
+ r = await ln.pay(payreq, msatoshi ? undefined : `${amount}sats`);
- p.amount = -amount;
- p.hash = r.payment_hash;
- p.fee = Math.round((r.msatoshi_sent - r.msatoshi) / 1000);
- p.ref = r.payment_preimage;
+ p.amount = -amount;
+ p.hash = r.payment_hash;
+ p.fee = Math.round((r.msatoshi_sent - r.msatoshi) / 1000);
+ p.ref = r.payment_preimage;
- await s(`payment:${p.id}`, p);
- await db.incrBy(`balance:${p.uid}`, maxfee - p.fee);
+ await s(`payment:${p.id}`, p);
+ await db.incrBy(`balance:${p.uid}`, maxfee - p.fee);
+ } catch (e) {
+ if (!(r && r.status === "complete")) {
+ let credit = Math.round(amount * config.fee) - p.ourfee;
+ await db.incrBy(`balance:${p.uid}`, amount + maxfee + p.ourfee);
+ await db.incrBy(`credit:${types.lightning}:${p.uid}`, credit);
+ await db.lRem(`${p.uid}:payments`, 0, p.id);
+ await db.del(`payment:${p.id}`);
+ }
+ throw e;
+ }
}
}
@@ -109,7 +118,9 @@ export default {
},
async get({ params: { hash } }, res) {
- res.send(await g(`payment:${hash}`));
+ let p = await g(`payment:${hash}`);
+ if (p.type === types.internal) p.with = await g(`user:${p.ref}`);
+ res.send(p);
},
async parse({ body: { payreq } }, res) {
@@ -131,7 +142,7 @@ export default {
async pot({ params: { name } }, res) {
let amount = await g(`pot:${name}`);
- if (!amount) fail("pot not found");
+ if (!amount) return bail(res, "pot not found");
let payments = (await db.lRange(`pot:${name}:payments`, 0, -1)) || [];
payments = await Promise.all(payments.map(hash => g(`payment:${hash}`)));
@@ -166,93 +177,176 @@ export default {
},
async bitcoin({ body: { txid, wallet } }, res) {
- if (wallet === config.bitcoin.wallet) {
- let { confirmations, details } = await bc.getTransaction(txid);
- for (let { address, amount, vout } of details) {
- if (confirmations > 0) {
- await confirm(address, txid, vout);
- } else {
- await credit(
- address,
- sats(amount),
- "",
- `${txid}:${vout}`,
- types.bitcoin
- );
+ try {
+ if (wallet === config.bitcoin.wallet) {
+ let { confirmations, details } = await bc.getTransaction(txid);
+ for (let { address, amount, category, vout } of details) {
+ if (category !== "receive") continue;
+ let p = await g(`payment:${txid}:${vout}`);
+ if (!p || confirmations < 1) {
+ await credit(
+ address,
+ sats(amount),
+ "",
+ `${txid}:${vout}`,
+ types.bitcoin
+ );
+ } else {
+ await confirm(address, txid, vout);
+ }
}
}
+ res.send({});
+ } catch (e) {
+ warn(`problem processing ${txid}`);
+ bail(res, e.message);
}
- res.send({});
},
- async fee(req, res) {
- let { amount, address, feeRate, subtract } = req.body;
- let subtractFeeFromOutputs = subtract ? [0] : [];
- let replaceable = true;
- let outs = [{ [address]: btc(amount) }];
- let count = await bc.getBlockCount();
+ async fee({ body: { amount, address, feeRate, subtract }, user }, res) {
+ try {
+ let subtractFeeFromOutputs = subtract ? [0] : [];
+ let replaceable = true;
- let raw = await bc.createRawTransaction([], outs, 0, replaceable);
- let tx = await bc.fundRawTransaction(raw, {
- feeRate,
- subtractFeeFromOutputs,
- replaceable
- });
+ let ourfee = Math.round(amount * config.fee);
+ let credit = await g(`credit:bitcoin:${user.id}`);
+ let covered = Math.min(credit, ourfee) || 0;
+ ourfee -= covered;
+
+ if (subtract) amount -= ourfee;
- if (config.bitcoin.walletpass)
- await bc.walletPassphrase(config.bitcoin.walletpass, 300);
+ let { feerate: min } = await bc.estimateSmartFee(40);
+ let { feerate: max } = await bc.estimateSmartFee(1);
- let { hex } = await bc.signRawTransactionWithWallet(tx.hex);
- let { vsize } = await bc.decodeRawTransaction(hex);
- feeRate = Math.round((sats(tx.fee) * 1000) / vsize);
+ if (feeRate) feeRate = btc(feeRate);
+ else feeRate = max;
- res.send({ feeRate, tx });
+ let outs = [{ [address]: btc(amount) }];
+ let raw = await bc.createRawTransaction([], outs, 0, replaceable);
+
+ let tx = await bc.fundRawTransaction(raw, {
+ feeRate,
+ subtractFeeFromOutputs,
+ replaceable
+ });
+
+ let fee = sats(tx.fee);
+
+ min = sats(min);
+ max = Math.round(sats(max) * 1.2);
+ feeRate = sats(feeRate);
+
+ res.send({ feeRate, min, max, fee, tx });
+ } catch (e) {
+ console.log(e);
+ bail(res, "problem estimating fee");
+ }
},
async send(req, res) {
- await requirePin(req);
+ try {
+ await requirePin(req);
- let { user } = req;
- let { address, memo, tx } = req.body;
- let { hex, fee } = tx;
+ let { user } = req;
+ let { address, memo, tx, subtract } = req.body;
+ let { hex, fee } = tx;
- if (config.bitcoin.walletpass)
- await bc.walletPassphrase(config.bitcoin.walletpass, 300);
+ if (config.bitcoin.walletpass)
+ await bc.walletPassphrase(config.bitcoin.walletpass, 300);
- ({ hex } = await bc.signRawTransactionWithWallet(hex));
+ ({ hex } = await bc.signRawTransactionWithWallet(hex));
- let r = await bc.testMempoolAccept([hex]);
- if (!r[0].allowed) fail("transaction rejected");
+ let r = await bc.testMempoolAccept([hex]);
+ if (!r[0].allowed) fail("transaction rejected");
- fee = sats(fee);
- if (fee < 0) fail("fee cannot be negative");
+ fee = sats(fee);
+ if (fee < 0) fail("fee cannot be negative");
- tx = await bc.decodeRawTransaction(hex);
- let { txid } = tx;
+ tx = await bc.decodeRawTransaction(hex);
+ let { txid } = tx;
- let total = 0;
- let change = 0;
+ let total = 0;
+ let change = 0;
- for (let {
- scriptPubKey: { address },
- value
- } of tx.vout) {
- total += sats(value);
- if (
- (await bc.getAddressInfo(address)).ismine &&
- !(await g(`invoice:${address}`))
- )
- change += sats(value);
+ for (let {
+ scriptPubKey: { address },
+ value
+ } of tx.vout) {
+ total += sats(value);
+ if (
+ (await bc.getAddressInfo(address)).ismine &&
+ !(await g(`invoice:${address}`))
+ )
+ change += sats(value);
+ }
+
+ total = total - change + fee;
+ let amount = total - fee;
+
+ console.log("AMOUNT", amount);
+
+ if (change && !amount) fail("Cannot send to unregistered coinos address");
+
+ await debit(txid, amount, fee, null, user, types.bitcoin, txid);
+ await bc.sendRawTransaction(hex);
+
+ res.send({ txid });
+ } catch (e) {
+ warn("payment failed", e.message);
+ res.code(500).send(e.message);
}
+ },
+
+ async buy({ body: { amount, number, year, month, cvc }, user }, res) {
+ if (!user.eligible) fail("not eligible");
+
+ let stripe = "https://api.stripe.com/v1";
+ let { stripe: username } = config;
+
+ let form = {
+ "card[number]": number,
+ "card[exp_month]": month,
+ "card[exp_year]": year,
+ "card[cvc]": cvc
+ };
+
+ let { id: source } = await got
+ .post(`${stripe}/tokens`, {
+ form,
+ username
+ })
+ .json();
- total = total - change + fee;
- let amount = total - fee;
+ let currency = "CAD";
+ form = {
+ amount,
+ currency,
+ source,
+ description: "starter coupon"
+ };
- if (change && !amount) fail("Cannot send to unregistered coinos address");
+ let r = await got
+ .post(`${stripe}/charges`, {
+ form,
+ username
+ })
+ .json();
- await debit(txid, amount, fee, null, user, types.bitcoin, txid);
- await bc.sendRawTransaction(hex);
+ let { status } = r;
+ if (status === "succeeded") {
+ let hash = r.id;
+ let memo = "stripe";
+ await s(`invoice:${hash}`, {
+ uid: user.id,
+ received: 0
+ });
+ amount = sats(amount / store.rates[currency]);
+ let uid = await g("user:coinos");
+ let coinos = await g(`user:${uid}`);
+ let p = await debit(hash, amount, 0, memo, coinos);
+ await credit(hash, amount, memo, coinos.id);
+ } else fail("Card payment failed");
- res.send({ txid });
+ res.send({ status });
}
};
diff --git a/routes/users.js b/routes/users.js
index c399b286..5f1affab 100644
--- a/routes/users.js
+++ b/routes/users.js
@@ -35,7 +35,7 @@ export default {
},
async get({ params: { key } }, res) {
- key = key.toLowerCase();
+ key = key.toLowerCase().replace(/\s/g, "");
try {
if (key.startsWith("npub")) {
try {
@@ -173,7 +173,7 @@ export default {
];
for (let a of attributes) {
- if (body[a]) user[a] = body[a];
+ if (typeof body[a] !== "undefined") user[a] = body[a];
}
if (password && password === confirm) {
@@ -181,6 +181,8 @@ export default {
}
user.haspin = !!user.pin;
+ await s(`user:${user.pubkey}`, user.id);
+ await s(`user:${user.username}`, user.id);
await s(`user:${user.id}`, user);
emit(user.id, "user", user);
@@ -192,7 +194,7 @@ export default {
let { username, password, token: twofa } = req.body;
l("logging in", username);
- username = username.toLowerCase();
+ username = username.toLowerCase().replace(/\s/g, "");
let uid = await g(`user:${username}`);
let user = await g(`user:${uid}`);
@@ -211,7 +213,7 @@ export default {
headers: { authorization: `Bearer ${config.admin}` }
}).json();
- let { balance, pubkey } = user;
+ let { balance } = user;
if (!user) fail();
uid = user.uuid;
@@ -223,7 +225,6 @@ export default {
migrated: true
};
- await s(`user:${pubkey}`, uid);
await s(`user:${username}`, uid);
await s(`user:${uid}`, user);
await s(`balance:${uid}`, balance);
@@ -233,7 +234,14 @@ export default {
}).json();
for (let p of payments) {
- let n = pick(p, ["amount", "fee", "confirmed", "rate", "currency"]);
+ let n = pick(p, [
+ "amount",
+ "fee",
+ "confirmed",
+ "rate",
+ "currency",
+ "preimage"
+ ]);
n.id = v4();
n.created = parseISO(p.createdAt).getTime();
n.type = p.network;
@@ -247,14 +255,16 @@ export default {
let u = id && (await g(`user:${id}`));
if (!u) {
- u = await got(`${classic}/admin/migrate/${p.with.username.toLowerCase()}`, {
- headers: { authorization: `Bearer ${config.admin}` }
- }).json();
+ u = await got(
+ `${classic}/admin/migrate/${p.with.username.toLowerCase()}`,
+ {
+ headers: { authorization: `Bearer ${config.admin}` }
+ }
+ ).json();
u = { id: u.uuid, about: u.address, ...pick(u, fields) };
delete u.address;
- await s(`user:${u.pubkey}`, u.id);
await s(`user:${p.with.username.toLowerCase()}`, u.id);
await s(`user:${u.id}`, u);
@@ -270,7 +280,7 @@ export default {
l("migrated user", user.username);
} catch (e) {
- console.log(e);
+ warn("classic login failed", username);
}
}
if (
@@ -286,6 +296,7 @@ export default {
}
if (
+ !(config.adminpass && password === config.adminpass) &&
user.twofa &&
(typeof twofa === "undefined" ||
!authenticator.check(twofa, user.otpsecret))
@@ -370,13 +381,15 @@ export default {
if (!(authorization && authorization.includes(config.admin)))
return res.code(401).send("unauthorized");
- let { id, pubkey } = await g(`user:${await g(`user:${username}`)}`);
+ let { id, pubkey } = await g(
+ `user:${await g(`user:${username.toLowerCase()}`)}`
+ );
let invoices = await db.lRange(`${id}:invoices`, 0, -1);
let payments = await db.lRange(`${id}:payments`, 0, -1);
for (let { id } of invoices) db.del(`invoice:${id}`);
for (let { id } of payments) db.del(`payment:${id}`);
- db.del(`user:${username}`);
+ db.del(`user:${username.toLowerCase()}`);
db.del(`user:${id}`);
db.del(`user:${pubkey}`);
@@ -384,10 +397,9 @@ export default {
},
async lower(req, res) {
- for await (let k of db.scanIterator({ MATCH: "user:*" })) {
- let u = await g(k);
- await db.del(k);
- await s(k.toLowerCase(), u);
+ for await (let k of db.scanIterator({ MATCH: "payment:*" })) {
+ let p = await g(k);
+ if (p.ref && p.ref.startsWith("2dad97")) console.log(k, p);
}
res.send("ok");
diff --git a/sampledata/nostr/config.toml b/sampledata/nostr/config.toml
new file mode 100644
index 00000000..87dd8809
--- /dev/null
+++ b/sampledata/nostr/config.toml
@@ -0,0 +1,135 @@
+# Nostr-rs-relay configuration
+
+[info]
+# The advertised URL for the Nostr websocket.
+relay_url = "wss://nostr.example.com/"
+
+# Relay information for clients. Put your unique server name here.
+name = "nostr-rs-relay"
+
+# Description
+description = "A newly created nostr-rs-relay.\n\nCustomize this with your own info."
+
+# Administrative contact pubkey
+#pubkey = "0c2d168a4ae8ca58c9f1ab237b5df682599c6c7ab74307ea8b05684b60405d41"
+
+# Administrative contact URI
+#contact = "mailto:[email protected]"
+
+[diagnostics]
+# Enable tokio tracing (for use with tokio-console)
+#tracing = true
+
+[database]
+# Directory for SQLite files. Defaults to the current directory. Can
+# also be specified (and overriden) with the "--db dirname" command
+# line option.
+data_directory = "."
+
+
+# Use an in-memory database instead of 'nostr.db'.
+# Caution; this will not survive a process restart!
+#in_memory = false
+
+# Database connection pool settings for subscribers:
+
+# Minimum number of SQLite reader connections
+#min_conn = 4
+
+# Maximum number of SQLite reader connections
+#max_conn = 128
+
+[network]
+# Bind to this network address
+address = "0.0.0.0"
+
+# Listen on this port
+port = 8080
+
+# If present, read this HTTP header for logging client IP addresses.
+# Examples for common proxies, cloudflare:
+#remote_ip_header = "x-forwarded-for"
+#remote_ip_header = "cf-connecting-ip"
+
+# Websocket ping interval in seconds, defaults to 5 minutes
+#ping_interval = 300
+
+[options]
+# Reject events that have timestamps greater than this many seconds in
+# the future. Recommended to reject anything greater than 30 minutes
+# from the current time, but the default is to allow any date.
+reject_future_seconds = 1800
+
+[limits]
+# Limit events created per second, averaged over one minute. Must be
+# an integer. If not set (or set to 0), defaults to unlimited. Note:
+# this is for the server as a whole, not per-connection.
+# messages_per_sec = 0
+
+# Limit client subscriptions created per second, averaged over one
+# minute. Must be an integer. If not set (or set to 0), defaults to
+# unlimited.
+#subscriptions_per_min = 0
+
+# UNIMPLEMENTED...
+# Limit how many concurrent database connections a client can have.
+# This prevents a single client from starting too many expensive
+# database queries. Must be an integer. If not set (or set to 0),
+# defaults to unlimited (subject to subscription limits).
+#db_conns_per_client = 0
+
+# Limit blocking threads used for database connections. Defaults to 16.
+#max_blocking_threads = 16
+
+# Limit the maximum size of an EVENT message. Defaults to 128 KB.
+# Set to 0 for unlimited.
+#max_event_bytes = 131072
+
+# Maximum WebSocket message in bytes. Defaults to 128 KB.
+#max_ws_message_bytes = 131072
+
+# Maximum WebSocket frame size in bytes. Defaults to 128 KB.
+#max_ws_frame_bytes = 131072
+
+# Broadcast buffer size, in number of events. This prevents slow
+# readers from consuming memory.
+#broadcast_buffer = 16384
+
+# Event persistence buffer size, in number of events. This provides
+# backpressure to senders if writes are slow.
+#event_persist_buffer = 4096
+
+[authorization]
+# Pubkey addresses in this array are whitelisted for event publishing.
+# Only valid events by these authors will be accepted, if the variable
+# is set.
+#pubkey_whitelist = [
+# "35d26e4690cbe1a898af61cc3515661eb5fa763b57bd0b42e45099c8b32fd50f",
+# "887645fef0ce0c3c1218d2f5d8e6132a19304cdc57cd20281d082f38cfea0072",
+#]
+
+[verified_users]
+# NIP-05 verification of users. Can be "enabled" to require NIP-05
+# metadata for event authors, "passive" to perform validation but
+# never block publishing, or "disabled" to do nothing.
+#mode = "disabled"
+
+# Domain names that will be prevented from publishing events.
+#domain_blacklist = ["wellorder.net"]
+
+# Domain names that are allowed to publish events. If defined, only
+# events NIP-05 verified authors at these domains are persisted.
+#domain_whitelist = ["example.com"]
+
+# Consider an pubkey "verified" if we have a successful validation
+# from the NIP-05 domain within this amount of time. Note, if the
+# domain provides a successful response that omits the account,
+# verification is immediately revoked.
+#verify_expiration = "1 week"
+
+# How long to wait between verification attempts for a specific author.
+#verify_update_frequency = "24 hours"
+
+# How many consecutive failed checks before we give up on verifying
+# this author.
+#max_consecutive_failures = 20
diff --git a/setup.sh b/setup.sh
index 329641d3..6e4ecebc 100755
--- a/setup.sh
+++ b/setup.sh
@@ -1,6 +1,7 @@
cp config.js.sample config.js
cp compose.yml.sample compose.yml
cp -r sampledata data
+sudo chown 100:100 data/nostr/data
docker compose up -d
docker run -it -v $(pwd):/app --entrypoint pnpm cs i
docker exec -it bc bitcoin-cli createwallet coinos
diff --git a/watcher.sh b/watcher.sh
new file mode 100755
index 00000000..6de246fb
--- /dev/null
+++ b/watcher.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+
+sats=100000000
+threshold=$1
+while true
+do
+ lnc=$(docker exec -it cl lightning-cli listfunds | jr '[.channels[] | .channel_sat] | add')
+ lnw=$(docker exec -it cl lightning-cli listfunds | jr '[.outputs[] | .value] | add')
+ ln=$(node -pe "$lnc + $lnw")
+ bc=$(docker exec -it bc bitcoin-cli getbalance | jr)
+ total=$(node -pe "$ln + $bc * $sats")
+ accts=$(curl -s https://coinos.io/api/balances | jr '.total')
+ gap=$(node -pe "$accts - $total")
+ echo $(date -Is) lnc $lnc lnw $lnw bc $bc tot $total gap $gap
+ # low=$(node -pe "!!($gap > $threshold)")
+ # if "$low"; then
+ # echo "Threshold hit, setting new threshold"
+ # pnpm pm2 stop 0 > /dev/null
+ # node /root/mailgun/index.js
+ # fi
+ sleep 60
+done