Can inject currency data in CurrencyNameTable (#6276)

This commit is contained in:
Nicolas Dorier 2024-10-04 22:24:44 +09:00 committed by GitHub
parent 206d222455
commit 64ba8248d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 179 additions and 53 deletions

View File

@ -1175,13 +1175,6 @@
"symbol":null,
"crypto":true
},
{
"name":"USDt",
"code":"USDT",
"divisibility":8,
"symbol":null,
"crypto":true
},
{
"name":"LCAD",
"code":"LCAD",
@ -1315,13 +1308,6 @@
"symbol": null,
"crypto": true
},
{
"name":"USDt",
"code":"USDT20",
"divisibility":6,
"symbol":null,
"crypto":true
},
{
"name":"FaucetToken",
"code":"FAU",

View File

@ -5,6 +5,9 @@ using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using NBitcoin;
using Newtonsoft.Json;
@ -18,14 +21,74 @@ namespace BTCPayServer.Services.Rates
public string Symbol { get; set; }
public bool Crypto { get; set; }
}
public class CurrencyNameTable
public interface CurrencyDataProvider
{
public static CurrencyNameTable Instance = new();
public CurrencyNameTable()
Task<CurrencyData[]> LoadCurrencyData(CancellationToken cancellationToken);
}
public class InMemoryCurrencyDataProvider : CurrencyDataProvider
{
private readonly CurrencyData[] _currencyData;
public InMemoryCurrencyDataProvider(CurrencyData[] currencyData)
{
_Currencies = LoadCurrency().ToDictionary(k => k.Code, StringComparer.InvariantCultureIgnoreCase);
_currencyData = currencyData;
}
public Task<CurrencyData[]> LoadCurrencyData(CancellationToken cancellationToken) => Task.FromResult(_currencyData);
}
public class AssemblyCurrencyDataProvider : CurrencyDataProvider
{
private readonly Assembly _assembly;
private readonly string _manifestResourceStream;
public AssemblyCurrencyDataProvider(Assembly assembly, string manifestResourceStream)
{
_assembly = assembly;
_manifestResourceStream = manifestResourceStream;
}
public Task<CurrencyData[]> LoadCurrencyData(CancellationToken cancellationToken)
{
var stream = _assembly.GetManifestResourceStream(_manifestResourceStream);
if (stream is null)
throw new InvalidOperationException("Unknown manifestResourceStream");
string content = null;
using (var reader = new StreamReader(stream, Encoding.UTF8))
{
content = reader.ReadToEnd();
}
var currencies = JsonConvert.DeserializeObject<CurrencyData[]>(content);
return Task.FromResult(currencies.ToArray());
}
}
public class CurrencyNameTable
{
public CurrencyNameTable(IEnumerable<CurrencyDataProvider> currencyDataProviders, ILogger<CurrencyNameTable> logger)
{
_currencyDataProviders = currencyDataProviders;
_logger = logger;
}
public async Task ReloadCurrencyData(CancellationToken cancellationToken)
{
var currencies = new Dictionary<string, CurrencyData>(StringComparer.InvariantCultureIgnoreCase);
var loadings = _currencyDataProviders.Select(c => (Task: c.LoadCurrencyData(cancellationToken), Prov: c)).ToList();
foreach (var loading in loadings)
{
try
{
foreach (var curr in await loading.Task)
{
currencies.TryAdd(curr.Code, curr);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error loading currency data for " + loading.Prov.GetType().FullName);
}
}
_Currencies = currencies;
}
static readonly Dictionary<string, IFormatProvider> _CurrencyProviders = new();
public NumberFormatInfo GetNumberFormatInfo(string currency, bool useFallback)
@ -123,20 +186,9 @@ namespace BTCPayServer.Services.Rates
currencyProviders.TryAdd(code, number);
}
readonly Dictionary<string, CurrencyData> _Currencies;
static CurrencyData[] LoadCurrency()
{
var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("BTCPayServer.Rating.Currencies.json");
string content = null;
using (var reader = new StreamReader(stream, Encoding.UTF8))
{
content = reader.ReadToEnd();
}
var currencies = JsonConvert.DeserializeObject<CurrencyData[]>(content);
return currencies;
}
Dictionary<string, CurrencyData> _Currencies = new();
private readonly IEnumerable<CurrencyDataProvider> _currencyDataProviders;
private readonly ILogger<CurrencyNameTable> _logger;
public IEnumerable<CurrencyData> Currencies => _Currencies.Values;

View File

@ -1,11 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using BTCPayServer.Services.Rates;
namespace BTCPayServer.Rating
{
public class CurrencyPair
{
private static readonly HashSet<string> _knownCurrencies;
static CurrencyPair()
{
var prov = new AssemblyCurrencyDataProvider(typeof(BTCPayServer.Rating.BidAsk).Assembly, "BTCPayServer.Rating.Currencies.json");
// It's OK this is sync function
_knownCurrencies = prov.LoadCurrencyData(default).GetAwaiter().GetResult()
.Select(c => c.Code).ToHashSet(StringComparer.OrdinalIgnoreCase);
}
public CurrencyPair(string left, string right)
{
ArgumentNullException.ThrowIfNull(right);
@ -49,10 +60,9 @@ namespace BTCPayServer.Rating
for (int i = 3; i < 5; i++)
{
var potentialCryptoName = currencyPair.Substring(0, i);
var currency = CurrencyNameTable.Instance.GetCurrencyData(potentialCryptoName, false);
if (currency != null)
if (_knownCurrencies.Contains(potentialCryptoName))
{
value = new CurrencyPair(currency.Code, currencyPair.Substring(i));
value = new CurrencyPair(potentialCryptoName, currencyPair.Substring(i));
return true;
}
}

View File

@ -40,6 +40,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NBitcoin;
using NBitcoin.DataEncoders;
@ -681,7 +682,6 @@ namespace BTCPayServer.Tests
[Fact]
public void CanAcceptInvoiceWithTolerance()
{
var networkProvider = CreateNetworkProvider(ChainName.Regtest);
var entity = new InvoiceEntity() { Currency = "USD" };
#pragma warning disable CS0618
entity.Payments = new List<PaymentEntity>();
@ -738,10 +738,29 @@ namespace BTCPayServer.Tests
Assert.True(FileTypeDetector.IsAudio(new byte[] { 0xFF, 0xF3, 0xE4, 0x64, 0x00, 0x20, 0xAD, 0xBD, 0x04, 0x00 }, "music.mp3"));
}
CurrencyNameTable GetCurrencyNameTable()
{
ServiceCollection services = new ServiceCollection();
services.AddLogging(o => o.AddProvider(this.TestLogProvider));
BTCPayServerServices.RegisterCurrencyData(services);
// One test fail without.
services.AddCurrencyData(new CurrencyData()
{
Code = "USDt",
Name = "USDt",
Divisibility = 8,
Symbol = null,
Crypto = true
});
var table = services.BuildServiceProvider().GetRequiredService<CurrencyNameTable>();
table.ReloadCurrencyData(default).GetAwaiter().GetResult();
return table;
}
[Fact]
public void RoundupCurrenciesCorrectly()
{
DisplayFormatter displayFormatter = new(CurrencyNameTable.Instance);
DisplayFormatter displayFormatter = new(GetCurrencyNameTable());
foreach (var test in new[]
{
(0.0005m, "0.0005 USD", "USD"), (0.001m, "0.001 USD", "USD"), (0.01m, "0.01 USD", "USD"),
@ -754,8 +773,8 @@ namespace BTCPayServer.Tests
actual = actual.Replace("¥", "¥"); // Hack so JPY test pass on linux as well
Assert.Equal(test.Item2, actual);
}
Assert.Equal(0, CurrencyNameTable.Instance.GetNumberFormatInfo("ARS").CurrencyDecimalDigits);
Assert.Equal(0, CurrencyNameTable.Instance.GetNumberFormatInfo("COP").CurrencyDecimalDigits);
Assert.Equal(0, GetCurrencyNameTable().GetNumberFormatInfo("ARS").CurrencyDecimalDigits);
Assert.Equal(0, GetCurrencyNameTable().GetNumberFormatInfo("COP").CurrencyDecimalDigits);
}
[Fact]
@ -1377,7 +1396,7 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
var btcPayNetworkProvider = CreateNetworkProvider(ChainName.Regtest);
foreach (var network in btcPayNetworkProvider.GetAll())
{
var cd = CurrencyNameTable.Instance.GetCurrencyData(network.CryptoCode, false);
var cd = GetCurrencyNameTable().GetCurrencyData(network.CryptoCode, false);
Assert.NotNull(cd);
Assert.Equal(network.Divisibility, cd.Divisibility);
Assert.True(cd.Crypto);
@ -1445,8 +1464,8 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
Assert.True(CurrencyValue.TryParse("1usd", out result));
Assert.Equal("1 USD", result.ToString());
Assert.True(CurrencyValue.TryParse("1.501 usd", out result));
Assert.Equal("1.50 USD", result.ToString());
Assert.False(CurrencyValue.TryParse("1.501 WTFF", out result));
Assert.Equal("1.501 USD", result.ToString());
Assert.True(CurrencyValue.TryParse("1.501 WTFF", out result));
Assert.False(CurrencyValue.TryParse("1,501 usd", out result));
Assert.False(CurrencyValue.TryParse("1.501", out result));
}

View File

@ -1543,7 +1543,7 @@ namespace BTCPayServer.Tests
var vm = await user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModelAsync<CheckoutAppearanceViewModel>();
Assert.Equal(2, vm.PaymentMethodCriteria.Count);
var criteria = Assert.Single(vm.PaymentMethodCriteria.Where(m => m.PaymentMethod == btcMethod.ToString()));
Assert.Equal(PaymentTypes.CHAIN.GetPaymentMethodId("BTC").ToString(), criteria.PaymentMethod);
Assert.Equal(btcMethod.ToString(), criteria.PaymentMethod);
criteria.Value = "5 USD";
criteria.Type = PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan;
Assert.IsType<RedirectToActionResult>(user.GetController<UIStoresController>().CheckoutAppearance(vm)

View File

@ -27,17 +27,20 @@ namespace BTCPayServer.Controllers.Greenfield
public class GreenfieldStoresController : ControllerBase
{
private readonly StoreRepository _storeRepository;
private readonly CurrencyNameTable _currencyNameTable;
private readonly UserManager<ApplicationUser> _userManager;
private readonly IFileService _fileService;
private readonly UriResolver _uriResolver;
public GreenfieldStoresController(
StoreRepository storeRepository,
CurrencyNameTable currencyNameTable,
UserManager<ApplicationUser> userManager,
IFileService fileService,
UriResolver uriResolver)
{
_storeRepository = storeRepository;
_currencyNameTable = currencyNameTable;
_userManager = userManager;
_fileService = fileService;
_uriResolver = uriResolver;
@ -335,7 +338,7 @@ namespace BTCPayServer.Controllers.Greenfield
{
request.AddModelError(data => data.PaymentMethodCriteria[index].CurrencyCode, "CurrencyCode is required", this);
}
else if (CurrencyNameTable.Instance.GetCurrencyData(pmc.CurrencyCode, false) is null)
else if (_currencyNameTable.GetCurrencyData(pmc.CurrencyCode, false) is null)
{
request.AddModelError(data => data.PaymentMethodCriteria[index].CurrencyCode, "CurrencyCode is invalid", this);
}

View File

@ -875,9 +875,10 @@ namespace BTCPayServer.Controllers
return extension?.Image ?? "";
}
// Show the "Common divisibility" rather than the payment method disibility.
// For example, BTC has commonly 8 digits, but on lightning it has 11. In this case, pick 8.
if (this._CurrencyNameTable.GetCurrencyData(prompt.Currency, false)?.Divisibility is not int divisibility)
var cd = this._CurrencyNameTable.GetCurrencyData(prompt.Currency, false);
// Show the "Common divisibility" rather than the payment method disibility.
// For example, BTC has commonly 8 digits, but on lightning it has 11. In this case, pick 8.
if (cd?.Divisibility is not int divisibility)
divisibility = prompt.Divisibility;
string ShowMoney(decimal value) => MoneyExtensions.ShowMoney(value, divisibility);

View File

@ -368,7 +368,12 @@ public partial class UIStoresController
var existingCriteria = blob.PaymentMethodCriteria.FirstOrDefault(c => c.PaymentMethod == paymentMethodId);
if (existingCriteria != null)
blob.PaymentMethodCriteria.Remove(existingCriteria);
CurrencyValue.TryParse(newCriteria.Value, out var cv);
if (CurrencyValue.TryParse(newCriteria.Value, out var cv))
{
var currencyData = _currencyNameTable.GetCurrencyData(cv.Currency, false);
if (currencyData is not null)
cv = cv.Round(currencyData.Divisibility);
}
blob.PaymentMethodCriteria.Add(new PaymentMethodCriteria()
{
Above = newCriteria.Type == PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan,

View File

@ -60,6 +60,7 @@ public partial class UIStoresController : Controller
WalletFileParsers onChainWalletParsers,
UriResolver uriResolver,
SettingsRepository settingsRepository,
CurrencyNameTable currencyNameTable,
EventAggregator eventAggregator)
{
_rateFactory = rateFactory;
@ -83,6 +84,7 @@ public partial class UIStoresController : Controller
_onChainWalletParsers = onChainWalletParsers;
_uriResolver = uriResolver;
_settingsRepository = settingsRepository;
_currencyNameTable = currencyNameTable;
_eventAggregator = eventAggregator;
_html = html;
_defaultRules = defaultRules;
@ -101,6 +103,7 @@ public partial class UIStoresController : Controller
private readonly UserManager<ApplicationUser> _userManager;
private readonly RateFetcher _rateFactory;
private readonly SettingsRepository _settingsRepository;
private readonly CurrencyNameTable _currencyNameTable;
private readonly ExplorerClientProvider _explorerProvider;
private readonly LanguageService _langService;
private readonly PaymentMethodHandlerDictionary _handlers;

View File

@ -21,10 +21,6 @@ namespace BTCPayServer
return false;
var currency = match.Groups[match.Groups.Count - 1].Value.ToUpperInvariant();
var currencyData = CurrencyNameTable.Instance.GetCurrencyData(currency, false);
if (currencyData == null)
return false;
v = Math.Round(v, currencyData.Divisibility);
value = new CurrencyValue()
{
Value = v,
@ -40,5 +36,11 @@ namespace BTCPayServer
{
return Value.ToString(CultureInfo.InvariantCulture) + " " + Currency;
}
public CurrencyValue Round(int divisibility) => new()
{
Value = Math.Round(Value, divisibility),
Currency = Currency
};
}
}

View File

@ -73,6 +73,7 @@ using BTCPayServer.Payouts;
using ExchangeSharp;
using Microsoft.Extensions.Localization;
using Microsoft.AspNetCore.Mvc.Localization;
using System.Reflection;
namespace BTCPayServer.Hosting
{
@ -161,6 +162,7 @@ namespace BTCPayServer.Hosting
services.AddSingleton<IUIExtension>(new UIExtension("Lightning/ViewLightningLikePaymentData", "store-invoices-payments"));
services.AddStartupTask<BlockExplorerLinkStartupTask>();
services.AddStartupTask<LoadCurrencyNameTableStartupTask>();
services.AddStartupTask<LoadTranslationsStartupTask>();
services.TryAddSingleton<InvoiceRepository>();
services.AddSingleton<PaymentService>();
@ -352,7 +354,8 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton<BTCPayWalletProvider>();
services.TryAddSingleton<WalletReceiveService>();
services.AddSingleton<IHostedService>(provider => provider.GetService<WalletReceiveService>());
services.TryAddSingleton<CurrencyNameTable>(CurrencyNameTable.Instance);
RegisterCurrencyData(services);
services.AddScheduledTask<FeeProviderFactory>(TimeSpan.FromMinutes(3.0));
services.AddSingleton<IFeeProviderFactory, FeeProviderFactory>(f => f.GetRequiredService<FeeProviderFactory>());
@ -548,6 +551,12 @@ o.GetRequiredService<IEnumerable<IPaymentLinkExtension>>().ToDictionary(o => o.P
services.AddSingleton<IWalletFileParser, WasabiWalletFileParser>();
}
internal static void RegisterCurrencyData(IServiceCollection services)
{
services.TryAddSingleton<CurrencyNameTable>();
services.AddSingleton<CurrencyDataProvider, AssemblyCurrencyDataProvider>(c => new AssemblyCurrencyDataProvider(typeof(BTCPayServer.Rating.BidAsk).Assembly, "BTCPayServer.Rating.Currencies.json"));
}
internal static void RegisterRateSources(IServiceCollection services)
{
// We need to be careful to only add exchanges which OnGetTickers implementation make only 1 request
@ -601,6 +610,12 @@ o.GetRequiredService<IEnumerable<IPaymentLinkExtension>>().ToDictionary(o => o.P
services.AddSingleton<BTCPayNetworkBase>(network);
return services;
}
public static IServiceCollection AddCurrencyData(this IServiceCollection services, params CurrencyData[] currencyData)
{
services.AddSingleton<CurrencyDataProvider, InMemoryCurrencyDataProvider>(c => new InMemoryCurrencyDataProvider(currencyData));
return services;
}
public static IServiceCollection AddBTCPayNetwork(this IServiceCollection services, BTCPayNetwork network)
{
services.AddSingleton(new DefaultRules(network.DefaultRateRules));

View File

@ -0,0 +1,21 @@
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Services.Rates;
namespace BTCPayServer.Hosting
{
public class LoadCurrencyNameTableStartupTask : IStartupTask
{
private readonly CurrencyNameTable _currencyNameTable;
public LoadCurrencyNameTableStartupTask(CurrencyNameTable currencyNameTable)
{
_currencyNameTable = currencyNameTable;
}
public async Task ExecuteAsync(CancellationToken cancellationToken = default)
{
await _currencyNameTable.ReloadCurrencyData(cancellationToken);
}
}
}

View File

@ -2,6 +2,7 @@ using System.Threading;
using BTCPayServer.Hosting;
using BTCPayServer.Payments;
using BTCPayServer.Services;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.DependencyInjection;
using NBitcoin;
@ -33,6 +34,14 @@ public partial class AltcoinsPlugin
}.SetDefaultElectrumMapping(ChainName);
services.AddBTCPayNetwork(network)
.AddTransactionLinkProvider(PaymentTypes.CHAIN.GetPaymentMethodId(nbxplorerNetwork.CryptoCode), new DefaultTransactionLinkProvider(LiquidBlockExplorer));
services.AddCurrencyData(new CurrencyData()
{
Code = "USDt",
Name = "USDt",
Divisibility = 8,
Symbol = null,
Crypto = true
});
selectedChains.Add("LBTC");
}