Disable cold wallet creation by default

This commit is contained in:
nicolas.dorier
2025-03-13 19:23:23 +09:00
parent 517dd7d85b
commit c3c6473e35
12 changed files with 127 additions and 92 deletions

View File

@@ -43,13 +43,13 @@ namespace BTCPayServer.Controllers.Greenfield
} }
var canUseHotWallet = await CanUseHotWallet(); var canUseHotWallet = await CanUseHotWallet();
if (request.SavePrivateKeys && !canUseHotWallet.HotWallet) if (request.SavePrivateKeys && !canUseHotWallet.CanCreateHotWallet)
{ {
ModelState.AddModelError(nameof(request.SavePrivateKeys), ModelState.AddModelError(nameof(request.SavePrivateKeys),
"This instance forbids non-admins from having a hot wallet for your store."); "This instance forbids non-admins from having a hot wallet for your store.");
} }
if (request.ImportKeysToRPC && !canUseHotWallet.RPCImport) if (request.ImportKeysToRPC && !canUseHotWallet.CanRPCImport)
{ {
ModelState.AddModelError(nameof(request.ImportKeysToRPC), ModelState.AddModelError(nameof(request.ImportKeysToRPC),
"This instance forbids non-admins from having importing the wallet addresses/keys to the underlying node."); "This instance forbids non-admins from having importing the wallet addresses/keys to the underlying node.");
@@ -120,7 +120,7 @@ namespace BTCPayServer.Controllers.Greenfield
return Ok(result); return Ok(result);
} }
private async Task<(bool HotWallet, bool RPCImport)> CanUseHotWallet() private async Task<WalletCreationPermissions> CanUseHotWallet()
{ {
return await _authorizationService.CanUseHotWallet(PoliciesSettings, User); return await _authorizationService.CanUseHotWallet(PoliciesSettings, User);
} }

View File

@@ -383,7 +383,7 @@ namespace BTCPayServer.Controllers.Greenfield
} }
//This API is only meant for hot wallet usage for now. We can expand later when we allow PSBT manipulation. //This API is only meant for hot wallet usage for now. We can expand later when we allow PSBT manipulation.
if (!(await CanUseHotWallet()).HotWallet) if (!(await CanUseHotWallet()).CanCreateHotWallet)
{ {
return this.CreateAPIError(503, "not-available", return this.CreateAPIError(503, "not-available",
$"You need to allow non-admins to use hotwallets for their stores (in /server/policies)"); $"You need to allow non-admins to use hotwallets for their stores (in /server/policies)");
@@ -802,7 +802,7 @@ namespace BTCPayServer.Controllers.Greenfield
} }
private async Task<(bool HotWallet, bool RPCImport)> CanUseHotWallet() private async Task<WalletCreationPermissions> CanUseHotWallet()
{ {
return await _authorizationService.CanUseHotWallet(PoliciesSettings, User); return await _authorizationService.CanUseHotWallet(PoliciesSettings, User);
} }

View File

@@ -54,10 +54,9 @@ public partial class UIStoresController
return checkResult; return checkResult;
} }
var (hotWallet, rpcImport) = await CanUseHotWallet(); var perm = await CanUseHotWallet();
vm.Network = network; vm.Network = network;
vm.CanUseHotWallet = hotWallet; vm.SetPermission(perm);
vm.CanUseRPCImport = rpcImport;
vm.SupportTaproot = network.NBitcoinNetwork.Consensus.SupportTaproot; vm.SupportTaproot = network.NBitcoinNetwork.Consensus.SupportTaproot;
vm.SupportSegwit = network.NBitcoinNetwork.Consensus.SupportSegwit; vm.SupportSegwit = network.NBitcoinNetwork.Consensus.SupportSegwit;
@@ -218,14 +217,13 @@ public partial class UIStoresController
} }
var isHotWallet = vm.Method == WalletSetupMethod.HotWallet; var isHotWallet = vm.Method == WalletSetupMethod.HotWallet;
var (hotWallet, rpcImport) = await CanUseHotWallet(); var isColdWallet = vm.Method == WalletSetupMethod.WatchOnly;
if (isHotWallet && !hotWallet) var perm = await CanUseHotWallet();
{ if (isHotWallet && !perm.CanCreateHotWallet)
return NotFound(); return NotFound();
} if (isColdWallet && !perm.CanCreateColdWallet)
return NotFound();
vm.CanUseHotWallet = hotWallet; vm.SetPermission(perm);
vm.CanUseRPCImport = rpcImport;
vm.SupportTaproot = network.NBitcoinNetwork.Consensus.SupportTaproot; vm.SupportTaproot = network.NBitcoinNetwork.Consensus.SupportTaproot;
vm.SupportSegwit = network.NBitcoinNetwork.Consensus.SupportSegwit; vm.SupportSegwit = network.NBitcoinNetwork.Consensus.SupportSegwit;
vm.Network = network; vm.Network = network;
@@ -236,7 +234,7 @@ public partial class UIStoresController
} }
else else
{ {
var canUsePayJoin = hotWallet && isHotWallet && network.SupportPayJoin; var canUsePayJoin = perm.CanCreateHotWallet && isHotWallet && network.SupportPayJoin;
vm.SetupRequest = new WalletSetupRequest vm.SetupRequest = new WalletSetupRequest
{ {
SavePrivateKeys = isHotWallet, SavePrivateKeys = isHotWallet,
@@ -260,8 +258,10 @@ public partial class UIStoresController
return checkResult; return checkResult;
} }
var (hotWallet, rpcImport) = await CanUseHotWallet(); var perm = await CanUseHotWallet();
if (!hotWallet && request.SavePrivateKeys || !rpcImport && request.ImportKeysToRPC) if ((!perm.CanCreateHotWallet && request.SavePrivateKeys) ||
(!perm.CanRPCImport && request.ImportKeysToRPC) ||
(!perm.CanCreateColdWallet && !request.SavePrivateKeys))
{ {
return NotFound(); return NotFound();
} }
@@ -279,12 +279,10 @@ public partial class UIStoresController
Source = isImport ? "SeedImported" : "NBXplorerGenerated", Source = isImport ? "SeedImported" : "NBXplorerGenerated",
IsHotWallet = isImport ? request.SavePrivateKeys : method == WalletSetupMethod.HotWallet, IsHotWallet = isImport ? request.SavePrivateKeys : method == WalletSetupMethod.HotWallet,
DerivationSchemeFormat = "BTCPay", DerivationSchemeFormat = "BTCPay",
CanUseHotWallet = hotWallet,
CanUseRPCImport = rpcImport,
SupportTaproot = network.NBitcoinNetwork.Consensus.SupportTaproot, SupportTaproot = network.NBitcoinNetwork.Consensus.SupportTaproot,
SupportSegwit = network.NBitcoinNetwork.Consensus.SupportSegwit SupportSegwit = network.NBitcoinNetwork.Consensus.SupportSegwit
}; };
vm.SetPermission(perm);
if (isImport && string.IsNullOrEmpty(request.ExistingMnemonic)) if (isImport && string.IsNullOrEmpty(request.ExistingMnemonic))
{ {
ModelState.AddModelError(nameof(request.ExistingMnemonic), StringLocalizer["Please provide your existing seed"]); ModelState.AddModelError(nameof(request.ExistingMnemonic), StringLocalizer["Please provide your existing seed"]);
@@ -404,7 +402,7 @@ public partial class UIStoresController
var storeBlob = store.GetStoreBlob(); var storeBlob = store.GetStoreBlob();
var excludeFilters = storeBlob.GetExcludedPaymentMethods(); var excludeFilters = storeBlob.GetExcludedPaymentMethods();
(bool canUseHotWallet, bool rpcImport) = await CanUseHotWallet(); var perm = await CanUseHotWallet();
var client = _explorerProvider.GetExplorerClient(network); var client = _explorerProvider.GetExplorerClient(network);
var handler = _handlers.GetBitcoinHandler(cryptoCode); var handler = _handlers.GetBitcoinHandler(cryptoCode);
@@ -426,7 +424,7 @@ public partial class UIStoresController
Label = derivation.Label, Label = derivation.Label,
SelectedSigningKey = derivation.SigningKey?.ToString(), SelectedSigningKey = derivation.SigningKey?.ToString(),
NBXSeedAvailable = derivation.IsHotWallet && NBXSeedAvailable = derivation.IsHotWallet &&
canUseHotWallet && perm.CanCreateHotWallet &&
!string.IsNullOrEmpty(await client.GetMetadataAsync<string>(derivation.AccountDerivation, !string.IsNullOrEmpty(await client.GetMetadataAsync<string>(derivation.AccountDerivation,
WellknownMetadataKeys.MasterHDKey)), WellknownMetadataKeys.MasterHDKey)),
AccountKeys = (derivation.AccountKeySettings ?? []) AccountKeys = (derivation.AccountKeySettings ?? [])
@@ -438,9 +436,9 @@ public partial class UIStoresController
}).ToList(), }).ToList(),
Config = ProtectString(JToken.FromObject(derivation, handler.Serializer).ToString()), Config = ProtectString(JToken.FromObject(derivation, handler.Serializer).ToString()),
PayJoinEnabled = storeBlob.PayJoinEnabled, PayJoinEnabled = storeBlob.PayJoinEnabled,
CanUsePayJoin = canUseHotWallet && network.SupportPayJoin && derivation.IsHotWallet, CanUsePayJoin = perm.CanCreateHotWallet && network.SupportPayJoin && derivation.IsHotWallet,
CanUseHotWallet = canUseHotWallet, CanUseHotWallet = perm.CanCreateHotWallet,
CanUseRPCImport = rpcImport, CanUseRPCImport = perm.CanRPCImport,
StoreName = store.StoreName, StoreName = store.StoreName,
CanSetupMultiSig = (derivation.AccountKeySettings ?? []).Length > 1, CanSetupMultiSig = (derivation.AccountKeySettings ?? []).Length > 1,
IsMultiSigOnServer = derivation.IsMultiSigOnServer, IsMultiSigOnServer = derivation.IsMultiSigOnServer,
@@ -589,11 +587,8 @@ public partial class UIStoresController
return NotFound(); return NotFound();
} }
(bool canUseHotWallet, bool _) = await CanUseHotWallet(); if (!(await CanUseHotWallet()).CanCreateHotWallet)
if (!canUseHotWallet)
{
return NotFound(); return NotFound();
}
var client = _explorerProvider.GetExplorerClient(network); var client = _explorerProvider.GetExplorerClient(network);
if (await GetSeed(client, derivation) != null) if (await GetSeed(client, derivation) != null)
@@ -753,7 +748,7 @@ public partial class UIStoresController
!string.IsNullOrEmpty(seed) ? seed : null; !string.IsNullOrEmpty(seed) ? seed : null;
} }
private async Task<(bool HotWallet, bool RPCImport)> CanUseHotWallet() private async Task<WalletCreationPermissions> CanUseHotWallet()
{ {
return await _authorizationService.CanUseHotWallet(_policiesSettings, User); return await _authorizationService.CanUseHotWallet(_policiesSettings, User);
} }

View File

@@ -795,7 +795,7 @@ namespace BTCPayServer.Controllers
private async Task<bool> CanUseHotWallet() private async Task<bool> CanUseHotWallet()
{ {
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>(); var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>();
return (await _authorizationService.CanUseHotWallet(policies, User)).HotWallet; return (await _authorizationService.CanUseHotWallet(policies, User)).CanCreateHotWallet;
} }
[HttpGet("{walletId}/send")] [HttpGet("{walletId}/send")]

View File

@@ -1,3 +1,4 @@
#nullable enable
using System.Security.Claims; using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants; using BTCPayServer.Abstractions.Constants;
@@ -9,6 +10,7 @@ using Microsoft.AspNetCore.Authorization;
namespace BTCPayServer namespace BTCPayServer
{ {
public record WalletCreationPermissions(bool CanCreateHotWallet, bool CanCreateColdWallet, bool CanRPCImport);
public static class AuthorizationExtensions public static class AuthorizationExtensions
{ {
public static async Task<bool> CanModifyStore(this IAuthorizationService authorizationService, ClaimsPrincipal user) public static async Task<bool> CanModifyStore(this IAuthorizationService authorizationService, ClaimsPrincipal user)
@@ -16,24 +18,26 @@ namespace BTCPayServer
return (await authorizationService.AuthorizeAsync(user, null, return (await authorizationService.AuthorizeAsync(user, null,
new PolicyRequirement(Policies.CanModifyStoreSettings))).Succeeded; new PolicyRequirement(Policies.CanModifyStoreSettings))).Succeeded;
} }
public static async Task<(bool HotWallet, bool RPCImport)> CanUseHotWallet( public static async Task<WalletCreationPermissions> CanUseHotWallet(
this IAuthorizationService authorizationService, this IAuthorizationService authorizationService,
PoliciesSettings policiesSettings, PoliciesSettings? policiesSettings,
ClaimsPrincipal user) ClaimsPrincipal user)
{ {
if (!user.Identity.IsAuthenticated) if (user.Identity?.IsAuthenticated is not true)
return (false, false); return new(false, false, false);
var claimUser = user.Identity as ClaimsIdentity; var claimUser = user.Identity as ClaimsIdentity;
if (claimUser is null) if (claimUser is null)
return (false, false); return new(false, false, false);
bool isAdmin = false; bool isAdmin = false;
if (claimUser.AuthenticationType == AuthenticationSchemes.Cookie) if (claimUser.AuthenticationType == AuthenticationSchemes.Cookie)
isAdmin = user.IsInRole(Roles.ServerAdmin); isAdmin = user.IsInRole(Roles.ServerAdmin);
else if (claimUser.AuthenticationType == GreenfieldConstants.AuthenticationType) else if (claimUser.AuthenticationType == GreenfieldConstants.AuthenticationType)
isAdmin = (await authorizationService.AuthorizeAsync(user, Policies.CanModifyServerSettings)).Succeeded; isAdmin = (await authorizationService.AuthorizeAsync(user, Policies.CanModifyServerSettings)).Succeeded;
return isAdmin ? (true, true) : return isAdmin ? new(true, true, true) :
(policiesSettings?.AllowHotWalletForAll is true, policiesSettings?.AllowHotWalletRPCImportForAll is true); new(policiesSettings?.AllowHotWalletForAll is true,
policiesSettings?.AllowCreateColdWalletForAll is true,
policiesSettings?.AllowHotWalletRPCImportForAll is true);
} }
} }
} }

View File

@@ -35,6 +35,8 @@ namespace BTCPayServer.Models.StoreViewModels
public BTCPayNetwork Network { get; set; } public BTCPayNetwork Network { get; set; }
[Display(Name = "Can use hot wallet")] [Display(Name = "Can use hot wallet")]
public bool CanUseHotWallet { get; set; } public bool CanUseHotWallet { get; set; }
[Display(Name = "Can create a new cold wallet")]
public bool CanCreateNewColdWallet { get; set; }
[Display(Name = "Can use RPC import")] [Display(Name = "Can use RPC import")]
public bool CanUseRPCImport { get; set; } public bool CanUseRPCImport { get; set; }
public bool SupportSegwit { get; set; } public bool SupportSegwit { get; set; }

View File

@@ -1,3 +1,6 @@
using System;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
namespace BTCPayServer.Models.StoreViewModels namespace BTCPayServer.Models.StoreViewModels
{ {
public enum WalletSetupMethod public enum WalletSetupMethod
@@ -34,5 +37,21 @@ namespace BTCPayServer.Models.StoreViewModels
WalletSetupMethod.WatchOnly => "GenerateWallet", WalletSetupMethod.WatchOnly => "GenerateWallet",
_ => "SetupWallet" _ => "SetupWallet"
}; };
internal void SetPermission(WalletCreationPermissions perm)
{
this.CanCreateNewColdWallet = perm.CanCreateColdWallet;
this.CanUseHotWallet = perm.CanCreateHotWallet;
this.CanUseRPCImport = perm.CanRPCImport;
}
public void SetViewData(ViewDataDictionary ViewData)
{
ViewData.Add(nameof(CanUseHotWallet), CanUseHotWallet);
ViewData.Add(nameof(CanCreateNewColdWallet), CanCreateNewColdWallet);
ViewData.Add(nameof(CanUseRPCImport), CanUseRPCImport);
ViewData.Add(nameof(SupportSegwit), SupportSegwit);
ViewData.Add(nameof(SupportTaproot), SupportTaproot);
ViewData.Add(nameof(Method), Method);
}
} }
} }

View File

@@ -52,6 +52,8 @@ namespace BTCPayServer.Services
[Display(Name = "Non-admins can create Hot Wallets for their Store")] [Display(Name = "Non-admins can create Hot Wallets for their Store")]
public bool AllowHotWalletForAll { get; set; } public bool AllowHotWalletForAll { get; set; }
[Display(Name = "Non-admins can create Cold Wallets for their Store")]
public bool AllowCreateColdWalletForAll { get; set; }
[Display(Name = "Non-admins can import Hot Wallets for their Store")] [Display(Name = "Non-admins can import Hot Wallets for their Store")]
public bool AllowHotWalletRPCImportForAll { get; set; } public bool AllowHotWalletRPCImportForAll { get; set; }

View File

@@ -120,6 +120,13 @@
</div> </div>
</div> </div>
</div> </div>
<div class="d-flex my-3">
<input asp-for="AllowCreateColdWalletForAll" type="checkbox" class="btcpay-toggle me-3" />
<div>
<label asp-for="AllowCreateColdWalletForAll" class="form-check-label"></label>
<span asp-validation-for="AllowCreateColdWalletForAll" class="text-danger"></span>
</div>
</div>
<div class="d-flex my-3"> <div class="d-flex my-3">
<input asp-for="AllowHotWalletRPCImportForAll" type="checkbox" class="btcpay-toggle me-3"/> <input asp-for="AllowHotWalletRPCImportForAll" type="checkbox" class="btcpay-toggle me-3"/>
<div> <div>

View File

@@ -1,14 +1,10 @@
@model WalletSetupViewModel @model WalletSetupViewModel
@{ @{
var isHotWallet = Model.Method == WalletSetupMethod.HotWallet; var isHotWallet = Model.Method == WalletSetupMethod.HotWallet;
var title = isHotWallet ? StringLocalizer["Create {0} Hot Wallet", Model.CryptoCode] : StringLocalizer["Create {0} Watch-Only Wallet", Model.CryptoCode]; var title = isHotWallet ? StringLocalizer["Create {0} Hot Wallet", Model.CryptoCode] : StringLocalizer["Create {0} Watch-Only Wallet", Model.CryptoCode];
Layout = "_LayoutWalletSetup"; Layout = "_LayoutWalletSetup";
ViewData.SetActivePage(StoreNavPages.OnchainSettings, title, $"{Context.GetStoreData().Id}-{Model.CryptoCode}"); ViewData.SetActivePage(StoreNavPages.OnchainSettings, title, $"{Context.GetStoreData().Id}-{Model.CryptoCode}");
ViewData.Add(nameof(Model.CanUseHotWallet), Model.CanUseHotWallet); Model.SetViewData(ViewData);
ViewData.Add(nameof(Model.CanUseRPCImport), Model.CanUseRPCImport);
ViewData.Add(nameof(Model.SupportSegwit), Model.SupportSegwit);
ViewData.Add(nameof(Model.SupportTaproot), Model.SupportTaproot);
ViewData.Add(nameof(Model.Method), Model.Method);
} }
@section Navbar { @section Navbar {

View File

@@ -8,56 +8,71 @@
} }
@section Navbar { @section Navbar {
<a asp-controller="UIStores" asp-action="SetupWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode"> <a asp-controller="UIStores" asp-action="SetupWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode">
<vc:icon symbol="back" /> <vc:icon symbol="back" />
</a> </a>
} }
<h1 class="text-center" text-translate="true">Choose your wallet option</h1> <h1 class="text-center" text-translate="true">Choose your wallet option</h1>
<div class="list-group mt-5"> <div class="list-group mt-5">
@if (Model.CanUseHotWallet) @if (Model.CanUseHotWallet)
{ {
<a asp-controller="UIStores" asp-action="GenerateWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" asp-route-method="@WalletSetupMethod.HotWallet.ToString()" id="GenerateHotwalletLink" class="list-group-item list-group-item-action"> <a asp-controller="UIStores" asp-action="GenerateWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" asp-route-method="@WalletSetupMethod.HotWallet.ToString()" id="GenerateHotwalletLink" class="list-group-item list-group-item-action">
<div class="image"> <div class="image">
<vc:icon symbol="wallet-hot"/> <vc:icon symbol="wallet-hot"/>
</div> </div>
<div class="content"> <div class="content">
<h4 text-translate="true">Hot wallet</h4> <h4 text-translate="true">Hot wallet</h4>
<p class="mb-0 text-secondary" text-translate="true"> <p class="mb-0 text-secondary" text-translate="true">
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. 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.
</p> </p>
</div> </div>
<vc:icon symbol="caret-right"/> <vc:icon symbol="caret-right"/>
</a> </a>
} }
else else
{ {
<div class="list-group-item text-muted"> <div class="list-group-item text-muted">
<div class="image"> <div class="image">
<vc:icon symbol="wallet-hot"/> <vc:icon symbol="wallet-hot"/>
</div> </div>
<div class="content"> <div class="content">
<h4 text-translate="true">Hot wallet</h4> <h4 text-translate="true">Hot wallet</h4>
<p class="mb-0" text-translate="true">Please note that creating a hot wallet is not supported by this instance for non administrators.</p> <p class="mb-0" text-translate="true">Please note that creating a hot wallet is not supported by this instance for non administrators.</p>
</div> </div>
</div> </div>
} }
</div> </div>
<div class="list-group mt-4"> <div class="list-group mt-4">
<a asp-controller="UIStores" asp-action="GenerateWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" asp-route-method="@WalletSetupMethod.WatchOnly.ToString()" id="GenerateWatchonlyLink" class="list-group-item list-group-item-action"> @if (Model.CanCreateNewColdWallet)
<div class="image"> {
<vc:icon symbol="wallet-watchonly"/> <a asp-controller="UIStores" asp-action="GenerateWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" asp-route-method="@WalletSetupMethod.WatchOnly.ToString()" id="GenerateWatchonlyLink" class="list-group-item list-group-item-action">
</div> <div class="image">
<div class="content"> <vc:icon symbol="wallet-watchonly"/>
<h4 text-translate="true">Watch-only wallet</h4> </div>
<p class="mb-0 text-secondary" text-translate="true"> <div class="content">
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. <h4 text-translate="true">Watch-only wallet</h4>
</p> <p class="mb-0 text-secondary" text-translate="true">
</div> 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.
<vc:icon symbol="caret-right" /> </p>
</a> </div>
<vc:icon symbol="caret-right" />
</a>
}
else
{
<div class="list-group-item text-muted">
<div class="image">
<vc:icon symbol="wallet-watchonly" />
</div>
<div class="content">
<h4 text-translate="true">Watch-only wallet</h4>
<p class="mb-0" text-translate="true">Please note that this instance does not support creating a new cold wallet for non-administrators. However, you can import one from other wallet software.</p>
</div>
</div>
}
</div> </div>

View File

@@ -18,12 +18,7 @@
<div class="my-5"> <div class="my-5">
@if (Model.CanUseHotWallet) @if (Model.CanUseHotWallet)
{ {
ViewData.Add(nameof(Model.CanUseHotWallet), Model.CanUseHotWallet); Model.SetViewData(ViewData);
ViewData.Add(nameof(Model.CanUseRPCImport), Model.CanUseRPCImport);
ViewData.Add(nameof(Model.SupportSegwit), Model.SupportSegwit);
ViewData.Add(nameof(Model.SupportTaproot), Model.SupportTaproot);
ViewData.Add(nameof(Model.Method), Model.Method);
<partial name="_GenerateWalletForm" model="Model.SetupRequest" /> <partial name="_GenerateWalletForm" model="Model.SetupRequest" />
} }
else else