@@ -34,6 +34,8 @@ package liquidity
34
34
35
35
import (
36
36
"context"
37
+ "crypto/sha256"
38
+ "encoding/json"
37
39
"errors"
38
40
"fmt"
39
41
"math"
@@ -48,6 +50,7 @@ import (
48
50
"github.com/lightninglabs/loop/loopdb"
49
51
clientrpc "github.com/lightninglabs/loop/looprpc"
50
52
"github.com/lightninglabs/loop/swap"
53
+ "github.com/lightninglabs/taproot-assets/rfqmsg"
51
54
"github.com/lightningnetwork/lnd/clock"
52
55
"github.com/lightningnetwork/lnd/funding"
53
56
"github.com/lightningnetwork/lnd/lntypes"
@@ -218,6 +221,11 @@ type Config struct {
218
221
LoopOutTerms func (ctx context.Context ,
219
222
initiator string ) (* loop.LoopOutTerms , error )
220
223
224
+ // GetAssetPrice returns the price of an asset in satoshis.
225
+ GetAssetPrice func (ctx context.Context , assetId string ,
226
+ peerPubkey []byte , assetAmt uint64 ,
227
+ maxPaymentAmt btcutil.Amount ) (btcutil.Amount , error )
228
+
221
229
// Clock allows easy mocking of time in unit tests.
222
230
Clock clock.Clock
223
231
@@ -305,6 +313,16 @@ func (m *Manager) Run(ctx context.Context) error {
305
313
}
306
314
}
307
315
316
+ // Try to automatically dispach an asset auto-loop.
317
+ for assetID := range m .params .AssetAutoloopParams {
318
+ err = m .easyAssetAutoloop (ctx , assetID )
319
+ if err != nil {
320
+ log .Errorf ("easy asset autoloop " +
321
+ "failed: id: %v, err: %v" ,
322
+ assetID , err )
323
+ }
324
+ }
325
+
308
326
case <- ctx .Done ():
309
327
return ctx .Err ()
310
328
}
@@ -505,6 +523,32 @@ func (m *Manager) easyAutoLoop(ctx context.Context) error {
505
523
return nil
506
524
}
507
525
526
+ // easyAssetAutoloop is the main entry point for the easy auto loop functionality
527
+ // for assets. This function will try to dispatch a swap in order to meet the
528
+ // easy autoloop requirements for the given asset. For easyAutoloop to work
529
+ // there needs to be an EasyAutoloopTarget defined in the parameters. Easy
530
+ // autoloop also uses the configured max inflight swaps and budget rules defined
531
+ // in the parameters.
532
+ func (m * Manager ) easyAssetAutoloop (ctx context.Context , assetID string ) error {
533
+ if ! m .params .Autoloop {
534
+ return nil
535
+ }
536
+
537
+ assetParams , ok := m .params .AssetAutoloopParams [assetID ]
538
+ if ! ok && ! assetParams .EnableEasyOut {
539
+ return nil
540
+ }
541
+
542
+ // First check if we should refresh our budget before calculating any
543
+ // swaps for autoloop.
544
+ m .refreshAutoloopBudget (ctx )
545
+
546
+ // Dispatch the best easy autoloop swap.
547
+ targetAmt := assetParams .LocalTargetAssetAmount
548
+
549
+ return m .dispatchBestAssetEasyAutoloopSwap (ctx , assetID , targetAmt )
550
+ }
551
+
508
552
// ForceAutoLoop force-ticks our auto-out ticker.
509
553
func (m * Manager ) ForceAutoLoop (ctx context.Context ) error {
510
554
select {
@@ -597,7 +641,7 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
597
641
builder := newLoopOutBuilder (m .cfg )
598
642
599
643
channel := m .pickEasyAutoloopChannel (
600
- channels , restrictions , loopOut , loopIn ,
644
+ usableChannels , restrictions , loopOut , loopIn , 0 ,
601
645
)
602
646
if channel == nil {
603
647
return fmt .Errorf ("no eligible channel for easy autoloop" )
@@ -637,7 +681,196 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
637
681
638
682
suggestion , err := builder .buildSwap (
639
683
ctx , channel .PubKeyBytes , outgoing , swapAmt , easyParams ,
640
- nil ,
684
+ )
685
+ if err != nil {
686
+ return err
687
+ }
688
+
689
+ var swp loop.OutRequest
690
+ if t , ok := suggestion .(* loopOutSwapSuggestion ); ok {
691
+ swp = t .OutRequest
692
+ } else {
693
+ return fmt .Errorf ("unexpected swap suggestion type: %T" , t )
694
+ }
695
+
696
+ // Dispatch a sticky loop out.
697
+ go m .dispatchStickyLoopOut (
698
+ ctx , swp , defaultAmountBackoffRetry , defaultAmountBackoff ,
699
+ )
700
+
701
+ return nil
702
+ }
703
+
704
+ // dispatchBestAssetEasyAutoloopSwap tries to dispatch a swap to bring the total
705
+ // local balance back to the target for the given asset.
706
+ func (m * Manager ) dispatchBestAssetEasyAutoloopSwap (ctx context.Context ,
707
+ assetID string , localTarget uint64 ) error {
708
+
709
+ if len (assetID ) != sha256 .Size * 2 {
710
+ return fmt .Errorf ("invalid asset id: %v" , assetID )
711
+ }
712
+
713
+ // Retrieve existing swaps.
714
+ loopOut , err := m .cfg .ListLoopOut (ctx )
715
+ if err != nil {
716
+ return err
717
+ }
718
+
719
+ loopIn , err := m .cfg .ListLoopIn (ctx )
720
+ if err != nil {
721
+ return err
722
+ }
723
+
724
+ // Get a summary of our existing swaps so that we can check our autoloop
725
+ // budget.
726
+ summary := m .checkExistingAutoLoops (ctx , loopOut , loopIn )
727
+
728
+ err = m .checkSummaryBudget (summary )
729
+ if err != nil {
730
+ return err
731
+ }
732
+
733
+ _ , err = m .checkSummaryInflight (summary )
734
+ if err != nil {
735
+ return err
736
+ }
737
+
738
+ // Get all channels in order to calculate current total local balance.
739
+ channels , err := m .cfg .Lnd .Client .ListChannels (ctx , false , false )
740
+ if err != nil {
741
+ return err
742
+ }
743
+
744
+ // If we are running a custom asset, we'll need to get a random asset
745
+ // peer pubkey in order to rfq the asset price.
746
+ var assetPeerPubkey []byte
747
+
748
+ usableChannels := []lndclient.ChannelInfo {}
749
+ localTotal := uint64 (0 )
750
+ for _ , channel := range channels {
751
+ // We are only interested in custom asset channels.
752
+ if ! channelIsCustom (channel ) {
753
+ continue
754
+ }
755
+
756
+ assetData := getCustomAssetData (channel , assetID )
757
+ if assetData == nil {
758
+ continue
759
+ }
760
+
761
+ // We'll overwrite the channel local balance to be
762
+ // the custom asset balance. This allows us to make
763
+ // use of existing logic.
764
+ channel .LocalBalance = btcutil .Amount (assetData .LocalBalance )
765
+ usableChannels = append (usableChannels , channel )
766
+
767
+ // We'll use a random peer pubkey in order to get a rfq for the asset
768
+ // to get a rough amount of sats to swap amount.
769
+ assetPeerPubkey = channel .PubKeyBytes [:]
770
+
771
+ localTotal += assetData .LocalBalance
772
+ }
773
+
774
+ // Since we're only autolooping-out we need to check if we are below
775
+ // the target, meaning that we already meet the requirements.
776
+ if localTotal <= localTarget {
777
+ log .Debugf ("Asset: %v... total local balance %v below target %v" ,
778
+ assetID [:8 ], localTotal , localTarget )
779
+ return nil
780
+ }
781
+
782
+ restrictions , err := m .cfg .Restrictions (
783
+ ctx , swap .TypeOut , getInitiator (m .params ),
784
+ )
785
+ if err != nil {
786
+ return err
787
+ }
788
+
789
+ // Calculate the assetAmount that we want to loop out. If it exceeds the
790
+ // max allowed clamp it to max.
791
+ assetAmount := localTotal - localTarget
792
+
793
+ // We need a request sat amount for the asset price request. We'll use
794
+ // the average of the min and max restrictions.
795
+ assetPriceRequestSatAmt := (restrictions .Minimum + restrictions .Maximum ) / 2
796
+
797
+ // If we run a custom asset, we'll need to convert the asset amount
798
+ // we want to swap to the satoshi amount.
799
+ satAmount , err := m .cfg .GetAssetPrice (
800
+ ctx , assetID , assetPeerPubkey , assetAmount ,
801
+ assetPriceRequestSatAmt ,
802
+ )
803
+ if err != nil {
804
+ return err
805
+ }
806
+
807
+ if satAmount > restrictions .Maximum {
808
+ log .Debugf ("Asset %v easy autoloop: using maximum allowed " +
809
+ "swap amount, maximum=%v, need to swap %v" ,
810
+ assetID [:8 ], restrictions .Maximum , satAmount )
811
+ satAmount = restrictions .Maximum
812
+ }
813
+
814
+ // If the amount we want to loop out is less than the minimum we can't
815
+ // proceed with a swap, so we return early.
816
+ if satAmount < restrictions .Minimum {
817
+ log .Debugf ("Asset %v easy autoloop: swap amount is below" +
818
+ " minimum swap size, minimum=%v, need to swap %v" ,
819
+ assetID [:8 ], restrictions .Minimum , satAmount )
820
+ return nil
821
+ }
822
+
823
+ satsPerAsset := float64 (satAmount ) / float64 (assetAmount )
824
+
825
+ log .Debugf ("Asset %v easy autoloop: local_total=%v, target=%v, " +
826
+ "attempting to loop out %v assets corresponding to %v sats" ,
827
+ assetID [:8 ], localTotal , localTarget , assetAmount , satAmount )
828
+
829
+ // Start building that swap.
830
+ builder := newLoopOutBuilder (m .cfg )
831
+
832
+ channel := m .pickEasyAutoloopChannel (
833
+ usableChannels , restrictions , loopOut , loopIn , satsPerAsset ,
834
+ )
835
+ if channel == nil {
836
+ return fmt .Errorf ("no eligible channel for easy autoloop" )
837
+ }
838
+
839
+ log .Debugf ("Asset %v easy autoloop: picked channel %v with local " +
840
+ "balance %v" , assetID [:8 ], channel .ChannelID ,
841
+ int (channel .LocalBalance ))
842
+
843
+ // If no fee is set, override our current parameters in order to use the
844
+ // default percent limit of easy-autoloop.
845
+ easyParams := m .params
846
+
847
+ switch feeLimit := easyParams .FeeLimit .(type ) {
848
+ case * FeePortion :
849
+ if feeLimit .PartsPerMillion == 0 {
850
+ easyParams .FeeLimit = & FeePortion {
851
+ PartsPerMillion : defaultFeePPM ,
852
+ }
853
+ }
854
+
855
+ default :
856
+ easyParams .FeeLimit = & FeePortion {
857
+ PartsPerMillion : defaultFeePPM ,
858
+ }
859
+ }
860
+
861
+ // Set the swap outgoing channel to the chosen channel.
862
+ outgoing := []lnwire.ShortChannelID {
863
+ lnwire .NewShortChanIDFromInt (channel .ChannelID ),
864
+ }
865
+
866
+ assetSwap := & assetSwapInfo {
867
+ assetID : assetID ,
868
+ peerPubkey : channel .PubKeyBytes [:],
869
+ }
870
+
871
+ suggestion , err := builder .buildSwap (
872
+ ctx , channel .PubKeyBytes , outgoing , satAmount , easyParams ,
873
+ withAssetSwapInfo (assetSwap ),
641
874
)
642
875
if err != nil {
643
876
return err
@@ -1420,7 +1653,7 @@ func (m *Manager) waitForSwapPayment(ctx context.Context, swapHash lntypes.Hash,
1420
1653
// swap conflicts.
1421
1654
func (m * Manager ) pickEasyAutoloopChannel (channels []lndclient.ChannelInfo ,
1422
1655
restrictions * Restrictions , loopOut []* loopdb.LoopOut ,
1423
- loopIn []* loopdb.LoopIn ) * lndclient.ChannelInfo {
1656
+ loopIn []* loopdb.LoopIn , satsPerAsset float64 ) * lndclient.ChannelInfo {
1424
1657
1425
1658
traffic := m .currentSwapTraffic (loopOut , loopIn )
1426
1659
@@ -1434,10 +1667,6 @@ func (m *Manager) pickEasyAutoloopChannel(channels []lndclient.ChannelInfo,
1434
1667
// Check each channel, since channels are already sorted we return the
1435
1668
// first channel that passes all checks.
1436
1669
for _ , channel := range channels {
1437
- if channelIsCustom (channel ) {
1438
- continue
1439
- }
1440
-
1441
1670
shortChanID := lnwire .NewShortChanIDFromInt (channel .ChannelID )
1442
1671
1443
1672
if ! channel .Active {
@@ -1460,7 +1689,16 @@ func (m *Manager) pickEasyAutoloopChannel(channels []lndclient.ChannelInfo,
1460
1689
continue
1461
1690
}
1462
1691
1463
- if channel .LocalBalance < restrictions .Minimum {
1692
+ localBalance := channel .LocalBalance
1693
+
1694
+ // If we're running a custom asset, the local balance is
1695
+ // denominated in the asset's unit, so we convert it to
1696
+ // back to sats to check the minimum.
1697
+ if channelIsCustom (channel ) {
1698
+ localBalance = localBalance .MulF64 (satsPerAsset )
1699
+ }
1700
+
1701
+ if localBalance < restrictions .Minimum {
1464
1702
log .Debugf ("Channel %v cannot be used for easy " +
1465
1703
"autoloop: insufficient local balance %v," +
1466
1704
"minimum is %v, skipping remaining channels" ,
@@ -1571,3 +1809,28 @@ func channelIsCustom(channel lndclient.ChannelInfo) bool {
1571
1809
// don't want to consider it for swaps.
1572
1810
return channel .CustomChannelData != nil
1573
1811
}
1812
+
1813
+ // getCustomAssetData returns the asset data for a custom channel.
1814
+ func getCustomAssetData (channel lndclient.ChannelInfo , assetID string ,
1815
+ ) * rfqmsg.JsonAssetChanInfo {
1816
+
1817
+ if channel .CustomChannelData == nil {
1818
+ return nil
1819
+ }
1820
+
1821
+ var assetData rfqmsg.JsonAssetChannel
1822
+ err := json .Unmarshal (channel .CustomChannelData , & assetData )
1823
+ if err != nil {
1824
+ log .Errorf ("Error unmarshalling custom channel %v data: %v" ,
1825
+ channel .ChannelID , err )
1826
+ return nil
1827
+ }
1828
+
1829
+ for _ , asset := range assetData .Assets {
1830
+ if asset .AssetInfo .AssetGenesis .AssetID == assetID {
1831
+ return & asset
1832
+ }
1833
+ }
1834
+
1835
+ return nil
1836
+ }
0 commit comments