diff --git a/api/api.go b/api/api.go index 8e2ae829..354c2b5e 100644 --- a/api/api.go +++ b/api/api.go @@ -67,6 +67,13 @@ func (api *api) CreateApp(createAppRequest *CreateAppRequest) (*CreateAppRespons "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( + "cannot create app with superuser permission without specifying unlock password") + } + } + expiresAt, err := api.parseExpiresAt(createAppRequest.ExpiresAt) if err != nil { return nil, fmt.Errorf("invalid expiresAt: %v", err) @@ -207,12 +214,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, @@ -221,12 +233,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/models.go b/api/models.go index 16d074d9..9aac7b7c 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..3c93aeb4 100644 --- a/apps/apps_service.go +++ b/apps/apps_service.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "slices" + "strings" "time" "github.com/getAlby/hub/constants" @@ -44,6 +45,14 @@ func (svc *appsService) CreateApp(name string, pubkey string, maxAmountSat uint6 return nil, "", errors.New("Sub-wallet app connection cannot have sign_message scope") } + 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(), ",")) + } + var pairingPublicKey string var pairingSecretKey string if pubkey == "" { 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..b297807f 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"; @@ -132,6 +132,20 @@ const Permissions: React.FC = ({ )} + {permissions.scopes.includes("superuser") && ( + <> +
+ +

Superuser Access

+
+ +

+ This app can create other app connections. Please make sure you + trust this app. +

+ + )} + {!permissions.isolated && permissions.scopes.includes("pay_invoice") && ( <> {!readOnly && !budgetReadOnly ? ( diff --git a/frontend/src/components/Scopes.tsx b/frontend/src/components/Scopes.tsx index 99f83211..b0f40bf5 100644 --- a/frontend/src/components/Scopes.tsx +++ b/frontend/src/components/Scopes.tsx @@ -52,7 +52,7 @@ const Scopes: React.FC = ({ onScopesChanged, }) => { const fullAccessScopes: Scope[] = React.useMemo(() => { - return [...capabilities.scopes]; + return capabilities.scopes.filter((scope) => scope !== "superuser"); }, [capabilities.scopes]); const readOnlyScopes: Scope[] = React.useMemo(() => { @@ -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 === fullAccessScopes.length && + scopes.every((scope) => fullAccessScopes.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/components/connections/AppCardConnectionInfo.tsx b/frontend/src/components/connections/AppCardConnectionInfo.tsx index 61fff3aa..82259a5c 100644 --- a/frontend/src/components/connections/AppCardConnectionInfo.tsx +++ b/frontend/src/components/connections/AppCardConnectionInfo.tsx @@ -1,5 +1,5 @@ import dayjs from "dayjs"; -import { BrickWall, CircleCheck, PlusCircle } from "lucide-react"; +import { BrickWall, CircleCheck, Crown, PlusCircle } from "lucide-react"; import { Link } from "react-router-dom"; import { Button } from "src/components/ui/button"; import { Progress } from "src/components/ui/progress"; @@ -34,7 +34,34 @@ export function AppCardConnectionInfo({ return ( <> - {connection.isolated ? ( + {connection.scopes.indexOf("superuser") > -1 ? ( + <> +
+
+ + Superuser +
+
+
+
+

+ You've spent +

+

+ {new Intl.NumberFormat().format(connection.budgetUsage)} sats +

+
+
+
+
+ Last used:{" "} + {connection.lastEventAt + ? dayjs(connection.lastEventAt).fromNow() + : "Never"} +
+
+ + ) : connection.isolated ? ( <>
diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index acd647ec..70120d18 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -35,6 +35,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"; @@ -240,6 +241,10 @@ const routes = [ path: "uncle-jim", element: , }, + { + path: "alby-go", + element: , + }, { path: "buzzpay", element: , diff --git a/frontend/src/screens/internal-apps/AlbyGo.tsx b/frontend/src/screens/internal-apps/AlbyGo.tsx new file mode 100644 index 00000000..8405823b --- /dev/null +++ b/frontend/src/screens/internal-apps/AlbyGo.tsx @@ -0,0 +1,334 @@ +import { Globe, InfoIcon } from "lucide-react"; +import React from "react"; +import { useNavigate } 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 { 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 { Checkbox } from "src/components/ui/checkbox"; +import { Input } from "src/components/ui/input"; +import { Label } from "src/components/ui/label"; +import { LoadingButton } from "src/components/ui/loading-button"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "src/components/ui/tooltip"; +import { useToast } from "src/components/ui/use-toast"; +import { useApp } from "src/hooks/useApp"; +import { createApp } from "src/requests/createApp"; +import { ConnectAppCard } from "src/screens/apps/AppCreated"; + +export function AlbyGo() { + const [isSuperuser, setSuperuser] = React.useState(true); + 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 app = suggestedApps.find((app) => app.id === "alby-go"); + const { data: createdApp } = useApp(appPubkey, true); + const navigate = useNavigate(); + const { toast } = useToast(); + if (!app) { + return null; + } + + function onClickCreateConnection() { + if (!isSuperuser) { + navigate("/apps/new?app=alby-go"); + return; + } + setShowCreateConnectionDialog(true); + } + async function onSubmitCreateConnection(e: React.FormEvent) { + e.preventDefault(); + setLoading(true); + try { + const createAppResponse = await createApp({ + name: "Alby Go", + scopes: [ + "pay_invoice", + "get_balance", + "get_info", + "make_invoice", + "lookup_invoice", + "list_transactions", + "sign_message", + "notifications", + "superuser", + ], + isolated: false, + metadata: { + app_store_app_id: "alby-go", + }, + unlockPassword, + }); + 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); + } + + return ( +
+ {showCreateConnectionDialog && ( + + +
+ + Confirm New Connection + +
+

+ Alby Go will be given permission to create other app + connections which can spend your balance. Please enter + your unlock password to continue. +

+
+ + setUnlockPassword(e.target.value)} + value={unlockPassword} + /> +
+
+
+
+ + setShowCreateConnectionDialog(false)} + > + Cancel + + + Confirm + + +
+
+
+ )} + +
+ +
+
{app.title}
+
+ {app.description} +
+
+
+ + } + description="" + /> +
+
+ + + 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. +
  • +
+
+ +
+
+ {createdApp && connectionSecret && ( + + )} + {!createdApp && ( + + + Configure Alby Go + + +
+ + setSuperuser(e.valueOf() as boolean) + } + checked={isSuperuser} + /> + +
+ { + + } +
+
+ )} +
+
+ {(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 && ( + + + + )} + + + )} +
+
+
+ ); +} diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 7f2f566a..4f3f6a60 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 = { @@ -194,6 +198,7 @@ export interface CreateAppRequest { returnTo?: string; isolated?: boolean; metadata?: AppMetadata; + unlockPassword?: string; // required to create superuser apps } export interface CreateAppResponse { diff --git a/nip47/controllers/create_connection_controller.go b/nip47/controllers/create_connection_controller.go new file mode 100644 index 00000000..b4aa1902 --- /dev/null +++ b/nip47/controllers/create_connection_controller.go @@ -0,0 +1,118 @@ +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 createConnectionBudgetParams struct { + Budget uint64 `json:"budget"` + RenewalPeriod string `json:"renewal_period"` +} + +type createConnectionParams struct { + Pubkey string `json:"pubkey"` // pubkey of the app connection + Name string `json:"name"` + Methods []string `json:"methods"` + Budget createConnectionBudgetParams `json:"budget"` + 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 + } + + // explicitly do not allow creating an app with create_connection permission + if slices.Contains(params.Methods, 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 + } + + supportedMethods := controller.lnClient.GetSupportedNIP47Methods() + if slices.ContainsFunc(params.Methods, 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.Methods) + + // ensure there is at least one scope + if len(scopes) == 0 { + publishResponse(&models.Response{ + ResultType: nip47Request.Method, + Error: &models.Error{ + Code: constants.ERROR_INTERNAL, + Message: "No methods provided", + }, + }, nostr.Tags{}) + return + } + + app, _, err := controller.appsService.CreateApp(params.Name, params.Pubkey, params.Budget.Budget, params.Budget.RenewalPeriod, 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..0801f7d3 --- /dev/null +++ b/nip47/controllers/create_connection_controller_test.go @@ -0,0 +1,312 @@ +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/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 TestHandleCreateConnectionEvent(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", + "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 + } + + permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) + transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). + 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, 1, len(permissions)) + assert.Equal(t, constants.GET_INFO_SCOPE, permissions[0].Scope) +} + +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) + + _, _, err = svc.AppsService.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", + "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 + } + + permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) + transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). + 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 + } + + permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) + transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). + HandleCreateConnectionEvent(ctx, nip47Request, dbRequestEvent.ID, publishResponse) + + assert.NotNil(t, publishedResponse.Error) + assert.Equal(t, constants.ERROR_INTERNAL, publishedResponse.Error.Code) + assert.Equal(t, "No 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", + "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 + } + + permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) + transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). + 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_CreateConnectionMethod(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", + "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 + } + + permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) + transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). + 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) +} +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", + "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 + } + + permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) + transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). + 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) +} diff --git a/nip47/controllers/get_balance_controller_test.go b/nip47/controllers/get_balance_controller_test.go index b1931024..95e24291 100644 --- a/nip47/controllers/get_balance_controller_test.go +++ b/nip47/controllers/get_balance_controller_test.go @@ -48,7 +48,7 @@ func TestHandleGetBalanceEvent(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleGetBalanceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Equal(t, int64(21000), publishedResponse.Result.(*getBalanceResponse).Balance) @@ -82,7 +82,7 @@ func TestHandleGetBalanceEvent_IsolatedApp_NoTransactions(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleGetBalanceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Equal(t, int64(0), publishedResponse.Result.(*getBalanceResponse).Balance) @@ -129,7 +129,7 @@ func TestHandleGetBalanceEvent_IsolatedApp_Transactions(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). 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..05363e47 100644 --- a/nip47/controllers/get_budget_controller_test.go +++ b/nip47/controllers/get_budget_controller_test.go @@ -59,7 +59,7 @@ func TestHandleGetBudgetEvent_NoRenewal(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleGetBudgetEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Equal(t, uint64(400000), publishedResponse.Result.(*getBudgetResponse).TotalBudget) @@ -105,7 +105,7 @@ func TestHandleGetBudgetEvent_NoneUsed(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleGetBudgetEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Equal(t, uint64(400000), publishedResponse.Result.(*getBudgetResponse).TotalBudget) @@ -159,7 +159,7 @@ func TestHandleGetBudgetEvent_HalfUsed(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleGetBudgetEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Equal(t, uint64(400000), publishedResponse.Result.(*getBudgetResponse).TotalBudget) @@ -210,7 +210,7 @@ func TestHandleGetBudgetEvent_NoBudget(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleGetBudgetEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Equal(t, struct{}{}, publishedResponse.Result) @@ -242,7 +242,7 @@ func TestHandleGetBudgetEvent_NoPayInvoicePermission(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleGetBudgetEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Equal(t, struct{}{}, publishedResponse.Result) diff --git a/nip47/controllers/get_info_controller_test.go b/nip47/controllers/get_info_controller_test.go index c55855ab..ad6818b3 100644 --- a/nip47/controllers/get_info_controller_test.go +++ b/nip47/controllers/get_info_controller_test.go @@ -56,7 +56,7 @@ func TestHandleGetInfoEvent_NoPermission(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleGetInfoEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Nil(t, publishedResponse.Error) @@ -105,7 +105,7 @@ func TestHandleGetInfoEvent_WithPermission(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleGetInfoEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Nil(t, publishedResponse.Error) @@ -157,7 +157,7 @@ func TestHandleGetInfoEvent_WithMetadata(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleGetInfoEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Nil(t, publishedResponse.Error) @@ -216,7 +216,7 @@ func TestHandleGetInfoEvent_WithNotifications(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). 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..76ccbd0f 100644 --- a/nip47/controllers/list_transactions_controller_test.go +++ b/nip47/controllers/list_transactions_controller_test.go @@ -77,7 +77,7 @@ func TestHandleListTransactionsEvent(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleListTransactionsEvent(ctx, nip47Request, dbRequestEvent.ID, *dbRequestEvent.AppId, publishResponse) assert.Nil(t, publishedResponse.Error) @@ -148,7 +148,7 @@ func TestHandleListTransactionsEvent_UnpaidOutgoingOnly(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleListTransactionsEvent(ctx, nip47Request, dbRequestEvent.ID, *dbRequestEvent.AppId, publishResponse) assert.Nil(t, publishedResponse.Error) @@ -210,7 +210,7 @@ func TestHandleListTransactionsEvent_UnpaidIncomingOnly(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleListTransactionsEvent(ctx, nip47Request, dbRequestEvent.ID, *dbRequestEvent.AppId, publishResponse) assert.Nil(t, publishedResponse.Error) @@ -272,7 +272,7 @@ func TestHandleListTransactionsEvent_Unpaid(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleListTransactionsEvent(ctx, nip47Request, dbRequestEvent.ID, *dbRequestEvent.AppId, publishResponse) assert.Nil(t, publishedResponse.Error) @@ -340,7 +340,7 @@ func TestHandleListTransactionsEvent_Paid(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). 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..0d342f02 100644 --- a/nip47/controllers/lookup_invoice_controller_test.go +++ b/nip47/controllers/lookup_invoice_controller_test.go @@ -68,7 +68,7 @@ func TestHandleLookupInvoiceEvent(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). 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..4239ddeb 100644 --- a/nip47/controllers/make_invoice_controller_test.go +++ b/nip47/controllers/make_invoice_controller_test.go @@ -66,7 +66,7 @@ func TestHandleMakeInvoiceEvent(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). 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..ee39466f 100644 --- a/nip47/controllers/multi_pay_invoice_controller_test.go +++ b/nip47/controllers/multi_pay_invoice_controller_test.go @@ -119,7 +119,7 @@ func TestHandleMultiPayInvoiceEvent_Success(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleMultiPayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) var paymentHashes = []string{ @@ -187,7 +187,7 @@ func TestHandleMultiPayInvoiceEvent_OneMalformedInvoice(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleMultiPayInvoiceEvent(ctx, nip47Request, requestEvent.ID, app, publishResponse) assert.Equal(t, 2, len(responses)) @@ -248,7 +248,7 @@ func TestHandleMultiPayInvoiceEvent_OneExpiredInvoice(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleMultiPayInvoiceEvent(ctx, nip47Request, requestEvent.ID, app, publishResponse) assert.Equal(t, 2, len(responses)) @@ -321,7 +321,7 @@ func TestHandleMultiPayInvoiceEvent_IsolatedApp_OneBudgetExceeded(t *testing.T) permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleMultiPayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Equal(t, 2, len(responses)) @@ -401,7 +401,7 @@ func TestHandleMultiPayInvoiceEvent_LNClient_OnePaymentFailed(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). 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..69f258d2 100644 --- a/nip47/controllers/multi_pay_keysend_controller_test.go +++ b/nip47/controllers/multi_pay_keysend_controller_test.go @@ -108,7 +108,7 @@ func TestHandleMultiPayKeysendEvent_Success(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleMultiPayKeysendEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Equal(t, 2, len(responses)) @@ -160,7 +160,7 @@ func TestHandleMultiPayKeysendEvent_OneBudgetExceeded(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). 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..ffb9c616 100644 --- a/nip47/controllers/nip47_controller.go +++ b/nip47/controllers/nip47_controller.go @@ -1,6 +1,7 @@ package controllers import ( + "github.com/getAlby/hub/apps" "github.com/getAlby/hub/events" "github.com/getAlby/hub/lnclient" "github.com/getAlby/hub/nip47/permissions" @@ -14,14 +15,22 @@ type nip47Controller struct { eventPublisher events.EventPublisher permissionsService permissions.PermissionsService transactionsService transactions.TransactionsService + appsService apps.AppsService } -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) *nip47Controller { return &nip47Controller{ lnClient: lnClient, db: db, eventPublisher: eventPublisher, permissionsService: permissionsService, transactionsService: transactionsService, + appsService: appsService, } } diff --git a/nip47/controllers/pay_invoice_controller_test.go b/nip47/controllers/pay_invoice_controller_test.go index 4ead9348..4643e3cf 100644 --- a/nip47/controllers/pay_invoice_controller_test.go +++ b/nip47/controllers/pay_invoice_controller_test.go @@ -87,7 +87,7 @@ func TestHandlePayInvoiceEvent(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandlePayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse, nostr.Tags{}) assert.Equal(t, "123preimage", publishedResponse.Result.(payResponse).Preimage) @@ -138,7 +138,7 @@ func TestHandlePayInvoiceEvent_0Amount(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandlePayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse, nostr.Tags{}) assert.Equal(t, "123preimage", publishedResponse.Result.(payResponse).Preimage) @@ -183,7 +183,7 @@ func TestHandlePayInvoiceEvent_MalformedInvoice(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandlePayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse, nostr.Tags{}) assert.Nil(t, publishedResponse.Result) @@ -224,7 +224,7 @@ func TestHandlePayInvoiceEvent_ExpiredInvoice(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). 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..fb8bbc0c 100644 --- a/nip47/controllers/pay_keysend_controller_test.go +++ b/nip47/controllers/pay_keysend_controller_test.go @@ -80,7 +80,7 @@ func TestHandlePayKeysendEvent(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandlePayKeysendEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse, nostr.Tags{}) assert.Nil(t, publishedResponse.Error) @@ -121,7 +121,7 @@ func TestHandlePayKeysendEvent_WithPreimage(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). 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..87667e7b 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) 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..a25e639e 100644 --- a/nip47/models/models.go +++ b/nip47/models/models.go @@ -10,6 +10,7 @@ const ( RESPONSE_KIND = 23195 LEGACY_NOTIFICATION_KIND = 23196 NOTIFICATION_KIND = 23197 + NWA_EVENT_KIND = 33195 // NIP-44 // request methods PAY_INVOICE_METHOD = "pay_invoice" @@ -23,6 +24,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..c8864c41 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), 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..30949333 100644 --- a/nip47/publish_nip47_info.go +++ b/nip47/publish_nip47_info.go @@ -56,6 +56,12 @@ 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}) + // also include the LN address for apps that use it (e.g. nostr apps assign lud16 to the profile) + lightningAddress, _ := svc.albyOAuthSvc.GetLightningAddress() + tags = append(tags, []string{"lud16", lightningAddress}) } 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 14475f40..3a80b6d5 100644 --- a/service/service.go +++ b/service/service.go @@ -112,14 +112,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,