@@ -23,6 +23,7 @@ import (
2323 "github.com/lightningnetwork/lnd/lnwire"
2424 "github.com/lightningnetwork/lnd/record"
2525 "github.com/urfave/cli"
26+ "google.golang.org/grpc"
2627)
2728
2829const (
@@ -223,8 +224,66 @@ var (
223224 Usage : "the asset ID of the asset to use when sending " +
224225 "payments with assets" ,
225226 }
227+
228+ assetAmountFlag = cli.Uint64Flag {
229+ Name : "asset_amount" ,
230+ Usage : "the amount of the asset to send in the asset keysend " +
231+ "payment" ,
232+ }
233+
234+ rfqPeerPubKeyFlag = cli.StringFlag {
235+ Name : "rfq_peer_pubkey" ,
236+ Usage : "(optional) the public key of the peer to ask for a " +
237+ "quote when converting from assets to sats; must be " +
238+ "set if there are multiple channels with the same " +
239+ "asset ID present" ,
240+ }
226241)
227242
243+ // resultStreamWrapper is a wrapper around the SendPaymentClient stream that
244+ // implements the generic PaymentResultStream interface.
245+ type resultStreamWrapper struct {
246+ amountMsat int64
247+ stream tchrpc.TaprootAssetChannels_SendPaymentClient
248+ }
249+
250+ // Recv receives the next payment result from the stream.
251+ //
252+ // NOTE: This method is part of the PaymentResultStream interface.
253+ func (w * resultStreamWrapper ) Recv () (* lnrpc.Payment , error ) {
254+ resp , err := w .stream .Recv ()
255+ if err != nil {
256+ return nil , err
257+ }
258+
259+ res := resp .Result
260+ switch r := res .(type ) {
261+ // The very first response might be an accepted sell order, which we
262+ // just print out.
263+ case * tchrpc.SendPaymentResponse_AcceptedSellOrder :
264+ quote := r .AcceptedSellOrder
265+ msatPerUnit := quote .BidPrice
266+ numUnits := uint64 (w .amountMsat ) / msatPerUnit
267+
268+ fmt .Printf ("Got quote for %v asset units at %v msat/unit from " +
269+ "peer %s with SCID %d\n " , numUnits , msatPerUnit ,
270+ quote .Peer , quote .Scid )
271+
272+ resp , err = w .stream .Recv ()
273+ if err != nil {
274+ return nil , err
275+ }
276+
277+ return resp .GetPaymentResult (), nil
278+
279+ case * tchrpc.SendPaymentResponse_PaymentResult :
280+ return r .PaymentResult , nil
281+
282+ default :
283+ return nil , fmt .Errorf ("unexpected response type: %T" , r )
284+ }
285+ }
286+
228287var sendPaymentCommand = cli.Command {
229288 Name : "sendpayment" ,
230289 Category : commands .SendPaymentCommand .Category ,
@@ -236,14 +295,16 @@ var sendPaymentCommand = cli.Command{
236295
237296 Note that this will only work in concert with the --keysend argument.
238297 ` ,
239- ArgsUsage : commands .SendPaymentCommand .ArgsUsage + " --asset_id=X" ,
240- Flags : append (commands .SendPaymentCommand .Flags , assetIDFlag ),
241- Action : sendPayment ,
298+ ArgsUsage : commands .SendPaymentCommand .ArgsUsage + " --asset_id=X " +
299+ "--asset_amount=Y [--rfq_peer_pubkey=Z]" ,
300+ Flags : append (
301+ commands .SendPaymentCommand .Flags , assetIDFlag , assetAmountFlag ,
302+ rfqPeerPubKeyFlag ,
303+ ),
304+ Action : sendPayment ,
242305}
243306
244307func sendPayment (ctx * cli.Context ) error {
245- ctxb := context .Background ()
246-
247308 // Show command help if no arguments provided
248309 if ctx .NArg () == 0 && ctx .NumFlags () == 0 {
249310 _ = cli .ShowCommandHelp (ctx , "sendpayment" )
@@ -254,67 +315,32 @@ func sendPayment(ctx *cli.Context) error {
254315 if err != nil {
255316 return fmt .Errorf ("unable to make rpc con: %w" , err )
256317 }
257-
258318 defer cleanup ()
259319
260- lndClient := lnrpc .NewLightningClient (lndConn )
320+ tapdConn , cleanup , err := connectTapdClient (ctx )
321+ if err != nil {
322+ return fmt .Errorf ("error creating tapd connection: %w" , err )
323+ }
324+ defer cleanup ()
261325
262326 switch {
263327 case ! ctx .IsSet (assetIDFlag .Name ):
264328 return fmt .Errorf ("the --asset_id flag must be set" )
265329 case ! ctx .IsSet ("keysend" ):
266330 return fmt .Errorf ("the --keysend flag must be set" )
267- case ! ctx .IsSet ("amt" ):
268- return fmt .Errorf ("--amt must be set" )
331+ case ! ctx .IsSet (assetAmountFlag . Name ):
332+ return fmt .Errorf ("--asset_amount must be set" )
269333 }
270334
271335 assetIDStr := ctx .String (assetIDFlag .Name )
272- _ , err = hex .DecodeString (assetIDStr )
336+ assetIDBytes , err : = hex .DecodeString (assetIDStr )
273337 if err != nil {
274338 return fmt .Errorf ("unable to decode assetID: %v" , err )
275339 }
276340
277- // First, based on the asset ID and amount, we'll make sure that this
278- // channel even has enough funds to send.
279- assetBalances , err := computeAssetBalances (lndClient )
280- if err != nil {
281- return fmt .Errorf ("unable to compute asset balances: %w" , err )
282- }
283-
284- balance , ok := assetBalances .Assets [assetIDStr ]
285- if ! ok {
286- return fmt .Errorf ("unable to send asset_id=%v, not in " +
287- "channel" , assetIDStr )
288- }
289-
290- amtToSend := ctx .Uint64 ("amt" )
291- if amtToSend > balance .LocalBalance {
292- return fmt .Errorf ("insufficient balance, want to send %v, " +
293- "only have %v" , amtToSend , balance .LocalBalance )
294- }
295-
296- tapdConn , cleanup , err := connectTapdClient (ctx )
297- if err != nil {
298- return fmt .Errorf ("error creating tapd connection: %w" , err )
299- }
300- defer cleanup ()
301-
302- tchrpcClient := tchrpc .NewTaprootAssetChannelsClient (tapdConn )
303-
304- encodeReq := & tchrpc.EncodeCustomRecordsRequest_RouterSendPayment {
305- RouterSendPayment : & tchrpc.RouterSendPaymentData {
306- AssetAmounts : map [string ]uint64 {
307- assetIDStr : amtToSend ,
308- },
309- },
310- }
311- encodeResp , err := tchrpcClient .EncodeCustomRecords (
312- ctxb , & tchrpc.EncodeCustomRecordsRequest {
313- Input : encodeReq ,
314- },
315- )
316- if err != nil {
317- return fmt .Errorf ("error encoding custom records: %w" , err )
341+ assetAmountToSend := ctx .Uint64 (assetAmountFlag .Name )
342+ if assetAmountToSend == 0 {
343+ return fmt .Errorf ("must specify asset amount to send" )
318344 }
319345
320346 // With the asset specific work out of the way, we'll parse the rest of
@@ -339,15 +365,20 @@ func sendPayment(ctx *cli.Context) error {
339365 "is instead: %v" , len (destNode ))
340366 }
341367
368+ rfqPeerKey , err := hex .DecodeString (ctx .String (rfqPeerPubKeyFlag .Name ))
369+ if err != nil {
370+ return fmt .Errorf ("unable to decode RFQ peer public key: " +
371+ "%w" , err )
372+ }
373+
342374 // We use a constant amount of 500 to carry the asset HTLCs. In the
343375 // future, we can use the double HTLC trick here, though it consumes
344376 // more commitment space.
345377 const htlcCarrierAmt = 500
346378 req := & routerrpc.SendPaymentRequest {
347- Dest : destNode ,
348- Amt : htlcCarrierAmt ,
349- DestCustomRecords : make (map [uint64 ][]byte ),
350- FirstHopCustomRecords : encodeResp .CustomRecords ,
379+ Dest : destNode ,
380+ Amt : htlcCarrierAmt ,
381+ DestCustomRecords : make (map [uint64 ][]byte ),
351382 }
352383
353384 if ctx .IsSet ("payment_hash" ) {
@@ -370,7 +401,33 @@ func sendPayment(ctx *cli.Context) error {
370401
371402 req .PaymentHash = rHash
372403
373- return commands .SendPaymentRequest (ctx , req )
404+ return commands .SendPaymentRequest (
405+ ctx , req , lndConn , tapdConn , func (ctx context.Context ,
406+ payConn grpc.ClientConnInterface ,
407+ req * routerrpc.SendPaymentRequest ) (
408+ commands.PaymentResultStream , error ) {
409+
410+ tchrpcClient := tchrpc .NewTaprootAssetChannelsClient (
411+ payConn ,
412+ )
413+
414+ stream , err := tchrpcClient .SendPayment (
415+ ctx , & tchrpc.SendPaymentRequest {
416+ AssetId : assetIDBytes ,
417+ AssetAmount : assetAmountToSend ,
418+ PeerPubkey : rfqPeerKey ,
419+ PaymentRequest : req ,
420+ },
421+ )
422+ if err != nil {
423+ return nil , err
424+ }
425+
426+ return & resultStreamWrapper {
427+ stream : stream ,
428+ }, nil
429+ },
430+ )
374431}
375432
376433var payInvoiceCommand = cli.Command {
@@ -434,24 +491,6 @@ func payInvoice(ctx *cli.Context) error {
434491 return fmt .Errorf ("unable to decode assetID: %v" , err )
435492 }
436493
437- // First, based on the asset ID and amount, we'll make sure that this
438- // channel even has enough funds to send.
439- assetBalances , err := computeAssetBalances (lndClient )
440- if err != nil {
441- return fmt .Errorf ("unable to compute asset balances: %w" , err )
442- }
443-
444- balance , ok := assetBalances .Assets [assetIDStr ]
445- if ! ok {
446- return fmt .Errorf ("unable to send asset_id=%v, not in " +
447- "channel" , assetIDStr )
448- }
449-
450- if balance .LocalBalance == 0 {
451- return fmt .Errorf ("no asset balance available for asset_id=%v" ,
452- assetIDStr )
453- }
454-
455494 var assetID asset.ID
456495 copy (assetID [:], assetIDBytes )
457496
@@ -462,88 +501,35 @@ func payInvoice(ctx *cli.Context) error {
462501
463502 defer cleanup ()
464503
465- peerPubKey , err := hex .DecodeString (balance .Channel .RemotePubkey )
466- if err != nil {
467- return fmt .Errorf ("unable to decode peer pubkey: %w" , err )
504+ req := & routerrpc.SendPaymentRequest {
505+ PaymentRequest : commands .StripPrefix (payReq ),
468506 }
469507
470- rfqClient := rfqrpc .NewRfqClient (tapdConn )
508+ return commands .SendPaymentRequest (
509+ ctx , req , lndConn , tapdConn , func (ctx context.Context ,
510+ payConn grpc.ClientConnInterface ,
511+ req * routerrpc.SendPaymentRequest ) (
512+ commands.PaymentResultStream , error ) {
471513
472- timeoutSeconds := uint32 (60 )
473- fmt .Printf ("Asking peer %x for quote to sell assets to pay for " +
474- "invoice over %d msats; waiting up to %ds\n " , peerPubKey ,
475- decodeResp .NumMsat , timeoutSeconds )
514+ tchrpcClient := tchrpc .NewTaprootAssetChannelsClient (
515+ payConn ,
516+ )
476517
477- resp , err := rfqClient .AddAssetSellOrder (
478- ctxb , & rfqrpc.AddAssetSellOrderRequest {
479- AssetSpecifier : & rfqrpc.AssetSpecifier {
480- Id : & rfqrpc.AssetSpecifier_AssetIdStr {
481- AssetIdStr : assetIDStr ,
518+ stream , err := tchrpcClient .SendPayment (
519+ ctx , & tchrpc.SendPaymentRequest {
520+ AssetId : assetIDBytes ,
482521 },
483- },
484- // TODO(guggero): This should actually be the max BTC
485- // amount (invoice amount plus fee limit) in
486- // milli-satoshi, not the asset amount. Need to change
487- // the whole RFQ API to do that though.
488- MaxAssetAmount : balance .LocalBalance ,
489- MinAsk : uint64 (decodeResp .NumMsat ),
490- Expiry : uint64 (decodeResp .Expiry ),
491- PeerPubKey : peerPubKey ,
492- TimeoutSeconds : timeoutSeconds ,
493- },
494- )
495- if err != nil {
496- return fmt .Errorf ("error adding sell order: %w" , err )
497- }
498-
499- var acceptedQuote * rfqrpc.PeerAcceptedSellQuote
500- switch r := resp .Response .(type ) {
501- case * rfqrpc.AddAssetSellOrderResponse_AcceptedQuote :
502- acceptedQuote = r .AcceptedQuote
503-
504- case * rfqrpc.AddAssetSellOrderResponse_InvalidQuote :
505- return fmt .Errorf ("peer %v sent back an invalid quote, " +
506- "status: %v" , r .InvalidQuote .Peer ,
507- r .InvalidQuote .Status .String ())
508-
509- case * rfqrpc.AddAssetSellOrderResponse_RejectedQuote :
510- return fmt .Errorf ("peer %v rejected the quote, code: %v, " +
511- "error message: %v" , r .RejectedQuote .Peer ,
512- r .RejectedQuote .ErrorCode , r .RejectedQuote .ErrorMessage )
513-
514- default :
515- return fmt .Errorf ("unexpected response type: %T" , r )
516- }
517-
518- msatPerUnit := acceptedQuote .BidPrice
519- numUnits := uint64 (decodeResp .NumMsat ) / msatPerUnit
520-
521- fmt .Printf ("Got quote for %v asset units at %v msat/unit from peer " +
522- "%x with SCID %d\n " , numUnits , msatPerUnit , peerPubKey ,
523- acceptedQuote .Scid )
524-
525- tchrpcClient := tchrpc .NewTaprootAssetChannelsClient (tapdConn )
522+ )
523+ if err != nil {
524+ return nil , err
525+ }
526526
527- encodeReq := & tchrpc.EncodeCustomRecordsRequest_RouterSendPayment {
528- RouterSendPayment : & tchrpc.RouterSendPaymentData {
529- RfqId : acceptedQuote .Id ,
530- },
531- }
532- encodeResp , err := tchrpcClient .EncodeCustomRecords (
533- ctxb , & tchrpc.EncodeCustomRecordsRequest {
534- Input : encodeReq ,
527+ return & resultStreamWrapper {
528+ amountMsat : decodeResp .NumMsat ,
529+ stream : stream ,
530+ }, nil
535531 },
536532 )
537- if err != nil {
538- return fmt .Errorf ("error encoding custom records: %w" , err )
539- }
540-
541- req := & routerrpc.SendPaymentRequest {
542- PaymentRequest : commands .StripPrefix (payReq ),
543- FirstHopCustomRecords : encodeResp .CustomRecords ,
544- }
545-
546- return commands .SendPaymentRequest (ctx , req )
547533}
548534
549535var addInvoiceCommand = cli.Command {
0 commit comments