feat: request channel info + orders from alby oauth endpoint (#1714)

* feat: request channel info + orders from alby oauth endpoint

* fix: tests

* chore: use identifier instead of url

* chore: change timeout to 30s

* fix: handle optional invoice when requesting lsp order

* chore: minor code cleanup, fixed incorrect error messages

---------

Co-authored-by: Roland Bewick <roland.bewick@gmail.com>
This commit is contained in:
Adithya Vardhan 2025-09-18 11:34:46 +05:30 committed by GitHub
parent 0be4f9b135
commit 55e5477076
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 250 additions and 332 deletions

View file

@ -1152,8 +1152,8 @@ func (svc *albyOAuthService) GetLSPChannelOffer(ctx context.Context) (*LSPChanne
logger.Logger.WithFields(logrus.Fields{
"body": string(body),
"status_code": res.StatusCode,
}).Error("users endpoint returned non-success code")
return nil, fmt.Errorf("users endpoint returned non-success code: %s", string(body))
}).Error("lsp channel offer endpoint returned non-success code")
return nil, fmt.Errorf("lsp channel offer endpoint returned non-success code: %s", string(body))
}
lspChannelOffer := &LSPChannelOffer{}
@ -1166,6 +1166,157 @@ func (svc *albyOAuthService) GetLSPChannelOffer(ctx context.Context) (*LSPChanne
return lspChannelOffer, nil
}
func (svc *albyOAuthService) GetLSPInfo(ctx context.Context, lspIdentifier, network string) (*LSPInfo, error) {
token, err := svc.fetchUserToken(ctx)
if err != nil {
logger.Logger.WithError(err).Error("Failed to fetch user token")
return nil, err
}
var client *http.Client
if token != nil {
client = svc.oauthConf.Client(ctx, token)
} else {
client = &http.Client{}
}
client.Timeout = 30 * time.Second
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/internal/lsp/%s/%s/v1/get_info", albyOAuthAPIURL, lspIdentifier, network), nil)
if err != nil {
logger.Logger.WithError(err).Error("Failed to create lsp info request")
return nil, err
}
setDefaultRequestHeaders(req)
res, err := client.Do(req)
if err != nil {
logger.Logger.WithError(err).Error("Failed to request lsp info")
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("lsp info endpoint returned non-success code")
return nil, fmt.Errorf("lsp info endpoint returned non-success code: %s", string(body))
}
type lsps1LSPInfo struct {
MinRequiredChannelConfirmations uint64 `json:"min_required_channel_confirmations"`
MinFundingConfirmsWithinBlocks uint64 `json:"min_funding_confirms_within_blocks"`
MaxChannelExpiryBlocks uint64 `json:"max_channel_expiry_blocks"`
URIs []string `json:"uris"`
}
lsps1LspInfo := &lsps1LSPInfo{}
err = json.Unmarshal(body, lsps1LspInfo)
if err != nil {
logger.Logger.WithError(err).Error("Failed to decode API response")
return nil, err
}
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 nil, 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 nil, errors.New("could not decode LSP URI")
}
port, err := strconv.Atoi(parts[3])
if err != nil {
logger.Logger.WithField("port", parts[3]).WithError(err).Error("Failed to decode port number")
return nil, err
}
return &LSPInfo{
Pubkey: parts[1],
Address: parts[2],
Port: uint16(port),
MaxChannelExpiryBlocks: lsps1LspInfo.MaxChannelExpiryBlocks,
MinRequiredChannelConfirmations: lsps1LspInfo.MinRequiredChannelConfirmations,
MinFundingConfirmsWithinBlocks: lsps1LspInfo.MinFundingConfirmsWithinBlocks,
}, nil
}
func (svc *albyOAuthService) CreateLSPOrder(ctx context.Context, lsp, network string, lspChannelRequest *LSPChannelRequest) (*LSPChannelResponse, error) {
token, err := svc.fetchUserToken(ctx)
if err != nil {
logger.Logger.WithError(err).Error("Failed to fetch user token")
return nil, err
}
var client *http.Client
if token != nil {
client = svc.oauthConf.Client(ctx, token)
} else {
client = &http.Client{}
}
client.Timeout = 30 * time.Second
payloadBytes, err := json.Marshal(lspChannelRequest)
if err != nil {
return nil, err
}
bodyReader := bytes.NewReader(payloadBytes)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("%s/internal/lsp/%s/%s/v1/create_order", albyOAuthAPIURL, lsp, network), bodyReader)
if err != nil {
logger.Logger.WithError(err).Error("Failed to create lsp order request")
return nil, err
}
setDefaultRequestHeaders(req)
res, err := client.Do(req)
if err != nil {
logger.Logger.WithError(err).Error("Failed to request lsp order")
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("lsp create order endpoint returned non-success code")
return nil, fmt.Errorf("lsp create order endpoint returned non-success code: %s", string(body))
}
channelResponse := &LSPChannelResponse{}
err = json.Unmarshal(body, channelResponse)
if err != nil {
logger.Logger.WithError(err).Error("Failed to decode API response")
return nil, err
}
return channelResponse, nil
}
func (svc *albyOAuthService) RequestAutoChannel(ctx context.Context, lnClient lnclient.LNClient, isPublic bool) (*AutoChannelResponse, error) {
nodeInfo, err := lnClient.GetInfo(ctx)
if err != nil {
@ -1173,9 +1324,7 @@ func (svc *albyOAuthService) RequestAutoChannel(ctx context.Context, lnClient ln
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")
lspInfo, err := svc.GetLSPInfo(ctx, "alby", nodeInfo.Network)
if err != nil {
logger.Logger.WithError(err).Error("Failed to request LSP info")
@ -1183,26 +1332,26 @@ func (svc *albyOAuthService) RequestAutoChannel(ctx context.Context, lnClient ln
}
err = lnClient.ConnectPeer(ctx, &lnclient.ConnectPeerRequest{
Pubkey: pubkey,
Address: address,
Port: port,
Pubkey: lspInfo.Pubkey,
Address: lspInfo.Address,
Port: lspInfo.Port,
})
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"pubkey": pubkey,
"address": address,
"port": port,
"pubkey": lspInfo.Pubkey,
"address": lspInfo.Address,
"port": lspInfo.Port,
}).WithError(err).Error("Failed to connect to peer")
return nil, err
}
logger.Logger.WithFields(logrus.Fields{
"pubkey": pubkey,
"pubkey": lspInfo.Pubkey,
"public": isPublic,
}).Info("Requesting auto channel")
autoChannelResponse, err := svc.requestAutoChannel(ctx, requestUrl+"/auto_channel", nodeInfo.Pubkey, isPublic)
autoChannelResponse, err := svc.requestAutoChannel(ctx, fmt.Sprintf("%s/internal/lsp/alby/%s/auto_channel", albyOAuthAPIURL, nodeInfo.Network), nodeInfo.Pubkey, isPublic)
if err != nil {
logger.Logger.WithError(err).Error("Failed to request auto channel")
return nil, err
@ -1345,86 +1494,6 @@ func (svc *albyOAuthService) requestAutoChannel(ctx context.Context, url string,
}, 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.NewRequestWithContext(ctx, 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).Debug("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)

View file

@ -16,6 +16,8 @@ type AlbyService interface {
type AlbyOAuthService interface {
events.EventSubscriber
GetLSPChannelOffer(ctx context.Context) (*LSPChannelOffer, error)
GetLSPInfo(ctx context.Context, lsp, network string) (*LSPInfo, error)
CreateLSPOrder(ctx context.Context, lsp, network string, lspChannelRequest *LSPChannelRequest) (*LSPChannelResponse, error)
GetAuthUrl() string
GetUserIdentifier() (string, error)
GetLightningAddress() (string, error)
@ -119,7 +121,7 @@ type ChannelPeerSuggestion struct {
MaximumChannelSize uint64 `json:"maximumChannelSize"`
Name string `json:"name"`
Image string `json:"image"`
Url string `json:"url"`
Identifier string `json:"identifier"`
ContactUrl string `json:"contactUrl"`
Type string `json:"type"`
Terms string `json:"terms"`
@ -149,6 +151,42 @@ type BitcoinRate struct {
RateFloat float64 `json:"rate_float"`
RateCents int64 `json:"rate_cents"`
}
type ErrorResponse struct {
Message string `json:"message"`
}
type LSPChannelPaymentBolt11 struct {
Invoice string `json:"invoice"`
FeeTotalSat string `json:"fee_total_sat"`
}
type LSPChannelPayment struct {
Bolt11 LSPChannelPaymentBolt11 `json:"bolt11"`
// TODO: add onchain
}
type LSPChannelResponse struct {
Payment *LSPChannelPayment `json:"payment"`
}
type LSPChannelRequest struct {
PublicKey string `json:"public_key"`
LSPBalanceSat string `json:"lsp_balance_sat"`
ClientBalanceSat string `json:"client_balance_sat"`
RequiredChannelConfirmations uint64 `json:"required_channel_confirmations"`
FundingConfirmsWithinBlocks uint64 `json:"funding_confirms_within_blocks"`
ChannelExpiryBlocks uint64 `json:"channel_expiry_blocks"`
Token string `json:"token"`
RefundOnchainAddress string `json:"refund_onchain_address"`
AnnounceChannel bool `json:"announce_channel"`
}
type LSPInfo struct {
Pubkey string
Address string
Port uint16
MaxChannelExpiryBlocks uint64
MinRequiredChannelConfirmations uint64
MinFundingConfirmsWithinBlocks uint64
}

View file

@ -1,37 +1,21 @@
package api
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"regexp"
"strconv"
"strings"
"time"
"github.com/getAlby/hub/alby"
"github.com/getAlby/hub/config"
"github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/logger"
"github.com/getAlby/hub/lsp"
"github.com/getAlby/hub/utils"
"github.com/getAlby/hub/version"
decodepay "github.com/nbd-wtf/ln-decodepay"
"github.com/sirupsen/logrus"
)
type lspInfo struct {
Pubkey string
Address string
Port uint16
MaxChannelExpiryBlocks uint64
MinRequiredChannelConfirmations uint64
MinFundingConfirmsWithinBlocks uint64
}
func (api *api) RequestLSPOrder(ctx context.Context, request *LSPOrderRequest) (*LSPOrderResponse, error) {
if api.svc.GetLNClient() == nil {
@ -42,24 +26,24 @@ func (api *api) RequestLSPOrder(ctx context.Context, request *LSPOrderRequest) (
return nil, fmt.Errorf("unsupported LSP type: %v", request.LSPType)
}
logger.Logger.Info("Requesting LSP info")
lspInfo, err := api.getLSPS1LSPInfo(ctx, request.LSPUrl+"/get_info")
if err != nil {
logger.Logger.WithError(err).Error("Failed to request LSP info")
return nil, err
}
logger.Logger.Info("Requesting own node info")
nodeInfo, err := api.svc.GetLNClient().GetInfo(ctx)
if err != nil {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"url": request.LSPUrl,
"lspIdentifier": request.LSPIdentifier,
}).Error("Failed to request own node info", err)
return nil, err
}
logger.Logger.Info("Requesting LSP info")
lspInfo, err := api.albyOAuthSvc.GetLSPInfo(ctx, request.LSPIdentifier, nodeInfo.Network)
if err != nil {
logger.Logger.WithError(err).Error("Failed to request LSP info")
return nil, err
}
logger.Logger.WithField("lspInfo", lspInfo).Info("Connecting to LSP node as a peer")
err = api.svc.GetLNClient().ConnectPeer(ctx, &lnclient.ConnectPeerRequest{
@ -73,29 +57,23 @@ func (api *api) RequestLSPOrder(ctx context.Context, request *LSPOrderRequest) (
return nil, err
}
invoice, fee, err := api.requestLSPS1Invoice(ctx, request, nodeInfo.Pubkey, lspInfo.MaxChannelExpiryBlocks, lspInfo.MinRequiredChannelConfirmations, lspInfo.MinFundingConfirmsWithinBlocks)
invoice, fee, err := api.requestLSPS1Invoice(ctx, request, nodeInfo.Network, nodeInfo.Pubkey, lspInfo.MaxChannelExpiryBlocks, lspInfo.MinRequiredChannelConfirmations, lspInfo.MinFundingConfirmsWithinBlocks)
invoiceAmount := uint64(0)
incomingLiquidity := request.Amount
if err != nil {
logger.Logger.WithError(err).Error("Failed to request invoice")
return nil, err
}
paymentRequest, err := decodepay.Decodepay(invoice)
if err != nil {
logger.Logger.WithError(err).Error("Failed to decode bolt11 invoice")
return nil, err
}
if invoice != "" {
paymentRequest, err := decodepay.Decodepay(invoice)
if err != nil {
logger.Logger.WithError(err).Error("Failed to decode bolt11 invoice")
return nil, err
}
invoiceAmount := uint64(paymentRequest.MSatoshi / 1000)
incomingLiquidity := uint64(0)
outgoingLiquidity := uint64(0)
if invoiceAmount < request.Amount {
// assume that the invoice is only the fee
// and that the user is requesting incoming liquidity (LSPS1)
incomingLiquidity = request.Amount
} else {
outgoingLiquidity = invoiceAmount - fee
invoiceAmount = uint64(paymentRequest.MSatoshi / 1000)
}
newChannelResponse := &LSPOrderResponse{
@ -103,7 +81,7 @@ func (api *api) RequestLSPOrder(ctx context.Context, request *LSPOrderRequest) (
Fee: fee,
InvoiceAmount: invoiceAmount,
IncomingLiquidity: incomingLiquidity,
OutgoingLiquidity: outgoingLiquidity,
OutgoingLiquidity: uint64(0), // JIT channel no longer supported
}
logger.Logger.WithFields(logrus.Fields{
@ -113,115 +91,7 @@ func (api *api) RequestLSPOrder(ctx context.Context, request *LSPOrderRequest) (
return newChannelResponse, nil
}
func (api *api) getLSPS1LSPInfo(ctx context.Context, url string) (*lspInfo, error) {
type lsps1LSPInfo struct {
MinRequiredChannelConfirmations uint64 `json:"min_required_channel_confirmations"`
MinFundingConfirmsWithinBlocks uint64 `json:"min_funding_confirms_within_blocks"`
MaxChannelExpiryBlocks uint64 `json:"max_channel_expiry_blocks"`
URIs []string `json:"uris"`
}
var lsps1LspInfo lsps1LSPInfo
client := http.Client{
Timeout: time.Second * 10,
}
req, err := http.NewRequestWithContext(ctx, 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
}
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 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{
"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{
"url": url,
}).Error("Failed to deserialize json")
return nil, 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 nil, 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 nil, errors.New("could not decode LSP URI")
}
port, err := strconv.Atoi(parts[3])
if err != nil {
logger.Logger.WithField("port", parts[3]).WithError(err).Error("Failed to decode port number")
return nil, err
}
return &lspInfo{
Pubkey: parts[1],
Address: parts[2],
Port: uint16(port),
MaxChannelExpiryBlocks: lsps1LspInfo.MaxChannelExpiryBlocks,
MinRequiredChannelConfirmations: lsps1LspInfo.MinRequiredChannelConfirmations,
MinFundingConfirmsWithinBlocks: lsps1LspInfo.MinFundingConfirmsWithinBlocks,
}, nil
}
func (api *api) requestLSPS1Invoice(ctx context.Context, request *LSPOrderRequest, pubkey string, channelExpiryBlocks uint64, minRequiredChannelConfirmations uint64, minFundingConfirmsWithinBlocks uint64) (invoice string, fee uint64, err error) {
client := http.Client{
Timeout: time.Second * 60,
}
type lsps1ChannelRequest struct {
PublicKey string `json:"public_key"`
LSPBalanceSat string `json:"lsp_balance_sat"`
ClientBalanceSat string `json:"client_balance_sat"`
RequiredChannelConfirmations uint64 `json:"required_channel_confirmations"`
FundingConfirmsWithinBlocks uint64 `json:"funding_confirms_within_blocks"`
ChannelExpiryBlocks uint64 `json:"channel_expiry_blocks"`
Token string `json:"token"`
RefundOnchainAddress string `json:"refund_onchain_address"`
AnnounceChannel bool `json:"announce_channel"`
}
func (api *api) requestLSPS1Invoice(ctx context.Context, request *LSPOrderRequest, network, pubkey string, channelExpiryBlocks uint64, minRequiredChannelConfirmations uint64, minFundingConfirmsWithinBlocks uint64) (invoice string, fee uint64, err error) {
refundAddress, err := api.svc.GetLNClient().GetNewOnchainAddress(ctx)
if err != nil {
logger.Logger.WithError(err).Error("Failed to request onchain address")
@ -250,17 +120,17 @@ func (api *api) requestLSPS1Invoice(ctx context.Context, request *LSPOrderReques
}
token := ""
if request.LSPUrl == "https://lsps1.lnolymp.us/api/v1" {
if request.LSPIdentifier == "olympus" {
token = "AlbyHub/" + version.Tag
}
// set a non-empty token to notify LNServer that we support 0-conf
// (Pre-v1.17.2 does not support 0-conf)
if request.LSPUrl == "https://www.lnserver.com/lsp/wave" {
if request.LSPIdentifier == "lnserver" {
token = "AlbyHub/" + version.Tag
}
newLSPS1ChannelRequest := lsps1ChannelRequest{
lsps1ChannelRequest := &alby.LSPChannelRequest{
PublicKey: pubkey,
LSPBalanceSat: strconv.FormatUint(request.Amount, 10),
ClientBalanceSat: "0",
@ -272,85 +142,19 @@ func (api *api) requestLSPS1Invoice(ctx context.Context, request *LSPOrderReques
AnnounceChannel: request.Public,
}
payloadBytes, err := json.Marshal(newLSPS1ChannelRequest)
channelResponse, err := api.albyOAuthSvc.CreateLSPOrder(ctx, request.LSPIdentifier, network, lsps1ChannelRequest)
if err != nil {
return "", 0, err
}
bodyReader := bytes.NewReader(payloadBytes)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, request.LSPUrl+"/create_order", bodyReader)
invoice = channelResponse.Payment.Bolt11.Invoice
fee, err = strconv.ParseUint(channelResponse.Payment.Bolt11.FeeTotalSat, 10, 64)
if err != nil {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"url": request.LSPUrl,
}).Error("Failed to create new channel request")
return "", 0, err
}
setDefaultRequestHeaders(req)
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{
"newLSPS1ChannelRequest": newLSPS1ChannelRequest,
"body": string(body),
"statusCode": res.StatusCode,
}).Error("create_order endpoint returned non-success code")
return "", 0, fmt.Errorf("create_order 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 newLSPS1ChannelResponse struct {
Payment *newLSPS1ChannelPayment `json:"payment"`
}
var newChannelResponse newLSPS1ChannelResponse
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.Payment.Bolt11.Invoice
fee, err = strconv.ParseUint(newChannelResponse.Payment.Bolt11.FeeTotalSat, 10, 64)
if err != nil {
logger.Logger.WithError(err).WithFields(logrus.Fields{
"url": request.LSPUrl,
"lspIdentifier": request.LSPIdentifier,
}).Error("Failed to parse fee")
return "", 0, fmt.Errorf("failed to parse fee %v", err)
}
return invoice, fee, nil
}
func setDefaultRequestHeaders(req *http.Request) {
req.Header.Set("User-Agent", "AlbyHub/"+version.Tag)
}

View file

@ -459,10 +459,10 @@ type BasicRestoreWailsRequest struct {
type NetworkGraphResponse = lnclient.NetworkGraphResponse
type LSPOrderRequest struct {
Amount uint64 `json:"amount"`
LSPType string `json:"lspType"`
LSPUrl string `json:"lspUrl"`
Public bool `json:"public"`
Amount uint64 `json:"amount"`
LSPType string `json:"lspType"`
LSPIdentifier string `json:"lspIdentifier"`
Public bool `json:"public"`
}
type LSPOrderResponse struct {

View file

@ -12,6 +12,7 @@ import (
"github.com/getAlby/hub/events"
"github.com/getAlby/hub/logger"
"github.com/getAlby/hub/version"
decodepay "github.com/nbd-wtf/ln-decodepay"
"github.com/sirupsen/logrus"
)
@ -57,8 +58,8 @@ func (api *api) RebalanceChannel(ctx context.Context, rebalanceChannelRequest *R
return nil, err
}
setDefaultRequestHeaders(req)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "AlbyHub/"+version.Tag)
client := http.Client{
Timeout: time.Second * 60,

View file

@ -569,12 +569,12 @@ function PayLightningChannelOrder({ order }: { order: NewChannelOrder }) {
if (!current) {
(async () => {
try {
if (!order.lspType || !order.lspUrl) {
if (!order.lspType || !order.lspIdentifier) {
throw new Error("missing lsp info in order");
}
const newLSPOrderRequest: LSPOrderRequest = {
lspType: order.lspType,
lspUrl: order.lspUrl,
lspIdentifier: order.lspIdentifier,
amount: parseInt(order.amount),
public: order.isPublic,
};
@ -609,7 +609,13 @@ function PayLightningChannelOrder({ order }: { order: NewChannelOrder }) {
}
return true;
});
}, [channels, order.amount, order.isPublic, order.lspType, order.lspUrl]);
}, [
channels,
order.amount,
order.isPublic,
order.lspType,
order.lspIdentifier,
]);
const canPayInternally =
channels &&

View file

@ -141,7 +141,7 @@ function NewChannelInternal({
setOrder((current) => ({
...current,
lspType: selectedPeer.type,
lspUrl: selectedPeer.url,
lspIdentifier: selectedPeer.identifier,
...(!selectedPeer.publicChannelsAllowed && { isPublic: false }),
}));
}
@ -193,7 +193,7 @@ function NewChannelInternal({
throw new Error("Unexpected order or partner payment method");
}
order.lspType = bestPartner.type;
order.lspUrl = bestPartner.url;
order.lspIdentifier = bestPartner.identifier;
}
useChannelOrderStore.getState().setOrder(order as NewChannelOrder);

View file

@ -476,7 +476,7 @@ export type RecommendedChannelPeer = {
| {
paymentMethod: "lightning";
type: LSPType;
url: string;
identifier: string;
contactUrl: string;
terms?: string;
pubkey?: string;
@ -529,7 +529,7 @@ export type AlbyBalance = {
export type LSPOrderRequest = {
amount: number;
lspType: LSPType;
lspUrl: string;
lspIdentifier: string;
public: boolean;
};
@ -663,7 +663,7 @@ export type OnchainOrder = {
export type LightningOrder = {
paymentMethod: "lightning";
lspType: LSPType;
lspUrl: string;
lspIdentifier: string;
} & NewChannelOrderCommon;
export type NewChannelOrder = OnchainOrder | LightningOrder;