@@ -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+ assetAmt uint64 , minSatAmt btcutil.Amount ) (btcutil.Amount ,
226+ error )
227+
221228 // Clock allows easy mocking of time in unit tests.
222229 Clock clock.Clock
223230
@@ -305,6 +312,14 @@ 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 %v" +
319+ " failed: %v" , assetId , err )
320+ }
321+ }
322+
308323 case <- ctx .Done ():
309324 return ctx .Err ()
310325 }
@@ -501,7 +516,38 @@ func (m *Manager) easyAutoLoop(ctx context.Context) error {
501516 m .refreshAutoloopBudget (ctx )
502517
503518 // Dispatch the best easy autoloop swap.
504- err := m .dispatchBestEasyAutoloopSwap (ctx )
519+ err := m .dispatchBestEasyAutoloopSwap (
520+ ctx , "" , m .params .EasyAutoloopTarget ,
521+ )
522+ if err != nil {
523+ return err
524+ }
525+
526+ return nil
527+ }
528+
529+ // easyAutoLoopAsset is the main entry point for the easy auto loop functionality
530+ // for assets. This function will try to dispatch a swap in order to meet the
531+ // easy autoloop requirements for the given asset. For easyAutoloop to work
532+ // there needs to be an EasyAutoloopTarget defined in the parameters. Easy
533+ // autoloop also uses the configured max inflight swaps and budget rules defined
534+ // in the parameters.
535+ func (m * Manager ) easyAutoLoopAsset (ctx context.Context , assetId string ) error {
536+ assetParams := m .params .AssetAutoloopParams [assetId ]
537+
538+ if ! 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+ err := m .dispatchBestEasyAutoloopSwap (
548+ ctx , assetId ,
549+ btcutil .Amount (assetParams .EasyAutoloopTargetAmount ),
550+ )
505551 if err != nil {
506552 return err
507553 }
@@ -522,7 +568,9 @@ func (m *Manager) ForceAutoLoop(ctx context.Context) error {
522568
523569// dispatchBestEasyAutoloopSwap tries to dispatch a swap to bring the total
524570// local balance back to the target.
525- func (m * Manager ) dispatchBestEasyAutoloopSwap (ctx context.Context ) error {
571+ func (m * Manager ) dispatchBestEasyAutoloopSwap (ctx context.Context ,
572+ assetId string , localTarget btcutil.Amount ) error {
573+
526574 // Retrieve existing swaps.
527575 loopOut , err := m .cfg .ListLoopOut (ctx )
528576 if err != nil {
@@ -557,16 +605,30 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
557605 localTotal := btcutil .Amount (0 )
558606 for _ , channel := range channels {
559607 if channelIsCustom (channel ) {
560- continue
608+ if assetId == "" {
609+ continue
610+ }
611+ assetData := getCustomAssetData (channel , assetId )
612+
613+ if assetData == nil {
614+ continue
615+ }
616+ // We'll overwrite the channel local balance to be
617+ // the custom asset balance. This allows us to make
618+ // use of existing logic.
619+ channel .LocalBalance = btcutil .Amount (
620+ assetData .LocalBalance ,
621+ )
561622 }
623+
562624 localTotal += channel .LocalBalance
563625 }
564626
565627 // Since we're only autolooping-out we need to check if we are below
566628 // the target, meaning that we already meet the requirements.
567- if localTotal <= m . params . EasyAutoloopTarget {
629+ if localTotal <= localTarget {
568630 log .Debugf ("total local balance %v below target %v" ,
569- localTotal , m . params . EasyAutoloopTarget )
631+ localTotal , localTarget )
570632 return nil
571633 }
572634
@@ -579,23 +641,36 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
579641
580642 // Calculate the amount that we want to loop out. If it exceeds the max
581643 // allowed clamp it to max.
582- amount := localTotal - m .params .EasyAutoloopTarget
583- if amount > restrictions .Maximum {
584- amount = restrictions .Maximum
644+ amount := localTotal - localTarget
645+ satAmount := amount
646+
647+ // If we run a custom asset, we'll need to convert the asset amount
648+ // we want to swap to the satoshi amount.
649+ if assetId != "" {
650+ satAmount , err = m .cfg .GetAssetPrice (
651+ ctx , assetId , uint64 (amount ), restrictions .Minimum ,
652+ )
653+ if err != nil {
654+ return err
655+ }
656+ }
657+
658+ if satAmount > restrictions .Maximum {
659+ satAmount = restrictions .Maximum
585660 }
586661
587662 // If the amount we want to loop out is less than the minimum we can't
588663 // proceed with a swap, so we return early.
589- if amount < restrictions .Minimum {
664+ if satAmount < restrictions .Minimum {
590665 log .Debugf ("easy autoloop: swap amount is below minimum swap " +
591666 "size, minimum=%v, need to swap %v" ,
592- restrictions .Minimum , amount )
667+ restrictions .Minimum , satAmount )
593668 return nil
594669 }
595670
596671 log .Debugf ("easy autoloop: local_total=%v, target=%v, " +
597672 "attempting to loop out %v" , localTotal ,
598- m . params . EasyAutoloopTarget , amount )
673+ localTarget , amount )
599674
600675 // Start building that swap.
601676 builder := newLoopOutBuilder (m .cfg )
@@ -611,7 +686,7 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
611686 channel .ChannelID , channel .LocalBalance )
612687
613688 swapAmt , err := btcutil .NewAmount (
614- math .Min (channel .LocalBalance .ToBTC (), amount .ToBTC ()),
689+ math .Min (channel .LocalBalance .ToBTC (), satAmount .ToBTC ()),
615690 )
616691 if err != nil {
617692 return err
@@ -639,9 +714,17 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
639714 lnwire .NewShortChanIDFromInt (channel .ChannelID ),
640715 }
641716
717+ var assetSwap * assetSwapInfo
718+ if assetId != "" {
719+ assetSwap = & assetSwapInfo {
720+ assetId : assetId ,
721+ peerPubkey : channel .PubKeyBytes [:],
722+ }
723+ }
724+
642725 suggestion , err := builder .buildSwap (
643726 ctx , channel .PubKeyBytes , outgoing , swapAmt , easyParams ,
644- nil ,
727+ assetSwap ,
645728 )
646729 if err != nil {
647730 return err
@@ -1441,10 +1524,6 @@ func (m *Manager) pickEasyAutoloopChannel(channels []lndclient.ChannelInfo,
14411524 // Check each channel, since channels are already sorted we return the
14421525 // first channel that passes all checks.
14431526 for _ , channel := range channels {
1444- if channelIsCustom (channel ) {
1445- continue
1446- }
1447-
14481527 shortChanID := lnwire .NewShortChanIDFromInt (channel .ChannelID )
14491528
14501529 if ! channel .Active {
@@ -1467,7 +1546,12 @@ func (m *Manager) pickEasyAutoloopChannel(channels []lndclient.ChannelInfo,
14671546 continue
14681547 }
14691548
1470- if channel .LocalBalance < restrictions .Minimum {
1549+ if channel .LocalBalance < restrictions .Minimum &&
1550+ // If we use a custom channel, the local balance is
1551+ // denominated in the asset's unit, so we don't need to
1552+ // check the minimum.
1553+ ! channelIsCustom (channel ) {
1554+
14711555 log .Debugf ("Channel %v cannot be used for easy " +
14721556 "autoloop: insufficient local balance %v," +
14731557 "minimum is %v, skipping remaining channels" ,
@@ -1578,3 +1662,27 @@ func channelIsCustom(channel lndclient.ChannelInfo) bool {
15781662 // don't want to consider it for swaps.
15791663 return channel .CustomChannelData != nil
15801664}
1665+
1666+ // getCustomAssetData returns the asset data for a custom channel.
1667+ func getCustomAssetData (channel lndclient.ChannelInfo , assetId string ,
1668+ ) * rfqmsg.JsonAssetChanInfo {
1669+
1670+ if channel .CustomChannelData == nil {
1671+ return nil
1672+ }
1673+
1674+ var assetData rfqmsg.JsonAssetChannel
1675+ err := json .Unmarshal (channel .CustomChannelData , & assetData )
1676+ if err != nil {
1677+ log .Errorf ("Error unmarshalling custom channel data: %v" , err )
1678+ return nil
1679+ }
1680+
1681+ for _ , asset := range assetData .Assets {
1682+ if asset .AssetInfo .AssetGenesis .AssetID == assetId {
1683+ return & asset
1684+ }
1685+ }
1686+
1687+ return nil
1688+ }
0 commit comments