http_service.go raw

   1  package http
   2  
   3  import (
   4  	"bytes"
   5  	"crypto/rand"
   6  	"encoding/hex"
   7  	"encoding/json"
   8  	"errors"
   9  	"fmt"
  10  	"net/http"
  11  	"slices"
  12  	"strconv"
  13  	"strings"
  14  	"sync"
  15  	"time"
  16  
  17  	"github.com/getAlby/go-nostr"
  18  	"github.com/golang-jwt/jwt/v5"
  19  	"github.com/labstack/echo/v4"
  20  	"github.com/labstack/echo/v4/middleware"
  21  	"github.com/sirupsen/logrus"
  22  	"gorm.io/gorm"
  23  
  24  	"github.com/getAlby/hub/apps"
  25  	"github.com/getAlby/hub/config"
  26  	"github.com/getAlby/hub/events"
  27  	"github.com/getAlby/hub/logger"
  28  	"github.com/getAlby/hub/service"
  29  
  30  	"github.com/getAlby/hub/api"
  31  	"github.com/getAlby/hub/frontend"
  32  )
  33  
  34  type authTokenResponse struct {
  35  	Token string `json:"token"`
  36  }
  37  
  38  type jwtCustomClaims struct {
  39  	// we can add extra claims here
  40  	// Name  string `json:"name"`
  41  	// Admin bool   `json:"admin"`
  42  	Permission string `json:"permission,omitempty"` // "full" or "readonly"
  43  	jwt.RegisteredClaims
  44  }
  45  
  46  type HttpService struct {
  47  	api            api.API
  48  	albyHttpSvc    *AlbyHttpService
  49  	cfg            config.Config
  50  	eventPublisher events.EventPublisher
  51  	db             *gorm.DB
  52  	appsSvc        apps.AppsService
  53  
  54  	nip07Challenges      map[string]time.Time
  55  	nip07ChallengesMutex sync.Mutex
  56  }
  57  
  58  func NewHttpService(svc service.Service, eventPublisher events.EventPublisher) *HttpService {
  59  	return &HttpService{
  60  		api:             api.NewAPI(svc, svc.GetDB(), svc.GetConfig(), svc.GetKeys(), svc.GetAlbySvc(), svc.GetAlbyOAuthSvc(), eventPublisher),
  61  		albyHttpSvc:     NewAlbyHttpService(svc, svc.GetAlbySvc(), svc.GetAlbyOAuthSvc(), svc.GetConfig().GetEnv()),
  62  		cfg:             svc.GetConfig(),
  63  		eventPublisher:  eventPublisher,
  64  		db:              svc.GetDB(),
  65  		appsSvc:         apps.NewAppsService(svc.GetDB(), eventPublisher, svc.GetKeys(), svc.GetConfig()),
  66  		nip07Challenges: map[string]time.Time{},
  67  	}
  68  }
  69  
  70  func (httpSvc *HttpService) RegisterSharedRoutes(e *echo.Echo) {
  71  	e.HideBanner = true
  72  
  73  	e.Use(middleware.SecureWithConfig(middleware.SecureConfig{
  74  		ContentTypeNosniff: "nosniff",
  75  		XFrameOptions:      "DENY",
  76  		// when making changes here, also update the CSP in frontend/vite.config.ts
  77  		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.mleku.dev wss://relay.getalby.com wss://relay2.getalby.com; frame-src https://www.youtube-nocookie.com",
  78  		ReferrerPolicy:        "no-referrer",
  79  	}))
  80  	e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
  81  		LogURI:       true,
  82  		LogStatus:    true,
  83  		LogRemoteIP:  true,
  84  		LogUserAgent: true,
  85  		LogHost:      true,
  86  		LogRequestID: true,
  87  		LogValuesFunc: func(c echo.Context, values middleware.RequestLoggerValues) error {
  88  			logger.Logger.WithFields(logrus.Fields{
  89  				"uri":        values.URI,
  90  				"status":     values.Status,
  91  				"remote_ip":  values.RemoteIP,
  92  				"user_agent": values.UserAgent,
  93  				"host":       values.Host,
  94  				"request_id": values.RequestID,
  95  			}).Info("handled API request")
  96  			return nil
  97  		},
  98  	}))
  99  
 100  	e.Use(middleware.Recover())
 101  	e.Use(middleware.RequestID())
 102  
 103  	e.GET("/api/info", httpSvc.infoHandler)
 104  	e.POST("/api/setup", httpSvc.setupHandler)
 105  	e.POST("/api/restore", httpSvc.restoreBackupHandler)
 106  
 107  	// A single global rate limiter (one bucket for all callers, not per-IP)
 108  	// shared by every endpoint that verifies the unlock password, to bound how
 109  	// fast the password can be guessed.
 110  	unlockRateLimiter := middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{
 111  		Store: middleware.NewRateLimiterMemoryStoreWithConfig(
 112  			// burst of 2 so unlocking and then immediately acting is not blocked
 113  			middleware.RateLimiterMemoryStoreConfig{Rate: 1, Burst: 2},
 114  		),
 115  		IdentifierExtractor: func(c echo.Context) (string, error) {
 116  			return "", nil
 117  		},
 118  	})
 119  	e.POST("/api/start", httpSvc.startHandler, unlockRateLimiter)
 120  	e.POST("/api/unlock", httpSvc.unlockHandler, unlockRateLimiter)
 121  	e.POST("/api/backup", httpSvc.createBackupHandler, unlockRateLimiter)
 122  	e.GET("/logout", httpSvc.logoutHandler)
 123  
 124  	e.GET("/api/nip07/challenge", httpSvc.nip07ChallengeHandler)
 125  	e.POST("/api/nip07/auth", httpSvc.nip07AuthHandler)
 126  
 127  	frontend.RegisterHandlers(e)
 128  
 129  	// restricted routes: accept either a password-issued Bearer JWT or a
 130  	// NIP-07 session cookie
 131  	readOnlyApiGroup := e.Group("/api")
 132  	readOnlyApiGroup.Use(httpSvc.authMiddleware)
 133  
 134  	readOnlyApiGroup.GET("/apps", httpSvc.appsListHandler)
 135  	readOnlyApiGroup.GET("/apps/:pubkey", httpSvc.appsShowByPubkeyHandler)
 136  	readOnlyApiGroup.GET("/v2/apps/:id", httpSvc.appsShowHandler)
 137  	readOnlyApiGroup.GET("/channels", httpSvc.channelsListHandler)
 138  	readOnlyApiGroup.GET("/channels/suggestions", httpSvc.channelPeerSuggestionsHandler)
 139  	readOnlyApiGroup.GET("/channel-offer", httpSvc.channelOfferHandler)
 140  	readOnlyApiGroup.GET("/node/connection-info", httpSvc.nodeConnectionInfoHandler)
 141  	readOnlyApiGroup.GET("/node/status", httpSvc.nodeStatusHandler)
 142  	readOnlyApiGroup.GET("/node/network-graph", httpSvc.nodeNetworkGraphHandler)
 143  	readOnlyApiGroup.GET("/node/transactions", httpSvc.listOnchainTransactionsHandler)
 144  	readOnlyApiGroup.GET("/peers", httpSvc.listPeers)
 145  	readOnlyApiGroup.GET("/wallet/address", httpSvc.onchainAddressHandler)
 146  	readOnlyApiGroup.GET("/wallet/capabilities", httpSvc.capabilitiesHandler)
 147  	readOnlyApiGroup.GET("/transactions", httpSvc.listTransactionsHandler)
 148  	readOnlyApiGroup.GET("/transactions/:paymentHash", httpSvc.lookupTransactionHandler)
 149  	readOnlyApiGroup.GET("/balances", httpSvc.balancesHandler)
 150  	readOnlyApiGroup.GET("/mempool", httpSvc.mempoolApiHandler)
 151  	readOnlyApiGroup.GET("/health", httpSvc.healthHandler)
 152  	readOnlyApiGroup.GET("/commands", httpSvc.getCustomNodeCommandsHandler)
 153  	readOnlyApiGroup.GET("/swaps", httpSvc.listSwapsHandler)
 154  	readOnlyApiGroup.GET("/swaps/:swapId", httpSvc.lookupSwapHandler)
 155  	readOnlyApiGroup.GET("/swaps/out/info", httpSvc.getSwapOutInfoHandler)
 156  	readOnlyApiGroup.GET("/swaps/in/info", httpSvc.getSwapInInfoHandler)
 157  	readOnlyApiGroup.GET("/autoswap", httpSvc.getAutoSwapConfigHandler)
 158  	readOnlyApiGroup.GET("/forwards", httpSvc.forwardsHandler)
 159  
 160  	// Full access API group - requires a token with full permissions
 161  	fullAccessApiGroup := e.Group("/api")
 162  	fullAccessApiGroup.Use(httpSvc.authMiddleware)
 163  	fullAccessApiGroup.Use(httpSvc.requireFullAccess)
 164  
 165  	fullAccessApiGroup.POST("/event", httpSvc.eventHandler)
 166  	fullAccessApiGroup.PATCH("/unlock-password", httpSvc.changeUnlockPasswordHandler, unlockRateLimiter)
 167  	fullAccessApiGroup.PATCH("/auto-unlock", httpSvc.autoUnlockHandler, unlockRateLimiter)
 168  	fullAccessApiGroup.PATCH("/settings", httpSvc.updateSettingsHandler)
 169  	fullAccessApiGroup.PATCH("/apps/:pubkey", httpSvc.appsUpdateHandler)
 170  	fullAccessApiGroup.PATCH("/transactions/:id/labels", httpSvc.setTransactionUserLabelsHandler)
 171  	fullAccessApiGroup.DELETE("/apps/:pubkey", httpSvc.appsDeleteHandler)
 172  	fullAccessApiGroup.POST("/transfers", httpSvc.transfersHandler)
 173  	fullAccessApiGroup.POST("/apps", httpSvc.appsCreateHandler, unlockRateLimiter)
 174  	fullAccessApiGroup.POST("/lightning-addresses", httpSvc.lightningAddressesCreateHandler)
 175  	fullAccessApiGroup.DELETE("/lightning-addresses/:appId", httpSvc.lightningAddressesDeleteHandler)
 176  	fullAccessApiGroup.POST("/mnemonic", httpSvc.mnemonicHandler, unlockRateLimiter)
 177  	fullAccessApiGroup.PATCH("/backup-reminder", httpSvc.backupReminderHandler)
 178  	fullAccessApiGroup.POST("/channels", httpSvc.openChannelHandler)
 179  	fullAccessApiGroup.POST("/channels/rebalance", httpSvc.rebalanceChannelHandler)
 180  	fullAccessApiGroup.POST("/lsp-orders", httpSvc.newInstantChannelInvoiceHandler)
 181  	fullAccessApiGroup.POST("/node/migrate-storage", httpSvc.migrateNodeStorageHandler)
 182  	fullAccessApiGroup.POST("/peers", httpSvc.connectPeerHandler)
 183  	fullAccessApiGroup.DELETE("/peers/:peerId", httpSvc.disconnectPeerHandler)
 184  	fullAccessApiGroup.DELETE("/peers/:peerId/channels/:channelId", httpSvc.closeChannelHandler)
 185  	fullAccessApiGroup.PATCH("/peers/:peerId/channels/:channelId", httpSvc.updateChannelHandler)
 186  	fullAccessApiGroup.POST("/wallet/new-address", httpSvc.newOnchainAddressHandler)
 187  	fullAccessApiGroup.POST("/wallet/redeem-onchain-funds", httpSvc.redeemOnchainFundsHandler)
 188  	fullAccessApiGroup.POST("/wallet/sign-message", httpSvc.signMessageHandler)
 189  	fullAccessApiGroup.POST("/wallet/sync", httpSvc.walletSyncHandler)
 190  	fullAccessApiGroup.POST("/payments/:invoice", httpSvc.sendPaymentHandler)
 191  	fullAccessApiGroup.POST("/invoices", httpSvc.makeInvoiceHandler)
 192  	fullAccessApiGroup.POST("/offers", httpSvc.makeOfferHandler)
 193  	fullAccessApiGroup.POST("/reset-router", httpSvc.resetRouterHandler)
 194  	fullAccessApiGroup.POST("/stop", httpSvc.stopHandler)
 195  	fullAccessApiGroup.POST("/command", httpSvc.execCustomNodeCommandHandler)
 196  	fullAccessApiGroup.POST("/swaps/out", httpSvc.initiateSwapOutHandler)
 197  	fullAccessApiGroup.POST("/swaps/in", httpSvc.initiateSwapInHandler)
 198  	fullAccessApiGroup.POST("/swaps/refund", httpSvc.refundSwapHandler)
 199  	fullAccessApiGroup.GET("/swaps/mnemonic", httpSvc.swapMnemonicHandler)
 200  	fullAccessApiGroup.GET("/log/:type", httpSvc.getLogOutputHandler)
 201  	fullAccessApiGroup.POST("/autoswap", httpSvc.enableAutoSwapOutHandler, unlockRateLimiter)
 202  	fullAccessApiGroup.DELETE("/autoswap", httpSvc.disableAutoSwapOutHandler)
 203  	fullAccessApiGroup.POST("/node/alias", httpSvc.setNodeAliasHandler)
 204  
 205  	httpSvc.albyHttpSvc.RegisterSharedRoutes(readOnlyApiGroup, fullAccessApiGroup, e)
 206  }
 207  
 208  func (httpSvc *HttpService) infoHandler(c echo.Context) error {
 209  	responseBody, err := httpSvc.api.GetInfo(c.Request().Context())
 210  	if err != nil {
 211  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
 212  			Message: err.Error(),
 213  		})
 214  	}
 215  
 216  	responseBody.Unlocked = httpSvc.isAuthenticated(c)
 217  
 218  	return c.JSON(http.StatusOK, responseBody)
 219  }
 220  
 221  const (
 222  	nip07AuthKind      = 27235
 223  	nip07SessionCookie = "albyhub_session"
 224  	nip07ChallengeTTL  = 5 * time.Minute
 225  	nip07EventMaxAge   = 5 * time.Minute
 226  	nip07SessionExpiry = 30 * 24 * time.Hour
 227  )
 228  
 229  type nip07ChallengeResponse struct {
 230  	Challenge string `json:"challenge"`
 231  	Pubkey    string `json:"pubkey"`
 232  }
 233  
 234  type nip07AuthRequest struct {
 235  	Challenge string      `json:"challenge"`
 236  	Event     nostr.Event `json:"event"`
 237  }
 238  
 239  // isAuthenticated reports whether the request carries a valid session, either
 240  // a password-issued Bearer JWT or a NIP-07 session cookie.
 241  func (httpSvc *HttpService) isAuthenticated(c echo.Context) bool {
 242  	authHeader := c.Request().Header.Get("Authorization")
 243  	if strings.HasPrefix(authHeader, "Bearer ") {
 244  		tokenString := strings.TrimPrefix(authHeader, "Bearer ")
 245  		if secret, err := httpSvc.cfg.GetJWTSecret(); err == nil && secret != "" {
 246  			if httpSvc.parseJWT(tokenString, secret) != nil {
 247  				return true
 248  			}
 249  		}
 250  	}
 251  	if cookie, err := c.Cookie(nip07SessionCookie); err == nil && cookie.Value != "" {
 252  		if secret, err := httpSvc.cfg.GetSessionSecret(); err == nil && secret != "" {
 253  			if httpSvc.parseJWT(cookie.Value, secret) != nil {
 254  				return true
 255  			}
 256  		}
 257  	}
 258  	return false
 259  }
 260  
 261  func (httpSvc *HttpService) authMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
 262  	return func(c echo.Context) error {
 263  		authHeader := c.Request().Header.Get("Authorization")
 264  		if strings.HasPrefix(authHeader, "Bearer ") {
 265  			tokenString := strings.TrimPrefix(authHeader, "Bearer ")
 266  			if secret, err := httpSvc.cfg.GetJWTSecret(); err == nil && secret != "" {
 267  				if token := httpSvc.parseJWT(tokenString, secret); token != nil {
 268  					c.Set("user", token)
 269  					return next(c)
 270  				}
 271  			}
 272  		}
 273  		if cookie, err := c.Cookie(nip07SessionCookie); err == nil && cookie.Value != "" {
 274  			if secret, err := httpSvc.cfg.GetSessionSecret(); err == nil && secret != "" {
 275  				if token := httpSvc.parseJWT(cookie.Value, secret); token != nil {
 276  					c.Set("user", token)
 277  					return next(c)
 278  				}
 279  			}
 280  		}
 281  		return c.JSON(http.StatusUnauthorized, ErrorResponse{
 282  			Message: "unauthorized",
 283  		})
 284  	}
 285  }
 286  
 287  func (httpSvc *HttpService) parseJWT(tokenString, secret string) *jwt.Token {
 288  	token, err := jwt.ParseWithClaims(tokenString, &jwtCustomClaims{}, func(token *jwt.Token) (interface{}, error) {
 289  		return []byte(secret), nil
 290  	})
 291  	if err != nil || !token.Valid {
 292  		return nil
 293  	}
 294  	return token
 295  }
 296  
 297  func (httpSvc *HttpService) nip07ChallengeHandler(c echo.Context) error {
 298  	pubkey := httpSvc.cfg.GetNip07OwnerPubkey()
 299  	if pubkey == "" {
 300  		return c.JSON(http.StatusBadRequest, ErrorResponse{
 301  			Message: "NIP-07 auth is not configured",
 302  		})
 303  	}
 304  
 305  	challengeBytes := make([]byte, 32)
 306  	if _, err := rand.Read(challengeBytes); err != nil {
 307  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
 308  			Message: "Failed to generate challenge",
 309  		})
 310  	}
 311  	challenge := hex.EncodeToString(challengeBytes)
 312  
 313  	httpSvc.nip07ChallengesMutex.Lock()
 314  	httpSvc.nip07Challenges[challenge] = time.Now().Add(nip07ChallengeTTL)
 315  	httpSvc.nip07ChallengesMutex.Unlock()
 316  
 317  	return c.JSON(http.StatusOK, nip07ChallengeResponse{
 318  		Challenge: challenge,
 319  		Pubkey:    pubkey,
 320  	})
 321  }
 322  
 323  func (httpSvc *HttpService) nip07AuthHandler(c echo.Context) error {
 324  	ownerPubkey := httpSvc.cfg.GetNip07OwnerPubkey()
 325  	if ownerPubkey == "" {
 326  		return c.JSON(http.StatusBadRequest, ErrorResponse{
 327  			Message: "NIP-07 auth is not configured",
 328  		})
 329  	}
 330  
 331  	var req nip07AuthRequest
 332  	if err := c.Bind(&req); err != nil {
 333  		return c.JSON(http.StatusBadRequest, ErrorResponse{
 334  			Message: fmt.Sprintf("Bad request: %s", err.Error()),
 335  		})
 336  	}
 337  
 338  	httpSvc.nip07ChallengesMutex.Lock()
 339  	expiresAt, ok := httpSvc.nip07Challenges[req.Challenge]
 340  	if ok {
 341  		delete(httpSvc.nip07Challenges, req.Challenge)
 342  	}
 343  	httpSvc.nip07ChallengesMutex.Unlock()
 344  	if !ok || time.Now().After(expiresAt) {
 345  		return c.JSON(http.StatusUnauthorized, ErrorResponse{
 346  			Message: "invalid or expired challenge",
 347  		})
 348  	}
 349  
 350  	if req.Event.PubKey != ownerPubkey {
 351  		return c.JSON(http.StatusUnauthorized, ErrorResponse{
 352  			Message: "event not signed by the configured owner",
 353  		})
 354  	}
 355  	if req.Event.Kind != nip07AuthKind {
 356  		return c.JSON(http.StatusUnauthorized, ErrorResponse{
 357  			Message: "unexpected event kind",
 358  		})
 359  	}
 360  	valid, err := req.Event.CheckSignature()
 361  	if err != nil || !valid {
 362  		return c.JSON(http.StatusUnauthorized, ErrorResponse{
 363  			Message: "invalid event signature",
 364  		})
 365  	}
 366  	if req.Event.CreatedAt.Time().Before(time.Now().Add(-nip07EventMaxAge)) ||
 367  		req.Event.CreatedAt.Time().After(time.Now().Add(nip07EventMaxAge)) {
 368  		return c.JSON(http.StatusUnauthorized, ErrorResponse{
 369  			Message: "event timestamp outside allowed window",
 370  		})
 371  	}
 372  	challengeTagged := false
 373  	for _, tag := range req.Event.Tags {
 374  		if len(tag) == 2 && tag[0] == "challenge" && tag[1] == req.Challenge {
 375  			challengeTagged = true
 376  			break
 377  		}
 378  	}
 379  	if !challengeTagged {
 380  		return c.JSON(http.StatusUnauthorized, ErrorResponse{
 381  			Message: "event does not sign the challenge",
 382  		})
 383  	}
 384  
 385  	token, err := httpSvc.createSessionJWT(nil, "full")
 386  	if err != nil {
 387  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
 388  			Message: fmt.Sprintf("Failed to create session: %s", err.Error()),
 389  		})
 390  	}
 391  
 392  	cookie := &http.Cookie{
 393  		Name:     nip07SessionCookie,
 394  		Value:    token,
 395  		Path:     "/",
 396  		HttpOnly: true,
 397  		Secure:   c.IsTLS() || c.Request().Header.Get("X-Forwarded-Proto") == "https",
 398  		SameSite: http.SameSiteLaxMode,
 399  		Expires:  time.Now().Add(nip07SessionExpiry),
 400  	}
 401  	c.SetCookie(cookie)
 402  
 403  	return c.JSON(http.StatusOK, map[string]string{"pubkey": ownerPubkey})
 404  }
 405  
 406  func (httpSvc *HttpService) eventHandler(c echo.Context) error {
 407  	var sendEventRequest api.SendEventRequest
 408  	if err := c.Bind(&sendEventRequest); err != nil {
 409  		return c.JSON(http.StatusBadRequest, ErrorResponse{
 410  			Message: fmt.Sprintf("Bad request: %s", err.Error()),
 411  		})
 412  	}
 413  
 414  	httpSvc.api.SendEvent(sendEventRequest.Event, sendEventRequest.Properties)
 415  
 416  	return c.NoContent(http.StatusOK)
 417  }
 418  
 419  func (httpSvc *HttpService) mnemonicHandler(c echo.Context) error {
 420  	var mnemonicRequest api.MnemonicRequest
 421  	if err := c.Bind(&mnemonicRequest); err != nil {
 422  		return c.JSON(http.StatusBadRequest, ErrorResponse{
 423  			Message: fmt.Sprintf("Bad request: %s", err.Error()),
 424  		})
 425  	}
 426  
 427  	responseBody, err := httpSvc.api.GetMnemonic(mnemonicRequest.UnlockPassword)
 428  
 429  	if err != nil {
 430  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
 431  			Message: err.Error(),
 432  		})
 433  	}
 434  
 435  	return c.JSON(http.StatusOK, responseBody)
 436  }
 437  
 438  func (httpSvc *HttpService) backupReminderHandler(c echo.Context) error {
 439  	var backupReminderRequest api.BackupReminderRequest
 440  	if err := c.Bind(&backupReminderRequest); err != nil {
 441  		return c.JSON(http.StatusBadRequest, ErrorResponse{
 442  			Message: fmt.Sprintf("Bad request: %s", err.Error()),
 443  		})
 444  	}
 445  
 446  	err := httpSvc.api.SetNextBackupReminder(&backupReminderRequest)
 447  	if err != nil {
 448  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
 449  			Message: fmt.Sprintf("Failed to store backup reminder: %s", err.Error()),
 450  		})
 451  	}
 452  
 453  	return c.NoContent(http.StatusNoContent)
 454  }
 455  
 456  func (httpSvc *HttpService) startHandler(c echo.Context) error {
 457  	var startRequest api.StartRequest
 458  	if err := c.Bind(&startRequest); err != nil {
 459  		return c.JSON(http.StatusBadRequest, ErrorResponse{
 460  			Message: fmt.Sprintf("Bad request: %s", err.Error()),
 461  		})
 462  	}
 463  
 464  	if !httpSvc.cfg.CheckUnlockPassword(startRequest.UnlockPassword) {
 465  		return c.JSON(http.StatusUnauthorized, ErrorResponse{
 466  			Message: "Invalid password",
 467  		})
 468  	}
 469  
 470  	go httpSvc.api.Start(&startRequest)
 471  
 472  	// with NIP-07 auth configured the unlock password only starts the node;
 473  	// the UI session must be established via NIP-07 instead
 474  	if httpSvc.cfg.GetNip07OwnerPubkey() != "" {
 475  		return c.NoContent(http.StatusNoContent)
 476  	}
 477  
 478  	err := httpSvc.cfg.LoadJWTSecret(startRequest.UnlockPassword)
 479  
 480  	if err != nil {
 481  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
 482  			Message: fmt.Sprintf("Failed to load JWT secret: %s", err.Error()),
 483  		})
 484  	}
 485  
 486  	token, err := httpSvc.createJWT(nil, "full")
 487  
 488  	if err != nil {
 489  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
 490  			Message: fmt.Sprintf("Failed to save session: %s", err.Error()),
 491  		})
 492  	}
 493  
 494  	return c.JSON(http.StatusOK, &authTokenResponse{
 495  		Token: token,
 496  	})
 497  }
 498  
 499  func (httpSvc *HttpService) unlockHandler(c echo.Context) error {
 500  	if httpSvc.cfg.GetNip07OwnerPubkey() != "" {
 501  		return c.JSON(http.StatusForbidden, ErrorResponse{
 502  			Message: "Password unlock is disabled. Sign in with Nostr.",
 503  		})
 504  	}
 505  
 506  	var unlockRequest api.UnlockRequest
 507  	if err := c.Bind(&unlockRequest); err != nil {
 508  		return c.JSON(http.StatusBadRequest, ErrorResponse{
 509  			Message: fmt.Sprintf("Bad request: %s", err.Error()),
 510  		})
 511  	}
 512  
 513  	if !httpSvc.cfg.CheckUnlockPassword(unlockRequest.UnlockPassword) {
 514  		return c.JSON(http.StatusUnauthorized, ErrorResponse{
 515  			Message: "Invalid password",
 516  		})
 517  	}
 518  
 519  	if unlockRequest.Permission == "" {
 520  		return c.JSON(http.StatusBadRequest, ErrorResponse{
 521  			Message: "Permission field is required",
 522  		})
 523  	}
 524  
 525  	if !slices.Contains([]string{"full", "readonly"}, unlockRequest.Permission) {
 526  		return c.JSON(http.StatusBadRequest, ErrorResponse{
 527  			Message: "Permission field is unknown",
 528  		})
 529  	}
 530  
 531  	_, err := httpSvc.api.GetNodeStatus(c.Request().Context())
 532  	if err != nil {
 533  		if errors.Is(err, api.ErrLNClientNotStarted) {
 534  			return c.JSON(http.StatusBadRequest, ErrorResponse{
 535  				Message: "Node is not running, start it before unlocking.",
 536  			})
 537  		}
 538  
 539  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
 540  			Message: err.Error(),
 541  		})
 542  	}
 543  
 544  	token, err := httpSvc.createJWT(unlockRequest.TokenExpiryDays, unlockRequest.Permission)
 545  	if err != nil {
 546  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
 547  			Message: fmt.Sprintf("Failed to save session: %s", err.Error()),
 548  		})
 549  	}
 550  
 551  	httpSvc.eventPublisher.Publish(&events.Event{
 552  		Event: "nwc_unlocked",
 553  	})
 554  
 555  	return c.JSON(http.StatusOK, &authTokenResponse{
 556  		Token: token,
 557  	})
 558  }
 559  
 560  func (httpSvc *HttpService) requireFullAccess(next echo.HandlerFunc) echo.HandlerFunc {
 561  	return func(c echo.Context) error {
 562  		token := c.Get("user").(*jwt.Token)
 563  		claims := token.Claims.(*jwtCustomClaims)
 564  
 565  		// Allow if no permission specified (backward compatibility) or if full access
 566  		if claims.Permission == "" || claims.Permission == "full" {
 567  			return next(c)
 568  		}
 569  
 570  		return c.JSON(http.StatusForbidden, ErrorResponse{
 571  			Message: "This operation requires full access permissions",
 572  		})
 573  	}
 574  }
 575  
 576  func (httpSvc *HttpService) changeUnlockPasswordHandler(c echo.Context) error {
 577  	var changeUnlockPasswordRequest api.ChangeUnlockPasswordRequest
 578  	if err := c.Bind(&changeUnlockPasswordRequest); err != nil {
 579  		return c.JSON(http.StatusBadRequest, ErrorResponse{
 580  			Message: fmt.Sprintf("Bad request: %s", err.Error()),
 581  		})
 582  	}
 583  
 584  	err := httpSvc.api.ChangeUnlockPassword(&changeUnlockPasswordRequest)
 585  	if err != nil {
 586  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
 587  			Message: fmt.Sprintf("Failed to change unlock password: %s", err.Error()),
 588  		})
 589  	}
 590  
 591  	return c.NoContent(http.StatusNoContent)
 592  }
 593  
 594  func (httpSvc *HttpService) updateSettingsHandler(c echo.Context) error {
 595  	var updateSettingsRequest api.UpdateSettingsRequest
 596  	if err := c.Bind(&updateSettingsRequest); err != nil {
 597  		return c.JSON(http.StatusBadRequest, ErrorResponse{
 598  			Message: fmt.Sprintf("Bad request: %s", err.Error()),
 599  		})
 600  	}
 601  
 602  	err := httpSvc.api.UpdateSettings(&updateSettingsRequest)
 603  	if err != nil {
 604  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
 605  			Message: fmt.Sprintf("Failed to update settings: %s", err.Error()),
 606  		})
 607  	}
 608  
 609  	return c.NoContent(http.StatusNoContent)
 610  }
 611  
 612  func (httpSvc *HttpService) autoUnlockHandler(c echo.Context) error {
 613  	var autoUnlockRequest api.AutoUnlockRequest
 614  	if err := c.Bind(&autoUnlockRequest); err != nil {
 615  		return c.JSON(http.StatusBadRequest, ErrorResponse{
 616  			Message: fmt.Sprintf("Bad request: %s", err.Error()),
 617  		})
 618  	}
 619  
 620  	err := httpSvc.api.SetAutoUnlockPassword(autoUnlockRequest.UnlockPassword)
 621  	if err != nil {
 622  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
 623  			Message: fmt.Sprintf("Failed to set auto unlock password: %s", err.Error()),
 624  		})
 625  	}
 626  
 627  	return c.NoContent(http.StatusNoContent)
 628  }
 629  
 630  func (httpSvc *HttpService) createJWT(tokenExpiryDays *uint64, permission string) (string, error) {
 631  	secret, err := httpSvc.cfg.GetJWTSecret()
 632  	if err != nil {
 633  		return "", err
 634  	}
 635  	return signSessionJWT(secret, tokenExpiryDays, permission)
 636  }
 637  
 638  func (httpSvc *HttpService) createSessionJWT(tokenExpiryDays *uint64, permission string) (string, error) {
 639  	secret, err := httpSvc.cfg.GetSessionSecret()
 640  	if err != nil {
 641  		return "", err
 642  	}
 643  	return signSessionJWT(secret, tokenExpiryDays, permission)
 644  }
 645  
 646  func signSessionJWT(secret string, tokenExpiryDays *uint64, permission string) (string, error) {
 647  	if !slices.Contains([]string{"full", "readonly"}, permission) {
 648  		return "", errors.New("invalid token permission")
 649  	}
 650  
 651  	expiryDays := uint64(30)
 652  	if tokenExpiryDays != nil {
 653  		expiryDays = *tokenExpiryDays
 654  	}
 655  
 656  	// Set custom claims
 657  	claims := &jwtCustomClaims{
 658  		Permission: permission,
 659  		RegisteredClaims: jwt.RegisteredClaims{
 660  			ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24 * time.Duration(expiryDays))),
 661  		},
 662  	}
 663  
 664  	// Create token with claims
 665  	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
 666  
 667  	if token == nil {
 668  		return "", errors.New("failed to create token")
 669  	}
 670  
 671  	return token.SignedString([]byte(secret))
 672  }
 673  
 674  func (httpSvc *HttpService) channelsListHandler(c echo.Context) error {
 675  	ctx := c.Request().Context()
 676  
 677  	channels, err := httpSvc.api.ListChannels(ctx)
 678  
 679  	if err != nil {
 680  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
 681  			Message: err.Error(),
 682  		})
 683  	}
 684  
 685  	return c.JSON(http.StatusOK, channels)
 686  }
 687  
 688  func (httpSvc *HttpService) channelPeerSuggestionsHandler(c echo.Context) error {
 689  	ctx := c.Request().Context()
 690  
 691  	suggestions, err := httpSvc.api.GetChannelPeerSuggestions(ctx)
 692  
 693  	if err != nil {
 694  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
 695  			Message: err.Error(),
 696  		})
 697  	}
 698  
 699  	return c.JSON(http.StatusOK, suggestions)
 700  }
 701  
 702  func (httpSvc *HttpService) channelOfferHandler(c echo.Context) error {
 703  	ctx := c.Request().Context()
 704  
 705  	suggestions, err := httpSvc.api.GetLSPChannelOffer(ctx)
 706  
 707  	if err != nil {
 708  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
 709  			Message: err.Error(),
 710  		})
 711  	}
 712  
 713  	return c.JSON(http.StatusOK, suggestions)
 714  }
 715  
 716  func (httpSvc *HttpService) resetRouterHandler(c echo.Context) error {
 717  	var resetRouterRequest api.ResetRouterRequest
 718  	if err := c.Bind(&resetRouterRequest); err != nil {
 719  		return c.JSON(http.StatusBadRequest, ErrorResponse{
 720  			Message: fmt.Sprintf("Bad request: %s", err.Error()),
 721  		})
 722  	}
 723  
 724  	err := httpSvc.api.ResetRouter(resetRouterRequest.Key)
 725  
 726  	if err != nil {
 727  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
 728  			Message: err.Error(),
 729  		})
 730  	}
 731  
 732  	return c.NoContent(http.StatusNoContent)
 733  }
 734  
 735  func (httpSvc *HttpService) stopHandler(c echo.Context) error {
 736  
 737  	err := httpSvc.api.Stop()
 738  
 739  	if err != nil {
 740  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
 741  			Message: err.Error(),
 742  		})
 743  	}
 744  
 745  	return c.NoContent(http.StatusNoContent)
 746  }
 747  
 748  func (httpSvc *HttpService) nodeConnectionInfoHandler(c echo.Context) error {
 749  	ctx := c.Request().Context()
 750  
 751  	info, err := httpSvc.api.GetNodeConnectionInfo(ctx)
 752  
 753  	if err != nil {
 754  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
 755  			Message: err.Error(),
 756  		})
 757  	}
 758  
 759  	return c.JSON(http.StatusOK, info)
 760  }
 761  
 762  func (httpSvc *HttpService) nodeStatusHandler(c echo.Context) error {
 763  	ctx := c.Request().Context()
 764  
 765  	info, err := httpSvc.api.GetNodeStatus(ctx)
 766  
 767  	if err != nil {
 768  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
 769  			Message: err.Error(),
 770  		})
 771  	}
 772  
 773  	return c.JSON(http.StatusOK, info)
 774  }
 775  
 776  func (httpSvc *HttpService) nodeNetworkGraphHandler(c echo.Context) error {
 777  	ctx := c.Request().Context()
 778  
 779  	nodeIds := strings.Split(c.QueryParam("nodeIds"), ",")
 780  
 781  	info, err := httpSvc.api.GetNetworkGraph(ctx, nodeIds)
 782  
 783  	if err != nil {
 784  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
 785  			Message: err.Error(),
 786  		})
 787  	}
 788  
 789  	return c.JSON(http.StatusOK, info)
 790  }
 791  
 792  func (httpSvc *HttpService) migrateNodeStorageHandler(c echo.Context) error {
 793  	ctx := c.Request().Context()
 794  	var migrateNodeStorageRequest api.MigrateNodeStorageRequest
 795  	if err := c.Bind(&migrateNodeStorageRequest); err != nil {
 796  		return c.JSON(http.StatusBadRequest, ErrorResponse{
 797  			Message: fmt.Sprintf("Bad request: %s", err.Error()),
 798  		})
 799  	}
 800  
 801  	err := httpSvc.api.MigrateNodeStorage(ctx, migrateNodeStorageRequest.To)
 802  
 803  	if err != nil {
 804  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
 805  			Message: err.Error(),
 806  		})
 807  	}
 808  
 809  	return c.NoContent(http.StatusNoContent)
 810  }
 811  
 812  func (httpSvc *HttpService) balancesHandler(c echo.Context) error {
 813  	ctx := c.Request().Context()
 814  
 815  	balances, err := httpSvc.api.GetBalances(ctx)
 816  
 817  	if err != nil {
 818  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
 819  			Message: err.Error(),
 820  		})
 821  	}
 822  
 823  	return c.JSON(http.StatusOK, balances)
 824  }
 825  
 826  func (httpSvc *HttpService) sendPaymentHandler(c echo.Context) error {
 827  	ctx := c.Request().Context()
 828  
 829  	var payInvoiceRequest api.PayInvoiceRequest
 830  	if err := c.Bind(&payInvoiceRequest); err != nil {
 831  		return c.JSON(http.StatusBadRequest, ErrorResponse{
 832  			Message: fmt.Sprintf("Bad request: %s", err.Error()),
 833  		})
 834  	}
 835  	amountMsat := api.ResolveToMsat(payInvoiceRequest.AmountSat, payInvoiceRequest.AmountMsat, nil, payInvoiceRequest.Amount)
 836  
 837  	paymentResponse, err := httpSvc.api.SendPayment(ctx, c.Param("invoice"), amountMsat, payInvoiceRequest.Metadata, payInvoiceRequest.FromAppID)
 838  
 839  	if err != nil {
 840  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
 841  			Message: err.Error(),
 842  		})
 843  	}
 844  
 845  	return c.JSON(http.StatusOK, paymentResponse)
 846  }
 847  
 848  func (httpSvc *HttpService) makeOfferHandler(c echo.Context) error {
 849  	ctx := c.Request().Context()
 850  
 851  	var makeOfferRequest api.MakeOfferRequest
 852  	if err := c.Bind(&makeOfferRequest); err != nil {
 853  		return c.JSON(http.StatusBadRequest, ErrorResponse{
 854  			Message: fmt.Sprintf("Bad request: %s", err.Error()),
 855  		})
 856  	}
 857  
 858  	offer, err := httpSvc.api.MakeOffer(ctx, makeOfferRequest.Description)
 859  
 860  	if err != nil {
 861  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
 862  			Message: fmt.Sprintf("Failed to generate BOLT-12 offer: %s", err.Error()),
 863  		})
 864  	}
 865  
 866  	return c.JSON(http.StatusOK, offer)
 867  }
 868  
 869  func (httpSvc *HttpService) makeInvoiceHandler(c echo.Context) error {
 870  	var makeInvoiceRequest api.MakeInvoiceRequest
 871  	if err := c.Bind(&makeInvoiceRequest); err != nil {
 872  		return c.JSON(http.StatusBadRequest, ErrorResponse{
 873  			Message: fmt.Sprintf("Bad request: %s", err.Error()),
 874  		})
 875  	}
 876  
 877  	amountMsat := uint64(0)
 878  	resolvedAmountMsat := api.ResolveToMsat(makeInvoiceRequest.AmountSat, makeInvoiceRequest.AmountMsat, nil, makeInvoiceRequest.Amount)
 879  	if resolvedAmountMsat != nil {
 880  		amountMsat = *resolvedAmountMsat
 881  	}
 882  
 883  	invoice, err := httpSvc.api.CreateInvoice(c.Request().Context(), amountMsat, makeInvoiceRequest.Description, makeInvoiceRequest.ToAppID)
 884  
 885  	if err != nil {
 886  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
 887  			Message: err.Error(),
 888  		})
 889  	}
 890  
 891  	return c.JSON(http.StatusOK, invoice)
 892  }
 893  
 894  func (httpSvc *HttpService) lookupTransactionHandler(c echo.Context) error {
 895  	ctx := c.Request().Context()
 896  
 897  	transaction, err := httpSvc.api.LookupInvoice(ctx, c.Param("paymentHash"))
 898  
 899  	if err != nil {
 900  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
 901  			Message: err.Error(),
 902  		})
 903  	}
 904  
 905  	return c.JSON(http.StatusOK, transaction)
 906  }
 907  
 908  func (httpSvc *HttpService) setTransactionUserLabelsHandler(c echo.Context) error {
 909  	ctx := c.Request().Context()
 910  
 911  	var requestData api.SetTransactionUserLabelsRequest
 912  	if err := c.Bind(&requestData); err != nil {
 913  		return c.JSON(http.StatusBadRequest, ErrorResponse{
 914  			Message: fmt.Sprintf("Bad request: %s", err.Error()),
 915  		})
 916  	}
 917  
 918  	transactionID, err := strconv.ParseUint(c.Param("id"), 10, 64)
 919  	if err != nil {
 920  		return c.JSON(http.StatusBadRequest, ErrorResponse{
 921  			Message: "Invalid transaction ID",
 922  		})
 923  	}
 924  
 925  	err = httpSvc.api.SetTransactionUserLabels(ctx, uint(transactionID), requestData.Labels)
 926  	if err != nil {
 927  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
 928  			Message: err.Error(),
 929  		})
 930  	}
 931  
 932  	return c.NoContent(http.StatusNoContent)
 933  }
 934  
 935  func (httpSvc *HttpService) listTransactionsHandler(c echo.Context) error {
 936  	ctx := c.Request().Context()
 937  
 938  	limit := uint64(20)
 939  	offset := uint64(0)
 940  	var appId *uint
 941  
 942  	if limitParam := c.QueryParam("limit"); limitParam != "" {
 943  		if parsedLimit, err := strconv.ParseUint(limitParam, 10, 64); err == nil {
 944  			limit = parsedLimit
 945  		}
 946  	}
 947  
 948  	if offsetParam := c.QueryParam("offset"); offsetParam != "" {
 949  		if parsedOffset, err := strconv.ParseUint(offsetParam, 10, 64); err == nil {
 950  			offset = parsedOffset
 951  		}
 952  	}
 953  
 954  	if appIdParam := c.QueryParam("appId"); appIdParam != "" {
 955  		if parsedAppId, err := strconv.ParseUint(appIdParam, 10, 64); err == nil {
 956  			var unsignedAppId = uint(parsedAppId)
 957  			appId = &unsignedAppId
 958  		}
 959  	}
 960  
 961  	filters, err := api.ParseListTransactionsFilters(c.QueryParams())
 962  	if err != nil {
 963  		return c.JSON(http.StatusBadRequest, ErrorResponse{
 964  			Message: err.Error(),
 965  		})
 966  	}
 967  
 968  	transactions, err := httpSvc.api.ListTransactions(ctx, appId, limit, offset, filters)
 969  
 970  	if err != nil {
 971  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
 972  			Message: err.Error(),
 973  		})
 974  	}
 975  
 976  	return c.JSON(http.StatusOK, transactions)
 977  }
 978  
 979  func (httpSvc *HttpService) listOnchainTransactionsHandler(c echo.Context) error {
 980  	ctx := c.Request().Context()
 981  
 982  	transactions, err := httpSvc.api.ListOnchainTransactions(ctx)
 983  
 984  	if err != nil {
 985  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
 986  			Message: err.Error(),
 987  		})
 988  	}
 989  
 990  	return c.JSON(http.StatusOK, transactions)
 991  }
 992  
 993  func (httpSvc *HttpService) walletSyncHandler(c echo.Context) error {
 994  	httpSvc.api.SyncWallet()
 995  
 996  	return c.NoContent(http.StatusNoContent)
 997  }
 998  
 999  func (httpSvc *HttpService) mempoolApiHandler(c echo.Context) error {
1000  	endpoint := c.QueryParam("endpoint")
1001  	if endpoint == "" {
1002  		return c.JSON(http.StatusBadRequest, ErrorResponse{
1003  			Message: "Invalid pubkey parameter",
1004  		})
1005  	}
1006  
1007  	response, err := httpSvc.api.RequestMempoolApi(c.Request().Context(), endpoint)
1008  	if err != nil {
1009  		logger.Logger.WithField("endpoint", endpoint).WithError(err).Error("Failed to request mempool API")
1010  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1011  			Message: fmt.Sprintf("Failed to request mempool API: %s", err.Error()),
1012  		})
1013  	}
1014  
1015  	return c.JSON(http.StatusOK, response)
1016  }
1017  
1018  func (httpSvc *HttpService) capabilitiesHandler(c echo.Context) error {
1019  	response, err := httpSvc.api.GetWalletCapabilities(c.Request().Context())
1020  	if err != nil {
1021  		logger.Logger.WithError(err).Error("Failed to request wallet capabilities")
1022  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1023  			Message: fmt.Sprintf("Failed to request wallet capabilities: %s", err.Error()),
1024  		})
1025  	}
1026  
1027  	return c.JSON(http.StatusOK, response)
1028  }
1029  
1030  func (httpSvc *HttpService) listPeers(c echo.Context) error {
1031  	peers, err := httpSvc.api.ListPeers(c.Request().Context())
1032  	if err != nil {
1033  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1034  			Message: fmt.Sprintf("Failed to list peers: %s", err.Error()),
1035  		})
1036  	}
1037  
1038  	return c.JSON(http.StatusOK, peers)
1039  }
1040  
1041  func (httpSvc *HttpService) connectPeerHandler(c echo.Context) error {
1042  	ctx := c.Request().Context()
1043  
1044  	var connectPeerRequest api.ConnectPeerRequest
1045  	if err := c.Bind(&connectPeerRequest); err != nil {
1046  		return c.JSON(http.StatusBadRequest, ErrorResponse{
1047  			Message: fmt.Sprintf("Bad request: %s", err.Error()),
1048  		})
1049  	}
1050  
1051  	err := httpSvc.api.ConnectPeer(ctx, &connectPeerRequest)
1052  
1053  	if err != nil {
1054  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1055  			Message: fmt.Sprintf("Failed to connect peer: %s", err.Error()),
1056  		})
1057  	}
1058  
1059  	return c.NoContent(http.StatusNoContent)
1060  }
1061  
1062  func (httpSvc *HttpService) openChannelHandler(c echo.Context) error {
1063  	ctx := c.Request().Context()
1064  
1065  	var openChannelRequest api.OpenChannelRequest
1066  	if err := c.Bind(&openChannelRequest); err != nil {
1067  		return c.JSON(http.StatusBadRequest, ErrorResponse{
1068  			Message: fmt.Sprintf("Bad request: %s", err.Error()),
1069  		})
1070  	}
1071  
1072  	openChannelResponse, err := httpSvc.api.OpenChannel(ctx, &openChannelRequest)
1073  
1074  	if err != nil {
1075  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1076  			Message: fmt.Sprintf("Failed to open channel: %s", err.Error()),
1077  		})
1078  	}
1079  
1080  	return c.JSON(http.StatusOK, openChannelResponse)
1081  }
1082  
1083  func (httpSvc *HttpService) rebalanceChannelHandler(c echo.Context) error {
1084  	ctx := c.Request().Context()
1085  
1086  	var rebalanceChannelRequest api.RebalanceChannelRequest
1087  	if err := c.Bind(&rebalanceChannelRequest); err != nil {
1088  		return c.JSON(http.StatusBadRequest, ErrorResponse{
1089  			Message: fmt.Sprintf("Bad request: %s", err.Error()),
1090  		})
1091  	}
1092  
1093  	rebalanceChannelResponse, err := httpSvc.api.RebalanceChannel(ctx, &rebalanceChannelRequest)
1094  
1095  	if err != nil {
1096  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1097  			Message: fmt.Sprintf("Failed to rebalance channel: %s", err.Error()),
1098  		})
1099  	}
1100  
1101  	return c.JSON(http.StatusOK, rebalanceChannelResponse)
1102  }
1103  
1104  func (httpSvc *HttpService) disconnectPeerHandler(c echo.Context) error {
1105  	ctx := c.Request().Context()
1106  
1107  	err := httpSvc.api.DisconnectPeer(ctx, c.Param("peerId"))
1108  
1109  	if err != nil {
1110  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1111  			Message: fmt.Sprintf("Failed to disconnect peer: %s", err.Error()),
1112  		})
1113  	}
1114  
1115  	return c.NoContent(http.StatusNoContent)
1116  }
1117  
1118  func (httpSvc *HttpService) closeChannelHandler(c echo.Context) error {
1119  	ctx := c.Request().Context()
1120  
1121  	closeChannelResponse, err := httpSvc.api.CloseChannel(ctx, c.Param("peerId"), c.Param("channelId"), c.QueryParam("force") == "true")
1122  
1123  	if err != nil {
1124  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1125  			Message: fmt.Sprintf("Failed to close channel: %s", err.Error()),
1126  		})
1127  	}
1128  
1129  	return c.JSON(http.StatusOK, closeChannelResponse)
1130  }
1131  
1132  func (httpSvc *HttpService) updateChannelHandler(c echo.Context) error {
1133  	ctx := c.Request().Context()
1134  
1135  	var updateChannelRequest api.UpdateChannelRequest
1136  	if err := c.Bind(&updateChannelRequest); err != nil {
1137  		return c.JSON(http.StatusBadRequest, ErrorResponse{
1138  			Message: fmt.Sprintf("Bad request: %s", err.Error()),
1139  		})
1140  	}
1141  
1142  	updateChannelRequest.NodeId = c.Param("peerId")
1143  	updateChannelRequest.ChannelId = c.Param("channelId")
1144  
1145  	err := httpSvc.api.UpdateChannel(ctx, &updateChannelRequest)
1146  
1147  	if err != nil {
1148  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1149  			Message: fmt.Sprintf("Failed to update channel: %s", err.Error()),
1150  		})
1151  	}
1152  
1153  	return c.NoContent(http.StatusNoContent)
1154  }
1155  
1156  func (httpSvc *HttpService) newInstantChannelInvoiceHandler(c echo.Context) error {
1157  	ctx := c.Request().Context()
1158  
1159  	var newWrappedInvoiceRequest api.LSPOrderRequest
1160  	if err := c.Bind(&newWrappedInvoiceRequest); err != nil {
1161  		return c.JSON(http.StatusBadRequest, ErrorResponse{
1162  			Message: fmt.Sprintf("Bad request: %s", err.Error()),
1163  		})
1164  	}
1165  
1166  	newLSPOrderResponse, err := httpSvc.api.RequestLSPOrder(ctx, &newWrappedInvoiceRequest)
1167  
1168  	if err != nil {
1169  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1170  			Message: fmt.Sprintf("Failed to request new channel order from LSP: %s", err.Error()),
1171  		})
1172  	}
1173  
1174  	return c.JSON(http.StatusOK, newLSPOrderResponse)
1175  }
1176  
1177  func (httpSvc *HttpService) onchainAddressHandler(c echo.Context) error {
1178  	ctx := c.Request().Context()
1179  
1180  	address, err := httpSvc.api.GetUnusedOnchainAddress(ctx)
1181  
1182  	if err != nil {
1183  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1184  			Message: fmt.Sprintf("Failed to request new onchain address: %s", err.Error()),
1185  		})
1186  	}
1187  
1188  	return c.JSON(http.StatusOK, address)
1189  }
1190  
1191  func (httpSvc *HttpService) newOnchainAddressHandler(c echo.Context) error {
1192  	ctx := c.Request().Context()
1193  
1194  	address, err := httpSvc.api.GetNewOnchainAddress(ctx)
1195  
1196  	if err != nil {
1197  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1198  			Message: fmt.Sprintf("Failed to request new onchain address: %s", err.Error()),
1199  		})
1200  	}
1201  
1202  	return c.JSON(http.StatusOK, address)
1203  }
1204  
1205  func (httpSvc *HttpService) redeemOnchainFundsHandler(c echo.Context) error {
1206  	ctx := c.Request().Context()
1207  
1208  	var redeemOnchainFundsRequest api.RedeemOnchainFundsRequest
1209  	if err := c.Bind(&redeemOnchainFundsRequest); err != nil {
1210  		return c.JSON(http.StatusBadRequest, ErrorResponse{
1211  			Message: fmt.Sprintf("Bad request: %s", err.Error()),
1212  		})
1213  	}
1214  
1215  	amountSat := uint64(0)
1216  	resolvedAmountSat := api.ResolveToSat(redeemOnchainFundsRequest.AmountSat, nil, redeemOnchainFundsRequest.Amount, nil)
1217  	if resolvedAmountSat != nil {
1218  		amountSat = *resolvedAmountSat
1219  	}
1220  
1221  	redeemOnchainFundsResponse, err := httpSvc.api.RedeemOnchainFunds(ctx, redeemOnchainFundsRequest.ToAddress, amountSat, redeemOnchainFundsRequest.FeeRate, redeemOnchainFundsRequest.SendAll)
1222  
1223  	if err != nil {
1224  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1225  			Message: fmt.Sprintf("Failed to redeem onchain funds: %s", err.Error()),
1226  		})
1227  	}
1228  
1229  	return c.JSON(http.StatusOK, redeemOnchainFundsResponse)
1230  }
1231  
1232  func (httpSvc *HttpService) signMessageHandler(c echo.Context) error {
1233  	ctx := c.Request().Context()
1234  
1235  	var signMessageRequest api.SignMessageRequest
1236  	if err := c.Bind(&signMessageRequest); err != nil {
1237  		return c.JSON(http.StatusBadRequest, ErrorResponse{
1238  			Message: fmt.Sprintf("Bad request: %s", err.Error()),
1239  		})
1240  	}
1241  
1242  	signMessageResponse, err := httpSvc.api.SignMessage(ctx, signMessageRequest.Message)
1243  
1244  	if err != nil {
1245  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1246  			Message: fmt.Sprintf("Failed to sign message: %s", err.Error()),
1247  		})
1248  	}
1249  	return c.JSON(http.StatusOK, signMessageResponse)
1250  }
1251  
1252  func (httpSvc *HttpService) appsListHandler(c echo.Context) error {
1253  	limit := uint64(0)
1254  	offset := uint64(0)
1255  
1256  	if limitParam := c.QueryParam("limit"); limitParam != "" {
1257  		if parsedLimit, err := strconv.ParseUint(limitParam, 10, 64); err == nil {
1258  			limit = parsedLimit
1259  		}
1260  	}
1261  
1262  	if offsetParam := c.QueryParam("offset"); offsetParam != "" {
1263  		if parsedOffset, err := strconv.ParseUint(offsetParam, 10, 64); err == nil {
1264  			offset = parsedOffset
1265  		}
1266  	}
1267  
1268  	filtersJSON := c.QueryParam("filters")
1269  	var filters api.ListAppsFilters
1270  	if filtersJSON != "" {
1271  		err := json.Unmarshal([]byte(filtersJSON), &filters)
1272  		if err != nil {
1273  			logger.Logger.WithError(err).WithFields(logrus.Fields{
1274  				"filters": filtersJSON,
1275  			}).Error("Failed to deserialize app filters")
1276  			return err
1277  		}
1278  	}
1279  
1280  	orderBy := c.QueryParam("order_by")
1281  
1282  	apps, err := httpSvc.api.ListApps(limit, offset, filters, orderBy)
1283  
1284  	if err != nil {
1285  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1286  			Message: err.Error(),
1287  		})
1288  	}
1289  
1290  	return c.JSON(http.StatusOK, apps)
1291  }
1292  
1293  func (httpSvc *HttpService) appsShowByPubkeyHandler(c echo.Context) error {
1294  	dbApp := httpSvc.appsSvc.GetAppByPubkey(c.Param("pubkey"))
1295  
1296  	if dbApp == nil {
1297  		return c.JSON(http.StatusNotFound, ErrorResponse{
1298  			Message: "App not found",
1299  		})
1300  	}
1301  
1302  	response, err := httpSvc.api.GetApp(dbApp)
1303  	if err != nil {
1304  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1305  			Message: err.Error(),
1306  		})
1307  	}
1308  
1309  	return c.JSON(http.StatusOK, response)
1310  }
1311  
1312  func (httpSvc *HttpService) appsShowHandler(c echo.Context) error {
1313  	appIdStr := c.Param("id")
1314  	if appIdStr == "" {
1315  		return c.JSON(http.StatusBadRequest, ErrorResponse{
1316  			Message: "App ID is required",
1317  		})
1318  	}
1319  
1320  	appId, err := strconv.ParseUint(appIdStr, 10, 64)
1321  	if err != nil {
1322  		return c.JSON(http.StatusBadRequest, ErrorResponse{
1323  			Message: "Invalid App ID",
1324  		})
1325  	}
1326  
1327  	dbApp := httpSvc.appsSvc.GetAppById(uint(appId))
1328  
1329  	if dbApp == nil {
1330  		return c.JSON(http.StatusNotFound, ErrorResponse{
1331  			Message: "App not found",
1332  		})
1333  	}
1334  
1335  	response, err := httpSvc.api.GetApp(dbApp)
1336  	if err != nil {
1337  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1338  			Message: err.Error(),
1339  		})
1340  	}
1341  
1342  	return c.JSON(http.StatusOK, response)
1343  }
1344  
1345  func (httpSvc *HttpService) appsUpdateHandler(c echo.Context) error {
1346  	var requestData api.UpdateAppRequest
1347  	if err := c.Bind(&requestData); err != nil {
1348  		return c.JSON(http.StatusBadRequest, ErrorResponse{
1349  			Message: fmt.Sprintf("Bad request: %s", err.Error()),
1350  		})
1351  	}
1352  
1353  	dbApp := httpSvc.appsSvc.GetAppByPubkey(c.Param("pubkey"))
1354  
1355  	if dbApp == nil {
1356  		return c.JSON(http.StatusNotFound, ErrorResponse{
1357  			Message: "App not found",
1358  		})
1359  	}
1360  
1361  	err := httpSvc.api.UpdateApp(dbApp, &requestData)
1362  
1363  	if err != nil {
1364  		logger.Logger.WithError(err).Error("Failed to update app")
1365  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1366  			Message: fmt.Sprintf("Failed to update app: %v", err),
1367  		})
1368  	}
1369  
1370  	return c.NoContent(http.StatusNoContent)
1371  }
1372  
1373  func (httpSvc *HttpService) transfersHandler(c echo.Context) error {
1374  	var requestData api.TransferRequest
1375  	if err := c.Bind(&requestData); err != nil {
1376  		return c.JSON(http.StatusBadRequest, ErrorResponse{
1377  			Message: fmt.Sprintf("Bad request: %s", err.Error()),
1378  		})
1379  	}
1380  
1381  	amountMsat := uint64(0)
1382  	resolvedAmountMsat := api.ResolveToMsat(requestData.AmountSat, requestData.AmountMsat, nil, nil)
1383  	if resolvedAmountMsat != nil {
1384  		amountMsat = *resolvedAmountMsat
1385  	}
1386  
1387  	err := httpSvc.api.Transfer(c.Request().Context(), requestData.FromAppId, requestData.ToAppId, amountMsat, requestData.Description)
1388  
1389  	if err != nil {
1390  		logger.Logger.WithError(err).Error("Failed to transfer funds")
1391  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1392  			Message: fmt.Sprintf("Failed to transfer funds: %v", err),
1393  		})
1394  	}
1395  
1396  	return c.NoContent(http.StatusNoContent)
1397  }
1398  
1399  func (httpSvc *HttpService) appsDeleteHandler(c echo.Context) error {
1400  	dbApp := httpSvc.appsSvc.GetAppByPubkey(c.Param("pubkey"))
1401  	if dbApp == nil {
1402  		return c.JSON(http.StatusNotFound, ErrorResponse{
1403  			Message: "App not found",
1404  		})
1405  	}
1406  
1407  	if err := httpSvc.api.DeleteApp(dbApp); err != nil {
1408  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1409  			Message: "Failed to delete app",
1410  		})
1411  	}
1412  	return c.NoContent(http.StatusNoContent)
1413  }
1414  
1415  func (httpSvc *HttpService) appsCreateHandler(c echo.Context) error {
1416  	var requestData api.CreateAppRequest
1417  	if err := c.Bind(&requestData); err != nil {
1418  		return c.JSON(http.StatusBadRequest, ErrorResponse{
1419  			Message: fmt.Sprintf("Bad request: %s", err.Error()),
1420  		})
1421  	}
1422  
1423  	responseBody, err := httpSvc.api.CreateApp(&requestData)
1424  
1425  	if err != nil {
1426  		logger.Logger.WithField("appName", requestData.Name).WithError(err).Error("Failed to save app")
1427  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1428  			Message: fmt.Sprintf("Failed to save app: %v", err),
1429  		})
1430  	}
1431  
1432  	return c.JSON(http.StatusOK, responseBody)
1433  }
1434  
1435  func (httpSvc *HttpService) lightningAddressesCreateHandler(c echo.Context) error {
1436  	var requestData api.CreateLightningAddressRequest
1437  	if err := c.Bind(&requestData); err != nil {
1438  		return c.JSON(http.StatusBadRequest, ErrorResponse{
1439  			Message: fmt.Sprintf("Bad request: %s", err.Error()),
1440  		})
1441  	}
1442  
1443  	err := httpSvc.api.CreateLightningAddress(c.Request().Context(), &requestData)
1444  
1445  	if err != nil {
1446  		logger.Logger.WithField("request", requestData).WithError(err).Error("Failed to create lightning address")
1447  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1448  			Message: err.Error(),
1449  		})
1450  	}
1451  
1452  	return c.NoContent(http.StatusNoContent)
1453  }
1454  
1455  func (httpSvc *HttpService) lightningAddressesDeleteHandler(c echo.Context) error {
1456  	appIdStr := c.Param("appId")
1457  	if appIdStr == "" {
1458  		return c.JSON(http.StatusBadRequest, ErrorResponse{
1459  			Message: "App ID is required",
1460  		})
1461  	}
1462  
1463  	appId, err := strconv.ParseUint(appIdStr, 10, 64)
1464  	if err != nil {
1465  		return c.JSON(http.StatusBadRequest, ErrorResponse{
1466  			Message: "Invalid App ID",
1467  		})
1468  	}
1469  
1470  	err = httpSvc.api.DeleteLightningAddress(c.Request().Context(), uint(appId))
1471  	if err != nil {
1472  		logger.Logger.WithField("appId", appId).WithError(err).Error("Failed to delete lightning address")
1473  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1474  			Message: err.Error(),
1475  		})
1476  	}
1477  
1478  	return c.NoContent(http.StatusNoContent)
1479  }
1480  
1481  func (httpSvc *HttpService) setupHandler(c echo.Context) error {
1482  	var setupRequest api.SetupRequest
1483  	if err := c.Bind(&setupRequest); err != nil {
1484  		return c.JSON(http.StatusBadRequest, ErrorResponse{
1485  			Message: fmt.Sprintf("Bad request: %s", err.Error()),
1486  		})
1487  	}
1488  
1489  	err := httpSvc.api.Setup(c.Request().Context(), &setupRequest)
1490  	if err != nil {
1491  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1492  			Message: fmt.Sprintf("Failed to setup node: %s", err.Error()),
1493  		})
1494  	}
1495  
1496  	return c.NoContent(http.StatusNoContent)
1497  }
1498  
1499  func (httpSvc *HttpService) getLogOutputHandler(c echo.Context) error {
1500  	var getLogRequest api.GetLogOutputRequest
1501  	if err := c.Bind(&getLogRequest); err != nil {
1502  		return c.JSON(http.StatusBadRequest, ErrorResponse{
1503  			Message: fmt.Sprintf("Bad request: %s", err.Error()),
1504  		})
1505  	}
1506  
1507  	logType := c.Param("type")
1508  	if logType != api.LogTypeNode && logType != api.LogTypeApp {
1509  		return c.JSON(http.StatusBadRequest, ErrorResponse{
1510  			Message: fmt.Sprintf("Invalid log type parameter: '%s'", logType),
1511  		})
1512  	}
1513  
1514  	getLogResponse, err := httpSvc.api.GetLogOutput(c.Request().Context(), logType, &getLogRequest)
1515  	if err != nil {
1516  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1517  			Message: fmt.Sprintf("Failed to get log output: %v", err),
1518  		})
1519  	}
1520  
1521  	return c.JSON(http.StatusOK, getLogResponse)
1522  }
1523  
1524  func (httpSvc *HttpService) getCustomNodeCommandsHandler(c echo.Context) error {
1525  	nodeCommandsResponse, err := httpSvc.api.GetCustomNodeCommands()
1526  	if err != nil {
1527  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1528  			Message: fmt.Sprintf("Failed to get node commands: %v", err),
1529  		})
1530  	}
1531  
1532  	return c.JSON(http.StatusOK, nodeCommandsResponse)
1533  }
1534  
1535  func (httpSvc *HttpService) execCustomNodeCommandHandler(c echo.Context) error {
1536  	var execCommandRequest api.ExecuteCustomNodeCommandRequest
1537  	if err := c.Bind(&execCommandRequest); err != nil {
1538  		return c.JSON(http.StatusBadRequest, ErrorResponse{
1539  			Message: fmt.Sprintf("Bad request: %s", err.Error()),
1540  		})
1541  	}
1542  
1543  	execCommandResponse, err := httpSvc.api.ExecuteCustomNodeCommand(c.Request().Context(), execCommandRequest.Command)
1544  	if err != nil {
1545  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1546  			Message: fmt.Sprintf("Failed to execute command: %v", err),
1547  		})
1548  	}
1549  
1550  	return c.JSON(http.StatusOK, execCommandResponse)
1551  }
1552  
1553  func (httpSvc *HttpService) logoutHandler(c echo.Context) error {
1554  	c.SetCookie(&http.Cookie{
1555  		Name:     nip07SessionCookie,
1556  		Value:    "",
1557  		Path:     "/",
1558  		HttpOnly: true,
1559  		Secure:   c.IsTLS() || c.Request().Header.Get("X-Forwarded-Proto") == "https",
1560  		SameSite: http.SameSiteLaxMode,
1561  		MaxAge:   -1,
1562  		Expires:  time.Unix(0, 0),
1563  	})
1564  
1565  	redirectUrl := httpSvc.cfg.GetEnv().GetBaseFrontendUrl()
1566  	if redirectUrl == "" {
1567  		redirectUrl = "/"
1568  	}
1569  
1570  	return c.Redirect(http.StatusFound, redirectUrl)
1571  }
1572  
1573  func (httpSvc *HttpService) createBackupHandler(c echo.Context) error {
1574  	var backupRequest api.BasicBackupRequest
1575  	if err := c.Bind(&backupRequest); err != nil {
1576  		return c.JSON(http.StatusBadRequest, ErrorResponse{
1577  			Message: fmt.Sprintf("Bad request: %s", err.Error()),
1578  		})
1579  	}
1580  
1581  	if !httpSvc.cfg.CheckUnlockPassword(backupRequest.UnlockPassword) {
1582  		return c.JSON(http.StatusUnauthorized, ErrorResponse{
1583  			Message: "Invalid password",
1584  		})
1585  	}
1586  
1587  	var buffer bytes.Buffer
1588  	err := httpSvc.api.CreateBackup(backupRequest.UnlockPassword, &buffer)
1589  	if err != nil {
1590  		return c.String(500, fmt.Sprintf("Failed to create backup: %v", err))
1591  	}
1592  
1593  	c.Response().Header().Set("Content-Type", "application/octet-stream")
1594  	c.Response().Header().Set("Content-Disposition", "attachment; filename=albyhub.bkp")
1595  	c.Response().WriteHeader(http.StatusOK)
1596  	c.Response().Write(buffer.Bytes())
1597  	return nil
1598  }
1599  
1600  func (httpSvc *HttpService) restoreBackupHandler(c echo.Context) error {
1601  	info, err := httpSvc.api.GetInfo(c.Request().Context())
1602  	if err != nil {
1603  		return err
1604  	}
1605  	if info.SetupCompleted {
1606  		return errors.New("setup already completed")
1607  	}
1608  
1609  	password := c.FormValue("unlockPassword")
1610  
1611  	fileHeader, err := c.FormFile("backup")
1612  	if err != nil {
1613  		return c.JSON(http.StatusBadRequest, ErrorResponse{
1614  			Message: fmt.Sprintf("Failed to get backup file header: %v", err),
1615  		})
1616  	}
1617  
1618  	file, err := fileHeader.Open()
1619  	if err != nil {
1620  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1621  			Message: fmt.Sprintf("Failed to open backup file: %v", err),
1622  		})
1623  	}
1624  	defer file.Close()
1625  
1626  	err = httpSvc.api.RestoreBackup(password, file)
1627  	if err != nil {
1628  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1629  			Message: fmt.Sprintf("Failed to restore backup: %v", err),
1630  		})
1631  	}
1632  
1633  	return c.NoContent(http.StatusNoContent)
1634  }
1635  
1636  func (httpSvc *HttpService) healthHandler(c echo.Context) error {
1637  	healthResponse, err := httpSvc.api.Health(c.Request().Context())
1638  	if err != nil {
1639  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1640  			Message: fmt.Sprintf("Failed to check node health: %v", err),
1641  		})
1642  	}
1643  
1644  	return c.JSON(http.StatusOK, healthResponse)
1645  }
1646  
1647  func (httpSvc *HttpService) listSwapsHandler(c echo.Context) error {
1648  	swaps, err := httpSvc.api.ListSwaps()
1649  
1650  	if err != nil {
1651  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1652  			Message: err.Error(),
1653  		})
1654  	}
1655  
1656  	return c.JSON(http.StatusOK, swaps)
1657  }
1658  
1659  func (httpSvc *HttpService) lookupSwapHandler(c echo.Context) error {
1660  	swap, err := httpSvc.api.LookupSwap(c.Param("swapId"))
1661  	if err != nil {
1662  		return c.JSON(http.StatusNotFound, ErrorResponse{
1663  			Message: "App not found",
1664  		})
1665  	}
1666  
1667  	return c.JSON(http.StatusOK, swap)
1668  }
1669  
1670  func (httpSvc *HttpService) getSwapOutInfoHandler(c echo.Context) error {
1671  	swapOutFeesResponse, err := httpSvc.api.GetSwapOutInfo()
1672  	if err != nil {
1673  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1674  			Message: fmt.Sprintf("Failed to get swap out info: %v", err),
1675  		})
1676  	}
1677  
1678  	return c.JSON(http.StatusOK, swapOutFeesResponse)
1679  }
1680  
1681  func (httpSvc *HttpService) getSwapInInfoHandler(c echo.Context) error {
1682  	swapOutFeesResponse, err := httpSvc.api.GetSwapInInfo()
1683  	if err != nil {
1684  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1685  			Message: fmt.Sprintf("Failed to get swap in info: %v", err),
1686  		})
1687  	}
1688  
1689  	return c.JSON(http.StatusOK, swapOutFeesResponse)
1690  }
1691  
1692  func (httpSvc *HttpService) initiateSwapOutHandler(c echo.Context) error {
1693  	var initiateSwapOutRequest api.InitiateSwapRequest
1694  	if err := c.Bind(&initiateSwapOutRequest); err != nil {
1695  		return c.JSON(http.StatusBadRequest, ErrorResponse{
1696  			Message: fmt.Sprintf("Bad request: %s", err.Error()),
1697  		})
1698  	}
1699  
1700  	swapOutResponse, err := httpSvc.api.InitiateSwapOut(c.Request().Context(), &initiateSwapOutRequest)
1701  	if err != nil {
1702  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1703  			Message: fmt.Sprintf("Failed to initiate swap out: %v", err),
1704  		})
1705  	}
1706  
1707  	return c.JSON(http.StatusOK, swapOutResponse)
1708  }
1709  
1710  func (httpSvc *HttpService) initiateSwapInHandler(c echo.Context) error {
1711  	var initiateSwapInRequest api.InitiateSwapRequest
1712  	if err := c.Bind(&initiateSwapInRequest); err != nil {
1713  		return c.JSON(http.StatusBadRequest, ErrorResponse{
1714  			Message: fmt.Sprintf("Bad request: %s", err.Error()),
1715  		})
1716  	}
1717  
1718  	txId, err := httpSvc.api.InitiateSwapIn(c.Request().Context(), &initiateSwapInRequest)
1719  	if err != nil {
1720  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1721  			Message: fmt.Sprintf("Failed to initiate swap in: %v", err),
1722  		})
1723  	}
1724  
1725  	return c.JSON(http.StatusOK, txId)
1726  }
1727  
1728  func (httpSvc *HttpService) refundSwapHandler(c echo.Context) error {
1729  	var refundSwapInRequest api.RefundSwapRequest
1730  	if err := c.Bind(&refundSwapInRequest); err != nil {
1731  		return c.JSON(http.StatusBadRequest, ErrorResponse{
1732  			Message: fmt.Sprintf("Bad request: %s", err.Error()),
1733  		})
1734  	}
1735  
1736  	err := httpSvc.api.RefundSwap(&refundSwapInRequest)
1737  	if err != nil {
1738  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1739  			Message: err.Error(),
1740  		})
1741  	}
1742  
1743  	return c.NoContent(http.StatusNoContent)
1744  }
1745  
1746  func (httpSvc *HttpService) swapMnemonicHandler(c echo.Context) error {
1747  	mnemonic := httpSvc.api.GetSwapMnemonic()
1748  	return c.JSON(http.StatusOK, mnemonic)
1749  }
1750  
1751  func (httpSvc *HttpService) getAutoSwapConfigHandler(c echo.Context) error {
1752  	getAutoSwapConfigResponse, err := httpSvc.api.GetAutoSwapConfig()
1753  	if err != nil {
1754  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1755  			Message: fmt.Sprintf("Failed to get swap settings: %v", err),
1756  		})
1757  	}
1758  
1759  	return c.JSON(http.StatusOK, getAutoSwapConfigResponse)
1760  }
1761  
1762  func (httpSvc *HttpService) enableAutoSwapOutHandler(c echo.Context) error {
1763  	var enableAutoSwapRequest api.EnableAutoSwapRequest
1764  	if err := c.Bind(&enableAutoSwapRequest); err != nil {
1765  		return c.JSON(http.StatusBadRequest, ErrorResponse{
1766  			Message: fmt.Sprintf("Bad request: %s", err.Error()),
1767  		})
1768  	}
1769  
1770  	err := httpSvc.api.EnableAutoSwapOut(c.Request().Context(), &enableAutoSwapRequest)
1771  	if err != nil {
1772  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1773  			Message: fmt.Sprintf("Failed to save swap settings: %v", err),
1774  		})
1775  	}
1776  
1777  	return c.NoContent(http.StatusNoContent)
1778  }
1779  
1780  func (httpSvc *HttpService) disableAutoSwapOutHandler(c echo.Context) error {
1781  	err := httpSvc.api.DisableAutoSwap()
1782  
1783  	if err != nil {
1784  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1785  			Message: err.Error(),
1786  		})
1787  	}
1788  
1789  	return c.NoContent(http.StatusNoContent)
1790  }
1791  
1792  func (httpSvc *HttpService) setNodeAliasHandler(c echo.Context) error {
1793  	var setNodeAliasRequest api.SetNodeAliasRequest
1794  	if err := c.Bind(&setNodeAliasRequest); err != nil {
1795  		return c.JSON(http.StatusBadRequest, ErrorResponse{
1796  			Message: fmt.Sprintf("Bad request: %s", err.Error()),
1797  		})
1798  	}
1799  
1800  	err := httpSvc.api.SetNodeAlias(setNodeAliasRequest.NodeAlias)
1801  	if err != nil {
1802  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1803  			Message: fmt.Sprintf("Failed to set node alias: %s", err.Error()),
1804  		})
1805  	}
1806  
1807  	return c.NoContent(http.StatusNoContent)
1808  }
1809  
1810  func (httpSvc *HttpService) forwardsHandler(c echo.Context) error {
1811  	forwards, err := httpSvc.api.GetForwards()
1812  	if err != nil {
1813  		return c.JSON(http.StatusInternalServerError, ErrorResponse{
1814  			Message: fmt.Sprintf("Failed to get forwards: %s", err.Error()),
1815  		})
1816  	}
1817  
1818  	return c.JSON(http.StatusOK, forwards)
1819  }
1820