diff --git a/alby/alby_oauth_service.go b/alby/alby_oauth_service.go index dd9915b9..ad1af1b9 100644 --- a/alby/alby_oauth_service.go +++ b/alby/alby_oauth_service.go @@ -20,6 +20,7 @@ import ( "golang.org/x/oauth2" "gorm.io/gorm" + "github.com/getAlby/hub/apps" "github.com/getAlby/hub/config" "github.com/getAlby/hub/constants" "github.com/getAlby/hub/db" @@ -346,9 +347,9 @@ func (svc *albyOAuthService) DrainSharedWallet(ctx context.Context, lnClient lnc 10 // Alby fee reserve (10 sats) if amountSat < 1 { - return errors.New("Not enough balance remaining") + return errors.New("not enough balance remaining") } - amount := amountSat * 1000 + amount := uint64(amountSat * 1000) logger.Logger.WithField("amount", amount).WithError(err).Error("Draining Alby shared wallet funds") @@ -492,7 +493,7 @@ func (svc *albyOAuthService) LinkAccount(ctx context.Context, lnClient lnclient. scopes = append(scopes, constants.NOTIFICATIONS_SCOPE) } - app, _, err := db.NewDBService(svc.db, svc.eventPublisher).CreateApp( + app, _, err := apps.NewAppsService(svc.db, svc.eventPublisher).CreateApp( ALBY_ACCOUNT_APP_NAME, connectionPubkey, budget, diff --git a/api/api.go b/api/api.go index 51ea0e2f..ea0b6f38 100644 --- a/api/api.go +++ b/api/api.go @@ -1,6 +1,7 @@ package api import ( + "bytes" "context" "encoding/json" "errors" @@ -17,6 +18,7 @@ import ( "gorm.io/gorm" "github.com/getAlby/hub/alby" + "github.com/getAlby/hub/apps" "github.com/getAlby/hub/config" "github.com/getAlby/hub/constants" "github.com/getAlby/hub/db" @@ -33,7 +35,7 @@ import ( type api struct { db *gorm.DB - dbSvc db.DBService + appsSvc apps.AppsService cfg config.Config svc service.Service permissionsSvc permissions.PermissionsService @@ -46,7 +48,7 @@ type api struct { func NewAPI(svc service.Service, gormDB *gorm.DB, config config.Config, keys keys.Keys, albyOAuthSvc alby.AlbyOAuthService, eventPublisher events.EventPublisher) *api { return &api{ db: gormDB, - dbSvc: db.NewDBService(gormDB, eventPublisher), + appsSvc: apps.NewAppsService(gormDB, eventPublisher), cfg: config, svc: svc, permissionsSvc: permissions.NewPermissionsService(gormDB, eventPublisher), @@ -71,7 +73,7 @@ func (api *api) CreateApp(createAppRequest *CreateAppRequest) (*CreateAppRespons } } - app, pairingSecretKey, err := api.dbSvc.CreateApp( + app, pairingSecretKey, err := api.appsSvc.CreateApp( createAppRequest.Name, createAppRequest.Pubkey, createAppRequest.MaxAmountSat, @@ -93,9 +95,21 @@ func (api *api) CreateApp(createAppRequest *CreateAppRequest) (*CreateAppRespons responseBody.Pubkey = app.NostrPubkey responseBody.PairingSecret = pairingSecretKey - lightningAddress, err := api.albyOAuthSvc.GetLightningAddress() - if err != nil { - return nil, err + var lightningAddress string + + if !app.Isolated { + lightningAddress, err = api.albyOAuthSvc.GetLightningAddress() + if err != nil { + return nil, err + } + } + + connectionSecret := fmt.Sprintf("nostr+walletconnect://%s?relay=%s&secret=%s", api.keys.GetNostrPublicKey(), relayUrl, pairingSecretKey) + if app.Isolated /* TODO: && createAppRequest.GenerateLightningAddress */ { + lightningAddress, err = api.generateLightningAddress(connectionSecret) + if err != nil { + return nil, err + } } if createAppRequest.ReturnTo != "" { @@ -104,7 +118,7 @@ func (api *api) CreateApp(createAppRequest *CreateAppRequest) (*CreateAppRespons query := returnToUrl.Query() query.Add("relay", relayUrl) query.Add("pubkey", api.keys.GetNostrPublicKey()) - if lightningAddress != "" && !app.Isolated { + if lightningAddress != "" { query.Add("lud16", lightningAddress) } returnToUrl.RawQuery = query.Encode() @@ -113,10 +127,10 @@ func (api *api) CreateApp(createAppRequest *CreateAppRequest) (*CreateAppRespons } var lud16 string - if lightningAddress != "" && !app.Isolated { + if lightningAddress != "" { 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("%s%s", connectionSecret, lud16) return responseBody, nil } @@ -900,3 +914,51 @@ func (api *api) parseExpiresAt(expiresAtString string) (*time.Time, error) { } return expiresAt, nil } + +func (api *api) generateLightningAddress(connectionSecret string) (string, error) { + + client := &http.Client{Timeout: 10 * time.Second} + + type createUserRequest struct { + ConnectionSecret string `json:"connectionSecret"` + } + request := createUserRequest{ + ConnectionSecret: connectionSecret, + } + body := bytes.NewBuffer([]byte{}) + err := json.NewEncoder(body).Encode(&request) + if err != nil { + return "", err + } + + // TODO: update URL + albyLiteURL := "http://localhost:8081" + req, err := http.NewRequest("POST", fmt.Sprintf("%s/users", albyLiteURL), body) + if err != nil { + return "", err + } + + resp, err := client.Do(req) + if err != nil { + logger.Logger.WithError(err).Error("Failed to create new alby lite user") + return "", err + } + + if resp.StatusCode >= 300 { + logger.Logger.WithFields(logrus.Fields{ + "status": resp.StatusCode, + }).Error("Request to create new alby lite user returned non-success status") + return "", fmt.Errorf("unexpected status code from alby lite: %d", resp.StatusCode) + } + + type createUserResponse struct { + LightningAddress string `json:"lightningAddress"` + } + response := &createUserResponse{} + err = json.NewDecoder(resp.Body).Decode(response) + if err != nil { + logger.Logger.WithError(err).Error("Failed to decode API response") + return "", err + } + return response.LightningAddress, nil +} diff --git a/api/models.go b/api/models.go index 5d2e1645..6bafc408 100644 --- a/api/models.go +++ b/api/models.go @@ -12,9 +12,10 @@ import ( type API interface { CreateApp(createAppRequest *CreateAppRequest) (*CreateAppResponse, error) - UpdateApp(userApp *db.App, updateAppRequest *UpdateAppRequest) error - DeleteApp(userApp *db.App) error - GetApp(userApp *db.App) *App + UpdateApp(app *db.App, updateAppRequest *UpdateAppRequest) error + TopupIsolatedApp(ctx context.Context, app *db.App, amountMsat uint64) error + DeleteApp(app *db.App) error + GetApp(app *db.App) *App ListApps() ([]App, error) ListChannels(ctx context.Context) ([]Channel, error) GetChannelPeerSuggestions(ctx context.Context) ([]alby.ChannelPeerSuggestion, error) @@ -36,7 +37,7 @@ type API interface { GetBalances(ctx context.Context) (*BalancesResponse, error) ListTransactions(ctx context.Context, limit uint64, offset uint64) (*ListTransactionsResponse, error) SendPayment(ctx context.Context, invoice string) (*SendPaymentResponse, error) - CreateInvoice(ctx context.Context, amount int64, description string) (*MakeInvoiceResponse, error) + CreateInvoice(ctx context.Context, amount uint64, description string) (*MakeInvoiceResponse, error) LookupInvoice(ctx context.Context, paymentHash string) (*LookupInvoiceResponse, error) RequestMempoolApi(endpoint string) (interface{}, error) GetInfo(ctx context.Context) (*InfoResponse, error) @@ -86,6 +87,10 @@ type UpdateAppRequest struct { Metadata Metadata `json:"metadata,omitempty"` } +type TopupIsolatedAppRequest struct { + AmountSat uint64 `json:"amountSat"` +} + type CreateAppRequest struct { Name string `json:"name"` Pubkey string `json:"pubkey"` @@ -283,7 +288,7 @@ type SignMessageResponse struct { } type MakeInvoiceRequest struct { - Amount int64 `json:"amount"` + Amount uint64 `json:"amount"` Description string `json:"description"` } diff --git a/api/transactions.go b/api/transactions.go index d499fd98..6b02e920 100644 --- a/api/transactions.go +++ b/api/transactions.go @@ -7,12 +7,13 @@ import ( "strings" "time" + "github.com/getAlby/hub/db" "github.com/getAlby/hub/logger" "github.com/getAlby/hub/transactions" "github.com/sirupsen/logrus" ) -func (api *api) CreateInvoice(ctx context.Context, amount int64, description string) (*MakeInvoiceResponse, error) { +func (api *api) CreateInvoice(ctx context.Context, amount uint64, description string) (*MakeInvoiceResponse, error) { if api.svc.GetLNClient() == nil { return nil, errors.New("LNClient not started") } @@ -116,6 +117,24 @@ func toApiTransaction(transaction *transactions.Transaction) *Transaction { } } +func (api *api) TopupIsolatedApp(ctx context.Context, userApp *db.App, amountMsat uint64) error { + if api.svc.GetLNClient() == nil { + return errors.New("LNClient not started") + } + if !userApp.Isolated { + return errors.New("app is not isolated") + } + + transaction, err := api.svc.GetTransactionsService().MakeInvoice(ctx, amountMsat, "top up", "", 0, nil, api.svc.GetLNClient(), &userApp.ID, nil) + + if err != nil { + return err + } + + _, err = api.svc.GetTransactionsService().SendPaymentSync(ctx, transaction.PaymentRequest, api.svc.GetLNClient(), nil, nil) + return err +} + func toApiBoostagram(boostagram *transactions.Boostagram) *Boostagram { return &Boostagram{ AppName: boostagram.AppName, diff --git a/db/db_service.go b/apps/apps_service.go similarity index 67% rename from db/db_service.go rename to apps/apps_service.go index 32753ba6..5c699d4d 100644 --- a/db/db_service.go +++ b/apps/apps_service.go @@ -1,4 +1,4 @@ -package db +package apps import ( "encoding/hex" @@ -9,6 +9,7 @@ import ( "time" "github.com/getAlby/hub/constants" + "github.com/getAlby/hub/db" "github.com/getAlby/hub/events" "github.com/getAlby/hub/logger" "github.com/nbd-wtf/go-nostr" @@ -16,19 +17,24 @@ import ( "gorm.io/gorm" ) -type dbService struct { +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) + GetAppByPubkey(pubkey string) *db.App +} + +type appsService struct { db *gorm.DB eventPublisher events.EventPublisher } -func NewDBService(db *gorm.DB, eventPublisher events.EventPublisher) *dbService { - return &dbService{ +func NewAppsService(db *gorm.DB, eventPublisher events.EventPublisher) *appsService { + return &appsService{ db: db, eventPublisher: eventPublisher, } } -func (svc *dbService) CreateApp(name string, pubkey string, maxAmountSat uint64, budgetRenewal string, expiresAt *time.Time, scopes []string, isolated bool, metadata map[string]interface{}) (*App, string, error) { +func (svc *appsService) CreateApp(name string, pubkey string, maxAmountSat uint64, budgetRenewal string, expiresAt *time.Time, scopes []string, isolated bool, metadata map[string]interface{}) (*db.App, string, error) { if isolated && (slices.Contains(scopes, constants.SIGN_MESSAGE_SCOPE)) { // cannot sign messages because the isolated app is a custodial subaccount return nil, "", errors.New("isolated app cannot have sign_message scope") @@ -59,7 +65,7 @@ func (svc *dbService) CreateApp(name string, pubkey string, maxAmountSat uint64, } } - app := App{Name: name, NostrPubkey: pairingPublicKey, Isolated: isolated, Metadata: datatypes.JSON(metadataBytes)} + app := db.App{Name: name, NostrPubkey: pairingPublicKey, Isolated: isolated, Metadata: datatypes.JSON(metadataBytes)} err := svc.db.Transaction(func(tx *gorm.DB) error { err := tx.Save(&app).Error @@ -68,7 +74,7 @@ func (svc *dbService) CreateApp(name string, pubkey string, maxAmountSat uint64, } for _, scope := range scopes { - appPermission := AppPermission{ + appPermission := db.AppPermission{ App: app, Scope: scope, ExpiresAt: expiresAt, @@ -100,3 +106,12 @@ func (svc *dbService) CreateApp(name string, pubkey string, maxAmountSat uint64, return &app, pairingSecretKey, nil } + +func (svc *appsService) GetAppByPubkey(pubkey string) *db.App { + dbApp := db.App{} + findResult := svc.db.Where("nostr_pubkey = ?", pubkey).First(&dbApp) + if findResult.RowsAffected == 0 { + return nil + } + return &dbApp +} diff --git a/db/models.go b/db/models.go index 3126ed8c..9123f327 100644 --- a/db/models.go +++ b/db/models.go @@ -86,10 +86,6 @@ type Transaction struct { FailureReason string } -type DBService interface { - CreateApp(name string, pubkey string, maxAmountSat uint64, budgetRenewal string, expiresAt *time.Time, scopes []string, isolated bool, metadata map[string]interface{}) (*App, string, error) -} - const ( REQUEST_EVENT_STATE_HANDLER_EXECUTING = "executing" REQUEST_EVENT_STATE_HANDLER_EXECUTED = "executed" diff --git a/frontend/src/components/IsolatedAppTopupDialog.tsx b/frontend/src/components/IsolatedAppTopupDialog.tsx new file mode 100644 index 00000000..e70cc3b7 --- /dev/null +++ b/frontend/src/components/IsolatedAppTopupDialog.tsx @@ -0,0 +1,90 @@ +import React from "react"; +import { + AlertDialog, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "src/components/ui/alert-dialog"; +import { Input } from "src/components/ui/input"; +import { LoadingButton } from "src/components/ui/loading-button"; +import { useToast } from "src/components/ui/use-toast"; +import { useApp } from "src/hooks/useApp"; +import { handleRequestError } from "src/utils/handleRequestError"; +import { request } from "src/utils/request"; + +type IsolatedAppTopupProps = { + appPubkey: string; +}; + +export function IsolatedAppTopupDialog({ + appPubkey, + children, +}: React.PropsWithChildren) { + const { mutate: reloadApp } = useApp(appPubkey); + const [amountSat, setAmountSat] = React.useState(""); + const [loading, setLoading] = React.useState(false); + const [open, setOpen] = React.useState(false); + const { toast } = useToast(); + async function onSubmit(e: React.FormEvent) { + e.preventDefault(); + setLoading(true); + try { + await request(`/api/apps/${appPubkey}/topup`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + amountSat: +amountSat, + }), + }); + await reloadApp(); + toast({ + title: "Successfully increased isolated app balance", + }); + setOpen(false); + } catch (error) { + handleRequestError( + toast, + "Failed to increase isolated app balance", + error + ); + } + setLoading(false); + } + return ( + + {children} + +
+ + Increase Isolated App Balance + + As the owner of your Alby Hub, you must make sure you have enough + funds in your channels for this app to make payments matching its + balance. + + { + setAmountSat(e.target.value.trim()); + }} + /> + + + Cancel + Top Up + +
+
+
+ ); +} diff --git a/frontend/src/screens/apps/AppCreated.tsx b/frontend/src/screens/apps/AppCreated.tsx index ee50e0aa..5aaf99b4 100644 --- a/frontend/src/screens/apps/AppCreated.tsx +++ b/frontend/src/screens/apps/AppCreated.tsx @@ -4,6 +4,7 @@ import { Link, Navigate, useLocation, useNavigate } from "react-router-dom"; import AppHeader from "src/components/AppHeader"; import ExternalLink from "src/components/ExternalLink"; +import { IsolatedAppTopupDialog } from "src/components/IsolatedAppTopupDialog"; import Loading from "src/components/Loading"; import QRCode from "src/components/QRCode"; import { SuggestedApp, suggestedApps } from "src/components/SuggestedAppData"; @@ -87,22 +88,36 @@ function AppCreatedInternal() { />
-

- 1. Open{" "} - {appstoreApp?.webLink ? ( - - {appstoreApp.title} - - ) : ( - "the app you wish to connect" - )}{" "} - and look for a way to attach a wallet (most apps provide this option - in settings) -

-

2. Scan or paste the connection secret

+
    +
  1. + Open{" "} + {appstoreApp?.webLink ? ( + + {appstoreApp.title} + + ) : ( + "the app you wish to connect" + )}{" "} + and look for a way to attach a wallet (most apps provide this + option in settings) +
  2. + {app?.isolated && ( +
  3. + Optional: Increase isolated balance ( + {new Intl.NumberFormat().format(Math.floor(app.balance / 1000))}{" "} + sats){" "} + + + +
  4. + )} +
  5. Scan or paste the connection secret
  6. +
{app && ( + + )} diff --git a/frontend/src/screens/internal-apps/UncleJim.tsx b/frontend/src/screens/internal-apps/UncleJim.tsx index 3f74df4f..9ad62c3a 100644 --- a/frontend/src/screens/internal-apps/UncleJim.tsx +++ b/frontend/src/screens/internal-apps/UncleJim.tsx @@ -3,6 +3,7 @@ import React from "react"; import AppHeader from "src/components/AppHeader"; import AppCard from "src/components/connections/AppCard"; import ExternalLink from "src/components/ExternalLink"; +import { IsolatedAppTopupDialog } from "src/components/IsolatedAppTopupDialog"; import { Accordion, AccordionContent, @@ -282,6 +283,21 @@ export function UncleJim() { + + {app && ( + <> +

+ {name} currently has{" "} + {new Intl.NumberFormat().format(Math.floor(app.balance / 1000))}{" "} + sats +

+ + + + + )}
)} diff --git a/http/http_service.go b/http/http_service.go index 32e61baf..2ac98882 100644 --- a/http/http_service.go +++ b/http/http_service.go @@ -16,8 +16,8 @@ import ( "github.com/sirupsen/logrus" "gorm.io/gorm" + "github.com/getAlby/hub/apps" "github.com/getAlby/hub/config" - "github.com/getAlby/hub/db" "github.com/getAlby/hub/events" "github.com/getAlby/hub/logger" "github.com/getAlby/hub/service" @@ -43,6 +43,7 @@ type HttpService struct { cfg config.Config eventPublisher events.EventPublisher db *gorm.DB + appsSvc apps.AppsService } func NewHttpService(svc service.Service, eventPublisher events.EventPublisher) *HttpService { @@ -52,6 +53,7 @@ func NewHttpService(svc service.Service, eventPublisher events.EventPublisher) * cfg: svc.GetConfig(), eventPublisher: eventPublisher, db: svc.GetDB(), + appsSvc: apps.NewAppsService(svc.GetDB(), eventPublisher), } } @@ -116,6 +118,7 @@ func (httpSvc *HttpService) RegisterSharedRoutes(e *echo.Echo) { restrictedGroup.GET("/api/apps/:pubkey", httpSvc.appsShowHandler) restrictedGroup.PATCH("/api/apps/:pubkey", httpSvc.appsUpdateHandler) restrictedGroup.DELETE("/api/apps/:pubkey", httpSvc.appsDeleteHandler) + restrictedGroup.POST("/api/apps/:pubkey/topup", httpSvc.isolatedAppTopupHandler) restrictedGroup.POST("/api/apps", httpSvc.appsCreateHandler) restrictedGroup.POST("/api/mnemonic", httpSvc.mnemonicHandler) restrictedGroup.PATCH("/api/backup-reminder", httpSvc.backupReminderHandler) @@ -756,16 +759,15 @@ func (httpSvc *HttpService) appsListHandler(c echo.Context) error { func (httpSvc *HttpService) appsShowHandler(c echo.Context) error { // TODO: move this to DB service - dbApp := db.App{} - findResult := httpSvc.db.Where("nostr_pubkey = ?", c.Param("pubkey")).First(&dbApp) + dbApp := httpSvc.appsSvc.GetAppByPubkey(c.Param("pubkey")) - if findResult.RowsAffected == 0 { + if dbApp == nil { return c.JSON(http.StatusNotFound, ErrorResponse{ - Message: "App does not exist", + Message: "App not found", }) } - response := httpSvc.api.GetApp(&dbApp) + response := httpSvc.api.GetApp(dbApp) return c.JSON(http.StatusOK, response) } @@ -778,17 +780,15 @@ func (httpSvc *HttpService) appsUpdateHandler(c echo.Context) error { }) } - // TODO: move this to DB service - dbApp := db.App{} - findResult := httpSvc.db.Where("nostr_pubkey = ?", c.Param("pubkey")).First(&dbApp) + dbApp := httpSvc.appsSvc.GetAppByPubkey(c.Param("pubkey")) - if findResult.RowsAffected == 0 { + if dbApp == nil { return c.JSON(http.StatusNotFound, ErrorResponse{ - Message: "App does not exist", + Message: "App not found", }) } - err := httpSvc.api.UpdateApp(&dbApp, &requestData) + err := httpSvc.api.UpdateApp(dbApp, &requestData) if err != nil { logger.Logger.WithError(err).Error("Failed to update app") @@ -800,28 +800,43 @@ func (httpSvc *HttpService) appsUpdateHandler(c echo.Context) error { return c.NoContent(http.StatusNoContent) } -func (httpSvc *HttpService) appsDeleteHandler(c echo.Context) error { - pubkey := c.Param("pubkey") - if pubkey == "" { +func (httpSvc *HttpService) isolatedAppTopupHandler(c echo.Context) error { + var requestData api.TopupIsolatedAppRequest + if err := c.Bind(&requestData); err != nil { return c.JSON(http.StatusBadRequest, ErrorResponse{ - Message: "Invalid pubkey parameter", + Message: fmt.Sprintf("Bad request: %s", err.Error()), }) } - // TODO: move this to DB service - dbApp := db.App{} - result := httpSvc.db.Where("nostr_pubkey = ?", pubkey).First(&dbApp) - if result.Error != nil { - if errors.Is(result.Error, gorm.ErrRecordNotFound) { - return c.JSON(http.StatusNotFound, ErrorResponse{ - Message: "App not found", - }) - } + + dbApp := httpSvc.appsSvc.GetAppByPubkey(c.Param("pubkey")) + + if dbApp == nil { + return c.JSON(http.StatusNotFound, ErrorResponse{ + Message: "App not found", + }) + } + + err := httpSvc.api.TopupIsolatedApp(c.Request().Context(), dbApp, requestData.AmountSat*1000) + + if err != nil { + logger.Logger.WithError(err).Error("Failed to topup isolated app") return c.JSON(http.StatusInternalServerError, ErrorResponse{ - Message: "Failed to fetch app", + Message: fmt.Sprintf("Failed to topup isolated app: %v", err), + }) + } + + return c.NoContent(http.StatusNoContent) +} + +func (httpSvc *HttpService) appsDeleteHandler(c echo.Context) error { + dbApp := httpSvc.appsSvc.GetAppByPubkey(c.Param("pubkey")) + if dbApp == nil { + return c.JSON(http.StatusNotFound, ErrorResponse{ + Message: "App not found", }) } - if err := httpSvc.api.DeleteApp(&dbApp); err != nil { + if err := httpSvc.api.DeleteApp(dbApp); err != nil { return c.JSON(http.StatusInternalServerError, ErrorResponse{ Message: "Failed to delete app", }) diff --git a/nip47/controllers/make_invoice_controller.go b/nip47/controllers/make_invoice_controller.go index 5f9d4e8c..bd17123d 100644 --- a/nip47/controllers/make_invoice_controller.go +++ b/nip47/controllers/make_invoice_controller.go @@ -11,10 +11,10 @@ import ( ) type makeInvoiceParams struct { - Amount int64 `json:"amount"` + Amount uint64 `json:"amount"` Description string `json:"description"` DescriptionHash string `json:"description_hash"` - Expiry int64 `json:"expiry"` + Expiry uint64 `json:"expiry"` Metadata map[string]interface{} `json:"metadata,omitempty"` } type makeInvoiceResponse struct { diff --git a/transactions/transactions_service.go b/transactions/transactions_service.go index d46d9828..b0787ec8 100644 --- a/transactions/transactions_service.go +++ b/transactions/transactions_service.go @@ -33,7 +33,7 @@ type transactionsService struct { type TransactionsService interface { events.EventSubscriber - MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64, metadata map[string]interface{}, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error) + MakeInvoice(ctx context.Context, amount uint64, description string, descriptionHash string, expiry uint64, metadata map[string]interface{}, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error) LookupTransaction(ctx context.Context, paymentHash string, transactionType *string, lnClient lnclient.LNClient, appId *uint) (*Transaction, error) ListTransactions(ctx context.Context, from, until, limit, offset uint64, unpaidOutgoing bool, unpaidIncoming bool, transactionType *string, lnClient lnclient.LNClient, appId *uint) (transactions []Transaction, err error) SendPaymentSync(ctx context.Context, payReq string, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error) @@ -105,7 +105,7 @@ func NewTransactionsService(db *gorm.DB, eventPublisher events.EventPublisher) * } } -func (svc *transactionsService) MakeInvoice(ctx context.Context, amount int64, description string, descriptionHash string, expiry int64, metadata map[string]interface{}, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error) { +func (svc *transactionsService) MakeInvoice(ctx context.Context, amount uint64, description string, descriptionHash string, expiry uint64, metadata map[string]interface{}, lnClient lnclient.LNClient, appId *uint, requestEventId *uint) (*Transaction, error) { var metadataBytes []byte if metadata != nil { var err error @@ -119,7 +119,7 @@ func (svc *transactionsService) MakeInvoice(ctx context.Context, amount int64, d } } - lnClientTransaction, err := lnClient.MakeInvoice(ctx, amount, description, descriptionHash, expiry) + lnClientTransaction, err := lnClient.MakeInvoice(ctx, int64(amount), description, descriptionHash, int64(expiry)) if err != nil { logger.Logger.WithError(err).Error("Failed to create transaction") return nil, err diff --git a/wails/wails_app.go b/wails/wails_app.go index 4425476e..d15650ba 100644 --- a/wails/wails_app.go +++ b/wails/wails_app.go @@ -6,6 +6,7 @@ import ( "log" "github.com/getAlby/hub/api" + "github.com/getAlby/hub/apps" "github.com/getAlby/hub/logger" "github.com/getAlby/hub/service" "github.com/wailsapp/wails/v2" @@ -17,17 +18,19 @@ import ( ) type WailsApp struct { - ctx context.Context - svc service.Service - api api.API - db *gorm.DB + ctx context.Context + svc service.Service + api api.API + db *gorm.DB + appsSvc apps.AppsService } func NewApp(svc service.Service) *WailsApp { return &WailsApp{ - svc: svc, - api: api.NewAPI(svc, svc.GetDB(), svc.GetConfig(), svc.GetKeys(), svc.GetAlbyOAuthSvc(), svc.GetEventPublisher()), - db: svc.GetDB(), + 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()), } } diff --git a/wails/wails_handlers.go b/wails/wails_handlers.go index 36088e2f..9fb5fcda 100644 --- a/wails/wails_handlers.go +++ b/wails/wails_handlers.go @@ -13,7 +13,6 @@ import ( "github.com/getAlby/hub/alby" "github.com/getAlby/hub/api" - "github.com/getAlby/hub/db" "github.com/getAlby/hub/logger" "github.com/wailsapp/wails/v2/pkg/runtime" ) @@ -59,18 +58,14 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string switch { case len(appMatch) > 1: pubkey := appMatch[1] - - // TODO: move this to DB service - dbApp := db.App{} - findResult := app.db.Where("nostr_pubkey = ?", pubkey).First(&dbApp) - - if findResult.RowsAffected == 0 { + dbApp := app.appsSvc.GetAppByPubkey(pubkey) + if dbApp == nil { return WailsRequestRouterResponse{Body: nil, Error: "App does not exist"} } switch method { case "GET": - app := app.api.GetApp(&dbApp) + app := app.api.GetApp(dbApp) return WailsRequestRouterResponse{Body: app, Error: ""} case "PATCH": updateAppRequest := &api.UpdateAppRequest{} @@ -83,13 +78,13 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string }).WithError(err).Error("Failed to decode request to wails router") return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } - err = app.api.UpdateApp(&dbApp, updateAppRequest) + err = app.api.UpdateApp(dbApp, updateAppRequest) if err != nil { return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } return WailsRequestRouterResponse{Body: nil, Error: ""} case "DELETE": - err := app.api.DeleteApp(&dbApp) + err := app.api.DeleteApp(dbApp) if err != nil { return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } @@ -97,6 +92,37 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string } } + appTopupRegex := regexp.MustCompile( + `/api/apps/([0-9a-f]+)/topup`, + ) + + appTopupMatch := appTopupRegex.FindStringSubmatch(route) + + switch { + case len(appTopupMatch) > 1: + pubkey := appTopupMatch[1] + dbApp := app.appsSvc.GetAppByPubkey(pubkey) + if dbApp == nil { + return WailsRequestRouterResponse{Body: nil, Error: "App does not exist"} + } + + topupIsolatedAppRequest := &api.TopupIsolatedAppRequest{} + err := json.Unmarshal([]byte(body), topupIsolatedAppRequest) + if err != nil { + logger.Logger.WithFields(logrus.Fields{ + "route": route, + "method": method, + "body": body, + }).WithError(err).Error("Failed to decode request to wails router") + return WailsRequestRouterResponse{Body: nil, Error: err.Error()} + } + err = app.api.TopupIsolatedApp(ctx, dbApp, topupIsolatedAppRequest.AmountSat*1000) + if err != nil { + return WailsRequestRouterResponse{Body: nil, Error: err.Error()} + } + return WailsRequestRouterResponse{Body: nil, Error: ""} + } + peerChannelRegex := regexp.MustCompile( `/api/peers/([^/]+)/channels/([^/]+)`, )