feat: Split new channel flow into "Increase Spending Balance" and "Increase Receiving Capacity" (#500)

This commit is contained in:
Roland 2024-06-26 11:44:52 +07:00 committed by GitHub
parent 85381b9183
commit 42d70d53ac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 445 additions and 158 deletions

View file

@ -662,6 +662,16 @@ func (svc *albyOAuthService) GetChannelPeerSuggestions(ctx context.Context) ([]C
return nil, err
}
// TODO: remove once alby API is updated
for i, suggestion := range suggestions {
if suggestion.BrokenLspType != "" {
suggestions[i].LspType = suggestion.BrokenLspType
}
if suggestion.BrokenLspUrl != "" {
suggestions[i].LspUrl = suggestion.BrokenLspUrl
}
}
logger.Logger.WithFields(logrus.Fields{"channel_suggestions": suggestions}).Info("Alby channel peer suggestions response")
return suggestions, nil
}

View file

@ -52,7 +52,10 @@ type ChannelPeerSuggestion struct {
MinimumChannelSize uint64 `json:"minimumChannelSize"`
Name string `json:"name"`
Image string `json:"image"`
Lsp string `json:"lsp"`
BrokenLspUrl string `json:"lsp_url"`
BrokenLspType string `json:"lsp_type"`
LspUrl string `json:"lspUrl"`
LspType string `json:"lspType"`
}
type ErrorResponse struct {

View file

@ -28,33 +28,12 @@ type lspInfo struct {
}
func (api *api) NewInstantChannelInvoice(ctx context.Context, request *NewInstantChannelInvoiceRequest) (*NewInstantChannelInvoiceResponse, error) {
var selectedLsp lsp.LSP
switch request.LSP {
case "VOLTAGE":
selectedLsp = lsp.VoltageLSP()
case "OLYMPUS_FLOW_2_0":
selectedLsp = lsp.OlympusLSP()
case "OLYMPUS_MUTINYNET_FLOW_2_0":
selectedLsp = lsp.OlympusMutinynetFlowLSP()
case "OLYMPUS_MUTINYNET_LSPS1":
selectedLsp = lsp.OlympusMutinynetLSPS1LSP()
case "ALBY":
selectedLsp = lsp.AlbyPlebsLSP()
case "ALBY_MUTINYNET":
selectedLsp = lsp.AlbyMutinynetPlebsLSP()
case "MEGALITH":
selectedLsp = lsp.MegalithLSP()
case "MEGALITH_MUTINYNET":
selectedLsp = lsp.MegalithMutinynetLSP()
default:
return nil, errors.New("unknown LSP")
}
if api.svc.GetLNClient() == nil {
return nil, errors.New("LNClient not started")
}
if selectedLsp.LspType != lsp.LSP_TYPE_LSPS1 && request.Public {
if request.LSPType != lsp.LSP_TYPE_LSPS1 && request.Public {
return nil, errors.New("This LSP option does not support public channels")
}
@ -62,17 +41,17 @@ func (api *api) NewInstantChannelInvoice(ctx context.Context, request *NewInstan
var lspInfo *lspInfo
var err error
switch selectedLsp.LspType {
switch request.LSPType {
case lsp.LSP_TYPE_FLOW_2_0:
fallthrough
case lsp.LSP_TYPE_PMLSP:
lspInfo, err = api.getFlowLSPInfo(selectedLsp.Url + "/info")
lspInfo, err = api.getFlowLSPInfo(request.LSPUrl + "/info")
case lsp.LSP_TYPE_LSPS1:
lspInfo, err = api.getLSPS1LSPInfo(selectedLsp.Url + "/get_info")
lspInfo, err = api.getLSPS1LSPInfo(request.LSPUrl + "/get_info")
default:
return nil, fmt.Errorf("unsupported LSP type: %v", selectedLsp.LspType)
return nil, fmt.Errorf("unsupported LSP type: %v", request.LSPType)
}
if err != nil {
logger.Logger.WithError(err).Error("Failed to request LSP info")
@ -84,7 +63,7 @@ func (api *api) NewInstantChannelInvoice(ctx context.Context, request *NewInstan
nodeInfo, err := api.svc.GetLNClient().GetInfo(ctx)
if err != nil {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"url": selectedLsp.Url,
"url": request.LSPUrl,
}).Error("Failed to request own node info", err)
return nil, err
}
@ -105,16 +84,13 @@ func (api *api) NewInstantChannelInvoice(ctx context.Context, request *NewInstan
invoice := ""
var fee uint64 = 0
switch selectedLsp.LspType {
switch request.LSPType {
case lsp.LSP_TYPE_FLOW_2_0:
invoice, fee, err = api.requestFlow20WrappedInvoice(ctx, &selectedLsp, request.Amount, nodeInfo.Pubkey)
invoice, fee, err = api.requestFlow20WrappedInvoice(ctx, request, nodeInfo.Pubkey)
case lsp.LSP_TYPE_PMLSP:
invoice, fee, err = api.requestPMLSPInvoice(&selectedLsp, request.Amount, nodeInfo.Pubkey)
invoice, fee, err = api.requestPMLSPInvoice(request, nodeInfo.Pubkey)
case lsp.LSP_TYPE_LSPS1:
invoice, fee, err = api.requestLSPS1Invoice(ctx, &selectedLsp, request.Amount, nodeInfo.Pubkey, request.Public, lspInfo.MaxChannelExpiryBlocks)
default:
return nil, fmt.Errorf("unsupported LSP type: %v", selectedLsp.LspType)
invoice, fee, err = api.requestLSPS1Invoice(ctx, request, nodeInfo.Pubkey, lspInfo.MaxChannelExpiryBlocks)
}
if err != nil {
logger.Logger.WithError(err).Error("Failed to request invoice")
@ -291,7 +267,7 @@ func (api *api) getFlowLSPInfo(url string) (*lspInfo, error) {
}, nil
}
func (api *api) requestFlow20WrappedInvoice(ctx context.Context, selectedLsp *lsp.LSP, amount uint64, pubkey string) (invoice string, fee uint64, err error) {
func (api *api) requestFlow20WrappedInvoice(ctx context.Context, request *NewInstantChannelInvoiceRequest, pubkey string) (invoice string, fee uint64, err error) {
logger.Logger.Infoln("Requesting fee information")
type FeeRequest struct {
@ -309,7 +285,7 @@ func (api *api) requestFlow20WrappedInvoice(ctx context.Context, selectedLsp *ls
Timeout: time.Second * 10,
}
payloadBytes, err := json.Marshal(FeeRequest{
AmountMsat: amount * 1000,
AmountMsat: request.Amount * 1000,
Pubkey: pubkey,
})
if err != nil {
@ -317,10 +293,10 @@ func (api *api) requestFlow20WrappedInvoice(ctx context.Context, selectedLsp *ls
}
bodyReader := bytes.NewReader(payloadBytes)
req, err := http.NewRequest(http.MethodPost, selectedLsp.Url+"/fee", bodyReader)
req, err := http.NewRequest(http.MethodPost, request.LSPUrl+"/fee", bodyReader)
if err != nil {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"url": selectedLsp.Url,
"url": request.LSPUrl,
}).Error("Failed to create lsp fee request")
return "", 0, err
}
@ -330,7 +306,7 @@ func (api *api) requestFlow20WrappedInvoice(ctx context.Context, selectedLsp *ls
res, err := client.Do(req)
if err != nil {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"url": selectedLsp.Url,
"url": request.LSPUrl,
}).Error("Failed to request lsp fee")
return "", 0, err
}
@ -340,7 +316,7 @@ func (api *api) requestFlow20WrappedInvoice(ctx context.Context, selectedLsp *ls
body, err := io.ReadAll(res.Body)
if err != nil {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"url": selectedLsp.Url,
"url": request.LSPUrl,
}).Error("Failed to read response body")
return "", 0, errors.New("failed to read response body")
}
@ -356,13 +332,13 @@ func (api *api) requestFlow20WrappedInvoice(ctx context.Context, selectedLsp *ls
err = json.Unmarshal(body, &feeResponse)
if err != nil {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"url": selectedLsp.Url,
"url": request.LSPUrl,
}).Error("Failed to deserialize json")
return "", 0, fmt.Errorf("failed to deserialize json %s %s", selectedLsp.Url, string(body))
return "", 0, fmt.Errorf("failed to deserialize json %s %s", request.LSPUrl, string(body))
}
logger.Logger.WithError(err).WithFields(logrus.Fields{
"url": selectedLsp.Url,
"url": request.LSPUrl,
"feeResponse": feeResponse,
}).Info("Got fee response")
if feeResponse.Id == "" {
@ -376,7 +352,7 @@ func (api *api) requestFlow20WrappedInvoice(ctx context.Context, selectedLsp *ls
// because we don't want the sender to pay the fee
// see: https://docs.voltage.cloud/voltage-lsp#gqBqV
makeInvoiceResponse, err := api.svc.GetLNClient().MakeInvoice(ctx, int64(amount)*1000-int64(feeResponse.FeeAmountMsat), "", "", 60*60)
makeInvoiceResponse, err := api.svc.GetLNClient().MakeInvoice(ctx, int64(request.Amount)*1000-int64(feeResponse.FeeAmountMsat), "", "", 60*60)
if err != nil {
logger.Logger.WithError(err).Error("Failed to request own invoice")
return "", 0, fmt.Errorf("failed to request own invoice %v", err)
@ -406,10 +382,10 @@ func (api *api) requestFlow20WrappedInvoice(ctx context.Context, selectedLsp *ls
}
bodyReader := bytes.NewReader(payloadBytes)
req, err := http.NewRequest(http.MethodPost, selectedLsp.Url+"/proposal", bodyReader)
req, err := http.NewRequest(http.MethodPost, request.LSPUrl+"/proposal", bodyReader)
if err != nil {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"url": selectedLsp.Url,
"url": request.LSPUrl,
}).Error("Failed to create lsp fee request")
return "", 0, err
}
@ -419,7 +395,7 @@ func (api *api) requestFlow20WrappedInvoice(ctx context.Context, selectedLsp *ls
res, err := client.Do(req)
if err != nil {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"url": selectedLsp.Url,
"url": request.LSPUrl,
}).Error("Failed to request lsp fee")
return "", 0, err
}
@ -429,7 +405,7 @@ func (api *api) requestFlow20WrappedInvoice(ctx context.Context, selectedLsp *ls
body, err := io.ReadAll(res.Body)
if err != nil {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"url": selectedLsp.Url,
"url": request.LSPUrl,
}).Error("Failed to read response body")
return "", 0, errors.New("failed to read response body")
}
@ -445,14 +421,14 @@ func (api *api) requestFlow20WrappedInvoice(ctx context.Context, selectedLsp *ls
err = json.Unmarshal(body, &proposalResponse)
if err != nil {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"url": selectedLsp.Url,
"url": request.LSPUrl,
}).Error("Failed to deserialize json")
return "", 0, fmt.Errorf("failed to deserialize json %s %s", selectedLsp.Url, string(body))
return "", 0, fmt.Errorf("failed to deserialize json %s %s", request.LSPUrl, string(body))
}
logger.Logger.WithField("proposalResponse", proposalResponse).Info("Got proposal response")
if proposalResponse.Bolt11 == "" {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"url": selectedLsp.Url,
"url": request.LSPUrl,
"proposalResponse": proposalResponse,
}).Error("No bolt11 in proposal response")
return "", 0, fmt.Errorf("no bolt11 in proposal response %v", proposalResponse)
@ -463,7 +439,7 @@ func (api *api) requestFlow20WrappedInvoice(ctx context.Context, selectedLsp *ls
return invoice, fee, nil
}
func (api *api) requestPMLSPInvoice(selectedLsp *lsp.LSP, amount uint64, pubkey string) (invoice string, fee uint64, err error) {
func (api *api) requestPMLSPInvoice(request *NewInstantChannelInvoiceRequest, pubkey string) (invoice string, fee uint64, err error) {
type NewInstantChannelRequest struct {
Amount uint64 `json:"amount"`
Pubkey string `json:"pubkey"`
@ -473,7 +449,7 @@ func (api *api) requestPMLSPInvoice(selectedLsp *lsp.LSP, amount uint64, pubkey
Timeout: time.Second * 10,
}
payloadBytes, err := json.Marshal(NewInstantChannelRequest{
Amount: amount,
Amount: request.Amount,
Pubkey: pubkey,
})
if err != nil {
@ -481,10 +457,10 @@ func (api *api) requestPMLSPInvoice(selectedLsp *lsp.LSP, amount uint64, pubkey
}
bodyReader := bytes.NewReader(payloadBytes)
req, err := http.NewRequest(http.MethodPost, selectedLsp.Url+"/new-channel", bodyReader)
req, err := http.NewRequest(http.MethodPost, request.LSPUrl+"/new-channel", bodyReader)
if err != nil {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"url": selectedLsp.Url,
"url": request.LSPUrl,
}).Error("Failed to create new channel request")
return "", 0, err
}
@ -494,7 +470,7 @@ func (api *api) requestPMLSPInvoice(selectedLsp *lsp.LSP, amount uint64, pubkey
res, err := client.Do(req)
if err != nil {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"url": selectedLsp.Url,
"url": request.LSPUrl,
}).Error("Failed to request new channel invoice")
return "", 0, err
}
@ -504,7 +480,7 @@ func (api *api) requestPMLSPInvoice(selectedLsp *lsp.LSP, amount uint64, pubkey
body, err := io.ReadAll(res.Body)
if err != nil {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"url": selectedLsp.Url,
"url": request.LSPUrl,
}).Error("Failed to read response body")
return "", 0, errors.New("failed to read response body")
}
@ -527,9 +503,9 @@ func (api *api) requestPMLSPInvoice(selectedLsp *lsp.LSP, amount uint64, pubkey
err = json.Unmarshal(body, &newChannelResponse)
if err != nil {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"url": selectedLsp.Url,
"url": request.LSPUrl,
}).Error("Failed to deserialize json")
return "", 0, fmt.Errorf("failed to deserialize json %s %s", selectedLsp.Url, string(body))
return "", 0, fmt.Errorf("failed to deserialize json %s %s", request.LSPUrl, string(body))
}
invoice = newChannelResponse.Invoice
@ -538,7 +514,7 @@ func (api *api) requestPMLSPInvoice(selectedLsp *lsp.LSP, amount uint64, pubkey
return invoice, fee, nil
}
func (api *api) requestLSPS1Invoice(ctx context.Context, selectedLsp *lsp.LSP, amount uint64, pubkey string, public bool, channelExpiryBlocks uint64) (invoice string, fee uint64, err error) {
func (api *api) requestLSPS1Invoice(ctx context.Context, request *NewInstantChannelInvoiceRequest, pubkey string, channelExpiryBlocks uint64) (invoice string, fee uint64, err error) {
client := http.Client{
Timeout: time.Second * 10,
}
@ -563,7 +539,7 @@ func (api *api) requestLSPS1Invoice(ctx context.Context, selectedLsp *lsp.LSP, a
var requiredChannelConfirmations uint64 = 0
if public {
if request.Public {
// as per BOLT-7 6 confirmations are required for the channel to be gossiped
// https://github.com/lightning/bolts/blob/master/07-routing-gossip.md#requirements
requiredChannelConfirmations = 6
@ -571,14 +547,14 @@ func (api *api) requestLSPS1Invoice(ctx context.Context, selectedLsp *lsp.LSP, a
newLSPS1ChannelRequest := NewLSPS1ChannelRequest{
PublicKey: pubkey,
LSPBalanceSat: strconv.FormatUint(amount, 10),
LSPBalanceSat: strconv.FormatUint(request.Amount, 10),
ClientBalanceSat: "0",
RequiredChannelConfirmations: requiredChannelConfirmations,
FundingConfirmsWithinBlocks: 6,
ChannelExpiryBlocks: channelExpiryBlocks,
Token: "",
RefundOnchainAddress: refundAddress,
AnnounceChannel: public,
AnnounceChannel: request.Public,
}
payloadBytes, err := json.Marshal(newLSPS1ChannelRequest)
@ -587,10 +563,10 @@ func (api *api) requestLSPS1Invoice(ctx context.Context, selectedLsp *lsp.LSP, a
}
bodyReader := bytes.NewReader(payloadBytes)
req, err := http.NewRequest(http.MethodPost, selectedLsp.Url+"/create_order", bodyReader)
req, err := http.NewRequest(http.MethodPost, request.LSPUrl+"/create_order", bodyReader)
if err != nil {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"url": selectedLsp.Url,
"url": request.LSPUrl,
}).Error("Failed to create new channel request")
return "", 0, err
}
@ -600,7 +576,7 @@ func (api *api) requestLSPS1Invoice(ctx context.Context, selectedLsp *lsp.LSP, a
res, err := client.Do(req)
if err != nil {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"url": selectedLsp.Url,
"url": request.LSPUrl,
}).Error("Failed to request new channel invoice")
return "", 0, err
}
@ -610,7 +586,7 @@ func (api *api) requestLSPS1Invoice(ctx context.Context, selectedLsp *lsp.LSP, a
body, err := io.ReadAll(res.Body)
if err != nil {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"url": selectedLsp.Url,
"url": request.LSPUrl,
}).Error("Failed to read response body")
return "", 0, errors.New("failed to read response body")
}
@ -642,16 +618,16 @@ func (api *api) requestLSPS1Invoice(ctx context.Context, selectedLsp *lsp.LSP, a
err = json.Unmarshal(body, &newChannelResponse)
if err != nil {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"url": selectedLsp.Url,
"url": request.LSPUrl,
}).Error("Failed to deserialize json")
return "", 0, fmt.Errorf("failed to deserialize json %s %s", selectedLsp.Url, string(body))
return "", 0, fmt.Errorf("failed to deserialize json %s %s", request.LSPUrl, string(body))
}
invoice = newChannelResponse.Payment.Bolt11.Invoice
fee, err = strconv.ParseUint(newChannelResponse.Payment.Bolt11.FeeTotalSat, 10, 64)
if err != nil {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"url": selectedLsp.Url,
"url": request.LSPUrl,
}).Error("Failed to parse fee")
return "", 0, fmt.Errorf("failed to parse fee %v", err)
}

View file

@ -234,9 +234,10 @@ type BasicRestoreWailsRequest struct {
type NetworkGraphResponse = lnclient.NetworkGraphResponse
type NewInstantChannelInvoiceRequest struct {
Amount uint64 `json:"amount"`
LSP string `json:"lsp"`
Public bool `json:"public"`
Amount uint64 `json:"amount"`
LSPType string `json:"lspType"`
LSPUrl string `json:"lspUrl"`
Public bool `json:"public"`
}
type NewInstantChannelInvoiceResponse struct {

View file

@ -28,7 +28,7 @@ function SidebarHint() {
// Don't distract with hints while opening a channel or on the settings page
if (
location.pathname.endsWith("/channels/order") ||
location.pathname.endsWith("/channels/new") ||
location.pathname.endsWith("/channels") ||
location.pathname.startsWith("/settings")
) {
return null;
@ -73,7 +73,7 @@ function SidebarHint() {
title="Open Your First Channel"
description="Deposit bitcoin by onchain or lightning payment to start using your new wallet."
buttonText="Begin Now"
buttonLink="/channels/new"
buttonLink="/channels"
/>
);
}

View file

@ -23,7 +23,8 @@ import ShowApp from "src/screens/apps/ShowApp";
import AppStore from "src/screens/appstore/AppStore";
import Channels from "src/screens/channels/Channels";
import { CurrentChannelOrder } from "src/screens/channels/CurrentChannelOrder";
import NewChannel from "src/screens/channels/NewChannel";
import IncreaseIncomingCapacity from "src/screens/channels/IncreaseIncomingCapacity";
import IncreaseOutgoingCapacity from "src/screens/channels/IncreaseOutgoingCapacity";
import MigrateAlbyFunds from "src/screens/onboarding/MigrateAlbyFunds";
import { Success } from "src/screens/onboarding/Success";
import BuyBitcoin from "src/screens/onchain/BuyBitcoin";
@ -146,9 +147,14 @@ const routes = [
element: <Channels />,
},
{
path: "new",
element: <NewChannel />,
handle: { crumb: () => "New Channel" },
path: "outgoing",
element: <IncreaseOutgoingCapacity />,
handle: { crumb: () => "Increase Spending Balance" },
},
{
path: "incoming",
element: <IncreaseIncomingCapacity />,
handle: { crumb: () => "Increase Receiving Capacity" },
},
{
path: "order",

View file

@ -279,20 +279,21 @@ export default function Channels() {
<DropdownMenuContent className="w-56">
<DropdownMenuGroup>
<DropdownMenuItem>
<div className="flex flex-row gap-10 items-center w-full">
<div className="whitespace-nowrap flex flex-row items-center gap-2">
Node
</div>
<div className="overflow-hidden text-ellipsis">
<div
className="flex flex-row gap-4 items-center w-full cursor-pointer"
onClick={() => {
if (!nodeConnectionInfo) {
return;
}
copyToClipboard(nodeConnectionInfo.pubkey);
}}
>
<div>Node</div>
<div className="overflow-hidden text-ellipsis flex-1">
{nodeConnectionInfo?.pubkey || "Loading..."}
</div>
{nodeConnectionInfo && (
<CopyIcon
className="shrink-0 w-4 h-4"
onClick={() => {
copyToClipboard(nodeConnectionInfo.pubkey);
}}
/>
<CopyIcon className="shrink-0 w-4 h-4" />
)}
</div>
</DropdownMenuItem>
@ -340,9 +341,9 @@ export default function Channels() {
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<Link to="/channels/new">
{/* <Link to="/channels/new">
<Button>Open Channel</Button>
</Link>
</Link> */}
</>
}
></AppHeader>
@ -436,7 +437,7 @@ export default function Channels() {
)}
</CardContent>
<CardFooter className="flex justify-end">
<Link to="/channels/new">
<Link to="/channels/outgoing">
<Button variant="outline">Increase</Button>
</Link>
</CardFooter>
@ -458,7 +459,7 @@ export default function Channels() {
</div>
</CardContent>
<CardFooter className="flex justify-end">
<Link to="/channels/new">
<Link to="/channels/incoming">
<Button variant="outline">Increase</Button>
</Link>
</CardFooter>
@ -471,7 +472,7 @@ export default function Channels() {
title="No Channels Available"
description="Connect to the Lightning Network by establishing your first channel and start transacting."
buttonText="Open Channel"
buttonLink="/channels/new"
buttonLink="/channels/outgoing"
/>
)}

View file

@ -565,12 +565,13 @@ function PayLightningChannelOrder({ order }: { order: NewChannelOrder }) {
(async () => {
try {
setPrevChannels(channels);
if (!order.lsp) {
throw new Error("no lsp selected");
if (!order.lspType || !order.lspUrl) {
throw new Error("missing lsp info in order");
}
const newInstantChannelInvoiceRequest: NewInstantChannelInvoiceRequest =
{
lsp: order.lsp,
lspType: order.lspType,
lspUrl: order.lspUrl,
amount: parseInt(order.amount),
public: order.isPublic,
};
@ -596,15 +597,22 @@ function PayLightningChannelOrder({ order }: { order: NewChannelOrder }) {
}
return true;
});
}, [channels, csrf, order.amount, order.isPublic, order.lsp]);
}, [
channels,
csrf,
order.amount,
order.isPublic,
order.lspType,
order.lspUrl,
]);
return (
<div className="flex flex-col gap-5">
<AppHeader
title={"Buy an Instant Channel"}
title={"Buy Channel"}
description={
wrappedInvoiceResponse
? "Complete Payment to open an instant channel to your node"
? "Complete Payment to open a channel to your node"
: "Please wait, loading..."
}
/>

View file

@ -0,0 +1,302 @@
import { Box, Zap } from "lucide-react";
import React, { FormEvent } from "react";
import { Link, useNavigate } from "react-router-dom";
import AppHeader from "src/components/AppHeader";
import ExternalLink from "src/components/ExternalLink";
import Loading from "src/components/Loading";
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 { useChannelPeerSuggestions } from "src/hooks/useChannelPeerSuggestions";
import { useInfo } from "src/hooks/useInfo";
import { cn, formatAmount } from "src/lib/utils";
import useChannelOrderStore from "src/state/ChannelOrderStore";
import { Network, NewChannelOrder, RecommendedChannelPeer } from "src/types";
function getPeerKey(peer: RecommendedChannelPeer) {
return JSON.stringify(peer);
}
export default function IncreaseIncomingCapacity() {
const { data: info } = useInfo();
if (!info?.network) {
return <Loading />;
}
return <NewChannelInternal network={info.network} />;
}
function NewChannelInternal({ network }: { network: Network }) {
const { data: _channelPeerSuggestions } = useChannelPeerSuggestions();
const navigate = useNavigate();
const [order, setOrder] = React.useState<Partial<NewChannelOrder>>({
paymentMethod: "lightning",
status: "pay",
});
const [selectedPeer, setSelectedPeer] = React.useState<
RecommendedChannelPeer | undefined
>();
const channelPeerSuggestions = React.useMemo(() => {
return _channelPeerSuggestions
? [
..._channelPeerSuggestions.filter(
(peer) =>
peer.paymentMethod === "lightning" && peer.lspType === "LSPS1"
),
]
: undefined;
}, [_channelPeerSuggestions]);
function setPublic(isPublic: boolean) {
setOrder((current) => ({
...current,
isPublic,
}));
}
const setAmount = React.useCallback((amount: string) => {
setOrder((current) => ({
...current,
amount,
}));
}, []);
React.useEffect(() => {
if (!channelPeerSuggestions) {
return;
}
const recommendedPeer = channelPeerSuggestions.find(
(peer) =>
peer.network === network && peer.paymentMethod === order.paymentMethod
);
setSelectedPeer(recommendedPeer);
}, [network, order.paymentMethod, channelPeerSuggestions]);
React.useEffect(() => {
if (selectedPeer) {
if (
selectedPeer.paymentMethod === "lightning" &&
order.paymentMethod === "lightning"
) {
setOrder((current) => ({
...current,
lspType: selectedPeer.lspType,
lspUrl: selectedPeer.lspUrl,
}));
}
setAmount(selectedPeer.minimumChannelSize.toString());
}
}, [order.paymentMethod, selectedPeer, setAmount]);
const selectedCardStyles = "border-primary border-2 font-medium";
const presetAmounts = [1_000_000, 2_000_000, 3_000_000];
function onSubmit(e: FormEvent) {
e.preventDefault();
useChannelOrderStore.getState().setOrder(order as NewChannelOrder);
navigate("/channels/order");
}
if (!channelPeerSuggestions) {
return <Loading />;
}
return (
<>
<AppHeader
title="Increase Receiving Capacity"
description="Purchase a channel with incoming capacity to receive payments"
/>
<form
onSubmit={onSubmit}
className="md:max-w-md max-w-full flex flex-col gap-5"
>
<div className="grid gap-1.5">
<Label htmlFor="amount">Channel size (sats)</Label>
{order.amount && +order.amount < 200_000 && (
<p className="text-muted-foreground text-xs">
For a smooth experience consider a opening a channel of 200k sats
in size or more.{" "}
<ExternalLink
to="https://guides.getalby.com/user-guide/v/alby-account-and-browser-extension/alby-hub/liquidity"
className="underline"
>
Learn more
</ExternalLink>
</p>
)}
<Input
id="amount"
type="number"
required
min={selectedPeer?.minimumChannelSize || 100000}
value={order.amount}
onChange={(e) => {
setAmount(e.target.value.trim());
}}
/>
<div className="grid grid-cols-3 gap-1.5 text-muted-foreground text-xs">
{presetAmounts.map((amount) => (
<div
key={amount}
className={cn(
"text-center border rounded p-2 cursor-pointer hover:border-muted-foreground",
+(order.amount || "0") === amount &&
"border-primary hover:border-primary"
)}
onClick={() => setAmount(amount.toString())}
>
{formatAmount(amount * 1000, 0)}
</div>
))}
</div>
</div>
<div className="grid gap-3">
<Label htmlFor="amount">Payment method</Label>
<div className="grid grid-cols-2 gap-3">
<div
//onClick={() => setPaymentMethod("onchain")}
className="flex-1 opacity-50 select-none"
>
<div
className={cn(
"rounded-xl border bg-card text-card-foreground shadow p-5 flex flex-col items-center gap-3",
order.paymentMethod === "onchain"
? selectedCardStyles
: undefined
)}
>
<Box className="w-4 h-4" />
Onchain
</div>
</div>
<Link
to="#"
//onClick={() => setPaymentMethod("lightning")}
>
<div
className={cn(
"rounded-xl border bg-card text-card-foreground shadow p-5 flex flex-col items-center gap-3",
order.paymentMethod === "lightning"
? selectedCardStyles
: undefined
)}
>
<Zap className="w-4 h-4" />
Lightning
</div>
</Link>
</div>
</div>
<div className="flex flex-col gap-3">
{selectedPeer && (
<div className="grid gap-1.5">
<Label>Channel peer</Label>
<Select
value={getPeerKey(selectedPeer)}
onValueChange={(value) =>
setSelectedPeer(
channelPeerSuggestions.find((x) => getPeerKey(x) === value)
)
}
>
<SelectTrigger>
<SelectValue placeholder="Select channel peer" />
</SelectTrigger>
<SelectContent>
{channelPeerSuggestions
.filter(
(peer) =>
peer.network === network &&
peer.paymentMethod === order.paymentMethod
)
.map((peer) => (
<SelectItem
value={getPeerKey(peer)}
key={getPeerKey(peer)}
>
<div className="flex items-center gap-3">
<div className="flex items-center gap-3">
{peer.name !== "Custom" && (
<img
src={peer.image}
className="w-8 h-8 object-contain"
/>
)}
<div>
{peer.name}
{peer.minimumChannelSize > 0 && (
<span className="ml-4 text-xs text-muted-foreground">
Min.{" "}
{new Intl.NumberFormat().format(
peer.minimumChannelSize
)}{" "}
sats
</span>
)}
</div>
</div>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
{selectedPeer.name === "Custom" && (
<>
<div className="grid gap-1.5"></div>
</>
)}
</div>
)}
</div>
{order.paymentMethod === "lightning" && (
<NewChannelLightning order={order} setOrder={setOrder} />
)}
<div className="mt-2 flex items-top space-x-2">
<Checkbox
id="public-channel"
defaultChecked={order.isPublic}
onCheckedChange={() => setPublic(!order.isPublic)}
className="mr-2"
/>
<div className="grid gap-1.5 leading-none">
<Label htmlFor="public-channel" className="flex items-center gap-2">
Public Channel
</Label>
<p className="text-xs text-muted-foreground">
Enable if you want to receive keysend payments. (e.g. podcasting)
</p>
</div>
</div>
<Button size="lg">Next</Button>
</form>
</>
);
}
type NewChannelLightningProps = {
order: Partial<NewChannelOrder>;
setOrder(order: Partial<NewChannelOrder>): void;
};
function NewChannelLightning(props: NewChannelLightningProps) {
if (props.order.paymentMethod !== "lightning") {
throw new Error("unexpected payment method");
}
return null;
}

View file

@ -32,7 +32,7 @@ function getPeerKey(peer: RecommendedChannelPeer) {
return JSON.stringify(peer);
}
export default function NewChannel() {
export default function IncreaseOutgoingCapacity() {
const { data: info } = useInfo();
if (!info?.network) {
@ -66,7 +66,13 @@ function NewChannelInternal({ network }: { network: Network }) {
image: "",
};
return _channelPeerSuggestions
? [..._channelPeerSuggestions, customOption]
? [
..._channelPeerSuggestions.filter(
(peer) =>
peer.paymentMethod !== "lightning" || peer.lspType !== "LSPS1"
),
customOption,
]
: undefined;
}, [_channelPeerSuggestions, network]);
@ -121,7 +127,8 @@ function NewChannelInternal({ network }: { network: Network }) {
) {
setOrder((current) => ({
...current,
lsp: selectedPeer.lsp,
lspType: selectedPeer.lspType,
lspUrl: selectedPeer.lspUrl,
}));
}
setAmount(selectedPeer.minimumChannelSize.toString());
@ -144,7 +151,7 @@ function NewChannelInternal({ network }: { network: Network }) {
return (
<>
<AppHeader
title="Open a channel"
title="Increase Spending Balance"
description="Funds used to open a channel minus fees will be added to your spending balance"
/>
<form

View file

@ -53,7 +53,8 @@ export default function MigrateAlbyFunds() {
}
const newInstantChannelInvoiceRequest: NewInstantChannelInvoiceRequest =
{
lsp: "ALBY",
lspUrl: "https://lsp.albylabs.com",
lspType: "PMLSP",
amount,
public: false,
};
@ -213,7 +214,7 @@ export default function MigrateAlbyFunds() {
>
Migrate Funds and Open Channel
</LoadingButton>
<Link to="/channels/new">
<Link to="/channels">
<Button variant="link">Explore Other Options</Button>
</Link>
</form>
@ -229,7 +230,7 @@ export default function MigrateAlbyFunds() {
external wallet though.
</AlertDescription>
</Alert>
<Link to="/channels/new" className="w-full">
<Link to="/channels" className="w-full">
<Button className="w-full">Explore Other Options</Button>
</Link>
</>

View file

@ -61,7 +61,7 @@ function OnboardingChecklist() {
description:
"Establish a new Lightning channel to enable fast and low-fee Bitcoin transactions.",
checked: hasChannel,
to: "/channels/new",
to: "/channels",
},
{
title: "Link your Alby Account",

View file

@ -254,6 +254,8 @@ export type SetupNodeInfo = Partial<{
phoenixdAuthorization?: string;
}>;
export type LSPType = "LSPS1" | "Flow 2.0" | "PMLSP";
export type RecommendedChannelPeer = {
network: Network;
image: string;
@ -267,7 +269,8 @@ export type RecommendedChannelPeer = {
}
| {
paymentMethod: "lightning";
lsp: string;
lspType: LSPType;
lspUrl: string;
}
);
@ -289,7 +292,8 @@ export type AlbyBalance = {
export type NewInstantChannelInvoiceRequest = {
amount: number;
lsp: string;
lspType: LSPType;
lspUrl: string;
public: boolean;
};
@ -334,6 +338,7 @@ export type NewChannelOrder = {
}
| {
paymentMethod: "lightning";
lsp: string;
lspType: LSPType;
lspUrl: string;
}
);

View file

@ -67,18 +67,16 @@ func NewLDKService(ctx context.Context, cfg config.Config, eventPublisher events
"[::]:9735",
}
config.TrustedPeers0conf = []string{
lsp.VoltageLSP().Pubkey,
lsp.OlympusLSP().Pubkey,
lsp.AlbyPlebsLSP().Pubkey,
lsp.MegalithLSP().Pubkey,
// Mutinynet
lsp.AlbyMutinynetPlebsLSP().Pubkey,
lsp.OlympusMutinynetFlowLSP().Pubkey,
lsp.OlympusMutinynetLSP().Pubkey,
lsp.MegalithMutinynetLSP().Pubkey,
}
config.AnchorChannelsConfig.TrustedPeersNoReserve = []string{
lsp.VoltageLSP().Pubkey,
lsp.OlympusLSP().Pubkey,
lsp.AlbyPlebsLSP().Pubkey,
lsp.MegalithLSP().Pubkey,
@ -86,7 +84,7 @@ func NewLDKService(ctx context.Context, cfg config.Config, eventPublisher events
// Mutinynet
lsp.AlbyMutinynetPlebsLSP().Pubkey,
lsp.OlympusMutinynetFlowLSP().Pubkey,
lsp.OlympusMutinynetLSP().Pubkey,
lsp.MegalithMutinynetLSP().Pubkey,
}
@ -110,7 +108,8 @@ func NewLDKService(ctx context.Context, cfg config.Config, eventPublisher events
// TODO: remove when https://github.com/lightningdevkit/rust-lightning/issues/2914 is merged
// LDK default HTLC inflight value is 10% of the channel size. If an LSPS service is configured this will be set to 0.
builder.SetLiquiditySourceLsps2("52.88.33.119:9735", lsp.VoltageLSP().Pubkey, nil)
// The liquidity source below is not used because we do not use the native LDK-node LSPS2 API.
builder.SetLiquiditySourceLsps2("52.88.33.119:9735", lsp.OlympusLSP().Pubkey, nil)
//builder.SetLogDirPath (filepath.Join(newpath, "./logs")); // missing?
node, err := builder.Build()

View file

@ -1,9 +1,7 @@
package lsp
type LSP struct {
Pubkey string
Url string
LspType string
Pubkey string
}
const (
@ -12,73 +10,43 @@ const (
LSP_TYPE_LSPS1 = "LSPS1"
)
func VoltageLSP() LSP {
func OlympusMutinynetLSP() LSP {
lsp := LSP{
Pubkey: "03aefa43fbb4009b21a4129d05953974b7dbabbbfb511921410080860fca8ee1f0",
Url: "https://lsp.voltageapi.com/api/v1",
LspType: LSP_TYPE_FLOW_2_0,
}
return lsp
}
func OlympusMutinynetFlowLSP() LSP {
lsp := LSP{
Pubkey: "032ae843e4d7d177f151d021ac8044b0636ec72b1ce3ffcde5c04748db2517ab03",
Url: "https://mutinynet-flow.lnolymp.us/api/v1",
LspType: LSP_TYPE_FLOW_2_0,
}
return lsp
}
func OlympusMutinynetLSPS1LSP() LSP {
lsp := LSP{
Pubkey: "032ae843e4d7d177f151d021ac8044b0636ec72b1ce3ffcde5c04748db2517ab03",
Url: "https://mutinynet-lsps1.lnolymp.us/api/v1",
LspType: LSP_TYPE_LSPS1,
Pubkey: "032ae843e4d7d177f151d021ac8044b0636ec72b1ce3ffcde5c04748db2517ab03",
}
return lsp
}
func OlympusLSP() LSP {
lsp := LSP{
Pubkey: "031b301307574bbe9b9ac7b79cbe1700e31e544513eae0b5d7497483083f99e581",
Url: "https://0conf.lnolymp.us/api/v1",
LspType: LSP_TYPE_FLOW_2_0,
Pubkey: "031b301307574bbe9b9ac7b79cbe1700e31e544513eae0b5d7497483083f99e581",
}
return lsp
}
func AlbyPlebsLSP() LSP {
lsp := LSP{
Pubkey: "029ca15ad2ea3077f5f0524c4c9bc266854c14b9fc81b9cc3d6b48e2460af13f65",
Url: "https://lsp.albylabs.com",
LspType: LSP_TYPE_PMLSP,
Pubkey: "029ca15ad2ea3077f5f0524c4c9bc266854c14b9fc81b9cc3d6b48e2460af13f65",
}
return lsp
}
func AlbyMutinynetPlebsLSP() LSP {
lsp := LSP{
Pubkey: "02f7029c14f3d805843e065d42e9bdc57f5f414249f335906bbe282ff99b2be17a",
Url: "https://lsp-mutinynet.albylabs.com",
LspType: LSP_TYPE_PMLSP,
Pubkey: "02f7029c14f3d805843e065d42e9bdc57f5f414249f335906bbe282ff99b2be17a",
}
return lsp
}
func MegalithMutinynetLSP() LSP {
lsp := LSP{
Pubkey: "03e30fda71887a916ef5548a4d02b06fe04aaa1a8de9e24134ce7f139cf79d7579",
Url: "https://lsp1.mutiny.megalith-node.com/api/lsps1/v1",
LspType: LSP_TYPE_LSPS1,
Pubkey: "03e30fda71887a916ef5548a4d02b06fe04aaa1a8de9e24134ce7f139cf79d7579",
}
return lsp
}
func MegalithLSP() LSP {
lsp := LSP{
Pubkey: "038a9e56512ec98da2b5789761f7af8f280baf98a09282360cd6ff1381b5e889bf",
Url: "https://megalithic.me/api/lsps1/v1",
LspType: LSP_TYPE_LSPS1,
Pubkey: "038a9e56512ec98da2b5789761f7af8f280baf98a09282360cd6ff1381b5e889bf",
}
return lsp
}