diff options
| author | Adam Soltys <[email protected]> | 2026-09-21 19:21:29 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-21 19:21:43 -0700 |
| commit | 671cdb3af944b5e6d39361f4a208547d0e9f7291 (patch) | |
| tree | f79311f96084d6a28708efd1223e85f18ff56926 /src/routes/(app)/ln | |
| parent | dcca73434f93e8cf8a912e55a6e098791277a976 (diff) | |
| download | coinos-ui-671cdb3af944b5e6d39361f4a208547d0e9f7291.tar.gz coinos-ui-671cdb3af944b5e6d39361f4a208547d0e9f7291.zip | |
send: quote the lightning routing fee up front and add Max to lightning sends
The lightning send page now asks the server what the payment will cost
before showing the Send button: the exact routing fee from the route
query, the platform fee, and the total. The quoted fee is submitted as
the maxfee, so what the user sees is what gets debited; advanced
settings still let them override it, and if the quote fails the page
falls back to the old 2% reserve.
Max on the amount step asks the server for the largest amount whose
amount + routing fee + platform fee lands exactly on the balance, and
for an offer picks up the invoice fetched for that exact amount.
LNURL-pay gets a Max button too. The invoice only exists once we name an
amount, so the action fetches a probe invoice at the ceiling, lets the
server solve for the amount, then fetches the real invoice for it.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Diffstat (limited to 'src/routes/(app)/ln')
| -rw-r--r-- | src/routes/(app)/ln/[lnurl]/+page.server.ts | 55 |
1 files changed, 54 insertions, 1 deletions
diff --git a/src/routes/(app)/ln/[lnurl]/+page.server.ts b/src/routes/(app)/ln/[lnurl]/+page.server.ts index f2628ce1..04cff338 100644 --- a/src/routes/(app)/ln/[lnurl]/+page.server.ts +++ b/src/routes/(app)/ln/[lnurl]/+page.server.ts @@ -8,7 +8,7 @@ const { decode, fromWords } = bech32; const lnurlFetch = async (url: string) => get(`/lnurl/proxy?url=${encodeURIComponent(url)}`); -export async function load({ params, parent }) { +export async function load({ cookies, params, parent }) { const { user } = await parent(); const rates = await getRates(); @@ -58,6 +58,12 @@ export async function load({ params, parent }) { error(500, "We only support LNURLp and LNURLw at this time"); data.rate = rates[user?.currency || "USD"]; + // Lightning sends draw on the main account; the Max button needs its balance + if (user && data.tag === "payRequest") { + try { + ({ balance: data.balance } = await get(`/account/${user.id}`, auth(cookies))); + } catch (e) {} + } return data; } @@ -88,6 +94,53 @@ export const actions = { redirect(307, path); }, + // Send everything to an LNURL-pay endpoint. The routing cost depends on the + // amount and the amount on the routing cost, and the invoice only exists + // once we name an amount — so fetch a probe invoice at the most we could + // possibly send (its destination and route hints are what the quote needs), + // let the server solve for the amount that lands the balance on zero, then + // fetch the real invoice for exactly that amount. + max: async ({ cookies, request }) => { + let { callback, minSendable, maxSendable, comment } = await fd(request); + minSendable = Math.round(minSendable / 1000); + maxSendable = Math.round(maxSendable / 1000); + + const user = await get("/me", auth(cookies)); + const { balance } = await get(`/account/${user.id}`, auth(cookies)); + const ceiling = Math.min(balance, maxSendable); + if (ceiling < minSendable) + return fail(400, { error: `Amount must be at least ${minSendable} sats` }); + + const invoiceFor = async (amount: number) => { + const urlObj = new URL(callback); + urlObj.searchParams.set("amount", (amount * 1000).toString()); + if (comment) urlObj.searchParams.set("comment", comment); + const { pr, reason } = await lnurlFetch(urlObj.toString()); + if (!pr) throw new Error(reason || "Could not fetch invoice"); + return pr; + }; + + let pr; + try { + const probe = await invoiceFor(ceiling); + const { amount } = await post( + "/lightning/quote", + { payreq: probe, max: true, ceiling }, + auth(cookies), + ); + if (amount < minSendable) + return fail(400, { error: `Amount must be at least ${minSendable} sats` }); + pr = await invoiceFor(amount); + } catch (e) { + const { error } = e as Error & { error?: string }; + return fail(400, { error: (e as Error).message || error }); + } + + let path = `/send/lightning/${pr}`; + if (comment) path += `/${encodeURIComponent(comment)}`; + redirect(307, path); + }, + withdraw: async ({ cookies, fetch, request }) => { let error; |
