mirror of
https://github.com/getAlby/hub.git
synced 2026-08-13 12:33:39 +02:00
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:
parent
0be4f9b135
commit
55e5477076
8 changed files with 250 additions and 332 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
254
api/lsp.go
254
api/lsp.go
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 &&
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue