mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-01-19 05:33:31 +01:00
Make wallet able to send to multiple destinations (#847)
* Make wallet able to send to multiple destinations * fix tests * update e2e tests * fix e2e part 2 * make headless again * pr changes * make wallet look exactly as old one when only 1 dest
This commit is contained in:
parent
3d436c3b0e
commit
88c931ec13
@ -53,8 +53,14 @@ namespace BTCPayServer.Tests
|
||||
var sendDestination = new Key().PubKey.Hash.GetAddress(user.SupportedNetwork.NBitcoinNetwork).ToString();
|
||||
var sendModel = new WalletSendModel()
|
||||
{
|
||||
Destination = sendDestination,
|
||||
Amount = 0.1m,
|
||||
Outputs = new List<WalletSendModel.TransactionOutput>()
|
||||
{
|
||||
new WalletSendModel.TransactionOutput()
|
||||
{
|
||||
DestinationAddress = sendDestination,
|
||||
Amount = 0.1m,
|
||||
}
|
||||
},
|
||||
FeeSatoshiPerByte = 1,
|
||||
CurrentBalance = 1.5m
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using BTCPayServer;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
@ -287,8 +287,7 @@ namespace BTCPayServer.Tests
|
||||
// Send to bob
|
||||
s.Driver.FindElement(By.Id("WalletSend")).Click();
|
||||
var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest);
|
||||
s.Driver.FindElement(By.Id("Destination")).SendKeys(bob.ToString());
|
||||
s.Driver.FindElement(By.Id("Amount")).SendKeys("1");
|
||||
SetTransactionOutput(0, bob, 1);
|
||||
s.Driver.ScrollTo(By.Id("SendMenu"));
|
||||
s.Driver.FindElement(By.Id("SendMenu")).ForceClick();
|
||||
s.Driver.FindElement(By.CssSelector("button[value=seed]")).Click();
|
||||
@ -302,6 +301,19 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
|
||||
Assert.Equal(walletTransactionLink, s.Driver.Url);
|
||||
}
|
||||
|
||||
void SetTransactionOutput(int index, BitcoinAddress dest, decimal amount, bool subtract = false)
|
||||
{
|
||||
s.Driver.FindElement(By.Id($"Outputs_{index}__DestinationAddress")).SendKeys(dest.ToString());
|
||||
var amountElement = s.Driver.FindElement(By.Id($"Outputs_{index}__Amount"));
|
||||
amountElement.Clear();
|
||||
amountElement.SendKeys(amount.ToString());
|
||||
var checkboxElement = s.Driver.FindElement(By.Id($"Outputs_{index}__SubtractFeesFromOutput"));
|
||||
if (checkboxElement.Selected != subtract)
|
||||
{
|
||||
checkboxElement.Click();
|
||||
}
|
||||
}
|
||||
SignWith(mnemonic);
|
||||
var accountKey = root.Derive(new KeyPath("m/49'/0'/0'")).GetWif(Network.RegTest).ToString();
|
||||
SignWith(accountKey);
|
||||
|
@ -1672,8 +1672,14 @@ namespace BTCPayServer.Tests
|
||||
var wallet = tester.PayTester.GetController<WalletsController>();
|
||||
var psbt = wallet.CreatePSBT(btcNetwork, onchainBTC, new WalletSendModel()
|
||||
{
|
||||
Amount = 0.5m,
|
||||
Destination = new Key().PubKey.GetAddress(btcNetwork.NBitcoinNetwork).ToString(),
|
||||
Outputs = new List<WalletSendModel.TransactionOutput>()
|
||||
{
|
||||
new WalletSendModel.TransactionOutput()
|
||||
{
|
||||
Amount = 0.5m,
|
||||
DestinationAddress = new Key().PubKey.GetAddress(btcNetwork.NBitcoinNetwork).ToString(),
|
||||
}
|
||||
},
|
||||
FeeSatoshiPerByte = 1
|
||||
}, default).GetAwaiter().GetResult();
|
||||
|
||||
|
@ -19,21 +19,28 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
var nbx = ExplorerClientProvider.GetExplorerClient(network);
|
||||
CreatePSBTRequest psbtRequest = new CreatePSBTRequest();
|
||||
CreatePSBTDestination psbtDestination = new CreatePSBTDestination();
|
||||
psbtRequest.Destinations.Add(psbtDestination);
|
||||
|
||||
foreach (var transactionOutput in sendModel.Outputs)
|
||||
{
|
||||
var psbtDestination = new CreatePSBTDestination();
|
||||
psbtRequest.Destinations.Add(psbtDestination);
|
||||
psbtDestination.Destination = BitcoinAddress.Create(transactionOutput.DestinationAddress, network.NBitcoinNetwork);
|
||||
psbtDestination.Amount = Money.Coins(transactionOutput.Amount.Value);
|
||||
psbtDestination.SubstractFees = transactionOutput.SubtractFeesFromOutput;
|
||||
}
|
||||
|
||||
if (network.SupportRBF)
|
||||
{
|
||||
psbtRequest.RBF = !sendModel.DisableRBF;
|
||||
}
|
||||
psbtDestination.Destination = BitcoinAddress.Create(sendModel.Destination, network.NBitcoinNetwork);
|
||||
psbtDestination.Amount = Money.Coins(sendModel.Amount.Value);
|
||||
|
||||
psbtRequest.FeePreference = new FeePreference();
|
||||
psbtRequest.FeePreference.ExplicitFeeRate = new FeeRate(Money.Satoshis(sendModel.FeeSatoshiPerByte), 1);
|
||||
if (sendModel.NoChange)
|
||||
{
|
||||
psbtRequest.ExplicitChangeAddress = psbtDestination.Destination;
|
||||
psbtRequest.ExplicitChangeAddress = psbtRequest.Destinations.First().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");
|
||||
|
@ -163,13 +163,20 @@ namespace BTCPayServer.Controllers
|
||||
var rateRules = store.GetStoreBlob().GetRateRules(NetworkProvider);
|
||||
rateRules.Spread = 0.0m;
|
||||
var currencyPair = new Rating.CurrencyPair(paymentMethod.PaymentId.CryptoCode, GetCurrencyCode(storeData.DefaultLang) ?? "USD");
|
||||
WalletSendModel model = new WalletSendModel()
|
||||
double.TryParse(defaultAmount, out var amount);
|
||||
var model = new WalletSendModel()
|
||||
{
|
||||
Destination = defaultDestination,
|
||||
Outputs = new List<WalletSendModel.TransactionOutput>()
|
||||
{
|
||||
new WalletSendModel.TransactionOutput()
|
||||
{
|
||||
Amount = Convert.ToDecimal(amount),
|
||||
DestinationAddress = defaultDestination
|
||||
}
|
||||
},
|
||||
CryptoCode = walletId.CryptoCode
|
||||
};
|
||||
if (double.TryParse(defaultAmount, out var amount))
|
||||
model.Amount = (decimal)amount;
|
||||
|
||||
|
||||
var feeProvider = _feeRateProvider.CreateFeeProvider(network);
|
||||
var recommendedFees = feeProvider.GetFeeRateAsync();
|
||||
@ -204,7 +211,7 @@ namespace BTCPayServer.Controllers
|
||||
[Route("{walletId}/send")]
|
||||
public async Task<IActionResult> WalletSend(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, WalletSendModel vm, string command = null, CancellationToken cancellation = default)
|
||||
WalletId walletId, WalletSendModel vm, string command = "", CancellationToken cancellation = default)
|
||||
{
|
||||
if (walletId?.StoreId == null)
|
||||
return NotFound();
|
||||
@ -215,17 +222,76 @@ namespace BTCPayServer.Controllers
|
||||
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)
|
||||
decimal transactionAmountSum = 0;
|
||||
|
||||
if (command == "add-output")
|
||||
{
|
||||
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");
|
||||
vm.Outputs.Add(new WalletSendModel.TransactionOutput());
|
||||
return View(vm);
|
||||
|
||||
}
|
||||
if (command.StartsWith("remove-output", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
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>();
|
||||
|
||||
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), "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",
|
||||
ModelState);
|
||||
}
|
||||
}
|
||||
|
||||
if (subtractFeesOutputsCount.Count > 1)
|
||||
{
|
||||
foreach (var subtractFeesOutput in subtractFeesOutputsCount)
|
||||
{
|
||||
vm.AddModelError(model => model.Outputs[subtractFeesOutput].SubtractFeesFromOutput,
|
||||
"You can only subtract fees from one output", ModelState);
|
||||
}
|
||||
}else if (vm.CurrentBalance == transactionAmountSum && vm.Outputs.Count > 1)
|
||||
{
|
||||
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", ModelState);
|
||||
}
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
return View(vm);
|
||||
|
||||
@ -238,15 +304,16 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
catch (NBXplorerException ex)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Amount), ex.Error.Message);
|
||||
ModelState.AddModelError(string.Empty, ex.Error.Message);
|
||||
return View(vm);
|
||||
}
|
||||
catch (NotSupportedException)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Destination), "You need to update your version of NBXplorer");
|
||||
ModelState.AddModelError(string.Empty, "You need to update your version of NBXplorer");
|
||||
return View(vm);
|
||||
}
|
||||
derivationScheme.RebaseKeyPaths(psbt.PSBT);
|
||||
|
||||
switch (command)
|
||||
{
|
||||
case "ledger":
|
||||
@ -254,10 +321,13 @@ namespace BTCPayServer.Controllers
|
||||
case "seed":
|
||||
return SignWithSeed(walletId, psbt.PSBT.ToBase64());
|
||||
case "analyze-psbt":
|
||||
return RedirectToAction(nameof(WalletPSBT), new { walletId = walletId, psbt = psbt.PSBT.ToBase64(), FileName= $"Send-{vm.Amount.Value}-{network.CryptoCode}-to-{destination[0].ToString()}.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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private ViewResult ViewWalletSendLedger(PSBT psbt, BitcoinAddress hintChange = null)
|
||||
|
@ -1,9 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Mails;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
|
29
BTCPayServer/Extensions/ModelStateExtensions.cs
Normal file
29
BTCPayServer/Extensions/ModelStateExtensions.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Linq.Expressions;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public static class ModelStateExtensions
|
||||
{
|
||||
public static void AddModelError<TModel, TProperty>(
|
||||
this ModelStateDictionary modelState,
|
||||
Expression<Func<TModel, TProperty>> ex,
|
||||
string message
|
||||
)
|
||||
{
|
||||
var key = ExpressionHelper.GetExpressionText(ex);
|
||||
modelState.AddModelError(key, message);
|
||||
}
|
||||
|
||||
public static void AddModelError<TModel, TProperty>(this TModel source,
|
||||
Expression<Func<TModel, TProperty>> ex,
|
||||
string message,
|
||||
ModelStateDictionary modelState)
|
||||
{
|
||||
var key = ExpressionHelper.GetExpressionText(ex);
|
||||
modelState.AddModelError(key, message);
|
||||
}
|
||||
}
|
||||
}
|
@ -8,22 +8,27 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
{
|
||||
public class WalletSendModel
|
||||
{
|
||||
[Required]
|
||||
public string Destination { get; set; }
|
||||
|
||||
public List<TransactionOutput> Outputs { get; set; } = new List<TransactionOutput>();
|
||||
|
||||
[Range(0.0, double.MaxValue)]
|
||||
[Required]
|
||||
public decimal? Amount { get; set; }
|
||||
public class TransactionOutput
|
||||
{
|
||||
[Display(Name = "Destination Address")]
|
||||
[Required]
|
||||
public string DestinationAddress { get; set; }
|
||||
|
||||
[Display(Name = "Amount")] [Required] [Range(0.0, double.MaxValue)]public decimal? Amount { get; set; }
|
||||
|
||||
|
||||
[Display(Name = "Subtract fees from this output amount")]
|
||||
public bool SubtractFeesFromOutput { get; set; }
|
||||
}
|
||||
public decimal CurrentBalance { get; set; }
|
||||
|
||||
public string CryptoCode { get; set; }
|
||||
|
||||
public int RecommendedSatoshiPerByte { get; set; }
|
||||
|
||||
[Display(Name = "Subtract fees from amount")]
|
||||
public bool SubstractFees { get; set; }
|
||||
|
||||
[Range(1, int.MaxValue)]
|
||||
[Display(Name = "Fee rate (satoshi per byte)")]
|
||||
[Required]
|
||||
|
@ -1,4 +1,5 @@
|
||||
@model WalletSendModel
|
||||
@using Microsoft.AspNetCore.Mvc.ModelBinding
|
||||
@model WalletSendModel
|
||||
@{
|
||||
Layout = "../Shared/_NavLayout.cshtml";
|
||||
ViewData["Title"] = "Manage wallet";
|
||||
@ -10,52 +11,96 @@
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<p>
|
||||
Send funds to a destination address.
|
||||
Send funds to destinations
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="row no-gutters">
|
||||
<div class="col-md-12">
|
||||
<form method="post">
|
||||
<input type="hidden" asp-for="Divisibility" />
|
||||
<input type="hidden" asp-for="Fiat" />
|
||||
<input type="hidden" asp-for="Rate" />
|
||||
<input type="hidden" asp-for="CurrentBalance" />
|
||||
<input type="hidden" asp-for="RecommendedSatoshiPerByte" />
|
||||
<input type="hidden" asp-for="CryptoCode" />
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="Destination"></label>
|
||||
<input asp-for="Destination" class="form-control" />
|
||||
<span asp-validation-for="Destination" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Amount"></label>
|
||||
<div class="input-group">
|
||||
<input asp-for="Amount" asp-format="{0}" class="form-control" onkeyup='updateFiatValue();' />
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text text-muted" style="display:none;" id="fiatValue"></span>
|
||||
<input type="hidden" asp-for="Divisibility"/>
|
||||
<input type="hidden" asp-for="Fiat"/>
|
||||
<input type="hidden" asp-for="Rate"/>
|
||||
<input type="hidden" asp-for="CurrentBalance"/>
|
||||
<input type="hidden" asp-for="RecommendedSatoshiPerByte"/>
|
||||
<input type="hidden" asp-for="CryptoCode"/>
|
||||
<ul class="text-danger">
|
||||
@foreach (var errors in ViewData.ModelState.Where(pair => pair.Key == string.Empty && pair.Value.ValidationState == ModelValidationState.Invalid))
|
||||
{
|
||||
foreach (var error in
|
||||
errors.Value.Errors)
|
||||
{
|
||||
<li>@error.ErrorMessage</li>
|
||||
}
|
||||
}
|
||||
</ul>
|
||||
<div class="list-group mb-2">
|
||||
@if (Model.Outputs.Count > 1)
|
||||
{
|
||||
<div class="list-group-item ">
|
||||
<h5 class="mb-1">Destinations</h5>
|
||||
</div>
|
||||
</div>
|
||||
<span asp-validation-for="Amount" class="text-danger"></span>
|
||||
<p class="form-text text-muted crypto-info">
|
||||
Your current balance is <a id="crypto-balance-link" href="#"><span>@Model.CurrentBalance</span></a> <span>@Model.CryptoCode</span>.
|
||||
</p>
|
||||
}
|
||||
@for (var index = 0; index < Model.Outputs.Count; index++)
|
||||
{
|
||||
<div class="list-group-item transaction-output-form p-0 pl-lg-2 @(Model.Outputs.Count==1? "border-0": "") ">
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-12 @(Model.Outputs.Count==1? "col-lg-12": "col-lg-10") py-2 ">
|
||||
<div class="form-group">
|
||||
<label asp-for="Outputs[index].Amount" class="control-label"></label>
|
||||
<div class="input-group">
|
||||
<input asp-for="Outputs[index].Amount" type="number" step="any" asp-format="{0}" class="form-control output-amount"/>
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text text-muted fiat-value" style="display:none;"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="form-text text-muted crypto-info">
|
||||
Your current balance is
|
||||
<button type="button" class="crypto-balance-link btn btn-link p-0">@Model.CurrentBalance</button> <span>@Model.CryptoCode</span>.
|
||||
</p>
|
||||
|
||||
<span asp-validation-for="Outputs[index].Amount" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Outputs[index].DestinationAddress" class="control-label"></label>
|
||||
<input asp-for="Outputs[index].DestinationAddress" class="form-control"/>
|
||||
<span asp-validation-for="Outputs[index].DestinationAddress" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Outputs[index].SubtractFeesFromOutput" class="control-label"></label>
|
||||
<input type="checkbox" asp-for="Outputs[index].SubtractFeesFromOutput" class="form-check subtract-fees"/>
|
||||
<span asp-validation-for="Outputs[index].SubtractFeesFromOutput" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-2 pull-right">
|
||||
@if (Model.Outputs.Count > 1)
|
||||
{
|
||||
<button type="submit" title="Remove Destination" name="command" value="@($"remove-output:{index}")"
|
||||
class="d-block d-lg-none d-xl-none btn btn-danger mb-2 ml-2">
|
||||
Remove Destination
|
||||
</button>
|
||||
<button type="submit" title="Remove Destination" name="command" value="@($"remove-output:{index}")"
|
||||
class="d-none d-lg-block remove-destination-btn text-decoration-none h-100 align-middle btn text-danger btn-link fa fa-times rounded-0 pull-right">
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="FeeSatoshiPerByte"></label>
|
||||
<input asp-for="FeeSatoshiPerByte" class="form-control" />
|
||||
<input asp-for="FeeSatoshiPerByte" type="number" step="any" class="form-control"/>
|
||||
<span id="FeeRate-Error" class="text-danger"></span>
|
||||
<p class="form-text text-muted crypto-info">
|
||||
The recommended value is <a id="crypto-fee-link" href="#"><span>@Model.RecommendedSatoshiPerByte</span></a> satoshi per byte.
|
||||
The recommended value is
|
||||
<button type="button" id="crypto-fee-link" class="btn btn-link p-0">@Model.RecommendedSatoshiPerByte</button> satoshi per byte.
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="SubstractFees"></label>
|
||||
<input asp-for="SubstractFees" class="form-check" />
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header" id="accordian-dev-info-notification-header">
|
||||
<h2 class="mb-0">
|
||||
@ -68,22 +113,26 @@
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label asp-for="NoChange"></label>
|
||||
<a href="https://docs.btcpayserver.org/features/wallet#make-sure-no-change-utxo-is-created-expert-mode" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
|
||||
<input asp-for="NoChange" class="form-check" />
|
||||
<a href="https://docs.btcpayserver.org/features/wallet#make-sure-no-change-utxo-is-created-expert-mode" target="_blank">
|
||||
<span class="fa fa-question-circle-o" title="More information..."></span>
|
||||
</a>
|
||||
<input asp-for="NoChange" class="form-check"/>
|
||||
</div>
|
||||
@if (Model.SupportRBF)
|
||||
{
|
||||
<div class="form-group">
|
||||
<label asp-for="DisableRBF"></label>
|
||||
<a href="https://bitcoin.org/en/glossary/rbf" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
|
||||
<input asp-for="DisableRBF" class="form-check" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="DisableRBF"></label>
|
||||
<a href="https://bitcoin.org/en/glossary/rbf" target="_blank">
|
||||
<span class="fa fa-question-circle-o" title="More information..."></span>
|
||||
</a>
|
||||
<input asp-for="DisableRBF" class="form-check"/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="dropdown" style="margin-top:16px;">
|
||||
<div class="form-group d-flex mt-2">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-primary dropdown-toggle" type="button" id="SendMenu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Sign with...
|
||||
</button>
|
||||
@ -93,11 +142,23 @@
|
||||
<button name="command" type="submit" class="dropdown-item" value="analyze-psbt">... a wallet supporting PSBT</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" name="command" value="add-output" class="ml-1 btn btn-secondary">Add another destination </button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts
|
||||
{
|
||||
<script src="~/js/WalletSend.js" type="text/javascript" defer="defer"></script>
|
||||
<style>
|
||||
.remove-destination-btn{
|
||||
font-size: 1.5rem;
|
||||
border-radius: 0;
|
||||
}
|
||||
.remove-destination-btn:hover{
|
||||
background-color: #CCCCCC;
|
||||
}
|
||||
</style>
|
||||
}
|
||||
|
@ -1,32 +1,47 @@
|
||||
function updateFiatValue() {
|
||||
function updateFiatValue(element) {
|
||||
|
||||
if (!element) {
|
||||
element = $(this);
|
||||
}
|
||||
var rateStr = $("#Rate").val();
|
||||
var divisibilityStr = $("#Divisibility").val();
|
||||
var fiat = $("#Fiat").val();
|
||||
var rate = parseFloat(rateStr);
|
||||
var divisibility = parseInt(divisibilityStr);
|
||||
if (!isNaN(rate) && !isNaN(divisibility)) {
|
||||
var fiatValue = $("#fiatValue");
|
||||
var amountValue = parseFloat($("#Amount").val());
|
||||
var fiatValue = $(element).parents(".input-group").first().find(".fiat-value");
|
||||
var amountValue = parseFloat($(element).val());
|
||||
if (!isNaN(amountValue)) {
|
||||
fiatValue.css("display", "inline");
|
||||
fiatValue.show();
|
||||
fiatValue.text("= " + (rate * amountValue).toFixed(divisibility) + " " + fiat);
|
||||
} else {
|
||||
fiatValue.text("");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateFiatValueWithCurrentElement() {
|
||||
updateFiatValue($(this))
|
||||
}
|
||||
|
||||
$(function () {
|
||||
updateFiatValue();
|
||||
|
||||
|
||||
$(".output-amount").on("input", updateFiatValueWithCurrentElement).each(updateFiatValueWithCurrentElement);
|
||||
|
||||
$("#crypto-fee-link").on("click", function (elem) {
|
||||
elem.preventDefault();
|
||||
var val = $("#crypto-fee-link").text();
|
||||
var val = $(this).text();
|
||||
$("#FeeSatoshiPerByte").val(val);
|
||||
return false;
|
||||
});
|
||||
|
||||
$("#crypto-balance-link").on("click", function (elem) {
|
||||
elem.preventDefault();
|
||||
var val = $("#crypto-balance-link").text();
|
||||
$("#Amount").val(val);
|
||||
$("#SubstractFees").prop('checked', true);
|
||||
$(".crypto-balance-link").on("click", function (elem) {
|
||||
var val = $(this).text();
|
||||
var parentContainer = $(this).parents(".transaction-output-form");
|
||||
var outputAmountElement = parentContainer.find(".output-amount");
|
||||
outputAmountElement.val(val);
|
||||
parentContainer.find(".subtract-fees").prop('checked', true);
|
||||
updateFiatValue(outputAmountElement);
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user