fix: fee limits for LND and LDK, isolated balance calculation (#937)

This commit is contained in:
Roland 2025-01-06 23:08:09 +07:00 committed by GitHub
parent 83006f2b2d
commit 188e7c6fa6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 145 additions and 42 deletions

View file

@ -71,7 +71,7 @@ type App struct {
BudgetUsage uint64 `json:"budgetUsage"`
BudgetRenewal string `json:"budgetRenewal"`
Isolated bool `json:"isolated"`
Balance uint64 `json:"balance"`
Balance int64 `json:"balance"`
Metadata Metadata `json:"metadata,omitempty"`
}

View file

@ -5,9 +5,9 @@ import (
"gorm.io/gorm"
)
func GetIsolatedBalance(tx *gorm.DB, appId uint) uint64 {
func GetIsolatedBalance(tx *gorm.DB, appId uint) int64 {
var received struct {
Sum uint64
Sum int64
}
tx.
Table("transactions").
@ -15,7 +15,7 @@ func GetIsolatedBalance(tx *gorm.DB, appId uint) uint64 {
Where("app_id = ? AND type = ? AND state = ?", appId, constants.TRANSACTION_TYPE_INCOMING, constants.TRANSACTION_STATE_SETTLED).Scan(&received)
var spent struct {
Sum uint64
Sum int64
}
tx.

View file

@ -0,0 +1,69 @@
package queries
import (
"testing"
"github.com/getAlby/hub/constants"
"github.com/getAlby/hub/db"
"github.com/getAlby/hub/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetIsolatedBalance_PendingNoOverflow(t *testing.T) {
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
require.NoError(t, err)
app, _, err := tests.CreateApp(svc)
assert.NoError(t, err)
app.Isolated = true
svc.DB.Save(&app)
paymentAmount := uint64(1000) // 1 sat
tx := db.Transaction{
AppId: &app.ID,
RequestEventId: nil,
Type: constants.TRANSACTION_TYPE_OUTGOING,
State: constants.TRANSACTION_STATE_PENDING,
FeeReserveMsat: uint64(10000),
AmountMsat: paymentAmount,
PaymentRequest: tests.MockInvoice,
PaymentHash: tests.MockPaymentHash,
SelfPayment: true,
}
svc.DB.Save(&tx)
balance := GetIsolatedBalance(svc.DB, app.ID)
assert.Equal(t, int64(-11000), balance)
}
func TestGetIsolatedBalance_SettledNoOverflow(t *testing.T) {
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
require.NoError(t, err)
app, _, err := tests.CreateApp(svc)
assert.NoError(t, err)
app.Isolated = true
svc.DB.Save(&app)
paymentAmount := uint64(1000) // 1 sat
tx := db.Transaction{
AppId: &app.ID,
RequestEventId: nil,
Type: constants.TRANSACTION_TYPE_OUTGOING,
State: constants.TRANSACTION_STATE_SETTLED,
FeeReserveMsat: uint64(0),
AmountMsat: paymentAmount,
PaymentRequest: tests.MockInvoice,
PaymentHash: tests.MockPaymentHash,
SelfPayment: true,
}
svc.DB.Save(&tx)
balance := GetIsolatedBalance(svc.DB, app.ID)
assert.Equal(t, int64(-1000), balance)
}

View file

@ -30,6 +30,7 @@ import (
"github.com/getAlby/hub/logger"
"github.com/getAlby/hub/lsp"
"github.com/getAlby/hub/service/keys"
"github.com/getAlby/hub/transactions"
"github.com/getAlby/hub/utils"
)
@ -414,6 +415,12 @@ func (ls *LDKService) Shutdown() error {
return nil
}
func getMaxTotalRoutingFeeLimit(amountMsat uint64) ldk_node.MaxTotalRoutingFeeLimit {
return ldk_node.MaxTotalRoutingFeeLimitSome{
AmountMsat: transactions.CalculateFeeReserveMsat(amountMsat),
}
}
func (ls *LDKService) SendPaymentSync(ctx context.Context, invoice string, amount *uint64) (*lnclient.PayInvoiceResponse, error) {
paymentRequest, err := decodepay.Decodepay(invoice)
if err != nil {
@ -424,13 +431,13 @@ func (ls *LDKService) SendPaymentSync(ctx context.Context, invoice string, amoun
return nil, err
}
paymentAmount := uint64(paymentRequest.MSatoshi)
paymentAmountMsat := uint64(paymentRequest.MSatoshi)
if amount != nil {
paymentAmount = *amount
paymentAmountMsat = *amount
}
maxSpendable := ls.getMaxSpendable()
if paymentAmount > maxSpendable {
if paymentAmountMsat > maxSpendable {
ls.eventPublisher.Publish(&events.Event{
Event: "nwc_outgoing_liquidity_required",
Properties: map[string]interface{}{
@ -447,10 +454,15 @@ func (ls *LDKService) SendPaymentSync(ctx context.Context, invoice string, amoun
defer ls.ldkEventBroadcaster.CancelSubscription(ldkEventSubscription)
var paymentHash string
maxTotalRoutingFeeMsat := getMaxTotalRoutingFeeLimit(paymentAmountMsat)
sendingParams := &ldk_node.SendingParameters{
MaxTotalRoutingFeeMsat: &maxTotalRoutingFeeMsat,
}
if amount == nil {
paymentHash, err = ls.node.Bolt11Payment().Send(invoice, nil)
paymentHash, err = ls.node.Bolt11Payment().Send(invoice, sendingParams)
} else {
paymentHash, err = ls.node.Bolt11Payment().SendUsingAmount(invoice, *amount, nil)
paymentHash, err = ls.node.Bolt11Payment().SendUsingAmount(invoice, *amount, sendingParams)
}
if err != nil {
logger.Logger.WithError(err).Error("SendPayment failed")
@ -539,7 +551,12 @@ func (ls *LDKService) SendKeysend(ctx context.Context, amount uint64, destinatio
ldkEventSubscription := ls.ldkEventBroadcaster.Subscribe()
defer ls.ldkEventBroadcaster.CancelSubscription(ldkEventSubscription)
paymentHash, err := ls.node.SpontaneousPayment().Send(amount, destination, nil, customTlvs, &preimage)
maxTotalRoutingFeeMsat := getMaxTotalRoutingFeeLimit(amount)
sendingParams := &ldk_node.SendingParameters{
MaxTotalRoutingFeeMsat: &maxTotalRoutingFeeMsat,
}
paymentHash, err := ls.node.SpontaneousPayment().Send(amount, destination, sendingParams, customTlvs, &preimage)
if err != nil {
logger.Logger.WithError(err).Error("Keysend failed")
return nil, err

View file

@ -23,6 +23,7 @@ import (
"github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/lnclient/lnd/wrapper"
"github.com/getAlby/hub/logger"
"github.com/getAlby/hub/transactions"
"github.com/sirupsen/logrus"
// "gorm.io/gorm"
@ -314,7 +315,24 @@ func (svc *LNDService) LookupInvoice(ctx context.Context, paymentHash string) (t
}
func (svc *LNDService) SendPaymentSync(ctx context.Context, payReq string, amount *uint64) (*lnclient.PayInvoiceResponse, error) {
sendRequest := &lnrpc.SendRequest{PaymentRequest: payReq}
paymentRequest, err := decodepay.Decodepay(payReq)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"bolt11": payReq,
}).WithError(err).Error("Failed to decode bolt11 invoice")
return nil, err
}
paymentAmountMsat := uint64(paymentRequest.MSatoshi)
if amount != nil {
paymentAmountMsat = *amount
}
sendRequest := &lnrpc.SendRequest{PaymentRequest: payReq, FeeLimit: &lnrpc.FeeLimit{
Limit: &lnrpc.FeeLimit_FixedMsat{
FixedMsat: int64(transactions.CalculateFeeReserveMsat(paymentAmountMsat)),
},
}}
if amount != nil {
sendRequest.AmtMsat = int64(*amount)
@ -378,6 +396,11 @@ func (svc *LNDService) SendKeysend(ctx context.Context, amount uint64, destinati
PaymentHash: paymentHashBytes,
DestFeatures: []lnrpc.FeatureBit{lnrpc.FeatureBit_TLV_ONION_REQ},
DestCustomRecords: destCustomRecords,
FeeLimit: &lnrpc.FeeLimit{
Limit: &lnrpc.FeeLimit_FixedMsat{
FixedMsat: int64(transactions.CalculateFeeReserveMsat(amount)),
},
},
}
resp, err := svc.client.SendPaymentSync(ctx, sendPaymentRequest)

View file

@ -17,7 +17,7 @@ const (
)
type getBalanceResponse struct {
Balance uint64 `json:"balance"`
Balance int64 `json:"balance"`
// MaxAmount int `json:"max_amount"`
// BudgetRenewal string `json:"budget_renewal"`
}
@ -29,12 +29,12 @@ func (controller *nip47Controller) HandleGetBalanceEvent(ctx context.Context, ni
"request_event_id": requestEventId,
}).Debug("Getting balance")
balance := uint64(0)
balance := int64(0)
if app.Isolated {
balance = queries.GetIsolatedBalance(controller.db, app.ID)
} else {
balances, err := controller.lnClient.GetBalances(ctx)
balance = uint64(balances.Lightning.TotalSpendable)
balance = balances.Lightning.TotalSpendable
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"request_event_id": requestEventId,

View file

@ -51,7 +51,7 @@ func TestHandleGetBalanceEvent(t *testing.T) {
NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
HandleGetBalanceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)
assert.Equal(t, uint64(21000), publishedResponse.Result.(*getBalanceResponse).Balance)
assert.Equal(t, int64(21000), publishedResponse.Result.(*getBalanceResponse).Balance)
assert.Nil(t, publishedResponse.Error)
}
@ -85,7 +85,7 @@ func TestHandleGetBalanceEvent_IsolatedApp_NoTransactions(t *testing.T) {
NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
HandleGetBalanceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)
assert.Equal(t, uint64(0), publishedResponse.Result.(*getBalanceResponse).Balance)
assert.Equal(t, int64(0), publishedResponse.Result.(*getBalanceResponse).Balance)
assert.Nil(t, publishedResponse.Error)
}
func TestHandleGetBalanceEvent_IsolatedApp_Transactions(t *testing.T) {
@ -132,6 +132,6 @@ func TestHandleGetBalanceEvent_IsolatedApp_Transactions(t *testing.T) {
NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
HandleGetBalanceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)
assert.Equal(t, uint64(1000), publishedResponse.Result.(*getBalanceResponse).Balance)
assert.Equal(t, int64(1000), publishedResponse.Result.(*getBalanceResponse).Balance)
assert.Nil(t, publishedResponse.Error)
}

View file

@ -323,14 +323,9 @@ func TestSendPaymentSync_IsolatedApp_BalanceSufficient_FailedPayment(t *testing.
}
func TestCalculateFeeReserve(t *testing.T) {
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
require.NoError(t, err)
transactionsService := NewTransactionsService(svc.DB, svc.EventPublisher)
assert.Equal(t, uint64(10_000), transactionsService.calculateFeeReserveMsat(0))
assert.Equal(t, uint64(10_000), transactionsService.calculateFeeReserveMsat(10_000))
assert.Equal(t, uint64(10_000), transactionsService.calculateFeeReserveMsat(100_000))
assert.Equal(t, uint64(10_000), transactionsService.calculateFeeReserveMsat(1000_000))
assert.Equal(t, uint64(20_000), transactionsService.calculateFeeReserveMsat(2000_000))
assert.Equal(t, uint64(10_000), CalculateFeeReserveMsat(0))
assert.Equal(t, uint64(10_000), CalculateFeeReserveMsat(10_000))
assert.Equal(t, uint64(10_000), CalculateFeeReserveMsat(100_000))
assert.Equal(t, uint64(10_000), CalculateFeeReserveMsat(1000_000))
assert.Equal(t, uint64(20_000), CalculateFeeReserveMsat(2000_000))
}

View file

@ -418,7 +418,7 @@ func TestSendKeysend_IsolatedAppToNoApp(t *testing.T) {
result := svc.DB.Find(&transactions)
assert.Equal(t, int64(3), result.RowsAffected)
// expect balance to be decreased
assert.Equal(t, uint64(10000), queries.GetIsolatedBalance(svc.DB, app.ID))
assert.Equal(t, int64(10000), queries.GetIsolatedBalance(svc.DB, app.ID))
}
func TestSendKeysend_IsolatedAppToIsolatedApp(t *testing.T) {
@ -510,10 +510,10 @@ func TestSendKeysend_IsolatedAppToIsolatedApp(t *testing.T) {
result := svc.DB.Find(&transactions)
assert.Equal(t, int64(3), result.RowsAffected)
// expect balance to be decreased
assert.Equal(t, uint64(10000), queries.GetIsolatedBalance(svc.DB, app.ID))
assert.Equal(t, int64(10000), queries.GetIsolatedBalance(svc.DB, app.ID))
// expect app2 to receive the payment
assert.Equal(t, uint64(123000), queries.GetIsolatedBalance(svc.DB, app2.ID))
assert.Equal(t, int64(123000), queries.GetIsolatedBalance(svc.DB, app2.ID))
// check notifications
assert.Equal(t, 2, len(mockEventConsumer.GetConsumedEvents()))

View file

@ -106,7 +106,7 @@ func TestSendPaymentSync_SelfPayment_NoAppToIsolatedApp(t *testing.T) {
result := svc.DB.Find(&transactions)
assert.Equal(t, int64(2), result.RowsAffected)
// expect balance to be increased
assert.Equal(t, uint64(123000), queries.GetIsolatedBalance(svc.DB, app.ID))
assert.Equal(t, int64(123000), queries.GetIsolatedBalance(svc.DB, app.ID))
}
func TestSendPaymentSync_SelfPayment_NoAppToApp(t *testing.T) {
@ -229,7 +229,7 @@ func TestSendPaymentSync_SelfPayment_IsolatedAppToNoApp(t *testing.T) {
result := svc.DB.Find(&transactions)
assert.Equal(t, int64(3), result.RowsAffected)
// expect balance to be decreased
assert.Equal(t, uint64(10000), queries.GetIsolatedBalance(svc.DB, app.ID))
assert.Equal(t, int64(10000), queries.GetIsolatedBalance(svc.DB, app.ID))
}
func TestSendPaymentSync_SelfPayment_IsolatedAppToApp(t *testing.T) {
@ -305,7 +305,7 @@ func TestSendPaymentSync_SelfPayment_IsolatedAppToApp(t *testing.T) {
result := svc.DB.Find(&transactions)
assert.Equal(t, int64(3), result.RowsAffected)
// expect balance to be decreased
assert.Equal(t, uint64(10000), queries.GetIsolatedBalance(svc.DB, app.ID))
assert.Equal(t, int64(10000), queries.GetIsolatedBalance(svc.DB, app.ID))
}
func TestSendPaymentSync_SelfPayment_IsolatedAppToIsolatedApp(t *testing.T) {
@ -387,7 +387,7 @@ func TestSendPaymentSync_SelfPayment_IsolatedAppToIsolatedApp(t *testing.T) {
result := svc.DB.Find(&transactions)
assert.Equal(t, int64(3), result.RowsAffected)
// expect balance to be decreased
assert.Equal(t, uint64(10000), queries.GetIsolatedBalance(svc.DB, app.ID))
assert.Equal(t, int64(10000), queries.GetIsolatedBalance(svc.DB, app.ID))
// check notifications
assert.Equal(t, 2, len(mockEventConsumer.GetConsumedEvents()))
@ -473,5 +473,5 @@ func TestSendPaymentSync_SelfPayment_IsolatedAppToSelf(t *testing.T) {
assert.Equal(t, int64(3), result.RowsAffected)
// expect balance to be unchanged
assert.Equal(t, uint64(133000), queries.GetIsolatedBalance(svc.DB, app.ID))
assert.Equal(t, int64(133000), queries.GetIsolatedBalance(svc.DB, app.ID))
}

View file

@ -250,7 +250,7 @@ func (svc *transactionsService) SendPaymentSync(ctx context.Context, payReq stri
RequestEventId: requestEventId,
Type: constants.TRANSACTION_TYPE_OUTGOING,
State: constants.TRANSACTION_STATE_PENDING,
FeeReserveMsat: svc.calculateFeeReserveMsat(paymentAmount),
FeeReserveMsat: CalculateFeeReserveMsat(paymentAmount),
AmountMsat: paymentAmount,
PaymentRequest: payReq,
PaymentHash: paymentRequest.PaymentHash,
@ -363,7 +363,7 @@ func (svc *transactionsService) SendKeysend(ctx context.Context, amount uint64,
RequestEventId: requestEventId,
Type: constants.TRANSACTION_TYPE_OUTGOING,
State: constants.TRANSACTION_STATE_PENDING,
FeeReserveMsat: svc.calculateFeeReserveMsat(uint64(amount)),
FeeReserveMsat: CalculateFeeReserveMsat(uint64(amount)),
AmountMsat: amount,
Metadata: datatypes.JSON(metadataBytes),
Boostagram: datatypes.JSON(boostagramBytes),
@ -796,7 +796,7 @@ func (svc *transactionsService) interceptSelfPayment(paymentHash string) (*lncli
}
func (svc *transactionsService) validateCanPay(tx *gorm.DB, appId *uint, amount uint64, description string) error {
amountWithFeeReserve := amount + svc.calculateFeeReserveMsat(amount)
amountWithFeeReserve := amount + CalculateFeeReserveMsat(amount)
// ensure balance for isolated apps
if appId != nil {
@ -820,7 +820,7 @@ func (svc *transactionsService) validateCanPay(tx *gorm.DB, appId *uint, amount
if app.Isolated {
balance := queries.GetIsolatedBalance(tx, appPermission.AppId)
if amountWithFeeReserve > balance {
if int64(amountWithFeeReserve) > balance {
message := NewInsufficientBalanceError().Error()
if description != "" {
message += " " + description
@ -862,9 +862,8 @@ func (svc *transactionsService) validateCanPay(tx *gorm.DB, appId *uint, amount
}
// max of 1% or 10000 millisats (10 sats)
func (svc *transactionsService) calculateFeeReserveMsat(amount uint64) uint64 {
// NOTE: LDK defaults to 1% of the payment amount + 50 sats
return uint64(math.Max(math.Ceil(float64(amount)*0.01), 10000))
func CalculateFeeReserveMsat(amountMsat uint64) uint64 {
return uint64(math.Max(math.Ceil(float64(amountMsat)*0.01), 10000))
}
func makePreimageHex() ([]byte, error) {