Skip to content

Commit

Permalink
Add get_budget implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
jklein24 committed Oct 7, 2024
1 parent 71183a1 commit 6208eff
Show file tree
Hide file tree
Showing 19 changed files with 268 additions and 7 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ nostr-wallet-connect
nwc.db
.breez
.data
.idea

frontend/dist
frontend/node_modules
Expand Down
1 change: 1 addition & 0 deletions constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const (
const (
PAY_INVOICE_SCOPE = "pay_invoice" // also covers pay_keysend and multi_* payment methods
GET_BALANCE_SCOPE = "get_balance"
GET_BUDGET_SCOPE = "get_budget"
GET_INFO_SCOPE = "get_info"
MAKE_INVOICE_SCOPE = "make_invoice"
LOOKUP_INVOICE_SCOPE = "lookup_invoice"
Expand Down
17 changes: 17 additions & 0 deletions db/queries/get_budget_usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,20 @@ func getStartOfBudget(budget_type string) time.Time {
return time.Time{}
}
}

func GetBudgetRenewsAt(budgetRenewal string) int64 {
now := time.Now()
budgetStart := getStartOfBudget(budgetRenewal)
switch budgetRenewal {
case constants.BUDGET_RENEWAL_DAILY:
return budgetStart.AddDate(0, 0, 1).Unix()
case constants.BUDGET_RENEWAL_WEEKLY:
return budgetStart.AddDate(0, 0, 7).Unix()
case constants.BUDGET_RENEWAL_MONTHLY:
return budgetStart.AddDate(0, 1, 0).Unix()
case constants.BUDGET_RENEWAL_YEARLY:
return budgetStart.AddDate(1, 0, 0).Unix()
default: //"never"
return now.Unix()
}
}
2 changes: 2 additions & 0 deletions frontend/src/components/Scopes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ const Scopes: React.FC<ScopesProps> = ({
const readOnlyScopes: Scope[] = React.useMemo(() => {
const readOnlyScopes: Scope[] = [
"get_balance",
"get_budget",
"get_info",
"make_invoice",
"lookup_invoice",
Expand All @@ -79,6 +80,7 @@ const Scopes: React.FC<ScopesProps> = ({
const isolatedScopes: Scope[] = [
"pay_invoice",
"get_balance",
"get_budget",
"make_invoice",
"lookup_invoice",
"list_transactions",
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/screens/apps/NewApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ const NewAppInternal = ({ capabilities }: NewAppInternalProps) => {
if (requestMethodsSet.has("get_balance")) {
scopes.push("get_balance");
}
if (requestMethodsSet.has("get_budget")) {
scopes.push("get_budget");
}
if (requestMethodsSet.has("make_invoice")) {
scopes.push("make_invoice");
}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/screens/internal-apps/UncleJim.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export function UncleJim() {
name,
scopes: [
"get_balance",
"get_budget",
"get_info",
"list_transactions",
"lookup_invoice",
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type BackendType =
export type Nip47RequestMethod =
| "get_info"
| "get_balance"
| "get_budget"
| "make_invoice"
| "pay_invoice"
| "pay_keysend"
Expand All @@ -41,6 +42,7 @@ export type BudgetRenewalType =
export type Scope =
| "pay_invoice" // also used for pay_keysend, multi_pay_invoice, multi_pay_keysend
| "get_balance"
| "get_budget"
| "get_info"
| "make_invoice"
| "lookup_invoice"
Expand All @@ -56,6 +58,7 @@ export type ScopeIconMap = {

export const scopeIconMap: ScopeIconMap = {
get_balance: WalletMinimal,
get_budget: WalletMinimal,
get_info: Info,
list_transactions: NotebookTabs,
lookup_invoice: Search,
Expand All @@ -81,6 +84,7 @@ export const validBudgetRenewals: BudgetRenewalType[] = [

export const scopeDescriptions: Record<Scope, string> = {
get_balance: "Read your balance",
get_budget: "See its remaining budget",
get_info: "Read your node info",
list_transactions: "Read transaction history",
lookup_invoice: "Lookup status of invoices",
Expand Down
2 changes: 1 addition & 1 deletion lnclient/breez/breez.go
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@ func (bs *BreezService) DisconnectPeer(ctx context.Context, peerId string) error
}

func (bs *BreezService) GetSupportedNIP47Methods() []string {
return []string{"pay_invoice" /*"pay_keysend",*/, "get_balance", "get_info", "make_invoice", "lookup_invoice", "list_transactions", "multi_pay_invoice", "multi_pay_keysend", "sign_message"}
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"}
}

func (bs *BreezService) GetSupportedNIP47NotificationTypes() []string {
Expand Down
2 changes: 1 addition & 1 deletion lnclient/cashu/cashu.go
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@ func (cs *CashuService) checkInvoice(cashuInvoice *storage.Invoice) {
}

func (cs *CashuService) GetSupportedNIP47Methods() []string {
return []string{"pay_invoice", "get_balance", "get_info", "make_invoice", "lookup_invoice", "list_transactions", "multi_pay_invoice"}
return []string{"pay_invoice", "get_balance", "get_balance", "get_info", "make_invoice", "lookup_invoice", "list_transactions", "multi_pay_invoice"}
}

func (cs *CashuService) GetSupportedNIP47NotificationTypes() []string {
Expand Down
2 changes: 1 addition & 1 deletion lnclient/greenlight/greenlight.go
Original file line number Diff line number Diff line change
Expand Up @@ -674,7 +674,7 @@ func (gs *GreenlightService) DisconnectPeer(ctx context.Context, peerId string)
}

func (gs *GreenlightService) GetSupportedNIP47Methods() []string {
return []string{"pay_invoice" /*"pay_keysend",*/, "get_balance", "get_info", "make_invoice", "lookup_invoice", "list_transactions", "multi_pay_invoice", "multi_pay_keysend", "sign_message"}
return []string{"pay_invoice" /*"pay_keysend",*/, "get_balance", "get_balance", "get_info", "make_invoice", "lookup_invoice", "list_transactions", "multi_pay_invoice", "multi_pay_keysend", "sign_message"}
}

func (gs *GreenlightService) GetSupportedNIP47NotificationTypes() []string {
Expand Down
2 changes: 1 addition & 1 deletion lnclient/ldk/ldk.go
Original file line number Diff line number Diff line change
Expand Up @@ -1631,7 +1631,7 @@ func (ls *LDKService) UpdateLastWalletSyncRequest() {
}

func (ls *LDKService) GetSupportedNIP47Methods() []string {
return []string{"pay_invoice", "pay_keysend", "get_balance", "get_info", "make_invoice", "lookup_invoice", "list_transactions", "multi_pay_invoice", "multi_pay_keysend", "sign_message"}
return []string{"pay_invoice", "pay_keysend", "get_balance", "get_balance", "get_info", "make_invoice", "lookup_invoice", "list_transactions", "multi_pay_invoice", "multi_pay_keysend", "sign_message"}
}

func (ls *LDKService) GetSupportedNIP47NotificationTypes() []string {
Expand Down
2 changes: 1 addition & 1 deletion lnclient/lnd/lnd.go
Original file line number Diff line number Diff line change
Expand Up @@ -1084,7 +1084,7 @@ func (svc *LNDService) DisconnectPeer(ctx context.Context, peerId string) error

func (svc *LNDService) GetSupportedNIP47Methods() []string {
return []string{
"pay_invoice", "pay_keysend", "get_balance", "get_info", "make_invoice", "lookup_invoice", "list_transactions", "multi_pay_invoice", "multi_pay_keysend", "sign_message",
"pay_invoice", "pay_keysend", "get_balance", "get_balance", "get_info", "make_invoice", "lookup_invoice", "list_transactions", "multi_pay_invoice", "multi_pay_keysend", "sign_message",
}
}

Expand Down
2 changes: 1 addition & 1 deletion lnclient/phoenixd/phoenixd.go
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,7 @@ func (svc *PhoenixService) UpdateChannel(ctx context.Context, updateChannelReque
}

func (svc *PhoenixService) GetSupportedNIP47Methods() []string {
return []string{"pay_invoice", "get_balance", "get_info", "make_invoice", "lookup_invoice", "list_transactions", "multi_pay_invoice"}
return []string{"pay_invoice", "get_balance", "get_balance", "get_info", "make_invoice", "lookup_invoice", "list_transactions", "multi_pay_invoice"}
}

func (svc *PhoenixService) GetSupportedNIP47NotificationTypes() []string {
Expand Down
51 changes: 51 additions & 0 deletions nip47/controllers/get_budget_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package controllers

import (
"context"
"github.com/getAlby/hub/db/queries"
"github.com/nbd-wtf/go-nostr"

"github.com/getAlby/hub/db"
"github.com/getAlby/hub/logger"
"github.com/getAlby/hub/nip47/models"
"github.com/sirupsen/logrus"
)

type getBudgetResponse struct {
UsedBudget uint64 `json:"used_budget"`
TotalBudget uint64 `json:"total_budget"`
RenewsAt uint64 `json:"renews_at"`
RenewalPeriod string `json:"renewal_period"`
}

func (controller *nip47Controller) HandleGetBudgetEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, app *db.App, publishResponse publishFunc) {

logger.Logger.WithFields(logrus.Fields{
"request_event_id": requestEventId,
}).Debug("Getting budget")

appPermission := db.AppPermission{}
controller.db.Where("app_id = ? AND scope = ?", app.ID, models.PAY_INVOICE_METHOD).First(&appPermission)

maxAmount := appPermission.MaxAmountSat
if maxAmount == 0 {
publishResponse(&models.Response{
ResultType: nip47Request.Method,
Result: struct{}{},
}, nostr.Tags{})
return
}

usedBudget := queries.GetBudgetUsageSat(controller.db, &appPermission)
responsePayload := &getBudgetResponse{
TotalBudget: uint64(maxAmount * 1000),
UsedBudget: usedBudget * 1000,
RenewalPeriod: appPermission.BudgetRenewal,
RenewsAt: uint64(queries.GetBudgetRenewsAt(appPermission.BudgetRenewal)),
}

publishResponse(&models.Response{
ResultType: nip47Request.Method,
Result: responsePayload,
}, nostr.Tags{})
}
172 changes: 172 additions & 0 deletions nip47/controllers/get_budget_controller_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package controllers

import (
"context"
"encoding/json"
"testing"
"time"

"github.com/nbd-wtf/go-nostr"
"github.com/stretchr/testify/assert"

"github.com/getAlby/hub/constants"
"github.com/getAlby/hub/db"
"github.com/getAlby/hub/nip47/models"
"github.com/getAlby/hub/nip47/permissions"
"github.com/getAlby/hub/tests"
"github.com/getAlby/hub/transactions"
)

const nip47GetBudgetJson = `
{
"method": "get_budget"
}
`

func TestHandleGetBudgetEvent_noneUsed(t *testing.T) {
ctx := context.TODO()
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
assert.NoError(t, err)

nip47Request := &models.Request{}
err = json.Unmarshal([]byte(nip47GetBudgetJson), nip47Request)
assert.NoError(t, err)

app, _, err := tests.CreateApp(svc)
assert.NoError(t, err)
now := time.Now()

appPermission := &db.AppPermission{
AppId: app.ID,
App: *app,
Scope: constants.PAY_INVOICE_SCOPE,
MaxAmountSat: 400,
BudgetRenewal: constants.BUDGET_RENEWAL_MONTHLY,
}
err = svc.DB.Create(appPermission).Error
assert.NoError(t, err)

dbRequestEvent := &db.RequestEvent{}
err = svc.DB.Create(&dbRequestEvent).Error
assert.NoError(t, err)

var publishedResponse *models.Response

publishResponse := func(response *models.Response, tags nostr.Tags) {
publishedResponse = response
}

permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher)
NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
HandleGetBudgetEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)

assert.Equal(t, uint64(400000), publishedResponse.Result.(*getBudgetResponse).TotalBudget)
assert.Equal(t, uint64(0), publishedResponse.Result.(*getBudgetResponse).UsedBudget)
renewsAt := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()).AddDate(0, 1, 0).Unix()
assert.Equal(t, uint64(renewsAt), publishedResponse.Result.(*getBudgetResponse).RenewsAt)
assert.Equal(t, constants.BUDGET_RENEWAL_MONTHLY, publishedResponse.Result.(*getBudgetResponse).RenewalPeriod)
assert.Nil(t, publishedResponse.Error)
}

func TestHandleGetBudgetEvent_halfUsed(t *testing.T) {
ctx := context.TODO()
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
assert.NoError(t, err)

nip47Request := &models.Request{}
err = json.Unmarshal([]byte(nip47GetBudgetJson), nip47Request)
assert.NoError(t, err)

app, _, err := tests.CreateApp(svc)
assert.NoError(t, err)
now := time.Now()

appPermission := &db.AppPermission{
AppId: app.ID,
App: *app,
Scope: constants.PAY_INVOICE_SCOPE,
MaxAmountSat: 400,
BudgetRenewal: constants.BUDGET_RENEWAL_MONTHLY,
}
err = svc.DB.Create(appPermission).Error
assert.NoError(t, err)

svc.DB.Create(&db.Transaction{
AppId: &app.ID,
State: constants.TRANSACTION_STATE_SETTLED,
Type: constants.TRANSACTION_TYPE_OUTGOING,
AmountMsat: 200000,
})

dbRequestEvent := &db.RequestEvent{}
err = svc.DB.Create(&dbRequestEvent).Error
assert.NoError(t, err)

var publishedResponse *models.Response

publishResponse := func(response *models.Response, tags nostr.Tags) {
publishedResponse = response
}

permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher)
NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
HandleGetBudgetEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)

assert.Equal(t, uint64(400000), publishedResponse.Result.(*getBudgetResponse).TotalBudget)
assert.Equal(t, uint64(200000), publishedResponse.Result.(*getBudgetResponse).UsedBudget)
renewsAt := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()).AddDate(0, 1, 0).Unix()
assert.Equal(t, uint64(renewsAt), publishedResponse.Result.(*getBudgetResponse).RenewsAt)
assert.Equal(t, constants.BUDGET_RENEWAL_MONTHLY, publishedResponse.Result.(*getBudgetResponse).RenewalPeriod)
assert.Nil(t, publishedResponse.Error)
}

func TestHandleGetBudgetEvent_noBudget(t *testing.T) {
ctx := context.TODO()
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
assert.NoError(t, err)

nip47Request := &models.Request{}
err = json.Unmarshal([]byte(nip47GetBudgetJson), nip47Request)
assert.NoError(t, err)

app, _, err := tests.CreateApp(svc)
assert.NoError(t, err)

appPermission := &db.AppPermission{
AppId: app.ID,
App: *app,
Scope: constants.PAY_INVOICE_SCOPE,
}
err = svc.DB.Create(appPermission).Error
assert.NoError(t, err)

svc.DB.Create(&db.Transaction{
AppId: &app.ID,
State: constants.TRANSACTION_STATE_SETTLED,
Type: constants.TRANSACTION_TYPE_OUTGOING,
AmountMsat: 200000,
})

dbRequestEvent := &db.RequestEvent{}
err = svc.DB.Create(&dbRequestEvent).Error
assert.NoError(t, err)

var publishedResponse *models.Response

publishResponse := func(response *models.Response, tags nostr.Tags) {
publishedResponse = response
}

permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher)
transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher)
NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc).
HandleGetBudgetEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse)

assert.Equal(t, struct{}{}, publishedResponse.Result)
assert.Nil(t, publishedResponse.Error)
}
3 changes: 3 additions & 0 deletions nip47/event_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,9 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela
case models.GET_BALANCE_METHOD:
controller.
HandleGetBalanceEvent(ctx, nip47Request, requestEvent.ID, &app, publishResponse)
case models.GET_BUDGET_METHOD:
controller.
HandleGetBudgetEvent(ctx, nip47Request, requestEvent.ID, &app, publishResponse)
case models.MAKE_INVOICE_METHOD:
controller.
HandleMakeInvoiceEvent(ctx, nip47Request, requestEvent.ID, app.ID, publishResponse)
Expand Down
1 change: 1 addition & 0 deletions nip47/models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const (
// 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"
Expand Down
Loading

0 comments on commit 6208eff

Please sign in to comment.