chore: update bark bindings to v0.12.1 (#2477)

Moves from bark 0.2.3 to 0.4.0, which changed the FFI surface:

- WalletOpen takes the network and a WalletOpenArgs, replacing WalletCreate
  and the separate RunDaemon call.
- Bolt11Invoice takes an optional anti-DoS token, unused here.
- LightningReceiveStatus is now LightningReceiveState, reporting progress
  via State rather than a PreimageRevealed bool.

Movements expose PaymentHash and sends expose a typed terminal state, so both
are read from those instead of the movement metadata JSON. A send movement that
is neither pending nor successful now resolves the SendPaymentSync waiter
instead of being ignored.
This commit is contained in:
Peter 2026-07-29 08:25:25 +01:00 committed by GitHub
parent be17bc4e26
commit ce46a0f8a0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 78 additions and 64 deletions

2
go.mod
View file

@ -19,7 +19,7 @@ require (
github.com/stretchr/testify v1.11.1
github.com/tyler-smith/go-bip39 v1.1.0
github.com/wailsapp/wails/v2 v2.12.0
gitlab.com/ark-bitcoin/bark-ffi-bindings/golang v0.8.0
gitlab.com/ark-bitcoin/bark-ffi-bindings/golang v0.12.1
golang.org/x/crypto v0.52.0
golang.org/x/oauth2 v0.36.0
google.golang.org/grpc v1.79.3

4
go.sum
View file

@ -666,8 +666,8 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
gitlab.com/ark-bitcoin/bark-ffi-bindings/golang v0.8.0 h1:YKHSM8iNFMmTQ/QPUxC5U9KVim10F7sIGt3ou2wet+o=
gitlab.com/ark-bitcoin/bark-ffi-bindings/golang v0.8.0/go.mod h1:1jAwB/XR4i3D72fz3qWAd41tQLYcOCGfWZHMagn5fNg=
gitlab.com/ark-bitcoin/bark-ffi-bindings/golang v0.12.1 h1:jEnl0leC9n7EsT9Rn339nC9e/9GWCMPXzmzowzxmY24=
gitlab.com/ark-bitcoin/bark-ffi-bindings/golang v0.12.1/go.mod h1:1jAwB/XR4i3D72fz3qWAd41tQLYcOCGfWZHMagn5fNg=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.etcd.io/etcd/api/v3 v3.5.16 h1:WvmyJVbjWqK4R1E+B12RRHz3bRGy9XVfh++MgbN+6n0=

View file

@ -4,10 +4,8 @@ package bark
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"strconv"
"strings"
"sync"
@ -31,11 +29,14 @@ const (
// Subsystem name reported on movements produced for outgoing lightning
// payments (see bark's Subsystem::LIGHTNING_SEND).
lightningSendSubsystem = "lightning_send"
// The status a movement is created with; every other status is terminal.
movementStatusPending = "pending"
// Movement status reported once a movement has settled. A movement first
// appears as "pending" and is updated to this once complete.
movementStatusSuccessful = "successful"
// Movement status reported when a send was definitively not paid.
movementStatusFailed = "failed"
// LightningReceive.State values in which we hold the preimage.
receiveStatePreimageRevealed = "preimage-revealed"
receiveStateSettled = "settled"
// Grace period to allow the notification loop to unwind on shutdown.
shutdownGracePeriod = 10 * time.Second
)
@ -123,7 +124,6 @@ func NewBarkService(ctx context.Context, eventPublisher events.EventPublisher, w
cfg := bark.Config{
ServerAddress: config.ServerAddress,
Network: network,
RoundTxRequiredConfirmations: &roundTxRequiredConfirmations,
}
esploraAddress := config.EsploraAddress
@ -135,23 +135,7 @@ func NewBarkService(ctx context.Context, eventPublisher events.EventPublisher, w
cfg.ServerAccessToken = &token
}
_, statErr := os.Stat(workDir)
isFirstSetup := statErr != nil && errors.Is(statErr, os.ErrNotExist)
logger.Logger.WithFields(logrus.Fields{
"workDir": workDir,
"isFirstSetup": isFirstSetup,
}).Info("Opening Bark wallet")
var wallet *bark.Wallet
if isFirstSetup {
wallet, err = bark.WalletCreate(mnemonic, cfg, workDir, false)
} else {
wallet, err = bark.WalletOpen(mnemonic, cfg, workDir)
}
if err != nil {
return nil, fmt.Errorf("failed to open bark wallet: %w", err)
}
logger.Logger.WithField("workDir", workDir).Info("Opening Bark wallet")
// Bark provides a built-in background daemon that periodically syncs with
// the Ark server and blockchain, participates in rounds, and — crucially for
@ -159,8 +143,13 @@ func NewBarkService(ctx context.Context, eventPublisher events.EventPublisher, w
// payment notifications and reveals the preimage, crediting the balance). We
// don't poll for receives ourselves; instead we observe the resulting wallet
// notifications (see runNotificationLoop) to emit payment-received events.
if err := wallet.RunDaemon(nil); err != nil {
logger.Logger.WithError(err).Warn("Bark daemon failed to start")
wallet, err := bark.WalletOpen(network, mnemonic, cfg, bark.WalletOpenArgs{
Datadir: workDir,
RunDaemon: true,
CreateIfNotExists: true,
})
if err != nil {
return nil, fmt.Errorf("failed to open bark wallet: %w", err)
}
loopCtx, cancelFn := context.WithCancel(context.Background())
@ -260,6 +249,8 @@ func (bs *BarkService) handleLightningReceiveMovement(movement bark.Movement) {
// A receive is only credited once its movement settles. We always hold the
// preimage for our own receives, so PreimageRevealed isn't a useful signal;
// the balance is credited when the movement status reaches "successful".
// An abandoned receive finishes as "canceled": no funds arrived, so there is
// nothing to report.
if movement.Status != movementStatusSuccessful {
return
}
@ -269,13 +260,13 @@ func (bs *BarkService) handleLightningReceiveMovement(movement bark.Movement) {
return
}
receive, err := bs.wallet.LightningReceiveStatus(paymentHash)
if err != nil || receive == nil {
receive, err := bs.wallet.LightningReceiveState(paymentHash)
if err != nil {
logger.Logger.WithError(err).WithField("paymentHash", paymentHash).Warn("Failed to look up claimed Bark receive")
return
}
tx, err := bs.lightningReceiveToTransaction(receive)
tx, err := bs.lightningReceiveToTransaction(&receive)
if err != nil {
logger.Logger.WithError(err).WithField("paymentHash", receive.PaymentHash).Warn("Failed to convert claimed Bark receive to transaction")
return
@ -296,55 +287,60 @@ func (bs *BarkService) handleLightningReceiveMovement(movement bark.Movement) {
// goroutine is gone) it falls back to publishing nwc_lnclient_payment_sent /
// _failed so the transactions service can recover the db transaction state.
func (bs *BarkService) handleLightningSendMovement(movement bark.Movement) {
if movement.Status != movementStatusSuccessful && movement.Status != movementStatusFailed {
if movement.Status == movementStatusPending {
return
}
var meta struct {
PaymentHash string `json:"payment_hash"`
PaymentPreimage string `json:"payment_preimage"`
}
if err := json.Unmarshal([]byte(movement.MetadataJson), &meta); err != nil || meta.PaymentHash == "" {
logger.Logger.WithError(err).WithField("movementId", movement.Id).Debug("Bark lightning send movement missing payment_hash")
paymentHash, ok := paymentHashFromMovement(movement)
if !ok {
return
}
if movement.Status == movementStatusFailed {
bs.deliverSendResult(meta.PaymentHash, sendResult{err: errors.New("bark lightning send failed")}, func() {
// The movement can be canceled or failed so we should just check if it
// wasn't successful.
if movement.Status != movementStatusSuccessful {
reason := fmt.Sprintf("bark lightning send %s", movement.Status)
logger.Logger.WithFields(logrus.Fields{
"paymentHash": paymentHash,
"status": movement.Status,
"reason": reason,
}).Warn("Bark lightning send did not succeed")
bs.deliverSendResult(paymentHash, sendResult{err: errors.New(reason)}, func() {
bs.eventPublisher.Publish(&events.Event{
Event: "nwc_lnclient_payment_failed",
Properties: &lnclient.PaymentFailedEventProperties{
Transaction: &lnclient.Transaction{
Type: constants.TRANSACTION_TYPE_OUTGOING,
PaymentHash: meta.PaymentHash,
PaymentHash: paymentHash,
},
Reason: "bark lightning send failed",
Reason: reason,
},
})
})
return
}
if meta.PaymentPreimage == "" {
logger.Logger.WithField("paymentHash", meta.PaymentHash).Error("Bark lightning send reported successful but preimage is missing from movement metadata")
bs.deliverSendResult(meta.PaymentHash, sendResult{err: errors.New("bark lightning send completed without a preimage")}, nil)
preimage, err := bs.getSettledSendPreimage(paymentHash)
if err != nil {
logger.Logger.WithError(err).WithField("paymentHash", paymentHash).Error("Bark lightning send reported successful but no preimage is available")
bs.deliverSendResult(paymentHash, sendResult{err: fmt.Errorf("bark lightning send completed without a preimage: %w", err)}, nil)
return
}
feeMsat := movement.OffchainFeeSats * 1000
logger.Logger.WithFields(logrus.Fields{
"paymentHash": meta.PaymentHash,
"paymentHash": paymentHash,
"feeMsat": feeMsat,
}).Info("Bark lightning send completed")
bs.deliverSendResult(meta.PaymentHash, sendResult{preimage: meta.PaymentPreimage, feeMsat: feeMsat}, func() {
bs.deliverSendResult(paymentHash, sendResult{preimage: preimage, feeMsat: feeMsat}, func() {
settledAt := time.Now().Unix()
bs.eventPublisher.Publish(&events.Event{
Event: "nwc_lnclient_payment_sent",
Properties: &lnclient.Transaction{
Type: constants.TRANSACTION_TYPE_OUTGOING,
PaymentHash: meta.PaymentHash,
Preimage: meta.PaymentPreimage,
PaymentHash: paymentHash,
Preimage: preimage,
FeesPaidMsat: int64(feeMsat),
SettledAt: &settledAt,
},
@ -352,6 +348,24 @@ func (bs *BarkService) handleLightningSendMovement(movement bark.Movement) {
})
}
// Reads the preimage from the lightning-send's own state. Bark records the paid
// invoice before finishing the movement, so it is always persisted by the time
// the successful movement is observed.
func (bs *BarkService) getSettledSendPreimage(paymentHash string) (string, error) {
status, err := bs.wallet.LightningSendState(paymentHash)
if err != nil {
return "", fmt.Errorf("failed to look up bark lightning send state: %w", err)
}
paid, ok := status.(bark.LightningSendStatusPaid)
if !ok {
return "", fmt.Errorf("send is in state %T, expected settled", status)
}
if paid.Preimage == "" {
return "", errors.New("settled send has an empty preimage")
}
return paid.Preimage, nil
}
// deliverSendResult delivers to the SendPaymentSync waiter if present, else
// runs fallback (used to publish an event for the hub-restart recovery path).
func (bs *BarkService) deliverSendResult(paymentHash string, res sendResult, fallback func()) {
@ -365,17 +379,14 @@ func (bs *BarkService) deliverSendResult(paymentHash string, res sendResult, fal
}
func paymentHashFromMovement(movement bark.Movement) (string, bool) {
var meta struct {
PaymentHash string `json:"payment_hash"`
}
if err := json.Unmarshal([]byte(movement.MetadataJson), &meta); err != nil || meta.PaymentHash == "" {
logger.Logger.WithError(err).WithFields(logrus.Fields{
if movement.PaymentHash == nil || *movement.PaymentHash == "" {
logger.Logger.WithFields(logrus.Fields{
"movementId": movement.Id,
"subsystemName": movement.SubsystemName,
}).Debug("Bark lightning movement missing payment_hash")
return "", false
}
return meta.PaymentHash, true
return *movement.PaymentHash, true
}
// notificationLogFields turns a Bark wallet notification into structured log
@ -429,7 +440,8 @@ func (bs *BarkService) MakeInvoice(ctx context.Context, amountMsat int64, descri
desc = &description
}
invoice, err := bs.wallet.Bolt11Invoice(uint64(amountMsat/1000), desc)
// The nil argument is an optional anti-DoS token, which we don't use.
invoice, err := bs.wallet.Bolt11Invoice(uint64(amountMsat/1000), desc, nil)
if err != nil {
return nil, fmt.Errorf("bark Bolt11Invoice failed: %w", err)
}
@ -443,18 +455,17 @@ func (bs *BarkService) MakeInvoice(ctx context.Context, amountMsat int64, descri
expiresAtUnix := time.UnixMilli(int64(paymentRequest.CreatedAt) * 1000).Add(time.Duration(paymentRequest.Expiry) * time.Second).Unix()
// The preimage is generated alongside the invoice but is not returned by
// Bolt11Invoice. Fetch it via the receive status so consumers can rely on
// Bolt11Invoice. Fetch it via the receive state so consumers can rely on
// lookup_invoice exposing the real preimage.
var preimage string
receive, err := bs.wallet.LightningReceiveStatus(paymentRequest.PaymentHash)
receive, err := bs.wallet.LightningReceiveState(paymentRequest.PaymentHash)
if err != nil {
logger.Logger.WithError(err).WithField("paymentHash", paymentRequest.PaymentHash).Error("Failed to fetch bark receive status for preimage")
return nil, err
logger.Logger.WithError(err).WithField("paymentHash", paymentRequest.PaymentHash).Error("Failed to fetch bark receive state for preimage")
return nil, fmt.Errorf("failed to fetch bark receive state for preimage: %w", err)
}
preimage = receive.PaymentPreimage
if preimage == "" {
if receive.PaymentPreimage == nil || *receive.PaymentPreimage == "" {
return nil, errors.New("no preimage available")
}
preimage := *receive.PaymentPreimage
return &lnclient.Transaction{
Type: constants.TRANSACTION_TYPE_INCOMING,
@ -551,10 +562,13 @@ func (bs *BarkService) lightningReceiveToTransaction(receive *bark.LightningRece
Description: paymentRequest.Description,
DescriptionHash: paymentRequest.DescriptionHash,
}
if receive.PreimageRevealed {
// "preimage-revealed" until the claim is recorded, "settled" after.
if receive.State == receiveStatePreimageRevealed || receive.State == receiveStateSettled {
now := time.Now().Unix()
tx.SettledAt = &now
tx.Preimage = receive.PaymentPreimage
if receive.PaymentPreimage != nil {
tx.Preimage = *receive.PaymentPreimage
}
}
return tx, nil
}