ldk.go raw
1 package ldk
2
3 import (
4 "context"
5 "crypto/sha256"
6 "encoding/hex"
7 "encoding/json"
8 "errors"
9 "fmt"
10 "math"
11 "net"
12 "net/http"
13 "net/url"
14 "os"
15 "path/filepath"
16 "slices"
17 "sort"
18 "strconv"
19 "strings"
20 "sync"
21 "time"
22
23 "github.com/getAlby/ldk-node-go/ldk_node"
24 "github.com/tyler-smith/go-bip32"
25
26 // "github.com/getAlby/hub/ldk_node"
27
28 decodepay "github.com/nbd-wtf/ln-decodepay"
29 "github.com/sirupsen/logrus"
30
31 "github.com/getAlby/hub/alby"
32 "github.com/getAlby/hub/config"
33 "github.com/getAlby/hub/events"
34 "github.com/getAlby/hub/lnclient"
35 "github.com/getAlby/hub/logger"
36 "github.com/getAlby/hub/lsp"
37 "github.com/getAlby/hub/nip47/models"
38 "github.com/getAlby/hub/nip47/notifications"
39 "github.com/getAlby/hub/service/keys"
40 "github.com/getAlby/hub/transactions"
41 )
42
43 type LDKService struct {
44 workdir string
45 node *ldk_node.Node
46 ldkEventBroadcaster LDKEventBroadcaster
47 cancel context.CancelFunc
48 ctx context.Context
49 network string
50 eventPublisher events.EventPublisher
51 syncing bool
52 lastFullSync time.Time
53 lastFeeEstimatesSync time.Time
54 cfg config.Config
55 lastWalletSyncRequest time.Time
56 redeemedOnchainFundsWithinThisSync bool
57 pubkey string
58 lsps2Pubkey string
59 lsps2Address string
60 lsps2InfoMu sync.Mutex
61 lsps2InfoFetchedAt time.Time
62 lsps2MinPaymentSizeMsat *uint64
63 lsps2MaxPaymentSizeMsat *uint64
64 lsps2OpeningFeeParamsMenu []ldk_node.Lsps2OpeningFeeParams
65 shuttingDown bool
66 eventHandlingMutex sync.Mutex
67 }
68
69 const resetRouterKey = "ResetRouter"
70 const maxInvoiceExpiry = 24 * time.Hour
71 const lsps2InfoCacheTTL = 60 * time.Minute
72
73 // cached opening fee params must be at most this old when used to derive the
74 // maximum LSP fee for a new JIT channel invoice
75 const lsps2FeeCapCacheTTL = 1 * time.Minute
76
77 // absolute ceiling on the LSPS2 opening fee accepted for a JIT channel,
78 // regardless of the fee menu the LSP advertises: the greater of a base amount
79 // and a percentage of the payment, so small payments can absorb the fixed
80 // cost of a channel open while larger payments cannot be overcharged.
81 const lsps2MaxOpeningFeeBaseMsat = 5_000_000
82 const lsps2MaxOpeningFeePercent = 10
83
84 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) {
85 if mnemonic == "" || workDir == "" {
86 return nil, errors.New("one or more required LDK configuration are missing")
87 }
88
89 setStartupState("Configuring node")
90
91 // create dir if not exists
92 newpath := filepath.Join(workDir)
93 err = os.MkdirAll(newpath, os.ModePerm)
94 if err != nil {
95 logger.Logger.WithError(err).Error("Failed to create LDK working dir")
96 return nil, err
97 }
98
99 ldkConfig := ldk_node.DefaultConfig()
100
101 ldkConfig.TrustedPeers0conf = []string{
102 lsp.OlympusLSP().Pubkey,
103 lsp.MegalithLSP().Pubkey,
104 "02b4552a7a85274e4da01a7c71ca57407181752e8568b31d51f13c111a2941dce3", // LNServer_Wave
105 "038ba8f67ba8ff5c48764cdd3251c33598d55b203546d08a8f0ec9dcd9f27e3637", // Flashsats
106 "03a5c38d0dfd2dd1ebe679c308788c468a0186720bb33973b1c56710ffe6696c08", // BHODL
107
108 // Mutinynet
109 lsp.OlympusMutinynetLSP().Pubkey,
110 lsp.MegalithMutinynetLSP().Pubkey,
111 "03f726f240f0391448fb31c33e130ecc9708c9137e1f4e77b5d17d5dec74b0dd1e", // flashsats
112 }
113
114 // rather than fully trusting our LSPs, we set the channel reserve to 0.
115 // this allows us to receive incoming channels without any on-chain balance
116 // but if the user has 0 on-chain balance when the channel is closed,
117 // we rely on the counterparty to bump the transaction.
118 // It's also possible in rare situations the counterparty can take
119 // funds if the channel was closed due to a stuck HTLC.
120 // Therefore, the user SHOULD add some on-chain funds to prevent this.
121 ldkConfig.AnchorChannelsConfig.PerChannelReserveSats = 0
122 ldkConfig.AnchorChannelsConfig.TrustedPeersNoReserve = []string{
123 /*lsp.OlympusLSP().Pubkey,
124 lsp.AlbyPlebsLSP().Pubkey,
125 lsp.MegalithLSP().Pubkey,
126 "02b4552a7a85274e4da01a7c71ca57407181752e8568b31d51f13c111a2941dce3", // LNServer_Wave
127 "0296b2db342fcf87ea94d981757fdf4d3e545bd5cef4919f58b5d38dfdd73bf5c9", // blocktank
128 "038ba8f67ba8ff5c48764cdd3251c33598d55b203546d08a8f0ec9dcd9f27e3637", // flashsats
129 "0370a5392cd7c81ff5128fa656ee6db0c4d11c778fcd6cb98cb6ba3b48394f5705", // lqwd
130
131 // Mutinynet
132 lsp.AlbyMutinynetPlebsLSP().Pubkey,
133 lsp.OlympusMutinynetLSP().Pubkey,
134 lsp.MegalithMutinynetLSP().Pubkey,
135 "0296820bbba5bd33719962bafd69996ee89e03ce7164d8f368cbb85463f5f47876", // flashsats
136 "035e8a9034a8c68f219aacadae748c7a3cd719109309db39b09886e5ff17696b1b", // lqwd*/
137 }
138
139 listeningAddresses := strings.Split(cfg.GetEnv().LDKListeningAddresses, ",")
140 ldkConfig.ListeningAddresses = &listeningAddresses
141 if cfg.GetEnv().LDKAnnouncementAddresses != "" {
142 announcementAddresses := strings.Split(cfg.GetEnv().LDKAnnouncementAddresses, ",")
143 ldkConfig.AnnouncementAddresses = &announcementAddresses
144 }
145
146 logLevel, err := strconv.Atoi(cfg.GetEnv().LDKLogLevel)
147 if err != nil {
148 // If parsing log level fails we default to info log level
149 logLevel = int(logrus.InfoLevel)
150 }
151
152 ldkLogger, err := NewLDKLogger(logrus.Level(logLevel), cfg.GetEnv().LogToFile, workDir)
153 if err != nil {
154 return nil, err
155 }
156 ldkConfig.TransientNetworkGraph = cfg.GetEnv().LDKTransientNetworkGraph
157
158 alias, _ := cfg.Get("NodeAlias", "")
159 if alias == "" {
160 alias = "Alby Hub"
161 }
162
163 builder := ldk_node.BuilderFromConfig(ldkConfig)
164 builder.SetCustomLogger(ldkLogger)
165 builder.SetNodeAlias(alias)
166 builder.SetEntropyBip39Mnemonic(mnemonic, nil)
167
168 liquiditySourceLsps2 := cfg.GetEnv().LDKLiquiditySourceLsps2
169 network := cfg.GetNetwork()
170
171 // if no explicit override, try a matching LSPS2 suggestion for this network
172 if liquiditySourceLsps2 == "" {
173 for _, suggestion := range channelPeerSuggestions {
174 if suggestion.PaymentMethod == "lightning" &&
175 suggestion.Type == lsp.LSP_TYPE_LSPS2 &&
176 suggestion.Network == network &&
177 suggestion.NodeAddress != "" {
178 liquiditySourceLsps2 = suggestion.NodeAddress
179 break
180 }
181 }
182 }
183
184 // fall back to a hardcoded per-network default
185 if liquiditySourceLsps2 == "" {
186 switch network {
187 case "signet":
188 // Megalith LSP 2 (Mutinynet)
189 liquiditySourceLsps2 = "03e30fda71887a916ef5548a4d02b06fe04aaa1a8de9e24134ce7f139cf79d7579@64.23.192.68:9736"
190 case "bitcoin":
191 // Megalith LSP 2
192 liquiditySourceLsps2 = "034066e29e402d9cf55af1ae1026cc5adf92eed1e0e421785442f53717ad1453b0@64.23.159.177:9735"
193 }
194 }
195
196 lsps2Pubkey, lsps2Address := parseLiquiditySourceLsps2(liquiditySourceLsps2)
197
198 if lsps2Pubkey != "" {
199 builder.SetLiquiditySourceLsps2(lsps2Pubkey, lsps2Address, nil)
200 }
201
202 switch network {
203 case "signet":
204 builder.SetNetwork(ldk_node.NetworkSignet)
205 case "regtest":
206 builder.SetNetwork(ldk_node.NetworkRegtest)
207 case "testnet":
208 builder.SetNetwork(ldk_node.NetworkSignet)
209 default:
210 builder.SetNetwork(ldk_node.NetworkBitcoin)
211 }
212
213 var chainSource string
214 if cfg.GetEnv().LDKBitcoindRpcHost != "" {
215 logger.Logger.WithFields(logrus.Fields{
216 "rpc_host": cfg.GetEnv().LDKBitcoindRpcHost,
217 "rpc_port": cfg.GetEnv().LDKBitcoindRpcPort,
218 }).Info("Using LDK node bitcoin RPC chain source")
219 port, err := strconv.ParseUint(cfg.GetEnv().LDKBitcoindRpcPort, 10, 16)
220 if err != nil {
221 return nil, err
222 }
223 builder.SetChainSourceBitcoindRpc(cfg.GetEnv().LDKBitcoindRpcHost, uint16(port), cfg.GetEnv().LDKBitcoindRpcUser, cfg.GetEnv().LDKBitcoindRpcPassword)
224 chainSource = "bitcoind"
225 } else if cfg.GetEnv().LDKElectrumServer != "" {
226 builder.SetChainSourceElectrum(cfg.GetEnv().LDKElectrumServer, &ldk_node.ElectrumSyncConfig{
227 // turn off background sync - we manage syncs ourselves
228 BackgroundSyncConfig: nil,
229 })
230 chainSource = "electrum"
231 } else {
232 logger.Logger.WithFields(logrus.Fields{
233 "esplora_url": cfg.GetEnv().LDKEsploraServer,
234 }).Info("Using LDK node esplora chain source")
235 builder.SetChainSourceEsplora(cfg.GetEnv().LDKEsploraServer, &ldk_node.EsploraSyncConfig{
236 // turn off background sync - we manage syncs ourselves
237 BackgroundSyncConfig: nil,
238 })
239 chainSource = "esplora"
240 }
241
242 if cfg.GetEnv().LDKGossipSource != "" {
243 logger.Logger.WithField("gossipSource", cfg.GetEnv().LDKGossipSource).Warn("LDK RGS instance set")
244 builder.SetGossipSourceRgs(cfg.GetEnv().LDKGossipSource)
245 }
246 builder.SetStorageDirPath(filepath.Join(newpath, "./storage"))
247
248 migrateStorage, _ := cfg.Get("LdkMigrateStorage", "")
249 clearMigrateStorageConfigValue := false
250 if migrateStorage == "VSS" {
251 clearMigrateStorageConfigValue = true
252 if vssToken == "" {
253 return nil, errors.New("migration enabled but no vss token found")
254 }
255 builder.MigrateStorage(ldk_node.MigrateStorageVss)
256 }
257
258 resetStateRequest := getResetStateRequest(cfg)
259 if resetStateRequest != nil {
260 builder.ResetState(*resetStateRequest)
261 }
262
263 logger.Logger.WithFields(logrus.Fields{
264 "migrate_storage": migrateStorage,
265 "vss_enabled": vssToken != "",
266 "node_alias": alias,
267 "listening_addresses": listeningAddresses,
268 "chain_source": chainSource,
269 }).Info("Creating LDK node")
270 setStartupState("Loading node data...")
271 var node *ldk_node.Node
272 if vssToken != "" {
273 node, err = builder.BuildWithVssStoreAndFixedHeaders(cfg.GetEnv().LDKVssUrl, "albyhub", map[string]string{
274 "Authorization": fmt.Sprintf("Bearer %s", vssToken),
275 })
276 } else {
277 node, err = builder.Build()
278 }
279
280 if err != nil {
281 logger.Logger.WithError(err).Error("Failed to create LDK node")
282 return nil, err
283 }
284
285 logger.Logger.WithFields(logrus.Fields{}).Info("LDK node created")
286
287 if clearMigrateStorageConfigValue {
288 err = cfg.SetUpdate("LdkMigrateStorage", "", "")
289 if err != nil {
290 logger.Logger.WithError(err).Error("Failed to clear LDK migrate storage config value")
291 return nil, err
292 }
293 }
294
295 ldkEventConsumer := make(chan *ldk_node.Event)
296 ldkCtx, cancel := context.WithCancel(ctx)
297 ldkEventBroadcaster := NewLDKEventBroadcaster(ldkCtx, ldkEventConsumer)
298 nodeId := node.NodeId()
299
300 ls := LDKService{
301 workdir: newpath,
302 node: node,
303 cancel: cancel,
304 ldkEventBroadcaster: ldkEventBroadcaster,
305 network: network,
306 eventPublisher: eventPublisher,
307 cfg: cfg,
308 pubkey: nodeId,
309 lsps2Pubkey: lsps2Pubkey,
310 lsps2Address: lsps2Address,
311 ctx: ldkCtx,
312 }
313
314 eventPublisher.RegisterSubscriber(&ls)
315
316 // TODO: remove after 2026-01-01 - we now log to app logs rather than ldk log files
317 // this line is just left to cleanup old logs after the update
318 deleteOldLDKLogs(filepath.Join(newpath, "./logs"))
319
320 // check for and forward new LDK events to LDKEventBroadcaster (through ldkEventConsumer)
321 go func() {
322 for {
323 select {
324 case <-ldkCtx.Done():
325 return
326 default:
327 }
328
329 // NextEventAsync parks this goroutine on a Go channel until the next event
330 // arrives - unlike WaitNextEvent it does not block an OS thread in FFI.
331 // NOTE: the call cannot be cancelled; after shutdown it stays parked until
332 // the node emits a final event or the process exits.
333 event := node.NextEventAsync()
334
335 // eventHandlingMutex is held while handling so Shutdown() can wait
336 // for in-flight event handling to finish before stopping the node.
337 // Events dropped without EventHandled() are redelivered by LDK on
338 // the next startup.
339 ok := func() bool {
340 ls.eventHandlingMutex.Lock()
341 defer ls.eventHandlingMutex.Unlock()
342
343 if ldkCtx.Err() != nil {
344 return false
345 }
346
347 ls.handleLdkEvent(&event)
348
349 select {
350 case ldkEventConsumer <- &event:
351 case <-ldkCtx.Done():
352 return false
353 }
354
355 if err := node.EventHandled(); err != nil {
356 logger.Logger.WithError(err).Error("Failed to mark LDK event as handled")
357 }
358 return true
359 }()
360 if !ok {
361 return
362 }
363 }
364 }()
365
366 logger.Logger.WithFields(logrus.Fields{
367 "nodeId": nodeId,
368 }).Info("Starting LDK node...")
369
370 setStartupState("Starting node...")
371
372 err = node.Start()
373 if err != nil {
374 logger.Logger.WithError(err).Error("Failed to start LDK node")
375 return nil, err
376 }
377
378 logger.Logger.WithFields(logrus.Fields{
379 "nodeId": nodeId,
380 "status": node.Status(),
381 }).Info("Started LDK node. Syncing wallet...")
382
383 setStartupState("Syncing node...")
384 syncStartTime := time.Now()
385 err = node.SyncWallets()
386 if err != nil {
387 logger.Logger.WithError(err).Error("Failed to sync LDK wallets")
388 ls.eventPublisher.Publish(&events.Event{
389 Event: "nwc_node_sync_failed",
390 Properties: map[string]interface{}{
391 "error": err.Error(),
392 "sync_type": "full",
393 "initial_sync": true,
394 "node_type": config.LDKBackendType,
395 },
396 })
397
398 shutdownErr := ls.Shutdown()
399 if shutdownErr != nil {
400 logger.Logger.WithError(shutdownErr).Error("Failed to shutdown LDK node")
401 }
402
403 return nil, err
404 }
405 ls.lastFullSync = time.Now()
406 ls.lastFeeEstimatesSync = time.Now()
407
408 logger.Logger.WithFields(logrus.Fields{
409 "nodeId": nodeId,
410 "status": node.Status(),
411 "duration": math.Ceil(time.Since(syncStartTime).Seconds()),
412 }).Info("LDK node synced successfully")
413
414 // setup background sync
415 go func() {
416 MIN_SYNC_INTERVAL := 1 * time.Minute
417 MIN_FEE_ESTIMATES_SYNC_INTERVAL := 5 * time.Minute
418 MAX_SYNC_INTERVAL := 1 * time.Hour // NOTE: this could be increased further (possibly to 6 hours)
419 for {
420 ls.syncing = false
421 select {
422 case <-ldkCtx.Done():
423 return
424 case <-time.After(MIN_SYNC_INTERVAL):
425 ls.syncing = true
426
427 channels := ls.node.ListChannels()
428 for _, channel := range channels {
429 if channel.Confirmations != nil && channel.ConfirmationsRequired != nil && *channel.Confirmations < *channel.ConfirmationsRequired {
430 logger.Logger.WithField("channel_id", channel.UserChannelId).Debug("Using short sync time while opening channel")
431 ls.lastWalletSyncRequest = time.Now()
432 break
433 }
434 }
435 balances := ls.node.ListBalances()
436 for _, balance := range balances.LightningBalances {
437 switch balanceType := (balance).(type) {
438 case ldk_node.LightningBalanceContentiousClaimable:
439 logger.Logger.WithField("channel_id", balanceType.ChannelId).Debug("Using short sync time while balances are contentious claimable after channel closure")
440 ls.lastWalletSyncRequest = time.Now()
441 }
442 }
443
444 if time.Since(ls.lastWalletSyncRequest) > MIN_SYNC_INTERVAL && time.Since(ls.lastFullSync) < MAX_SYNC_INTERVAL {
445
446 if time.Since(ls.lastFeeEstimatesSync) < MIN_FEE_ESTIMATES_SYNC_INTERVAL {
447 logger.Logger.Debug("Skipping updating fee estimates")
448 continue
449 }
450
451 // only update fee estimates
452 logger.Logger.Debug("Updating fee estimates")
453 err = node.UpdateFeeEstimates()
454 if err != nil {
455 logger.Logger.WithError(err).Error("Failed to update fee estimates")
456 ls.eventPublisher.Publish(&events.Event{
457 Event: "nwc_node_sync_failed",
458 Properties: map[string]interface{}{
459 "error": err.Error(),
460 "sync_type": "fee_estimates",
461 "node_type": config.LDKBackendType,
462 },
463 })
464 continue
465 }
466 ls.lastFeeEstimatesSync = time.Now()
467 continue
468 }
469
470 logger.Logger.Debug("Starting full background wallet sync")
471 syncStartTime := time.Now()
472 err = node.SyncWallets()
473
474 if err != nil {
475 logger.Logger.WithError(err).Error("Failed to sync LDK wallets")
476 ls.eventPublisher.Publish(&events.Event{
477 Event: "nwc_node_sync_failed",
478 Properties: map[string]interface{}{
479 "error": err.Error(),
480 "sync_type": "full",
481 "node_type": config.LDKBackendType,
482 },
483 })
484
485 // try again at next MIN_SYNC_INTERVAL
486 continue
487 }
488
489 ls.redeemedOnchainFundsWithinThisSync = false
490 ls.lastFullSync = time.Now()
491 // fee estimates happens as part of full sync
492 ls.lastFeeEstimatesSync = time.Now()
493
494 logger.Logger.WithFields(logrus.Fields{
495 "nodeId": nodeId,
496 "status": node.Status(),
497 "duration": math.Ceil(time.Since(syncStartTime).Seconds()),
498 }).Info("LDK node synced successfully")
499
500 // delete old payments while node is not syncing
501 ls.deleteOldLDKPayments()
502 }
503 }
504 }()
505
506 return &ls, nil
507 }
508
509 var shutdownMutex sync.Mutex
510
511 func (ls *LDKService) Shutdown() error {
512 shutdownMutex.Lock()
513 defer shutdownMutex.Unlock()
514 if ls.shuttingDown {
515 logger.Logger.Debug("LDK client is already shutting down")
516 return nil
517 }
518 ls.shuttingDown = true
519 ls.eventPublisher.RemoveSubscriber(ls)
520
521 logger.Logger.Info("shutting down LDK client")
522 logger.Logger.Info("cancelling LDK context")
523 ls.cancel()
524
525 // wait for in-flight LDK event handling to finish - handleLdkEvent makes
526 // node calls which must not run once the node is stopped and destroyed.
527 // Held until the end of Shutdown; the event loop checks the cancelled
528 // context under this mutex before touching the node.
529 ls.eventHandlingMutex.Lock()
530 defer ls.eventHandlingMutex.Unlock()
531
532 maxAttempts := 40
533 for i := 0; ls.syncing; i++ {
534 logger.Logger.WithField("attempt", i).Warn("Waiting for background sync to finish before stopping LDK node...")
535 time.Sleep(1 * time.Second)
536 if i > maxAttempts {
537 logger.Logger.Error("Timed out waiting for background sync to finish before stopping LDK node")
538 break
539 }
540 }
541
542 logger.Logger.Info("stopping LDK node")
543 shutdownChannel := make(chan error)
544 go func() {
545 shutdownChannel <- ls.node.Stop()
546 }()
547
548 select {
549 case err := <-shutdownChannel:
550 if err != nil {
551 logger.Logger.WithError(err).Error("Failed to stop LDK node")
552 // do not return error - we still need to destroy the node
553 } else {
554 logger.Logger.Info("LDK stop node succeeded")
555 }
556 case <-time.After(5 * time.Minute):
557 logger.Logger.Error("Timeout shutting down LDK node after 5 minutes")
558 }
559
560 logger.Logger.Debug("Destroying LDK node object")
561 ls.node.Destroy()
562
563 logger.Logger.Info("LDK shutdown complete")
564
565 return nil
566 }
567
568 func getMaxTotalRoutingFeeLimit(amountMsat uint64) uint64 {
569 return transactions.CalculateFeeReserveMsat(amountMsat)
570 }
571
572 func (ls *LDKService) MakeOffer(ctx context.Context, description string) (string, error) {
573 offer, err := ls.node.Bolt12Payment().ReceiveVariableAmount(description, nil)
574 if err != nil {
575 logger.Logger.WithError(err).Error("Failed to generate BOLT12 offer")
576 return "", err
577 }
578
579 logger.Logger.WithField("offer", offer).Info("Generated BOLT12 offer")
580 return offer.String(), nil
581 }
582
583 func (ls *LDKService) SendPaymentSync(invoice string, amountMsat *uint64) (*lnclient.PayInvoiceResponse, error) {
584 paymentRequest, err := decodepay.Decodepay(invoice)
585 if err != nil {
586 logger.Logger.WithFields(logrus.Fields{
587 "bolt11": invoice,
588 }).WithError(err).Error("Failed to decode bolt11 invoice")
589
590 return nil, err
591 }
592
593 paymentAmountMsat := uint64(paymentRequest.MSatoshi)
594 if amountMsat != nil {
595 paymentAmountMsat = *amountMsat
596 }
597
598 maxSpendable := ls.getMaxSpendable()
599 if paymentAmountMsat > maxSpendable {
600 ls.eventPublisher.Publish(&events.Event{
601 Event: "nwc_outgoing_liquidity_required",
602 Properties: map[string]interface{}{
603 // "amount": amount / 1000,
604 // "max_receivable": maxReceivable,
605 // "num_channels": len(gs.node.ListChannels()),
606 "node_type": config.LDKBackendType,
607 },
608 })
609 }
610
611 paymentStart := time.Now()
612 ldkEventSubscription := ls.ldkEventBroadcaster.Subscribe()
613 defer ls.ldkEventBroadcaster.CancelSubscription(ldkEventSubscription)
614
615 saturationPower := ls.cfg.GetEnv().LDKMaxChannelSaturationPowerOfHalf
616 maxPathCount := ls.cfg.GetEnv().LDKMaxPathCount
617 maxTotalRoutingFeeMsat := getMaxTotalRoutingFeeLimit(paymentAmountMsat)
618
619 routeParameters := &ldk_node.RouteParametersConfig{
620 MaxTotalRoutingFeeMsat: &maxTotalRoutingFeeMsat,
621 MaxChannelSaturationPowerOfHalf: saturationPower,
622 MaxPathCount: maxPathCount,
623 MaxTotalCltvExpiryDelta: 1008, // TODO: remove and use default
624 }
625
626 invoiceObj, err := ldk_node.Bolt11InvoiceFromStr(invoice)
627 if err != nil {
628 logger.Logger.WithError(err).Error("ldk failed to parse bolt 11 invoice from string")
629 return nil, err
630 }
631
632 var paymentHash string
633 if amountMsat == nil {
634 paymentHash, err = ls.node.Bolt11Payment().Send(invoiceObj, routeParameters)
635 } else {
636 paymentHash, err = ls.node.Bolt11Payment().SendUsingAmount(invoiceObj, *amountMsat, routeParameters)
637 }
638 if err != nil {
639 logger.Logger.WithError(err).Error("SendPayment failed")
640 return nil, err
641 }
642 feeMsat := uint64(0)
643 preimage := ""
644
645 for {
646 select {
647 case <-ls.ctx.Done():
648 return nil, ls.ctx.Err()
649
650 case ev := <-ldkEventSubscription:
651 switch event := (*ev).(type) {
652 case ldk_node.EventPaymentSuccessful:
653 if event.PaymentHash != paymentHash {
654 continue
655 }
656 logger.Logger.WithFields(logrus.Fields{
657 "event": event,
658 }).Info("Got payment success event")
659
660 if event.PaymentPreimage == nil {
661 logger.Logger.WithField("payment_hash", paymentHash).Error("No payment preimage in payment success event")
662 return nil, errors.New("payment preimage not found")
663 }
664
665 preimage = *event.PaymentPreimage
666
667 if event.FeePaidMsat != nil {
668 feeMsat = *event.FeePaidMsat
669 }
670
671 logger.Logger.WithFields(logrus.Fields{
672 "duration": time.Since(paymentStart).Milliseconds(),
673 "fee": feeMsat,
674 "payment_hash": event.PaymentHash,
675 }).Info("Successful payment")
676
677 return &lnclient.PayInvoiceResponse{
678 Preimage: preimage,
679 FeeMsat: feeMsat,
680 }, nil
681 case ldk_node.EventPaymentFailed:
682 if event.PaymentHash != nil && *event.PaymentHash == paymentHash {
683 failureReasonMessage := ls.getPaymentFailReason(&event)
684 logger.Logger.WithFields(logrus.Fields{
685 "payment_hash": paymentHash,
686 "reason": failureReasonMessage,
687 }).Error("Received payment failed event")
688 return nil, fmt.Errorf("received payment failed event: %s", failureReasonMessage)
689 }
690 }
691 }
692 }
693 }
694
695 func (ls *LDKService) SendKeysend(amountMsat uint64, destination string, custom_records []lnclient.TLVRecord, preimage string) (*lnclient.PayKeysendResponse, error) {
696 paymentStart := time.Now()
697 customTlvs := []ldk_node.CustomTlvRecord{}
698
699 for _, customRecord := range custom_records {
700 decodedValue, err := hex.DecodeString(customRecord.Value)
701 if err != nil {
702 return nil, err
703 }
704 customTlvs = append(customTlvs, ldk_node.CustomTlvRecord{
705 TypeNum: customRecord.Type,
706 Value: decodedValue,
707 })
708 }
709
710 ldkEventSubscription := ls.ldkEventBroadcaster.Subscribe()
711 defer ls.ldkEventBroadcaster.CancelSubscription(ldkEventSubscription)
712
713 saturationPower := ls.cfg.GetEnv().LDKMaxChannelSaturationPowerOfHalf
714 maxPathCount := ls.cfg.GetEnv().LDKMaxPathCount
715 maxTotalRoutingFeeMsat := getMaxTotalRoutingFeeLimit(amountMsat)
716
717 routeParameters := &ldk_node.RouteParametersConfig{
718 MaxTotalRoutingFeeMsat: &maxTotalRoutingFeeMsat,
719 MaxChannelSaturationPowerOfHalf: saturationPower,
720 MaxPathCount: maxPathCount,
721 MaxTotalCltvExpiryDelta: 1008, // TODO: remove and use default
722 }
723
724 paymentHash, err := ls.node.SpontaneousPayment().SendWithPreimageAndCustomTlvs(amountMsat, destination, customTlvs, preimage, routeParameters)
725 if err != nil {
726 logger.Logger.WithError(err).Error("Keysend failed")
727 return nil, err
728 }
729 feeMsat := uint64(0)
730 for {
731 select {
732 case <-ls.ctx.Done():
733 return nil, ls.ctx.Err()
734 case event := <-ldkEventSubscription:
735
736 eventPaymentSuccessful, isEventPaymentSuccessfulEvent := (*event).(ldk_node.EventPaymentSuccessful)
737 eventPaymentFailed, isEventPaymentFailedEvent := (*event).(ldk_node.EventPaymentFailed)
738
739 if isEventPaymentSuccessfulEvent && eventPaymentSuccessful.PaymentHash == paymentHash {
740 logger.Logger.Info("Got payment success event")
741
742 if eventPaymentSuccessful.FeePaidMsat != nil {
743 feeMsat = *eventPaymentSuccessful.FeePaidMsat
744 }
745 logger.Logger.WithFields(logrus.Fields{
746 "duration": time.Since(paymentStart).Milliseconds(),
747 "fee": feeMsat,
748 }).Info("Successful keysend payment")
749 return &lnclient.PayKeysendResponse{
750 FeeMsat: feeMsat,
751 }, nil
752 }
753 if isEventPaymentFailedEvent && eventPaymentFailed.PaymentHash != nil && *eventPaymentFailed.PaymentHash == paymentHash {
754
755 failureReasonMessage := ls.getPaymentFailReason(&eventPaymentFailed)
756
757 logger.Logger.WithFields(logrus.Fields{
758 "payment_hash": paymentHash,
759 "reason": failureReasonMessage,
760 }).Error("Received payment failed event")
761
762 return nil, fmt.Errorf("payment failed event: %s", failureReasonMessage)
763 }
764 }
765 }
766
767 }
768
769 func (ls *LDKService) getMaxReceivable() int64 {
770 var receivable int64 = 0
771 channels := ls.node.ListChannels()
772 for _, channel := range channels {
773 if channel.IsUsable {
774 receivable += min(int64(channel.InboundCapacityMsat), int64(*channel.InboundHtlcMaximumMsat))
775 }
776 }
777 return int64(receivable)
778 }
779
780 func (ls *LDKService) hasPublicChannel() bool {
781 channels := ls.node.ListChannels()
782 for _, channel := range channels {
783 if channel.IsAnnounced {
784 return true
785 }
786 }
787 return false
788 }
789
790 func (ls *LDKService) getMaxSpendable() uint64 {
791 var spendable uint64 = 0
792 channels := ls.node.ListChannels()
793 for _, channel := range channels {
794 if channel.IsUsable {
795 spendable += min(channel.OutboundCapacityMsat, *channel.CounterpartyOutboundHtlcMaximumMsat)
796 }
797 }
798 return spendable
799 }
800
801 func (ls *LDKService) MakeInvoice(ctx context.Context, amountMsat int64, description string, descriptionHash string, expirySeconds int64, throughNodePubkey *string) (transaction *lnclient.Transaction, err error) {
802
803 if expirySeconds < 0 || expirySeconds > int64(maxInvoiceExpiry/time.Second) {
804 return nil, errors.New("invalid invoice expiry")
805 }
806
807 maxReceivable := ls.getMaxReceivable()
808
809 jitChannelsEnabled, _ := ls.cfg.Get("JitChannelsEnabled", "")
810 // JIT channels are only used for users without a public channel - users with
811 // a public channel should increase inbound liquidity manually.
812 isJitInvoice := ls.lsps2Pubkey != "" &&
813 jitChannelsEnabled != "false" &&
814 !ls.hasPublicChannel() &&
815 amountMsat > maxReceivable
816
817 if amountMsat > maxReceivable && !isJitInvoice {
818 ls.eventPublisher.Publish(&events.Event{
819 Event: "nwc_incoming_liquidity_required",
820 Properties: map[string]interface{}{
821 // "amount": amount / 1000,
822 // "max_receivable": maxReceivable,
823 // "num_channels": len(gs.node.ListChannels()),
824 "node_type": config.LDKBackendType,
825 },
826 })
827 }
828
829 if expirySeconds == 0 {
830 expirySeconds = lnclient.DEFAULT_INVOICE_EXPIRY
831 }
832
833 var descriptionType ldk_node.Bolt11InvoiceDescription
834 descriptionType = ldk_node.Bolt11InvoiceDescriptionDirect{
835 Description: description,
836 }
837 if description == "" && descriptionHash != "" {
838 descriptionType = ldk_node.Bolt11InvoiceDescriptionHash{
839 Hash: descriptionHash,
840 }
841 }
842
843 var invoiceObj *ldk_node.Bolt11Invoice
844 if isJitInvoice {
845 // cap the opening fee the LSP may deduct from the incoming payment
846 maxLspFeeLimitMsat := ls.getLsps2MaxTotalOpeningFeeMsat(uint64(amountMsat))
847 invoiceObj, err = ls.node.Bolt11Payment().ReceiveViaJitChannel(
848 uint64(amountMsat),
849 descriptionType,
850 uint32(expirySeconds),
851 &maxLspFeeLimitMsat,
852 )
853 } else {
854 invoiceObj, err = ls.node.Bolt11Payment().Receive(
855 uint64(amountMsat),
856 descriptionType,
857 uint32(expirySeconds),
858 )
859 }
860
861 if err != nil {
862 logger.Logger.WithError(err).Error("MakeInvoice failed")
863 return nil, err
864 }
865
866 payment := ls.node.Payment(invoiceObj.PaymentHash())
867 invoice := invoiceObj.String()
868 paymentRequest, err := decodepay.Decodepay(invoice)
869 if err != nil {
870 logger.Logger.WithFields(logrus.Fields{
871 "bolt11": invoice,
872 }).WithError(err).Error("Failed to decode bolt11 invoice")
873
874 return nil, err
875 }
876 expiresAtUnix := time.UnixMilli(int64(paymentRequest.CreatedAt) * 1000).Add(time.Duration(paymentRequest.Expiry) * time.Second).Unix()
877
878 preimage := ""
879 estimatedLspFeeMsat := int64(0)
880 if payment != nil {
881 switch kind := payment.Kind.(type) {
882 case ldk_node.PaymentKindBolt11:
883 if kind.Preimage != nil {
884 preimage = *kind.Preimage
885 }
886 case ldk_node.PaymentKindBolt11Jit:
887 if kind.Preimage != nil {
888 preimage = *kind.Preimage
889 }
890 if kind.LspFeeLimits.MaxTotalOpeningFeeMsat != nil {
891 estimatedLspFeeMsat = int64(*kind.LspFeeLimits.MaxTotalOpeningFeeMsat)
892 } else if kind.LspFeeLimits.MaxProportionalOpeningFeePpmMsat != nil && amountMsat > 0 {
893 estimatedLspFeeMsat = int64((uint64(amountMsat) * *kind.LspFeeLimits.MaxProportionalOpeningFeePpmMsat) / 1_000_000)
894 }
895 }
896 }
897
898 transaction = &lnclient.Transaction{
899 Type: "incoming",
900 Invoice: invoice,
901 PaymentHash: paymentRequest.PaymentHash,
902 Preimage: preimage,
903 AmountMsat: amountMsat,
904 FeesPaidMsat: estimatedLspFeeMsat,
905 CreatedAt: int64(paymentRequest.CreatedAt),
906 ExpiresAt: &expiresAtUnix,
907 Description: paymentRequest.Description,
908 DescriptionHash: paymentRequest.DescriptionHash,
909 }
910
911 return transaction, nil
912 }
913
914 func (ls *LDKService) LookupInvoice(ctx context.Context, paymentHash string) (transaction *lnclient.Transaction, err error) {
915 // this method shouldn't be any more because this LNClient supports notifications
916 return nil, errors.New("this method should not be called")
917 }
918
919 func (ls *LDKService) ListOnchainTransactions(ctx context.Context) ([]lnclient.OnchainTransaction, error) {
920 transactions := []lnclient.OnchainTransaction{}
921 for _, payment := range ls.node.ListPayments() {
922 onchainPaymentKind, isOnchainPaymentKind := payment.Kind.(ldk_node.PaymentKindOnchain)
923 if !isOnchainPaymentKind {
924 continue
925 }
926
927 transactionType := "incoming"
928 if payment.Direction == ldk_node.PaymentDirectionOutbound {
929 transactionType = "outgoing"
930 }
931
932 var amountMsat uint64
933 if payment.AmountMsat != nil {
934 amountMsat = *payment.AmountMsat
935 }
936 var status string
937 var height uint32
938 var numConfirmations uint32
939 switch onchainPaymentStatus := onchainPaymentKind.Status.(type) {
940 case ldk_node.ConfirmationStatusConfirmed:
941 status = "confirmed"
942 height = onchainPaymentStatus.Height
943 nodeStatus := ls.node.Status()
944 numConfirmations = nodeStatus.CurrentBestBlock.Height - height
945 case ldk_node.ConfirmationStatusUnconfirmed:
946 status = "unconfirmed"
947 }
948
949 createdAt := payment.CreatedAt
950 if createdAt == 0 {
951 createdAt = payment.LatestUpdateTimestamp
952 }
953
954 transactions = append(transactions, lnclient.OnchainTransaction{
955 AmountSat: amountMsat / 1000,
956 CreatedAt: createdAt,
957 State: status,
958 Type: transactionType,
959 NumConfirmations: numConfirmations,
960 TxId: onchainPaymentKind.Txid,
961 })
962
963 }
964 sort.SliceStable(transactions, func(i, j int) bool {
965 return transactions[i].CreatedAt > transactions[j].CreatedAt
966 })
967 return transactions, nil
968 }
969
970 func (ls *LDKService) GetInfo(ctx context.Context) (info *lnclient.NodeInfo, err error) {
971 // TODO: should alias, color be configured in LDK-node? or can we manage them in NWC?
972 // an alias is only needed if the user has public channels and wants their node to be publicly visible?
973 status := ls.node.Status()
974 return &lnclient.NodeInfo{
975 Alias: "NWC",
976 Color: "#897FFF",
977 Pubkey: ls.node.NodeId(),
978 Network: ls.network,
979 BlockHeight: status.CurrentBestBlock.Height,
980 BlockHash: status.CurrentBestBlock.BlockHash,
981 }, nil
982 }
983
984 func (ls *LDKService) ListChannels(ctx context.Context) ([]lnclient.Channel, error) {
985
986 ldkChannels := ls.node.ListChannels()
987
988 channels := []lnclient.Channel{}
989
990 // logger.Logger.WithFields(logrus.Fields{
991 // "channels": ldkChannels,
992 // }).Debug("Listed Channels")
993
994 for _, ldkChannel := range ldkChannels {
995 fundingTxId := ""
996 fundingTxVout := uint32(0)
997 if ldkChannel.FundingTxo != nil {
998 fundingTxId = ldkChannel.FundingTxo.Txid
999 fundingTxVout = ldkChannel.FundingTxo.Vout
1000 }
1001
1002 internalChannel := map[string]interface{}{}
1003 internalChannel["channel"] = ldkChannel
1004 internalChannel["config"] = map[string]interface{}{
1005 "AcceptUnderpayingHtlcs": ldkChannel.Config.AcceptUnderpayingHtlcs,
1006 "CltvExpiryDelta": ldkChannel.Config.CltvExpiryDelta,
1007 "ForceCloseAvoidanceMaxFeeSatoshis": ldkChannel.Config.ForceCloseAvoidanceMaxFeeSatoshis,
1008 "ForwardingFeeBaseMsat": ldkChannel.Config.ForwardingFeeBaseMsat,
1009 "ForwardingFeeProportionalMillionths": ldkChannel.Config.ForwardingFeeProportionalMillionths,
1010 "MaxDustHtlcExposure": ldkChannel.Config.MaxDustHtlcExposure,
1011 }
1012
1013 unspendablePunishmentReserveSat := uint64(0)
1014 if ldkChannel.UnspendablePunishmentReserve != nil {
1015 unspendablePunishmentReserveSat = *ldkChannel.UnspendablePunishmentReserve
1016 }
1017
1018 var channelError *string
1019
1020 if fundingTxId == "" {
1021 channelErrorValue := "This channel has no funding transaction. Please contact support@getalby.com"
1022 channelError = &channelErrorValue
1023 } else if ldkChannel.IsUsable && ldkChannel.CounterpartyForwardingInfoFeeBaseMsat == nil {
1024 // if we don't have this, routing will not work (LND <-> LDK interoperability bug - https://github.com/lightningnetwork/lnd/issues/6870 )
1025 channelErrorValue := "Counterparty forwarding info is not yet available, but normally resolves automatically. Try restarting Alby Hub if this warning does not resolve within a few hours."
1026 channelError = &channelErrorValue
1027 }
1028
1029 isActive := ldkChannel.IsUsable /* superset of ldkChannel.IsReady */ && channelError == nil
1030
1031 // Public channels require 6 confirmations before they can be gossiped/announced
1032 // (BOLT-7), and they only become usable once announced. However, LDK accepts
1033 // channels from trusted LSP peers as 0-conf, so it reports ConfirmationsRequired
1034 // as nil/0. Override to 6 for public channels so the UI shows confirmation
1035 // progress while opening instead of an indefinite blank loading spinner.
1036 confirmationsRequired := ldkChannel.ConfirmationsRequired
1037 if ldkChannel.IsAnnounced {
1038 publicChannelConfirmationsRequired := uint32(6)
1039 if confirmationsRequired == nil || *confirmationsRequired < publicChannelConfirmationsRequired {
1040 confirmationsRequired = &publicChannelConfirmationsRequired
1041 }
1042 }
1043
1044 channels = append(channels, lnclient.Channel{
1045 InternalChannel: internalChannel,
1046 LocalBalanceMsat: int64(ldkChannel.ChannelValueSats*1000 - ldkChannel.InboundCapacityMsat - ldkChannel.CounterpartyUnspendablePunishmentReserve*1000),
1047 LocalSpendableBalanceMsat: int64(ldkChannel.OutboundCapacityMsat),
1048 RemoteBalanceMsat: int64(ldkChannel.InboundCapacityMsat),
1049 RemotePubkey: ldkChannel.CounterpartyNodeId,
1050 Id: ldkChannel.UserChannelId, // CloseChannel takes the UserChannelId
1051 Active: isActive,
1052 Public: ldkChannel.IsAnnounced,
1053 FundingTxId: fundingTxId,
1054 FundingTxVout: fundingTxVout,
1055 Confirmations: ldkChannel.Confirmations,
1056 ConfirmationsRequired: confirmationsRequired,
1057 ForwardingFeeBaseMsat: ldkChannel.Config.ForwardingFeeBaseMsat,
1058 ForwardingFeeProportionalMillionths: ldkChannel.Config.ForwardingFeeProportionalMillionths,
1059 UnspendablePunishmentReserveSat: unspendablePunishmentReserveSat,
1060 CounterpartyUnspendablePunishmentReserveSat: ldkChannel.CounterpartyUnspendablePunishmentReserve,
1061 Error: channelError,
1062 IsOutbound: ldkChannel.IsOutbound,
1063 })
1064 }
1065
1066 return channels, nil
1067 }
1068
1069 func (ls *LDKService) GetNodeConnectionInfo(ctx context.Context) (nodeConnectionInfo *lnclient.NodeConnectionInfo, err error) {
1070 nodeConnectionInfo = &lnclient.NodeConnectionInfo{
1071 Pubkey: ls.node.NodeId(),
1072 }
1073
1074 if ls.cfg.GetEnv().LDKAnnouncementAddresses != "" {
1075 addresses := strings.Split(ls.cfg.GetEnv().LDKAnnouncementAddresses, ",")
1076 for _, address := range addresses {
1077 address = strings.TrimSpace(address)
1078 if address == "" {
1079 continue
1080 }
1081
1082 var ip string
1083 var portStr string
1084
1085 if strings.HasPrefix(address, "[") {
1086 // IPv6 format: [ipv6]:port
1087 closeBracket := strings.Index(address, "]")
1088 if closeBracket > 0 {
1089 ip = address[0 : closeBracket+1]
1090 if closeBracket+2 < len(address) && address[closeBracket+1] == ':' {
1091 portStr = address[closeBracket+2:]
1092 }
1093 }
1094 } else {
1095 // IPv4 or hostname format: ip:port
1096 parts := strings.Split(address, ":")
1097 if len(parts) >= 2 {
1098 portStr = parts[len(parts)-1]
1099 ip = strings.Join(parts[:len(parts)-1], ":")
1100 }
1101 }
1102
1103 if portStr != "" {
1104 if port, parseErr := strconv.Atoi(portStr); parseErr == nil && ip != "" {
1105 nodeConnectionInfo.Address = ip
1106 nodeConnectionInfo.Port = port
1107 break
1108 }
1109 }
1110 }
1111 }
1112
1113 return nodeConnectionInfo, nil
1114 }
1115
1116 func (ls *LDKService) ConnectPeer(ctx context.Context, connectPeerRequest *lnclient.ConnectPeerRequest) error {
1117 peers := ls.node.ListPeers()
1118
1119 var foundPeer *ldk_node.PeerDetails
1120 for _, peer := range peers {
1121 if peer.NodeId == connectPeerRequest.Pubkey {
1122 foundPeer = &peer
1123 break
1124 }
1125 }
1126
1127 if foundPeer != nil && !strings.Contains(foundPeer.Address, connectPeerRequest.Address) {
1128 logger.Logger.WithFields(logrus.Fields{
1129 "existing_address": foundPeer.Address,
1130 "new_address": connectPeerRequest.Address,
1131 }).Warn("peer address changed, disconnecting first")
1132 // disconnect first to ensure new IP address is saved in case of re-connecting
1133 err := ls.node.Disconnect(connectPeerRequest.Pubkey)
1134 if err != nil {
1135 // non-critical: only log an error
1136 logger.Logger.WithField("request", connectPeerRequest).WithError(err).Error("Disconnect failed while connecting peer")
1137 }
1138 }
1139
1140 err := ls.node.Connect(connectPeerRequest.Pubkey, connectPeerRequest.Address+":"+strconv.Itoa(int(connectPeerRequest.Port)), true)
1141 if err != nil {
1142 logger.Logger.WithField("request", connectPeerRequest).WithError(err).Error("ConnectPeer failed")
1143 return err
1144 }
1145
1146 return nil
1147 }
1148
1149 func (ls *LDKService) OpenChannel(ctx context.Context, openChannelRequest *lnclient.OpenChannelRequest) (*lnclient.OpenChannelResponse, error) {
1150 peers := ls.node.ListPeers()
1151 var foundPeer *ldk_node.PeerDetails
1152 for _, peer := range peers {
1153 if peer.NodeId == openChannelRequest.Pubkey {
1154
1155 foundPeer = &peer
1156 break
1157 }
1158 }
1159
1160 if foundPeer == nil {
1161 return nil, errors.New("node is not peered yet")
1162 }
1163
1164 ldkEventSubscription := ls.ldkEventBroadcaster.Subscribe()
1165 defer ls.ldkEventBroadcaster.CancelSubscription(ldkEventSubscription)
1166
1167 logger.Logger.WithField("peer_id", foundPeer.NodeId).Info("Opening channel")
1168 var userChannelId string
1169 var err error
1170 if openChannelRequest.Public {
1171 userChannelId, err = ls.node.OpenAnnouncedChannel(foundPeer.NodeId, foundPeer.Address, uint64(openChannelRequest.AmountSats), nil, nil)
1172 } else {
1173 userChannelId, err = ls.node.OpenChannel(foundPeer.NodeId, foundPeer.Address, uint64(openChannelRequest.AmountSats), nil, nil)
1174 }
1175 if err != nil {
1176 logger.Logger.WithError(err).Error("OpenChannel failed")
1177 return nil, err
1178 }
1179
1180 // userChannelId allows to locally keep track of the channel (and is also used to close the channel)
1181 logger.Logger.WithFields(logrus.Fields{
1182 "peer_id": foundPeer.NodeId,
1183 "channel_id": userChannelId,
1184 }).Info("Funded channel")
1185
1186 for start := time.Now(); time.Since(start) < time.Second*60; {
1187 event := <-ldkEventSubscription
1188
1189 channelPendingEvent, isChannelPendingEvent := (*event).(ldk_node.EventChannelPending)
1190 channelClosedEvent, isChannelClosedEvent := (*event).(ldk_node.EventChannelClosed)
1191
1192 if isChannelClosedEvent {
1193 closureReason := ls.getChannelCloseReason(&channelClosedEvent)
1194 logger.Logger.WithFields(logrus.Fields{
1195 "event": channelClosedEvent,
1196 "reason": closureReason,
1197 }).Info("Failed to open channel")
1198
1199 return nil, fmt.Errorf("failed to open channel with %s: %s", foundPeer.NodeId, closureReason)
1200 }
1201
1202 if !isChannelPendingEvent {
1203 continue
1204 }
1205
1206 return &lnclient.OpenChannelResponse{
1207 FundingTxId: channelPendingEvent.FundingTxo.Txid,
1208 }, nil
1209 }
1210
1211 return nil, errors.New("open channel timeout")
1212 }
1213
1214 func (ls *LDKService) UpdateChannel(ctx context.Context, updateChannelRequest *lnclient.UpdateChannelRequest) error {
1215 channels := ls.node.ListChannels()
1216
1217 var foundChannel *ldk_node.ChannelDetails
1218 for _, channel := range channels {
1219 if channel.UserChannelId == updateChannelRequest.ChannelId && channel.CounterpartyNodeId == updateChannelRequest.NodeId {
1220 foundChannel = &channel
1221 break
1222 }
1223 }
1224
1225 if foundChannel == nil {
1226 logger.Logger.WithField("request", updateChannelRequest).Error("failed to find channel to update")
1227 return errors.New("channel not found")
1228 }
1229
1230 existingConfig := foundChannel.Config
1231 existingConfig.ForwardingFeeBaseMsat = updateChannelRequest.ForwardingFeeBaseMsat
1232 existingConfig.ForwardingFeeProportionalMillionths = updateChannelRequest.ForwardingFeeProportionalMillionths
1233
1234 if updateChannelRequest.MaxDustHtlcExposureFromFeeRateMultiplier > 0 {
1235 existingConfig.MaxDustHtlcExposure = ldk_node.MaxDustHtlcExposureFeeRateMultiplier{
1236 Multiplier: updateChannelRequest.MaxDustHtlcExposureFromFeeRateMultiplier,
1237 }
1238 }
1239
1240 err := ls.node.UpdateChannelConfig(updateChannelRequest.ChannelId, updateChannelRequest.NodeId, existingConfig)
1241 if err != nil {
1242 logger.Logger.WithError(err).Error("UpdateChannelConfig failed")
1243 return err
1244 }
1245 return nil
1246 }
1247
1248 func (ls *LDKService) CloseChannel(ctx context.Context, closeChannelRequest *lnclient.CloseChannelRequest) error {
1249 logger.Logger.WithFields(logrus.Fields{
1250 "request": closeChannelRequest,
1251 }).Info("Closing Channel")
1252
1253 var err error
1254 if closeChannelRequest.Force {
1255 err = ls.node.ForceCloseChannel(closeChannelRequest.ChannelId, closeChannelRequest.NodeId, nil)
1256 } else {
1257 err = ls.node.CloseChannel(closeChannelRequest.ChannelId, closeChannelRequest.NodeId)
1258 }
1259 if err != nil {
1260 logger.Logger.WithError(err).Error("CloseChannel failed")
1261 return err
1262 }
1263 return nil
1264 }
1265
1266 func (ls *LDKService) GetNewOnchainAddress(ctx context.Context) (string, error) {
1267 address, err := ls.node.OnchainPayment().NewAddress()
1268 if err != nil {
1269 logger.Logger.WithError(err).Error("NewOnchainAddress failed")
1270 return "", err
1271 }
1272 return address, nil
1273 }
1274
1275 func (ls *LDKService) GetOnchainBalance(ctx context.Context) (*lnclient.OnchainBalanceResponse, error) {
1276 nodeStatus := ls.node.Status()
1277 channels := ls.node.ListChannels()
1278 balances := ls.node.ListBalances()
1279 logger.Logger.WithFields(logrus.Fields{
1280 "balances": balances,
1281 }).Debug("Listed Balances")
1282
1283 type internalLightningBalance struct {
1284 BalanceType string
1285 Balance ldk_node.LightningBalance
1286 }
1287
1288 internalLightningBalances := []internalLightningBalance{}
1289
1290 pendingBalancesDetails := make([]lnclient.PendingBalanceDetails, 0)
1291
1292 pendingBalancesFromChannelClosuresSat := uint64(0)
1293 // increase pending balance from any lightning balances for channels that are pending closure
1294 // (they do not exist in our list of open channels)
1295 for _, balance := range balances.LightningBalances {
1296 increasePendingBalance := func(nodeId, channelId string, amountSat uint64, fundingTxId ldk_node.Txid, fundingTxIndex uint16) {
1297 if !slices.ContainsFunc(channels, func(channel ldk_node.ChannelDetails) bool {
1298 return channel.ChannelId == channelId
1299 }) {
1300 pendingBalancesFromChannelClosuresSat += amountSat
1301 pendingBalancesDetails = append(pendingBalancesDetails, lnclient.PendingBalanceDetails{
1302 NodeId: nodeId,
1303 ChannelId: channelId,
1304 AmountSat: amountSat,
1305 FundingTxId: fundingTxId,
1306 FundingTxVout: uint32(fundingTxIndex),
1307 })
1308 }
1309 }
1310
1311 // include the balance type as it's useful to know the state of the channel
1312 internalLightningBalances = append(internalLightningBalances, internalLightningBalance{
1313 BalanceType: fmt.Sprintf("%T", balance),
1314 Balance: balance,
1315 })
1316 switch balanceType := (balance).(type) {
1317 case ldk_node.LightningBalanceClaimableOnChannelClose:
1318 increasePendingBalance(balanceType.CounterpartyNodeId, balanceType.ChannelId, balanceType.AmountSatoshis, balanceType.FundingTxId, balanceType.FundingTxIndex)
1319 case ldk_node.LightningBalanceClaimableAwaitingConfirmations:
1320 increasePendingBalance(balanceType.CounterpartyNodeId, balanceType.ChannelId, balanceType.AmountSatoshis, balanceType.FundingTxId, balanceType.FundingTxIndex)
1321 case ldk_node.LightningBalanceContentiousClaimable:
1322 increasePendingBalance(balanceType.CounterpartyNodeId, balanceType.ChannelId, balanceType.AmountSatoshis, balanceType.FundingTxId, balanceType.FundingTxIndex)
1323 case ldk_node.LightningBalanceMaybeTimeoutClaimableHtlc:
1324 increasePendingBalance(balanceType.CounterpartyNodeId, balanceType.ChannelId, balanceType.AmountSatoshis, balanceType.FundingTxId, balanceType.FundingTxIndex)
1325 case ldk_node.LightningBalanceMaybePreimageClaimableHtlc:
1326 increasePendingBalance(balanceType.CounterpartyNodeId, balanceType.ChannelId, balanceType.AmountSatoshis, balanceType.FundingTxId, balanceType.FundingTxIndex)
1327 case ldk_node.LightningBalanceCounterpartyRevokedOutputClaimable:
1328 increasePendingBalance(balanceType.CounterpartyNodeId, balanceType.ChannelId, balanceType.AmountSatoshis, balanceType.FundingTxId, balanceType.FundingTxIndex)
1329 }
1330 }
1331
1332 pendingSweepBalanceDetails := make([]lnclient.PendingBalanceDetails, 0)
1333 increasePendingBalanceFromClosure := func(nodeId, channelId *string, amountSat uint64, fundingTxId *ldk_node.Txid, fundingTxIndex *uint16) {
1334 pendingBalancesFromChannelClosuresSat += amountSat
1335
1336 if nodeId != nil && channelId != nil && fundingTxId != nil && fundingTxIndex != nil {
1337 pendingSweepBalanceDetails = append(pendingSweepBalanceDetails, lnclient.PendingBalanceDetails{
1338 NodeId: *nodeId,
1339 ChannelId: *channelId,
1340 AmountSat: amountSat,
1341 FundingTxId: *fundingTxId,
1342 FundingTxVout: uint32(*fundingTxIndex),
1343 })
1344 }
1345 }
1346
1347 // increase pending balance from any lightning balances for channels that were closed
1348 for _, balance := range balances.PendingBalancesFromChannelClosures {
1349 switch pendingType := (balance).(type) {
1350 case ldk_node.PendingSweepBalancePendingBroadcast:
1351 increasePendingBalanceFromClosure(pendingType.CounterpartyNodeId, pendingType.ChannelId, pendingType.AmountSatoshis, pendingType.FundingTxId, pendingType.FundingTxIndex)
1352 case ldk_node.PendingSweepBalanceBroadcastAwaitingConfirmation:
1353 increasePendingBalanceFromClosure(pendingType.CounterpartyNodeId, pendingType.ChannelId, pendingType.AmountSatoshis, pendingType.FundingTxId, pendingType.FundingTxIndex)
1354 case ldk_node.PendingSweepBalanceAwaitingThresholdConfirmations:
1355 if nodeStatus.CurrentBestBlock.Height < pendingType.ConfirmationHeight+6 {
1356 // LDK now keeps the balance in this state for four weeks even after the funds are confirmed to be swept
1357 // to confirm the channel monitors are archived before the sweeper entries are dropped
1358 // so now we just check for 6 confirmations
1359 increasePendingBalanceFromClosure(pendingType.CounterpartyNodeId, pendingType.ChannelId, pendingType.AmountSatoshis, pendingType.FundingTxId, pendingType.FundingTxIndex)
1360 }
1361 }
1362 }
1363
1364 return &lnclient.OnchainBalanceResponse{
1365 SpendableSat: int64(balances.SpendableOnchainBalanceSats),
1366 TotalSat: int64(balances.TotalOnchainBalanceSats - balances.TotalAnchorChannelsReserveSats),
1367 ReservedSat: int64(balances.TotalAnchorChannelsReserveSats),
1368 PendingBalancesFromChannelClosuresSat: pendingBalancesFromChannelClosuresSat,
1369 PendingBalancesDetails: pendingBalancesDetails,
1370 PendingSweepBalancesDetails: pendingSweepBalanceDetails,
1371 InternalBalances: map[string]interface{}{
1372 "internal_lightning_balances": internalLightningBalances,
1373 "all_balances": balances,
1374 },
1375 }, nil
1376 }
1377
1378 func (ls *LDKService) RedeemOnchainFunds(ctx context.Context, toAddress string, amountSat uint64, feeRate *uint64, sendAll bool) (string, error) {
1379 if ls.redeemedOnchainFundsWithinThisSync {
1380 return "", errors.New("please wait a minute for the wallet to sync before doing another on-chain payment")
1381 }
1382
1383 var feePtr **ldk_node.FeeRate
1384 if feeRate != nil {
1385 fee := ldk_node.FeeRateFromSatPerVbUnchecked(*feeRate)
1386 feePtr = &fee
1387 }
1388
1389 var txId string
1390 var err error
1391
1392 if !sendAll {
1393 // NOTE: this may fail if user does not reserve enough for the onchain transaction
1394 // and can also drain the anchor reserves if the user provides a too high amount.
1395 txId, err = ls.node.OnchainPayment().SendToAddress(toAddress, amountSat, feePtr)
1396 } else {
1397 txId, err = ls.node.OnchainPayment().SendAllToAddress(toAddress, false, feePtr)
1398 }
1399
1400 if err != nil {
1401 logger.Logger.WithField("send_all", sendAll).WithError(err).Error("LDK onchain payment to redeem funds failed")
1402 return "", err
1403 }
1404
1405 // make sure we do a sync after sending on-chain funds
1406 ls.redeemedOnchainFundsWithinThisSync = true
1407 ls.lastWalletSyncRequest = time.Now()
1408
1409 // FIXME: remove once LDK-node returns an error if it can't broadcast the transaction
1410
1411 tryCheckTransactionWasBroadcasted := func() error {
1412 url := ls.cfg.GetEnv().MempoolApi + "/tx/" + txId
1413
1414 client := http.Client{
1415 Timeout: time.Second * 10,
1416 }
1417
1418 req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
1419 if err != nil {
1420 logger.Logger.WithError(err).WithFields(logrus.Fields{
1421 "url": url,
1422 }).Error("Failed to create http request")
1423 return err
1424 }
1425
1426 res, err := client.Do(req)
1427 if err != nil {
1428 logger.Logger.WithError(err).WithFields(logrus.Fields{
1429 "url": url,
1430 }).Error("Failed to send request")
1431 return err
1432 }
1433
1434 if res.StatusCode >= 300 {
1435 // transaction not found
1436 return errors.New("unexpected status code")
1437 }
1438
1439 return nil
1440 }
1441
1442 for attempt := 1; attempt < 30; attempt++ {
1443 err := tryCheckTransactionWasBroadcasted()
1444 if err != nil {
1445 logger.Logger.WithError(err).WithField("attempt", attempt).Error("Failed to fetch broadcasted transaction")
1446 time.Sleep(1 * time.Second)
1447 continue
1448 }
1449
1450 return txId, nil
1451 }
1452 return "", errors.New("ran out of attempts to fetch broadcasted transaction")
1453 }
1454
1455 func (ls *LDKService) ResetRouter(key string) error {
1456 err := ls.cfg.SetUpdate(resetRouterKey, key, "")
1457 if err != nil {
1458 logger.Logger.WithError(err).Error("Failed to set reset router key")
1459 return err
1460 }
1461
1462 return nil
1463 }
1464
1465 func (ls *LDKService) SignMessage(ctx context.Context, message string) (string, error) {
1466 signedMessage := ls.node.SignMessage([]byte(message))
1467
1468 return signedMessage, nil
1469 }
1470
1471 func (ls *LDKService) ldkPaymentToTransaction(payment *ldk_node.PaymentDetails) (*lnclient.Transaction, error) {
1472 // logger.Logger.WithField("payment", payment).Debug("Mapping LDK payment to transaction")
1473
1474 transactionType := "incoming"
1475 if payment.Direction == ldk_node.PaymentDirectionOutbound {
1476 transactionType = "outgoing"
1477 }
1478
1479 var expiresAt *int64
1480 var createdAt int64
1481 var description string
1482 var descriptionHash string
1483 var bolt11Invoice string
1484 var settledAt *int64
1485 preimage := ""
1486 paymentHash := ""
1487 metadata := map[string]interface{}{}
1488
1489 bolt11PaymentKind, isBolt11PaymentKind := payment.Kind.(ldk_node.PaymentKindBolt11)
1490
1491 if isBolt11PaymentKind && bolt11PaymentKind.Bolt11Invoice != nil {
1492 bolt11Invoice = *bolt11PaymentKind.Bolt11Invoice
1493 paymentRequest, err := decodepay.Decodepay(strings.ToLower(bolt11Invoice))
1494 if err != nil {
1495 logger.Logger.WithFields(logrus.Fields{
1496 "bolt11": bolt11Invoice,
1497 }).WithError(err).Error("Failed to decode bolt11 invoice")
1498
1499 return nil, err
1500 }
1501 createdAt = int64(paymentRequest.CreatedAt)
1502 expiresAtUnix := time.UnixMilli(int64(paymentRequest.CreatedAt) * 1000).Add(time.Duration(paymentRequest.Expiry) * time.Second).Unix()
1503 expiresAt = &expiresAtUnix
1504 description = paymentRequest.Description
1505 descriptionHash = paymentRequest.DescriptionHash
1506 if payment.Status == ldk_node.PaymentStatusSucceeded {
1507 if bolt11PaymentKind.Preimage != nil {
1508 preimage = *bolt11PaymentKind.Preimage
1509 }
1510 settledAt = &createdAt // fallback settledAt to created at time
1511 if payment.LatestUpdateTimestamp > 0 {
1512 lastUpdate := int64(payment.LatestUpdateTimestamp)
1513 settledAt = &lastUpdate
1514 }
1515 }
1516 paymentHash = bolt11PaymentKind.Hash
1517 }
1518
1519 bolt11JitPaymentKind, isBolt11JitPaymentKind := payment.Kind.(ldk_node.PaymentKindBolt11Jit)
1520 if isBolt11JitPaymentKind {
1521 createdAt = int64(payment.CreatedAt)
1522 if payment.CreatedAt == 0 {
1523 createdAt = int64(payment.LatestUpdateTimestamp)
1524 }
1525 if payment.Status == ldk_node.PaymentStatusSucceeded && bolt11JitPaymentKind.Preimage != nil {
1526 preimage = *bolt11JitPaymentKind.Preimage
1527 lastUpdate := int64(payment.LatestUpdateTimestamp)
1528 settledAt = &lastUpdate
1529 }
1530 paymentHash = bolt11JitPaymentKind.Hash
1531 }
1532
1533 bolt12PaymentKind, isBolt12PaymentKind := payment.Kind.(ldk_node.PaymentKindBolt12Offer)
1534
1535 if isBolt12PaymentKind {
1536 createdAt = int64(payment.CreatedAt)
1537
1538 if bolt12PaymentKind.Hash == nil {
1539 return nil, errors.New("BOLT-12 payment has no payment hash")
1540 }
1541 paymentHash = *bolt12PaymentKind.Hash
1542
1543 offer := map[string]interface{}{}
1544 offer["id"] = bolt12PaymentKind.OfferId
1545
1546 if bolt12PaymentKind.PayerNote != nil {
1547 offer["payer_note"] = *bolt12PaymentKind.PayerNote
1548 }
1549
1550 metadata["offer"] = offer
1551
1552 if payment.Status == ldk_node.PaymentStatusSucceeded {
1553 if bolt12PaymentKind.Preimage != nil {
1554 preimage = *bolt12PaymentKind.Preimage
1555 }
1556 lastUpdate := int64(payment.LatestUpdateTimestamp)
1557 settledAt = &lastUpdate
1558 }
1559 }
1560
1561 spontaneousPaymentKind, isSpontaneousPaymentKind := payment.Kind.(ldk_node.PaymentKindSpontaneous)
1562 if isSpontaneousPaymentKind {
1563 // keysend payment
1564 lastUpdate := int64(payment.LatestUpdateTimestamp)
1565 createdAt = int64(payment.CreatedAt)
1566 // TODO: remove this check some point in the future
1567 // all payments after v0.6.2 will have createdAt set
1568 if createdAt == 0 {
1569 createdAt = lastUpdate
1570 }
1571 if payment.Status == ldk_node.PaymentStatusSucceeded {
1572 settledAt = &lastUpdate
1573 }
1574 paymentHash = spontaneousPaymentKind.Hash
1575 if spontaneousPaymentKind.Preimage != nil {
1576 preimage = *spontaneousPaymentKind.Preimage
1577 }
1578
1579 tlvRecords := []lnclient.TLVRecord{}
1580 for _, tlv := range spontaneousPaymentKind.CustomTlvs {
1581 tlvRecords = append(tlvRecords, lnclient.TLVRecord{
1582 Type: tlv.Type,
1583 Value: hex.EncodeToString(tlv.Value),
1584 })
1585 }
1586 metadata["tlv_records"] = tlvRecords
1587 }
1588
1589 var amountMsat uint64 = 0
1590 if payment.AmountMsat != nil {
1591 amountMsat = *payment.AmountMsat
1592 }
1593
1594 var feeMsat uint64 = 0
1595 if payment.FeePaidMsat != nil {
1596 feeMsat = *payment.FeePaidMsat
1597 }
1598 if isBolt11JitPaymentKind && bolt11JitPaymentKind.CounterpartySkimmedFeeMsat != nil {
1599 feeMsat = *bolt11JitPaymentKind.CounterpartySkimmedFeeMsat
1600 }
1601
1602 return &lnclient.Transaction{
1603 Type: transactionType,
1604 Preimage: preimage,
1605 PaymentHash: paymentHash,
1606 SettledAt: settledAt,
1607 AmountMsat: int64(amountMsat),
1608 Invoice: bolt11Invoice,
1609 FeesPaidMsat: int64(feeMsat),
1610 CreatedAt: createdAt,
1611 Description: description,
1612 DescriptionHash: descriptionHash,
1613 ExpiresAt: expiresAt,
1614 Metadata: metadata,
1615 }, nil
1616 }
1617
1618 func (ls *LDKService) ListPeers(ctx context.Context) ([]lnclient.PeerDetails, error) {
1619 peers := ls.node.ListPeers()
1620 ret := make([]lnclient.PeerDetails, 0, len(peers))
1621 for _, peer := range peers {
1622 ret = append(ret, lnclient.PeerDetails{
1623 NodeId: peer.NodeId,
1624 Address: peer.Address,
1625 IsPersisted: peer.IsPersisted,
1626 IsConnected: peer.IsConnected,
1627 })
1628 }
1629 return ret, nil
1630 }
1631
1632 func (ls *LDKService) GetNetworkGraph(ctx context.Context, nodeIds []string) (lnclient.NetworkGraphResponse, error) {
1633 graph := ls.node.NetworkGraph()
1634
1635 type NodeInfoWithId struct {
1636 Node *ldk_node.NodeInfo `json:"node"`
1637 NodeId string `json:"nodeId"`
1638 }
1639
1640 nodes := []NodeInfoWithId{}
1641 channels := []*ldk_node.ChannelInfo{}
1642 for _, nodeId := range nodeIds {
1643 _, err := hex.DecodeString(nodeId)
1644 if err != nil {
1645 return nil, err
1646 }
1647 if len(nodeId) != 66 {
1648 return nil, errors.New("unexpected node ID length")
1649 }
1650 graphNode := graph.Node(nodeId)
1651 if graphNode != nil {
1652 nodes = append(nodes, NodeInfoWithId{
1653 Node: graphNode,
1654 NodeId: nodeId,
1655 })
1656 if graphNode.Channels != nil {
1657 for _, channelId := range graphNode.Channels {
1658 graphChannel := graph.Channel(channelId)
1659 if graphChannel != nil {
1660 channels = append(channels, graphChannel)
1661 }
1662 }
1663 }
1664 }
1665 }
1666
1667 networkGraph := map[string]interface{}{
1668 "nodes": nodes,
1669 "channels": channels,
1670 }
1671 return networkGraph, nil
1672 }
1673
1674 func (ls *LDKService) GetLogOutput(ctx context.Context, maxLen int) ([]byte, error) {
1675 return []byte("Node logs are now included in application logs"), nil
1676 }
1677
1678 func (ls *LDKService) handleLdkEvent(event *ldk_node.Event) {
1679 logger.Logger.WithFields(logrus.Fields{
1680 "event": event,
1681 }).Info("Received LDK event")
1682
1683 switch eventType := (*event).(type) {
1684 case ldk_node.EventChannelReady:
1685 channels := ls.node.ListChannels()
1686 channelIndex := slices.IndexFunc(channels, func(c ldk_node.ChannelDetails) bool {
1687 return c.ChannelId == eventType.ChannelId
1688 })
1689 if channelIndex == -1 {
1690 logger.Logger.WithField("event", eventType).Error("Failed to find channel by ID")
1691 return
1692 }
1693
1694 channel := channels[channelIndex]
1695
1696 // assume it's a JIT channel if the channel peer matches
1697 // checking outbound capacity doesn't work (outbound capacity can be initially 0)
1698 isJit := !channel.IsOutbound && ls.lsps2Pubkey != "" && *eventType.CounterpartyNodeId == ls.lsps2Pubkey
1699
1700 isTrusted := eventType.CounterpartyNodeId != nil &&
1701 (slices.Contains(ls.node.Config().AnchorChannelsConfig.TrustedPeersNoReserve, *eventType.CounterpartyNodeId) || isJit)
1702
1703 ls.eventPublisher.Publish(&events.Event{
1704 Event: "nwc_channel_ready",
1705 Properties: map[string]interface{}{
1706 "counterparty_node_id": eventType.CounterpartyNodeId,
1707 "node_type": config.LDKBackendType,
1708 "public": channel.IsAnnounced,
1709 "jit": isJit,
1710 "capacity": channel.ChannelValueSats,
1711 "is_outbound": channel.IsOutbound,
1712 "trusted": isTrusted,
1713 },
1714 })
1715
1716 ls.backupChannels()
1717
1718 if eventType.CounterpartyNodeId == nil {
1719 logger.Logger.WithField("event", eventType).Error("channel ready event has no counterparty node ID")
1720 return
1721 }
1722
1723 maxDustHtlcExposureFromFeeRateMultiplier := uint64(0)
1724 if isTrusted {
1725 // avoid closures like "ProcessingError: Peer sent update_fee with a feerate (62500)
1726 // which may over-expose us to dust-in-flight on our counterparty's transactions (totaling 69348000 msat)"
1727 maxDustHtlcExposureFromFeeRateMultiplier = 100_000 // default * 10
1728 }
1729
1730 // set a super-high forwarding fee of 100K sats by default to disable unwanted routing by default
1731 forwardingFeeBaseMsat := uint32(100_000_000)
1732
1733 err := ls.UpdateChannel(context.Background(), &lnclient.UpdateChannelRequest{
1734 ChannelId: eventType.UserChannelId,
1735 NodeId: *eventType.CounterpartyNodeId,
1736 MaxDustHtlcExposureFromFeeRateMultiplier: maxDustHtlcExposureFromFeeRateMultiplier,
1737 ForwardingFeeBaseMsat: forwardingFeeBaseMsat,
1738 })
1739
1740 if err != nil {
1741 logger.Logger.WithField("event", eventType).Error("channel ready event has no counterparty node ID")
1742 return
1743 }
1744
1745 case ldk_node.EventChannelClosed:
1746 // make sure we do a sync after receiving a channel closed event
1747 ls.lastWalletSyncRequest = time.Now()
1748
1749 closureReason := ls.getChannelCloseReason(&eventType)
1750 logger.Logger.WithFields(logrus.Fields{
1751 "event": event,
1752 "reason": closureReason,
1753 }).Info("Channel closed")
1754 onchainBalance, err := ls.GetOnchainBalance(context.Background())
1755 if err != nil {
1756 logger.Logger.WithError(err).Error("failed to retrieve on-chain balance when closing channel")
1757 }
1758 var pendingBalance uint64
1759 var fundingTxId string
1760 var fundingTxVout uint32
1761 var fundingTxUrl string
1762
1763 if onchainBalance != nil {
1764 logger.Logger.WithField("onchain_balance", onchainBalance).Info("got on-chain balance when closing channel")
1765
1766 for _, details := range onchainBalance.PendingBalancesDetails {
1767 if details.ChannelId == eventType.ChannelId {
1768 fundingTxId = details.FundingTxId
1769 fundingTxVout = details.FundingTxVout
1770 fundingTxUrl = fmt.Sprintf("https://mempool.space/tx/%s#flow=&vout=%d", fundingTxId, fundingTxVout)
1771 pendingBalance += details.AmountSat
1772 }
1773 }
1774 for _, details := range onchainBalance.PendingSweepBalancesDetails {
1775 if details.ChannelId == eventType.ChannelId {
1776 fundingTxId = details.FundingTxId
1777 fundingTxVout = details.FundingTxVout
1778 fundingTxUrl = fmt.Sprintf("https://mempool.space/tx/%s#flow=&vout=%d", fundingTxId, fundingTxVout)
1779 pendingBalance += details.AmountSat
1780 }
1781 }
1782 }
1783
1784 var counterpartyNodeId string
1785 var counterpartyNodeUrl string
1786 if eventType.CounterpartyNodeId != nil {
1787 counterpartyNodeId = *eventType.CounterpartyNodeId
1788 counterpartyNodeUrl = "https://amboss.space/node/" + counterpartyNodeId
1789 }
1790
1791 ls.eventPublisher.Publish(&events.Event{
1792 Event: "nwc_channel_closed",
1793 Properties: map[string]interface{}{
1794 "counterparty_node_id": counterpartyNodeId,
1795 "counterparty_node_url": counterpartyNodeUrl,
1796 "reason": closureReason,
1797 "node_type": config.LDKBackendType,
1798 "pending_balance": pendingBalance,
1799 "funding_tx_id": fundingTxId,
1800 "funding_tx_vout": fundingTxVout,
1801 "funding_tx_url": fundingTxUrl,
1802 },
1803 })
1804 case ldk_node.EventPaymentReceived:
1805 if eventType.PaymentId == nil {
1806 logger.Logger.WithField("payment_hash", eventType.PaymentHash).Error("payment received event has no payment ID")
1807 return
1808 }
1809 payment := ls.node.Payment(*eventType.PaymentId)
1810 if payment == nil {
1811 logger.Logger.WithField("payment_id", *eventType.PaymentId).Error("could not find LDK payment")
1812 return
1813 }
1814
1815 transaction, err := ls.ldkPaymentToTransaction(payment)
1816 if err != nil {
1817 logger.Logger.WithField("payment_id", *eventType.PaymentId).Error("failed to convert LDK payment to transaction")
1818 return
1819 }
1820
1821 ls.eventPublisher.Publish(&events.Event{
1822 Event: "nwc_lnclient_payment_received",
1823 Properties: transaction,
1824 })
1825 case ldk_node.EventPaymentSuccessful:
1826 if eventType.PaymentId == nil {
1827 logger.Logger.WithField("payment_hash", eventType.PaymentHash).Error("payment received event has no payment ID")
1828 return
1829 }
1830 payment := ls.node.Payment(*eventType.PaymentId)
1831 if payment == nil {
1832 logger.Logger.WithField("payment_id", *eventType.PaymentId).Error("could not find LDK payment")
1833 return
1834 }
1835
1836 transaction, err := ls.ldkPaymentToTransaction(payment)
1837 if err != nil {
1838 logger.Logger.WithField("payment_id", *eventType.PaymentId).Error("failed to convert LDK payment to transaction")
1839 return
1840 }
1841
1842 ls.eventPublisher.Publish(&events.Event{
1843 Event: "nwc_lnclient_payment_sent",
1844 Properties: transaction,
1845 })
1846 case ldk_node.EventPaymentFailed:
1847 if eventType.PaymentId == nil {
1848 logger.Logger.WithField("payment_hash", eventType.PaymentHash).Error("payment failed event has no payment ID")
1849 return
1850 }
1851 payment := ls.node.Payment(*eventType.PaymentId)
1852 if payment == nil {
1853 logger.Logger.WithField("payment_id", *eventType.PaymentId).Error("could not find LDK payment")
1854 return
1855 }
1856
1857 transaction, err := ls.ldkPaymentToTransaction(payment)
1858 if err != nil {
1859 logger.Logger.WithField("payment_id", *eventType.PaymentId).Error("failed to convert LDK payment to transaction")
1860 return
1861 }
1862
1863 reason := ls.getPaymentFailReason(&eventType)
1864
1865 ls.eventPublisher.Publish(&events.Event{
1866 Event: "nwc_lnclient_payment_failed",
1867 Properties: &lnclient.PaymentFailedEventProperties{
1868 Transaction: transaction,
1869 Reason: reason,
1870 },
1871 })
1872 case ldk_node.EventPaymentForwarded:
1873 logger.Logger.WithFields(logrus.Fields{
1874 "total_fee_earned_msat": eventType.TotalFeeEarnedMsat,
1875 "outbound_amount_forwarded_msat": eventType.OutboundAmountForwardedMsat,
1876 }).Info("LDK Payment forwarded")
1877 if eventType.TotalFeeEarnedMsat == nil || eventType.OutboundAmountForwardedMsat == nil {
1878 logger.Logger.WithFields(logrus.Fields{
1879 "earned_msat": eventType.TotalFeeEarnedMsat,
1880 "outbound_amount_forwarded_msat": eventType.OutboundAmountForwardedMsat,
1881 }).Error("forwarded payment has missing required fields")
1882 return
1883 }
1884 ls.eventPublisher.Publish(&events.Event{
1885 Event: "nwc_payment_forwarded",
1886 Properties: &lnclient.PaymentForwardedEventProperties{
1887 TotalFeeEarnedMsat: *eventType.TotalFeeEarnedMsat,
1888 OutboundAmountForwardedMsat: *eventType.OutboundAmountForwardedMsat,
1889 },
1890 })
1891
1892 case ldk_node.EventPaymentClaimable:
1893 if eventType.ClaimDeadline == nil {
1894 logger.Logger.WithField("payment_id", eventType.PaymentId).Error("claimable payment has no claim deadline")
1895 return
1896 }
1897
1898 logger.Logger.WithFields(logrus.Fields{
1899 "claimable_amount_msats": eventType.ClaimableAmountMsat,
1900 "payment_hash": eventType.PaymentHash,
1901 "claim_deadline": *eventType.ClaimDeadline,
1902 }).Info("LDK Payment Claimable")
1903
1904 payment := ls.node.Payment(eventType.PaymentId)
1905 if payment == nil {
1906 logger.Logger.WithField("payment_id", eventType.PaymentId).Error("could not find LDK payment")
1907 return
1908 }
1909
1910 transaction, err := ls.ldkPaymentToTransaction(payment)
1911 if err != nil {
1912 logger.Logger.WithField("payment_id", eventType.PaymentId).Error("failed to convert LDK payment to transaction")
1913 return
1914 }
1915 transaction.SettleDeadline = eventType.ClaimDeadline
1916 ls.eventPublisher.Publish(&events.Event{
1917 Event: "nwc_lnclient_hold_invoice_accepted",
1918 Properties: transaction,
1919 })
1920 }
1921 }
1922
1923 func (ls *LDKService) backupChannels() {
1924 ldkChannels := ls.node.ListChannels()
1925 ldkPeers := ls.node.ListPeers()
1926 channels := make([]events.ChannelBackup, 0, len(ldkChannels))
1927 for _, ldkChannel := range ldkChannels {
1928 var fundingTxId string
1929 var fundingTxVout uint32
1930 if ldkChannel.FundingTxo != nil {
1931 fundingTxId = ldkChannel.FundingTxo.Txid
1932 fundingTxVout = ldkChannel.FundingTxo.Vout
1933 }
1934
1935 var peer *ldk_node.PeerDetails
1936 for _, matchingPeer := range ldkPeers {
1937 if matchingPeer.NodeId == ldkChannel.CounterpartyNodeId {
1938 peer = &matchingPeer
1939 }
1940 }
1941 if peer == nil {
1942 logger.Logger.WithField("peer_id", ldkChannel.CounterpartyNodeId).Error("failed to find peer for channel")
1943 continue
1944 }
1945
1946 channels = append(channels, events.ChannelBackup{
1947 ChannelID: ldkChannel.ChannelId,
1948 PeerID: ldkChannel.CounterpartyNodeId,
1949 PeerSocketAddress: peer.Address,
1950 ChannelSize: ldkChannel.ChannelValueSats,
1951 FundingTxID: fundingTxId,
1952 FundingTxVout: fundingTxVout,
1953 })
1954 }
1955
1956 monitors, err := ls.node.GetEncodedChannelMonitors()
1957 if err != nil {
1958 logger.Logger.WithError(err).Error("Failed to list channel monitors")
1959 return
1960 }
1961 encodedMonitors := []events.EncodedChannelMonitorBackup{}
1962
1963 for _, monitor := range monitors {
1964 encodedMonitors = append(encodedMonitors, events.EncodedChannelMonitorBackup{
1965 Key: monitor.Key,
1966 Value: hex.EncodeToString(monitor.Value),
1967 })
1968 }
1969
1970 event := &events.StaticChannelsBackupEvent{
1971 Channels: channels,
1972 Monitors: encodedMonitors,
1973 NodeID: ls.node.NodeId(),
1974 }
1975
1976 ls.saveStaticChannelBackupToDisk(event)
1977
1978 ls.eventPublisher.Publish(&events.Event{
1979 Event: "nwc_backup_channels",
1980 Properties: event,
1981 })
1982 }
1983
1984 func (ls *LDKService) saveStaticChannelBackupToDisk(event *events.StaticChannelsBackupEvent) {
1985 backupDirectory := filepath.Join(ls.workdir, "static_channel_backups")
1986 err := os.MkdirAll(backupDirectory, os.ModePerm)
1987 if err != nil {
1988 logger.Logger.WithError(err).Error("Failed to make static channel backup directory")
1989 return
1990 }
1991
1992 backupFilePath := filepath.Join(backupDirectory, time.Now().Format("2006-01-02T15-04-05")+".json")
1993 eventBytes, err := json.Marshal(event)
1994 if err != nil {
1995 logger.Logger.WithError(err).Error("Failed to serialize static channel backup to json")
1996 return
1997 }
1998 err = os.WriteFile(backupFilePath, eventBytes, 0644)
1999 if err != nil {
2000 logger.Logger.WithError(err).Error("Failed to write static channel backup to disk")
2001 return
2002 }
2003 logger.Logger.WithField("backupPath", backupFilePath).Debug("Saved static channel backup to disk")
2004 }
2005
2006 func (ls *LDKService) GetBalances(ctx context.Context, includeInactiveChannels bool) (*lnclient.BalancesResponse, error) {
2007 onchainBalance, err := ls.GetOnchainBalance(ctx)
2008 if err != nil {
2009 logger.Logger.WithError(err).Error("Failed to retrieve onchain balance")
2010 return nil, err
2011 }
2012
2013 var totalReceivable int64 = 0
2014 var totalSpendable int64 = 0
2015 var nextMaxReceivable int64 = 0
2016 var nextMaxSpendable int64 = 0
2017 var nextMaxReceivableMPP int64 = 0
2018 var nextMaxSpendableMPP int64 = 0
2019 channels := ls.node.ListChannels()
2020 for _, channel := range channels {
2021 if channel.IsUsable || includeInactiveChannels {
2022 // spending or receiving amount may be constrained by channel configuration (e.g. ACINQ does this)
2023 channelConstrainedSpendable := min(int64(channel.OutboundCapacityMsat), int64(*channel.CounterpartyOutboundHtlcMaximumMsat))
2024 channelConstrainedReceivable := min(int64(channel.InboundCapacityMsat), int64(*channel.InboundHtlcMaximumMsat))
2025
2026 nextMaxSpendable = max(nextMaxSpendable, channelConstrainedSpendable)
2027 nextMaxReceivable = max(nextMaxReceivable, channelConstrainedReceivable)
2028
2029 nextMaxSpendableMPP += channelConstrainedSpendable
2030 nextMaxReceivableMPP += channelConstrainedReceivable
2031
2032 // these are what the wallet can send and receive, but not necessarily in one go
2033 totalSpendable += int64(channel.OutboundCapacityMsat)
2034 totalReceivable += int64(channel.InboundCapacityMsat)
2035 }
2036 }
2037
2038 return &lnclient.BalancesResponse{
2039 Onchain: *onchainBalance,
2040 Lightning: lnclient.LightningBalanceResponse{
2041 TotalSpendableMsat: totalSpendable,
2042 TotalReceivableMsat: totalReceivable,
2043 NextMaxSpendableMsat: nextMaxSpendable,
2044 NextMaxReceivableMsat: nextMaxReceivable,
2045 NextMaxSpendableMPPMsat: nextMaxSpendableMPP,
2046 NextMaxReceivableMPPMsat: nextMaxReceivableMPP,
2047 },
2048 }, nil
2049 }
2050
2051 func (ls *LDKService) GetStorageDir() (string, error) {
2052 // Note: the below will return the path including the WORK_DIR which is harder to use,
2053 // so for now we just return a hardcoded value.
2054 // cfg := ls.node.Config()
2055 // return cfg.StorageDirPath, nil
2056 return "ldk/storage", nil
2057 }
2058
2059 func (ls *LDKService) deleteOldLDKPayments() {
2060 payments := ls.node.ListPayments()
2061
2062 now := time.Now()
2063 for _, payment := range payments {
2064 paymentCreatedAt := time.Unix(int64(payment.CreatedAt), 0)
2065
2066 deletablePaymentKind := false
2067 switch (payment.Kind).(type) {
2068 case ldk_node.PaymentKindBolt11:
2069 deletablePaymentKind = true
2070 case ldk_node.PaymentKindBolt11Jit:
2071 deletablePaymentKind = true
2072 case ldk_node.PaymentKindSpontaneous:
2073 deletablePaymentKind = true
2074 }
2075 if !deletablePaymentKind {
2076 logger.Logger.WithFields(logrus.Fields{
2077 "created_at": paymentCreatedAt,
2078 "payment_id": payment.Id,
2079 }).Debug("Skipping undeletable payment kind")
2080 continue
2081 }
2082
2083 if paymentCreatedAt.Add(maxInvoiceExpiry).Before(now) {
2084 logger.Logger.WithFields(logrus.Fields{
2085 "created_at": paymentCreatedAt,
2086 "payment_id": payment.Id,
2087 }).Debug("Deleting old payment")
2088 err := ls.node.RemovePayment(payment.Id)
2089 if err != nil {
2090 logger.Logger.WithError(err).WithField("id", payment.Id).Error("failed to delete old payment")
2091 }
2092 }
2093 }
2094 }
2095
2096 func deleteOldLDKLogs(ldkLogDir string) {
2097 logger.Logger.WithField("ldkLogDir", ldkLogDir).Debug("Deleting old LDK logs")
2098 files, err := os.ReadDir(ldkLogDir)
2099 if err != nil {
2100 if errors.Is(err, os.ErrNotExist) {
2101 // no log file directory - expected when VSS is enabled
2102 return
2103 }
2104 logger.Logger.WithField("path", ldkLogDir).WithError(err).Error("Failed to list ldk log directory")
2105 return
2106 }
2107
2108 for _, file := range files {
2109 // get files with a date (e.g. ldk_node_2024_03_29.log)
2110 if strings.HasPrefix(file.Name(), "ldk_node_2") && strings.HasSuffix(file.Name(), ".log") {
2111 filePath := filepath.Join(ldkLogDir, file.Name())
2112 fileInfo, err := file.Info()
2113 if err != nil {
2114 logger.Logger.WithField("filePath", filePath).WithError(err).Error("Failed to get file info")
2115 continue
2116 }
2117 // delete files last modified over 3 days ago
2118 if fileInfo.ModTime().Before(time.Now().AddDate(0, 0, -3)) {
2119 err := os.Remove(filePath)
2120 if err != nil {
2121 logger.Logger.WithField("filePath", filePath).WithError(err).Error("Failed to get file info")
2122 continue
2123 }
2124 logger.Logger.WithField("filePath", filePath).Info("Deleted old LDK log file")
2125 }
2126 }
2127 }
2128 }
2129
2130 func (ls *LDKService) GetNodeStatus(ctx context.Context) (nodeStatus *lnclient.NodeStatus, err error) {
2131 status := ls.node.Status()
2132 return &lnclient.NodeStatus{
2133 IsReady: status.IsRunning,
2134 InternalNodeStatus: status,
2135 }, nil
2136 }
2137
2138 func (ls *LDKService) DisconnectPeer(ctx context.Context, peerId string) error {
2139 return ls.node.Disconnect(peerId)
2140 }
2141
2142 func (ls *LDKService) UpdateLastWalletSyncRequest() {
2143 ls.lastWalletSyncRequest = time.Now()
2144 }
2145
2146 func (ls *LDKService) GetSupportedNIP47Methods() []string {
2147 return []string{
2148 models.PAY_INVOICE_METHOD,
2149 models.PAY_KEYSEND_METHOD,
2150 models.GET_BALANCE_METHOD,
2151 models.GET_BUDGET_METHOD,
2152 models.GET_INFO_METHOD,
2153 models.MAKE_INVOICE_METHOD,
2154 models.LOOKUP_INVOICE_METHOD,
2155 models.LIST_TRANSACTIONS_METHOD,
2156 models.MULTI_PAY_INVOICE_METHOD,
2157 models.MULTI_PAY_KEYSEND_METHOD,
2158 models.SIGN_MESSAGE_METHOD,
2159 models.MAKE_HOLD_INVOICE_METHOD,
2160 models.SETTLE_HOLD_INVOICE_METHOD,
2161 models.CANCEL_HOLD_INVOICE_METHOD,
2162 }
2163 }
2164
2165 func (ls *LDKService) GetSupportedNIP47NotificationTypes() []string {
2166 return []string{
2167 notifications.PAYMENT_RECEIVED_NOTIFICATION,
2168 notifications.PAYMENT_SENT_NOTIFICATION,
2169 notifications.HOLD_INVOICE_ACCEPTED_NOTIFICATION,
2170 }
2171 }
2172
2173 func (ls *LDKService) getPaymentFailReason(eventPaymentFailed *ldk_node.EventPaymentFailed) string {
2174 var failureReason ldk_node.PaymentFailureReason
2175 var failureReasonMessage string
2176 if eventPaymentFailed.Reason != nil {
2177 failureReason = *eventPaymentFailed.Reason
2178 }
2179 switch failureReason {
2180 case ldk_node.PaymentFailureReasonRecipientRejected:
2181 failureReasonMessage = "RecipientRejected"
2182 case ldk_node.PaymentFailureReasonUserAbandoned:
2183 failureReasonMessage = "UserAbandoned"
2184 case ldk_node.PaymentFailureReasonRetriesExhausted:
2185 failureReasonMessage = "RetriesExhausted"
2186 case ldk_node.PaymentFailureReasonPaymentExpired:
2187 failureReasonMessage = "PaymentExpired"
2188 case ldk_node.PaymentFailureReasonRouteNotFound:
2189 failureReasonMessage = "RouteNotFound"
2190 case ldk_node.PaymentFailureReasonUnexpectedError:
2191 failureReasonMessage = "UnexpectedError"
2192 case ldk_node.PaymentFailureReasonUnknownRequiredFeatures:
2193 failureReasonMessage = "UnknownRequiredFeatures"
2194 case ldk_node.PaymentFailureReasonInvoiceRequestExpired:
2195 failureReasonMessage = "InvoiceRequestExpired"
2196 case ldk_node.PaymentFailureReasonInvoiceRequestRejected:
2197 failureReasonMessage = "InvoiceRequestRejected"
2198 case ldk_node.PaymentFailureReasonBlindedPathCreationFailed:
2199 failureReasonMessage = "BlindedPathCreationFailed"
2200 default:
2201 failureReasonMessage = "UnknownError"
2202 }
2203 return failureReasonMessage
2204 }
2205
2206 func (ls *LDKService) getChannelCloseReason(event *ldk_node.EventChannelClosed) string {
2207 var reason string
2208
2209 switch reasonType := (*event.Reason).(type) {
2210 case ldk_node.ClosureReasonCounterpartyForceClosed:
2211 reason = fmt.Sprintf("CounterpartyForceClosed (Peer message: %s)", reasonType.PeerMsg)
2212 case ldk_node.ClosureReasonHolderForceClosed:
2213 reason = "HolderForceClosed"
2214 case ldk_node.ClosureReasonLegacyCooperativeClosure:
2215 reason = "LegacyCooperativeClosure"
2216 case ldk_node.ClosureReasonCounterpartyInitiatedCooperativeClosure:
2217 reason = "CounterpartyInitiatedCooperativeClosure"
2218 case ldk_node.ClosureReasonLocallyInitiatedCooperativeClosure:
2219 reason = "LocallyInitiatedCooperativeClosure"
2220 case ldk_node.ClosureReasonCommitmentTxConfirmed:
2221 reason = "CommitmentTxConfirmed"
2222 case ldk_node.ClosureReasonFundingTimedOut:
2223 reason = "FundingTimedOut"
2224 case ldk_node.ClosureReasonProcessingError:
2225 reason = fmt.Sprintf("ProcessingError: %s", reasonType.Err)
2226 case ldk_node.ClosureReasonDisconnectedPeer:
2227 reason = "DisconnectedPeer"
2228 case ldk_node.ClosureReasonOutdatedChannelManager:
2229 reason = "OutdatedChannelManager"
2230 case ldk_node.ClosureReasonCounterpartyCoopClosedUnfundedChannel:
2231 reason = "CounterpartyCoopClosedUnfundedChannel"
2232 case ldk_node.ClosureReasonFundingBatchClosure:
2233 reason = "FundingBatchClosure"
2234 case ldk_node.ClosureReasonHtlCsTimedOut:
2235 reason = "HTLCsTimedOut"
2236 default:
2237 reason = fmt.Sprintf("Unknown: %s", *event.Reason)
2238 }
2239
2240 return reason
2241 }
2242
2243 func (ls *LDKService) GetPubkey() string {
2244 return ls.pubkey
2245 }
2246
2247 func (ls *LDKService) PayOfferSync(ctx context.Context, offer string, amount uint64, payerNote string) (*lnclient.PayOfferResponse, error) {
2248 // TODO: this is only for testing MakeOffer and needs improvements
2249 // (+ BOLT-12 payments need to go through transactions service)
2250 // TODO: send liquidity event if amount too large
2251 offerObj, err := ldk_node.OfferFromStr(offer)
2252 if err != nil {
2253 return nil, err
2254 }
2255
2256 paymentStart := time.Now()
2257 ldkEventSubscription := ls.ldkEventBroadcaster.Subscribe()
2258 defer ls.ldkEventBroadcaster.CancelSubscription(ldkEventSubscription)
2259
2260 // TODO: use normal send if no amount is provided
2261 // TODO: configure sending params to ensure fee reserve is used, etc.
2262 paymentId, err := ls.node.Bolt12Payment().SendUsingAmount(offerObj, amount, nil, &payerNote, nil)
2263 if err != nil {
2264 logger.Logger.WithError(err).Error("Failed to initiate BOLT-12 variable amount payment")
2265 return nil, errors.New("failed to initiate BOLT-12 variable amount payment")
2266 }
2267
2268 logger.Logger.WithFields(logrus.Fields{
2269 "payment_id": paymentId,
2270 }).Info("Initiated BOLT-12 variable amount payment")
2271
2272 feeMsat := uint64(0)
2273 preimage := ""
2274
2275 payment := ls.node.Payment(paymentId)
2276 if payment == nil {
2277 return nil, errors.New("payment not found by payment ID")
2278 }
2279
2280 paymentHash := ""
2281
2282 for start := time.Now(); time.Since(start) < time.Second*60; {
2283 event := <-ldkEventSubscription
2284
2285 eventPaymentSuccessful, isEventPaymentSuccessfulEvent := (*event).(ldk_node.EventPaymentSuccessful)
2286 eventPaymentFailed, isEventPaymentFailedEvent := (*event).(ldk_node.EventPaymentFailed)
2287
2288 if isEventPaymentSuccessfulEvent && eventPaymentSuccessful.PaymentId != nil && *eventPaymentSuccessful.PaymentId == paymentId {
2289 logger.Logger.Info("Got payment success event")
2290 payment := ls.node.Payment(paymentId)
2291 if payment == nil {
2292 logger.Logger.Errorf("Couldn't find payment by payment ID: %v", paymentId)
2293 return nil, errors.New("payment not found")
2294 }
2295
2296 bolt12PaymentKind, ok := payment.Kind.(ldk_node.PaymentKindBolt12Offer)
2297
2298 if !ok {
2299 logger.Logger.WithFields(logrus.Fields{
2300 "payment": payment,
2301 }).Error("Payment is not a BOLT-12 offer kind")
2302 return nil, errors.New("payment is not a BOLT-12 offer")
2303 }
2304
2305 if bolt12PaymentKind.Preimage == nil {
2306 logger.Logger.Errorf("No payment preimage for payment ID: %v", paymentId)
2307 return nil, errors.New("payment preimage not found")
2308 }
2309 preimage = *bolt12PaymentKind.Preimage
2310
2311 if bolt12PaymentKind.Hash == nil {
2312 logger.Logger.Errorf("No payment hash for payment ID: %v", paymentId)
2313 return nil, errors.New("payment hash not found")
2314 }
2315 paymentHash = *bolt12PaymentKind.Hash
2316
2317 if eventPaymentSuccessful.FeePaidMsat != nil {
2318 feeMsat = *eventPaymentSuccessful.FeePaidMsat
2319 }
2320 break
2321 }
2322 if isEventPaymentFailedEvent && eventPaymentFailed.PaymentId != nil && *eventPaymentFailed.PaymentId == paymentId {
2323 reason := ls.getPaymentFailReason(&eventPaymentFailed)
2324
2325 logger.Logger.WithFields(logrus.Fields{
2326 "payment_id": paymentId,
2327 "reason": reason,
2328 }).Error("Received payment failed event")
2329
2330 return nil, fmt.Errorf("received payment failed event: %s", reason)
2331 }
2332 }
2333
2334 logger.Logger.WithFields(logrus.Fields{
2335 "duration": time.Since(paymentStart).Milliseconds(),
2336 "feeMsat": feeMsat,
2337 }).Info("Successful BOLT-12 payment")
2338
2339 return &lnclient.PayOfferResponse{
2340 PaymentHash: paymentHash,
2341 Preimage: preimage,
2342 FeeMsat: feeMsat,
2343 }, nil
2344 }
2345
2346 const nodeCommandPayBOLT12Offer = "pay_bolt12_offer"
2347 const nodeCommandExportPathfindingScores = "export_pathfinding_scores"
2348 const nodeCommandListChannelMonitorSizes = "list_channel_monitor_sizes"
2349
2350 func (ls *LDKService) GetCustomNodeCommandDefinitions() []lnclient.CustomNodeCommandDef {
2351 return []lnclient.CustomNodeCommandDef{
2352 {
2353 Name: nodeCommandPayBOLT12Offer,
2354 Description: "Send payments to a BOLT-12 offer. NOTE: this is for testing only. Payment will not show in transaction list.",
2355 Args: []lnclient.CustomNodeCommandArgDef{
2356 {
2357 Name: "offer",
2358 Description: "BOLT-12 offer of receiver",
2359 },
2360 {
2361 Name: "amount",
2362 Description: "amount to send in millisats",
2363 },
2364 {
2365 Name: "payer_note",
2366 Description: "note to the recepient",
2367 },
2368 },
2369 },
2370 {
2371 Name: nodeCommandExportPathfindingScores,
2372 Description: "Exports pathfinding scores from the LDK node.",
2373 Args: []lnclient.CustomNodeCommandArgDef{}, // Assuming no arguments for now
2374 },
2375 {
2376 Name: nodeCommandListChannelMonitorSizes,
2377 Description: "List Channel Monitor sizes from the LDK node.",
2378 Args: []lnclient.CustomNodeCommandArgDef{},
2379 },
2380 }
2381 }
2382
2383 func (ls *LDKService) ExecuteCustomNodeCommand(ctx context.Context, command *lnclient.CustomNodeCommandRequest) (*lnclient.CustomNodeCommandResponse, error) {
2384 switch command.Name {
2385 case nodeCommandPayBOLT12Offer:
2386 var offer string
2387 var amount uint64
2388 var payerNote string
2389 var err error
2390 for i := range command.Args {
2391 switch command.Args[i].Name {
2392 case "offer":
2393 offer = command.Args[i].Value
2394 case "amount":
2395 amount, err = strconv.ParseUint(string(command.Args[i].Value), 10, 64)
2396 case "payer_note":
2397 payerNote = command.Args[i].Value
2398 }
2399 }
2400 if err != nil {
2401 return nil, err
2402 }
2403
2404 payOfferResponse, err := ls.PayOfferSync(ctx, offer, amount, payerNote)
2405
2406 if err != nil {
2407 return nil, err
2408 }
2409
2410 return &lnclient.CustomNodeCommandResponse{
2411 Response: map[string]interface{}{
2412 "paymentHash": payOfferResponse.PaymentHash,
2413 "preimage": payOfferResponse.Preimage,
2414 "feeMsat": payOfferResponse.FeeMsat,
2415 },
2416 }, nil
2417 case nodeCommandExportPathfindingScores:
2418 scores, err := ls.node.ExportPathfindingScores()
2419 if err != nil {
2420 logger.Logger.WithError(err).Error("ExportPathfindingScores command failed")
2421 return nil, fmt.Errorf("failed to export pathfinding scores: %w", err)
2422 }
2423 return &lnclient.CustomNodeCommandResponse{
2424 Response: map[string]interface{}{
2425 "scores": hex.EncodeToString(scores),
2426 },
2427 }, nil
2428 case nodeCommandListChannelMonitorSizes:
2429 channelMonitorSizes := ls.node.ListChannelMonitorSizes()
2430 channels := ls.node.ListChannels()
2431 type channelMonitorSizeResponse struct {
2432 SizeBytes uint64 `json:"sizeBytes"`
2433 RemotePubkey string `json:"remotePubkey"`
2434 HasWarning bool `json:"hasWarning"`
2435 }
2436 channelMonitorSizesResponse := []channelMonitorSizeResponse{}
2437 for _, channelMonitorSizeInfo := range channelMonitorSizes {
2438 for _, channel := range channels {
2439 if channel.ChannelId == channelMonitorSizeInfo.ChannelId {
2440 channelMonitorSizesResponse = append(channelMonitorSizesResponse, channelMonitorSizeResponse{
2441 SizeBytes: channelMonitorSizeInfo.SizeBytes,
2442 RemotePubkey: channel.CounterpartyNodeId,
2443 HasWarning: channelMonitorSizeInfo.SizeBytes >= ls.cfg.GetEnv().LDKChannelMonitorWarningSizeBytes,
2444 })
2445 }
2446 }
2447 }
2448
2449 return &lnclient.CustomNodeCommandResponse{
2450 Response: channelMonitorSizesResponse,
2451 }, nil
2452 }
2453
2454 return nil, lnclient.ErrUnknownCustomNodeCommand
2455 }
2456
2457 func (ls *LDKService) MakeHoldInvoice(ctx context.Context, amountMsat int64, description string, descriptionHash string, expirySeconds int64, paymentHash string, minCltvExpiryDelta *uint64) (*lnclient.Transaction, error) {
2458 if expirySeconds < 0 || expirySeconds > int64(maxInvoiceExpiry/time.Second) {
2459 return nil, errors.New("invalid invoice expiry")
2460 }
2461
2462 maxReceivable := ls.getMaxReceivable()
2463
2464 if amountMsat > maxReceivable {
2465 ls.eventPublisher.Publish(&events.Event{
2466 Event: "nwc_incoming_liquidity_required",
2467 Properties: map[string]interface{}{
2468 "node_type": config.LDKBackendType,
2469 },
2470 })
2471 }
2472
2473 if expirySeconds == 0 {
2474 expirySeconds = lnclient.DEFAULT_INVOICE_EXPIRY
2475 }
2476
2477 var descriptionType ldk_node.Bolt11InvoiceDescription
2478 descriptionType = ldk_node.Bolt11InvoiceDescriptionDirect{
2479 Description: description,
2480 }
2481 if description == "" && descriptionHash != "" {
2482 descriptionType = ldk_node.Bolt11InvoiceDescriptionHash{
2483 Hash: descriptionHash,
2484 }
2485 }
2486
2487 decodedPaymentHash, err := hex.DecodeString(paymentHash)
2488 if err != nil {
2489 logger.Logger.WithError(err).WithField("paymentHash", paymentHash).Error("Failed to decode payment hash for MakeHoldInvoice")
2490 return nil, fmt.Errorf("failed to decode payment hash: %w", err)
2491 }
2492 if len(decodedPaymentHash) != 32 {
2493 return nil, errors.New("payment hash must be 32 bytes")
2494 }
2495 var paymentHash32 [32]byte
2496 copy(paymentHash32[:], decodedPaymentHash)
2497
2498 ldkPaymentHash := ldk_node.PaymentHash(hex.EncodeToString(paymentHash32[:]))
2499
2500 var invoiceObj *ldk_node.Bolt11Invoice
2501 if minCltvExpiryDelta != nil {
2502 if *minCltvExpiryDelta > uint64(65535) {
2503 return nil, errors.New("min_cltv_expiry_delta must be <= 65535")
2504 }
2505 invoiceObj, err = ls.node.Bolt11Payment().ReceiveForHashWithMinCltvExpiryDelta(
2506 uint64(amountMsat),
2507 descriptionType,
2508 uint32(expirySeconds),
2509 ldkPaymentHash,
2510 uint16(*minCltvExpiryDelta),
2511 )
2512 } else {
2513 invoiceObj, err = ls.node.Bolt11Payment().ReceiveForHash(
2514 uint64(amountMsat),
2515 descriptionType,
2516 uint32(expirySeconds),
2517 ldkPaymentHash,
2518 )
2519 }
2520
2521 if err != nil {
2522 logger.Logger.WithError(err).Error("MakeHoldInvoice failed")
2523 return nil, err
2524 }
2525
2526 payment := ls.node.Payment(invoiceObj.PaymentHash())
2527 invoice := *payment.Kind.(ldk_node.PaymentKindBolt11).Bolt11Invoice
2528 paymentRequest, err := decodepay.Decodepay(invoice)
2529 if err != nil {
2530 logger.Logger.WithFields(logrus.Fields{
2531 "bolt11": invoice,
2532 }).WithError(err).Error("Failed to decode bolt11 invoice")
2533 return nil, err
2534 }
2535 expiresAtUnix := time.UnixMilli(int64(paymentRequest.CreatedAt) * 1000).Add(time.Duration(paymentRequest.Expiry) * time.Second).Unix()
2536
2537 transaction := &lnclient.Transaction{
2538 Type: "incoming",
2539 Invoice: *payment.Kind.(ldk_node.PaymentKindBolt11).Bolt11Invoice,
2540 PaymentHash: paymentRequest.PaymentHash,
2541 AmountMsat: amountMsat,
2542 CreatedAt: int64(payment.CreatedAt),
2543 ExpiresAt: &expiresAtUnix,
2544 Description: paymentRequest.Description,
2545 DescriptionHash: paymentRequest.DescriptionHash,
2546 }
2547
2548 return transaction, nil
2549 }
2550
2551 func (ls *LDKService) CancelHoldInvoice(ctx context.Context, paymentHash string) error {
2552 _, err := hex.DecodeString(paymentHash)
2553 if err != nil {
2554 logger.Logger.WithError(err).WithField("paymentHash", paymentHash).Error("Failed to decode payment hash for CancelHoldInvoice")
2555 return err
2556 }
2557
2558 err = ls.node.Bolt11Payment().FailForHash(paymentHash)
2559 if err != nil {
2560 logger.Logger.WithError(err).WithField("paymentHash", paymentHash).Error("CancelHoldInvoice failed")
2561 }
2562 return err
2563 }
2564
2565 func (ls *LDKService) SettleHoldInvoice(ctx context.Context, preimage string) error {
2566 decodedPreimage, err := hex.DecodeString(preimage)
2567 if err != nil {
2568 logger.Logger.WithError(err).WithField("preimage", preimage).Error("Failed to decode preimage for SettleHoldInvoice")
2569 return err
2570 }
2571 if len(decodedPreimage) != 32 {
2572 return errors.New("preimage must be 32 bytes")
2573 }
2574
2575 paymentHash256 := sha256.New()
2576 paymentHash256.Write(decodedPreimage)
2577 paymentHashBytes := paymentHash256.Sum(nil)
2578 paymentHash := hex.EncodeToString(paymentHashBytes)
2579
2580 paymentDetails := ls.node.Payment(paymentHash)
2581
2582 if paymentDetails == nil {
2583 logger.Logger.WithField("payment_hash", paymentHash).Error("SettleHoldInvoice: Could not find payment by derived hash")
2584 return errors.New("payment not found for derived hash")
2585 }
2586 if paymentDetails.AmountMsat == nil {
2587 logger.Logger.WithField("payment_hash", paymentHash).Error("SettleHoldInvoice: Payment has no amount_msat")
2588 return errors.New("payment has no amount_msat")
2589 }
2590
2591 err = ls.node.Bolt11Payment().ClaimForHash(paymentHash, *paymentDetails.AmountMsat, preimage)
2592 if err != nil {
2593 logger.Logger.WithError(err).WithField("preimage", preimage).WithField("derived_payment_hash", paymentHash).Error("SettleHoldInvoice failed")
2594 }
2595 return err
2596 }
2597
2598 func GetVssNodeIdentifier(keys keys.Keys) (string, error) {
2599 key, err := keys.DeriveKey([]uint32{bip32.FirstHardenedChild + 2})
2600
2601 if err != nil {
2602 return "", err
2603 }
2604
2605 // return a 6-character hex string of the hash of a derived key to ensure if same user
2606 // runs multiple hubs with different mnemonics, they are all
2607 // saved in the VSS under different user_tokens.
2608 pubkeyHash256 := sha256.New()
2609 pubkeyHash256.Write(key.Key)
2610 pubkeyHashBytes := pubkeyHash256.Sum(nil)
2611 return hex.EncodeToString(pubkeyHashBytes[0:3]), nil
2612 }
2613
2614 func getResetStateRequest(cfg config.Config) *ldk_node.ResetState {
2615 resetKey, err := cfg.Get(resetRouterKey, "")
2616 if err != nil {
2617 logger.Logger.Error("Failed to retrieve ResetRouter key")
2618 return nil
2619 }
2620
2621 if resetKey == "" {
2622 return nil
2623 }
2624
2625 err = cfg.SetUpdate(resetRouterKey, "", "")
2626 if err != nil {
2627 logger.Logger.WithError(err).Error("Failed to remove reset router key")
2628 return nil
2629 }
2630
2631 var ret ldk_node.ResetState
2632
2633 switch resetKey {
2634 case "ALL":
2635 ret = ldk_node.ResetStateAll
2636 case "Scorer":
2637 ret = ldk_node.ResetStateScorer
2638 case "NetworkGraph":
2639 ret = ldk_node.ResetStateNetworkGraph
2640 case "NodeMetrics":
2641 ret = ldk_node.ResetStateNodeMetrics
2642 default:
2643 logger.Logger.WithField("key", resetKey).Error("Unknown reset router key")
2644 return nil
2645 }
2646
2647 return &ret
2648 }
2649
2650 func (ls *LDKService) ConsumeEvent(ctx context.Context, event *events.Event, globalProperties map[string]interface{}) {
2651 if event.Event == "nwc_alby_account_connected" {
2652 // backup existing channels to the user's Alby Account on first connect
2653 ls.backupChannels()
2654 }
2655 }
2656
2657 func (ls *LDKService) GetChainDataSource() (string, string) {
2658 if endpoint := ls.cfg.GetEnv().LDKBitcoindRpcHost; endpoint != "" {
2659 rpcPort := ls.cfg.GetEnv().LDKBitcoindRpcPort
2660 return "bitcoind", sanitizeChainEndpoint(endpoint, rpcPort)
2661 }
2662 if endpoint := ls.cfg.GetEnv().LDKElectrumServer; endpoint != "" {
2663 return "electrum", sanitizeChainEndpoint(endpoint, "")
2664 }
2665
2666 // Fallback to Esplora
2667 endpoint := ls.cfg.GetEnv().LDKEsploraServer
2668 return "esplora", sanitizeChainEndpoint(endpoint, "")
2669 }
2670
2671 func (ls *LDKService) GetLiquiditySourceLsps2() string {
2672 if ls.lsps2Pubkey == "" || ls.lsps2Address == "" {
2673 return ""
2674 }
2675 return fmt.Sprintf("%s@%s", ls.lsps2Pubkey, ls.lsps2Address)
2676 }
2677
2678 func (ls *LDKService) GetLiquiditySourceLsps2MinPaymentSizeMsat() *uint64 {
2679 ls.fetchLsps2OpeningFeeParams(lsps2InfoCacheTTL)
2680
2681 ls.lsps2InfoMu.Lock()
2682 defer ls.lsps2InfoMu.Unlock()
2683
2684 return ls.lsps2MinPaymentSizeMsat
2685 }
2686
2687 func (ls *LDKService) GetLiquiditySourceLsps2MaxPaymentSizeMsat() *uint64 {
2688 ls.fetchLsps2OpeningFeeParams(lsps2InfoCacheTTL)
2689
2690 ls.lsps2InfoMu.Lock()
2691 defer ls.lsps2InfoMu.Unlock()
2692
2693 return ls.lsps2MaxPaymentSizeMsat
2694 }
2695
2696 // getLsps2MaxTotalOpeningFeeMsat returns the maximum opening fee to accept
2697 // for a JIT channel invoice of the given payment size, derived from the
2698 // LSP's advertised opening fee menu and an absolute ceiling.
2699 func (ls *LDKService) getLsps2MaxTotalOpeningFeeMsat(paymentSizeMsat uint64) uint64 {
2700 ls.fetchLsps2OpeningFeeParams(lsps2FeeCapCacheTTL)
2701
2702 ls.lsps2InfoMu.Lock()
2703 defer ls.lsps2InfoMu.Unlock()
2704
2705 return computeLsps2MaxTotalOpeningFeeMsat(paymentSizeMsat, ls.lsps2OpeningFeeParamsMenu)
2706 }
2707
2708 func (ls *LDKService) fetchLsps2OpeningFeeParams(maxCacheAge time.Duration) {
2709 if ls.lsps2Pubkey == "" || ls.lsps2Address == "" {
2710 return
2711 }
2712
2713 ls.lsps2InfoMu.Lock()
2714 defer ls.lsps2InfoMu.Unlock()
2715
2716 if !ls.lsps2InfoFetchedAt.IsZero() && time.Since(ls.lsps2InfoFetchedAt) < maxCacheAge {
2717 return
2718 }
2719
2720 response, err := ls.node.Lsps2Liquidity().RequestOpeningFeeParams()
2721 if err != nil {
2722 logger.Logger.WithError(err).Warn("Failed to fetch LSPS2 opening fee params")
2723 return
2724 }
2725
2726 var minPaymentSizeMsat *uint64
2727 var maxPaymentSizeMsat *uint64
2728 for _, params := range response.OpeningFeeParamsMenu {
2729 effectiveMinPaymentSizeMsat, ok := computeLsps2MinPaymentSizeMsat(params)
2730 if !ok {
2731 continue
2732 }
2733 if minPaymentSizeMsat == nil || effectiveMinPaymentSizeMsat < *minPaymentSizeMsat {
2734 value := effectiveMinPaymentSizeMsat
2735 minPaymentSizeMsat = &value
2736 }
2737 if maxPaymentSizeMsat == nil || params.MaxPaymentSizeMsat > *maxPaymentSizeMsat {
2738 value := params.MaxPaymentSizeMsat
2739 maxPaymentSizeMsat = &value
2740 }
2741 }
2742
2743 ls.lsps2MinPaymentSizeMsat = minPaymentSizeMsat
2744 ls.lsps2MaxPaymentSizeMsat = maxPaymentSizeMsat
2745 ls.lsps2OpeningFeeParamsMenu = response.OpeningFeeParamsMenu
2746 ls.lsps2InfoFetchedAt = time.Now()
2747 }
2748
2749 // computeLsps2MaxTotalOpeningFeeMsat returns the maximum LSPS2 opening fee to
2750 // accept for a payment of the given size: the highest fee the advertised fee
2751 // menu allows for that size, further limited by the absolute fee ceiling. The
2752 // ceiling alone is used when no menu entry covers the payment size.
2753 func computeLsps2MaxTotalOpeningFeeMsat(paymentSizeMsat uint64, menu []ldk_node.Lsps2OpeningFeeParams) uint64 {
2754 maxAcceptableFeeMsat := lsps2MaxAcceptableOpeningFeeMsat(paymentSizeMsat)
2755
2756 var menuMaxFeeMsat *uint64
2757 for _, params := range menu {
2758 if paymentSizeMsat < params.MinPaymentSizeMsat || paymentSizeMsat > params.MaxPaymentSizeMsat {
2759 continue
2760 }
2761 feeMsat := ldk_node.Lsps2ComputeOpeningFeeMsat(paymentSizeMsat, params)
2762 if feeMsat == nil {
2763 continue
2764 }
2765 if menuMaxFeeMsat == nil || *feeMsat > *menuMaxFeeMsat {
2766 menuMaxFeeMsat = feeMsat
2767 }
2768 }
2769
2770 if menuMaxFeeMsat != nil && *menuMaxFeeMsat < maxAcceptableFeeMsat {
2771 return *menuMaxFeeMsat
2772 }
2773 return maxAcceptableFeeMsat
2774 }
2775
2776 // the absolute ceiling on the LSPS2 opening fee for a payment of the given
2777 // size, independent of the fees the LSP advertises
2778 func lsps2MaxAcceptableOpeningFeeMsat(paymentSizeMsat uint64) uint64 {
2779 return max(lsps2MaxOpeningFeeBaseMsat, paymentSizeMsat/100*lsps2MaxOpeningFeePercent)
2780 }
2781
2782 // finds the smallest incoming payment for which the user is left
2783 // with a usable amount after the LSP skims its LSPS2 opening fee and the fee
2784 // stays within the absolute fee ceiling applied when creating JIT invoices.
2785 func computeLsps2MinPaymentSizeMsat(params ldk_node.Lsps2OpeningFeeParams) (uint64, bool) {
2786 // The smallest amount the user must net after the opening fee. We require a
2787 // whole satoshi rather than a single millisat so the minimum payment size
2788 // represents a usable receive.
2789 const minNetReceiveMsat = 1000
2790
2791 paymentSizeMsat := params.MinPaymentSizeMsat
2792
2793 for range 8 {
2794 openingFeeMsat := ldk_node.Lsps2ComputeOpeningFeeMsat(paymentSizeMsat, params)
2795 if openingFeeMsat == nil {
2796 return 0, false
2797 }
2798 // The incoming amount must exceed the opening fee by at least 1 sat,
2799 // otherwise the user receives a sub-satoshi (effectively zero) amount
2800 // after the LSP skims its fee. The fee must also stay within the
2801 // absolute fee ceiling, otherwise invoices of this size are rejected.
2802 if *openingFeeMsat+minNetReceiveMsat <= paymentSizeMsat &&
2803 *openingFeeMsat <= lsps2MaxAcceptableOpeningFeeMsat(paymentSizeMsat) {
2804 return paymentSizeMsat, paymentSizeMsat <= params.MaxPaymentSizeMsat
2805 }
2806
2807 nextPaymentSizeMsat := *openingFeeMsat + minNetReceiveMsat
2808 if *openingFeeMsat > lsps2MaxOpeningFeeBaseMsat {
2809 // the smallest payment size at which a fee this large stays within
2810 // the percentage part of the ceiling
2811 minSizeForFeeMsat := (*openingFeeMsat + lsps2MaxOpeningFeePercent - 1) / lsps2MaxOpeningFeePercent * 100
2812 nextPaymentSizeMsat = max(nextPaymentSizeMsat, minSizeForFeeMsat)
2813 }
2814 if nextPaymentSizeMsat <= paymentSizeMsat || nextPaymentSizeMsat > params.MaxPaymentSizeMsat {
2815 return 0, false
2816 }
2817 paymentSizeMsat = nextPaymentSizeMsat
2818 }
2819
2820 return 0, false
2821 }
2822
2823 func sanitizeChainEndpoint(endpoint string, port string) string {
2824 u, err := url.Parse(endpoint)
2825 if err != nil || u.Host == "" {
2826 u, err = url.Parse("//" + endpoint)
2827 }
2828 if err != nil {
2829 return endpoint
2830 }
2831
2832 u.User = nil
2833 host := u.Hostname()
2834 if host == "" {
2835 return endpoint
2836 }
2837
2838 existingPort := u.Port()
2839 if existingPort == "" {
2840 existingPort = port
2841 }
2842
2843 if existingPort != "" {
2844 u.Host = net.JoinHostPort(host, existingPort)
2845 } else {
2846 u.Host = host
2847 }
2848
2849 sanitized := u.String()
2850 if u.Scheme == "" {
2851 return strings.TrimPrefix(sanitized, "//")
2852 }
2853
2854 return sanitized
2855 }
2856
2857 func parseLiquiditySourceLsps2(lsps2Address string) (pubkey string, address string) {
2858 entry := strings.TrimSpace(lsps2Address)
2859 if entry == "" {
2860 return "", ""
2861 }
2862
2863 pubkey, address, hasSeparator := strings.Cut(entry, "@")
2864 if !hasSeparator || pubkey == "" || address == "" {
2865 logger.Logger.WithField("entry", entry).Warn("Invalid LDK_LSPS2_ADDRESS, expected <pubkey>@<host>:<port>")
2866 return "", ""
2867 }
2868
2869 if _, _, err := net.SplitHostPort(address); err != nil {
2870 logger.Logger.WithField("entry", entry).WithError(err).Warn("Invalid LDK_LSPS2_ADDRESS host:port")
2871 return "", ""
2872 }
2873
2874 return pubkey, address
2875 }
2876