mirror of
https://github.com/getAlby/hub.git
synced 2026-08-13 12:33:39 +02:00
feat: choose payment method when opening first channel (#1606)
* feat: choose payment method when opening first channel (WIP) * chore: update terms * chore: consume lsp endpoint, display fees+LSP in manual channel flow, first channel ui improvements * chore: improve handling of fee_credits payment method * chore: add terms and description * feat: add terms to manual increase incoming capacity flow * chore: add error handling for get_info endpoint * feat: allow alby account to pay for manual channel order * chore: use consistent logging methods * chore: rename lsp balance sats field to be consistent * chore: update to use consistent field name for public key in auto channel request * chore: rename channel suggestion url and type fields * feat: support included payment method * chore: update LDK startup peers * chore: move non-oauth methods out of alby oauth service * chore: improve terms around duration, only display duration in terms modal
This commit is contained in:
parent
49c1500011
commit
13c24c7a87
24 changed files with 774 additions and 317 deletions
|
|
@ -51,9 +51,8 @@ const (
|
|||
)
|
||||
|
||||
const (
|
||||
albyOAuthAPIURL = "https://api.getalby.com"
|
||||
albyInternalAPIURL = "https://getalby.com/api"
|
||||
albyOAuthAuthUrl = "https://getalby.com/oauth"
|
||||
albyOAuthAPIURL = "https://api.getalby.com"
|
||||
albyOAuthAuthUrl = "https://getalby.com/oauth"
|
||||
)
|
||||
|
||||
const ALBY_ACCOUNT_APP_NAME = "getalby.com"
|
||||
|
|
@ -242,88 +241,6 @@ func (svc *albyOAuthService) fetchUserToken(ctx context.Context) (*oauth2.Token,
|
|||
return newToken, nil
|
||||
}
|
||||
|
||||
func (svc *albyOAuthService) GetInfo(ctx context.Context) (*AlbyInfo, error) {
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("%s/internal/info", albyInternalAPIURL), nil)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Error creating request to alby info endpoint")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
setDefaultRequestHeaders(req)
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to fetch /info")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
type albyInfoHub struct {
|
||||
LatestVersion string `json:"latest_version"`
|
||||
LatestReleaseNotes string `json:"latest_release_notes"`
|
||||
}
|
||||
|
||||
type albyInfoIncident struct {
|
||||
Name string `json:"name"`
|
||||
Started string `json:"started"`
|
||||
Status string `json:"status"`
|
||||
Impact string `json:"impact"`
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
type albyInfo struct {
|
||||
Hub albyInfoHub `json:"hub"`
|
||||
Status string `json:"status"`
|
||||
Healthy bool `json:"healthy"`
|
||||
AccountAvailable bool `json:"account_available"` // false if country is blocked (can still use Alby Hub without an Alby Account)
|
||||
Incidents []albyInfoIncident `json:"incidents"`
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to read response body")
|
||||
return nil, errors.New("failed to read response body")
|
||||
}
|
||||
|
||||
if res.StatusCode >= 300 {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"body": string(body),
|
||||
"status_code": res.StatusCode,
|
||||
}).Error("info endpoint returned non-success code")
|
||||
return nil, fmt.Errorf("info endpoint returned non-success code: %s", string(body))
|
||||
}
|
||||
|
||||
info := &albyInfo{}
|
||||
err = json.Unmarshal(body, info)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to decode API response")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
incidents := []AlbyInfoIncident{}
|
||||
for _, incident := range info.Incidents {
|
||||
incidents = append(incidents, AlbyInfoIncident{
|
||||
Name: incident.Name,
|
||||
Started: incident.Started,
|
||||
Status: incident.Status,
|
||||
Impact: incident.Impact,
|
||||
Url: incident.Url,
|
||||
})
|
||||
}
|
||||
|
||||
return &AlbyInfo{
|
||||
Hub: AlbyInfoHub{
|
||||
LatestVersion: info.Hub.LatestVersion,
|
||||
LatestReleaseNotes: info.Hub.LatestReleaseNotes,
|
||||
},
|
||||
Status: info.Status,
|
||||
Healthy: info.Healthy,
|
||||
AccountAvailable: info.AccountAvailable,
|
||||
Incidents: incidents,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (svc *albyOAuthService) GetVssAuthToken(ctx context.Context, nodeIdentifier string) (string, error) {
|
||||
logger.Logger.WithField("node_identifier", nodeIdentifier).Debug("fetching VSS token")
|
||||
token, err := svc.fetchUserToken(ctx)
|
||||
|
|
@ -1156,6 +1073,9 @@ func (svc *albyOAuthService) activateAlbyAccountNWCNode(ctx context.Context, wal
|
|||
|
||||
body := bytes.NewBuffer([]byte{})
|
||||
err = json.NewEncoder(body).Encode(&activateNodeRequest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("PUT", fmt.Sprintf("%s/internal/nwcs/activate", albyOAuthAPIURL), body)
|
||||
if err != nil {
|
||||
|
|
@ -1198,13 +1118,19 @@ func (svc *albyOAuthService) activateAlbyAccountNWCNode(ctx context.Context, wal
|
|||
return nil
|
||||
}
|
||||
|
||||
func (svc *albyOAuthService) GetChannelPeerSuggestions(ctx context.Context) ([]ChannelPeerSuggestion, error) {
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("%s/internal/channel_suggestions", albyInternalAPIURL), nil)
|
||||
func (svc *albyOAuthService) GetLSPChannelOffer(ctx context.Context) (*LSPChannelOffer, error) {
|
||||
token, err := svc.fetchUserToken(ctx)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Error creating request to channel_suggestions endpoint")
|
||||
logger.Logger.WithError(err).Error("Failed to fetch user token")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := svc.oauthConf.Client(ctx, token)
|
||||
client.Timeout = 10 * time.Second
|
||||
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("%s/internal/lsp", albyOAuthAPIURL), nil)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Error creating request /me")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
@ -1212,7 +1138,7 @@ func (svc *albyOAuthService) GetChannelPeerSuggestions(ctx context.Context) ([]C
|
|||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to fetch channel_suggestions endpoint")
|
||||
logger.Logger.WithError(err).Error("Failed to fetch /me")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
@ -1226,77 +1152,18 @@ func (svc *albyOAuthService) GetChannelPeerSuggestions(ctx context.Context) ([]C
|
|||
logger.Logger.WithFields(logrus.Fields{
|
||||
"body": string(body),
|
||||
"status_code": res.StatusCode,
|
||||
}).Error("channel suggestions endpoint returned non-success code")
|
||||
return nil, fmt.Errorf("channel suggestions endpoint returned non-success code: %s", string(body))
|
||||
}).Error("users endpoint returned non-success code")
|
||||
return nil, fmt.Errorf("users endpoint returned non-success code: %s", string(body))
|
||||
}
|
||||
|
||||
var suggestions []ChannelPeerSuggestion
|
||||
err = json.Unmarshal(body, &suggestions)
|
||||
lspChannelOffer := &LSPChannelOffer{}
|
||||
err = json.Unmarshal(body, lspChannelOffer)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Errorf("Failed to decode API response")
|
||||
logger.Logger.WithError(err).Error("Failed to decode API response")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger.Logger.WithFields(logrus.Fields{"channel_suggestions": suggestions}).Debug("Alby channel peer suggestions response")
|
||||
return suggestions, nil
|
||||
}
|
||||
|
||||
func (svc *albyOAuthService) GetBitcoinRate(ctx context.Context) (*BitcoinRate, error) {
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
currency := svc.cfg.GetCurrency()
|
||||
|
||||
url := fmt.Sprintf("%s/rates/%s", albyInternalAPIURL, currency)
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"currency": currency,
|
||||
"error": err,
|
||||
}).Error("Error creating request to Bitcoin rate endpoint")
|
||||
return nil, err
|
||||
}
|
||||
setDefaultRequestHeaders(req)
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"currency": currency,
|
||||
"error": err,
|
||||
}).Error("Failed to fetch Bitcoin rate from API")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).WithFields(logrus.Fields{
|
||||
"url": url,
|
||||
}).Error("Failed to read response body")
|
||||
return nil, errors.New("failed to read response body")
|
||||
}
|
||||
|
||||
if res.StatusCode >= 300 {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"currency": currency,
|
||||
"body": string(body),
|
||||
"status_code": res.StatusCode,
|
||||
}).Error("Bitcoin rate endpoint returned non-success code")
|
||||
return nil, fmt.Errorf("bitcoin rate endpoint returned non-success code: %s", string(body))
|
||||
}
|
||||
|
||||
var rate = &BitcoinRate{}
|
||||
err = json.Unmarshal(body, rate)
|
||||
if err != nil {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"currency": currency,
|
||||
"body": string(body),
|
||||
"error": err,
|
||||
}).Error("Failed to decode Bitcoin rate API response")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rate, nil
|
||||
return lspChannelOffer, nil
|
||||
}
|
||||
|
||||
func (svc *albyOAuthService) RequestAutoChannel(ctx context.Context, lnClient lnclient.LNClient, isPublic bool) (*AutoChannelResponse, error) {
|
||||
|
|
@ -1353,7 +1220,7 @@ func (svc *albyOAuthService) requestAutoChannel(ctx context.Context, url string,
|
|||
client.Timeout = 60 * time.Second
|
||||
|
||||
type autoChannelRequest struct {
|
||||
NodePubkey string `json:"node_pubkey"`
|
||||
PublicKey string `json:"public_key"`
|
||||
AnnounceChannel bool `json:"announce_channel"`
|
||||
NodeType string `json:"node_type"`
|
||||
}
|
||||
|
|
@ -1363,7 +1230,7 @@ func (svc *albyOAuthService) requestAutoChannel(ctx context.Context, url string,
|
|||
return nil, errors.New("failed to get LN backend type")
|
||||
}
|
||||
newAutoChannelRequest := autoChannelRequest{
|
||||
NodePubkey: pubkey,
|
||||
PublicKey: pubkey,
|
||||
AnnounceChannel: isPublic,
|
||||
NodeType: backendType,
|
||||
}
|
||||
|
|
|
|||
210
alby/alby_service.go
Normal file
210
alby/alby_service.go
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
package alby
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/getAlby/hub/config"
|
||||
"github.com/getAlby/hub/logger"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const albyInternalAPIURL = "https://getalby.com/api"
|
||||
|
||||
type albyService struct {
|
||||
cfg config.Config
|
||||
}
|
||||
|
||||
func NewAlbyService(cfg config.Config) *albyService {
|
||||
albySvc := &albyService{
|
||||
cfg: cfg,
|
||||
}
|
||||
return albySvc
|
||||
}
|
||||
|
||||
func (svc *albyService) GetBitcoinRate(ctx context.Context) (*BitcoinRate, error) {
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
currency := svc.cfg.GetCurrency()
|
||||
|
||||
url := fmt.Sprintf("%s/rates/%s", albyInternalAPIURL, currency)
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"currency": currency,
|
||||
"error": err,
|
||||
}).Error("Error creating request to Bitcoin rate endpoint")
|
||||
return nil, err
|
||||
}
|
||||
setDefaultRequestHeaders(req)
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"currency": currency,
|
||||
"error": err,
|
||||
}).Error("Failed to fetch Bitcoin rate from API")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).WithFields(logrus.Fields{
|
||||
"url": url,
|
||||
}).Error("Failed to read response body")
|
||||
return nil, errors.New("failed to read response body")
|
||||
}
|
||||
|
||||
if res.StatusCode >= 300 {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"currency": currency,
|
||||
"body": string(body),
|
||||
"status_code": res.StatusCode,
|
||||
}).Error("Bitcoin rate endpoint returned non-success code")
|
||||
return nil, fmt.Errorf("bitcoin rate endpoint returned non-success code: %s", string(body))
|
||||
}
|
||||
|
||||
var rate = &BitcoinRate{}
|
||||
err = json.Unmarshal(body, rate)
|
||||
if err != nil {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"currency": currency,
|
||||
"body": string(body),
|
||||
"error": err,
|
||||
}).Error("Failed to decode Bitcoin rate API response")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rate, nil
|
||||
}
|
||||
|
||||
func (svc *albyService) GetChannelPeerSuggestions(ctx context.Context) ([]ChannelPeerSuggestion, error) {
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("%s/internal/channel_suggestions", albyInternalAPIURL), nil)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Error creating request to channel_suggestions endpoint")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
setDefaultRequestHeaders(req)
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to fetch channel_suggestions endpoint")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to read response body")
|
||||
return nil, errors.New("failed to read response body")
|
||||
}
|
||||
|
||||
if res.StatusCode >= 300 {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"body": string(body),
|
||||
"status_code": res.StatusCode,
|
||||
}).Error("channel suggestions endpoint returned non-success code")
|
||||
return nil, fmt.Errorf("channel suggestions endpoint returned non-success code: %s", string(body))
|
||||
}
|
||||
|
||||
var suggestions []ChannelPeerSuggestion
|
||||
err = json.Unmarshal(body, &suggestions)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Errorf("Failed to decode API response")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger.Logger.WithFields(logrus.Fields{"channel_suggestions": suggestions}).Debug("Alby channel peer suggestions response")
|
||||
return suggestions, nil
|
||||
}
|
||||
|
||||
func (svc *albyService) GetInfo(ctx context.Context) (*AlbyInfo, error) {
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("%s/internal/info", albyInternalAPIURL), nil)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Error creating request to alby info endpoint")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
setDefaultRequestHeaders(req)
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to fetch /info")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
type albyInfoHub struct {
|
||||
LatestVersion string `json:"latest_version"`
|
||||
LatestReleaseNotes string `json:"latest_release_notes"`
|
||||
}
|
||||
|
||||
type albyInfoIncident struct {
|
||||
Name string `json:"name"`
|
||||
Started string `json:"started"`
|
||||
Status string `json:"status"`
|
||||
Impact string `json:"impact"`
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
type albyInfo struct {
|
||||
Hub albyInfoHub `json:"hub"`
|
||||
Status string `json:"status"`
|
||||
Healthy bool `json:"healthy"`
|
||||
AccountAvailable bool `json:"account_available"` // false if country is blocked (can still use Alby Hub without an Alby Account)
|
||||
Incidents []albyInfoIncident `json:"incidents"`
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to read response body")
|
||||
return nil, errors.New("failed to read response body")
|
||||
}
|
||||
|
||||
if res.StatusCode >= 300 {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"body": string(body),
|
||||
"status_code": res.StatusCode,
|
||||
}).Error("info endpoint returned non-success code")
|
||||
return nil, fmt.Errorf("info endpoint returned non-success code: %s", string(body))
|
||||
}
|
||||
|
||||
info := &albyInfo{}
|
||||
err = json.Unmarshal(body, info)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to decode API response")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
incidents := []AlbyInfoIncident{}
|
||||
for _, incident := range info.Incidents {
|
||||
incidents = append(incidents, AlbyInfoIncident{
|
||||
Name: incident.Name,
|
||||
Started: incident.Started,
|
||||
Status: incident.Status,
|
||||
Impact: incident.Impact,
|
||||
Url: incident.Url,
|
||||
})
|
||||
}
|
||||
|
||||
return &AlbyInfo{
|
||||
Hub: AlbyInfoHub{
|
||||
LatestVersion: info.Hub.LatestVersion,
|
||||
LatestReleaseNotes: info.Hub.LatestReleaseNotes,
|
||||
},
|
||||
Status: info.Status,
|
||||
Healthy: info.Healthy,
|
||||
AccountAvailable: info.AccountAvailable,
|
||||
Incidents: incidents,
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -7,11 +7,15 @@ import (
|
|||
"github.com/getAlby/hub/lnclient"
|
||||
)
|
||||
|
||||
type AlbyService interface {
|
||||
GetInfo(ctx context.Context) (*AlbyInfo, error)
|
||||
GetBitcoinRate(ctx context.Context) (*BitcoinRate, error)
|
||||
GetChannelPeerSuggestions(ctx context.Context) ([]ChannelPeerSuggestion, error)
|
||||
}
|
||||
|
||||
type AlbyOAuthService interface {
|
||||
events.EventSubscriber
|
||||
GetInfo(ctx context.Context) (*AlbyInfo, error)
|
||||
GetChannelPeerSuggestions(ctx context.Context) ([]ChannelPeerSuggestion, error)
|
||||
GetBitcoinRate(ctx context.Context) (*BitcoinRate, error)
|
||||
GetLSPChannelOffer(ctx context.Context) (*LSPChannelOffer, error)
|
||||
GetAuthUrl() string
|
||||
GetUserIdentifier() (string, error)
|
||||
GetLightningAddress() (string, error)
|
||||
|
|
@ -107,20 +111,35 @@ type AlbyBalance struct {
|
|||
}
|
||||
|
||||
type ChannelPeerSuggestion struct {
|
||||
Network string `json:"network"`
|
||||
PaymentMethod string `json:"paymentMethod"`
|
||||
Pubkey string `json:"pubkey"`
|
||||
Host string `json:"host"`
|
||||
MinimumChannelSize uint64 `json:"minimumChannelSize"`
|
||||
MaximumChannelSize uint64 `json:"maximumChannelSize"`
|
||||
Name string `json:"name"`
|
||||
Image string `json:"image"`
|
||||
BrokenLspUrl string `json:"lsp_url"`
|
||||
BrokenLspType string `json:"lsp_type"`
|
||||
LspUrl string `json:"lspUrl"`
|
||||
LspType string `json:"lspType"`
|
||||
Note string `json:"note"`
|
||||
PublicChannelsAllowed bool `json:"publicChannelsAllowed"`
|
||||
Network string `json:"network"`
|
||||
PaymentMethod string `json:"paymentMethod"`
|
||||
Pubkey string `json:"pubkey"`
|
||||
Host string `json:"host"`
|
||||
MinimumChannelSize uint64 `json:"minimumChannelSize"`
|
||||
MaximumChannelSize uint64 `json:"maximumChannelSize"`
|
||||
Name string `json:"name"`
|
||||
Image string `json:"image"`
|
||||
Url string `json:"url"`
|
||||
ContactUrl string `json:"contactUrl"`
|
||||
Type string `json:"type"`
|
||||
Terms string `json:"terms"`
|
||||
Description string `json:"description"`
|
||||
Note string `json:"note"`
|
||||
PublicChannelsAllowed bool `json:"publicChannelsAllowed"`
|
||||
FeeTotalSat1m *uint32 `json:"feeTotalSat1m"`
|
||||
FeeTotalSat2m *uint32 `json:"feeTotalSat2m"`
|
||||
FeeTotalSat3m *uint32 `json:"feeTotalSat3m"`
|
||||
}
|
||||
|
||||
type LSPChannelOffer struct {
|
||||
LspName string `json:"lspName"`
|
||||
LspContactUrl string `json:"lspContactUrl"`
|
||||
LspBalanceSat uint64 `json:"lspBalanceSat"`
|
||||
FeeTotalSat uint64 `json:"feeTotalSat"`
|
||||
FeeTotalUsd uint64 `json:"feeTotalUsd"` // in cents
|
||||
CurrentPaymentMethod string `json:"currentPaymentMethod"`
|
||||
Terms string `json:"terms"`
|
||||
LspDescription string `json:"lspDescription"`
|
||||
}
|
||||
|
||||
type BitcoinRate struct {
|
||||
|
|
|
|||
12
api/api.go
12
api/api.go
|
|
@ -44,12 +44,13 @@ type api struct {
|
|||
permissionsSvc permissions.PermissionsService
|
||||
keys keys.Keys
|
||||
albyOAuthSvc alby.AlbyOAuthService
|
||||
albySvc alby.AlbyService
|
||||
startupError error
|
||||
startupErrorTime time.Time
|
||||
eventPublisher events.EventPublisher
|
||||
}
|
||||
|
||||
func NewAPI(svc service.Service, gormDB *gorm.DB, config config.Config, keys keys.Keys, albyOAuthSvc alby.AlbyOAuthService, eventPublisher events.EventPublisher) *api {
|
||||
func NewAPI(svc service.Service, gormDB *gorm.DB, config config.Config, keys keys.Keys, albySvc alby.AlbyService, albyOAuthSvc alby.AlbyOAuthService, eventPublisher events.EventPublisher) *api {
|
||||
return &api{
|
||||
db: gormDB,
|
||||
appsSvc: apps.NewAppsService(gormDB, eventPublisher, keys, config),
|
||||
|
|
@ -57,6 +58,7 @@ func NewAPI(svc service.Service, gormDB *gorm.DB, config config.Config, keys key
|
|||
svc: svc,
|
||||
permissionsSvc: permissions.NewPermissionsService(gormDB, eventPublisher),
|
||||
keys: keys,
|
||||
albySvc: albySvc,
|
||||
albyOAuthSvc: albyOAuthSvc,
|
||||
eventPublisher: eventPublisher,
|
||||
}
|
||||
|
|
@ -585,7 +587,11 @@ func (api *api) ListChannels(ctx context.Context) ([]Channel, error) {
|
|||
}
|
||||
|
||||
func (api *api) GetChannelPeerSuggestions(ctx context.Context) ([]alby.ChannelPeerSuggestion, error) {
|
||||
return api.albyOAuthSvc.GetChannelPeerSuggestions(ctx)
|
||||
return api.albySvc.GetChannelPeerSuggestions(ctx)
|
||||
}
|
||||
|
||||
func (api *api) GetLSPChannelOffer(ctx context.Context) (*alby.LSPChannelOffer, error) {
|
||||
return api.albyOAuthSvc.GetLSPChannelOffer(ctx)
|
||||
}
|
||||
|
||||
func (api *api) ResetRouter(key string) error {
|
||||
|
|
@ -1423,7 +1429,7 @@ func (api *api) GetLogOutput(ctx context.Context, logType string, getLogRequest
|
|||
func (api *api) Health(ctx context.Context) (*HealthResponse, error) {
|
||||
var alarms []HealthAlarm
|
||||
|
||||
albyInfo, err := api.albyOAuthSvc.GetInfo(ctx)
|
||||
albyInfo, err := api.albySvc.GetInfo(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
13
api/lsp.go
13
api/lsp.go
|
|
@ -42,7 +42,7 @@ func (api *api) RequestLSPOrder(ctx context.Context, request *LSPOrderRequest) (
|
|||
return nil, fmt.Errorf("unsupported LSP type: %v", request.LSPType)
|
||||
}
|
||||
|
||||
logger.Logger.Infoln("Requesting LSP info")
|
||||
logger.Logger.Info("Requesting LSP info")
|
||||
lspInfo, err := api.getLSPS1LSPInfo(request.LSPUrl + "/get_info")
|
||||
|
||||
if err != nil {
|
||||
|
|
@ -50,7 +50,7 @@ func (api *api) RequestLSPOrder(ctx context.Context, request *LSPOrderRequest) (
|
|||
return nil, err
|
||||
}
|
||||
|
||||
logger.Logger.Infoln("Requesting own node info")
|
||||
logger.Logger.Info("Requesting own node info")
|
||||
|
||||
nodeInfo, err := api.svc.GetLNClient().GetInfo(ctx)
|
||||
if err != nil {
|
||||
|
|
@ -152,6 +152,15 @@ func (api *api) getLSPS1LSPInfo(url string) (*lspInfo, error) {
|
|||
return nil, errors.New("failed to read response body")
|
||||
}
|
||||
|
||||
if res.StatusCode >= 300 {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"url": url,
|
||||
"body": string(body),
|
||||
"statusCode": res.StatusCode,
|
||||
}).Error("get_info endpoint returned non-success code")
|
||||
return nil, fmt.Errorf("get info endpoint returned non-success code: %s", string(body))
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &lsps1LspInfo)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).WithFields(logrus.Fields{
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ type API interface {
|
|||
DeleteLightningAddress(ctx context.Context, appId uint) error
|
||||
ListChannels(ctx context.Context) ([]Channel, error)
|
||||
GetChannelPeerSuggestions(ctx context.Context) ([]alby.ChannelPeerSuggestion, error)
|
||||
GetLSPChannelOffer(ctx context.Context) (*alby.LSPChannelOffer, error)
|
||||
ResetRouter(key string) error
|
||||
ChangeUnlockPassword(changeUnlockPasswordRequest *ChangeUnlockPasswordRequest) error
|
||||
SetAutoUnlockPassword(unlockPassword string) error
|
||||
|
|
|
|||
|
|
@ -6,11 +6,13 @@ import { cn } from "src/lib/utils";
|
|||
type FormattedFiatAmountProps = {
|
||||
amount: number;
|
||||
className?: string;
|
||||
showApprox?: boolean;
|
||||
};
|
||||
|
||||
export default function FormattedFiatAmount({
|
||||
amount,
|
||||
className,
|
||||
showApprox,
|
||||
}: FormattedFiatAmountProps) {
|
||||
const { data: info } = useInfo();
|
||||
const { data: bitcoinRate, error: bitcoinRateError } = useBitcoinRate();
|
||||
|
|
@ -21,6 +23,7 @@ export default function FormattedFiatAmount({
|
|||
|
||||
return (
|
||||
<div className={cn("text-sm text-muted-foreground", className)}>
|
||||
{showApprox && bitcoinRate && "~"}
|
||||
{!bitcoinRate ? (
|
||||
<Skeleton className="w-20"> </Skeleton>
|
||||
) : (
|
||||
|
|
|
|||
83
frontend/src/components/channels/LSPTermsDialog.tsx
Normal file
83
frontend/src/components/channels/LSPTermsDialog.tsx
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import { InfoIcon } from "lucide-react";
|
||||
import ExternalLink from "src/components/ExternalLink";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "src/components/ui/alert-dialog";
|
||||
|
||||
type LSPTermsDialogProps = {
|
||||
name: string;
|
||||
description: string;
|
||||
contactUrl: string;
|
||||
terms: string | undefined;
|
||||
trigger: React.ReactNode;
|
||||
};
|
||||
export function LSPTermsDialog({
|
||||
name,
|
||||
description,
|
||||
contactUrl,
|
||||
terms,
|
||||
trigger,
|
||||
}: LSPTermsDialogProps) {
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<div className="cursor-pointer">{trigger}</div>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Channel Terms - {name}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
<div className="grid gap-4">
|
||||
<p>{description}</p>
|
||||
<p>
|
||||
Learn more about{" "}
|
||||
<ExternalLink to={contactUrl} className="underline">
|
||||
{name}
|
||||
</ExternalLink>
|
||||
</p>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
Duration: at least 3 months
|
||||
<ExternalLink to="https://guides.getalby.com/user-guide/alby-hub/faq/how-to-open-a-payment-channel">
|
||||
<InfoIcon className="size-4 text-muted-foreground" />
|
||||
</ExternalLink>
|
||||
</div>
|
||||
{terms && <p>{terms}</p>}
|
||||
|
||||
<p>
|
||||
The duration for which a Lightning Channel remains open is not
|
||||
determined or guaranteed by Alby; we will make reasonable
|
||||
efforts to share information provided by the relevant LSP, but
|
||||
actual availability depends on the Lightning Network and the
|
||||
LSP's operations. Channels may be closed at any time, including
|
||||
by force closure initiated by the network or counterparties.
|
||||
</p>
|
||||
|
||||
<p>The purchase of a payment channel is non-refundable.</p>
|
||||
|
||||
<p>
|
||||
To learn more about opening channels, see{" "}
|
||||
<ExternalLink
|
||||
className="underline"
|
||||
to="https://guides.getalby.com/user-guide/alby-hub/faq/how-to-open-a-payment-channel"
|
||||
>
|
||||
How to open a payment channel?
|
||||
</ExternalLink>
|
||||
</p>
|
||||
</div>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Close</AlertDialogCancel>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ export const localStorageKeys = {
|
|||
|
||||
export const ONCHAIN_DUST_SATS = 1000;
|
||||
export const ALBY_HIDE_HOSTED_BALANCE_BELOW = 100;
|
||||
export const ALBY_MIN_HOSTED_BALANCE_FOR_FIRST_CHANNEL = 30_000;
|
||||
export const ALBY_MIN_HOSTED_BALANCE_FOR_FIRST_CHANNEL = 10_000;
|
||||
|
||||
export const LIST_TRANSACTIONS_LIMIT = 20;
|
||||
export const LIST_APPS_LIMIT = 20;
|
||||
|
|
|
|||
8
frontend/src/hooks/useLSPChannelOffer.tsx
Normal file
8
frontend/src/hooks/useLSPChannelOffer.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import useSWR from "swr";
|
||||
|
||||
import { LSPChannelOffer } from "src/types";
|
||||
import { swrFetcher } from "src/utils/swr";
|
||||
|
||||
export function useLSPChannelOffer() {
|
||||
return useSWR<LSPChannelOffer>("/api/channel-offer", swrFetcher);
|
||||
}
|
||||
|
|
@ -112,7 +112,7 @@ export function ConnectAlbyAccount({ connectUrl }: ConnectAlbyAccountProps) {
|
|||
Maybe later
|
||||
</LinkButton>
|
||||
</div>
|
||||
<div className="text-muted-foreground flex flex-col items-center text-xs gap-2 mt-10">
|
||||
<div className="text-muted-foreground flex flex-col items-center text-xs gap-2 mt-5 -mb-10">
|
||||
<Badge title="Pro" variant="outline">
|
||||
<SparklesIcon className="size-4" />
|
||||
</Badge>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
PayInvoiceResponse,
|
||||
} from "src/types";
|
||||
|
||||
import { CopyIcon, InfoIcon, QrCodeIcon, RefreshCwIcon } from "lucide-react";
|
||||
import { CopyIcon, QrCodeIcon, RefreshCwIcon } from "lucide-react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import AppHeader from "src/components/AppHeader";
|
||||
|
|
@ -588,8 +588,16 @@ function PayLightningChannelOrder({ order }: { order: NewChannelOrder }) {
|
|||
body: JSON.stringify(newLSPOrderRequest),
|
||||
}
|
||||
);
|
||||
if (!response?.invoice) {
|
||||
throw new Error("No invoice in response");
|
||||
if (!response) {
|
||||
throw new Error("no LSP order response");
|
||||
}
|
||||
|
||||
if (!response.invoice) {
|
||||
// assume payment is handled by Alby Account
|
||||
// we will wait for a channel to be opened to us
|
||||
useChannelOrderStore.getState().updateOrder({
|
||||
status: "paid",
|
||||
});
|
||||
}
|
||||
setLspOrderResponse(response);
|
||||
} catch (error) {
|
||||
|
|
@ -623,9 +631,9 @@ function PayLightningChannelOrder({ order }: { order: NewChannelOrder }) {
|
|||
: "Please wait, loading..."
|
||||
}
|
||||
/>
|
||||
{!lspOrderResponse && <Loading />}
|
||||
{!lspOrderResponse?.invoice && <Loading />}
|
||||
|
||||
{lspOrderResponse && (
|
||||
{lspOrderResponse?.invoice && (
|
||||
<>
|
||||
<div className="max-w-md flex flex-col gap-5">
|
||||
<div className="border rounded-lg slashed-zero">
|
||||
|
|
@ -657,29 +665,6 @@ function PayLightningChannelOrder({ order }: { order: NewChannelOrder }) {
|
|||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{/* <TableRow>
|
||||
<TableCell className="font-medium p-3 flex flex-row gap-1.5 items-center">
|
||||
Fee
|
||||
</TableCell>
|
||||
<TableCell className="text-right p-3">
|
||||
{new Intl.NumberFormat().format(lspOrderResponse.fee)}{" "}
|
||||
sats
|
||||
</TableCell>
|
||||
</TableRow> */}
|
||||
{lspOrderResponse.incomingLiquidity > 0 && (
|
||||
<TableRow>
|
||||
<TableCell className="font-medium p-3 flex items-center gap-2">
|
||||
Duration
|
||||
<ExternalLink to="https://guides.getalby.com/user-guide/alby-hub/faq/how-to-open-a-payment-channel">
|
||||
<InfoIcon className="size-4 text-muted-foreground" />
|
||||
</ExternalLink>
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="p-3 text-right">
|
||||
at least 3 months
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
<TableRow>
|
||||
<TableCell className="font-medium p-3">
|
||||
Amount to pay
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ import {
|
|||
|
||||
import LightningNetworkDarkSVG from "public/images/illustrations/lightning-network-dark.svg";
|
||||
import LightningNetworkLightSVG from "public/images/illustrations/lightning-network-light.svg";
|
||||
import { LSPTermsDialog } from "src/components/channels/LSPTermsDialog";
|
||||
|
||||
function getPeerKey(peer: RecommendedChannelPeer) {
|
||||
return JSON.stringify(peer);
|
||||
|
|
@ -99,7 +100,7 @@ function NewChannelInternal({
|
|||
? [
|
||||
..._channelPeerSuggestions.filter(
|
||||
(peer) =>
|
||||
peer.paymentMethod === "lightning" && peer.lspType === "LSPS1"
|
||||
peer.paymentMethod === "lightning" && peer.type === "LSPS1"
|
||||
),
|
||||
]
|
||||
: undefined;
|
||||
|
|
@ -139,14 +140,28 @@ function NewChannelInternal({
|
|||
) {
|
||||
setOrder((current) => ({
|
||||
...current,
|
||||
lspType: selectedPeer.lspType,
|
||||
lspUrl: selectedPeer.lspUrl,
|
||||
lspType: selectedPeer.type,
|
||||
lspUrl: selectedPeer.url,
|
||||
...(!selectedPeer.publicChannelsAllowed && { isPublic: false }),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}, [order.paymentMethod, selectedPeer]);
|
||||
|
||||
// find the best channel partner
|
||||
const okPartners = channelPeerSuggestions?.filter(
|
||||
(partner) =>
|
||||
parseInt(order.amount || "0") >= partner.minimumChannelSize &&
|
||||
parseInt(order.amount || "0") <= partner.maximumChannelSize &&
|
||||
partner.network === network &&
|
||||
partner.paymentMethod === "lightning" &&
|
||||
partner.type === "LSPS1" &&
|
||||
partner.pubkey &&
|
||||
!channels.some((channel) => channel.remotePubkey === partner.pubkey)
|
||||
);
|
||||
|
||||
const bestPartner = okPartners?.[0];
|
||||
|
||||
function onSubmit(e: FormEvent) {
|
||||
e.preventDefault();
|
||||
try {
|
||||
|
|
@ -162,20 +177,7 @@ function NewChannelInternal({
|
|||
throw new Error("No amount set");
|
||||
}
|
||||
|
||||
// find the best channel partner
|
||||
const okPartners = channelPeerSuggestions.filter(
|
||||
(partner) =>
|
||||
amount >= partner.minimumChannelSize &&
|
||||
amount <= partner.maximumChannelSize &&
|
||||
partner.network === network &&
|
||||
partner.paymentMethod === "lightning" &&
|
||||
partner.lspType === "LSPS1" &&
|
||||
partner.pubkey &&
|
||||
!channels.some((channel) => channel.remotePubkey === partner.pubkey)
|
||||
);
|
||||
|
||||
const partner = okPartners[0];
|
||||
if (!partner) {
|
||||
if (!bestPartner) {
|
||||
toast.error("No channel partner found", {
|
||||
description:
|
||||
"No ideal channel partner found. Please choose from the advanced options to continue",
|
||||
|
|
@ -185,12 +187,12 @@ function NewChannelInternal({
|
|||
order.paymentMethod = "lightning";
|
||||
if (
|
||||
order.paymentMethod !== "lightning" ||
|
||||
partner.paymentMethod !== "lightning"
|
||||
bestPartner.paymentMethod !== "lightning"
|
||||
) {
|
||||
throw new Error("Unexpected order or partner payment method");
|
||||
}
|
||||
order.lspType = partner.lspType;
|
||||
order.lspUrl = partner.lspUrl;
|
||||
order.lspType = bestPartner.type;
|
||||
order.lspUrl = bestPartner.url;
|
||||
}
|
||||
|
||||
useChannelOrderStore.getState().setOrder(order as NewChannelOrder);
|
||||
|
|
@ -207,6 +209,19 @@ function NewChannelInternal({
|
|||
return <Loading />;
|
||||
}
|
||||
|
||||
const selectedPartner = showAdvanced ? selectedPeer : bestPartner;
|
||||
|
||||
const estimatedChannelPrice =
|
||||
selectedPartner?.paymentMethod === "lightning"
|
||||
? order.amount === "1000000"
|
||||
? selectedPartner["feeTotalSat1m"]
|
||||
: order.amount === "2000000"
|
||||
? selectedPartner["feeTotalSat2m"]
|
||||
: order.amount === "3000000"
|
||||
? selectedPartner["feeTotalSat3m"]
|
||||
: undefined
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppHeader
|
||||
|
|
@ -300,6 +315,30 @@ function NewChannelInternal({
|
|||
</div>
|
||||
))}
|
||||
</div>
|
||||
{estimatedChannelPrice && (
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{" "}
|
||||
Estimated channel price:{" "}
|
||||
<span className="font-semibold">
|
||||
{new Intl.NumberFormat().format(estimatedChannelPrice)} sats
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
{selectedPartner?.paymentMethod === "lightning" && (
|
||||
<div className="flex justify-between items-center">
|
||||
<p className="text-sm">
|
||||
You will receive a channel from{" "}
|
||||
<span className="font-medium">{selectedPartner.name}</span>.{" "}
|
||||
</p>
|
||||
<LSPTermsDialog
|
||||
contactUrl={selectedPartner.contactUrl}
|
||||
description={selectedPartner.description}
|
||||
name={selectedPartner.name}
|
||||
terms={selectedPartner.terms}
|
||||
trigger={<p className="text-xs underline">View Terms</p>}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{showAdvanced && (
|
||||
<>
|
||||
|
|
@ -346,9 +385,9 @@ function NewChannelInternal({
|
|||
Min.{" "}
|
||||
{new Intl.NumberFormat().format(
|
||||
peer.minimumChannelSize
|
||||
)}
|
||||
)}{" "}
|
||||
sats
|
||||
<span className="mr-10" />
|
||||
<span className="mr-5" />
|
||||
Max.{" "}
|
||||
{new Intl.NumberFormat().format(
|
||||
peer.maximumChannelSize
|
||||
|
|
@ -407,6 +446,7 @@ function NewChannelInternal({
|
|||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!showAdvanced && (
|
||||
<Button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ function NewChannelInternal({
|
|||
paymentMethod: "onchain",
|
||||
minimumChannelSize: 0,
|
||||
maximumChannelSize: 0,
|
||||
description: "",
|
||||
pubkey: "",
|
||||
host: "",
|
||||
image: "",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
import { ChevronDownIcon, InfoIcon } from "lucide-react";
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
CreditCardIcon,
|
||||
InfoIcon,
|
||||
WalletIcon,
|
||||
} from "lucide-react";
|
||||
import React from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
|
|
@ -21,10 +26,19 @@ import { Invoice } from "@getalby/lightning-tools";
|
|||
import { MempoolAlert } from "src/components/MempoolAlert";
|
||||
import { PayLightningInvoice } from "src/components/PayLightningInvoice";
|
||||
import { Table, TableBody, TableCell, TableRow } from "src/components/ui/table";
|
||||
import { ALBY_MIN_HOSTED_BALANCE_FOR_FIRST_CHANNEL } from "src/constants";
|
||||
|
||||
import LightningNetworkDarkSVG from "public/images/illustrations/lightning-network-dark.svg";
|
||||
import LightningNetworkLightSVG from "public/images/illustrations/lightning-network-light.svg";
|
||||
import { LSPTermsDialog } from "src/components/channels/LSPTermsDialog";
|
||||
import FormattedFiatAmount from "src/components/FormattedFiatAmount";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "src/components/ui/tooltip";
|
||||
import { useLSPChannelOffer } from "src/hooks/useLSPChannelOffer";
|
||||
import { cn } from "src/lib/utils";
|
||||
|
||||
export function FirstChannel() {
|
||||
const { data: info } = useInfo();
|
||||
|
|
@ -32,6 +46,7 @@ export function FirstChannel() {
|
|||
const [isLoading, setLoading] = React.useState(false);
|
||||
const [showAdvanced, setShowAdvanced] = React.useState(false);
|
||||
const [isPublic, setPublic] = React.useState(false);
|
||||
const { data: lspChannelOffer } = useLSPChannelOffer();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const [invoice, setInvoice] = React.useState<string>();
|
||||
|
|
@ -50,7 +65,7 @@ export function FirstChannel() {
|
|||
}
|
||||
}, [info, navigate]);
|
||||
|
||||
if (!info?.albyAccountConnected || !channels) {
|
||||
if (!info?.albyAccountConnected || !channels || !lspChannelOffer) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
|
|
@ -86,10 +101,6 @@ export function FirstChannel() {
|
|||
}
|
||||
}
|
||||
|
||||
const canPayForFirstChannel =
|
||||
albyBalance &&
|
||||
albyBalance.sats >= ALBY_MIN_HOSTED_BALANCE_FOR_FIRST_CHANNEL;
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppHeader
|
||||
|
|
@ -116,18 +127,6 @@ export function FirstChannel() {
|
|||
{new Intl.NumberFormat().format(channelSize)} sats
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="font-medium p-3 flex items-center gap-2">
|
||||
Duration
|
||||
<ExternalLink to="https://guides.getalby.com/user-guide/alby-hub/faq/how-to-open-a-payment-channel#which-lightning-service-provider-to-choose">
|
||||
<InfoIcon className="size-4 text-muted-foreground" />
|
||||
</ExternalLink>
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="p-3 text-right">
|
||||
at least 3 months
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{invoice && (
|
||||
<TableRow>
|
||||
<TableCell className="font-medium p-3">
|
||||
|
|
@ -173,51 +172,131 @@ export function FirstChannel() {
|
|||
src={LightningNetworkLightSVG}
|
||||
className="w-full dark:hidden"
|
||||
/>
|
||||
{canPayForFirstChannel ? (
|
||||
<>
|
||||
<p>
|
||||
You currently have{" "}
|
||||
<span className="font-medium text-foreground sensitive slashed-zero">
|
||||
{new Intl.NumberFormat().format(albyBalance?.sats)} Alby fee
|
||||
credits.
|
||||
</span>{" "}
|
||||
<Link
|
||||
to="https://guides.getalby.com/user-guide/alby-account/faq/what-are-fee-credits-in-my-alby-account"
|
||||
target="_blank"
|
||||
className="underline"
|
||||
>
|
||||
Learn more
|
||||
</Link>
|
||||
</p>
|
||||
<p>
|
||||
These fee credits will be applied to open your first Lightning
|
||||
channel.
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p>
|
||||
You're now going to open your first lightning channel and can
|
||||
begin using your Hub in the booming bitcoin economy!
|
||||
</p>
|
||||
<p>
|
||||
After paying a lightning invoice to cover on-chain fees,
|
||||
you'll immediately be able to receive and send bitcoin with
|
||||
your Hub.
|
||||
</p>
|
||||
<p className="text-muted-foreground">
|
||||
Alby Hub works with selected service providers (LSPs) which
|
||||
provide the best network connectivity and liquidity to receive
|
||||
payments.{" "}
|
||||
<ExternalLink
|
||||
className="underline"
|
||||
to="https://guides.getalby.com/user-guide/alby-hub/faq/how-to-open-a-payment-channel"
|
||||
>
|
||||
Learn more
|
||||
</ExternalLink>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<p>
|
||||
You're now going to open your first lightning channel and can
|
||||
begin using your Hub in the booming bitcoin economy!
|
||||
</p>
|
||||
<p className="text-muted-foreground">
|
||||
Alby Hub works with selected service providers (LSPs) which
|
||||
provide the best network connectivity and liquidity to receive
|
||||
payments.
|
||||
</p>
|
||||
<p>
|
||||
A payment is required to purchase a channel from{" "}
|
||||
<ExternalLink
|
||||
to={lspChannelOffer.lspContactUrl}
|
||||
className="underline"
|
||||
>
|
||||
{lspChannelOffer.lspName}
|
||||
</ExternalLink>
|
||||
. Once your channel is opened, you'll immediately be able to
|
||||
receive and send bitcoin with your Hub.
|
||||
</p>
|
||||
</>
|
||||
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className="font-medium p-3">
|
||||
Channel Cost
|
||||
</TableCell>
|
||||
<TableCell className="p-3 flex flex-col gap-2 items-end justify-center">
|
||||
<p>
|
||||
<span
|
||||
className={cn(
|
||||
lspChannelOffer.currentPaymentMethod === "included" &&
|
||||
"line-through"
|
||||
)}
|
||||
>
|
||||
{new Intl.NumberFormat(undefined, {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
}).format(lspChannelOffer.feeTotalUsd / 100)}
|
||||
</span>
|
||||
{lspChannelOffer.currentPaymentMethod === "included" && (
|
||||
<span> $0.00</span>
|
||||
)}
|
||||
</p>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="font-medium p-3 align-top">
|
||||
<div className="flex flex-1 items-center gap-1">
|
||||
Receiving Capacity{" "}
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="flex flex-row items-center">
|
||||
<InfoIcon className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-sm">
|
||||
You will be able to receive up to this amount of
|
||||
sats in this channel.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="p-3 flex flex-col gap-2 items-end justify-center align-top">
|
||||
<span>
|
||||
{new Intl.NumberFormat().format(
|
||||
lspChannelOffer.lspBalanceSat
|
||||
)}{" "}
|
||||
sats
|
||||
</span>
|
||||
<FormattedFiatAmount
|
||||
amount={lspChannelOffer.lspBalanceSat}
|
||||
className="text-xs"
|
||||
showApprox
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{lspChannelOffer.currentPaymentMethod !== "prepaid" &&
|
||||
lspChannelOffer.currentPaymentMethod !== "included" && (
|
||||
<TableRow>
|
||||
<TableCell className="font-medium p-3 flex items-center gap-2">
|
||||
Payment method
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="p-3 text-right">
|
||||
<ExternalLink to="https://getalby.com/payment_details">
|
||||
<div className="capitalize flex items-center justify-end gap-1 font-medium">
|
||||
{lspChannelOffer.currentPaymentMethod === "card" ? (
|
||||
<CreditCardIcon className="size-4" />
|
||||
) : (
|
||||
<WalletIcon className="size-4" />
|
||||
)}
|
||||
{lspChannelOffer.currentPaymentMethod.replace(
|
||||
"_",
|
||||
" "
|
||||
)}
|
||||
</div>
|
||||
</ExternalLink>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
<TableRow>
|
||||
<TableCell className="font-medium p-3 flex items-center gap-2">
|
||||
Terms
|
||||
{/* <ExternalLink to="https://guides.getalby.com/user-guide/alby-hub/faq/how-to-open-a-payment-channel">
|
||||
<InfoIcon className="size-4 text-muted-foreground" />
|
||||
</ExternalLink> */}
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="p-3 text-right">
|
||||
<LSPTermsDialog
|
||||
contactUrl={lspChannelOffer.lspContactUrl}
|
||||
description={lspChannelOffer.lspDescription}
|
||||
name={lspChannelOffer.lspName}
|
||||
terms={lspChannelOffer.terms}
|
||||
trigger=<span className="font-medium">View</span>
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
{showAdvanced && (
|
||||
<>
|
||||
<div className="mt-2 flex items-top space-x-2">
|
||||
|
|
@ -247,7 +326,7 @@ export function FirstChannel() {
|
|||
</>
|
||||
)}
|
||||
{!showAdvanced && (
|
||||
<div>
|
||||
<div className="flex items-center justify-center -mt-5">
|
||||
<Button
|
||||
type="button"
|
||||
variant="link"
|
||||
|
|
@ -255,12 +334,52 @@ export function FirstChannel() {
|
|||
onClick={() => setShowAdvanced((current) => !current)}
|
||||
>
|
||||
Advanced Options
|
||||
<ChevronDownIcon className="size-4 ml-1" />
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<LoadingButton loading={isLoading} onClick={openChannel}>
|
||||
Open Channel
|
||||
{lspChannelOffer.currentPaymentMethod === "fee_credits" && (
|
||||
<>
|
||||
<p className="text-sm">
|
||||
You currently have{" "}
|
||||
<span className="font-medium text-foreground sensitive slashed-zero">
|
||||
{new Intl.NumberFormat().format(albyBalance?.sats || 0)}{" "}
|
||||
</span>{" "}
|
||||
Alby fee credits which will be used to open your first
|
||||
Lightning channel.{" "}
|
||||
<ExternalLink
|
||||
to="https://guides.getalby.com/user-guide/alby-account/faq/what-are-fee-credits-in-my-alby-account"
|
||||
className="underline"
|
||||
>
|
||||
Learn more
|
||||
</ExternalLink>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{lspChannelOffer.currentPaymentMethod !== "prepaid" &&
|
||||
lspChannelOffer.currentPaymentMethod !== "fee_credits" &&
|
||||
lspChannelOffer.currentPaymentMethod !== "included" && (
|
||||
<p className="text-xs text-muted-foreground flex items-center justify-center -mb-2">
|
||||
The cost will be included in your next subscription payment
|
||||
</p>
|
||||
)}
|
||||
{lspChannelOffer.currentPaymentMethod === "included" && (
|
||||
<p className="text-xs text-muted-foreground flex items-center justify-center -mb-2">
|
||||
This channel comes free with your Alby Pro subscription
|
||||
</p>
|
||||
)}
|
||||
<LoadingButton
|
||||
loading={isLoading}
|
||||
onClick={openChannel}
|
||||
className="gap-0"
|
||||
>
|
||||
{lspChannelOffer.currentPaymentMethod === "prepaid" ? (
|
||||
<>Continue</>
|
||||
) : lspChannelOffer.currentPaymentMethod === "included" ? (
|
||||
<>Confirm</>
|
||||
) : (
|
||||
<>Confirm Payment</>
|
||||
)}
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -441,6 +441,22 @@ export type SetupNodeInfo = Partial<{
|
|||
|
||||
export type LSPType = "LSPS1";
|
||||
|
||||
export type LSPChannelOffer = {
|
||||
lspName: string;
|
||||
lspDescription: string;
|
||||
lspContactUrl: string;
|
||||
lspBalanceSat: number;
|
||||
feeTotalSat: number;
|
||||
feeTotalUsd: number;
|
||||
currentPaymentMethod:
|
||||
| "card"
|
||||
| "wallet"
|
||||
| "prepaid"
|
||||
| "fee_credits"
|
||||
| "included";
|
||||
terms: string;
|
||||
};
|
||||
|
||||
export type RecommendedChannelPeer = {
|
||||
network: Network;
|
||||
image: string;
|
||||
|
|
@ -449,6 +465,7 @@ export type RecommendedChannelPeer = {
|
|||
maximumChannelSize: number;
|
||||
note: string;
|
||||
publicChannelsAllowed: boolean;
|
||||
description: string;
|
||||
} & (
|
||||
| {
|
||||
paymentMethod: "onchain";
|
||||
|
|
@ -457,9 +474,14 @@ export type RecommendedChannelPeer = {
|
|||
}
|
||||
| {
|
||||
paymentMethod: "lightning";
|
||||
lspType: LSPType;
|
||||
lspUrl: string;
|
||||
type: LSPType;
|
||||
url: string;
|
||||
contactUrl: string;
|
||||
terms?: string;
|
||||
pubkey?: string;
|
||||
feeTotalSat1m?: number;
|
||||
feeTotalSat2m?: number;
|
||||
feeTotalSat3m?: number;
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -511,7 +533,7 @@ export type LSPOrderRequest = {
|
|||
};
|
||||
|
||||
export type LSPOrderResponse = {
|
||||
invoice: string;
|
||||
invoice?: string;
|
||||
fee: number;
|
||||
invoiceAmount: number;
|
||||
incomingLiquidity: number;
|
||||
|
|
|
|||
|
|
@ -13,13 +13,15 @@ import (
|
|||
)
|
||||
|
||||
type AlbyHttpService struct {
|
||||
albySvc alby.AlbyService
|
||||
albyOAuthSvc alby.AlbyOAuthService
|
||||
appConfig *config.AppConfig
|
||||
svc service.Service
|
||||
}
|
||||
|
||||
func NewAlbyHttpService(svc service.Service, albyOAuthSvc alby.AlbyOAuthService, appConfig *config.AppConfig) *AlbyHttpService {
|
||||
func NewAlbyHttpService(svc service.Service, albySvc alby.AlbyService, albyOAuthSvc alby.AlbyOAuthService, appConfig *config.AppConfig) *AlbyHttpService {
|
||||
return &AlbyHttpService{
|
||||
albySvc: albySvc,
|
||||
albyOAuthSvc: albyOAuthSvc,
|
||||
appConfig: appConfig,
|
||||
svc: svc,
|
||||
|
|
@ -74,7 +76,7 @@ func (albyHttpSvc *AlbyHttpService) unlinkHandler(c echo.Context) error {
|
|||
}
|
||||
|
||||
func (albyHttpSvc *AlbyHttpService) albyInfoHandler(c echo.Context) error {
|
||||
info, err := albyHttpSvc.albyOAuthSvc.GetInfo(c.Request().Context())
|
||||
info, err := albyHttpSvc.albySvc.GetInfo(c.Request().Context())
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to request alby info endpoint")
|
||||
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
||||
|
|
@ -86,7 +88,7 @@ func (albyHttpSvc *AlbyHttpService) albyInfoHandler(c echo.Context) error {
|
|||
}
|
||||
|
||||
func (albyHttpSvc *AlbyHttpService) albyBitcoinRateHandler(c echo.Context) error {
|
||||
rate, err := albyHttpSvc.albyOAuthSvc.GetBitcoinRate(c.Request().Context())
|
||||
rate, err := albyHttpSvc.albySvc.GetBitcoinRate(c.Request().Context())
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to get Bitcoin rate")
|
||||
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
||||
|
|
|
|||
|
|
@ -49,8 +49,8 @@ type HttpService struct {
|
|||
|
||||
func NewHttpService(svc service.Service, eventPublisher events.EventPublisher) *HttpService {
|
||||
return &HttpService{
|
||||
api: api.NewAPI(svc, svc.GetDB(), svc.GetConfig(), svc.GetKeys(), svc.GetAlbyOAuthSvc(), svc.GetEventPublisher()),
|
||||
albyHttpSvc: NewAlbyHttpService(svc, svc.GetAlbyOAuthSvc(), svc.GetConfig().GetEnv()),
|
||||
api: api.NewAPI(svc, svc.GetDB(), svc.GetConfig(), svc.GetKeys(), svc.GetAlbySvc(), svc.GetAlbyOAuthSvc(), svc.GetEventPublisher()),
|
||||
albyHttpSvc: NewAlbyHttpService(svc, svc.GetAlbySvc(), svc.GetAlbyOAuthSvc(), svc.GetConfig().GetEnv()),
|
||||
cfg: svc.GetConfig(),
|
||||
eventPublisher: eventPublisher,
|
||||
db: svc.GetDB(),
|
||||
|
|
@ -137,6 +137,7 @@ func (httpSvc *HttpService) RegisterSharedRoutes(e *echo.Echo) {
|
|||
restrictedApiGroup.POST("/channels", httpSvc.openChannelHandler)
|
||||
restrictedApiGroup.POST("/channels/rebalance", httpSvc.rebalanceChannelHandler)
|
||||
restrictedApiGroup.GET("/channels/suggestions", httpSvc.channelPeerSuggestionsHandler)
|
||||
restrictedApiGroup.GET("/channel-offer", httpSvc.channelOfferHandler)
|
||||
restrictedApiGroup.POST("/lsp-orders", httpSvc.newInstantChannelInvoiceHandler)
|
||||
restrictedApiGroup.GET("/node/connection-info", httpSvc.nodeConnectionInfoHandler)
|
||||
restrictedApiGroup.GET("/node/status", httpSvc.nodeStatusHandler)
|
||||
|
|
@ -437,6 +438,20 @@ func (httpSvc *HttpService) channelPeerSuggestionsHandler(c echo.Context) error
|
|||
return c.JSON(http.StatusOK, suggestions)
|
||||
}
|
||||
|
||||
func (httpSvc *HttpService) channelOfferHandler(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
suggestions, err := httpSvc.api.GetLSPChannelOffer(ctx)
|
||||
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, suggestions)
|
||||
}
|
||||
|
||||
func (httpSvc *HttpService) resetRouterHandler(c echo.Context) error {
|
||||
var resetRouterRequest api.ResetRouterRequest
|
||||
if err := c.Bind(&resetRouterRequest); err != nil {
|
||||
|
|
|
|||
|
|
@ -305,12 +305,18 @@ func NewLDKService(ctx context.Context, cfg config.Config, eventPublisher events
|
|||
// TODO: Remove once LDK can correctly do gossip with CLN and Eclair nodes
|
||||
// see https://github.com/lightningdevkit/rust-lightning/issues/3075
|
||||
peers := []string{
|
||||
"031b301307574bbe9b9ac7b79cbe1700e31e544513eae0b5d7497483083f99e581@45.79.192.236:9735", // Olympus
|
||||
"0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1@192.243.215.102:9735", // LQwD
|
||||
"035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226@170.75.163.209:9735", // WoS
|
||||
"02fcc5bfc48e83f06c04483a2985e1c390cb0f35058baa875ad2053858b8e80dbd@35.239.148.251:9735", // Blink
|
||||
// "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226@170.75.163.209:9735", // WoS
|
||||
// "02fcc5bfc48e83f06c04483a2985e1c390cb0f35058baa875ad2053858b8e80dbd@35.239.148.251:9735", // Blink
|
||||
// "027100442c3b79f606f80f322d98d499eefcb060599efc5d4ecb00209c2cb54190@3.230.33.224:9735", // c=
|
||||
"038a9e56512ec98da2b5789761f7af8f280baf98a09282360cd6ff1381b5e889bf@64.23.162.51:9735", // Megalith LSP
|
||||
|
||||
// Connect to our LSPs for both:
|
||||
// - Gossip data
|
||||
// - Ability for auto / free channels for users with eligible Alby subscriptions
|
||||
"0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1@192.243.215.102:9735", // LQwD
|
||||
"031b301307574bbe9b9ac7b79cbe1700e31e544513eae0b5d7497483083f99e581@45.79.192.236:9735", // Olympus
|
||||
"038a9e56512ec98da2b5789761f7af8f280baf98a09282360cd6ff1381b5e889bf@64.23.162.51:9735", // Megalith LSP
|
||||
"02b4552a7a85274e4da01a7c71ca57407181752e8568b31d51f13c111a2941dce3@159.223.176.115:48049", // LNServer_Wave
|
||||
"038ba8f67ba8ff5c48764cdd3251c33598d55b203546d08a8f0ec9dcd9f27e3637@52.24.240.84:9735", // flashsats
|
||||
}
|
||||
logger.Logger.Info("Connecting to some peers to retrieve P2P gossip data")
|
||||
for _, peer := range peers {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ type Service interface {
|
|||
Shutdown()
|
||||
|
||||
// TODO: remove getters (currently used by http / wails services)
|
||||
GetAlbySvc() alby.AlbyService
|
||||
GetAlbyOAuthSvc() alby.AlbyOAuthService
|
||||
GetEventPublisher() events.EventPublisher
|
||||
GetLNClient() lnclient.LNClient
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ type service struct {
|
|||
lnClient lnclient.LNClient
|
||||
transactionsService transactions.TransactionsService
|
||||
swapsService swaps.SwapsService
|
||||
albySvc alby.AlbyService
|
||||
albyOAuthSvc alby.AlbyOAuthService
|
||||
eventPublisher events.EventPublisher
|
||||
ctx context.Context
|
||||
|
|
@ -117,6 +118,7 @@ func NewService(ctx context.Context) (*service, error) {
|
|||
|
||||
keys := keys.NewKeys()
|
||||
|
||||
albySvc := alby.NewAlbyService(cfg)
|
||||
albyOAuthSvc := alby.NewAlbyOAuthService(gormDB, cfg, keys, eventPublisher)
|
||||
|
||||
transactionsSvc := transactions.NewTransactionsService(gormDB, eventPublisher)
|
||||
|
|
@ -127,6 +129,7 @@ func NewService(ctx context.Context) (*service, error) {
|
|||
ctx: ctx,
|
||||
wg: &wg,
|
||||
eventPublisher: eventPublisher,
|
||||
albySvc: albySvc,
|
||||
albyOAuthSvc: albyOAuthSvc,
|
||||
nip47Service: nip47.NewNip47Service(gormDB, cfg, keys, eventPublisher, albyOAuthSvc),
|
||||
transactionsService: transactionsSvc,
|
||||
|
|
@ -247,6 +250,10 @@ func (svc *service) GetConfig() config.Config {
|
|||
return svc.cfg
|
||||
}
|
||||
|
||||
func (svc *service) GetAlbySvc() alby.AlbyService {
|
||||
return svc.albySvc
|
||||
}
|
||||
|
||||
func (svc *service) GetAlbyOAuthSvc() alby.AlbyOAuthService {
|
||||
return svc.albyOAuthSvc
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,6 +89,52 @@ func (_c *MockService_GetAlbyOAuthSvc_Call) RunAndReturn(run func() alby.AlbyOAu
|
|||
return _c
|
||||
}
|
||||
|
||||
// GetAlbySvc provides a mock function for the type MockService
|
||||
func (_mock *MockService) GetAlbySvc() alby.AlbyService {
|
||||
ret := _mock.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetAlbySvc")
|
||||
}
|
||||
|
||||
var r0 alby.AlbyService
|
||||
if returnFunc, ok := ret.Get(0).(func() alby.AlbyService); ok {
|
||||
r0 = returnFunc()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(alby.AlbyService)
|
||||
}
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockService_GetAlbySvc_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAlbySvc'
|
||||
type MockService_GetAlbySvc_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetAlbySvc is a helper method to define mock.On call
|
||||
func (_e *MockService_Expecter) GetAlbySvc() *MockService_GetAlbySvc_Call {
|
||||
return &MockService_GetAlbySvc_Call{Call: _e.mock.On("GetAlbySvc")}
|
||||
}
|
||||
|
||||
func (_c *MockService_GetAlbySvc_Call) Run(run func()) *MockService_GetAlbySvc_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_GetAlbySvc_Call) Return(albyService alby.AlbyService) *MockService_GetAlbySvc_Call {
|
||||
_c.Call.Return(albyService)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_GetAlbySvc_Call) RunAndReturn(run func() alby.AlbyService) *MockService_GetAlbySvc_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetConfig provides a mock function for the type MockService
|
||||
func (_mock *MockService) GetConfig() config.Config {
|
||||
ret := _mock.Called()
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ type WailsApp struct {
|
|||
func NewApp(svc service.Service) *WailsApp {
|
||||
return &WailsApp{
|
||||
svc: svc,
|
||||
api: api.NewAPI(svc, svc.GetDB(), svc.GetConfig(), svc.GetKeys(), svc.GetAlbyOAuthSvc(), svc.GetEventPublisher()),
|
||||
api: api.NewAPI(svc, svc.GetDB(), svc.GetConfig(), svc.GetKeys(), svc.GetAlbySvc(), svc.GetAlbyOAuthSvc(), svc.GetEventPublisher()),
|
||||
db: svc.GetDB(),
|
||||
appsSvc: apps.NewAppsService(svc.GetDB(), svc.GetEventPublisher(), svc.GetKeys(), svc.GetConfig()),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -364,7 +364,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string
|
|||
}
|
||||
return WailsRequestRouterResponse{Body: nil, Error: ""}
|
||||
case "/api/alby/info":
|
||||
info, err := app.svc.GetAlbyOAuthSvc().GetInfo(ctx)
|
||||
info, err := app.svc.GetAlbySvc().GetInfo(ctx)
|
||||
if err != nil {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"route": route,
|
||||
|
|
@ -421,7 +421,7 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string
|
|||
}
|
||||
return WailsRequestRouterResponse{Body: nil, Error: ""}
|
||||
case "/api/alby/rates":
|
||||
rate, err := app.svc.GetAlbyOAuthSvc().GetBitcoinRate(ctx)
|
||||
rate, err := app.svc.GetAlbySvc().GetBitcoinRate(ctx)
|
||||
if err != nil {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"route": route,
|
||||
|
|
@ -502,6 +502,13 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string
|
|||
}
|
||||
return WailsRequestRouterResponse{Body: openChannelResponse, Error: ""}
|
||||
}
|
||||
case "/api/channel-offer":
|
||||
offer, err := app.api.GetLSPChannelOffer(ctx)
|
||||
if err != nil {
|
||||
return WailsRequestRouterResponse{Body: nil, Error: err.Error()}
|
||||
}
|
||||
res := WailsRequestRouterResponse{Body: offer, Error: ""}
|
||||
return res
|
||||
case "/api/channels/suggestions":
|
||||
suggestions, err := app.api.GetChannelPeerSuggestions(ctx)
|
||||
if err != nil {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue