@@ -34,6 +34,7 @@ package liquidity
3434
3535import (
3636 "context"
37+ "encoding/json"
3738 "errors"
3839 "fmt"
3940 "math"
@@ -48,6 +49,7 @@ import (
4849 "github.com/lightninglabs/loop/loopdb"
4950 clientrpc "github.com/lightninglabs/loop/looprpc"
5051 "github.com/lightninglabs/loop/swap"
52+ "github.com/lightninglabs/taproot-assets/rfqmsg"
5153 "github.com/lightningnetwork/lnd/clock"
5254 "github.com/lightningnetwork/lnd/funding"
5355 "github.com/lightningnetwork/lnd/lntypes"
@@ -218,6 +220,11 @@ type Config struct {
218220 LoopOutTerms func (ctx context.Context ,
219221 initiator string ) (* loop.LoopOutTerms , error )
220222
223+ // GetAssetPrice returns the price of an asset in satoshis.
224+ GetAssetPrice func (ctx context.Context , assetId string ,
225+ peerPubkey []byte , assetAmt , minSatAmt uint64 ) (btcutil.Amount ,
226+ error )
227+
221228 // Clock allows easy mocking of time in unit tests.
222229 Clock clock.Clock
223230
@@ -305,6 +312,15 @@ func (m *Manager) Run(ctx context.Context) error {
305312 }
306313 }
307314
315+ for assetID := range m .params .AssetAutoloopParams {
316+ err = m .easyAutoLoopAsset (ctx , assetID )
317+ if err != nil {
318+ log .Errorf ("easy autoloop asset " +
319+ "failed: id: %v, err: %v" ,
320+ assetID , err )
321+ }
322+ }
323+
308324 case <- ctx .Done ():
309325 return ctx .Err ()
310326 }
@@ -501,7 +517,45 @@ func (m *Manager) easyAutoLoop(ctx context.Context) error {
501517 m .refreshAutoloopBudget (ctx )
502518
503519 // Dispatch the best easy autoloop swap.
504- err := m .dispatchBestEasyAutoloopSwap (ctx )
520+ err := m .dispatchBestEasyAutoloopSwap (
521+ ctx , m .params .EasyAutoloopTarget ,
522+ )
523+ if err != nil {
524+ return err
525+ }
526+
527+ return nil
528+ }
529+
530+ // easyAutoLoopAsset is the main entry point for the easy auto loop functionality
531+ // for assets. This function will try to dispatch a swap in order to meet the
532+ // easy autoloop requirements for the given asset. For easyAutoloop to work
533+ // there needs to be an EasyAutoloopTarget defined in the parameters. Easy
534+ // autoloop also uses the configured max inflight swaps and budget rules defined
535+ // in the parameters.
536+ func (m * Manager ) easyAutoLoopAsset (ctx context.Context , assetID string ) error {
537+ if ! m .params .Autoloop {
538+ return nil
539+ }
540+
541+ assetParams , ok := m .params .AssetAutoloopParams [assetID ]
542+ if ! ok {
543+ return nil
544+ }
545+
546+ if ! assetParams .EnableEasyOut {
547+ return nil
548+ }
549+
550+ // First check if we should refresh our budget before calculating any
551+ // swaps for autoloop.
552+ m .refreshAutoloopBudget (ctx )
553+
554+ // Dispatch the best easy autoloop swap.
555+ err := m .dispatchBestAssetEasyAutoloopSwap (
556+ ctx , assetID ,
557+ assetParams .EasyAutoloopTargetAmount ,
558+ )
505559 if err != nil {
506560 return err
507561 }
@@ -522,7 +576,9 @@ func (m *Manager) ForceAutoLoop(ctx context.Context) error {
522576
523577// dispatchBestEasyAutoloopSwap tries to dispatch a swap to bring the total
524578// local balance back to the target.
525- func (m * Manager ) dispatchBestEasyAutoloopSwap (ctx context.Context ) error {
579+ func (m * Manager ) dispatchBestEasyAutoloopSwap (ctx context.Context ,
580+ localTarget btcutil.Amount ) error {
581+
526582 // Retrieve existing swaps.
527583 loopOut , err := m .cfg .ListLoopOut (ctx )
528584 if err != nil {
@@ -559,14 +615,15 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
559615 if channelIsCustom (channel ) {
560616 continue
561617 }
618+
562619 localTotal += channel .LocalBalance
563620 }
564621
565622 // Since we're only autolooping-out we need to check if we are below
566623 // the target, meaning that we already meet the requirements.
567- if localTotal <= m . params . EasyAutoloopTarget {
624+ if localTotal <= localTarget {
568625 log .Debugf ("total local balance %v below target %v" ,
569- localTotal , m . params . EasyAutoloopTarget )
626+ localTotal , localTarget )
570627 return nil
571628 }
572629
@@ -579,10 +636,7 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
579636
580637 // Calculate the amount that we want to loop out. If it exceeds the max
581638 // allowed clamp it to max.
582- amount := localTotal - m .params .EasyAutoloopTarget
583- if amount > restrictions .Maximum {
584- amount = restrictions .Maximum
585- }
639+ amount := localTotal - localTarget
586640
587641 // If the amount we want to loop out is less than the minimum we can't
588642 // proceed with a swap, so we return early.
@@ -595,7 +649,7 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
595649
596650 log .Debugf ("easy autoloop: local_total=%v, target=%v, " +
597651 "attempting to loop out %v" , localTotal ,
598- m . params . EasyAutoloopTarget , amount )
652+ localTarget , amount )
599653
600654 // Start building that swap.
601655 builder := newLoopOutBuilder (m .cfg )
@@ -610,8 +664,184 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
610664 log .Debugf ("easy autoloop: picked channel %v with local balance %v" ,
611665 channel .ChannelID , channel .LocalBalance )
612666
613- swapAmt , err := btcutil .NewAmount (
614- math .Min (channel .LocalBalance .ToBTC (), amount .ToBTC ()),
667+ // If no fee is set, override our current parameters in order to use the
668+ // default percent limit of easy-autoloop.
669+ easyParams := m .params
670+
671+ switch feeLimit := easyParams .FeeLimit .(type ) {
672+ case * FeePortion :
673+ if feeLimit .PartsPerMillion == 0 {
674+ easyParams .FeeLimit = & FeePortion {
675+ PartsPerMillion : defaultFeePPM ,
676+ }
677+ }
678+ default :
679+ easyParams .FeeLimit = & FeePortion {
680+ PartsPerMillion : defaultFeePPM ,
681+ }
682+ }
683+
684+ // Set the swap outgoing channel to the chosen channel.
685+ outgoing := []lnwire.ShortChannelID {
686+ lnwire .NewShortChanIDFromInt (channel .ChannelID ),
687+ }
688+
689+ suggestion , err := builder .buildSwap (
690+ ctx , channel .PubKeyBytes , outgoing , amount , easyParams ,
691+ )
692+ if err != nil {
693+ return err
694+ }
695+
696+ var swp loop.OutRequest
697+ if t , ok := suggestion .(* loopOutSwapSuggestion ); ok {
698+ swp = t .OutRequest
699+ } else {
700+ return fmt .Errorf ("unexpected swap suggestion type: %T" , t )
701+ }
702+
703+ // Dispatch a sticky loop out.
704+ go m .dispatchStickyLoopOut (
705+ ctx , swp , defaultAmountBackoffRetry , defaultAmountBackoff ,
706+ )
707+
708+ return nil
709+ }
710+
711+ // dispatchBestAssetEasyAutoloopSwap tries to dispatch a swap to bring the total
712+ // local balance back to the target for the given asset.
713+ func (m * Manager ) dispatchBestAssetEasyAutoloopSwap (ctx context.Context ,
714+ assetID string , localTarget uint64 ) error {
715+ if assetID == "" && len (assetID ) != 32 {
716+ return fmt .Errorf ("invalid asset id: %v" , assetID )
717+ }
718+
719+ // Retrieve existing swaps.
720+ loopOut , err := m .cfg .ListLoopOut (ctx )
721+ if err != nil {
722+ return err
723+ }
724+
725+ loopIn , err := m .cfg .ListLoopIn (ctx )
726+ if err != nil {
727+ return err
728+ }
729+
730+ // Get a summary of our existing swaps so that we can check our autoloop
731+ // budget.
732+ summary := m .checkExistingAutoLoops (ctx , loopOut , loopIn )
733+
734+ err = m .checkSummaryBudget (summary )
735+ if err != nil {
736+ return err
737+ }
738+
739+ _ , err = m .checkSummaryInflight (summary )
740+ if err != nil {
741+ return err
742+ }
743+
744+ // Get all channels in order to calculate current total local balance.
745+ channels , err := m .cfg .Lnd .Client .ListChannels (ctx , false , false )
746+ if err != nil {
747+ return err
748+ }
749+
750+ // If we are running a custom asset, we'll need to get a random asset
751+ // peer pubkey in order to rfq the asset price.
752+ var assetPeerPubkey []byte
753+
754+ localTotal := uint64 (0 )
755+ for _ , channel := range channels {
756+ // We'll only interested in custom asset channels.
757+ if ! channelIsCustom (channel ) {
758+ continue
759+ }
760+
761+ assetData := getCustomAssetData (channel , assetID )
762+ if assetData == nil {
763+ continue
764+ }
765+
766+ // We'll overwrite the channel local balance to be
767+ // the custom asset balance. This allows us to make
768+ // use of existing logic.
769+ channel .LocalBalance = btcutil .Amount (
770+ assetData .LocalBalance ,
771+ )
772+
773+ assetPeerPubkey = channel .PubKeyBytes [:]
774+
775+ localTotal += assetData .LocalBalance
776+
777+ // We'll overwrite the channel local balance in order to
778+ // reuse channel selection logic.
779+ channel .LocalBalance = btcutil .Amount (localTotal )
780+ }
781+
782+ // Since we're only autolooping-out we need to check if we are below
783+ // the target, meaning that we already meet the requirements.
784+ if localTotal <= localTarget {
785+ log .Debugf ("asset: %v... total local balance %v below target %v" ,
786+ assetID [:8 ], localTotal , localTarget )
787+ return nil
788+ }
789+
790+ restrictions , err := m .cfg .Restrictions (
791+ ctx , swap .TypeOut , getInitiator (m .params ),
792+ )
793+ if err != nil {
794+ return err
795+ }
796+
797+ // Calculate the assetAmount that we want to loop out. If it exceeds the max
798+ // allowed clamp it to max.
799+ assetAmount := localTotal - localTarget
800+
801+ // If we run a custom asset, we'll need to convert the asset amount
802+ // we want to swap to the satoshi amount.
803+ satAmount , err := m .cfg .GetAssetPrice (
804+ ctx , assetID , assetPeerPubkey , uint64 (assetAmount ),
805+ uint64 (restrictions .Minimum ),
806+ )
807+ if err != nil {
808+ return err
809+ }
810+
811+ if satAmount > restrictions .Maximum {
812+ satAmount = restrictions .Maximum
813+ }
814+
815+ // If the amount we want to loop out is less than the minimum we can't
816+ // proceed with a swap, so we return early.
817+ if satAmount < restrictions .Minimum {
818+ log .Debugf ("asset %v easy autoloop: swap amount is below" +
819+ " minimum swap size, minimum=%v, need to swap %v" ,
820+ assetID [:8 ], restrictions .Minimum , satAmount )
821+ return nil
822+ }
823+
824+ log .Debugf ("asset %v easy autoloop: local_total=%v, target=%v, " +
825+ "attempting to loop out %v" , assetID [:8 ], localTotal ,
826+ localTarget , assetAmount )
827+
828+ // Start building that swap.
829+ builder := newLoopOutBuilder (m .cfg )
830+
831+ channel := m .pickEasyAutoloopChannel (
832+ channels , restrictions , loopOut , loopIn ,
833+ )
834+ if channel == nil {
835+ return fmt .Errorf ("no eligible channel for easy autoloop" )
836+ }
837+
838+ log .Debugf ("asset %v easy autoloop: picked channel %v with local" +
839+ " balance %v" , assetID [:8 ], channel .ChannelID ,
840+ channel .LocalBalance )
841+
842+ swapAmt := satAmount
843+ swapAmt , err = btcutil .NewAmount (
844+ math .Min (channel .LocalBalance .ToBTC (), satAmount .ToBTC ()),
615845 )
616846 if err != nil {
617847 return err
@@ -639,9 +869,14 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
639869 lnwire .NewShortChanIDFromInt (channel .ChannelID ),
640870 }
641871
872+ assetSwap := & assetSwapInfo {
873+ assetID : assetID ,
874+ peerPubkey : channel .PubKeyBytes [:],
875+ }
876+
642877 suggestion , err := builder .buildSwap (
643878 ctx , channel .PubKeyBytes , outgoing , swapAmt , easyParams ,
644- nil ,
879+ withAssetSwapInfo ( assetSwap ) ,
645880 )
646881 if err != nil {
647882 return err
@@ -1440,10 +1675,6 @@ func (m *Manager) pickEasyAutoloopChannel(channels []lndclient.ChannelInfo,
14401675 // Check each channel, since channels are already sorted we return the
14411676 // first channel that passes all checks.
14421677 for _ , channel := range channels {
1443- if channelIsCustom (channel ) {
1444- continue
1445- }
1446-
14471678 shortChanID := lnwire .NewShortChanIDFromInt (channel .ChannelID )
14481679
14491680 if ! channel .Active {
@@ -1466,7 +1697,12 @@ func (m *Manager) pickEasyAutoloopChannel(channels []lndclient.ChannelInfo,
14661697 continue
14671698 }
14681699
1469- if channel .LocalBalance < restrictions .Minimum {
1700+ if channel .LocalBalance < restrictions .Minimum &&
1701+ // If we use a custom channel, the local balance is
1702+ // denominated in the asset's unit, so we don't need to
1703+ // check the minimum.
1704+ ! channelIsCustom (channel ) {
1705+
14701706 log .Debugf ("Channel %v cannot be used for easy " +
14711707 "autoloop: insufficient local balance %v," +
14721708 "minimum is %v, skipping remaining channels" ,
@@ -1577,3 +1813,28 @@ func channelIsCustom(channel lndclient.ChannelInfo) bool {
15771813 // don't want to consider it for swaps.
15781814 return channel .CustomChannelData != nil
15791815}
1816+
1817+ // getCustomAssetData returns the asset data for a custom channel.
1818+ func getCustomAssetData (channel lndclient.ChannelInfo , assetID string ,
1819+ ) * rfqmsg.JsonAssetChanInfo {
1820+
1821+ if channel .CustomChannelData == nil {
1822+ return nil
1823+ }
1824+
1825+ var assetData rfqmsg.JsonAssetChannel
1826+ err := json .Unmarshal (channel .CustomChannelData , & assetData )
1827+ if err != nil {
1828+ log .Errorf ("Error unmarshalling custom channel %v data: %v" ,
1829+ channel .ChannelID , err )
1830+ return nil
1831+ }
1832+
1833+ for _ , asset := range assetData .Assets {
1834+ if asset .AssetInfo .AssetGenesis .AssetID == assetID {
1835+ return & asset
1836+ }
1837+ }
1838+
1839+ return nil
1840+ }
0 commit comments