aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-02-14 15:46:01 -0800
committerAdam Soltys <[email protected]>2026-02-14 15:46:01 -0800
commit526b21bc668c453250ebce8dece9ba330bac0133 (patch)
tree5e9c84a2cca6b4a5766c6cdc9f33f6c62f5776f3
parent843c343db567cd224dadf347ac16736bdc8b24a8 (diff)
downloadcoinos-server-526b21bc668c453250ebce8dece9ba330bac0133.tar.gz
coinos-server-526b21bc668c453250ebce8dece9ba330bac0133.zip
custodial ark sends
-rw-r--r--bun.lock6
-rw-r--r--lib/ark.ts14
-rw-r--r--lib/db.ts8
-rw-r--r--lib/invoices.ts18
-rw-r--r--lib/lightning.ts4
-rw-r--r--lib/migrate.ts12
-rw-r--r--lib/payments.ts22
-rw-r--r--package.json2
-rw-r--r--routes/payments.ts66
-rw-r--r--routes/users.ts4
10 files changed, 107 insertions, 49 deletions
diff --git a/bun.lock b/bun.lock
index a75b0cce..9c5de8d0 100644
--- a/bun.lock
+++ b/bun.lock
@@ -5,7 +5,7 @@
"": {
"name": "coinos",
"dependencies": {
- "@arkade-os/sdk": "^0.3.12",
+ "@arkade-os/sdk": "0.3.13",
"@asoltys/clightning-client": "^0.2.0",
"@aws-sdk/client-ses": "^3.984.0",
"@cashu/cashu-ts": "^2.9.0",
@@ -82,7 +82,7 @@
"@apimatic/schema": ["@apimatic/[email protected]", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-RCke4toXjA7fBRxQVa1GR+Lj9utVOEJ3voDI26dhk+bZuAac4UXPzkTEaIO3AIe/o8pcKCOkpNIzhzm57Cv2Qg=="],
- "@arkade-os/sdk": ["@arkade-os/[email protected]", "", { "dependencies": { "@marcbachmann/cel-js": "7.0.0", "@noble/curves": "2.0.0", "@noble/secp256k1": "3.0.0", "@scure/base": "2.0.0", "@scure/btc-signer": "2.0.1", "bip68": "1.0.4" } }, "sha512-/zE8XiDo6yOCaspMcO3Gua/gLGYmYLRFbqaD1ORkaGI7hH/Gq+m0tkIwArVlwtK50loHK4jvS8clovcaV9CARg=="],
+ "@arkade-os/sdk": ["@arkade-os/[email protected]", "", { "dependencies": { "@marcbachmann/cel-js": "7.3.1", "@noble/curves": "2.0.0", "@noble/secp256k1": "3.0.0", "@scure/base": "2.0.0", "@scure/btc-signer": "2.0.1", "bip68": "1.0.4" } }, "sha512-eC6XGifqVf2Xu+sf/3T2duUgLiXlgh/IQXHD+v09wsu8ik/CZedH4gvQpetTjRMKti3DdY/QVLC1OQTKKWgvqQ=="],
"@asoltys/clightning-client": ["@asoltys/[email protected]", "", { "dependencies": { "debug": "^4.1.1" } }, "sha512-CYbkyGoS0X1ha/YcFk3sTiI7aRHpPMnib/26WODucfJNrTbWl/9FJ4bpod9a6t9Jbv5OXCa0dYspcPK3kZ71pg=="],
@@ -262,7 +262,7 @@
"@lukeed/ms": ["@lukeed/[email protected]", "", {}, "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA=="],
- "@marcbachmann/cel-js": ["@marcbachmann/[email protected]", "", {}, "sha512-sbtR8pvTAsDPiyigebwLyqweLnocmBS9MG5tTnVUL2PZ9Hc2Dj2e4tnvTcZ1s463Og9NKD59TaPvTK9CEZQhiQ=="],
+ "@marcbachmann/cel-js": ["@marcbachmann/[email protected]", "", { "bin": { "cel-evaluate": "bin/cel-evaluate.js" } }, "sha512-P6o26TvjStT8V8+8EF+yq9Pp7ZFV00bpiUMbssr76XbIZGxaB+NNWeBp6WNxOrR9gp0JPzvJueCKHpOs5LE9PQ=="],
"@noble/ciphers": ["@noble/[email protected]", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="],
diff --git a/lib/ark.ts b/lib/ark.ts
index 00ea4db4..0d2d180f 100644
--- a/lib/ark.ts
+++ b/lib/ark.ts
@@ -9,16 +9,22 @@ let wallet: any;
const getWallet = async () => {
if (wallet) return wallet;
- const { arkPrivateKey, arkServerUrl } = config.ark;
+ const { arkPrivateKey, arkServerUrl, esploraUrl } = config.ark;
const identity = SingleKey.fromHex(arkPrivateKey);
- wallet = await Wallet.create({ identity, arkServerUrl });
+ wallet = await Wallet.create({ identity, arkServerUrl, esploraUrl });
return wallet;
};
export const sendArk = async (address: string, amount: number) => {
- const w = await getWallet();
- return w.sendBitcoin({ address, amount });
+ const timeout = new Promise((_, reject) =>
+ setTimeout(() => reject(new Error("Ark send timed out")), 45000),
+ );
+ const send = async () => {
+ const w = await getWallet();
+ return w.sendBitcoin({ address, amount });
+ };
+ return Promise.race([send(), timeout]);
};
export const getArkAddress = async () => {
diff --git a/lib/db.ts b/lib/db.ts
index 5ebe38b5..61e542ae 100644
--- a/lib/db.ts
+++ b/lib/db.ts
@@ -67,9 +67,15 @@ export const g = async (k) => {
export const s = (k, v) => {
if (k === "user:null" || k === "user:undefined") fail("null user");
- db.set(k, JSON.stringify(v));
+ return db.set(k, JSON.stringify(v));
};
+export async function* scan(pattern: string) {
+ for await (const keys of db.scanIterator({ MATCH: pattern })) {
+ for (const k of keys) yield k;
+ }
+}
+
export const ga = async (k) => {
const v = await archive.get(k);
try {
diff --git a/lib/invoices.ts b/lib/invoices.ts
index bf6c511e..f7bb6897 100644
--- a/lib/invoices.ts
+++ b/lib/invoices.ts
@@ -69,9 +69,25 @@ export const generate = async ({ invoice, user }) => {
let path;
let paymentHash;
- if (account.type === "ark") {
+ if (type === PaymentType.ark && account.type !== "ark") {
+ // Custodial account receiving via Ark: use vault's ark address
+ const accounts = await db.lRange(`${user.id}:accounts`, 0, -1);
+ let vaultAddress;
+ for (const accId of accounts) {
+ const acc = await g(`account:${accId}`);
+ if (acc?.type === "ark" && acc.arkAddress) {
+ vaultAddress = acc.arkAddress;
+ break;
+ }
+ }
+ if (!vaultAddress) fail("No ark vault found");
+ text = vaultAddress;
+ hash = id;
+ await s(`custodial-ark-invoice:${vaultAddress}`, id);
+ } else if (account.type === "ark") {
type = PaymentType.ark;
text = account.arkAddress;
+ hash = id;
} else if (account.seed || (account.pubkey && account.fingerprint)) {
type = PaymentType.bitcoin;
const nextIndex = account.nextIndex || 0;
diff --git a/lib/lightning.ts b/lib/lightning.ts
index e4c2e3cb..4824d9ab 100644
--- a/lib/lightning.ts
+++ b/lib/lightning.ts
@@ -1,4 +1,4 @@
-import { db, g, s } from "$lib/db";
+import { db, g, s, scan } from "$lib/db";
import ln, { lnListen, LightningUnavailableError } from "$lib/ln";
import { err, l, warn } from "$lib/logging";
import { handleZap } from "$lib/nostr";
@@ -171,7 +171,7 @@ export async function replay(index) {
}
export const fixBolt12 = async (_, res) => {
- for await (const k of db.scanIterator({ MATCH: "payment:*" })) {
+ for await (const k of scan("payment:*")) {
const p = await g(k);
if (p.type === "bolt12") {
console.log(k);
diff --git a/lib/migrate.ts b/lib/migrate.ts
index d0e38276..79f9546c 100644
--- a/lib/migrate.ts
+++ b/lib/migrate.ts
@@ -1,4 +1,4 @@
-import { db, g, s } from "$lib/db";
+import { db, g, s, scan } from "$lib/db";
import { findLastUsedIndex, parseDescriptor } from "$lib/esplora";
import { l, warn } from "$lib/logging";
import {
@@ -64,7 +64,7 @@ export const migrateBalancesToTB = async () => {
let count = 0;
// Migrate balance:* keys
- for await (const k of db.scanIterator({ MATCH: "balance:*" })) {
+ for await (const k of scan("balance:*")) {
try {
const id = k.split(":")[1];
const balance = Number.parseInt((await db.get(k)) || "0");
@@ -77,7 +77,7 @@ export const migrateBalancesToTB = async () => {
}
// Migrate pending:* keys
- for await (const k of db.scanIterator({ MATCH: "pending:*" })) {
+ for await (const k of scan("pending:*")) {
try {
const id = k.split(":")[1];
const pending = Number.parseInt((await db.get(k)) || "0");
@@ -88,7 +88,7 @@ export const migrateBalancesToTB = async () => {
}
// Migrate credit:*:* keys
- for await (const k of db.scanIterator({ MATCH: "credit:*:*" })) {
+ for await (const k of scan("credit:*:*")) {
try {
const parts = k.split(":");
const type = parts[1];
@@ -113,7 +113,7 @@ export const migrateToMicrosats = async () => {
let count = 0;
// Iterate through all users and multiply their TB balances
- for await (const k of db.scanIterator({ MATCH: "user:*" })) {
+ for await (const k of scan("user:*")) {
try {
const raw = await db.get(k);
if (!raw) continue;
@@ -142,7 +142,7 @@ export const migrateAutowithdraw = async () => {
let count = 0;
- for await (const k of db.scanIterator({ MATCH: "user:*" })) {
+ for await (const k of scan("user:*")) {
try {
const raw = await db.get(k);
if (!raw || !raw.startsWith("{")) continue;
diff --git a/lib/payments.ts b/lib/payments.ts
index f6a6bd23..03b52dfa 100644
--- a/lib/payments.ts
+++ b/lib/payments.ts
@@ -520,6 +520,14 @@ const pay = async ({ aid = undefined, amount, to, user }) => {
});
if (to.startsWith("ark") || to.startsWith("tark")) {
+ // Check for local vault - do internal transfer
+ const vault = await g(`arkaddr:${to}`);
+ if (vault) {
+ const recipient = await getUser(vault.uid);
+ if (recipient)
+ return sendInternal({ amount, recipient, sender: user });
+ }
+
const txid = await sendArk(to, amount);
const p = await debit({
hash: txid,
@@ -528,20 +536,6 @@ const pay = async ({ aid = undefined, amount, to, user }) => {
type: PaymentType.ark,
});
- const vault = await g(`arkaddr:${to}`);
- if (vault) {
- const { rate, currency } = await getUserRate(user);
- await createArkPayment({
- aid: vault.aid,
- uid: vault.uid,
- amount,
- hash: txid,
- rate,
- currency,
- extraHashMappings: [txid],
- });
- }
-
return p;
}
diff --git a/package.json b/package.json
index b960b7ff..16e147d6 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,7 @@
"format": "biome format *.js **/*.js --write"
},
"dependencies": {
- "@arkade-os/sdk": "^0.3.12",
+ "@arkade-os/sdk": "0.3.13",
"@asoltys/clightning-client": "^0.2.0",
"@aws-sdk/client-ses": "^3.984.0",
"@cashu/cashu-ts": "^2.9.0",
diff --git a/routes/payments.ts b/routes/payments.ts
index 13e3cb29..5f6f7a85 100644
--- a/routes/payments.ts
+++ b/routes/payments.ts
@@ -774,7 +774,31 @@ export default {
try {
await requirePin({ body, user });
+ // Check for local vault - do internal transfer
+ const vault = await g(`arkaddr:${address}`);
+ if (vault) {
+ const recipient = await getUser(vault.uid);
+ if (recipient) {
+ // Find recipient's pending ark invoice via custodial mapping
+ let invoice;
+ const account = await g(`account:${vault.aid}`);
+ if (account?.arkAddress) {
+ const iid = await g(`custodial-ark-invoice:${account.arkAddress}`);
+ if (iid) {
+ invoice = await getInvoice(iid);
+ if (invoice && !invoice.hash) invoice.hash = invoice.id;
+ }
+ }
+
+ l("ark local send", user.username, recipient.username, amount, "invoice:", invoice?.id, "received:", invoice?.received, "hash:", invoice?.hash);
+ const p = await sendInternal({ amount, invoice, recipient, sender: user });
+ return res.send(p);
+ }
+ }
+
+ l("ark send", user.username, address, amount);
const txid = await sendArk(address, amount);
+ l("ark send complete", txid);
const hash = txid;
const p = await debit({
@@ -785,20 +809,6 @@ export default {
aid,
});
- const vault = await g(`arkaddr:${address}`);
- if (vault) {
- const { rate, currency } = await getUserRate(user);
- await createArkPayment({
- aid: vault.aid,
- uid: vault.uid,
- amount,
- hash,
- rate,
- currency,
- extraHashMappings: [hash],
- });
- }
-
res.send(p);
} catch (e) {
warn(user.username, "ark payment failed", e.message);
@@ -903,6 +913,9 @@ export default {
await completePayment(invoice, p, user);
+ // Clean up custodial ark invoice index
+ if (invoice.text) await db.del(`custodial-ark-invoice:${invoice.text}`);
+
res.send(p);
} catch (e) {
bail(res, e.message);
@@ -982,7 +995,30 @@ export default {
}
}
- res.send({ synced, received, payments });
+ // Check for pending custodial ark invoice to forward
+ let forward;
+ if (received > 0) {
+ const account = await g(`account:${aid}`);
+ l("arkSync forward check", aid, account?.arkAddress, received);
+ if (account?.arkAddress) {
+ const iid = await g(`custodial-ark-invoice:${account.arkAddress}`);
+ l("arkSync custodial invoice lookup", account.arkAddress, iid);
+ if (iid) {
+ const inv = await getInvoice(iid);
+ if (inv && (inv.received < inv.amount || inv.amount === 0)) {
+ const serverArkAddress = await getArkAddress();
+ forward = {
+ iid,
+ amount: inv.amount || received,
+ serverArkAddress,
+ };
+ l("arkSync returning forward", forward);
+ }
+ }
+ }
+ }
+
+ res.send({ synced, received, payments, forward });
} finally {
await db.del(lockKey);
}
diff --git a/routes/users.ts b/routes/users.ts
index cf958927..4435ba6f 100644
--- a/routes/users.ts
+++ b/routes/users.ts
@@ -2,7 +2,7 @@ import { createReadStream } from "node:fs";
import { unlink, writeFile } from "node:fs/promises";
import config from "$config";
import { requirePin } from "$lib/auth";
-import { db, g, ga, gf, s } from "$lib/db";
+import { db, g, ga, gf, s, scan } from "$lib/db";
import { err, l, warn } from "$lib/logging";
import { mail, templates } from "$lib/mail";
import { getNostrUser, getProfile, serverPubkey2 } from "$lib/nostr";
@@ -119,7 +119,7 @@ export default {
const users = [];
- for await (const k of db.scanIterator({ MATCH: "user:*" })) {
+ for await (const k of scan("user:*")) {
const val = await g(k);
if (!val || typeof val !== "object" || !val.id || !val.username) continue;
const uid = val.id;