/// <summary> /// Checks if a transaction has valid UTXOs that are spent by it. /// </summary> /// <param name="transaction">The transaction to check.</param> /// <param name="coins">Returns the coins found if this parameter supplies an empty coin list.</param> /// <returns><c>True</c> if UTXO's are valid and <c>false</c> otherwise.</returns> private bool TransactionHasValidUTXOs(Transaction transaction, List <Coin> coins = null) { lock (this.lockObject) { // All the input UTXO's should be present in spending details of the multi-sig address. foreach (TxIn input in transaction.Inputs) { TransactionData transactionData = this.Wallet.MultiSigAddress.Transactions .Where(t => t.Id == input.PrevOut.Hash && t.Index == input.PrevOut.N && t.SpendingDetails?.TransactionId == transaction.GetHash()) .SingleOrDefault(); if (transactionData == null) { return(false); } coins?.Add(new Coin(transactionData.Id, (uint)transactionData.Index, transactionData.Amount, transactionData.ScriptPubKey)); } return(true); } }
/// <summary> /// Compares transaction data to determine the order of inclusion in the transaction. /// </summary> /// <param name="x">First transaction data.</param> /// <param name="y">Second transaction data.</param> /// <returns>Returns <c>0</c> if the outputs are the same and <c>-1<c> or <c>1</c> depending on whether the first or second output takes precedence.</returns> public static int CompareTransactionData(TransactionData x, TransactionData y) { // The oldest UTXO (determined by block height) is selected first. if ((x.BlockHeight ?? int.MaxValue) != (y.BlockHeight ?? int.MaxValue)) { return(((x.BlockHeight ?? int.MaxValue) < (y.BlockHeight ?? int.MaxValue)) ? -1 : 1); } // If a block has more than one UTXO, then they are selected in order of transaction id. if (x.Id != y.Id) { return((x.Id < y.Id) ? -1 : 1); } // If multiple UTXOs appear within a transaction then they are selected in ascending index order. if (x.Index != y.Index) { return((x.Index < y.Index) ? -1 : 1); } return(0); }
/// <summary> /// Mark an output as spent, the credit of the output will not be used to calculate the balance. /// The output will remain in the wallet for history (and reorg). /// </summary> /// <param name="transaction">The transaction from which details are added.</param> /// <param name="paidToOutputs">A list of payments made out</param> /// <param name="spendingTransactionId">The id of the transaction containing the output being spent, if this is a spending transaction.</param> /// <param name="spendingTransactionIndex">The index of the output in the transaction being referenced, if this is a spending transaction.</param> /// <param name="blockHeight">Height of the block.</param> /// <param name="blockHash">Hash of the block.</param> /// <param name="block">The block containing the transaction to add.</param> private void AddSpendingTransactionToWallet(Transaction transaction, IEnumerable <TxOut> paidToOutputs, uint256 spendingTransactionId, int?spendingTransactionIndex, int?blockHeight = null, uint256 blockHash = null, Block block = null) { Guard.NotNull(transaction, nameof(transaction)); Guard.NotNull(paidToOutputs, nameof(paidToOutputs)); Guard.Assert(blockHash == (blockHash ?? block?.GetHash())); // Get the transaction being spent. TransactionData spentTransaction = this.Wallet.MultiSigAddress.Transactions.SingleOrDefault(t => (t.Id == spendingTransactionId) && (t.Index == spendingTransactionIndex)); if (spentTransaction == null) { // Strange, why would it be null? this.logger.LogTrace("(-)[TX_NULL]"); return; } // If the details of this spending transaction are seen for the first time. if (spentTransaction.SpendingDetails == null) { this.logger.LogTrace("Spending UTXO '{0}-{1}' is new. BlockHeight={2}", spendingTransactionId, spendingTransactionIndex, blockHeight); List <PaymentDetails> payments = new List <PaymentDetails>(); foreach (TxOut paidToOutput in paidToOutputs) { // Figure out how to retrieve the destination address. string destinationAddress = string.Empty; ScriptTemplate scriptTemplate = paidToOutput.ScriptPubKey.FindTemplate(this.network); switch (scriptTemplate.Type) { // Pay to PubKey can be found in outputs of staking transactions. case TxOutType.TX_PUBKEY: PubKey pubKey = PayToPubkeyTemplate.Instance.ExtractScriptPubKeyParameters(paidToOutput.ScriptPubKey); destinationAddress = pubKey.GetAddress(this.network).ToString(); break; // Pay to PubKey hash is the regular, most common type of output. case TxOutType.TX_PUBKEYHASH: destinationAddress = paidToOutput.ScriptPubKey.GetDestinationAddress(this.network).ToString(); break; case TxOutType.TX_NONSTANDARD: case TxOutType.TX_SCRIPTHASH: destinationAddress = paidToOutput.ScriptPubKey.GetDestinationAddress(this.network).ToString(); break; case TxOutType.TX_MULTISIG: case TxOutType.TX_NULL_DATA: case TxOutType.TX_SEGWIT: break; } payments.Add(new PaymentDetails { DestinationScriptPubKey = paidToOutput.ScriptPubKey, DestinationAddress = destinationAddress, Amount = paidToOutput.Value }); } SpendingDetails spendingDetails = new SpendingDetails { TransactionId = transaction.GetHash(), Payments = payments, CreationTime = DateTimeOffset.FromUnixTimeSeconds(block?.Header.Time ?? transaction.Time), BlockHeight = blockHeight, BlockHash = blockHash, Hex = transaction.ToHex(), IsCoinStake = transaction.IsCoinStake == false ? (bool?)null : true }; spentTransaction.SpendingDetails = spendingDetails; spentTransaction.MerkleProof = null; } else // If this spending transaction is being confirmed in a block. { this.logger.LogTrace("Spending transaction ID '{0}' is being confirmed, updating. BlockHeight={1}", spendingTransactionId, blockHeight); // Update the block height. if (spentTransaction.SpendingDetails.BlockHeight == null && blockHeight != null) { spentTransaction.SpendingDetails.BlockHeight = blockHeight; spentTransaction.SpendingDetails.BlockHash = blockHash; } // Update the block time to be that of the block in which the transaction is confirmed. if (block != null) { spentTransaction.SpendingDetails.CreationTime = DateTimeOffset.FromUnixTimeSeconds(block.Header.Time); } } }
/// <summary> /// Adds a transaction that credits the wallet with new coins. /// This method is can be called many times for the same transaction (idempotent). /// </summary> /// <param name="transaction">The transaction from which details are added.</param> /// <param name="utxo">The unspent output to add to the wallet.</param> /// <param name="blockHeight">Height of the block.</param> /// <param name="blockHash">Hash of the block.</param> /// <param name="block">The block containing the transaction to add.</param> private void AddTransactionToWallet(Transaction transaction, TxOut utxo, int?blockHeight = null, uint256 blockHash = null, Block block = null) { Guard.NotNull(transaction, nameof(transaction)); Guard.NotNull(utxo, nameof(utxo)); Guard.Assert(blockHash == (blockHash ?? block?.GetHash())); uint256 transactionHash = transaction.GetHash(); // Get the collection of transactions to add to. Script script = utxo.ScriptPubKey; // Check if a similar UTXO exists or not (same transaction ID and same index). // New UTXOs are added, existing ones are updated. int index = transaction.Outputs.IndexOf(utxo); Money amount = utxo.Value; TransactionData foundTransaction = this.Wallet.MultiSigAddress.Transactions.FirstOrDefault(t => (t.Id == transactionHash) && (t.Index == index)); if (foundTransaction == null) { this.logger.LogTrace("UTXO '{0}-{1}' not found, creating. BlockHeight={2}, BlockHash={3}", transactionHash, index, blockHeight, blockHash); TransactionData newTransaction = new TransactionData { Amount = amount, BlockHeight = blockHeight, BlockHash = blockHash, Id = transactionHash, CreationTime = DateTimeOffset.FromUnixTimeSeconds(block?.Header.Time ?? transaction.Time), Index = index, ScriptPubKey = script, Hex = transaction.ToHex() }; // Add the Merkle proof to the (non-spending) transaction. if (block != null) { newTransaction.MerkleProof = new MerkleBlock(block, new[] { transactionHash }).PartialMerkleTree; } this.Wallet.MultiSigAddress.Transactions.Add(newTransaction); this.AddInputKeysLookupLock(newTransaction); } else { this.logger.LogTrace("Transaction ID '{0}-{1}' found, updating BlockHeight={2}, BlockHash={3}.", transactionHash, index, blockHeight, blockHash); // Update the block height and block hash. if ((foundTransaction.BlockHeight == null) && (blockHeight != null)) { foundTransaction.BlockHeight = blockHeight; foundTransaction.BlockHash = blockHash; } // Update the block time. if (block != null) { foundTransaction.CreationTime = DateTimeOffset.FromUnixTimeSeconds(block.Header.Time); } // Add the Merkle proof now that the transaction is confirmed in a block. if ((block != null) && (foundTransaction.MerkleProof == null)) { foundTransaction.MerkleProof = new MerkleBlock(block, new[] { transactionHash }).PartialMerkleTree; } } this.TransactionFoundInternal(script); }