private async Task UpdatePSBTCore(UpdatePSBTRequest update, NBXplorerNetwork network)
        {
            var repo = RepositoryProvider.GetRepository(network);
            var rpc  = Waiters.GetWaiter(network);

            await UpdateInputsUTXO(update, repo, rpc);

            if (update.DerivationScheme is DerivationStrategyBase)
            {
                foreach (var extpub in update.DerivationScheme.GetExtPubKeys().Select(e => e.GetWif(network.NBitcoinNetwork)))
                {
                    update.PSBT.GlobalXPubs.AddOrReplace(extpub, new RootedKeyPath(extpub, new KeyPath()));
                }
                await UpdateHDKeyPathsWitnessAndRedeem(update, repo);
            }

            foreach (var input in update.PSBT.Inputs)
            {
                input.TrySlimUTXO();
            }

            if (update.RebaseKeyPaths != null)
            {
                foreach (var rebase in update.RebaseKeyPaths)
                {
                    var rootedKeyPath = rebase.GetRootedKeyPath();
                    if (rootedKeyPath == null)
                    {
                        throw new NBXplorerException(new NBXplorerError(400, "missing-parameter", "rebaseKeyPaths[].rootedKeyPath is missing"));
                    }
                    update.PSBT.RebaseKeyPaths(rebase.AccountKey, rootedKeyPath);
                }
            }
        }
        public Task StartAsync(CancellationToken cancellationToken)
        {
            if (_Disposed)
            {
                throw new ObjectDisposedException(nameof(BrokerHostedService));
            }

            _senderBlock        = CreateClientBlock();
            _senderTransactions = CreateClientTransaction();

            _subscriptions.Add(_EventAggregator.Subscribe <Events.NewBlockEvent>(async o =>
            {
                var chain = ChainProvider.GetChain(o.CryptoCode);
                if (chain == null)
                {
                    return;
                }
                var block = chain.GetBlock(o.BlockId);
                if (block != null)
                {
                    var nbe = new Models.NewBlockEvent()
                    {
                        CryptoCode        = o.CryptoCode,
                        Hash              = block.Hash,
                        Height            = block.Height,
                        PreviousBlockHash = block?.Previous
                    };
                    await _senderBlock.Send(nbe);
                }
            }));


            _subscriptions.Add(_EventAggregator.Subscribe <Events.NewTransactionMatchEvent>(async o =>
            {
                var network = Waiters.GetWaiter(o.CryptoCode);
                if (network == null)
                {
                    return;
                }
                var chain = ChainProvider.GetChain(o.CryptoCode);
                if (chain == null)
                {
                    return;
                }
                var txe = new Models.NewTransactionEvent()
                {
                    TrackedSource      = o.Match.TrackedSource,
                    DerivationStrategy = o.Match.DerivationStrategy,
                    CryptoCode         = o.CryptoCode,
                    BlockId            = o.BlockId,
                    TransactionData    = Utils.ToTransactionResult(true, chain, new[] { o.SavedTransaction }),
                    Inputs             = o.Match.Inputs,
                    Outputs            = o.Match.Outputs
                };
                await _senderTransactions.Send(txe);
            }));

            Logs.Configuration.LogInformation("Starting Azure Service Bus Message Broker");
            return(Task.CompletedTask);
        }
Exemple #3
0
        public async Task <IActionResult> GetTransaction(
            [ModelBinder(BinderType = typeof(UInt256ModelBinding))]
            uint256 txId,
            bool includeTransaction = true,
            string cryptoCode       = null)
        {
            var network = GetNetwork(cryptoCode, false);
            var chain   = this.ChainProvider.GetChain(network);
            var result  = await RepositoryProvider.GetRepository(network).GetSavedTransactions(txId);

            if (result.Length == 0)
            {
                var waiter = Waiters.GetWaiter(cryptoCode);
                if (waiter.RPCAvailable &&
                    waiter.HasTxIndex &&
                    await waiter.RPC.TryGetRawTransaction(txId) is Repository.SavedTransaction savedTransaction)
                {
                    result = new[] { savedTransaction };
                }
                else
                {
                    return(NotFound());
                }
            }
            var tx = Utils.ToTransactionResult(chain, result);

            if (!includeTransaction)
            {
                tx.Transaction = null;
            }
            return(Json(tx));
        }
Exemple #4
0
        public async Task <IActionResult> GetStatus(string cryptoCode)
        {
            var network = GetNetwork(cryptoCode, false);
            var waiter  = Waiters.GetWaiter(network);
            var chain   = ChainProvider.GetChain(network);
            var repo    = RepositoryProvider.GetRepository(network);
            var now     = DateTimeOffset.UtcNow;


            var location = waiter.GetLocation();

            var blockchainInfoAsync = waiter.RPCAvailable ? waiter.RPC.GetBlockchainInfoAsyncEx() : null;
            var networkInfoAsync    = waiter.RPCAvailable ? waiter.RPC.GetNetworkInfoAsync() : null;
            await repo.Ping();

            var pingAfter = DateTimeOffset.UtcNow;

            GetBlockchainInfoResponse blockchainInfo = blockchainInfoAsync == null ? null : await blockchainInfoAsync;
            GetNetworkInfoResponse    networkInfo    = networkInfoAsync == null ? null : await networkInfoAsync;
            var status = new StatusResult()
            {
                NetworkType          = network.NBitcoinNetwork.NetworkType,
                CryptoCode           = network.CryptoCode,
                Version              = typeof(MainController).GetTypeInfo().Assembly.GetCustomAttribute <AssemblyFileVersionAttribute>().Version,
                SupportedCryptoCodes = Waiters.All().Select(w => w.Network.CryptoCode).ToArray(),
                RepositoryPingTime   = (pingAfter - now).TotalSeconds,
                IsFullySynched       = true
            };

            if (status.RepositoryPingTime > 30)
            {
                Logs.Explorer.LogWarning($"Repository ping exceeded 30 seconds ({(int)status.RepositoryPingTime}), please report the issue to NBXplorer developers");
            }

            if (blockchainInfo != null)
            {
                status.BitcoinStatus = new BitcoinStatus()
                {
                    IsSynched            = !waiter.IsSynchingCore(blockchainInfo),
                    Blocks               = (int)blockchainInfo.Blocks,
                    Headers              = (int)blockchainInfo.Headers,
                    VerificationProgress = blockchainInfo.VerificationProgress,
                    MinRelayTxFee        = new FeeRate(Money.Coins((decimal)networkInfo.relayfee), 1000),
                    IncrementalRelayFee  = new FeeRate(Money.Coins((decimal)networkInfo.incrementalfee), 1000),
                    Capabilities         = new NodeCapabilities()
                    {
                        CanScanTxoutSet  = waiter.RPC.Capabilities.SupportScanUTXOSet,
                        CanSupportSegwit = waiter.RPC.Capabilities.SupportSegwit
                    }
                };
                status.IsFullySynched &= status.BitcoinStatus.IsSynched;
            }
            status.ChainHeight     = chain.Height;
            status.SyncHeight      = location == null ? (int?)null : chain.FindFork(location).Height;
            status.IsFullySynched &= blockchainInfo != null &&
                                     waiter.State == BitcoinDWaiterState.Ready &&
                                     status.SyncHeight.HasValue &&
                                     blockchainInfo.Headers - status.SyncHeight.Value < 3;
            return(Json(status));
        }
Exemple #5
0
        public async Task <GetFeeRateResult> GetFeeRate(int blockCount, string cryptoCode)
        {
            var network = GetNetwork(cryptoCode, true);

            if (!network.SupportEstimatesSmartFee)
            {
                throw new NBXplorerError(400, "fee-estimation-unavailable", $"{cryptoCode} does not support estimatesmartfee").AsException();
            }
            var waiter = Waiters.GetWaiter(network);
            var result = await waiter.RPC.SendCommandAsync("estimatesmartfee", blockCount);

            var obj             = (JObject)result.Result;
            var feeRateProperty = obj.Property("feerate");
            var rate            = feeRateProperty == null ? (decimal) - 1 : obj["feerate"].Value <decimal>();

            if (rate == -1)
            {
                throw new NBXplorerError(400, "fee-estimation-unavailable", $"It is currently impossible to estimate fees, please try again later.").AsException();
            }
            return(new GetFeeRateResult()
            {
                FeeRate = new FeeRate(Money.Coins(Math.Round(rate / 1000, 8)), 1),
                BlockCount = obj["blocks"].Value <int>()
            });
        }
        private async Task UpdatePSBTCore(UpdatePSBTRequest update, NBXplorerNetwork network)
        {
            var repo = RepositoryProvider.GetRepository(network);
            var rpc  = Waiters.GetWaiter(network);

            await UpdateUTXO(update, repo, rpc);

            if (update.DerivationScheme is DerivationStrategyBase derivationScheme)
            {
                if (update.IncludeGlobalXPub is true)
                {
                    foreach (var extpub in derivationScheme.GetExtPubKeys().Select(e => e.GetWif(network.NBitcoinNetwork)))
                    {
                        update.PSBT.GlobalXPubs.AddOrReplace(extpub, new RootedKeyPath(extpub, new KeyPath()));
                    }
                }
                await UpdateHDKeyPathsWitnessAndRedeem(update, repo);
            }
            if (!update.AlwaysIncludeNonWitnessUTXO)
            {
                foreach (var input in update.PSBT.Inputs)
                {
                    input.TrySlimUTXO();
                }
            }


            HashSet <PubKey> rebased = new HashSet <PubKey>();

            if (update.RebaseKeyPaths != null)
            {
                if (update.RebaseKeyPaths.Any(r => r.AccountKey is null))
                {
                    throw new NBXplorerException(new NBXplorerError(400, "missing-parameter", "rebaseKeyPaths[].accountKey is missing"));
                }
                foreach (var rebase in update.RebaseKeyPaths.Where(r => rebased.Add(r.AccountKey.GetPublicKey())))
                {
                    if (rebase.AccountKeyPath == null)
                    {
                        throw new NBXplorerException(new NBXplorerError(400, "missing-parameter", "rebaseKeyPaths[].accountKeyPath is missing"));
                    }
                    update.PSBT.RebaseKeyPaths(rebase.AccountKey, rebase.AccountKeyPath);
                }
            }

            if (update.DerivationScheme is DerivationStrategyBase derivationScheme2)
            {
                var accountKeyPath = await repo.GetMetadata <RootedKeyPath>(
                    new DerivationSchemeTrackedSource(derivationScheme2), WellknownMetadataKeys.AccountKeyPath);

                if (accountKeyPath != null)
                {
                    foreach (var pubkey in derivationScheme2.GetExtPubKeys().Where(p => rebased.Add(p.PubKey)))
                    {
                        update.PSBT.RebaseKeyPaths(pubkey, accountKeyPath);
                    }
                }
            }
        }
Exemple #7
0
        private BitcoinDWaiter GetWaiter(NBXplorerNetwork network)
        {
            var waiter = Waiters.GetWaiter(network);

            if (!waiter.RPCAvailable)
            {
                throw RPCUnavailable();
            }
            return(waiter);
        }
Exemple #8
0
        private NBXplorerNetwork GetNetwork(string cryptoCode)
        {
            if (cryptoCode == null)
            {
                throw new ArgumentNullException(nameof(cryptoCode));
            }
            cryptoCode = cryptoCode.ToUpperInvariant();
            var network = Waiters.GetWaiter(cryptoCode)?.Network;

            if (network == null)
            {
                throw new NBXplorerException(new NBXplorerError(404, "cryptoCode-not-supported", $"{cryptoCode} is not supported"));
            }
            return(network);
        }
Exemple #9
0
        public async Task <IActionResult> Rescan(string cryptoCode, [FromBody] RescanRequest rescanRequest)
        {
            if (rescanRequest == null)
            {
                throw new ArgumentNullException(nameof(rescanRequest));
            }
            if (rescanRequest?.Transactions == null)
            {
                throw new NBXplorerException(new NBXplorerError(400, "transactions-missing", "You must specify 'transactions'"));
            }

            bool willFetchTransactions = rescanRequest.Transactions.Any(t => t.Transaction == null);
            bool needTxIndex           = rescanRequest.Transactions.Any(t => t.Transaction == null && t.BlockId == null);
            var  network = GetNetwork(cryptoCode, willFetchTransactions);

            var rpc  = Waiters.GetWaiter(cryptoCode).RPC.PrepareBatch();
            var repo = RepositoryProvider.GetRepository(network);

            var fetchingTransactions = rescanRequest
                                       .Transactions
                                       .Select(t => FetchTransaction(rpc, t))
                                       .ToArray();

            await rpc.SendBatchAsync();

            await Task.WhenAll(fetchingTransactions);

            var transactions = fetchingTransactions.Select(t => t.GetAwaiter().GetResult())
                               .Where(tx => tx.Transaction != null)
                               .ToArray();

            foreach (var txs in transactions.GroupBy(t => t.BlockId, t => (t.Transaction, t.BlockTime))
                     .OrderBy(t => t.First().BlockTime))
            {
                repo.SaveTransactions(txs.First().BlockTime, txs.Select(t => t.Transaction).ToArray(), txs.Key);
                foreach (var tx in txs)
                {
                    var matches = repo.GetMatches(tx.Transaction).Select(m => new MatchedTransaction()
                    {
                        BlockId = txs.Key, Match = m
                    }).ToArray();
                    repo.SaveMatches(tx.BlockTime, matches);
                    AddressPoolService.RefillAddressPoolIfNeeded(network, matches);
                }
            }

            return(Ok());
        }
Exemple #10
0
        public async Task <GetFeeRateResult> GetFeeRate(int blockCount, string cryptoCode)
        {
            var network = GetNetwork(cryptoCode, true);

            if (!network.SupportEstimatesSmartFee)
            {
                throw new NBXplorerError(400, "fee-estimation-unavailable", $"{cryptoCode} does not support estimatesmartfee").AsException();
            }
            var waiter = Waiters.GetWaiter(network);
            var rate   = await waiter.RPC.GetFeeRateAsyncEx(blockCount);

            if (rate == null)
            {
                throw new NBXplorerError(400, "fee-estimation-unavailable", $"It is currently impossible to estimate fees, please try again later.").AsException();
            }
            return(rate);
        }
Exemple #11
0
        public async Task <GetFeeRateResult> GetFeeRate(int blockCount, string cryptoCode)
        {
            var network = GetNetwork(cryptoCode, true);
            var waiter  = Waiters.GetWaiter(network);
            EstimateSmartFeeResponse rate = null;

            try
            {
                rate = await waiter.RPC.TryEstimateSmartFeeAsync(blockCount);
            }
            catch (RPCException ex) when(ex.RPCCode == RPCErrorCode.RPC_METHOD_NOT_FOUND)
            {
            }
            if (rate == null)
            {
                throw new NBXplorerError(400, "fee-estimation-unavailable", $"It is currently impossible to estimate fees, please try again later.").AsException();
            }
            return(new GetFeeRateResult()
            {
                BlockCount = rate.Blocks, FeeRate = rate.FeeRate
            });
        }
Exemple #12
0
        private NBXplorerNetwork GetNetwork(string cryptoCode, bool checkRPC)
        {
            if (cryptoCode == null)
            {
                throw new ArgumentNullException(nameof(cryptoCode));
            }
            cryptoCode = cryptoCode.ToUpperInvariant();
            var network = Waiters.GetWaiter(cryptoCode)?.Network;

            if (network == null)
            {
                throw new NBXplorerException(new NBXplorerError(404, "cryptoCode-not-supported", $"{cryptoCode} is not supported"));
            }

            if (checkRPC)
            {
                var waiter = Waiters.GetWaiter(network);
                if (waiter == null || !waiter.RPCAvailable || waiter.RPC.Capabilities == null)
                {
                    throw new NBXplorerError(400, "rpc-unavailable", $"The RPC interface is currently not available.").AsException();
                }
            }
            return(network);
        }
Exemple #13
0
        public async Task <IActionResult> GetStatus(string cryptoCode)
        {
            var network = GetNetwork(cryptoCode, false);
            var waiter  = Waiters.GetWaiter(network);
            var chain   = ChainProvider.GetChain(network);
            var repo    = RepositoryProvider.GetRepository(network);

            var location = waiter.GetLocation();
            GetBlockchainInfoResponse blockchainInfo = null;

            if (waiter.RPCAvailable)
            {
                try
                {
                    var rpc = waiter.RPC.Clone();
                    rpc.RequestTimeout = TimeSpan.FromMinutes(1.0);
                    blockchainInfo     = await rpc.GetBlockchainInfoAsyncEx();
                }
                catch (OperationCanceledException)                // Timeout, can happen if core is really busy
                {
                }
            }

            var status = new StatusResult()
            {
                NetworkType          = network.NBitcoinNetwork.NetworkType,
                CryptoCode           = network.CryptoCode,
                Version              = typeof(MainController).GetTypeInfo().Assembly.GetCustomAttribute <AssemblyFileVersionAttribute>().Version,
                SupportedCryptoCodes = Waiters.All().Select(w => w.Network.CryptoCode).ToArray(),
                IsFullySynched       = true
            };

            GetNetworkInfoResponse networkInfo = waiter.NetworkInfo;

            if (blockchainInfo != null && networkInfo != null)
            {
                status.BitcoinStatus = new BitcoinStatus()
                {
                    IsSynched            = !waiter.IsSynchingCore(blockchainInfo),
                    Blocks               = (int)blockchainInfo.Blocks,
                    Headers              = (int)blockchainInfo.Headers,
                    VerificationProgress = blockchainInfo.VerificationProgress,
                    MinRelayTxFee        = networkInfo.GetRelayFee(),
                    IncrementalRelayFee  = networkInfo.GetIncrementalFee(),
                    Capabilities         = new NodeCapabilities()
                    {
                        CanScanTxoutSet  = waiter.RPC.Capabilities.SupportScanUTXOSet,
                        CanSupportSegwit = waiter.RPC.Capabilities.SupportSegwit
                    },
                    ExternalAddresses = (networkInfo.localaddresses ?? Array.Empty <GetNetworkInfoResponse.LocalAddress>())
                                        .Select(l => $"{l.address}:{l.port}").ToArray()
                };
                status.IsFullySynched &= status.BitcoinStatus.IsSynched;
            }
            status.ChainHeight     = chain.Height;
            status.SyncHeight      = location == null ? (int?)null : chain.FindFork(location).Height;
            status.IsFullySynched &= blockchainInfo != null &&
                                     waiter.State == BitcoinDWaiterState.Ready &&
                                     status.SyncHeight.HasValue &&
                                     blockchainInfo.Headers - status.SyncHeight.Value < 3;
            if (status.IsFullySynched)
            {
                var now = DateTimeOffset.UtcNow;
                await repo.Ping();

                var pingAfter = DateTimeOffset.UtcNow;
                status.RepositoryPingTime = (pingAfter - now).TotalSeconds;
                if (status.RepositoryPingTime > 30)
                {
                    Logs.Explorer.LogWarning($"Repository ping exceeded 30 seconds ({(int)status.RepositoryPingTime}), please report the issue to NBXplorer developers");
                }
            }
            return(Json(status));
        }
        public async Task <IActionResult> CreatePSBT(
            [ModelBinder(BinderType = typeof(NetworkModelBinder))]
            NBXplorerNetwork network,
            [ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))]
            DerivationStrategyBase strategy,
            [FromBody]
            JObject body)
        {
            if (body == null)
            {
                throw new ArgumentNullException(nameof(body));
            }
            CreatePSBTRequest request = ParseJObject <CreatePSBTRequest>(body, network);

            if (strategy == null)
            {
                throw new ArgumentNullException(nameof(strategy));
            }

            var repo      = RepositoryProvider.GetRepository(network);
            var txBuilder = request.Seed is int s?network.NBitcoinNetwork.CreateTransactionBuilder(s)
                                : network.NBitcoinNetwork.CreateTransactionBuilder();

            CreatePSBTSuggestions suggestions = null;

            if (!(request.DisableFingerprintRandomization is true) &&
                fingerprintService.GetDistribution(network) is FingerprintDistribution distribution)
            {
                suggestions ??= new CreatePSBTSuggestions();
                var known = new List <(Fingerprint feature, bool value)>();
                if (request.RBF is bool rbf)
                {
                    known.Add((Fingerprint.RBF, rbf));
                }
                if (request.DiscourageFeeSniping is bool feeSnipping)
                {
                    known.Add((Fingerprint.FeeSniping, feeSnipping));
                }
                if (request.LockTime is LockTime l)
                {
                    if (l == LockTime.Zero)
                    {
                        known.Add((Fingerprint.TimelockZero, true));
                    }
                }
                if (request.Version is uint version)
                {
                    if (version == 1)
                    {
                        known.Add((Fingerprint.V1, true));
                    }
                    if (version == 2)
                    {
                        known.Add((Fingerprint.V2, true));
                    }
                }
                known.Add((Fingerprint.SpendFromMixed, false));
                known.Add((Fingerprint.SequenceMixed, false));
                if (strategy is DirectDerivationStrategy direct)
                {
                    if (direct.Segwit)
                    {
                        known.Add((Fingerprint.SpendFromP2WPKH, true));
                    }
                    else
                    {
                        known.Add((Fingerprint.SpendFromP2PKH, true));
                    }
                }
                else
                {
                    // TODO: What if multisig? For now we consider it p2wpkh
                    known.Add((Fingerprint.SpendFromP2SHP2WPKH, true));
                }

                Fingerprint fingerprint = distribution.PickFingerprint(txBuilder.ShuffleRandom);
                try
                {
                    fingerprint = distribution.KnowingThat(known.ToArray())
                                  .PickFingerprint(txBuilder.ShuffleRandom);
                }
                catch (InvalidOperationException)
                {
                }

                request.RBF ??= fingerprint.HasFlag(Fingerprint.RBF);
                request.DiscourageFeeSniping ??= fingerprint.HasFlag(Fingerprint.FeeSniping);
                if (request.LockTime is null && fingerprint.HasFlag(Fingerprint.TimelockZero))
                {
                    request.LockTime = new LockTime(0);
                }
                if (request.Version is null && fingerprint.HasFlag(Fingerprint.V1))
                {
                    request.Version = 1;
                }
                if (request.Version is null && fingerprint.HasFlag(Fingerprint.V2))
                {
                    request.Version = 2;
                }
                suggestions.ShouldEnforceLowR = fingerprint.HasFlag(Fingerprint.LowR);
            }

            var waiter = Waiters.GetWaiter(network);

            if (waiter.NetworkInfo?.GetRelayFee() is FeeRate feeRate)
            {
                txBuilder.StandardTransactionPolicy.MinRelayTxFee = feeRate;
            }

            txBuilder.OptInRBF = !(request.RBF is false);
            if (request.LockTime is LockTime lockTime)
            {
                txBuilder.SetLockTime(lockTime);
            }
            // Discourage fee sniping.
            //
            // For a large miner the value of the transactions in the best block and
            // the mempool can exceed the cost of deliberately attempting to mine two
            // blocks to orphan the current best block. By setting nLockTime such that
            // only the next block can include the transaction, we discourage this
            // practice as the height restricted and limited blocksize gives miners
            // considering fee sniping fewer options for pulling off this attack.
            //
            // A simple way to think about this is from the wallet's point of view we
            // always want the blockchain to move forward. By setting nLockTime this
            // way we're basically making the statement that we only want this
            // transaction to appear in the next block; we don't want to potentially
            // encourage reorgs by allowing transactions to appear at lower heights
            // than the next block in forks of the best chain.
            //
            // Of course, the subsidy is high enough, and transaction volume low
            // enough, that fee sniping isn't a problem yet, but by implementing a fix
            // now we ensure code won't be written that makes assumptions about
            // nLockTime that preclude a fix later.
            else if (!(request.DiscourageFeeSniping is false))
            {
                if (waiter.State is BitcoinDWaiterState.Ready)
                {
                    int blockHeight = ChainProvider.GetChain(network).Height;
                    // Secondly occasionally randomly pick a nLockTime even further back, so
                    // that transactions that are delayed after signing for whatever reason,
                    // e.g. high-latency mix networks and some CoinJoin implementations, have
                    // better privacy.
                    if (txBuilder.ShuffleRandom.Next(0, 10) == 0)
                    {
                        blockHeight = Math.Max(0, blockHeight - txBuilder.ShuffleRandom.Next(0, 100));
                    }
                    txBuilder.SetLockTime(new LockTime(blockHeight));
                }
                else
                {
                    txBuilder.SetLockTime(new LockTime(0));
                }
            }
            var utxos = (await GetUTXOs(network.CryptoCode, strategy, null)).As <UTXOChanges>().GetUnspentUTXOs(request.MinConfirmations);
            var availableCoinsByOutpoint = utxos.ToDictionary(o => o.Outpoint);

            if (request.IncludeOnlyOutpoints != null)
            {
                var includeOnlyOutpoints = request.IncludeOnlyOutpoints.ToHashSet();
                availableCoinsByOutpoint = availableCoinsByOutpoint.Where(c => includeOnlyOutpoints.Contains(c.Key)).ToDictionary(o => o.Key, o => o.Value);
            }

            if (request.ExcludeOutpoints?.Any() is true)
            {
                var excludedOutpoints = request.ExcludeOutpoints.ToHashSet();
                availableCoinsByOutpoint = availableCoinsByOutpoint.Where(c => !excludedOutpoints.Contains(c.Key)).ToDictionary(o => o.Key, o => o.Value);
            }

            if (request.MinValue != null)
            {
                availableCoinsByOutpoint = availableCoinsByOutpoint.Where(c => request.MinValue >= (Money)c.Value.Value).ToDictionary(o => o.Key, o => o.Value);
            }

            ICoin[] coins = null;
            if (strategy.GetDerivation().Redeem != null)
            {
                // We need to add the redeem script to the coins
                var hdKeys = strategy.AsHDRedeemScriptPubKey().AsHDKeyCache();
                var arr    = availableCoinsByOutpoint.Values.ToArray();
                coins = new ICoin[arr.Length];
                // Can be very intense CPU wise
                Parallel.For(0, coins.Length, i =>
                {
                    coins[i] = ((Coin)arr[i].AsCoin()).ToScriptCoin(hdKeys.Derive(arr[i].KeyPath).ScriptPubKey);
                });
            }
            else
            {
                coins = availableCoinsByOutpoint.Values.Select(v => v.AsCoin()).ToArray();
            }
            txBuilder.AddCoins(coins);

            foreach (var dest in request.Destinations)
            {
                if (dest.SweepAll)
                {
                    try
                    {
                        txBuilder.SendAll(dest.Destination);
                    }
                    catch
                    {
                        throw new NBXplorerException(new NBXplorerError(400, "not-enough-funds", "You can't sweep funds, because you don't have any."));
                    }
                }
                else
                {
                    txBuilder.Send(dest.Destination, dest.Amount);
                    if (dest.SubstractFees)
                    {
                        try
                        {
                            txBuilder.SubtractFees();
                        }
                        catch
                        {
                            throw new NBXplorerException(new NBXplorerError(400, "not-enough-funds", "You can't substract fee on this destination, because not enough money was sent to it"));
                        }
                    }
                }
            }
            (Script ScriptPubKey, KeyPath KeyPath)change = (null, null);
            bool hasChange = false;

            if (request.ExplicitChangeAddress == null)
            {
                var keyInfo = await repo.GetUnused(strategy, DerivationFeature.Change, 0, false);

                change = (keyInfo.ScriptPubKey, keyInfo.KeyPath);
            }
            else
            {
                // The provided explicit change might have a known keyPath, let's change for it
                KeyPath keyPath  = null;
                var     keyInfos = await repo.GetKeyInformations(new[] { request.ExplicitChangeAddress.ScriptPubKey });

                if (keyInfos.TryGetValue(request.ExplicitChangeAddress.ScriptPubKey, out var kis))
                {
                    keyPath = kis.FirstOrDefault(k => k.DerivationStrategy == strategy)?.KeyPath;
                }
                change = (request.ExplicitChangeAddress.ScriptPubKey, keyPath);
            }
            txBuilder.SetChange(change.ScriptPubKey);
            PSBT psbt = null;

            try
            {
                if (request.FeePreference?.ExplicitFeeRate is FeeRate explicitFeeRate)
                {
                    txBuilder.SendEstimatedFees(explicitFeeRate);
                }
                else if (request.FeePreference?.BlockTarget is int blockTarget)
                {
                    try
                    {
                        var rate = await GetFeeRate(blockTarget, network.CryptoCode);

                        txBuilder.SendEstimatedFees(rate.FeeRate);
                    }
                    catch (NBXplorerException e) when(e.Error.Code == "fee-estimation-unavailable" && request.FeePreference?.FallbackFeeRate is FeeRate fallbackFeeRate)
                    {
                        txBuilder.SendEstimatedFees(fallbackFeeRate);
                    }
                }
                else if (request.FeePreference?.ExplicitFee is Money explicitFee)
                {
                    txBuilder.SendFees(explicitFee);
                }
                else
                {
                    try
                    {
                        var rate = await GetFeeRate(1, network.CryptoCode);

                        txBuilder.SendEstimatedFees(rate.FeeRate);
                    }
                    catch (NBXplorerException e) when(e.Error.Code == "fee-estimation-unavailable" && request.FeePreference?.FallbackFeeRate is FeeRate fallbackFeeRate)
                    {
                        txBuilder.SendEstimatedFees(fallbackFeeRate);
                    }
                }
                psbt      = txBuilder.BuildPSBT(false);
                hasChange = psbt.Outputs.Any(o => o.ScriptPubKey == change.ScriptPubKey);
            }
            catch (NotEnoughFundsException)
            {
                throw new NBXplorerException(new NBXplorerError(400, "not-enough-funds", "Not enough funds for doing this transaction"));
            }
            // We made sure we can build the PSBT, so now we can reserve the change address if we need to
            if (hasChange && request.ExplicitChangeAddress == null && request.ReserveChangeAddress)
            {
                var derivation = await repo.GetUnused(strategy, DerivationFeature.Change, 0, true);

                // In most of the time, this is the same as previously, so no need to rebuild PSBT
                if (derivation.ScriptPubKey != change.ScriptPubKey)
                {
                    change = (derivation.ScriptPubKey, derivation.KeyPath);
                    txBuilder.SetChange(change.ScriptPubKey);
                    psbt = txBuilder.BuildPSBT(false);
                }
            }

            var tx = psbt.GetOriginalTransaction();

            if (request.Version is uint v)
            {
                tx.Version = v;
            }
            psbt = txBuilder.CreatePSBTFrom(tx, false, SigHash.All);

            var update = new UpdatePSBTRequest()
            {
                DerivationScheme            = strategy,
                PSBT                        = psbt,
                RebaseKeyPaths              = request.RebaseKeyPaths,
                AlwaysIncludeNonWitnessUTXO = request.AlwaysIncludeNonWitnessUTXO,
                IncludeGlobalXPub           = request.IncludeGlobalXPub
            };

            await UpdatePSBTCore(update, network);

            var resp = new CreatePSBTResponse()
            {
                PSBT          = update.PSBT,
                ChangeAddress = hasChange ? change.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork) : null,
                Suggestions   = suggestions
            };

            return(Json(resp, network.JsonSerializerSettings));
        }
        public async Task <IActionResult> CreatePSBT(
            [ModelBinder(BinderType = typeof(NetworkModelBinder))]
            NBXplorerNetwork network,
            [ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))]
            DerivationStrategyBase strategy,
            [FromBody]
            JObject body)
        {
            if (body == null)
            {
                throw new ArgumentNullException(nameof(body));
            }
            CreatePSBTRequest request = ParseJObject <CreatePSBTRequest>(body, network);

            if (strategy == null)
            {
                throw new ArgumentNullException(nameof(strategy));
            }
            var repo      = RepositoryProvider.GetRepository(network);
            var txBuilder = request.Seed is int s?network.NBitcoinNetwork.CreateTransactionBuilder(s)
                                : network.NBitcoinNetwork.CreateTransactionBuilder();

            if (Waiters.GetWaiter(network).NetworkInfo?.GetRelayFee() is FeeRate feeRate)
            {
                txBuilder.StandardTransactionPolicy.MinRelayTxFee = feeRate;
            }

            txBuilder.OptInRBF = request.RBF;
            if (request.LockTime is LockTime lockTime)
            {
                txBuilder.SetLockTime(lockTime);
                txBuilder.OptInRBF = true;
            }
            var utxos = (await GetUTXOs(network.CryptoCode, strategy, null)).GetUnspentCoins(request.MinConfirmations);
            var availableCoinsByOutpoint = utxos.ToDictionary(o => o.Outpoint);

            if (request.IncludeOnlyOutpoints != null)
            {
                var includeOnlyOutpoints = request.IncludeOnlyOutpoints.ToHashSet();
                availableCoinsByOutpoint = availableCoinsByOutpoint.Where(c => includeOnlyOutpoints.Contains(c.Key)).ToDictionary(o => o.Key, o => o.Value);
            }

            if (request.ExcludeOutpoints?.Any() is true)
            {
                var excludedOutpoints = request.ExcludeOutpoints.ToHashSet();
                availableCoinsByOutpoint = availableCoinsByOutpoint.Where(c => !excludedOutpoints.Contains(c.Key)).ToDictionary(o => o.Key, o => o.Value);
            }
            txBuilder.AddCoins(availableCoinsByOutpoint.Values);

            foreach (var dest in request.Destinations)
            {
                if (dest.SweepAll)
                {
                    txBuilder.SendAll(dest.Destination);
                }
                else
                {
                    txBuilder.Send(dest.Destination, dest.Amount);
                    if (dest.SubstractFees)
                    {
                        txBuilder.SubtractFees();
                    }
                }
            }
            (Script ScriptPubKey, KeyPath KeyPath)change = (null, null);
            bool hasChange = false;

            // We first build the transaction with a change which keep the length of the expected change scriptPubKey
            // This allow us to detect if there is a change later in the constructed transaction.
            // This defend against bug which can happen if one of the destination is the same as the expected change
            // This assume that a script with only 0 can't be created from a strategy, nor by passing any data to explicitChangeAddress
            if (request.ExplicitChangeAddress == null)
            {
                // The dummyScriptPubKey is necessary to know the size of the change
                var dummyScriptPubKey = utxos.FirstOrDefault()?.ScriptPubKey ?? strategy.GetDerivation(0).ScriptPubKey;
                change = (Script.FromBytesUnsafe(new byte[dummyScriptPubKey.Length]), null);
            }
            else
            {
                change = (Script.FromBytesUnsafe(new byte[request.ExplicitChangeAddress.ScriptPubKey.Length]), null);
            }
            txBuilder.SetChange(change.ScriptPubKey);
            PSBT psbt = null;

            try
            {
                if (request.FeePreference?.ExplicitFeeRate is FeeRate explicitFeeRate)
                {
                    txBuilder.SendEstimatedFees(explicitFeeRate);
                }
                else if (request.FeePreference?.BlockTarget is int blockTarget)
                {
                    try
                    {
                        var rate = await GetFeeRate(blockTarget, network.CryptoCode);

                        txBuilder.SendEstimatedFees(rate.FeeRate);
                    }
                    catch (NBXplorerException e) when(e.Error.Code == "fee-estimation-unavailable" && request.FeePreference?.FallbackFeeRate is FeeRate fallbackFeeRate)
                    {
                        txBuilder.SendEstimatedFees(fallbackFeeRate);
                    }
                }
                else if (request.FeePreference?.ExplicitFee is Money explicitFee)
                {
                    txBuilder.SendFees(explicitFee);
                }
                else
                {
                    try
                    {
                        var rate = await GetFeeRate(1, network.CryptoCode);

                        txBuilder.SendEstimatedFees(rate.FeeRate);
                    }
                    catch (NBXplorerException e) when(e.Error.Code == "fee-estimation-unavailable" && request.FeePreference?.FallbackFeeRate is FeeRate fallbackFeeRate)
                    {
                        txBuilder.SendEstimatedFees(fallbackFeeRate);
                    }
                }
                psbt      = txBuilder.BuildPSBT(false);
                hasChange = psbt.Outputs.Any(o => o.ScriptPubKey == change.ScriptPubKey);
            }
            catch (NotEnoughFundsException)
            {
                throw new NBXplorerException(new NBXplorerError(400, "not-enough-funds", "Not enough funds for doing this transaction"));
            }
            if (hasChange)             // We need to reserve an address, so we need to build again the psbt
            {
                if (request.ExplicitChangeAddress == null)
                {
                    var derivation = await repo.GetUnused(strategy, DerivationFeature.Change, 0, request.ReserveChangeAddress);

                    change = (derivation.ScriptPubKey, derivation.KeyPath);
                }
                else
                {
                    change = (request.ExplicitChangeAddress.ScriptPubKey, null);
                }
                txBuilder.SetChange(change.ScriptPubKey);
                psbt = txBuilder.BuildPSBT(false);
            }

            var tx = psbt.GetOriginalTransaction();

            if (request.Version is uint v)
            {
                tx.Version = v;
            }
            psbt = txBuilder.CreatePSBTFrom(tx, false, SigHash.All);

            // Maybe it is a change that we know about, let's search in the DB
            if (hasChange && change.KeyPath == null)
            {
                var keyInfos = await repo.GetKeyInformations(new[] { request.ExplicitChangeAddress.ScriptPubKey });

                if (keyInfos.TryGetValue(request.ExplicitChangeAddress.ScriptPubKey, out var kis))
                {
                    var ki = kis.FirstOrDefault(k => k.DerivationStrategy == strategy);
                    if (ki != null)
                    {
                        change = (change.ScriptPubKey, kis.First().KeyPath);
                    }
                }
            }

            await UpdatePSBTCore(new UpdatePSBTRequest()
            {
                DerivationScheme = strategy,
                PSBT             = psbt,
                RebaseKeyPaths   = request.RebaseKeyPaths
            }, network);

            var resp = new CreatePSBTResponse()
            {
                PSBT          = psbt,
                ChangeAddress = hasChange ? change.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork) : null
            };

            return(Json(resp, network.JsonSerializerSettings));
        }
Exemple #16
0
        public async Task <IActionResult> ConnectWebSocket(
            string cryptoCode,
            bool includeTransaction        = true,
            CancellationToken cancellation = default)
        {
            if (!HttpContext.WebSockets.IsWebSocketRequest)
            {
                return(NotFound());
            }

            GetNetwork(cryptoCode, false);             // Internally check if cryptoCode is correct

            string listenAllDerivationSchemes = null;
            string listenAllTrackedSource     = null;
            var    listenedBlocks             = new ConcurrentDictionary <string, string>();
            var    listenedDerivations        = new ConcurrentDictionary <(Network, DerivationStrategyBase), DerivationStrategyBase>();
            var    listenedTrackedSource      = new ConcurrentDictionary <(Network, TrackedSource), TrackedSource>();

            WebsocketMessageListener server        = new WebsocketMessageListener(await HttpContext.WebSockets.AcceptWebSocketAsync(), _SerializerSettings);
            CompositeDisposable      subscriptions = new CompositeDisposable();

            subscriptions.Add(_EventAggregator.Subscribe <Models.NewBlockEvent>(async o =>
            {
                if (listenedBlocks.ContainsKey(o.CryptoCode))
                {
                    await server.Send(o);
                }
            }));
            subscriptions.Add(_EventAggregator.Subscribe <Models.NewTransactionEvent>(async o =>
            {
                var network = Waiters.GetWaiter(o.CryptoCode);
                if (network == null)
                {
                    return;
                }

                bool forward         = false;
                var derivationScheme = (o.TrackedSource as DerivationSchemeTrackedSource)?.DerivationStrategy;
                if (derivationScheme != null)
                {
                    forward |= listenAllDerivationSchemes == "*" ||
                               listenAllDerivationSchemes == o.CryptoCode ||
                               listenedDerivations.ContainsKey((network.Network.NBitcoinNetwork, derivationScheme));
                }

                forward |= listenAllTrackedSource == "*" || listenAllTrackedSource == o.CryptoCode ||
                           listenedTrackedSource.ContainsKey((network.Network.NBitcoinNetwork, o.TrackedSource));

                if (forward)
                {
                    var derivation = (o.TrackedSource as DerivationSchemeTrackedSource)?.DerivationStrategy;
                    await server.Send(o);
                }
            }));
            try
            {
                while (server.Socket.State == WebSocketState.Open)
                {
                    object message = await server.NextMessageAsync(cancellation);

                    switch (message)
                    {
                    case Models.NewBlockEventRequest r:
                        r.CryptoCode = r.CryptoCode ?? cryptoCode;
                        listenedBlocks.TryAdd(r.CryptoCode, r.CryptoCode);
                        break;

                    case Models.NewTransactionEventRequest r:
                        var network = Waiters.GetWaiter(r.CryptoCode)?.Network;
                        if (r.DerivationSchemes != null)
                        {
                            r.CryptoCode = r.CryptoCode ?? cryptoCode;
                            if (network != null)
                            {
                                foreach (var derivation in r.DerivationSchemes)
                                {
                                    var parsed = new DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivation);
                                    listenedDerivations.TryAdd((network.NBitcoinNetwork, parsed), parsed);
                                }
                            }
                        }
                        else if (
                            // Back compat: If no derivation scheme precised and ListenAllDerivationSchemes not set, we listen all
                            (r.TrackedSources == null && r.ListenAllDerivationSchemes == null) ||
                            (r.ListenAllDerivationSchemes != null && r.ListenAllDerivationSchemes.Value))
                        {
                            listenAllDerivationSchemes = r.CryptoCode;
                        }

                        if (r.ListenAllTrackedSource != null && r.ListenAllTrackedSource.Value)
                        {
                            listenAllTrackedSource = r.CryptoCode;
                        }
                        else if (r.TrackedSources != null)
                        {
                            r.CryptoCode = r.CryptoCode ?? cryptoCode;
                            if (network != null)
                            {
                                foreach (var trackedSource in r.TrackedSources)
                                {
                                    if (TrackedSource.TryParse(trackedSource, out var parsed, network.NBitcoinNetwork))
                                    {
                                        listenedTrackedSource.TryAdd((network.NBitcoinNetwork, parsed), parsed);
                                    }
                                }
                            }
                        }

                        break;

                    default:
                        break;
                    }
                }
            }
            catch when(server.Socket.State != WebSocketState.Open)
            {
            }
            finally { subscriptions.Dispose(); await server.DisposeAsync(cancellation); }
            return(new EmptyResult());
        }
Exemple #17
0
        public async Task <IActionResult> ConnectWebSocket(
            string cryptoCode,
            bool includeTransaction        = true,
            CancellationToken cancellation = default)
        {
            if (!HttpContext.WebSockets.IsWebSocketRequest)
            {
                return(NotFound());
            }

            GetNetwork(cryptoCode);             // Internally check if cryptoCode is correct

            string listenAllDerivationSchemes = null;
            var    listenedBlocks             = new ConcurrentDictionary <string, string>();
            var    listenedDerivations        = new ConcurrentDictionary <(Network, DerivationStrategyBase), DerivationStrategyBase>();

            WebsocketMessageListener server        = new WebsocketMessageListener(await HttpContext.WebSockets.AcceptWebSocketAsync(), _SerializerSettings);
            CompositeDisposable      subscriptions = new CompositeDisposable();

            subscriptions.Add(_EventAggregator.Subscribe <Events.NewBlockEvent>(async o =>
            {
                if (listenedBlocks.ContainsKey(o.CryptoCode))
                {
                    var chain = ChainProvider.GetChain(o.CryptoCode);
                    if (chain == null)
                    {
                        return;
                    }
                    var block = chain.GetBlock(o.BlockId);
                    if (block != null)
                    {
                        await server.Send(new Models.NewBlockEvent()
                        {
                            CryptoCode        = o.CryptoCode,
                            Hash              = block.HashBlock,
                            Height            = block.Height,
                            PreviousBlockHash = block?.Previous.HashBlock
                        });
                    }
                }
            }));
            subscriptions.Add(_EventAggregator.Subscribe <Events.NewTransactionMatchEvent>(async o =>
            {
                var network = Waiters.GetWaiter(o.CryptoCode);
                if (network == null)
                {
                    return;
                }
                if (
                    listenAllDerivationSchemes == "*" ||
                    listenAllDerivationSchemes == o.CryptoCode ||
                    listenedDerivations.ContainsKey((network.Network.NBitcoinNetwork, o.Match.DerivationStrategy)))
                {
                    var chain = ChainProvider.GetChain(o.CryptoCode);
                    if (chain == null)
                    {
                        return;
                    }
                    var blockHeader = o.BlockId == null ? null : chain.GetBlock(o.BlockId);
                    await server.Send(new Models.NewTransactionEvent()
                    {
                        CryptoCode         = o.CryptoCode,
                        DerivationStrategy = o.Match.DerivationStrategy,
                        BlockId            = blockHeader?.HashBlock,
                        TransactionData    = ToTransactionResult(includeTransaction, chain, new[] { o.SavedTransaction }),
                        Inputs             = o.Match.Inputs,
                        Outputs            = o.Match.Outputs
                    });
                }
            }));
            try
            {
                while (server.Socket.State == WebSocketState.Open)
                {
                    object message = await server.NextMessageAsync(cancellation);

                    switch (message)
                    {
                    case Models.NewBlockEventRequest r:
                        r.CryptoCode = r.CryptoCode ?? cryptoCode;
                        listenedBlocks.TryAdd(r.CryptoCode, r.CryptoCode);
                        break;

                    case Models.NewTransactionEventRequest r:
                        if (r.DerivationSchemes != null)
                        {
                            r.CryptoCode = r.CryptoCode ?? cryptoCode;
                            var network = Waiters.GetWaiter(r.CryptoCode)?.Network;
                            if (network == null)
                            {
                                break;
                            }
                            foreach (var derivation in r.DerivationSchemes)
                            {
                                var parsed = new DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivation);
                                listenedDerivations.TryAdd((network.NBitcoinNetwork, parsed), parsed);
                            }
                        }
                        else
                        {
                            listenAllDerivationSchemes = r.CryptoCode;
                        }
                        break;

                    default:
                        break;
                    }
                }
            }
            catch when(server.Socket.State != WebSocketState.Open)
            {
            }
            finally { subscriptions.Dispose(); await server.DisposeAsync(cancellation); }
            return(new EmptyResult());
        }
        public async Task <IActionResult> CreatePSBT(
            [ModelBinder(BinderType = typeof(NetworkModelBinder))]
            NBXplorerNetwork network,
            [ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))]
            DerivationStrategyBase strategy,
            [FromBody]
            JObject body)
        {
            if (body == null)
            {
                throw new ArgumentNullException(nameof(body));
            }
            CreatePSBTRequest request = ParseJObject <CreatePSBTRequest>(body, network);

            if (strategy == null)
            {
                throw new ArgumentNullException(nameof(strategy));
            }
            var repo      = RepositoryProvider.GetRepository(network);
            var txBuilder = request.Seed is int s?network.NBitcoinNetwork.CreateTransactionBuilder(s)
                                : network.NBitcoinNetwork.CreateTransactionBuilder();

            if (Waiters.GetWaiter(network).NetworkInfo?.GetRelayFee() is FeeRate feeRate)
            {
                txBuilder.StandardTransactionPolicy.MinRelayTxFee = feeRate;
            }

            txBuilder.OptInRBF = request.RBF;
            if (request.LockTime is LockTime lockTime)
            {
                txBuilder.SetLockTime(lockTime);
                txBuilder.OptInRBF = true;
            }
            var utxos = (await GetUTXOs(network.CryptoCode, strategy, null)).As <UTXOChanges>().GetUnspentCoins(request.MinConfirmations);
            var availableCoinsByOutpoint = utxos.ToDictionary(o => o.Outpoint);

            if (request.IncludeOnlyOutpoints != null)
            {
                var includeOnlyOutpoints = request.IncludeOnlyOutpoints.ToHashSet();
                availableCoinsByOutpoint = availableCoinsByOutpoint.Where(c => includeOnlyOutpoints.Contains(c.Key)).ToDictionary(o => o.Key, o => o.Value);
            }

            if (request.ExcludeOutpoints?.Any() is true)
            {
                var excludedOutpoints = request.ExcludeOutpoints.ToHashSet();
                availableCoinsByOutpoint = availableCoinsByOutpoint.Where(c => !excludedOutpoints.Contains(c.Key)).ToDictionary(o => o.Key, o => o.Value);
            }

            if (request.MinValue != null)
            {
                availableCoinsByOutpoint = availableCoinsByOutpoint.Where(c => request.MinValue >= c.Value.Amount).ToDictionary(o => o.Key, o => o.Value);
            }
            txBuilder.AddCoins(availableCoinsByOutpoint.Values);

            foreach (var dest in request.Destinations)
            {
                if (dest.SweepAll)
                {
                    try
                    {
                        txBuilder.SendAll(dest.Destination);
                    }
                    catch
                    {
                        throw new NBXplorerException(new NBXplorerError(400, "not-enough-funds", "You can't sweep funds, because you don't have any."));
                    }
                }
                else
                {
                    txBuilder.Send(dest.Destination, dest.Amount);
                    if (dest.SubstractFees)
                    {
                        try
                        {
                            txBuilder.SubtractFees();
                        }
                        catch
                        {
                            throw new NBXplorerException(new NBXplorerError(400, "not-enough-funds", "You can't substract fee on this destination, because not enough money was sent to it"));
                        }
                    }
                }
            }
            (Script ScriptPubKey, KeyPath KeyPath)change = (null, null);
            bool hasChange = false;

            if (request.ExplicitChangeAddress == null)
            {
                var keyInfo = await repo.GetUnused(strategy, DerivationFeature.Change, 0, false);

                change = (keyInfo.ScriptPubKey, keyInfo.KeyPath);
            }
            else
            {
                // The provided explicit change might have a known keyPath, let's change for it
                KeyPath keyPath  = null;
                var     keyInfos = await repo.GetKeyInformations(new[] { request.ExplicitChangeAddress.ScriptPubKey });

                if (keyInfos.TryGetValue(request.ExplicitChangeAddress.ScriptPubKey, out var kis))
                {
                    keyPath = kis.FirstOrDefault(k => k.DerivationStrategy == strategy)?.KeyPath;
                }
                change = (request.ExplicitChangeAddress.ScriptPubKey, keyPath);
            }
            txBuilder.SetChange(change.ScriptPubKey);
            PSBT psbt = null;

            try
            {
                if (request.FeePreference?.ExplicitFeeRate is FeeRate explicitFeeRate)
                {
                    txBuilder.SendEstimatedFees(explicitFeeRate);
                }
                else if (request.FeePreference?.BlockTarget is int blockTarget)
                {
                    try
                    {
                        var rate = await GetFeeRate(blockTarget, network.CryptoCode);

                        txBuilder.SendEstimatedFees(rate.FeeRate);
                    }
                    catch (NBXplorerException e) when(e.Error.Code == "fee-estimation-unavailable" && request.FeePreference?.FallbackFeeRate is FeeRate fallbackFeeRate)
                    {
                        txBuilder.SendEstimatedFees(fallbackFeeRate);
                    }
                }
                else if (request.FeePreference?.ExplicitFee is Money explicitFee)
                {
                    txBuilder.SendFees(explicitFee);
                }
                else
                {
                    try
                    {
                        var rate = await GetFeeRate(1, network.CryptoCode);

                        txBuilder.SendEstimatedFees(rate.FeeRate);
                    }
                    catch (NBXplorerException e) when(e.Error.Code == "fee-estimation-unavailable" && request.FeePreference?.FallbackFeeRate is FeeRate fallbackFeeRate)
                    {
                        txBuilder.SendEstimatedFees(fallbackFeeRate);
                    }
                }
                psbt      = txBuilder.BuildPSBT(false);
                hasChange = psbt.Outputs.Any(o => o.ScriptPubKey == change.ScriptPubKey);
            }
            catch (NotEnoughFundsException)
            {
                throw new NBXplorerException(new NBXplorerError(400, "not-enough-funds", "Not enough funds for doing this transaction"));
            }
            // We made sure we can build the PSBT, so now we can reserve the change address if we need to
            if (hasChange && request.ExplicitChangeAddress == null && request.ReserveChangeAddress)
            {
                var derivation = await repo.GetUnused(strategy, DerivationFeature.Change, 0, true);

                // In most of the time, this is the same as previously, so no need to rebuild PSBT
                if (derivation.ScriptPubKey != change.ScriptPubKey)
                {
                    change = (derivation.ScriptPubKey, derivation.KeyPath);
                    txBuilder.SetChange(change.ScriptPubKey);
                    psbt = txBuilder.BuildPSBT(false);
                }
            }

            var tx = psbt.GetOriginalTransaction();

            if (request.Version is uint v)
            {
                tx.Version = v;
            }
            psbt = txBuilder.CreatePSBTFrom(tx, false, SigHash.All);

            await UpdatePSBTCore(new UpdatePSBTRequest()
            {
                DerivationScheme = strategy,
                PSBT             = psbt,
                RebaseKeyPaths   = request.RebaseKeyPaths
            }, network);

            var resp = new CreatePSBTResponse()
            {
                PSBT          = psbt,
                ChangeAddress = hasChange ? change.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork) : null
            };

            return(Json(resp, network.JsonSerializerSettings));
        }
        public async Task <IActionResult> CreatePSBT(
            [ModelBinder(BinderType = typeof(NetworkModelBinder))]
            NBXplorerNetwork network,
            [ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))]
            DerivationStrategyBase strategy,
            [FromBody]
            JObject body)
        {
            if (body == null)
            {
                throw new ArgumentNullException(nameof(body));
            }
            CreatePSBTRequest request = ParseJObject <CreatePSBTRequest>(body, network);

            if (strategy == null)
            {
                throw new ArgumentNullException(nameof(strategy));
            }
            var repo  = RepositoryProvider.GetRepository(network);
            var utxos = await GetUTXOs(network.CryptoCode, strategy, null);

            var txBuilder = request.Seed is int s?network.NBitcoinNetwork.CreateTransactionBuilder(s)
                                : network.NBitcoinNetwork.CreateTransactionBuilder();

            if (Waiters.GetWaiter(network).NetworkInfo?.GetRelayFee() is FeeRate feeRate)
            {
                txBuilder.StandardTransactionPolicy.MinRelayTxFee = feeRate;
            }

            txBuilder.OptInRBF = request.RBF;
            if (request.LockTime is LockTime lockTime)
            {
                txBuilder.SetLockTime(lockTime);
                txBuilder.OptInRBF = true;
            }

            var availableCoinsByOutpoint = utxos.GetUnspentCoins(request.MinConfirmations).ToDictionary(o => o.Outpoint);

            if (request.IncludeOnlyOutpoints != null)
            {
                var includeOnlyOutpoints = request.IncludeOnlyOutpoints.ToHashSet();
                availableCoinsByOutpoint = availableCoinsByOutpoint.Where(c => includeOnlyOutpoints.Contains(c.Key)).ToDictionary(o => o.Key, o => o.Value);
            }

            if (request.ExcludeOutpoints?.Any() is true)
            {
                var excludedOutpoints = request.ExcludeOutpoints.ToHashSet();
                availableCoinsByOutpoint = availableCoinsByOutpoint.Where(c => !excludedOutpoints.Contains(c.Key)).ToDictionary(o => o.Key, o => o.Value);
            }
            txBuilder.AddCoins(availableCoinsByOutpoint.Values);

            foreach (var dest in request.Destinations)
            {
                if (dest.SweepAll)
                {
                    txBuilder.SendAll(dest.Destination);
                }
                else
                {
                    txBuilder.Send(dest.Destination, dest.Amount);
                    if (dest.SubstractFees)
                    {
                        txBuilder.SubtractFees();
                    }
                }
            }
            (Script ScriptPubKey, KeyPath KeyPath)change = (null, null);
            bool hasChange = false;

            // We first build the transaction with a change which keep the length of the expected change scriptPubKey
            // This allow us to detect if there is a change later in the constructed transaction.
            // This defend against bug which can happen if one of the destination is the same as the expected change
            // This assume that a script with only 0 can't be created from a strategy, nor by passing any data to explicitChangeAddress
            if (request.ExplicitChangeAddress == null)
            {
                // The dummyScriptPubKey is necessary to know the size of the change
                var dummyScriptPubKey = utxos.Unconfirmed.UTXOs.FirstOrDefault()?.ScriptPubKey ??
                                        utxos.Confirmed.UTXOs.FirstOrDefault()?.ScriptPubKey ?? strategy.Derive(0).ScriptPubKey;
                change = (Script.FromBytesUnsafe(new byte[dummyScriptPubKey.Length]), null);
            }
            else
            {
                change = (Script.FromBytesUnsafe(new byte[request.ExplicitChangeAddress.ScriptPubKey.Length]), null);
            }
            txBuilder.SetChange(change.ScriptPubKey);
            PSBT psbt = null;

            try
            {
                if (request.FeePreference?.ExplicitFeeRate is FeeRate explicitFeeRate)
                {
                    txBuilder.SendEstimatedFees(explicitFeeRate);
                }
                else if (request.FeePreference?.BlockTarget is int blockTarget)
                {
                    try
                    {
                        var rate = await GetFeeRate(blockTarget, network.CryptoCode);

                        txBuilder.SendEstimatedFees(rate.FeeRate);
                    }
                    catch (NBXplorerException e) when(e.Error.Code == "fee-estimation-unavailable" && request.FeePreference?.FallbackFeeRate is FeeRate fallbackFeeRate)
                    {
                        txBuilder.SendEstimatedFees(fallbackFeeRate);
                    }
                }
                else if (request.FeePreference?.ExplicitFee is Money explicitFee)
                {
                    txBuilder.SendFees(explicitFee);
                }
                else
                {
                    try
                    {
                        var rate = await GetFeeRate(1, network.CryptoCode);

                        txBuilder.SendEstimatedFees(rate.FeeRate);
                    }
                    catch (NBXplorerException e) when(e.Error.Code == "fee-estimation-unavailable" && request.FeePreference?.FallbackFeeRate is FeeRate fallbackFeeRate)
                    {
                        txBuilder.SendEstimatedFees(fallbackFeeRate);
                    }
                }
                psbt      = txBuilder.BuildPSBT(false);
                hasChange = psbt.Outputs.Any(o => o.ScriptPubKey == change.ScriptPubKey);
            }
            catch (NotEnoughFundsException)
            {
                throw new NBXplorerException(new NBXplorerError(400, "not-enough-funds", "Not enough funds for doing this transaction"));
            }
            if (hasChange)             // We need to reserve an address, so we need to build again the psbt
            {
                if (request.ExplicitChangeAddress == null)
                {
                    var derivation = await repo.GetUnused(strategy, DerivationFeature.Change, 0, request.ReserveChangeAddress);

                    change = (derivation.ScriptPubKey, derivation.KeyPath);
                }
                else
                {
                    change = (request.ExplicitChangeAddress.ScriptPubKey, null);
                }
                txBuilder.SetChange(change.ScriptPubKey);
                psbt = txBuilder.BuildPSBT(false);
            }

            var tx = psbt.GetOriginalTransaction();

            if (request.Version is uint v)
            {
                tx.Version = v;
            }
            psbt = txBuilder.CreatePSBTFrom(tx, false, SigHash.All);
            var outputsKeyInformations = repo.GetKeyInformations(psbt.Outputs.Where(o => !o.HDKeyPaths.Any()).Select(o => o.ScriptPubKey).ToArray());
            var utxosByOutpoint        = utxos.GetUnspentUTXOs().ToDictionary(u => u.Outpoint);

            // Maybe it is a change that we know about, let's search in the DB
            if (hasChange && change.KeyPath == null)
            {
                var keyInfos = await repo.GetKeyInformations(new[] { request.ExplicitChangeAddress.ScriptPubKey });

                if (keyInfos.TryGetValue(request.ExplicitChangeAddress.ScriptPubKey, out var kis))
                {
                    var ki = kis.FirstOrDefault(k => k.DerivationStrategy == strategy);
                    if (ki != null)
                    {
                        change = (change.ScriptPubKey, kis.First().KeyPath);
                    }
                }
            }


            var pubkeys  = strategy.GetExtPubKeys().Select(p => p.AsHDKeyCache()).ToArray();
            var keyPaths = psbt.Inputs.Select(i => utxosByOutpoint[i.PrevOut].KeyPath).ToList();

            if (hasChange && change.KeyPath != null)
            {
                keyPaths.Add(change.KeyPath);
            }
            var fps = new Dictionary <PubKey, HDFingerprint>();

            foreach (var pubkey in pubkeys)
            {
                // We derive everything the fastest way possible on multiple cores
                pubkey.Derive(keyPaths.ToArray());
                fps.TryAdd(pubkey.GetPublicKey(), pubkey.GetPublicKey().GetHDFingerPrint());
            }

            foreach (var input in psbt.Inputs)
            {
                var utxo = utxosByOutpoint[input.PrevOut];
                foreach (var pubkey in pubkeys)
                {
                    var childPubKey = pubkey.Derive(utxo.KeyPath);
                    NBitcoin.Extensions.TryAdd(input.HDKeyPaths, childPubKey.GetPublicKey(), Tuple.Create(fps[pubkey.GetPublicKey()], utxo.KeyPath));
                }
            }

            await Task.WhenAll(psbt.Inputs
                               .Select(async(input) =>
            {
                if (input.WitnessUtxo == null)                         // We need previous tx
                {
                    var prev = await repo.GetSavedTransactions(input.PrevOut.Hash);
                    if (prev?.Any() is true)
                    {
                        input.NonWitnessUtxo = prev[0].Transaction;
                    }
                }
            }).ToArray());

            var outputsKeyInformationsResult = await outputsKeyInformations;

            foreach (var output in psbt.Outputs)
            {
                foreach (var keyInfo in outputsKeyInformationsResult[output.ScriptPubKey].Where(o => o.DerivationStrategy == strategy))
                {
                    foreach (var pubkey in pubkeys)
                    {
                        var childPubKey = pubkey.Derive(keyInfo.KeyPath);
                        NBitcoin.Extensions.TryAdd(output.HDKeyPaths, childPubKey.GetPublicKey(), Tuple.Create(fps[pubkey.GetPublicKey()], keyInfo.KeyPath));
                    }
                }
            }

            if (request.RebaseKeyPaths != null)
            {
                foreach (var rebase in request.RebaseKeyPaths)
                {
                    if (rebase.AccountKeyPath == null)
                    {
                        throw new NBXplorerException(new NBXplorerError(400, "missing-parameter", "rebaseKeyPaths[].accountKeyPath is missing"));
                    }
                    if (rebase.AccountKey == null)
                    {
                        throw new NBXplorerException(new NBXplorerError(400, "missing-parameter", "rebaseKeyPaths[].accountKey is missing"));
                    }
                    psbt.RebaseKeyPaths(rebase.AccountKey, rebase.AccountKeyPath, rebase.MasterFingerprint);
                }
            }

            var resp = new CreatePSBTResponse()
            {
                PSBT          = psbt,
                ChangeAddress = hasChange ? change.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork) : null
            };

            return(Json(resp, network.JsonSerializerSettings));
        }