DepositBitcoin.tsx raw
1 import {
2 CopyIcon,
3 CreditCardIcon,
4 ExternalLinkIcon,
5 RefreshCwIcon,
6 } from "lucide-react";
7 import { useEffect, useRef, useState } from "react";
8 import AppHeader from "src/components/AppHeader";
9 import { FixedFloatButton } from "src/components/FixedFloatButton";
10 import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
11 import FormattedFiatAmount from "src/components/FormattedFiatAmount";
12 import Loading from "src/components/Loading";
13 import LottieLoading from "src/components/LottieLoading";
14 import LottieSuccess from "src/components/LottieSuccess";
15 import { MempoolAlert } from "src/components/MempoolAlert";
16 import OnchainAddressDisplay from "src/components/OnchainAddressDisplay";
17 import QRCode from "src/components/QRCode";
18 import ResponsiveLinkButton from "src/components/ResponsiveLinkButton";
19 import { Button } from "src/components/ui/button";
20 import {
21 Card,
22 CardContent,
23 CardHeader,
24 CardTitle,
25 } from "src/components/ui/card";
26 import { ExternalLinkButton } from "src/components/ui/custom/external-link-button";
27 import { LinkButton } from "src/components/ui/custom/link-button";
28 import { LoadingButton } from "src/components/ui/custom/loading-button";
29 import { Separator } from "src/components/ui/separator";
30 import { useInfo } from "src/hooks/useInfo";
31 import { useMempoolApi } from "src/hooks/useMempoolApi";
32 import { useOnchainAddress } from "src/hooks/useOnchainAddress";
33 import { useSyncWallet } from "src/hooks/useSyncWallet";
34 import { copyToClipboard } from "src/lib/clipboard";
35 import { MempoolUtxo } from "src/types";
36
37 export default function DepositBitcoin() {
38 useSyncWallet();
39 const {
40 data: onchainAddress,
41 getNewAddress,
42 loadingAddress,
43 } = useOnchainAddress();
44 const { data: mempoolAddressUtxos } = useMempoolApi<MempoolUtxo[]>(
45 onchainAddress ? `/address/${onchainAddress}/utxo` : undefined,
46 3000
47 );
48 const [txId, setTxId] = useState("");
49 const [confirmedAmountSat, setConfirmedAmountSat] = useState<number | null>(
50 null
51 );
52 const [pendingAmountSat, setPendingAmountSat] = useState<number | null>(null);
53 const startTimeRef = useRef(0);
54
55 useEffect(() => {
56 if (startTimeRef.current === 0) {
57 startTimeRef.current = Math.floor(Date.now() / 1000);
58 }
59 }, []);
60
61 useEffect(() => {
62 if (
63 !mempoolAddressUtxos ||
64 mempoolAddressUtxos.length === 0 ||
65 startTimeRef.current === 0
66 ) {
67 return;
68 }
69
70 if (txId) {
71 const utxo = mempoolAddressUtxos.find((utxo) => utxo.txid === txId);
72 if (utxo?.status.confirmed) {
73 setConfirmedAmountSat(utxo.value);
74 setPendingAmountSat(null);
75 }
76 } else {
77 const unconfirmed = mempoolAddressUtxos.find(
78 (utxo) => !utxo.status.confirmed
79 );
80 if (unconfirmed) {
81 setTxId(unconfirmed.txid);
82 setPendingAmountSat(unconfirmed.value);
83 return;
84 }
85
86 const confirmed = mempoolAddressUtxos.find(
87 (utxo) =>
88 utxo.status.confirmed &&
89 !!utxo.status.block_time &&
90 utxo.status.block_time >= startTimeRef.current
91 );
92 if (confirmed) {
93 setTxId(confirmed.txid);
94 setConfirmedAmountSat(confirmed.value);
95 setPendingAmountSat(null);
96 }
97 }
98 }, [mempoolAddressUtxos, txId]);
99
100 if (!onchainAddress) {
101 return <Loading />;
102 }
103
104 return (
105 <div className="grid gap-5">
106 <AppHeader
107 pageTitle="Deposit Bitcoin to On-Chain Balance"
108 title="Deposit Bitcoin to On-Chain Balance"
109 description="Deposit bitcoin to your on-chain address which then can be used to open new lightning channels."
110 contentRight={
111 <ResponsiveLinkButton
112 icon={CreditCardIcon}
113 text="Buy Bitcoin"
114 to="/channels/onchain/buy-bitcoin"
115 />
116 }
117 />
118 <MempoolAlert />
119 <div className="w-80">
120 {confirmedAmountSat ? (
121 <DepositSuccess amountSat={confirmedAmountSat} txId={txId} />
122 ) : txId ? (
123 <DepositPending amountSat={pendingAmountSat} txId={txId} />
124 ) : (
125 <Card>
126 <CardContent className="grid gap-6 justify-center">
127 <a
128 href={`bitcoin:${onchainAddress}`}
129 target="_blank"
130 className="flex justify-center"
131 >
132 <QRCode value={onchainAddress} paymentType="onchain" />
133 </a>
134
135 <div className="flex flex-wrap gap-2 items-center justify-center">
136 <OnchainAddressDisplay address={onchainAddress} />
137 </div>
138
139 <div className="flex flex-col">
140 <div className="flex w-full flex-col gap-3">
141 <LoadingButton
142 variant="outline"
143 onClick={getNewAddress}
144 className="w-full"
145 loading={loadingAddress}
146 >
147 {!loadingAddress && <RefreshCwIcon />}
148 Change
149 </LoadingButton>
150 <Button
151 variant="secondary"
152 className="w-full"
153 onClick={() => {
154 copyToClipboard(onchainAddress);
155 }}
156 >
157 <CopyIcon />
158 Copy
159 </Button>
160 </div>
161 <Separator className="my-4" />
162 <FixedFloatButton
163 to="BTC"
164 address={onchainAddress}
165 className="w-full"
166 variant="outline"
167 >
168 <ExternalLinkIcon className="size-4" />
169 Top Up with Crypto
170 </FixedFloatButton>
171 </div>
172 </CardContent>
173 </Card>
174 )}
175 </div>
176 </div>
177 );
178 }
179
180 function DepositPending({
181 amountSat,
182 txId,
183 }: {
184 amountSat: number | null;
185 txId: string;
186 }) {
187 const { data: info } = useInfo();
188
189 return (
190 <Card className="w-full">
191 <CardHeader>
192 <CardTitle className="text-center">Awaiting Confirmation</CardTitle>
193 </CardHeader>
194 <CardContent className="flex flex-col items-center gap-4">
195 <LottieLoading size={288} />
196 {amountSat && (
197 <div className="flex flex-col gap-2 items-center">
198 <p className="text-xl font-semibold slashed-zero">
199 <FormattedBitcoinAmount amountMsat={amountSat * 1000} />
200 </p>
201 <FormattedFiatAmount amountSat={amountSat} />
202 </div>
203 )}
204 <div className="w-full">
205 <ExternalLinkButton
206 to={`${info?.mempoolUrl}/tx/${txId}`}
207 variant="outline"
208 className="mt-2 w-full"
209 >
210 View on Mempool
211 <ExternalLinkIcon className="size-4" />
212 </ExternalLinkButton>
213 </div>
214 </CardContent>
215 </Card>
216 );
217 }
218
219 function DepositSuccess({
220 amountSat,
221 txId,
222 }: {
223 amountSat: number;
224 txId: string;
225 }) {
226 const { data: info } = useInfo();
227
228 return (
229 <>
230 <Card className="w-full">
231 <CardHeader>
232 <CardTitle className="text-center">Payment Received!</CardTitle>
233 </CardHeader>
234 <CardContent className="flex flex-col items-center gap-4">
235 <LottieSuccess />
236 <div className="flex flex-col gap-2 items-center">
237 <p className="text-xl font-semibold slashed-zero">
238 <FormattedBitcoinAmount amountMsat={amountSat * 1000} />
239 </p>
240 <FormattedFiatAmount amountSat={amountSat} />
241 </div>
242 <div className="w-full">
243 <ExternalLinkButton
244 to={`${info?.mempoolUrl}/tx/${txId}`}
245 variant="outline"
246 className="mt-2 w-full"
247 >
248 View on Mempool
249 <ExternalLinkIcon className="size-4" />
250 </ExternalLinkButton>
251 </div>
252 </CardContent>
253 </Card>
254 <LinkButton to="/channels" className="mt-4 w-full">
255 Back To Node
256 </LinkButton>
257 </>
258 );
259 }
260