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