cln.go raw
1 package cln
2
3 import (
4 "context"
5 crand "crypto/rand"
6 "crypto/sha256"
7 "crypto/tls"
8 "crypto/x509"
9 "encoding/hex"
10 "errors"
11 "fmt"
12 "math"
13 mrand "math/rand"
14 "os"
15 "path/filepath"
16 "regexp"
17 "slices"
18 "sort"
19 "strconv"
20 "strings"
21 "time"
22 "unicode"
23
24 "github.com/getAlby/hub/config"
25 "github.com/getAlby/hub/events"
26 "github.com/getAlby/hub/lnclient"
27 clngrpc "github.com/getAlby/hub/lnclient/cln/clngrpc"
28 clngrpcHold "github.com/getAlby/hub/lnclient/cln/clngrpc_hold"
29 "github.com/getAlby/hub/logger"
30 "github.com/getAlby/hub/nip47/models"
31 "github.com/getAlby/hub/nip47/notifications"
32 "github.com/google/uuid"
33 "github.com/sirupsen/logrus"
34 "google.golang.org/grpc"
35 "google.golang.org/grpc/credentials"
36 )
37
38 type CLNService struct {
39 ctx context.Context
40 client clngrpc.NodeClient
41 clientHold clngrpcHold.HoldClient
42 holdEnabled bool
43 conn *grpc.ClientConn
44 connHold *grpc.ClientConn
45 eventPublisher events.EventPublisher
46 pubkey string
47 enableNotifications bool
48 cancel context.CancelFunc
49 }
50
51 func NewCLNService(ctx context.Context, eventPublisher events.EventPublisher, address, lightningDir, addressHold string) (lnclient lnclient.LNClient, err error) {
52 logger.Logger.WithFields(logrus.Fields{
53 "address": address,
54 "lightningDir": lightningDir,
55 "addressHold": addressHold,
56 }).Info("Creating new CLN gRPC service")
57
58 // CLN grpc client
59 tlsConfig, err := loadTLSCredentials(lightningDir, "cln")
60 if err != nil {
61 return nil, fmt.Errorf("failed to load CLN TLS credentials: %w", err)
62 }
63
64 creds := credentials.NewTLS(tlsConfig)
65
66 conn, err := grpc.NewClient(
67 address,
68 grpc.WithTransportCredentials(creds),
69 )
70 if err != nil {
71 return nil, fmt.Errorf("failed to connect to CLN gRPC: %w", err)
72 }
73
74 client := clngrpc.NewNodeClient(conn)
75
76 ctx, cancel := context.WithCancel(ctx)
77
78 var connHold *grpc.ClientConn
79 var tlsConfigHold *tls.Config
80
81 defer func() {
82 if err != nil {
83 cancel()
84 if conn != nil {
85 conn.Close()
86 }
87 if connHold != nil {
88 connHold.Close()
89 }
90 }
91 }()
92
93 svc := &CLNService{
94 ctx: ctx,
95 client: client,
96 holdEnabled: false,
97 conn: conn,
98 eventPublisher: eventPublisher,
99 cancel: cancel,
100 }
101
102 // Cln hold plugin grpc client
103 if addressHold != "" {
104 tlsConfigHold, err = loadTLSCredentials(lightningDir, "hold")
105 if err != nil {
106 return nil, fmt.Errorf("failed to load hold pluginTLS credentials: %w", err)
107 }
108
109 credsHold := credentials.NewTLS(tlsConfigHold)
110
111 connHold, err = grpc.NewClient(
112 addressHold,
113 grpc.WithTransportCredentials(credsHold),
114 )
115 if err != nil {
116 return nil, fmt.Errorf("failed to connect to hold plugin gRPC: %w", err)
117 }
118
119 clientHold := clngrpcHold.NewHoldClient(connHold)
120
121 logger.Logger.Info("Testing CLN hold plugin gRPC connection")
122 _, err = svc.clientHold.List(ctx, &clngrpcHold.ListRequest{Constraint: &clngrpcHold.ListRequest_Pagination_{
123 Pagination: &clngrpcHold.ListRequest_Pagination{
124 IndexStart: 0,
125 Limit: 1,
126 },
127 }})
128
129 if err != nil {
130 logger.Logger.WithError(err).Error("Failed to connect to CLN hold plugin")
131 return nil, fmt.Errorf("failed to connect to CLN hold plugin: %w", err)
132 }
133
134 svc.connHold = connHold
135 svc.clientHold = clientHold
136 svc.holdEnabled = true
137 logger.Logger.Info("Successfully connected to CLN hold plugin via gRPC")
138
139 go svc.subscribeOpenHoldInvoices(ctx)
140 } else {
141 logger.Logger.Info("No hold plugin configured")
142 }
143
144 logger.Logger.Info("Testing CLN gRPC connection")
145 resp, err := svc.GetInfo(ctx)
146 if err != nil {
147 logger.Logger.WithError(err).Error("Failed to connect to CLN")
148 return nil, fmt.Errorf("failed to connect to CLN: %w", err)
149 }
150
151 svc.pubkey = resp.Pubkey
152
153 getinfo, err := svc.client.Getinfo(ctx, &clngrpc.GetinfoRequest{})
154 if err != nil {
155 return nil, fmt.Errorf("getinfo failed: %w", err)
156 }
157
158 svc.enableNotifications, err = atOrAboveVersion(getinfo.Version, "26.04")
159 if err != nil {
160 logger.Logger.Errorf("Failed to check CLN version %s: %v", getinfo.Version, err)
161 }
162
163 if svc.enableNotifications {
164 logger.Logger.Info("Enabling notifications")
165 go svc.subscribeSuccessfulPayments(ctx)
166 go svc.subscribeFailedPayments(ctx)
167 go svc.subscribeInvoices(ctx)
168 go svc.trackForwardedPayments(ctx)
169 go svc.subscribeChannelEvents(ctx)
170 }
171
172 logger.Logger.Info("Successfully connected to CLN via gRPC")
173 return svc, nil
174 }
175 func loadTLSCredentials(lightningDir string, serverName string) (*tls.Config, error) {
176 if serverName != "cln" {
177 lightningDir = filepath.Join(lightningDir, serverName)
178 }
179 certPath := filepath.Join(lightningDir, "ca.pem")
180 clientCertPath := filepath.Join(lightningDir, "client.pem")
181 clientKeyPath := filepath.Join(lightningDir, "client-key.pem")
182
183 serverCA, err := os.ReadFile(certPath)
184 if err != nil {
185 return nil, fmt.Errorf("failed to read server CA cert: %w", err)
186 }
187
188 certPool := x509.NewCertPool()
189 if !certPool.AppendCertsFromPEM(serverCA) {
190 return nil, fmt.Errorf("failed to add server CA cert to pool")
191 }
192
193 clientCert, err := tls.LoadX509KeyPair(clientCertPath, clientKeyPath)
194 if err != nil {
195 return nil, fmt.Errorf("failed to load client cert/key: %w", err)
196 }
197
198 return &tls.Config{
199 Certificates: []tls.Certificate{clientCert},
200 RootCAs: certPool,
201 ServerName: serverName, // CLN uses "cln" as default ServerName, hold plugin uses "hold"
202 MinVersion: tls.VersionTLS12,
203 }, nil
204 }
205
206 func (svc *CLNService) subscribeSuccessfulPayments(ctx context.Context) {
207 for {
208 select {
209 case <-ctx.Done():
210 return
211 default:
212 paymentStream, err := svc.client.SubscribeSendPaySuccess(ctx, &clngrpc.StreamSendPaySuccessRequest{})
213 if err != nil {
214 logger.Logger.WithError(err).Error("Error subscribing to successful payments")
215 select {
216 case <-ctx.Done():
217 return
218 case <-time.After(10 * time.Second):
219 continue
220 }
221 }
222 paymentsLoop:
223 for {
224 payment, err := paymentStream.Recv()
225 if err != nil {
226 logger.Logger.WithError(err).Error("Failed to receive sendpay_success notification")
227 select {
228 case <-ctx.Done():
229 return
230 case <-time.After(2 * time.Second):
231 break paymentsLoop
232 }
233 }
234
235 transaction, err := svc.clnSendpaySuccessToTransaction(ctx, payment)
236 if err != nil {
237 logger.Logger.WithError(err).Error("Failed to convert notification to transaction")
238 continue
239 }
240 svc.eventPublisher.Publish(&events.Event{
241 Event: "nwc_lnclient_payment_sent",
242 Properties: transaction,
243 })
244
245 }
246 }
247 }
248 }
249
250 func (svc *CLNService) clnSendpaySuccessToTransaction(ctx context.Context, payment *clngrpc.SendPaySuccessNotification) (*lnclient.Transaction, error) {
251 var invstring string
252 if payment.Bolt11 != nil {
253 invstring = *payment.Bolt11
254 } else if payment.Bolt12 != nil {
255 invstring = *payment.Bolt12
256 }
257
258 var amountMsat int64
259 if payment.AmountMsat != nil {
260 amountMsat = int64(payment.AmountMsat.Msat)
261 }
262
263 var feesPaidMsat int64
264 if payment.AmountSentMsat != nil {
265 feesPaidMsat = int64(payment.AmountSentMsat.Msat) - amountMsat
266 }
267
268 var settledAt *int64
269 if payment.CompletedAt != nil {
270 SettledAtUint64 := int64(*payment.CompletedAt)
271 settledAt = &SettledAtUint64
272 }
273
274 var descriptionHash string
275 var expiresAt *int64
276
277 if invstring != "" {
278 decodedInvoice, err := svc.client.Decode(ctx, &clngrpc.DecodeRequest{String_: invstring})
279 if err != nil {
280 return nil, fmt.Errorf("decode failed: %w", err)
281 }
282
283 switch decodedInvoice.ItemType {
284 case clngrpc.DecodeResponse_BOLT12_INVOICE:
285 if decodedInvoice.OfferAbsoluteExpiry != nil {
286 expiresAtint64 := int64(*decodedInvoice.OfferAbsoluteExpiry)
287 expiresAt = &expiresAtint64
288 }
289 case clngrpc.DecodeResponse_BOLT11_INVOICE:
290 if decodedInvoice.CreatedAt != nil && decodedInvoice.Expiry != nil {
291 expiresAtint64 := int64(*decodedInvoice.CreatedAt) + int64(*decodedInvoice.Expiry)
292 expiresAt = &expiresAtint64
293 }
294 if decodedInvoice.DescriptionHash != nil {
295 descriptionHash = hex.EncodeToString(decodedInvoice.DescriptionHash)
296 }
297 default:
298 return nil, fmt.Errorf("invstring `%s` is not a bolt11 or bolt12 invoice", invstring)
299 }
300 }
301
302 return &lnclient.Transaction{
303 Type: "outgoing",
304 Invoice: invstring,
305 Description: payment.GetDescription(),
306 DescriptionHash: descriptionHash,
307 Preimage: hex.EncodeToString(payment.GetPaymentPreimage()),
308 PaymentHash: hex.EncodeToString(payment.GetPaymentHash()),
309 AmountMsat: amountMsat,
310 FeesPaidMsat: feesPaidMsat,
311 CreatedAt: int64(payment.CreatedAt),
312 ExpiresAt: expiresAt,
313 SettledAt: settledAt,
314 }, nil
315 }
316
317 func (svc *CLNService) subscribeFailedPayments(ctx context.Context) {
318 for {
319 select {
320 case <-ctx.Done():
321 return
322 default:
323 paymentStream, err := svc.client.SubscribeSendPayFailure(ctx, &clngrpc.StreamSendPayFailureRequest{})
324 if err != nil {
325 logger.Logger.WithError(err).Error("Error subscribing to failed payments")
326 select {
327 case <-ctx.Done():
328 return
329 case <-time.After(10 * time.Second):
330 continue
331 }
332 }
333 paymentsLoop:
334 for {
335 payment, err := paymentStream.Recv()
336 if err != nil {
337 logger.Logger.WithError(err).Error("Failed to receive sendpay_failure notification")
338 select {
339 case <-ctx.Done():
340 return
341 case <-time.After(2 * time.Second):
342 break paymentsLoop
343 }
344 }
345
346 transaction, err := svc.clnSendpayFailureToTransaction(ctx, payment)
347 if err != nil {
348 logger.Logger.WithError(err).Error("Failed to convert notification to transaction")
349 continue
350 }
351 svc.eventPublisher.Publish(&events.Event{
352 Event: "nwc_lnclient_payment_failed",
353 Properties: transaction,
354 })
355
356 }
357 }
358 }
359 }
360
361 func (svc *CLNService) clnSendpayFailureToTransaction(ctx context.Context, payment *clngrpc.SendPayFailureNotification) (*lnclient.Transaction, error) {
362 if payment.Data == nil {
363 return nil, fmt.Errorf("sendpay_failure data is nil")
364 }
365 var invstring string
366 if payment.Data.Bolt11 != nil {
367 invstring = *payment.Data.Bolt11
368 } else if payment.Data.Bolt12 != nil {
369 invstring = *payment.Data.Bolt12
370 }
371
372 var amountMsat int64
373 if payment.Data.AmountMsat != nil {
374 amountMsat = int64(payment.Data.AmountMsat.Msat)
375 }
376
377 var feesPaidMsat int64
378 if payment.Data.AmountSentMsat != nil {
379 feesPaidMsat = int64(payment.Data.AmountSentMsat.Msat) - amountMsat
380 }
381
382 var settledAt int64
383 if payment.Data.CompletedAt != nil {
384 SettledAtUint64 := *payment.Data.CompletedAt
385 settledAt = int64(SettledAtUint64)
386 }
387
388 var descriptionHash string
389 var expiresAt int64
390
391 if invstring != "" {
392 decodedInvoice, err := svc.client.Decode(ctx, &clngrpc.DecodeRequest{String_: invstring})
393 if err != nil {
394 return nil, fmt.Errorf("decode failed: %w", err)
395 }
396
397 switch decodedInvoice.ItemType {
398 case clngrpc.DecodeResponse_BOLT12_INVOICE:
399 if decodedInvoice.OfferAbsoluteExpiry != nil {
400 expiresAt = int64(*decodedInvoice.OfferAbsoluteExpiry)
401 }
402 case clngrpc.DecodeResponse_BOLT11_INVOICE:
403 if decodedInvoice.CreatedAt != nil && decodedInvoice.Expiry != nil {
404 expiresAt = int64(*decodedInvoice.CreatedAt) + int64(*decodedInvoice.Expiry)
405 }
406 if decodedInvoice.DescriptionHash != nil {
407 descriptionHash = hex.EncodeToString(decodedInvoice.DescriptionHash)
408 }
409 default:
410 return nil, fmt.Errorf("invstring `%s` is not a bolt11 or bolt12 invoice", invstring)
411 }
412 }
413
414 return &lnclient.Transaction{
415 Type: "outgoing",
416 Invoice: invstring,
417 Description: payment.Data.GetDescription(),
418 DescriptionHash: descriptionHash,
419 Preimage: hex.EncodeToString(payment.Data.GetPaymentPreimage()),
420 PaymentHash: hex.EncodeToString(payment.Data.GetPaymentHash()),
421 AmountMsat: amountMsat,
422 FeesPaidMsat: feesPaidMsat,
423 CreatedAt: int64(payment.Data.GetCreatedAt()),
424 ExpiresAt: &expiresAt,
425 SettledAt: &settledAt,
426 }, nil
427 }
428
429 func (svc *CLNService) subscribeInvoices(ctx context.Context) {
430 for {
431 select {
432 case <-ctx.Done():
433 return
434 default:
435 invoiceStream, err := svc.client.SubscribeInvoicePayment(ctx, &clngrpc.StreamInvoicePaymentRequest{})
436 if err != nil {
437 logger.Logger.WithError(err).Error("Error subscribing to invoices")
438 select {
439 case <-ctx.Done():
440 return
441 case <-time.After(10 * time.Second):
442 continue
443 }
444 }
445 invoicesLoop:
446 for {
447 invoice_notif, err := invoiceStream.Recv()
448 if err != nil {
449 logger.Logger.WithError(err).Error("Failed to receive invoice")
450 select {
451 case <-ctx.Done():
452 return
453 case <-time.After(2 * time.Second):
454 break invoicesLoop
455 }
456 }
457
458 listinvoice, err := svc.client.ListInvoices(ctx, &clngrpc.ListinvoicesRequest{
459 Label: &invoice_notif.Label,
460 })
461 if err != nil {
462 logger.Logger.WithFields(logrus.Fields{
463 "label": invoice_notif.Label,
464 }).WithError(err).Error("Failed to list invoice")
465 continue
466 }
467 if len(listinvoice.Invoices) != 1 {
468 logger.Logger.WithFields(logrus.Fields{
469 "label": invoice_notif.Label,
470 "count": len(listinvoice.Invoices),
471 }).Error("Failed to list invoice")
472 continue
473 }
474
475 invoice := listinvoice.Invoices[0]
476
477 transaction, err := svc.clnInvoiceToTransaction(ctx, invoice)
478 if err != nil {
479 logger.Logger.WithError(err).Error("Failed to convert invoice to transaction")
480 continue
481 }
482
483 svc.eventPublisher.Publish(&events.Event{
484 Event: "nwc_lnclient_payment_received",
485 Properties: transaction,
486 })
487 }
488 }
489 }
490 }
491
492 func (svc *CLNService) trackForwardedPayments(ctx context.Context) {
493 // NOTE: this only tracks payments when hub is online and attached
494 for {
495 select {
496 case <-ctx.Done():
497 return
498 default:
499 forwardsStream, err := svc.client.SubscribeForwardEvent(ctx, &clngrpc.StreamForwardEventRequest{})
500 if err != nil {
501 logger.Logger.WithError(err).Error("failed to read forwarding history")
502 select {
503 case <-ctx.Done():
504 return
505 case <-time.After(10 * time.Second):
506 continue
507 }
508 }
509 forwardsLoop:
510 for {
511 forwardNotif, err := forwardsStream.Recv()
512 if err != nil {
513 logger.Logger.WithError(err).Error("Failed to receive invoice")
514 select {
515 case <-ctx.Done():
516 return
517 case <-time.After(2 * time.Second):
518 break forwardsLoop
519 }
520 }
521
522 if forwardNotif.Status != clngrpc.ForwardEventNotification_SETTLED {
523 continue
524 }
525
526 if forwardNotif.FeeMsat == nil || forwardNotif.OutMsat == nil {
527 logger.Logger.WithFields(logrus.Fields{
528 "earned_msat": forwardNotif.FeeMsat,
529 "outbound_amount_forwarded_msat": forwardNotif.OutMsat,
530 }).Error("forwarded payment has missing required fields")
531 continue
532 }
533
534 svc.eventPublisher.Publish(&events.Event{
535 Event: "nwc_payment_forwarded",
536 Properties: &lnclient.PaymentForwardedEventProperties{
537 TotalFeeEarnedMsat: forwardNotif.FeeMsat.Msat,
538 OutboundAmountForwardedMsat: forwardNotif.OutMsat.Msat,
539 },
540 })
541 }
542 }
543 }
544 }
545
546 func (svc *CLNService) subscribeChannelEvents(ctx context.Context) {
547 for {
548 select {
549 case <-ctx.Done():
550 return
551 default:
552 channelEvents, err := svc.client.SubscribeChannelStateChanged(ctx, &clngrpc.StreamChannelStateChangedRequest{})
553 if err != nil {
554 logger.Logger.WithError(err).Error("Error subscribing to channel events")
555 select {
556 case <-ctx.Done():
557 return
558 case <-time.After(10 * time.Second):
559 continue
560 }
561 }
562 channelEventsLoop:
563 for {
564 event, err := channelEvents.Recv()
565 if err != nil {
566 logger.Logger.WithError(err).Error("Failed to receive channel event")
567 select {
568 case <-ctx.Done():
569 return
570 case <-time.After(2 * time.Second):
571 break channelEventsLoop
572 }
573 }
574
575 if event.NewState != clngrpc.ChannelState_ChanneldNormal && event.NewState != clngrpc.ChannelState_Onchain {
576 continue
577 }
578
579 if event.ShortChannelId == nil {
580 logger.Logger.Warn("Received CHANNELD_NORMAL channel event with no short channel id")
581 continue
582 }
583
584 channels, err := svc.client.ListPeerChannels(ctx, &clngrpc.ListpeerchannelsRequest{ShortChannelId: event.ShortChannelId})
585 if err != nil {
586 logger.Logger.WithError(err).Error("Failed to ListPeerChannels")
587 continue
588 }
589 if len(channels.Channels) != 1 {
590 logger.Logger.WithFields(logrus.Fields{
591 "short_channel_id": event.ShortChannelId,
592 "count": len(channels.Channels),
593 }).Error("Expected one channel in ListPeerChannels response")
594 continue
595 }
596 channel := channels.Channels[0]
597 peerId := hex.EncodeToString(event.PeerId)
598 var capacity uint64
599 if channel.TotalMsat != nil {
600 capacity = channel.TotalMsat.Msat / 1000
601 }
602 isOutbound := channel.Opener == clngrpc.ChannelSide_LOCAL
603 public := !channel.GetPrivate()
604
605 switch event.NewState {
606 case clngrpc.ChannelState_ChanneldNormal:
607 logger.Logger.WithFields(logrus.Fields{
608 "counterparty_node_id": peerId,
609 "public": public,
610 "capacity": capacity,
611 "is_outbound": isOutbound,
612 }).Info("Channel opened")
613
614 svc.eventPublisher.Publish(&events.Event{
615 Event: "nwc_channel_ready",
616 Properties: map[string]interface{}{
617 "counterparty_node_id": peerId,
618 "node_type": config.CLNBackendType,
619 "public": public,
620 "capacity": capacity,
621 "is_outbound": isOutbound,
622 },
623 })
624 case clngrpc.ChannelState_Onchain:
625 logger.Logger.WithFields(logrus.Fields{
626 "counterparty_node_id": peerId,
627 "reason": channel.Status,
628 }).Info("Channel closed")
629
630 svc.eventPublisher.Publish(&events.Event{
631 Event: "nwc_channel_closed",
632 Properties: map[string]interface{}{
633 "counterparty_node_id": peerId,
634 "counterparty_node_url": "https://amboss.space/node/" + peerId,
635 "reason": channel.Status,
636 "node_type": config.CLNBackendType,
637 },
638 })
639 }
640 }
641 }
642 }
643 }
644
645 func (c *CLNService) subscribeOpenHoldInvoices(ctx context.Context) {
646 holdinvoices := make([]*clngrpcHold.Invoice, 0)
647
648 const (
649 maxRetries = 5
650 baseBackoff = 500 * time.Millisecond
651 maxBackoff = 5 * time.Second
652 pageSize = 200
653 )
654
655 start := int64(1)
656
657 for {
658 var lsr *clngrpcHold.ListResponse
659 var err error
660
661 for attempt := 0; attempt <= maxRetries; attempt++ {
662 lsr, err = c.clientHold.List(ctx, &clngrpcHold.ListRequest{
663 Constraint: &clngrpcHold.ListRequest_Pagination_{
664 Pagination: &clngrpcHold.ListRequest_Pagination{
665 IndexStart: start,
666 Limit: pageSize,
667 },
668 },
669 })
670
671 if err == nil {
672 break
673 }
674
675 if attempt == maxRetries {
676 logger.Logger.WithError(err).
677 WithField("start", start).
678 Error("List invoices failed after retries")
679 return
680 }
681
682 backoff := min(baseBackoff*time.Duration(1<<attempt), maxBackoff)
683
684 jitter := time.Duration(mrand.Int63n(int64(backoff / 2)))
685 sleep := backoff/2 + jitter
686
687 logger.Logger.WithError(err).
688 WithFields(logrus.Fields{
689 "attempt": attempt + 1,
690 "sleep": sleep,
691 }).
692 Warn("List invoices failed, retrying")
693
694 select {
695 case <-time.After(sleep):
696 case <-ctx.Done():
697 logger.Logger.WithError(ctx.Err()).
698 Warn("Context cancelled during retry backoff")
699 return
700 }
701 }
702
703 if lsr == nil || len(lsr.Invoices) == 0 {
704 break
705 }
706
707 holdinvoices = append(holdinvoices, lsr.Invoices...)
708 start = lsr.Invoices[len(lsr.Invoices)-1].Id + 1
709 }
710
711 for _, invoice := range holdinvoices {
712 if invoice.State == clngrpcHold.InvoiceState_UNPAID {
713 paymentHashHex := hex.EncodeToString(invoice.PaymentHash)
714 logger.Logger.WithFields(logrus.Fields{
715 "paymentHash": paymentHashHex,
716 "addIndex": invoice.Id,
717 }).Info("Resubscribing to pending hold invoice")
718
719 go c.subscribeSingleInvoice(invoice.PaymentHash)
720 }
721 }
722 }
723
724 func (c *CLNService) subscribeSingleInvoice(paymentHashBytes []byte) {
725 // Use the global context for the lifetime of this subscription, but create a cancellable one for this specific task
726 // This allows the goroutine to be potentially cancelled externally if needed, though it primarily exits on invoice state change.
727 // We use a background context derived from the global one to avoid cancelling if the original request context finishes.
728 ctx, cancel := context.WithCancel(c.ctx)
729 defer cancel() // Ensure cancellation happens on exit
730
731 paymentHashHex := hex.EncodeToString(paymentHashBytes)
732 log := logger.Logger.WithField("paymentHash", paymentHashHex)
733
734 log.Info("Starting subscribeSingleInvoice goroutine")
735
736 subReq := &clngrpcHold.TrackRequest{
737 PaymentHash: paymentHashBytes,
738 }
739
740 invoiceStream, err := c.clientHold.Track(ctx, subReq)
741 if err != nil {
742 log.WithError(err).Error("SubscribeSingleInvoice call failed")
743 // Goroutine will exit
744 return
745 }
746
747 log.Info("Successfully subscribed to single invoice stream")
748
749 defer func() {
750 log.Info("Exiting subscribeSingleInvoice goroutine")
751 if r := recover(); r != nil {
752 log.WithField("panic", r).Errorf("PANIC recovered in single invoice stream processing")
753 }
754 }()
755
756 for {
757 trackResponse, err := invoiceStream.Recv()
758
759 if err != nil {
760 log.WithError(err).Error("Failed to receive single invoice update from stream")
761 return
762 }
763 if ctx.Err() != nil {
764 log.Info("Context cancelled, exiting single invoice subscription loop")
765 return
766 }
767
768 log.WithFields(logrus.Fields{
769 "rawState": trackResponse.State.String(),
770 }).Info("Raw update received from single invoice stream")
771
772 switch trackResponse.State {
773 case clngrpcHold.InvoiceState_ACCEPTED:
774 log.Info("Hold invoice accepted, publishing internal event")
775
776 tx, err := c.buildHoldInvoiceTransaction(ctx, paymentHashBytes)
777 if err != nil {
778 logger.Logger.WithError(err).Error("failed to build hold invoice transaction")
779 return
780 }
781
782 c.eventPublisher.Publish(&events.Event{
783 Event: "nwc_lnclient_hold_invoice_accepted",
784 Properties: tx,
785 })
786 case clngrpcHold.InvoiceState_CANCELLED:
787 log.Info("Hold invoice canceled, ending subscription")
788 return // Invoice reached final state, exit goroutine
789 case clngrpcHold.InvoiceState_PAID:
790 return // Invoice reached final state, exit goroutine
791 case clngrpcHold.InvoiceState_UNPAID:
792 // Continue loop
793 }
794 }
795 }
796
797 func (c *CLNService) buildHoldInvoiceTransaction(ctx context.Context, paymentHash []byte) (*lnclient.Transaction, error) {
798 invoice, err := c.fetchHoldInvoice(ctx, paymentHash)
799 if err != nil {
800 return nil, err
801 }
802
803 decodedInvoice, err := c.client.Decode(ctx, &clngrpc.DecodeRequest{
804 String_: invoice.Invoice,
805 })
806 if err != nil {
807 return nil, fmt.Errorf("decode failed: %w", err)
808 }
809
810 return clnHoldInvoiceToTransaction(invoice, decodedInvoice)
811 }
812
813 func (c *CLNService) fetchHoldInvoice(ctx context.Context, paymentHash []byte) (*clngrpcHold.Invoice, error) {
814 if !c.holdEnabled {
815 return nil, errors.New("hold client not configured")
816 }
817
818 resp, err := c.clientHold.List(ctx, &clngrpcHold.ListRequest{
819 Constraint: &clngrpcHold.ListRequest_PaymentHash{
820 PaymentHash: paymentHash,
821 },
822 })
823 if err != nil {
824 return nil, fmt.Errorf("hold list failed: %w", err)
825 }
826
827 if len(resp.Invoices) != 1 {
828 return nil, fmt.Errorf("expected 1 invoice, got %d", len(resp.Invoices))
829 }
830
831 return resp.Invoices[0], nil
832 }
833
834 func clnHoldInvoiceToTransaction(invoice *clngrpcHold.Invoice, decodedInvoice *clngrpc.DecodeResponse) (*lnclient.Transaction, error) {
835 description := ""
836 if decodedInvoice.Description != nil {
837 description = *decodedInvoice.Description
838 }
839
840 descriptionHash := ""
841 if len(decodedInvoice.DescriptionHash) > 0 {
842 descriptionHash = hex.EncodeToString(decodedInvoice.DescriptionHash)
843 }
844
845 amountMsat := int64(0)
846 if decodedInvoice.AmountMsat != nil {
847 amountMsat = int64(decodedInvoice.AmountMsat.Msat)
848 }
849
850 var minExpiry *uint32
851 for _, htlc := range invoice.Htlcs {
852 if htlc.CltvExpiry == nil {
853 continue
854 }
855
856 exp := uint32(*htlc.CltvExpiry)
857
858 if minExpiry == nil || exp < *minExpiry {
859 minExpiry = &exp
860 }
861 }
862
863 tx := &lnclient.Transaction{
864 Type: "incoming",
865 Invoice: invoice.Invoice,
866 Description: description,
867 DescriptionHash: descriptionHash,
868 Preimage: hex.EncodeToString(invoice.Preimage),
869 PaymentHash: hex.EncodeToString(invoice.PaymentHash),
870 AmountMsat: amountMsat,
871 CreatedAt: int64(invoice.CreatedAt),
872 SettledAt: nil,
873 FeesPaidMsat: 0,
874 Metadata: lnclient.Metadata{},
875 SettleDeadline: minExpiry,
876 }
877
878 if decodedInvoice.Expiry != nil {
879 expiresAt := int64(invoice.CreatedAt + *decodedInvoice.Expiry)
880 tx.ExpiresAt = &expiresAt
881 }
882
883 return tx, nil
884 }
885
886 func (c *CLNService) CloseChannel(ctx context.Context, closeChannelRequest *lnclient.CloseChannelRequest) error {
887 logger.Logger.WithFields(logrus.Fields{
888 "closeChannelRequest": closeChannelRequest,
889 }).Debug("Closing Channel")
890
891 req := &clngrpc.CloseRequest{
892 Id: closeChannelRequest.ChannelId,
893 }
894
895 if closeChannelRequest.Force {
896 // There is no force option in CLN, only a Unilateraltimeout after which the channel will be force closed
897 // 0 means waiting forever so we choose 1 second
898 timeout := uint32(1)
899 req.Unilateraltimeout = &timeout
900 }
901
902 _, err := c.client.Close(ctx, req)
903 if err != nil {
904 logger.Logger.WithError(err).Error("Failed to close channel")
905 return fmt.Errorf("close failed: %w", err)
906 }
907
908 return nil
909 }
910
911 func (c *CLNService) ConnectPeer(ctx context.Context, connectPeerRequest *lnclient.ConnectPeerRequest) error {
912 logger.Logger.WithFields(logrus.Fields{
913 "connectPeerRequest": connectPeerRequest,
914 }).Debug("Connecting to Peer")
915
916 port := uint32(connectPeerRequest.Port)
917 req := &clngrpc.ConnectRequest{
918 Id: connectPeerRequest.Pubkey,
919 Host: &connectPeerRequest.Address,
920 Port: &port,
921 }
922
923 _, err := c.client.ConnectPeer(ctx, req)
924 if err != nil {
925 logger.Logger.WithError(err).Error("Failed to connect peer")
926 return err
927 }
928
929 return nil
930 }
931
932 func (c *CLNService) DisconnectPeer(ctx context.Context, peerId string) error {
933 logger.Logger.WithFields(logrus.Fields{
934 "peerId": peerId,
935 }).Debug("Disconnecting Peer")
936
937 pubkey, err := hex.DecodeString(peerId)
938 if err != nil {
939 return err
940 }
941 req := &clngrpc.DisconnectRequest{
942 Id: pubkey,
943 }
944
945 _, err = c.client.Disconnect(ctx, req)
946 if err != nil {
947 logger.Logger.WithError(err).Error("Failed to disconnect peer")
948 return err
949 }
950
951 return nil
952 }
953
954 func (c *CLNService) GetCustomNodeCommandDefinitions() []lnclient.CustomNodeCommandDef {
955 return nil
956 }
957
958 func (c *CLNService) ExecuteCustomNodeCommand(ctx context.Context, command *lnclient.CustomNodeCommandRequest) (*lnclient.CustomNodeCommandResponse, error) {
959 return nil, nil
960 }
961
962 func (c *CLNService) GetBalances(ctx context.Context, includeInactiveChannels bool) (*lnclient.BalancesResponse, error) {
963 logger.Logger.WithFields(logrus.Fields{
964 "includeInactiveChannels": includeInactiveChannels,
965 }).Debug("Get all Balances")
966
967 onchainBalance, err := c.GetOnchainBalance(ctx)
968 if err != nil {
969 return nil, err
970 }
971
972 resp, err := c.client.ListPeerChannels(ctx, &clngrpc.ListpeerchannelsRequest{})
973 if err != nil {
974 return nil, fmt.Errorf("listpeerchannels failed: %w", err)
975 }
976
977 lightning := lnclient.LightningBalanceResponse{}
978
979 for _, ch := range resp.Channels {
980 if ch == nil {
981 continue
982 }
983
984 // Never include closing or closed channels
985 if ch.State != clngrpc.ChannelState_ChanneldNormal {
986 continue
987 }
988
989 // This isn't perfect to determine if a channel is active
990 active := ch.PeerConnected
991 include := active || includeInactiveChannels
992 if !include {
993 continue
994 }
995
996 if ch.SpendableMsat != nil {
997 spendable := int64(ch.SpendableMsat.Msat)
998 lightning.TotalSpendableMsat += spendable
999
1000 if spendable > lightning.NextMaxSpendableMsat {
1001 lightning.NextMaxSpendableMsat = spendable
1002 }
1003 }
1004
1005 if ch.ReceivableMsat != nil {
1006 receivable := int64(ch.ReceivableMsat.Msat)
1007 lightning.TotalReceivableMsat += receivable
1008
1009 if receivable > lightning.NextMaxReceivableMsat {
1010 lightning.NextMaxReceivableMsat = receivable
1011 }
1012 }
1013 }
1014
1015 lightning.NextMaxSpendableMPPMsat = lightning.TotalSpendableMsat
1016 lightning.NextMaxReceivableMPPMsat = lightning.TotalReceivableMsat
1017
1018 return &lnclient.BalancesResponse{
1019 Onchain: *onchainBalance,
1020 Lightning: lightning,
1021 }, nil
1022 }
1023
1024 func (c *CLNService) GetInfo(ctx context.Context) (*lnclient.NodeInfo, error) {
1025 resp, err := c.client.Getinfo(ctx, &clngrpc.GetinfoRequest{})
1026 if err != nil {
1027 return nil, fmt.Errorf("getinfo failed: %w", err)
1028 }
1029
1030 return &lnclient.NodeInfo{
1031 Alias: resp.GetAlias(),
1032 Color: hex.EncodeToString(resp.Color),
1033 Pubkey: hex.EncodeToString(resp.Id),
1034 Network: resp.Network,
1035 BlockHeight: resp.Blockheight,
1036 BlockHash: "", // Not directly available
1037 }, nil
1038 }
1039
1040 func (c *CLNService) GetLogOutput(ctx context.Context, maxLen int) ([]byte, error) {
1041 return []byte{}, nil
1042 }
1043
1044 func (c *CLNService) GetNetworkGraph(ctx context.Context, nodeIds []string) (lnclient.NetworkGraphResponse, error) {
1045 logger.Logger.WithFields(logrus.Fields{
1046 "nodeIds": nodeIds,
1047 }).Debug("Get Network Graph")
1048
1049 listnodes := make([]*clngrpc.ListnodesNodes, 0)
1050 listchannels := make([]*clngrpc.ListchannelsChannels, 0)
1051
1052 for _, nodeId := range nodeIds {
1053 nodeIdBytes, err := hex.DecodeString(nodeId)
1054 if err != nil {
1055 logger.Logger.WithError(err).Error("failed to decode nodeId string")
1056 return nil, fmt.Errorf("failed to decode nodeId string: %w", err)
1057 }
1058
1059 listnode, err := c.client.ListNodes(ctx, &clngrpc.ListnodesRequest{Id: nodeIdBytes})
1060 if err != nil {
1061 logger.Logger.WithError(err).Error("listnodes failed")
1062 return nil, err
1063 }
1064 listnodes = append(listnodes, listnode.Nodes...)
1065
1066 listchannel, err := c.client.ListChannels(ctx, &clngrpc.ListchannelsRequest{Source: nodeIdBytes})
1067 if err != nil {
1068 logger.Logger.WithError(err).Error("listchannels failed")
1069 return nil, err
1070 }
1071 listchannels = append(listchannels, listchannel.Channels...)
1072
1073 listchannel, err = c.client.ListChannels(ctx, &clngrpc.ListchannelsRequest{Destination: nodeIdBytes})
1074 if err != nil {
1075 logger.Logger.WithError(err).Error("listchannels failed")
1076 return nil, err
1077 }
1078 listchannels = append(listchannels, listchannel.Channels...)
1079 }
1080
1081 type NetworkNode struct {
1082 NodeId string `json:"nodeId"`
1083 Alias string `json:"alias"`
1084 Color string `json:"color"`
1085 Addresses []string `json:"addresses"`
1086 Features string `json:"features"`
1087 }
1088
1089 type NodeInfoWithId struct {
1090 Node *NetworkNode `json:"node"`
1091 NodeId string `json:"nodeId"`
1092 }
1093
1094 type NetworkChannel struct {
1095 Scid string `json:"scid"`
1096 Node1 string `json:"node1"`
1097 Node2 string `json:"node2"`
1098 Capacity uint64 `json:"capacity"`
1099 Active bool `json:"active"`
1100 Public bool `json:"public"`
1101 }
1102
1103 nodes := []NodeInfoWithId{}
1104 channels := []*NetworkChannel{}
1105
1106 for _, node := range listnodes {
1107 nodeIdStr := hex.EncodeToString(node.Nodeid)
1108 addrs := []string{}
1109 for _, a := range node.Addresses {
1110 addrs = append(addrs, fmt.Sprintf("%s:%d", a.GetAddress(), a.GetPort()))
1111 }
1112 networkNode := NetworkNode{
1113 NodeId: nodeIdStr,
1114 Alias: node.GetAlias(),
1115 Color: hex.EncodeToString(node.Color),
1116 Addresses: addrs,
1117 Features: hex.EncodeToString(node.Features),
1118 }
1119 nodes = append(nodes, NodeInfoWithId{
1120 Node: &networkNode,
1121 NodeId: nodeIdStr,
1122 })
1123
1124 }
1125
1126 seen := make(map[string]struct{})
1127 for _, edge := range listchannels {
1128 key := fmt.Sprintf("%s:%d", edge.ShortChannelId, edge.Direction)
1129 if _, ok := seen[key]; ok {
1130 continue
1131 }
1132 seen[key] = struct{}{}
1133 channel := NetworkChannel{
1134 Scid: edge.ShortChannelId,
1135 Node1: hex.EncodeToString(edge.Source),
1136 Node2: hex.EncodeToString(edge.Destination),
1137 Capacity: sat(edge.AmountMsat),
1138 Active: edge.Active,
1139 Public: edge.Public,
1140 }
1141 channels = append(channels, &channel)
1142
1143 }
1144
1145 networkGraph := map[string]interface{}{
1146 "nodes": nodes,
1147 "channels": channels,
1148 }
1149 return networkGraph, nil
1150 }
1151
1152 func (c *CLNService) GetNewOnchainAddress(ctx context.Context) (string, error) {
1153 resp, err := c.client.NewAddr(ctx, &clngrpc.NewaddrRequest{})
1154 if err != nil {
1155 logger.Logger.WithError(err).Error("Failed to generate onchain address")
1156 return "", err
1157 }
1158
1159 if resp.Bech32 != nil {
1160 return *resp.Bech32, nil
1161 }
1162
1163 if resp.P2Tr != nil {
1164 return *resp.P2Tr, nil
1165 }
1166
1167 logger.Logger.WithField("resp", resp).Error("No known onchain address type returned")
1168 return "", fmt.Errorf("unknown default onchain address type")
1169 }
1170
1171 func (c *CLNService) GetNodeConnectionInfo(ctx context.Context) (*lnclient.NodeConnectionInfo, error) {
1172 resp, err := c.client.Getinfo(ctx, &clngrpc.GetinfoRequest{})
1173 if err != nil {
1174 return nil, fmt.Errorf("getinfo failed: %w", err)
1175 }
1176
1177 var (
1178 ipv4 *clngrpc.GetinfoAddress
1179 ipv6 *clngrpc.GetinfoAddress
1180 torv3 *clngrpc.GetinfoAddress
1181 )
1182
1183 for _, addr := range resp.Address {
1184 if addr == nil {
1185 continue
1186 }
1187
1188 switch addr.ItemType {
1189 case clngrpc.GetinfoAddress_IPV4:
1190 if ipv4 == nil {
1191 ipv4 = addr
1192 }
1193 case clngrpc.GetinfoAddress_IPV6:
1194 if ipv6 == nil {
1195 ipv6 = addr
1196 }
1197 case clngrpc.GetinfoAddress_TORV3:
1198 if torv3 == nil {
1199 torv3 = addr
1200 }
1201 }
1202 }
1203
1204 var selected *clngrpc.GetinfoAddress
1205 switch {
1206 case ipv4 != nil:
1207 selected = ipv4
1208 case ipv6 != nil:
1209 selected = ipv6
1210 case torv3 != nil:
1211 selected = torv3
1212 default:
1213 addr := "not announced"
1214 selected = &clngrpc.GetinfoAddress{
1215 Address: &addr,
1216 Port: 0,
1217 }
1218 }
1219
1220 return &lnclient.NodeConnectionInfo{
1221 Pubkey: hex.EncodeToString(resp.Id),
1222 Address: selected.GetAddress(),
1223 Port: int(selected.Port),
1224 }, nil
1225 }
1226
1227 func (c *CLNService) GetNodeStatus(ctx context.Context) (nodeStatus *lnclient.NodeStatus, err error) {
1228 resp, err := c.client.Getinfo(ctx, &clngrpc.GetinfoRequest{})
1229 if err != nil {
1230 return nil, fmt.Errorf("getinfo failed: %w", err)
1231 }
1232
1233 ready := false
1234 if resp != nil {
1235 if resp.WarningBitcoindSync == nil && resp.WarningLightningdSync == nil {
1236 ready = true
1237 }
1238 }
1239
1240 return &lnclient.NodeStatus{
1241 IsReady: ready,
1242 InternalNodeStatus: 0,
1243 }, nil
1244 }
1245
1246 func (c *CLNService) GetOnchainBalance(ctx context.Context) (*lnclient.OnchainBalanceResponse, error) {
1247 lf, err := c.client.ListFunds(ctx, &clngrpc.ListfundsRequest{})
1248 if err != nil {
1249 return nil, fmt.Errorf("listfunds failed: %w", err)
1250 }
1251
1252 lpc, err := c.client.ListPeerChannels(ctx, &clngrpc.ListpeerchannelsRequest{})
1253 if err != nil {
1254 return nil, fmt.Errorf("listpeerchannels failed: %w", err)
1255 }
1256
1257 chByID := make(map[string]*clngrpc.ListpeerchannelsChannels)
1258 for _, ch := range lpc.Channels {
1259 if ch == nil || len(ch.ChannelId) == 0 {
1260 continue
1261 }
1262 chByID[hex.EncodeToString(ch.ChannelId)] = ch
1263 }
1264
1265 balances := &lnclient.OnchainBalanceResponse{
1266 PendingBalancesDetails: []lnclient.PendingBalanceDetails{},
1267 PendingSweepBalancesDetails: []lnclient.PendingBalanceDetails{},
1268 }
1269
1270 var reservedSats int64
1271
1272 for _, utxo := range lf.Outputs {
1273 if utxo == nil || utxo.AmountMsat == nil {
1274 continue
1275 }
1276
1277 amt := satInt64(utxo.AmountMsat)
1278 balances.TotalSat += amt
1279
1280 if utxo.Reserved {
1281 balances.ReservedSat += amt
1282 reservedSats += amt
1283 }
1284
1285 switch utxo.Status {
1286 case clngrpc.ListfundsOutputs_CONFIRMED:
1287 if !utxo.Reserved {
1288 balances.SpendableSat += amt
1289 }
1290
1291 case clngrpc.ListfundsOutputs_UNCONFIRMED:
1292 balances.PendingSweepBalancesDetails = append(
1293 balances.PendingSweepBalancesDetails,
1294 lnclient.PendingBalanceDetails{
1295 AmountSat: uint64(amt),
1296 FundingTxId: hex.EncodeToString(utxo.Txid),
1297 FundingTxVout: utxo.Output,
1298 },
1299 )
1300 }
1301 }
1302
1303 for _, ch := range lf.Channels {
1304 if ch == nil || ch.OurAmountMsat == nil || !isClosingState(ch.State) {
1305 continue
1306 }
1307
1308 amt := sat(ch.OurAmountMsat)
1309 balances.PendingBalancesFromChannelClosuresSat += amt
1310 chanIdStr := hex.EncodeToString(ch.ChannelId)
1311
1312 detail := lnclient.PendingBalanceDetails{
1313 ChannelId: chanIdStr,
1314 NodeId: hex.EncodeToString(ch.PeerId),
1315 AmountSat: amt,
1316 }
1317
1318 if pc, ok := chByID[chanIdStr]; ok {
1319 if len(pc.FundingTxid) > 0 {
1320 detail.FundingTxId = hex.EncodeToString(pc.FundingTxid)
1321 }
1322 if pc.FundingOutnum != nil {
1323 detail.FundingTxVout = *pc.FundingOutnum
1324 }
1325 }
1326
1327 balances.PendingBalancesDetails = append(
1328 balances.PendingBalancesDetails,
1329 detail,
1330 )
1331 }
1332
1333 balances.InternalBalances = map[string]int64{
1334 "reserved": reservedSats,
1335 }
1336
1337 return balances, nil
1338 }
1339
1340 func isClosingState(state clngrpc.ChannelState) bool {
1341 switch state {
1342 case clngrpc.ChannelState_ChanneldShuttingDown,
1343 clngrpc.ChannelState_ClosingdSigexchange,
1344 clngrpc.ChannelState_ClosingdComplete,
1345 clngrpc.ChannelState_AwaitingUnilateral,
1346 clngrpc.ChannelState_FundingSpendSeen:
1347 return true
1348 default:
1349 return false
1350 }
1351 }
1352
1353 func isOpeningState(state clngrpc.ChannelState) bool {
1354 switch state {
1355 case clngrpc.ChannelState_ChanneldAwaitingLockin,
1356 clngrpc.ChannelState_DualopendAwaitingLockin,
1357 clngrpc.ChannelState_DualopendOpenCommittReady,
1358 clngrpc.ChannelState_DualopendOpenCommitted,
1359 clngrpc.ChannelState_DualopendOpenInit,
1360 clngrpc.ChannelState_Openingd:
1361 return true
1362 default:
1363 return false
1364 }
1365 }
1366
1367 func isConfirmedState(state clngrpc.ChannelState) bool {
1368 switch state {
1369 case clngrpc.ChannelState_AwaitingUnilateral,
1370 clngrpc.ChannelState_ChanneldAwaitingSplice,
1371 clngrpc.ChannelState_ChanneldNormal,
1372 clngrpc.ChannelState_ChanneldShuttingDown,
1373 clngrpc.ChannelState_ClosingdComplete,
1374 clngrpc.ChannelState_ClosingdSigexchange,
1375 clngrpc.ChannelState_FundingSpendSeen,
1376 clngrpc.ChannelState_Onchain:
1377 return true
1378 default:
1379 return false
1380 }
1381 }
1382
1383 func msatInt64(a *clngrpc.Amount) int64 {
1384 if a == nil {
1385 return 0
1386 }
1387 return int64(a.Msat)
1388 }
1389
1390 func satInt64(a *clngrpc.Amount) int64 {
1391 if a == nil {
1392 return 0
1393 }
1394 return int64(a.Msat / 1000)
1395 }
1396
1397 func sat(a *clngrpc.Amount) uint64 {
1398 if a == nil {
1399 return 0
1400 }
1401 return a.Msat / 1000
1402 }
1403
1404 func localFeeBaseMsat(ch *clngrpc.ListpeerchannelsChannels) uint32 {
1405 if ch == nil {
1406 return 0
1407 }
1408 u := ch.Updates
1409 if u == nil {
1410 return 0
1411 }
1412 l := u.Local
1413 if l == nil {
1414 return 0
1415 }
1416 f := l.FeeBaseMsat
1417 if f == nil {
1418 return 0
1419 }
1420 return uint32(f.Msat)
1421 }
1422
1423 func localFeePPM(ch *clngrpc.ListpeerchannelsChannels) uint32 {
1424 if ch == nil {
1425 return 0
1426 }
1427 u := ch.Updates
1428 if u == nil {
1429 return 0
1430 }
1431 l := u.Local
1432 if l == nil {
1433 return 0
1434 }
1435 f := l.FeeProportionalMillionths
1436 return f
1437 }
1438
1439 func (c *CLNService) GetPubkey() string {
1440 return c.pubkey
1441 }
1442
1443 func (c *CLNService) GetStorageDir() (string, error) {
1444 return "", nil
1445 }
1446
1447 func (c *CLNService) GetSupportedNIP47Methods() []string {
1448 logger.Logger.Info("GetSupportedNIP47Methods")
1449 methods := []string{
1450 models.PAY_INVOICE_METHOD,
1451 models.PAY_KEYSEND_METHOD,
1452 models.GET_BALANCE_METHOD,
1453 models.GET_BUDGET_METHOD,
1454 models.GET_INFO_METHOD,
1455 models.MAKE_INVOICE_METHOD,
1456 models.LOOKUP_INVOICE_METHOD,
1457 models.LIST_TRANSACTIONS_METHOD,
1458 models.MULTI_PAY_INVOICE_METHOD,
1459 models.MULTI_PAY_KEYSEND_METHOD,
1460 models.SIGN_MESSAGE_METHOD,
1461 }
1462
1463 if c.holdEnabled {
1464 methods = append(methods,
1465 models.MAKE_HOLD_INVOICE_METHOD,
1466 models.SETTLE_HOLD_INVOICE_METHOD,
1467 models.CANCEL_HOLD_INVOICE_METHOD,
1468 )
1469 }
1470
1471 return methods
1472 }
1473
1474 func (c *CLNService) GetSupportedNIP47NotificationTypes() []string {
1475 result := make([]string, 0)
1476
1477 if c.holdEnabled {
1478 result = append(result,
1479 notifications.HOLD_INVOICE_ACCEPTED_NOTIFICATION)
1480 }
1481 if c.enableNotifications {
1482 result = append(result,
1483 notifications.PAYMENT_RECEIVED_NOTIFICATION,
1484 notifications.PAYMENT_SENT_NOTIFICATION)
1485 }
1486 return result
1487 }
1488
1489 func (c *CLNService) ListChannels(ctx context.Context) (channels []lnclient.Channel, err error) {
1490 resp, err := c.client.ListPeerChannels(ctx, &clngrpc.ListpeerchannelsRequest{})
1491 if err != nil {
1492 logger.Logger.WithError(err).Error("listpeerchannels failed")
1493 return nil, err
1494 }
1495
1496 infoResp, infoErr := c.client.Getinfo(ctx, &clngrpc.GetinfoRequest{})
1497 if infoErr != nil {
1498 logger.Logger.WithError(infoErr).Error("getinfo failed")
1499 return nil, infoErr
1500 }
1501
1502 blockheight := infoResp.Blockheight
1503
1504 reChanHeight := regexp.MustCompile(`(\d+)x.*`)
1505
1506 for _, channel := range resp.Channels {
1507 if channel == nil {
1508 continue
1509 }
1510
1511 var errorStrings []string
1512
1513 // We could check the funding-confirms config but it's only for remote openers
1514 // In reality channels often confirm with 3 or 6 confirmations
1515 ConfirmationsRequired := uint32(6)
1516 if isConfirmedState(channel.State) {
1517 ConfirmationsRequired = 0
1518 } else if isOpeningState(channel.State) {
1519 confRequired, errStr := confirmationsRequiredFromStatus(channel.Status)
1520 if errStr != nil {
1521 logger.Logger.Error(*errStr)
1522 errorStrings = append(errorStrings, *errStr)
1523 } else {
1524 ConfirmationsRequired = confRequired
1525 }
1526
1527 } else {
1528 errStr := fmt.Sprintf("unexpected clngrpc.ChannelState: %#v", channel.State)
1529 logger.Logger.Error(errStr)
1530 errorStrings = append(errorStrings, errStr)
1531 }
1532
1533 var chanBlock *uint32
1534 if channel.ShortChannelId != nil {
1535 match := reChanHeight.FindStringSubmatch(*channel.ShortChannelId)
1536 if len(match) > 1 {
1537 num, err := strconv.Atoi(match[1])
1538 if err != nil {
1539 errStr := fmt.Sprintf("Error converting number: %v", err)
1540 logger.Logger.Error(errStr)
1541 errorStrings = append(errorStrings, errStr)
1542 }
1543 num32 := uint32(num)
1544 chanBlock = &num32
1545 }
1546 }
1547
1548 var Confirmations uint32
1549 if chanBlock != nil {
1550 if blockheight >= *chanBlock {
1551 Confirmations = (blockheight - *chanBlock) + 1
1552 } else {
1553 Confirmations = 0
1554 }
1555 } else {
1556 Confirmations = 0
1557 }
1558
1559 isActive := channel.State == clngrpc.ChannelState_ChanneldNormal && channel.PeerConnected
1560
1561 var Error *string
1562 if len(errorStrings) > 0 {
1563 combined := strings.Join(errorStrings, "; ")
1564 Error = &combined
1565 }
1566
1567 LocalBalance := msatInt64(channel.ToUsMsat)
1568 TotalBalance := msatInt64(channel.TotalMsat)
1569 RemoteBalance := int64(0)
1570 if TotalBalance >= LocalBalance {
1571 RemoteBalance = TotalBalance - LocalBalance
1572 }
1573
1574 channels = append(channels, lnclient.Channel{
1575 LocalBalanceMsat: LocalBalance,
1576 LocalSpendableBalanceMsat: msatInt64(channel.SpendableMsat),
1577 RemoteBalanceMsat: RemoteBalance,
1578 Id: hex.EncodeToString(channel.ChannelId),
1579 RemotePubkey: hex.EncodeToString(channel.PeerId),
1580 FundingTxId: hex.EncodeToString(channel.FundingTxid),
1581 FundingTxVout: channel.GetFundingOutnum(),
1582 Active: isActive,
1583 Public: !channel.GetPrivate(),
1584 InternalChannel: channel,
1585 Confirmations: &Confirmations,
1586 ConfirmationsRequired: &ConfirmationsRequired,
1587 ForwardingFeeBaseMsat: localFeeBaseMsat(channel),
1588 ForwardingFeeProportionalMillionths: localFeePPM(channel),
1589 UnspendablePunishmentReserveSat: sat(channel.OurReserveMsat),
1590 CounterpartyUnspendablePunishmentReserveSat: sat(channel.TheirReserveMsat),
1591 Error: Error,
1592 IsOutbound: channel.GetOpener() == clngrpc.ChannelSide_LOCAL,
1593 })
1594 }
1595 return channels, nil
1596 }
1597
1598 func confirmationsRequiredFromStatus(status []string) (uint32, *string) {
1599 reStatus := regexp.MustCompile(`.*Funding needs (\d+) more confirmations to be ready.*`)
1600
1601 for _, status := range status {
1602 match := reStatus.FindStringSubmatch(status)
1603 if len(match) > 1 {
1604 num, err := strconv.Atoi(match[1])
1605 if err != nil {
1606 errStr := fmt.Sprintf("Error converting number of confirmations required: %v", err)
1607 return 0, &errStr
1608 }
1609 return uint32(num), nil
1610 }
1611 }
1612
1613 errNotFound := "Could not find status indicating number of confirmations required"
1614 return 0, &errNotFound
1615 }
1616
1617 func (c *CLNService) ListOnchainTransactions(ctx context.Context) ([]lnclient.OnchainTransaction, error) {
1618 account := "wallet"
1619 req := &clngrpc.BkprlistaccounteventsRequest{
1620 Account: &account,
1621 }
1622 bkpr, err := c.client.BkprListAccountEvents(ctx, req)
1623 if err != nil {
1624 return nil, fmt.Errorf("bkprlistaccountevents failed: %w", err)
1625 }
1626
1627 infoResp, infoErr := c.client.Getinfo(ctx, &clngrpc.GetinfoRequest{})
1628 if infoErr != nil {
1629 logger.Logger.WithError(infoErr).Error("getinfo failed")
1630 return nil, infoErr
1631 }
1632
1633 blockheight := infoResp.Blockheight
1634
1635 transactions := make([]lnclient.OnchainTransaction, 0)
1636
1637 for _, event := range bkpr.Events {
1638 if event.ItemType != clngrpc.BkprlistaccounteventsEvents_CHAIN {
1639 continue
1640 }
1641 transactionType := "incoming"
1642 AmountSat := sat(event.CreditMsat)
1643 debitSat := sat(event.DebitMsat)
1644 if debitSat > 0 {
1645 transactionType = "outgoing"
1646 AmountSat = debitSat
1647 }
1648
1649 numConfirmations := uint32(0)
1650 if event.Blockheight != nil && blockheight >= *event.Blockheight {
1651 numConfirmations = blockheight - *event.Blockheight
1652 }
1653
1654 TxIdHex := hex.EncodeToString(event.Txid)
1655
1656 transactions = append(transactions, lnclient.OnchainTransaction{
1657 AmountSat: AmountSat,
1658 CreatedAt: uint64(event.Timestamp),
1659 State: "confirmed",
1660 Type: transactionType,
1661 NumConfirmations: numConfirmations,
1662 TxId: TxIdHex,
1663 })
1664 }
1665
1666 slices.Reverse(transactions)
1667
1668 sort.SliceStable(transactions, func(i, j int) bool {
1669 return transactions[i].CreatedAt > transactions[j].CreatedAt
1670 })
1671
1672 return transactions, nil
1673 }
1674
1675 func (c *CLNService) ListPeers(ctx context.Context) ([]lnclient.PeerDetails, error) {
1676 resp, err := c.client.ListPeers(ctx, &clngrpc.ListpeersRequest{})
1677 if err != nil {
1678 return nil, fmt.Errorf("listpeers failed: %w", err)
1679 }
1680
1681 peers := make([]lnclient.PeerDetails, 0, len(resp.Peers))
1682 for _, peer := range resp.Peers {
1683 if peer == nil {
1684 continue
1685 }
1686
1687 req_node := &clngrpc.ListnodesRequest{Id: peer.Id}
1688
1689 resp_node, err := c.client.ListNodes(ctx, req_node)
1690 if err != nil {
1691 return nil, fmt.Errorf("listnodes failed: %w", err)
1692 }
1693
1694 if len(resp_node.Nodes) == 0 {
1695 addr := ""
1696 peers = append(peers, lnclient.PeerDetails{
1697 NodeId: hex.EncodeToString(peer.Id),
1698 Address: addr,
1699 IsPersisted: peer.GetNumChannels() > 0,
1700 IsConnected: peer.Connected,
1701 })
1702 continue
1703 } else {
1704 var (
1705 ipv4 *clngrpc.ListnodesNodesAddresses
1706 ipv6 *clngrpc.ListnodesNodesAddresses
1707 torv3 *clngrpc.ListnodesNodesAddresses
1708 )
1709
1710 for _, addr := range resp_node.Nodes[0].Addresses {
1711 if addr == nil {
1712 continue
1713 }
1714
1715 switch addr.ItemType {
1716 case clngrpc.ListnodesNodesAddresses_IPV4:
1717 if ipv4 == nil {
1718 ipv4 = addr
1719 }
1720 case clngrpc.ListnodesNodesAddresses_IPV6:
1721 if ipv6 == nil {
1722 ipv6 = addr
1723 }
1724 case clngrpc.ListnodesNodesAddresses_TORV3:
1725 if torv3 == nil {
1726 torv3 = addr
1727 }
1728 }
1729 }
1730
1731 var selected *clngrpc.ListnodesNodesAddresses
1732 switch {
1733 case ipv4 != nil:
1734 selected = ipv4
1735 case ipv6 != nil:
1736 selected = ipv6
1737 case torv3 != nil:
1738 selected = torv3
1739 default:
1740 addr := ""
1741 selected = &clngrpc.ListnodesNodesAddresses{
1742 Address: &addr,
1743 Port: 0,
1744 }
1745 }
1746
1747 peers = append(peers, lnclient.PeerDetails{
1748 NodeId: hex.EncodeToString(peer.Id),
1749 Address: *selected.Address,
1750 IsPersisted: peer.GetNumChannels() > 0,
1751 IsConnected: peer.Connected,
1752 })
1753 }
1754 }
1755
1756 return peers, nil
1757
1758 }
1759
1760 func (c *CLNService) LookupInvoice(ctx context.Context, paymentHash string) (transaction *lnclient.Transaction, err error) {
1761 logger.Logger.WithFields(logrus.Fields{
1762 "paymentHash": paymentHash,
1763 }).Debug("Lookup Invoice")
1764
1765 paymentHashBytes, err := hex.DecodeString(paymentHash)
1766 if err != nil {
1767 logger.Logger.WithError(err).Error("failed to decode payment hash")
1768 return nil, fmt.Errorf("failed to decode payment hash: %w", err)
1769 }
1770 req := &clngrpc.ListinvoicesRequest{PaymentHash: paymentHashBytes}
1771
1772 resp, err := c.client.ListInvoices(ctx, req)
1773 if err != nil {
1774 logger.Logger.WithError(err).Error("listinvoices failed")
1775 return nil, fmt.Errorf("listinvoices failed: %w", err)
1776 }
1777 if len(resp.Invoices) == 0 {
1778 return nil, fmt.Errorf("invoice not found")
1779 }
1780
1781 transaction, err = c.clnInvoiceToTransaction(ctx, resp.Invoices[0])
1782 if err != nil {
1783 logger.Logger.WithError(err).Error("failed to convert invoice to transaction")
1784 return nil, fmt.Errorf("failed to convert invoice to transaction: %w", err)
1785 }
1786
1787 return transaction, nil
1788 }
1789
1790 func (c *CLNService) clnInvoiceToTransaction(ctx context.Context, invoice *clngrpc.ListinvoicesInvoices) (*lnclient.Transaction, error) {
1791 var invstring string
1792 var bolt11Invoice string
1793 if invoice.Bolt11 != nil {
1794 invstring = *invoice.Bolt11
1795 bolt11Invoice = *invoice.Bolt11
1796 } else if invoice.Bolt12 != nil {
1797 invstring = *invoice.Bolt12
1798 } else {
1799 return nil, fmt.Errorf("bolt11 and bolt12 missing from invoice")
1800 }
1801
1802 var amountMsat int64
1803 if invoice.Status == clngrpc.ListinvoicesInvoices_PAID && invoice.AmountReceivedMsat != nil {
1804 amountMsat = int64(invoice.AmountReceivedMsat.Msat)
1805 } else if invoice.AmountMsat != nil {
1806 amountMsat = int64(invoice.AmountMsat.Msat)
1807 } else {
1808 amountMsat = 0
1809 }
1810
1811 expires_at := int64(invoice.ExpiresAt)
1812
1813 var paid_at *int64
1814 if invoice.Status == clngrpc.ListinvoicesInvoices_PAID {
1815 if invoice.PaidAt == nil {
1816 return nil, fmt.Errorf("paid_at missing from paid invoice")
1817 }
1818 paid_at_int64 := int64(*invoice.PaidAt)
1819 paid_at = &paid_at_int64
1820 }
1821
1822 decoded_invoice, err := c.client.Decode(ctx, &clngrpc.DecodeRequest{String_: invstring})
1823 if err != nil {
1824 return nil, fmt.Errorf("decode failed: %w", err)
1825 }
1826
1827 var created_at int64
1828 metadata := map[string]interface{}{}
1829 var description_hash string
1830 switch decoded_invoice.ItemType {
1831 case clngrpc.DecodeResponse_BOLT12_INVOICE:
1832 if decoded_invoice.InvoiceCreatedAt == nil {
1833 return nil, fmt.Errorf("invoice_created_at missing from bolt12 invoice")
1834 }
1835 created_at = int64(*decoded_invoice.InvoiceCreatedAt)
1836
1837 offer := map[string]interface{}{}
1838 offer["id"] = hex.EncodeToString(decoded_invoice.OfferId)
1839 if invoice.InvreqPayerNote != nil {
1840 offer["payer_note"] = *invoice.InvreqPayerNote
1841 }
1842 metadata["offer"] = offer
1843 case clngrpc.DecodeResponse_BOLT11_INVOICE:
1844 if decoded_invoice.CreatedAt == nil {
1845 return nil, fmt.Errorf("created_at missing from bolt11 invoice")
1846 }
1847 created_at = int64(*decoded_invoice.CreatedAt)
1848
1849 if decoded_invoice.DescriptionHash != nil {
1850 description_hash = hex.EncodeToString(decoded_invoice.DescriptionHash)
1851 }
1852
1853 default:
1854 return nil, fmt.Errorf("invoice is not a bolt11 or bolt12 invoice")
1855 }
1856
1857 var description string
1858 if invoice.Description != nil {
1859 description = *invoice.Description
1860 } else {
1861 description = ""
1862 }
1863
1864 transaction := &lnclient.Transaction{
1865 Type: "incoming",
1866 Invoice: bolt11Invoice,
1867 Description: description,
1868 DescriptionHash: description_hash,
1869 Preimage: hex.EncodeToString(invoice.PaymentPreimage),
1870 PaymentHash: hex.EncodeToString(invoice.PaymentHash),
1871 AmountMsat: amountMsat,
1872 FeesPaidMsat: 0,
1873 CreatedAt: created_at,
1874 ExpiresAt: &expires_at,
1875 SettledAt: paid_at,
1876 Metadata: metadata,
1877 }
1878 return transaction, nil
1879 }
1880
1881 func (c *CLNService) MakeHoldInvoice(ctx context.Context, amountMsat int64, description string, descriptionHash string, expiry int64, paymentHash string, minCltvExpiryDelta *uint64) (transaction *lnclient.Transaction, err error) {
1882 if !c.holdEnabled {
1883 return nil, errors.New("hold plugin not configured")
1884 }
1885
1886 logger.Logger.WithFields(logrus.Fields{
1887 "amount": amountMsat,
1888 "description": description,
1889 "description_hash": descriptionHash,
1890 "expiry": expiry,
1891 "payment_hash": paymentHash,
1892 "minCltvExpiryDelta": minCltvExpiryDelta,
1893 }).Debug("Make Hold Invoice")
1894
1895 paymentHashBytes, err := hex.DecodeString(paymentHash)
1896 if err != nil {
1897 logger.Logger.WithFields(logrus.Fields{
1898 "paymentHash": paymentHash,
1899 }).WithError(err).Error("Invalid payment hash")
1900 return nil, fmt.Errorf("Invalid payment hash: %v", err)
1901 }
1902
1903 if expiry == 0 {
1904 expiry = lnclient.DEFAULT_INVOICE_EXPIRY
1905 }
1906 expiryUint64 := uint64(expiry)
1907
1908 req := &clngrpcHold.InvoiceRequest{
1909 PaymentHash: paymentHashBytes,
1910 AmountMsat: uint64(amountMsat),
1911 Expiry: &expiryUint64,
1912 MinFinalCltvExpiry: minCltvExpiryDelta,
1913 }
1914
1915 if descriptionHash != "" {
1916 descriptionHashBytes, err := hex.DecodeString(descriptionHash)
1917 if err != nil {
1918 logger.Logger.WithFields(logrus.Fields{
1919 "descriptionHash": descriptionHash,
1920 }).WithError(err).Error("Invalid description hash")
1921 return nil, fmt.Errorf("Invalid description hash: %v", err)
1922 }
1923 req.Description = &clngrpcHold.InvoiceRequest_Hash{
1924 Hash: descriptionHashBytes,
1925 }
1926 } else {
1927 req.Description = &clngrpcHold.InvoiceRequest_Memo{
1928 Memo: description,
1929 }
1930 }
1931
1932 resp, err := c.clientHold.Invoice(ctx, req)
1933 if err != nil {
1934 logger.Logger.WithFields(logrus.Fields{
1935 "paymentHash": paymentHash,
1936 }).WithError(err).Error("Failed to make hold invoice")
1937 return nil, fmt.Errorf("Failed to make hold invoice: %v", err)
1938 }
1939
1940 expiresAt := time.Now().Unix() + expiry
1941
1942 go c.subscribeSingleInvoice(paymentHashBytes)
1943 logger.Logger.WithField("paymentHash", paymentHash).Info("Launched single invoice subscription goroutine")
1944
1945 transaction = &lnclient.Transaction{
1946 Type: "incoming",
1947 Invoice: resp.Bolt11,
1948 Description: description,
1949 DescriptionHash: descriptionHash,
1950 Preimage: "",
1951 PaymentHash: paymentHash,
1952 AmountMsat: amountMsat,
1953 FeesPaidMsat: 0,
1954 CreatedAt: time.Now().Unix(),
1955 ExpiresAt: &expiresAt,
1956 SettledAt: nil,
1957 Metadata: lnclient.Metadata{},
1958 SettleDeadline: nil,
1959 }
1960 return transaction, nil
1961 }
1962
1963 func (c *CLNService) SettleHoldInvoice(ctx context.Context, preimage string) (err error) {
1964 if !c.holdEnabled {
1965 return errors.New("hold plugin not configured")
1966 }
1967
1968 logger.Logger.WithFields(logrus.Fields{
1969 "preimage": preimage,
1970 }).Debug("Settle Hold Invoice")
1971
1972 preimageBytes, err := hex.DecodeString(preimage)
1973 if err != nil {
1974 logger.Logger.WithFields(logrus.Fields{
1975 "preimage": preimage,
1976 }).WithError(err).Error("Invalid preimage")
1977 return fmt.Errorf("Invalid preimage: %v", err)
1978 }
1979
1980 _, err = c.clientHold.Settle(ctx, &clngrpcHold.SettleRequest{
1981 PaymentPreimage: preimageBytes,
1982 })
1983 if err != nil {
1984 logger.Logger.WithFields(logrus.Fields{
1985 "preimage": preimage,
1986 }).WithError(err).Error("Failed to settle hold invoice")
1987 return fmt.Errorf("Failed to settle hold invoice: %v", err)
1988 }
1989
1990 return nil
1991 }
1992
1993 func (c *CLNService) CancelHoldInvoice(ctx context.Context, paymentHash string) (err error) {
1994 if !c.holdEnabled {
1995 return errors.New("hold plugin not configured")
1996 }
1997
1998 logger.Logger.WithFields(logrus.Fields{
1999 "paymentHash": paymentHash,
2000 }).Debug("Cancel Hold Invoice")
2001
2002 paymentHashBytes, err := hex.DecodeString(paymentHash)
2003 if err != nil {
2004 logger.Logger.WithFields(logrus.Fields{
2005 "paymentHash": paymentHash,
2006 }).WithError(err).Error("Invalid paymentHash")
2007 return fmt.Errorf("Invalid paymentHash: %v", err)
2008 }
2009
2010 _, err = c.clientHold.Cancel(ctx, &clngrpcHold.CancelRequest{
2011 PaymentHash: paymentHashBytes,
2012 })
2013 if err != nil {
2014 logger.Logger.WithFields(logrus.Fields{
2015 "paymentHash": paymentHash,
2016 }).WithError(err).Error("Failed to cancel hold invoice")
2017 return fmt.Errorf("Failed to cancel hold invoice: %v", err)
2018 }
2019
2020 return nil
2021 }
2022
2023 func (c *CLNService) MakeInvoice(ctx context.Context, amountMsat int64, description string, descriptionHash string, expiry int64, throughNodePubkey *string) (transaction *lnclient.Transaction, err error) {
2024 logger.Logger.WithFields(logrus.Fields{
2025 "amount": amountMsat,
2026 "description": description,
2027 "description_hash": descriptionHash,
2028 "expiry": expiry,
2029 "through_node_pubkey": throughNodePubkey,
2030 }).Debug("Make Invoice")
2031
2032 label := "AlbyHub-" + uuid.NewString()
2033
2034 var deschashonly bool
2035 if descriptionHash != "" {
2036 if description == "" {
2037 return nil, fmt.Errorf("Must have description when using description_hash")
2038 }
2039 myDescriptionHash := sha256.Sum256([]byte(description))
2040 if descriptionHash != hex.EncodeToString(myDescriptionHash[:]) {
2041 return nil, fmt.Errorf("description_hash does not match description")
2042 }
2043 deschashonly = true
2044 }
2045
2046 if expiry == 0 {
2047 expiry = lnclient.DEFAULT_INVOICE_EXPIRY
2048 }
2049 myExpiry := uint64(expiry)
2050
2051 Amount := clngrpc.AmountOrAny{
2052 Value: &clngrpc.AmountOrAny_Amount{Amount: &clngrpc.Amount{Msat: uint64(amountMsat)}}}
2053 // amount 0 is often used for "any" amount but CLN doesn't support 0 directly
2054 if amountMsat == 0 {
2055 Amount = clngrpc.AmountOrAny{
2056 Value: &clngrpc.AmountOrAny_Any{Any: true}}
2057 }
2058
2059 preimage, err := GeneratePreimage()
2060 if err != nil {
2061 return nil, err
2062 }
2063
2064 req := &clngrpc.InvoiceRequest{
2065 Description: description,
2066 Label: label,
2067 Preimage: preimage,
2068 Expiry: &myExpiry,
2069 Deschashonly: &deschashonly,
2070 AmountMsat: &Amount,
2071 }
2072
2073 Exposeprivatechannels := []string{}
2074
2075 if throughNodePubkey != nil {
2076 throughNodePubkeyBytes, err := hex.DecodeString(*throughNodePubkey)
2077 if err != nil {
2078 return nil, err
2079 }
2080 lpc, err := c.client.ListPeerChannels(ctx, &clngrpc.ListpeerchannelsRequest{
2081 Id: throughNodePubkeyBytes,
2082 })
2083 if err != nil {
2084 logger.Logger.WithError(err).Error("listpeerchannels failed")
2085 return nil, fmt.Errorf("listpeerchannels failed")
2086 }
2087
2088 for _, channel := range lpc.Channels {
2089 if channel.ShortChannelId != nil {
2090 Exposeprivatechannels = append(Exposeprivatechannels, *channel.ShortChannelId)
2091 continue
2092 }
2093 if channel.Alias != nil {
2094 if channel.Alias.Remote != nil {
2095 Exposeprivatechannels = append(Exposeprivatechannels, *channel.Alias.Remote)
2096 }
2097 }
2098 }
2099 }
2100
2101 if len(Exposeprivatechannels) > 0 {
2102 req.Exposeprivatechannels = Exposeprivatechannels
2103 }
2104
2105 resp, err := c.client.Invoice(ctx, req)
2106 if err != nil {
2107 logger.Logger.WithError(err).Error("invoice failed")
2108 return nil, fmt.Errorf("invoice failed: %w", err)
2109 }
2110
2111 expiresAt := int64(resp.ExpiresAt)
2112
2113 transaction = &lnclient.Transaction{
2114 Type: "incoming",
2115 Invoice: resp.Bolt11,
2116 Description: description,
2117 DescriptionHash: descriptionHash,
2118 Preimage: hex.EncodeToString(preimage),
2119 PaymentHash: hex.EncodeToString(resp.PaymentHash),
2120 AmountMsat: amountMsat,
2121 FeesPaidMsat: 0,
2122 CreatedAt: time.Now().Unix(),
2123 ExpiresAt: &expiresAt,
2124 SettledAt: nil,
2125 Metadata: lnclient.Metadata{},
2126 SettleDeadline: nil,
2127 }
2128
2129 return transaction, nil
2130 }
2131
2132 func GeneratePreimage() ([]byte, error) {
2133 preimage := make([]byte, 32)
2134
2135 _, err := crand.Read(preimage[:])
2136 if err != nil {
2137 return nil, fmt.Errorf("failed to generate preimage: %w", err)
2138 }
2139
2140 return preimage, nil
2141 }
2142
2143 func (c *CLNService) MakeOffer(ctx context.Context, description string) (string, error) {
2144 logger.Logger.WithFields(logrus.Fields{
2145 "description": description,
2146 }).Debug("Make Offer")
2147
2148 req := &clngrpc.OfferRequest{
2149 Amount: "any",
2150 Description: &description,
2151 }
2152 resp, err := c.client.Offer(ctx, req)
2153 if err != nil {
2154 logger.Logger.WithError(err).Error("offer failed")
2155 return "", fmt.Errorf("offer failed: %w", err)
2156 }
2157 if resp == nil {
2158 return "", fmt.Errorf("empty offer response")
2159 }
2160
2161 return resp.Bolt12, nil
2162 }
2163
2164 func (c *CLNService) OpenChannel(ctx context.Context, openChannelRequest *lnclient.OpenChannelRequest) (*lnclient.OpenChannelResponse, error) {
2165 logger.Logger.WithFields(logrus.Fields{
2166 "openChannelRequest": openChannelRequest,
2167 }).Debug("Open Channel")
2168
2169 Amount := clngrpc.AmountOrAll{Value: &clngrpc.AmountOrAll_Amount{
2170 Amount: &clngrpc.Amount{Msat: uint64(openChannelRequest.AmountSats) * 1000},
2171 }}
2172
2173 Id, err := hex.DecodeString(openChannelRequest.Pubkey)
2174 if err != nil {
2175 return nil, fmt.Errorf("Could not convert Pubkey to bytes")
2176 }
2177
2178 req := &clngrpc.FundchannelRequest{
2179 Amount: &Amount,
2180 Announce: &openChannelRequest.Public,
2181 Id: Id,
2182 }
2183 resp, err := c.client.FundChannel(ctx, req)
2184 if err != nil {
2185 logger.Logger.WithError(err).Error("fundchannel failed")
2186 return nil, fmt.Errorf("fundchannel failed: %w", err)
2187 }
2188
2189 if resp == nil {
2190 return nil, fmt.Errorf("empty fundchannel response")
2191 }
2192
2193 FundingTxId := hex.EncodeToString(resp.Txid)
2194
2195 return &lnclient.OpenChannelResponse{
2196 FundingTxId: FundingTxId,
2197 }, nil
2198
2199 }
2200
2201 func (c *CLNService) RedeemOnchainFunds(ctx context.Context, toAddress string, amount uint64, feeRate *uint64, sendAll bool) (txId string, err error) {
2202 logger.Logger.WithFields(logrus.Fields{
2203 "toAddress": toAddress,
2204 "amount": amount,
2205 "feeRate": feeRate,
2206 "sendAll": sendAll,
2207 }).Debug("Redeem Onchain Funds")
2208
2209 Satoshi := clngrpc.AmountOrAll{Value: &clngrpc.AmountOrAll_Amount{
2210 Amount: &clngrpc.Amount{Msat: uint64(amount) * 1000},
2211 }}
2212 if sendAll {
2213 Satoshi = clngrpc.AmountOrAll{Value: &clngrpc.AmountOrAll_All{
2214 All: true,
2215 }}
2216 }
2217
2218 req := &clngrpc.WithdrawRequest{
2219 Destination: toAddress,
2220 Satoshi: &Satoshi,
2221 }
2222
2223 if feeRate != nil {
2224 if *feeRate > math.MaxUint32/1000 {
2225 return "", fmt.Errorf("fee rate too high")
2226 }
2227 req.Feerate = &clngrpc.Feerate{
2228 Style: &clngrpc.Feerate_Perkb{
2229 Perkb: uint32(*feeRate) * 1000,
2230 },
2231 }
2232 }
2233
2234 resp, err := c.client.Withdraw(ctx, req)
2235 if err != nil {
2236 logger.Logger.WithError(err).Error("withdraw failed")
2237 return "", fmt.Errorf("withdraw failed: %w", err)
2238 }
2239
2240 if resp == nil {
2241 return "", fmt.Errorf("empty withdraw response")
2242 }
2243
2244 return hex.EncodeToString(resp.Txid), nil
2245
2246 }
2247
2248 func (c *CLNService) ResetRouter(key string) error {
2249 return nil
2250 }
2251
2252 func (c *CLNService) SendKeysend(amount uint64, destination string, customRecords []lnclient.TLVRecord, preimage string) (*lnclient.PayKeysendResponse, error) {
2253 logger.Logger.WithFields(logrus.Fields{
2254 "amount": amount,
2255 "destination": destination,
2256 "customRecords": customRecords,
2257 "preimage": preimage,
2258 }).Debug("Send Keysend")
2259
2260 if preimage != "" {
2261 return nil, errors.New("preimage not supported for keysends")
2262 }
2263
2264 Destination, err := hex.DecodeString(destination)
2265 if err != nil {
2266 logger.Logger.WithError(err).Error("Failed to decode payee pubkey")
2267 return nil, err
2268 }
2269
2270 req := &clngrpc.KeysendRequest{
2271 Destination: Destination,
2272 AmountMsat: &clngrpc.Amount{Msat: amount},
2273 }
2274
2275 if len(customRecords) > 0 {
2276 Extratlvs := clngrpc.TlvStream{}
2277 for _, record := range customRecords {
2278 valueBytes, err := hex.DecodeString(record.Value)
2279 if err != nil {
2280 return nil, fmt.Errorf("could not decode TLV value to bytes: %v", record.Value)
2281 }
2282
2283 entry := clngrpc.TlvEntry{
2284 Type: record.Type,
2285 Value: valueBytes,
2286 }
2287
2288 Extratlvs.Entries = append(Extratlvs.Entries, &entry)
2289 }
2290 req.Extratlvs = &Extratlvs
2291 }
2292
2293 resp, err := c.client.KeySend(c.ctx, req)
2294 if err != nil {
2295 logger.Logger.WithError(err).Error("keysend failed")
2296 return nil, fmt.Errorf("keysend failed: %w", err)
2297 }
2298
2299 if resp == nil {
2300 return nil, fmt.Errorf("empty keysend response")
2301 }
2302
2303 feeMsat := uint64(0)
2304
2305 if resp.AmountSentMsat != nil && resp.AmountMsat != nil {
2306 feeMsat = resp.AmountSentMsat.Msat - resp.AmountMsat.Msat
2307 }
2308 return &lnclient.PayKeysendResponse{FeeMsat: feeMsat}, nil
2309 }
2310
2311 func (c *CLNService) SendPaymentProbes(ctx context.Context, invoice string) error {
2312 return nil
2313 }
2314
2315 func (c *CLNService) SendPaymentSync(payReq string, amount *uint64) (*lnclient.PayInvoiceResponse, error) {
2316 logger.Logger.WithFields(logrus.Fields{
2317 "payReq": payReq,
2318 "amount": amount,
2319 }).Debug("Send Payment Sync")
2320
2321 dec_req := &clngrpc.DecodeRequest{
2322 String_: payReq,
2323 }
2324
2325 dec_resp, err := c.client.Decode(c.ctx, dec_req)
2326 if err != nil {
2327 logger.Logger.WithError(err).Error("decode failed")
2328 return nil, fmt.Errorf("decode failed: %w", err)
2329 }
2330 if dec_resp == nil {
2331 return nil, fmt.Errorf("decode result empty")
2332 }
2333 if !dec_resp.Valid {
2334 return nil, fmt.Errorf("payReq not valid")
2335 }
2336
2337 var amountMsat *clngrpc.Amount
2338 if amount != nil {
2339 amountMsat = &clngrpc.Amount{
2340 Msat: *amount,
2341 }
2342 }
2343
2344 req := &clngrpc.XpayRequest{
2345 Invstring: payReq,
2346 AmountMsat: amountMsat,
2347 }
2348
2349 resp, err := c.client.Xpay(c.ctx, req)
2350 if err != nil {
2351 logger.Logger.WithError(err).Error("xpay failed")
2352 return nil, fmt.Errorf("xpay failed: %w", err)
2353 }
2354
2355 feePaidMsat := uint64(0)
2356 if resp.AmountSentMsat != nil {
2357 if resp.AmountMsat != nil {
2358 feePaidMsat = resp.AmountSentMsat.Msat - resp.AmountMsat.Msat
2359 }
2360 }
2361
2362 return &lnclient.PayInvoiceResponse{
2363 Preimage: hex.EncodeToString(resp.PaymentPreimage),
2364 FeeMsat: feePaidMsat,
2365 }, err
2366 }
2367
2368 func (c *CLNService) SendSpontaneousPaymentProbes(ctx context.Context, amountMsat uint64, nodeId string) error {
2369 return nil
2370 }
2371
2372 func (c *CLNService) Shutdown() error {
2373 logger.Logger.Info("Cancelling CLN context")
2374 c.cancel()
2375
2376 logger.Logger.Info("Closing gRPC connections")
2377 if c.conn != nil {
2378 if err := c.conn.Close(); err != nil {
2379 logger.Logger.WithError(err).Error("Failed to close CLN gRPC connection")
2380 }
2381 }
2382
2383 if c.connHold != nil {
2384 if err := c.connHold.Close(); err != nil {
2385 logger.Logger.WithError(err).Error("Failed to close CLN hold plugin gRPC connection")
2386 }
2387 }
2388
2389 logger.Logger.Info("CLN backend shutdown complete")
2390 return nil
2391 }
2392
2393 func (c *CLNService) SignMessage(ctx context.Context, message string) (string, error) {
2394 logger.Logger.WithFields(logrus.Fields{
2395 "message": message,
2396 }).Debug("Signing Message")
2397
2398 req := &clngrpc.SignmessageRequest{
2399 Message: message,
2400 }
2401 resp, err := c.client.SignMessage(ctx, req)
2402 if err != nil {
2403 logger.Logger.WithError(err).Error("signmessage failed")
2404 return "", fmt.Errorf("signmessage failed: %w", err)
2405 }
2406 if resp == nil {
2407 return "", fmt.Errorf("signmessage result empty")
2408 }
2409
2410 return resp.Zbase, nil
2411 }
2412
2413 func (c *CLNService) UpdateChannel(ctx context.Context, updateChannelRequest *lnclient.UpdateChannelRequest) error {
2414 logger.Logger.WithFields(logrus.Fields{
2415 "updateChannelRequest": updateChannelRequest,
2416 }).Debug("Updating Channel")
2417
2418 req := &clngrpc.SetchannelRequest{
2419 Id: updateChannelRequest.ChannelId,
2420 Feebase: &clngrpc.Amount{Msat: uint64(updateChannelRequest.ForwardingFeeBaseMsat)},
2421 Feeppm: &updateChannelRequest.ForwardingFeeProportionalMillionths,
2422 }
2423
2424 resp, err := c.client.SetChannel(ctx, req)
2425 if err != nil {
2426 logger.Logger.WithError(err).Error("setchannel failed")
2427 return fmt.Errorf("setchannel failed: %w", err)
2428 }
2429 if resp == nil {
2430 return fmt.Errorf("setchannel result empty")
2431 }
2432
2433 return nil
2434 }
2435
2436 func (c *CLNService) UpdateLastWalletSyncRequest() {
2437 }
2438
2439 func atOrAboveVersion(myVersion string, minVersion string) (bool, error) {
2440 idx := strings.IndexRune(myVersion, 'v')
2441 if idx == -1 {
2442 return false, fmt.Errorf("could not find v in version string")
2443 }
2444 cleanStartMyVersion := myVersion[idx+1:]
2445
2446 var builder strings.Builder
2447 for _, r := range cleanStartMyVersion {
2448 if unicode.IsDigit(r) || r == '.' {
2449 builder.WriteRune(r)
2450 } else {
2451 break
2452 }
2453 }
2454 fullCleanMyVersion := builder.String()
2455
2456 myVersionParts := strings.Split(fullCleanMyVersion, ".")
2457 minVersionParts := strings.Split(minVersion, ".")
2458
2459 if len(myVersionParts) <= 1 || len(myVersionParts) > 3 {
2460 return false, fmt.Errorf("version string parse error: %s", myVersion)
2461 }
2462
2463 for i := 0; i < len(myVersionParts) && i < len(minVersionParts); i++ {
2464 myNum, err := strconv.ParseUint(myVersionParts[i], 10, 32)
2465 if err != nil {
2466 return false, err
2467 }
2468 minNum, err := strconv.ParseUint(minVersionParts[i], 10, 32)
2469 if err != nil {
2470 return false, err
2471 }
2472
2473 if myNum != minNum {
2474 return myNum > minNum, nil
2475 }
2476 }
2477
2478 return len(myVersionParts) >= len(minVersionParts), nil
2479 }
2480