@@ -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 .dispatchBestEasyAutoloopSwap (
556+ ctx , assetID ,
557+ btcutil .Amount (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+ assetID string , localTarget btcutil.Amount ) error {
581+
526582 // Retrieve existing swaps.
527583 loopOut , err := m .cfg .ListLoopOut (ctx )
528584 if err != nil {
@@ -554,19 +610,39 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
554610 return err
555611 }
556612
613+ // If we are running a custom asset, we'll need to get a random asset
614+ // peer pubkey in order to rfq the asset price.
615+ var assetPeerPubkey []byte
616+
557617 localTotal := btcutil .Amount (0 )
558618 for _ , channel := range channels {
559619 if channelIsCustom (channel ) {
560- continue
620+ if assetID == "" {
621+ continue
622+ }
623+ assetData := getCustomAssetData (channel , assetID )
624+
625+ if assetData == nil {
626+ continue
627+ }
628+ // We'll overwrite the channel local balance to be
629+ // the custom asset balance. This allows us to make
630+ // use of existing logic.
631+ channel .LocalBalance = btcutil .Amount (
632+ assetData .LocalBalance ,
633+ )
634+
635+ assetPeerPubkey = channel .PubKeyBytes [:]
561636 }
637+
562638 localTotal += channel .LocalBalance
563639 }
564640
565641 // Since we're only autolooping-out we need to check if we are below
566642 // the target, meaning that we already meet the requirements.
567- if localTotal <= m . params . EasyAutoloopTarget {
643+ if localTotal <= localTarget {
568644 log .Debugf ("total local balance %v below target %v" ,
569- localTotal , m . params . EasyAutoloopTarget )
645+ localTotal , localTarget )
570646 return nil
571647 }
572648
@@ -579,23 +655,37 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
579655
580656 // Calculate the amount that we want to loop out. If it exceeds the max
581657 // allowed clamp it to max.
582- amount := localTotal - m .params .EasyAutoloopTarget
583- if amount > restrictions .Maximum {
584- amount = restrictions .Maximum
658+ amount := localTotal - localTarget
659+ satAmount := amount
660+
661+ // If we run a custom asset, we'll need to convert the asset amount
662+ // we want to swap to the satoshi amount.
663+ if assetID != "" {
664+ satAmount , err = m .cfg .GetAssetPrice (
665+ ctx , assetID , assetPeerPubkey , uint64 (amount ),
666+ uint64 (restrictions .Minimum ),
667+ )
668+ if err != nil {
669+ return err
670+ }
671+ }
672+
673+ if satAmount > restrictions .Maximum {
674+ satAmount = restrictions .Maximum
585675 }
586676
587677 // If the amount we want to loop out is less than the minimum we can't
588678 // proceed with a swap, so we return early.
589- if amount < restrictions .Minimum {
679+ if satAmount < restrictions .Minimum {
590680 log .Debugf ("easy autoloop: swap amount is below minimum swap " +
591681 "size, minimum=%v, need to swap %v" ,
592- restrictions .Minimum , amount )
682+ restrictions .Minimum , satAmount )
593683 return nil
594684 }
595685
596686 log .Debugf ("easy autoloop: local_total=%v, target=%v, " +
597687 "attempting to loop out %v" , localTotal ,
598- m . params . EasyAutoloopTarget , amount )
688+ localTarget , amount )
599689
600690 // Start building that swap.
601691 builder := newLoopOutBuilder (m .cfg )
@@ -610,11 +700,14 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
610700 log .Debugf ("easy autoloop: picked channel %v with local balance %v" ,
611701 channel .ChannelID , channel .LocalBalance )
612702
613- swapAmt , err := btcutil .NewAmount (
614- math .Min (channel .LocalBalance .ToBTC (), amount .ToBTC ()),
615- )
616- if err != nil {
617- return err
703+ swapAmt := satAmount
704+ if assetID == "" {
705+ swapAmt , err = btcutil .NewAmount (
706+ math .Min (channel .LocalBalance .ToBTC (), satAmount .ToBTC ()),
707+ )
708+ if err != nil {
709+ return err
710+ }
618711 }
619712
620713 // If no fee is set, override our current parameters in order to use the
@@ -639,9 +732,17 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
639732 lnwire .NewShortChanIDFromInt (channel .ChannelID ),
640733 }
641734
735+ var assetSwap * assetSwapInfo
736+ if assetID != "" {
737+ assetSwap = & assetSwapInfo {
738+ assetID : assetID ,
739+ peerPubkey : channel .PubKeyBytes [:],
740+ }
741+ }
742+
642743 suggestion , err := builder .buildSwap (
643744 ctx , channel .PubKeyBytes , outgoing , swapAmt , easyParams ,
644- nil ,
745+ assetSwap ,
645746 )
646747 if err != nil {
647748 return err
@@ -1441,10 +1542,6 @@ func (m *Manager) pickEasyAutoloopChannel(channels []lndclient.ChannelInfo,
14411542 // Check each channel, since channels are already sorted we return the
14421543 // first channel that passes all checks.
14431544 for _ , channel := range channels {
1444- if channelIsCustom (channel ) {
1445- continue
1446- }
1447-
14481545 shortChanID := lnwire .NewShortChanIDFromInt (channel .ChannelID )
14491546
14501547 if ! channel .Active {
@@ -1467,7 +1564,12 @@ func (m *Manager) pickEasyAutoloopChannel(channels []lndclient.ChannelInfo,
14671564 continue
14681565 }
14691566
1470- if channel .LocalBalance < restrictions .Minimum {
1567+ if channel .LocalBalance < restrictions .Minimum &&
1568+ // If we use a custom channel, the local balance is
1569+ // denominated in the asset's unit, so we don't need to
1570+ // check the minimum.
1571+ ! channelIsCustom (channel ) {
1572+
14711573 log .Debugf ("Channel %v cannot be used for easy " +
14721574 "autoloop: insufficient local balance %v," +
14731575 "minimum is %v, skipping remaining channels" ,
@@ -1578,3 +1680,27 @@ func channelIsCustom(channel lndclient.ChannelInfo) bool {
15781680 // don't want to consider it for swaps.
15791681 return channel .CustomChannelData != nil
15801682}
1683+
1684+ // getCustomAssetData returns the asset data for a custom channel.
1685+ func getCustomAssetData (channel lndclient.ChannelInfo , assetID string ,
1686+ ) * rfqmsg.JsonAssetChanInfo {
1687+
1688+ if channel .CustomChannelData == nil {
1689+ return nil
1690+ }
1691+
1692+ var assetData rfqmsg.JsonAssetChannel
1693+ err := json .Unmarshal (channel .CustomChannelData , & assetData )
1694+ if err != nil {
1695+ log .Errorf ("Error unmarshalling custom channel data: %v" , err )
1696+ return nil
1697+ }
1698+
1699+ for _ , asset := range assetData .Assets {
1700+ if asset .AssetInfo .AssetGenesis .AssetID == assetID {
1701+ return & asset
1702+ }
1703+ }
1704+
1705+ return nil
1706+ }
0 commit comments