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:
Roland 2025-08-27 23:34:15 +07:00 committed by GitHub
parent 49c1500011
commit 13c24c7a87
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 774 additions and 317 deletions

View file

@ -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
View 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
}

View file

@ -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 {

View file

@ -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
}

View file

@ -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{

View file

@ -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

View file

@ -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">&nbsp;</Skeleton>
) : (

View 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>
);
}

View file

@ -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;

View 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);
}

View file

@ -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>

View file

@ -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

View file

@ -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"

View file

@ -95,6 +95,7 @@ function NewChannelInternal({
paymentMethod: "onchain",
minimumChannelSize: 0,
maximumChannelSize: 0,
description: "",
pubkey: "",
host: "",
image: "",

View file

@ -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>
</>

View file

@ -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;

View file

@ -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{

View file

@ -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 {

View file

@ -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 {

View file

@ -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

View file

@ -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
}

View file

@ -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()

View file

@ -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()),
}

View file

@ -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 {