コード例 #1
0
        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));
        }