Skip to content

Commit 58e4638

Browse files
committed
liquidity: add easy asset autoloop out
1 parent 503afed commit 58e4638

File tree

1 file changed

+137
-18
lines changed

1 file changed

+137
-18
lines changed

liquidity/liquidity.go

Lines changed: 137 additions & 18 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,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

Comments
 (0)