mirror of
https://github.com/getAlby/hub.git
synced 2026-08-13 12:33:39 +02:00
The Wails request router included the full request body in its error log entries, and the HTTP app creation handler logged the whole request struct on failure. Log only the route, method and error instead, matching the existing behavior of the /api/mnemonic handler, and log only the route and method for requests in the desktop frontend. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1616 lines
48 KiB
Go
1616 lines
48 KiB
Go
package http
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
echojwt "github.com/labstack/echo-jwt/v4"
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/labstack/echo/v4/middleware"
|
|
"github.com/sirupsen/logrus"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/getAlby/hub/apps"
|
|
"github.com/getAlby/hub/config"
|
|
"github.com/getAlby/hub/events"
|
|
"github.com/getAlby/hub/logger"
|
|
"github.com/getAlby/hub/service"
|
|
|
|
"github.com/getAlby/hub/api"
|
|
"github.com/getAlby/hub/frontend"
|
|
)
|
|
|
|
type authTokenResponse struct {
|
|
Token string `json:"token"`
|
|
}
|
|
|
|
type jwtCustomClaims struct {
|
|
// we can add extra claims here
|
|
// Name string `json:"name"`
|
|
// Admin bool `json:"admin"`
|
|
Permission string `json:"permission,omitempty"` // "full" or "readonly"
|
|
jwt.RegisteredClaims
|
|
}
|
|
|
|
type HttpService struct {
|
|
api api.API
|
|
albyHttpSvc *AlbyHttpService
|
|
cfg config.Config
|
|
eventPublisher events.EventPublisher
|
|
db *gorm.DB
|
|
appsSvc apps.AppsService
|
|
}
|
|
|
|
func NewHttpService(svc service.Service, eventPublisher events.EventPublisher) *HttpService {
|
|
return &HttpService{
|
|
api: api.NewAPI(svc, svc.GetDB(), svc.GetConfig(), svc.GetKeys(), svc.GetAlbySvc(), svc.GetAlbyOAuthSvc(), eventPublisher),
|
|
albyHttpSvc: NewAlbyHttpService(svc, svc.GetAlbySvc(), svc.GetAlbyOAuthSvc(), svc.GetConfig().GetEnv()),
|
|
cfg: svc.GetConfig(),
|
|
eventPublisher: eventPublisher,
|
|
db: svc.GetDB(),
|
|
appsSvc: apps.NewAppsService(svc.GetDB(), eventPublisher, svc.GetKeys(), svc.GetConfig()),
|
|
}
|
|
}
|
|
|
|
func (httpSvc *HttpService) RegisterSharedRoutes(e *echo.Echo) {
|
|
e.HideBanner = true
|
|
|
|
e.Use(middleware.SecureWithConfig(middleware.SecureConfig{
|
|
ContentTypeNosniff: "nosniff",
|
|
XFrameOptions: "DENY",
|
|
// when making changes here, also update the CSP in frontend/vite.config.ts
|
|
ContentSecurityPolicy: "default-src 'self'; img-src 'self' https://uploads.getalby-assets.com https://cdn.getalby-assets.com https://getalby.com; connect-src 'self' https://api.getalby.com https://getalby.com https://zapplanner.albylabs.com wss://relay.getalby.com wss://relay2.getalby.com; frame-src https://www.youtube-nocookie.com",
|
|
ReferrerPolicy: "no-referrer",
|
|
}))
|
|
e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
|
|
LogURI: true,
|
|
LogStatus: true,
|
|
LogRemoteIP: true,
|
|
LogUserAgent: true,
|
|
LogHost: true,
|
|
LogRequestID: true,
|
|
LogValuesFunc: func(c echo.Context, values middleware.RequestLoggerValues) error {
|
|
logger.Logger.WithFields(logrus.Fields{
|
|
"uri": values.URI,
|
|
"status": values.Status,
|
|
"remote_ip": values.RemoteIP,
|
|
"user_agent": values.UserAgent,
|
|
"host": values.Host,
|
|
"request_id": values.RequestID,
|
|
}).Info("handled API request")
|
|
return nil
|
|
},
|
|
}))
|
|
|
|
e.Use(middleware.Recover())
|
|
e.Use(middleware.RequestID())
|
|
|
|
e.GET("/api/info", httpSvc.infoHandler)
|
|
e.POST("/api/setup", httpSvc.setupHandler)
|
|
e.POST("/api/restore", httpSvc.restoreBackupHandler)
|
|
|
|
// allow one unlock request per second
|
|
unlockRateLimiter := middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(1))
|
|
e.POST("/api/start", httpSvc.startHandler, unlockRateLimiter)
|
|
e.POST("/api/unlock", httpSvc.unlockHandler, unlockRateLimiter)
|
|
e.POST("/api/backup", httpSvc.createBackupHandler, unlockRateLimiter)
|
|
e.GET("/logout", httpSvc.logoutHandler, unlockRateLimiter)
|
|
|
|
frontend.RegisterHandlers(e)
|
|
|
|
// restricted routes
|
|
// Configure middleware with the custom claims type
|
|
jwtConfig := echojwt.Config{
|
|
NewClaimsFunc: func(c echo.Context) jwt.Claims {
|
|
return new(jwtCustomClaims)
|
|
},
|
|
// use a custom key func as the JWT secret will change if the user changes their unlock password
|
|
KeyFunc: func(token *jwt.Token) (interface{}, error) {
|
|
secret, err := httpSvc.cfg.GetJWTSecret()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return []byte(secret), nil
|
|
},
|
|
}
|
|
// Read-only API group - accessible to both full and readonly tokens
|
|
readOnlyApiGroup := e.Group("/api")
|
|
readOnlyApiGroup.Use(echojwt.WithConfig(jwtConfig))
|
|
|
|
readOnlyApiGroup.GET("/apps", httpSvc.appsListHandler)
|
|
readOnlyApiGroup.GET("/apps/:pubkey", httpSvc.appsShowByPubkeyHandler)
|
|
readOnlyApiGroup.GET("/v2/apps/:id", httpSvc.appsShowHandler)
|
|
readOnlyApiGroup.GET("/channels", httpSvc.channelsListHandler)
|
|
readOnlyApiGroup.GET("/channels/suggestions", httpSvc.channelPeerSuggestionsHandler)
|
|
readOnlyApiGroup.GET("/channel-offer", httpSvc.channelOfferHandler)
|
|
readOnlyApiGroup.GET("/node/connection-info", httpSvc.nodeConnectionInfoHandler)
|
|
readOnlyApiGroup.GET("/node/status", httpSvc.nodeStatusHandler)
|
|
readOnlyApiGroup.GET("/node/network-graph", httpSvc.nodeNetworkGraphHandler)
|
|
readOnlyApiGroup.GET("/node/transactions", httpSvc.listOnchainTransactionsHandler)
|
|
readOnlyApiGroup.GET("/peers", httpSvc.listPeers)
|
|
readOnlyApiGroup.GET("/wallet/address", httpSvc.onchainAddressHandler)
|
|
readOnlyApiGroup.GET("/wallet/capabilities", httpSvc.capabilitiesHandler)
|
|
readOnlyApiGroup.GET("/transactions", httpSvc.listTransactionsHandler)
|
|
readOnlyApiGroup.GET("/transactions/:paymentHash", httpSvc.lookupTransactionHandler)
|
|
readOnlyApiGroup.GET("/balances", httpSvc.balancesHandler)
|
|
readOnlyApiGroup.GET("/mempool", httpSvc.mempoolApiHandler)
|
|
readOnlyApiGroup.GET("/log/:type", httpSvc.getLogOutputHandler)
|
|
readOnlyApiGroup.GET("/health", httpSvc.healthHandler)
|
|
readOnlyApiGroup.GET("/commands", httpSvc.getCustomNodeCommandsHandler)
|
|
readOnlyApiGroup.GET("/swaps", httpSvc.listSwapsHandler)
|
|
readOnlyApiGroup.GET("/swaps/:swapId", httpSvc.lookupSwapHandler)
|
|
readOnlyApiGroup.GET("/swaps/out/info", httpSvc.getSwapOutInfoHandler)
|
|
readOnlyApiGroup.GET("/swaps/in/info", httpSvc.getSwapInInfoHandler)
|
|
readOnlyApiGroup.GET("/autoswap", httpSvc.getAutoSwapConfigHandler)
|
|
readOnlyApiGroup.GET("/forwards", httpSvc.forwardsHandler)
|
|
|
|
// Full access API group - requires a token with full permissions
|
|
fullAccessApiGroup := e.Group("/api")
|
|
fullAccessApiGroup.Use(echojwt.WithConfig(jwtConfig))
|
|
fullAccessApiGroup.Use(httpSvc.requireFullAccess)
|
|
|
|
fullAccessApiGroup.POST("/event", httpSvc.eventHandler)
|
|
fullAccessApiGroup.PATCH("/unlock-password", httpSvc.changeUnlockPasswordHandler)
|
|
fullAccessApiGroup.PATCH("/auto-unlock", httpSvc.autoUnlockHandler)
|
|
fullAccessApiGroup.PATCH("/settings", httpSvc.updateSettingsHandler)
|
|
fullAccessApiGroup.PATCH("/apps/:pubkey", httpSvc.appsUpdateHandler)
|
|
fullAccessApiGroup.PATCH("/transactions/:id/labels", httpSvc.setTransactionUserLabelsHandler)
|
|
fullAccessApiGroup.DELETE("/apps/:pubkey", httpSvc.appsDeleteHandler)
|
|
fullAccessApiGroup.POST("/transfers", httpSvc.transfersHandler)
|
|
fullAccessApiGroup.POST("/apps", httpSvc.appsCreateHandler)
|
|
fullAccessApiGroup.POST("/lightning-addresses", httpSvc.lightningAddressesCreateHandler)
|
|
fullAccessApiGroup.DELETE("/lightning-addresses/:appId", httpSvc.lightningAddressesDeleteHandler)
|
|
fullAccessApiGroup.POST("/mnemonic", httpSvc.mnemonicHandler)
|
|
fullAccessApiGroup.PATCH("/backup-reminder", httpSvc.backupReminderHandler)
|
|
fullAccessApiGroup.POST("/channels", httpSvc.openChannelHandler)
|
|
fullAccessApiGroup.POST("/channels/rebalance", httpSvc.rebalanceChannelHandler)
|
|
fullAccessApiGroup.POST("/lsp-orders", httpSvc.newInstantChannelInvoiceHandler)
|
|
fullAccessApiGroup.POST("/node/migrate-storage", httpSvc.migrateNodeStorageHandler)
|
|
fullAccessApiGroup.POST("/peers", httpSvc.connectPeerHandler)
|
|
fullAccessApiGroup.DELETE("/peers/:peerId", httpSvc.disconnectPeerHandler)
|
|
fullAccessApiGroup.DELETE("/peers/:peerId/channels/:channelId", httpSvc.closeChannelHandler)
|
|
fullAccessApiGroup.PATCH("/peers/:peerId/channels/:channelId", httpSvc.updateChannelHandler)
|
|
fullAccessApiGroup.POST("/wallet/new-address", httpSvc.newOnchainAddressHandler)
|
|
fullAccessApiGroup.POST("/wallet/redeem-onchain-funds", httpSvc.redeemOnchainFundsHandler)
|
|
fullAccessApiGroup.POST("/wallet/sign-message", httpSvc.signMessageHandler)
|
|
fullAccessApiGroup.POST("/wallet/sync", httpSvc.walletSyncHandler)
|
|
fullAccessApiGroup.POST("/payments/:invoice", httpSvc.sendPaymentHandler)
|
|
fullAccessApiGroup.POST("/invoices", httpSvc.makeInvoiceHandler)
|
|
fullAccessApiGroup.POST("/offers", httpSvc.makeOfferHandler)
|
|
fullAccessApiGroup.POST("/reset-router", httpSvc.resetRouterHandler)
|
|
fullAccessApiGroup.POST("/stop", httpSvc.stopHandler)
|
|
fullAccessApiGroup.POST("/command", httpSvc.execCustomNodeCommandHandler)
|
|
fullAccessApiGroup.POST("/swaps/out", httpSvc.initiateSwapOutHandler)
|
|
fullAccessApiGroup.POST("/swaps/in", httpSvc.initiateSwapInHandler)
|
|
fullAccessApiGroup.POST("/swaps/refund", httpSvc.refundSwapHandler)
|
|
fullAccessApiGroup.GET("/swaps/mnemonic", httpSvc.swapMnemonicHandler)
|
|
fullAccessApiGroup.POST("/autoswap", httpSvc.enableAutoSwapOutHandler)
|
|
fullAccessApiGroup.DELETE("/autoswap", httpSvc.disableAutoSwapOutHandler)
|
|
fullAccessApiGroup.POST("/node/alias", httpSvc.setNodeAliasHandler)
|
|
|
|
httpSvc.albyHttpSvc.RegisterSharedRoutes(readOnlyApiGroup, fullAccessApiGroup, e)
|
|
}
|
|
|
|
func (httpSvc *HttpService) infoHandler(c echo.Context) error {
|
|
responseBody, err := httpSvc.api.GetInfo(c.Request().Context())
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: err.Error(),
|
|
})
|
|
}
|
|
|
|
authHeader := c.Request().Header.Get("Authorization")
|
|
if authHeader != "" {
|
|
parts := strings.Split(authHeader, " ")
|
|
if parts[0] == "Bearer" {
|
|
tokenString := parts[1]
|
|
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
|
secret, err := httpSvc.cfg.GetJWTSecret()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return []byte(secret), nil
|
|
})
|
|
if err != nil {
|
|
logger.Logger.WithError(err).Error("failed to parse token")
|
|
}
|
|
responseBody.Unlocked = err == nil && token != nil && token.Valid
|
|
}
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, responseBody)
|
|
}
|
|
|
|
func (httpSvc *HttpService) eventHandler(c echo.Context) error {
|
|
var sendEventRequest api.SendEventRequest
|
|
if err := c.Bind(&sendEventRequest); err != nil {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
httpSvc.api.SendEvent(sendEventRequest.Event, sendEventRequest.Properties)
|
|
|
|
return c.NoContent(http.StatusOK)
|
|
}
|
|
|
|
func (httpSvc *HttpService) mnemonicHandler(c echo.Context) error {
|
|
var mnemonicRequest api.MnemonicRequest
|
|
if err := c.Bind(&mnemonicRequest); err != nil {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
responseBody, err := httpSvc.api.GetMnemonic(mnemonicRequest.UnlockPassword)
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, responseBody)
|
|
}
|
|
|
|
func (httpSvc *HttpService) backupReminderHandler(c echo.Context) error {
|
|
var backupReminderRequest api.BackupReminderRequest
|
|
if err := c.Bind(&backupReminderRequest); err != nil {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
err := httpSvc.api.SetNextBackupReminder(&backupReminderRequest)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to store backup reminder: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
func (httpSvc *HttpService) startHandler(c echo.Context) error {
|
|
var startRequest api.StartRequest
|
|
if err := c.Bind(&startRequest); err != nil {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
if !httpSvc.cfg.CheckUnlockPassword(startRequest.UnlockPassword) {
|
|
return c.JSON(http.StatusUnauthorized, ErrorResponse{
|
|
Message: "Invalid password",
|
|
})
|
|
}
|
|
|
|
err := httpSvc.cfg.LoadJWTSecret(startRequest.UnlockPassword)
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to load JWT secret: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
token, err := httpSvc.createJWT(nil, "full")
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to save session: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
go httpSvc.api.Start(&startRequest)
|
|
|
|
return c.JSON(http.StatusOK, &authTokenResponse{
|
|
Token: token,
|
|
})
|
|
}
|
|
|
|
func (httpSvc *HttpService) unlockHandler(c echo.Context) error {
|
|
var unlockRequest api.UnlockRequest
|
|
if err := c.Bind(&unlockRequest); err != nil {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
if !httpSvc.cfg.CheckUnlockPassword(unlockRequest.UnlockPassword) {
|
|
return c.JSON(http.StatusUnauthorized, ErrorResponse{
|
|
Message: "Invalid password",
|
|
})
|
|
}
|
|
|
|
if unlockRequest.Permission == "" {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: "Permission field is required",
|
|
})
|
|
}
|
|
|
|
if !slices.Contains([]string{"full", "readonly"}, unlockRequest.Permission) {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: "Permission field is unknown",
|
|
})
|
|
}
|
|
|
|
_, err := httpSvc.api.GetNodeStatus(c.Request().Context())
|
|
if err != nil {
|
|
if errors.Is(err, api.ErrLNClientNotStarted) {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: "Node is not running, start it before unlocking.",
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: err.Error(),
|
|
})
|
|
}
|
|
|
|
token, err := httpSvc.createJWT(unlockRequest.TokenExpiryDays, unlockRequest.Permission)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to save session: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
httpSvc.eventPublisher.Publish(&events.Event{
|
|
Event: "nwc_unlocked",
|
|
})
|
|
|
|
return c.JSON(http.StatusOK, &authTokenResponse{
|
|
Token: token,
|
|
})
|
|
}
|
|
|
|
func (httpSvc *HttpService) requireFullAccess(next echo.HandlerFunc) echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
token := c.Get("user").(*jwt.Token)
|
|
claims := token.Claims.(*jwtCustomClaims)
|
|
|
|
// Allow if no permission specified (backward compatibility) or if full access
|
|
if claims.Permission == "" || claims.Permission == "full" {
|
|
return next(c)
|
|
}
|
|
|
|
return c.JSON(http.StatusForbidden, ErrorResponse{
|
|
Message: "This operation requires full access permissions",
|
|
})
|
|
}
|
|
}
|
|
|
|
func (httpSvc *HttpService) changeUnlockPasswordHandler(c echo.Context) error {
|
|
var changeUnlockPasswordRequest api.ChangeUnlockPasswordRequest
|
|
if err := c.Bind(&changeUnlockPasswordRequest); err != nil {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
err := httpSvc.api.ChangeUnlockPassword(&changeUnlockPasswordRequest)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to change unlock password: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
func (httpSvc *HttpService) updateSettingsHandler(c echo.Context) error {
|
|
var updateSettingsRequest api.UpdateSettingsRequest
|
|
if err := c.Bind(&updateSettingsRequest); err != nil {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
err := httpSvc.api.UpdateSettings(&updateSettingsRequest)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to update settings: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
func (httpSvc *HttpService) autoUnlockHandler(c echo.Context) error {
|
|
var autoUnlockRequest api.AutoUnlockRequest
|
|
if err := c.Bind(&autoUnlockRequest); err != nil {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
err := httpSvc.api.SetAutoUnlockPassword(autoUnlockRequest.UnlockPassword)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to set auto unlock password: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
func (httpSvc *HttpService) createJWT(tokenExpiryDays *uint64, permission string) (string, error) {
|
|
if !slices.Contains([]string{"full", "readonly"}, permission) {
|
|
return "", errors.New("invalid token permission")
|
|
}
|
|
|
|
expiryDays := uint64(30)
|
|
if tokenExpiryDays != nil {
|
|
expiryDays = *tokenExpiryDays
|
|
}
|
|
|
|
// Set custom claims
|
|
claims := &jwtCustomClaims{
|
|
Permission: permission,
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24 * time.Duration(expiryDays))),
|
|
},
|
|
}
|
|
|
|
// Create token with claims
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
|
|
if token == nil {
|
|
return "", errors.New("failed to create token")
|
|
}
|
|
|
|
secret, err := httpSvc.cfg.GetJWTSecret()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
signed, err := token.SignedString([]byte(secret))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return signed, nil
|
|
}
|
|
|
|
func (httpSvc *HttpService) channelsListHandler(c echo.Context) error {
|
|
ctx := c.Request().Context()
|
|
|
|
channels, err := httpSvc.api.ListChannels(ctx)
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, channels)
|
|
}
|
|
|
|
func (httpSvc *HttpService) channelPeerSuggestionsHandler(c echo.Context) error {
|
|
ctx := c.Request().Context()
|
|
|
|
suggestions, err := httpSvc.api.GetChannelPeerSuggestions(ctx)
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, suggestions)
|
|
}
|
|
|
|
func (httpSvc *HttpService) channelOfferHandler(c echo.Context) error {
|
|
ctx := c.Request().Context()
|
|
|
|
suggestions, err := httpSvc.api.GetLSPChannelOffer(ctx)
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, suggestions)
|
|
}
|
|
|
|
func (httpSvc *HttpService) resetRouterHandler(c echo.Context) error {
|
|
var resetRouterRequest api.ResetRouterRequest
|
|
if err := c.Bind(&resetRouterRequest); err != nil {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
err := httpSvc.api.ResetRouter(resetRouterRequest.Key)
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
func (httpSvc *HttpService) stopHandler(c echo.Context) error {
|
|
|
|
err := httpSvc.api.Stop()
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
func (httpSvc *HttpService) nodeConnectionInfoHandler(c echo.Context) error {
|
|
ctx := c.Request().Context()
|
|
|
|
info, err := httpSvc.api.GetNodeConnectionInfo(ctx)
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, info)
|
|
}
|
|
|
|
func (httpSvc *HttpService) nodeStatusHandler(c echo.Context) error {
|
|
ctx := c.Request().Context()
|
|
|
|
info, err := httpSvc.api.GetNodeStatus(ctx)
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, info)
|
|
}
|
|
|
|
func (httpSvc *HttpService) nodeNetworkGraphHandler(c echo.Context) error {
|
|
ctx := c.Request().Context()
|
|
|
|
nodeIds := strings.Split(c.QueryParam("nodeIds"), ",")
|
|
|
|
info, err := httpSvc.api.GetNetworkGraph(ctx, nodeIds)
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, info)
|
|
}
|
|
|
|
func (httpSvc *HttpService) migrateNodeStorageHandler(c echo.Context) error {
|
|
ctx := c.Request().Context()
|
|
var migrateNodeStorageRequest api.MigrateNodeStorageRequest
|
|
if err := c.Bind(&migrateNodeStorageRequest); err != nil {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
err := httpSvc.api.MigrateNodeStorage(ctx, migrateNodeStorageRequest.To)
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
func (httpSvc *HttpService) balancesHandler(c echo.Context) error {
|
|
ctx := c.Request().Context()
|
|
|
|
balances, err := httpSvc.api.GetBalances(ctx)
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, balances)
|
|
}
|
|
|
|
func (httpSvc *HttpService) sendPaymentHandler(c echo.Context) error {
|
|
ctx := c.Request().Context()
|
|
|
|
var payInvoiceRequest api.PayInvoiceRequest
|
|
if err := c.Bind(&payInvoiceRequest); err != nil {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
|
})
|
|
}
|
|
amountMsat := api.ResolveToMsat(payInvoiceRequest.AmountSat, payInvoiceRequest.AmountMsat, nil, payInvoiceRequest.Amount)
|
|
|
|
paymentResponse, err := httpSvc.api.SendPayment(ctx, c.Param("invoice"), amountMsat, payInvoiceRequest.Metadata, payInvoiceRequest.FromAppID)
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, paymentResponse)
|
|
}
|
|
|
|
func (httpSvc *HttpService) makeOfferHandler(c echo.Context) error {
|
|
ctx := c.Request().Context()
|
|
|
|
var makeOfferRequest api.MakeOfferRequest
|
|
if err := c.Bind(&makeOfferRequest); err != nil {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
offer, err := httpSvc.api.MakeOffer(ctx, makeOfferRequest.Description)
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to generate BOLT-12 offer: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, offer)
|
|
}
|
|
|
|
func (httpSvc *HttpService) makeInvoiceHandler(c echo.Context) error {
|
|
var makeInvoiceRequest api.MakeInvoiceRequest
|
|
if err := c.Bind(&makeInvoiceRequest); err != nil {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
amountMsat := uint64(0)
|
|
resolvedAmountMsat := api.ResolveToMsat(makeInvoiceRequest.AmountSat, makeInvoiceRequest.AmountMsat, nil, makeInvoiceRequest.Amount)
|
|
if resolvedAmountMsat != nil {
|
|
amountMsat = *resolvedAmountMsat
|
|
}
|
|
|
|
invoice, err := httpSvc.api.CreateInvoice(c.Request().Context(), amountMsat, makeInvoiceRequest.Description, makeInvoiceRequest.ToAppID)
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, invoice)
|
|
}
|
|
|
|
func (httpSvc *HttpService) lookupTransactionHandler(c echo.Context) error {
|
|
ctx := c.Request().Context()
|
|
|
|
transaction, err := httpSvc.api.LookupInvoice(ctx, c.Param("paymentHash"))
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, transaction)
|
|
}
|
|
|
|
func (httpSvc *HttpService) setTransactionUserLabelsHandler(c echo.Context) error {
|
|
ctx := c.Request().Context()
|
|
|
|
var requestData api.SetTransactionUserLabelsRequest
|
|
if err := c.Bind(&requestData); err != nil {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
transactionID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: "Invalid transaction ID",
|
|
})
|
|
}
|
|
|
|
err = httpSvc.api.SetTransactionUserLabels(ctx, uint(transactionID), requestData.Labels)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
func (httpSvc *HttpService) listTransactionsHandler(c echo.Context) error {
|
|
ctx := c.Request().Context()
|
|
|
|
limit := uint64(20)
|
|
offset := uint64(0)
|
|
var appId *uint
|
|
|
|
if limitParam := c.QueryParam("limit"); limitParam != "" {
|
|
if parsedLimit, err := strconv.ParseUint(limitParam, 10, 64); err == nil {
|
|
limit = parsedLimit
|
|
}
|
|
}
|
|
|
|
if offsetParam := c.QueryParam("offset"); offsetParam != "" {
|
|
if parsedOffset, err := strconv.ParseUint(offsetParam, 10, 64); err == nil {
|
|
offset = parsedOffset
|
|
}
|
|
}
|
|
|
|
if appIdParam := c.QueryParam("appId"); appIdParam != "" {
|
|
if parsedAppId, err := strconv.ParseUint(appIdParam, 10, 64); err == nil {
|
|
var unsignedAppId = uint(parsedAppId)
|
|
appId = &unsignedAppId
|
|
}
|
|
}
|
|
|
|
filters, err := api.ParseListTransactionsFilters(c.QueryParams())
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: err.Error(),
|
|
})
|
|
}
|
|
|
|
transactions, err := httpSvc.api.ListTransactions(ctx, appId, limit, offset, filters)
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, transactions)
|
|
}
|
|
|
|
func (httpSvc *HttpService) listOnchainTransactionsHandler(c echo.Context) error {
|
|
ctx := c.Request().Context()
|
|
|
|
transactions, err := httpSvc.api.ListOnchainTransactions(ctx)
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, transactions)
|
|
}
|
|
|
|
func (httpSvc *HttpService) walletSyncHandler(c echo.Context) error {
|
|
httpSvc.api.SyncWallet()
|
|
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
func (httpSvc *HttpService) mempoolApiHandler(c echo.Context) error {
|
|
endpoint := c.QueryParam("endpoint")
|
|
if endpoint == "" {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: "Invalid pubkey parameter",
|
|
})
|
|
}
|
|
|
|
response, err := httpSvc.api.RequestMempoolApi(c.Request().Context(), endpoint)
|
|
if err != nil {
|
|
logger.Logger.WithField("endpoint", endpoint).WithError(err).Error("Failed to request mempool API")
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to request mempool API: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, response)
|
|
}
|
|
|
|
func (httpSvc *HttpService) capabilitiesHandler(c echo.Context) error {
|
|
response, err := httpSvc.api.GetWalletCapabilities(c.Request().Context())
|
|
if err != nil {
|
|
logger.Logger.WithError(err).Error("Failed to request wallet capabilities")
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to request wallet capabilities: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, response)
|
|
}
|
|
|
|
func (httpSvc *HttpService) listPeers(c echo.Context) error {
|
|
peers, err := httpSvc.api.ListPeers(c.Request().Context())
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to list peers: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, peers)
|
|
}
|
|
|
|
func (httpSvc *HttpService) connectPeerHandler(c echo.Context) error {
|
|
ctx := c.Request().Context()
|
|
|
|
var connectPeerRequest api.ConnectPeerRequest
|
|
if err := c.Bind(&connectPeerRequest); err != nil {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
err := httpSvc.api.ConnectPeer(ctx, &connectPeerRequest)
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to connect peer: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
func (httpSvc *HttpService) openChannelHandler(c echo.Context) error {
|
|
ctx := c.Request().Context()
|
|
|
|
var openChannelRequest api.OpenChannelRequest
|
|
if err := c.Bind(&openChannelRequest); err != nil {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
openChannelResponse, err := httpSvc.api.OpenChannel(ctx, &openChannelRequest)
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to open channel: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, openChannelResponse)
|
|
}
|
|
|
|
func (httpSvc *HttpService) rebalanceChannelHandler(c echo.Context) error {
|
|
ctx := c.Request().Context()
|
|
|
|
var rebalanceChannelRequest api.RebalanceChannelRequest
|
|
if err := c.Bind(&rebalanceChannelRequest); err != nil {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
rebalanceChannelResponse, err := httpSvc.api.RebalanceChannel(ctx, &rebalanceChannelRequest)
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to rebalance channel: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, rebalanceChannelResponse)
|
|
}
|
|
|
|
func (httpSvc *HttpService) disconnectPeerHandler(c echo.Context) error {
|
|
ctx := c.Request().Context()
|
|
|
|
err := httpSvc.api.DisconnectPeer(ctx, c.Param("peerId"))
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to disconnect peer: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
func (httpSvc *HttpService) closeChannelHandler(c echo.Context) error {
|
|
ctx := c.Request().Context()
|
|
|
|
closeChannelResponse, err := httpSvc.api.CloseChannel(ctx, c.Param("peerId"), c.Param("channelId"), c.QueryParam("force") == "true")
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to close channel: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, closeChannelResponse)
|
|
}
|
|
|
|
func (httpSvc *HttpService) updateChannelHandler(c echo.Context) error {
|
|
ctx := c.Request().Context()
|
|
|
|
var updateChannelRequest api.UpdateChannelRequest
|
|
if err := c.Bind(&updateChannelRequest); err != nil {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
updateChannelRequest.NodeId = c.Param("peerId")
|
|
updateChannelRequest.ChannelId = c.Param("channelId")
|
|
|
|
err := httpSvc.api.UpdateChannel(ctx, &updateChannelRequest)
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to update channel: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
func (httpSvc *HttpService) newInstantChannelInvoiceHandler(c echo.Context) error {
|
|
ctx := c.Request().Context()
|
|
|
|
var newWrappedInvoiceRequest api.LSPOrderRequest
|
|
if err := c.Bind(&newWrappedInvoiceRequest); err != nil {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
newLSPOrderResponse, err := httpSvc.api.RequestLSPOrder(ctx, &newWrappedInvoiceRequest)
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to request new channel order from LSP: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, newLSPOrderResponse)
|
|
}
|
|
|
|
func (httpSvc *HttpService) onchainAddressHandler(c echo.Context) error {
|
|
ctx := c.Request().Context()
|
|
|
|
address, err := httpSvc.api.GetUnusedOnchainAddress(ctx)
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to request new onchain address: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, address)
|
|
}
|
|
|
|
func (httpSvc *HttpService) newOnchainAddressHandler(c echo.Context) error {
|
|
ctx := c.Request().Context()
|
|
|
|
address, err := httpSvc.api.GetNewOnchainAddress(ctx)
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to request new onchain address: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, address)
|
|
}
|
|
|
|
func (httpSvc *HttpService) redeemOnchainFundsHandler(c echo.Context) error {
|
|
ctx := c.Request().Context()
|
|
|
|
var redeemOnchainFundsRequest api.RedeemOnchainFundsRequest
|
|
if err := c.Bind(&redeemOnchainFundsRequest); err != nil {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
amountSat := uint64(0)
|
|
resolvedAmountSat := api.ResolveToSat(redeemOnchainFundsRequest.AmountSat, nil, redeemOnchainFundsRequest.Amount, nil)
|
|
if resolvedAmountSat != nil {
|
|
amountSat = *resolvedAmountSat
|
|
}
|
|
|
|
redeemOnchainFundsResponse, err := httpSvc.api.RedeemOnchainFunds(ctx, redeemOnchainFundsRequest.ToAddress, amountSat, redeemOnchainFundsRequest.FeeRate, redeemOnchainFundsRequest.SendAll)
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to redeem onchain funds: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, redeemOnchainFundsResponse)
|
|
}
|
|
|
|
func (httpSvc *HttpService) signMessageHandler(c echo.Context) error {
|
|
ctx := c.Request().Context()
|
|
|
|
var signMessageRequest api.SignMessageRequest
|
|
if err := c.Bind(&signMessageRequest); err != nil {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
signMessageResponse, err := httpSvc.api.SignMessage(ctx, signMessageRequest.Message)
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to sign message: %s", err.Error()),
|
|
})
|
|
}
|
|
return c.JSON(http.StatusOK, signMessageResponse)
|
|
}
|
|
|
|
func (httpSvc *HttpService) appsListHandler(c echo.Context) error {
|
|
limit := uint64(0)
|
|
offset := uint64(0)
|
|
|
|
if limitParam := c.QueryParam("limit"); limitParam != "" {
|
|
if parsedLimit, err := strconv.ParseUint(limitParam, 10, 64); err == nil {
|
|
limit = parsedLimit
|
|
}
|
|
}
|
|
|
|
if offsetParam := c.QueryParam("offset"); offsetParam != "" {
|
|
if parsedOffset, err := strconv.ParseUint(offsetParam, 10, 64); err == nil {
|
|
offset = parsedOffset
|
|
}
|
|
}
|
|
|
|
filtersJSON := c.QueryParam("filters")
|
|
var filters api.ListAppsFilters
|
|
if filtersJSON != "" {
|
|
err := json.Unmarshal([]byte(filtersJSON), &filters)
|
|
if err != nil {
|
|
logger.Logger.WithError(err).WithFields(logrus.Fields{
|
|
"filters": filtersJSON,
|
|
}).Error("Failed to deserialize app filters")
|
|
return err
|
|
}
|
|
}
|
|
|
|
orderBy := c.QueryParam("order_by")
|
|
|
|
apps, err := httpSvc.api.ListApps(limit, offset, filters, orderBy)
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, apps)
|
|
}
|
|
|
|
func (httpSvc *HttpService) appsShowByPubkeyHandler(c echo.Context) error {
|
|
dbApp := httpSvc.appsSvc.GetAppByPubkey(c.Param("pubkey"))
|
|
|
|
if dbApp == nil {
|
|
return c.JSON(http.StatusNotFound, ErrorResponse{
|
|
Message: "App not found",
|
|
})
|
|
}
|
|
|
|
response, err := httpSvc.api.GetApp(dbApp)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, response)
|
|
}
|
|
|
|
func (httpSvc *HttpService) appsShowHandler(c echo.Context) error {
|
|
appIdStr := c.Param("id")
|
|
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",
|
|
})
|
|
}
|
|
|
|
dbApp := httpSvc.appsSvc.GetAppById(uint(appId))
|
|
|
|
if dbApp == nil {
|
|
return c.JSON(http.StatusNotFound, ErrorResponse{
|
|
Message: "App not found",
|
|
})
|
|
}
|
|
|
|
response, err := httpSvc.api.GetApp(dbApp)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, response)
|
|
}
|
|
|
|
func (httpSvc *HttpService) appsUpdateHandler(c echo.Context) error {
|
|
var requestData api.UpdateAppRequest
|
|
if err := c.Bind(&requestData); err != nil {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
dbApp := httpSvc.appsSvc.GetAppByPubkey(c.Param("pubkey"))
|
|
|
|
if dbApp == nil {
|
|
return c.JSON(http.StatusNotFound, ErrorResponse{
|
|
Message: "App not found",
|
|
})
|
|
}
|
|
|
|
err := httpSvc.api.UpdateApp(dbApp, &requestData)
|
|
|
|
if err != nil {
|
|
logger.Logger.WithError(err).Error("Failed to update app")
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to update app: %v", err),
|
|
})
|
|
}
|
|
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
func (httpSvc *HttpService) transfersHandler(c echo.Context) error {
|
|
var requestData api.TransferRequest
|
|
if err := c.Bind(&requestData); err != nil {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
amountMsat := uint64(0)
|
|
resolvedAmountMsat := api.ResolveToMsat(requestData.AmountSat, requestData.AmountMsat, nil, nil)
|
|
if resolvedAmountMsat != nil {
|
|
amountMsat = *resolvedAmountMsat
|
|
}
|
|
|
|
err := httpSvc.api.Transfer(c.Request().Context(), requestData.FromAppId, requestData.ToAppId, amountMsat, requestData.Description)
|
|
|
|
if err != nil {
|
|
logger.Logger.WithError(err).Error("Failed to transfer funds")
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to transfer funds: %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 {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: "Failed to delete app",
|
|
})
|
|
}
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
func (httpSvc *HttpService) appsCreateHandler(c echo.Context) error {
|
|
var requestData api.CreateAppRequest
|
|
if err := c.Bind(&requestData); err != nil {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
responseBody, err := httpSvc.api.CreateApp(&requestData)
|
|
|
|
if err != nil {
|
|
logger.Logger.WithField("appName", requestData.Name).WithError(err).Error("Failed to save app")
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to save app: %v", err),
|
|
})
|
|
}
|
|
|
|
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 {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
err := httpSvc.api.Setup(c.Request().Context(), &setupRequest)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to setup node: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
func (httpSvc *HttpService) getLogOutputHandler(c echo.Context) error {
|
|
var getLogRequest api.GetLogOutputRequest
|
|
if err := c.Bind(&getLogRequest); err != nil {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
logType := c.Param("type")
|
|
if logType != api.LogTypeNode && logType != api.LogTypeApp {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: fmt.Sprintf("Invalid log type parameter: '%s'", logType),
|
|
})
|
|
}
|
|
|
|
getLogResponse, err := httpSvc.api.GetLogOutput(c.Request().Context(), logType, &getLogRequest)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to get log output: %v", err),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, getLogResponse)
|
|
}
|
|
|
|
func (httpSvc *HttpService) getCustomNodeCommandsHandler(c echo.Context) error {
|
|
nodeCommandsResponse, err := httpSvc.api.GetCustomNodeCommands()
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to get node commands: %v", err),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, nodeCommandsResponse)
|
|
}
|
|
|
|
func (httpSvc *HttpService) execCustomNodeCommandHandler(c echo.Context) error {
|
|
var execCommandRequest api.ExecuteCustomNodeCommandRequest
|
|
if err := c.Bind(&execCommandRequest); err != nil {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
execCommandResponse, err := httpSvc.api.ExecuteCustomNodeCommand(c.Request().Context(), execCommandRequest.Command)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to execute command: %v", err),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, execCommandResponse)
|
|
}
|
|
|
|
func (httpSvc *HttpService) logoutHandler(c echo.Context) error {
|
|
redirectUrl := httpSvc.cfg.GetEnv().GetBaseFrontendUrl()
|
|
if redirectUrl == "" {
|
|
redirectUrl = "/"
|
|
}
|
|
|
|
return c.Redirect(http.StatusFound, redirectUrl)
|
|
}
|
|
|
|
func (httpSvc *HttpService) createBackupHandler(c echo.Context) error {
|
|
var backupRequest api.BasicBackupRequest
|
|
if err := c.Bind(&backupRequest); err != nil {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
if !httpSvc.cfg.CheckUnlockPassword(backupRequest.UnlockPassword) {
|
|
return c.JSON(http.StatusUnauthorized, ErrorResponse{
|
|
Message: "Invalid password",
|
|
})
|
|
}
|
|
|
|
var buffer bytes.Buffer
|
|
err := httpSvc.api.CreateBackup(backupRequest.UnlockPassword, &buffer)
|
|
if err != nil {
|
|
return c.String(500, fmt.Sprintf("Failed to create backup: %v", err))
|
|
}
|
|
|
|
c.Response().Header().Set("Content-Type", "application/octet-stream")
|
|
c.Response().Header().Set("Content-Disposition", "attachment; filename=albyhub.bkp")
|
|
c.Response().WriteHeader(http.StatusOK)
|
|
c.Response().Write(buffer.Bytes())
|
|
return nil
|
|
}
|
|
|
|
func (httpSvc *HttpService) restoreBackupHandler(c echo.Context) error {
|
|
info, err := httpSvc.api.GetInfo(c.Request().Context())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if info.SetupCompleted {
|
|
return errors.New("setup already completed")
|
|
}
|
|
|
|
password := c.FormValue("unlockPassword")
|
|
|
|
fileHeader, err := c.FormFile("backup")
|
|
if err != nil {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to get backup file header: %v", err),
|
|
})
|
|
}
|
|
|
|
file, err := fileHeader.Open()
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to open backup file: %v", err),
|
|
})
|
|
}
|
|
defer file.Close()
|
|
|
|
err = httpSvc.api.RestoreBackup(password, file)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to restore backup: %v", err),
|
|
})
|
|
}
|
|
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
func (httpSvc *HttpService) healthHandler(c echo.Context) error {
|
|
healthResponse, err := httpSvc.api.Health(c.Request().Context())
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to check node health: %v", err),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, healthResponse)
|
|
}
|
|
|
|
func (httpSvc *HttpService) listSwapsHandler(c echo.Context) error {
|
|
swaps, err := httpSvc.api.ListSwaps()
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, swaps)
|
|
}
|
|
|
|
func (httpSvc *HttpService) lookupSwapHandler(c echo.Context) error {
|
|
swap, err := httpSvc.api.LookupSwap(c.Param("swapId"))
|
|
if err != nil {
|
|
return c.JSON(http.StatusNotFound, ErrorResponse{
|
|
Message: "App not found",
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, swap)
|
|
}
|
|
|
|
func (httpSvc *HttpService) getSwapOutInfoHandler(c echo.Context) error {
|
|
swapOutFeesResponse, err := httpSvc.api.GetSwapOutInfo()
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to get swap out info: %v", err),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, swapOutFeesResponse)
|
|
}
|
|
|
|
func (httpSvc *HttpService) getSwapInInfoHandler(c echo.Context) error {
|
|
swapOutFeesResponse, err := httpSvc.api.GetSwapInInfo()
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to get swap in info: %v", err),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, swapOutFeesResponse)
|
|
}
|
|
|
|
func (httpSvc *HttpService) initiateSwapOutHandler(c echo.Context) error {
|
|
var initiateSwapOutRequest api.InitiateSwapRequest
|
|
if err := c.Bind(&initiateSwapOutRequest); err != nil {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
swapOutResponse, err := httpSvc.api.InitiateSwapOut(c.Request().Context(), &initiateSwapOutRequest)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to initiate swap out: %v", err),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, swapOutResponse)
|
|
}
|
|
|
|
func (httpSvc *HttpService) initiateSwapInHandler(c echo.Context) error {
|
|
var initiateSwapInRequest api.InitiateSwapRequest
|
|
if err := c.Bind(&initiateSwapInRequest); err != nil {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
txId, err := httpSvc.api.InitiateSwapIn(c.Request().Context(), &initiateSwapInRequest)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to initiate swap in: %v", err),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, txId)
|
|
}
|
|
|
|
func (httpSvc *HttpService) refundSwapHandler(c echo.Context) error {
|
|
var refundSwapInRequest api.RefundSwapRequest
|
|
if err := c.Bind(&refundSwapInRequest); err != nil {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
err := httpSvc.api.RefundSwap(&refundSwapInRequest)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
func (httpSvc *HttpService) swapMnemonicHandler(c echo.Context) error {
|
|
mnemonic := httpSvc.api.GetSwapMnemonic()
|
|
return c.JSON(http.StatusOK, mnemonic)
|
|
}
|
|
|
|
func (httpSvc *HttpService) getAutoSwapConfigHandler(c echo.Context) error {
|
|
getAutoSwapConfigResponse, err := httpSvc.api.GetAutoSwapConfig()
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to get swap settings: %v", err),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, getAutoSwapConfigResponse)
|
|
}
|
|
|
|
func (httpSvc *HttpService) enableAutoSwapOutHandler(c echo.Context) error {
|
|
var enableAutoSwapRequest api.EnableAutoSwapRequest
|
|
if err := c.Bind(&enableAutoSwapRequest); err != nil {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
err := httpSvc.api.EnableAutoSwapOut(c.Request().Context(), &enableAutoSwapRequest)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to save swap settings: %v", err),
|
|
})
|
|
}
|
|
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
func (httpSvc *HttpService) disableAutoSwapOutHandler(c echo.Context) error {
|
|
err := httpSvc.api.DisableAutoSwap()
|
|
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
func (httpSvc *HttpService) setNodeAliasHandler(c echo.Context) error {
|
|
var setNodeAliasRequest api.SetNodeAliasRequest
|
|
if err := c.Bind(&setNodeAliasRequest); err != nil {
|
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
err := httpSvc.api.SetNodeAlias(setNodeAliasRequest.NodeAlias)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to set node alias: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
func (httpSvc *HttpService) forwardsHandler(c echo.Context) error {
|
|
forwards, err := httpSvc.api.GetForwards()
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Message: fmt.Sprintf("Failed to get forwards: %s", err.Error()),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, forwards)
|
|
}
|