Skip to content

Commit

Permalink
feat: budget check in transactions service, correctly pass payment er…
Browse files Browse the repository at this point in the history
…rors to NIP-47 response

also reduce query duplication
  • Loading branch information
rolznz committed Jul 11, 2024
1 parent aefd4cf commit 79895a9
Show file tree
Hide file tree
Showing 18 changed files with 245 additions and 193 deletions.
3 changes: 2 additions & 1 deletion alby/alby_oauth_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"gorm.io/gorm"

"github.com/getAlby/hub/config"
"github.com/getAlby/hub/constants"
"github.com/getAlby/hub/db"
"github.com/getAlby/hub/events"
"github.com/getAlby/hub/lnclient"
Expand Down Expand Up @@ -394,7 +395,7 @@ func (svc *albyOAuthService) LinkAccount(ctx context.Context, lnClient lnclient.
}
notificationTypes := lnClient.GetSupportedNIP47NotificationTypes()
if len(notificationTypes) > 0 {
scopes = append(scopes, permissions.NOTIFICATIONS_SCOPE)
scopes = append(scopes, constants.NOTIFICATIONS_SCOPE)
}

app, _, err := db.NewDBService(svc.db, svc.eventPublisher).CreateApp(
Expand Down
22 changes: 12 additions & 10 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import (

"github.com/getAlby/hub/alby"
"github.com/getAlby/hub/config"
"github.com/getAlby/hub/constants"
"github.com/getAlby/hub/db"
"github.com/getAlby/hub/db/queries"
"github.com/getAlby/hub/events"
"github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/logger"
Expand Down Expand Up @@ -66,7 +68,7 @@ func (api *api) CreateApp(createAppRequest *CreateAppRequest) (*CreateAppRespons
}
}

app, pairingSecretKey, err := api.dbSvc.CreateApp(createAppRequest.Name, createAppRequest.Pubkey, createAppRequest.MaxAmount, createAppRequest.BudgetRenewal, expiresAt, createAppRequest.Scopes)
app, pairingSecretKey, err := api.dbSvc.CreateApp(createAppRequest.Name, createAppRequest.Pubkey, createAppRequest.MaxAmountSat, createAppRequest.BudgetRenewal, expiresAt, createAppRequest.Scopes)

if err != nil {
return nil, err
Expand Down Expand Up @@ -102,7 +104,7 @@ func (api *api) CreateApp(createAppRequest *CreateAppRequest) (*CreateAppRespons
}

func (api *api) UpdateApp(userApp *db.App, updateAppRequest *UpdateAppRequest) error {
maxAmount := updateAppRequest.MaxAmount
maxAmount := updateAppRequest.MaxAmountSat
budgetRenewal := updateAppRequest.BudgetRenewal

if len(updateAppRequest.Scopes) == 0 {
Expand Down Expand Up @@ -184,7 +186,7 @@ func (api *api) GetApp(dbApp *db.App) *App {
requestMethods := []string{}
for _, appPerm := range appPermissions {
expiresAt = appPerm.ExpiresAt
if appPerm.Scope == permissions.PAY_INVOICE_SCOPE {
if appPerm.Scope == constants.PAY_INVOICE_SCOPE {
//find the pay_invoice-specific permissions
paySpecificPermission = appPerm
}
Expand All @@ -195,7 +197,7 @@ func (api *api) GetApp(dbApp *db.App) *App {
budgetUsage := uint64(0)
maxAmount := uint64(paySpecificPermission.MaxAmount)
if maxAmount > 0 {
budgetUsage = api.permissionsSvc.GetBudgetUsage(&paySpecificPermission)
budgetUsage = queries.GetBudgetUsage(api.db, &paySpecificPermission)
}

response := App{
Expand All @@ -206,7 +208,7 @@ func (api *api) GetApp(dbApp *db.App) *App {
UpdatedAt: dbApp.UpdatedAt,
NostrPubkey: dbApp.NostrPubkey,
ExpiresAt: expiresAt,
MaxAmount: maxAmount,
MaxAmountSat: maxAmount,
Scopes: requestMethods,
BudgetUsage: budgetUsage,
BudgetRenewal: paySpecificPermission.BudgetRenewal,
Expand Down Expand Up @@ -247,11 +249,11 @@ func (api *api) ListApps() ([]App, error) {
for _, appPermission := range permissionsMap[dbApp.ID] {
apiApp.Scopes = append(apiApp.Scopes, appPermission.Scope)
apiApp.ExpiresAt = appPermission.ExpiresAt
if appPermission.Scope == permissions.PAY_INVOICE_SCOPE {
if appPermission.Scope == constants.PAY_INVOICE_SCOPE {
apiApp.BudgetRenewal = appPermission.BudgetRenewal
apiApp.MaxAmount = uint64(appPermission.MaxAmount)
if apiApp.MaxAmount > 0 {
apiApp.BudgetUsage = api.permissionsSvc.GetBudgetUsage(&appPermission)
apiApp.MaxAmountSat = uint64(appPermission.MaxAmount)
if apiApp.MaxAmountSat > 0 {
apiApp.BudgetUsage = queries.GetBudgetUsage(api.db, &appPermission)
}
}
}
Expand Down Expand Up @@ -655,7 +657,7 @@ func (api *api) GetWalletCapabilities(ctx context.Context) (*WalletCapabilitiesR
return nil, err
}
if len(notificationTypes) > 0 {
scopes = append(scopes, permissions.NOTIFICATIONS_SCOPE)
scopes = append(scopes, constants.NOTIFICATIONS_SCOPE)
}

return &WalletCapabilitiesResponse{
Expand Down
6 changes: 3 additions & 3 deletions api/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ type App struct {
LastEventAt *time.Time `json:"lastEventAt"`
ExpiresAt *time.Time `json:"expiresAt"`
Scopes []string `json:"scopes"`
MaxAmount uint64 `json:"maxAmount"`
MaxAmountSat uint64 `json:"maxAmount"`
BudgetUsage uint64 `json:"budgetUsage"`
BudgetRenewal string `json:"budgetRenewal"`
}
Expand All @@ -75,7 +75,7 @@ type ListAppsResponse struct {
}

type UpdateAppRequest struct {
MaxAmount uint64 `json:"maxAmount"`
MaxAmountSat uint64 `json:"maxAmount"`
BudgetRenewal string `json:"budgetRenewal"`
ExpiresAt string `json:"expiresAt"`
Scopes []string `json:"scopes"`
Expand All @@ -84,7 +84,7 @@ type UpdateAppRequest struct {
type CreateAppRequest struct {
Name string `json:"name"`
Pubkey string `json:"pubkey"`
MaxAmount uint64 `json:"maxAmount"`
MaxAmountSat uint64 `json:"maxAmount"`
BudgetRenewal string `json:"budgetRenewal"`
ExpiresAt string `json:"expiresAt"`
Scopes []string `json:"scopes"`
Expand Down
31 changes: 31 additions & 0 deletions constants/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package constants

// shared constants used by multiple packages

const (
TRANSACTION_TYPE_INCOMING = "incoming"
TRANSACTION_TYPE_OUTGOING = "outgoing"

TRANSACTION_STATE_PENDING = "PENDING"
TRANSACTION_STATE_SETTLED = "SETTLED"
TRANSACTION_STATE_FAILED = "FAILED"
)

const (
BUDGET_RENEWAL_DAILY = "daily"
BUDGET_RENEWAL_WEEKLY = "weekly"
BUDGET_RENEWAL_MONTHLY = "monthly"
BUDGET_RENEWAL_YEARLY = "yearly"
BUDGET_RENEWAL_NEVER = "never"
)

const (
PAY_INVOICE_SCOPE = "pay_invoice" // also covers pay_keysend and multi_* payment methods
GET_BALANCE_SCOPE = "get_balance"
GET_INFO_SCOPE = "get_info"
MAKE_INVOICE_SCOPE = "make_invoice"
LOOKUP_INVOICE_SCOPE = "lookup_invoice"
LIST_TRANSACTIONS_SCOPE = "list_transactions"
SIGN_MESSAGE_SCOPE = "sign_message"
NOTIFICATIONS_SCOPE = "notifications" // covers all notification types
)
4 changes: 2 additions & 2 deletions db/db_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func NewDBService(db *gorm.DB, eventPublisher events.EventPublisher) *dbService
}
}

func (svc *dbService) CreateApp(name string, pubkey string, maxAmount uint64, budgetRenewal string, expiresAt *time.Time, scopes []string) (*App, string, error) {
func (svc *dbService) CreateApp(name string, pubkey string, maxAmountSat uint64, budgetRenewal string, expiresAt *time.Time, scopes []string) (*App, string, error) {
var pairingPublicKey string
var pairingSecretKey string
if pubkey == "" {
Expand Down Expand Up @@ -53,7 +53,7 @@ func (svc *dbService) CreateApp(name string, pubkey string, maxAmount uint64, bu
Scope: scope,
ExpiresAt: expiresAt,
//these fields are only relevant for pay_invoice
MaxAmount: int(maxAmount),
MaxAmount: int(maxAmountSat),
BudgetRenewal: budgetRenewal,
}
err = tx.Create(&appPermission).Error
Expand Down
4 changes: 2 additions & 2 deletions db/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ type AppPermission struct {
AppId uint `validate:"required"`
App App
Scope string `validate:"required"`
MaxAmount int
MaxAmount int // TODO: rename to MaxAmountSat
BudgetRenewal string
ExpiresAt *time.Time
CreatedAt time.Time
Expand Down Expand Up @@ -80,7 +80,7 @@ type Transaction struct {
}

type DBService interface {
CreateApp(name string, pubkey string, maxAmount uint64, budgetRenewal string, expiresAt *time.Time, scopes []string) (*App, string, error)
CreateApp(name string, pubkey string, maxAmountSat uint64, budgetRenewal string, expiresAt *time.Time, scopes []string) (*App, string, error)
}

const (
Expand Down
45 changes: 45 additions & 0 deletions db/queries/get_budget_usage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package queries

import (
"time"

"github.com/getAlby/hub/constants"
"github.com/getAlby/hub/db"
"gorm.io/gorm"
)

func GetBudgetUsage(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").
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
}

func getStartOfBudget(budget_type string) time.Time {
now := time.Now()
switch budget_type {
case constants.BUDGET_RENEWAL_DAILY:
// TODO: Use the location of the user, instead of the server
return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
case constants.BUDGET_RENEWAL_WEEKLY:
weekday := now.Weekday()
var startOfWeek time.Time
if weekday == 0 {
startOfWeek = now.AddDate(0, 0, -6)
} else {
startOfWeek = now.AddDate(0, 0, -int(weekday)+1)
}
return time.Date(startOfWeek.Year(), startOfWeek.Month(), startOfWeek.Day(), 0, 0, 0, 0, startOfWeek.Location())
case constants.BUDGET_RENEWAL_MONTHLY:
return time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
case constants.BUDGET_RENEWAL_YEARLY:
return time.Date(now.Year(), time.January, 1, 0, 0, 0, 0, now.Location())
default: //"never"
return time.Time{}
}
}
27 changes: 27 additions & 0 deletions db/queries/get_isolated_balance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package queries

import (
"github.com/getAlby/hub/constants"
"gorm.io/gorm"
)

func GetIsolatedBalance(tx *gorm.DB, appId uint) uint64 {
var received struct {
Sum uint64
}
tx.
Table("transactions").
Select("SUM(amount) 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
}
// TODO: ensure fee reserve on these payments
tx.
Table("transactions").
Select("SUM(amount + fee) 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
}
21 changes: 2 additions & 19 deletions nip47/controllers/get_balance_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import (
"context"

"github.com/getAlby/hub/db"
"github.com/getAlby/hub/db/queries"
"github.com/getAlby/hub/logger"
"github.com/getAlby/hub/nip47/models"
"github.com/getAlby/hub/transactions"
"github.com/nbd-wtf/go-nostr"
"github.com/sirupsen/logrus"
)
Expand Down Expand Up @@ -41,24 +41,7 @@ func (controller *nip47Controller) HandleGetBalanceEvent(ctx context.Context, ni
})
balance := uint64(0)
if appPermission.BalanceType == "isolated" {
// TODO: remove duplication in transactions service
var received struct {
Sum uint64
}
controller.db.
Table("transactions").
Select("SUM(amount) as sum").
Where("app_id = ? AND type = ? AND state = ?", appPermission.AppId, transactions.TRANSACTION_TYPE_INCOMING, transactions.TRANSACTION_STATE_SETTLED).Scan(&received)

var spent struct {
Sum uint64
}
controller.db.
Table("transactions").
Select("SUM(amount + fee) as sum").
Where("app_id = ? AND type = ? AND (state = ? OR state = ?)", appPermission.AppId, transactions.TRANSACTION_TYPE_OUTGOING, transactions.TRANSACTION_STATE_SETTLED, transactions.TRANSACTION_STATE_PENDING).Scan(&spent)

balance = received.Sum - spent.Sum
balance = queries.GetIsolatedBalance(controller.db, appPermission.AppId)
} else {
balance_signed, err := controller.lnClient.GetBalance(ctx)
balance = uint64(balance_signed)
Expand Down
4 changes: 2 additions & 2 deletions nip47/controllers/get_info_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ package controllers
import (
"context"

"github.com/getAlby/hub/constants"
"github.com/getAlby/hub/db"
"github.com/getAlby/hub/logger"
"github.com/getAlby/hub/nip47/models"
permissions "github.com/getAlby/hub/nip47/permissions"
"github.com/nbd-wtf/go-nostr"
"github.com/sirupsen/logrus"
)
Expand Down Expand Up @@ -34,7 +34,7 @@ func (controller *nip47Controller) HandleGetInfoEvent(ctx context.Context, nip47
}

// basic permissions check
hasPermission, _, _ := controller.permissionsService.HasPermission(app, permissions.GET_INFO_SCOPE, 0)
hasPermission, _, _ := controller.permissionsService.HasPermission(app, constants.GET_INFO_SCOPE, 0)
if hasPermission {
logger.Logger.WithFields(logrus.Fields{
"request_event_id": requestEventId,
Expand Down
12 changes: 1 addition & 11 deletions nip47/controllers/lookup_invoice_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@ package controllers

import (
"context"
"errors"
"fmt"
"strings"

"github.com/getAlby/hub/logger"
"github.com/getAlby/hub/nip47/models"
"github.com/getAlby/hub/transactions"
"github.com/nbd-wtf/go-nostr"
decodepay "github.com/nbd-wtf/ln-decodepay"
"github.com/sirupsen/logrus"
Expand Down Expand Up @@ -74,17 +72,9 @@ func (controller *nip47Controller) HandleLookupInvoiceEvent(ctx context.Context,
"payment_hash": paymentHash,
}).Infof("Failed to lookup invoice: %v", err)

code := models.ERROR_INTERNAL
if errors.Is(err, transactions.NewNotFoundError()) {
code = models.ERROR_NOT_FOUND
}

publishResponse(&models.Response{
ResultType: nip47Request.Method,
Error: &models.Error{
Code: code,
Message: err.Error(),
},
Error: mapNip47Error(err),
}, nostr.Tags{})
return
}
Expand Down
26 changes: 26 additions & 0 deletions nip47/controllers/map_nip47_error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package controllers

import (
"errors"

"github.com/getAlby/hub/nip47/models"
"github.com/getAlby/hub/transactions"
)

func mapNip47Error(err error) *models.Error {
code := models.ERROR_INTERNAL
if errors.Is(err, transactions.NewNotFoundError()) {
code = models.ERROR_NOT_FOUND
}
if errors.Is(err, transactions.NewInsufficientBalanceError()) {
code = models.ERROR_INSUFFICIENT_BALANCE
}
if errors.Is(err, transactions.NewQuotaExceededError()) {
code = models.ERROR_QUOTA_EXCEEDED
}

return &models.Error{
Code: code,
Message: err.Error(),
}
}
5 changes: 1 addition & 4 deletions nip47/controllers/pay_invoice_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,7 @@ func (controller *nip47Controller) pay(ctx context.Context, bolt11 string, payme
})
publishResponse(&models.Response{
ResultType: nip47Request.Method,
Error: &models.Error{
Code: models.ERROR_INTERNAL,
Message: err.Error(),
},
Error: mapNip47Error(err),
}, tags)
return
}
Expand Down
Loading

0 comments on commit 79895a9

Please sign in to comment.