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