2024-05-30 00:06:06 +07:00
package api
import (
"context"
2026-04-21 22:33:25 +05:30
"encoding/hex"
2024-05-30 00:06:06 +07:00
"encoding/json"
"errors"
2025-01-30 14:20:17 +03:00
"flag"
2024-05-30 00:06:06 +07:00
"fmt"
"io"
"net/http"
"net/url"
2026-04-21 22:33:25 +05:30
"os"
2024-07-01 20:30:40 +07:00
"slices"
2025-04-16 20:58:32 +05:30
"strconv"
2025-03-06 15:27:32 +07:00
"strings"
2024-06-19 23:25:39 +07:00
"sync"
2024-05-30 00:06:06 +07:00
"time"
"github.com/sirupsen/logrus"
2024-09-03 21:31:28 +07:00
"gorm.io/datatypes"
2024-05-30 00:06:06 +07:00
"gorm.io/gorm"
2024-07-05 20:32:40 +07:00
"github.com/getAlby/hub/alby"
2024-10-28 13:29:34 +07:00
"github.com/getAlby/hub/apps"
2024-07-05 20:32:40 +07:00
"github.com/getAlby/hub/config"
2024-07-19 23:30:22 +07:00
"github.com/getAlby/hub/constants"
2024-07-05 20:32:40 +07:00
"github.com/getAlby/hub/db"
2024-07-19 23:30:22 +07:00
"github.com/getAlby/hub/db/queries"
2024-07-05 20:32:40 +07:00
"github.com/getAlby/hub/events"
"github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/logger"
permissions "github.com/getAlby/hub/nip47/permissions"
"github.com/getAlby/hub/service"
"github.com/getAlby/hub/service/keys"
2025-07-10 18:36:12 +05:30
"github.com/getAlby/hub/swaps"
2024-07-05 20:32:40 +07:00
"github.com/getAlby/hub/utils"
"github.com/getAlby/hub/version"
2024-05-30 00:06:06 +07:00
)
type api struct {
2024-09-04 10:46:12 +05:30
db * gorm . DB
2024-10-28 13:29:34 +07:00
appsSvc apps . AppsService
2024-09-04 10:46:12 +05:30
cfg config . Config
svc service . Service
permissionsSvc permissions . PermissionsService
keys keys . Keys
albyOAuthSvc alby . AlbyOAuthService
2025-08-27 23:34:15 +07:00
albySvc alby . AlbyService
2024-09-04 10:46:12 +05:30
startupError error
startupErrorTime time . Time
2025-07-11 15:08:32 +07:00
eventPublisher events . EventPublisher
2024-05-30 00:06:06 +07:00
}
2025-08-27 23:34:15 +07:00
func NewAPI ( svc service . Service , gormDB * gorm . DB , config config . Config , keys keys . Keys , albySvc alby . AlbyService , albyOAuthSvc alby . AlbyOAuthService , eventPublisher events . EventPublisher ) * api {
2024-05-30 00:06:06 +07:00
return & api {
2024-06-17 19:42:09 +07:00
db : gormDB ,
2025-02-27 19:12:55 +07:00
appsSvc : apps . NewAppsService ( gormDB , eventPublisher , keys , config ) ,
2024-06-17 19:42:09 +07:00
cfg : config ,
svc : svc ,
2024-06-29 22:29:55 +07:00
permissionsSvc : permissions . NewPermissionsService ( gormDB , eventPublisher ) ,
2024-06-17 19:42:09 +07:00
keys : keys ,
2025-08-27 23:34:15 +07:00
albySvc : albySvc ,
2024-06-17 19:42:09 +07:00
albyOAuthSvc : albyOAuthSvc ,
2025-07-11 15:08:32 +07:00
eventPublisher : eventPublisher ,
2024-05-30 00:06:06 +07:00
}
}
func ( api * api ) CreateApp ( createAppRequest * CreateAppRequest ) ( * CreateAppResponse , error ) {
2025-02-27 19:12:55 +07:00
if slices . Contains ( createAppRequest . Scopes , constants . SUPERUSER_SCOPE ) {
if ! api . cfg . CheckUnlockPassword ( createAppRequest . UnlockPassword ) {
return nil , fmt . Errorf (
"incorrect unlock password to create app with superuser permission" )
}
2024-11-26 20:30:53 -06:00
}
2026-05-01 15:41:56 +05:30
maxAmountSat := uint64 ( 0 )
resolvedMaxAmountSat := ResolveToSat ( createAppRequest . MaxAmountSat , createAppRequest . MaxAmountMsat , createAppRequest . MaxAmount , nil )
if resolvedMaxAmountSat != nil {
maxAmountSat = * resolvedMaxAmountSat
}
2025-07-14 07:45:48 +02:00
if createAppRequest . Name == alby . ALBY_ACCOUNT_APP_NAME {
return nil , fmt . Errorf ( "Reserved app name: %s" , alby . ALBY_ACCOUNT_APP_NAME )
}
2024-05-30 00:06:06 +07:00
expiresAt , err := api . parseExpiresAt ( createAppRequest . ExpiresAt )
if err != nil {
return nil , fmt . Errorf ( "invalid expiresAt: %v" , err )
}
2024-07-01 20:30:40 +07:00
for _ , scope := range createAppRequest . Scopes {
if ! slices . Contains ( permissions . AllScopes ( ) , scope ) {
return nil , fmt . Errorf ( "did not recognize requested scope: %s" , scope )
2024-06-17 19:42:09 +07:00
}
}
2024-10-28 13:29:34 +07:00
app , pairingSecretKey , err := api . appsSvc . CreateApp (
2024-07-19 23:30:22 +07:00
createAppRequest . Name ,
createAppRequest . Pubkey ,
2026-05-01 15:41:56 +05:30
maxAmountSat ,
2024-07-19 23:30:22 +07:00
createAppRequest . BudgetRenewal ,
expiresAt ,
createAppRequest . Scopes ,
2024-09-02 17:40:34 +07:00
createAppRequest . Isolated ,
2024-11-07 13:06:01 +01:00
createAppRequest . Metadata ,
)
2024-05-30 00:06:06 +07:00
if err != nil {
return nil , err
}
2025-11-06 18:01:09 +07:00
relayUrls := api . cfg . GetRelayUrls ( )
2024-05-30 00:06:06 +07:00
2025-02-06 12:30:28 +07:00
lightningAddress , err := api . albyOAuthSvc . GetLightningAddress ( )
if err != nil {
return nil , err
}
2024-05-30 00:06:06 +07:00
responseBody := & CreateAppResponse { }
2024-08-23 14:15:15 +05:30
responseBody . Id = app . ID
2025-02-27 19:12:55 +07:00
responseBody . Name = app . Name
2024-11-07 13:06:01 +01:00
responseBody . Pubkey = app . AppPubkey
2024-05-30 00:06:06 +07:00
responseBody . PairingSecret = pairingSecretKey
2025-01-31 12:08:18 +07:00
responseBody . WalletPubkey = * app . WalletPubkey
2025-11-06 18:01:09 +07:00
responseBody . RelayUrls = relayUrls
2025-02-06 12:30:28 +07:00
responseBody . Lud16 = lightningAddress
2024-09-03 07:52:54 +02:00
2024-05-30 00:06:06 +07:00
if createAppRequest . ReturnTo != "" {
returnToUrl , err := url . Parse ( createAppRequest . ReturnTo )
if err == nil {
query := returnToUrl . Query ( )
2025-11-06 18:01:09 +07:00
for _ , relayUrl := range relayUrls {
query . Add ( "relay" , relayUrl )
}
2024-11-07 13:06:01 +01:00
query . Add ( "pubkey" , * app . WalletPubkey )
2024-09-04 20:57:27 +02:00
if lightningAddress != "" && ! app . Isolated {
2024-09-03 07:52:54 +02:00
query . Add ( "lud16" , lightningAddress )
}
2024-05-30 00:06:06 +07:00
returnToUrl . RawQuery = query . Encode ( )
responseBody . ReturnTo = returnToUrl . String ( )
}
}
var lud16 string
2024-09-04 20:57:27 +02:00
if lightningAddress != "" && ! app . Isolated {
2024-09-03 07:52:54 +02:00
lud16 = fmt . Sprintf ( "&lud16=%s" , lightningAddress )
}
2025-11-06 18:01:09 +07:00
responseBody . PairingUri = fmt . Sprintf ( "nostr+walletconnect://%s?relay=%s&secret=%s%s" , * app . WalletPubkey , strings . Join ( relayUrls , "&relay=" ) , pairingSecretKey , lud16 )
2024-11-07 13:06:01 +01:00
2024-05-30 00:06:06 +07:00
return responseBody , nil
}
func ( api * api ) UpdateApp ( userApp * db . App , updateAppRequest * UpdateAppRequest ) error {
2026-05-01 15:41:56 +05:30
resolvedMaxAmountSat := ResolveToSat ( updateAppRequest . MaxAmountSat , updateAppRequest . MaxAmountMsat , updateAppRequest . MaxAmount , nil )
2025-10-14 13:12:55 +05:30
err := api . db . Transaction ( func ( tx * gorm . DB ) error {
// Initialize name with current app name, update if provided
name := userApp . Name
2024-08-22 23:45:48 +05:30
2025-10-14 13:12:55 +05:30
// Update app name if provided and different
if updateAppRequest . Name != nil {
name = * updateAppRequest . Name
2024-05-30 00:06:06 +07:00
2025-10-14 13:12:55 +05:30
if name == "" {
return fmt . Errorf ( "won't update an app to have no name" )
}
if name != userApp . Name {
err := tx . Model ( & db . App { } ) . Where ( "id" , userApp . ID ) . Update ( "name" , name ) . Error
if err != nil {
return err
}
2024-08-22 23:45:48 +05:30
}
}
2025-10-14 13:12:55 +05:30
// Update app isolation if provided and different
if updateAppRequest . Isolated != nil {
isolated := * updateAppRequest . Isolated
if isolated != userApp . Isolated {
if ! isolated {
var existingMetadata Metadata
if userApp . Metadata != nil {
err := json . Unmarshal ( userApp . Metadata , & existingMetadata )
if err != nil {
logger . Logger . WithError ( err ) . WithFields ( logrus . Fields {
"app_id" : userApp . ID ,
} ) . Error ( "Failed to deserialize app metadata" )
return err
}
2026-02-26 12:25:06 +05:30
if existingMetadata [ constants . METADATA_APPSTORE_APP_ID_KEY ] == constants . SUBWALLET_APPSTORE_APP_ID {
2025-10-14 13:12:55 +05:30
return errors . New ( "Cannot update sub-wallet to be non-isolated" )
}
}
}
err := tx . Model ( & db . App { } ) . Where ( "id" , userApp . ID ) . Update ( "isolated" , isolated ) . Error
if err != nil {
return err
}
2024-11-26 20:30:53 -06:00
}
}
2025-10-14 13:12:55 +05:30
// Update the app metadata if provided
2024-09-03 21:31:28 +07:00
if updateAppRequest . Metadata != nil {
var metadataBytes [ ] byte
var err error
2025-10-14 13:12:55 +05:30
metadataBytes , err = json . Marshal ( * updateAppRequest . Metadata )
2024-09-03 21:31:28 +07:00
if err != nil {
logger . Logger . WithError ( err ) . Error ( "Failed to serialize metadata" )
return err
}
err = tx . Model ( & db . App { } ) . Where ( "id" , userApp . ID ) . Update ( "metadata" , datatypes . JSON ( metadataBytes ) ) . Error
if err != nil {
return err
}
}
2025-10-14 13:12:55 +05:30
// Handle permissions updates only if any permission-related field is provided
2026-05-01 15:41:56 +05:30
if updateAppRequest . Scopes != nil || resolvedMaxAmountSat != nil ||
2025-10-14 13:12:55 +05:30
updateAppRequest . BudgetRenewal != nil || updateAppRequest . ExpiresAt != nil || updateAppRequest . UpdateExpiresAt {
2024-05-30 00:06:06 +07:00
2025-10-14 13:12:55 +05:30
// Get current values or use provided ones
2026-04-17 13:53:28 +05:30
var maxAmountSat uint64
2025-10-14 13:12:55 +05:30
var budgetRenewal string
var expiresAt * time . Time
2024-05-30 00:06:06 +07:00
2025-10-14 13:12:55 +05:30
// Get existing permissions to use as defaults
var existingPermissions [ ] db . AppPermission
if err := tx . Where ( "app_id = ?" , userApp . ID ) . Find ( & existingPermissions ) . Error ; err != nil {
return err
}
2025-02-27 19:12:55 +07:00
2025-10-14 13:12:55 +05:30
// Use existing values as defaults
if len ( existingPermissions ) > 0 {
// Find pay_invoice permission for budget-related fields
for _ , perm := range existingPermissions {
if perm . Scope == constants . PAY_INVOICE_SCOPE {
2026-04-17 13:53:28 +05:30
maxAmountSat = uint64 ( perm . MaxAmountSat )
2025-10-14 13:12:55 +05:30
budgetRenewal = perm . BudgetRenewal
expiresAt = perm . ExpiresAt
break
}
2024-05-30 00:06:06 +07:00
}
2025-10-14 13:12:55 +05:30
}
// Override with provided values
2026-05-01 15:41:56 +05:30
if resolvedMaxAmountSat != nil {
maxAmountSat = * resolvedMaxAmountSat
2025-10-14 13:12:55 +05:30
}
if updateAppRequest . BudgetRenewal != nil {
budgetRenewal = * updateAppRequest . BudgetRenewal
}
if updateAppRequest . ExpiresAt != nil {
parsedExpiresAt , err := api . parseExpiresAt ( * updateAppRequest . ExpiresAt )
if err != nil {
return fmt . Errorf ( "invalid expiresAt: %v" , err )
2024-05-30 00:06:06 +07:00
}
2025-10-14 13:12:55 +05:30
expiresAt = parsedExpiresAt
}
if updateAppRequest . ExpiresAt == nil && updateAppRequest . UpdateExpiresAt {
expiresAt = nil
2024-05-30 00:06:06 +07:00
}
2025-10-14 13:12:55 +05:30
// Update existing permissions with new budget and expiry
err := tx . Model ( & db . AppPermission { } ) . Where ( "app_id" , userApp . ID ) . Updates ( map [ string ] interface { } {
"ExpiresAt" : expiresAt ,
2026-04-17 13:53:28 +05:30
"MaxAmountSat" : maxAmountSat ,
2025-10-14 13:12:55 +05:30
"BudgetRenewal" : budgetRenewal ,
} ) . Error
if err != nil {
2024-05-30 00:06:06 +07:00
return err
}
2025-10-14 13:12:55 +05:30
// Handle scope changes only if scopes were provided
if updateAppRequest . Scopes != nil {
if len ( updateAppRequest . Scopes ) == 0 {
return fmt . Errorf ( "won't update an app to have no request methods" )
}
existingScopeMap := make ( map [ string ] bool )
for _ , perm := range existingPermissions {
existingScopeMap [ perm . Scope ] = true
}
if slices . Contains ( updateAppRequest . Scopes , constants . SUPERUSER_SCOPE ) && ! existingScopeMap [ constants . SUPERUSER_SCOPE ] {
return fmt . Errorf ( "cannot update app to add superuser permission" )
}
// Add new permissions
for _ , scope := range updateAppRequest . Scopes {
if ! existingScopeMap [ scope ] {
perm := db . AppPermission {
App : * userApp ,
Scope : scope ,
ExpiresAt : expiresAt ,
2026-04-17 13:53:28 +05:30
MaxAmountSat : int ( maxAmountSat ) ,
2025-10-14 13:12:55 +05:30
BudgetRenewal : budgetRenewal ,
}
if err := tx . Create ( & perm ) . Error ; err != nil {
return err
}
}
delete ( existingScopeMap , scope )
}
// Remove old permissions
for scope := range existingScopeMap {
if err := tx . Where ( "app_id = ? AND scope = ?" , userApp . ID , scope ) . Delete ( & db . AppPermission { } ) . Error ; err != nil {
return err
}
}
}
2024-05-30 00:06:06 +07:00
}
2025-10-14 13:12:55 +05:30
// Publish update event
2024-12-20 12:54:45 +01:00
api . svc . GetEventPublisher ( ) . Publish ( & events . Event {
Event : "nwc_app_updated" ,
Properties : map [ string ] interface { } {
"name" : name ,
"id" : userApp . ID ,
} ,
} )
2024-05-30 00:06:06 +07:00
// commit transaction
return nil
} )
return err
}
func ( api * api ) DeleteApp ( userApp * db . App ) error {
2025-11-06 12:11:12 +01:00
// Delete lightning address if one exists
if api . appsSvc . HasLightningAddress ( userApp ) {
err := api . DeleteLightningAddress ( context . Background ( ) , userApp . ID )
if err != nil {
logger . Logger . WithError ( err ) . WithFields ( logrus . Fields {
"app_id" : userApp . ID ,
} ) . Error ( "Failed to delete lightning address during app deletion" )
}
}
2024-11-07 13:06:01 +01:00
return api . appsSvc . DeleteApp ( userApp )
2024-05-30 00:06:06 +07:00
}
2025-07-08 22:57:41 +07:00
func ( api * api ) CreateLightningAddress ( ctx context . Context , createLightningAddressRequest * CreateLightningAddressRequest ) error {
app := api . appsSvc . GetAppById ( createLightningAddressRequest . AppId )
if app == nil {
return errors . New ( "app not found" )
}
var metadata map [ string ] interface { }
err := json . Unmarshal ( app . Metadata , & metadata )
if err != nil {
logger . Logger . WithError ( err ) . WithFields ( logrus . Fields {
"app_id" : app . ID ,
} ) . Error ( "Failed to deserialize app metadata" )
return err
}
createLightningAddressResponse , err := api . albyOAuthSvc . CreateLightningAddress ( ctx , createLightningAddressRequest . Address , createLightningAddressRequest . AppId )
if err != nil {
logger . Logger . WithError ( err ) . Error ( "Failed to create lightning address for app" )
return err
}
metadata [ "lud16" ] = createLightningAddressResponse . FullAddress
err = api . appsSvc . SetAppMetadata ( app . ID , metadata )
if err != nil {
logger . Logger . WithError ( err ) . Error ( "Failed to add lightning address to app metadata" )
return err
}
return nil
}
func ( api * api ) DeleteLightningAddress ( ctx context . Context , appId uint ) error {
app := api . appsSvc . GetAppById ( appId )
if app == nil {
return errors . New ( "app not found" )
}
var metadata map [ string ] interface { }
err := json . Unmarshal ( app . Metadata , & metadata )
if err != nil {
logger . Logger . WithError ( err ) . WithFields ( logrus . Fields {
"app_id" : app . ID ,
} ) . Error ( "Failed to deserialize app metadata" )
return err
}
if metadata [ "lud16" ] == nil {
return errors . New ( "no lightning address set" )
}
lud16 := metadata [ "lud16" ] . ( string )
if ! strings . Contains ( lud16 , "@" ) {
return errors . New ( "invalid lightning address" )
}
address := strings . Split ( lud16 , "@" ) [ 0 ]
// Call the Alby OAuth service to delete the lightning address
err = api . albyOAuthSvc . DeleteLightningAddress ( ctx , address )
if err != nil {
logger . Logger . WithError ( err ) . Error ( "Failed to delete lightning address for app" )
return err
}
delete ( metadata , "lud16" )
err = api . appsSvc . SetAppMetadata ( app . ID , metadata )
if err != nil {
logger . Logger . WithError ( err ) . Error ( "Failed to remove lightning address from app metadata" )
return err
}
return nil
}
2026-02-27 15:01:25 +05:30
func ( api * api ) GetApp ( dbApp * db . App ) ( * App , error ) {
2024-05-30 00:06:06 +07:00
paySpecificPermission := db . AppPermission { }
appPermissions := [ ] db . AppPermission { }
var expiresAt * time . Time
2026-02-27 15:01:25 +05:30
if err := api . db . Where ( "app_id = ?" , dbApp . ID ) . Find ( & appPermissions ) . Error ; err != nil {
logger . Logger . WithError ( err ) . WithFields ( logrus . Fields {
"app_id" : dbApp . ID ,
} ) . Error ( "Failed to list app permissions" )
return nil , err
}
2024-05-30 00:06:06 +07:00
requestMethods := [ ] string { }
for _ , appPerm := range appPermissions {
expiresAt = appPerm . ExpiresAt
2024-07-19 23:30:22 +07:00
if appPerm . Scope == constants . PAY_INVOICE_SCOPE {
2024-12-23 12:25:59 +03:00
// find the pay_invoice-specific permissions
2024-05-30 00:06:06 +07:00
paySpecificPermission = appPerm
}
2024-07-01 20:30:40 +07:00
requestMethods = append ( requestMethods , appPerm . Scope )
2024-05-30 00:06:06 +07:00
}
2024-12-23 12:25:59 +03:00
// renewsIn := ""
2026-04-17 13:53:28 +05:30
maxAmountSat := uint64 ( paySpecificPermission . MaxAmountSat )
budgetUsageMsat , err := queries . GetBudgetUsageMsat ( api . db , & paySpecificPermission )
2026-02-27 15:01:25 +05:30
if err != nil {
logger . Logger . WithError ( err ) . WithFields ( logrus . Fields {
"app_id" : dbApp . ID ,
} ) . Error ( "Failed to get budget usage for app" )
return nil , err
}
2024-05-30 00:06:06 +07:00
2024-09-02 17:40:34 +07:00
var metadata Metadata
if dbApp . Metadata != nil {
jsonErr := json . Unmarshal ( dbApp . Metadata , & metadata )
if jsonErr != nil {
logger . Logger . WithError ( jsonErr ) . WithFields ( logrus . Fields {
"app_id" : dbApp . ID ,
} ) . Error ( "Failed to deserialize app metadata" )
}
}
2025-03-04 19:08:08 +07:00
walletPubkey := api . keys . GetNostrPublicKey ( )
uniqueWalletPubkey := false
if dbApp . WalletPubkey != nil {
walletPubkey = * dbApp . WalletPubkey
uniqueWalletPubkey = true
}
2024-05-30 00:06:06 +07:00
response := App {
2026-04-09 16:26:45 +05:30
ID : dbApp . ID ,
Name : dbApp . Name ,
Description : dbApp . Description ,
CreatedAt : dbApp . CreatedAt ,
UpdatedAt : dbApp . UpdatedAt ,
AppPubkey : dbApp . AppPubkey ,
ExpiresAt : expiresAt ,
2026-04-17 13:53:28 +05:30
MaxAmount : maxAmountSat ,
MaxAmountSat : maxAmountSat ,
MaxAmountMsat : maxAmountSat * 1000 ,
2026-04-09 16:26:45 +05:30
Scopes : requestMethods ,
2026-04-17 13:53:28 +05:30
BudgetUsage : budgetUsageMsat / 1000 ,
BudgetUsageSat : budgetUsageMsat / 1000 ,
BudgetUsageMsat : budgetUsageMsat ,
2026-04-09 16:26:45 +05:30
BudgetRenewal : paySpecificPermission . BudgetRenewal ,
Isolated : dbApp . Isolated ,
Metadata : metadata ,
WalletPubkey : walletPubkey ,
UniqueWalletPubkey : uniqueWalletPubkey ,
LastUsedAt : dbApp . LastUsedAt ,
LastSettledTransactionAt : dbApp . LastSettledTransactionAt ,
2024-07-19 23:30:22 +07:00
}
if dbApp . Isolated {
2026-04-17 13:53:28 +05:30
balanceMsat , err := queries . GetIsolatedBalanceMsat ( api . db , dbApp . ID )
2026-02-27 15:01:25 +05:30
if err != nil {
logger . Logger . WithError ( err ) . WithFields ( logrus . Fields {
"app_id" : dbApp . ID ,
} ) . Error ( "Failed to get isolated app balance" )
return nil , err
}
2026-04-17 13:53:28 +05:30
response . Balance = balanceMsat
response . BalanceSat = balanceMsat / 1000
response . BalanceMsat = balanceMsat
2024-05-30 00:06:06 +07:00
}
2026-02-27 15:01:25 +05:30
return & response , nil
2024-05-30 00:06:06 +07:00
}
2025-07-29 20:57:18 +07:00
func ( api * api ) ListApps ( limit uint64 , offset uint64 , filters ListAppsFilters , orderBy string ) ( * ListAppsResponse , error ) {
2024-05-30 00:06:06 +07:00
// TODO: join dbApps and permissions
dbApps := [ ] db . App { }
2025-07-29 20:57:18 +07:00
query := api . db
if filters . Name != "" {
2025-08-27 22:51:18 +07:00
// searching for "Damus" will return "Damus" and "Damus (1)"
2025-10-14 11:57:21 +05:30
// Use case-insensitive search for both SQLite and PostgreSQL
if api . db . Dialector . Name ( ) == "postgres" {
query = query . Where ( "name ILIKE ?" , filters . Name + "%" )
} else {
query = query . Where ( "name LIKE ?" , filters . Name + "%" )
}
2025-07-29 20:57:18 +07:00
}
if filters . AppStoreAppId != "" {
2026-02-26 12:25:06 +05:30
query = query . Where ( datatypes . JSONQuery ( "metadata" ) . Equals ( filters . AppStoreAppId , constants . METADATA_APPSTORE_APP_ID_KEY ) )
2025-07-29 20:57:18 +07:00
}
if filters . Unused {
// find unused non-subwallet apps not used in the past 60 days
query = query . Where ( "last_used_at IS NULL OR last_used_at < ?" , time . Now ( ) . Add ( - 60 * 24 * time . Hour ) )
2025-08-19 17:31:54 +07:00
}
2026-02-26 12:25:06 +05:30
if filters . SubWallets != nil {
if * filters . SubWallets {
query = query . Where ( datatypes . JSONQuery ( "metadata" ) . Equals ( constants . SUBWALLET_APPSTORE_APP_ID , constants . METADATA_APPSTORE_APP_ID_KEY ) )
2025-07-29 20:57:18 +07:00
} else {
2026-02-26 12:25:06 +05:30
// exclude subwallets :scream:
if api . db . Dialector . Name ( ) == "sqlite" {
query = query . Where ( fmt . Sprintf ( "metadata is NULL OR JSON_EXTRACT(metadata, '$.%s') IS NULL OR JSON_EXTRACT(metadata, '$.%s') != ?" , constants . METADATA_APPSTORE_APP_ID_KEY , constants . METADATA_APPSTORE_APP_ID_KEY ) , constants . SUBWALLET_APPSTORE_APP_ID )
} else {
query = query . Where ( fmt . Sprintf ( "metadata IS NULL OR metadata->>'%s' IS NULL OR metadata->>'%s' != ?" , constants . METADATA_APPSTORE_APP_ID_KEY , constants . METADATA_APPSTORE_APP_ID_KEY ) , constants . SUBWALLET_APPSTORE_APP_ID )
}
2025-07-29 20:57:18 +07:00
}
}
2026-04-09 16:26:45 +05:30
query = query . Order ( resolveAppOrderBy ( orderBy ) )
2025-07-29 20:57:18 +07:00
if limit == 0 {
limit = 100
}
var totalCount int64
result := query . Model ( & db . App { } ) . Count ( & totalCount )
if result . Error != nil {
logger . Logger . WithError ( result . Error ) . Error ( "Failed to count DB apps" )
return nil , result . Error
}
2026-02-26 12:25:06 +05:30
var totalBalance * int64
2026-04-17 13:53:28 +05:30
var totalBalanceSat * int64
2026-02-26 12:25:06 +05:30
if filters . SubWallets != nil && * filters . SubWallets {
2026-04-17 13:53:28 +05:30
totalBalanceMsat , err := queries . GetTotalSubwalletBalanceMsat ( api . db )
2026-02-26 12:25:06 +05:30
if err != nil {
logger . Logger . WithError ( err ) . Error ( "Failed to calculate total subwallet balance" )
return nil , err
}
totalBalance = & totalBalanceMsat
2026-04-17 13:53:28 +05:30
totalBalanceSatVal := totalBalanceMsat / 1000
totalBalanceSat = & totalBalanceSatVal
2026-02-26 12:25:06 +05:30
}
2025-07-29 20:57:18 +07:00
query = query . Offset ( int ( offset ) ) . Limit ( int ( limit ) )
err := query . Find ( & dbApps ) . Error
2024-08-10 17:08:08 +07:00
if err != nil {
logger . Logger . WithError ( err ) . Error ( "Failed to list apps" )
return nil , err
}
2024-05-30 00:06:06 +07:00
2025-07-29 20:57:18 +07:00
appIds := [ ] uint64 { }
for _ , app := range dbApps {
appIds = append ( appIds , uint64 ( app . ID ) )
}
2024-07-01 20:30:40 +07:00
appPermissions := [ ] db . AppPermission { }
2025-07-29 20:57:18 +07:00
err = api . db . Where ( "app_id IN ?" , appIds ) . Find ( & appPermissions ) . Error
2024-08-10 17:08:08 +07:00
if err != nil {
logger . Logger . WithError ( err ) . Error ( "Failed to list app permissions" )
return nil , err
}
2024-05-30 00:06:06 +07:00
permissionsMap := make ( map [ uint ] [ ] db . AppPermission )
2024-07-01 20:30:40 +07:00
for _ , perm := range appPermissions {
2024-05-30 00:06:06 +07:00
permissionsMap [ perm . AppId ] = append ( permissionsMap [ perm . AppId ] , perm )
}
apiApps := [ ] App { }
2024-07-19 23:30:22 +07:00
for _ , dbApp := range dbApps {
2025-03-04 19:08:08 +07:00
walletPubkey := api . keys . GetNostrPublicKey ( )
uniqueWalletPubkey := false
if dbApp . WalletPubkey != nil {
walletPubkey = * dbApp . WalletPubkey
uniqueWalletPubkey = true
}
2024-05-30 00:06:06 +07:00
apiApp := App {
2026-04-09 16:26:45 +05:30
ID : dbApp . ID ,
Name : dbApp . Name ,
Description : dbApp . Description ,
CreatedAt : dbApp . CreatedAt ,
UpdatedAt : dbApp . UpdatedAt ,
AppPubkey : dbApp . AppPubkey ,
Isolated : dbApp . Isolated ,
WalletPubkey : walletPubkey ,
UniqueWalletPubkey : uniqueWalletPubkey ,
LastUsedAt : dbApp . LastUsedAt ,
LastSettledTransactionAt : dbApp . LastSettledTransactionAt ,
2024-07-19 23:30:22 +07:00
}
if dbApp . Isolated {
2026-04-17 13:53:28 +05:30
balanceMsat , err := queries . GetIsolatedBalanceMsat ( api . db , dbApp . ID )
2026-02-27 15:01:25 +05:30
if err != nil {
logger . Logger . WithError ( err ) . WithFields ( logrus . Fields {
"app_id" : dbApp . ID ,
} ) . Error ( "Failed to get isolated app balance" )
return nil , err
}
2026-04-17 13:53:28 +05:30
apiApp . Balance = balanceMsat
apiApp . BalanceSat = balanceMsat / 1000
apiApp . BalanceMsat = balanceMsat
2024-05-30 00:06:06 +07:00
}
2024-07-19 23:30:22 +07:00
for _ , appPermission := range permissionsMap [ dbApp . ID ] {
2024-07-01 20:30:40 +07:00
apiApp . Scopes = append ( apiApp . Scopes , appPermission . Scope )
apiApp . ExpiresAt = appPermission . ExpiresAt
2024-07-19 23:30:22 +07:00
if appPermission . Scope == constants . PAY_INVOICE_SCOPE {
2024-07-01 20:30:40 +07:00
apiApp . BudgetRenewal = appPermission . BudgetRenewal
2026-04-17 13:53:28 +05:30
apiApp . MaxAmount = uint64 ( appPermission . MaxAmountSat )
2024-07-19 23:30:22 +07:00
apiApp . MaxAmountSat = uint64 ( appPermission . MaxAmountSat )
2026-04-17 13:53:28 +05:30
apiApp . MaxAmountMsat = uint64 ( appPermission . MaxAmountSat ) * 1000
budgetUsageMsat , err := queries . GetBudgetUsageMsat ( api . db , & appPermission )
2026-02-27 15:01:25 +05:30
if err != nil {
logger . Logger . WithError ( err ) . WithFields ( logrus . Fields {
"app_id" : dbApp . ID ,
} ) . Error ( "Failed to get budget usage for app" )
return nil , err
}
2026-04-17 13:53:28 +05:30
apiApp . BudgetUsage = budgetUsageMsat / 1000
apiApp . BudgetUsageSat = budgetUsageMsat / 1000
apiApp . BudgetUsageMsat = budgetUsageMsat
2024-05-30 00:06:06 +07:00
}
}
2024-09-02 17:40:34 +07:00
var metadata Metadata
if dbApp . Metadata != nil {
jsonErr := json . Unmarshal ( dbApp . Metadata , & metadata )
if jsonErr != nil {
logger . Logger . WithError ( jsonErr ) . WithFields ( logrus . Fields {
"app_id" : dbApp . ID ,
} ) . Error ( "Failed to deserialize app metadata" )
}
apiApp . Metadata = metadata
}
2024-05-30 00:06:06 +07:00
apiApps = append ( apiApps , apiApp )
}
2025-07-29 20:57:18 +07:00
return & ListAppsResponse {
2026-04-17 13:53:28 +05:30
Apps : apiApps ,
TotalCount : uint64 ( totalCount ) ,
TotalBalance : totalBalance ,
TotalBalanceSat : totalBalanceSat ,
TotalBalanceMsat : totalBalance ,
2025-07-29 20:57:18 +07:00
} , nil
2024-05-30 00:06:06 +07:00
}
2026-04-09 16:26:45 +05:30
func resolveAppOrderBy ( orderBy string ) string {
switch orderBy {
case "created_at" :
return "created_at DESC"
case "last_settled_transaction" :
return "last_settled_transaction_at IS NULL, last_settled_transaction_at DESC"
default :
return "last_used_at IS NULL, last_used_at DESC"
}
}
2024-07-31 09:10:26 +02:00
func ( api * api ) ListChannels ( ctx context . Context ) ( [ ] Channel , error ) {
2026-02-27 14:15:09 +05:30
lnClient := api . svc . GetLNClient ( )
if lnClient == nil {
2026-04-07 17:38:47 +05:30
return nil , ErrLNClientNotStarted
2024-05-30 00:06:06 +07:00
}
2026-02-27 14:15:09 +05:30
channels , err := lnClient . ListChannels ( ctx )
2024-07-31 09:10:26 +02:00
if err != nil {
return nil , err
}
apiChannels := [ ] Channel { }
for _ , channel := range channels {
status := "offline"
if channel . Active {
status = "online"
} else if channel . Confirmations != nil && channel . ConfirmationsRequired != nil && * channel . ConfirmationsRequired > * channel . Confirmations {
status = "opening"
}
apiChannels = append ( apiChannels , Channel {
2026-05-01 15:41:56 +05:30
LocalBalance : channel . LocalBalanceMsat ,
LocalBalanceSat : channel . LocalBalanceMsat / 1000 ,
LocalBalanceMsat : channel . LocalBalanceMsat ,
LocalSpendableBalance : channel . LocalSpendableBalanceMsat ,
LocalSpendableBalanceSat : channel . LocalSpendableBalanceMsat / 1000 ,
LocalSpendableBalanceMsat : channel . LocalSpendableBalanceMsat ,
RemoteBalance : channel . RemoteBalanceMsat ,
RemoteBalanceSat : channel . RemoteBalanceMsat / 1000 ,
RemoteBalanceMsat : channel . RemoteBalanceMsat ,
2024-07-31 09:10:26 +02:00
Id : channel . Id ,
RemotePubkey : channel . RemotePubkey ,
FundingTxId : channel . FundingTxId ,
2025-01-06 23:33:13 +07:00
FundingTxVout : channel . FundingTxVout ,
2024-07-31 09:10:26 +02:00
Active : channel . Active ,
Public : channel . Public ,
InternalChannel : channel . InternalChannel ,
Confirmations : channel . Confirmations ,
ConfirmationsRequired : channel . ConfirmationsRequired ,
ForwardingFeeBaseMsat : channel . ForwardingFeeBaseMsat ,
2025-08-21 18:09:03 +07:00
ForwardingFeeProportionalMillionths : channel . ForwardingFeeProportionalMillionths ,
2026-05-01 15:41:56 +05:30
UnspendablePunishmentReserve : channel . UnspendablePunishmentReserveSat ,
UnspendablePunishmentReserveSat : channel . UnspendablePunishmentReserveSat ,
CounterpartyUnspendablePunishmentReserve : channel . CounterpartyUnspendablePunishmentReserveSat ,
CounterpartyUnspendablePunishmentReserveSat : channel . CounterpartyUnspendablePunishmentReserveSat ,
2026-04-17 13:53:28 +05:30
Error : channel . Error ,
IsOutbound : channel . IsOutbound ,
Status : status ,
2024-07-31 09:10:26 +02:00
} )
}
2025-03-06 15:27:32 +07:00
slices . SortFunc ( apiChannels , func ( a , b Channel ) int {
// sort by channel size first
aSize := a . LocalBalance + a . RemoteBalance
bSize := b . LocalBalance + b . RemoteBalance
if aSize != bSize {
return int ( bSize - aSize )
}
// then by local balance in the channel
if a . LocalBalance != b . LocalBalance {
return int ( b . LocalBalance - a . LocalBalance )
}
// finally sort by channel ID to prevent sort randomly changing
return strings . Compare ( b . Id , a . Id )
} )
2024-07-31 09:10:26 +02:00
return apiChannels , nil
2024-05-30 00:06:06 +07:00
}
func ( api * api ) GetChannelPeerSuggestions ( ctx context . Context ) ( [ ] alby . ChannelPeerSuggestion , error ) {
2025-08-27 23:34:15 +07:00
return api . albySvc . GetChannelPeerSuggestions ( ctx )
feat: stories (#2172)
* feat: integrate Stories widget with backend endpoint
Add stories endpoint plumbing for HTTP and Wails, wire the Home Stories card
to fetch from /api/alby/stories, and keep it first in the right column.
Made-with: Cursor
* feat(home): story modal CTAs and preview fallback
- Add contextual actions in the story dialog (update hub with version,
open Alby Go in-app, install extension) keyed by kind or title
- Use preview stories when the stories API request fails
- Pass hub version from useInfo into the update link
Made-with: Cursor
* feat(stories): polish modal, drop preview fallback
- Widen modal and put video edge-to-edge with overlay close button
- Drop verbose header and 'Watch on YouTube' button
- Remove previewStories fallback so widget hides until upstream API ships
- Tighten title line-height
* feat(stories): render cta from API instead of mapping by kind
Move CTA copy and URLs into the API response. Hub renders story.cta
directly, so adding new story types no longer requires a hub release.
* chore(csp): allow cdn.getalby-assets.com in img-src
* feat(stories): bump avatar size and add ring gap
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(stories): post-review cleanups
- Use react-router Link for in-tab CTA instead of plain <a>.
- Drop redundant www.youtube.com from frame-src (embeds always go through nocookie).
- Tighten stories endpoint status check from >= 300 to >= 400.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(stories): address CodeRabbit feedback
- Switch StoriesWidget to useSWR + swrFetcher (project convention).
- Guard story iframe with isYouTubeUrl so non-YouTube urls never embed.
- Wrap GetStories errors with fmt.Errorf("...: %w", err).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(stories): drop isYouTubeUrl guard
Stories are curated and always YouTube; the runtime check was
redundant. CSP frame-src still constrains the iframe source.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(stories): drop getYouTubeEmbedUrl, embed videoUrl as-is
The Alby API now sends canonical youtube-nocookie embed URLs with
autoplay/rel query strings (getAlby/getalby.com#2568), so the
runtime normalization is no longer needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(stories): use w-16 instead of arbitrary w-[73px]
Match the avatar's size token; no magic numbers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(stories): take videoId from API and assemble embed url locally
Pairs with getAlby/getalby.com#2568. The API now sends just the
YouTube videoId; the hub composes the canonical embed URL so the
domain/query-string format stays in one place.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(stories): treat 3xx as non-success, matching file convention
The other status checks in alby_oauth_service.go all use >= 300;
align GetStories so redirects don't slip through.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(stories): move viewed-storage key to constants, widen story button
Address review feedback:
- Centralize the localStorage key for viewed stories in localStorageKeys
alongside the other keys.
- Widen the story button from w-16 to w-20 so "Alby Extension" fits on
one line and matches the other titles.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(stories): bump story button to w-24 so titles fit one line
w-20 still wrapped "Alby Extension"; w-24 fits all current titles
without truncation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Revert "chore(stories): bump story button to w-24 so titles fit one line"
This reverts commit 0b47438f501aee4b2a91827bec4267ac06072747.
* chore(stories): split title words onto separate lines
Reserve two lines for every story title so avatars align regardless of
title length.
* chore(stories): align homeStoriesViewed key with sibling pattern
* chore(stories): fit titles on one line
* chore(stories): widen story button to w-21 for one-line titles
---------
Co-authored-by: René Aaron <rene@twentyuno.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 10:50:15 +02:00
}
func ( api * api ) GetStories ( ctx context . Context ) ( [ ] alby . Story , error ) {
return api . albyOAuthSvc . GetStories ( ctx )
2025-08-27 23:34:15 +07:00
}
func ( api * api ) GetLSPChannelOffer ( ctx context . Context ) ( * alby . LSPChannelOffer , error ) {
return api . albyOAuthSvc . GetLSPChannelOffer ( ctx )
2024-05-30 00:06:06 +07:00
}
func ( api * api ) ResetRouter ( key string ) error {
2026-02-27 14:15:09 +05:30
lnClient := api . svc . GetLNClient ( )
if lnClient == nil {
2026-04-07 17:38:47 +05:30
return ErrLNClientNotStarted
2024-05-30 00:06:06 +07:00
}
2026-02-27 14:15:09 +05:30
err := lnClient . ResetRouter ( key )
2024-05-30 00:06:06 +07:00
if err != nil {
return err
}
// Because the above method has to stop the node to reset the router,
// We also need to stop the lnclient and ask the user to start it again
return api . Stop ( )
}
func ( api * api ) ChangeUnlockPassword ( changeUnlockPasswordRequest * ChangeUnlockPasswordRequest ) error {
if api . svc . GetLNClient ( ) == nil {
2026-04-07 17:38:47 +05:30
return ErrLNClientNotStarted
2024-05-30 00:06:06 +07:00
}
2025-01-07 20:08:10 +07:00
autoUnlockPassword , err := api . cfg . Get ( "AutoUnlockPassword" , "" )
if err != nil {
return err
}
if autoUnlockPassword != "" {
2025-06-24 21:23:19 +05:30
return errors . New ( "please disable auto-unlock before using this feature" )
2025-01-07 20:08:10 +07:00
}
err = api . cfg . ChangeUnlockPassword ( changeUnlockPasswordRequest . CurrentUnlockPassword , changeUnlockPasswordRequest . NewUnlockPassword )
2024-05-30 00:06:06 +07:00
if err != nil {
2024-06-17 19:42:09 +07:00
logger . Logger . WithError ( err ) . Error ( "failed to change unlock password" )
2024-05-30 00:06:06 +07:00
return err
}
// Because all the encrypted fields have changed
// we also need to stop the lnclient and ask the user to start it again
return api . Stop ( )
}
2025-01-07 20:08:10 +07:00
func ( api * api ) SetAutoUnlockPassword ( unlockPassword string ) error {
if api . svc . GetLNClient ( ) == nil {
2026-04-07 17:38:47 +05:30
return ErrLNClientNotStarted
2025-01-07 20:08:10 +07:00
}
err := api . cfg . SetAutoUnlockPassword ( unlockPassword )
if err != nil {
logger . Logger . WithError ( err ) . Error ( "failed to set auto unlock password" )
return err
}
return nil
}
2024-05-30 00:06:06 +07:00
func ( api * api ) Stop ( ) error {
2024-09-04 14:01:16 +07:00
if ! startMutex . TryLock ( ) {
// do not allow to stop twice in case this is somehow called twice
return errors . New ( "app is busy" )
}
defer startMutex . Unlock ( )
2024-06-17 19:42:09 +07:00
logger . Logger . Info ( "Running Stop command" )
2024-05-30 00:06:06 +07:00
if api . svc . GetLNClient ( ) == nil {
2026-04-07 17:38:47 +05:30
return ErrLNClientNotStarted
2024-05-30 00:06:06 +07:00
}
2024-06-17 19:42:09 +07:00
2024-07-13 20:01:33 +07:00
// stop the lnclient, nostr relay etc.
2024-05-30 00:06:06 +07:00
// The user will be forced to re-enter their unlock password to restart the node
2024-07-13 20:01:33 +07:00
api . svc . StopApp ( )
2024-06-17 19:42:09 +07:00
2024-07-13 20:01:33 +07:00
return nil
2024-05-30 00:06:06 +07:00
}
2026-05-25 21:56:07 +07:00
func ( api * api ) GetNodeConnectionInfo ( ctx context . Context ) ( * NodeConnectionInfo , error ) {
2026-02-27 14:15:09 +05:30
lnClient := api . svc . GetLNClient ( )
if lnClient == nil {
2026-04-07 17:38:47 +05:30
return nil , ErrLNClientNotStarted
2024-05-30 00:06:06 +07:00
}
2026-05-25 21:56:07 +07:00
info , err := lnClient . GetNodeConnectionInfo ( ctx )
if err != nil {
return nil , err
}
return & NodeConnectionInfo {
Pubkey : info . Pubkey ,
Address : info . Address ,
Port : info . Port ,
} , nil
2024-05-30 00:06:06 +07:00
}
2025-07-10 18:36:12 +05:30
func ( api * api ) RefundSwap ( refundSwapRequest * RefundSwapRequest ) error {
2025-10-14 11:09:54 +05:30
if api . svc . GetSwapsService ( ) == nil {
return errors . New ( "SwapsService not started" )
}
2025-11-18 09:35:20 +05:30
return api . svc . GetSwapsService ( ) . RefundSwap ( refundSwapRequest . SwapId , refundSwapRequest . Address , false )
2025-07-10 18:36:12 +05:30
}
func ( api * api ) GetAutoSwapConfig ( ) ( * GetAutoSwapConfigResponse , error ) {
2026-03-25 16:58:16 +00:00
if api . svc . GetSwapsService ( ) == nil {
return nil , errors . New ( "SwapsService not started" )
}
2025-07-10 18:36:12 +05:30
swapOutBalanceThresholdStr , _ := api . cfg . Get ( config . AutoSwapBalanceThresholdKey , "" )
swapOutAmountStr , _ := api . cfg . Get ( config . AutoSwapAmountKey , "" )
swapOutDestination , _ := api . cfg . Get ( config . AutoSwapDestinationKey , "" )
2025-04-18 16:01:38 +05:30
2026-03-25 16:58:16 +00:00
if xpub := api . svc . GetSwapsService ( ) . GetDecryptedAutoSwapXpub ( ) ; xpub != "" {
swapOutDestination = xpub
}
2025-07-10 18:36:12 +05:30
swapOutEnabled := swapOutBalanceThresholdStr != "" && swapOutAmountStr != ""
2026-04-17 13:53:28 +05:30
var swapOutBalanceThresholdSat , swapOutAmountSat uint64
2025-07-10 18:36:12 +05:30
if swapOutEnabled {
2025-04-18 16:01:38 +05:30
var err error
2026-04-17 13:53:28 +05:30
if swapOutBalanceThresholdSat , err = strconv . ParseUint ( swapOutBalanceThresholdStr , 10 , 64 ) ; err != nil {
2025-07-10 18:36:12 +05:30
return nil , fmt . Errorf ( "invalid autoswap out balance threshold: %w" , err )
2025-04-18 16:01:38 +05:30
}
2026-04-17 13:53:28 +05:30
if swapOutAmountSat , err = strconv . ParseUint ( swapOutAmountStr , 10 , 64 ) ; err != nil {
2025-07-10 18:36:12 +05:30
return nil , fmt . Errorf ( "invalid autoswap out amount: %w" , err )
2025-04-18 16:01:38 +05:30
}
}
2025-07-10 18:36:12 +05:30
return & GetAutoSwapConfigResponse {
2026-04-17 13:53:28 +05:30
Type : constants . SWAP_TYPE_OUT ,
Enabled : swapOutEnabled ,
BalanceThreshold : swapOutBalanceThresholdSat ,
BalanceThresholdSat : swapOutBalanceThresholdSat ,
SwapAmount : swapOutAmountSat ,
SwapAmountSat : swapOutAmountSat ,
Destination : swapOutDestination ,
2025-07-10 18:36:12 +05:30
} , nil
}
func ( api * api ) LookupSwap ( swapId string ) ( * LookupSwapResponse , error ) {
2025-10-14 11:09:54 +05:30
if api . svc . GetSwapsService ( ) == nil {
return nil , errors . New ( "SwapsService not started" )
}
2025-07-10 18:36:12 +05:30
dbSwap , err := api . svc . GetSwapsService ( ) . GetSwap ( swapId )
if err != nil {
logger . Logger . WithError ( err ) . Error ( "failed to fetch swap info" )
return nil , err
}
return toApiSwap ( dbSwap ) , nil
}
func ( api * api ) ListSwaps ( ) ( * ListSwapsResponse , error ) {
2025-10-14 11:09:54 +05:30
if api . svc . GetSwapsService ( ) == nil {
return nil , errors . New ( "SwapsService not started" )
}
2025-07-10 18:36:12 +05:30
swaps , err := api . svc . GetSwapsService ( ) . ListSwaps ( )
if err != nil {
return nil , err
}
apiSwaps := [ ] Swap { }
for _ , swap := range swaps {
apiSwaps = append ( apiSwaps , * toApiSwap ( & swap ) )
}
return & ListSwapsResponse {
Swaps : apiSwaps ,
} , nil
}
func toApiSwap ( swap * swaps . Swap ) * Swap {
return & Swap {
Id : swap . SwapId ,
Type : swap . Type ,
State : swap . State ,
Invoice : swap . Invoice ,
2026-05-01 15:41:56 +05:30
SendAmount : swap . SendAmountSat ,
SendAmountSat : swap . SendAmountSat ,
ReceiveAmount : swap . ReceiveAmountSat ,
ReceiveAmountSat : swap . ReceiveAmountSat ,
2025-07-10 18:36:12 +05:30
PaymentHash : swap . PaymentHash ,
DestinationAddress : swap . DestinationAddress ,
RefundAddress : swap . RefundAddress ,
LockupAddress : swap . LockupAddress ,
LockupTxId : swap . LockupTxId ,
ClaimTxId : swap . ClaimTxId ,
AutoSwap : swap . AutoSwap ,
BoltzPubkey : swap . BoltzPubkey ,
CreatedAt : swap . CreatedAt . Format ( time . RFC3339 ) ,
UpdatedAt : swap . UpdatedAt . Format ( time . RFC3339 ) ,
2025-08-29 16:47:31 +07:00
UsedXpub : swap . UsedXpub ,
2025-07-10 18:36:12 +05:30
}
}
2025-08-15 16:58:00 +05:30
func ( api * api ) GetSwapInInfo ( ) ( * SwapInfoResponse , error ) {
2025-10-14 11:09:54 +05:30
if api . svc . GetSwapsService ( ) == nil {
return nil , errors . New ( "SwapsService not started" )
}
2025-08-15 16:58:00 +05:30
swapInInfo , err := api . svc . GetSwapsService ( ) . GetSwapInInfo ( )
2025-07-10 18:36:12 +05:30
if err != nil {
logger . Logger . WithError ( err ) . Error ( "failed to calculate fee info" )
return nil , err
}
2025-08-15 16:58:00 +05:30
return & SwapInfoResponse {
2026-04-17 13:53:28 +05:30
AlbyServiceFee : swapInInfo . AlbyServiceFee ,
BoltzServiceFee : swapInInfo . BoltzServiceFee ,
2026-05-01 15:41:56 +05:30
BoltzNetworkFee : swapInInfo . BoltzNetworkFeeSat ,
BoltzNetworkFeeSat : swapInInfo . BoltzNetworkFeeSat ,
MinAmount : swapInInfo . MinAmountSat ,
MinAmountSat : swapInInfo . MinAmountSat ,
MaxAmount : swapInInfo . MaxAmountSat ,
MaxAmountSat : swapInInfo . MaxAmountSat ,
2025-07-10 18:36:12 +05:30
} , nil
}
2025-08-15 16:58:00 +05:30
func ( api * api ) GetSwapOutInfo ( ) ( * SwapInfoResponse , error ) {
2025-10-14 11:09:54 +05:30
if api . svc . GetSwapsService ( ) == nil {
return nil , errors . New ( "SwapsService not started" )
}
2025-08-15 16:58:00 +05:30
swapOutInfo , err := api . svc . GetSwapsService ( ) . GetSwapOutInfo ( )
2025-04-29 13:32:17 +05:30
if err != nil {
logger . Logger . WithError ( err ) . Error ( "failed to calculate fee info" )
return nil , err
}
2025-08-15 16:58:00 +05:30
return & SwapInfoResponse {
2026-04-17 13:53:28 +05:30
AlbyServiceFee : swapOutInfo . AlbyServiceFee ,
BoltzServiceFee : swapOutInfo . BoltzServiceFee ,
2026-05-01 15:41:56 +05:30
BoltzNetworkFee : swapOutInfo . BoltzNetworkFeeSat ,
BoltzNetworkFeeSat : swapOutInfo . BoltzNetworkFeeSat ,
MinAmount : swapOutInfo . MinAmountSat ,
MinAmountSat : swapOutInfo . MinAmountSat ,
MaxAmount : swapOutInfo . MaxAmountSat ,
MaxAmountSat : swapOutInfo . MaxAmountSat ,
2025-04-18 16:01:38 +05:30
} , nil
}
2025-07-10 18:36:12 +05:30
func ( api * api ) InitiateSwapOut ( ctx context . Context , initiateSwapOutRequest * InitiateSwapRequest ) ( * swaps . SwapResponse , error ) {
lnClient := api . svc . GetLNClient ( )
if lnClient == nil {
2026-04-07 17:38:47 +05:30
return nil , ErrLNClientNotStarted
2025-07-10 18:36:12 +05:30
}
2025-10-14 11:09:54 +05:30
if api . svc . GetSwapsService ( ) == nil {
return nil , errors . New ( "SwapsService not started" )
}
2026-05-01 15:41:56 +05:30
amountSat := uint64 ( 0 )
resolvedAmountSat := ResolveToSat ( initiateSwapOutRequest . SwapAmountSat , nil , initiateSwapOutRequest . SwapAmount , nil )
if resolvedAmountSat != nil {
amountSat = * resolvedAmountSat
}
2025-07-10 18:36:12 +05:30
destination := initiateSwapOutRequest . Destination
2026-05-01 15:41:56 +05:30
if amountSat == 0 {
2025-07-10 18:36:12 +05:30
return nil , errors . New ( "invalid swap amount" )
}
2026-05-01 15:41:56 +05:30
swapOutResponse , err := api . svc . GetSwapsService ( ) . SwapOut ( amountSat , destination , false , false )
2025-07-10 18:36:12 +05:30
if err != nil {
logger . Logger . WithFields ( logrus . Fields {
2026-05-01 15:41:56 +05:30
"amount_sat" : amountSat ,
2025-07-10 18:36:12 +05:30
"destination" : destination ,
} ) . WithError ( err ) . Error ( "Failed to initiate swap out" )
return nil , err
}
return swapOutResponse , nil
}
func ( api * api ) InitiateSwapIn ( ctx context . Context , initiateSwapInRequest * InitiateSwapRequest ) ( * swaps . SwapResponse , error ) {
lnClient := api . svc . GetLNClient ( )
if lnClient == nil {
2026-04-07 17:38:47 +05:30
return nil , ErrLNClientNotStarted
2025-07-10 18:36:12 +05:30
}
2025-10-14 11:09:54 +05:30
if api . svc . GetSwapsService ( ) == nil {
return nil , errors . New ( "SwapsService not started" )
}
2026-05-01 15:41:56 +05:30
amountSat := uint64 ( 0 )
resolvedAmountSat := ResolveToSat ( initiateSwapInRequest . SwapAmountSat , nil , initiateSwapInRequest . SwapAmount , nil )
if resolvedAmountSat != nil {
amountSat = * resolvedAmountSat
}
2025-07-10 18:36:12 +05:30
2026-05-01 15:41:56 +05:30
if amountSat == 0 {
2025-07-10 18:36:12 +05:30
return nil , errors . New ( "invalid swap amount" )
}
2026-05-01 15:41:56 +05:30
swapInResponse , err := api . svc . GetSwapsService ( ) . SwapIn ( amountSat , false )
2025-07-10 18:36:12 +05:30
if err != nil {
logger . Logger . WithFields ( logrus . Fields {
2026-05-01 15:41:56 +05:30
"amount_sat" : amountSat ,
2025-07-10 18:36:12 +05:30
} ) . WithError ( err ) . Error ( "Failed to initiate swap in" )
return nil , err
}
return swapInResponse , nil
}
func ( api * api ) EnableAutoSwapOut ( ctx context . Context , enableAutoSwapsRequest * EnableAutoSwapRequest ) error {
2026-03-25 16:58:16 +00:00
if api . svc . GetSwapsService ( ) == nil {
return errors . New ( "SwapsService not started" )
}
encryptionKey := ""
if enableAutoSwapsRequest . Destination != "" {
switch enableAutoSwapsRequest . DestinationType {
case "address" :
if err := api . svc . GetSwapsService ( ) . ValidateAddress ( enableAutoSwapsRequest . Destination ) ; err != nil {
return err
}
case "xpub" :
if ! api . cfg . CheckUnlockPassword ( enableAutoSwapsRequest . UnlockPassword ) {
return errors . New ( "invalid unlock password" )
}
if err := api . svc . GetSwapsService ( ) . ValidateXpub ( enableAutoSwapsRequest . Destination ) ; err != nil {
return err
}
encryptionKey = enableAutoSwapsRequest . UnlockPassword
default :
return errors . New ( "destination type must be address or xpub" )
}
}
2026-05-01 15:41:56 +05:30
balanceThresholdSat := uint64 ( 0 )
resolvedBalanceThresholdSat := ResolveToSat ( enableAutoSwapsRequest . BalanceThresholdSat , nil , enableAutoSwapsRequest . BalanceThreshold , nil )
if resolvedBalanceThresholdSat != nil {
balanceThresholdSat = * resolvedBalanceThresholdSat
}
err := api . cfg . SetUpdate ( config . AutoSwapBalanceThresholdKey , strconv . FormatUint ( balanceThresholdSat , 10 ) , "" )
2025-04-16 20:58:32 +05:30
if err != nil {
logger . Logger . WithError ( err ) . Error ( "Failed to save autoswap balance threshold to config" )
2025-04-18 12:03:45 +05:30
return err
2025-04-16 20:58:32 +05:30
}
2026-05-01 15:41:56 +05:30
swapAmountSat := uint64 ( 0 )
resolvedSwapAmountSat := ResolveToSat ( enableAutoSwapsRequest . SwapAmountSat , nil , enableAutoSwapsRequest . SwapAmount , nil )
if resolvedSwapAmountSat != nil {
swapAmountSat = * resolvedSwapAmountSat
}
err = api . cfg . SetUpdate ( config . AutoSwapAmountKey , strconv . FormatUint ( swapAmountSat , 10 ) , "" )
2025-04-18 14:41:11 +05:30
if err != nil {
2025-04-28 14:20:10 +05:30
logger . Logger . WithError ( err ) . Error ( "Failed to save autoswap amount to config" )
2025-04-18 14:41:11 +05:30
return err
}
2026-03-25 16:58:16 +00:00
err = api . cfg . SetUpdate ( config . AutoSwapDestinationKey , enableAutoSwapsRequest . Destination , encryptionKey )
2025-04-16 20:58:32 +05:30
if err != nil {
logger . Logger . WithError ( err ) . Error ( "Failed to save autoswap destination to config" )
2025-04-18 12:03:45 +05:30
return err
2025-04-16 20:58:32 +05:30
}
2026-03-25 16:58:16 +00:00
return api . svc . GetSwapsService ( ) . EnableAutoSwapOut ( enableAutoSwapsRequest . UnlockPassword )
2025-04-16 20:58:32 +05:30
}
2025-07-10 18:36:12 +05:30
func ( api * api ) DisableAutoSwap ( ) error {
keys := [ ] string { config . AutoSwapBalanceThresholdKey , config . AutoSwapAmountKey , config . AutoSwapDestinationKey }
2025-04-18 16:01:38 +05:30
2025-07-10 18:36:12 +05:30
for _ , key := range keys {
if err := api . cfg . SetUpdate ( key , "" , "" ) ; err != nil {
logger . Logger . WithError ( err ) . Errorf ( "Failed to remove autoswap config for key: %s" , key )
return err
}
}
2025-04-18 16:01:38 +05:30
2025-10-14 11:09:54 +05:30
if api . svc . GetSwapsService ( ) != nil {
api . svc . GetSwapsService ( ) . StopAutoSwapOut ( )
}
2025-04-18 16:01:38 +05:30
return nil
}
2025-07-10 18:36:12 +05:30
func ( api * api ) GetSwapMnemonic ( ) string {
return api . keys . GetSwapMnemonic ( )
}
2026-05-25 21:56:07 +07:00
func ( api * api ) GetNodeStatus ( ctx context . Context ) ( * NodeStatus , error ) {
2026-02-27 14:15:09 +05:30
lnClient := api . svc . GetLNClient ( )
if lnClient == nil {
2026-04-07 17:38:47 +05:30
return nil , ErrLNClientNotStarted
2024-05-30 00:06:06 +07:00
}
2026-05-25 21:56:07 +07:00
nodeStatus , err := lnClient . GetNodeStatus ( ctx )
if err != nil {
return nil , err
}
if nodeStatus == nil {
return nil , nil
}
return toApiNodeStatus ( nodeStatus ) , nil
}
func toApiNodeStatus ( nodeStatus * lnclient . NodeStatus ) * NodeStatus {
return & NodeStatus {
IsReady : nodeStatus . IsReady ,
InternalNodeStatus : nodeStatus . InternalNodeStatus ,
}
2024-05-30 00:06:06 +07:00
}
2026-05-25 21:56:07 +07:00
func ( api * api ) ListPeers ( ctx context . Context ) ( [ ] PeerDetails , error ) {
2026-02-27 14:15:09 +05:30
lnClient := api . svc . GetLNClient ( )
if lnClient == nil {
2026-04-07 17:38:47 +05:30
return nil , ErrLNClientNotStarted
2024-05-30 00:06:06 +07:00
}
2026-05-25 21:56:07 +07:00
peers , err := lnClient . ListPeers ( ctx )
if err != nil {
return nil , err
}
apiPeers := make ( [ ] PeerDetails , 0 , len ( peers ) )
for _ , peer := range peers {
apiPeers = append ( apiPeers , PeerDetails {
NodeId : peer . NodeId ,
Address : peer . Address ,
IsPersisted : peer . IsPersisted ,
IsConnected : peer . IsConnected ,
} )
}
return apiPeers , nil
2024-05-30 00:06:06 +07:00
}
func ( api * api ) ConnectPeer ( ctx context . Context , connectPeerRequest * ConnectPeerRequest ) error {
2026-02-27 14:15:09 +05:30
lnClient := api . svc . GetLNClient ( )
if lnClient == nil {
2026-04-07 17:38:47 +05:30
return ErrLNClientNotStarted
2024-05-30 00:06:06 +07:00
}
2026-05-25 21:56:07 +07:00
return lnClient . ConnectPeer ( ctx , & lnclient . ConnectPeerRequest {
Pubkey : connectPeerRequest . Pubkey ,
Address : connectPeerRequest . Address ,
Port : connectPeerRequest . Port ,
} )
2024-05-30 00:06:06 +07:00
}
func ( api * api ) OpenChannel ( ctx context . Context , openChannelRequest * OpenChannelRequest ) ( * OpenChannelResponse , error ) {
2026-02-27 14:15:09 +05:30
lnClient := api . svc . GetLNClient ( )
if lnClient == nil {
2026-04-07 17:38:47 +05:30
return nil , ErrLNClientNotStarted
2024-05-30 00:06:06 +07:00
}
2026-05-25 21:56:07 +07:00
resp , err := lnClient . OpenChannel ( ctx , & lnclient . OpenChannelRequest {
Pubkey : openChannelRequest . Pubkey ,
AmountSats : openChannelRequest . AmountSats ,
Public : openChannelRequest . Public ,
} )
if err != nil {
return nil , err
}
return & OpenChannelResponse {
FundingTxId : resp . FundingTxId ,
} , nil
2024-05-30 00:06:06 +07:00
}
2024-05-30 23:04:32 +07:00
func ( api * api ) DisconnectPeer ( ctx context . Context , peerId string ) error {
2026-02-27 14:15:09 +05:30
lnClient := api . svc . GetLNClient ( )
if lnClient == nil {
2026-04-07 17:38:47 +05:30
return ErrLNClientNotStarted
2024-05-30 23:04:32 +07:00
}
2024-06-17 19:42:09 +07:00
logger . Logger . WithFields ( logrus . Fields {
2024-05-30 23:04:32 +07:00
"peer_id" : peerId ,
} ) . Info ( "Disconnecting peer" )
2026-02-27 14:15:09 +05:30
return lnClient . DisconnectPeer ( ctx , peerId )
2024-05-30 23:04:32 +07:00
}
2024-05-30 00:06:06 +07:00
func ( api * api ) CloseChannel ( ctx context . Context , peerId , channelId string , force bool ) ( * CloseChannelResponse , error ) {
2026-02-27 14:15:09 +05:30
lnClient := api . svc . GetLNClient ( )
if lnClient == nil {
2026-04-07 17:38:47 +05:30
return nil , ErrLNClientNotStarted
2024-05-30 00:06:06 +07:00
}
2024-06-17 19:42:09 +07:00
logger . Logger . WithFields ( logrus . Fields {
2024-05-30 00:06:06 +07:00
"peer_id" : peerId ,
"channel_id" : channelId ,
"force" : force ,
} ) . Info ( "Closing channel" )
2026-05-25 21:56:07 +07:00
err := lnClient . CloseChannel ( ctx , & lnclient . CloseChannelRequest {
2024-05-30 00:06:06 +07:00
NodeId : peerId ,
ChannelId : channelId ,
Force : force ,
} )
2026-05-25 21:56:07 +07:00
if err != nil {
return nil , err
}
return & CloseChannelResponse { } , nil
2024-05-30 00:06:06 +07:00
}
2024-06-18 15:22:19 +07:00
func ( api * api ) UpdateChannel ( ctx context . Context , updateChannelRequest * UpdateChannelRequest ) error {
2026-02-27 14:15:09 +05:30
lnClient := api . svc . GetLNClient ( )
if lnClient == nil {
2026-04-07 17:38:47 +05:30
return ErrLNClientNotStarted
2024-06-18 15:22:19 +07:00
}
logger . Logger . WithFields ( logrus . Fields {
"request" : updateChannelRequest ,
} ) . Info ( "updating channel" )
2026-05-25 21:56:07 +07:00
return lnClient . UpdateChannel ( ctx , & lnclient . UpdateChannelRequest {
ChannelId : updateChannelRequest . ChannelId ,
NodeId : updateChannelRequest . NodeId ,
ForwardingFeeBaseMsat : updateChannelRequest . ForwardingFeeBaseMsat ,
ForwardingFeeProportionalMillionths : updateChannelRequest . ForwardingFeeProportionalMillionths ,
MaxDustHtlcExposureFromFeeRateMultiplier : updateChannelRequest . MaxDustHtlcExposureFromFeeRateMultiplier ,
} )
2024-06-18 15:22:19 +07:00
}
2025-06-04 20:58:19 +05:30
func ( api * api ) MakeOffer ( ctx context . Context , description string ) ( string , error ) {
2026-02-27 14:15:09 +05:30
lnClient := api . svc . GetLNClient ( )
if lnClient == nil {
2026-04-07 17:38:47 +05:30
return "" , ErrLNClientNotStarted
2025-06-04 20:58:19 +05:30
}
2026-02-27 14:15:09 +05:30
offer , err := lnClient . MakeOffer ( ctx , description )
2025-06-04 20:58:19 +05:30
if err != nil {
return "" , err
}
return offer , nil
}
2024-06-11 06:49:13 +03:00
func ( api * api ) GetNewOnchainAddress ( ctx context . Context ) ( string , error ) {
2026-02-27 14:15:09 +05:30
lnClient := api . svc . GetLNClient ( )
if lnClient == nil {
2026-04-07 17:38:47 +05:30
return "" , ErrLNClientNotStarted
2024-05-30 00:06:06 +07:00
}
2026-02-27 14:15:09 +05:30
address , err := lnClient . GetNewOnchainAddress ( ctx )
2024-05-30 00:06:06 +07:00
if err != nil {
2024-06-11 06:49:13 +03:00
return "" , err
2024-05-30 00:06:06 +07:00
}
2024-06-11 06:49:13 +03:00
2024-09-25 17:31:05 +07:00
err = api . cfg . SetUpdate ( config . OnchainAddressKey , address , "" )
if err != nil {
logger . Logger . WithError ( err ) . Error ( "Failed to save new onchain address to config" )
}
2024-06-11 06:49:13 +03:00
return address , nil
}
func ( api * api ) GetUnusedOnchainAddress ( ctx context . Context ) ( string , error ) {
if api . svc . GetLNClient ( ) == nil {
2026-04-07 17:38:47 +05:30
return "" , ErrLNClientNotStarted
2024-06-11 06:49:13 +03:00
}
2024-06-17 19:42:09 +07:00
currentAddress , err := api . cfg . Get ( config . OnchainAddressKey , "" )
2024-06-11 06:49:13 +03:00
if err != nil {
2024-06-17 19:42:09 +07:00
logger . Logger . WithError ( err ) . Error ( "Failed to get current address from config" )
2024-06-11 06:49:13 +03:00
return "" , err
}
if currentAddress != "" {
// check if address has any transactions
2025-09-02 18:22:30 +05:30
response , err := api . RequestEsploraApi ( ctx , "/address/" + currentAddress + "/txs" )
2024-06-11 06:49:13 +03:00
if err != nil {
2024-06-17 19:42:09 +07:00
logger . Logger . WithError ( err ) . Error ( "Failed to get current address transactions" )
2024-06-11 06:49:13 +03:00
return currentAddress , nil
}
transactions , ok := response . ( [ ] interface { } )
if ! ok {
2024-06-17 19:42:09 +07:00
logger . Logger . WithField ( "response" , response ) . Error ( "Failed to cast esplora address txs response" , response )
2024-06-11 06:49:13 +03:00
return currentAddress , nil
}
if len ( transactions ) == 0 {
// address has not been used yet
return currentAddress , nil
}
}
newAddress , err := api . GetNewOnchainAddress ( ctx )
if err != nil {
2024-06-17 19:42:09 +07:00
logger . Logger . WithError ( err ) . Error ( "Failed to retrieve new onchain address" )
2024-06-11 06:49:13 +03:00
return "" , err
}
return newAddress , nil
2024-05-30 00:06:06 +07:00
}
func ( api * api ) SignMessage ( ctx context . Context , message string ) ( * SignMessageResponse , error ) {
2026-02-27 14:15:09 +05:30
lnClient := api . svc . GetLNClient ( )
if lnClient == nil {
2026-04-07 17:38:47 +05:30
return nil , ErrLNClientNotStarted
2024-05-30 00:06:06 +07:00
}
2026-02-27 14:15:09 +05:30
signature , err := lnClient . SignMessage ( ctx , message )
2024-05-30 00:06:06 +07:00
if err != nil {
return nil , err
}
return & SignMessageResponse {
Message : message ,
Signature : signature ,
} , nil
}
2026-04-17 13:53:28 +05:30
func ( api * api ) RedeemOnchainFunds ( ctx context . Context , toAddress string , amountSat uint64 , feeRate * uint64 , sendAll bool ) ( * RedeemOnchainFundsResponse , error ) {
2026-02-27 14:15:09 +05:30
lnClient := api . svc . GetLNClient ( )
if lnClient == nil {
2026-04-07 17:38:47 +05:30
return nil , ErrLNClientNotStarted
2024-05-30 00:06:06 +07:00
}
2026-04-17 13:53:28 +05:30
txId , err := lnClient . RedeemOnchainFunds ( ctx , toAddress , amountSat , feeRate , sendAll )
2024-05-30 00:06:06 +07:00
if err != nil {
return nil , err
}
return & RedeemOnchainFundsResponse {
TxId : txId ,
} , nil
}
func ( api * api ) GetBalances ( ctx context . Context ) ( * BalancesResponse , error ) {
2026-02-27 14:15:09 +05:30
lnClient := api . svc . GetLNClient ( )
if lnClient == nil {
2026-04-07 17:38:47 +05:30
return nil , ErrLNClientNotStarted
2024-05-30 00:06:06 +07:00
}
2026-02-27 14:15:09 +05:30
balances , err := lnClient . GetBalances ( ctx , false )
2024-05-30 00:06:06 +07:00
if err != nil {
return nil , err
}
2026-05-25 21:56:07 +07:00
return toApiBalances ( balances ) , nil
}
func toApiBalances ( balances * lnclient . BalancesResponse ) * BalancesResponse {
totalSpendableMsat := balances . Lightning . TotalSpendableMsat
totalReceivableMsat := balances . Lightning . TotalReceivableMsat
nextMaxSpendableMsat := balances . Lightning . NextMaxSpendableMsat
nextMaxReceivableMsat := balances . Lightning . NextMaxReceivableMsat
nextMaxSpendableMPPMsat := balances . Lightning . NextMaxSpendableMPPMsat
nextMaxReceivableMPPMsat := balances . Lightning . NextMaxReceivableMPPMsat
return & BalancesResponse {
Onchain : OnchainBalanceResponse {
Spendable : balances . Onchain . SpendableSat ,
SpendableSat : balances . Onchain . SpendableSat ,
Total : balances . Onchain . TotalSat ,
TotalSat : balances . Onchain . TotalSat ,
Reserved : balances . Onchain . ReservedSat ,
ReservedSat : balances . Onchain . ReservedSat ,
PendingBalancesFromChannelClosures : balances . Onchain . PendingBalancesFromChannelClosuresSat ,
PendingBalancesFromChannelClosuresSat : balances . Onchain . PendingBalancesFromChannelClosuresSat ,
PendingBalancesDetails : toApiPendingBalanceDetails ( balances . Onchain . PendingBalancesDetails ) ,
PendingSweepBalancesDetails : toApiPendingBalanceDetails ( balances . Onchain . PendingSweepBalancesDetails ) ,
InternalBalances : balances . Onchain . InternalBalances ,
} ,
Lightning : LightningBalanceResponse {
TotalSpendable : totalSpendableMsat ,
TotalSpendableSat : totalSpendableMsat / 1000 ,
TotalSpendableMsat : totalSpendableMsat ,
TotalReceivable : totalReceivableMsat ,
TotalReceivableSat : totalReceivableMsat / 1000 ,
TotalReceivableMsat : totalReceivableMsat ,
NextMaxSpendable : nextMaxSpendableMsat ,
NextMaxSpendableSat : nextMaxSpendableMsat / 1000 ,
NextMaxSpendableMsat : nextMaxSpendableMsat ,
NextMaxReceivable : nextMaxReceivableMsat ,
NextMaxReceivableSat : nextMaxReceivableMsat / 1000 ,
NextMaxReceivableMsat : nextMaxReceivableMsat ,
NextMaxSpendableMPP : nextMaxSpendableMPPMsat ,
NextMaxSpendableMPPSat : nextMaxSpendableMPPMsat / 1000 ,
NextMaxSpendableMPPMsat : nextMaxSpendableMPPMsat ,
NextMaxReceivableMPP : nextMaxReceivableMPPMsat ,
NextMaxReceivableMPPSat : nextMaxReceivableMPPMsat / 1000 ,
NextMaxReceivableMPPMsat : nextMaxReceivableMPPMsat ,
} ,
}
}
func toApiPendingBalanceDetails ( details [ ] lnclient . PendingBalanceDetails ) [ ] PendingBalanceDetails {
if details == nil {
return nil
}
apiDetails := make ( [ ] PendingBalanceDetails , 0 , len ( details ) )
for _ , d := range details {
apiDetails = append ( apiDetails , PendingBalanceDetails {
ChannelId : d . ChannelId ,
NodeId : d . NodeId ,
Amount : d . AmountSat ,
AmountSat : d . AmountSat ,
FundingTxId : d . FundingTxId ,
FundingTxVout : d . FundingTxVout ,
} )
}
return apiDetails
2024-05-30 00:06:06 +07:00
}
2024-06-11 06:49:13 +03:00
// TODO: remove dependency on this endpoint
2025-09-02 18:22:30 +05:30
func ( api * api ) RequestMempoolApi ( ctx context . Context , endpoint string ) ( interface { } , error ) {
2024-06-17 19:42:09 +07:00
url := api . cfg . GetEnv ( ) . MempoolApi + endpoint
2024-05-30 00:06:06 +07:00
client := http . Client {
Timeout : time . Second * 10 ,
}
2025-09-02 18:22:30 +05:30
req , err := http . NewRequestWithContext ( ctx , http . MethodGet , url , nil )
2024-05-30 00:06:06 +07:00
if err != nil {
2024-06-17 19:42:09 +07:00
logger . Logger . WithError ( err ) . WithFields ( logrus . Fields {
2024-05-30 00:06:06 +07:00
"url" : url ,
} ) . Error ( "Failed to create http request" )
return nil , err
}
res , err := client . Do ( req )
if err != nil {
2024-06-17 19:42:09 +07:00
logger . Logger . WithError ( err ) . WithFields ( logrus . Fields {
2024-05-30 00:06:06 +07:00
"url" : url ,
} ) . Error ( "Failed to send request" )
return nil , err
}
defer res . Body . Close ( )
body , readErr := io . ReadAll ( res . Body )
if readErr != nil {
2024-06-17 19:42:09 +07:00
logger . Logger . WithError ( err ) . WithFields ( logrus . Fields {
2024-05-30 00:06:06 +07:00
"url" : url ,
} ) . Error ( "Failed to read response body" )
return nil , errors . New ( "failed to read response body" )
}
2026-03-31 20:51:26 +03:00
if res . StatusCode != http . StatusOK {
logger . Logger . WithFields ( logrus . Fields {
"endpoint" : endpoint ,
"status_code" : res . StatusCode ,
"body" : string ( body ) ,
} ) . Error ( "Mempool endpoint returned non-success code" )
return nil , fmt . Errorf ( "mempool endpoint returned non-success code: %s" , string ( body ) )
}
2024-05-30 00:06:06 +07:00
var jsonContent interface { }
jsonErr := json . Unmarshal ( body , & jsonContent )
if jsonErr != nil {
2024-06-17 19:42:09 +07:00
logger . Logger . WithError ( jsonErr ) . WithFields ( logrus . Fields {
2024-05-30 00:06:06 +07:00
"url" : url ,
} ) . Error ( "Failed to deserialize json" )
return nil , fmt . Errorf ( "failed to deserialize json %s %s" , url , string ( body ) )
}
return jsonContent , nil
}
func ( api * api ) GetInfo ( ctx context . Context ) ( * InfoResponse , error ) {
info := InfoResponse { }
2024-06-17 19:42:09 +07:00
backendType , _ := api . cfg . Get ( "LNBackendType" , "" )
2024-11-29 15:16:20 -06:00
ldkVssEnabled , _ := api . cfg . Get ( "LdkVssEnabled" , "" )
2026-06-10 09:47:07 +02:00
jitChannelsEnabled , _ := api . cfg . Get ( "JitChannelsEnabled" , "" )
2025-01-07 20:08:10 +07:00
autoUnlockPassword , _ := api . cfg . Get ( "AutoUnlockPassword" , "" )
2026-02-09 17:39:25 +03:00
setupCompleted , err := api . cfg . SetupCompleted ( )
if err != nil {
logger . Logger . WithError ( err ) . Error ( "Failed to check if setup is completed" )
return nil , err
}
info . SetupCompleted = setupCompleted
2025-01-31 17:16:50 +05:30
info . Currency = api . cfg . GetCurrency ( )
2025-11-10 09:48:27 +01:00
info . BitcoinDisplayFormat = api . cfg . GetBitcoinDisplayFormat ( )
2025-02-14 16:03:50 +07:00
info . StartupState = api . svc . GetStartupState ( )
2024-09-04 10:46:12 +05:30
if api . startupError != nil {
info . StartupError = api . startupError . Error ( )
info . StartupErrorTime = api . startupErrorTime
}
2026-02-27 14:15:09 +05:30
lnClient := api . svc . GetLNClient ( )
info . Running = lnClient != nil
2024-05-30 00:06:06 +07:00
info . BackendType = backendType
2024-06-17 19:42:09 +07:00
info . AlbyAuthUrl = api . albyOAuthSvc . GetAuthUrl ( )
info . OAuthRedirect = ! api . cfg . GetEnv ( ) . IsDefaultClientId ( )
2024-06-22 12:22:01 +07:00
info . Version = version . Tag
2024-08-08 16:36:46 +07:00
info . EnableAdvancedSetup = api . cfg . GetEnv ( ) . EnableAdvancedSetup
2026-02-10 09:00:08 -08:00
info . HideUpdateBanner = api . cfg . GetEnv ( ) . HideUpdateBanner
2024-11-29 15:16:20 -06:00
info . LdkVssEnabled = ldkVssEnabled == "true"
2026-06-10 09:47:07 +02:00
info . JitChannelsEnabled = jitChannelsEnabled != "false"
2024-12-17 13:53:58 +07:00
info . VssSupported = backendType == config . LDKBackendType && api . cfg . GetEnv ( ) . LDKVssUrl != ""
2026-05-23 15:10:02 +02:00
info . SupportsBolt12 = backendType == config . LDKBackendType || backendType == config . CLNBackendType
2025-01-07 20:08:10 +07:00
info . AutoUnlockPasswordEnabled = autoUnlockPassword != ""
info . AutoUnlockPasswordSupported = api . cfg . GetEnv ( ) . IsDefaultClientId ( )
2025-11-06 18:01:09 +07:00
info . Relays = [ ] InfoResponseRelay { }
for _ , relayStatus := range api . svc . GetRelayStatuses ( ) {
info . Relays = append ( info . Relays , InfoResponseRelay {
Url : relayStatus . Url ,
Online : relayStatus . Online ,
} )
}
2025-06-24 21:23:19 +05:30
info . MempoolUrl = api . cfg . GetMempoolUrl ( )
2025-11-06 18:01:09 +07:00
info . AlbyAccountConnected = api . albyOAuthSvc . IsConnected ( ctx )
albyUserIdentifier , err := api . albyOAuthSvc . GetUserIdentifier ( )
2024-05-30 00:06:06 +07:00
if err != nil {
2024-06-17 19:42:09 +07:00
logger . Logger . WithError ( err ) . Error ( "Failed to get alby user identifier" )
2024-05-30 00:06:06 +07:00
return nil , err
}
info . AlbyUserIdentifier = albyUserIdentifier
2025-11-06 18:01:09 +07:00
2026-02-27 14:15:09 +05:30
if lnClient != nil {
nodeInfo , err := lnClient . GetInfo ( ctx )
2024-05-30 00:06:06 +07:00
if err != nil {
2024-06-17 19:42:09 +07:00
logger . Logger . WithError ( err ) . Error ( "Failed to get nodeInfo" )
2024-05-30 00:06:06 +07:00
return nil , err
}
info . Network = nodeInfo . Network
2026-04-06 22:55:13 +01:00
if backendType == config . LDKBackendType {
// Only LDK supports this right now. Using a local interface here
// so we don't have to bloat the main LNClient interface for everyone else.
type chainSourceProvider interface {
GetChainDataSource ( ) ( string , string )
}
2026-06-10 09:47:07 +02:00
type lsps2SourceProvider interface {
GetLiquiditySourceLsps2 ( ) string
}
type lsps2MinPaymentSizeProvider interface {
GetLiquiditySourceLsps2MinPaymentSizeMsat ( ) * uint64
}
type lsps2MaxPaymentSizeProvider interface {
GetLiquiditySourceLsps2MaxPaymentSizeMsat ( ) * uint64
}
2026-04-06 22:55:13 +01:00
if ldkService , ok := api . svc . GetLNClient ( ) . ( chainSourceProvider ) ; ok {
info . ChainDataSourceType , info . ChainDataSourceAddress = ldkService . GetChainDataSource ( )
}
2026-06-10 09:47:07 +02:00
if ldkService , ok := api . svc . GetLNClient ( ) . ( lsps2SourceProvider ) ; ok {
info . JitChannelsLiquiditySource = ldkService . GetLiquiditySourceLsps2 ( )
}
if ldkService , ok := api . svc . GetLNClient ( ) . ( lsps2MinPaymentSizeProvider ) ; ok {
info . JitChannelsMinPaymentSizeMsat = ldkService . GetLiquiditySourceLsps2MinPaymentSizeMsat ( )
}
if ldkService , ok := api . svc . GetLNClient ( ) . ( lsps2MaxPaymentSizeProvider ) ; ok {
info . JitChannelsMaxPaymentSizeMsat = ldkService . GetLiquiditySourceLsps2MaxPaymentSizeMsat ( )
}
2026-04-06 22:55:13 +01:00
}
2024-05-30 00:06:06 +07:00
}
2024-06-17 19:42:09 +07:00
info . NextBackupReminder , _ = api . cfg . Get ( "NextBackupReminder" , "" )
2024-05-30 00:06:06 +07:00
2025-06-18 13:33:14 +07:00
info . NodeAlias , _ = api . cfg . Get ( "NodeAlias" , "" )
2024-05-30 00:06:06 +07:00
return & info , nil
}
2026-06-10 09:47:07 +02:00
func ( api * api ) setCurrency ( currency string ) error {
2025-01-31 17:16:50 +05:30
if currency == "" {
return fmt . Errorf ( "currency value cannot be empty" )
}
err := api . cfg . SetCurrency ( currency )
if err != nil {
logger . Logger . WithError ( err ) . Error ( "Failed to update currency" )
return err
}
return nil
}
2026-06-10 09:47:07 +02:00
func ( api * api ) setBitcoinDisplayFormat ( format string ) error {
2025-11-10 09:48:27 +01:00
if format != constants . BITCOIN_DISPLAY_FORMAT_SATS && format != constants . BITCOIN_DISPLAY_FORMAT_BIP177 {
return fmt . Errorf ( "bitcoin display format must be '%s' or '%s'" , constants . BITCOIN_DISPLAY_FORMAT_SATS , constants . BITCOIN_DISPLAY_FORMAT_BIP177 )
}
err := api . cfg . SetBitcoinDisplayFormat ( format )
if err != nil {
logger . Logger . WithError ( err ) . Error ( "Failed to update bitcoin display format" )
return err
}
return nil
}
2026-06-10 09:47:07 +02:00
func ( api * api ) setJitChannelsEnabled ( enabled bool ) error {
value := "true"
if ! enabled {
value = "false"
}
err := api . cfg . SetUpdate ( "JitChannelsEnabled" , value , "" )
if err != nil {
logger . Logger . WithError ( err ) . Error ( "Failed to update JIT channels setting" )
return err
}
return nil
}
2025-11-10 09:48:27 +01:00
func ( api * api ) UpdateSettings ( updateSettingsRequest * UpdateSettingsRequest ) error {
if updateSettingsRequest . Currency != "" {
2026-06-10 09:47:07 +02:00
err := api . setCurrency ( updateSettingsRequest . Currency )
2025-11-10 09:48:27 +01:00
if err != nil {
return fmt . Errorf ( "failed to set currency: %w" , err )
}
}
if updateSettingsRequest . BitcoinDisplayFormat != "" {
2026-06-10 09:47:07 +02:00
err := api . setBitcoinDisplayFormat ( updateSettingsRequest . BitcoinDisplayFormat )
2025-11-10 09:48:27 +01:00
if err != nil {
return fmt . Errorf ( "failed to set bitcoin display format: %w" , err )
}
}
2026-06-10 09:47:07 +02:00
if updateSettingsRequest . JitChannelsEnabled != nil {
err := api . setJitChannelsEnabled ( * updateSettingsRequest . JitChannelsEnabled )
if err != nil {
return fmt . Errorf ( "failed to set JIT channels setting: %w" , err )
}
}
2025-11-10 09:48:27 +01:00
return nil
}
2025-06-18 13:33:14 +07:00
func ( api * api ) SetNodeAlias ( nodeAlias string ) error {
err := api . cfg . SetUpdate ( "NodeAlias" , nodeAlias , "" )
if err != nil {
logger . Logger . WithError ( err ) . Error ( "Failed to save node alias to config" )
return err
}
return nil
}
2024-08-08 11:08:14 +02:00
func ( api * api ) GetMnemonic ( unlockPassword string ) ( * MnemonicResponse , error ) {
if ! api . cfg . CheckUnlockPassword ( unlockPassword ) {
return nil , fmt . Errorf ( "wrong password" )
}
mnemonic , err := api . cfg . Get ( "Mnemonic" , unlockPassword )
if err != nil {
return nil , fmt . Errorf ( "failed to fetch encryption key: %w" , err )
}
resp := MnemonicResponse {
Mnemonic : mnemonic ,
}
2025-02-24 16:12:38 +01:00
return & resp , nil
2024-05-30 00:06:06 +07:00
}
func ( api * api ) SetNextBackupReminder ( backupReminderRequest * BackupReminderRequest ) error {
2024-09-25 17:31:05 +07:00
err := api . cfg . SetUpdate ( "NextBackupReminder" , backupReminderRequest . NextBackupReminder , "" )
if err != nil {
logger . Logger . WithError ( err ) . Error ( "Failed to save next backup reminder to config" )
}
2024-05-30 00:06:06 +07:00
return nil
}
2024-06-19 23:25:39 +07:00
var startMutex sync . Mutex
2024-09-04 10:46:12 +05:30
func ( api * api ) Start ( startRequest * StartRequest ) {
api . startupError = nil
2025-12-17 00:25:33 +07:00
err := api . startInternal ( startRequest )
2024-09-04 10:46:12 +05:30
if err != nil {
logger . Logger . WithError ( err ) . Error ( "Failed to start node" )
api . startupError = err
api . startupErrorTime = time . Now ( )
}
}
2025-12-17 00:25:33 +07:00
func ( api * api ) startInternal ( startRequest * StartRequest ) ( err error ) {
2024-06-19 23:25:39 +07:00
if ! startMutex . TryLock ( ) {
// do not allow to start twice in case this is somehow called twice
2024-09-04 14:01:16 +07:00
return errors . New ( "app is busy" )
2024-06-19 23:25:39 +07:00
}
defer startMutex . Unlock ( )
2024-05-30 00:06:06 +07:00
return api . svc . StartApp ( startRequest . UnlockPassword )
}
func ( api * api ) Setup ( ctx context . Context , setupRequest * SetupRequest ) error {
2024-08-24 20:25:05 +07:00
if ! startMutex . TryLock ( ) {
// do not allow to start twice in case this is somehow called twice
2024-09-04 14:01:16 +07:00
return errors . New ( "app is busy" )
2024-08-24 20:25:05 +07:00
}
defer startMutex . Unlock ( )
2024-05-30 00:06:06 +07:00
info , err := api . GetInfo ( ctx )
if err != nil {
2024-06-17 19:42:09 +07:00
logger . Logger . WithError ( err ) . Error ( "Failed to get info" )
2024-05-30 00:06:06 +07:00
return err
}
if info . SetupCompleted {
2024-06-17 19:42:09 +07:00
logger . Logger . Error ( "Cannot re-setup node" )
2024-05-30 00:06:06 +07:00
return errors . New ( "setup already completed" )
}
2024-08-24 20:25:05 +07:00
if setupRequest . UnlockPassword == "" {
return errors . New ( "no unlock password provided" )
}
2026-06-04 11:57:29 +07:00
// Bark and Cashu both store wallet state on local disk and have no
// remote-backup mechanism, so they cannot run in environments without
// persistent volumes (e.g. Alby Cloud). The default OAuth client ID
// identifies a local / self-hosted deployment.
if ! api . cfg . GetEnv ( ) . IsDefaultClientId ( ) {
switch setupRequest . LNBackendType {
case config . BarkBackendType , config . CashuBackendType :
return fmt . Errorf ( "%s backend is not supported in this environment (no persistent storage)" , setupRequest . LNBackendType )
}
}
2024-09-25 17:31:05 +07:00
err = api . cfg . SaveUnlockPasswordCheck ( setupRequest . UnlockPassword )
if err != nil {
return err
}
2024-05-30 00:06:06 +07:00
// update next backup reminder
2024-09-25 17:31:05 +07:00
err = api . cfg . SetUpdate ( "NextBackupReminder" , setupRequest . NextBackupReminder , "" )
if err != nil {
logger . Logger . WithError ( err ) . Error ( "Failed to save next backup reminder" )
}
2024-05-30 00:06:06 +07:00
// only update non-empty values
if setupRequest . LNBackendType != "" {
2024-09-25 17:31:05 +07:00
err = api . cfg . SetUpdate ( "LNBackendType" , setupRequest . LNBackendType , "" )
if err != nil {
logger . Logger . WithError ( err ) . Error ( "Failed to save backend type" )
return err
}
2024-05-30 00:06:06 +07:00
}
if setupRequest . Mnemonic != "" {
2024-09-25 17:31:05 +07:00
err = api . cfg . SetUpdate ( "Mnemonic" , setupRequest . Mnemonic , setupRequest . UnlockPassword )
if err != nil {
logger . Logger . WithError ( err ) . Error ( "Failed to save encrypted mnemonic" )
return err
}
2024-05-30 00:06:06 +07:00
}
if setupRequest . LNDAddress != "" {
2024-09-25 17:31:05 +07:00
err = api . cfg . SetUpdate ( "LNDAddress" , setupRequest . LNDAddress , setupRequest . UnlockPassword )
if err != nil {
logger . Logger . WithError ( err ) . Error ( "Failed to save lnd address" )
return err
}
2024-05-30 00:06:06 +07:00
}
2026-04-21 22:33:25 +05:30
if setupRequest . LNDCertFile != "" {
certBytes , err := os . ReadFile ( setupRequest . LNDCertFile )
if err != nil {
logger . Logger . WithError ( err ) . Error ( "Failed to read lnd cert file" )
return err
}
certHex := hex . EncodeToString ( certBytes )
err = api . cfg . SetUpdate ( "LNDCertHex" , certHex , setupRequest . UnlockPassword )
2024-09-25 17:31:05 +07:00
if err != nil {
logger . Logger . WithError ( err ) . Error ( "Failed to save lnd cert hex" )
return err
}
2024-05-30 00:06:06 +07:00
}
2026-04-21 22:33:25 +05:30
if setupRequest . LNDMacaroonFile != "" {
macaroonBytes , err := os . ReadFile ( setupRequest . LNDMacaroonFile )
if err != nil {
logger . Logger . WithError ( err ) . Error ( "Failed to read lnd macaroon file" )
return err
}
macaroonHex := hex . EncodeToString ( macaroonBytes )
err = api . cfg . SetUpdate ( "LNDMacaroonHex" , macaroonHex , setupRequest . UnlockPassword )
2024-09-25 17:31:05 +07:00
if err != nil {
logger . Logger . WithError ( err ) . Error ( "Failed to save lnd macaroon hex" )
return err
}
2024-05-30 00:06:06 +07:00
}
2024-06-04 10:47:51 +03:00
if setupRequest . PhoenixdAddress != "" {
2024-09-25 17:31:05 +07:00
err = api . cfg . SetUpdate ( "PhoenixdAddress" , setupRequest . PhoenixdAddress , setupRequest . UnlockPassword )
if err != nil {
logger . Logger . WithError ( err ) . Error ( "Failed to save phoenix address" )
return err
}
2024-06-04 10:47:51 +03:00
}
if setupRequest . PhoenixdAuthorization != "" {
2024-09-25 17:31:05 +07:00
err = api . cfg . SetUpdate ( "PhoenixdAuthorization" , setupRequest . PhoenixdAuthorization , setupRequest . UnlockPassword )
if err != nil {
logger . Logger . WithError ( err ) . Error ( "Failed to save phoenix auth" )
return err
}
2024-06-04 10:47:51 +03:00
}
2024-06-14 19:12:22 +03:00
if setupRequest . CashuMintUrl != "" {
2024-09-25 17:31:05 +07:00
err = api . cfg . SetUpdate ( "CashuMintUrl" , setupRequest . CashuMintUrl , setupRequest . UnlockPassword )
if err != nil {
logger . Logger . WithError ( err ) . Error ( "Failed to save cashu mint url" )
return err
}
2024-06-14 19:12:22 +03:00
}
2026-05-03 21:36:13 +02:00
if setupRequest . CLNAddress != "" {
err = api . cfg . SetUpdate ( "CLNAddress" , setupRequest . CLNAddress , setupRequest . UnlockPassword )
if err != nil {
logger . Logger . WithError ( err ) . Error ( "Failed to save CLN address" )
return err
}
}
if setupRequest . CLNLightningDir != "" {
err = api . cfg . SetUpdate ( "CLNLightningDir" , setupRequest . CLNLightningDir , setupRequest . UnlockPassword )
if err != nil {
logger . Logger . WithError ( err ) . Error ( "Failed to save CLN Lightning directory path" )
return err
}
}
if setupRequest . CLNAddressHold != "" {
err = api . cfg . SetUpdate ( "CLNAddressHold" , setupRequest . CLNAddressHold , setupRequest . UnlockPassword )
if err != nil {
logger . Logger . WithError ( err ) . Error ( "Failed to save cln hold plugin address" )
return err
}
}
2024-05-30 00:06:06 +07:00
return nil
}
2024-07-01 20:30:40 +07:00
func ( api * api ) GetWalletCapabilities ( ctx context . Context ) ( * WalletCapabilitiesResponse , error ) {
2026-02-27 14:15:09 +05:30
lnClient := api . svc . GetLNClient ( )
if lnClient == nil {
2026-04-07 17:38:47 +05:30
return nil , ErrLNClientNotStarted
2024-07-01 20:30:40 +07:00
}
2026-02-27 14:15:09 +05:30
methods := lnClient . GetSupportedNIP47Methods ( )
notificationTypes := lnClient . GetSupportedNIP47NotificationTypes ( )
2024-07-01 20:30:40 +07:00
scopes , err := permissions . RequestMethodsToScopes ( methods )
if err != nil {
return nil , err
}
if len ( notificationTypes ) > 0 {
2024-07-19 23:30:22 +07:00
scopes = append ( scopes , constants . NOTIFICATIONS_SCOPE )
2024-07-01 20:30:40 +07:00
}
return & WalletCapabilitiesResponse {
Methods : methods ,
NotificationTypes : notificationTypes ,
Scopes : scopes ,
} , nil
}
2024-12-16 12:32:35 +07:00
func ( api * api ) MigrateNodeStorage ( ctx context . Context , to string ) error {
if api . svc . GetLNClient ( ) == nil {
2026-04-07 17:38:47 +05:30
return ErrLNClientNotStarted
2024-12-16 12:32:35 +07:00
}
if to != "VSS" {
2025-06-24 21:23:19 +05:30
return fmt . Errorf ( "migration type not supported: %s" , to )
2024-12-16 12:32:35 +07:00
}
ldkVssEnabled , err := api . cfg . Get ( "LdkVssEnabled" , "" )
if err != nil {
return err
}
if ldkVssEnabled == "true" {
return errors . New ( "VSS already enabled" )
}
2024-12-17 13:53:58 +07:00
if api . cfg . GetEnv ( ) . LDKVssUrl == "" {
2025-06-24 21:23:19 +05:30
return errors . New ( "no VSS URL set" )
2024-12-17 13:53:58 +07:00
}
2024-12-16 12:32:35 +07:00
api . cfg . SetUpdate ( "LdkVssEnabled" , "true" , "" )
api . cfg . SetUpdate ( "LdkMigrateStorage" , "VSS" , "" )
return api . Stop ( )
}
2024-08-22 18:29:51 +05:30
func ( api * api ) GetNetworkGraph ( ctx context . Context , nodeIds [ ] string ) ( NetworkGraphResponse , error ) {
2026-02-27 14:15:09 +05:30
lnClient := api . svc . GetLNClient ( )
if lnClient == nil {
2026-04-07 17:38:47 +05:30
return nil , ErrLNClientNotStarted
2024-05-30 00:06:06 +07:00
}
2026-02-27 14:15:09 +05:30
return lnClient . GetNetworkGraph ( ctx , nodeIds )
2024-05-30 00:06:06 +07:00
}
func ( api * api ) SyncWallet ( ) error {
2026-02-27 14:15:09 +05:30
lnClient := api . svc . GetLNClient ( )
if lnClient == nil {
2026-04-07 17:38:47 +05:30
return ErrLNClientNotStarted
2024-05-30 00:06:06 +07:00
}
2026-02-27 14:15:09 +05:30
lnClient . UpdateLastWalletSyncRequest ( )
2024-05-30 00:06:06 +07:00
return nil
}
2026-05-25 21:56:07 +07:00
func ( api * api ) ListOnchainTransactions ( ctx context . Context ) ( [ ] OnchainTransaction , error ) {
2026-02-27 14:15:09 +05:30
lnClient := api . svc . GetLNClient ( )
if lnClient == nil {
2026-04-07 17:38:47 +05:30
return nil , ErrLNClientNotStarted
2025-05-02 13:51:37 +07:00
}
2026-05-25 21:56:07 +07:00
transactions , err := lnClient . ListOnchainTransactions ( ctx )
if err != nil {
return nil , err
}
apiTransactions := make ( [ ] OnchainTransaction , 0 , len ( transactions ) )
for _ , t := range transactions {
apiTransactions = append ( apiTransactions , OnchainTransaction {
AmountSat : t . AmountSat ,
CreatedAt : t . CreatedAt ,
State : t . State ,
Type : t . Type ,
NumConfirmations : t . NumConfirmations ,
TxId : t . TxId ,
} )
}
return apiTransactions , nil
2025-05-02 13:51:37 +07:00
}
2024-05-30 00:06:06 +07:00
func ( api * api ) GetLogOutput ( ctx context . Context , logType string , getLogRequest * GetLogOutputRequest ) ( * GetLogOutputResponse , error ) {
var err error
var logData [ ] byte
if logType == LogTypeNode {
2026-02-27 14:15:09 +05:30
lnClient := api . svc . GetLNClient ( )
if lnClient == nil {
2026-04-07 17:38:47 +05:30
return nil , ErrLNClientNotStarted
2024-05-30 00:06:06 +07:00
}
2026-02-27 14:15:09 +05:30
logData , err = lnClient . GetLogOutput ( ctx , getLogRequest . MaxLen )
2024-05-30 00:06:06 +07:00
if err != nil {
return nil , err
}
} else if logType == LogTypeApp {
2024-06-17 19:42:09 +07:00
logFileName := logger . GetLogFilePath ( )
2024-12-23 12:25:59 +03:00
if logFileName == "" {
logData = [ ] byte ( "file log is disabled" )
} else {
logData , err = utils . ReadFileTail ( logFileName , getLogRequest . MaxLen )
if err != nil {
return nil , err
}
2024-05-30 00:06:06 +07:00
}
} else {
return nil , fmt . Errorf ( "invalid log type: '%s'" , logType )
}
return & GetLogOutputResponse { Log : string ( logData ) } , nil
}
2025-01-17 11:29:20 +03:00
func ( api * api ) Health ( ctx context . Context ) ( * HealthResponse , error ) {
var alarms [ ] HealthAlarm
2025-08-27 23:34:15 +07:00
albyInfo , err := api . albySvc . GetInfo ( ctx )
2025-01-17 11:29:20 +03:00
if err != nil {
return nil , err
}
if ! albyInfo . Healthy {
alarms = append ( alarms , NewHealthAlarm ( HealthAlarmKindAlbyService , albyInfo . Incidents ) )
}
2025-11-18 11:03:51 +07:00
relayStatuses := api . svc . GetRelayStatuses ( )
if len ( relayStatuses ) > 0 {
isAnyNostrRelayOffline := false
offlineRelayUrls := [ ] string { }
for _ , relayStatus := range relayStatuses {
if ! relayStatus . Online {
isAnyNostrRelayOffline = true
offlineRelayUrls = append ( offlineRelayUrls , relayStatus . Url )
}
}
if isAnyNostrRelayOffline {
alarms = append ( alarms , NewHealthAlarm ( HealthAlarmKindNostrRelayOffline , offlineRelayUrls ) )
2025-11-06 18:01:09 +07:00
}
2025-01-17 11:29:20 +03:00
}
2025-03-31 21:55:33 +07:00
ldkVssEnabled , _ := api . cfg . Get ( "LdkVssEnabled" , "" )
if ldkVssEnabled == "true" {
albyMe , err := api . albyOAuthSvc . GetMe ( ctx )
if err != nil {
return nil , err
}
if albyMe . Subscription . PlanCode == "" {
alarms = append ( alarms , NewHealthAlarm ( HealthAlarmKindVssNoSubscription , nil ) )
}
}
2025-01-17 11:29:20 +03:00
lnClient := api . svc . GetLNClient ( )
if lnClient != nil {
2025-03-24 16:52:19 +07:00
nodeStatus , _ := lnClient . GetNodeStatus ( ctx )
2025-01-17 11:29:20 +03:00
if nodeStatus == nil || ! nodeStatus . IsReady {
2026-05-25 21:56:07 +07:00
var apiNodeStatus * NodeStatus
if nodeStatus != nil {
apiNodeStatus = toApiNodeStatus ( nodeStatus )
}
alarms = append ( alarms , NewHealthAlarm ( HealthAlarmKindNodeNotReady , apiNodeStatus ) )
2025-01-17 11:29:20 +03:00
}
channels , err := lnClient . ListChannels ( ctx )
if err != nil {
return nil , err
}
offlineChannels := slices . DeleteFunc ( channels , func ( channel lnclient . Channel ) bool {
2025-02-12 20:03:45 +05:30
if channel . Active {
return true
}
if channel . Confirmations == nil || channel . ConfirmationsRequired == nil {
return false
}
return * channel . Confirmations < * channel . ConfirmationsRequired
2025-01-17 11:29:20 +03:00
} )
if len ( offlineChannels ) > 0 {
alarms = append ( alarms , NewHealthAlarm ( HealthAlarmKindChannelsOffline , nil ) )
}
}
return & HealthResponse { Alarms : alarms } , nil
}
2025-01-30 14:20:17 +03:00
func ( api * api ) GetCustomNodeCommands ( ) ( * CustomNodeCommandsResponse , error ) {
lnClient := api . svc . GetLNClient ( )
if lnClient == nil {
2026-04-07 17:38:47 +05:30
return nil , ErrLNClientNotStarted
2025-01-30 14:20:17 +03:00
}
allCommandDefs := lnClient . GetCustomNodeCommandDefinitions ( )
commandDefs := make ( [ ] CustomNodeCommandDef , 0 , len ( allCommandDefs ) )
for _ , commandDef := range allCommandDefs {
argDefs := make ( [ ] CustomNodeCommandArgDef , 0 , len ( commandDef . Args ) )
for _ , argDef := range commandDef . Args {
argDefs = append ( argDefs , CustomNodeCommandArgDef {
Name : argDef . Name ,
Description : argDef . Description ,
} )
}
commandDefs = append ( commandDefs , CustomNodeCommandDef {
Name : commandDef . Name ,
Description : commandDef . Description ,
Args : argDefs ,
} )
}
return & CustomNodeCommandsResponse { Commands : commandDefs } , nil
}
func ( api * api ) ExecuteCustomNodeCommand ( ctx context . Context , command string ) ( interface { } , error ) {
lnClient := api . svc . GetLNClient ( )
if lnClient == nil {
2026-04-07 17:38:47 +05:30
return nil , ErrLNClientNotStarted
2025-01-30 14:20:17 +03:00
}
// Split command line into arguments. Command name must be the first argument.
parsedArgs , err := utils . ParseCommandLine ( command )
if err != nil {
return nil , fmt . Errorf ( "failed to parse node command: %w" , err )
} else if len ( parsedArgs ) == 0 {
return nil , errors . New ( "no command provided" )
}
// Look up the requested command definition.
allCommandDefs := lnClient . GetCustomNodeCommandDefinitions ( )
commandDefIdx := slices . IndexFunc ( allCommandDefs , func ( def lnclient . CustomNodeCommandDef ) bool {
return def . Name == parsedArgs [ 0 ]
} )
if commandDefIdx < 0 {
return nil , fmt . Errorf ( "unknown command: %q" , parsedArgs [ 0 ] )
}
// Build flag set.
commandDef := allCommandDefs [ commandDefIdx ]
flagSet := flag . NewFlagSet ( commandDef . Name , flag . ContinueOnError )
for _ , argDef := range commandDef . Args {
flagSet . String ( argDef . Name , "" , argDef . Description )
}
if err = flagSet . Parse ( parsedArgs [ 1 : ] ) ; err != nil {
return nil , fmt . Errorf ( "failed to parse command arguments: %w" , err )
}
// Collect flags that have been set.
argValues := make ( map [ string ] string )
flagSet . Visit ( func ( f * flag . Flag ) {
argValues [ f . Name ] = f . Value . String ( )
} )
reqArgs := make ( [ ] lnclient . CustomNodeCommandArg , 0 , len ( argValues ) )
for _ , argDef := range commandDef . Args {
if argValue , ok := argValues [ argDef . Name ] ; ok {
reqArgs = append ( reqArgs , lnclient . CustomNodeCommandArg {
Name : argDef . Name ,
Value : argValue ,
} )
}
}
nodeResp , err := lnClient . ExecuteCustomNodeCommand ( ctx , & lnclient . CustomNodeCommandRequest {
Name : commandDef . Name ,
Args : reqArgs ,
} )
if err != nil {
return nil , fmt . Errorf ( "node failed to execute custom command: %w" , err )
}
return nodeResp . Response , nil
}
2025-08-21 18:39:39 +07:00
func ( api * api ) SendEvent ( event string , properties interface { } ) {
2025-06-04 15:11:33 +02:00
api . svc . GetEventPublisher ( ) . Publish ( & events . Event {
2025-08-21 18:39:39 +07:00
Event : event ,
Properties : properties ,
2025-06-04 15:11:33 +02:00
} )
}
2024-05-30 00:06:06 +07:00
func ( api * api ) parseExpiresAt ( expiresAtString string ) ( * time . Time , error ) {
var expiresAt * time . Time
if expiresAtString != "" {
var err error
expiresAtValue , err := time . Parse ( time . RFC3339 , expiresAtString )
if err != nil {
2024-06-17 19:42:09 +07:00
logger . Logger . WithField ( "expiresAt" , expiresAtString ) . Error ( "Invalid expiresAt" )
2024-05-30 00:06:06 +07:00
return nil , fmt . Errorf ( "invalid expiresAt: %v" , err )
}
expiresAt = & expiresAtValue
}
return expiresAt , nil
}
2025-08-21 18:09:03 +07:00
func ( api * api ) GetForwards ( ) ( * GetForwardsResponse , error ) {
var forwards [ ] db . Forward
err := api . db . Find ( & forwards ) . Error
if err != nil {
return nil , err
}
2026-04-17 13:53:28 +05:30
var totalOutboundAmountMsat uint64
var totalFeeEarnedMsat uint64
2025-08-21 18:09:03 +07:00
for _ , forward := range forwards {
2026-04-17 13:53:28 +05:30
totalOutboundAmountMsat += forward . OutboundAmountForwardedMsat
totalFeeEarnedMsat += forward . TotalFeeEarnedMsat
2025-08-21 18:09:03 +07:00
}
numForwards := len ( forwards )
return & GetForwardsResponse {
2026-04-17 13:53:28 +05:30
OutboundAmountForwardedSat : totalOutboundAmountMsat / 1000 ,
OutboundAmountForwardedMsat : totalOutboundAmountMsat ,
TotalFeeEarnedSat : totalFeeEarnedMsat / 1000 ,
TotalFeeEarnedMsat : totalFeeEarnedMsat ,
2025-08-21 18:09:03 +07:00
NumForwards : uint64 ( numForwards ) ,
} , nil
}