feat: just in time channels with lsps2 (#2275)

* feat: just in time channels with lsps2

* fix: clarify JIT receive channel fee

* fix: fees

* fix: fees 2

* fix: don't show low inbound when LSPS2 is active

* fix: remove the receive limit below the input if LSPS2 is being used

* fix: simplify

* fix: bring back fee % for outgoing

* fix: remove unneeded changes

* fix: typo

* fix: unneeded

* fix: don't show open first channel is LSPS2

* feat: clearer JIT channel fee copy on receive screen

* fix: add LSPS2 var info

* fix: don't duplicate JIT fee hint on create invoice form

* fix: make paymentDone a standard boolean

* fix: update to golang:1.26 in Dockerfile

* feat: read LSPS2 sources from channel suggestions, set minimum receive amount, update guide link

* docs:  update LDK_LSPS2_ADDRESSES to be used as an override

* fix: only show minimum jit receive amount on validation error

* fix: add more detail to receive error when receiving low amounts with jit

* fix: do not use JIT when user has public channels

* feat: add option to disable JIT

* fix: isTrusted check, add jit property to event

* fix: do not require node restart for toggling JIT

* chore: simplify JIT alert

* chore: add guide link on node settings JIT description

* feat: fetch the lsp2info to have access to params like minimum/maximum payment size

* refactor: share single learn-more link across JIT fee hint branches

* fix: remove variable amount invoice support

* fix: use lsps2info for min payment size and remove channelPeerSuggestion usage of minimumChannelSize

* fix: only do amount validation according to lsps2Info values if jit is enabled in settings

* feat: add jit first payment fee alert on receive via lightning address

* fix: remove unnecessary conditional

* fix: ensure at least one sat is left over when opening JIT channel

* chore: remove hardcoded suggestions

* chore: rename JIT enabled config variable

* fix: ui checks when JIT is disabled

* fix: amount input validation message

* fix: formatting

---------

Co-authored-by: anon <anon@anon.com>
Co-authored-by: saunter <68239231+stackingsaunter@users.noreply.github.com>
Co-authored-by: fmar <fmar@fmar>
Co-authored-by: René Aaron <rene@twentyuno.net>
Co-authored-by: Roland Bewick <roland.bewick@gmail.com>
This commit is contained in:
frnandu 2026-06-10 09:47:07 +02:00 committed by GitHub
parent e5dc19ae68
commit b55978d7bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 678 additions and 90 deletions

View file

@ -246,6 +246,7 @@ _To configure via env, the following parameters must be provided:_
- `LDK_MAX_PATH_COUNT`: Maximum number of paths that may be used by MPP payments.
- `LDK_LOG_LEVEL`: Log level for the LDK node. Higher is more verbose. Default: 3. This is separate from the main application log level, allowing you to enable more verbose LDK logging (e.g., level 4, 5 or 6) without enabling verbose logging for the entire application.
- `LDK_CHANNEL_MONITOR_WARNING_SIZE_BYTES`: If a channel monitor is larger than this value, a performance warning will be shown on the node page.
- `LDK_LSPS2_ADDRESSES`: Override the LSPS2 just-in-time (JIT) LSP provider for receiving. When set, Alby Hub can receive payments even without inbound liquidity: the configured LSP opens a channel on the fly and the fee is deducted from the incoming payment. Expected format is a single `<pubkey>@<host>:<port>`. When set, the "Open Your First Channel" prompts are hidden since the first channel is created automatically on the first receive.
#### LDK Network Configuration

View file

@ -119,6 +119,7 @@ type ChannelPeerSuggestion struct {
Description string `json:"description"`
Note string `json:"note"`
PublicChannelsAllowed bool `json:"publicChannelsAllowed"`
NodeAddress string `json:"nodeAddress"`
FeeTotalSat1m *uint32 `json:"feeTotalSat1m"`
FeeTotalSat2m *uint32 `json:"feeTotalSat2m"`
FeeTotalSat3m *uint32 `json:"feeTotalSat3m"`

View file

@ -1493,6 +1493,7 @@ func (api *api) GetInfo(ctx context.Context) (*InfoResponse, error) {
info := InfoResponse{}
backendType, _ := api.cfg.Get("LNBackendType", "")
ldkVssEnabled, _ := api.cfg.Get("LdkVssEnabled", "")
jitChannelsEnabled, _ := api.cfg.Get("JitChannelsEnabled", "")
autoUnlockPassword, _ := api.cfg.Get("AutoUnlockPassword", "")
setupCompleted, err := api.cfg.SetupCompleted()
if err != nil {
@ -1516,6 +1517,7 @@ func (api *api) GetInfo(ctx context.Context) (*InfoResponse, error) {
info.EnableAdvancedSetup = api.cfg.GetEnv().EnableAdvancedSetup
info.HideUpdateBanner = api.cfg.GetEnv().HideUpdateBanner
info.LdkVssEnabled = ldkVssEnabled == "true"
info.JitChannelsEnabled = jitChannelsEnabled != "false"
info.VssSupported = backendType == config.LDKBackendType && api.cfg.GetEnv().LDKVssUrl != ""
info.SupportsBolt12 = backendType == config.LDKBackendType || backendType == config.CLNBackendType
info.AutoUnlockPasswordEnabled = autoUnlockPassword != ""
@ -1552,10 +1554,28 @@ func (api *api) GetInfo(ctx context.Context) (*InfoResponse, error) {
type chainSourceProvider interface {
GetChainDataSource() (string, string)
}
type lsps2SourceProvider interface {
GetLiquiditySourceLsps2() string
}
type lsps2MinPaymentSizeProvider interface {
GetLiquiditySourceLsps2MinPaymentSizeMsat() *uint64
}
type lsps2MaxPaymentSizeProvider interface {
GetLiquiditySourceLsps2MaxPaymentSizeMsat() *uint64
}
if ldkService, ok := api.svc.GetLNClient().(chainSourceProvider); ok {
info.ChainDataSourceType, info.ChainDataSourceAddress = ldkService.GetChainDataSource()
}
if ldkService, ok := api.svc.GetLNClient().(lsps2SourceProvider); ok {
info.JitChannelsLiquiditySource = ldkService.GetLiquiditySourceLsps2()
}
if ldkService, ok := api.svc.GetLNClient().(lsps2MinPaymentSizeProvider); ok {
info.JitChannelsMinPaymentSizeMsat = ldkService.GetLiquiditySourceLsps2MinPaymentSizeMsat()
}
if ldkService, ok := api.svc.GetLNClient().(lsps2MaxPaymentSizeProvider); ok {
info.JitChannelsMaxPaymentSizeMsat = ldkService.GetLiquiditySourceLsps2MaxPaymentSizeMsat()
}
}
}
@ -1566,7 +1586,7 @@ func (api *api) GetInfo(ctx context.Context) (*InfoResponse, error) {
return &info, nil
}
func (api *api) SetCurrency(currency string) error {
func (api *api) setCurrency(currency string) error {
if currency == "" {
return fmt.Errorf("currency value cannot be empty")
}
@ -1580,7 +1600,7 @@ func (api *api) SetCurrency(currency string) error {
return nil
}
func (api *api) SetBitcoinDisplayFormat(format string) error {
func (api *api) setBitcoinDisplayFormat(format string) error {
if format != constants.BITCOIN_DISPLAY_FORMAT_SATS && format != constants.BITCOIN_DISPLAY_FORMAT_BIP177 {
return fmt.Errorf("bitcoin display format must be '%s' or '%s'", constants.BITCOIN_DISPLAY_FORMAT_SATS, constants.BITCOIN_DISPLAY_FORMAT_BIP177)
}
@ -1594,21 +1614,43 @@ func (api *api) SetBitcoinDisplayFormat(format string) error {
return nil
}
func (api *api) setJitChannelsEnabled(enabled bool) error {
value := "true"
if !enabled {
value = "false"
}
err := api.cfg.SetUpdate("JitChannelsEnabled", value, "")
if err != nil {
logger.Logger.WithError(err).Error("Failed to update JIT channels setting")
return err
}
return nil
}
func (api *api) UpdateSettings(updateSettingsRequest *UpdateSettingsRequest) error {
if updateSettingsRequest.Currency != "" {
err := api.SetCurrency(updateSettingsRequest.Currency)
err := api.setCurrency(updateSettingsRequest.Currency)
if err != nil {
return fmt.Errorf("failed to set currency: %w", err)
}
}
if updateSettingsRequest.BitcoinDisplayFormat != "" {
err := api.SetBitcoinDisplayFormat(updateSettingsRequest.BitcoinDisplayFormat)
err := api.setBitcoinDisplayFormat(updateSettingsRequest.BitcoinDisplayFormat)
if err != nil {
return fmt.Errorf("failed to set bitcoin display format: %w", err)
}
}
if updateSettingsRequest.JitChannelsEnabled != nil {
err := api.setJitChannelsEnabled(*updateSettingsRequest.JitChannelsEnabled)
if err != nil {
return fmt.Errorf("failed to set JIT channels setting: %w", err)
}
}
return nil
}

View file

@ -64,8 +64,6 @@ type API interface {
MigrateNodeStorage(ctx context.Context, to string) error
GetWalletCapabilities(ctx context.Context) (*WalletCapabilitiesResponse, error)
Health(ctx context.Context) (*HealthResponse, error)
SetCurrency(currency string) error
SetBitcoinDisplayFormat(format string) error
UpdateSettings(updateSettingsRequest *UpdateSettingsRequest) error
LookupSwap(swapId string) (*LookupSwapResponse, error)
ListSwaps() (*ListSwapsResponse, error)
@ -332,6 +330,10 @@ type InfoResponse struct {
MempoolUrl string `json:"mempoolUrl"`
ChainDataSourceType string `json:"chainDataSourceType,omitempty"`
ChainDataSourceAddress string `json:"chainDataSourceAddress,omitempty"`
JitChannelsLiquiditySource string `json:"jitChannelsLiquiditySource,omitempty"`
JitChannelsMinPaymentSizeMsat *uint64 `json:"jitChannelsMinPaymentSizeMsat,omitempty"`
JitChannelsMaxPaymentSizeMsat *uint64 `json:"jitChannelsMaxPaymentSizeMsat,omitempty"`
JitChannelsEnabled bool `json:"jitChannelsEnabled"`
HideUpdateBanner bool `json:"hideUpdateBanner"`
SupportsBolt12 bool `json:"supportsBolt12"`
}
@ -339,6 +341,7 @@ type InfoResponse struct {
type UpdateSettingsRequest struct {
Currency string `json:"currency"`
BitcoinDisplayFormat string `json:"bitcoinDisplayFormat"`
JitChannelsEnabled *bool `json:"jitChannelsEnabled"`
}
type SetNodeAliasRequest struct {

View file

@ -38,6 +38,7 @@ type AppConfig struct {
LDKMaxPathCount uint8 `envconfig:"LDK_MAX_PATH_COUNT" default:"5"`
LDKChannelMonitorWarningSizeBytes uint64 `envconfig:"LDK_CHANNEL_MONITOR_WARNING_SIZE_BYTES" default:"5000000"`
LDKVssUrl string `envconfig:"LDK_VSS_URL" default:"https://vss.getalbypro.com/vss"`
LDKLiquiditySourceLsps2 string `envconfig:"LDK_LSPS2_ADDRESSES"`
LDKListeningAddresses string `envconfig:"LDK_LISTENING_ADDRESSES" default:"[::]:9735"`
LDKAnnouncementAddresses string `envconfig:"LDK_ANNOUNCEMENT_ADDRESSES"`
LDKTransientNetworkGraph bool `envconfig:"LDK_TRANSIENT_NETWORK_GRAPH" default:"false"`

View file

@ -298,6 +298,10 @@ export function CurrencyInputField({
}
function handleChangeMode(event: React.ChangeEvent<HTMLInputElement>) {
// clear any custom validity set via onInvalid so the field re-validates
// on the next submit
event.currentTarget.setCustomValidity("");
const nextValue = event.target.value.trim();
if (mode === "bitcoin") {

View file

@ -0,0 +1,48 @@
import { InfoIcon } from "lucide-react";
import ExternalLink from "src/components/ExternalLink";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import { Alert, AlertDescription, AlertTitle } from "src/components/ui/alert";
import { useChannels } from "src/hooks/useChannels";
import { useInfo } from "src/hooks/useInfo";
export default function FirstChannelJitAlert() {
const { data: info } = useInfo();
const { data: channels } = useChannels();
// a JIT channel only opens when the feature is enabled AND an LSPS2 liquidity
// source is actually configured (jitChannelsEnabled alone is just a settings
// toggle and can be true on backends without an LSPS2 source).
const lsps2Source = info?.jitChannelsEnabled
? info.jitChannelsLiquiditySource
: undefined;
// only relevant when the user has no channels yet - their first received
// payment will open the channel.
if (!lsps2Source || !channels || channels.length > 0) {
return null;
}
const minPaymentSizeMsat = info?.jitChannelsMinPaymentSizeMsat;
return (
<Alert>
<InfoIcon className="h-4 w-4" />
<AlertTitle>First payment opens a channel</AlertTitle>
<AlertDescription className="inline">
A channel fee applies.{" "}
{!!minPaymentSizeMsat && (
<>
Minimum payment{" "}
<FormattedBitcoinAmount amountMsat={minPaymentSizeMsat} />.{" "}
</>
)}
<ExternalLink
to="https://guides.getalby.com/user-guide/alby-hub/faq/what-are-just-in-time-channels"
className="underline"
>
Learn more
</ExternalLink>
</AlertDescription>
</Alert>
);
}

View file

@ -1,4 +1,5 @@
import { CopyIcon, LinkIcon, ReceiptTextIcon, ZapIcon } from "lucide-react";
import FirstChannelJitAlert from "src/components/FirstChannelJitAlert";
import Loading from "src/components/Loading";
import QRCode from "src/components/QRCode";
import { Button } from "src/components/ui/button";
@ -18,6 +19,8 @@ export function ReceiveToLightning() {
}
return (
<div className="grid gap-2">
<FirstChannelJitAlert />
<Card>
<CardContent className="flex flex-col items-center gap-6">
<QRCode value={me.lightning_address} className="w-full h-auto" />
@ -56,5 +59,6 @@ export function ReceiveToLightning() {
</LinkButton>
</CardFooter>
</Card>
</div>
);
}

View file

@ -277,10 +277,10 @@ function TransactionItem({ tx, transactionListKey }: Props) {
<TransactionDetailRow label="Date & Time">
{updatedAt.format("D MMMM YYYY, HH:mm")}
</TransactionDetailRow>
{tx.state != "failed" && type == "outgoing" && (
{tx.state != "failed" && tx.feesPaidMsat > 0 && (
<TransactionDetailRow label="Fee">
<FormattedBitcoinAmount amountMsat={tx.feesPaidMsat} />
{tx.feesPaidMsat > 0 && (
{type == "outgoing" && (
<>
&nbsp;(
{((tx.feesPaidMsat / tx.amountMsat) * 100).toFixed(2)}%)

View file

@ -6,6 +6,7 @@ import { useInfo } from "src/hooks/useInfo";
import {
ArrowRightLeftIcon,
BoxIcon,
BugIcon,
CloudBackupIcon,
CodeIcon,
@ -162,6 +163,11 @@ export default function SettingsLayout() {
</NavGroup>
<NavGroup label="Advanced">
{info?.backendType === "LDK" && (
<MenuItem to="/settings/node" icon={BoxIcon}>
Node
</MenuItem>
)}
<MenuItem to="/settings/developer" icon={CodeIcon}>
Developer
</MenuItem>

View file

@ -62,7 +62,8 @@ export const useOnboardingData = (): UseOnboardingDataResponse => {
transactions.totalCount > 0 || balances.lightning.totalSpendableSat > 0;
const checklistItems: Omit<ChecklistItem, "disabled">[] = [
...(hasChannelManagement
...(hasChannelManagement &&
!(info.jitChannelsEnabled && info.jitChannelsLiquiditySource)
? [
{
title: "Open your first channel",

View file

@ -49,6 +49,7 @@ import Peers from "src/screens/peers/Peers";
import { About } from "src/screens/settings/About";
import { AlbyAccount } from "src/screens/settings/AlbyAccount";
import { AutoUnlock } from "src/screens/settings/AutoUnlock";
import { NodeSettings } from "src/screens/settings/NodeSettings";
import Backup from "src/screens/settings/Backup";
import { ChangeUnlockPassword } from "src/screens/settings/ChangeUnlockPassword";
import DebugTools from "src/screens/settings/DebugTools";
@ -253,6 +254,11 @@ const routes: RouteObject[] = [
element: <AutoUnlock />,
handle: { crumb: () => "Auto Unlock" },
},
{
path: "node",
element: <NodeSettings />,
handle: { crumb: () => "Node" },
},
{
path: "change-unlock-password",
element: <ChangeUnlockPassword />,

View file

@ -326,7 +326,10 @@ export default function Channels() {
{!!channels?.length && (
<>
{/* If all channels have less than 20% incoming capacity, show a warning */}
{channels?.every(
{!(
info?.jitChannelsEnabled && info?.jitChannelsLiquiditySource
) &&
channels?.every(
(channel) =>
channel.remoteBalanceMsat <
(channel.localBalanceMsat + channel.remoteBalanceMsat) * 0.2

View file

@ -1,13 +1,30 @@
import { ExternalLinkIcon } from "lucide-react";
import ExternalLink from "src/components/ExternalLink";
import Loading from "src/components/Loading";
import SettingsHeader from "src/components/SettingsHeader";
import { Badge } from "src/components/ui/badge";
import { useAlbyMe } from "src/hooks/useAlbyMe";
import { useNodeDetails } from "src/hooks/useNodeDetails";
import { useInfo } from "src/hooks/useInfo";
export function About() {
const { data: info } = useInfo();
const { data: albyMe, error: albyMeError } = useAlbyMe();
const lsps2Source = info?.jitChannelsLiquiditySource;
const lsps2Pubkey = lsps2Source?.includes("@")
? lsps2Source.split("@")[0]
: undefined;
const { data: lsps2NodeDetails } = useNodeDetails(lsps2Pubkey);
const lsps2Label =
lsps2NodeDetails?.alias ||
(lsps2Pubkey ? lsps2Pubkey.slice(0, 8) + "..." : lsps2Source);
const lsps2MinPaymentSizeSat = info?.jitChannelsMinPaymentSizeMsat
? Math.ceil(info.jitChannelsMinPaymentSizeMsat / 1000)
: undefined;
const lsps2MaxPaymentSizeSat = info?.jitChannelsMaxPaymentSizeMsat
? Math.floor(info.jitChannelsMaxPaymentSizeMsat / 1000)
: undefined;
if (!info || (info.albyAccountConnected && !albyMe && !albyMeError)) {
return <Loading />;
@ -56,6 +73,40 @@ export function About() {
</div>
</div>
)}
{info.jitChannelsLiquiditySource && (
<div className="grid gap-2">
<p className="font-medium text-sm">
Just-in-Time channels Liquidity Source LSPS2
</p>
<div className="flex flex-col gap-1 text-sm text-muted-foreground">
{lsps2Pubkey ? (
<ExternalLink
to={`${info.mempoolUrl}/lightning/node/${lsps2Pubkey}`}
className="inline-flex items-center gap-1 underline w-fit"
>
{lsps2Label}
<ExternalLinkIcon className="size-4" />
</ExternalLink>
) : (
<p>{lsps2Label}</p>
)}
<p className="break-all">{info.jitChannelsLiquiditySource}</p>
{(lsps2MinPaymentSizeSat || lsps2MaxPaymentSizeSat) && (
<p>
JIT payment size:{" "}
{lsps2MinPaymentSizeSat
? new Intl.NumberFormat().format(lsps2MinPaymentSizeSat)
: "?"}
{" - "}
{lsps2MaxPaymentSizeSat
? new Intl.NumberFormat().format(lsps2MaxPaymentSizeSat)
: "?"}{" "}
sats
</p>
)}
</div>
</div>
)}
<div className="grid gap-2">
<p className="font-medium text-sm">Nostr Relays</p>
{info.relays.map((relay) => (

View file

@ -0,0 +1,84 @@
import { toast } from "sonner";
import ExternalLink from "src/components/ExternalLink";
import Loading from "src/components/Loading";
import SettingsHeader from "src/components/SettingsHeader";
import { Checkbox } from "src/components/ui/checkbox";
import { Label } from "src/components/ui/label";
import { useInfo } from "src/hooks/useInfo";
import { handleRequestError } from "src/utils/handleRequestError";
import { request } from "src/utils/request";
export function NodeSettings() {
const { data: info, mutate: refetchInfo } = useInfo();
if (!info) {
return <Loading />;
}
if (info.backendType !== "LDK") {
return <p>Your Hub does not support this feature.</p>;
}
const hasJitSource = !!info.jitChannelsLiquiditySource;
async function setJitChannelsEnabled(enabled: boolean) {
try {
await request("/api/settings", {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ jitChannelsEnabled: enabled }),
});
await refetchInfo();
toast(enabled ? "JIT channels enabled" : "JIT channels disabled");
} catch (error) {
handleRequestError("Failed to update JIT channels setting", error);
}
}
return (
<>
<SettingsHeader
pageTitle="Node"
title="Node"
description="Configure your node's behavior"
/>
<div className="flex flex-col gap-4">
<div>
<p className="text-muted-foreground">
JIT (just-in-time) channels let you receive payments larger than
your current inbound capacity by automatically opening a new channel
through a liquidity provider. The provider's fee is deducted from
the incoming payment.{" "}
<ExternalLink
to="https://guides.getalby.com/user-guide/alby-hub/faq/what-are-just-in-time-channels"
className="underline"
>
Learn more
</ExternalLink>
</p>
</div>
<div className="flex items-center">
<Checkbox
id="jit-channels"
checked={info.jitChannelsEnabled}
disabled={!hasJitSource}
onCheckedChange={(checked) =>
setJitChannelsEnabled(checked === true)
}
/>
<Label htmlFor="jit-channels" className="ml-2 cursor-pointer">
Enable JIT channels for receiving
</Label>
</div>
{!hasJitSource && (
<p className="text-sm text-muted-foreground">
No JIT liquidity source is available for your network, so JIT
channels can't be used.
</p>
)}
</div>
</>
);
}

View file

@ -16,7 +16,7 @@ import { useChannels } from "src/hooks/useChannels";
import { useInfo } from "src/hooks/useInfo";
export default function Lightning() {
const { hasChannelManagement } = useInfo();
const { data: info, hasChannelManagement } = useInfo();
const { data: balances } = useBalances(true);
const { data: channels } = useChannels();
@ -37,7 +37,10 @@ export default function Lightning() {
balances.lightning.totalReceivableMsat <
balances.lightning.totalSpendableMsat * 0.1;
const showOpenFirstChannel =
hasChannelManagement && channels && !hasChannelsOpen;
hasChannelManagement &&
channels &&
!hasChannelsOpen &&
!(info?.jitChannelsEnabled && info?.jitChannelsLiquiditySource);
return (
<>

View file

@ -10,6 +10,7 @@ import React from "react";
import { toast } from "sonner";
import AppHeader from "src/components/AppHeader";
import { CurrencyInputField } from "src/components/CurrencyInputField";
import ExternalLink from "src/components/ExternalLink";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import FormattedFiatAmount from "src/components/FormattedFiatAmount";
import Loading from "src/components/Loading";
@ -30,6 +31,7 @@ import { Input } from "src/components/ui/input";
import { Label } from "src/components/ui/label";
import { useAlbyMe } from "src/hooks/useAlbyMe";
import { useBalances } from "src/hooks/useBalances";
import { useChannels } from "src/hooks/useChannels";
import { useInfo } from "src/hooks/useInfo";
import { useTransaction } from "src/hooks/useTransaction";
@ -42,6 +44,7 @@ export default function ReceiveInvoice() {
const { data: info, hasChannelManagement } = useInfo();
const { data: me } = useAlbyMe();
const { data: balances } = useBalances();
const { data: channels } = useChannels();
const [isLoading, setLoading] = React.useState(false);
const [amountSat, setAmountSat] = React.useState<string>("");
@ -49,17 +52,49 @@ export default function ReceiveInvoice() {
const [transaction, setTransaction] = React.useState<Transaction | null>(
null
);
const [paymentDone, setPaymentDone] = React.useState(false);
const { data: invoiceData } = useTransaction(
transaction ? transaction.paymentHash : "",
true
);
React.useEffect(() => {
if (invoiceData?.settledAt) {
setPaymentDone(true);
const paymentDone = !!invoiceData?.settledAt;
const jitChannelsEnabled = !!info?.jitChannelsEnabled;
const configuredLsps2Source = info?.jitChannelsLiquiditySource;
const lsps2Source = jitChannelsEnabled ? configuredLsps2Source : undefined;
const lsps2MinimumPaymentSizeSat = React.useMemo(() => {
if (jitChannelsEnabled && info?.jitChannelsMinPaymentSizeMsat) {
return Math.ceil(info.jitChannelsMinPaymentSizeMsat / 1000);
}
}, [invoiceData]);
return undefined;
}, [info?.jitChannelsMinPaymentSizeMsat, jitChannelsEnabled]);
// only enforce the minimum on the input when the user has no channels yet -
// their first channel must meet the minimum size.
const jitMinimumReceiveSat = channels?.length
? undefined
: lsps2MinimumPaymentSizeSat;
const lsps2MaximumPaymentSizeSat = React.useMemo(() => {
if (jitChannelsEnabled && info?.jitChannelsMaxPaymentSizeMsat) {
return Math.floor(info.jitChannelsMaxPaymentSizeMsat / 1000);
}
return undefined;
}, [info?.jitChannelsMaxPaymentSizeMsat, jitChannelsEnabled]);
const jitMaximumReceiveSat =
hasChannelManagement && lsps2Source
? lsps2MaximumPaymentSizeSat
: !lsps2Source && hasChannelManagement
? balances?.lightning.totalReceivableSat
: undefined;
const totalReceivableMsat = balances?.lightning.totalReceivableMsat ?? 0;
const requestedAmountMsat = +amountSat * 1000 || transaction?.amountMsat || 0;
const isNearReceivingCapacity =
!!hasChannelManagement && requestedAmountMsat >= 0.8 * totalReceivableMsat;
const isJitReceiveInvoice =
!!hasChannelManagement &&
!!lsps2Source &&
!!transaction &&
transaction.amountMsat > totalReceivableMsat;
const displayedJitFeeMsat = paymentDone
? (invoiceData?.feesPaidMsat ?? 0)
: (transaction?.feesPaidMsat ?? 0);
if (!balances || !info || (info.albyAccountConnected && !me)) {
return <Loading />;
@ -88,8 +123,25 @@ export default function ReceiveInvoice() {
toast("Successfully created invoice");
}
} catch (e) {
const requestedAmountSat = parseInt(amountSat) || 0;
// the user already has channels but this amount exceeds their receiving
// capacity (so a new channel is needed) and is below the LSP's minimum
// channel size - the receive may have failed because the amount was too
// small to open a second channel, so add a hint alongside the error.
const likelyTooSmallForNewChannel =
jitChannelsEnabled &&
!!channels?.length &&
!!lsps2MinimumPaymentSizeSat &&
requestedAmountSat < lsps2MinimumPaymentSizeSat &&
requestedAmountSat * 1000 > totalReceivableMsat;
let description = "" + e;
if (likelyTooSmallForNewChannel) {
description += `\n\nThis amount is over your receiving capacity and may be too small to open a new Lightning channel. Try receiving at least ${new Intl.NumberFormat().format(
lsps2MinimumPaymentSizeSat as number
)} sats, or lower the amount to fit your current capacity.`;
}
toast.error("Failed to create invoice", {
description: "" + e,
description,
});
console.error(e);
} finally {
@ -101,6 +153,19 @@ export default function ReceiveInvoice() {
copyToClipboard(transaction?.invoice as string);
};
const newChannelFeeAlert = (
<p className="text-sm text-muted-foreground text-center">
Includes a <FormattedBitcoinAmount amountMsat={displayedJitFeeMsat} />{" "}
channel fee.{" "}
<ExternalLink
to="https://guides.getalby.com/user-guide/alby-hub/faq/what-are-just-in-time-channels"
className="underline"
>
Learn more
</ExternalLink>
</p>
);
return (
<div className="grid gap-5">
<AppHeader
@ -109,9 +174,7 @@ export default function ReceiveInvoice() {
/>
<div className="flex flex-col md:flex-row gap-12">
<div className="w-full md:max-w-lg grid gap-6">
{hasChannelManagement &&
(+amountSat * 1000 || transaction?.amountMsat || 0) >=
0.8 * balances.lightning.totalReceivableMsat && (
{!lsps2Source && !transaction && isNearReceivingCapacity && (
<LowReceivingCapacityAlert />
)}
<div>
@ -138,6 +201,9 @@ export default function ReceiveInvoice() {
className="text-xl"
/>
</div>
{isJitReceiveInvoice && displayedJitFeeMsat >= 1000 && (
<div className="w-full">{newChannelFeeAlert}</div>
)}
</CardContent>
<CardFooter className="flex flex-col gap-2">
<Button
@ -174,7 +240,6 @@ export default function ReceiveInvoice() {
<CardFooter className="flex flex-col gap-2 pt-2">
<Button
onClick={() => {
setPaymentDone(false);
setTransaction(null);
}}
variant="outline"
@ -201,19 +266,42 @@ export default function ReceiveInvoice() {
id="amount"
valueSat={amountSat}
onValueSatChange={setAmountSat}
minSat={1}
maxSat={
hasChannelManagement
? balances.lightning.totalReceivableSat
: undefined
minSat={jitMinimumReceiveSat ?? 1}
onInvalid={(e) => {
if (
jitMinimumReceiveSat &&
e.currentTarget.validity.rangeUnderflow
) {
e.currentTarget.setCustomValidity(
`You need to receive at least ${new Intl.NumberFormat().format(
jitMinimumReceiveSat
)} sats to open your first lightning channel`
);
} else if (
jitMaximumReceiveSat &&
e.currentTarget.validity.rangeOverflow
) {
e.currentTarget.setCustomValidity(
lsps2Source
? `This JIT channel setup supports receiving at most ${new Intl.NumberFormat().format(
jitMaximumReceiveSat
)} sats in a single payment`
: `You can receive at most ${new Intl.NumberFormat().format(
jitMaximumReceiveSat
)} sats with your current capacity`
);
} else {
e.currentTarget.setCustomValidity("");
}
}}
maxSat={jitMaximumReceiveSat}
autoFocus
contextRows={
hasChannelManagement
hasChannelManagement && !lsps2Source && jitMaximumReceiveSat
? [
{
label: "Receive limit",
amountSat: balances.lightning.totalReceivableSat,
amountSat: jitMaximumReceiveSat,
},
]
: undefined

View file

@ -179,6 +179,10 @@ export interface InfoResponse {
bitcoinDisplayFormat: BitcoinDisplayFormat;
chainDataSourceType?: string;
chainDataSourceAddress?: string;
jitChannelsLiquiditySource?: string;
jitChannelsMinPaymentSizeMsat?: number;
jitChannelsMaxPaymentSizeMsat?: number;
jitChannelsEnabled: boolean;
hideUpdateBanner: boolean;
supportsBolt12: boolean;
}
@ -475,7 +479,7 @@ export type SetupNodeInfo = Partial<{
clnAddressHold?: string;
}>;
export type LSPType = "LSPS1";
export type LSPType = "LSPS1" | "LSPS2";
export type LSPChannelOfferPaymentMethod =
| "card"
@ -509,9 +513,8 @@ export type RecommendedChannelPeer = {
pubkey: string;
host: string;
}
| {
| ({
paymentMethod: "lightning";
type: LSPType;
identifier: string;
contactUrl: string;
terms?: string;
@ -520,7 +523,10 @@ export type RecommendedChannelPeer = {
feeTotalSat1m?: number;
feeTotalSat2m?: number;
feeTotalSat3m?: number;
}
} & (
| { type: "LSPS1" }
| { type: "LSPS2"; nodeAddress: string } // nodeid@ip:port
))
);
export type AlbyInfo = {

2
go.mod
View file

@ -8,7 +8,7 @@ require (
github.com/btcsuite/btcd/btcutil v1.2.0
github.com/elnosh/gonuts v0.4.2
github.com/getAlby/go-nostr v0.0.0-20260513161014-22fb7840c7a4
github.com/getAlby/ldk-node-go v0.0.0-20260424111754-3690cdb3031c
github.com/getAlby/ldk-node-go v0.0.0-20260608130949-5ba22268f000
github.com/go-gormigrate/gormigrate/v2 v2.1.6
github.com/google/uuid v1.6.0
github.com/labstack/echo/v4 v4.15.2

4
go.sum
View file

@ -172,8 +172,8 @@ github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/getAlby/go-nostr v0.0.0-20260513161014-22fb7840c7a4 h1:Z93wPXKIMY4Emr+zDz0R0NrSg1FEVGrzcqqomuSrmko=
github.com/getAlby/go-nostr v0.0.0-20260513161014-22fb7840c7a4/go.mod h1:BtlkV9evCTjpY0YeFhoNgycp7XNFbnfVXJPoykp+NtM=
github.com/getAlby/ldk-node-go v0.0.0-20260424111754-3690cdb3031c h1:ikai5+taiPSgbaocdVMdxPkmOhnXs5V/gCX+J/P8iRw=
github.com/getAlby/ldk-node-go v0.0.0-20260424111754-3690cdb3031c/go.mod h1:8BRjtKcz8E0RyYTPEbMS8VIdgredcGSLne8vHDtcRLg=
github.com/getAlby/ldk-node-go v0.0.0-20260608130949-5ba22268f000 h1:rlW59HX0myVvttj1VKiyNPNP564ecEDlUHRxC2dIDYs=
github.com/getAlby/ldk-node-go v0.0.0-20260608130949-5ba22268f000/go.mod h1:8BRjtKcz8E0RyYTPEbMS8VIdgredcGSLne8vHDtcRLg=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gormigrate/gormigrate/v2 v2.1.6 h1:VtX+l1Stj2v5RGubVQk0LS/8EPGXR+ldcOyCmlmKoyg=
github.com/go-gormigrate/gormigrate/v2 v2.1.6/go.mod h1:PZpedQc4tWaxn6kvXicwhinh3L0seLpMc5ReKRX5id4=

View file

@ -28,6 +28,7 @@ import (
decodepay "github.com/nbd-wtf/ln-decodepay"
"github.com/sirupsen/logrus"
"github.com/getAlby/hub/alby"
"github.com/getAlby/hub/config"
"github.com/getAlby/hub/events"
"github.com/getAlby/hub/lnclient"
@ -54,13 +55,20 @@ type LDKService struct {
lastWalletSyncRequest time.Time
redeemedOnchainFundsWithinThisSync bool
pubkey string
lsps2Pubkey string
lsps2Address string
lsps2InfoMu sync.Mutex
lsps2InfoFetchedAt time.Time
lsps2MinPaymentSizeMsat *uint64
lsps2MaxPaymentSizeMsat *uint64
shuttingDown bool
}
const resetRouterKey = "ResetRouter"
const maxInvoiceExpiry = 24 * time.Hour
const lsps2InfoCacheTTL = 60 * time.Minute
func NewLDKService(ctx context.Context, cfg config.Config, eventPublisher events.EventPublisher, mnemonic, workDir string, vssToken string, setStartupState func(startupState string)) (result lnclient.LNClient, err error) {
func NewLDKService(ctx context.Context, cfg config.Config, eventPublisher events.EventPublisher, mnemonic, workDir string, vssToken string, setStartupState func(startupState string), channelPeerSuggestions []alby.ChannelPeerSuggestion) (result lnclient.LNClient, err error) {
if mnemonic == "" || workDir == "" {
return nil, errors.New("one or more required LDK configuration are missing")
}
@ -143,7 +151,40 @@ func NewLDKService(ctx context.Context, cfg config.Config, eventPublisher events
builder.SetNodeAlias(alias)
builder.SetEntropyBip39Mnemonic(mnemonic, nil)
liquiditySourceLsps2 := cfg.GetEnv().LDKLiquiditySourceLsps2
network := cfg.GetNetwork()
// if no explicit override, try a matching LSPS2 suggestion for this network
if liquiditySourceLsps2 == "" {
for _, suggestion := range channelPeerSuggestions {
if suggestion.PaymentMethod == "lightning" &&
suggestion.Type == lsp.LSP_TYPE_LSPS2 &&
suggestion.Network == network &&
suggestion.NodeAddress != "" {
liquiditySourceLsps2 = suggestion.NodeAddress
break
}
}
}
// fall back to a hardcoded per-network default
if liquiditySourceLsps2 == "" {
switch network {
case "signet":
// Alby LSP (Mutinynet)
liquiditySourceLsps2 = "025010bd608771bc13f08f696e3dd226bf3a9ae6ea461e3922ed9bdca7bb0edfe5@141.95.84.44:9735"
case "bitcoin":
// Megalith LSP 2
liquiditySourceLsps2 = "034066e29e402d9cf55af1ae1026cc5adf92eed1e0e421785442f53717ad1453b0@64.23.159.177:9735"
}
}
lsps2Pubkey, lsps2Address := parseLiquiditySourceLsps2(liquiditySourceLsps2)
if lsps2Pubkey != "" {
builder.SetLiquiditySourceLsps2(lsps2Pubkey, lsps2Address, nil)
}
switch network {
case "signet":
builder.SetNetwork(ldk_node.NetworkSignet)
@ -251,6 +292,8 @@ func NewLDKService(ctx context.Context, cfg config.Config, eventPublisher events
eventPublisher: eventPublisher,
cfg: cfg,
pubkey: nodeId,
lsps2Pubkey: lsps2Pubkey,
lsps2Address: lsps2Address,
ctx: ldkCtx,
}
@ -691,6 +734,16 @@ func (ls *LDKService) getMaxReceivable() int64 {
return int64(receivable)
}
func (ls *LDKService) hasPublicChannel() bool {
channels := ls.node.ListChannels()
for _, channel := range channels {
if channel.IsAnnounced {
return true
}
}
return false
}
func (ls *LDKService) getMaxSpendable() uint64 {
var spendable uint64 = 0
channels := ls.node.ListChannels()
@ -710,7 +763,15 @@ func (ls *LDKService) MakeInvoice(ctx context.Context, amountMsat int64, descrip
maxReceivable := ls.getMaxReceivable()
if amountMsat > maxReceivable {
jitChannelsEnabled, _ := ls.cfg.Get("JitChannelsEnabled", "")
// JIT channels are only used for users without a public channel - users with
// a public channel should increase inbound liquidity manually.
isJitInvoice := ls.lsps2Pubkey != "" &&
jitChannelsEnabled != "false" &&
!ls.hasPublicChannel() &&
amountMsat > maxReceivable
if amountMsat > maxReceivable && !isJitInvoice {
ls.eventPublisher.Publish(&events.Event{
Event: "nwc_incoming_liquidity_required",
Properties: map[string]interface{}{
@ -736,9 +797,21 @@ func (ls *LDKService) MakeInvoice(ctx context.Context, amountMsat int64, descrip
}
}
invoiceObj, err := ls.node.Bolt11Payment().Receive(uint64(amountMsat),
var invoiceObj *ldk_node.Bolt11Invoice
if isJitInvoice {
invoiceObj, err = ls.node.Bolt11Payment().ReceiveViaJitChannel(
uint64(amountMsat),
descriptionType,
uint32(expiry))
uint32(expiry),
nil,
)
} else {
invoiceObj, err = ls.node.Bolt11Payment().Receive(
uint64(amountMsat),
descriptionType,
uint32(expiry),
)
}
if err != nil {
logger.Logger.WithError(err).Error("MakeInvoice failed")
@ -746,7 +819,7 @@ func (ls *LDKService) MakeInvoice(ctx context.Context, amountMsat int64, descrip
}
payment := ls.node.Payment(invoiceObj.PaymentHash())
invoice := *payment.Kind.(ldk_node.PaymentKindBolt11).Bolt11Invoice
invoice := invoiceObj.String()
paymentRequest, err := decodepay.Decodepay(invoice)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
@ -757,12 +830,33 @@ func (ls *LDKService) MakeInvoice(ctx context.Context, amountMsat int64, descrip
}
expiresAtUnix := time.UnixMilli(int64(paymentRequest.CreatedAt) * 1000).Add(time.Duration(paymentRequest.Expiry) * time.Second).Unix()
preimage := ""
estimatedLspFeeMsat := int64(0)
if payment != nil {
switch kind := payment.Kind.(type) {
case ldk_node.PaymentKindBolt11:
if kind.Preimage != nil {
preimage = *kind.Preimage
}
case ldk_node.PaymentKindBolt11Jit:
if kind.Preimage != nil {
preimage = *kind.Preimage
}
if kind.LspFeeLimits.MaxTotalOpeningFeeMsat != nil {
estimatedLspFeeMsat = int64(*kind.LspFeeLimits.MaxTotalOpeningFeeMsat)
} else if kind.LspFeeLimits.MaxProportionalOpeningFeePpmMsat != nil && amountMsat > 0 {
estimatedLspFeeMsat = int64((uint64(amountMsat) * *kind.LspFeeLimits.MaxProportionalOpeningFeePpmMsat) / 1_000_000)
}
}
}
transaction = &lnclient.Transaction{
Type: "incoming",
Invoice: invoice,
PaymentHash: paymentRequest.PaymentHash,
Preimage: *payment.Kind.(ldk_node.PaymentKindBolt11).Preimage,
Preimage: preimage,
AmountMsat: amountMsat,
FeesPaidMsat: estimatedLspFeeMsat,
CreatedAt: int64(paymentRequest.CreatedAt),
ExpiresAt: &expiresAtUnix,
Description: paymentRequest.Description,
@ -1377,6 +1471,20 @@ func (ls *LDKService) ldkPaymentToTransaction(payment *ldk_node.PaymentDetails)
paymentHash = bolt11PaymentKind.Hash
}
bolt11JitPaymentKind, isBolt11JitPaymentKind := payment.Kind.(ldk_node.PaymentKindBolt11Jit)
if isBolt11JitPaymentKind {
createdAt = int64(payment.CreatedAt)
if payment.CreatedAt == 0 {
createdAt = int64(payment.LatestUpdateTimestamp)
}
if payment.Status == ldk_node.PaymentStatusSucceeded && bolt11JitPaymentKind.Preimage != nil {
preimage = *bolt11JitPaymentKind.Preimage
lastUpdate := int64(payment.LatestUpdateTimestamp)
settledAt = &lastUpdate
}
paymentHash = bolt11JitPaymentKind.Hash
}
bolt12PaymentKind, isBolt12PaymentKind := payment.Kind.(ldk_node.PaymentKindBolt12Offer)
if isBolt12PaymentKind {
@ -1442,6 +1550,9 @@ func (ls *LDKService) ldkPaymentToTransaction(payment *ldk_node.PaymentDetails)
if payment.FeePaidMsat != nil {
feeMsat = *payment.FeePaidMsat
}
if isBolt11JitPaymentKind && bolt11JitPaymentKind.CounterpartySkimmedFeeMsat != nil {
feeMsat = *bolt11JitPaymentKind.CounterpartySkimmedFeeMsat
}
return &lnclient.Transaction{
Type: transactionType,
@ -1535,15 +1646,22 @@ func (ls *LDKService) handleLdkEvent(event *ldk_node.Event) {
return
}
isTrusted := eventType.CounterpartyNodeId != nil && slices.Contains(ls.node.Config().AnchorChannelsConfig.TrustedPeersNoReserve, *eventType.CounterpartyNodeId)
channel := channels[channelIndex]
// assume it's a JIT channel if the channel peer matches
// checking outbound capacity doesn't work (outbound capacity can be initially 0)
isJit := !channel.IsOutbound && ls.lsps2Pubkey != "" && *eventType.CounterpartyNodeId == ls.lsps2Pubkey
isTrusted := eventType.CounterpartyNodeId != nil &&
(slices.Contains(ls.node.Config().AnchorChannelsConfig.TrustedPeersNoReserve, *eventType.CounterpartyNodeId) || isJit)
ls.eventPublisher.Publish(&events.Event{
Event: "nwc_channel_ready",
Properties: map[string]interface{}{
"counterparty_node_id": eventType.CounterpartyNodeId,
"node_type": config.LDKBackendType,
"public": channel.IsAnnounced,
"jit": isJit,
"capacity": channel.ChannelValueSats,
"is_outbound": channel.IsOutbound,
"trusted": isTrusted,
@ -1904,6 +2022,8 @@ func (ls *LDKService) deleteOldLDKPayments() {
switch (payment.Kind).(type) {
case ldk_node.PaymentKindBolt11:
deletablePaymentKind = true
case ldk_node.PaymentKindBolt11Jit:
deletablePaymentKind = true
case ldk_node.PaymentKindSpontaneous:
deletablePaymentKind = true
}
@ -2503,6 +2623,95 @@ func (ls *LDKService) GetChainDataSource() (string, string) {
return "esplora", sanitizeChainEndpoint(endpoint, "")
}
func (ls *LDKService) GetLiquiditySourceLsps2() string {
if ls.lsps2Pubkey == "" || ls.lsps2Address == "" {
return ""
}
return fmt.Sprintf("%s@%s", ls.lsps2Pubkey, ls.lsps2Address)
}
func (ls *LDKService) GetLiquiditySourceLsps2MinPaymentSizeMsat() *uint64 {
ls.fetchLsps2OpeningFeeParams()
return ls.lsps2MinPaymentSizeMsat
}
func (ls *LDKService) GetLiquiditySourceLsps2MaxPaymentSizeMsat() *uint64 {
ls.fetchLsps2OpeningFeeParams()
return ls.lsps2MaxPaymentSizeMsat
}
func (ls *LDKService) fetchLsps2OpeningFeeParams() {
if ls.lsps2Pubkey == "" || ls.lsps2Address == "" {
return
}
ls.lsps2InfoMu.Lock()
defer ls.lsps2InfoMu.Unlock()
if !ls.lsps2InfoFetchedAt.IsZero() && time.Since(ls.lsps2InfoFetchedAt) < lsps2InfoCacheTTL {
return
}
response, err := ls.node.Lsps2Liquidity().RequestOpeningFeeParams()
if err != nil {
logger.Logger.WithError(err).Warn("Failed to fetch LSPS2 opening fee params")
return
}
var minPaymentSizeMsat *uint64
var maxPaymentSizeMsat *uint64
for _, params := range response.OpeningFeeParamsMenu {
effectiveMinPaymentSizeMsat, ok := computeLsps2MinPaymentSizeMsat(params)
if !ok {
continue
}
if minPaymentSizeMsat == nil || effectiveMinPaymentSizeMsat < *minPaymentSizeMsat {
value := effectiveMinPaymentSizeMsat
minPaymentSizeMsat = &value
}
if maxPaymentSizeMsat == nil || params.MaxPaymentSizeMsat > *maxPaymentSizeMsat {
value := params.MaxPaymentSizeMsat
maxPaymentSizeMsat = &value
}
}
ls.lsps2MinPaymentSizeMsat = minPaymentSizeMsat
ls.lsps2MaxPaymentSizeMsat = maxPaymentSizeMsat
ls.lsps2InfoFetchedAt = time.Now()
}
// finds the smallest incoming payment for which the user is left
// with a usable amount after the LSP skims its LSPS2 opening fee.
func computeLsps2MinPaymentSizeMsat(params ldk_node.Lsps2OpeningFeeParams) (uint64, bool) {
// The smallest amount the user must net after the opening fee. We require a
// whole satoshi rather than a single millisat so the minimum payment size
// represents a usable receive.
const minNetReceiveMsat = 1000
paymentSizeMsat := params.MinPaymentSizeMsat
for range 8 {
openingFeeMsat := ldk_node.Lsps2ComputeOpeningFeeMsat(paymentSizeMsat, params)
if openingFeeMsat == nil {
return 0, false
}
// The incoming amount must exceed the opening fee by at least 1 sat,
// otherwise the user receives a sub-satoshi (effectively zero) amount
// after the LSP skims its fee.
if *openingFeeMsat+minNetReceiveMsat <= paymentSizeMsat {
return paymentSizeMsat, paymentSizeMsat <= params.MaxPaymentSizeMsat
}
nextPaymentSizeMsat := *openingFeeMsat + minNetReceiveMsat
if nextPaymentSizeMsat <= paymentSizeMsat || nextPaymentSizeMsat > params.MaxPaymentSizeMsat {
return 0, false
}
paymentSizeMsat = nextPaymentSizeMsat
}
return 0, false
}
func sanitizeChainEndpoint(endpoint string, port string) string {
u, err := url.Parse(endpoint)
if err != nil || u.Host == "" {
@ -2536,3 +2745,23 @@ func sanitizeChainEndpoint(endpoint string, port string) string {
return sanitized
}
func parseLiquiditySourceLsps2(lsps2Address string) (pubkey string, address string) {
entry := strings.TrimSpace(lsps2Address)
if entry == "" {
return "", ""
}
pubkey, address, hasSeparator := strings.Cut(entry, "@")
if !hasSeparator || pubkey == "" || address == "" {
logger.Logger.WithField("entry", entry).Warn("Invalid LDK_LSPS2_ADDRESS, expected <pubkey>@<host>:<port>")
return "", ""
}
if _, _, err := net.SplitHostPort(address); err != nil {
logger.Logger.WithField("entry", entry).WithError(err).Warn("Invalid LDK_LSPS2_ADDRESS host:port")
return "", ""
}
return pubkey, address
}

View file

@ -6,6 +6,7 @@ type LSP struct {
const (
LSP_TYPE_LSPS1 = "LSPS1"
LSP_TYPE_LSPS2 = "LSPS2"
)
func OlympusMutinynetLSP() LSP {

View file

@ -10,7 +10,7 @@ import (
)
type makeInvoiceParams struct {
Amount uint64 `json:"amount"`
Amount uint64 `json:"amount"` // msats (NIP-47)
Description string `json:"description"`
DescriptionHash string `json:"description_hash"`
Expiry uint64 `json:"expiry"`

View file

@ -362,7 +362,12 @@ func (svc *service) launchLNBackend(ctx context.Context, encryptionKey string) e
setStartupState := func(startupState string) {
svc.startupState = startupState
}
lnClient, err = ldk.NewLDKService(ctx, svc.cfg, svc.eventPublisher, mnemonic, ldkWorkdir, vssToken, setStartupState)
channelPeerSuggestions, suggestionsErr := svc.albySvc.GetChannelPeerSuggestions(ctx)
if suggestionsErr != nil {
logger.Logger.WithError(suggestionsErr).Warn("Failed to fetch channel peer suggestions for LSPS2 liquidity source")
}
lnClient, err = ldk.NewLDKService(ctx, svc.cfg, svc.eventPublisher, mnemonic, ldkWorkdir, vssToken, setStartupState, channelPeerSuggestions)
case config.PhoenixBackendType:
PhoenixdAddress, _ := svc.cfg.Get("PhoenixdAddress", encryptionKey)
PhoenixdAuthorization, _ := svc.cfg.Get("PhoenixdAuthorization", encryptionKey)

View file

@ -205,6 +205,7 @@ func (svc *transactionsService) MakeInvoice(ctx context.Context, amountMsat uint
Type: lnClientTransaction.Type,
State: constants.TRANSACTION_STATE_PENDING,
AmountMsat: uint64(lnClientTransaction.AmountMsat),
FeeMsat: uint64(max(lnClientTransaction.FeesPaidMsat, 0)),
Description: description,
DescriptionHash: descriptionHash,
PaymentRequest: lnClientTransaction.Invoice,