diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 057b54588..fe0f6b1e5 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -2,7 +2,7 @@ Exe netcoreapp2.0 - 1.0.1.24 + 1.0.1.25 NU1701 diff --git a/BTCPayServer/Controllers/StoresController.cs b/BTCPayServer/Controllers/StoresController.cs index cf52fa433..266aa470c 100644 --- a/BTCPayServer/Controllers/StoresController.cs +++ b/BTCPayServer/Controllers/StoresController.cs @@ -120,61 +120,17 @@ namespace BTCPayServer.Controllers return HttpContext.Request.GetAbsoluteRoot() + "/stores/" + storeId + "/"; } - class WebSocketTransport : LedgerWallet.Transports.ILedgerTransport - { - private readonly WebSocket webSocket; - - public WebSocketTransport(System.Net.WebSockets.WebSocket webSocket) - { - if (webSocket == null) - throw new ArgumentNullException(nameof(webSocket)); - this.webSocket = webSocket; - } - - public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(10); - public async Task Exchange(byte[][] apdus) - { - List responses = new List(); - using (CancellationTokenSource cts = new CancellationTokenSource(Timeout)) - { - foreach (var apdu in apdus) - { - await this.webSocket.SendAsync(new ArraySegment(apdu), WebSocketMessageType.Binary, true, cts.Token); - } - foreach (var apdu in apdus) - { - byte[] response = new byte[300]; - var result = await this.webSocket.ReceiveAsync(new ArraySegment(response), cts.Token); - Array.Resize(ref response, result.Count); - responses.Add(response); - } - } - return responses.ToArray(); - } - } - - class LedgerTestResult - { - public bool Success { get; set; } - public string Error { get; set; } - } - - class GetInfoResult + public class GetInfoResult { public int RecommendedSatoshiPerByte { get; set; } public double Balance { get; set; } } - class SendToAddressResult + public class SendToAddressResult { public string TransactionId { get; set; } } - class GetXPubResult - { - public string ExtPubKey { get; set; } - } - [HttpGet] [Route("{storeId}/ws/ledger")] public async Task LedgerConnection( @@ -193,185 +149,131 @@ namespace BTCPayServer.Controllers return NotFound(); var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync(); - var ledgerTransport = new WebSocketTransport(webSocket); - var ledger = new LedgerWallet.LedgerClient(ledgerTransport); + + var hw = new HardwareWalletService(webSocket); + object result = null; try { - if (command == "test") + BTCPayNetwork network = null; + if (cryptoCode != null) { - var version = await ledger.GetFirmwareVersionAsync(); - await Send(webSocket, new LedgerTestResult() { Success = true }); - } - if (command == "getxpub") - { - var network = _NetworkProvider.GetNetwork(cryptoCode); - try - { - var pubkey = await GetExtPubKey(ledger, network, new KeyPath("49'").Derive(network.CoinType).Derive(0, true), false); - var derivation = new DerivationStrategyFactory(network.NBitcoinNetwork).CreateDirectDerivationStrategy(pubkey, new DerivationStrategyOptions() - { - P2SH = true, - Legacy = false - }); - await Send(webSocket, new GetXPubResult() { ExtPubKey = derivation.ToString() }); - } - catch(FormatException) - { - await Send(webSocket, new LedgerTestResult() { Success = false, Error = "Unsupported ledger app" }); - } - } - if (command == "getinfo") - { - var network = _NetworkProvider.GetNetwork(cryptoCode); - var strategy = store.GetDerivationStrategies(_NetworkProvider).FirstOrDefault(s => s.Network.NBitcoinNetwork == network.NBitcoinNetwork); - if (strategy == null) - { - await Send(webSocket, new LedgerTestResult() { Success = false, Error = $"Derivation strategy for {cryptoCode} is not set" }); - return new EmptyResult(); - } - DirectDerivationStrategy directStrategy = GetDirectStrategy(strategy); - if (directStrategy == null) - { - await Send(webSocket, new LedgerTestResult() { Success = false, Error = $"The feature does not work for multi-sig or non-segwit wallets" }); - return new EmptyResult(); - } - - var foundKeyPath = await GetKeyPath(ledger, network, directStrategy); - - if (foundKeyPath == null) - { - await Send(webSocket, new LedgerTestResult() { Success = false, Error = $"This store is not configured to use this ledger" }); - return new EmptyResult(); - } - - var feeProvider = _FeeRateProvider.CreateFeeProvider(network); - var recommendedFees = feeProvider.GetFeeRateAsync(); - var balance = _WalletProvider.GetWallet(network).GetBalance(strategy.DerivationStrategyBase); - - await Send(webSocket, new GetInfoResult() { Balance = (double)(await balance).ToDecimal(MoneyUnit.BTC), RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi }); + network = _NetworkProvider.GetNetwork(cryptoCode); + if (network == null) + throw new FormatException("Invalid value for crypto code"); } - if (command == "sendtoaddress") + BitcoinAddress destinationAddress = null; + if (destination != null) { - var network = _NetworkProvider.GetNetwork(cryptoCode); - var strategy = store.GetDerivationStrategies(_NetworkProvider).FirstOrDefault(s => s.Network.NBitcoinNetwork == network.NBitcoinNetwork); - if (strategy == null) - { - await Send(webSocket, new LedgerTestResult() { Success = false, Error = $"Derivation strategy for {cryptoCode} is not set" }); - return new EmptyResult(); - } - - DirectDerivationStrategy directStrategy = GetDirectStrategy(strategy); - if (directStrategy == null) - { - await Send(webSocket, new LedgerTestResult() { Success = false, Error = $"The feature does not work for multi-sig or non-segwit wallets" }); - return new EmptyResult(); - } - - var foundKeyPath = await GetKeyPath(ledger, network, directStrategy); - - if (foundKeyPath == null) - { - await Send(webSocket, new LedgerTestResult() { Success = false, Error = $"This store is not configured to use this ledger" }); - return new EmptyResult(); - } - - BitcoinAddress destinationAddress = null; try { - destinationAddress = BitcoinAddress.Create(destination.Trim()); - } - catch - { - await Send(webSocket, new LedgerTestResult() { Success = false, Error = $"Invalid destination address" }); - return new EmptyResult(); + destinationAddress = BitcoinAddress.Create(destination); } + catch { } + if (destinationAddress == null) + throw new FormatException("Invalid value for destination"); + } - Money amountBTC = null; - try - { - amountBTC = Money.Parse(amount); - } - catch - { - await Send(webSocket, new LedgerTestResult() { Success = false, Error = $"Invalid amount" }); - return new EmptyResult(); - } - if (amount <= Money.Zero) - { - await Send(webSocket, new LedgerTestResult() { Success = false, Error = "The amount should be above zero" }); - return new EmptyResult(); - } - - FeeRate feeRateValue = null; + FeeRate feeRateValue = null; + if (feeRate != null) + { try { feeRateValue = new FeeRate(Money.Satoshis(int.Parse(feeRate)), 1); } - catch - { - await Send(webSocket, new LedgerTestResult() { Success = false, Error = "Invalid fee rate" }); - return new EmptyResult(); - } + catch { } + if (feeRateValue == null || feeRateValue.FeePerK <= Money.Zero) + throw new FormatException("Invalid value for fee rate"); + } - if (feeRateValue.FeePerK <= Money.Zero) - { - await Send(webSocket, new LedgerTestResult() { Success = false, Error = "The fee rate should be above zero" }); - return new EmptyResult(); - } - - bool substractFeeBool = bool.Parse(substractFees); - - var wallet = _WalletProvider.GetWallet(network); - var unspentCoins = await wallet.GetUnspentCoins(strategy.DerivationStrategyBase); - - TransactionBuilder builder = new TransactionBuilder(); - builder.AddCoins(unspentCoins.Item1); - builder.Send(destinationAddress, amountBTC); - if (substractFeeBool) - builder.SubtractFees(); - var change = await wallet.GetChangeAddressAsync(strategy.DerivationStrategyBase); - builder.SetChange(change.Item1); - builder.SendEstimatedFees(feeRateValue); - builder.Shuffle(); - var unsigned = builder.BuildTransaction(false); - - Dictionary keyPaths = unspentCoins.Item2; - var hasChange = unsigned.Outputs.Count == 2; - var usedCoins = builder.FindSpentCoins(unsigned); - ledgerTransport.Timeout = TimeSpan.FromMinutes(5); - var fullySigned = await ledger.SignTransactionAsync( - usedCoins.Select(c => new SignatureRequest - { - InputCoin = c, - KeyPath = foundKeyPath.Derive(keyPaths[c.Outpoint]), - PubKey = directStrategy.Root.Derive(keyPaths[c.Outpoint]).PubKey - }).ToArray(), - unsigned, - hasChange ? foundKeyPath.Derive(change.Item2) : null); + Money amountBTC = null; + if (amount != null) + { try { - var result = await wallet.BroadcastTransactionsAsync(new List() { fullySigned }); - if (!result[0].Success) + amountBTC = Money.Parse(amount); + } + catch { } + if (amountBTC == null || amountBTC <= Money.Zero) + throw new FormatException("Invalid value for amount"); + } + + bool subsctractFeesValue = false; + if (substractFees != null) + { + try + { + subsctractFeesValue = bool.Parse(substractFees); + } + catch { throw new FormatException("Invalid value for substract fees"); } + } + if (command == "test") + { + result = await hw.Test(); + } + if (command == "getxpub") + { + result = await hw.GetExtPubKey(network); + } + if (command == "getinfo") + { + var strategy = GetDirectDerivationStrategy(store, network); + var strategyBase = GetDerivationStrategy(store, network); + if (!await hw.SupportDerivation(network, strategy)) + { + throw new Exception($"This store is not configured to use this ledger"); + } + + var feeProvider = _FeeRateProvider.CreateFeeProvider(network); + var recommendedFees = feeProvider.GetFeeRateAsync(); + var balance = _WalletProvider.GetWallet(network).GetBalance(strategyBase); + result = new GetInfoResult() { Balance = (double)(await balance).ToDecimal(MoneyUnit.BTC), RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi }; + } + + if (command == "sendtoaddress") + { + var strategy = GetDirectDerivationStrategy(store, network); + var strategyBase = GetDerivationStrategy(store, network); + var wallet = _WalletProvider.GetWallet(network); + var change = wallet.GetChangeAddressAsync(strategyBase); + var unspentCoins = await wallet.GetUnspentCoins(strategyBase); + var changeAddress = await change; + unspentCoins.Item2.TryAdd(changeAddress.Item1.ScriptPubKey, changeAddress.Item2); + var transaction = await hw.SendToAddress(strategy, unspentCoins.Item1, network, + new[] { (destinationAddress as IDestination, amountBTC, subsctractFeesValue) }, + feeRateValue, + changeAddress.Item1, + unspentCoins.Item2); + try + { + var broadcastResult = await wallet.BroadcastTransactionsAsync(new List() { transaction }); + if (!broadcastResult[0].Success) { - await Send(webSocket, new LedgerTestResult() { Success = false, Error = $"RPC Error while broadcasting: {result[0].RPCCode} {result[0].RPCCodeMessage} {result[0].RPCMessage}" }); - return new EmptyResult(); + throw new Exception($"RPC Error while broadcasting: {broadcastResult[0].RPCCode} {broadcastResult[0].RPCCodeMessage} {broadcastResult[0].RPCMessage}"); } } catch (Exception ex) { - await Send(webSocket, new LedgerTestResult() { Success = false, Error = "Error while broadcasting: " + ex.Message }); - return new EmptyResult(); + throw new Exception("Error while broadcasting: " + ex.Message); } - await Send(webSocket, new SendToAddressResult() { TransactionId = fullySigned.GetHash().ToString() }); + result = new SendToAddressResult() { TransactionId = transaction.GetHash().ToString() }; } } - catch (LedgerWallet.LedgerWalletException ex) - { try { await Send(webSocket, new LedgerTestResult() { Success = false, Error = ex.Message }); } catch { } } catch (OperationCanceledException) - { try { await Send(webSocket, new LedgerTestResult() { Success = false, Error = "timeout" }); } catch { } } + { result = new LedgerTestResult() { Success = false, Error = "Timeout" }; } catch (Exception ex) - { try { await Send(webSocket, new LedgerTestResult() { Success = false, Error = ex.Message }); } catch { } } + { result = new LedgerTestResult() { Success = false, Error = ex.Message }; } + + try + { + if (result != null) + { + UTF8Encoding UTF8NOBOM = new UTF8Encoding(false); + var bytes = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, _MvcJsonOptions.SerializerSettings)); + await webSocket.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token); + } + } + catch { } finally { await webSocket.CloseSocket(); @@ -380,60 +282,26 @@ namespace BTCPayServer.Controllers return new EmptyResult(); } - private static async Task GetKeyPath(LedgerClient ledger, BTCPayNetwork network, DirectDerivationStrategy directStrategy) + private DirectDerivationStrategy GetDirectDerivationStrategy(StoreData store, BTCPayNetwork network) { - KeyPath foundKeyPath = null; - foreach (var account in - new[] { new KeyPath("49'"), new KeyPath("44'") } - .Select(purpose => purpose.Derive(network.CoinType)) - .SelectMany(coinType => Enumerable.Range(0, 5).Select(i => coinType.Derive(i, true)))) - { - try - { - var extpubkey = await GetExtPubKey(ledger, network, account, true); - if (directStrategy.Root.PubKey == extpubkey.ExtPubKey.PubKey) - { - foundKeyPath = account; - break; - } - } - catch (FormatException) - { - throw new Exception($"The opened ledger app does not support {network.NBitcoinNetwork.Name}"); - } - } - - return foundKeyPath; - } - - private static async Task GetExtPubKey(LedgerClient ledger, BTCPayNetwork network, KeyPath account, bool onlyChaincode) - { - var pubKey = await ledger.GetWalletPubKeyAsync(account); - if (pubKey.Address.Network != network.NBitcoinNetwork) - { - if (network.DefaultSettings.ChainType == NBXplorer.ChainType.Main) - throw new Exception($"The opened ledger app should be for {network.NBitcoinNetwork.Name}, not for {pubKey.Address.Network}"); - } - var fingerprint = onlyChaincode ? new byte[4] : (await ledger.GetWalletPubKeyAsync(account.Parent)).UncompressedPublicKey.Compress().Hash.ToBytes().Take(4).ToArray(); - var extpubkey = new ExtPubKey(pubKey.UncompressedPublicKey.Compress(), pubKey.ChainCode, (byte)account.Indexes.Length, fingerprint, account.Indexes.Last()).GetWif(network.NBitcoinNetwork); - return extpubkey; - } - - private static DirectDerivationStrategy GetDirectStrategy(DerivationStrategy strategy) - { - var directStrategy = strategy.DerivationStrategyBase as DirectDerivationStrategy; + var strategy = GetDerivationStrategy(store, network); + var directStrategy = strategy as DirectDerivationStrategy; if (directStrategy == null) - directStrategy = (strategy.DerivationStrategyBase as P2SHDerivationStrategy).Inner as DirectDerivationStrategy; + directStrategy = (strategy as P2SHDerivationStrategy).Inner as DirectDerivationStrategy; if (!directStrategy.Segwit) return null; return directStrategy; } - UTF8Encoding UTF8NOBOM = new UTF8Encoding(false); - private async Task Send(WebSocket webSocket, object result) + private DerivationStrategyBase GetDerivationStrategy(StoreData store, BTCPayNetwork network) { - var bytes = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, _MvcJsonOptions.SerializerSettings)); - await webSocket.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token); + var strategy = store.GetDerivationStrategies(_NetworkProvider).FirstOrDefault(s => s.Network.NBitcoinNetwork == network.NBitcoinNetwork); + if (strategy == null) + { + throw new Exception($"Derivation strategy for {network.CryptoCode} is not set"); + } + + return strategy.DerivationStrategyBase; } [HttpGet] diff --git a/BTCPayServer/Services/HardwareWalletService.cs b/BTCPayServer/Services/HardwareWalletService.cs new file mode 100644 index 000000000..5e9295c59 --- /dev/null +++ b/BTCPayServer/Services/HardwareWalletService.cs @@ -0,0 +1,226 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; +using LedgerWallet; +using NBitcoin; +using NBXplorer.DerivationStrategy; + +namespace BTCPayServer.Services +{ + + public class HardwareWalletException : Exception + { + public HardwareWalletException() { } + public HardwareWalletException(string message) : base(message) { } + public HardwareWalletException(string message, Exception inner) : base(message, inner) { } + } + public class HardwareWalletService + { + class WebSocketTransport : LedgerWallet.Transports.ILedgerTransport + { + private readonly WebSocket webSocket; + + public WebSocketTransport(System.Net.WebSockets.WebSocket webSocket) + { + if (webSocket == null) + throw new ArgumentNullException(nameof(webSocket)); + this.webSocket = webSocket; + } + + public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(10); + public async Task Exchange(byte[][] apdus) + { + List responses = new List(); + using (CancellationTokenSource cts = new CancellationTokenSource(Timeout)) + { + foreach (var apdu in apdus) + { + await this.webSocket.SendAsync(new ArraySegment(apdu), WebSocketMessageType.Binary, true, cts.Token); + } + foreach (var apdu in apdus) + { + byte[] response = new byte[300]; + var result = await this.webSocket.ReceiveAsync(new ArraySegment(response), cts.Token); + Array.Resize(ref response, result.Count); + responses.Add(response); + } + } + return responses.ToArray(); + } + } + + private readonly LedgerClient _Ledger; + public LedgerClient Ledger + { + get + { + return _Ledger; + } + } + WebSocketTransport _Transport = null; + public HardwareWalletService(System.Net.WebSockets.WebSocket ledgerWallet) + { + if (ledgerWallet == null) + throw new ArgumentNullException(nameof(ledgerWallet)); + _Transport = new WebSocketTransport(ledgerWallet); + _Ledger = new LedgerClient(_Transport); + } + + public async Task Test() + { + var version = await _Ledger.GetFirmwareVersionAsync(); + return new LedgerTestResult() { Success = true }; + } + + public async Task GetExtPubKey(BTCPayNetwork network) + { + if (network == null) + throw new ArgumentNullException(nameof(network)); + + var pubkey = await GetExtPubKey(_Ledger, network, new KeyPath("49'").Derive(network.CoinType).Derive(0, true), false); + var derivation = new DerivationStrategyFactory(network.NBitcoinNetwork).CreateDirectDerivationStrategy(pubkey, new DerivationStrategyOptions() + { + P2SH = true, + Legacy = false + }); + return new GetXPubResult() { ExtPubKey = derivation.ToString() }; + } + + private static async Task GetExtPubKey(LedgerClient ledger, BTCPayNetwork network, KeyPath account, bool onlyChaincode) + { + try + { + var pubKey = await ledger.GetWalletPubKeyAsync(account); + if (pubKey.Address.Network != network.NBitcoinNetwork) + { + if (network.DefaultSettings.ChainType == NBXplorer.ChainType.Main) + throw new Exception($"The opened ledger app should be for {network.NBitcoinNetwork.Name}, not for {pubKey.Address.Network}"); + } + var fingerprint = onlyChaincode ? new byte[4] : (await ledger.GetWalletPubKeyAsync(account.Parent)).UncompressedPublicKey.Compress().Hash.ToBytes().Take(4).ToArray(); + var extpubkey = new ExtPubKey(pubKey.UncompressedPublicKey.Compress(), pubKey.ChainCode, (byte)account.Indexes.Length, fingerprint, account.Indexes.Last()).GetWif(network.NBitcoinNetwork); + return extpubkey; + } + catch (FormatException) + { + throw new HardwareWalletException("Unsupported ledger app"); + } + } + + public async Task SupportDerivation(BTCPayNetwork network, DirectDerivationStrategy strategy) + { + if (network == null) + throw new ArgumentNullException(nameof(Network)); + if (strategy == null) + throw new ArgumentNullException(nameof(strategy)); + return await GetKeyPath(_Ledger, network, strategy) != null; + } + + private static async Task GetKeyPath(LedgerClient ledger, BTCPayNetwork network, DirectDerivationStrategy directStrategy) + { + KeyPath foundKeyPath = null; + foreach (var account in + new[] { new KeyPath("49'"), new KeyPath("44'") } + .Select(purpose => purpose.Derive(network.CoinType)) + .SelectMany(coinType => Enumerable.Range(0, 5).Select(i => coinType.Derive(i, true)))) + { + try + { + var extpubkey = await GetExtPubKey(ledger, network, account, true); + if (directStrategy.Root.PubKey == extpubkey.ExtPubKey.PubKey) + { + foundKeyPath = account; + break; + } + } + catch (FormatException) + { + throw new Exception($"The opened ledger app does not support {network.NBitcoinNetwork.Name}"); + } + } + + return foundKeyPath; + } + + public async Task SendToAddress(DirectDerivationStrategy strategy, + Coin[] coins, BTCPayNetwork network, + (IDestination destination, Money amount, bool substractFees)[] send, + FeeRate feeRate, + IDestination changeAddress, + Dictionary keypaths = null) + { + if (strategy == null) + throw new ArgumentNullException(nameof(strategy)); + if (network == null) + throw new ArgumentNullException(nameof(network)); + if (feeRate == null) + throw new ArgumentNullException(nameof(feeRate)); + if (changeAddress == null) + throw new ArgumentNullException(nameof(changeAddress)); + if (keypaths == null) + throw new ArgumentNullException(nameof(keypaths)); + if (feeRate.FeePerK <= Money.Zero) + { + throw new ArgumentOutOfRangeException(nameof(feeRate), "The fee rate should be above zero"); + } + + foreach (var element in send) + { + if (element.destination == null) + throw new ArgumentNullException(nameof(element.destination)); + if (element.amount == null) + throw new ArgumentNullException(nameof(element.amount)); + if (element.amount <= Money.Zero) + throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero"); + } + + var foundKeyPath = await GetKeyPath(Ledger, network, strategy); + + if (foundKeyPath == null) + { + throw new HardwareWalletException($"This store is not configured to use this ledger"); + } + + TransactionBuilder builder = new TransactionBuilder(); + builder.AddCoins(coins); + + foreach (var element in send) + { + builder.Send(element.destination, element.amount); + if (element.substractFees) + builder.SubtractFees(); + } + builder.SetChange(changeAddress); + builder.SendEstimatedFees(feeRate); + builder.Shuffle(); + var unsigned = builder.BuildTransaction(false); + + var hasChange = unsigned.Outputs.Count == 2; + var usedCoins = builder.FindSpentCoins(unsigned); + _Transport.Timeout = TimeSpan.FromMinutes(5); + var fullySigned = await Ledger.SignTransactionAsync( + usedCoins.Select(c => new SignatureRequest + { + InputCoin = c, + KeyPath = foundKeyPath.Derive(keypaths[c.TxOut.ScriptPubKey]), + PubKey = strategy.Root.Derive(keypaths[c.TxOut.ScriptPubKey]).PubKey + }).ToArray(), + unsigned, + hasChange ? foundKeyPath.Derive(keypaths[changeAddress.ScriptPubKey]) : null); + return fullySigned; + } + } + + public class LedgerTestResult + { + public bool Success { get; set; } + public string Error { get; set; } + } + + public class GetXPubResult + { + public string ExtPubKey { get; set; } + } +} diff --git a/BTCPayServer/Services/Wallets/BTCPayWallet.cs b/BTCPayServer/Services/Wallets/BTCPayWallet.cs index a6f9175f0..37b41b5bf 100644 --- a/BTCPayServer/Services/Wallets/BTCPayWallet.cs +++ b/BTCPayServer/Services/Wallets/BTCPayWallet.cs @@ -117,13 +117,13 @@ namespace BTCPayServer.Services.Wallets return Task.WhenAll(tasks); } - public async Task<(Coin[], Dictionary)> GetUnspentCoins(DerivationStrategyBase derivationStrategy, CancellationToken cancellation = default(CancellationToken)) + public async Task<(Coin[], Dictionary)> GetUnspentCoins(DerivationStrategyBase derivationStrategy, CancellationToken cancellation = default(CancellationToken)) { var changes = await _Client.GetUTXOsAsync(derivationStrategy, null, false, cancellation).ConfigureAwait(false); - var keyPaths = new Dictionary(); + var keyPaths = new Dictionary(); foreach (var coin in changes.GetUnspentUTXOs()) { - keyPaths.TryAdd(coin.Outpoint, coin.KeyPath); + keyPaths.TryAdd(coin.ScriptPubKey, coin.KeyPath); } return (changes.GetUnspentCoins(), keyPaths); } diff --git a/BTCPayServer/wwwroot/js/StoreWallet.js b/BTCPayServer/wwwroot/js/StoreWallet.js index 625182e15..c5373fc11 100644 --- a/BTCPayServer/wwwroot/js/StoreWallet.js +++ b/BTCPayServer/wwwroot/js/StoreWallet.js @@ -1,6 +1,8 @@ $(function () { var ledgerDetected = false; var bridge = new ledgerwebsocket.LedgerWebSocketBridge(srvModel.serverUrl + "ws/ledger"); + var recommendedFees = ""; + var recommendedBalance = ""; function WriteAlert(type, message) { $(".alert").removeClass("alert-danger"); @@ -25,6 +27,15 @@ $("#sendform").on("submit", function (elem) { elem.preventDefault(); + if ($("#amount-textbox").val() === "") { + $("#amount-textbox").val(recommendedBalance); + $("#substract-checkbox").prop("checked", true); + } + + if ($("#fee-textbox").val() === "") { + $("#fee-textbox").val(recommendedFees); + } + var args = ""; args += "cryptoCode=" + $("#cryptoCurrencies").val(); args += "&destination=" + $("#destination-textbox").val(); @@ -95,6 +106,8 @@ else { Write('check', 'success', 'This store is configured to use your ledger'); $(".crypto-info").css("display", "block"); + recommendedFees = result.recommendedSatoshiPerByte; + recommendedBalance = result.balance; $("#crypto-fee").text(result.recommendedSatoshiPerByte); $("#crypto-balance").text(result.balance); $("#crypto-code").text(cryptoCode);