diff --git a/alby/alby_oauth_service.go b/alby/alby_oauth_service.go
index fc74191a..994e256d 100644
--- a/alby/alby_oauth_service.go
+++ b/alby/alby_oauth_service.go
@@ -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,
}
diff --git a/alby/alby_service.go b/alby/alby_service.go
new file mode 100644
index 00000000..5b1cce60
--- /dev/null
+++ b/alby/alby_service.go
@@ -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
+}
diff --git a/alby/models.go b/alby/models.go
index 52c7cb18..3e18ca9c 100644
--- a/alby/models.go
+++ b/alby/models.go
@@ -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 {
diff --git a/api/api.go b/api/api.go
index 29553dbe..d919e967 100644
--- a/api/api.go
+++ b/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
}
diff --git a/api/lsp.go b/api/lsp.go
index 3e49fe0a..c3990f5c 100644
--- a/api/lsp.go
+++ b/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{
diff --git a/api/models.go b/api/models.go
index 921fa2ca..569a76a2 100644
--- a/api/models.go
+++ b/api/models.go
@@ -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
diff --git a/frontend/src/components/FormattedFiatAmount.tsx b/frontend/src/components/FormattedFiatAmount.tsx
index fd677768..6fdd4ecf 100644
--- a/frontend/src/components/FormattedFiatAmount.tsx
+++ b/frontend/src/components/FormattedFiatAmount.tsx
@@ -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 (
+ {showApprox && bitcoinRate && "~"}
{!bitcoinRate ? (
) : (
diff --git a/frontend/src/components/channels/LSPTermsDialog.tsx b/frontend/src/components/channels/LSPTermsDialog.tsx
new file mode 100644
index 00000000..bf93b2ba
--- /dev/null
+++ b/frontend/src/components/channels/LSPTermsDialog.tsx
@@ -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 (
+
+
+ {trigger}
+
+
+
+ Channel Terms - {name}
+
+
+
{description}
+
+ Learn more about{" "}
+
+ {name}
+
+
+
+
+ Duration: at least 3 months
+
+
+
+
+ {terms &&
{terms}
}
+
+
+ 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.
+
+
+
The purchase of a payment channel is non-refundable.
+
+
+ To learn more about opening channels, see{" "}
+
+ How to open a payment channel?
+
+
+
+
+
+
+ Close
+
+
+
+ );
+}
diff --git a/frontend/src/constants.ts b/frontend/src/constants.ts
index 7c52db8f..b6375667 100644
--- a/frontend/src/constants.ts
+++ b/frontend/src/constants.ts
@@ -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;
diff --git a/frontend/src/hooks/useLSPChannelOffer.tsx b/frontend/src/hooks/useLSPChannelOffer.tsx
new file mode 100644
index 00000000..143a5751
--- /dev/null
+++ b/frontend/src/hooks/useLSPChannelOffer.tsx
@@ -0,0 +1,8 @@
+import useSWR from "swr";
+
+import { LSPChannelOffer } from "src/types";
+import { swrFetcher } from "src/utils/swr";
+
+export function useLSPChannelOffer() {
+ return useSWR
("/api/channel-offer", swrFetcher);
+}
diff --git a/frontend/src/screens/ConnectAlbyAccount.tsx b/frontend/src/screens/ConnectAlbyAccount.tsx
index d47de233..b8ad8b7a 100644
--- a/frontend/src/screens/ConnectAlbyAccount.tsx
+++ b/frontend/src/screens/ConnectAlbyAccount.tsx
@@ -112,7 +112,7 @@ export function ConnectAlbyAccount({ connectUrl }: ConnectAlbyAccountProps) {
Maybe later
-
+
diff --git a/frontend/src/screens/channels/CurrentChannelOrder.tsx b/frontend/src/screens/channels/CurrentChannelOrder.tsx
index 0ee66764..a9b3a61b 100644
--- a/frontend/src/screens/channels/CurrentChannelOrder.tsx
+++ b/frontend/src/screens/channels/CurrentChannelOrder.tsx
@@ -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 &&
}
+ {!lspOrderResponse?.invoice &&
}
- {lspOrderResponse && (
+ {lspOrderResponse?.invoice && (
<>
@@ -657,29 +665,6 @@ function PayLightningChannelOrder({ order }: { order: NewChannelOrder }) {
)}
- {/*
-
- Fee
-
-
- {new Intl.NumberFormat().format(lspOrderResponse.fee)}{" "}
- sats
-
- */}
- {lspOrderResponse.incomingLiquidity > 0 && (
-
-
- Duration
-
-
-
-
-
-
- at least 3 months
-
-
- )}
Amount to pay
diff --git a/frontend/src/screens/channels/IncreaseIncomingCapacity.tsx b/frontend/src/screens/channels/IncreaseIncomingCapacity.tsx
index d5e33b3f..31690998 100644
--- a/frontend/src/screens/channels/IncreaseIncomingCapacity.tsx
+++ b/frontend/src/screens/channels/IncreaseIncomingCapacity.tsx
@@ -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 ;
}
+ 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 (
<>
))}
+ {estimatedChannelPrice && (
+
+ {" "}
+ Estimated channel price:{" "}
+
+ {new Intl.NumberFormat().format(estimatedChannelPrice)} sats
+
+
+ )}
+ {selectedPartner?.paymentMethod === "lightning" && (
+
+
+ You will receive a channel from{" "}
+ {selectedPartner.name} .{" "}
+
+
View Terms}
+ />
+
+ )}
{showAdvanced && (
<>
@@ -346,9 +385,9 @@ function NewChannelInternal({
Min.{" "}
{new Intl.NumberFormat().format(
peer.minimumChannelSize
- )}
+ )}{" "}
sats
-
+
Max.{" "}
{new Intl.NumberFormat().format(
peer.maximumChannelSize
@@ -407,6 +446,7 @@ function NewChannelInternal({
>
)}
+
{!showAdvanced && (
();
@@ -50,7 +65,7 @@ export function FirstChannel() {
}
}, [info, navigate]);
- if (!info?.albyAccountConnected || !channels) {
+ if (!info?.albyAccountConnected || !channels || !lspChannelOffer) {
return ;
}
@@ -86,10 +101,6 @@ export function FirstChannel() {
}
}
- const canPayForFirstChannel =
- albyBalance &&
- albyBalance.sats >= ALBY_MIN_HOSTED_BALANCE_FOR_FIRST_CHANNEL;
-
return (
<>
-
-
- Duration
-
-
-
-
-
-
- at least 3 months
-
-
{invoice && (
@@ -173,51 +172,131 @@ export function FirstChannel() {
src={LightningNetworkLightSVG}
className="w-full dark:hidden"
/>
- {canPayForFirstChannel ? (
- <>
-
- You currently have{" "}
-
- {new Intl.NumberFormat().format(albyBalance?.sats)} Alby fee
- credits.
- {" "}
-
- Learn more
-
-
-
- These fee credits will be applied to open your first Lightning
- channel.
-
- >
- ) : (
- <>
-
- You're now going to open your first lightning channel and can
- begin using your Hub in the booming bitcoin economy!
-
-
- After paying a lightning invoice to cover on-chain fees,
- you'll immediately be able to receive and send bitcoin with
- your Hub.
-
-
- Alby Hub works with selected service providers (LSPs) which
- provide the best network connectivity and liquidity to receive
- payments.{" "}
-
- Learn more
-
-
- >
- )}
+ <>
+
+ You're now going to open your first lightning channel and can
+ begin using your Hub in the booming bitcoin economy!
+
+
+ Alby Hub works with selected service providers (LSPs) which
+ provide the best network connectivity and liquidity to receive
+ payments.
+
+
+ A payment is required to purchase a channel from{" "}
+
+ {lspChannelOffer.lspName}
+
+ . Once your channel is opened, you'll immediately be able to
+ receive and send bitcoin with your Hub.
+
+ >
+
+
+
+
+
+ Channel Cost
+
+
+
+
+ {new Intl.NumberFormat(undefined, {
+ style: "currency",
+ currency: "USD",
+ }).format(lspChannelOffer.feeTotalUsd / 100)}
+
+ {lspChannelOffer.currentPaymentMethod === "included" && (
+ $0.00
+ )}
+
+
+
+
+
+
+ Receiving Capacity{" "}
+
+
+
+
+
+
+
+
+ You will be able to receive up to this amount of
+ sats in this channel.
+
+
+
+
+
+
+
+ {new Intl.NumberFormat().format(
+ lspChannelOffer.lspBalanceSat
+ )}{" "}
+ sats
+
+
+
+
+ {lspChannelOffer.currentPaymentMethod !== "prepaid" &&
+ lspChannelOffer.currentPaymentMethod !== "included" && (
+
+
+ Payment method
+
+
+
+
+
+ {lspChannelOffer.currentPaymentMethod === "card" ? (
+
+ ) : (
+
+ )}
+ {lspChannelOffer.currentPaymentMethod.replace(
+ "_",
+ " "
+ )}
+
+
+
+
+ )}
+
+
+ Terms
+ {/*
+
+ */}
+
+
+
+ View
+ />
+
+
+
+
{showAdvanced && (
<>
@@ -247,7 +326,7 @@ export function FirstChannel() {
>
)}
{!showAdvanced && (
-
+
setShowAdvanced((current) => !current)}
>
Advanced Options
-
+
)}
-
- Open Channel
+ {lspChannelOffer.currentPaymentMethod === "fee_credits" && (
+ <>
+
+ You currently have{" "}
+
+ {new Intl.NumberFormat().format(albyBalance?.sats || 0)}{" "}
+ {" "}
+ Alby fee credits which will be used to open your first
+ Lightning channel.{" "}
+
+ Learn more
+
+
+ >
+ )}
+ {lspChannelOffer.currentPaymentMethod !== "prepaid" &&
+ lspChannelOffer.currentPaymentMethod !== "fee_credits" &&
+ lspChannelOffer.currentPaymentMethod !== "included" && (
+
+ The cost will be included in your next subscription payment
+
+ )}
+ {lspChannelOffer.currentPaymentMethod === "included" && (
+
+ This channel comes free with your Alby Pro subscription
+
+ )}
+
+ {lspChannelOffer.currentPaymentMethod === "prepaid" ? (
+ <>Continue>
+ ) : lspChannelOffer.currentPaymentMethod === "included" ? (
+ <>Confirm>
+ ) : (
+ <>Confirm Payment>
+ )}
>
diff --git a/frontend/src/types.ts b/frontend/src/types.ts
index dcea21ee..a60edcfe 100644
--- a/frontend/src/types.ts
+++ b/frontend/src/types.ts
@@ -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;
diff --git a/http/alby_http_service.go b/http/alby_http_service.go
index 584337aa..d068f0c5 100644
--- a/http/alby_http_service.go
+++ b/http/alby_http_service.go
@@ -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{
diff --git a/http/http_service.go b/http/http_service.go
index 7f382fe3..fb95dd35 100644
--- a/http/http_service.go
+++ b/http/http_service.go
@@ -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 {
diff --git a/lnclient/ldk/ldk.go b/lnclient/ldk/ldk.go
index 93454879..15aef126 100644
--- a/lnclient/ldk/ldk.go
+++ b/lnclient/ldk/ldk.go
@@ -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 {
diff --git a/service/models.go b/service/models.go
index 4033d9a8..32afd938 100644
--- a/service/models.go
+++ b/service/models.go
@@ -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
diff --git a/service/service.go b/service/service.go
index ffce2138..636cbe5e 100644
--- a/service/service.go
+++ b/service/service.go
@@ -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
}
diff --git a/tests/mocks/Service.go b/tests/mocks/Service.go
index f6b93fc0..4805ea92 100644
--- a/tests/mocks/Service.go
+++ b/tests/mocks/Service.go
@@ -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()
diff --git a/wails/wails_app.go b/wails/wails_app.go
index cb13f336..cc41605e 100644
--- a/wails/wails_app.go
+++ b/wails/wails_app.go
@@ -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()),
}
diff --git a/wails/wails_handlers.go b/wails/wails_handlers.go
index 8f1afb70..86c128ea 100644
--- a/wails/wails_handlers.go
+++ b/wails/wails_handlers.go
@@ -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 {