Compare commits
11 Commits
Author | SHA1 | Date | |
---|---|---|---|
ac70a77361 | |||
59a2432af9 | |||
ea4fa8d5d4 | |||
ade3eff75c | |||
db2a2a2b6c | |||
579dcb5af8 | |||
69247dee8a | |||
7b9541b8e9 | |||
a12e4d7f64 | |||
897da9b07a | |||
293525d480 |
@ -97,7 +97,12 @@ namespace BTCPayServer.Tests
|
||||
.UseConfiguration(conf)
|
||||
.ConfigureServices(s =>
|
||||
{
|
||||
s.AddSingleton<IRateProvider>(new MockRateProvider(new Rate("USD", 5000m)));
|
||||
var mockRates = new MockRateProviderFactory();
|
||||
var btc = new MockRateProvider("BTC", new Rate("USD", 5000m));
|
||||
var ltc = new MockRateProvider("LTC", new Rate("USD", 500m));
|
||||
mockRates.AddMock(btc);
|
||||
mockRates.AddMock(ltc);
|
||||
s.AddSingleton<IRateProviderFactory>(mockRates);
|
||||
s.AddLogging(l =>
|
||||
{
|
||||
l.SetMinimumLevel(LogLevel.Information)
|
||||
|
@ -519,6 +519,7 @@ namespace BTCPayServer.Tests
|
||||
}, Facade.Merchant);
|
||||
|
||||
var cashCow = tester.ExplorerNode;
|
||||
cashCow.Generate(2); // get some money in case
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
|
||||
var firstPayment = Money.Coins(0.04m);
|
||||
cashCow.SendToAddress(invoiceAddress, firstPayment);
|
||||
@ -553,6 +554,7 @@ namespace BTCPayServer.Tests
|
||||
invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
|
||||
firstPayment = Money.Coins(0.04m);
|
||||
cashCow.SendToAddress(invoiceAddress, firstPayment);
|
||||
Logs.Tester.LogInformation("First payment sent to " + invoiceAddress);
|
||||
Eventually(() =>
|
||||
{
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
@ -566,7 +568,7 @@ namespace BTCPayServer.Tests
|
||||
var secondPayment = Money.Coins(decimal.Parse(ltcCryptoInfo.Due));
|
||||
cashCow.Generate(2); // LTC is not worth a lot, so just to make sure we have money...
|
||||
cashCow.SendToAddress(invoiceAddress, secondPayment);
|
||||
|
||||
Logs.Tester.LogInformation("Second payment sent to " + invoiceAddress);
|
||||
Eventually(() =>
|
||||
{
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
@ -765,7 +767,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
private void Eventually(Action act)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource(10000);
|
||||
CancellationTokenSource cts = new CancellationTokenSource(20000);
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
|
@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<Version>1.0.1.13</Version>
|
||||
<Version>1.0.1.18</Version>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="Build\dockerfiles\**" />
|
||||
|
@ -162,6 +162,7 @@ namespace BTCPayServer.Controllers
|
||||
CustomerEmail = invoice.RefundMail,
|
||||
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
|
||||
MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds,
|
||||
MaxTimeMinutes = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalMinutes,
|
||||
ItemDesc = invoice.ProductInformation.ItemDesc,
|
||||
Rate = FormatCurrency(cryptoData),
|
||||
MerchantRefLink = invoice.RedirectURL ?? "/",
|
||||
|
@ -80,9 +80,9 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl)
|
||||
{
|
||||
var derivationStrategies = store.GetDerivationStrategies(_NetworkProvider).ToList();
|
||||
var derivationStrategies = store.GetDerivationStrategies(_NetworkProvider).Where(c => _ExplorerClients.IsAvailable(c.Network.CryptoCode)).ToList();
|
||||
if (derivationStrategies.Count == 0)
|
||||
throw new BitpayHttpException(400, "This store has not configured the derivation strategy");
|
||||
throw new BitpayHttpException(400, "No derivation strategy are available now for this store");
|
||||
var entity = new InvoiceEntity
|
||||
{
|
||||
InvoiceTime = DateTimeOffset.UtcNow
|
||||
|
@ -7,34 +7,68 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class RateController : Controller
|
||||
{
|
||||
IRateProvider _RateProvider;
|
||||
IRateProviderFactory _RateProviderFactory;
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
CurrencyNameTable _CurrencyNameTable;
|
||||
public RateController(IRateProvider rateProvider, CurrencyNameTable currencyNameTable)
|
||||
StoreRepository _StoreRepo;
|
||||
public RateController(
|
||||
IRateProviderFactory rateProviderFactory,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
StoreRepository storeRepo,
|
||||
CurrencyNameTable currencyNameTable)
|
||||
{
|
||||
_RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider));
|
||||
_RateProviderFactory = rateProviderFactory ?? throw new ArgumentNullException(nameof(rateProviderFactory));
|
||||
_NetworkProvider = networkProvider;
|
||||
_StoreRepo = storeRepo;
|
||||
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
|
||||
}
|
||||
|
||||
[Route("rates")]
|
||||
[HttpGet]
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<DataWrapper<NBitpayClient.Rate[]>> GetRates()
|
||||
public async Task<IActionResult> GetRates(string cryptoCode = null, string storeId = null)
|
||||
{
|
||||
var allRates = (await _RateProvider.GetRatesAsync());
|
||||
return new DataWrapper<NBitpayClient.Rate[]>
|
||||
(allRates.Select(r =>
|
||||
var result = await GetRates2(cryptoCode, storeId);
|
||||
var rates = (result as JsonResult)?.Value as NBitpayClient.Rate[];
|
||||
if(rates == null)
|
||||
return result;
|
||||
return Json(new DataWrapper<NBitpayClient.Rate[]>(rates));
|
||||
}
|
||||
|
||||
[Route("api/rates")]
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetRates2(string cryptoCode = null, string storeId = null)
|
||||
{
|
||||
cryptoCode = cryptoCode ?? "BTC";
|
||||
var network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||
if (network == null)
|
||||
return NotFound();
|
||||
var rateProvider = _RateProviderFactory.GetRateProvider(network);
|
||||
if (rateProvider == null)
|
||||
return NotFound();
|
||||
|
||||
if (storeId != null)
|
||||
{
|
||||
var store = await _StoreRepo.FindStore(storeId);
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
rateProvider = store.GetStoreBlob().ApplyRateRules(network, rateProvider);
|
||||
}
|
||||
|
||||
var allRates = (await rateProvider.GetRatesAsync());
|
||||
return Json(allRates.Select(r =>
|
||||
new NBitpayClient.Rate()
|
||||
{
|
||||
Code = r.Currency,
|
||||
Name = _CurrencyNameTable.GetCurrencyData(r.Currency)?.Name,
|
||||
Value = r.Value
|
||||
}).Where(n => n.Name != null).ToArray());
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -166,7 +166,7 @@ namespace BTCPayServer.Controllers
|
||||
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
|
||||
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
|
||||
vm.RateMultiplier = (double)storeBlob.GetRateMultiplier();
|
||||
vm.PreferredExchange = storeBlob.PreferredExchange;
|
||||
vm.PreferredExchange = storeBlob.PreferredExchange.IsCoinAverage() ? "coinaverage" : storeBlob.PreferredExchange;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
@ -243,7 +243,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
await _Repo.UpdateStore(store);
|
||||
StatusMessage = $"Derivation scheme for {vm.CryptoCurrency} has been modified.";
|
||||
StatusMessage = $"Derivation scheme for {network.CryptoCode} has been modified.";
|
||||
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
|
||||
}
|
||||
else
|
||||
@ -278,6 +278,8 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
if (model.PreferredExchange != null)
|
||||
model.PreferredExchange = model.PreferredExchange.Trim().ToLowerInvariant();
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
@ -311,6 +313,8 @@ namespace BTCPayServer.Controllers
|
||||
blob.NetworkFeeDisabled = !model.NetworkFee;
|
||||
blob.MonitoringExpiration = model.MonitoringExpiration;
|
||||
blob.InvoiceExpiration = model.InvoiceExpiration;
|
||||
|
||||
bool newExchange = blob.PreferredExchange != model.PreferredExchange;
|
||||
blob.PreferredExchange = model.PreferredExchange;
|
||||
|
||||
blob.SetRateMultiplier(model.RateMultiplier);
|
||||
@ -320,14 +324,14 @@ namespace BTCPayServer.Controllers
|
||||
needUpdate = true;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(blob.PreferredExchange))
|
||||
if (!blob.PreferredExchange.IsCoinAverage() && newExchange)
|
||||
{
|
||||
using (HttpClient client = new HttpClient())
|
||||
{
|
||||
var rate = await client.GetAsync(model.RateSource);
|
||||
if (rate.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.PreferredExchange), $"Invalid exchange ({model.RateSource})");
|
||||
ModelState.AddModelError(nameof(model.PreferredExchange), $"Unsupported exchange ({model.RateSource})");
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
|
@ -239,7 +239,7 @@ namespace BTCPayServer.Data
|
||||
|
||||
public IRateProvider ApplyRateRules(BTCPayNetwork network, IRateProvider rateProvider)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(PreferredExchange))
|
||||
if (!PreferredExchange.IsCoinAverage())
|
||||
{
|
||||
// If the original rateProvider is a cache, use the same inner provider as fallback, and same memory cache to wrap it all
|
||||
if (rateProvider is CachedRateProvider cachedRateProvider)
|
||||
@ -248,7 +248,7 @@ namespace BTCPayServer.Data
|
||||
new CoinAverageRateProvider(network.CryptoCode) { Exchange = PreferredExchange },
|
||||
cachedRateProvider.Inner
|
||||
});
|
||||
rateProvider = new CachedRateProvider(network.CryptoCode, rateProvider, cachedRateProvider.MemoryCache);
|
||||
rateProvider = new CachedRateProvider(network.CryptoCode, rateProvider, cachedRateProvider.MemoryCache) { AdditionalScope = PreferredExchange };
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -6,6 +6,7 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Logging;
|
||||
using NBXplorer;
|
||||
using BTCPayServer.HostedServices;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
@ -15,9 +16,10 @@ namespace BTCPayServer
|
||||
BTCPayServerOptions _Options;
|
||||
|
||||
public BTCPayNetworkProvider NetworkProviders => _NetworkProviders;
|
||||
|
||||
public ExplorerClientProvider(BTCPayNetworkProvider networkProviders, BTCPayServerOptions options)
|
||||
NBXplorerDashboard _Dashboard;
|
||||
public ExplorerClientProvider(BTCPayNetworkProvider networkProviders, BTCPayServerOptions options, NBXplorerDashboard dashboard)
|
||||
{
|
||||
_Dashboard = dashboard;
|
||||
_NetworkProviders = networkProviders;
|
||||
_Options = options;
|
||||
|
||||
@ -68,6 +70,16 @@ namespace BTCPayServer
|
||||
return GetExplorerClient(network.CryptoCode);
|
||||
}
|
||||
|
||||
public bool IsAvailable(BTCPayNetwork network)
|
||||
{
|
||||
return IsAvailable(network.CryptoCode);
|
||||
}
|
||||
|
||||
public bool IsAvailable(string cryptoCode)
|
||||
{
|
||||
return _Clients.ContainsKey(cryptoCode) && _Dashboard.IsFullySynched(cryptoCode);
|
||||
}
|
||||
|
||||
public BTCPayNetwork GetNetwork(string cryptoCode)
|
||||
{
|
||||
var network = _NetworkProviders.GetNetwork(cryptoCode);
|
||||
|
@ -31,6 +31,12 @@ namespace BTCPayServer
|
||||
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
|
||||
}
|
||||
|
||||
public static bool IsCoinAverage(this string exchangeName)
|
||||
{
|
||||
string[] coinAverages = new[] { "coinaverage", "bitcoinaverage" };
|
||||
return String.IsNullOrWhiteSpace(exchangeName) ? true : coinAverages.Contains(exchangeName, StringComparer.OrdinalIgnoreCase) ? true : false;
|
||||
}
|
||||
|
||||
public static async Task<Dictionary<uint256, TransactionResult>> GetTransactions(this BTCPayWallet client, uint256[] hashes, CancellationToken cts = default(CancellationToken))
|
||||
{
|
||||
hashes = hashes.Distinct().ToArray();
|
||||
|
@ -68,7 +68,11 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoiceIdFromScriptPubKey(scriptPubKey, network.CryptoCode);
|
||||
if (invoice != null)
|
||||
{
|
||||
String address = scriptPubKey.GetDestinationAddress(network.NBitcoinNetwork)?.ToString() ?? scriptPubKey.ToString();
|
||||
Logs.PayServer.LogInformation($"{address} is mapping to invoice {invoice}");
|
||||
_WatchRequests.Add(invoice);
|
||||
}
|
||||
}
|
||||
|
||||
async Task NotifyBlock()
|
||||
@ -82,8 +86,11 @@ namespace BTCPayServer.HostedServices
|
||||
private async Task UpdateInvoice(string invoiceId, CancellationToken cancellation)
|
||||
{
|
||||
Dictionary<BTCPayNetwork, KnownState> changes = new Dictionary<BTCPayNetwork, KnownState>();
|
||||
while (!cancellation.IsCancellationRequested)
|
||||
int maxLoop = 5;
|
||||
int loopCount = -1;
|
||||
while (!cancellation.IsCancellationRequested && loopCount < maxLoop)
|
||||
{
|
||||
loopCount++;
|
||||
try
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, true).ConfigureAwait(false);
|
||||
@ -122,7 +129,7 @@ namespace BTCPayServer.HostedServices
|
||||
break;
|
||||
}
|
||||
|
||||
if (!changed || cancellation.IsCancellationRequested)
|
||||
if (updateContext.Events.Count == 0 || cancellation.IsCancellationRequested)
|
||||
break;
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellation.IsCancellationRequested)
|
||||
@ -190,7 +197,7 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1003, "invoice_paidInFull"));
|
||||
invoice.Status = "paid";
|
||||
invoice.ExceptionStatus = null;
|
||||
invoice.ExceptionStatus = totalPaid > accounting.TotalDue ? "paidOver" : null;
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.MarkDirty();
|
||||
}
|
||||
@ -202,13 +209,6 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
}
|
||||
|
||||
if (totalPaid > accounting.TotalDue && invoice.ExceptionStatus != "paidOver")
|
||||
{
|
||||
invoice.ExceptionStatus = "paidOver";
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.MarkDirty();
|
||||
}
|
||||
|
||||
if (totalPaid < accounting.TotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != "paidPartial")
|
||||
{
|
||||
invoice.ExceptionStatus = "paidPartial";
|
||||
@ -278,7 +278,7 @@ namespace BTCPayServer.HostedServices
|
||||
private IEnumerable<Task<NetworkCoins>> GetCoinsPerNetwork(UpdateInvoiceContext context, InvoiceEntity invoice, DerivationStrategy[] strategies)
|
||||
{
|
||||
return strategies
|
||||
.Select(d => (Wallet: _WalletProvider.GetWallet(d.Network),
|
||||
.Select(d => (Wallet: _WalletProvider.IsAvailable(d.Network) ? _WalletProvider.GetWallet(d.Network) : null,
|
||||
Network: d.Network,
|
||||
Strategy: d.DerivationStrategyBase))
|
||||
.Where(d => d.Wallet != null)
|
||||
@ -445,8 +445,8 @@ namespace BTCPayServer.HostedServices
|
||||
leases.Add(_EventAggregator.Subscribe<Events.TxOutReceivedEvent>(async b => { await NotifyReceived(b.ScriptPubKey, b.Network); }));
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async b =>
|
||||
{
|
||||
if(b.Name == "invoice_created")
|
||||
{
|
||||
if (b.Name == "invoice_created")
|
||||
{
|
||||
await Watch(b.InvoiceId);
|
||||
}
|
||||
}));
|
||||
@ -483,29 +483,31 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
Logs.PayServer.LogInformation("Start watching invoices");
|
||||
await Task.Delay(1).ConfigureAwait(false); // Small hack so that the caller does not block on GetConsumingEnumerable
|
||||
ConcurrentDictionary<string, Task> executing = new ConcurrentDictionary<string, Task>();
|
||||
ConcurrentDictionary<string, Lazy<Task>> executing = new ConcurrentDictionary<string, Lazy<Task>>();
|
||||
try
|
||||
{
|
||||
// This loop just make sure an invoice will not be updated at the same time by two tasks.
|
||||
// If an update is happening while a request come, then the update is deferred when the executing task is over
|
||||
foreach (var item in _WatchRequests.GetConsumingEnumerable(cancellation))
|
||||
{
|
||||
bool added = false;
|
||||
var task = executing.GetOrAdd(item, async i =>
|
||||
var localItem = item;
|
||||
var toExecute = new Lazy<Task>(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
added = true;
|
||||
await UpdateInvoice(i, cancellation);
|
||||
await UpdateInvoice(localItem, cancellation);
|
||||
}
|
||||
catch (Exception ex) when (!cancellation.IsCancellationRequested)
|
||||
finally
|
||||
{
|
||||
Logs.PayServer.LogCritical(ex, $"Error in the InvoiceWatcher loop (Invoice {i})");
|
||||
executing.TryRemove(localItem, out Lazy<Task> unused);
|
||||
}
|
||||
});
|
||||
|
||||
if (!added && task.Status == TaskStatus.RanToCompletion)
|
||||
}, false);
|
||||
var executingTask = executing.GetOrAdd(item, toExecute);
|
||||
executingTask.Value.GetAwaiter(); // Make sure it run
|
||||
if (executingTask != toExecute)
|
||||
{
|
||||
executing.TryRemove(item, out Task t);
|
||||
_WatchRequests.Add(item);
|
||||
// What was planned can't run for now, rebook it when the executingTask finish
|
||||
var unused = executingTask.Value.ContinueWith(t => _WatchRequests.Add(localItem));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -514,7 +516,7 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
finally
|
||||
{
|
||||
await Task.WhenAll(executing.Values);
|
||||
await Task.WhenAll(executing.Values.Select(v => v.Value).ToArray());
|
||||
}
|
||||
Logs.PayServer.LogInformation("Stop watching invoices");
|
||||
}
|
||||
|
@ -41,6 +41,11 @@ namespace BTCPayServer.HostedServices
|
||||
return _Summaries.All(s => s.Value.Status != null && s.Value.Status.IsFullySynched);
|
||||
}
|
||||
|
||||
public bool IsFullySynched(string cryptoCode)
|
||||
{
|
||||
return _Summaries.Any(s => s.Key.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase) && s.Value.Status != null && s.Value.Status.IsFullySynched);
|
||||
}
|
||||
|
||||
public IEnumerable<NBXplorerSummary> GetAll()
|
||||
{
|
||||
return _Summaries.Values;
|
||||
|
@ -39,5 +39,6 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public string OrderId { get; set; }
|
||||
public string CryptoImage { get; set; }
|
||||
public string NetworkFeeDescription { get; internal set; }
|
||||
public int MaxTimeMinutes { get; internal set; }
|
||||
}
|
||||
}
|
||||
|
@ -47,14 +47,14 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
|
||||
public List<StoreViewModel.DerivationScheme> DerivationSchemes { get; set; } = new List<StoreViewModel.DerivationScheme>();
|
||||
|
||||
[Display(Name = "Preferred exchange rate...")]
|
||||
[Display(Name = "Preferred price source (eg. bitfinex, bitstamp...)")]
|
||||
public string PreferredExchange { get; set; }
|
||||
|
||||
public string RateSource
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.IsNullOrEmpty(PreferredExchange) ? "https://apiv2.bitcoinaverage.com/indices/global/ticker/short" : $"https://apiv2.bitcoinaverage.com/exchanges/{PreferredExchange}";
|
||||
return PreferredExchange.IsCoinAverage() ? "https://apiv2.bitcoinaverage.com/indices/global/ticker/short" : $"https://apiv2.bitcoinaverage.com/exchanges/{PreferredExchange}";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -40,7 +40,7 @@ namespace BTCPayServer.Services.Rates
|
||||
|
||||
public Task<decimal> GetRateAsync(string currency)
|
||||
{
|
||||
return MemoryCache.GetOrCreateAsync("CURR_" + currency + "_" + _CryptoCode, (ICacheEntry entry) =>
|
||||
return MemoryCache.GetOrCreateAsync("CURR_" + currency + "_" + _CryptoCode + "_" + AdditionalScope, (ICacheEntry entry) =>
|
||||
{
|
||||
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
|
||||
return _Inner.GetRateAsync(currency);
|
||||
@ -49,11 +49,13 @@ namespace BTCPayServer.Services.Rates
|
||||
|
||||
public Task<ICollection<Rate>> GetRatesAsync()
|
||||
{
|
||||
return MemoryCache.GetOrCreateAsync("GLOBAL_RATES", (ICacheEntry entry) =>
|
||||
return MemoryCache.GetOrCreateAsync("GLOBAL_RATES_" + _CryptoCode + "_" + AdditionalScope, (ICacheEntry entry) =>
|
||||
{
|
||||
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
|
||||
return _Inner.GetRatesAsync();
|
||||
});
|
||||
}
|
||||
|
||||
public string AdditionalScope { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -6,17 +6,38 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class MockRateProviderFactory : IRateProviderFactory
|
||||
{
|
||||
List<MockRateProvider> _Mocks = new List<MockRateProvider>();
|
||||
public MockRateProviderFactory()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void AddMock(MockRateProvider mock)
|
||||
{
|
||||
_Mocks.Add(mock);
|
||||
}
|
||||
public IRateProvider GetRateProvider(BTCPayNetwork network)
|
||||
{
|
||||
return _Mocks.FirstOrDefault(m => m.CryptoCode == network.CryptoCode);
|
||||
}
|
||||
}
|
||||
public class MockRateProvider : IRateProvider
|
||||
{
|
||||
List<Rate> _Rates;
|
||||
|
||||
public MockRateProvider(params Rate[] rates)
|
||||
public string CryptoCode { get; }
|
||||
|
||||
public MockRateProvider(string cryptoCode, params Rate[] rates)
|
||||
{
|
||||
_Rates = new List<Rate>(rates);
|
||||
CryptoCode = cryptoCode;
|
||||
}
|
||||
public MockRateProvider(List<Rate> rates)
|
||||
public MockRateProvider(string cryptoCode, List<Rate> rates)
|
||||
{
|
||||
_Rates = rates;
|
||||
CryptoCode = cryptoCode;
|
||||
}
|
||||
public Task<decimal> GetRateAsync(string currency)
|
||||
{
|
||||
|
@ -38,5 +38,10 @@ namespace BTCPayServer.Services.Wallets
|
||||
return null;
|
||||
return new BTCPayWallet(client, _TransactionCacheProvider.GetTransactionCache(network), network);
|
||||
}
|
||||
|
||||
public bool IsAvailable(BTCPayNetwork network)
|
||||
{
|
||||
return _Client.IsAvailable(network);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -492,7 +492,7 @@
|
||||
<div>
|
||||
<div class="expired__body">
|
||||
<div class="expired__header" i18n="">What happened?</div>
|
||||
<div class="expired__text" i18n="">This invoice has expired. An invoice is only valid for 15 minutes. You can <div class="expired__text__link i18n-return-to-merchant">return to {{srvModel.storeName}}</div> if you would like to submit your payment again.</div>
|
||||
<div class="expired__text" i18n="">This invoice has expired. An invoice is only valid for @Model.MaxTimeMinutes minutes. You can <div class="expired__text__link i18n-return-to-merchant">return to {{srvModel.storeName}}</div> if you would like to submit your payment again.</div>
|
||||
<div class="expired__text" i18n="">If you tried to send a payment, it has not yet been accepted by the Bitcoin network. We have not yet received your funds.</div>
|
||||
<div class="expired__text" i18n="">
|
||||
If the transaction
|
||||
@ -504,7 +504,7 @@
|
||||
<span class="expired__text__bullet" i18n="">Invoice ID:</span> {{srvModel.invoiceId}}<br>
|
||||
<!---->
|
||||
<span>
|
||||
<span class="expired__text__bullet" i18n="">Order ID:</span> {{srvModel.OrderId}}
|
||||
<span class="expired__text__bullet" i18n="">Order ID:</span> {{srvModel.orderId}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -43,7 +43,7 @@
|
||||
<input asp-for="PreferredExchange" class="form-control" />
|
||||
<span asp-validation-for="PreferredExchange" class="text-danger"></span>
|
||||
<p id="PreferredExchangeHelpBlock" class="form-text text-muted">
|
||||
Current rate source is <a href="@Model.RateSource" target="_blank">@Model.RateSource</a>.<small> (using 1 minute cache)</small>
|
||||
Current price source is <a href="@Model.RateSource" target="_blank">@Model.PreferredExchange</a>.<small> (using 1 minute cache)</small>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
@ -19,7 +19,7 @@ Vue.config.ignoredElements = [
|
||||
'low-fee-timeline',
|
||||
// Ignoring custom HTML5 elements, eg: bp-spinner
|
||||
/^bp-/
|
||||
]
|
||||
];
|
||||
var checkoutCtrl = new Vue({
|
||||
el: '#checkoutCtrl',
|
||||
components: {
|
||||
@ -28,7 +28,7 @@ var checkoutCtrl = new Vue({
|
||||
data: {
|
||||
srvModel: srvModel
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
var display = $(".timer-row__time-left"); // Timer container
|
||||
|
||||
@ -88,7 +88,7 @@ function emailForm() {
|
||||
$("#emailAddressForm").addClass("ng-touched ng-dirty ng-submitted ng-invalid");
|
||||
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/* =============== Even listeners =============== */
|
||||
@ -136,7 +136,7 @@ $("#copy-tab").click(function () {
|
||||
$(".payment-tabs__slider").addClass("slide-right");
|
||||
}
|
||||
|
||||
if (!($("#copy").is(".active"))) {
|
||||
if (!$("#copy").is(".active")) {
|
||||
$("#copy").show();
|
||||
$("#copy").addClass("active");
|
||||
|
||||
@ -284,7 +284,7 @@ function progressStart(timerMax) {
|
||||
|
||||
var now = new Date();
|
||||
var timeDiff = end.getTime() - now.getTime();
|
||||
var perc = 100 - Math.round((timeDiff / timerMax) * 100);
|
||||
var perc = 100 - Math.round(timeDiff / timerMax * 100);
|
||||
|
||||
if (perc === 75 && (status === "paidPartial" || status === "new")) {
|
||||
$(".timer-row").addClass("expiring-soon");
|
||||
|
Reference in New Issue
Block a user