diff --git a/BTCPayServer/Controllers/StoresController.Onchain.cs b/BTCPayServer/Controllers/StoresController.Onchain.cs index 25d5647f4..f2808dfd9 100644 --- a/BTCPayServer/Controllers/StoresController.Onchain.cs +++ b/BTCPayServer/Controllers/StoresController.Onchain.cs @@ -11,11 +11,11 @@ using BTCPayServer.Models; using BTCPayServer.Models.StoreViewModels; using BTCPayServer.Payments; using BTCPayServer.Services; -using BTCPayServer.Services.Wallets; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using NBitcoin; +using NBXplorer; using NBXplorer.DerivationStrategy; using NBXplorer.Models; @@ -411,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(); @@ -421,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 ReplaceWallet(string storeId, string cryptoCode) { var checkResult = IsAvailable(cryptoCode, out var store, out var network); if (checkResult != null) @@ -435,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 ? "

Please note that this is a hot wallet!

" : "") + - "

Do not remove the wallet if you have not backed it up!

" + + $"

Please note that this is a {walletType} wallet!

" + + $"

Do not replace the wallet if you have not backed it up{additionalText}.

" + + "

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 .

"; + + 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 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 = + $"

Please note that this is a {walletType} wallet!

" + + $"

Do not remove the wallet if you have not backed it up{additionalText}.

" + "

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.

"; @@ -480,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)) { @@ -529,7 +585,7 @@ namespace BTCPayServer.Controllers return (true, true); var policies = await _settingsRepository.GetSettingAsync(); var hotWallet = policies?.AllowHotWalletForAll is true; - return (hotWallet, hotWallet && policies?.AllowHotWalletRPCImportForAll is true); + return (hotWallet, hotWallet && policies.AllowHotWalletRPCImportForAll is true); } private async Task ReadAllText(IFormFile file) @@ -539,5 +595,11 @@ namespace BTCPayServer.Controllers return await stream.ReadToEndAsync(); } } + + private async Task IsHotWallet(string cryptoCode, DerivationSchemeSettings derivation) + { + return derivation.IsHotWallet && await _ExplorerProvider.GetExplorerClient(cryptoCode) + .GetMetadataAsync(derivation.AccountDerivation, WellknownMetadataKeys.MasterHDKey) != null; + } } } diff --git a/BTCPayServer/Models/StoreViewModels/WalletSetupViewModel.cs b/BTCPayServer/Models/StoreViewModels/WalletSetupViewModel.cs index 82f400771..9803bac02 100644 --- a/BTCPayServer/Models/StoreViewModels/WalletSetupViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/WalletSetupViewModel.cs @@ -20,6 +20,7 @@ namespace BTCPayServer.Models.StoreViewModels public WalletSetupMethod? Method { get; set; } public GenerateWalletRequest SetupRequest { get; set; } public string StoreId { get; set; } + public bool IsHotWallet { get; set; } public string ViewName => Method switch diff --git a/BTCPayServer/Views/Shared/Confirm.cshtml b/BTCPayServer/Views/Shared/Confirm.cshtml index 23aed1ae7..a87dc385e 100644 --- a/BTCPayServer/Views/Shared/Confirm.cshtml +++ b/BTCPayServer/Views/Shared/Confirm.cshtml @@ -31,8 +31,8 @@ @if (!String.IsNullOrEmpty(Model.Action)) { } diff --git a/BTCPayServer/Views/Stores/GenerateWalletOptions.cshtml b/BTCPayServer/Views/Stores/GenerateWalletOptions.cshtml index 4c8d93f3d..8878f338d 100644 --- a/BTCPayServer/Views/Stores/GenerateWalletOptions.cshtml +++ b/BTCPayServer/Views/Stores/GenerateWalletOptions.cshtml @@ -23,8 +23,9 @@

Hot wallet

- Allows spending directly from your BTCPay Server. - Each private key associated with an address generated will be stored as metadata and would be accessible to anyone with admin access to your server. Use at your own risk! + Wallet's private key is stored on the server. + Spending the funds you received is convenient. + To minimize the risk of theft, regularly withdraw funds to a different wallet.

@@ -51,7 +52,10 @@

Watch-only wallet

-

Needs to be imported into an external wallet to spend or provide the seed while spending.

+

+ Wallet's private key is erased from the server. Higher security. + To spend, you have to manually input the private key or import it into an external wallet. +

diff --git a/BTCPayServer/Views/Stores/ModifyWallet.cshtml b/BTCPayServer/Views/Stores/ModifyWallet.cshtml index 4f5c3c003..23c5e70bb 100644 --- a/BTCPayServer/Views/Stores/ModifyWallet.cshtml +++ b/BTCPayServer/Views/Stores/ModifyWallet.cshtml @@ -44,6 +44,10 @@ Source @Model.Source + + Type + @(Model.IsHotWallet ? "Hot wallet" : "Watch-only wallet") + Enabled @@ -57,7 +61,7 @@
- + Replace wallet