Compare commits
14 Commits
Author | SHA1 | Date | |
---|---|---|---|
de6f0008a6 | |||
7618eacef1 | |||
842e083ebe | |||
1c510df3fc | |||
c80ffe396e | |||
401a31e5c2 | |||
2df60bd121 | |||
6d10c8a6c1 | |||
44898b5e23 | |||
c0f53db561 | |||
133fb96d28 | |||
98b7ad62af | |||
3069fe0dd9 | |||
729555b96f |
@ -38,7 +38,7 @@ services:
|
||||
- postgres
|
||||
|
||||
bitcoin-nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:1.0.0.40
|
||||
image: nicolasdorier/nbxplorer:1.0.0.42
|
||||
ports:
|
||||
- "32838:32838"
|
||||
expose:
|
||||
@ -56,7 +56,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
litecoin-nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:1.0.0.40
|
||||
image: nicolasdorier/nbxplorer:1.0.0.43
|
||||
ports:
|
||||
- "32839:32839"
|
||||
expose:
|
||||
|
@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<Version>1.0.0.55</Version>
|
||||
<Version>1.0.0.62</Version>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="Build\dockerfiles\**" />
|
||||
|
@ -139,8 +139,7 @@ namespace BTCPayServer.Controllers
|
||||
OrderId = invoice.OrderId,
|
||||
InvoiceId = invoice.Id,
|
||||
BtcAddress = cryptoData.DepositAddress,
|
||||
BtcAmount = (accounting.TotalDue - cryptoData.TxFee).ToString(),
|
||||
BtcTotalDue = accounting.TotalDue.ToString(),
|
||||
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
|
||||
BtcDue = accounting.Due.ToString(),
|
||||
CustomerEmail = invoice.RefundMail,
|
||||
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
|
||||
@ -149,14 +148,24 @@ namespace BTCPayServer.Controllers
|
||||
Rate = FormatCurrency(cryptoData),
|
||||
MerchantRefLink = invoice.RedirectURL ?? "/",
|
||||
StoreName = store.StoreName,
|
||||
TxFees = cryptoData.TxFee.ToString(),
|
||||
InvoiceBitcoinUrl = cryptoInfo.PaymentUrls.BIP21,
|
||||
TxCount = accounting.TxCount,
|
||||
BtcPaid = accounting.Paid.ToString(),
|
||||
Status = invoice.Status,
|
||||
CryptoImage = "/" + Url.Content(network.CryptoImagePath)
|
||||
CryptoImage = "/" + Url.Content(network.CryptoImagePath),
|
||||
NetworkFeeDescription = $"{accounting.TxCount} transaction{(accounting.TxCount > 1 ? "s" : "")} x {cryptoData.TxFee} {network.CryptoCode}",
|
||||
AvailableCryptos = invoice.GetCryptoData().Select(kv=> new PaymentModel.AvailableCrypto()
|
||||
{
|
||||
CryptoCode = kv.Key,
|
||||
CryptoImage = "/" + _NetworkProvider.GetNetwork(kv.Key).CryptoImagePath,
|
||||
Link = Url.Action(nameof(Checkout), new { invoiceId = invoiceId, cryptoCode = kv.Key })
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
var isMultiCurrency = invoice.Payments.Select(p=>p.GetCryptoCode()).Concat(new[] { network.CryptoCode }).Distinct().Count() > 1;
|
||||
if (isMultiCurrency)
|
||||
model.NetworkFeeDescription = $"{accounting.NetworkFee} {network.CryptoCode}";
|
||||
|
||||
var expiration = TimeSpan.FromSeconds(model.ExpirationSeconds);
|
||||
model.TimeLeft = PrettyPrint(expiration);
|
||||
return model;
|
||||
|
@ -415,6 +415,7 @@ namespace BTCPayServer.Controllers
|
||||
pairingCode = ((DataWrapper<List<PairingCodeResponse>>)await _TokenController.Tokens(tokenRequest)).Data[0].PairingCode;
|
||||
}
|
||||
|
||||
GeneratedPairingCode = pairingCode;
|
||||
return RedirectToAction(nameof(RequestPairing), new
|
||||
{
|
||||
pairingCode = pairingCode,
|
||||
@ -422,6 +423,8 @@ namespace BTCPayServer.Controllers
|
||||
});
|
||||
}
|
||||
|
||||
public string GeneratedPairingCode { get; set; }
|
||||
|
||||
[HttpGet]
|
||||
[Route("/api-tokens")]
|
||||
[Route("{storeId}/Tokens/Create")]
|
||||
|
@ -78,10 +78,10 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateInvoice(string invoiceId)
|
||||
private async Task UpdateInvoice(string invoiceId, CancellationToken cancellation)
|
||||
{
|
||||
Dictionary<BTCPayNetwork, KnownState> changes = new Dictionary<BTCPayNetwork, KnownState>();
|
||||
while (true)
|
||||
while (!cancellation.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -121,17 +121,17 @@ namespace BTCPayServer.HostedServices
|
||||
break;
|
||||
}
|
||||
|
||||
if (!changed || _Cts.Token.IsCancellationRequested)
|
||||
if (!changed || cancellation.IsCancellationRequested)
|
||||
break;
|
||||
}
|
||||
catch (OperationCanceledException) when (_Cts.Token.IsCancellationRequested)
|
||||
catch (OperationCanceledException) when (cancellation.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogError(ex, "Unhandled error on watching invoice " + invoiceId);
|
||||
await Task.Delay(10000, _Cts.Token).ConfigureAwait(false);
|
||||
await Task.Delay(10000, cancellation).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -140,36 +140,6 @@ namespace BTCPayServer.HostedServices
|
||||
private async Task UpdateInvoice(UpdateInvoiceContext context)
|
||||
{
|
||||
var invoice = context.Invoice;
|
||||
//Fetch unknown payments
|
||||
var strategies = invoice.GetDerivationStrategies(_NetworkProvider).ToArray();
|
||||
var getCoinsResponsesAsync = strategies
|
||||
.Select(d => _Wallet.GetCoins(d, context.KnownStates.TryGet(d.Network), _Cts.Token))
|
||||
.ToArray();
|
||||
await Task.WhenAll(getCoinsResponsesAsync);
|
||||
var getCoinsResponses = getCoinsResponsesAsync.Select(g => g.Result).ToArray();
|
||||
foreach (var response in getCoinsResponses)
|
||||
{
|
||||
response.Coins = response.Coins.Where(c => invoice.AvailableAddressHashes.Contains(c.ScriptPubKey.Hash.ToString() + response.Strategy.Network.CryptoCode)).ToArray();
|
||||
}
|
||||
var coins = getCoinsResponses.Where(s => s.Coins.Length != 0).FirstOrDefault();
|
||||
bool dirtyAddress = false;
|
||||
if (coins != null)
|
||||
{
|
||||
context.ModifiedKnownStates.Add(coins.Strategy.Network, coins.State);
|
||||
var alreadyAccounted = new HashSet<OutPoint>(invoice.Payments.Select(p => p.Outpoint));
|
||||
foreach (var coin in coins.Coins.Where(c => !alreadyAccounted.Contains(c.Outpoint)))
|
||||
{
|
||||
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin, coins.Strategy.Network.CryptoCode).ConfigureAwait(false);
|
||||
invoice.Payments.Add(payment);
|
||||
context.Events.Add(new InvoicePaymentEvent(invoice.Id));
|
||||
dirtyAddress = true;
|
||||
}
|
||||
}
|
||||
//////
|
||||
var network = coins?.Strategy?.Network ?? _NetworkProvider.GetNetwork(invoice.GetCryptoData().First().Key);
|
||||
var cryptoData = invoice.GetCryptoData(network);
|
||||
var cryptoDataAll = invoice.GetCryptoData();
|
||||
var accounting = cryptoData.Calculate();
|
||||
if (invoice.Status == "new" && invoice.ExpirationTime < DateTimeOffset.UtcNow)
|
||||
{
|
||||
context.MarkDirty();
|
||||
@ -179,96 +149,130 @@ namespace BTCPayServer.HostedServices
|
||||
invoice.Status = "expired";
|
||||
}
|
||||
|
||||
if (invoice.Status == "new" || invoice.Status == "expired")
|
||||
foreach (NetworkCoins coins in await GetCoinsPerNetwork(context, invoice))
|
||||
{
|
||||
var totalPaid = (await GetPaymentsWithTransaction(network, invoice)).Select(p => p.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
|
||||
if (totalPaid >= accounting.TotalDue)
|
||||
bool dirtyAddress = false;
|
||||
if (coins.State != null)
|
||||
context.ModifiedKnownStates.AddOrReplace(coins.Strategy.Network, coins.State);
|
||||
var alreadyAccounted = new HashSet<OutPoint>(invoice.Payments.Select(p => p.Outpoint));
|
||||
foreach (var coin in coins.Coins.Where(c => !alreadyAccounted.Contains(c.Outpoint)))
|
||||
{
|
||||
if (invoice.Status == "new")
|
||||
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin, coins.Strategy.Network.CryptoCode).ConfigureAwait(false);
|
||||
invoice.Payments.Add(payment);
|
||||
context.Events.Add(new InvoicePaymentEvent(invoice.Id));
|
||||
dirtyAddress = true;
|
||||
}
|
||||
var network = coins.Strategy.Network;
|
||||
var cryptoData = invoice.GetCryptoData(network);
|
||||
var cryptoDataAll = invoice.GetCryptoData();
|
||||
var accounting = cryptoData.Calculate();
|
||||
|
||||
if (invoice.Status == "new" || invoice.Status == "expired")
|
||||
{
|
||||
var totalPaid = (await GetPaymentsWithTransaction(network, invoice)).Select(p => p.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
|
||||
if (totalPaid >= accounting.TotalDue)
|
||||
{
|
||||
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "paid"));
|
||||
invoice.Status = "paid";
|
||||
invoice.ExceptionStatus = null;
|
||||
if (invoice.Status == "new")
|
||||
{
|
||||
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "paid"));
|
||||
invoice.Status = "paid";
|
||||
invoice.ExceptionStatus = null;
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.MarkDirty();
|
||||
}
|
||||
else if (invoice.Status == "expired")
|
||||
{
|
||||
invoice.ExceptionStatus = "paidLate";
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
if (totalPaid > accounting.TotalDue && invoice.ExceptionStatus != "paidOver")
|
||||
{
|
||||
invoice.ExceptionStatus = "paidOver";
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.MarkDirty();
|
||||
}
|
||||
else if (invoice.Status == "expired")
|
||||
|
||||
if (totalPaid < accounting.TotalDue && invoice.Payments.Count != 0 && invoice.ExceptionStatus != "paidPartial")
|
||||
{
|
||||
invoice.ExceptionStatus = "paidLate";
|
||||
invoice.ExceptionStatus = "paidPartial";
|
||||
context.MarkDirty();
|
||||
if (dirtyAddress)
|
||||
{
|
||||
var address = await _Wallet.ReserveAddressAsync(coins.Strategy);
|
||||
Logs.PayServer.LogInformation("Generate new " + address);
|
||||
await _InvoiceRepository.NewAddress(invoice.Id, address, network);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (invoice.Status == "paid")
|
||||
{
|
||||
var transactions = await GetPaymentsWithTransaction(network, invoice);
|
||||
if (invoice.SpeedPolicy == SpeedPolicy.HighSpeed)
|
||||
{
|
||||
transactions = transactions.Where(t => t.Confirmations >= 1 || !t.Transaction.RBF);
|
||||
}
|
||||
else if (invoice.SpeedPolicy == SpeedPolicy.MediumSpeed)
|
||||
{
|
||||
transactions = transactions.Where(t => t.Confirmations >= 1);
|
||||
}
|
||||
else if (invoice.SpeedPolicy == SpeedPolicy.LowSpeed)
|
||||
{
|
||||
transactions = transactions.Where(t => t.Confirmations >= 6);
|
||||
}
|
||||
|
||||
var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
|
||||
|
||||
if (// Is after the monitoring deadline
|
||||
(invoice.MonitoringExpiration < DateTimeOffset.UtcNow)
|
||||
&&
|
||||
// And not enough amount confirmed
|
||||
(totalConfirmed < accounting.TotalDue))
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "invalid"));
|
||||
invoice.Status = "invalid";
|
||||
context.MarkDirty();
|
||||
}
|
||||
else if (totalConfirmed >= accounting.TotalDue)
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "confirmed"));
|
||||
invoice.Status = "confirmed";
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
if (totalPaid > accounting.TotalDue && invoice.ExceptionStatus != "paidOver")
|
||||
if (invoice.Status == "confirmed")
|
||||
{
|
||||
invoice.ExceptionStatus = "paidOver";
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.MarkDirty();
|
||||
}
|
||||
|
||||
if (totalPaid < accounting.TotalDue && invoice.Payments.Count != 0 && invoice.ExceptionStatus != "paidPartial")
|
||||
{
|
||||
invoice.ExceptionStatus = "paidPartial";
|
||||
context.MarkDirty();
|
||||
if (dirtyAddress)
|
||||
var transactions = await GetPaymentsWithTransaction(network, invoice);
|
||||
transactions = transactions.Where(t => t.Confirmations >= 6);
|
||||
var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
|
||||
if (totalConfirmed >= accounting.TotalDue)
|
||||
{
|
||||
var address = await _Wallet.ReserveAddressAsync(coins.Strategy);
|
||||
Logs.PayServer.LogInformation("Generate new " + address);
|
||||
await _InvoiceRepository.NewAddress(invoice.Id, address, network);
|
||||
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "complete"));
|
||||
invoice.Status = "complete";
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (invoice.Status == "paid")
|
||||
private async Task<IEnumerable<NetworkCoins>> GetCoinsPerNetwork(UpdateInvoiceContext context, InvoiceEntity invoice)
|
||||
{
|
||||
var strategies = invoice.GetDerivationStrategies(_NetworkProvider).ToArray();
|
||||
var getCoinsResponsesAsync = strategies
|
||||
.Select(d => _Wallet.GetCoins(d, context.KnownStates.TryGet(d.Network)))
|
||||
.ToArray();
|
||||
await Task.WhenAll(getCoinsResponsesAsync);
|
||||
var getCoinsResponses = getCoinsResponsesAsync.Select(g => g.Result).ToArray();
|
||||
foreach (var response in getCoinsResponses)
|
||||
{
|
||||
var transactions = await GetPaymentsWithTransaction(network, invoice);
|
||||
if (invoice.SpeedPolicy == SpeedPolicy.HighSpeed)
|
||||
{
|
||||
transactions = transactions.Where(t => t.Confirmations >= 1 || !t.Transaction.RBF);
|
||||
}
|
||||
else if (invoice.SpeedPolicy == SpeedPolicy.MediumSpeed)
|
||||
{
|
||||
transactions = transactions.Where(t => t.Confirmations >= 1);
|
||||
}
|
||||
else if (invoice.SpeedPolicy == SpeedPolicy.LowSpeed)
|
||||
{
|
||||
transactions = transactions.Where(t => t.Confirmations >= 6);
|
||||
}
|
||||
|
||||
var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
|
||||
|
||||
if (// Is after the monitoring deadline
|
||||
(invoice.MonitoringExpiration < DateTimeOffset.UtcNow)
|
||||
&&
|
||||
// And not enough amount confirmed
|
||||
(totalConfirmed < accounting.TotalDue))
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "invalid"));
|
||||
invoice.Status = "invalid";
|
||||
context.MarkDirty();
|
||||
}
|
||||
else if (totalConfirmed >= accounting.TotalDue)
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "confirmed"));
|
||||
invoice.Status = "confirmed";
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
if (invoice.Status == "confirmed")
|
||||
{
|
||||
var transactions = await GetPaymentsWithTransaction(network, invoice);
|
||||
transactions = transactions.Where(t => t.Confirmations >= 6);
|
||||
var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
|
||||
if (totalConfirmed >= accounting.TotalDue)
|
||||
{
|
||||
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "complete"));
|
||||
invoice.Status = "complete";
|
||||
context.MarkDirty();
|
||||
}
|
||||
response.Coins = response.Coins.Where(c => invoice.AvailableAddressHashes.Contains(c.ScriptPubKey.Hash.ToString() + response.Strategy.Network.CryptoCode)).ToArray();
|
||||
}
|
||||
return getCoinsResponses.Where(s => s.Coins.Length != 0).ToArray();
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<AccountedPaymentEntity>> GetPaymentsWithTransaction(BTCPayNetwork network, InvoiceEntity invoice)
|
||||
@ -336,10 +340,6 @@ namespace BTCPayServer.HostedServices
|
||||
set
|
||||
{
|
||||
_PollInterval = value;
|
||||
if (_UpdatePendingInvoices != null)
|
||||
{
|
||||
_UpdatePendingInvoices.Change(0, (int)value.TotalMilliseconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -352,30 +352,16 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
BlockingCollection<string> _WatchRequests = new BlockingCollection<string>(new ConcurrentQueue<string>());
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_Cts.Cancel();
|
||||
}
|
||||
|
||||
|
||||
Thread _Thread;
|
||||
TaskCompletionSource<bool> _RunningTask;
|
||||
Task _Poller;
|
||||
Task _Loop;
|
||||
CancellationTokenSource _Cts;
|
||||
Timer _UpdatePendingInvoices;
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_RunningTask = new TaskCompletionSource<bool>();
|
||||
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
_Thread = new Thread(Run) { Name = "InvoiceWatcher" };
|
||||
_Thread.Start();
|
||||
_UpdatePendingInvoices = new Timer(async s =>
|
||||
{
|
||||
foreach (var pending in await _InvoiceRepository.GetPendingInvoices())
|
||||
{
|
||||
_WatchRequests.Add(pending);
|
||||
}
|
||||
}, null, 0, (int)PollInterval.TotalMilliseconds);
|
||||
|
||||
_Poller = StartPoller(_Cts.Token);
|
||||
_Loop = StartLoop(_Cts.Token);
|
||||
|
||||
leases.Add(_EventAggregator.Subscribe<Events.NewBlockEvent>(async b => { await NotifyBlock(); }));
|
||||
leases.Add(_EventAggregator.Subscribe<Events.TxOutReceivedEvent>(async b => { await NotifyReceived(b.ScriptPubKey, b.Network); }));
|
||||
@ -384,53 +370,70 @@ namespace BTCPayServer.HostedServices
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
void Run()
|
||||
|
||||
private async Task StartPoller(CancellationToken cancellation)
|
||||
{
|
||||
Logs.PayServer.LogInformation("Start watching invoices");
|
||||
ConcurrentDictionary<string, Lazy<Task>> updating = new ConcurrentDictionary<string, Lazy<Task>>();
|
||||
try
|
||||
{
|
||||
foreach (var item in _WatchRequests.GetConsumingEnumerable(_Cts.Token))
|
||||
while (!cancellation.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
_Cts.Token.ThrowIfCancellationRequested();
|
||||
var localItem = item;
|
||||
// If the invoice is already updating, ignore
|
||||
Lazy<Task> updateInvoice = new Lazy<Task>(() => UpdateInvoice(localItem), false);
|
||||
if (updating.TryAdd(item, updateInvoice))
|
||||
foreach (var pending in await _InvoiceRepository.GetPendingInvoices())
|
||||
{
|
||||
updateInvoice.Value.ContinueWith(i => updating.TryRemove(item, out updateInvoice));
|
||||
_WatchRequests.Add(pending);
|
||||
}
|
||||
await Task.Delay(PollInterval, cancellation);
|
||||
}
|
||||
catch (Exception ex) when (!_Cts.Token.IsCancellationRequested)
|
||||
catch (Exception ex) when (!cancellation.IsCancellationRequested)
|
||||
{
|
||||
Logs.PayServer.LogCritical(ex, $"Error in the InvoiceWatcher loop (Invoice {item})");
|
||||
_Cts.Token.WaitHandle.WaitOne(2000);
|
||||
Logs.PayServer.LogError(ex, $"Unhandled exception in InvoiceWatcher poller");
|
||||
await Task.Delay(PollInterval, cancellation);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
catch when (cancellation.IsCancellationRequested) { }
|
||||
}
|
||||
|
||||
async Task StartLoop(CancellationToken cancellation)
|
||||
{
|
||||
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>();
|
||||
try
|
||||
{
|
||||
try
|
||||
foreach (var item in _WatchRequests.GetConsumingEnumerable(cancellation))
|
||||
{
|
||||
Task.WaitAll(updating.Select(c => c.Value.Value).ToArray());
|
||||
var task = executing.GetOrAdd(item, async i =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await UpdateInvoice(i, cancellation);
|
||||
}
|
||||
catch (Exception ex) when (!cancellation.IsCancellationRequested)
|
||||
{
|
||||
Logs.PayServer.LogCritical(ex, $"Error in the InvoiceWatcher loop (Invoice {item})");
|
||||
await Task.Delay(2000, cancellation);
|
||||
}
|
||||
finally { executing.TryRemove(item, out Task useless); }
|
||||
});
|
||||
}
|
||||
catch (AggregateException) { }
|
||||
_RunningTask.TrySetResult(true);
|
||||
}
|
||||
catch when (cancellation.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
Logs.PayServer.LogInformation("Stop watching invoices");
|
||||
await Task.WhenAll(executing.Values);
|
||||
}
|
||||
Logs.PayServer.LogInformation("Stop watching invoices");
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
leases.Dispose();
|
||||
_UpdatePendingInvoices.Dispose();
|
||||
_Cts.Cancel();
|
||||
return Task.WhenAny(_RunningTask.Task, Task.Delay(-1, cancellationToken));
|
||||
return Task.WhenAll(_Poller, _Loop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -102,6 +102,7 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
private async Task Listen(BTCPayNetwork network)
|
||||
{
|
||||
bool cleanup = false;
|
||||
try
|
||||
{
|
||||
if (_Sessions.ContainsKey(network.CryptoCode))
|
||||
@ -117,7 +118,7 @@ namespace BTCPayServer.HostedServices
|
||||
await session.DisposeAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
cleanup = true;
|
||||
using (session)
|
||||
{
|
||||
await session.ListenNewBlockAsync(_Cts.Token).ConfigureAwait(false);
|
||||
@ -149,17 +150,20 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
}
|
||||
catch when (_Cts.IsCancellationRequested) { }
|
||||
catch(Exception ex)
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogError(ex, $"Error while connecting to WebSocket of NBXplorer ({network.CryptoCode})");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Logs.PayServer.LogInformation($"Disconnected from WebSocket of NBXplorer ({network.CryptoCode})");
|
||||
_Sessions.TryRemove(network.CryptoCode, out NotificationSession unused);
|
||||
if (_Sessions.Count == 0 && _Cts.IsCancellationRequested)
|
||||
if (cleanup)
|
||||
{
|
||||
_RunningTask.TrySetResult(true);
|
||||
Logs.PayServer.LogInformation($"Disconnected from WebSocket of NBXplorer ({network.CryptoCode})");
|
||||
_Sessions.TryRemove(network.CryptoCode, out NotificationSession unused);
|
||||
if (_Sessions.Count == 0 && _Cts.IsCancellationRequested)
|
||||
{
|
||||
_RunningTask.TrySetResult(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -82,95 +82,106 @@ namespace BTCPayServer.HostedServices
|
||||
BTCPayNetwork _Network;
|
||||
EventAggregator _Aggregator;
|
||||
ExplorerClient _Client;
|
||||
Timer _Timer;
|
||||
ManualResetEventSlim _Idle = new ManualResetEventSlim(true);
|
||||
|
||||
CancellationTokenSource _Cts;
|
||||
Task _Loop;
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Timer = new Timer(Callback, null, 0, (int)TimeSpan.FromMinutes(1.0).TotalMilliseconds);
|
||||
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
_Loop = StartLoop(_Cts.Token);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
void Callback(object state)
|
||||
private async Task StartLoop(CancellationToken cancellation)
|
||||
{
|
||||
if (!_Idle.IsSet)
|
||||
return;
|
||||
Logs.PayServer.LogInformation($"Starting listening NBXplorer ({_Network.CryptoCode})");
|
||||
try
|
||||
{
|
||||
_Idle.Reset();
|
||||
CheckStatus().GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogError(ex, "Error while checking NBXplorer state");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_Idle.Set();
|
||||
while (!cancellation.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
while (await StepAsync(cancellation))
|
||||
{
|
||||
|
||||
}
|
||||
await Task.Delay(PollInterval, cancellation);
|
||||
}
|
||||
catch (Exception ex) when (!cancellation.IsCancellationRequested)
|
||||
{
|
||||
Logs.PayServer.LogError(ex, $"Unhandled exception in NBXplorerWaiter ({_Network.CryptoCode})");
|
||||
await Task.Delay(TimeSpan.FromSeconds(10), cancellation);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch when (cancellation.IsCancellationRequested) { }
|
||||
}
|
||||
|
||||
async Task CheckStatus()
|
||||
{
|
||||
while (await StepAsync())
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> StepAsync()
|
||||
private async Task<bool> StepAsync(CancellationToken cancellation)
|
||||
{
|
||||
var oldState = State;
|
||||
|
||||
StatusResult status = null;
|
||||
switch (State)
|
||||
try
|
||||
{
|
||||
case NBXplorerState.NotConnected:
|
||||
status = await GetStatusWithTimeout();
|
||||
if (status != null)
|
||||
{
|
||||
if (status.IsFullySynched)
|
||||
switch (State)
|
||||
{
|
||||
case NBXplorerState.NotConnected:
|
||||
status = await _Client.GetStatusAsync(cancellation);
|
||||
if (status != null)
|
||||
{
|
||||
if (status.IsFullySynched)
|
||||
{
|
||||
State = NBXplorerState.Ready;
|
||||
}
|
||||
else
|
||||
{
|
||||
State = NBXplorerState.Synching;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case NBXplorerState.Synching:
|
||||
status = await _Client.GetStatusAsync(cancellation);
|
||||
if (status == null)
|
||||
{
|
||||
State = NBXplorerState.NotConnected;
|
||||
}
|
||||
else if (status.IsFullySynched)
|
||||
{
|
||||
State = NBXplorerState.Ready;
|
||||
}
|
||||
else
|
||||
break;
|
||||
case NBXplorerState.Ready:
|
||||
status = await _Client.GetStatusAsync(cancellation);
|
||||
if (status == null)
|
||||
{
|
||||
State = NBXplorerState.NotConnected;
|
||||
}
|
||||
else if (!status.IsFullySynched)
|
||||
{
|
||||
State = NBXplorerState.Synching;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case NBXplorerState.Synching:
|
||||
status = await GetStatusWithTimeout();
|
||||
if (status == null)
|
||||
{
|
||||
State = NBXplorerState.NotConnected;
|
||||
}
|
||||
else if (status.IsFullySynched)
|
||||
{
|
||||
State = NBXplorerState.Ready;
|
||||
}
|
||||
break;
|
||||
case NBXplorerState.Ready:
|
||||
status = await GetStatusWithTimeout();
|
||||
if (status == null)
|
||||
{
|
||||
State = NBXplorerState.NotConnected;
|
||||
}
|
||||
else if (!status.IsFullySynched)
|
||||
{
|
||||
State = NBXplorerState.Synching;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
catch when (cancellation.IsCancellationRequested)
|
||||
{
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
State = NBXplorerState.NotConnected;
|
||||
Logs.PayServer.LogError(ex, $"Error while trying to connect to NBXplorer ({_Network.CryptoCode})");
|
||||
}
|
||||
if (oldState != State)
|
||||
{
|
||||
if (State == NBXplorerState.Synching)
|
||||
{
|
||||
SetInterval(TimeSpan.FromSeconds(10));
|
||||
PollInterval = TimeSpan.FromSeconds(10);
|
||||
}
|
||||
else
|
||||
{
|
||||
SetInterval(TimeSpan.FromMinutes(1));
|
||||
PollInterval = TimeSpan.FromMinutes(1);
|
||||
}
|
||||
_Aggregator.Publish(new NBXplorerStateChangedEvent(_Network, oldState, State));
|
||||
}
|
||||
@ -178,43 +189,14 @@ namespace BTCPayServer.HostedServices
|
||||
return oldState != State;
|
||||
}
|
||||
|
||||
private void SetInterval(TimeSpan interval)
|
||||
{
|
||||
try
|
||||
{
|
||||
_Timer.Change(0, (int)interval.TotalMilliseconds);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private async Task<StatusResult> GetStatusWithTimeout()
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource();
|
||||
using (cts)
|
||||
{
|
||||
var cancellation = cts.Token;
|
||||
while (!cancellation.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var status = await _Client.GetStatusAsync(cancellation).ConfigureAwait(false);
|
||||
return status;
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public TimeSpan PollInterval { get; set; } = TimeSpan.FromMinutes(1.0);
|
||||
|
||||
public NBXplorerState State { get; private set; }
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Timer.Dispose();
|
||||
_Timer = null;
|
||||
_Idle.Wait();
|
||||
return Task.CompletedTask;
|
||||
_Cts.Cancel();
|
||||
return _Loop;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -205,11 +205,9 @@ namespace BTCPayServer.Hosting
|
||||
act();
|
||||
return;
|
||||
}
|
||||
catch
|
||||
catch when(!cts.IsCancellationRequested)
|
||||
{
|
||||
if (cts.IsCancellationRequested)
|
||||
throw;
|
||||
Thread.Sleep(1000);
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -99,39 +99,73 @@ namespace BTCPayServer.Hosting
|
||||
|
||||
private void RewriteHostIfNeeded(HttpContext httpContext)
|
||||
{
|
||||
string reverseProxyScheme = null;
|
||||
if (httpContext.Request.Headers.TryGetValue("X-Forwarded-Proto", out StringValues proto))
|
||||
{
|
||||
var scheme = proto.SingleOrDefault();
|
||||
if (scheme != null)
|
||||
{
|
||||
reverseProxyScheme = scheme;
|
||||
}
|
||||
}
|
||||
|
||||
ushort? reverseProxyPort = null;
|
||||
if (httpContext.Request.Headers.TryGetValue("X-Forwarded-Port", out StringValues port))
|
||||
{
|
||||
var portString = port.SingleOrDefault();
|
||||
if (portString != null && ushort.TryParse(portString, out ushort pp))
|
||||
{
|
||||
reverseProxyPort = pp;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure that code executing after this point think that the external url has been hit.
|
||||
if (_Options.ExternalUrl != null)
|
||||
{
|
||||
httpContext.Request.Scheme = _Options.ExternalUrl.Scheme;
|
||||
if (reverseProxyScheme != null && _Options.ExternalUrl.Scheme != reverseProxyScheme)
|
||||
{
|
||||
if (reverseProxyScheme == "http" && _Options.ExternalUrl.Scheme == "https")
|
||||
Logs.PayServer.LogWarning($"BTCPay ExternalUrl setting expected to use scheme '{_Options.ExternalUrl.Scheme}' externally, but the reverse proxy uses scheme '{reverseProxyScheme}'");
|
||||
httpContext.Request.Scheme = reverseProxyScheme;
|
||||
}
|
||||
else
|
||||
{
|
||||
httpContext.Request.Scheme = _Options.ExternalUrl.Scheme;
|
||||
}
|
||||
if (_Options.ExternalUrl.IsDefaultPort)
|
||||
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host);
|
||||
else
|
||||
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host, _Options.ExternalUrl.Port);
|
||||
{
|
||||
if (reverseProxyPort != null && _Options.ExternalUrl.Port != reverseProxyPort.Value)
|
||||
{
|
||||
Logs.PayServer.LogWarning($"BTCPay ExternalUrl setting expected to use port '{_Options.ExternalUrl.Port}' externally, but the reverse proxy uses port '{reverseProxyPort.Value}'");
|
||||
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host, reverseProxyPort.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host, _Options.ExternalUrl.Port);
|
||||
}
|
||||
}
|
||||
}
|
||||
// NGINX pass X-Forwarded-Proto and X-Forwarded-Port, so let's use that to have better guess of the real domain
|
||||
else
|
||||
{
|
||||
ushort? p = null;
|
||||
if (httpContext.Request.Headers.TryGetValue("X-Forwarded-Proto", out StringValues proto))
|
||||
if (reverseProxyScheme != null)
|
||||
{
|
||||
var scheme = proto.SingleOrDefault();
|
||||
if (scheme != null)
|
||||
{
|
||||
httpContext.Request.Scheme = scheme;
|
||||
if (scheme == "http")
|
||||
p = 80;
|
||||
if (scheme == "https")
|
||||
p = 443;
|
||||
}
|
||||
httpContext.Request.Scheme = reverseProxyScheme;
|
||||
if (reverseProxyScheme == "http")
|
||||
p = 80;
|
||||
if (reverseProxyScheme == "https")
|
||||
p = 443;
|
||||
}
|
||||
if (httpContext.Request.Headers.TryGetValue("X-Forwarded-Port", out StringValues port))
|
||||
|
||||
|
||||
if (reverseProxyPort != null)
|
||||
{
|
||||
var portString = port.SingleOrDefault();
|
||||
if (portString != null && ushort.TryParse(portString, out ushort pp))
|
||||
{
|
||||
p = pp;
|
||||
}
|
||||
p = reverseProxyPort.Value;
|
||||
}
|
||||
|
||||
if (p.HasValue)
|
||||
{
|
||||
bool isDefault = httpContext.Request.Scheme == "http" && p.Value == 80;
|
||||
|
@ -7,6 +7,13 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
{
|
||||
public class PaymentModel
|
||||
{
|
||||
public class AvailableCrypto
|
||||
{
|
||||
public string CryptoCode { get; set; }
|
||||
public string CryptoImage { get; set; }
|
||||
public string Link { get; set; }
|
||||
}
|
||||
public List<AvailableCrypto> AvailableCryptos { get; set; } = new List<AvailableCrypto>();
|
||||
public string CryptoCode { get; set; }
|
||||
public string ServerUrl { get; set; }
|
||||
public string InvoiceId { get; set; }
|
||||
@ -23,15 +30,14 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public string ItemDesc { get; set; }
|
||||
public string TimeLeft { get; set; }
|
||||
public string Rate { get; set; }
|
||||
public string BtcAmount { get; set; }
|
||||
public string TxFees { get; set; }
|
||||
public string OrderAmount { get; set; }
|
||||
public string InvoiceBitcoinUrl { get; set; }
|
||||
public string BtcTotalDue { get; set; }
|
||||
public int TxCount { get; set; }
|
||||
public string BtcPaid { get; set; }
|
||||
public string StoreEmail { get; set; }
|
||||
|
||||
public string OrderId { get; set; }
|
||||
public string CryptoImage { get; set; }
|
||||
public string NetworkFeeDescription { get; internal set; }
|
||||
}
|
||||
}
|
||||
|
@ -495,10 +495,13 @@ namespace BTCPayServer.Services.Invoices
|
||||
.OrderByDescending(p => p.ReceivedTime)
|
||||
.Select(_ =>
|
||||
{
|
||||
paidTxFee = _.GetValue(cryptoData, CryptoCode, cryptoData[_.GetCryptoCode()].TxFee);
|
||||
var txFee = _.GetValue(cryptoData, CryptoCode, cryptoData[_.GetCryptoCode()].TxFee);
|
||||
paid += _.GetValue(cryptoData, CryptoCode);
|
||||
if(!paidEnough)
|
||||
totalDue += paidTxFee;
|
||||
if (!paidEnough)
|
||||
{
|
||||
totalDue += txFee;
|
||||
paidTxFee += txFee;
|
||||
}
|
||||
paidEnough |= totalDue <= paid;
|
||||
if (CryptoCode == _.GetCryptoCode())
|
||||
{
|
||||
@ -513,6 +516,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
txCount++;
|
||||
totalDue += TxFee;
|
||||
paidTxFee += TxFee;
|
||||
}
|
||||
var accounting = new CryptoDataAccounting();
|
||||
accounting.TotalDue = totalDue;
|
||||
@ -520,7 +524,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
accounting.TxCount = txCount;
|
||||
accounting.CryptoPaid = cryptoPaid;
|
||||
accounting.Due = Money.Max(accounting.TotalDue - accounting.Paid, Money.Zero);
|
||||
accounting.NetworkFee = TxFee * txCount;
|
||||
accounting.NetworkFee = paidTxFee;
|
||||
return accounting;
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,7 @@ namespace BTCPayServer.Services.Wallets
|
||||
public uint256 UnconfirmedHash { get; set; }
|
||||
public uint256 ConfirmedHash { get; set; }
|
||||
}
|
||||
public class GetCoinsResult
|
||||
public class NetworkCoins
|
||||
{
|
||||
public Coin[] Coins { get; set; }
|
||||
public KnownState State { get; set; }
|
||||
@ -26,16 +26,12 @@ namespace BTCPayServer.Services.Wallets
|
||||
public class BTCPayWallet
|
||||
{
|
||||
private ExplorerClientProvider _Client;
|
||||
ApplicationDbContextFactory _DBFactory;
|
||||
|
||||
public BTCPayWallet(ExplorerClientProvider client, ApplicationDbContextFactory factory)
|
||||
public BTCPayWallet(ExplorerClientProvider client)
|
||||
{
|
||||
if (client == null)
|
||||
throw new ArgumentNullException(nameof(client));
|
||||
if (factory == null)
|
||||
throw new ArgumentNullException(nameof(factory));
|
||||
_Client = client;
|
||||
_DBFactory = factory;
|
||||
}
|
||||
|
||||
|
||||
@ -58,12 +54,14 @@ namespace BTCPayServer.Services.Wallets
|
||||
return client.GetTransactionAsync(txId, cancellation);
|
||||
}
|
||||
|
||||
public async Task<GetCoinsResult> GetCoins(DerivationStrategy strategy, KnownState state, CancellationToken cancellation = default(CancellationToken))
|
||||
public async Task<NetworkCoins> GetCoins(DerivationStrategy strategy, KnownState state, CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
var client = _Client.GetExplorerClient(strategy.Network);
|
||||
if (client == null)
|
||||
return new NetworkCoins() { Coins = new Coin[0], State = null, Strategy = strategy };
|
||||
var changes = await client.SyncAsync(strategy.DerivationStrategyBase, state?.ConfirmedHash, state?.UnconfirmedHash, true, cancellation).ConfigureAwait(false);
|
||||
var utxos = changes.Confirmed.UTXOs.Concat(changes.Unconfirmed.UTXOs).Select(c => c.AsCoin()).ToArray();
|
||||
return new GetCoinsResult()
|
||||
return new NetworkCoins()
|
||||
{
|
||||
Coins = utxos,
|
||||
State = new KnownState() { ConfirmedHash = changes.Confirmed.Hash, UnconfirmedHash = changes.Unconfirmed.Hash },
|
||||
@ -78,6 +76,7 @@ namespace BTCPayServer.Services.Wallets
|
||||
return Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
|
||||
public async Task<Money> GetBalance(DerivationStrategy derivationStrategy)
|
||||
{
|
||||
var client = _Client.GetExplorerClient(derivationStrategy.Network);
|
||||
|
@ -123,14 +123,14 @@
|
||||
<div class="line-items">
|
||||
<!---->
|
||||
<div class="line-items__item">
|
||||
<div class="line-items__item__label" i18n="">Payment Amount</div>
|
||||
<div class="line-items__item__value">{{srvModel.btcAmount}} {{ srvModel.cryptoCode }}</div>
|
||||
<div class="line-items__item__label" i18n="">Order Amount</div>
|
||||
<div class="line-items__item__value">{{srvModel.orderAmount}} {{ srvModel.cryptoCode }}</div>
|
||||
</div>
|
||||
<div class="line-items__item">
|
||||
<div class="line-items__item__label">
|
||||
<span i18n="">Network Cost</span>
|
||||
</div>
|
||||
<div class="line-items__item__value" i18n="">{{srvModel.txCount }} transaction x {{ srvModel.txFees}} {{ srvModel.cryptoCode }}</div>
|
||||
<div class="line-items__item__value" i18n="">{{srvModel.networkFeeDescription }}</div>
|
||||
</div>
|
||||
<div class="line-items__item">
|
||||
<div class="line-items__item__label">
|
||||
@ -158,6 +158,7 @@
|
||||
<div adjust-height="" class="payment-box">
|
||||
<div class="bp-view payment scan" id="scan">
|
||||
<div class="payment__scan">
|
||||
<img :src="srvModel.cryptoImage" style="position: absolute; height:64px; width:64px; left:118px; top:96px;" />
|
||||
<qrcode :val="srvModel.invoiceBitcoinUrl" :size="256" bg-color="#f5f5f7" fg-color="#000" />
|
||||
</div>
|
||||
<div class="payment__details__instruction__open-wallet">
|
||||
@ -360,7 +361,7 @@
|
||||
<div class="manual-box__address__value copy-cursor" ngxclipboard="">
|
||||
<div class="manual-box__address__wrapper">
|
||||
<div class="manual-box__address__wrapper__logo">
|
||||
<img src="@Model.CryptoImage">
|
||||
<img :src="srvModel.cryptoImage" />
|
||||
</div>
|
||||
<div class="manual-box__address__wrapper__value">{{srvModel.btcAddress}}</div>
|
||||
</div>
|
||||
@ -605,8 +606,17 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div class="footer__item no-hover" style="opacity: 1; padding-left: 0; max-height: 21px; display: none;">
|
||||
<div></div>
|
||||
<div class="footer__item no-hover" style="opacity: 1; padding-left: 0; max-height: 21px;">
|
||||
@if(Model.AvailableCryptos.Count > 1)
|
||||
{
|
||||
<div style="text-align:center">Accepted here</div>
|
||||
<div style="text-align:center">
|
||||
@foreach(var crypto in Model.AvailableCryptos)
|
||||
{
|
||||
<a style="text-decoration:none;" href="@crypto.Link" onclick="srvModel.cryptoCode='@crypto.CryptoCode'; return false;"><img style="height:32px; margin-right:5px; margin-left:5px;" alt="@crypto.CryptoCode" src="@crypto.CryptoImage" /></a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -133,9 +133,9 @@
|
||||
@if(!line.Status.IsFullySynched && line.Status.BitcoinStatus != null)
|
||||
{
|
||||
<div class="progress">
|
||||
<div class="progress-bar" role="progressbar" aria-valuenow="@((int)line.Status.BitcoinStatus.VerificationProgress * 100)"
|
||||
aria-valuemin="0" aria-valuemax="100" style="width:@((int)line.Status.BitcoinStatus.VerificationProgress * 100)%">
|
||||
@((int)line.Status.BitcoinStatus.VerificationProgress * 100)%
|
||||
<div class="progress-bar" role="progressbar" aria-valuenow="@((int)(line.Status.BitcoinStatus.VerificationProgress * 100))"
|
||||
aria-valuemin="0" aria-valuemax="100" style="width:@((int)(line.Status.BitcoinStatus.VerificationProgress * 100))%">
|
||||
@((int)(line.Status.BitcoinStatus.VerificationProgress * 100))%
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
Reference in New Issue
Block a user