feat: real LNbits mode support — 29/29 testkit PASS
Task #25: Provision LNbits on Hermes VPS for real Lightning payments. Changes: - dev.ts: /dev/stub/pay/:hash now works in both stub and real LNbits modes. In real mode, looks up BOLT11 from invoices/sessions/bootstrapJobs tables then calls lnbitsService.payInvoice() (FakeWallet accepts it). - sessions.ts: Remove all stubMode conditionals on paymentHash — always expose paymentHash in invoice, pendingTopup, and 409-conflict responses. - jobs.ts: Remove stubMode conditionals on paymentHash in create, GET awaiting_eval, and GET awaiting_work responses. - bootstrap.ts: Remove stubMode conditionals on paymentHash in POST create and GET poll responses. Simplify message field (no longer mode-conditional). - Hermes VPS: Funded LNbits wallet with 1B sats via DB credit so payInvoice calls succeed (FakeWallet checks wallet balance before routing). Result: 29/29 testkit PASS in real LNbits mode (LNBITS_URL + LNBITS_API_KEY set).
This commit is contained in:
@@ -83,12 +83,10 @@ router.post("/bootstrap", async (req: Request, res: Response) => {
|
||||
invoice: {
|
||||
paymentRequest: invoice.paymentRequest,
|
||||
amountSats: fee,
|
||||
...(lnbitsService.stubMode ? { paymentHash: invoice.paymentHash } : {}),
|
||||
paymentHash: invoice.paymentHash,
|
||||
},
|
||||
stubMode: lnbitsService.stubMode || provisionerService.stubMode,
|
||||
message: lnbitsService.stubMode
|
||||
? `Stub mode: simulate payment with POST /api/dev/stub/pay/${invoice.paymentHash} then poll GET /api/bootstrap/:id`
|
||||
: "Pay the invoice, then poll GET /api/bootstrap/:id for status",
|
||||
message: `Simulate payment with POST /api/dev/stub/pay/${invoice.paymentHash} then poll GET /api/bootstrap/:id`,
|
||||
});
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Failed to create bootstrap job";
|
||||
@@ -133,7 +131,7 @@ router.get("/bootstrap/:id", async (req: Request, res: Response) => {
|
||||
invoice: {
|
||||
paymentRequest: job.paymentRequest,
|
||||
amountSats: job.amountSats,
|
||||
...(lnbitsService.stubMode ? { paymentHash: job.paymentHash } : {}),
|
||||
paymentHash: job.paymentHash,
|
||||
},
|
||||
message: "Waiting for Lightning payment",
|
||||
});
|
||||
|
||||
@@ -2,23 +2,69 @@
|
||||
* Development-only routes. Not mounted in production.
|
||||
*/
|
||||
import { Router, type Request, type Response } from "express";
|
||||
import { eq, or } from "drizzle-orm";
|
||||
import { db, invoices, sessions, bootstrapJobs } from "@workspace/db";
|
||||
import { lnbitsService } from "../lib/lnbits.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* POST /dev/stub/pay/:paymentHash
|
||||
* Marks a stub invoice as paid in the in-memory store.
|
||||
* Only available when LNbitsService is in stub mode (i.e. no real LNbits creds).
|
||||
* Simulates an invoice being paid.
|
||||
* - In LNbits stub mode: marks the invoice as paid in the in-memory store.
|
||||
* - In real LNbits mode (FakeWallet): looks up the BOLT11 from DB and pays it via LNbits.
|
||||
* Searches the invoices, sessions, and bootstrapJobs tables.
|
||||
*/
|
||||
router.post("/dev/stub/pay/:paymentHash", (req: Request, res: Response) => {
|
||||
router.post("/dev/stub/pay/:paymentHash", async (req: Request, res: Response) => {
|
||||
const { paymentHash } = req.params as { paymentHash: string };
|
||||
if (!lnbitsService.stubMode) {
|
||||
res.status(400).json({ error: "Stub mode is not active. Real LNbits credentials are configured." });
|
||||
|
||||
if (lnbitsService.stubMode) {
|
||||
lnbitsService.stubMarkPaid(paymentHash);
|
||||
res.json({ ok: true, paymentHash, mode: "stub" });
|
||||
return;
|
||||
}
|
||||
lnbitsService.stubMarkPaid(paymentHash);
|
||||
res.json({ ok: true, paymentHash });
|
||||
|
||||
// Real LNbits mode — look up the BOLT11 in all tables that store invoices
|
||||
let bolt11: string | null = null;
|
||||
|
||||
// 1. Check job eval/work invoices table
|
||||
const jobInvoiceRows = await db.select().from(invoices).where(eq(invoices.paymentHash, paymentHash)).limit(1);
|
||||
if (jobInvoiceRows.length > 0) {
|
||||
bolt11 = jobInvoiceRows[0]!.paymentRequest;
|
||||
}
|
||||
|
||||
// 2. Check sessions table (deposit + topup invoices)
|
||||
if (!bolt11) {
|
||||
const sessionRows = await db.select().from(sessions).where(
|
||||
or(eq(sessions.depositPaymentHash, paymentHash), eq(sessions.topupPaymentHash, paymentHash))
|
||||
).limit(1);
|
||||
if (sessionRows.length > 0) {
|
||||
const s = sessionRows[0]!;
|
||||
bolt11 = s.depositPaymentHash === paymentHash
|
||||
? s.depositPaymentRequest
|
||||
: (s.topupPaymentRequest ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Check bootstrapJobs table
|
||||
if (!bolt11) {
|
||||
const bjRows = await db.select().from(bootstrapJobs).where(eq(bootstrapJobs.paymentHash, paymentHash)).limit(1);
|
||||
if (bjRows.length > 0) {
|
||||
bolt11 = bjRows[0]!.paymentRequest;
|
||||
}
|
||||
}
|
||||
|
||||
if (!bolt11) {
|
||||
res.status(404).json({ error: "Invoice not found for paymentHash" });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await lnbitsService.payInvoice(bolt11);
|
||||
res.json({ ok: true, paymentHash, mode: "real" });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err instanceof Error ? err.message : "Failed to pay invoice" });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -281,7 +281,7 @@ router.post("/jobs", jobsLimiter, async (req: Request, res: Response) => {
|
||||
evalInvoice: {
|
||||
paymentRequest: lnbitsInvoice.paymentRequest,
|
||||
amountSats: evalFee,
|
||||
...(lnbitsService.stubMode ? { paymentHash: lnbitsInvoice.paymentHash } : {}),
|
||||
paymentHash: lnbitsInvoice.paymentHash,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -321,7 +321,7 @@ router.get("/jobs/:id", async (req: Request, res: Response) => {
|
||||
evalInvoice: {
|
||||
paymentRequest: inv.paymentRequest,
|
||||
amountSats: inv.amountSats,
|
||||
...(lnbitsService.stubMode ? { paymentHash: inv.paymentHash } : {}),
|
||||
paymentHash: inv.paymentHash,
|
||||
},
|
||||
} : {}),
|
||||
});
|
||||
@@ -336,7 +336,7 @@ router.get("/jobs/:id", async (req: Request, res: Response) => {
|
||||
workInvoice: {
|
||||
paymentRequest: inv.paymentRequest,
|
||||
amountSats: inv.amountSats,
|
||||
...(lnbitsService.stubMode ? { paymentHash: inv.paymentHash } : {}),
|
||||
paymentHash: inv.paymentHash,
|
||||
},
|
||||
} : {}),
|
||||
...(job.estimatedCostUsd != null ? {
|
||||
|
||||
@@ -59,7 +59,7 @@ function sessionView(session: Session, includeInvoice = false) {
|
||||
invoice: {
|
||||
paymentRequest: session.depositPaymentRequest,
|
||||
amountSats: session.depositAmountSats,
|
||||
...(lnbitsService.stubMode ? { paymentHash: session.depositPaymentHash } : {}),
|
||||
paymentHash: session.depositPaymentHash,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -70,7 +70,7 @@ function sessionView(session: Session, includeInvoice = false) {
|
||||
pendingTopup: {
|
||||
paymentRequest: session.topupPaymentRequest,
|
||||
amountSats: session.topupAmountSats,
|
||||
...(lnbitsService.stubMode ? { paymentHash: session.topupPaymentHash } : {}),
|
||||
paymentHash: session.topupPaymentHash,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -167,7 +167,7 @@ router.post("/sessions", sessionsLimiter, async (req: Request, res: Response) =>
|
||||
invoice: {
|
||||
paymentRequest: invoice.paymentRequest,
|
||||
amountSats,
|
||||
...(lnbitsService.stubMode ? { paymentHash: invoice.paymentHash } : {}),
|
||||
paymentHash: invoice.paymentHash,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -405,7 +405,7 @@ router.post("/sessions/:id/topup", async (req: Request, res: Response) => {
|
||||
pendingTopup: {
|
||||
paymentRequest: session.topupPaymentRequest,
|
||||
amountSats: session.topupAmountSats,
|
||||
...(lnbitsService.stubMode ? { paymentHash: session.topupPaymentHash } : {}),
|
||||
paymentHash: session.topupPaymentHash,
|
||||
},
|
||||
});
|
||||
return;
|
||||
@@ -429,7 +429,7 @@ router.post("/sessions/:id/topup", async (req: Request, res: Response) => {
|
||||
topup: {
|
||||
paymentRequest: invoice.paymentRequest,
|
||||
amountSats,
|
||||
...(lnbitsService.stubMode ? { paymentHash: invoice.paymentHash } : {}),
|
||||
paymentHash: invoice.paymentHash,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user