diff options
Diffstat (limited to 'src/routes/(app)/send')
| -rw-r--r-- | src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.server.ts | 45 | ||||
| -rw-r--r-- | src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.svelte | 76 |
2 files changed, 112 insertions, 9 deletions
diff --git a/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.server.ts b/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.server.ts index 2a30defa..2b637e75 100644 --- a/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.server.ts +++ b/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.server.ts @@ -1,13 +1,35 @@ import getRates from "$lib/rates"; -import { auth, fd, post } from "$lib/utils"; +import { auth, fd, get, post } from "$lib/utils"; import { fail, redirect } from "@sveltejs/kit"; +// Ask the server what this send will actually cost: the routing fee askrene +// quotes for the invoice (the same answer xpay gets when it pays) plus the +// platform fee. The quoted routing fee becomes the payment's maxfee, so what +// the user sees is what gets debited. A failed quote isn't fatal — the page +// falls back to the old manual max-fee reserve. +const quote = async (body, cookies) => { + try { + const q = await post("/lightning/quote", body, auth(cookies)); + return { ...q, quoteError: undefined }; + } catch (e) { + const { message } = e as Error; + return { fee: undefined, ourfee: undefined, quoteError: message }; + } +}; + export async function load({ cookies, params, parent }) { const { user } = await parent(); let data = { ...params }; const rates = await getRates(); data = { ...data, ...(await post("/parse", params, auth(cookies))) }; + // Lightning sends always draw on the main account + const { balance } = await get(`/account/${user.id}`, auth(cookies)); + data.balance = balance; + + if (data.amount) + data = { ...data, ...(await quote({ payreq: params.payreq, amount: data.amount }, cookies)) }; + data.rate = rates[user.currency]; return data; } @@ -27,9 +49,30 @@ export const actions = { data.payreq = invoice; } + data = { ...data, ...(await quote({ payreq: data.payreq || payreq, amount }, cookies)) }; + return data; }, + // Send everything: the server solves for the largest amount whose amount + + // routing fee + platform fee lands exactly on the balance. For an offer it + // also fetches the invoice for that exact amount. + max: async ({ cookies, params }) => { + const { payreq } = params; + try { + const q = await post("/lightning/quote", { payreq, max: true }, auth(cookies)); + let data = { amount: q.amount, fee: q.fee, ourfee: q.ourfee, total: q.total, max: true }; + if (q.payreq) { + data = { ...data, ...(await post("/parse", { payreq: q.payreq }, auth(cookies))) }; + data.payreq = q.payreq; + } + return data; + } catch (e) { + const { message } = e as Error; + return fail(400, { message }); + } + }, + send: async ({ cookies, request }) => { let p; try { diff --git a/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.svelte b/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.svelte index 105f3ace..c84e5d53 100644 --- a/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.svelte +++ b/src/routes/(app)/send/lightning/[payreq]/[...memo]/+page.svelte @@ -11,7 +11,8 @@ let { data, form } = $props(); - let { alias, memo, payreq, ourfee, user } = $derived({ ...data, ...form }); + let { alias, memo, payreq, ourfee, fee, quoteError, balance, user } = + $derived({ ...data, ...form }); let a = $state(); let { currency, locked } = $derived(user); let locale = loc(user); @@ -27,18 +28,36 @@ let next = $state(); let toggle = () => (show = !show); let amount = $derived(form?.amount || data.amount); + + // The max routing fee we hand the server. When the route was quoted this is + // the exact fee askrene found, so the debit matches what's shown; without a + // quote fall back to the old 2% reserve (any unused part is credited back). + let quoted = $derived(typeof fee === "number"); + let fallback = $derived(Math.max(5, Math.round(amount * 0.02) || 0)); let maxfee = $state( - Math.max(5, Math.round(untrack(() => amount) * 0.02 || 0)), + untrack(() => (typeof fee === "number" ? fee : Math.max(5, Math.round(amount * 0.02) || 0))), + ); + $effect(() => (maxfee = quoted ? fee : fallback)); + + let total = $derived((amount || 0) + (maxfee || 0) + (ourfee || 0)); + + // Server-side messages worth showing verbatim: a balance shortfall, or the + // pre-flight check that the max fee is below what routing now costs. + let verbatim = $derived( + form?.message && + (form.message.includes($t("payments.insufficientFunds")) || + form.message.includes("Routing fee")), ); - $effect(() => (maxfee = Math.max(5, Math.round(amount * 0.02) || 0))); </script> <div class="container px-4 max-w-xl mx-auto text-center space-y-2"> {#if form?.message} <div class="text-red-600 text-center"> - {#if form.message.includes($t("payments.insufficientFunds"))} + {#if verbatim} <div>{form.message}</div> - <div>{$t("payments.lockedBalance")}: {locked}</div> + {#if form.message.includes($t("payments.insufficientFunds"))} + <div>{$t("payments.lockedBalance")}: {locked}</div> + {/if} {:else} {$t("payments.failedToRoute")} {/if} @@ -58,6 +77,23 @@ <span class="break-words font-semibold">{alias}</span> </div> + {#if quoted} + <div class="text-center"> + <h2 class="text-secondary text-lg">{$t("payments.routingFee")}</h2> + + <div class="flex flex-wrap gap-4 justify-center"> + <div class="my-auto"> + <h2 class="text-xl"> + {f(toFiat(fee, $rate), currency)} + </h2> + <h3 class="text-secondary">⚡️{s(fee)}</h3> + </div> + </div> + </div> + {:else if quoteError} + <div class="text-secondary">{$t("payments.noQuote")}</div> + {/if} + {#if ourfee} <div class="text-center"> <h2 class="text-secondary text-lg">{$t("payments.platformFee")}</h2> @@ -73,6 +109,14 @@ </div> {/if} + {#if quoted && (fee || ourfee)} + <div class="text-center"> + <h2 class="text-secondary text-lg">{$t("payments.total")}</h2> + <h2 class="text-xl">{f(toFiat(total, $rate), currency)}</h2> + <h3 class="text-secondary">⚡️{s(total)}</h3> + </div> + {/if} + <form method="POST" use:enhance @@ -141,9 +185,25 @@ bind:rate={$rate} submit={next} /> - <button type="submit" class="btn" bind:this={next} - >{$t("payments.next")}</button - > + + <div class="flex justify-center gap-2"> + <button + type="submit" + class="btn !w-auto grow" + formaction="?/max" + disabled={loading} + onclick={submit} + > + {#if loading} + <Spinner /> + {:else} + Max ⚡️{s(balance)} + {/if} + </button> + <button type="submit" class="btn btn-accent !w-auto grow" bind:this={next} + >{$t("payments.next")}</button + > + </div> </form> {/if} </div> |
