Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 55 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
83de003
feat(appwalletKey): add GetBIP32ChildKey
frnandu Oct 14, 2024
546322c
feat: fix interface
frnandu Oct 14, 2024
8cbe566
feat: adding subscription WiP
frnandu Oct 15, 2024
853beed
feat: handle nostr subscriptions for lifecycle of apps
frnandu Oct 18, 2024
8cb135d
Delete .idea/.gitignore
frnandu Oct 18, 2024
616a69b
Delete .idea/hub.iml
frnandu Oct 18, 2024
55113ce
Delete .idea/modules.xml
frnandu Oct 18, 2024
78c2faa
Delete .idea/vcs.xml
frnandu Oct 18, 2024
e7ae82c
fix: remove unnecessary
frnandu Oct 18, 2024
dc404dd
fix: missing handling legacy app
frnandu Oct 18, 2024
b145c41
fix: review fixes
frnandu Oct 21, 2024
23b35c8
fix: use app.ID for key calculation instead of passing in event
frnandu Oct 21, 2024
b64c8cf
fix: add TODO
frnandu Oct 21, 2024
d0b0354
fix: remove unnecessary check
frnandu Oct 25, 2024
0c5949c
fix: improve err handling and remove check
frnandu Oct 25, 2024
324f113
Merge remote-tracking branch 'origin/master' into feat/wallet-child-k…
rolznz Oct 28, 2024
b4d1f3e
chore: store master nostr key to avoid deriving each time
rolznz Oct 29, 2024
a1dc936
chore: rename app nostr_pubkey to app_pubkey, extract app consumers i…
rolznz Oct 29, 2024
bedc82d
chore: finish renaming
rolznz Oct 29, 2024
200c172
fix: update app wallet pubkey on app creation
rolznz Oct 29, 2024
aef3142
fix: not NULL check
rolznz Oct 29, 2024
96a71bc
fix: fix HandleEvent
frnandu Oct 29, 2024
b359339
fix: error handling
frnandu Oct 29, 2024
aa2c3c1
fix: error handling
frnandu Oct 29, 2024
9f69b1a
fix: move StartSubscription to start.go
frnandu Oct 29, 2024
0d61bb2
fix: remove duplicated error check
frnandu Oct 29, 2024
b530632
fix: make tests use AppsService for creating apps
frnandu Oct 29, 2024
d4c6a60
chore: remove unused code
rolznz Oct 29, 2024
63c3fd9
chore: minor event handler improvements
rolznz Oct 29, 2024
1d79b8a
fix: add event_handler tests for legacy app
frnandu Oct 29, 2024
55d9515
chore: add comment about legacy apps in deleteAppConsumer
rolznz Oct 29, 2024
b1bcc58
Merge branch 'feat/wallet-child-key-per-connection' of github.com:get…
rolznz Oct 29, 2024
c56d1d1
fix: remove unused app from tests
rolznz Oct 29, 2024
bc923eb
fix: error handling in startAppWalletSubscription
rolznz Oct 29, 2024
606fb32
fix: only create event info and nostr subscription for master key if …
frnandu Oct 30, 2024
932a9fb
fix: add legacy tests
frnandu Oct 31, 2024
17c9c0b
fix: move fetching of Nip47 event info to deleteAppConsumer
frnandu Oct 31, 2024
8127fea
fix: fixed arguments
frnandu Oct 31, 2024
7cff2a7
fix: use require instead of assert
frnandu Nov 1, 2024
339d420
Merge branch 'master' into feat/wallet-child-key-per-connection
frnandu Nov 1, 2024
606c33b
fix: adapt GetAppWalletKey to use DeriveKey with path 1'
frnandu Nov 1, 2024
877509e
fix: for backends that don't use a mnemonic, create appKey from nostr…
frnandu Nov 4, 2024
c47d923
fix: cleanup eventPublisher Subscribers when relay reconnects
frnandu Nov 4, 2024
7bfd140
fix: bip32.FirstHardenedChild + appID
frnandu Nov 6, 2024
ce4c5cf
fix: remove unused env vars
frnandu Nov 6, 2024
8c1ef4b
fix: generate new mnemonic if empty
frnandu Nov 6, 2024
aea59f0
fix: add tests.CreateTestServiceWithMnemonic to fix TestEncryptedBackup
frnandu Nov 6, 2024
78f59bc
fix: handle both relay and main ctx Done
frnandu Nov 6, 2024
b322f4c
Merge branch 'master' into feat/wallet-child-key-per-connection
rolznz Nov 7, 2024
b4bf53f
chore: add keys tests
rolznz Nov 7, 2024
caed3b3
chore: add extra assertions to keys test
rolznz Nov 7, 2024
2b14d3c
chore: log when legacy app subscription is created
rolznz Nov 7, 2024
dc54213
chore: remove unnecessary break
rolznz Nov 7, 2024
0be7e32
chore: add log when relay is successfully connected
rolznz Nov 7, 2024
287607d
fix: only auto-start node if it has been started before
rolznz Nov 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion alby/alby_oauth_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 2 additions & 6 deletions alby/alby_oauth_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
18 changes: 10 additions & 8 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -80,7 +80,8 @@ func (api *api) CreateApp(createAppRequest *CreateAppRequest) (*CreateAppRespons
expiresAt,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up issue: The expiresAt, scopes checks above are to be shifted to CreateApp method in apps_service and UpdateApp, GetApp, ListApps, TopupIsolatedApp methods need to be added there. I think we can also GetAppByPubkey directly in those methods so we don't have to call it separately in http_service and wails_service

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@im-adithya could you create an issue and link some code?

createAppRequest.Scopes,
createAppRequest.Isolated,
createAppRequest.Metadata)
createAppRequest.Metadata,
)

if err != nil {
return nil, err
Expand All @@ -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()
Expand All @@ -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)
}
Expand All @@ -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
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
}

Expand Down
2 changes: 1 addition & 1 deletion api/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
42 changes: 39 additions & 3 deletions apps/apps_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,29 @@ 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"
)

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

Expand Down Expand Up @@ -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
Expand All @@ -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
})
Expand All @@ -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
}
Expand Down
26 changes: 26 additions & 0 deletions db/migrations/202410141503_add_wallet_pubkey.go
Original file line number Diff line number Diff line change
@@ -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
},
}
1 change: 1 addition & 0 deletions db/migrations/migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
17 changes: 9 additions & 8 deletions db/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/TransactionItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/connections/AlbyConnectionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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" />
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/connections/AppCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/connections/AppCardConnectionInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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">
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/connections/AppCardNotice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/hooks/useDeleteApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,23 @@ 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",
},
});
toast({ title: "Connection deleted" });
if (onSuccess) {
onSuccess(nostrPubkey);
onSuccess(appPubkey);
}
} catch (error) {
await handleRequestError(toast, "Failed to delete connection", error);
Expand Down
Loading
Loading