Feat: NWA auth for self-hosted hubs (#1016)

* feat: nwc create_connection command (WIP)

* feat: allow creating superuser apps from the ui

* fix: pass methods rather than scopes in create_connection method

* chore: add extra tests

* fix: use browser router in http mode

* fix: update links to not use hash router

* fix: add redirect from hash router url

* fix: return nostrWalletConnectUrl in nwc connection success event and message

* feat: publish nwa event

* chore: use nwc info event instead of nwa event

* feat: create custom alby go detail page

* chore: allow creating/editing apps with the same name

* fix: convert budget from msats to sats (#1111)

* chore: address NWA feedback

* chore: address feedback

- adjust button copy
- make create_connection methods consistent with http deeplink flow
- remove unused constant
- add empty string check before adding lud16 tag
- fix test

* feat: add support for notification_types in create_connection method

* fix: shorter button copy

* fix: do not include lud16 tag in published info event

* Feat: add lud16 to get_info response (#1128)

feat: add lud16 to get_info response

* chore: minor alby go screen improvements

* fix: incorrect unlock password error message to create app with superuser access

* chore: minor ui improvements on alby go detail page

* chore: avoid duplicate app names by adding a suffix

* chore: move scopes check to apps service, add new tests, DRY controller test setup

* fix: scopes component full access scopes and isolated scope group check

* fix: use supported capabilities for Alby Go

* fix: return correct app name

* chore: address minor comments

---------

Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
This commit is contained in:
Roland 2025-02-27 19:12:55 +07:00 committed by GitHub
parent 1d5730ad15
commit 3e1d16d423
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
49 changed files with 1856 additions and 297 deletions

View file

@ -14,3 +14,6 @@ packages:
github.com/getAlby/hub/lnclient:
interfaces:
LNClient:
github.com/getAlby/hub/config:
interfaces:
Config:

View file

@ -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,

View file

@ -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
}
}

23
api/apps_test.go Normal file
View file

@ -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())
}

View file

@ -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 {

View file

@ -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
}

View file

@ -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())
}

View file

@ -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

View file

@ -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<PermissionsProps> = ({
</>
)}
</>
{permissions.scopes.includes("superuser") && (
<>
<div className="flex items-center gap-2 mt-4">
<AlertTriangleIcon className="w-4 h-4" />
<p className="text-sm font-medium">
This app can create other app connections
</p>
</div>
<p className="text-muted-foreground text-sm">
Make sure to set budgets on connections created by this app.
</p>
</>
)}
</div>
);
};

View file

@ -87,10 +87,17 @@ const Scopes: React.FC<ScopesProps> = ({
}, [capabilities.scopes]);
const [scopeGroup, setScopeGroup] = React.useState<ScopeGroup>(() => {
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 (

View file

@ -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: (
<>
<div>
<h3 className="font-medium">In Alby Go</h3>
<ul className="list-inside text-muted-foreground">
<li>
1. Download and open{" "}
<span className="font-medium text-foreground">Alby Go</span> on
your Android or iOS device
</li>
<li>
2. Click on{" "}
<span className="font-medium text-foreground">
Connect Wallet
</span>
</li>
</ul>
</div>
<div>
<h3 className="font-medium">In Alby Hub</h3>
<ul className="list-inside text-muted-foreground">
<li>
4. Click{" "}
<Link
to="/apps/new?app=alby-go"
className="font-medium text-foreground underline"
>
Connect to Alby Go
</Link>
</li>
<li>5. Set app's wallet permissions (full access recommended)</li>
</ul>
</div>
<div>
<h3 className="font-medium">In Alby Go</h3>
<ul className="list-inside text-muted-foreground">
<li>6. Scan or paste the connection secret from Alby Hub</li>
</ul>
</div>
</>
),
},
{
id: "pullthatupjamie-ai",
title: "Pull That Up Jamie!",

View file

@ -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: <UncleJim />,
},
{
path: "alby-go",
element: <AlbyGo />,
},
{
path: "buzzpay",
element: <BuzzPay />,

View file

@ -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<string>();
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,

View file

@ -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),

View file

@ -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<string>();
const [connectionSecret, setConnectionSecret] = React.useState<string>("");
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 <Loading />;
}
return (
<div className="grid gap-5">
<AlertDialog open={showCreateConnectionDialog}>
<AlertDialogContent>
<form onSubmit={onSubmitCreateConnection}>
<AlertDialogHeader>
<AlertDialogTitle>Confirm New Connection</AlertDialogTitle>
<AlertDialogDescription>
<div className="flex flex-col">
<p>
Alby Go will be given permission to create other app
connections which can spend your balance.
</p>
<p className="mt-4">
Alby Go will be given a 100k sat / month budget by default
which you can edit after creating the connection.
</p>
<p className="mt-4">
Warning: Alby Go can create connections with a larger budget
than the one set for Alby Go. Make sure to always set a
budget.
</p>
<p className="mt-4">
Please enter your unlock password to continue.
</p>
<div className="grid gap-1.5 mt-2">
<Label htmlFor="password">Unlock Password</Label>
<Input
autoFocus
type="password"
name="password"
required
onChange={(e) => setUnlockPassword(e.target.value)}
value={unlockPassword}
/>
</div>
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter className="mt-3">
<AlertDialogCancel
onClick={() => setShowCreateConnectionDialog(false)}
>
Cancel
</AlertDialogCancel>
<LoadingButton loading={loading} type="submit">
Confirm
</LoadingButton>
</AlertDialogFooter>
</form>
</AlertDialogContent>
</AlertDialog>
<AppHeader
title={
<>
<div className="flex flex-row items-center">
<img src={app.logo} className="w-14 h-14 rounded-lg mr-4" />
<div className="flex flex-col">
<div>{app.title}</div>
<div className="text-sm font-normal text-muted-foreground">
{app.description}
</div>
</div>
</div>
</>
}
description=""
contentRight={
!createdApp && (
<Link to={`/apps/new?app=${app.id}`}>
<Button>
<NostrWalletConnectIcon className="w-4 h-4 mr-2" />
Connect to {app.title}
</Button>
</Link>
)
}
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="flex flex-col w-full gap-6">
<Card>
<CardHeader>
<CardTitle className="text-2xl">About the App</CardTitle>
</CardHeader>
{app.extendedDescription && (
<CardContent className="flex flex-col gap-3">
<p className="text-muted-foreground">
{app.extendedDescription}
</p>
</CardContent>
)}
</Card>
<Card>
<CardHeader>
<CardTitle className="text-2xl">How to Connect</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-3">
<>
<div>
<h3 className="font-medium">In Alby Go</h3>
<ul className="list-inside text-muted-foreground">
<li>
1. Download and open{" "}
<span className="font-medium text-foreground">
Alby Go
</span>{" "}
on your Android or iOS device
</li>
<li>
2. Click on{" "}
<span className="font-medium text-foreground">
Connect Wallet
</span>
</li>
<li>
3.{" "}
<span className="font-medium text-foreground">
Scan or paste
</span>{" "}
the connection secret from Alby Hub that will be revealed
once you create the connection below.
</li>
</ul>
</div>
</>
</CardContent>
</Card>
</div>
<div className="flex flex-col w-full gap-6">
{(app.appleLink ||
app.playLink ||
app.zapStoreLink ||
app.chromeLink ||
app.firefoxLink) && (
<Card>
<CardHeader>
<CardTitle className="text-2xl">Get This App</CardTitle>
</CardHeader>
<CardFooter className="flex flex-row gap-2">
{app.playLink && (
<ExternalLink to={app.playLink}>
<Button variant="outline">
<PlayStoreIcon className="w-4 h-4 mr-2" />
Play Store
</Button>
</ExternalLink>
)}
{app.appleLink && (
<ExternalLink to={app.appleLink}>
<Button variant="outline">
<AppleIcon className="w-4 h-4 mr-2" />
App Store
</Button>
</ExternalLink>
)}
{app.zapStoreLink && (
<ExternalLink to={app.zapStoreLink}>
<Button variant="outline">
<ZapStoreIcon className="w-4 h-4 mr-2" />
Zapstore
</Button>
</ExternalLink>
)}
{app.chromeLink && (
<ExternalLink to={app.chromeLink}>
<Button variant="outline">
<ChromeIcon className="w-4 h-4 mr-2" />
Chrome Web Store
</Button>
</ExternalLink>
)}
{app.firefoxLink && (
<ExternalLink to={app.firefoxLink}>
<Button variant="outline">
<FirefoxIcon className="w-4 h-4 mr-2" />
Firefox Add-Ons
</Button>
</ExternalLink>
)}
</CardFooter>
</Card>
)}
{app.webLink && (
<Card>
<CardHeader>
<CardTitle className="text-2xl">Links</CardTitle>
</CardHeader>
<CardFooter className="flex flex-row gap-2">
{app.webLink && (
<ExternalLink to={app.webLink}>
<Button variant="outline">
<Globe className="w-4 h-4 mr-2" />
Website
</Button>
</ExternalLink>
)}
</CardFooter>
</Card>
)}
{createdApp && connectionSecret && (
<div className="mb-16 w-full">
<ConnectAppCard app={createdApp} pairingUri={connectionSecret} />
</div>
)}
{!createdApp && (
<Card>
<CardHeader>
<CardTitle className="text-2xl">One Tap Connections</CardTitle>
</CardHeader>
<CardContent>
<p className="text-muted-foreground">
Use Alby Go to quickly connect other apps to your hub with one
tap on mobile.
</p>
{
<Button className="mt-8" onClick={onClickCreateConnection}>
<NostrWalletConnectIcon className="w-4 h-4 mr-2" />
Connect with One Tap Connections
</Button>
}
</CardContent>
</Card>
)}
</div>
</div>
</div>
);
}

View file

@ -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"],

View file

@ -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"],

View file

@ -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: [

View file

@ -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();

View file

@ -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<Scope, string> = {
pay_invoice: "Send payments",
sign_message: "Sign messages",
notifications: "Receive wallet notifications",
superuser: "Create other app connections",
};
export const expiryOptions: Record<string, number> = {
@ -195,6 +199,7 @@ export interface CreateAppRequest {
returnTo?: string;
isolated?: boolean;
metadata?: AppMetadata;
unlockPassword?: string; // required to create superuser apps
}
export interface CreateAppResponse {

View file

@ -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()),
}
}

View file

@ -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)
}

View file

@ -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{})
}

View file

@ -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)
}

View file

@ -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)

View file

@ -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)

View file

@ -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
}

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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{}{

View file

@ -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))

View file

@ -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

View file

@ -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,
}
}

View file

@ -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)

View file

@ -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)

View file

@ -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,

View file

@ -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)

View file

@ -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 {

View file

@ -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,
}
}

View file

@ -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,
}
}

View file

@ -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)

View file

@ -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")

View file

@ -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")

View file

@ -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,

View file

@ -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
}

646
tests/mocks/Config.go Normal file
View file

@ -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
}

View file

@ -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,

View file

@ -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()),
}
}