alby_oauth_service.go raw

   1  package alby
   2  
   3  import (
   4  	"bytes"
   5  	"context"
   6  	"encoding/json"
   7  	"errors"
   8  	"fmt"
   9  	"io"
  10  	"net/http"
  11  	"regexp"
  12  	"slices"
  13  	"strconv"
  14  	"strings"
  15  	"sync"
  16  	"time"
  17  
  18  	decodepay "github.com/nbd-wtf/ln-decodepay"
  19  	"github.com/sirupsen/logrus"
  20  	"github.com/tyler-smith/go-bip32"
  21  	"golang.org/x/oauth2"
  22  	"gorm.io/gorm"
  23  
  24  	"github.com/getAlby/hub/apps"
  25  	"github.com/getAlby/hub/config"
  26  	"github.com/getAlby/hub/constants"
  27  	"github.com/getAlby/hub/db"
  28  	"github.com/getAlby/hub/events"
  29  	"github.com/getAlby/hub/lnclient"
  30  	"github.com/getAlby/hub/logger"
  31  	"github.com/getAlby/hub/nip47/permissions"
  32  	"github.com/getAlby/hub/service/keys"
  33  	"github.com/getAlby/hub/utils"
  34  	"github.com/getAlby/hub/version"
  35  )
  36  
  37  type albyOAuthService struct {
  38  	cfg            config.Config
  39  	oauthConf      *oauth2.Config
  40  	db             *gorm.DB
  41  	keys           keys.Keys
  42  	eventPublisher events.EventPublisher
  43  }
  44  
  45  const (
  46  	accessTokenKey       = "AlbyOAuthAccessToken"
  47  	accessTokenExpiryKey = "AlbyOAuthAccessTokenExpiry"
  48  	refreshTokenKey      = "AlbyOAuthRefreshToken"
  49  	userIdentifierKey    = "AlbyUserIdentifier"
  50  	lightningAddressKey  = "AlbyLightningAddress"
  51  )
  52  
  53  const (
  54  	albyOAuthAPIURL  = "https://api.getalby.com"
  55  	albyOAuthAuthUrl = "https://getalby.com/oauth"
  56  )
  57  
  58  const ALBY_ACCOUNT_APP_NAME = "getalby.com"
  59  
  60  func NewAlbyOAuthService(db *gorm.DB, cfg config.Config, keys keys.Keys, eventPublisher events.EventPublisher) *albyOAuthService {
  61  	conf := &oauth2.Config{
  62  		ClientID:     cfg.GetEnv().AlbyClientId,
  63  		ClientSecret: cfg.GetEnv().AlbyClientSecret,
  64  		Scopes:       []string{"account:read"},
  65  		Endpoint: oauth2.Endpoint{
  66  			TokenURL:  albyOAuthAPIURL + "/oauth/token",
  67  			AuthURL:   albyOAuthAuthUrl,
  68  			AuthStyle: 2, // use HTTP Basic Authorization https://pkg.go.dev/golang.org/x/oauth2#AuthStyle
  69  		},
  70  	}
  71  
  72  	if cfg.GetEnv().IsDefaultClientId() {
  73  		conf.RedirectURL = "https://getalby.com/hub/callback"
  74  	} else {
  75  		conf.RedirectURL = cfg.GetEnv().BaseUrl + "/api/alby/callback"
  76  	}
  77  
  78  	albyOAuthSvc := &albyOAuthService{
  79  		oauthConf:      conf,
  80  		cfg:            cfg,
  81  		db:             db,
  82  		keys:           keys,
  83  		eventPublisher: eventPublisher,
  84  	}
  85  	return albyOAuthSvc
  86  }
  87  
  88  func (svc *albyOAuthService) RemoveOAuthAccessToken() error {
  89  	err := svc.cfg.SetUpdate(accessTokenKey, "", "")
  90  	if err != nil {
  91  		logger.Logger.WithError(err).Error("failed to remove access token")
  92  	}
  93  	return err
  94  }
  95  
  96  func (svc *albyOAuthService) CallbackHandler(ctx context.Context, code string) error {
  97  	token, err := svc.oauthConf.Exchange(ctx, code)
  98  	if err != nil {
  99  		logger.Logger.WithError(err).Error("Failed to exchange token")
 100  		return err
 101  	}
 102  	svc.saveToken(token)
 103  
 104  	me, err := svc.GetMe(ctx)
 105  	if err != nil {
 106  		logger.Logger.WithError(err).Error("Failed to fetch user me")
 107  		// remove token so user can retry
 108  		cfgErr := svc.cfg.SetUpdate(accessTokenKey, "", "")
 109  		if cfgErr != nil {
 110  			logger.Logger.WithError(cfgErr).Error("failed to remove existing access token")
 111  		}
 112  		return err
 113  	}
 114  
 115  	existingUserIdentifier, err := svc.GetUserIdentifier()
 116  	if err != nil {
 117  		logger.Logger.WithError(err).Error("Failed to get alby user identifier")
 118  		return err
 119  	}
 120  
 121  	if existingUserIdentifier == "" {
 122  		// save the user's alby account ID on first time login
 123  		err := svc.cfg.SetUpdate(userIdentifierKey, me.Identifier, "")
 124  		if err != nil {
 125  			logger.Logger.WithError(err).Error("Failed to set user identifier")
 126  			return err
 127  		}
 128  		// notify that this was the first time the user connected their account
 129  		svc.eventPublisher.Publish(&events.Event{
 130  			Event:      "nwc_alby_account_connected",
 131  			Properties: map[string]interface{}{},
 132  		})
 133  	} else if me.Identifier != existingUserIdentifier {
 134  		// remove token so user can retry with correct account
 135  		err := svc.cfg.SetUpdate(accessTokenKey, "", "")
 136  		if err != nil {
 137  			logger.Logger.WithError(err).Error("Failed to set user access token")
 138  		}
 139  		return errors.New("Alby Hub is connected to a different alby account. Please log out of your Alby Account at getalby.com and try again.")
 140  	}
 141  
 142  	return nil
 143  }
 144  
 145  func (svc *albyOAuthService) GetUserIdentifier() (string, error) {
 146  	userIdentifier, err := svc.cfg.Get(userIdentifierKey, "")
 147  	if err != nil {
 148  		logger.Logger.WithError(err).Error("Failed to fetch user identifier from user configs")
 149  		return "", err
 150  	}
 151  	return userIdentifier, nil
 152  }
 153  
 154  func (svc *albyOAuthService) GetLightningAddress() (string, error) {
 155  	lightningAddress, err := svc.cfg.Get(lightningAddressKey, "")
 156  	if err != nil {
 157  		logger.Logger.WithError(err).Error("Failed to fetch lightning address from user configs")
 158  		return "", err
 159  	}
 160  	return lightningAddress, nil
 161  }
 162  
 163  func (svc *albyOAuthService) IsConnected(ctx context.Context) bool {
 164  	token, err := svc.fetchUserToken(ctx)
 165  	if err != nil {
 166  		logger.Logger.WithError(err).Error("Failed to check fetch token")
 167  	}
 168  	return token != nil
 169  }
 170  
 171  func (svc *albyOAuthService) saveToken(token *oauth2.Token) {
 172  	err := svc.cfg.SetUpdate(accessTokenExpiryKey, strconv.FormatInt(token.Expiry.Unix(), 10), "")
 173  	if err != nil {
 174  		logger.Logger.WithError(err).Error("Failed to save access token expiry")
 175  	}
 176  	err = svc.cfg.SetUpdate(accessTokenKey, token.AccessToken, "")
 177  	if err != nil {
 178  		logger.Logger.WithError(err).Error("Failed to save access token")
 179  	}
 180  	err = svc.cfg.SetUpdate(refreshTokenKey, token.RefreshToken, "")
 181  	if err != nil {
 182  		logger.Logger.WithError(err).Error("Failed to save refresh token")
 183  	}
 184  }
 185  
 186  var tokenMutex sync.Mutex
 187  
 188  func (svc *albyOAuthService) fetchUserToken(ctx context.Context) (*oauth2.Token, error) {
 189  	tokenMutex.Lock()
 190  	defer tokenMutex.Unlock()
 191  	accessToken, err := svc.cfg.Get(accessTokenKey, "")
 192  	if err != nil {
 193  		return nil, err
 194  	}
 195  
 196  	if accessToken == "" {
 197  		return nil, nil
 198  	}
 199  
 200  	expiry, err := svc.cfg.Get(accessTokenExpiryKey, "")
 201  	if err != nil {
 202  		return nil, err
 203  	}
 204  
 205  	if expiry == "" {
 206  		return nil, nil
 207  	}
 208  
 209  	expiry64, err := strconv.ParseInt(expiry, 10, 64)
 210  	if err != nil {
 211  		return nil, err
 212  	}
 213  	refreshToken, err := svc.cfg.Get(refreshTokenKey, "")
 214  	if err != nil {
 215  		return nil, err
 216  	}
 217  
 218  	if refreshToken == "" {
 219  		return nil, nil
 220  	}
 221  
 222  	currentToken := &oauth2.Token{
 223  		AccessToken:  accessToken,
 224  		Expiry:       time.Unix(expiry64, 0),
 225  		RefreshToken: refreshToken,
 226  	}
 227  
 228  	// only use the current token if it has at least 60 seconds before expiry
 229  	if currentToken.Expiry.After(time.Now().Add(time.Duration(60) * time.Second)) {
 230  		logger.Logger.Debug("Using existing Alby OAuth token")
 231  		return currentToken, nil
 232  	}
 233  
 234  	newToken, err := svc.oauthConf.TokenSource(ctx, currentToken).Token()
 235  	if err != nil {
 236  		logger.Logger.WithError(err).Warn("Failed to refresh existing token")
 237  		return nil, err
 238  	}
 239  
 240  	svc.saveToken(newToken)
 241  	return newToken, nil
 242  }
 243  
 244  func (svc *albyOAuthService) GetVssAuthToken(ctx context.Context, nodeIdentifier string) (string, error) {
 245  	logger.Logger.WithField("node_identifier", nodeIdentifier).Debug("fetching VSS token")
 246  	token, err := svc.fetchUserToken(ctx)
 247  	if err != nil {
 248  		logger.Logger.WithError(err).Error("Failed to fetch user token")
 249  		return "", err
 250  	}
 251  
 252  	client := svc.oauthConf.Client(ctx, token)
 253  	client.Timeout = 10 * time.Second
 254  
 255  	type vssAuthTokenRequest struct {
 256  		Identifier string `json:"identifier"`
 257  	}
 258  
 259  	body := bytes.NewBuffer([]byte{})
 260  	payload := vssAuthTokenRequest{
 261  		Identifier: nodeIdentifier,
 262  	}
 263  	err = json.NewEncoder(body).Encode(&payload)
 264  
 265  	if err != nil {
 266  		logger.Logger.WithError(err).Error("Failed to encode request payload")
 267  		return "", err
 268  	}
 269  
 270  	req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/internal/auth_tokens", albyOAuthAPIURL), body)
 271  	if err != nil {
 272  		logger.Logger.WithError(err).Error("Error creating request for vss auth token endpoint")
 273  		return "", err
 274  	}
 275  
 276  	setDefaultRequestHeaders(req)
 277  
 278  	res, err := client.Do(req)
 279  	if err != nil {
 280  		logger.Logger.WithError(err).Error("Failed to fetch vss auth token endpoint")
 281  		return "", err
 282  	}
 283  
 284  	if res.StatusCode >= 300 {
 285  		return "", fmt.Errorf("request to /internal/auth_tokens returned non-success status: %d", res.StatusCode)
 286  	}
 287  
 288  	type vssTokenResponse struct {
 289  		Token string `json:"token"`
 290  	}
 291  
 292  	vssResponse := &vssTokenResponse{}
 293  	err = json.NewDecoder(res.Body).Decode(vssResponse)
 294  	if err != nil {
 295  		logger.Logger.WithError(err).Error("Failed to decode API response")
 296  		return "", err
 297  	}
 298  
 299  	if vssResponse.Token == "" {
 300  		logger.Logger.WithField("vss_response", vssResponse).WithError(err).Error("No token in API response")
 301  		return "", errors.New("no token in vss response")
 302  	}
 303  
 304  	return vssResponse.Token, nil
 305  }
 306  
 307  func (svc *albyOAuthService) CreateLightningAddress(ctx context.Context, address string, appId uint) (*CreateLightningAddressResponse, error) {
 308  	logger.Logger.WithFields(logrus.Fields{
 309  		"address": address,
 310  		"app_id":  appId,
 311  	}).Debug("creating lightning address")
 312  	token, err := svc.fetchUserToken(ctx)
 313  	if err != nil {
 314  		logger.Logger.WithError(err).Error("Failed to fetch user token")
 315  		return nil, err
 316  	}
 317  
 318  	client := svc.oauthConf.Client(ctx, token)
 319  	client.Timeout = 10 * time.Second
 320  
 321  	type createLightningAddressRequest struct {
 322  		Address string `json:"address"`
 323  		AppId   uint   `json:"app_id"`
 324  	}
 325  
 326  	body := bytes.NewBuffer([]byte{})
 327  	payload := createLightningAddressRequest{
 328  		Address: address,
 329  		AppId:   appId,
 330  	}
 331  	err = json.NewEncoder(body).Encode(&payload)
 332  
 333  	if err != nil {
 334  		logger.Logger.WithError(err).Error("Failed to encode request payload")
 335  		return nil, err
 336  	}
 337  
 338  	req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/internal/lightning_addresses", albyOAuthAPIURL), body)
 339  	if err != nil {
 340  		logger.Logger.WithError(err).Error("Error creating request for vss auth token endpoint")
 341  		return nil, err
 342  	}
 343  
 344  	setDefaultRequestHeaders(req)
 345  
 346  	res, err := client.Do(req)
 347  	if err != nil {
 348  		logger.Logger.WithError(err).Error("Failed to fetch vss auth token endpoint")
 349  		return nil, err
 350  	}
 351  
 352  	responseBody, err := io.ReadAll(res.Body)
 353  	if err != nil {
 354  		logger.Logger.WithError(err).Error("Failed to read response body")
 355  		return nil, errors.New("failed to read response body")
 356  	}
 357  
 358  	if res.StatusCode == 422 {
 359  		type createLightningAddressErrors struct {
 360  			Address []string `json:"address"`
 361  		}
 362  		lightningAddressErrors := &createLightningAddressErrors{}
 363  		err = json.Unmarshal(responseBody, lightningAddressErrors)
 364  		if err != nil {
 365  			logger.Logger.WithError(err).Error("failed to unmarshal errors response")
 366  			return nil, err
 367  		}
 368  		if len(lightningAddressErrors.Address) == 0 {
 369  			return nil, errors.New("unknown error occurred")
 370  		}
 371  		return nil, errors.New(lightningAddressErrors.Address[0])
 372  	}
 373  
 374  	if res.StatusCode >= 300 {
 375  		return nil, fmt.Errorf("POST request to /internal/lightning_addresses/%s returned non-success status: %d %s", address, res.StatusCode, string(responseBody))
 376  	}
 377  
 378  	createLightningAddressResponse := &CreateLightningAddressResponse{}
 379  	err = json.Unmarshal(responseBody, createLightningAddressResponse)
 380  	if err != nil {
 381  		logger.Logger.WithError(err).Error("failed to unmarshal response")
 382  		return nil, err
 383  	}
 384  
 385  	return createLightningAddressResponse, nil
 386  }
 387  
 388  func (svc *albyOAuthService) DeleteLightningAddress(ctx context.Context, address string) error {
 389  	logger.Logger.WithFields(logrus.Fields{
 390  		"address": address,
 391  	}).Debug("deleting lightning address")
 392  	token, err := svc.fetchUserToken(ctx)
 393  	if err != nil {
 394  		logger.Logger.WithError(err).Error("Failed to fetch user token")
 395  		return err
 396  	}
 397  
 398  	client := svc.oauthConf.Client(ctx, token)
 399  	client.Timeout = 10 * time.Second
 400  
 401  	req, err := http.NewRequestWithContext(ctx, "DELETE", fmt.Sprintf("%s/internal/lightning_addresses/%s", albyOAuthAPIURL, address), nil)
 402  	if err != nil {
 403  		logger.Logger.WithError(err).Error("Error creating request for delete lightning address endpoint")
 404  		return err
 405  	}
 406  
 407  	setDefaultRequestHeaders(req)
 408  
 409  	res, err := client.Do(req)
 410  	if err != nil {
 411  		logger.Logger.WithError(err).Error("Failed to request delete lightning address endpoint")
 412  		return err
 413  	}
 414  
 415  	responseBody, err := io.ReadAll(res.Body)
 416  	if err != nil {
 417  		logger.Logger.WithError(err).Error("Failed to read response body")
 418  		return errors.New("failed to read response body")
 419  	}
 420  
 421  	if res.StatusCode == http.StatusNotFound {
 422  		// The lightning address was already deleted on the Alby account
 423  		// (e.g. removed previously). Treat as success so deletion is idempotent.
 424  		logger.Logger.WithField("address", address).Info("Lightning address already deleted on Alby account, ignoring 404")
 425  		return nil
 426  	}
 427  
 428  	if res.StatusCode >= 300 {
 429  		return fmt.Errorf("DELETE request to /internal/lightning_addresses/%s returned non-success status: %d %s", address, res.StatusCode, string(responseBody))
 430  	}
 431  
 432  	return nil
 433  }
 434  
 435  func (svc *albyOAuthService) GetMe(ctx context.Context) (*AlbyMe, error) {
 436  	token, err := svc.fetchUserToken(ctx)
 437  	if err != nil {
 438  		logger.Logger.WithError(err).Error("Failed to fetch user token")
 439  		return nil, err
 440  	}
 441  
 442  	client := svc.oauthConf.Client(ctx, token)
 443  	client.Timeout = 10 * time.Second
 444  
 445  	req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/internal/users", albyOAuthAPIURL), nil)
 446  	if err != nil {
 447  		logger.Logger.WithError(err).Error("Error creating request /me")
 448  		return nil, err
 449  	}
 450  
 451  	setDefaultRequestHeaders(req)
 452  
 453  	res, err := client.Do(req)
 454  	if err != nil {
 455  		logger.Logger.WithError(err).Error("Failed to fetch /me")
 456  		return nil, err
 457  	}
 458  
 459  	body, err := io.ReadAll(res.Body)
 460  	if err != nil {
 461  		logger.Logger.WithError(err).Error("Failed to read response body")
 462  		return nil, errors.New("failed to read response body")
 463  	}
 464  
 465  	if res.StatusCode >= 300 {
 466  		logger.Logger.WithFields(logrus.Fields{
 467  			"body":        string(body),
 468  			"status_code": res.StatusCode,
 469  		}).Error("users endpoint returned non-success code")
 470  		return nil, fmt.Errorf("users endpoint returned non-success code: %s", string(body))
 471  	}
 472  
 473  	me := &AlbyMe{}
 474  	err = json.Unmarshal(body, me)
 475  	if err != nil {
 476  		logger.Logger.WithError(err).Error("Failed to decode API response")
 477  		return nil, err
 478  	}
 479  
 480  	err = svc.cfg.SetUpdate(lightningAddressKey, me.LightningAddress, "")
 481  	if err != nil {
 482  		logger.Logger.WithError(err).Error("Failed to save lightning address")
 483  	}
 484  
 485  	logger.Logger.WithFields(logrus.Fields{"me": me}).Info("Alby me response")
 486  	return me, nil
 487  }
 488  
 489  func (svc *albyOAuthService) GetAuthUrl() string {
 490  	return svc.oauthConf.AuthCodeURL("unused")
 491  }
 492  
 493  func (svc *albyOAuthService) UnlinkAccount(ctx context.Context) error {
 494  	ldkVssEnabled, err := svc.cfg.Get("LdkVssEnabled", "")
 495  	if err != nil {
 496  		logger.Logger.WithError(err).Error("Failed to fetch LdkVssEnabled user config")
 497  		return err
 498  	}
 499  
 500  	if ldkVssEnabled == "true" {
 501  		return errors.New("alby account cannot be unlinked while VSS is activated")
 502  	}
 503  
 504  	destroyAlbyAccountErr := svc.destroyAlbyAccountNWCNode(ctx)
 505  	if destroyAlbyAccountErr != nil {
 506  		// non-critical error - we still want to disconnect
 507  		logger.Logger.WithError(err).Error("Failed to destroy Alby Account NWC node")
 508  	}
 509  	svc.deleteAlbyAccountApps()
 510  
 511  	err = svc.cfg.SetUpdate(userIdentifierKey, "", "")
 512  	if err != nil {
 513  		logger.Logger.WithError(err).Error("Failed to remove user identifier from config")
 514  	}
 515  	err = svc.cfg.SetUpdate(accessTokenKey, "", "")
 516  	if err != nil {
 517  		logger.Logger.WithError(err).Error("Failed to remove access token from config")
 518  	}
 519  	err = svc.cfg.SetUpdate(accessTokenExpiryKey, "", "")
 520  	if err != nil {
 521  		logger.Logger.WithError(err).Error("Failed to remove access token expiry from config")
 522  	}
 523  	err = svc.cfg.SetUpdate(refreshTokenKey, "", "")
 524  	if err != nil {
 525  		logger.Logger.WithError(err).Error("Failed to remove refresh token from config")
 526  	}
 527  	err = svc.cfg.SetUpdate(lightningAddressKey, "", "")
 528  	if err != nil {
 529  		logger.Logger.WithError(err).Error("Failed to remove lightning address from config")
 530  	}
 531  
 532  	return nil
 533  }
 534  
 535  func (svc *albyOAuthService) LinkAccount(ctx context.Context, lnClient lnclient.LNClient, budgetSat uint64, renewal string) error {
 536  	if lnClient == nil {
 537  		return errors.New("LNClient not available")
 538  	}
 539  
 540  	svc.deleteAlbyAccountApps()
 541  
 542  	connectionPubkey, err := svc.createAlbyAccountNWCNode(ctx)
 543  	if err != nil {
 544  		logger.Logger.WithError(err).Error("Failed to create alby account nwc node")
 545  		return err
 546  	}
 547  
 548  	scopes, err := permissions.RequestMethodsToScopes(lnClient.GetSupportedNIP47Methods())
 549  	if err != nil {
 550  		logger.Logger.WithError(err).Error("Failed to get scopes from LNClient request methods")
 551  		return err
 552  	}
 553  	notificationTypes := lnClient.GetSupportedNIP47NotificationTypes()
 554  	if len(notificationTypes) > 0 {
 555  		scopes = append(scopes, constants.NOTIFICATIONS_SCOPE)
 556  	}
 557  
 558  	app, _, err := apps.NewAppsService(svc.db, svc.eventPublisher, svc.keys, svc.cfg).CreateApp(
 559  		ALBY_ACCOUNT_APP_NAME,
 560  		connectionPubkey,
 561  		budgetSat,
 562  		renewal,
 563  		nil,
 564  		scopes,
 565  		false,
 566  		nil,
 567  	)
 568  
 569  	if err != nil {
 570  		logger.Logger.WithError(err).Error("Failed to create app connection")
 571  		return err
 572  	}
 573  
 574  	logger.Logger.WithFields(logrus.Fields{
 575  		"app": app,
 576  	}).Info("Created alby app connection")
 577  
 578  	err = svc.activateAlbyAccountNWCNode(ctx, *app.WalletPubkey)
 579  	if err != nil {
 580  		logger.Logger.WithError(err).Error("Failed to activate alby account nwc node")
 581  		return err
 582  	}
 583  
 584  	return nil
 585  }
 586  
 587  func (svc *albyOAuthService) ConsumeEvent(ctx context.Context, event *events.Event, globalProperties map[string]interface{}) {
 588  	defer func() {
 589  		// ensure the app cannot panic if firing events to Alby API fails
 590  		if r := recover(); r != nil {
 591  			logger.Logger.WithField("event", event).WithField("r", r).Error("Failed to consume event in alby oauth service")
 592  		}
 593  	}()
 594  
 595  	accessToken, err := svc.cfg.Get(accessTokenKey, "")
 596  	if err != nil {
 597  		logger.Logger.WithError(err).Error("failed to get access token from config")
 598  		return
 599  	}
 600  
 601  	if accessToken == "" {
 602  		logger.Logger.WithFields(logrus.Fields{
 603  			"event": event,
 604  		}).Debug("user has not authed yet, skipping event")
 605  		return
 606  	}
 607  
 608  	if !svc.cfg.GetEnv().SendEventsToAlby {
 609  		logger.Logger.WithField("event", event).Debug("Skipped sending to alby events API (alby event logging disabled)")
 610  		return
 611  	}
 612  
 613  	// ensure we do not send unintended events to Alby API
 614  	if !slices.Contains(getEventWhitelist(), event.Event) {
 615  		logger.Logger.WithField("event", event).Debug("Skipped sending non-whitelisted event to alby events API")
 616  		return
 617  	}
 618  
 619  	if event.Event == "nwc_backup_channels" {
 620  		// if backup fails, try again (max 3 attempts)
 621  		for i := 0; i < 3; i++ {
 622  			if err := svc.backupChannels(ctx, event); err != nil {
 623  				logger.Logger.WithField("attempt", i).WithError(err).Error("Failed to backup channels")
 624  				continue
 625  			}
 626  			break
 627  		}
 628  		return
 629  	}
 630  
 631  	if event.Event == "nwc_payment_received" {
 632  		type paymentReceivedEventProperties struct {
 633  			PaymentHash string `json:"payment_hash"`
 634  		}
 635  		// pass a new custom event with less detail
 636  		event = &events.Event{
 637  			Event: event.Event,
 638  			Properties: &paymentReceivedEventProperties{
 639  				PaymentHash: event.Properties.(*db.Transaction).PaymentHash,
 640  			},
 641  		}
 642  	}
 643  
 644  	if event.Event == "nwc_payment_sent" {
 645  		type paymentSentEventProperties struct {
 646  			PaymentHash string `json:"payment_hash"`
 647  			Duration    uint64 `json:"duration"`
 648  		}
 649  
 650  		// pass a new custom event with less detail
 651  		event = &events.Event{
 652  			Event: event.Event,
 653  			Properties: &paymentSentEventProperties{
 654  				PaymentHash: event.Properties.(*db.Transaction).PaymentHash,
 655  				Duration:    uint64(event.Properties.(*db.Transaction).SettledAt.Unix() - event.Properties.(*db.Transaction).CreatedAt.Unix()),
 656  			},
 657  		}
 658  	}
 659  
 660  	if event.Event == "nwc_payment_failed" {
 661  		transaction, ok := event.Properties.(*db.Transaction)
 662  		if !ok {
 663  			logger.Logger.WithField("event", event).Error("Failed to cast event")
 664  			return
 665  		}
 666  
 667  		type paymentFailedEventProperties struct {
 668  			PaymentHash string `json:"payment_hash"`
 669  			Reason      string `json:"reason"`
 670  		}
 671  
 672  		// pass a new custom event with less detail
 673  		event = &events.Event{
 674  			Event: event.Event,
 675  			Properties: &paymentFailedEventProperties{
 676  				PaymentHash: transaction.PaymentHash,
 677  				Reason:      transaction.FailureReason,
 678  			},
 679  		}
 680  	}
 681  
 682  	token, err := svc.fetchUserToken(ctx)
 683  	if err != nil {
 684  		logger.Logger.WithError(err).Error("Failed to fetch user token")
 685  		return
 686  	}
 687  
 688  	client := svc.oauthConf.Client(ctx, token)
 689  	client.Timeout = 10 * time.Second
 690  
 691  	// encode event without global properties
 692  	originalEventBuffer := bytes.NewBuffer([]byte{})
 693  	err = json.NewEncoder(originalEventBuffer).Encode(event)
 694  
 695  	if err != nil {
 696  		logger.Logger.WithError(err).Error("Failed to encode request payload")
 697  		return
 698  	}
 699  
 700  	type eventWithPropertiesMap struct {
 701  		Event      string                 `json:"event"`
 702  		Properties map[string]interface{} `json:"properties"`
 703  	}
 704  
 705  	var eventWithGlobalProperties eventWithPropertiesMap
 706  	err = json.Unmarshal(originalEventBuffer.Bytes(), &eventWithGlobalProperties)
 707  	if err != nil {
 708  		logger.Logger.WithError(err).Error("Failed to decode request payload")
 709  		return
 710  	}
 711  	if eventWithGlobalProperties.Properties == nil {
 712  		eventWithGlobalProperties.Properties = map[string]interface{}{}
 713  	}
 714  
 715  	// add global properties to each published event
 716  	for k, v := range globalProperties {
 717  		_, exists := eventWithGlobalProperties.Properties[k]
 718  		if exists {
 719  			logger.Logger.WithField("key", k).Debug("Key already exists in event properties, skipping global property")
 720  			continue
 721  		}
 722  		eventWithGlobalProperties.Properties[k] = v
 723  	}
 724  
 725  	body := bytes.NewBuffer([]byte{})
 726  	err = json.NewEncoder(body).Encode(&eventWithGlobalProperties)
 727  
 728  	if err != nil {
 729  		logger.Logger.WithError(err).Error("Failed to encode request payload")
 730  		return
 731  	}
 732  
 733  	req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/events", albyOAuthAPIURL), body)
 734  	if err != nil {
 735  		logger.Logger.WithError(err).Error("Error creating request /events")
 736  		return
 737  	}
 738  
 739  	setDefaultRequestHeaders(req)
 740  
 741  	resp, err := client.Do(req)
 742  	if err != nil {
 743  		logger.Logger.WithFields(logrus.Fields{
 744  			"event": eventWithGlobalProperties,
 745  		}).WithError(err).Error("Failed to send request to /events")
 746  		return
 747  	}
 748  
 749  	if resp.StatusCode >= 300 {
 750  		logger.Logger.WithFields(logrus.Fields{
 751  			"event":  eventWithGlobalProperties,
 752  			"status": resp.StatusCode,
 753  		}).Error("Request to /events returned non-success status")
 754  		return
 755  	}
 756  }
 757  
 758  type channelsBackup struct {
 759  	Description string `json:"description"`
 760  	Data        string `json:"data"`
 761  	NodePubkey  string `json:"node_pubkey"`
 762  }
 763  
 764  func (svc *albyOAuthService) createEncryptedChannelBackup(event *events.StaticChannelsBackupEvent) (*channelsBackup, error) {
 765  
 766  	eventData := bytes.NewBuffer([]byte{})
 767  	err := json.NewEncoder(eventData).Encode(event)
 768  	if err != nil {
 769  		return nil, fmt.Errorf("failed to encode channels backup data:  %w", err)
 770  	}
 771  
 772  	backupKey, err := svc.keys.DeriveKey([]uint32{bip32.FirstHardenedChild})
 773  	if err != nil {
 774  		logger.Logger.WithError(err).Error("Failed to generate channels backup key")
 775  		return nil, err
 776  	}
 777  
 778  	encrypted, err := config.AesGcmEncryptWithKey(eventData.String(), backupKey.Key)
 779  	if err != nil {
 780  		return nil, fmt.Errorf("failed to encrypt channels backup data: %w", err)
 781  	}
 782  
 783  	backup := &channelsBackup{
 784  		Description: "channels_v2",
 785  		Data:        encrypted,
 786  		NodePubkey:  event.NodeID,
 787  	}
 788  	return backup, nil
 789  }
 790  
 791  func (svc *albyOAuthService) backupChannels(ctx context.Context, event *events.Event) error {
 792  	bkpEvent, ok := event.Properties.(*events.StaticChannelsBackupEvent)
 793  	if !ok {
 794  		return fmt.Errorf("invalid nwc_backup_channels event properties, could not cast to the expected type: %+v", event.Properties)
 795  	}
 796  
 797  	backup, err := svc.createEncryptedChannelBackup(bkpEvent)
 798  	if err != nil {
 799  		return fmt.Errorf("failed to encrypt channel backup: %w", err)
 800  	}
 801  
 802  	token, err := svc.fetchUserToken(ctx)
 803  	if err != nil {
 804  		return fmt.Errorf("failed to fetch user token: %w", err)
 805  	}
 806  
 807  	client := svc.oauthConf.Client(ctx, token)
 808  	client.Timeout = 10 * time.Second
 809  
 810  	body := bytes.NewBuffer([]byte{})
 811  	err = json.NewEncoder(body).Encode(backup)
 812  	if err != nil {
 813  		return fmt.Errorf("failed to encode channels backup request payload: %w", err)
 814  	}
 815  
 816  	req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/internal/backups", albyOAuthAPIURL), body)
 817  	if err != nil {
 818  		return fmt.Errorf("failed to create request: %w", err)
 819  	}
 820  
 821  	setDefaultRequestHeaders(req)
 822  
 823  	resp, err := client.Do(req)
 824  	if err != nil {
 825  		return fmt.Errorf("failed to send request to /internal/backups: %w", err)
 826  	}
 827  
 828  	if resp.StatusCode >= 300 {
 829  		return fmt.Errorf("request to /internal/backups returned non-success status: %d", resp.StatusCode)
 830  	}
 831  
 832  	return nil
 833  }
 834  
 835  func (svc *albyOAuthService) createAlbyAccountNWCNode(ctx context.Context) (string, error) {
 836  	token, err := svc.fetchUserToken(ctx)
 837  	if err != nil {
 838  		logger.Logger.WithError(err).Error("Failed to fetch user token")
 839  	}
 840  
 841  	client := svc.oauthConf.Client(ctx, token)
 842  	client.Timeout = 10 * time.Second
 843  
 844  	type createNWCNodeRequest struct {
 845  	}
 846  
 847  	createNodeRequest := createNWCNodeRequest{}
 848  
 849  	body := bytes.NewBuffer([]byte{})
 850  	err = json.NewEncoder(body).Encode(&createNodeRequest)
 851  
 852  	if err != nil {
 853  		logger.Logger.WithError(err).Error("Failed to encode request payload")
 854  		return "", err
 855  	}
 856  
 857  	req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/internal/nwcs", albyOAuthAPIURL), body)
 858  	if err != nil {
 859  		logger.Logger.WithError(err).Error("Error creating request /internal/nwcs")
 860  		return "", err
 861  	}
 862  
 863  	setDefaultRequestHeaders(req)
 864  
 865  	resp, err := client.Do(req)
 866  	if err != nil {
 867  		logger.Logger.WithError(err).Error("Failed to send request to /internal/nwcs")
 868  		return "", err
 869  	}
 870  
 871  	if resp.StatusCode >= 300 {
 872  		logger.Logger.WithFields(logrus.Fields{
 873  			"status": resp.StatusCode,
 874  		}).Error("Request to /internal/nwcs returned non-success status")
 875  		return "", errors.New("request to /internal/nwcs returned non-success status")
 876  	}
 877  
 878  	type CreateNWCNodeResponse struct {
 879  		Pubkey string `json:"pubkey"`
 880  	}
 881  
 882  	responsePayload := &CreateNWCNodeResponse{}
 883  	err = json.NewDecoder(resp.Body).Decode(responsePayload)
 884  	if err != nil {
 885  		logger.Logger.WithError(err).Error("Failed to decode response payload")
 886  		return "", err
 887  	}
 888  
 889  	logger.Logger.WithFields(logrus.Fields{
 890  		"pubkey": responsePayload.Pubkey,
 891  	}).Info("Created alby nwc node successfully")
 892  
 893  	return responsePayload.Pubkey, nil
 894  }
 895  
 896  func (svc *albyOAuthService) destroyAlbyAccountNWCNode(ctx context.Context) error {
 897  	token, err := svc.fetchUserToken(ctx)
 898  	if err != nil {
 899  		logger.Logger.WithError(err).Error("Failed to fetch user token")
 900  	}
 901  
 902  	client := svc.oauthConf.Client(ctx, token)
 903  	client.Timeout = 10 * time.Second
 904  
 905  	req, err := http.NewRequestWithContext(ctx, "DELETE", fmt.Sprintf("%s/internal/nwcs", albyOAuthAPIURL), nil)
 906  	if err != nil {
 907  		logger.Logger.WithError(err).Error("Error creating request /internal/nwcs")
 908  		return err
 909  	}
 910  
 911  	setDefaultRequestHeaders(req)
 912  
 913  	resp, err := client.Do(req)
 914  	if err != nil {
 915  		logger.Logger.WithError(err).Error("Failed to send request to /internal/nwcs")
 916  		return err
 917  	}
 918  
 919  	if resp.StatusCode >= 300 {
 920  		logger.Logger.WithFields(logrus.Fields{
 921  			"status": resp.StatusCode,
 922  		}).Error("Request to /internal/nwcs returned non-success status")
 923  		return errors.New("request to /internal/nwcs returned non-success status")
 924  	}
 925  
 926  	logger.Logger.Info("Removed alby account nwc node successfully")
 927  
 928  	return nil
 929  }
 930  
 931  func (svc *albyOAuthService) activateAlbyAccountNWCNode(ctx context.Context, walletServicePubkey string) error {
 932  	token, err := svc.fetchUserToken(ctx)
 933  	if err != nil {
 934  		logger.Logger.WithError(err).Error("Failed to fetch user token")
 935  	}
 936  
 937  	client := svc.oauthConf.Client(ctx, token)
 938  	client.Timeout = 10 * time.Second
 939  
 940  	type activateNWCNodeRequest struct {
 941  		WalletPubkey string `json:"wallet_pubkey"`
 942  		RelayUrl     string `json:"relay_url"`
 943  	}
 944  
 945  	activateNodeRequest := activateNWCNodeRequest{
 946  		WalletPubkey: walletServicePubkey,
 947  		RelayUrl:     svc.cfg.GetRelayUrls()[0], // TODO: pass all URLs if/when Alby Account supports it
 948  	}
 949  
 950  	body := bytes.NewBuffer([]byte{})
 951  	err = json.NewEncoder(body).Encode(&activateNodeRequest)
 952  	if err != nil {
 953  		return err
 954  	}
 955  
 956  	req, err := http.NewRequestWithContext(ctx, "PUT", fmt.Sprintf("%s/internal/nwcs/activate", albyOAuthAPIURL), body)
 957  	if err != nil {
 958  		logger.Logger.WithError(err).Error("Error creating request /internal/nwcs/activate")
 959  		return err
 960  	}
 961  
 962  	setDefaultRequestHeaders(req)
 963  
 964  	resp, err := client.Do(req)
 965  	if err != nil {
 966  		logger.Logger.WithFields(logrus.Fields{
 967  			"activate_node_request": activateNodeRequest,
 968  		}).WithError(err).Error("Failed to send request to /internal/nwcs/activate")
 969  		return err
 970  	}
 971  
 972  	if resp.StatusCode >= 300 {
 973  		bodyString := ""
 974  		bodyBytes, err := io.ReadAll(resp.Body)
 975  		if err != nil {
 976  			logger.Logger.WithFields(logrus.Fields{
 977  				"activate_node_request": activateNodeRequest,
 978  				"status":                resp.StatusCode,
 979  			}).Error("Failed to read response body from to /internal/nwcs/activate")
 980  		}
 981  		if bodyBytes != nil {
 982  			bodyString = string(bodyBytes)
 983  		}
 984  
 985  		logger.Logger.WithFields(logrus.Fields{
 986  			"status":  resp.StatusCode,
 987  			"message": bodyString,
 988  		}).Error("Request to /internal/nwcs/activate returned non-success status")
 989  		return errors.New("request to /internal/nwcs/activate returned non-success status")
 990  	}
 991  
 992  	logger.Logger.Info("Activated alby nwc node successfully")
 993  
 994  	return nil
 995  }
 996  
 997  func (svc *albyOAuthService) GetLSPChannelOffer(ctx context.Context) (*LSPChannelOffer, error) {
 998  	token, err := svc.fetchUserToken(ctx)
 999  	if err != nil {
1000  		logger.Logger.WithError(err).Error("Failed to fetch user token")
1001  		return nil, err
1002  	}
1003  
1004  	client := svc.oauthConf.Client(ctx, token)
1005  	client.Timeout = 10 * time.Second
1006  
1007  	req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/internal/lsp", albyOAuthAPIURL), nil)
1008  	if err != nil {
1009  		logger.Logger.WithError(err).Error("Error creating request /me")
1010  		return nil, err
1011  	}
1012  
1013  	setDefaultRequestHeaders(req)
1014  
1015  	res, err := client.Do(req)
1016  	if err != nil {
1017  		logger.Logger.WithError(err).Error("Failed to fetch /me")
1018  		return nil, err
1019  	}
1020  
1021  	body, err := io.ReadAll(res.Body)
1022  	if err != nil {
1023  		logger.Logger.WithError(err).Error("Failed to read response body")
1024  		return nil, errors.New("failed to read response body")
1025  	}
1026  
1027  	if res.StatusCode >= 300 {
1028  		logger.Logger.WithFields(logrus.Fields{
1029  			"body":        string(body),
1030  			"status_code": res.StatusCode,
1031  		}).Error("lsp channel offer endpoint returned non-success code")
1032  		return nil, fmt.Errorf("lsp channel offer endpoint returned non-success code: %s", string(body))
1033  	}
1034  
1035  	lspChannelOffer := &LSPChannelOffer{}
1036  	err = json.Unmarshal(body, lspChannelOffer)
1037  	if err != nil {
1038  		logger.Logger.WithError(err).Error("Failed to decode API response")
1039  		return nil, err
1040  	}
1041  
1042  	return lspChannelOffer, nil
1043  }
1044  
1045  func (svc *albyOAuthService) GetLSPInfo(ctx context.Context, lspIdentifier, network string) (*LSPInfo, error) {
1046  	token, err := svc.fetchUserToken(ctx)
1047  	if err != nil {
1048  		logger.Logger.WithError(err).Error("Failed to fetch user token")
1049  		return nil, err
1050  	}
1051  
1052  	var client *http.Client
1053  	if token != nil {
1054  		client = svc.oauthConf.Client(ctx, token)
1055  	} else {
1056  		client = &http.Client{}
1057  	}
1058  	client.Timeout = 30 * time.Second
1059  
1060  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/internal/lsp/%s/%s/v1/get_info", albyOAuthAPIURL, lspIdentifier, network), nil)
1061  	if err != nil {
1062  		logger.Logger.WithError(err).Error("Failed to create lsp info request")
1063  		return nil, err
1064  	}
1065  
1066  	setDefaultRequestHeaders(req)
1067  
1068  	res, err := client.Do(req)
1069  	if err != nil {
1070  		logger.Logger.WithError(err).Error("Failed to request lsp info")
1071  		return nil, err
1072  	}
1073  
1074  	body, err := io.ReadAll(res.Body)
1075  	if err != nil {
1076  		logger.Logger.WithError(err).Error("Failed to read response body")
1077  		return nil, errors.New("failed to read response body")
1078  	}
1079  
1080  	if res.StatusCode >= 300 {
1081  		logger.Logger.WithFields(logrus.Fields{
1082  			"body":        string(body),
1083  			"status_code": res.StatusCode,
1084  		}).Error("lsp info endpoint returned non-success code")
1085  		return nil, fmt.Errorf("lsp info endpoint returned non-success code: %s", string(body))
1086  	}
1087  
1088  	type lsps1LSPInfo struct {
1089  		MinRequiredChannelConfirmations uint64   `json:"min_required_channel_confirmations"`
1090  		MinFundingConfirmsWithinBlocks  uint64   `json:"min_funding_confirms_within_blocks"`
1091  		MaxChannelExpiryBlocks          uint64   `json:"max_channel_expiry_blocks"`
1092  		URIs                            []string `json:"uris"`
1093  	}
1094  
1095  	lsps1LspInfo := &lsps1LSPInfo{}
1096  	err = json.Unmarshal(body, lsps1LspInfo)
1097  	if err != nil {
1098  		logger.Logger.WithError(err).Error("Failed to decode API response")
1099  		return nil, err
1100  	}
1101  
1102  	httpUris := utils.Filter(lsps1LspInfo.URIs, func(uri string) bool {
1103  		return !strings.Contains(uri, ".onion")
1104  	})
1105  	if len(httpUris) == 0 {
1106  		logger.Logger.WithField("uris", lsps1LspInfo.URIs).WithError(err).Error("Couldn't find HTTP URI")
1107  		return nil, err
1108  	}
1109  	uri := httpUris[0]
1110  
1111  	// make sure it's a valid IPv4 URI
1112  	regex := regexp.MustCompile(`^([0-9a-f]+)@([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+):([0-9]+)$`)
1113  	parts := regex.FindStringSubmatch(uri)
1114  	logger.Logger.WithField("parts", parts).Info("Split URI")
1115  	if parts == nil || len(parts) != 4 {
1116  		logger.Logger.WithField("parts", parts).Error("Unsupported URI")
1117  		return nil, errors.New("could not decode LSP URI")
1118  	}
1119  
1120  	port, err := strconv.Atoi(parts[3])
1121  	if err != nil {
1122  		logger.Logger.WithField("port", parts[3]).WithError(err).Error("Failed to decode port number")
1123  
1124  		return nil, err
1125  	}
1126  
1127  	return &LSPInfo{
1128  		Pubkey:                          parts[1],
1129  		Address:                         parts[2],
1130  		Port:                            uint16(port),
1131  		MaxChannelExpiryBlocks:          lsps1LspInfo.MaxChannelExpiryBlocks,
1132  		MinRequiredChannelConfirmations: lsps1LspInfo.MinRequiredChannelConfirmations,
1133  		MinFundingConfirmsWithinBlocks:  lsps1LspInfo.MinFundingConfirmsWithinBlocks,
1134  	}, nil
1135  }
1136  
1137  func (svc *albyOAuthService) CreateLSPOrder(ctx context.Context, lsp, network string, lspChannelRequest *LSPChannelRequest) (*LSPChannelResponse, error) {
1138  	token, err := svc.fetchUserToken(ctx)
1139  	if err != nil {
1140  		logger.Logger.WithError(err).Error("Failed to fetch user token")
1141  		return nil, err
1142  	}
1143  
1144  	var client *http.Client
1145  	if token != nil {
1146  		client = svc.oauthConf.Client(ctx, token)
1147  	} else {
1148  		client = &http.Client{}
1149  	}
1150  	client.Timeout = 30 * time.Second
1151  
1152  	payloadBytes, err := json.Marshal(lspChannelRequest)
1153  	if err != nil {
1154  		return nil, err
1155  	}
1156  	bodyReader := bytes.NewReader(payloadBytes)
1157  
1158  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("%s/internal/lsp/%s/%s/v1/create_order", albyOAuthAPIURL, lsp, network), bodyReader)
1159  	if err != nil {
1160  		logger.Logger.WithError(err).Error("Failed to create lsp order request")
1161  		return nil, err
1162  	}
1163  
1164  	setDefaultRequestHeaders(req)
1165  
1166  	res, err := client.Do(req)
1167  	if err != nil {
1168  		logger.Logger.WithError(err).Error("Failed to request lsp order")
1169  		return nil, err
1170  	}
1171  
1172  	body, err := io.ReadAll(res.Body)
1173  	if err != nil {
1174  		logger.Logger.WithError(err).Error("Failed to read response body")
1175  		return nil, errors.New("failed to read response body")
1176  	}
1177  
1178  	if res.StatusCode >= 300 {
1179  		logger.Logger.WithFields(logrus.Fields{
1180  			"body":        string(body),
1181  			"status_code": res.StatusCode,
1182  		}).Error("lsp create order endpoint returned non-success code")
1183  		return nil, fmt.Errorf("lsp create order endpoint returned non-success code: %s", string(body))
1184  	}
1185  
1186  	channelResponse := &LSPChannelResponse{}
1187  	err = json.Unmarshal(body, channelResponse)
1188  	if err != nil {
1189  		logger.Logger.WithError(err).Error("Failed to decode API response")
1190  		return nil, err
1191  	}
1192  
1193  	return channelResponse, nil
1194  }
1195  
1196  func (svc *albyOAuthService) RequestAutoChannel(ctx context.Context, lnClient lnclient.LNClient, isPublic bool) (*AutoChannelResponse, error) {
1197  	if lnClient == nil {
1198  		return nil, errors.New("LNClient not available")
1199  	}
1200  
1201  	nodeInfo, err := lnClient.GetInfo(ctx)
1202  	if err != nil {
1203  		logger.Logger.WithError(err).Error("Failed to request own node info", err)
1204  		return nil, err
1205  	}
1206  
1207  	lspInfo, err := svc.GetLSPInfo(ctx, "alby", nodeInfo.Network)
1208  
1209  	if err != nil {
1210  		logger.Logger.WithError(err).Error("Failed to request LSP info")
1211  		return nil, err
1212  	}
1213  
1214  	err = lnClient.ConnectPeer(ctx, &lnclient.ConnectPeerRequest{
1215  		Pubkey:  lspInfo.Pubkey,
1216  		Address: lspInfo.Address,
1217  		Port:    lspInfo.Port,
1218  	})
1219  
1220  	if err != nil {
1221  		logger.Logger.WithFields(logrus.Fields{
1222  			"pubkey":  lspInfo.Pubkey,
1223  			"address": lspInfo.Address,
1224  			"port":    lspInfo.Port,
1225  		}).WithError(err).Error("Failed to connect to peer")
1226  		return nil, err
1227  	}
1228  
1229  	logger.Logger.WithFields(logrus.Fields{
1230  		"pubkey": lspInfo.Pubkey,
1231  		"public": isPublic,
1232  	}).Info("Requesting auto channel")
1233  
1234  	autoChannelResponse, err := svc.requestAutoChannel(ctx, fmt.Sprintf("%s/internal/lsp/alby/%s/auto_channel", albyOAuthAPIURL, nodeInfo.Network), nodeInfo.Pubkey, isPublic)
1235  	if err != nil {
1236  		logger.Logger.WithError(err).Error("Failed to request auto channel")
1237  		return nil, err
1238  	}
1239  	return autoChannelResponse, nil
1240  }
1241  
1242  func (svc *albyOAuthService) requestAutoChannel(ctx context.Context, url string, pubkey string, isPublic bool) (*AutoChannelResponse, error) {
1243  	token, err := svc.fetchUserToken(ctx)
1244  	if err != nil {
1245  		logger.Logger.WithError(err).Error("Failed to fetch user token")
1246  	}
1247  
1248  	client := svc.oauthConf.Client(ctx, token)
1249  	client.Timeout = 60 * time.Second
1250  
1251  	type autoChannelRequest struct {
1252  		PublicKey       string `json:"public_key"`
1253  		AnnounceChannel bool   `json:"announce_channel"`
1254  		NodeType        string `json:"node_type"`
1255  	}
1256  
1257  	backendType, err := svc.cfg.Get("LNBackendType", "")
1258  	if err != nil {
1259  		return nil, errors.New("failed to get LN backend type")
1260  	}
1261  	newAutoChannelRequest := autoChannelRequest{
1262  		PublicKey:       pubkey,
1263  		AnnounceChannel: isPublic,
1264  		NodeType:        backendType,
1265  	}
1266  
1267  	payloadBytes, err := json.Marshal(newAutoChannelRequest)
1268  	if err != nil {
1269  		return nil, err
1270  	}
1271  	bodyReader := bytes.NewReader(payloadBytes)
1272  
1273  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bodyReader)
1274  	if err != nil {
1275  		logger.Logger.WithError(err).WithFields(logrus.Fields{
1276  			"url": url,
1277  		}).Error("Failed to create auto channel request")
1278  		return nil, err
1279  	}
1280  
1281  	setDefaultRequestHeaders(req)
1282  
1283  	res, err := client.Do(req)
1284  	if err != nil {
1285  		logger.Logger.WithError(err).WithFields(logrus.Fields{
1286  			"url": url,
1287  		}).Error("Failed to request auto channel invoice")
1288  		return nil, err
1289  	}
1290  
1291  	defer res.Body.Close()
1292  
1293  	body, err := io.ReadAll(res.Body)
1294  	if err != nil {
1295  		logger.Logger.WithError(err).WithFields(logrus.Fields{
1296  			"url": url,
1297  		}).Error("Failed to read response body")
1298  		return nil, errors.New("failed to read response body")
1299  	}
1300  
1301  	if res.StatusCode >= 300 {
1302  		logger.Logger.WithFields(logrus.Fields{
1303  			"request":     newAutoChannelRequest,
1304  			"body":        string(body),
1305  			"status_code": res.StatusCode,
1306  		}).Error("auto channel endpoint returned non-success code")
1307  		return nil, fmt.Errorf("auto channel endpoint returned non-success code: %s", string(body))
1308  	}
1309  
1310  	type newLSPS1ChannelPaymentBolt11 struct {
1311  		Invoice     string `json:"invoice"`
1312  		FeeTotalSat string `json:"fee_total_sat"`
1313  	}
1314  
1315  	type newLSPS1ChannelPayment struct {
1316  		Bolt11 newLSPS1ChannelPaymentBolt11 `json:"bolt11"`
1317  		// TODO: add onchain
1318  	}
1319  	type autoChannelResponse struct {
1320  		LspBalanceSat string                  `json:"lsp_balance_sat"`
1321  		Payment       *newLSPS1ChannelPayment `json:"payment"`
1322  	}
1323  
1324  	var newAutoChannelResponse autoChannelResponse
1325  
1326  	err = json.Unmarshal(body, &newAutoChannelResponse)
1327  	if err != nil {
1328  		logger.Logger.WithError(err).WithFields(logrus.Fields{
1329  			"url": url,
1330  		}).Error("Failed to deserialize json")
1331  		return nil, fmt.Errorf("failed to deserialize json %s %s", url, string(body))
1332  	}
1333  
1334  	var invoice string
1335  	var feeSat uint64
1336  
1337  	if newAutoChannelResponse.Payment != nil {
1338  		invoice = newAutoChannelResponse.Payment.Bolt11.Invoice
1339  		feeSat, err = strconv.ParseUint(newAutoChannelResponse.Payment.Bolt11.FeeTotalSat, 10, 64)
1340  		if err != nil {
1341  			logger.Logger.WithError(err).WithFields(logrus.Fields{
1342  				"url": url,
1343  			}).Error("Failed to parse fee")
1344  			return nil, fmt.Errorf("failed to parse fee %v", err)
1345  		}
1346  
1347  		paymentRequest, err := decodepay.Decodepay(invoice)
1348  		if err != nil {
1349  			logger.Logger.WithError(err).Error("Failed to decode bolt11 invoice")
1350  			return nil, err
1351  		}
1352  
1353  		if feeSat != uint64(paymentRequest.MSatoshi/1000) {
1354  			logger.Logger.WithFields(logrus.Fields{
1355  				"invoice_amount": paymentRequest.MSatoshi / 1000,
1356  				"fee":            feeSat,
1357  			}).WithError(err).Error("Invoice amount does not match LSP fee")
1358  			return nil, errors.New("invoice amount does not match LSP fee")
1359  		}
1360  	}
1361  
1362  	channelSizeSat, err := strconv.ParseUint(newAutoChannelResponse.LspBalanceSat, 10, 64)
1363  	if err != nil {
1364  		logger.Logger.WithError(err).WithFields(logrus.Fields{
1365  			"url": url,
1366  		}).Error("Failed to parse lsp balance sat")
1367  		return nil, fmt.Errorf("failed to parse lsp balance sat %v", err)
1368  	}
1369  
1370  	return &AutoChannelResponse{
1371  		Invoice:        invoice,
1372  		Fee:            feeSat,
1373  		FeeSat:         feeSat,
1374  		ChannelSize:    channelSizeSat,
1375  		ChannelSizeSat: channelSizeSat,
1376  	}, nil
1377  }
1378  
1379  func (svc *albyOAuthService) GetStories(ctx context.Context) ([]Story, error) {
1380  	client := &http.Client{Timeout: 10 * time.Second}
1381  	url := fmt.Sprintf("%s/stories", albyInternalAPIURL)
1382  
1383  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
1384  	if err != nil {
1385  		logger.Logger.WithError(err).Error("Error creating request to stories endpoint")
1386  		return nil, fmt.Errorf("create stories request: %w", err)
1387  	}
1388  	setDefaultRequestHeaders(req)
1389  
1390  	res, err := client.Do(req)
1391  	if err != nil {
1392  		logger.Logger.WithError(err).Error("Failed to fetch stories from API")
1393  		return nil, fmt.Errorf("fetch stories: %w", err)
1394  	}
1395  	defer res.Body.Close()
1396  
1397  	body, err := io.ReadAll(res.Body)
1398  	if err != nil {
1399  		logger.Logger.WithError(err).WithFields(logrus.Fields{
1400  			"url": url,
1401  		}).Error("Failed to read response body")
1402  		return nil, fmt.Errorf("read stories response body: %w", err)
1403  	}
1404  
1405  	if res.StatusCode >= 300 {
1406  		logger.Logger.WithFields(logrus.Fields{
1407  			"body":        string(body),
1408  			"status_code": res.StatusCode,
1409  		}).Error("stories endpoint returned non-success code")
1410  		return nil, fmt.Errorf("stories endpoint returned %d: %s", res.StatusCode, string(body))
1411  	}
1412  
1413  	var stories []Story
1414  	if err := json.Unmarshal(body, &stories); err != nil {
1415  		logger.Logger.WithFields(logrus.Fields{
1416  			"body":  string(body),
1417  			"error": err,
1418  		}).Error("Failed to decode stories API response")
1419  		return nil, fmt.Errorf("decode stories response: %w", err)
1420  	}
1421  
1422  	return stories, nil
1423  }
1424  
1425  func setDefaultRequestHeaders(req *http.Request) {
1426  	req.Header.Set("Content-Type", "application/json")
1427  	req.Header.Set("User-Agent", "AlbyHub/"+version.Tag)
1428  }
1429  
1430  func (svc *albyOAuthService) deleteAlbyAccountApps() {
1431  	// delete any existing getalby.com connections so when re-linking the user only has one
1432  	err := svc.db.Where("name = ?", ALBY_ACCOUNT_APP_NAME).Delete(&db.App{}).Error
1433  	if err != nil {
1434  		logger.Logger.WithError(err).Error("Failed to delete Alby Account apps")
1435  	}
1436  }
1437  
1438  // whitelist of events that can be sent to the alby API
1439  // (e.g. to enable encrypted static channel backups and sending email notifications)
1440  func getEventWhitelist() []string {
1441  	return []string{
1442  		"nwc_backup_channels",
1443  		"nwc_payment_received",
1444  		"nwc_payment_sent",
1445  		"nwc_payment_failed",
1446  		"nwc_app_created",
1447  		"nwc_app_updated",
1448  		"nwc_app_deleted",
1449  		"nwc_unlocked",
1450  		"nwc_node_sync_failed",
1451  		"nwc_outgoing_liquidity_required",
1452  		"nwc_incoming_liquidity_required",
1453  		"nwc_budget_warning",
1454  		"nwc_channel_ready",
1455  		"nwc_channel_closed",
1456  		"nwc_permission_denied",
1457  		"nwc_started",
1458  		"nwc_stopped",
1459  		"nwc_node_started",
1460  		"nwc_node_start_failed",
1461  		"nwc_node_stop_failed",
1462  		"nwc_node_stopped",
1463  		"nwc_alby_account_connected",
1464  		"nwc_swap_succeeded",
1465  		"nwc_rebalance_succeeded",
1466  
1467  		// client-side events
1468  		"payment_failed_details",
1469  		"debit_card_url_clicked",
1470  		"debit_card_connect",
1471  	}
1472  }
1473