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