mirror of
https://github.com/getAlby/hub.git
synced 2026-08-13 12:33:39 +02:00
feat: new first channel flow (#324)
* feat: new first channel flow * chore: hide public channel checkbox behind advanced options * chore: always use new channel flow * chore: add extra links to other ways to open channels * fix: check if payment exists on order response * chore: remove request to pay channel with alby shared node funds, add error handling * feat: use auto channel flow for first channel * chore: better other options on channel purchase pages * chore: add user agent to alby api requests * chore: remove unused lsp types * chore: remove unused constant * fix: hide sidebar hint on first channel flow * fix: sync wallet on opening page * fix: do not shown 0 confirmations on opening first channel page * fix: make sure confetti only shows once * fix: only show transferred alby funds toast once * feat: add minimum threshold, add illustration * fix: add constant * fix: spacing, illustrations, copy * fix: copy * fix: copy * fix: exchange light / dark images * chore: update first channel opened page CTA to go to wallet --------- Co-authored-by: René Aaron <rene@twentyuno.net>
This commit is contained in:
parent
60d1467f82
commit
b86c39d542
25 changed files with 931 additions and 770 deletions
|
|
@ -185,11 +185,9 @@ Follow the steps to integrate Mutinynet with your NWC Next setup:
|
|||
|
||||
2. Proceed as described in the [Development](https://github.com/getAlby/hub#Development) section to run the frontend and backend
|
||||
|
||||
3. During onboarding, after setting your password and authorizing via Alby OAuth, you'll be directed to `/onboarding/lightning/migrate-alby`. Click "Skip For Now" to access your wallet interface
|
||||
3. Navigate to `channels/outgoing`, copy your On-Chain Address, then visit the [Mutinynet Faucet](https://faucet.mutinynet.com/) to deposit sats. Ensure the transaction confirms on [mempool.space](https://mutinynet.com/)
|
||||
|
||||
4. Navigate to `channels/onchain/deposit-bitcoin`, copy your On-Chain Address, then visit the [Mutinynet Faucet](https://faucet.mutinynet.com/) to deposit sats. Ensure the transaction confirms on [mempool.space](https://mutinynet.com/)
|
||||
|
||||
5. Your On-chain balance will update under `/channels`
|
||||
4. Your On-chain balance will update under `/channels`
|
||||
|
||||
### Opening a channel from Mutinynet
|
||||
|
||||
|
|
|
|||
|
|
@ -6,12 +6,16 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
decodepay "github.com/nbd-wtf/ln-decodepay"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/oauth2"
|
||||
"gorm.io/gorm"
|
||||
|
|
@ -25,6 +29,8 @@ import (
|
|||
"github.com/getAlby/hub/nip47/permissions"
|
||||
"github.com/getAlby/hub/service/keys"
|
||||
"github.com/getAlby/hub/transactions"
|
||||
"github.com/getAlby/hub/utils"
|
||||
"github.com/getAlby/hub/version"
|
||||
)
|
||||
|
||||
type albyOAuthService struct {
|
||||
|
|
@ -209,7 +215,7 @@ func (svc *albyOAuthService) GetMe(ctx context.Context) (*AlbyMe, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "NWC-next")
|
||||
setDefaultRequestHeaders(req)
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
|
|
@ -244,7 +250,7 @@ func (svc *albyOAuthService) GetBalance(ctx context.Context) (*AlbyBalance, erro
|
|||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "NWC-next")
|
||||
setDefaultRequestHeaders(req)
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
|
|
@ -328,8 +334,7 @@ func (svc *albyOAuthService) SendPayment(ctx context.Context, invoice string) er
|
|||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "NWC-next")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
setDefaultRequestHeaders(req)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
|
|
@ -561,8 +566,7 @@ func (svc *albyOAuthService) consumeEvent(ctx context.Context, event *events.Eve
|
|||
return
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "NWC-next")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
setDefaultRequestHeaders(req)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
|
|
@ -630,8 +634,7 @@ func (svc *albyOAuthService) backupChannels(ctx context.Context, event *events.E
|
|||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "NWC-next")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
setDefaultRequestHeaders(req)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
|
|
@ -675,8 +678,7 @@ func (svc *albyOAuthService) createAlbyAccountNWCNode(ctx context.Context) (stri
|
|||
return "", err
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "NWC-next")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
setDefaultRequestHeaders(req)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
|
|
@ -726,8 +728,7 @@ func (svc *albyOAuthService) activateAlbyAccountNWCNode(ctx context.Context) err
|
|||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "NWC-next")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
setDefaultRequestHeaders(req)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
|
|
@ -763,7 +764,7 @@ func (svc *albyOAuthService) GetChannelPeerSuggestions(ctx context.Context) ([]C
|
|||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "NWC-next")
|
||||
setDefaultRequestHeaders(req)
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
|
|
@ -790,3 +791,261 @@ func (svc *albyOAuthService) GetChannelPeerSuggestions(ctx context.Context) ([]C
|
|||
logger.Logger.WithFields(logrus.Fields{"channel_suggestions": suggestions}).Info("Alby channel peer suggestions response")
|
||||
return suggestions, nil
|
||||
}
|
||||
|
||||
func (svc *albyOAuthService) RequestAutoChannel(ctx context.Context, lnClient lnclient.LNClient, isPublic bool) (*AutoChannelResponse, error) {
|
||||
nodeInfo, err := lnClient.GetInfo(ctx)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to request own node info", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
requestUrl := fmt.Sprintf("https://api.getalby.com/internal/lsp/alby/%s", nodeInfo.Network)
|
||||
|
||||
pubkey, address, port, err := svc.getLSPInfo(ctx, requestUrl+"/v1/get_info")
|
||||
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to request LSP info")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = lnClient.ConnectPeer(ctx, &lnclient.ConnectPeerRequest{
|
||||
Pubkey: pubkey,
|
||||
Address: address,
|
||||
Port: port,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"pubkey": pubkey,
|
||||
"address": address,
|
||||
"port": port,
|
||||
}).WithError(err).Error("Failed to connect to peer")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"pubkey": pubkey,
|
||||
"public": isPublic,
|
||||
}).Info("Requesting auto channel")
|
||||
|
||||
autoChannelResponse, err := svc.requestAutoChannel(ctx, requestUrl+"/auto_channel", nodeInfo.Pubkey, isPublic)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to request auto channel")
|
||||
return nil, err
|
||||
}
|
||||
return autoChannelResponse, nil
|
||||
}
|
||||
|
||||
func (svc *albyOAuthService) requestAutoChannel(ctx context.Context, url string, pubkey string, isPublic bool) (*AutoChannelResponse, error) {
|
||||
token, err := svc.fetchUserToken(ctx)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to fetch user token")
|
||||
}
|
||||
|
||||
client := svc.oauthConf.Client(ctx, token)
|
||||
client.Timeout = 60 * time.Second
|
||||
|
||||
type autoChannelRequest struct {
|
||||
NodePubkey string `json:"node_pubkey"`
|
||||
AnnounceChannel bool `json:"announce_channel"`
|
||||
}
|
||||
|
||||
newAutoChannelRequest := autoChannelRequest{
|
||||
NodePubkey: pubkey,
|
||||
AnnounceChannel: isPublic,
|
||||
}
|
||||
|
||||
payloadBytes, err := json.Marshal(newAutoChannelRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bodyReader := bytes.NewReader(payloadBytes)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, url, bodyReader)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).WithFields(logrus.Fields{
|
||||
"url": url,
|
||||
}).Error("Failed to create auto channel request")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
setDefaultRequestHeaders(req)
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).WithFields(logrus.Fields{
|
||||
"url": url,
|
||||
}).Error("Failed to request auto channel invoice")
|
||||
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{
|
||||
"newLSPS1ChannelRequest": newAutoChannelRequest,
|
||||
"body": string(body),
|
||||
"statusCode": res.StatusCode,
|
||||
}).Error("auto channel endpoint returned non-success code")
|
||||
return nil, fmt.Errorf("auto channel endpoint returned non-success code: %s", string(body))
|
||||
}
|
||||
|
||||
type newLSPS1ChannelPaymentBolt11 struct {
|
||||
Invoice string `json:"invoice"`
|
||||
FeeTotalSat string `json:"fee_total_sat"`
|
||||
}
|
||||
|
||||
type newLSPS1ChannelPayment struct {
|
||||
Bolt11 newLSPS1ChannelPaymentBolt11 `json:"bolt11"`
|
||||
// TODO: add onchain
|
||||
}
|
||||
type autoChannelResponse struct {
|
||||
LspBalanceSat string `json:"lsp_balance_sat"`
|
||||
Payment *newLSPS1ChannelPayment `json:"payment"`
|
||||
}
|
||||
|
||||
var newAutoChannelResponse autoChannelResponse
|
||||
|
||||
err = json.Unmarshal(body, &newAutoChannelResponse)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).WithFields(logrus.Fields{
|
||||
"url": url,
|
||||
}).Error("Failed to deserialize json")
|
||||
return nil, fmt.Errorf("failed to deserialize json %s %s", url, string(body))
|
||||
}
|
||||
|
||||
var invoice string
|
||||
var fee uint64
|
||||
|
||||
if newAutoChannelResponse.Payment != nil {
|
||||
invoice = newAutoChannelResponse.Payment.Bolt11.Invoice
|
||||
fee, err = strconv.ParseUint(newAutoChannelResponse.Payment.Bolt11.FeeTotalSat, 10, 64)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).WithFields(logrus.Fields{
|
||||
"url": url,
|
||||
}).Error("Failed to parse fee")
|
||||
return nil, fmt.Errorf("failed to parse fee %v", err)
|
||||
}
|
||||
|
||||
paymentRequest, err := decodepay.Decodepay(invoice)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to decode bolt11 invoice")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if fee != uint64(paymentRequest.MSatoshi/1000) {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"invoice_amount": paymentRequest.MSatoshi / 1000,
|
||||
"fee": fee,
|
||||
}).WithError(err).Error("Invoice amount does not match LSP fee")
|
||||
return nil, errors.New("invoice amount does not match LSP fee")
|
||||
}
|
||||
}
|
||||
|
||||
channelSize, err := strconv.ParseUint(newAutoChannelResponse.LspBalanceSat, 10, 64)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).WithFields(logrus.Fields{
|
||||
"url": url,
|
||||
}).Error("Failed to parse lsp balance sat")
|
||||
return nil, fmt.Errorf("failed to parse lsp balance sat %v", err)
|
||||
}
|
||||
|
||||
return &AutoChannelResponse{
|
||||
Invoice: invoice,
|
||||
Fee: fee,
|
||||
ChannelSize: channelSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (svc *albyOAuthService) getLSPInfo(ctx context.Context, url string) (pubkey string, address string, port uint16, err error) {
|
||||
|
||||
token, err := svc.fetchUserToken(ctx)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to fetch user token")
|
||||
}
|
||||
|
||||
client := svc.oauthConf.Client(ctx, token)
|
||||
client.Timeout = 60 * time.Second
|
||||
|
||||
type lsps1LSPInfo struct {
|
||||
URIs []string `json:"uris"`
|
||||
}
|
||||
var lsps1LspInfo lsps1LSPInfo
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).WithFields(logrus.Fields{
|
||||
"url": url,
|
||||
}).Error("Failed to create lsp info request")
|
||||
return "", "", uint16(0), err
|
||||
}
|
||||
|
||||
setDefaultRequestHeaders(req)
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).WithFields(logrus.Fields{
|
||||
"url": url,
|
||||
}).Error("Failed to request lsp info")
|
||||
return "", "", uint16(0), 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 "", "", uint16(0), errors.New("failed to read response body")
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &lsps1LspInfo)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).WithFields(logrus.Fields{
|
||||
"url": url,
|
||||
}).Error("Failed to deserialize json")
|
||||
return "", "", uint16(0), fmt.Errorf("failed to deserialize json %s %s", url, string(body))
|
||||
}
|
||||
|
||||
httpUris := utils.Filter(lsps1LspInfo.URIs, func(uri string) bool {
|
||||
return !strings.Contains(uri, ".onion")
|
||||
})
|
||||
if len(httpUris) == 0 {
|
||||
logger.Logger.WithField("uris", lsps1LspInfo.URIs).WithError(err).Error("Couldn't find HTTP URI")
|
||||
|
||||
return "", "", uint16(0), err
|
||||
}
|
||||
uri := httpUris[0]
|
||||
|
||||
// make sure it's a valid IPv4 URI
|
||||
regex := regexp.MustCompile(`^([0-9a-f]+)@([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+):([0-9]+)$`)
|
||||
parts := regex.FindStringSubmatch(uri)
|
||||
logger.Logger.WithField("parts", parts).Info("Split URI")
|
||||
if parts == nil || len(parts) != 4 {
|
||||
logger.Logger.WithField("parts", parts).Error("Unsupported URI")
|
||||
return "", "", uint16(0), errors.New("could not decode LSP URI")
|
||||
}
|
||||
|
||||
portValue, err := strconv.Atoi(parts[3])
|
||||
if err != nil {
|
||||
logger.Logger.WithField("port", parts[3]).WithError(err).Error("Failed to decode port number")
|
||||
|
||||
return "", "", uint16(0), err
|
||||
}
|
||||
|
||||
return parts[1], parts[2], uint16(portValue), nil
|
||||
}
|
||||
|
||||
func setDefaultRequestHeaders(req *http.Request) {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("User-Agent", "AlbyHub/"+version.Tag)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ type AlbyOAuthService interface {
|
|||
GetMe(ctx context.Context) (*AlbyMe, error)
|
||||
SendPayment(ctx context.Context, invoice string) error
|
||||
DrainSharedWallet(ctx context.Context, lnClient lnclient.LNClient) error
|
||||
RequestAutoChannel(ctx context.Context, lnClient lnclient.LNClient, isPublic bool) (*AutoChannelResponse, error)
|
||||
}
|
||||
|
||||
type AlbyBalanceResponse struct {
|
||||
|
|
@ -34,6 +35,16 @@ type AlbyLinkAccountRequest struct {
|
|||
Renewal string `json:"renewal"`
|
||||
}
|
||||
|
||||
type AutoChannelRequest struct {
|
||||
IsPublic bool `json:"isPublic"`
|
||||
}
|
||||
|
||||
type AutoChannelResponse struct {
|
||||
Invoice string `json:"invoice"`
|
||||
ChannelSize uint64 `json:"channelSize"`
|
||||
Fee uint64 `json:"fee"`
|
||||
}
|
||||
|
||||
type AlbyMeHub struct {
|
||||
LatestVersion string `json:"latest_version"`
|
||||
}
|
||||
|
|
|
|||
360
api/lsp.go
360
api/lsp.go
|
|
@ -28,32 +28,19 @@ type lspInfo struct {
|
|||
MaxChannelExpiryBlocks uint64
|
||||
}
|
||||
|
||||
func (api *api) NewInstantChannelInvoice(ctx context.Context, request *NewInstantChannelInvoiceRequest) (*NewInstantChannelInvoiceResponse, error) {
|
||||
func (api *api) RequestLSPOrder(ctx context.Context, request *LSPOrderRequest) (*LSPOrderResponse, error) {
|
||||
|
||||
if api.svc.GetLNClient() == nil {
|
||||
return nil, errors.New("LNClient not started")
|
||||
}
|
||||
|
||||
if request.LSPType != lsp.LSP_TYPE_LSPS1 && request.Public {
|
||||
return nil, errors.New("This LSP option does not support public channels")
|
||||
if request.LSPType != lsp.LSP_TYPE_LSPS1 {
|
||||
return nil, fmt.Errorf("unsupported LSP type: %v", request.LSPType)
|
||||
}
|
||||
|
||||
logger.Logger.Infoln("Requesting LSP info")
|
||||
lspInfo, err := api.getLSPS1LSPInfo(request.LSPUrl + "/get_info")
|
||||
|
||||
var lspInfo *lspInfo
|
||||
var err error
|
||||
switch request.LSPType {
|
||||
case lsp.LSP_TYPE_FLOW_2_0:
|
||||
fallthrough
|
||||
case lsp.LSP_TYPE_PMLSP:
|
||||
lspInfo, err = api.getFlowLSPInfo(request.LSPUrl + "/info")
|
||||
|
||||
case lsp.LSP_TYPE_LSPS1:
|
||||
lspInfo, err = api.getLSPS1LSPInfo(request.LSPUrl + "/get_info")
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported LSP type: %v", request.LSPType)
|
||||
}
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to request LSP info")
|
||||
return nil, err
|
||||
|
|
@ -82,17 +69,8 @@ func (api *api) NewInstantChannelInvoice(ctx context.Context, request *NewInstan
|
|||
return nil, err
|
||||
}
|
||||
|
||||
invoice := ""
|
||||
var fee uint64 = 0
|
||||
invoice, fee, err := api.requestLSPS1Invoice(ctx, request, nodeInfo.Pubkey, lspInfo.MaxChannelExpiryBlocks)
|
||||
|
||||
switch request.LSPType {
|
||||
case lsp.LSP_TYPE_FLOW_2_0:
|
||||
invoice, fee, err = api.requestFlow20WrappedInvoice(ctx, request, nodeInfo.Pubkey)
|
||||
case lsp.LSP_TYPE_PMLSP:
|
||||
invoice, fee, err = api.requestPMLSPInvoice(request, nodeInfo.Pubkey)
|
||||
case lsp.LSP_TYPE_LSPS1:
|
||||
invoice, fee, err = api.requestLSPS1Invoice(ctx, request, nodeInfo.Pubkey, lspInfo.MaxChannelExpiryBlocks)
|
||||
}
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to request invoice")
|
||||
return nil, err
|
||||
|
|
@ -116,7 +94,7 @@ func (api *api) NewInstantChannelInvoice(ctx context.Context, request *NewInstan
|
|||
outgoingLiquidity = invoiceAmount - fee
|
||||
}
|
||||
|
||||
newChannelResponse := &NewInstantChannelInvoiceResponse{
|
||||
newChannelResponse := &LSPOrderResponse{
|
||||
Invoice: invoice,
|
||||
Fee: fee,
|
||||
InvoiceAmount: invoiceAmount,
|
||||
|
|
@ -208,327 +186,13 @@ func (api *api) getLSPS1LSPInfo(url string) (*lspInfo, error) {
|
|||
MaxChannelExpiryBlocks: lsps1LspInfo.MaxChannelExpiryBlocks,
|
||||
}, nil
|
||||
}
|
||||
func (api *api) getFlowLSPInfo(url string) (*lspInfo, error) {
|
||||
type FlowLSPConnectionMethod struct {
|
||||
Address string `json:"address"`
|
||||
Port uint16 `json:"port"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
type FlowLSPInfo struct {
|
||||
Pubkey string `json:"pubkey"`
|
||||
ConnectionMethods []FlowLSPConnectionMethod `json:"connection_methods"`
|
||||
}
|
||||
var flowLspInfo FlowLSPInfo
|
||||
|
||||
func (api *api) requestLSPS1Invoice(ctx context.Context, request *LSPOrderRequest, pubkey string, channelExpiryBlocks uint64) (invoice string, fee uint64, err error) {
|
||||
client := http.Client{
|
||||
Timeout: time.Second * 10,
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).WithFields(logrus.Fields{
|
||||
"url": url,
|
||||
}).Error("Failed to create lsp info request")
|
||||
return nil, err
|
||||
Timeout: time.Second * 60,
|
||||
}
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).WithFields(logrus.Fields{
|
||||
"url": url,
|
||||
}).Error("Failed to request lsp info")
|
||||
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")
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &flowLspInfo)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).WithFields(logrus.Fields{
|
||||
"url": url,
|
||||
}).Error("Failed to deserialize json")
|
||||
return nil, fmt.Errorf("failed to deserialize json %s %s", url, string(body))
|
||||
}
|
||||
|
||||
ipIndex := -1
|
||||
for i, cm := range flowLspInfo.ConnectionMethods {
|
||||
if strings.HasPrefix(cm.Type, "ip") {
|
||||
ipIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ipIndex == -1 {
|
||||
logger.Logger.Error("No ipv4/ipv6 connection method found in LSP info")
|
||||
return nil, errors.New("unexpected LSP connection method")
|
||||
}
|
||||
|
||||
return &lspInfo{
|
||||
Pubkey: flowLspInfo.Pubkey,
|
||||
Address: flowLspInfo.ConnectionMethods[ipIndex].Address,
|
||||
Port: flowLspInfo.ConnectionMethods[ipIndex].Port,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (api *api) requestFlow20WrappedInvoice(ctx context.Context, request *NewInstantChannelInvoiceRequest, pubkey string) (invoice string, fee uint64, err error) {
|
||||
logger.Logger.Infoln("Requesting fee information")
|
||||
|
||||
type FeeRequest struct {
|
||||
AmountMsat uint64 `json:"amount_msat"`
|
||||
Pubkey string `json:"pubkey"`
|
||||
}
|
||||
type FeeResponse struct {
|
||||
FeeAmountMsat uint64 `json:"fee_amount_msat"`
|
||||
Id string `json:"id"`
|
||||
}
|
||||
|
||||
var feeResponse FeeResponse
|
||||
{
|
||||
client := http.Client{
|
||||
Timeout: time.Second * 10,
|
||||
}
|
||||
payloadBytes, err := json.Marshal(FeeRequest{
|
||||
AmountMsat: request.Amount * 1000,
|
||||
Pubkey: pubkey,
|
||||
})
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
bodyReader := bytes.NewReader(payloadBytes)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, request.LSPUrl+"/fee", bodyReader)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).WithFields(logrus.Fields{
|
||||
"url": request.LSPUrl,
|
||||
}).Error("Failed to create lsp fee request")
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).WithFields(logrus.Fields{
|
||||
"url": request.LSPUrl,
|
||||
}).Error("Failed to request lsp fee")
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).WithFields(logrus.Fields{
|
||||
"url": request.LSPUrl,
|
||||
}).Error("Failed to read response body")
|
||||
return "", 0, errors.New("failed to read response body")
|
||||
}
|
||||
|
||||
if res.StatusCode >= 300 {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"body": string(body),
|
||||
"statusCode": res.StatusCode,
|
||||
}).Error("fee endpoint returned non-success code")
|
||||
return "", 0, fmt.Errorf("fee endpoint returned non-success code: %s", string(body))
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &feeResponse)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).WithFields(logrus.Fields{
|
||||
"url": request.LSPUrl,
|
||||
}).Error("Failed to deserialize json")
|
||||
return "", 0, fmt.Errorf("failed to deserialize json %s %s", request.LSPUrl, string(body))
|
||||
}
|
||||
|
||||
logger.Logger.WithError(err).WithFields(logrus.Fields{
|
||||
"url": request.LSPUrl,
|
||||
"feeResponse": feeResponse,
|
||||
}).Info("Got fee response")
|
||||
if feeResponse.Id == "" {
|
||||
logger.Logger.WithError(err).WithFields(logrus.Fields{
|
||||
"feeResponse": feeResponse,
|
||||
}).Error("No fee id in fee response")
|
||||
return "", 0, fmt.Errorf("no fee id in fee response %v", feeResponse)
|
||||
}
|
||||
fee = feeResponse.FeeAmountMsat / 1000
|
||||
}
|
||||
|
||||
// because we don't want the sender to pay the fee
|
||||
// see: https://docs.voltage.cloud/voltage-lsp#gqBqV
|
||||
makeInvoiceResponse, err := api.svc.GetLNClient().MakeInvoice(ctx, int64(request.Amount)*1000-int64(feeResponse.FeeAmountMsat), "", "", 60*60)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to request own invoice")
|
||||
return "", 0, fmt.Errorf("failed to request own invoice %v", err)
|
||||
}
|
||||
|
||||
type ProposalRequest struct {
|
||||
Bolt11 string `json:"bolt11"`
|
||||
FeeId string `json:"fee_id"`
|
||||
}
|
||||
type ProposalResponse struct {
|
||||
Bolt11 string `json:"jit_bolt11"`
|
||||
}
|
||||
|
||||
logger.Logger.Infoln("Proposing invoice")
|
||||
|
||||
var proposalResponse ProposalResponse
|
||||
{
|
||||
client := http.Client{
|
||||
Timeout: time.Second * 10,
|
||||
}
|
||||
payloadBytes, err := json.Marshal(ProposalRequest{
|
||||
Bolt11: makeInvoiceResponse.Invoice,
|
||||
FeeId: feeResponse.Id,
|
||||
})
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
bodyReader := bytes.NewReader(payloadBytes)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, request.LSPUrl+"/proposal", bodyReader)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).WithFields(logrus.Fields{
|
||||
"url": request.LSPUrl,
|
||||
}).Error("Failed to create lsp fee request")
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).WithFields(logrus.Fields{
|
||||
"url": request.LSPUrl,
|
||||
}).Error("Failed to request lsp fee")
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).WithFields(logrus.Fields{
|
||||
"url": request.LSPUrl,
|
||||
}).Error("Failed to read response body")
|
||||
return "", 0, errors.New("failed to read response body")
|
||||
}
|
||||
|
||||
if res.StatusCode >= 300 {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"body": string(body),
|
||||
"statusCode": res.StatusCode,
|
||||
}).Error("proposal endpoint returned non-success code")
|
||||
return "", 0, fmt.Errorf("proposal endpoint returned non-success code: %s", string(body))
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &proposalResponse)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).WithFields(logrus.Fields{
|
||||
"url": request.LSPUrl,
|
||||
}).Error("Failed to deserialize json")
|
||||
return "", 0, fmt.Errorf("failed to deserialize json %s %s", request.LSPUrl, string(body))
|
||||
}
|
||||
logger.Logger.WithField("proposalResponse", proposalResponse).Info("Got proposal response")
|
||||
if proposalResponse.Bolt11 == "" {
|
||||
logger.Logger.WithError(err).WithFields(logrus.Fields{
|
||||
"url": request.LSPUrl,
|
||||
"proposalResponse": proposalResponse,
|
||||
}).Error("No bolt11 in proposal response")
|
||||
return "", 0, fmt.Errorf("no bolt11 in proposal response %v", proposalResponse)
|
||||
}
|
||||
}
|
||||
invoice = proposalResponse.Bolt11
|
||||
|
||||
return invoice, fee, nil
|
||||
}
|
||||
|
||||
func (api *api) requestPMLSPInvoice(request *NewInstantChannelInvoiceRequest, pubkey string) (invoice string, fee uint64, err error) {
|
||||
type NewInstantChannelRequest struct {
|
||||
Amount uint64 `json:"amount"`
|
||||
Pubkey string `json:"pubkey"`
|
||||
}
|
||||
|
||||
client := http.Client{
|
||||
Timeout: time.Second * 10,
|
||||
}
|
||||
payloadBytes, err := json.Marshal(NewInstantChannelRequest{
|
||||
Amount: request.Amount,
|
||||
Pubkey: pubkey,
|
||||
})
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
bodyReader := bytes.NewReader(payloadBytes)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, request.LSPUrl+"/new-channel", bodyReader)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).WithFields(logrus.Fields{
|
||||
"url": request.LSPUrl,
|
||||
}).Error("Failed to create new channel request")
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).WithFields(logrus.Fields{
|
||||
"url": request.LSPUrl,
|
||||
}).Error("Failed to request new channel invoice")
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).WithFields(logrus.Fields{
|
||||
"url": request.LSPUrl,
|
||||
}).Error("Failed to read response body")
|
||||
return "", 0, errors.New("failed to read response body")
|
||||
}
|
||||
|
||||
if res.StatusCode >= 300 {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"body": string(body),
|
||||
"statusCode": res.StatusCode,
|
||||
}).Error("new-channel endpoint returned non-success code")
|
||||
return "", 0, fmt.Errorf("new-channel endpoint returned non-success code: %s", string(body))
|
||||
}
|
||||
|
||||
type newInstantChannelResponse struct {
|
||||
FeeAmountMsat uint64 `json:"fee_amount_msat"`
|
||||
Invoice string `json:"invoice"`
|
||||
}
|
||||
|
||||
var newChannelResponse newInstantChannelResponse
|
||||
|
||||
err = json.Unmarshal(body, &newChannelResponse)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).WithFields(logrus.Fields{
|
||||
"url": request.LSPUrl,
|
||||
}).Error("Failed to deserialize json")
|
||||
return "", 0, fmt.Errorf("failed to deserialize json %s %s", request.LSPUrl, string(body))
|
||||
}
|
||||
|
||||
invoice = newChannelResponse.Invoice
|
||||
fee = newChannelResponse.FeeAmountMsat / 1000
|
||||
|
||||
return invoice, fee, nil
|
||||
}
|
||||
|
||||
func (api *api) requestLSPS1Invoice(ctx context.Context, request *NewInstantChannelInvoiceRequest, pubkey string, channelExpiryBlocks uint64) (invoice string, fee uint64, err error) {
|
||||
client := http.Client{
|
||||
Timeout: time.Second * 10,
|
||||
}
|
||||
|
||||
type NewLSPS1ChannelRequest struct {
|
||||
type lsps1ChannelRequest struct {
|
||||
PublicKey string `json:"public_key"`
|
||||
LSPBalanceSat string `json:"lsp_balance_sat"`
|
||||
ClientBalanceSat string `json:"client_balance_sat"`
|
||||
|
|
@ -554,7 +218,7 @@ func (api *api) requestLSPS1Invoice(ctx context.Context, request *NewInstantChan
|
|||
requiredChannelConfirmations = 6
|
||||
}
|
||||
|
||||
newLSPS1ChannelRequest := NewLSPS1ChannelRequest{
|
||||
newLSPS1ChannelRequest := lsps1ChannelRequest{
|
||||
PublicKey: pubkey,
|
||||
LSPBalanceSat: strconv.FormatUint(request.Amount, 10),
|
||||
ClientBalanceSat: "0",
|
||||
|
|
@ -619,7 +283,7 @@ func (api *api) requestLSPS1Invoice(ctx context.Context, request *NewInstantChan
|
|||
// TODO: add onchain
|
||||
}
|
||||
type newLSPS1ChannelResponse struct {
|
||||
Payment newLSPS1ChannelPayment `json:"payment"`
|
||||
Payment *newLSPS1ChannelPayment `json:"payment"`
|
||||
}
|
||||
|
||||
var newChannelResponse newLSPS1ChannelResponse
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ type API interface {
|
|||
GetNetworkGraph(nodeIds []string) (NetworkGraphResponse, error)
|
||||
SyncWallet() error
|
||||
GetLogOutput(ctx context.Context, logType string, getLogRequest *GetLogOutputRequest) (*GetLogOutputResponse, error)
|
||||
NewInstantChannelInvoice(ctx context.Context, request *NewInstantChannelInvoiceRequest) (*NewInstantChannelInvoiceResponse, error)
|
||||
RequestLSPOrder(ctx context.Context, request *LSPOrderRequest) (*LSPOrderResponse, error)
|
||||
CreateBackup(unlockPassword string, w io.Writer) error
|
||||
RestoreBackup(unlockPassword string, r io.Reader) error
|
||||
GetWalletCapabilities(ctx context.Context) (*WalletCapabilitiesResponse, error)
|
||||
|
|
@ -265,14 +265,14 @@ type BasicRestoreWailsRequest struct {
|
|||
|
||||
type NetworkGraphResponse = lnclient.NetworkGraphResponse
|
||||
|
||||
type NewInstantChannelInvoiceRequest struct {
|
||||
type LSPOrderRequest struct {
|
||||
Amount uint64 `json:"amount"`
|
||||
LSPType string `json:"lspType"`
|
||||
LSPUrl string `json:"lspUrl"`
|
||||
Public bool `json:"public"`
|
||||
}
|
||||
|
||||
type NewInstantChannelInvoiceResponse struct {
|
||||
type LSPOrderResponse struct {
|
||||
Invoice string `json:"invoice"`
|
||||
Fee uint64 `json:"fee"`
|
||||
InvoiceAmount uint64 `json:"invoiceAmount"`
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 66 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 63 KiB |
|
|
@ -39,6 +39,7 @@ function SidebarHint() {
|
|||
if (
|
||||
!location.pathname.startsWith("/home") &&
|
||||
!location.pathname.startsWith("/channels/order") &&
|
||||
!location.pathname.startsWith("/channels/first") &&
|
||||
!isLoading &&
|
||||
openChecklistItems.length
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -3,11 +3,6 @@ export const localStorageKeys = {
|
|||
channelOrder: "channelOrder",
|
||||
};
|
||||
|
||||
const MIN_0CONF_BALANCE = 200000; // 100,000 for Alby. 20000 works for Olympus and Voltage
|
||||
export const ALBY_SERVICE_FEE = 8 / 1000;
|
||||
export const ALBY_MIN_BALANCE = Math.ceil(
|
||||
MIN_0CONF_BALANCE * (1 + ALBY_SERVICE_FEE)
|
||||
);
|
||||
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
// src/hooks/useOnboardingData.ts
|
||||
|
||||
import { ALBY_MIN_BALANCE, ALBY_SERVICE_FEE } from "src/constants";
|
||||
import { useAlbyBalance } from "src/hooks/useAlbyBalance";
|
||||
import { useAlbyMe } from "src/hooks/useAlbyMe";
|
||||
import { useApps } from "src/hooks/useApps";
|
||||
|
|
@ -59,21 +58,13 @@ export const useOnboardingData = (): UseOnboardingDataResponse => {
|
|||
apps && apps.find((x) => x.name !== "getalby.com") !== undefined;
|
||||
const hasTransaction = transactions.length > 0;
|
||||
|
||||
const canMigrateAlbyFundsToNewChannel =
|
||||
hasChannelManagement &&
|
||||
info.backendType === "LDK" &&
|
||||
albyBalance.sats * (1 - ALBY_SERVICE_FEE) >
|
||||
ALBY_MIN_BALANCE + 50000; /* accommodate for onchain fees */
|
||||
|
||||
const checklistItems: Omit<ChecklistItem, "disabled">[] = [
|
||||
{
|
||||
title: "1. Open your first channel",
|
||||
description:
|
||||
"Establish a new Lightning channel to enable fast and low-fee Bitcoin transactions.",
|
||||
checked: hasChannel,
|
||||
to: canMigrateAlbyFundsToNewChannel
|
||||
? "/onboarding/lightning/migrate-alby"
|
||||
: "/channels/outgoing",
|
||||
to: "/channels/first",
|
||||
},
|
||||
{
|
||||
title: "2. Link to your Alby Account",
|
||||
|
|
|
|||
|
|
@ -26,7 +26,9 @@ import Channels from "src/screens/channels/Channels";
|
|||
import { CurrentChannelOrder } from "src/screens/channels/CurrentChannelOrder";
|
||||
import IncreaseIncomingCapacity from "src/screens/channels/IncreaseIncomingCapacity";
|
||||
import IncreaseOutgoingCapacity from "src/screens/channels/IncreaseOutgoingCapacity";
|
||||
import MigrateAlbyFunds from "src/screens/onboarding/MigrateAlbyFunds";
|
||||
import { FirstChannel } from "src/screens/channels/first/FirstChannel";
|
||||
import { OpenedFirstChannel } from "src/screens/channels/first/OpenedFirstChannel";
|
||||
import { OpeningFirstChannel } from "src/screens/channels/first/OpeningFirstChannel";
|
||||
import { Success } from "src/screens/onboarding/Success";
|
||||
import BuyBitcoin from "src/screens/onchain/BuyBitcoin";
|
||||
import DepositBitcoin from "src/screens/onchain/DepositBitcoin";
|
||||
|
|
@ -176,6 +178,21 @@ const routes = [
|
|||
index: true,
|
||||
element: <Channels />,
|
||||
},
|
||||
{
|
||||
path: "first",
|
||||
element: <FirstChannel />,
|
||||
handle: { crumb: () => "Open Your First Channel" },
|
||||
},
|
||||
{
|
||||
path: "first/opening",
|
||||
element: <OpeningFirstChannel />,
|
||||
handle: { crumb: () => "Opening Your First Channel" },
|
||||
},
|
||||
{
|
||||
path: "first/opened",
|
||||
element: <OpenedFirstChannel />,
|
||||
handle: { crumb: () => "First Channel Opened!" },
|
||||
},
|
||||
{
|
||||
path: "outgoing",
|
||||
element: <IncreaseOutgoingCapacity />,
|
||||
|
|
@ -325,10 +342,6 @@ const routes = [
|
|||
path: "onboarding",
|
||||
element: <OnboardingRedirect />,
|
||||
children: [
|
||||
{
|
||||
path: "lightning/migrate-alby",
|
||||
element: <MigrateAlbyFunds />,
|
||||
},
|
||||
{
|
||||
path: "success",
|
||||
element: <Success />,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import React from "react";
|
||||
import {
|
||||
Channel,
|
||||
ConnectPeerRequest,
|
||||
NewChannelOrder,
|
||||
Node,
|
||||
|
|
@ -36,6 +35,7 @@ import {
|
|||
import { Input } from "src/components/ui/input";
|
||||
import { Label } from "src/components/ui/label";
|
||||
import { LoadingButton } from "src/components/ui/loading-button";
|
||||
import { Separator } from "src/components/ui/separator";
|
||||
import { Table, TableBody, TableCell, TableRow } from "src/components/ui/table";
|
||||
import {
|
||||
Tooltip,
|
||||
|
|
@ -55,10 +55,7 @@ import { copyToClipboard } from "src/lib/clipboard";
|
|||
import { splitSocketAddress } from "src/lib/utils";
|
||||
import { Success } from "src/screens/onboarding/Success";
|
||||
import useChannelOrderStore from "src/state/ChannelOrderStore";
|
||||
import {
|
||||
NewInstantChannelInvoiceRequest,
|
||||
NewInstantChannelInvoiceResponse,
|
||||
} from "src/types";
|
||||
import { LSPOrderRequest, LSPOrderResponse } from "src/types";
|
||||
import { request } from "src/utils/request";
|
||||
init({
|
||||
showBalance: false,
|
||||
|
|
@ -96,6 +93,9 @@ function ChannelOrderInternal({ order }: { order: NewChannelOrder }) {
|
|||
break;
|
||||
}
|
||||
break;
|
||||
case "paid":
|
||||
// LSPS1 only
|
||||
return <PaidLightningChannelOrder />;
|
||||
case "opening":
|
||||
return <ChannelOpening fundingTxId={order.fundingTxId} />;
|
||||
case "success":
|
||||
|
|
@ -525,29 +525,18 @@ function PayBitcoinChannelOrderWithSpendableFunds({
|
|||
);
|
||||
}
|
||||
|
||||
function PayLightningChannelOrder({ order }: { order: NewChannelOrder }) {
|
||||
if (order.paymentMethod !== "lightning") {
|
||||
throw new Error("incorrect payment method");
|
||||
}
|
||||
const { data: csrf } = useCSRF();
|
||||
const { toast } = useToast();
|
||||
function useWaitForNewChannel() {
|
||||
const order = useChannelOrderStore((store) => store.order);
|
||||
const { data: channels } = useChannels(true);
|
||||
const [, setRequestedInvoice] = React.useState(false);
|
||||
const [prevChannels, setPrevChannels] = React.useState<
|
||||
Channel[] | undefined
|
||||
>();
|
||||
const [wrappedInvoiceResponse, setWrappedInvoiceResponse] = React.useState<
|
||||
NewInstantChannelInvoiceResponse | undefined
|
||||
>();
|
||||
const { toast } = useToast();
|
||||
|
||||
// This is not a good check if user already has enough inbound liquidity
|
||||
// - check balance instead or how else to check the invoice is paid?
|
||||
const newChannel =
|
||||
channels && prevChannels
|
||||
channels && order?.prevChannelIds
|
||||
? channels.find(
|
||||
(newChannel) =>
|
||||
!prevChannels.some((current) => current.id === newChannel.id) &&
|
||||
newChannel.fundingTxId
|
||||
!order.prevChannelIds.some(
|
||||
(current) => newChannel.id === current
|
||||
) && newChannel.fundingTxId
|
||||
)
|
||||
: undefined;
|
||||
|
||||
|
|
@ -564,9 +553,34 @@ function PayLightningChannelOrder({ order }: { order: NewChannelOrder }) {
|
|||
})();
|
||||
}
|
||||
}, [newChannel, toast]);
|
||||
}
|
||||
|
||||
function PaidLightningChannelOrder() {
|
||||
useWaitForNewChannel();
|
||||
|
||||
return (
|
||||
<div className="flex w-full h-full gap-2 items-center justify-center">
|
||||
<Loading /> <p>Waiting for channel to be opened...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PayLightningChannelOrder({ order }: { order: NewChannelOrder }) {
|
||||
if (order.paymentMethod !== "lightning") {
|
||||
throw new Error("incorrect payment method");
|
||||
}
|
||||
const { data: csrf } = useCSRF();
|
||||
const { toast } = useToast();
|
||||
const { data: channels } = useChannels(true);
|
||||
const [, setRequestedInvoice] = React.useState(false);
|
||||
|
||||
const [wrappedInvoiceResponse, setWrappedInvoiceResponse] = React.useState<
|
||||
LSPOrderResponse | undefined
|
||||
>();
|
||||
|
||||
useWaitForNewChannel();
|
||||
|
||||
React.useEffect(() => {
|
||||
// TODO: move fetching to NewChannel page otherwise fee cannot be retrieved
|
||||
if (!channels || !csrf) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -574,26 +588,24 @@ function PayLightningChannelOrder({ order }: { order: NewChannelOrder }) {
|
|||
if (!current) {
|
||||
(async () => {
|
||||
try {
|
||||
setPrevChannels(channels);
|
||||
if (!order.lspType || !order.lspUrl) {
|
||||
throw new Error("missing lsp info in order");
|
||||
}
|
||||
const newInstantChannelInvoiceRequest: NewInstantChannelInvoiceRequest =
|
||||
{
|
||||
lspType: order.lspType,
|
||||
lspUrl: order.lspUrl,
|
||||
amount: parseInt(order.amount),
|
||||
public: order.isPublic,
|
||||
};
|
||||
const response = await request<NewInstantChannelInvoiceResponse>(
|
||||
"/api/instant-channel-invoices",
|
||||
const newLSPOrderRequest: LSPOrderRequest = {
|
||||
lspType: order.lspType,
|
||||
lspUrl: order.lspUrl,
|
||||
amount: parseInt(order.amount),
|
||||
public: order.isPublic,
|
||||
};
|
||||
const response = await request<LSPOrderResponse>(
|
||||
"/api/lsp-orders",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-CSRF-Token": csrf,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(newInstantChannelInvoiceRequest),
|
||||
body: JSON.stringify(newLSPOrderRequest),
|
||||
}
|
||||
);
|
||||
if (!response?.invoice) {
|
||||
|
|
@ -625,7 +637,6 @@ function PayLightningChannelOrder({ order }: { order: NewChannelOrder }) {
|
|||
wrappedInvoiceResponse.invoiceAmount
|
||||
);
|
||||
const [isPaying, setPaying] = React.useState(false);
|
||||
const [paid, setPaid] = React.useState(false);
|
||||
const [payExternally, setPayExternally] = React.useState(false);
|
||||
|
||||
return (
|
||||
|
|
@ -697,77 +708,88 @@ function PayLightningChannelOrder({ order }: { order: NewChannelOrder }) {
|
|||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
{paid ? (
|
||||
<div className="flex gap-2 items-center justify-center">
|
||||
<Loading /> <p>Waiting for channel to be opened...</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{canPayInternally && (
|
||||
<>
|
||||
<LoadingButton
|
||||
loading={isPaying}
|
||||
className="mt-4"
|
||||
onClick={async () => {
|
||||
try {
|
||||
if (!csrf) {
|
||||
throw new Error("csrf not loaded");
|
||||
}
|
||||
setPaying(true);
|
||||
const payInvoiceResponse =
|
||||
await request<PayInvoiceResponse>(
|
||||
`/api/payments/${wrappedInvoiceResponse.invoice}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-CSRF-Token": csrf,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
if (payInvoiceResponse) {
|
||||
setPaid(true);
|
||||
toast({
|
||||
title: "Channel successfully requested",
|
||||
});
|
||||
}
|
||||
setPaid(true);
|
||||
} catch (e) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to send: " + e,
|
||||
});
|
||||
console.error(e);
|
||||
<>
|
||||
{canPayInternally && (
|
||||
<>
|
||||
<LoadingButton
|
||||
loading={isPaying}
|
||||
className="mt-4"
|
||||
onClick={async () => {
|
||||
try {
|
||||
if (!csrf) {
|
||||
throw new Error("csrf not loaded");
|
||||
}
|
||||
setPaying(false);
|
||||
}}
|
||||
>
|
||||
Pay and open channel
|
||||
</LoadingButton>
|
||||
{!payExternally && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="link"
|
||||
className="text-muted-foreground text-xs"
|
||||
onClick={() => setPayExternally(true)}
|
||||
>
|
||||
Pay with another wallet
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
setPaying(true);
|
||||
|
||||
{(payExternally || !canPayInternally) && (
|
||||
<Payment
|
||||
invoice={wrappedInvoiceResponse.invoice}
|
||||
payment={
|
||||
newChannel ? { preimage: "dummy preimage" } : undefined
|
||||
}
|
||||
paymentMethods="external"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
await request<PayInvoiceResponse>(
|
||||
`/api/payments/${wrappedInvoiceResponse.invoice}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-CSRF-Token": csrf,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
useChannelOrderStore.getState().updateOrder({
|
||||
status: "paid",
|
||||
});
|
||||
toast({
|
||||
title: "Channel successfully requested",
|
||||
});
|
||||
} catch (e) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to send: " + e,
|
||||
});
|
||||
console.error(e);
|
||||
}
|
||||
setPaying(false);
|
||||
}}
|
||||
>
|
||||
Pay and open channel
|
||||
</LoadingButton>
|
||||
{!payExternally && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="link"
|
||||
className="text-muted-foreground text-xs"
|
||||
onClick={() => setPayExternally(true)}
|
||||
>
|
||||
Pay with another wallet
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{(payExternally || !canPayInternally) && (
|
||||
<Payment
|
||||
invoice={wrappedInvoiceResponse.invoice}
|
||||
paymentMethods="external"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex-1 flex flex-col justify-end items-center gap-4">
|
||||
<Separator className="my-16" />
|
||||
<p className="text-sm text-muted-foreground text-center">
|
||||
Other options
|
||||
</p>
|
||||
<Link to="/channels/outgoing" className="w-full">
|
||||
<Button className="w-full" variant="secondary">
|
||||
Increase spending balance
|
||||
</Button>
|
||||
</Link>
|
||||
<ExternalLink
|
||||
to="https://www.getalby.com/topup"
|
||||
className="w-full"
|
||||
>
|
||||
<Button className="w-full" variant="secondary">
|
||||
Buy Bitcoin
|
||||
</Button>
|
||||
</ExternalLink>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { ChevronDown } from "lucide-react";
|
||||
import React, { FormEvent } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import AppHeader from "src/components/AppHeader";
|
||||
import ExternalLink from "src/components/ExternalLink";
|
||||
import Loading from "src/components/Loading";
|
||||
|
|
@ -21,7 +21,12 @@ import { useChannels } from "src/hooks/useChannels";
|
|||
import { useInfo } from "src/hooks/useInfo";
|
||||
import { cn, formatAmount } from "src/lib/utils";
|
||||
import useChannelOrderStore from "src/state/ChannelOrderStore";
|
||||
import { Network, NewChannelOrder, RecommendedChannelPeer } from "src/types";
|
||||
import {
|
||||
Channel,
|
||||
Network,
|
||||
NewChannelOrder,
|
||||
RecommendedChannelPeer,
|
||||
} from "src/types";
|
||||
|
||||
function getPeerKey(peer: RecommendedChannelPeer) {
|
||||
return JSON.stringify(peer);
|
||||
|
|
@ -29,18 +34,25 @@ function getPeerKey(peer: RecommendedChannelPeer) {
|
|||
|
||||
export default function IncreaseIncomingCapacity() {
|
||||
const { data: info } = useInfo();
|
||||
const { data: channels } = useChannels();
|
||||
|
||||
if (!info?.network) {
|
||||
if (!info?.network || !channels) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return <NewChannelInternal network={info.network} />;
|
||||
return <NewChannelInternal network={info.network} channels={channels} />;
|
||||
}
|
||||
|
||||
function NewChannelInternal({ network }: { network: Network }) {
|
||||
function NewChannelInternal({
|
||||
network,
|
||||
channels,
|
||||
}: {
|
||||
network: Network;
|
||||
channels: Channel[];
|
||||
}) {
|
||||
const { data: _channelPeerSuggestions } = useChannelPeerSuggestions();
|
||||
const navigate = useNavigate();
|
||||
const { data: channels } = useChannels();
|
||||
|
||||
const { toast } = useToast();
|
||||
|
||||
const presetAmounts = [1_000_000, 2_000_000, 3_000_000];
|
||||
|
|
@ -49,6 +61,7 @@ function NewChannelInternal({ network }: { network: Network }) {
|
|||
paymentMethod: "lightning",
|
||||
status: "pay",
|
||||
amount: presetAmounts[0].toString(),
|
||||
prevChannelIds: channels.map((channel) => channel.id),
|
||||
});
|
||||
|
||||
const [showAdvanced, setShowAdvanced] = React.useState(false);
|
||||
|
|
@ -175,10 +188,19 @@ function NewChannelInternal({ network }: { network: Network }) {
|
|||
<AppHeader
|
||||
title="Increase Receiving Capacity"
|
||||
description="Purchase a channel with incoming capacity to receive payments"
|
||||
contentRight={
|
||||
<div className="flex items-end">
|
||||
<Link to="/channels/outgoing">
|
||||
<Button className="w-full" variant="secondary">
|
||||
Need spending capacity?
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<form
|
||||
onSubmit={onSubmit}
|
||||
className="md:max-w-md max-w-full flex flex-col gap-5"
|
||||
className="md:max-w-md max-w-full flex flex-col gap-5 flex-1"
|
||||
>
|
||||
<div className="grid gap-1.5">
|
||||
<Label htmlFor="amount">Channel size (sats)</Label>
|
||||
|
|
@ -312,7 +334,7 @@ function NewChannelInternal({ network }: { network: Network }) {
|
|||
Public Channel
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Enable if you want to receive keysend payments. (e.g.
|
||||
Only enable if you want to receive keysend payments. (e.g.
|
||||
podcasting)
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -331,6 +353,21 @@ function NewChannelInternal({ network }: { network: Network }) {
|
|||
</Button>
|
||||
)}
|
||||
<Button size="lg">Next</Button>
|
||||
<div className="flex-1 flex flex-col justify-end items-center gap-4">
|
||||
<p className="mt-32 text-sm text-muted-foreground text-center">
|
||||
Other options
|
||||
</p>
|
||||
<Link to="/channels/outgoing" className="w-full">
|
||||
<Button className="w-full" variant="secondary">
|
||||
Increase spending balance
|
||||
</Button>
|
||||
</Link>
|
||||
<ExternalLink to="https://www.getalby.com/topup" className="w-full">
|
||||
<Button className="w-full" variant="secondary">
|
||||
Buy Bitcoin
|
||||
</Button>
|
||||
</ExternalLink>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -210,7 +210,7 @@ function NewChannelInternal({ network }: { network: Network }) {
|
|||
/>
|
||||
<form
|
||||
onSubmit={onSubmit}
|
||||
className="md:max-w-md max-w-full flex flex-col gap-5"
|
||||
className="md:max-w-md max-w-full flex flex-col gap-5 flex-1"
|
||||
>
|
||||
<div className="grid gap-1.5">
|
||||
<Label htmlFor="amount">Channel size (sats)</Label>
|
||||
|
|
@ -369,6 +369,22 @@ function NewChannelInternal({ network }: { network: Network }) {
|
|||
</Button>
|
||||
)}
|
||||
<Button size="lg">{openImmediately ? "Open Channel" : "Next"}</Button>
|
||||
|
||||
<div className="flex-1 flex flex-col justify-end items-center gap-4">
|
||||
<p className="mt-32 text-sm text-muted-foreground text-center">
|
||||
Other options
|
||||
</p>
|
||||
<Link to="/channels/incoming" className="w-full">
|
||||
<Button className="w-full" variant="secondary">
|
||||
Increase receiving capacity
|
||||
</Button>
|
||||
</Link>
|
||||
<ExternalLink to="https://www.getalby.com/topup" className="w-full">
|
||||
<Button className="w-full" variant="secondary">
|
||||
Buy Bitcoin
|
||||
</Button>
|
||||
</ExternalLink>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
200
frontend/src/screens/channels/first/FirstChannel.tsx
Normal file
200
frontend/src/screens/channels/first/FirstChannel.tsx
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
import { Payment } from "@getalby/bitcoin-connect-react";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import React from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import AppHeader from "src/components/AppHeader";
|
||||
import ExternalLink from "src/components/ExternalLink";
|
||||
import Loading from "src/components/Loading";
|
||||
import { Button } from "src/components/ui/button";
|
||||
import { Checkbox } from "src/components/ui/checkbox";
|
||||
import { Label } from "src/components/ui/label";
|
||||
import { LoadingButton } from "src/components/ui/loading-button";
|
||||
import { Separator } from "src/components/ui/separator";
|
||||
import { useToast } from "src/components/ui/use-toast";
|
||||
import { useAlbyBalance } from "src/hooks/useAlbyBalance";
|
||||
import { useChannels } from "src/hooks/useChannels";
|
||||
import { useCSRF } from "src/hooks/useCSRF";
|
||||
import { useInfo } from "src/hooks/useInfo";
|
||||
import { AutoChannelRequest, AutoChannelResponse } from "src/types";
|
||||
import { request } from "src/utils/request";
|
||||
|
||||
import { ALBY_MIN_HOSTED_BALANCE_FOR_FIRST_CHANNEL } from "src/constants";
|
||||
import lightningNetworkDark from "/images/illustrations/lightning-network-dark.svg";
|
||||
import lightningNetworkLight from "/images/illustrations/lightning-network-light.svg";
|
||||
|
||||
export function FirstChannel() {
|
||||
const { data: info } = useInfo();
|
||||
const { data: channels } = useChannels(true);
|
||||
const [isLoading, setLoading] = React.useState(false);
|
||||
const [showAdvanced, setShowAdvanced] = React.useState(false);
|
||||
const [isPublic, setPublic] = React.useState(false);
|
||||
const { data: csrf } = useCSRF();
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
const [invoice, setInvoice] = React.useState<string>();
|
||||
const [channelSize, setChannelSize] = React.useState<number>();
|
||||
const { data: albyBalance } = useAlbyBalance();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (channels?.length) {
|
||||
navigate("/channels/first/opening");
|
||||
}
|
||||
}, [channels, navigate]);
|
||||
|
||||
if (!info || !channels) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
async function openChannel() {
|
||||
if (!info || !channels || !csrf) {
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const newInstantChannelInvoiceRequest: AutoChannelRequest = {
|
||||
isPublic,
|
||||
};
|
||||
const autoChannelResponse = await request<AutoChannelResponse>(
|
||||
"/api/alby/auto-channel",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-CSRF-Token": csrf,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(newInstantChannelInvoiceRequest),
|
||||
}
|
||||
);
|
||||
if (!autoChannelResponse) {
|
||||
throw new Error("unexpected auto channel response");
|
||||
}
|
||||
|
||||
setInvoice(autoChannelResponse.invoice);
|
||||
setChannelSize(autoChannelResponse.channelSize);
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
console.error(error);
|
||||
toast({
|
||||
title: "Something went wrong. Please try again",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const canPayForFirstChannel =
|
||||
albyBalance &&
|
||||
albyBalance.sats >= ALBY_MIN_HOSTED_BALANCE_FOR_FIRST_CHANNEL;
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppHeader
|
||||
title="Open Your First Channel"
|
||||
description="Open a channel to another lightning network node to join the lightning network"
|
||||
/>
|
||||
{invoice && channelSize && (
|
||||
<div className="flex flex-col gap-4 items-center justify-center max-w-md">
|
||||
<p className="text-muted-foreground">
|
||||
Please pay the lightning invoice below which will cover the costs of
|
||||
opening your first channel. You will receive a channel with{" "}
|
||||
{new Intl.NumberFormat().format(channelSize)} sats of incoming
|
||||
liquidity.
|
||||
</p>
|
||||
<Payment invoice={invoice} paymentMethods="external" />
|
||||
|
||||
<Separator className="mt-8" />
|
||||
<p className="mt-8 text-sm mb-2 text-muted-foreground">
|
||||
Other options
|
||||
</p>
|
||||
<Link to="/channels/outgoing" className="w-full">
|
||||
<Button className="w-full" variant="secondary">
|
||||
Open Channel with On-Chain Bitcoin
|
||||
</Button>
|
||||
</Link>
|
||||
<ExternalLink to="https://www.getalby.com/topup" className="w-full">
|
||||
<Button className="w-full" variant="secondary">
|
||||
Buy Bitcoin
|
||||
</Button>
|
||||
</ExternalLink>
|
||||
</div>
|
||||
)}
|
||||
{!invoice && (
|
||||
<>
|
||||
<div className="flex flex-col gap-6 max-w-md text-muted-foreground">
|
||||
<img
|
||||
src={lightningNetworkDark}
|
||||
className="w-full hidden dark:block"
|
||||
/>
|
||||
<img src={lightningNetworkLight} className="w-full dark:hidden" />
|
||||
{canPayForFirstChannel ? (
|
||||
<>
|
||||
<p>
|
||||
Your Alby hosted balance currently holds{" "}
|
||||
<span className="font-medium text-foreground">
|
||||
{new Intl.NumberFormat().format(albyBalance?.sats)} sats
|
||||
</span>
|
||||
.
|
||||
</p>
|
||||
<p>
|
||||
Those funds will be used to open your first lightning channel
|
||||
and then migrated to your Hub spending balance.
|
||||
</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 able to receive and send bitcoin with your
|
||||
Hub.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{showAdvanced && (
|
||||
<>
|
||||
<div className="mt-2 flex items-top space-x-2">
|
||||
<Checkbox
|
||||
id="public-channel"
|
||||
onCheckedChange={() => setPublic(!isPublic)}
|
||||
className="mr-2"
|
||||
/>
|
||||
<div className="grid gap-1.5 leading-none">
|
||||
<Label
|
||||
htmlFor="public-channel"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
Public Channel
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Only enable if you want to receive keysend payments. (e.g.
|
||||
podcasting)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!showAdvanced && (
|
||||
<div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="link"
|
||||
className="text-muted-foreground text-xs px-0"
|
||||
onClick={() => setShowAdvanced((current) => !current)}
|
||||
>
|
||||
Advanced Options
|
||||
<ChevronDown className="w-4 h-4 ml-1" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<LoadingButton loading={isLoading} onClick={openChannel}>
|
||||
Open Channel
|
||||
{albyBalance && albyBalance?.sats > 0 && <> and Migrate Funds</>}
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
99
frontend/src/screens/channels/first/OpenedFirstChannel.tsx
Normal file
99
frontend/src/screens/channels/first/OpenedFirstChannel.tsx
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import confetti from "canvas-confetti";
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import ExternalLink from "src/components/ExternalLink";
|
||||
import TwoColumnLayoutHeader from "src/components/TwoColumnLayoutHeader";
|
||||
import { Button } from "src/components/ui/button";
|
||||
import { useToast } from "src/components/ui/use-toast";
|
||||
import { ALBY_HIDE_HOSTED_BALANCE_BELOW } from "src/constants";
|
||||
import { useAlbyBalance } from "src/hooks/useAlbyBalance";
|
||||
import { useCSRF } from "src/hooks/useCSRF";
|
||||
import { request } from "src/utils/request";
|
||||
|
||||
export function OpenedFirstChannel() {
|
||||
const { data: csrf } = useCSRF();
|
||||
const { data: albyBalance, mutate: reloadAlbyBalance } = useAlbyBalance();
|
||||
const [, setShowedAlbyMigrationToast] = React.useState(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
// automatically drain Alby balance into new channel if possible
|
||||
// TODO: remove this code once all Alby users have migrated to Alby Hub
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
if (
|
||||
!csrf ||
|
||||
!albyBalance ||
|
||||
albyBalance.sats < ALBY_HIDE_HOSTED_BALANCE_BELOW
|
||||
) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await request("/api/alby/drain", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-CSRF-Token": csrf,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
await reloadAlbyBalance();
|
||||
// This may run multiple times (to drain the final 1%), but we should only show a toast once
|
||||
setShowedAlbyMigrationToast((current) => {
|
||||
if (!current) {
|
||||
toast({
|
||||
description:
|
||||
"🎉 Funds from Alby shared wallet transferred to your Alby Hub!",
|
||||
});
|
||||
}
|
||||
return true;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to transfer any alby shared wallet funds", error);
|
||||
}
|
||||
})();
|
||||
}, [albyBalance, csrf, reloadAlbyBalance, toast]);
|
||||
|
||||
React.useEffect(() => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
setTimeout(
|
||||
() => {
|
||||
confetti({
|
||||
origin: {
|
||||
x: Math.random(),
|
||||
y: Math.random(),
|
||||
},
|
||||
colors: ["#000", "#333", "#666", "#999", "#BBB", "#FFF"],
|
||||
});
|
||||
},
|
||||
Math.floor(Math.random() * 1000)
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-center gap-5 p-5 max-w-md items-stretch">
|
||||
<TwoColumnLayoutHeader
|
||||
title="Channel Opened"
|
||||
description="Your new lightning channel is ready to use"
|
||||
/>
|
||||
|
||||
<p>
|
||||
Congratulations! Your first lightning channel is active and can be used
|
||||
to send and receive payments.
|
||||
</p>
|
||||
<p>
|
||||
To ensure you can both send and receive, make sure to balance your{" "}
|
||||
<ExternalLink
|
||||
to="https://guides.getalby.com/user-guide/v/alby-account-and-browser-extension/alby-hub/liquidity"
|
||||
className="underline"
|
||||
>
|
||||
channel's liquidity
|
||||
</ExternalLink>
|
||||
.
|
||||
</p>
|
||||
|
||||
<Link to="/wallet" className="flex justify-center mt-8">
|
||||
<Button>Go To Your Wallet</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
57
frontend/src/screens/channels/first/OpeningFirstChannel.tsx
Normal file
57
frontend/src/screens/channels/first/OpeningFirstChannel.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Loading from "src/components/Loading";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "src/components/ui/card";
|
||||
import { useChannels } from "src/hooks/useChannels";
|
||||
import { useSyncWallet } from "src/hooks/useSyncWallet";
|
||||
|
||||
export function OpeningFirstChannel() {
|
||||
useSyncWallet();
|
||||
const { data: channels } = useChannels(true);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const firstChannel = channels?.[0];
|
||||
|
||||
React.useEffect(() => {
|
||||
if (firstChannel?.active) {
|
||||
navigate("/channels/first/opened");
|
||||
}
|
||||
}, [firstChannel, navigate]);
|
||||
|
||||
if (!firstChannel || !firstChannel.confirmationsRequired) {
|
||||
// 0-conf channel, this should only take a few seconds
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col justify-center gap-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Your channel is being opened</CardTitle>
|
||||
<CardDescription>
|
||||
Waiting for {firstChannel.confirmationsRequired} confirmations
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-row gap-2">
|
||||
<Loading />
|
||||
{firstChannel.confirmations ?? "0"} /{" "}
|
||||
{firstChannel.confirmationsRequired ?? "unknown"} confirmations
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="w-full mt-40 gap-20 flex flex-col items-center justify-center">
|
||||
<p>Feel free to leave this page or browse around Alby Hub!</p>
|
||||
<p>We'll send you an email as soon as your channel is active.</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,250 +0,0 @@
|
|||
import { AlertTriangle } from "lucide-react";
|
||||
import React from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import Loading from "src/components/Loading";
|
||||
import TwoColumnLayoutHeader from "src/components/TwoColumnLayoutHeader";
|
||||
import { Alert, AlertDescription, AlertTitle } from "src/components/ui/alert";
|
||||
import { Button } from "src/components/ui/button";
|
||||
import { LoadingButton } from "src/components/ui/loading-button";
|
||||
import { Table, TableBody, TableCell, TableRow } from "src/components/ui/table";
|
||||
import { useToast } from "src/components/ui/use-toast";
|
||||
import { ALBY_MIN_BALANCE, ALBY_SERVICE_FEE } from "src/constants";
|
||||
import { useAlbyBalance } from "src/hooks/useAlbyBalance";
|
||||
import { useAlbyMe } from "src/hooks/useAlbyMe";
|
||||
import { useCSRF } from "src/hooks/useCSRF";
|
||||
import { useChannels } from "src/hooks/useChannels";
|
||||
import { useInfo } from "src/hooks/useInfo";
|
||||
import {
|
||||
NewInstantChannelInvoiceRequest,
|
||||
NewInstantChannelInvoiceResponse,
|
||||
} from "src/types";
|
||||
import { handleRequestError } from "src/utils/handleRequestError";
|
||||
import { request } from "src/utils/request";
|
||||
|
||||
export default function MigrateAlbyFunds() {
|
||||
const { data: albyMe } = useAlbyMe();
|
||||
const { data: albyBalance } = useAlbyBalance();
|
||||
const { data: csrf } = useCSRF();
|
||||
const { data: info } = useInfo();
|
||||
const { data: channels } = useChannels(true);
|
||||
const { mutate: refetchInfo } = useInfo();
|
||||
const { toast } = useToast();
|
||||
const [prePurchaseChannelCount, setPrePurchaseChannelCount] = React.useState<
|
||||
number | undefined
|
||||
>();
|
||||
const [error, setError] = React.useState("");
|
||||
const [hasRequestedInvoice, setRequestedInvoice] = React.useState(false);
|
||||
const [isOpeningChannel, setOpeningChannel] = React.useState(false);
|
||||
const navigate = useNavigate();
|
||||
const [amount, setAmount] = React.useState<number>(0);
|
||||
|
||||
const [instantChannelResponse, setInstantChannelResponse] = React.useState<
|
||||
NewInstantChannelInvoiceResponse | undefined
|
||||
>();
|
||||
|
||||
const requestWrappedInvoice = React.useCallback(
|
||||
async (amount: number) => {
|
||||
try {
|
||||
if (!info) {
|
||||
throw new Error("Info not loaded");
|
||||
}
|
||||
// other node implementations may not work / may not support 0-conf.
|
||||
// so for now we are not allowing other backend types.
|
||||
// They can open a channel with a different method and then migrate
|
||||
// their shared funds once they have enough receiving capacity.
|
||||
if (info.backendType !== "LDK") {
|
||||
throw new Error("Only LDK backend is supported");
|
||||
}
|
||||
|
||||
if (!channels) {
|
||||
throw new Error("Channels not loaded");
|
||||
}
|
||||
setPrePurchaseChannelCount(channels.length);
|
||||
if (!csrf) {
|
||||
throw new Error("csrf not loaded");
|
||||
}
|
||||
const newInstantChannelInvoiceRequest: NewInstantChannelInvoiceRequest =
|
||||
{
|
||||
lspUrl: "https://lsp.albylabs.com",
|
||||
lspType: "PMLSP",
|
||||
amount,
|
||||
public: false,
|
||||
};
|
||||
const response = await request<NewInstantChannelInvoiceResponse>(
|
||||
"/api/instant-channel-invoices",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-CSRF-Token": csrf,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(newInstantChannelInvoiceRequest),
|
||||
}
|
||||
);
|
||||
if (!response?.invoice) {
|
||||
throw new Error("No invoice in response");
|
||||
}
|
||||
setInstantChannelResponse(response);
|
||||
} catch (error) {
|
||||
setError("Failed to connect to request wrapped invoice: " + error);
|
||||
}
|
||||
},
|
||||
[channels, csrf, info]
|
||||
);
|
||||
|
||||
const payWrappedInvoice = React.useCallback(
|
||||
async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
if (!instantChannelResponse) {
|
||||
throw new Error("No invoice");
|
||||
}
|
||||
if (!csrf) {
|
||||
throw new Error("No csrf token");
|
||||
}
|
||||
setOpeningChannel(true);
|
||||
await request("/api/alby/pay", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-CSRF-Token": csrf,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
invoice: instantChannelResponse.invoice,
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
handleRequestError(
|
||||
toast,
|
||||
"Failed to pay channel funding invoice",
|
||||
error
|
||||
);
|
||||
setOpeningChannel(false);
|
||||
}
|
||||
},
|
||||
[csrf, toast, instantChannelResponse]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (hasRequestedInvoice || !info || !channels || !albyMe || !albyBalance) {
|
||||
return;
|
||||
}
|
||||
setRequestedInvoice(true);
|
||||
const _amount = Math.floor(albyBalance.sats * (1 - ALBY_SERVICE_FEE));
|
||||
setAmount(_amount);
|
||||
if (_amount < ALBY_MIN_BALANCE) {
|
||||
return;
|
||||
}
|
||||
requestWrappedInvoice(_amount);
|
||||
}, [
|
||||
hasRequestedInvoice,
|
||||
albyBalance,
|
||||
channels,
|
||||
albyMe,
|
||||
info,
|
||||
requestWrappedInvoice,
|
||||
]);
|
||||
|
||||
const hasOpenedChannel =
|
||||
channels &&
|
||||
prePurchaseChannelCount !== undefined &&
|
||||
channels.length > prePurchaseChannelCount;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (hasOpenedChannel) {
|
||||
(async () => {
|
||||
toast({ title: "Successfully opened channel" });
|
||||
await refetchInfo();
|
||||
navigate("/onboarding/success");
|
||||
})();
|
||||
}
|
||||
}, [hasOpenedChannel, navigate, refetchInfo, toast]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-center gap-5 p-5 max-w-md items-stretch">
|
||||
<TwoColumnLayoutHeader
|
||||
title="Open a Channel"
|
||||
description="You can use your remaining balance on Alby hosted lightning wallet to
|
||||
fund your first lightning channel."
|
||||
/>
|
||||
{error ? (
|
||||
<>
|
||||
<Alert>
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertTitle>Error requesting wrapped invoice</AlertTitle>
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
</>
|
||||
) : !albyMe ||
|
||||
!albyBalance ||
|
||||
!channels ||
|
||||
(amount >= ALBY_MIN_BALANCE && !instantChannelResponse) ? (
|
||||
<Loading className="mx-auto" />
|
||||
) : instantChannelResponse &&
|
||||
amount - instantChannelResponse.fee >= ALBY_MIN_BALANCE ? (
|
||||
<>
|
||||
<div className="border rounded-lg">
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow className="border-b-0">
|
||||
<TableCell className="font-medium p-3">
|
||||
Current Account balance
|
||||
</TableCell>
|
||||
<TableCell className="text-right p-3">
|
||||
{new Intl.NumberFormat().format(albyBalance.sats)} sats
|
||||
</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(
|
||||
albyBalance.sats - amount + instantChannelResponse.fee
|
||||
)}{" "}
|
||||
sats
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="font-medium p-3">
|
||||
Alby Hub Balance
|
||||
</TableCell>
|
||||
<TableCell className="font-semibold text-right p-3">
|
||||
{new Intl.NumberFormat().format(
|
||||
amount - instantChannelResponse.fee
|
||||
)}{" "}
|
||||
sats
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<form className="flex flex-col justify-between text-center gap-2">
|
||||
<LoadingButton
|
||||
onClick={payWrappedInvoice}
|
||||
disabled={isOpeningChannel}
|
||||
loading={isOpeningChannel}
|
||||
>
|
||||
Migrate Funds and Open Channel
|
||||
</LoadingButton>
|
||||
</form>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Alert>
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertTitle>Not enough funds available!</AlertTitle>
|
||||
<AlertDescription>
|
||||
You don't have enough funds in your Alby account to fund a new
|
||||
channel right now. You can open a channel manually and pay with an
|
||||
external wallet though.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<Link to="/channels" className="w-full">
|
||||
<Button className="w-full">Explore Other Options</Button>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ export function Success() {
|
|||
Math.floor(Math.random() * 1000)
|
||||
);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-center gap-5 p-5 max-w-md items-stretch">
|
||||
|
|
|
|||
|
|
@ -283,7 +283,7 @@ export type SetupNodeInfo = Partial<{
|
|||
phoenixdAuthorization?: string;
|
||||
}>;
|
||||
|
||||
export type LSPType = "LSPS1" | "Flow 2.0" | "PMLSP";
|
||||
export type LSPType = "LSPS1";
|
||||
|
||||
export type RecommendedChannelPeer = {
|
||||
network: Network;
|
||||
|
|
@ -324,14 +324,14 @@ export type AlbyBalance = {
|
|||
sats: number;
|
||||
};
|
||||
|
||||
export type NewInstantChannelInvoiceRequest = {
|
||||
export type LSPOrderRequest = {
|
||||
amount: number;
|
||||
lspType: LSPType;
|
||||
lspUrl: string;
|
||||
public: boolean;
|
||||
};
|
||||
|
||||
export type NewInstantChannelInvoiceResponse = {
|
||||
export type LSPOrderResponse = {
|
||||
invoice: string;
|
||||
fee: number;
|
||||
invoiceAmount: number;
|
||||
|
|
@ -339,6 +339,15 @@ export type NewInstantChannelInvoiceResponse = {
|
|||
outgoingLiquidity: number;
|
||||
};
|
||||
|
||||
export type AutoChannelRequest = {
|
||||
isPublic: boolean;
|
||||
};
|
||||
export type AutoChannelResponse = {
|
||||
invoice?: string;
|
||||
fee?: number;
|
||||
channelSize: number;
|
||||
};
|
||||
|
||||
export type RedeemOnchainFundsResponse = {
|
||||
txId: string;
|
||||
};
|
||||
|
|
@ -372,13 +381,14 @@ export type Transaction = {
|
|||
metadata: unknown;
|
||||
};
|
||||
|
||||
export type NewChannelOrderStatus = "pay" | "success" | "opening";
|
||||
export type NewChannelOrderStatus = "pay" | "paid" | "success" | "opening";
|
||||
|
||||
export type NewChannelOrder = {
|
||||
amount: string;
|
||||
isPublic: boolean;
|
||||
status: NewChannelOrderStatus;
|
||||
fundingTxId?: string;
|
||||
prevChannelIds: string[];
|
||||
} & (
|
||||
| {
|
||||
paymentMethod: "onchain";
|
||||
|
|
|
|||
|
|
@ -32,6 +32,28 @@ func (albyHttpSvc *AlbyHttpService) RegisterSharedRoutes(e *echo.Echo, authMiddl
|
|||
e.POST("/api/alby/pay", albyHttpSvc.albyPayHandler, authMiddleware)
|
||||
e.POST("/api/alby/drain", albyHttpSvc.albyDrainHandler, authMiddleware)
|
||||
e.POST("/api/alby/link-account", albyHttpSvc.albyLinkAccountHandler, authMiddleware)
|
||||
e.POST("/api/alby/auto-channel", albyHttpSvc.autoChannelHandler, authMiddleware)
|
||||
}
|
||||
|
||||
func (albyHttpSvc *AlbyHttpService) autoChannelHandler(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
var autoChannelRequest alby.AutoChannelRequest
|
||||
if err := c.Bind(&autoChannelRequest); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
||||
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
||||
})
|
||||
}
|
||||
|
||||
autoChannelResponseResponse, err := albyHttpSvc.albyOAuthSvc.RequestAutoChannel(ctx, albyHttpSvc.svc.GetLNClient(), autoChannelRequest.IsPublic)
|
||||
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
||||
Message: fmt.Sprintf("Failed to request wrapped invoice: %s", err.Error()),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, autoChannelResponseResponse)
|
||||
}
|
||||
|
||||
func (albyHttpSvc *AlbyHttpService) albyCallbackHandler(c echo.Context) error {
|
||||
|
|
|
|||
|
|
@ -91,8 +91,7 @@ func (httpSvc *HttpService) RegisterSharedRoutes(e *echo.Echo) {
|
|||
e.GET("/api/channels", httpSvc.channelsListHandler, authMiddleware)
|
||||
e.POST("/api/channels", httpSvc.openChannelHandler, authMiddleware)
|
||||
e.GET("/api/channels/suggestions", httpSvc.channelPeerSuggestionsHandler, authMiddleware)
|
||||
// TODO: review naming
|
||||
e.POST("/api/instant-channel-invoices", httpSvc.newInstantChannelInvoiceHandler, authMiddleware)
|
||||
e.POST("/api/lsp-orders", httpSvc.newInstantChannelInvoiceHandler, authMiddleware)
|
||||
e.GET("/api/node/connection-info", httpSvc.nodeConnectionInfoHandler, authMiddleware)
|
||||
e.GET("/api/node/status", httpSvc.nodeStatusHandler, authMiddleware)
|
||||
e.GET("/api/node/network-graph", httpSvc.nodeNetworkGraphHandler, authMiddleware)
|
||||
|
|
@ -625,14 +624,14 @@ func (httpSvc *HttpService) updateChannelHandler(c echo.Context) error {
|
|||
func (httpSvc *HttpService) newInstantChannelInvoiceHandler(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
var newWrappedInvoiceRequest api.NewInstantChannelInvoiceRequest
|
||||
var newWrappedInvoiceRequest api.LSPOrderRequest
|
||||
if err := c.Bind(&newWrappedInvoiceRequest); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
||||
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
||||
})
|
||||
}
|
||||
|
||||
newWrappedInvoiceResponse, err := httpSvc.api.NewInstantChannelInvoice(ctx, &newWrappedInvoiceRequest)
|
||||
newWrappedInvoiceResponse, err := httpSvc.api.RequestLSPOrder(ctx, &newWrappedInvoiceRequest)
|
||||
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
||||
|
|
|
|||
|
|
@ -816,7 +816,7 @@ func (ls *LDKService) ListChannels(ctx context.Context) ([]lnclient.Channel, err
|
|||
channelError = &channelErrorValue
|
||||
}
|
||||
|
||||
isActive := ldkChannel.IsUsable /* superset of ldkChannel.IsReady */ && channelError == nil
|
||||
isActive := ldkChannel.IsUsable /* superset of ldkChannel.IsReady */ && channelError == nil && fundingTxId != ""
|
||||
|
||||
channels = append(channels, lnclient.Channel{
|
||||
InternalChannel: internalChannel,
|
||||
|
|
|
|||
|
|
@ -5,9 +5,7 @@ type LSP struct {
|
|||
}
|
||||
|
||||
const (
|
||||
LSP_TYPE_FLOW_2_0 = "Flow 2.0"
|
||||
LSP_TYPE_PMLSP = "PMLSP"
|
||||
LSP_TYPE_LSPS1 = "LSPS1"
|
||||
LSP_TYPE_LSPS1 = "LSPS1"
|
||||
)
|
||||
|
||||
func OlympusMutinynetLSP() LSP {
|
||||
|
|
|
|||
|
|
@ -450,11 +450,10 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string
|
|||
return WailsRequestRouterResponse{Body: nil, Error: err.Error()}
|
||||
}
|
||||
return WailsRequestRouterResponse{Body: *capabilitiesResponse, Error: ""}
|
||||
// TODO: review naming
|
||||
case "/api/instant-channel-invoices":
|
||||
newInstantChannelRequest := &api.NewInstantChannelInvoiceRequest{}
|
||||
case "/api/lsp-orders":
|
||||
newInstantChannelRequest := &api.LSPOrderRequest{}
|
||||
err := json.Unmarshal([]byte(body), newInstantChannelRequest)
|
||||
newInstantChannelResponse, err := app.api.NewInstantChannelInvoice(ctx, newInstantChannelRequest)
|
||||
newInstantChannelResponse, err := app.api.RequestLSPOrder(ctx, newInstantChannelRequest)
|
||||
if err != nil {
|
||||
return WailsRequestRouterResponse{Body: nil, Error: err.Error()}
|
||||
}
|
||||
|
|
@ -504,6 +503,24 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string
|
|||
infoResponse.Unlocked = infoResponse.Running
|
||||
res := WailsRequestRouterResponse{Body: *infoResponse, Error: ""}
|
||||
return res
|
||||
case "/api/alby/auto-channel":
|
||||
newAutoChannelRequest := &alby.AutoChannelRequest{}
|
||||
err := json.Unmarshal([]byte(body), newAutoChannelRequest)
|
||||
if err != nil {
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"route": route,
|
||||
"method": method,
|
||||
"body": body,
|
||||
}).WithError(err).Error("Failed to decode request to wails router")
|
||||
return WailsRequestRouterResponse{Body: nil, Error: err.Error()}
|
||||
}
|
||||
autoChannelResponse, err := app.svc.GetAlbyOAuthSvc().RequestAutoChannel(ctx, app.svc.GetLNClient(), newAutoChannelRequest.IsPublic)
|
||||
if err != nil {
|
||||
return WailsRequestRouterResponse{Body: *autoChannelResponse, Error: err.Error()}
|
||||
}
|
||||
res := WailsRequestRouterResponse{Error: ""}
|
||||
return res
|
||||
|
||||
case "/api/alby/link-account":
|
||||
linkAccountRequest := &alby.AlbyLinkAccountRequest{}
|
||||
err := json.Unmarshal([]byte(body), linkAccountRequest)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue