@@ -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,61 @@ 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+ type resultStreamWrapper struct {
244+ amountMsat int64
245+ stream tchrpc.TaprootAssetChannels_SendPaymentClient
246+ }
247+
248+ func (w * resultStreamWrapper ) Recv () (* lnrpc.Payment , error ) {
249+ resp , err := w .stream .Recv ()
250+ if err != nil {
251+ return nil , err
252+ }
253+
254+ res := resp .Result
255+ switch r := res .(type ) {
256+ // The very first response might be an accepted sell order, which we
257+ // just print out.
258+ case * tchrpc.SendPaymentResponse_AcceptedSellOrder :
259+ quote := r .AcceptedSellOrder
260+ msatPerUnit := quote .BidPrice
261+ numUnits := uint64 (w .amountMsat ) / msatPerUnit
262+
263+ fmt .Printf ("Got quote for %v asset units at %v msat/unit from " +
264+ "peer %s with SCID %d\n " , numUnits , msatPerUnit ,
265+ quote .Peer , quote .Scid )
266+
267+ resp , err = w .stream .Recv ()
268+ if err != nil {
269+ return nil , err
270+ }
271+
272+ return resp .GetPaymentResult (), nil
273+
274+ case * tchrpc.SendPaymentResponse_PaymentResult :
275+ return r .PaymentResult , nil
276+
277+ default :
278+ return nil , fmt .Errorf ("unexpected response type: %T" , r )
279+ }
280+ }
281+
228282var sendPaymentCommand = cli.Command {
229283 Name : "sendpayment" ,
230284 Category : commands .SendPaymentCommand .Category ,
@@ -236,14 +290,16 @@ var sendPaymentCommand = cli.Command{
236290
237291 Note that this will only work in concert with the --keysend argument.
238292 ` ,
239- ArgsUsage : commands .SendPaymentCommand .ArgsUsage + " --asset_id=X" ,
240- Flags : append (commands .SendPaymentCommand .Flags , assetIDFlag ),
241- Action : sendPayment ,
293+ ArgsUsage : commands .SendPaymentCommand .ArgsUsage + " --asset_id=X " +
294+ "--asset_amount=Y [--rfq_peer_pubkey=Z]" ,
295+ Flags : append (
296+ commands .SendPaymentCommand .Flags , assetIDFlag , assetAmountFlag ,
297+ rfqPeerPubKeyFlag ,
298+ ),
299+ Action : sendPayment ,
242300}
243301
244302func sendPayment (ctx * cli.Context ) error {
245- ctxb := context .Background ()
246-
247303 // Show command help if no arguments provided
248304 if ctx .NArg () == 0 && ctx .NumFlags () == 0 {
249305 _ = cli .ShowCommandHelp (ctx , "sendpayment" )
@@ -254,67 +310,32 @@ func sendPayment(ctx *cli.Context) error {
254310 if err != nil {
255311 return fmt .Errorf ("unable to make rpc con: %w" , err )
256312 }
257-
258313 defer cleanup ()
259314
260- lndClient := lnrpc .NewLightningClient (lndConn )
315+ tapdConn , cleanup , err := connectTapdClient (ctx )
316+ if err != nil {
317+ return fmt .Errorf ("error creating tapd connection: %w" , err )
318+ }
319+ defer cleanup ()
261320
262321 switch {
263322 case ! ctx .IsSet (assetIDFlag .Name ):
264323 return fmt .Errorf ("the --asset_id flag must be set" )
265324 case ! ctx .IsSet ("keysend" ):
266325 return fmt .Errorf ("the --keysend flag must be set" )
267- case ! ctx .IsSet ("amt" ):
268- return fmt .Errorf ("--amt must be set" )
326+ case ! ctx .IsSet (assetAmountFlag . Name ):
327+ return fmt .Errorf ("--asset_amount must be set" )
269328 }
270329
271330 assetIDStr := ctx .String (assetIDFlag .Name )
272- _ , err = hex .DecodeString (assetIDStr )
331+ assetIDBytes , err : = hex .DecodeString (assetIDStr )
273332 if err != nil {
274333 return fmt .Errorf ("unable to decode assetID: %v" , err )
275334 }
276335
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 )
336+ assetAmountToSend := ctx .Uint64 (assetAmountFlag .Name )
337+ if assetAmountToSend == 0 {
338+ return fmt .Errorf ("must specify asset amount to send" )
318339 }
319340
320341 // With the asset specific work out of the way, we'll parse the rest of
@@ -339,15 +360,20 @@ func sendPayment(ctx *cli.Context) error {
339360 "is instead: %v" , len (destNode ))
340361 }
341362
363+ rfqPeerKey , err := hex .DecodeString (ctx .String (rfqPeerPubKeyFlag .Name ))
364+ if err != nil {
365+ return fmt .Errorf ("unable to decode RFQ peer public key: " +
366+ "%w" , err )
367+ }
368+
342369 // We use a constant amount of 500 to carry the asset HTLCs. In the
343370 // future, we can use the double HTLC trick here, though it consumes
344371 // more commitment space.
345372 const htlcCarrierAmt = 500
346373 req := & routerrpc.SendPaymentRequest {
347- Dest : destNode ,
348- Amt : htlcCarrierAmt ,
349- DestCustomRecords : make (map [uint64 ][]byte ),
350- FirstHopCustomRecords : encodeResp .CustomRecords ,
374+ Dest : destNode ,
375+ Amt : htlcCarrierAmt ,
376+ DestCustomRecords : make (map [uint64 ][]byte ),
351377 }
352378
353379 if ctx .IsSet ("payment_hash" ) {
@@ -370,7 +396,33 @@ func sendPayment(ctx *cli.Context) error {
370396
371397 req .PaymentHash = rHash
372398
373- return commands .SendPaymentRequest (ctx , req )
399+ return commands .SendPaymentRequest (
400+ ctx , req , lndConn , tapdConn , func (ctx context.Context ,
401+ payConn grpc.ClientConnInterface ,
402+ req * routerrpc.SendPaymentRequest ) (
403+ commands.PaymentResultStream , error ) {
404+
405+ tchrpcClient := tchrpc .NewTaprootAssetChannelsClient (
406+ payConn ,
407+ )
408+
409+ stream , err := tchrpcClient .SendPayment (
410+ ctx , & tchrpc.SendPaymentRequest {
411+ AssetId : assetIDBytes ,
412+ AssetAmount : assetAmountToSend ,
413+ PeerPubkey : rfqPeerKey ,
414+ PaymentRequest : req ,
415+ },
416+ )
417+ if err != nil {
418+ return nil , err
419+ }
420+
421+ return & resultStreamWrapper {
422+ stream : stream ,
423+ }, nil
424+ },
425+ )
374426}
375427
376428var payInvoiceCommand = cli.Command {
@@ -434,24 +486,6 @@ func payInvoice(ctx *cli.Context) error {
434486 return fmt .Errorf ("unable to decode assetID: %v" , err )
435487 }
436488
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-
455489 var assetID asset.ID
456490 copy (assetID [:], assetIDBytes )
457491
@@ -462,88 +496,35 @@ func payInvoice(ctx *cli.Context) error {
462496
463497 defer cleanup ()
464498
465- peerPubKey , err := hex .DecodeString (balance .Channel .RemotePubkey )
466- if err != nil {
467- return fmt .Errorf ("unable to decode peer pubkey: %w" , err )
499+ req := & routerrpc.SendPaymentRequest {
500+ PaymentRequest : commands .StripPrefix (payReq ),
468501 }
469502
470- rfqClient := rfqrpc .NewRfqClient (tapdConn )
503+ return commands .SendPaymentRequest (
504+ ctx , req , lndConn , tapdConn , func (ctx context.Context ,
505+ payConn grpc.ClientConnInterface ,
506+ req * routerrpc.SendPaymentRequest ) (
507+ commands.PaymentResultStream , error ) {
471508
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 )
509+ tchrpcClient := tchrpc .NewTaprootAssetChannelsClient (
510+ payConn ,
511+ )
476512
477- resp , err := rfqClient .AddAssetSellOrder (
478- ctxb , & rfqrpc.AddAssetSellOrderRequest {
479- AssetSpecifier : & rfqrpc.AssetSpecifier {
480- Id : & rfqrpc.AssetSpecifier_AssetIdStr {
481- AssetIdStr : assetIDStr ,
513+ stream , err := tchrpcClient .SendPayment (
514+ ctx , & tchrpc.SendPaymentRequest {
515+ AssetId : assetIDBytes ,
482516 },
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 )
517+ )
518+ if err != nil {
519+ return nil , err
520+ }
526521
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 ,
522+ return & resultStreamWrapper {
523+ amountMsat : decodeResp .NumMsat ,
524+ stream : stream ,
525+ }, nil
535526 },
536527 )
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 )
547528}
548529
549530var addInvoiceCommand = cli.Command {
0 commit comments