diff --git a/.mockery.yaml b/.mockery.yaml index f904dd45..8898627d 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -14,3 +14,6 @@ packages: github.com/getAlby/hub/lnclient: interfaces: LNClient: + github.com/getAlby/hub/config: + interfaces: + Config: diff --git a/alby/alby_oauth_service.go b/alby/alby_oauth_service.go index e5e53f74..1d6db3fe 100644 --- a/alby/alby_oauth_service.go +++ b/alby/alby_oauth_service.go @@ -673,7 +673,7 @@ func (svc *albyOAuthService) LinkAccount(ctx context.Context, lnClient lnclient. scopes = append(scopes, constants.NOTIFICATIONS_SCOPE) } - app, _, err := apps.NewAppsService(svc.db, svc.eventPublisher, svc.keys).CreateApp( + app, _, err := apps.NewAppsService(svc.db, svc.eventPublisher, svc.keys, svc.cfg).CreateApp( ALBY_ACCOUNT_APP_NAME, connectionPubkey, budget, diff --git a/api/api.go b/api/api.go index 3984bc23..89b65816 100644 --- a/api/api.go +++ b/api/api.go @@ -48,7 +48,7 @@ type api struct { func NewAPI(svc service.Service, gormDB *gorm.DB, config config.Config, keys keys.Keys, albyOAuthSvc alby.AlbyOAuthService, eventPublisher events.EventPublisher) *api { return &api{ db: gormDB, - appsSvc: apps.NewAppsService(gormDB, eventPublisher, keys), + appsSvc: apps.NewAppsService(gormDB, eventPublisher, keys, config), cfg: config, svc: svc, permissionsSvc: permissions.NewPermissionsService(gormDB, eventPublisher), @@ -58,13 +58,11 @@ func NewAPI(svc service.Service, gormDB *gorm.DB, config config.Config, keys key } func (api *api) CreateApp(createAppRequest *CreateAppRequest) (*CreateAppResponse, error) { - backendType, _ := api.cfg.Get("LNBackendType", "") - if createAppRequest.Isolated && - backendType != config.LDKBackendType && - backendType != config.LNDBackendType && - backendType != config.PhoenixBackendType { - return nil, fmt.Errorf( - "sub-wallets are currently not supported on your node backend. Try LDK or LND") + if slices.Contains(createAppRequest.Scopes, constants.SUPERUSER_SCOPE) { + if !api.cfg.CheckUnlockPassword(createAppRequest.UnlockPassword) { + return nil, fmt.Errorf( + "incorrect unlock password to create app with superuser permission") + } } expiresAt, err := api.parseExpiresAt(createAppRequest.ExpiresAt) @@ -72,10 +70,6 @@ func (api *api) CreateApp(createAppRequest *CreateAppRequest) (*CreateAppRespons return nil, fmt.Errorf("invalid expiresAt: %v", err) } - if len(createAppRequest.Scopes) == 0 { - return nil, fmt.Errorf("won't create an app without scopes") - } - for _, scope := range createAppRequest.Scopes { if !slices.Contains(permissions.AllScopes(), scope) { return nil, fmt.Errorf("did not recognize requested scope: %s", scope) @@ -106,7 +100,7 @@ func (api *api) CreateApp(createAppRequest *CreateAppRequest) (*CreateAppRespons responseBody := &CreateAppResponse{} responseBody.Id = app.ID - responseBody.Name = createAppRequest.Name + responseBody.Name = app.Name responseBody.Pubkey = app.AppPubkey responseBody.PairingSecret = pairingSecretKey responseBody.WalletPubkey = *app.WalletPubkey @@ -208,12 +202,17 @@ func (api *api) UpdateApp(userApp *db.App, updateAppRequest *UpdateAppRequest) e existingScopeMap[perm.Scope] = true } + if slices.Contains(newScopes, constants.SUPERUSER_SCOPE) && !existingScopeMap[constants.SUPERUSER_SCOPE] { + return fmt.Errorf( + "cannot update app to add superuser permission") + } + // Add new permissions - for _, method := range newScopes { - if !existingScopeMap[method] { + for _, scope := range newScopes { + if !existingScopeMap[scope] { perm := db.AppPermission{ App: *userApp, - Scope: method, + Scope: scope, ExpiresAt: expiresAt, MaxAmountSat: int(maxAmount), BudgetRenewal: budgetRenewal, @@ -222,12 +221,12 @@ func (api *api) UpdateApp(userApp *db.App, updateAppRequest *UpdateAppRequest) e return err } } - delete(existingScopeMap, method) + delete(existingScopeMap, scope) } // Remove old permissions - for method := range existingScopeMap { - if err := tx.Where("app_id = ? AND scope = ?", userApp.ID, method).Delete(&db.AppPermission{}).Error; err != nil { + for scope := range existingScopeMap { + if err := tx.Where("app_id = ? AND scope = ?", userApp.ID, scope).Delete(&db.AppPermission{}).Error; err != nil { return err } } diff --git a/api/apps_test.go b/api/apps_test.go new file mode 100644 index 00000000..ff00783f --- /dev/null +++ b/api/apps_test.go @@ -0,0 +1,23 @@ +package api + +import ( + "testing" + + "github.com/getAlby/hub/constants" + "github.com/getAlby/hub/tests/mocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCreateApp_SuperuserScopeIncorrectPassword(t *testing.T) { + cfg := mocks.NewMockConfig(t) + cfg.On("CheckUnlockPassword", "").Return(false) + theAPI := &api{svc: mocks.NewMockService(t), cfg: cfg} + response, err := theAPI.CreateApp(&CreateAppRequest{ + Scopes: []string{constants.SUPERUSER_SCOPE}, + }) + + assert.Nil(t, response) + require.Error(t, err) + assert.Equal(t, "incorrect unlock password to create app with superuser permission", err.Error()) +} diff --git a/api/models.go b/api/models.go index 06afd6f3..ecc1ab82 100644 --- a/api/models.go +++ b/api/models.go @@ -99,15 +99,16 @@ type TopupIsolatedAppRequest struct { } type CreateAppRequest struct { - Name string `json:"name"` - Pubkey string `json:"pubkey"` - MaxAmountSat uint64 `json:"maxAmount"` - BudgetRenewal string `json:"budgetRenewal"` - ExpiresAt string `json:"expiresAt"` - Scopes []string `json:"scopes"` - ReturnTo string `json:"returnTo"` - Isolated bool `json:"isolated"` - Metadata Metadata `json:"metadata,omitempty"` + Name string `json:"name"` + Pubkey string `json:"pubkey"` + MaxAmountSat uint64 `json:"maxAmount"` + BudgetRenewal string `json:"budgetRenewal"` + ExpiresAt string `json:"expiresAt"` + Scopes []string `json:"scopes"` + ReturnTo string `json:"returnTo"` + Isolated bool `json:"isolated"` + Metadata Metadata `json:"metadata,omitempty"` + UnlockPassword string `json:"unlockPassword"` } type StartRequest struct { diff --git a/apps/apps_service.go b/apps/apps_service.go index b98ec0f1..fb68edbf 100644 --- a/apps/apps_service.go +++ b/apps/apps_service.go @@ -6,8 +6,10 @@ import ( "errors" "fmt" "slices" + "strings" "time" + "github.com/getAlby/hub/config" "github.com/getAlby/hub/constants" "github.com/getAlby/hub/db" "github.com/getAlby/hub/events" @@ -28,20 +30,45 @@ type appsService struct { db *gorm.DB eventPublisher events.EventPublisher keys keys.Keys + cfg config.Config } -func NewAppsService(db *gorm.DB, eventPublisher events.EventPublisher, keys keys.Keys) *appsService { +func NewAppsService(db *gorm.DB, eventPublisher events.EventPublisher, keys keys.Keys, cfg config.Config) *appsService { return &appsService{ db: db, eventPublisher: eventPublisher, keys: keys, + cfg: cfg, } } func (svc *appsService) CreateApp(name string, pubkey string, maxAmountSat uint64, budgetRenewal string, expiresAt *time.Time, scopes []string, isolated bool, metadata map[string]interface{}) (*db.App, string, error) { - if isolated && (slices.Contains(scopes, constants.SIGN_MESSAGE_SCOPE)) { - // cannot sign messages because the isolated app is a custodial sub-wallet - return nil, "", errors.New("Sub-wallet app connection cannot have sign_message scope") + if isolated { + if slices.Contains(scopes, constants.SIGN_MESSAGE_SCOPE) { + // cannot sign messages because the isolated app is a custodial sub-wallet + return nil, "", errors.New("Sub-wallet app connection cannot have sign_message scope") + } + + backendType, _ := svc.cfg.Get("LNBackendType", "") + if backendType != config.LDKBackendType && + backendType != config.LNDBackendType && + backendType != config.PhoenixBackendType { + return nil, "", fmt.Errorf( + "sub-wallets are currently not supported on your node backend. Try LDK or LND") + } + } + + if budgetRenewal == "" { + budgetRenewal = constants.BUDGET_RENEWAL_NEVER + } + + if !slices.Contains(constants.GetBudgetRenewals(), budgetRenewal) { + return nil, "", fmt.Errorf("invalid budget renewal. Must be one of %s", strings.Join(constants.GetBudgetRenewals(), ",")) + } + + // ensure there is at least one scope + if scopes == nil || len(scopes) == 0 { + return nil, "", errors.New("no scopes provided") } var pairingPublicKey string @@ -69,7 +96,21 @@ func (svc *appsService) CreateApp(name string, pubkey string, maxAmountSat uint6 } } - app := db.App{Name: name, AppPubkey: pairingPublicKey, Isolated: isolated, Metadata: datatypes.JSON(metadataBytes)} + // use a suffix to avoid duplicate names + nameIndex := 0 + var freeName string + for ; ; nameIndex++ { + freeName = name + if nameIndex > 0 { + freeName += fmt.Sprintf(" (%d)", nameIndex) + } + existingApp := svc.GetAppByName(freeName) + if existingApp == nil { + break + } + } + + app := db.App{Name: freeName, AppPubkey: pairingPublicKey, Isolated: isolated, Metadata: datatypes.JSON(metadataBytes)} err := svc.db.Transaction(func(tx *gorm.DB) error { err := tx.Save(&app).Error @@ -151,3 +192,12 @@ func (svc *appsService) GetAppByPubkey(pubkey string) *db.App { } return &dbApp } + +func (svc *appsService) GetAppByName(name string) *db.App { + dbApp := db.App{} + findResult := svc.db.Where("name = ?", name).First(&dbApp) + if findResult.RowsAffected == 0 { + return nil + } + return &dbApp +} diff --git a/apps/tests/apps_service_test.go b/apps/tests/apps_service_test.go new file mode 100644 index 00000000..ee4a413d --- /dev/null +++ b/apps/tests/apps_service_test.go @@ -0,0 +1,58 @@ +package tests + +import ( + "testing" + + "github.com/getAlby/hub/apps" + "github.com/getAlby/hub/config" + "github.com/getAlby/hub/constants" + "github.com/getAlby/hub/tests" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHandleCreateApp_NilScopes(t *testing.T) { + // ctx := context.TODO() + svc, err := tests.CreateTestService(t) + require.NoError(t, err) + defer svc.Remove() + + appsService := apps.NewAppsService(svc.DB, svc.EventPublisher, svc.Keys, svc.Cfg) + app, secretKey, err := appsService.CreateApp("Test", "", 0, "monthly", nil, nil, false, nil) + + assert.Nil(t, app) + assert.Equal(t, "", secretKey) + require.Error(t, err) + assert.Equal(t, "no scopes provided", err.Error()) +} + +func TestHandleCreateApp_EmptyScopes(t *testing.T) { + // ctx := context.TODO() + svc, err := tests.CreateTestService(t) + require.NoError(t, err) + defer svc.Remove() + + appsService := apps.NewAppsService(svc.DB, svc.EventPublisher, svc.Keys, svc.Cfg) + app, secretKey, err := appsService.CreateApp("Test", "", 0, "monthly", nil, []string{}, false, nil) + + assert.Nil(t, app) + assert.Equal(t, "", secretKey) + require.Error(t, err) + assert.Equal(t, "no scopes provided", err.Error()) +} + +func TestHandleCreateApp_IsolatedUnsupportedBackendType(t *testing.T) { + // ctx := context.TODO() + svc, err := tests.CreateTestService(t) + require.NoError(t, err) + defer svc.Remove() + svc.Cfg.SetUpdate("BackendType", config.CashuBackendType, "") + + appsService := apps.NewAppsService(svc.DB, svc.EventPublisher, svc.Keys, svc.Cfg) + app, secretKey, err := appsService.CreateApp("Test", "", 0, "monthly", nil, []string{constants.GET_INFO_SCOPE}, true, nil) + + assert.Nil(t, app) + assert.Equal(t, "", secretKey) + require.Error(t, err) + assert.Equal(t, "sub-wallets are currently not supported on your node backend. Try LDK or LND", err.Error()) +} diff --git a/constants/constants.go b/constants/constants.go index 620f19d9..70b8f2e0 100644 --- a/constants/constants.go +++ b/constants/constants.go @@ -19,6 +19,16 @@ const ( BUDGET_RENEWAL_NEVER = "never" ) +func GetBudgetRenewals() []string { + return []string{ + BUDGET_RENEWAL_DAILY, + BUDGET_RENEWAL_WEEKLY, + BUDGET_RENEWAL_MONTHLY, + BUDGET_RENEWAL_YEARLY, + BUDGET_RENEWAL_NEVER, + } +} + const ( PAY_INVOICE_SCOPE = "pay_invoice" // also covers pay_keysend and multi_* payment methods GET_BALANCE_SCOPE = "get_balance" @@ -28,6 +38,7 @@ const ( LIST_TRANSACTIONS_SCOPE = "list_transactions" SIGN_MESSAGE_SCOPE = "sign_message" NOTIFICATIONS_SCOPE = "notifications" // covers all notification types + SUPERUSER_SCOPE = "superuser" ) // limit encoded metadata length, otherwise relays may have trouble listing multiple transactions diff --git a/frontend/src/components/Permissions.tsx b/frontend/src/components/Permissions.tsx index b9d1301d..be640540 100644 --- a/frontend/src/components/Permissions.tsx +++ b/frontend/src/components/Permissions.tsx @@ -1,4 +1,4 @@ -import { BrickWall, PlusCircle } from "lucide-react"; +import { AlertTriangleIcon, BrickWall, PlusCircle } from "lucide-react"; import React from "react"; import BudgetAmountSelect from "src/components/BudgetAmountSelect"; import BudgetRenewalSelect from "src/components/BudgetRenewalSelect"; @@ -227,6 +227,20 @@ const Permissions: React.FC = ({ )} + + {permissions.scopes.includes("superuser") && ( + <> +
+ +

+ This app can create other app connections +

+
+

+ Make sure to set budgets on connections created by this app. +

+ + )} ); }; diff --git a/frontend/src/components/Scopes.tsx b/frontend/src/components/Scopes.tsx index 99f83211..446b2ccf 100644 --- a/frontend/src/components/Scopes.tsx +++ b/frontend/src/components/Scopes.tsx @@ -87,10 +87,17 @@ const Scopes: React.FC = ({ }, [capabilities.scopes]); const [scopeGroup, setScopeGroup] = React.useState(() => { - if (isolated && scopes.length === capabilities.scopes.length) { + if ( + isolated && + scopes.length === isolatedScopes.length && + scopes.every((scope) => isolatedScopes.includes(scope)) + ) { return "isolated"; } - if (scopes.length === capabilities.scopes.length) { + if ( + scopes.length === fullAccessScopes.length && + scopes.every((scope) => fullAccessScopes.includes(scope)) + ) { return "full_access"; } if ( diff --git a/frontend/src/components/SuggestedAppData.tsx b/frontend/src/components/SuggestedAppData.tsx index 0c695059..63c0db01 100644 --- a/frontend/src/components/SuggestedAppData.tsx +++ b/frontend/src/components/SuggestedAppData.tsx @@ -63,6 +63,19 @@ export const suggestedApps: SuggestedApp[] = [ internal: true, logo: uncleJim, }, + { + id: "alby-go", + title: "Alby Go", + description: "A simple mobile wallet that works great with Alby Hub", + webLink: "https://albygo.com", + playLink: + "https://play.google.com/store/apps/details?id=com.getalby.mobile", + appleLink: "https://apps.apple.com/us/app/alby-go/id6471335774", + zapStoreLink: "https://zapstore.dev/download/", + logo: albyGo, + extendedDescription: "Sends and receives payments seamlessly from your Hub", + internal: true, + }, { id: "buzzpay", title: "BuzzPay PoS", @@ -1623,59 +1636,6 @@ export const suggestedApps: SuggestedApp[] = [ ), }, - { - id: "alby-go", - title: "Alby Go", - description: "A simple mobile wallet that works great with Alby Hub", - webLink: "https://albygo.com", - playLink: - "https://play.google.com/store/apps/details?id=com.getalby.mobile", - appleLink: "https://apps.apple.com/us/app/alby-go/id6471335774", - zapStoreLink: "https://zapstore.dev/download/", - logo: albyGo, - extendedDescription: "Sends and receives payments seamlessly from your Hub", - guide: ( - <> -
-

In Alby Go

-
    -
  • - 1. Download and open{" "} - Alby Go on - your Android or iOS device -
  • -
  • - 2. Click on{" "} - - Connect Wallet - -
  • -
-
-
-

In Alby Hub

-
    -
  • - 4. Click{" "} - - Connect to Alby Go - -
  • -
  • 5. Set app's wallet permissions (full access recommended)
  • -
-
-
-

In Alby Go

-
    -
  • 6. Scan or paste the connection secret from Alby Hub
  • -
-
- - ), - }, { id: "pullthatupjamie-ai", title: "Pull That Up Jamie!", diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index 945c1ac4..78249c93 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -36,6 +36,7 @@ import { OpeningAutoChannel } from "src/screens/channels/auto/OpeningAutoChannel import { FirstChannel } from "src/screens/channels/first/FirstChannel"; import { OpenedFirstChannel } from "src/screens/channels/first/OpenedFirstChannel"; import { OpeningFirstChannel } from "src/screens/channels/first/OpeningFirstChannel"; +import { AlbyGo } from "src/screens/internal-apps/AlbyGo"; import { BuzzPay } from "src/screens/internal-apps/BuzzPay"; import { SimpleBoost } from "src/screens/internal-apps/SimpleBoost"; import { UncleJim } from "src/screens/internal-apps/UncleJim"; @@ -241,6 +242,10 @@ const routes = [ path: "uncle-jim", element: , }, + { + path: "alby-go", + element: , + }, { path: "buzzpay", element: , diff --git a/frontend/src/screens/apps/NewApp.tsx b/frontend/src/screens/apps/NewApp.tsx index bd7d7b9c..679c9646 100644 --- a/frontend/src/screens/apps/NewApp.tsx +++ b/frontend/src/screens/apps/NewApp.tsx @@ -20,7 +20,6 @@ import { Label } from "src/components/ui/label"; import { LoadingButton } from "src/components/ui/loading-button"; import { Separator } from "src/components/ui/separator"; import { useToast } from "src/components/ui/use-toast"; -import { useApps } from "src/hooks/useApps"; import { useCapabilities } from "src/hooks/useCapabilities"; import { createApp } from "src/requests/createApp"; import { handleRequestError } from "src/utils/handleRequestError"; @@ -45,7 +44,6 @@ const NewAppInternal = ({ capabilities }: NewAppInternalProps) => { const { toast } = useToast(); const navigate = useNavigate(); - const { data: apps } = useApps(); const [unsupportedError, setUnsupportedError] = useState(); const [isLoading, setLoading] = React.useState(false); @@ -187,10 +185,6 @@ const NewAppInternal = ({ capabilities }: NewAppInternalProps) => { setLoading(true); try { - if (apps?.some((existingApp) => existingApp.name === appName)) { - throw new Error("A connection with the same name already exists."); - } - const createAppRequest: CreateAppRequest = { name: appName, pubkey, diff --git a/frontend/src/screens/apps/ShowApp.tsx b/frontend/src/screens/apps/ShowApp.tsx index 3e269c99..bc80461f 100644 --- a/frontend/src/screens/apps/ShowApp.tsx +++ b/frontend/src/screens/apps/ShowApp.tsx @@ -42,7 +42,6 @@ import { import { Input } from "src/components/ui/input"; import { Table, TableBody, TableCell, TableRow } from "src/components/ui/table"; import { useToast } from "src/components/ui/use-toast"; -import { useApps } from "src/hooks/useApps"; import { useCapabilities } from "src/hooks/useCapabilities"; function ShowApp() { @@ -77,7 +76,6 @@ function AppInternal({ app, refetchApp, capabilities }: AppInternalProps) { const { toast } = useToast(); const navigate = useNavigate(); const location = useLocation(); - const { data: apps } = useApps(); const [isEditingName, setIsEditingName] = React.useState(false); const [isEditingPermissions, setIsEditingPermissions] = React.useState(false); @@ -102,16 +100,6 @@ function AppInternal({ app, refetchApp, capabilities }: AppInternalProps) { const handleSave = async () => { try { - if ( - isEditingName && - apps?.some( - (existingApp) => - existingApp.name === name && existingApp.id !== app.id - ) - ) { - throw new Error("A connection with the same name already exists."); - } - const updateAppRequest: UpdateAppRequest = { name, scopes: Array.from(permissions.scopes), diff --git a/frontend/src/screens/internal-apps/AlbyGo.tsx b/frontend/src/screens/internal-apps/AlbyGo.tsx new file mode 100644 index 00000000..bfa75a2d --- /dev/null +++ b/frontend/src/screens/internal-apps/AlbyGo.tsx @@ -0,0 +1,329 @@ +import { Globe } from "lucide-react"; +import React from "react"; +import { Link } from "react-router-dom"; +import AppHeader from "src/components/AppHeader"; +import ExternalLink from "src/components/ExternalLink"; +import { AppleIcon } from "src/components/icons/Apple"; +import { ChromeIcon } from "src/components/icons/Chrome"; +import { FirefoxIcon } from "src/components/icons/Firefox"; +import { NostrWalletConnectIcon } from "src/components/icons/NostrWalletConnectIcon"; +import { PlayStoreIcon } from "src/components/icons/PlayStore"; +import { ZapStoreIcon } from "src/components/icons/ZapStore"; +import Loading from "src/components/Loading"; +import { suggestedApps } from "src/components/SuggestedAppData"; +import { + AlertDialog, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "src/components/ui/alert-dialog"; +import { Button } from "src/components/ui/button"; +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, +} from "src/components/ui/card"; +import { Input } from "src/components/ui/input"; +import { Label } from "src/components/ui/label"; +import { LoadingButton } from "src/components/ui/loading-button"; +import { useToast } from "src/components/ui/use-toast"; +import { useApp } from "src/hooks/useApp"; +import { useCapabilities } from "src/hooks/useCapabilities"; +import { createApp } from "src/requests/createApp"; +import { ConnectAppCard } from "src/screens/apps/AppCreated"; + +export function AlbyGo() { + const [loading, setLoading] = React.useState(false); + const [appPubkey, setAppPubkey] = React.useState(); + const [connectionSecret, setConnectionSecret] = React.useState(""); + const [unlockPassword, setUnlockPassword] = React.useState(""); + const [showCreateConnectionDialog, setShowCreateConnectionDialog] = + React.useState(false); + const { data: createdApp } = useApp(appPubkey, true); + const { toast } = useToast(); + const { data: capabilities } = useCapabilities(); + + const app = suggestedApps.find((app) => app.id === "alby-go"); + if (!app) { + return null; + } + + function onClickCreateConnection() { + setShowCreateConnectionDialog(true); + } + + async function onSubmitCreateConnection(e: React.FormEvent) { + e.preventDefault(); + setLoading(true); + try { + if (!capabilities) { + throw new Error("capabilities not loaded"); + } + + // TODO: fetch scopes from useCapabilities + const createAppResponse = await createApp({ + name: "Alby Go", + scopes: [...capabilities.scopes, "superuser"], + isolated: false, + metadata: { + app_store_app_id: "alby-go", + }, + unlockPassword, + maxAmount: 100_000, + budgetRenewal: "monthly", + }); + setConnectionSecret(createAppResponse.pairingUri); + setAppPubkey(createAppResponse.pairingPublicKey); + toast({ title: "Alby Go connection created" }); + } catch (error) { + console.error(error); + toast({ + variant: "destructive", + title: "Something went wrong: " + error, + }); + } + setLoading(false); + setShowCreateConnectionDialog(false); + setUnlockPassword(""); + } + + if (!capabilities) { + return ; + } + + return ( +
+ + +
+ + Confirm New Connection + +
+

+ Alby Go will be given permission to create other app + connections which can spend your balance. +

+ +

+ Alby Go will be given a 100k sat / month budget by default + which you can edit after creating the connection. +

+ +

+ Warning: Alby Go can create connections with a larger budget + than the one set for Alby Go. Make sure to always set a + budget. +

+ +

+ Please enter your unlock password to continue. +

+
+ + setUnlockPassword(e.target.value)} + value={unlockPassword} + /> +
+
+
+
+ + setShowCreateConnectionDialog(false)} + > + Cancel + + + Confirm + + +
+
+
+ +
+ +
+
{app.title}
+
+ {app.description} +
+
+
+ + } + description="" + contentRight={ + !createdApp && ( + + + + ) + } + /> +
+
+ + + About the App + + {app.extendedDescription && ( + +

+ {app.extendedDescription} +

+
+ )} +
+ + + How to Connect + + + <> +
+

In Alby Go

+
    +
  • + 1. Download and open{" "} + + Alby Go + {" "} + on your Android or iOS device +
  • +
  • + 2. Click on{" "} + + Connect Wallet + +
  • +
  • + 3.{" "} + + Scan or paste + {" "} + the connection secret from Alby Hub that will be revealed + once you create the connection below. +
  • +
+
+ +
+
+
+
+ {(app.appleLink || + app.playLink || + app.zapStoreLink || + app.chromeLink || + app.firefoxLink) && ( + + + Get This App + + + {app.playLink && ( + + + + )} + {app.appleLink && ( + + + + )} + {app.zapStoreLink && ( + + + + )} + {app.chromeLink && ( + + + + )} + {app.firefoxLink && ( + + + + )} + + + )} + {app.webLink && ( + + + Links + + + {app.webLink && ( + + + + )} + + + )} + {createdApp && connectionSecret && ( +
+ +
+ )} + {!createdApp && ( + + + One Tap Connections + + +

+ Use Alby Go to quickly connect other apps to your hub with one + tap on mobile. +

+ { + + } +
+
+ )} +
+
+
+ ); +} diff --git a/frontend/src/screens/internal-apps/BuzzPay.tsx b/frontend/src/screens/internal-apps/BuzzPay.tsx index d344993f..5e4752b9 100644 --- a/frontend/src/screens/internal-apps/BuzzPay.tsx +++ b/frontend/src/screens/internal-apps/BuzzPay.tsx @@ -37,10 +37,6 @@ export function BuzzPay() { setLoading(true); (async () => { try { - if (apps?.some((existingApp) => existingApp.name === name)) { - throw new Error("A connection with the same name already exists."); - } - const createAppResponse = await createApp({ name, scopes: ["get_info", "lookup_invoice", "make_invoice"], diff --git a/frontend/src/screens/internal-apps/SimpleBoost.tsx b/frontend/src/screens/internal-apps/SimpleBoost.tsx index 186194df..1d3757b2 100644 --- a/frontend/src/screens/internal-apps/SimpleBoost.tsx +++ b/frontend/src/screens/internal-apps/SimpleBoost.tsx @@ -44,10 +44,6 @@ export function SimpleBoost() { setLoading(true); (async () => { try { - if (apps?.some((existingApp) => existingApp.name === name)) { - throw new Error("A connection with the same name already exists."); - } - const createAppResponse = await createApp({ name, scopes: ["lookup_invoice", "make_invoice"], diff --git a/frontend/src/screens/internal-apps/UncleJim.tsx b/frontend/src/screens/internal-apps/UncleJim.tsx index 05dca232..1269c2f4 100644 --- a/frontend/src/screens/internal-apps/UncleJim.tsx +++ b/frontend/src/screens/internal-apps/UncleJim.tsx @@ -41,10 +41,6 @@ export function UncleJim() { setLoading(true); try { - if (apps?.some((existingApp) => existingApp.name === name)) { - throw new Error("A connection with the same name already exists."); - } - const createAppRequest: CreateAppRequest = { name, scopes: [ diff --git a/frontend/src/screens/internal-apps/ZapPlanner.tsx b/frontend/src/screens/internal-apps/ZapPlanner.tsx index 933df17f..64bc5988 100644 --- a/frontend/src/screens/internal-apps/ZapPlanner.tsx +++ b/frontend/src/screens/internal-apps/ZapPlanner.tsx @@ -103,10 +103,6 @@ export function ZapPlanner() { e.preventDefault(); setSubmitting(true); try { - if (apps?.some((existingApp) => existingApp.name === recipientName)) { - throw new Error("A connection with the same name already exists."); - } - // validate lighning address const ln = new LightningAddress(recipientLightningAddress); await ln.fetch(); diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 70d53557..cd7b8f4e 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -1,6 +1,7 @@ import { Bell, CirclePlus, + Crown, HandCoins, Info, LucideIcon, @@ -47,7 +48,8 @@ export type Scope = | "lookup_invoice" | "list_transactions" | "sign_message" - | "notifications"; // covers all notification types + | "notifications" // covers all notification types + | "superuser"; export type Nip47NotificationType = "payment_received" | "payment_sent"; @@ -64,6 +66,7 @@ export const scopeIconMap: ScopeIconMap = { pay_invoice: HandCoins, sign_message: PenLine, notifications: Bell, + superuser: Crown, }; export type WalletCapabilities = { @@ -89,6 +92,7 @@ export const scopeDescriptions: Record = { pay_invoice: "Send payments", sign_message: "Sign messages", notifications: "Receive wallet notifications", + superuser: "Create other app connections", }; export const expiryOptions: Record = { @@ -195,6 +199,7 @@ export interface CreateAppRequest { returnTo?: string; isolated?: boolean; metadata?: AppMetadata; + unlockPassword?: string; // required to create superuser apps } export interface CreateAppResponse { diff --git a/http/http_service.go b/http/http_service.go index 38c7585a..f04683f8 100644 --- a/http/http_service.go +++ b/http/http_service.go @@ -53,7 +53,7 @@ func NewHttpService(svc service.Service, eventPublisher events.EventPublisher) * cfg: svc.GetConfig(), eventPublisher: eventPublisher, db: svc.GetDB(), - appsSvc: apps.NewAppsService(svc.GetDB(), eventPublisher, svc.GetKeys()), + appsSvc: apps.NewAppsService(svc.GetDB(), eventPublisher, svc.GetKeys(), svc.GetConfig()), } } diff --git a/nip47/controllers/controllers_test.go b/nip47/controllers/controllers_test.go new file mode 100644 index 00000000..fc807d7a --- /dev/null +++ b/nip47/controllers/controllers_test.go @@ -0,0 +1,15 @@ +package controllers + +import ( + "github.com/getAlby/hub/alby" + "github.com/getAlby/hub/nip47/permissions" + "github.com/getAlby/hub/tests" + "github.com/getAlby/hub/transactions" +) + +func NewTestNip47Controller(svc *tests.TestService) *nip47Controller { + permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) + transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) + albyOAuthSvc := alby.NewAlbyOAuthService(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher) + return NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService, albyOAuthSvc) +} diff --git a/nip47/controllers/create_connection_controller.go b/nip47/controllers/create_connection_controller.go new file mode 100644 index 00000000..6c14e117 --- /dev/null +++ b/nip47/controllers/create_connection_controller.go @@ -0,0 +1,135 @@ +package controllers + +import ( + "context" + "slices" + "time" + + "github.com/getAlby/hub/constants" + "github.com/getAlby/hub/logger" + "github.com/getAlby/hub/nip47/models" + "github.com/getAlby/hub/nip47/permissions" + "github.com/nbd-wtf/go-nostr" + "github.com/sirupsen/logrus" +) + +type createConnectionParams struct { + Pubkey string `json:"pubkey"` // pubkey of the app connection + Name string `json:"name"` + RequestMethods []string `json:"request_methods"` + NotificationTypes []string `json:"notification_types"` + MaxAmount uint64 `json:"max_amount"` + BudgetRenewal string `json:"budget_renewal"` + ExpiresAt *uint64 `json:"expires_at"` // unix timestamp + Isolated bool `json:"isolated"` + Metadata map[string]interface{} `json:"metadata,omitempty"` +} + +type createConnectionResponse struct { + // pubkey is given, user requesting already knows relay. + WalletPubkey string `json:"wallet_pubkey"` +} + +func (controller *nip47Controller) HandleCreateConnectionEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, publishResponse publishFunc) { + params := &createConnectionParams{} + resp := decodeRequest(nip47Request, params) + if resp != nil { + publishResponse(resp, nostr.Tags{}) + return + } + + logger.Logger.WithFields(logrus.Fields{ + "request_event_id": requestEventId, + "params": params, + }).Info("creating app") + + var expiresAt *time.Time + if params.ExpiresAt != nil { + expiresAtUnsigned := *params.ExpiresAt + expiresAtValue := time.Unix(int64(expiresAtUnsigned), 0) + expiresAt = &expiresAtValue + } + + maxAmountSat := params.MaxAmount / 1000 + + // explicitly do not allow creating an app with create_connection permission + if slices.Contains(params.RequestMethods, models.CREATE_CONNECTION_METHOD) { + publishResponse(&models.Response{ + ResultType: nip47Request.Method, + Error: &models.Error{ + Code: constants.ERROR_INTERNAL, + Message: "cannot create a new app that has create_connection permission via NWC", + }, + }, nostr.Tags{}) + return + } + + // ensure there is at least one request method + if len(params.RequestMethods) == 0 { + publishResponse(&models.Response{ + ResultType: nip47Request.Method, + Error: &models.Error{ + Code: constants.ERROR_INTERNAL, + Message: "No request methods provided", + }, + }, nostr.Tags{}) + return + } + + supportedMethods := controller.lnClient.GetSupportedNIP47Methods() + if slices.ContainsFunc(params.RequestMethods, func(method string) bool { + return !slices.Contains(supportedMethods, method) + }) { + publishResponse(&models.Response{ + ResultType: nip47Request.Method, + Error: &models.Error{ + Code: constants.ERROR_INTERNAL, + Message: "One or more methods are not supported by the current LNClient", + }, + }, nostr.Tags{}) + return + } + + scopes, err := permissions.RequestMethodsToScopes(params.RequestMethods) + + supportedNotificationTypes := controller.lnClient.GetSupportedNIP47NotificationTypes() + if len(params.NotificationTypes) > 0 { + if slices.ContainsFunc(params.NotificationTypes, func(method string) bool { + return !slices.Contains(supportedNotificationTypes, method) + }) { + publishResponse(&models.Response{ + ResultType: nip47Request.Method, + Error: &models.Error{ + Code: constants.ERROR_INTERNAL, + Message: "One or more notification types are not supported by the current LNClient", + }, + }, nostr.Tags{}) + return + } + scopes = append(scopes, constants.NOTIFICATIONS_SCOPE) + } + + app, _, err := controller.appsService.CreateApp(params.Name, params.Pubkey, maxAmountSat, params.BudgetRenewal, expiresAt, scopes, params.Isolated, params.Metadata) + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "request_event_id": requestEventId, + }).WithError(err).Error("Failed to create app") + publishResponse(&models.Response{ + ResultType: nip47Request.Method, + Error: &models.Error{ + Code: constants.ERROR_INTERNAL, + Message: err.Error(), + }, + }, nostr.Tags{}) + return + } + + responsePayload := createConnectionResponse{ + WalletPubkey: *app.WalletPubkey, + } + + publishResponse(&models.Response{ + ResultType: nip47Request.Method, + Result: responsePayload, + }, nostr.Tags{}) +} diff --git a/nip47/controllers/create_connection_controller_test.go b/nip47/controllers/create_connection_controller_test.go new file mode 100644 index 00000000..063866ac --- /dev/null +++ b/nip47/controllers/create_connection_controller_test.go @@ -0,0 +1,363 @@ +package controllers + +import ( + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/nbd-wtf/go-nostr" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/getAlby/hub/apps" + "github.com/getAlby/hub/config" + "github.com/getAlby/hub/constants" + "github.com/getAlby/hub/db" + "github.com/getAlby/hub/nip47/models" + "github.com/getAlby/hub/tests" +) + +func TestHandleCreateConnectionEvent(t *testing.T) { + ctx := context.TODO() + svc, err := tests.CreateTestService(t) + svc.Cfg.SetUpdate("LNBackendType", config.LDKBackendType, "") + require.NoError(t, err) + defer svc.Remove() + + pairingSecretKey := nostr.GeneratePrivateKey() + pairingPublicKey, err := nostr.GetPublicKey(pairingSecretKey) + require.NoError(t, err) + + nip47CreateConnectionJson := fmt.Sprintf(` +{ + "method": "create_connection", + "params": { + "pubkey": "%s", + "name": "Test 123", + "request_methods": ["get_info", "pay_invoice"], + "notification_types": ["payment_received"], + "max_amount": 100000000, + "budget_renewal": "monthly", + "isolated": true + } +} +`, pairingPublicKey) + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47CreateConnectionJson), nip47Request) + 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 + } + + NewTestNip47Controller(svc). + HandleCreateConnectionEvent(ctx, nip47Request, dbRequestEvent.ID, publishResponse) + + assert.Nil(t, publishedResponse.Error) + assert.Equal(t, models.CREATE_CONNECTION_METHOD, publishedResponse.ResultType) + createAppResult := publishedResponse.Result.(createConnectionResponse) + + assert.NotNil(t, createAppResult.WalletPubkey) + app := db.App{} + err = svc.DB.First(&app).Error + assert.NoError(t, err) + assert.Equal(t, pairingPublicKey, app.AppPubkey) + assert.Equal(t, createAppResult.WalletPubkey, *app.WalletPubkey) + + permissions := []db.AppPermission{} + err = svc.DB.Find(&permissions).Error + assert.NoError(t, err) + assert.Equal(t, 3, len(permissions)) + assert.Equal(t, constants.GET_INFO_SCOPE, permissions[0].Scope) + assert.Equal(t, constants.PAY_INVOICE_SCOPE, permissions[1].Scope) + assert.Equal(t, constants.NOTIFICATIONS_SCOPE, permissions[2].Scope) + + assert.True(t, app.Isolated) + assert.Equal(t, 100_000, permissions[1].MaxAmountSat) + assert.Equal(t, constants.BUDGET_RENEWAL_MONTHLY, permissions[1].BudgetRenewal) +} + +func TestHandleCreateConnectionEvent_IsolatedUnsupportedBackendType(t *testing.T) { + ctx := context.TODO() + svc, err := tests.CreateTestService(t) + require.NoError(t, err) + defer svc.Remove() + svc.Cfg.SetUpdate("BackendType", config.CashuBackendType, "") + + pairingSecretKey := nostr.GeneratePrivateKey() + pairingPublicKey, err := nostr.GetPublicKey(pairingSecretKey) + require.NoError(t, err) + + nip47CreateConnectionJson := fmt.Sprintf(` +{ + "method": "create_connection", + "params": { + "pubkey": "%s", + "name": "Test 123", + "request_methods": ["get_info", "pay_invoice"], + "notification_types": ["payment_received"], + "max_amount": 100000000, + "budget_renewal": "monthly", + "isolated": true + } +} +`, pairingPublicKey) + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47CreateConnectionJson), nip47Request) + 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 + } + + NewTestNip47Controller(svc). + HandleCreateConnectionEvent(ctx, nip47Request, dbRequestEvent.ID, publishResponse) + + assert.NotNil(t, publishedResponse.Error) + assert.Equal(t, constants.ERROR_INTERNAL, publishedResponse.Error.Code) + assert.Equal(t, "sub-wallets are currently not supported on your node backend. Try LDK or LND", publishedResponse.Error.Message) + assert.Equal(t, models.CREATE_CONNECTION_METHOD, publishedResponse.ResultType) +} + +func TestHandleCreateConnectionEvent_PubkeyAlreadyExists(t *testing.T) { + ctx := context.TODO() + svc, err := tests.CreateTestService(t) + require.NoError(t, err) + defer svc.Remove() + + pairingSecretKey := nostr.GeneratePrivateKey() + pairingPublicKey, err := nostr.GetPublicKey(pairingSecretKey) + require.NoError(t, err) + + appsSvc := apps.NewAppsService(svc.DB, svc.EventPublisher, svc.Keys, svc.Cfg) + _, _, err = appsSvc.CreateApp("Existing App", pairingPublicKey, 0, constants.BUDGET_RENEWAL_NEVER, nil, []string{models.GET_INFO_METHOD}, false, nil) + + nip47CreateConnectionJson := fmt.Sprintf(` +{ + "method": "create_connection", + "params": { + "pubkey": "%s", + "name": "Test 123", + "request_methods": ["get_info"] + } +} +`, pairingPublicKey) + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47CreateConnectionJson), nip47Request) + 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 + } + + NewTestNip47Controller(svc). + HandleCreateConnectionEvent(ctx, nip47Request, dbRequestEvent.ID, publishResponse) + + assert.NotNil(t, publishedResponse.Error) + assert.Equal(t, constants.ERROR_INTERNAL, publishedResponse.Error.Code) + assert.Equal(t, "duplicated key not allowed", publishedResponse.Error.Message) + assert.Equal(t, models.CREATE_CONNECTION_METHOD, publishedResponse.ResultType) + assert.Nil(t, publishedResponse.Result) +} + +func TestHandleCreateConnectionEvent_NoMethods(t *testing.T) { + ctx := context.TODO() + svc, err := tests.CreateTestService(t) + require.NoError(t, err) + defer svc.Remove() + + pairingSecretKey := nostr.GeneratePrivateKey() + pairingPublicKey, err := nostr.GetPublicKey(pairingSecretKey) + require.NoError(t, err) + + nip47CreateConnectionJson := fmt.Sprintf(` +{ + "method": "create_connection", + "params": { + "pubkey": "%s", + "name": "Test 123" + } +} +`, pairingPublicKey) + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47CreateConnectionJson), nip47Request) + 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 + } + + NewTestNip47Controller(svc). + HandleCreateConnectionEvent(ctx, nip47Request, dbRequestEvent.ID, publishResponse) + + assert.NotNil(t, publishedResponse.Error) + assert.Equal(t, constants.ERROR_INTERNAL, publishedResponse.Error.Code) + assert.Equal(t, "No request methods provided", publishedResponse.Error.Message) + assert.Equal(t, models.CREATE_CONNECTION_METHOD, publishedResponse.ResultType) + assert.Nil(t, publishedResponse.Result) +} + +func TestHandleCreateConnectionEvent_UnsupportedMethod(t *testing.T) { + ctx := context.TODO() + svc, err := tests.CreateTestService(t) + require.NoError(t, err) + defer svc.Remove() + + pairingSecretKey := nostr.GeneratePrivateKey() + pairingPublicKey, err := nostr.GetPublicKey(pairingSecretKey) + require.NoError(t, err) + + nip47CreateConnectionJson := fmt.Sprintf(` +{ + "method": "create_connection", + "params": { + "pubkey": "%s", + "name": "Test 123", + "request_methods": ["non_existent"] + } +} +`, pairingPublicKey) + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47CreateConnectionJson), nip47Request) + 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 + } + + NewTestNip47Controller(svc). + HandleCreateConnectionEvent(ctx, nip47Request, dbRequestEvent.ID, publishResponse) + + assert.NotNil(t, publishedResponse.Error) + assert.Equal(t, constants.ERROR_INTERNAL, publishedResponse.Error.Code) + assert.Equal(t, "One or more methods are not supported by the current LNClient", publishedResponse.Error.Message) + assert.Equal(t, models.CREATE_CONNECTION_METHOD, publishedResponse.ResultType) + assert.Nil(t, publishedResponse.Result) +} + +func TestHandleCreateConnectionEvent_UnsupportedNotificationType(t *testing.T) { + ctx := context.TODO() + svc, err := tests.CreateTestService(t) + require.NoError(t, err) + defer svc.Remove() + + pairingSecretKey := nostr.GeneratePrivateKey() + pairingPublicKey, err := nostr.GetPublicKey(pairingSecretKey) + require.NoError(t, err) + + nip47CreateConnectionJson := fmt.Sprintf(` +{ + "method": "create_connection", + "params": { + "pubkey": "%s", + "name": "Test 123", + "request_methods": ["get_info"], + "notification_types": ["non_existent"] + } +} +`, pairingPublicKey) + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47CreateConnectionJson), nip47Request) + 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 + } + + NewTestNip47Controller(svc). + HandleCreateConnectionEvent(ctx, nip47Request, dbRequestEvent.ID, publishResponse) + + assert.NotNil(t, publishedResponse.Error) + assert.Equal(t, constants.ERROR_INTERNAL, publishedResponse.Error.Code) + assert.Equal(t, "One or more notification types are not supported by the current LNClient", publishedResponse.Error.Message) + assert.Equal(t, models.CREATE_CONNECTION_METHOD, publishedResponse.ResultType) + assert.Nil(t, publishedResponse.Result) +} + +func TestHandleCreateConnectionEvent_DoNotAllowCreateConnectionMethod(t *testing.T) { + ctx := context.TODO() + svc, err := tests.CreateTestService(t) + require.NoError(t, err) + defer svc.Remove() + + pairingSecretKey := nostr.GeneratePrivateKey() + pairingPublicKey, err := nostr.GetPublicKey(pairingSecretKey) + require.NoError(t, err) + + nip47CreateConnectionJson := fmt.Sprintf(` +{ + "method": "create_connection", + "params": { + "pubkey": "%s", + "name": "Test 123", + "request_methods": ["create_connection"] + } +} +`, pairingPublicKey) + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47CreateConnectionJson), nip47Request) + 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 + } + + NewTestNip47Controller(svc). + HandleCreateConnectionEvent(ctx, nip47Request, dbRequestEvent.ID, publishResponse) + + assert.NotNil(t, publishedResponse.Error) + assert.Equal(t, constants.ERROR_INTERNAL, publishedResponse.Error.Code) + assert.Equal(t, "cannot create a new app that has create_connection permission via NWC", publishedResponse.Error.Message) + assert.Equal(t, models.CREATE_CONNECTION_METHOD, publishedResponse.ResultType) + assert.Nil(t, publishedResponse.Result) +} diff --git a/nip47/controllers/get_balance_controller_test.go b/nip47/controllers/get_balance_controller_test.go index b1931024..3daae84b 100644 --- a/nip47/controllers/get_balance_controller_test.go +++ b/nip47/controllers/get_balance_controller_test.go @@ -12,9 +12,7 @@ import ( "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 nip47GetBalanceJson = ` @@ -46,9 +44,7 @@ func TestHandleGetBalanceEvent(t *testing.T) { 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). + NewTestNip47Controller(svc). HandleGetBalanceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Equal(t, int64(21000), publishedResponse.Result.(*getBalanceResponse).Balance) @@ -80,9 +76,7 @@ func TestHandleGetBalanceEvent_IsolatedApp_NoTransactions(t *testing.T) { 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). + NewTestNip47Controller(svc). HandleGetBalanceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Equal(t, int64(0), publishedResponse.Result.(*getBalanceResponse).Balance) @@ -127,9 +121,7 @@ func TestHandleGetBalanceEvent_IsolatedApp_Transactions(t *testing.T) { 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). + NewTestNip47Controller(svc). HandleGetBalanceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Equal(t, int64(1000), publishedResponse.Result.(*getBalanceResponse).Balance) diff --git a/nip47/controllers/get_budget_controller_test.go b/nip47/controllers/get_budget_controller_test.go index 93b1c842..31cb326c 100644 --- a/nip47/controllers/get_budget_controller_test.go +++ b/nip47/controllers/get_budget_controller_test.go @@ -13,9 +13,7 @@ import ( "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 = ` @@ -57,9 +55,7 @@ func TestHandleGetBudgetEvent_NoRenewal(t *testing.T) { 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). + NewTestNip47Controller(svc). HandleGetBudgetEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Equal(t, uint64(400000), publishedResponse.Result.(*getBudgetResponse).TotalBudget) @@ -103,9 +99,7 @@ func TestHandleGetBudgetEvent_NoneUsed(t *testing.T) { 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). + NewTestNip47Controller(svc). HandleGetBudgetEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Equal(t, uint64(400000), publishedResponse.Result.(*getBudgetResponse).TotalBudget) @@ -157,9 +151,7 @@ func TestHandleGetBudgetEvent_HalfUsed(t *testing.T) { 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). + NewTestNip47Controller(svc). HandleGetBudgetEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Equal(t, uint64(400000), publishedResponse.Result.(*getBudgetResponse).TotalBudget) @@ -208,9 +200,7 @@ func TestHandleGetBudgetEvent_NoBudget(t *testing.T) { 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). + NewTestNip47Controller(svc). HandleGetBudgetEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Equal(t, struct{}{}, publishedResponse.Result) @@ -240,9 +230,7 @@ func TestHandleGetBudgetEvent_NoPayInvoicePermission(t *testing.T) { 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). + NewTestNip47Controller(svc). HandleGetBudgetEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Equal(t, struct{}{}, publishedResponse.Result) diff --git a/nip47/controllers/get_info_controller.go b/nip47/controllers/get_info_controller.go index 42553bf7..0432de38 100644 --- a/nip47/controllers/get_info_controller.go +++ b/nip47/controllers/get_info_controller.go @@ -13,15 +13,16 @@ import ( ) type getInfoResponse struct { - Alias string `json:"alias"` - Color string `json:"color"` - Pubkey string `json:"pubkey"` - Network string `json:"network"` - BlockHeight uint32 `json:"block_height"` - BlockHash string `json:"block_hash"` - Methods []string `json:"methods"` - Notifications []string `json:"notifications"` - Metadata interface{} `json:"metadata,omitempty"` + Alias string `json:"alias"` + Color string `json:"color"` + Pubkey string `json:"pubkey"` + Network string `json:"network"` + BlockHeight uint32 `json:"block_height"` + BlockHash string `json:"block_hash"` + Methods []string `json:"methods"` + Notifications []string `json:"notifications"` + Metadata interface{} `json:"metadata,omitempty"` + LightningAddress string `json:"lud16"` } func (controller *nip47Controller) HandleGetInfoEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, app *db.App, publishResponse publishFunc) { @@ -90,6 +91,10 @@ func (controller *nip47Controller) HandleGetInfoEvent(ctx context.Context, nip47 if metadata["name"] == nil { metadata["name"] = app.Name } + if !app.Isolated { + lightningAddress, _ := controller.albyOAuthService.GetLightningAddress() + responsePayload.LightningAddress = lightningAddress + } responsePayload.Metadata = metadata } diff --git a/nip47/controllers/get_info_controller_test.go b/nip47/controllers/get_info_controller_test.go index c55855ab..7d5ba072 100644 --- a/nip47/controllers/get_info_controller_test.go +++ b/nip47/controllers/get_info_controller_test.go @@ -12,9 +12,7 @@ import ( "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 nip47GetInfoJson = ` @@ -40,6 +38,9 @@ func TestHandleGetInfoEvent_NoPermission(t *testing.T) { err = svc.DB.Create(&dbRequestEvent).Error assert.NoError(t, err) + // delete the existing app permissions (the app was created with get_info scope) + svc.DB.Exec("delete from app_permissions") + appPermission := &db.AppPermission{ AppId: app.ID, Scope: constants.GET_BALANCE_SCOPE, @@ -54,9 +55,7 @@ func TestHandleGetInfoEvent_NoPermission(t *testing.T) { 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). + NewTestNip47Controller(svc). HandleGetInfoEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Nil(t, publishedResponse.Error) @@ -103,9 +102,7 @@ func TestHandleGetInfoEvent_WithPermission(t *testing.T) { 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). + NewTestNip47Controller(svc). HandleGetInfoEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Nil(t, publishedResponse.Error) @@ -130,7 +127,7 @@ func TestHandleGetInfoEvent_WithMetadata(t *testing.T) { "a": 123, } - app, _, err := svc.AppsService.CreateApp("test", "", 0, "monthly", nil, nil, false, metadata) + app, _, err := svc.AppsService.CreateApp("test", "", 0, "monthly", nil, []string{constants.GET_INFO_SCOPE}, false, metadata) assert.NoError(t, err) nip47Request := &models.Request{} @@ -155,9 +152,7 @@ func TestHandleGetInfoEvent_WithMetadata(t *testing.T) { 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). + NewTestNip47Controller(svc). HandleGetInfoEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Nil(t, publishedResponse.Error) @@ -214,9 +209,7 @@ func TestHandleGetInfoEvent_WithNotifications(t *testing.T) { 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). + NewTestNip47Controller(svc). HandleGetInfoEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Nil(t, publishedResponse.Error) diff --git a/nip47/controllers/list_transactions_controller_test.go b/nip47/controllers/list_transactions_controller_test.go index 5dbd86ee..21b8414b 100644 --- a/nip47/controllers/list_transactions_controller_test.go +++ b/nip47/controllers/list_transactions_controller_test.go @@ -13,9 +13,7 @@ import ( "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" ) func TestHandleListTransactionsEvent(t *testing.T) { @@ -75,9 +73,7 @@ func TestHandleListTransactionsEvent(t *testing.T) { 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). + NewTestNip47Controller(svc). HandleListTransactionsEvent(ctx, nip47Request, dbRequestEvent.ID, *dbRequestEvent.AppId, publishResponse) assert.Nil(t, publishedResponse.Error) @@ -146,9 +142,7 @@ func TestHandleListTransactionsEvent_UnpaidOutgoingOnly(t *testing.T) { 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). + NewTestNip47Controller(svc). HandleListTransactionsEvent(ctx, nip47Request, dbRequestEvent.ID, *dbRequestEvent.AppId, publishResponse) assert.Nil(t, publishedResponse.Error) @@ -208,9 +202,7 @@ func TestHandleListTransactionsEvent_UnpaidIncomingOnly(t *testing.T) { 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). + NewTestNip47Controller(svc). HandleListTransactionsEvent(ctx, nip47Request, dbRequestEvent.ID, *dbRequestEvent.AppId, publishResponse) assert.Nil(t, publishedResponse.Error) @@ -270,9 +262,7 @@ func TestHandleListTransactionsEvent_Unpaid(t *testing.T) { 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). + NewTestNip47Controller(svc). HandleListTransactionsEvent(ctx, nip47Request, dbRequestEvent.ID, *dbRequestEvent.AppId, publishResponse) assert.Nil(t, publishedResponse.Error) @@ -338,9 +328,7 @@ func TestHandleListTransactionsEvent_Paid(t *testing.T) { 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). + NewTestNip47Controller(svc). HandleListTransactionsEvent(ctx, nip47Request, dbRequestEvent.ID, *dbRequestEvent.AppId, publishResponse) assert.Nil(t, publishedResponse.Error) diff --git a/nip47/controllers/lookup_invoice_controller_test.go b/nip47/controllers/lookup_invoice_controller_test.go index ac998016..ae187c17 100644 --- a/nip47/controllers/lookup_invoice_controller_test.go +++ b/nip47/controllers/lookup_invoice_controller_test.go @@ -12,9 +12,7 @@ import ( "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" ) var nip47LookupInvoiceJson = ` @@ -66,9 +64,7 @@ func TestHandleLookupInvoiceEvent(t *testing.T) { 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). + NewTestNip47Controller(svc). HandleLookupInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, *dbRequestEvent.AppId, publishResponse) assert.Nil(t, publishedResponse.Error) diff --git a/nip47/controllers/make_invoice_controller_test.go b/nip47/controllers/make_invoice_controller_test.go index 6fc86262..9be1bd2d 100644 --- a/nip47/controllers/make_invoice_controller_test.go +++ b/nip47/controllers/make_invoice_controller_test.go @@ -11,9 +11,7 @@ import ( "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 nip47MakeInvoiceJson = ` @@ -64,9 +62,7 @@ func TestHandleMakeInvoiceEvent(t *testing.T) { 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). + NewTestNip47Controller(svc). HandleMakeInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, *dbRequestEvent.AppId, publishResponse) expectedMetadata := map[string]interface{}{ diff --git a/nip47/controllers/multi_pay_invoice_controller_test.go b/nip47/controllers/multi_pay_invoice_controller_test.go index 26334eb8..d9b642d4 100644 --- a/nip47/controllers/multi_pay_invoice_controller_test.go +++ b/nip47/controllers/multi_pay_invoice_controller_test.go @@ -17,9 +17,7 @@ import ( "github.com/getAlby/hub/lnclient" "github.com/getAlby/hub/logger" "github.com/getAlby/hub/nip47/models" - "github.com/getAlby/hub/nip47/permissions" "github.com/getAlby/hub/tests" - "github.com/getAlby/hub/transactions" ) const nip47MultiPayJson = ` @@ -117,9 +115,7 @@ func TestHandleMultiPayInvoiceEvent_Success(t *testing.T) { err = svc.DB.Create(&dbRequestEvent).Error assert.NoError(t, err) - permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) - transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewTestNip47Controller(svc). HandleMultiPayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) var paymentHashes = []string{ @@ -185,9 +181,7 @@ func TestHandleMultiPayInvoiceEvent_OneMalformedInvoice(t *testing.T) { requestEvent := &db.RequestEvent{} svc.DB.Save(requestEvent) - permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) - transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewTestNip47Controller(svc). HandleMultiPayInvoiceEvent(ctx, nip47Request, requestEvent.ID, app, publishResponse) assert.Equal(t, 2, len(responses)) @@ -246,9 +240,7 @@ func TestHandleMultiPayInvoiceEvent_OneExpiredInvoice(t *testing.T) { requestEvent := &db.RequestEvent{} svc.DB.Save(requestEvent) - permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) - transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewTestNip47Controller(svc). HandleMultiPayInvoiceEvent(ctx, nip47Request, requestEvent.ID, app, publishResponse) assert.Equal(t, 2, len(responses)) @@ -319,9 +311,7 @@ func TestHandleMultiPayInvoiceEvent_IsolatedApp_OneBudgetExceeded(t *testing.T) err = svc.DB.Create(&dbRequestEvent).Error assert.NoError(t, err) - permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) - transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewTestNip47Controller(svc). HandleMultiPayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Equal(t, 2, len(responses)) @@ -399,9 +389,7 @@ func TestHandleMultiPayInvoiceEvent_LNClient_OnePaymentFailed(t *testing.T) { err = svc.DB.Create(&dbRequestEvent).Error assert.NoError(t, err) - permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) - transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewTestNip47Controller(svc). HandleMultiPayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Equal(t, 2, len(responses)) diff --git a/nip47/controllers/multi_pay_keysend_controller_test.go b/nip47/controllers/multi_pay_keysend_controller_test.go index 5c0a6d96..4b1f35ee 100644 --- a/nip47/controllers/multi_pay_keysend_controller_test.go +++ b/nip47/controllers/multi_pay_keysend_controller_test.go @@ -13,9 +13,7 @@ import ( "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 nip47MultiPayKeysendJson = ` @@ -106,9 +104,7 @@ func TestHandleMultiPayKeysendEvent_Success(t *testing.T) { dTags = append(dTags, tags) } - permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) - transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewTestNip47Controller(svc). HandleMultiPayKeysendEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Equal(t, 2, len(responses)) @@ -158,9 +154,7 @@ func TestHandleMultiPayKeysendEvent_OneBudgetExceeded(t *testing.T) { dTags = append(dTags, tags) } - permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) - transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewTestNip47Controller(svc). HandleMultiPayKeysendEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) // we can't guarantee which request was processed first diff --git a/nip47/controllers/nip47_controller.go b/nip47/controllers/nip47_controller.go index fd45e9a1..1fc33303 100644 --- a/nip47/controllers/nip47_controller.go +++ b/nip47/controllers/nip47_controller.go @@ -1,6 +1,8 @@ package controllers import ( + "github.com/getAlby/hub/alby" + "github.com/getAlby/hub/apps" "github.com/getAlby/hub/events" "github.com/getAlby/hub/lnclient" "github.com/getAlby/hub/nip47/permissions" @@ -14,14 +16,25 @@ type nip47Controller struct { eventPublisher events.EventPublisher permissionsService permissions.PermissionsService transactionsService transactions.TransactionsService + appsService apps.AppsService + albyOAuthService alby.AlbyOAuthService } -func NewNip47Controller(lnClient lnclient.LNClient, db *gorm.DB, eventPublisher events.EventPublisher, permissionsService permissions.PermissionsService, transactionsService transactions.TransactionsService) *nip47Controller { +func NewNip47Controller( + lnClient lnclient.LNClient, + db *gorm.DB, + eventPublisher events.EventPublisher, + permissionsService permissions.PermissionsService, + transactionsService transactions.TransactionsService, + appsService apps.AppsService, + albyOAuthService alby.AlbyOAuthService) *nip47Controller { return &nip47Controller{ lnClient: lnClient, db: db, eventPublisher: eventPublisher, permissionsService: permissionsService, transactionsService: transactionsService, + appsService: appsService, + albyOAuthService: albyOAuthService, } } diff --git a/nip47/controllers/pay_invoice_controller_test.go b/nip47/controllers/pay_invoice_controller_test.go index 4ead9348..0360f5f3 100644 --- a/nip47/controllers/pay_invoice_controller_test.go +++ b/nip47/controllers/pay_invoice_controller_test.go @@ -12,7 +12,6 @@ import ( "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" ) @@ -85,14 +84,13 @@ func TestHandlePayInvoiceEvent(t *testing.T) { 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). + NewTestNip47Controller(svc). HandlePayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse, nostr.Tags{}) assert.Equal(t, "123preimage", publishedResponse.Result.(payResponse).Preimage) transactionType := constants.TRANSACTION_TYPE_OUTGOING + transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) transaction, err := transactionsSvc.LookupTransaction(ctx, "23277d5e13fce5534f9752c62fcf9337a2a6b0ebea9d21fa816a4c9d054cf93b", &transactionType, svc.LNClient, &app.ID) assert.NoError(t, err) @@ -136,14 +134,13 @@ func TestHandlePayInvoiceEvent_0Amount(t *testing.T) { 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). + NewTestNip47Controller(svc). HandlePayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse, nostr.Tags{}) assert.Equal(t, "123preimage", publishedResponse.Result.(payResponse).Preimage) transactionType := constants.TRANSACTION_TYPE_OUTGOING + transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) transaction, err := transactionsSvc.LookupTransaction(ctx, tests.Mock0AmountPaymentHash, &transactionType, svc.LNClient, &app.ID) assert.NoError(t, err) // from the request amount @@ -181,9 +178,7 @@ func TestHandlePayInvoiceEvent_MalformedInvoice(t *testing.T) { 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). + NewTestNip47Controller(svc). HandlePayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse, nostr.Tags{}) assert.Nil(t, publishedResponse.Result) @@ -222,9 +217,7 @@ func TestHandlePayInvoiceEvent_ExpiredInvoice(t *testing.T) { 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). + NewTestNip47Controller(svc). HandlePayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse, nostr.Tags{}) assert.Nil(t, publishedResponse.Result) diff --git a/nip47/controllers/pay_keysend_controller_test.go b/nip47/controllers/pay_keysend_controller_test.go index 3b06202d..436fa2a1 100644 --- a/nip47/controllers/pay_keysend_controller_test.go +++ b/nip47/controllers/pay_keysend_controller_test.go @@ -12,9 +12,7 @@ import ( "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 nip47KeysendJson = ` @@ -78,9 +76,7 @@ func TestHandlePayKeysendEvent(t *testing.T) { 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). + NewTestNip47Controller(svc). HandlePayKeysendEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse, nostr.Tags{}) assert.Nil(t, publishedResponse.Error) @@ -119,9 +115,7 @@ func TestHandlePayKeysendEvent_WithPreimage(t *testing.T) { 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). + NewTestNip47Controller(svc). HandlePayKeysendEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse, nostr.Tags{}) assert.Nil(t, publishedResponse.Error) diff --git a/nip47/event_handler.go b/nip47/event_handler.go index b8a4e748..36e3b5ee 100644 --- a/nip47/event_handler.go +++ b/nip47/event_handler.go @@ -298,7 +298,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela } } - controller := controllers.NewNip47Controller(lnClient, svc.db, svc.eventPublisher, svc.permissionsService, svc.transactionsService) + controller := controllers.NewNip47Controller(lnClient, svc.db, svc.eventPublisher, svc.permissionsService, svc.transactionsService, svc.appsService, svc.albyOAuthSvc) switch nip47Request.Method { case models.MULTI_PAY_INVOICE_METHOD: @@ -334,6 +334,9 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela case models.SIGN_MESSAGE_METHOD: controller. HandleSignMessageEvent(ctx, nip47Request, requestEvent.ID, publishResponse) + case models.CREATE_CONNECTION_METHOD: + controller. + HandleCreateConnectionEvent(ctx, nip47Request, requestEvent.ID, publishResponse) default: publishResponse(&models.Response{ ResultType: nip47Request.Method, diff --git a/nip47/event_handler_test.go b/nip47/event_handler_test.go index 9eead0e3..8219e3fc 100644 --- a/nip47/event_handler_test.go +++ b/nip47/event_handler_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/getAlby/hub/alby" "github.com/getAlby/hub/constants" "github.com/getAlby/hub/db" "github.com/getAlby/hub/nip47/cipher" @@ -66,7 +67,8 @@ func doTestCreateResponse(t *testing.T, svc *tests.TestService, nip47Version str }, } - nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher) + albyOAuthSvc := alby.NewAlbyOAuthService(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher) + nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher, albyOAuthSvc) res, err := nip47svc.CreateResponse(reqEvent, nip47Response, nostr.Tags{}, nip47Cipher, svc.Keys.GetNostrSecretKey()) assert.NoError(t, err) @@ -104,7 +106,8 @@ func TestHandleResponse_Nip44_WithPermission(t *testing.T) { } func doTestHandleResponse_WithPermission(t *testing.T, svc *tests.TestService, createAppFn tests.CreateAppFn, version string) { - nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher) + albyOAuthSvc := alby.NewAlbyOAuthService(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher) + nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher, albyOAuthSvc) reqPrivateKey := nostr.GeneratePrivateKey() reqPubkey, err := nostr.GetPublicKey(reqPrivateKey) @@ -172,7 +175,7 @@ func doTestHandleResponse_WithPermission(t *testing.T, svc *tests.TestService, c assert.Nil(t, unmarshalledResponse.Error) assert.Equal(t, models.GET_INFO_METHOD, unmarshalledResponse.ResultType) expectedMethods := slices.Concat([]string{constants.GET_BALANCE_SCOPE}, permissions.GetAlwaysGrantedMethods()) - assert.Equal(t, expectedMethods, unmarshalledResponse.Result.Methods) + assert.ElementsMatch(t, expectedMethods, unmarshalledResponse.Result.Methods) } func TestHandleResponse_Nip04_DuplicateRequest(t *testing.T) { @@ -192,7 +195,8 @@ func TestHandleResponse_Nip44_DuplicateRequest(t *testing.T) { } func doTestHandleResponse_DuplicateRequest(t *testing.T, svc *tests.TestService, createAppFn tests.CreateAppFn, version string) { - nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher) + albyOAuthSvc := alby.NewAlbyOAuthService(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher) + nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher, albyOAuthSvc) reqPrivateKey := nostr.GeneratePrivateKey() reqPubkey, err := nostr.GetPublicKey(reqPrivateKey) @@ -266,7 +270,8 @@ func TestHandleResponse_Nip44_NoPermission(t *testing.T) { } func doTestHandleResponse_NoPermission(t *testing.T, svc *tests.TestService, createAppFn tests.CreateAppFn, version string) { - nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher) + albyOAuthSvc := alby.NewAlbyOAuthService(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher) + nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher, albyOAuthSvc) reqPrivateKey := nostr.GeneratePrivateKey() reqPubkey, err := nostr.GetPublicKey(reqPrivateKey) @@ -337,7 +342,8 @@ func TestHandleResponse_Nip44_OldRequestForPayment(t *testing.T) { } func doTestHandleResponse_OldRequestForPayment(t *testing.T, svc *tests.TestService, createAppFn tests.CreateAppFn, version string) { - nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher) + albyOAuthSvc := alby.NewAlbyOAuthService(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher) + nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher, albyOAuthSvc) reqPrivateKey := nostr.GeneratePrivateKey() reqPubkey, err := nostr.GetPublicKey(reqPrivateKey) @@ -412,7 +418,8 @@ func TestHandleResponse_Nip44_IncorrectPubkey(t *testing.T) { } func doTestHandleResponse_IncorrectPubkey(t *testing.T, svc *tests.TestService, createAppFn tests.CreateAppFn, version string) { - nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher) + albyOAuthSvc := alby.NewAlbyOAuthService(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher) + nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher, albyOAuthSvc) reqPrivateKey := nostr.GeneratePrivateKey() reqPubkey, err := nostr.GetPublicKey(reqPrivateKey) @@ -469,7 +476,8 @@ func TestHandleResponse_NoApp(t *testing.T) { svc, err := tests.CreateTestService(t) require.NoError(t, err) defer svc.Remove() - nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher) + albyOAuthSvc := alby.NewAlbyOAuthService(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher) + nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher, albyOAuthSvc) reqPrivateKey := nostr.GeneratePrivateKey() reqPubkey, err := nostr.GetPublicKey(reqPrivateKey) @@ -522,7 +530,8 @@ func TestHandleResponse_IncorrectVersions(t *testing.T) { } func doTestHandleResponse_IncorrectVersion(t *testing.T, svc *tests.TestService, appVersion, requestVersion string) { - nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher) + albyOAuthSvc := alby.NewAlbyOAuthService(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher) + nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher, albyOAuthSvc) reqPrivateKey := nostr.GeneratePrivateKey() reqPubkey, err := nostr.GetPublicKey(reqPrivateKey) diff --git a/nip47/models/models.go b/nip47/models/models.go index 6c7be0b0..5549a5a8 100644 --- a/nip47/models/models.go +++ b/nip47/models/models.go @@ -23,6 +23,7 @@ const ( MULTI_PAY_INVOICE_METHOD = "multi_pay_invoice" MULTI_PAY_KEYSEND_METHOD = "multi_pay_keysend" SIGN_MESSAGE_METHOD = "sign_message" + CREATE_CONNECTION_METHOD = "create_connection" ) type Transaction struct { diff --git a/nip47/nip47_service.go b/nip47/nip47_service.go index 21710b22..6169b54e 100644 --- a/nip47/nip47_service.go +++ b/nip47/nip47_service.go @@ -3,6 +3,8 @@ package nip47 import ( "context" + "github.com/getAlby/hub/alby" + "github.com/getAlby/hub/apps" "github.com/getAlby/hub/config" "github.com/getAlby/hub/events" "github.com/getAlby/hub/lnclient" @@ -19,6 +21,8 @@ import ( type nip47Service struct { permissionsService permissions.PermissionsService transactionsService transactions.TransactionsService + appsService apps.AppsService + albyOAuthSvc alby.AlbyOAuthService nip47NotificationQueue notifications.Nip47NotificationQueue cfg config.Config keys keys.Keys @@ -36,15 +40,17 @@ type Nip47Service interface { CreateResponse(initialEvent *nostr.Event, content interface{}, tags nostr.Tags, cipher *cipher.Nip47Cipher, walletPrivKey string) (result *nostr.Event, err error) } -func NewNip47Service(db *gorm.DB, cfg config.Config, keys keys.Keys, eventPublisher events.EventPublisher) *nip47Service { +func NewNip47Service(db *gorm.DB, cfg config.Config, keys keys.Keys, eventPublisher events.EventPublisher, albyOAuthSvc alby.AlbyOAuthService) *nip47Service { return &nip47Service{ nip47NotificationQueue: notifications.NewNip47NotificationQueue(), cfg: cfg, db: db, permissionsService: permissions.NewPermissionsService(db, eventPublisher), transactionsService: transactions.NewTransactionsService(db, eventPublisher), + appsService: apps.NewAppsService(db, eventPublisher, keys, cfg), eventPublisher: eventPublisher, keys: keys, + albyOAuthSvc: albyOAuthSvc, } } diff --git a/nip47/permissions/permissions.go b/nip47/permissions/permissions.go index 02be5454..dc9ceb6b 100644 --- a/nip47/permissions/permissions.go +++ b/nip47/permissions/permissions.go @@ -79,6 +79,11 @@ func (svc *permissionsService) GetPermittedMethods(app *db.App, lnClient lnclien // only return methods supported by the lnClient lnClientSupportedMethods := lnClient.GetSupportedNIP47Methods() requestMethods = utils.Filter(requestMethods, func(requestMethod string) bool { + // TODO: better way to exclude methods unrelated to the lnclient + if requestMethod == models.CREATE_CONNECTION_METHOD { + return true + } + return slices.Contains(lnClientSupportedMethods, requestMethod) }) @@ -121,6 +126,8 @@ func scopeToRequestMethods(scope string) []string { return []string{models.LIST_TRANSACTIONS_METHOD} case constants.SIGN_MESSAGE_SCOPE: return []string{models.SIGN_MESSAGE_METHOD} + case constants.SUPERUSER_SCOPE: + return []string{models.CREATE_CONNECTION_METHOD} } return []string{} } @@ -158,6 +165,8 @@ func RequestMethodToScope(requestMethod string) (string, error) { return constants.LIST_TRANSACTIONS_SCOPE, nil case models.SIGN_MESSAGE_METHOD: return constants.SIGN_MESSAGE_SCOPE, nil + case models.CREATE_CONNECTION_METHOD: + return constants.SUPERUSER_SCOPE, nil } logger.Logger.WithField("request_method", requestMethod).Error("Unsupported request method") return "", fmt.Errorf("unsupported request method: %s", requestMethod) @@ -173,6 +182,7 @@ func AllScopes() []string { constants.LIST_TRANSACTIONS_SCOPE, constants.SIGN_MESSAGE_SCOPE, constants.NOTIFICATIONS_SCOPE, + constants.SUPERUSER_SCOPE, } } diff --git a/nip47/permissions/permissions_test.go b/nip47/permissions/permissions_test.go index 1b82593e..50e6e9e0 100644 --- a/nip47/permissions/permissions_test.go +++ b/nip47/permissions/permissions_test.go @@ -56,35 +56,6 @@ func TestHasPermission_Expired(t *testing.T) { assert.Equal(t, "This app has expired", message) } -// TODO: move to transactions service -/*func TestHasPermission_Exceeded(t *testing.T) { - defer tests.RemoveTestService() - svc, err := tests.CreateTestService(t) - require.NoError(t, err) - - app, _, err := tests.CreateApp(svc) - assert.NoError(t, err) - - budgetRenewal := "never" - expiresAt := time.Now().Add(24 * time.Hour) - appPermission := &db.AppPermission{ - AppId: app.ID, - App: *app, - Scope: constants.PAY_INVOICE_SCOPE, - MaxAmountSat: 10, - BudgetRenewal: budgetRenewal, - ExpiresAt: &expiresAt, - } - err = svc.DB.Create(appPermission).Error - assert.NoError(t, err) - - permissionsSvc := NewPermissionsService(svc.DB, svc.EventPublisher) - result, code, message := permissionsSvc.HasPermission(app, PAY_INVOICE_SCOPE, 100*1000) - assert.False(t, result) - assert.Equal(t, constants.ERROR_QUOTA_EXCEEDED, code) - assert.Equal(t, "Insufficient budget remaining to make payment", message) -}*/ - func TestHasPermission_OK(t *testing.T) { svc, err := tests.CreateTestService(t) require.NoError(t, err) @@ -145,6 +116,16 @@ func TestRequestMethodsToScopes_GetInfo(t *testing.T) { assert.Equal(t, []string{constants.GET_INFO_SCOPE}, scopes) } +func TestRequestMethodToScope_CreateConnection(t *testing.T) { + scope, err := RequestMethodToScope(models.CREATE_CONNECTION_METHOD) + assert.NoError(t, err) + assert.Equal(t, constants.SUPERUSER_SCOPE, scope) +} +func TestScopeToRequestMethods_Superuser(t *testing.T) { + methods := scopeToRequestMethods(constants.SUPERUSER_SCOPE) + assert.Equal(t, []string{models.CREATE_CONNECTION_METHOD}, methods) +} + func TestGetPermittedMethods_AlwaysGranted(t *testing.T) { svc, err := tests.CreateTestService(t) require.NoError(t, err) diff --git a/nip47/publish_nip47_info.go b/nip47/publish_nip47_info.go index 4477a90c..4be77580 100644 --- a/nip47/publish_nip47_info.go +++ b/nip47/publish_nip47_info.go @@ -56,6 +56,9 @@ func (svc *nip47Service) PublishNip47Info(ctx context.Context, relay nostrmodels } capabilities = svc.permissionsService.GetPermittedMethods(&app, lnClient) permitsNotifications = svc.permissionsService.PermitsNotifications(&app) + + // NWA: associate the info event with the app so that the app can receive the wallet pubkey + tags = append(tags, []string{"p", app.AppPubkey}) } if permitsNotifications && len(lnClient.GetSupportedNIP47NotificationTypes()) > 0 { capabilities = append(capabilities, "notifications") diff --git a/service/create_app_consumer.go b/service/create_app_consumer.go index 3b3af50b..19b6eb42 100644 --- a/service/create_app_consumer.go +++ b/service/create_app_consumer.go @@ -7,6 +7,7 @@ import ( "github.com/nbd-wtf/go-nostr" "github.com/sirupsen/logrus" + "github.com/getAlby/hub/db" "github.com/getAlby/hub/events" "github.com/getAlby/hub/logger" ) @@ -33,6 +34,18 @@ func (s *createAppConsumer) ConsumeEvent(ctx context.Context, event *events.Even logger.Logger.WithField("event", event).Error("Failed to get app id") return } + + app := db.App{} + err := s.svc.db.First(&app, &db.App{ + ID: id, + }).Error + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "id": id, + }).WithError(err).Error("Failed to find app for id") + return + } + walletPrivKey, err := s.svc.keys.GetAppWalletKey(id) if err != nil { logger.Logger.WithError(err).Error("Failed to calculate app wallet priv key") diff --git a/service/service.go b/service/service.go index 0c57e147..cf2089b6 100644 --- a/service/service.go +++ b/service/service.go @@ -113,14 +113,16 @@ func NewService(ctx context.Context) (*service, error) { keys := keys.NewKeys() + albyOAuthSvc := alby.NewAlbyOAuthService(gormDB, cfg, keys, eventPublisher) + var wg sync.WaitGroup svc := &service{ cfg: cfg, ctx: ctx, wg: &wg, eventPublisher: eventPublisher, - albyOAuthSvc: alby.NewAlbyOAuthService(gormDB, cfg, keys, eventPublisher), - nip47Service: nip47.NewNip47Service(gormDB, cfg, keys, eventPublisher), + albyOAuthSvc: albyOAuthSvc, + nip47Service: nip47.NewNip47Service(gormDB, cfg, keys, eventPublisher, albyOAuthSvc), transactionsService: transactions.NewTransactionsService(gormDB, eventPublisher), db: gormDB, keys: keys, diff --git a/tests/create_app.go b/tests/create_app.go index 09ba929c..161ac756 100644 --- a/tests/create_app.go +++ b/tests/create_app.go @@ -3,6 +3,7 @@ package tests import ( "time" + "github.com/getAlby/hub/constants" db "github.com/getAlby/hub/db" "github.com/getAlby/hub/events" "github.com/getAlby/hub/nip47/cipher" @@ -27,7 +28,7 @@ func CreateAppWithPrivateKey(svc *TestService, senderPrivkey, nip47Version strin } var expiresAt *time.Time - app, pairingSecretKey, err := svc.AppsService.CreateApp("test", senderPubkey, 0, "monthly", expiresAt, nil, false, nil) + app, pairingSecretKey, err := svc.AppsService.CreateApp("test", senderPubkey, 0, "monthly", expiresAt, []string{constants.GET_INFO_SCOPE}, false, nil) if pairingSecretKey == "" { pairingSecretKey = senderPrivkey } diff --git a/tests/mocks/Config.go b/tests/mocks/Config.go new file mode 100644 index 00000000..fe75854a --- /dev/null +++ b/tests/mocks/Config.go @@ -0,0 +1,646 @@ +// Code generated by mockery v2.52.1. DO NOT EDIT. + +package mocks + +import ( + config "github.com/getAlby/hub/config" + mock "github.com/stretchr/testify/mock" +) + +// MockConfig is an autogenerated mock type for the Config type +type MockConfig struct { + mock.Mock +} + +type MockConfig_Expecter struct { + mock *mock.Mock +} + +func (_m *MockConfig) EXPECT() *MockConfig_Expecter { + return &MockConfig_Expecter{mock: &_m.Mock} +} + +// ChangeUnlockPassword provides a mock function with given fields: currentUnlockPassword, newUnlockPassword +func (_m *MockConfig) ChangeUnlockPassword(currentUnlockPassword string, newUnlockPassword string) error { + ret := _m.Called(currentUnlockPassword, newUnlockPassword) + + if len(ret) == 0 { + panic("no return value specified for ChangeUnlockPassword") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string, string) error); ok { + r0 = rf(currentUnlockPassword, newUnlockPassword) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockConfig_ChangeUnlockPassword_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ChangeUnlockPassword' +type MockConfig_ChangeUnlockPassword_Call struct { + *mock.Call +} + +// ChangeUnlockPassword is a helper method to define mock.On call +// - currentUnlockPassword string +// - newUnlockPassword string +func (_e *MockConfig_Expecter) ChangeUnlockPassword(currentUnlockPassword interface{}, newUnlockPassword interface{}) *MockConfig_ChangeUnlockPassword_Call { + return &MockConfig_ChangeUnlockPassword_Call{Call: _e.mock.On("ChangeUnlockPassword", currentUnlockPassword, newUnlockPassword)} +} + +func (_c *MockConfig_ChangeUnlockPassword_Call) Run(run func(currentUnlockPassword string, newUnlockPassword string)) *MockConfig_ChangeUnlockPassword_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(string)) + }) + return _c +} + +func (_c *MockConfig_ChangeUnlockPassword_Call) Return(_a0 error) *MockConfig_ChangeUnlockPassword_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockConfig_ChangeUnlockPassword_Call) RunAndReturn(run func(string, string) error) *MockConfig_ChangeUnlockPassword_Call { + _c.Call.Return(run) + return _c +} + +// CheckUnlockPassword provides a mock function with given fields: password +func (_m *MockConfig) CheckUnlockPassword(password string) bool { + ret := _m.Called(password) + + if len(ret) == 0 { + panic("no return value specified for CheckUnlockPassword") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(string) bool); ok { + r0 = rf(password) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// MockConfig_CheckUnlockPassword_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CheckUnlockPassword' +type MockConfig_CheckUnlockPassword_Call struct { + *mock.Call +} + +// CheckUnlockPassword is a helper method to define mock.On call +// - password string +func (_e *MockConfig_Expecter) CheckUnlockPassword(password interface{}) *MockConfig_CheckUnlockPassword_Call { + return &MockConfig_CheckUnlockPassword_Call{Call: _e.mock.On("CheckUnlockPassword", password)} +} + +func (_c *MockConfig_CheckUnlockPassword_Call) Run(run func(password string)) *MockConfig_CheckUnlockPassword_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *MockConfig_CheckUnlockPassword_Call) Return(_a0 bool) *MockConfig_CheckUnlockPassword_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockConfig_CheckUnlockPassword_Call) RunAndReturn(run func(string) bool) *MockConfig_CheckUnlockPassword_Call { + _c.Call.Return(run) + return _c +} + +// Get provides a mock function with given fields: key, encryptionKey +func (_m *MockConfig) Get(key string, encryptionKey string) (string, error) { + ret := _m.Called(key, encryptionKey) + + if len(ret) == 0 { + panic("no return value specified for Get") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(string, string) (string, error)); ok { + return rf(key, encryptionKey) + } + if rf, ok := ret.Get(0).(func(string, string) string); ok { + r0 = rf(key, encryptionKey) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(string, string) error); ok { + r1 = rf(key, encryptionKey) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockConfig_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' +type MockConfig_Get_Call struct { + *mock.Call +} + +// Get is a helper method to define mock.On call +// - key string +// - encryptionKey string +func (_e *MockConfig_Expecter) Get(key interface{}, encryptionKey interface{}) *MockConfig_Get_Call { + return &MockConfig_Get_Call{Call: _e.mock.On("Get", key, encryptionKey)} +} + +func (_c *MockConfig_Get_Call) Run(run func(key string, encryptionKey string)) *MockConfig_Get_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(string)) + }) + return _c +} + +func (_c *MockConfig_Get_Call) Return(_a0 string, _a1 error) *MockConfig_Get_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockConfig_Get_Call) RunAndReturn(run func(string, string) (string, error)) *MockConfig_Get_Call { + _c.Call.Return(run) + return _c +} + +// GetCurrency provides a mock function with no fields +func (_m *MockConfig) GetCurrency() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetCurrency") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// MockConfig_GetCurrency_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetCurrency' +type MockConfig_GetCurrency_Call struct { + *mock.Call +} + +// GetCurrency is a helper method to define mock.On call +func (_e *MockConfig_Expecter) GetCurrency() *MockConfig_GetCurrency_Call { + return &MockConfig_GetCurrency_Call{Call: _e.mock.On("GetCurrency")} +} + +func (_c *MockConfig_GetCurrency_Call) Run(run func()) *MockConfig_GetCurrency_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockConfig_GetCurrency_Call) Return(_a0 string) *MockConfig_GetCurrency_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockConfig_GetCurrency_Call) RunAndReturn(run func() string) *MockConfig_GetCurrency_Call { + _c.Call.Return(run) + return _c +} + +// GetEnv provides a mock function with no fields +func (_m *MockConfig) GetEnv() *config.AppConfig { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetEnv") + } + + var r0 *config.AppConfig + if rf, ok := ret.Get(0).(func() *config.AppConfig); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*config.AppConfig) + } + } + + return r0 +} + +// MockConfig_GetEnv_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetEnv' +type MockConfig_GetEnv_Call struct { + *mock.Call +} + +// GetEnv is a helper method to define mock.On call +func (_e *MockConfig_Expecter) GetEnv() *MockConfig_GetEnv_Call { + return &MockConfig_GetEnv_Call{Call: _e.mock.On("GetEnv")} +} + +func (_c *MockConfig_GetEnv_Call) Run(run func()) *MockConfig_GetEnv_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockConfig_GetEnv_Call) Return(_a0 *config.AppConfig) *MockConfig_GetEnv_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockConfig_GetEnv_Call) RunAndReturn(run func() *config.AppConfig) *MockConfig_GetEnv_Call { + _c.Call.Return(run) + return _c +} + +// GetJWTSecret provides a mock function with no fields +func (_m *MockConfig) GetJWTSecret() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetJWTSecret") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// MockConfig_GetJWTSecret_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetJWTSecret' +type MockConfig_GetJWTSecret_Call struct { + *mock.Call +} + +// GetJWTSecret is a helper method to define mock.On call +func (_e *MockConfig_Expecter) GetJWTSecret() *MockConfig_GetJWTSecret_Call { + return &MockConfig_GetJWTSecret_Call{Call: _e.mock.On("GetJWTSecret")} +} + +func (_c *MockConfig_GetJWTSecret_Call) Run(run func()) *MockConfig_GetJWTSecret_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockConfig_GetJWTSecret_Call) Return(_a0 string) *MockConfig_GetJWTSecret_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockConfig_GetJWTSecret_Call) RunAndReturn(run func() string) *MockConfig_GetJWTSecret_Call { + _c.Call.Return(run) + return _c +} + +// GetRelayUrl provides a mock function with no fields +func (_m *MockConfig) GetRelayUrl() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetRelayUrl") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// MockConfig_GetRelayUrl_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRelayUrl' +type MockConfig_GetRelayUrl_Call struct { + *mock.Call +} + +// GetRelayUrl is a helper method to define mock.On call +func (_e *MockConfig_Expecter) GetRelayUrl() *MockConfig_GetRelayUrl_Call { + return &MockConfig_GetRelayUrl_Call{Call: _e.mock.On("GetRelayUrl")} +} + +func (_c *MockConfig_GetRelayUrl_Call) Run(run func()) *MockConfig_GetRelayUrl_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockConfig_GetRelayUrl_Call) Return(_a0 string) *MockConfig_GetRelayUrl_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockConfig_GetRelayUrl_Call) RunAndReturn(run func() string) *MockConfig_GetRelayUrl_Call { + _c.Call.Return(run) + return _c +} + +// SaveUnlockPasswordCheck provides a mock function with given fields: encryptionKey +func (_m *MockConfig) SaveUnlockPasswordCheck(encryptionKey string) error { + ret := _m.Called(encryptionKey) + + if len(ret) == 0 { + panic("no return value specified for SaveUnlockPasswordCheck") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(encryptionKey) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockConfig_SaveUnlockPasswordCheck_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SaveUnlockPasswordCheck' +type MockConfig_SaveUnlockPasswordCheck_Call struct { + *mock.Call +} + +// SaveUnlockPasswordCheck is a helper method to define mock.On call +// - encryptionKey string +func (_e *MockConfig_Expecter) SaveUnlockPasswordCheck(encryptionKey interface{}) *MockConfig_SaveUnlockPasswordCheck_Call { + return &MockConfig_SaveUnlockPasswordCheck_Call{Call: _e.mock.On("SaveUnlockPasswordCheck", encryptionKey)} +} + +func (_c *MockConfig_SaveUnlockPasswordCheck_Call) Run(run func(encryptionKey string)) *MockConfig_SaveUnlockPasswordCheck_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *MockConfig_SaveUnlockPasswordCheck_Call) Return(_a0 error) *MockConfig_SaveUnlockPasswordCheck_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockConfig_SaveUnlockPasswordCheck_Call) RunAndReturn(run func(string) error) *MockConfig_SaveUnlockPasswordCheck_Call { + _c.Call.Return(run) + return _c +} + +// SetAutoUnlockPassword provides a mock function with given fields: unlockPassword +func (_m *MockConfig) SetAutoUnlockPassword(unlockPassword string) error { + ret := _m.Called(unlockPassword) + + if len(ret) == 0 { + panic("no return value specified for SetAutoUnlockPassword") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(unlockPassword) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockConfig_SetAutoUnlockPassword_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetAutoUnlockPassword' +type MockConfig_SetAutoUnlockPassword_Call struct { + *mock.Call +} + +// SetAutoUnlockPassword is a helper method to define mock.On call +// - unlockPassword string +func (_e *MockConfig_Expecter) SetAutoUnlockPassword(unlockPassword interface{}) *MockConfig_SetAutoUnlockPassword_Call { + return &MockConfig_SetAutoUnlockPassword_Call{Call: _e.mock.On("SetAutoUnlockPassword", unlockPassword)} +} + +func (_c *MockConfig_SetAutoUnlockPassword_Call) Run(run func(unlockPassword string)) *MockConfig_SetAutoUnlockPassword_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *MockConfig_SetAutoUnlockPassword_Call) Return(_a0 error) *MockConfig_SetAutoUnlockPassword_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockConfig_SetAutoUnlockPassword_Call) RunAndReturn(run func(string) error) *MockConfig_SetAutoUnlockPassword_Call { + _c.Call.Return(run) + return _c +} + +// SetCurrency provides a mock function with given fields: value +func (_m *MockConfig) SetCurrency(value string) error { + ret := _m.Called(value) + + if len(ret) == 0 { + panic("no return value specified for SetCurrency") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(value) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockConfig_SetCurrency_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetCurrency' +type MockConfig_SetCurrency_Call struct { + *mock.Call +} + +// SetCurrency is a helper method to define mock.On call +// - value string +func (_e *MockConfig_Expecter) SetCurrency(value interface{}) *MockConfig_SetCurrency_Call { + return &MockConfig_SetCurrency_Call{Call: _e.mock.On("SetCurrency", value)} +} + +func (_c *MockConfig_SetCurrency_Call) Run(run func(value string)) *MockConfig_SetCurrency_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *MockConfig_SetCurrency_Call) Return(_a0 error) *MockConfig_SetCurrency_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockConfig_SetCurrency_Call) RunAndReturn(run func(string) error) *MockConfig_SetCurrency_Call { + _c.Call.Return(run) + return _c +} + +// SetIgnore provides a mock function with given fields: key, value, encryptionKey +func (_m *MockConfig) SetIgnore(key string, value string, encryptionKey string) error { + ret := _m.Called(key, value, encryptionKey) + + if len(ret) == 0 { + panic("no return value specified for SetIgnore") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string, string, string) error); ok { + r0 = rf(key, value, encryptionKey) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockConfig_SetIgnore_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetIgnore' +type MockConfig_SetIgnore_Call struct { + *mock.Call +} + +// SetIgnore is a helper method to define mock.On call +// - key string +// - value string +// - encryptionKey string +func (_e *MockConfig_Expecter) SetIgnore(key interface{}, value interface{}, encryptionKey interface{}) *MockConfig_SetIgnore_Call { + return &MockConfig_SetIgnore_Call{Call: _e.mock.On("SetIgnore", key, value, encryptionKey)} +} + +func (_c *MockConfig_SetIgnore_Call) Run(run func(key string, value string, encryptionKey string)) *MockConfig_SetIgnore_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(string), args[2].(string)) + }) + return _c +} + +func (_c *MockConfig_SetIgnore_Call) Return(_a0 error) *MockConfig_SetIgnore_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockConfig_SetIgnore_Call) RunAndReturn(run func(string, string, string) error) *MockConfig_SetIgnore_Call { + _c.Call.Return(run) + return _c +} + +// SetUpdate provides a mock function with given fields: key, value, encryptionKey +func (_m *MockConfig) SetUpdate(key string, value string, encryptionKey string) error { + ret := _m.Called(key, value, encryptionKey) + + if len(ret) == 0 { + panic("no return value specified for SetUpdate") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string, string, string) error); ok { + r0 = rf(key, value, encryptionKey) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockConfig_SetUpdate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetUpdate' +type MockConfig_SetUpdate_Call struct { + *mock.Call +} + +// SetUpdate is a helper method to define mock.On call +// - key string +// - value string +// - encryptionKey string +func (_e *MockConfig_Expecter) SetUpdate(key interface{}, value interface{}, encryptionKey interface{}) *MockConfig_SetUpdate_Call { + return &MockConfig_SetUpdate_Call{Call: _e.mock.On("SetUpdate", key, value, encryptionKey)} +} + +func (_c *MockConfig_SetUpdate_Call) Run(run func(key string, value string, encryptionKey string)) *MockConfig_SetUpdate_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(string), args[2].(string)) + }) + return _c +} + +func (_c *MockConfig_SetUpdate_Call) Return(_a0 error) *MockConfig_SetUpdate_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockConfig_SetUpdate_Call) RunAndReturn(run func(string, string, string) error) *MockConfig_SetUpdate_Call { + _c.Call.Return(run) + return _c +} + +// SetupCompleted provides a mock function with no fields +func (_m *MockConfig) SetupCompleted() bool { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for SetupCompleted") + } + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// MockConfig_SetupCompleted_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetupCompleted' +type MockConfig_SetupCompleted_Call struct { + *mock.Call +} + +// SetupCompleted is a helper method to define mock.On call +func (_e *MockConfig_Expecter) SetupCompleted() *MockConfig_SetupCompleted_Call { + return &MockConfig_SetupCompleted_Call{Call: _e.mock.On("SetupCompleted")} +} + +func (_c *MockConfig_SetupCompleted_Call) Run(run func()) *MockConfig_SetupCompleted_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockConfig_SetupCompleted_Call) Return(_a0 bool) *MockConfig_SetupCompleted_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockConfig_SetupCompleted_Call) RunAndReturn(run func() bool) *MockConfig_SetupCompleted_Call { + _c.Call.Return(run) + return _c +} + +// NewMockConfig creates a new instance of MockConfig. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockConfig(t interface { + mock.TestingT + Cleanup(func()) +}) *MockConfig { + mock := &MockConfig{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/tests/test_service.go b/tests/test_service.go index bf5a7ec7..c4d06689 100644 --- a/tests/test_service.go +++ b/tests/test_service.go @@ -60,7 +60,7 @@ func CreateTestServiceWithMnemonic(t *testing.T, mnemonic string, unlockPassword eventPublisher := events.NewEventPublisher() - appsService := apps.NewAppsService(gormDb, eventPublisher, keys) + appsService := apps.NewAppsService(gormDb, eventPublisher, keys, cfg) return &TestService{ Cfg: cfg, diff --git a/wails/wails_app.go b/wails/wails_app.go index 298de22b..0c4ef707 100644 --- a/wails/wails_app.go +++ b/wails/wails_app.go @@ -30,7 +30,7 @@ func NewApp(svc service.Service) *WailsApp { svc: svc, api: api.NewAPI(svc, svc.GetDB(), svc.GetConfig(), svc.GetKeys(), svc.GetAlbyOAuthSvc(), svc.GetEventPublisher()), db: svc.GetDB(), - appsSvc: apps.NewAppsService(svc.GetDB(), svc.GetEventPublisher(), svc.GetKeys()), + appsSvc: apps.NewAppsService(svc.GetDB(), svc.GetEventPublisher(), svc.GetKeys(), svc.GetConfig()), } }