Skip to content

Commit 731d248

Browse files
authored
feat: increase isolated app balance (#710)
1 parent 430c9fe commit 731d248

15 files changed

+297
-89
lines changed

alby/alby_oauth_service.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"golang.org/x/oauth2"
2121
"gorm.io/gorm"
2222

23+
"github.com/getAlby/hub/apps"
2324
"github.com/getAlby/hub/config"
2425
"github.com/getAlby/hub/constants"
2526
"github.com/getAlby/hub/db"
@@ -368,9 +369,9 @@ func (svc *albyOAuthService) DrainSharedWallet(ctx context.Context, lnClient lnc
368369
10 // Alby fee reserve (10 sats)
369370

370371
if amountSat < 1 {
371-
return errors.New("Not enough balance remaining")
372+
return errors.New("not enough balance remaining")
372373
}
373-
amount := amountSat * 1000
374+
amount := uint64(amountSat * 1000)
374375

375376
logger.Logger.WithField("amount", amount).WithError(err).Error("Draining Alby shared wallet funds")
376377

@@ -526,7 +527,7 @@ func (svc *albyOAuthService) LinkAccount(ctx context.Context, lnClient lnclient.
526527
scopes = append(scopes, constants.NOTIFICATIONS_SCOPE)
527528
}
528529

529-
app, _, err := db.NewDBService(svc.db, svc.eventPublisher).CreateApp(
530+
app, _, err := apps.NewAppsService(svc.db, svc.eventPublisher).CreateApp(
530531
ALBY_ACCOUNT_APP_NAME,
531532
connectionPubkey,
532533
budget,

api/api.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"gorm.io/gorm"
1818

1919
"github.com/getAlby/hub/alby"
20+
"github.com/getAlby/hub/apps"
2021
"github.com/getAlby/hub/config"
2122
"github.com/getAlby/hub/constants"
2223
"github.com/getAlby/hub/db"
@@ -33,7 +34,7 @@ import (
3334

3435
type api struct {
3536
db *gorm.DB
36-
dbSvc db.DBService
37+
appsSvc apps.AppsService
3738
cfg config.Config
3839
svc service.Service
3940
permissionsSvc permissions.PermissionsService
@@ -46,7 +47,7 @@ type api struct {
4647
func NewAPI(svc service.Service, gormDB *gorm.DB, config config.Config, keys keys.Keys, albyOAuthSvc alby.AlbyOAuthService, eventPublisher events.EventPublisher) *api {
4748
return &api{
4849
db: gormDB,
49-
dbSvc: db.NewDBService(gormDB, eventPublisher),
50+
appsSvc: apps.NewAppsService(gormDB, eventPublisher),
5051
cfg: config,
5152
svc: svc,
5253
permissionsSvc: permissions.NewPermissionsService(gormDB, eventPublisher),
@@ -71,7 +72,7 @@ func (api *api) CreateApp(createAppRequest *CreateAppRequest) (*CreateAppRespons
7172
}
7273
}
7374

74-
app, pairingSecretKey, err := api.dbSvc.CreateApp(
75+
app, pairingSecretKey, err := api.appsSvc.CreateApp(
7576
createAppRequest.Name,
7677
createAppRequest.Pubkey,
7778
createAppRequest.MaxAmountSat,

api/models.go

+10-5
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ import (
1212

1313
type API interface {
1414
CreateApp(createAppRequest *CreateAppRequest) (*CreateAppResponse, error)
15-
UpdateApp(userApp *db.App, updateAppRequest *UpdateAppRequest) error
16-
DeleteApp(userApp *db.App) error
17-
GetApp(userApp *db.App) *App
15+
UpdateApp(app *db.App, updateAppRequest *UpdateAppRequest) error
16+
TopupIsolatedApp(ctx context.Context, app *db.App, amountMsat uint64) error
17+
DeleteApp(app *db.App) error
18+
GetApp(app *db.App) *App
1819
ListApps() ([]App, error)
1920
ListChannels(ctx context.Context) ([]Channel, error)
2021
GetChannelPeerSuggestions(ctx context.Context) ([]alby.ChannelPeerSuggestion, error)
@@ -36,7 +37,7 @@ type API interface {
3637
GetBalances(ctx context.Context) (*BalancesResponse, error)
3738
ListTransactions(ctx context.Context, limit uint64, offset uint64) (*ListTransactionsResponse, error)
3839
SendPayment(ctx context.Context, invoice string) (*SendPaymentResponse, error)
39-
CreateInvoice(ctx context.Context, amount int64, description string) (*MakeInvoiceResponse, error)
40+
CreateInvoice(ctx context.Context, amount uint64, description string) (*MakeInvoiceResponse, error)
4041
LookupInvoice(ctx context.Context, paymentHash string) (*LookupInvoiceResponse, error)
4142
RequestMempoolApi(endpoint string) (interface{}, error)
4243
GetInfo(ctx context.Context) (*InfoResponse, error)
@@ -86,6 +87,10 @@ type UpdateAppRequest struct {
8687
Metadata Metadata `json:"metadata,omitempty"`
8788
}
8889

90+
type TopupIsolatedAppRequest struct {
91+
AmountSat uint64 `json:"amountSat"`
92+
}
93+
8994
type CreateAppRequest struct {
9095
Name string `json:"name"`
9196
Pubkey string `json:"pubkey"`
@@ -283,7 +288,7 @@ type SignMessageResponse struct {
283288
}
284289

285290
type MakeInvoiceRequest struct {
286-
Amount int64 `json:"amount"`
291+
Amount uint64 `json:"amount"`
287292
Description string `json:"description"`
288293
}
289294

api/transactions.go

+20-1
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ import (
77
"strings"
88
"time"
99

10+
"github.com/getAlby/hub/db"
1011
"github.com/getAlby/hub/logger"
1112
"github.com/getAlby/hub/transactions"
1213
"github.com/sirupsen/logrus"
1314
)
1415

15-
func (api *api) CreateInvoice(ctx context.Context, amount int64, description string) (*MakeInvoiceResponse, error) {
16+
func (api *api) CreateInvoice(ctx context.Context, amount uint64, description string) (*MakeInvoiceResponse, error) {
1617
if api.svc.GetLNClient() == nil {
1718
return nil, errors.New("LNClient not started")
1819
}
@@ -116,6 +117,24 @@ func toApiTransaction(transaction *transactions.Transaction) *Transaction {
116117
}
117118
}
118119

120+
func (api *api) TopupIsolatedApp(ctx context.Context, userApp *db.App, amountMsat uint64) error {
121+
if api.svc.GetLNClient() == nil {
122+
return errors.New("LNClient not started")
123+
}
124+
if !userApp.Isolated {
125+
return errors.New("app is not isolated")
126+
}
127+
128+
transaction, err := api.svc.GetTransactionsService().MakeInvoice(ctx, amountMsat, "top up", "", 0, nil, api.svc.GetLNClient(), &userApp.ID, nil)
129+
130+
if err != nil {
131+
return err
132+
}
133+
134+
_, err = api.svc.GetTransactionsService().SendPaymentSync(ctx, transaction.PaymentRequest, api.svc.GetLNClient(), nil, nil)
135+
return err
136+
}
137+
119138
func toApiBoostagram(boostagram *transactions.Boostagram) *Boostagram {
120139
return &Boostagram{
121140
AppName: boostagram.AppName,

db/db_service.go apps/apps_service.go

+22-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package db
1+
package apps
22

33
import (
44
"encoding/hex"
@@ -9,26 +9,32 @@ import (
99
"time"
1010

1111
"github.com/getAlby/hub/constants"
12+
"github.com/getAlby/hub/db"
1213
"github.com/getAlby/hub/events"
1314
"github.com/getAlby/hub/logger"
1415
"github.com/nbd-wtf/go-nostr"
1516
"gorm.io/datatypes"
1617
"gorm.io/gorm"
1718
)
1819

19-
type dbService struct {
20+
type AppsService interface {
21+
CreateApp(name string, pubkey string, maxAmountSat uint64, budgetRenewal string, expiresAt *time.Time, scopes []string, isolated bool, metadata map[string]interface{}) (*db.App, string, error)
22+
GetAppByPubkey(pubkey string) *db.App
23+
}
24+
25+
type appsService struct {
2026
db *gorm.DB
2127
eventPublisher events.EventPublisher
2228
}
2329

24-
func NewDBService(db *gorm.DB, eventPublisher events.EventPublisher) *dbService {
25-
return &dbService{
30+
func NewAppsService(db *gorm.DB, eventPublisher events.EventPublisher) *appsService {
31+
return &appsService{
2632
db: db,
2733
eventPublisher: eventPublisher,
2834
}
2935
}
3036

31-
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) {
37+
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) {
3238
if isolated && (slices.Contains(scopes, constants.SIGN_MESSAGE_SCOPE)) {
3339
// cannot sign messages because the isolated app is a custodial subaccount
3440
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,
5965
}
6066
}
6167

62-
app := App{Name: name, NostrPubkey: pairingPublicKey, Isolated: isolated, Metadata: datatypes.JSON(metadataBytes)}
68+
app := db.App{Name: name, NostrPubkey: pairingPublicKey, Isolated: isolated, Metadata: datatypes.JSON(metadataBytes)}
6369

6470
err := svc.db.Transaction(func(tx *gorm.DB) error {
6571
err := tx.Save(&app).Error
@@ -68,7 +74,7 @@ func (svc *dbService) CreateApp(name string, pubkey string, maxAmountSat uint64,
6874
}
6975

7076
for _, scope := range scopes {
71-
appPermission := AppPermission{
77+
appPermission := db.AppPermission{
7278
App: app,
7379
Scope: scope,
7480
ExpiresAt: expiresAt,
@@ -100,3 +106,12 @@ func (svc *dbService) CreateApp(name string, pubkey string, maxAmountSat uint64,
100106

101107
return &app, pairingSecretKey, nil
102108
}
109+
110+
func (svc *appsService) GetAppByPubkey(pubkey string) *db.App {
111+
dbApp := db.App{}
112+
findResult := svc.db.Where("nostr_pubkey = ?", pubkey).First(&dbApp)
113+
if findResult.RowsAffected == 0 {
114+
return nil
115+
}
116+
return &dbApp
117+
}

db/models.go

-4
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,6 @@ type Transaction struct {
8686
FailureReason string
8787
}
8888

89-
type DBService interface {
90-
CreateApp(name string, pubkey string, maxAmountSat uint64, budgetRenewal string, expiresAt *time.Time, scopes []string, isolated bool, metadata map[string]interface{}) (*App, string, error)
91-
}
92-
9389
const (
9490
REQUEST_EVENT_STATE_HANDLER_EXECUTING = "executing"
9591
REQUEST_EVENT_STATE_HANDLER_EXECUTED = "executed"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import React from "react";
2+
import {
3+
AlertDialog,
4+
AlertDialogCancel,
5+
AlertDialogContent,
6+
AlertDialogDescription,
7+
AlertDialogFooter,
8+
AlertDialogHeader,
9+
AlertDialogTitle,
10+
AlertDialogTrigger,
11+
} from "src/components/ui/alert-dialog";
12+
import { Input } from "src/components/ui/input";
13+
import { LoadingButton } from "src/components/ui/loading-button";
14+
import { useToast } from "src/components/ui/use-toast";
15+
import { useApp } from "src/hooks/useApp";
16+
import { handleRequestError } from "src/utils/handleRequestError";
17+
import { request } from "src/utils/request";
18+
19+
type IsolatedAppTopupProps = {
20+
appPubkey: string;
21+
};
22+
23+
export function IsolatedAppTopupDialog({
24+
appPubkey,
25+
children,
26+
}: React.PropsWithChildren<IsolatedAppTopupProps>) {
27+
const { mutate: reloadApp } = useApp(appPubkey);
28+
const [amountSat, setAmountSat] = React.useState("");
29+
const [loading, setLoading] = React.useState(false);
30+
const [open, setOpen] = React.useState(false);
31+
const { toast } = useToast();
32+
async function onSubmit(e: React.FormEvent) {
33+
e.preventDefault();
34+
setLoading(true);
35+
try {
36+
await request(`/api/apps/${appPubkey}/topup`, {
37+
method: "POST",
38+
headers: {
39+
"Content-Type": "application/json",
40+
},
41+
body: JSON.stringify({
42+
amountSat: +amountSat,
43+
}),
44+
});
45+
await reloadApp();
46+
toast({
47+
title: "Successfully increased isolated app balance",
48+
});
49+
setOpen(false);
50+
} catch (error) {
51+
handleRequestError(
52+
toast,
53+
"Failed to increase isolated app balance",
54+
error
55+
);
56+
}
57+
setLoading(false);
58+
}
59+
return (
60+
<AlertDialog open={open} onOpenChange={setOpen}>
61+
<AlertDialogTrigger asChild>{children}</AlertDialogTrigger>
62+
<AlertDialogContent>
63+
<form onSubmit={onSubmit}>
64+
<AlertDialogHeader>
65+
<AlertDialogTitle>Increase Isolated App Balance</AlertDialogTitle>
66+
<AlertDialogDescription>
67+
As the owner of your Alby Hub, you must make sure you have enough
68+
funds in your channels for this app to make payments matching its
69+
balance.
70+
</AlertDialogDescription>
71+
<Input
72+
autoFocus
73+
id="amount"
74+
type="number"
75+
required
76+
value={amountSat}
77+
onChange={(e) => {
78+
setAmountSat(e.target.value.trim());
79+
}}
80+
/>
81+
</AlertDialogHeader>
82+
<AlertDialogFooter className="mt-5">
83+
<AlertDialogCancel>Cancel</AlertDialogCancel>
84+
<LoadingButton loading={loading}>Top Up</LoadingButton>
85+
</AlertDialogFooter>
86+
</form>
87+
</AlertDialogContent>
88+
</AlertDialog>
89+
);
90+
}

frontend/src/screens/apps/AppCreated.tsx

+31-16
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Link, Navigate, useLocation, useNavigate } from "react-router-dom";
44

55
import AppHeader from "src/components/AppHeader";
66
import ExternalLink from "src/components/ExternalLink";
7+
import { IsolatedAppTopupDialog } from "src/components/IsolatedAppTopupDialog";
78
import Loading from "src/components/Loading";
89
import QRCode from "src/components/QRCode";
910
import { SuggestedApp, suggestedApps } from "src/components/SuggestedAppData";
@@ -87,22 +88,36 @@ function AppCreatedInternal() {
8788
/>
8889
<div className="flex flex-col gap-3 sensitive">
8990
<div>
90-
<p>
91-
1. Open{" "}
92-
{appstoreApp?.webLink ? (
93-
<ExternalLink
94-
className="font-semibold underline"
95-
to={appstoreApp.webLink}
96-
>
97-
{appstoreApp.title}
98-
</ExternalLink>
99-
) : (
100-
"the app you wish to connect"
101-
)}{" "}
102-
and look for a way to attach a wallet (most apps provide this option
103-
in settings)
104-
</p>
105-
<p>2. Scan or paste the connection secret</p>
91+
<ol className="list-decimal list-inside">
92+
<li>
93+
Open{" "}
94+
{appstoreApp?.webLink ? (
95+
<ExternalLink
96+
className="font-semibold underline"
97+
to={appstoreApp.webLink}
98+
>
99+
{appstoreApp.title}
100+
</ExternalLink>
101+
) : (
102+
"the app you wish to connect"
103+
)}{" "}
104+
and look for a way to attach a wallet (most apps provide this
105+
option in settings)
106+
</li>
107+
{app?.isolated && (
108+
<li>
109+
Optional: Increase isolated balance (
110+
{new Intl.NumberFormat().format(Math.floor(app.balance / 1000))}{" "}
111+
sats){" "}
112+
<IsolatedAppTopupDialog appPubkey={app.nostrPubkey}>
113+
<Button size="sm" variant="secondary">
114+
Increase
115+
</Button>
116+
</IsolatedAppTopupDialog>
117+
</li>
118+
)}
119+
<li>Scan or paste the connection secret</li>
120+
</ol>
106121
</div>
107122
{app && (
108123
<ConnectAppCard

0 commit comments

Comments
 (0)