mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
liquidity: add easy asset autoloop out
This commit is contained in:
parent
e3f049ae6f
commit
3c79e9d028
1 changed files with 271 additions and 8 deletions
|
|
@ -34,6 +34,8 @@ package liquidity
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
|
|
@ -48,6 +50,7 @@ import (
|
|||
"github.com/lightninglabs/loop/loopdb"
|
||||
clientrpc "github.com/lightninglabs/loop/looprpc"
|
||||
"github.com/lightninglabs/loop/swap"
|
||||
"github.com/lightninglabs/taproot-assets/rfqmsg"
|
||||
"github.com/lightningnetwork/lnd/clock"
|
||||
"github.com/lightningnetwork/lnd/funding"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
|
|
@ -218,6 +221,11 @@ type Config struct {
|
|||
LoopOutTerms func(ctx context.Context,
|
||||
initiator string) (*loop.LoopOutTerms, error)
|
||||
|
||||
// GetAssetPrice returns the price of an asset in satoshis.
|
||||
GetAssetPrice func(ctx context.Context, assetId string,
|
||||
peerPubkey []byte, assetAmt uint64,
|
||||
maxPaymentAmt btcutil.Amount) (btcutil.Amount, error)
|
||||
|
||||
// Clock allows easy mocking of time in unit tests.
|
||||
Clock clock.Clock
|
||||
|
||||
|
|
@ -305,6 +313,16 @@ func (m *Manager) Run(ctx context.Context) error {
|
|||
}
|
||||
}
|
||||
|
||||
// Try to automatically dispach an asset auto-loop.
|
||||
for assetID := range m.params.AssetAutoloopParams {
|
||||
err = m.easyAssetAutoloop(ctx, assetID)
|
||||
if err != nil {
|
||||
log.Errorf("easy asset autoloop "+
|
||||
"failed: id: %v, err: %v",
|
||||
assetID, err)
|
||||
}
|
||||
}
|
||||
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
|
|
@ -505,6 +523,32 @@ func (m *Manager) easyAutoLoop(ctx context.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// easyAssetAutoloop is the main entry point for the easy auto loop functionality
|
||||
// for assets. This function will try to dispatch a swap in order to meet the
|
||||
// easy autoloop requirements for the given asset. For easyAutoloop to work
|
||||
// there needs to be an EasyAutoloopTarget defined in the parameters. Easy
|
||||
// autoloop also uses the configured max inflight swaps and budget rules defined
|
||||
// in the parameters.
|
||||
func (m *Manager) easyAssetAutoloop(ctx context.Context, assetID string) error {
|
||||
if !m.params.Autoloop {
|
||||
return nil
|
||||
}
|
||||
|
||||
assetParams, ok := m.params.AssetAutoloopParams[assetID]
|
||||
if !ok && !assetParams.EnableEasyOut {
|
||||
return nil
|
||||
}
|
||||
|
||||
// First check if we should refresh our budget before calculating any
|
||||
// swaps for autoloop.
|
||||
m.refreshAutoloopBudget(ctx)
|
||||
|
||||
// Dispatch the best easy autoloop swap.
|
||||
targetAmt := assetParams.LocalTargetAssetAmount
|
||||
|
||||
return m.dispatchBestAssetEasyAutoloopSwap(ctx, assetID, targetAmt)
|
||||
}
|
||||
|
||||
// ForceAutoLoop force-ticks our auto-out ticker.
|
||||
func (m *Manager) ForceAutoLoop(ctx context.Context) error {
|
||||
select {
|
||||
|
|
@ -597,7 +641,7 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
|
|||
builder := newLoopOutBuilder(m.cfg)
|
||||
|
||||
channel := m.pickEasyAutoloopChannel(
|
||||
channels, restrictions, loopOut, loopIn,
|
||||
usableChannels, restrictions, loopOut, loopIn, 0,
|
||||
)
|
||||
if channel == nil {
|
||||
return fmt.Errorf("no eligible channel for easy autoloop")
|
||||
|
|
@ -637,7 +681,196 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
|
|||
|
||||
suggestion, err := builder.buildSwap(
|
||||
ctx, channel.PubKeyBytes, outgoing, swapAmt, easyParams,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var swp loop.OutRequest
|
||||
if t, ok := suggestion.(*loopOutSwapSuggestion); ok {
|
||||
swp = t.OutRequest
|
||||
} else {
|
||||
return fmt.Errorf("unexpected swap suggestion type: %T", t)
|
||||
}
|
||||
|
||||
// Dispatch a sticky loop out.
|
||||
go m.dispatchStickyLoopOut(
|
||||
ctx, swp, defaultAmountBackoffRetry, defaultAmountBackoff,
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// dispatchBestAssetEasyAutoloopSwap tries to dispatch a swap to bring the total
|
||||
// local balance back to the target for the given asset.
|
||||
func (m *Manager) dispatchBestAssetEasyAutoloopSwap(ctx context.Context,
|
||||
assetID string, localTarget uint64) error {
|
||||
|
||||
if len(assetID) != sha256.Size*2 {
|
||||
return fmt.Errorf("invalid asset id: %v", assetID)
|
||||
}
|
||||
|
||||
// Retrieve existing swaps.
|
||||
loopOut, err := m.cfg.ListLoopOut(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
loopIn, err := m.cfg.ListLoopIn(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get a summary of our existing swaps so that we can check our autoloop
|
||||
// budget.
|
||||
summary := m.checkExistingAutoLoops(ctx, loopOut, loopIn)
|
||||
|
||||
err = m.checkSummaryBudget(summary)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = m.checkSummaryInflight(summary)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get all channels in order to calculate current total local balance.
|
||||
channels, err := m.cfg.Lnd.Client.ListChannels(ctx, false, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If we are running a custom asset, we'll need to get a random asset
|
||||
// peer pubkey in order to rfq the asset price.
|
||||
var assetPeerPubkey []byte
|
||||
|
||||
usableChannels := []lndclient.ChannelInfo{}
|
||||
localTotal := uint64(0)
|
||||
for _, channel := range channels {
|
||||
// We are only interested in custom asset channels.
|
||||
if !channelIsCustom(channel) {
|
||||
continue
|
||||
}
|
||||
|
||||
assetData := getCustomAssetData(channel, assetID)
|
||||
if assetData == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// We'll overwrite the channel local balance to be
|
||||
// the custom asset balance. This allows us to make
|
||||
// use of existing logic.
|
||||
channel.LocalBalance = btcutil.Amount(assetData.LocalBalance)
|
||||
usableChannels = append(usableChannels, channel)
|
||||
|
||||
// We'll use a random peer pubkey in order to get a rfq for the asset
|
||||
// to get a rough amount of sats to swap amount.
|
||||
assetPeerPubkey = channel.PubKeyBytes[:]
|
||||
|
||||
localTotal += assetData.LocalBalance
|
||||
}
|
||||
|
||||
// Since we're only autolooping-out we need to check if we are below
|
||||
// the target, meaning that we already meet the requirements.
|
||||
if localTotal <= localTarget {
|
||||
log.Debugf("Asset: %v... total local balance %v below target %v",
|
||||
assetID[:8], localTotal, localTarget)
|
||||
return nil
|
||||
}
|
||||
|
||||
restrictions, err := m.cfg.Restrictions(
|
||||
ctx, swap.TypeOut, getInitiator(m.params),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Calculate the assetAmount that we want to loop out. If it exceeds the
|
||||
// max allowed clamp it to max.
|
||||
assetAmount := localTotal - localTarget
|
||||
|
||||
// We need a request sat amount for the asset price request. We'll use
|
||||
// the average of the min and max restrictions.
|
||||
assetPriceRequestSatAmt := (restrictions.Minimum + restrictions.Maximum) / 2
|
||||
|
||||
// If we run a custom asset, we'll need to convert the asset amount
|
||||
// we want to swap to the satoshi amount.
|
||||
satAmount, err := m.cfg.GetAssetPrice(
|
||||
ctx, assetID, assetPeerPubkey, assetAmount,
|
||||
assetPriceRequestSatAmt,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if satAmount > restrictions.Maximum {
|
||||
log.Debugf("Asset %v easy autoloop: using maximum allowed "+
|
||||
"swap amount, maximum=%v, need to swap %v",
|
||||
assetID[:8], restrictions.Maximum, satAmount)
|
||||
satAmount = restrictions.Maximum
|
||||
}
|
||||
|
||||
// If the amount we want to loop out is less than the minimum we can't
|
||||
// proceed with a swap, so we return early.
|
||||
if satAmount < restrictions.Minimum {
|
||||
log.Debugf("Asset %v easy autoloop: swap amount is below"+
|
||||
" minimum swap size, minimum=%v, need to swap %v",
|
||||
assetID[:8], restrictions.Minimum, satAmount)
|
||||
return nil
|
||||
}
|
||||
|
||||
satsPerAsset := float64(satAmount) / float64(assetAmount)
|
||||
|
||||
log.Debugf("Asset %v easy autoloop: local_total=%v, target=%v, "+
|
||||
"attempting to loop out %v assets corresponding to %v sats",
|
||||
assetID[:8], localTotal, localTarget, assetAmount, satAmount)
|
||||
|
||||
// Start building that swap.
|
||||
builder := newLoopOutBuilder(m.cfg)
|
||||
|
||||
channel := m.pickEasyAutoloopChannel(
|
||||
usableChannels, restrictions, loopOut, loopIn, satsPerAsset,
|
||||
)
|
||||
if channel == nil {
|
||||
return fmt.Errorf("no eligible channel for easy autoloop")
|
||||
}
|
||||
|
||||
log.Debugf("Asset %v easy autoloop: picked channel %v with local "+
|
||||
"balance %v", assetID[:8], channel.ChannelID,
|
||||
int(channel.LocalBalance))
|
||||
|
||||
// If no fee is set, override our current parameters in order to use the
|
||||
// default percent limit of easy-autoloop.
|
||||
easyParams := m.params
|
||||
|
||||
switch feeLimit := easyParams.FeeLimit.(type) {
|
||||
case *FeePortion:
|
||||
if feeLimit.PartsPerMillion == 0 {
|
||||
easyParams.FeeLimit = &FeePortion{
|
||||
PartsPerMillion: defaultFeePPM,
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
easyParams.FeeLimit = &FeePortion{
|
||||
PartsPerMillion: defaultFeePPM,
|
||||
}
|
||||
}
|
||||
|
||||
// Set the swap outgoing channel to the chosen channel.
|
||||
outgoing := []lnwire.ShortChannelID{
|
||||
lnwire.NewShortChanIDFromInt(channel.ChannelID),
|
||||
}
|
||||
|
||||
assetSwap := &assetSwapInfo{
|
||||
assetID: assetID,
|
||||
peerPubkey: channel.PubKeyBytes[:],
|
||||
}
|
||||
|
||||
suggestion, err := builder.buildSwap(
|
||||
ctx, channel.PubKeyBytes, outgoing, satAmount, easyParams,
|
||||
withAssetSwapInfo(assetSwap),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -1420,7 +1653,7 @@ func (m *Manager) waitForSwapPayment(ctx context.Context, swapHash lntypes.Hash,
|
|||
// swap conflicts.
|
||||
func (m *Manager) pickEasyAutoloopChannel(channels []lndclient.ChannelInfo,
|
||||
restrictions *Restrictions, loopOut []*loopdb.LoopOut,
|
||||
loopIn []*loopdb.LoopIn) *lndclient.ChannelInfo {
|
||||
loopIn []*loopdb.LoopIn, satsPerAsset float64) *lndclient.ChannelInfo {
|
||||
|
||||
traffic := m.currentSwapTraffic(loopOut, loopIn)
|
||||
|
||||
|
|
@ -1434,10 +1667,6 @@ func (m *Manager) pickEasyAutoloopChannel(channels []lndclient.ChannelInfo,
|
|||
// Check each channel, since channels are already sorted we return the
|
||||
// first channel that passes all checks.
|
||||
for _, channel := range channels {
|
||||
if channelIsCustom(channel) {
|
||||
continue
|
||||
}
|
||||
|
||||
shortChanID := lnwire.NewShortChanIDFromInt(channel.ChannelID)
|
||||
|
||||
if !channel.Active {
|
||||
|
|
@ -1460,7 +1689,16 @@ func (m *Manager) pickEasyAutoloopChannel(channels []lndclient.ChannelInfo,
|
|||
continue
|
||||
}
|
||||
|
||||
if channel.LocalBalance < restrictions.Minimum {
|
||||
localBalance := channel.LocalBalance
|
||||
|
||||
// If we're running a custom asset, the local balance is
|
||||
// denominated in the asset's unit, so we convert it to
|
||||
// back to sats to check the minimum.
|
||||
if channelIsCustom(channel) {
|
||||
localBalance = localBalance.MulF64(satsPerAsset)
|
||||
}
|
||||
|
||||
if localBalance < restrictions.Minimum {
|
||||
log.Debugf("Channel %v cannot be used for easy "+
|
||||
"autoloop: insufficient local balance %v,"+
|
||||
"minimum is %v, skipping remaining channels",
|
||||
|
|
@ -1571,3 +1809,28 @@ func channelIsCustom(channel lndclient.ChannelInfo) bool {
|
|||
// don't want to consider it for swaps.
|
||||
return channel.CustomChannelData != nil
|
||||
}
|
||||
|
||||
// getCustomAssetData returns the asset data for a custom channel.
|
||||
func getCustomAssetData(channel lndclient.ChannelInfo, assetID string,
|
||||
) *rfqmsg.JsonAssetChanInfo {
|
||||
|
||||
if channel.CustomChannelData == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var assetData rfqmsg.JsonAssetChannel
|
||||
err := json.Unmarshal(channel.CustomChannelData, &assetData)
|
||||
if err != nil {
|
||||
log.Errorf("Error unmarshalling custom channel %v data: %v",
|
||||
channel.ChannelID, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, asset := range assetData.Assets {
|
||||
if asset.AssetInfo.AssetGenesis.AssetID == assetID {
|
||||
return &asset
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue