public async Task <IActionResult> CreatePSBT( [ModelBinder(BinderType = typeof(NetworkModelBinder))] NBXplorerNetwork network, [ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))] DerivationStrategyBase strategy, [FromBody] JObject body) { if (body == null) { throw new ArgumentNullException(nameof(body)); } CreatePSBTRequest request = ParseJObject <CreatePSBTRequest>(body, network); if (strategy == null) { throw new ArgumentNullException(nameof(strategy)); } var repo = RepositoryProvider.GetRepository(network); var txBuilder = request.Seed is int s?network.NBitcoinNetwork.CreateTransactionBuilder(s) : network.NBitcoinNetwork.CreateTransactionBuilder(); if (Waiters.GetWaiter(network).NetworkInfo?.GetRelayFee() is FeeRate feeRate) { txBuilder.StandardTransactionPolicy.MinRelayTxFee = feeRate; } txBuilder.OptInRBF = request.RBF; if (request.LockTime is LockTime lockTime) { txBuilder.SetLockTime(lockTime); txBuilder.OptInRBF = true; } var utxos = (await GetUTXOs(network.CryptoCode, strategy, null)).As <UTXOChanges>().GetUnspentCoins(request.MinConfirmations); var availableCoinsByOutpoint = utxos.ToDictionary(o => o.Outpoint); if (request.IncludeOnlyOutpoints != null) { var includeOnlyOutpoints = request.IncludeOnlyOutpoints.ToHashSet(); availableCoinsByOutpoint = availableCoinsByOutpoint.Where(c => includeOnlyOutpoints.Contains(c.Key)).ToDictionary(o => o.Key, o => o.Value); } if (request.ExcludeOutpoints?.Any() is true) { var excludedOutpoints = request.ExcludeOutpoints.ToHashSet(); availableCoinsByOutpoint = availableCoinsByOutpoint.Where(c => !excludedOutpoints.Contains(c.Key)).ToDictionary(o => o.Key, o => o.Value); } if (request.MinValue != null) { availableCoinsByOutpoint = availableCoinsByOutpoint.Where(c => request.MinValue >= c.Value.Amount).ToDictionary(o => o.Key, o => o.Value); } txBuilder.AddCoins(availableCoinsByOutpoint.Values); foreach (var dest in request.Destinations) { if (dest.SweepAll) { try { txBuilder.SendAll(dest.Destination); } catch { throw new NBXplorerException(new NBXplorerError(400, "not-enough-funds", "You can't sweep funds, because you don't have any.")); } } else { txBuilder.Send(dest.Destination, dest.Amount); if (dest.SubstractFees) { try { txBuilder.SubtractFees(); } catch { throw new NBXplorerException(new NBXplorerError(400, "not-enough-funds", "You can't substract fee on this destination, because not enough money was sent to it")); } } } } (Script ScriptPubKey, KeyPath KeyPath)change = (null, null); bool hasChange = false; if (request.ExplicitChangeAddress == null) { var keyInfo = await repo.GetUnused(strategy, DerivationFeature.Change, 0, false); change = (keyInfo.ScriptPubKey, keyInfo.KeyPath); } else { // The provided explicit change might have a known keyPath, let's change for it KeyPath keyPath = null; var keyInfos = await repo.GetKeyInformations(new[] { request.ExplicitChangeAddress.ScriptPubKey }); if (keyInfos.TryGetValue(request.ExplicitChangeAddress.ScriptPubKey, out var kis)) { keyPath = kis.FirstOrDefault(k => k.DerivationStrategy == strategy)?.KeyPath; } change = (request.ExplicitChangeAddress.ScriptPubKey, keyPath); } txBuilder.SetChange(change.ScriptPubKey); PSBT psbt = null; try { if (request.FeePreference?.ExplicitFeeRate is FeeRate explicitFeeRate) { txBuilder.SendEstimatedFees(explicitFeeRate); } else if (request.FeePreference?.BlockTarget is int blockTarget) { try { var rate = await GetFeeRate(blockTarget, network.CryptoCode); txBuilder.SendEstimatedFees(rate.FeeRate); } catch (NBXplorerException e) when(e.Error.Code == "fee-estimation-unavailable" && request.FeePreference?.FallbackFeeRate is FeeRate fallbackFeeRate) { txBuilder.SendEstimatedFees(fallbackFeeRate); } } else if (request.FeePreference?.ExplicitFee is Money explicitFee) { txBuilder.SendFees(explicitFee); } else { try { var rate = await GetFeeRate(1, network.CryptoCode); txBuilder.SendEstimatedFees(rate.FeeRate); } catch (NBXplorerException e) when(e.Error.Code == "fee-estimation-unavailable" && request.FeePreference?.FallbackFeeRate is FeeRate fallbackFeeRate) { txBuilder.SendEstimatedFees(fallbackFeeRate); } } psbt = txBuilder.BuildPSBT(false); hasChange = psbt.Outputs.Any(o => o.ScriptPubKey == change.ScriptPubKey); } catch (NotEnoughFundsException) { throw new NBXplorerException(new NBXplorerError(400, "not-enough-funds", "Not enough funds for doing this transaction")); } // We made sure we can build the PSBT, so now we can reserve the change address if we need to if (hasChange && request.ExplicitChangeAddress == null && request.ReserveChangeAddress) { var derivation = await repo.GetUnused(strategy, DerivationFeature.Change, 0, true); // In most of the time, this is the same as previously, so no need to rebuild PSBT if (derivation.ScriptPubKey != change.ScriptPubKey) { change = (derivation.ScriptPubKey, derivation.KeyPath); txBuilder.SetChange(change.ScriptPubKey); psbt = txBuilder.BuildPSBT(false); } } var tx = psbt.GetOriginalTransaction(); if (request.Version is uint v) { tx.Version = v; } psbt = txBuilder.CreatePSBTFrom(tx, false, SigHash.All); await UpdatePSBTCore(new UpdatePSBTRequest() { DerivationScheme = strategy, PSBT = psbt, RebaseKeyPaths = request.RebaseKeyPaths }, network); var resp = new CreatePSBTResponse() { PSBT = psbt, ChangeAddress = hasChange ? change.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork) : null }; return(Json(resp, network.JsonSerializerSettings)); }
public async Task <IActionResult> CreatePSBT( [ModelBinder(BinderType = typeof(NetworkModelBinder))] NBXplorerNetwork network, [ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))] DerivationStrategyBase strategy, [FromBody] JObject body) { if (body == null) { throw new ArgumentNullException(nameof(body)); } CreatePSBTRequest request = ParseJObject <CreatePSBTRequest>(body, network); if (strategy == null) { throw new ArgumentNullException(nameof(strategy)); } var repo = RepositoryProvider.GetRepository(network); var txBuilder = request.Seed is int s?network.NBitcoinNetwork.CreateTransactionBuilder(s) : network.NBitcoinNetwork.CreateTransactionBuilder(); CreatePSBTSuggestions suggestions = null; if (!(request.DisableFingerprintRandomization is true) && fingerprintService.GetDistribution(network) is FingerprintDistribution distribution) { suggestions ??= new CreatePSBTSuggestions(); var known = new List <(Fingerprint feature, bool value)>(); if (request.RBF is bool rbf) { known.Add((Fingerprint.RBF, rbf)); } if (request.DiscourageFeeSniping is bool feeSnipping) { known.Add((Fingerprint.FeeSniping, feeSnipping)); } if (request.LockTime is LockTime l) { if (l == LockTime.Zero) { known.Add((Fingerprint.TimelockZero, true)); } } if (request.Version is uint version) { if (version == 1) { known.Add((Fingerprint.V1, true)); } if (version == 2) { known.Add((Fingerprint.V2, true)); } } known.Add((Fingerprint.SpendFromMixed, false)); known.Add((Fingerprint.SequenceMixed, false)); if (strategy is DirectDerivationStrategy direct) { if (direct.Segwit) { known.Add((Fingerprint.SpendFromP2WPKH, true)); } else { known.Add((Fingerprint.SpendFromP2PKH, true)); } } else { // TODO: What if multisig? For now we consider it p2wpkh known.Add((Fingerprint.SpendFromP2SHP2WPKH, true)); } Fingerprint fingerprint = distribution.PickFingerprint(txBuilder.ShuffleRandom); try { fingerprint = distribution.KnowingThat(known.ToArray()) .PickFingerprint(txBuilder.ShuffleRandom); } catch (InvalidOperationException) { } request.RBF ??= fingerprint.HasFlag(Fingerprint.RBF); request.DiscourageFeeSniping ??= fingerprint.HasFlag(Fingerprint.FeeSniping); if (request.LockTime is null && fingerprint.HasFlag(Fingerprint.TimelockZero)) { request.LockTime = new LockTime(0); } if (request.Version is null && fingerprint.HasFlag(Fingerprint.V1)) { request.Version = 1; } if (request.Version is null && fingerprint.HasFlag(Fingerprint.V2)) { request.Version = 2; } suggestions.ShouldEnforceLowR = fingerprint.HasFlag(Fingerprint.LowR); } var waiter = Waiters.GetWaiter(network); if (waiter.NetworkInfo?.GetRelayFee() is FeeRate feeRate) { txBuilder.StandardTransactionPolicy.MinRelayTxFee = feeRate; } txBuilder.OptInRBF = !(request.RBF is false); if (request.LockTime is LockTime lockTime) { txBuilder.SetLockTime(lockTime); } // Discourage fee sniping. // // For a large miner the value of the transactions in the best block and // the mempool can exceed the cost of deliberately attempting to mine two // blocks to orphan the current best block. By setting nLockTime such that // only the next block can include the transaction, we discourage this // practice as the height restricted and limited blocksize gives miners // considering fee sniping fewer options for pulling off this attack. // // A simple way to think about this is from the wallet's point of view we // always want the blockchain to move forward. By setting nLockTime this // way we're basically making the statement that we only want this // transaction to appear in the next block; we don't want to potentially // encourage reorgs by allowing transactions to appear at lower heights // than the next block in forks of the best chain. // // Of course, the subsidy is high enough, and transaction volume low // enough, that fee sniping isn't a problem yet, but by implementing a fix // now we ensure code won't be written that makes assumptions about // nLockTime that preclude a fix later. else if (!(request.DiscourageFeeSniping is false)) { if (waiter.State is BitcoinDWaiterState.Ready) { int blockHeight = ChainProvider.GetChain(network).Height; // Secondly occasionally randomly pick a nLockTime even further back, so // that transactions that are delayed after signing for whatever reason, // e.g. high-latency mix networks and some CoinJoin implementations, have // better privacy. if (txBuilder.ShuffleRandom.Next(0, 10) == 0) { blockHeight = Math.Max(0, blockHeight - txBuilder.ShuffleRandom.Next(0, 100)); } txBuilder.SetLockTime(new LockTime(blockHeight)); } else { txBuilder.SetLockTime(new LockTime(0)); } } var utxos = (await GetUTXOs(network.CryptoCode, strategy, null)).As <UTXOChanges>().GetUnspentUTXOs(request.MinConfirmations); var availableCoinsByOutpoint = utxos.ToDictionary(o => o.Outpoint); if (request.IncludeOnlyOutpoints != null) { var includeOnlyOutpoints = request.IncludeOnlyOutpoints.ToHashSet(); availableCoinsByOutpoint = availableCoinsByOutpoint.Where(c => includeOnlyOutpoints.Contains(c.Key)).ToDictionary(o => o.Key, o => o.Value); } if (request.ExcludeOutpoints?.Any() is true) { var excludedOutpoints = request.ExcludeOutpoints.ToHashSet(); availableCoinsByOutpoint = availableCoinsByOutpoint.Where(c => !excludedOutpoints.Contains(c.Key)).ToDictionary(o => o.Key, o => o.Value); } if (request.MinValue != null) { availableCoinsByOutpoint = availableCoinsByOutpoint.Where(c => request.MinValue >= (Money)c.Value.Value).ToDictionary(o => o.Key, o => o.Value); } ICoin[] coins = null; if (strategy.GetDerivation().Redeem != null) { // We need to add the redeem script to the coins var hdKeys = strategy.AsHDRedeemScriptPubKey().AsHDKeyCache(); var arr = availableCoinsByOutpoint.Values.ToArray(); coins = new ICoin[arr.Length]; // Can be very intense CPU wise Parallel.For(0, coins.Length, i => { coins[i] = ((Coin)arr[i].AsCoin()).ToScriptCoin(hdKeys.Derive(arr[i].KeyPath).ScriptPubKey); }); } else { coins = availableCoinsByOutpoint.Values.Select(v => v.AsCoin()).ToArray(); } txBuilder.AddCoins(coins); foreach (var dest in request.Destinations) { if (dest.SweepAll) { try { txBuilder.SendAll(dest.Destination); } catch { throw new NBXplorerException(new NBXplorerError(400, "not-enough-funds", "You can't sweep funds, because you don't have any.")); } } else { txBuilder.Send(dest.Destination, dest.Amount); if (dest.SubstractFees) { try { txBuilder.SubtractFees(); } catch { throw new NBXplorerException(new NBXplorerError(400, "not-enough-funds", "You can't substract fee on this destination, because not enough money was sent to it")); } } } } (Script ScriptPubKey, KeyPath KeyPath)change = (null, null); bool hasChange = false; if (request.ExplicitChangeAddress == null) { var keyInfo = await repo.GetUnused(strategy, DerivationFeature.Change, 0, false); change = (keyInfo.ScriptPubKey, keyInfo.KeyPath); } else { // The provided explicit change might have a known keyPath, let's change for it KeyPath keyPath = null; var keyInfos = await repo.GetKeyInformations(new[] { request.ExplicitChangeAddress.ScriptPubKey }); if (keyInfos.TryGetValue(request.ExplicitChangeAddress.ScriptPubKey, out var kis)) { keyPath = kis.FirstOrDefault(k => k.DerivationStrategy == strategy)?.KeyPath; } change = (request.ExplicitChangeAddress.ScriptPubKey, keyPath); } txBuilder.SetChange(change.ScriptPubKey); PSBT psbt = null; try { if (request.FeePreference?.ExplicitFeeRate is FeeRate explicitFeeRate) { txBuilder.SendEstimatedFees(explicitFeeRate); } else if (request.FeePreference?.BlockTarget is int blockTarget) { try { var rate = await GetFeeRate(blockTarget, network.CryptoCode); txBuilder.SendEstimatedFees(rate.FeeRate); } catch (NBXplorerException e) when(e.Error.Code == "fee-estimation-unavailable" && request.FeePreference?.FallbackFeeRate is FeeRate fallbackFeeRate) { txBuilder.SendEstimatedFees(fallbackFeeRate); } } else if (request.FeePreference?.ExplicitFee is Money explicitFee) { txBuilder.SendFees(explicitFee); } else { try { var rate = await GetFeeRate(1, network.CryptoCode); txBuilder.SendEstimatedFees(rate.FeeRate); } catch (NBXplorerException e) when(e.Error.Code == "fee-estimation-unavailable" && request.FeePreference?.FallbackFeeRate is FeeRate fallbackFeeRate) { txBuilder.SendEstimatedFees(fallbackFeeRate); } } psbt = txBuilder.BuildPSBT(false); hasChange = psbt.Outputs.Any(o => o.ScriptPubKey == change.ScriptPubKey); } catch (NotEnoughFundsException) { throw new NBXplorerException(new NBXplorerError(400, "not-enough-funds", "Not enough funds for doing this transaction")); } // We made sure we can build the PSBT, so now we can reserve the change address if we need to if (hasChange && request.ExplicitChangeAddress == null && request.ReserveChangeAddress) { var derivation = await repo.GetUnused(strategy, DerivationFeature.Change, 0, true); // In most of the time, this is the same as previously, so no need to rebuild PSBT if (derivation.ScriptPubKey != change.ScriptPubKey) { change = (derivation.ScriptPubKey, derivation.KeyPath); txBuilder.SetChange(change.ScriptPubKey); psbt = txBuilder.BuildPSBT(false); } } var tx = psbt.GetOriginalTransaction(); if (request.Version is uint v) { tx.Version = v; } psbt = txBuilder.CreatePSBTFrom(tx, false, SigHash.All); var update = new UpdatePSBTRequest() { DerivationScheme = strategy, PSBT = psbt, RebaseKeyPaths = request.RebaseKeyPaths, AlwaysIncludeNonWitnessUTXO = request.AlwaysIncludeNonWitnessUTXO, IncludeGlobalXPub = request.IncludeGlobalXPub }; await UpdatePSBTCore(update, network); var resp = new CreatePSBTResponse() { PSBT = update.PSBT, ChangeAddress = hasChange ? change.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork) : null, Suggestions = suggestions }; return(Json(resp, network.JsonSerializerSettings)); }
public async Task <IActionResult> WalletSend( [ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId, WalletSendModel vm, string command = "", CancellationToken cancellation = default) { if (walletId?.StoreId == null) { return(NotFound()); } var store = await Repository.FindStore(walletId.StoreId, GetUserId()); if (store == null) { return(NotFound()); } var network = this.NetworkProvider.GetNetwork <BTCPayNetwork>(walletId?.CryptoCode); if (network == null || network.ReadonlyWallet) { return(NotFound()); } vm.SupportRBF = network.SupportRBF; decimal transactionAmountSum = 0; if (command == "add-output") { ModelState.Clear(); vm.Outputs.Add(new WalletSendModel.TransactionOutput()); return(View(vm)); } if (command.StartsWith("remove-output", StringComparison.InvariantCultureIgnoreCase)) { ModelState.Clear(); var index = int.Parse(command.Substring(command.IndexOf(":", StringComparison.InvariantCultureIgnoreCase) + 1), CultureInfo.InvariantCulture); vm.Outputs.RemoveAt(index); return(View(vm)); } if (!vm.Outputs.Any()) { ModelState.AddModelError(string.Empty, "Please add at least one transaction output"); return(View(vm)); } var subtractFeesOutputsCount = new List <int>(); var substractFees = vm.Outputs.Any(o => o.SubtractFeesFromOutput); for (var i = 0; i < vm.Outputs.Count; i++) { var transactionOutput = vm.Outputs[i]; if (transactionOutput.SubtractFeesFromOutput) { subtractFeesOutputsCount.Add(i); } transactionOutput.DestinationAddress = transactionOutput.DestinationAddress?.Trim() ?? string.Empty; try { BitcoinAddress.Create(transactionOutput.DestinationAddress, network.NBitcoinNetwork); } catch { var inputName = string.Format(CultureInfo.InvariantCulture, "Outputs[{0}].", i.ToString(CultureInfo.InvariantCulture)) + nameof(transactionOutput.DestinationAddress); ModelState.AddModelError(inputName, "Invalid address"); } if (transactionOutput.Amount.HasValue) { transactionAmountSum += transactionOutput.Amount.Value; if (vm.CurrentBalance == transactionOutput.Amount.Value && !transactionOutput.SubtractFeesFromOutput) { vm.AddModelError(model => model.Outputs[i].SubtractFeesFromOutput, "You are sending your entire balance to the same destination, you should subtract the fees", this); } } } if (subtractFeesOutputsCount.Count > 1) { foreach (var subtractFeesOutput in subtractFeesOutputsCount) { vm.AddModelError(model => model.Outputs[subtractFeesOutput].SubtractFeesFromOutput, "You can only subtract fees from one output", this); } } else if (vm.CurrentBalance == transactionAmountSum && !substractFees) { ModelState.AddModelError(string.Empty, "You are sending your entire balance, you should subtract the fees from an output"); } if (vm.CurrentBalance < transactionAmountSum) { for (var i = 0; i < vm.Outputs.Count; i++) { vm.AddModelError(model => model.Outputs[i].Amount, "You are sending more than what you own", this); } } if (!ModelState.IsValid) { return(View(vm)); } DerivationSchemeSettings derivationScheme = GetDerivationSchemeSettings(walletId); CreatePSBTResponse psbt = null; try { psbt = await CreatePSBT(network, derivationScheme, vm, cancellation); } catch (NBXplorerException ex) { ModelState.AddModelError(string.Empty, ex.Error.Message); return(View(vm)); } catch (NotSupportedException) { ModelState.AddModelError(string.Empty, "You need to update your version of NBXplorer"); return(View(vm)); } derivationScheme.RebaseKeyPaths(psbt.PSBT); switch (command) { case "vault": return(ViewVault(walletId, psbt.PSBT)); case "ledger": return(ViewWalletSendLedger(psbt.PSBT, psbt.ChangeAddress)); case "seed": return(SignWithSeed(walletId, psbt.PSBT.ToBase64())); case "analyze-psbt": var name = $"Send-{string.Join('_', vm.Outputs.Select(output => $"{output.Amount}->{output.DestinationAddress}{(output.SubtractFeesFromOutput ? "-Fees" : string.Empty)}"))}.psbt"; return(RedirectToWalletPSBT(walletId, psbt.PSBT, name)); default: return(View(vm)); } }
public async Task <IActionResult> CreatePSBT( [ModelBinder(BinderType = typeof(NetworkModelBinder))] NBXplorerNetwork network, [ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))] DerivationStrategyBase strategy, [FromBody] JObject body) { if (body == null) { throw new ArgumentNullException(nameof(body)); } CreatePSBTRequest request = ParseJObject <CreatePSBTRequest>(body, network); if (strategy == null) { throw new ArgumentNullException(nameof(strategy)); } var repo = RepositoryProvider.GetRepository(network); var txBuilder = request.Seed is int s?network.NBitcoinNetwork.CreateTransactionBuilder(s) : network.NBitcoinNetwork.CreateTransactionBuilder(); if (Waiters.GetWaiter(network).NetworkInfo?.GetRelayFee() is FeeRate feeRate) { txBuilder.StandardTransactionPolicy.MinRelayTxFee = feeRate; } txBuilder.OptInRBF = request.RBF; if (request.LockTime is LockTime lockTime) { txBuilder.SetLockTime(lockTime); txBuilder.OptInRBF = true; } var utxos = (await GetUTXOs(network.CryptoCode, strategy, null)).GetUnspentCoins(request.MinConfirmations); var availableCoinsByOutpoint = utxos.ToDictionary(o => o.Outpoint); if (request.IncludeOnlyOutpoints != null) { var includeOnlyOutpoints = request.IncludeOnlyOutpoints.ToHashSet(); availableCoinsByOutpoint = availableCoinsByOutpoint.Where(c => includeOnlyOutpoints.Contains(c.Key)).ToDictionary(o => o.Key, o => o.Value); } if (request.ExcludeOutpoints?.Any() is true) { var excludedOutpoints = request.ExcludeOutpoints.ToHashSet(); availableCoinsByOutpoint = availableCoinsByOutpoint.Where(c => !excludedOutpoints.Contains(c.Key)).ToDictionary(o => o.Key, o => o.Value); } txBuilder.AddCoins(availableCoinsByOutpoint.Values); foreach (var dest in request.Destinations) { if (dest.SweepAll) { txBuilder.SendAll(dest.Destination); } else { txBuilder.Send(dest.Destination, dest.Amount); if (dest.SubstractFees) { txBuilder.SubtractFees(); } } } (Script ScriptPubKey, KeyPath KeyPath)change = (null, null); bool hasChange = false; // We first build the transaction with a change which keep the length of the expected change scriptPubKey // This allow us to detect if there is a change later in the constructed transaction. // This defend against bug which can happen if one of the destination is the same as the expected change // This assume that a script with only 0 can't be created from a strategy, nor by passing any data to explicitChangeAddress if (request.ExplicitChangeAddress == null) { // The dummyScriptPubKey is necessary to know the size of the change var dummyScriptPubKey = utxos.FirstOrDefault()?.ScriptPubKey ?? strategy.GetDerivation(0).ScriptPubKey; change = (Script.FromBytesUnsafe(new byte[dummyScriptPubKey.Length]), null); } else { change = (Script.FromBytesUnsafe(new byte[request.ExplicitChangeAddress.ScriptPubKey.Length]), null); } txBuilder.SetChange(change.ScriptPubKey); PSBT psbt = null; try { if (request.FeePreference?.ExplicitFeeRate is FeeRate explicitFeeRate) { txBuilder.SendEstimatedFees(explicitFeeRate); } else if (request.FeePreference?.BlockTarget is int blockTarget) { try { var rate = await GetFeeRate(blockTarget, network.CryptoCode); txBuilder.SendEstimatedFees(rate.FeeRate); } catch (NBXplorerException e) when(e.Error.Code == "fee-estimation-unavailable" && request.FeePreference?.FallbackFeeRate is FeeRate fallbackFeeRate) { txBuilder.SendEstimatedFees(fallbackFeeRate); } } else if (request.FeePreference?.ExplicitFee is Money explicitFee) { txBuilder.SendFees(explicitFee); } else { try { var rate = await GetFeeRate(1, network.CryptoCode); txBuilder.SendEstimatedFees(rate.FeeRate); } catch (NBXplorerException e) when(e.Error.Code == "fee-estimation-unavailable" && request.FeePreference?.FallbackFeeRate is FeeRate fallbackFeeRate) { txBuilder.SendEstimatedFees(fallbackFeeRate); } } psbt = txBuilder.BuildPSBT(false); hasChange = psbt.Outputs.Any(o => o.ScriptPubKey == change.ScriptPubKey); } catch (NotEnoughFundsException) { throw new NBXplorerException(new NBXplorerError(400, "not-enough-funds", "Not enough funds for doing this transaction")); } if (hasChange) // We need to reserve an address, so we need to build again the psbt { if (request.ExplicitChangeAddress == null) { var derivation = await repo.GetUnused(strategy, DerivationFeature.Change, 0, request.ReserveChangeAddress); change = (derivation.ScriptPubKey, derivation.KeyPath); } else { change = (request.ExplicitChangeAddress.ScriptPubKey, null); } txBuilder.SetChange(change.ScriptPubKey); psbt = txBuilder.BuildPSBT(false); } var tx = psbt.GetOriginalTransaction(); if (request.Version is uint v) { tx.Version = v; } psbt = txBuilder.CreatePSBTFrom(tx, false, SigHash.All); // Maybe it is a change that we know about, let's search in the DB if (hasChange && change.KeyPath == null) { var keyInfos = await repo.GetKeyInformations(new[] { request.ExplicitChangeAddress.ScriptPubKey }); if (keyInfos.TryGetValue(request.ExplicitChangeAddress.ScriptPubKey, out var kis)) { var ki = kis.FirstOrDefault(k => k.DerivationStrategy == strategy); if (ki != null) { change = (change.ScriptPubKey, kis.First().KeyPath); } } } await UpdatePSBTCore(new UpdatePSBTRequest() { DerivationScheme = strategy, PSBT = psbt, RebaseKeyPaths = request.RebaseKeyPaths }, network); var resp = new CreatePSBTResponse() { PSBT = psbt, ChangeAddress = hasChange ? change.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork) : null }; return(Json(resp, network.JsonSerializerSettings)); }
public async Task <IActionResult> LedgerConnection( [ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId, string command, // getinfo // getxpub int account = 0, // sendtoaddress string psbt = null, string hintChange = null ) { if (!HttpContext.WebSockets.IsWebSocketRequest) { return(NotFound()); } var network = NetworkProvider.GetNetwork <BTCPayNetwork>(walletId.CryptoCode); if (network == null) { throw new FormatException("Invalid value for crypto code"); } var storeData = (await Repository.FindStore(walletId.StoreId, GetUserId())); var derivationSettings = GetDerivationSchemeSettings(walletId, storeData); var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync(); using (var normalOperationTimeout = new CancellationTokenSource()) using (var signTimeout = new CancellationTokenSource()) { normalOperationTimeout.CancelAfter(TimeSpan.FromMinutes(30)); var hw = new LedgerHardwareWalletService(webSocket); var model = new WalletSendLedgerModel(); object result = null; try { if (command == "test") { result = await hw.Test(normalOperationTimeout.Token); } if (command == "sendtoaddress") { if (!_dashboard.IsFullySynched(network.CryptoCode, out var summary)) { throw new Exception($"{network.CryptoCode}: not started or fully synched"); } var accountKey = derivationSettings.GetSigningAccountKeySettings(); // Some deployment does not have the AccountKeyPath set, let's fix this... if (accountKey.AccountKeyPath == null) { // If the saved wallet key path is not present or incorrect, let's scan the wallet to see if it can sign strategy var foundKeyPath = await hw.FindKeyPathFromDerivation(network, derivationSettings.AccountDerivation, normalOperationTimeout.Token); accountKey.AccountKeyPath = foundKeyPath ?? throw new HardwareWalletException($"This store is not configured to use this ledger"); storeData.SetSupportedPaymentMethod(derivationSettings); await Repository.UpdateStore(storeData); } // If it has already the AccountKeyPath, we did not looked up for it, so we need to check if we are on the right ledger else { // Checking if ledger is right with the RootFingerprint is faster as it does not need to make a query to the parent xpub, // but some deployment does not have it, so let's use AccountKeyPath instead if (accountKey.RootFingerprint == null) { var actualPubKey = await hw.GetExtPubKey(network, accountKey.AccountKeyPath, normalOperationTimeout.Token); if (!derivationSettings.AccountDerivation.GetExtPubKeys().Any(p => p.GetPublicKey() == actualPubKey.GetPublicKey())) { throw new HardwareWalletException($"This store is not configured to use this ledger"); } } // We have the root fingerprint, we can check the root from it else { var actualPubKey = await hw.GetPubKey(network, new KeyPath(), normalOperationTimeout.Token); if (actualPubKey.GetHDFingerPrint() != accountKey.RootFingerprint.Value) { throw new HardwareWalletException($"This store is not configured to use this ledger"); } } } // Some deployment does not have the RootFingerprint set, let's fix this... if (accountKey.RootFingerprint == null) { accountKey.RootFingerprint = (await hw.GetPubKey(network, new KeyPath(), normalOperationTimeout.Token)).GetHDFingerPrint(); storeData.SetSupportedPaymentMethod(derivationSettings); await Repository.UpdateStore(storeData); } var psbtResponse = new CreatePSBTResponse() { PSBT = PSBT.Parse(psbt, network.NBitcoinNetwork), ChangeAddress = string.IsNullOrEmpty(hintChange) ? null : BitcoinAddress.Create(hintChange, network.NBitcoinNetwork) }; derivationSettings.RebaseKeyPaths(psbtResponse.PSBT); signTimeout.CancelAfter(TimeSpan.FromMinutes(5)); psbtResponse.PSBT = await hw.SignTransactionAsync(psbtResponse.PSBT, accountKey.GetRootedKeyPath(), accountKey.AccountKey, psbtResponse.ChangeAddress?.ScriptPubKey, signTimeout.Token); result = new SendToAddressResult() { PSBT = psbtResponse.PSBT.ToBase64() }; } } catch (OperationCanceledException) { result = new LedgerTestResult() { Success = false, Error = "Timeout" }; } catch (Exception ex) { result = new LedgerTestResult() { Success = false, Error = ex.Message }; } finally { hw.Dispose(); } try { if (result != null) { UTF8Encoding UTF8NOBOM = new UTF8Encoding(false); var bytes = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, _mvcJsonOptions.Value.SerializerSettings)); await webSocket.SendAsync(new ArraySegment <byte>(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token); } } catch { } finally { await webSocket.CloseSocket(); } } return(new EmptyResult()); }
public async Task <IActionResult> WalletSend( [ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId, WalletSendModel vm, string command = null, CancellationToken cancellation = default) { if (walletId?.StoreId == null) { return(NotFound()); } var store = await Repository.FindStore(walletId.StoreId, GetUserId()); if (store == null) { return(NotFound()); } var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode); if (network == null) { return(NotFound()); } vm.SupportRBF = network.SupportRBF; var destination = ParseDestination(vm.Destination, network.NBitcoinNetwork); if (destination == null) { ModelState.AddModelError(nameof(vm.Destination), "Invalid address"); } if (vm.Amount.HasValue) { if (vm.CurrentBalance == vm.Amount.Value && !vm.SubstractFees) { ModelState.AddModelError(nameof(vm.Amount), "You are sending all your balance to the same destination, you should substract the fees"); } if (vm.CurrentBalance < vm.Amount.Value) { ModelState.AddModelError(nameof(vm.Amount), "You are sending more than what you own"); } } if (!ModelState.IsValid) { return(View(vm)); } DerivationSchemeSettings derivationScheme = await GetDerivationSchemeSettings(walletId); CreatePSBTResponse psbt = null; try { psbt = await CreatePSBT(network, derivationScheme, vm, cancellation); } catch (NBXplorerException ex) { ModelState.AddModelError(nameof(vm.Amount), ex.Error.Message); return(View(vm)); } catch (NotSupportedException) { ModelState.AddModelError(nameof(vm.Destination), "You need to update your version of NBXplorer"); return(View(vm)); } derivationScheme.RebaseKeyPaths(psbt.PSBT); if (command == "ledger") { return(ViewWalletSendLedger(psbt.PSBT, psbt.ChangeAddress)); } else if (command == "analyze-psbt") { return(ViewPSBT(psbt.PSBT, $"Send-{vm.Amount.Value}-{network.CryptoCode}-to-{destination[0].ToString()}.psbt")); } return(View(vm)); }
public async Task <IActionResult> WalletSend( [ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId, WalletSendModel vm, string command = "", CancellationToken cancellation = default) { if (walletId?.StoreId == null) { return(NotFound()); } var store = await Repository.FindStore(walletId.StoreId, GetUserId()); if (store == null) { return(NotFound()); } var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode); if (network == null) { return(NotFound()); } vm.SupportRBF = network.SupportRBF; decimal transactionAmountSum = 0; if (command == "add-output") { ModelState.Clear(); vm.Outputs.Add(new WalletSendModel.TransactionOutput()); return(View(vm)); } if (command.StartsWith("remove-output", StringComparison.InvariantCultureIgnoreCase)) { ModelState.Clear(); var index = int.Parse(command.Substring(command.IndexOf(":", StringComparison.InvariantCultureIgnoreCase) + 1), CultureInfo.InvariantCulture); vm.Outputs.RemoveAt(index); return(View(vm)); } if (!vm.Outputs.Any()) { ModelState.AddModelError(string.Empty, "Por favor agregue al menos una salida de transacción"); return(View(vm)); } var subtractFeesOutputsCount = new List <int>(); var substractFees = vm.Outputs.Any(o => o.SubtractFeesFromOutput); for (var i = 0; i < vm.Outputs.Count; i++) { var transactionOutput = vm.Outputs[i]; if (transactionOutput.SubtractFeesFromOutput) { subtractFeesOutputsCount.Add(i); } var destination = ParseDestination(transactionOutput.DestinationAddress, network.NBitcoinNetwork); if (destination == null) { ModelState.AddModelError(nameof(transactionOutput.DestinationAddress), "Dirección inválida"); } if (transactionOutput.Amount.HasValue) { transactionAmountSum += transactionOutput.Amount.Value; if (vm.CurrentBalance == transactionOutput.Amount.Value && !transactionOutput.SubtractFeesFromOutput) { vm.AddModelError(model => model.Outputs[i].SubtractFeesFromOutput, "Está enviando todo su saldo al mismo destino, debe restar las tarifas", ModelState); } } } if (subtractFeesOutputsCount.Count > 1) { foreach (var subtractFeesOutput in subtractFeesOutputsCount) { vm.AddModelError(model => model.Outputs[subtractFeesOutput].SubtractFeesFromOutput, "Solo puedes restar tarifas de una salida", ModelState); } } else if (vm.CurrentBalance == transactionAmountSum && !substractFees) { ModelState.AddModelError(string.Empty, "Está enviando todo su saldo, debe restar las tarifas de una salida"); } if (vm.CurrentBalance < transactionAmountSum) { for (var i = 0; i < vm.Outputs.Count; i++) { vm.AddModelError(model => model.Outputs[i].Amount, "Estás enviando más de lo que tienes", ModelState); } } if (!ModelState.IsValid) { return(View(vm)); } DerivationSchemeSettings derivationScheme = await GetDerivationSchemeSettings(walletId); CreatePSBTResponse psbt = null; try { psbt = await CreatePSBT(network, derivationScheme, vm, cancellation); } catch (NBXplorerException ex) { ModelState.AddModelError(string.Empty, ex.Error.Message); return(View(vm)); } catch (NotSupportedException) { ModelState.AddModelError(string.Empty, "Necesitas actualizar tu versión de NBXplorer"); return(View(vm)); } derivationScheme.RebaseKeyPaths(psbt.PSBT); switch (command) { case "ledger": return(ViewWalletSendLedger(psbt.PSBT, psbt.ChangeAddress)); case "seed": return(SignWithSeed(walletId, psbt.PSBT.ToBase64())); case "analyze-psbt": var name = $"Send-{string.Join('_', vm.Outputs.Select(output => $"{output.Amount}->{output.DestinationAddress}{(output.SubtractFeesFromOutput ? "-Fees" : string.Empty)}"))}.psbt"; return(RedirectToAction(nameof(WalletPSBT), new { walletId = walletId, psbt = psbt.PSBT.ToBase64(), FileName = name })); default: return(View(vm)); } }
public async Task <IActionResult> CreatePSBT( [ModelBinder(BinderType = typeof(NetworkModelBinder))] NBXplorerNetwork network, [ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))] DerivationStrategyBase strategy, [FromBody] JObject body) { if (body == null) { throw new ArgumentNullException(nameof(body)); } CreatePSBTRequest request = ParseJObject <CreatePSBTRequest>(body, network); if (strategy == null) { throw new ArgumentNullException(nameof(strategy)); } var repo = RepositoryProvider.GetRepository(network); var utxos = await GetUTXOs(network.CryptoCode, strategy, null); var txBuilder = request.Seed is int s?network.NBitcoinNetwork.CreateTransactionBuilder(s) : network.NBitcoinNetwork.CreateTransactionBuilder(); if (Waiters.GetWaiter(network).NetworkInfo?.GetRelayFee() is FeeRate feeRate) { txBuilder.StandardTransactionPolicy.MinRelayTxFee = feeRate; } txBuilder.OptInRBF = request.RBF; if (request.LockTime is LockTime lockTime) { txBuilder.SetLockTime(lockTime); txBuilder.OptInRBF = true; } var availableCoinsByOutpoint = utxos.GetUnspentCoins(request.MinConfirmations).ToDictionary(o => o.Outpoint); if (request.IncludeOnlyOutpoints != null) { var includeOnlyOutpoints = request.IncludeOnlyOutpoints.ToHashSet(); availableCoinsByOutpoint = availableCoinsByOutpoint.Where(c => includeOnlyOutpoints.Contains(c.Key)).ToDictionary(o => o.Key, o => o.Value); } if (request.ExcludeOutpoints?.Any() is true) { var excludedOutpoints = request.ExcludeOutpoints.ToHashSet(); availableCoinsByOutpoint = availableCoinsByOutpoint.Where(c => !excludedOutpoints.Contains(c.Key)).ToDictionary(o => o.Key, o => o.Value); } txBuilder.AddCoins(availableCoinsByOutpoint.Values); foreach (var dest in request.Destinations) { if (dest.SweepAll) { txBuilder.SendAll(dest.Destination); } else { txBuilder.Send(dest.Destination, dest.Amount); if (dest.SubstractFees) { txBuilder.SubtractFees(); } } } (Script ScriptPubKey, KeyPath KeyPath)change = (null, null); bool hasChange = false; // We first build the transaction with a change which keep the length of the expected change scriptPubKey // This allow us to detect if there is a change later in the constructed transaction. // This defend against bug which can happen if one of the destination is the same as the expected change // This assume that a script with only 0 can't be created from a strategy, nor by passing any data to explicitChangeAddress if (request.ExplicitChangeAddress == null) { // The dummyScriptPubKey is necessary to know the size of the change var dummyScriptPubKey = utxos.Unconfirmed.UTXOs.FirstOrDefault()?.ScriptPubKey ?? utxos.Confirmed.UTXOs.FirstOrDefault()?.ScriptPubKey ?? strategy.Derive(0).ScriptPubKey; change = (Script.FromBytesUnsafe(new byte[dummyScriptPubKey.Length]), null); } else { change = (Script.FromBytesUnsafe(new byte[request.ExplicitChangeAddress.ScriptPubKey.Length]), null); } txBuilder.SetChange(change.ScriptPubKey); PSBT psbt = null; try { if (request.FeePreference?.ExplicitFeeRate is FeeRate explicitFeeRate) { txBuilder.SendEstimatedFees(explicitFeeRate); } else if (request.FeePreference?.BlockTarget is int blockTarget) { try { var rate = await GetFeeRate(blockTarget, network.CryptoCode); txBuilder.SendEstimatedFees(rate.FeeRate); } catch (NBXplorerException e) when(e.Error.Code == "fee-estimation-unavailable" && request.FeePreference?.FallbackFeeRate is FeeRate fallbackFeeRate) { txBuilder.SendEstimatedFees(fallbackFeeRate); } } else if (request.FeePreference?.ExplicitFee is Money explicitFee) { txBuilder.SendFees(explicitFee); } else { try { var rate = await GetFeeRate(1, network.CryptoCode); txBuilder.SendEstimatedFees(rate.FeeRate); } catch (NBXplorerException e) when(e.Error.Code == "fee-estimation-unavailable" && request.FeePreference?.FallbackFeeRate is FeeRate fallbackFeeRate) { txBuilder.SendEstimatedFees(fallbackFeeRate); } } psbt = txBuilder.BuildPSBT(false); hasChange = psbt.Outputs.Any(o => o.ScriptPubKey == change.ScriptPubKey); } catch (NotEnoughFundsException) { throw new NBXplorerException(new NBXplorerError(400, "not-enough-funds", "Not enough funds for doing this transaction")); } if (hasChange) // We need to reserve an address, so we need to build again the psbt { if (request.ExplicitChangeAddress == null) { var derivation = await repo.GetUnused(strategy, DerivationFeature.Change, 0, request.ReserveChangeAddress); change = (derivation.ScriptPubKey, derivation.KeyPath); } else { change = (request.ExplicitChangeAddress.ScriptPubKey, null); } txBuilder.SetChange(change.ScriptPubKey); psbt = txBuilder.BuildPSBT(false); } var tx = psbt.GetOriginalTransaction(); if (request.Version is uint v) { tx.Version = v; } psbt = txBuilder.CreatePSBTFrom(tx, false, SigHash.All); var outputsKeyInformations = repo.GetKeyInformations(psbt.Outputs.Where(o => !o.HDKeyPaths.Any()).Select(o => o.ScriptPubKey).ToArray()); var utxosByOutpoint = utxos.GetUnspentUTXOs().ToDictionary(u => u.Outpoint); // Maybe it is a change that we know about, let's search in the DB if (hasChange && change.KeyPath == null) { var keyInfos = await repo.GetKeyInformations(new[] { request.ExplicitChangeAddress.ScriptPubKey }); if (keyInfos.TryGetValue(request.ExplicitChangeAddress.ScriptPubKey, out var kis)) { var ki = kis.FirstOrDefault(k => k.DerivationStrategy == strategy); if (ki != null) { change = (change.ScriptPubKey, kis.First().KeyPath); } } } var pubkeys = strategy.GetExtPubKeys().Select(p => p.AsHDKeyCache()).ToArray(); var keyPaths = psbt.Inputs.Select(i => utxosByOutpoint[i.PrevOut].KeyPath).ToList(); if (hasChange && change.KeyPath != null) { keyPaths.Add(change.KeyPath); } var fps = new Dictionary <PubKey, HDFingerprint>(); foreach (var pubkey in pubkeys) { // We derive everything the fastest way possible on multiple cores pubkey.Derive(keyPaths.ToArray()); fps.TryAdd(pubkey.GetPublicKey(), pubkey.GetPublicKey().GetHDFingerPrint()); } foreach (var input in psbt.Inputs) { var utxo = utxosByOutpoint[input.PrevOut]; foreach (var pubkey in pubkeys) { var childPubKey = pubkey.Derive(utxo.KeyPath); NBitcoin.Extensions.TryAdd(input.HDKeyPaths, childPubKey.GetPublicKey(), Tuple.Create(fps[pubkey.GetPublicKey()], utxo.KeyPath)); } } await Task.WhenAll(psbt.Inputs .Select(async(input) => { if (input.WitnessUtxo == null) // We need previous tx { var prev = await repo.GetSavedTransactions(input.PrevOut.Hash); if (prev?.Any() is true) { input.NonWitnessUtxo = prev[0].Transaction; } } }).ToArray()); var outputsKeyInformationsResult = await outputsKeyInformations; foreach (var output in psbt.Outputs) { foreach (var keyInfo in outputsKeyInformationsResult[output.ScriptPubKey].Where(o => o.DerivationStrategy == strategy)) { foreach (var pubkey in pubkeys) { var childPubKey = pubkey.Derive(keyInfo.KeyPath); NBitcoin.Extensions.TryAdd(output.HDKeyPaths, childPubKey.GetPublicKey(), Tuple.Create(fps[pubkey.GetPublicKey()], keyInfo.KeyPath)); } } } if (request.RebaseKeyPaths != null) { foreach (var rebase in request.RebaseKeyPaths) { if (rebase.AccountKeyPath == null) { throw new NBXplorerException(new NBXplorerError(400, "missing-parameter", "rebaseKeyPaths[].accountKeyPath is missing")); } if (rebase.AccountKey == null) { throw new NBXplorerException(new NBXplorerError(400, "missing-parameter", "rebaseKeyPaths[].accountKey is missing")); } psbt.RebaseKeyPaths(rebase.AccountKey, rebase.AccountKeyPath, rebase.MasterFingerprint); } } var resp = new CreatePSBTResponse() { PSBT = psbt, ChangeAddress = hasChange ? change.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork) : null }; return(Json(resp, network.JsonSerializerSettings)); }