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
|
|
|
|
|
|
|
|
// NotificationTypeUnfinishedSwap is the notification type for unfinished
|
|
|
|
|
// swap notifications.
|
|
|
|
|
NotificationTypeUnfinishedSwap
|
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
|
|
|
|
|
|
|
|
// 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
|
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
|
|
|
|
|
|
|
|
|
|
subscribers map[NotificationType][]subscriber
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-10 18:18:46 +02:00
|
|
|
return &Manager{
|
|
|
|
|
cfg: cfg,
|
|
|
|
|
subscribers: make(map[NotificationType][]subscriber),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type subscriber struct {
|
|
|
|
|
subCtx context.Context
|
2026-03-05 12:18:05 +01:00
|
|
|
recvChan any
|
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,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
)
|
|
|
|
|
close(notifChan)
|
2025-10-30 17:55:09 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return notifChan
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m.addSubscriber(NotificationTypeUnfinishedSwap, sub)
|
|
|
|
|
context.AfterFunc(ctx, func() {
|
|
|
|
|
m.removeSubscriber(NotificationTypeUnfinishedSwap, sub)
|
|
|
|
|
close(notifChan)
|
|
|
|
|
})
|
2024-11-14 15:03:44 +01:00
|
|
|
|
|
|
|
|
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)
|
2024-09-10 18:18:46 +02:00
|
|
|
m.handleNotification(notification)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log.Errorf("Error receiving notification: %v", err)
|
|
|
|
|
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// handleNotification handles an incoming notification from the server,
|
|
|
|
|
// forwarding it to the appropriate subscribers.
|
2024-11-14 15:03:44 +01:00
|
|
|
func (m *Manager) handleNotification(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)
|
|
|
|
|
|
|
|
|
|
recvChan <- reservationNtfn
|
|
|
|
|
}
|
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)
|
|
|
|
|
|
|
|
|
|
recvChan <- staticLoopInSweepRequestNtfn
|
|
|
|
|
}
|
2024-09-10 18:18:46 +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)
|
|
|
|
|
|
|
|
|
|
recvChan <- unfinishedSwapNtfn
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-10 18:18:46 +02:00
|
|
|
default:
|
|
|
|
|
log.Warnf("Received unknown notification type: %v",
|
2024-11-14 15:03:44 +01:00
|
|
|
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 {
|
|
|
|
|
if s != sub {
|
|
|
|
|
newSubs = append(newSubs, s)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
m.subscribers[notifType] = newSubs
|
|
|
|
|
}
|