Skip to content

Commit

Permalink
feat: backend-specific capabilities (#508)
Browse files Browse the repository at this point in the history
Co-authored-by: Adithya Vardhan <imadithyavardhan@gmail.com>
  • Loading branch information
rolznz and im-adithya authored Jul 1, 2024
1 parent d89f08b commit 12412ff
Show file tree
Hide file tree
Showing 39 changed files with 736 additions and 371 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ If the client creates the secret the client only needs to share the public key o
- `max_amount` (optional) maximum amount in sats that can be sent per renewal period
- `budget_renewal` (optional) reset the budget at the end of the given budget renewal. Can be `never` (default), `daily`, `weekly`, `monthly`, `yearly`
- `request_methods` (optional) url encoded, space separated list of request types that you need permission for: `pay_invoice` (default), `get_balance` (see NIP47). For example: `..&request_methods=pay_invoice%20get_balance`
- `notification_types` (optional) url encoded, space separated list of notification types that you need permission for: For example: `..&notification_types=payment_received%20payment_sent`

Example:

Expand Down
5 changes: 2 additions & 3 deletions alby/alby_oauth_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"math"
"net/http"
"strconv"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -377,7 +376,7 @@ func (svc *albyOAuthService) GetAuthUrl() string {
return svc.oauthConf.AuthCodeURL("unused")
}

func (svc *albyOAuthService) LinkAccount(ctx context.Context) error {
func (svc *albyOAuthService) LinkAccount(ctx context.Context, lnClient lnclient.LNClient) error {
connectionPubkey, err := svc.createAlbyAccountNWCNode(ctx)
if err != nil {
logger.Logger.WithError(err).Error("Failed to create alby account nwc node")
Expand All @@ -390,7 +389,7 @@ func (svc *albyOAuthService) LinkAccount(ctx context.Context) error {
1_000_000,
nip47.BUDGET_RENEWAL_MONTHLY,
nil,
strings.Split(nip47.CAPABILITIES, " "),
lnClient.GetSupportedNIP47Methods(),
)

if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion alby/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type AlbyOAuthService interface {
GetAuthUrl() string
GetUserIdentifier() (string, error)
IsConnected(ctx context.Context) bool
LinkAccount(ctx context.Context) error
LinkAccount(ctx context.Context, lnClient lnclient.LNClient) error
CallbackHandler(ctx context.Context, code string) error
GetBalance(ctx context.Context) (*AlbyBalance, error)
GetMe(ctx context.Context) (*AlbyMe, error)
Expand Down
107 changes: 62 additions & 45 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"io"
"net/http"
"net/url"
"strings"
"slices"
"sync"
"time"

Expand All @@ -21,7 +21,6 @@ import (
"github.com/getAlby/nostr-wallet-connect/events"
"github.com/getAlby/nostr-wallet-connect/lnclient"
"github.com/getAlby/nostr-wallet-connect/logger"
nip47 "github.com/getAlby/nostr-wallet-connect/nip47/models"
permissions "github.com/getAlby/nostr-wallet-connect/nip47/permissions"
"github.com/getAlby/nostr-wallet-connect/service"
"github.com/getAlby/nostr-wallet-connect/service/keys"
Expand Down Expand Up @@ -57,21 +56,17 @@ func (api *api) CreateApp(createAppRequest *CreateAppRequest) (*CreateAppRespons
return nil, fmt.Errorf("invalid expiresAt: %v", err)
}

// request methods are a space separated list of known request kinds TODO: it should be a string array in the API
requestMethods := strings.Split(createAppRequest.RequestMethods, " ")
if len(requestMethods) == 0 {
return nil, fmt.Errorf("won't create an app without request methods")
if len(createAppRequest.Scopes) == 0 {
return nil, fmt.Errorf("won't create an app without scopes")
}

for _, m := range requestMethods {
// TODO: this should be backend-specific
//if we don't know this method, we return an error
if !strings.Contains(nip47.CAPABILITIES, m) {
return nil, fmt.Errorf("did not recognize request method: %s", m)
for _, scope := range createAppRequest.Scopes {
if !slices.Contains(permissions.AllScopes(), scope) {
return nil, fmt.Errorf("did not recognize requested scope: %s", scope)
}
}

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

if err != nil {
return nil, err
Expand Down Expand Up @@ -110,11 +105,10 @@ func (api *api) UpdateApp(userApp *db.App, updateAppRequest *UpdateAppRequest) e
maxAmount := updateAppRequest.MaxAmount
budgetRenewal := updateAppRequest.BudgetRenewal

requestMethods := updateAppRequest.RequestMethods
if requestMethods == "" {
if len(updateAppRequest.Scopes) == 0 {
return fmt.Errorf("won't update an app to have no request methods")
}
newRequestMethods := strings.Split(requestMethods, " ")
newScopes := updateAppRequest.Scopes

expiresAt, err := api.parseExpiresAt(updateAppRequest.ExpiresAt)
if err != nil {
Expand All @@ -137,17 +131,17 @@ func (api *api) UpdateApp(userApp *db.App, updateAppRequest *UpdateAppRequest) e
return err
}

existingMethodMap := make(map[string]bool)
existingScopeMap := make(map[string]bool)
for _, perm := range existingPermissions {
existingMethodMap[perm.RequestMethod] = true
existingScopeMap[perm.Scope] = true
}

// Add new permissions
for _, method := range newRequestMethods {
if !existingMethodMap[method] {
for _, method := range newScopes {
if !existingScopeMap[method] {
perm := db.AppPermission{
App: *userApp,
RequestMethod: method,
Scope: method,
ExpiresAt: expiresAt,
MaxAmount: int(maxAmount),
BudgetRenewal: budgetRenewal,
Expand All @@ -156,12 +150,12 @@ func (api *api) UpdateApp(userApp *db.App, updateAppRequest *UpdateAppRequest) e
return err
}
}
delete(existingMethodMap, method)
delete(existingScopeMap, method)
}

// Remove old permissions
for method := range existingMethodMap {
if err := tx.Where("app_id = ? AND request_method = ?", userApp.ID, method).Delete(&db.AppPermission{}).Error; err != nil {
for method := range existingScopeMap {
if err := tx.Where("app_id = ? AND scope = ?", userApp.ID, method).Delete(&db.AppPermission{}).Error; err != nil {
return err
}
}
Expand Down Expand Up @@ -190,11 +184,11 @@ func (api *api) GetApp(userApp *db.App) *App {
requestMethods := []string{}
for _, appPerm := range appPermissions {
expiresAt = appPerm.ExpiresAt
if appPerm.RequestMethod == nip47.PAY_INVOICE_METHOD {
if appPerm.Scope == permissions.PAY_INVOICE_SCOPE {
//find the pay_invoice-specific permissions
paySpecificPermission = appPerm
}
requestMethods = append(requestMethods, appPerm.RequestMethod)
requestMethods = append(requestMethods, appPerm.Scope)
}

//renewsIn := ""
Expand All @@ -205,16 +199,16 @@ func (api *api) GetApp(userApp *db.App) *App {
}

response := App{
Name: userApp.Name,
Description: userApp.Description,
CreatedAt: userApp.CreatedAt,
UpdatedAt: userApp.UpdatedAt,
NostrPubkey: userApp.NostrPubkey,
ExpiresAt: expiresAt,
MaxAmount: maxAmount,
RequestMethods: requestMethods,
BudgetUsage: budgetUsage,
BudgetRenewal: paySpecificPermission.BudgetRenewal,
Name: userApp.Name,
Description: userApp.Description,
CreatedAt: userApp.CreatedAt,
UpdatedAt: userApp.UpdatedAt,
NostrPubkey: userApp.NostrPubkey,
ExpiresAt: expiresAt,
MaxAmount: maxAmount,
Scopes: requestMethods,
BudgetUsage: budgetUsage,
BudgetRenewal: paySpecificPermission.BudgetRenewal,
}

if lastEventResult.RowsAffected > 0 {
Expand All @@ -230,11 +224,11 @@ func (api *api) ListApps() ([]App, error) {
dbApps := []db.App{}
api.db.Find(&dbApps)

permissions := []db.AppPermission{}
api.db.Find(&permissions)
appPermissions := []db.AppPermission{}
api.db.Find(&appPermissions)

permissionsMap := make(map[uint][]db.AppPermission)
for _, perm := range permissions {
for _, perm := range appPermissions {
permissionsMap[perm.AppId] = append(permissionsMap[perm.AppId], perm)
}

Expand All @@ -249,14 +243,14 @@ func (api *api) ListApps() ([]App, error) {
NostrPubkey: userApp.NostrPubkey,
}

for _, permission := range permissionsMap[userApp.ID] {
apiApp.RequestMethods = append(apiApp.RequestMethods, permission.RequestMethod)
apiApp.ExpiresAt = permission.ExpiresAt
if permission.RequestMethod == nip47.PAY_INVOICE_METHOD {
apiApp.BudgetRenewal = permission.BudgetRenewal
apiApp.MaxAmount = uint64(permission.MaxAmount)
for _, appPermission := range permissionsMap[userApp.ID] {
apiApp.Scopes = append(apiApp.Scopes, appPermission.Scope)
apiApp.ExpiresAt = appPermission.ExpiresAt
if appPermission.Scope == permissions.PAY_INVOICE_SCOPE {
apiApp.BudgetRenewal = appPermission.BudgetRenewal
apiApp.MaxAmount = uint64(appPermission.MaxAmount)
if apiApp.MaxAmount > 0 {
apiApp.BudgetUsage = api.permissionsSvc.GetBudgetUsage(&permission)
apiApp.BudgetUsage = api.permissionsSvc.GetBudgetUsage(&appPermission)
}
}
}
Expand Down Expand Up @@ -687,6 +681,29 @@ func (api *api) Setup(ctx context.Context, setupRequest *SetupRequest) error {
return nil
}

func (api *api) GetWalletCapabilities(ctx context.Context) (*WalletCapabilitiesResponse, error) {
if api.svc.GetLNClient() == nil {
return nil, errors.New("LNClient not started")
}

methods := api.svc.GetLNClient().GetSupportedNIP47Methods()
notificationTypes := api.svc.GetLNClient().GetSupportedNIP47NotificationTypes()

scopes, err := permissions.RequestMethodsToScopes(methods)
if err != nil {
return nil, err
}
if len(notificationTypes) > 0 {
scopes = append(scopes, permissions.NOTIFICATIONS_SCOPE)
}

return &WalletCapabilitiesResponse{
Methods: methods,
NotificationTypes: notificationTypes,
Scopes: scopes,
}, nil
}

func (api *api) SendPaymentProbes(ctx context.Context, sendPaymentProbesRequest *SendPaymentProbesRequest) (*SendPaymentProbesResponse, error) {
if api.svc.GetLNClient() == nil {
return nil, errors.New("LNClient not started")
Expand Down
6 changes: 3 additions & 3 deletions api/backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"archive/zip"
"os"
"path/filepath"
"slices"

"crypto/aes"
"crypto/cipher"
Expand All @@ -19,6 +18,7 @@ import (

"github.com/getAlby/nostr-wallet-connect/db"
"github.com/getAlby/nostr-wallet-connect/logger"
"github.com/getAlby/nostr-wallet-connect/utils"
"golang.org/x/crypto/pbkdf2"
)

Expand Down Expand Up @@ -73,8 +73,8 @@ func (api *api) CreateBackup(unlockPassword string, w io.Writer) error {
logger.Logger.WithField("lnFiles", lnFiles).Info("Listed node storage dir")

// Avoid backing up log files.
slices.DeleteFunc(lnFiles, func(s string) bool {
return filepath.Ext(s) == ".log"
lnFiles = utils.Filter(lnFiles, func(s string) bool {
return filepath.Ext(s) != ".log"
})

filesToArchive = append(filesToArchive, lnFiles...)
Expand Down
41 changes: 24 additions & 17 deletions api/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ type API interface {
NewInstantChannelInvoice(ctx context.Context, request *NewInstantChannelInvoiceRequest) (*NewInstantChannelInvoiceResponse, error)
CreateBackup(unlockPassword string, w io.Writer) error
RestoreBackup(unlockPassword string, r io.Reader) error
GetWalletCapabilities(ctx context.Context) (*WalletCapabilitiesResponse, error)
}

type App struct {
Expand All @@ -62,33 +63,33 @@ type App struct {
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`

LastEventAt *time.Time `json:"lastEventAt"`
ExpiresAt *time.Time `json:"expiresAt"`
RequestMethods []string `json:"requestMethods"`
MaxAmount uint64 `json:"maxAmount"`
BudgetUsage uint64 `json:"budgetUsage"`
BudgetRenewal string `json:"budgetRenewal"`
LastEventAt *time.Time `json:"lastEventAt"`
ExpiresAt *time.Time `json:"expiresAt"`
Scopes []string `json:"scopes"`
MaxAmount uint64 `json:"maxAmount"`
BudgetUsage uint64 `json:"budgetUsage"`
BudgetRenewal string `json:"budgetRenewal"`
}

type ListAppsResponse struct {
Apps []App `json:"apps"`
}

type UpdateAppRequest struct {
MaxAmount uint64 `json:"maxAmount"`
BudgetRenewal string `json:"budgetRenewal"`
ExpiresAt string `json:"expiresAt"`
RequestMethods string `json:"requestMethods"`
MaxAmount uint64 `json:"maxAmount"`
BudgetRenewal string `json:"budgetRenewal"`
ExpiresAt string `json:"expiresAt"`
Scopes []string `json:"scopes"`
}

type CreateAppRequest struct {
Name string `json:"name"`
Pubkey string `json:"pubkey"`
MaxAmount uint64 `json:"maxAmount"`
BudgetRenewal string `json:"budgetRenewal"`
ExpiresAt string `json:"expiresAt"`
RequestMethods string `json:"requestMethods"`
ReturnTo string `json:"returnTo"`
Name string `json:"name"`
Pubkey string `json:"pubkey"`
MaxAmount uint64 `json:"maxAmount"`
BudgetRenewal string `json:"budgetRenewal"`
ExpiresAt string `json:"expiresAt"`
Scopes []string `json:"scopes"`
ReturnTo string `json:"returnTo"`
}

type StartRequest struct {
Expand Down Expand Up @@ -261,3 +262,9 @@ type NewInstantChannelInvoiceResponse struct {
IncomingLiquidity uint64 `json:"incomingLiquidity"`
OutgoingLiquidity uint64 `json:"outgoingLiquidity"`
}

type WalletCapabilitiesResponse struct {
Scopes []string `json:"scopes"`
Methods []string `json:"methods"`
NotificationTypes []string `json:"notificationTypes"`
}
11 changes: 6 additions & 5 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, requestMethods []string) (*App, string, error) {
func (svc *dbService) CreateApp(name string, pubkey string, maxAmount uint64, budgetRenewal string, expiresAt *time.Time, scopes []string) (*App, string, error) {
var pairingPublicKey string
var pairingSecretKey string
if pubkey == "" {
Expand All @@ -47,11 +47,11 @@ func (svc *dbService) CreateApp(name string, pubkey string, maxAmount uint64, bu
return err
}

for _, m := range requestMethods {
for _, scope := range scopes {
appPermission := AppPermission{
App: app,
RequestMethod: m,
ExpiresAt: expiresAt,
App: app,
Scope: scope,
ExpiresAt: expiresAt,
//these fields are only relevant for pay_invoice
MaxAmount: int(maxAmount),
BudgetRenewal: budgetRenewal,
Expand All @@ -61,6 +61,7 @@ func (svc *dbService) CreateApp(name string, pubkey string, maxAmount uint64, bu
return err
}
}

// commit transaction
return nil
})
Expand Down
Loading

0 comments on commit 12412ff

Please sign in to comment.