diff --git a/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs b/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs index da38b633f..26422aa2d 100644 --- a/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs +++ b/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs @@ -77,7 +77,7 @@ namespace BTCPayServer.Tests // Get enabled state from overview action StoreViewModel storeModel; - response = controller.UpdateStore(); + response = await controller.UpdateStore(); storeModel = (StoreViewModel)Assert.IsType(response).Model; var lnNode = storeModel.LightningNodes.Find(node => node.CryptoCode == cryptoCode); Assert.NotNull(lnNode); @@ -85,11 +85,11 @@ namespace BTCPayServer.Tests WalletSetupViewModel setupVm; var storeId = user.StoreId; - response = await controller.GenerateWallet(storeId, cryptoCode, WalletSetupMethod.GenerateOptions, new GenerateWalletRequest()); + response = await controller.GenerateWallet(storeId, cryptoCode, WalletSetupMethod.GenerateOptions, new WalletSetupRequest()); Assert.IsType(response); // Get enabled state from overview action - response = controller.UpdateStore(); + response = await controller.UpdateStore(); storeModel = (StoreViewModel)Assert.IsType(response).Model; var derivationScheme = storeModel.DerivationSchemes.Find(scheme => scheme.Crypto == cryptoCode); Assert.NotNull(derivationScheme); @@ -98,7 +98,7 @@ namespace BTCPayServer.Tests // Disable wallet response = controller.SetWalletEnabled(storeId, cryptoCode, false).GetAwaiter().GetResult(); Assert.IsType(response); - response = controller.UpdateStore(); + response = await controller.UpdateStore(); storeModel = (StoreViewModel)Assert.IsType(response).Model; derivationScheme = storeModel.DerivationSchemes.Find(scheme => scheme.Crypto == cryptoCode); Assert.NotNull(derivationScheme); diff --git a/BTCPayServer.Tests/CrowdfundTests.cs b/BTCPayServer.Tests/CrowdfundTests.cs index a01f3c9d4..ffea8b55e 100644 --- a/BTCPayServer.Tests/CrowdfundTests.cs +++ b/BTCPayServer.Tests/CrowdfundTests.cs @@ -165,7 +165,7 @@ namespace BTCPayServer.Tests var user = tester.NewAccount(); user.GrantAccess(); user.RegisterDerivationScheme("BTC"); - user.ModifyStore(s => s.NetworkFeeMode = NetworkFeeMode.Never); + await user.ModifyStore(s => s.NetworkFeeMode = NetworkFeeMode.Never); var apps = user.GetController(); var vm = Assert.IsType(Assert.IsType(apps.CreateApp().Result).Model); vm.Name = "test"; diff --git a/BTCPayServer.Tests/PayJoinTests.cs b/BTCPayServer.Tests/PayJoinTests.cs index 99dc61448..ac9ba286c 100644 --- a/BTCPayServer.Tests/PayJoinTests.cs +++ b/BTCPayServer.Tests/PayJoinTests.cs @@ -238,19 +238,15 @@ namespace BTCPayServer.Tests var receiverSeed = s.GenerateWallet("BTC", "", true, true, format); var receiverWalletId = new WalletId(receiver.storeId, "BTC"); - //payjoin is not enabled by default. + //payjoin is enabled by default. var invoiceId = s.CreateInvoice(receiver.storeName); s.GoToInvoiceCheckout(invoiceId); var bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn")) .GetAttribute("href"); - Assert.DoesNotContain($"{PayjoinClient.BIP21EndpointKey}=", bip21); + Assert.Contains($"{PayjoinClient.BIP21EndpointKey}=", bip21); s.GoToHome(); s.GoToStore(receiver.storeId); - //payjoin is not enabled by default. - Assert.False(s.Driver.FindElement(By.Id("PayJoinEnabled")).Selected); - s.Driver.SetCheckbox(By.Id("PayJoinEnabled"), true); - s.Driver.FindElement(By.Id("Save")).Click(); Assert.True(s.Driver.FindElement(By.Id("PayJoinEnabled")).Selected); var sender = s.CreateNewStore(); @@ -519,7 +515,7 @@ namespace BTCPayServer.Tests address = (await nbx.GetUnusedAsync(bob.DerivationScheme, DerivationFeature.Deposit)).Address; tester.ExplorerNode.SendToAddress(address, Money.Coins(1.1m)); await notifications.NextEventAsync(); - bob.ModifyStore(s => s.PayJoinEnabled = true); + await bob.ModifyStore(s => s.PayJoinEnabled = true); var invoice = bob.BitPay.CreateInvoice( new Invoice() { Price = 0.1m, Currency = "BTC", FullNotifications = true }); var invoiceBIP21 = new BitcoinUrlBuilder(invoice.CryptoInfo.First().PaymentUrls.BIP21, diff --git a/BTCPayServer.Tests/TestAccount.cs b/BTCPayServer.Tests/TestAccount.cs index 095919234..19c286143 100644 --- a/BTCPayServer.Tests/TestAccount.cs +++ b/BTCPayServer.Tests/TestAccount.cs @@ -125,28 +125,22 @@ namespace BTCPayServer.Tests CreateStoreAsync().GetAwaiter().GetResult(); } - public void SetNetworkFeeMode(NetworkFeeMode mode) + public async Task SetNetworkFeeMode(NetworkFeeMode mode) { - ModifyStore((store) => + await ModifyStore(store => { store.NetworkFeeMode = mode; }); } - public void ModifyStore(Action modify) + public async Task ModifyStore(Action modify) { var storeController = GetController(); - StoreViewModel store = (StoreViewModel)((ViewResult)storeController.UpdateStore()).Model; + var response = await storeController.UpdateStore(); + StoreViewModel store = (StoreViewModel)((ViewResult)response).Model; modify(store); storeController.UpdateStore(store).GetAwaiter().GetResult(); } - public Task ModifyStoreAsync(Action modify) - { - var storeController = GetController(); - StoreViewModel store = (StoreViewModel)((ViewResult)storeController.UpdateStore()).Model; - modify(store); - return storeController.UpdateStore(store); - } public T GetController(bool setImplicitStore = true) where T : Controller { @@ -181,7 +175,7 @@ namespace BTCPayServer.Tests SupportedNetwork = parent.NetworkProvider.GetNetwork(cryptoCode); var store = parent.PayTester.GetController(UserId, StoreId, true); - var generateRequest = new GenerateWalletRequest() + var generateRequest = new WalletSetupRequest { ScriptPubKeyType = segwit, SavePrivateKeys = importKeysToNBX, @@ -196,7 +190,7 @@ namespace BTCPayServer.Tests public Task EnablePayJoin() { - return ModifyStoreAsync(s => s.PayJoinEnabled = true); + return ModifyStore(s => s.PayJoinEnabled = true); } public GenerateWalletResponse GenerateWalletResponseV { get; set; } diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 580c8a374..409c4c3f9 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -750,7 +750,8 @@ namespace BTCPayServer.Tests // Set tolerance to 50% var stores = user.GetController(); - var vm = Assert.IsType(Assert.IsType(stores.UpdateStore()).Model); + var response = await stores.UpdateStore(); + var vm = Assert.IsType(Assert.IsType(response).Model); Assert.Equal(0.0, vm.PaymentTolerance); vm.PaymentTolerance = 50.0; Assert.IsType(stores.UpdateStore(vm).Result); @@ -941,8 +942,8 @@ namespace BTCPayServer.Tests await user.GrantAccessAsync(true); await user.RegisterDerivationSchemeAsync("BTC"); await user.RegisterLightningNodeAsync("BTC", LightningConnectionType.CLightning); - user.SetNetworkFeeMode(NetworkFeeMode.Never); - await user.ModifyStoreAsync(model => model.SpeedPolicy = SpeedPolicy.HighSpeed); + await user.SetNetworkFeeMode(NetworkFeeMode.Never); + await user.ModifyStore(model => model.SpeedPolicy = SpeedPolicy.HighSpeed); var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(0.0001m, "BTC")); await tester.WaitForEvent(async () => { @@ -990,7 +991,8 @@ namespace BTCPayServer.Tests var user = tester.NewAccount(); user.GrantAccess(true); var storeController = user.GetController(); - Assert.IsType(storeController.UpdateStore()); + var storeResponse = await storeController.UpdateStore(); + Assert.IsType(storeResponse); Assert.IsType(storeController.SetupLightningNode(user.StoreId, "BTC")); var testResult = storeController.SetupLightningNode(user.StoreId, new LightningNodeViewModel @@ -1013,9 +1015,10 @@ namespace BTCPayServer.Tests new LightningNodeViewModel { ConnectionString = tester.MerchantCharge.Client.Uri.AbsoluteUri }, "save", "BTC").GetAwaiter().GetResult()); + storeResponse = await storeController.UpdateStore(); var storeVm = Assert.IsType(Assert - .IsType(storeController.UpdateStore()).Model); + .IsType(storeResponse).Model); Assert.Single(storeVm.LightningNodes.Where(l => !string.IsNullOrEmpty(l.Address))); } } @@ -1128,8 +1131,8 @@ namespace BTCPayServer.Tests var acc = tester.NewAccount(); acc.GrantAccess(); acc.RegisterDerivationScheme("BTC"); - acc.ModifyStore(s => s.SpeedPolicy = SpeedPolicy.LowSpeed); - var invoice = acc.BitPay.CreateInvoice(new Invoice() + await acc.ModifyStore(s => s.SpeedPolicy = SpeedPolicy.LowSpeed); + var invoice = acc.BitPay.CreateInvoice(new Invoice { Price = 5.0m, Currency = "USD", @@ -1545,7 +1548,7 @@ namespace BTCPayServer.Tests var user = tester.NewAccount(); user.GrantAccess(); user.RegisterDerivationScheme("BTC"); - user.SetNetworkFeeMode(NetworkFeeMode.Always); + await user.SetNetworkFeeMode(NetworkFeeMode.Always); var invoice = user.BitPay.CreateInvoice(new Invoice() { Price = 5000.0m, Currency = "USD" }, Facade.Merchant); var payment1 = invoice.BtcDue + Money.Coins(0.0001m); @@ -1646,7 +1649,7 @@ namespace BTCPayServer.Tests Logs.Tester.LogInformation( $"Let's test if we can RBF a normal payment without adding fees to the invoice"); - user.SetNetworkFeeMode(NetworkFeeMode.MultiplePaymentsOnly); + await user.SetNetworkFeeMode(NetworkFeeMode.MultiplePaymentsOnly); invoice = user.BitPay.CreateInvoice(new Invoice() { Price = 5000.0m, Currency = "USD" }, Facade.Merchant); payment1 = invoice.BtcDue; tx1 = new uint256(tester.ExplorerNode.SendCommand("sendtoaddress", new object[] @@ -1952,7 +1955,7 @@ namespace BTCPayServer.Tests }); Assert.Equal(404, (int)response.StatusCode); - user.ModifyStore(s => s.AnyoneCanCreateInvoice = true); + await user.ModifyStore(s => s.AnyoneCanCreateInvoice = true); Logs.Tester.LogInformation("Bad store with anyone can create invoice = 403"); response = await tester.PayTester.HttpClient.SendAsync( @@ -2449,7 +2452,7 @@ namespace BTCPayServer.Tests var user = tester.NewAccount(); user.GrantAccess(); user.RegisterDerivationScheme("BTC"); - user.SetNetworkFeeMode(NetworkFeeMode.Always); + await user.SetNetworkFeeMode(NetworkFeeMode.Always); var invoice = user.BitPay.CreateInvoice( new Invoice() { @@ -2525,7 +2528,7 @@ namespace BTCPayServer.Tests foreach (var networkFeeMode in Enum.GetValues(typeof(NetworkFeeMode)).Cast()) { Logs.Tester.LogInformation($"Trying with {nameof(networkFeeMode)}={networkFeeMode}"); - user.SetNetworkFeeMode(networkFeeMode); + await user.SetNetworkFeeMode(networkFeeMode); var invoice = user.BitPay.CreateInvoice( new Invoice() { @@ -2612,7 +2615,7 @@ namespace BTCPayServer.Tests var user = tester.NewAccount(); user.GrantAccess(); user.RegisterDerivationScheme("BTC"); - user.SetNetworkFeeMode(NetworkFeeMode.Always); + await user.SetNetworkFeeMode(NetworkFeeMode.Always); var invoice = user.BitPay.CreateInvoice( new Invoice() { diff --git a/BTCPayServer/Controllers/StoresController.Onchain.cs b/BTCPayServer/Controllers/StoresController.Onchain.cs index 23d233820..29184d668 100644 --- a/BTCPayServer/Controllers/StoresController.Onchain.cs +++ b/BTCPayServer/Controllers/StoresController.Onchain.cs @@ -62,7 +62,7 @@ namespace BTCPayServer.Controllers } else if (vm.Method == WalletSetupMethod.Seed) { - vm.SetupRequest = new GenerateWalletRequest(); + vm.SetupRequest = new WalletSetupRequest(); } return View(vm.ViewName, vm); @@ -162,6 +162,7 @@ namespace BTCPayServer.Controllers store.SetSupportedPaymentMethod(paymentMethodId, strategy); storeBlob.SetExcluded(paymentMethodId, false); storeBlob.Hints.Wallet = false; + storeBlob.PayJoinEnabled = vm.IsHotWallet && vm.SetupRequest.PayJoinEnabled; store.SetStoreBlob(storeBlob); } catch @@ -169,7 +170,6 @@ namespace BTCPayServer.Controllers ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid derivation scheme"); return View(vm.ViewName, vm); } - await _Repo.UpdateStore(store); _EventAggregator.Publish(new WalletChangedEvent { WalletId = new WalletId(vm.StoreId, vm.CryptoCode) }); @@ -217,14 +217,20 @@ namespace BTCPayServer.Controllers } else { - vm.SetupRequest = new GenerateWalletRequest { SavePrivateKeys = isHotWallet }; + var canUsePayJoin = hotWallet && isHotWallet && network.SupportPayJoin; + vm.SetupRequest = new WalletSetupRequest + { + SavePrivateKeys = isHotWallet, + CanUsePayJoin = canUsePayJoin, + PayJoinEnabled = canUsePayJoin + }; } return View(vm.ViewName, vm); } internal GenerateWalletResponse GenerateWalletResponse; [HttpPost("{storeId}/onchain/{cryptoCode}/generate/{method}")] - public async Task GenerateWallet(string storeId, string cryptoCode, WalletSetupMethod method, GenerateWalletRequest request) + public async Task GenerateWallet(string storeId, string cryptoCode, WalletSetupMethod method, WalletSetupRequest request) { var checkResult = IsAvailable(cryptoCode, out var store, out var network); if (checkResult != null) @@ -240,7 +246,6 @@ namespace BTCPayServer.Controllers var client = _ExplorerProvider.GetExplorerClient(cryptoCode); var isImport = method == WalletSetupMethod.Seed; - var vm = new WalletSetupViewModel { StoreId = storeId, @@ -253,7 +258,7 @@ namespace BTCPayServer.Controllers Source = isImport ? "SeedImported" : "NBXplorerGenerated", IsHotWallet = isImport ? request.SavePrivateKeys : method == WalletSetupMethod.HotWallet, DerivationSchemeFormat = "BTCPay", - CanUseHotWallet = true, + CanUseHotWallet = hotWallet, CanUseRPCImport = rpcImport }; @@ -370,7 +375,6 @@ namespace BTCPayServer.Controllers } var (hotWallet, rpcImport) = await CanUseHotWallet(); - var isHotWallet = await IsHotWallet(vm.CryptoCode, derivation); vm.CanUseHotWallet = hotWallet; vm.CanUseRPCImport = rpcImport; @@ -381,13 +385,13 @@ namespace BTCPayServer.Controllers vm.DerivationScheme = derivation.AccountDerivation.ToString(); vm.KeyPath = derivation.GetSigningAccountKeySettings().AccountKeyPath?.ToString(); vm.Config = ProtectString(derivation.ToJson()); - vm.IsHotWallet = isHotWallet; + vm.IsHotWallet = derivation.IsHotWallet; return View(vm); } [HttpGet("{storeId}/onchain/{cryptoCode}/replace")] - public async Task ReplaceWallet(string storeId, string cryptoCode) + public ActionResult ReplaceWallet(string storeId, string cryptoCode) { var checkResult = IsAvailable(cryptoCode, out var store, out var network); if (checkResult != null) @@ -396,9 +400,8 @@ namespace BTCPayServer.Controllers } var derivation = GetExistingDerivationStrategy(cryptoCode, store); - var isHotWallet = await IsHotWallet(cryptoCode, derivation); - var walletType = isHotWallet ? "hot" : "watch-only"; - var additionalText = isHotWallet + var walletType = derivation.IsHotWallet ? "hot" : "watch-only"; + var additionalText = derivation.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 = @@ -435,7 +438,7 @@ namespace BTCPayServer.Controllers } [HttpGet("{storeId}/onchain/{cryptoCode}/delete")] - public async Task DeleteWallet(string storeId, string cryptoCode) + public ActionResult DeleteWallet(string storeId, string cryptoCode) { var checkResult = IsAvailable(cryptoCode, out var store, out var network); if (checkResult != null) @@ -444,9 +447,8 @@ namespace BTCPayServer.Controllers } var derivation = GetExistingDerivationStrategy(cryptoCode, store); - var isHotWallet = await IsHotWallet(cryptoCode, derivation); - var walletType = isHotWallet ? "hot" : "watch-only"; - var additionalText = isHotWallet + var walletType = derivation.IsHotWallet ? "hot" : "watch-only"; + var additionalText = derivation.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 = @@ -582,11 +584,5 @@ 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/Controllers/StoresController.cs b/BTCPayServer/Controllers/StoresController.cs index 4076049c7..7136e966e 100644 --- a/BTCPayServer/Controllers/StoresController.cs +++ b/BTCPayServer/Controllers/StoresController.cs @@ -466,7 +466,6 @@ namespace BTCPayServer.Controllers } } - if (!ModelState.IsValid) { return View(model); @@ -558,12 +557,9 @@ namespace BTCPayServer.Controllers } } } - - - - [HttpGet] - [Route("{storeId}")] - public IActionResult UpdateStore() + + [HttpGet("{storeId}")] + public async Task UpdateStore() { var store = HttpContext.GetStoreData(); if (store == null) @@ -586,12 +582,17 @@ namespace BTCPayServer.Controllers vm.PayJoinEnabled = storeBlob.PayJoinEnabled; vm.HintWallet = storeBlob.Hints.Wallet; vm.HintLightning = storeBlob.Hints.Lightning; + + (bool canUseHotWallet, _) = await CanUseHotWallet(); + vm.CanUsePayJoin = canUseHotWallet && store + .GetSupportedPaymentMethods(_NetworkProvider) + .OfType() + .Any(settings => settings.Network.SupportPayJoin && settings.IsHotWallet); + return View(vm); } - - - [HttpPost] - [Route("{storeId}")] + + [HttpPost("{storeId}")] public async Task UpdateStore(StoreViewModel model, string command = null) { bool needUpdate = false; @@ -635,11 +636,7 @@ namespace BTCPayServer.Controllers { var problematicPayjoinEnabledMethods = CurrentStore.GetSupportedPaymentMethods(_NetworkProvider) .OfType() - .Where(settings => - settings.Network.SupportPayJoin && - string.IsNullOrEmpty(_ExplorerProvider.GetExplorerClient(settings.Network) - .GetMetadata(settings.AccountDerivation, - WellknownMetadataKeys.Mnemonic))) + .Where(settings => settings.Network.SupportPayJoin && !settings.IsHotWallet) .Select(settings => settings.PaymentId.CryptoCode) .ToArray(); diff --git a/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs b/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs index 75fb019d8..54fe28dd5 100644 --- a/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs @@ -88,6 +88,7 @@ namespace BTCPayServer.Models.StoreViewModels [Display(Name = "Enable Payjoin/P2EP")] public bool PayJoinEnabled { get; set; } + public bool CanUsePayJoin { get; set; } public bool HintWallet { get; set; } public bool HintLightning { get; set; } diff --git a/BTCPayServer/Models/StoreViewModels/WalletSetupRequest.cs b/BTCPayServer/Models/StoreViewModels/WalletSetupRequest.cs new file mode 100644 index 000000000..9d043f1ed --- /dev/null +++ b/BTCPayServer/Models/StoreViewModels/WalletSetupRequest.cs @@ -0,0 +1,10 @@ +using NBXplorer.Models; + +namespace BTCPayServer.Models.StoreViewModels +{ + public class WalletSetupRequest : GenerateWalletRequest + { + public bool PayJoinEnabled { get; set; } + public bool CanUsePayJoin { get; set; } + } +} diff --git a/BTCPayServer/Models/StoreViewModels/WalletSetupViewModel.cs b/BTCPayServer/Models/StoreViewModels/WalletSetupViewModel.cs index 9803bac02..44fe309ae 100644 --- a/BTCPayServer/Models/StoreViewModels/WalletSetupViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/WalletSetupViewModel.cs @@ -1,5 +1,3 @@ -using NBXplorer.Models; - namespace BTCPayServer.Models.StoreViewModels { public enum WalletSetupMethod @@ -18,7 +16,7 @@ namespace BTCPayServer.Models.StoreViewModels public class WalletSetupViewModel : DerivationSchemeViewModel { public WalletSetupMethod? Method { get; set; } - public GenerateWalletRequest SetupRequest { get; set; } + public WalletSetupRequest SetupRequest { get; set; } public string StoreId { get; set; } public bool IsHotWallet { get; set; } diff --git a/BTCPayServer/Views/Stores/UpdateStore.cshtml b/BTCPayServer/Views/Stores/UpdateStore.cshtml index 0e8f88f79..a4b910d4d 100644 --- a/BTCPayServer/Views/Stores/UpdateStore.cshtml +++ b/BTCPayServer/Views/Stores/UpdateStore.cshtml @@ -175,22 +175,27 @@

Payment

-