aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2025-11-26 22:36:59 -0800
committerAdam Soltys <[email protected]>2026-02-06 08:58:10 -0800
commitbc953b56f3908e3a557976b9029b617dad751755 (patch)
tree6e65399fd615c6cf5c7e9382b212a707531e4886
parente95e3b8e35a858cc200de6c0b74e3e172e4f6d6e (diff)
downloadcoinos-server-bc953b56f3908e3a557976b9029b617dad751755.tar.gz
coinos-server-bc953b56f3908e3a557976b9029b617dad751755.zip
ark
-rwxr-xr-xbun.lockbbin230690 -> 231433 bytes
-rw-r--r--lib/invoices.ts19
-rw-r--r--lib/utils.ts4
-rw-r--r--package.json1
-rw-r--r--routes/invoices.ts6
-rw-r--r--routes/users.ts88
6 files changed, 104 insertions, 14 deletions
diff --git a/bun.lockb b/bun.lockb
index 4ff2d8a6..93cb3d29 100755
--- a/bun.lockb
+++ b/bun.lockb
Binary files differ
diff --git a/lib/invoices.ts b/lib/invoices.ts
index 9666d217..6ddffe54 100644
--- a/lib/invoices.ts
+++ b/lib/invoices.ts
@@ -63,17 +63,16 @@ export const generate = async ({ invoice, user }) => {
let text;
let paymentHash;
- if (account.seed) {
- if (type !== PaymentType.ark) {
- type = PaymentType.bitcoin;
- const node = rpc({ ...config[type], wallet: aid });
- hash = await node.getNewAddress({ address_type });
- text = bip21(hash, invoice);
+ if (account.type === "ark") {
+ type = PaymentType.ark;
+ text = account.arkAddress;
+ } else if (account.seed) {
+ type = PaymentType.bitcoin;
+ const node = rpc({ ...config[type], wallet: aid });
+ hash = await node.getNewAddress({ address_type });
+ text = bip21(hash, invoice);
- ({ hdkeypath: path } = await node.getAddressInfo(hash));
- } else {
- text = hash;
- }
+ ({ hdkeypath: path } = await node.getAddressInfo(hash));
} else if (type === PaymentType.lightning) {
let r;
if (bolt11) {
diff --git a/lib/utils.ts b/lib/utils.ts
index add09782..878ef037 100644
--- a/lib/utils.ts
+++ b/lib/utils.ts
@@ -47,6 +47,10 @@ export const bip21 = (address, { amount, memo, tip, type }) => {
return `${network}:${address}?${url.toString()}`;
};
+export const getAccount = async (id) => {
+ return g(`account:${id}`);
+}
+
export const getUser = async (username, fields = undefined) => {
if (username === "undefined") fail("invalid user");
const k = username?.replace(/\s/g, "").toLowerCase();
diff --git a/package.json b/package.json
index 71ce64f7..f682fcf7 100644
--- a/package.json
+++ b/package.json
@@ -26,6 +26,7 @@
"buffer-crc32": "^0.2.13",
"clightning-client": "github:asoltys/lightning-client-js#master",
"date-fns": "^2.30.0",
+ "eventsource": "^4.1.0",
"fastify": "^4.29.1",
"file-type": "^18.7.0",
"fluent-ffmpeg": "^2.1.3",
diff --git a/routes/invoices.ts b/routes/invoices.ts
index 63338621..bc96c3f0 100644
--- a/routes/invoices.ts
+++ b/routes/invoices.ts
@@ -2,9 +2,8 @@ import config from "$config";
import { db, g, gf, s } from "$lib/db";
import { generate } from "$lib/invoices";
import { err } from "$lib/logging";
-import { bail, fail, fields, getInvoice, getUser } from "$lib/utils";
+import { bail, fail, fields, getAccount, getInvoice, getUser } from "$lib/utils";
import rpc from "@coinos/rpc";
-import { v4 } from "uuid";
export default {
async get(req, res) {
@@ -18,6 +17,7 @@ export default {
if (invoice) {
invoice.secret = undefined;
invoice.user = await getUser(invoice.uid, fields);
+ invoice.account = await getAccount(invoice.aid);
invoice.items ||= [];
}
@@ -40,6 +40,8 @@ export default {
const result = await generate({ invoice, user });
res.send(result);
} catch (e) {
+ console.trace();
+ console.log(e);
err(
"problem generating invoice",
req.user?.username,
diff --git a/routes/users.ts b/routes/users.ts
index a25b6a8e..f564d09f 100644
--- a/routes/users.ts
+++ b/routes/users.ts
@@ -11,6 +11,8 @@ import { emit } from "$lib/sockets";
import upload from "$lib/upload";
import { bail, fail, fields, getUser, pick } from "$lib/utils";
import whitelist from "$lib/whitelist";
+import rpc from "@coinos/rpc";
+import { bech32m } from "@scure/base";
import { $ } from "bun";
import got from "got";
@@ -835,6 +837,21 @@ export default {
account.balance = await g(`balance:${id}`);
accounts.push(account);
}
+
+ if (account.type === PaymentType.ark) {
+ const decoded = bech32m.decode(account.arkAddress, 1023);
+ const data = new Uint8Array(bech32m.fromWords(decoded.words));
+
+ const vtxoTaprootKey = data.slice(33, 65);
+
+ const vtxoHex = Buffer.from(vtxoTaprootKey).toString("hex");
+ const r = await fetch(
+ `http://arkd:7070/v1/indexer/vtxos?scripts=5120${vtxoHex}`,
+ { "content-type": "application/json" },
+ );
+ const { vtxos } = await r.json();
+ account.balance = vtxos.reduce((a, b) => a + b.amount, 0);
+ }
}
res.send(accounts.reverse());
@@ -846,11 +863,68 @@ export default {
async createAccount(req, res) {
try {
- const { name, type } = req.body;
+ const { fingerprint, pubkey, name, seed, type, arkAddress, accountIndex } =
+ req.body;
const { user } = req;
+ const { id: uid } = user;
const id = v4();
- const account = { id, name, type, uid: user.id };
+ const account = {
+ id,
+ name,
+ seed,
+ type,
+ uid,
+ descriptors: [],
+ arkAddress,
+ accountIndex,
+ };
+
+ // ARK accounts don't need Bitcoin Core wallet setup
+ if (type === "ark") {
+ await db
+ .multi()
+ .set(`account:${id}`, JSON.stringify(account))
+ .set(`balance:${id}`, 0)
+ .set(`pending:${id}`, 0)
+ .lPush(`${user.id}:accounts`, id)
+ .exec();
+
+ res.send(account);
+ return;
+ }
+
+ // Bitcoin on-chain wallet setup
+ let node = rpc(config[type]);
+
+ await node.createWallet({
+ wallet_name: id,
+ descriptors: true,
+ disable_private_keys: true,
+ load_on_startup: true,
+ });
+
+ node = rpc({ ...config[type], wallet: id });
+
+ for (const i of [0, 1]) {
+ const desc = `wpkh([${fingerprint}]${pubkey}/${i}/*)`;
+ const { checksum } = await node.getDescriptorInfo(desc);
+ account.descriptors.push({
+ desc: `${desc}#${checksum}`,
+ range: [0, 100],
+ next_index: 0,
+ timestamp: "now",
+ internal: i === 1,
+ active: true,
+ });
+ }
+
+ await node.importDescriptors(account.descriptors);
+
+ if (typeof accountIndex !== "undefined") {
+ user.accountIndex = (Number.parseInt(accountIndex) || 0) + 1;
+ await s(`user:${uid}`, user);
+ }
await db
.multi()
@@ -890,6 +964,16 @@ export default {
const pos = await db.lPos(`${uid}:accounts`, id);
if (!(account && pos != null)) fail("account not found");
+ // Only unload Bitcoin Core wallet for non-ARK accounts
+ if (account.type !== "ark") {
+ try {
+ const node = rpc({ ...config[account.type], wallet: id });
+ await node.unloadWallet(id);
+ } catch (e) {
+ warn("failed to unload wallet", id);
+ }
+ }
+
await db
.multi()
.lRem(`${uid}:accounts`, 1, id)