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 (
{description}
+
+ Learn more about{" "}
+
{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{" "}
+
+ You will receive a channel from{" "} + {selectedPartner.name}.{" "} +
+