< Summary - Combined Code Coverage

Information
Class: NLightning.Infrastructure.Bitcoin.Transactions.BaseTransaction
Assembly: NLightning.Infrastructure.Bitcoin
File(s): /home/runner/work/nlightning/nlightning/src/NLightning.Infrastructure.Bitcoin/Transactions/BaseTransaction.cs
Tag: 30_15166811759
Line coverage
73%
Covered lines: 111
Uncovered lines: 41
Coverable lines: 152
Total lines: 331
Line coverage: 73%
Branch coverage
61%
Covered branches: 43
Total branches: 70
Branch coverage: 61.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)50%11100%
get_Outputs()100%11100%
get_CalculatedFee()100%11100%
get_Finalized()100%11100%
get_FinalizedTransaction()50%22100%
get_TxId()100%11100%
get_IsValid()50%22100%
.ctor(...)100%22100%
SetLockTime(...)100%11100%
SignTransaction(...)33.33%6.84671.43%
CalculateAndCheckFees(...)0%620%
AppendRemoteSignatureToTransaction(...)100%11100%
SignTransactionWithExistingKeys()100%11100%
get_TotalInputAmount()100%11100%
get_TotalOutputAmount()100%11100%
CheckTransactionAmounts(...)100%22100%
CalculateOutputWeight()85.71%34.272880%
CalculateInputWeight()50%31.341660.87%
CalculateTransactionFee(...)100%11100%
AddCoin(...)100%210%
AddCoin(...)100%210%
AddOutput(...)100%11100%
AddOutputRange(...)0%4260%
ClearOutputsFromTransaction()100%210%
RemoveOutput(...)100%11100%
AddOrderedOutputsToTransaction()75%4.03487.5%

File(s)

/home/runner/work/nlightning/nlightning/src/NLightning.Infrastructure.Bitcoin/Transactions/BaseTransaction.cs

#LineLine coverage
 1using NBitcoin;
 2
 3namespace NLightning.Infrastructure.Bitcoin.Transactions;
 4
 5using Comparers;
 6using Domain.Money;
 7using Domain.Protocol.Constants;
 8using Outputs;
 9
 10public abstract class BaseTransaction
 11{
 12    #region Private Fields
 13    private readonly bool _hasAnchorOutput;
 14    private readonly TransactionBuilder _builder;
 17615    private readonly List<(Coin, Sequence)> _coins = [];
 16
 17    private Transaction _transaction;
 18    #endregion
 19
 20    #region Protected Properties
 262021    protected List<BaseOutput> Outputs { get; private set; } = [];
 66422    protected LightningMoney CalculatedFee { get; } = LightningMoney.Zero;
 93623    protected bool Finalized { get; private set; }
 12824    protected Transaction FinalizedTransaction => Finalized
 12825        ? _transaction
 12826        : throw new Exception("Transaction not finalized.");
 27    #endregion
 28
 29    #region Public Properties
 98830    public uint256 TxId { get; private set; } = uint256.Zero;
 10431    public bool IsValid => Finalized
 10432        ? _builder.Verify(_transaction)
 10433        : throw new Exception("Transaction not finalized.");
 34    #endregion
 35
 36    #region Constructors
 37
 3638    protected BaseTransaction(bool hasAnchorOutput, Network network, uint version, SigHash sigHash, params Coin[] coins)
 39    {
 3640        _hasAnchorOutput = hasAnchorOutput;
 41
 3642        _builder = network.CreateTransactionBuilder();
 3643        _builder.SetSigningOptions(sigHash, false);
 3644        _builder.DustPrevention = false;
 3645        _builder.SetVersion(version);
 46
 7247        _coins = coins.Select(c => (c, Sequence.Final)).ToList();
 48
 3649        _transaction = Transaction.Create(network);
 3650        _transaction.Version = version;
 7251        _transaction.Inputs.AddRange(_coins.Select(c => new TxIn(c.Item1.Outpoint)));
 3652    }
 53
 14054    protected BaseTransaction(bool hasAnchorOutput, Network network, uint version, SigHash sigHash,
 14055                              params (Coin, Sequence)[] coins)
 56    {
 14057        _hasAnchorOutput = hasAnchorOutput;
 58
 14059        _builder = network.CreateTransactionBuilder();
 14060        _builder.SetSigningOptions(sigHash, false);
 14061        _builder.DustPrevention = false;
 14062        _builder.SetVersion(version);
 63
 14064        _coins.AddRange(coins);
 65
 14066        _transaction = Transaction.Create(network);
 14067        _transaction.Version = version;
 56068        foreach (var (coin, sequence) in _coins)
 69        {
 14070            _transaction.Inputs.Add(coin.Outpoint, null, null, sequence);
 71        }
 72
 14073    }
 74    #endregion
 75
 76    #region Abstract Methods
 77    internal abstract void ConstructTransaction(LightningMoney currentFeePerKw);
 78    #endregion
 79
 80    #region Protected Methods
 81    protected void SetLockTime(LockTime lockTime)
 82    {
 13683        _transaction.LockTime = lockTime;
 13684    }
 85
 86    protected void SignTransaction(params BitcoinSecret[] secrets)
 87    {
 13288        ArgumentNullException.ThrowIfNull(secrets);
 89
 90        // Check if the output amount is greater than the input amount
 13291        if (!CheckTransactionAmounts())
 092            throw new InvalidOperationException("Output amount cannot exceed input amount.");
 93
 94        // Sign all inputs
 13295        ArgumentNullException.ThrowIfNull(secrets);
 96
 13297        if (Finalized)
 98        {
 99            // Remove signature from inputs
 0100            _transaction.Inputs.Clear();
 0101            foreach (var (coin, sequence) in _coins)
 102            {
 0103                _transaction.Inputs.Add(coin.Outpoint, null, null, sequence);
 104            }
 105        }
 106        else
 107        {
 108            // Add our keys
 264109            _builder.AddKeys(secrets.Select(ISecret (s) => s).ToArray());
 264110            _builder.AddCoins(_coins.Select(c => c.Item1));
 111        }
 112
 132113        _transaction = _builder.SignTransactionInPlace(_transaction);
 114
 132115        TxId = _transaction.GetHash();
 132116        Finalized = true;
 132117    }
 118
 119    protected void CalculateAndCheckFees(LightningMoney currentFeePerKw)
 120    {
 121        // Calculate transaction fee
 0122        CalculateTransactionFee(currentFeePerKw);
 123
 124        // Check if the output amount plus fees is greater than the input amount
 0125        if (!CheckTransactionAmounts(CalculatedFee))
 0126            throw new InvalidOperationException("Output amount cannot exceed input amount.");
 0127    }
 128
 129    protected void AppendRemoteSignatureToTransaction(ITransactionSignature remoteSignature, PubKey remotePubKey)
 130    {
 104131        _builder.AddKnownSignature(remotePubKey, remoteSignature, _transaction.Inputs[0].PrevOut);
 104132    }
 133
 134    protected void SignTransactionWithExistingKeys()
 135    {
 104136        _transaction = _builder.SignTransactionInPlace(_transaction);
 137
 104138        TxId = _transaction.GetHash();
 104139        Finalized = true;
 104140    }
 141
 296142    protected LightningMoney TotalInputAmount => _coins.Sum(c => (LightningMoney)c.Item1.Amount);
 143
 672144    protected LightningMoney TotalOutputAmount => Outputs.Sum(o => o.Amount);
 145
 146    protected bool CheckTransactionAmounts(LightningMoney? fees = null)
 147    {
 148        // Check if the output amount is greater than the input amount
 132149        return TotalOutputAmount + (fees ?? LightningMoney.Zero) <= TotalInputAmount;
 150    }
 151
 152    protected int CalculateOutputWeight()
 153    {
 132154        var outputWeight = WeightConstants.TRANSACTION_BASE_WEIGHT;
 132155        if (_hasAnchorOutput)
 156        {
 40157            outputWeight += 8; // Add 8 more bytes for (count_tx_out * 4)
 158        }
 159
 1352160        foreach (var output in Outputs)
 161        {
 544162            switch (output)
 163            {
 164                case FundingOutput:
 16165                    outputWeight += WeightConstants.P2WSH_OUTPUT_WEIGHT;
 16166                    break;
 16167                case ChangeOutput changeOutput when changeOutput.ScriptPubKey.IsScriptType(ScriptType.P2PKH):
 0168                    outputWeight += WeightConstants.P2PKH_OUTPUT_WEIGHT;
 0169                    break;
 16170                case ChangeOutput changeOutput when changeOutput.ScriptPubKey.IsScriptType(ScriptType.P2SH):
 0171                    outputWeight += WeightConstants.P2SH_OUTPUT_WEIGHT;
 0172                    break;
 16173                case ChangeOutput changeOutput when changeOutput.ScriptPubKey.IsScriptType(ScriptType.P2WPKH):
 4174                    outputWeight += WeightConstants.P2WPKH_OUTPUT_WEIGHT;
 4175                    break;
 12176                case ChangeOutput changeOutput when changeOutput.ScriptPubKey.IsScriptType(ScriptType.P2WSH):
 12177                    outputWeight += WeightConstants.P2WSH_OUTPUT_WEIGHT;
 12178                    break;
 179                case ChangeOutput changeOutput:
 0180                    outputWeight += changeOutput.ScriptPubKey.Length;
 0181                    break;
 182                case ToLocalOutput:
 116183                case ToRemoteOutput when _hasAnchorOutput:
 156184                    outputWeight += WeightConstants.P2WSH_OUTPUT_WEIGHT;
 156185                    break;
 186                case ToRemoteOutput:
 76187                    outputWeight += WeightConstants.P2WPKH_OUTPUT_WEIGHT;
 76188                    break;
 189                case ToAnchorOutput:
 80190                    outputWeight += WeightConstants.ANCHOR_OUTPUT_WEIGHT;
 80191                    break;
 192                case OfferedHtlcOutput:
 193                case ReceivedHtlcOutput:
 200194                    outputWeight += WeightConstants.HTLC_OUTPUT_WEIGHT;
 195                    break;
 196            }
 197        }
 198
 132199        return outputWeight;
 200    }
 201
 202    protected int CalculateInputWeight()
 203    {
 16204        var inputWeight = 0;
 16205        var mustAddWitnessHeader = false;
 206
 64207        foreach (var (coin, _) in _coins)
 208        {
 32209            var input = _transaction.Inputs.SingleOrDefault(i => i.PrevOut == coin.Outpoint)
 16210                        ?? throw new NullReferenceException("Input not found in transaction.");
 211
 16212            if (input.WitScript.PushCount > 0)
 213            {
 0214                mustAddWitnessHeader = true;
 215            }
 216
 16217            if (coin.ScriptPubKey.IsScriptType(ScriptType.P2PKH))
 218            {
 4219                inputWeight += 4 * Math.Max(WeightConstants.P2PKH_INTPUT_WEIGHT, input.ToBytes().Length);
 220            }
 12221            else if (coin.ScriptPubKey.IsScriptType(ScriptType.P2SH))
 222            {
 0223                inputWeight += 4 * Math.Max(WeightConstants.P2SH_INTPUT_WEIGHT, input.ToBytes().Length);
 0224                inputWeight += input.WitScript.ToBytes().Length;
 225            }
 12226            else if (coin.ScriptPubKey.IsScriptType(ScriptType.P2WPKH))
 227            {
 12228                inputWeight += 4 * Math.Max(WeightConstants.P2WPKH_INTPUT_WEIGHT, input.ToBytes().Length);
 12229                inputWeight += input.WitScript.ToBytes().Length;
 230            }
 0231            else if (coin.ScriptPubKey.IsScriptType(ScriptType.P2WSH))
 232            {
 0233                inputWeight += 4 * Math.Max(WeightConstants.P2WSH_INTPUT_WEIGHT, input.ToBytes().Length);
 0234                inputWeight += Math.Max(WeightConstants.MULTISIG_WITNESS_WEIGHT, input.WitScript.ToBytes().Length);
 235            }
 236            else
 237            {
 0238                inputWeight += 4 * Math.Max(WeightConstants.P2UNKOWN_S_INTPUT_WEIGHT, input.ToBytes().Length);
 0239                inputWeight += input.WitScript.ToBytes().Length;
 240            }
 241        }
 242
 16243        if (mustAddWitnessHeader)
 244        {
 0245            inputWeight += WeightConstants.WITNESS_HEADER;
 246        }
 247
 16248        return inputWeight;
 249    }
 250
 251    protected void CalculateTransactionFee(LightningMoney currentFeePerKw)
 252    {
 16253        var outputWeight = CalculateOutputWeight();
 16254        var inputWeight = CalculateInputWeight();
 255
 16256        CalculatedFee.Satoshi = (outputWeight + inputWeight) * currentFeePerKw.Satoshi / 1000L;
 16257    }
 258
 259    #region Input Management
 260    protected void AddCoin(Coin coin, Sequence sequence)
 261    {
 0262        ArgumentNullException.ThrowIfNull(coin);
 263
 0264        _transaction.Inputs.Add(coin.Outpoint, null, null, sequence);
 0265    }
 266    protected void AddCoin(Coin coin)
 267    {
 0268        ArgumentNullException.ThrowIfNull(coin);
 269
 0270        _coins.Add((coin, Sequence.Final));
 0271        _transaction.Inputs.Add(coin.Outpoint, null, null, Sequence.Final);
 0272    }
 273    #endregion
 274
 275    #region Output Management
 276    protected void AddOutput(BaseOutput baseOutput)
 277    {
 852278        ArgumentNullException.ThrowIfNull(baseOutput);
 279
 852280        Outputs.Add(baseOutput);
 852281    }
 282
 283    protected void AddOutputRange(IEnumerable<BaseOutput> outputs)
 284    {
 0285        ArgumentNullException.ThrowIfNull(outputs);
 286
 0287        var outputBases = outputs as BaseOutput[] ?? outputs.ToArray();
 0288        if (outputBases.Length == 0)
 0289            return;
 290
 0291        foreach (var output in outputBases)
 292        {
 0293            ArgumentNullException.ThrowIfNull(output);
 0294            Outputs.Add(output);
 295        }
 0296    }
 297
 298    protected void ClearOutputsFromTransaction()
 299    {
 0300        _transaction.Outputs.Clear();
 0301    }
 302
 303    protected void RemoveOutput(BaseOutput? baseOutput)
 304    {
 256305        ArgumentNullException.ThrowIfNull(baseOutput);
 306
 256307        Outputs.Remove(baseOutput);
 256308    }
 309
 310    protected void AddOrderedOutputsToTransaction()
 311    {
 312        // Clear TxOuts
 132313        _transaction.Outputs.Clear();
 314
 132315        switch (Outputs.Count)
 316        {
 317            case 0:
 0318                return;
 319            case 1:
 20320                _transaction.Outputs.Add(Outputs[0].ToTxOut());
 20321                break;
 322            default:
 323                // Add ordered outputs
 600324                Outputs = Outputs.OrderBy(o => o, TransactionOutputComparer.Instance).ToList();
 600325                _transaction.Outputs.AddRange(Outputs.Select(o => o.ToTxOut()));
 326                break;
 327        }
 112328    }
 329    #endregion
 330    #endregion
 331}