diff --git a/BTCPayServer.Tests/Extensions.cs b/BTCPayServer.Tests/Extensions.cs index e3d4d1e06..32d3f1023 100644 --- a/BTCPayServer.Tests/Extensions.cs +++ b/BTCPayServer.Tests/Extensions.cs @@ -122,6 +122,13 @@ retry: driver.ExecuteJavaScript($"document.getElementById('{element}').{funcName}()"); } + public static void WaitWalletTransactionsLoaded(this IWebDriver driver) + { + var wait = new WebDriverWait(driver, SeleniumTester.ImplicitWait); + wait.UntilJsIsReady(); + wait.Until(d => d.WaitForElement(By.CssSelector("#WalletTransactions[data-loaded='true']"))); + } + public static IWebElement WaitForElement(this IWebDriver driver, By selector) { var wait = new WebDriverWait(driver, SeleniumTester.ImplicitWait); diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index a413585bf..b0ce5df90 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -1441,7 +1441,7 @@ namespace BTCPayServer.Tests s.Driver.FindElement(By.Id("CancelWizard")).Click(); // Check the label is applied to the tx - + s.Driver.WaitWalletTransactionsLoaded(); 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 @@ -1492,7 +1492,9 @@ namespace BTCPayServer.Tests // Check the tx sent earlier arrived s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click(); - Assert.Contains(tx.ToString(), s.Driver.PageSource); + s.Driver.WaitWalletTransactionsLoaded(); + s.Driver.FindElement(By.PartialLinkText(tx.ToString())); + var walletTransactionUri = new Uri(s.Driver.Url); // Send to bob @@ -1618,9 +1620,8 @@ namespace BTCPayServer.Tests // Transactions list is empty s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click(); - Assert.Contains("There are no transactions yet.", s.Driver.PageSource); - s.Driver.AssertElementNotFound(By.Id("ExportDropdownToggle")); - s.Driver.AssertElementNotFound(By.Id("ActionsDropdownToggle")); + s.Driver.WaitWalletTransactionsLoaded(); + Assert.Contains("There are no transactions yet", s.Driver.FindElement(By.Id("WalletTransactions")).Text); } [Fact] diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 5242dbe29..34e86e01d 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -719,7 +719,7 @@ namespace BTCPayServer.Tests btcDerivationScheme.GetDerivation(new KeyPath("0/90")).ScriptPubKey, Money.Coins(1.0m)); tester.ExplorerNode.Generate(1); var transactions = Assert.IsType(Assert - .IsType(walletController.WalletTransactions(walletId).Result).Model); + .IsType(walletController.WalletTransactions(walletId, loadTransactions: true).Result).Model); Assert.Empty(transactions.Transactions); Assert.IsType(walletController.WalletRescan(walletId, rescan).Result); @@ -748,7 +748,7 @@ namespace BTCPayServer.Tests Assert.NotNull(rescan.TimeOfScan); Assert.Equal(1, rescan.LastSuccess.Found); transactions = Assert.IsType(Assert - .IsType(walletController.WalletTransactions(walletId).Result).Model); + .IsType(walletController.WalletTransactions(walletId, loadTransactions: true).Result).Model); var tx = Assert.Single(transactions.Transactions); Assert.Equal(tx.Id, txId.ToString()); @@ -763,7 +763,7 @@ namespace BTCPayServer.Tests await walletController.ModifyTransaction(walletId, tx.Id, addcomment: "hello")); transactions = Assert.IsType(Assert - .IsType(walletController.WalletTransactions(walletId).Result).Model); + .IsType(walletController.WalletTransactions(walletId, loadTransactions: true).Result).Model); tx = Assert.Single(transactions.Transactions); Assert.Equal("hello", tx.Comment); @@ -775,7 +775,7 @@ namespace BTCPayServer.Tests await walletController.ModifyTransaction(walletId, tx.Id, removelabel: "test2")); transactions = Assert.IsType(Assert - .IsType(walletController.WalletTransactions(walletId).Result).Model); + .IsType(walletController.WalletTransactions(walletId, loadTransactions: true).Result).Model); tx = Assert.Single(transactions.Transactions); Assert.Equal("hello", tx.Comment); diff --git a/BTCPayServer/Components/StoreRecentTransactions/StoreRecentTransactions.cs b/BTCPayServer/Components/StoreRecentTransactions/StoreRecentTransactions.cs index 16a38e651..ca1e4d674 100644 --- a/BTCPayServer/Components/StoreRecentTransactions/StoreRecentTransactions.cs +++ b/BTCPayServer/Components/StoreRecentTransactions/StoreRecentTransactions.cs @@ -58,7 +58,7 @@ public class StoreRecentTransactions : ViewComponent { var network = derivationSettings.Network; var wallet = _walletProvider.GetWallet(network); - var allTransactions = await wallet.FetchTransactionHistory(derivationSettings.AccountDerivation, 0, 5, TimeSpan.FromDays(31.0)); + var allTransactions = await wallet.FetchTransactionHistory(derivationSettings.AccountDerivation, 0, 5, TimeSpan.FromDays(31.0), cancellationToken: this.HttpContext.RequestAborted); var walletTransactionsInfo = await _walletRepository.GetWalletTransactionsInfo(vm.WalletId, allTransactions.Select(t => t.TransactionId.ToString()).ToArray()); transactions = allTransactions diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldStoreOnChainWalletsController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldStoreOnChainWalletsController.cs index d930caad1..448ff0d22 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldStoreOnChainWalletsController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldStoreOnChainWalletsController.cs @@ -182,7 +182,8 @@ namespace BTCPayServer.Controllers.Greenfield [FromQuery] TransactionStatus[]? statusFilter = null, [FromQuery] string? labelFilter = null, [FromQuery] int skip = 0, - [FromQuery] int limit = int.MaxValue + [FromQuery] int limit = int.MaxValue, + CancellationToken cancellationToken = default ) { if (IsInvalidWalletRequest(cryptoCode, out var network, @@ -197,7 +198,7 @@ namespace BTCPayServer.Controllers.Greenfield if (statusFilter?.Any() is true || !string.IsNullOrWhiteSpace(labelFilter)) preFiltering = false; var txs = await wallet.FetchTransactionHistory(derivationScheme.AccountDerivation, preFiltering ? skip : 0, - preFiltering ? limit : int.MaxValue); + preFiltering ? limit : int.MaxValue, cancellationToken: cancellationToken); if (!preFiltering) { var filteredList = new List(txs.Count); diff --git a/BTCPayServer/Controllers/UIWalletsController.cs b/BTCPayServer/Controllers/UIWalletsController.cs index 348bac650..5b2eb6f0f 100644 --- a/BTCPayServer/Controllers/UIWalletsController.cs +++ b/BTCPayServer/Controllers/UIWalletsController.cs @@ -212,7 +212,9 @@ namespace BTCPayServer.Controllers WalletId walletId, string? labelFilter = null, int skip = 0, - int count = 50 + int count = 50, + bool loadTransactions = false, + CancellationToken cancellationToken = default ) { var paymentMethod = GetDerivationSchemeSettings(walletId); @@ -223,25 +225,25 @@ namespace BTCPayServer.Controllers // We can't filter at the database level if we need to apply label filter var preFiltering = string.IsNullOrEmpty(labelFilter); - var transactions = await wallet.FetchTransactionHistory(paymentMethod.AccountDerivation, preFiltering ? skip : null, preFiltering ? count : null); - var walletTransactionsInfo = await WalletRepository.GetWalletTransactionsInfo(walletId, transactions.Select(t => t.TransactionId.ToString()).ToArray()); var model = new ListTransactionsViewModel { Skip = skip, Count = count }; model.Labels.AddRange( (await WalletRepository.GetWalletLabels(walletId)) .Select(c => (c.Label, c.Color, ColorPalette.Default.TextColor(c.Color)))); + IList? transactions = null; + Dictionary? walletTransactionsInfo = null; + if (loadTransactions) + { + transactions = await wallet.FetchTransactionHistory(paymentMethod.AccountDerivation, preFiltering ? skip : null, preFiltering ? count : null, cancellationToken: cancellationToken); + walletTransactionsInfo = await WalletRepository.GetWalletTransactionsInfo(walletId, transactions.Select(t => t.TransactionId.ToString()).ToArray()); + } + if (labelFilter != null) { model.PaginationQuery = new Dictionary { { "labelFilter", labelFilter } }; } - if (transactions == null) + if (transactions == null || walletTransactionsInfo is null) { - TempData.SetStatusMessageModel(new StatusMessageModel() - { - Severity = StatusMessageModel.StatusSeverity.Error, - Message = - "There was an error retrieving the transactions list. Is NBXplorer configured correctly?" - }); model.Transactions = new List(); } else @@ -1311,7 +1313,7 @@ namespace BTCPayServer.Controllers [HttpGet("{walletId}/export")] public async Task Export( [ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId, - string format, string? labelFilter = null) + string format, string? labelFilter = null, CancellationToken cancellationToken = default) { var paymentMethod = GetDerivationSchemeSettings(walletId); if (paymentMethod == null) @@ -1319,7 +1321,7 @@ namespace BTCPayServer.Controllers var wallet = _walletProvider.GetWallet(paymentMethod.Network); var walletTransactionsInfoAsync = WalletRepository.GetWalletTransactionsInfo(walletId, (string[]?)null); - var input = await wallet.FetchTransactionHistory(paymentMethod.AccountDerivation); + var input = await wallet.FetchTransactionHistory(paymentMethod.AccountDerivation, cancellationToken: cancellationToken); var walletTransactionsInfo = await walletTransactionsInfoAsync; var export = new TransactionsExport(wallet, walletTransactionsInfo); var res = export.Process(input, format); diff --git a/BTCPayServer/Services/Wallets/BTCPayWallet.cs b/BTCPayServer/Services/Wallets/BTCPayWallet.cs index ee3b89fb7..3af082c06 100644 --- a/BTCPayServer/Services/Wallets/BTCPayWallet.cs +++ b/BTCPayServer/Services/Wallets/BTCPayWallet.cs @@ -215,7 +215,7 @@ namespace BTCPayServer.Services.Wallets return await completionSource.Task; } List dummy = new List(); - public async Task> FetchTransactionHistory(DerivationStrategyBase derivationStrategyBase, int? skip = null, int? count = null, TimeSpan? interval = null) + public async Task> FetchTransactionHistory(DerivationStrategyBase derivationStrategyBase, int? skip = null, int? count = null, TimeSpan? interval = null, CancellationToken cancellationToken = default) { // This is two paths: // * Sometimes we can ask the DB to do the filtering of rows: If that's the case, we should try to filter at the DB level directly as it is the most efficient. @@ -243,18 +243,21 @@ namespace BTCPayServer.Services.Wallets else { await using var ctx = await NbxplorerConnectionFactory.OpenConnection(); - var rows = await ctx.QueryAsync<(string tx_id, DateTimeOffset seen_at, string blk_id, long? blk_height, long balance_change, string asset_id, long confs)>( - "SELECT r.tx_id, r.seen_at, t.blk_id, t.blk_height, r.balance_change, r.asset_id, COALESCE((SELECT height FROM get_tip('BTC')) - t.blk_height + 1, 0) AS confs " + + var cmd = new CommandDefinition( + commandText: "SELECT r.tx_id, r.seen_at, t.blk_id, t.blk_height, r.balance_change, r.asset_id, COALESCE((SELECT height FROM get_tip('BTC')) - t.blk_height + 1, 0) AS confs " + "FROM get_wallets_recent(@wallet_id, @code, @interval, @count, @skip) r " + "JOIN txs t USING (code, tx_id) " + - "ORDER BY r.seen_at DESC", new + "ORDER BY r.seen_at DESC", + parameters: new { wallet_id = NBXplorer.Client.DBUtils.nbxv1_get_wallet_id(Network.CryptoCode, derivationStrategyBase.ToString()), code = Network.CryptoCode, count = count, skip = skip, interval = interval is TimeSpan t ? t : TimeSpan.FromDays(365 * 1000) - }); + }, + cancellationToken: cancellationToken); + var rows = await ctx.QueryAsync<(string tx_id, DateTimeOffset seen_at, string blk_id, long? blk_height, long balance_change, string asset_id, long confs)>(cmd); rows.TryGetNonEnumeratedCount(out int c); var lines = new List(c); foreach (var row in rows) diff --git a/BTCPayServer/Views/UIWallets/WalletTransactions.cshtml b/BTCPayServer/Views/UIWallets/WalletTransactions.cshtml index 5dea1a7c1..37c4e53e9 100644 --- a/BTCPayServer/Views/UIWallets/WalletTransactions.cshtml +++ b/BTCPayServer/Views/UIWallets/WalletTransactions.cshtml @@ -49,10 +49,10 @@ @* Custom Range Modal *@ } @@ -147,80 +143,66 @@ } -@if (Model.Transactions.Any()) -{ - -
- -
-
- - - - - - - - - - - - - - -
- - -
- Date - -
-
LabelTransaction IdAmount
-
-
- - -
-
- Loading... -
-
-
- - +
-} -else -{ -

- There are no transactions @(string.IsNullOrEmpty(labelFilter) ? "yet" : $"labeled with \"{labelFilter}\""). -

-} + + +
+ +
+ + + + + + + + + + + + + + +
+ + +
+ Date + +
+
LabelTransaction IdAmount
+
+ + +
+
+ Loading... +
+
+
+ +

If BTCPay Server shows you an invalid balance, rescan your wallet.