mirror of
https://github.com/getAlby/hub.git
synced 2026-08-13 12:33:39 +02:00
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:
parent
4b6d48867e
commit
5fdc803f7a
38 changed files with 956 additions and 264 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
18
api/api.go
18
api/api.go
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
26
db/migrations/202410141503_add_wallet_pubkey.go
Normal file
26
db/migrations/202410141503_add_wallet_pubkey.go
Normal 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
|
||||
},
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
17
db/models.go
17
db/models.go
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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() -
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ export interface App {
|
|||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
nostrPubkey: string;
|
||||
appPubkey: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
lastEventAt?: string;
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
192
nip47/event_handler_legacy_test.go
Normal file
192
nip47/event_handler_legacy_test.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
58
service/create_app_consumer.go
Normal file
58
service/create_app_consumer.go
Normal 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")
|
||||
}()
|
||||
}
|
||||
67
service/delete_app_consumer.go
Normal file
67
service/delete_app_consumer.go
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
104
service/keys/keys_test.go
Normal 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())
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
128
service/start.go
128
service/start.go
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue