You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

350 lines
8.4 KiB

  1. package lndclient
  2. import (
  3. "context"
  4. "encoding/hex"
  5. "errors"
  6. "fmt"
  7. "sync"
  8. "time"
  9. "github.com/btcsuite/btcd/chaincfg"
  10. "github.com/btcsuite/btcutil"
  11. "github.com/lightningnetwork/lnd/channeldb"
  12. "github.com/lightningnetwork/lnd/lnrpc"
  13. "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
  14. "github.com/lightningnetwork/lnd/lntypes"
  15. "github.com/lightningnetwork/lnd/zpay32"
  16. "google.golang.org/grpc"
  17. "google.golang.org/grpc/codes"
  18. "google.golang.org/grpc/status"
  19. )
  20. // LightningClient exposes base lightning functionality.
  21. type LightningClient interface {
  22. PayInvoice(ctx context.Context, invoice string,
  23. maxFee btcutil.Amount,
  24. outgoingChannel *uint64) chan PaymentResult
  25. GetInfo(ctx context.Context) (*Info, error)
  26. EstimateFeeToP2WSH(ctx context.Context, amt btcutil.Amount,
  27. confTarget int32) (btcutil.Amount, error)
  28. ConfirmedWalletBalance(ctx context.Context) (btcutil.Amount, error)
  29. AddInvoice(ctx context.Context, in *invoicesrpc.AddInvoiceData) (
  30. lntypes.Hash, string, error)
  31. }
  32. // Info contains info about the connected lnd node.
  33. type Info struct {
  34. BlockHeight uint32
  35. IdentityPubkey [33]byte
  36. Alias string
  37. Network string
  38. }
  39. var (
  40. // ErrMalformedServerResponse is returned when the swap and/or prepay
  41. // invoice is malformed.
  42. ErrMalformedServerResponse = errors.New(
  43. "one or more invoices are malformed",
  44. )
  45. // ErrNoRouteToServer is returned if no quote can returned because there
  46. // is no route to the server.
  47. ErrNoRouteToServer = errors.New("no off-chain route to server")
  48. // PaymentResultUnknownPaymentHash is the string result returned by
  49. // SendPayment when the final node indicates the hash is unknown.
  50. PaymentResultUnknownPaymentHash = "UnknownPaymentHash"
  51. // PaymentResultSuccess is the string result returned by SendPayment
  52. // when the payment was successful.
  53. PaymentResultSuccess = ""
  54. // PaymentResultAlreadyPaid is the string result returned by SendPayment
  55. // when the payment was already completed in a previous SendPayment
  56. // call.
  57. PaymentResultAlreadyPaid = channeldb.ErrAlreadyPaid.Error()
  58. // PaymentResultInFlight is the string result returned by SendPayment
  59. // when the payment was initiated in a previous SendPayment call and
  60. // still in flight.
  61. PaymentResultInFlight = channeldb.ErrPaymentInFlight.Error()
  62. paymentPollInterval = 3 * time.Second
  63. )
  64. type lightningClient struct {
  65. client lnrpc.LightningClient
  66. wg sync.WaitGroup
  67. params *chaincfg.Params
  68. adminMac serializedMacaroon
  69. }
  70. func newLightningClient(conn *grpc.ClientConn,
  71. params *chaincfg.Params, adminMac serializedMacaroon) *lightningClient {
  72. return &lightningClient{
  73. client: lnrpc.NewLightningClient(conn),
  74. params: params,
  75. adminMac: adminMac,
  76. }
  77. }
  78. // PaymentResult signals the result of a payment.
  79. type PaymentResult struct {
  80. Err error
  81. Preimage lntypes.Preimage
  82. PaidFee btcutil.Amount
  83. PaidAmt btcutil.Amount
  84. }
  85. func (s *lightningClient) WaitForFinished() {
  86. s.wg.Wait()
  87. }
  88. func (s *lightningClient) ConfirmedWalletBalance(ctx context.Context) (
  89. btcutil.Amount, error) {
  90. rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
  91. defer cancel()
  92. rpcCtx = s.adminMac.WithMacaroonAuth(rpcCtx)
  93. resp, err := s.client.WalletBalance(rpcCtx, &lnrpc.WalletBalanceRequest{})
  94. if err != nil {
  95. return 0, err
  96. }
  97. return btcutil.Amount(resp.ConfirmedBalance), nil
  98. }
  99. func (s *lightningClient) GetInfo(ctx context.Context) (*Info, error) {
  100. rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
  101. defer cancel()
  102. rpcCtx = s.adminMac.WithMacaroonAuth(rpcCtx)
  103. resp, err := s.client.GetInfo(rpcCtx, &lnrpc.GetInfoRequest{})
  104. if err != nil {
  105. return nil, err
  106. }
  107. pubKey, err := hex.DecodeString(resp.IdentityPubkey)
  108. if err != nil {
  109. return nil, err
  110. }
  111. var pubKeyArray [33]byte
  112. copy(pubKeyArray[:], pubKey)
  113. return &Info{
  114. BlockHeight: resp.BlockHeight,
  115. IdentityPubkey: pubKeyArray,
  116. Alias: resp.Alias,
  117. Network: resp.Chains[0].Network,
  118. }, nil
  119. }
  120. func (s *lightningClient) EstimateFeeToP2WSH(ctx context.Context,
  121. amt btcutil.Amount, confTarget int32) (btcutil.Amount,
  122. error) {
  123. rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
  124. defer cancel()
  125. // Generate dummy p2wsh address for fee estimation.
  126. wsh := [32]byte{}
  127. p2wshAddress, err := btcutil.NewAddressWitnessScriptHash(
  128. wsh[:], s.params,
  129. )
  130. if err != nil {
  131. return 0, err
  132. }
  133. rpcCtx = s.adminMac.WithMacaroonAuth(rpcCtx)
  134. resp, err := s.client.EstimateFee(
  135. rpcCtx,
  136. &lnrpc.EstimateFeeRequest{
  137. TargetConf: confTarget,
  138. AddrToAmount: map[string]int64{
  139. p2wshAddress.String(): int64(amt),
  140. },
  141. },
  142. )
  143. if err != nil {
  144. return 0, err
  145. }
  146. return btcutil.Amount(resp.FeeSat), nil
  147. }
  148. // PayInvoice pays an invoice.
  149. func (s *lightningClient) PayInvoice(ctx context.Context, invoice string,
  150. maxFee btcutil.Amount, outgoingChannel *uint64) chan PaymentResult {
  151. // Use buffer to prevent blocking.
  152. paymentChan := make(chan PaymentResult, 1)
  153. // Execute payment in parallel, because it will block until server
  154. // discovers preimage.
  155. s.wg.Add(1)
  156. go func() {
  157. defer s.wg.Done()
  158. result := s.payInvoice(ctx, invoice, maxFee, outgoingChannel)
  159. if result != nil {
  160. paymentChan <- *result
  161. }
  162. }()
  163. return paymentChan
  164. }
  165. // payInvoice tries to send a payment and returns the final result. If
  166. // necessary, it will poll lnd for the payment result.
  167. func (s *lightningClient) payInvoice(ctx context.Context, invoice string,
  168. maxFee btcutil.Amount, outgoingChannel *uint64) *PaymentResult {
  169. payReq, err := zpay32.Decode(invoice, s.params)
  170. if err != nil {
  171. return &PaymentResult{
  172. Err: fmt.Errorf("invoice decode: %v", err),
  173. }
  174. }
  175. if payReq.MilliSat == nil {
  176. return &PaymentResult{
  177. Err: errors.New("no amount in invoice"),
  178. }
  179. }
  180. hash := lntypes.Hash(*payReq.PaymentHash)
  181. ctx = s.adminMac.WithMacaroonAuth(ctx)
  182. for {
  183. // Create no timeout context as this call can block for a long
  184. // time.
  185. req := &lnrpc.SendRequest{
  186. FeeLimit: &lnrpc.FeeLimit{
  187. Limit: &lnrpc.FeeLimit_Fixed{
  188. Fixed: int64(maxFee),
  189. },
  190. },
  191. PaymentRequest: invoice,
  192. }
  193. if outgoingChannel != nil {
  194. req.OutgoingChanId = *outgoingChannel
  195. }
  196. payResp, err := s.client.SendPaymentSync(ctx, req)
  197. if status.Code(err) == codes.Canceled {
  198. return nil
  199. }
  200. if err == nil {
  201. // TODO: Use structured payment error when available,
  202. // instead of this britle string matching.
  203. switch payResp.PaymentError {
  204. // Paid successfully.
  205. case PaymentResultSuccess:
  206. log.Infof(
  207. "Payment %v completed", hash,
  208. )
  209. r := payResp.PaymentRoute
  210. preimage, err := lntypes.MakePreimage(
  211. payResp.PaymentPreimage,
  212. )
  213. if err != nil {
  214. return &PaymentResult{Err: err}
  215. }
  216. return &PaymentResult{
  217. PaidFee: btcutil.Amount(r.TotalFees),
  218. PaidAmt: btcutil.Amount(
  219. r.TotalAmt - r.TotalFees,
  220. ),
  221. Preimage: preimage,
  222. }
  223. // Invoice was already paid on a previous run.
  224. case PaymentResultAlreadyPaid:
  225. log.Infof(
  226. "Payment %v already completed", hash,
  227. )
  228. // Unfortunately lnd doesn't return the route if
  229. // the payment was successful in a previous
  230. // call. Assume paid fees 0 and take paid amount
  231. // from invoice.
  232. return &PaymentResult{
  233. PaidFee: 0,
  234. PaidAmt: payReq.MilliSat.ToSatoshis(),
  235. }
  236. // If the payment is already in flight, we will poll
  237. // again later for an outcome.
  238. //
  239. // TODO: Improve this when lnd expose more API to
  240. // tracking existing payments.
  241. case PaymentResultInFlight:
  242. log.Infof(
  243. "Payment %v already in flight", hash,
  244. )
  245. time.Sleep(paymentPollInterval)
  246. // Other errors are transformed into an error struct.
  247. default:
  248. log.Warnf(
  249. "Payment %v failed: %v", hash,
  250. payResp.PaymentError,
  251. )
  252. return &PaymentResult{
  253. Err: errors.New(payResp.PaymentError),
  254. }
  255. }
  256. }
  257. }
  258. }
  259. func (s *lightningClient) AddInvoice(ctx context.Context,
  260. in *invoicesrpc.AddInvoiceData) (lntypes.Hash, string, error) {
  261. rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
  262. defer cancel()
  263. rpcIn := &lnrpc.Invoice{
  264. Memo: in.Memo,
  265. Value: int64(in.Value),
  266. Expiry: in.Expiry,
  267. CltvExpiry: in.CltvExpiry,
  268. Private: true,
  269. }
  270. if in.Preimage != nil {
  271. rpcIn.RPreimage = in.Preimage[:]
  272. }
  273. if in.Hash != nil {
  274. rpcIn.RHash = in.Hash[:]
  275. }
  276. rpcCtx = s.adminMac.WithMacaroonAuth(rpcCtx)
  277. resp, err := s.client.AddInvoice(rpcCtx, rpcIn)
  278. if err != nil {
  279. return lntypes.Hash{}, "", err
  280. }
  281. hash, err := lntypes.MakeHash(resp.RHash)
  282. if err != nil {
  283. return lntypes.Hash{}, "", err
  284. }
  285. return hash, resp.PaymentRequest, nil
  286. }