@@ -23,6 +23,7 @@ import (
23
23
"github.com/lightningnetwork/lnd/lnwire"
24
24
"github.com/lightningnetwork/lnd/record"
25
25
"github.com/urfave/cli"
26
+ "google.golang.org/grpc"
26
27
)
27
28
28
29
const (
@@ -223,8 +224,66 @@ var (
223
224
Usage : "the asset ID of the asset to use when sending " +
224
225
"payments with assets" ,
225
226
}
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
+ }
226
241
)
227
242
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
+
228
287
var sendPaymentCommand = cli.Command {
229
288
Name : "sendpayment" ,
230
289
Category : commands .SendPaymentCommand .Category ,
@@ -236,14 +295,16 @@ var sendPaymentCommand = cli.Command{
236
295
237
296
Note that this will only work in concert with the --keysend argument.
238
297
` ,
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 ,
242
305
}
243
306
244
307
func sendPayment (ctx * cli.Context ) error {
245
- ctxb := context .Background ()
246
-
247
308
// Show command help if no arguments provided
248
309
if ctx .NArg () == 0 && ctx .NumFlags () == 0 {
249
310
_ = cli .ShowCommandHelp (ctx , "sendpayment" )
@@ -254,67 +315,32 @@ func sendPayment(ctx *cli.Context) error {
254
315
if err != nil {
255
316
return fmt .Errorf ("unable to make rpc con: %w" , err )
256
317
}
257
-
258
318
defer cleanup ()
259
319
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 ()
261
325
262
326
switch {
263
327
case ! ctx .IsSet (assetIDFlag .Name ):
264
328
return fmt .Errorf ("the --asset_id flag must be set" )
265
329
case ! ctx .IsSet ("keysend" ):
266
330
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" )
269
333
}
270
334
271
335
assetIDStr := ctx .String (assetIDFlag .Name )
272
- _ , err = hex .DecodeString (assetIDStr )
336
+ assetIDBytes , err : = hex .DecodeString (assetIDStr )
273
337
if err != nil {
274
338
return fmt .Errorf ("unable to decode assetID: %v" , err )
275
339
}
276
340
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" )
318
344
}
319
345
320
346
// 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 {
339
365
"is instead: %v" , len (destNode ))
340
366
}
341
367
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
+
342
374
// We use a constant amount of 500 to carry the asset HTLCs. In the
343
375
// future, we can use the double HTLC trick here, though it consumes
344
376
// more commitment space.
345
377
const htlcCarrierAmt = 500
346
378
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 ),
351
382
}
352
383
353
384
if ctx .IsSet ("payment_hash" ) {
@@ -370,7 +401,33 @@ func sendPayment(ctx *cli.Context) error {
370
401
371
402
req .PaymentHash = rHash
372
403
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
+ )
374
431
}
375
432
376
433
var payInvoiceCommand = cli.Command {
@@ -434,24 +491,6 @@ func payInvoice(ctx *cli.Context) error {
434
491
return fmt .Errorf ("unable to decode assetID: %v" , err )
435
492
}
436
493
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
-
455
494
var assetID asset.ID
456
495
copy (assetID [:], assetIDBytes )
457
496
@@ -462,88 +501,35 @@ func payInvoice(ctx *cli.Context) error {
462
501
463
502
defer cleanup ()
464
503
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 ),
468
506
}
469
507
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 ) {
471
513
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
+ )
476
517
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 ,
482
521
},
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
+ }
526
526
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
535
531
},
536
532
)
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 )
547
533
}
548
534
549
535
var addInvoiceCommand = cli.Command {
0 commit comments