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 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));
        }
Exemple #3
0
        public async Task <IActionResult> WalletSend(
            [ModelBinder(typeof(WalletIdModelBinder))]
            WalletId walletId, WalletSendModel vm, string command = "", CancellationToken cancellation = default)
        {
            if (walletId?.StoreId == null)
            {
                return(NotFound());
            }
            var store = await Repository.FindStore(walletId.StoreId, GetUserId());

            if (store == null)
            {
                return(NotFound());
            }
            var network = this.NetworkProvider.GetNetwork <BTCPayNetwork>(walletId?.CryptoCode);

            if (network == null || network.ReadonlyWallet)
            {
                return(NotFound());
            }
            vm.SupportRBF = network.SupportRBF;
            decimal transactionAmountSum = 0;

            if (command == "add-output")
            {
                ModelState.Clear();
                vm.Outputs.Add(new WalletSendModel.TransactionOutput());
                return(View(vm));
            }
            if (command.StartsWith("remove-output", StringComparison.InvariantCultureIgnoreCase))
            {
                ModelState.Clear();
                var index = int.Parse(command.Substring(command.IndexOf(":", StringComparison.InvariantCultureIgnoreCase) + 1), CultureInfo.InvariantCulture);
                vm.Outputs.RemoveAt(index);
                return(View(vm));
            }


            if (!vm.Outputs.Any())
            {
                ModelState.AddModelError(string.Empty,
                                         "Please add at least one transaction output");
                return(View(vm));
            }

            var subtractFeesOutputsCount = new List <int>();
            var substractFees            = vm.Outputs.Any(o => o.SubtractFeesFromOutput);

            for (var i = 0; i < vm.Outputs.Count; i++)
            {
                var transactionOutput = vm.Outputs[i];
                if (transactionOutput.SubtractFeesFromOutput)
                {
                    subtractFeesOutputsCount.Add(i);
                }
                transactionOutput.DestinationAddress = transactionOutput.DestinationAddress?.Trim() ?? string.Empty;

                try
                {
                    BitcoinAddress.Create(transactionOutput.DestinationAddress, network.NBitcoinNetwork);
                }
                catch
                {
                    var inputName =
                        string.Format(CultureInfo.InvariantCulture, "Outputs[{0}].", i.ToString(CultureInfo.InvariantCulture)) +
                        nameof(transactionOutput.DestinationAddress);

                    ModelState.AddModelError(inputName, "Invalid address");
                }

                if (transactionOutput.Amount.HasValue)
                {
                    transactionAmountSum += transactionOutput.Amount.Value;

                    if (vm.CurrentBalance == transactionOutput.Amount.Value &&
                        !transactionOutput.SubtractFeesFromOutput)
                    {
                        vm.AddModelError(model => model.Outputs[i].SubtractFeesFromOutput,
                                         "You are sending your entire balance to the same destination, you should subtract the fees",
                                         this);
                    }
                }
            }

            if (subtractFeesOutputsCount.Count > 1)
            {
                foreach (var subtractFeesOutput in subtractFeesOutputsCount)
                {
                    vm.AddModelError(model => model.Outputs[subtractFeesOutput].SubtractFeesFromOutput,
                                     "You can only subtract fees from one output", this);
                }
            }
            else if (vm.CurrentBalance == transactionAmountSum && !substractFees)
            {
                ModelState.AddModelError(string.Empty,
                                         "You are sending your entire balance, you should subtract the fees from an output");
            }

            if (vm.CurrentBalance < transactionAmountSum)
            {
                for (var i = 0; i < vm.Outputs.Count; i++)
                {
                    vm.AddModelError(model => model.Outputs[i].Amount,
                                     "You are sending more than what you own", this);
                }
            }

            if (!ModelState.IsValid)
            {
                return(View(vm));
            }

            DerivationSchemeSettings derivationScheme = GetDerivationSchemeSettings(walletId);

            CreatePSBTResponse psbt = null;

            try
            {
                psbt = await CreatePSBT(network, derivationScheme, vm, cancellation);
            }
            catch (NBXplorerException ex)
            {
                ModelState.AddModelError(string.Empty, ex.Error.Message);
                return(View(vm));
            }
            catch (NotSupportedException)
            {
                ModelState.AddModelError(string.Empty, "You need to update your version of NBXplorer");
                return(View(vm));
            }
            derivationScheme.RebaseKeyPaths(psbt.PSBT);

            switch (command)
            {
            case "vault":
                return(ViewVault(walletId, psbt.PSBT));

            case "ledger":
                return(ViewWalletSendLedger(psbt.PSBT, psbt.ChangeAddress));

            case "seed":
                return(SignWithSeed(walletId, psbt.PSBT.ToBase64()));

            case "analyze-psbt":
                var name =
                    $"Send-{string.Join('_', vm.Outputs.Select(output => $"{output.Amount}->{output.DestinationAddress}{(output.SubtractFeesFromOutput ? "-Fees" : string.Empty)}"))}.psbt";
                return(RedirectToWalletPSBT(walletId, psbt.PSBT, name));

            default:
                return(View(vm));
            }
        }
        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 #5
0
        public async Task <IActionResult> LedgerConnection(
            [ModelBinder(typeof(WalletIdModelBinder))]
            WalletId walletId,
            string command,
            // getinfo
            // getxpub
            int account = 0,
            // sendtoaddress
            string psbt       = null,
            string hintChange = null
            )
        {
            if (!HttpContext.WebSockets.IsWebSocketRequest)
            {
                return(NotFound());
            }

            var network = NetworkProvider.GetNetwork <BTCPayNetwork>(walletId.CryptoCode);

            if (network == null)
            {
                throw new FormatException("Invalid value for crypto code");
            }
            var storeData          = (await Repository.FindStore(walletId.StoreId, GetUserId()));
            var derivationSettings = GetDerivationSchemeSettings(walletId, storeData);

            var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();

            using (var normalOperationTimeout = new CancellationTokenSource())
                using (var signTimeout = new CancellationTokenSource())
                {
                    normalOperationTimeout.CancelAfter(TimeSpan.FromMinutes(30));
                    var    hw     = new LedgerHardwareWalletService(webSocket);
                    var    model  = new WalletSendLedgerModel();
                    object result = null;
                    try
                    {
                        if (command == "test")
                        {
                            result = await hw.Test(normalOperationTimeout.Token);
                        }
                        if (command == "sendtoaddress")
                        {
                            if (!_dashboard.IsFullySynched(network.CryptoCode, out var summary))
                            {
                                throw new Exception($"{network.CryptoCode}: not started or fully synched");
                            }

                            var accountKey = derivationSettings.GetSigningAccountKeySettings();
                            // Some deployment does not have the AccountKeyPath set, let's fix this...
                            if (accountKey.AccountKeyPath == null)
                            {
                                // If the saved wallet key path is not present or incorrect, let's scan the wallet to see if it can sign strategy
                                var foundKeyPath = await hw.FindKeyPathFromDerivation(network,
                                                                                      derivationSettings.AccountDerivation,
                                                                                      normalOperationTimeout.Token);

                                accountKey.AccountKeyPath = foundKeyPath ?? throw new HardwareWalletException($"This store is not configured to use this ledger");
                                storeData.SetSupportedPaymentMethod(derivationSettings);
                                await Repository.UpdateStore(storeData);
                            }
                            // If it has already the AccountKeyPath, we did not looked up for it, so we need to check if we are on the right ledger
                            else
                            {
                                // Checking if ledger is right with the RootFingerprint is faster as it does not need to make a query to the parent xpub,
                                // but some deployment does not have it, so let's use AccountKeyPath instead
                                if (accountKey.RootFingerprint == null)
                                {
                                    var actualPubKey = await hw.GetExtPubKey(network, accountKey.AccountKeyPath, normalOperationTimeout.Token);

                                    if (!derivationSettings.AccountDerivation.GetExtPubKeys().Any(p => p.GetPublicKey() == actualPubKey.GetPublicKey()))
                                    {
                                        throw new HardwareWalletException($"This store is not configured to use this ledger");
                                    }
                                }
                                // We have the root fingerprint, we can check the root from it
                                else
                                {
                                    var actualPubKey = await hw.GetPubKey(network, new KeyPath(), normalOperationTimeout.Token);

                                    if (actualPubKey.GetHDFingerPrint() != accountKey.RootFingerprint.Value)
                                    {
                                        throw new HardwareWalletException($"This store is not configured to use this ledger");
                                    }
                                }
                            }

                            // Some deployment does not have the RootFingerprint set, let's fix this...
                            if (accountKey.RootFingerprint == null)
                            {
                                accountKey.RootFingerprint = (await hw.GetPubKey(network, new KeyPath(), normalOperationTimeout.Token)).GetHDFingerPrint();
                                storeData.SetSupportedPaymentMethod(derivationSettings);
                                await Repository.UpdateStore(storeData);
                            }

                            var psbtResponse = new CreatePSBTResponse()
                            {
                                PSBT          = PSBT.Parse(psbt, network.NBitcoinNetwork),
                                ChangeAddress = string.IsNullOrEmpty(hintChange) ? null : BitcoinAddress.Create(hintChange, network.NBitcoinNetwork)
                            };


                            derivationSettings.RebaseKeyPaths(psbtResponse.PSBT);

                            signTimeout.CancelAfter(TimeSpan.FromMinutes(5));
                            psbtResponse.PSBT = await hw.SignTransactionAsync(psbtResponse.PSBT, accountKey.GetRootedKeyPath(), accountKey.AccountKey, psbtResponse.ChangeAddress?.ScriptPubKey, signTimeout.Token);

                            result = new SendToAddressResult()
                            {
                                PSBT = psbtResponse.PSBT.ToBase64()
                            };
                        }
                    }
                    catch (OperationCanceledException)
                    { result = new LedgerTestResult()
                      {
                          Success = false, Error = "Timeout"
                      }; }
                    catch (Exception ex)
                    { result = new LedgerTestResult()
                      {
                          Success = false, Error = ex.Message
                      }; }
                    finally { hw.Dispose(); }
                    try
                    {
                        if (result != null)
                        {
                            UTF8Encoding UTF8NOBOM = new UTF8Encoding(false);
                            var          bytes     = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, _mvcJsonOptions.Value.SerializerSettings));
                            await webSocket.SendAsync(new ArraySegment <byte>(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token);
                        }
                    }
                    catch { }
                    finally
                    {
                        await webSocket.CloseSocket();
                    }
                }
            return(new EmptyResult());
        }
        public async Task <IActionResult> WalletSend(
            [ModelBinder(typeof(WalletIdModelBinder))]
            WalletId walletId, WalletSendModel vm, string command = null, CancellationToken cancellation = default)
        {
            if (walletId?.StoreId == null)
            {
                return(NotFound());
            }
            var store = await Repository.FindStore(walletId.StoreId, GetUserId());

            if (store == null)
            {
                return(NotFound());
            }
            var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode);

            if (network == null)
            {
                return(NotFound());
            }
            vm.SupportRBF = network.SupportRBF;
            var destination = ParseDestination(vm.Destination, network.NBitcoinNetwork);

            if (destination == null)
            {
                ModelState.AddModelError(nameof(vm.Destination), "Invalid address");
            }

            if (vm.Amount.HasValue)
            {
                if (vm.CurrentBalance == vm.Amount.Value && !vm.SubstractFees)
                {
                    ModelState.AddModelError(nameof(vm.Amount), "You are sending all your balance to the same destination, you should substract the fees");
                }
                if (vm.CurrentBalance < vm.Amount.Value)
                {
                    ModelState.AddModelError(nameof(vm.Amount), "You are sending more than what you own");
                }
            }
            if (!ModelState.IsValid)
            {
                return(View(vm));
            }

            DerivationSchemeSettings derivationScheme = await GetDerivationSchemeSettings(walletId);

            CreatePSBTResponse psbt = null;

            try
            {
                psbt = await CreatePSBT(network, derivationScheme, vm, cancellation);
            }
            catch (NBXplorerException ex)
            {
                ModelState.AddModelError(nameof(vm.Amount), ex.Error.Message);
                return(View(vm));
            }
            catch (NotSupportedException)
            {
                ModelState.AddModelError(nameof(vm.Destination), "You need to update your version of NBXplorer");
                return(View(vm));
            }
            derivationScheme.RebaseKeyPaths(psbt.PSBT);
            if (command == "ledger")
            {
                return(ViewWalletSendLedger(psbt.PSBT, psbt.ChangeAddress));
            }
            else if (command == "analyze-psbt")
            {
                return(ViewPSBT(psbt.PSBT, $"Send-{vm.Amount.Value}-{network.CryptoCode}-to-{destination[0].ToString()}.psbt"));
            }
            return(View(vm));
        }
        public async Task <IActionResult> WalletSend(
            [ModelBinder(typeof(WalletIdModelBinder))]
            WalletId walletId, WalletSendModel vm, string command = "", CancellationToken cancellation = default)
        {
            if (walletId?.StoreId == null)
            {
                return(NotFound());
            }
            var store = await Repository.FindStore(walletId.StoreId, GetUserId());

            if (store == null)
            {
                return(NotFound());
            }
            var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode);

            if (network == null)
            {
                return(NotFound());
            }
            vm.SupportRBF = network.SupportRBF;
            decimal transactionAmountSum = 0;

            if (command == "add-output")
            {
                ModelState.Clear();
                vm.Outputs.Add(new WalletSendModel.TransactionOutput());
                return(View(vm));
            }
            if (command.StartsWith("remove-output", StringComparison.InvariantCultureIgnoreCase))
            {
                ModelState.Clear();
                var index = int.Parse(command.Substring(command.IndexOf(":", StringComparison.InvariantCultureIgnoreCase) + 1), CultureInfo.InvariantCulture);
                vm.Outputs.RemoveAt(index);
                return(View(vm));
            }


            if (!vm.Outputs.Any())
            {
                ModelState.AddModelError(string.Empty,
                                         "Por favor agregue al menos una salida de transacción");
                return(View(vm));
            }

            var subtractFeesOutputsCount = new List <int>();
            var substractFees            = vm.Outputs.Any(o => o.SubtractFeesFromOutput);

            for (var i = 0; i < vm.Outputs.Count; i++)
            {
                var transactionOutput = vm.Outputs[i];
                if (transactionOutput.SubtractFeesFromOutput)
                {
                    subtractFeesOutputsCount.Add(i);
                }
                var destination = ParseDestination(transactionOutput.DestinationAddress, network.NBitcoinNetwork);
                if (destination == null)
                {
                    ModelState.AddModelError(nameof(transactionOutput.DestinationAddress), "Dirección inválida");
                }

                if (transactionOutput.Amount.HasValue)
                {
                    transactionAmountSum += transactionOutput.Amount.Value;

                    if (vm.CurrentBalance == transactionOutput.Amount.Value &&
                        !transactionOutput.SubtractFeesFromOutput)
                    {
                        vm.AddModelError(model => model.Outputs[i].SubtractFeesFromOutput,
                                         "Está enviando todo su saldo al mismo destino, debe restar las tarifas",
                                         ModelState);
                    }
                }
            }

            if (subtractFeesOutputsCount.Count > 1)
            {
                foreach (var subtractFeesOutput in subtractFeesOutputsCount)
                {
                    vm.AddModelError(model => model.Outputs[subtractFeesOutput].SubtractFeesFromOutput,
                                     "Solo puedes restar tarifas de una salida", ModelState);
                }
            }
            else if (vm.CurrentBalance == transactionAmountSum && !substractFees)
            {
                ModelState.AddModelError(string.Empty,
                                         "Está enviando todo su saldo, debe restar las tarifas de una salida");
            }

            if (vm.CurrentBalance < transactionAmountSum)
            {
                for (var i = 0; i < vm.Outputs.Count; i++)
                {
                    vm.AddModelError(model => model.Outputs[i].Amount,
                                     "Estás enviando más de lo que tienes", ModelState);
                }
            }

            if (!ModelState.IsValid)
            {
                return(View(vm));
            }

            DerivationSchemeSettings derivationScheme = await GetDerivationSchemeSettings(walletId);

            CreatePSBTResponse psbt = null;

            try
            {
                psbt = await CreatePSBT(network, derivationScheme, vm, cancellation);
            }
            catch (NBXplorerException ex)
            {
                ModelState.AddModelError(string.Empty, ex.Error.Message);
                return(View(vm));
            }
            catch (NotSupportedException)
            {
                ModelState.AddModelError(string.Empty, "Necesitas actualizar tu versión de NBXplorer");
                return(View(vm));
            }
            derivationScheme.RebaseKeyPaths(psbt.PSBT);

            switch (command)
            {
            case "ledger":
                return(ViewWalletSendLedger(psbt.PSBT, psbt.ChangeAddress));

            case "seed":
                return(SignWithSeed(walletId, psbt.PSBT.ToBase64()));

            case "analyze-psbt":
                var name =
                    $"Send-{string.Join('_', vm.Outputs.Select(output => $"{output.Amount}->{output.DestinationAddress}{(output.SubtractFeesFromOutput ? "-Fees" : string.Empty)}"))}.psbt";
                return(RedirectToAction(nameof(WalletPSBT), new { walletId = walletId, psbt = psbt.PSBT.ToBase64(), FileName = name }));

            default:
                return(View(vm));
            }
        }
        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));
        }