Skip to content
2 changes: 1 addition & 1 deletion frontend/src/components/CloseChannelDialogContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export function CloseChannelDialogContent({ alias, channel }: Props) {
Are you sure you want to close the channel with {alias}?
</AlertDialogTitle>
<AlertDialogDescription>
This channel is inactive. Some channels require up to 6 onchain
This channel is inactive. Some channels require up to 6 on-chain
confirmations before they are usable.
</AlertDialogDescription>
</AlertDialogHeader>
Expand Down
131 changes: 131 additions & 0 deletions frontend/src/components/FeeRateField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { AlertTriangleIcon, ExternalLinkIcon, PencilIcon } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import ExternalLink from "src/components/ExternalLink";
import Loading from "src/components/Loading";
import { Button } from "src/components/ui/button";
import { Input } from "src/components/ui/input";
import { Label } from "src/components/ui/label";
import { useInfo } from "src/hooks/useInfo";
import { useMempoolApi } from "src/hooks/useMempoolApi";

type RecommendedFees = {
fastestFee: number;
halfHourFee: number;
economyFee: number;
minimumFee: number;
};

type FeeRateFieldProps = {
feeRate: string;
onFeeRateChange: (value: string) => void;
};

export function FeeRateField({ feeRate, onFeeRateChange }: FeeRateFieldProps) {
const { data: info } = useInfo();
const { data: recommendedFees, error: mempoolError } =
useMempoolApi<RecommendedFees>("/v1/fees/recommended");
const [isEditing, setIsEditing] = useState(false);
const hasInitializedDefaultFee = useRef(false);

useEffect(() => {
if (
recommendedFees?.fastestFee &&
!hasInitializedDefaultFee.current &&
!feeRate
) {
hasInitializedDefaultFee.current = true;
onFeeRateChange(recommendedFees.fastestFee.toString());
}
}, [feeRate, onFeeRateChange, recommendedFees]);

useEffect(() => {
if (mempoolError) {
setIsEditing(true);
}
}, [mempoolError]);

if (!info || (!recommendedFees && !mempoolError)) {
return (
<div className="flex items-center justify-between">
<Label>On-chain Fee Rate (sat/vB)</Label>
<Loading className="w-4 h-4" />
</div>
);
}

if (!isEditing) {
return (
<div className="flex items-center justify-between">
<Label>On-chain Fee Rate (sat/vB)</Label>
{feeRate ? (
<button
type="button"
className="flex items-center gap-2 cursor-pointer"
onClick={() => setIsEditing(true)}
>
<p className="text-sm">{feeRate} sat/vB</p>
<PencilIcon className="w-4 h-4" />
</button>
) : (
<Loading className="w-4 h-4" />
)}
</div>
);
}

return (
<div className="grid gap-2">
<Label htmlFor="fee-rate">On-chain Fee Rate (sat/vB)</Label>
{mempoolError && (
<div className="text-muted-foreground text-xs flex gap-1 items-center">
<AlertTriangleIcon className="h-3 w-3" />
Failed to fetch fee estimates. Try refreshing the page.
</div>
)}
<Input
id="fee-rate"
type="number"
value={feeRate}
step={1}
required
min={recommendedFees?.minimumFee || 1}
onChange={(e) => {
onFeeRateChange(e.target.value);
}}
/>
{recommendedFees && (
<div className="flex items-center mt-2 gap-4">
<Button
variant="positive"
className="rounded-full"
type="button"
onClick={() =>
onFeeRateChange(recommendedFees.economyFee.toString())
}
>
Low priority: {recommendedFees.economyFee}
</Button>
<Button
variant="positive"
className="rounded-full"
type="button"
onClick={() =>
onFeeRateChange(recommendedFees.fastestFee.toString())
}
>
High priority: {recommendedFees.fastestFee}
</Button>
{info.mempoolUrl && (
<ExternalLink
to={info.mempoolUrl}
className="text-muted-foreground text-sm underline flex items-center gap-2"
>
View on Mempool
<ExternalLinkIcon className="w-4 h-4" />
</ExternalLink>
)}
</div>
)}
</div>
);
}
2 changes: 1 addition & 1 deletion frontend/src/screens/channels/CurrentChannelOrder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ function PayBitcoinChannelOrderTopup({ order }: { order: NewChannelOrder }) {
</p>
<p className="text-xs text-muted-foreground">
This amount includes cost for the channel opening and potential
channel onchain reserves.
channel on-chain reserves.
</p>
<div className="flex flex-row gap-2 items-center">
<Input
Expand Down
128 changes: 16 additions & 112 deletions frontend/src/screens/wallet/WithdrawOnchainFunds.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import {
AlertTriangleIcon,
ChevronDownIcon,
CopyIcon,
ExternalLinkIcon,
InfoIcon,
} from "lucide-react";
import { AlertTriangleIcon, CopyIcon, ExternalLinkIcon } from "lucide-react";
import React from "react";
import { toast } from "sonner";
import { AnchorReserveAlert } from "src/components/AnchorReserveAlert";
import AppHeader from "src/components/AppHeader";
import ExternalLink from "src/components/ExternalLink";
import { FeeRateField } from "src/components/FeeRateField";
import { FixedFloatButton } from "src/components/FixedFloatButton";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import Loading from "src/components/Loading";
Expand All @@ -33,7 +28,6 @@ import { Separator } from "src/components/ui/separator";
import { ONCHAIN_DUST_SATS } from "src/constants";
import { useBalances } from "src/hooks/useBalances";
import { useInfo } from "src/hooks/useInfo";
import { useMempoolApi } from "src/hooks/useMempoolApi";

import { copyToClipboard } from "src/lib/clipboard";
import {
Expand All @@ -45,33 +39,14 @@ import { request } from "src/utils/request";
export default function WithdrawOnchainFunds() {
const { data: info } = useInfo();
const { data: balances } = useBalances();
const { data: recommendedFees, error: mempoolError } = useMempoolApi<{
fastestFee: number;
halfHourFee: number;
economyFee: number;
minimumFee: number;
}>("/v1/fees/recommended");
const [isLoading, setLoading] = React.useState(false);
const [onchainAddress, setOnchainAddress] = React.useState("");
const [amountSat, setAmountSat] = React.useState("");
const [feeRate, setFeeRate] = React.useState("");
const [sendAll, setSendAll] = React.useState(false);
const [showAdvanced, setShowAdvanced] = React.useState(false);
const [transactionId, setTransactionId] = React.useState("");
const [confirmDialogOpen, setConfirmDialogOpen] = React.useState(false);

React.useEffect(() => {
if (mempoolError) {
setShowAdvanced(true);
}
}, [mempoolError]);

React.useEffect(() => {
if (recommendedFees?.fastestFee) {
setFeeRate(recommendedFees.fastestFee.toString());
}
}, [recommendedFees]);

const copy = (text: string) => {
copyToClipboard(text);
};
Expand All @@ -80,7 +55,7 @@ export default function WithdrawOnchainFunds() {
setLoading(true);
try {
if (!onchainAddress) {
throw new Error("No onchain address");
throw new Error("No on-chain address");
}
if (!feeRate) {
throw new Error("No fee rate set");
Expand Down Expand Up @@ -111,14 +86,14 @@ export default function WithdrawOnchainFunds() {
body: JSON.stringify(payload),
}
);
console.info("Redeemed onchain funds", response);
console.info("Redeemed on-chain funds", response);
if (!response?.txId) {
throw new Error("No address in response");
}
setTransactionId(response.txId);
} catch (error) {
console.error(error);
toast.error("Failed to redeem onchain funds", {
toast.error("Failed to redeem on-chain funds", {
description: "" + error,
});
}
Expand Down Expand Up @@ -157,14 +132,14 @@ export default function WithdrawOnchainFunds() {
);
}

if (!info || !balances || (!recommendedFees && !mempoolError)) {
if (!info || !balances) {
return <Loading />;
}

if (balances.onchain.spendableSat <= ONCHAIN_DUST_SATS) {
return (
<p>
You currently don't have enough sats to pay for an onchain transaction.
You currently don't have enough sats to pay for an on-chain transaction.
</p>
);
}
Expand All @@ -174,12 +149,12 @@ export default function WithdrawOnchainFunds() {
<AppHeader
pageTitle="Withdraw On-Chain Balance"
title="Withdraw On-Chain Balance"
description="Withdraw your onchain funds to another bitcoin wallet"
description="Withdraw your on-chain funds to another bitcoin wallet"
/>

<div className="max-w-lg">
<p>
Your on-chain balance will be withdrawn to the onchain bitcoin wallet
Your on-chain balance will be withdrawn to the on-chain bitcoin wallet
address you specify below.
</p>
<form
Expand All @@ -194,7 +169,7 @@ export default function WithdrawOnchainFunds() {
<Label htmlFor="amount">Amount</Label>
<div className="flex justify-between items-center">
<p className="text-sm text-muted-foreground sensitive slashed-zero">
Current onchain balance:{" "}
Current on-chain balance:{" "}
<FormattedBitcoinAmount
amountMsat={balances.onchain.spendableSat * 1000}
/>
Expand Down Expand Up @@ -227,7 +202,7 @@ export default function WithdrawOnchainFunds() {
<AlertTriangleIcon />
<AlertTitle>Entire wallet balance will be sent</AlertTitle>
<AlertDescription>
Your entire wallet balance will be sent minus onchain
Your entire wallet balance will be sent minus on-chain
transaction fees. The exact amount cannot be determined until
the payment is made.
</AlertDescription>
Expand All @@ -239,7 +214,7 @@ export default function WithdrawOnchainFunds() {
/>
</div>
<div className="grid gap-2">
<Label htmlFor="onchain-address">Onchain Address</Label>
<Label htmlFor="onchain-address">On-chain Address</Label>
<Input
id="onchain-address"
type="text"
Expand All @@ -255,72 +230,9 @@ export default function WithdrawOnchainFunds() {
</p>
</div>
{(info?.backendType === "LDK" || info?.backendType === "LND") && (
<>
{showAdvanced && (
<div className="grid gap-2">
<Label htmlFor="fee-rate">Fee Rate (Sat/vB)</Label>
{mempoolError && (
<div className="text-muted-foreground text-xs flex gap-1 items-center">
<AlertTriangleIcon className="h-3 w-3" />
Failed to fetch fee estimates. Try refreshing the page.
</div>
)}
<Input
id="fee-rate"
type="number"
value={feeRate}
step={1}
required
min={recommendedFees?.minimumFee || 1}
onChange={(e) => {
setFeeRate(e.target.value);
}}
/>
{recommendedFees && (
<div className="flex items-center mt-2 gap-4">
<Button
variant="positive"
className="rounded-full"
type="button"
onClick={() =>
setFeeRate(recommendedFees.economyFee.toString())
}
>
Low priority: {recommendedFees.economyFee}
</Button>{" "}
<Button
variant="positive"
className="rounded-full"
type="button"
onClick={() =>
setFeeRate(recommendedFees.fastestFee.toString())
}
>
High priority: {recommendedFees.fastestFee}
</Button>{" "}
<ExternalLink
to={info?.mempoolUrl}
className="text-sm text-muted-foreground underline flex items-center gap-2"
>
View on Mempool
<ExternalLinkIcon className="w-4 h-4" />
</ExternalLink>
</div>
)}
</div>
)}
{!showAdvanced && (
<Button
type="button"
variant="link"
className="text-muted-foreground text-xs"
onClick={() => setShowAdvanced((current) => !current)}
>
<ChevronDownIcon />
Advanced Options
</Button>
)}
</>
<div className="border-t pt-4">
<FeeRateField feeRate={feeRate} onFeeRateChange={setFeeRate} />
</div>
)}

<div>
Expand All @@ -329,18 +241,10 @@ export default function WithdrawOnchainFunds() {
open={confirmDialogOpen}
>
<Button className="w-full">Withdraw</Button>
{feeRate && (
<div className="mt-2 text-muted-foreground text-sm flex gap-1 items-center justify-center">
<InfoIcon className="h-4 w-4" />
On-chain payment will be made with{" "}
<span className="font-semibold">{feeRate} sat/vB</span> fee
</div>
)}

<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Confirm Onchain Transaction
Confirm On-chain Transaction
</AlertDialogTitle>
<AlertDialogDescription>
<div>
Expand Down
Loading
Loading