@@ -221,6 +221,11 @@ type Config struct {
221221 LoopOutTerms func (ctx context.Context ,
222222 initiator string ) (* loop.LoopOutTerms , error )
223223
224+ // ListStaticLoopIn returns all static-address loop-ins that liquidity
225+ // should consider for budget accounting, in-flight limits, and peer
226+ // traffic.
227+ ListStaticLoopIn func (context.Context ) ([]* StaticLoopInInfo , error )
228+
224229 // GetAssetPrice returns the price of an asset in satoshis.
225230 GetAssetPrice func (ctx context.Context , assetId string ,
226231 peerPubkey []byte , assetAmt uint64 ,
@@ -574,9 +579,19 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
574579 return err
575580 }
576581
582+ // Load the static loop-in snapshot once for the whole easy-autoloop
583+ // tick so budget and traffic checks cannot drift and do not need to hit
584+ // the store twice.
585+ staticLoopIns , err := m .loadStaticLoopIns (ctx )
586+ if err != nil {
587+ return err
588+ }
589+
577590 // Get a summary of our existing swaps so that we can check our autoloop
578591 // budget.
579- summary := m .checkExistingAutoLoops (ctx , loopOut , loopIn )
592+ summary := m .checkExistingAutoLoopsWithStatic (
593+ loopOut , loopIn , staticLoopIns ,
594+ )
580595
581596 err = m .checkSummaryBudget (summary )
582597 if err != nil {
@@ -640,9 +655,13 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
640655 // Start building that swap.
641656 builder := newLoopOutBuilder (m .cfg )
642657
643- channel := m .pickEasyAutoloopChannel (
644- usableChannels , restrictions , loopOut , loopIn , 0 ,
658+ channel , err := m .pickEasyAutoloopChannelWithStatic (
659+ usableChannels , restrictions , loopOut , loopIn ,
660+ staticLoopIns , 0 ,
645661 )
662+ if err != nil {
663+ return err
664+ }
646665 if channel == nil {
647666 return fmt .Errorf ("no eligible channel for easy autoloop" )
648667 }
@@ -721,9 +740,19 @@ func (m *Manager) dispatchBestAssetEasyAutoloopSwap(ctx context.Context,
721740 return err
722741 }
723742
743+ // Load the static loop-in snapshot once for the whole easy-autoloop
744+ // tick so budget and traffic checks cannot drift and do not need to hit
745+ // the store twice.
746+ staticLoopIns , err := m .loadStaticLoopIns (ctx )
747+ if err != nil {
748+ return err
749+ }
750+
724751 // Get a summary of our existing swaps so that we can check our autoloop
725752 // budget.
726- summary := m .checkExistingAutoLoops (ctx , loopOut , loopIn )
753+ summary := m .checkExistingAutoLoopsWithStatic (
754+ loopOut , loopIn , staticLoopIns ,
755+ )
727756
728757 err = m .checkSummaryBudget (summary )
729758 if err != nil {
@@ -829,9 +858,13 @@ func (m *Manager) dispatchBestAssetEasyAutoloopSwap(ctx context.Context,
829858 // Start building that swap.
830859 builder := newLoopOutBuilder (m .cfg )
831860
832- channel := m .pickEasyAutoloopChannel (
833- usableChannels , restrictions , loopOut , loopIn , satsPerAsset ,
861+ channel , err := m .pickEasyAutoloopChannelWithStatic (
862+ usableChannels , restrictions , loopOut , loopIn ,
863+ staticLoopIns , satsPerAsset ,
834864 )
865+ if err != nil {
866+ return err
867+ }
835868 if channel == nil {
836869 return fmt .Errorf ("no eligible channel for easy autoloop" )
837870 }
@@ -990,9 +1023,16 @@ func (m *Manager) SuggestSwaps(ctx context.Context) (
9901023 return nil , err
9911024 }
9921025
1026+ staticLoopIns , err := m .loadStaticLoopIns (ctx )
1027+ if err != nil {
1028+ return nil , err
1029+ }
1030+
9931031 // Get a summary of our existing swaps so that we can check our autoloop
9941032 // budget.
995- summary := m .checkExistingAutoLoops (ctx , loopOut , loopIn )
1033+ summary := m .checkExistingAutoLoopsWithStatic (
1034+ loopOut , loopIn , staticLoopIns ,
1035+ )
9961036
9971037 err = m .checkSummaryBudget (summary )
9981038 if err != nil {
@@ -1037,7 +1077,9 @@ func (m *Manager) SuggestSwaps(ctx context.Context) (
10371077
10381078 // Get a summary of the channels and peers that are not eligible due
10391079 // to ongoing swaps.
1040- traffic := m .currentSwapTraffic (loopOut , loopIn )
1080+ traffic := m .currentSwapTrafficWithStatic (
1081+ loopOut , loopIn , staticLoopIns ,
1082+ )
10411083
10421084 var (
10431085 suggestions []swapSuggestion
@@ -1182,6 +1224,18 @@ func (m *Manager) SuggestSwaps(ctx context.Context) (
11821224 return resp , nil
11831225}
11841226
1227+ // loadStaticLoopIns retrieves the static loop-ins that liquidity uses for
1228+ // shared accounting and traffic calculations.
1229+ func (m * Manager ) loadStaticLoopIns (ctx context.Context ) (
1230+ []* StaticLoopInInfo , error ) {
1231+
1232+ if m .cfg .ListStaticLoopIn == nil {
1233+ return nil , nil
1234+ }
1235+
1236+ return m .cfg .ListStaticLoopIn (ctx )
1237+ }
1238+
11851239// suggestSwap checks whether we can currently perform a swap, and creates a
11861240// swap request for the rule provided.
11871241func (m * Manager ) suggestSwap (ctx context.Context , traffic * swapTraffic ,
@@ -1308,12 +1362,28 @@ func (e *existingAutoLoopSummary) totalFees() btcutil.Amount {
13081362}
13091363
13101364// checkExistingAutoLoops calculates the total amount that has been spent by
1311- // automatically dispatched swaps that have completed, and the worst-case fee
1312- // total for our set of ongoing, automatically dispatched swaps as well as a
1313- // current in-flight count.
1314- func (m * Manager ) checkExistingAutoLoops (_ context.Context ,
1365+ // automatically dispatched swaps that have completed, the worst-case fee total
1366+ // for our set of ongoing automatically dispatched swaps, and the current
1367+ // in-flight count.
1368+ func (m * Manager ) checkExistingAutoLoops (ctx context.Context ,
13151369 loopOuts []* loopdb.LoopOut ,
1316- loopIns []* loopdb.LoopIn ) * existingAutoLoopSummary {
1370+ loopIns []* loopdb.LoopIn ) (* existingAutoLoopSummary , error ) {
1371+
1372+ staticLoopIns , err := m .loadStaticLoopIns (ctx )
1373+ if err != nil {
1374+ return nil , err
1375+ }
1376+
1377+ return m .checkExistingAutoLoopsWithStatic (
1378+ loopOuts , loopIns , staticLoopIns ,
1379+ ), nil
1380+ }
1381+
1382+ // checkExistingAutoLoopsWithStatic calculates our autoloop budget summary from
1383+ // the provided swap snapshots.
1384+ func (m * Manager ) checkExistingAutoLoopsWithStatic (
1385+ loopOuts []* loopdb.LoopOut , loopIns []* loopdb.LoopIn ,
1386+ staticLoopIns []* StaticLoopInInfo ) * existingAutoLoopSummary {
13171387
13181388 var summary existingAutoLoopSummary
13191389
@@ -1370,14 +1440,72 @@ func (m *Manager) checkExistingAutoLoops(_ context.Context,
13701440 }
13711441 }
13721442
1443+ for _ , in := range staticLoopIns {
1444+ if ! isAutoloopLabel (in .Label ) {
1445+ continue
1446+ }
1447+
1448+ inBudget := ! in .LastUpdateTime .Before (
1449+ m .params .AutoloopBudgetLastRefresh ,
1450+ )
1451+
1452+ switch {
1453+ case in .Pending :
1454+ summary .inFlightCount ++
1455+ summary .pendingFees += staticLoopInWorstCaseFees (
1456+ in .NumDeposits , in .HasChange , in .QuotedSwapFee ,
1457+ in .HtlcTxFeeRate , defaultLoopInSweepFee ,
1458+ )
1459+
1460+ case ! inBudget :
1461+ continue
1462+
1463+ case in .Failed :
1464+ // Static loop-in failure accounting stays pessimistic
1465+ // here. Once the swap is terminal we no longer know
1466+ // from liquidity's persisted view whether the timeout
1467+ // path actually confirmed, so we reserve the same
1468+ // worst-case fee shape we used while the swap was in
1469+ // flight.
1470+ // TODO: Persist real static-address swap costs,
1471+ // similar to loopdb.SwapCost, and use that exact
1472+ // terminal value here instead of the pessimistic
1473+ // worst-case estimate.
1474+ summary .spentFees += staticLoopInWorstCaseFees (
1475+ in .NumDeposits , in .HasChange , in .QuotedSwapFee ,
1476+ in .HtlcTxFeeRate , defaultLoopInSweepFee ,
1477+ )
1478+
1479+ default :
1480+ summary .spentFees += in .QuotedSwapFee
1481+ }
1482+ }
1483+
13731484 return & summary
13741485}
13751486
13761487// currentSwapTraffic examines our existing swaps and returns a summary of the
13771488// current activity which can be used to determine whether we should perform
13781489// any swaps.
1379- func (m * Manager ) currentSwapTraffic (loopOut []* loopdb.LoopOut ,
1380- loopIn []* loopdb.LoopIn ) * swapTraffic {
1490+ func (m * Manager ) currentSwapTraffic (ctx context.Context ,
1491+ loopOut []* loopdb.LoopOut ,
1492+ loopIn []* loopdb.LoopIn ) (* swapTraffic , error ) {
1493+
1494+ staticLoopIns , err := m .loadStaticLoopIns (ctx )
1495+ if err != nil {
1496+ return nil , err
1497+ }
1498+
1499+ return m .currentSwapTrafficWithStatic (
1500+ loopOut , loopIn , staticLoopIns ,
1501+ ), nil
1502+ }
1503+
1504+ // currentSwapTrafficWithStatic builds the shared traffic view from the
1505+ // provided swap snapshots.
1506+ func (m * Manager ) currentSwapTrafficWithStatic (loopOut []* loopdb.LoopOut ,
1507+ loopIn []* loopdb.LoopIn ,
1508+ staticLoopIns []* StaticLoopInInfo ) * swapTraffic {
13811509
13821510 traffic := newSwapTraffic ()
13831511
@@ -1408,9 +1536,7 @@ func (m *Manager) currentSwapTraffic(loopOut []*loopdb.LoopOut,
14081536
14091537 if failedAt .After (failureCutoff ) {
14101538 for _ , id := range chanSet {
1411- chanID := lnwire .NewShortChanIDFromInt (
1412- id ,
1413- )
1539+ chanID := lnwire .NewShortChanIDFromInt (id )
14141540
14151541 traffic .failedLoopOut [chanID ] = failedAt
14161542 }
@@ -1464,6 +1590,22 @@ func (m *Manager) currentSwapTraffic(loopOut []*loopdb.LoopOut,
14641590 }
14651591 }
14661592
1593+ for _ , in := range staticLoopIns {
1594+ if in .LastHop == nil {
1595+ continue
1596+ }
1597+
1598+ pubkey := * in .LastHop
1599+
1600+ switch {
1601+ case in .Pending && in .BlocksLoopIn :
1602+ traffic .ongoingLoopIn [pubkey ] = true
1603+
1604+ case in .Failed && in .LastUpdateTime .After (failureCutoff ):
1605+ traffic .failedLoopIn [pubkey ] = in .LastUpdateTime
1606+ }
1607+ }
1608+
14671609 return traffic
14681610}
14691611
@@ -1651,11 +1793,34 @@ func (m *Manager) waitForSwapPayment(ctx context.Context, swapHash lntypes.Hash,
16511793// This function prioritizes channels with high local balance but also consults
16521794// previous failures and ongoing swaps to avoid temporary channel failures or
16531795// swap conflicts.
1654- func (m * Manager ) pickEasyAutoloopChannel (channels []lndclient.ChannelInfo ,
1655- restrictions * Restrictions , loopOut []* loopdb.LoopOut ,
1656- loopIn []* loopdb.LoopIn , satsPerAsset float64 ) * lndclient.ChannelInfo {
1796+ func (m * Manager ) pickEasyAutoloopChannel (ctx context.Context ,
1797+ channels []lndclient.ChannelInfo , restrictions * Restrictions ,
1798+ loopOut []* loopdb.LoopOut , loopIn []* loopdb.LoopIn ,
1799+ satsPerAsset float64 ) (* lndclient.ChannelInfo , error ) {
16571800
1658- traffic := m .currentSwapTraffic (loopOut , loopIn )
1801+ staticLoopIns , err := m .loadStaticLoopIns (ctx )
1802+ if err != nil {
1803+ return nil , err
1804+ }
1805+
1806+ return m .pickEasyAutoloopChannelWithStatic (
1807+ channels , restrictions , loopOut , loopIn , staticLoopIns ,
1808+ satsPerAsset ,
1809+ )
1810+ }
1811+
1812+ // pickEasyAutoloopChannelWithStatic picks an easy-autoloop channel using a
1813+ // shared static loop-in snapshot so callers can reuse one store load across
1814+ // budget and traffic checks within the same autoloop tick.
1815+ func (m * Manager ) pickEasyAutoloopChannelWithStatic (
1816+ channels []lndclient.ChannelInfo , restrictions * Restrictions ,
1817+ loopOut []* loopdb.LoopOut , loopIn []* loopdb.LoopIn ,
1818+ staticLoopIns []* StaticLoopInInfo ,
1819+ satsPerAsset float64 ) (* lndclient.ChannelInfo , error ) {
1820+
1821+ traffic := m .currentSwapTrafficWithStatic (
1822+ loopOut , loopIn , staticLoopIns ,
1823+ )
16591824
16601825 // Sort the candidate channels based on descending local balance. We
16611826 // want to prioritize picking a channel with the highest possible local
@@ -1722,13 +1887,13 @@ func (m *Manager) pickEasyAutoloopChannel(channels []lndclient.ChannelInfo,
17221887 "minimum is %v, skipping remaining channels" ,
17231888 channel .ChannelID , channel .LocalBalance ,
17241889 restrictions .Minimum )
1725- return nil
1890+ return nil , nil
17261891 }
17271892
1728- return & channel
1893+ return & channel , nil
17291894 }
17301895
1731- return nil
1896+ return nil , nil
17321897}
17331898
17341899func (m * Manager ) numActiveStickyLoops () int {
0 commit comments