Skip to content

Commit 4da89ec

Browse files
committed
liquidity: add easy asset autoloop out
1 parent e316862 commit 4da89ec

File tree

1 file changed

+265
-6
lines changed

1 file changed

+265
-6
lines changed

liquidity/liquidity.go

Lines changed: 265 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.easyAssetAutoloop(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,40 @@ func (m *Manager) easyAutoLoop(ctx context.Context) error {
509525
return nil
510526
}
511527

528+
// easyAssetAutoloop 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) easyAssetAutoloop(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+
targetAmt := assetParams.LocalTargetAssetAmount
554+
err := m.dispatchBestAssetEasyAutoloopSwap(ctx, assetID, targetAmt)
555+
if err != nil {
556+
return err
557+
}
558+
559+
return nil
560+
}
561+
512562
// ForceAutoLoop force-ticks our auto-out ticker.
513563
func (m *Manager) ForceAutoLoop(ctx context.Context) error {
514564
select {
@@ -641,7 +691,190 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
641691

642692
suggestion, err := builder.buildSwap(
643693
ctx, channel.PubKeyBytes, outgoing, swapAmt, easyParams,
644-
nil,
694+
)
695+
if err != nil {
696+
return err
697+
}
698+
699+
var swp loop.OutRequest
700+
if t, ok := suggestion.(*loopOutSwapSuggestion); ok {
701+
swp = t.OutRequest
702+
} else {
703+
return fmt.Errorf("unexpected swap suggestion type: %T", t)
704+
}
705+
706+
// Dispatch a sticky loop out.
707+
go m.dispatchStickyLoopOut(
708+
ctx, swp, defaultAmountBackoffRetry, defaultAmountBackoff,
709+
)
710+
711+
return nil
712+
}
713+
714+
// dispatchBestAssetEasyAutoloopSwap tries to dispatch a swap to bring the total
715+
// local balance back to the target for the given asset.
716+
func (m *Manager) dispatchBestAssetEasyAutoloopSwap(ctx context.Context,
717+
assetID string, localTarget uint64) error {
718+
719+
if len(assetID) != 32 {
720+
return fmt.Errorf("invalid asset id: %v", assetID)
721+
}
722+
723+
// Retrieve existing swaps.
724+
loopOut, err := m.cfg.ListLoopOut(ctx)
725+
if err != nil {
726+
return err
727+
}
728+
729+
loopIn, err := m.cfg.ListLoopIn(ctx)
730+
if err != nil {
731+
return err
732+
}
733+
734+
// Get a summary of our existing swaps so that we can check our autoloop
735+
// budget.
736+
summary := m.checkExistingAutoLoops(ctx, loopOut, loopIn)
737+
738+
err = m.checkSummaryBudget(summary)
739+
if err != nil {
740+
return err
741+
}
742+
743+
_, err = m.checkSummaryInflight(summary)
744+
if err != nil {
745+
return err
746+
}
747+
748+
// Get all channels in order to calculate current total local balance.
749+
channels, err := m.cfg.Lnd.Client.ListChannels(ctx, false, false)
750+
if err != nil {
751+
return err
752+
}
753+
754+
// If we are running a custom asset, we'll need to get a random asset
755+
// peer pubkey in order to rfq the asset price.
756+
var assetPeerPubkey []byte
757+
758+
localTotal := uint64(0)
759+
for _, channel := range channels {
760+
// We are only interested in custom asset channels.
761+
if !channelIsCustom(channel) {
762+
continue
763+
}
764+
765+
assetData := getCustomAssetData(channel, assetID)
766+
if assetData == nil {
767+
continue
768+
}
769+
770+
// We'll overwrite the channel local balance to be
771+
// the custom asset balance. This allows us to make
772+
// use of existing logic.
773+
channel.LocalBalance = btcutil.Amount(
774+
assetData.LocalBalance,
775+
)
776+
777+
// We'll use a random peer pubkey in order to get a rfq for the asset
778+
// to get a rough amount of sats to swap amount.
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(assetData.LocalBalance)
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
804+
// max 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+
int(channel.LocalBalance))
847+
848+
// If no fee is set, override our current parameters in order to use the
849+
// default percent limit of easy-autoloop.
850+
easyParams := m.params
851+
852+
switch feeLimit := easyParams.FeeLimit.(type) {
853+
case *FeePortion:
854+
if feeLimit.PartsPerMillion == 0 {
855+
easyParams.FeeLimit = &FeePortion{
856+
PartsPerMillion: defaultFeePPM,
857+
}
858+
}
859+
default:
860+
easyParams.FeeLimit = &FeePortion{
861+
PartsPerMillion: defaultFeePPM,
862+
}
863+
}
864+
865+
// Set the swap outgoing channel to the chosen channel.
866+
outgoing := []lnwire.ShortChannelID{
867+
lnwire.NewShortChanIDFromInt(channel.ChannelID),
868+
}
869+
870+
assetSwap := &assetSwapInfo{
871+
assetID: assetID,
872+
peerPubkey: channel.PubKeyBytes[:],
873+
}
874+
875+
suggestion, err := builder.buildSwap(
876+
ctx, channel.PubKeyBytes, outgoing, satAmount, easyParams,
877+
withAssetSwapInfo(assetSwap),
645878
)
646879
if err != nil {
647880
return err
@@ -1440,10 +1673,6 @@ func (m *Manager) pickEasyAutoloopChannel(channels []lndclient.ChannelInfo,
14401673
// Check each channel, since channels are already sorted we return the
14411674
// first channel that passes all checks.
14421675
for _, channel := range channels {
1443-
if channelIsCustom(channel) {
1444-
continue
1445-
}
1446-
14471676
shortChanID := lnwire.NewShortChanIDFromInt(channel.ChannelID)
14481677

14491678
if !channel.Active {
@@ -1466,7 +1695,12 @@ func (m *Manager) pickEasyAutoloopChannel(channels []lndclient.ChannelInfo,
14661695
continue
14671696
}
14681697

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

0 commit comments

Comments
 (0)