aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-03-02 22:50:42 -0800
committerAdam Soltys <[email protected]>2026-03-02 22:50:42 -0800
commit97fffca3e63e2edb4818305abdbcbb8953344aa3 (patch)
tree2791ae61b72b995dea8c2c0e3b8ead3cd0965a80 /src
parent11d358ad0412b794ecf89b4da9f99ac41ed9c3fe (diff)
downloadcoinos-ui-97fffca3e63e2edb4818305abdbcbb8953344aa3.tar.gz
coinos-ui-97fffca3e63e2edb4818305abdbcbb8953344aa3.zip
fee bumping ui
Diffstat (limited to 'src')
-rw-r--r--src/routes/(app)/payment/[id]/+page.svelte113
-rw-r--r--src/routes/(app)/payment/[id]/bump-estimate/+server.ts12
-rw-r--r--src/routes/(app)/payment/[id]/bump/+server.ts2
-rw-r--r--src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.server.ts3
-rw-r--r--src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte83
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]} &mdash; {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>