Skip to content

Commit d2fe455

Browse files
committed
liquidity: add easy asset autoloop out
1 parent fa524bf commit d2fe455

File tree

1 file changed

+278
-17
lines changed

1 file changed

+278
-17
lines changed

liquidity/liquidity.go

Lines changed: 278 additions & 17 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.dispatchBestAssetEasyAutoloopSwap(
556+
ctx, assetID,
557+
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+
localTarget btcutil.Amount) error {
581+
526582
// Retrieve existing swaps.
527583
loopOut, err := m.cfg.ListLoopOut(ctx)
528584
if err != nil {
@@ -559,14 +615,15 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
559615
if channelIsCustom(channel) {
560616
continue
561617
}
618+
562619
localTotal += channel.LocalBalance
563620
}
564621

565622
// Since we're only autolooping-out we need to check if we are below
566623
// the target, meaning that we already meet the requirements.
567-
if localTotal <= m.params.EasyAutoloopTarget {
624+
if localTotal <= localTarget {
568625
log.Debugf("total local balance %v below target %v",
569-
localTotal, m.params.EasyAutoloopTarget)
626+
localTotal, localTarget)
570627
return nil
571628
}
572629

@@ -579,10 +636,7 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
579636

580637
// Calculate the amount that we want to loop out. If it exceeds the max
581638
// allowed clamp it to max.
582-
amount := localTotal - m.params.EasyAutoloopTarget
583-
if amount > restrictions.Maximum {
584-
amount = restrictions.Maximum
585-
}
639+
amount := localTotal - localTarget
586640

587641
// If the amount we want to loop out is less than the minimum we can't
588642
// proceed with a swap, so we return early.
@@ -595,7 +649,7 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
595649

596650
log.Debugf("easy autoloop: local_total=%v, target=%v, "+
597651
"attempting to loop out %v", localTotal,
598-
m.params.EasyAutoloopTarget, amount)
652+
localTarget, amount)
599653

600654
// Start building that swap.
601655
builder := newLoopOutBuilder(m.cfg)
@@ -610,8 +664,184 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
610664
log.Debugf("easy autoloop: picked channel %v with local balance %v",
611665
channel.ChannelID, channel.LocalBalance)
612666

613-
swapAmt, err := btcutil.NewAmount(
614-
math.Min(channel.LocalBalance.ToBTC(), amount.ToBTC()),
667+
// If no fee is set, override our current parameters in order to use the
668+
// default percent limit of easy-autoloop.
669+
easyParams := m.params
670+
671+
switch feeLimit := easyParams.FeeLimit.(type) {
672+
case *FeePortion:
673+
if feeLimit.PartsPerMillion == 0 {
674+
easyParams.FeeLimit = &FeePortion{
675+
PartsPerMillion: defaultFeePPM,
676+
}
677+
}
678+
default:
679+
easyParams.FeeLimit = &FeePortion{
680+
PartsPerMillion: defaultFeePPM,
681+
}
682+
}
683+
684+
// Set the swap outgoing channel to the chosen channel.
685+
outgoing := []lnwire.ShortChannelID{
686+
lnwire.NewShortChanIDFromInt(channel.ChannelID),
687+
}
688+
689+
suggestion, err := builder.buildSwap(
690+
ctx, channel.PubKeyBytes, outgoing, amount, easyParams,
691+
)
692+
if err != nil {
693+
return err
694+
}
695+
696+
var swp loop.OutRequest
697+
if t, ok := suggestion.(*loopOutSwapSuggestion); ok {
698+
swp = t.OutRequest
699+
} else {
700+
return fmt.Errorf("unexpected swap suggestion type: %T", t)
701+
}
702+
703+
// Dispatch a sticky loop out.
704+
go m.dispatchStickyLoopOut(
705+
ctx, swp, defaultAmountBackoffRetry, defaultAmountBackoff,
706+
)
707+
708+
return nil
709+
}
710+
711+
// dispatchBestAssetEasyAutoloopSwap tries to dispatch a swap to bring the total
712+
// local balance back to the target for the given asset.
713+
func (m *Manager) dispatchBestAssetEasyAutoloopSwap(ctx context.Context,
714+
assetID string, localTarget uint64) error {
715+
if assetID == "" && len(assetID) != 32 {
716+
return fmt.Errorf("invalid asset id: %v", assetID)
717+
}
718+
719+
// Retrieve existing swaps.
720+
loopOut, err := m.cfg.ListLoopOut(ctx)
721+
if err != nil {
722+
return err
723+
}
724+
725+
loopIn, err := m.cfg.ListLoopIn(ctx)
726+
if err != nil {
727+
return err
728+
}
729+
730+
// Get a summary of our existing swaps so that we can check our autoloop
731+
// budget.
732+
summary := m.checkExistingAutoLoops(ctx, loopOut, loopIn)
733+
734+
err = m.checkSummaryBudget(summary)
735+
if err != nil {
736+
return err
737+
}
738+
739+
_, err = m.checkSummaryInflight(summary)
740+
if err != nil {
741+
return err
742+
}
743+
744+
// Get all channels in order to calculate current total local balance.
745+
channels, err := m.cfg.Lnd.Client.ListChannels(ctx, false, false)
746+
if err != nil {
747+
return err
748+
}
749+
750+
// If we are running a custom asset, we'll need to get a random asset
751+
// peer pubkey in order to rfq the asset price.
752+
var assetPeerPubkey []byte
753+
754+
localTotal := uint64(0)
755+
for _, channel := range channels {
756+
// We'll only interested in custom asset channels.
757+
if !channelIsCustom(channel) {
758+
continue
759+
}
760+
761+
assetData := getCustomAssetData(channel, assetID)
762+
if assetData == nil {
763+
continue
764+
}
765+
766+
// We'll overwrite the channel local balance to be
767+
// the custom asset balance. This allows us to make
768+
// use of existing logic.
769+
channel.LocalBalance = btcutil.Amount(
770+
assetData.LocalBalance,
771+
)
772+
773+
assetPeerPubkey = channel.PubKeyBytes[:]
774+
775+
localTotal += assetData.LocalBalance
776+
777+
// We'll overwrite the channel local balance in order to
778+
// reuse channel selection logic.
779+
channel.LocalBalance = btcutil.Amount(localTotal)
780+
}
781+
782+
// Since we're only autolooping-out we need to check if we are below
783+
// the target, meaning that we already meet the requirements.
784+
if localTotal <= localTarget {
785+
log.Debugf("asset: %v... total local balance %v below target %v",
786+
assetID[:8], localTotal, localTarget)
787+
return nil
788+
}
789+
790+
restrictions, err := m.cfg.Restrictions(
791+
ctx, swap.TypeOut, getInitiator(m.params),
792+
)
793+
if err != nil {
794+
return err
795+
}
796+
797+
// Calculate the assetAmount that we want to loop out. If it exceeds the max
798+
// allowed clamp it to max.
799+
assetAmount := localTotal - localTarget
800+
801+
// If we run a custom asset, we'll need to convert the asset amount
802+
// we want to swap to the satoshi amount.
803+
satAmount, err := m.cfg.GetAssetPrice(
804+
ctx, assetID, assetPeerPubkey, uint64(assetAmount),
805+
uint64(restrictions.Minimum),
806+
)
807+
if err != nil {
808+
return err
809+
}
810+
811+
if satAmount > restrictions.Maximum {
812+
satAmount = restrictions.Maximum
813+
}
814+
815+
// If the amount we want to loop out is less than the minimum we can't
816+
// proceed with a swap, so we return early.
817+
if satAmount < restrictions.Minimum {
818+
log.Debugf("asset %v easy autoloop: swap amount is below"+
819+
" minimum swap size, minimum=%v, need to swap %v",
820+
assetID[:8], restrictions.Minimum, satAmount)
821+
return nil
822+
}
823+
824+
log.Debugf("asset %v easy autoloop: local_total=%v, target=%v, "+
825+
"attempting to loop out %v", assetID[:8], localTotal,
826+
localTarget, assetAmount)
827+
828+
// Start building that swap.
829+
builder := newLoopOutBuilder(m.cfg)
830+
831+
channel := m.pickEasyAutoloopChannel(
832+
channels, restrictions, loopOut, loopIn,
833+
)
834+
if channel == nil {
835+
return fmt.Errorf("no eligible channel for easy autoloop")
836+
}
837+
838+
log.Debugf("asset %v easy autoloop: picked channel %v with local"+
839+
" balance %v", assetID[:8], channel.ChannelID,
840+
channel.LocalBalance)
841+
842+
swapAmt := satAmount
843+
swapAmt, err = btcutil.NewAmount(
844+
math.Min(channel.LocalBalance.ToBTC(), satAmount.ToBTC()),
615845
)
616846
if err != nil {
617847
return err
@@ -639,9 +869,14 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
639869
lnwire.NewShortChanIDFromInt(channel.ChannelID),
640870
}
641871

872+
assetSwap := &assetSwapInfo{
873+
assetID: assetID,
874+
peerPubkey: channel.PubKeyBytes[:],
875+
}
876+
642877
suggestion, err := builder.buildSwap(
643878
ctx, channel.PubKeyBytes, outgoing, swapAmt, easyParams,
644-
nil,
879+
withAssetSwapInfo(assetSwap),
645880
)
646881
if err != nil {
647882
return err
@@ -1440,10 +1675,6 @@ func (m *Manager) pickEasyAutoloopChannel(channels []lndclient.ChannelInfo,
14401675
// Check each channel, since channels are already sorted we return the
14411676
// first channel that passes all checks.
14421677
for _, channel := range channels {
1443-
if channelIsCustom(channel) {
1444-
continue
1445-
}
1446-
14471678
shortChanID := lnwire.NewShortChanIDFromInt(channel.ChannelID)
14481679

14491680
if !channel.Active {
@@ -1466,7 +1697,12 @@ func (m *Manager) pickEasyAutoloopChannel(channels []lndclient.ChannelInfo,
14661697
continue
14671698
}
14681699

1469-
if channel.LocalBalance < restrictions.Minimum {
1700+
if channel.LocalBalance < restrictions.Minimum &&
1701+
// If we use a custom channel, the local balance is
1702+
// denominated in the asset's unit, so we don't need to
1703+
// check the minimum.
1704+
!channelIsCustom(channel) {
1705+
14701706
log.Debugf("Channel %v cannot be used for easy "+
14711707
"autoloop: insufficient local balance %v,"+
14721708
"minimum is %v, skipping remaining channels",
@@ -1577,3 +1813,28 @@ func channelIsCustom(channel lndclient.ChannelInfo) bool {
15771813
// don't want to consider it for swaps.
15781814
return channel.CustomChannelData != nil
15791815
}
1816+
1817+
// getCustomAssetData returns the asset data for a custom channel.
1818+
func getCustomAssetData(channel lndclient.ChannelInfo, assetID string,
1819+
) *rfqmsg.JsonAssetChanInfo {
1820+
1821+
if channel.CustomChannelData == nil {
1822+
return nil
1823+
}
1824+
1825+
var assetData rfqmsg.JsonAssetChannel
1826+
err := json.Unmarshal(channel.CustomChannelData, &assetData)
1827+
if err != nil {
1828+
log.Errorf("Error unmarshalling custom channel %v data: %v",
1829+
channel.ChannelID, err)
1830+
return nil
1831+
}
1832+
1833+
for _, asset := range assetData.Assets {
1834+
if asset.AssetInfo.AssetGenesis.AssetID == assetID {
1835+
return &asset
1836+
}
1837+
}
1838+
1839+
return nil
1840+
}

0 commit comments

Comments
 (0)