@@ -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 .easyAssetAutoloop (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,40 @@ func (m *Manager) easyAutoLoop(ctx context.Context) error {
509525 return nil
510526}
511527
528+ // easyAssetAutoloop 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 ) easyAssetAutoloop (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+ targetAmt := assetParams .LocalTargetAssetAmount
554+ err := m .dispatchBestAssetEasyAutoloopSwap (ctx , assetID , targetAmt )
555+ if err != nil {
556+ return err
557+ }
558+
559+ return nil
560+ }
561+
512562// ForceAutoLoop force-ticks our auto-out ticker.
513563func (m * Manager ) ForceAutoLoop (ctx context.Context ) error {
514564 select {
@@ -641,7 +691,190 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
641691
642692 suggestion , err := builder .buildSwap (
643693 ctx , channel .PubKeyBytes , outgoing , swapAmt , easyParams ,
644- nil ,
694+ )
695+ if err != nil {
696+ return err
697+ }
698+
699+ var swp loop.OutRequest
700+ if t , ok := suggestion .(* loopOutSwapSuggestion ); ok {
701+ swp = t .OutRequest
702+ } else {
703+ return fmt .Errorf ("unexpected swap suggestion type: %T" , t )
704+ }
705+
706+ // Dispatch a sticky loop out.
707+ go m .dispatchStickyLoopOut (
708+ ctx , swp , defaultAmountBackoffRetry , defaultAmountBackoff ,
709+ )
710+
711+ return nil
712+ }
713+
714+ // dispatchBestAssetEasyAutoloopSwap tries to dispatch a swap to bring the total
715+ // local balance back to the target for the given asset.
716+ func (m * Manager ) dispatchBestAssetEasyAutoloopSwap (ctx context.Context ,
717+ assetID string , localTarget uint64 ) error {
718+
719+ if len (assetID ) != 32 {
720+ return fmt .Errorf ("invalid asset id: %v" , assetID )
721+ }
722+
723+ // Retrieve existing swaps.
724+ loopOut , err := m .cfg .ListLoopOut (ctx )
725+ if err != nil {
726+ return err
727+ }
728+
729+ loopIn , err := m .cfg .ListLoopIn (ctx )
730+ if err != nil {
731+ return err
732+ }
733+
734+ // Get a summary of our existing swaps so that we can check our autoloop
735+ // budget.
736+ summary := m .checkExistingAutoLoops (ctx , loopOut , loopIn )
737+
738+ err = m .checkSummaryBudget (summary )
739+ if err != nil {
740+ return err
741+ }
742+
743+ _ , err = m .checkSummaryInflight (summary )
744+ if err != nil {
745+ return err
746+ }
747+
748+ // Get all channels in order to calculate current total local balance.
749+ channels , err := m .cfg .Lnd .Client .ListChannels (ctx , false , false )
750+ if err != nil {
751+ return err
752+ }
753+
754+ // If we are running a custom asset, we'll need to get a random asset
755+ // peer pubkey in order to rfq the asset price.
756+ var assetPeerPubkey []byte
757+
758+ localTotal := uint64 (0 )
759+ for _ , channel := range channels {
760+ // We are only interested in custom asset channels.
761+ if ! channelIsCustom (channel ) {
762+ continue
763+ }
764+
765+ assetData := getCustomAssetData (channel , assetID )
766+ if assetData == nil {
767+ continue
768+ }
769+
770+ // We'll overwrite the channel local balance to be
771+ // the custom asset balance. This allows us to make
772+ // use of existing logic.
773+ channel .LocalBalance = btcutil .Amount (
774+ assetData .LocalBalance ,
775+ )
776+
777+ // We'll use a random peer pubkey in order to get a rfq for the asset
778+ // to get a rough amount of sats to swap amount.
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 (assetData .LocalBalance )
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
804+ // max 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+ int (channel .LocalBalance ))
847+
848+ // If no fee is set, override our current parameters in order to use the
849+ // default percent limit of easy-autoloop.
850+ easyParams := m .params
851+
852+ switch feeLimit := easyParams .FeeLimit .(type ) {
853+ case * FeePortion :
854+ if feeLimit .PartsPerMillion == 0 {
855+ easyParams .FeeLimit = & FeePortion {
856+ PartsPerMillion : defaultFeePPM ,
857+ }
858+ }
859+ default :
860+ easyParams .FeeLimit = & FeePortion {
861+ PartsPerMillion : defaultFeePPM ,
862+ }
863+ }
864+
865+ // Set the swap outgoing channel to the chosen channel.
866+ outgoing := []lnwire.ShortChannelID {
867+ lnwire .NewShortChanIDFromInt (channel .ChannelID ),
868+ }
869+
870+ assetSwap := & assetSwapInfo {
871+ assetID : assetID ,
872+ peerPubkey : channel .PubKeyBytes [:],
873+ }
874+
875+ suggestion , err := builder .buildSwap (
876+ ctx , channel .PubKeyBytes , outgoing , satAmount , easyParams ,
877+ withAssetSwapInfo (assetSwap ),
645878 )
646879 if err != nil {
647880 return err
@@ -1440,10 +1673,6 @@ func (m *Manager) pickEasyAutoloopChannel(channels []lndclient.ChannelInfo,
14401673 // Check each channel, since channels are already sorted we return the
14411674 // first channel that passes all checks.
14421675 for _ , channel := range channels {
1443- if channelIsCustom (channel ) {
1444- continue
1445- }
1446-
14471676 shortChanID := lnwire .NewShortChanIDFromInt (channel .ChannelID )
14481677
14491678 if ! channel .Active {
@@ -1466,7 +1695,12 @@ func (m *Manager) pickEasyAutoloopChannel(channels []lndclient.ChannelInfo,
14661695 continue
14671696 }
14681697
1469- if channel .LocalBalance < restrictions .Minimum {
1698+ if channel .LocalBalance < restrictions .Minimum &&
1699+ // If we use a custom channel, the local balance is
1700+ // denominated in the asset's unit, so we don't need to
1701+ // check the minimum.
1702+ ! channelIsCustom (channel ) {
1703+
14701704 log .Debugf ("Channel %v cannot be used for easy " +
14711705 "autoloop: insufficient local balance %v," +
14721706 "minimum is %v, skipping remaining channels" ,
@@ -1577,3 +1811,28 @@ func channelIsCustom(channel lndclient.ChannelInfo) bool {
15771811 // don't want to consider it for swaps.
15781812 return channel .CustomChannelData != nil
15791813}
1814+
1815+ // getCustomAssetData returns the asset data for a custom channel.
1816+ func getCustomAssetData (channel lndclient.ChannelInfo , assetID string ,
1817+ ) * rfqmsg.JsonAssetChanInfo {
1818+
1819+ if channel .CustomChannelData == nil {
1820+ return nil
1821+ }
1822+
1823+ var assetData rfqmsg.JsonAssetChannel
1824+ err := json .Unmarshal (channel .CustomChannelData , & assetData )
1825+ if err != nil {
1826+ log .Errorf ("Error unmarshalling custom channel %v data: %v" ,
1827+ channel .ChannelID , err )
1828+ return nil
1829+ }
1830+
1831+ for _ , asset := range assetData .Assets {
1832+ if asset .AssetInfo .AssetGenesis .AssetID == assetID {
1833+ return & asset
1834+ }
1835+ }
1836+
1837+ return nil
1838+ }
0 commit comments