Skip to content

Commit

Permalink
feat: add fee reserves to unsettled outgoing transactions
Browse files Browse the repository at this point in the history
  • Loading branch information
rolznz committed Jul 12, 2024
1 parent 7773186 commit 66a2795
Show file tree
Hide file tree
Showing 7 changed files with 42 additions and 23 deletions.
4 changes: 2 additions & 2 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ func (api *api) GetApp(dbApp *db.App) *App {
budgetUsage := uint64(0)
maxAmount := uint64(paySpecificPermission.MaxAmount)
if maxAmount > 0 {
budgetUsage = queries.GetBudgetUsage(api.db, &paySpecificPermission)
budgetUsage = queries.GetBudgetUsageSat(api.db, &paySpecificPermission)
}

response := App{
Expand Down Expand Up @@ -253,7 +253,7 @@ func (api *api) ListApps() ([]App, error) {
apiApp.BudgetRenewal = appPermission.BudgetRenewal
apiApp.MaxAmountSat = uint64(appPermission.MaxAmount)
if apiApp.MaxAmountSat > 0 {
apiApp.BudgetUsage = queries.GetBudgetUsage(api.db, &appPermission)
apiApp.BudgetUsage = queries.GetBudgetUsageSat(api.db, &appPermission)
}
}
}
Expand Down
1 change: 1 addition & 0 deletions db/migrations/202407012100_transactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ CREATE TABLE transactions(
description_hash text,
amount integer,
fee integer,
fee_reserve integer,
created_at datetime,
updated_at datetime,
expires_at datetime,
Expand Down
1 change: 1 addition & 0 deletions db/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ type Transaction struct {
State string
Amount uint64 // in millisats
Fee *uint64 // in millisats
FeeReserve *uint64 // in millisats, set for unsettled outgoing payments
PaymentRequest string
PaymentHash string
Description string
Expand Down
5 changes: 2 additions & 3 deletions db/queries/get_budget_usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,13 @@ import (
"gorm.io/gorm"
)

func GetBudgetUsage(tx *gorm.DB, appPermission *db.AppPermission) uint64 {
func GetBudgetUsageSat(tx *gorm.DB, appPermission *db.AppPermission) uint64 {
var result struct {
Sum uint64
}
// TODO: ensure fee reserve on these payments
tx.
Table("transactions").
Select("SUM(amount + fee) as sum").
Select("SUM(amount + coalesce(fee, 0) + coalesce(fee_reserve, 0)) as sum").
Where("app_id = ? AND type = ? AND (state = ? OR state = ?) AND created_at > ?", appPermission.AppId, constants.TRANSACTION_TYPE_OUTGOING, constants.TRANSACTION_STATE_SETTLED, constants.TRANSACTION_STATE_PENDING, getStartOfBudget(appPermission.BudgetRenewal)).Scan(&result)
return result.Sum / 1000
}
Expand Down
4 changes: 2 additions & 2 deletions db/queries/get_isolated_balance.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ func GetIsolatedBalance(tx *gorm.DB, appId uint) uint64 {
var spent struct {
Sum uint64
}
// TODO: ensure fee reserve on these payments

tx.
Table("transactions").
Select("SUM(amount + fee) as sum").
Select("SUM(amount + coalesce(fee, 0) + coalesce(fee_reserve, 0)) as sum").
Where("app_id = ? AND type = ? AND (state = ? OR state = ?)", appId, constants.TRANSACTION_TYPE_OUTGOING, constants.TRANSACTION_STATE_SETTLED, constants.TRANSACTION_STATE_PENDING).Scan(&spent)

return received.Sum - spent.Sum
Expand Down
2 changes: 1 addition & 1 deletion nip47/permissions/permissions.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func (svc *permissionsService) HasPermission(app *db.App, scope string, amountMs
if scope == constants.PAY_INVOICE_SCOPE {
maxAmount := appPermission.MaxAmount
if maxAmount != 0 {
budgetUsage := queries.GetBudgetUsage(svc.db, &appPermission)
budgetUsage := queries.GetBudgetUsageSat(svc.db, &appPermission)

if budgetUsage+amountMsat/1000 > uint64(maxAmount) {
return false, models.ERROR_QUOTA_EXCEEDED, "Insufficient budget remaining to make payment"
Expand Down
48 changes: 33 additions & 15 deletions transactions/transactions_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,13 @@ func (svc *transactionsService) SendPaymentSync(ctx context.Context, payReq stri
expiresAtValue := time.Now().Add(time.Duration(paymentRequest.Expiry) * time.Second)
expiresAt = &expiresAtValue
}
feeReserve := svc.calculateFeeReserve(uint64(paymentRequest.MSatoshi))
dbTransaction = &db.Transaction{
AppId: appId,
RequestEventId: requestEventId,
Type: constants.TRANSACTION_TYPE_OUTGOING,
State: constants.TRANSACTION_STATE_PENDING,
FeeReserve: &feeReserve,
Amount: uint64(paymentRequest.MSatoshi),
PaymentRequest: payReq,
PaymentHash: paymentRequest.PaymentHash,
Expand Down Expand Up @@ -207,12 +209,14 @@ func (svc *transactionsService) SendPaymentSync(ctx context.Context, payReq stri
}

// the payment definitely succeeded
feeReserve := uint64(0)
now := time.Now()
dbErr := svc.db.Model(dbTransaction).Updates(&db.Transaction{
State: constants.TRANSACTION_STATE_SETTLED,
Preimage: &response.Preimage,
Fee: response.Fee,
SettledAt: &now,
State: constants.TRANSACTION_STATE_SETTLED,
Preimage: &response.Preimage,
Fee: response.Fee,
FeeReserve: &feeReserve,
SettledAt: &now,
}).Error
if dbErr != nil {
logger.Logger.WithFields(logrus.Fields{
Expand Down Expand Up @@ -241,11 +245,13 @@ func (svc *transactionsService) SendKeysend(ctx context.Context, amount uint64,
}

// NOTE: transaction is created without payment hash :scream:
feeReserve := svc.calculateFeeReserve(uint64(amount))
dbTransaction = &db.Transaction{
AppId: appId,
RequestEventId: requestEventId,
Type: constants.TRANSACTION_TYPE_OUTGOING,
State: constants.TRANSACTION_STATE_PENDING,
FeeReserve: &feeReserve,
Amount: amount,
Metadata: string(metadataBytes),
}
Expand Down Expand Up @@ -309,11 +315,13 @@ func (svc *transactionsService) SendKeysend(ctx context.Context, amount uint64,

// the payment definitely succeeded
now := time.Now()
feeReserve := uint64(0)
dbErr := svc.db.Model(dbTransaction).Updates(&db.Transaction{
State: constants.TRANSACTION_STATE_SETTLED,
PaymentHash: paymentHash,
Preimage: &preimage,
Fee: &fee,
FeeReserve: &feeReserve,
SettledAt: &now,
}).Error
if dbErr != nil {
Expand Down Expand Up @@ -439,11 +447,13 @@ func (svc *transactionsService) checkUnsettledTransaction(ctx context.Context, t
// the payment definitely succeeded
now := time.Now()
fee := uint64(lnClientTransaction.FeesPaid)
feeReserve := uint64(0)
dbErr := svc.db.Model(transaction).Updates(&db.Transaction{
State: constants.TRANSACTION_STATE_SETTLED,
Preimage: &lnClientTransaction.Preimage,
Fee: &fee,
SettledAt: &now,
State: constants.TRANSACTION_STATE_SETTLED,
Preimage: &lnClientTransaction.Preimage,
Fee: &fee,
FeeReserve: &feeReserve,
SettledAt: &now,
}).Error
if dbErr != nil {
logger.Logger.WithFields(logrus.Fields{
Expand Down Expand Up @@ -551,11 +561,13 @@ func (svc *transactionsService) ConsumeEvent(ctx context.Context, event *events.

settledAt := time.Now()
fee := uint64(lnClientTransaction.FeesPaid)
feeReserve := uint64(0)
err := svc.db.Model(&dbTransaction).Updates(&db.Transaction{
Fee: &fee,
Preimage: &lnClientTransaction.Preimage,
State: constants.TRANSACTION_STATE_SETTLED,
SettledAt: &settledAt,
Fee: &fee,
FeeReserve: &feeReserve,
Preimage: &lnClientTransaction.Preimage,
State: constants.TRANSACTION_STATE_SETTLED,
SettledAt: &settledAt,
}).Error
if err != nil {
logger.Logger.WithFields(logrus.Fields{
Expand Down Expand Up @@ -640,7 +652,7 @@ func (svc *transactionsService) interceptSelfPayment(paymentHash string) (*lncli
}

func (svc *transactionsService) validateCanPay(tx *gorm.DB, appId *uint, amount uint64) error {
amountWithFeeReserve := amount + uint64(math.Max(math.Ceil(float64(amount)*0.01), 10))
amountWithFeeReserve := amount + svc.calculateFeeReserve(amount)

// ensure balance for isolated apps
if appId != nil {
Expand All @@ -659,12 +671,18 @@ func (svc *transactionsService) validateCanPay(tx *gorm.DB, appId *uint, amount
}

if appPermission.MaxAmount > 0 {
budgetUsage := queries.GetBudgetUsage(tx, &appPermission)
if int(amountWithFeeReserve/1000) > appPermission.MaxAmount-int(budgetUsage) {
budgetUsageSat := queries.GetBudgetUsageSat(tx, &appPermission)
if int(amountWithFeeReserve/1000) > appPermission.MaxAmount-int(budgetUsageSat) {
return NewQuotaExceededError()
}
}
}

return nil
}

// max of 1% or 10000 millisats (10 sats)
func (svc *transactionsService) calculateFeeReserve(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))
}

0 comments on commit 66a2795

Please sign in to comment.