예제 #1
0
        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);
                }
            }
        }
        private async Task UpdatePSBTCore(UpdatePSBTRequest update, NBXplorerNetwork network)
        {
            var repo = RepositoryProvider.GetRepository(network);

            await UpdateInputsUTXO(update, repo);

            if (update.DerivationScheme is DerivationStrategyBase)
            {
                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);
                }
            }
        }
예제 #3
0
        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);
                    }
                }
            }
        }
예제 #4
0
 public Task <UpdatePSBTResponse> UpdatePSBTAsync(UpdatePSBTRequest request, CancellationToken cancellation = default)
 {
     if (request == null)
     {
         throw new ArgumentNullException(nameof(request));
     }
     return(this.SendAsync <UpdatePSBTResponse>(HttpMethod.Post, request, "v1/cryptos/{0}/psbt/update", new object[] { CryptoCode }, cancellation));
 }
        private async Task UpdateUTXO(UpdatePSBTRequest update, Repository repo, BitcoinDWaiter rpc)
        {
            AnnotatedTransactionCollection txs = null;

            // First, we check for data in our history
            foreach (var input in update.PSBT.Inputs.Where(NeedUTXO))
            {
                txs = txs ?? await GetAnnotatedTransactions(repo, ChainProvider.GetChain(repo.Network), new DerivationSchemeTrackedSource(update.DerivationScheme));

                if (txs.GetByTxId(input.PrevOut.Hash) is AnnotatedTransaction tx)
                {
                    if (!tx.Record.Key.IsPruned)
                    {
                        input.NonWitnessUtxo = tx.Record.Transaction;
                    }
                    else
                    {
                        input.WitnessUtxo = tx.Record.ReceivedCoins.FirstOrDefault(c => c.Outpoint.N == input.Index)?.TxOut;
                    }
                }
            }

            // then, we search data in the saved transactions
            await Task.WhenAll(update.PSBT.Inputs
                               .Where(NeedUTXO)
                               .Select(async(input) =>
            {
                // If this is not segwit, or we are unsure of it, let's try to grab from our saved transactions
                if (input.NonWitnessUtxo == null)
                {
                    var prev = await repo.GetSavedTransactions(input.PrevOut.Hash);
                    if (prev.FirstOrDefault() is Repository.SavedTransaction saved)
                    {
                        input.NonWitnessUtxo = saved.Transaction;
                    }
                }
            }).ToArray());

            // finally, we check with rpc's txindex
            if (rpc?.RPCAvailable is true && rpc?.HasTxIndex is true)
            {
                var batch           = rpc.RPC.PrepareBatch();
                var getTransactions = Task.WhenAll(update.PSBT.Inputs
                                                   .Where(NeedUTXO)
                                                   .Where(input => input.NonWitnessUtxo == null)
                                                   .Select(async input =>
                {
                    var tx = await batch.GetRawTransactionAsync(input.PrevOut.Hash, false);
                    if (tx != null)
                    {
                        input.NonWitnessUtxo = tx;
                    }
                }).ToArray());
                await batch.SendBatchAsync();

                await getTransactions;
            }
        }
        private static async Task UpdateHDKeyPathsWitnessAndRedeem(UpdatePSBTRequest update, Repository repo)
        {
            var strategy = update.DerivationScheme;
            var pubkeys  = strategy.GetExtPubKeys().Select(p => p.AsHDKeyCache()).ToArray();
            var keyInfosByScriptPubKey = new Dictionary <Script, KeyPathInformation>();
            var scriptPubKeys          = update.PSBT.Outputs.OfType <PSBTCoin>().Concat(update.PSBT.Inputs)
                                         .Where(o => !o.HDKeyPaths.Any())
                                         .Select(o => o.GetCoin()?.ScriptPubKey)
                                         .Where(s => s != null).ToArray();

            foreach (var keyInfos in (await repo.GetKeyInformations(scriptPubKeys)))
            {
                var keyInfo = keyInfos.Value.FirstOrDefault(k => k.DerivationStrategy == strategy);
                if (keyInfo != null)
                {
                    keyInfosByScriptPubKey.TryAdd(keyInfo.ScriptPubKey, keyInfo);
                }
            }

            var fps = new Dictionary <PubKey, HDFingerprint>();

            foreach (var pubkey in pubkeys)
            {
                // We derive everything the fastest way possible on multiple cores
                pubkey.Derive(keyInfosByScriptPubKey.Select(s => s.Value.KeyPath).ToArray());
                fps.TryAdd(pubkey.GetPublicKey(), pubkey.GetPublicKey().GetHDFingerPrint());
            }

            List <Script> redeems = new List <Script>();

            foreach (var c in update.PSBT.Outputs.OfType <PSBTCoin>().Concat(update.PSBT.Inputs))
            {
                var script = c.GetCoin()?.ScriptPubKey;
                if (script != null &&
                    keyInfosByScriptPubKey.TryGetValue(script, out var keyInfo))
                {
                    foreach (var pubkey in pubkeys)
                    {
                        var childPubKey = pubkey.Derive(keyInfo.KeyPath);
                        NBitcoin.Extensions.AddOrReplace(c.HDKeyPaths, childPubKey.GetPublicKey(), new RootedKeyPath(fps[pubkey.GetPublicKey()], keyInfo.KeyPath));
                        if (keyInfo.Redeem != null)
                        {
                            redeems.Add(keyInfo.Redeem);
                        }
                    }
                }
            }
            if (redeems.Count != 0)
            {
                update.PSBT.AddScripts(redeems.ToArray());
            }
        }
예제 #7
0
        private static async Task UpdateInputsUTXO(UpdatePSBTRequest update, Repository repo, BitcoinDWaiter rpc)
        {
            await Task.WhenAll(update.PSBT.Inputs
                               .Where(NeedNonWitnessUtxo)
                               .Where(NotFinalized)
                               .Select(async(input) =>
            {
                // If this is not segwit, or we are unsure of it, let's try to grab from our saved transactions
                if (input.NonWitnessUtxo == null)
                {
                    var prev = await repo.GetSavedTransactions(input.PrevOut.Hash);
                    if (prev.FirstOrDefault() is Repository.SavedTransaction saved)
                    {
                        input.NonWitnessUtxo = saved.Transaction;
                    }
                }

                // Maybe we don't have the saved transaction, but we have the WitnessUTXO from the derivation scheme UTXOs
                if (input.NonWitnessUtxo == null)
                {
                    var tx = (await repo.GetTransactions(new DerivationSchemeTrackedSource(update.DerivationScheme), input.PrevOut.Hash)).FirstOrDefault();
                    if (tx != null)
                    {
                        input.NonWitnessUtxo = tx.Transaction;
                    }
                }
            }).ToArray());

            if (rpc?.RPCAvailable is true && rpc?.HasTxIndex is true)
            {
                var batch           = rpc.RPC.PrepareBatch();
                var getTransactions = Task.WhenAll(update.PSBT.Inputs
                                                   .Where(NeedNonWitnessUtxo)
                                                   .Where(NotFinalized)
                                                   .Where(input => input.NonWitnessUtxo == null)
                                                   .Select(async input =>
                {
                    var tx = await batch.GetRawTransactionAsync(input.PrevOut.Hash, false);
                    if (tx != null)
                    {
                        input.NonWitnessUtxo = tx;
                    }
                }).ToArray());
                await batch.SendBatchAsync();

                await getTransactions;
            }
        }
예제 #8
0
        private static async Task UpdateInputsUTXO(UpdatePSBTRequest update, Repository repo)
        {
            await Task.WhenAll(update.PSBT.Inputs
                               .Select(async(input) =>
            {
                var isWitness = (input.GetSignableCoin() ?? input.GetCoin())?.GetHashVersion() is HashVersion.Witness;
                // We are sure we are using segwit, so no need to fetch anything else
                if (isWitness)
                {
                    return;
                }
                // If this is not segwit, or we are unsure of it, let's try to grab from our saved transactions
                if (input.NonWitnessUtxo == null)
                {
                    var prev = await repo.GetSavedTransactions(input.PrevOut.Hash);
                    if (prev.FirstOrDefault() is Repository.SavedTransaction saved)
                    {
                        input.NonWitnessUtxo = saved.Transaction;
                    }
                }

                // Maybe we don't have the saved transaction, but we have the WitnessUTXO from the derivation scheme UTXOs
                if (input.NonWitnessUtxo == null && input.WitnessUtxo == null && update.DerivationScheme != null)
                {
                    var tx = (await repo.GetTransactions(new DerivationSchemeTrackedSource(update.DerivationScheme), input.PrevOut.Hash)).FirstOrDefault();
                    if (tx != null)
                    {
                        var output = tx.GetReceivedOutputs().FirstOrDefault(o => o.Index == input.PrevOut.N);
                        if (output != null)
                        {
                            input.WitnessUtxo = repo.Network.NBitcoinNetwork.Consensus.ConsensusFactory.CreateTxOut();
                            input.WitnessUtxo.ScriptPubKey = output.ScriptPubKey;
                            input.WitnessUtxo.Value = output.Value;
                        }
                    }
                }
            }).ToArray());
        }
예제 #9
0
 public UpdatePSBTResponse UpdatePSBT(UpdatePSBTRequest request, CancellationToken cancellation = default)
 {
     return(UpdatePSBTAsync(request, cancellation).GetAwaiter().GetResult());
 }
예제 #10
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));
        }
예제 #11
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();

            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);

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

            await UpdatePSBTCore(update, network);

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

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