From 97fffca3e63e2edb4818305abdbcbb8953344aa3 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Mon, 2 Mar 2026 22:50:42 -0800 Subject: fee bumping ui --- src/routes/(app)/payment/[id]/+page.svelte | 113 ++++++++++++++++++++- .../(app)/payment/[id]/bump-estimate/+server.ts | 12 +++ src/routes/(app)/payment/[id]/bump/+server.ts | 2 +- .../[amount]/[...feeRate]/+page.server.ts | 3 +- .../[address]/[amount]/[...feeRate]/+page.svelte | 83 ++++++++++++--- 5 files changed, 194 insertions(+), 19 deletions(-) create mode 100644 src/routes/(app)/payment/[id]/bump-estimate/+server.ts (limited to 'src') diff --git a/src/routes/(app)/payment/[id]/+page.svelte b/src/routes/(app)/payment/[id]/+page.svelte index aad0cf7f..55daca8d 100644 --- a/src/routes/(app)/payment/[id]/+page.svelte +++ b/src/routes/(app)/payment/[id]/+page.svelte @@ -58,12 +58,53 @@ success("Printing!"); }; + let showBump = $state(false); + let bumpLoading = $state(false); + let bumpEstimate: any = $state(null); + let bumpTargetRate = $state(0); + let bumpError = $state(""); + + let canBump = $derived( + type === "bitcoin" && !confirmed && amount < 0 && p.bumpReserve > 0 && !p.childTxid, + ); + + let fetchEstimate = async (targetRate: number) => { + bumpTargetRate = targetRate; + bumpError = ""; + try { + bumpEstimate = await post(`/payment/${id}/bump-estimate`, { id, targetFeeRate: targetRate }); + } catch (e: any) { + bumpError = e.message; + } + }; + + let openBump = async () => { + showBump = true; + bumpEstimate = null; + bumpError = ""; + try { + const est = await post(`/payment/${id}/bump-estimate`, { id, targetFeeRate: 10 }); + bumpEstimate = est; + bumpTargetRate = est.fees?.fastestFee || 10; + await fetchEstimate(bumpTargetRate); + } catch (e: any) { + bumpError = e.message; + } + }; + let bump = async () => { + bumpLoading = true; + bumpError = ""; try { - await post(`/payment/${id}/bump`, { id }); + const res = await post(`/payment/${id}/bump`, { id, targetFeeRate: bumpTargetRate }); + p.childTxid = res.txid; + p.bumpedFee = res.childFee; + showBump = false; + success("Transaction bumped!"); } catch (e: any) { - fail(e.message); + bumpError = e.message; } + bumpLoading = false; }; let direction = $state(""); @@ -225,6 +266,74 @@ {/if} + {#if !confirmed && p.bumpReserve > 0} + {@render field("Bump Reserve (refundable)", p.bumpReserve)} + {/if} + + {#if p.childTxid} + {@render copyable("Child Txid", p.childTxid, `${expl}/tx/${p.childTxid}`)} + {/if} + + {#if canBump} +
+ {#if !showBump} + + {:else} +
+

Speed up transaction

+ + {#if bumpEstimate?.fees} +
+ + +
+ + {#if bumpEstimate.cost !== undefined} +
+ Cost: + {bumpEstimate.cost} sats from your {p.bumpReserve} sat reserve +
+ {/if} + {/if} + + {#if bumpError} +
{bumpError}
+ {/if} + +
+ + +
+
+ {/if} +
+ {/if} + {#if type === types.lightning || type === types.bolt12} {@render copyable($t("payments.invoice"), hash)} {@render copyable($t("payments.preimage"), ref)} diff --git a/src/routes/(app)/payment/[id]/bump-estimate/+server.ts b/src/routes/(app)/payment/[id]/bump-estimate/+server.ts new file mode 100644 index 00000000..dc5a8642 --- /dev/null +++ b/src/routes/(app)/payment/[id]/bump-estimate/+server.ts @@ -0,0 +1,12 @@ +import { auth, post } from "$lib/utils"; +import { json, error } from "@sveltejs/kit"; + +export async function POST({ cookies, request }) { + const body = await request.json(); + try { + const res = await post("/bump/estimate", body, auth(cookies)); + return json(res); + } catch (e: any) { + error(400, e.message); + } +} diff --git a/src/routes/(app)/payment/[id]/bump/+server.ts b/src/routes/(app)/payment/[id]/bump/+server.ts index b24980a0..275df1be 100644 --- a/src/routes/(app)/payment/[id]/bump/+server.ts +++ b/src/routes/(app)/payment/[id]/bump/+server.ts @@ -4,7 +4,7 @@ import { json, error } from "@sveltejs/kit"; export async function POST({ cookies, request }) { const body = await request.json(); try { - const res = await post("/replace", body, auth(cookies)); + const res = await post("/bump", body, auth(cookies)); return json(res); } catch (e: any) { error(400, e.message); diff --git a/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.server.ts b/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.server.ts index 717b5a4c..54891aad 100644 --- a/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.server.ts +++ b/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.server.ts @@ -31,7 +31,7 @@ export async function load({ params: { address, amount, feeRate }, cookies, pare } try { - const { fee, fees, inputs, ourfee, hex, subtract } = await post( + const { fee, fees, inputs, ourfee, hex, subtract, bumpReserve } = await post( "/bitcoin/fee", { address, amount, feeRate, aid }, auth(cookies), @@ -50,6 +50,7 @@ export async function load({ params: { address, amount, feeRate }, cookies, pare hex, inputs, forward, + bumpReserve, }; } catch (e) { const { message } = e as Error; diff --git a/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte b/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte index 246cc824..fcae8598 100644 --- a/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte +++ b/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte @@ -129,7 +129,7 @@ let toggle = () => (submitting = !submitting); - let { account, amount, address, message, fee, fees, subtract, ourfee, hex, inputs, forward } = + let { account, amount, address, message, fee, fees, subtract, ourfee, hex, inputs, forward, bumpReserve } = $derived(data); let feeRate: any = $state((() => data.feeRate)()); @@ -152,16 +152,34 @@ submit: any = $state(), showSettings = $state(); - let feeNames = { - fastestFee: $t("payments.fastest"), - halfHourFee: $t("payments.fast"), - hourFee: $t("payments.medium"), - }; + let customRate = $state(""); + let showCustom = $state(false); + + let presets = $derived(fees ? [ + { label: $t("payments.economy"), rate: 0.1 }, + { label: $t("payments.medium"), rate: fees.hourFee }, + { label: $t("payments.fast"), rate: fees.halfHourFee }, + { label: $t("payments.fastest"), rate: fees.fastestFee }, + ] : []); let toggleSettings = () => (showSettings = !showSettings); let setFee = () => goto(`/send/bitcoin/${address}/${amount}/${feeRate}`); let goBack = () => goto(`/send/bitcoin/${address}`); + + let pickRate = (r: number) => { + feeRate = r; + showCustom = false; + setFee(); + }; + + let applyCustom = () => { + const r = parseFloat(customRate); + if (r >= 0.1) { + feeRate = r; + setFee(); + } + };
@@ -187,18 +205,53 @@

{$t("payments.networkFee")}

-
- - +
+ {#each presets as preset} + + {/each} +
+ + {#if showCustom} +
+ e.key === 'Enter' && applyCustom()} + /> + sat/vB +
+ {/if} + +
+ {#if bumpReserve > 0} +
+

Bump Reserve

+
Refundable on confirmation
+ +
+ {/if} + {#if ourfee}

{$t("payments.platformFee")}

-- cgit v1.2.3