Merge pull request #2296 from dennisreimann/wallet-setup-finetuning

Wallet setup finetuning
This commit is contained in:
Nicolas Dorier
2021-03-06 13:51:01 +09:00
committed by GitHub
19 changed files with 309 additions and 1086 deletions

View File

@@ -1,400 +0,0 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
namespace BTCPayServer.Controllers
{
public partial class StoresController
{
[HttpGet]
[Route("{storeId}/derivations/{cryptoCode}")]
public async Task<IActionResult> AddDerivationScheme(string storeId, string cryptoCode)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
var network = cryptoCode == null ? null : _ExplorerProvider.GetNetwork(cryptoCode);
if (network == null)
{
return NotFound();
}
DerivationSchemeViewModel vm = new DerivationSchemeViewModel();
vm.CryptoCode = cryptoCode;
vm.RootKeyPath = network.GetRootKeyPath();
vm.Network = network;
var derivation = GetExistingDerivationStrategy(vm.CryptoCode, store);
if (derivation != null)
{
vm.DerivationScheme = derivation.AccountDerivation.ToString();
vm.Config = derivation.ToJson();
}
vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.BTCLike));
var hotWallet = await CanUseHotWallet();
vm.CanUseHotWallet = hotWallet.HotWallet;
vm.CanUseRPCImport = hotWallet.RPCImport;
return View(vm);
}
private DerivationSchemeSettings GetExistingDerivationStrategy(string cryptoCode, StoreData store)
{
var id = new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike);
var existing = store.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationSchemeSettings>()
.FirstOrDefault(d => d.PaymentId == id);
return existing;
}
[HttpPost]
[Route("{storeId}/derivations/{cryptoCode}")]
[ApiExplorerSettings(IgnoreApi = true)]
public async Task<IActionResult> AddDerivationScheme(string storeId, [FromForm] DerivationSchemeViewModel vm,
string cryptoCode)
{
vm.CryptoCode = cryptoCode;
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
var network = cryptoCode == null ? null : _ExplorerProvider.GetNetwork(cryptoCode);
if (network == null)
{
return NotFound();
}
vm.Network = network;
vm.RootKeyPath = network.GetRootKeyPath();
DerivationSchemeSettings strategy = null;
var wallet = _WalletProvider.GetWallet(network);
if (wallet == null)
{
return NotFound();
}
if (!string.IsNullOrEmpty(vm.Config))
{
if (!DerivationSchemeSettings.TryParseFromJson(vm.Config, network, out strategy))
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = "Config file was not in the correct format"
});
vm.Confirmation = false;
return View(nameof(AddDerivationScheme), vm);
}
}
if (vm.WalletFile != null)
{
if (!DerivationSchemeSettings.TryParseFromWalletFile(await ReadAllText(vm.WalletFile), network,
out strategy))
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = "Wallet file was not in the correct format"
});
vm.Confirmation = false;
return View(nameof(AddDerivationScheme), vm);
}
}
else if (!string.IsNullOrEmpty(vm.WalletFileContent))
{
if (!DerivationSchemeSettings.TryParseFromWalletFile(vm.WalletFileContent, network, out strategy))
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = "QR import was not in the correct format"
});
vm.Confirmation = false;
return View(nameof(AddDerivationScheme), vm);
}
}
else
{
try
{
if (!string.IsNullOrEmpty(vm.DerivationScheme))
{
var newStrategy = ParseDerivationStrategy(vm.DerivationScheme, null, network);
if (newStrategy.AccountDerivation != strategy?.AccountDerivation)
{
var accountKey = string.IsNullOrEmpty(vm.AccountKey)
? null
: new BitcoinExtPubKey(vm.AccountKey, network.NBitcoinNetwork);
if (accountKey != null)
{
var accountSettings =
newStrategy.AccountKeySettings.FirstOrDefault(a => a.AccountKey == accountKey);
if (accountSettings != null)
{
accountSettings.AccountKeyPath =
vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath);
accountSettings.RootFingerprint = string.IsNullOrEmpty(vm.RootFingerprint)
? (HDFingerprint?)null
: new HDFingerprint(
NBitcoin.DataEncoders.Encoders.Hex.DecodeData(vm.RootFingerprint));
}
}
strategy = newStrategy;
strategy.Source = vm.Source;
vm.DerivationScheme = strategy.AccountDerivation.ToString();
}
}
else
{
strategy = null;
}
}
catch
{
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
vm.Confirmation = false;
return View(nameof(AddDerivationScheme), vm);
}
}
var oldConfig = vm.Config;
vm.Config = strategy == null ? null : strategy.ToJson();
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
var exisingStrategy = store.GetSupportedPaymentMethods(_NetworkProvider)
.Where(c => c.PaymentId == paymentMethodId)
.OfType<DerivationSchemeSettings>()
.FirstOrDefault();
var storeBlob = store.GetStoreBlob();
var wasExcluded = storeBlob.GetExcludedPaymentMethods().Match(paymentMethodId);
var willBeExcluded = !vm.Enabled;
var showAddress = // Show addresses if:
// - If the user is testing the hint address in confirmation screen
(vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress)) ||
// - The user is clicking on continue after changing the config
(!vm.Confirmation && oldConfig != vm.Config) ||
// - The user is clicking on continue without changing config nor enabling/disabling
(!vm.Confirmation && oldConfig == vm.Config && willBeExcluded == wasExcluded);
showAddress = showAddress && strategy != null;
if (!showAddress)
{
try
{
if (strategy != null)
await wallet.TrackAsync(strategy.AccountDerivation);
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
storeBlob.SetExcluded(paymentMethodId, willBeExcluded);
storeBlob.Hints.Wallet = false;
store.SetStoreBlob(storeBlob);
}
catch
{
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
return View(vm);
}
await _Repo.UpdateStore(store);
_EventAggregator.Publish(new WalletChangedEvent() {WalletId = new WalletId(storeId, cryptoCode)});
if (willBeExcluded != wasExcluded)
{
var label = willBeExcluded ? "disabled" : "enabled";
TempData[WellKnownTempData.SuccessMessage] =
$"On-Chain payments for {network.CryptoCode} has been {label}.";
}
else
{
TempData[WellKnownTempData.SuccessMessage] =
$"Derivation settings for {network.CryptoCode} has been modified.";
}
// This is success case when derivation scheme is added to the store
return RedirectToAction(nameof(UpdateStore), new {storeId = storeId});
}
else if (!string.IsNullOrEmpty(vm.HintAddress))
{
BitcoinAddress address = null;
try
{
address = BitcoinAddress.Create(vm.HintAddress, network.NBitcoinNetwork);
}
catch
{
ModelState.AddModelError(nameof(vm.HintAddress), "Invalid hint address");
return ShowAddresses(vm, strategy);
}
try
{
var newStrategy = ParseDerivationStrategy(vm.DerivationScheme, address.ScriptPubKey, network);
if (newStrategy.AccountDerivation != strategy.AccountDerivation)
{
strategy.AccountDerivation = newStrategy.AccountDerivation;
strategy.AccountOriginal = null;
}
}
catch
{
ModelState.AddModelError(nameof(vm.HintAddress), "Impossible to find a match with this address");
return ShowAddresses(vm, strategy);
}
vm.HintAddress = "";
TempData[WellKnownTempData.SuccessMessage] =
"Address successfully found, please verify that the rest is correct and click on \"Confirm\"";
ModelState.Remove(nameof(vm.HintAddress));
ModelState.Remove(nameof(vm.DerivationScheme));
}
return ShowAddresses(vm, strategy);
}
[HttpPost]
[Route("{storeId}/derivations/{cryptoCode}/generatenbxwallet")]
public async Task<IActionResult> GenerateNBXWallet(string storeId, string cryptoCode,
GenerateWalletRequest request)
{
var hotWallet = await CanUseHotWallet();
if (!hotWallet.HotWallet || (!hotWallet.RPCImport && request.ImportKeysToRPC))
{
return NotFound();
}
var network = _NetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var client = _ExplorerProvider.GetExplorerClient(cryptoCode);
GenerateWalletResponse response;
try
{
response = await client.GenerateWalletAsync(request);
}
catch (Exception e)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Html = $"There was an error generating your wallet: {e.Message}"
});
return RedirectToAction(nameof(AddDerivationScheme), new {storeId, cryptoCode});
}
if (response == null)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Html = "There was an error generating your wallet. Is your node available?"
});
return RedirectToAction(nameof(AddDerivationScheme), new {storeId, cryptoCode});
}
var store = HttpContext.GetStoreData();
var result = await AddDerivationScheme(storeId,
new DerivationSchemeViewModel()
{
Confirmation = string.IsNullOrEmpty(request.ExistingMnemonic),
Network = network,
RootFingerprint = response.AccountKeyPath.MasterFingerprint.ToString(),
RootKeyPath = network.GetRootKeyPath(),
CryptoCode = cryptoCode,
DerivationScheme = response.DerivationScheme.ToString(),
Source = "NBXplorer",
AccountKey = response.AccountHDKey.Neuter().ToWif(),
DerivationSchemeFormat = "BTCPay",
KeyPath = response.AccountKeyPath.KeyPath.ToString(),
Enabled = !store.GetStoreBlob()
.IsExcluded(new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike))
}, cryptoCode);
if (!ModelState.IsValid || !(result is RedirectToActionResult))
return result;
TempData.Clear();
if (string.IsNullOrEmpty(request.ExistingMnemonic))
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Success,
Html = $"<span class='text-centered'>Your wallet has been generated.</span>"
});
var vm = new RecoverySeedBackupViewModel()
{
CryptoCode = cryptoCode,
Mnemonic = response.Mnemonic,
Passphrase = response.Passphrase,
IsStored = request.SavePrivateKeys,
ReturnUrl = Url.Action(nameof(UpdateStore), new {storeId})
};
return this.RedirectToRecoverySeedBackup(vm);
}
else
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Warning,
Html = "Please check your addresses and confirm"
});
}
return result;
}
private async Task<(bool HotWallet, bool RPCImport)> CanUseHotWallet()
{
var isAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings))
.Succeeded;
if (isAdmin)
return (true, true);
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>();
var hotWallet = policies?.AllowHotWalletForAll is true;
return (hotWallet, hotWallet && policies?.AllowHotWalletRPCImportForAll is true);
}
private async Task<string> ReadAllText(IFormFile file)
{
using (var stream = new StreamReader(file.OpenReadStream()))
{
return await stream.ReadToEndAsync();
}
}
private IActionResult
ShowAddresses(DerivationSchemeViewModel vm, DerivationSchemeSettings strategy)
{
vm.DerivationScheme = strategy.AccountDerivation.ToString();
var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
if (!string.IsNullOrEmpty(vm.DerivationScheme))
{
var line = strategy.AccountDerivation.GetLineFor(deposit);
for (int i = 0; i < 10; i++)
{
var keyPath = deposit.GetKeyPath((uint)i);
var rootedKeyPath = vm.GetAccountKeypath()?.Derive(keyPath);
var derivation = line.Derive((uint)i);
var address = strategy.Network.NBXplorerNetwork.CreateAddress(strategy.AccountDerivation,
line.KeyPathTemplate.GetKeyPath((uint)i),
derivation.ScriptPubKey).ToString();
vm.AddressSamples.Add((keyPath.ToString(), address, rootedKeyPath));
}
}
vm.Confirmation = true;
ModelState.Remove(nameof(vm.Config)); // Remove the cached value
return View(nameof(AddDerivationScheme), vm);
}
}
}

View File

@@ -1,16 +1,21 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBXplorer;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
@@ -105,42 +110,34 @@ namespace BTCPayServer.Controllers
return View(vm.ViewName, vm);
}
}
else
else if (!string.IsNullOrEmpty(vm.DerivationScheme))
{
try
{
if (!string.IsNullOrEmpty(vm.DerivationScheme))
var newStrategy = ParseDerivationStrategy(vm.DerivationScheme, null, network);
if (newStrategy.AccountDerivation != strategy?.AccountDerivation)
{
var newStrategy = ParseDerivationStrategy(vm.DerivationScheme, null, network);
if (newStrategy.AccountDerivation != strategy?.AccountDerivation)
var accountKey = string.IsNullOrEmpty(vm.AccountKey)
? null
: new BitcoinExtPubKey(vm.AccountKey, network.NBitcoinNetwork);
if (accountKey != null)
{
var accountKey = string.IsNullOrEmpty(vm.AccountKey)
? null
: new BitcoinExtPubKey(vm.AccountKey, network.NBitcoinNetwork);
if (accountKey != null)
var accountSettings =
newStrategy.AccountKeySettings.FirstOrDefault(a => a.AccountKey == accountKey);
if (accountSettings != null)
{
var accountSettings =
newStrategy.AccountKeySettings.FirstOrDefault(a => a.AccountKey == accountKey);
if (accountSettings != null)
{
accountSettings.AccountKeyPath =
vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath);
accountSettings.RootFingerprint = string.IsNullOrEmpty(vm.RootFingerprint)
? (HDFingerprint?)null
: new HDFingerprint(
NBitcoin.DataEncoders.Encoders.Hex.DecodeData(vm.RootFingerprint));
}
accountSettings.AccountKeyPath =
vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath);
accountSettings.RootFingerprint = string.IsNullOrEmpty(vm.RootFingerprint)
? (HDFingerprint?)null
: new HDFingerprint(
NBitcoin.DataEncoders.Encoders.Hex.DecodeData(vm.RootFingerprint));
}
strategy = newStrategy;
strategy.Source = vm.Source;
vm.DerivationScheme = strategy.AccountDerivation.ToString();
}
}
else
{
ModelState.AddModelError(nameof(vm.DerivationScheme), "Please provide your extended public key");
return View(vm.ViewName, vm);
strategy = newStrategy;
strategy.Source = vm.Source;
vm.DerivationScheme = strategy.AccountDerivation.ToString();
}
}
catch
@@ -149,6 +146,11 @@ namespace BTCPayServer.Controllers
return View(vm.ViewName, vm);
}
}
else
{
ModelState.AddModelError(nameof(vm.DerivationScheme), "Please provide your extended public key");
return View(vm.ViewName, vm);
}
var oldConfig = vm.Config;
vm.Config = strategy?.ToJson();
@@ -409,6 +411,8 @@ namespace BTCPayServer.Controllers
}
var (hotWallet, rpcImport) = await CanUseHotWallet();
var isHotWallet = await IsHotWallet(vm.CryptoCode, derivation);
vm.CanUseHotWallet = hotWallet;
vm.CanUseRPCImport = rpcImport;
vm.RootKeyPath = network.GetRootKeyPath();
@@ -419,12 +423,13 @@ namespace BTCPayServer.Controllers
vm.KeyPath = derivation.GetSigningAccountKeySettings().AccountKeyPath?.ToString();
vm.Config = derivation.ToJson();
vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.BTCLike));
vm.IsHotWallet = isHotWallet;
return View(vm);
}
[HttpGet("{storeId}/onchain/{cryptoCode}/delete")]
public IActionResult DeleteWallet(string storeId, string cryptoCode)
[HttpGet("{storeId}/onchain/{cryptoCode}/replace")]
public async Task<IActionResult> ReplaceWallet(string storeId, string cryptoCode)
{
var checkResult = IsAvailable(cryptoCode, out var store, out var network);
if (checkResult != null)
@@ -433,9 +438,62 @@ namespace BTCPayServer.Controllers
}
var derivation = GetExistingDerivationStrategy(cryptoCode, store);
var isHotWallet = await IsHotWallet(cryptoCode, derivation);
var walletType = isHotWallet ? "hot" : "watch-only";
var additionalText = isHotWallet
? ""
: " or imported into an external wallet. If you no longer have access to your private key (recovery seed), immediately replace the wallet";
var description =
(derivation.IsHotWallet ? "<p class=\"text-danger font-weight-bold\">Please note that this is a hot wallet!</p> " : "") +
"<p class=\"text-danger font-weight-bold\">Do not remove the wallet if you have not backed it up!</p>" +
$"<p class=\"text-danger font-weight-bold\">Please note that this is a {walletType} wallet!</p>" +
$"<p class=\"text-danger font-weight-bold\">Do not replace the wallet if you have not backed it up{additionalText}.</p>" +
"<p class=\"text-left mb-0\">Replacing the wallet will erase the current wallet data from the server. " +
"The current wallet will be replaced once you finish the setup of the new wallet. If you cancel the setup, the current wallet will stay active .</p>";
return View("Confirm", new ConfirmModel
{
Title = $"Replace {network.CryptoCode} wallet",
Description = description,
DescriptionHtml = true,
Action = "Setup new wallet"
});
}
[HttpPost("{storeId}/onchain/{cryptoCode}/replace")]
public IActionResult ConfirmReplaceWallet(string storeId, string cryptoCode)
{
var checkResult = IsAvailable(cryptoCode, out var store, out _);
if (checkResult != null)
{
return checkResult;
}
var derivation = GetExistingDerivationStrategy(cryptoCode, store);
if (derivation == null)
{
return NotFound();
}
return RedirectToAction(nameof(SetupWallet), new {storeId, cryptoCode});
}
[HttpGet("{storeId}/onchain/{cryptoCode}/delete")]
public async Task<IActionResult> DeleteWallet(string storeId, string cryptoCode)
{
var checkResult = IsAvailable(cryptoCode, out var store, out var network);
if (checkResult != null)
{
return checkResult;
}
var derivation = GetExistingDerivationStrategy(cryptoCode, store);
var isHotWallet = await IsHotWallet(cryptoCode, derivation);
var walletType = isHotWallet ? "hot" : "watch-only";
var additionalText = isHotWallet
? ""
: " or imported into an external wallet. If you no longer have access to your private key (recovery seed), immediately replace the wallet";
var description =
$"<p class=\"text-danger font-weight-bold\">Please note that this is a {walletType} wallet!</p>" +
$"<p class=\"text-danger font-weight-bold\">Do not remove the wallet if you have not backed it up{additionalText}.</p>" +
"<p class=\"text-left mb-0\">Removing the wallet will erase the wallet data from the server. " +
$"The store won't be able to receive {network.CryptoCode} onchain payments until a new wallet is set up.</p>";
@@ -478,7 +536,7 @@ namespace BTCPayServer.Controllers
private IActionResult ConfirmAddresses(WalletSetupViewModel vm, DerivationSchemeSettings strategy)
{
vm.DerivationScheme = strategy.AccountDerivation.ToString();
var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
var deposit = new KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
if (!string.IsNullOrEmpty(vm.DerivationScheme))
{
@@ -509,5 +567,39 @@ namespace BTCPayServer.Controllers
return store == null || network == null ? NotFound() : null;
}
private DerivationSchemeSettings GetExistingDerivationStrategy(string cryptoCode, StoreData store)
{
var id = new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike);
var existing = store.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationSchemeSettings>()
.FirstOrDefault(d => d.PaymentId == id);
return existing;
}
private async Task<(bool HotWallet, bool RPCImport)> CanUseHotWallet()
{
var isAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings))
.Succeeded;
if (isAdmin)
return (true, true);
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>();
var hotWallet = policies?.AllowHotWalletForAll is true;
return (hotWallet, hotWallet && policies.AllowHotWalletRPCImportForAll is true);
}
private async Task<string> ReadAllText(IFormFile file)
{
using (var stream = new StreamReader(file.OpenReadStream()))
{
return await stream.ReadToEndAsync();
}
}
private async Task<bool> IsHotWallet(string cryptoCode, DerivationSchemeSettings derivation)
{
return derivation.IsHotWallet && await _ExplorerProvider.GetExplorerClient(cryptoCode)
.GetMetadataAsync<string>(derivation.AccountDerivation, WellknownMetadataKeys.MasterHDKey) != null;
}
}
}