Refactoring to keep coin logic out of InvoiceWatcher

This commit is contained in:
nicolas.dorier 2018-02-17 01:34:40 +09:00
parent 66597aed46
commit 5de330b1f9
19 changed files with 504 additions and 333 deletions

View File

@ -629,11 +629,11 @@ namespace BTCPayServer.Tests
Assert.Equal("new", invoice.Status);
Assert.False((bool)((JValue)invoice.ExceptionStatus).Value);
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime));
Assert.Empty(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime + TimeSpan.FromDays(1)));
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5)));
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime));
Assert.Empty(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime - TimeSpan.FromDays(1)));
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime));
Assert.Empty(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime + TimeSpan.FromDays(2)));
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime - TimeSpan.FromDays(5)));
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime + TimeSpan.FromDays(1.0)));
Assert.Empty(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime - TimeSpan.FromDays(1)));
var firstPayment = Money.Coins(0.04m);

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;
@ -63,13 +64,13 @@ namespace BTCPayServer
public string CryptoImagePath { get; set; }
public NBXplorer.NBXplorerNetwork NBXplorerNetwork { get; set; }
public BTCPayDefaultSettings DefaultSettings { get; set; }
public KeyPath CoinType { get; internal set; }
public int MaxTrackedConfirmation { get; internal set; } = 7;
public override string ToString()
{
return CryptoCode;
}
}
}
}

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.0.1.31</Version>
<Version>1.0.1.32</Version>
<NoWarn>NU1701</NoWarn>
</PropertyGroup>
<ItemGroup>

View File

@ -239,6 +239,7 @@ namespace BTCPayServer.Controllers
try
{
leases.Add(_EventAggregator.Subscribe<Events.InvoiceDataChangedEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
leases.Add(_EventAggregator.Subscribe<Events.InvoiceNewAddressEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
while (true)
{

View File

@ -219,7 +219,7 @@ namespace BTCPayServer.Controllers
{
var strategy = GetDirectDerivationStrategy(store, network);
var strategyBase = GetDerivationStrategy(store, network);
if (!await hw.SupportDerivation(network, strategy))
if (strategy == null || !await hw.SupportDerivation(network, strategy))
{
throw new Exception($"This store is not configured to use this ledger");
}
@ -238,12 +238,11 @@ namespace BTCPayServer.Controllers
var change = wallet.GetChangeAddressAsync(strategyBase);
var unspentCoins = await wallet.GetUnspentCoins(strategyBase);
var changeAddress = await change;
unspentCoins.Item2.TryAdd(changeAddress.Item1.ScriptPubKey, changeAddress.Item2);
var transaction = await hw.SendToAddress(strategy, unspentCoins.Item1, network,
var transaction = await hw.SendToAddress(strategy, unspentCoins, network,
new[] { (destinationAddress as IDestination, amountBTC, subsctractFeesValue) },
feeRateValue,
changeAddress.Item1,
unspentCoins.Item2);
changeAddress.Item2);
try
{
var broadcastResult = await wallet.BroadcastTransactionsAsync(new List<Transaction>() { transaction });
@ -256,6 +255,7 @@ namespace BTCPayServer.Controllers
{
throw new Exception("Error while broadcasting: " + ex.Message);
}
wallet.InvalidateCache(strategyBase);
result = new SendToAddressResult() { TransactionId = transaction.GetHash().ToString() };
}
}

View File

@ -88,7 +88,9 @@ namespace BTCPayServer
}
}
Logs.Events.LogInformation(evt.ToString());
var log = evt.ToString();
if(!String.IsNullOrEmpty(log))
Logs.Events.LogInformation(log);
foreach (var sub in actionList)
{
try

View File

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Events
{
public class InvoiceNeedUpdateEvent
{
public InvoiceNeedUpdateEvent(string invoiceId)
{
if (invoiceId == null)
throw new ArgumentNullException(nameof(invoiceId));
InvoiceId = invoiceId;
}
public string InvoiceId { get; set; }
public override string ToString()
{
return string.Empty;
}
}
}

View File

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Events
{
public class InvoiceNewAddressEvent
{
public InvoiceNewAddressEvent(string invoiceId, string address, BTCPayNetwork network)
{
Address = address;
InvoiceId = invoiceId;
Network = network;
}
public string Address { get; set; }
public string InvoiceId { get; set; }
public BTCPayNetwork Network { get; set; }
public override string ToString()
{
return $"{Network.CryptoCode}: New address {Address} for invoice {InvoiceId}";
}
}
}

View File

@ -1,22 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
using NBXplorer.DerivationStrategy;
namespace BTCPayServer.Events
{
public class TxOutReceivedEvent
{
public BTCPayNetwork Network { get; set; }
public Script ScriptPubKey { get; set; }
public DerivationStrategyBase DerivationStrategy { get; set; }
public override string ToString()
{
String address = ScriptPubKey.GetDestinationAddress(Network.NBitcoinNetwork)?.ToString() ?? ScriptPubKey.ToString();
return $"{address} received a transaction ({Network.CryptoCode})";
}
}
}

View File

@ -43,43 +43,19 @@ namespace BTCPayServer.HostedServices
InvoiceRepository _InvoiceRepository;
EventAggregator _EventAggregator;
BTCPayWalletProvider _WalletProvider;
BTCPayNetworkProvider _NetworkProvider;
public InvoiceWatcher(
IHostingEnvironment env,
BTCPayNetworkProvider networkProvider,
InvoiceRepository invoiceRepository,
EventAggregator eventAggregator,
BTCPayWalletProvider walletProvider)
EventAggregator eventAggregator)
{
PollInterval = TimeSpan.FromMinutes(1.0);
_WalletProvider = walletProvider ?? throw new ArgumentNullException(nameof(walletProvider));
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
_EventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_NetworkProvider = networkProvider;
}
CompositeDisposable leases = new CompositeDisposable();
async Task NotifyReceived(Events.TxOutReceivedEvent evt)
{
var invoiceId = await _InvoiceRepository.GetInvoiceIdFromScriptPubKey(evt.ScriptPubKey, evt.Network.CryptoCode);
if (invoiceId != null)
{
String address = evt.ScriptPubKey.GetDestinationAddress(evt.Network.NBitcoinNetwork)?.ToString() ?? evt.ScriptPubKey.ToString();
_WalletProvider.GetWallet(evt.Network).InvalidateCache(evt.DerivationStrategy);
Logs.PayServer.LogInformation($"{address} is mapping to invoice {invoiceId}");
_WatchRequests.Add(invoiceId);
}
}
async Task NotifyBlock()
{
foreach (var invoice in await _InvoiceRepository.GetPendingInvoices())
{
_WatchRequests.Add(invoice);
}
}
CompositeDisposable leases = new CompositeDisposable();
private async Task UpdateInvoice(string invoiceId, CancellationToken cancellation)
{
@ -150,37 +126,18 @@ namespace BTCPayServer.HostedServices
}
var derivationStrategies = invoice.GetDerivationStrategies(_NetworkProvider).ToArray();
var payments = await GetPaymentsWithTransaction(derivationStrategies, invoice);
foreach (Task<NetworkCoins> coinsAsync in GetCoinsPerNetwork(context, invoice, derivationStrategies))
var payments = invoice.GetPayments().Where(p => p.Accounted).ToArray();
foreach (BTCPayNetwork network in _NetworkProvider.GetAll())
{
var coins = await coinsAsync;
if (coins.TimestampedCoins.Length == 0)
continue;
bool dirtyAddress = false;
var alreadyAccounted = new HashSet<OutPoint>(invoice.GetPayments(coins.Wallet.Network).Select(p => p.Outpoint));
foreach (var coin in coins.TimestampedCoins.Where(c => !alreadyAccounted.Contains(c.Coin.Outpoint)))
{
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.DateTime, coin.Coin, coins.Wallet.Network.CryptoCode).ConfigureAwait(false);
#pragma warning disable CS0618
invoice.Payments.Add(payment);
#pragma warning restore CS0618
alreadyAccounted.Add(coin.Coin.Outpoint);
context.Events.Add(new InvoiceEvent(invoice, 1002, "invoice_receivedPayment"));
dirtyAddress = true;
}
if (dirtyAddress)
{
payments = await GetPaymentsWithTransaction(derivationStrategies, invoice);
}
var network = coins.Wallet.Network;
var cryptoData = invoice.GetCryptoData(network, _NetworkProvider);
if (cryptoData == null) // Altcoin not supported
continue;
var cryptoDataAll = invoice.GetCryptoData(_NetworkProvider);
var accounting = cryptoData.Calculate();
if (invoice.Status == "new" || invoice.Status == "expired")
{
var totalPaid = payments.Select(p => p.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
var totalPaid = payments.Select(p => p.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
if (totalPaid >= accounting.TotalDue)
{
if (invoice.Status == "new")
@ -203,32 +160,14 @@ namespace BTCPayServer.HostedServices
{
invoice.ExceptionStatus = "paidPartial";
context.MarkDirty();
if (dirtyAddress)
{
var address = await coins.Wallet.ReserveAddressAsync(coins.Strategy);
Logs.PayServer.LogInformation("Generate new " + address);
await _InvoiceRepository.NewAddress(invoice.Id, address, network);
}
}
}
if (invoice.Status == "paid")
{
IEnumerable<AccountedPaymentEntity> transactions = payments;
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 transactions = payments.Where(p => p.GetCryptoPaymentData().PaymentConfirmed(p, invoice.SpeedPolicy, network));
var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
var totalConfirmed = transactions.Select(t => t.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
if (// Is after the monitoring deadline
(invoice.MonitoringExpiration < DateTimeOffset.UtcNow)
@ -252,9 +191,8 @@ namespace BTCPayServer.HostedServices
if (invoice.Status == "confirmed")
{
IEnumerable<AccountedPaymentEntity> transactions = payments;
transactions = transactions.Where(t => t.Confirmations >= 6);
var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
var transactions = payments.Where(p => p.GetCryptoPaymentData().PaymentCompleted(p, network));
var totalConfirmed = transactions.Select(t => t.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
if (totalConfirmed >= accounting.TotalDue)
{
context.Events.Add(new InvoiceEvent(invoice, 1006, "invoice_completed"));
@ -265,127 +203,6 @@ namespace BTCPayServer.HostedServices
}
}
private IEnumerable<Task<NetworkCoins>> GetCoinsPerNetwork(UpdateInvoiceContext context, InvoiceEntity invoice, DerivationStrategy[] strategies)
{
return strategies
.Select(d => (Wallet: _WalletProvider.IsAvailable(d.Network) ? _WalletProvider.GetWallet(d.Network) : null,
Network: d.Network,
Strategy: d.DerivationStrategyBase))
.Where(d => d.Wallet != null)
.Select(d => (Network: d.Network,
Coins: d.Wallet.GetCoins(d.Strategy)))
.Select(async d =>
{
var coins = await d.Coins;
// Keep only coins from the invoice
coins.TimestampedCoins = coins.TimestampedCoins.Where(c => invoice.AvailableAddressHashes.Contains(c.Coin.ScriptPubKey.Hash.ToString() + d.Network.CryptoCode)).ToArray();
return coins;
})
.ToArray();
}
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 wallet = _WalletProvider.GetWallet(network);
if (wallet == null)
continue;
var transactions = await wallet.GetTransactions(invoice.GetPayments(wallet.Network)
.Select(t => t.Outpoint.Hash)
.ToArray());
var conflicts = GetConflicts(transactions.Select(t => t.Value));
foreach (var payment in invoice.GetPayments(network))
{
if (!transactions.TryGetValue(payment.Outpoint.Hash, out TransactionResult 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);
}
}
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
{
@ -399,11 +216,15 @@ namespace BTCPayServer.HostedServices
}
}
private async Task Watch(string invoiceId)
private void Watch(string invoiceId)
{
if (invoiceId == null)
throw new ArgumentNullException(nameof(invoiceId));
_WatchRequests.Add(invoiceId);
}
private async Task Wait(string invoiceId)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
try
{
@ -411,62 +232,57 @@ namespace BTCPayServer.HostedServices
if (invoice.ExpirationTime > now)
{
await Task.Delay(invoice.ExpirationTime - now, _Cts.Token);
_WatchRequests.Add(invoiceId);
}
_WatchRequests.Add(invoiceId);
now = DateTimeOffset.UtcNow;
if (invoice.MonitoringExpiration > now)
{
await Task.Delay(invoice.MonitoringExpiration - now, _Cts.Token);
}
_WatchRequests.Add(invoiceId);
}
catch when (_Cts.IsCancellationRequested)
{ }
}
BlockingCollection<string> _WatchRequests = new BlockingCollection<string>(new ConcurrentQueue<string>());
Task _Poller;
Task _Loop;
Task _WaitingInvoices;
CancellationTokenSource _Cts;
public Task StartAsync(CancellationToken cancellationToken)
{
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_Poller = StartPoller(_Cts.Token);
_Loop = StartLoop(_Cts.Token);
_WaitingInvoices = WaitPendingInvoices();
leases.Add(_EventAggregator.Subscribe<Events.NewBlockEvent>(async b => { await NotifyBlock(); }));
leases.Add(_EventAggregator.Subscribe<Events.TxOutReceivedEvent>(async b => { await NotifyReceived(b); }));
leases.Add(_EventAggregator.Subscribe<Events.InvoiceNeedUpdateEvent>(async b =>
{
Watch(b.InvoiceId);
}));
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async b =>
{
if (b.Name == "invoice_created")
{
await Watch(b.InvoiceId);
Watch(b.InvoiceId);
await Wait(b.InvoiceId);
}
if (b.Name == "invoice_receivedPayment")
{
Watch(b.InvoiceId);
}
}));
return Task.CompletedTask;
}
private async Task StartPoller(CancellationToken cancellation)
private async Task WaitPendingInvoices()
{
try
{
while (!cancellation.IsCancellationRequested)
{
try
{
foreach (var pending in await _InvoiceRepository.GetPendingInvoices())
{
_WatchRequests.Add(pending);
}
await Task.Delay(PollInterval, cancellation);
}
catch (Exception ex) when (!cancellation.IsCancellationRequested)
{
Logs.PayServer.LogError(ex, $"Unhandled exception in InvoiceWatcher poller");
await Task.Delay(PollInterval, cancellation);
}
}
}
catch when (cancellation.IsCancellationRequested) { }
await Task.WhenAll((await _InvoiceRepository.GetPendingInvoices())
.Select(id => Wait(id)).ToArray());
_WaitingInvoices = null;
}
async Task StartLoop(CancellationToken cancellation)
@ -515,7 +331,7 @@ namespace BTCPayServer.HostedServices
{
leases.Dispose();
_Cts.Cancel();
return Task.WhenAll(_Poller, _Loop);
return _Loop;
}
}
}

View File

@ -13,6 +13,9 @@ using System.Collections.Concurrent;
using NBXplorer.DerivationStrategy;
using BTCPayServer.Events;
using BTCPayServer.Services;
using BTCPayServer.Services.Wallets;
using NBitcoin;
using NBXplorer.Models;
namespace BTCPayServer.HostedServices
{
@ -24,15 +27,15 @@ namespace BTCPayServer.HostedServices
InvoiceRepository _InvoiceRepository;
private TaskCompletionSource<bool> _RunningTask;
private CancellationTokenSource _Cts;
NBXplorerDashboard _Dashboards;
BTCPayWalletProvider _Wallets;
public NBXplorerListener(ExplorerClientProvider explorerClients,
NBXplorerDashboard dashboard,
BTCPayWalletProvider wallets,
InvoiceRepository invoiceRepository,
EventAggregator aggregator, IApplicationLifetime lifetime)
{
PollInterval = TimeSpan.FromMinutes(1.0);
_Dashboards = dashboard;
_Wallets = wallets;
_InvoiceRepository = invoiceRepository;
_ExplorerClients = explorerClients;
_Aggregator = aggregator;
@ -68,17 +71,21 @@ namespace BTCPayServer.HostedServices
{
if (nbxplorerEvent.NewState == NBXplorerState.Ready)
{
await Listen(nbxplorerEvent.Network);
var wallet = _Wallets.GetWallet(nbxplorerEvent.Network);
if (_Wallets.IsAvailable(wallet.Network))
{
await Listen(wallet);
}
}
}));
_ListenPoller = new Timer(async s =>
{
foreach (var nbxplorerState in _Dashboards.GetAll())
foreach (var wallet in _Wallets.GetWallets())
{
if (nbxplorerState.Status != null && nbxplorerState.Status.IsFullySynched)
if (_Wallets.IsAvailable(wallet.Network))
{
await Listen(nbxplorerState.Network);
await Listen(wallet);
}
}
}, null, 0, (int)PollInterval.TotalMilliseconds);
@ -104,8 +111,9 @@ namespace BTCPayServer.HostedServices
return Task.CompletedTask;
}
private async Task Listen(BTCPayNetwork network)
private async Task Listen(BTCPayWallet wallet)
{
var network = wallet.Network;
bool cleanup = false;
try
{
@ -123,10 +131,16 @@ namespace BTCPayServer.HostedServices
return;
}
cleanup = true;
using (session)
{
await session.ListenNewBlockAsync(_Cts.Token).ConfigureAwait(false);
await session.ListenDerivationSchemesAsync((await GetStrategies(network)).ToArray(), _Cts.Token).ConfigureAwait(false);
Logs.PayServer.LogInformation($"{network.CryptoCode}: if any pending invoice got paid while offline...");
int paymentCount = await FindPaymentViaPolling(wallet, network);
Logs.PayServer.LogInformation($"{network.CryptoCode}: {paymentCount} payments happened while offline");
Logs.PayServer.LogInformation($"Connected to WebSocket of NBXplorer ({network.CryptoCode})");
while (!_Cts.IsCancellationRequested)
{
@ -134,17 +148,35 @@ namespace BTCPayServer.HostedServices
switch (newEvent)
{
case NBXplorer.Models.NewBlockEvent evt:
await Task.WhenAll((await _InvoiceRepository.GetPendingInvoices())
.Select(invoiceId => UpdatePaymentStates(wallet, invoiceId))
.ToArray());
_Aggregator.Publish(new Events.NewBlockEvent() { CryptoCode = evt.CryptoCode });
break;
case NBXplorer.Models.NewTransactionEvent evt:
foreach (var txout in evt.Outputs)
wallet.InvalidateCache(evt.DerivationStrategy);
foreach (var output in evt.Outputs)
{
_Aggregator.Publish(new Events.TxOutReceivedEvent()
foreach (var txCoin in evt.TransactionData.Transaction.Outputs.AsCoins()
.Where(o => o.ScriptPubKey == output.ScriptPubKey)
.Select(o => output.Redeem == null ? o : o.ToScriptCoin(output.Redeem)))
{
Network = network,
ScriptPubKey = txout.ScriptPubKey,
DerivationStrategy = txout.DerivationStrategy
});
var invoice = await _InvoiceRepository.GetInvoiceFromScriptPubKey(output.ScriptPubKey, network.CryptoCode);
if (invoice != null)
{
var payment = invoice.GetPayments().FirstOrDefault(p => p.Outpoint == txCoin.Outpoint);
if (payment == null)
{
payment = await _InvoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, txCoin, network.CryptoCode);
await ReceivedPayment(wallet, invoice.Id, payment, evt.DerivationStrategy);
}
else
{
await UpdatePaymentStates(wallet, invoice.Id);
}
}
}
}
break;
default:
@ -173,6 +205,164 @@ namespace BTCPayServer.HostedServices
}
}
async Task<InvoiceEntity> UpdatePaymentStates(BTCPayWallet wallet, string invoiceId)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, false);
List<PaymentEntity> updatedPaymentEntities = new List<PaymentEntity>();
var transactions = await wallet.GetTransactions(invoice.GetPayments(wallet.Network)
.Select(t => t.Outpoint.Hash)
.ToArray());
var conflicts = GetConflicts(transactions.Select(t => t.Value));
foreach (var payment in invoice.GetPayments(wallet.Network))
{
if (!transactions.TryGetValue(payment.Outpoint.Hash, out TransactionResult tx))
continue;
var txId = tx.Transaction.GetHash();
var txConflict = conflicts.GetConflict(txId);
var accounted = txConflict == null || txConflict.IsWinner(txId);
bool updated = false;
if (accounted != payment.Accounted)
{
updated = true;
payment.Accounted = accounted;
}
var bitcoinLike = payment.GetCryptoPaymentData() as BitcoinLikePaymentData;
// Legacy
if (bitcoinLike == null)
{
#pragma warning disable CS0618 // Type or member is obsolete
payment.CryptoPaymentDataType = "BTCLike";
#pragma warning restore CS0618 // Type or member is obsolete
bitcoinLike = new BitcoinLikePaymentData();
bitcoinLike.ConfirmationCount = tx.Confirmations;
bitcoinLike.RBF = tx.Transaction.RBF;
payment.SetCryptoPaymentData(bitcoinLike);
updated = true;
}
if (bitcoinLike.ConfirmationCount != tx.Confirmations)
{
if(wallet.Network.MaxTrackedConfirmation >= bitcoinLike.ConfirmationCount)
{
bitcoinLike.ConfirmationCount = tx.Confirmations;
payment.SetCryptoPaymentData(bitcoinLike);
updated = true;
}
}
if (updated)
updatedPaymentEntities.Add(payment);
}
await _InvoiceRepository.UpdatePayments(updatedPaymentEntities);
if (updatedPaymentEntities.Count != 0)
_Aggregator.Publish(new Events.InvoiceNeedUpdateEvent(invoice.Id));
return invoice;
}
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));
}
private async Task<int> FindPaymentViaPolling(BTCPayWallet wallet, BTCPayNetwork network)
{
int totalPayment = 0;
var invoices = await _InvoiceRepository.GetPendingInvoices();
foreach (var invoiceId in invoices)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, true);
var alreadyAccounted = new HashSet<OutPoint>(invoice.GetPayments(network).Select(p => p.Outpoint));
var strategy = invoice.GetDerivationStrategy(network);
if (strategy == null)
continue;
var coins = (await wallet.GetUnspentCoins(strategy))
.Where(c => invoice.AvailableAddressHashes.Contains(c.Coin.ScriptPubKey.Hash.ToString() + network.CryptoCode))
.ToArray();
foreach (var coin in coins.Where(c => !alreadyAccounted.Contains(c.Coin.Outpoint)))
{
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.Timestamp, coin.Coin, network.CryptoCode).ConfigureAwait(false);
alreadyAccounted.Add(coin.Coin.Outpoint);
invoice = await ReceivedPayment(wallet, invoice.Id, payment, strategy);
totalPayment++;
}
}
return totalPayment;
}
private async Task<InvoiceEntity> ReceivedPayment(BTCPayWallet wallet, string invoiceId, PaymentEntity payment, DerivationStrategyBase strategy)
{
var invoice = (await UpdatePaymentStates(wallet, invoiceId));
var cryptoData = invoice.GetCryptoData(wallet.Network, _ExplorerClients.NetworkProviders);
if (cryptoData.GetDepositAddress().ScriptPubKey == payment.GetScriptPubKey() && cryptoData.Calculate().Due > Money.Zero)
{
var address = await wallet.ReserveAddressAsync(strategy);
await _InvoiceRepository.NewAddress(invoiceId, address, wallet.Network);
_Aggregator.Publish(new InvoiceNewAddressEvent(invoiceId, address.ToString(), wallet.Network));
cryptoData.DepositAddress = address.ToString();
invoice.SetCryptoData(cryptoData);
}
wallet.InvalidateCache(strategy);
_Aggregator.Publish(new InvoiceEvent(invoiceId, 1002, "invoice_receivedPayment"));
return invoice;
}
private async Task<List<DerivationStrategyBase>> GetStrategies(BTCPayNetwork network)
{
List<DerivationStrategyBase> strategies = new List<DerivationStrategyBase>();

View File

@ -193,6 +193,7 @@ namespace BTCPayServer.HostedServices
_Aggregator.Publish(new NBXplorerErrorEvent(_Network, error));
}
_Dashboard.Publish(_Network, State, status, error);
if (oldState != State)
{
if (State == NBXplorerState.Synching)
@ -205,7 +206,6 @@ namespace BTCPayServer.HostedServices
}
_Aggregator.Publish(new NBXplorerStateChangedEvent(_Network, oldState, State));
}
_Dashboard.Publish(_Network, State, status, error);
return oldState != State;
}

View File

@ -93,13 +93,13 @@ namespace BTCPayServer.Hosting
});
// Needed to debug U2F for ledger support
//services.Configure<KestrelServerOptions>(kestrel =>
//{
// kestrel.Listen(IPAddress.Loopback, 5012, l =>
// {
// l.UseHttps("devtest.pfx", "toto");
// });
//});
services.Configure<KestrelServerOptions>(kestrel =>
{
kestrel.Listen(IPAddress.Loopback, 5012, l =>
{
l.UseHttps("devtest.pfx", "toto");
});
});
}
// Big hack, tests fails if only call AddHangfire because Hangfire fail at initializing at the second test run

View File

@ -1,12 +1,9 @@
// <auto-generated />
using BTCPayServer.Data;
using BTCPayServer.Services.Invoices;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using System;
namespace BTCPayServer.Migrations

View File

@ -4,6 +4,7 @@ using System.Linq;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Services.Wallets;
using LedgerWallet;
using NBitcoin;
using NBXplorer.DerivationStrategy;
@ -115,6 +116,8 @@ namespace BTCPayServer.Services
throw new ArgumentNullException(nameof(Network));
if (strategy == null)
throw new ArgumentNullException(nameof(strategy));
if (!strategy.Segwit)
return false;
return await GetKeyPath(_Ledger, network, strategy) != null;
}
@ -145,11 +148,11 @@ namespace BTCPayServer.Services
}
public async Task<Transaction> SendToAddress(DirectDerivationStrategy strategy,
Coin[] coins, BTCPayNetwork network,
ReceivedCoin[] coins, BTCPayNetwork network,
(IDestination destination, Money amount, bool substractFees)[] send,
FeeRate feeRate,
IDestination changeAddress,
Dictionary<Script, KeyPath> keypaths = null)
KeyPath changeKeyPath)
{
if (strategy == null)
throw new ArgumentNullException(nameof(strategy));
@ -159,8 +162,6 @@ namespace BTCPayServer.Services
throw new ArgumentNullException(nameof(feeRate));
if (changeAddress == null)
throw new ArgumentNullException(nameof(changeAddress));
if (keypaths == null)
throw new ArgumentNullException(nameof(keypaths));
if (feeRate.FeePerK <= Money.Zero)
{
throw new ArgumentOutOfRangeException(nameof(feeRate), "The fee rate should be above zero");
@ -184,7 +185,7 @@ namespace BTCPayServer.Services
}
TransactionBuilder builder = new TransactionBuilder();
builder.AddCoins(coins);
builder.AddCoins(coins.Select(c=>c.Coin).ToArray());
foreach (var element in send)
{
@ -197,6 +198,12 @@ namespace BTCPayServer.Services
builder.Shuffle();
var unsigned = builder.BuildTransaction(false);
var keypaths = new Dictionary<Script, KeyPath>();
foreach(var c in coins)
{
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
}
var hasChange = unsigned.Outputs.Count == 2;
var usedCoins = builder.FindSpentCoins(unsigned);
_Transport.Timeout = TimeSpan.FromMinutes(5);
@ -208,7 +215,7 @@ namespace BTCPayServer.Services
PubKey = strategy.Root.Derive(keypaths[c.TxOut.ScriptPubKey]).PubKey
}).ToArray(),
unsigned,
hasChange ? foundKeyPath.Derive(keypaths[changeAddress.ScriptPubKey]) : null);
hasChange ? foundKeyPath.Derive(changeKeyPath) : null);
return fullySigned;
}
}

View File

@ -10,6 +10,7 @@ using NBitcoin.DataEncoders;
using BTCPayServer.Data;
using NBXplorer.Models;
using NBXplorer;
using NBXplorer.DerivationStrategy;
namespace BTCPayServer.Services.Invoices
{
@ -167,6 +168,28 @@ namespace BTCPayServer.Services.Invoices
set;
}
public DerivationStrategyBase GetDerivationStrategy(BTCPayNetwork network)
{
#pragma warning disable CS0618
JObject strategies = JObject.Parse(DerivationStrategies);
#pragma warning restore CS0618
foreach (var strat in strategies.Properties())
{
if (strat.Name == network.CryptoCode)
{
return BTCPayServer.DerivationStrategy.Parse(strat.Value.Value<string>(), network).DerivationStrategyBase;
}
}
#pragma warning disable CS0618
if (network.IsBTC && !string.IsNullOrEmpty(DerivationStrategy))
{
return BTCPayServer.DerivationStrategy.Parse(DerivationStrategy, network).DerivationStrategyBase;
}
return null;
#pragma warning restore CS0618
}
public IEnumerable<DerivationStrategy> GetDerivationStrategies(BTCPayNetworkProvider networks)
{
#pragma warning disable CS0618
@ -370,7 +393,7 @@ namespace BTCPayServer.Services.Invoices
dto.PaymentUrls = cryptoInfo.PaymentUrls;
}
#pragma warning restore CS0618
if(!info.IsPhantomBTC)
if (!info.IsPhantomBTC)
dto.CryptoInfo.Add(cryptoInfo);
}
@ -512,6 +535,15 @@ namespace BTCPayServer.Services.Invoices
[JsonProperty(PropertyName = "depositAddress")]
public string DepositAddress { get; set; }
public BitcoinAddress GetDepositAddress()
{
if(string.IsNullOrEmpty(DepositAddress))
{
return null;
}
return BitcoinAddress.Create(DepositAddress, Network.NBitcoinNetwork);
}
[JsonIgnore]
public bool IsPhantomBTC { get; set; }
@ -566,17 +598,6 @@ namespace BTCPayServer.Services.Invoices
}
public class AccountedPaymentEntity
{
public int Confirmations
{
get;
set;
}
public PaymentEntity Payment { get; set; }
public Transaction Transaction { get; set; }
}
public class PaymentEntity
{
public DateTimeOffset ReceivedTime
@ -612,6 +633,41 @@ namespace BTCPayServer.Services.Invoices
get;
set;
}
[Obsolete("Use GetCryptoPaymentData() instead")]
public string CryptoPaymentData { get; set; }
[Obsolete("Use GetCryptoPaymentData() instead")]
public string CryptoPaymentDataType { get; set; }
public CryptoPaymentData GetCryptoPaymentData()
{
#pragma warning disable CS0618
if (string.IsNullOrEmpty(CryptoPaymentDataType))
{
return NullPaymentData.Instance;
}
if (CryptoPaymentDataType == "BTCLike")
{
return JsonConvert.DeserializeObject<BitcoinLikePaymentData>(CryptoPaymentData);
}
else
return NullPaymentData.Instance;
#pragma warning restore CS0618
}
public void SetCryptoPaymentData(CryptoPaymentData cryptoPaymentData)
{
#pragma warning disable CS0618
if (cryptoPaymentData is BitcoinLikePaymentData)
{
CryptoPaymentDataType = "BTCLike";
}
else
throw new NotSupportedException(cryptoPaymentData.ToString());
CryptoPaymentData = JsonConvert.SerializeObject(cryptoPaymentData);
#pragma warning restore CS0618
}
public Money GetValue()
{
#pragma warning disable CS0618
@ -641,6 +697,62 @@ namespace BTCPayServer.Services.Invoices
return CryptoCode ?? "BTC";
#pragma warning restore CS0618
}
}
public interface CryptoPaymentData
{
bool PaymentCompleted(PaymentEntity entity, BTCPayNetwork network);
bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy, BTCPayNetwork network);
}
public class NullPaymentData : CryptoPaymentData
{
private static readonly NullPaymentData _Instance = new NullPaymentData();
public static NullPaymentData Instance
{
get
{
return _Instance;
}
}
public bool PaymentCompleted(PaymentEntity entity, BTCPayNetwork network)
{
return false;
}
public bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy, BTCPayNetwork network)
{
return false;
}
}
public class BitcoinLikePaymentData : CryptoPaymentData
{
public int ConfirmationCount { get; set; }
public bool RBF { get; set; }
public bool PaymentCompleted(PaymentEntity entity, BTCPayNetwork network)
{
return ConfirmationCount >= 6;
}
public bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy, BTCPayNetwork network)
{
if (speedPolicy == SpeedPolicy.HighSpeed)
{
return ConfirmationCount >= 1 || !RBF;
}
else if (speedPolicy == SpeedPolicy.MediumSpeed)
{
return ConfirmationCount >= 1;
}
else if (speedPolicy == SpeedPolicy.LowSpeed)
{
return ConfirmationCount >= 6;
}
return false;
}
}
}

View File

@ -58,12 +58,22 @@ namespace BTCPayServer.Services.Invoices
}
}
public async Task<string> GetInvoiceIdFromScriptPubKey(Script scriptPubKey, string cryptoCode)
public async Task<InvoiceEntity> GetInvoiceFromScriptPubKey(Script scriptPubKey, string cryptoCode)
{
using (var db = _ContextFactory.CreateContext())
{
var result = await db.AddressInvoices.FindAsync(scriptPubKey.Hash.ToString() + "#" + cryptoCode);
return result?.InvoiceDataId;
var key = scriptPubKey.Hash.ToString() + "#" + cryptoCode;
var result = await db.AddressInvoices
#pragma warning disable CS0618
.Where(a => a.Address == key)
#pragma warning restore CS0618
.Select(a => a.InvoiceData)
.Include(a => a.Payments)
.Include(a => a.RefundAddresses)
.FirstOrDefaultAsync();
if (result == null)
return null;
return ToEntity(result);
}
}
@ -121,7 +131,7 @@ namespace BTCPayServer.Services.Invoices
{
InvoiceDataId = invoice.Id,
CreatedTime = DateTimeOffset.UtcNow,
}.SetHash(BitcoinAddress.Create(cryptoData.DepositAddress, cryptoData.Network.NBitcoinNetwork).ScriptPubKey.Hash, cryptoData.CryptoCode));
}.SetHash(cryptoData.GetDepositAddress().ScriptPubKey.Hash, cryptoData.CryptoCode));
context.HistoricalAddressInvoices.Add(new HistoricalAddressInvoiceData()
{
InvoiceDataId = invoice.Id,
@ -444,14 +454,16 @@ namespace BTCPayServer.Services.Invoices
Output = receivedCoin.TxOut,
CryptoCode = cryptoCode,
#pragma warning restore CS0618
ReceivedTime = date.UtcDateTime
ReceivedTime = date.UtcDateTime,
Accounted = false
};
entity.SetCryptoPaymentData(new BitcoinLikePaymentData());
PaymentData data = new PaymentData
{
Id = receivedCoin.Outpoint.ToString(),
Blob = ToBytes(entity, null),
InvoiceDataId = invoiceId
InvoiceDataId = invoiceId,
Accounted = false
};
context.Payments.Add(data);
@ -473,8 +485,10 @@ namespace BTCPayServer.Services.Invoices
var data = new PaymentData();
data.Id = payment.Outpoint.ToString();
data.Accounted = payment.Accounted;
data.Blob = ToBytes(payment, null);
context.Attach(data);
context.Entry(data).Property(o => o.Accounted).IsModified = true;
context.Entry(data).Property(o => o.Blob).IsModified = true;
}
await context.SaveChangesAsync().ConfigureAwait(false);
}

View File

@ -20,6 +20,13 @@ namespace BTCPayServer.Services.Wallets
{
public UTXOChanges PreviousCall { get; set; }
}
public class ReceivedCoin
{
public Coin Coin { get; set; }
public DateTimeOffset Timestamp { get; set; }
public KeyPath KeyPath { get; set; }
}
public class NetworkCoins
{
public class TimestampedCoin
@ -56,7 +63,7 @@ namespace BTCPayServer.Services.Wallets
}
}
public TimeSpan CacheSpan { get; private set; } = TimeSpan.FromMinutes(30);
public TimeSpan CacheSpan { get; private set; } = TimeSpan.FromMinutes(5);
public async Task<BitcoinAddress> ReserveAddressAsync(DerivationStrategyBase derivationStrategy)
{
@ -106,18 +113,6 @@ namespace BTCPayServer.Services.Wallets
}
ConcurrentDictionary<string, TaskCompletionSource<UTXOChanges>> _FetchingUTXOs = new ConcurrentDictionary<string, TaskCompletionSource<UTXOChanges>>();
public async Task<NetworkCoins> GetCoins(DerivationStrategyBase strategy, CancellationToken cancellation = default(CancellationToken))
{
UTXOChanges changes = await GetUTXOChanges(strategy, cancellation);
return new NetworkCoins()
{
TimestampedCoins = changes.Confirmed.UTXOs.Concat(changes.Unconfirmed.UTXOs).Select(c => new NetworkCoins.TimestampedCoin() { Coin = c.AsCoin(), DateTime = c.Timestamp }).ToArray(),
Strategy = strategy,
Wallet = this
};
}
private async Task<UTXOChanges> GetUTXOChanges(DerivationStrategyBase strategy, CancellationToken cancellation)
{
var thisCompletionSource = new TaskCompletionSource<UTXOChanges>();
@ -149,7 +144,7 @@ namespace BTCPayServer.Services.Wallets
});
completionSource.TrySetResult(utxos);
}
catch(Exception ex)
catch (Exception ex)
{
completionSource.TrySetException(ex);
}
@ -168,15 +163,18 @@ namespace BTCPayServer.Services.Wallets
public async Task<(Coin[], Dictionary<Script, KeyPath>)> GetUnspentCoins(DerivationStrategyBase derivationStrategy, CancellationToken cancellation = default(CancellationToken))
public async Task<ReceivedCoin[]> GetUnspentCoins(DerivationStrategyBase derivationStrategy, CancellationToken cancellation = default(CancellationToken))
{
var changes = await GetUTXOChanges(derivationStrategy, cancellation);
var keyPaths = new Dictionary<Script, KeyPath>();
foreach (var coin in changes.GetUnspentUTXOs())
{
keyPaths.TryAdd(coin.ScriptPubKey, coin.KeyPath);
}
return (changes.GetUnspentCoins(), keyPaths);
if (derivationStrategy == null)
throw new ArgumentNullException(nameof(derivationStrategy));
return (await GetUTXOChanges(derivationStrategy, cancellation))
.GetUnspentUTXOs()
.Select(c => new ReceivedCoin()
{
Coin = c.AsCoin(derivationStrategy),
KeyPath = c.KeyPath,
Timestamp = c.Timestamp
}).ToArray();
}
public async Task<Money> GetBalance(DerivationStrategyBase derivationStrategy, CancellationToken cancellation = default(CancellationToken))

View File

@ -48,5 +48,11 @@ namespace BTCPayServer.Services.Wallets
{
return _Client.IsAvailable(network);
}
public IEnumerable<BTCPayWallet> GetWallets()
{
foreach (var w in _Wallets)
yield return w.Value;
}
}
}