Skip to content

Commit 5d19b1d

Browse files
committed
liquidity: add asset loop out unit test
1 parent 23a53ee commit 5d19b1d

File tree

1 file changed

+357
-0
lines changed

1 file changed

+357
-0
lines changed

liquidity/autoloop_test.go

+357
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package liquidity
22

33
import (
4+
"context"
5+
"encoding/hex"
6+
"encoding/json"
47
"testing"
58
"time"
69

@@ -11,6 +14,7 @@ import (
1114
"github.com/lightninglabs/loop/loopdb"
1215
"github.com/lightninglabs/loop/swap"
1316
"github.com/lightninglabs/loop/test"
17+
"github.com/lightninglabs/taproot-assets/rfqmsg"
1418
"github.com/lightningnetwork/lnd/lntypes"
1519
"github.com/lightningnetwork/lnd/lnwire"
1620
"github.com/lightningnetwork/lnd/routing/route"
@@ -1520,3 +1524,356 @@ func existingInFromRequest(in *loop.LoopInRequest, initTime time.Time,
15201524
},
15211525
}
15221526
}
1527+
1528+
// TestEasyAssetAutoloop tests that the easy asset autoloop logic works as
1529+
// expected. This involves testing that channels are correctly selected and
1530+
// that the balance target is successfully met.
1531+
func TestEasyAssetAutoloop(t *testing.T) {
1532+
defer test.Guard(t)
1533+
1534+
// Common variables for asset tests.
1535+
assetId := [32]byte{0x01}
1536+
assetStr := hex.EncodeToString(assetId[:])
1537+
addr, err := btcutil.DecodeAddress(p2wkhAddr, nil)
1538+
require.NoError(t, err)
1539+
1540+
// Sub-test 1: Single asset channel.
1541+
t.Run("single asset channel", func(t *testing.T) {
1542+
// Prepare a channel with asset custom data.
1543+
customChanData := rfqmsg.JsonAssetChannel{
1544+
Assets: []rfqmsg.JsonAssetChanInfo{
1545+
{
1546+
AssetInfo: rfqmsg.JsonAssetUtxo{
1547+
AssetGenesis: rfqmsg.JsonAssetGenesis{
1548+
AssetID: assetStr,
1549+
},
1550+
},
1551+
LocalBalance: 950000,
1552+
RemoteBalance: 0,
1553+
Capacity: 100000,
1554+
},
1555+
},
1556+
}
1557+
customChanDataBytes, err := json.Marshal(customChanData)
1558+
require.NoError(t, err)
1559+
1560+
assetChan := lndclient.ChannelInfo{
1561+
Active: true,
1562+
ChannelID: chanID1.ToUint64(),
1563+
PubKeyBytes: peer1,
1564+
LocalBalance: 95000,
1565+
RemoteBalance: 0,
1566+
Capacity: 100000,
1567+
CustomChannelData: customChanDataBytes,
1568+
}
1569+
1570+
channels := []lndclient.ChannelInfo{assetChan}
1571+
params := Parameters{
1572+
Autoloop: true,
1573+
DestAddr: addr,
1574+
AutoFeeBudget: 36000,
1575+
AutoFeeRefreshPeriod: time.Hour * 3,
1576+
AutoloopBudgetLastRefresh: testBudgetStart,
1577+
MaxAutoInFlight: 2,
1578+
FailureBackOff: time.Hour,
1579+
SweepConfTarget: 10,
1580+
HtlcConfTarget: defaultHtlcConfTarget,
1581+
FeeLimit: defaultFeePortion(),
1582+
AssetAutoloopParams: map[string]AssetParams{
1583+
assetStr: {
1584+
EnableEasyOut: true,
1585+
LocalTargetAssetAmount: 75000,
1586+
},
1587+
},
1588+
}
1589+
1590+
c := newAutoloopTestCtx(t, params, channels, testRestrictions)
1591+
// For testing, simply return asset units 1:1 to satoshis.
1592+
assetPriceFunc := func(ctx context.Context, assetId string,
1593+
peerPubkey []byte, assetAmt uint64, minSatAmt btcutil.Amount) (
1594+
btcutil.Amount, error) {
1595+
1596+
return btcutil.Amount(assetAmt), nil
1597+
}
1598+
c.manager.cfg.GetAssetPrice = assetPriceFunc
1599+
c.start()
1600+
1601+
// In this scenario we expect a swap of maxAmt (here chosen as 50000)
1602+
// on our single asset channel.
1603+
maxAmt := 50000
1604+
chanSwap := &loop.OutRequest{
1605+
Amount: btcutil.Amount(maxAmt),
1606+
DestAddr: addr,
1607+
OutgoingChanSet: loopdb.ChannelSet{assetChan.ChannelID},
1608+
Label: labels.AutoloopLabel(swap.TypeOut),
1609+
Initiator: autoloopSwapInitiator,
1610+
}
1611+
quotesOut := []quoteRequestResp{
1612+
{
1613+
request: &loop.LoopOutQuoteRequest{
1614+
Amount: btcutil.Amount(maxAmt),
1615+
AssetRFQRequest: &loop.AssetRFQRequest{
1616+
AssetId: assetId[:],
1617+
AssetEdgeNode: []byte("edge"),
1618+
},
1619+
},
1620+
quote: &loop.LoopOutQuote{
1621+
SwapFee: 1,
1622+
PrepayAmount: 1,
1623+
MinerFee: 1,
1624+
LoopOutRfq: &loop.LoopOutRfq{
1625+
PrepayRfqId: []byte("prepay"),
1626+
SwapRfqId: []byte("swap"),
1627+
},
1628+
},
1629+
},
1630+
}
1631+
expectedOut := []loopOutRequestResp{
1632+
{
1633+
request: chanSwap,
1634+
response: &loop.LoopOutSwapInfo{
1635+
SwapHash: lntypes.Hash{1},
1636+
},
1637+
},
1638+
}
1639+
step := &easyAutoloopStep{
1640+
minAmt: 1,
1641+
maxAmt: btcutil.Amount(maxAmt),
1642+
quotesOut: quotesOut,
1643+
expectedOut: expectedOut,
1644+
}
1645+
c.easyautoloop(step, false)
1646+
c.stop()
1647+
})
1648+
1649+
// Sub-test 2: Two asset channels.
1650+
t.Run("two asset channels", func(t *testing.T) {
1651+
// Reuse the same custom channel data for both channels.
1652+
customChanData := rfqmsg.JsonAssetChannel{
1653+
Assets: []rfqmsg.JsonAssetChanInfo{
1654+
{
1655+
AssetInfo: rfqmsg.JsonAssetUtxo{
1656+
AssetGenesis: rfqmsg.JsonAssetGenesis{
1657+
AssetID: assetStr,
1658+
},
1659+
},
1660+
LocalBalance: 950000,
1661+
RemoteBalance: 0,
1662+
Capacity: 100000,
1663+
},
1664+
},
1665+
}
1666+
customChanDataBytes1, err := json.Marshal(customChanData)
1667+
require.NoError(t, err)
1668+
1669+
customChanData.Assets[0].LocalBalance = 1050000
1670+
customChanDataBytes2, err := json.Marshal(customChanData)
1671+
require.NoError(t, err)
1672+
1673+
// Create two asset channels with different local balances.
1674+
assetChan1 := lndclient.ChannelInfo{
1675+
Active: true,
1676+
ChannelID: chanID1.ToUint64(),
1677+
PubKeyBytes: peer1,
1678+
CustomChannelData: customChanDataBytes1,
1679+
}
1680+
assetChan2 := lndclient.ChannelInfo{
1681+
Active: true,
1682+
ChannelID: chanID2.ToUint64(), // different channel ID
1683+
PubKeyBytes: peer2,
1684+
CustomChannelData: customChanDataBytes2,
1685+
}
1686+
1687+
channels := []lndclient.ChannelInfo{assetChan1, assetChan2}
1688+
params := Parameters{
1689+
Autoloop: true,
1690+
DestAddr: addr,
1691+
AutoFeeBudget: 36000,
1692+
AutoFeeRefreshPeriod: time.Hour * 3,
1693+
AutoloopBudgetLastRefresh: testBudgetStart,
1694+
MaxAutoInFlight: 2,
1695+
FailureBackOff: time.Hour,
1696+
SweepConfTarget: 10,
1697+
HtlcConfTarget: defaultHtlcConfTarget,
1698+
FeeLimit: defaultFeePortion(),
1699+
AssetAutoloopParams: map[string]AssetParams{
1700+
assetStr: {
1701+
EnableEasyOut: true,
1702+
LocalTargetAssetAmount: 75000,
1703+
},
1704+
},
1705+
}
1706+
1707+
c := newAutoloopTestCtx(t, params, channels, testRestrictions)
1708+
assetPriceFunc := func(ctx context.Context, assetId string,
1709+
peerPubkey []byte, assetAmt uint64, minSatAmt btcutil.Amount) (
1710+
btcutil.Amount, error) {
1711+
1712+
return btcutil.Amount(assetAmt), nil
1713+
}
1714+
c.manager.cfg.GetAssetPrice = assetPriceFunc
1715+
c.start()
1716+
1717+
// Expect a swap on the channel with the higher local balance (assetChan2).
1718+
maxAmt := 40000
1719+
chanSwap := &loop.OutRequest{
1720+
Amount: btcutil.Amount(maxAmt),
1721+
DestAddr: addr,
1722+
OutgoingChanSet: loopdb.ChannelSet{assetChan2.ChannelID},
1723+
Label: labels.AutoloopLabel(swap.TypeOut),
1724+
Initiator: autoloopSwapInitiator,
1725+
}
1726+
quotesOut := []quoteRequestResp{
1727+
{
1728+
request: &loop.LoopOutQuoteRequest{
1729+
Amount: btcutil.Amount(maxAmt),
1730+
AssetRFQRequest: &loop.AssetRFQRequest{
1731+
AssetId: assetId[:],
1732+
AssetEdgeNode: []byte("edge"),
1733+
},
1734+
},
1735+
quote: &loop.LoopOutQuote{
1736+
SwapFee: 1,
1737+
PrepayAmount: 1,
1738+
MinerFee: 1,
1739+
LoopOutRfq: &loop.LoopOutRfq{
1740+
PrepayRfqId: []byte("prepay"),
1741+
SwapRfqId: []byte("swap"),
1742+
},
1743+
},
1744+
},
1745+
}
1746+
expectedOut := []loopOutRequestResp{
1747+
{
1748+
request: chanSwap,
1749+
response: &loop.LoopOutSwapInfo{
1750+
SwapHash: lntypes.Hash{1},
1751+
},
1752+
},
1753+
}
1754+
step := &easyAutoloopStep{
1755+
minAmt: 1,
1756+
maxAmt: btcutil.Amount(maxAmt),
1757+
quotesOut: quotesOut,
1758+
expectedOut: expectedOut,
1759+
}
1760+
c.easyautoloop(step, false)
1761+
c.stop()
1762+
})
1763+
1764+
// Sub-test 3: Mixed asset and non-asset channels.
1765+
t.Run("non asset and normal channel", func(t *testing.T) {
1766+
// Create an asset channel with custom asset data.
1767+
customChanData := rfqmsg.JsonAssetChannel{
1768+
Assets: []rfqmsg.JsonAssetChanInfo{
1769+
{
1770+
AssetInfo: rfqmsg.JsonAssetUtxo{
1771+
AssetGenesis: rfqmsg.JsonAssetGenesis{
1772+
AssetID: assetStr,
1773+
},
1774+
},
1775+
LocalBalance: 950000,
1776+
RemoteBalance: 0,
1777+
Capacity: 100000,
1778+
},
1779+
},
1780+
}
1781+
customChanDataBytes, err := json.Marshal(customChanData)
1782+
require.NoError(t, err)
1783+
assetChan := lndclient.ChannelInfo{
1784+
Active: true,
1785+
ChannelID: chanID1.ToUint64(),
1786+
PubKeyBytes: peer1,
1787+
LocalBalance: 95000,
1788+
RemoteBalance: 0,
1789+
Capacity: 100000,
1790+
CustomChannelData: customChanDataBytes,
1791+
}
1792+
1793+
// Create a normal channel (no custom channel data).
1794+
normalChan := lndclient.ChannelInfo{
1795+
Active: true,
1796+
ChannelID: chanID2.ToUint64(),
1797+
PubKeyBytes: peer1,
1798+
LocalBalance: 100000,
1799+
RemoteBalance: 0,
1800+
Capacity: 100000,
1801+
}
1802+
1803+
channels := []lndclient.ChannelInfo{assetChan, normalChan}
1804+
params := Parameters{
1805+
Autoloop: true,
1806+
DestAddr: addr,
1807+
AutoFeeBudget: 36000,
1808+
AutoFeeRefreshPeriod: time.Hour * 3,
1809+
AutoloopBudgetLastRefresh: testBudgetStart,
1810+
MaxAutoInFlight: 2,
1811+
FailureBackOff: time.Hour,
1812+
SweepConfTarget: 10,
1813+
HtlcConfTarget: defaultHtlcConfTarget,
1814+
FeeLimit: defaultFeePortion(),
1815+
AssetAutoloopParams: map[string]AssetParams{
1816+
assetStr: {
1817+
EnableEasyOut: true,
1818+
LocalTargetAssetAmount: 75000,
1819+
},
1820+
},
1821+
}
1822+
1823+
c := newAutoloopTestCtx(t, params, channels, testRestrictions)
1824+
assetPriceFunc := func(ctx context.Context, assetId string,
1825+
peerPubkey []byte, assetAmt uint64, minSatAmt btcutil.Amount) (
1826+
btcutil.Amount, error) {
1827+
1828+
return btcutil.Amount(assetAmt), nil
1829+
}
1830+
c.manager.cfg.GetAssetPrice = assetPriceFunc
1831+
c.start()
1832+
1833+
maxAmtAsset := 50000
1834+
1835+
assetSwap := &loop.OutRequest{
1836+
Amount: btcutil.Amount(maxAmtAsset),
1837+
DestAddr: addr,
1838+
OutgoingChanSet: loopdb.ChannelSet{assetChan.ChannelID},
1839+
Label: labels.AutoloopLabel(swap.TypeOut),
1840+
Initiator: autoloopSwapInitiator,
1841+
}
1842+
quotesOut := []quoteRequestResp{
1843+
{
1844+
request: &loop.LoopOutQuoteRequest{
1845+
Amount: btcutil.Amount(maxAmtAsset),
1846+
AssetRFQRequest: &loop.AssetRFQRequest{
1847+
AssetId: assetId[:],
1848+
AssetEdgeNode: []byte("edge"),
1849+
},
1850+
},
1851+
quote: &loop.LoopOutQuote{
1852+
SwapFee: 1,
1853+
PrepayAmount: 1,
1854+
MinerFee: 1,
1855+
LoopOutRfq: &loop.LoopOutRfq{
1856+
PrepayRfqId: []byte("prepay"),
1857+
SwapRfqId: []byte("swap"),
1858+
},
1859+
},
1860+
},
1861+
}
1862+
expectedOut := []loopOutRequestResp{
1863+
{
1864+
request: assetSwap,
1865+
response: &loop.LoopOutSwapInfo{
1866+
SwapHash: lntypes.Hash{1},
1867+
},
1868+
},
1869+
}
1870+
step := &easyAutoloopStep{
1871+
minAmt: 1,
1872+
maxAmt: 50000,
1873+
quotesOut: quotesOut,
1874+
expectedOut: expectedOut,
1875+
}
1876+
c.easyautoloop(step, false)
1877+
c.stop()
1878+
})
1879+
}

0 commit comments

Comments
 (0)