private async Task UpdatePSBTCore(UpdatePSBTRequest update, NBXplorerNetwork network) { var repo = RepositoryProvider.GetRepository(network); var rpc = Waiters.GetWaiter(network); await UpdateInputsUTXO(update, repo, rpc); if (update.DerivationScheme is DerivationStrategyBase) { foreach (var extpub in update.DerivationScheme.GetExtPubKeys().Select(e => e.GetWif(network.NBitcoinNetwork))) { update.PSBT.GlobalXPubs.AddOrReplace(extpub, new RootedKeyPath(extpub, new KeyPath())); } await UpdateHDKeyPathsWitnessAndRedeem(update, repo); } foreach (var input in update.PSBT.Inputs) { input.TrySlimUTXO(); } if (update.RebaseKeyPaths != null) { foreach (var rebase in update.RebaseKeyPaths) { var rootedKeyPath = rebase.GetRootedKeyPath(); if (rootedKeyPath == null) { throw new NBXplorerException(new NBXplorerError(400, "missing-parameter", "rebaseKeyPaths[].rootedKeyPath is missing")); } update.PSBT.RebaseKeyPaths(rebase.AccountKey, rootedKeyPath); } } }
public Task StartAsync(CancellationToken cancellationToken) { if (_Disposed) { throw new ObjectDisposedException(nameof(BrokerHostedService)); } _senderBlock = CreateClientBlock(); _senderTransactions = CreateClientTransaction(); _subscriptions.Add(_EventAggregator.Subscribe <Events.NewBlockEvent>(async o => { var chain = ChainProvider.GetChain(o.CryptoCode); if (chain == null) { return; } var block = chain.GetBlock(o.BlockId); if (block != null) { var nbe = new Models.NewBlockEvent() { CryptoCode = o.CryptoCode, Hash = block.Hash, Height = block.Height, PreviousBlockHash = block?.Previous }; await _senderBlock.Send(nbe); } })); _subscriptions.Add(_EventAggregator.Subscribe <Events.NewTransactionMatchEvent>(async o => { var network = Waiters.GetWaiter(o.CryptoCode); if (network == null) { return; } var chain = ChainProvider.GetChain(o.CryptoCode); if (chain == null) { return; } var txe = new Models.NewTransactionEvent() { TrackedSource = o.Match.TrackedSource, DerivationStrategy = o.Match.DerivationStrategy, CryptoCode = o.CryptoCode, BlockId = o.BlockId, TransactionData = Utils.ToTransactionResult(true, chain, new[] { o.SavedTransaction }), Inputs = o.Match.Inputs, Outputs = o.Match.Outputs }; await _senderTransactions.Send(txe); })); Logs.Configuration.LogInformation("Starting Azure Service Bus Message Broker"); return(Task.CompletedTask); }
public async Task <IActionResult> GetTransaction( [ModelBinder(BinderType = typeof(UInt256ModelBinding))] uint256 txId, bool includeTransaction = true, string cryptoCode = null) { var network = GetNetwork(cryptoCode, false); var chain = this.ChainProvider.GetChain(network); var result = await RepositoryProvider.GetRepository(network).GetSavedTransactions(txId); if (result.Length == 0) { var waiter = Waiters.GetWaiter(cryptoCode); if (waiter.RPCAvailable && waiter.HasTxIndex && await waiter.RPC.TryGetRawTransaction(txId) is Repository.SavedTransaction savedTransaction) { result = new[] { savedTransaction }; } else { return(NotFound()); } } var tx = Utils.ToTransactionResult(chain, result); if (!includeTransaction) { tx.Transaction = null; } return(Json(tx)); }
public async Task <IActionResult> GetStatus(string cryptoCode) { var network = GetNetwork(cryptoCode, false); var waiter = Waiters.GetWaiter(network); var chain = ChainProvider.GetChain(network); var repo = RepositoryProvider.GetRepository(network); var now = DateTimeOffset.UtcNow; var location = waiter.GetLocation(); var blockchainInfoAsync = waiter.RPCAvailable ? waiter.RPC.GetBlockchainInfoAsyncEx() : null; var networkInfoAsync = waiter.RPCAvailable ? waiter.RPC.GetNetworkInfoAsync() : null; await repo.Ping(); var pingAfter = DateTimeOffset.UtcNow; GetBlockchainInfoResponse blockchainInfo = blockchainInfoAsync == null ? null : await blockchainInfoAsync; GetNetworkInfoResponse networkInfo = networkInfoAsync == null ? null : await networkInfoAsync; var status = new StatusResult() { NetworkType = network.NBitcoinNetwork.NetworkType, CryptoCode = network.CryptoCode, Version = typeof(MainController).GetTypeInfo().Assembly.GetCustomAttribute <AssemblyFileVersionAttribute>().Version, SupportedCryptoCodes = Waiters.All().Select(w => w.Network.CryptoCode).ToArray(), RepositoryPingTime = (pingAfter - now).TotalSeconds, IsFullySynched = true }; if (status.RepositoryPingTime > 30) { Logs.Explorer.LogWarning($"Repository ping exceeded 30 seconds ({(int)status.RepositoryPingTime}), please report the issue to NBXplorer developers"); } if (blockchainInfo != null) { status.BitcoinStatus = new BitcoinStatus() { IsSynched = !waiter.IsSynchingCore(blockchainInfo), Blocks = (int)blockchainInfo.Blocks, Headers = (int)blockchainInfo.Headers, VerificationProgress = blockchainInfo.VerificationProgress, MinRelayTxFee = new FeeRate(Money.Coins((decimal)networkInfo.relayfee), 1000), IncrementalRelayFee = new FeeRate(Money.Coins((decimal)networkInfo.incrementalfee), 1000), Capabilities = new NodeCapabilities() { CanScanTxoutSet = waiter.RPC.Capabilities.SupportScanUTXOSet, CanSupportSegwit = waiter.RPC.Capabilities.SupportSegwit } }; status.IsFullySynched &= status.BitcoinStatus.IsSynched; } status.ChainHeight = chain.Height; status.SyncHeight = location == null ? (int?)null : chain.FindFork(location).Height; status.IsFullySynched &= blockchainInfo != null && waiter.State == BitcoinDWaiterState.Ready && status.SyncHeight.HasValue && blockchainInfo.Headers - status.SyncHeight.Value < 3; return(Json(status)); }
public async Task <GetFeeRateResult> GetFeeRate(int blockCount, string cryptoCode) { var network = GetNetwork(cryptoCode, true); if (!network.SupportEstimatesSmartFee) { throw new NBXplorerError(400, "fee-estimation-unavailable", $"{cryptoCode} does not support estimatesmartfee").AsException(); } var waiter = Waiters.GetWaiter(network); var result = await waiter.RPC.SendCommandAsync("estimatesmartfee", blockCount); var obj = (JObject)result.Result; var feeRateProperty = obj.Property("feerate"); var rate = feeRateProperty == null ? (decimal) - 1 : obj["feerate"].Value <decimal>(); if (rate == -1) { throw new NBXplorerError(400, "fee-estimation-unavailable", $"It is currently impossible to estimate fees, please try again later.").AsException(); } return(new GetFeeRateResult() { FeeRate = new FeeRate(Money.Coins(Math.Round(rate / 1000, 8)), 1), BlockCount = obj["blocks"].Value <int>() }); }
private async Task UpdatePSBTCore(UpdatePSBTRequest update, NBXplorerNetwork network) { var repo = RepositoryProvider.GetRepository(network); var rpc = Waiters.GetWaiter(network); await UpdateUTXO(update, repo, rpc); if (update.DerivationScheme is DerivationStrategyBase derivationScheme) { if (update.IncludeGlobalXPub is true) { foreach (var extpub in derivationScheme.GetExtPubKeys().Select(e => e.GetWif(network.NBitcoinNetwork))) { update.PSBT.GlobalXPubs.AddOrReplace(extpub, new RootedKeyPath(extpub, new KeyPath())); } } await UpdateHDKeyPathsWitnessAndRedeem(update, repo); } if (!update.AlwaysIncludeNonWitnessUTXO) { foreach (var input in update.PSBT.Inputs) { input.TrySlimUTXO(); } } HashSet <PubKey> rebased = new HashSet <PubKey>(); if (update.RebaseKeyPaths != null) { if (update.RebaseKeyPaths.Any(r => r.AccountKey is null)) { throw new NBXplorerException(new NBXplorerError(400, "missing-parameter", "rebaseKeyPaths[].accountKey is missing")); } foreach (var rebase in update.RebaseKeyPaths.Where(r => rebased.Add(r.AccountKey.GetPublicKey()))) { if (rebase.AccountKeyPath == null) { throw new NBXplorerException(new NBXplorerError(400, "missing-parameter", "rebaseKeyPaths[].accountKeyPath is missing")); } update.PSBT.RebaseKeyPaths(rebase.AccountKey, rebase.AccountKeyPath); } } if (update.DerivationScheme is DerivationStrategyBase derivationScheme2) { var accountKeyPath = await repo.GetMetadata <RootedKeyPath>( new DerivationSchemeTrackedSource(derivationScheme2), WellknownMetadataKeys.AccountKeyPath); if (accountKeyPath != null) { foreach (var pubkey in derivationScheme2.GetExtPubKeys().Where(p => rebased.Add(p.PubKey))) { update.PSBT.RebaseKeyPaths(pubkey, accountKeyPath); } } } }
private BitcoinDWaiter GetWaiter(NBXplorerNetwork network) { var waiter = Waiters.GetWaiter(network); if (!waiter.RPCAvailable) { throw RPCUnavailable(); } return(waiter); }
private NBXplorerNetwork GetNetwork(string cryptoCode) { if (cryptoCode == null) { throw new ArgumentNullException(nameof(cryptoCode)); } cryptoCode = cryptoCode.ToUpperInvariant(); var network = Waiters.GetWaiter(cryptoCode)?.Network; if (network == null) { throw new NBXplorerException(new NBXplorerError(404, "cryptoCode-not-supported", $"{cryptoCode} is not supported")); } return(network); }
public async Task <IActionResult> Rescan(string cryptoCode, [FromBody] RescanRequest rescanRequest) { if (rescanRequest == null) { throw new ArgumentNullException(nameof(rescanRequest)); } if (rescanRequest?.Transactions == null) { throw new NBXplorerException(new NBXplorerError(400, "transactions-missing", "You must specify 'transactions'")); } bool willFetchTransactions = rescanRequest.Transactions.Any(t => t.Transaction == null); bool needTxIndex = rescanRequest.Transactions.Any(t => t.Transaction == null && t.BlockId == null); var network = GetNetwork(cryptoCode, willFetchTransactions); var rpc = Waiters.GetWaiter(cryptoCode).RPC.PrepareBatch(); var repo = RepositoryProvider.GetRepository(network); var fetchingTransactions = rescanRequest .Transactions .Select(t => FetchTransaction(rpc, t)) .ToArray(); await rpc.SendBatchAsync(); await Task.WhenAll(fetchingTransactions); var transactions = fetchingTransactions.Select(t => t.GetAwaiter().GetResult()) .Where(tx => tx.Transaction != null) .ToArray(); foreach (var txs in transactions.GroupBy(t => t.BlockId, t => (t.Transaction, t.BlockTime)) .OrderBy(t => t.First().BlockTime)) { repo.SaveTransactions(txs.First().BlockTime, txs.Select(t => t.Transaction).ToArray(), txs.Key); foreach (var tx in txs) { var matches = repo.GetMatches(tx.Transaction).Select(m => new MatchedTransaction() { BlockId = txs.Key, Match = m }).ToArray(); repo.SaveMatches(tx.BlockTime, matches); AddressPoolService.RefillAddressPoolIfNeeded(network, matches); } } return(Ok()); }
public async Task <GetFeeRateResult> GetFeeRate(int blockCount, string cryptoCode) { var network = GetNetwork(cryptoCode, true); if (!network.SupportEstimatesSmartFee) { throw new NBXplorerError(400, "fee-estimation-unavailable", $"{cryptoCode} does not support estimatesmartfee").AsException(); } var waiter = Waiters.GetWaiter(network); var rate = await waiter.RPC.GetFeeRateAsyncEx(blockCount); if (rate == null) { throw new NBXplorerError(400, "fee-estimation-unavailable", $"It is currently impossible to estimate fees, please try again later.").AsException(); } return(rate); }
public async Task <GetFeeRateResult> GetFeeRate(int blockCount, string cryptoCode) { var network = GetNetwork(cryptoCode, true); var waiter = Waiters.GetWaiter(network); EstimateSmartFeeResponse rate = null; try { rate = await waiter.RPC.TryEstimateSmartFeeAsync(blockCount); } catch (RPCException ex) when(ex.RPCCode == RPCErrorCode.RPC_METHOD_NOT_FOUND) { } if (rate == null) { throw new NBXplorerError(400, "fee-estimation-unavailable", $"It is currently impossible to estimate fees, please try again later.").AsException(); } return(new GetFeeRateResult() { BlockCount = rate.Blocks, FeeRate = rate.FeeRate }); }
private NBXplorerNetwork GetNetwork(string cryptoCode, bool checkRPC) { if (cryptoCode == null) { throw new ArgumentNullException(nameof(cryptoCode)); } cryptoCode = cryptoCode.ToUpperInvariant(); var network = Waiters.GetWaiter(cryptoCode)?.Network; if (network == null) { throw new NBXplorerException(new NBXplorerError(404, "cryptoCode-not-supported", $"{cryptoCode} is not supported")); } if (checkRPC) { var waiter = Waiters.GetWaiter(network); if (waiter == null || !waiter.RPCAvailable || waiter.RPC.Capabilities == null) { throw new NBXplorerError(400, "rpc-unavailable", $"The RPC interface is currently not available.").AsException(); } } return(network); }
public async Task <IActionResult> GetStatus(string cryptoCode) { var network = GetNetwork(cryptoCode, false); var waiter = Waiters.GetWaiter(network); var chain = ChainProvider.GetChain(network); var repo = RepositoryProvider.GetRepository(network); var location = waiter.GetLocation(); GetBlockchainInfoResponse blockchainInfo = null; if (waiter.RPCAvailable) { try { var rpc = waiter.RPC.Clone(); rpc.RequestTimeout = TimeSpan.FromMinutes(1.0); blockchainInfo = await rpc.GetBlockchainInfoAsyncEx(); } catch (OperationCanceledException) // Timeout, can happen if core is really busy { } } var status = new StatusResult() { NetworkType = network.NBitcoinNetwork.NetworkType, CryptoCode = network.CryptoCode, Version = typeof(MainController).GetTypeInfo().Assembly.GetCustomAttribute <AssemblyFileVersionAttribute>().Version, SupportedCryptoCodes = Waiters.All().Select(w => w.Network.CryptoCode).ToArray(), IsFullySynched = true }; GetNetworkInfoResponse networkInfo = waiter.NetworkInfo; if (blockchainInfo != null && networkInfo != null) { status.BitcoinStatus = new BitcoinStatus() { IsSynched = !waiter.IsSynchingCore(blockchainInfo), Blocks = (int)blockchainInfo.Blocks, Headers = (int)blockchainInfo.Headers, VerificationProgress = blockchainInfo.VerificationProgress, MinRelayTxFee = networkInfo.GetRelayFee(), IncrementalRelayFee = networkInfo.GetIncrementalFee(), Capabilities = new NodeCapabilities() { CanScanTxoutSet = waiter.RPC.Capabilities.SupportScanUTXOSet, CanSupportSegwit = waiter.RPC.Capabilities.SupportSegwit }, ExternalAddresses = (networkInfo.localaddresses ?? Array.Empty <GetNetworkInfoResponse.LocalAddress>()) .Select(l => $"{l.address}:{l.port}").ToArray() }; status.IsFullySynched &= status.BitcoinStatus.IsSynched; } status.ChainHeight = chain.Height; status.SyncHeight = location == null ? (int?)null : chain.FindFork(location).Height; status.IsFullySynched &= blockchainInfo != null && waiter.State == BitcoinDWaiterState.Ready && status.SyncHeight.HasValue && blockchainInfo.Headers - status.SyncHeight.Value < 3; if (status.IsFullySynched) { var now = DateTimeOffset.UtcNow; await repo.Ping(); var pingAfter = DateTimeOffset.UtcNow; status.RepositoryPingTime = (pingAfter - now).TotalSeconds; if (status.RepositoryPingTime > 30) { Logs.Explorer.LogWarning($"Repository ping exceeded 30 seconds ({(int)status.RepositoryPingTime}), please report the issue to NBXplorer developers"); } } return(Json(status)); }
public async Task <IActionResult> CreatePSBT( [ModelBinder(BinderType = typeof(NetworkModelBinder))] NBXplorerNetwork network, [ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))] DerivationStrategyBase strategy, [FromBody] JObject body) { if (body == null) { throw new ArgumentNullException(nameof(body)); } CreatePSBTRequest request = ParseJObject <CreatePSBTRequest>(body, network); if (strategy == null) { throw new ArgumentNullException(nameof(strategy)); } var repo = RepositoryProvider.GetRepository(network); var txBuilder = request.Seed is int s?network.NBitcoinNetwork.CreateTransactionBuilder(s) : network.NBitcoinNetwork.CreateTransactionBuilder(); CreatePSBTSuggestions suggestions = null; if (!(request.DisableFingerprintRandomization is true) && fingerprintService.GetDistribution(network) is FingerprintDistribution distribution) { suggestions ??= new CreatePSBTSuggestions(); var known = new List <(Fingerprint feature, bool value)>(); if (request.RBF is bool rbf) { known.Add((Fingerprint.RBF, rbf)); } if (request.DiscourageFeeSniping is bool feeSnipping) { known.Add((Fingerprint.FeeSniping, feeSnipping)); } if (request.LockTime is LockTime l) { if (l == LockTime.Zero) { known.Add((Fingerprint.TimelockZero, true)); } } if (request.Version is uint version) { if (version == 1) { known.Add((Fingerprint.V1, true)); } if (version == 2) { known.Add((Fingerprint.V2, true)); } } known.Add((Fingerprint.SpendFromMixed, false)); known.Add((Fingerprint.SequenceMixed, false)); if (strategy is DirectDerivationStrategy direct) { if (direct.Segwit) { known.Add((Fingerprint.SpendFromP2WPKH, true)); } else { known.Add((Fingerprint.SpendFromP2PKH, true)); } } else { // TODO: What if multisig? For now we consider it p2wpkh known.Add((Fingerprint.SpendFromP2SHP2WPKH, true)); } Fingerprint fingerprint = distribution.PickFingerprint(txBuilder.ShuffleRandom); try { fingerprint = distribution.KnowingThat(known.ToArray()) .PickFingerprint(txBuilder.ShuffleRandom); } catch (InvalidOperationException) { } request.RBF ??= fingerprint.HasFlag(Fingerprint.RBF); request.DiscourageFeeSniping ??= fingerprint.HasFlag(Fingerprint.FeeSniping); if (request.LockTime is null && fingerprint.HasFlag(Fingerprint.TimelockZero)) { request.LockTime = new LockTime(0); } if (request.Version is null && fingerprint.HasFlag(Fingerprint.V1)) { request.Version = 1; } if (request.Version is null && fingerprint.HasFlag(Fingerprint.V2)) { request.Version = 2; } suggestions.ShouldEnforceLowR = fingerprint.HasFlag(Fingerprint.LowR); } var waiter = Waiters.GetWaiter(network); if (waiter.NetworkInfo?.GetRelayFee() is FeeRate feeRate) { txBuilder.StandardTransactionPolicy.MinRelayTxFee = feeRate; } txBuilder.OptInRBF = !(request.RBF is false); if (request.LockTime is LockTime lockTime) { txBuilder.SetLockTime(lockTime); } // Discourage fee sniping. // // For a large miner the value of the transactions in the best block and // the mempool can exceed the cost of deliberately attempting to mine two // blocks to orphan the current best block. By setting nLockTime such that // only the next block can include the transaction, we discourage this // practice as the height restricted and limited blocksize gives miners // considering fee sniping fewer options for pulling off this attack. // // A simple way to think about this is from the wallet's point of view we // always want the blockchain to move forward. By setting nLockTime this // way we're basically making the statement that we only want this // transaction to appear in the next block; we don't want to potentially // encourage reorgs by allowing transactions to appear at lower heights // than the next block in forks of the best chain. // // Of course, the subsidy is high enough, and transaction volume low // enough, that fee sniping isn't a problem yet, but by implementing a fix // now we ensure code won't be written that makes assumptions about // nLockTime that preclude a fix later. else if (!(request.DiscourageFeeSniping is false)) { if (waiter.State is BitcoinDWaiterState.Ready) { int blockHeight = ChainProvider.GetChain(network).Height; // Secondly occasionally randomly pick a nLockTime even further back, so // that transactions that are delayed after signing for whatever reason, // e.g. high-latency mix networks and some CoinJoin implementations, have // better privacy. if (txBuilder.ShuffleRandom.Next(0, 10) == 0) { blockHeight = Math.Max(0, blockHeight - txBuilder.ShuffleRandom.Next(0, 100)); } txBuilder.SetLockTime(new LockTime(blockHeight)); } else { txBuilder.SetLockTime(new LockTime(0)); } } var utxos = (await GetUTXOs(network.CryptoCode, strategy, null)).As <UTXOChanges>().GetUnspentUTXOs(request.MinConfirmations); var availableCoinsByOutpoint = utxos.ToDictionary(o => o.Outpoint); if (request.IncludeOnlyOutpoints != null) { var includeOnlyOutpoints = request.IncludeOnlyOutpoints.ToHashSet(); availableCoinsByOutpoint = availableCoinsByOutpoint.Where(c => includeOnlyOutpoints.Contains(c.Key)).ToDictionary(o => o.Key, o => o.Value); } if (request.ExcludeOutpoints?.Any() is true) { var excludedOutpoints = request.ExcludeOutpoints.ToHashSet(); availableCoinsByOutpoint = availableCoinsByOutpoint.Where(c => !excludedOutpoints.Contains(c.Key)).ToDictionary(o => o.Key, o => o.Value); } if (request.MinValue != null) { availableCoinsByOutpoint = availableCoinsByOutpoint.Where(c => request.MinValue >= (Money)c.Value.Value).ToDictionary(o => o.Key, o => o.Value); } ICoin[] coins = null; if (strategy.GetDerivation().Redeem != null) { // We need to add the redeem script to the coins var hdKeys = strategy.AsHDRedeemScriptPubKey().AsHDKeyCache(); var arr = availableCoinsByOutpoint.Values.ToArray(); coins = new ICoin[arr.Length]; // Can be very intense CPU wise Parallel.For(0, coins.Length, i => { coins[i] = ((Coin)arr[i].AsCoin()).ToScriptCoin(hdKeys.Derive(arr[i].KeyPath).ScriptPubKey); }); } else { coins = availableCoinsByOutpoint.Values.Select(v => v.AsCoin()).ToArray(); } txBuilder.AddCoins(coins); foreach (var dest in request.Destinations) { if (dest.SweepAll) { try { txBuilder.SendAll(dest.Destination); } catch { throw new NBXplorerException(new NBXplorerError(400, "not-enough-funds", "You can't sweep funds, because you don't have any.")); } } else { txBuilder.Send(dest.Destination, dest.Amount); if (dest.SubstractFees) { try { txBuilder.SubtractFees(); } catch { throw new NBXplorerException(new NBXplorerError(400, "not-enough-funds", "You can't substract fee on this destination, because not enough money was sent to it")); } } } } (Script ScriptPubKey, KeyPath KeyPath)change = (null, null); bool hasChange = false; if (request.ExplicitChangeAddress == null) { var keyInfo = await repo.GetUnused(strategy, DerivationFeature.Change, 0, false); change = (keyInfo.ScriptPubKey, keyInfo.KeyPath); } else { // The provided explicit change might have a known keyPath, let's change for it KeyPath keyPath = null; var keyInfos = await repo.GetKeyInformations(new[] { request.ExplicitChangeAddress.ScriptPubKey }); if (keyInfos.TryGetValue(request.ExplicitChangeAddress.ScriptPubKey, out var kis)) { keyPath = kis.FirstOrDefault(k => k.DerivationStrategy == strategy)?.KeyPath; } change = (request.ExplicitChangeAddress.ScriptPubKey, keyPath); } txBuilder.SetChange(change.ScriptPubKey); PSBT psbt = null; try { if (request.FeePreference?.ExplicitFeeRate is FeeRate explicitFeeRate) { txBuilder.SendEstimatedFees(explicitFeeRate); } else if (request.FeePreference?.BlockTarget is int blockTarget) { try { var rate = await GetFeeRate(blockTarget, network.CryptoCode); txBuilder.SendEstimatedFees(rate.FeeRate); } catch (NBXplorerException e) when(e.Error.Code == "fee-estimation-unavailable" && request.FeePreference?.FallbackFeeRate is FeeRate fallbackFeeRate) { txBuilder.SendEstimatedFees(fallbackFeeRate); } } else if (request.FeePreference?.ExplicitFee is Money explicitFee) { txBuilder.SendFees(explicitFee); } else { try { var rate = await GetFeeRate(1, network.CryptoCode); txBuilder.SendEstimatedFees(rate.FeeRate); } catch (NBXplorerException e) when(e.Error.Code == "fee-estimation-unavailable" && request.FeePreference?.FallbackFeeRate is FeeRate fallbackFeeRate) { txBuilder.SendEstimatedFees(fallbackFeeRate); } } psbt = txBuilder.BuildPSBT(false); hasChange = psbt.Outputs.Any(o => o.ScriptPubKey == change.ScriptPubKey); } catch (NotEnoughFundsException) { throw new NBXplorerException(new NBXplorerError(400, "not-enough-funds", "Not enough funds for doing this transaction")); } // We made sure we can build the PSBT, so now we can reserve the change address if we need to if (hasChange && request.ExplicitChangeAddress == null && request.ReserveChangeAddress) { var derivation = await repo.GetUnused(strategy, DerivationFeature.Change, 0, true); // In most of the time, this is the same as previously, so no need to rebuild PSBT if (derivation.ScriptPubKey != change.ScriptPubKey) { change = (derivation.ScriptPubKey, derivation.KeyPath); txBuilder.SetChange(change.ScriptPubKey); psbt = txBuilder.BuildPSBT(false); } } var tx = psbt.GetOriginalTransaction(); if (request.Version is uint v) { tx.Version = v; } psbt = txBuilder.CreatePSBTFrom(tx, false, SigHash.All); var update = new UpdatePSBTRequest() { DerivationScheme = strategy, PSBT = psbt, RebaseKeyPaths = request.RebaseKeyPaths, AlwaysIncludeNonWitnessUTXO = request.AlwaysIncludeNonWitnessUTXO, IncludeGlobalXPub = request.IncludeGlobalXPub }; await UpdatePSBTCore(update, network); var resp = new CreatePSBTResponse() { PSBT = update.PSBT, ChangeAddress = hasChange ? change.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork) : null, Suggestions = suggestions }; return(Json(resp, network.JsonSerializerSettings)); }
public async Task <IActionResult> CreatePSBT( [ModelBinder(BinderType = typeof(NetworkModelBinder))] NBXplorerNetwork network, [ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))] DerivationStrategyBase strategy, [FromBody] JObject body) { if (body == null) { throw new ArgumentNullException(nameof(body)); } CreatePSBTRequest request = ParseJObject <CreatePSBTRequest>(body, network); if (strategy == null) { throw new ArgumentNullException(nameof(strategy)); } var repo = RepositoryProvider.GetRepository(network); var txBuilder = request.Seed is int s?network.NBitcoinNetwork.CreateTransactionBuilder(s) : network.NBitcoinNetwork.CreateTransactionBuilder(); if (Waiters.GetWaiter(network).NetworkInfo?.GetRelayFee() is FeeRate feeRate) { txBuilder.StandardTransactionPolicy.MinRelayTxFee = feeRate; } txBuilder.OptInRBF = request.RBF; if (request.LockTime is LockTime lockTime) { txBuilder.SetLockTime(lockTime); txBuilder.OptInRBF = true; } var utxos = (await GetUTXOs(network.CryptoCode, strategy, null)).GetUnspentCoins(request.MinConfirmations); var availableCoinsByOutpoint = utxos.ToDictionary(o => o.Outpoint); if (request.IncludeOnlyOutpoints != null) { var includeOnlyOutpoints = request.IncludeOnlyOutpoints.ToHashSet(); availableCoinsByOutpoint = availableCoinsByOutpoint.Where(c => includeOnlyOutpoints.Contains(c.Key)).ToDictionary(o => o.Key, o => o.Value); } if (request.ExcludeOutpoints?.Any() is true) { var excludedOutpoints = request.ExcludeOutpoints.ToHashSet(); availableCoinsByOutpoint = availableCoinsByOutpoint.Where(c => !excludedOutpoints.Contains(c.Key)).ToDictionary(o => o.Key, o => o.Value); } txBuilder.AddCoins(availableCoinsByOutpoint.Values); foreach (var dest in request.Destinations) { if (dest.SweepAll) { txBuilder.SendAll(dest.Destination); } else { txBuilder.Send(dest.Destination, dest.Amount); if (dest.SubstractFees) { txBuilder.SubtractFees(); } } } (Script ScriptPubKey, KeyPath KeyPath)change = (null, null); bool hasChange = false; // We first build the transaction with a change which keep the length of the expected change scriptPubKey // This allow us to detect if there is a change later in the constructed transaction. // This defend against bug which can happen if one of the destination is the same as the expected change // This assume that a script with only 0 can't be created from a strategy, nor by passing any data to explicitChangeAddress if (request.ExplicitChangeAddress == null) { // The dummyScriptPubKey is necessary to know the size of the change var dummyScriptPubKey = utxos.FirstOrDefault()?.ScriptPubKey ?? strategy.GetDerivation(0).ScriptPubKey; change = (Script.FromBytesUnsafe(new byte[dummyScriptPubKey.Length]), null); } else { change = (Script.FromBytesUnsafe(new byte[request.ExplicitChangeAddress.ScriptPubKey.Length]), null); } txBuilder.SetChange(change.ScriptPubKey); PSBT psbt = null; try { if (request.FeePreference?.ExplicitFeeRate is FeeRate explicitFeeRate) { txBuilder.SendEstimatedFees(explicitFeeRate); } else if (request.FeePreference?.BlockTarget is int blockTarget) { try { var rate = await GetFeeRate(blockTarget, network.CryptoCode); txBuilder.SendEstimatedFees(rate.FeeRate); } catch (NBXplorerException e) when(e.Error.Code == "fee-estimation-unavailable" && request.FeePreference?.FallbackFeeRate is FeeRate fallbackFeeRate) { txBuilder.SendEstimatedFees(fallbackFeeRate); } } else if (request.FeePreference?.ExplicitFee is Money explicitFee) { txBuilder.SendFees(explicitFee); } else { try { var rate = await GetFeeRate(1, network.CryptoCode); txBuilder.SendEstimatedFees(rate.FeeRate); } catch (NBXplorerException e) when(e.Error.Code == "fee-estimation-unavailable" && request.FeePreference?.FallbackFeeRate is FeeRate fallbackFeeRate) { txBuilder.SendEstimatedFees(fallbackFeeRate); } } psbt = txBuilder.BuildPSBT(false); hasChange = psbt.Outputs.Any(o => o.ScriptPubKey == change.ScriptPubKey); } catch (NotEnoughFundsException) { throw new NBXplorerException(new NBXplorerError(400, "not-enough-funds", "Not enough funds for doing this transaction")); } if (hasChange) // We need to reserve an address, so we need to build again the psbt { if (request.ExplicitChangeAddress == null) { var derivation = await repo.GetUnused(strategy, DerivationFeature.Change, 0, request.ReserveChangeAddress); change = (derivation.ScriptPubKey, derivation.KeyPath); } else { change = (request.ExplicitChangeAddress.ScriptPubKey, null); } txBuilder.SetChange(change.ScriptPubKey); psbt = txBuilder.BuildPSBT(false); } var tx = psbt.GetOriginalTransaction(); if (request.Version is uint v) { tx.Version = v; } psbt = txBuilder.CreatePSBTFrom(tx, false, SigHash.All); // Maybe it is a change that we know about, let's search in the DB if (hasChange && change.KeyPath == null) { var keyInfos = await repo.GetKeyInformations(new[] { request.ExplicitChangeAddress.ScriptPubKey }); if (keyInfos.TryGetValue(request.ExplicitChangeAddress.ScriptPubKey, out var kis)) { var ki = kis.FirstOrDefault(k => k.DerivationStrategy == strategy); if (ki != null) { change = (change.ScriptPubKey, kis.First().KeyPath); } } } await UpdatePSBTCore(new UpdatePSBTRequest() { DerivationScheme = strategy, PSBT = psbt, RebaseKeyPaths = request.RebaseKeyPaths }, network); var resp = new CreatePSBTResponse() { PSBT = psbt, ChangeAddress = hasChange ? change.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork) : null }; return(Json(resp, network.JsonSerializerSettings)); }
public async Task <IActionResult> ConnectWebSocket( string cryptoCode, bool includeTransaction = true, CancellationToken cancellation = default) { if (!HttpContext.WebSockets.IsWebSocketRequest) { return(NotFound()); } GetNetwork(cryptoCode, false); // Internally check if cryptoCode is correct string listenAllDerivationSchemes = null; string listenAllTrackedSource = null; var listenedBlocks = new ConcurrentDictionary <string, string>(); var listenedDerivations = new ConcurrentDictionary <(Network, DerivationStrategyBase), DerivationStrategyBase>(); var listenedTrackedSource = new ConcurrentDictionary <(Network, TrackedSource), TrackedSource>(); WebsocketMessageListener server = new WebsocketMessageListener(await HttpContext.WebSockets.AcceptWebSocketAsync(), _SerializerSettings); CompositeDisposable subscriptions = new CompositeDisposable(); subscriptions.Add(_EventAggregator.Subscribe <Models.NewBlockEvent>(async o => { if (listenedBlocks.ContainsKey(o.CryptoCode)) { await server.Send(o); } })); subscriptions.Add(_EventAggregator.Subscribe <Models.NewTransactionEvent>(async o => { var network = Waiters.GetWaiter(o.CryptoCode); if (network == null) { return; } bool forward = false; var derivationScheme = (o.TrackedSource as DerivationSchemeTrackedSource)?.DerivationStrategy; if (derivationScheme != null) { forward |= listenAllDerivationSchemes == "*" || listenAllDerivationSchemes == o.CryptoCode || listenedDerivations.ContainsKey((network.Network.NBitcoinNetwork, derivationScheme)); } forward |= listenAllTrackedSource == "*" || listenAllTrackedSource == o.CryptoCode || listenedTrackedSource.ContainsKey((network.Network.NBitcoinNetwork, o.TrackedSource)); if (forward) { var derivation = (o.TrackedSource as DerivationSchemeTrackedSource)?.DerivationStrategy; await server.Send(o); } })); try { while (server.Socket.State == WebSocketState.Open) { object message = await server.NextMessageAsync(cancellation); switch (message) { case Models.NewBlockEventRequest r: r.CryptoCode = r.CryptoCode ?? cryptoCode; listenedBlocks.TryAdd(r.CryptoCode, r.CryptoCode); break; case Models.NewTransactionEventRequest r: var network = Waiters.GetWaiter(r.CryptoCode)?.Network; if (r.DerivationSchemes != null) { r.CryptoCode = r.CryptoCode ?? cryptoCode; if (network != null) { foreach (var derivation in r.DerivationSchemes) { var parsed = new DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivation); listenedDerivations.TryAdd((network.NBitcoinNetwork, parsed), parsed); } } } else if ( // Back compat: If no derivation scheme precised and ListenAllDerivationSchemes not set, we listen all (r.TrackedSources == null && r.ListenAllDerivationSchemes == null) || (r.ListenAllDerivationSchemes != null && r.ListenAllDerivationSchemes.Value)) { listenAllDerivationSchemes = r.CryptoCode; } if (r.ListenAllTrackedSource != null && r.ListenAllTrackedSource.Value) { listenAllTrackedSource = r.CryptoCode; } else if (r.TrackedSources != null) { r.CryptoCode = r.CryptoCode ?? cryptoCode; if (network != null) { foreach (var trackedSource in r.TrackedSources) { if (TrackedSource.TryParse(trackedSource, out var parsed, network.NBitcoinNetwork)) { listenedTrackedSource.TryAdd((network.NBitcoinNetwork, parsed), parsed); } } } } break; default: break; } } } catch when(server.Socket.State != WebSocketState.Open) { } finally { subscriptions.Dispose(); await server.DisposeAsync(cancellation); } return(new EmptyResult()); }
public async Task <IActionResult> ConnectWebSocket( string cryptoCode, bool includeTransaction = true, CancellationToken cancellation = default) { if (!HttpContext.WebSockets.IsWebSocketRequest) { return(NotFound()); } GetNetwork(cryptoCode); // Internally check if cryptoCode is correct string listenAllDerivationSchemes = null; var listenedBlocks = new ConcurrentDictionary <string, string>(); var listenedDerivations = new ConcurrentDictionary <(Network, DerivationStrategyBase), DerivationStrategyBase>(); WebsocketMessageListener server = new WebsocketMessageListener(await HttpContext.WebSockets.AcceptWebSocketAsync(), _SerializerSettings); CompositeDisposable subscriptions = new CompositeDisposable(); subscriptions.Add(_EventAggregator.Subscribe <Events.NewBlockEvent>(async o => { if (listenedBlocks.ContainsKey(o.CryptoCode)) { var chain = ChainProvider.GetChain(o.CryptoCode); if (chain == null) { return; } var block = chain.GetBlock(o.BlockId); if (block != null) { await server.Send(new Models.NewBlockEvent() { CryptoCode = o.CryptoCode, Hash = block.HashBlock, Height = block.Height, PreviousBlockHash = block?.Previous.HashBlock }); } } })); subscriptions.Add(_EventAggregator.Subscribe <Events.NewTransactionMatchEvent>(async o => { var network = Waiters.GetWaiter(o.CryptoCode); if (network == null) { return; } if ( listenAllDerivationSchemes == "*" || listenAllDerivationSchemes == o.CryptoCode || listenedDerivations.ContainsKey((network.Network.NBitcoinNetwork, o.Match.DerivationStrategy))) { var chain = ChainProvider.GetChain(o.CryptoCode); if (chain == null) { return; } var blockHeader = o.BlockId == null ? null : chain.GetBlock(o.BlockId); await server.Send(new Models.NewTransactionEvent() { CryptoCode = o.CryptoCode, DerivationStrategy = o.Match.DerivationStrategy, BlockId = blockHeader?.HashBlock, TransactionData = ToTransactionResult(includeTransaction, chain, new[] { o.SavedTransaction }), Inputs = o.Match.Inputs, Outputs = o.Match.Outputs }); } })); try { while (server.Socket.State == WebSocketState.Open) { object message = await server.NextMessageAsync(cancellation); switch (message) { case Models.NewBlockEventRequest r: r.CryptoCode = r.CryptoCode ?? cryptoCode; listenedBlocks.TryAdd(r.CryptoCode, r.CryptoCode); break; case Models.NewTransactionEventRequest r: if (r.DerivationSchemes != null) { r.CryptoCode = r.CryptoCode ?? cryptoCode; var network = Waiters.GetWaiter(r.CryptoCode)?.Network; if (network == null) { break; } foreach (var derivation in r.DerivationSchemes) { var parsed = new DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivation); listenedDerivations.TryAdd((network.NBitcoinNetwork, parsed), parsed); } } else { listenAllDerivationSchemes = r.CryptoCode; } break; default: break; } } } catch when(server.Socket.State != WebSocketState.Open) { } finally { subscriptions.Dispose(); await server.DisposeAsync(cancellation); } return(new EmptyResult()); }
public async Task <IActionResult> CreatePSBT( [ModelBinder(BinderType = typeof(NetworkModelBinder))] NBXplorerNetwork network, [ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))] DerivationStrategyBase strategy, [FromBody] JObject body) { if (body == null) { throw new ArgumentNullException(nameof(body)); } CreatePSBTRequest request = ParseJObject <CreatePSBTRequest>(body, network); if (strategy == null) { throw new ArgumentNullException(nameof(strategy)); } var repo = RepositoryProvider.GetRepository(network); var txBuilder = request.Seed is int s?network.NBitcoinNetwork.CreateTransactionBuilder(s) : network.NBitcoinNetwork.CreateTransactionBuilder(); if (Waiters.GetWaiter(network).NetworkInfo?.GetRelayFee() is FeeRate feeRate) { txBuilder.StandardTransactionPolicy.MinRelayTxFee = feeRate; } txBuilder.OptInRBF = request.RBF; if (request.LockTime is LockTime lockTime) { txBuilder.SetLockTime(lockTime); txBuilder.OptInRBF = true; } var utxos = (await GetUTXOs(network.CryptoCode, strategy, null)).As <UTXOChanges>().GetUnspentCoins(request.MinConfirmations); var availableCoinsByOutpoint = utxos.ToDictionary(o => o.Outpoint); if (request.IncludeOnlyOutpoints != null) { var includeOnlyOutpoints = request.IncludeOnlyOutpoints.ToHashSet(); availableCoinsByOutpoint = availableCoinsByOutpoint.Where(c => includeOnlyOutpoints.Contains(c.Key)).ToDictionary(o => o.Key, o => o.Value); } if (request.ExcludeOutpoints?.Any() is true) { var excludedOutpoints = request.ExcludeOutpoints.ToHashSet(); availableCoinsByOutpoint = availableCoinsByOutpoint.Where(c => !excludedOutpoints.Contains(c.Key)).ToDictionary(o => o.Key, o => o.Value); } if (request.MinValue != null) { availableCoinsByOutpoint = availableCoinsByOutpoint.Where(c => request.MinValue >= c.Value.Amount).ToDictionary(o => o.Key, o => o.Value); } txBuilder.AddCoins(availableCoinsByOutpoint.Values); foreach (var dest in request.Destinations) { if (dest.SweepAll) { try { txBuilder.SendAll(dest.Destination); } catch { throw new NBXplorerException(new NBXplorerError(400, "not-enough-funds", "You can't sweep funds, because you don't have any.")); } } else { txBuilder.Send(dest.Destination, dest.Amount); if (dest.SubstractFees) { try { txBuilder.SubtractFees(); } catch { throw new NBXplorerException(new NBXplorerError(400, "not-enough-funds", "You can't substract fee on this destination, because not enough money was sent to it")); } } } } (Script ScriptPubKey, KeyPath KeyPath)change = (null, null); bool hasChange = false; if (request.ExplicitChangeAddress == null) { var keyInfo = await repo.GetUnused(strategy, DerivationFeature.Change, 0, false); change = (keyInfo.ScriptPubKey, keyInfo.KeyPath); } else { // The provided explicit change might have a known keyPath, let's change for it KeyPath keyPath = null; var keyInfos = await repo.GetKeyInformations(new[] { request.ExplicitChangeAddress.ScriptPubKey }); if (keyInfos.TryGetValue(request.ExplicitChangeAddress.ScriptPubKey, out var kis)) { keyPath = kis.FirstOrDefault(k => k.DerivationStrategy == strategy)?.KeyPath; } change = (request.ExplicitChangeAddress.ScriptPubKey, keyPath); } txBuilder.SetChange(change.ScriptPubKey); PSBT psbt = null; try { if (request.FeePreference?.ExplicitFeeRate is FeeRate explicitFeeRate) { txBuilder.SendEstimatedFees(explicitFeeRate); } else if (request.FeePreference?.BlockTarget is int blockTarget) { try { var rate = await GetFeeRate(blockTarget, network.CryptoCode); txBuilder.SendEstimatedFees(rate.FeeRate); } catch (NBXplorerException e) when(e.Error.Code == "fee-estimation-unavailable" && request.FeePreference?.FallbackFeeRate is FeeRate fallbackFeeRate) { txBuilder.SendEstimatedFees(fallbackFeeRate); } } else if (request.FeePreference?.ExplicitFee is Money explicitFee) { txBuilder.SendFees(explicitFee); } else { try { var rate = await GetFeeRate(1, network.CryptoCode); txBuilder.SendEstimatedFees(rate.FeeRate); } catch (NBXplorerException e) when(e.Error.Code == "fee-estimation-unavailable" && request.FeePreference?.FallbackFeeRate is FeeRate fallbackFeeRate) { txBuilder.SendEstimatedFees(fallbackFeeRate); } } psbt = txBuilder.BuildPSBT(false); hasChange = psbt.Outputs.Any(o => o.ScriptPubKey == change.ScriptPubKey); } catch (NotEnoughFundsException) { throw new NBXplorerException(new NBXplorerError(400, "not-enough-funds", "Not enough funds for doing this transaction")); } // We made sure we can build the PSBT, so now we can reserve the change address if we need to if (hasChange && request.ExplicitChangeAddress == null && request.ReserveChangeAddress) { var derivation = await repo.GetUnused(strategy, DerivationFeature.Change, 0, true); // In most of the time, this is the same as previously, so no need to rebuild PSBT if (derivation.ScriptPubKey != change.ScriptPubKey) { change = (derivation.ScriptPubKey, derivation.KeyPath); txBuilder.SetChange(change.ScriptPubKey); psbt = txBuilder.BuildPSBT(false); } } var tx = psbt.GetOriginalTransaction(); if (request.Version is uint v) { tx.Version = v; } psbt = txBuilder.CreatePSBTFrom(tx, false, SigHash.All); await UpdatePSBTCore(new UpdatePSBTRequest() { DerivationScheme = strategy, PSBT = psbt, RebaseKeyPaths = request.RebaseKeyPaths }, network); var resp = new CreatePSBTResponse() { PSBT = psbt, ChangeAddress = hasChange ? change.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork) : null }; return(Json(resp, network.JsonSerializerSettings)); }
public async Task <IActionResult> CreatePSBT( [ModelBinder(BinderType = typeof(NetworkModelBinder))] NBXplorerNetwork network, [ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))] DerivationStrategyBase strategy, [FromBody] JObject body) { if (body == null) { throw new ArgumentNullException(nameof(body)); } CreatePSBTRequest request = ParseJObject <CreatePSBTRequest>(body, network); if (strategy == null) { throw new ArgumentNullException(nameof(strategy)); } var repo = RepositoryProvider.GetRepository(network); var utxos = await GetUTXOs(network.CryptoCode, strategy, null); var txBuilder = request.Seed is int s?network.NBitcoinNetwork.CreateTransactionBuilder(s) : network.NBitcoinNetwork.CreateTransactionBuilder(); if (Waiters.GetWaiter(network).NetworkInfo?.GetRelayFee() is FeeRate feeRate) { txBuilder.StandardTransactionPolicy.MinRelayTxFee = feeRate; } txBuilder.OptInRBF = request.RBF; if (request.LockTime is LockTime lockTime) { txBuilder.SetLockTime(lockTime); txBuilder.OptInRBF = true; } var availableCoinsByOutpoint = utxos.GetUnspentCoins(request.MinConfirmations).ToDictionary(o => o.Outpoint); if (request.IncludeOnlyOutpoints != null) { var includeOnlyOutpoints = request.IncludeOnlyOutpoints.ToHashSet(); availableCoinsByOutpoint = availableCoinsByOutpoint.Where(c => includeOnlyOutpoints.Contains(c.Key)).ToDictionary(o => o.Key, o => o.Value); } if (request.ExcludeOutpoints?.Any() is true) { var excludedOutpoints = request.ExcludeOutpoints.ToHashSet(); availableCoinsByOutpoint = availableCoinsByOutpoint.Where(c => !excludedOutpoints.Contains(c.Key)).ToDictionary(o => o.Key, o => o.Value); } txBuilder.AddCoins(availableCoinsByOutpoint.Values); foreach (var dest in request.Destinations) { if (dest.SweepAll) { txBuilder.SendAll(dest.Destination); } else { txBuilder.Send(dest.Destination, dest.Amount); if (dest.SubstractFees) { txBuilder.SubtractFees(); } } } (Script ScriptPubKey, KeyPath KeyPath)change = (null, null); bool hasChange = false; // We first build the transaction with a change which keep the length of the expected change scriptPubKey // This allow us to detect if there is a change later in the constructed transaction. // This defend against bug which can happen if one of the destination is the same as the expected change // This assume that a script with only 0 can't be created from a strategy, nor by passing any data to explicitChangeAddress if (request.ExplicitChangeAddress == null) { // The dummyScriptPubKey is necessary to know the size of the change var dummyScriptPubKey = utxos.Unconfirmed.UTXOs.FirstOrDefault()?.ScriptPubKey ?? utxos.Confirmed.UTXOs.FirstOrDefault()?.ScriptPubKey ?? strategy.Derive(0).ScriptPubKey; change = (Script.FromBytesUnsafe(new byte[dummyScriptPubKey.Length]), null); } else { change = (Script.FromBytesUnsafe(new byte[request.ExplicitChangeAddress.ScriptPubKey.Length]), null); } txBuilder.SetChange(change.ScriptPubKey); PSBT psbt = null; try { if (request.FeePreference?.ExplicitFeeRate is FeeRate explicitFeeRate) { txBuilder.SendEstimatedFees(explicitFeeRate); } else if (request.FeePreference?.BlockTarget is int blockTarget) { try { var rate = await GetFeeRate(blockTarget, network.CryptoCode); txBuilder.SendEstimatedFees(rate.FeeRate); } catch (NBXplorerException e) when(e.Error.Code == "fee-estimation-unavailable" && request.FeePreference?.FallbackFeeRate is FeeRate fallbackFeeRate) { txBuilder.SendEstimatedFees(fallbackFeeRate); } } else if (request.FeePreference?.ExplicitFee is Money explicitFee) { txBuilder.SendFees(explicitFee); } else { try { var rate = await GetFeeRate(1, network.CryptoCode); txBuilder.SendEstimatedFees(rate.FeeRate); } catch (NBXplorerException e) when(e.Error.Code == "fee-estimation-unavailable" && request.FeePreference?.FallbackFeeRate is FeeRate fallbackFeeRate) { txBuilder.SendEstimatedFees(fallbackFeeRate); } } psbt = txBuilder.BuildPSBT(false); hasChange = psbt.Outputs.Any(o => o.ScriptPubKey == change.ScriptPubKey); } catch (NotEnoughFundsException) { throw new NBXplorerException(new NBXplorerError(400, "not-enough-funds", "Not enough funds for doing this transaction")); } if (hasChange) // We need to reserve an address, so we need to build again the psbt { if (request.ExplicitChangeAddress == null) { var derivation = await repo.GetUnused(strategy, DerivationFeature.Change, 0, request.ReserveChangeAddress); change = (derivation.ScriptPubKey, derivation.KeyPath); } else { change = (request.ExplicitChangeAddress.ScriptPubKey, null); } txBuilder.SetChange(change.ScriptPubKey); psbt = txBuilder.BuildPSBT(false); } var tx = psbt.GetOriginalTransaction(); if (request.Version is uint v) { tx.Version = v; } psbt = txBuilder.CreatePSBTFrom(tx, false, SigHash.All); var outputsKeyInformations = repo.GetKeyInformations(psbt.Outputs.Where(o => !o.HDKeyPaths.Any()).Select(o => o.ScriptPubKey).ToArray()); var utxosByOutpoint = utxos.GetUnspentUTXOs().ToDictionary(u => u.Outpoint); // Maybe it is a change that we know about, let's search in the DB if (hasChange && change.KeyPath == null) { var keyInfos = await repo.GetKeyInformations(new[] { request.ExplicitChangeAddress.ScriptPubKey }); if (keyInfos.TryGetValue(request.ExplicitChangeAddress.ScriptPubKey, out var kis)) { var ki = kis.FirstOrDefault(k => k.DerivationStrategy == strategy); if (ki != null) { change = (change.ScriptPubKey, kis.First().KeyPath); } } } var pubkeys = strategy.GetExtPubKeys().Select(p => p.AsHDKeyCache()).ToArray(); var keyPaths = psbt.Inputs.Select(i => utxosByOutpoint[i.PrevOut].KeyPath).ToList(); if (hasChange && change.KeyPath != null) { keyPaths.Add(change.KeyPath); } var fps = new Dictionary <PubKey, HDFingerprint>(); foreach (var pubkey in pubkeys) { // We derive everything the fastest way possible on multiple cores pubkey.Derive(keyPaths.ToArray()); fps.TryAdd(pubkey.GetPublicKey(), pubkey.GetPublicKey().GetHDFingerPrint()); } foreach (var input in psbt.Inputs) { var utxo = utxosByOutpoint[input.PrevOut]; foreach (var pubkey in pubkeys) { var childPubKey = pubkey.Derive(utxo.KeyPath); NBitcoin.Extensions.TryAdd(input.HDKeyPaths, childPubKey.GetPublicKey(), Tuple.Create(fps[pubkey.GetPublicKey()], utxo.KeyPath)); } } await Task.WhenAll(psbt.Inputs .Select(async(input) => { if (input.WitnessUtxo == null) // We need previous tx { var prev = await repo.GetSavedTransactions(input.PrevOut.Hash); if (prev?.Any() is true) { input.NonWitnessUtxo = prev[0].Transaction; } } }).ToArray()); var outputsKeyInformationsResult = await outputsKeyInformations; foreach (var output in psbt.Outputs) { foreach (var keyInfo in outputsKeyInformationsResult[output.ScriptPubKey].Where(o => o.DerivationStrategy == strategy)) { foreach (var pubkey in pubkeys) { var childPubKey = pubkey.Derive(keyInfo.KeyPath); NBitcoin.Extensions.TryAdd(output.HDKeyPaths, childPubKey.GetPublicKey(), Tuple.Create(fps[pubkey.GetPublicKey()], keyInfo.KeyPath)); } } } if (request.RebaseKeyPaths != null) { foreach (var rebase in request.RebaseKeyPaths) { if (rebase.AccountKeyPath == null) { throw new NBXplorerException(new NBXplorerError(400, "missing-parameter", "rebaseKeyPaths[].accountKeyPath is missing")); } if (rebase.AccountKey == null) { throw new NBXplorerException(new NBXplorerError(400, "missing-parameter", "rebaseKeyPaths[].accountKey is missing")); } psbt.RebaseKeyPaths(rebase.AccountKey, rebase.AccountKeyPath, rebase.MasterFingerprint); } } var resp = new CreatePSBTResponse() { PSBT = psbt, ChangeAddress = hasChange ? change.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork) : null }; return(Json(resp, network.JsonSerializerSettings)); }