mirror of
https://github.com/getAlby/hub.git
synced 2026-08-13 12:33:39 +02:00
feat: create sub-wallet lightning address (#1452)
* feat: create sub-wallet lightning address (WIP) * feat: create subwallet lightning address from show page, improve error toast * feat: add delete lightning address * chore: add wails handlers for creating and deleting lightning addresses * fix: delete request * fix: log property format * chore: use full address from response instead of hardcoding alby domain * chore: rename lightning address hooks * chore: check plan code for creating lightning address on show app page
This commit is contained in:
parent
08f6212888
commit
ec590faa49
12 changed files with 598 additions and 22 deletions
|
|
@ -288,8 +288,8 @@ func (svc *albyOAuthService) GetInfo(ctx context.Context) (*AlbyInfo, error) {
|
|||
|
||||
if res.StatusCode >= 300 {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"body": string(body),
|
||||
"statusCode": res.StatusCode,
|
||||
"body": string(body),
|
||||
"status_code": res.StatusCode,
|
||||
}).Error("info endpoint returned non-success code")
|
||||
return nil, fmt.Errorf("info endpoint returned non-success code: %s", string(body))
|
||||
}
|
||||
|
|
@ -379,13 +379,132 @@ func (svc *albyOAuthService) GetVssAuthToken(ctx context.Context, nodeIdentifier
|
|||
}
|
||||
|
||||
if vssResponse.Token == "" {
|
||||
logger.Logger.WithField("vssResponse", vssResponse).WithError(err).Error("No token in API response")
|
||||
logger.Logger.WithField("vss_response", vssResponse).WithError(err).Error("No token in API response")
|
||||
return "", errors.New("no token in vss response")
|
||||
}
|
||||
|
||||
return vssResponse.Token, nil
|
||||
}
|
||||
|
||||
func (svc *albyOAuthService) CreateLightningAddress(ctx context.Context, address string, appId uint) (*CreateLightningAddressResponse, error) {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"address": address,
|
||||
"app_id": appId,
|
||||
}).Debug("creating lightning address")
|
||||
token, err := svc.fetchUserToken(ctx)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to fetch user token")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := svc.oauthConf.Client(ctx, token)
|
||||
|
||||
type createLightningAddressRequest struct {
|
||||
Address string `json:"address"`
|
||||
AppId uint `json:"app_id"`
|
||||
}
|
||||
|
||||
body := bytes.NewBuffer([]byte{})
|
||||
payload := createLightningAddressRequest{
|
||||
Address: address,
|
||||
AppId: appId,
|
||||
}
|
||||
err = json.NewEncoder(body).Encode(&payload)
|
||||
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to encode request payload")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", fmt.Sprintf("%s/internal/lightning_addresses", albyOAuthAPIURL), body)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Error creating request for vss auth token endpoint")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
setDefaultRequestHeaders(req)
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to fetch vss auth token endpoint")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
responseBody, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to read response body")
|
||||
return nil, errors.New("failed to read response body")
|
||||
}
|
||||
|
||||
if res.StatusCode == 422 {
|
||||
type createLightningAddressErrors struct {
|
||||
Address []string `json:"address"`
|
||||
}
|
||||
lightningAddressErrors := &createLightningAddressErrors{}
|
||||
err = json.Unmarshal(responseBody, lightningAddressErrors)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("failed to unmarshal errors response")
|
||||
return nil, err
|
||||
}
|
||||
if len(lightningAddressErrors.Address) == 0 {
|
||||
return nil, errors.New("unknown error occurred")
|
||||
}
|
||||
return nil, errors.New(lightningAddressErrors.Address[0])
|
||||
}
|
||||
|
||||
if res.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("POST request to /internal/lightning_addresses/%s returned non-success status: %d %s", address, res.StatusCode, string(responseBody))
|
||||
}
|
||||
|
||||
createLightningAddressResponse := &CreateLightningAddressResponse{}
|
||||
err = json.Unmarshal(responseBody, createLightningAddressResponse)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("failed to unmarshal response")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return createLightningAddressResponse, nil
|
||||
}
|
||||
|
||||
func (svc *albyOAuthService) DeleteLightningAddress(ctx context.Context, address string) error {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"address": address,
|
||||
}).Debug("deleting lightning address")
|
||||
token, err := svc.fetchUserToken(ctx)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to fetch user token")
|
||||
return err
|
||||
}
|
||||
|
||||
client := svc.oauthConf.Client(ctx, token)
|
||||
|
||||
req, err := http.NewRequest("DELETE", fmt.Sprintf("%s/internal/lightning_addresses/%s", albyOAuthAPIURL, address), nil)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Error creating request for delete lightning address endpoint")
|
||||
return err
|
||||
}
|
||||
|
||||
setDefaultRequestHeaders(req)
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to delete lightning address endpoint")
|
||||
return err
|
||||
}
|
||||
|
||||
responseBody, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to read response body")
|
||||
return errors.New("failed to read response body")
|
||||
}
|
||||
|
||||
if res.StatusCode >= 300 {
|
||||
return fmt.Errorf("DELETE request to /internal/lightning_addresses/%s returned non-success status: %d %s", address, res.StatusCode, string(responseBody))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *albyOAuthService) GetMe(ctx context.Context) (*AlbyMe, error) {
|
||||
token, err := svc.fetchUserToken(ctx)
|
||||
if err != nil {
|
||||
|
|
@ -417,8 +536,8 @@ func (svc *albyOAuthService) GetMe(ctx context.Context) (*AlbyMe, error) {
|
|||
|
||||
if res.StatusCode >= 300 {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"body": string(body),
|
||||
"statusCode": res.StatusCode,
|
||||
"body": string(body),
|
||||
"status_code": res.StatusCode,
|
||||
}).Error("users endpoint returned non-success code")
|
||||
return nil, fmt.Errorf("users endpoint returned non-success code: %s", string(body))
|
||||
}
|
||||
|
|
@ -471,8 +590,8 @@ func (svc *albyOAuthService) GetBalance(ctx context.Context) (*AlbyBalance, erro
|
|||
|
||||
if res.StatusCode >= 300 {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"body": string(body),
|
||||
"statusCode": res.StatusCode,
|
||||
"body": string(body),
|
||||
"status_code": res.StatusCode,
|
||||
}).Error("balance endpoint returned non-success code")
|
||||
return nil, fmt.Errorf("balance endpoint returned non-success code: %s", string(body))
|
||||
}
|
||||
|
|
@ -1095,8 +1214,8 @@ func (svc *albyOAuthService) GetChannelPeerSuggestions(ctx context.Context) ([]C
|
|||
|
||||
if res.StatusCode >= 300 {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"body": string(body),
|
||||
"statusCode": res.StatusCode,
|
||||
"body": string(body),
|
||||
"status_code": res.StatusCode,
|
||||
}).Error("channel suggestions endpoint returned non-success code")
|
||||
return nil, fmt.Errorf("channel suggestions endpoint returned non-success code: %s", string(body))
|
||||
}
|
||||
|
|
@ -1149,9 +1268,9 @@ func (svc *albyOAuthService) GetBitcoinRate(ctx context.Context) (*BitcoinRate,
|
|||
|
||||
if res.StatusCode >= 300 {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"currency": currency,
|
||||
"body": string(body),
|
||||
"statusCode": res.StatusCode,
|
||||
"currency": currency,
|
||||
"body": string(body),
|
||||
"status_code": res.StatusCode,
|
||||
}).Error("Bitcoin rate endpoint returned non-success code")
|
||||
return nil, fmt.Errorf("bitcoin rate endpoint returned non-success code: %s", string(body))
|
||||
}
|
||||
|
|
@ -1275,9 +1394,9 @@ func (svc *albyOAuthService) requestAutoChannel(ctx context.Context, url string,
|
|||
|
||||
if res.StatusCode >= 300 {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"newLSPS1ChannelRequest": newAutoChannelRequest,
|
||||
"body": string(body),
|
||||
"statusCode": res.StatusCode,
|
||||
"request": newAutoChannelRequest,
|
||||
"body": string(body),
|
||||
"status_code": res.StatusCode,
|
||||
}).Error("auto channel endpoint returned non-success code")
|
||||
return nil, fmt.Errorf("auto channel endpoint returned non-success code: %s", string(body))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,13 @@ type AlbyOAuthService interface {
|
|||
RequestAutoChannel(ctx context.Context, lnClient lnclient.LNClient, isPublic bool) (*AutoChannelResponse, error)
|
||||
GetVssAuthToken(ctx context.Context, nodeIdentifier string) (string, error)
|
||||
RemoveOAuthAccessToken() error
|
||||
CreateLightningAddress(ctx context.Context, address string, appId uint) (*CreateLightningAddressResponse, error)
|
||||
DeleteLightningAddress(ctx context.Context, address string) error
|
||||
}
|
||||
|
||||
type CreateLightningAddressResponse struct {
|
||||
Address string `json:"address"`
|
||||
FullAddress string `json:"full_address"`
|
||||
}
|
||||
|
||||
type AlbyBalanceResponse struct {
|
||||
|
|
|
|||
73
api/api.go
73
api/api.go
|
|
@ -251,6 +251,79 @@ func (api *api) DeleteApp(userApp *db.App) error {
|
|||
return api.appsSvc.DeleteApp(userApp)
|
||||
}
|
||||
|
||||
func (api *api) CreateLightningAddress(ctx context.Context, createLightningAddressRequest *CreateLightningAddressRequest) error {
|
||||
app := api.appsSvc.GetAppById(createLightningAddressRequest.AppId)
|
||||
if app == nil {
|
||||
return errors.New("app not found")
|
||||
}
|
||||
|
||||
var metadata map[string]interface{}
|
||||
err := json.Unmarshal(app.Metadata, &metadata)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).WithFields(logrus.Fields{
|
||||
"app_id": app.ID,
|
||||
}).Error("Failed to deserialize app metadata")
|
||||
return err
|
||||
}
|
||||
|
||||
createLightningAddressResponse, err := api.albyOAuthSvc.CreateLightningAddress(ctx, createLightningAddressRequest.Address, createLightningAddressRequest.AppId)
|
||||
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to create lightning address for app")
|
||||
return err
|
||||
}
|
||||
|
||||
metadata["lud16"] = createLightningAddressResponse.FullAddress
|
||||
err = api.appsSvc.SetAppMetadata(app.ID, metadata)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to add lightning address to app metadata")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *api) DeleteLightningAddress(ctx context.Context, appId uint) error {
|
||||
app := api.appsSvc.GetAppById(appId)
|
||||
if app == nil {
|
||||
return errors.New("app not found")
|
||||
}
|
||||
|
||||
var metadata map[string]interface{}
|
||||
err := json.Unmarshal(app.Metadata, &metadata)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).WithFields(logrus.Fields{
|
||||
"app_id": app.ID,
|
||||
}).Error("Failed to deserialize app metadata")
|
||||
return err
|
||||
}
|
||||
|
||||
if metadata["lud16"] == nil {
|
||||
return errors.New("no lightning address set")
|
||||
}
|
||||
|
||||
lud16 := metadata["lud16"].(string)
|
||||
if !strings.Contains(lud16, "@") {
|
||||
return errors.New("invalid lightning address")
|
||||
}
|
||||
address := strings.Split(lud16, "@")[0]
|
||||
|
||||
// Call the Alby OAuth service to delete the lightning address
|
||||
err = api.albyOAuthSvc.DeleteLightningAddress(ctx, address)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to delete lightning address for app")
|
||||
return err
|
||||
}
|
||||
|
||||
delete(metadata, "lud16")
|
||||
err = api.appsSvc.SetAppMetadata(app.ID, metadata)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to remove lightning address from app metadata")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *api) GetApp(dbApp *db.App) *App {
|
||||
|
||||
var lastEvent db.RequestEvent
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ type API interface {
|
|||
DeleteApp(app *db.App) error
|
||||
GetApp(app *db.App) *App
|
||||
ListApps() ([]App, error)
|
||||
CreateLightningAddress(ctx context.Context, createLightningAddressRequest *CreateLightningAddressRequest) error
|
||||
DeleteLightningAddress(ctx context.Context, appId uint) error
|
||||
ListChannels(ctx context.Context) ([]Channel, error)
|
||||
GetChannelPeerSuggestions(ctx context.Context) ([]alby.ChannelPeerSuggestion, error)
|
||||
ResetRouter(key string) error
|
||||
|
|
@ -120,6 +122,11 @@ type CreateAppRequest struct {
|
|||
UnlockPassword string `json:"unlockPassword"`
|
||||
}
|
||||
|
||||
type CreateLightningAddressRequest struct {
|
||||
Address string `json:"address"`
|
||||
AppId uint `json:"appId"`
|
||||
}
|
||||
|
||||
type EnableAutoSwapsRequest struct {
|
||||
BalanceThreshold uint64 `json:"balanceThreshold"`
|
||||
SwapAmount uint64 `json:"swapAmount"`
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ type AppsService interface {
|
|||
CreateApp(name string, pubkey string, maxAmountSat uint64, budgetRenewal string, expiresAt *time.Time, scopes []string, isolated bool, metadata map[string]interface{}) (*db.App, string, error)
|
||||
DeleteApp(app *db.App) error
|
||||
GetAppByPubkey(pubkey string) *db.App
|
||||
GetAppById(id uint) *db.App
|
||||
SetAppMetadata(appId uint, metadata map[string]interface{}) error
|
||||
}
|
||||
|
||||
type appsService struct {
|
||||
|
|
@ -193,6 +195,15 @@ func (svc *appsService) GetAppByPubkey(pubkey string) *db.App {
|
|||
return &dbApp
|
||||
}
|
||||
|
||||
func (svc *appsService) GetAppById(id uint) *db.App {
|
||||
dbApp := db.App{}
|
||||
findResult := svc.db.Where("id = ?", id).First(&dbApp)
|
||||
if findResult.RowsAffected == 0 {
|
||||
return nil
|
||||
}
|
||||
return &dbApp
|
||||
}
|
||||
|
||||
func (svc *appsService) GetAppByName(name string) *db.App {
|
||||
dbApp := db.App{}
|
||||
findResult := svc.db.Where("name = ?", name).First(&dbApp)
|
||||
|
|
@ -201,3 +212,20 @@ func (svc *appsService) GetAppByName(name string) *db.App {
|
|||
}
|
||||
return &dbApp
|
||||
}
|
||||
|
||||
func (svc *appsService) SetAppMetadata(id uint, metadata map[string]interface{}) error {
|
||||
var metadataBytes []byte
|
||||
metadataBytes, err := json.Marshal(metadata)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to serialize metadata")
|
||||
return err
|
||||
}
|
||||
|
||||
err = svc.db.Model(&db.App{}).Where("id", id).Update("metadata", datatypes.JSON(metadataBytes)).Error
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).WithField("metadata", metadata).Error("failed to update transaction metadata")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
45
frontend/src/hooks/useCreateLightningAddress.ts
Normal file
45
frontend/src/hooks/useCreateLightningAddress.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import React from "react";
|
||||
import { useToast } from "src/components/ui/use-toast";
|
||||
import { useApp } from "src/hooks/useApp";
|
||||
import { request } from "src/utils/request";
|
||||
|
||||
export function useCreateLightningAddress(appPubkey?: string) {
|
||||
const { toast } = useToast();
|
||||
const { data: app, mutate: refetchApp } = useApp(appPubkey);
|
||||
const [creatingLightningAddress, setCreatingLightningAddress] =
|
||||
React.useState(false);
|
||||
|
||||
async function createLightningAddress(intendedLightningAddress: string) {
|
||||
try {
|
||||
if (!app) {
|
||||
throw new Error("app not found");
|
||||
}
|
||||
setCreatingLightningAddress(true);
|
||||
await request("/api/lightning-addresses", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
address: intendedLightningAddress,
|
||||
appId: app.id,
|
||||
}),
|
||||
});
|
||||
await refetchApp();
|
||||
toast({
|
||||
title: "Successfully created lightning address",
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Failed to create lightning address",
|
||||
description: (error as Error).message.replace(
|
||||
"500 ",
|
||||
""
|
||||
) /* remove 500 error code */,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
setCreatingLightningAddress(false);
|
||||
}
|
||||
return { createLightningAddress, creatingLightningAddress };
|
||||
}
|
||||
41
frontend/src/hooks/useDeleteLightningAddress.ts
Normal file
41
frontend/src/hooks/useDeleteLightningAddress.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import React from "react";
|
||||
import { useToast } from "src/components/ui/use-toast";
|
||||
import { useApp } from "src/hooks/useApp";
|
||||
import { request } from "src/utils/request";
|
||||
|
||||
export function useDeleteLightningAddress(appPubkey?: string) {
|
||||
const { toast } = useToast();
|
||||
const { data: app, mutate: refetchApp } = useApp(appPubkey);
|
||||
const [deletingLightningAddress, setDeletingLightningAddress] =
|
||||
React.useState(false);
|
||||
|
||||
async function deleteLightningAddress() {
|
||||
try {
|
||||
if (!app) {
|
||||
throw new Error("app not found");
|
||||
}
|
||||
setDeletingLightningAddress(true);
|
||||
await request(`/api/lightning-addresses/${app.id}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
await refetchApp();
|
||||
toast({
|
||||
title: "Successfully deleted lightning address",
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Failed to delete lightning address",
|
||||
description: (error as Error).message,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
setDeletingLightningAddress(false);
|
||||
}
|
||||
return {
|
||||
deleteLightningAddress,
|
||||
deletingLightningAddress,
|
||||
};
|
||||
}
|
||||
|
|
@ -40,6 +40,7 @@ import {
|
|||
CardTitle,
|
||||
} from "src/components/ui/card";
|
||||
import { Input } from "src/components/ui/input";
|
||||
import { LoadingButton } from "src/components/ui/loading-button";
|
||||
import { Table, TableBody, TableCell, TableRow } from "src/components/ui/table";
|
||||
import {
|
||||
Tooltip,
|
||||
|
|
@ -48,8 +49,12 @@ import {
|
|||
TooltipTrigger,
|
||||
} from "src/components/ui/tooltip";
|
||||
import { useToast } from "src/components/ui/use-toast";
|
||||
import { UpgradeDialog } from "src/components/UpgradeDialog";
|
||||
import { SUBWALLET_APPSTORE_APP_ID } from "src/constants";
|
||||
import { useAlbyMe } from "src/hooks/useAlbyMe";
|
||||
import { useCapabilities } from "src/hooks/useCapabilities";
|
||||
import { useCreateLightningAddress } from "src/hooks/useCreateLightningAddress";
|
||||
import { useDeleteLightningAddress } from "src/hooks/useDeleteLightningAddress";
|
||||
|
||||
function ShowApp() {
|
||||
const { pubkey } = useParams() as { pubkey: string };
|
||||
|
|
@ -85,6 +90,15 @@ function AppInternal({ app, refetchApp, capabilities }: AppInternalProps) {
|
|||
const location = useLocation();
|
||||
const [isEditingName, setIsEditingName] = React.useState(false);
|
||||
const [isEditingPermissions, setIsEditingPermissions] = React.useState(false);
|
||||
const [intendedLightningAddress, setIntendedLightningAddress] =
|
||||
React.useState("");
|
||||
const { createLightningAddress, creatingLightningAddress } =
|
||||
useCreateLightningAddress(app.appPubkey);
|
||||
const {
|
||||
deleteLightningAddress: deleteSubwalletLightningAddress,
|
||||
deletingLightningAddress,
|
||||
} = useDeleteLightningAddress(app.appPubkey);
|
||||
const { data: albyMe } = useAlbyMe();
|
||||
|
||||
React.useEffect(() => {
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
|
|
@ -284,6 +298,72 @@ function AppInternal({ app, refetchApp, capabilities }: AppInternalProps) {
|
|||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{app.isolated &&
|
||||
app.metadata?.app_store_app_id ===
|
||||
SUBWALLET_APPSTORE_APP_ID && (
|
||||
<TableRow>
|
||||
<TableCell className="font-medium">
|
||||
Lightning Address
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground break-all">
|
||||
{app.metadata.lud16}
|
||||
{!app.metadata.lud16 && (
|
||||
<div className="max-w-96 flex items-center gap-2">
|
||||
<Input
|
||||
type="text"
|
||||
value={intendedLightningAddress}
|
||||
onChange={(e) =>
|
||||
setIntendedLightningAddress(e.target.value)
|
||||
}
|
||||
required
|
||||
autoComplete="off"
|
||||
endAdornment={
|
||||
<span className="mr-1 text-muted-foreground text-xs">
|
||||
@getalby.com
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
{!albyMe?.subscription.plan_code ? (
|
||||
<UpgradeDialog>
|
||||
<Button
|
||||
className="shrink-0"
|
||||
size="lg"
|
||||
variant="secondary"
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</UpgradeDialog>
|
||||
) : (
|
||||
<LoadingButton
|
||||
className="shrink-0"
|
||||
size="lg"
|
||||
variant="secondary"
|
||||
loading={creatingLightningAddress}
|
||||
onClick={() =>
|
||||
createLightningAddress(
|
||||
intendedLightningAddress
|
||||
)
|
||||
}
|
||||
>
|
||||
Create
|
||||
</LoadingButton>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{app.metadata.lud16 && (
|
||||
<LoadingButton
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
className="ml-4"
|
||||
loading={deletingLightningAddress}
|
||||
onClick={deleteSubwalletLightningAddress}
|
||||
>
|
||||
Remove
|
||||
</LoadingButton>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
<TableRow>
|
||||
<TableCell className="font-medium">Last used</TableCell>
|
||||
<TableCell className="text-muted-foreground">
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
ExternalLinkIcon,
|
||||
TriangleAlert,
|
||||
} from "lucide-react";
|
||||
import React from "react";
|
||||
import QRCode from "react-qr-code";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import AppHeader from "src/components/AppHeader";
|
||||
|
|
@ -24,6 +25,7 @@ import { Badge } from "src/components/ui/badge";
|
|||
import { Button, ExternalLinkButton } from "src/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
|
|
@ -31,6 +33,7 @@ import {
|
|||
} from "src/components/ui/card";
|
||||
import { Input } from "src/components/ui/input";
|
||||
import { Label } from "src/components/ui/label";
|
||||
import { LoadingButton } from "src/components/ui/loading-button";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
|
|
@ -38,7 +41,10 @@ import {
|
|||
} from "src/components/ui/popover";
|
||||
import { Textarea } from "src/components/ui/textarea";
|
||||
import { useToast } from "src/components/ui/use-toast";
|
||||
import { UpgradeDialog } from "src/components/UpgradeDialog";
|
||||
import { useAlbyMe } from "src/hooks/useAlbyMe";
|
||||
import { useApp } from "src/hooks/useApp";
|
||||
import { useCreateLightningAddress } from "src/hooks/useCreateLightningAddress";
|
||||
import { useNodeConnectionInfo } from "src/hooks/useNodeConnectionInfo";
|
||||
import { copyToClipboard } from "src/lib/clipboard";
|
||||
import { ConnectAppCard } from "src/screens/apps/AppCreated";
|
||||
|
|
@ -50,8 +56,15 @@ export function SubwalletCreated() {
|
|||
|
||||
const { state } = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const createAppResponse = state as CreateAppResponse;
|
||||
const { data: app } = useApp(createAppResponse.pairingPublicKey, true);
|
||||
const createAppResponse = state as CreateAppResponse | undefined;
|
||||
const { data: app } = useApp(createAppResponse?.pairingPublicKey, true);
|
||||
const [intendedLightningAddress, setIntendedLightningAddress] =
|
||||
React.useState(createAppResponse?.name || "");
|
||||
|
||||
const { data: albyMe } = useAlbyMe();
|
||||
|
||||
const { createLightningAddress, creatingLightningAddress } =
|
||||
useCreateLightningAddress(createAppResponse?.pairingPublicKey);
|
||||
|
||||
if (!createAppResponse?.pairingUri) {
|
||||
navigate("/");
|
||||
|
|
@ -349,7 +362,7 @@ export function SubwalletCreated() {
|
|||
</Link>
|
||||
</div>
|
||||
{app && (
|
||||
<div className="col-span-2">
|
||||
<div className="col-span-2 flex flex-col gap-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{name}</CardTitle>
|
||||
|
|
@ -369,6 +382,78 @@ export function SubwalletCreated() {
|
|||
</IsolatedAppTopupDialog>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
{!app.metadata?.lud16 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Lightning address</CardTitle>
|
||||
<CardDescription>
|
||||
Create a lightning address for this sub-account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Input
|
||||
type="text"
|
||||
value={intendedLightningAddress}
|
||||
onChange={(e) =>
|
||||
setIntendedLightningAddress(e.target.value)
|
||||
}
|
||||
required
|
||||
autoComplete="off"
|
||||
endAdornment={
|
||||
<span className="mr-1 text-muted-foreground text-xs">
|
||||
@getalby.com
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-row justify-end">
|
||||
{!albyMe?.subscription.plan_code ? (
|
||||
<UpgradeDialog>
|
||||
<Button size="sm" variant="secondary">
|
||||
Create Lightning Address
|
||||
</Button>
|
||||
</UpgradeDialog>
|
||||
) : (
|
||||
<LoadingButton
|
||||
loading={creatingLightningAddress}
|
||||
onClick={() =>
|
||||
createLightningAddress(intendedLightningAddress)
|
||||
}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
>
|
||||
Create Lightning Address
|
||||
</LoadingButton>
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)}
|
||||
{app.metadata?.lud16 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Lightning address</CardTitle>
|
||||
<CardDescription>
|
||||
Your lightning address for this sub-account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="font-semibold">{app.metadata.lud16}</p>
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-row justify-end">
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (app.metadata?.lud16) {
|
||||
copyToClipboard(app.metadata.lud16, toast);
|
||||
}
|
||||
}}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
>
|
||||
Copy
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -190,10 +190,10 @@ export type HealthResponse = {
|
|||
|
||||
export type Network = "bitcoin" | "testnet" | "signet";
|
||||
|
||||
export type AppMetadata = { app_store_app_id?: string } & Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
export type AppMetadata = {
|
||||
app_store_app_id?: string;
|
||||
lud16?: string;
|
||||
} & Record<string, unknown>;
|
||||
|
||||
export type SwapsSettingsResponse = {
|
||||
enabled: boolean;
|
||||
|
|
|
|||
|
|
@ -124,6 +124,8 @@ func (httpSvc *HttpService) RegisterSharedRoutes(e *echo.Echo) {
|
|||
restrictedApiGroup.DELETE("/apps/:pubkey", httpSvc.appsDeleteHandler)
|
||||
restrictedApiGroup.POST("/apps/:pubkey/topup", httpSvc.isolatedAppTopupHandler)
|
||||
restrictedApiGroup.POST("/apps", httpSvc.appsCreateHandler)
|
||||
restrictedApiGroup.POST("/lightning-addresses", httpSvc.lightningAddressesCreateHandler)
|
||||
restrictedApiGroup.DELETE("/lightning-addresses/:appId", httpSvc.lightningAddressesDeleteHandler)
|
||||
restrictedApiGroup.POST("/mnemonic", httpSvc.mnemonicHandler)
|
||||
restrictedApiGroup.PATCH("/backup-reminder", httpSvc.backupReminderHandler)
|
||||
restrictedApiGroup.GET("/channels", httpSvc.channelsListHandler)
|
||||
|
|
@ -1003,6 +1005,52 @@ func (httpSvc *HttpService) appsCreateHandler(c echo.Context) error {
|
|||
return c.JSON(http.StatusOK, responseBody)
|
||||
}
|
||||
|
||||
func (httpSvc *HttpService) lightningAddressesCreateHandler(c echo.Context) error {
|
||||
var requestData api.CreateLightningAddressRequest
|
||||
if err := c.Bind(&requestData); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
||||
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
||||
})
|
||||
}
|
||||
|
||||
err := httpSvc.api.CreateLightningAddress(c.Request().Context(), &requestData)
|
||||
|
||||
if err != nil {
|
||||
logger.Logger.WithField("request", requestData).WithError(err).Error("Failed to create lightning address")
|
||||
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (httpSvc *HttpService) lightningAddressesDeleteHandler(c echo.Context) error {
|
||||
appIdStr := c.Param("appId")
|
||||
if appIdStr == "" {
|
||||
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
||||
Message: "App ID is required",
|
||||
})
|
||||
}
|
||||
|
||||
appId, err := strconv.ParseUint(appIdStr, 10, 64)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
||||
Message: "Invalid App ID",
|
||||
})
|
||||
}
|
||||
|
||||
err = httpSvc.api.DeleteLightningAddress(c.Request().Context(), uint(appId))
|
||||
if err != nil {
|
||||
logger.Logger.WithField("appId", appId).WithError(err).Error("Failed to delete lightning address")
|
||||
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (httpSvc *HttpService) setupHandler(c echo.Context) error {
|
||||
var setupRequest api.SetupRequest
|
||||
if err := c.Bind(&setupRequest); err != nil {
|
||||
|
|
|
|||
|
|
@ -1082,6 +1082,49 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string
|
|||
|
||||
app.api.SendEvent(sendEventRequest.Event)
|
||||
|
||||
return WailsRequestRouterResponse{Body: nil, Error: ""}
|
||||
}
|
||||
case "/api/lightning-addresses":
|
||||
switch method {
|
||||
case "POST":
|
||||
createLightningAddressRequest := &api.CreateLightningAddressRequest{}
|
||||
err := json.Unmarshal([]byte(body), createLightningAddressRequest)
|
||||
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.CreateLightningAddress(ctx, createLightningAddressRequest)
|
||||
if err != nil {
|
||||
return WailsRequestRouterResponse{Body: nil, Error: err.Error()}
|
||||
}
|
||||
return WailsRequestRouterResponse{Body: nil, Error: ""}
|
||||
}
|
||||
}
|
||||
|
||||
lightningAddressRegex := regexp.MustCompile(
|
||||
`/api/lightning-addresses/([^/]+)`,
|
||||
)
|
||||
lightningAddressMatch := lightningAddressRegex.FindStringSubmatch(route)
|
||||
|
||||
switch {
|
||||
case len(lightningAddressMatch) == 2:
|
||||
appIdStr := lightningAddressMatch[1]
|
||||
appId, err := strconv.ParseUint(appIdStr, 10, 64)
|
||||
if err != nil {
|
||||
return WailsRequestRouterResponse{Body: nil, Error: "Invalid app ID"}
|
||||
}
|
||||
|
||||
switch method {
|
||||
case "DELETE":
|
||||
err := app.api.DeleteLightningAddress(ctx, uint(appId))
|
||||
if err != nil {
|
||||
return WailsRequestRouterResponse{Body: nil, Error: err.Error()}
|
||||
}
|
||||
return WailsRequestRouterResponse{Body: nil, Error: ""}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue