Compare commits

...

6 Commits

10 changed files with 162 additions and 154 deletions

View File

@ -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.42
ports:
- "32839:32839"
expose:

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.0.0.59</Version>
<Version>1.0.0.61</Version>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Build\dockerfiles\**" />

View File

@ -153,7 +153,13 @@ namespace BTCPayServer.Controllers
BtcPaid = accounting.Paid.ToString(),
Status = invoice.Status,
CryptoImage = "/" + Url.Content(network.CryptoImagePath),
NetworkFeeDescription = $"{accounting.TxCount} transaction{(accounting.TxCount > 1 ? "s" : "")} x {cryptoData.TxFee} {network.CryptoCode}"
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;

View File

@ -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")]

View File

@ -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);
}
}
}
@ -143,7 +143,7 @@ namespace BTCPayServer.HostedServices
//Fetch unknown payments
var strategies = invoice.GetDerivationStrategies(_NetworkProvider).ToArray();
var getCoinsResponsesAsync = strategies
.Select(d => _Wallet.GetCoins(d, context.KnownStates.TryGet(d.Network), _Cts.Token))
.Select(d => _Wallet.GetCoins(d, context.KnownStates.TryGet(d.Network)))
.ToArray();
await Task.WhenAll(getCoinsResponsesAsync);
var getCoinsResponses = getCoinsResponsesAsync.Select(g => g.Result).ToArray();
@ -155,7 +155,8 @@ namespace BTCPayServer.HostedServices
bool dirtyAddress = false;
if (coins != null)
{
context.ModifiedKnownStates.Add(coins.Strategy.Network, coins.State);
if (coins.State != 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)))
{
@ -336,10 +337,6 @@ namespace BTCPayServer.HostedServices
set
{
_PollInterval = value;
if (_UpdatePendingInvoices != null)
{
_UpdatePendingInvoices.Change(0, (int)value.TotalMilliseconds);
}
}
}
@ -352,30 +349,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 +367,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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -205,11 +205,9 @@ namespace BTCPayServer.Hosting
act();
return;
}
catch
catch when(!cts.IsCancellationRequested)
{
if (cts.IsCancellationRequested)
throw;
Thread.Sleep(1000);
Thread.Sleep(100);
}
}
}

View File

@ -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; }

View File

@ -61,6 +61,8 @@ namespace BTCPayServer.Services.Wallets
public async Task<GetCoinsResult> GetCoins(DerivationStrategy strategy, KnownState state, CancellationToken cancellation = default(CancellationToken))
{
var client = _Client.GetExplorerClient(strategy.Network);
if (client == null)
return new GetCoinsResult() { 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()

View File

@ -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>