aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-02-03 09:15:39 -0800
committerAdam Soltys <[email protected]>2026-02-06 08:58:12 -0800
commit5e3bfba5eb3d54cc03551f98624a2e92dca5825f (patch)
tree723e0be4e4f0a987fa2375fcf0360022f1232eaa
parent4850177f50b7ff5a0f37acaa92190219d6f72da5 (diff)
downloadcoinos-server-5e3bfba5eb3d54cc03551f98624a2e92dca5825f.tar.gz
coinos-server-5e3bfba5eb3d54cc03551f98624a2e92dca5825f.zip
custodial ark payments
-rwxr-xr-xbun.lockbbin231433 -> 234863 bytes
-rw-r--r--fund-and-settle.sh31
-rw-r--r--inspect-ark.ts23
-rw-r--r--lib/ark.ts32
-rw-r--r--lib/invoices.ts2
-rw-r--r--package.json1
-rw-r--r--routes/payments.ts44
-rw-r--r--routes/users.ts2
8 files changed, 109 insertions, 26 deletions
diff --git a/bun.lockb b/bun.lockb
index 93cb3d29..d596ea75 100755
--- a/bun.lockb
+++ b/bun.lockb
Binary files differ
diff --git a/fund-and-settle.sh b/fund-and-settle.sh
new file mode 100644
index 00000000..103ef055
--- /dev/null
+++ b/fund-and-settle.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+set -e
+
+# 1. Send BTC to boarding address and mine
+echo "Funding boarding address..."
+docker exec bc bitcoin-cli -regtest -rpcuser=admin1 -rpcpassword=123 -rpcwallet=coinos sendtoaddress "bcrt1p4pm6myqffj07g8quw9er2vdnvf8rj7ns4nwwt9l5gxjf8pmjkueqh9hezk" 1
+docker exec bc bitcoin-cli -regtest -rpcuser=admin1 -rpcpassword=123 generatetoaddress 1 "$(docker exec bc bitcoin-cli -regtest -rpcuser=admin1 -rpcpassword=123 -rpcwallet=coinos getnewaddress)"
+echo "Funded and confirmed."
+
+# 2. Start settle in background
+echo "Starting settle..."
+cd /home/adam/coinos-server && bun inspect-ark.ts &
+SETTLE_PID=$!
+
+# 3. Wait for intent to register then mine blocks
+echo "Waiting for intent registration..."
+for i in $(seq 1 60); do
+ sleep 5
+ INTENTS=$(docker exec arkd arkd intents 2>&1)
+ if echo "$INTENTS" | grep -q "boardingInputs"; then
+ echo "Intent registered! Mining block..."
+ docker exec bc bitcoin-cli -regtest -rpcuser=admin1 -rpcpassword=123 generatetoaddress 1 "$(docker exec bc bitcoin-cli -regtest -rpcuser=admin1 -rpcpassword=123 -rpcwallet=coinos getnewaddress)"
+ break
+ fi
+ echo "No intent yet (attempt $i)..."
+done
+
+# 4. Wait for settle to complete
+echo "Waiting for settle..."
+wait $SETTLE_PID
+echo "Done."
diff --git a/inspect-ark.ts b/inspect-ark.ts
new file mode 100644
index 00000000..8c1a551b
--- /dev/null
+++ b/inspect-ark.ts
@@ -0,0 +1,23 @@
+import { EventSource } from "eventsource";
+if (!globalThis.EventSource) globalThis.EventSource = EventSource;
+import { SingleKey, Wallet } from "@arkade-os/sdk";
+
+const key = "0ebe9a690651e9e08d7c2f4db106d8ec264725241fdb34c57aedb26155b0e027";
+const identity = SingleKey.fromHex(key);
+const wallet = await Wallet.create({ identity, arkServerUrl: "http://localhost:7070" });
+
+const balance = await wallet.getBalance();
+console.log("Balance:", JSON.stringify(balance));
+
+console.log("Settling...");
+try {
+ const txid = await wallet.settle();
+ console.log("Settle txid:", txid);
+
+ const balanceAfter = await wallet.getBalance();
+ console.log("Balance after:", JSON.stringify(balanceAfter));
+} catch(e: any) {
+ console.error("Error:", e.message);
+}
+
+process.exit(0);
diff --git a/lib/ark.ts b/lib/ark.ts
new file mode 100644
index 00000000..44fa4fdd
--- /dev/null
+++ b/lib/ark.ts
@@ -0,0 +1,32 @@
+import { EventSource } from "eventsource";
+if (!globalThis.EventSource) globalThis.EventSource = EventSource;
+
+import config from "$config";
+import { SingleKey, Wallet } from "@arkade-os/sdk";
+
+let wallet: any;
+
+const getWallet = async () => {
+ if (wallet) return wallet;
+
+ const { arkPrivateKey, arkServerUrl } = config.ark;
+ const identity = SingleKey.fromHex(arkPrivateKey);
+
+ wallet = await Wallet.create({ identity, arkServerUrl });
+ return wallet;
+};
+
+export const sendArk = async (address: string, amount: number) => {
+ const w = await getWallet();
+ return w.sendBitcoin({ address, amount });
+};
+
+export const getArkAddress = async () => {
+ const w = await getWallet();
+ return w.getAddress();
+};
+
+export const getArkBalance = async () => {
+ const w = await getWallet();
+ return w.getBalance();
+};
diff --git a/lib/invoices.ts b/lib/invoices.ts
index 6ddffe54..50cddd9a 100644
--- a/lib/invoices.ts
+++ b/lib/invoices.ts
@@ -131,6 +131,8 @@ export const generate = async ({ invoice, user }) => {
} else if (type === PaymentType.ecash) {
hash = id;
text = request(id, amount, memo);
+ } else if (type === PaymentType.ark) {
+ hash = id;
} else {
fail(`unrecognized type ${type}`);
}
diff --git a/package.json b/package.json
index f682fcf7..f5132e71 100644
--- a/package.json
+++ b/package.json
@@ -8,6 +8,7 @@
"format": "biome format *.js **/*.js --write"
},
"dependencies": {
+ "@arkade-os/sdk": "^0.3.12",
"@asoltys/clightning-client": "^0.1.10",
"@aws-sdk/client-ses": "^3.886.0",
"@cashu/cashu-ts": "^2.7.2",
diff --git a/routes/payments.ts b/routes/payments.ts
index 752c7ce8..9b079e11 100644
--- a/routes/payments.ts
+++ b/routes/payments.ts
@@ -1,5 +1,6 @@
import config from "$config";
import api from "$lib/api";
+import { sendArk } from "$lib/ark";
import { requirePin } from "$lib/auth";
import { archive, db, g, gf, s } from "$lib/db";
import { generate } from "$lib/invoices";
@@ -751,34 +752,27 @@ export default {
},
async ark(req, res) {
- const { user } = req;
- const { amount, hash, aid } = req.body;
+ const { body, user } = req;
+ const { address, amount, aid } = body;
- const { currency } = user;
- const rates = await g("rates");
- const rate = rates[currency];
- const p = {
- id: v4(),
- aid,
- amount: -amount,
- hash,
- confirmed: true,
- rate,
- currency,
- type: PaymentType.ark,
- created: Date.now(),
- };
+ try {
+ await requirePin({ body, user });
+ const txid = await sendArk(address, amount);
+ const hash = txid;
- const { id: uid } = user;
- await s(`payment:${hash}`, p.id);
- await s(`payment:${p.id}`, p);
- await db
- .multi()
- .lPush(`${aid || uid}:payments`, p.id)
- .set(`${aid || uid}:payments:last`, p.created)
- .exec();
+ const p = await debit({
+ hash,
+ amount,
+ user,
+ type: PaymentType.ark,
+ aid,
+ });
- res.send(p);
+ res.send(p);
+ } catch (e) {
+ warn(user.username, "ark payment failed", e.message);
+ res.code(500).send(e.message);
+ }
},
async arkReceive(req, res) {
diff --git a/routes/users.ts b/routes/users.ts
index ca15924f..a63ac4ae 100644
--- a/routes/users.ts
+++ b/routes/users.ts
@@ -846,7 +846,7 @@ export default {
const vtxoHex = Buffer.from(vtxoTaprootKey).toString("hex");
const r = await fetch(
- `http://arkd:7070/v1/indexer/vtxos?scripts=5120${vtxoHex}`,
+ `http://arkd:7070/v1/indexer/vtxos?scripts=5120${vtxoHex}&spendable_only=true`,
{ "content-type": "application/json" },
);
const { vtxos } = await r.json();