From eb67f73eb76876d1b2073d4e0154db94d8548418 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Tue, 9 Jul 2024 13:30:18 +0700 Subject: [PATCH] feat: update existing transaction from nwc_payment_sent event in transactions service --- alby/alby_oauth_service.go | 29 +++++++++++++++ events/models.go | 5 --- lnclient/ldk/ldk.go | 40 ++++++++++++--------- nip47/notifications/nip47_notifier.go | 2 +- transactions/transactions_service.go | 51 ++++++++++++++++++++++++--- 5 files changed, 99 insertions(+), 28 deletions(-) diff --git a/alby/alby_oauth_service.go b/alby/alby_oauth_service.go index 407c139b..18efccd8 100644 --- a/alby/alby_oauth_service.go +++ b/alby/alby_oauth_service.go @@ -429,6 +429,35 @@ func (svc *albyOAuthService) ConsumeEvent(ctx context.Context, event *events.Eve return nil } + if event.Event == "nwc_payment_received" { + type paymentReceivedEventProperties struct { + PaymentHash string `json:"payment_hash"` + } + // pass a new custom event with less detail + event = &events.Event{ + Event: event.Event, + Properties: &paymentReceivedEventProperties{ + PaymentHash: event.Properties.(*lnclient.Transaction).PaymentHash, + }, + } + } + + if event.Event == "nwc_payment_sent" { + type paymentSentEventProperties struct { + PaymentHash string `json:"payment_hash"` + Duration uint64 `json:"duration"` + } + + // pass a new custom event with less detail + event = &events.Event{ + Event: event.Event, + Properties: &paymentSentEventProperties{ + PaymentHash: event.Properties.(*lnclient.Transaction).PaymentHash, + Duration: uint64(*event.Properties.(*lnclient.Transaction).SettledAt - event.Properties.(*lnclient.Transaction).CreatedAt), + }, + } + } + token, err := svc.fetchUserToken(ctx) if err != nil { logger.Logger.WithError(err).Error("Failed to fetch user token") diff --git a/events/models.go b/events/models.go index 62b0a582..4a427b55 100644 --- a/events/models.go +++ b/events/models.go @@ -18,11 +18,6 @@ type Event struct { Properties interface{} `json:"properties,omitempty"` } -type PaymentSentEventProperties struct { - PaymentHash string `json:"payment_hash"` - Duration uint64 `json:"duration"` -} - type ChannelBackupEvent struct { Channels []ChannelBackupInfo `json:"channels"` } diff --git a/lnclient/ldk/ldk.go b/lnclient/ldk/ldk.go index 2fb663e3..0ff26100 100644 --- a/lnclient/ldk/ldk.go +++ b/lnclient/ldk/ldk.go @@ -478,7 +478,9 @@ func (ls *LDKService) SendPaymentSync(ctx context.Context, invoice string) (*lnc } } if preimage == "" { - // TODO: this doesn't necessarily mean it will fail - we should return a different response + logger.Logger.WithFields(logrus.Fields{ + "paymentHash": paymentHash, + }).Warn("Timed out waiting for payment to be sent") return nil, lnclient.NewTimeoutError() } @@ -583,6 +585,9 @@ func (ls *LDKService) SendKeysend(ctx context.Context, amount uint64, destinatio } } if preimage == "" { + logger.Logger.WithFields(logrus.Fields{ + "paymentHash": paymentHash, + }).Warn("Timed out waiting for keysend to be sent") return paymentHash, "", 0, lnclient.NewTimeoutError() } @@ -1276,28 +1281,29 @@ func (ls *LDKService) handleLdkEvent(event *ldk_node.Event) { Event: "nwc_payment_received", Properties: transaction, }) - case ldk_node.EventPaymentSuccessful: - // TODO: trigger transaction update - // TODO: trigger transaction update for payment failed - var duration uint64 = 0 - if eventType.PaymentId != nil { - payment := ls.node.Payment(*eventType.PaymentId) - if payment == nil { - logger.Logger.WithField("payment_id", *eventType.PaymentId).Error("could not find LDK payment") - return - } - duration = payment.LastUpdate - payment.CreatedAt + if eventType.PaymentId == nil { + logger.Logger.WithField("payment_hash", eventType.PaymentHash).Error("payment received event has no payment ID") + return + } + payment := ls.node.Payment(*eventType.PaymentId) + if payment == nil { + logger.Logger.WithField("payment_id", *eventType.PaymentId).Error("could not find LDK payment") + return + } + + transaction, err := ls.ldkPaymentToTransaction(payment) + if err != nil { + logger.Logger.WithField("payment_id", *eventType.PaymentId).Error("failed to convert LDK payment to transaction") + return } ls.eventPublisher.Publish(&events.Event{ - Event: "nwc_payment_sent", - Properties: &events.PaymentSentEventProperties{ - PaymentHash: eventType.PaymentHash, - Duration: duration, - }, + Event: "nwc_payment_sent", + Properties: transaction, }) } + // TODO: trigger transaction update for payment failed } func (ls *LDKService) publishChannelsBackupEvent() { diff --git a/nip47/notifications/nip47_notifier.go b/nip47/notifications/nip47_notifier.go index 24935295..a18b18f1 100644 --- a/nip47/notifications/nip47_notifier.go +++ b/nip47/notifications/nip47_notifier.go @@ -74,7 +74,7 @@ func (notifier *Nip47Notifier) ConsumeEvent(ctx context.Context, event *events.E }, nostr.Tags{}) case "nwc_payment_sent": - paymentSentEventProperties, ok := event.Properties.(*events.PaymentSentEventProperties) + paymentSentEventProperties, ok := event.Properties.(*lnclient.Transaction) if !ok { logger.Logger.WithField("event", event).Error("Failed to cast event") return errors.New("failed to cast event") diff --git a/transactions/transactions_service.go b/transactions/transactions_service.go index a1f1ccaa..bae1ddce 100644 --- a/transactions/transactions_service.go +++ b/transactions/transactions_service.go @@ -142,8 +142,10 @@ func (svc *transactionsService) SendPaymentSync(ctx context.Context, payReq stri "bolt11": payReq, }).WithError(err).Error("Failed to send payment") - // TODO: this is untested if errors.Is(err, lnclient.NewTimeoutError()) { + logger.Logger.WithFields(logrus.Fields{ + "bolt11": payReq, + }).WithError(err).Error("Timed out waiting for payment to be sent. It may still succeed. Skipping update of transaction status") // we cannot update the payment to failed as it still might succeed. // we'll need to check the status of it later return nil, err @@ -215,10 +217,16 @@ func (svc *transactionsService) SendKeysend(ctx context.Context, amount uint64, "amount": amount, }).WithError(err).Error("Failed to send payment") - // TODO: this is untested if errors.Is(err, lnclient.NewTimeoutError()) { + + logger.Logger.WithFields(logrus.Fields{ + "destination": destination, + "amount": amount, + }).WithError(err).Error("Timed out waiting for payment to be sent. It may still succeed. Skipping update of transaction status") + // we cannot update the payment to failed as it still might succeed. // we'll need to check the status of it later + // but we have the payment hash now, so save it on the transaction dbErr := svc.db.Model(dbTransaction).Updates(&db.Transaction{ PaymentHash: paymentHash, }).Error @@ -373,11 +381,10 @@ func (svc *transactionsService) ConsumeEvent(ctx context.Context, event *events. return errors.New("failed to cast event") } - settledAt := time.Now() err := svc.db.Transaction(func(tx *gorm.DB) error { var dbTransaction *db.Transaction - result := tx.Find(&dbTransaction, &db.Transaction{ + result := tx.Find(dbTransaction, &db.Transaction{ Type: TRANSACTION_TYPE_INCOMING, PaymentHash: lnClientTransaction.PaymentHash, }) @@ -417,6 +424,7 @@ func (svc *transactionsService) ConsumeEvent(ctx context.Context, event *events. } } + settledAt := time.Now() fee := uint64(lnClientTransaction.FeesPaid) err := tx.Model(dbTransaction).Updates(&db.Transaction{ @@ -432,6 +440,7 @@ func (svc *transactionsService) ConsumeEvent(ctx context.Context, event *events. return err } + logger.Logger.WithField("id", dbTransaction.ID).Info("Marked incoming transaction as settled") return nil }) @@ -441,8 +450,40 @@ func (svc *transactionsService) ConsumeEvent(ctx context.Context, event *events. }).WithError(err).Error("Failed to execute DB transaction") return err } + case "nwc_payment_sent": + lnClientTransaction, ok := event.Properties.(*lnclient.Transaction) + if !ok { + logger.Logger.WithField("event", event).Error("Failed to cast event") + return errors.New("failed to cast event") + } + + var dbTransaction *db.Transaction + result := svc.db.Find(&dbTransaction, &db.Transaction{ + Type: TRANSACTION_TYPE_OUTGOING, + PaymentHash: lnClientTransaction.PaymentHash, + }) + + if result.RowsAffected == 0 { + logger.Logger.WithField("event", event).Error("Failed to find outgoing transaction by payment hash") + return errors.New("could not find outgoing transaction by payment hash") + } - // TODO: support nwc_payment_sent and nwc_payment_failed () + settledAt := time.Now() + fee := uint64(lnClientTransaction.FeesPaid) + err := svc.db.Model(dbTransaction).Updates(&db.Transaction{ + Fee: &fee, + Preimage: &lnClientTransaction.Preimage, + State: TRANSACTION_STATE_SETTLED, + SettledAt: &settledAt, + }).Error + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "payment_hash": lnClientTransaction.PaymentHash, + }).WithError(err).Error("Failed to update transaction") + return err + } + logger.Logger.WithField("id", dbTransaction.ID).Info("Marked outgoing transaction as settled") + // TODO: support nwc_payment_failed } return nil