aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2024-09-24 12:30:51 -0700
committerAdam Soltys <[email protected]>2024-09-24 12:30:51 -0700
commit80365c227c5a774ad65f5dad8b989e1966630160 (patch)
treea9d6cd130e48e815fb60b91263c5d9b5a83b8808
parent8c8c71d54d23960ffc4caa94619bae7c6a69514b (diff)
downloadcoinos-server-80365c227c5a774ad65f5dad8b989e1966630160.tar.gz
coinos-server-80365c227c5a774ad65f5dad8b989e1966630160.zip
reconcile onchain balance
-rw-r--r--lib/invoices.ts5
-rw-r--r--lib/payments.ts72
-rw-r--r--lib/register.ts20
-rw-r--r--routes/payments.ts18
-rw-r--r--routes/users.ts90
5 files changed, 140 insertions, 65 deletions
diff --git a/lib/invoices.ts b/lib/invoices.ts
index 60bcba0f..d7f09de5 100644
--- a/lib/invoices.ts
+++ b/lib/invoices.ts
@@ -51,13 +51,15 @@ export let generate = async ({ invoice, user }) => {
let id = v4();
- let hash, text;
+ let hash, text, path;
if (account.seed) {
type = "bitcoin";
let node = rpc({ ...config[type], wallet: aid });
hash = await node.getNewAddress();
text = bip21(hash, invoice);
+
+ ({ hdkeypath: path } = await node.getAddressInfo(hash));
} else if (type === types.lightning) {
let r;
if (bolt11) {
@@ -107,6 +109,7 @@ export let generate = async ({ invoice, user }) => {
received: 0,
request_id,
memoPrompt,
+ path,
prompt,
secret,
text,
diff --git a/lib/payments.ts b/lib/payments.ts
index 6d83b28f..9d2d9b79 100644
--- a/lib/payments.ts
+++ b/lib/payments.ts
@@ -11,6 +11,7 @@ import {
fail,
fiat,
getInvoice,
+ getUser,
sleep,
SATS,
sats,
@@ -154,7 +155,7 @@ export let credit = async ({
return;
}
- let { tip } = inv;
+ let { path, tip } = inv;
tip = parseInt(tip) || 0;
if (!memo) ({ memo } = inv);
@@ -162,7 +163,7 @@ export let credit = async ({
if (amount < 0 || tip < 0) fail("Invalid amount");
if (type === types.internal) amount += tip;
- let user = await g(`user:${inv.uid}`);
+ let user = await getUser(inv.uid);
let { id: uid, currency, username } = user;
let rates = await g("rates");
@@ -186,6 +187,7 @@ export let credit = async ({
iid: inv.id,
hash,
amount: amount - tip,
+ path,
uid,
rate,
currency,
@@ -319,7 +321,7 @@ export let completePayment = async (p, user) => {
};
let pay = async ({ aid = undefined, amount, to, user }) => {
- if (!aid) aid = user.id;
+ if (!aid) aid = user.id;
amount = parseInt(amount) || 0;
let lnurl, pr;
if (to.includes("@") && to.includes(".")) {
@@ -440,6 +442,7 @@ export let sendOnchain = async (params) => {
let amount = total - change;
+ console.log("debiting", aid, amount);
let p = await debit({
aid,
hash: txid,
@@ -613,7 +616,6 @@ export let sendInternal = async ({
invoice = await generate({
invoice: { amount, type: "lightning" },
user: recipient,
- sender,
});
let { hash } = invoice;
@@ -718,11 +720,16 @@ export let build = async ({
let { vin } = await node.decodeRawTransaction(tx.hex);
for (let { txid, vout } of vin) {
- let nonWitnessUtxo = await node.getRawTransaction(txid);
- let tx = await node.decodeRawTransaction(nonWitnessUtxo);
- let { address } = tx.vout[vout].scriptPubKey;
+ 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);
- inputs.push({ nonWitnessUtxo, path });
+ let witnessUtxo = {
+ amount: Math.round(prevOutput.value * SATS),
+ script: prevOutput.scriptPubKey.hex,
+ };
+ inputs.push({ witnessUtxo, path });
}
return { feeRate, ourfee, fee, fees, hex: tx.hex, inputs };
@@ -760,3 +767,52 @@ export let catchUp = async () => {
setTimeout(catchUp, 10000);
};
+
+export let reconcile = async (account) => {
+ let { descriptors, id, uid, type } = account;
+ let user = await getUser(uid);
+ let node = rpc({ ...config[type], wallet: id });
+ let { total_amount } = await node.scanTxOutSet("start", descriptors);
+ let total = Math.round(total_amount * SATS);
+ let { balanceAdjustment: memo } = t(user);
+
+ let balance = await g(`balance:${id}`);
+
+ let inflight = 0;
+ for (let pid of await db.sMembers(`inflight:${id}`)) {
+ let p = await g(`payment:${pid}`);
+ inflight += Math.abs(p.amount) + p.fee;
+ }
+
+ balance += inflight;
+
+ let amount = Math.abs(total - balance);
+ let hash = v4();
+
+ if (total > balance) {
+ let inv = {
+ memo,
+ type: types.reconcile,
+ hash,
+ amount,
+ uid,
+ aid: id,
+ };
+ await s(`invoice:${hash}`, inv);
+ await credit({
+ hash,
+ amount,
+ type: types.reconcile,
+ aid: id,
+ });
+ } else if (total < balance) {
+ await debit({
+ aid: id,
+ amount,
+ hash: v4(),
+ memo,
+ user,
+ type: types.reconcile,
+ });
+ }
+};
diff --git a/lib/register.ts b/lib/register.ts
index eb20ae23..f2e97fed 100644
--- a/lib/register.ts
+++ b/lib/register.ts
@@ -4,7 +4,7 @@ import { v4 } from "uuid";
import config from "$config";
import countries from "$lib/countries";
-import { s } from "$lib/db";
+import { db } from "$lib/db";
import { l, warn } from "$lib/logging";
import { fail, getUser } from "$lib/utils";
@@ -52,10 +52,20 @@ export default async (user, ip) => {
user.migrated = true;
user.locktime = 300;
- await s(`user:${id}`, user);
- await s(`user:${username}`, id);
- await s(`user:${pubkey}`, id);
- await s(`balance:${id}`, 0);
+ let account = JSON.stringify({
+ id,
+ type: "ecash",
+ name: "Spending",
+ });
+
+ db.multi()
+ .set(`user:${id}`, JSON.stringify(user))
+ .set(`user:${username}`, id)
+ .set(`user:${pubkey}`, id)
+ .set(`balance:${id}`, 0)
+ .set(`account:${id}`, account)
+ .lPush(`${id}:accounts`, id)
+ .exec();
l("new user", username);
diff --git a/routes/payments.ts b/routes/payments.ts
index 02056eeb..d4993b87 100644
--- a/routes/payments.ts
+++ b/routes/payments.ts
@@ -260,8 +260,10 @@ export default {
try {
let node = rpc({ ...config[type], wallet });
-
let { confirmations, details } = await node.getTransaction(txid);
+ let hot = wallet === config[type].wallet;
+ let aid;
+ if (!hot) aid = wallet;
for (let { address, amount, asset, category, vout } of details) {
if (!address) continue;
@@ -272,8 +274,14 @@ export default {
if (typeof p === "string") p = await g(`payment:${p}`);
if (!p) continue;
- if (confirmations >= 1) p.confirmed = true;
- await s(`payment:${p.id}`, p);
+ if (confirmations) {
+ p.confirmed = true;
+ await s(`payment:${p.id}`, p);
+ if (aid) await db.sRem(`inflight:${aid}`, p.id);
+ } else {
+ if (aid) await db.sAdd(`inflight:${aid}`, p.id);
+ }
+
emit(p.uid, "payment", p);
continue;
}
@@ -282,9 +290,6 @@ export default {
if (typeof p === "string") p = await g(`payment:${p}`);
if (!p) {
- let aid;
- let bal = await g(`balance:${wallet}`);
- if (bal !== null) aid = wallet;
await credit({
hash: address,
amount: sats(amount),
@@ -357,6 +362,7 @@ export default {
res.send(p);
} catch (e) {
+ console.log(e);
warn(user.username, "payment failed", e.message);
res.code(500).send(e.message);
}
diff --git a/routes/users.ts b/routes/users.ts
index 44a87c95..34d20584 100644
--- a/routes/users.ts
+++ b/routes/users.ts
@@ -1,7 +1,7 @@
import { appendFile } from "node:fs/promises";
import { g, s, db } from "$lib/db";
import config from "$config";
-import { SATS, pick, bail, fail, getUser, t } from "$lib/utils";
+import { pick, bail, fail, getUser } from "$lib/utils";
import jwt from "jsonwebtoken";
import { authenticator } from "otplib";
import whitelist from "$lib/whitelist";
@@ -10,7 +10,7 @@ import { emit } from "$lib/sockets";
import register from "$lib/register";
import { requirePin } from "$lib/auth";
import { v4 } from "uuid";
-import { credit, types } from "$lib/payments";
+import { reconcile, types } from "$lib/payments";
import { bech32 } from "bech32";
import { mail, templates } from "$lib/mail";
import upload from "$lib/upload";
@@ -594,29 +594,34 @@ export default {
},
async accounts(req, res) {
- let { user } = req;
+ try {
+ let { user } = req;
+
+ let accounts = [];
+ for (let id of await db.lRange(`${user.id}:accounts`, 0, -1)) {
+ let account = await g(`account:${id}`);
+ account.balance = await g(`balance:${id}`);
- let accounts = [];
- for (let id of await db.lRange(`${user.id}:accounts`, 0, -1)) {
- let account = await g(`account:${id}`);
- account.balance = await g(`balance:${id}`);
+ if (account.seed) await reconcile(account);
- if (account.type === "savings") {
+ accounts.push(account);
}
- accounts.push(account);
+ res.send(accounts.reverse());
+ } catch (e) {
+ console.log(e);
+ bail(res, e.message);
}
-
- res.send(accounts.reverse());
},
async createAccount(req, res) {
try {
let { fingerprint, pubkey, name, seed, type } = req.body;
let { user } = req;
+ let { id: uid } = user;
let id = v4();
- let account = { id, name, seed, type };
+ let account = { id, name, seed, type, uid, descriptors: [] };
let node = rpc(config[type]);
@@ -628,11 +633,10 @@ export default {
node = rpc({ ...config[type], wallet: id });
- let descriptors = [];
for (let i of [0, 1]) {
let desc = `wpkh([${fingerprint}]${pubkey}/${i}/*)`;
let { checksum } = await node.getDescriptorInfo(desc);
- descriptors.push({
+ account.descriptors.push({
desc: `${desc}#${checksum}`,
range: [0, 100],
next_index: 0,
@@ -642,34 +646,17 @@ export default {
});
}
- await node.importDescriptors(descriptors);
- let { total_amount } = await node.scanTxOutSet("start", descriptors);
- let amount = Math.round(total_amount * SATS);
- let { balanceAdjustment: memo } = t(user);
+ await node.importDescriptors(account.descriptors);
- if (amount) {
- let hash = v4();
- let inv = {
- memo,
- type: types.reconcile,
- hash,
- amount,
- uid: user.id,
- aid: id,
- };
- await s(`invoice:${hash}`, inv);
- await credit({
- hash,
- amount,
- type: types.reconcile,
- aid: id,
- });
- }
+ await db
+ .multi()
+ .set(`account:${id}`, JSON.stringify(account))
+ .set(`balance:${id}`, 0)
+ .set(`pending:${id}`, 0)
+ .lPush(`${user.id}:accounts`, id)
+ .exec();
- await s(`account:${id}`, account);
- await s(`balance:${id}`, amount);
- await s(`pending:${id}`, 0);
- await db.lPush(`${user.id}:accounts`, id);
+ await reconcile(account);
await appendFile("/bitcoin.conf", `wallet=${id}\n`);
@@ -691,12 +678,25 @@ export default {
},
async deleteAccount(req, res) {
- let { id } = req.body;
- let { id: uid } = req.user;
+ try {
+ let { id } = req.body;
+ let { id: uid } = req.user;
+ let { type } = await g(`account:${id}`);
+ let node = rpc({ ...config[type], wallet: id });
+ await node.unloadWallet(id);
- await db.lRem(`${uid}:accounts`, 1, id);
- await db.del(`account:${id}`);
+ await db
+ .multi()
+ .lRem(`${uid}:accounts`, 1, id)
+ .del(`account:${id}`)
+ .del(`balance:${id}`)
+ .del(`${id}:payments`)
+ .exec();
- res.send({ ok: true });
+ res.send({ ok: true });
+ } catch (e) {
+ console.log(e);
+ bail(res, e.message);
+ }
},
};