liquidity: add asset loop out unit test

This commit is contained in:
sputn1ck 2025-03-10 10:33:03 -05:00
parent e7b2836605
commit 014e919402
No known key found for this signature in database
GPG key ID: 671103D881A5F0E4

View file

@ -1,6 +1,9 @@
package liquidity
import (
"context"
"encoding/hex"
"encoding/json"
"testing"
"time"
@ -11,6 +14,7 @@ import (
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/swap"
"github.com/lightninglabs/loop/test"
"github.com/lightninglabs/taproot-assets/rfqmsg"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route"
@ -1520,3 +1524,356 @@ func existingInFromRequest(in *loop.LoopInRequest, initTime time.Time,
},
}
}
// TestEasyAssetAutoloop tests that the easy asset autoloop logic works as
// expected. This involves testing that channels are correctly selected and
// that the balance target is successfully met.
func TestEasyAssetAutoloop(t *testing.T) {
defer test.Guard(t)
// Common variables for asset tests.
assetId := [32]byte{0x01}
assetStr := hex.EncodeToString(assetId[:])
addr, err := btcutil.DecodeAddress(p2wkhAddr, nil)
require.NoError(t, err)
// Sub-test 1: Single asset channel.
t.Run("single asset channel", func(t *testing.T) {
// Prepare a channel with asset custom data.
customChanData := rfqmsg.JsonAssetChannel{
Assets: []rfqmsg.JsonAssetChanInfo{
{
AssetInfo: rfqmsg.JsonAssetUtxo{
AssetGenesis: rfqmsg.JsonAssetGenesis{
AssetID: assetStr,
},
},
LocalBalance: 950000,
RemoteBalance: 0,
Capacity: 100000,
},
},
}
customChanDataBytes, err := json.Marshal(customChanData)
require.NoError(t, err)
assetChan := lndclient.ChannelInfo{
Active: true,
ChannelID: chanID1.ToUint64(),
PubKeyBytes: peer1,
LocalBalance: 95000,
RemoteBalance: 0,
Capacity: 100000,
CustomChannelData: customChanDataBytes,
}
channels := []lndclient.ChannelInfo{assetChan}
params := Parameters{
Autoloop: true,
DestAddr: addr,
AutoFeeBudget: 36000,
AutoFeeRefreshPeriod: time.Hour * 3,
AutoloopBudgetLastRefresh: testBudgetStart,
MaxAutoInFlight: 2,
FailureBackOff: time.Hour,
SweepConfTarget: 10,
HtlcConfTarget: defaultHtlcConfTarget,
FeeLimit: defaultFeePortion(),
AssetAutoloopParams: map[string]AssetParams{
assetStr: {
EnableEasyOut: true,
LocalTargetAssetAmount: 75000,
},
},
}
c := newAutoloopTestCtx(t, params, channels, testRestrictions)
// For testing, simply return asset units 1:1 to satoshis.
assetPriceFunc := func(ctx context.Context, assetId string,
peerPubkey []byte, assetAmt uint64, minSatAmt btcutil.Amount) (
btcutil.Amount, error) {
return btcutil.Amount(assetAmt), nil
}
c.manager.cfg.GetAssetPrice = assetPriceFunc
c.start()
// In this scenario we expect a swap of maxAmt (here chosen as 50000)
// on our single asset channel.
maxAmt := 50000
chanSwap := &loop.OutRequest{
Amount: btcutil.Amount(maxAmt),
DestAddr: addr,
OutgoingChanSet: loopdb.ChannelSet{assetChan.ChannelID},
Label: labels.AutoloopLabel(swap.TypeOut),
Initiator: autoloopSwapInitiator,
}
quotesOut := []quoteRequestResp{
{
request: &loop.LoopOutQuoteRequest{
Amount: btcutil.Amount(maxAmt),
AssetRFQRequest: &loop.AssetRFQRequest{
AssetId: assetId[:],
AssetEdgeNode: []byte("edge"),
},
},
quote: &loop.LoopOutQuote{
SwapFee: 1,
PrepayAmount: 1,
MinerFee: 1,
LoopOutRfq: &loop.LoopOutRfq{
PrepayRfqId: []byte("prepay"),
SwapRfqId: []byte("swap"),
},
},
},
}
expectedOut := []loopOutRequestResp{
{
request: chanSwap,
response: &loop.LoopOutSwapInfo{
SwapHash: lntypes.Hash{1},
},
},
}
step := &easyAutoloopStep{
minAmt: 1,
maxAmt: btcutil.Amount(maxAmt),
quotesOut: quotesOut,
expectedOut: expectedOut,
}
c.easyautoloop(step, false)
c.stop()
})
// Sub-test 2: Two asset channels.
t.Run("two asset channels", func(t *testing.T) {
// Reuse the same custom channel data for both channels.
customChanData := rfqmsg.JsonAssetChannel{
Assets: []rfqmsg.JsonAssetChanInfo{
{
AssetInfo: rfqmsg.JsonAssetUtxo{
AssetGenesis: rfqmsg.JsonAssetGenesis{
AssetID: assetStr,
},
},
LocalBalance: 950000,
RemoteBalance: 0,
Capacity: 100000,
},
},
}
customChanDataBytes1, err := json.Marshal(customChanData)
require.NoError(t, err)
customChanData.Assets[0].LocalBalance = 1050000
customChanDataBytes2, err := json.Marshal(customChanData)
require.NoError(t, err)
// Create two asset channels with different local balances.
assetChan1 := lndclient.ChannelInfo{
Active: true,
ChannelID: chanID1.ToUint64(),
PubKeyBytes: peer1,
CustomChannelData: customChanDataBytes1,
}
assetChan2 := lndclient.ChannelInfo{
Active: true,
ChannelID: chanID2.ToUint64(), // different channel ID
PubKeyBytes: peer2,
CustomChannelData: customChanDataBytes2,
}
channels := []lndclient.ChannelInfo{assetChan1, assetChan2}
params := Parameters{
Autoloop: true,
DestAddr: addr,
AutoFeeBudget: 36000,
AutoFeeRefreshPeriod: time.Hour * 3,
AutoloopBudgetLastRefresh: testBudgetStart,
MaxAutoInFlight: 2,
FailureBackOff: time.Hour,
SweepConfTarget: 10,
HtlcConfTarget: defaultHtlcConfTarget,
FeeLimit: defaultFeePortion(),
AssetAutoloopParams: map[string]AssetParams{
assetStr: {
EnableEasyOut: true,
LocalTargetAssetAmount: 75000,
},
},
}
c := newAutoloopTestCtx(t, params, channels, testRestrictions)
assetPriceFunc := func(ctx context.Context, assetId string,
peerPubkey []byte, assetAmt uint64, minSatAmt btcutil.Amount) (
btcutil.Amount, error) {
return btcutil.Amount(assetAmt), nil
}
c.manager.cfg.GetAssetPrice = assetPriceFunc
c.start()
// Expect a swap on the channel with the higher local balance (assetChan2).
maxAmt := 40000
chanSwap := &loop.OutRequest{
Amount: btcutil.Amount(maxAmt),
DestAddr: addr,
OutgoingChanSet: loopdb.ChannelSet{assetChan2.ChannelID},
Label: labels.AutoloopLabel(swap.TypeOut),
Initiator: autoloopSwapInitiator,
}
quotesOut := []quoteRequestResp{
{
request: &loop.LoopOutQuoteRequest{
Amount: btcutil.Amount(maxAmt),
AssetRFQRequest: &loop.AssetRFQRequest{
AssetId: assetId[:],
AssetEdgeNode: []byte("edge"),
},
},
quote: &loop.LoopOutQuote{
SwapFee: 1,
PrepayAmount: 1,
MinerFee: 1,
LoopOutRfq: &loop.LoopOutRfq{
PrepayRfqId: []byte("prepay"),
SwapRfqId: []byte("swap"),
},
},
},
}
expectedOut := []loopOutRequestResp{
{
request: chanSwap,
response: &loop.LoopOutSwapInfo{
SwapHash: lntypes.Hash{1},
},
},
}
step := &easyAutoloopStep{
minAmt: 1,
maxAmt: btcutil.Amount(maxAmt),
quotesOut: quotesOut,
expectedOut: expectedOut,
}
c.easyautoloop(step, false)
c.stop()
})
// Sub-test 3: Mixed asset and non-asset channels.
t.Run("non asset and normal channel", func(t *testing.T) {
// Create an asset channel with custom asset data.
customChanData := rfqmsg.JsonAssetChannel{
Assets: []rfqmsg.JsonAssetChanInfo{
{
AssetInfo: rfqmsg.JsonAssetUtxo{
AssetGenesis: rfqmsg.JsonAssetGenesis{
AssetID: assetStr,
},
},
LocalBalance: 950000,
RemoteBalance: 0,
Capacity: 100000,
},
},
}
customChanDataBytes, err := json.Marshal(customChanData)
require.NoError(t, err)
assetChan := lndclient.ChannelInfo{
Active: true,
ChannelID: chanID1.ToUint64(),
PubKeyBytes: peer1,
LocalBalance: 95000,
RemoteBalance: 0,
Capacity: 100000,
CustomChannelData: customChanDataBytes,
}
// Create a normal channel (no custom channel data).
normalChan := lndclient.ChannelInfo{
Active: true,
ChannelID: chanID2.ToUint64(),
PubKeyBytes: peer1,
LocalBalance: 100000,
RemoteBalance: 0,
Capacity: 100000,
}
channels := []lndclient.ChannelInfo{assetChan, normalChan}
params := Parameters{
Autoloop: true,
DestAddr: addr,
AutoFeeBudget: 36000,
AutoFeeRefreshPeriod: time.Hour * 3,
AutoloopBudgetLastRefresh: testBudgetStart,
MaxAutoInFlight: 2,
FailureBackOff: time.Hour,
SweepConfTarget: 10,
HtlcConfTarget: defaultHtlcConfTarget,
FeeLimit: defaultFeePortion(),
AssetAutoloopParams: map[string]AssetParams{
assetStr: {
EnableEasyOut: true,
LocalTargetAssetAmount: 75000,
},
},
}
c := newAutoloopTestCtx(t, params, channels, testRestrictions)
assetPriceFunc := func(ctx context.Context, assetId string,
peerPubkey []byte, assetAmt uint64, minSatAmt btcutil.Amount) (
btcutil.Amount, error) {
return btcutil.Amount(assetAmt), nil
}
c.manager.cfg.GetAssetPrice = assetPriceFunc
c.start()
maxAmtAsset := 50000
assetSwap := &loop.OutRequest{
Amount: btcutil.Amount(maxAmtAsset),
DestAddr: addr,
OutgoingChanSet: loopdb.ChannelSet{assetChan.ChannelID},
Label: labels.AutoloopLabel(swap.TypeOut),
Initiator: autoloopSwapInitiator,
}
quotesOut := []quoteRequestResp{
{
request: &loop.LoopOutQuoteRequest{
Amount: btcutil.Amount(maxAmtAsset),
AssetRFQRequest: &loop.AssetRFQRequest{
AssetId: assetId[:],
AssetEdgeNode: []byte("edge"),
},
},
quote: &loop.LoopOutQuote{
SwapFee: 1,
PrepayAmount: 1,
MinerFee: 1,
LoopOutRfq: &loop.LoopOutRfq{
PrepayRfqId: []byte("prepay"),
SwapRfqId: []byte("swap"),
},
},
},
}
expectedOut := []loopOutRequestResp{
{
request: assetSwap,
response: &loop.LoopOutSwapInfo{
SwapHash: lntypes.Hash{1},
},
},
}
step := &easyAutoloopStep{
minAmt: 1,
maxAmt: 50000,
quotesOut: quotesOut,
expectedOut: expectedOut,
}
c.easyautoloop(step, false)
c.stop()
})
}