Skip to content

Commit

Permalink
fix: fee limits for LND and LDK, isolated balance calculation (#937)
Browse files Browse the repository at this point in the history
  • Loading branch information
rolznz authored Jan 6, 2025
1 parent 83006f2 commit 188e7c6
Show file tree
Hide file tree
Showing 11 changed files with 145 additions and 42 deletions.
2 changes: 1 addition & 1 deletion api/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}

Expand Down
6 changes: 3 additions & 3 deletions db/queries/get_isolated_balance.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@ 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").
Select("SUM(amount_msat) as sum").
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.
Expand Down
69 changes: 69 additions & 0 deletions db/queries/get_isolated_balance_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
29 changes: 23 additions & 6 deletions lnclient/ldk/ldk.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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 {
Expand All @@ -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{}{
Expand All @@ -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")
Expand Down Expand Up @@ -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
Expand Down
25 changes: 24 additions & 1 deletion lnclient/lnd/lnd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions nip47/controllers/get_balance_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
Expand All @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions nip47/controllers/get_balance_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}
15 changes: 5 additions & 10 deletions transactions/isolated_app_payments_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
6 changes: 3 additions & 3 deletions transactions/keysend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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()))
Expand Down
10 changes: 5 additions & 5 deletions transactions/self_payments_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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()))
Expand Down Expand Up @@ -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))
}
13 changes: 6 additions & 7 deletions transactions/transactions_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit 188e7c6

Please sign in to comment.