@@ -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,41 @@ 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+ assetParams , ok := m .params .AssetAutoloopParams [assetID ]
538+ if ! ok {
539+ return nil
540+ }
541+
542+ if ! assetParams .EnableEasyOut {
543+ return nil
544+ }
545+
546+ // First check if we should refresh our budget before calculating any
547+ // swaps for autoloop.
548+ m .refreshAutoloopBudget (ctx )
549+
550+ // Dispatch the best easy autoloop swap.
551+ err := m .dispatchBestEasyAutoloopSwap (
552+ ctx , assetID ,
553+ btcutil .Amount (assetParams .EasyAutoloopTargetAmount ),
554+ )
505555 if err != nil {
506556 return err
507557 }
@@ -522,7 +572,9 @@ func (m *Manager) ForceAutoLoop(ctx context.Context) error {
522572
523573// dispatchBestEasyAutoloopSwap tries to dispatch a swap to bring the total
524574// local balance back to the target.
525- func (m * Manager ) dispatchBestEasyAutoloopSwap (ctx context.Context ) error {
575+ func (m * Manager ) dispatchBestEasyAutoloopSwap (ctx context.Context ,
576+ assetID string , localTarget btcutil.Amount ) error {
577+
526578 // Retrieve existing swaps.
527579 loopOut , err := m .cfg .ListLoopOut (ctx )
528580 if err != nil {
@@ -554,19 +606,39 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
554606 return err
555607 }
556608
609+ // If we are running a custom asset, we'll need to get a random asset
610+ // peer pubkey in order to rfq the asset price.
611+ var assetPeerPubkey []byte
612+
557613 localTotal := btcutil .Amount (0 )
558614 for _ , channel := range channels {
559615 if channelIsCustom (channel ) {
560- continue
616+ if assetID == "" {
617+ continue
618+ }
619+ assetData := getCustomAssetData (channel , assetID )
620+
621+ if assetData == nil {
622+ continue
623+ }
624+ // We'll overwrite the channel local balance to be
625+ // the custom asset balance. This allows us to make
626+ // use of existing logic.
627+ channel .LocalBalance = btcutil .Amount (
628+ assetData .LocalBalance ,
629+ )
630+
631+ assetPeerPubkey = channel .PubKeyBytes [:]
561632 }
633+
562634 localTotal += channel .LocalBalance
563635 }
564636
565637 // Since we're only autolooping-out we need to check if we are below
566638 // the target, meaning that we already meet the requirements.
567- if localTotal <= m . params . EasyAutoloopTarget {
639+ if localTotal <= localTarget {
568640 log .Debugf ("total local balance %v below target %v" ,
569- localTotal , m . params . EasyAutoloopTarget )
641+ localTotal , localTarget )
570642 return nil
571643 }
572644
@@ -579,23 +651,37 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
579651
580652 // Calculate the amount that we want to loop out. If it exceeds the max
581653 // allowed clamp it to max.
582- amount := localTotal - m .params .EasyAutoloopTarget
583- if amount > restrictions .Maximum {
584- amount = restrictions .Maximum
654+ amount := localTotal - localTarget
655+ satAmount := amount
656+
657+ // If we run a custom asset, we'll need to convert the asset amount
658+ // we want to swap to the satoshi amount.
659+ if assetID != "" {
660+ satAmount , err = m .cfg .GetAssetPrice (
661+ ctx , assetID , assetPeerPubkey , uint64 (amount ),
662+ uint64 (restrictions .Minimum ),
663+ )
664+ if err != nil {
665+ return err
666+ }
667+ }
668+
669+ if satAmount > restrictions .Maximum {
670+ satAmount = restrictions .Maximum
585671 }
586672
587673 // If the amount we want to loop out is less than the minimum we can't
588674 // proceed with a swap, so we return early.
589- if amount < restrictions .Minimum {
675+ if satAmount < restrictions .Minimum {
590676 log .Debugf ("easy autoloop: swap amount is below minimum swap " +
591677 "size, minimum=%v, need to swap %v" ,
592- restrictions .Minimum , amount )
678+ restrictions .Minimum , satAmount )
593679 return nil
594680 }
595681
596682 log .Debugf ("easy autoloop: local_total=%v, target=%v, " +
597683 "attempting to loop out %v" , localTotal ,
598- m . params . EasyAutoloopTarget , amount )
684+ localTarget , amount )
599685
600686 // Start building that swap.
601687 builder := newLoopOutBuilder (m .cfg )
@@ -611,7 +697,7 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
611697 channel .ChannelID , channel .LocalBalance )
612698
613699 swapAmt , err := btcutil .NewAmount (
614- math .Min (channel .LocalBalance .ToBTC (), amount .ToBTC ()),
700+ math .Min (channel .LocalBalance .ToBTC (), satAmount .ToBTC ()),
615701 )
616702 if err != nil {
617703 return err
@@ -639,9 +725,17 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
639725 lnwire .NewShortChanIDFromInt (channel .ChannelID ),
640726 }
641727
728+ var assetSwap * assetSwapInfo
729+ if assetID != "" {
730+ assetSwap = & assetSwapInfo {
731+ assetID : assetID ,
732+ peerPubkey : channel .PubKeyBytes [:],
733+ }
734+ }
735+
642736 suggestion , err := builder .buildSwap (
643737 ctx , channel .PubKeyBytes , outgoing , swapAmt , easyParams ,
644- nil ,
738+ assetSwap ,
645739 )
646740 if err != nil {
647741 return err
@@ -1441,10 +1535,6 @@ func (m *Manager) pickEasyAutoloopChannel(channels []lndclient.ChannelInfo,
14411535 // Check each channel, since channels are already sorted we return the
14421536 // first channel that passes all checks.
14431537 for _ , channel := range channels {
1444- if channelIsCustom (channel ) {
1445- continue
1446- }
1447-
14481538 shortChanID := lnwire .NewShortChanIDFromInt (channel .ChannelID )
14491539
14501540 if ! channel .Active {
@@ -1467,7 +1557,12 @@ func (m *Manager) pickEasyAutoloopChannel(channels []lndclient.ChannelInfo,
14671557 continue
14681558 }
14691559
1470- if channel .LocalBalance < restrictions .Minimum {
1560+ if channel .LocalBalance < restrictions .Minimum &&
1561+ // If we use a custom channel, the local balance is
1562+ // denominated in the asset's unit, so we don't need to
1563+ // check the minimum.
1564+ ! channelIsCustom (channel ) {
1565+
14711566 log .Debugf ("Channel %v cannot be used for easy " +
14721567 "autoloop: insufficient local balance %v," +
14731568 "minimum is %v, skipping remaining channels" ,
@@ -1578,3 +1673,27 @@ func channelIsCustom(channel lndclient.ChannelInfo) bool {
15781673 // don't want to consider it for swaps.
15791674 return channel .CustomChannelData != nil
15801675}
1676+
1677+ // getCustomAssetData returns the asset data for a custom channel.
1678+ func getCustomAssetData (channel lndclient.ChannelInfo , assetID string ,
1679+ ) * rfqmsg.JsonAssetChanInfo {
1680+
1681+ if channel .CustomChannelData == nil {
1682+ return nil
1683+ }
1684+
1685+ var assetData rfqmsg.JsonAssetChannel
1686+ err := json .Unmarshal (channel .CustomChannelData , & assetData )
1687+ if err != nil {
1688+ log .Errorf ("Error unmarshalling custom channel data: %v" , err )
1689+ return nil
1690+ }
1691+
1692+ for _ , asset := range assetData .Assets {
1693+ if asset .AssetInfo .AssetGenesis .AssetID == assetID {
1694+ return & asset
1695+ }
1696+ }
1697+
1698+ return nil
1699+ }
0 commit comments