bark.go raw

   1  //go:build (darwin && (amd64 || arm64)) || (linux && (amd64 || arm64)) || (windows && amd64)
   2  
   3  package bark
   4  
   5  import (
   6  	"context"
   7  	"errors"
   8  	"fmt"
   9  	"strconv"
  10  	"strings"
  11  	"sync"
  12  	"time"
  13  
  14  	decodepay "github.com/nbd-wtf/ln-decodepay"
  15  	"github.com/sirupsen/logrus"
  16  	bark "gitlab.com/ark-bitcoin/bark-ffi-bindings/golang/bark"
  17  
  18  	"github.com/getAlby/hub/constants"
  19  	"github.com/getAlby/hub/events"
  20  	"github.com/getAlby/hub/lnclient"
  21  	"github.com/getAlby/hub/logger"
  22  	"github.com/getAlby/hub/nip47/notifications"
  23  )
  24  
  25  const (
  26  	// Subsystem name reported on movements produced when a lightning receive is
  27  	// claimed (see bark's Subsystem::LIGHTNING_RECEIVE).
  28  	lightningReceiveSubsystem = "lightning_receive"
  29  	// Subsystem name reported on movements produced for outgoing lightning
  30  	// payments (see bark's Subsystem::LIGHTNING_SEND).
  31  	lightningSendSubsystem = "lightning_send"
  32  	// The status a movement is created with; every other status is terminal.
  33  	movementStatusPending = "pending"
  34  	// Movement status reported once a movement has settled. A movement first
  35  	// appears as "pending" and is updated to this once complete.
  36  	movementStatusSuccessful = "successful"
  37  	// LightningReceive.State values at or past preimage reveal.
  38  	// "delivering" (added in bark 0.6.0) sits between preimage reveal and
  39  	// settlement: the claim is recorded and delivery resumes automatically,
  40  	// so the funds are already irrevocably received.
  41  	receiveStatePreimageRevealed = "preimage-revealed"
  42  	receiveStateDelivering       = "delivering"
  43  	receiveStateSettled          = "settled"
  44  	// Grace period to allow the notification loop to unwind on shutdown.
  45  	shutdownGracePeriod = 10 * time.Second
  46  )
  47  
  48  // Config holds the user-configurable settings for connecting to an Ark server.
  49  type Config struct {
  50  	// Network is the bitcoin network name (e.g. "signet", "bitcoin").
  51  	Network string
  52  	// ServerAddress is the Ark server URL.
  53  	ServerAddress string
  54  	// EsploraAddress is the Esplora server URL used for chain data.
  55  	EsploraAddress string
  56  	// ServerAccessToken is an optional access token required by some Ark
  57  	// servers (currently used to gate mainnet access ahead of a public launch).
  58  	ServerAccessToken string
  59  	// LogLevel is the logrus level (as an int string, e.g. "3" for Info) used
  60  	// for bark's own internal logs. Defaults to Info if empty/unparseable.
  61  	LogLevel string
  62  	// LogToFile controls whether bark's logs are also written to a dedicated
  63  	// bark.log file alongside the other backend logs.
  64  	LogToFile bool
  65  }
  66  
  67  type BarkService struct {
  68  	wallet         *bark.Wallet
  69  	workDir        string
  70  	network        string
  71  	eventPublisher events.EventPublisher
  72  	pubkey         string
  73  	cancelFn       context.CancelFunc
  74  	loopWg         sync.WaitGroup
  75  	// payment_hash -> waiter that handleLightningSendMovement signals.
  76  	inflightSends    map[string]chan sendResult
  77  	inflightSendsMtx sync.Mutex
  78  }
  79  
  80  type sendResult struct {
  81  	preimage string
  82  	feeMsat  uint64
  83  	err      error
  84  }
  85  
  86  // parseNetwork maps an Alby Hub network name onto a bark network.
  87  func parseNetwork(network string) (bark.Network, error) {
  88  	switch network {
  89  	case "bitcoin", "mainnet":
  90  		return bark.NetworkBitcoin, nil
  91  	case "testnet":
  92  		return bark.NetworkTestnet, nil
  93  	case "signet":
  94  		return bark.NetworkSignet, nil
  95  	case "regtest":
  96  		return bark.NetworkRegtest, nil
  97  	default:
  98  		return 0, fmt.Errorf("unsupported bark network: %q", network)
  99  	}
 100  }
 101  
 102  func NewBarkService(ctx context.Context, eventPublisher events.EventPublisher, workDir, mnemonic string, config Config) (lnclient.LNClient, error) {
 103  	if mnemonic == "" {
 104  		return nil, errors.New("no mnemonic configured")
 105  	}
 106  	if workDir == "" {
 107  		return nil, errors.New("no bark work directory configured")
 108  	}
 109  	if config.ServerAddress == "" {
 110  		return nil, errors.New("no bark server address configured")
 111  	}
 112  
 113  	network, err := parseNetwork(config.Network)
 114  	if err != nil {
 115  		return nil, err
 116  	}
 117  
 118  	// Forward bark's internal logs into a dedicated logger. Done before opening
 119  	// the wallet so any logs emitted during open are captured.
 120  	logLevel, err := strconv.Atoi(config.LogLevel)
 121  	if err != nil {
 122  		logLevel = int(logrus.InfoLevel)
 123  	}
 124  	installBarkLogger(logrus.Level(logLevel), config.LogToFile, workDir)
 125  
 126  	// Usually, you have two wait 2 blocks. You can set nb_min_round_confirmations=0 to make it go faster.
 127  	roundTxRequiredConfirmations := uint32(0)
 128  
 129  	cfg := bark.Config{
 130  		ServerAddress:                config.ServerAddress,
 131  		RoundTxRequiredConfirmations: &roundTxRequiredConfirmations,
 132  	}
 133  	esploraAddress := config.EsploraAddress
 134  	if esploraAddress != "" {
 135  		cfg.EsploraAddress = &esploraAddress
 136  	}
 137  	if config.ServerAccessToken != "" {
 138  		token := config.ServerAccessToken
 139  		cfg.ServerAccessToken = &token
 140  	}
 141  
 142  	logger.Logger.WithField("workDir", workDir).Info("Opening Bark wallet")
 143  
 144  	// Bark provides a built-in background daemon that periodically syncs with
 145  	// the Ark server and blockchain, participates in rounds, and — crucially for
 146  	// us — claims incoming lightning receives via the mailbox (it long-polls for
 147  	// payment notifications and reveals the preimage, crediting the balance). We
 148  	// don't poll for receives ourselves; instead we observe the resulting wallet
 149  	// notifications (see runNotificationLoop) to emit payment-received events.
 150  	wallet, err := bark.WalletOpen(network, mnemonic, cfg, bark.WalletOpenArgs{
 151  		Datadir:           workDir,
 152  		RunDaemon:         true,
 153  		CreateIfNotExists: true,
 154  	})
 155  	if err != nil {
 156  		return nil, fmt.Errorf("failed to open bark wallet: %w", err)
 157  	}
 158  
 159  	loopCtx, cancelFn := context.WithCancel(context.Background())
 160  	bs := &BarkService{
 161  		wallet:         wallet,
 162  		workDir:        workDir,
 163  		network:        config.Network,
 164  		eventPublisher: eventPublisher,
 165  		pubkey:         wallet.Fingerprint(),
 166  		cancelFn:       cancelFn,
 167  		inflightSends:  make(map[string]chan sendResult),
 168  	}
 169  
 170  	// Run maintenance immediately on startup so a wallet that was briefly
 171  	// offline refreshes any VTXOs that drifted towards expiry before they are
 172  	// swept by the server. This is fire-and-forget as it may join an Ark round
 173  	// and take some time.
 174  	go func() {
 175  		if err := bs.wallet.Maintenance(); err != nil {
 176  			logger.Logger.WithError(err).Warn("Bark startup maintenance failed")
 177  		}
 178  	}()
 179  
 180  	bs.loopWg.Add(1)
 181  	go bs.runNotificationLoop(loopCtx)
 182  
 183  	return bs, nil
 184  }
 185  
 186  // runNotificationLoop consumes the wallet's notification stream and publishes a
 187  // payment-received event whenever the daemon claims an incoming lightning
 188  // receive. The daemon does the actual claiming (it long-polls the mailbox and
 189  // reveals the preimage); claiming a receive produces a lightning-receive
 190  // movement, which surfaces here as a MovementCreated notification. This is
 191  // event-driven — NextNotification blocks until something happens — so we no
 192  // longer poll every few seconds.
 193  func (bs *BarkService) runNotificationLoop(ctx context.Context) {
 194  	defer bs.loopWg.Done()
 195  
 196  	notifications := bs.wallet.Notifications()
 197  	defer notifications.Destroy()
 198  
 199  	// NextNotification blocks; CancelNextNotificationWait unblocks it (returning
 200  	// nil) so the loop can exit promptly on shutdown.
 201  	go func() {
 202  		<-ctx.Done()
 203  		notifications.CancelNextNotificationWait()
 204  	}()
 205  
 206  	for {
 207  		if ctx.Err() != nil {
 208  			return
 209  		}
 210  		notif, err := notifications.NextNotification()
 211  		if err != nil {
 212  			logger.Logger.WithError(err).Debug("Bark NextNotification failed")
 213  			// Back off briefly so a persistent error doesn't spin the loop.
 214  			select {
 215  			case <-ctx.Done():
 216  				return
 217  			case <-time.After(time.Second):
 218  			}
 219  			continue
 220  		}
 221  		if notif == nil {
 222  			// nil is returned when the wait was cancelled (shutdown) or the
 223  			// notification source was shut down permanently.
 224  			return
 225  		}
 226  		bs.handleNotification(*notif)
 227  	}
 228  }
 229  
 230  func (bs *BarkService) handleNotification(notif bark.WalletNotification) {
 231  	logger.Logger.WithFields(notificationLogFields(notif)).Debug("Received Bark notification")
 232  
 233  	var movement bark.Movement
 234  	switch n := notif.(type) {
 235  	case bark.WalletNotificationMovementCreated:
 236  		movement = n.Movement
 237  	case bark.WalletNotificationMovementUpdated:
 238  		movement = n.Movement
 239  	default:
 240  		// Channel lagging and other kinds carry no movement to act on.
 241  		return
 242  	}
 243  
 244  	switch {
 245  	case strings.Contains(movement.SubsystemName, lightningReceiveSubsystem):
 246  		bs.handleLightningReceiveMovement(movement)
 247  	case strings.Contains(movement.SubsystemName, lightningSendSubsystem):
 248  		bs.handleLightningSendMovement(movement)
 249  	}
 250  }
 251  
 252  func (bs *BarkService) handleLightningReceiveMovement(movement bark.Movement) {
 253  	// A receive is only credited once its movement settles. We always hold the
 254  	// preimage for our own receives, so PreimageRevealed isn't a useful signal;
 255  	// the balance is credited when the movement status reaches "successful".
 256  	// An abandoned receive finishes as "canceled": no funds arrived, so there is
 257  	// nothing to report.
 258  	if movement.Status != movementStatusSuccessful {
 259  		return
 260  	}
 261  
 262  	paymentHash, ok := paymentHashFromMovement(movement)
 263  	if !ok {
 264  		return
 265  	}
 266  
 267  	receive, err := bs.wallet.LightningReceiveState(paymentHash)
 268  	if err != nil {
 269  		logger.Logger.WithError(err).WithField("paymentHash", paymentHash).Warn("Failed to look up claimed Bark receive")
 270  		return
 271  	}
 272  
 273  	tx, err := bs.lightningReceiveToTransaction(&receive)
 274  	if err != nil {
 275  		logger.Logger.WithError(err).WithField("paymentHash", receive.PaymentHash).Warn("Failed to convert claimed Bark receive to transaction")
 276  		return
 277  	}
 278  	logger.Logger.WithFields(logrus.Fields{
 279  		"paymentHash": receive.PaymentHash,
 280  		"amountSats":  receive.AmountSats,
 281  	}).Info("Bark lightning receive claimed")
 282  	bs.eventPublisher.Publish(&events.Event{
 283  		Event:      "nwc_lnclient_payment_received",
 284  		Properties: tx,
 285  	})
 286  }
 287  
 288  // handleLightningSendMovement delivers a terminal lightning_send outcome to
 289  // the SendPaymentSync waiter for the matching payment_hash. If no waiter is
 290  // registered (e.g. the hub was restarted mid-send and SendPaymentSync's
 291  // goroutine is gone) it falls back to publishing nwc_lnclient_payment_sent /
 292  // _failed so the transactions service can recover the db transaction state.
 293  func (bs *BarkService) handleLightningSendMovement(movement bark.Movement) {
 294  	if movement.Status == movementStatusPending {
 295  		return
 296  	}
 297  
 298  	paymentHash, ok := paymentHashFromMovement(movement)
 299  	if !ok {
 300  		return
 301  	}
 302  
 303  	// The movement can be canceled or failed so we should just check if it
 304  	// wasn't successful.
 305  	if movement.Status != movementStatusSuccessful {
 306  		reason := fmt.Sprintf("bark lightning send %s", movement.Status)
 307  		logger.Logger.WithFields(logrus.Fields{
 308  			"paymentHash": paymentHash,
 309  			"status":      movement.Status,
 310  			"reason":      reason,
 311  		}).Warn("Bark lightning send did not succeed")
 312  		bs.deliverSendResult(paymentHash, sendResult{err: errors.New(reason)}, func() {
 313  			bs.eventPublisher.Publish(&events.Event{
 314  				Event: "nwc_lnclient_payment_failed",
 315  				Properties: &lnclient.PaymentFailedEventProperties{
 316  					Transaction: &lnclient.Transaction{
 317  						Type:        constants.TRANSACTION_TYPE_OUTGOING,
 318  						PaymentHash: paymentHash,
 319  					},
 320  					Reason: reason,
 321  				},
 322  			})
 323  		})
 324  		return
 325  	}
 326  
 327  	preimage, err := bs.getSettledSendPreimage(paymentHash)
 328  	if err != nil {
 329  		logger.Logger.WithError(err).WithField("paymentHash", paymentHash).Error("Bark lightning send reported successful but no preimage is available")
 330  		bs.deliverSendResult(paymentHash, sendResult{err: fmt.Errorf("bark lightning send completed without a preimage: %w", err)}, nil)
 331  		return
 332  	}
 333  
 334  	feeMsat := movement.OffchainFeeSats * 1000
 335  	logger.Logger.WithFields(logrus.Fields{
 336  		"paymentHash": paymentHash,
 337  		"feeMsat":     feeMsat,
 338  	}).Info("Bark lightning send completed")
 339  
 340  	bs.deliverSendResult(paymentHash, sendResult{preimage: preimage, feeMsat: feeMsat}, func() {
 341  		settledAt := time.Now().Unix()
 342  		bs.eventPublisher.Publish(&events.Event{
 343  			Event: "nwc_lnclient_payment_sent",
 344  			Properties: &lnclient.Transaction{
 345  				Type:         constants.TRANSACTION_TYPE_OUTGOING,
 346  				PaymentHash:  paymentHash,
 347  				Preimage:     preimage,
 348  				FeesPaidMsat: int64(feeMsat),
 349  				SettledAt:    &settledAt,
 350  			},
 351  		})
 352  	})
 353  }
 354  
 355  // Reads the preimage from the lightning-send's own state. Bark records the paid
 356  // invoice before finishing the movement, so it is always persisted by the time
 357  // the successful movement is observed.
 358  func (bs *BarkService) getSettledSendPreimage(paymentHash string) (string, error) {
 359  	status, err := bs.wallet.LightningSendState(paymentHash)
 360  	if err != nil {
 361  		return "", fmt.Errorf("failed to look up bark lightning send state: %w", err)
 362  	}
 363  	paid, ok := status.(bark.LightningSendStatusPaid)
 364  	if !ok {
 365  		return "", fmt.Errorf("send is in state %T, expected settled", status)
 366  	}
 367  	if paid.Preimage == "" {
 368  		return "", errors.New("settled send has an empty preimage")
 369  	}
 370  	return paid.Preimage, nil
 371  }
 372  
 373  // deliverSendResult delivers to the SendPaymentSync waiter if present, else
 374  // runs fallback (used to publish an event for the hub-restart recovery path).
 375  func (bs *BarkService) deliverSendResult(paymentHash string, res sendResult, fallback func()) {
 376  	if ch, ok := bs.takeInflightSend(paymentHash); ok {
 377  		ch <- res
 378  		return
 379  	}
 380  	if fallback != nil {
 381  		fallback()
 382  	}
 383  }
 384  
 385  func paymentHashFromMovement(movement bark.Movement) (string, bool) {
 386  	if movement.PaymentHash == nil || *movement.PaymentHash == "" {
 387  		logger.Logger.WithFields(logrus.Fields{
 388  			"movementId":    movement.Id,
 389  			"subsystemName": movement.SubsystemName,
 390  		}).Debug("Bark lightning movement missing payment_hash")
 391  		return "", false
 392  	}
 393  	return *movement.PaymentHash, true
 394  }
 395  
 396  // notificationLogFields turns a Bark wallet notification into structured log
 397  // fields describing its concrete type, rather than logging the raw interface
 398  // pointer (which would just print an address).
 399  func notificationLogFields(notif bark.WalletNotification) logrus.Fields {
 400  	switch n := notif.(type) {
 401  	case bark.WalletNotificationMovementCreated:
 402  		return movementLogFields("movement_created", n.Movement)
 403  	case bark.WalletNotificationMovementUpdated:
 404  		return movementLogFields("movement_updated", n.Movement)
 405  	case bark.WalletNotificationChannelLagging:
 406  		return logrus.Fields{"kind": "channel_lagging"}
 407  	default:
 408  		return logrus.Fields{"kind": fmt.Sprintf("%T", notif)}
 409  	}
 410  }
 411  
 412  func movementLogFields(kind string, m bark.Movement) logrus.Fields {
 413  	return logrus.Fields{
 414  		"kind":                 kind,
 415  		"movementId":           m.Id,
 416  		"status":               m.Status,
 417  		"subsystemName":        m.SubsystemName,
 418  		"subsystemKind":        m.SubsystemKind,
 419  		"metadataJson":         m.MetadataJson,
 420  		"intendedBalanceSats":  m.IntendedBalanceSats,
 421  		"effectiveBalanceSats": m.EffectiveBalanceSats,
 422  		"offchainFeeSats":      m.OffchainFeeSats,
 423  		"sentToAddresses":      m.SentToAddresses,
 424  		"receivedOnAddresses":  m.ReceivedOnAddresses,
 425  		"inputVtxoIds":         m.InputVtxoIds,
 426  		"outputVtxoIds":        m.OutputVtxoIds,
 427  		"exitedVtxoIds":        m.ExitedVtxoIds,
 428  		"createdAt":            m.CreatedAt,
 429  		"updatedAt":            m.UpdatedAt,
 430  		"completedAt":          m.CompletedAt,
 431  	}
 432  }
 433  
 434  func (bs *BarkService) MakeInvoice(ctx context.Context, amountMsat int64, description string, descriptionHash string, expiry int64, throughNodePubkey *string) (*lnclient.Transaction, error) {
 435  	if amountMsat <= 0 {
 436  		return nil, errors.New("0-amount invoices not supported")
 437  	}
 438  	if amountMsat%1000 != 0 {
 439  		return nil, errors.New("amount must be a whole number of sats")
 440  	}
 441  
 442  	var desc *string
 443  	if description != "" {
 444  		desc = &description
 445  	}
 446  
 447  	// The nil argument is an optional anti-DoS token, which we don't use.
 448  	invoice, err := bs.wallet.Bolt11Invoice(uint64(amountMsat/1000), desc, nil)
 449  	if err != nil {
 450  		return nil, fmt.Errorf("bark Bolt11Invoice failed: %w", err)
 451  	}
 452  
 453  	paymentRequest, err := decodepay.Decodepay(invoice.Invoice)
 454  	if err != nil {
 455  		logger.Logger.WithError(err).WithField("bolt11", invoice.Invoice).Error("Failed to decode bark-generated bolt11 invoice")
 456  		return nil, err
 457  	}
 458  
 459  	expiresAtUnix := time.UnixMilli(int64(paymentRequest.CreatedAt) * 1000).Add(time.Duration(paymentRequest.Expiry) * time.Second).Unix()
 460  
 461  	// The preimage is generated alongside the invoice but is not returned by
 462  	// Bolt11Invoice. Fetch it via the receive state so consumers can rely on
 463  	// lookup_invoice exposing the real preimage.
 464  	receive, err := bs.wallet.LightningReceiveState(paymentRequest.PaymentHash)
 465  	if err != nil {
 466  		logger.Logger.WithError(err).WithField("paymentHash", paymentRequest.PaymentHash).Error("Failed to fetch bark receive state for preimage")
 467  		return nil, fmt.Errorf("failed to fetch bark receive state for preimage: %w", err)
 468  	}
 469  	if receive.PaymentPreimage == nil || *receive.PaymentPreimage == "" {
 470  		return nil, errors.New("no preimage available")
 471  	}
 472  	preimage := *receive.PaymentPreimage
 473  
 474  	return &lnclient.Transaction{
 475  		Type:            constants.TRANSACTION_TYPE_INCOMING,
 476  		Invoice:         invoice.Invoice,
 477  		Preimage:        preimage,
 478  		PaymentHash:     paymentRequest.PaymentHash,
 479  		AmountMsat:      amountMsat,
 480  		CreatedAt:       int64(paymentRequest.CreatedAt),
 481  		ExpiresAt:       &expiresAtUnix,
 482  		Description:     paymentRequest.Description,
 483  		DescriptionHash: paymentRequest.DescriptionHash,
 484  	}, nil
 485  }
 486  
 487  func (bs *BarkService) SendPaymentSync(invoice string, amountMsat *uint64) (*lnclient.PayInvoiceResponse, error) {
 488  	// 0-amount invoices not supported initially — keeps the surface minimal.
 489  	if amountMsat != nil {
 490  		return nil, errors.New("0-amount invoices not supported")
 491  	}
 492  
 493  	paymentRequest, decodeErr := decodepay.Decodepay(invoice)
 494  	if decodeErr != nil {
 495  		return nil, fmt.Errorf("failed to decode invoice: %w", decodeErr)
 496  	}
 497  	paymentHash := paymentRequest.PaymentHash
 498  
 499  	// Register a waiter BEFORE initiating the send so a notification that
 500  	// arrives before this goroutine reaches the receive cannot be missed.
 501  	resultCh := make(chan sendResult, 1)
 502  	if err := bs.registerInflightSend(paymentHash, resultCh); err != nil {
 503  		return nil, err
 504  	}
 505  	defer bs.clearInflightSend(paymentHash)
 506  
 507  	if _, err := bs.wallet.PayLightningInvoice(invoice, nil, false); err != nil {
 508  		return nil, fmt.Errorf("bark PayLightningInvoice failed: %w", err)
 509  	}
 510  
 511  	// Block until handleLightningSendMovement delivers a terminal result.
 512  	res := <-resultCh
 513  	if res.err != nil {
 514  		return nil, res.err
 515  	}
 516  	return &lnclient.PayInvoiceResponse{
 517  		Preimage: res.preimage,
 518  		FeeMsat:  res.feeMsat,
 519  	}, nil
 520  }
 521  
 522  func (bs *BarkService) registerInflightSend(paymentHash string, ch chan sendResult) error {
 523  	bs.inflightSendsMtx.Lock()
 524  	defer bs.inflightSendsMtx.Unlock()
 525  	if _, exists := bs.inflightSends[paymentHash]; exists {
 526  		return fmt.Errorf("a bark lightning send is already in flight for payment hash %s", paymentHash)
 527  	}
 528  	bs.inflightSends[paymentHash] = ch
 529  	return nil
 530  }
 531  
 532  func (bs *BarkService) clearInflightSend(paymentHash string) {
 533  	bs.inflightSendsMtx.Lock()
 534  	defer bs.inflightSendsMtx.Unlock()
 535  	delete(bs.inflightSends, paymentHash)
 536  }
 537  
 538  func (bs *BarkService) takeInflightSend(paymentHash string) (chan sendResult, bool) {
 539  	bs.inflightSendsMtx.Lock()
 540  	defer bs.inflightSendsMtx.Unlock()
 541  	ch, ok := bs.inflightSends[paymentHash]
 542  	if ok {
 543  		delete(bs.inflightSends, paymentHash)
 544  	}
 545  	return ch, ok
 546  }
 547  
 548  func (bs *BarkService) LookupInvoice(ctx context.Context, paymentHash string) (*lnclient.Transaction, error) {
 549  	return nil, errors.New("this method should not be called")
 550  }
 551  
 552  func (bs *BarkService) lightningReceiveToTransaction(receive *bark.LightningReceive) (*lnclient.Transaction, error) {
 553  	paymentRequest, err := decodepay.Decodepay(receive.Invoice)
 554  	if err != nil {
 555  		return nil, err
 556  	}
 557  	expiresAtUnix := time.UnixMilli(int64(paymentRequest.CreatedAt) * 1000).Add(time.Duration(paymentRequest.Expiry) * time.Second).Unix()
 558  
 559  	tx := &lnclient.Transaction{
 560  		Type:            constants.TRANSACTION_TYPE_INCOMING,
 561  		Invoice:         receive.Invoice,
 562  		PaymentHash:     receive.PaymentHash,
 563  		AmountMsat:      paymentRequest.MSatoshi,
 564  		CreatedAt:       int64(paymentRequest.CreatedAt),
 565  		ExpiresAt:       &expiresAtUnix,
 566  		Description:     paymentRequest.Description,
 567  		DescriptionHash: paymentRequest.DescriptionHash,
 568  	}
 569  	// Only report the receive as settled when we can include the preimage —
 570  	// a settled transaction without one is rejected by the transactions
 571  	// service.
 572  	if receive.PaymentPreimage != nil && receiveIsPaid(receive.State) {
 573  		tx.Preimage = *receive.PaymentPreimage
 574  		settledAt := time.Now().Unix()
 575  		if receive.SettledAt != nil {
 576  			settledAt = *receive.SettledAt
 577  		}
 578  		tx.SettledAt = &settledAt
 579  	}
 580  	return tx, nil
 581  }
 582  
 583  // receiveIsPaid reports whether a receive's state is at or past preimage
 584  // reveal, meaning the payer holds the preimage and the payment is final.
 585  // The state is the only reliable signal: bark generates and stores the
 586  // preimage at invoice creation, so LightningReceive.PaymentPreimage can be
 587  // set long before anything is paid.
 588  func receiveIsPaid(state string) bool {
 589  	switch state {
 590  	case receiveStatePreimageRevealed, receiveStateDelivering, receiveStateSettled:
 591  		return true
 592  	}
 593  	return false
 594  }
 595  
 596  func (bs *BarkService) GetBalances(ctx context.Context, includeInactiveChannels bool) (*lnclient.BalancesResponse, error) {
 597  	balance, err := bs.wallet.Balance()
 598  	if err != nil {
 599  		return nil, err
 600  	}
 601  	spendableMsat := int64(balance.SpendableSats) * 1000
 602  
 603  	return &lnclient.BalancesResponse{
 604  		Onchain: lnclient.OnchainBalanceResponse{
 605  			PendingBalancesDetails:      []lnclient.PendingBalanceDetails{},
 606  			PendingSweepBalancesDetails: []lnclient.PendingBalanceDetails{},
 607  		},
 608  		Lightning: lnclient.LightningBalanceResponse{
 609  			TotalSpendableMsat:      spendableMsat,
 610  			NextMaxSpendableMsat:    spendableMsat,
 611  			NextMaxSpendableMPPMsat: spendableMsat,
 612  		},
 613  	}, nil
 614  }
 615  
 616  func (bs *BarkService) GetInfo(ctx context.Context) (*lnclient.NodeInfo, error) {
 617  	return &lnclient.NodeInfo{
 618  		Alias:   "Bark",
 619  		Color:   "#897FFF",
 620  		Pubkey:  bs.pubkey,
 621  		Network: bs.network,
 622  	}, nil
 623  }
 624  
 625  func (bs *BarkService) GetNodeStatus(ctx context.Context) (*lnclient.NodeStatus, error) {
 626  	return &lnclient.NodeStatus{
 627  		IsReady: true,
 628  	}, nil
 629  }
 630  
 631  func (bs *BarkService) GetNodeConnectionInfo(ctx context.Context) (*lnclient.NodeConnectionInfo, error) {
 632  	return &lnclient.NodeConnectionInfo{
 633  		Pubkey: bs.pubkey,
 634  	}, nil
 635  }
 636  
 637  func (bs *BarkService) GetPubkey() string {
 638  	return bs.pubkey
 639  }
 640  
 641  func (bs *BarkService) GetSupportedNIP47Methods() []string {
 642  	return []string{"pay_invoice", "get_balance", "get_budget", "get_info", "make_invoice", "lookup_invoice", "list_transactions", "multi_pay_invoice"}
 643  }
 644  
 645  func (bs *BarkService) GetSupportedNIP47NotificationTypes() []string {
 646  	// payment_received is emitted from runNotificationLoop when the daemon
 647  	// claims an incoming receive; payment_sent is emitted by the transactions
 648  	// service when our synchronous SendPaymentSync succeeds.
 649  	return []string{
 650  		notifications.PAYMENT_RECEIVED_NOTIFICATION,
 651  		notifications.PAYMENT_SENT_NOTIFICATION,
 652  	}
 653  }
 654  
 655  func (bs *BarkService) Shutdown() error {
 656  	if bs.cancelFn != nil {
 657  		bs.cancelFn()
 658  		done := make(chan struct{})
 659  		go func() {
 660  			bs.loopWg.Wait()
 661  			close(done)
 662  		}()
 663  		select {
 664  		case <-done:
 665  		case <-time.After(shutdownGracePeriod):
 666  			logger.Logger.Warn("Timed out waiting for Bark background loops to stop")
 667  		}
 668  	}
 669  	if err := bs.wallet.StopDaemon(); err != nil {
 670  		logger.Logger.WithError(err).Warn("Bark StopDaemon failed")
 671  	}
 672  	bs.wallet.Destroy()
 673  	return nil
 674  }
 675  
 676  // --- unsupported / stubbed methods ---
 677  
 678  func (bs *BarkService) SendKeysend(amountMsat uint64, destination string, customRecords []lnclient.TLVRecord, preimage string) (*lnclient.PayKeysendResponse, error) {
 679  	return nil, errors.New("keysend not supported")
 680  }
 681  
 682  func (bs *BarkService) MakeHoldInvoice(ctx context.Context, amountMsat int64, description string, descriptionHash string, expiry int64, paymentHash string, minCltvExpiryDelta *uint64) (*lnclient.Transaction, error) {
 683  	return nil, errors.New("not implemented")
 684  }
 685  
 686  func (bs *BarkService) SettleHoldInvoice(ctx context.Context, preimage string) error {
 687  	return errors.New("not implemented")
 688  }
 689  
 690  func (bs *BarkService) CancelHoldInvoice(ctx context.Context, paymentHash string) error {
 691  	return errors.New("not implemented")
 692  }
 693  
 694  func (bs *BarkService) ListChannels(ctx context.Context) ([]lnclient.Channel, error) {
 695  	return []lnclient.Channel{}, nil
 696  }
 697  
 698  func (bs *BarkService) ConnectPeer(ctx context.Context, connectPeerRequest *lnclient.ConnectPeerRequest) error {
 699  	return nil
 700  }
 701  
 702  func (bs *BarkService) OpenChannel(ctx context.Context, openChannelRequest *lnclient.OpenChannelRequest) (*lnclient.OpenChannelResponse, error) {
 703  	return nil, nil
 704  }
 705  
 706  func (bs *BarkService) CloseChannel(ctx context.Context, closeChannelRequest *lnclient.CloseChannelRequest) error {
 707  	return nil
 708  }
 709  
 710  func (bs *BarkService) UpdateChannel(ctx context.Context, updateChannelRequest *lnclient.UpdateChannelRequest) error {
 711  	return nil
 712  }
 713  
 714  func (bs *BarkService) DisconnectPeer(ctx context.Context, peerId string) error {
 715  	return nil
 716  }
 717  
 718  func (bs *BarkService) GetNewOnchainAddress(ctx context.Context) (string, error) {
 719  	return "", errors.New("not implemented")
 720  }
 721  
 722  func (bs *BarkService) GetOnchainBalance(ctx context.Context) (*lnclient.OnchainBalanceResponse, error) {
 723  	return &lnclient.OnchainBalanceResponse{}, nil
 724  }
 725  
 726  func (bs *BarkService) RedeemOnchainFunds(ctx context.Context, toAddress string, amountSat uint64, feeRate *uint64, sendAll bool) (string, error) {
 727  	return "", errors.New("not implemented")
 728  }
 729  
 730  func (bs *BarkService) ListPeers(ctx context.Context) ([]lnclient.PeerDetails, error) {
 731  	return nil, nil
 732  }
 733  
 734  func (bs *BarkService) GetNetworkGraph(ctx context.Context, nodeIds []string) (lnclient.NetworkGraphResponse, error) {
 735  	return nil, nil
 736  }
 737  
 738  func (bs *BarkService) GetLogOutput(ctx context.Context, maxLen int) ([]byte, error) {
 739  	return []byte{}, nil
 740  }
 741  
 742  func (bs *BarkService) SignMessage(ctx context.Context, message string) (string, error) {
 743  	return "", errors.New("not implemented")
 744  }
 745  
 746  func (bs *BarkService) GetStorageDir() (string, error) {
 747  	return bs.workDir, nil
 748  }
 749  
 750  func (bs *BarkService) ResetRouter(key string) error {
 751  	return errors.New("not implemented")
 752  }
 753  
 754  func (bs *BarkService) UpdateLastWalletSyncRequest() {}
 755  
 756  func (bs *BarkService) MakeOffer(ctx context.Context, description string) (string, error) {
 757  	return "", errors.New("not supported")
 758  }
 759  
 760  func (bs *BarkService) ListOnchainTransactions(ctx context.Context) ([]lnclient.OnchainTransaction, error) {
 761  	return nil, errors.ErrUnsupported
 762  }
 763  
 764  const (
 765  	nodeCommandDebug                  = "debug"
 766  	nodeCommandClaimLightningReceives = "claimlightningreceives"
 767  	nodeCommandRunMaintenance         = "runmaintenance"
 768  	nodeCommandRecoveryReport         = "recoveryreport"
 769  )
 770  
 771  func (bs *BarkService) GetCustomNodeCommandDefinitions() []lnclient.CustomNodeCommandDef {
 772  	return []lnclient.CustomNodeCommandDef{
 773  		{
 774  			Name:        nodeCommandDebug,
 775  			Description: "Dump the wallet's balance breakdown, VTXOs, pending lightning receives, movement history and Ark server info. Useful for debugging a receive that did not credit your balance.",
 776  			Args:        nil,
 777  		},
 778  		{
 779  			Name:        nodeCommandClaimLightningReceives,
 780  			Description: "Attempt to claim any pending/unclaimed lightning receives. Use this if an invoice was paid but the funds have not shown up in your balance.",
 781  			Args:        nil,
 782  		},
 783  		{
 784  			Name:        nodeCommandRunMaintenance,
 785  			Description: "Run wallet maintenance, which progresses pending rounds and refreshes VTXOs. Use this to nudge funds that are stuck 'pending in round'.",
 786  			Args:        nil,
 787  		},
 788  		{
 789  			Name:        nodeCommandRecoveryReport,
 790  			Description: "Show the result of the seed-recovery scan that runs when a wallet is created from an existing recovery phrase. Use this to verify your funds were restored after migrating to a new device.",
 791  			Args:        nil,
 792  		},
 793  	}
 794  }
 795  
 796  func (bs *BarkService) ExecuteCustomNodeCommand(ctx context.Context, command *lnclient.CustomNodeCommandRequest) (*lnclient.CustomNodeCommandResponse, error) {
 797  	switch command.Name {
 798  	case nodeCommandDebug:
 799  		return bs.executeCommandDebug()
 800  	case nodeCommandClaimLightningReceives:
 801  		return bs.executeCommandClaimLightningReceives()
 802  	case nodeCommandRunMaintenance:
 803  		return bs.executeCommandRunMaintenance()
 804  	case nodeCommandRecoveryReport:
 805  		return bs.executeCommandRecoveryReport()
 806  	}
 807  
 808  	return nil, lnclient.ErrUnknownCustomNodeCommand
 809  }
 810  
 811  func (bs *BarkService) executeCommandDebug() (*lnclient.CustomNodeCommandResponse, error) {
 812  	// Sync first so we report current state rather than a stale snapshot (the
 813  	// same pattern GetBalances uses before reading the balance).
 814  	if err := bs.wallet.Sync(); err != nil {
 815  		logger.Logger.WithError(err).Warn("Bark sync failed before collecting debug info")
 816  	}
 817  
 818  	response := map[string]interface{}{
 819  		"network": bs.network,
 820  		"pubkey":  bs.pubkey,
 821  	}
 822  
 823  	if balance, err := bs.wallet.Balance(); err != nil {
 824  		response["balanceError"] = err.Error()
 825  	} else {
 826  		response["balance"] = balance
 827  	}
 828  
 829  	if claimable, err := bs.wallet.ClaimableLightningReceiveBalanceSats(); err != nil {
 830  		response["claimableLightningReceiveSatsError"] = err.Error()
 831  	} else {
 832  		response["claimableLightningReceiveSats"] = claimable
 833  	}
 834  
 835  	if vtxos, err := bs.wallet.Vtxos(); err != nil {
 836  		response["vtxosError"] = err.Error()
 837  	} else {
 838  		response["vtxos"] = vtxos
 839  	}
 840  
 841  	if spendable, err := bs.wallet.SpendableVtxos(); err != nil {
 842  		response["spendableVtxosError"] = err.Error()
 843  	} else {
 844  		response["spendableVtxos"] = spendable
 845  	}
 846  
 847  	if pending, err := bs.wallet.PendingLightningReceives(); err != nil {
 848  		response["pendingLightningReceivesError"] = err.Error()
 849  	} else {
 850  		response["pendingLightningReceives"] = pending
 851  	}
 852  
 853  	if history, err := bs.wallet.History(); err != nil {
 854  		response["historyError"] = err.Error()
 855  	} else {
 856  		response["history"] = history
 857  	}
 858  
 859  	// Round state explains funds stuck in PendingInRoundSats: such funds sit in a
 860  	// round whose funding tx is waiting for confirmations (6 on mainnet), which
 861  	// the daemon progresses automatically once confirmed.
 862  	if rounds, err := bs.wallet.PendingRoundStates(); err != nil {
 863  		response["pendingRoundStatesError"] = err.Error()
 864  	} else {
 865  		response["pendingRoundStates"] = rounds
 866  	}
 867  
 868  	if nextRoundStartTime, err := bs.wallet.NextRoundStartTime(); err != nil {
 869  		response["nextRoundStartTimeError"] = err.Error()
 870  	} else {
 871  		response["nextRoundStartTime"] = nextRoundStartTime
 872  	}
 873  
 874  	if arkInfo := bs.wallet.ArkInfo(); arkInfo != nil {
 875  		response["arkInfo"] = arkInfo
 876  	}
 877  
 878  	return &lnclient.CustomNodeCommandResponse{
 879  		Response: response,
 880  	}, nil
 881  }
 882  
 883  func (bs *BarkService) executeCommandRunMaintenance() (*lnclient.CustomNodeCommandResponse, error) {
 884  	if err := bs.wallet.Maintenance(); err != nil {
 885  		return nil, fmt.Errorf("failed to run maintenance: %w", err)
 886  	}
 887  
 888  	logger.Logger.Debug("Ran Bark maintenance")
 889  
 890  	balance, err := bs.wallet.Balance()
 891  	if err != nil {
 892  		return nil, fmt.Errorf("maintenance succeeded but failed to read balance: %w", err)
 893  	}
 894  
 895  	return &lnclient.CustomNodeCommandResponse{
 896  		Response: map[string]interface{}{
 897  			"message": "Maintenance completed.",
 898  			"balance": balance,
 899  		},
 900  	}, nil
 901  }
 902  
 903  func (bs *BarkService) executeCommandClaimLightningReceives() (*lnclient.CustomNodeCommandResponse, error) {
 904  	if err := bs.wallet.Sync(); err != nil {
 905  		logger.Logger.WithError(err).Warn("Bark sync failed before claiming lightning receives")
 906  	}
 907  
 908  	// wait=false: attempt to claim what is already claimable without blocking on
 909  	// the server long-polling for not-yet-arrived payments.
 910  	claimed, err := bs.wallet.TryClaimAllLightningReceives(false)
 911  	if err != nil {
 912  		return nil, fmt.Errorf("failed to claim lightning receives: %w", err)
 913  	}
 914  
 915  	logger.Logger.WithField("count", len(claimed)).Info("Attempted to claim Bark lightning receives")
 916  
 917  	return &lnclient.CustomNodeCommandResponse{
 918  		Response: map[string]interface{}{
 919  			"claimedCount": len(claimed),
 920  			"claimed":      claimed,
 921  		},
 922  	}, nil
 923  }
 924  
 925  func (bs *BarkService) executeCommandRecoveryReport() (*lnclient.CustomNodeCommandResponse, error) {
 926  	// The report is produced by the seed-recovery scan bark runs during the
 927  	// wallet open that creates the wallet locally (e.g. when restoring from a
 928  	// recovery phrase on a new device). It is only available in the session
 929  	// that created the wallet; on subsequent starts no scan runs.
 930  	report := bs.wallet.RecoveryReport()
 931  	if report == nil {
 932  		return &lnclient.CustomNodeCommandResponse{
 933  			Response: map[string]interface{}{
 934  				"message": "No recovery scan ran on this wallet start. A scan only runs when the wallet is first created, e.g. after restoring from a recovery phrase.",
 935  			},
 936  		}, nil
 937  	}
 938  
 939  	return &lnclient.CustomNodeCommandResponse{
 940  		Response: map[string]interface{}{
 941  			"isComplete": report.IsComplete,
 942  			"recovered":  report.Recovered,
 943  			"skipped":    report.Skipped,
 944  			"foreign":    report.Foreign,
 945  			"failed":     report.Failed,
 946  			"exited":     report.Exited,
 947  		},
 948  	}, nil
 949  }
 950