mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 05:54:26 +01:00
Add fee information in wallet tx report and tx list (#6857)
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
@@ -15,6 +16,7 @@ using BTCPayServer.Payments;
|
|||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
using BTCPayServer.Services.Rates;
|
using BTCPayServer.Services.Rates;
|
||||||
|
using BTCPayServer.Services.Wallets;
|
||||||
using BTCPayServer.Views.Manage;
|
using BTCPayServer.Views.Manage;
|
||||||
using BTCPayServer.Views.Server;
|
using BTCPayServer.Views.Server;
|
||||||
using BTCPayServer.Views.Stores;
|
using BTCPayServer.Views.Stores;
|
||||||
@@ -611,7 +613,8 @@ namespace BTCPayServer.Tests
|
|||||||
await s.RegisterNewUser(true);
|
await s.RegisterNewUser(true);
|
||||||
await s.CreateNewStore();
|
await s.CreateNewStore();
|
||||||
|
|
||||||
await s.AddDerivationScheme("BTC", new ExtKey().Neuter().GetWif(Network.RegTest).ToString() + "-[legacy]");
|
var btcDerivationScheme = new ExtKey().Neuter().GetWif(Network.RegTest).ToString() + "-[legacy]";
|
||||||
|
await s.AddDerivationScheme("BTC", btcDerivationScheme);
|
||||||
await s.AddDerivationScheme("LTC", new ExtKey().Neuter().GetWif(NBitcoin.Altcoins.Litecoin.Instance.Regtest).ToString() + "-[legacy]");
|
await s.AddDerivationScheme("LTC", new ExtKey().Neuter().GetWif(NBitcoin.Altcoins.Litecoin.Instance.Regtest).ToString() + "-[legacy]");
|
||||||
|
|
||||||
await s.GoToStore();
|
await s.GoToStore();
|
||||||
@@ -685,9 +688,41 @@ namespace BTCPayServer.Tests
|
|||||||
await pmo.AssertRowContains(txId, "500.00 USD");
|
await pmo.AssertRowContains(txId, "500.00 USD");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await s.GoToWallet(new(s.StoreId, "BTC"), WalletsNavPages.Transactions);
|
|
||||||
await s.ClickViewReport();
|
|
||||||
|
|
||||||
|
var fee = Money.Zero;
|
||||||
|
var feeRate = FeeRate.Zero;
|
||||||
|
// Quick check on some internal of wallet that isn't related to this test
|
||||||
|
var wallet = s.Server.PayTester.GetService<BTCPayWalletProvider>().GetWallet("BTC");
|
||||||
|
var derivation = s.Server.GetNetwork("BTC").NBXplorerNetwork.DerivationStrategyFactory.Parse(btcDerivationScheme);
|
||||||
|
foreach (var forceHasFeeInfo in new bool?[]{ true, false, null})
|
||||||
|
foreach (var inefficient in new[] { true, false })
|
||||||
|
{
|
||||||
|
wallet.ForceInefficientPath = inefficient;
|
||||||
|
wallet.ForceHasFeeInformation = forceHasFeeInfo;
|
||||||
|
wallet.InvalidateCache(derivation);
|
||||||
|
var fetched = await wallet.FetchTransactionHistory(derivation);
|
||||||
|
var tx = fetched.First(f => f.TransactionId == txIds["BTC"]);
|
||||||
|
if (forceHasFeeInfo is true or null || inefficient)
|
||||||
|
{
|
||||||
|
Assert.NotNull(tx.Fee);
|
||||||
|
Assert.NotNull(tx.FeeRate);
|
||||||
|
fee = tx.Fee;
|
||||||
|
feeRate = tx.FeeRate;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Assert.Null(tx.Fee);
|
||||||
|
Assert.Null(tx.FeeRate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wallet.InvalidateCache(derivation);
|
||||||
|
wallet.ForceHasFeeInformation = null;
|
||||||
|
wallet.ForceInefficientPath = false;
|
||||||
|
|
||||||
|
var pmo3 = await s.GoToWalletTransactions(new(s.StoreId, "BTC"));
|
||||||
|
await pmo3.AssertRowContains(txIds["BTC"], $"{fee} ({feeRate})");
|
||||||
|
|
||||||
|
await s.ClickViewReport();
|
||||||
var csvTxt = await s.DownloadReportCSV();
|
var csvTxt = await s.DownloadReportCSV();
|
||||||
var csvTester = new CSVWalletsTester(csvTxt);
|
var csvTester = new CSVWalletsTester(csvTxt);
|
||||||
|
|
||||||
@@ -698,6 +733,8 @@ namespace BTCPayServer.Tests
|
|||||||
csvTester
|
csvTester
|
||||||
.ForTxId(txIds[cryptoCode].ToString())
|
.ForTxId(txIds[cryptoCode].ToString())
|
||||||
.AssertValues(
|
.AssertValues(
|
||||||
|
("FeeRate", feeRate.SatoshiPerByte.ToString(CultureInfo.InvariantCulture)),
|
||||||
|
("Fee", fee.ToString()),
|
||||||
("Rate (USD)", "5000"),
|
("Rate (USD)", "5000"),
|
||||||
("Rate (CAD)", "4500"),
|
("Rate (CAD)", "4500"),
|
||||||
("Rate (JPY)", "700000"),
|
("Rate (JPY)", "700000"),
|
||||||
@@ -717,6 +754,23 @@ namespace BTCPayServer.Tests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This shouldn't crash if NBX doesn't support fee fetching
|
||||||
|
wallet.ForceHasFeeInformation = false;
|
||||||
|
await s.Page.ReloadAsync();
|
||||||
|
csvTxt = await s.DownloadReportCSV();
|
||||||
|
csvTester = new CSVWalletsTester(csvTxt);
|
||||||
|
csvTester
|
||||||
|
.ForTxId(txIds["BTC"].ToString())
|
||||||
|
.AssertValues(
|
||||||
|
("FeeRate", ""),
|
||||||
|
("Fee", ""),
|
||||||
|
("Rate (USD)", "5000"),
|
||||||
|
("Rate (CAD)", "4500"),
|
||||||
|
("Rate (JPY)", "700000"),
|
||||||
|
("Rate (EUR)", "4000")
|
||||||
|
);
|
||||||
|
wallet.ForceHasFeeInformation = null;
|
||||||
|
|
||||||
var invId = await s.CreateInvoice(storeId: s.StoreId, amount: 10_000);
|
var invId = await s.CreateInvoice(storeId: s.StoreId, amount: 10_000);
|
||||||
await s.GoToInvoiceCheckout(invId);
|
await s.GoToInvoiceCheckout(invId);
|
||||||
await s.PayInvoice();
|
await s.PayInvoice();
|
||||||
@@ -1160,51 +1214,51 @@ namespace BTCPayServer.Tests
|
|||||||
{
|
{
|
||||||
await s.Page.ClickAsync("button:has-text('Pay')");
|
await s.Page.ClickAsync("button:has-text('Pay')");
|
||||||
await s.Page.WaitForLoadStateAsync();
|
await s.Page.WaitForLoadStateAsync();
|
||||||
|
|
||||||
await s.Page.WaitForSelectorAsync("iframe[name='btcpay']", new() { Timeout = 10000 });
|
await s.Page.WaitForSelectorAsync("iframe[name='btcpay']", new() { Timeout = 10000 });
|
||||||
|
|
||||||
var iframe = s.Page.Frame("btcpay");
|
var iframe = s.Page.Frame("btcpay");
|
||||||
Assert.NotNull(iframe);
|
Assert.NotNull(iframe);
|
||||||
|
|
||||||
await iframe.FillAsync("#test-payment-amount", "0.05");
|
await iframe.FillAsync("#test-payment-amount", "0.05");
|
||||||
await iframe.ClickAsync("#FakePayment");
|
await iframe.ClickAsync("#FakePayment");
|
||||||
await iframe.WaitForSelectorAsync("#CheatSuccessMessage", new() { Timeout = 10000 });
|
await iframe.WaitForSelectorAsync("#CheatSuccessMessage", new() { Timeout = 10000 });
|
||||||
|
|
||||||
invoiceId = s.Page.Url.Split('/').Last();
|
invoiceId = s.Page.Url.Split('/').Last();
|
||||||
}
|
}
|
||||||
await s.GoToInvoices();
|
await s.GoToInvoices();
|
||||||
|
|
||||||
await s.Page.ClickAsync("[data-invoice-state-badge] .dropdown-toggle");
|
await s.Page.ClickAsync("[data-invoice-state-badge] .dropdown-toggle");
|
||||||
await s.Page.ClickAsync("[data-invoice-state-badge] .dropdown-menu button:has-text('Mark as settled')");
|
await s.Page.ClickAsync("[data-invoice-state-badge] .dropdown-menu button:has-text('Mark as settled')");
|
||||||
await s.Page.WaitForLoadStateAsync();
|
await s.Page.WaitForLoadStateAsync();
|
||||||
|
|
||||||
await s.GoToStore();
|
await s.GoToStore();
|
||||||
await s.Page.ClickAsync("#StoreNav-PaymentRequests");
|
await s.Page.ClickAsync("#StoreNav-PaymentRequests");
|
||||||
await s.Page.WaitForLoadStateAsync();
|
await s.Page.WaitForLoadStateAsync();
|
||||||
|
|
||||||
var opening2 = s.Page.Context.WaitForPageAsync();
|
var opening2 = s.Page.Context.WaitForPageAsync();
|
||||||
await s.Page.ClickAsync("a:has-text('View')");
|
await s.Page.ClickAsync("a:has-text('View')");
|
||||||
await using (_ = await s.SwitchPage(opening2))
|
await using (_ = await s.SwitchPage(opening2))
|
||||||
{
|
{
|
||||||
await s.Page.WaitForLoadStateAsync();
|
await s.Page.WaitForLoadStateAsync();
|
||||||
|
|
||||||
var markSettledExists = await s.Page.Locator("button:has-text('Mark as settled')").CountAsync();
|
var markSettledExists = await s.Page.Locator("button:has-text('Mark as settled')").CountAsync();
|
||||||
Assert.True(markSettledExists > 0, "Mark as settled button should be visible on public page after invoice is settled");
|
Assert.True(markSettledExists > 0, "Mark as settled button should be visible on public page after invoice is settled");
|
||||||
await s.Page.ClickAsync("button:has-text('Mark as settled')");
|
await s.Page.ClickAsync("button:has-text('Mark as settled')");
|
||||||
await s.Page.WaitForLoadStateAsync();
|
await s.Page.WaitForLoadStateAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
await s.GoToStore();
|
await s.GoToStore();
|
||||||
await s.Page.ClickAsync("#StoreNav-PaymentRequests");
|
await s.Page.ClickAsync("#StoreNav-PaymentRequests");
|
||||||
await s.Page.WaitForLoadStateAsync();
|
await s.Page.WaitForLoadStateAsync();
|
||||||
|
|
||||||
var listContent = await s.Page.ContentAsync();
|
var listContent = await s.Page.ContentAsync();
|
||||||
var isSettledInList = listContent.Contains("Settled");
|
var isSettledInList = listContent.Contains("Settled");
|
||||||
var isPendingInList = listContent.Contains("Pending");
|
var isPendingInList = listContent.Contains("Pending");
|
||||||
|
|
||||||
var settledBadgeExists = await s.Page.Locator(".badge:has-text('Settled')").CountAsync();
|
var settledBadgeExists = await s.Page.Locator(".badge:has-text('Settled')").CountAsync();
|
||||||
var pendingBadgeExists = await s.Page.Locator(".badge:has-text('Pending')").CountAsync();
|
var pendingBadgeExists = await s.Page.Locator(".badge:has-text('Pending')").CountAsync();
|
||||||
|
|
||||||
Assert.True(isSettledInList || settledBadgeExists > 0, "Payment request should show as Settled in the list");
|
Assert.True(isSettledInList || settledBadgeExists > 0, "Payment request should show as Settled in the list");
|
||||||
Assert.False(isPendingInList && pendingBadgeExists > 0, "Payment request should not show as Pending anymore");
|
Assert.False(isPendingInList && pendingBadgeExists > 0, "Payment request should not show as Pending anymore");
|
||||||
}
|
}
|
||||||
@@ -1224,10 +1278,10 @@ namespace BTCPayServer.Tests
|
|||||||
var admin = s.AsTestAccount();
|
var admin = s.AsTestAccount();
|
||||||
await s.GoToHome();
|
await s.GoToHome();
|
||||||
await s.GoToServer(ServerNavPages.Policies);
|
await s.GoToServer(ServerNavPages.Policies);
|
||||||
|
|
||||||
Assert.True(await s.Page.Locator("#EnableRegistration").IsCheckedAsync());
|
Assert.True(await s.Page.Locator("#EnableRegistration").IsCheckedAsync());
|
||||||
Assert.False(await s.Page.Locator("#RequiresUserApproval").IsCheckedAsync());
|
Assert.False(await s.Page.Locator("#RequiresUserApproval").IsCheckedAsync());
|
||||||
|
|
||||||
await s.Page.Locator("#RequiresUserApproval").ClickAsync();
|
await s.Page.Locator("#RequiresUserApproval").ClickAsync();
|
||||||
await s.ClickPagePrimary();
|
await s.ClickPagePrimary();
|
||||||
await s.FindAlertMessage(partialText: "Policies updated successfully");
|
await s.FindAlertMessage(partialText: "Policies updated successfully");
|
||||||
@@ -1255,7 +1309,7 @@ namespace BTCPayServer.Tests
|
|||||||
{
|
{
|
||||||
Assert.Equal("1", await s.Page.Locator("#NotificationsBadge").TextContentAsync());
|
Assert.Equal("1", await s.Page.Locator("#NotificationsBadge").TextContentAsync());
|
||||||
});
|
});
|
||||||
|
|
||||||
await s.Page.Locator("#NotificationsHandle").ClickAsync();
|
await s.Page.Locator("#NotificationsHandle").ClickAsync();
|
||||||
Assert.Matches($"New user {unapproved.RegisterDetails.Email} requires approval", await s.Page.Locator("#NotificationsList .notification").TextContentAsync());
|
Assert.Matches($"New user {unapproved.RegisterDetails.Email} requires approval", await s.Page.Locator("#NotificationsList .notification").TextContentAsync());
|
||||||
await s.Page.Locator("#NotificationsMarkAllAsSeen").ClickAsync();
|
await s.Page.Locator("#NotificationsMarkAllAsSeen").ClickAsync();
|
||||||
@@ -1302,7 +1356,7 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.Equal(1, await rows.CountAsync());
|
Assert.Equal(1, await rows.CountAsync());
|
||||||
Assert.Contains(autoApproved.RegisterDetails.Email, await rows.First.TextContentAsync());
|
Assert.Contains(autoApproved.RegisterDetails.Email, await rows.First.TextContentAsync());
|
||||||
await Expect(s.Page.Locator("#UsersList tr.user-overview-row:first-child .user-approved")).Not.ToBeVisibleAsync();
|
await Expect(s.Page.Locator("#UsersList tr.user-overview-row:first-child .user-approved")).Not.ToBeVisibleAsync();
|
||||||
|
|
||||||
await s.Page.ClickAsync("#UsersList tr.user-overview-row:first-child .user-edit");
|
await s.Page.ClickAsync("#UsersList tr.user-overview-row:first-child .user-edit");
|
||||||
await Expect(s.Page.Locator("#Approved")).Not.ToBeVisibleAsync();
|
await Expect(s.Page.Locator("#Approved")).Not.ToBeVisibleAsync();
|
||||||
|
|
||||||
@@ -1313,12 +1367,12 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.Equal(1, await rows.CountAsync());
|
Assert.Equal(1, await rows.CountAsync());
|
||||||
Assert.Contains(unapproved.RegisterDetails.Email, await rows.First.TextContentAsync());
|
Assert.Contains(unapproved.RegisterDetails.Email, await rows.First.TextContentAsync());
|
||||||
Assert.Contains("Pending Approval", await s.Page.Locator("#UsersList tr.user-overview-row:first-child .user-status").TextContentAsync());
|
Assert.Contains("Pending Approval", await s.Page.Locator("#UsersList tr.user-overview-row:first-child .user-status").TextContentAsync());
|
||||||
|
|
||||||
await s.Page.ClickAsync("#UsersList tr.user-overview-row:first-child .user-edit");
|
await s.Page.ClickAsync("#UsersList tr.user-overview-row:first-child .user-edit");
|
||||||
await s.Page.ClickAsync("#Approved");
|
await s.Page.ClickAsync("#Approved");
|
||||||
await s.ClickPagePrimary();
|
await s.ClickPagePrimary();
|
||||||
await s.FindAlertMessage(partialText: "User successfully updated");
|
await s.FindAlertMessage(partialText: "User successfully updated");
|
||||||
|
|
||||||
await s.GoToServer(ServerNavPages.Users);
|
await s.GoToServer(ServerNavPages.Users);
|
||||||
Assert.Contains(unapproved.RegisterDetails.Email, await s.Page.GetAttributeAsync("#SearchTerm", "value"));
|
Assert.Contains(unapproved.RegisterDetails.Email, await s.Page.GetAttributeAsync("#SearchTerm", "value"));
|
||||||
Assert.Equal(1, await rows.CountAsync());
|
Assert.Equal(1, await rows.CountAsync());
|
||||||
|
|||||||
@@ -658,6 +658,7 @@ namespace BTCPayServer.Controllers
|
|||||||
vm.Positive = tx.BalanceChange.GetValue(wallet.Network) >= 0;
|
vm.Positive = tx.BalanceChange.GetValue(wallet.Network) >= 0;
|
||||||
vm.Balance = tx.BalanceChange.ShowMoney(wallet.Network);
|
vm.Balance = tx.BalanceChange.ShowMoney(wallet.Network);
|
||||||
vm.IsConfirmed = tx.Confirmations != 0;
|
vm.IsConfirmed = tx.Confirmations != 0;
|
||||||
|
vm.HistoryLine = tx;
|
||||||
// If support isn't possible, we want the user to be able to click so he can see why it doesn't work
|
// If support isn't possible, we want the user to be able to click so he can see why it doesn't work
|
||||||
vm.CanBumpFee =
|
vm.CanBumpFee =
|
||||||
tx.Confirmations == 0 &&
|
tx.Confirmations == 0 &&
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
|
using BTCPayServer.Services.Wallets;
|
||||||
|
|
||||||
namespace BTCPayServer.Models.WalletViewModels
|
namespace BTCPayServer.Models.WalletViewModels
|
||||||
{
|
{
|
||||||
@@ -23,6 +24,7 @@ namespace BTCPayServer.Models.WalletViewModels
|
|||||||
public RateBook WalletRateBook { get; set; }
|
public RateBook WalletRateBook { get; set; }
|
||||||
public RateBook InvoiceRateBook { get; set; }
|
public RateBook InvoiceRateBook { get; set; }
|
||||||
public string InvoiceId { get; set; }
|
public string InvoiceId { get; set; }
|
||||||
|
public TransactionHistoryLine HistoryLine { get; set; }
|
||||||
}
|
}
|
||||||
public HashSet<(string Text, string Color, string TextColor)> Labels { get; set; } = new();
|
public HashSet<(string Text, string Color, string TextColor)> Labels { get; set; } = new();
|
||||||
public List<TransactionViewModel> Transactions { get; set; } = new();
|
public List<TransactionViewModel> Transactions { get; set; } = new();
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using BTCPayServer.Payments.Bitcoin;
|
|||||||
using BTCPayServer.Rating;
|
using BTCPayServer.Rating;
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
using BTCPayServer.Services.Stores;
|
using BTCPayServer.Services.Stores;
|
||||||
|
using BTCPayServer.Services.Wallets;
|
||||||
using Dapper;
|
using Dapper;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
|
|
||||||
@@ -19,7 +20,8 @@ public class OnChainWalletReportProvider(
|
|||||||
StoreRepository storeRepository,
|
StoreRepository storeRepository,
|
||||||
InvoiceRepository invoiceRepository,
|
InvoiceRepository invoiceRepository,
|
||||||
PaymentMethodHandlerDictionary handlers,
|
PaymentMethodHandlerDictionary handlers,
|
||||||
WalletRepository walletRepository)
|
WalletRepository walletRepository,
|
||||||
|
BTCPayWalletProvider walletProvider)
|
||||||
: ReportProvider
|
: ReportProvider
|
||||||
{
|
{
|
||||||
public override string Name => "Wallets";
|
public override string Name => "Wallets";
|
||||||
@@ -37,6 +39,8 @@ public class OnChainWalletReportProvider(
|
|||||||
new("InvoiceId", "invoice_id"),
|
new("InvoiceId", "invoice_id"),
|
||||||
new("Confirmed", "boolean"),
|
new("Confirmed", "boolean"),
|
||||||
new("BalanceChange", "amount"),
|
new("BalanceChange", "amount"),
|
||||||
|
new("Fee", "amount"),
|
||||||
|
new("FeeRate", "number"),
|
||||||
},
|
},
|
||||||
Charts =
|
Charts =
|
||||||
{
|
{
|
||||||
@@ -68,10 +72,12 @@ public class OnChainWalletReportProvider(
|
|||||||
var network = ((IHasNetwork)handlers[pmi]).Network;
|
var network = ((IHasNetwork)handlers[pmi]).Network;
|
||||||
cryptoCodes.Add(network.CryptoCode);
|
cryptoCodes.Add(network.CryptoCode);
|
||||||
var walletId = new WalletId(store.Id, network.CryptoCode);
|
var walletId = new WalletId(store.Id, network.CryptoCode);
|
||||||
|
|
||||||
|
var selectFee = walletProvider.GetWallet(network).SelectFeeColumns();
|
||||||
var command = new CommandDefinition(
|
var command = new CommandDefinition(
|
||||||
commandText:
|
commandText:
|
||||||
"""
|
$"""
|
||||||
SELECT r.tx_id, r.seen_at, t.blk_id, t.blk_height, r.balance_change
|
SELECT r.tx_id, r.seen_at, t.blk_id, t.blk_height, r.balance_change, {selectFee}
|
||||||
FROM get_wallets_recent(@wallet_id, @code, @asset_id, @interval, NULL, NULL) r
|
FROM get_wallets_recent(@wallet_id, @code, @asset_id, @interval, NULL, NULL) r
|
||||||
JOIN txs t USING (code, tx_id)
|
JOIN txs t USING (code, tx_id)
|
||||||
ORDER BY r.seen_at
|
ORDER BY r.seen_at
|
||||||
@@ -99,6 +105,11 @@ public class OnChainWalletReportProvider(
|
|||||||
values.Add(null);
|
values.Add(null);
|
||||||
values.Add((long?)r.blk_height is not null);
|
values.Add((long?)r.blk_height is not null);
|
||||||
values.Add(new FormattedAmount(balanceChange, network.Divisibility).ToJObject());
|
values.Add(new FormattedAmount(balanceChange, network.Divisibility).ToJObject());
|
||||||
|
|
||||||
|
decimal? fee = r.fee is null ? null : Money.Satoshis((long)r.fee).ToDecimal(MoneyUnit.BTC);
|
||||||
|
decimal? feeRate = r.feerate is null ? null : (decimal)r.feerate;
|
||||||
|
values.Add(fee is null ? null : new FormattedAmount(fee.Value, network.Divisibility).ToJObject());
|
||||||
|
values.Add(feeRate);
|
||||||
}
|
}
|
||||||
|
|
||||||
var objects = await walletRepository.GetWalletObjects(new GetWalletObjectsQuery
|
var objects = await walletRepository.GetWalletObjects(new GetWalletObjectsQuery
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ namespace BTCPayServer.Services.Wallets
|
|||||||
}
|
}
|
||||||
return await completionSource.Task;
|
return await completionSource.Task;
|
||||||
}
|
}
|
||||||
|
public bool ForceInefficientPath { get; set; }
|
||||||
List<TransactionInformation> dummy = new List<TransactionInformation>();
|
List<TransactionInformation> dummy = new List<TransactionInformation>();
|
||||||
public async Task<IList<TransactionHistoryLine>> FetchTransactionHistory(DerivationStrategyBase derivationStrategyBase, int? skip = null, int? count = null, TimeSpan? interval = null, CancellationToken cancellationToken = default)
|
public async Task<IList<TransactionHistoryLine>> FetchTransactionHistory(DerivationStrategyBase derivationStrategyBase, int? skip = null, int? count = null, TimeSpan? interval = null, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -259,7 +260,7 @@ namespace BTCPayServer.Services.Wallets
|
|||||||
// * 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.
|
// * 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.
|
||||||
// * Sometimes we can't query the DB or the given network need to do additional filtering. In such case, we can't really filter at the DB level, and we need to fetch all transactions in memory.
|
// * Sometimes we can't query the DB or the given network need to do additional filtering. In such case, we can't really filter at the DB level, and we need to fetch all transactions in memory.
|
||||||
var needAdditionalFiltering = _Network.FilterValidTransactions(dummy) != dummy;
|
var needAdditionalFiltering = _Network.FilterValidTransactions(dummy) != dummy;
|
||||||
if (!NbxplorerConnectionFactory.Available || needAdditionalFiltering)
|
if (ForceInefficientPath || !NbxplorerConnectionFactory.Available || needAdditionalFiltering)
|
||||||
{
|
{
|
||||||
var txs = await FetchTransactions(derivationStrategyBase);
|
var txs = await FetchTransactions(derivationStrategyBase);
|
||||||
var txinfos = txs.UnconfirmedTransactions.Transactions.Concat(txs.ConfirmedTransactions.Transactions)
|
var txinfos = txs.UnconfirmedTransactions.Transactions.Concat(txs.ConfirmedTransactions.Transactions)
|
||||||
@@ -282,7 +283,7 @@ namespace BTCPayServer.Services.Wallets
|
|||||||
{
|
{
|
||||||
await using var ctx = await NbxplorerConnectionFactory.OpenConnection();
|
await using var ctx = await NbxplorerConnectionFactory.OpenConnection();
|
||||||
var cmd = new CommandDefinition(
|
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 " +
|
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, {SelectFeeColumns()} " +
|
||||||
"FROM get_wallets_recent(@wallet_id, @code, @interval, @count, @skip) r " +
|
"FROM get_wallets_recent(@wallet_id, @code, @interval, @count, @skip) r " +
|
||||||
"JOIN txs t USING (code, tx_id) " +
|
"JOIN txs t USING (code, tx_id) " +
|
||||||
"ORDER BY r.seen_at DESC",
|
"ORDER BY r.seen_at DESC",
|
||||||
@@ -295,7 +296,7 @@ namespace BTCPayServer.Services.Wallets
|
|||||||
interval = interval is TimeSpan t ? t : TimeSpan.FromDays(365 * 1000)
|
interval = interval is TimeSpan t ? t : TimeSpan.FromDays(365 * 1000)
|
||||||
},
|
},
|
||||||
cancellationToken: cancellationToken);
|
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);
|
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, long? fee, decimal? feerate)>(cmd);
|
||||||
rows.TryGetNonEnumeratedCount(out int c);
|
rows.TryGetNonEnumeratedCount(out int c);
|
||||||
var lines = new List<TransactionHistoryLine>(c);
|
var lines = new List<TransactionHistoryLine>(c);
|
||||||
foreach (var row in rows)
|
foreach (var row in rows)
|
||||||
@@ -307,12 +308,22 @@ namespace BTCPayServer.Services.Wallets
|
|||||||
SeenAt = row.seen_at,
|
SeenAt = row.seen_at,
|
||||||
TransactionId = uint256.Parse(row.tx_id),
|
TransactionId = uint256.Parse(row.tx_id),
|
||||||
Confirmations = row.confs,
|
Confirmations = row.confs,
|
||||||
BlockHash = string.IsNullOrEmpty(row.blk_id) ? null : uint256.Parse(row.blk_id)
|
BlockHash = string.IsNullOrEmpty(row.blk_id) ? null : uint256.Parse(row.blk_id),
|
||||||
|
Fee = row.fee is null ? null : Money.Satoshis(row.fee.Value),
|
||||||
|
FeeRate = row.feerate is null ? null : new FeeRate(row.feerate.Value)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal string SelectFeeColumns()
|
||||||
|
=> HasFeeInformation() ? "(metadata->'fees')::BIGINT AS fee, (metadata->'feeRate')::NUMERIC AS feerate" : "NULL AS fee, NULL AS feerate";
|
||||||
|
|
||||||
|
public bool? ForceHasFeeInformation { get; set; }
|
||||||
|
internal bool HasFeeInformation()
|
||||||
|
=> ForceHasFeeInformation ?? AsVersion(this.Dashboard.Get(Network.CryptoCode)?.Status?.Version ?? "") >= new Version("2.5.18");
|
||||||
|
|
||||||
public async Task<BumpableTransactions> GetBumpableTransactions(DerivationStrategyBase derivationStrategyBase, CancellationToken cancellationToken)
|
public async Task<BumpableTransactions> GetBumpableTransactions(DerivationStrategyBase derivationStrategyBase, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var result = new BumpableTransactions();
|
var result = new BumpableTransactions();
|
||||||
@@ -440,7 +451,9 @@ namespace BTCPayServer.Services.Wallets
|
|||||||
Confirmations = t.Confirmations,
|
Confirmations = t.Confirmations,
|
||||||
Height = t.Height,
|
Height = t.Height,
|
||||||
SeenAt = t.Timestamp,
|
SeenAt = t.Timestamp,
|
||||||
TransactionId = t.TransactionId
|
TransactionId = t.TransactionId,
|
||||||
|
Fee = t.Metadata?.Fees,
|
||||||
|
FeeRate = t.Metadata?.FeeRate
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -570,5 +583,7 @@ namespace BTCPayServer.Services.Wallets
|
|||||||
public uint256 TransactionId { get; set; }
|
public uint256 TransactionId { get; set; }
|
||||||
public uint256 BlockHash { get; set; }
|
public uint256 BlockHash { get; set; }
|
||||||
public IMoney BalanceChange { get; set; }
|
public IMoney BalanceChange { get; set; }
|
||||||
|
public FeeRate FeeRate { get; set; }
|
||||||
|
public Money Fee { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -234,6 +234,7 @@
|
|||||||
<th text-translate="true" style="min-width:125px">Label</th>
|
<th text-translate="true" style="min-width:125px">Label</th>
|
||||||
<th text-translate="true">Transaction</th>
|
<th text-translate="true">Transaction</th>
|
||||||
<th text-translate="true" class="amount-col">Amount</th>
|
<th text-translate="true" class="amount-col">Amount</th>
|
||||||
|
<th text-translate="true" class="fee-col">Transaction fee</th>
|
||||||
@foreach (var rate in Model.Rates)
|
@foreach (var rate in Model.Rates)
|
||||||
{
|
{
|
||||||
<th class="rate-col"><span text-translate="true">Rate</span> <span>(@rate)</span></th>
|
<th class="rate-col"><span text-translate="true">Rate</span> <span>(@rate)</span></th>
|
||||||
|
|||||||
@@ -27,6 +27,14 @@
|
|||||||
<td class="align-middle amount-col">
|
<td class="align-middle amount-col">
|
||||||
<span data-sensitive class="text-@(transaction.Positive ? "success" : "danger")@(transaction.IsConfirmed ? "" : " opacity-50")">@transaction.Balance</span>
|
<span data-sensitive class="text-@(transaction.Positive ? "success" : "danger")@(transaction.IsConfirmed ? "" : " opacity-50")">@transaction.Balance</span>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="align-middle fee-col">
|
||||||
|
<span data-sensitive class="@(transaction.IsConfirmed ? "" : " opacity-50")">
|
||||||
|
@if (transaction.HistoryLine is { Fee: { } fee, FeeRate: { } fr })
|
||||||
|
{
|
||||||
|
<span><span>@fee</span> <span class="text-muted">(@fr)</span></span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
@foreach (var rate in transaction.Rates)
|
@foreach (var rate in transaction.Rates)
|
||||||
{
|
{
|
||||||
<td class="align-middle rate-col">
|
<td class="align-middle rate-col">
|
||||||
|
|||||||
@@ -1182,6 +1182,10 @@ input.ts-wrapper.form-control:not(.ts-hidden-accessible,.ts-inline) {
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
.fee-col {
|
||||||
|
text-align: right;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
.rate-col {
|
.rate-col {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|||||||
Reference in New Issue
Block a user