11package liquidity
22
33import (
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