From 911d5f86ce5ac4ee958d823e8faaf73f5c259ef3 Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Mon, 2 Dec 2024 19:36:35 -0600 Subject: [PATCH] feat: return transaction state and allow fetching unpaid transactions by state (#835) * feat: return transaction state and allow fetching unpaid transactions by state * chore: add list transactions tests --- .../list_transactions_controller.go | 17 +- .../list_transactions_controller_test.go | 269 +++++++++++++++++- nip47/models/models.go | 1 + nip47/models/transactions.go | 2 + 4 files changed, 274 insertions(+), 15 deletions(-) diff --git a/nip47/controllers/list_transactions_controller.go b/nip47/controllers/list_transactions_controller.go index 6dad5b6a6..67b9c6fb4 100644 --- a/nip47/controllers/list_transactions_controller.go +++ b/nip47/controllers/list_transactions_controller.go @@ -11,12 +11,14 @@ import ( ) type listTransactionsParams struct { - From uint64 `json:"from,omitempty"` - Until uint64 `json:"until,omitempty"` - Limit uint64 `json:"limit,omitempty"` - Offset uint64 `json:"offset,omitempty"` - Unpaid bool `json:"unpaid,omitempty"` - Type string `json:"type,omitempty"` + From uint64 `json:"from,omitempty"` + Until uint64 `json:"until,omitempty"` + Limit uint64 `json:"limit,omitempty"` + Offset uint64 `json:"offset,omitempty"` + Unpaid bool `json:"unpaid,omitempty"` + UnpaidOutgoing bool `json:"unpaid_outgoing,omitempty"` + UnpaidIncoming bool `json:"unpaid_incoming,omitempty"` + Type string `json:"type,omitempty"` } type listTransactionsResponse struct { @@ -48,8 +50,7 @@ func (controller *nip47Controller) HandleListTransactionsEvent(ctx context.Conte transactionType = &listParams.Type } - // TODO: listParams.Unpaid needs to be updated to support ability to fetch only unpaid outgoing transactions - dbTransactions, err := controller.transactionsService.ListTransactions(ctx, listParams.From, listParams.Until, limit, listParams.Offset, listParams.Unpaid, listParams.Unpaid, transactionType, controller.lnClient, &appId, false) + dbTransactions, err := controller.transactionsService.ListTransactions(ctx, listParams.From, listParams.Until, limit, listParams.Offset, listParams.Unpaid || listParams.UnpaidOutgoing, listParams.Unpaid || listParams.UnpaidIncoming, transactionType, controller.lnClient, &appId, false) if err != nil { logger.Logger.WithFields(logrus.Fields{ "params": listParams, diff --git a/nip47/controllers/list_transactions_controller_test.go b/nip47/controllers/list_transactions_controller_test.go index 2463b7b1a..4f5321aa6 100644 --- a/nip47/controllers/list_transactions_controller_test.go +++ b/nip47/controllers/list_transactions_controller_test.go @@ -18,7 +18,13 @@ import ( "github.com/getAlby/hub/transactions" ) -const nip47ListTransactionsJson = ` +func TestHandleListTransactionsEvent(t *testing.T) { + ctx := context.TODO() + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + require.NoError(t, err) + + const nip47ListTransactionsJson = ` { "method": "list_transactions", "params": { @@ -31,12 +37,6 @@ const nip47ListTransactionsJson = ` } ` -func TestHandleListTransactionsEvent(t *testing.T) { - ctx := context.TODO() - defer tests.RemoveTestService() - svc, err := tests.CreateTestService() - require.NoError(t, err) - nip47Request := &models.Request{} err = json.Unmarshal([]byte(nip47ListTransactionsJson), nip47Request) assert.NoError(t, err) @@ -93,6 +93,261 @@ func TestHandleListTransactionsEvent(t *testing.T) { assert.Equal(t, tests.MockLNClientTransactions[0].Amount, transaction.Amount) assert.Equal(t, tests.MockLNClientTransactions[0].FeesPaid, transaction.FeesPaid) assert.Equal(t, tests.MockLNClientTransactions[0].SettledAt, transaction.SettledAt) + assert.Equal(t, "settled", transaction.State) +} + +func TestHandleListTransactionsEvent_UnpaidOutgoingOnly(t *testing.T) { + ctx := context.TODO() + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + require.NoError(t, err) + + const nip47ListTransactionsJson = ` +{ + "method": "list_transactions", + "params": { + "from": 0, + "until": 0, + "limit": 10, + "offset": 0, + "unpaid_outgoing": true + } +} +` + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47ListTransactionsJson), nip47Request) + assert.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{ + AppId: &app.ID, + } + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + err = svc.DB.Create(&db.Transaction{ + Type: constants.TRANSACTION_TYPE_INCOMING, + State: constants.TRANSACTION_STATE_PENDING, + }).Error + assert.NoError(t, err) + + err = svc.DB.Create(&db.Transaction{ + Type: constants.TRANSACTION_TYPE_OUTGOING, + State: constants.TRANSACTION_STATE_PENDING, + }).Error + assert.NoError(t, err) + + var publishedResponse *models.Response + + publishResponse := func(response *models.Response, tags nostr.Tags) { + publishedResponse = response + } + + permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) + transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + HandleListTransactionsEvent(ctx, nip47Request, dbRequestEvent.ID, *dbRequestEvent.AppId, publishResponse) + + assert.Nil(t, publishedResponse.Error) + + assert.Equal(t, 1, len(publishedResponse.Result.(*listTransactionsResponse).Transactions)) + transaction := publishedResponse.Result.(*listTransactionsResponse).Transactions[0] + assert.Equal(t, constants.TRANSACTION_TYPE_OUTGOING, transaction.Type) +} + +func TestHandleListTransactionsEvent_UnpaidIncomingOnly(t *testing.T) { + ctx := context.TODO() + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + require.NoError(t, err) + + const nip47ListTransactionsJson = ` +{ + "method": "list_transactions", + "params": { + "from": 0, + "until": 0, + "limit": 10, + "offset": 0, + "unpaid_incoming": true + } +} +` + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47ListTransactionsJson), nip47Request) + assert.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{ + AppId: &app.ID, + } + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + err = svc.DB.Create(&db.Transaction{ + Type: constants.TRANSACTION_TYPE_INCOMING, + State: constants.TRANSACTION_STATE_PENDING, + }).Error + assert.NoError(t, err) + + err = svc.DB.Create(&db.Transaction{ + Type: constants.TRANSACTION_TYPE_OUTGOING, + State: constants.TRANSACTION_STATE_PENDING, + }).Error + assert.NoError(t, err) + + var publishedResponse *models.Response + + publishResponse := func(response *models.Response, tags nostr.Tags) { + publishedResponse = response + } + + permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) + transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + HandleListTransactionsEvent(ctx, nip47Request, dbRequestEvent.ID, *dbRequestEvent.AppId, publishResponse) + + assert.Nil(t, publishedResponse.Error) + + assert.Equal(t, 1, len(publishedResponse.Result.(*listTransactionsResponse).Transactions)) + transaction := publishedResponse.Result.(*listTransactionsResponse).Transactions[0] + assert.Equal(t, constants.TRANSACTION_TYPE_INCOMING, transaction.Type) +} + +func TestHandleListTransactionsEvent_Unpaid(t *testing.T) { + ctx := context.TODO() + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + require.NoError(t, err) + + const nip47ListTransactionsJson = ` +{ + "method": "list_transactions", + "params": { + "from": 0, + "until": 0, + "limit": 10, + "offset": 0, + "unpaid": true + } +} +` + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47ListTransactionsJson), nip47Request) + assert.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{ + AppId: &app.ID, + } + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + err = svc.DB.Create(&db.Transaction{ + Type: constants.TRANSACTION_TYPE_INCOMING, + State: constants.TRANSACTION_STATE_PENDING, + }).Error + assert.NoError(t, err) + + err = svc.DB.Create(&db.Transaction{ + Type: constants.TRANSACTION_TYPE_OUTGOING, + State: constants.TRANSACTION_STATE_PENDING, + }).Error + assert.NoError(t, err) + + var publishedResponse *models.Response + + publishResponse := func(response *models.Response, tags nostr.Tags) { + publishedResponse = response + } + + permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) + transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + HandleListTransactionsEvent(ctx, nip47Request, dbRequestEvent.ID, *dbRequestEvent.AppId, publishResponse) + + assert.Nil(t, publishedResponse.Error) + + assert.Equal(t, 2, len(publishedResponse.Result.(*listTransactionsResponse).Transactions)) +} + +func TestHandleListTransactionsEvent_Paid(t *testing.T) { + ctx := context.TODO() + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + require.NoError(t, err) + + const nip47ListTransactionsJson = ` +{ + "method": "list_transactions", + "params": { + "from": 0, + "until": 0, + "limit": 10, + "offset": 0 + } +} +` + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47ListTransactionsJson), nip47Request) + assert.NoError(t, err) + + app, _, err := tests.CreateApp(svc) + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{ + AppId: &app.ID, + } + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + err = svc.DB.Create(&db.Transaction{ + Type: constants.TRANSACTION_TYPE_INCOMING, + State: constants.TRANSACTION_STATE_PENDING, + }).Error + assert.NoError(t, err) + + err = svc.DB.Create(&db.Transaction{ + Type: constants.TRANSACTION_TYPE_OUTGOING, + State: constants.TRANSACTION_STATE_PENDING, + }).Error + assert.NoError(t, err) + + settledPaymentHash := "dummy payment hash" + + err = svc.DB.Create(&db.Transaction{ + Type: constants.TRANSACTION_TYPE_OUTGOING, + State: constants.TRANSACTION_STATE_SETTLED, + PaymentHash: settledPaymentHash, + }).Error + assert.NoError(t, err) + + var publishedResponse *models.Response + + publishResponse := func(response *models.Response, tags nostr.Tags) { + publishedResponse = response + } + + permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) + transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + HandleListTransactionsEvent(ctx, nip47Request, dbRequestEvent.ID, *dbRequestEvent.AppId, publishResponse) + + assert.Nil(t, publishedResponse.Error) + + assert.Equal(t, 1, len(publishedResponse.Result.(*listTransactionsResponse).Transactions)) + transaction := publishedResponse.Result.(*listTransactionsResponse).Transactions[0] + assert.Equal(t, settledPaymentHash, transaction.PaymentHash) } // TODO: add tests for pagination args diff --git a/nip47/models/models.go b/nip47/models/models.go index 648cd921f..05ad61873 100644 --- a/nip47/models/models.go +++ b/nip47/models/models.go @@ -26,6 +26,7 @@ const ( type Transaction struct { Type string `json:"type"` + State string `json:"state"` Invoice string `json:"invoice"` Description string `json:"description"` DescriptionHash string `json:"description_hash"` diff --git a/nip47/models/transactions.go b/nip47/models/transactions.go index 09c77c89a..e8c85ff17 100644 --- a/nip47/models/transactions.go +++ b/nip47/models/transactions.go @@ -2,6 +2,7 @@ package models import ( "encoding/json" + "strings" "github.com/getAlby/hub/logger" "github.com/getAlby/hub/transactions" @@ -36,6 +37,7 @@ func ToNip47Transaction(transaction *transactions.Transaction) *Transaction { return &Transaction{ Type: transaction.Type, + State: strings.ToLower(transaction.State), Invoice: transaction.PaymentRequest, Description: transaction.Description, DescriptionHash: transaction.DescriptionHash,