@@ -61,8 +61,8 @@ func createTestAssetNetwork(t *harnessTest, net *NetworkHarness, charlieTap,
6161 daveTap , erinTap , fabiaTap , yaraTap , universeTap * tapClient ,
6262 mintedAsset * taprpc.Asset , assetSendAmount , charlieFundingAmount ,
6363 daveFundingAmount ,
64- erinFundingAmount uint64 , pushSat int64 ) (* tchrpc. FundChannelResponse ,
65- * tchrpc. FundChannelResponse , * tchrpc. FundChannelResponse ) {
64+ erinFundingAmount uint64 , pushSat int64 ) (* lnrpc. ChannelPoint ,
65+ * lnrpc. ChannelPoint , * lnrpc. ChannelPoint ) {
6666
6767 ctxb := context .Background ()
6868 assetID := mintedAsset .AssetGenesis .AssetId
@@ -256,7 +256,26 @@ func createTestAssetNetwork(t *harnessTest, net *NetworkHarness, charlieTap,
256256 t .t , erinTap .node , fabiaTap .node , erinFundingAmount , assetID ,
257257 )
258258
259- return fundRespCD , fundRespDY , fundRespEF
259+ chanPointCD := & lnrpc.ChannelPoint {
260+ OutputIndex : uint32 (fundRespCD .OutputIndex ),
261+ FundingTxid : & lnrpc.ChannelPoint_FundingTxidStr {
262+ FundingTxidStr : fundRespCD .Txid ,
263+ },
264+ }
265+ chanPointDY := & lnrpc.ChannelPoint {
266+ OutputIndex : uint32 (fundRespDY .OutputIndex ),
267+ FundingTxid : & lnrpc.ChannelPoint_FundingTxidStr {
268+ FundingTxidStr : fundRespDY .Txid ,
269+ },
270+ }
271+ chanPointEF := & lnrpc.ChannelPoint {
272+ OutputIndex : uint32 (fundRespEF .OutputIndex ),
273+ FundingTxid : & lnrpc.ChannelPoint_FundingTxidStr {
274+ FundingTxidStr : fundRespEF .Txid ,
275+ },
276+ }
277+
278+ return chanPointCD , chanPointDY , chanPointEF
260279}
261280
262281func assertNumAssetUTXOs (t * testing.T , tapdClient * tapClient ,
@@ -586,6 +605,67 @@ func getAssetChannelBalance(t *testing.T, node *HarnessNode, assetID []byte,
586605 balance .RemoteBalance .Sat
587606}
588607
608+ func fetchChannel (t * testing.T , node * HarnessNode ,
609+ chanPoint * lnrpc.ChannelPoint ) * lnrpc.Channel {
610+
611+ ctxb := context .Background ()
612+ ctxt , cancel := context .WithTimeout (ctxb , defaultTimeout )
613+ defer cancel ()
614+
615+ channelResp , err := node .ListChannels (ctxt , & lnrpc.ListChannelsRequest {
616+ ActiveOnly : true ,
617+ })
618+ require .NoError (t , err )
619+
620+ chanFundingHash , err := lnrpc .GetChanPointFundingTxid (chanPoint )
621+ require .NoError (t , err )
622+
623+ chanPointStr := fmt .Sprintf ("%v:%v" , chanFundingHash ,
624+ chanPoint .OutputIndex )
625+
626+ var targetChan * lnrpc.Channel
627+ for _ , channel := range channelResp .Channels {
628+ if channel .ChannelPoint == chanPointStr {
629+ targetChan = channel
630+
631+ break
632+ }
633+ }
634+ require .NotNil (t , targetChan )
635+
636+ return targetChan
637+ }
638+
639+ func assertChannelSatBalance (t * testing.T , node * HarnessNode ,
640+ chanPoint * lnrpc.ChannelPoint , local , remote int64 ) {
641+
642+ targetChan := fetchChannel (t , node , chanPoint )
643+
644+ require .InDelta (t , local , targetChan .LocalBalance , 1 )
645+ require .InDelta (t , remote , targetChan .RemoteBalance , 1 )
646+ }
647+
648+ func assertChannelAssetBalance (t * testing.T , node * HarnessNode ,
649+ chanPoint * lnrpc.ChannelPoint , local , remote uint64 ) {
650+
651+ targetChan := fetchChannel (t , node , chanPoint )
652+
653+ var assetBalance rfqmsg.JsonAssetChannel
654+ err := json .Unmarshal (targetChan .CustomChannelData , & assetBalance )
655+ require .NoError (t , err )
656+
657+ require .Len (t , assetBalance .Assets , 1 )
658+
659+ require .InDelta (t , local , assetBalance .Assets [0 ].LocalBalance , 1 )
660+ require .InDelta (t , remote , assetBalance .Assets [0 ].RemoteBalance , 1 )
661+ }
662+
663+ // addRoutingFee adds the default routing fee (1 part per million fee rate plus
664+ // 1000 milli-satoshi base fee) to the given milli-satoshi amount.
665+ func addRoutingFee (amt lnwire.MilliSatoshi ) lnwire.MilliSatoshi {
666+ return amt + (amt / 1000_000 ) + 1000
667+ }
668+
589669func sendAssetKeySendPayment (t * testing.T , src , dst * HarnessNode , amt uint64 ,
590670 assetID []byte , btcAmt fn.Option [int64 ],
591671 expectedStatus lnrpc.Payment_PaymentStatus ,
@@ -702,9 +782,11 @@ func createAndPayNormalInvoice(t *testing.T, src, rfqPeer, dst *HarnessNode,
702782 })
703783 require .NoError (t , err )
704784
705- return payInvoiceWithAssets (
785+ numUnits , _ := payInvoiceWithAssets (
706786 t , src , rfqPeer , invoiceResp , assetID , smallShards ,
707787 )
788+
789+ return numUnits
708790}
709791
710792func payInvoiceWithSatoshi (t * testing.T , payer * HarnessNode ,
@@ -730,7 +812,7 @@ func payInvoiceWithSatoshi(t *testing.T, payer *HarnessNode,
730812
731813func payInvoiceWithAssets (t * testing.T , payer , rfqPeer * HarnessNode ,
732814 invoice * lnrpc.AddInvoiceResponse , assetID []byte ,
733- smallShards bool ) uint64 {
815+ smallShards bool ) ( uint64 , rfqmath. BigIntFixedPoint ) {
734816
735817 ctxb := context .Background ()
736818 ctxt , cancel := context .WithTimeout (ctxb , defaultTimeout )
@@ -774,19 +856,21 @@ func payInvoiceWithAssets(t *testing.T, payer, rfqPeer *HarnessNode,
774856 rate , err := rfqrpc .UnmarshalFixedPoint (rpcRate )
775857 require .NoError (t , err )
776858
859+ t .Logf ("Got quote for %v asset units per BTC" , rate )
860+
777861 amountMsat := lnwire .MilliSatoshi (decodedInvoice .NumMsat )
778862 milliSatsFP := rfqmath .MilliSatoshiToUnits (amountMsat , * rate )
779863 numUnits := milliSatsFP .ScaleTo (0 ).ToUint64 ()
780- msatPerUnit := uint64 (decodedInvoice .NumMsat ) / numUnits
781- t .Logf ("Got quote for %v asset units at %v msat/unit from peer %s " +
864+ msatPerUnit := float64 (decodedInvoice .NumMsat ) / float64 ( numUnits )
865+ t .Logf ("Got quote for %v asset units at %3f msat/unit from peer %s " +
782866 "with SCID %d" , numUnits , msatPerUnit , peerPubKey ,
783867 acceptedQuote .Scid )
784868
785869 result , err := getAssetPaymentResult (stream )
786870 require .NoError (t , err )
787871 require .Equal (t , lnrpc .Payment_SUCCEEDED , result .Status )
788872
789- return numUnits
873+ return numUnits , * rate
790874}
791875
792876func createAssetInvoice (t * testing.T , dstRfqPeer , dst * HarnessNode ,
@@ -825,19 +909,70 @@ func createAssetInvoice(t *testing.T, dstRfqPeer, dst *HarnessNode,
825909 rate , err := rfqrpc .UnmarshalFixedPoint (rpcRate )
826910 require .NoError (t , err )
827911
912+ t .Logf ("Got quote for %v asset units per BTC" , rate )
913+
828914 assetUnits := rfqmath .NewBigIntFixedPoint (assetAmount , 0 )
829915 numMSats := rfqmath .UnitsToMilliSatoshi (assetUnits , * rate )
830- mSatPerUnit := uint64 (decodedInvoice .NumMsat ) / assetAmount
916+ mSatPerUnit := float64 (decodedInvoice .NumMsat ) / float64 ( assetAmount )
831917
832918 require .EqualValues (t , numMSats , decodedInvoice .NumMsat )
833919
834- t .Logf ("Got quote for %d sats at %v msat/unit from peer %x with SCID " +
835- "%d" , decodedInvoice .NumMsat , mSatPerUnit , dstRfqPeer . PubKey [:] ,
836- resp .AcceptedBuyQuote .Scid )
920+ t .Logf ("Got quote for %d mSats at %3f msat/unit from peer %x with " +
921+ "SCID %d" , decodedInvoice .NumMsat , mSatPerUnit ,
922+ dstRfqPeer . PubKey [:], resp .AcceptedBuyQuote .Scid )
837923
838924 return resp .InvoiceResult
839925}
840926
927+ // assertPaymentHtlcAssets makes sure the payment with the given hash shows the
928+ // individual HTLCs that arrived for it and that they show the correct asset
929+ // amounts for the given ID when decoded.
930+ func assertPaymentHtlcAssets (t * testing.T , node * HarnessNode , payHash []byte ,
931+ assetID []byte , assetAmount uint64 ) {
932+
933+ ctxb := context .Background ()
934+ ctxt , cancel := context .WithTimeout (ctxb , defaultTimeout )
935+ defer cancel ()
936+
937+ stream , err := node .RouterClient .TrackPaymentV2 (
938+ ctxt , & routerrpc.TrackPaymentRequest {
939+ PaymentHash : payHash ,
940+ NoInflightUpdates : true ,
941+ },
942+ )
943+ require .NoError (t , err )
944+
945+ payment , err := stream .Recv ()
946+ require .NoError (t , err )
947+ require .NotNil (t , payment )
948+ require .NotEmpty (t , payment .Htlcs )
949+
950+ t .Logf ("Asset payment: %v" , toProtoJSON (t , payment ))
951+
952+ targetID := hex .EncodeToString (assetID )
953+
954+ var totalAssetAmount uint64
955+ for _ , htlc := range payment .Htlcs {
956+ require .NotNil (t , htlc .Route )
957+ require .NotEmpty (t , htlc .Route .CustomChannelData )
958+
959+ jsonHtlc := & rfqmsg.JsonHtlc {}
960+ err := json .Unmarshal (htlc .Route .CustomChannelData , jsonHtlc )
961+ require .NoError (t , err )
962+
963+ for _ , balance := range jsonHtlc .Balances {
964+ if balance .AssetID != targetID {
965+ continue
966+ }
967+
968+ totalAssetAmount += balance .Amount
969+ }
970+ }
971+
972+ // Due to rounding we allow up to 1 unit of error.
973+ require .InDelta (t , assetAmount , totalAssetAmount , 1 )
974+ }
975+
841976func waitForSendEvent (t * testing.T ,
842977 sendEvents taprpc.TaprootAssets_SubscribeSendEventsClient ,
843978 expectedState tapfreighter.SendState ) {
@@ -862,6 +997,14 @@ type coOpCloseBalanceCheck func(t *testing.T, local, remote *HarnessNode,
862997 closeTx * wire.MsgTx , closeUpdate * lnrpc.ChannelCloseUpdate ,
863998 assetID , groupKey []byte , universeTap * tapClient )
864999
1000+ // noOpCoOpCloseBalanceCheck is a no-op implementation of the co-op close
1001+ // balance check that can be used in tests.
1002+ func noOpCoOpCloseBalanceCheck (_ * testing.T , _ , _ * HarnessNode , _ * wire.MsgTx ,
1003+ _ * lnrpc.ChannelCloseUpdate , _ , _ []byte , _ * tapClient ) {
1004+
1005+ // This is a no-op function.
1006+ }
1007+
8651008// closeAssetChannelAndAssert closes the channel between the local and remote
8661009// node and asserts the final balances of the closing transaction.
8671010func closeAssetChannelAndAssert (t * harnessTest , net * NetworkHarness ,
0 commit comments