Exemple #1
0
 public void AddOrUpdate(SmartTransaction tx)
 {
     lock (Lock)
     {
         AddOrUpdateNoLock(tx);
     }
 }
Exemple #2
0
        public bool TryUpdate(SmartTransaction tx)
        {
            var hash = tx.GetHash();

            lock (Lock)
            {
                // Do Contains first, because it's fast.
                if (ConfirmedStore.TryUpdate(tx))
                {
                    return(true);
                }
                else if (tx.Confirmed && MempoolStore.TryRemove(hash, out SmartTransaction originalTx))
                {
                    originalTx.TryUpdate(tx);
                    ConfirmedStore.TryAddOrUpdate(originalTx);
                    return(true);
                }
                else if (MempoolStore.TryUpdate(tx))
                {
                    return(true);
                }
            }

            return(false);
        }
Exemple #3
0
 public bool TryGetTransaction(uint256 hash, out SmartTransaction sameStx)
 {
     lock (TransactionsLock)
     {
         return(Transactions.TryGetValue(hash, out sameStx));
     }
 }
        private bool TryUpdateNoLockNoSerialization(SmartTransaction tx)
        {
            var hash = tx.GetHash();

            if (Transactions.TryGetValue(hash, out var found))
            {
                return(found.TryUpdate(tx));
            }

            return(false);
        }
Exemple #5
0
        public virtual bool TryGetTransaction(uint256 hash, out SmartTransaction sameStx)
        {
            lock (Lock)
            {
                if (MempoolStore.TryGetTransaction(hash, out sameStx))
                {
                    return(true);
                }

                return(ConfirmedStore.TryGetTransaction(hash, out sameStx));
            }
        }
        private async Task InitializeTransactionsNoLockAsync(CancellationToken cancel)
        {
            try
            {
                IoHelpers.EnsureFileExists(TransactionsFileManager.FilePath);
                cancel.ThrowIfCancellationRequested();

                var allLines = await TransactionsFileManager.ReadAllLinesAsync(cancel).ConfigureAwait(false);

                var allTransactions = allLines
                                      .Select(x => SmartTransaction.FromLine(x, Network))
                                      .OrderByBlockchain();

                var added   = false;
                var updated = false;
                lock (TransactionsLock)
                {
                    foreach (var tx in allTransactions)
                    {
                        var(isAdded, isUpdated) = TryAddOrUpdateNoLockNoSerialization(tx);
                        if (isAdded)
                        {
                            added = true;
                        }
                        if (isUpdated)
                        {
                            updated = true;
                        }
                    }
                }

                if (added || updated)
                {
                    cancel.ThrowIfCancellationRequested();

                    // Another process worked into the file and appended the same transaction into it.
                    // In this case we correct the file by serializing the unique set.
                    await SerializeAllTransactionsNoLockAsync().ConfigureAwait(false);
                }
            }
            catch (Exception ex) when(ex is not OperationCanceledException)
            {
                // We found a corrupted entry. Stop here.
                // Delete the currupted file.
                // Do not try to autocorrect, because the internal data structures are throwing events that may confuse the consumers of those events.
                Logger.LogError($"{TransactionsFileManager.FileNameWithoutExtension} file got corrupted. Deleting it...");
                TransactionsFileManager.DeleteMe();
                throw;
            }
        }
        public bool TryUpdate(SmartTransaction tx)
        {
            bool ret;

            lock (TransactionsLock)
            {
                ret = TryUpdateNoLockNoSerialization(tx);
            }

            if (ret)
            {
                AbandonedTasks.AddAndClearCompleted(TryUpdateFileAsync(tx));
            }

            return(ret);
        }
Exemple #8
0
        public bool TryRemove(uint256 hash, out SmartTransaction stx)
        {
            bool isRemoved;

            lock (TransactionsLock)
            {
                isRemoved = Transactions.Remove(hash, out stx);
            }

            if (isRemoved)
            {
                _ = TryRemoveFromFileAsync(hash);
            }

            return(isRemoved);
        }
Exemple #9
0
        public bool TryUpdate(SmartTransaction tx)
        {
            bool ret;

            lock (TransactionsLock)
            {
                ret = TryUpdateNoLockNoSerialization(tx);
            }

            if (ret)
            {
                _ = TryUpdateFileAsync(tx);
            }

            return(ret);
        }
Exemple #10
0
        public bool TryRemove(uint256 hash, out SmartTransaction stx)
        {
            bool isRemoved;

            lock (TransactionsLock)
            {
                isRemoved = Transactions.Remove(hash, out stx);
            }

            if (isRemoved)
            {
                AbandonedTasks.AddAndClearCompleted(TryRemoveFromFileAsync(hash));
            }

            return(isRemoved);
        }
        private (bool isAdded, bool isUpdated) TryAddOrUpdateNoLockNoSerialization(SmartTransaction tx)
        {
            var hash = tx.GetHash();

            if (Transactions.TryAdd(hash, tx))
            {
                return(true, false);
            }
            else
            {
                if (Transactions[hash].TryUpdate(tx))
                {
                    return(false, true);
                }
                else
                {
                    return(false, false);
                }
            }
        }
Exemple #12
0
        public (bool isAdded, bool isUpdated) TryAddOrUpdate(SmartTransaction tx)
        {
            (bool isAdded, bool isUpdated)ret;

            lock (TransactionsLock)
            {
                ret = TryAddOrUpdateNoLockNoSerialization(tx);
            }

            if (ret.isAdded)
            {
                _ = TryAppendToFileAsync(tx);
            }

            if (ret.isUpdated)
            {
                _ = TryUpdateFileAsync(tx);
            }

            return(ret);
        }
        public (bool isAdded, bool isUpdated) TryAddOrUpdate(SmartTransaction tx)
        {
            (bool isAdded, bool isUpdated)ret;

            lock (TransactionsLock)
            {
                ret = TryAddOrUpdateNoLockNoSerialization(tx);
            }

            if (ret.isAdded)
            {
                AbandonedTasks.AddAndClearCompleted(TryAppendToFileAsync(tx));
            }

            if (ret.isUpdated)
            {
                AbandonedTasks.AddAndClearCompleted(TryUpdateFileAsync(tx));
            }

            return(ret);
        }
Exemple #14
0
        private void AddOrUpdateNoLock(SmartTransaction tx)
        {
            var hash = tx.GetHash();

            if (tx.Confirmed)
            {
                if (MempoolStore.TryRemove(hash, out SmartTransaction found))
                {
                    found.TryUpdate(tx);
                    ConfirmedStore.TryAddOrUpdate(found);
                }
                else
                {
                    ConfirmedStore.TryAddOrUpdate(tx);
                }
            }
            else
            {
                if (!ConfirmedStore.TryUpdate(tx))
                {
                    MempoolStore.TryAddOrUpdate(tx);
                }
            }
        }
        private async Task TryCommitToFileAsync(ITxStoreOperation operation)
        {
            try
            {
                if (operation is null || operation.IsEmpty)
                {
                    return;
                }

                // Make sure that only one call can continue.
                lock (OperationsLock)
                {
                    var isRunning = Operations.Any();
                    Operations.Add(operation);
                    if (isRunning)
                    {
                        return;
                    }
                }

                // Wait until the operation list calms down.
                IEnumerable <ITxStoreOperation> operationsToExecute;
                while (true)
                {
                    var count = Operations.Count;

                    await Task.Delay(100).ConfigureAwait(false);

                    lock (OperationsLock)
                    {
                        if (count == Operations.Count)
                        {
                            // Merge operations.
                            operationsToExecute = OperationMerger.Merge(Operations).ToList();
                            Operations.Clear();
                            break;
                        }
                    }
                }

                using (await TransactionsFileAsyncLock.LockAsync().ConfigureAwait(false))
                {
                    foreach (ITxStoreOperation op in operationsToExecute)
                    {
                        if (op is Append appendOperation)
                        {
                            var toAppends = appendOperation.Transactions;

                            try
                            {
                                await TransactionsFileManager.AppendAllLinesAsync(toAppends.ToBlockchainOrderedLines()).ConfigureAwait(false);
                            }
                            catch
                            {
                                await SerializeAllTransactionsNoLockAsync().ConfigureAwait(false);
                            }
                        }
                        else if (op is Remove removeOperation)
                        {
                            var toRemoves = removeOperation.Transactions;

                            string[] allLines = await TransactionsFileManager.ReadAllLinesAsync().ConfigureAwait(false);

                            var toSerialize = new List <string>();
                            foreach (var line in allLines)
                            {
                                var startsWith = false;
                                foreach (var toRemoveString in toRemoves.Select(x => x.ToString()))
                                {
                                    startsWith = startsWith || line.StartsWith(toRemoveString, StringComparison.Ordinal);
                                }

                                if (!startsWith)
                                {
                                    toSerialize.Add(line);
                                }
                            }

                            try
                            {
                                await TransactionsFileManager.WriteAllLinesAsync(toSerialize).ConfigureAwait(false);
                            }
                            catch
                            {
                                await SerializeAllTransactionsNoLockAsync().ConfigureAwait(false);
                            }
                        }
                        else if (op is Update updateOperation)
                        {
                            var toUpdates = updateOperation.Transactions;

                            string[] allLines = await TransactionsFileManager.ReadAllLinesAsync().ConfigureAwait(false);

                            IEnumerable <SmartTransaction> allTransactions = allLines.Select(x => SmartTransaction.FromLine(x, Network));
                            var toSerialize = new List <SmartTransaction>();

                            foreach (SmartTransaction tx in allTransactions)
                            {
                                var txsToUpdateWith = toUpdates.Where(x => x == tx);
                                foreach (var txToUpdateWith in txsToUpdateWith)
                                {
                                    tx.TryUpdate(txToUpdateWith);
                                }
                                toSerialize.Add(tx);
                            }

                            try
                            {
                                await TransactionsFileManager.WriteAllLinesAsync(toSerialize.ToBlockchainOrderedLines()).ConfigureAwait(false);
                            }
                            catch
                            {
                                await SerializeAllTransactionsNoLockAsync().ConfigureAwait(false);
                            }
                        }
                        else
                        {
                            throw new NotSupportedException();
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Logger.LogError(ex);
            }
        }
Exemple #16
0
        /// <exception cref="ArgumentException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="ArgumentOutOfRangeException"></exception>
        public BuildTransactionResult BuildTransaction(
            PaymentIntent payments,
            Func <FeeRate> feeRateFetcher,
            IEnumerable <OutPoint>?allowedInputs = null,
            Func <LockTime>?lockTimeSelector     = null,
            IPayjoinClient?payjoinClient         = null)
        {
            lockTimeSelector ??= () => LockTime.Zero;

            long totalAmount = payments.TotalAmount.Satoshi;

            if (totalAmount is < 0 or > Constants.MaximumNumberOfSatoshis)
            {
                throw new ArgumentOutOfRangeException($"{nameof(payments)}.{nameof(payments.TotalAmount)} sum cannot be smaller than 0 or greater than {Constants.MaximumNumberOfSatoshis}.");
            }

            // Get allowed coins to spend.
            var availableCoinsView = Coins.Unspent();
            List <SmartCoin> allowedSmartCoinInputs = AllowUnconfirmed            // Inputs that can be used to build the transaction.
                                        ? availableCoinsView.ToList()
                                        : availableCoinsView.Confirmed().ToList();

            if (allowedInputs is not null)             // If allowedInputs are specified then select the coins from them.
            {
                if (!allowedInputs.Any())
                {
                    throw new ArgumentException($"{nameof(allowedInputs)} is not null, but empty.");
                }

                allowedSmartCoinInputs = allowedSmartCoinInputs
                                         .Where(x => allowedInputs.Any(y => y.Hash == x.TransactionId && y.N == x.Index))
                                         .ToList();

                // Add those that have the same script, because common ownership is already exposed.
                // But only if the user didn't click the "max" button. In this case he'd send more money than what he'd think.
                if (payments.ChangeStrategy != ChangeStrategy.AllRemainingCustom)
                {
                    var allScripts = allowedSmartCoinInputs.Select(x => x.ScriptPubKey).ToHashSet();
                    foreach (var coin in availableCoinsView.Where(x => !allowedSmartCoinInputs.Any(y => x.TransactionId == y.TransactionId && x.Index == y.Index)))
                    {
                        if (!(AllowUnconfirmed || coin.Confirmed))
                        {
                            continue;
                        }

                        if (allScripts.Contains(coin.ScriptPubKey))
                        {
                            allowedSmartCoinInputs.Add(coin);
                        }
                    }
                }
            }

            // Get and calculate fee
            Logger.LogInfo("Calculating dynamic transaction fee...");

            TransactionBuilder builder = Network.CreateTransactionBuilder();

            builder.SetCoinSelector(new SmartCoinSelector(allowedSmartCoinInputs));
            builder.AddCoins(allowedSmartCoinInputs.Select(c => c.Coin));
            builder.SetLockTime(lockTimeSelector());

            foreach (var request in payments.Requests.Where(x => x.Amount.Type == MoneyRequestType.Value))
            {
                var amountRequest = request.Amount;

                builder.Send(request.Destination, amountRequest.Amount);
                if (amountRequest.SubtractFee)
                {
                    builder.SubtractFees();
                }
            }

            HdPubKey?changeHdPubKey;

            if (payments.TryGetCustomRequest(out DestinationRequest? custChange))
            {
                var changeScript = custChange.Destination.ScriptPubKey;
                KeyManager.TryGetKeyForScriptPubKey(changeScript, out HdPubKey? hdPubKey);
                changeHdPubKey = hdPubKey;

                var changeStrategy = payments.ChangeStrategy;
                if (changeStrategy == ChangeStrategy.Custom)
                {
                    builder.SetChange(changeScript);
                }
                else if (changeStrategy == ChangeStrategy.AllRemainingCustom)
                {
                    builder.SendAllRemaining(changeScript);
                }
                else
                {
                    throw new NotSupportedException(payments.ChangeStrategy.ToString());
                }
            }
            else
            {
                KeyManager.AssertCleanKeysIndexed(isInternal: true);
                KeyManager.AssertLockedInternalKeysIndexed(14);
                changeHdPubKey = KeyManager.GetKeys(KeyState.Clean, true).First();

                builder.SetChange(changeHdPubKey.P2wpkhScript);
            }

            builder.OptInRBF = true;

            FeeRate feeRate = feeRateFetcher();

            builder.SendEstimatedFees(feeRate);

            var psbt = builder.BuildPSBT(false);

            var spentCoins = psbt.Inputs.Select(txin => allowedSmartCoinInputs.First(y => y.OutPoint == txin.PrevOut)).ToArray();

            var realToSend = payments.Requests
                             .Select(t =>
                                     (label: t.Label,
                                      destination: t.Destination,
                                      amount: psbt.Outputs.FirstOrDefault(o => o.ScriptPubKey == t.Destination.ScriptPubKey)?.Value))
                             .Where(i => i.amount is not null);

            if (!psbt.TryGetFee(out var fee))
            {
                throw new InvalidOperationException("Impossible to get the fees of the PSBT, this should never happen.");
            }
            Logger.LogInfo($"Fee: {fee.Satoshi} Satoshi.");

            var vSize = builder.EstimateSize(psbt.GetOriginalTransaction(), true);

            Logger.LogInfo($"Estimated tx size: {vSize} vBytes.");

            // Do some checks
            Money totalSendAmountNoFee = realToSend.Sum(x => x.amount);

            if (totalSendAmountNoFee == Money.Zero)
            {
                throw new InvalidOperationException("The amount after subtracting the fee is too small to be sent.");
            }

            Money totalOutgoingAmountNoFee;

            if (changeHdPubKey is null)
            {
                totalOutgoingAmountNoFee = totalSendAmountNoFee;
            }
            else
            {
                totalOutgoingAmountNoFee = realToSend.Where(x => !changeHdPubKey.ContainsScript(x.destination.ScriptPubKey)).Sum(x => x.amount);
            }
            decimal totalOutgoingAmountNoFeeDecimal = totalOutgoingAmountNoFee.ToDecimal(MoneyUnit.BTC);

            // Cannot divide by zero, so use the closest number we have to zero.
            decimal totalOutgoingAmountNoFeeDecimalDivisor = totalOutgoingAmountNoFeeDecimal == 0 ? decimal.MinValue : totalOutgoingAmountNoFeeDecimal;
            decimal feePc = 100 * fee.ToDecimal(MoneyUnit.BTC) / totalOutgoingAmountNoFeeDecimalDivisor;

            if (feePc > 1)
            {
                Logger.LogInfo($"The transaction fee is {feePc:0.#}% of the sent amount.{Environment.NewLine}"
                               + $"Sending:\t {totalOutgoingAmountNoFee.ToString(fplus: false, trimExcessZero: true)} BTC.{Environment.NewLine}"
                               + $"Fee:\t\t {fee.Satoshi} Satoshi.");
            }
            if (feePc > 100)
            {
                throw new InvalidOperationException($"The transaction fee is more than the sent amount: {feePc:0.#}%.");
            }

            if (spentCoins.Any(u => !u.Confirmed))
            {
                Logger.LogInfo("Unconfirmed transaction is spent.");
            }

            // Build the transaction
            Logger.LogInfo("Signing transaction...");

            // It must be watch only, too, because if we have the key and also hardware wallet, we do not care we can sign.
            psbt.AddKeyPaths(KeyManager);
            psbt.AddPrevTxs(TransactionStore);

            Transaction tx;

            if (KeyManager.IsWatchOnly)
            {
                tx = psbt.GetGlobalTransaction();
            }
            else
            {
                IEnumerable <ExtKey> signingKeys = KeyManager.GetSecrets(Password, spentCoins.Select(x => x.ScriptPubKey).ToArray());
                builder = builder.AddKeys(signingKeys.ToArray());
                builder.SignPSBT(psbt);

                var isPayjoin = false;

                // Try to pay using payjoin
                if (payjoinClient is not null)
                {
                    psbt      = TryNegotiatePayjoin(payjoinClient, builder, psbt, changeHdPubKey);
                    isPayjoin = true;
                    psbt.AddKeyPaths(KeyManager);
                    psbt.AddPrevTxs(TransactionStore);
                }

                psbt.Finalize();
                tx = psbt.ExtractTransaction();

                var checkResults = builder.Check(tx).ToList();
                if (!psbt.TryGetEstimatedFeeRate(out var actualFeeRate))
                {
                    throw new InvalidOperationException("Impossible to get the fee rate of the PSBT, this should never happen.");
                }

                if (!isPayjoin)
                {
                    // Manually check the feerate, because some inaccuracy is possible.
                    var sb1 = feeRate.SatoshiPerByte;
                    var sb2 = actualFeeRate.SatoshiPerByte;
                    if (Math.Abs(sb1 - sb2) > 2)                     // 2s/b inaccuracy ok.
                    {
                        // So it'll generate a transactionpolicy error thrown below.
                        checkResults.Add(new NotEnoughFundsPolicyError("Fees different than expected"));
                    }
                }
                if (checkResults.Count > 0)
                {
                    throw new InvalidTxException(tx, checkResults);
                }
            }

            var smartTransaction = new SmartTransaction(tx, Height.Unknown, label: SmartLabel.Merge(payments.Requests.Select(x => x.Label)));

            foreach (var coin in spentCoins)
            {
                smartTransaction.WalletInputs.Add(coin);
            }
            var label = SmartLabel.Merge(payments.Requests.Select(x => x.Label).Concat(smartTransaction.WalletInputs.Select(x => x.HdPubKey.Label)));

            for (var i = 0U; i < tx.Outputs.Count; i++)
            {
                TxOut output = tx.Outputs[i];
                if (KeyManager.TryGetKeyForScriptPubKey(output.ScriptPubKey, out HdPubKey? foundKey))
                {
                    var smartCoin = new SmartCoin(smartTransaction, i, foundKey);
                    label = SmartLabel.Merge(label, smartCoin.HdPubKey.Label);                     // foundKey's label is already added to the coinlabel.
                    smartTransaction.WalletOutputs.Add(smartCoin);
                }
            }

            foreach (var coin in smartTransaction.WalletOutputs)
            {
                var foundPaymentRequest = payments.Requests.FirstOrDefault(x => x.Destination.ScriptPubKey == coin.ScriptPubKey);

                // If change then we concatenate all the labels.
                // The foundkeylabel has already been added previously, so no need to concatenate.
                if (foundPaymentRequest is null)                 // Then it's autochange.
                {
                    coin.HdPubKey.SetLabel(label);
                }
                else
                {
                    coin.HdPubKey.SetLabel(SmartLabel.Merge(coin.HdPubKey.Label, foundPaymentRequest.Label));
                }
            }

            Logger.LogInfo($"Transaction is successfully built: {tx.GetHash()}.");
            var sign = !KeyManager.IsWatchOnly;

            return(new BuildTransactionResult(smartTransaction, psbt, sign, fee, feePc));
        }
Exemple #17
0
        public bool Process(SmartTransaction tx)
        {
            if (!tx.Transaction.PossiblyP2WPKHInvolved())
            {
                return(false);                // We do not care about non-witness transactions for other than mempool cleanup.
            }

            uint256 txId           = tx.GetHash();
            var     walletRelevant = false;

            if (tx.Confirmed)
            {
                foreach (var coin in Coins.AsAllCoinsView().CreatedBy(txId))
                {
                    coin.Height    = tx.Height;
                    walletRelevant = true;                     // relevant
                }

                if (walletRelevant)
                {
                    TransactionStore.AddOrUpdate(tx);
                }
            }

            if (!tx.Transaction.IsCoinBase && !walletRelevant)             // Transactions we already have and processed would be "double spends" but they shouldn't.
            {
                var doubleSpends = new List <SmartCoin>();
                foreach (SmartCoin coin in Coins.AsAllCoinsView())
                {
                    var spent = false;
                    foreach (TxoRef spentOutput in coin.SpentOutputs)
                    {
                        foreach (TxIn txIn in tx.Transaction.Inputs)
                        {
                            if (spentOutput.TransactionId == txIn.PrevOut.Hash && spentOutput.Index == txIn.PrevOut.N)                             // Do not do (spentOutput == txIn.PrevOut), it's faster this way, because it won't check for null.
                            {
                                doubleSpends.Add(coin);
                                spent          = true;
                                walletRelevant = true;
                                break;
                            }
                        }
                        if (spent)
                        {
                            break;
                        }
                    }
                }

                if (doubleSpends.Any())
                {
                    if (tx.Height == Height.Mempool)
                    {
                        // if the received transaction is spending at least one input already
                        // spent by a previous unconfirmed transaction signaling RBF then it is not a double
                        // spanding transaction but a replacement transaction.
                        var isReplacemenetTx = doubleSpends.Any(x => x.IsReplaceable && !x.Confirmed);
                        if (isReplacemenetTx)
                        {
                            // Undo the replaced transaction by removing the coins it created (if other coin
                            // spends it, remove that too and so on) and restoring those that it destroyed
                            // ones. After undoing the replaced transaction it will process the replacement
                            // transaction.
                            var replacedTxId = doubleSpends.First().TransactionId;
                            var(destroyed, restored) = Coins.Undo(replacedTxId);

                            ReplaceTransactionReceived?.Invoke(this, new ReplaceTransactionReceivedEventArgs(tx, destroyed, restored));

                            tx.SetReplacement();
                            walletRelevant = true;
                        }
                        else
                        {
                            DoubleSpendReceived?.Invoke(this, new DoubleSpendReceivedEventArgs(tx, Enumerable.Empty <SmartCoin>()));
                            return(false);
                        }
                    }
                    else                     // new confirmation always enjoys priority
                    {
                        // remove double spent coins recursively (if other coin spends it, remove that too and so on), will add later if they came to our keys
                        foreach (SmartCoin doubleSpentCoin in doubleSpends)
                        {
                            Coins.Remove(doubleSpentCoin);
                        }

                        DoubleSpendReceived?.Invoke(this, new DoubleSpendReceivedEventArgs(tx, doubleSpends));
                        walletRelevant = true;
                    }
                }
            }

            var  isLikelyCoinJoinOutput = false;
            bool hasEqualOutputs        = tx.Transaction.GetIndistinguishableOutputs(includeSingle: false).FirstOrDefault() != default;

            if (hasEqualOutputs)
            {
                var  receiveKeys         = KeyManager.GetKeys(x => tx.Transaction.Outputs.Any(y => y.ScriptPubKey == x.P2wpkhScript));
                bool allReceivedInternal = receiveKeys.All(x => x.IsInternal);
                if (allReceivedInternal)
                {
                    // It is likely a coinjoin if the diff between receive and sent amount is small and have at least 2 equal outputs.
                    Money spentAmount    = Coins.OutPoints(tx.Transaction.Inputs.ToTxoRefs()).TotalAmount();
                    Money receivedAmount = tx.Transaction.Outputs.Where(x => receiveKeys.Any(y => y.P2wpkhScript == x.ScriptPubKey)).Sum(x => x.Value);
                    bool  receivedAlmostAsMuchAsSpent = spentAmount.Almost(receivedAmount, Money.Coins(0.005m));

                    if (receivedAlmostAsMuchAsSpent)
                    {
                        isLikelyCoinJoinOutput = true;
                    }
                }
            }

            List <SmartCoin> newCoins      = new List <SmartCoin>();
            List <SmartCoin> spentOwnCoins = null;

            for (var i = 0U; i < tx.Transaction.Outputs.Count; i++)
            {
                // If transaction received to any of the wallet keys:
                var      output   = tx.Transaction.Outputs[i];
                HdPubKey foundKey = KeyManager.GetKeyForScriptPubKey(output.ScriptPubKey);
                if (foundKey != default)
                {
                    walletRelevant = true;

                    if (output.Value <= DustThreshold)
                    {
                        continue;
                    }

                    foundKey.SetKeyState(KeyState.Used, KeyManager);
                    spentOwnCoins ??= Coins.OutPoints(tx.Transaction.Inputs.ToTxoRefs()).ToList();
                    var anonset = tx.Transaction.GetAnonymitySet(i);
                    if (spentOwnCoins.Count != 0)
                    {
                        anonset += spentOwnCoins.Min(x => x.AnonymitySet) - 1;                         // Minus 1, because do not count own.
                    }

                    SmartCoin newCoin = new SmartCoin(txId, i, output.ScriptPubKey, output.Value, tx.Transaction.Inputs.ToTxoRefs().ToArray(), tx.Height, tx.IsRBF, anonset, isLikelyCoinJoinOutput, foundKey.Label, spenderTransactionId: null, false, pubKey: foundKey);                     // Do not inherit locked status from key, that's different.
                    // If we did not have it.
                    if (Coins.TryAdd(newCoin))
                    {
                        newCoins.Add(newCoin);

                        // Make sure there's always 21 clean keys generated and indexed.
                        KeyManager.AssertCleanKeysIndexed(isInternal: foundKey.IsInternal);

                        if (foundKey.IsInternal)
                        {
                            // Make sure there's always 14 internal locked keys generated and indexed.
                            KeyManager.AssertLockedInternalKeysIndexed(14);
                        }
                    }
                    else                                      // If we had this coin already.
                    {
                        if (newCoin.Height != Height.Mempool) // Update the height of this old coin we already had.
                        {
                            SmartCoin oldCoin = Coins.AsAllCoinsView().GetByOutPoint(new OutPoint(txId, i));
                            if (oldCoin != null)                             // Just to be sure, it is a concurrent collection.
                            {
                                oldCoin.Height = newCoin.Height;
                            }
                        }
                    }
                }
            }

            // If spends any of our coin
            for (var i = 0; i < tx.Transaction.Inputs.Count; i++)
            {
                var input = tx.Transaction.Inputs[i];

                var foundCoin = Coins.GetByOutPoint(input.PrevOut);
                if (foundCoin != null)
                {
                    walletRelevant = true;
                    var alreadyKnown = foundCoin.SpenderTransactionId == txId;
                    foundCoin.SpenderTransactionId = txId;

                    if (!alreadyKnown)
                    {
                        Coins.Spend(foundCoin);
                        CoinSpent?.Invoke(this, foundCoin);
                    }

                    if (tx.Confirmed)
                    {
                        SpenderConfirmed?.Invoke(this, foundCoin);
                    }
                }
            }

            if (walletRelevant)
            {
                TransactionStore.AddOrUpdate(tx);
            }

            foreach (var newCoin in newCoins)
            {
                CoinReceived?.Invoke(this, newCoin);
            }

            return(walletRelevant);
        }
 public DoubleSpendReceivedEventArgs(SmartTransaction smartTransaction, IEnumerable <SmartCoin> remove)
 {
     SmartTransaction = smartTransaction;
     Remove           = remove;
 }
 public override bool TryGetTransaction(uint256 hash, out SmartTransaction sameStx)
 {
     sameStx = null;
     return(false);
 }
Exemple #20
0
 public ReplaceTransactionReceivedEventArgs(SmartTransaction smartTransaction, IEnumerable <SmartCoin> destroyedCoins, IEnumerable <SmartCoin> restoredCoins)
 {
     SmartTransaction = smartTransaction;
     DestroyedCoins   = destroyedCoins;
     RestoredCoins    = restoredCoins;
 }