Skip to content

Commit 3a1d90a

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

File tree

1 file changed

+272
-6
lines changed

1 file changed

+272
-6
lines changed

liquidity/liquidity.go

Lines changed: 272 additions & 6 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 uint64, minSatAmt btcutil.Amount) (
226+
btcutil.Amount, 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
}
@@ -509,6 +525,42 @@ func (m *Manager) easyAutoLoop(ctx context.Context) error {
509525
return nil
510526
}
511527

528+
// easyAutoLoopAsset is the main entry point for the easy auto loop functionality
529+
// for assets. This function will try to dispatch a swap in order to meet the
530+
// easy autoloop requirements for the given asset. For easyAutoloop to work
531+
// there needs to be an EasyAutoloopTarget defined in the parameters. Easy
532+
// autoloop also uses the configured max inflight swaps and budget rules defined
533+
// in the parameters.
534+
func (m *Manager) easyAutoLoopAsset(ctx context.Context, assetID string) error {
535+
if !m.params.Autoloop {
536+
return nil
537+
}
538+
539+
assetParams, ok := m.params.AssetAutoloopParams[assetID]
540+
if !ok {
541+
return nil
542+
}
543+
544+
if !assetParams.EnableEasyOut {
545+
return nil
546+
}
547+
548+
// First check if we should refresh our budget before calculating any
549+
// swaps for autoloop.
550+
m.refreshAutoloopBudget(ctx)
551+
552+
// Dispatch the best easy autoloop swap.
553+
err := m.dispatchBestAssetEasyAutoloopSwap(
554+
ctx, assetID,
555+
assetParams.EasyAutoloopTargetAmount,
556+
)
557+
if err != nil {
558+
return err
559+
}
560+
561+
return nil
562+
}
563+
512564
// ForceAutoLoop force-ticks our auto-out ticker.
513565
func (m *Manager) ForceAutoLoop(ctx context.Context) error {
514566
select {
@@ -641,7 +693,195 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
641693

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

14491685
if !channel.Active {
@@ -1466,7 +1702,12 @@ func (m *Manager) pickEasyAutoloopChannel(channels []lndclient.ChannelInfo,
14661702
continue
14671703
}
14681704

1469-
if channel.LocalBalance < restrictions.Minimum {
1705+
if channel.LocalBalance < restrictions.Minimum &&
1706+
// If we use a custom channel, the local balance is
1707+
// denominated in the asset's unit, so we don't need to
1708+
// check the minimum.
1709+
!channelIsCustom(channel) {
1710+
14701711
log.Debugf("Channel %v cannot be used for easy "+
14711712
"autoloop: insufficient local balance %v,"+
14721713
"minimum is %v, skipping remaining channels",
@@ -1577,3 +1818,28 @@ func channelIsCustom(channel lndclient.ChannelInfo) bool {
15771818
// don't want to consider it for swaps.
15781819
return channel.CustomChannelData != nil
15791820
}
1821+
1822+
// getCustomAssetData returns the asset data for a custom channel.
1823+
func getCustomAssetData(channel lndclient.ChannelInfo, assetID string,
1824+
) *rfqmsg.JsonAssetChanInfo {
1825+
1826+
if channel.CustomChannelData == nil {
1827+
return nil
1828+
}
1829+
1830+
var assetData rfqmsg.JsonAssetChannel
1831+
err := json.Unmarshal(channel.CustomChannelData, &assetData)
1832+
if err != nil {
1833+
log.Errorf("Error unmarshalling custom channel %v data: %v",
1834+
channel.ChannelID, err)
1835+
return nil
1836+
}
1837+
1838+
for _, asset := range assetData.Assets {
1839+
if asset.AssetInfo.AssetGenesis.AssetID == assetID {
1840+
return &asset
1841+
}
1842+
}
1843+
1844+
return nil
1845+
}

0 commit comments

Comments
 (0)