mirror of
https://github.com/getAlby/hub.git
synced 2026-08-13 12:33:39 +02:00
feat: move events to transactions service (#510)
* feat: move events to transactions service (WIP) * fix: tests * chore: improve quota exceeded error message * fix: list transactions offset * fix: add nwc_permission_denied events in transactions service * fix: check for duplicate payment * fix: unstable test * chore: add payment settled tests * fix: tests race condition * fix: internal keysend support * chore: add tests for check unsettled transactions fallback * chore: add extra tests * chore: extra tests * fix: pass metadata in keysend self payments * fix: throw error if transaction is already marked as sent * fix: mark transaction settled * fix: consume events in alby oauth service * chore: add TODOs * fix: do not fire duplicate failed payment events * docs: add basic architecture docs to readme
This commit is contained in:
parent
2cdc20b6e0
commit
5f1b0decdf
53 changed files with 1104 additions and 377 deletions
63
README.md
63
README.md
|
|
@ -425,3 +425,66 @@ In this repository. Or manually download the docker-compose.yml file and then ru
|
|||
### Render.com
|
||||
|
||||
[](https://render.com/deploy?repo=https://github.com/getAlby/hub)
|
||||
|
||||
## Alby Hub Architecture
|
||||
|
||||
### NWC Wallet Service
|
||||
|
||||
At a high level Alby Hub is an [NWC](https://nwc.dev) wallet service which allows users to use their single wallet seamlessly within a multitude of apps(clients). Any client that supports NWC and has a valid connection secret can communicate with the wallet service to execute commands on the underlying wallet (internally called LNClient).
|
||||
|
||||
### LNClient
|
||||
|
||||
The LNClient interface abstracts the differences between wallet implementations and allows users to run Alby Hub with their preferred wallet, such as LDK, LND, Phoenixd, Cashu, Breez, Greenlight.
|
||||
|
||||
### Transactions Service
|
||||
|
||||
Alby Hub maintains its own database of transactions to enable features like self-payments for isolated app connections (subaccounts), additional metadata (that apps can provide when creating invoices or making keysend payments), and to associate transactions with apps, providing additional context to users about how their wallet is being used across apps.
|
||||
|
||||
The transactions service sits between the LNClient and two possible entry points: the NIP-47 handlers, and our internal API which is used by the Alby Hub frontend.
|
||||
|
||||
### Event Publisher
|
||||
|
||||
Internally Alby Hub uses a basic implementation of the pubsub messaging pattern which allows different parts of the system to fire or consume events. For example, the LNClients can fire events when they asynchronously receive or send a payment, which is consumed by the transaction service to update our internal transaction database, and then fire its own events which can be consumed by the NIP-47 notifier to publish notification events to subscribing apps, and also by the Alby OAuth service to send events to the Alby Account (to enable features such as encrypted static channel backups, email notifications of payments, and more).
|
||||
|
||||
#### Published Events
|
||||
|
||||
- `nwc_started` - when Alby Hub process starts
|
||||
- `nwc_stopped` - when Alby Hub process gracefully exits
|
||||
- `nwc_node_started` - when Alby Hub successfully starts or connects to the configured LNClient.
|
||||
- `nwc_node_start_failed` - The LNClient failed to sync or could not be connected to (e.g. network error, or incorrect configuration for an external node)
|
||||
- `nwc_node_stopped` the LNClient was gracefully stopped
|
||||
- `nwc_node_stop_failed` - failed to request the node to stop. Ideally this never happens.
|
||||
- `nwc_node_sync_failed` - the node failed to sync onchain, wallet or fee estimates.
|
||||
- `nwc_unlocked` - when user enters correct password (HTTP only)
|
||||
- `nwc_channel_ready` - a new channel is opened, active and ready to use
|
||||
- `nwc_channel_closed` - a channel was closed (could be co-operatively or a force closure)
|
||||
- `nwc_backup_channels` - send a list of channels that can be used as a SCB.
|
||||
- `nwc_outgoing_liquidity_required` - when user tries to pay an invoice more than their current outgoing liquidity across active channels
|
||||
- `nwc_incoming_liquidity_required` - when user tries to creates an invoice more than their current incoming liquidity across active channels
|
||||
- `nwc_permission_denied` - a NIP-47 request was denied - either due to the app connection not having permission for a certain command, or the app does not have insufficient balance or budget to make the payment.
|
||||
- `nwc_payment_failed` - failed to make a lightning payment
|
||||
- `nwc_payment_sent` - successfully made a lightning payment
|
||||
- `nwc_payment_received` - received a lightning payment
|
||||
- `nwc_lnclient_*` - underlying LNClient events, consumed only by the transactions service.
|
||||
|
||||
### NIP-47 Handlers
|
||||
|
||||
Alby Hub subscribes to a standard Nostr relay and listens for whitelisted events from known pubkeys and handles these requests in a similar way as a standard HTTP API controller, and either doing requests to the underling LNClient, or to the transactions service in the case of payments and invoices.
|
||||
|
||||
### Frontend
|
||||
|
||||
The Alby Hub frontend is a standard React app that can run in one of two modes: as an HTTP server, or desktop app, built by Wails. To abstract away, both the HTTP service and Wails handlers pass requests through to the API, where the business logic is located, for direct requests from user interactions.
|
||||
|
||||
#### Authentication
|
||||
|
||||
Alby Hub uses simple JWT auth in HTTP mode, which also allows the HTTP API to be exposed to external apps, which can use Alby Hub's API to have access to extra functionality currently not covered by the NIP-47 spec, however there are downsides - this API is not a public spec, and only works over HTTP. Therefore, apps are recommended to use NIP-47 where possible.
|
||||
|
||||
### Encryption
|
||||
|
||||
Sensitive data such as the seed phrase are saved AES-encrypted by the user's unlock password, and only decrypted in-memory in order to run the lightning node. This data is not logged and is only transferred over encrypted channels, and always requires the user's unlock password to access.
|
||||
|
||||
All requests to the wallet service are made with one of the following ways:
|
||||
|
||||
- NIP-47 - requests encrypted by NIP-04 using randomly-generated keypairs (one per app connection) and sent via websocket through the configured relay.
|
||||
- HTTP - requests encrypted by JWT and ideally HTTPS (except self-hosted, which can be protected by firewall)
|
||||
- Desktop mode - requests are made internally through the Wails router, without any kind of network traffic.
|
||||
|
|
|
|||
|
|
@ -292,7 +292,7 @@ func (svc *albyOAuthService) DrainSharedWallet(ctx context.Context, lnClient lnc
|
|||
|
||||
logger.Logger.WithField("amount", amount).WithError(err).Error("Draining Alby shared wallet funds")
|
||||
|
||||
transaction, err := transactions.NewTransactionsService(svc.db).MakeInvoice(ctx, amount, "Send shared wallet funds to Alby Hub", "", 120, nil, lnClient, nil, nil)
|
||||
transaction, err := transactions.NewTransactionsService(svc.db, svc.eventPublisher).MakeInvoice(ctx, amount, "Send shared wallet funds to Alby Hub", "", 120, nil, lnClient, nil, nil)
|
||||
if err != nil {
|
||||
logger.Logger.WithField("amount", amount).WithError(err).Error("Failed to make invoice")
|
||||
return err
|
||||
|
|
@ -460,11 +460,15 @@ func (svc *albyOAuthService) LinkAccount(ctx context.Context, lnClient lnclient.
|
|||
}
|
||||
|
||||
func (svc *albyOAuthService) ConsumeEvent(ctx context.Context, event *events.Event, globalProperties map[string]interface{}) {
|
||||
// run non-blocking
|
||||
go svc.consumeEvent(ctx, event, globalProperties)
|
||||
}
|
||||
defer func() {
|
||||
// ensure the app cannot panic if firing events to Alby API fails
|
||||
if r := recover(); r != nil {
|
||||
logger.Logger.WithField("event", event).WithField("r", r).Error("Failed to consume event in alby oauth service")
|
||||
}
|
||||
}()
|
||||
|
||||
// TODO: we should have a whitelist rather than a blacklist, so new events are not automatically sent
|
||||
|
||||
func (svc *albyOAuthService) consumeEvent(ctx context.Context, event *events.Event, globalProperties map[string]interface{}) {
|
||||
// TODO: rename this config option to be specific to the alby API
|
||||
if !svc.cfg.GetEnv().LogEvents {
|
||||
logger.Logger.WithField("event", event).Debug("Skipped sending to alby events API")
|
||||
|
|
@ -478,6 +482,11 @@ func (svc *albyOAuthService) consumeEvent(ctx context.Context, event *events.Eve
|
|||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(event.Event, "nwc_lnclient_") {
|
||||
// don't consume internal LNClient events
|
||||
return
|
||||
}
|
||||
|
||||
if event.Event == "nwc_payment_received" {
|
||||
type paymentReceivedEventProperties struct {
|
||||
PaymentHash string `json:"payment_hash"`
|
||||
|
|
@ -486,7 +495,7 @@ func (svc *albyOAuthService) consumeEvent(ctx context.Context, event *events.Eve
|
|||
event = &events.Event{
|
||||
Event: event.Event,
|
||||
Properties: &paymentReceivedEventProperties{
|
||||
PaymentHash: event.Properties.(*lnclient.Transaction).PaymentHash,
|
||||
PaymentHash: event.Properties.(*db.Transaction).PaymentHash,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -501,20 +510,20 @@ func (svc *albyOAuthService) consumeEvent(ctx context.Context, event *events.Eve
|
|||
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),
|
||||
PaymentHash: event.Properties.(*db.Transaction).PaymentHash,
|
||||
Duration: uint64(event.Properties.(*db.Transaction).SettledAt.Unix() - event.Properties.(*db.Transaction).CreatedAt.Unix()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if event.Event == "nwc_payment_failed_async" {
|
||||
paymentFailedAsyncProperties, ok := event.Properties.(*events.PaymentFailedAsyncProperties)
|
||||
if event.Event == "nwc_payment_failed" {
|
||||
transaction, ok := event.Properties.(*db.Transaction)
|
||||
if !ok {
|
||||
logger.Logger.WithField("event", event).Error("Failed to cast event")
|
||||
return
|
||||
}
|
||||
|
||||
type paymentSentEventProperties struct {
|
||||
type paymentFailedEventProperties struct {
|
||||
PaymentHash string `json:"payment_hash"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
|
@ -522,9 +531,9 @@ func (svc *albyOAuthService) consumeEvent(ctx context.Context, event *events.Eve
|
|||
// pass a new custom event with less detail
|
||||
event = &events.Event{
|
||||
Event: event.Event,
|
||||
Properties: &paymentSentEventProperties{
|
||||
PaymentHash: paymentFailedAsyncProperties.Transaction.PaymentHash,
|
||||
Reason: paymentFailedAsyncProperties.Reason,
|
||||
Properties: &paymentFailedEventProperties{
|
||||
PaymentHash: transaction.PaymentHash,
|
||||
Reason: transaction.FailureReason,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,3 +35,17 @@ const (
|
|||
// each transaction would have to have a maximum size of 10240
|
||||
// accounting for encryption and other metadata in the response, this is set to 2048 characters
|
||||
const INVOICE_METADATA_MAX_LENGTH = 2048
|
||||
|
||||
// errors used by NIP-47 and the transaction service
|
||||
const (
|
||||
ERROR_INTERNAL = "INTERNAL"
|
||||
ERROR_NOT_IMPLEMENTED = "NOT_IMPLEMENTED"
|
||||
ERROR_QUOTA_EXCEEDED = "QUOTA_EXCEEDED"
|
||||
ERROR_INSUFFICIENT_BALANCE = "INSUFFICIENT_BALANCE"
|
||||
ERROR_UNAUTHORIZED = "UNAUTHORIZED"
|
||||
ERROR_EXPIRED = "EXPIRED"
|
||||
ERROR_RESTRICTED = "RESTRICTED"
|
||||
ERROR_BAD_REQUEST = "BAD_REQUEST"
|
||||
ERROR_NOT_FOUND = "NOT_FOUND"
|
||||
ERROR_OTHER = "OTHER"
|
||||
)
|
||||
|
|
|
|||
26
db/migrations/202408191242_transaction_failure_reason.go
Normal file
26
db/migrations/202408191242_transaction_failure_reason.go
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// This migration removes old app permissions for request methods (now we use scopes)
|
||||
var _202408191242_transaction_failure_reason = &gormigrate.Migration{
|
||||
ID: "202408191242_transaction_failure_reason",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
|
||||
if err := tx.Exec(`
|
||||
ALTER TABLE transactions ADD failure_reason string;
|
||||
`).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ func Migrate(gormDB *gorm.DB) error {
|
|||
_202407201604_transactions_indexes,
|
||||
_202407262257_remove_invalid_scopes,
|
||||
_202408061737_add_boostagrams_and_use_json,
|
||||
_202408191242_transaction_failure_reason,
|
||||
})
|
||||
|
||||
return m.Migrate()
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ type Transaction struct {
|
|||
Metadata datatypes.JSON
|
||||
SelfPayment bool
|
||||
Boostagram datatypes.JSON
|
||||
FailureReason string
|
||||
}
|
||||
|
||||
type DBService interface {
|
||||
|
|
|
|||
|
|
@ -48,9 +48,8 @@ func (ep *eventPublisher) Publish(event *Event) {
|
|||
defer ep.subscriberMtx.Unlock()
|
||||
logger.Logger.WithFields(logrus.Fields{"event": event, "global": ep.globalProperties}).Debug("Publishing event")
|
||||
for _, listener := range ep.listeners {
|
||||
// events are consumed in sequence as some listeners depend on earlier consumers
|
||||
// (e.g. NIP-47 notifier depends on transactions service updating transactions)
|
||||
listener.ConsumeEvent(context.Background(), event, ep.globalProperties)
|
||||
// consume event without blocking thread
|
||||
go listener.ConsumeEvent(context.Background(), event, ep.globalProperties)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ package events
|
|||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/getAlby/hub/lnclient"
|
||||
)
|
||||
|
||||
type EventSubscriber interface {
|
||||
|
|
@ -34,8 +32,3 @@ type ChannelBackupInfo struct {
|
|||
FundingTxID string `json:"funding_tx_id"`
|
||||
FundingTxVout uint32 `json:"funding_tx_vout"`
|
||||
}
|
||||
|
||||
type PaymentFailedAsyncProperties struct {
|
||||
Transaction *lnclient.Transaction
|
||||
Reason string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1314,7 +1314,7 @@ func (ls *LDKService) handleLdkEvent(event *ldk_node.Event) {
|
|||
}
|
||||
|
||||
ls.eventPublisher.Publish(&events.Event{
|
||||
Event: "nwc_payment_received",
|
||||
Event: "nwc_lnclient_payment_received",
|
||||
Properties: transaction,
|
||||
})
|
||||
case ldk_node.EventPaymentSuccessful:
|
||||
|
|
@ -1335,7 +1335,7 @@ func (ls *LDKService) handleLdkEvent(event *ldk_node.Event) {
|
|||
}
|
||||
|
||||
ls.eventPublisher.Publish(&events.Event{
|
||||
Event: "nwc_payment_sent",
|
||||
Event: "nwc_lnclient_payment_sent",
|
||||
Properties: transaction,
|
||||
})
|
||||
case ldk_node.EventPaymentFailed:
|
||||
|
|
@ -1358,8 +1358,8 @@ func (ls *LDKService) handleLdkEvent(event *ldk_node.Event) {
|
|||
reason := ls.getPaymentFailReason(&eventType)
|
||||
|
||||
ls.eventPublisher.Publish(&events.Event{
|
||||
Event: "nwc_payment_failed_async",
|
||||
Properties: &events.PaymentFailedAsyncProperties{
|
||||
Event: "nwc_lnclient_payment_failed",
|
||||
Properties: &lnclient.PaymentFailedEventProperties{
|
||||
Transaction: transaction,
|
||||
Reason: reason,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -495,8 +495,8 @@ func NewLNDService(ctx context.Context, eventPublisher events.EventPublisher, ln
|
|||
continue
|
||||
}
|
||||
eventPublisher.Publish(&events.Event{
|
||||
Event: "nwc_payment_failed_async",
|
||||
Properties: &events.PaymentFailedAsyncProperties{
|
||||
Event: "nwc_lnclient_payment_failed",
|
||||
Properties: &lnclient.PaymentFailedEventProperties{
|
||||
Transaction: transaction,
|
||||
Reason: payment.FailureReason.String(),
|
||||
},
|
||||
|
|
@ -511,7 +511,7 @@ func NewLNDService(ctx context.Context, eventPublisher events.EventPublisher, ln
|
|||
continue
|
||||
}
|
||||
eventPublisher.Publish(&events.Event{
|
||||
Event: "nwc_payment_sent",
|
||||
Event: "nwc_lnclient_payment_sent",
|
||||
Properties: transaction,
|
||||
})
|
||||
default:
|
||||
|
|
@ -561,7 +561,7 @@ func NewLNDService(ctx context.Context, eventPublisher events.EventPublisher, ln
|
|||
}).Info("Received new invoice")
|
||||
|
||||
eventPublisher.Publish(&events.Event{
|
||||
Event: "nwc_payment_received",
|
||||
Event: "nwc_lnclient_payment_received",
|
||||
Properties: lndInvoiceToTransaction(invoice),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -171,6 +171,11 @@ type BalancesResponse struct {
|
|||
|
||||
type NetworkGraphResponse = interface{}
|
||||
|
||||
type PaymentFailedEventProperties struct {
|
||||
Transaction *Transaction
|
||||
Reason string
|
||||
}
|
||||
|
||||
// default invoice expiry in seconds (1 day)
|
||||
const DEFAULT_INVOICE_EXPIRY = 86400
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package controllers
|
|||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/getAlby/hub/constants"
|
||||
"github.com/getAlby/hub/logger"
|
||||
"github.com/getAlby/hub/nip47/models"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
|
@ -17,7 +18,7 @@ func decodeRequest(request *models.Request, methodParams interface{}) *models.Re
|
|||
return &models.Response{
|
||||
ResultType: request.Method,
|
||||
Error: &models.Error{
|
||||
Code: models.ERROR_BAD_REQUEST,
|
||||
Code: constants.ERROR_BAD_REQUEST,
|
||||
Message: err.Error(),
|
||||
}}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package controllers
|
|||
import (
|
||||
"context"
|
||||
|
||||
"github.com/getAlby/hub/constants"
|
||||
"github.com/getAlby/hub/db"
|
||||
"github.com/getAlby/hub/db/queries"
|
||||
"github.com/getAlby/hub/logger"
|
||||
|
|
@ -41,7 +42,7 @@ func (controller *nip47Controller) HandleGetBalanceEvent(ctx context.Context, ni
|
|||
publishResponse(&models.Response{
|
||||
ResultType: nip47Request.Method,
|
||||
Error: &models.Error{
|
||||
Code: models.ERROR_INTERNAL,
|
||||
Code: constants.ERROR_INTERNAL,
|
||||
Message: err.Error(),
|
||||
},
|
||||
}, nostr.Tags{})
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ func TestHandleGetBalanceEvent(t *testing.T) {
|
|||
}
|
||||
|
||||
permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
|
||||
HandleGetBalanceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)
|
||||
|
||||
|
|
@ -80,7 +80,7 @@ func TestHandleGetBalanceEvent_IsolatedApp_NoTransactions(t *testing.T) {
|
|||
}
|
||||
|
||||
permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
|
||||
HandleGetBalanceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)
|
||||
|
||||
|
|
@ -127,7 +127,7 @@ func TestHandleGetBalanceEvent_IsolatedApp_Transactions(t *testing.T) {
|
|||
}
|
||||
|
||||
permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
|
||||
HandleGetBalanceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ func (controller *nip47Controller) HandleGetInfoEvent(ctx context.Context, nip47
|
|||
publishResponse(&models.Response{
|
||||
ResultType: nip47Request.Method,
|
||||
Error: &models.Error{
|
||||
Code: models.ERROR_INTERNAL,
|
||||
Code: constants.ERROR_INTERNAL,
|
||||
Message: err.Error(),
|
||||
},
|
||||
}, nostr.Tags{})
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ func TestHandleGetInfoEvent_NoPermission(t *testing.T) {
|
|||
}
|
||||
|
||||
permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
|
||||
HandleGetInfoEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)
|
||||
|
||||
|
|
@ -102,7 +102,7 @@ func TestHandleGetInfoEvent_WithPermission(t *testing.T) {
|
|||
}
|
||||
|
||||
permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
|
||||
HandleGetInfoEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)
|
||||
|
||||
|
|
@ -158,7 +158,7 @@ func TestHandleGetInfoEvent_WithNotifications(t *testing.T) {
|
|||
}
|
||||
|
||||
permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
|
||||
HandleGetInfoEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package controllers
|
|||
import (
|
||||
"context"
|
||||
|
||||
"github.com/getAlby/hub/constants"
|
||||
"github.com/getAlby/hub/logger"
|
||||
"github.com/getAlby/hub/nip47/models"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
|
|
@ -57,7 +58,7 @@ func (controller *nip47Controller) HandleListTransactionsEvent(ctx context.Conte
|
|||
publishResponse(&models.Response{
|
||||
ResultType: nip47Request.Method,
|
||||
Error: &models.Error{
|
||||
Code: models.ERROR_INTERNAL,
|
||||
Code: constants.ERROR_INTERNAL,
|
||||
Message: err.Error(),
|
||||
},
|
||||
}, nostr.Tags{})
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ func TestHandleListTransactionsEvent(t *testing.T) {
|
|||
}
|
||||
|
||||
permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
|
||||
HandleListTransactionsEvent(ctx, nip47Request, dbRequestEvent.ID, *dbRequestEvent.AppId, publishResponse)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/getAlby/hub/constants"
|
||||
"github.com/getAlby/hub/logger"
|
||||
"github.com/getAlby/hub/nip47/models"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
|
|
@ -49,7 +50,7 @@ func (controller *nip47Controller) HandleLookupInvoiceEvent(ctx context.Context,
|
|||
publishResponse(&models.Response{
|
||||
ResultType: nip47Request.Method,
|
||||
Error: &models.Error{
|
||||
Code: models.ERROR_INTERNAL,
|
||||
Code: constants.ERROR_INTERNAL,
|
||||
Message: fmt.Sprintf("Failed to decode bolt11 invoice: %s", err.Error()),
|
||||
},
|
||||
}, nostr.Tags{})
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ func TestHandleLookupInvoiceEvent(t *testing.T) {
|
|||
}
|
||||
|
||||
permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
|
||||
HandleLookupInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, *dbRequestEvent.AppId, publishResponse)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package controllers
|
|||
import (
|
||||
"context"
|
||||
|
||||
"github.com/getAlby/hub/constants"
|
||||
"github.com/getAlby/hub/logger"
|
||||
"github.com/getAlby/hub/nip47/models"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
|
|
@ -53,7 +54,7 @@ func (controller *nip47Controller) HandleMakeInvoiceEvent(ctx context.Context, n
|
|||
publishResponse(&models.Response{
|
||||
ResultType: nip47Request.Method,
|
||||
Error: &models.Error{
|
||||
Code: models.ERROR_INTERNAL,
|
||||
Code: constants.ERROR_INTERNAL,
|
||||
Message: err.Error(),
|
||||
},
|
||||
}, nostr.Tags{})
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ func TestHandleMakeInvoiceEvent(t *testing.T) {
|
|||
}
|
||||
|
||||
permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
|
||||
HandleMakeInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, *dbRequestEvent.AppId, publishResponse)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,20 +3,21 @@ package controllers
|
|||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/getAlby/hub/constants"
|
||||
"github.com/getAlby/hub/nip47/models"
|
||||
"github.com/getAlby/hub/transactions"
|
||||
)
|
||||
|
||||
func mapNip47Error(err error) *models.Error {
|
||||
code := models.ERROR_INTERNAL
|
||||
code := constants.ERROR_INTERNAL
|
||||
if errors.Is(err, transactions.NewNotFoundError()) {
|
||||
code = models.ERROR_NOT_FOUND
|
||||
code = constants.ERROR_NOT_FOUND
|
||||
}
|
||||
if errors.Is(err, transactions.NewInsufficientBalanceError()) {
|
||||
code = models.ERROR_INSUFFICIENT_BALANCE
|
||||
code = constants.ERROR_INSUFFICIENT_BALANCE
|
||||
}
|
||||
if errors.Is(err, transactions.NewQuotaExceededError()) {
|
||||
code = models.ERROR_QUOTA_EXCEEDED
|
||||
code = constants.ERROR_QUOTA_EXCEEDED
|
||||
}
|
||||
|
||||
return &models.Error{
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/getAlby/hub/constants"
|
||||
"github.com/getAlby/hub/db"
|
||||
"github.com/getAlby/hub/logger"
|
||||
"github.com/getAlby/hub/nip47/models"
|
||||
|
|
@ -30,6 +31,7 @@ func (controller *nip47Controller) HandleMultiPayInvoiceEvent(ctx context.Contex
|
|||
publishResponse(resp, nostr.Tags{})
|
||||
return
|
||||
}
|
||||
logger.Logger.WithField("multiPayParams", multiPayParams).Debug("sending multi payment")
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(multiPayParams.Invoices))
|
||||
|
|
@ -52,7 +54,7 @@ func (controller *nip47Controller) HandleMultiPayInvoiceEvent(ctx context.Contex
|
|||
publishResponse(&models.Response{
|
||||
ResultType: nip47Request.Method,
|
||||
Error: &models.Error{
|
||||
Code: models.ERROR_INTERNAL,
|
||||
Code: constants.ERROR_INTERNAL,
|
||||
Message: fmt.Sprintf("Failed to decode bolt11 invoice: %s", err.Error()),
|
||||
},
|
||||
}, nostr.Tags{dTag})
|
||||
|
|
|
|||
|
|
@ -8,11 +8,13 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/getAlby/hub/constants"
|
||||
"github.com/getAlby/hub/db"
|
||||
"github.com/getAlby/hub/lnclient"
|
||||
"github.com/getAlby/hub/logger"
|
||||
"github.com/getAlby/hub/nip47/models"
|
||||
"github.com/getAlby/hub/nip47/permissions"
|
||||
"github.com/getAlby/hub/tests"
|
||||
|
|
@ -27,22 +29,7 @@ const nip47MultiPayJson = `
|
|||
"invoice": "lntb1230n1pjypux0pp5xgxzcks5jtx06k784f9dndjh664wc08ucrganpqn52d0ftrh9n8sdqyw3jscqzpgxqyz5vqsp5rkx7cq252p3frx8ytjpzc55rkgyx2mfkzzraa272dqvr2j6leurs9qyyssqhutxa24r5hqxstchz5fxlslawprqjnarjujp5sm3xj7ex73s32sn54fthv2aqlhp76qmvrlvxppx9skd3r5ut5xutgrup8zuc6ay73gqmra29m"
|
||||
},
|
||||
{
|
||||
"invoice": "lntb1230n1pjypux0pp5xgxzcks5jtx06k784f9dndjh664wc08ucrganpqn52d0ftrh9n8sdqyw3jscqzpgxqyz5vqsp5rkx7cq252p3frx8ytjpzc55rkgyx2mfkzzraa272dqvr2j6leurs9qyyssqhutxa24r5hqxstchz5fxlslawprqjnarjujp5sm3xj7ex73s32sn54fthv2aqlhp76qmvrlvxppx9skd3r5ut5xutgrup8zuc6ay73gqmra29m"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const nip47MultiPayOneOverflowingBudgetJson = `
|
||||
{
|
||||
"method": "multi_pay_invoice",
|
||||
"params": {
|
||||
"invoices": [{
|
||||
"invoice": "lnbcrt5u1pjuywzppp5h69dt59cypca2wxu69sw8ga0g39a3yx7dqug5nthrw3rcqgfdu4qdqqcqzzsxqyz5vqsp5gzlpzszyj2k30qmpme7jsfzr24wqlvt9xdmr7ay34lfelz050krs9qyyssq038x07nh8yuv8hdpjh5y8kqp7zcd62ql9na9xh7pla44htjyy02sz23q7qm2tza6ct4ypljk54w9k9qsrsu95usk8ce726ytep6vhhsq9mhf9a"
|
||||
},
|
||||
{
|
||||
"invoice": "lntb1230n1pjypux0pp5xgxzcks5jtx06k784f9dndjh664wc08ucrganpqn52d0ftrh9n8sdqyw3jscqzpgxqyz5vqsp5rkx7cq252p3frx8ytjpzc55rkgyx2mfkzzraa272dqvr2j6leurs9qyyssqhutxa24r5hqxstchz5fxlslawprqjnarjujp5sm3xj7ex73s32sn54fthv2aqlhp76qmvrlvxppx9skd3r5ut5xutgrup8zuc6ay73gqmra29m"
|
||||
"invoice": "lntbs1230n1pnvxqc2dqqnp4q0w0f29u6f7yrrpr5y6wj45gtnyhtch9u2m2j7qrws8eevrw90c72pp57gnea9rwqh9c62dl67akgyhuxm7dd3fgwufyuyctgx3awuv8f7cqsp56rtp7kryxssfp3lk7h79uv7n55dc4nwuvslva64caxz45ysefmeq9qyysgqcqpcxqyz5vq7trlnnrjjtfkaw3evfgqh7nxayppkvlkxa2nzhg39zs372j7hff8kht7j40hl0elh2ukhu26nzawvk3aqszdl8ppxhzsgtumemewtccq3xryqt"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -65,13 +52,22 @@ const nip47MultiPayOneMalformedInvoiceJson = `
|
|||
}
|
||||
`
|
||||
|
||||
func TestHandleMultiPayInvoiceEvent(t *testing.T) {
|
||||
func TestHandleMultiPayInvoiceEvent_Success(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
|
||||
defer tests.RemoveTestService()
|
||||
svc, err := tests.CreateTestService()
|
||||
assert.NoError(t, err)
|
||||
|
||||
var preimages = []string{"123preimage", "123preimage2"}
|
||||
|
||||
svc.LNClient.(*tests.MockLn).PayInvoiceResponses = []*lnclient.PayInvoiceResponse{{
|
||||
Preimage: preimages[0],
|
||||
}, {
|
||||
Preimage: preimages[1],
|
||||
}}
|
||||
svc.LNClient.(*tests.MockLn).PayInvoiceErrors = []error{nil, nil}
|
||||
|
||||
app, _, err := tests.CreateApp(svc)
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
|
@ -104,17 +100,30 @@ func TestHandleMultiPayInvoiceEvent(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
|
||||
permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
|
||||
HandleMultiPayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)
|
||||
|
||||
assert.Equal(t, 2, len(responses))
|
||||
for i := 0; i < len(responses); i++ {
|
||||
assert.Equal(t, "123preimage", responses[i].Result.(payResponse).Preimage)
|
||||
assert.Equal(t, tests.MockPaymentHash, dTags[i].GetFirst([]string{"d"}).Value())
|
||||
assert.Nil(t, responses[i].Error)
|
||||
var paymentHashes = []string{
|
||||
"320c2c5a1492ccfd5bc7aa4ad9b657d6aaec3cfcc0d1d98413a29af4ac772ccf",
|
||||
"f2279e946e05cb8d29bfd7bb6412fc36fcd6c52877124e130b41a3d771874fb0",
|
||||
}
|
||||
|
||||
assert.Equal(t, 2, len(responses))
|
||||
|
||||
// we can't guarantee which request was processed first
|
||||
// so swap them if they are back to front
|
||||
if dTags[0].GetFirst([]string{"d"}).Value() != paymentHashes[0] {
|
||||
responses[0], responses[1] = responses[1], responses[0]
|
||||
dTags[0], dTags[1] = dTags[1], dTags[0]
|
||||
preimages[0], preimages[1] = preimages[1], preimages[0]
|
||||
}
|
||||
|
||||
for i := 0; i < len(responses); i++ {
|
||||
assert.Equal(t, preimages[i], responses[i].Result.(payResponse).Preimage)
|
||||
assert.Equal(t, paymentHashes[i], dTags[i].GetFirst([]string{"d"}).Value())
|
||||
assert.Nil(t, responses[i].Error)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleMultiPayInvoiceEvent_OneMalformedInvoice(t *testing.T) {
|
||||
|
|
@ -155,7 +164,7 @@ func TestHandleMultiPayInvoiceEvent_OneMalformedInvoice(t *testing.T) {
|
|||
svc.DB.Save(requestEvent)
|
||||
|
||||
permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
|
||||
HandleMultiPayInvoiceEvent(ctx, nip47Request, requestEvent.ID, app, publishResponse)
|
||||
|
||||
|
|
@ -170,7 +179,7 @@ func TestHandleMultiPayInvoiceEvent_OneMalformedInvoice(t *testing.T) {
|
|||
}
|
||||
|
||||
assert.Equal(t, "invoiceId123", dTags[0].GetFirst([]string{"d"}).Value())
|
||||
assert.Equal(t, models.ERROR_INTERNAL, responses[0].Error.Code)
|
||||
assert.Equal(t, constants.ERROR_INTERNAL, responses[0].Error.Code)
|
||||
assert.Nil(t, responses[0].Result)
|
||||
|
||||
assert.Equal(t, tests.MockPaymentHash, dTags[1].GetFirst([]string{"d"}).Value())
|
||||
|
|
@ -228,7 +237,7 @@ func TestHandleMultiPayInvoiceEvent_IsolatedApp_OneBudgetExceeded(t *testing.T)
|
|||
assert.NoError(t, err)
|
||||
|
||||
permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
|
||||
HandleMultiPayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)
|
||||
|
||||
|
|
@ -242,13 +251,22 @@ func TestHandleMultiPayInvoiceEvent_IsolatedApp_OneBudgetExceeded(t *testing.T)
|
|||
dTags[0], dTags[1] = dTags[1], dTags[0]
|
||||
}
|
||||
|
||||
assert.Equal(t, "320c2c5a1492ccfd5bc7aa4ad9b657d6aaec3cfcc0d1d98413a29af4ac772ccf", dTags[0].GetFirst([]string{"d"}).Value())
|
||||
// we cannot guarantee which payment will be made first,
|
||||
// so ensure we have results for both payment hashes
|
||||
var paymentHashes = []string{
|
||||
"320c2c5a1492ccfd5bc7aa4ad9b657d6aaec3cfcc0d1d98413a29af4ac772ccf",
|
||||
"f2279e946e05cb8d29bfd7bb6412fc36fcd6c52877124e130b41a3d771874fb0",
|
||||
}
|
||||
|
||||
assert.NotEqual(t, dTags[0].GetFirst([]string{"d"}).Value(), dTags[1].GetFirst([]string{"d"}).Value())
|
||||
|
||||
assert.Contains(t, paymentHashes, dTags[0].GetFirst([]string{"d"}).Value())
|
||||
assert.Equal(t, "123preimage", responses[0].Result.(payResponse).Preimage)
|
||||
assert.Nil(t, responses[0].Error)
|
||||
|
||||
assert.Equal(t, tests.MockPaymentHash, dTags[1].GetFirst([]string{"d"}).Value())
|
||||
assert.Contains(t, paymentHashes, dTags[1].GetFirst([]string{"d"}).Value())
|
||||
assert.Nil(t, responses[1].Result)
|
||||
assert.Equal(t, models.ERROR_INSUFFICIENT_BALANCE, responses[1].Error.Code)
|
||||
assert.Equal(t, constants.ERROR_INSUFFICIENT_BALANCE, responses[1].Error.Code)
|
||||
}
|
||||
|
||||
func TestHandleMultiPayInvoiceEvent_LNClient_OnePaymentFailed(t *testing.T) {
|
||||
|
|
@ -284,6 +302,10 @@ func TestHandleMultiPayInvoiceEvent_LNClient_OnePaymentFailed(t *testing.T) {
|
|||
var mu sync.Mutex
|
||||
|
||||
publishResponse := func(response *models.Response, tags nostr.Tags) {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"response": response,
|
||||
"tags": tags,
|
||||
}).Info("Publish response")
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
responses = append(responses, response)
|
||||
|
|
@ -295,12 +317,14 @@ func TestHandleMultiPayInvoiceEvent_LNClient_OnePaymentFailed(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
|
||||
permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
|
||||
HandleMultiPayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)
|
||||
|
||||
assert.Equal(t, 2, len(responses))
|
||||
assert.Equal(t, 2, len(dTags))
|
||||
|
||||
logger.Logger.WithField("dTags", dTags).WithField("responses", responses).Info("Got responses")
|
||||
// we can't guarantee which request was processed first
|
||||
// so swap them if they are back to front
|
||||
if responses[0].Result == nil {
|
||||
|
|
@ -308,12 +332,21 @@ func TestHandleMultiPayInvoiceEvent_LNClient_OnePaymentFailed(t *testing.T) {
|
|||
dTags[0], dTags[1] = dTags[1], dTags[0]
|
||||
}
|
||||
|
||||
assert.Equal(t, "320c2c5a1492ccfd5bc7aa4ad9b657d6aaec3cfcc0d1d98413a29af4ac772ccf", dTags[0].GetFirst([]string{"d"}).Value())
|
||||
// we cannot guarantee which payment will be made first,
|
||||
// so ensure we have results for both payment hashes
|
||||
var paymentHashes = []string{
|
||||
"320c2c5a1492ccfd5bc7aa4ad9b657d6aaec3cfcc0d1d98413a29af4ac772ccf",
|
||||
"f2279e946e05cb8d29bfd7bb6412fc36fcd6c52877124e130b41a3d771874fb0",
|
||||
}
|
||||
|
||||
assert.NotEqual(t, dTags[0].GetFirst([]string{"d"}).Value(), dTags[1].GetFirst([]string{"d"}).Value())
|
||||
|
||||
assert.Contains(t, paymentHashes, dTags[0].GetFirst([]string{"d"}).Value())
|
||||
assert.Equal(t, "123preimage", responses[0].Result.(payResponse).Preimage)
|
||||
assert.Nil(t, responses[0].Error)
|
||||
|
||||
assert.Equal(t, tests.MockPaymentHash, dTags[1].GetFirst([]string{"d"}).Value())
|
||||
assert.Contains(t, paymentHashes, dTags[1].GetFirst([]string{"d"}).Value())
|
||||
assert.Nil(t, responses[1].Result)
|
||||
assert.Equal(t, models.ERROR_INTERNAL, responses[1].Error.Code)
|
||||
assert.Equal(t, constants.ERROR_INTERNAL, responses[1].Error.Code)
|
||||
assert.Equal(t, "Some error", responses[1].Error.Message)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ const nip47MultiPayKeysendJson = `
|
|||
"params": {
|
||||
"keysends": [{
|
||||
"amount": 123000,
|
||||
"pubkey": "123pubkey",
|
||||
"pubkey": "123pubkey2",
|
||||
"tlv_records": [{
|
||||
"type": 5482373484,
|
||||
"value": "fajsn341414fq"
|
||||
|
|
@ -31,7 +31,7 @@ const nip47MultiPayKeysendJson = `
|
|||
},
|
||||
{
|
||||
"amount": 123000,
|
||||
"pubkey": "123pubkey",
|
||||
"pubkey": "123pubkey2",
|
||||
"tlv_records": [{
|
||||
"type": 5482373484,
|
||||
"value": "fajsn341414fq"
|
||||
|
|
@ -48,7 +48,7 @@ const nip47MultiPayKeysendOneOverflowingBudgetJson = `
|
|||
"params": {
|
||||
"keysends": [{
|
||||
"amount": 123000,
|
||||
"pubkey": "123pubkey",
|
||||
"pubkey": "123pubkey2",
|
||||
"id": "customId",
|
||||
"tlv_records": [{
|
||||
"type": 5482373484,
|
||||
|
|
@ -68,7 +68,7 @@ const nip47MultiPayKeysendOneOverflowingBudgetJson = `
|
|||
}
|
||||
`
|
||||
|
||||
func TestHandleMultiPayKeysendEvent(t *testing.T) {
|
||||
func TestHandleMultiPayKeysendEvent_Success(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
defer tests.RemoveTestService()
|
||||
svc, err := tests.CreateTestService()
|
||||
|
|
@ -106,7 +106,7 @@ func TestHandleMultiPayKeysendEvent(t *testing.T) {
|
|||
}
|
||||
|
||||
permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
|
||||
HandleMultiPayKeysendEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)
|
||||
|
||||
|
|
@ -115,7 +115,7 @@ func TestHandleMultiPayKeysendEvent(t *testing.T) {
|
|||
assert.Equal(t, 64, len(responses[i].Result.(payResponse).Preimage))
|
||||
assert.Equal(t, uint64(1), responses[i].Result.(payResponse).FeesPaid)
|
||||
assert.Nil(t, responses[i].Error)
|
||||
assert.Equal(t, "123pubkey", dTags[i].GetFirst([]string{"d"}).Value())
|
||||
assert.Equal(t, "123pubkey2", dTags[i].GetFirst([]string{"d"}).Value())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -158,7 +158,7 @@ func TestHandleMultiPayKeysendEvent_OneBudgetExceeded(t *testing.T) {
|
|||
}
|
||||
|
||||
permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
|
||||
HandleMultiPayKeysendEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)
|
||||
|
||||
|
|
@ -175,5 +175,5 @@ func TestHandleMultiPayKeysendEvent_OneBudgetExceeded(t *testing.T) {
|
|||
assert.Equal(t, uint64(1), responses[0].Result.(payResponse).FeesPaid)
|
||||
|
||||
assert.Nil(t, responses[1].Result)
|
||||
assert.Equal(t, models.ERROR_QUOTA_EXCEEDED, responses[1].Error.Code)
|
||||
assert.Equal(t, constants.ERROR_QUOTA_EXCEEDED, responses[1].Error.Code)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/getAlby/hub/constants"
|
||||
"github.com/getAlby/hub/db"
|
||||
"github.com/getAlby/hub/events"
|
||||
"github.com/getAlby/hub/logger"
|
||||
"github.com/getAlby/hub/nip47/models"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
|
|
@ -40,7 +40,7 @@ func (controller *nip47Controller) HandlePayInvoiceEvent(ctx context.Context, ni
|
|||
publishResponse(&models.Response{
|
||||
ResultType: nip47Request.Method,
|
||||
Error: &models.Error{
|
||||
Code: models.ERROR_INTERNAL,
|
||||
Code: constants.ERROR_INTERNAL,
|
||||
Message: fmt.Sprintf("Failed to decode bolt11 invoice: %s", err.Error()),
|
||||
},
|
||||
}, tags)
|
||||
|
|
@ -64,14 +64,6 @@ func (controller *nip47Controller) pay(ctx context.Context, bolt11 string, payme
|
|||
"app_id": app.ID,
|
||||
"bolt11": bolt11,
|
||||
}).Infof("Failed to send payment: %v", err)
|
||||
controller.eventPublisher.Publish(&events.Event{
|
||||
Event: "nwc_payment_failed",
|
||||
Properties: map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"invoice": bolt11,
|
||||
"amount": paymentRequest.MSatoshi / 1000,
|
||||
},
|
||||
})
|
||||
publishResponse(&models.Response{
|
||||
ResultType: nip47Request.Method,
|
||||
Error: mapNip47Error(err),
|
||||
|
|
@ -79,14 +71,6 @@ func (controller *nip47Controller) pay(ctx context.Context, bolt11 string, payme
|
|||
return
|
||||
}
|
||||
|
||||
controller.eventPublisher.Publish(&events.Event{
|
||||
Event: "nwc_payment_succeeded",
|
||||
Properties: map[string]interface{}{
|
||||
"bolt11": bolt11,
|
||||
"amount": paymentRequest.MSatoshi / 1000,
|
||||
},
|
||||
})
|
||||
|
||||
publishResponse(&models.Response{
|
||||
ResultType: nip47Request.Method,
|
||||
Result: payResponse{
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ func TestHandlePayInvoiceEvent(t *testing.T) {
|
|||
}
|
||||
|
||||
permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
|
||||
HandlePayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse, nostr.Tags{})
|
||||
|
||||
|
|
@ -105,11 +105,11 @@ func TestHandlePayInvoiceEvent_MalformedInvoice(t *testing.T) {
|
|||
}
|
||||
|
||||
permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
|
||||
HandlePayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse, nostr.Tags{})
|
||||
|
||||
assert.Nil(t, publishedResponse.Result)
|
||||
assert.Equal(t, models.ERROR_INTERNAL, publishedResponse.Error.Code)
|
||||
assert.Equal(t, constants.ERROR_INTERNAL, publishedResponse.Error.Code)
|
||||
assert.Equal(t, "Failed to decode bolt11 invoice: bolt11 too short", publishedResponse.Error.Message)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import (
|
|||
"context"
|
||||
|
||||
"github.com/getAlby/hub/db"
|
||||
"github.com/getAlby/hub/events"
|
||||
"github.com/getAlby/hub/lnclient"
|
||||
"github.com/getAlby/hub/logger"
|
||||
"github.com/getAlby/hub/nip47/models"
|
||||
|
|
@ -43,27 +42,13 @@ func (controller *nip47Controller) payKeysend(ctx context.Context, payKeysendPar
|
|||
"appId": app.ID,
|
||||
"recipientPubkey": payKeysendParams.Pubkey,
|
||||
}).Infof("Failed to send keysend payment: %v", err)
|
||||
controller.eventPublisher.Publish(&events.Event{
|
||||
Event: "nwc_payment_failed",
|
||||
Properties: map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"keysend": true,
|
||||
"amount": payKeysendParams.Amount / 1000,
|
||||
},
|
||||
})
|
||||
publishResponse(&models.Response{
|
||||
ResultType: nip47Request.Method,
|
||||
Error: mapNip47Error(err),
|
||||
}, tags)
|
||||
return
|
||||
}
|
||||
controller.eventPublisher.Publish(&events.Event{
|
||||
Event: "nwc_payment_succeeded",
|
||||
Properties: map[string]interface{}{
|
||||
"keysend": true,
|
||||
"amount": payKeysendParams.Amount / 1000,
|
||||
},
|
||||
})
|
||||
|
||||
publishResponse(&models.Response{
|
||||
ResultType: nip47Request.Method,
|
||||
Result: payResponse{
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ const nip47KeysendJson = `
|
|||
"method": "pay_keysend",
|
||||
"params": {
|
||||
"amount": 123000,
|
||||
"pubkey": "123pubkey",
|
||||
"pubkey": "123pubkey2",
|
||||
"tlv_records": [{
|
||||
"type": 5482373484,
|
||||
"value": "fajsn341414fq"
|
||||
|
|
@ -35,7 +35,7 @@ const nip47KeysendJsonWithPreimage = `
|
|||
"method": "pay_keysend",
|
||||
"params": {
|
||||
"amount": 123000,
|
||||
"pubkey": "123pubkey",
|
||||
"pubkey": "123pubkey2",
|
||||
"preimage": "018465013e2337234a7e5530a21c4a8cf70d84231f4a8ff0b1e2cce3cb2bd03b",
|
||||
"tlv_records": [{
|
||||
"type": 5482373484,
|
||||
|
|
@ -78,7 +78,7 @@ func TestHandlePayKeysendEvent(t *testing.T) {
|
|||
}
|
||||
|
||||
permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
|
||||
HandlePayKeysendEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse, nostr.Tags{})
|
||||
|
||||
|
|
@ -119,7 +119,7 @@ func TestHandlePayKeysendEvent_WithPreimage(t *testing.T) {
|
|||
}
|
||||
|
||||
permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
|
||||
HandlePayKeysendEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse, nostr.Tags{})
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package controllers
|
|||
import (
|
||||
"context"
|
||||
|
||||
"github.com/getAlby/hub/constants"
|
||||
"github.com/getAlby/hub/logger"
|
||||
"github.com/getAlby/hub/nip47/models"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
|
|
@ -38,7 +39,7 @@ func (controller *nip47Controller) HandleSignMessageEvent(ctx context.Context, n
|
|||
publishResponse(&models.Response{
|
||||
ResultType: nip47Request.Method,
|
||||
Error: &models.Error{
|
||||
Code: models.ERROR_INTERNAL,
|
||||
Code: constants.ERROR_INTERNAL,
|
||||
Message: err.Error(),
|
||||
},
|
||||
}, nostr.Tags{})
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/getAlby/hub/constants"
|
||||
"github.com/getAlby/hub/db"
|
||||
"github.com/getAlby/hub/events"
|
||||
"github.com/getAlby/hub/lnclient"
|
||||
|
|
@ -70,7 +71,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela
|
|||
}).WithError(err).Error("Failed to save nostr event")
|
||||
nip47Response = &models.Response{
|
||||
Error: &models.Error{
|
||||
Code: models.ERROR_INTERNAL,
|
||||
Code: constants.ERROR_INTERNAL,
|
||||
Message: fmt.Sprintf("Failed to save nostr event: %s", err.Error()),
|
||||
},
|
||||
}
|
||||
|
|
@ -96,7 +97,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela
|
|||
|
||||
nip47Response = &models.Response{
|
||||
Error: &models.Error{
|
||||
Code: models.ERROR_UNAUTHORIZED,
|
||||
Code: constants.ERROR_UNAUTHORIZED,
|
||||
Message: "The public key does not have a wallet connected.",
|
||||
},
|
||||
}
|
||||
|
|
@ -128,7 +129,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela
|
|||
|
||||
nip47Response = &models.Response{
|
||||
Error: &models.Error{
|
||||
Code: models.ERROR_UNAUTHORIZED,
|
||||
Code: constants.ERROR_UNAUTHORIZED,
|
||||
Message: fmt.Sprintf("Failed to save app to nostr event: %s", err.Error()),
|
||||
},
|
||||
}
|
||||
|
|
@ -274,7 +275,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela
|
|||
publishResponse(&models.Response{
|
||||
ResultType: nip47Request.Method,
|
||||
Error: &models.Error{
|
||||
Code: models.ERROR_INTERNAL,
|
||||
Code: constants.ERROR_INTERNAL,
|
||||
Message: err.Error(),
|
||||
},
|
||||
}, nostr.Tags{})
|
||||
|
|
@ -348,7 +349,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela
|
|||
publishResponse(&models.Response{
|
||||
ResultType: nip47Request.Method,
|
||||
Error: &models.Error{
|
||||
Code: models.ERROR_NOT_IMPLEMENTED,
|
||||
Code: constants.ERROR_NOT_IMPLEMENTED,
|
||||
Message: fmt.Sprintf("Unknown method: %s", nip47Request.Method),
|
||||
},
|
||||
}, nostr.Tags{})
|
||||
|
|
|
|||
|
|
@ -21,17 +21,6 @@ const (
|
|||
MULTI_PAY_INVOICE_METHOD = "multi_pay_invoice"
|
||||
MULTI_PAY_KEYSEND_METHOD = "multi_pay_keysend"
|
||||
SIGN_MESSAGE_METHOD = "sign_message"
|
||||
|
||||
ERROR_INTERNAL = "INTERNAL"
|
||||
ERROR_NOT_IMPLEMENTED = "NOT_IMPLEMENTED"
|
||||
ERROR_QUOTA_EXCEEDED = "QUOTA_EXCEEDED"
|
||||
ERROR_INSUFFICIENT_BALANCE = "INSUFFICIENT_BALANCE"
|
||||
ERROR_UNAUTHORIZED = "UNAUTHORIZED"
|
||||
ERROR_EXPIRED = "EXPIRED"
|
||||
ERROR_RESTRICTED = "RESTRICTED"
|
||||
ERROR_BAD_REQUEST = "BAD_REQUEST"
|
||||
ERROR_NOT_FOUND = "NOT_FOUND"
|
||||
OTHER = "OTHER"
|
||||
)
|
||||
|
||||
type Transaction struct {
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ func NewNip47Service(db *gorm.DB, cfg config.Config, keys keys.Keys, eventPublis
|
|||
cfg: cfg,
|
||||
db: db,
|
||||
permissionsService: permissions.NewPermissionsService(db, eventPublisher),
|
||||
transactionsService: transactions.NewTransactionsService(db),
|
||||
transactionsService: transactions.NewTransactionsService(db, eventPublisher),
|
||||
eventPublisher: eventPublisher,
|
||||
keys: keys,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,26 +44,14 @@ func NewNip47Notifier(relay nostrmodels.Relay, db *gorm.DB, cfg config.Config, k
|
|||
}
|
||||
|
||||
func (notifier *Nip47Notifier) ConsumeEvent(ctx context.Context, event *events.Event) {
|
||||
|
||||
// TODO: should listen to transaction service events instead
|
||||
// then self-payments will also trigger NIP-47 notifications
|
||||
switch event.Event {
|
||||
case "nwc_payment_received":
|
||||
lnClientTransaction, ok := event.Properties.(*lnclient.Transaction)
|
||||
transaction, ok := event.Properties.(*db.Transaction)
|
||||
if !ok {
|
||||
logger.Logger.WithField("event", event).Error("Failed to cast event")
|
||||
return
|
||||
}
|
||||
|
||||
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).
|
||||
WithError(err).
|
||||
Error("Failed to lookup transaction by payment hash")
|
||||
return
|
||||
}
|
||||
notification := PaymentReceivedNotification{
|
||||
Transaction: *models.ToNip47Transaction(transaction),
|
||||
}
|
||||
|
|
@ -74,21 +62,12 @@ func (notifier *Nip47Notifier) ConsumeEvent(ctx context.Context, event *events.E
|
|||
}, nostr.Tags{}, transaction.AppId)
|
||||
|
||||
case "nwc_payment_sent":
|
||||
paymentSentEventProperties, ok := event.Properties.(*lnclient.Transaction)
|
||||
transaction, ok := event.Properties.(*db.Transaction)
|
||||
if !ok {
|
||||
logger.Logger.WithField("event", event).Error("Failed to cast event")
|
||||
return
|
||||
}
|
||||
|
||||
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).
|
||||
WithError(err).
|
||||
Error("Failed to lookup invoice by payment hash")
|
||||
return
|
||||
}
|
||||
notification := PaymentSentNotification{
|
||||
Transaction: *models.ToNip47Transaction(transaction),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ func TestSendNotification_PaymentReceived(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
|
||||
settledAt := time.Unix(*tests.MockLNClientTransaction.SettledAt, 0)
|
||||
err = svc.DB.Create(&db.Transaction{
|
||||
initialTransaction := db.Transaction{
|
||||
Type: constants.TRANSACTION_TYPE_INCOMING,
|
||||
PaymentRequest: tests.MockLNClientTransaction.Invoice,
|
||||
Description: tests.MockLNClientTransaction.Description,
|
||||
|
|
@ -60,17 +60,17 @@ func TestSendNotification_PaymentReceived(t *testing.T) {
|
|||
FeeMsat: uint64(tests.MockLNClientTransaction.FeesPaid),
|
||||
SettledAt: &settledAt,
|
||||
AppId: &app.ID,
|
||||
}).Error
|
||||
State: constants.TRANSACTION_STATE_SETTLED,
|
||||
}
|
||||
err = svc.DB.Create(&initialTransaction).Error
|
||||
assert.NoError(t, err)
|
||||
|
||||
nip47NotificationQueue := NewNip47NotificationQueue()
|
||||
svc.EventPublisher.RegisterSubscriber(NewMockConsumer(nip47NotificationQueue))
|
||||
|
||||
testEvent := &events.Event{
|
||||
Event: "nwc_payment_received",
|
||||
Properties: &lnclient.Transaction{
|
||||
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
|
||||
},
|
||||
Event: "nwc_payment_received",
|
||||
Properties: &initialTransaction,
|
||||
}
|
||||
|
||||
svc.EventPublisher.Publish(testEvent)
|
||||
|
|
@ -81,7 +81,7 @@ func TestSendNotification_PaymentReceived(t *testing.T) {
|
|||
relay := tests.NewMockRelay()
|
||||
|
||||
permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
|
||||
notifier := NewNip47Notifier(relay, svc.DB, svc.Cfg, svc.Keys, permissionsSvc, transactionsSvc, svc.LNClient)
|
||||
notifier.ConsumeEvent(ctx, receivedEvent)
|
||||
|
|
@ -129,7 +129,7 @@ func TestSendNotification_PaymentSent(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
|
||||
settledAt := time.Unix(*tests.MockLNClientTransaction.SettledAt, 0)
|
||||
err = svc.DB.Create(&db.Transaction{
|
||||
initialTransaction := db.Transaction{
|
||||
Type: constants.TRANSACTION_TYPE_OUTGOING,
|
||||
PaymentRequest: tests.MockLNClientTransaction.Invoice,
|
||||
Description: tests.MockLNClientTransaction.Description,
|
||||
|
|
@ -140,17 +140,16 @@ func TestSendNotification_PaymentSent(t *testing.T) {
|
|||
FeeMsat: uint64(tests.MockLNClientTransaction.FeesPaid),
|
||||
SettledAt: &settledAt,
|
||||
AppId: &app.ID,
|
||||
}).Error
|
||||
}
|
||||
err = svc.DB.Create(&initialTransaction).Error
|
||||
assert.NoError(t, err)
|
||||
|
||||
nip47NotificationQueue := NewNip47NotificationQueue()
|
||||
svc.EventPublisher.RegisterSubscriber(NewMockConsumer(nip47NotificationQueue))
|
||||
|
||||
testEvent := &events.Event{
|
||||
Event: "nwc_payment_sent",
|
||||
Properties: &lnclient.Transaction{
|
||||
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
|
||||
},
|
||||
Event: "nwc_payment_sent",
|
||||
Properties: &initialTransaction,
|
||||
}
|
||||
|
||||
svc.EventPublisher.Publish(testEvent)
|
||||
|
|
@ -161,7 +160,7 @@ func TestSendNotification_PaymentSent(t *testing.T) {
|
|||
relay := tests.NewMockRelay()
|
||||
|
||||
permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
|
||||
notifier := NewNip47Notifier(relay, svc.DB, svc.Cfg, svc.Keys, permissionsSvc, transactionsSvc, svc.LNClient)
|
||||
notifier.ConsumeEvent(ctx, receivedEvent)
|
||||
|
|
@ -221,7 +220,7 @@ func TestSendNotificationNoPermission(t *testing.T) {
|
|||
relay := tests.NewMockRelay()
|
||||
|
||||
permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB)
|
||||
transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
|
||||
notifier := NewNip47Notifier(relay, svc.DB, svc.Cfg, svc.Keys, permissionsSvc, transactionsSvc, svc.LNClient)
|
||||
notifier.ConsumeEvent(ctx, receivedEvent)
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ func (svc *permissionsService) HasPermission(app *db.App, scope string) (result
|
|||
})
|
||||
if findPermissionResult.RowsAffected == 0 {
|
||||
// No permission for this request method
|
||||
return false, models.ERROR_RESTRICTED, fmt.Sprintf("This app does not have the %s scope", scope)
|
||||
return false, constants.ERROR_RESTRICTED, fmt.Sprintf("This app does not have the %s scope", scope)
|
||||
}
|
||||
expiresAt := appPermission.ExpiresAt
|
||||
if expiresAt != nil && expiresAt.Before(time.Now()) {
|
||||
|
|
@ -55,7 +55,7 @@ func (svc *permissionsService) HasPermission(app *db.App, scope string) (result
|
|||
"pubkey": app.NostrPubkey,
|
||||
}).Info("This pubkey is expired")
|
||||
|
||||
return false, models.ERROR_EXPIRED, "This app has expired"
|
||||
return false, constants.ERROR_EXPIRED, "This app has expired"
|
||||
}
|
||||
|
||||
return true, "", ""
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ func TestHasPermission_NoPermission(t *testing.T) {
|
|||
permissionsSvc := NewPermissionsService(svc.DB, svc.EventPublisher)
|
||||
result, code, message := permissionsSvc.HasPermission(app, constants.PAY_INVOICE_SCOPE)
|
||||
assert.False(t, result)
|
||||
assert.Equal(t, models.ERROR_RESTRICTED, code)
|
||||
assert.Equal(t, constants.ERROR_RESTRICTED, code)
|
||||
assert.Equal(t, "This app does not have the pay_invoice scope", message)
|
||||
}
|
||||
|
||||
|
|
@ -50,7 +50,7 @@ func TestHasPermission_Expired(t *testing.T) {
|
|||
permissionsSvc := NewPermissionsService(svc.DB, svc.EventPublisher)
|
||||
result, code, message := permissionsSvc.HasPermission(app, constants.PAY_INVOICE_SCOPE)
|
||||
assert.False(t, result)
|
||||
assert.Equal(t, models.ERROR_EXPIRED, code)
|
||||
assert.Equal(t, constants.ERROR_EXPIRED, code)
|
||||
assert.Equal(t, "This app has expired", message)
|
||||
}
|
||||
|
||||
|
|
@ -79,7 +79,7 @@ func TestHasPermission_Expired(t *testing.T) {
|
|||
permissionsSvc := NewPermissionsService(svc.DB, svc.EventPublisher)
|
||||
result, code, message := permissionsSvc.HasPermission(app, PAY_INVOICE_SCOPE, 100*1000)
|
||||
assert.False(t, result)
|
||||
assert.Equal(t, models.ERROR_QUOTA_EXCEEDED, code)
|
||||
assert.Equal(t, constants.ERROR_QUOTA_EXCEEDED, code)
|
||||
assert.Equal(t, "Insufficient budget remaining to make payment", message)
|
||||
}*/
|
||||
|
||||
|
|
|
|||
|
|
@ -104,14 +104,11 @@ func NewService(ctx context.Context) (*service, error) {
|
|||
eventPublisher: eventPublisher,
|
||||
albyOAuthSvc: alby.NewAlbyOAuthService(gormDB, cfg, keys, eventPublisher),
|
||||
nip47Service: nip47.NewNip47Service(gormDB, cfg, keys, eventPublisher),
|
||||
transactionsService: transactions.NewTransactionsService(gormDB),
|
||||
transactionsService: transactions.NewTransactionsService(gormDB, eventPublisher),
|
||||
db: gormDB,
|
||||
keys: keys,
|
||||
}
|
||||
|
||||
// Note: order is important here: transactions service will update transactions
|
||||
// from payment events, which will then be consumed by the NIP-47 service to send notifications
|
||||
// TODO: transactions service should fire its own events
|
||||
eventPublisher.RegisterSubscriber(svc.transactionsService)
|
||||
eventPublisher.RegisterSubscriber(svc.nip47Service)
|
||||
eventPublisher.RegisterSubscriber(svc.albyOAuthSvc)
|
||||
|
|
|
|||
28
tests/mock_event_consumer.go
Normal file
28
tests/mock_event_consumer.go
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
package tests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/getAlby/hub/events"
|
||||
)
|
||||
|
||||
type mockEventConsumer struct {
|
||||
consumedEvents []*events.Event
|
||||
}
|
||||
|
||||
func NewMockEventConsumer() *mockEventConsumer {
|
||||
return &mockEventConsumer{
|
||||
consumedEvents: []*events.Event{},
|
||||
}
|
||||
}
|
||||
|
||||
func (e *mockEventConsumer) ConsumeEvent(ctx context.Context, event *events.Event, globalProperties map[string]interface{}) {
|
||||
e.consumedEvents = append(e.consumedEvents, event)
|
||||
}
|
||||
|
||||
func (e *mockEventConsumer) GetConsumeEvents() []*events.Event {
|
||||
// events are consumed async - give it a bit of time for tests
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
return e.consumedEvents
|
||||
}
|
||||
|
|
@ -57,9 +57,11 @@ var MockLNClientTransactions = []lnclient.Transaction{
|
|||
var MockLNClientTransaction = &MockLNClientTransactions[0]
|
||||
|
||||
type MockLn struct {
|
||||
PayInvoiceResponses []*lnclient.PayInvoiceResponse
|
||||
PayInvoiceErrors []error
|
||||
Pubkey string
|
||||
PayInvoiceResponses []*lnclient.PayInvoiceResponse
|
||||
PayInvoiceErrors []error
|
||||
Pubkey string
|
||||
MockTransaction *lnclient.Transaction
|
||||
SupportedNotificationTypes *[]string
|
||||
}
|
||||
|
||||
func NewMockLn() (*MockLn, error) {
|
||||
|
|
@ -99,6 +101,9 @@ func (mln *MockLn) MakeInvoice(ctx context.Context, amount int64, description st
|
|||
}
|
||||
|
||||
func (mln *MockLn) LookupInvoice(ctx context.Context, paymentHash string) (transaction *lnclient.Transaction, err error) {
|
||||
if mln.MockTransaction != nil {
|
||||
return mln.MockTransaction, nil
|
||||
}
|
||||
return MockLNClientTransaction, nil
|
||||
}
|
||||
|
||||
|
|
@ -178,6 +183,10 @@ func (mln *MockLn) GetSupportedNIP47Methods() []string {
|
|||
return []string{"pay_invoice", "pay_keysend", "get_balance", "get_info", "make_invoice", "lookup_invoice", "list_transactions", "multi_pay_invoice", "multi_pay_keysend", "sign_message"}
|
||||
}
|
||||
func (mln *MockLn) GetSupportedNIP47NotificationTypes() []string {
|
||||
if mln.SupportedNotificationTypes != nil {
|
||||
return *mln.SupportedNotificationTypes
|
||||
}
|
||||
|
||||
return []string{"payment_received", "payment_sent"}
|
||||
}
|
||||
func (mln *MockLn) GetPubkey() string {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ func TestSendPaymentSync_App_NoPermission(t *testing.T) {
|
|||
err = svc.DB.Create(&dbRequestEvent).Error
|
||||
assert.NoError(t, err)
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, &app.ID, &dbRequestEvent.ID)
|
||||
|
||||
assert.Error(t, err)
|
||||
|
|
@ -54,7 +54,7 @@ func TestSendPaymentSync_App_WithPermission(t *testing.T) {
|
|||
err = svc.DB.Create(&dbRequestEvent).Error
|
||||
assert.NoError(t, err)
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, &app.ID, &dbRequestEvent.ID)
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -88,12 +88,21 @@ func TestSendPaymentSync_App_BudgetExceeded(t *testing.T) {
|
|||
err = svc.DB.Create(&dbRequestEvent).Error
|
||||
assert.NoError(t, err)
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
mockEventConsumer := tests.NewMockEventConsumer()
|
||||
svc.EventPublisher.RegisterSubscriber(mockEventConsumer)
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
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)
|
||||
|
||||
assert.Equal(t, 1, len(mockEventConsumer.GetConsumeEvents()))
|
||||
assert.Equal(t, "nwc_permission_denied", mockEventConsumer.GetConsumeEvents()[0].Event)
|
||||
assert.Equal(t, app.Name, mockEventConsumer.GetConsumeEvents()[0].Properties.(map[string]interface{})["app_name"])
|
||||
assert.Equal(t, constants.ERROR_QUOTA_EXCEEDED, mockEventConsumer.GetConsumeEvents()[0].Properties.(map[string]interface{})["code"])
|
||||
assert.Equal(t, NewQuotaExceededError().Error(), mockEventConsumer.GetConsumeEvents()[0].Properties.(map[string]interface{})["message"])
|
||||
}
|
||||
|
||||
func TestSendPaymentSync_App_BudgetExceeded_SettledPayment(t *testing.T) {
|
||||
|
|
@ -128,7 +137,7 @@ func TestSendPaymentSync_App_BudgetExceeded_SettledPayment(t *testing.T) {
|
|||
err = svc.DB.Create(&dbRequestEvent).Error
|
||||
assert.NoError(t, err)
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, &app.ID, &dbRequestEvent.ID)
|
||||
|
||||
assert.Error(t, err)
|
||||
|
|
@ -167,7 +176,7 @@ func TestSendPaymentSync_App_BudgetExceeded_UnsettledPayment(t *testing.T) {
|
|||
err = svc.DB.Create(&dbRequestEvent).Error
|
||||
assert.NoError(t, err)
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, &app.ID, &dbRequestEvent.ID)
|
||||
|
||||
assert.Error(t, err)
|
||||
|
|
@ -207,7 +216,7 @@ func TestSendPaymentSync_App_BudgetNotExceeded_FailedPayment(t *testing.T) {
|
|||
err = svc.DB.Create(&dbRequestEvent).Error
|
||||
assert.NoError(t, err)
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, &app.ID, &dbRequestEvent.ID)
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
|
|
|||
96
transactions/check_unsettled_transaction_test.go
Normal file
96
transactions/check_unsettled_transaction_test.go
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
package transactions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/getAlby/hub/constants"
|
||||
"github.com/getAlby/hub/db"
|
||||
"github.com/getAlby/hub/lnclient"
|
||||
"github.com/getAlby/hub/tests"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCheckUnsettledTransaction(t *testing.T) {
|
||||
defer tests.RemoveTestService()
|
||||
svc, err := tests.CreateTestService()
|
||||
assert.NoError(t, err)
|
||||
|
||||
dbTransaction := db.Transaction{
|
||||
State: constants.TRANSACTION_STATE_PENDING,
|
||||
Type: constants.TRANSACTION_TYPE_OUTGOING,
|
||||
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
|
||||
AmountMsat: 123000,
|
||||
}
|
||||
svc.DB.Create(&dbTransaction)
|
||||
|
||||
mockEventConsumer := tests.NewMockEventConsumer()
|
||||
svc.EventPublisher.RegisterSubscriber(mockEventConsumer)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
settledAt := time.Now().Unix()
|
||||
svc.LNClient.(*tests.MockLn).MockTransaction = &lnclient.Transaction{
|
||||
SettledAt: &settledAt,
|
||||
Preimage: "dummy",
|
||||
}
|
||||
|
||||
// do not allow checking unsettled transactions if notifications are supported
|
||||
transactionsService.checkUnsettledTransaction(context.TODO(), &dbTransaction, svc.LNClient)
|
||||
assert.Equal(t, constants.TRANSACTION_STATE_PENDING, dbTransaction.State)
|
||||
|
||||
svc.LNClient.(*tests.MockLn).SupportedNotificationTypes = &[]string{}
|
||||
transactionsService.checkUnsettledTransaction(context.TODO(), &dbTransaction, svc.LNClient)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, dbTransaction.State)
|
||||
assert.Equal(t, 1, len(mockEventConsumer.GetConsumeEvents()))
|
||||
assert.Equal(t, "nwc_payment_sent", mockEventConsumer.GetConsumeEvents()[0].Event)
|
||||
settledTransaction := mockEventConsumer.GetConsumeEvents()[0].Properties.(*db.Transaction)
|
||||
assert.Equal(t, &dbTransaction, settledTransaction)
|
||||
}
|
||||
|
||||
func TestCheckUnsettledTransactions(t *testing.T) {
|
||||
defer tests.RemoveTestService()
|
||||
svc, err := tests.CreateTestService()
|
||||
assert.NoError(t, err)
|
||||
|
||||
dbTransaction := db.Transaction{
|
||||
State: constants.TRANSACTION_STATE_PENDING,
|
||||
Type: constants.TRANSACTION_TYPE_OUTGOING,
|
||||
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
|
||||
AmountMsat: 123000,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
svc.DB.Create(&dbTransaction)
|
||||
|
||||
mockEventConsumer := tests.NewMockEventConsumer()
|
||||
svc.EventPublisher.RegisterSubscriber(mockEventConsumer)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
settledAt := time.Now().Unix()
|
||||
|
||||
svc.LNClient.(*tests.MockLn).MockTransaction = &lnclient.Transaction{
|
||||
SettledAt: &settledAt,
|
||||
Preimage: "dummy",
|
||||
}
|
||||
|
||||
// do not allow checking unsettled transactions if notifications are supported
|
||||
transactionsService.checkUnsettledTransactions(context.TODO(), svc.LNClient)
|
||||
|
||||
svc.DB.Find(&dbTransaction, db.Transaction{
|
||||
ID: dbTransaction.ID,
|
||||
})
|
||||
assert.Equal(t, constants.TRANSACTION_STATE_PENDING, dbTransaction.State)
|
||||
|
||||
svc.LNClient.(*tests.MockLn).SupportedNotificationTypes = &[]string{}
|
||||
transactionsService.checkUnsettledTransactions(context.TODO(), svc.LNClient)
|
||||
|
||||
svc.DB.Find(&dbTransaction, db.Transaction{
|
||||
ID: dbTransaction.ID,
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, dbTransaction.State)
|
||||
assert.Equal(t, 1, len(mockEventConsumer.GetConsumeEvents()))
|
||||
assert.Equal(t, "nwc_payment_sent", mockEventConsumer.GetConsumeEvents()[0].Event)
|
||||
settledTransaction := mockEventConsumer.GetConsumeEvents()[0].Properties.(*db.Transaction)
|
||||
assert.Equal(t, dbTransaction.ID, settledTransaction.ID)
|
||||
}
|
||||
|
|
@ -34,7 +34,7 @@ func TestSendPaymentSync_IsolatedApp_NoBalance(t *testing.T) {
|
|||
err = svc.DB.Create(&dbRequestEvent).Error
|
||||
assert.NoError(t, err)
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, &app.ID, &dbRequestEvent.ID)
|
||||
|
||||
assert.Error(t, err)
|
||||
|
|
@ -73,12 +73,21 @@ func TestSendPaymentSync_IsolatedApp_BalanceInsufficient(t *testing.T) {
|
|||
AmountMsat: 132000, // invoice is 123000 msat, but we also calculate fee reserves max of(10 sats or 1%)
|
||||
})
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
mockEventConsumer := tests.NewMockEventConsumer()
|
||||
svc.EventPublisher.RegisterSubscriber(mockEventConsumer)
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
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)
|
||||
|
||||
assert.Equal(t, 1, len(mockEventConsumer.GetConsumeEvents()))
|
||||
assert.Equal(t, "nwc_permission_denied", mockEventConsumer.GetConsumeEvents()[0].Event)
|
||||
assert.Equal(t, app.Name, mockEventConsumer.GetConsumeEvents()[0].Properties.(map[string]interface{})["app_name"])
|
||||
assert.Equal(t, constants.ERROR_INSUFFICIENT_BALANCE, mockEventConsumer.GetConsumeEvents()[0].Properties.(map[string]interface{})["code"])
|
||||
assert.Equal(t, NewInsufficientBalanceError().Error(), mockEventConsumer.GetConsumeEvents()[0].Properties.(map[string]interface{})["message"])
|
||||
}
|
||||
|
||||
func TestSendPaymentSync_IsolatedApp_BalanceSufficient(t *testing.T) {
|
||||
|
|
@ -112,7 +121,7 @@ func TestSendPaymentSync_IsolatedApp_BalanceSufficient(t *testing.T) {
|
|||
AmountMsat: 133000, // invoice is 123000 msat, but we also calculate fee reserves max of(10 sats or 1%)
|
||||
})
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, &app.ID, &dbRequestEvent.ID)
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -161,7 +170,7 @@ func TestSendPaymentSync_IsolatedApp_BalanceInsufficient_OutstandingPayment(t *t
|
|||
AmountMsat: 1000,
|
||||
})
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, &app.ID, &dbRequestEvent.ID)
|
||||
|
||||
assert.Error(t, err)
|
||||
|
|
@ -207,7 +216,7 @@ func TestSendPaymentSync_IsolatedApp_BalanceInsufficient_SettledPayment(t *testi
|
|||
AmountMsat: 1000,
|
||||
})
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, &app.ID, &dbRequestEvent.ID)
|
||||
|
||||
assert.Error(t, err)
|
||||
|
|
@ -252,7 +261,7 @@ func TestSendPaymentSync_IsolatedApp_BalanceSufficient_UnrelatedPayment(t *testi
|
|||
AmountMsat: 1000,
|
||||
})
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, &app.ID, &dbRequestEvent.ID)
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -300,7 +309,7 @@ func TestSendPaymentSync_IsolatedApp_BalanceSufficient_FailedPayment(t *testing.
|
|||
AmountMsat: 1000,
|
||||
})
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, &app.ID, &dbRequestEvent.ID)
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
|
|
|||
|
|
@ -2,11 +2,14 @@ package transactions
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/getAlby/hub/constants"
|
||||
"github.com/getAlby/hub/db"
|
||||
"github.com/getAlby/hub/db/queries"
|
||||
"github.com/getAlby/hub/lnclient"
|
||||
"github.com/getAlby/hub/tests"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -19,7 +22,10 @@ func TestSendKeysend(t *testing.T) {
|
|||
svc, err := tests.CreateTestService()
|
||||
assert.NoError(t, err)
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
mockEventConsumer := tests.NewMockEventConsumer()
|
||||
svc.EventPublisher.RegisterSubscriber(mockEventConsumer)
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", nil, "", svc.LNClient, nil, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
|
@ -35,6 +41,11 @@ func TestSendKeysend(t *testing.T) {
|
|||
assert.Zero(t, transaction.FeeReserveMsat)
|
||||
assert.NotNil(t, transaction.Preimage)
|
||||
assert.Equal(t, 64, len(*transaction.Preimage))
|
||||
|
||||
assert.Equal(t, 1, len(mockEventConsumer.GetConsumeEvents()))
|
||||
assert.Equal(t, "nwc_payment_sent", mockEventConsumer.GetConsumeEvents()[0].Event)
|
||||
settledTransaction := mockEventConsumer.GetConsumeEvents()[0].Properties.(*db.Transaction)
|
||||
assert.Equal(t, transaction, settledTransaction)
|
||||
}
|
||||
func TestSendKeysend_CustomPreimage(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
|
|
@ -44,7 +55,7 @@ func TestSendKeysend_CustomPreimage(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
|
||||
customPreimage := "018465013e2337234a7e5530a21c4a8cf70d84231f4a8ff0b1e2cce3cb2bd03b"
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", nil, customPreimage, svc.LNClient, nil, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
|
@ -76,7 +87,7 @@ func TestSendKeysend_App_NoPermission(t *testing.T) {
|
|||
err = svc.DB.Create(&dbRequestEvent).Error
|
||||
assert.NoError(t, err)
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", nil, "", svc.LNClient, &app.ID, &dbRequestEvent.ID)
|
||||
|
||||
assert.Error(t, err)
|
||||
|
|
@ -106,7 +117,7 @@ func TestSendKeysend_App_WithPermission(t *testing.T) {
|
|||
err = svc.DB.Create(appPermission).Error
|
||||
assert.NoError(t, err)
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", nil, "", svc.LNClient, &app.ID, &dbRequestEvent.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
|
@ -149,11 +160,20 @@ func TestSendKeysend_App_BudgetExceeded(t *testing.T) {
|
|||
err = svc.DB.Create(appPermission).Error
|
||||
assert.NoError(t, err)
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
mockEventConsumer := tests.NewMockEventConsumer()
|
||||
svc.EventPublisher.RegisterSubscriber(mockEventConsumer)
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", nil, "", svc.LNClient, &app.ID, &dbRequestEvent.ID)
|
||||
|
||||
assert.ErrorIs(t, err, NewQuotaExceededError())
|
||||
assert.Nil(t, transaction)
|
||||
|
||||
assert.Equal(t, 1, len(mockEventConsumer.GetConsumeEvents()))
|
||||
assert.Equal(t, "nwc_permission_denied", mockEventConsumer.GetConsumeEvents()[0].Event)
|
||||
assert.Equal(t, app.Name, mockEventConsumer.GetConsumeEvents()[0].Properties.(map[string]interface{})["app_name"])
|
||||
assert.Equal(t, constants.ERROR_QUOTA_EXCEEDED, mockEventConsumer.GetConsumeEvents()[0].Properties.(map[string]interface{})["code"])
|
||||
assert.Equal(t, NewQuotaExceededError().Error(), mockEventConsumer.GetConsumeEvents()[0].Properties.(map[string]interface{})["message"])
|
||||
}
|
||||
func TestSendKeysend_App_BudgetNotExceeded(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
|
|
@ -178,7 +198,7 @@ func TestSendKeysend_App_BudgetNotExceeded(t *testing.T) {
|
|||
err = svc.DB.Create(appPermission).Error
|
||||
assert.NoError(t, err)
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", nil, "", svc.LNClient, &app.ID, &dbRequestEvent.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
|
@ -229,7 +249,7 @@ func TestSendKeysend_App_BalanceExceeded(t *testing.T) {
|
|||
AmountMsat: 10000, // invoice is 1000 msat, but we also calculate fee reserves max of(10 sats or 1%)
|
||||
})
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", nil, "", svc.LNClient, &app.ID, &dbRequestEvent.ID)
|
||||
|
||||
assert.ErrorIs(t, err, NewInsufficientBalanceError())
|
||||
|
|
@ -267,7 +287,7 @@ func TestSendKeysend_App_BalanceSufficient(t *testing.T) {
|
|||
AmountMsat: 11000, // invoice is 1000 msat, but we also calculate fee reserves max of(10 sats or 1%)
|
||||
})
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", nil, "", svc.LNClient, &app.ID, &dbRequestEvent.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
|
@ -294,7 +314,7 @@ func TestSendKeysend_TLVs(t *testing.T) {
|
|||
svc, err := tests.CreateTestService()
|
||||
assert.NoError(t, err)
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
transaction, err := transactionsService.SendKeysend(ctx, uint64(1000), "fake destination", []lnclient.TLVRecord{
|
||||
{
|
||||
Type: 7629169,
|
||||
|
|
@ -334,3 +354,174 @@ func TestSendKeysend_TLVs(t *testing.T) {
|
|||
assert.Equal(t, 64, len(*transaction.Preimage))
|
||||
assert.Zero(t, transaction.FeeReserveMsat)
|
||||
}
|
||||
|
||||
func TestSendKeysend_IsolatedAppToNoApp(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
|
||||
defer tests.RemoveTestService()
|
||||
svc, err := tests.CreateTestService()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// setup for 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, // payment 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 := "c8aeb44ae8eb269c8dbfb7ec5c263f0bfa3d755bc0ca641b8ee118673afda657"
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
transaction, err := transactionsService.SendKeysend(ctx, 123000, "02a5056398235568fc049a5d563f1adf666041d590b268167e4fa145fbf71aa578", []lnclient.TLVRecord{}, mockPreimage, svc.LNClient, &app.ID, &dbRequestEvent.ID)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, transaction)
|
||||
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, transaction.PaymentHash, &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 TestSendKeysend_IsolatedAppToIsolatedApp(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
|
||||
defer tests.RemoveTestService()
|
||||
svc, err := tests.CreateTestService()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// setup for 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, // payment 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 := "c8aeb44ae8eb269c8dbfb7ec5c263f0bfa3d755bc0ca641b8ee118673afda657"
|
||||
|
||||
// Keysend from app 1 to app 2
|
||||
tlvRecords := []lnclient.TLVRecord{
|
||||
{
|
||||
Type: 696969,
|
||||
Value: hex.EncodeToString([]byte(strconv.FormatUint(uint64(app2.ID), 10))),
|
||||
},
|
||||
{
|
||||
Type: 7629169,
|
||||
Value: "7b22616374696f6e223a22626f6f7374222c2276616c75655f6d736174223a313030302c2276616c75655f6d7361745f746f74616c223a313030302c226170705f6e616d65223a22e29aa1205765624c4e2044656d6f222c226170705f76657273696f6e223a22312e30222c22666565644944223a2268747470733a2f2f66656564732e706f6463617374696e6465782e6f72672f706332302e786d6c222c22706f6463617374223a22506f6463617374696e6720322e30222c22657069736f6465223a22457069736f6465203130343a2041204e65772044756d70222c227473223a32312c226e616d65223a22e29aa1205765624c4e2044656d6f222c2273656e6465725f6e616d65223a225361746f736869204e616b616d6f746f222c226d657373616765223a22476f20706f6463617374696e6721227d",
|
||||
},
|
||||
}
|
||||
|
||||
mockEventConsumer := tests.NewMockEventConsumer()
|
||||
svc.EventPublisher.RegisterSubscriber(mockEventConsumer)
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
transaction, err := transactionsService.SendKeysend(ctx, 123000, "02a5056398235568fc049a5d563f1adf666041d590b268167e4fa145fbf71aa578", tlvRecords, mockPreimage, svc.LNClient, &app.ID, &dbRequestEvent.ID)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, transaction)
|
||||
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, transaction.PaymentHash, &transactionType, svc.LNClient, &app2.ID)
|
||||
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)
|
||||
|
||||
// receiving app should have the same data as what was sent
|
||||
assert.Equal(t, transaction.Description, incomingTransaction.Description)
|
||||
assert.Equal(t, transaction.Metadata, incomingTransaction.Metadata)
|
||||
assert.Equal(t, transaction.Boostagram, incomingTransaction.Boostagram)
|
||||
|
||||
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))
|
||||
|
||||
// expect app2 to receive the payment
|
||||
assert.Equal(t, uint64(123000), queries.GetIsolatedBalance(svc.DB, app2.ID))
|
||||
|
||||
// check notifications
|
||||
assert.Equal(t, 2, len(mockEventConsumer.GetConsumeEvents()))
|
||||
|
||||
assert.Equal(t, "nwc_payment_sent", mockEventConsumer.GetConsumeEvents()[1].Event)
|
||||
settledTransaction := mockEventConsumer.GetConsumeEvents()[1].Properties.(*db.Transaction)
|
||||
assert.Equal(t, transaction.ID, settledTransaction.ID)
|
||||
|
||||
assert.Equal(t, "nwc_payment_received", mockEventConsumer.GetConsumeEvents()[0].Event)
|
||||
receivedTransaction := mockEventConsumer.GetConsumeEvents()[0].Properties.(*db.Transaction)
|
||||
assert.Equal(t, incomingTransaction.ID, receivedTransaction.ID)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ func TestListTransactions(t *testing.T) {
|
|||
AmountMsat: 123000,
|
||||
})
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
|
||||
incomingTransactions, err := transactionsService.ListTransactions(ctx, 0, 0, 0, 0, false, nil, svc.LNClient, nil)
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -72,7 +72,7 @@ func TestListTransactions_Unsettled(t *testing.T) {
|
|||
AmountMsat: 123000,
|
||||
})
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
|
||||
incomingTransactions, err := transactionsService.ListTransactions(ctx, 0, 0, 0, 0, true, nil, svc.LNClient, nil)
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -107,7 +107,7 @@ func TestListTransactions_Limit(t *testing.T) {
|
|||
Description: "second",
|
||||
})
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
|
||||
incomingTransactions, err := transactionsService.ListTransactions(ctx, 0, 0, 1, 0, false, nil, svc.LNClient, nil)
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -131,7 +131,7 @@ func TestListTransactions_Offset(t *testing.T) {
|
|||
Preimage: &mockPreimage,
|
||||
AmountMsat: 123000,
|
||||
Description: "first",
|
||||
CreatedAt: time.Now().Add(1 * time.Minute),
|
||||
CreatedAt: time.Now().Add(3 * time.Minute),
|
||||
})
|
||||
svc.DB.Create(&db.Transaction{
|
||||
State: constants.TRANSACTION_STATE_SETTLED,
|
||||
|
|
@ -141,14 +141,34 @@ func TestListTransactions_Offset(t *testing.T) {
|
|||
Preimage: &mockPreimage,
|
||||
AmountMsat: 123000,
|
||||
Description: "second",
|
||||
CreatedAt: time.Now().Add(2 * time.Minute),
|
||||
})
|
||||
svc.DB.Create(&db.Transaction{
|
||||
State: constants.TRANSACTION_STATE_SETTLED,
|
||||
Type: constants.TRANSACTION_TYPE_INCOMING,
|
||||
PaymentRequest: tests.MockLNClientTransaction.Invoice,
|
||||
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
|
||||
Preimage: &mockPreimage,
|
||||
AmountMsat: 123000,
|
||||
Description: "third",
|
||||
CreatedAt: time.Now().Add(1 * time.Minute),
|
||||
})
|
||||
svc.DB.Create(&db.Transaction{
|
||||
State: constants.TRANSACTION_STATE_SETTLED,
|
||||
Type: constants.TRANSACTION_TYPE_INCOMING,
|
||||
PaymentRequest: tests.MockLNClientTransaction.Invoice,
|
||||
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
|
||||
Preimage: &mockPreimage,
|
||||
AmountMsat: 123000,
|
||||
Description: "fourth",
|
||||
})
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
|
||||
incomingTransactions, err := transactionsService.ListTransactions(ctx, 0, 0, 1, 1, false, nil, svc.LNClient, nil)
|
||||
incomingTransactions, err := transactionsService.ListTransactions(ctx, 0, 0, 1, 2, false, nil, svc.LNClient, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(incomingTransactions))
|
||||
assert.Equal(t, "second", incomingTransactions[0].Description)
|
||||
assert.Equal(t, "third", incomingTransactions[0].Description)
|
||||
}
|
||||
|
||||
func TestListTransactions_FromUntil(t *testing.T) {
|
||||
|
|
@ -189,7 +209,7 @@ func TestListTransactions_FromUntil(t *testing.T) {
|
|||
Description: "third",
|
||||
})
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
|
||||
incomingTransactions, err := transactionsService.ListTransactions(ctx, uint64(time.Now().Add(4*time.Minute).Unix()), uint64(time.Now().Add(6*time.Minute).Unix()), 0, 0, false, nil, svc.LNClient, nil)
|
||||
assert.NoError(t, err)
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ func TestLookupTransaction_IncomingPayment(t *testing.T) {
|
|||
AmountMsat: 123000,
|
||||
})
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
|
||||
incomingTransaction, err := transactionsService.LookupTransaction(ctx, tests.MockLNClientTransaction.PaymentHash, nil, svc.LNClient, nil)
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -54,7 +54,7 @@ func TestLookupTransaction_OutgoingPayment(t *testing.T) {
|
|||
AmountMsat: 123000,
|
||||
})
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
|
||||
outgoingTransaction, err := transactionsService.LookupTransaction(ctx, tests.MockLNClientTransaction.PaymentHash, nil, svc.LNClient, nil)
|
||||
assert.NoError(t, err)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ func TestMakeInvoice_NoApp(t *testing.T) {
|
|||
txMetadata := make(map[string]interface{})
|
||||
txMetadata["randomkey"] = strings.Repeat("a", constants.INVOICE_METADATA_MAX_LENGTH-16) // json encoding adds 16 characters - {"randomkey":""}
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
transaction, err := transactionsService.MakeInvoice(ctx, 1234, "Hello world", "", 0, txMetadata, svc.LNClient, nil, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
|
@ -46,7 +46,7 @@ func TestMakeInvoice_MetadataTooLarge(t *testing.T) {
|
|||
metadata := make(map[string]interface{})
|
||||
metadata["randomkey"] = strings.Repeat("a", constants.INVOICE_METADATA_MAX_LENGTH-15) // json encoding adds 16 characters
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
transaction, err := transactionsService.MakeInvoice(ctx, 1234, "Hello world", "", 0, metadata, svc.LNClient, nil, nil)
|
||||
|
||||
assert.Error(t, err)
|
||||
|
|
@ -68,7 +68,7 @@ func TestMakeInvoice_App(t *testing.T) {
|
|||
err = svc.DB.Create(&dbRequestEvent).Error
|
||||
assert.NoError(t, err)
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
transaction, err := transactionsService.MakeInvoice(ctx, 1234, "Hello world", "", 0, nil, svc.LNClient, &app.ID, &dbRequestEvent.ID)
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
|
|
|||
|
|
@ -30,10 +30,10 @@ func TestNotifications_ReceivedKnownPayment(t *testing.T) {
|
|||
AmountMsat: 123000,
|
||||
})
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
|
||||
transactionsService.ConsumeEvent(ctx, &events.Event{
|
||||
Event: "nwc_payment_received",
|
||||
Event: "nwc_lnclient_payment_received",
|
||||
Properties: tests.MockLNClientTransaction,
|
||||
}, map[string]interface{}{})
|
||||
|
||||
|
|
@ -56,10 +56,10 @@ func TestNotifications_ReceivedUnknownPayment(t *testing.T) {
|
|||
svc, err := tests.CreateTestService()
|
||||
assert.NoError(t, err)
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
|
||||
transactionsService.ConsumeEvent(ctx, &events.Event{
|
||||
Event: "nwc_payment_received",
|
||||
Event: "nwc_lnclient_payment_received",
|
||||
Properties: tests.MockLNClientTransaction,
|
||||
}, map[string]interface{}{})
|
||||
|
||||
|
|
@ -83,7 +83,7 @@ func TestNotifications_ReceivedKeysend(t *testing.T) {
|
|||
svc, err := tests.CreateTestService()
|
||||
assert.NoError(t, err)
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
|
||||
metadata := map[string]interface{}{}
|
||||
|
||||
|
|
@ -108,7 +108,7 @@ func TestNotifications_ReceivedKeysend(t *testing.T) {
|
|||
}
|
||||
|
||||
transactionsService.ConsumeEvent(ctx, &events.Event{
|
||||
Event: "nwc_payment_received",
|
||||
Event: "nwc_lnclient_payment_received",
|
||||
Properties: transaction,
|
||||
}, map[string]interface{}{})
|
||||
|
||||
|
|
@ -158,10 +158,10 @@ func TestNotifications_SentKnownPayment(t *testing.T) {
|
|||
FeeReserveMsat: uint64(10000),
|
||||
})
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
|
||||
transactionsService.ConsumeEvent(ctx, &events.Event{
|
||||
Event: "nwc_payment_sent",
|
||||
Event: "nwc_lnclient_payment_sent",
|
||||
Properties: tests.MockLNClientTransaction,
|
||||
}, map[string]interface{}{})
|
||||
|
||||
|
|
@ -185,14 +185,14 @@ func TestNotifications_SentUnknownPayment(t *testing.T) {
|
|||
svc, err := tests.CreateTestService()
|
||||
assert.NoError(t, err)
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
|
||||
transactions := []db.Transaction{}
|
||||
result := svc.DB.Find(&transactions)
|
||||
assert.Equal(t, int64(0), result.RowsAffected)
|
||||
|
||||
transactionsService.ConsumeEvent(ctx, &events.Event{
|
||||
Event: "nwc_payment_sent",
|
||||
Event: "nwc_lnclient_payment_sent",
|
||||
Properties: tests.MockLNClientTransaction,
|
||||
}, map[string]interface{}{})
|
||||
|
||||
|
|
@ -218,11 +218,11 @@ func TestNotifications_FailedKnownPayment(t *testing.T) {
|
|||
FeeReserveMsat: uint64(10000),
|
||||
})
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
|
||||
transactionsService.ConsumeEvent(ctx, &events.Event{
|
||||
Event: "nwc_payment_failed_async",
|
||||
Properties: &events.PaymentFailedAsyncProperties{
|
||||
Event: "nwc_lnclient_payment_failed",
|
||||
Properties: &lnclient.PaymentFailedEventProperties{
|
||||
Transaction: tests.MockLNClientTransaction,
|
||||
Reason: "Some failure reason",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,11 +4,14 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/getAlby/hub/constants"
|
||||
"github.com/getAlby/hub/db"
|
||||
"github.com/getAlby/hub/lnclient"
|
||||
"github.com/getAlby/hub/tests"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestSendPaymentSync_NoApp(t *testing.T) {
|
||||
|
|
@ -18,7 +21,7 @@ func TestSendPaymentSync_NoApp(t *testing.T) {
|
|||
svc, err := tests.CreateTestService()
|
||||
assert.NoError(t, err)
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, nil, nil)
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -28,6 +31,169 @@ func TestSendPaymentSync_NoApp(t *testing.T) {
|
|||
assert.Equal(t, "123preimage", *transaction.Preimage)
|
||||
}
|
||||
|
||||
func TestSendPaymentSync_Duplicate(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
|
||||
defer tests.RemoveTestService()
|
||||
svc, err := tests.CreateTestService()
|
||||
assert.NoError(t, err)
|
||||
|
||||
svc.DB.Create(&db.Transaction{
|
||||
State: constants.TRANSACTION_STATE_SETTLED,
|
||||
Type: constants.TRANSACTION_TYPE_OUTGOING,
|
||||
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
|
||||
AmountMsat: 123000,
|
||||
})
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, nil, nil)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "this invoice has already been paid", err.Error())
|
||||
assert.Nil(t, transaction)
|
||||
}
|
||||
|
||||
func TestMarkSettled_Sent(t *testing.T) {
|
||||
defer tests.RemoveTestService()
|
||||
svc, err := tests.CreateTestService()
|
||||
assert.NoError(t, err)
|
||||
|
||||
dbTransaction := db.Transaction{
|
||||
State: constants.TRANSACTION_STATE_PENDING,
|
||||
Type: constants.TRANSACTION_TYPE_OUTGOING,
|
||||
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
|
||||
AmountMsat: 123000,
|
||||
}
|
||||
svc.DB.Create(&dbTransaction)
|
||||
|
||||
mockEventConsumer := tests.NewMockEventConsumer()
|
||||
svc.EventPublisher.RegisterSubscriber(mockEventConsumer)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
err = svc.DB.Transaction(func(tx *gorm.DB) error {
|
||||
_, err = transactionsService.markTransactionSettled(tx, &dbTransaction, "test", 0, false)
|
||||
return err
|
||||
})
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, dbTransaction.State)
|
||||
assert.Equal(t, 1, len(mockEventConsumer.GetConsumeEvents()))
|
||||
assert.Equal(t, "nwc_payment_sent", mockEventConsumer.GetConsumeEvents()[0].Event)
|
||||
settledTransaction := mockEventConsumer.GetConsumeEvents()[0].Properties.(*db.Transaction)
|
||||
assert.Equal(t, &dbTransaction, settledTransaction)
|
||||
}
|
||||
|
||||
func TestMarkSettled_Received(t *testing.T) {
|
||||
defer tests.RemoveTestService()
|
||||
svc, err := tests.CreateTestService()
|
||||
assert.NoError(t, err)
|
||||
|
||||
dbTransaction := db.Transaction{
|
||||
State: constants.TRANSACTION_STATE_PENDING,
|
||||
Type: constants.TRANSACTION_TYPE_INCOMING,
|
||||
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
|
||||
AmountMsat: 123000,
|
||||
}
|
||||
svc.DB.Create(&dbTransaction)
|
||||
|
||||
mockEventConsumer := tests.NewMockEventConsumer()
|
||||
svc.EventPublisher.RegisterSubscriber(mockEventConsumer)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
err = svc.DB.Transaction(func(tx *gorm.DB) error {
|
||||
_, err = transactionsService.markTransactionSettled(tx, &dbTransaction, "test", 0, false)
|
||||
return err
|
||||
})
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, dbTransaction.State)
|
||||
assert.Equal(t, 1, len(mockEventConsumer.GetConsumeEvents()))
|
||||
assert.Equal(t, "nwc_payment_received", mockEventConsumer.GetConsumeEvents()[0].Event)
|
||||
settledTransaction := mockEventConsumer.GetConsumeEvents()[0].Properties.(*db.Transaction)
|
||||
assert.Equal(t, &dbTransaction, settledTransaction)
|
||||
}
|
||||
|
||||
func TestDoNotMarkSettledTwice(t *testing.T) {
|
||||
defer tests.RemoveTestService()
|
||||
svc, err := tests.CreateTestService()
|
||||
assert.NoError(t, err)
|
||||
|
||||
settledAt := time.Now().Add(time.Duration(-1) * time.Minute)
|
||||
dbTransaction := db.Transaction{
|
||||
State: constants.TRANSACTION_STATE_SETTLED,
|
||||
Type: constants.TRANSACTION_TYPE_OUTGOING,
|
||||
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
|
||||
AmountMsat: 123000,
|
||||
SettledAt: &settledAt,
|
||||
}
|
||||
svc.DB.Create(&dbTransaction)
|
||||
|
||||
mockEventConsumer := tests.NewMockEventConsumer()
|
||||
svc.EventPublisher.RegisterSubscriber(mockEventConsumer)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
err = svc.DB.Transaction(func(tx *gorm.DB) error {
|
||||
_, err = transactionsService.markTransactionSettled(tx, &dbTransaction, "test", 0, false)
|
||||
return err
|
||||
})
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Zero(t, len(mockEventConsumer.GetConsumeEvents()))
|
||||
}
|
||||
|
||||
func TestMarkFailed(t *testing.T) {
|
||||
defer tests.RemoveTestService()
|
||||
svc, err := tests.CreateTestService()
|
||||
assert.NoError(t, err)
|
||||
|
||||
dbTransaction := db.Transaction{
|
||||
State: constants.TRANSACTION_STATE_PENDING,
|
||||
Type: constants.TRANSACTION_TYPE_OUTGOING,
|
||||
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
|
||||
AmountMsat: 123000,
|
||||
}
|
||||
svc.DB.Create(&dbTransaction)
|
||||
|
||||
mockEventConsumer := tests.NewMockEventConsumer()
|
||||
svc.EventPublisher.RegisterSubscriber(mockEventConsumer)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
err = svc.DB.Transaction(func(tx *gorm.DB) error {
|
||||
return transactionsService.markPaymentFailed(tx, &dbTransaction, "some routing error")
|
||||
})
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, constants.TRANSACTION_STATE_FAILED, dbTransaction.State)
|
||||
assert.Equal(t, 1, len(mockEventConsumer.GetConsumeEvents()))
|
||||
assert.Equal(t, "nwc_payment_failed", mockEventConsumer.GetConsumeEvents()[0].Event)
|
||||
settledTransaction := mockEventConsumer.GetConsumeEvents()[0].Properties.(*db.Transaction)
|
||||
assert.Equal(t, &dbTransaction, settledTransaction)
|
||||
assert.Equal(t, "some routing error", settledTransaction.FailureReason)
|
||||
}
|
||||
|
||||
func TestDoNotMarkFailedTwice(t *testing.T) {
|
||||
defer tests.RemoveTestService()
|
||||
svc, err := tests.CreateTestService()
|
||||
assert.NoError(t, err)
|
||||
|
||||
updatedAt := time.Now().Add(time.Duration(-1) * time.Minute)
|
||||
dbTransaction := db.Transaction{
|
||||
State: constants.TRANSACTION_STATE_FAILED,
|
||||
Type: constants.TRANSACTION_TYPE_OUTGOING,
|
||||
PaymentHash: tests.MockLNClientTransaction.PaymentHash,
|
||||
AmountMsat: 123000,
|
||||
UpdatedAt: updatedAt,
|
||||
}
|
||||
svc.DB.Create(&dbTransaction)
|
||||
|
||||
mockEventConsumer := tests.NewMockEventConsumer()
|
||||
svc.EventPublisher.RegisterSubscriber(mockEventConsumer)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
err = svc.DB.Transaction(func(tx *gorm.DB) error {
|
||||
return transactionsService.markPaymentFailed(tx, &dbTransaction, "some routing error")
|
||||
})
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, updatedAt, dbTransaction.UpdatedAt)
|
||||
assert.Zero(t, len(mockEventConsumer.GetConsumeEvents()))
|
||||
}
|
||||
|
||||
func TestSendPaymentSync_FailedRemovesFeeReserve(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
|
||||
|
|
@ -38,7 +204,10 @@ func TestSendPaymentSync_FailedRemovesFeeReserve(t *testing.T) {
|
|||
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)
|
||||
mockEventConsumer := tests.NewMockEventConsumer()
|
||||
svc.EventPublisher.RegisterSubscriber(mockEventConsumer)
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, nil, nil)
|
||||
|
||||
assert.Error(t, err)
|
||||
|
|
@ -52,6 +221,9 @@ func TestSendPaymentSync_FailedRemovesFeeReserve(t *testing.T) {
|
|||
assert.Equal(t, constants.TRANSACTION_STATE_FAILED, transaction.State)
|
||||
assert.Zero(t, transaction.FeeReserveMsat)
|
||||
assert.Nil(t, transaction.Preimage)
|
||||
|
||||
assert.Equal(t, 1, len(mockEventConsumer.GetConsumeEvents()))
|
||||
assert.Equal(t, "nwc_payment_failed", mockEventConsumer.GetConsumeEvents()[0].Event)
|
||||
}
|
||||
|
||||
func TestSendPaymentSync_PendingHasFeeReserve(t *testing.T) {
|
||||
|
|
@ -65,7 +237,7 @@ func TestSendPaymentSync_PendingHasFeeReserve(t *testing.T) {
|
|||
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)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockLNClientTransaction.Invoice, svc.LNClient, nil, nil)
|
||||
|
||||
assert.Error(t, err)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ func TestReceiveKeysendWithCustomKey(t *testing.T) {
|
|||
svc, err := tests.CreateTestService()
|
||||
assert.NoError(t, err)
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
app, _, err := tests.CreateApp(svc)
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
|
@ -48,7 +48,7 @@ func TestReceiveKeysendWithCustomKey(t *testing.T) {
|
|||
}
|
||||
|
||||
event := events.Event{
|
||||
Event: "nwc_payment_received",
|
||||
Event: "nwc_lnclient_payment_received",
|
||||
Properties: &tx,
|
||||
}
|
||||
transactionsService.ConsumeEvent(ctx, &event, map[string]interface{}{})
|
||||
|
|
@ -66,13 +66,13 @@ func TestReceiveKeysend(t *testing.T) {
|
|||
svc, err := tests.CreateTestService()
|
||||
assert.NoError(t, err)
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
_, _, err = tests.CreateApp(svc)
|
||||
assert.NoError(t, err)
|
||||
|
||||
tx := tests.MockLNClientTransaction
|
||||
event := events.Event{
|
||||
Event: "nwc_payment_received",
|
||||
Event: "nwc_lnclient_payment_received",
|
||||
Properties: tx,
|
||||
}
|
||||
transactionsService.ConsumeEvent(ctx, &event, map[string]interface{}{})
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ func TestSendPaymentSync_SelfPayment_NoAppToNoApp(t *testing.T) {
|
|||
AmountMsat: 123000,
|
||||
})
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockInvoice, svc.LNClient, nil, nil)
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -83,7 +83,7 @@ func TestSendPaymentSync_SelfPayment_NoAppToIsolatedApp(t *testing.T) {
|
|||
AppId: &app.ID,
|
||||
})
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockInvoice, svc.LNClient, nil, nil)
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -136,7 +136,7 @@ func TestSendPaymentSync_SelfPayment_NoAppToApp(t *testing.T) {
|
|||
AppId: &app.ID,
|
||||
})
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockInvoice, svc.LNClient, nil, nil)
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -205,7 +205,7 @@ func TestSendPaymentSync_SelfPayment_IsolatedAppToNoApp(t *testing.T) {
|
|||
AmountMsat: 123000,
|
||||
})
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockInvoice, svc.LNClient, &app.ID, &dbRequestEvent.ID)
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -280,7 +280,7 @@ func TestSendPaymentSync_SelfPayment_IsolatedAppToApp(t *testing.T) {
|
|||
AppId: &app2.ID,
|
||||
})
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockInvoice, svc.LNClient, &app.ID, &dbRequestEvent.ID)
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -359,7 +359,10 @@ func TestSendPaymentSync_SelfPayment_IsolatedAppToIsolatedApp(t *testing.T) {
|
|||
AppId: &app2.ID,
|
||||
})
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
mockEventConsumer := tests.NewMockEventConsumer()
|
||||
svc.EventPublisher.RegisterSubscriber(mockEventConsumer)
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockInvoice, svc.LNClient, &app.ID, &dbRequestEvent.ID)
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -384,6 +387,17 @@ func TestSendPaymentSync_SelfPayment_IsolatedAppToIsolatedApp(t *testing.T) {
|
|||
assert.Equal(t, int64(3), result.RowsAffected)
|
||||
// expect balance to be decreased
|
||||
assert.Equal(t, uint64(10000), queries.GetIsolatedBalance(svc.DB, app.ID))
|
||||
|
||||
// check notifications
|
||||
assert.Equal(t, 2, len(mockEventConsumer.GetConsumeEvents()))
|
||||
|
||||
assert.Equal(t, "nwc_payment_sent", mockEventConsumer.GetConsumeEvents()[1].Event)
|
||||
settledTransaction := mockEventConsumer.GetConsumeEvents()[1].Properties.(*db.Transaction)
|
||||
assert.Equal(t, transaction.ID, settledTransaction.ID)
|
||||
|
||||
assert.Equal(t, "nwc_payment_received", mockEventConsumer.GetConsumeEvents()[0].Event)
|
||||
receivedTransaction := mockEventConsumer.GetConsumeEvents()[0].Properties.(*db.Transaction)
|
||||
assert.Equal(t, incomingTransaction.ID, receivedTransaction.ID)
|
||||
}
|
||||
|
||||
func TestSendPaymentSync_SelfPayment_IsolatedAppToSelf(t *testing.T) {
|
||||
|
|
@ -433,7 +447,7 @@ func TestSendPaymentSync_SelfPayment_IsolatedAppToSelf(t *testing.T) {
|
|||
AppId: &app.ID,
|
||||
})
|
||||
|
||||
transactionsService := NewTransactionsService(svc.DB)
|
||||
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
|
||||
transaction, err := transactionsService.SendPaymentSync(ctx, tests.MockInvoice, svc.LNClient, &app.ID, &dbRequestEvent.ID)
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
|
|
|||
|
|
@ -27,7 +27,8 @@ import (
|
|||
)
|
||||
|
||||
type transactionsService struct {
|
||||
db *gorm.DB
|
||||
db *gorm.DB
|
||||
eventPublisher events.EventPublisher
|
||||
}
|
||||
|
||||
type TransactionsService interface {
|
||||
|
|
@ -94,12 +95,13 @@ func NewQuotaExceededError() error {
|
|||
}
|
||||
|
||||
func (err *quotaExceededError) Error() string {
|
||||
return "Your wallet has exceeded its spending quota"
|
||||
return "Your app does not have enough budget remaining to make this payment. Please review this app in the connections page of your Alby Hub."
|
||||
}
|
||||
|
||||
func NewTransactionsService(db *gorm.DB) *transactionsService {
|
||||
func NewTransactionsService(db *gorm.DB, eventPublisher events.EventPublisher) *transactionsService {
|
||||
return &transactionsService{
|
||||
db: db,
|
||||
db: db,
|
||||
eventPublisher: eventPublisher,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -172,6 +174,16 @@ func (svc *transactionsService) SendPaymentSync(ctx context.Context, payReq stri
|
|||
var dbTransaction db.Transaction
|
||||
|
||||
err = svc.db.Transaction(func(tx *gorm.DB) error {
|
||||
var existingSettledTransaction db.Transaction
|
||||
if tx.Limit(1).Find(&existingSettledTransaction, &db.Transaction{
|
||||
Type: constants.TRANSACTION_TYPE_OUTGOING,
|
||||
PaymentHash: paymentRequest.PaymentHash,
|
||||
State: constants.TRANSACTION_STATE_SETTLED,
|
||||
}).RowsAffected > 0 {
|
||||
logger.Logger.WithField("payment_hash", dbTransaction.PaymentHash).Info("this invoice has already been paid")
|
||||
return errors.New("this invoice has already been paid")
|
||||
}
|
||||
|
||||
err := svc.validateCanPay(tx, appId, uint64(paymentRequest.MSatoshi))
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -230,36 +242,24 @@ func (svc *transactionsService) SendPaymentSync(ctx context.Context, payReq stri
|
|||
}
|
||||
|
||||
// As the LNClient did not return a timeout error, we assume the payment definitely failed
|
||||
dbErr := svc.db.Model(&dbTransaction).Updates(map[string]interface{}{
|
||||
"State": constants.TRANSACTION_STATE_FAILED,
|
||||
"FeeReserveMsat": 0,
|
||||
}).Error
|
||||
if dbErr != nil {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"bolt11": payReq,
|
||||
}).WithError(dbErr).Error("Failed to update DB transaction")
|
||||
}
|
||||
svc.db.Transaction(func(tx *gorm.DB) error {
|
||||
return svc.markPaymentFailed(tx, &dbTransaction, err.Error())
|
||||
})
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// the payment definitely succeeded
|
||||
now := time.Now()
|
||||
dbErr := svc.db.Model(&dbTransaction).Updates(map[string]interface{}{
|
||||
"State": constants.TRANSACTION_STATE_SETTLED,
|
||||
"Preimage": &response.Preimage,
|
||||
"FeeMsat": response.Fee,
|
||||
"FeeReserveMsat": 0,
|
||||
"SettledAt": &now,
|
||||
}).Error
|
||||
if dbErr != nil {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"bolt11": payReq,
|
||||
}).WithError(dbErr).Error("Failed to update DB transaction")
|
||||
var settledTransaction *db.Transaction
|
||||
err = svc.db.Transaction(func(tx *gorm.DB) error {
|
||||
settledTransaction, err = svc.markTransactionSettled(tx, &dbTransaction, response.Preimage, response.Fee, selfPayment)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: check the fields are updated here
|
||||
return &dbTransaction, nil
|
||||
return settledTransaction, nil
|
||||
}
|
||||
|
||||
func (svc *transactionsService) SendKeysend(ctx context.Context, amount uint64, destination string, customRecords []lnclient.TLVRecord, preimage string, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error) {
|
||||
|
|
@ -299,6 +299,8 @@ func (svc *transactionsService) SendKeysend(ctx context.Context, amount uint64,
|
|||
|
||||
var dbTransaction db.Transaction
|
||||
|
||||
selfPayment := destination == lnClient.GetPubkey()
|
||||
|
||||
err = svc.db.Transaction(func(tx *gorm.DB) error {
|
||||
err := svc.validateCanPay(tx, appId, amount)
|
||||
if err != nil {
|
||||
|
|
@ -317,6 +319,7 @@ func (svc *transactionsService) SendKeysend(ctx context.Context, amount uint64,
|
|||
Boostagram: datatypes.JSON(boostagramBytes),
|
||||
PaymentHash: paymentHash,
|
||||
Preimage: &preimage,
|
||||
SelfPayment: selfPayment,
|
||||
}
|
||||
err = tx.Create(&dbTransaction).Error
|
||||
|
||||
|
|
@ -331,7 +334,39 @@ func (svc *transactionsService) SendKeysend(ctx context.Context, amount uint64,
|
|||
return nil, err
|
||||
}
|
||||
|
||||
payKeysendResponse, err := lnClient.SendKeysend(ctx, amount, destination, customRecords, preimage)
|
||||
var payKeysendResponse *lnclient.PayKeysendResponse
|
||||
|
||||
if selfPayment {
|
||||
// for keysend self-payments we need to create an incoming payment at the time of the payment
|
||||
recipientAppId := svc.getAppIdFromCustomRecords(customRecords)
|
||||
dbTransaction := db.Transaction{
|
||||
AppId: recipientAppId,
|
||||
RequestEventId: nil, // it is related to this request but for a different app
|
||||
Type: constants.TRANSACTION_TYPE_INCOMING,
|
||||
State: constants.TRANSACTION_STATE_PENDING,
|
||||
AmountMsat: amount,
|
||||
PaymentHash: paymentHash,
|
||||
Preimage: &preimage,
|
||||
Description: svc.getDescriptionFromCustomRecords(customRecords),
|
||||
Metadata: datatypes.JSON(metadataBytes),
|
||||
Boostagram: datatypes.JSON(boostagramBytes),
|
||||
SelfPayment: true,
|
||||
}
|
||||
err = svc.db.Create(&dbTransaction).Error
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to create DB transaction")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = svc.interceptSelfPayment(paymentHash)
|
||||
if err == nil {
|
||||
payKeysendResponse = &lnclient.PayKeysendResponse{
|
||||
Fee: 0,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
payKeysendResponse, err = lnClient.SendKeysend(ctx, amount, destination, customRecords, preimage)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
|
|
@ -377,22 +412,17 @@ func (svc *transactionsService) SendKeysend(ctx context.Context, amount uint64,
|
|||
}
|
||||
|
||||
// the payment definitely succeeded
|
||||
now := time.Now()
|
||||
dbErr := svc.db.Model(&dbTransaction).Updates(map[string]interface{}{
|
||||
"State": constants.TRANSACTION_STATE_SETTLED,
|
||||
"FeeMsat": &payKeysendResponse.Fee,
|
||||
"FeeReserveMsat": 0,
|
||||
"SettledAt": &now,
|
||||
}).Error
|
||||
if dbErr != nil {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"destination": destination,
|
||||
"amount": amount,
|
||||
}).WithError(dbErr).Error("Failed to update DB transaction")
|
||||
var settledTransaction *db.Transaction
|
||||
err = svc.db.Transaction(func(tx *gorm.DB) error {
|
||||
settledTransaction, err = svc.markTransactionSettled(tx, &dbTransaction, preimage, payKeysendResponse.Fee, selfPayment)
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: check the fields are updated here
|
||||
return &dbTransaction, nil
|
||||
return settledTransaction, nil
|
||||
}
|
||||
|
||||
func (svc *transactionsService) LookupTransaction(ctx context.Context, paymentHash string, transactionType *string, lnClient lnclient.LNClient, appId *uint) (*Transaction, error) {
|
||||
|
|
@ -419,13 +449,13 @@ func (svc *transactionsService) LookupTransaction(ctx context.Context, paymentHa
|
|||
|
||||
// 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{
|
||||
result := tx.Order("settled_at desc, created_at desc").Limit(1).Find(&transaction, &db.Transaction{
|
||||
//Type: transactionType,
|
||||
PaymentHash: paymentHash,
|
||||
})
|
||||
|
||||
if result.Error != nil {
|
||||
logger.Logger.WithError(result.Error).Error("Failed to lookup DB transaction")
|
||||
logger.Logger.WithError(result.Error).Error("Failed to lookup transaction")
|
||||
return nil, result.Error
|
||||
}
|
||||
|
||||
|
|
@ -433,7 +463,7 @@ func (svc *transactionsService) LookupTransaction(ctx context.Context, paymentHa
|
|||
logger.Logger.WithFields(logrus.Fields{
|
||||
"payment_hash": paymentHash,
|
||||
"app_id": appId,
|
||||
}).WithError(result.Error).Error("Failed to lookup DB transaction")
|
||||
}).WithError(result.Error).Error("transaction not found")
|
||||
return nil, NewNotFoundError()
|
||||
}
|
||||
|
||||
|
|
@ -471,7 +501,7 @@ func (svc *transactionsService) ListTransactions(ctx context.Context, from, unti
|
|||
tx = tx.Limit(int(limit))
|
||||
}
|
||||
if offset > 0 {
|
||||
tx = tx.Offset(int(limit))
|
||||
tx = tx.Offset(int(offset))
|
||||
}
|
||||
|
||||
if appId != nil {
|
||||
|
|
@ -531,26 +561,20 @@ func (svc *transactionsService) checkUnsettledTransaction(ctx context.Context, t
|
|||
}
|
||||
// update transaction state
|
||||
if lnClientTransaction.SettledAt != nil {
|
||||
// the payment definitely succeeded
|
||||
now := time.Now()
|
||||
dbErr := svc.db.Model(transaction).Updates(map[string]interface{}{
|
||||
"State": constants.TRANSACTION_STATE_SETTLED,
|
||||
"Preimage": &lnClientTransaction.Preimage,
|
||||
"FeeMsat": lnClientTransaction.FeesPaid,
|
||||
"FeeReserveMsat": 0,
|
||||
"SettledAt": &now,
|
||||
}).Error
|
||||
if dbErr != nil {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"bolt11": transaction.PaymentRequest,
|
||||
}).WithError(dbErr).Error("Failed to update DB transaction")
|
||||
err = svc.db.Transaction(func(tx *gorm.DB) error {
|
||||
_, err = svc.markTransactionSettled(tx, transaction, lnClientTransaction.Preimage, uint64(lnClientTransaction.FeesPaid), false)
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to mark payment sent when checking unsettled transaction")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *transactionsService) ConsumeEvent(ctx context.Context, event *events.Event, globalProperties map[string]interface{}) {
|
||||
switch event.Event {
|
||||
case "nwc_payment_received":
|
||||
case "nwc_lnclient_payment_received":
|
||||
lnClientTransaction, ok := event.Properties.(*lnclient.Transaction)
|
||||
if !ok {
|
||||
logger.Logger.WithField("event", event).Error("Failed to cast event")
|
||||
|
|
@ -614,22 +638,8 @@ func (svc *transactionsService) ConsumeEvent(ctx context.Context, event *events.
|
|||
}
|
||||
}
|
||||
|
||||
settledAt := time.Now()
|
||||
|
||||
err := tx.Model(&dbTransaction).Updates(map[string]interface{}{
|
||||
"FeeMsat": lnClientTransaction.FeesPaid,
|
||||
"Preimage": &lnClientTransaction.Preimage,
|
||||
"State": constants.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
|
||||
}
|
||||
|
||||
return nil
|
||||
_, err := svc.markTransactionSettled(tx, &dbTransaction, lnClientTransaction.Preimage, uint64(lnClientTransaction.FeesPaid), false)
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
|
|
@ -638,8 +648,7 @@ func (svc *transactionsService) ConsumeEvent(ctx context.Context, event *events.
|
|||
}).WithError(err).Error("Failed to execute DB transaction")
|
||||
return
|
||||
}
|
||||
logger.Logger.WithField("payment_hash", dbTransaction.PaymentHash).Info("Marked incoming transaction as settled")
|
||||
case "nwc_payment_sent":
|
||||
case "nwc_lnclient_payment_sent":
|
||||
lnClientTransaction, ok := event.Properties.(*lnclient.Transaction)
|
||||
if !ok {
|
||||
logger.Logger.WithField("event", event).Error("Failed to cast event")
|
||||
|
|
@ -653,6 +662,10 @@ func (svc *transactionsService) ConsumeEvent(ctx context.Context, event *events.
|
|||
PaymentHash: lnClientTransaction.PaymentHash,
|
||||
})
|
||||
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
|
||||
if result.RowsAffected == 0 {
|
||||
// Note: payments made from outside cannot be associated with an app
|
||||
// for now this is disabled as it only applies to LND, and we do not import LND transactions either.
|
||||
|
|
@ -660,14 +673,7 @@ func (svc *transactionsService) ConsumeEvent(ctx context.Context, event *events.
|
|||
return NewNotFoundError()
|
||||
}
|
||||
|
||||
settledAt := time.Now()
|
||||
err := tx.Model(&dbTransaction).Updates(map[string]interface{}{
|
||||
"FeeMsat": lnClientTransaction.FeesPaid,
|
||||
"FeeReserveMsat": 0,
|
||||
"Preimage": &lnClientTransaction.Preimage,
|
||||
"State": constants.TRANSACTION_STATE_SETTLED,
|
||||
"SettledAt": &settledAt,
|
||||
}).Error
|
||||
_, err := svc.markTransactionSettled(tx, &dbTransaction, lnClientTransaction.Preimage, uint64(lnClientTransaction.FeesPaid), false)
|
||||
return err
|
||||
})
|
||||
|
||||
|
|
@ -677,10 +683,8 @@ func (svc *transactionsService) ConsumeEvent(ctx context.Context, event *events.
|
|||
}).WithError(err).Error("Failed to update transaction")
|
||||
return
|
||||
}
|
||||
logger.Logger.WithField("payment_hash", dbTransaction.PaymentHash).Info("Marked outgoing transaction as settled")
|
||||
|
||||
case "nwc_payment_failed_async":
|
||||
paymentFailedAsyncProperties, ok := event.Properties.(*events.PaymentFailedAsyncProperties)
|
||||
case "nwc_lnclient_payment_failed":
|
||||
paymentFailedAsyncProperties, ok := event.Properties.(*lnclient.PaymentFailedEventProperties)
|
||||
if !ok {
|
||||
logger.Logger.WithField("event", event).Error("Failed to cast event")
|
||||
return
|
||||
|
|
@ -694,29 +698,19 @@ func (svc *transactionsService) ConsumeEvent(ctx context.Context, event *events.
|
|||
PaymentHash: lnClientTransaction.PaymentHash,
|
||||
})
|
||||
|
||||
// Note: this will happen for keysend payments since our transaction entry will not have a payment
|
||||
// hash at this point
|
||||
if result.RowsAffected == 0 {
|
||||
logger.Logger.WithField("event", event).Error("Failed to find outgoing transaction by payment hash")
|
||||
return
|
||||
}
|
||||
|
||||
err := svc.db.Model(&dbTransaction).Updates(map[string]interface{}{
|
||||
"State": constants.TRANSACTION_STATE_FAILED,
|
||||
"FeeReserveMsat": 0,
|
||||
}).Error
|
||||
if err != nil {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"payment_hash": lnClientTransaction.PaymentHash,
|
||||
}).WithError(err).Error("Failed to update transaction")
|
||||
return
|
||||
}
|
||||
logger.Logger.WithField("payment_hash", dbTransaction.PaymentHash).Info("Marked outgoing transaction as failed")
|
||||
svc.db.Transaction(func(tx *gorm.DB) error {
|
||||
return svc.markPaymentFailed(tx, &dbTransaction, paymentFailedAsyncProperties.Reason)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *transactionsService) interceptSelfPayment(paymentHash string) (*lnclient.PayInvoiceResponse, error) {
|
||||
// TODO: extract into separate function
|
||||
logger.Logger.WithField("payment_hash", paymentHash).Debug("Intercepting self payment")
|
||||
incomingTransaction := db.Transaction{}
|
||||
result := svc.db.Limit(1).Find(&incomingTransaction, &db.Transaction{
|
||||
Type: constants.TRANSACTION_TYPE_INCOMING,
|
||||
|
|
@ -734,19 +728,15 @@ func (svc *transactionsService) interceptSelfPayment(paymentHash string) (*lncli
|
|||
return nil, errors.New("preimage is not set on transaction. Self payments not supported")
|
||||
}
|
||||
|
||||
// update the incoming transaction
|
||||
now := time.Now()
|
||||
err := svc.db.Model(&incomingTransaction).Updates(map[string]interface{}{
|
||||
"State": constants.TRANSACTION_STATE_SETTLED,
|
||||
"SettledAt": &now,
|
||||
"SelfPayment": true,
|
||||
}).Error
|
||||
err := svc.db.Transaction(func(tx *gorm.DB) error {
|
||||
_, err := svc.markTransactionSettled(tx, &incomingTransaction, *incomingTransaction.Preimage, uint64(0), true)
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: publish event for self payment
|
||||
|
||||
return &lnclient.PayInvoiceResponse{
|
||||
Preimage: *incomingTransaction.Preimage,
|
||||
Fee: 0,
|
||||
|
|
@ -779,6 +769,14 @@ func (svc *transactionsService) validateCanPay(tx *gorm.DB, appId *uint, amount
|
|||
balance := queries.GetIsolatedBalance(tx, appPermission.AppId)
|
||||
|
||||
if amountWithFeeReserve > balance {
|
||||
svc.eventPublisher.Publish(&events.Event{
|
||||
Event: "nwc_permission_denied",
|
||||
Properties: map[string]interface{}{
|
||||
"app_name": app.Name,
|
||||
"code": constants.ERROR_INSUFFICIENT_BALANCE,
|
||||
"message": NewInsufficientBalanceError().Error(),
|
||||
},
|
||||
})
|
||||
return NewInsufficientBalanceError()
|
||||
}
|
||||
}
|
||||
|
|
@ -786,6 +784,14 @@ func (svc *transactionsService) validateCanPay(tx *gorm.DB, appId *uint, amount
|
|||
if appPermission.MaxAmountSat > 0 {
|
||||
budgetUsageSat := queries.GetBudgetUsageSat(tx, &appPermission)
|
||||
if int(amountWithFeeReserve/1000) > appPermission.MaxAmountSat-int(budgetUsageSat) {
|
||||
svc.eventPublisher.Publish(&events.Event{
|
||||
Event: "nwc_permission_denied",
|
||||
Properties: map[string]interface{}{
|
||||
"app_name": app.Name,
|
||||
"code": constants.ERROR_QUOTA_EXCEEDED,
|
||||
"message": NewQuotaExceededError().Error(),
|
||||
},
|
||||
})
|
||||
return NewQuotaExceededError()
|
||||
}
|
||||
}
|
||||
|
|
@ -877,3 +883,89 @@ func (svc *transactionsService) getAppIdFromCustomRecords(customRecords []lnclie
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *transactionsService) markTransactionSettled(tx *gorm.DB, dbTransaction *db.Transaction, preimage string, fee uint64, selfPayment bool) (*db.Transaction, error) {
|
||||
// TODO: it would be better to have a database constraint so we cannot have two pending payments
|
||||
var existingSettledTransaction db.Transaction
|
||||
if tx.Limit(1).Find(&existingSettledTransaction, &db.Transaction{
|
||||
Type: dbTransaction.Type,
|
||||
PaymentHash: dbTransaction.PaymentHash,
|
||||
State: constants.TRANSACTION_STATE_SETTLED,
|
||||
}).RowsAffected > 0 {
|
||||
logger.Logger.WithField("payment_hash", dbTransaction.PaymentHash).Error("payment already marked as sent")
|
||||
return &existingSettledTransaction, nil
|
||||
}
|
||||
|
||||
if preimage == "" {
|
||||
return nil, errors.New("no preimage in payment")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
err := tx.Model(dbTransaction).Updates(map[string]interface{}{
|
||||
"State": constants.TRANSACTION_STATE_SETTLED,
|
||||
"Preimage": &preimage,
|
||||
"FeeMsat": fee,
|
||||
"FeeReserveMsat": 0,
|
||||
"SettledAt": &now,
|
||||
"SelfPayment": selfPayment,
|
||||
}).Error
|
||||
if err != nil {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"payment_hash": dbTransaction.PaymentHash,
|
||||
}).WithError(err).Error("Failed to update DB transaction")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"payment_hash": dbTransaction.PaymentHash,
|
||||
"type": dbTransaction.Type,
|
||||
}).Info("Marked transaction as settled")
|
||||
|
||||
event := "nwc_payment_sent"
|
||||
if dbTransaction.Type == constants.TRANSACTION_TYPE_INCOMING {
|
||||
event = "nwc_payment_received"
|
||||
}
|
||||
|
||||
svc.eventPublisher.Publish(&events.Event{
|
||||
Event: event,
|
||||
Properties: dbTransaction,
|
||||
})
|
||||
|
||||
return dbTransaction, nil
|
||||
}
|
||||
|
||||
func (svc *transactionsService) markPaymentFailed(tx *gorm.DB, dbTransaction *db.Transaction, reason string) error {
|
||||
var existingTransaction db.Transaction
|
||||
result := tx.Limit(1).Find(&existingTransaction, &db.Transaction{
|
||||
ID: dbTransaction.ID,
|
||||
})
|
||||
|
||||
if result.Error != nil {
|
||||
logger.Logger.WithField("payment_hash", dbTransaction.PaymentHash).WithError(result.Error).Error("could not find transaction to mark as failed")
|
||||
return result.Error
|
||||
}
|
||||
|
||||
if existingTransaction.State == constants.TRANSACTION_STATE_FAILED {
|
||||
logger.Logger.WithField("payment_hash", dbTransaction.PaymentHash).Info("payment already marked as failed")
|
||||
return nil
|
||||
}
|
||||
|
||||
err := tx.Model(dbTransaction).Updates(map[string]interface{}{
|
||||
"State": constants.TRANSACTION_STATE_FAILED,
|
||||
"FeeReserveMsat": 0,
|
||||
"FailureReason": reason,
|
||||
}).Error
|
||||
if err != nil {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"payment_hash": dbTransaction.PaymentHash,
|
||||
}).WithError(err).Error("Failed to mark transaction as failed")
|
||||
return err
|
||||
}
|
||||
logger.Logger.WithField("payment_hash", dbTransaction.PaymentHash).Info("Marked transaction as failed")
|
||||
|
||||
svc.eventPublisher.Publish(&events.Event{
|
||||
Event: "nwc_payment_failed",
|
||||
Properties: dbTransaction,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue