Skip to content

Commit e601386

Browse files
committed
liquidity: add easy asset autoloop out
1 parent a03cc4f commit e601386

File tree

1 file changed

+148
-22
lines changed

1 file changed

+148
-22
lines changed

liquidity/liquidity.go

Lines changed: 148 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ package liquidity
3434

3535
import (
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

Comments
 (0)