Add expert mode to BTCPay with No Change UTXO option

This commit is contained in:
nicolas.dorier
2019-01-15 23:50:45 +09:00
parent c333902468
commit e620fc0283
6 changed files with 75 additions and 25 deletions

View File

@@ -146,7 +146,7 @@ namespace BTCPayServer.Controllers
[Route("{walletId}/send")] [Route("{walletId}/send")]
public async Task<IActionResult> WalletSend( public async Task<IActionResult> WalletSend(
[ModelBinder(typeof(WalletIdModelBinder))] [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) if (walletId?.StoreId == null)
return NotFound(); return NotFound();
@@ -195,6 +195,7 @@ namespace BTCPayServer.Controllers
} }
catch (Exception ex) { model.RateError = ex.Message; } catch (Exception ex) { model.RateError = ex.Message; }
} }
model.AdvancedMode = advancedMode;
return View(model); return View(model);
} }
@@ -202,7 +203,7 @@ namespace BTCPayServer.Controllers
[Route("{walletId}/send")] [Route("{walletId}/send")]
public async Task<IActionResult> WalletSend( public async Task<IActionResult> WalletSend(
[ModelBinder(typeof(WalletIdModelBinder))] [ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletSendModel vm) WalletId walletId, WalletSendModel vm, string command = null)
{ {
if (walletId?.StoreId == null) if (walletId?.StoreId == null)
return NotFound(); return NotFound();
@@ -212,6 +213,14 @@ namespace BTCPayServer.Controllers
var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode); var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode);
if (network == null) if (network == null)
return NotFound(); return NotFound();
if (command == "noob" || command == "expert")
{
ModelState.Clear();
vm.AdvancedMode = command == "expert";
return View(vm);
}
var destination = ParseDestination(vm.Destination, network.NBitcoinNetwork); var destination = ParseDestination(vm.Destination, network.NBitcoinNetwork);
if (destination == null) if (destination == null)
ModelState.AddModelError(nameof(vm.Destination), "Invalid address"); ModelState.AddModelError(nameof(vm.Destination), "Invalid address");
@@ -231,7 +240,8 @@ namespace BTCPayServer.Controllers
Destination = vm.Destination, Destination = vm.Destination,
Amount = vm.Amount.Value, Amount = vm.Amount.Value,
SubstractFees = vm.SubstractFees, SubstractFees = vm.SubstractFees,
FeeSatoshiPerByte = vm.FeeSatoshiPerByte FeeSatoshiPerByte = vm.FeeSatoshiPerByte,
NoChange = vm.NoChange
}); });
} }
@@ -403,6 +413,7 @@ namespace BTCPayServer.Controllers
// getxpub // getxpub
int account = 0, int account = 0,
// sendtoaddress // sendtoaddress
bool noChange = false,
string destination = null, string amount = null, string feeRate = null, string substractFees = null string destination = null, string amount = null, string feeRate = null, string substractFees = null
) )
{ {
@@ -487,24 +498,16 @@ namespace BTCPayServer.Controllers
var strategy = GetDirectDerivationStrategy(derivationScheme); var strategy = GetDirectDerivationStrategy(derivationScheme);
var wallet = _walletProvider.GetWallet(network); var wallet = _walletProvider.GetWallet(network);
var change = wallet.GetChangeAddressAsync(derivationScheme); var change = wallet.GetChangeAddressAsync(derivationScheme);
var keypaths = new Dictionary<Script, KeyPath>();
var unspentCoins = await wallet.GetUnspentCoins(derivationScheme); List<Coin> availableCoins = new List<Coin>();
var changeAddress = await change; foreach (var c in await wallet.GetUnspentCoins(derivationScheme))
var send = new[] { (
destination: destinationAddress as IDestination,
amount: amountBTC,
substractFees: subsctractFeesValue) };
foreach (var element in send)
{ {
if (element.destination == null) keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
throw new ArgumentNullException(nameof(element.destination)); availableCoins.Add(c.Coin);
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 changeAddress = await change;
var storeBlob = storeData.GetStoreBlob(); var storeBlob = storeData.GetStoreBlob();
var paymentId = new Payments.PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike); var paymentId = new Payments.PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike);
var foundKeyPath = storeBlob.GetWalletKeyPathRoot(paymentId); var foundKeyPath = storeBlob.GetWalletKeyPathRoot(paymentId);
@@ -520,10 +523,25 @@ namespace BTCPayServer.Controllers
storeData.SetStoreBlob(storeBlob); storeData.SetStoreBlob(storeBlob);
await Repository.UpdateStore(storeData); 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(); TransactionBuilder builder = network.NBitcoinNetwork.CreateTransactionBuilder();
builder.StandardTransactionPolicy.MinRelayTxFee = summary.Status.BitcoinStatus.MinRelayTxFee; builder.StandardTransactionPolicy.MinRelayTxFee = summary.Status.BitcoinStatus.MinRelayTxFee;
builder.AddCoins(unspentCoins.Select(c => c.Coin).ToArray()); builder.AddCoins(availableCoins);
foreach (var element in send) foreach (var element in send)
{ {
@@ -531,6 +549,7 @@ namespace BTCPayServer.Controllers
if (element.substractFees) if (element.substractFees)
builder.SubtractFees(); builder.SubtractFees();
} }
builder.SetChange(changeAddress.Item1); builder.SetChange(changeAddress.Item1);
if (network.MinFee == null) if (network.MinFee == null)
@@ -547,13 +566,15 @@ namespace BTCPayServer.Controllers
} }
var unsigned = builder.BuildTransaction(false); var unsigned = builder.BuildTransaction(false);
var keypaths = new Dictionary<Script, KeyPath>(); var hasChange = unsigned.Outputs.Any(o => o.ScriptPubKey == changeAddress.Item1.ScriptPubKey);
foreach (var c in unspentCoins) if (noChange && hasChange)
{ {
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath); availableCoins = builder.FindSpentCoins(unsigned).Cast<Coin>().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); var usedCoins = builder.FindSpentCoins(unsigned);
Dictionary<uint256, Transaction> parentTransactions = new Dictionary<uint256, Transaction>(); Dictionary<uint256, Transaction> parentTransactions = new Dictionary<uint256, Transaction>();

View File

@@ -11,5 +11,6 @@ namespace BTCPayServer.Models.WalletViewModels
public bool SubstractFees { get; set; } public bool SubstractFees { get; set; }
public decimal Amount { get; set; } public decimal Amount { get; set; }
public string Destination { get; set; } public string Destination { get; set; }
public bool NoChange { get; set; }
} }
} }

View File

@@ -28,6 +28,10 @@ namespace BTCPayServer.Models.WalletViewModels
[Display(Name = "Fee rate (satoshi per byte)")] [Display(Name = "Fee rate (satoshi per byte)")]
[Required] [Required]
public int FeeSatoshiPerByte { get; set; } 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 decimal? Rate { get; set; }
public int Divisibility { get; set; } public int Divisibility { get; set; }
public string Fiat { get; set; } public string Fiat { get; set; }

View File

@@ -55,7 +55,23 @@
<label asp-for="SubstractFees"></label> <label asp-for="SubstractFees"></label>
<input asp-for="SubstractFees" class="form-check" /> <input asp-for="SubstractFees" class="form-check" />
</div> </div>
<button type="submit" class="btn btn-primary">Confirm</button> @if (Model.AdvancedMode)
{
<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" />
</div>
}
<button name="command" type="submit" class="btn btn-primary">Confirm</button>
@if (Model.AdvancedMode)
{
<button name="command" type="submit" value="noob" class="btn btn-secondary">Use noob mode</button>
}
else
{
<button name="command" type="submit" value="expert" class="btn btn-secondary">Use expert mode</button>
}
</form> </form>
</div> </div>
</div> </div>

View File

@@ -16,6 +16,7 @@
<input type="hidden" asp-for="Amount" /> <input type="hidden" asp-for="Amount" />
<input type="hidden" asp-for="FeeSatoshiPerByte" /> <input type="hidden" asp-for="FeeSatoshiPerByte" />
<input type="hidden" asp-for="SubstractFees" /> <input type="hidden" asp-for="SubstractFees" />
<input type="hidden" asp-for="NoChange" />
<p> <p>
You can send money received by this store to an address with the help of your Ledger Wallet. <br /> You can send money received by this store to an address with the help of your Ledger Wallet. <br />
If you don't have a Ledger Wallet, use Electrum with your favorite hardware wallet to transfer crypto. <br /> If you don't have a Ledger Wallet, use Electrum with your favorite hardware wallet to transfer crypto. <br />

View File

@@ -3,6 +3,7 @@
var amount = $("#Amount").val(); var amount = $("#Amount").val();
var fee = $("#FeeSatoshiPerByte").val(); var fee = $("#FeeSatoshiPerByte").val();
var substractFee = $("#SubstractFees").val(); var substractFee = $("#SubstractFees").val();
var noChange = $("#NoChange").val();
var loc = window.location, ws_uri; var loc = window.location, ws_uri;
if (loc.protocol === "https:") { if (loc.protocol === "https:") {
@@ -48,8 +49,14 @@
args += "&amount=" + amount; args += "&amount=" + amount;
args += "&feeRate=" + fee; args += "&feeRate=" + fee;
args += "&substractFees=" + substractFee; 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"); var confirmButton = $("#confirm-button");
confirmButton.prop("disabled", true); confirmButton.prop("disabled", true);