2024-09-10 18:18:46 +02:00
|
|
|
package notifications
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"sync"
|
|
|
|
|
"time"
|
|
|
|
|
|
2024-10-30 14:18:12 +01:00
|
|
|
"github.com/lightninglabs/aperture/l402"
|
2024-09-10 18:18:46 +02:00
|
|
|
"github.com/lightninglabs/loop/swapserverrpc"
|
2025-01-29 20:31:58 +01:00
|
|
|
"github.com/lightningnetwork/lnd/lntypes"
|
2024-09-10 18:18:46 +02:00
|
|
|
"google.golang.org/grpc"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// NotificationType is the type of notification that the manager can handle.
|
|
|
|
|
type NotificationType int
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
// NotificationTypeUnknown is the default notification type.
|
|
|
|
|
NotificationTypeUnknown NotificationType = iota
|
|
|
|
|
|
|
|
|
|
// NotificationTypeReservation is the notification type for reservation
|
|
|
|
|
// notifications.
|
|
|
|
|
NotificationTypeReservation
|
2024-11-14 15:03:44 +01:00
|
|
|
|
|
|
|
|
// NotificationTypeStaticLoopInSweepRequest is the notification type for
|
|
|
|
|
// static loop in sweep requests.
|
|
|
|
|
NotificationTypeStaticLoopInSweepRequest
|
2025-10-30 17:55:09 +01:00
|
|
|
|
2026-07-08 13:56:11 +02:00
|
|
|
// NotificationTypeStaticLoopInRiskAccepted is the notification type for
|
|
|
|
|
// static loop in confirmation risk acceptance.
|
|
|
|
|
NotificationTypeStaticLoopInRiskAccepted
|
|
|
|
|
|
|
|
|
|
// NotificationTypeStaticLoopInRiskRejected is the notification type for
|
|
|
|
|
// static loop in confirmation risk rejection.
|
|
|
|
|
NotificationTypeStaticLoopInRiskRejected
|
|
|
|
|
|
2025-10-30 17:55:09 +01:00
|
|
|
// NotificationTypeUnfinishedSwap is the notification type for unfinished
|
|
|
|
|
// swap notifications.
|
|
|
|
|
NotificationTypeUnfinishedSwap
|
2026-04-03 19:41:39 -05:00
|
|
|
|
|
|
|
|
// NotificationTypeHtlcConfirmed is the notification type for HTLC
|
|
|
|
|
// confirmed notifications.
|
|
|
|
|
NotificationTypeHtlcConfirmed
|
2024-09-10 18:18:46 +02:00
|
|
|
)
|
|
|
|
|
|
2025-01-29 20:21:52 +01:00
|
|
|
const (
|
|
|
|
|
// defaultMinAliveConnTime is the default minimum time that the
|
|
|
|
|
// connection to the server needs to be alive before we consider it a
|
|
|
|
|
// successful connection.
|
|
|
|
|
defaultMinAliveConnTime = time.Minute
|
2025-10-30 17:56:14 +01:00
|
|
|
|
2026-04-03 19:41:39 -05:00
|
|
|
// htlcConfirmedSubscriberSendTimeout is how long we wait for a busy
|
|
|
|
|
// htlc-confirmed subscriber before dropping the notification.
|
|
|
|
|
htlcConfirmedSubscriberSendTimeout = 200 * time.Millisecond
|
|
|
|
|
|
2026-06-19 14:52:17 +02:00
|
|
|
// defaultMaxQueuedNotifications is the default number of notifications
|
|
|
|
|
// we queue per subscriber before dropping new notifications.
|
|
|
|
|
defaultMaxQueuedNotifications = 1024
|
|
|
|
|
|
2025-10-30 17:56:14 +01:00
|
|
|
// current_version is the current version of the notification listener.
|
|
|
|
|
current_version = swapserverrpc.SubscribeNotificationsRequest_V1
|
2025-01-29 20:21:52 +01:00
|
|
|
)
|
|
|
|
|
|
2024-09-10 18:18:46 +02:00
|
|
|
// Client is the interface that the notification manager needs to implement in
|
|
|
|
|
// order to be able to subscribe to notifications.
|
|
|
|
|
type Client interface {
|
|
|
|
|
// SubscribeNotifications subscribes to the notifications from the server.
|
|
|
|
|
SubscribeNotifications(ctx context.Context,
|
|
|
|
|
in *swapserverrpc.SubscribeNotificationsRequest,
|
|
|
|
|
opts ...grpc.CallOption) (
|
|
|
|
|
swapserverrpc.SwapServer_SubscribeNotificationsClient, error)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Config contains all the services that the notification manager needs to
|
|
|
|
|
// operate.
|
|
|
|
|
type Config struct {
|
|
|
|
|
// Client is the client used to communicate with the swap server.
|
|
|
|
|
Client Client
|
|
|
|
|
|
2024-10-30 14:18:12 +01:00
|
|
|
// CurrentToken returns the token that is currently contained in the
|
|
|
|
|
// store or an l402.ErrNoToken error if there is none.
|
|
|
|
|
CurrentToken func() (*l402.Token, error)
|
2025-01-29 20:21:52 +01:00
|
|
|
|
|
|
|
|
// MinAliveConnTime is the minimum time that the connection to the
|
|
|
|
|
// server needs to be alive before we consider it a successful.
|
|
|
|
|
MinAliveConnTime time.Duration
|
2026-06-19 14:52:17 +02:00
|
|
|
|
|
|
|
|
// MaxQueuedNotifications is the maximum number of notifications that
|
|
|
|
|
// can wait in each subscriber's delivery queue.
|
|
|
|
|
MaxQueuedNotifications int
|
2026-07-08 13:57:15 +02:00
|
|
|
|
|
|
|
|
// PersistStaticLoopInRiskDecision durably records static loop-in
|
|
|
|
|
// confirmation-risk decisions. If this fails, the notification is still
|
|
|
|
|
// cached and forwarded so a later subscriber can process it after the swap
|
|
|
|
|
// row exists.
|
|
|
|
|
PersistStaticLoopInRiskDecision func(context.Context, lntypes.Hash,
|
|
|
|
|
bool) error
|
2024-09-10 18:18:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Manager is a manager for notifications that the swap server sends to the
|
|
|
|
|
// client.
|
|
|
|
|
type Manager struct {
|
2025-08-19 17:06:42 +02:00
|
|
|
sync.Mutex
|
|
|
|
|
|
2024-09-10 18:18:46 +02:00
|
|
|
cfg *Config
|
|
|
|
|
|
|
|
|
|
hasL402 bool
|
|
|
|
|
|
2026-07-08 13:57:15 +02:00
|
|
|
// subscribers holds active notification subscribers by notification
|
|
|
|
|
// type. It is guarded by the Manager mutex.
|
2024-09-10 18:18:46 +02:00
|
|
|
subscribers map[NotificationType][]subscriber
|
2026-07-08 13:56:11 +02:00
|
|
|
|
2026-07-08 13:57:15 +02:00
|
|
|
// staticLoopInRiskAccepted caches accepted risk decisions by swap hash
|
|
|
|
|
// so a later matching subscriber can receive a previously delivered
|
|
|
|
|
// server decision.
|
2026-07-08 13:56:11 +02:00
|
|
|
staticLoopInRiskAccepted map[lntypes.Hash]*swapserverrpc.
|
|
|
|
|
ServerStaticLoopInRiskAcceptedNotification
|
|
|
|
|
|
2026-07-08 13:57:15 +02:00
|
|
|
// staticLoopInRiskRejected caches rejected risk decisions by swap hash
|
|
|
|
|
// so a later matching subscriber can receive a previously delivered
|
|
|
|
|
// server decision.
|
2026-07-08 13:56:11 +02:00
|
|
|
staticLoopInRiskRejected map[lntypes.Hash]*swapserverrpc.
|
|
|
|
|
ServerStaticLoopInRiskRejectedNotification
|
2026-07-08 13:57:15 +02:00
|
|
|
|
|
|
|
|
// staticLoopInRiskPersisted records whether the cached risk decision for
|
|
|
|
|
// a swap hash was durably persisted. Unpersisted decisions remain cached
|
|
|
|
|
// after subscriber cancellation so they can be replayed.
|
|
|
|
|
staticLoopInRiskPersisted map[lntypes.Hash]bool
|
2024-09-10 18:18:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewManager creates a new notification manager.
|
|
|
|
|
func NewManager(cfg *Config) *Manager {
|
2025-01-29 20:21:52 +01:00
|
|
|
// Set the default minimum alive connection time if it's not set.
|
|
|
|
|
if cfg.MinAliveConnTime == 0 {
|
|
|
|
|
cfg.MinAliveConnTime = defaultMinAliveConnTime
|
|
|
|
|
}
|
2026-06-19 14:52:17 +02:00
|
|
|
if cfg.MaxQueuedNotifications <= 0 {
|
|
|
|
|
cfg.MaxQueuedNotifications = defaultMaxQueuedNotifications
|
|
|
|
|
}
|
2025-01-29 20:21:52 +01:00
|
|
|
|
2024-09-10 18:18:46 +02:00
|
|
|
return &Manager{
|
|
|
|
|
cfg: cfg,
|
|
|
|
|
subscribers: make(map[NotificationType][]subscriber),
|
2026-07-08 13:56:11 +02:00
|
|
|
staticLoopInRiskAccepted: make(
|
|
|
|
|
map[lntypes.Hash]*swapserverrpc.
|
|
|
|
|
ServerStaticLoopInRiskAcceptedNotification,
|
|
|
|
|
),
|
|
|
|
|
staticLoopInRiskRejected: make(
|
|
|
|
|
map[lntypes.Hash]*swapserverrpc.
|
|
|
|
|
ServerStaticLoopInRiskRejectedNotification,
|
|
|
|
|
),
|
2026-07-08 13:57:15 +02:00
|
|
|
staticLoopInRiskPersisted: make(map[lntypes.Hash]bool),
|
2024-09-10 18:18:46 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type subscriber struct {
|
|
|
|
|
subCtx context.Context
|
2026-03-05 12:18:05 +01:00
|
|
|
recvChan any
|
2026-07-08 13:56:11 +02:00
|
|
|
swapHash *lntypes.Hash
|
2026-07-08 13:57:15 +02:00
|
|
|
enqueue func(any)
|
2026-06-19 14:52:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// newNotificationQueue creates a per-subscriber FIFO delivery function.
|
|
|
|
|
func newNotificationQueue[T any](ctx context.Context,
|
|
|
|
|
recvChan chan T, maxPending int) func(any) {
|
|
|
|
|
|
|
|
|
|
type queue struct {
|
|
|
|
|
sync.Mutex
|
|
|
|
|
|
|
|
|
|
pending []T
|
|
|
|
|
notify chan struct{}
|
|
|
|
|
closed bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
q := &queue{
|
|
|
|
|
notify: make(chan struct{}, 1),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
closeQueue := func() {
|
|
|
|
|
q.Lock()
|
|
|
|
|
q.closed = true
|
|
|
|
|
q.pending = nil
|
|
|
|
|
q.Unlock()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
|
defer close(recvChan)
|
|
|
|
|
defer closeQueue()
|
|
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
select {
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
return
|
|
|
|
|
default:
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
q.Lock()
|
|
|
|
|
if len(q.pending) == 0 {
|
|
|
|
|
q.Unlock()
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
case <-q.notify:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ntfn := q.pending[0]
|
|
|
|
|
var zero T
|
|
|
|
|
q.pending[0] = zero
|
|
|
|
|
q.pending = q.pending[1:]
|
|
|
|
|
q.Unlock()
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
case recvChan <- ntfn:
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
return func(ntfn any) {
|
|
|
|
|
typedNtfn, ok := ntfn.(T)
|
|
|
|
|
if !ok {
|
|
|
|
|
log.Warnf("unexpected notification type %T", ntfn)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
q.Lock()
|
|
|
|
|
if q.closed {
|
|
|
|
|
q.Unlock()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if len(q.pending) >= maxPending {
|
|
|
|
|
q.Unlock()
|
|
|
|
|
log.Warnf("dropping notification for slow subscriber: "+
|
|
|
|
|
"queue depth %d reached", maxPending)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
q.pending = append(q.pending, typedNtfn)
|
|
|
|
|
q.Unlock()
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
case q.notify <- struct{}{}:
|
|
|
|
|
default:
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// queueNotification queues or synchronously sends a must-deliver notification.
|
|
|
|
|
func queueNotification[T any](sub subscriber, recvChan chan T, ntfn T) {
|
|
|
|
|
if sub.enqueue != nil {
|
|
|
|
|
sub.enqueue(ntfn)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log.Warnf("subscriber has no notification queue, falling back to " +
|
|
|
|
|
"blocking send")
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
case recvChan <- ntfn:
|
|
|
|
|
case <-sub.subCtx.Done():
|
|
|
|
|
}
|
2024-09-10 18:18:46 +02:00
|
|
|
}
|
|
|
|
|
|
2026-07-08 13:57:15 +02:00
|
|
|
// dropNotification sends a best-effort notification to a subscriber.
|
|
|
|
|
func dropNotification[T any](sub subscriber, recvChan chan T, ntfn T,
|
|
|
|
|
description string) {
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
case recvChan <- ntfn:
|
|
|
|
|
case <-sub.subCtx.Done():
|
|
|
|
|
default:
|
|
|
|
|
log.Debugf("Dropping %s notification for slow subscriber",
|
|
|
|
|
description)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-10 18:18:46 +02:00
|
|
|
// SubscribeReservations subscribes to the reservation notifications.
|
|
|
|
|
func (m *Manager) SubscribeReservations(ctx context.Context,
|
|
|
|
|
) <-chan *swapserverrpc.ServerReservationNotification {
|
|
|
|
|
|
|
|
|
|
notifChan := make(chan *swapserverrpc.ServerReservationNotification, 1)
|
|
|
|
|
sub := subscriber{
|
|
|
|
|
subCtx: ctx,
|
|
|
|
|
recvChan: notifChan,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m.addSubscriber(NotificationTypeReservation, sub)
|
|
|
|
|
|
2025-10-30 17:55:09 +01:00
|
|
|
context.AfterFunc(ctx, func() {
|
2024-09-10 18:18:46 +02:00
|
|
|
m.removeSubscriber(NotificationTypeReservation, sub)
|
|
|
|
|
close(notifChan)
|
2025-10-30 17:55:09 +01:00
|
|
|
})
|
2024-09-10 18:18:46 +02:00
|
|
|
|
|
|
|
|
return notifChan
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-14 15:03:44 +01:00
|
|
|
// SubscribeStaticLoopInSweepRequests subscribes to the static loop in sweep
|
|
|
|
|
// requests.
|
|
|
|
|
func (m *Manager) SubscribeStaticLoopInSweepRequests(ctx context.Context,
|
|
|
|
|
) <-chan *swapserverrpc.ServerStaticLoopInSweepNotification {
|
|
|
|
|
|
|
|
|
|
notifChan := make(
|
|
|
|
|
chan *swapserverrpc.ServerStaticLoopInSweepNotification, 1,
|
|
|
|
|
)
|
2025-10-30 17:55:09 +01:00
|
|
|
|
2024-11-14 15:03:44 +01:00
|
|
|
sub := subscriber{
|
|
|
|
|
subCtx: ctx,
|
|
|
|
|
recvChan: notifChan,
|
2026-06-19 14:52:17 +02:00
|
|
|
enqueue: newNotificationQueue(
|
|
|
|
|
ctx, notifChan, m.cfg.MaxQueuedNotifications,
|
|
|
|
|
),
|
2024-11-14 15:03:44 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m.addSubscriber(NotificationTypeStaticLoopInSweepRequest, sub)
|
|
|
|
|
|
2025-10-30 17:55:09 +01:00
|
|
|
context.AfterFunc(ctx, func() {
|
2024-11-14 15:03:44 +01:00
|
|
|
m.removeSubscriber(
|
2025-10-30 17:55:09 +01:00
|
|
|
NotificationTypeStaticLoopInSweepRequest,
|
|
|
|
|
sub,
|
2024-11-14 15:03:44 +01:00
|
|
|
)
|
2025-10-30 17:55:09 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return notifChan
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 13:57:15 +02:00
|
|
|
func subscribeStaticLoopInRiskDecision[T any](m *Manager, ctx context.Context,
|
|
|
|
|
swapHash lntypes.Hash, notifType NotificationType,
|
|
|
|
|
notifications map[lntypes.Hash]T) <-chan T {
|
2026-07-08 13:56:11 +02:00
|
|
|
|
2026-07-08 13:57:15 +02:00
|
|
|
notifChan := make(chan T, 1)
|
2026-07-08 13:56:11 +02:00
|
|
|
sub := subscriber{
|
|
|
|
|
subCtx: ctx,
|
|
|
|
|
recvChan: notifChan,
|
|
|
|
|
swapHash: &swapHash,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m.Lock()
|
2026-07-08 13:57:15 +02:00
|
|
|
m.subscribers[notifType] = append(m.subscribers[notifType], sub)
|
|
|
|
|
if ntfn, ok := notifications[swapHash]; ok {
|
2026-07-08 13:56:11 +02:00
|
|
|
notifChan <- ntfn
|
2026-07-08 13:57:15 +02:00
|
|
|
if m.staticLoopInRiskPersisted[swapHash] {
|
|
|
|
|
delete(notifications, swapHash)
|
|
|
|
|
delete(m.staticLoopInRiskPersisted, swapHash)
|
|
|
|
|
}
|
2026-07-08 13:56:11 +02:00
|
|
|
}
|
|
|
|
|
m.Unlock()
|
|
|
|
|
|
|
|
|
|
context.AfterFunc(ctx, func() {
|
2026-07-08 13:57:15 +02:00
|
|
|
m.removeSubscriber(notifType, sub)
|
2026-07-08 13:56:11 +02:00
|
|
|
m.Lock()
|
2026-07-08 13:57:15 +02:00
|
|
|
if _, ok := notifications[swapHash]; ok &&
|
|
|
|
|
m.staticLoopInRiskPersisted[swapHash] {
|
|
|
|
|
|
|
|
|
|
delete(notifications, swapHash)
|
|
|
|
|
delete(m.staticLoopInRiskPersisted, swapHash)
|
|
|
|
|
}
|
2026-07-08 13:56:11 +02:00
|
|
|
m.Unlock()
|
|
|
|
|
close(notifChan)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return notifChan
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 13:57:15 +02:00
|
|
|
// SubscribeStaticLoopInRiskAccepted subscribes to static loop in risk accepted
|
2026-07-08 13:56:11 +02:00
|
|
|
// notifications.
|
2026-07-08 13:57:15 +02:00
|
|
|
func (m *Manager) SubscribeStaticLoopInRiskAccepted(ctx context.Context,
|
2026-07-08 13:56:11 +02:00
|
|
|
swapHash lntypes.Hash,
|
2026-07-08 13:57:15 +02:00
|
|
|
) <-chan *swapserverrpc.ServerStaticLoopInRiskAcceptedNotification {
|
2026-07-08 13:56:11 +02:00
|
|
|
|
2026-07-08 13:57:15 +02:00
|
|
|
return subscribeStaticLoopInRiskDecision(
|
|
|
|
|
m, ctx, swapHash, NotificationTypeStaticLoopInRiskAccepted,
|
|
|
|
|
m.staticLoopInRiskAccepted,
|
2026-07-08 13:56:11 +02:00
|
|
|
)
|
2026-07-08 13:57:15 +02:00
|
|
|
}
|
2026-07-08 13:56:11 +02:00
|
|
|
|
2026-07-08 13:57:15 +02:00
|
|
|
// SubscribeStaticLoopInRiskRejected subscribes to static loop in risk rejected
|
|
|
|
|
// notifications.
|
|
|
|
|
func (m *Manager) SubscribeStaticLoopInRiskRejected(ctx context.Context,
|
|
|
|
|
swapHash lntypes.Hash,
|
|
|
|
|
) <-chan *swapserverrpc.ServerStaticLoopInRiskRejectedNotification {
|
2026-07-08 13:56:11 +02:00
|
|
|
|
2026-07-08 13:57:15 +02:00
|
|
|
return subscribeStaticLoopInRiskDecision(
|
|
|
|
|
m, ctx, swapHash, NotificationTypeStaticLoopInRiskRejected,
|
|
|
|
|
m.staticLoopInRiskRejected,
|
2026-07-08 13:56:11 +02:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-30 17:55:09 +01:00
|
|
|
// SubscribeUnfinishedSwaps subscribes to the unfinished swap notifications.
|
|
|
|
|
func (m *Manager) SubscribeUnfinishedSwaps(ctx context.Context,
|
|
|
|
|
) <-chan *swapserverrpc.ServerUnfinishedSwapNotification {
|
|
|
|
|
|
|
|
|
|
notifChan := make(
|
|
|
|
|
chan *swapserverrpc.ServerUnfinishedSwapNotification, 1,
|
|
|
|
|
)
|
|
|
|
|
sub := subscriber{
|
|
|
|
|
subCtx: ctx,
|
|
|
|
|
recvChan: notifChan,
|
2026-06-19 14:52:17 +02:00
|
|
|
enqueue: newNotificationQueue(
|
|
|
|
|
ctx, notifChan, m.cfg.MaxQueuedNotifications,
|
|
|
|
|
),
|
2025-10-30 17:55:09 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m.addSubscriber(NotificationTypeUnfinishedSwap, sub)
|
|
|
|
|
context.AfterFunc(ctx, func() {
|
|
|
|
|
m.removeSubscriber(NotificationTypeUnfinishedSwap, sub)
|
|
|
|
|
})
|
2024-11-14 15:03:44 +01:00
|
|
|
|
|
|
|
|
return notifChan
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 19:41:39 -05:00
|
|
|
// SubscribeHtlcConfirmed subscribes to the HTLC confirmed notifications.
|
|
|
|
|
func (m *Manager) SubscribeHtlcConfirmed(ctx context.Context,
|
|
|
|
|
) <-chan *swapserverrpc.ServerHtlcConfirmedNotification {
|
|
|
|
|
|
|
|
|
|
notifChan := make(
|
|
|
|
|
chan *swapserverrpc.ServerHtlcConfirmedNotification, 1,
|
|
|
|
|
)
|
|
|
|
|
sub := subscriber{
|
|
|
|
|
subCtx: ctx,
|
|
|
|
|
recvChan: notifChan,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m.addSubscriber(NotificationTypeHtlcConfirmed, sub)
|
|
|
|
|
context.AfterFunc(ctx, func() {
|
|
|
|
|
m.removeSubscriber(NotificationTypeHtlcConfirmed, sub)
|
|
|
|
|
close(notifChan)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return notifChan
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-10 18:18:46 +02:00
|
|
|
// Run starts the notification manager. It will keep on running until the
|
|
|
|
|
// context is canceled. It will subscribe to notifications and forward them to
|
|
|
|
|
// the subscribers. On a first successful connection to the server, it will
|
|
|
|
|
// close the readyChan to signal that the manager is ready.
|
|
|
|
|
func (m *Manager) Run(ctx context.Context) error {
|
|
|
|
|
// Initially we want to immediately try to connect to the server.
|
2025-01-29 20:21:52 +01:00
|
|
|
var (
|
2025-01-29 20:31:58 +01:00
|
|
|
waitTime time.Duration
|
|
|
|
|
backoff time.Duration
|
|
|
|
|
attempts int
|
2025-02-06 19:45:24 +01:00
|
|
|
timer = time.NewTimer(0)
|
2025-01-29 20:21:52 +01:00
|
|
|
)
|
2024-09-10 18:18:46 +02:00
|
|
|
|
|
|
|
|
// Start the notification runloop.
|
|
|
|
|
for {
|
|
|
|
|
// Increase the wait time for the next iteration.
|
2025-01-29 20:31:58 +01:00
|
|
|
backoff = waitTime + time.Duration(attempts)*time.Second
|
2025-01-29 20:21:52 +01:00
|
|
|
waitTime = 0
|
2025-02-06 19:45:24 +01:00
|
|
|
|
|
|
|
|
// Reset the timer with the new backoff time.
|
|
|
|
|
timer.Reset(backoff)
|
2024-09-10 18:18:46 +02:00
|
|
|
|
|
|
|
|
// Return if the context has been canceled.
|
|
|
|
|
select {
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
return nil
|
|
|
|
|
|
|
|
|
|
case <-timer.C:
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// In order to create a valid l402 we first are going to call
|
2025-01-29 20:21:52 +01:00
|
|
|
// the FetchL402 method. As a client might not have outbound
|
|
|
|
|
// capacity yet, we'll retry until we get a valid response.
|
2024-09-10 18:18:46 +02:00
|
|
|
if !m.hasL402 {
|
2025-01-29 20:31:58 +01:00
|
|
|
token, err := m.cfg.CurrentToken()
|
2024-09-10 18:18:46 +02:00
|
|
|
if err != nil {
|
2025-01-29 20:21:52 +01:00
|
|
|
// We only log the error if it's not the case
|
|
|
|
|
// that we don't have a token yet to avoid
|
|
|
|
|
// spamming the logs.
|
2024-10-30 14:18:12 +01:00
|
|
|
if err != l402.ErrNoToken {
|
2025-01-29 20:21:52 +01:00
|
|
|
log.Errorf("Error getting L402 from "+
|
|
|
|
|
"the store: %v", err)
|
2024-10-30 14:18:12 +01:00
|
|
|
}
|
2025-02-06 19:45:24 +01:00
|
|
|
|
|
|
|
|
// Use a default of 1 second wait time to avoid
|
|
|
|
|
// hogging the CPU.
|
|
|
|
|
waitTime = time.Second
|
2024-09-10 18:18:46 +02:00
|
|
|
continue
|
|
|
|
|
}
|
2025-01-29 20:31:58 +01:00
|
|
|
|
|
|
|
|
// If the preimage is empty, we don't have a valid L402
|
|
|
|
|
// yet so we'll continue to retry with the incremental
|
|
|
|
|
// backoff.
|
|
|
|
|
emptyPreimage := lntypes.Preimage{}
|
|
|
|
|
if token.Preimage == emptyPreimage {
|
|
|
|
|
attempts++
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
attempts = 0
|
2024-09-10 18:18:46 +02:00
|
|
|
m.hasL402 = true
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-29 20:21:52 +01:00
|
|
|
connectAttempted := time.Now()
|
|
|
|
|
err := m.subscribeNotifications(ctx)
|
2024-09-10 18:18:46 +02:00
|
|
|
if err != nil {
|
2025-01-29 20:21:52 +01:00
|
|
|
log.Errorf("Error subscribing to notifications: %v",
|
|
|
|
|
err)
|
|
|
|
|
}
|
|
|
|
|
connectionAliveTime := time.Since(connectAttempted)
|
|
|
|
|
|
|
|
|
|
// Note that we may be able to connet to the stream but not
|
|
|
|
|
// able to use it if the client is unable to pay for their
|
|
|
|
|
// L402. In this case the subscription will fail on the first
|
|
|
|
|
// read immediately after connecting. We'll therefore only
|
|
|
|
|
// consider the connection successful if we were able to use
|
|
|
|
|
// the stream for at least the minimum alive connection time
|
|
|
|
|
// (which defaults to 1 minute).
|
|
|
|
|
if connectionAliveTime > m.cfg.MinAliveConnTime {
|
|
|
|
|
// Reset the backoff to 10 seconds and the connect
|
|
|
|
|
// attempts to zero if we were really connected for a
|
|
|
|
|
// considerable amount of time (1 minute).
|
|
|
|
|
waitTime = time.Second * 10
|
2025-01-29 20:31:58 +01:00
|
|
|
attempts = 0
|
2025-01-29 20:21:52 +01:00
|
|
|
} else {
|
|
|
|
|
// We either failed to connect or the stream
|
|
|
|
|
// disconnected immediately, so we just increase the
|
|
|
|
|
// backoff.
|
2025-01-29 20:31:58 +01:00
|
|
|
attempts++
|
2024-09-10 18:18:46 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// subscribeNotifications subscribes to the notifications from the server.
|
2025-01-29 20:21:52 +01:00
|
|
|
func (m *Manager) subscribeNotifications(ctx context.Context) error {
|
2024-09-10 18:18:46 +02:00
|
|
|
callCtx, cancel := context.WithCancel(ctx)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
notifStream, err := m.cfg.Client.SubscribeNotifications(
|
2025-10-30 17:56:14 +01:00
|
|
|
callCtx, &swapserverrpc.SubscribeNotificationsRequest{
|
|
|
|
|
Version: current_version,
|
|
|
|
|
},
|
2024-09-10 18:18:46 +02:00
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log.Debugf("Successfully subscribed to server notifications")
|
|
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
notification, err := notifStream.Recv()
|
|
|
|
|
if err == nil && notification != nil {
|
2024-11-14 15:03:44 +01:00
|
|
|
log.Tracef("Received notification: %v", notification)
|
2026-07-08 13:57:15 +02:00
|
|
|
m.handleNotification(ctx, notification)
|
2024-09-10 18:18:46 +02:00
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log.Errorf("Error receiving notification: %v", err)
|
|
|
|
|
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 13:57:15 +02:00
|
|
|
// staticLoopInRiskDecisionName returns the log label for a risk decision.
|
|
|
|
|
func staticLoopInRiskDecisionName(accepted bool) string {
|
|
|
|
|
if accepted {
|
|
|
|
|
return "accepted"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "rejected"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// handleStaticLoopInRiskDecision persists, caches, and forwards a risk
|
|
|
|
|
// decision notification to the matching subscriber.
|
|
|
|
|
func (m *Manager) handleStaticLoopInRiskDecision(ctx context.Context,
|
|
|
|
|
swapHashBytes []byte, accepted bool, notifType NotificationType,
|
|
|
|
|
cacheDecision func(lntypes.Hash, bool),
|
|
|
|
|
notifySubscriber func(subscriber)) {
|
|
|
|
|
|
|
|
|
|
decision := staticLoopInRiskDecisionName(accepted)
|
|
|
|
|
persisted := m.cfg.PersistStaticLoopInRiskDecision == nil
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
swapHash lntypes.Hash
|
|
|
|
|
hasSwapHash bool
|
|
|
|
|
)
|
|
|
|
|
if swapHashBytes != nil {
|
|
|
|
|
hash, err := lntypes.MakeHash(swapHashBytes)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Warnf("Received invalid static loop in risk "+
|
|
|
|
|
"%s notification: %v", decision, err)
|
|
|
|
|
} else {
|
|
|
|
|
swapHash = hash
|
|
|
|
|
hasSwapHash = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if hasSwapHash && m.cfg.PersistStaticLoopInRiskDecision != nil {
|
|
|
|
|
err := m.cfg.PersistStaticLoopInRiskDecision(
|
|
|
|
|
ctx, swapHash, accepted,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Errorf("Unable to persist static loop in risk "+
|
|
|
|
|
"%s notification: %v", decision, err)
|
|
|
|
|
} else {
|
|
|
|
|
persisted = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m.Lock()
|
|
|
|
|
defer m.Unlock()
|
|
|
|
|
|
|
|
|
|
if hasSwapHash {
|
|
|
|
|
cacheDecision(swapHash, persisted)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, sub := range m.subscribers[notifType] {
|
|
|
|
|
if !hasSwapHash || sub.swapHash == nil ||
|
|
|
|
|
*sub.swapHash != swapHash {
|
|
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
notifySubscriber(sub)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-10 18:18:46 +02:00
|
|
|
// handleNotification handles an incoming notification from the server,
|
|
|
|
|
// forwarding it to the appropriate subscribers.
|
2026-07-08 13:57:15 +02:00
|
|
|
func (m *Manager) handleNotification(ctx context.Context, ntfn *swapserverrpc.
|
2024-09-10 18:18:46 +02:00
|
|
|
SubscribeNotificationsResponse) {
|
|
|
|
|
|
2024-11-14 15:03:44 +01:00
|
|
|
switch ntfn.Notification.(type) {
|
|
|
|
|
case *swapserverrpc.SubscribeNotificationsResponse_ReservationNotification: // nolint: lll
|
2024-09-10 18:18:46 +02:00
|
|
|
// We'll forward the reservation notification to all subscribers.
|
2024-11-14 15:03:44 +01:00
|
|
|
reservationNtfn := ntfn.GetReservationNotification()
|
2024-09-10 18:18:46 +02:00
|
|
|
m.Lock()
|
|
|
|
|
defer m.Unlock()
|
|
|
|
|
|
|
|
|
|
for _, sub := range m.subscribers[NotificationTypeReservation] {
|
|
|
|
|
recvChan := sub.recvChan.(chan *swapserverrpc.
|
|
|
|
|
ServerReservationNotification)
|
|
|
|
|
|
2026-06-19 14:48:02 +02:00
|
|
|
select {
|
|
|
|
|
case recvChan <- reservationNtfn:
|
|
|
|
|
case <-sub.subCtx.Done():
|
|
|
|
|
default:
|
|
|
|
|
log.Debugf("Dropping reservation " +
|
|
|
|
|
"notification for slow subscriber")
|
|
|
|
|
}
|
2024-09-10 18:18:46 +02:00
|
|
|
}
|
2024-11-14 15:03:44 +01:00
|
|
|
case *swapserverrpc.SubscribeNotificationsResponse_StaticLoopInSweep: // nolint: lll
|
|
|
|
|
// We'll forward the static loop in sweep request to all
|
|
|
|
|
// subscribers.
|
|
|
|
|
staticLoopInSweepRequestNtfn := ntfn.GetStaticLoopInSweep()
|
|
|
|
|
m.Lock()
|
|
|
|
|
defer m.Unlock()
|
|
|
|
|
|
|
|
|
|
for _, sub := range m.subscribers[NotificationTypeStaticLoopInSweepRequest] { // nolint: lll
|
|
|
|
|
recvChan := sub.recvChan.(chan *swapserverrpc.
|
|
|
|
|
ServerStaticLoopInSweepNotification)
|
|
|
|
|
|
2026-06-19 14:52:17 +02:00
|
|
|
queueNotification(sub, recvChan, staticLoopInSweepRequestNtfn)
|
2024-11-14 15:03:44 +01:00
|
|
|
}
|
2024-09-10 18:18:46 +02:00
|
|
|
|
2026-07-08 13:56:11 +02:00
|
|
|
case *swapserverrpc.SubscribeNotificationsResponse_StaticLoopInRiskAccepted: // nolint: lll
|
|
|
|
|
// We'll forward the static loop in risk accepted notification to the
|
|
|
|
|
// subscriber for the matching swap.
|
|
|
|
|
riskAcceptedNtfn := ntfn.GetStaticLoopInRiskAccepted()
|
2026-07-08 13:57:15 +02:00
|
|
|
var swapHashBytes []byte
|
2026-07-08 13:56:11 +02:00
|
|
|
if riskAcceptedNtfn != nil {
|
2026-07-08 13:57:15 +02:00
|
|
|
swapHashBytes = riskAcceptedNtfn.SwapHash
|
2026-07-08 13:56:11 +02:00
|
|
|
}
|
|
|
|
|
|
2026-07-08 13:57:15 +02:00
|
|
|
m.handleStaticLoopInRiskDecision(
|
|
|
|
|
ctx, swapHashBytes, true,
|
|
|
|
|
NotificationTypeStaticLoopInRiskAccepted,
|
|
|
|
|
func(swapHash lntypes.Hash, persisted bool) {
|
|
|
|
|
m.staticLoopInRiskAccepted[swapHash] =
|
|
|
|
|
riskAcceptedNtfn
|
|
|
|
|
m.staticLoopInRiskPersisted[swapHash] = persisted
|
|
|
|
|
delete(m.staticLoopInRiskRejected, swapHash)
|
|
|
|
|
},
|
|
|
|
|
func(sub subscriber) {
|
|
|
|
|
recvChan := sub.recvChan.(chan *swapserverrpc.
|
|
|
|
|
ServerStaticLoopInRiskAcceptedNotification)
|
|
|
|
|
dropNotification(
|
|
|
|
|
sub, recvChan, riskAcceptedNtfn,
|
|
|
|
|
"static loop in risk accepted",
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
)
|
2026-07-08 13:56:11 +02:00
|
|
|
|
|
|
|
|
case *swapserverrpc.SubscribeNotificationsResponse_StaticLoopInRiskRejected: // nolint: lll
|
|
|
|
|
// We'll forward the static loop in risk rejected notification to the
|
|
|
|
|
// subscriber for the matching swap.
|
|
|
|
|
riskRejectedNtfn := ntfn.GetStaticLoopInRiskRejected()
|
2026-07-08 13:57:15 +02:00
|
|
|
var swapHashBytes []byte
|
2026-07-08 13:56:11 +02:00
|
|
|
if riskRejectedNtfn != nil {
|
2026-07-08 13:57:15 +02:00
|
|
|
swapHashBytes = riskRejectedNtfn.SwapHash
|
2026-07-08 13:56:11 +02:00
|
|
|
}
|
|
|
|
|
|
2026-07-08 13:57:15 +02:00
|
|
|
m.handleStaticLoopInRiskDecision(
|
|
|
|
|
ctx, swapHashBytes, false,
|
|
|
|
|
NotificationTypeStaticLoopInRiskRejected,
|
|
|
|
|
func(swapHash lntypes.Hash, persisted bool) {
|
|
|
|
|
m.staticLoopInRiskRejected[swapHash] =
|
|
|
|
|
riskRejectedNtfn
|
|
|
|
|
m.staticLoopInRiskPersisted[swapHash] = persisted
|
|
|
|
|
delete(m.staticLoopInRiskAccepted, swapHash)
|
|
|
|
|
},
|
|
|
|
|
func(sub subscriber) {
|
|
|
|
|
recvChan := sub.recvChan.(chan *swapserverrpc.
|
|
|
|
|
ServerStaticLoopInRiskRejectedNotification)
|
|
|
|
|
dropNotification(
|
|
|
|
|
sub, recvChan, riskRejectedNtfn,
|
|
|
|
|
"static loop in risk rejected",
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
)
|
2026-07-08 13:56:11 +02:00
|
|
|
|
2025-10-30 17:55:09 +01:00
|
|
|
case *swapserverrpc.SubscribeNotificationsResponse_UnfinishedSwap: // nolint: lll
|
|
|
|
|
// We'll forward the unfinished swap notification to all
|
|
|
|
|
// subscribers.
|
|
|
|
|
unfinishedSwapNtfn := ntfn.GetUnfinishedSwap()
|
|
|
|
|
m.Lock()
|
|
|
|
|
defer m.Unlock()
|
|
|
|
|
|
|
|
|
|
for _, sub := range m.subscribers[NotificationTypeUnfinishedSwap] {
|
|
|
|
|
recvChan := sub.recvChan.(chan *swapserverrpc.
|
|
|
|
|
ServerUnfinishedSwapNotification)
|
|
|
|
|
|
2026-06-19 14:52:17 +02:00
|
|
|
queueNotification(sub, recvChan, unfinishedSwapNtfn)
|
2025-10-30 17:55:09 +01:00
|
|
|
}
|
|
|
|
|
|
2026-04-03 19:41:39 -05:00
|
|
|
case *swapserverrpc.SubscribeNotificationsResponse_HtlcConfirmed:
|
|
|
|
|
// We'll forward the htlc confirmed notification to all
|
|
|
|
|
// subscribers. We wait briefly for a slow subscriber and
|
|
|
|
|
// then drop to avoid stalling the notification pipeline.
|
|
|
|
|
htlcConfirmedNtfn := ntfn.GetHtlcConfirmed()
|
|
|
|
|
m.Lock()
|
|
|
|
|
defer m.Unlock()
|
|
|
|
|
|
|
|
|
|
subscribers := m.subscribers[NotificationTypeHtlcConfirmed]
|
|
|
|
|
for _, sub := range subscribers {
|
|
|
|
|
recvChan := sub.recvChan.(chan *swapserverrpc.
|
|
|
|
|
ServerHtlcConfirmedNotification)
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
case recvChan <- htlcConfirmedNtfn:
|
|
|
|
|
|
|
|
|
|
case <-time.After(htlcConfirmedSubscriberSendTimeout):
|
|
|
|
|
log.Infof("Dropping htlc confirmed " +
|
|
|
|
|
"notification, subscriber busy")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-10 18:18:46 +02:00
|
|
|
default:
|
2026-04-03 19:41:43 -05:00
|
|
|
log.Debugf("Received unknown notification type: %v", ntfn)
|
2024-09-10 18:18:46 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// addSubscriber adds a subscriber to the manager.
|
|
|
|
|
func (m *Manager) addSubscriber(notifType NotificationType, sub subscriber) {
|
|
|
|
|
m.Lock()
|
|
|
|
|
defer m.Unlock()
|
|
|
|
|
m.subscribers[notifType] = append(m.subscribers[notifType], sub)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// removeSubscriber removes a subscriber from the manager.
|
|
|
|
|
func (m *Manager) removeSubscriber(notifType NotificationType, sub subscriber) {
|
|
|
|
|
m.Lock()
|
|
|
|
|
defer m.Unlock()
|
|
|
|
|
subs := m.subscribers[notifType]
|
|
|
|
|
newSubs := make([]subscriber, 0, len(subs))
|
|
|
|
|
for _, s := range subs {
|
2026-06-19 14:52:17 +02:00
|
|
|
if s.recvChan != sub.recvChan {
|
2024-09-10 18:18:46 +02:00
|
|
|
newSubs = append(newSubs, s)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
m.subscribers[notifType] = newSubs
|
|
|
|
|
}
|