From 22fe5aaed315ca39dd99b4f67328d55f9895e06f Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Thu, 18 Jul 2024 22:17:19 +0700 Subject: [PATCH 1/5] chore: add transactions service payment tests --- tests/test_service.go | 4 +- transactions/app_payments_test.go | 219 ++++++++++++ transactions/isolated_app_payments_test.go | 312 ++++++++++++++++++ ...s_service_test.go => make_invoice_test.go} | 28 +- transactions/payments_test.go | 26 ++ transactions/todo_test.go | 9 + 6 files changed, 584 insertions(+), 14 deletions(-) create mode 100644 transactions/app_payments_test.go create mode 100644 transactions/isolated_app_payments_test.go rename transactions/{transactions_service_test.go => make_invoice_test.go} (57%) create mode 100644 transactions/payments_test.go create mode 100644 transactions/todo_test.go diff --git a/tests/test_service.go b/tests/test_service.go index ec3a5b6b..f58cfbef 100644 --- a/tests/test_service.go +++ b/tests/test_service.go @@ -2,6 +2,7 @@ package tests import ( "os" + "strconv" "github.com/getAlby/hub/config" "github.com/getAlby/hub/db" @@ -9,6 +10,7 @@ import ( "github.com/getAlby/hub/lnclient" "github.com/getAlby/hub/logger" "github.com/getAlby/hub/service/keys" + "github.com/sirupsen/logrus" "gorm.io/gorm" ) @@ -25,7 +27,7 @@ func CreateTestService() (svc *TestService, err error) { return nil, err } - logger.Init("") + logger.Init(strconv.Itoa(int(logrus.DebugLevel))) appConfig := &config.AppConfig{ Workdir: ".test", diff --git a/transactions/app_payments_test.go b/transactions/app_payments_test.go new file mode 100644 index 00000000..023c7d07 --- /dev/null +++ b/transactions/app_payments_test.go @@ -0,0 +1,219 @@ +package transactions + +import ( + "context" + "testing" + "time" + + "github.com/getAlby/hub/constants" + "github.com/getAlby/hub/db" + "github.com/getAlby/hub/tests" + "github.com/stretchr/testify/assert" +) + +func TestSendPaymentSync_App_NoPermission(t *testing.T) { + ctx := context.TODO() + + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + transactionsService := NewTransactionsService(svc.DB) + transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, &app.ID, &dbRequestEvent.ID) + + assert.Error(t, err) + assert.Equal(t, "app does not have pay_invoice scope", err.Error()) + assert.Nil(t, transaction) +} +func TestSendPaymentSync_App_WithPermission(t *testing.T) { + ctx := context.TODO() + + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + + appPermission := &db.AppPermission{ + AppId: app.ID, + App: *app, + Scope: constants.PAY_INVOICE_SCOPE, + } + err = svc.DB.Create(appPermission).Error + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + transactionsService := NewTransactionsService(svc.DB) + transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, &app.ID, &dbRequestEvent.ID) + + assert.NoError(t, err) + assert.Equal(t, uint64(123000), transaction.AmountMsat) + assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State) + assert.Equal(t, "123preimage", *transaction.Preimage) + assert.Equal(t, app.ID, *transaction.AppId) + assert.Equal(t, dbRequestEvent.ID, *transaction.RequestEventId) +} + +func TestSendPaymentSync_App_BudgetExceeded(t *testing.T) { + ctx := context.TODO() + + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + + appPermission := &db.AppPermission{ + AppId: app.ID, + App: *app, + Scope: constants.PAY_INVOICE_SCOPE, + MaxAmountSat: 1, + } + err = svc.DB.Create(appPermission).Error + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + transactionsService := NewTransactionsService(svc.DB) + transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, &app.ID, &dbRequestEvent.ID) + + assert.Error(t, err) + assert.ErrorIs(t, err, NewQuotaExceededError()) + assert.Nil(t, transaction) +} + +func TestSendPaymentSync_App_BudgetExceeded_SettledPayment(t *testing.T) { + ctx := context.TODO() + + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + + appPermission := &db.AppPermission{ + AppId: app.ID, + App: *app, + Scope: constants.PAY_INVOICE_SCOPE, + MaxAmountSat: 133, // invoice is 123 sats, but we also calculate fee reserves max of(10 sats or 1%) + } + err = svc.DB.Create(appPermission).Error + assert.NoError(t, err) + + // 1 sat payment pushes app over the limit + svc.DB.Create(&db.Transaction{ + AppId: &app.ID, + State: constants.TRANSACTION_STATE_SETTLED, + Type: constants.TRANSACTION_TYPE_OUTGOING, + AmountMsat: 1000, + CreatedAt: time.Now(), + }) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + transactionsService := NewTransactionsService(svc.DB) + transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, &app.ID, &dbRequestEvent.ID) + + assert.Error(t, err) + assert.ErrorIs(t, err, NewQuotaExceededError()) + assert.Nil(t, transaction) +} +func TestSendPaymentSync_App_BudgetExceeded_UnsettledPayment(t *testing.T) { + ctx := context.TODO() + + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + + appPermission := &db.AppPermission{ + AppId: app.ID, + App: *app, + Scope: constants.PAY_INVOICE_SCOPE, + MaxAmountSat: 133, // invoice is 123 sats, but we also calculate fee reserves max of(10 sats or 1%) + } + err = svc.DB.Create(appPermission).Error + assert.NoError(t, err) + + // 1 sat payment pushes app over the limit + svc.DB.Create(&db.Transaction{ + AppId: &app.ID, + State: constants.TRANSACTION_STATE_PENDING, + Type: constants.TRANSACTION_TYPE_OUTGOING, + AmountMsat: 1000, + CreatedAt: time.Now(), + }) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + transactionsService := NewTransactionsService(svc.DB) + transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, &app.ID, &dbRequestEvent.ID) + + assert.Error(t, err) + assert.ErrorIs(t, err, NewQuotaExceededError()) + assert.Nil(t, transaction) +} + +func TestSendPaymentSync_App_BudgetNotExceeded_FailedPayment(t *testing.T) { + ctx := context.TODO() + + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + + appPermission := &db.AppPermission{ + AppId: app.ID, + App: *app, + Scope: constants.PAY_INVOICE_SCOPE, + MaxAmountSat: 133, // invoice is 123 sats, but we also calculate fee reserves max of(10 sats or 1%) + } + err = svc.DB.Create(appPermission).Error + assert.NoError(t, err) + + // 1 sat payment would push app over the limit, but it failed so its not counted + svc.DB.Create(&db.Transaction{ + AppId: &app.ID, + State: constants.TRANSACTION_STATE_FAILED, + Type: constants.TRANSACTION_TYPE_OUTGOING, + AmountMsat: 1000, + CreatedAt: time.Now(), + }) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + transactionsService := NewTransactionsService(svc.DB) + transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, &app.ID, &dbRequestEvent.ID) + + assert.NoError(t, err) + assert.Equal(t, uint64(123000), transaction.AmountMsat) + assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State) + assert.Equal(t, "123preimage", *transaction.Preimage) + assert.Equal(t, app.ID, *transaction.AppId) + assert.Equal(t, dbRequestEvent.ID, *transaction.RequestEventId) +} diff --git a/transactions/isolated_app_payments_test.go b/transactions/isolated_app_payments_test.go new file mode 100644 index 00000000..bb1ad6d4 --- /dev/null +++ b/transactions/isolated_app_payments_test.go @@ -0,0 +1,312 @@ +package transactions + +import ( + "context" + "testing" + + "github.com/getAlby/hub/constants" + "github.com/getAlby/hub/db" + "github.com/getAlby/hub/tests" + "github.com/stretchr/testify/assert" +) + +func TestSendPaymentSync_IsolatedApp_NoBalance(t *testing.T) { + ctx := context.TODO() + + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + app.Isolated = true + svc.DB.Save(&app) + + appPermission := &db.AppPermission{ + AppId: app.ID, + App: *app, + Scope: constants.PAY_INVOICE_SCOPE, + } + err = svc.DB.Create(appPermission).Error + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + transactionsService := NewTransactionsService(svc.DB) + transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, &app.ID, &dbRequestEvent.ID) + + assert.Error(t, err) + assert.ErrorIs(t, err, NewInsufficientBalanceError()) + assert.Nil(t, transaction) +} + +func TestSendPaymentSync_IsolatedApp_BalanceInsufficient(t *testing.T) { + ctx := context.TODO() + + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + app.Isolated = true + svc.DB.Save(&app) + + appPermission := &db.AppPermission{ + AppId: app.ID, + App: *app, + Scope: constants.PAY_INVOICE_SCOPE, + } + err = svc.DB.Create(appPermission).Error + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + svc.DB.Create(&db.Transaction{ + AppId: &app.ID, + State: constants.TRANSACTION_STATE_SETTLED, + Type: constants.TRANSACTION_TYPE_INCOMING, + AmountMsat: 132000, // invoice is 123000 msat, but we also calculate fee reserves max of(10 sats or 1%) + }) + + transactionsService := NewTransactionsService(svc.DB) + transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, &app.ID, &dbRequestEvent.ID) + + assert.Error(t, err) + assert.ErrorIs(t, err, NewInsufficientBalanceError()) + assert.Nil(t, transaction) +} + +func TestSendPaymentSync_IsolatedApp_BalanceSufficient(t *testing.T) { + ctx := context.TODO() + + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + app.Isolated = true + svc.DB.Save(&app) + + appPermission := &db.AppPermission{ + AppId: app.ID, + App: *app, + Scope: constants.PAY_INVOICE_SCOPE, + } + err = svc.DB.Create(appPermission).Error + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + svc.DB.Create(&db.Transaction{ + AppId: &app.ID, + State: constants.TRANSACTION_STATE_SETTLED, + Type: constants.TRANSACTION_TYPE_INCOMING, + AmountMsat: 133000, // invoice is 123000 msat, but we also calculate fee reserves max of(10 sats or 1%) + }) + + transactionsService := NewTransactionsService(svc.DB) + transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, &app.ID, &dbRequestEvent.ID) + + assert.NoError(t, err) + assert.Equal(t, uint64(123000), transaction.AmountMsat) + assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State) + assert.Equal(t, "123preimage", *transaction.Preimage) + assert.Equal(t, app.ID, *transaction.AppId) + assert.Equal(t, dbRequestEvent.ID, *transaction.RequestEventId) +} + +func TestSendPaymentSync_IsolatedApp_BalanceInsufficient_OutstandingPayment(t *testing.T) { + ctx := context.TODO() + + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + app.Isolated = true + svc.DB.Save(&app) + + appPermission := &db.AppPermission{ + AppId: app.ID, + App: *app, + Scope: constants.PAY_INVOICE_SCOPE, + } + err = svc.DB.Create(appPermission).Error + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + svc.DB.Create(&db.Transaction{ + AppId: &app.ID, + State: constants.TRANSACTION_STATE_SETTLED, + Type: constants.TRANSACTION_TYPE_INCOMING, + AmountMsat: 133000, // invoice is 123000 msat, but we also calculate fee reserves max of(10 sats or 1%) + }) + + svc.DB.Create(&db.Transaction{ + AppId: &app.ID, + State: constants.TRANSACTION_STATE_PENDING, + Type: constants.TRANSACTION_TYPE_OUTGOING, + AmountMsat: 1000, + }) + + transactionsService := NewTransactionsService(svc.DB) + transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, &app.ID, &dbRequestEvent.ID) + + assert.Error(t, err) + assert.ErrorIs(t, err, NewInsufficientBalanceError()) + assert.Nil(t, transaction) +} + +func TestSendPaymentSync_IsolatedApp_BalanceInsufficient_SettledPayment(t *testing.T) { + ctx := context.TODO() + + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + app.Isolated = true + svc.DB.Save(&app) + + appPermission := &db.AppPermission{ + AppId: app.ID, + App: *app, + Scope: constants.PAY_INVOICE_SCOPE, + } + err = svc.DB.Create(appPermission).Error + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + svc.DB.Create(&db.Transaction{ + AppId: &app.ID, + State: constants.TRANSACTION_STATE_SETTLED, + Type: constants.TRANSACTION_TYPE_INCOMING, + AmountMsat: 133000, // invoice is 123000 msat, but we also calculate fee reserves max of(10 sats or 1%) + }) + + svc.DB.Create(&db.Transaction{ + AppId: &app.ID, + State: constants.TRANSACTION_STATE_SETTLED, + Type: constants.TRANSACTION_TYPE_OUTGOING, + AmountMsat: 1000, + }) + + transactionsService := NewTransactionsService(svc.DB) + transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, &app.ID, &dbRequestEvent.ID) + + assert.Error(t, err) + assert.ErrorIs(t, err, NewInsufficientBalanceError()) + assert.Nil(t, transaction) +} + +func TestSendPaymentSync_IsolatedApp_BalanceSufficient_UnrelatedPayment(t *testing.T) { + ctx := context.TODO() + + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + app.Isolated = true + svc.DB.Save(&app) + + appPermission := &db.AppPermission{ + AppId: app.ID, + App: *app, + Scope: constants.PAY_INVOICE_SCOPE, + } + err = svc.DB.Create(appPermission).Error + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + svc.DB.Create(&db.Transaction{ + AppId: &app.ID, + State: constants.TRANSACTION_STATE_SETTLED, + Type: constants.TRANSACTION_TYPE_INCOMING, + AmountMsat: 133000, // invoice is 123000 msat, but we also calculate fee reserves max of(10 sats or 1%) + }) + svc.DB.Create(&db.Transaction{ + AppId: nil, // unrelated to this app + State: constants.TRANSACTION_STATE_SETTLED, + Type: constants.TRANSACTION_TYPE_OUTGOING, + AmountMsat: 1000, + }) + + transactionsService := NewTransactionsService(svc.DB) + transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, &app.ID, &dbRequestEvent.ID) + + assert.NoError(t, err) + assert.Equal(t, uint64(123000), transaction.AmountMsat) + assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State) + assert.Equal(t, "123preimage", *transaction.Preimage) + assert.Equal(t, app.ID, *transaction.AppId) + assert.Equal(t, dbRequestEvent.ID, *transaction.RequestEventId) +} + +func TestSendPaymentSync_IsolatedApp_BalanceSufficient_FailedPayment(t *testing.T) { + ctx := context.TODO() + + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + app.Isolated = true + svc.DB.Save(&app) + + appPermission := &db.AppPermission{ + AppId: app.ID, + App: *app, + Scope: constants.PAY_INVOICE_SCOPE, + } + err = svc.DB.Create(appPermission).Error + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + svc.DB.Create(&db.Transaction{ + AppId: &app.ID, + State: constants.TRANSACTION_STATE_SETTLED, + Type: constants.TRANSACTION_TYPE_INCOMING, + AmountMsat: 133000, // invoice is 123000 msat, but we also calculate fee reserves max of(10 sats or 1%) + }) + svc.DB.Create(&db.Transaction{ + AppId: &app.ID, + State: constants.TRANSACTION_STATE_FAILED, + Type: constants.TRANSACTION_TYPE_OUTGOING, + AmountMsat: 1000, + }) + + transactionsService := NewTransactionsService(svc.DB) + transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, &app.ID, &dbRequestEvent.ID) + + assert.NoError(t, err) + assert.Equal(t, uint64(123000), transaction.AmountMsat) + assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State) + assert.Equal(t, "123preimage", *transaction.Preimage) + assert.Equal(t, app.ID, *transaction.AppId) + assert.Equal(t, dbRequestEvent.ID, *transaction.RequestEventId) +} diff --git a/transactions/transactions_service_test.go b/transactions/make_invoice_test.go similarity index 57% rename from transactions/transactions_service_test.go rename to transactions/make_invoice_test.go index c7990775..15b6ab2f 100644 --- a/transactions/transactions_service_test.go +++ b/transactions/make_invoice_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/getAlby/hub/constants" + "github.com/getAlby/hub/db" "github.com/getAlby/hub/tests" "github.com/stretchr/testify/assert" ) @@ -25,26 +26,27 @@ func TestMakeInvoice_NoApp(t *testing.T) { assert.Equal(t, tests.MockLNClientTransaction.Preimage, *transaction.Preimage) } -func TestSendPaymentSync_NoApp(t *testing.T) { +func TestMakeInvoice_App(t *testing.T) { ctx := context.TODO() defer tests.RemoveTestService() svc, err := tests.CreateTestService() assert.NoError(t, err) - transactionsService := NewTransactionsService(svc.DB) - transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, nil, nil) + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error assert.NoError(t, err) - assert.Equal(t, uint64(123000), transaction.AmountMsat) - assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State) - assert.Equal(t, "123preimage", *transaction.Preimage) -} -// TODO: apps & events -// TODO: isolated apps & events -// TODO: self payments + transactionsService := NewTransactionsService(svc.DB) + transaction, err := transactionsService.MakeInvoice(ctx, 1234, "Hello world", "", 0, svc.LNClient, &app.ID, &dbRequestEvent.ID) -// TODO: keysend -// TODO: lookup invoice -// TODO: list transactions + assert.NoError(t, err) + assert.Equal(t, uint64(tests.MockLNClientTransaction.Amount), transaction.AmountMsat) + assert.Equal(t, constants.TRANSACTION_STATE_PENDING, transaction.State) + assert.Equal(t, tests.MockLNClientTransaction.Preimage, *transaction.Preimage) + assert.Equal(t, app.ID, *transaction.AppId) + assert.Equal(t, dbRequestEvent.ID, *transaction.RequestEventId) +} diff --git a/transactions/payments_test.go b/transactions/payments_test.go new file mode 100644 index 00000000..300b52fb --- /dev/null +++ b/transactions/payments_test.go @@ -0,0 +1,26 @@ +package transactions + +import ( + "context" + "testing" + + "github.com/getAlby/hub/constants" + "github.com/getAlby/hub/tests" + "github.com/stretchr/testify/assert" +) + +func TestSendPaymentSync_NoApp(t *testing.T) { + ctx := context.TODO() + + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + transactionsService := NewTransactionsService(svc.DB) + transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, nil, nil) + + assert.NoError(t, err) + assert.Equal(t, uint64(123000), transaction.AmountMsat) + assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State) + assert.Equal(t, "123preimage", *transaction.Preimage) +} diff --git a/transactions/todo_test.go b/transactions/todo_test.go new file mode 100644 index 00000000..b795cde9 --- /dev/null +++ b/transactions/todo_test.go @@ -0,0 +1,9 @@ +package transactions + +// TODO: self payments + +// TODO: keysend +// TODO: lookup invoice +// TODO: list transactions + +// TODO: notifications From 3f749bf36c6cfc361de53ff4be1991520d8d7554 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Fri, 19 Jul 2024 12:27:10 +0700 Subject: [PATCH 2/5] chore: add tests for self payments --- api/transactions.go | 2 +- db/migrations/202407012100_transactions.go | 3 +- db/models.go | 1 + .../controllers/lookup_invoice_controller.go | 2 +- nip47/notifications/nip47_notifier.go | 6 +- tests/mock_ln_client.go | 5 + transactions/self_payments_test.go | 462 ++++++++++++++++++ transactions/todo_test.go | 3 +- transactions/transactions_service.go | 22 +- 9 files changed, 492 insertions(+), 14 deletions(-) create mode 100644 transactions/self_payments_test.go diff --git a/api/transactions.go b/api/transactions.go index 3a4869c7..327f58cd 100644 --- a/api/transactions.go +++ b/api/transactions.go @@ -26,7 +26,7 @@ func (api *api) LookupInvoice(ctx context.Context, paymentHash string) (*LookupI if api.svc.GetLNClient() == nil { return nil, errors.New("LNClient not started") } - transaction, err := api.svc.GetTransactionsService().LookupTransaction(ctx, paymentHash, api.svc.GetLNClient(), nil) + transaction, err := api.svc.GetTransactionsService().LookupTransaction(ctx, paymentHash, nil, api.svc.GetLNClient(), nil) if err != nil { return nil, err } diff --git a/db/migrations/202407012100_transactions.go b/db/migrations/202407012100_transactions.go index c3354b53..7c978202 100644 --- a/db/migrations/202407012100_transactions.go +++ b/db/migrations/202407012100_transactions.go @@ -36,7 +36,8 @@ CREATE TABLE transactions( updated_at datetime, expires_at datetime, settled_at datetime, - metadata text + metadata text, + self_payment boolean ); DROP TABLE payments; diff --git a/db/models.go b/db/models.go index 727cd23f..28faa190 100644 --- a/db/models.go +++ b/db/models.go @@ -76,6 +76,7 @@ type Transaction struct { UpdatedAt time.Time SettledAt *time.Time Metadata string + SelfPayment bool } type DBService interface { diff --git a/nip47/controllers/lookup_invoice_controller.go b/nip47/controllers/lookup_invoice_controller.go index 353162c1..44afb7b5 100644 --- a/nip47/controllers/lookup_invoice_controller.go +++ b/nip47/controllers/lookup_invoice_controller.go @@ -58,7 +58,7 @@ func (controller *nip47Controller) HandleLookupInvoiceEvent(ctx context.Context, paymentHash = paymentRequest.PaymentHash } - dbTransaction, err := controller.transactionsService.LookupTransaction(ctx, paymentHash, controller.lnClient, &appId) + dbTransaction, err := controller.transactionsService.LookupTransaction(ctx, paymentHash, nil, controller.lnClient, &appId) if err != nil { logger.Logger.WithFields(logrus.Fields{ "request_event_id": requestEventId, diff --git a/nip47/notifications/nip47_notifier.go b/nip47/notifications/nip47_notifier.go index 4382f405..080da16b 100644 --- a/nip47/notifications/nip47_notifier.go +++ b/nip47/notifications/nip47_notifier.go @@ -55,7 +55,8 @@ func (notifier *Nip47Notifier) ConsumeEvent(ctx context.Context, event *events.E return } - transaction, err := notifier.transactionsService.LookupTransaction(ctx, lnClientTransaction.PaymentHash, notifier.lnClient, nil) + transactionType := constants.TRANSACTION_TYPE_INCOMING + transaction, err := notifier.transactionsService.LookupTransaction(ctx, lnClientTransaction.PaymentHash, &transactionType, notifier.lnClient, nil) if err != nil { logger.Logger. WithField("paymentHash", lnClientTransaction.PaymentHash). @@ -79,7 +80,8 @@ func (notifier *Nip47Notifier) ConsumeEvent(ctx context.Context, event *events.E return } - transaction, err := notifier.transactionsService.LookupTransaction(ctx, paymentSentEventProperties.PaymentHash, notifier.lnClient, nil) + transactionType := constants.TRANSACTION_TYPE_OUTGOING + transaction, err := notifier.transactionsService.LookupTransaction(ctx, paymentSentEventProperties.PaymentHash, &transactionType, notifier.lnClient, nil) if err != nil { logger.Logger. WithField("paymentHash", paymentSentEventProperties.PaymentHash). diff --git a/tests/mock_ln_client.go b/tests/mock_ln_client.go index a7b62139..522b17b7 100644 --- a/tests/mock_ln_client.go +++ b/tests/mock_ln_client.go @@ -59,6 +59,7 @@ var MockLNClientTransaction = &MockLNClientTransactions[0] type MockLn struct { PayInvoiceResponses []*lnclient.PayInvoiceResponse PayInvoiceErrors []error + Pubkey string } func NewMockLn() (*MockLn, error) { @@ -178,5 +179,9 @@ func (mln *MockLn) GetSupportedNIP47NotificationTypes() []string { return []string{"payment_received", "payment_sent"} } func (mln *MockLn) GetPubkey() string { + if mln.Pubkey != "" { + return mln.Pubkey + } + return "123pubkey" } diff --git a/transactions/self_payments_test.go b/transactions/self_payments_test.go new file mode 100644 index 00000000..9c573d0d --- /dev/null +++ b/transactions/self_payments_test.go @@ -0,0 +1,462 @@ +package transactions + +import ( + "context" + "testing" + + "github.com/getAlby/hub/constants" + "github.com/getAlby/hub/db" + "github.com/getAlby/hub/db/queries" + "github.com/getAlby/hub/tests" + "github.com/stretchr/testify/assert" +) + +func TestSendPaymentSync_SelfPayment_NoAppToNoApp(t *testing.T) { + ctx := context.TODO() + + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + // pubkey matches mock invoice = self payment + svc.LNClient.(*tests.MockLn).Pubkey = "02a5056398235568fc049a5d563f1adf666041d590b268167e4fa145fbf71aa578" + + mockPreimage := "123preimage" + svc.DB.Create(&db.Transaction{ + State: constants.TRANSACTION_STATE_PENDING, + Type: constants.TRANSACTION_TYPE_INCOMING, + PaymentRequest: tests.MockInvoice, + PaymentHash: tests.MockPaymentHash, + Preimage: &mockPreimage, + AmountMsat: 123000, + }) + + transactionsService := NewTransactionsService(svc.DB) + transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockInvoice, svc.LNClient, nil, nil) + + assert.NoError(t, err) + assert.Equal(t, uint64(123000), transaction.AmountMsat) + assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State) + assert.Equal(t, mockPreimage, *transaction.Preimage) + assert.True(t, transaction.SelfPayment) + + transactionType := constants.TRANSACTION_TYPE_INCOMING + incomingTransaction, err := transactionsService.LookupTransaction(ctx, tests.MockPaymentHash, &transactionType, svc.LNClient, nil) + assert.NoError(t, err) + assert.Equal(t, uint64(123000), incomingTransaction.AmountMsat) + assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, incomingTransaction.State) + assert.Equal(t, mockPreimage, *incomingTransaction.Preimage) + assert.True(t, incomingTransaction.SelfPayment) + + transactions := []db.Transaction{} + result := svc.DB.Find(&transactions) + assert.Equal(t, int64(2), result.RowsAffected) +} + +func TestSendPaymentSync_SelfPayment_NoAppToIsolatedApp(t *testing.T) { + ctx := context.TODO() + + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + // pubkey matches mock invoice = self payment + svc.LNClient.(*tests.MockLn).Pubkey = "02a5056398235568fc049a5d563f1adf666041d590b268167e4fa145fbf71aa578" + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + app.Isolated = true + svc.DB.Save(&app) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + mockPreimage := "123preimage" + svc.DB.Create(&db.Transaction{ + State: constants.TRANSACTION_STATE_PENDING, + Type: constants.TRANSACTION_TYPE_INCOMING, + PaymentRequest: tests.MockInvoice, + PaymentHash: tests.MockPaymentHash, + Preimage: &mockPreimage, + AmountMsat: 123000, + AppId: &app.ID, + }) + + transactionsService := NewTransactionsService(svc.DB) + transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockInvoice, svc.LNClient, nil, nil) + + assert.NoError(t, err) + assert.Equal(t, uint64(123000), transaction.AmountMsat) + assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State) + assert.Equal(t, mockPreimage, *transaction.Preimage) + assert.True(t, transaction.SelfPayment) + + transactionType := constants.TRANSACTION_TYPE_INCOMING + incomingTransaction, err := transactionsService.LookupTransaction(ctx, tests.MockPaymentHash, &transactionType, svc.LNClient, nil) + assert.NoError(t, err) + assert.Equal(t, uint64(123000), incomingTransaction.AmountMsat) + assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, incomingTransaction.State) + assert.Equal(t, mockPreimage, *incomingTransaction.Preimage) + assert.Equal(t, app.ID, *incomingTransaction.AppId) + assert.True(t, incomingTransaction.SelfPayment) + + transactions := []db.Transaction{} + result := svc.DB.Find(&transactions) + assert.Equal(t, int64(2), result.RowsAffected) + // expect balance to be increased + assert.Equal(t, uint64(123000), queries.GetIsolatedBalance(svc.DB, app.ID)) +} + +func TestSendPaymentSync_SelfPayment_NoAppToApp(t *testing.T) { + ctx := context.TODO() + + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + // pubkey matches mock invoice = self payment + svc.LNClient.(*tests.MockLn).Pubkey = "02a5056398235568fc049a5d563f1adf666041d590b268167e4fa145fbf71aa578" + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + mockPreimage := "123preimage" + svc.DB.Create(&db.Transaction{ + State: constants.TRANSACTION_STATE_PENDING, + Type: constants.TRANSACTION_TYPE_INCOMING, + PaymentRequest: tests.MockInvoice, + PaymentHash: tests.MockPaymentHash, + Preimage: &mockPreimage, + AmountMsat: 123000, + AppId: &app.ID, + }) + + transactionsService := NewTransactionsService(svc.DB) + transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockInvoice, svc.LNClient, nil, nil) + + assert.NoError(t, err) + assert.Equal(t, uint64(123000), transaction.AmountMsat) + assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State) + assert.Equal(t, mockPreimage, *transaction.Preimage) + assert.True(t, transaction.SelfPayment) + + transactionType := constants.TRANSACTION_TYPE_INCOMING + incomingTransaction, err := transactionsService.LookupTransaction(ctx, tests.MockPaymentHash, &transactionType, svc.LNClient, nil) + assert.NoError(t, err) + assert.Equal(t, uint64(123000), incomingTransaction.AmountMsat) + assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, incomingTransaction.State) + assert.Equal(t, mockPreimage, *incomingTransaction.Preimage) + assert.Equal(t, app.ID, *incomingTransaction.AppId) + assert.True(t, incomingTransaction.SelfPayment) + + transactions := []db.Transaction{} + result := svc.DB.Find(&transactions) + assert.Equal(t, int64(2), result.RowsAffected) +} + +func TestSendPaymentSync_SelfPayment_IsolatedAppToNoApp(t *testing.T) { + ctx := context.TODO() + + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + // pubkey matches mock invoice = self payment + svc.LNClient.(*tests.MockLn).Pubkey = "02a5056398235568fc049a5d563f1adf666041d590b268167e4fa145fbf71aa578" + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + app.Isolated = true + err = svc.DB.Save(&app).Error + assert.NoError(t, err) + + appPermission := &db.AppPermission{ + AppId: app.ID, + App: *app, + Scope: constants.PAY_INVOICE_SCOPE, + } + err = svc.DB.Create(appPermission).Error + assert.NoError(t, err) + + // give the isolated app 133 sats + svc.DB.Create(&db.Transaction{ + AppId: &app.ID, + State: constants.TRANSACTION_STATE_SETTLED, + Type: constants.TRANSACTION_TYPE_INCOMING, + AmountMsat: 133000, // invoice is 123000 msat, but we also calculate fee reserves max of(10 sats or 1%) + }) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + mockPreimage := "123preimage" + svc.DB.Create(&db.Transaction{ + State: constants.TRANSACTION_STATE_PENDING, + Type: constants.TRANSACTION_TYPE_INCOMING, + PaymentRequest: tests.MockInvoice, + PaymentHash: tests.MockPaymentHash, + Preimage: &mockPreimage, + AmountMsat: 123000, + }) + + transactionsService := NewTransactionsService(svc.DB) + transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockInvoice, svc.LNClient, &app.ID, &dbRequestEvent.ID) + + assert.NoError(t, err) + assert.Equal(t, uint64(123000), transaction.AmountMsat) + assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State) + assert.Equal(t, mockPreimage, *transaction.Preimage) + assert.Equal(t, app.ID, *transaction.AppId) + assert.Equal(t, dbRequestEvent.ID, *transaction.RequestEventId) + assert.True(t, transaction.SelfPayment) + + transactionType := constants.TRANSACTION_TYPE_INCOMING + incomingTransaction, err := transactionsService.LookupTransaction(ctx, tests.MockPaymentHash, &transactionType, svc.LNClient, nil) + assert.NoError(t, err) + assert.Equal(t, uint64(123000), incomingTransaction.AmountMsat) + assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, incomingTransaction.State) + assert.Equal(t, mockPreimage, *incomingTransaction.Preimage) + assert.True(t, incomingTransaction.SelfPayment) + + transactions := []db.Transaction{} + result := svc.DB.Find(&transactions) + assert.Equal(t, int64(3), result.RowsAffected) + // expect balance to be decreased + assert.Equal(t, uint64(10000), queries.GetIsolatedBalance(svc.DB, app.ID)) +} + +func TestSendPaymentSync_SelfPayment_IsolatedAppToApp(t *testing.T) { + ctx := context.TODO() + + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + // pubkey matches mock invoice = self payment + svc.LNClient.(*tests.MockLn).Pubkey = "02a5056398235568fc049a5d563f1adf666041d590b268167e4fa145fbf71aa578" + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + app.Isolated = true + err = svc.DB.Save(&app).Error + assert.NoError(t, err) + app2, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + + appPermission := &db.AppPermission{ + AppId: app.ID, + App: *app, + Scope: constants.PAY_INVOICE_SCOPE, + } + err = svc.DB.Create(appPermission).Error + assert.NoError(t, err) + + // give the isolated app 133 sats + svc.DB.Create(&db.Transaction{ + AppId: &app.ID, + State: constants.TRANSACTION_STATE_SETTLED, + Type: constants.TRANSACTION_TYPE_INCOMING, + AmountMsat: 133000, // invoice is 123000 msat, but we also calculate fee reserves max of(10 sats or 1%) + }) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + mockPreimage := "123preimage" + svc.DB.Create(&db.Transaction{ + State: constants.TRANSACTION_STATE_PENDING, + Type: constants.TRANSACTION_TYPE_INCOMING, + PaymentRequest: tests.MockInvoice, + PaymentHash: tests.MockPaymentHash, + Preimage: &mockPreimage, + AmountMsat: 123000, + AppId: &app2.ID, + }) + + transactionsService := NewTransactionsService(svc.DB) + transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockInvoice, svc.LNClient, &app.ID, &dbRequestEvent.ID) + + assert.NoError(t, err) + assert.Equal(t, uint64(123000), transaction.AmountMsat) + assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State) + assert.Equal(t, mockPreimage, *transaction.Preimage) + assert.Equal(t, app.ID, *transaction.AppId) + assert.Equal(t, dbRequestEvent.ID, *transaction.RequestEventId) + assert.True(t, transaction.SelfPayment) + + transactionType := constants.TRANSACTION_TYPE_INCOMING + incomingTransaction, err := transactionsService.LookupTransaction(ctx, tests.MockPaymentHash, &transactionType, svc.LNClient, nil) + assert.NoError(t, err) + assert.Equal(t, uint64(123000), incomingTransaction.AmountMsat) + assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, incomingTransaction.State) + assert.Equal(t, mockPreimage, *incomingTransaction.Preimage) + assert.Equal(t, app2.ID, *incomingTransaction.AppId) + assert.True(t, incomingTransaction.SelfPayment) + + transactions := []db.Transaction{} + result := svc.DB.Find(&transactions) + assert.Equal(t, int64(3), result.RowsAffected) + // expect balance to be decreased + assert.Equal(t, uint64(10000), queries.GetIsolatedBalance(svc.DB, app.ID)) +} + +func TestSendPaymentSync_SelfPayment_IsolatedAppToIsolatedApp(t *testing.T) { + ctx := context.TODO() + + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + // pubkey matches mock invoice = self payment + svc.LNClient.(*tests.MockLn).Pubkey = "02a5056398235568fc049a5d563f1adf666041d590b268167e4fa145fbf71aa578" + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + app.Isolated = true + err = svc.DB.Save(&app).Error + assert.NoError(t, err) + app2, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + app2.Isolated = true + err = svc.DB.Save(&app2).Error + assert.NoError(t, err) + + appPermission := &db.AppPermission{ + AppId: app.ID, + App: *app, + Scope: constants.PAY_INVOICE_SCOPE, + } + err = svc.DB.Create(appPermission).Error + assert.NoError(t, err) + + // give the isolated app 133 sats + svc.DB.Create(&db.Transaction{ + AppId: &app.ID, + State: constants.TRANSACTION_STATE_SETTLED, + Type: constants.TRANSACTION_TYPE_INCOMING, + AmountMsat: 133000, // invoice is 123000 msat, but we also calculate fee reserves max of(10 sats or 1%) + }) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + mockPreimage := "123preimage" + svc.DB.Create(&db.Transaction{ + State: constants.TRANSACTION_STATE_PENDING, + Type: constants.TRANSACTION_TYPE_INCOMING, + PaymentRequest: tests.MockInvoice, + PaymentHash: tests.MockPaymentHash, + Preimage: &mockPreimage, + AmountMsat: 123000, + AppId: &app2.ID, + }) + + transactionsService := NewTransactionsService(svc.DB) + transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockInvoice, svc.LNClient, &app.ID, &dbRequestEvent.ID) + + assert.NoError(t, err) + assert.Equal(t, uint64(123000), transaction.AmountMsat) + assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State) + assert.Equal(t, mockPreimage, *transaction.Preimage) + assert.Equal(t, app.ID, *transaction.AppId) + assert.Equal(t, dbRequestEvent.ID, *transaction.RequestEventId) + assert.True(t, transaction.SelfPayment) + + transactionType := constants.TRANSACTION_TYPE_INCOMING + incomingTransaction, err := transactionsService.LookupTransaction(ctx, tests.MockPaymentHash, &transactionType, svc.LNClient, nil) + assert.NoError(t, err) + assert.Equal(t, uint64(123000), incomingTransaction.AmountMsat) + assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, incomingTransaction.State) + assert.Equal(t, mockPreimage, *incomingTransaction.Preimage) + assert.Equal(t, app2.ID, *incomingTransaction.AppId) + assert.True(t, incomingTransaction.SelfPayment) + + transactions := []db.Transaction{} + result := svc.DB.Find(&transactions) + assert.Equal(t, int64(3), result.RowsAffected) + // expect balance to be decreased + assert.Equal(t, uint64(10000), queries.GetIsolatedBalance(svc.DB, app.ID)) +} + +func TestSendPaymentSync_SelfPayment_IsolatedAppToSelf(t *testing.T) { + ctx := context.TODO() + + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + // pubkey matches mock invoice = self payment + svc.LNClient.(*tests.MockLn).Pubkey = "02a5056398235568fc049a5d563f1adf666041d590b268167e4fa145fbf71aa578" + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + app.Isolated = true + err = svc.DB.Save(&app).Error + assert.NoError(t, err) + + appPermission := &db.AppPermission{ + AppId: app.ID, + App: *app, + Scope: constants.PAY_INVOICE_SCOPE, + } + err = svc.DB.Create(appPermission).Error + assert.NoError(t, err) + + // give the isolated app 133 sats + svc.DB.Create(&db.Transaction{ + AppId: &app.ID, + State: constants.TRANSACTION_STATE_SETTLED, + Type: constants.TRANSACTION_TYPE_INCOMING, + AmountMsat: 133000, // invoice is 123000 msat, but we also calculate fee reserves max of(10 sats or 1%) + }) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + mockPreimage := "123preimage" + svc.DB.Create(&db.Transaction{ + State: constants.TRANSACTION_STATE_PENDING, + Type: constants.TRANSACTION_TYPE_INCOMING, + PaymentRequest: tests.MockInvoice, + PaymentHash: tests.MockPaymentHash, + Preimage: &mockPreimage, + AmountMsat: 123000, + AppId: &app.ID, + }) + + transactionsService := NewTransactionsService(svc.DB) + transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockInvoice, svc.LNClient, &app.ID, &dbRequestEvent.ID) + + assert.NoError(t, err) + assert.Equal(t, uint64(123000), transaction.AmountMsat) + assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State) + assert.Equal(t, mockPreimage, *transaction.Preimage) + assert.Equal(t, app.ID, *transaction.AppId) + assert.Equal(t, dbRequestEvent.ID, *transaction.RequestEventId) + assert.True(t, transaction.SelfPayment) + + transactionType := constants.TRANSACTION_TYPE_INCOMING + incomingTransaction, err := transactionsService.LookupTransaction(ctx, tests.MockPaymentHash, &transactionType, svc.LNClient, nil) + assert.NoError(t, err) + assert.Equal(t, uint64(123000), incomingTransaction.AmountMsat) + assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, incomingTransaction.State) + assert.Equal(t, mockPreimage, *incomingTransaction.Preimage) + assert.Equal(t, app.ID, *incomingTransaction.AppId) + assert.True(t, incomingTransaction.SelfPayment) + + transactions := []db.Transaction{} + result := svc.DB.Find(&transactions) + assert.Equal(t, int64(3), result.RowsAffected) + + // expect balance to be unchanged + assert.Equal(t, uint64(133000), queries.GetIsolatedBalance(svc.DB, app.ID)) +} diff --git a/transactions/todo_test.go b/transactions/todo_test.go index b795cde9..d267ee25 100644 --- a/transactions/todo_test.go +++ b/transactions/todo_test.go @@ -1,9 +1,8 @@ package transactions -// TODO: self payments - // TODO: keysend // TODO: lookup invoice // TODO: list transactions // TODO: notifications +// TODO: fee reserve removed diff --git a/transactions/transactions_service.go b/transactions/transactions_service.go index f28fce55..38f2923a 100644 --- a/transactions/transactions_service.go +++ b/transactions/transactions_service.go @@ -27,7 +27,7 @@ type transactionsService struct { type TransactionsService interface { events.EventSubscriber MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error) - LookupTransaction(ctx context.Context, paymentHash string, lnClient lnclient.LNClient, appId *uint) (*Transaction, error) + LookupTransaction(ctx context.Context, paymentHash string, transactionType *string, lnClient lnclient.LNClient, appId *uint) (*Transaction, error) ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaid bool, invoiceType string, lnClient lnclient.LNClient, appId *uint) (transactions []Transaction, err error) SendPaymentSync(ctx context.Context, payReq string, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error) SendKeysend(ctx context.Context, amount uint64, destination string, customRecords []lnclient.TLVRecord, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error) @@ -135,6 +135,8 @@ func (svc *transactionsService) SendPaymentSync(ctx context.Context, payReq stri return nil, err } + selfPayment := paymentRequest.Payee != "" && paymentRequest.Payee == lnClient.GetPubkey() + var dbTransaction db.Transaction err = svc.db.Transaction(func(tx *gorm.DB) error { @@ -161,6 +163,7 @@ func (svc *transactionsService) SendPaymentSync(ctx context.Context, payReq stri Description: paymentRequest.Description, DescriptionHash: paymentRequest.DescriptionHash, ExpiresAt: expiresAt, + SelfPayment: selfPayment, // Metadata: metadata, } err = tx.Create(&dbTransaction).Error @@ -175,7 +178,7 @@ func (svc *transactionsService) SendPaymentSync(ctx context.Context, payReq stri } var response *lnclient.PayInvoiceResponse - if paymentRequest.Payee != "" && paymentRequest.Payee == lnClient.GetPubkey() { + if selfPayment { response, err = svc.interceptSelfPayment(paymentRequest.PaymentHash) } else { response, err = lnClient.SendPaymentSync(ctx, payReq) @@ -335,7 +338,7 @@ func (svc *transactionsService) SendKeysend(ctx context.Context, amount uint64, return &dbTransaction, nil } -func (svc *transactionsService) LookupTransaction(ctx context.Context, paymentHash string, lnClient lnclient.LNClient, appId *uint) (*Transaction, error) { +func (svc *transactionsService) LookupTransaction(ctx context.Context, paymentHash string, transactionType *string, lnClient lnclient.LNClient, appId *uint) (*Transaction, error) { transaction := db.Transaction{} tx := svc.db @@ -350,6 +353,10 @@ func (svc *transactionsService) LookupTransaction(ctx context.Context, paymentHa } } + if transactionType != nil { + tx = tx.Where("type == ?", *transactionType) + } + // order settled first, otherwise by created date, as there can be multiple outgoing payments // for the same payment hash (if you tried to pay an invoice multiple times - e.g. the first time failed) result := tx.Order("settled_at desc, created_at desc").Find(&transaction, &db.Transaction{ @@ -624,16 +631,17 @@ func (svc *transactionsService) interceptSelfPayment(paymentHash string) (*lncli return nil, NewNotFoundError() } if incomingTransaction.Preimage == nil { - return nil, errors.New("preimage is not set on transaction. Self payments not supported.") + return nil, errors.New("preimage is not set on transaction. Self payments not supported") } // update the incoming transaction now := time.Now() fee := uint64(0) err := svc.db.Model(&incomingTransaction).Updates(&db.Transaction{ - State: constants.TRANSACTION_STATE_SETTLED, - FeeMsat: &fee, - SettledAt: &now, + State: constants.TRANSACTION_STATE_SETTLED, + FeeMsat: &fee, + SettledAt: &now, + SelfPayment: true, }).Error if err != nil { return nil, err From 82f0dc5f7ba4b1bebdb156d15701a7d8d17d2798 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Fri, 19 Jul 2024 12:33:00 +0700 Subject: [PATCH 3/5] fix: notifications tests --- .gitignore | 3 ++- nip47/notifications/nip47_notifier_test.go | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 4f2df2e3..916190a9 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,5 @@ frontend/src/utils/openLink.ts glalby *.db-shm -*.db-wal \ No newline at end of file +*.db-wal +*.db-journal \ No newline at end of file diff --git a/nip47/notifications/nip47_notifier_test.go b/nip47/notifications/nip47_notifier_test.go index c1462501..bf7030fb 100644 --- a/nip47/notifications/nip47_notifier_test.go +++ b/nip47/notifications/nip47_notifier_test.go @@ -51,7 +51,7 @@ func TestSendNotification_PaymentReceived(t *testing.T) { feesPaid := uint64(tests.MockLNClientTransaction.FeesPaid) settledAt := time.Unix(*tests.MockLNClientTransaction.SettledAt, 0) err = svc.DB.Create(&db.Transaction{ - Type: tests.MockLNClientTransaction.Type, + Type: constants.TRANSACTION_TYPE_INCOMING, PaymentRequest: tests.MockLNClientTransaction.Invoice, Description: tests.MockLNClientTransaction.Description, DescriptionHash: tests.MockLNClientTransaction.DescriptionHash, @@ -101,7 +101,7 @@ func TestSendNotification_PaymentReceived(t *testing.T) { assert.Equal(t, PAYMENT_RECEIVED_NOTIFICATION, unmarshalledResponse.NotificationType) transaction := (unmarshalledResponse.Notification.(*PaymentReceivedNotification)) - assert.Equal(t, tests.MockLNClientTransaction.Type, transaction.Type) + assert.Equal(t, constants.TRANSACTION_TYPE_INCOMING, transaction.Type) assert.Equal(t, tests.MockLNClientTransaction.Invoice, transaction.Invoice) assert.Equal(t, tests.MockLNClientTransaction.Description, transaction.Description) assert.Equal(t, tests.MockLNClientTransaction.DescriptionHash, transaction.DescriptionHash) @@ -132,7 +132,7 @@ func TestSendNotification_PaymentSent(t *testing.T) { feesPaid := uint64(tests.MockLNClientTransaction.FeesPaid) settledAt := time.Unix(*tests.MockLNClientTransaction.SettledAt, 0) err = svc.DB.Create(&db.Transaction{ - Type: tests.MockLNClientTransaction.Type, + Type: constants.TRANSACTION_TYPE_OUTGOING, PaymentRequest: tests.MockLNClientTransaction.Invoice, Description: tests.MockLNClientTransaction.Description, DescriptionHash: tests.MockLNClientTransaction.DescriptionHash, @@ -182,7 +182,7 @@ func TestSendNotification_PaymentSent(t *testing.T) { assert.Equal(t, PAYMENT_SENT_NOTIFICATION, unmarshalledResponse.NotificationType) transaction := (unmarshalledResponse.Notification.(*PaymentReceivedNotification)) - assert.Equal(t, tests.MockLNClientTransaction.Type, transaction.Type) + assert.Equal(t, constants.TRANSACTION_TYPE_OUTGOING, transaction.Type) assert.Equal(t, tests.MockLNClientTransaction.Invoice, transaction.Invoice) assert.Equal(t, tests.MockLNClientTransaction.Description, transaction.Description) assert.Equal(t, tests.MockLNClientTransaction.DescriptionHash, transaction.DescriptionHash) From 6a2ce7236e097a727adda39de88b6179ebbf11cf Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Fri, 19 Jul 2024 13:27:49 +0700 Subject: [PATCH 4/5] chore: add notifications tests for transactions service --- transactions/notifications_test.go | 179 +++++++++++++++++++++++++++ transactions/todo_test.go | 3 +- transactions/transactions_service.go | 76 +++++++++--- 3 files changed, 237 insertions(+), 21 deletions(-) create mode 100644 transactions/notifications_test.go diff --git a/transactions/notifications_test.go b/transactions/notifications_test.go new file mode 100644 index 00000000..25721bd4 --- /dev/null +++ b/transactions/notifications_test.go @@ -0,0 +1,179 @@ +package transactions + +import ( + "context" + "testing" + + "github.com/getAlby/hub/constants" + "github.com/getAlby/hub/db" + "github.com/getAlby/hub/events" + "github.com/getAlby/hub/tests" + "github.com/stretchr/testify/assert" +) + +func TestNotifications_ReceivedKnownPayment(t *testing.T) { + ctx := context.TODO() + + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + mockPreimage := tests.MockLNClientTransaction.Preimage + svc.DB.Create(&db.Transaction{ + State: constants.TRANSACTION_STATE_PENDING, + Type: constants.TRANSACTION_TYPE_INCOMING, + PaymentRequest: tests.MockLNClientTransaction.Invoice, + PaymentHash: tests.MockLNClientTransaction.PaymentHash, + Preimage: &mockPreimage, + AmountMsat: 123000, + }) + + transactionsService := NewTransactionsService(svc.DB) + + transactionsService.ConsumeEvent(ctx, &events.Event{ + Event: "nwc_payment_received", + Properties: tests.MockLNClientTransaction, + }, map[string]interface{}{}) + + incomingTransaction, err := transactionsService.LookupTransaction(ctx, tests.MockLNClientTransaction.PaymentHash, nil, svc.LNClient, nil) + assert.NoError(t, err) + assert.Equal(t, uint64(123000), incomingTransaction.AmountMsat) + assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, incomingTransaction.State) + assert.Equal(t, tests.MockLNClientTransaction.Preimage, *incomingTransaction.Preimage) + assert.Nil(t, incomingTransaction.FeeReserveMsat) + + transactions := []db.Transaction{} + result := svc.DB.Find(&transactions) + assert.Equal(t, int64(1), result.RowsAffected) +} + +func TestNotifications_ReceivedUnknownPayment(t *testing.T) { + ctx := context.TODO() + + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + transactionsService := NewTransactionsService(svc.DB) + + transactionsService.ConsumeEvent(ctx, &events.Event{ + Event: "nwc_payment_received", + Properties: tests.MockLNClientTransaction, + }, map[string]interface{}{}) + + transactionType := constants.TRANSACTION_TYPE_INCOMING + incomingTransaction, err := transactionsService.LookupTransaction(ctx, tests.MockLNClientTransaction.PaymentHash, &transactionType, svc.LNClient, nil) + assert.NoError(t, err) + assert.Equal(t, uint64(tests.MockLNClientTransaction.Amount), incomingTransaction.AmountMsat) + assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, incomingTransaction.State) + assert.Equal(t, tests.MockLNClientTransaction.Preimage, *incomingTransaction.Preimage) + assert.Nil(t, incomingTransaction.FeeReserveMsat) + + transactions := []db.Transaction{} + result := svc.DB.Find(&transactions) + assert.Equal(t, int64(1), result.RowsAffected) +} + +func TestNotifications_SentKnownPayment(t *testing.T) { + ctx := context.TODO() + + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + feeReserve := uint64(10000) + svc.DB.Create(&db.Transaction{ + State: constants.TRANSACTION_STATE_PENDING, + Type: constants.TRANSACTION_TYPE_OUTGOING, + PaymentRequest: tests.MockLNClientTransaction.Invoice, + PaymentHash: tests.MockLNClientTransaction.PaymentHash, + AmountMsat: 123000, + FeeReserveMsat: &feeReserve, + }) + + transactionsService := NewTransactionsService(svc.DB) + + transactionsService.ConsumeEvent(ctx, &events.Event{ + Event: "nwc_payment_sent", + Properties: tests.MockLNClientTransaction, + }, map[string]interface{}{}) + + transactionType := constants.TRANSACTION_TYPE_OUTGOING + outgoingTransaction, err := transactionsService.LookupTransaction(ctx, tests.MockLNClientTransaction.PaymentHash, &transactionType, svc.LNClient, nil) + assert.NoError(t, err) + assert.Equal(t, uint64(123000), outgoingTransaction.AmountMsat) + assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, outgoingTransaction.State) + assert.Equal(t, tests.MockLNClientTransaction.Preimage, *outgoingTransaction.Preimage) + assert.Zero(t, *outgoingTransaction.FeeReserveMsat) + + transactions := []db.Transaction{} + result := svc.DB.Find(&transactions) + assert.Equal(t, int64(1), result.RowsAffected) +} + +func TestNotifications_SentUnknownPayment(t *testing.T) { + ctx := context.TODO() + + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + transactionsService := NewTransactionsService(svc.DB) + + transactions := []db.Transaction{} + result := svc.DB.Find(&transactions) + assert.Equal(t, int64(0), result.RowsAffected) + + transactionsService.ConsumeEvent(ctx, &events.Event{ + Event: "nwc_payment_sent", + Properties: tests.MockLNClientTransaction, + }, map[string]interface{}{}) + + transactionType := constants.TRANSACTION_TYPE_OUTGOING + outgoingTransaction, err := transactionsService.LookupTransaction(ctx, tests.MockLNClientTransaction.PaymentHash, &transactionType, svc.LNClient, nil) + assert.NoError(t, err) + assert.Equal(t, uint64(tests.MockLNClientTransaction.Amount), outgoingTransaction.AmountMsat) + assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, outgoingTransaction.State) + assert.Equal(t, tests.MockLNClientTransaction.Preimage, *outgoingTransaction.Preimage) + assert.Zero(t, *outgoingTransaction.FeeReserveMsat) + + transactions = []db.Transaction{} + result = svc.DB.Find(&transactions) + assert.Equal(t, int64(1), result.RowsAffected) +} + +func TestNotifications_FailedKnownPayment(t *testing.T) { + ctx := context.TODO() + + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + feeReserve := uint64(10000) + svc.DB.Create(&db.Transaction{ + State: constants.TRANSACTION_STATE_PENDING, + Type: constants.TRANSACTION_TYPE_OUTGOING, + PaymentRequest: tests.MockLNClientTransaction.Invoice, + PaymentHash: tests.MockLNClientTransaction.PaymentHash, + AmountMsat: 123000, + FeeReserveMsat: &feeReserve, + }) + + transactionsService := NewTransactionsService(svc.DB) + + transactionsService.ConsumeEvent(ctx, &events.Event{ + Event: "nwc_payment_failed_async", + Properties: tests.MockLNClientTransaction, + }, map[string]interface{}{}) + + transactionType := constants.TRANSACTION_TYPE_OUTGOING + outgoingTransaction, err := transactionsService.LookupTransaction(ctx, tests.MockLNClientTransaction.PaymentHash, &transactionType, svc.LNClient, nil) + assert.NoError(t, err) + assert.Equal(t, constants.TRANSACTION_STATE_FAILED, outgoingTransaction.State) + assert.Nil(t, outgoingTransaction.Preimage) + assert.Zero(t, *outgoingTransaction.FeeReserveMsat) + + transactions := []db.Transaction{} + result := svc.DB.Find(&transactions) + assert.Equal(t, int64(1), result.RowsAffected) +} diff --git a/transactions/todo_test.go b/transactions/todo_test.go index d267ee25..511fb72c 100644 --- a/transactions/todo_test.go +++ b/transactions/todo_test.go @@ -5,4 +5,5 @@ package transactions // TODO: list transactions // TODO: notifications -// TODO: fee reserve removed +// TODO: fee reserve removed - successful payment +// TODO: fee reserve removed - failed payment diff --git a/transactions/transactions_service.go b/transactions/transactions_service.go index 38f2923a..787e9ee5 100644 --- a/transactions/transactions_service.go +++ b/transactions/transactions_service.go @@ -486,7 +486,7 @@ func (svc *transactionsService) ConsumeEvent(ctx context.Context, event *events. }) if result.RowsAffected == 0 { - // Note: brand new payments (keysend only) cannot be associated with an app + // Note: brand new payments cannot be associated with an app var metadata string if lnClientTransaction.Metadata != nil { metadataBytes, err := json.Marshal(lnClientTransaction.Metadata) @@ -554,26 +554,60 @@ func (svc *transactionsService) ConsumeEvent(ctx context.Context, event *events. } var dbTransaction db.Transaction - result := svc.db.Find(&dbTransaction, &db.Transaction{ - Type: constants.TRANSACTION_TYPE_OUTGOING, - PaymentHash: lnClientTransaction.PaymentHash, - }) + err := svc.db.Transaction(func(tx *gorm.DB) error { + result := tx.Find(&dbTransaction, &db.Transaction{ + Type: constants.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 - } + if result.RowsAffected == 0 { + // Note: brand new payments cannot be associated with an app + var metadata string + if lnClientTransaction.Metadata != nil { + metadataBytes, err := json.Marshal(lnClientTransaction.Metadata) + if err != nil { + logger.Logger.WithError(err).Error("Failed to serialize transaction metadata") + return err + } + metadata = string(metadataBytes) + } + var expiresAt *time.Time + if lnClientTransaction.ExpiresAt != nil { + expiresAtValue := time.Unix(*lnClientTransaction.ExpiresAt, 0) + expiresAt = &expiresAtValue + } + dbTransaction = db.Transaction{ + Type: constants.TRANSACTION_TYPE_OUTGOING, + AmountMsat: uint64(lnClientTransaction.Amount), + PaymentRequest: lnClientTransaction.Invoice, + PaymentHash: lnClientTransaction.PaymentHash, + Description: lnClientTransaction.Description, + DescriptionHash: lnClientTransaction.DescriptionHash, + ExpiresAt: expiresAt, + Metadata: metadata, + } + err := tx.Create(&dbTransaction).Error + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "payment_hash": lnClientTransaction.PaymentHash, + }).WithError(err).Error("Failed to create transaction") + return err + } + } + + settledAt := time.Now() + fee := uint64(lnClientTransaction.FeesPaid) + feeReserve := uint64(0) + err := tx.Model(&dbTransaction).Updates(&db.Transaction{ + FeeMsat: &fee, + FeeReserveMsat: &feeReserve, + Preimage: &lnClientTransaction.Preimage, + State: constants.TRANSACTION_STATE_SETTLED, + SettledAt: &settledAt, + }).Error + return err + }) - settledAt := time.Now() - fee := uint64(lnClientTransaction.FeesPaid) - feeReserve := uint64(0) - err := svc.db.Model(&dbTransaction).Updates(&db.Transaction{ - FeeMsat: &fee, - FeeReserveMsat: &feeReserve, - Preimage: &lnClientTransaction.Preimage, - State: constants.TRANSACTION_STATE_SETTLED, - SettledAt: &settledAt, - }).Error if err != nil { logger.Logger.WithFields(logrus.Fields{ "payment_hash": lnClientTransaction.PaymentHash, @@ -602,8 +636,10 @@ func (svc *transactionsService) ConsumeEvent(ctx context.Context, event *events. return } + feeReserve := uint64(0) err := svc.db.Model(&dbTransaction).Updates(&db.Transaction{ - State: constants.TRANSACTION_STATE_FAILED, + State: constants.TRANSACTION_STATE_FAILED, + FeeReserveMsat: &feeReserve, }).Error if err != nil { logger.Logger.WithFields(logrus.Fields{ From 30c17999cd8ff010c36db23e2d83429a1fd5df8e Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Fri, 19 Jul 2024 13:45:43 +0700 Subject: [PATCH 5/5] chore: add fee reserve tests for transactions service --- tests/mock_ln_client.go | 4 +- transactions/payments_test.go | 56 ++++++++++++++++++++++++++++ transactions/todo_test.go | 4 -- transactions/transactions_service.go | 4 +- 4 files changed, 61 insertions(+), 7 deletions(-) diff --git a/tests/mock_ln_client.go b/tests/mock_ln_client.go index 522b17b7..a9043625 100644 --- a/tests/mock_ln_client.go +++ b/tests/mock_ln_client.go @@ -33,7 +33,7 @@ var MockLNClientTransactions = []lnclient.Transaction{ Description: "mock invoice 1", DescriptionHash: "hash1", Preimage: "preimage1", - PaymentHash: "payment_hash_1", + PaymentHash: MockPaymentHash, Amount: 1000, FeesPaid: 50, SettledAt: &MockTimeUnix, @@ -48,7 +48,7 @@ var MockLNClientTransactions = []lnclient.Transaction{ Description: "mock invoice 2", DescriptionHash: "hash2", Preimage: "preimage2", - PaymentHash: "payment_hash_2", + PaymentHash: MockPaymentHash, Amount: 2000, FeesPaid: 75, SettledAt: &MockTimeUnix, diff --git a/transactions/payments_test.go b/transactions/payments_test.go index 300b52fb..ba75a03e 100644 --- a/transactions/payments_test.go +++ b/transactions/payments_test.go @@ -2,9 +2,11 @@ package transactions import ( "context" + "errors" "testing" "github.com/getAlby/hub/constants" + "github.com/getAlby/hub/lnclient" "github.com/getAlby/hub/tests" "github.com/stretchr/testify/assert" ) @@ -22,5 +24,59 @@ func TestSendPaymentSync_NoApp(t *testing.T) { assert.NoError(t, err) assert.Equal(t, uint64(123000), transaction.AmountMsat) assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, transaction.State) + assert.Zero(t, *transaction.FeeReserveMsat) assert.Equal(t, "123preimage", *transaction.Preimage) } + +func TestSendPaymentSync_FailedRemovesFeeReserve(t *testing.T) { + ctx := context.TODO() + + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + svc.LNClient.(*tests.MockLn).PayInvoiceErrors = append(svc.LNClient.(*tests.MockLn).PayInvoiceErrors, errors.New("Some error")) + svc.LNClient.(*tests.MockLn).PayInvoiceResponses = append(svc.LNClient.(*tests.MockLn).PayInvoiceResponses, nil) + + transactionsService := NewTransactionsService(svc.DB) + transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, nil, nil) + + assert.Error(t, err) + assert.Nil(t, transaction) + + transactionType := constants.TRANSACTION_TYPE_OUTGOING + transaction, err = transactionsService.LookupTransaction(ctx, tests.MockLNClientTransaction.PaymentHash, &transactionType, svc.LNClient, nil) + assert.Nil(t, err) + + assert.Equal(t, uint64(123000), transaction.AmountMsat) + assert.Equal(t, constants.TRANSACTION_STATE_FAILED, transaction.State) + assert.Zero(t, *transaction.FeeReserveMsat) + assert.Nil(t, transaction.Preimage) +} + +func TestSendPaymentSync_PendingHasFeeReserve(t *testing.T) { + ctx := context.TODO() + + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + assert.NoError(t, err) + + // timeout will leave the payment as pending + svc.LNClient.(*tests.MockLn).PayInvoiceErrors = append(svc.LNClient.(*tests.MockLn).PayInvoiceErrors, lnclient.NewTimeoutError()) + svc.LNClient.(*tests.MockLn).PayInvoiceResponses = append(svc.LNClient.(*tests.MockLn).PayInvoiceResponses, nil) + + transactionsService := NewTransactionsService(svc.DB) + transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, nil, nil) + + assert.Error(t, err) + assert.Nil(t, transaction) + + transactionType := constants.TRANSACTION_TYPE_OUTGOING + transaction, err = transactionsService.LookupTransaction(ctx, tests.MockLNClientTransaction.PaymentHash, &transactionType, svc.LNClient, nil) + assert.Nil(t, err) + + assert.Equal(t, uint64(123000), transaction.AmountMsat) + assert.Equal(t, constants.TRANSACTION_STATE_PENDING, transaction.State) + assert.Equal(t, uint64(10000), *transaction.FeeReserveMsat) + assert.Nil(t, transaction.Preimage) +} diff --git a/transactions/todo_test.go b/transactions/todo_test.go index 511fb72c..efacd742 100644 --- a/transactions/todo_test.go +++ b/transactions/todo_test.go @@ -3,7 +3,3 @@ package transactions // TODO: keysend // TODO: lookup invoice // TODO: list transactions - -// TODO: notifications -// TODO: fee reserve removed - successful payment -// TODO: fee reserve removed - failed payment diff --git a/transactions/transactions_service.go b/transactions/transactions_service.go index 787e9ee5..ebd32be6 100644 --- a/transactions/transactions_service.go +++ b/transactions/transactions_service.go @@ -199,8 +199,10 @@ func (svc *transactionsService) SendPaymentSync(ctx context.Context, payReq stri } // As the LNClient did not return a timeout error, we assume the payment definitely failed + feeReserve := uint64(0) dbErr := svc.db.Model(&dbTransaction).Updates(&db.Transaction{ - State: constants.TRANSACTION_STATE_FAILED, + State: constants.TRANSACTION_STATE_FAILED, + FeeReserveMsat: &feeReserve, }).Error if dbErr != nil { logger.Logger.WithFields(logrus.Fields{