From 822b928fe884ff76cc378008b62ae2a5d13e4912 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Fri, 3 May 2024 15:00:30 +0700 Subject: [PATCH] feat: minor UI improvements on channel pages, onchain new channel deposit flow WIP --- api.go | 2 + frontend/src/hooks/useBalances.ts | 14 +- .../src/screens/channels/ChannelOrder.tsx | 166 +++++++++++++++++- frontend/src/screens/channels/Channels.tsx | 66 ++++--- frontend/src/screens/channels/NewChannel.tsx | 122 ++++++++++--- ldk.go | 2 + models/lsp/lsp.go | 8 + 7 files changed, 315 insertions(+), 65 deletions(-) diff --git a/api.go b/api.go index c5d5afb2..f50af66f 100644 --- a/api.go +++ b/api.go @@ -473,6 +473,8 @@ func (api *API) NewInstantChannelInvoice(ctx context.Context, request *models.Ne selectedLsp = lsp.OlympusMutinynetLSPS1LSP() case "ALBY": selectedLsp = lsp.AlbyPlebsLSP() + case "ALBY_MUTINYNET": + selectedLsp = lsp.AlbyMutinynetPlebsLSP() default: return nil, errors.New("unknown LSP") } diff --git a/frontend/src/hooks/useBalances.ts b/frontend/src/hooks/useBalances.ts index 897d2df2..74809356 100644 --- a/frontend/src/hooks/useBalances.ts +++ b/frontend/src/hooks/useBalances.ts @@ -1,8 +1,16 @@ -import useSWR from "swr"; +import useSWR, { SWRConfiguration } from "swr"; import { BalancesResponse } from "src/types"; import { swrFetcher } from "src/utils/swr"; -export function useBalances() { - return useSWR("/api/balances", swrFetcher); +const pollConfiguration: SWRConfiguration = { + refreshInterval: 3000, +}; + +export function useBalances(poll = false) { + return useSWR( + "/api/balances", + swrFetcher, + poll ? pollConfiguration : undefined + ); } diff --git a/frontend/src/screens/channels/ChannelOrder.tsx b/frontend/src/screens/channels/ChannelOrder.tsx index 322f244a..71fdebcb 100644 --- a/frontend/src/screens/channels/ChannelOrder.tsx +++ b/frontend/src/screens/channels/ChannelOrder.tsx @@ -2,6 +2,7 @@ import React from "react"; import { localStorageKeys } from "src/constants"; import { ConnectPeerRequest, + GetOnchainAddressResponse, NewChannelOrder, Node, OpenChannelRequest, @@ -13,11 +14,13 @@ import { useNavigate } from "react-router-dom"; import AppHeader from "src/components/AppHeader"; import Loading from "src/components/Loading"; import TwoColumnLayoutHeader from "src/components/TwoColumnLayoutHeader"; +import { Input } from "src/components/ui/input"; import { Label } from "src/components/ui/label"; import { LoadingButton } from "src/components/ui/loading-button"; import { Separator } from "src/components/ui/separator"; import { Table, TableBody, TableCell, TableRow } from "src/components/ui/table"; import { useToast } from "src/components/ui/use-toast"; +import { useBalances } from "src/hooks/useBalances"; import { useCSRF } from "src/hooks/useCSRF"; import { useChannels } from "src/hooks/useChannels"; import { useInfo } from "src/hooks/useInfo"; @@ -71,7 +74,136 @@ export function ChannelOrderInternal({ // TODO: move these to new files export function PayBitcoinChannelOrder({ order }: { order: NewChannelOrder }) { - // TODO: check if enough balance, otherwise topup! + if (order.paymentMethod !== "onchain") { + throw new Error("incorrect payment method"); + } + const { data: balances } = useBalances(true); + + if (!balances) { + return ; + } + + // TODO: do not hardcode the transaction fee + const ESTIMATED_TRANSACTION_FEE = 10000; + const requiredAmount = +order.amount + ESTIMATED_TRANSACTION_FEE; + if (balances.onchain.spendable >= requiredAmount) { + return ; + } + if (balances.onchain.total >= requiredAmount) { + return ; + } + return ; +} + +export function PayBitcoinChannelOrderWaitingDepositConfirmation() { + const existingAddress = localStorage.getItem(localStorageKeys.onchainAddress); + + return ( + <> +

Funds sent to address {existingAddress}

+

+ + Waiting for one block confirmation. (estimated time: 10 minutes) +

+ + ); +} + +export function PayBitcoinChannelOrderTopup({ + order, +}: { + order: NewChannelOrder; +}) { + if (order.paymentMethod !== "onchain") { + throw new Error("incorrect payment method"); + } + + const { data: csrf } = useCSRF(); + const [onchainAddress, setOnchainAddress] = React.useState(); + const [isLoading, setLoading] = React.useState(false); + + const getNewAddress = React.useCallback(async () => { + if (!csrf) { + return; + } + setLoading(true); + try { + const response = await request( + "/api/wallet/new-address", + { + method: "POST", + headers: { + "X-CSRF-Token": csrf, + "Content-Type": "application/json", + }, + //body: JSON.stringify({}), + } + ); + if (!response?.address) { + throw new Error("No address in response"); + } + localStorage.setItem(localStorageKeys.onchainAddress, response.address); + setOnchainAddress(response.address); + } catch (error) { + alert("Failed to request a new address: " + error); + } finally { + setLoading(false); + } + }, [csrf]); + + React.useEffect(() => { + const existingAddress = localStorage.getItem( + localStorageKeys.onchainAddress + ); + if (existingAddress) { + setOnchainAddress(existingAddress); + return; + } + getNewAddress(); + }, [getNewAddress]); + + function confirmGetNewAddress() { + if (confirm("Do you want a fresh address?")) { + getNewAddress(); + } + } + + if (!onchainAddress) { + return ( +
+ +
+ ); + } + + return ( +
+ +
+ + +
+
+ + Get a new address + +
+
+ ); +} + +export function PayBitcoinChannelOrderWithSpendableFunds({ + order, +}: { + order: NewChannelOrder; +}) { if (order.paymentMethod !== "onchain") { throw new Error("incorrect payment method"); } @@ -79,6 +211,7 @@ export function PayBitcoinChannelOrder({ order }: { order: NewChannelOrder }) { const [nodeDetails, setNodeDetails] = React.useState(); const { data: csrf } = useCSRF(); const navigate = useNavigate(); + const { toast } = useToast(); const { pubkey, host } = order; @@ -172,7 +305,14 @@ export function PayBitcoinChannelOrder({ order }: { order: NewChannelOrder }) { throw new Error("No funding txid in response"); } // TODO: Success screen? - alert(`🎉 Published tx: ${openChannelResponse.fundingTxId}`); + // alert(`🎉 Published tx: ${openChannelResponse.fundingTxId}`); + toast({ + title: + "Published channel opening TX: " + openChannelResponse.fundingTxId, + }); + await refetchInfo(); + // TODO: instead update order + localStorage.removeItem(localStorageKeys.channelOrder); navigate("/channels"); } catch (error) { console.error(error); @@ -184,21 +324,26 @@ export function PayBitcoinChannelOrder({ order }: { order: NewChannelOrder }) { return (
- +
- + ⬤ {nodeDetails?.alias ? ( <> {nodeDetails.alias} - ({nodeDetails.active_channel_count}{" "} - channels) + ({nodeDetails.active_channel_count} channels) ) : ( @@ -212,8 +357,7 @@ export function PayBitcoinChannelOrder({ order }: { order: NewChannelOrder }) {
- {new Intl.NumberFormat().format(+order.amount)}{" "} - sats + {new Intl.NumberFormat().format(+order.amount)} sats
@@ -263,7 +407,8 @@ export function PayLightningChannelOrder({ (async () => { toast({ title: "Channel opened!" }); await refetchInfo(); - navigate("/"); + localStorage.removeItem(localStorageKeys.channelOrder); + navigate("/channels"); })(); } }, [hasOpenedChannel, navigate, refetchInfo, toast]); @@ -360,3 +505,6 @@ export function PayLightningChannelOrder({
); } +function refetchInfo() { + throw new Error("Function not implemented."); +} diff --git a/frontend/src/screens/channels/Channels.tsx b/frontend/src/screens/channels/Channels.tsx index c8392efa..2a7615bb 100644 --- a/frontend/src/screens/channels/Channels.tsx +++ b/frontend/src/screens/channels/Channels.tsx @@ -6,7 +6,7 @@ import { CopyIcon, Hotel, MoreHorizontal, - Trash2 + Trash2, } from "lucide-react"; import React from "react"; import { Link, useNavigate } from "react-router-dom"; @@ -39,7 +39,7 @@ import { TableRow, } from "src/components/ui/table.tsx"; import { toast } from "src/components/ui/use-toast.ts"; -import { ONCHAIN_DUST_SATS } from "src/constants.ts"; +import { ONCHAIN_DUST_SATS, localStorageKeys } from "src/constants.ts"; import { useAlbyBalance } from "src/hooks/useAlbyBalance.ts"; import { useBalances } from "src/hooks/useBalances.ts"; import { useChannels } from "src/hooks/useChannels"; @@ -117,8 +117,9 @@ export default function Channels() { } if ( !confirm( - `Are you sure you want to close the channel with ${nodes.find((node) => node.public_key === nodeId)?.alias || - "Unknown Node" + `Are you sure you want to close the channel with ${ + nodes.find((node) => node.public_key === nodeId)?.alias || + "Unknown Node" }?\n\nNode ID: ${nodeId}\n\nChannel ID: ${channelId}` ) ) { @@ -264,15 +265,21 @@ export default function Channels() { )} + + {localStorage.getItem(localStorageKeys.channelOrder) && ( + + + + )} } >
- {albyBalance?.sats && albyBalance.sats > 0 && + {albyBalance?.sats && ( @@ -281,12 +288,10 @@ export default function Channels() { -
- {albyBalance?.sats} sats -
+
{albyBalance?.sats} sats
- } + )} @@ -361,11 +366,10 @@ export default function Channels() {
- {balances && new Intl.NumberFormat().format( - Math.floor( - balances.lightning.totalReceivable / 1000 - ) - )}{" "} + {balances && + new Intl.NumberFormat().format( + Math.floor(balances.lightning.totalReceivable / 1000) + )}{" "} sats
@@ -399,7 +403,11 @@ export default function Channels() { return ( - {channel.active ? Online : Offline}{" "} + {channel.active ? ( + Online + ) : ( + Offline + )}{" "} {formatAmount(capacity)} sats - {formatAmount(channel.localBalance)} sats - {formatAmount(channel.remoteBalance)} sats + + {formatAmount(channel.localBalance)} sats + + + {formatAmount(channel.remoteBalance)} sats + - - closeChannel( - channel.id, - channel.remotePubkey, - channel.active - )}> + + closeChannel( + channel.id, + channel.remotePubkey, + channel.active + ) + } + > Close Channel diff --git a/frontend/src/screens/channels/NewChannel.tsx b/frontend/src/screens/channels/NewChannel.tsx index 47e31596..a4f038df 100644 --- a/frontend/src/screens/channels/NewChannel.tsx +++ b/frontend/src/screens/channels/NewChannel.tsx @@ -1,4 +1,4 @@ -import { Box, Zap } from "lucide-react"; +import { Box, Info, Zap } from "lucide-react"; import React, { FormEvent } from "react"; import { Link, useNavigate } from "react-router-dom"; import albyImage from "src/assets/images/peers/alby.svg"; @@ -7,12 +7,31 @@ import olympusImage from "src/assets/images/peers/olympus.svg"; import AppHeader from "src/components/AppHeader"; import Loading from "src/components/Loading"; import { Alert, AlertDescription, AlertTitle } from "src/components/ui/alert"; -import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from "src/components/ui/breadcrumb"; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "src/components/ui/breadcrumb"; import { Button } from "src/components/ui/button"; import { Checkbox } from "src/components/ui/checkbox"; import { Input } from "src/components/ui/input"; import { Label } from "src/components/ui/label"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "src/components/ui/select"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "src/components/ui/select"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "src/components/ui/tooltip"; import { ALBY_MIN_BALANCE, ALBY_SERVICE_FEE, @@ -30,16 +49,16 @@ type RecommendedPeer = { name: string; minimumChannelSize: number; } & ( - | { + | { paymentMethod: "onchain"; pubkey: string; host: string; } - | { + | { paymentMethod: "lightning"; lsp: string; } - ); +); const recommendedPeers: RecommendedPeer[] = [ { @@ -88,7 +107,14 @@ const recommendedPeers: RecommendedPeer[] = [ name: "Olympus Mutinynet (Flow 2.0)", image: olympusImage, }, - + { + paymentMethod: "lightning", + network: "signet", + lsp: "ALBY_MUTINYNET", + minimumChannelSize: 150_000, + name: "Alby Mutinynet", + image: albyImage, + }, { network: "signet", paymentMethod: "onchain", @@ -198,7 +224,10 @@ function NewChannelInternal({ network }: { network: Network }) { title="Open a channel" description="Funds used to open a channel minus fees will be added to your spending balance" /> -
+ {/* TODO: move to somewhere else? */} {info?.backendType === "LDK" && albyBalance && @@ -231,13 +260,18 @@ function NewChannelInternal({ network }: { network: Network }) {
- setPaymentMethod("onchain")} className="flex-1"> + setPaymentMethod("onchain")} + className="flex-1" + >
Onchain @@ -245,7 +279,8 @@ function NewChannelInternal({ network }: { network: Network }) { setPaymentMethod("lightning")}>
- + setSelectedPeer( + recommendedPeers.find((x) => x.name === value) + ) + } + > @@ -275,28 +317,34 @@ function NewChannelInternal({ network }: { network: Network }) { peer.network === network && peer.paymentMethod === order.paymentMethod ) - .map((peer) => + .map((peer) => (
{peer.name} - {peer.minimumChannelSize > 0 && - Min. {new Intl.NumberFormat().format(peer.minimumChannelSize)} sats - } + {peer.minimumChannelSize > 0 && ( + + Min.{" "} + {new Intl.NumberFormat().format( + peer.minimumChannelSize + )}{" "} + sats + + )}
- )} + ))} - {selectedPeer.name === "Custom" && <> -
- -
- } + {selectedPeer.name === "Custom" && ( + <> +
+ + )}
)}
@@ -311,7 +359,7 @@ function NewChannelInternal({ network }: { network: Network }) { )} - + ); } @@ -403,7 +451,12 @@ function NewChannelOnchain(props: NewChannelOnchainProps) { /> {nodeDetails && (
- ⬤ + + ⬤ + {nodeDetails.alias && ( <> {nodeDetails.alias} ({nodeDetails.active_channel_count}{" "} @@ -438,7 +491,22 @@ function NewChannelOnchain(props: NewChannelOnchainProps) { onCheckedChange={() => setPublic(!isPublic)} className="mr-2" /> - +
diff --git a/ldk.go b/ldk.go index 3b88a10b..dd30c392 100644 --- a/ldk.go +++ b/ldk.go @@ -56,6 +56,7 @@ func NewLDKService(ctx context.Context, svc *Service, mnemonic, workDir string, lsp.VoltageLSP().Pubkey, lsp.OlympusLSP().Pubkey, lsp.AlbyPlebsLSP().Pubkey, + lsp.AlbyMutinynetPlebsLSP().Pubkey, lsp.OlympusMutinynetFlowLSP().Pubkey, } config.AnchorChannelsConfig.TrustedPeersNoReserve = []string{ @@ -63,6 +64,7 @@ func NewLDKService(ctx context.Context, svc *Service, mnemonic, workDir string, lsp.OlympusMutinynetLSPS1LSP().Pubkey, lsp.OlympusMutinynetFlowLSP().Pubkey, lsp.AlbyPlebsLSP().Pubkey, + lsp.AlbyMutinynetPlebsLSP().Pubkey, "0296b2db342fcf87ea94d981757fdf4d3e545bd5cef4919f58b5d38dfdd73bf5c9", // blocktank } diff --git a/models/lsp/lsp.go b/models/lsp/lsp.go index c70d71f3..0fbcff28 100644 --- a/models/lsp/lsp.go +++ b/models/lsp/lsp.go @@ -62,3 +62,11 @@ func AlbyPlebsLSP() LSP { } return lsp } +func AlbyMutinynetPlebsLSP() LSP { + lsp := LSP{ + Pubkey: "02f7029c14f3d805843e065d42e9bdc57f5f414249f335906bbe282ff99b2be17a", + Url: "https://lsp-mutinynet.albylabs.com", + LspType: LSP_TYPE_PMLSP, + } + return lsp +}