From 13c24c7a8760be35b18a9279a39743ed5382dd7c Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Wed, 27 Aug 2025 23:34:15 +0700 Subject: [PATCH] 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 --- alby/alby_oauth_service.go | 185 ++----------- alby/alby_service.go | 210 +++++++++++++++ alby/models.go | 53 ++-- api/api.go | 12 +- api/lsp.go | 13 +- api/models.go | 1 + .../src/components/FormattedFiatAmount.tsx | 3 + .../components/channels/LSPTermsDialog.tsx | 83 ++++++ frontend/src/constants.ts | 2 +- frontend/src/hooks/useLSPChannelOffer.tsx | 8 + frontend/src/screens/ConnectAlbyAccount.tsx | 2 +- .../screens/channels/CurrentChannelOrder.tsx | 41 +-- .../channels/IncreaseIncomingCapacity.tsx | 84 ++++-- .../channels/IncreaseOutgoingCapacity.tsx | 1 + .../screens/channels/first/FirstChannel.tsx | 255 +++++++++++++----- frontend/src/types.ts | 28 +- http/alby_http_service.go | 8 +- http/http_service.go | 19 +- lnclient/ldk/ldk.go | 16 +- service/models.go | 1 + service/service.go | 7 + tests/mocks/Service.go | 46 ++++ wails/wails_app.go | 2 +- wails/wails_handlers.go | 11 +- 24 files changed, 774 insertions(+), 317 deletions(-) create mode 100644 alby/alby_service.go create mode 100644 frontend/src/components/channels/LSPTermsDialog.tsx create mode 100644 frontend/src/hooks/useLSPChannelOffer.tsx 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 && (
)} - - 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 {