diff --git a/BTCPayServer.Tests/Extensions.cs b/BTCPayServer.Tests/Extensions.cs index 9452cbe79..b0dc8ee02 100644 --- a/BTCPayServer.Tests/Extensions.cs +++ b/BTCPayServer.Tests/Extensions.cs @@ -1,11 +1,9 @@ using System; using System.Diagnostics; using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; using BTCPayServer.Services.Wallets; -using BTCPayServer.Tests.Logging; using Microsoft.AspNetCore.Mvc; using Microsoft.Playwright; using NBXplorer.DerivationStrategy; diff --git a/BTCPayServer.Tests/PlaywrightTester.cs b/BTCPayServer.Tests/PlaywrightTester.cs index 1d84ed129..a4056e9b1 100644 --- a/BTCPayServer.Tests/PlaywrightTester.cs +++ b/BTCPayServer.Tests/PlaywrightTester.cs @@ -50,7 +50,7 @@ namespace BTCPayServer.Tests Browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions { Headless = Server.PayTester.InContainer, - SlowMo = Server.PayTester.InContainer ? 0 : 50, // Add slight delay, nicer during dev + SlowMo = 0 // 50 if you want to slow down }); var context = await Browser.NewContextAsync(); Page = await context.NewPageAsync(); @@ -230,8 +230,9 @@ namespace BTCPayServer.Tests var isImport = !string.IsNullOrEmpty(seed); await GoToWalletSettings(cryptoCode); // Replace previous wallet case - if (await Page.Locator("#ChangeWalletLink").IsVisibleAsync()) + if (await Page.Locator("#ActionsDropdownToggle").IsVisibleAsync()) { + TestLogs.LogInformation($"Replacing the wallet"); await Page.ClickAsync("#ActionsDropdownToggle"); await Page.ClickAsync("#ChangeWalletLink"); await Page.FillAsync("#ConfirmInput", "REPLACE"); @@ -624,24 +625,25 @@ namespace BTCPayServer.Tests { await GoToWallet(walletId, navPages: WalletsNavPages.Transactions); await Page.Locator("#WalletTransactions[data-loaded='true']").WaitForAsync(new() { State = WaitForSelectorState.Visible }); - return new WalletTransactionsPMO(Page); + return new WalletTransactionsPMO(this); } #nullable enable - public class WalletTransactionsPMO(IPage page) + public class WalletTransactionsPMO(PlaywrightTester tester) { - public Task SelectAll() => page.SetCheckedAsync(".mass-action-select-all", true); + private IPage Page => tester.Page; + public Task SelectAll() => Page.SetCheckedAsync(".mass-action-select-all", true); public async Task Select(params uint256[] txs) { foreach (var txId in txs) { - await page.SetCheckedAsync($"{TxRowSelector(txId)} .mass-action-select", true); + await Page.SetCheckedAsync($"{TxRowSelector(txId)} .mass-action-select", true); } } - public Task BumpFeeSelected() => page.ClickAsync("#BumpFee"); + public Task BumpFeeSelected() => Page.ClickAsync("#BumpFee"); - public Task BumpFee(uint256? txId = null) => page.ClickAsync($"{TxRowSelector(txId)} .bumpFee-btn"); + public Task BumpFee(uint256? txId = null) => Page.ClickAsync($"{TxRowSelector(txId)} .bumpFee-btn"); static string TxRowSelector(uint256? txId = null) => txId is null ? ".transaction-row:first-of-type" : $".transaction-row[data-value=\"{txId}\"]"; public Task AssertHasLabels(string label) => AssertHasLabels(null, label); @@ -653,23 +655,32 @@ namespace BTCPayServer.Tests retry: await WaitTransactionsLoaded(); var selector = $"{TxRowSelector(txId)} .transaction-label[data-value=\"{label}\"]"; - if (await page.Locator(selector).IsVisibleAsync()) + if (await Page.Locator(selector).IsVisibleAsync()) return; if (tried > 5) { - await page.Locator(selector).WaitForAsync(); + try + { + await Page.Locator(selector).WaitForAsync(); + } + catch + { + await tester.TakeScreenshot("AssertHasLabels.png"); + throw; + } + return; } tried++; - await page.ReloadAsync(); + await Page.ReloadAsync(); goto retry; } - public Task WaitTransactionsLoaded() => page.Locator("#WalletTransactions[data-loaded='true']").WaitForAsync(); + public Task WaitTransactionsLoaded() => Page.Locator("#WalletTransactions[data-loaded='true']").WaitForAsync(); public async Task AssertNotFound(uint256 txId) { - Assert.False(await page.Locator(TxRowSelector(txId)).IsVisibleAsync()); + Assert.False(await Page.Locator(TxRowSelector(txId)).IsVisibleAsync()); } } @@ -690,6 +701,8 @@ namespace BTCPayServer.Tests public Task Sign() => page.ClickAsync("#SignTransaction"); public Task SetFeeRate(decimal val) => page.FillAsync("[name=\"FeeSatoshiPerByte\"]", val.ToString(CultureInfo.InvariantCulture)); + + public Task FillAmount(decimal amount) => page.FillAsync("[name='Outputs[0].Amount']", amount.ToString(CultureInfo.InvariantCulture)); } public async Task MarkAsSettled() @@ -698,5 +711,22 @@ namespace BTCPayServer.Tests var client = await this.AsTestAccount().CreateClient(); await client.MarkInvoiceStatus(StoreId, txId, new() { Status = InvoiceStatus.Settled }); } + + public WalletTransactionsPMO InWalletTransactions() => new WalletTransactionsPMO(this); + + public WalletBroadcastPMO InBroadcast() => new WalletBroadcastPMO(Page); + + public class WalletBroadcastPMO(IPage page) + { + public async Task AssertSending(BitcoinAddress destination, decimal amount) + { + await page.WaitForSelectorAsync($"td:text('{destination}')"); + var amt = await page.WaitForSelectorAsync($"td:text('{destination}') >> .. >> :nth-child(3)"); + var actual = (await amt!.TextContentAsync()).NormalizeWhitespaces(); + var expected = ("-" + Money.Coins(amount).ToString() + " " + "BTC").NormalizeWhitespaces(); + Assert.Equal(expected, actual); + } + public async Task Broadcast() => await page.ClickAsync("#BroadcastTransaction"); + } } } diff --git a/BTCPayServer.Tests/PlaywrightTests.cs b/BTCPayServer.Tests/PlaywrightTests.cs index f69034c65..44fc4cb93 100644 --- a/BTCPayServer.Tests/PlaywrightTests.cs +++ b/BTCPayServer.Tests/PlaywrightTests.cs @@ -1,18 +1,25 @@ using System; +using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net; using System.Threading.Tasks; using BTCPayServer.Abstractions.Models; using BTCPayServer.Client.Models; using BTCPayServer.Data; +using BTCPayServer.Payments; using BTCPayServer.Services; using BTCPayServer.Services.Invoices; +using BTCPayServer.Services.Rates; using BTCPayServer.Views.Manage; using BTCPayServer.Views.Server; using BTCPayServer.Views.Stores; +using BTCPayServer.Views.Wallets; using Microsoft.AspNetCore.Identity; using Microsoft.Playwright; using NBitcoin; +using NBitcoin.Payment; +using NBXplorer.Models; using Newtonsoft.Json.Linq; using Xunit; using Xunit.Abstractions; @@ -55,10 +62,10 @@ namespace BTCPayServer.Tests await s.Page.SelectOptionAsync("#FormId", "Email"); await s.ClickPagePrimary(); await s.FindAlertMessage(partialText: "App updated"); + var opening = s.Page.Context.WaitForPageAsync(); await s.Page.ClickAsync("#ViewApp"); - var popOutPage = await s.Page.Context.WaitForPageAsync(); string invoiceId; - await using (var o = await s.SwitchPage(popOutPage)) + await using (_ = await s.SwitchPage(opening)) { await s.Page.Locator("button[type='submit']").First.ClickAsync(); await s.Page.FillAsync("[name='buyerEmail']", "aa@aa.com"); @@ -79,8 +86,9 @@ namespace BTCPayServer.Tests await s.ClickPagePrimary(); await s.Page.Locator("a[id^='Edit-']").First.ClickAsync(); var editUrl = new Uri(s.Page.Url); + opening = s.Page.Context.WaitForPageAsync(); await s.Page.ClickAsync("#ViewPaymentRequest"); - popOutPage = await s.Page.Context.WaitForPageAsync(); + var popOutPage = await opening; await popOutPage.ClickAsync("[data-test='form-button']"); Assert.Contains("Enter your email", await popOutPage.ContentAsync()); await popOutPage.FillAsync("input[name='buyerEmail']", "aa@aa.com"); @@ -581,5 +589,265 @@ namespace BTCPayServer.Tests Assert.Equal(TimeSpan.FromMinutes(5), newStore.DisplayExpirationTimer); Assert.Equal(TimeSpan.FromMinutes(15), newStore.InvoiceExpiration); } + + [Fact] + public async Task CanManageWallet() + { + await using var s = CreatePlaywrightTester(); + await s.StartAsync(); + await s.RegisterNewUser(true); + var (_, storeId) = await s.CreateNewStore(); + const string cryptoCode = "BTC"; + + // In this test, we try to spend from a manual seed. We import the xpub 49'/0'/0', + // then try to use the seed to sign the transaction + await s.GenerateWallet(cryptoCode, "", true); + + //let's test quickly the wallet send page + await s.Page.ClickAsync($"#StoreNav-Wallet{cryptoCode}"); + await s.Page.ClickAsync("#WalletNav-Send"); + //you cannot use the Sign with NBX option without saving private keys when generating the wallet. + Assert.DoesNotContain("nbx-seed", await s.Page.ContentAsync()); + Assert.Equal(0, await s.Page.Locator("#GoBack").CountAsync()); + await s.Page.ClickAsync("#SignTransaction"); + await s.Page.WaitForSelectorAsync("text=Destination Address field is required"); + Assert.Equal(0, await s.Page.Locator("#GoBack").CountAsync()); + await s.Page.ClickAsync("#CancelWizard"); + await s.Page.ClickAsync("#WalletNav-Receive"); + + //generate a receiving address + await s.Page.WaitForSelectorAsync("#address-tab .qr-container"); + Assert.True(await s.Page.Locator("#address-tab .qr-container").IsVisibleAsync()); + // no previous page in the wizard, hence no back button + Assert.Equal(0, await s.Page.Locator("#GoBack").CountAsync()); + var receiveAddr = await s.Page.Locator("#Address").GetAttributeAsync("data-text"); + + // Can add a label? + await TestUtils.EventuallyAsync(async () => + { + await s.Page.ClickAsync("div.label-manager input"); + await Task.Delay(500); + await s.Page.FillAsync("div.label-manager input", "test-label"); + await s.Page.Keyboard.PressAsync("Enter"); + await Task.Delay(500); + await s.Page.FillAsync("div.label-manager input", "label2"); + await s.Page.Keyboard.PressAsync("Enter"); + await Task.Delay(500); + }); + + await TestUtils.EventuallyAsync(async () => + { + await s.Page.ReloadAsync(); + await s.Page.WaitForSelectorAsync("[data-value='test-label']"); + }); + + Assert.True(await s.Page.Locator("#address-tab .qr-container").IsVisibleAsync()); + Assert.Equal(receiveAddr, await s.Page.Locator("#Address").GetAttributeAsync("data-text")); + await TestUtils.EventuallyAsync(async () => + { + var content = await s.Page.ContentAsync(); + Assert.Contains("test-label", content); + }); + + // Remove a label + await s.Page.WaitForSelectorAsync("[data-value='test-label']"); + await s.Page.ClickAsync("[data-value='test-label']"); + await Task.Delay(500); + await s.Page.EvaluateAsync(@"() => { + const l = document.querySelector('[data-value=""test-label""]'); + l.click(); + l.nextSibling.dispatchEvent(new KeyboardEvent('keydown', {'key': 'Delete', keyCode: 8})); + }"); + await Task.Delay(500); + await s.Page.ReloadAsync(); + Assert.DoesNotContain("test-label", await s.Page.ContentAsync()); + Assert.Equal(0, await s.Page.Locator("#GoBack").CountAsync()); + + //send money to addr and ensure it changed + var sess = await s.Server.ExplorerClient.CreateWebsocketNotificationSessionAsync(); + await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(receiveAddr!, Network.RegTest), + Money.Parse("0.1")); + await sess.WaitNext(e => e.Outputs.FirstOrDefault()?.Address.ToString() == receiveAddr); + await Task.Delay(200); + await s.Page.ReloadAsync(); + await s.Page.ClickAsync("button[value=generate-new-address]"); + Assert.NotEqual(receiveAddr, await s.Page.Locator("#Address").GetAttributeAsync("data-text")); + receiveAddr = await s.Page.Locator("#Address").GetAttributeAsync("data-text"); + await s.Page.ClickAsync("#CancelWizard"); + + // Check the label is applied to the tx + var wt = s.InWalletTransactions(); + await wt.AssertHasLabels("label2"); + + //change the wallet and ensure old address is not there and generating a new one does not result in the prev one + await s.GenerateWallet(cryptoCode, "", true); + await s.GoToWallet(null, WalletsNavPages.Receive); + await s.Page.ClickAsync("button[value=generate-new-address]"); + var newAddr = await s.Page.Locator("#Address").GetAttributeAsync("data-text"); + Assert.NotEqual(receiveAddr, newAddr); + + var invoiceId = await s.CreateInvoice(storeId); + var invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId); + var btc = PaymentTypes.CHAIN.GetPaymentMethodId("BTC"); + var address = invoice.GetPaymentPrompt(btc)!.Destination; + + //wallet should have been imported to bitcoin core wallet in watch only mode. + var result = + await s.Server.ExplorerNode.GetAddressInfoAsync(BitcoinAddress.Create(address, Network.RegTest)); + Assert.True(result.IsWatchOnly); + await s.GoToStore(storeId); + var mnemonic = await s.GenerateWallet(cryptoCode, "", true, true); + + //lets import and save private keys + invoiceId = await s.CreateInvoice(storeId); + invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId); + address = invoice.GetPaymentPrompt(btc)!.Destination; + result = await s.Server.ExplorerNode.GetAddressInfoAsync( + BitcoinAddress.Create(address, Network.RegTest)); + //spendable from bitcoin core wallet! + Assert.False(result.IsWatchOnly); + var tx = await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(address, Network.RegTest), + Money.Coins(3.0m)); + await s.Server.ExplorerNode.GenerateAsync(1); + + await s.GoToStore(storeId); + await s.GoToWalletSettings(); + var url = s.Page.Url; + await s.ClickOnAllSectionLinks("#Nav-Wallets"); + + // Make sure wallet info is correct + await s.GoToUrl(url); + + await s.Page.WaitForSelectorAsync("#AccountKeys_0__MasterFingerprint"); + Assert.Equal(mnemonic.DeriveExtKey().GetPublicKey().GetHDFingerPrint().ToString(), + await s.Page.Locator("#AccountKeys_0__MasterFingerprint").GetAttributeAsync("value")); + Assert.Equal("m/84'/1'/0'", + await s.Page.Locator("#AccountKeys_0__AccountKeyPath").GetAttributeAsync("value")); + + // Make sure we can rescan, because we are admin! + await s.Page.ClickAsync("#ActionsDropdownToggle"); + await s.Page.ClickAsync("#Rescan"); + await s.Page.GetByText("The batch size make sure").WaitForAsync(); + // + // Check the tx sent earlier arrived + wt = await s.GoToWalletTransactions(); + await wt.WaitTransactionsLoaded(); + await s.Page.Locator($"[data-text='{tx}']").WaitForAsync(); + + var walletTransactionUri = new Uri(s.Page.Url); + + // Send to bob + var ws = await s.GoToWalletSend(); + var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest); + await ws.FillAddress(bob); + await ws.FillAmount(1); + await ws.Sign(); + // Back button should lead back to the previous page inside the send wizard + var backUrl = await s.Page.Locator("#GoBack").GetAttributeAsync("href"); + Assert.EndsWith($"/send?returnUrl={Uri.EscapeDataString(walletTransactionUri.AbsolutePath)}", backUrl); + // Cancel button should lead to the page that referred to the send wizard + var cancelUrl = await s.Page.Locator("#CancelWizard").GetAttributeAsync("href"); + Assert.EndsWith(walletTransactionUri.AbsolutePath, cancelUrl); + + // Broadcast + var wb = s.InBroadcast(); + await wb.AssertSending(bob, 1.0m); + await wb.Broadcast(); + Assert.Equal(walletTransactionUri.ToString(), s.Page.Url); + + await s.Page.ClickAsync($"#StoreNav-Wallet{cryptoCode}"); + await s.Page.ClickAsync("#WalletNav-Send"); + + var jack = new Key().PubKey.Hash.GetAddress(Network.RegTest); + await ws.FillAddress(jack); + await ws.FillAmount(0.01m); + await ws.Sign(); + + await wb.AssertSending(jack, 0.01m); + Assert.EndsWith("psbt/ready", s.Page.Url); + await wb.Broadcast(); + await s.FindAlertMessage(); + + var bip21 = invoice.EntityToDTO(s.Server.PayTester.GetService>(), s.Server.PayTester.GetService()).CryptoInfo.First().PaymentUrls.BIP21; + //let's make bip21 more interesting + bip21 += "&label=Solid Snake&message=Snake? Snake? SNAAAAKE!"; + var parsedBip21 = new BitcoinUrlBuilder(bip21, Network.RegTest); + await s.GoToWalletSend(); + + // ReSharper disable once AsyncVoidMethod + async void PasteBIP21(object sender, IDialog e) + { + await e.AcceptAsync(bip21); + } + s.Page.Dialog += PasteBIP21; + await s.Page.ClickAsync("#bip21parse"); + s.Page.Dialog -= PasteBIP21; + await s.FindAlertMessage(StatusMessageModel.StatusSeverity.Info); + + Assert.Equal(parsedBip21.Amount!.ToString(false), + await s.Page.Locator("#Outputs_0__Amount").GetAttributeAsync("value")); + Assert.Equal(parsedBip21.Address!.ToString(), + await s.Page.Locator("#Outputs_0__DestinationAddress").GetAttributeAsync("value")); + + await s.Page.ClickAsync("#CancelWizard"); + await s.GoToWalletSettings(); + var settingsUri = new Uri(s.Page.Url); + await s.Page.ClickAsync("#ActionsDropdownToggle"); + await s.Page.ClickAsync("#ViewSeed"); + + // Seed backup page + var recoveryPhrase = await s.Page.Locator("#RecoveryPhrase").First.GetAttributeAsync("data-mnemonic"); + Assert.Equal(mnemonic.ToString(), recoveryPhrase); + Assert.Contains("The recovery phrase will also be stored on the server as a hot wallet.", + await s.Page.ContentAsync()); + + // No confirmation, just a link to return to the wallet + Assert.Equal(0, await s.Page.Locator("#confirm").CountAsync()); + await s.Page.ClickAsync("#proceed"); + Assert.Equal(settingsUri.ToString(), s.Page.Url); + + // Once more, test the cancel link of the wallet send page leads back to the previous page + await s.Page.ClickAsync("#WalletNav-Send"); + cancelUrl = await s.Page.Locator("#CancelWizard").GetAttributeAsync("href"); + Assert.EndsWith(settingsUri.AbsolutePath, cancelUrl); + // no previous page in the wizard, hence no back button + Assert.Equal(0, await s.Page.Locator("#GoBack").CountAsync()); + await s.Page.ClickAsync("#CancelWizard"); + Assert.Equal(settingsUri.ToString(), s.Page.Url); + + // Transactions list contains export, ensure functions are present. + await s.GoToWalletTransactions(); + + await s.Page.ClickAsync(".mass-action-select-all"); + await s.Page.Locator("#BumpFee").WaitForAsync(); + + // JSON export + await s.Page.ClickAsync("#ExportDropdownToggle"); + var opening = s.Page.Context.WaitForPageAsync(); + await s.Page.ClickAsync("#ExportJSON"); + await using (_ = await s.SwitchPage(opening)) + { + await s.Page.WaitForLoadStateAsync(); + Assert.Contains(s.WalletId.ToString(), s.Page.Url); + Assert.EndsWith("export?format=json", s.Page.Url); + Assert.Contains("\"Amount\": \"3.00000000\"", await s.Page.ContentAsync()); + } + + // CSV export + await s.Page.ClickAsync("#ExportDropdownToggle"); + var download = await s.Page.RunAndWaitForDownloadAsync(async () => + { + await s.Page.ClickAsync("#ExportCSV"); + }); + Assert.Contains(tx.ToString(), await File.ReadAllTextAsync(await download.PathAsync())); + + // BIP-329 export + await s.Page.ClickAsync("#ExportDropdownToggle"); + download = await s.Page.RunAndWaitForDownloadAsync(async () => + { + await s.Page.ClickAsync("#ExportBIP329"); + }); + Assert.Contains(tx.ToString(), await File.ReadAllTextAsync(await download.PathAsync())); + } } } diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index acd9e3454..d86d6eab6 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -1493,238 +1493,6 @@ namespace BTCPayServer.Tests } } - [Fact(Timeout = TestTimeout)] - public async Task CanManageWallet() - { - using var s = CreateSeleniumTester(); - await s.StartAsync(); - s.RegisterNewUser(true); - (_, string storeId) = s.CreateNewStore(); - const string cryptoCode = "BTC"; - - // In this test, we try to spend from a manual seed. We import the xpub 49'/0'/0', - // then try to use the seed to sign the transaction - s.GenerateWallet(cryptoCode, "", true); - - //let's test quickly the wallet send page - s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click(); - s.Driver.FindElement(By.Id("WalletNav-Send")).Click(); - //you cannot use the Sign with NBX option without saving private keys when generating the wallet. - Assert.DoesNotContain("nbx-seed", s.Driver.PageSource); - Assert.True(s.Driver.ElementDoesNotExist(By.Id("GoBack"))); - s.Driver.FindElement(By.Id("SignTransaction")).Click(); - Assert.Contains("Destination Address field is required", s.Driver.PageSource); - Assert.True(s.Driver.ElementDoesNotExist(By.Id("GoBack"))); - s.Driver.FindElement(By.Id("CancelWizard")).Click(); - s.Driver.FindElement(By.Id("WalletNav-Receive")).Click(); - - //generate a receiving address - Assert.True(s.Driver.FindElement(By.CssSelector("#address-tab .qr-container")).Displayed); - // no previous page in the wizard, hence no back button - Assert.True(s.Driver.ElementDoesNotExist(By.Id("GoBack"))); - var receiveAddr = s.Driver.FindElement(By.Id("Address")).GetAttribute("data-text"); - - // Can add a label? - await TestUtils.EventuallyAsync(async () => - { - s.Driver.WaitForElement(By.CssSelector("div.label-manager input")).Click(); - await Task.Delay(500); - s.Driver.WaitForElement(By.CssSelector("div.label-manager input")).SendKeys("test-label" + Keys.Enter); - await Task.Delay(500); - s.Driver.WaitForElement(By.CssSelector("div.label-manager input")).SendKeys("label2" + Keys.Enter); - }); - - TestUtils.Eventually(() => - { - s.Driver.Navigate().Refresh(); - Assert.NotNull(s.Driver.FindElement(By.CssSelector("[data-value='test-label']"))); - }); - - Assert.True(s.Driver.FindElement(By.CssSelector("#address-tab .qr-container")).Displayed); - Assert.Equal(receiveAddr, s.Driver.FindElement(By.Id("Address")).GetAttribute("data-text")); - TestUtils.Eventually(() => - { - Assert.Contains("test-label", s.Driver.PageSource); - }); - - // Remove a label - s.Driver.WaitForElement(By.CssSelector("[data-value='test-label']")).Click(); - await Task.Delay(500); - s.Driver.ExecuteJavaScript("var l=document.querySelector('[data-value=\"test-label\"]');l.click();l.nextSibling.dispatchEvent(new KeyboardEvent('keydown', {'key': 'Delete', keyCode: 8}));"); - await Task.Delay(500); - await s.Driver.Navigate().RefreshAsync(); - Assert.DoesNotContain("test-label", s.Driver.PageSource); - Assert.True(s.Driver.ElementDoesNotExist(By.Id("GoBack"))); - - //send money to addr and ensure it changed - var sess = await s.Server.ExplorerClient.CreateWebsocketNotificationSessionAsync(); - await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(receiveAddr, Network.RegTest), - Money.Parse("0.1")); - await sess.WaitNext(e => e.Outputs.FirstOrDefault()?.Address.ToString() == receiveAddr); - await Task.Delay(200); - s.Driver.Navigate().Refresh(); - s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click(); - Assert.NotEqual(receiveAddr, s.Driver.FindElement(By.Id("Address")).GetAttribute("data-text")); - receiveAddr = s.Driver.FindElement(By.Id("Address")).GetAttribute("data-text"); - s.Driver.FindElement(By.Id("CancelWizard")).Click(); - - // Check the label is applied to the tx - s.Driver.WaitWalletTransactionsLoaded(); - // Sometimes this fails in local, but not CI - Assert.Equal("label2", s.Driver.FindElement(By.XPath("//*[@id=\"WalletTransactionsList\"]//*[contains(@class, 'transaction-label')]")).Text); - - //change the wallet and ensure old address is not there and generating a new one does not result in the prev one - s.GenerateWallet(cryptoCode, "", true); - s.GoToWallet(null, WalletsNavPages.Receive); - s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click(); - Assert.NotEqual(receiveAddr, s.Driver.FindElement(By.Id("Address")).GetAttribute("data-text")); - - var invoiceId = s.CreateInvoice(storeId); - var invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId); - var btc = PaymentTypes.CHAIN.GetPaymentMethodId("BTC"); - var address = invoice.GetPaymentPrompt(btc).Destination; - - //wallet should have been imported to bitcoin core wallet in watch only mode. - var result = - await s.Server.ExplorerNode.GetAddressInfoAsync(BitcoinAddress.Create(address, Network.RegTest)); - Assert.True(result.IsWatchOnly); - s.GoToStore(storeId); - var mnemonic = s.GenerateWallet(cryptoCode, "", true, true); - - //lets import and save private keys - invoiceId = s.CreateInvoice(storeId); - invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId); - address = invoice.GetPaymentPrompt(btc).Destination; - result = await s.Server.ExplorerNode.GetAddressInfoAsync( - BitcoinAddress.Create(address, Network.RegTest)); - //spendable from bitcoin core wallet! - Assert.False(result.IsWatchOnly); - var tx = s.Server.ExplorerNode.SendToAddress(BitcoinAddress.Create(address, Network.RegTest), - Money.Coins(3.0m)); - await s.Server.ExplorerNode.GenerateAsync(1); - - s.GoToStore(storeId); - s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click(); - s.ClickOnAllSectionLinks(); - - // Make sure wallet info is correct - s.GoToWalletSettings(cryptoCode); - Assert.Contains(mnemonic.DeriveExtKey().GetPublicKey().GetHDFingerPrint().ToString(), - s.Driver.FindElement(By.Id("AccountKeys_0__MasterFingerprint")).GetAttribute("value")); - Assert.Contains("m/84'/1'/0'", - s.Driver.FindElement(By.Id("AccountKeys_0__AccountKeyPath")).GetAttribute("value")); - - // Make sure we can rescan, because we are admin! - s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click(); - s.Driver.FindElement(By.Id("Rescan")).Click(); - Assert.Contains("The batch size make sure", s.Driver.PageSource); - - // Check the tx sent earlier arrived - s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click(); - s.Driver.WaitWalletTransactionsLoaded(); - s.Driver.FindElement(By.CssSelector($"[data-text='{tx}']")); - - var walletTransactionUri = new Uri(s.Driver.Url); - - // Send to bob - s.Driver.FindElement(By.Id("WalletNav-Send")).Click(); - var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest); - SetTransactionOutput(s, 0, bob, 1); - s.Driver.FindElement(By.Id("SignTransaction")).Click(); - // Back button should lead back to the previous page inside the send wizard - var backUrl = s.Driver.FindElement(By.Id("GoBack")).GetAttribute("href"); - Assert.EndsWith($"/send?returnUrl={Uri.EscapeDataString(walletTransactionUri.AbsolutePath)}", backUrl); - // Cancel button should lead to the page that referred to the send wizard - var cancelUrl = s.Driver.FindElement(By.Id("CancelWizard")).GetAttribute("href"); - Assert.EndsWith(walletTransactionUri.AbsolutePath, cancelUrl); - - // Broadcast - Assert.Contains(bob.ToString(), s.Driver.PageSource); - Assert.Contains("1.00000000", s.Driver.PageSource); - s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click(); - Assert.Equal(walletTransactionUri.ToString(), s.Driver.Url); - - s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click(); - s.Driver.FindElement(By.Id("WalletNav-Send")).Click(); - - var jack = new Key().PubKey.Hash.GetAddress(Network.RegTest); - SetTransactionOutput(s, 0, jack, 0.01m); - s.Driver.FindElement(By.Id("SignTransaction")).Click(); - s.Driver.WaitForElement(By.CssSelector("button[value=broadcast]")); - Assert.Contains(jack.ToString(), s.Driver.PageSource); - Assert.Contains("0.01000000", s.Driver.PageSource); - Assert.EndsWith("psbt/ready", s.Driver.Url); - s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click(); - Assert.Equal(walletTransactionUri.ToString(), s.Driver.Url); - var bip21 = invoice.EntityToDTO(s.Server.PayTester.GetService>(), s.Server.PayTester.GetService()).CryptoInfo.First().PaymentUrls.BIP21; - //let's make bip21 more interesting - bip21 += "&label=Solid Snake&message=Snake? Snake? SNAAAAKE!"; - var parsedBip21 = new BitcoinUrlBuilder(bip21, Network.RegTest); - s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click(); - s.Driver.FindElement(By.Id("WalletNav-Send")).Click(); - s.Driver.FindElement(By.Id("bip21parse")).Click(); - s.Driver.SwitchTo().Alert().SendKeys(bip21); - s.Driver.SwitchTo().Alert().Accept(); - s.FindAlertMessage(StatusMessageModel.StatusSeverity.Info); - Assert.Equal(parsedBip21.Amount.ToString(false), - s.Driver.FindElement(By.Id("Outputs_0__Amount")).GetAttribute("value")); - Assert.Equal(parsedBip21.Address.ToString(), - s.Driver.FindElement(By.Id("Outputs_0__DestinationAddress")).GetAttribute("value")); - - s.Driver.FindElement(By.Id("CancelWizard")).Click(); - s.GoToWalletSettings(cryptoCode); - var settingsUri = new Uri(s.Driver.Url); - s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click(); - s.Driver.FindElement(By.Id("ViewSeed")).Click(); - - // Seed backup page - var recoveryPhrase = s.Driver.FindElements(By.Id("RecoveryPhrase")).First() - .GetAttribute("data-mnemonic"); - Assert.Equal(mnemonic.ToString(), recoveryPhrase); - Assert.Contains("The recovery phrase will also be stored on the server as a hot wallet.", - s.Driver.PageSource); - - // No confirmation, just a link to return to the wallet - Assert.Empty(s.Driver.FindElements(By.Id("confirm"))); - s.Driver.FindElement(By.Id("proceed")).Click(); - Assert.Equal(settingsUri.ToString(), s.Driver.Url); - - // Once more, test the cancel link of the wallet send page leads back to the previous page - s.Driver.FindElement(By.Id("WalletNav-Send")).Click(); - cancelUrl = s.Driver.FindElement(By.Id("CancelWizard")).GetAttribute("href"); - Assert.EndsWith(settingsUri.AbsolutePath, cancelUrl); - // no previous page in the wizard, hence no back button - Assert.True(s.Driver.ElementDoesNotExist(By.Id("GoBack"))); - s.Driver.FindElement(By.Id("CancelWizard")).Click(); - Assert.Equal(settingsUri.ToString(), s.Driver.Url); - - // Transactions list contains export, ensure functions are present. - s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click(); - - s.Driver.FindElement(By.ClassName("mass-action-select-all")).Click(); - s.Driver.FindElement(By.Id("BumpFee")); - - // JSON export - s.Driver.FindElement(By.Id("ExportDropdownToggle")).Click(); - s.Driver.FindElement(By.Id("ExportJSON")).Click(); - Thread.Sleep(1000); - s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last()); - Assert.Contains(s.WalletId.ToString(), s.Driver.Url); - Assert.EndsWith("export?format=json", s.Driver.Url); - Assert.Contains("\"Amount\": \"3.00000000\"", s.Driver.PageSource); - s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First()); - - // CSV export - s.Driver.FindElement(By.Id("ExportDropdownToggle")).Click(); - s.Driver.FindElement(By.Id("ExportCSV")).Click(); - s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First()); - - // BIP-329 export - s.Driver.FindElement(By.Id("ExportDropdownToggle")).Click(); - s.Driver.FindElement(By.Id("ExportBIP329")).Click(); - s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First()); - } - [Fact] [Trait("Selenium", "Selenium")] [Trait("Lightning", "Lightning")] diff --git a/btcpayserver.sln.DotSettings b/btcpayserver.sln.DotSettings index 26383ff26..a9369863e 100644 --- a/btcpayserver.sln.DotSettings +++ b/btcpayserver.sln.DotSettings @@ -1,4 +1,5 @@  + BIP BTC CPFP HWI