< Summary - Combined Code Coverage

Information
Class: NLightning.Infrastructure.Repositories.Memory.UtxoMemoryRepository
Assembly: NLightning.Infrastructure.Repositories
File(s): /home/runner/work/NLightning/NLightning/src/NLightning.Infrastructure.Repositories/Memory/UtxoMemoryRepository.cs
Tag: 57_24045730253
Line coverage
0%
Covered lines: 0
Uncovered lines: 135
Coverable lines: 135
Total lines: 219
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 60
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/NLightning/NLightning/src/NLightning.Infrastructure.Repositories/Memory/UtxoMemoryRepository.cs

#LineLine coverage
 1using System.Collections.Concurrent;
 2using System.Diagnostics.CodeAnalysis;
 3
 4namespace NLightning.Infrastructure.Repositories.Memory;
 5
 6using Domain.Bitcoin.Interfaces;
 7using Domain.Bitcoin.ValueObjects;
 8using Domain.Bitcoin.Wallet.Models;
 9using Domain.Channels.ValueObjects;
 10using Domain.Money;
 11
 12public class UtxoMemoryRepository : IUtxoMemoryRepository
 13{
 014    private readonly ConcurrentDictionary<(TxId, uint), UtxoModel> _utxoSet = [];
 15
 16    public void Add(UtxoModel utxoModel)
 017    {
 018        if (!_utxoSet.TryAdd((utxoModel.TxId, utxoModel.Index), utxoModel))
 019            throw new InvalidOperationException("Cannot add Utxo");
 020    }
 21
 22    public void Spend(UtxoModel utxoModel)
 023    {
 024        _utxoSet.TryRemove((utxoModel.TxId, utxoModel.Index), out _);
 025    }
 26
 27    public bool TryGetUtxo(TxId txId, uint index, [MaybeNullWhen(false)] out UtxoModel utxoModel)
 028    {
 029        return _utxoSet.TryGetValue((txId, index), out utxoModel);
 030    }
 31
 32    public LightningMoney GetConfirmedBalance(uint currentBlockHeight)
 033    {
 034        return LightningMoney.Satoshis(_utxoSet.Values
 035                                               .Where(x => x.BlockHeight + 3 <= currentBlockHeight)
 036                                               .Sum(x => x.Amount.Satoshi));
 037    }
 38
 39    public LightningMoney GetUnconfirmedBalance(uint currentBlockHeight)
 040    {
 041        return LightningMoney.Satoshis(_utxoSet.Values
 042                                               .Where(x => x.BlockHeight + 3 > currentBlockHeight)
 043                                               .Sum(x => x.Amount.Satoshi));
 044    }
 45
 46    public LightningMoney GetLockedBalance()
 047    {
 048        return LightningMoney.Satoshis(_utxoSet.Values
 049                                               .Where(x => x.LockedToChannelId is not null)
 050                                               .Sum(x => x.Amount.Satoshi));
 051    }
 52
 53    public void Load(List<UtxoModel> utxoSet)
 054    {
 055        foreach (var utxoModel in utxoSet)
 056            _utxoSet.TryAdd((utxoModel.TxId, utxoModel.Index), utxoModel);
 057    }
 58
 59    public List<UtxoModel> LockUtxosToSpendOnChannel(LightningMoney requestFundingAmount, ChannelId channelId)
 060    {
 61        // Get available UTXOs (not already locked for other channels)
 062        var availableUtxos = _utxoSet.Values
 063                                     .Where(utxo => utxo.LockedToChannelId is null)
 064                                     .OrderByDescending(utxo => utxo.Amount.Satoshi)
 065                                     .ToList();
 66
 067        if (availableUtxos.Count == 0)
 068            throw new InvalidOperationException("No available UTXOs");
 69
 70        // Try Branch and Bound to find an exact match or minimize inputs
 071        var selectedUtxos = BranchAndBound(availableUtxos, requestFundingAmount);
 72
 073        if (selectedUtxos == null || selectedUtxos.Count == 0)
 074            throw new InvalidOperationException("Insufficient funds");
 75
 76        // Lock the selected UTXOs for this channel
 077        foreach (var selectedUtxo in selectedUtxos)
 078        {
 079            selectedUtxo.LockedToChannelId = channelId;
 080            _utxoSet[(selectedUtxo.TxId, selectedUtxo.Index)] = selectedUtxo;
 081        }
 82
 083        return selectedUtxos;
 084    }
 85
 86    public List<UtxoModel> GetLockedUtxosForChannel(ChannelId channelId)
 087    {
 088        return _utxoSet.Values.Where(x => x.LockedToChannelId.HasValue && x.LockedToChannelId.Value.Equals(channelId))
 089                       .ToList();
 090    }
 91
 92    public List<UtxoModel> ReturnUtxosNotSpentOnChannel(ChannelId channelId)
 093    {
 094        var utxos = _utxoSet.Values
 095                            .Where(x => x.LockedToChannelId.HasValue && x.LockedToChannelId.Value.Equals(channelId))
 096                            .ToList();
 097        foreach (var utxo in utxos)
 098        {
 099            utxo.LockedToChannelId = null;
 0100            _utxoSet[(utxo.TxId, utxo.Index)] = utxo;
 0101        }
 102
 0103        return utxos;
 0104    }
 105
 106    public void ConfirmSpendOnChannel(ChannelId channelId)
 0107    {
 0108        var utxos = _utxoSet.Values.Where(x => x.LockedToChannelId.HasValue &&
 0109                                               x.LockedToChannelId.Value.Equals(channelId));
 0110        foreach (var utxo in utxos)
 0111            _utxoSet.TryRemove((utxo.TxId, utxo.Index), out _);
 0112    }
 113
 114    public void UpgradeChannelIdOnLockedUtxos(ChannelId oldChannelId, ChannelId newChannelId)
 0115    {
 0116        var utxos = _utxoSet.Values
 0117                            .Where(x => x.LockedToChannelId.HasValue && x.LockedToChannelId.Value.Equals(oldChannelId))
 0118                            .ToList();
 119        // If there's no locked utxos, we have a problem
 0120        if (utxos.Count == 0)
 0121            throw new InvalidOperationException("No available UTXOs");
 122
 0123        foreach (var utxo in utxos)
 0124        {
 0125            utxo.LockedToChannelId = newChannelId;
 0126            _utxoSet[(utxo.TxId, utxo.Index)] = utxo;
 0127        }
 0128    }
 129
 130    private static List<UtxoModel>? BranchAndBound(List<UtxoModel> utxos, LightningMoney targetAmount)
 0131    {
 132        const int maxTries = 100_000;
 0133        var tries = 0;
 134
 135        // Best solution found so far
 0136        List<UtxoModel>? bestSelection = null;
 0137        var bestWaste = long.MaxValue;
 138
 139        // Current selection being explored
 0140        var targetSatoshis = targetAmount.Satoshi;
 141
 142        // Stack for depth-first search: (index, includeUtxo)
 0143        var stack = new Stack<(int index, bool include, List<UtxoModel> selection, long value)>();
 0144        stack.Push((0, true, [], 0));
 0145        stack.Push((0, false, [], 0));
 146
 0147        while (stack.Count > 0 && tries < maxTries)
 0148        {
 0149            tries++;
 0150            var (index, include, selection, value) = stack.Pop();
 151
 0152            if (include && index < utxos.Count)
 0153            {
 0154                selection = new List<UtxoModel>(selection) { utxos[index] };
 0155                value += utxos[index].Amount.Satoshi;
 0156            }
 157
 158            // Check if we found a valid solution
 0159            if (value >= targetSatoshis)
 0160            {
 0161                var waste = value - targetSatoshis;
 162
 163                // Perfect match (changeless transaction)
 0164                if (waste == 0)
 0165                    return selection;
 166
 167                // Better solution than the current best
 0168                if (waste < bestWaste ||
 0169                    (waste == bestWaste && selection.Count < (bestSelection?.Count ?? int.MaxValue)))
 0170                {
 0171                    bestSelection = new List<UtxoModel>(selection);
 0172                    bestWaste = waste;
 0173                }
 174
 0175                continue; // Prune this branch
 176            }
 177
 178            // Move to the next UTXO
 0179            var nextIndex = index + 1;
 0180            if (nextIndex >= utxos.Count)
 0181                continue;
 182
 183            // Calculate upper bound (current value + all remaining UTXOs)
 0184            var upperBound = value;
 0185            for (var i = nextIndex; i < utxos.Count; i++)
 0186                upperBound += utxos[i].Amount.Satoshi;
 187
 188            // Prune if we can't reach the target even with all remaining UTXOs
 0189            if (upperBound < targetSatoshis)
 0190                continue;
 191
 192            // Explore both branches: include and exclude the next UTXO
 0193            stack.Push((nextIndex, false, [.. selection], value));
 0194            stack.Push((nextIndex, true, [.. selection], value));
 0195        }
 196
 197        // If no exact match found, return the best solution or fallback to greedy
 198        // Fallback: simple greedy approach if BnB didn't find a solution
 0199        return bestSelection ?? GreedySelection(utxos, targetAmount);
 0200    }
 201
 202    private static List<UtxoModel>? GreedySelection(List<UtxoModel> utxos, LightningMoney targetAmount)
 0203    {
 0204        var selected = new List<UtxoModel>();
 0205        long currentSum = 0;
 0206        var targetSatoshis = targetAmount.Satoshi;
 207
 0208        foreach (var utxo in utxos)
 0209        {
 0210            selected.Add(utxo);
 0211            currentSum += utxo.Amount.Satoshi;
 212
 0213            if (currentSum >= targetSatoshis)
 0214                return selected;
 0215        }
 216
 0217        return null; // Insufficient funds
 0218    }
 219}

Methods/Properties

.ctor()
Add(NLightning.Domain.Bitcoin.Wallet.Models.UtxoModel)
Add(NLightning.Domain.Bitcoin.Wallet.Models.UtxoModel)
Spend(NLightning.Domain.Bitcoin.Wallet.Models.UtxoModel)
Spend(NLightning.Domain.Bitcoin.Wallet.Models.UtxoModel)
TryGetUtxo(NLightning.Domain.Bitcoin.ValueObjects.TxId,System.UInt32,NLightning.Domain.Bitcoin.Wallet.Models.UtxoModel&)
TryGetUtxo(NLightning.Domain.Bitcoin.ValueObjects.TxId,System.UInt32,NLightning.Domain.Bitcoin.Wallet.Models.UtxoModel&)
GetConfirmedBalance(System.UInt32)
GetConfirmedBalance(System.UInt32)
GetUnconfirmedBalance(System.UInt32)
GetUnconfirmedBalance(System.UInt32)
GetLockedBalance()
GetLockedBalance()
Load(System.Collections.Generic.List`1<NLightning.Domain.Bitcoin.Wallet.Models.UtxoModel>)
Load(System.Collections.Generic.List`1<NLightning.Domain.Bitcoin.Wallet.Models.UtxoModel>)
LockUtxosToSpendOnChannel(NLightning.Domain.Money.LightningMoney,NLightning.Domain.Channels.ValueObjects.ChannelId)
LockUtxosToSpendOnChannel(NLightning.Domain.Money.LightningMoney,NLightning.Domain.Channels.ValueObjects.ChannelId)
GetLockedUtxosForChannel(NLightning.Domain.Channels.ValueObjects.ChannelId)
GetLockedUtxosForChannel(NLightning.Domain.Channels.ValueObjects.ChannelId)
ReturnUtxosNotSpentOnChannel(NLightning.Domain.Channels.ValueObjects.ChannelId)
ReturnUtxosNotSpentOnChannel(NLightning.Domain.Channels.ValueObjects.ChannelId)
ConfirmSpendOnChannel(NLightning.Domain.Channels.ValueObjects.ChannelId)
ConfirmSpendOnChannel(NLightning.Domain.Channels.ValueObjects.ChannelId)
UpgradeChannelIdOnLockedUtxos(NLightning.Domain.Channels.ValueObjects.ChannelId,NLightning.Domain.Channels.ValueObjects.ChannelId)
UpgradeChannelIdOnLockedUtxos(NLightning.Domain.Channels.ValueObjects.ChannelId,NLightning.Domain.Channels.ValueObjects.ChannelId)
BranchAndBound(System.Collections.Generic.List`1<NLightning.Domain.Bitcoin.Wallet.Models.UtxoModel>,NLightning.Domain.Money.LightningMoney)
BranchAndBound(System.Collections.Generic.List`1<NLightning.Domain.Bitcoin.Wallet.Models.UtxoModel>,NLightning.Domain.Money.LightningMoney)
GreedySelection(System.Collections.Generic.List`1<NLightning.Domain.Bitcoin.Wallet.Models.UtxoModel>,NLightning.Domain.Money.LightningMoney)
GreedySelection(System.Collections.Generic.List`1<NLightning.Domain.Bitcoin.Wallet.Models.UtxoModel>,NLightning.Domain.Money.LightningMoney)