aboutsummaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
authorDedan Kibere <[email protected]>2024-10-27 23:05:49 +0300
committerGitHub <[email protected]>2024-10-27 23:05:49 +0300
commit87059fe805d3372263d26b391f8855139f84ad34 (patch)
tree7099fbce72e7614c0c689d2f9a62e69f8b2e621a /lib
parent041db323e4c23d87663bc29519af604137f3eb42 (diff)
parentd09712ef8f8ea8218ce229b233fc0013e6c06f7a (diff)
downloadcoinos-server-87059fe805d3372263d26b391f8855139f84ad34.tar.gz
coinos-server-87059fe805d3372263d26b391f8855139f84ad34.zip
Merge branch 'coinos:master' into master
Diffstat (limited to 'lib')
-rw-r--r--lib/app.ts2
-rw-r--r--lib/db.ts2
-rw-r--r--lib/ecash.ts77
-rw-r--r--lib/invoices.ts2
-rw-r--r--lib/ln.ts8
-rw-r--r--lib/locations.ts7
-rw-r--r--lib/mqtt.ts3
-rw-r--r--lib/notifications.ts89
-rw-r--r--lib/payments.ts310
-rw-r--r--lib/utils.ts79
-rw-r--r--lib/whitelist.ts1
11 files changed, 300 insertions, 280 deletions
diff --git a/lib/app.ts b/lib/app.ts
index 51b43d05..3b31a923 100644
--- a/lib/app.ts
+++ b/lib/app.ts
@@ -46,7 +46,7 @@ app.addHook("onResponse", async (req, reply) => {
app.register(fastifyRateLimit, {
allowList: (req) => req.raw.url?.includes("public"),
- max: 100,
+ max: 500,
timeWindow: 2000,
keyGenerator: (req) => req.headers["cf-connecting-ip"] as string,
errorResponseBuilder: () => {
diff --git a/lib/db.ts b/lib/db.ts
index 50c78e85..053ecefb 100644
--- a/lib/db.ts
+++ b/lib/db.ts
@@ -38,7 +38,7 @@ local pid = KEYS[6]
local total = tonumber(ARGV[1])
local credit = tonumber(ARGV[2])
-local payment = cjson.decode(redis.call('get', paymentKey))
+local payment = redis.call('get', paymentKey)
if payment ~= nil then
redis.call('incrby', balanceKey, total)
diff --git a/lib/ecash.ts b/lib/ecash.ts
index 8f91e70c..d9d6728e 100644
--- a/lib/ecash.ts
+++ b/lib/ecash.ts
@@ -1,67 +1,92 @@
import config from "$config";
+import { g, s } from "$lib/db";
+import { lnb } from "$lib/ln";
+import { fail, wait } from "$lib/utils";
import {
CashuMint,
CashuWallet,
+ MintQuoteState,
getDecodedToken,
getEncodedToken,
getEncodedTokenV4,
} from "@cashu/cashu-ts";
-import { g, s } from "$lib/db";
-import { fail } from "$lib/utils";
-let m = new CashuMint(config.mintUrl);
-let w = new CashuWallet(m);
+const m = new CashuMint(config.mintUrl);
+const w = new CashuWallet(m);
-let enc = (proofs, v = 4) =>
+const enc = (proofs, v = 4) =>
(v === 4 ? getEncodedTokenV4 : getEncodedToken)({
token: [{ mint: config.mintUrl, proofs }],
});
-let dec = (token) => getDecodedToken(token).token[0];
+const dec = (token) => getDecodedToken(token).token[0];
-let ext = async (mint) => {
- let issuer = new CashuMint(mint);
- let { pubkey: issuerPk } = await issuer.getInfo();
- let { pubkey: ourPk } = await m.getInfo();
+const ext = async (mint) => {
+ const issuer = new CashuMint(mint);
+ const { pubkey: issuerPk } = await issuer.getInfo();
+ const { pubkey: ourPk } = await m.getInfo();
return issuerPk !== ourPk;
};
export async function get(id, v = 4) {
- let token = await g(`cash:${id}`);
+ const token = await g(`cash:${id}`);
if (v < 4) return enc(dec(token).proofs, 3);
return token;
}
export async function claim(token) {
- let { proofs: current } = dec(await g(`cash`));
- let { mint } = dec(token);
+ const { proofs: current } = dec(await g("cash"));
+ const { mint } = dec(token);
if (await ext(mint)) fail("Unable to receive from other mints");
- let rcvd = await w.receive(token);
+ const rcvd = await w.receive(token);
- await s(`cash`, enc([...current, ...rcvd]));
+ await s("cash", enc([...current, ...rcvd]));
return rcvd.reduce((a, b) => a + b.amount, 0);
}
export async function mint(amount, v = 4) {
- let { proofs } = dec(await g(`cash`));
- let { send, returnChange } = await w.send(amount, proofs);
- let rcvd = await w.receive(enc(send, v));
- let change = enc(returnChange, v);
- await s(`cash`, change);
+ const { proofs } = dec(await g("cash"));
+ const { send, returnChange } = await w.send(amount, proofs);
+ const rcvd = await w.receive(enc(send, v));
+ const change = enc(returnChange, v);
+ await s("cash", change);
return enc(rcvd, v);
}
export async function check(token) {
- let o = dec(token);
- let { mint, proofs } = o;
- let total = proofs.reduce((a, b) => a + b.amount, 0);
+ const o = dec(token);
+ const { mint, proofs } = o;
+ const total = proofs.reduce((a, b) => a + b.amount, 0);
- let external = await ext(mint);
+ const external = await ext(mint);
- let r = await w.checkProofsSpent(proofs);
- let spent = r.reduce((a, b) => a + b.amount, 0);
+ const r = await w.checkProofsSpent(proofs);
+ const spent = r.reduce((a, b) => a + b.amount, 0);
return { total, spent, mint, external };
}
+
+export async function init(amount = 10000) {
+ try {
+ await new Promise((r) => setTimeout(r, 2000));
+ const { quote, request } = await w.createMintQuote(amount);
+ await lnb.pay(request);
+
+ await wait(async () => {
+ const { state } = await w.checkMintQuote(quote);
+ return state === MintQuoteState.PAID;
+ });
+
+ const { proofs } = await w.mintTokens(amount, quote);
+
+ const cash = getEncodedTokenV4({
+ token: [{ mint: config.mintUrl, proofs }],
+ });
+
+ await s("cash", cash);
+ } catch (e) {
+ console.log(e);
+ }
+}
diff --git a/lib/invoices.ts b/lib/invoices.ts
index a17288ca..f2653d74 100644
--- a/lib/invoices.ts
+++ b/lib/invoices.ts
@@ -91,7 +91,7 @@ export let generate = async ({ invoice, user }) => {
} else if (type === types.internal) {
hash = id;
} else {
- fail("unrecognized type");
+ fail("unrecognized type", type);
}
invoice = {
diff --git a/lib/ln.ts b/lib/ln.ts
index c4445b26..fcc6adce 100644
--- a/lib/ln.ts
+++ b/lib/ln.ts
@@ -1,7 +1,5 @@
import config from "$config";
-let ln;
-let { LightningClient } = (await import("clightning-client")).default;
-ln = new LightningClient(config.lightning);
-
-export default ln;
+const { LightningClient } = (await import("clightning-client")).default;
+export default new LightningClient(config.lightning);
+export const lnb = new LightningClient(config.lightningb);
diff --git a/lib/locations.ts b/lib/locations.ts
index 560d5259..11981c19 100644
--- a/lib/locations.ts
+++ b/lib/locations.ts
@@ -1,7 +1,7 @@
import { err } from "$lib/logging";
import got from "got";
import { g, s } from "$lib/db";
-import { fields, pick } from "$lib/utils";
+import { fields, pick, uniq } from "$lib/utils";
export let getLocations = async () => {
try {
@@ -40,7 +40,10 @@ export let getLocations = async () => {
locations.push(...previous);
- await s("locations", locations);
+ await s(
+ "locations",
+ uniq(locations, (l) => l.id),
+ );
await s("locations:since", new Date().toISOString().split(".")[0] + "Z");
} catch (e) {
err("problem fetching locations", e);
diff --git a/lib/mqtt.ts b/lib/mqtt.ts
index 636dc3e1..4212989d 100644
--- a/lib/mqtt.ts
+++ b/lib/mqtt.ts
@@ -1,5 +1,4 @@
import mqtt from "mqtt";
import config from "$config";
-export let mqtt1 = mqtt.connect("mqtt://ln.coinos.io", config.mqtt1);
-export let mqtt2 = mqtt.connect("mqtt://mqtt.coinos.io", config.mqtt2);
+export default mqtt.connect("mqtt://mqtt.coinos.io", config.mqtt);
diff --git a/lib/notifications.ts b/lib/notifications.ts
index af47a6d7..023fdeff 100644
--- a/lib/notifications.ts
+++ b/lib/notifications.ts
@@ -1,38 +1,77 @@
+import { emit } from "$lib/sockets";
+import { err, warn } from "$lib/logging";
+import { db } from "$lib/db";
+import mqtt from "$lib/mqtt";
import config from "$config";
+import { mail, templates } from "$lib/mail";
+import { f, fmt, link, fiat, t } from "$lib/utils";
import webpush from "web-push";
if (config.vapid) {
webpush.setVapidDetails(
- config.vapid.url,
- config.vapid.publicKey,
- config.vapid.privateKey,
+ `mailto:${config.support}`,
+ config.vapid.pk,
+ config.vapid.sk,
);
}
-export const notify = async (user, payload) => {
- try {
- let { subscriptions } = user;
+export const notify = async (p, user, withdrawal) => {
+ emit(user.id, "payment", p);
+ let { username } = user;
+ let { paymentReceived } = t(user);
- if (subscriptions) {
- let i = 0;
- for (let subscription of subscriptions) {
- i++;
- try {
- await webpush.sendNotification(subscription, payload);
- } catch (e) {
- if (
- e.body &&
- (e.body.includes("expired") || e.body.includes("No such"))
- ) {
- user.subscriptions.splice(i, 1);
- await user.save();
- } else {
- err("problem sending notification", e);
- }
- }
- }
+ try {
+ if (user.verified && user.notify) {
+ mail(user, paymentReceived, templates.paymentReceived, {
+ ...t(user),
+ username,
+ payment: {
+ amount: fmt(p.amount),
+ link: link(p.id),
+ tip: p.tip ? fmt(p.tip) : undefined,
+ fiat: f(fiat(p.amount, p.rate), p.currency),
+ fiatTip: p.tip ? f(fiat(p.tip, p.rate), p.currency) : undefined,
+ memo: p.memo,
+ items:
+ p.items &&
+ p.items.map((i) => {
+ return {
+ quantity: i.quantity,
+ name: i.name,
+ total: i.quantity * i.price,
+ totalFiat: f(i.quantity * i.price, p.currency),
+ };
+ }),
+ },
+ withdrawal,
+ });
}
} catch (e) {
- console.log("problem notifying", e);
+ err("problem emailing", e.message);
+ }
+
+ let subscriptions = await db.sMembers(`${user.id}:subscriptions`);
+
+ let payload = {
+ title: paymentReceived,
+ body: `${fmt(p.amount)} ${f(fiat(p.amount, p.rate), p.currency)}`,
+ url: `/payment/${p.id}`,
+ };
+
+ for (let s of subscriptions) {
+ webpush
+ .sendNotification(JSON.parse(s), JSON.stringify(payload))
+ .catch((e) => {
+ warn("sub failed", e.message);
+ db.sRem(`${user.id}:subscriptions`, s);
+ });
+ }
+
+ if (config.mqtt) {
+ if (!mqtt.connected) await mqtt.reconnect();
+ mqtt.publish(
+ username,
+ `pay:${p.amount}:${p.tip}:${p.rate}:${p.created}:${p.id}:${p.memo}:${p.items}`,
+ );
}
};
diff --git a/lib/payments.ts b/lib/payments.ts
index cc55e5ce..88b9230d 100644
--- a/lib/payments.ts
+++ b/lib/payments.ts
@@ -1,41 +1,36 @@
import config from "$config";
+import api from "$lib/api";
+import { db, g, s } from "$lib/db";
import { generate } from "$lib/invoices";
-import { emit } from "$lib/sockets";
-import { v4 } from "uuid";
+import ln from "$lib/ln";
+import { err, l, warn } from "$lib/logging";
import { notify } from "$lib/notifications";
-import { db, g, s } from "$lib/db";
-import { l, err, warn } from "$lib/logging";
import {
- f,
+ SATS,
btc,
fail,
- fiat,
+ fmt,
+ formatReceipt,
getInvoice,
getPayment,
getUser,
- sleep,
- SATS,
+ link,
sats,
- formatReceipt,
+ sleep,
t,
} from "$lib/utils";
import { callWebhook } from "$lib/webhooks";
-import got from "got";
-import { mqtt1, mqtt2 } from "$lib/mqtt";
-import { mail, templates } from "$lib/mail";
-import api from "$lib/api";
-import { bech32 } from "bech32";
import rpc from "@coinos/rpc";
+import { bech32 } from "bech32";
+import got from "got";
+import { v4 } from "uuid";
-import ln from "$lib/ln";
-
-let bc = rpc(config.bitcoin);
-let lq = rpc(config.liquid);
+const bc = rpc(config.bitcoin);
+const lq = rpc(config.liquid);
-let { URL } = process.env;
-let dust = 547;
+const dust = 547;
-export let types = {
+export const types = {
internal: "internal",
bitcoin: "bitcoin",
lightning: "lightning",
@@ -45,7 +40,7 @@ export let types = {
reconcile: "reconcile",
};
-export let debit = async ({
+export const debit = async ({
aid = undefined,
hash,
amount,
@@ -61,12 +56,12 @@ export let debit = async ({
// if (!user.unlimited && amount > 1000000)
// fail(`⚡️${amount} exceeds max withdrawal of ⚡️1,000,000`);
let ref;
- let { id: uid, currency } = user;
+ const { id: uid, currency } = user;
- let rates = await g("rates");
+ const rates = await g("rates");
if (!rate) rate = rates[currency];
- let invoice = await getInvoice(hash);
+ const invoice = await getInvoice(hash);
let iid;
if (invoice) {
@@ -75,7 +70,7 @@ export let debit = async ({
ref = invoice.uid;
- let equivalentRate =
+ const equivalentRate =
invoice.rate * (rates[currency] / rates[invoice.currency]);
if (Math.abs(invoice.rate / rates[invoice.currency] - 1) < 0.01) {
@@ -85,7 +80,7 @@ export let debit = async ({
}
}
- let tip = parseInt(invoice?.tip) || null;
+ const tip = parseInt(invoice?.tip) || null;
if (tip < 0) fail("Invalid tip");
if (!amount || amount < 0) fail("Amount must be greater than zero");
@@ -109,8 +104,8 @@ export let debit = async ({
if (ourfee.err) fail(ourfee.err);
- let id = v4();
- let p = {
+ const id = v4();
+ const p = {
id,
aid,
amount: -amount,
@@ -140,7 +135,7 @@ export let debit = async ({
return p;
};
-export let credit = async ({
+export const credit = async ({
hash,
amount,
memo = "",
@@ -150,7 +145,7 @@ export let credit = async ({
}) => {
amount = parseInt(amount) || 0;
- let inv = await getInvoice(hash);
+ const inv = await getInvoice(hash);
if (!inv) {
await db.sAdd("missing", ref.split(":")[0]);
return;
@@ -164,16 +159,16 @@ export let credit = async ({
if (amount < 0 || tip < 0) fail("Invalid amount");
if (type === types.internal) amount += tip;
- let user = await getUser(inv.uid);
- let { id: uid, currency, username } = user;
+ const user = await getUser(inv.uid);
+ const { id: uid, currency, username } = user;
- let rates = await g("rates");
+ const rates = await g("rates");
let rate = rates[currency];
if (!rate) await sleep(1000);
rate = rates[currency];
- let equivalentRate = inv.rate * (rates[currency] / rates[inv.currency]);
+ const equivalentRate = inv.rate * (rates[currency] / rates[inv.currency]);
if (Math.abs(inv.rate / rates[inv.currency] - 1) < 0.01) {
rate = equivalentRate;
@@ -181,8 +176,8 @@ export let credit = async ({
// warn("rate slipped", hash, invoice.rate, equivalentRate);
}
- let id = v4();
- let p = {
+ const id = v4();
+ const p = {
aid,
id,
iid: inv.id,
@@ -209,7 +204,7 @@ export let credit = async ({
let balanceKey = "balance";
if ([types.bitcoin, types.liquid].includes(type)) {
- let [txid, vout] = ref.split(":").slice(-2);
+ const [txid, vout] = ref.split(":").slice(-2);
p.confirmed = false;
balanceKey = "pending";
await s(`payment:${txid}:${vout}`, id);
@@ -217,7 +212,7 @@ export let credit = async ({
await s(`payment:${hash}`, id);
}
- let m = await db.multi();
+ const m = await db.multi();
if ([types.bitcoin, types.liquid, types.lightning].includes(type))
m.incrBy(`credit:${type}:${uid}`, Math.round(amount * config.fee));
@@ -228,59 +223,28 @@ export let credit = async ({
.incrBy(`${balanceKey}:${aid || uid}`, amount)
.exec();
- emit(uid, "payment", p);
- notify(user, `Received ${amount} sats`);
- callWebhook(inv, p);
- l(username, "received", type, amount);
-
- let items;
- if (inv.items && inv.items.length) {
+ if (inv.items?.length) {
formatReceipt(inv.items, inv.currency);
p.items = inv.items;
}
- if (config.mqtt1) {
- if (!mqtt1.connected) await mqtt1.reconnect();
- mqtt1.publish(
- username,
- `pay:${p.amount}:${p.tip}:${p.rate}:${p.created}:${p.id}:${p.memo}:${items}`,
- );
- }
-
- if (config.mqtt2) {
- if (!mqtt2.connected) await mqtt2.reconnect();
- mqtt2.publish(
- username,
- `pay:${p.amount}:${p.tip}:${p.rate}:${p.created}:${p.id}:${p.memo}:${items}`,
- );
- }
-
- try {
- await completePayment(p, user);
- } catch (e) {
- console.log(e);
- }
+ await completePayment(inv, p, user);
return p;
};
-let fmt = (sats) =>
- "⚡️" +
- new Intl.NumberFormat("en-US", { maximumFractionDigits: 0 }).format(sats);
-
-export let completePayment = async (p, user) => {
- let { id, autowithdraw, threshold, reserve, destination, username } = user;
- let link = (id) => `${URL}/payment/${id}`;
+export const completePayment = async (inv, p, user) => {
+ const { id, autowithdraw, threshold, reserve, destination, username } = user;
+ let withdrawal;
if (p.confirmed) {
- let withdrawal;
if (autowithdraw) {
try {
- let to = destination.trim();
- let balance = await g(`balance:${id}`);
- let amount = balance - reserve;
+ const to = destination.trim();
+ const balance = await g(`balance:${id}`);
+ const amount = balance - reserve;
if (balance > threshold) {
l("initiating autowithdrawal", amount, to, balance, threshold);
- let w = await pay({ amount, to, user });
+ const w = await pay({ amount, to, user });
withdrawal = {
amount: fmt(-w.amount),
link: link(w.id),
@@ -291,42 +255,20 @@ export let completePayment = async (p, user) => {
warn(username, "autowithdraw failed", e.message);
}
}
-
- let { paymentReceived } = t(user);
- if (user.verified && user.notify) {
- mail(user, paymentReceived, templates.paymentReceived, {
- ...t(user),
- username,
- payment: {
- amount: fmt(p.amount),
- link: link(p.id),
- tip: p.tip ? fmt(p.tip) : undefined,
- fiat: f(fiat(p.amount, p.rate), p.currency),
- fiatTip: p.tip ? f(fiat(p.tip, p.rate), p.currency) : undefined,
- memo: p.memo,
- items:
- p.items &&
- p.items.map((i) => {
- return {
- quantity: i.quantity,
- name: i.name,
- total: i.quantity * i.price,
- totalFiat: f(i.quantity * i.price, p.currency),
- };
- }),
- },
- withdrawal,
- });
- }
}
+
+ notify(p, user, withdrawal);
+ l(username, "received", p.type, p.amount);
+ callWebhook(inv, p);
};
-let pay = async ({ aid = undefined, amount, to, user }) => {
+const pay = async ({ aid = undefined, amount, to, user }) => {
if (!aid) aid = user.id;
amount = parseInt(amount) || 0;
- let lnurl, pr;
+ let lnurl;
+ let pr;
if (to.includes("@") && to.includes(".")) {
- let [name, domain] = to.split("@");
+ const [name, domain] = to.split("@");
lnurl = `https://${domain}/.well-known/lnurlp/${name}`;
} else if (to.startsWith("lnurl")) {
lnurl = Buffer.from(
@@ -334,10 +276,10 @@ let pay = async ({ aid = undefined, amount, to, user }) => {
).toString();
}
- let maxfee = getMaxFee(amount);
+ const maxfee = getMaxFee(amount);
if (lnurl) {
amount -= maxfee;
- let { callback } = (await got(lnurl).json()) as any;
+ const { callback } = (await got(lnurl).json()) as any;
({ pr } = (await got(`${callback}?amount=${amount * 1000}`).json()) as any);
} else if (to.startsWith("ln")) {
amount -= maxfee;
@@ -349,8 +291,9 @@ let pay = async ({ aid = undefined, amount, to, user }) => {
: await sendOnchain({ aid, amount, address: to, user, subtract: true });
};
-export let decode = async (hex) => {
- let type, tx;
+export const decode = async (hex) => {
+ let type;
+ let tx;
try {
tx = await bc.decodeRawTransaction(hex);
type = types.bitcoin;
@@ -367,14 +310,14 @@ export let decode = async (hex) => {
return { tx, type };
};
-let inflight = {};
-export let sendOnchain = async (params) => {
+const inflight = {};
+export const sendOnchain = async (params) => {
let { aid, hex, rate, user, signed } = params;
if (!aid) aid = user.id;
if (!hex) ({ hex } = await build(params));
- let { tx, type } = await decode(hex);
- let node =
+ const { tx, type } = await decode(hex);
+ const node =
aid === user.id ? rpc(config[type]) : rpc({ ...config[type], wallet: aid });
let { txid } = tx;
@@ -393,7 +336,7 @@ export let sendOnchain = async (params) => {
({ txid } = await node.decodeRawTransaction(hex));
- let r = await node.testMempoolAccept([hex]);
+ const r = await node.testMempoolAccept([hex]);
if (!r[0].allowed) fail("transaction rejected");
let total = 0;
@@ -401,7 +344,7 @@ export let sendOnchain = async (params) => {
let change = 0;
if (type === types.liquid) {
- for (let {
+ for (const {
asset,
scriptPubKey: { address, type },
value,
@@ -420,18 +363,18 @@ export let sendOnchain = async (params) => {
}
} else {
let totalIn = 0;
- for await (let { txid, vout } of tx.vin) {
- let hex = await node.getRawTransaction(txid);
- let tx = await node.decodeRawTransaction(hex);
+ for await (const { txid, vout } of tx.vin) {
+ const hex = await node.getRawTransaction(txid);
+ const tx = await node.decodeRawTransaction(hex);
totalIn += sats(tx.vout[vout].value);
}
- for (let {
+ for (const {
scriptPubKey: { address },
value,
} of tx.vout) {
total += sats(value);
- let invoice = await g(`invoice:${address}`);
+ const invoice = await g(`invoice:${address}`);
if (invoice?.aid === aid) fail("Cannot send to internal address");
if ((await node.getAddressInfo(address)).ismine) {
@@ -442,9 +385,9 @@ export let sendOnchain = async (params) => {
fee = totalIn - total;
}
- let amount = total - change;
+ const amount = total - change;
- let p = await debit({
+ const p = await debit({
aid,
hash: txid,
amount,
@@ -467,7 +410,7 @@ export let sendOnchain = async (params) => {
}
};
-let getMaxFee = (n) =>
+const getMaxFee = (n) =>
Math.round(
n < 100
? n * 5
@@ -482,7 +425,7 @@ let getMaxFee = (n) =>
: n * 0.01,
);
-export let sendLightning = async ({
+export const sendLightning = async ({
user,
pr,
amount,
@@ -497,15 +440,15 @@ export let sendLightning = async ({
}
let total = amount;
- let decoded = await ln.decode(pr);
- let { amount_msat } = decoded;
+ const decoded = await ln.decode(pr);
+ const { amount_msat } = decoded;
if (amount_msat) total = Math.round(amount_msat / 1000);
maxfee = parseInt(maxfee) || getMaxFee(total);
if (maxfee < 0) fail("Max fee cannot be negative");
- let { pays } = await ln.listpays(pr);
+ const { pays } = await ln.listpays(pr);
if (pays.find((p) => p.status === "complete"))
fail("Invoice has already been paid");
@@ -542,14 +485,13 @@ export let sendLightning = async ({
console.log("failed to process payment", e, p);
}
} catch (e) {
- await reverse(p);
throw e;
}
return p;
};
-export let sendInternal = async ({
+export const sendInternal = async ({
amount,
invoice = undefined,
recipient,
@@ -561,15 +503,15 @@ export let sendInternal = async ({
user: recipient,
});
- let { hash } = invoice;
+ const { hash } = invoice;
let memo;
- let p = await debit({ hash, amount, memo, user: sender });
+ const p = await debit({ hash, amount, memo, user: sender });
await credit({ hash, amount, memo, ref: sender.id });
return p;
};
-let getAddressType = async (a) => {
+const getAddressType = async (a) => {
try {
await bc.getAddressInfo(a);
return types.bitcoin;
@@ -583,7 +525,7 @@ let getAddressType = async (a) => {
}
};
-export let build = async ({
+export const build = async ({
aid,
amount,
address,
@@ -591,25 +533,29 @@ export let build = async ({
subtract,
user,
}) => {
- let type = await getAddressType(address);
+ const type = await getAddressType(address);
if (!aid) aid = user.id;
- let node =
+ const node =
aid === user.id ? rpc(config[type]) : rpc({ ...config[type], wallet: aid });
amount = parseInt(amount);
if (amount < 0) fail("invalid amount");
- let fees: any = await fetch(`${api[type]}/fees/recommended`).then((r) =>
+ const fees: any = await fetch(`${api[type]}/fees/recommended`).then((r) =>
r.json(),
);
+ fees.hourFee = fees.halfHourFee;
+ fees.halfHourFee = fees.fastestFee;
+ fees.fastestFee = Math.round(fees.fastestFee * 1.1);
+
if (!feeRate) {
feeRate = fees.halfHourFee;
}
if (feeRate < fees.economyFee) fail("fee rate too low");
- let replaceable = false;
+ const replaceable = false;
let outs = [{ [address]: btc(amount) }];
@@ -634,10 +580,10 @@ export let build = async ({
else throw e;
}
- let balance = await g(`balance:${aid}`);
+ const balance = await g(`balance:${aid}`);
let ourfee = Math.round(amount * config.fee);
- let credit = await g(`credit:${type}:${aid}`);
- let covered = Math.min(credit, ourfee);
+ const credit = await g(`credit:${type}:${aid}`);
+ const covered = Math.min(credit, ourfee);
ourfee -= covered;
if (aid) ourfee = 0;
@@ -659,16 +605,16 @@ export let build = async ({
fee = sats(tx.fee);
}
- let inputs = [];
- let { vin } = await node.decodeRawTransaction(tx.hex);
+ const inputs = [];
+ const { vin } = await node.decodeRawTransaction(tx.hex);
- for (let { txid, vout } of vin) {
- let rawTx = await node.getRawTransaction(txid);
- let tx = await node.decodeRawTransaction(rawTx);
- let prevOutput = tx.vout[vout];
- let { address } = prevOutput.scriptPubKey;
- let { hdkeypath: path } = await node.getAddressInfo(address);
- let witnessUtxo = {
+ for (const { txid, vout } of vin) {
+ const rawTx = await node.getRawTransaction(txid);
+ const tx = await node.decodeRawTransaction(rawTx);
+ const prevOutput = tx.vout[vout];
+ const { address } = prevOutput.scriptPubKey;
+ const { hdkeypath: path } = await node.getAddressInfo(address);
+ const witnessUtxo = {
amount: Math.round(prevOutput.value * SATS),
script: prevOutput.scriptPubKey.hex,
};
@@ -678,10 +624,10 @@ export let build = async ({
return { feeRate, ourfee, fee, fees, hex: tx.hex, inputs };
};
-export let catchUp = async () => {
+export const catchUp = async () => {
try {
- let txns = [];
- for (let [type, n] of Object.entries({ bitcoin: bc, liquid: lq })) {
+ const txns = [];
+ for (const [type, n] of Object.entries({ bitcoin: bc, liquid: lq })) {
txns.push(
...(await n.listTransactions("*", 10)).filter((tx) => {
tx.type = type;
@@ -690,7 +636,7 @@ export let catchUp = async () => {
);
}
- for (let { txid, type } of txns) {
+ for (const { txid, type } of txns) {
try {
if (await db.zScore("seen", txid)) continue;
await got.post(`http://localhost:${process.env.PORT || 3119}/confirm`, {
@@ -711,33 +657,33 @@ export let catchUp = async () => {
setTimeout(catchUp, 10000);
};
-export let reconcile = async (account, initial = false) => {
+export const reconcile = async (account, initial = false) => {
try {
- let { descriptors, id, uid, type } = account;
- let user = await getUser(uid);
- let node = rpc({ ...config[type], wallet: id });
+ const { descriptors, id, uid, type } = account;
+ const user = await getUser(uid);
+ const node = rpc({ ...config[type], wallet: id });
let total;
if (initial) {
- let progress = await node.scanTxOutSet("status");
+ const progress = await node.scanTxOutSet("status");
if (progress) return setTimeout(() => reconcile(account, initial), 1000);
- let { total_amount } = await node.scanTxOutSet("start", descriptors);
+ const { total_amount } = await node.scanTxOutSet("start", descriptors);
total = Math.round(total_amount * SATS);
} else {
total = Math.round((await node.getBalance({ minconf: 1 })) * SATS);
}
- let { balanceAdjustment: memo } = t(user);
+ const { balanceAdjustment: memo } = t(user);
- let balance = await g(`balance:${id}`);
+ const balance = await g(`balance:${id}`);
- let amount = Math.abs(total - balance);
- let hash = v4();
+ const amount = Math.abs(total - balance);
+ const hash = v4();
if (total > balance) {
- let inv = {
+ const inv = {
memo,
type: types.reconcile,
hash,
@@ -770,17 +716,17 @@ export let reconcile = async (account, initial = false) => {
}
};
-export let check = async () => {
+export const check = async () => {
try {
- let payments = await db.sMembers("pending");
+ const payments = await db.sMembers("pending");
- for (let pr of payments) {
- let p = await getPayment(pr);
+ for (const pr of payments) {
+ const p = await getPayment(pr);
if (!p) continue;
- let { pays } = await ln.listpays(pr);
+ const { pays } = await ln.listpays(pr);
- let failed = !pays.length || pays.every((p) => p.status === "failed");
- let completed = pays.find((p) => p.status === "complete");
+ const failed = !pays.length || pays.every((p) => p.status === "failed");
+ const completed = pays.find((p) => p.status === "complete");
if (completed) await finalize(completed, p);
else if (failed) await reverse(p);
@@ -792,11 +738,11 @@ export let check = async () => {
setTimeout(check, 2000);
};
-let finalize = async (r, p) => {
+const finalize = async (r, p) => {
await db.sRem("pending", p.hash);
l("payment completed", p.id, r.payment_preimage);
- let maxfee = p.fee;
+ const maxfee = p.fee;
p.fee = Math.round((r.amount_sent_msat - r.amount_msat) / 1000);
p.ref = r.payment_preimage;
@@ -808,12 +754,12 @@ let finalize = async (r, p) => {
return p;
};
-let reverse = async (p) => {
- let total = Math.abs(p.amount) + p.fee;
- let ourfee = p.ourfee || 0;
- let credit = Math.round(total * config.fee) - ourfee;
+const reverse = async (p) => {
+ const total = Math.abs(p.amount) + p.fee;
+ const ourfee = p.ourfee || 0;
+ const credit = Math.round(total * config.fee) - ourfee;
- let k = await db.reverse(
+ const k = await db.reverse(
`payment:${p.id}`,
`balance:${p.uid}`,
`credit:${types.lightning}:${p.uid}`,
diff --git a/lib/utils.ts b/lib/utils.ts
index b7f55ff1..b1ff862b 100644
--- a/lib/utils.ts
+++ b/lib/utils.ts
@@ -1,42 +1,44 @@
+import config from "$config";
+import { g, s } from "$lib/db";
import locales from "$lib/locales/index";
-import { randomBytes, bytesToHex } from "@noble/hashes/utils";
import migrate from "$lib/migrate";
-import { g, s } from "$lib/db";
-import config from "$config";
+import { bytesToHex, randomBytes } from "@noble/hashes/utils";
import { getPublicKey } from "nostr";
-export let fail = (msg) => {
+const { URL } = process.env;
+
+export const fail = (msg) => {
throw new Error(msg);
};
-export let nada = () => {};
+export const nada = () => {};
-export let sleep = (n) => new Promise((r) => setTimeout(r, n));
-export let wait = async (f, s = 300, n = 50) => {
+export const sleep = (n) => new Promise((r) => setTimeout(r, n));
+export const wait = async (f, s = 300, n = 50) => {
let i = 0;
while (!(await f()) && i < s) (await sleep(n)) && i++;
if (i >= s) fail("timeout");
return f();
};
-export let prod = process.env.NODE_ENV === "production";
+export const prod = process.env.NODE_ENV === "production";
-export let bail = (res, msg) => res.code(500).send(msg);
+export const bail = (res, msg) => res.code(500).send(msg);
-export let SATS = 100000000;
-export let sats = (n) => Math.round(n * SATS);
-export let btc = (n) => parseFloat((n / SATS).toFixed(8));
-export let fiat = (n, r) => (n * r) / SATS;
+export const SATS = 100000000;
+export const sats = (n) => Math.round(n * SATS);
+export const btc = (n) => parseFloat((n / SATS).toFixed(8));
+export const fiat = (n, r) => (n * r) / SATS;
-export let uniq = (a, k) => [...new Map(a.map((x) => [k(x), x])).values()];
-export let pick = (O, K) =>
+export const uniq = (a, k) => [...new Map(a.map((x) => [k(x), x])).values()];
+export const pick = (O, K) =>
K.reduce((o, k) => (typeof O[k] !== "undefined" && (o[k] = O[k]), o), {});
-export let bip21 = (address, { amount, memo, tip, type }) => {
+export const bip21 = (address, { amount, memo, tip, type }) => {
if (!(amount || memo)) return address;
- let network = { liquid: "liquidnetwork", bitcoin: "bitcoin" }[type];
- let url = new URLSearchParams();
+ const network = { liquid: "liquidnetwork", bitcoin: "bitcoin" }[type];
+ const url = new URLSearchParams();
if (amount) {
url.append("amount", btc(amount + tip).toFixed(8));
@@ -48,7 +50,7 @@ export let bip21 = (address, { amount, memo, tip, type }) => {
return `${network}:${address}?${url.toString()}`;
};
-export let fields = [
+export const fields = [
"cipher",
"pubkey",
"password",
@@ -68,9 +70,9 @@ export let fields = [
"banner",
];
-export let getUser = async (username) => {
+export const getUser = async (username) => {
if (username === "undefined") fail("invalid user");
- let user = await migrate(username);
+ const user = await migrate(username);
if (user && !user.anon && !user.nwc) {
user.nwc = bytesToHex(randomBytes(32));
await s(getPublicKey(user.nwc), user.id);
@@ -80,20 +82,20 @@ export let getUser = async (username) => {
return user;
};
-export let getInvoice = async (hash) => {
+export const getInvoice = async (hash) => {
let iid = await g(`invoice:${hash}`);
- if (iid && iid.id) iid = iid.id;
- else if (iid && iid.hash) iid = iid.hash;
+ if (iid?.id) iid = iid.id;
+ else if (iid?.hash) iid = iid.hash;
return await g(`invoice:${iid}`);
};
-export let getPayment = async (id) => {
+export const getPayment = async (id) => {
let p = await g(`payment:${id}`);
if (typeof p === "string") p = await g(`payment:${p}`);
return p;
};
-export let f = (s, currency) =>
+export const f = (s, currency) =>
new Intl.NumberFormat("en-US", {
style: "currency",
currency,
@@ -106,14 +108,14 @@ export function formatReceipt(items, currency) {
const words = text.split(" ");
const lines = [];
let currentLine = "";
- words.forEach((word) => {
+ for (const word of words) {
if (currentLine.length + word.length + 1 > maxWidth) {
lines.push(currentLine);
currentLine = word;
} else {
currentLine += (currentLine.length > 0 ? " " : "") + word;
}
- });
+ }
if (currentLine) lines.push(currentLine);
return lines;
}
@@ -122,7 +124,7 @@ export function formatReceipt(items, currency) {
let maxQuantityLength = 0;
let maxPriceLength = 0;
- items.forEach((item) => {
+ for (const item of items) {
const quantityLength = String(item.quantity).length;
if (quantityLength > maxQuantityLength) {
maxQuantityLength = quantityLength;
@@ -132,7 +134,7 @@ export function formatReceipt(items, currency) {
if (priceLength > maxPriceLength) {
maxPriceLength = priceLength;
}
- });
+ }
// Add padding to the widths for aesthetic spacing
return {
@@ -154,7 +156,7 @@ export function formatReceipt(items, currency) {
const nameLines = wrapText(item.name, nameColumnWidth);
// Construct the full line(s) with the first line including the price
- let fullLines = [
+ const fullLines = [
`${quantityStr}${nameLines[0].padEnd(nameColumnWidth)}${priceStr}`,
];
// Add any additional name lines, properly indented
@@ -167,11 +169,11 @@ export function formatReceipt(items, currency) {
.join("\n");
}
-export let t = ({ language = "en" }) => locales[language];
+export const t = ({ language = "en" }) => locales[language];
-export let time = (() => {
- let count = 0,
- started = false;
+export const time = (() => {
+ let count = 0;
+ let started = false;
return (s = "") => {
if (!started) {
console.time("");
@@ -180,3 +182,10 @@ export let time = (() => {
console.timeLog("", ++count, s);
};
})();
+
+export const fmt = (sats) =>
+ `⚡️${new Intl.NumberFormat("en-US", { maximumFractionDigits: 0 }).format(
+ sats,
+ )}`;
+
+export const link = (id) => `${URL}/payment/${id}`;
diff --git a/lib/whitelist.ts b/lib/whitelist.ts
index 54d3aafc..b0975d2c 100644
--- a/lib/whitelist.ts
+++ b/lib/whitelist.ts
@@ -33,6 +33,7 @@ export default [
"profile",
"prompt",
"pubkey",
+ "push",
"reserve",
"salt",
"seed",