Skip to content

Commit

Permalink
feat: increase isolated app balance (#710)
Browse files Browse the repository at this point in the history
  • Loading branch information
rolznz authored Oct 28, 2024
1 parent 430c9fe commit 731d248
Show file tree
Hide file tree
Showing 15 changed files with 297 additions and 89 deletions.
7 changes: 4 additions & 3 deletions alby/alby_oauth_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -368,9 +369,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")

Expand Down Expand Up @@ -526,7 +527,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,
Expand Down
7 changes: 4 additions & 3 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,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"
Expand All @@ -33,7 +34,7 @@ import (

type api struct {
db *gorm.DB
dbSvc db.DBService
appsSvc apps.AppsService
cfg config.Config
svc service.Service
permissionsSvc permissions.PermissionsService
Expand All @@ -46,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,
dbSvc: db.NewDBService(gormDB, eventPublisher),
appsSvc: apps.NewAppsService(gormDB, eventPublisher),
cfg: config,
svc: svc,
permissionsSvc: permissions.NewPermissionsService(gormDB, eventPublisher),
Expand All @@ -71,7 +72,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,
Expand Down
15 changes: 10 additions & 5 deletions api/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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"`
Expand Down Expand Up @@ -283,7 +288,7 @@ type SignMessageResponse struct {
}

type MakeInvoiceRequest struct {
Amount int64 `json:"amount"`
Amount uint64 `json:"amount"`
Description string `json:"description"`
}

Expand Down
21 changes: 20 additions & 1 deletion api/transactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down Expand Up @@ -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,
Expand Down
29 changes: 22 additions & 7 deletions db/db_service.go → apps/apps_service.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package db
package apps

import (
"encoding/hex"
Expand All @@ -9,26 +9,32 @@ 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"
"gorm.io/datatypes"
"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")
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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
}
4 changes: 0 additions & 4 deletions db/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
90 changes: 90 additions & 0 deletions frontend/src/components/IsolatedAppTopupDialog.tsx
Original file line number Diff line number Diff line change
@@ -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<IsolatedAppTopupProps>) {
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 (
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogTrigger asChild>{children}</AlertDialogTrigger>
<AlertDialogContent>
<form onSubmit={onSubmit}>
<AlertDialogHeader>
<AlertDialogTitle>Increase Isolated App Balance</AlertDialogTitle>
<AlertDialogDescription>
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.
</AlertDialogDescription>
<Input
autoFocus
id="amount"
type="number"
required
value={amountSat}
onChange={(e) => {
setAmountSat(e.target.value.trim());
}}
/>
</AlertDialogHeader>
<AlertDialogFooter className="mt-5">
<AlertDialogCancel>Cancel</AlertDialogCancel>
<LoadingButton loading={loading}>Top Up</LoadingButton>
</AlertDialogFooter>
</form>
</AlertDialogContent>
</AlertDialog>
);
}
47 changes: 31 additions & 16 deletions frontend/src/screens/apps/AppCreated.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -87,22 +88,36 @@ function AppCreatedInternal() {
/>
<div className="flex flex-col gap-3 sensitive">
<div>
<p>
1. Open{" "}
{appstoreApp?.webLink ? (
<ExternalLink
className="font-semibold underline"
to={appstoreApp.webLink}
>
{appstoreApp.title}
</ExternalLink>
) : (
"the app you wish to connect"
)}{" "}
and look for a way to attach a wallet (most apps provide this option
in settings)
</p>
<p>2. Scan or paste the connection secret</p>
<ol className="list-decimal list-inside">
<li>
Open{" "}
{appstoreApp?.webLink ? (
<ExternalLink
className="font-semibold underline"
to={appstoreApp.webLink}
>
{appstoreApp.title}
</ExternalLink>
) : (
"the app you wish to connect"
)}{" "}
and look for a way to attach a wallet (most apps provide this
option in settings)
</li>
{app?.isolated && (
<li>
Optional: Increase isolated balance (
{new Intl.NumberFormat().format(Math.floor(app.balance / 1000))}{" "}
sats){" "}
<IsolatedAppTopupDialog appPubkey={app.nostrPubkey}>
<Button size="sm" variant="secondary">
Increase
</Button>
</IsolatedAppTopupDialog>
</li>
)}
<li>Scan or paste the connection secret</li>
</ol>
</div>
{app && (
<ConnectAppCard
Expand Down
Loading

0 comments on commit 731d248

Please sign in to comment.