diff options
| author | Adam Soltys <[email protected]> | 2026-03-02 22:50:42 -0800 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-03-02 22:50:42 -0800 |
| commit | 97fffca3e63e2edb4818305abdbcbb8953344aa3 (patch) | |
| tree | 2791ae61b72b995dea8c2c0e3b8ead3cd0965a80 /src | |
| parent | 11d358ad0412b794ecf89b4da9f99ac41ed9c3fe (diff) | |
| download | coinos-ui-97fffca3e63e2edb4818305abdbcbb8953344aa3.tar.gz coinos-ui-97fffca3e63e2edb4818305abdbcbb8953344aa3.zip | |
fee bumping ui
Diffstat (limited to 'src')
5 files changed, 194 insertions, 19 deletions
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 @@ </div> {/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} + <div class="py-4 border-b border-base-300"> + {#if !showBump} + <button type="button" class="btn btn-accent w-full" onclick={openBump}> + Speed up + </button> + {:else} + <div class="space-y-3"> + <h3 class="font-semibold">Speed up transaction</h3> + + {#if bumpEstimate?.fees} + <div class="flex flex-wrap gap-2 justify-center"> + <button + type="button" + class="btn btn-sm" + class:btn-accent={bumpTargetRate === bumpEstimate.fees.halfHourFee} + onclick={() => fetchEstimate(bumpEstimate.fees.halfHourFee)} + > + Fast ({bumpEstimate.fees.halfHourFee}) + </button> + <button + type="button" + class="btn btn-sm" + class:btn-accent={bumpTargetRate === bumpEstimate.fees.fastestFee} + onclick={() => fetchEstimate(bumpEstimate.fees.fastestFee)} + > + Fastest ({bumpEstimate.fees.fastestFee}) + </button> + </div> + + {#if bumpEstimate.cost !== undefined} + <div class="text-center text-sm"> + <span class="text-secondary">Cost:</span> + {bumpEstimate.cost} sats from your {p.bumpReserve} sat reserve + </div> + {/if} + {/if} + + {#if bumpError} + <div class="text-red-600 text-sm text-center">{bumpError}</div> + {/if} + + <div class="flex gap-2 justify-center"> + <button type="button" class="btn btn-sm" onclick={() => (showBump = false)}> + Cancel + </button> + <button + type="button" + class="btn btn-sm btn-accent" + onclick={bump} + disabled={bumpLoading || !bumpEstimate || bumpEstimate.cost > p.bumpReserve} + > + {bumpLoading ? "Bumping..." : "Confirm bump"} + </button> + </div> + </div> + {/if} + </div> + {/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(); + } + }; </script> <div class="container px-4 max-w-xl mx-auto space-y-5 text-center no-transition"> @@ -187,18 +205,53 @@ <div class="text-center"> <h2 class="text-secondary text-lg">{$t("payments.networkFee")}</h2> - <div class="flex flex-wrap gap-4 justify-center"> - <select bind:value={feeRate} onchange={setFee}> - {#each Object.keys(feeNames) as feeName} - <option value={fees[feeName]}> - {feeNames[feeName]} — {fees[feeName]} sats/vb - </option> - {/each} - </select> - <Amount amount={fee} rate={$rate} {currency} {locale} /> + <div class="flex flex-wrap gap-2 justify-center mb-2"> + {#each presets as preset} + <button + type="button" + class="btn btn-sm" + class:btn-accent={feeRate == preset.rate && !showCustom} + onclick={() => pickRate(preset.rate)} + > + {preset.label} ({preset.rate}) + </button> + {/each} + <button + type="button" + class="btn btn-sm" + class:btn-accent={showCustom} + onclick={() => { showCustom = true; customRate = String(feeRate); }} + > + Custom + </button> </div> + + {#if showCustom} + <div class="flex items-center justify-center gap-2 mb-2"> + <input + type="number" + min="0.1" + step="0.1" + class="input input-bordered w-24 text-center" + bind:value={customRate} + onblur={applyCustom} + onkeydown={(e) => e.key === 'Enter' && applyCustom()} + /> + <span class="text-secondary text-sm">sat/vB</span> + </div> + {/if} + + <Amount amount={fee} rate={$rate} {currency} {locale} /> </div> + {#if bumpReserve > 0} + <div class="text-center"> + <h2 class="text-secondary text-lg">Bump Reserve</h2> + <div class="text-sm text-secondary">Refundable on confirmation</div> + <Amount amount={bumpReserve} rate={$rate} {currency} {locale} /> + </div> + {/if} + {#if ourfee} <div class="text-center"> <h2 class="text-secondary text-lg">{$t("payments.platformFee")}</h2> |
