void ExecuteAndCheckSendTransactions(string[] txsHex, RpcSendTransactions expected, RpcSendTransactions node0Response,
                                             RpcSendTransactions node1Response)
        {
            rpcClientFactoryMock.SetUpPredefinedResponse(
                ("umockNode0:sendrawtransactions", node0Response),
                ("umockNode1:sendrawtransactions", node1Response));


            var c = new RpcMultiClient(new MockNodes(2), rpcClientFactoryMock, NullLogger <RpcMultiClient> .Instance);

            var r = c.SendRawTransactionsAsync(
                txsHex.Select(x =>
                              (HelperTools.HexStringToByteArray(x), true, true)).ToArray()).Result;

            Assert.AreEqual(JsonSerializer.Serialize(expected), JsonSerializer.Serialize(r));
        }
        void TestMixedTxC1(RpcSendTransactions node0Response, RpcSendTransactions node1Response)
        {
            ExecuteAndCheckSendTransactions(new[] { txC1Hex }, new RpcSendTransactions
            {
                Known   = new string[0],
                Evicted = new string[0],

                Invalid = new[]
                {
                    new RpcSendTransactions.RpcInvalidTx()
                    {
                        Txid         = txC1Hash,
                        RejectReason = "Mixed results"
                    }
                }
            },
                                            node0Response,
                                            node1Response);
        }
        public void SendRawTransationsInvalid()
        {
            // Empty response means, that everything was accepted
            var invalidResponse =
                new RpcSendTransactions
            {
                Known   = new string[0],
                Evicted = new string[0],

                Invalid = new[]
                {
                    new RpcSendTransactions.RpcInvalidTx
                    {
                        Txid = txC1Hash
                    }
                }
            };

            ExecuteAndCheckSendTransactions(
                new [] { txC1Hex },
                invalidResponse,
                invalidResponse,
                invalidResponse);
        }
Ejemplo n.º 4
0
        public async Task <SubmitTransactionsResponse> SubmitTransactionsAsync(IEnumerable <SubmitTransaction> requestEnum, UserAndIssuer user)
        {
            var request = requestEnum.ToArray();

            logger.LogInformation($"Processing {request.Length} incoming transactions");
            // Take snapshot of current metadata and use use it for all transactions
            var info           = blockChainInfo.GetInfo();
            var currentMinerId = await minerId.GetCurrentMinerIdAsync();

            var consolidationParameters = info.ConsolidationTxParameters;

            // Use the same quotes for all transactions in single request
            var quotes = feeQuoteRepository.GetValidFeeQuotesByIdentity(user).ToArray();

            if (quotes == null || !quotes.Any())
            {
                throw new Exception("No fee quotes available");
            }

            var responses            = new List <SubmitTransactionOneResponse>();
            var transactionsToSubmit = new List <(string transactionId, SubmitTransaction transaction, bool allowhighfees, bool dontCheckFees)>();
            int failureCount         = 0;

            IDictionary <uint256, byte[]> allTxs = new Dictionary <uint256, byte[]>();

            foreach (var oneTx in request)
            {
                if ((oneTx.RawTx == null || oneTx.RawTx.Length == 0) && string.IsNullOrEmpty(oneTx.RawTxString))
                {
                    AddFailureResponse(null, $"{nameof(SubmitTransaction.RawTx)} is required", ref responses);

                    failureCount++;
                    continue;
                }

                if (oneTx.RawTx == null)
                {
                    try
                    {
                        oneTx.RawTx = HelperTools.HexStringToByteArray(oneTx.RawTxString);
                    }
                    catch (Exception ex)
                    {
                        AddFailureResponse(null, ex.Message, ref responses);

                        failureCount++;
                        continue;
                    }
                }
                uint256 txId       = Hashes.DoubleSHA256(oneTx.RawTx);
                string  txIdString = txId.ToString();

                if (allTxs.ContainsKey(txId))
                {
                    AddFailureResponse(txIdString, "Transaction with this id occurs more than once within request", ref responses);

                    failureCount++;
                    continue;
                }

                var vc     = new ValidationContext(oneTx);
                var errors = oneTx.Validate(vc);
                if (errors.Count() > 0)
                {
                    AddFailureResponse(txIdString, string.Join(",", errors.Select(x => x.ErrorMessage)), ref responses);

                    failureCount++;
                    continue;
                }
                allTxs.Add(txId, oneTx.RawTx);
                bool okToMine  = false;
                bool okToRelay = false;
                if (await txRepository.TransactionExistsAsync(txId.ToBytes()))
                {
                    AddFailureResponse(txIdString, "Transaction already known", ref responses);

                    failureCount++;
                    continue;
                }

                Transaction    transaction    = null;
                CollidedWith[] colidedWith    = {};
                Exception      exception      = null;
                string[]       prevOutsErrors = { };
                try
                {
                    transaction = HelperTools.ParseBytesToTransaction(oneTx.RawTx);

                    if (transaction.IsCoinBase)
                    {
                        throw new ExceptionWithSafeErrorMessage("Invalid transaction - coinbase transactions are not accepted");
                    }
                    var(sumPrevOuputs, prevOuts) = await CollectPreviousOuputs(transaction, new ReadOnlyDictionary <uint256, byte[]>(allTxs), rpcMultiClient);

                    prevOutsErrors = prevOuts.Where(x => !string.IsNullOrEmpty(x.Error)).Select(x => x.Error).ToArray();
                    colidedWith    = prevOuts.Where(x => x.CollidedWith != null).Select(x => x.CollidedWith).ToArray();

                    if (IsConsolidationTxn(transaction, consolidationParameters, prevOuts))
                    {
                        (okToMine, okToRelay) = (true, true);
                    }
                    else
                    {
                        foreach (var feeQuote in quotes)
                        {
                            var(okToMineTmp, okToRelayTmp) =
                                CheckFees(transaction, oneTx.RawTx.LongLength, sumPrevOuputs, feeQuote);
                            if (GetCheckFeesValue(okToMineTmp, okToRelayTmp) > GetCheckFeesValue(okToMine, okToRelay))
                            {
                                // save best combination
                                (okToMine, okToRelay) = (okToMineTmp, okToRelayTmp);
                            }
                        }
                    }
                }
                catch (Exception ex)
                {
                    exception = ex;
                }

                if (exception != null || colidedWith.Any() || transaction == null || prevOutsErrors.Any())
                {
                    var oneResponse = new SubmitTransactionOneResponse
                    {
                        Txid         = txIdString,
                        ReturnResult = ResultCodes.Failure,
                        // Include non null ConflictedWith only if a collision has been detected
                        ConflictedWith = !colidedWith.Any() ? null : colidedWith.Select(
                            x => new SubmitTransactionConflictedTxResponse
                        {
                            Txid = x.TxId,
                            Size = x.Size,
                            Hex  = x.Hex,
                        }).ToArray()
                    };

                    if (transaction is null)
                    {
                        oneResponse.ResultDescription = "Can not parse transaction";
                    }
                    else if (exception is ExceptionWithSafeErrorMessage)
                    {
                        oneResponse.ResultDescription = exception.Message;
                    }
                    else if (exception != null)
                    {
                        oneResponse.ResultDescription = "Error fetching inputs";
                    }
                    else // colidedWith !=null and there is no exception or prevOutsErrors is not empty
                    {
                        // return "Missing inputs" regardless of error returned from gettxouts (which is usually "missing")
                        oneResponse.ResultDescription = "Missing inputs";
                    }
                    logger.LogError($"Can not calculate fee for {txIdString}. Error: {oneResponse.ResultDescription} Exception: {exception?.ToString() ?? ""}");


                    responses.Add(oneResponse);
                    failureCount++;
                    continue;
                }

                // Transactions  was successfully analyzed
                if (!okToMine && !okToRelay)
                {
                    AddFailureResponse(txIdString, "Not enough fees", ref responses);

                    failureCount++;
                }
                else
                {
                    bool allowHighFees = false;
                    bool dontcheckfee  = okToMine;

                    oneTx.TransactionInputs = transaction.Inputs.AsIndexedInputs().Select(x => new TxInput
                    {
                        N        = x.Index,
                        PrevN    = x.PrevOut.N,
                        PrevTxId = x.PrevOut.Hash.ToBytes()
                    }).ToList();
                    transactionsToSubmit.Add((txIdString, oneTx, allowHighFees, dontcheckfee));
                }
            }

            RpcSendTransactions rpcResponse;

            Exception submitException = null;

            if (transactionsToSubmit.Any())
            {
                // Submit all collected transactions in one call
                try
                {
                    rpcResponse = await rpcMultiClient.SendRawTransactionsAsync(
                        transactionsToSubmit.Select(x => (x.transaction.RawTx, x.allowhighfees, x.dontCheckFees))
                        .ToArray());
                }
                catch (Exception ex)
                {
                    submitException = ex;
                    rpcResponse     = null;
                }
            }
            else
            {
                // Simulate empty response
                rpcResponse = new RpcSendTransactions();
            }


            // Initialize common fields
            var result = new SubmitTransactionsResponse
            {
                Timestamp = clock.UtcNow(),
                MinerId   = currentMinerId,
                CurrentHighestBlockHash   = info.BestBlockHash,
                CurrentHighestBlockHeight = info.BestBlockHeight,
                // TxSecondMempoolExpiry
                // Remaining of the fields are initialized bellow
            };

            if (submitException != null)
            {
                var unableToSubmit = transactionsToSubmit.Select(x =>
                                                                 new SubmitTransactionOneResponse
                {
                    Txid              = x.transactionId,
                    ReturnResult      = ResultCodes.Failure,
                    ResultDescription = "Error while submitting transactions to the node" // do not expose detailed error message. It might contain internal IPS etc
                });

                logger.LogError($"Error while submitting transactions to the node {submitException}");
                responses.AddRange(unableToSubmit);
                result.Txs          = responses.ToArray();
                result.FailureCount = result.Txs.Length; // all of the transactions have failed

                return(result);
            }
            else // submitted without error
            {
                var(submitFailureCount, transformed) = TransformRpcResponse(rpcResponse,
                                                                            transactionsToSubmit.Select(x => x.transactionId).ToArray());
                responses.AddRange(transformed);
                result.Txs          = responses.ToArray();
                result.FailureCount = failureCount + submitFailureCount;

                var successfullTxs = transactionsToSubmit.Where(x => transformed.Any(y => y.ReturnResult == ResultCodes.Success && y.Txid == x.transactionId));
                await txRepository.InsertTxsAsync(successfullTxs.Select(x => new Tx
                {
                    CallbackToken = x.transaction.CallbackToken,
                    CallbackUrl = x.transaction.CallbackUrl,
                    CallbackEncryption = x.transaction.CallbackEncryption,
                    DSCheck = x.transaction.DsCheck,
                    MerkleProof = x.transaction.MerkleProof,
                    TxExternalId = new uint256(x.transactionId),
                    TxPayload = x.transaction.RawTx,
                    ReceivedAt = clock.UtcNow(),
                    TxIn = x.transaction.TransactionInputs
                }).ToList());

                return(result);
            }
        }
Ejemplo n.º 5
0
        public static (int failureCount, SubmitTransactionOneResponse[] responses) TransformRpcResponse(RpcSendTransactions rpcResponse, string[] allSubmitedTxIds)
        {
            // Track which transaction was already processed, so that we only return one response per txid:
            var processed = new Dictionary <string, object>(StringComparer.InvariantCulture);

            int failed   = 0;
            var respones = new List <SubmitTransactionOneResponse>();

            if (rpcResponse.Invalid != null)
            {
                foreach (var invalid in rpcResponse.Invalid)
                {
                    if (processed.TryAdd(invalid.Txid, null))
                    {
                        respones.Add(new SubmitTransactionOneResponse
                        {
                            Txid              = invalid.Txid,
                            ReturnResult      = ResultCodes.Failure,
                            ResultDescription =
                                (invalid.RejectCode + " " + invalid.RejectReason).Trim(),

                            ConflictedWith = invalid.CollidedWith?.Select(t =>
                                                                          new SubmitTransactionConflictedTxResponse {
                                Txid = t.Txid,
                                Size = t.Size,
                                Hex  = t.Hex
                            }
                                                                          ).ToArray()
                        });

                        failed++;
                    }
                }
            }

            if (rpcResponse.Evicted != null)
            {
                foreach (var evicted in rpcResponse.Evicted)
                {
                    if (processed.TryAdd(evicted, null))
                    {
                        respones.Add(new SubmitTransactionOneResponse
                        {
                            Txid              = evicted,
                            ReturnResult      = ResultCodes.Failure,
                            ResultDescription =
                                "evicted" // This only happens if mempool is full and contain no P2P transactions (which have low priority)
                        });
                        failed++;
                    }
                }
            }


            if (rpcResponse.Known != null)
            {
                foreach (var known in rpcResponse.Known)
                {
                    if (processed.TryAdd(known, null))
                    {
                        respones.Add(new SubmitTransactionOneResponse
                        {
                            Txid              = known,
                            ReturnResult      = ResultCodes.Success,
                            ResultDescription =
                                "Already known"
                        });
                    }
                }
            }

            // If a transaction is not present in response, then it was successfully accepted as a new transaction
            foreach (var txId in allSubmitedTxIds)
            {
                if (!processed.ContainsKey(txId))
                {
                    respones.Add(new SubmitTransactionOneResponse
                    {
                        Txid         = txId,
                        ReturnResult = ResultCodes.Success,
                    });
                }
            }

            return(failed, respones.ToArray());
        }
Ejemplo n.º 6
0
        public async Task <SubmitTransactionsResponse> SubmitTransactionsAsync(IEnumerable <SubmitTransaction> requestEnum, UserAndIssuer user)
        {
            var request = requestEnum.ToArray();

            logger.LogInformation($"Processing {request.Length} incoming transactions");
            // Take snapshot of current metadata and use use it for all transactions
            var info = await blockChainInfo.GetInfoAsync();

            var currentMinerId = await minerId.GetCurrentMinerIdAsync();

            var consolidationParameters = info.ConsolidationTxParameters;

            // Use the same quotes for all transactions in single request
            var quotes = feeQuoteRepository.GetValidFeeQuotesByIdentity(user).ToArray();

            if (quotes == null || !quotes.Any())
            {
                throw new Exception("No fee quotes available");
            }

            var responses            = new List <SubmitTransactionOneResponse>();
            var transactionsToSubmit = new List <(string transactionId, SubmitTransaction transaction, bool allowhighfees, bool dontCheckFees, bool listUnconfirmedAncestors, Dictionary <string, object> config)>();
            int failureCount         = 0;

            IDictionary <uint256, byte[]> allTxs = new Dictionary <uint256, byte[]>();

            foreach (var oneTx in request)
            {
                if (!string.IsNullOrEmpty(oneTx.MerkleFormat) && !MerkleFormat.ValidFormats.Any(x => x == oneTx.MerkleFormat))
                {
                    AddFailureResponse(null, $"Invalid merkle format {oneTx.MerkleFormat}. Supported formats: {String.Join(",", MerkleFormat.ValidFormats)}.", ref responses);

                    failureCount++;
                    continue;
                }

                if ((oneTx.RawTx == null || oneTx.RawTx.Length == 0) && string.IsNullOrEmpty(oneTx.RawTxString))
                {
                    AddFailureResponse(null, $"{nameof(SubmitTransaction.RawTx)} is required", ref responses);

                    failureCount++;
                    continue;
                }

                if (oneTx.RawTx == null)
                {
                    try
                    {
                        oneTx.RawTx = HelperTools.HexStringToByteArray(oneTx.RawTxString);
                    }
                    catch (Exception ex)
                    {
                        AddFailureResponse(null, ex.Message, ref responses);

                        failureCount++;
                        continue;
                    }
                }
                uint256 txId       = Hashes.DoubleSHA256(oneTx.RawTx);
                string  txIdString = txId.ToString();
                logger.LogInformation($"Processing transaction: { txIdString }");

                if (oneTx.MerkleProof && (appSettings.DontParseBlocks.Value || appSettings.DontInsertTransactions.Value))
                {
                    AddFailureResponse(txIdString, $"Transaction requires merkle proof notification but this instance of mAPI does not support callbacks", ref responses);

                    failureCount++;
                    continue;
                }

                if (oneTx.DsCheck && (appSettings.DontParseBlocks.Value || appSettings.DontInsertTransactions.Value))
                {
                    AddFailureResponse(txIdString, $"Transaction requires double spend notification but this instance of mAPI does not support callbacks", ref responses);

                    failureCount++;
                    continue;
                }

                if (allTxs.ContainsKey(txId))
                {
                    AddFailureResponse(txIdString, "Transaction with this id occurs more than once within request", ref responses);

                    failureCount++;
                    continue;
                }

                var vc     = new ValidationContext(oneTx);
                var errors = oneTx.Validate(vc);
                if (errors.Any())
                {
                    AddFailureResponse(txIdString, string.Join(",", errors.Select(x => x.ErrorMessage)), ref responses);

                    failureCount++;
                    continue;
                }
                allTxs.Add(txId, oneTx.RawTx);
                bool okToMine  = false;
                bool okToRelay = false;
                Dictionary <string, object> policies = null;
                if (await txRepository.TransactionExistsAsync(txId.ToBytes()))
                {
                    AddFailureResponse(txIdString, "Transaction already known", ref responses);

                    failureCount++;
                    continue;
                }

                Transaction    transaction    = null;
                CollidedWith[] colidedWith    = Array.Empty <CollidedWith>();
                Exception      exception      = null;
                string[]       prevOutsErrors = Array.Empty <string>();
                try
                {
                    transaction = HelperTools.ParseBytesToTransaction(oneTx.RawTx);

                    if (transaction.IsCoinBase)
                    {
                        throw new ExceptionWithSafeErrorMessage("Invalid transaction - coinbase transactions are not accepted");
                    }
                    var(sumPrevOuputs, prevOuts) = await CollectPreviousOuputs(transaction, new ReadOnlyDictionary <uint256, byte[]>(allTxs), rpcMultiClient);

                    prevOutsErrors = prevOuts.Where(x => !string.IsNullOrEmpty(x.Error)).Select(x => x.Error).ToArray();
                    colidedWith    = prevOuts.Where(x => x.CollidedWith != null).Select(x => x.CollidedWith).ToArray();
                    logger.LogInformation($"CollectPreviousOuputs for {txIdString} returned { prevOuts.Length } prevOuts ({prevOutsErrors.Length } prevOutsErrors, {colidedWith.Length} colidedWith).");

                    if (appSettings.CheckFeeDisabled.Value || IsConsolidationTxn(transaction, consolidationParameters, prevOuts))
                    {
                        logger.LogInformation($"{txIdString}: appSettings.CheckFeeDisabled { appSettings.CheckFeeDisabled }");
                        (okToMine, okToRelay) = (true, true);
                    }
                    else
                    {
                        logger.LogInformation($"Starting with CheckFees calculation for {txIdString} and { quotes.Length} quotes.");
                        foreach (var feeQuote in quotes)
                        {
                            var(okToMineTmp, okToRelayTmp) =
                                CheckFees(transaction, oneTx.RawTx.LongLength, sumPrevOuputs, feeQuote);
                            if (GetCheckFeesValue(okToMineTmp, okToRelayTmp) > GetCheckFeesValue(okToMine, okToRelay))
                            {
                                // save best combination
                                (okToMine, okToRelay, policies) = (okToMineTmp, okToRelayTmp, feeQuote.PoliciesDict);
                            }
                        }
                        logger.LogInformation($"Finished with CheckFees calculation for {txIdString} and { quotes.Length} quotes: { (okToMine, okToRelay, policies == null ? "" : string.Join(";", policies.Select(x => x.Key + "=" + x.Value)) )}.");
                    }
                }
                catch (Exception ex)
                {
                    exception = ex;
                }

                if (exception != null || colidedWith.Any() || transaction == null || prevOutsErrors.Any())
                {
                    var oneResponse = new SubmitTransactionOneResponse
                    {
                        Txid         = txIdString,
                        ReturnResult = ResultCodes.Failure,
                        // Include non null ConflictedWith only if a collision has been detected
                        ConflictedWith = !colidedWith.Any() ? null : colidedWith.Select(
                            x => new SubmitTransactionConflictedTxResponse
                        {
                            Txid = x.TxId,
                            Size = x.Size,
                            Hex  = x.Hex,
                        }).ToArray()
                    };

                    if (transaction is null)
                    {
                        oneResponse.ResultDescription = "Can not parse transaction";
                    }
                    else if (exception is ExceptionWithSafeErrorMessage)
                    {
                        oneResponse.ResultDescription = exception.Message;
                    }
                    else if (exception != null)
                    {
                        oneResponse.ResultDescription = "Error fetching inputs";
                    }
                    else if (oneResponse.ConflictedWith != null && oneResponse.ConflictedWith.Any(c => c.Txid == oneResponse.Txid))
                    {
                        oneResponse.ResultDescription = "Transaction already in the mempool";
                        oneResponse.ConflictedWith    = null;
                    }
                    else
                    {
                        // return "Missing inputs" regardless of error returned from gettxouts (which is usually "missing")
                        oneResponse.ResultDescription = "Missing inputs";
                    }
                    logger.LogError($"Can not calculate fee for {txIdString}. Error: {oneResponse.ResultDescription} Exception: {exception?.ToString() ?? ""}");


                    responses.Add(oneResponse);
                    failureCount++;
                    continue;
                }

                // Transactions  was successfully analyzed
                if (!okToMine && !okToRelay)
                {
                    AddFailureResponse(txIdString, "Not enough fees", ref responses);

                    failureCount++;
                }
                else
                {
                    bool allowHighFees            = false;
                    bool dontcheckfee             = okToMine;
                    bool listUnconfirmedAncestors = false;

                    oneTx.TransactionInputs = transaction.Inputs.AsIndexedInputs().Select(x => new TxInput
                    {
                        N        = x.Index,
                        PrevN    = x.PrevOut.N,
                        PrevTxId = x.PrevOut.Hash.ToBytes()
                    }).ToList();
                    if (oneTx.DsCheck)
                    {
                        foreach (TxInput txInput in oneTx.TransactionInputs)
                        {
                            var prevOut = await txRepository.GetPrevOutAsync(txInput.PrevTxId, txInput.PrevN);

                            if (prevOut == null)
                            {
                                listUnconfirmedAncestors = true;
                                break;
                            }
                        }
                    }
                    transactionsToSubmit.Add((txIdString, oneTx, allowHighFees, dontcheckfee, listUnconfirmedAncestors, policies));
                }
            }

            logger.LogInformation($"TransactionsToSubmit: { transactionsToSubmit.Count }: { string.Join("; ", transactionsToSubmit.Select(x => x.transactionId))} ");

            RpcSendTransactions rpcResponse;

            Exception submitException = null;

            if (transactionsToSubmit.Any())
            {
                // Submit all collected transactions in one call
                try
                {
                    rpcResponse = await rpcMultiClient.SendRawTransactionsAsync(
                        transactionsToSubmit.Select(x => (x.transaction.RawTx, x.allowhighfees, x.dontCheckFees, x.listUnconfirmedAncestors, x.config))
                        .ToArray());
                }
                catch (Exception ex)
                {
                    submitException = ex;
                    rpcResponse     = null;
                }
            }
            else
            {
                // Simulate empty response
                rpcResponse = new RpcSendTransactions();
            }


            // Initialize common fields
            var result = new SubmitTransactionsResponse
            {
                Timestamp = clock.UtcNow(),
                MinerId   = currentMinerId,
                CurrentHighestBlockHash   = info.BestBlockHash,
                CurrentHighestBlockHeight = info.BestBlockHeight,
                // TxSecondMempoolExpiry
                // Remaining of the fields are initialized bellow
            };

            if (submitException != null)
            {
                var unableToSubmit = transactionsToSubmit.Select(x =>
                                                                 new SubmitTransactionOneResponse
                {
                    Txid              = x.transactionId,
                    ReturnResult      = ResultCodes.Failure,
                    ResultDescription = "Error while submitting transactions to the node" // do not expose detailed error message. It might contain internal IPS etc
                });

                logger.LogError($"Error while submitting transactions to the node {submitException}");
                responses.AddRange(unableToSubmit);
                result.Txs          = responses.ToArray();
                result.FailureCount = result.Txs.Length; // all of the transactions have failed

                return(result);
            }
            else // submitted without error
            {
                var(submitFailureCount, transformed) = TransformRpcResponse(rpcResponse,
                                                                            transactionsToSubmit.Select(x => x.transactionId).ToArray());
                responses.AddRange(transformed);
                result.Txs          = responses.ToArray();
                result.FailureCount = failureCount + submitFailureCount;


                if (!appSettings.DontInsertTransactions.Value)
                {
                    var successfullTxs = transactionsToSubmit.Where(x => transformed.Any(y => y.ReturnResult == ResultCodes.Success && y.Txid == x.transactionId));
                    logger.LogInformation($"Starting with InsertTxsAsync: { successfullTxs.Count() }: { string.Join("; ", successfullTxs.Select(x => x.transactionId))} (TransactionsToSubmit: { transactionsToSubmit.Count })");
                    var watch = System.Diagnostics.Stopwatch.StartNew();
                    await txRepository.InsertTxsAsync(successfullTxs.Select(x => new Tx
                    {
                        CallbackToken = x.transaction.CallbackToken,
                        CallbackUrl = x.transaction.CallbackUrl,
                        CallbackEncryption = x.transaction.CallbackEncryption,
                        DSCheck = x.transaction.DsCheck,
                        MerkleProof = x.transaction.MerkleProof,
                        MerkleFormat = x.transaction.MerkleFormat,
                        TxExternalId = new uint256(x.transactionId),
                        TxPayload = x.transaction.RawTx,
                        ReceivedAt = clock.UtcNow(),
                        TxIn = x.transaction.TransactionInputs
                    }).ToList(), false);

                    long unconfirmedAncestorsCount = 0;
                    if (rpcResponse.Unconfirmed != null)
                    {
                        List <Tx> unconfirmedAncestors = new();
                        foreach (var unconfirmed in rpcResponse.Unconfirmed)
                        {
                            unconfirmedAncestors.AddRange(unconfirmed.Ancestors.Select(u => new Tx
                            {
                                TxExternalId = new uint256(u.Txid),
                                ReceivedAt   = clock.UtcNow(),
                                TxIn         = u.Vin.Select(i => new TxInput()
                                {
                                    PrevTxId = (new uint256(i.Txid)).ToBytes(),
                                    PrevN    = i.Vout
                                }).ToList()
                            })
                                                          );
                        }
                        await txRepository.InsertTxsAsync(unconfirmedAncestors, true);

                        unconfirmedAncestorsCount = unconfirmedAncestors.Count;
                    }
                    watch.Stop();
                    logger.LogInformation($"Finished with InsertTxsAsync: { successfullTxs.Count() } found unconfirmedAncestors { unconfirmedAncestorsCount } took {watch.ElapsedMilliseconds} ms.");
                }

                return(result);
            }
        }