lsp.go raw
1 package api
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "strconv"
8
9 "github.com/getAlby/hub/alby"
10 "github.com/getAlby/hub/config"
11 "github.com/getAlby/hub/lnclient"
12 "github.com/getAlby/hub/logger"
13 "github.com/getAlby/hub/lsp"
14 "github.com/getAlby/hub/version"
15 decodepay "github.com/nbd-wtf/ln-decodepay"
16 "github.com/sirupsen/logrus"
17 )
18
19 func (api *api) RequestLSPOrder(ctx context.Context, request *LSPOrderRequest) (*LSPOrderResponse, error) {
20 lnClient := api.svc.GetLNClient()
21 if lnClient == nil {
22 return nil, ErrLNClientNotStarted
23 }
24
25 if request.LSPType != lsp.LSP_TYPE_LSPS1 {
26 return nil, fmt.Errorf("unsupported LSP type: %v", request.LSPType)
27 }
28
29 logger.Logger.Info("Requesting own node info")
30
31 nodeInfo, err := lnClient.GetInfo(ctx)
32 if err != nil {
33 logger.Logger.WithError(err).WithFields(logrus.Fields{
34 "lspIdentifier": request.LSPIdentifier,
35 }).Error("Failed to request own node info", err)
36 return nil, err
37 }
38
39 logger.Logger.Info("Requesting LSP info")
40 lspInfo, err := api.albyOAuthSvc.GetLSPInfo(ctx, request.LSPIdentifier, nodeInfo.Network)
41
42 if err != nil {
43 logger.Logger.WithError(err).Error("Failed to request LSP info")
44 return nil, err
45 }
46
47 logger.Logger.WithField("lspInfo", lspInfo).Info("Connecting to LSP node as a peer")
48
49 err = lnClient.ConnectPeer(ctx, &lnclient.ConnectPeerRequest{
50 Pubkey: lspInfo.Pubkey,
51 Address: lspInfo.Address,
52 Port: lspInfo.Port,
53 })
54
55 if err != nil {
56 logger.Logger.WithError(err).Error("Failed to connect to peer")
57 return nil, err
58 }
59
60 invoice, feeSat, err := api.requestLSPS1Invoice(ctx, lnClient, request, nodeInfo.Network, nodeInfo.Pubkey, lspInfo.MaxChannelExpiryBlocks, lspInfo.MinRequiredChannelConfirmations, lspInfo.MinFundingConfirmsWithinBlocks)
61 invoiceAmountSat := uint64(0)
62 incomingLiquiditySat := uint64(0)
63 resolvedAmountSat := ResolveToSat(request.AmountSat, nil, request.Amount, nil)
64 if resolvedAmountSat != nil {
65 incomingLiquiditySat = *resolvedAmountSat
66 }
67
68 if err != nil {
69 logger.Logger.WithError(err).Error("Failed to request invoice")
70 return nil, err
71 }
72
73 if invoice != "" {
74 paymentRequest, err := decodepay.Decodepay(invoice)
75 if err != nil {
76 logger.Logger.WithError(err).Error("Failed to decode bolt11 invoice")
77 return nil, err
78 }
79
80 invoiceAmountSat = uint64(paymentRequest.MSatoshi / 1000)
81 }
82
83 newChannelResponse := &LSPOrderResponse{
84 Invoice: invoice,
85 Fee: feeSat,
86 FeeSat: feeSat,
87 InvoiceAmount: invoiceAmountSat,
88 InvoiceAmountSat: invoiceAmountSat,
89 IncomingLiquidity: incomingLiquiditySat,
90 IncomingLiquiditySat: incomingLiquiditySat,
91 OutgoingLiquidity: uint64(0),
92 OutgoingLiquiditySat: uint64(0),
93 }
94
95 logger.Logger.WithFields(logrus.Fields{
96 "newChannelResponse": newChannelResponse,
97 }).Debug("New Channel response")
98
99 return newChannelResponse, nil
100 }
101
102 func (api *api) requestLSPS1Invoice(ctx context.Context, lnClient lnclient.LNClient, request *LSPOrderRequest, network, pubkey string, channelExpiryBlocks uint64, minRequiredChannelConfirmations uint64, minFundingConfirmsWithinBlocks uint64) (invoice string, feeSat uint64, err error) {
103 refundAddress, err := lnClient.GetNewOnchainAddress(ctx)
104 if err != nil {
105 logger.Logger.WithError(err).Error("Failed to request onchain address")
106 return "", 0, err
107 }
108
109 var requiredChannelConfirmations uint64 = 0
110
111 backendType, err := api.cfg.Get("LNBackendType", "")
112 if err != nil {
113 return "", 0, errors.New("failed to get LN backend type")
114 }
115
116 if backendType != config.LDKBackendType {
117 // LND does not support 0-conf by default
118 requiredChannelConfirmations = 1
119 }
120
121 // Some LSPs (e.g. Olympus) require more min confirmations, as per the spec ours must be at least as many blocks
122 requiredChannelConfirmations = max(requiredChannelConfirmations, minRequiredChannelConfirmations)
123
124 if request.Public {
125 // as per BOLT-7 6 confirmations are required for the channel to be gossiped
126 // https://github.com/lightning/bolts/blob/master/07-routing-gossip.md#requirements
127 requiredChannelConfirmations = 6
128 }
129
130 token := ""
131 if request.LSPIdentifier == "olympus" {
132 token = "AlbyHub/" + version.Tag
133 }
134
135 // set a non-empty token to notify LNServer that we support 0-conf
136 // (Pre-v1.17.2 does not support 0-conf)
137 if request.LSPIdentifier == "lnserver" {
138 token = "AlbyHub/" + version.Tag
139 }
140
141 // set a non-empty token to notify Flashsats that we support 0-conf
142 // (Pre-v1.21.0 does not support 0-conf)
143 if request.LSPIdentifier == "flashsats" {
144 token = "AlbyHub/" + version.Tag
145 }
146
147 amountSat := uint64(0)
148 resolvedAmountSat := ResolveToSat(request.AmountSat, nil, request.Amount, nil)
149 if resolvedAmountSat != nil {
150 amountSat = *resolvedAmountSat
151 }
152 lspBalanceSat := strconv.FormatUint(amountSat, 10)
153
154 lsps1ChannelRequest := &alby.LSPChannelRequest{
155 PublicKey: pubkey,
156 LSPBalanceSat: lspBalanceSat,
157 ClientBalanceSat: "0",
158 RequiredChannelConfirmations: requiredChannelConfirmations,
159 FundingConfirmsWithinBlocks: minFundingConfirmsWithinBlocks,
160 ChannelExpiryBlocks: channelExpiryBlocks,
161 Token: token,
162 RefundOnchainAddress: refundAddress,
163 AnnounceChannel: request.Public,
164 }
165
166 channelResponse, err := api.albyOAuthSvc.CreateLSPOrder(ctx, request.LSPIdentifier, network, lsps1ChannelRequest)
167 if err != nil {
168 return "", 0, err
169 }
170
171 if channelResponse.Payment != nil {
172 invoice = channelResponse.Payment.Bolt11.Invoice
173 feeSat, err = strconv.ParseUint(channelResponse.Payment.Bolt11.FeeTotalSat, 10, 64)
174 if err != nil {
175 logger.Logger.WithError(err).WithFields(logrus.Fields{
176 "lspIdentifier": request.LSPIdentifier,
177 }).Error("Failed to parse fee")
178 return "", 0, fmt.Errorf("failed to parse fee %v", err)
179 }
180 }
181
182 return invoice, feeSat, nil
183 }
184