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