public async Task <IActionResult> WalletSendLedger( [ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId, WalletSendLedgerModel vm) { if (walletId?.StoreId == null) { return(NotFound()); } var store = await Repository.FindStore(walletId.StoreId, GetUserId()); DerivationSchemeSettings paymentMethod = GetPaymentMethod(walletId, store); if (paymentMethod == null) { return(NotFound()); } var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode); if (network == null) { return(NotFound()); } return(View(vm)); }
public async Task <IActionResult> LedgerConnection( [ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId, string command, // getinfo // getxpub int account = 0, // sendtoaddress bool noChange = false, string destination = null, string amount = null, string feeRate = null, bool substractFees = false, bool disableRBF = false ) { if (!HttpContext.WebSockets.IsWebSocketRequest) { return(NotFound()); } var cryptoCode = walletId.CryptoCode; var storeData = (await Repository.FindStore(walletId.StoreId, GetUserId())); var derivationSettings = GetPaymentMethod(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 HardwareWalletService(webSocket); var model = new WalletSendLedgerModel(); object result = null; try { BTCPayNetwork network = null; if (cryptoCode != null) { network = NetworkProvider.GetNetwork(cryptoCode); if (network == null) { throw new FormatException("Invalid value for crypto code"); } } if (destination != null) { try { BitcoinAddress.Create(destination.Trim(), network.NBitcoinNetwork); model.Destination = destination.Trim(); } catch { } } if (feeRate != null) { try { model.FeeSatoshiPerByte = int.Parse(feeRate, CultureInfo.InvariantCulture); } catch { } if (model.FeeSatoshiPerByte <= 0) { throw new FormatException("Invalid value for fee rate"); } } if (amount != null) { try { model.Amount = Money.Parse(amount).ToDecimal(MoneyUnit.BTC); } catch { } if (model.Amount <= 0m) { throw new FormatException("Invalid value for amount"); } } model.SubstractFees = substractFees; model.NoChange = noChange; model.DisableRBF = disableRBF; 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 strategy = GetDirectDerivationStrategy(derivationSettings.AccountDerivation); // Some deployment have the wallet root key path saved in the store blob // If it does, we only have to make 1 call to the hw to check if it can sign the given strategy, if (derivationSettings.AccountKeyPath == null || !await hw.CanSign(network, strategy, derivationSettings.AccountKeyPath, normalOperationTimeout.Token)) { // 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.FindKeyPath(network, strategy, normalOperationTimeout.Token); if (foundKeyPath == null) { throw new HardwareWalletException($"This store is not configured to use this ledger"); } derivationSettings.AccountKeyPath = foundKeyPath; storeData.SetSupportedPaymentMethod(derivationSettings); await Repository.UpdateStore(storeData); } var psbt = await CreatePSBT(network, derivationSettings, model, normalOperationTimeout.Token); signTimeout.CancelAfter(TimeSpan.FromMinutes(5)); psbt.PSBT = await hw.SignTransactionAsync(psbt.PSBT, psbt.ChangeAddress?.ScriptPubKey, signTimeout.Token); if (!psbt.PSBT.TryFinalize(out var errors)) { throw new Exception($"Error while finalizing the transaction ({new PSBTException(errors).ToString()})"); } var transaction = psbt.PSBT.ExtractTransaction(); try { var broadcastResult = await ExplorerClientProvider.GetExplorerClient(network).BroadcastAsync(transaction); if (!broadcastResult.Success) { throw new Exception($"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}"); } } catch (Exception ex) { throw new Exception("Error while broadcasting: " + ex.Message); } var wallet = _walletProvider.GetWallet(network); wallet.InvalidateCache(derivationSettings.AccountDerivation); result = new SendToAddressResult() { TransactionId = transaction.GetHash().ToString() }; } } 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()); }
private async Task <CreatePSBTResponse> CreatePSBT(BTCPayNetwork network, DerivationSchemeSettings derivationSettings, WalletSendLedgerModel sendModel, CancellationToken cancellationToken) { var nbx = ExplorerClientProvider.GetExplorerClient(network); CreatePSBTRequest psbtRequest = new CreatePSBTRequest(); CreatePSBTDestination psbtDestination = new CreatePSBTDestination(); psbtRequest.Destinations.Add(psbtDestination); if (network.SupportRBF) { psbtRequest.RBF = !sendModel.DisableRBF; } psbtDestination.Destination = BitcoinAddress.Create(sendModel.Destination, network.NBitcoinNetwork); psbtDestination.Amount = Money.Coins(sendModel.Amount); psbtRequest.FeePreference = new FeePreference(); psbtRequest.FeePreference.ExplicitFeeRate = new FeeRate(Money.Satoshis(sendModel.FeeSatoshiPerByte), 1); if (sendModel.NoChange) { psbtRequest.ExplicitChangeAddress = psbtDestination.Destination; } psbtDestination.SubstractFees = sendModel.SubstractFees; var psbt = (await nbx.CreatePSBTAsync(derivationSettings.AccountDerivation, psbtRequest, cancellationToken)); if (psbt == null) { throw new NotSupportedException("You need to update your version of NBXplorer"); } if (network.MinFee != null) { psbt.PSBT.TryGetFee(out var fee); if (fee < network.MinFee) { psbtRequest.FeePreference = new FeePreference() { ExplicitFee = network.MinFee }; psbt = (await nbx.CreatePSBTAsync(derivationSettings.AccountDerivation, psbtRequest, cancellationToken)); } } if (derivationSettings.AccountKeyPath != null && derivationSettings.AccountKeyPath.Indexes.Length != 0) { // NBX only know the path relative to the account xpub. // Here we rebase the hd_keys in the PSBT to have a keypath relative to the root HD so the wallet can sign // Note that the fingerprint of the hd keys are now 0, which is wrong // However, hardware wallets does not give a damn, and sometimes does not even allow us to get this fingerprint anyway. foreach (var o in psbt.PSBT.Inputs.OfType <PSBTCoin>().Concat(psbt.PSBT.Outputs)) { var rootFP = derivationSettings.RootFingerprint is HDFingerprint fp ? fp : default; foreach (var keypath in o.HDKeyPaths.ToList()) { var newKeyPath = derivationSettings.AccountKeyPath.Derive(keypath.Value.Item2); o.HDKeyPaths.Remove(keypath.Key); o.HDKeyPaths.Add(keypath.Key, Tuple.Create(rootFP, newKeyPath)); } } } return(psbt); }
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)); } var sendModel = new WalletSendLedgerModel() { Destination = vm.Destination, Amount = vm.Amount.Value, SubstractFees = vm.SubstractFees, FeeSatoshiPerByte = vm.FeeSatoshiPerByte, NoChange = vm.NoChange, DisableRBF = vm.DisableRBF }; if (command == "ledger") { return(RedirectToAction(nameof(WalletSendLedger), sendModel)); } else { var storeData = (await Repository.FindStore(walletId.StoreId, GetUserId())); var derivationScheme = GetPaymentMethod(walletId, storeData); try { var psbt = await CreatePSBT(network, derivationScheme, sendModel, cancellation); return(File(psbt.PSBT.ToBytes(), "application/octet-stream", $"Send-{vm.Amount.Value}-{network.CryptoCode}-to-{destination[0].ToString()}.psbt")); } 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)); } } }
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()); }
private async Task <CreatePSBTResponse> CreatePSBT(BTCPayNetwork network, DerivationStrategyBase derivationScheme, WalletSendLedgerModel sendModel, CancellationToken cancellationToken) { var nbx = ExplorerClientProvider.GetExplorerClient(network); CreatePSBTRequest psbtRequest = new CreatePSBTRequest(); CreatePSBTDestination psbtDestination = new CreatePSBTDestination(); psbtRequest.Destinations.Add(psbtDestination); if (network.SupportRBF) { psbtRequest.RBF = !sendModel.DisableRBF; } psbtDestination.Destination = BitcoinAddress.Create(sendModel.Destination, network.NBitcoinNetwork); psbtDestination.Amount = Money.Coins(sendModel.Amount); psbtRequest.FeePreference = new FeePreference(); psbtRequest.FeePreference.ExplicitFeeRate = new FeeRate(Money.Satoshis(sendModel.FeeSatoshiPerByte), 1); if (sendModel.NoChange) { psbtRequest.ExplicitChangeAddress = psbtDestination.Destination; } psbtDestination.SubstractFees = sendModel.SubstractFees; var psbt = (await nbx.CreatePSBTAsync(derivationScheme, psbtRequest, cancellationToken)); if (psbt == null) { throw new NotSupportedException("You need to update your version of NBXplorer"); } if (network.MinFee != null) { psbt.PSBT.TryGetFee(out var fee); if (fee < network.MinFee) { psbtRequest.FeePreference = new FeePreference() { ExplicitFee = network.MinFee }; psbt = (await nbx.CreatePSBTAsync(derivationScheme, psbtRequest, cancellationToken)); } } return(psbt); }