Compare commits
15 Commits
Author | SHA1 | Date | |
---|---|---|---|
59a2432af9 | |||
ea4fa8d5d4 | |||
ade3eff75c | |||
db2a2a2b6c | |||
579dcb5af8 | |||
69247dee8a | |||
7b9541b8e9 | |||
a12e4d7f64 | |||
897da9b07a | |||
293525d480 | |||
198e810355 | |||
fe25e00c94 | |||
8b129ab2e5 | |||
b8068b2ae8 | |||
3007a6bbc8 |
@ -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);
|
||||
@ -751,6 +753,10 @@ namespace BTCPayServer.Tests
|
||||
var b = cached.GetRateAsync("JPY").GetAwaiter().GetResult();
|
||||
//Manually check that cache get hit after 10 sec
|
||||
var c = cached.GetRateAsync("JPY").GetAwaiter().GetResult();
|
||||
|
||||
var bitstamp = new CoinAverageRateProvider("BTC") { Exchange = "bitstamp" };
|
||||
var bitstampRate = bitstamp.GetRateAsync("USD").GetAwaiter().GetResult();
|
||||
Assert.Throws<RateUnavailableException>(() => bitstamp.GetRateAsync("XXXXX").GetAwaiter().GetResult());
|
||||
}
|
||||
|
||||
private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx)
|
||||
@ -761,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.11</Version>
|
||||
<Version>1.0.1.17</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());
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ using NBXplorer.DerivationStrategy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
@ -95,7 +96,7 @@ namespace BTCPayServer.Controllers
|
||||
var stores = await _Repo.GetStoresByUserId(GetUserId());
|
||||
var balances = stores
|
||||
.Select(s => s.GetDerivationStrategies(_NetworkProvider)
|
||||
.Select(d => (Wallet: _WalletProvider.GetWallet(d.Network),
|
||||
.Select(d => (Wallet: _WalletProvider.GetWallet(d.Network),
|
||||
DerivationStrategy: d.DerivationStrategyBase))
|
||||
.Where(_ => _.Wallet != null)
|
||||
.Select(async _ => (await _.Wallet.GetBalance(_.DerivationStrategy)).ToString() + " " + _.Wallet.Network.CryptoCode))
|
||||
@ -165,6 +166,7 @@ namespace BTCPayServer.Controllers
|
||||
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
|
||||
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
|
||||
vm.RateMultiplier = (double)storeBlob.GetRateMultiplier();
|
||||
vm.PreferredExchange = storeBlob.PreferredExchange.IsCoinAverage() ? "coinaverage" : storeBlob.PreferredExchange;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
@ -241,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
|
||||
@ -276,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();
|
||||
@ -309,6 +313,10 @@ 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);
|
||||
|
||||
if (store.SetStoreBlob(blob))
|
||||
@ -316,6 +324,19 @@ namespace BTCPayServer.Controllers
|
||||
needUpdate = true;
|
||||
}
|
||||
|
||||
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), $"Unsupported exchange ({model.RateSource})");
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (needUpdate)
|
||||
{
|
||||
await _Repo.UpdateStore(store);
|
||||
|
@ -235,9 +235,29 @@ namespace BTCPayServer.Data
|
||||
}
|
||||
|
||||
public List<RateRule> RateRules { get; set; } = new List<RateRule>();
|
||||
public string PreferredExchange { get; set; }
|
||||
|
||||
public IRateProvider ApplyRateRules(BTCPayNetwork network, IRateProvider rateProvider)
|
||||
{
|
||||
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)
|
||||
{
|
||||
rateProvider = new FallbackRateProvider(new IRateProvider[] {
|
||||
new CoinAverageRateProvider(network.CryptoCode) { Exchange = PreferredExchange },
|
||||
cachedRateProvider.Inner
|
||||
});
|
||||
rateProvider = new CachedRateProvider(network.CryptoCode, rateProvider, cachedRateProvider.MemoryCache) { AdditionalScope = PreferredExchange };
|
||||
}
|
||||
else
|
||||
{
|
||||
rateProvider = new FallbackRateProvider(new IRateProvider[] {
|
||||
new CoinAverageRateProvider(network.CryptoCode) { Exchange = PreferredExchange },
|
||||
rateProvider
|
||||
});
|
||||
}
|
||||
}
|
||||
if (RateRules == null || RateRules.Count == 0)
|
||||
return rateProvider;
|
||||
return new TweakRateProvider(network, rateProvider, RateRules.ToList());
|
||||
|
@ -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)
|
||||
@ -194,7 +201,7 @@ namespace BTCPayServer.HostedServices
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.MarkDirty();
|
||||
}
|
||||
else if (invoice.Status == "expired")
|
||||
else if (invoice.Status == "expired" && invoice.ExceptionStatus != "paidLate")
|
||||
{
|
||||
invoice.ExceptionStatus = "paidLate";
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1009, "invoice_paidAfterExpiration"));
|
||||
@ -278,7 +285,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 +452,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 +490,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 +523,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,6 +47,17 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
|
||||
public List<StoreViewModel.DerivationScheme> DerivationSchemes { get; set; } = new List<StoreViewModel.DerivationScheme>();
|
||||
|
||||
[Display(Name = "Preferred price source (eg. bitfinex, bitstamp...)")]
|
||||
public string PreferredExchange { get; set; }
|
||||
|
||||
public string RateSource
|
||||
{
|
||||
get
|
||||
{
|
||||
return PreferredExchange.IsCoinAverage() ? "https://apiv2.bitcoinaverage.com/indices/global/ticker/short" : $"https://apiv2.bitcoinaverage.com/exchanges/{PreferredExchange}";
|
||||
}
|
||||
}
|
||||
|
||||
[Display(Name = "Multiply the original rate by ...")]
|
||||
[Range(0.01, 10.0)]
|
||||
public double RateMultiplier
|
||||
|
@ -11,6 +11,15 @@ namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
IMemoryCache _Cache;
|
||||
ConcurrentDictionary<string, IRateProvider> _Providers = new ConcurrentDictionary<string, IRateProvider>();
|
||||
|
||||
public IMemoryCache Cache
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Cache;
|
||||
}
|
||||
}
|
||||
|
||||
public CachedDefaultRateProviderFactory(IMemoryCache cache)
|
||||
{
|
||||
if (cache == null)
|
||||
@ -18,10 +27,12 @@ namespace BTCPayServer.Services.Rates
|
||||
_Cache = cache;
|
||||
}
|
||||
|
||||
public IRateProvider RateProvider { get; set; }
|
||||
|
||||
public TimeSpan CacheSpan { get; set; } = TimeSpan.FromMinutes(1.0);
|
||||
public IRateProvider GetRateProvider(BTCPayNetwork network)
|
||||
{
|
||||
return _Providers.GetOrAdd(network.CryptoCode, new CachedRateProvider(network.CryptoCode, network.DefaultRateProvider, _Cache) { CacheSpan = CacheSpan });
|
||||
return _Providers.GetOrAdd(network.CryptoCode, new CachedRateProvider(network.CryptoCode, RateProvider ?? network.DefaultRateProvider, _Cache) { CacheSpan = CacheSpan });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,19 +19,28 @@ namespace BTCPayServer.Services.Rates
|
||||
if (memoryCache == null)
|
||||
throw new ArgumentNullException(nameof(memoryCache));
|
||||
this._Inner = inner;
|
||||
this._MemoryCache = memoryCache;
|
||||
this.MemoryCache = memoryCache;
|
||||
this._CryptoCode = cryptoCode;
|
||||
}
|
||||
|
||||
public IRateProvider Inner
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Inner;
|
||||
}
|
||||
}
|
||||
|
||||
public TimeSpan CacheSpan
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = TimeSpan.FromMinutes(1.0);
|
||||
public IMemoryCache MemoryCache { get => _MemoryCache; private set => _MemoryCache = value; }
|
||||
|
||||
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);
|
||||
@ -40,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; }
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,8 @@ namespace BTCPayServer.Services.Rates
|
||||
CryptoCode = cryptoCode ?? "BTC";
|
||||
}
|
||||
|
||||
public string Exchange { get; set; }
|
||||
|
||||
public string CryptoCode { get; set; }
|
||||
|
||||
public string Market
|
||||
@ -45,7 +47,15 @@ namespace BTCPayServer.Services.Rates
|
||||
|
||||
private async Task<Dictionary<string, decimal>> GetRatesCore()
|
||||
{
|
||||
var resp = await _Client.GetAsync($"https://apiv2.bitcoinaverage.com/indices/{Market}/ticker/short");
|
||||
HttpResponseMessage resp = null;
|
||||
if (Exchange == null)
|
||||
{
|
||||
resp = await _Client.GetAsync($"https://apiv2.bitcoinaverage.com/indices/{Market}/ticker/short");
|
||||
}
|
||||
else
|
||||
{
|
||||
resp = await _Client.GetAsync($"https://apiv2.bitcoinaverage.com/exchanges/{Exchange}");
|
||||
}
|
||||
using (resp)
|
||||
{
|
||||
|
||||
@ -57,9 +67,23 @@ namespace BTCPayServer.Services.Rates
|
||||
throw new CoinAverageException("Unauthorized access to the API, premium plan needed");
|
||||
resp.EnsureSuccessStatusCode();
|
||||
var rates = JObject.Parse(await resp.Content.ReadAsStringAsync());
|
||||
if(Exchange != null)
|
||||
{
|
||||
rates = (JObject)rates["symbols"];
|
||||
}
|
||||
return rates.Properties()
|
||||
.Where(p => p.Name.StartsWith(CryptoCode, StringComparison.OrdinalIgnoreCase))
|
||||
.ToDictionary(p => p.Name.Substring(CryptoCode.Length, p.Name.Length - CryptoCode.Length), p => ToDecimal(p.Value["last"]));
|
||||
.ToDictionary(p => p.Name.Substring(CryptoCode.Length, p.Name.Length - CryptoCode.Length), p =>
|
||||
{
|
||||
if (Exchange == null)
|
||||
{
|
||||
return ToDecimal(p.Value["last"]);
|
||||
}
|
||||
else
|
||||
{
|
||||
return ToDecimal(p.Value["bid"]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@
|
||||
@Model.ToJSVariableModel("srvModel")
|
||||
</script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/1.7.1/clipboard.min.js"></script>
|
||||
<script src="~/js/vue.js" type="text/javascript" defer="defer"></script>
|
||||
<script src="~/js/vue.min.js" type="text/javascript" defer="defer"></script>
|
||||
<script src="~/js/vue-qrcode.js" type="text/javascript" defer="defer"></script>
|
||||
<script src="~/js/core.js" type="text/javascript" defer="defer"></script>
|
||||
<!-- <script src="img/Intl.js" type="text/javascript" defer="defer"></script>
|
||||
@ -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>
|
||||
|
@ -38,6 +38,14 @@
|
||||
<label asp-for="NetworkFee"></label>
|
||||
<input asp-for="NetworkFee" type="checkbox" class="form-check" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="PreferredExchange"></label>
|
||||
<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 price source is <a href="@Model.RateSource" target="_blank">@Model.PreferredExchange</a>.<small> (using 1 minute cache)</small>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="RateMultiplier"></label>
|
||||
<input asp-for="RateMultiplier" class="form-control" />
|
||||
|
@ -43,7 +43,7 @@ h6 {
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
/*font-size: 16px;*/
|
||||
line-height: 1.5;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
@ -14,6 +14,12 @@
|
||||
*/
|
||||
|
||||
// TODO: Vue controller... complete migrate to it for binding, animations can stay in jQuery
|
||||
Vue.config.ignoredElements = [
|
||||
'line-items',
|
||||
'low-fee-timeline',
|
||||
// Ignoring custom HTML5 elements, eg: bp-spinner
|
||||
/^bp-/
|
||||
];
|
||||
var checkoutCtrl = new Vue({
|
||||
el: '#checkoutCtrl',
|
||||
components: {
|
||||
@ -22,7 +28,7 @@ var checkoutCtrl = new Vue({
|
||||
data: {
|
||||
srvModel: srvModel
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
var display = $(".timer-row__time-left"); // Timer container
|
||||
|
||||
@ -82,7 +88,7 @@ function emailForm() {
|
||||
$("#emailAddressForm").addClass("ng-touched ng-dirty ng-submitted ng-invalid");
|
||||
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/* =============== Even listeners =============== */
|
||||
@ -130,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");
|
||||
|
||||
@ -278,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");
|
||||
|
File diff suppressed because it is too large
Load Diff
6
BTCPayServer/wwwroot/js/vue.min.js
vendored
Normal file
6
BTCPayServer/wwwroot/js/vue.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user