feat: app child key derived from wallet master key (#736)

* feat(appwalletKey): add GetBIP32ChildKey

* feat: fix interface

* feat: adding subscription WiP

* feat: handle nostr subscriptions for lifecycle of apps

* Delete .idea/.gitignore

* Delete .idea/hub.iml

* Delete .idea/modules.xml

* Delete .idea/vcs.xml

* fix: remove unnecessary

* fix: missing handling legacy app

* fix: review fixes

* fix: use app.ID for key calculation instead of passing in event

* fix: add TODO

* fix: remove unnecessary check

* fix: improve err handling and remove check

* chore: store master nostr key to avoid deriving each time

* chore: rename app nostr_pubkey to app_pubkey, extract app consumers into separate files

* chore: finish renaming

* fix: update app wallet pubkey on app creation

* fix: not NULL check

* fix: fix HandleEvent

* fix: error handling

* fix: error handling

* fix: move StartSubscription to start.go

* fix: remove duplicated error check

* fix: make tests use AppsService for creating apps

* chore: remove unused code

* chore: minor event handler improvements

- fix error code response when failing to update request event
- fix log content when failing to decrypt request event
- move logging of app to correct place

* fix: add event_handler tests for legacy app

* chore: add comment about legacy apps in deleteAppConsumer

* fix: remove unused app from tests

* fix: error handling in startAppWalletSubscription

* fix: only create event info and nostr subscription for master key if there are legacy apps

* fix: add legacy tests

* fix: move fetching of Nip47 event info to deleteAppConsumer

* fix: fixed arguments

* fix: use require instead of assert

* fix: adapt GetAppWalletKey to use DeriveKey with path 1'

* fix: for backends that don't use a mnemonic, create appKey from nostrSecretKey

* fix: cleanup eventPublisher Subscribers when relay reconnects

* fix: bip32.FirstHardenedChild + appID

* fix: remove unused env vars

* fix: generate new mnemonic if empty

* fix: add tests.CreateTestServiceWithMnemonic to fix TestEncryptedBackup

* fix: handle both relay and main ctx Done

* chore: add keys tests

* chore: add extra assertions to keys test

* chore: log when legacy app subscription is created

* chore: remove unnecessary break

* chore: add log when relay is successfully connected

* fix: only auto-start node if it has been started before

---------

Co-authored-by: Roland Bewick <roland.bewick@gmail.com>
This commit is contained in:
frnandu 2024-11-07 13:06:01 +01:00 committed by GitHub
parent 4b6d48867e
commit 5fdc803f7a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 956 additions and 264 deletions

View file

@ -17,7 +17,6 @@ FRONTEND_URL=http://localhost:5173
#AUTO_UNLOCK_PASSWORD=123
#WORK_DIR=.data
#DATABASE_URI=nwc.db
#NOSTR_PRIVKEY=
#JWT_SECRET=secretsecret
#RELAY=wss://relay.getalby.com/v1
#RELAY=ws://localhost:7447/v1

View file

@ -24,7 +24,7 @@ Go to the [Deploy it yourself](#deploy-it-yourself) section below.
## Supported Backends
By default Alby Hub uses the embedded LDK based lightning node. Optionally it can be configured to use an external node:
By default Alby Hub uses the embedded LDK based lightning node. Optionally it can be configured to use an external node:
- LND
- Phoenixd
@ -41,6 +41,7 @@ By default Alby Hub uses the embedded LDK based lightning node. Optionally it ca
- Yarn
### Environment setup
$ cp .env.example .env
# edit the config for your needs (Read further down for all the available env options)
$ vim .env
@ -135,8 +136,6 @@ Breez SDK requires gcc to build the Breez bindings. Run `choco install mingw` an
The following configuration options can be set as environment variables or in a .env file
- `NOSTR_PRIVKEY`: the private key of this service. Should be a securely randomly generated 32 byte hex string.
- `CLIENT_NOSTR_PUBKEY`: if set, this service will only listen to events authored by this public key. You can set this to your own nostr public key.
- `RELAY`: default: "wss://relay.getalby.com/v1"
- `JWT_SECRET`: a randomly generated secret string. (only needed in http mode)
- `DATABASE_URI`: a sqlite filename. Default: $XDG_DATA_HOME/albyhub/nwc.db
@ -362,7 +361,6 @@ Go to the [Quick start script](https://github.com/getAlby/hub/tree/master/script
Go to the [Quick start script](https://github.com/getAlby/hub/blob/master/scripts/pi-aarch64) which you can run as a service.
#### Quick start (Raspberry PI Zero)
Go to the [Quick start script](https://github.com/getAlby/hub/tree/master/scripts/pi-arm) which you can run as a service.

View file

@ -528,7 +528,7 @@ func (svc *albyOAuthService) LinkAccount(ctx context.Context, lnClient lnclient.
scopes = append(scopes, constants.NOTIFICATIONS_SCOPE)
}
app, _, err := apps.NewAppsService(svc.db, svc.eventPublisher).CreateApp(
app, _, err := apps.NewAppsService(svc.db, svc.eventPublisher, svc.keys).CreateApp(
ALBY_ACCOUNT_APP_NAME,
connectionPubkey,
budget,

View file

@ -41,14 +41,10 @@ func TestExistingEncryptedBackup(t *testing.T) {
func TestEncryptedBackup(t *testing.T) {
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
require.NoError(t, err)
mnemonic := "limit reward expect search tissue call visa fit thank cream brave jump"
unlockPassword := "123"
svc.Cfg.SetUpdate("Mnemonic", mnemonic, unlockPassword)
err = svc.Keys.Init(svc.Cfg, unlockPassword)
assert.NoError(t, err)
svc, err := tests.CreateTestServiceWithMnemonic(mnemonic, unlockPassword)
require.NoError(t, err)
albyOAuthSvc := NewAlbyOAuthService(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher)
encryptedBackup, err := albyOAuthSvc.createEncryptedChannelBackup(&events.StaticChannelsBackupEvent{

View file

@ -47,7 +47,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),
appsSvc: apps.NewAppsService(gormDB, eventPublisher, keys),
cfg: config,
svc: svc,
permissionsSvc: permissions.NewPermissionsService(gormDB, eventPublisher),
@ -80,7 +80,8 @@ func (api *api) CreateApp(createAppRequest *CreateAppRequest) (*CreateAppRespons
expiresAt,
createAppRequest.Scopes,
createAppRequest.Isolated,
createAppRequest.Metadata)
createAppRequest.Metadata,
)
if err != nil {
return nil, err
@ -91,7 +92,7 @@ func (api *api) CreateApp(createAppRequest *CreateAppRequest) (*CreateAppRespons
responseBody := &CreateAppResponse{}
responseBody.Id = app.ID
responseBody.Name = createAppRequest.Name
responseBody.Pubkey = app.NostrPubkey
responseBody.Pubkey = app.AppPubkey
responseBody.PairingSecret = pairingSecretKey
lightningAddress, err := api.albyOAuthSvc.GetLightningAddress()
@ -104,7 +105,7 @@ func (api *api) CreateApp(createAppRequest *CreateAppRequest) (*CreateAppRespons
if err == nil {
query := returnToUrl.Query()
query.Add("relay", relayUrl)
query.Add("pubkey", api.keys.GetNostrPublicKey())
query.Add("pubkey", *app.WalletPubkey)
if lightningAddress != "" && !app.Isolated {
query.Add("lud16", lightningAddress)
}
@ -117,7 +118,8 @@ func (api *api) CreateApp(createAppRequest *CreateAppRequest) (*CreateAppRespons
if lightningAddress != "" && !app.Isolated {
lud16 = fmt.Sprintf("&lud16=%s", lightningAddress)
}
responseBody.PairingUri = fmt.Sprintf("nostr+walletconnect://%s?relay=%s&secret=%s%s", api.keys.GetNostrPublicKey(), relayUrl, pairingSecretKey, lud16)
responseBody.PairingUri = fmt.Sprintf("nostr+walletconnect://%s?relay=%s&secret=%s%s", *app.WalletPubkey, relayUrl, pairingSecretKey, lud16)
return responseBody, nil
}
@ -216,7 +218,7 @@ func (api *api) UpdateApp(userApp *db.App, updateAppRequest *UpdateAppRequest) e
}
func (api *api) DeleteApp(userApp *db.App) error {
return api.db.Delete(userApp).Error
return api.appsSvc.DeleteApp(userApp)
}
func (api *api) GetApp(dbApp *db.App) *App {
@ -260,7 +262,7 @@ func (api *api) GetApp(dbApp *db.App) *App {
Description: dbApp.Description,
CreatedAt: dbApp.CreatedAt,
UpdatedAt: dbApp.UpdatedAt,
NostrPubkey: dbApp.NostrPubkey,
AppPubkey: dbApp.AppPubkey,
ExpiresAt: expiresAt,
MaxAmountSat: maxAmount,
Scopes: requestMethods,
@ -311,7 +313,7 @@ func (api *api) ListApps() ([]App, error) {
Description: dbApp.Description,
CreatedAt: dbApp.CreatedAt,
UpdatedAt: dbApp.UpdatedAt,
NostrPubkey: dbApp.NostrPubkey,
AppPubkey: dbApp.AppPubkey,
Isolated: dbApp.Isolated,
}

View file

@ -60,7 +60,7 @@ type App struct {
ID uint `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
NostrPubkey string `json:"nostrPubkey"`
AppPubkey string `json:"appPubkey"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
LastEventAt *time.Time `json:"lastEventAt"`

View file

@ -12,6 +12,7 @@ import (
"github.com/getAlby/hub/db"
"github.com/getAlby/hub/events"
"github.com/getAlby/hub/logger"
"github.com/getAlby/hub/service/keys"
"github.com/nbd-wtf/go-nostr"
"gorm.io/datatypes"
"gorm.io/gorm"
@ -19,18 +20,21 @@ import (
type AppsService interface {
CreateApp(name string, pubkey string, maxAmountSat uint64, budgetRenewal string, expiresAt *time.Time, scopes []string, isolated bool, metadata map[string]interface{}) (*db.App, string, error)
DeleteApp(app *db.App) error
GetAppByPubkey(pubkey string) *db.App
}
type appsService struct {
db *gorm.DB
eventPublisher events.EventPublisher
keys keys.Keys
}
func NewAppsService(db *gorm.DB, eventPublisher events.EventPublisher) *appsService {
func NewAppsService(db *gorm.DB, eventPublisher events.EventPublisher, keys keys.Keys) *appsService {
return &appsService{
db: db,
eventPublisher: eventPublisher,
keys: keys,
}
}
@ -65,7 +69,7 @@ func (svc *appsService) CreateApp(name string, pubkey string, maxAmountSat uint6
}
}
app := db.App{Name: name, NostrPubkey: pairingPublicKey, Isolated: isolated, Metadata: datatypes.JSON(metadataBytes)}
app := db.App{Name: name, AppPubkey: pairingPublicKey, Isolated: isolated, Metadata: datatypes.JSON(metadataBytes)}
err := svc.db.Transaction(func(tx *gorm.DB) error {
err := tx.Save(&app).Error
@ -88,6 +92,21 @@ func (svc *appsService) CreateApp(name string, pubkey string, maxAmountSat uint6
}
}
appWalletPrivKey, err := svc.keys.GetAppWalletKey(app.ID)
if err != nil {
return fmt.Errorf("error generating wallet child private key: %w", err)
}
appWalletPubkey, err := nostr.GetPublicKey(appWalletPrivKey)
if err != nil {
return fmt.Errorf("error generating wallet child public key: %w", err)
}
err = tx.Model(&app).Update("wallet_pubkey", appWalletPubkey).Error
if err != nil {
return err
}
// commit transaction
return nil
})
@ -101,15 +120,32 @@ func (svc *appsService) CreateApp(name string, pubkey string, maxAmountSat uint6
Event: "app_created",
Properties: map[string]interface{}{
"name": name,
"id": app.ID,
},
})
return &app, pairingSecretKey, nil
}
func (svc *appsService) DeleteApp(app *db.App) error {
err := svc.db.Delete(app).Error
if err != nil {
return err
}
svc.eventPublisher.Publish(&events.Event{
Event: "app_deleted",
Properties: map[string]interface{}{
"name": app.Name,
"id": app.ID,
},
})
return nil
}
func (svc *appsService) GetAppByPubkey(pubkey string) *db.App {
dbApp := db.App{}
findResult := svc.db.Where("nostr_pubkey = ?", pubkey).First(&dbApp)
findResult := svc.db.Where("app_pubkey = ?", pubkey).First(&dbApp)
if findResult.RowsAffected == 0 {
return nil
}

View file

@ -0,0 +1,26 @@
package migrations
import (
_ "embed"
"github.com/go-gormigrate/gormigrate/v2"
"gorm.io/gorm"
)
var _202410141503_add_wallet_pubkey = &gormigrate.Migration{
ID: "202410141503_add_wallet_pubkey",
Migrate: func(tx *gorm.DB) error {
if err := tx.Exec(`
ALTER TABLE apps ADD COLUMN wallet_pubkey TEXT;
ALTER TABLE apps RENAME COLUMN nostr_pubkey TO app_pubkey;
`).Error; err != nil {
return err
}
return nil
},
Rollback: func(tx *gorm.DB) error {
return nil
},
}

View file

@ -22,6 +22,7 @@ func Migrate(gormDB *gorm.DB) error {
_202408061737_add_boostagrams_and_use_json,
_202408191242_transaction_failure_reason,
_202408291715_app_metadata,
_202410141503_add_wallet_pubkey,
})
return m.Migrate()

View file

@ -16,14 +16,15 @@ type UserConfig struct {
}
type App struct {
ID uint
Name string `validate:"required"`
Description string
NostrPubkey string `validate:"required"`
CreatedAt time.Time
UpdatedAt time.Time
Isolated bool
Metadata datatypes.JSON
ID uint
Name string `validate:"required"`
Description string
AppPubkey string `validate:"required"`
WalletPubkey *string
CreatedAt time.Time
UpdatedAt time.Time
Isolated bool
Metadata datatypes.JSON
}
type AppPermission struct {

View file

@ -184,7 +184,7 @@ function TransactionItem({ tx }: Props) {
{app && (
<div className="mt-8">
<p>App</p>
<Link to={`/apps/${app.nostrPubkey}`}>
<Link to={`/apps/${app.appPubkey}`}>
<p className="font-semibold">
{app.name === "getalby.com" ? "Alby Account" : app.name}
</p>

View file

@ -161,7 +161,7 @@ function AlbyConnectionCard({ connection }: { connection?: App }) {
{connection && (
<div className="slashed-zero">
<Link
to={`/apps/${connection.nostrPubkey}?edit=true`}
to={`/apps/${connection.appPubkey}?edit=true`}
className="absolute top-0 right-0"
>
<EditIcon className="w-4 h-4 hidden group-hover:inline text-muted-foreground hover:text-card-foreground" />

View file

@ -24,7 +24,7 @@ export default function AppCard({ app }: Props) {
return (
<Card
className="flex flex-col group cursor-pointer"
onClick={() => navigate(`/apps/${app.nostrPubkey}`)}
onClick={() => navigate(`/apps/${app.appPubkey}`)}
>
<CardHeader>
<CardTitle className="relative">

View file

@ -115,7 +115,7 @@ export function AppCardConnectionInfo({
? dayjs(connection.lastEventAt).fromNow()
: "Never"}
</div>
<Link to={`/apps/${connection.nostrPubkey}?edit=true`}>
<Link to={`/apps/${connection.appPubkey}?edit=true`}>
<Button variant="outline">
<PlusCircle className="w-4 h-4 mr-2" />
Set Budget
@ -151,7 +151,7 @@ export function AppCardConnectionInfo({
: "Never"}
</div>
<Link
to={`/apps/${connection.nostrPubkey}?edit=true`}
to={`/apps/${connection.appPubkey}?edit=true`}
onClick={(e) => e.stopPropagation()}
>
<Button variant="outline">

View file

@ -30,7 +30,7 @@ export function AppCardNotice({ app }: AppCardNoticeProps) {
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Link to={`/apps/${app.nostrPubkey}`}>
<Link to={`/apps/${app.appPubkey}`}>
<Badge variant="destructive">
<CalendarClock className="w-3 h-3 mr-2" />
Expired
@ -44,7 +44,7 @@ export function AppCardNotice({ app }: AppCardNoticeProps) {
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Link to={`/apps/${app.nostrPubkey}`}>
<Link to={`/apps/${app.appPubkey}`}>
<Badge variant="outline">
<CalendarClock className="w-3 h-3 mr-2" />
Expires Soon

View file

@ -4,15 +4,15 @@ import { useToast } from "src/components/ui/use-toast";
import { handleRequestError } from "src/utils/handleRequestError";
import { request } from "src/utils/request";
export function useDeleteApp(onSuccess?: (nostrPubkey: string) => void) {
export function useDeleteApp(onSuccess?: (appPubkey: string) => void) {
const [isDeleting, setDeleting] = React.useState(false);
const { toast } = useToast();
const deleteApp = React.useCallback(
async (nostrPubkey: string) => {
async (appPubkey: string) => {
setDeleting(true);
try {
await request(`/api/apps/${nostrPubkey}`, {
await request(`/api/apps/${appPubkey}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
@ -20,7 +20,7 @@ export function useDeleteApp(onSuccess?: (nostrPubkey: string) => void) {
});
toast({ title: "Connection deleted" });
if (onSuccess) {
onSuccess(nostrPubkey);
onSuccess(appPubkey);
}
} catch (error) {
await handleRequestError(toast, "Failed to delete connection", error);

View file

@ -109,7 +109,7 @@ function AppCreatedInternal() {
Optional: Increase isolated balance (
{new Intl.NumberFormat().format(Math.floor(app.balance / 1000))}{" "}
sats){" "}
<IsolatedAppTopupDialog appPubkey={app.nostrPubkey}>
<IsolatedAppTopupDialog appPubkey={app.appPubkey}>
<Button size="sm" variant="secondary">
Increase
</Button>
@ -168,7 +168,7 @@ export function ConnectAppCard({
{timeout && (
<div className="text-sm flex flex-col gap-2 items-center text-center">
Connecting is taking longer than usual.
<Link to={`/apps/${app?.nostrPubkey}`}>
<Link to={`/apps/${app?.appPubkey}`}>
<Button variant="secondary">Continue anyway</Button>
</Link>
</div>

View file

@ -21,7 +21,7 @@ function AppList() {
const albyConnection = apps.find((x) => x.name === albyConnectionName);
const otherApps = apps
.filter((x) => x.nostrPubkey !== albyConnection?.nostrPubkey)
.filter((x) => x.appPubkey !== albyConnection?.appPubkey)
.sort(
(a, b) =>
new Date(b.lastEventAt ?? 0).getTime() -

View file

@ -119,7 +119,7 @@ function AppInternal({ app, refetchApp, capabilities }: AppInternalProps) {
maxAmount: permissions.maxAmount,
};
await request(`/api/apps/${app.nostrPubkey}`, {
await request(`/api/apps/${app.appPubkey}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
@ -208,7 +208,7 @@ function AppInternal({ app, refetchApp, capabilities }: AppInternalProps) {
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => deleteApp(app.nostrPubkey)}
onClick={() => deleteApp(app.appPubkey)}
disabled={isDeleting}
>
Continue
@ -235,7 +235,7 @@ function AppInternal({ app, refetchApp, capabilities }: AppInternalProps) {
<TableRow>
<TableCell className="font-medium">Public Key</TableCell>
<TableCell className="text-muted-foreground break-all">
{app.nostrPubkey}
{app.appPubkey}
</TableCell>
</TableRow>
{app.isolated && (
@ -246,7 +246,7 @@ function AppInternal({ app, refetchApp, capabilities }: AppInternalProps) {
Math.floor(app.balance / 1000)
)}{" "}
sats{" "}
<IsolatedAppTopupDialog appPubkey={app.nostrPubkey}>
<IsolatedAppTopupDialog appPubkey={app.appPubkey}>
<Button size="sm" variant="secondary">
Increase
</Button>

View file

@ -113,7 +113,7 @@ export interface App {
id: number;
name: string;
description: string;
nostrPubkey: string;
appPubkey: string;
createdAt: string;
updatedAt: string;
lastEventAt?: string;

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),
appsSvc: apps.NewAppsService(svc.GetDB(), eventPublisher, svc.GetKeys()),
}
}

View file

@ -47,15 +47,6 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela
return
}
ss, err := nip04.ComputeSharedSecret(event.PubKey, svc.keys.GetNostrSecretKey())
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"requestEventNostrId": event.ID,
"eventKind": event.Kind,
}).WithError(err).Error("Failed to compute shared secret")
return
}
// store request event
requestEvent := db.RequestEvent{AppId: nil, NostrId: event.ID, State: db.REQUEST_EVENT_STATE_HANDLER_EXECUTING}
err = svc.db.Create(&requestEvent).Error
@ -70,87 +61,16 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela
"requestEventNostrId": event.ID,
"eventKind": event.Kind,
}).WithError(err).Error("Failed to save nostr event")
nip47Response = &models.Response{
Error: &models.Error{
Code: constants.ERROR_INTERNAL,
Message: fmt.Sprintf("Failed to save nostr event: %s", err.Error()),
},
}
resp, err := svc.CreateResponse(event, nip47Response, nostr.Tags{}, ss)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"requestEventNostrId": event.ID,
"eventKind": event.Kind,
}).WithError(err).Error("Failed to process event")
}
svc.publishResponseEvent(ctx, relay, &requestEvent, resp, nil)
return
}
app := db.App{}
err = svc.db.First(&app, &db.App{
NostrPubkey: event.PubKey,
AppPubkey: event.PubKey,
}).Error
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"nostrPubkey": event.PubKey,
"appPubkey": event.PubKey,
}).WithError(err).Error("Failed to find app for nostr pubkey")
nip47Response = &models.Response{
Error: &models.Error{
Code: constants.ERROR_UNAUTHORIZED,
Message: "The public key does not have a wallet connected.",
},
}
resp, err := svc.CreateResponse(event, nip47Response, nostr.Tags{}, ss)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"requestEventNostrId": event.ID,
"eventKind": event.Kind,
}).WithError(err).Error("Failed to process event")
}
svc.publishResponseEvent(ctx, relay, &requestEvent, resp, &app)
requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR
err = svc.db.Save(&requestEvent).Error
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"nostrPubkey": event.PubKey,
}).WithError(err).Error("Failed to save state to nostr event")
}
return
}
requestEvent.AppId = &app.ID
err = svc.db.Save(&requestEvent).Error
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"nostrPubkey": event.PubKey,
}).WithError(err).Error("Failed to save app to nostr event")
nip47Response = &models.Response{
Error: &models.Error{
Code: constants.ERROR_UNAUTHORIZED,
Message: fmt.Sprintf("Failed to save app to nostr event: %s", err.Error()),
},
}
resp, err := svc.CreateResponse(event, nip47Response, nostr.Tags{}, ss)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"requestEventNostrId": event.ID,
"eventKind": event.Kind,
}).WithError(err).Error("Failed to process event")
}
svc.publishResponseEvent(ctx, relay, &requestEvent, resp, &app)
requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR
err = svc.db.Save(&requestEvent).Error
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"nostrPubkey": event.PubKey,
}).WithError(err).Error("Failed to save state to nostr event")
}
return
}
@ -160,24 +80,69 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela
"appId": app.ID,
}).Debug("App found for nostr event")
//to be extra safe, decrypt using the key found from the app
ss, err = nip04.ComputeSharedSecret(app.NostrPubkey, svc.keys.GetNostrSecretKey())
appWalletPrivKey := svc.keys.GetNostrSecretKey()
if app.WalletPubkey != nil {
// This is a new child key derived from master using app ID as index
appWalletPrivKey, err = svc.keys.GetAppWalletKey(app.ID)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"appId": app.ID,
}).WithError(err).Error("error deriving child key")
return
}
}
ss, err := nip04.ComputeSharedSecret(app.AppPubkey, appWalletPrivKey)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"requestEventNostrId": event.ID,
"eventKind": event.Kind,
}).WithError(err).Error("Failed to process event")
}).WithError(err).Error("Failed to compute shared secret")
requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR
err = svc.db.Save(&requestEvent).Error
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"nostrPubkey": event.PubKey,
"appPubkey": event.PubKey,
}).WithError(err).Error("Failed to save state to nostr event")
}
return
}
requestEvent.AppId = &app.ID
err = svc.db.Save(&requestEvent).Error
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"appPubkey": event.PubKey,
}).WithError(err).Error("Failed to save app to nostr event")
nip47Response = &models.Response{
Error: &models.Error{
Code: constants.ERROR_INTERNAL,
Message: fmt.Sprintf("Failed to save app to nostr event: %s", err.Error()),
},
}
resp, err := svc.CreateResponse(event, nip47Response, nostr.Tags{}, ss, appWalletPrivKey)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"requestEventNostrId": event.ID,
"eventKind": event.Kind,
}).WithError(err).Error("Failed to process event")
}
svc.publishResponseEvent(ctx, relay, &requestEvent, resp, &app)
requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR
err = svc.db.Save(&requestEvent).Error
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"appPubkey": event.PubKey,
}).WithError(err).Error("Failed to save state to nostr event")
}
return
}
payload, err := nip04.Decrypt(event.Content, ss)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
@ -188,13 +153,13 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela
logger.Logger.WithFields(logrus.Fields{
"requestEventNostrId": event.ID,
"eventKind": event.Kind,
}).WithError(err).Error("Failed to process event")
}).WithError(err).Error("Failed to decrypt request event")
requestEvent.State = db.REQUEST_EVENT_STATE_HANDLER_ERROR
err = svc.db.Save(&requestEvent).Error
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"nostrPubkey": event.PubKey,
"appPubkey": event.PubKey,
}).WithError(err).Error("Failed to save state to nostr event")
}
@ -212,7 +177,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela
err = svc.db.Save(&requestEvent).Error
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"nostrPubkey": event.PubKey,
"appPubkey": event.PubKey,
}).WithError(err).Error("Failed to save state to nostr event")
}
@ -226,7 +191,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela
// TODO: replace with a channel
// TODO: update all previous occurences of svc.publishResponseEvent to also use the channel
publishResponse := func(nip47Response *models.Response, tags nostr.Tags) {
resp, err := svc.CreateResponse(event, nip47Response, tags, ss)
resp, err := svc.CreateResponse(event, nip47Response, tags, ss, appWalletPrivKey)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"requestEventNostrId": event.ID,
@ -257,7 +222,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela
err = svc.db.Save(&requestEvent).Error
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"nostrPubkey": event.PubKey,
"appPubkey": event.PubKey,
}).WithError(err).Error("Failed to save state to nostr event")
}
}
@ -296,7 +261,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela
Properties: map[string]interface{}{
"request_method": nip47Request.Method,
"app_name": app.Name,
// "app_pubkey": app.NostrPubkey,
// "app_pubkey": app.AppPubkey,
"code": code,
"message": message,
},
@ -360,7 +325,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela
}
}
func (svc *nip47Service) CreateResponse(initialEvent *nostr.Event, content interface{}, tags nostr.Tags, ss []byte) (result *nostr.Event, err error) {
func (svc *nip47Service) CreateResponse(initialEvent *nostr.Event, content interface{}, tags nostr.Tags, ss []byte, appWalletPrivKey string) (result *nostr.Event, err error) {
payloadBytes, err := json.Marshal(content)
if err != nil {
return nil, err
@ -373,14 +338,20 @@ func (svc *nip47Service) CreateResponse(initialEvent *nostr.Event, content inter
allTags := nostr.Tags{[]string{"p", initialEvent.PubKey}, []string{"e", initialEvent.ID}}
allTags = append(allTags, tags...)
appWalletPubKey, err := nostr.GetPublicKey(appWalletPrivKey)
if err != nil {
logger.Logger.WithError(err).Error("Error converting nostr privkey to pubkey")
return
}
resp := &nostr.Event{
PubKey: svc.keys.GetNostrPublicKey(),
PubKey: appWalletPubKey,
CreatedAt: nostr.Now(),
Kind: models.RESPONSE_KIND,
Tags: allTags,
Content: msg,
}
err = resp.Sign(svc.keys.GetNostrSecretKey())
err = resp.Sign(appWalletPrivKey)
if err != nil {
return nil, err
}

View file

@ -0,0 +1,192 @@
package nip47
import (
"context"
"encoding/json"
"slices"
"testing"
"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/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip04"
"github.com/stretchr/testify/assert"
)
func TestHandleResponse_LegacyApp_WithPermission(t *testing.T) {
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
assert.NoError(t, err)
nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher)
reqPrivateKey := nostr.GeneratePrivateKey()
reqPubkey, err := nostr.GetPublicKey(reqPrivateKey)
assert.NoError(t, err)
app, ss, err := tests.CreateLegacyApp(svc, reqPrivateKey)
assert.NoError(t, err)
appPermission := &db.AppPermission{
AppId: app.ID,
App: *app,
Scope: constants.GET_BALANCE_SCOPE,
}
err = svc.DB.Create(appPermission).Error
assert.NoError(t, err)
content := map[string]interface{}{
"method": models.GET_INFO_METHOD,
}
payloadBytes, err := json.Marshal(content)
assert.NoError(t, err)
msg, err := nip04.Encrypt(string(payloadBytes), ss)
assert.NoError(t, err)
reqEvent := &nostr.Event{
Kind: models.REQUEST_KIND,
PubKey: reqPubkey,
CreatedAt: nostr.Now(),
Tags: nostr.Tags{},
Content: msg,
}
err = reqEvent.Sign(reqPrivateKey)
assert.NoError(t, err)
relay := tests.NewMockRelay()
nip47svc.HandleEvent(context.TODO(), relay, reqEvent, svc.LNClient)
assert.NotNil(t, relay.PublishedEvent)
assert.NotEmpty(t, relay.PublishedEvent.Content)
decrypted, err := nip04.Decrypt(relay.PublishedEvent.Content, ss)
assert.NoError(t, err)
type getInfoResult struct {
Methods []string `json:"methods"`
}
type getInfoResponseWrapper struct {
models.Response
Result getInfoResult `json:"result"`
}
unmarshalledResponse := getInfoResponseWrapper{}
err = json.Unmarshal([]byte(decrypted), &unmarshalledResponse)
assert.NoError(t, err)
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)
}
func TestHandleResponse_LegacyApp_NoPermission(t *testing.T) {
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
assert.NoError(t, err)
nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher)
reqPrivateKey := nostr.GeneratePrivateKey()
reqPubkey, err := nostr.GetPublicKey(reqPrivateKey)
assert.NoError(t, err)
_, ss, err := tests.CreateLegacyApp(svc, reqPrivateKey)
assert.NoError(t, err)
content := map[string]interface{}{
"method": models.GET_BALANCE_METHOD,
}
payloadBytes, err := json.Marshal(content)
assert.NoError(t, err)
msg, err := nip04.Encrypt(string(payloadBytes), ss)
assert.NoError(t, err)
reqEvent := &nostr.Event{
Kind: models.REQUEST_KIND,
PubKey: reqPubkey,
CreatedAt: nostr.Now(),
Tags: nostr.Tags{},
Content: msg,
}
err = reqEvent.Sign(reqPrivateKey)
assert.NoError(t, err)
relay := tests.NewMockRelay()
nip47svc.HandleEvent(context.TODO(), relay, reqEvent, svc.LNClient)
assert.NotNil(t, relay.PublishedEvent)
assert.NotEmpty(t, relay.PublishedEvent.Content)
decrypted, err := nip04.Decrypt(relay.PublishedEvent.Content, ss)
assert.NoError(t, err)
unmarshalledResponse := models.Response{}
err = json.Unmarshal([]byte(decrypted), &unmarshalledResponse)
assert.NoError(t, err)
assert.Nil(t, unmarshalledResponse.Result)
assert.Equal(t, models.GET_BALANCE_METHOD, unmarshalledResponse.ResultType)
assert.Equal(t, "RESTRICTED", unmarshalledResponse.Error.Code)
assert.Equal(t, "This app does not have the get_balance scope", unmarshalledResponse.Error.Message)
}
func TestHandleResponse_LegacyApp_IncorrectPubkey(t *testing.T) {
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
assert.NoError(t, err)
nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher)
reqPrivateKey := nostr.GeneratePrivateKey()
reqPubkey, err := nostr.GetPublicKey(reqPrivateKey)
assert.NoError(t, err)
reqPrivateKey2 := nostr.GeneratePrivateKey()
app, ss, err := tests.CreateLegacyApp(svc, reqPrivateKey)
assert.NoError(t, err)
appPermission := &db.AppPermission{
AppId: app.ID,
App: *app,
Scope: constants.GET_BALANCE_SCOPE,
}
err = svc.DB.Create(appPermission).Error
assert.NoError(t, err)
content := map[string]interface{}{
"method": models.GET_BALANCE_METHOD,
}
payloadBytes, err := json.Marshal(content)
assert.NoError(t, err)
msg, err := nip04.Encrypt(string(payloadBytes), ss)
assert.NoError(t, err)
reqEvent := &nostr.Event{
Kind: models.REQUEST_KIND,
CreatedAt: nostr.Now(),
Tags: nostr.Tags{},
Content: msg,
}
err = reqEvent.Sign(reqPrivateKey2)
assert.NoError(t, err)
// set a different pubkey (this will not pass validation)
reqEvent.PubKey = reqPubkey
relay := tests.NewMockRelay()
nip47svc.HandleEvent(context.TODO(), relay, reqEvent, svc.LNClient)
assert.Nil(t, relay.PublishedEvent)
}

View file

@ -54,7 +54,7 @@ func TestCreateResponse(t *testing.T) {
nip47svc := NewNip47Service(svc.DB, svc.Cfg, svc.Keys, svc.EventPublisher)
res, err := nip47svc.CreateResponse(reqEvent, nip47Response, nostr.Tags{}, ss)
res, err := nip47svc.CreateResponse(reqEvent, nip47Response, nostr.Tags{}, ss, svc.Keys.GetNostrSecretKey())
assert.NoError(t, err)
assert.Equal(t, reqPubkey, res.Tags.GetFirst([]string{"p"}).Value())
assert.Equal(t, reqEvent.ID, res.Tags.GetFirst([]string{"e"}).Value())
@ -121,9 +121,6 @@ func TestHandleResponse_WithPermission(t *testing.T) {
assert.NotNil(t, relay.PublishedEvent)
assert.NotEmpty(t, relay.PublishedEvent.Content)
ss, err = nip04.ComputeSharedSecret(svc.Keys.GetNostrPublicKey(), reqPrivateKey)
assert.NoError(t, err)
decrypted, err := nip04.Decrypt(relay.PublishedEvent.Content, ss)
assert.NoError(t, err)
@ -242,9 +239,6 @@ func TestHandleResponse_NoPermission(t *testing.T) {
assert.NotNil(t, relay.PublishedEvent)
assert.NotEmpty(t, relay.PublishedEvent.Content)
ss, err = nip04.ComputeSharedSecret(svc.Keys.GetNostrPublicKey(), reqPrivateKey)
assert.NoError(t, err)
decrypted, err := nip04.Decrypt(relay.PublishedEvent.Content, ss)
assert.NoError(t, err)
@ -299,23 +293,8 @@ func TestHandleResponse_NoApp(t *testing.T) {
nip47svc.HandleEvent(context.TODO(), relay, reqEvent, svc.LNClient)
assert.NotNil(t, relay.PublishedEvent)
assert.NotEmpty(t, relay.PublishedEvent.Content)
ss, err = nip04.ComputeSharedSecret(svc.Keys.GetNostrPublicKey(), reqPrivateKey)
assert.NoError(t, err)
decrypted, err := nip04.Decrypt(relay.PublishedEvent.Content, ss)
assert.NoError(t, err)
unmarshalledResponse := models.Response{}
err = json.Unmarshal([]byte(decrypted), &unmarshalledResponse)
assert.NoError(t, err)
assert.Nil(t, unmarshalledResponse.Result)
assert.Equal(t, "", unmarshalledResponse.ResultType)
assert.Equal(t, "UNAUTHORIZED", unmarshalledResponse.Error.Code)
assert.Equal(t, "The public key does not have a wallet connected.", unmarshalledResponse.Error.Message)
// it shouldn't return anything for an invalid app key
assert.Nil(t, relay.PublishedEvent)
}
func TestHandleResponse_IncorrectPubkey(t *testing.T) {
@ -330,7 +309,7 @@ func TestHandleResponse_IncorrectPubkey(t *testing.T) {
reqPrivateKey2 := nostr.GeneratePrivateKey()
app, _, err := tests.CreateAppWithPrivateKey(svc, reqPrivateKey)
app, ss, err := tests.CreateAppWithPrivateKey(svc, reqPrivateKey)
assert.NoError(t, err)
appPermission := &db.AppPermission{
@ -341,9 +320,6 @@ func TestHandleResponse_IncorrectPubkey(t *testing.T) {
err = svc.DB.Create(appPermission).Error
assert.NoError(t, err)
_, ss, err := tests.CreateAppWithPrivateKey(svc, reqPrivateKey2)
assert.NoError(t, err)
content := map[string]interface{}{
"method": models.GET_BALANCE_METHOD,
}

View file

@ -2,7 +2,6 @@ package nip47
import (
"context"
"github.com/getAlby/hub/config"
"github.com/getAlby/hub/events"
"github.com/getAlby/hub/lnclient"
@ -29,8 +28,10 @@ type Nip47Service interface {
events.EventSubscriber
StartNotifier(ctx context.Context, relay *nostr.Relay, lnClient lnclient.LNClient)
HandleEvent(ctx context.Context, relay nostrmodels.Relay, event *nostr.Event, lnClient lnclient.LNClient)
PublishNip47Info(ctx context.Context, relay nostrmodels.Relay, lnClient lnclient.LNClient) error
CreateResponse(initialEvent *nostr.Event, content interface{}, tags nostr.Tags, ss []byte) (result *nostr.Event, err error)
GetNip47Info(ctx context.Context, relay *nostr.Relay, appWalletPubKey string) (*nostr.Event, error)
PublishNip47Info(ctx context.Context, relay nostrmodels.Relay, appWalletPubKey string, appWalletPrivKey string, lnClient lnclient.LNClient) (*nostr.Event, error)
PublishNip47InfoDeletion(ctx context.Context, relay nostrmodels.Relay, appWalletPubKey string, appWalletPrivKey string, infoEventId string) error
CreateResponse(initialEvent *nostr.Event, content interface{}, tags nostr.Tags, ss []byte, walletPrivKey string) (result *nostr.Event, err error)
}
func NewNip47Service(db *gorm.DB, cfg config.Config, keys keys.Keys, eventPublisher events.EventPublisher) *nip47Service {

View file

@ -108,7 +108,21 @@ func (notifier *Nip47Notifier) notifySubscriber(ctx context.Context, app *db.App
"appId": app.ID,
}).Debug("Notifying subscriber")
ss, err := nip04.ComputeSharedSecret(app.NostrPubkey, notifier.keys.GetNostrSecretKey())
var err error
appWalletPrivKey := notifier.keys.GetNostrSecretKey()
if app.WalletPubkey != nil {
appWalletPrivKey, err = notifier.keys.GetAppWalletKey(app.ID)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"notification": notification,
"appId": app.ID,
}).WithError(err).Error("error deriving child key")
return
}
}
ss, err := nip04.ComputeSharedSecret(app.AppPubkey, appWalletPrivKey)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"notification": notification,
@ -134,17 +148,26 @@ func (notifier *Nip47Notifier) notifySubscriber(ctx context.Context, app *db.App
return
}
allTags := nostr.Tags{[]string{"p", app.NostrPubkey}}
allTags := nostr.Tags{[]string{"p", app.AppPubkey}}
allTags = append(allTags, tags...)
appWalletPubKey, err := nostr.GetPublicKey(appWalletPrivKey)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"notification": notification,
"appId": app.ID,
}).WithError(err).Error("Failed to calculate app wallet pub key")
return
}
event := &nostr.Event{
PubKey: notifier.keys.GetNostrPublicKey(),
PubKey: appWalletPubKey,
CreatedAt: nostr.Now(),
Kind: models.NOTIFICATION_KIND,
Tags: allTags,
Content: msg,
}
err = event.Sign(notifier.keys.GetNostrSecretKey())
err = event.Sign(appWalletPrivKey)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"notification": notification,

View file

@ -3,6 +3,7 @@ package notifications
import (
"context"
"encoding/json"
"github.com/nbd-wtf/go-nostr"
"testing"
"time"
@ -32,21 +33,15 @@ func (svc *mockConsumer) ConsumeEvent(ctx context.Context, event *events.Event,
svc.nip47NotificationQueue.AddToQueue(event)
}
func TestSendNotification_PaymentReceived(t *testing.T) {
func doTestSendNotificationPaymentReceived(t *testing.T, svc *tests.TestService, app *db.App, ss []byte) {
ctx := context.TODO()
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
require.NoError(t, err)
app, ss, err := tests.CreateApp(svc)
assert.NoError(t, err)
appPermission := &db.AppPermission{
AppId: app.ID,
App: *app,
Scope: constants.NOTIFICATIONS_SCOPE,
}
err = svc.DB.Create(appPermission).Error
err := svc.DB.Create(appPermission).Error
assert.NoError(t, err)
settledAt := time.Unix(*tests.MockLNClientTransaction.SettledAt, 0)
@ -110,23 +105,37 @@ func TestSendNotification_PaymentReceived(t *testing.T) {
assert.Equal(t, tests.MockLNClientTransaction.Amount, transaction.Amount)
assert.Equal(t, tests.MockLNClientTransaction.FeesPaid, transaction.FeesPaid)
assert.Equal(t, tests.MockLNClientTransaction.SettledAt, transaction.SettledAt)
}
func TestSendNotification_PaymentSent(t *testing.T) {
ctx := context.TODO()
func TestSendNotification_PaymentReceived(t *testing.T) {
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
require.NoError(t, err)
app, ss, err := tests.CreateApp(svc)
assert.NoError(t, err)
doTestSendNotificationPaymentReceived(t, svc, app, ss)
}
func TestSendNotification_Legacy_PaymentReceived(t *testing.T) {
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
require.NoError(t, err)
app, ss, err := tests.CreateLegacyApp(svc, nostr.GeneratePrivateKey())
assert.NoError(t, err)
doTestSendNotificationPaymentReceived(t, svc, app, ss)
}
func doTestSendNotificationPaymentSent(t *testing.T, svc *tests.TestService, app *db.App, ss []byte) {
ctx := context.TODO()
appPermission := &db.AppPermission{
AppId: app.ID,
App: *app,
Scope: constants.NOTIFICATIONS_SCOPE,
}
err = svc.DB.Create(appPermission).Error
err := svc.DB.Create(appPermission).Error
assert.NoError(t, err)
settledAt := time.Unix(*tests.MockLNClientTransaction.SettledAt, 0)
@ -191,13 +200,28 @@ func TestSendNotification_PaymentSent(t *testing.T) {
assert.Equal(t, tests.MockLNClientTransaction.SettledAt, transaction.SettledAt)
}
func TestSendNotificationNoPermission(t *testing.T) {
ctx := context.TODO()
func TestSendNotification_PaymentSent(t *testing.T) {
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
require.NoError(t, err)
_, _, err = tests.CreateApp(svc)
app, ss, err := tests.CreateApp(svc)
assert.NoError(t, err)
doTestSendNotificationPaymentSent(t, svc, app, ss)
}
func TestSendNotification_Legacy_PaymentSent(t *testing.T) {
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
require.NoError(t, err)
app, ss, err := tests.CreateLegacyApp(svc, nostr.GeneratePrivateKey())
assert.NoError(t, err)
doTestSendNotificationPaymentSent(t, svc, app, ss)
}
func doTestSendNotificationNoPermission(t *testing.T, svc *tests.TestService) {
ctx := context.TODO()
svc.DB.Create(&db.Transaction{
PaymentHash: tests.MockPaymentHash,
@ -228,3 +252,20 @@ func TestSendNotificationNoPermission(t *testing.T) {
assert.Nil(t, relay.PublishedEvent)
}
func TestSendNotification_NoPermission(t *testing.T) {
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
require.NoError(t, err)
_, _, err = tests.CreateApp(svc)
assert.NoError(t, err)
doTestSendNotificationNoPermission(t, svc)
}
func TestSendNotification_Legacy_NoPermission(t *testing.T) {
defer tests.RemoveTestService()
svc, err := tests.CreateTestService()
require.NoError(t, err)
_, _, err = tests.CreateLegacyApp(svc, nostr.GeneratePrivateKey())
assert.NoError(t, err)
doTestSendNotificationNoPermission(t, svc)
}

View file

@ -51,7 +51,7 @@ func (svc *permissionsService) HasPermission(app *db.App, scope string) (result
"scope": scope,
"expiresAt": expiresAt.Unix(),
"appId": app.ID,
"pubkey": app.NostrPubkey,
"pubkey": app.AppPubkey,
}).Info("This pubkey is expired")
return false, constants.ERROR_EXPIRED, "This app has expired"

View file

@ -3,6 +3,7 @@ package nip47
import (
"context"
"fmt"
"strconv"
"strings"
"github.com/getAlby/hub/lnclient"
@ -11,7 +12,27 @@ import (
"github.com/nbd-wtf/go-nostr"
)
func (svc *nip47Service) PublishNip47Info(ctx context.Context, relay nostrmodels.Relay, lnClient lnclient.LNClient) error {
func (svc *nip47Service) GetNip47Info(ctx context.Context, relay *nostr.Relay, appWalletPubKey string) (*nostr.Event, error) {
filter := nostr.Filter{
Kinds: []int{models.INFO_EVENT_KIND},
Authors: []string{appWalletPubKey},
Limit: 1,
}
events, err := relay.QuerySync(ctx, filter)
if err != nil {
return nil, err
}
if len(events) == 0 {
return nil, nil
}
return events[0], nil
}
func (svc *nip47Service) PublishNip47Info(ctx context.Context, relay nostrmodels.Relay, appWalletPubKey string, appWalletPrivKey string, lnClient lnclient.LNClient) (*nostr.Event, error) {
// TODO: should the capabilities be based on the app permissions? (for non-legacy apps)
capabilities := lnClient.GetSupportedNIP47Methods()
if len(lnClient.GetSupportedNIP47NotificationTypes()) > 0 {
capabilities = append(capabilities, "notifications")
@ -21,9 +42,27 @@ func (svc *nip47Service) PublishNip47Info(ctx context.Context, relay nostrmodels
ev.Kind = models.INFO_EVENT_KIND
ev.Content = strings.Join(capabilities, " ")
ev.CreatedAt = nostr.Now()
ev.PubKey = svc.keys.GetNostrPublicKey()
ev.PubKey = appWalletPubKey
ev.Tags = nostr.Tags{[]string{"notifications", strings.Join(lnClient.GetSupportedNIP47NotificationTypes(), " ")}}
err := ev.Sign(svc.keys.GetNostrSecretKey())
err := ev.Sign(appWalletPrivKey)
if err != nil {
return nil, err
}
err = relay.Publish(ctx, *ev)
if err != nil {
return nil, fmt.Errorf("nostr publish not successful: %s", err)
}
return ev, nil
}
func (svc *nip47Service) PublishNip47InfoDeletion(ctx context.Context, relay nostrmodels.Relay, appWalletPubKey string, appWalletPrivKey string, infoEventId string) error {
ev := &nostr.Event{}
ev.Kind = nostr.KindDeletion
ev.Content = "deleting nip47 info since app connection for this key was deleted"
ev.Tags = nostr.Tags{[]string{"e", infoEventId}, []string{"k", strconv.Itoa(models.INFO_EVENT_KIND)}}
ev.CreatedAt = nostr.Now()
ev.PubKey = appWalletPubKey
err := ev.Sign(appWalletPrivKey)
if err != nil {
return err
}

View file

@ -0,0 +1,58 @@
package service
import (
"context"
"github.com/getAlby/hub/events"
"github.com/getAlby/hub/logger"
"github.com/nbd-wtf/go-nostr"
"github.com/sirupsen/logrus"
)
type createAppConsumer struct {
events.EventSubscriber
svc *service
relay *nostr.Relay
}
// When a new app is created, subscribe to it on the relay
func (s *createAppConsumer) ConsumeEvent(ctx context.Context, event *events.Event, globalProperties map[string]interface{}) {
if event.Event != "app_created" {
return
}
properties, ok := event.Properties.(map[string]interface{})
if !ok {
logger.Logger.WithField("event", event).Error("Failed to cast event.Properties to map")
return
}
id, ok := properties["id"].(uint)
if !ok {
logger.Logger.WithField("event", event).Error("Failed to get app id")
return
}
walletPrivKey, err := s.svc.keys.GetAppWalletKey(id)
if err != nil {
logger.Logger.WithError(err).Error("Failed to calculate app wallet priv key")
return
}
walletPubKey, err := nostr.GetPublicKey(walletPrivKey)
if err != nil {
logger.Logger.WithError(err).Error("Failed to calculate app wallet pub key")
return
}
go func() {
_, err := s.svc.GetNip47Service().PublishNip47Info(ctx, s.relay, walletPubKey, walletPrivKey, s.svc.lnClient)
if err != nil {
logger.Logger.WithError(err).Error("Could not publish NIP47 info")
}
err = s.svc.startAppWalletSubscription(ctx, s.relay, walletPubKey)
if err != nil {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"app_id": id}).Error("Failed to subscribe to wallet")
}
logger.Logger.WithFields(logrus.Fields{
"app_id": id}).Info("App Nostr Subscription ended")
}()
}

View file

@ -0,0 +1,67 @@
package service
import (
"context"
"github.com/getAlby/hub/events"
"github.com/getAlby/hub/logger"
"github.com/nbd-wtf/go-nostr"
)
type deleteAppConsumer struct {
events.EventSubscriber
walletPubkey string
relay *nostr.Relay
nostrSubscription *nostr.Subscription
svc *service
}
// When an app is deleted, unsubscribe from events for that app on the relay
// and publish a deletion event for that app's info event
func (s *deleteAppConsumer) ConsumeEvent(ctx context.Context, event *events.Event, globalProperties map[string]interface{}) {
if event.Event != "app_deleted" {
return
}
properties, ok := event.Properties.(map[string]interface{})
if !ok {
logger.Logger.WithField("event", event).Error("Failed to cast event.Properties to map")
return
}
id, ok := properties["id"].(uint)
if !ok {
logger.Logger.WithField("event", event).Error("missing id in properties event")
return
}
walletPrivKey, err := s.svc.keys.GetAppWalletKey(id)
if err != nil {
logger.Logger.WithError(err).WithField("id", id).Error("Failed to calculate app wallet priv key")
return
}
walletPubKey, err := nostr.GetPublicKey(walletPrivKey)
if err != nil {
logger.Logger.WithError(err).WithField("id", id).Error("Failed to calculate app wallet pub key")
return
}
// Note: for legacy apps this check will always return false as the wallet pubkey
// generated by the id will not match the master key which is used for all legacy apps
if s.walletPubkey == walletPubKey {
s.nostrSubscription.Unsub()
// remove this consumer as subscriber in eventPublisher
s.svc.eventPublisher.RemoveSubscriber(s)
// get nip47 event info for this app wallet key
nip47InfoEvent, err := s.svc.GetNip47Service().GetNip47Info(ctx, s.relay, s.walletPubkey)
if err != nil {
logger.Logger.WithError(err).Error("Could not get nip47 info event")
return
}
if nip47InfoEvent != nil {
err = s.svc.nip47Service.PublishNip47InfoDeletion(ctx, s.relay, walletPubKey, walletPrivKey, nip47InfoEvent.ID)
if err != nil {
logger.Logger.WithError(err).WithField("event", event).Error("Failed to publish nip47 info deletion")
}
}
}
}

View file

@ -1,8 +1,11 @@
package keys
import (
"encoding/hex"
"errors"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/getAlby/hub/config"
"github.com/getAlby/hub/logger"
"github.com/nbd-wtf/go-nostr"
@ -16,6 +19,8 @@ type Keys interface {
GetNostrPublicKey() string
// Wallet Service Nostr secret key (DEPRECATED)
GetNostrSecretKey() string
// Derives a BIP32 child key from appKey derived child dedicated for app wallet keys
GetAppWalletKey(childIndex uint) (string, error)
// Derives a child BIP-32 key from the app key (derived from the mnemonic)
DeriveKey(path []uint32) (*bip32.Key, error)
}
@ -55,22 +60,39 @@ func (keys *keys) Init(cfg config.Config, encryptionKey string) error {
return err
}
if mnemonic != "" {
masterKey, err := bip32.NewMasterKey(bip39.NewSeed(mnemonic, ""))
if mnemonic == "" {
// for backends that don't use a mnemonic, create one anyway for deriving keys
entropy, err := bip39.NewEntropy(128)
if err != nil {
logger.Logger.WithError(err).Error("Failed to create seed from mnemonic")
logger.Logger.WithError(err).Error("Failed to generate entropy for mnemonic")
return err
}
albyHubIndex := uint32(bip32.FirstHardenedChild + 128029 /* 🐝 */)
appKey, err := masterKey.NewChildKey(albyHubIndex)
mnemonic, err = bip39.NewMnemonic(entropy)
if err != nil {
logger.Logger.WithError(err).Error("Failed to create seed from mnemonic")
logger.Logger.WithError(err).Error("Failed to generate mnemonic")
return err
}
err = cfg.SetUpdate("Mnemonic", mnemonic, encryptionKey)
if err != nil {
logger.Logger.WithError(err).Error("Failed to save mnemonic")
return err
}
keys.appKey = appKey
}
masterKey, err := bip32.NewMasterKey(bip39.NewSeed(mnemonic, ""))
if err != nil {
logger.Logger.WithError(err).Error("Failed to create seed from mnemonic")
return err
}
albyHubIndex := uint32(bip32.FirstHardenedChild + 128029 /* 🐝 */)
appKey, err := masterKey.NewChildKey(albyHubIndex)
if err != nil {
logger.Logger.WithError(err).Error("Failed to create seed from mnemonic")
return err
}
keys.appKey = appKey
return nil
}
@ -82,6 +104,16 @@ func (keys *keys) GetNostrSecretKey() string {
return keys.nostrSecretKey
}
func (keys *keys) GetAppWalletKey(appID uint) (string, error) {
path := []uint32{bip32.FirstHardenedChild + 1, bip32.FirstHardenedChild + uint32(appID)}
key, err := keys.DeriveKey(path)
if err != nil {
return "", err
}
childPrivKey, _ := btcec.PrivKeyFromBytes(key.Key)
return hex.EncodeToString(childPrivKey.Serialize()), nil
}
func (keys *keys) DeriveKey(path []uint32) (*bip32.Key, error) {
if keys.appKey == nil {
return nil, errors.New("app key not set")

104
service/keys/keys_test.go Normal file
View file

@ -0,0 +1,104 @@
package keys
import (
"strings"
"testing"
"github.com/getAlby/hub/config"
"github.com/getAlby/hub/db"
"github.com/nbd-wtf/go-nostr"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tyler-smith/go-bip32"
"github.com/tyler-smith/go-bip39"
)
const testDB = "test.db"
func TestUseExistingMnemonic(t *testing.T) {
gormDb, err := db.NewDB(testDB, true)
require.NoError(t, err)
mnemonic := "thought turkey ask pottery head say catalog desk pledge elbow naive mimic"
unlockPassword := "123"
config, err := config.NewConfig(&config.AppConfig{}, gormDb)
require.NoError(t, err)
config.SetUpdate("Mnemonic", mnemonic, unlockPassword)
keys := NewKeys()
err = keys.Init(config, unlockPassword)
require.NoError(t, err)
mnemonicFromConfig, err := config.Get("Mnemonic", unlockPassword)
require.NoError(t, err)
require.Equal(t, mnemonic, mnemonicFromConfig)
// ensure backup key uses correct derivation path
derivedKeyFromKeys, err := keys.DeriveKey([]uint32{bip32.FirstHardenedChild})
require.NoError(t, err)
masterKey, err := bip32.NewMasterKey(bip39.NewSeed(mnemonic, ""))
assert.NoError(t, err)
appKey, err := masterKey.NewChildKey(bip32.FirstHardenedChild + 128029 /* 🐝 */)
assert.NoError(t, err)
encryptedChannelsBackupKey, err := appKey.NewChildKey(bip32.FirstHardenedChild)
assert.NoError(t, err)
assert.Equal(t, encryptedChannelsBackupKey.String(), derivedKeyFromKeys.String())
// get a wallet key for app ID 2, expect it is derived correctly
appWalletPrivateKey, err := keys.GetAppWalletKey(2)
require.NoError(t, err)
appWalletPubkey, err := nostr.GetPublicKey(appWalletPrivateKey)
require.NoError(t, err)
assert.Equal(t, "dd9e304d24f29f3481d5cf18a76c85ca3e95931aee3c997a27f267e975e72976", appWalletPubkey)
}
func TestGenerateNewMnemonic(t *testing.T) {
gormDb, err := db.NewDB(testDB, true)
require.NoError(t, err)
unlockPassword := "123"
config, err := config.NewConfig(&config.AppConfig{}, gormDb)
require.NoError(t, err)
keys := NewKeys()
err = keys.Init(config, unlockPassword)
require.NoError(t, err)
mnemonicFromConfig, err := config.Get("Mnemonic", unlockPassword)
require.NoError(t, err)
// expect a new 12-word mnemonic to be saved
assert.Equal(t, 12, len(strings.Split(mnemonicFromConfig, " ")))
// re-create keys, ensure same mnemonic is used
keys = NewKeys()
err = keys.Init(config, unlockPassword)
require.NoError(t, err)
mnemonicFromConfig2, err := config.Get("Mnemonic", unlockPassword)
require.NoError(t, err)
assert.Equal(t, mnemonicFromConfig, mnemonicFromConfig2)
// check derivation
derivedKeyFromKeys, err := keys.DeriveKey([]uint32{bip32.FirstHardenedChild})
require.NoError(t, err)
masterKey, err := bip32.NewMasterKey(bip39.NewSeed(mnemonicFromConfig, ""))
assert.NoError(t, err)
appKey, err := masterKey.NewChildKey(bip32.FirstHardenedChild + 128029 /* 🐝 */)
assert.NoError(t, err)
encryptedChannelsBackupKey, err := appKey.NewChildKey(bip32.FirstHardenedChild)
assert.NoError(t, err)
assert.Equal(t, encryptedChannelsBackupKey.String(), derivedKeyFromKeys.String())
}

View file

@ -130,7 +130,10 @@ func NewService(ctx context.Context) (*service, error) {
}
if appConfig.AutoUnlockPassword != "" {
svc.StartApp(appConfig.AutoUnlockPassword)
nodeLastStartTime, _ := cfg.Get("NodeLastStartTime", "")
if nodeLastStartTime != "" {
svc.StartApp(appConfig.AutoUnlockPassword)
}
}
return svc, nil
@ -148,31 +151,6 @@ func (svc *service) noticeHandler(notice string) {
logger.Logger.Infof("Received a notice %s", notice)
}
func (svc *service) StartSubscription(ctx context.Context, sub *nostr.Subscription) error {
svc.nip47Service.StartNotifier(ctx, sub.Relay, svc.lnClient)
go func() {
// block till EOS is received
<-sub.EndOfStoredEvents
logger.Logger.Debug("Received EOS")
// loop through incoming events
for event := range sub.Events {
go svc.nip47Service.HandleEvent(ctx, sub.Relay, event, svc.lnClient)
}
logger.Logger.Debug("Relay subscription events channel ended")
}()
<-ctx.Done()
if sub.Relay.ConnectionError != nil {
logger.Logger.WithField("connectionError", sub.Relay.ConnectionError).Error("Relay error")
return sub.Relay.ConnectionError
}
logger.Logger.Info("Exiting subscription...")
return nil
}
func finishRestoreNode(workDir string) error {
restoreDir := filepath.Join(workDir, "restore")
if restoreDirStat, err := os.Stat(restoreDir); err == nil && restoreDirStat.IsDir() {

View file

@ -8,6 +8,8 @@ import (
"strconv"
"time"
"github.com/getAlby/hub/db"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip19"
"github.com/sirupsen/logrus"
@ -45,7 +47,7 @@ func (svc *service) startNostr(ctx context.Context) error {
//Start infinite loop which will be only broken by canceling ctx (SIGINT)
var relay *nostr.Relay
waitToReconnectSeconds := 0
var createAppEventListener events.EventSubscriber
for i := 0; ; i++ {
// wait for a delay if any before retrying
@ -83,25 +85,56 @@ func (svc *service) startNostr(ctx context.Context) error {
}).WithError(err).Error("Failed to connect to relay")
continue
}
logger.Logger.WithFields(logrus.Fields{
"relay_url": relayUrl,
}).Info("Connected to the relay")
waitToReconnectSeconds = 0
//publish event with NIP-47 info
err = svc.nip47Service.PublishNip47Info(ctx, relay, svc.lnClient)
if err != nil {
logger.Logger.WithError(err).Error("Could not publish NIP47 info")
// register a subscriber for events of "app_created" which handles creation of nostr subscription for new app
if createAppEventListener != nil {
svc.eventPublisher.RemoveSubscriber(createAppEventListener)
}
createAppEventListener = &createAppConsumer{svc: svc, relay: relay}
svc.eventPublisher.RegisterSubscriber(createAppEventListener)
logger.Logger.Info("Subscribing to events")
sub, err := relay.Subscribe(ctx, svc.createFilters(svc.keys.GetNostrPublicKey()))
if err != nil {
logger.Logger.WithError(err).Error("Failed to subscribe to events")
continue
// start each app wallet subscription which have a child derived wallet key
svc.startAllExistingAppsWalletSubscriptions(ctx, relay)
// check if there are still legacy apps in DB
var legacyAppCount int64
result := svc.db.Model(&db.App{}).Where("wallet_pubkey IS NULL").Count(&legacyAppCount)
if result.Error != nil {
logger.Logger.WithError(result.Error).Error("Failed to count Legacy Apps")
return
}
err = svc.StartSubscription(sub.Context, sub)
if err != nil {
if legacyAppCount > 0 {
// re-publish single NIP47 event info for legacy apps
_, err := svc.GetNip47Service().PublishNip47Info(ctx, relay, svc.keys.GetNostrPublicKey(), svc.keys.GetNostrSecretKey(), svc.lnClient)
if err != nil {
logger.Logger.WithError(err).Error("Could not publish NIP47 info for legacy apps")
continue
}
logger.Logger.WithField("legacy_app_count", legacyAppCount).Info("Starting legacy app subscription")
// legacy single wallet subscription - only subscribe once for all legacy apps
// to ensure we do not get duplicate events
err = svc.startAppWalletSubscription(ctx, relay, svc.keys.GetNostrPublicKey())
if err != nil {
//err being non-nil means that we have an error on the websocket error channel. In this case we just try to reconnect.
logger.Logger.WithError(err).Error("Got an error from the relay while listening to subscription.")
continue
}
break
}
select {
case <-ctx.Done():
logger.Logger.Info("Main context cancelled, exiting...")
case <-relay.Context().Done():
//err being non-nil means that we have an error on the websocket error channel. In this case we just try to reconnect.
logger.Logger.WithError(err).Error("Got an error from the relay while listening to subscription.")
if relay.ConnectionError != nil {
logger.Logger.WithError(relay.ConnectionError).Error("Got an error from the relay, trying to reconnect")
} else {
logger.Logger.Error("Relay context cancelled, but no connection error...trying to reconnect")
}
continue
}
//err being nil means that the context was canceled and we should exit the program.
@ -113,6 +146,73 @@ func (svc *service) startNostr(ctx context.Context) error {
return nil
}
func (svc *service) startAllExistingAppsWalletSubscriptions(ctx context.Context, relay *nostr.Relay) {
var apps []db.App
result := svc.db.Where("wallet_pubkey IS NOT NULL").Find(&apps)
if result.Error != nil {
logger.Logger.WithError(result.Error).Error("Failed to fetch App records with non-empty WalletPubkey")
return
}
for _, app := range apps {
go func(app db.App) {
err := svc.startAppWalletSubscription(ctx, relay, *app.WalletPubkey)
if err != nil {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"app_id": app.ID}).Error("Subscription error")
return
}
}(app)
}
}
func (svc *service) startAppWalletSubscription(ctx context.Context, relay *nostr.Relay, appWalletPubKey string) error {
logger.Logger.Info("Subscribing to events for wallet ", appWalletPubKey)
sub, err := relay.Subscribe(ctx, svc.createFilters(appWalletPubKey))
if err != nil {
logger.Logger.WithError(err).Error("Failed to subscribe to events")
return err
}
// register a subscriber for "app_deleted" events, which handles nostr subscription cancel and nip47 info event deletion
deleteEventSubscriber := deleteAppConsumer{nostrSubscription: sub, walletPubkey: appWalletPubKey, svc: svc, relay: relay}
svc.eventPublisher.RegisterSubscriber(&deleteEventSubscriber)
err = svc.StartSubscription(sub.Context, sub)
svc.eventPublisher.RemoveSubscriber(&deleteEventSubscriber)
if err != nil {
logger.Logger.WithError(err).Error("Got an error from the relay while listening to subscription.")
return err
}
return nil
}
func (svc *service) StartSubscription(ctx context.Context, sub *nostr.Subscription) error {
svc.nip47Service.StartNotifier(ctx, sub.Relay, svc.lnClient)
go func() {
// block till EOS is received
<-sub.EndOfStoredEvents
logger.Logger.Debug("Received EOS")
// loop through incoming events
for event := range sub.Events {
go svc.nip47Service.HandleEvent(ctx, sub.Relay, event, svc.lnClient)
}
logger.Logger.Debug("Relay subscription events channel ended")
}()
<-ctx.Done()
if sub.Relay.ConnectionError != nil {
logger.Logger.WithField("connectionError", sub.Relay.ConnectionError).Error("Relay error")
return sub.Relay.ConnectionError
}
logger.Logger.Info("Exiting subscription...")
return nil
}
func (svc *service) StartApp(encryptionKey string) error {
if svc.lnClient != nil {
return errors.New("app already started")

View file

@ -1,32 +1,69 @@
package tests
import (
"github.com/getAlby/hub/db"
db "github.com/getAlby/hub/db"
"github.com/getAlby/hub/events"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip04"
"gorm.io/gorm"
"time"
)
func CreateApp(svc *TestService) (app *db.App, ss []byte, err error) {
senderPrivkey := nostr.GeneratePrivateKey()
return CreateAppWithPrivateKey(svc, senderPrivkey)
return CreateAppWithPrivateKey(svc, "")
}
func CreateAppWithPrivateKey(svc *TestService, senderPrivkey string) (app *db.App, ss []byte, err error) {
senderPubkey, err := nostr.GetPublicKey(senderPrivkey)
if err != nil {
return nil, nil, err
senderPubkey := ""
if senderPrivkey != "" {
var err error
senderPubkey, err = nostr.GetPublicKey(senderPrivkey)
if err != nil {
return nil, nil, err
}
}
ss, err = nip04.ComputeSharedSecret(svc.Keys.GetNostrPublicKey(), senderPrivkey)
if err != nil {
return nil, nil, err
var expiresAt *time.Time
app, pairingSecretKey, err := svc.AppsService.CreateApp("test", senderPubkey, 0, "monthly", expiresAt, nil, false, nil)
if pairingSecretKey == "" {
pairingSecretKey = senderPrivkey
}
app = &db.App{Name: "test", NostrPubkey: senderPubkey}
err = svc.DB.Create(app).Error
ss, err = nip04.ComputeSharedSecret(*app.WalletPubkey, pairingSecretKey)
if err != nil {
return nil, nil, err
}
return app, ss, nil
}
func CreateLegacyApp(svc *TestService, senderPrivkey string) (app *db.App, ss []byte, err error) {
pairingPublicKey, _ := nostr.GetPublicKey(senderPrivkey)
app = &db.App{Name: "test", AppPubkey: pairingPublicKey, Isolated: false}
err = svc.DB.Transaction(func(tx *gorm.DB) error {
err := tx.Save(&app).Error
if err != nil {
return err
}
// commit transaction
return nil
})
if err != nil {
return nil, nil, err
}
svc.EventPublisher.Publish(&events.Event{
Event: "app_created",
Properties: map[string]interface{}{
"name": "test",
"id": app.ID,
},
})
ss, err = nip04.ComputeSharedSecret(svc.Keys.GetNostrPublicKey(), senderPrivkey)
return app, ss, nil
}

View file

@ -4,6 +4,8 @@ import (
"os"
"strconv"
"github.com/getAlby/hub/apps"
"github.com/getAlby/hub/config"
"github.com/getAlby/hub/db"
"github.com/getAlby/hub/events"
@ -17,6 +19,10 @@ import (
const testDB = "test.db"
func CreateTestService() (svc *TestService, err error) {
return CreateTestServiceWithMnemonic("", "")
}
func CreateTestServiceWithMnemonic(mnemonic string, unlockPassword string) (svc *TestService, err error) {
gormDb, err := db.NewDB(testDB, true)
if err != nil {
return nil, err
@ -40,18 +46,25 @@ func CreateTestService() (svc *TestService, err error) {
if err != nil {
return nil, err
}
keys := keys.NewKeys()
keys.Init(cfg, "")
if mnemonic != "" {
cfg.SetUpdate("Mnemonic", mnemonic, unlockPassword)
}
keys.Init(cfg, unlockPassword)
eventPublisher := events.NewEventPublisher()
appsService := apps.NewAppsService(gormDb, eventPublisher, keys)
return &TestService{
Cfg: cfg,
LNClient: mockLn,
EventPublisher: eventPublisher,
DB: gormDb,
Keys: keys,
AppsService: appsService,
}, nil
}
@ -60,6 +73,7 @@ type TestService struct {
Cfg config.Config
LNClient lnclient.LNClient
EventPublisher events.EventPublisher
AppsService apps.AppsService
DB *gorm.DB
}

View file

@ -29,7 +29,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()),
appsSvc: apps.NewAppsService(svc.GetDB(), svc.GetEventPublisher(), svc.GetKeys()),
}
}