aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-02-13 15:33:37 -0800
committerAdam Soltys <[email protected]>2026-02-13 15:33:37 -0800
commitb2b9fe837db7308285909ec1b374546194405d00 (patch)
treebe9dbc10c7f0699b40ac2ed2534869e6b9e772e7
parent542cf1d084d189bcdf127c9c2359cced5267295e (diff)
downloadcoinos-server-b2b9fe837db7308285909ec1b374546194405d00.tar.gz
coinos-server-b2b9fe837db7308285909ec1b374546194405d00.zip
bolt12 autowithdraw
-rw-r--r--bun.lock4
-rw-r--r--lib/payments.ts35
-rw-r--r--package.json2
-rw-r--r--test/forward.test.ts150
-rw-r--r--test/integration/bolt12.test.ts307
-rw-r--r--test/preload.ts8
6 files changed, 494 insertions, 12 deletions
diff --git a/bun.lock b/bun.lock
index 50734b00..a75b0cce 100644
--- a/bun.lock
+++ b/bun.lock
@@ -6,7 +6,7 @@
"name": "coinos",
"dependencies": {
"@arkade-os/sdk": "^0.3.12",
- "@asoltys/clightning-client": "^0.1.10",
+ "@asoltys/clightning-client": "^0.2.0",
"@aws-sdk/client-ses": "^3.984.0",
"@cashu/cashu-ts": "^2.9.0",
"@coinos/rpc": "^0.0.6",
@@ -84,7 +84,7 @@
"@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=="],
- "@asoltys/clightning-client": ["@asoltys/[email protected]", "", { "dependencies": { "debug": "^4.1.1" } }, "sha512-Qg0VEvJN1wW4XeW4XnsQH+Vp63JHcVb2YccQEg+wy33BViu2AWFhZzvHhlbucriIOVLmIf7Wc1Me54ivs1sLHA=="],
+ "@asoltys/clightning-client": ["@asoltys/[email protected]", "", { "dependencies": { "debug": "^4.1.1" } }, "sha512-CYbkyGoS0X1ha/YcFk3sTiI7aRHpPMnib/26WODucfJNrTbWl/9FJ4bpod9a6t9Jbv5OXCa0dYspcPK3kZ71pg=="],
"@aws-crypto/sha256-browser": ["@aws-crypto/[email protected]", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="],
diff --git a/lib/payments.ts b/lib/payments.ts
index cc1391fe..3b6a4a86 100644
--- a/lib/payments.ts
+++ b/lib/payments.ts
@@ -521,13 +521,30 @@ const pay = async ({ aid = undefined, amount, to, user }) => {
});
}
- const fee = Math.max(5, Math.round(amount * 0.02));
+ let fee;
+ const ourfee = Math.round(amount * (config.fee.lightning || 0));
if (lnurl) {
- amount -= fee;
+ fee = Math.max(5, Math.round(amount * 0.02));
+ amount -= fee + ourfee;
const { callback } = (await got(lnurl).json()) as any;
({ pr } = (await got(`${callback}?amount=${amount * 1000}`).json()) as any);
+ } else if (to.startsWith("lno")) {
+ const { id: source } = await ln.getinfo();
+ const { offer_issuer_id } = await ln.decode(to);
+ const { routes } = await ln.getroutes(
+ source, offer_issuer_id, amount * 1000,
+ ["auto.localchans", "auto.sourcefree"],
+ Math.round(amount * 0.02) * 1000, 6,
+ );
+ fee = routes.length
+ ? Math.max(5, Math.round((routes[0].path[0].amount_msat - routes[0].amount_msat) / 1000))
+ : 0;
+ amount -= fee + ourfee;
+ const { invoice } = await ln.fetchinvoice(to, amount * 1000);
+ pr = invoice;
} else if (to.startsWith("ln")) {
- amount -= fee;
+ fee = Math.max(5, Math.round(amount * 0.02));
+ amount -= fee + ourfee;
pr = to;
}
@@ -763,7 +780,7 @@ export const sendKeysend = async ({
destination: pubkey,
amount_msat: amount * 1000,
maxfee: fee * 1000,
- retry_for: 10,
+ retry_for: 20,
extratlvs,
});
} catch (e) {
@@ -796,9 +813,9 @@ export const sendLightning = async ({
payee = invoice_node_id;
}
- let minfee = 2;
const { channels } = await ln.listpeerchannels();
- if (channels.some((c) => c.peer_id === payee)) minfee = 0;
+ const isDirect = channels.some((c) => c.peer_id === payee);
+ const minfee = isDirect ? 0 : Math.max(Math.round(amount * 0.005), 5);
fee = Math.max(Number.parseInt(fee || 0), minfee);
if (fee < 0) fail("Fee cannot be negative");
@@ -829,7 +846,7 @@ export const sendLightning = async ({
// bolt11: pr.replace(/\s/g, "").toLowerCase(),
amount_msat: amount_msat ? undefined : amount * 1000,
maxfee: fee * 1000,
- retry_for: 10,
+ retry_for: 20,
});
try {
@@ -1396,8 +1413,8 @@ const finalize = async (r, p) => {
nwcNotify(p);
const maxfee = p.fee;
- const { amount_msat } = await ln.decode(p.hash);
- p.fee = Math.round((r.amount_sent_msat - amount_msat) / 1000);
+ const { amount_msat, invoice_amount_msat } = await ln.decode(p.hash);
+ p.fee = Math.round((r.amount_sent_msat - (amount_msat || invoice_amount_msat)) / 1000);
p.ref = preimage;
if (!(await g(`payment:${p.id}`)).ref) {
diff --git a/package.json b/package.json
index ea8177f1..b960b7ff 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,7 @@
},
"dependencies": {
"@arkade-os/sdk": "^0.3.12",
- "@asoltys/clightning-client": "^0.1.10",
+ "@asoltys/clightning-client": "^0.2.0",
"@aws-sdk/client-ses": "^3.984.0",
"@cashu/cashu-ts": "^2.9.0",
"@coinos/rpc": "^0.0.6",
diff --git a/test/forward.test.ts b/test/forward.test.ts
index 1e0cd566..7f548b67 100644
--- a/test/forward.test.ts
+++ b/test/forward.test.ts
@@ -11,6 +11,9 @@ const mockTbCredit = tbCredit as any;
const mockWarn = warn as any;
const mockCallWebhook = callWebhook as any;
const mockLnXpay = ln.xpay as any;
+const mockLnFetchinvoice = ln.fetchinvoice as any;
+const mockLnDecode = ln.decode as any;
+const mockLnGetroutes = ln.getroutes as any;
import { completePayment } from "$lib/payments";
import routes from "$routes/payments";
@@ -77,12 +80,23 @@ const resetAll = () => {
mockWarn.mockClear();
mockCallWebhook.mockClear();
mockLnXpay.mockClear();
+ mockLnFetchinvoice.mockClear();
+ mockLnDecode.mockClear();
+ mockLnGetroutes.mockClear();
+
+ mockLnGetroutes.mockImplementation(async () => ({ routes: [] }));
mockLnXpay.mockImplementation(async () => ({
amount_sent_msat: 1_000_000,
payment_preimage: "preimage-abc123",
}));
+ mockLnDecode.mockImplementation(async () => ({
+ type: "bolt11",
+ amount_msat: 1_000_000,
+ payee: "test-payee",
+ }));
+
// Seed rates and limits
s.kvStore["rates"] = JSON.stringify({ USD: 50000, CAD: 68000 });
s.kvStore["limit"] = JSON.stringify(100_000_000);
@@ -361,3 +375,139 @@ describe("arkSync", () => {
expect(updated.confirmed).toBe(true);
});
});
+
+// =====================================================================
+// Tests: BOLT12 offer autowithdraw
+// =====================================================================
+
+const bolt12Offer = "lno1qgsqvgnwgcg35z6ee2h3yczraddm72xrfua9uve2rlrm9deu7xyfzrcgqyqs";
+const bolt12Invoice = "lni1qqg86n2pddz86n2ptszien5gqp3zzqmpkv93rjd";
+
+describe("BOLT12 autowithdraw", () => {
+ beforeEach(resetAll);
+
+ test("fetches invoice from BOLT12 offer and sends via Lightning", async () => {
+ mockLnFetchinvoice.mockImplementation(async () => ({
+ invoice: bolt12Invoice,
+ }));
+
+ mockLnDecode.mockImplementation(async (s: string) =>
+ s.startsWith("lno")
+ ? { type: "bolt12 offer", offer_issuer_id: "bolt12-payee" }
+ : { type: "bolt12 invoice", invoice_amount_msat: 980_000, invoice_node_id: "bolt12-payee" },
+ );
+
+ const user = makeUser({
+ autowithdraw: true,
+ threshold: 500,
+ reserve: 0,
+ destination: bolt12Offer,
+ });
+ seedUser(user);
+
+ const inv = makeInvoice({ type: "lightning", amount: 1000 });
+ const p = { amount: 1000, confirmed: true, type: "lightning", uid: custodialUid, created: Date.now() };
+
+ await completePayment(inv, p, user);
+
+ // decode called first with the offer to get node id
+ expect(mockLnDecode).toHaveBeenCalled();
+ expect(mockLnDecode.mock.calls[0][0]).toBe(bolt12Offer);
+
+ // getroutes called to estimate routing fee
+ expect(mockLnGetroutes).toHaveBeenCalled();
+
+ // fetchinvoice called with the offer and amount in msats
+ expect(mockLnFetchinvoice).toHaveBeenCalled();
+ const [offer, amountMsat] = mockLnFetchinvoice.mock.calls[0];
+ expect(offer).toBe(bolt12Offer);
+ expect(amountMsat).toBeGreaterThan(0);
+
+ // xpay called with the fetched bolt12 invoice
+ expect(mockLnXpay).toHaveBeenCalled();
+ });
+
+ test("forwards BOLT12 offer via invoice forward field", async () => {
+ mockLnFetchinvoice.mockImplementation(async () => ({
+ invoice: bolt12Invoice,
+ }));
+
+ mockLnDecode.mockImplementation(async (s: string) =>
+ s.startsWith("lno")
+ ? { type: "bolt12 offer", offer_issuer_id: "bolt12-payee" }
+ : { type: "bolt12 invoice", invoice_amount_msat: 980_000, invoice_node_id: "bolt12-payee" },
+ );
+
+ const user = makeUser();
+ const inv = makeInvoice({ forward: bolt12Offer });
+ const p = { amount: 1000, confirmed: true, type: "lightning", uid: custodialUid, created: Date.now() };
+
+ await completePayment(inv, p, user);
+
+ expect(mockLnFetchinvoice).toHaveBeenCalled();
+ expect(mockLnXpay).toHaveBeenCalled();
+ expect(inv.forwarded).toBe(true);
+ });
+
+ test("regular ln invoice is NOT passed through fetchinvoice", async () => {
+ const user = makeUser({
+ autowithdraw: true,
+ threshold: 500,
+ reserve: 0,
+ destination: bolt11,
+ });
+ seedUser(user);
+
+ const inv = makeInvoice({ type: "lightning", amount: 1000 });
+ const p = { amount: 1000, confirmed: true, type: "lightning", uid: custodialUid, created: Date.now() };
+
+ await completePayment(inv, p, user);
+
+ expect(mockLnFetchinvoice).not.toHaveBeenCalled();
+ expect(mockLnXpay).toHaveBeenCalled();
+ });
+
+ test("accounts for routing fee from getroutes on multi-hop", async () => {
+ // Mock balance is 10_000_000; autowithdraw sends balance - reserve
+ const balance = 10_000_000;
+ const routingFee = 50;
+
+ mockLnDecode.mockImplementation(async (s: string) =>
+ s.startsWith("lno")
+ ? { type: "bolt12 offer", offer_issuer_id: "bolt12-payee" }
+ : { type: "bolt12 invoice", invoice_amount_msat: null, invoice_node_id: "bolt12-payee" },
+ );
+
+ // Simulate a route with 50 sat routing fee (getroutes response format)
+ mockLnGetroutes.mockImplementation(async () => ({
+ routes: [{
+ amount_msat: balance * 1000,
+ path: [{ amount_msat: (balance + routingFee) * 1000 }],
+ }],
+ }));
+
+ mockLnFetchinvoice.mockImplementation(async () => ({
+ invoice: bolt12Invoice,
+ }));
+
+ const user = makeUser({
+ autowithdraw: true,
+ threshold: 500,
+ reserve: 0,
+ destination: bolt12Offer,
+ });
+ seedUser(user);
+
+ const inv = makeInvoice({ type: "lightning", amount: 1000 });
+ const p = { amount: 1000, confirmed: true, type: "lightning", uid: custodialUid, created: Date.now() };
+
+ await completePayment(inv, p, user);
+
+ // fetchinvoice amount should exclude routing fee and ourfee
+ const [, amountMsat] = mockLnFetchinvoice.mock.calls[0];
+ const ourfee = Math.round(balance * 0.001);
+ expect(amountMsat).toBe((balance - routingFee - ourfee) * 1000);
+
+ expect(mockLnXpay).toHaveBeenCalled();
+ });
+});
diff --git a/test/integration/bolt12.test.ts b/test/integration/bolt12.test.ts
new file mode 100644
index 00000000..06929c6c
--- /dev/null
+++ b/test/integration/bolt12.test.ts
@@ -0,0 +1,307 @@
+process.env.INTEGRATION = "1";
+
+import { describe, test, expect, beforeAll } from "bun:test";
+
+// =====================================================================
+// Helpers
+// =====================================================================
+
+const APP = "http://localhost:3119";
+
+const exec = async (cmd: string): Promise<string> => {
+ const proc = Bun.spawn(["bash", "-c", cmd], {
+ stdout: "pipe",
+ stderr: "pipe",
+ });
+ const stdout = await new Response(proc.stdout).text();
+ const code = await proc.exited;
+ if (code !== 0) {
+ const stderr = await new Response(proc.stderr).text();
+ throw new Error(`exec failed (${code}): ${stderr || stdout}`);
+ }
+ return stdout.trim();
+};
+
+const clExec = async (container: string, ...args: string[]): Promise<any> => {
+ const escaped = args.map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(" ");
+ const result = await exec(
+ `docker exec ${container} lightning-cli ${escaped}`,
+ );
+ try {
+ return JSON.parse(result);
+ } catch {
+ return result;
+ }
+};
+
+const mine = async (n: number) => {
+ const addr = await exec(
+ "docker exec bc bitcoin-cli -regtest -rpcwallet=coinos getnewaddress",
+ );
+ await exec(
+ `docker exec bc bitcoin-cli -regtest generatetoaddress ${n} ${addr}`,
+ );
+};
+
+const waitFor = async <T>(
+ fn: () => Promise<T>,
+ timeout = 30000,
+): Promise<T> => {
+ const start = Date.now();
+ let lastError: any;
+ while (Date.now() - start < timeout) {
+ try {
+ const result = await fn();
+ if (result) return result;
+ } catch (e) {
+ lastError = e;
+ }
+ await Bun.sleep(500);
+ }
+ throw new Error(`waitFor timed out: ${lastError?.message || "no result"}`);
+};
+
+const register = async (username: string, password: string) => {
+ const res = await fetch(`${APP}/register`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ user: { username, password } }),
+ });
+ if (!res.ok) throw new Error(`register failed: ${await res.text()}`);
+ return res.json();
+};
+
+const api = async (path: string, token: string, opts: any = {}) => {
+ const res = await fetch(`${APP}${path}`, {
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${token}`,
+ ...opts.headers,
+ },
+ ...opts,
+ });
+ return res.json();
+};
+
+const updateUser = (token: string, settings: any) =>
+ api("/user", token, {
+ method: "POST",
+ body: JSON.stringify(settings),
+ });
+
+const getMe = (token: string) => api("/me", token);
+
+const createInvoice = (token: string, invoice: any) =>
+ api("/invoice", token, {
+ method: "POST",
+ body: JSON.stringify({ invoice }),
+ });
+
+const sendInternal = (token: string, username: string, amount: number) =>
+ api("/send", token, {
+ method: "POST",
+ body: JSON.stringify({ username, amount }),
+ });
+
+const getPayments = (token: string) => api("/payments", token);
+
+// =====================================================================
+// Test state
+// =====================================================================
+
+let funderToken: string;
+let withdrawerToken: string;
+let funderUser: any;
+let withdrawerUser: any;
+
+// =====================================================================
+// Setup: register users, fund the funder via lightning
+// =====================================================================
+
+const ts = Date.now();
+const funderName = `intfunder${ts}`;
+const withdrawerName = `intwdraw${ts}`;
+
+beforeAll(async () => {
+ // Verify containers are running
+ try {
+ await exec("docker exec cl lightning-cli getinfo");
+ await exec("docker exec clb lightning-cli getinfo");
+ await exec("docker exec clc lightning-cli getinfo");
+ } catch {
+ throw new Error(
+ "Lightning containers not running. Start with: docker compose up -d cl clb clc",
+ );
+ }
+
+ // Register test users
+ const funder = await register(funderName, "testpass123");
+ funderToken = funder.token;
+ funderUser = funder;
+
+ const withdrawer = await register(withdrawerName, "testpass123");
+ withdrawerToken = withdrawer.token;
+ withdrawerUser = withdrawer;
+
+ // Fund the funder: generate a lightning invoice and pay from clb
+ const inv = await createInvoice(funderToken, {
+ amount: 500_000,
+ type: "lightning",
+ });
+
+ await clExec("clb", "pay", inv.hash);
+
+ // Wait for the payment to be credited
+ await waitFor(async () => {
+ const me = await getMe(funderToken);
+ return me.balance >= 500_000 ? me : null;
+ });
+}, 60000);
+
+// =====================================================================
+// Tests
+// =====================================================================
+
+describe("BOLT12 autowithdraw", () => {
+ test("autowithdraw to direct peer (clb)", async () => {
+ // Create a BOLT12 offer on clb
+ const offer = await clExec("clb", "offer", "any", "integration-test");
+ expect(offer.bolt12).toBeTruthy();
+
+ // Get clb's initial balance
+ const clbFundsBefore = await clExec("clb", "listfunds");
+ const clbBalanceBefore = clbFundsBefore.channels.reduce(
+ (s: number, c: any) => s + c.our_amount_msat,
+ 0,
+ );
+
+ // Configure withdrawer with autowithdraw to clb's offer
+ await updateUser(withdrawerToken, {
+ autowithdraw: "lightning",
+ threshold: 1000,
+ reserve: 0,
+ destination: offer.bolt12,
+ });
+
+ // Send 100k sats from funder to withdrawer (triggers autowithdraw)
+ const sendAmount = 100_000;
+ const sendResult = await sendInternal(
+ funderToken,
+ withdrawerName,
+ sendAmount,
+ );
+ expect(sendResult.amount).toBe(-sendAmount);
+
+ // Wait for autowithdraw to complete — withdrawer balance should drop near 0
+ const finalMe = await waitFor(async () => {
+ const me = await getMe(withdrawerToken);
+ return me.balance < 1000 ? me : null;
+ }, 20000);
+
+ expect(finalMe.balance).toBeLessThan(1000);
+
+ // clb should have received the payment
+ const clbFundsAfter = await clExec("clb", "listfunds");
+ const clbBalanceAfter = clbFundsAfter.channels.reduce(
+ (s: number, c: any) => s + c.our_amount_msat,
+ 0,
+ );
+ expect(clbBalanceAfter).toBeGreaterThan(clbBalanceBefore);
+
+ // Check payment records on withdrawer
+ const payments = await getPayments(withdrawerToken);
+ const withdrawal = payments.payments?.find(
+ (p: any) => p.amount < 0 && p.type === "lightning",
+ );
+ expect(withdrawal).toBeTruthy();
+ expect(withdrawal.fee).toBeGreaterThanOrEqual(0);
+ }, 30000);
+
+ test("autowithdraw routes through clb to clc (multi-hop)", async () => {
+ // Create a BOLT12 offer on clc
+ const offer = await clExec("clc", "offer", "any", "multihop-test");
+ expect(offer.bolt12).toBeTruthy();
+
+ // Configure withdrawer with autowithdraw to clc's offer
+ await updateUser(withdrawerToken, {
+ autowithdraw: "lightning",
+ threshold: 1000,
+ reserve: 0,
+ destination: offer.bolt12,
+ });
+
+ // Get clc's balance before
+ const clcFundsBefore = await clExec("clc", "listfunds");
+ const clcBalanceBefore = clcFundsBefore.channels.reduce(
+ (s: number, c: any) => s + c.our_amount_msat,
+ 0,
+ );
+
+ // Send 50k sats from funder to withdrawer
+ const sendAmount = 50_000;
+ await sendInternal(funderToken, withdrawerName, sendAmount);
+
+ // Wait for autowithdraw to complete
+ const finalMe = await waitFor(async () => {
+ const me = await getMe(withdrawerToken);
+ return me.balance < 1000 ? me : null;
+ }, 30000);
+
+ expect(finalMe.balance).toBeLessThan(1000);
+
+ // clc should have received the payment (routed cl→clb→clc)
+ const clcFundsAfter = await clExec("clc", "listfunds");
+ const clcBalanceAfter = clcFundsAfter.channels.reduce(
+ (s: number, c: any) => s + c.our_amount_msat,
+ 0,
+ );
+ expect(clcBalanceAfter).toBeGreaterThan(clcBalanceBefore);
+
+ // Check payment record has routing fee
+ const payments = await getPayments(withdrawerToken);
+ const withdrawal = payments.payments?.find(
+ (p: any) => p.amount < 0 && p.type === "lightning",
+ );
+ expect(withdrawal).toBeTruthy();
+ // Multi-hop should have non-zero routing fee
+ expect(withdrawal.fee).toBeGreaterThan(0);
+ }, 45000);
+
+ test("finalize() refunds unused routing budget", async () => {
+ // Create offer on clb (direct peer — minimal actual routing cost)
+ const offer = await clExec("clb", "offer", "any", "refund-test");
+
+ await updateUser(withdrawerToken, {
+ autowithdraw: "lightning",
+ threshold: 1000,
+ reserve: 0,
+ destination: offer.bolt12,
+ });
+
+ // Send 20k sats
+ const sendAmount = 20_000;
+ await sendInternal(funderToken, withdrawerName, sendAmount);
+
+ // Wait for autowithdraw
+ await waitFor(async () => {
+ const me = await getMe(withdrawerToken);
+ return me.balance < 1000 ? me : null;
+ }, 20000);
+
+ // Get the withdrawal payment record
+ const payments = await getPayments(withdrawerToken);
+ const withdrawal = payments.payments?.find(
+ (p: any) => p.amount < 0 && p.type === "lightning",
+ );
+ expect(withdrawal).toBeTruthy();
+
+ // For a direct peer, actual routing fee should be 0 or very small
+ // The pre-allocated budget (2% or getroutes estimate) should have been refunded
+ // via finalize(), so p.fee reflects the actual cost, not the budget
+ expect(withdrawal.fee).toBeLessThan(Math.abs(withdrawal.amount) * 0.02);
+
+ // Verify the refund happened — user should have gotten back the difference
+ // between the pre-allocated fee budget and the actual fee
+ expect(withdrawal.ref).toBeTruthy(); // preimage set by finalize()
+ }, 30000);
+});
diff --git a/test/preload.ts b/test/preload.ts
index 9541dacd..a3d3cbb9 100644
--- a/test/preload.ts
+++ b/test/preload.ts
@@ -3,6 +3,11 @@ import { mock } from "bun:test";
// This file is loaded before test files via bunfig.toml [test].preload
// Mocks must be set up here to intercept transitive imports
+if (process.env.INTEGRATION) {
+ // Skip all mocking — use real services
+ globalThis.__testStore = { kvStore: {}, listStore: {}, setStore: {} };
+} else {
+
let kvStore: Record<string, string> = {};
let listStore: Record<string, string[]> = {};
let setStore: Record<string, Set<string>> = {};
@@ -183,6 +188,7 @@ mock.module("$lib/ln", () => ({
listfunds: mock(async () => ({ channels: [] })),
keysend: mock(async () => ({})),
fetchinvoice: mock(async () => ({})),
+ getroutes: mock(async () => ({ routes: [] })),
sendinvoice: mock(async () => ({})),
},
}));
@@ -303,3 +309,5 @@ mock.module("$lib/utils", () => {
time: () => ({ start: () => {}, end: () => {} }),
};
});
+
+} // end INTEGRATION skip