diff --git a/BTCPayServer/Controllers/WalletsController.cs b/BTCPayServer/Controllers/WalletsController.cs index bda65045b..cf908d821 100644 --- a/BTCPayServer/Controllers/WalletsController.cs +++ b/BTCPayServer/Controllers/WalletsController.cs @@ -146,7 +146,7 @@ namespace BTCPayServer.Controllers [Route("{walletId}/send")] public async Task WalletSend( [ModelBinder(typeof(WalletIdModelBinder))] - WalletId walletId, string defaultDestination = null, string defaultAmount = null) + WalletId walletId, string defaultDestination = null, string defaultAmount = null, bool advancedMode = false) { if (walletId?.StoreId == null) return NotFound(); @@ -195,6 +195,7 @@ namespace BTCPayServer.Controllers } catch (Exception ex) { model.RateError = ex.Message; } } + model.AdvancedMode = advancedMode; return View(model); } @@ -202,7 +203,7 @@ namespace BTCPayServer.Controllers [Route("{walletId}/send")] public async Task WalletSend( [ModelBinder(typeof(WalletIdModelBinder))] - WalletId walletId, WalletSendModel vm) + WalletId walletId, WalletSendModel vm, string command = null) { if (walletId?.StoreId == null) return NotFound(); @@ -212,6 +213,14 @@ namespace BTCPayServer.Controllers var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode); if (network == null) return NotFound(); + + if (command == "noob" || command == "expert") + { + ModelState.Clear(); + vm.AdvancedMode = command == "expert"; + return View(vm); + } + var destination = ParseDestination(vm.Destination, network.NBitcoinNetwork); if (destination == null) ModelState.AddModelError(nameof(vm.Destination), "Invalid address"); @@ -231,7 +240,8 @@ namespace BTCPayServer.Controllers Destination = vm.Destination, Amount = vm.Amount.Value, SubstractFees = vm.SubstractFees, - FeeSatoshiPerByte = vm.FeeSatoshiPerByte + FeeSatoshiPerByte = vm.FeeSatoshiPerByte, + NoChange = vm.NoChange }); } @@ -403,6 +413,7 @@ namespace BTCPayServer.Controllers // getxpub int account = 0, // sendtoaddress + bool noChange = false, string destination = null, string amount = null, string feeRate = null, string substractFees = null ) { @@ -487,24 +498,16 @@ namespace BTCPayServer.Controllers var strategy = GetDirectDerivationStrategy(derivationScheme); var wallet = _walletProvider.GetWallet(network); var change = wallet.GetChangeAddressAsync(derivationScheme); - - var unspentCoins = await wallet.GetUnspentCoins(derivationScheme); - var changeAddress = await change; - var send = new[] { ( - destination: destinationAddress as IDestination, - amount: amountBTC, - substractFees: subsctractFeesValue) }; - - foreach (var element in send) + var keypaths = new Dictionary(); + List availableCoins = new List(); + foreach (var c in await wallet.GetUnspentCoins(derivationScheme)) { - 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"); + keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath); + availableCoins.Add(c.Coin); } + var changeAddress = await change; + var storeBlob = storeData.GetStoreBlob(); var paymentId = new Payments.PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike); var foundKeyPath = storeBlob.GetWalletKeyPathRoot(paymentId); @@ -520,10 +523,25 @@ namespace BTCPayServer.Controllers storeData.SetStoreBlob(storeBlob); await Repository.UpdateStore(storeData); } +retry: + var send = new[] { ( + destination: destinationAddress as IDestination, + amount: amountBTC, + substractFees: subsctractFeesValue) }; + + 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"); + } TransactionBuilder builder = network.NBitcoinNetwork.CreateTransactionBuilder(); builder.StandardTransactionPolicy.MinRelayTxFee = summary.Status.BitcoinStatus.MinRelayTxFee; - builder.AddCoins(unspentCoins.Select(c => c.Coin).ToArray()); + builder.AddCoins(availableCoins); foreach (var element in send) { @@ -531,6 +549,7 @@ namespace BTCPayServer.Controllers if (element.substractFees) builder.SubtractFees(); } + builder.SetChange(changeAddress.Item1); if (network.MinFee == null) @@ -547,13 +566,15 @@ namespace BTCPayServer.Controllers } var unsigned = builder.BuildTransaction(false); - var keypaths = new Dictionary(); - foreach (var c in unspentCoins) + var hasChange = unsigned.Outputs.Any(o => o.ScriptPubKey == changeAddress.Item1.ScriptPubKey); + if (noChange && hasChange) { - keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath); + availableCoins = builder.FindSpentCoins(unsigned).Cast().ToList(); + amountBTC = builder.FindSpentCoins(unsigned).Select(c => c.TxOut.Value).Sum(); + subsctractFeesValue = true; + goto retry; } - var hasChange = unsigned.Outputs.Count == 2; var usedCoins = builder.FindSpentCoins(unsigned); Dictionary parentTransactions = new Dictionary(); diff --git a/BTCPayServer/Models/WalletViewModels/WalletSendLedgerModel.cs b/BTCPayServer/Models/WalletViewModels/WalletSendLedgerModel.cs index a3aec83bc..948903714 100644 --- a/BTCPayServer/Models/WalletViewModels/WalletSendLedgerModel.cs +++ b/BTCPayServer/Models/WalletViewModels/WalletSendLedgerModel.cs @@ -11,5 +11,6 @@ namespace BTCPayServer.Models.WalletViewModels public bool SubstractFees { get; set; } public decimal Amount { get; set; } public string Destination { get; set; } + public bool NoChange { get; set; } } } diff --git a/BTCPayServer/Models/WalletViewModels/WalletSendModel.cs b/BTCPayServer/Models/WalletViewModels/WalletSendModel.cs index 4b006b619..7faa04a13 100644 --- a/BTCPayServer/Models/WalletViewModels/WalletSendModel.cs +++ b/BTCPayServer/Models/WalletViewModels/WalletSendModel.cs @@ -28,6 +28,10 @@ namespace BTCPayServer.Models.WalletViewModels [Display(Name = "Fee rate (satoshi per byte)")] [Required] public int FeeSatoshiPerByte { get; set; } + + [Display(Name = "Make sure no change UTXO is created")] + public bool NoChange { get; set; } + public bool AdvancedMode { get; set; } public decimal? Rate { get; set; } public int Divisibility { get; set; } public string Fiat { get; set; } diff --git a/BTCPayServer/Views/Wallets/WalletSend.cshtml b/BTCPayServer/Views/Wallets/WalletSend.cshtml index f601aa21a..4995946ec 100644 --- a/BTCPayServer/Views/Wallets/WalletSend.cshtml +++ b/BTCPayServer/Views/Wallets/WalletSend.cshtml @@ -55,7 +55,23 @@ - + @if (Model.AdvancedMode) + { +
+ + + +
+ } + + @if (Model.AdvancedMode) + { + + } + else + { + + } diff --git a/BTCPayServer/Views/Wallets/WalletSendLedger.cshtml b/BTCPayServer/Views/Wallets/WalletSendLedger.cshtml index 525b07d67..9e63d9b0c 100644 --- a/BTCPayServer/Views/Wallets/WalletSendLedger.cshtml +++ b/BTCPayServer/Views/Wallets/WalletSendLedger.cshtml @@ -16,6 +16,7 @@ +

You can send money received by this store to an address with the help of your Ledger Wallet.
If you don't have a Ledger Wallet, use Electrum with your favorite hardware wallet to transfer crypto.
diff --git a/BTCPayServer/wwwroot/js/WalletSendLedger.js b/BTCPayServer/wwwroot/js/WalletSendLedger.js index c1813fb32..40f9488ab 100644 --- a/BTCPayServer/wwwroot/js/WalletSendLedger.js +++ b/BTCPayServer/wwwroot/js/WalletSendLedger.js @@ -3,6 +3,7 @@ var amount = $("#Amount").val(); var fee = $("#FeeSatoshiPerByte").val(); var substractFee = $("#SubstractFees").val(); + var noChange = $("#NoChange").val(); var loc = window.location, ws_uri; if (loc.protocol === "https:") { @@ -48,8 +49,14 @@ args += "&amount=" + amount; args += "&feeRate=" + fee; args += "&substractFees=" + substractFee; + args += "&noChange=" + noChange; - WriteAlert("warning", 'Please validate the transaction on your ledger'); + if (noChange === "True") { + WriteAlert("warning", 'WARNING: Because you want to make sure no change UTXO is created, you will end up sending more than the chosen amount to your destination. Please validate the transaction on your ledger'); + } + else { + WriteAlert("warning", 'Please validate the transaction on your ledger'); + } var confirmButton = $("#confirm-button"); confirmButton.prop("disabled", true);