Compare commits

...

10 Commits

14 changed files with 394 additions and 323 deletions

View File

@ -38,7 +38,7 @@ services:
- postgres
bitcoin-nbxplorer:
image: nicolasdorier/nbxplorer:1.0.0.40
image: nicolasdorier/nbxplorer:1.0.0.45
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.45
ports:
- "32839:32839"
expose:

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.0.0.60</Version>
<Version>1.0.0.63</Version>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Build\dockerfiles\**" />
@ -24,7 +24,7 @@
<PackageReference Include="NBitcoin" Version="4.0.0.51" />
<PackageReference Include="NBitpayClient" Version="1.0.0.14" />
<PackageReference Include="DBreeze" Version="1.87.0" />
<PackageReference Include="NBXplorer.Client" Version="1.0.0.26" />
<PackageReference Include="NBXplorer.Client" Version="1.0.0.28" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.1" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.2" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.13" />

View File

@ -56,6 +56,7 @@ namespace BTCPayServer.Controllers
Fiat = FormatCurrency((decimal)dto.Price, dto.Currency),
NotificationUrl = invoice.NotificationURL,
ProductInformation = invoice.ProductInformation,
StatusException = invoice.ExceptionStatus
};
foreach (var data in invoice.GetCryptoData())
@ -74,7 +75,7 @@ namespace BTCPayServer.Controllers
}
var payments = invoice
.Payments
.GetPayments()
.Select(async payment =>
{
var m = new InvoiceDetailsModel.Payment();
@ -85,6 +86,7 @@ namespace BTCPayServer.Controllers
m.TransactionId = payment.Outpoint.Hash.ToString();
m.ReceivedTime = payment.ReceivedTime;
m.TransactionLink = string.Format(paymentNetwork.BlockExplorerLink, m.TransactionId);
m.Replaced = !payment.Accounted;
return m;
})
.ToArray();
@ -118,7 +120,8 @@ namespace BTCPayServer.Controllers
private async Task<PaymentModel> GetInvoiceModel(string invoiceId, string cryptoCode)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
if (invoice == null)
return null;
var store = await _StoreRepository.FindStore(invoice.StoreId);
if (cryptoCode == null)
cryptoCode = store.GetDefaultCrypto();

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

@ -34,6 +34,8 @@ namespace BTCPayServer
public ExplorerClient GetExplorerClient(BTCPayNetwork network)
{
if (network == null)
throw new ArgumentNullException(nameof(network));
return GetExplorerClient(network.CryptoCode);
}

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);
}
}
}
@ -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,153 +149,231 @@ namespace BTCPayServer.HostedServices
invoice.Status = "expired";
}
if (invoice.Status == "new" || invoice.Status == "expired")
var derivationStrategies = invoice.GetDerivationStrategies(_NetworkProvider).ToArray();
foreach (NetworkCoins coins in await GetCoinsPerNetwork(context, invoice, derivationStrategies))
{
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.GetPayments(coins.Strategy.Network).Select(p => p.Outpoint));
foreach (var coin in coins.TimestampedCoins.Where(c => !alreadyAccounted.Contains(c.Coin.Outpoint)))
{
if (invoice.Status == "new")
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.DateTime, coin.Coin, coins.Strategy.Network.CryptoCode).ConfigureAwait(false);
#pragma warning disable CS0618
invoice.Payments.Add(payment);
#pragma warning restore CS0618
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(derivationStrategies, 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.GetPayments().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(derivationStrategies, 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")
{
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 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)
if (invoice.Status == "confirmed")
{
var transactions = await GetPaymentsWithTransaction(derivationStrategies, invoice);
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();
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();
}
}
}
}
private async Task<IEnumerable<AccountedPaymentEntity>> GetPaymentsWithTransaction(BTCPayNetwork network, InvoiceEntity invoice)
private async Task<IEnumerable<NetworkCoins>> GetCoinsPerNetwork(UpdateInvoiceContext context, InvoiceEntity invoice, DerivationStrategy[] strategies)
{
var transactions = await _Wallet.GetTransactions(network, invoice.Payments.Select(t => t.Outpoint.Hash).ToArray());
var spentTxIn = new Dictionary<OutPoint, AccountedPaymentEntity>();
var result = invoice.Payments.Select(p => p.Outpoint).ToHashSet();
List<AccountedPaymentEntity> payments = new List<AccountedPaymentEntity>();
foreach (var payment in invoice.Payments)
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)
{
TransactionResult tx;
if (!transactions.TryGetValue(payment.Outpoint.Hash, out tx))
{
result.Remove(payment.Outpoint);
continue;
}
AccountedPaymentEntity accountedPayment = new AccountedPaymentEntity()
{
Confirmations = tx.Confirmations,
Transaction = tx.Transaction,
Payment = payment
};
payments.Add(accountedPayment);
foreach (var txin in tx.Transaction.Inputs)
{
if (!spentTxIn.TryAdd(txin.PrevOut, accountedPayment))
{
//We get a double spend
var existing = spentTxIn[txin.PrevOut];
response.TimestampedCoins = response.TimestampedCoins.Where(c => invoice.AvailableAddressHashes.Contains(c.Coin.ScriptPubKey.Hash.ToString() + response.Strategy.Network.CryptoCode)).ToArray();
}
return getCoinsResponses.Where(s => s.TimestampedCoins.Length != 0).ToArray();
}
//Take the most recent, the full node is already comparing fees correctly so we have the most likely to be confirmed
if (accountedPayment.Confirmations > 1 || existing.Payment.ReceivedTime < accountedPayment.Payment.ReceivedTime)
{
spentTxIn[txin.PrevOut] = accountedPayment;
result.Remove(existing.Payment.Outpoint);
}
private async Task<IEnumerable<AccountedPaymentEntity>> GetPaymentsWithTransaction(DerivationStrategy[] derivations, InvoiceEntity invoice)
{
List<PaymentEntity> updatedPaymentEntities = new List<PaymentEntity>();
List<AccountedPaymentEntity> accountedPayments = new List<AccountedPaymentEntity>();
foreach (var network in derivations.Select(d => d.Network))
{
var transactions = await _Wallet.GetTransactions(network, invoice.GetPayments(network).Select(t => t.Outpoint.Hash).ToArray());
var conflicts = GetConflicts(transactions.Select(t => t.Value));
foreach (var payment in invoice.GetPayments(network))
{
TransactionResult tx;
if (!transactions.TryGetValue(payment.Outpoint.Hash, out tx))
continue;
AccountedPaymentEntity accountedPayment = new AccountedPaymentEntity()
{
Confirmations = tx.Confirmations,
Transaction = tx.Transaction,
Payment = payment
};
var txId = accountedPayment.Transaction.GetHash();
var txConflict = conflicts.GetConflict(txId);
var accounted = txConflict == null || txConflict.IsWinner(txId);
if (accounted != payment.Accounted)
{
updatedPaymentEntities.Add(payment);
payment.Accounted = accounted;
}
if (accounted)
accountedPayments.Add(accountedPayment);
}
}
List<PaymentEntity> updated = new List<PaymentEntity>();
var accountedPayments = payments.Where(p =>
{
var accounted = result.Contains(p.Payment.Outpoint);
if (p.Payment.Accounted != accounted)
{
p.Payment.Accounted = accounted;
updated.Add(p.Payment);
}
return accounted;
}).ToArray();
await _InvoiceRepository.UpdatePayments(payments);
await _InvoiceRepository.UpdatePayments(updatedPaymentEntities);
return accountedPayments;
}
class TransactionConflict
{
public Dictionary<uint256, TransactionResult> Transactions { get; set; } = new Dictionary<uint256, TransactionResult>();
uint256 _Winner;
public bool IsWinner(uint256 txId)
{
if (_Winner == null)
{
var confirmed = Transactions.FirstOrDefault(t => t.Value.Confirmations >= 1);
if (!confirmed.Equals(default(KeyValuePair<uint256, TransactionResult>)))
{
_Winner = confirmed.Key;
}
else
{
// Take the most recent (bitcoin node would not forward a conflict without a successfull RBF)
_Winner = Transactions
.OrderByDescending(t => t.Value.Timestamp)
.First()
.Key;
}
}
return _Winner == txId;
}
}
class TransactionConflicts : List<TransactionConflict>
{
public TransactionConflicts(IEnumerable<TransactionConflict> collection) : base(collection)
{
}
public TransactionConflict GetConflict(uint256 txId)
{
return this.FirstOrDefault(c => c.Transactions.ContainsKey(txId));
}
}
private TransactionConflicts GetConflicts(IEnumerable<TransactionResult> transactions)
{
Dictionary<OutPoint, TransactionConflict> conflictsByOutpoint = new Dictionary<OutPoint, TransactionConflict>();
foreach (var tx in transactions)
{
var hash = tx.Transaction.GetHash();
foreach (var input in tx.Transaction.Inputs)
{
TransactionConflict conflict = new TransactionConflict();
if (!conflictsByOutpoint.TryAdd(input.PrevOut, conflict))
{
conflict = conflictsByOutpoint[input.PrevOut];
}
if (!conflict.Transactions.ContainsKey(hash))
conflict.Transactions.Add(hash, tx);
}
}
return new TransactionConflicts(conflictsByOutpoint.Where(c => c.Value.Transactions.Count > 1).Select(c => c.Value));
}
TimeSpan _PollInterval;
public TimeSpan PollInterval
{
@ -336,10 +384,6 @@ namespace BTCPayServer.HostedServices
set
{
_PollInterval = value;
if (_UpdatePendingInvoices != null)
{
_UpdatePendingInvoices.Change(0, (int)value.TotalMilliseconds);
}
}
}
@ -352,30 +396,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 +414,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

@ -48,6 +48,8 @@ namespace BTCPayServer.Models.InvoicingModels
get;
set;
}
public bool Replaced { get; set; }
}
public string StatusMessage
@ -70,7 +72,7 @@ namespace BTCPayServer.Models.InvoicingModels
{
get; set;
}
public string StatusException { get; set; }
public DateTimeOffset CreatedDate
{
get; set;

View File

@ -180,7 +180,7 @@ namespace BTCPayServer.Services.Invoices
var network = networks.GetNetwork(strat.Name);
if (network != null)
{
if (network == networks.BTC && btcReturned)
if (network == networks.BTC)
btcReturned = true;
yield return BTCPayServer.DerivationStrategy.Parse(strat.Value.Value<string>(), network);
}
@ -220,10 +220,27 @@ namespace BTCPayServer.Services.Invoices
{
get; set;
}
[Obsolete("Use GetPayments instead")]
public List<PaymentEntity> Payments
{
get; set;
}
#pragma warning disable CS0618
public List<PaymentEntity> GetPayments()
{
return Payments.ToList();
}
public List<PaymentEntity> GetPayments(string cryptoCode)
{
return Payments.Where(p=>p.CryptoCode == cryptoCode).ToList();
}
public List<PaymentEntity> GetPayments(BTCPayNetwork network)
{
return GetPayments(network.CryptoCode);
}
#pragma warning restore CS0618
public bool Refundable
{
get;
@ -490,9 +507,9 @@ namespace BTCPayServer.Services.Invoices
bool paidEnough = totalDue <= paid;
int txCount = 0;
var payments =
ParentEntity.Payments
ParentEntity.GetPayments()
.Where(p => p.Accounted)
.OrderByDescending(p => p.ReceivedTime)
.OrderBy(p => p.ReceivedTime)
.Select(_ =>
{
var txFee = _.GetValue(cryptoData, CryptoCode, cryptoData[_.GetCryptoCode()].TxFee);

View File

@ -107,7 +107,9 @@ namespace BTCPayServer.Services.Invoices
List<string> textSearch = new List<string>();
invoice = Clone(invoice);
invoice.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16));
#pragma warning disable CS0618
invoice.Payments = new List<PaymentEntity>();
#pragma warning restore CS0618
invoice.StoreId = storeId;
using (var context = _ContextFactory.CreateContext())
{
@ -309,12 +311,14 @@ namespace BTCPayServer.Services.Invoices
private InvoiceEntity ToEntity(InvoiceData invoice)
{
var entity = ToObject<InvoiceEntity>(invoice.Blob);
#pragma warning disable CS0618
entity.Payments = invoice.Payments.Select(p =>
{
var paymentEntity = ToObject<PaymentEntity>(p.Blob);
paymentEntity.Accounted = p.Accounted;
return paymentEntity;
}).ToList();
#pragma warning restore CS0618
entity.ExceptionStatus = invoice.ExceptionStatus;
entity.Status = invoice.Status;
entity.RefundMail = invoice.CustomerEmail;
@ -419,7 +423,7 @@ namespace BTCPayServer.Services.Invoices
AddToTextSearch(invoiceId, addresses.Select(a => a.ToString()).ToArray());
}
public async Task<PaymentEntity> AddPayment(string invoiceId, Coin receivedCoin, string cryptoCode)
public async Task<PaymentEntity> AddPayment(string invoiceId, DateTimeOffset date, Coin receivedCoin, string cryptoCode)
{
using (var context = _ContextFactory.CreateContext())
{
@ -430,7 +434,7 @@ namespace BTCPayServer.Services.Invoices
Output = receivedCoin.TxOut,
CryptoCode = cryptoCode,
#pragma warning restore CS0618
ReceivedTime = DateTime.UtcNow
ReceivedTime = date.UtcDateTime
};
PaymentData data = new PaymentData
@ -448,7 +452,7 @@ namespace BTCPayServer.Services.Invoices
}
}
public async Task UpdatePayments(List<AccountedPaymentEntity> payments)
public async Task UpdatePayments(List<PaymentEntity> payments)
{
if (payments.Count == 0)
return;
@ -457,8 +461,8 @@ namespace BTCPayServer.Services.Invoices
foreach (var payment in payments)
{
var data = new PaymentData();
data.Id = payment.Payment.Outpoint.ToString();
data.Accounted = payment.Payment.Accounted;
data.Id = payment.Outpoint.ToString();
data.Accounted = payment.Accounted;
context.Attach(data);
context.Entry(data).Property(o => o.Accounted).IsModified = true;
}

View File

@ -17,25 +17,26 @@ 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 class TimestampedCoin
{
public DateTimeOffset DateTime { get; set; }
public Coin Coin { get; set; }
}
public TimestampedCoin[] TimestampedCoins { get; set; }
public KnownState State { get; set; }
public DerivationStrategy Strategy { get; set; }
}
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;
}
@ -54,18 +55,23 @@ namespace BTCPayServer.Services.Wallets
public Task<TransactionResult> GetTransactionAsync(BTCPayNetwork network, uint256 txId, CancellationToken cancellation = default(CancellationToken))
{
if (network == null)
throw new ArgumentNullException(nameof(network));
if (txId == null)
throw new ArgumentNullException(nameof(txId));
var client = _Client.GetExplorerClient(network);
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() { TimestampedCoins = new NetworkCoins.TimestampedCoin[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,
TimestampedCoins = changes.Confirmed.UTXOs.Concat(changes.Unconfirmed.UTXOs).Select(c => new NetworkCoins.TimestampedCoin() { Coin = c.AsCoin(), DateTime = c.Timestamp }).ToArray(),
State = new KnownState() { ConfirmedHash = changes.Confirmed.Hash, UnconfirmedHash = changes.Unconfirmed.Hash },
Strategy = strategy,
};
@ -78,6 +84,7 @@ namespace BTCPayServer.Services.Wallets
return Task.WhenAll(tasks);
}
public async Task<Money> GetBalance(DerivationStrategy derivationStrategy)
{
var client = _Client.GetExplorerClient(derivationStrategy.Network);

View File

@ -613,7 +613,7 @@
<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>
<a style="text-decoration:none;" href="@crypto.Link" onclick="srvModel.cryptoCode='@crypto.CryptoCode'; fetchStatus(); return false;"><img style="height:32px; margin-right:5px; margin-left:5px;" alt="@crypto.CryptoCode" src="@crypto.CryptoImage" /></a>
}
</div>
}

View File

@ -57,6 +57,10 @@
<th>Status</th>
<td>@Model.Status</td>
</tr>
<tr>
<th>Status Exception</th>
<td>@Model.StatusException</td>
</tr>
<tr>
<th>Refund email</th>
<td>@Model.RefundEmail</td>
@ -174,6 +178,7 @@
<th>Deposit address</th>
<th>Transaction Id</th>
<th>Confirmations</th>
<th>Replaced</th>
</tr>
</thead>
<tbody>
@ -185,6 +190,7 @@
<td>@payment.DepositAddress</td>
<td><a href="@payment.TransactionLink" target="_blank">@payment.TransactionId</a></td>
<td>@payment.Confirmations</td>
<td>@payment.Replaced</td>
</tr>
}
</tbody>