From d6c1781a410e4375ea6bf51322919375852c65e3 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Wed, 18 Dec 2024 22:55:57 +0700 Subject: [PATCH 1/4] feat: nwc create_connection command (WIP) --- apps/apps_service.go | 4 + constants/constants.go | 1 + .../create_connection_controller.go | 77 ++++++++++++++++++ .../create_connection_controller_test.go | 81 +++++++++++++++++++ .../get_balance_controller_test.go | 6 +- .../controllers/get_budget_controller_test.go | 10 +-- nip47/controllers/get_info_controller_test.go | 6 +- .../list_transactions_controller_test.go | 10 +-- .../lookup_invoice_controller_test.go | 2 +- .../make_invoice_controller_test.go | 2 +- .../multi_pay_invoice_controller_test.go | 8 +- .../multi_pay_keysend_controller_test.go | 4 +- nip47/controllers/nip47_controller.go | 11 ++- .../pay_invoice_controller_test.go | 6 +- .../pay_keysend_controller_test.go | 4 +- nip47/event_handler.go | 5 +- nip47/models/models.go | 1 + nip47/nip47_service.go | 4 + nip47/permissions/permissions.go | 5 ++ nip47/permissions/permissions_test.go | 39 +++------ 20 files changed, 226 insertions(+), 60 deletions(-) create mode 100644 nip47/controllers/create_connection_controller.go create mode 100644 nip47/controllers/create_connection_controller_test.go diff --git a/apps/apps_service.go b/apps/apps_service.go index accdde7ec..8ffceabca 100644 --- a/apps/apps_service.go +++ b/apps/apps_service.go @@ -44,6 +44,10 @@ func (svc *appsService) CreateApp(name string, pubkey string, maxAmountSat uint6 return nil, "", errors.New("isolated app cannot have sign_message scope") } + // TODO: ensure there is at least one scope + + // TODO: validate budget renewal + var pairingPublicKey string var pairingSecretKey string if pubkey == "" { diff --git a/constants/constants.go b/constants/constants.go index 98a8afeed..6befbd117 100644 --- a/constants/constants.go +++ b/constants/constants.go @@ -28,6 +28,7 @@ const ( LIST_TRANSACTIONS_SCOPE = "list_transactions" SIGN_MESSAGE_SCOPE = "sign_message" NOTIFICATIONS_SCOPE = "notifications" // covers all notification types + SUPERUSER_SCOPE = "superuser" ) // limit encoded metadata length, otherwise relays may have trouble listing multiple transactions diff --git a/nip47/controllers/create_connection_controller.go b/nip47/controllers/create_connection_controller.go new file mode 100644 index 000000000..5cd38b192 --- /dev/null +++ b/nip47/controllers/create_connection_controller.go @@ -0,0 +1,77 @@ +package controllers + +import ( + "context" + "time" + + "github.com/getAlby/hub/constants" + "github.com/getAlby/hub/logger" + "github.com/getAlby/hub/nip47/models" + "github.com/nbd-wtf/go-nostr" + "github.com/sirupsen/logrus" +) + +type createConnectionBudgetParams struct { + Budget uint64 `json:"budget"` + RenewalPeriod string `json:"renewal_period"` +} + +type createConnectionParams struct { + Pubkey string `json:"pubkey"` // pubkey of the app connection + Name string `json:"name"` + Scopes []string `json:"scopes"` + Budget createConnectionBudgetParams `json:"budget"` + ExpiresAt *uint64 `json:"expires_at"` // unix timestamp + Isolated bool `json:"isolated"` + Metadata map[string]interface{} `json:"metadata,omitempty"` +} + +type createConnectionResponse struct { + // pubkey is given, user requesting already knows relay. + WalletPubkey string `json:"wallet_pubkey"` +} + +func (controller *nip47Controller) HandleCreateConnectionEvent(ctx context.Context, nip47Request *models.Request, requestEventId uint, publishResponse publishFunc) { + params := &createConnectionParams{} + resp := decodeRequest(nip47Request, params) + if resp != nil { + publishResponse(resp, nostr.Tags{}) + return + } + + logger.Logger.WithFields(logrus.Fields{ + "request_event_id": requestEventId, + "params": params, + }).Info("creating app") + + var expiresAt *time.Time + if params.ExpiresAt != nil { + expiresAtUnsigned := *params.ExpiresAt + expiresAtValue := time.Unix(int64(expiresAtUnsigned), 0) + expiresAt = &expiresAtValue + } + + app, _, err := controller.appsService.CreateApp(params.Name, params.Pubkey, params.Budget.Budget, params.Budget.RenewalPeriod, expiresAt, params.Scopes, params.Isolated, params.Metadata) + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "request_event_id": requestEventId, + }).WithError(err).Error("Failed to create app") + publishResponse(&models.Response{ + ResultType: nip47Request.Method, + Error: &models.Error{ + Code: constants.ERROR_INTERNAL, + Message: err.Error(), + }, + }, nostr.Tags{}) + return + } + + responsePayload := createConnectionResponse{ + WalletPubkey: *app.WalletPubkey, + } + + publishResponse(&models.Response{ + ResultType: nip47Request.Method, + Result: responsePayload, + }, nostr.Tags{}) +} diff --git a/nip47/controllers/create_connection_controller_test.go b/nip47/controllers/create_connection_controller_test.go new file mode 100644 index 000000000..962a77811 --- /dev/null +++ b/nip47/controllers/create_connection_controller_test.go @@ -0,0 +1,81 @@ +package controllers + +import ( + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/nbd-wtf/go-nostr" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/getAlby/hub/constants" + "github.com/getAlby/hub/db" + "github.com/getAlby/hub/nip47/models" + "github.com/getAlby/hub/nip47/permissions" + "github.com/getAlby/hub/tests" + "github.com/getAlby/hub/transactions" +) + +func TestHandleCreateConnectionEvent(t *testing.T) { + ctx := context.TODO() + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + require.NoError(t, err) + + pairingSecretKey := nostr.GeneratePrivateKey() + pairingPublicKey, err := nostr.GetPublicKey(pairingSecretKey) + require.NoError(t, err) + + nip47CreateConnectionJson := fmt.Sprintf(` +{ + "method": "create_connection", + "params": { + "pubkey": "%s", + "name": "Test 123", + "scopes": ["get_info"] + } +} +`, pairingPublicKey) + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47CreateConnectionJson), nip47Request) + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + var publishedResponse *models.Response + + publishResponse := func(response *models.Response, tags nostr.Tags) { + publishedResponse = response + } + + permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) + transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). + HandleCreateConnectionEvent(ctx, nip47Request, dbRequestEvent.ID, publishResponse) + + assert.Nil(t, publishedResponse.Error) + assert.Equal(t, models.CREATE_CONNECTION_METHOD, publishedResponse.ResultType) + createAppResult := publishedResponse.Result.(createConnectionResponse) + + assert.NotNil(t, createAppResult.WalletPubkey) + app := db.App{} + err = svc.DB.First(&app).Error + assert.NoError(t, err) + assert.Equal(t, pairingPublicKey, app.AppPubkey) + assert.Equal(t, createAppResult.WalletPubkey, *app.WalletPubkey) + + permissions := []db.AppPermission{} + err = svc.DB.Find(&permissions).Error + assert.NoError(t, err) + assert.Equal(t, 1, len(permissions)) + assert.Equal(t, constants.GET_INFO_SCOPE, permissions[0].Scope) +} + +// TODO: app already exists test +// TODO: validation - no pubkey, no scopes, wrong budget etc, +// TODO: review scopes diff --git a/nip47/controllers/get_balance_controller_test.go b/nip47/controllers/get_balance_controller_test.go index 1db1f09cb..81332259a 100644 --- a/nip47/controllers/get_balance_controller_test.go +++ b/nip47/controllers/get_balance_controller_test.go @@ -48,7 +48,7 @@ func TestHandleGetBalanceEvent(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleGetBalanceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Equal(t, uint64(21000), publishedResponse.Result.(*getBalanceResponse).Balance) @@ -82,7 +82,7 @@ func TestHandleGetBalanceEvent_IsolatedApp_NoTransactions(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleGetBalanceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Equal(t, uint64(0), publishedResponse.Result.(*getBalanceResponse).Balance) @@ -129,7 +129,7 @@ func TestHandleGetBalanceEvent_IsolatedApp_Transactions(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleGetBalanceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Equal(t, uint64(1000), publishedResponse.Result.(*getBalanceResponse).Balance) diff --git a/nip47/controllers/get_budget_controller_test.go b/nip47/controllers/get_budget_controller_test.go index 64df460de..1b7f705b9 100644 --- a/nip47/controllers/get_budget_controller_test.go +++ b/nip47/controllers/get_budget_controller_test.go @@ -59,7 +59,7 @@ func TestHandleGetBudgetEvent_NoRenewal(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleGetBudgetEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Equal(t, uint64(400000), publishedResponse.Result.(*getBudgetResponse).TotalBudget) @@ -105,7 +105,7 @@ func TestHandleGetBudgetEvent_NoneUsed(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleGetBudgetEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Equal(t, uint64(400000), publishedResponse.Result.(*getBudgetResponse).TotalBudget) @@ -159,7 +159,7 @@ func TestHandleGetBudgetEvent_HalfUsed(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleGetBudgetEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Equal(t, uint64(400000), publishedResponse.Result.(*getBudgetResponse).TotalBudget) @@ -210,7 +210,7 @@ func TestHandleGetBudgetEvent_NoBudget(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleGetBudgetEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Equal(t, struct{}{}, publishedResponse.Result) @@ -242,7 +242,7 @@ func TestHandleGetBudgetEvent_NoPayInvoicePermission(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleGetBudgetEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Equal(t, struct{}{}, publishedResponse.Result) diff --git a/nip47/controllers/get_info_controller_test.go b/nip47/controllers/get_info_controller_test.go index 0ec97f4a7..21a9fa998 100644 --- a/nip47/controllers/get_info_controller_test.go +++ b/nip47/controllers/get_info_controller_test.go @@ -56,7 +56,7 @@ func TestHandleGetInfoEvent_NoPermission(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleGetInfoEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Nil(t, publishedResponse.Error) @@ -105,7 +105,7 @@ func TestHandleGetInfoEvent_WithPermission(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleGetInfoEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Nil(t, publishedResponse.Error) @@ -161,7 +161,7 @@ func TestHandleGetInfoEvent_WithNotifications(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleGetInfoEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Nil(t, publishedResponse.Error) diff --git a/nip47/controllers/list_transactions_controller_test.go b/nip47/controllers/list_transactions_controller_test.go index 4f5321aa6..435f0affa 100644 --- a/nip47/controllers/list_transactions_controller_test.go +++ b/nip47/controllers/list_transactions_controller_test.go @@ -77,7 +77,7 @@ func TestHandleListTransactionsEvent(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleListTransactionsEvent(ctx, nip47Request, dbRequestEvent.ID, *dbRequestEvent.AppId, publishResponse) assert.Nil(t, publishedResponse.Error) @@ -148,7 +148,7 @@ func TestHandleListTransactionsEvent_UnpaidOutgoingOnly(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleListTransactionsEvent(ctx, nip47Request, dbRequestEvent.ID, *dbRequestEvent.AppId, publishResponse) assert.Nil(t, publishedResponse.Error) @@ -210,7 +210,7 @@ func TestHandleListTransactionsEvent_UnpaidIncomingOnly(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleListTransactionsEvent(ctx, nip47Request, dbRequestEvent.ID, *dbRequestEvent.AppId, publishResponse) assert.Nil(t, publishedResponse.Error) @@ -272,7 +272,7 @@ func TestHandleListTransactionsEvent_Unpaid(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleListTransactionsEvent(ctx, nip47Request, dbRequestEvent.ID, *dbRequestEvent.AppId, publishResponse) assert.Nil(t, publishedResponse.Error) @@ -340,7 +340,7 @@ func TestHandleListTransactionsEvent_Paid(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleListTransactionsEvent(ctx, nip47Request, dbRequestEvent.ID, *dbRequestEvent.AppId, publishResponse) assert.Nil(t, publishedResponse.Error) diff --git a/nip47/controllers/lookup_invoice_controller_test.go b/nip47/controllers/lookup_invoice_controller_test.go index 8cc61d649..c7d1019c5 100644 --- a/nip47/controllers/lookup_invoice_controller_test.go +++ b/nip47/controllers/lookup_invoice_controller_test.go @@ -68,7 +68,7 @@ func TestHandleLookupInvoiceEvent(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleLookupInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, *dbRequestEvent.AppId, publishResponse) assert.Nil(t, publishedResponse.Error) diff --git a/nip47/controllers/make_invoice_controller_test.go b/nip47/controllers/make_invoice_controller_test.go index 980cba3b8..678b1b906 100644 --- a/nip47/controllers/make_invoice_controller_test.go +++ b/nip47/controllers/make_invoice_controller_test.go @@ -66,7 +66,7 @@ func TestHandleMakeInvoiceEvent(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleMakeInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, *dbRequestEvent.AppId, publishResponse) expectedMetadata := map[string]interface{}{ diff --git a/nip47/controllers/multi_pay_invoice_controller_test.go b/nip47/controllers/multi_pay_invoice_controller_test.go index f3ab7275d..da9c18c27 100644 --- a/nip47/controllers/multi_pay_invoice_controller_test.go +++ b/nip47/controllers/multi_pay_invoice_controller_test.go @@ -102,7 +102,7 @@ func TestHandleMultiPayInvoiceEvent_Success(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleMultiPayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) var paymentHashes = []string{ @@ -170,7 +170,7 @@ func TestHandleMultiPayInvoiceEvent_OneMalformedInvoice(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleMultiPayInvoiceEvent(ctx, nip47Request, requestEvent.ID, app, publishResponse) assert.Equal(t, 2, len(responses)) @@ -243,7 +243,7 @@ func TestHandleMultiPayInvoiceEvent_IsolatedApp_OneBudgetExceeded(t *testing.T) permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleMultiPayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Equal(t, 2, len(responses)) @@ -323,7 +323,7 @@ func TestHandleMultiPayInvoiceEvent_LNClient_OnePaymentFailed(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleMultiPayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Equal(t, 2, len(responses)) diff --git a/nip47/controllers/multi_pay_keysend_controller_test.go b/nip47/controllers/multi_pay_keysend_controller_test.go index 9fdbbb1c0..4cd500af9 100644 --- a/nip47/controllers/multi_pay_keysend_controller_test.go +++ b/nip47/controllers/multi_pay_keysend_controller_test.go @@ -108,7 +108,7 @@ func TestHandleMultiPayKeysendEvent_Success(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleMultiPayKeysendEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) assert.Equal(t, 2, len(responses)) @@ -160,7 +160,7 @@ func TestHandleMultiPayKeysendEvent_OneBudgetExceeded(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandleMultiPayKeysendEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse) // we can't guarantee which request was processed first diff --git a/nip47/controllers/nip47_controller.go b/nip47/controllers/nip47_controller.go index fd45e9a12..ffb9c616e 100644 --- a/nip47/controllers/nip47_controller.go +++ b/nip47/controllers/nip47_controller.go @@ -1,6 +1,7 @@ package controllers import ( + "github.com/getAlby/hub/apps" "github.com/getAlby/hub/events" "github.com/getAlby/hub/lnclient" "github.com/getAlby/hub/nip47/permissions" @@ -14,14 +15,22 @@ type nip47Controller struct { eventPublisher events.EventPublisher permissionsService permissions.PermissionsService transactionsService transactions.TransactionsService + appsService apps.AppsService } -func NewNip47Controller(lnClient lnclient.LNClient, db *gorm.DB, eventPublisher events.EventPublisher, permissionsService permissions.PermissionsService, transactionsService transactions.TransactionsService) *nip47Controller { +func NewNip47Controller( + lnClient lnclient.LNClient, + db *gorm.DB, + eventPublisher events.EventPublisher, + permissionsService permissions.PermissionsService, + transactionsService transactions.TransactionsService, + appsService apps.AppsService) *nip47Controller { return &nip47Controller{ lnClient: lnClient, db: db, eventPublisher: eventPublisher, permissionsService: permissionsService, transactionsService: transactionsService, + appsService: appsService, } } diff --git a/nip47/controllers/pay_invoice_controller_test.go b/nip47/controllers/pay_invoice_controller_test.go index 54d6db6f5..8583c9cf8 100644 --- a/nip47/controllers/pay_invoice_controller_test.go +++ b/nip47/controllers/pay_invoice_controller_test.go @@ -78,7 +78,7 @@ func TestHandlePayInvoiceEvent(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandlePayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse, nostr.Tags{}) assert.Equal(t, "123preimage", publishedResponse.Result.(payResponse).Preimage) @@ -129,7 +129,7 @@ func TestHandlePayInvoiceEvent_0Amount(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandlePayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse, nostr.Tags{}) assert.Equal(t, "123preimage", publishedResponse.Result.(payResponse).Preimage) @@ -174,7 +174,7 @@ func TestHandlePayInvoiceEvent_MalformedInvoice(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandlePayInvoiceEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse, nostr.Tags{}) assert.Nil(t, publishedResponse.Result) diff --git a/nip47/controllers/pay_keysend_controller_test.go b/nip47/controllers/pay_keysend_controller_test.go index a5e2c4768..ed31b0a7e 100644 --- a/nip47/controllers/pay_keysend_controller_test.go +++ b/nip47/controllers/pay_keysend_controller_test.go @@ -80,7 +80,7 @@ func TestHandlePayKeysendEvent(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandlePayKeysendEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse, nostr.Tags{}) assert.Nil(t, publishedResponse.Error) @@ -121,7 +121,7 @@ func TestHandlePayKeysendEvent_WithPreimage(t *testing.T) { permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) - NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc). + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). HandlePayKeysendEvent(ctx, nip47Request, dbRequestEvent.ID, app, publishResponse, nostr.Tags{}) assert.Nil(t, publishedResponse.Error) diff --git a/nip47/event_handler.go b/nip47/event_handler.go index 1d0c2de49..5e7ae985c 100644 --- a/nip47/event_handler.go +++ b/nip47/event_handler.go @@ -293,7 +293,7 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela } } - controller := controllers.NewNip47Controller(lnClient, svc.db, svc.eventPublisher, svc.permissionsService, svc.transactionsService) + controller := controllers.NewNip47Controller(lnClient, svc.db, svc.eventPublisher, svc.permissionsService, svc.transactionsService, svc.appsService) switch nip47Request.Method { case models.MULTI_PAY_INVOICE_METHOD: @@ -329,6 +329,9 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela case models.SIGN_MESSAGE_METHOD: controller. HandleSignMessageEvent(ctx, nip47Request, requestEvent.ID, publishResponse) + case models.CREATE_CONNECTION_METHOD: + controller. + HandleCreateConnectionEvent(ctx, nip47Request, requestEvent.ID, publishResponse) default: publishResponse(&models.Response{ ResultType: nip47Request.Method, diff --git a/nip47/models/models.go b/nip47/models/models.go index 05ad61873..5b83189e1 100644 --- a/nip47/models/models.go +++ b/nip47/models/models.go @@ -22,6 +22,7 @@ const ( MULTI_PAY_INVOICE_METHOD = "multi_pay_invoice" MULTI_PAY_KEYSEND_METHOD = "multi_pay_keysend" SIGN_MESSAGE_METHOD = "sign_message" + CREATE_CONNECTION_METHOD = "create_connection" ) type Transaction struct { diff --git a/nip47/nip47_service.go b/nip47/nip47_service.go index 3079179d8..e16f1ceed 100644 --- a/nip47/nip47_service.go +++ b/nip47/nip47_service.go @@ -2,6 +2,8 @@ package nip47 import ( "context" + + "github.com/getAlby/hub/apps" "github.com/getAlby/hub/config" "github.com/getAlby/hub/events" "github.com/getAlby/hub/lnclient" @@ -17,6 +19,7 @@ import ( type nip47Service struct { permissionsService permissions.PermissionsService transactionsService transactions.TransactionsService + appsService apps.AppsService nip47NotificationQueue notifications.Nip47NotificationQueue cfg config.Config keys keys.Keys @@ -41,6 +44,7 @@ func NewNip47Service(db *gorm.DB, cfg config.Config, keys keys.Keys, eventPublis db: db, permissionsService: permissions.NewPermissionsService(db, eventPublisher), transactionsService: transactions.NewTransactionsService(db, eventPublisher), + appsService: apps.NewAppsService(db, eventPublisher, keys), eventPublisher: eventPublisher, keys: keys, } diff --git a/nip47/permissions/permissions.go b/nip47/permissions/permissions.go index 02be54548..ed165b6f7 100644 --- a/nip47/permissions/permissions.go +++ b/nip47/permissions/permissions.go @@ -121,6 +121,8 @@ func scopeToRequestMethods(scope string) []string { return []string{models.LIST_TRANSACTIONS_METHOD} case constants.SIGN_MESSAGE_SCOPE: return []string{models.SIGN_MESSAGE_METHOD} + case constants.SUPERUSER_SCOPE: + return []string{models.CREATE_CONNECTION_METHOD} } return []string{} } @@ -158,6 +160,8 @@ func RequestMethodToScope(requestMethod string) (string, error) { return constants.LIST_TRANSACTIONS_SCOPE, nil case models.SIGN_MESSAGE_METHOD: return constants.SIGN_MESSAGE_SCOPE, nil + case models.CREATE_CONNECTION_METHOD: + return constants.SUPERUSER_SCOPE, nil } logger.Logger.WithField("request_method", requestMethod).Error("Unsupported request method") return "", fmt.Errorf("unsupported request method: %s", requestMethod) @@ -173,6 +177,7 @@ func AllScopes() []string { constants.LIST_TRANSACTIONS_SCOPE, constants.SIGN_MESSAGE_SCOPE, constants.NOTIFICATIONS_SCOPE, + constants.SUPERUSER_SCOPE, } } diff --git a/nip47/permissions/permissions_test.go b/nip47/permissions/permissions_test.go index 8424c53eb..ce68b183c 100644 --- a/nip47/permissions/permissions_test.go +++ b/nip47/permissions/permissions_test.go @@ -55,35 +55,6 @@ func TestHasPermission_Expired(t *testing.T) { assert.Equal(t, "This app has expired", message) } -// TODO: move to transactions service -/*func TestHasPermission_Exceeded(t *testing.T) { - defer tests.RemoveTestService() - svc, err := tests.CreateTestService() - require.NoError(t, err) - - app, _, err := tests.CreateApp(svc) - assert.NoError(t, err) - - budgetRenewal := "never" - expiresAt := time.Now().Add(24 * time.Hour) - appPermission := &db.AppPermission{ - AppId: app.ID, - App: *app, - Scope: constants.PAY_INVOICE_SCOPE, - MaxAmountSat: 10, - BudgetRenewal: budgetRenewal, - ExpiresAt: &expiresAt, - } - err = svc.DB.Create(appPermission).Error - assert.NoError(t, err) - - permissionsSvc := NewPermissionsService(svc.DB, svc.EventPublisher) - result, code, message := permissionsSvc.HasPermission(app, PAY_INVOICE_SCOPE, 100*1000) - assert.False(t, result) - assert.Equal(t, constants.ERROR_QUOTA_EXCEEDED, code) - assert.Equal(t, "Insufficient budget remaining to make payment", message) -}*/ - func TestHasPermission_OK(t *testing.T) { defer tests.RemoveTestService() svc, err := tests.CreateTestService() @@ -144,6 +115,16 @@ func TestRequestMethodsToScopes_GetInfo(t *testing.T) { assert.Equal(t, []string{constants.GET_INFO_SCOPE}, scopes) } +func TestRequestMethodToScope_CreateConnection(t *testing.T) { + scope, err := RequestMethodToScope(models.CREATE_CONNECTION_METHOD) + assert.NoError(t, err) + assert.Equal(t, constants.SUPERUSER_SCOPE, scope) +} +func TestScopeToRequestMethods_Superuser(t *testing.T) { + methods := scopeToRequestMethods(constants.SUPERUSER_SCOPE) + assert.Equal(t, []string{models.CREATE_CONNECTION_METHOD}, methods) +} + func TestGetPermittedMethods_AlwaysGranted(t *testing.T) { defer tests.RemoveTestService() svc, err := tests.CreateTestService() From d3dddc2f78f49f407966c3a02ef3537b572c00cc Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Thu, 19 Dec 2024 20:33:27 +0700 Subject: [PATCH 2/4] feat: allow creating superuser apps from the ui --- api/api.go | 2 ++ frontend/src/components/Permissions.tsx | 16 +++++++++++++++- frontend/src/components/Scopes.tsx | 13 ++++++++++--- frontend/src/types.ts | 6 +++++- 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/api/api.go b/api/api.go index b79deb3d4..238e07e08 100644 --- a/api/api.go +++ b/api/api.go @@ -883,6 +883,8 @@ func (api *api) GetWalletCapabilities(ctx context.Context) (*WalletCapabilitiesR if len(notificationTypes) > 0 { scopes = append(scopes, constants.NOTIFICATIONS_SCOPE) } + // add always-supported capabilities + scopes = append(scopes, constants.SUPERUSER_SCOPE) return &WalletCapabilitiesResponse{ Methods: methods, diff --git a/frontend/src/components/Permissions.tsx b/frontend/src/components/Permissions.tsx index b9d1301dd..b297807fa 100644 --- a/frontend/src/components/Permissions.tsx +++ b/frontend/src/components/Permissions.tsx @@ -1,4 +1,4 @@ -import { BrickWall, PlusCircle } from "lucide-react"; +import { AlertTriangleIcon, BrickWall, PlusCircle } from "lucide-react"; import React from "react"; import BudgetAmountSelect from "src/components/BudgetAmountSelect"; import BudgetRenewalSelect from "src/components/BudgetRenewalSelect"; @@ -132,6 +132,20 @@ const Permissions: React.FC = ({ )} + {permissions.scopes.includes("superuser") && ( + <> +
+ +

Superuser Access

+
+ +

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

+ + )} + {!permissions.isolated && permissions.scopes.includes("pay_invoice") && ( <> {!readOnly && !budgetReadOnly ? ( diff --git a/frontend/src/components/Scopes.tsx b/frontend/src/components/Scopes.tsx index 06e31c9fa..ab625359a 100644 --- a/frontend/src/components/Scopes.tsx +++ b/frontend/src/components/Scopes.tsx @@ -52,7 +52,7 @@ const Scopes: React.FC = ({ onScopesChanged, }) => { const fullAccessScopes: Scope[] = React.useMemo(() => { - return [...capabilities.scopes]; + return capabilities.scopes.filter((scope) => scope !== "superuser"); }, [capabilities.scopes]); const readOnlyScopes: Scope[] = React.useMemo(() => { @@ -87,10 +87,17 @@ const Scopes: React.FC = ({ }, [capabilities.scopes]); const [scopeGroup, setScopeGroup] = React.useState(() => { - if (isolated && scopes.length === capabilities.scopes.length) { + if ( + isolated && + scopes.length === fullAccessScopes.length && + scopes.every((scope) => fullAccessScopes.includes(scope)) + ) { return "isolated"; } - if (scopes.length === capabilities.scopes.length) { + if ( + scopes.length === fullAccessScopes.length && + scopes.every((scope) => fullAccessScopes.includes(scope)) + ) { return "full_access"; } if ( diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 3a5f19f1c..602aa5a3e 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -1,6 +1,7 @@ import { Bell, CirclePlus, + Crown, HandCoins, Info, LucideIcon, @@ -47,7 +48,8 @@ export type Scope = | "lookup_invoice" | "list_transactions" | "sign_message" - | "notifications"; // covers all notification types + | "notifications" // covers all notification types + | "superuser"; export type Nip47NotificationType = "payment_received" | "payment_sent"; @@ -64,6 +66,7 @@ export const scopeIconMap: ScopeIconMap = { pay_invoice: HandCoins, sign_message: PenLine, notifications: Bell, + superuser: Crown, }; export type WalletCapabilities = { @@ -89,6 +92,7 @@ export const scopeDescriptions: Record = { pay_invoice: "Send payments", sign_message: "Sign messages", notifications: "Receive wallet notifications", + superuser: "Create other app connections", }; export const expiryOptions: Record = { From ec025645a4f13a2c92540a129d3335219f93e357 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Thu, 19 Dec 2024 20:49:50 +0700 Subject: [PATCH 3/4] fix: pass methods rather than scopes in create_connection method --- .../create_connection_controller.go | 22 +++++++++++++++++-- .../create_connection_controller_test.go | 4 ++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/nip47/controllers/create_connection_controller.go b/nip47/controllers/create_connection_controller.go index 5cd38b192..61c3e1ae5 100644 --- a/nip47/controllers/create_connection_controller.go +++ b/nip47/controllers/create_connection_controller.go @@ -2,11 +2,13 @@ package controllers import ( "context" + "slices" "time" "github.com/getAlby/hub/constants" "github.com/getAlby/hub/logger" "github.com/getAlby/hub/nip47/models" + "github.com/getAlby/hub/nip47/permissions" "github.com/nbd-wtf/go-nostr" "github.com/sirupsen/logrus" ) @@ -19,7 +21,7 @@ type createConnectionBudgetParams struct { type createConnectionParams struct { Pubkey string `json:"pubkey"` // pubkey of the app connection Name string `json:"name"` - Scopes []string `json:"scopes"` + Methods []string `json:"methods"` Budget createConnectionBudgetParams `json:"budget"` ExpiresAt *uint64 `json:"expires_at"` // unix timestamp Isolated bool `json:"isolated"` @@ -51,7 +53,23 @@ func (controller *nip47Controller) HandleCreateConnectionEvent(ctx context.Conte expiresAt = &expiresAtValue } - app, _, err := controller.appsService.CreateApp(params.Name, params.Pubkey, params.Budget.Budget, params.Budget.RenewalPeriod, expiresAt, params.Scopes, params.Isolated, params.Metadata) + // TODO: verify the LNClient supports the methods + supportedMethods := controller.lnClient.GetSupportedNIP47Methods() + if slices.ContainsFunc(params.Methods, func(method string) bool { + return !slices.Contains(supportedMethods, method) + }) { + publishResponse(&models.Response{ + ResultType: nip47Request.Method, + Error: &models.Error{ + Code: constants.ERROR_INTERNAL, + Message: "One or more methods are not supported by the current LNClient", + }, + }, nostr.Tags{}) + return + } + scopes, err := permissions.RequestMethodsToScopes(params.Methods) + + app, _, err := controller.appsService.CreateApp(params.Name, params.Pubkey, params.Budget.Budget, params.Budget.RenewalPeriod, expiresAt, scopes, params.Isolated, params.Metadata) if err != nil { logger.Logger.WithFields(logrus.Fields{ "request_event_id": requestEventId, diff --git a/nip47/controllers/create_connection_controller_test.go b/nip47/controllers/create_connection_controller_test.go index 962a77811..e455c04c6 100644 --- a/nip47/controllers/create_connection_controller_test.go +++ b/nip47/controllers/create_connection_controller_test.go @@ -34,7 +34,7 @@ func TestHandleCreateConnectionEvent(t *testing.T) { "params": { "pubkey": "%s", "name": "Test 123", - "scopes": ["get_info"] + "methods": ["get_info"] } } `, pairingPublicKey) @@ -78,4 +78,4 @@ func TestHandleCreateConnectionEvent(t *testing.T) { // TODO: app already exists test // TODO: validation - no pubkey, no scopes, wrong budget etc, -// TODO: review scopes +// TODO: ensure lnclient supports the methods From 181eda1265a1efb411d74abcb8e0f6d940f22c45 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Mon, 23 Dec 2024 15:44:16 +0700 Subject: [PATCH 4/4] chore: add extra tests --- apps/apps_service.go | 9 +- constants/constants.go | 10 + .../create_connection_controller.go | 12 ++ .../create_connection_controller_test.go | 190 +++++++++++++++++- nip47/event_handler_test.go | 2 +- 5 files changed, 217 insertions(+), 6 deletions(-) diff --git a/apps/apps_service.go b/apps/apps_service.go index 8ffceabca..e4b8c40e7 100644 --- a/apps/apps_service.go +++ b/apps/apps_service.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "slices" + "strings" "time" "github.com/getAlby/hub/constants" @@ -44,9 +45,13 @@ func (svc *appsService) CreateApp(name string, pubkey string, maxAmountSat uint6 return nil, "", errors.New("isolated app cannot have sign_message scope") } - // TODO: ensure there is at least one scope + if budgetRenewal == "" { + budgetRenewal = constants.BUDGET_RENEWAL_NEVER + } - // TODO: validate budget renewal + if !slices.Contains(constants.GetBudgetRenewals(), budgetRenewal) { + return nil, "", fmt.Errorf("invalid budget renewal. Must be one of %s", strings.Join(constants.GetBudgetRenewals(), ",")) + } var pairingPublicKey string var pairingSecretKey string diff --git a/constants/constants.go b/constants/constants.go index 6befbd117..a5c05aea3 100644 --- a/constants/constants.go +++ b/constants/constants.go @@ -19,6 +19,16 @@ const ( BUDGET_RENEWAL_NEVER = "never" ) +func GetBudgetRenewals() []string { + return []string{ + BUDGET_RENEWAL_DAILY, + BUDGET_RENEWAL_WEEKLY, + BUDGET_RENEWAL_MONTHLY, + BUDGET_RENEWAL_YEARLY, + BUDGET_RENEWAL_NEVER, + } +} + const ( PAY_INVOICE_SCOPE = "pay_invoice" // also covers pay_keysend and multi_* payment methods GET_BALANCE_SCOPE = "get_balance" diff --git a/nip47/controllers/create_connection_controller.go b/nip47/controllers/create_connection_controller.go index 61c3e1ae5..5149d9eed 100644 --- a/nip47/controllers/create_connection_controller.go +++ b/nip47/controllers/create_connection_controller.go @@ -69,6 +69,18 @@ func (controller *nip47Controller) HandleCreateConnectionEvent(ctx context.Conte } scopes, err := permissions.RequestMethodsToScopes(params.Methods) + // ensure there is at least one scope + if len(scopes) == 0 { + publishResponse(&models.Response{ + ResultType: nip47Request.Method, + Error: &models.Error{ + Code: constants.ERROR_INTERNAL, + Message: "No methods provided", + }, + }, nostr.Tags{}) + return + } + app, _, err := controller.appsService.CreateApp(params.Name, params.Pubkey, params.Budget.Budget, params.Budget.RenewalPeriod, expiresAt, scopes, params.Isolated, params.Metadata) if err != nil { logger.Logger.WithFields(logrus.Fields{ diff --git a/nip47/controllers/create_connection_controller_test.go b/nip47/controllers/create_connection_controller_test.go index e455c04c6..687ab15b8 100644 --- a/nip47/controllers/create_connection_controller_test.go +++ b/nip47/controllers/create_connection_controller_test.go @@ -76,6 +76,190 @@ func TestHandleCreateConnectionEvent(t *testing.T) { assert.Equal(t, constants.GET_INFO_SCOPE, permissions[0].Scope) } -// TODO: app already exists test -// TODO: validation - no pubkey, no scopes, wrong budget etc, -// TODO: ensure lnclient supports the methods +func TestHandleCreateConnectionEvent_PubkeyAlreadyExists(t *testing.T) { + ctx := context.TODO() + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + require.NoError(t, err) + + pairingSecretKey := nostr.GeneratePrivateKey() + pairingPublicKey, err := nostr.GetPublicKey(pairingSecretKey) + require.NoError(t, err) + + _, _, err = svc.AppsService.CreateApp("Existing App", pairingPublicKey, 0, constants.BUDGET_RENEWAL_NEVER, nil, []string{models.GET_INFO_METHOD}, false, nil) + + nip47CreateConnectionJson := fmt.Sprintf(` +{ + "method": "create_connection", + "params": { + "pubkey": "%s", + "name": "Test 123", + "methods": ["get_info"] + } +} +`, pairingPublicKey) + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47CreateConnectionJson), nip47Request) + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + var publishedResponse *models.Response + + publishResponse := func(response *models.Response, tags nostr.Tags) { + publishedResponse = response + } + + permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) + transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). + HandleCreateConnectionEvent(ctx, nip47Request, dbRequestEvent.ID, publishResponse) + + assert.NotNil(t, publishedResponse.Error) + assert.Equal(t, constants.ERROR_INTERNAL, publishedResponse.Error.Code) + assert.Equal(t, "duplicated key not allowed", publishedResponse.Error.Message) + assert.Equal(t, models.CREATE_CONNECTION_METHOD, publishedResponse.ResultType) + assert.Nil(t, publishedResponse.Result) +} + +func TestHandleCreateConnectionEvent_NoMethods(t *testing.T) { + ctx := context.TODO() + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + require.NoError(t, err) + + pairingSecretKey := nostr.GeneratePrivateKey() + pairingPublicKey, err := nostr.GetPublicKey(pairingSecretKey) + require.NoError(t, err) + + nip47CreateConnectionJson := fmt.Sprintf(` +{ + "method": "create_connection", + "params": { + "pubkey": "%s", + "name": "Test 123" + } +} +`, pairingPublicKey) + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47CreateConnectionJson), nip47Request) + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + var publishedResponse *models.Response + + publishResponse := func(response *models.Response, tags nostr.Tags) { + publishedResponse = response + } + + permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) + transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). + HandleCreateConnectionEvent(ctx, nip47Request, dbRequestEvent.ID, publishResponse) + + assert.NotNil(t, publishedResponse.Error) + assert.Equal(t, constants.ERROR_INTERNAL, publishedResponse.Error.Code) + assert.Equal(t, "No methods provided", publishedResponse.Error.Message) + assert.Equal(t, models.CREATE_CONNECTION_METHOD, publishedResponse.ResultType) + assert.Nil(t, publishedResponse.Result) +} + +func TestHandleCreateConnectionEvent_UnsupportedMethod(t *testing.T) { + ctx := context.TODO() + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + require.NoError(t, err) + + pairingSecretKey := nostr.GeneratePrivateKey() + pairingPublicKey, err := nostr.GetPublicKey(pairingSecretKey) + require.NoError(t, err) + + nip47CreateConnectionJson := fmt.Sprintf(` +{ + "method": "create_connection", + "params": { + "pubkey": "%s", + "name": "Test 123", + "methods": ["non_existent"] + } +} +`, pairingPublicKey) + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47CreateConnectionJson), nip47Request) + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + var publishedResponse *models.Response + + publishResponse := func(response *models.Response, tags nostr.Tags) { + publishedResponse = response + } + + permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) + transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). + HandleCreateConnectionEvent(ctx, nip47Request, dbRequestEvent.ID, publishResponse) + + assert.NotNil(t, publishedResponse.Error) + assert.Equal(t, constants.ERROR_INTERNAL, publishedResponse.Error.Code) + assert.Equal(t, "One or more methods are not supported by the current LNClient", publishedResponse.Error.Message) + assert.Equal(t, models.CREATE_CONNECTION_METHOD, publishedResponse.ResultType) + assert.Nil(t, publishedResponse.Result) +} +func TestHandleCreateConnectionEvent_DoNotAllowCreateConnectionMethod(t *testing.T) { + ctx := context.TODO() + defer tests.RemoveTestService() + svc, err := tests.CreateTestService() + require.NoError(t, err) + + pairingSecretKey := nostr.GeneratePrivateKey() + pairingPublicKey, err := nostr.GetPublicKey(pairingSecretKey) + require.NoError(t, err) + + nip47CreateConnectionJson := fmt.Sprintf(` +{ + "method": "create_connection", + "params": { + "pubkey": "%s", + "name": "Test 123", + "methods": ["create_connection"] + } +} +`, pairingPublicKey) + + nip47Request := &models.Request{} + err = json.Unmarshal([]byte(nip47CreateConnectionJson), nip47Request) + assert.NoError(t, err) + + dbRequestEvent := &db.RequestEvent{} + err = svc.DB.Create(&dbRequestEvent).Error + assert.NoError(t, err) + + var publishedResponse *models.Response + + publishResponse := func(response *models.Response, tags nostr.Tags) { + publishedResponse = response + } + + permissionsSvc := permissions.NewPermissionsService(svc.DB, svc.EventPublisher) + transactionsSvc := transactions.NewTransactionsService(svc.DB, svc.EventPublisher) + NewNip47Controller(svc.LNClient, svc.DB, svc.EventPublisher, permissionsSvc, transactionsSvc, svc.AppsService). + HandleCreateConnectionEvent(ctx, nip47Request, dbRequestEvent.ID, publishResponse) + + assert.NotNil(t, publishedResponse.Error) + assert.Equal(t, constants.ERROR_INTERNAL, publishedResponse.Error.Code) + assert.Equal(t, "One or more methods are not supported by the current LNClient", publishedResponse.Error.Message) + assert.Equal(t, models.CREATE_CONNECTION_METHOD, publishedResponse.ResultType) + assert.Nil(t, publishedResponse.Result) +} diff --git a/nip47/event_handler_test.go b/nip47/event_handler_test.go index 342f8fc93..f91281dce 100644 --- a/nip47/event_handler_test.go +++ b/nip47/event_handler_test.go @@ -141,7 +141,7 @@ func TestHandleResponse_WithPermission(t *testing.T) { assert.Nil(t, unmarshalledResponse.Error) assert.Equal(t, models.GET_INFO_METHOD, unmarshalledResponse.ResultType) expectedMethods := slices.Concat([]string{constants.GET_BALANCE_SCOPE}, permissions.GetAlwaysGrantedMethods()) - assert.Equal(t, expectedMethods, unmarshalledResponse.Result.Methods) + assert.ElementsMatch(t, expectedMethods, unmarshalledResponse.Result.Methods) } func TestHandleResponse_DuplicateRequest(t *testing.T) {