Skip to content

Commit 3c79e9d

Browse files
committed
liquidity: add easy asset autoloop out
1 parent e3f049a commit 3c79e9d

File tree

1 file changed

+271
-8
lines changed

1 file changed

+271
-8
lines changed

liquidity/liquidity.go

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

3535
import (
3636
"context"
37+
"crypto/sha256"
38+
"encoding/json"
3739
"errors"
3840
"fmt"
3941
"math"
@@ -48,6 +50,7 @@ import (
4850
"github.com/lightninglabs/loop/loopdb"
4951
clientrpc "github.com/lightninglabs/loop/looprpc"
5052
"github.com/lightninglabs/loop/swap"
53+
"github.com/lightninglabs/taproot-assets/rfqmsg"
5154
"github.com/lightningnetwork/lnd/clock"
5255
"github.com/lightningnetwork/lnd/funding"
5356
"github.com/lightningnetwork/lnd/lntypes"
@@ -218,6 +221,11 @@ type Config struct {
218221
LoopOutTerms func(ctx context.Context,
219222
initiator string) (*loop.LoopOutTerms, error)
220223

224+
// GetAssetPrice returns the price of an asset in satoshis.
225+
GetAssetPrice func(ctx context.Context, assetId string,
226+
peerPubkey []byte, assetAmt uint64,
227+
maxPaymentAmt btcutil.Amount) (btcutil.Amount, error)
228+
221229
// Clock allows easy mocking of time in unit tests.
222230
Clock clock.Clock
223231

@@ -305,6 +313,16 @@ func (m *Manager) Run(ctx context.Context) error {
305313
}
306314
}
307315

316+
// Try to automatically dispach an asset auto-loop.
317+
for assetID := range m.params.AssetAutoloopParams {
318+
err = m.easyAssetAutoloop(ctx, assetID)
319+
if err != nil {
320+
log.Errorf("easy asset autoloop "+
321+
"failed: id: %v, err: %v",
322+
assetID, err)
323+
}
324+
}
325+
308326
case <-ctx.Done():
309327
return ctx.Err()
310328
}
@@ -505,6 +523,32 @@ func (m *Manager) easyAutoLoop(ctx context.Context) error {
505523
return nil
506524
}
507525

526+
// easyAssetAutoloop is the main entry point for the easy auto loop functionality
527+
// for assets. This function will try to dispatch a swap in order to meet the
528+
// easy autoloop requirements for the given asset. For easyAutoloop to work
529+
// there needs to be an EasyAutoloopTarget defined in the parameters. Easy
530+
// autoloop also uses the configured max inflight swaps and budget rules defined
531+
// in the parameters.
532+
func (m *Manager) easyAssetAutoloop(ctx context.Context, assetID string) error {
533+
if !m.params.Autoloop {
534+
return nil
535+
}
536+
537+
assetParams, ok := m.params.AssetAutoloopParams[assetID]
538+
if !ok && !assetParams.EnableEasyOut {
539+
return nil
540+
}
541+
542+
// First check if we should refresh our budget before calculating any
543+
// swaps for autoloop.
544+
m.refreshAutoloopBudget(ctx)
545+
546+
// Dispatch the best easy autoloop swap.
547+
targetAmt := assetParams.LocalTargetAssetAmount
548+
549+
return m.dispatchBestAssetEasyAutoloopSwap(ctx, assetID, targetAmt)
550+
}
551+
508552
// ForceAutoLoop force-ticks our auto-out ticker.
509553
func (m *Manager) ForceAutoLoop(ctx context.Context) error {
510554
select {
@@ -597,7 +641,7 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
597641
builder := newLoopOutBuilder(m.cfg)
598642

599643
channel := m.pickEasyAutoloopChannel(
600-
channels, restrictions, loopOut, loopIn,
644+
usableChannels, restrictions, loopOut, loopIn, 0,
601645
)
602646
if channel == nil {
603647
return fmt.Errorf("no eligible channel for easy autoloop")
@@ -637,7 +681,196 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
637681

638682
suggestion, err := builder.buildSwap(
639683
ctx, channel.PubKeyBytes, outgoing, swapAmt, easyParams,
640-
nil,
684+
)
685+
if err != nil {
686+
return err
687+
}
688+
689+
var swp loop.OutRequest
690+
if t, ok := suggestion.(*loopOutSwapSuggestion); ok {
691+
swp = t.OutRequest
692+
} else {
693+
return fmt.Errorf("unexpected swap suggestion type: %T", t)
694+
}
695+
696+
// Dispatch a sticky loop out.
697+
go m.dispatchStickyLoopOut(
698+
ctx, swp, defaultAmountBackoffRetry, defaultAmountBackoff,
699+
)
700+
701+
return nil
702+
}
703+
704+
// dispatchBestAssetEasyAutoloopSwap tries to dispatch a swap to bring the total
705+
// local balance back to the target for the given asset.
706+
func (m *Manager) dispatchBestAssetEasyAutoloopSwap(ctx context.Context,
707+
assetID string, localTarget uint64) error {
708+
709+
if len(assetID) != sha256.Size*2 {
710+
return fmt.Errorf("invalid asset id: %v", assetID)
711+
}
712+
713+
// Retrieve existing swaps.
714+
loopOut, err := m.cfg.ListLoopOut(ctx)
715+
if err != nil {
716+
return err
717+
}
718+
719+
loopIn, err := m.cfg.ListLoopIn(ctx)
720+
if err != nil {
721+
return err
722+
}
723+
724+
// Get a summary of our existing swaps so that we can check our autoloop
725+
// budget.
726+
summary := m.checkExistingAutoLoops(ctx, loopOut, loopIn)
727+
728+
err = m.checkSummaryBudget(summary)
729+
if err != nil {
730+
return err
731+
}
732+
733+
_, err = m.checkSummaryInflight(summary)
734+
if err != nil {
735+
return err
736+
}
737+
738+
// Get all channels in order to calculate current total local balance.
739+
channels, err := m.cfg.Lnd.Client.ListChannels(ctx, false, false)
740+
if err != nil {
741+
return err
742+
}
743+
744+
// If we are running a custom asset, we'll need to get a random asset
745+
// peer pubkey in order to rfq the asset price.
746+
var assetPeerPubkey []byte
747+
748+
usableChannels := []lndclient.ChannelInfo{}
749+
localTotal := uint64(0)
750+
for _, channel := range channels {
751+
// We are only interested in custom asset channels.
752+
if !channelIsCustom(channel) {
753+
continue
754+
}
755+
756+
assetData := getCustomAssetData(channel, assetID)
757+
if assetData == nil {
758+
continue
759+
}
760+
761+
// We'll overwrite the channel local balance to be
762+
// the custom asset balance. This allows us to make
763+
// use of existing logic.
764+
channel.LocalBalance = btcutil.Amount(assetData.LocalBalance)
765+
usableChannels = append(usableChannels, channel)
766+
767+
// We'll use a random peer pubkey in order to get a rfq for the asset
768+
// to get a rough amount of sats to swap amount.
769+
assetPeerPubkey = channel.PubKeyBytes[:]
770+
771+
localTotal += assetData.LocalBalance
772+
}
773+
774+
// Since we're only autolooping-out we need to check if we are below
775+
// the target, meaning that we already meet the requirements.
776+
if localTotal <= localTarget {
777+
log.Debugf("Asset: %v... total local balance %v below target %v",
778+
assetID[:8], localTotal, localTarget)
779+
return nil
780+
}
781+
782+
restrictions, err := m.cfg.Restrictions(
783+
ctx, swap.TypeOut, getInitiator(m.params),
784+
)
785+
if err != nil {
786+
return err
787+
}
788+
789+
// Calculate the assetAmount that we want to loop out. If it exceeds the
790+
// max allowed clamp it to max.
791+
assetAmount := localTotal - localTarget
792+
793+
// We need a request sat amount for the asset price request. We'll use
794+
// the average of the min and max restrictions.
795+
assetPriceRequestSatAmt := (restrictions.Minimum + restrictions.Maximum) / 2
796+
797+
// If we run a custom asset, we'll need to convert the asset amount
798+
// we want to swap to the satoshi amount.
799+
satAmount, err := m.cfg.GetAssetPrice(
800+
ctx, assetID, assetPeerPubkey, assetAmount,
801+
assetPriceRequestSatAmt,
802+
)
803+
if err != nil {
804+
return err
805+
}
806+
807+
if satAmount > restrictions.Maximum {
808+
log.Debugf("Asset %v easy autoloop: using maximum allowed "+
809+
"swap amount, maximum=%v, need to swap %v",
810+
assetID[:8], restrictions.Maximum, satAmount)
811+
satAmount = restrictions.Maximum
812+
}
813+
814+
// If the amount we want to loop out is less than the minimum we can't
815+
// proceed with a swap, so we return early.
816+
if satAmount < restrictions.Minimum {
817+
log.Debugf("Asset %v easy autoloop: swap amount is below"+
818+
" minimum swap size, minimum=%v, need to swap %v",
819+
assetID[:8], restrictions.Minimum, satAmount)
820+
return nil
821+
}
822+
823+
satsPerAsset := float64(satAmount) / float64(assetAmount)
824+
825+
log.Debugf("Asset %v easy autoloop: local_total=%v, target=%v, "+
826+
"attempting to loop out %v assets corresponding to %v sats",
827+
assetID[:8], localTotal, localTarget, assetAmount, satAmount)
828+
829+
// Start building that swap.
830+
builder := newLoopOutBuilder(m.cfg)
831+
832+
channel := m.pickEasyAutoloopChannel(
833+
usableChannels, restrictions, loopOut, loopIn, satsPerAsset,
834+
)
835+
if channel == nil {
836+
return fmt.Errorf("no eligible channel for easy autoloop")
837+
}
838+
839+
log.Debugf("Asset %v easy autoloop: picked channel %v with local "+
840+
"balance %v", assetID[:8], channel.ChannelID,
841+
int(channel.LocalBalance))
842+
843+
// If no fee is set, override our current parameters in order to use the
844+
// default percent limit of easy-autoloop.
845+
easyParams := m.params
846+
847+
switch feeLimit := easyParams.FeeLimit.(type) {
848+
case *FeePortion:
849+
if feeLimit.PartsPerMillion == 0 {
850+
easyParams.FeeLimit = &FeePortion{
851+
PartsPerMillion: defaultFeePPM,
852+
}
853+
}
854+
855+
default:
856+
easyParams.FeeLimit = &FeePortion{
857+
PartsPerMillion: defaultFeePPM,
858+
}
859+
}
860+
861+
// Set the swap outgoing channel to the chosen channel.
862+
outgoing := []lnwire.ShortChannelID{
863+
lnwire.NewShortChanIDFromInt(channel.ChannelID),
864+
}
865+
866+
assetSwap := &assetSwapInfo{
867+
assetID: assetID,
868+
peerPubkey: channel.PubKeyBytes[:],
869+
}
870+
871+
suggestion, err := builder.buildSwap(
872+
ctx, channel.PubKeyBytes, outgoing, satAmount, easyParams,
873+
withAssetSwapInfo(assetSwap),
641874
)
642875
if err != nil {
643876
return err
@@ -1420,7 +1653,7 @@ func (m *Manager) waitForSwapPayment(ctx context.Context, swapHash lntypes.Hash,
14201653
// swap conflicts.
14211654
func (m *Manager) pickEasyAutoloopChannel(channels []lndclient.ChannelInfo,
14221655
restrictions *Restrictions, loopOut []*loopdb.LoopOut,
1423-
loopIn []*loopdb.LoopIn) *lndclient.ChannelInfo {
1656+
loopIn []*loopdb.LoopIn, satsPerAsset float64) *lndclient.ChannelInfo {
14241657

14251658
traffic := m.currentSwapTraffic(loopOut, loopIn)
14261659

@@ -1434,10 +1667,6 @@ func (m *Manager) pickEasyAutoloopChannel(channels []lndclient.ChannelInfo,
14341667
// Check each channel, since channels are already sorted we return the
14351668
// first channel that passes all checks.
14361669
for _, channel := range channels {
1437-
if channelIsCustom(channel) {
1438-
continue
1439-
}
1440-
14411670
shortChanID := lnwire.NewShortChanIDFromInt(channel.ChannelID)
14421671

14431672
if !channel.Active {
@@ -1460,7 +1689,16 @@ func (m *Manager) pickEasyAutoloopChannel(channels []lndclient.ChannelInfo,
14601689
continue
14611690
}
14621691

1463-
if channel.LocalBalance < restrictions.Minimum {
1692+
localBalance := channel.LocalBalance
1693+
1694+
// If we're running a custom asset, the local balance is
1695+
// denominated in the asset's unit, so we convert it to
1696+
// back to sats to check the minimum.
1697+
if channelIsCustom(channel) {
1698+
localBalance = localBalance.MulF64(satsPerAsset)
1699+
}
1700+
1701+
if localBalance < restrictions.Minimum {
14641702
log.Debugf("Channel %v cannot be used for easy "+
14651703
"autoloop: insufficient local balance %v,"+
14661704
"minimum is %v, skipping remaining channels",
@@ -1571,3 +1809,28 @@ func channelIsCustom(channel lndclient.ChannelInfo) bool {
15711809
// don't want to consider it for swaps.
15721810
return channel.CustomChannelData != nil
15731811
}
1812+
1813+
// getCustomAssetData returns the asset data for a custom channel.
1814+
func getCustomAssetData(channel lndclient.ChannelInfo, assetID string,
1815+
) *rfqmsg.JsonAssetChanInfo {
1816+
1817+
if channel.CustomChannelData == nil {
1818+
return nil
1819+
}
1820+
1821+
var assetData rfqmsg.JsonAssetChannel
1822+
err := json.Unmarshal(channel.CustomChannelData, &assetData)
1823+
if err != nil {
1824+
log.Errorf("Error unmarshalling custom channel %v data: %v",
1825+
channel.ChannelID, err)
1826+
return nil
1827+
}
1828+
1829+
for _, asset := range assetData.Assets {
1830+
if asset.AssetInfo.AssetGenesis.AssetID == assetID {
1831+
return &asset
1832+
}
1833+
}
1834+
1835+
return nil
1836+
}

0 commit comments

Comments
 (0)