@@ -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 uint64 , minSatAmt btcutil.Amount ) (
226+ btcutil.Amount , 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 }
@@ -509,6 +525,42 @@ func (m *Manager) easyAutoLoop(ctx context.Context) error {
509525 return nil
510526}
511527
528+ // easyAutoLoopAsset is the main entry point for the easy auto loop functionality
529+ // for assets. This function will try to dispatch a swap in order to meet the
530+ // easy autoloop requirements for the given asset. For easyAutoloop to work
531+ // there needs to be an EasyAutoloopTarget defined in the parameters. Easy
532+ // autoloop also uses the configured max inflight swaps and budget rules defined
533+ // in the parameters.
534+ func (m * Manager ) easyAutoLoopAsset (ctx context.Context , assetID string ) error {
535+ if ! m .params .Autoloop {
536+ return nil
537+ }
538+
539+ assetParams , ok := m .params .AssetAutoloopParams [assetID ]
540+ if ! ok {
541+ return nil
542+ }
543+
544+ if ! assetParams .EnableEasyOut {
545+ return nil
546+ }
547+
548+ // First check if we should refresh our budget before calculating any
549+ // swaps for autoloop.
550+ m .refreshAutoloopBudget (ctx )
551+
552+ // Dispatch the best easy autoloop swap.
553+ err := m .dispatchBestAssetEasyAutoloopSwap (
554+ ctx , assetID ,
555+ assetParams .EasyAutoloopTargetAmount ,
556+ )
557+ if err != nil {
558+ return err
559+ }
560+
561+ return nil
562+ }
563+
512564// ForceAutoLoop force-ticks our auto-out ticker.
513565func (m * Manager ) ForceAutoLoop (ctx context.Context ) error {
514566 select {
@@ -641,7 +693,195 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
641693
642694 suggestion , err := builder .buildSwap (
643695 ctx , channel .PubKeyBytes , outgoing , swapAmt , easyParams ,
644- nil ,
696+ )
697+ if err != nil {
698+ return err
699+ }
700+
701+ var swp loop.OutRequest
702+ if t , ok := suggestion .(* loopOutSwapSuggestion ); ok {
703+ swp = t .OutRequest
704+ } else {
705+ return fmt .Errorf ("unexpected swap suggestion type: %T" , t )
706+ }
707+
708+ // Dispatch a sticky loop out.
709+ go m .dispatchStickyLoopOut (
710+ ctx , swp , defaultAmountBackoffRetry , defaultAmountBackoff ,
711+ )
712+
713+ return nil
714+ }
715+
716+ // dispatchBestAssetEasyAutoloopSwap tries to dispatch a swap to bring the total
717+ // local balance back to the target for the given asset.
718+ func (m * Manager ) dispatchBestAssetEasyAutoloopSwap (ctx context.Context ,
719+ assetID string , localTarget uint64 ) error {
720+
721+ if assetID == "" && len (assetID ) != 32 {
722+ return fmt .Errorf ("invalid asset id: %v" , assetID )
723+ }
724+
725+ // Retrieve existing swaps.
726+ loopOut , err := m .cfg .ListLoopOut (ctx )
727+ if err != nil {
728+ return err
729+ }
730+
731+ loopIn , err := m .cfg .ListLoopIn (ctx )
732+ if err != nil {
733+ return err
734+ }
735+
736+ // Get a summary of our existing swaps so that we can check our autoloop
737+ // budget.
738+ summary := m .checkExistingAutoLoops (ctx , loopOut , loopIn )
739+
740+ err = m .checkSummaryBudget (summary )
741+ if err != nil {
742+ return err
743+ }
744+
745+ _ , err = m .checkSummaryInflight (summary )
746+ if err != nil {
747+ return err
748+ }
749+
750+ // Get all channels in order to calculate current total local balance.
751+ channels , err := m .cfg .Lnd .Client .ListChannels (ctx , false , false )
752+ if err != nil {
753+ return err
754+ }
755+
756+ // If we are running a custom asset, we'll need to get a random asset
757+ // peer pubkey in order to rfq the asset price.
758+ var assetPeerPubkey []byte
759+
760+ localTotal := uint64 (0 )
761+ for _ , channel := range channels {
762+ // We'll only interested in custom asset channels.
763+ if ! channelIsCustom (channel ) {
764+ continue
765+ }
766+
767+ assetData := getCustomAssetData (channel , assetID )
768+ if assetData == nil {
769+ continue
770+ }
771+
772+ // We'll overwrite the channel local balance to be
773+ // the custom asset balance. This allows us to make
774+ // use of existing logic.
775+ channel .LocalBalance = btcutil .Amount (
776+ assetData .LocalBalance ,
777+ )
778+
779+ assetPeerPubkey = channel .PubKeyBytes [:]
780+
781+ localTotal += assetData .LocalBalance
782+
783+ // We'll overwrite the channel local balance in order to
784+ // reuse channel selection logic.
785+ channel .LocalBalance = btcutil .Amount (localTotal )
786+ }
787+
788+ // Since we're only autolooping-out we need to check if we are below
789+ // the target, meaning that we already meet the requirements.
790+ if localTotal <= localTarget {
791+ log .Debugf ("asset: %v... total local balance %v below target %v" ,
792+ assetID [:8 ], localTotal , localTarget )
793+ return nil
794+ }
795+
796+ restrictions , err := m .cfg .Restrictions (
797+ ctx , swap .TypeOut , getInitiator (m .params ),
798+ )
799+ if err != nil {
800+ return err
801+ }
802+
803+ // Calculate the assetAmount that we want to loop out. If it exceeds the max
804+ // allowed clamp it to max.
805+ assetAmount := localTotal - localTarget
806+
807+ // If we run a custom asset, we'll need to convert the asset amount
808+ // we want to swap to the satoshi amount.
809+ satAmount , err := m .cfg .GetAssetPrice (
810+ ctx , assetID , assetPeerPubkey , assetAmount ,
811+ restrictions .Minimum ,
812+ )
813+ if err != nil {
814+ return err
815+ }
816+
817+ if satAmount > restrictions .Maximum {
818+ satAmount = restrictions .Maximum
819+ }
820+
821+ // If the amount we want to loop out is less than the minimum we can't
822+ // proceed with a swap, so we return early.
823+ if satAmount < restrictions .Minimum {
824+ log .Debugf ("asset %v easy autoloop: swap amount is below" +
825+ " minimum swap size, minimum=%v, need to swap %v" ,
826+ assetID [:8 ], restrictions .Minimum , satAmount )
827+ return nil
828+ }
829+
830+ log .Debugf ("asset %v easy autoloop: local_total=%v, target=%v, " +
831+ "attempting to loop out %v assets, %v sats" , assetID [:8 ],
832+ localTotal , localTarget , assetAmount , satAmount )
833+
834+ // Start building that swap.
835+ builder := newLoopOutBuilder (m .cfg )
836+
837+ channel := m .pickEasyAutoloopChannel (
838+ channels , restrictions , loopOut , loopIn ,
839+ )
840+ if channel == nil {
841+ return fmt .Errorf ("no eligible channel for easy autoloop" )
842+ }
843+
844+ log .Debugf ("asset %v easy autoloop: picked channel %v with local" +
845+ " balance %v" , assetID [:8 ], channel .ChannelID ,
846+ channel .LocalBalance )
847+
848+ swapAmt , err := btcutil .NewAmount (
849+ math .Min (channel .LocalBalance .ToBTC (), satAmount .ToBTC ()),
850+ )
851+ if err != nil {
852+ return err
853+ }
854+
855+ // If no fee is set, override our current parameters in order to use the
856+ // default percent limit of easy-autoloop.
857+ easyParams := m .params
858+
859+ switch feeLimit := easyParams .FeeLimit .(type ) {
860+ case * FeePortion :
861+ if feeLimit .PartsPerMillion == 0 {
862+ easyParams .FeeLimit = & FeePortion {
863+ PartsPerMillion : defaultFeePPM ,
864+ }
865+ }
866+ default :
867+ easyParams .FeeLimit = & FeePortion {
868+ PartsPerMillion : defaultFeePPM ,
869+ }
870+ }
871+
872+ // Set the swap outgoing channel to the chosen channel.
873+ outgoing := []lnwire.ShortChannelID {
874+ lnwire .NewShortChanIDFromInt (channel .ChannelID ),
875+ }
876+
877+ assetSwap := & assetSwapInfo {
878+ assetID : assetID ,
879+ peerPubkey : channel .PubKeyBytes [:],
880+ }
881+
882+ suggestion , err := builder .buildSwap (
883+ ctx , channel .PubKeyBytes , outgoing , swapAmt , easyParams ,
884+ withAssetSwapInfo (assetSwap ),
645885 )
646886 if err != nil {
647887 return err
@@ -1440,10 +1680,6 @@ func (m *Manager) pickEasyAutoloopChannel(channels []lndclient.ChannelInfo,
14401680 // Check each channel, since channels are already sorted we return the
14411681 // first channel that passes all checks.
14421682 for _ , channel := range channels {
1443- if channelIsCustom (channel ) {
1444- continue
1445- }
1446-
14471683 shortChanID := lnwire .NewShortChanIDFromInt (channel .ChannelID )
14481684
14491685 if ! channel .Active {
@@ -1466,7 +1702,12 @@ func (m *Manager) pickEasyAutoloopChannel(channels []lndclient.ChannelInfo,
14661702 continue
14671703 }
14681704
1469- if channel .LocalBalance < restrictions .Minimum {
1705+ if channel .LocalBalance < restrictions .Minimum &&
1706+ // If we use a custom channel, the local balance is
1707+ // denominated in the asset's unit, so we don't need to
1708+ // check the minimum.
1709+ ! channelIsCustom (channel ) {
1710+
14701711 log .Debugf ("Channel %v cannot be used for easy " +
14711712 "autoloop: insufficient local balance %v," +
14721713 "minimum is %v, skipping remaining channels" ,
@@ -1577,3 +1818,28 @@ func channelIsCustom(channel lndclient.ChannelInfo) bool {
15771818 // don't want to consider it for swaps.
15781819 return channel .CustomChannelData != nil
15791820}
1821+
1822+ // getCustomAssetData returns the asset data for a custom channel.
1823+ func getCustomAssetData (channel lndclient.ChannelInfo , assetID string ,
1824+ ) * rfqmsg.JsonAssetChanInfo {
1825+
1826+ if channel .CustomChannelData == nil {
1827+ return nil
1828+ }
1829+
1830+ var assetData rfqmsg.JsonAssetChannel
1831+ err := json .Unmarshal (channel .CustomChannelData , & assetData )
1832+ if err != nil {
1833+ log .Errorf ("Error unmarshalling custom channel %v data: %v" ,
1834+ channel .ChannelID , err )
1835+ return nil
1836+ }
1837+
1838+ for _ , asset := range assetData .Assets {
1839+ if asset .AssetInfo .AssetGenesis .AssetID == assetID {
1840+ return & asset
1841+ }
1842+ }
1843+
1844+ return nil
1845+ }
0 commit comments