diff options
| author | Adam Soltys <[email protected]> | 2026-02-13 15:33:37 -0800 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-02-13 15:33:37 -0800 |
| commit | b2b9fe837db7308285909ec1b374546194405d00 (patch) | |
| tree | be9dbc10c7f0699b40ac2ed2534869e6b9e772e7 /test/forward.test.ts | |
| parent | 542cf1d084d189bcdf127c9c2359cced5267295e (diff) | |
| download | coinos-server-b2b9fe837db7308285909ec1b374546194405d00.tar.gz coinos-server-b2b9fe837db7308285909ec1b374546194405d00.zip | |
bolt12 autowithdraw
Diffstat (limited to 'test/forward.test.ts')
| -rw-r--r-- | test/forward.test.ts | 150 |
1 files changed, 150 insertions, 0 deletions
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(); + }); +}); |
