@@ -34,6 +34,8 @@ package liquidity
3434
3535import (
3636 "context"
37+ "crypto/sha256"
38+ "encoding/json"
3739 "errors"
3840 "fmt"
3941 "math"
@@ -48,6 +50,7 @@ import (
4850 "github.com/lightninglabs/loop/loopdb"
4951 clientrpc "github.com/lightninglabs/loop/looprpc"
5052 "github.com/lightninglabs/loop/swap"
53+ "github.com/lightninglabs/taproot-assets/rfqmsg"
5154 "github.com/lightningnetwork/lnd/clock"
5255 "github.com/lightningnetwork/lnd/funding"
5356 "github.com/lightningnetwork/lnd/lntypes"
@@ -218,6 +221,11 @@ type Config struct {
218221 LoopOutTerms func (ctx context.Context ,
219222 initiator string ) (* loop.LoopOutTerms , error )
220223
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+
221229 // Clock allows easy mocking of time in unit tests.
222230 Clock clock.Clock
223231
@@ -305,6 +313,15 @@ func (m *Manager) Run(ctx context.Context) error {
305313 }
306314 }
307315
316+ for assetID := range m .params .AssetAutoloopParams {
317+ err = m .easyAssetAutoloop (ctx , assetID )
318+ if err != nil {
319+ log .Errorf ("easy asset autoloop " +
320+ "failed: id: %v, err: %v" ,
321+ assetID , err )
322+ }
323+ }
324+
308325 case <- ctx .Done ():
309326 return ctx .Err ()
310327 }
@@ -505,6 +522,32 @@ func (m *Manager) easyAutoLoop(ctx context.Context) error {
505522 return nil
506523}
507524
525+ // easyAssetAutoloop is the main entry point for the easy auto loop functionality
526+ // for assets. This function will try to dispatch a swap in order to meet the
527+ // easy autoloop requirements for the given asset. For easyAutoloop to work
528+ // there needs to be an EasyAutoloopTarget defined in the parameters. Easy
529+ // autoloop also uses the configured max inflight swaps and budget rules defined
530+ // in the parameters.
531+ func (m * Manager ) easyAssetAutoloop (ctx context.Context , assetID string ) error {
532+ if ! m .params .Autoloop {
533+ return nil
534+ }
535+
536+ assetParams , ok := m .params .AssetAutoloopParams [assetID ]
537+ if ! ok && ! assetParams .EnableEasyOut {
538+ return nil
539+ }
540+
541+ // First check if we should refresh our budget before calculating any
542+ // swaps for autoloop.
543+ m .refreshAutoloopBudget (ctx )
544+
545+ // Dispatch the best easy autoloop swap.
546+ targetAmt := assetParams .LocalTargetAssetAmount
547+
548+ return m .dispatchBestAssetEasyAutoloopSwap (ctx , assetID , targetAmt )
549+ }
550+
508551// ForceAutoLoop force-ticks our auto-out ticker.
509552func (m * Manager ) ForceAutoLoop (ctx context.Context ) error {
510553 select {
@@ -637,7 +680,195 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
637680
638681 suggestion , err := builder .buildSwap (
639682 ctx , channel .PubKeyBytes , outgoing , swapAmt , easyParams ,
640- nil ,
683+ )
684+ if err != nil {
685+ return err
686+ }
687+
688+ var swp loop.OutRequest
689+ if t , ok := suggestion .(* loopOutSwapSuggestion ); ok {
690+ swp = t .OutRequest
691+ } else {
692+ return fmt .Errorf ("unexpected swap suggestion type: %T" , t )
693+ }
694+
695+ // Dispatch a sticky loop out.
696+ go m .dispatchStickyLoopOut (
697+ ctx , swp , defaultAmountBackoffRetry , defaultAmountBackoff ,
698+ )
699+
700+ return nil
701+ }
702+
703+ // dispatchBestAssetEasyAutoloopSwap tries to dispatch a swap to bring the total
704+ // local balance back to the target for the given asset.
705+ func (m * Manager ) dispatchBestAssetEasyAutoloopSwap (ctx context.Context ,
706+ assetID string , localTarget uint64 ) error {
707+
708+ if len (assetID ) != sha256 .Size * 2 {
709+ return fmt .Errorf ("invalid asset id: %v" , assetID )
710+ }
711+
712+ // Retrieve existing swaps.
713+ loopOut , err := m .cfg .ListLoopOut (ctx )
714+ if err != nil {
715+ return err
716+ }
717+
718+ loopIn , err := m .cfg .ListLoopIn (ctx )
719+ if err != nil {
720+ return err
721+ }
722+
723+ // Get a summary of our existing swaps so that we can check our autoloop
724+ // budget.
725+ summary := m .checkExistingAutoLoops (ctx , loopOut , loopIn )
726+
727+ err = m .checkSummaryBudget (summary )
728+ if err != nil {
729+ return err
730+ }
731+
732+ _ , err = m .checkSummaryInflight (summary )
733+ if err != nil {
734+ return err
735+ }
736+
737+ // Get all channels in order to calculate current total local balance.
738+ channels , err := m .cfg .Lnd .Client .ListChannels (ctx , false , false )
739+ if err != nil {
740+ return err
741+ }
742+
743+ // If we are running a custom asset, we'll need to get a random asset
744+ // peer pubkey in order to rfq the asset price.
745+ var assetPeerPubkey []byte
746+
747+ usableChannels := []lndclient.ChannelInfo {}
748+ localTotal := uint64 (0 )
749+ for _ , channel := range channels {
750+ // We are only interested in custom asset channels.
751+ if ! channelIsCustom (channel ) {
752+ continue
753+ }
754+
755+ assetData := getCustomAssetData (channel , assetID )
756+ if assetData == nil {
757+ continue
758+ }
759+
760+ // We'll overwrite the channel local balance to be
761+ // the custom asset balance. This allows us to make
762+ // use of existing logic.
763+ channel .LocalBalance = btcutil .Amount (assetData .LocalBalance )
764+ usableChannels = append (usableChannels , channel )
765+
766+ // We'll use a random peer pubkey in order to get a rfq for the asset
767+ // to get a rough amount of sats to swap amount.
768+ assetPeerPubkey = channel .PubKeyBytes [:]
769+
770+ localTotal += assetData .LocalBalance
771+ }
772+
773+ // Since we're only autolooping-out we need to check if we are below
774+ // the target, meaning that we already meet the requirements.
775+ if localTotal <= localTarget {
776+ log .Debugf ("Asset: %v... total local balance %v below target %v" ,
777+ assetID [:8 ], localTotal , localTarget )
778+ return nil
779+ }
780+
781+ restrictions , err := m .cfg .Restrictions (
782+ ctx , swap .TypeOut , getInitiator (m .params ),
783+ )
784+ if err != nil {
785+ return err
786+ }
787+
788+ // Calculate the assetAmount that we want to loop out. If it exceeds the
789+ // max allowed clamp it to max.
790+ assetAmount := localTotal - localTarget
791+
792+ // We need a request sat amount for the asset price request. We'll use
793+ // the average of the min and max restrictions.
794+ assetPriceRequestSatAmt := (restrictions .Minimum + restrictions .Maximum ) / 2
795+
796+ // If we run a custom asset, we'll need to convert the asset amount
797+ // we want to swap to the satoshi amount.
798+ satAmount , err := m .cfg .GetAssetPrice (
799+ ctx , assetID , assetPeerPubkey , assetAmount ,
800+ assetPriceRequestSatAmt ,
801+ )
802+ if err != nil {
803+ return err
804+ }
805+
806+ if satAmount > restrictions .Maximum {
807+ log .Debugf ("Asset %v easy autoloop: using maximum allowed " +
808+ "swap amount, maximum=%v, need to swap %v" ,
809+ assetID [:8 ], restrictions .Maximum , satAmount )
810+ satAmount = restrictions .Maximum
811+ }
812+
813+ // If the amount we want to loop out is less than the minimum we can't
814+ // proceed with a swap, so we return early.
815+ if satAmount < restrictions .Minimum {
816+ log .Debugf ("Asset %v easy autoloop: swap amount is below" +
817+ " minimum swap size, minimum=%v, need to swap %v" ,
818+ assetID [:8 ], restrictions .Minimum , satAmount )
819+ return nil
820+ }
821+
822+ satsPerAsset := float64 (satAmount ) / float64 (assetAmount )
823+
824+ log .Debugf ("Asset %v easy autoloop: local_total=%v, target=%v, " +
825+ "attempting to loop out %v assets corresponding to %v sats" ,
826+ assetID [:8 ], localTotal , localTarget , assetAmount , satAmount )
827+
828+ // Start building that swap.
829+ builder := newLoopOutBuilder (m .cfg )
830+
831+ channel := m .pickEasyAutoloopChannel (
832+ usableChannels , restrictions , loopOut , loopIn , satsPerAsset ,
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+ int (channel .LocalBalance ))
841+
842+ // If no fee is set, override our current parameters in order to use the
843+ // default percent limit of easy-autoloop.
844+ easyParams := m .params
845+
846+ switch feeLimit := easyParams .FeeLimit .(type ) {
847+ case * FeePortion :
848+ if feeLimit .PartsPerMillion == 0 {
849+ easyParams .FeeLimit = & FeePortion {
850+ PartsPerMillion : defaultFeePPM ,
851+ }
852+ }
853+ default :
854+ easyParams .FeeLimit = & FeePortion {
855+ PartsPerMillion : defaultFeePPM ,
856+ }
857+ }
858+
859+ // Set the swap outgoing channel to the chosen channel.
860+ outgoing := []lnwire.ShortChannelID {
861+ lnwire .NewShortChanIDFromInt (channel .ChannelID ),
862+ }
863+
864+ assetSwap := & assetSwapInfo {
865+ assetID : assetID ,
866+ peerPubkey : channel .PubKeyBytes [:],
867+ }
868+
869+ suggestion , err := builder .buildSwap (
870+ ctx , channel .PubKeyBytes , outgoing , satAmount , easyParams ,
871+ withAssetSwapInfo (assetSwap ),
641872 )
642873 if err != nil {
643874 return err
@@ -1434,10 +1665,6 @@ func (m *Manager) pickEasyAutoloopChannel(channels []lndclient.ChannelInfo,
14341665 // Check each channel, since channels are already sorted we return the
14351666 // first channel that passes all checks.
14361667 for _ , channel := range channels {
1437- if channelIsCustom (channel ) {
1438- continue
1439- }
1440-
14411668 shortChanID := lnwire .NewShortChanIDFromInt (channel .ChannelID )
14421669
14431670 if ! channel .Active {
@@ -1460,7 +1687,17 @@ func (m *Manager) pickEasyAutoloopChannel(channels []lndclient.ChannelInfo,
14601687 continue
14611688 }
14621689
1463- if channel .LocalBalance < restrictions .Minimum {
1690+ localBalance := channel .LocalBalance
1691+
1692+ // If we're running a custom asset, the local balance is
1693+ // denominated in the asset's unit, so we convert it to
1694+ // back to sats to check the minimum.
1695+ if channelIsCustom (channel ) {
1696+ localBalance = localBalance .MulF64 (satsPerAsset )
1697+ }
1698+
1699+ if localBalance < restrictions .Minimum {
1700+
14641701 log .Debugf ("Channel %v cannot be used for easy " +
14651702 "autoloop: insufficient local balance %v," +
14661703 "minimum is %v, skipping remaining channels" ,
@@ -1571,3 +1808,28 @@ func channelIsCustom(channel lndclient.ChannelInfo) bool {
15711808 // don't want to consider it for swaps.
15721809 return channel .CustomChannelData != nil
15731810}
1811+
1812+ // getCustomAssetData returns the asset data for a custom channel.
1813+ func getCustomAssetData (channel lndclient.ChannelInfo , assetID string ,
1814+ ) * rfqmsg.JsonAssetChanInfo {
1815+
1816+ if channel .CustomChannelData == nil {
1817+ return nil
1818+ }
1819+
1820+ var assetData rfqmsg.JsonAssetChannel
1821+ err := json .Unmarshal (channel .CustomChannelData , & assetData )
1822+ if err != nil {
1823+ log .Errorf ("Error unmarshalling custom channel %v data: %v" ,
1824+ channel .ChannelID , err )
1825+ return nil
1826+ }
1827+
1828+ for _ , asset := range assetData .Assets {
1829+ if asset .AssetInfo .AssetGenesis .AssetID == assetID {
1830+ return & asset
1831+ }
1832+ }
1833+
1834+ return nil
1835+ }
0 commit comments