diff options
| author | Adam Soltys <[email protected]> | 2024-09-24 12:30:51 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2024-09-24 12:30:51 -0700 |
| commit | 80365c227c5a774ad65f5dad8b989e1966630160 (patch) | |
| tree | a9d6cd130e48e815fb60b91263c5d9b5a83b8808 | |
| parent | 8c8c71d54d23960ffc4caa94619bae7c6a69514b (diff) | |
| download | coinos-server-80365c227c5a774ad65f5dad8b989e1966630160.tar.gz coinos-server-80365c227c5a774ad65f5dad8b989e1966630160.zip | |
reconcile onchain balance
| -rw-r--r-- | lib/invoices.ts | 5 | ||||
| -rw-r--r-- | lib/payments.ts | 72 | ||||
| -rw-r--r-- | lib/register.ts | 20 | ||||
| -rw-r--r-- | routes/payments.ts | 18 | ||||
| -rw-r--r-- | routes/users.ts | 90 |
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); + } }, }; |
