feat: add hold invoices support for LND & LDK (#1298)

* feat: add LND hold invoices support

* fix: add HOLD_INVOICE_ACCEPTED_NOTIFICATION to notifications list returned by get_info

* fix: revert

* fix: remove unneeded null checks

* docs: add extra event type to README

* feat: add LND hold invoices support

* fix: add HOLD_INVOICE_ACCEPTED_NOTIFICATION to notifications list returned by get_info

* fix: revert

* fix: remove unneeded null checks

* docs: add extra event type to README

* fix: remove 0 expiry checking in the make_hold_invoice_controller

* fix: use JSON logging

* revert fly.toml

* fix: duplicated check

* fix: move publishing nwc_hold_invoice_accepted out of the transaction

* fix: move publishing nwc_hold_invoice_accepted out of the transaction

* fix: check the invoice state ACCEPTED before calling the lnClient.SettleHoldInvoice

* fix: check the invoice state ACCEPTED before calling the lnClient.SettleHoldInvoice

* fix: check the invoice state ACCEPTED before calling the lnClient.SettleHoldInvoice

* fix: check the invoice state ACCEPTED before calling the lnClient.SettleHoldInvoice

* fix: cancel hold invoice tests

* fix: make hold invoice tests

* fix: make hold invoice tests

* fix: settle hold invoice tests

* fix: resubscribe to pending hold invoices

* fix: missing WatchHoldInvoice
 in the mocks

* feat: add LDK impl

* fix: cleanup

* fix: payment_hash

* fix: remove unneeded update

* feat: add support for self payments for hold invoices (#1304)

Co-authored-by: frnandu <frnandu@gmail.com>

* fix: remove hold invoices scope

* fix: remove hold invoices scope

* fix: mock hold bolt11 expiry to 10 years

* fix: sleep 1 second to give a change of lookupinvoice to read cancelled

* fix: missing timeout param

* feat: add hold invoice settle deadline to transactions (#1324)

* chore: update mockery, remove unused hold invoice method

* chore: remove unnecessary comment

* chore: remove unused code

* fix: do not return hold transaction if lookup failed

* chore: remove unused code

* fix: return correct errors from nip47 controllers, remove unused code

* fix: failing test

* chore: remove unnecessary code

* fix: make error message more general

* chore: remove unused code

* fix: use correct context in lnd service

* fix: unstable hold payments test

---------

Co-authored-by: Roland Bewick <roland.bewick@gmail.com>
Co-authored-by: Roland <33993199+rolznz@users.noreply.github.com>
This commit is contained in:
frnandu 2025-05-26 11:51:23 +02:00 committed by GitHub
parent 56fa7c89cf
commit 04fc9c90dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
47 changed files with 2508 additions and 839 deletions

View file

@ -1,19 +1,14 @@
filename: "{{.InterfaceName}}.go"
dir: tests/mocks
outpkg: mocks
# Fix deprecation warnings:
issue-845-fix: True
resolve-type-alias: False
pkgname: mocks
template: testify
packages:
github.com/getAlby/hub/service:
interfaces:
Service:
github.com/getAlby/hub/lnclient:
interfaces:
LNClient:
github.com/getAlby/hub/config:
interfaces:
Config:
Config: {}
github.com/getAlby/hub/lnclient:
interfaces:
LNClient: {}
github.com/getAlby/hub/service:
interfaces:
Service: {}

View file

@ -529,6 +529,8 @@ Internally Alby Hub uses a basic implementation of the pubsub messaging pattern
- `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_hold_invoice_accepted` - accepted a lightning payment, but it needs to be cancelled or settled
- `nwc_hold_invoice_canceled` - accepted hold payment was explicitly cancelled
- `nwc_budget_warning` - successfully made a lightning payment, but budget is nearly exceeded
- `nwc_app_created` - a new app connection was created
- `nwc_app_deleted` - a new app connection was deleted

View file

@ -9,6 +9,7 @@ const (
TRANSACTION_STATE_PENDING = "PENDING"
TRANSACTION_STATE_SETTLED = "SETTLED"
TRANSACTION_STATE_FAILED = "FAILED"
TRANSACTION_STATE_ACCEPTED = "ACCEPTED"
)
const (

View file

@ -0,0 +1,26 @@
package migrations
import (
_ "embed"
"github.com/go-gormigrate/gormigrate/v2"
"gorm.io/gorm"
)
var _202505091314_hold_invoices = &gormigrate.Migration{
ID: "202505091314_hold_invoices",
Migrate: func(db *gorm.DB) error {
if err := db.Exec(`
ALTER TABLE transactions ADD COLUMN hold BOOLEAN;
ALTER TABLE transactions ADD COLUMN settle_deadline integer;
`).Error; err != nil {
return err
}
return nil
},
Rollback: func(tx *gorm.DB) error {
return nil
},
}

View file

@ -29,6 +29,7 @@ func Migrate(gormDB *gorm.DB) error {
_202410141503_add_wallet_pubkey,
_202412212345_fix_types,
_202504231037_add_indexes,
_202505091314_hold_invoices,
})
return m.Migrate()

View file

@ -85,6 +85,8 @@ type Transaction struct {
SelfPayment bool
Boostagram datatypes.JSON
FailureReason string
Hold bool
SettleDeadline *uint32 // block number for accepted hold invoices
}
const (

View file

@ -126,7 +126,12 @@ const NewAppInternal = ({ capabilities }: NewAppInternalProps) => {
if (requestMethodsSet.has("get_balance")) {
scopes.push("get_balance");
}
if (requestMethodsSet.has("make_invoice")) {
if (
requestMethodsSet.has("make_invoice") ||
requestMethodsSet.has("make_hold_invoice") ||
requestMethodsSet.has("settle_hold_invoice") ||
requestMethodsSet.has("cancel_hold_invoice")
) {
scopes.push("make_invoice");
}
if (requestMethodsSet.has("lookup_invoice")) {

View file

@ -24,7 +24,10 @@ export type Nip47RequestMethod =
| "list_transactions"
| "sign_message"
| "multi_pay_invoice"
| "multi_pay_keysend";
| "multi_pay_keysend"
| "make_hold_invoice"
| "settle_hold_invoice"
| "cancel_hold_invoice";
export type BudgetRenewalType =
| "daily"

View file

@ -120,6 +120,18 @@ func (cs *CashuService) MakeInvoice(ctx context.Context, amount int64, descripti
return cs.cashuMintQuoteToTransaction(mintQuote), nil
}
func (cs *CashuService) MakeHoldInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64, paymentHash string) (transaction *lnclient.Transaction, err error) {
return nil, errors.New("not implemented")
}
func (cs *CashuService) SettleHoldInvoice(ctx context.Context, preimage string) (err error) {
return errors.New("not implemented")
}
func (cs *CashuService) CancelHoldInvoice(ctx context.Context, paymentHash string) (err error) {
return errors.New("not implemented")
}
func (cs *CashuService) LookupInvoice(ctx context.Context, paymentHash string) (transaction *lnclient.Transaction, err error) {
mintQuote := cs.getMintQuoteByPaymentHash(paymentHash)
if mintQuote != nil {

View file

@ -31,6 +31,8 @@ import (
"github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/logger"
"github.com/getAlby/hub/lsp"
"github.com/getAlby/hub/nip47/models"
"github.com/getAlby/hub/nip47/notifications"
"github.com/getAlby/hub/service/keys"
"github.com/getAlby/hub/transactions"
)
@ -1602,6 +1604,35 @@ func (ls *LDKService) handleLdkEvent(event *ldk_node.Event) {
"total_fee_earned_msat": eventType.TotalFeeEarnedMsat,
"outbound_amount_forwarded_msat": eventType.OutboundAmountForwardedMsat,
}).Info("LDK Payment forwarded")
case ldk_node.EventPaymentClaimable:
if eventType.ClaimDeadline == nil {
logger.Logger.WithField("payment_id", eventType.PaymentId).Error("claimable payment has no claim deadline")
return
}
logger.Logger.WithFields(logrus.Fields{
"claimable_amount_msats": eventType.ClaimableAmountMsat,
"payment_hash": eventType.PaymentHash,
"claim_deadline": *eventType.ClaimDeadline,
}).Info("LDK Payment Claimable")
payment := ls.node.Payment(eventType.PaymentId)
if payment == nil {
logger.Logger.WithField("payment_id", eventType.PaymentId).Error("could not find LDK payment")
return
}
transaction, err := ls.ldkPaymentToTransaction(payment)
if err != nil {
logger.Logger.WithField("payment_id", eventType.PaymentId).Error("failed to convert LDK payment to transaction")
return
}
transaction.SettleDeadline = eventType.ClaimDeadline
ls.eventPublisher.Publish(&events.Event{
Event: "nwc_lnclient_hold_invoice_accepted",
Properties: transaction,
})
}
}
@ -1823,11 +1854,30 @@ func (ls *LDKService) UpdateLastWalletSyncRequest() {
}
func (ls *LDKService) GetSupportedNIP47Methods() []string {
return []string{"pay_invoice", "pay_keysend", "get_balance", "get_budget", "get_info", "make_invoice", "lookup_invoice", "list_transactions", "multi_pay_invoice", "multi_pay_keysend", "sign_message"}
return []string{
models.PAY_INVOICE_METHOD,
models.PAY_KEYSEND_METHOD,
models.GET_BALANCE_METHOD,
models.GET_BUDGET_METHOD,
models.GET_INFO_METHOD,
models.MAKE_INVOICE_METHOD,
models.LOOKUP_INVOICE_METHOD,
models.LIST_TRANSACTIONS_METHOD,
models.MULTI_PAY_INVOICE_METHOD,
models.MULTI_PAY_KEYSEND_METHOD,
models.SIGN_MESSAGE_METHOD,
models.MAKE_HOLD_INVOICE_METHOD,
models.SETTLE_HOLD_INVOICE_METHOD,
models.CANCEL_HOLD_INVOICE_METHOD,
}
}
func (ls *LDKService) GetSupportedNIP47NotificationTypes() []string {
return []string{"payment_received", "payment_sent"}
return []string{
notifications.PAYMENT_RECEIVED_NOTIFICATION,
notifications.PAYMENT_SENT_NOTIFICATION,
notifications.HOLD_INVOICE_ACCEPTED_NOTIFICATION,
}
}
func (ls *LDKService) getPaymentFailReason(eventPaymentFailed *ldk_node.EventPaymentFailed) string {
@ -1912,6 +1962,133 @@ func (ls *LDKService) ExecuteCustomNodeCommand(ctx context.Context, command *lnc
return nil, nil
}
func (ls *LDKService) MakeHoldInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64, paymentHash string) (*lnclient.Transaction, error) {
if time.Duration(expiry)*time.Second > maxInvoiceExpiry {
return nil, errors.New("expiry is too long")
}
maxReceivable := ls.getMaxReceivable()
if amount > maxReceivable {
ls.eventPublisher.Publish(&events.Event{
Event: "nwc_incoming_liquidity_required",
Properties: map[string]interface{}{
"node_type": config.LDKBackendType,
},
})
}
if expiry == 0 {
expiry = lnclient.DEFAULT_INVOICE_EXPIRY
}
var descriptionType ldk_node.Bolt11InvoiceDescription
descriptionType = ldk_node.Bolt11InvoiceDescriptionDirect{
Description: description,
}
if description == "" && descriptionHash != "" {
descriptionType = ldk_node.Bolt11InvoiceDescriptionHash{
Hash: descriptionHash,
}
}
decodedPaymentHash, err := hex.DecodeString(paymentHash)
if err != nil {
logger.Logger.WithError(err).WithField("paymentHash", paymentHash).Error("Failed to decode payment hash for MakeHoldInvoice")
return nil, fmt.Errorf("failed to decode payment hash: %w", err)
}
if len(decodedPaymentHash) != 32 {
return nil, errors.New("payment hash must be 32 bytes")
}
var paymentHash32 [32]byte
copy(paymentHash32[:], decodedPaymentHash)
ldkPaymentHash := ldk_node.PaymentHash(hex.EncodeToString(paymentHash32[:]))
invoice, err := checkLDKErr(ls.node.Bolt11Payment().ReceiveForHash(uint64(amount),
descriptionType,
uint32(expiry),
ldkPaymentHash))
if err != nil {
logger.Logger.WithError(err).Error("MakeHoldInvoice failed")
return nil, err
}
var expiresAt *int64
paymentRequest, err := decodepay.Decodepay(invoice)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"bolt11": invoice,
}).WithError(err).Error("Failed to decode bolt11 invoice")
return nil, err
}
expiresAtUnix := time.UnixMilli(int64(paymentRequest.CreatedAt) * 1000).Add(time.Duration(paymentRequest.Expiry) * time.Second).Unix()
expiresAt = &expiresAtUnix
description = paymentRequest.Description
descriptionHash = paymentRequest.DescriptionHash
transaction := &lnclient.Transaction{
Type: "incoming",
Invoice: invoice,
PaymentHash: paymentRequest.PaymentHash,
Amount: amount,
CreatedAt: int64(paymentRequest.CreatedAt),
ExpiresAt: expiresAt,
Description: description,
DescriptionHash: descriptionHash,
}
return transaction, nil
}
func (ls *LDKService) CancelHoldInvoice(ctx context.Context, paymentHash string) error {
_, err := hex.DecodeString(paymentHash)
if err != nil {
logger.Logger.WithError(err).WithField("paymentHash", paymentHash).Error("Failed to decode payment hash for CancelHoldInvoice")
return err
}
err = ls.node.Bolt11Payment().FailForHash(paymentHash).AsError()
if err != nil {
logger.Logger.WithError(err).WithField("paymentHash", paymentHash).Error("CancelHoldInvoice failed")
}
return err
}
func (ls *LDKService) SettleHoldInvoice(ctx context.Context, preimage string) error {
decodedPreimage, err := hex.DecodeString(preimage)
if err != nil {
logger.Logger.WithError(err).WithField("preimage", preimage).Error("Failed to decode preimage for SettleHoldInvoice")
return err
}
if len(decodedPreimage) != 32 {
return errors.New("preimage must be 32 bytes")
}
paymentHash256 := sha256.New()
paymentHash256.Write(decodedPreimage)
paymentHashBytes := paymentHash256.Sum(nil)
paymentHash := hex.EncodeToString(paymentHashBytes)
paymentDetails := ls.node.Payment(paymentHash)
if paymentDetails == nil {
logger.Logger.WithField("payment_hash", paymentHash).Error("SettleHoldInvoice: Could not find payment by derived hash")
return errors.New("payment not found for derived hash")
}
if paymentDetails.AmountMsat == nil {
logger.Logger.WithField("payment_hash", paymentHash).Error("SettleHoldInvoice: Payment has no amount_msat")
return errors.New("payment has no amount_msat")
}
err = ls.node.Bolt11Payment().ClaimForHash(paymentHash, *paymentDetails.AmountMsat, preimage).AsError()
if err != nil {
logger.Logger.WithError(err).WithField("preimage", preimage).WithField("derived_payment_hash", paymentHash).Error("SettleHoldInvoice failed")
}
return err
}
func GetVssNodeIdentifier(keys keys.Keys) (string, error) {
key, err := keys.DeriveKey([]uint32{bip32.FirstHardenedChild + 2})

View file

@ -24,12 +24,15 @@ import (
"github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/lnclient/lnd/wrapper"
"github.com/getAlby/hub/logger"
"github.com/getAlby/hub/nip47/models"
"github.com/getAlby/hub/nip47/notifications"
"github.com/getAlby/hub/transactions"
"github.com/sirupsen/logrus"
// "gorm.io/gorm"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
)
@ -37,6 +40,7 @@ type LNDService struct {
client *wrapper.LNDWrapper
nodeInfo *lnclient.NodeInfo
cancel context.CancelFunc
ctx context.Context
eventPublisher events.EventPublisher
}
@ -79,12 +83,14 @@ func NewLNDService(ctx context.Context, eventPublisher events.EventPublisher, ln
client: lndClient,
nodeInfo: nodeInfo,
cancel: cancel,
ctx: lndCtx,
eventPublisher: eventPublisher,
}
go lndService.subscribePayments(lndCtx)
go lndService.subscribeInvoices(lndCtx)
go lndService.subscribeChannelEvents(lndCtx)
go lndService.subscribeOpenHoldInvoices(lndCtx)
logger.Logger.WithField("alias", nodeInfo.Alias).Info("Connected to LND")
@ -279,6 +285,107 @@ func (svc *LNDService) subscribeChannelEvents(ctx context.Context) {
}
}
func (svc *LNDService) subscribeOpenHoldInvoices(ctx context.Context) {
oneWeekAgo := time.Now().AddDate(0, 0, -7).Unix()
listInvoicesResponse, err := svc.client.ListInvoices(ctx, &lnrpc.ListInvoiceRequest{
PendingOnly: true,
CreationDateStart: uint64(oneWeekAgo),
})
if err != nil {
logger.Logger.WithError(err).Error("Failed to list invoices for open hold invoices subscription")
return
}
for _, invoice := range listInvoicesResponse.Invoices {
if invoice.State == lnrpc.Invoice_OPEN {
paymentHashHex := hex.EncodeToString(invoice.RHash)
logger.Logger.WithFields(logrus.Fields{
"paymentHash": paymentHashHex,
"addIndex": invoice.AddIndex,
}).Info("Resubscribing to pending hold invoice")
go svc.subscribeSingleInvoice(invoice.RHash)
}
}
}
func (svc *LNDService) subscribeSingleInvoice(paymentHashBytes []byte) {
// Use the global context for the lifetime of this subscription, but create a cancellable one for this specific task
// This allows the goroutine to be potentially cancelled externally if needed, though it primarily exits on invoice state change.
// We use a background context derived from the global one to avoid cancelling if the original request context finishes.
ctx, cancel := context.WithCancel(svc.ctx)
defer cancel() // Ensure cancellation happens on exit
paymentHashHex := hex.EncodeToString(paymentHashBytes)
log := logger.Logger.WithField("paymentHash", paymentHashHex)
log.Info("Starting subscribeSingleInvoice goroutine")
subReq := &invoicesrpc.SubscribeSingleInvoiceRequest{
RHash: paymentHashBytes,
}
invoiceStream, err := svc.client.SubscribeSingleInvoice(ctx, subReq)
if err != nil {
log.WithError(err).Error("SubscribeSingleInvoice call failed")
// Goroutine will exit
return
}
log.Info("Successfully subscribed to single invoice stream")
defer func() {
log.Info("Exiting subscribeSingleInvoice goroutine")
if r := recover(); r != nil {
log.WithField("panic", r).Errorf("PANIC recovered in single invoice stream processing")
}
}()
for {
invoice, err := invoiceStream.Recv()
if err != nil {
log.WithError(err).Error("Failed to receive single invoice update from stream")
return
}
if ctx.Err() != nil {
log.Info("Context cancelled, exiting single invoice subscription loop")
return
}
log.WithFields(logrus.Fields{
"rawState": invoice.State.String(),
"addIndex": invoice.AddIndex,
"settleIndex": invoice.SettleIndex,
"amtPaidMsat": invoice.AmtPaidMsat,
}).Info("Raw update received from single invoice stream")
switch invoice.State {
case lnrpc.Invoice_ACCEPTED:
log.Info("Hold invoice accepted, publishing internal event")
transaction := lndInvoiceToTransaction(invoice)
var minExpiry uint32
for _, htlc := range invoice.Htlcs {
if htlc.ExpiryHeight < int32(minExpiry) || minExpiry == 0 {
minExpiry = uint32(htlc.ExpiryHeight)
}
}
transaction.SettleDeadline = &minExpiry
svc.eventPublisher.Publish(&events.Event{
Event: "nwc_lnclient_hold_invoice_accepted",
Properties: transaction,
})
case lnrpc.Invoice_CANCELED:
log.Info("Hold invoice canceled, ending subscription")
return // Invoice reached final state, exit goroutine
case lnrpc.Invoice_SETTLED:
return // Invoice reached final state, exit goroutine
case lnrpc.Invoice_OPEN:
// Continue loop
}
}
}
func (svc *LNDService) Shutdown() error {
logger.Logger.Info("cancelling LND context")
svc.cancel()
@ -512,6 +619,127 @@ func (svc *LNDService) MakeInvoice(ctx context.Context, amount int64, descriptio
return transaction, nil
}
func (svc *LNDService) MakeHoldInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64, paymentHash string) (transaction *lnclient.Transaction, err error) {
var descriptionHashBytes []byte
var paymentHashBytes []byte
if descriptionHash != "" {
descriptionHashBytes, err = hex.DecodeString(descriptionHash)
if err != nil || len(descriptionHashBytes) != 32 {
if err == nil {
err = errors.New("description hash must be 32 bytes hex")
}
logger.Logger.WithFields(logrus.Fields{
"descriptionHash": descriptionHash,
}).WithError(err).Error("Invalid description hash")
return nil, err
}
}
paymentHashBytes, err = hex.DecodeString(paymentHash)
if err != nil || len(paymentHashBytes) != 32 {
if err == nil {
err = errors.New("payment hash must be 32 bytes hex")
}
logger.Logger.WithFields(logrus.Fields{
"paymentHash": paymentHash,
}).WithError(err).Error("Invalid payment hash")
return nil, err
}
if expiry == 0 {
expiry = lnclient.DEFAULT_INVOICE_EXPIRY
}
channels, err := svc.ListChannels(ctx)
if err != nil {
return nil, err
}
hasPublicChannels := false
for _, channel := range channels {
if channel.Active && channel.Public {
hasPublicChannels = true
}
}
addInvoiceRequest := &invoicesrpc.AddHoldInvoiceRequest{
ValueMsat: amount,
Memo: description,
DescriptionHash: descriptionHashBytes,
Expiry: expiry,
Private: !hasPublicChannels,
Hash: paymentHashBytes,
}
_, err = svc.client.AddHoldInvoice(ctx, addInvoiceRequest)
if err != nil {
logger.Logger.WithError(err).Error("Failed to create hold invoice")
return nil, err
}
// Start subscribing to updates for this specific hold invoice in a separate goroutine
go svc.subscribeSingleInvoice(paymentHashBytes)
logger.Logger.WithField("paymentHash", paymentHash).Info("Launched single invoice subscription goroutine")
inv, err := svc.client.LookupInvoice(ctx, &lnrpc.PaymentHash{RHash: paymentHashBytes})
if err != nil {
logger.Logger.WithField("paymentHash", paymentHash).WithError(err).Error("Failed to lookup hold invoice after creation")
return nil, err
}
transaction = lndInvoiceToTransaction(inv)
return transaction, nil
}
func (svc *LNDService) SettleHoldInvoice(ctx context.Context, preimage string) (err error) {
preimageBytes, err := hex.DecodeString(preimage)
if err != nil || len(preimageBytes) != 32 {
if err == nil {
err = errors.New("preimage must be 32 bytes hex")
}
logger.Logger.WithFields(logrus.Fields{
"preimage": preimage,
}).WithError(err).Error("Invalid preimage")
return err
}
_, err = svc.client.SettleInvoice(ctx, &invoicesrpc.SettleInvoiceMsg{
Preimage: preimageBytes,
})
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"preimage": preimage,
}).WithError(err).Error("Failed to settle hold invoice")
return err
}
return nil
}
func (svc *LNDService) CancelHoldInvoice(ctx context.Context, paymentHash string) (err error) {
paymentHashBytes, err := hex.DecodeString(paymentHash)
if err != nil || len(paymentHashBytes) != 32 {
if err == nil {
err = errors.New("payment hash must be 32 bytes hex")
}
logger.Logger.WithFields(logrus.Fields{
"paymentHash": paymentHash,
}).WithError(err).Error("Invalid payment hash")
return err
}
_, err = svc.client.CancelInvoice(ctx, &invoicesrpc.CancelInvoiceMsg{
PaymentHash: paymentHashBytes,
})
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"paymentHash": paymentHash,
}).WithError(err).Error("Failed to cancel hold invoice")
return err
}
return nil
}
func (svc *LNDService) LookupInvoice(ctx context.Context, paymentHash string) (transaction *lnclient.Transaction, err error) {
paymentHashBytes, err := hex.DecodeString(paymentHash)
if err != nil || len(paymentHashBytes) != 32 {
@ -1202,12 +1430,25 @@ func (svc *LNDService) UpdateLastWalletSyncRequest() {}
func (svc *LNDService) GetSupportedNIP47Methods() []string {
return []string{
"pay_invoice", "pay_keysend", "get_balance", "get_budget", "get_info", "make_invoice", "lookup_invoice", "list_transactions", "multi_pay_invoice", "multi_pay_keysend", "sign_message",
models.PAY_INVOICE_METHOD,
models.PAY_KEYSEND_METHOD,
models.GET_BALANCE_METHOD,
models.GET_BUDGET_METHOD,
models.GET_INFO_METHOD,
models.MAKE_INVOICE_METHOD,
models.LOOKUP_INVOICE_METHOD,
models.LIST_TRANSACTIONS_METHOD,
models.MULTI_PAY_INVOICE_METHOD,
models.MULTI_PAY_KEYSEND_METHOD,
models.SIGN_MESSAGE_METHOD,
models.MAKE_HOLD_INVOICE_METHOD,
models.SETTLE_HOLD_INVOICE_METHOD,
models.CANCEL_HOLD_INVOICE_METHOD,
}
}
func (svc *LNDService) GetSupportedNIP47NotificationTypes() []string {
return []string{"payment_received", "payment_sent"}
return []string{notifications.PAYMENT_RECEIVED_NOTIFICATION, notifications.PAYMENT_SENT_NOTIFICATION, notifications.HOLD_INVOICE_ACCEPTED_NOTIFICATION}
}
func (svc *LNDService) GetPubkey() string {

View file

@ -4,6 +4,7 @@ import (
"context"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"google.golang.org/grpc"
)
@ -13,7 +14,11 @@ type LightningClientWrapper interface {
SendPaymentSync(ctx context.Context, req *lnrpc.SendRequest, options ...grpc.CallOption) (*lnrpc.SendResponse, error)
ChannelBalance(ctx context.Context, req *lnrpc.ChannelBalanceRequest, options ...grpc.CallOption) (*lnrpc.ChannelBalanceResponse, error)
AddInvoice(ctx context.Context, req *lnrpc.Invoice, options ...grpc.CallOption) (*lnrpc.AddInvoiceResponse, error)
AddHoldInvoice(ctx context.Context, req *invoicesrpc.AddHoldInvoiceRequest, options ...grpc.CallOption) (*invoicesrpc.AddHoldInvoiceResp, error)
SettleInvoice(ctx context.Context, req *invoicesrpc.SettleInvoiceMsg, options ...grpc.CallOption) (*invoicesrpc.SettleInvoiceResp, error)
CancelInvoice(ctx context.Context, req *invoicesrpc.CancelInvoiceMsg, options ...grpc.CallOption) (*invoicesrpc.CancelInvoiceResp, error)
SubscribeInvoices(ctx context.Context, req *lnrpc.InvoiceSubscription, options ...grpc.CallOption) (SubscribeInvoicesWrapper, error)
SubscribeSingleInvoice(ctx context.Context, req *invoicesrpc.SubscribeSingleInvoiceRequest, options ...grpc.CallOption) (SubscribeSingleInvoiceWrapper, error) // Added
SubscribePayment(ctx context.Context, req *routerrpc.TrackPaymentRequest, options ...grpc.CallOption) (SubscribePaymentWrapper, error)
LookupInvoice(ctx context.Context, req *lnrpc.PaymentHash, options ...grpc.CallOption) (*lnrpc.Invoice, error)
GetInfo(ctx context.Context, req *lnrpc.GetInfoRequest, options ...grpc.CallOption) (*lnrpc.GetInfoResponse, error)
@ -26,6 +31,11 @@ type LightningClientWrapper interface {
type SubscribeInvoicesWrapper interface {
Recv() (*lnrpc.Invoice, error)
}
type SubscribeSingleInvoiceWrapper interface {
Recv() (*lnrpc.Invoice, error)
}
type SubscribePaymentWrapper interface {
Recv() (*lnrpc.Payment, error)
}

View file

@ -8,6 +8,7 @@ import (
"errors"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"github.com/lightningnetwork/lnd/macaroons"
"google.golang.org/grpc"
@ -33,6 +34,7 @@ type LNDWrapper struct {
client lnrpc.LightningClient
routerClient routerrpc.RouterClient
stateClient lnrpc.StateClient
invoicesClient invoicesrpc.InvoicesClient
IdentityPubkey string
}
@ -85,9 +87,10 @@ func NewLNDclient(lndOptions LNDoptions) (result *LNDWrapper, err error) {
}
lnClient := lnrpc.NewLightningClient(conn)
return &LNDWrapper{
client: lnClient,
routerClient: routerrpc.NewRouterClient(conn),
stateClient: lnrpc.NewStateClient(conn),
client: lnClient,
routerClient: routerrpc.NewRouterClient(conn),
stateClient: lnrpc.NewStateClient(conn),
invoicesClient: invoicesrpc.NewInvoicesClient(conn),
}, nil
}
@ -115,10 +118,26 @@ func (wrapper *LNDWrapper) AddInvoice(ctx context.Context, req *lnrpc.Invoice, o
return wrapper.client.AddInvoice(ctx, req, options...)
}
func (wrapper *LNDWrapper) AddHoldInvoice(ctx context.Context, req *invoicesrpc.AddHoldInvoiceRequest, options ...grpc.CallOption) (*invoicesrpc.AddHoldInvoiceResp, error) {
return wrapper.invoicesClient.AddHoldInvoice(ctx, req, options...)
}
func (wrapper *LNDWrapper) SettleInvoice(ctx context.Context, req *invoicesrpc.SettleInvoiceMsg, options ...grpc.CallOption) (*invoicesrpc.SettleInvoiceResp, error) {
return wrapper.invoicesClient.SettleInvoice(ctx, req, options...)
}
func (wrapper *LNDWrapper) CancelInvoice(ctx context.Context, req *invoicesrpc.CancelInvoiceMsg, options ...grpc.CallOption) (*invoicesrpc.CancelInvoiceResp, error) {
return wrapper.invoicesClient.CancelInvoice(ctx, req, options...)
}
func (wrapper *LNDWrapper) SubscribeInvoices(ctx context.Context, req *lnrpc.InvoiceSubscription, options ...grpc.CallOption) (SubscribeInvoicesWrapper, error) {
return wrapper.client.SubscribeInvoices(ctx, req, options...)
}
func (wrapper *LNDWrapper) SubscribeSingleInvoice(ctx context.Context, req *invoicesrpc.SubscribeSingleInvoiceRequest, options ...grpc.CallOption) (SubscribeSingleInvoiceWrapper, error) {
return wrapper.invoicesClient.SubscribeSingleInvoice(ctx, req, options...)
}
func (wrapper *LNDWrapper) SubscribePayments(ctx context.Context, req *routerrpc.TrackPaymentsRequest, options ...grpc.CallOption) (routerrpc.Router_TrackPaymentsClient, error) {
return wrapper.routerClient.TrackPayments(ctx, req, options...)
}

View file

@ -38,6 +38,7 @@ type Transaction struct {
ExpiresAt *int64
SettledAt *int64
Metadata Metadata
SettleDeadline *uint32 // block number for accepted hold invoices
}
type OnchainTransaction struct {
@ -61,6 +62,9 @@ type LNClient interface {
GetPubkey() string
GetInfo(ctx context.Context) (info *NodeInfo, err error)
MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64) (transaction *Transaction, err error)
MakeHoldInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64, paymentHash string) (transaction *Transaction, err error)
SettleHoldInvoice(ctx context.Context, preimage string) (err error)
CancelHoldInvoice(ctx context.Context, paymentHash string) (err error)
LookupInvoice(ctx context.Context, paymentHash string) (transaction *Transaction, err error)
ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaid bool, invoiceType string) (transactions []Transaction, err error)
ListOnchainTransactions(ctx context.Context) ([]OnchainTransaction, error)
@ -249,3 +253,14 @@ func NewTimeoutError() error {
func (err *timeoutError) Error() string {
return "Timeout"
}
type holdInvoiceCanceledError struct {
}
func NewHoldInvoiceCanceledError() error {
return &holdInvoiceCanceledError{}
}
func (err *holdInvoiceCanceledError) Error() string {
return "Hold invoice canceled"
}

View file

@ -324,6 +324,18 @@ func (svc *PhoenixService) MakeInvoice(ctx context.Context, amount int64, descri
return tx, nil
}
func (svc *PhoenixService) MakeHoldInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64, paymentHash string) (transaction *lnclient.Transaction, err error) {
return nil, errors.New("not implemented")
}
func (svc *PhoenixService) SettleHoldInvoice(ctx context.Context, preimage string) (err error) {
return errors.New("not implemented")
}
func (svc *PhoenixService) CancelHoldInvoice(ctx context.Context, paymentHash string) (err error) {
return errors.New("not implemented")
}
func (svc *PhoenixService) LookupInvoice(ctx context.Context, paymentHash string) (transaction *lnclient.Transaction, err error) {
req, err := http.NewRequest(http.MethodGet, svc.Address+"/payments/incoming/"+paymentHash, nil)
if err != nil {
@ -394,6 +406,7 @@ func (svc *PhoenixService) ResetRouter(key string) error {
}
func (svc *PhoenixService) Shutdown() error {
// No specific shutdown actions needed for Phoenixd client via HTTP
return nil
}

View file

@ -0,0 +1,50 @@
package controllers
import (
"context"
"github.com/getAlby/hub/logger"
"github.com/getAlby/hub/nip47/models"
"github.com/nbd-wtf/go-nostr"
"github.com/sirupsen/logrus"
)
type cancelHoldInvoiceParams struct {
PaymentHash string `json:"payment_hash"`
}
type cancelHoldInvoiceResponse struct{}
func (controller *nip47Controller) HandleCancelHoldInvoiceEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, appId uint, publishResponse func(*models.Response, nostr.Tags)) {
cancelHoldInvoiceParams := &cancelHoldInvoiceParams{}
decodeErrResp := decodeRequest(nip47Request, cancelHoldInvoiceParams)
if decodeErrResp != nil {
publishResponse(decodeErrResp, nostr.Tags{})
return
}
logger.Logger.WithFields(logrus.Fields{
"requestEventId": requestEventId,
"appId": appId,
"paymentHash": cancelHoldInvoiceParams.PaymentHash,
}).Info("Canceling hold invoice")
err := controller.transactionsService.CancelHoldInvoice(ctx, cancelHoldInvoiceParams.PaymentHash, controller.lnClient)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"request_event_id": requestEventId,
"appId": appId,
"paymentHash": cancelHoldInvoiceParams.PaymentHash,
}).WithError(err).Error("Failed to cancel hold invoice")
publishResponse(&models.Response{
ResultType: nip47Request.Method,
Error: mapNip47Error(err),
}, nostr.Tags{})
return
}
publishResponse(&models.Response{
ResultType: nip47Request.Method,
Result: &cancelHoldInvoiceResponse{},
}, nostr.Tags{})
}

View file

@ -0,0 +1,135 @@
package controllers
import (
"context"
"encoding/json"
"testing"
"github.com/nbd-wtf/go-nostr"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/getAlby/hub/constants"
"github.com/getAlby/hub/db"
"github.com/getAlby/hub/nip47/models"
"github.com/getAlby/hub/tests"
)
const nip47CancelHoldInvoiceJson = `
{
"method": "cancel_hold_invoice",
"params": {
"payment_hash": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
}
}
`
const testCancelPaymentHash = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
type cancelHoldInvoiceTestSetup struct {
ctx context.Context
svc *tests.TestService
nip47Request *models.Request
app *db.App
dbRequestEvent *db.RequestEvent
publishCalled bool
response *models.Response
}
func setupCancelHoldInvoiceTest(t *testing.T, paymentHash string, initialTransactionState *string) *cancelHoldInvoiceTestSetup {
ctx := context.TODO()
svc, err := tests.CreateTestService(t)
require.NoError(t, err)
// Not using defer svc.Remove() as it might be called before the test function finishes in some cases.
// Caller should call svc.Remove()
nip47Request := &models.Request{}
requestJson := `
{
"method": "cancel_hold_invoice",
"params": {
"payment_hash": "` + paymentHash + `"
}
}
`
err = json.Unmarshal([]byte(requestJson), nip47Request)
require.NoError(t, err)
app, _, err := tests.CreateApp(svc)
require.NoError(t, err)
appPermission := &db.AppPermission{
AppId: app.ID,
Scope: constants.MAKE_INVOICE_SCOPE,
}
err = svc.DB.Create(appPermission).Error
require.NoError(t, err)
if initialTransactionState != nil {
transaction := &db.Transaction{
AppId: &app.ID,
PaymentHash: paymentHash,
Type: constants.TRANSACTION_TYPE_INCOMING,
State: *initialTransactionState,
AmountMsat: 1000,
}
err = svc.DB.Create(transaction).Error
require.NoError(t, err)
}
dbRequestEvent := &db.RequestEvent{
AppId: &app.ID,
}
err = svc.DB.Create(&dbRequestEvent).Error
require.NoError(t, err)
setup := &cancelHoldInvoiceTestSetup{
ctx: ctx,
svc: svc,
nip47Request: nip47Request,
app: app,
dbRequestEvent: dbRequestEvent,
}
return setup
}
func (s *cancelHoldInvoiceTestSetup) TearDown() {
s.svc.Remove()
}
func (s *cancelHoldInvoiceTestSetup) PublishResponse(response *models.Response, tags nostr.Tags) {
s.publishCalled = true
s.response = response
}
func TestHandleCancelHoldInvoiceEvent(t *testing.T) {
initialState := constants.TRANSACTION_STATE_ACCEPTED
setup := setupCancelHoldInvoiceTest(t, testCancelPaymentHash, &initialState)
defer setup.TearDown()
NewTestNip47Controller(setup.svc).
HandleCancelHoldInvoiceEvent(setup.ctx, setup.nip47Request, setup.dbRequestEvent.ID, *setup.dbRequestEvent.AppId, setup.PublishResponse)
assert.True(t, setup.publishCalled)
assert.Nil(t, setup.response.Error)
assert.Equal(t, &cancelHoldInvoiceResponse{}, setup.response.Result)
var updatedTransaction db.Transaction
err := setup.svc.DB.First(&updatedTransaction, "payment_hash = ?", testCancelPaymentHash).Error
assert.NoError(t, err)
assert.Equal(t, constants.TRANSACTION_STATE_FAILED, updatedTransaction.State)
}
func TestHandleCancelHoldInvoiceEvent_InvoiceNotFound(t *testing.T) {
nonExistentPaymentHash := "nonexistentpaymenthashnonexistentpaymenthashnonexistentpaymenthash"
setup := setupCancelHoldInvoiceTest(t, nonExistentPaymentHash, nil) // nil for initialTransactionState means no transaction created
defer setup.TearDown()
NewTestNip47Controller(setup.svc).
HandleCancelHoldInvoiceEvent(setup.ctx, setup.nip47Request, setup.dbRequestEvent.ID, *setup.dbRequestEvent.AppId, setup.PublishResponse)
assert.True(t, setup.publishCalled)
require.NotNil(t, setup.response.Error)
assert.Equal(t, constants.ERROR_NOT_FOUND, setup.response.Error.Code)
}

View file

@ -57,7 +57,7 @@ func (controller *nip47Controller) HandleCreateConnectionEvent(ctx context.Conte
publishResponse(&models.Response{
ResultType: nip47Request.Method,
Error: &models.Error{
Code: constants.ERROR_INTERNAL,
Code: constants.ERROR_BAD_REQUEST,
Message: "cannot create a new app that has create_connection permission via NWC",
},
}, nostr.Tags{})
@ -69,7 +69,7 @@ func (controller *nip47Controller) HandleCreateConnectionEvent(ctx context.Conte
publishResponse(&models.Response{
ResultType: nip47Request.Method,
Error: &models.Error{
Code: constants.ERROR_INTERNAL,
Code: constants.ERROR_BAD_REQUEST,
Message: "No request methods provided",
},
}, nostr.Tags{})
@ -83,7 +83,7 @@ func (controller *nip47Controller) HandleCreateConnectionEvent(ctx context.Conte
publishResponse(&models.Response{
ResultType: nip47Request.Method,
Error: &models.Error{
Code: constants.ERROR_INTERNAL,
Code: constants.ERROR_BAD_REQUEST,
Message: "One or more methods are not supported by the current LNClient",
},
}, nostr.Tags{})
@ -100,7 +100,7 @@ func (controller *nip47Controller) HandleCreateConnectionEvent(ctx context.Conte
publishResponse(&models.Response{
ResultType: nip47Request.Method,
Error: &models.Error{
Code: constants.ERROR_INTERNAL,
Code: constants.ERROR_BAD_REQUEST,
Message: "One or more notification types are not supported by the current LNClient",
},
}, nostr.Tags{})
@ -116,10 +116,7 @@ func (controller *nip47Controller) HandleCreateConnectionEvent(ctx context.Conte
}).WithError(err).Error("Failed to create app")
publishResponse(&models.Response{
ResultType: nip47Request.Method,
Error: &models.Error{
Code: constants.ERROR_INTERNAL,
Message: err.Error(),
},
Error: mapNip47Error(err),
}, nostr.Tags{})
return
}

View file

@ -220,7 +220,7 @@ func TestHandleCreateConnectionEvent_NoMethods(t *testing.T) {
HandleCreateConnectionEvent(ctx, nip47Request, dbRequestEvent.ID, publishResponse)
assert.NotNil(t, publishedResponse.Error)
assert.Equal(t, constants.ERROR_INTERNAL, publishedResponse.Error.Code)
assert.Equal(t, constants.ERROR_BAD_REQUEST, publishedResponse.Error.Code)
assert.Equal(t, "No request methods provided", publishedResponse.Error.Message)
assert.Equal(t, models.CREATE_CONNECTION_METHOD, publishedResponse.ResultType)
assert.Nil(t, publishedResponse.Result)
@ -265,7 +265,7 @@ func TestHandleCreateConnectionEvent_UnsupportedMethod(t *testing.T) {
HandleCreateConnectionEvent(ctx, nip47Request, dbRequestEvent.ID, publishResponse)
assert.NotNil(t, publishedResponse.Error)
assert.Equal(t, constants.ERROR_INTERNAL, publishedResponse.Error.Code)
assert.Equal(t, constants.ERROR_BAD_REQUEST, publishedResponse.Error.Code)
assert.Equal(t, "One or more methods are not supported by the current LNClient", publishedResponse.Error.Message)
assert.Equal(t, models.CREATE_CONNECTION_METHOD, publishedResponse.ResultType)
assert.Nil(t, publishedResponse.Result)
@ -311,7 +311,7 @@ func TestHandleCreateConnectionEvent_UnsupportedNotificationType(t *testing.T) {
HandleCreateConnectionEvent(ctx, nip47Request, dbRequestEvent.ID, publishResponse)
assert.NotNil(t, publishedResponse.Error)
assert.Equal(t, constants.ERROR_INTERNAL, publishedResponse.Error.Code)
assert.Equal(t, constants.ERROR_BAD_REQUEST, publishedResponse.Error.Code)
assert.Equal(t, "One or more notification types are not supported by the current LNClient", publishedResponse.Error.Message)
assert.Equal(t, models.CREATE_CONNECTION_METHOD, publishedResponse.ResultType)
assert.Nil(t, publishedResponse.Result)
@ -356,7 +356,7 @@ func TestHandleCreateConnectionEvent_DoNotAllowCreateConnectionMethod(t *testing
HandleCreateConnectionEvent(ctx, nip47Request, dbRequestEvent.ID, publishResponse)
assert.NotNil(t, publishedResponse.Error)
assert.Equal(t, constants.ERROR_INTERNAL, publishedResponse.Error.Code)
assert.Equal(t, constants.ERROR_BAD_REQUEST, publishedResponse.Error.Code)
assert.Equal(t, "cannot create a new app that has create_connection permission via NWC", publishedResponse.Error.Message)
assert.Equal(t, models.CREATE_CONNECTION_METHOD, publishedResponse.ResultType)
assert.Nil(t, publishedResponse.Result)

View file

@ -3,7 +3,6 @@ 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,10 +40,7 @@ func (controller *nip47Controller) HandleGetBalanceEvent(ctx context.Context, ni
}).WithError(err).Error("Failed to fetch balance")
publishResponse(&models.Response{
ResultType: nip47Request.Method,
Error: &models.Error{
Code: constants.ERROR_INTERNAL,
Message: err.Error(),
},
Error: mapNip47Error(err),
}, nostr.Tags{})
return
}

View file

@ -53,10 +53,7 @@ func (controller *nip47Controller) HandleGetInfoEvent(ctx context.Context, nip47
publishResponse(&models.Response{
ResultType: nip47Request.Method,
Error: &models.Error{
Code: constants.ERROR_INTERNAL,
Message: err.Error(),
},
Error: mapNip47Error(err),
}, nostr.Tags{})
return
}

View file

@ -3,7 +3,6 @@ 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"
@ -60,10 +59,7 @@ func (controller *nip47Controller) HandleListTransactionsEvent(ctx context.Conte
publishResponse(&models.Response{
ResultType: nip47Request.Method,
Error: &models.Error{
Code: constants.ERROR_INTERNAL,
Message: err.Error(),
},
Error: mapNip47Error(err),
}, nostr.Tags{})
return
}

View file

@ -50,7 +50,7 @@ func (controller *nip47Controller) HandleLookupInvoiceEvent(ctx context.Context,
publishResponse(&models.Response{
ResultType: nip47Request.Method,
Error: &models.Error{
Code: constants.ERROR_INTERNAL,
Code: constants.ERROR_BAD_REQUEST,
Message: fmt.Sprintf("Failed to decode bolt11 invoice: %s", err.Error()),
},
}, nostr.Tags{})

View file

@ -0,0 +1,101 @@
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"
"github.com/sirupsen/logrus"
)
type makeHoldInvoiceParams struct {
Amount uint64 `json:"amount"`
PaymentHash string `json:"payment_hash"`
Description string `json:"description"`
DescriptionHash string `json:"description_hash"`
Expiry uint64 `json:"expiry"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
type makeHoldInvoiceResponse struct {
models.Transaction
}
func (controller *nip47Controller) HandleMakeHoldInvoiceEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, appId uint, publishResponse func(*models.Response, nostr.Tags)) {
makeHoldInvoiceParams := &makeHoldInvoiceParams{}
decodeErrResp := decodeRequest(nip47Request, makeHoldInvoiceParams)
if decodeErrResp != nil {
publishResponse(decodeErrResp, nostr.Tags{})
return
}
if makeHoldInvoiceParams.PaymentHash == "" {
logger.Logger.WithFields(logrus.Fields{
"requestEventId": requestEventId,
"appId": appId,
}).Error("Payment hash is missing for make_hold_invoice")
publishResponse(&models.Response{
ResultType: nip47Request.Method,
Error: &models.Error{
Code: constants.ERROR_BAD_REQUEST,
Message: "payment_hash is required for make_hold_invoice",
},
}, nostr.Tags{})
return
}
logger.Logger.WithFields(logrus.Fields{
"requestEventId": requestEventId,
"appId": appId,
"amount": makeHoldInvoiceParams.Amount,
"description": makeHoldInvoiceParams.Description,
"descriptionHash": makeHoldInvoiceParams.DescriptionHash,
"expiry": makeHoldInvoiceParams.Expiry,
"paymentHash": makeHoldInvoiceParams.PaymentHash,
"metadata": makeHoldInvoiceParams.Metadata,
}).Info("Making hold invoice")
requestEventIdUint := uint(requestEventId)
transaction, err := controller.transactionsService.MakeHoldInvoice(
ctx,
makeHoldInvoiceParams.Amount,
makeHoldInvoiceParams.Description,
makeHoldInvoiceParams.DescriptionHash,
makeHoldInvoiceParams.Expiry,
makeHoldInvoiceParams.PaymentHash,
makeHoldInvoiceParams.Metadata,
controller.lnClient,
&appId,
&requestEventIdUint,
)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"request_event_id": requestEventId,
"appId": appId,
"amount": makeHoldInvoiceParams.Amount,
"description": makeHoldInvoiceParams.Description,
"descriptionHash": makeHoldInvoiceParams.DescriptionHash,
"expiry": makeHoldInvoiceParams.Expiry,
"paymentHash": makeHoldInvoiceParams.PaymentHash,
}).WithError(err).Error("Failed to make invoice")
publishResponse(&models.Response{
ResultType: nip47Request.Method,
Error: mapNip47Error(err),
}, nostr.Tags{})
return
}
nip47Transaction := models.ToNip47Transaction(transaction)
responsePayload := &makeHoldInvoiceResponse{
Transaction: *nip47Transaction,
}
publishResponse(&models.Response{
ResultType: nip47Request.Method,
Result: responsePayload,
}, nostr.Tags{})
}

View file

@ -0,0 +1,129 @@
package controllers
import (
"context"
"encoding/json"
"testing"
"github.com/nbd-wtf/go-nostr"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/getAlby/hub/constants"
"github.com/getAlby/hub/db"
"github.com/getAlby/hub/nip47/models"
"github.com/getAlby/hub/tests"
)
const nip47MakeHoldInvoiceJson = `
{
"method": "make_hold_invoice",
"params": {
"amount": 1000,
"description": "Hello, world",
"payment_hash": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"expiry": 3600,
"metadata": {
"a": 1,
"b": "2",
"c": {
"d": 3,
"e": [{
"f": "g"
},{
"h": "i"
}]
}
}
}
}
`
func TestHandleMakeHoldInvoiceEvent(t *testing.T) {
ctx := context.TODO()
svc, err := tests.CreateTestService(t)
require.NoError(t, err)
defer svc.Remove()
nip47Request := &models.Request{}
err = json.Unmarshal([]byte(nip47MakeHoldInvoiceJson), nip47Request)
assert.NoError(t, err)
app, _, err := tests.CreateApp(svc)
assert.NoError(t, err)
dbRequestEvent := &db.RequestEvent{
AppId: &app.ID,
}
err = svc.DB.Create(&dbRequestEvent).Error
assert.NoError(t, err)
var publishedResponse *models.Response
publishResponse := func(response *models.Response, tags nostr.Tags) {
publishedResponse = response
}
NewTestNip47Controller(svc).
HandleMakeHoldInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, *dbRequestEvent.AppId, publishResponse)
expectedMetadata := map[string]interface{}{
"a": float64(1),
"b": "2",
"c": map[string]interface{}{
"d": float64(3),
"e": []interface{}{
map[string]interface{}{"f": "g"},
map[string]interface{}{"h": "i"},
},
},
}
assert.Nil(t, publishedResponse.Error)
assert.Equal(t, tests.MockLNClientHoldTransaction.Invoice, publishedResponse.Result.(*makeHoldInvoiceResponse).Invoice)
assert.Equal(t, tests.MockLNClientHoldTransaction.PaymentHash, publishedResponse.Result.(*makeHoldInvoiceResponse).PaymentHash)
assert.Equal(t, expectedMetadata, publishedResponse.Result.(*makeHoldInvoiceResponse).Metadata)
}
const nip47MakeHoldInvoiceMissingPaymentHashJson = `
{
"method": "make_hold_invoice",
"params": {
"amount": 1000,
"description": "Hello, world",
"expiry": 3600
}
}
`
func TestHandleMakeHoldInvoiceEvent_MissingPaymentHash(t *testing.T) {
ctx := context.TODO()
svc, err := tests.CreateTestService(t)
require.NoError(t, err)
defer svc.Remove()
nip47Request := &models.Request{}
err = json.Unmarshal([]byte(nip47MakeHoldInvoiceMissingPaymentHashJson), nip47Request)
assert.NoError(t, err)
app, _, err := tests.CreateApp(svc)
assert.NoError(t, err)
dbRequestEvent := &db.RequestEvent{
AppId: &app.ID,
}
err = svc.DB.Create(&dbRequestEvent).Error
assert.NoError(t, err)
var publishedResponse *models.Response
publishResponse := func(response *models.Response, tags nostr.Tags) {
publishedResponse = response
}
NewTestNip47Controller(svc).
HandleMakeHoldInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, *dbRequestEvent.AppId, publishResponse)
require.NotNil(t, publishedResponse.Error)
assert.Equal(t, constants.ERROR_BAD_REQUEST, publishedResponse.Error.Code)
}

View file

@ -3,7 +3,6 @@ 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,10 +52,7 @@ func (controller *nip47Controller) HandleMakeInvoiceEvent(ctx context.Context, n
publishResponse(&models.Response{
ResultType: nip47Request.Method,
Error: &models.Error{
Code: constants.ERROR_INTERNAL,
Message: err.Error(),
},
Error: mapNip47Error(err),
}, nostr.Tags{})
return
}

View file

@ -55,7 +55,7 @@ func (controller *nip47Controller) HandleMultiPayInvoiceEvent(ctx context.Contex
publishResponse(&models.Response{
ResultType: nip47Request.Method,
Error: &models.Error{
Code: constants.ERROR_INTERNAL,
Code: constants.ERROR_BAD_REQUEST,
Message: fmt.Sprintf("Failed to decode bolt11 invoice: %s", err.Error()),
},
}, nostr.Tags{dTag})

View file

@ -196,7 +196,7 @@ func TestHandleMultiPayInvoiceEvent_OneMalformedInvoice(t *testing.T) {
}
assert.Equal(t, "invoiceId123", dTags[0].GetFirst([]string{"d"}).Value())
assert.Equal(t, constants.ERROR_INTERNAL, responses[0].Error.Code)
assert.Equal(t, constants.ERROR_BAD_REQUEST, responses[0].Error.Code)
assert.Nil(t, responses[0].Result)
assert.Equal(t, tests.MockPaymentHash, dTags[1].GetFirst([]string{"d"}).Value())

View file

@ -42,7 +42,7 @@ func (controller *nip47Controller) HandlePayInvoiceEvent(ctx context.Context, ni
publishResponse(&models.Response{
ResultType: nip47Request.Method,
Error: &models.Error{
Code: constants.ERROR_INTERNAL,
Code: constants.ERROR_BAD_REQUEST,
Message: fmt.Sprintf("Failed to decode bolt11 invoice: %s", err.Error()),
},
}, tags)

View file

@ -182,7 +182,7 @@ func TestHandlePayInvoiceEvent_MalformedInvoice(t *testing.T) {
HandlePayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse, nostr.Tags{})
assert.Nil(t, publishedResponse.Result)
assert.Equal(t, constants.ERROR_INTERNAL, publishedResponse.Error.Code)
assert.Equal(t, constants.ERROR_BAD_REQUEST, publishedResponse.Error.Code)
assert.Equal(t, "Failed to decode bolt11 invoice: bolt11 too short", publishedResponse.Error.Message)
}

View file

@ -0,0 +1,50 @@
package controllers
import (
"context"
"github.com/getAlby/hub/logger"
"github.com/getAlby/hub/nip47/models"
"github.com/nbd-wtf/go-nostr"
"github.com/sirupsen/logrus"
)
type settleHoldInvoiceParams struct {
Preimage string `json:"preimage"`
}
type settleHoldInvoiceResponse struct{}
func (controller *nip47Controller) HandleSettleHoldInvoiceEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, appId uint, publishResponse func(*models.Response, nostr.Tags)) {
settleHoldInvoiceParams := &settleHoldInvoiceParams{}
decodeErrResp := decodeRequest(nip47Request, settleHoldInvoiceParams)
if decodeErrResp != nil {
publishResponse(decodeErrResp, nostr.Tags{})
return
}
logger.Logger.WithFields(logrus.Fields{
"requestEventId": requestEventId,
"appId": appId,
"preimage": settleHoldInvoiceParams.Preimage,
}).Info("Settling hold invoice")
_, err := controller.transactionsService.SettleHoldInvoice(ctx, settleHoldInvoiceParams.Preimage, controller.lnClient)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"request_event_id": requestEventId,
"appId": appId,
"preimage": settleHoldInvoiceParams.Preimage,
}).WithError(err).Error("Failed to settle hold invoice")
publishResponse(&models.Response{
ResultType: nip47Request.Method,
Error: mapNip47Error(err),
}, nostr.Tags{})
return
}
publishResponse(&models.Response{
ResultType: nip47Request.Method,
Result: &settleHoldInvoiceResponse{},
}, nostr.Tags{})
}

View file

@ -0,0 +1,151 @@
package controllers
import (
"context"
"encoding/json"
"testing"
"github.com/nbd-wtf/go-nostr"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"time"
"github.com/getAlby/hub/constants"
"github.com/getAlby/hub/db"
"crypto/sha256"
"encoding/hex"
"github.com/getAlby/hub/nip47/models"
"github.com/getAlby/hub/tests"
)
const nip47SettleHoldInvoiceJson = `
{
"method": "settle_hold_invoice",
"params": {
"preimage": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
}
}
`
const testSettlePreimage = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
const testSettlePaymentHash = "b7e060a60bb7a82f536a73c17bde37a1b6cf5769ee4a8325bff76c55a95b6aa4"
type settleHoldInvoiceTestSetup struct {
ctx context.Context
svc *tests.TestService
nip47Request *models.Request
app *db.App
dbRequestEvent *db.RequestEvent
publishCalled bool
response *models.Response
}
func setupSettleHoldInvoiceTest(t *testing.T, preimage string, paymentHashToCreate string, initialTransactionState string) *settleHoldInvoiceTestSetup {
ctx := context.TODO()
svc, err := tests.CreateTestService(t)
require.NoError(t, err)
nip47Request := &models.Request{}
requestJson := `
{
"method": "settle_hold_invoice",
"params": {
"preimage": "` + preimage + `"
}
}
`
err = json.Unmarshal([]byte(requestJson), nip47Request)
require.NoError(t, err)
app, _, err := tests.CreateApp(svc)
require.NoError(t, err)
appPermission := &db.AppPermission{
AppId: app.ID,
Scope: constants.MAKE_INVOICE_SCOPE,
}
err = svc.DB.Create(appPermission).Error
require.NoError(t, err)
if paymentHashToCreate != "" && initialTransactionState != "" {
expiresAtVar := time.Now().Add(1 * time.Hour)
appIDForTx := app.ID
holdInvoice := &db.Transaction{
AppId: &appIDForTx,
Type: constants.TRANSACTION_TYPE_INCOMING,
State: initialTransactionState,
PaymentHash: paymentHashToCreate,
AmountMsat: 1000,
ExpiresAt: &expiresAtVar,
}
err = svc.DB.Create(holdInvoice).Error
require.NoError(t, err)
}
dbRequestEvent := &db.RequestEvent{
AppId: &app.ID,
}
err = svc.DB.Create(&dbRequestEvent).Error
require.NoError(t, err)
setup := &settleHoldInvoiceTestSetup{
ctx: ctx,
svc: svc,
nip47Request: nip47Request,
app: app,
dbRequestEvent: dbRequestEvent,
}
return setup
}
func (s *settleHoldInvoiceTestSetup) TearDown() {
s.svc.Remove()
}
func (s *settleHoldInvoiceTestSetup) PublishResponse(response *models.Response, tags nostr.Tags) {
s.publishCalled = true
s.response = response
}
func TestHandleSettleHoldInvoiceEvent(t *testing.T) {
preimageBytesForCheck, err := hex.DecodeString(testSettlePreimage)
require.NoError(t, err)
calculatedHashBytesForCheck := sha256.Sum256(preimageBytesForCheck)
calculatedPaymentHashForCheck := hex.EncodeToString(calculatedHashBytesForCheck[:])
assert.Equal(t, testSettlePaymentHash, calculatedPaymentHashForCheck)
setup := setupSettleHoldInvoiceTest(t, testSettlePreimage, testSettlePaymentHash, constants.TRANSACTION_STATE_ACCEPTED)
defer setup.TearDown()
controller := NewTestNip47Controller(setup.svc)
controller.HandleSettleHoldInvoiceEvent(setup.ctx, setup.nip47Request, setup.dbRequestEvent.ID, *setup.dbRequestEvent.AppId, setup.PublishResponse)
assert.True(t, setup.publishCalled)
assert.Nil(t, setup.response.Error)
assert.Equal(t, &settleHoldInvoiceResponse{}, setup.response.Result)
var settledTx db.Transaction
err = setup.svc.DB.First(&settledTx, "payment_hash = ?", testSettlePaymentHash).Error
assert.NoError(t, err)
assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, settledTx.State)
assert.NotNil(t, settledTx.Preimage)
assert.Equal(t, testSettlePreimage, *settledTx.Preimage)
}
func TestHandleSettleHoldInvoiceEvent_InvalidPreimage(t *testing.T) {
invalidPreimage := "invalidpreimageinvalidpreimageinvalidpreimageinvalidpreimageinvalid"
setup := setupSettleHoldInvoiceTest(t, invalidPreimage, testSettlePaymentHash, constants.TRANSACTION_STATE_ACCEPTED)
defer setup.TearDown()
controller := NewTestNip47Controller(setup.svc)
controller.HandleSettleHoldInvoiceEvent(setup.ctx, setup.nip47Request, setup.dbRequestEvent.ID, *setup.dbRequestEvent.AppId, setup.PublishResponse)
assert.True(t, setup.publishCalled)
require.NotNil(t, setup.response.Error)
assert.Equal(t, constants.ERROR_INTERNAL, setup.response.Error.Code)
}

View file

@ -3,7 +3,6 @@ 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,10 +37,7 @@ func (controller *nip47Controller) HandleSignMessageEvent(ctx context.Context, n
}).WithError(err).Error("Failed to sign message")
publishResponse(&models.Response{
ResultType: nip47Request.Method,
Error: &models.Error{
Code: constants.ERROR_INTERNAL,
Message: err.Error(),
},
Error: mapNip47Error(err),
}, nostr.Tags{})
return
}

View file

@ -235,7 +235,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela
nip47Response = &models.Response{
Error: &models.Error{
Code: constants.ERROR_INTERNAL,
Code: constants.ERROR_BAD_REQUEST,
Message: fmt.Sprintf("failed to decrypt: %s", decryptionErr.Error()),
},
}
@ -425,6 +425,15 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela
case models.CREATE_CONNECTION_METHOD:
controller.
HandleCreateConnectionEvent(ctx, nip47Request, requestEvent.ID, publishResponse)
case models.MAKE_HOLD_INVOICE_METHOD:
controller.
HandleMakeHoldInvoiceEvent(ctx, nip47Request, requestEvent.ID, app.ID, publishResponse)
case models.CANCEL_HOLD_INVOICE_METHOD:
controller.
HandleCancelHoldInvoiceEvent(ctx, nip47Request, requestEvent.ID, app.ID, publishResponse)
case models.SETTLE_HOLD_INVOICE_METHOD:
controller.
HandleSettleHoldInvoiceEvent(ctx, nip47Request, requestEvent.ID, app.ID, publishResponse)
default:
publishResponse(&models.Response{
ResultType: nip47Request.Method,

View file

@ -665,6 +665,6 @@ func doTestHandleResponse_EncryptionTagDoesNotMatchPayload(t *testing.T, svc *te
assert.NoError(t, err)
assert.Nil(t, unmarshalledResponse.Result)
// assert.Equal(t, models.GET_INFO_METHOD, unmarshalledResponse.ResultType)
assert.Equal(t, constants.ERROR_INTERNAL, unmarshalledResponse.Error.Code)
assert.Equal(t, constants.ERROR_BAD_REQUEST, unmarshalledResponse.Error.Code)
assert.Contains(t, unmarshalledResponse.Error.Message, "failed to decrypt:")
}

View file

@ -12,18 +12,21 @@ const (
NOTIFICATION_KIND = 23197
// request methods
PAY_INVOICE_METHOD = "pay_invoice"
GET_BALANCE_METHOD = "get_balance"
GET_BUDGET_METHOD = "get_budget"
GET_INFO_METHOD = "get_info"
MAKE_INVOICE_METHOD = "make_invoice"
LOOKUP_INVOICE_METHOD = "lookup_invoice"
LIST_TRANSACTIONS_METHOD = "list_transactions"
PAY_KEYSEND_METHOD = "pay_keysend"
MULTI_PAY_INVOICE_METHOD = "multi_pay_invoice"
MULTI_PAY_KEYSEND_METHOD = "multi_pay_keysend"
SIGN_MESSAGE_METHOD = "sign_message"
CREATE_CONNECTION_METHOD = "create_connection"
PAY_INVOICE_METHOD = "pay_invoice"
GET_BALANCE_METHOD = "get_balance"
GET_BUDGET_METHOD = "get_budget"
GET_INFO_METHOD = "get_info"
MAKE_INVOICE_METHOD = "make_invoice"
LOOKUP_INVOICE_METHOD = "lookup_invoice"
LIST_TRANSACTIONS_METHOD = "list_transactions"
PAY_KEYSEND_METHOD = "pay_keysend"
MULTI_PAY_INVOICE_METHOD = "multi_pay_invoice"
MULTI_PAY_KEYSEND_METHOD = "multi_pay_keysend"
SIGN_MESSAGE_METHOD = "sign_message"
CREATE_CONNECTION_METHOD = "create_connection"
MAKE_HOLD_INVOICE_METHOD = "make_hold_invoice"
CANCEL_HOLD_INVOICE_METHOD = "cancel_hold_invoice"
SETTLE_HOLD_INVOICE_METHOD = "settle_hold_invoice"
)
type Transaction struct {
@ -39,6 +42,7 @@ type Transaction struct {
CreatedAt int64 `json:"created_at"`
ExpiresAt *int64 `json:"expires_at"`
SettledAt *int64 `json:"settled_at"`
SettleDeadline *uint32 `json:"settle_deadline"` // block number for accepted hold invoices
Metadata interface{} `json:"metadata,omitempty"`
}

View file

@ -49,5 +49,6 @@ func ToNip47Transaction(transaction *transactions.Transaction) *Transaction {
ExpiresAt: expiresAt,
SettledAt: settledAt,
Metadata: metadata,
SettleDeadline: transaction.SettleDeadline,
}
}

View file

@ -8,8 +8,9 @@ type Notification struct {
}
const (
PAYMENT_RECEIVED_NOTIFICATION = "payment_received"
PAYMENT_SENT_NOTIFICATION = "payment_sent"
PAYMENT_RECEIVED_NOTIFICATION = "payment_received"
PAYMENT_SENT_NOTIFICATION = "payment_sent"
HOLD_INVOICE_ACCEPTED_NOTIFICATION = "hold_invoice_accepted"
)
type PaymentSentNotification struct {
@ -19,3 +20,7 @@ type PaymentSentNotification struct {
type PaymentReceivedNotification struct {
models.Transaction
}
type HoldInvoiceAcceptedNotification struct {
models.Transaction
}

View file

@ -70,6 +70,24 @@ func (notifier *Nip47Notifier) ConsumeEvent(ctx context.Context, event *events.E
Notification: notification,
NotificationType: PAYMENT_SENT_NOTIFICATION,
}, nostr.Tags{}, transaction.AppId)
case "nwc_hold_invoice_accepted":
dbTransaction, ok := event.Properties.(*db.Transaction)
if !ok {
logger.Logger.WithField("event", event).Error("Failed to cast event properties to db.Transaction for hold invoice accepted")
return
}
nip47Transaction := models.ToNip47Transaction(dbTransaction)
notification := HoldInvoiceAcceptedNotification{
Transaction: *nip47Transaction,
}
notifier.notifySubscribers(ctx, &Notification{
Notification: notification,
NotificationType: HOLD_INVOICE_ACCEPTED_NOTIFICATION,
}, nostr.Tags{}, dbTransaction.AppId)
}
}

View file

@ -119,7 +119,7 @@ func scopeToRequestMethods(scope string) []string {
case constants.GET_INFO_SCOPE:
return []string{models.GET_INFO_METHOD}
case constants.MAKE_INVOICE_SCOPE:
return []string{models.MAKE_INVOICE_METHOD}
return []string{models.MAKE_INVOICE_METHOD, models.MAKE_HOLD_INVOICE_METHOD, models.SETTLE_HOLD_INVOICE_METHOD, models.CANCEL_HOLD_INVOICE_METHOD}
case constants.LOOKUP_INVOICE_SCOPE:
return []string{models.LOOKUP_INVOICE_METHOD}
case constants.LIST_TRANSACTIONS_SCOPE:
@ -165,6 +165,8 @@ func RequestMethodToScope(requestMethod string) (string, error) {
return constants.LIST_TRANSACTIONS_SCOPE, nil
case models.SIGN_MESSAGE_METHOD:
return constants.SIGN_MESSAGE_SCOPE, nil
case models.MAKE_HOLD_INVOICE_METHOD, models.SETTLE_HOLD_INVOICE_METHOD, models.CANCEL_HOLD_INVOICE_METHOD:
return constants.MAKE_INVOICE_SCOPE, nil
case models.CREATE_CONNECTION_METHOD:
return constants.SUPERUSER_SCOPE, nil
}

View file

@ -66,6 +66,16 @@ var MockLNClientTransactions = []lnclient.Transaction{
}
var MockLNClientTransaction = &MockLNClientTransactions[0]
var MockLNClientHoldTransaction = &lnclient.Transaction{
Type: "incoming",
Invoice: "lntb10n1p5zg5p7dqud4hkx6eqdphkcepqd9h8vmmfvdjsnp4qw988hn4lhpu0my4rf0qkraft3wdx5aa0jnjmusgd23z3s0e9qv62pp5yaulxt6x83u4u0x2pck5pyg7fdxhjsd65c9lmu2a9r05qh0cgl6ssp5x57tsnnuc9hr99a9xzg5ylqma5fwvckxa50jqqay5zykqp83h9kq9qyysgqcqpcxqxf92hqqxh0avuskdnuzkk7mxslsdwem3qq3sf79a4ypmx0hax3rupp043yhv97h25vaarj0xrlcg2fdfdhpztsthettskyaylrz6vweztn0twqqv7kxmr",
Description: "mock hold invoice",
DescriptionHash: "",
Preimage: "4aa083cad11038359b4f614f3a3d6a8298ae17d5275412bc3eca4f5f4d27f2d4",
PaymentHash: "2779f32f463c795e3cca0e2d40911e4b4d7941baa60bfdf15d28df405df847f5",
Amount: 2000,
}
type MockLn struct {
PayInvoiceResponses []*lnclient.PayInvoiceResponse
PayInvoiceErrors []error
@ -106,6 +116,18 @@ func (mln *MockLn) MakeInvoice(ctx context.Context, amount int64, description st
return MockLNClientTransaction, nil
}
func (mln *MockLn) MakeHoldInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64, paymentHash string) (transaction *lnclient.Transaction, err error) {
return MockLNClientHoldTransaction, nil
}
func (mln *MockLn) SettleHoldInvoice(ctx context.Context, preimage string) (err error) {
return nil
}
func (mln *MockLn) CancelHoldInvoice(ctx context.Context, paymentHash string) (err error) {
return nil
}
func (mln *MockLn) LookupInvoice(ctx context.Context, paymentHash string) (transaction *lnclient.Transaction, err error) {
if mln.MockTransaction != nil {
return mln.MockTransaction, nil

View file

@ -1,12 +1,28 @@
// Code generated by mockery v2.53.2. DO NOT EDIT.
// Code generated by mockery; DO NOT EDIT.
// github.com/vektra/mockery
// template: testify
package mocks
import (
config "github.com/getAlby/hub/config"
"github.com/getAlby/hub/config"
mock "github.com/stretchr/testify/mock"
)
// NewMockConfig creates a new instance of MockConfig. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockConfig(t interface {
mock.TestingT
Cleanup(func())
}) *MockConfig {
mock := &MockConfig{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
// MockConfig is an autogenerated mock type for the Config type
type MockConfig struct {
mock.Mock
@ -20,21 +36,20 @@ func (_m *MockConfig) EXPECT() *MockConfig_Expecter {
return &MockConfig_Expecter{mock: &_m.Mock}
}
// ChangeUnlockPassword provides a mock function with given fields: currentUnlockPassword, newUnlockPassword
func (_m *MockConfig) ChangeUnlockPassword(currentUnlockPassword string, newUnlockPassword string) error {
ret := _m.Called(currentUnlockPassword, newUnlockPassword)
// ChangeUnlockPassword provides a mock function for the type MockConfig
func (_mock *MockConfig) ChangeUnlockPassword(currentUnlockPassword string, newUnlockPassword string) error {
ret := _mock.Called(currentUnlockPassword, newUnlockPassword)
if len(ret) == 0 {
panic("no return value specified for ChangeUnlockPassword")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(currentUnlockPassword, newUnlockPassword)
if returnFunc, ok := ret.Get(0).(func(string, string) error); ok {
r0 = returnFunc(currentUnlockPassword, newUnlockPassword)
} else {
r0 = ret.Error(0)
}
return r0
}
@ -44,8 +59,8 @@ type MockConfig_ChangeUnlockPassword_Call struct {
}
// ChangeUnlockPassword is a helper method to define mock.On call
// - currentUnlockPassword string
// - newUnlockPassword string
// - currentUnlockPassword
// - newUnlockPassword
func (_e *MockConfig_Expecter) ChangeUnlockPassword(currentUnlockPassword interface{}, newUnlockPassword interface{}) *MockConfig_ChangeUnlockPassword_Call {
return &MockConfig_ChangeUnlockPassword_Call{Call: _e.mock.On("ChangeUnlockPassword", currentUnlockPassword, newUnlockPassword)}
}
@ -57,31 +72,30 @@ func (_c *MockConfig_ChangeUnlockPassword_Call) Run(run func(currentUnlockPasswo
return _c
}
func (_c *MockConfig_ChangeUnlockPassword_Call) Return(_a0 error) *MockConfig_ChangeUnlockPassword_Call {
_c.Call.Return(_a0)
func (_c *MockConfig_ChangeUnlockPassword_Call) Return(err error) *MockConfig_ChangeUnlockPassword_Call {
_c.Call.Return(err)
return _c
}
func (_c *MockConfig_ChangeUnlockPassword_Call) RunAndReturn(run func(string, string) error) *MockConfig_ChangeUnlockPassword_Call {
func (_c *MockConfig_ChangeUnlockPassword_Call) RunAndReturn(run func(currentUnlockPassword string, newUnlockPassword string) error) *MockConfig_ChangeUnlockPassword_Call {
_c.Call.Return(run)
return _c
}
// CheckUnlockPassword provides a mock function with given fields: password
func (_m *MockConfig) CheckUnlockPassword(password string) bool {
ret := _m.Called(password)
// CheckUnlockPassword provides a mock function for the type MockConfig
func (_mock *MockConfig) CheckUnlockPassword(password string) bool {
ret := _mock.Called(password)
if len(ret) == 0 {
panic("no return value specified for CheckUnlockPassword")
}
var r0 bool
if rf, ok := ret.Get(0).(func(string) bool); ok {
r0 = rf(password)
if returnFunc, ok := ret.Get(0).(func(string) bool); ok {
r0 = returnFunc(password)
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
@ -91,7 +105,7 @@ type MockConfig_CheckUnlockPassword_Call struct {
}
// CheckUnlockPassword is a helper method to define mock.On call
// - password string
// - password
func (_e *MockConfig_Expecter) CheckUnlockPassword(password interface{}) *MockConfig_CheckUnlockPassword_Call {
return &MockConfig_CheckUnlockPassword_Call{Call: _e.mock.On("CheckUnlockPassword", password)}
}
@ -103,19 +117,19 @@ func (_c *MockConfig_CheckUnlockPassword_Call) Run(run func(password string)) *M
return _c
}
func (_c *MockConfig_CheckUnlockPassword_Call) Return(_a0 bool) *MockConfig_CheckUnlockPassword_Call {
_c.Call.Return(_a0)
func (_c *MockConfig_CheckUnlockPassword_Call) Return(b bool) *MockConfig_CheckUnlockPassword_Call {
_c.Call.Return(b)
return _c
}
func (_c *MockConfig_CheckUnlockPassword_Call) RunAndReturn(run func(string) bool) *MockConfig_CheckUnlockPassword_Call {
func (_c *MockConfig_CheckUnlockPassword_Call) RunAndReturn(run func(password string) bool) *MockConfig_CheckUnlockPassword_Call {
_c.Call.Return(run)
return _c
}
// Get provides a mock function with given fields: key, encryptionKey
func (_m *MockConfig) Get(key string, encryptionKey string) (string, error) {
ret := _m.Called(key, encryptionKey)
// Get provides a mock function for the type MockConfig
func (_mock *MockConfig) Get(key string, encryptionKey string) (string, error) {
ret := _mock.Called(key, encryptionKey)
if len(ret) == 0 {
panic("no return value specified for Get")
@ -123,21 +137,19 @@ func (_m *MockConfig) Get(key string, encryptionKey string) (string, error) {
var r0 string
var r1 error
if rf, ok := ret.Get(0).(func(string, string) (string, error)); ok {
return rf(key, encryptionKey)
if returnFunc, ok := ret.Get(0).(func(string, string) (string, error)); ok {
return returnFunc(key, encryptionKey)
}
if rf, ok := ret.Get(0).(func(string, string) string); ok {
r0 = rf(key, encryptionKey)
if returnFunc, ok := ret.Get(0).(func(string, string) string); ok {
r0 = returnFunc(key, encryptionKey)
} else {
r0 = ret.Get(0).(string)
}
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(key, encryptionKey)
if returnFunc, ok := ret.Get(1).(func(string, string) error); ok {
r1 = returnFunc(key, encryptionKey)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
@ -147,8 +159,8 @@ type MockConfig_Get_Call struct {
}
// Get is a helper method to define mock.On call
// - key string
// - encryptionKey string
// - key
// - encryptionKey
func (_e *MockConfig_Expecter) Get(key interface{}, encryptionKey interface{}) *MockConfig_Get_Call {
return &MockConfig_Get_Call{Call: _e.mock.On("Get", key, encryptionKey)}
}
@ -160,31 +172,30 @@ func (_c *MockConfig_Get_Call) Run(run func(key string, encryptionKey string)) *
return _c
}
func (_c *MockConfig_Get_Call) Return(_a0 string, _a1 error) *MockConfig_Get_Call {
_c.Call.Return(_a0, _a1)
func (_c *MockConfig_Get_Call) Return(s string, err error) *MockConfig_Get_Call {
_c.Call.Return(s, err)
return _c
}
func (_c *MockConfig_Get_Call) RunAndReturn(run func(string, string) (string, error)) *MockConfig_Get_Call {
func (_c *MockConfig_Get_Call) RunAndReturn(run func(key string, encryptionKey string) (string, error)) *MockConfig_Get_Call {
_c.Call.Return(run)
return _c
}
// GetCurrency provides a mock function with no fields
func (_m *MockConfig) GetCurrency() string {
ret := _m.Called()
// GetCurrency provides a mock function for the type MockConfig
func (_mock *MockConfig) GetCurrency() string {
ret := _mock.Called()
if len(ret) == 0 {
panic("no return value specified for GetCurrency")
}
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
if returnFunc, ok := ret.Get(0).(func() string); ok {
r0 = returnFunc()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
@ -205,8 +216,8 @@ func (_c *MockConfig_GetCurrency_Call) Run(run func()) *MockConfig_GetCurrency_C
return _c
}
func (_c *MockConfig_GetCurrency_Call) Return(_a0 string) *MockConfig_GetCurrency_Call {
_c.Call.Return(_a0)
func (_c *MockConfig_GetCurrency_Call) Return(s string) *MockConfig_GetCurrency_Call {
_c.Call.Return(s)
return _c
}
@ -215,23 +226,22 @@ func (_c *MockConfig_GetCurrency_Call) RunAndReturn(run func() string) *MockConf
return _c
}
// GetEnv provides a mock function with no fields
func (_m *MockConfig) GetEnv() *config.AppConfig {
ret := _m.Called()
// GetEnv provides a mock function for the type MockConfig
func (_mock *MockConfig) GetEnv() *config.AppConfig {
ret := _mock.Called()
if len(ret) == 0 {
panic("no return value specified for GetEnv")
}
var r0 *config.AppConfig
if rf, ok := ret.Get(0).(func() *config.AppConfig); ok {
r0 = rf()
if returnFunc, ok := ret.Get(0).(func() *config.AppConfig); ok {
r0 = returnFunc()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*config.AppConfig)
}
}
return r0
}
@ -252,8 +262,8 @@ func (_c *MockConfig_GetEnv_Call) Run(run func()) *MockConfig_GetEnv_Call {
return _c
}
func (_c *MockConfig_GetEnv_Call) Return(_a0 *config.AppConfig) *MockConfig_GetEnv_Call {
_c.Call.Return(_a0)
func (_c *MockConfig_GetEnv_Call) Return(appConfig *config.AppConfig) *MockConfig_GetEnv_Call {
_c.Call.Return(appConfig)
return _c
}
@ -262,21 +272,20 @@ func (_c *MockConfig_GetEnv_Call) RunAndReturn(run func() *config.AppConfig) *Mo
return _c
}
// GetJWTSecret provides a mock function with no fields
func (_m *MockConfig) GetJWTSecret() string {
ret := _m.Called()
// GetJWTSecret provides a mock function for the type MockConfig
func (_mock *MockConfig) GetJWTSecret() string {
ret := _mock.Called()
if len(ret) == 0 {
panic("no return value specified for GetJWTSecret")
}
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
if returnFunc, ok := ret.Get(0).(func() string); ok {
r0 = returnFunc()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
@ -297,8 +306,8 @@ func (_c *MockConfig_GetJWTSecret_Call) Run(run func()) *MockConfig_GetJWTSecret
return _c
}
func (_c *MockConfig_GetJWTSecret_Call) Return(_a0 string) *MockConfig_GetJWTSecret_Call {
_c.Call.Return(_a0)
func (_c *MockConfig_GetJWTSecret_Call) Return(s string) *MockConfig_GetJWTSecret_Call {
_c.Call.Return(s)
return _c
}
@ -307,21 +316,20 @@ func (_c *MockConfig_GetJWTSecret_Call) RunAndReturn(run func() string) *MockCon
return _c
}
// GetNetwork provides a mock function with no fields
func (_m *MockConfig) GetNetwork() string {
ret := _m.Called()
// GetNetwork provides a mock function for the type MockConfig
func (_mock *MockConfig) GetNetwork() string {
ret := _mock.Called()
if len(ret) == 0 {
panic("no return value specified for GetNetwork")
}
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
if returnFunc, ok := ret.Get(0).(func() string); ok {
r0 = returnFunc()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
@ -342,8 +350,8 @@ func (_c *MockConfig_GetNetwork_Call) Run(run func()) *MockConfig_GetNetwork_Cal
return _c
}
func (_c *MockConfig_GetNetwork_Call) Return(_a0 string) *MockConfig_GetNetwork_Call {
_c.Call.Return(_a0)
func (_c *MockConfig_GetNetwork_Call) Return(s string) *MockConfig_GetNetwork_Call {
_c.Call.Return(s)
return _c
}
@ -352,21 +360,20 @@ func (_c *MockConfig_GetNetwork_Call) RunAndReturn(run func() string) *MockConfi
return _c
}
// GetRelayUrl provides a mock function with no fields
func (_m *MockConfig) GetRelayUrl() string {
ret := _m.Called()
// GetRelayUrl provides a mock function for the type MockConfig
func (_mock *MockConfig) GetRelayUrl() string {
ret := _mock.Called()
if len(ret) == 0 {
panic("no return value specified for GetRelayUrl")
}
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
if returnFunc, ok := ret.Get(0).(func() string); ok {
r0 = returnFunc()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
@ -387,8 +394,8 @@ func (_c *MockConfig_GetRelayUrl_Call) Run(run func()) *MockConfig_GetRelayUrl_C
return _c
}
func (_c *MockConfig_GetRelayUrl_Call) Return(_a0 string) *MockConfig_GetRelayUrl_Call {
_c.Call.Return(_a0)
func (_c *MockConfig_GetRelayUrl_Call) Return(s string) *MockConfig_GetRelayUrl_Call {
_c.Call.Return(s)
return _c
}
@ -397,21 +404,20 @@ func (_c *MockConfig_GetRelayUrl_Call) RunAndReturn(run func() string) *MockConf
return _c
}
// SaveUnlockPasswordCheck provides a mock function with given fields: encryptionKey
func (_m *MockConfig) SaveUnlockPasswordCheck(encryptionKey string) error {
ret := _m.Called(encryptionKey)
// SaveUnlockPasswordCheck provides a mock function for the type MockConfig
func (_mock *MockConfig) SaveUnlockPasswordCheck(encryptionKey string) error {
ret := _mock.Called(encryptionKey)
if len(ret) == 0 {
panic("no return value specified for SaveUnlockPasswordCheck")
}
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(encryptionKey)
if returnFunc, ok := ret.Get(0).(func(string) error); ok {
r0 = returnFunc(encryptionKey)
} else {
r0 = ret.Error(0)
}
return r0
}
@ -421,7 +427,7 @@ type MockConfig_SaveUnlockPasswordCheck_Call struct {
}
// SaveUnlockPasswordCheck is a helper method to define mock.On call
// - encryptionKey string
// - encryptionKey
func (_e *MockConfig_Expecter) SaveUnlockPasswordCheck(encryptionKey interface{}) *MockConfig_SaveUnlockPasswordCheck_Call {
return &MockConfig_SaveUnlockPasswordCheck_Call{Call: _e.mock.On("SaveUnlockPasswordCheck", encryptionKey)}
}
@ -433,31 +439,30 @@ func (_c *MockConfig_SaveUnlockPasswordCheck_Call) Run(run func(encryptionKey st
return _c
}
func (_c *MockConfig_SaveUnlockPasswordCheck_Call) Return(_a0 error) *MockConfig_SaveUnlockPasswordCheck_Call {
_c.Call.Return(_a0)
func (_c *MockConfig_SaveUnlockPasswordCheck_Call) Return(err error) *MockConfig_SaveUnlockPasswordCheck_Call {
_c.Call.Return(err)
return _c
}
func (_c *MockConfig_SaveUnlockPasswordCheck_Call) RunAndReturn(run func(string) error) *MockConfig_SaveUnlockPasswordCheck_Call {
func (_c *MockConfig_SaveUnlockPasswordCheck_Call) RunAndReturn(run func(encryptionKey string) error) *MockConfig_SaveUnlockPasswordCheck_Call {
_c.Call.Return(run)
return _c
}
// SetAutoUnlockPassword provides a mock function with given fields: unlockPassword
func (_m *MockConfig) SetAutoUnlockPassword(unlockPassword string) error {
ret := _m.Called(unlockPassword)
// SetAutoUnlockPassword provides a mock function for the type MockConfig
func (_mock *MockConfig) SetAutoUnlockPassword(unlockPassword string) error {
ret := _mock.Called(unlockPassword)
if len(ret) == 0 {
panic("no return value specified for SetAutoUnlockPassword")
}
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(unlockPassword)
if returnFunc, ok := ret.Get(0).(func(string) error); ok {
r0 = returnFunc(unlockPassword)
} else {
r0 = ret.Error(0)
}
return r0
}
@ -467,7 +472,7 @@ type MockConfig_SetAutoUnlockPassword_Call struct {
}
// SetAutoUnlockPassword is a helper method to define mock.On call
// - unlockPassword string
// - unlockPassword
func (_e *MockConfig_Expecter) SetAutoUnlockPassword(unlockPassword interface{}) *MockConfig_SetAutoUnlockPassword_Call {
return &MockConfig_SetAutoUnlockPassword_Call{Call: _e.mock.On("SetAutoUnlockPassword", unlockPassword)}
}
@ -479,31 +484,30 @@ func (_c *MockConfig_SetAutoUnlockPassword_Call) Run(run func(unlockPassword str
return _c
}
func (_c *MockConfig_SetAutoUnlockPassword_Call) Return(_a0 error) *MockConfig_SetAutoUnlockPassword_Call {
_c.Call.Return(_a0)
func (_c *MockConfig_SetAutoUnlockPassword_Call) Return(err error) *MockConfig_SetAutoUnlockPassword_Call {
_c.Call.Return(err)
return _c
}
func (_c *MockConfig_SetAutoUnlockPassword_Call) RunAndReturn(run func(string) error) *MockConfig_SetAutoUnlockPassword_Call {
func (_c *MockConfig_SetAutoUnlockPassword_Call) RunAndReturn(run func(unlockPassword string) error) *MockConfig_SetAutoUnlockPassword_Call {
_c.Call.Return(run)
return _c
}
// SetCurrency provides a mock function with given fields: value
func (_m *MockConfig) SetCurrency(value string) error {
ret := _m.Called(value)
// SetCurrency provides a mock function for the type MockConfig
func (_mock *MockConfig) SetCurrency(value string) error {
ret := _mock.Called(value)
if len(ret) == 0 {
panic("no return value specified for SetCurrency")
}
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(value)
if returnFunc, ok := ret.Get(0).(func(string) error); ok {
r0 = returnFunc(value)
} else {
r0 = ret.Error(0)
}
return r0
}
@ -513,7 +517,7 @@ type MockConfig_SetCurrency_Call struct {
}
// SetCurrency is a helper method to define mock.On call
// - value string
// - value
func (_e *MockConfig_Expecter) SetCurrency(value interface{}) *MockConfig_SetCurrency_Call {
return &MockConfig_SetCurrency_Call{Call: _e.mock.On("SetCurrency", value)}
}
@ -525,31 +529,30 @@ func (_c *MockConfig_SetCurrency_Call) Run(run func(value string)) *MockConfig_S
return _c
}
func (_c *MockConfig_SetCurrency_Call) Return(_a0 error) *MockConfig_SetCurrency_Call {
_c.Call.Return(_a0)
func (_c *MockConfig_SetCurrency_Call) Return(err error) *MockConfig_SetCurrency_Call {
_c.Call.Return(err)
return _c
}
func (_c *MockConfig_SetCurrency_Call) RunAndReturn(run func(string) error) *MockConfig_SetCurrency_Call {
func (_c *MockConfig_SetCurrency_Call) RunAndReturn(run func(value string) error) *MockConfig_SetCurrency_Call {
_c.Call.Return(run)
return _c
}
// SetIgnore provides a mock function with given fields: key, value, encryptionKey
func (_m *MockConfig) SetIgnore(key string, value string, encryptionKey string) error {
ret := _m.Called(key, value, encryptionKey)
// SetIgnore provides a mock function for the type MockConfig
func (_mock *MockConfig) SetIgnore(key string, value string, encryptionKey string) error {
ret := _mock.Called(key, value, encryptionKey)
if len(ret) == 0 {
panic("no return value specified for SetIgnore")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string) error); ok {
r0 = rf(key, value, encryptionKey)
if returnFunc, ok := ret.Get(0).(func(string, string, string) error); ok {
r0 = returnFunc(key, value, encryptionKey)
} else {
r0 = ret.Error(0)
}
return r0
}
@ -559,9 +562,9 @@ type MockConfig_SetIgnore_Call struct {
}
// SetIgnore is a helper method to define mock.On call
// - key string
// - value string
// - encryptionKey string
// - key
// - value
// - encryptionKey
func (_e *MockConfig_Expecter) SetIgnore(key interface{}, value interface{}, encryptionKey interface{}) *MockConfig_SetIgnore_Call {
return &MockConfig_SetIgnore_Call{Call: _e.mock.On("SetIgnore", key, value, encryptionKey)}
}
@ -573,31 +576,30 @@ func (_c *MockConfig_SetIgnore_Call) Run(run func(key string, value string, encr
return _c
}
func (_c *MockConfig_SetIgnore_Call) Return(_a0 error) *MockConfig_SetIgnore_Call {
_c.Call.Return(_a0)
func (_c *MockConfig_SetIgnore_Call) Return(err error) *MockConfig_SetIgnore_Call {
_c.Call.Return(err)
return _c
}
func (_c *MockConfig_SetIgnore_Call) RunAndReturn(run func(string, string, string) error) *MockConfig_SetIgnore_Call {
func (_c *MockConfig_SetIgnore_Call) RunAndReturn(run func(key string, value string, encryptionKey string) error) *MockConfig_SetIgnore_Call {
_c.Call.Return(run)
return _c
}
// SetUpdate provides a mock function with given fields: key, value, encryptionKey
func (_m *MockConfig) SetUpdate(key string, value string, encryptionKey string) error {
ret := _m.Called(key, value, encryptionKey)
// SetUpdate provides a mock function for the type MockConfig
func (_mock *MockConfig) SetUpdate(key string, value string, encryptionKey string) error {
ret := _mock.Called(key, value, encryptionKey)
if len(ret) == 0 {
panic("no return value specified for SetUpdate")
}
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string) error); ok {
r0 = rf(key, value, encryptionKey)
if returnFunc, ok := ret.Get(0).(func(string, string, string) error); ok {
r0 = returnFunc(key, value, encryptionKey)
} else {
r0 = ret.Error(0)
}
return r0
}
@ -607,9 +609,9 @@ type MockConfig_SetUpdate_Call struct {
}
// SetUpdate is a helper method to define mock.On call
// - key string
// - value string
// - encryptionKey string
// - key
// - value
// - encryptionKey
func (_e *MockConfig_Expecter) SetUpdate(key interface{}, value interface{}, encryptionKey interface{}) *MockConfig_SetUpdate_Call {
return &MockConfig_SetUpdate_Call{Call: _e.mock.On("SetUpdate", key, value, encryptionKey)}
}
@ -621,31 +623,30 @@ func (_c *MockConfig_SetUpdate_Call) Run(run func(key string, value string, encr
return _c
}
func (_c *MockConfig_SetUpdate_Call) Return(_a0 error) *MockConfig_SetUpdate_Call {
_c.Call.Return(_a0)
func (_c *MockConfig_SetUpdate_Call) Return(err error) *MockConfig_SetUpdate_Call {
_c.Call.Return(err)
return _c
}
func (_c *MockConfig_SetUpdate_Call) RunAndReturn(run func(string, string, string) error) *MockConfig_SetUpdate_Call {
func (_c *MockConfig_SetUpdate_Call) RunAndReturn(run func(key string, value string, encryptionKey string) error) *MockConfig_SetUpdate_Call {
_c.Call.Return(run)
return _c
}
// SetupCompleted provides a mock function with no fields
func (_m *MockConfig) SetupCompleted() bool {
ret := _m.Called()
// SetupCompleted provides a mock function for the type MockConfig
func (_mock *MockConfig) SetupCompleted() bool {
ret := _mock.Called()
if len(ret) == 0 {
panic("no return value specified for SetupCompleted")
}
var r0 bool
if rf, ok := ret.Get(0).(func() bool); ok {
r0 = rf()
if returnFunc, ok := ret.Get(0).(func() bool); ok {
r0 = returnFunc()
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
@ -666,8 +667,8 @@ func (_c *MockConfig_SetupCompleted_Call) Run(run func()) *MockConfig_SetupCompl
return _c
}
func (_c *MockConfig_SetupCompleted_Call) Return(_a0 bool) *MockConfig_SetupCompleted_Call {
_c.Call.Return(_a0)
func (_c *MockConfig_SetupCompleted_Call) Return(b bool) *MockConfig_SetupCompleted_Call {
_c.Call.Return(b)
return _c
}
@ -675,17 +676,3 @@ func (_c *MockConfig_SetupCompleted_Call) RunAndReturn(run func() bool) *MockCon
_c.Call.Return(run)
return _c
}
// NewMockConfig creates a new instance of MockConfig. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockConfig(t interface {
mock.TestingT
Cleanup(func())
}) *MockConfig {
mock := &MockConfig{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

File diff suppressed because it is too large Load diff

View file

@ -1,26 +1,35 @@
// Code generated by mockery v2.53.2. DO NOT EDIT.
// Code generated by mockery; DO NOT EDIT.
// github.com/vektra/mockery
// template: testify
package mocks
import (
alby "github.com/getAlby/hub/alby"
config "github.com/getAlby/hub/config"
events "github.com/getAlby/hub/events"
gorm "gorm.io/gorm"
keys "github.com/getAlby/hub/service/keys"
lnclient "github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/alby"
"github.com/getAlby/hub/config"
"github.com/getAlby/hub/events"
"github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/service/keys"
"github.com/getAlby/hub/swaps"
"github.com/getAlby/hub/transactions"
mock "github.com/stretchr/testify/mock"
swaps "github.com/getAlby/hub/swaps"
transactions "github.com/getAlby/hub/transactions"
"gorm.io/gorm"
)
// NewMockService creates a new instance of MockService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockService(t interface {
mock.TestingT
Cleanup(func())
}) *MockService {
mock := &MockService{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
// MockService is an autogenerated mock type for the Service type
type MockService struct {
mock.Mock
@ -34,23 +43,22 @@ func (_m *MockService) EXPECT() *MockService_Expecter {
return &MockService_Expecter{mock: &_m.Mock}
}
// GetAlbyOAuthSvc provides a mock function with no fields
func (_m *MockService) GetAlbyOAuthSvc() alby.AlbyOAuthService {
ret := _m.Called()
// GetAlbyOAuthSvc provides a mock function for the type MockService
func (_mock *MockService) GetAlbyOAuthSvc() alby.AlbyOAuthService {
ret := _mock.Called()
if len(ret) == 0 {
panic("no return value specified for GetAlbyOAuthSvc")
}
var r0 alby.AlbyOAuthService
if rf, ok := ret.Get(0).(func() alby.AlbyOAuthService); ok {
r0 = rf()
if returnFunc, ok := ret.Get(0).(func() alby.AlbyOAuthService); ok {
r0 = returnFunc()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(alby.AlbyOAuthService)
}
}
return r0
}
@ -71,8 +79,8 @@ func (_c *MockService_GetAlbyOAuthSvc_Call) Run(run func()) *MockService_GetAlby
return _c
}
func (_c *MockService_GetAlbyOAuthSvc_Call) Return(_a0 alby.AlbyOAuthService) *MockService_GetAlbyOAuthSvc_Call {
_c.Call.Return(_a0)
func (_c *MockService_GetAlbyOAuthSvc_Call) Return(albyOAuthService alby.AlbyOAuthService) *MockService_GetAlbyOAuthSvc_Call {
_c.Call.Return(albyOAuthService)
return _c
}
@ -81,23 +89,22 @@ func (_c *MockService_GetAlbyOAuthSvc_Call) RunAndReturn(run func() alby.AlbyOAu
return _c
}
// GetConfig provides a mock function with no fields
func (_m *MockService) GetConfig() config.Config {
ret := _m.Called()
// GetConfig provides a mock function for the type MockService
func (_mock *MockService) GetConfig() config.Config {
ret := _mock.Called()
if len(ret) == 0 {
panic("no return value specified for GetConfig")
}
var r0 config.Config
if rf, ok := ret.Get(0).(func() config.Config); ok {
r0 = rf()
if returnFunc, ok := ret.Get(0).(func() config.Config); ok {
r0 = returnFunc()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(config.Config)
}
}
return r0
}
@ -118,8 +125,8 @@ func (_c *MockService_GetConfig_Call) Run(run func()) *MockService_GetConfig_Cal
return _c
}
func (_c *MockService_GetConfig_Call) Return(_a0 config.Config) *MockService_GetConfig_Call {
_c.Call.Return(_a0)
func (_c *MockService_GetConfig_Call) Return(config1 config.Config) *MockService_GetConfig_Call {
_c.Call.Return(config1)
return _c
}
@ -128,23 +135,22 @@ func (_c *MockService_GetConfig_Call) RunAndReturn(run func() config.Config) *Mo
return _c
}
// GetDB provides a mock function with no fields
func (_m *MockService) GetDB() *gorm.DB {
ret := _m.Called()
// GetDB provides a mock function for the type MockService
func (_mock *MockService) GetDB() *gorm.DB {
ret := _mock.Called()
if len(ret) == 0 {
panic("no return value specified for GetDB")
}
var r0 *gorm.DB
if rf, ok := ret.Get(0).(func() *gorm.DB); ok {
r0 = rf()
if returnFunc, ok := ret.Get(0).(func() *gorm.DB); ok {
r0 = returnFunc()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*gorm.DB)
}
}
return r0
}
@ -165,8 +171,8 @@ func (_c *MockService_GetDB_Call) Run(run func()) *MockService_GetDB_Call {
return _c
}
func (_c *MockService_GetDB_Call) Return(_a0 *gorm.DB) *MockService_GetDB_Call {
_c.Call.Return(_a0)
func (_c *MockService_GetDB_Call) Return(dB *gorm.DB) *MockService_GetDB_Call {
_c.Call.Return(dB)
return _c
}
@ -175,23 +181,22 @@ func (_c *MockService_GetDB_Call) RunAndReturn(run func() *gorm.DB) *MockService
return _c
}
// GetEventPublisher provides a mock function with no fields
func (_m *MockService) GetEventPublisher() events.EventPublisher {
ret := _m.Called()
// GetEventPublisher provides a mock function for the type MockService
func (_mock *MockService) GetEventPublisher() events.EventPublisher {
ret := _mock.Called()
if len(ret) == 0 {
panic("no return value specified for GetEventPublisher")
}
var r0 events.EventPublisher
if rf, ok := ret.Get(0).(func() events.EventPublisher); ok {
r0 = rf()
if returnFunc, ok := ret.Get(0).(func() events.EventPublisher); ok {
r0 = returnFunc()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(events.EventPublisher)
}
}
return r0
}
@ -212,8 +217,8 @@ func (_c *MockService_GetEventPublisher_Call) Run(run func()) *MockService_GetEv
return _c
}
func (_c *MockService_GetEventPublisher_Call) Return(_a0 events.EventPublisher) *MockService_GetEventPublisher_Call {
_c.Call.Return(_a0)
func (_c *MockService_GetEventPublisher_Call) Return(eventPublisher events.EventPublisher) *MockService_GetEventPublisher_Call {
_c.Call.Return(eventPublisher)
return _c
}
@ -222,23 +227,22 @@ func (_c *MockService_GetEventPublisher_Call) RunAndReturn(run func() events.Eve
return _c
}
// GetKeys provides a mock function with no fields
func (_m *MockService) GetKeys() keys.Keys {
ret := _m.Called()
// GetKeys provides a mock function for the type MockService
func (_mock *MockService) GetKeys() keys.Keys {
ret := _mock.Called()
if len(ret) == 0 {
panic("no return value specified for GetKeys")
}
var r0 keys.Keys
if rf, ok := ret.Get(0).(func() keys.Keys); ok {
r0 = rf()
if returnFunc, ok := ret.Get(0).(func() keys.Keys); ok {
r0 = returnFunc()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(keys.Keys)
}
}
return r0
}
@ -259,8 +263,8 @@ func (_c *MockService_GetKeys_Call) Run(run func()) *MockService_GetKeys_Call {
return _c
}
func (_c *MockService_GetKeys_Call) Return(_a0 keys.Keys) *MockService_GetKeys_Call {
_c.Call.Return(_a0)
func (_c *MockService_GetKeys_Call) Return(keys1 keys.Keys) *MockService_GetKeys_Call {
_c.Call.Return(keys1)
return _c
}
@ -269,23 +273,22 @@ func (_c *MockService_GetKeys_Call) RunAndReturn(run func() keys.Keys) *MockServ
return _c
}
// GetLNClient provides a mock function with no fields
func (_m *MockService) GetLNClient() lnclient.LNClient {
ret := _m.Called()
// GetLNClient provides a mock function for the type MockService
func (_mock *MockService) GetLNClient() lnclient.LNClient {
ret := _mock.Called()
if len(ret) == 0 {
panic("no return value specified for GetLNClient")
}
var r0 lnclient.LNClient
if rf, ok := ret.Get(0).(func() lnclient.LNClient); ok {
r0 = rf()
if returnFunc, ok := ret.Get(0).(func() lnclient.LNClient); ok {
r0 = returnFunc()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(lnclient.LNClient)
}
}
return r0
}
@ -306,8 +309,8 @@ func (_c *MockService_GetLNClient_Call) Run(run func()) *MockService_GetLNClient
return _c
}
func (_c *MockService_GetLNClient_Call) Return(_a0 lnclient.LNClient) *MockService_GetLNClient_Call {
_c.Call.Return(_a0)
func (_c *MockService_GetLNClient_Call) Return(lNClient lnclient.LNClient) *MockService_GetLNClient_Call {
_c.Call.Return(lNClient)
return _c
}
@ -316,21 +319,20 @@ func (_c *MockService_GetLNClient_Call) RunAndReturn(run func() lnclient.LNClien
return _c
}
// GetStartupState provides a mock function with no fields
func (_m *MockService) GetStartupState() string {
ret := _m.Called()
// GetStartupState provides a mock function for the type MockService
func (_mock *MockService) GetStartupState() string {
ret := _mock.Called()
if len(ret) == 0 {
panic("no return value specified for GetStartupState")
}
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
if returnFunc, ok := ret.Get(0).(func() string); ok {
r0 = returnFunc()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
@ -351,8 +353,8 @@ func (_c *MockService_GetStartupState_Call) Run(run func()) *MockService_GetStar
return _c
}
func (_c *MockService_GetStartupState_Call) Return(_a0 string) *MockService_GetStartupState_Call {
_c.Call.Return(_a0)
func (_c *MockService_GetStartupState_Call) Return(s string) *MockService_GetStartupState_Call {
_c.Call.Return(s)
return _c
}
@ -361,23 +363,22 @@ func (_c *MockService_GetStartupState_Call) RunAndReturn(run func() string) *Moc
return _c
}
// GetSwapsService provides a mock function with no fields
func (_m *MockService) GetSwapsService() swaps.SwapsService {
ret := _m.Called()
// GetSwapsService provides a mock function for the type MockService
func (_mock *MockService) GetSwapsService() swaps.SwapsService {
ret := _mock.Called()
if len(ret) == 0 {
panic("no return value specified for GetSwapsService")
}
var r0 swaps.SwapsService
if rf, ok := ret.Get(0).(func() swaps.SwapsService); ok {
r0 = rf()
if returnFunc, ok := ret.Get(0).(func() swaps.SwapsService); ok {
r0 = returnFunc()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(swaps.SwapsService)
}
}
return r0
}
@ -398,8 +399,8 @@ func (_c *MockService_GetSwapsService_Call) Run(run func()) *MockService_GetSwap
return _c
}
func (_c *MockService_GetSwapsService_Call) Return(_a0 swaps.SwapsService) *MockService_GetSwapsService_Call {
_c.Call.Return(_a0)
func (_c *MockService_GetSwapsService_Call) Return(swapsService swaps.SwapsService) *MockService_GetSwapsService_Call {
_c.Call.Return(swapsService)
return _c
}
@ -408,23 +409,22 @@ func (_c *MockService_GetSwapsService_Call) RunAndReturn(run func() swaps.SwapsS
return _c
}
// GetTransactionsService provides a mock function with no fields
func (_m *MockService) GetTransactionsService() transactions.TransactionsService {
ret := _m.Called()
// GetTransactionsService provides a mock function for the type MockService
func (_mock *MockService) GetTransactionsService() transactions.TransactionsService {
ret := _mock.Called()
if len(ret) == 0 {
panic("no return value specified for GetTransactionsService")
}
var r0 transactions.TransactionsService
if rf, ok := ret.Get(0).(func() transactions.TransactionsService); ok {
r0 = rf()
if returnFunc, ok := ret.Get(0).(func() transactions.TransactionsService); ok {
r0 = returnFunc()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(transactions.TransactionsService)
}
}
return r0
}
@ -445,8 +445,8 @@ func (_c *MockService_GetTransactionsService_Call) Run(run func()) *MockService_
return _c
}
func (_c *MockService_GetTransactionsService_Call) Return(_a0 transactions.TransactionsService) *MockService_GetTransactionsService_Call {
_c.Call.Return(_a0)
func (_c *MockService_GetTransactionsService_Call) Return(transactionsService transactions.TransactionsService) *MockService_GetTransactionsService_Call {
_c.Call.Return(transactionsService)
return _c
}
@ -455,21 +455,20 @@ func (_c *MockService_GetTransactionsService_Call) RunAndReturn(run func() trans
return _c
}
// IsRelayReady provides a mock function with no fields
func (_m *MockService) IsRelayReady() bool {
ret := _m.Called()
// IsRelayReady provides a mock function for the type MockService
func (_mock *MockService) IsRelayReady() bool {
ret := _mock.Called()
if len(ret) == 0 {
panic("no return value specified for IsRelayReady")
}
var r0 bool
if rf, ok := ret.Get(0).(func() bool); ok {
r0 = rf()
if returnFunc, ok := ret.Get(0).(func() bool); ok {
r0 = returnFunc()
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
@ -490,8 +489,8 @@ func (_c *MockService_IsRelayReady_Call) Run(run func()) *MockService_IsRelayRea
return _c
}
func (_c *MockService_IsRelayReady_Call) Return(_a0 bool) *MockService_IsRelayReady_Call {
_c.Call.Return(_a0)
func (_c *MockService_IsRelayReady_Call) Return(b bool) *MockService_IsRelayReady_Call {
_c.Call.Return(b)
return _c
}
@ -500,9 +499,10 @@ func (_c *MockService_IsRelayReady_Call) RunAndReturn(run func() bool) *MockServ
return _c
}
// Shutdown provides a mock function with no fields
func (_m *MockService) Shutdown() {
_m.Called()
// Shutdown provides a mock function for the type MockService
func (_mock *MockService) Shutdown() {
_mock.Called()
return
}
// MockService_Shutdown_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Shutdown'
@ -532,21 +532,20 @@ func (_c *MockService_Shutdown_Call) RunAndReturn(run func()) *MockService_Shutd
return _c
}
// StartApp provides a mock function with given fields: encryptionKey
func (_m *MockService) StartApp(encryptionKey string) error {
ret := _m.Called(encryptionKey)
// StartApp provides a mock function for the type MockService
func (_mock *MockService) StartApp(encryptionKey string) error {
ret := _mock.Called(encryptionKey)
if len(ret) == 0 {
panic("no return value specified for StartApp")
}
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(encryptionKey)
if returnFunc, ok := ret.Get(0).(func(string) error); ok {
r0 = returnFunc(encryptionKey)
} else {
r0 = ret.Error(0)
}
return r0
}
@ -556,7 +555,7 @@ type MockService_StartApp_Call struct {
}
// StartApp is a helper method to define mock.On call
// - encryptionKey string
// - encryptionKey
func (_e *MockService_Expecter) StartApp(encryptionKey interface{}) *MockService_StartApp_Call {
return &MockService_StartApp_Call{Call: _e.mock.On("StartApp", encryptionKey)}
}
@ -568,31 +567,30 @@ func (_c *MockService_StartApp_Call) Run(run func(encryptionKey string)) *MockSe
return _c
}
func (_c *MockService_StartApp_Call) Return(_a0 error) *MockService_StartApp_Call {
_c.Call.Return(_a0)
func (_c *MockService_StartApp_Call) Return(err error) *MockService_StartApp_Call {
_c.Call.Return(err)
return _c
}
func (_c *MockService_StartApp_Call) RunAndReturn(run func(string) error) *MockService_StartApp_Call {
func (_c *MockService_StartApp_Call) RunAndReturn(run func(encryptionKey string) error) *MockService_StartApp_Call {
_c.Call.Return(run)
return _c
}
// StartAutoSwaps provides a mock function with no fields
func (_m *MockService) StartAutoSwaps() error {
ret := _m.Called()
// StartAutoSwaps provides a mock function for the type MockService
func (_mock *MockService) StartAutoSwaps() error {
ret := _mock.Called()
if len(ret) == 0 {
panic("no return value specified for StartAutoSwaps")
}
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
if returnFunc, ok := ret.Get(0).(func() error); ok {
r0 = returnFunc()
} else {
r0 = ret.Error(0)
}
return r0
}
@ -613,8 +611,8 @@ func (_c *MockService_StartAutoSwaps_Call) Run(run func()) *MockService_StartAut
return _c
}
func (_c *MockService_StartAutoSwaps_Call) Return(_a0 error) *MockService_StartAutoSwaps_Call {
_c.Call.Return(_a0)
func (_c *MockService_StartAutoSwaps_Call) Return(err error) *MockService_StartAutoSwaps_Call {
_c.Call.Return(err)
return _c
}
@ -623,9 +621,10 @@ func (_c *MockService_StartAutoSwaps_Call) RunAndReturn(run func() error) *MockS
return _c
}
// StopApp provides a mock function with no fields
func (_m *MockService) StopApp() {
_m.Called()
// StopApp provides a mock function for the type MockService
func (_mock *MockService) StopApp() {
_mock.Called()
return
}
// MockService_StopApp_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'StopApp'
@ -654,17 +653,3 @@ func (_c *MockService_StopApp_Call) RunAndReturn(run func()) *MockService_StopAp
_c.Run(run)
return _c
}
// NewMockService creates a new instance of MockService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockService(t interface {
mock.TestingT
Cleanup(func())
}) *MockService {
mock := &MockService{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View file

@ -0,0 +1,31 @@
package transactions
import (
"context"
"github.com/getAlby/hub/db"
"github.com/getAlby/hub/events"
)
type holdInvoiceUpdatedConsumer struct {
paymentHash string
settledChannel chan<- *db.Transaction
canceledChannel chan<- *db.Transaction
}
func newHoldInvoiceUpdatedConsumer(paymentHash string, settledChannel chan<- *db.Transaction, canceledChannel chan<- *db.Transaction) *holdInvoiceUpdatedConsumer {
return &holdInvoiceUpdatedConsumer{
paymentHash: paymentHash,
settledChannel: settledChannel,
canceledChannel: canceledChannel,
}
}
func (consumer *holdInvoiceUpdatedConsumer) ConsumeEvent(ctx context.Context, event *events.Event, globalProperties map[string]interface{}) {
if event.Event == "nwc_payment_received" && event.Properties.(*db.Transaction).PaymentHash == consumer.paymentHash {
consumer.settledChannel <- event.Properties.(*db.Transaction)
}
if event.Event == "nwc_hold_invoice_canceled" && event.Properties.(*db.Transaction).PaymentHash == consumer.paymentHash {
consumer.canceledChannel <- event.Properties.(*db.Transaction)
}
}

View file

@ -0,0 +1,93 @@
package transactions
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/getAlby/hub/constants"
"github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/tests"
)
func TestSelfHoldPaymentSettled(t *testing.T) {
ctx := context.TODO()
svc, err := tests.CreateTestService(t)
require.NoError(t, err)
defer svc.Remove()
preimage := tests.MockLNClientHoldTransaction.Preimage
paymentHash := tests.MockLNClientHoldTransaction.PaymentHash
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
transaction, err := transactionsService.MakeHoldInvoice(ctx, 1000, "Hold payment test", "", 0, paymentHash, nil, svc.LNClient, nil, nil)
require.NoError(t, err)
assert.True(t, transaction.Hold)
// use the pubkey from the decoded tests.MockLNClientHoldTransaction invoice
svc.LNClient.(*tests.MockLn).Pubkey = "038a73de75fdc3c7ec951a5e0b0fa95c5cd353bd7ca72df2086aa228c1f92819a5"
go func() {
result, err := transactionsService.SendPaymentSync(ctx, transaction.PaymentRequest, nil, nil, svc.LNClient, nil, nil, nil)
assert.NoError(t, err)
require.NotNil(t, result)
assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, result.State)
assert.Equal(t, true, result.SelfPayment)
assert.Equal(t, false, result.Hold)
}()
// wait for payment to be accepted
time.Sleep(100 * time.Millisecond)
settledTransaction, err := transactionsService.SettleHoldInvoice(ctx, preimage, svc.LNClient)
assert.NoError(t, err)
require.NotNil(t, settledTransaction)
assert.Equal(t, constants.TRANSACTION_STATE_SETTLED, settledTransaction.State)
assert.Equal(t, true, settledTransaction.SelfPayment)
assert.Equal(t, true, settledTransaction.Hold)
}
func TestSelfHoldPaymentCanceled(t *testing.T) {
ctx := context.TODO()
svc, err := tests.CreateTestService(t)
require.NoError(t, err)
defer svc.Remove()
paymentHash := tests.MockLNClientHoldTransaction.PaymentHash
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
transaction, err := transactionsService.MakeHoldInvoice(ctx, 1000, "Hold payment test", "", 0, paymentHash, nil, svc.LNClient, nil, nil)
require.NoError(t, err)
assert.True(t, transaction.Hold)
// use the pubkey from the decoded tests.MockLNClientHoldTransaction invoice
svc.LNClient.(*tests.MockLn).Pubkey = "038a73de75fdc3c7ec951a5e0b0fa95c5cd353bd7ca72df2086aa228c1f92819a5"
go func() {
result, err := transactionsService.SendPaymentSync(ctx, transaction.PaymentRequest, nil, nil, svc.LNClient, nil, nil, nil)
assert.ErrorIs(t, err, lnclient.NewHoldInvoiceCanceledError())
assert.Nil(t, result)
outgoingTransactionType := constants.TRANSACTION_TYPE_OUTGOING
failedOutgoingTransaction, err := transactionsService.LookupTransaction(ctx, paymentHash, &outgoingTransactionType, svc.LNClient, nil)
assert.Nil(t, err)
require.NotNil(t, failedOutgoingTransaction)
assert.Equal(t, constants.TRANSACTION_STATE_FAILED, failedOutgoingTransaction.State)
assert.Equal(t, true, failedOutgoingTransaction.SelfPayment)
assert.Equal(t, false, failedOutgoingTransaction.Hold)
}()
// wait for payment to be accepted
time.Sleep(100 * time.Millisecond)
err = transactionsService.CancelHoldInvoice(ctx, paymentHash, svc.LNClient)
assert.NoError(t, err)
incomingTransactionType := constants.TRANSACTION_TYPE_INCOMING
updatedHoldTransaction, err := transactionsService.LookupTransaction(ctx, paymentHash, &incomingTransactionType, svc.LNClient, nil)
assert.Nil(t, err)
assert.Equal(t, constants.TRANSACTION_STATE_FAILED, updatedHoldTransaction.State)
assert.Equal(t, true, updatedHoldTransaction.SelfPayment)
assert.Equal(t, true, updatedHoldTransaction.Hold)
time.Sleep(1 * time.Second)
}

View file

@ -40,6 +40,9 @@ type TransactionsService interface {
ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaidOutgoing bool, unpaidIncoming bool, transactionType *string, lnClient lnclient.LNClient, appId *uint, forceFilterByAppId bool) (transactions []Transaction, totalCount uint64, err error)
SendPaymentSync(ctx context.Context, payReq string, amountMsat *uint64, metadata map[string]interface{}, lnClient lnclient.LNClient, appId *uint, requestEventId *uint, timeoutSeconds *int64) (*Transaction, error)
SendKeysend(ctx context.Context, amount uint64, destination string, customRecords []lnclient.TLVRecord, preimage string, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error)
MakeHoldInvoice(ctx context.Context, amount uint64, description string, descriptionHash string, expiry uint64, paymentHash string, metadata map[string]interface{}, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error)
SettleHoldInvoice(ctx context.Context, preimage string, lnClient lnclient.LNClient) (*Transaction, error)
CancelHoldInvoice(ctx context.Context, paymentHash string, lnClient lnclient.LNClient) error
}
const (
@ -188,6 +191,60 @@ func (svc *transactionsService) MakeInvoice(ctx context.Context, amount uint64,
return &dbTransaction, nil
}
func (svc *transactionsService) MakeHoldInvoice(ctx context.Context, amount uint64, description string, descriptionHash string, expiry uint64, paymentHash string, metadata map[string]interface{}, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error) {
var err error
var metadataBytes []byte
if metadata != nil {
metadataBytes, err = json.Marshal(metadata)
if err != nil {
logger.Logger.WithError(err).Error("Failed to serialize metadata")
return nil, err
}
if len(metadataBytes) > constants.INVOICE_METADATA_MAX_LENGTH {
return nil, fmt.Errorf("encoded invoice metadata provided is too large. Limit: %d Received: %d", constants.INVOICE_METADATA_MAX_LENGTH, len(metadataBytes))
}
}
lnClientTransaction, err := lnClient.MakeHoldInvoice(ctx, int64(amount), description, descriptionHash, int64(expiry), paymentHash)
if err != nil {
logger.Logger.WithError(err).Error("Failed to create hold invoice via LN client")
return nil, err
}
var preimage *string
if lnClientTransaction.Preimage != "" {
preimage = &lnClientTransaction.Preimage
}
var expiresAt *time.Time
if lnClientTransaction.ExpiresAt != nil {
expiresAtValue := time.Unix(*lnClientTransaction.ExpiresAt, 0)
expiresAt = &expiresAtValue
}
dbTransaction := db.Transaction{
AppId: appId,
RequestEventId: requestEventId,
Type: constants.TRANSACTION_TYPE_INCOMING,
State: constants.TRANSACTION_STATE_PENDING,
AmountMsat: uint64(lnClientTransaction.Amount),
Description: description,
DescriptionHash: descriptionHash,
PaymentRequest: lnClientTransaction.Invoice,
PaymentHash: lnClientTransaction.PaymentHash,
ExpiresAt: expiresAt,
Preimage: preimage,
Metadata: datatypes.JSON(metadataBytes),
Hold: true,
}
err = svc.db.Create(&dbTransaction).Error
if err != nil {
logger.Logger.WithError(err).Error("Failed to create hold invoice DB transaction")
return nil, err
}
return &dbTransaction, nil
}
func (svc *transactionsService) SendPaymentSync(ctx context.Context, payReq string, amountMsat *uint64, metadata map[string]interface{}, lnClient lnclient.LNClient, appId *uint, requestEventId *uint, timeoutSeconds *int64) (*Transaction, error) {
var metadataBytes []byte
if metadata != nil {
@ -291,7 +348,7 @@ func (svc *transactionsService) SendPaymentSync(ctx context.Context, payReq stri
var response *lnclient.PayInvoiceResponse
if selfPayment {
response, err = svc.interceptSelfPayment(paymentRequest.PaymentHash)
response, err = svc.interceptSelfPayment(ctx, paymentRequest.PaymentHash, lnClient)
} else {
response, err = lnClient.SendPaymentSync(ctx, payReq, amountMsat, timeoutSeconds)
}
@ -430,7 +487,7 @@ func (svc *transactionsService) SendKeysend(ctx context.Context, amount uint64,
return nil, err
}
_, err = svc.interceptSelfPayment(paymentHash)
_, err = svc.interceptSelfPayment(ctx, paymentHash, lnClient)
if err == nil {
payKeysendResponse = &lnclient.PayKeysendResponse{
Fee: 0,
@ -740,6 +797,19 @@ func (svc *transactionsService) ConsumeEvent(ctx context.Context, event *events.
}).WithError(err).Error("Failed to execute DB transaction")
return
}
case "nwc_lnclient_hold_invoice_accepted":
lnClientTransaction, ok := event.Properties.(*lnclient.Transaction)
if !ok {
logger.Logger.WithField("event", event).Error("Failed to cast event properties for hold invoice accepted")
return
}
if lnClientTransaction.SettleDeadline == nil {
logger.Logger.WithField("event", event).Error("Transaction has no settle deadline")
return
}
svc.markHoldInvoiceAccepted(lnClientTransaction.PaymentHash, *lnClientTransaction.SettleDeadline, false)
case "nwc_lnclient_payment_sent":
lnClientTransaction, ok := event.Properties.(*lnclient.Transaction)
if !ok {
@ -802,7 +872,60 @@ func (svc *transactionsService) ConsumeEvent(ctx context.Context, event *events.
}
}
func (svc *transactionsService) interceptSelfPayment(paymentHash string) (*lnclient.PayInvoiceResponse, error) {
func (svc *transactionsService) markHoldInvoiceAccepted(paymentHash string, settleDeadline uint32, selfPayment bool) {
logger.Logger.WithFields(logrus.Fields{
"paymentHash": paymentHash,
"self_payment": selfPayment,
}).Info("Processing hold invoice accepted event")
var dbTransaction db.Transaction
err := svc.db.Transaction(func(tx *gorm.DB) error {
result := tx.Where("payment_hash = ? AND type = ? AND state = ?", paymentHash, constants.TRANSACTION_TYPE_INCOMING, constants.TRANSACTION_STATE_PENDING).First(&dbTransaction)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
logger.Logger.WithFields(logrus.Fields{
"paymentHash": paymentHash,
}).Warn("No corresponding pending incoming transaction found in DB for accepted hold invoice")
}
logger.Logger.WithFields(logrus.Fields{
"paymentHash": paymentHash,
}).WithError(result.Error).Error("Failed to query DB for accepted hold invoice")
return result.Error
}
err := tx.Model(&dbTransaction).UpdateColumns(map[string]interface{}{
"state": constants.TRANSACTION_STATE_ACCEPTED,
"self_payment": selfPayment,
"settle_deadline": settleDeadline,
}).Error
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"paymentHash": paymentHash,
"dbTxID": dbTransaction.ID,
}).WithError(err).Error("Failed to update hold invoice state to accepted in DB")
return err
}
logger.Logger.WithFields(logrus.Fields{
"paymentHash": paymentHash,
"dbTxID": dbTransaction.ID,
}).Info("Updated hold invoice state to accepted in DB")
return nil
})
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"paymentHash": paymentHash,
}).WithError(err).Error("Failed DB transaction for hold invoice accepted event")
} else {
svc.eventPublisher.Publish(&events.Event{
Event: "nwc_hold_invoice_accepted",
Properties: &dbTransaction,
})
}
}
func (svc *transactionsService) interceptSelfPayment(ctx context.Context, paymentHash string, lnClient lnclient.LNClient) (*lnclient.PayInvoiceResponse, error) {
logger.Logger.WithField("payment_hash", paymentHash).Debug("Intercepting self payment")
incomingTransaction := db.Transaction{}
result := svc.db.Limit(1).Find(&incomingTransaction, &db.Transaction{
@ -817,6 +940,11 @@ func (svc *transactionsService) interceptSelfPayment(paymentHash string) (*lncli
if result.RowsAffected == 0 {
return nil, NewNotFoundError()
}
if incomingTransaction.Hold {
return svc.interceptSelfHoldPayment(ctx, paymentHash, lnClient)
}
if incomingTransaction.Preimage == nil {
return nil, errors.New("preimage is not set on transaction. Self payments not supported")
}
@ -836,6 +964,51 @@ func (svc *transactionsService) interceptSelfPayment(paymentHash string) (*lncli
}, nil
}
func (svc *transactionsService) interceptSelfHoldPayment(ctx context.Context, paymentHash string, lnClient lnclient.LNClient) (*lnclient.PayInvoiceResponse, error) {
settledChannel := make(chan *db.Transaction)
canceledChannel := make(chan *db.Transaction)
holdInvoiceUpdatedConsumer := newHoldInvoiceUpdatedConsumer(paymentHash, settledChannel, canceledChannel)
svc.eventPublisher.RegisterSubscriber(holdInvoiceUpdatedConsumer)
clientInfo, err := lnClient.GetInfo(ctx)
if err != nil {
return nil, errors.New("failed to get client info")
}
if clientInfo.BlockHeight == 0 {
return nil, errors.New("invalid client block height")
}
fakeSettleDeadline := clientInfo.BlockHeight + 24
svc.markHoldInvoiceAccepted(paymentHash, fakeSettleDeadline, true)
select {
case settledTransaction := <-settledChannel:
logger.Logger.WithField("settled_transaction", settledTransaction).Info("self hold payment was settled")
if settledTransaction.Preimage == nil {
return nil, errors.New("preimage is not set on self hold payment")
}
return &lnclient.PayInvoiceResponse{
Preimage: *settledTransaction.Preimage,
Fee: 0,
}, nil
case canceledTransaction := <-canceledChannel:
logger.Logger.WithField("canceled_transaction", canceledTransaction).Info("self hold payment was canceled")
return nil, lnclient.NewHoldInvoiceCanceledError()
case <-time.After(50 * time.Second):
logger.Logger.WithFields(logrus.Fields{
"payment_hash": paymentHash,
}).Error("Timeout executing self payment for hold invoice")
}
svc.eventPublisher.RemoveSubscriber(holdInvoiceUpdatedConsumer)
return nil, lnclient.NewTimeoutError()
}
func (svc *transactionsService) validateCanPay(tx *gorm.DB, appId *uint, amount uint64, description string) error {
amountWithFeeReserve := amount + CalculateFeeReserveMsat(amount)
@ -985,6 +1158,128 @@ func (svc *transactionsService) getAppIdFromCustomRecords(customRecords []lnclie
return nil
}
func (svc *transactionsService) SettleHoldInvoice(ctx context.Context, preimage string, lnClient lnclient.LNClient) (*Transaction, error) {
if len(preimage) != 64 {
return nil, errors.New("invalid preimage format")
}
preimageBytes, err := hex.DecodeString(preimage)
if err != nil {
return nil, fmt.Errorf("invalid preimage hex: %w", err)
}
paymentHashBytes := sha256.Sum256(preimageBytes)
paymentHash := hex.EncodeToString(paymentHashBytes[:])
var dbTransaction db.Transaction
result := svc.db.Limit(1).Find(&dbTransaction, &db.Transaction{
Type: constants.TRANSACTION_TYPE_INCOMING,
State: constants.TRANSACTION_STATE_ACCEPTED,
PaymentHash: paymentHash,
})
if result.RowsAffected == 0 {
logger.Logger.WithField("payment_hash", paymentHash).Error("Failed to find accepted hold invoice")
return nil, errors.New("failed to find accepted hold invoice")
}
if !dbTransaction.SelfPayment {
err = lnClient.SettleHoldInvoice(ctx, preimage)
}
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"preimage": preimage,
}).WithError(err).Error("Failed to settle hold invoice via LN client")
// Don't mark DB as failed here, as the settle might succeed later or might have already succeeded.
return nil, err
}
var settledTransaction *db.Transaction
err = svc.db.Transaction(func(tx *gorm.DB) error {
var err error
settledTransaction, err = svc.markTransactionSettled(tx, &dbTransaction, preimage, 0, dbTransaction.SelfPayment)
return err
})
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"payment_hash": paymentHash,
"preimage": preimage,
}).WithError(err).Error("Failed DB transaction while settling hold invoice")
return nil, err
}
return settledTransaction, nil
}
func (svc *transactionsService) CancelHoldInvoice(ctx context.Context, paymentHash string, lnClient lnclient.LNClient) error {
var dbTransaction db.Transaction
result := svc.db.Limit(1).Find(&dbTransaction, &db.Transaction{
Type: constants.TRANSACTION_TYPE_INCOMING,
State: constants.TRANSACTION_STATE_ACCEPTED,
PaymentHash: paymentHash,
})
if result.RowsAffected == 0 {
logger.Logger.WithField("payment_hash", paymentHash).Error("Failed to find accepted hold invoice")
return NewNotFoundError()
}
if !dbTransaction.SelfPayment {
err := lnClient.CancelHoldInvoice(ctx, paymentHash)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"payment_hash": paymentHash,
}).WithError(err).Error("Failed to cancel hold invoice via LN client")
// Don't mark DB as failed here, cancellation might have already happened or might succeed later.
return err
}
}
err := svc.db.Transaction(func(tx *gorm.DB) error {
var dbTransaction db.Transaction
result := tx.Limit(1).Find(&dbTransaction, &db.Transaction{
Type: constants.TRANSACTION_TYPE_INCOMING,
State: constants.TRANSACTION_STATE_ACCEPTED,
PaymentHash: paymentHash,
})
if result.Error != nil {
logger.Logger.WithFields(logrus.Fields{
"payment_hash": paymentHash,
}).WithError(result.Error).Error("Failed to find accepted hold invoice in DB for cancellation")
return result.Error
}
if result.RowsAffected == 0 {
logger.Logger.WithFields(logrus.Fields{
"payment_hash": paymentHash,
}).Warn("No accepted hold invoice found in DB to mark as failed due to cancellation")
return NewNotFoundError()
}
return svc.markPaymentFailed(tx, &dbTransaction, "Hold invoice was cancelled")
})
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"payment_hash": paymentHash,
}).WithError(err).Error("Failed DB transaction while canceling hold invoice")
return err
}
logger.Logger.WithFields(logrus.Fields{
"payment_hash": paymentHash,
}).Info("Marked hold invoice as failed in DB due to cancellation")
svc.eventPublisher.Publish(&events.Event{
Event: "nwc_hold_invoice_canceled",
Properties: &dbTransaction,
})
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