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:
Roland 2025-07-08 22:57:41 +07:00 committed by GitHub
parent 08f6212888
commit ec590faa49
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 598 additions and 22 deletions

View file

@ -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))
}

View file

@ -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 {

View file

@ -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

View file

@ -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"`

View file

@ -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
}

View 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 };
}

View 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,
};
}

View file

@ -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">

View file

@ -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>

View file

@ -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;

View file

@ -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 {

View file

@ -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: ""}
}
}