invoice watcher can watch several currencies

This commit is contained in:
nicolas.dorier
2018-01-07 02:16:42 +09:00
parent 781b2885cc
commit 63fceed5f4
4 changed files with 122 additions and 56 deletions

View File

@@ -71,13 +71,18 @@ namespace BTCPayServer
} }
public void Publish<T>(T evt) where T : class public void Publish<T>(T evt) where T : class
{
Publish(evt, typeof(T));
}
public void Publish(object evt, Type evtType)
{ {
if (evt == null) if (evt == null)
throw new ArgumentNullException(nameof(evt)); throw new ArgumentNullException(nameof(evt));
List<Action<object>> actionList = new List<Action<object>>(); List<Action<object>> actionList = new List<Action<object>>();
lock (_Subscriptions) lock (_Subscriptions)
{ {
if (_Subscriptions.TryGetValue(typeof(T), out Dictionary<Subscription, Action<object>> actions)) if (_Subscriptions.TryGetValue(evtType, out Dictionary<Subscription, Action<object>> actions))
{ {
actionList = actions.Values.ToList(); actionList = actions.Values.ToList();
} }

View File

@@ -17,6 +17,7 @@ using NBXplorer;
using NBXplorer.Models; using NBXplorer.Models;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using BTCPayServer.Services.Wallets;
namespace BTCPayServer namespace BTCPayServer
{ {
@@ -27,7 +28,7 @@ namespace BTCPayServer
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite"; return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
} }
public static async Task<Dictionary<uint256, TransactionResult>> GetTransactions(this ExplorerClient client, uint256[] hashes, CancellationToken cts = default(CancellationToken)) public static async Task<Dictionary<uint256, TransactionResult>> GetTransactions(this BTCPayWallet client, uint256[] hashes, CancellationToken cts = default(CancellationToken))
{ {
hashes = hashes.Distinct().ToArray(); hashes = hashes.Distinct().ToArray();
var transactions = hashes var transactions = hashes

View File

@@ -15,6 +15,7 @@ using Hangfire;
using BTCPayServer.Services.Wallets; using BTCPayServer.Services.Wallets;
using BTCPayServer.Controllers; using BTCPayServer.Controllers;
using BTCPayServer.Events; using BTCPayServer.Events;
using Microsoft.AspNetCore.Hosting;
namespace BTCPayServer.Services.Invoices namespace BTCPayServer.Services.Invoices
{ {
@@ -24,23 +25,42 @@ namespace BTCPayServer.Services.Invoices
} }
public class InvoiceWatcher : IHostedService public class InvoiceWatcher : IHostedService
{ {
class UpdateInvoiceContext
{
public UpdateInvoiceContext()
{
}
public Dictionary<BTCPayNetwork, KnownState> KnownStates { get; set; }
public Dictionary<BTCPayNetwork, KnownState> ModifiedKnownStates { get; set; } = new Dictionary<BTCPayNetwork, KnownState>();
public InvoiceEntity Invoice { get; set; }
public List<object> Events { get; set; } = new List<object>();
bool _Dirty = false;
public void MarkDirty()
{
_Dirty = true;
}
public bool Dirty => _Dirty;
}
InvoiceRepository _InvoiceRepository; InvoiceRepository _InvoiceRepository;
ExplorerClient _ExplorerClient;
EventAggregator _EventAggregator; EventAggregator _EventAggregator;
BTCPayWallet _Wallet; BTCPayWallet _Wallet;
BTCPayNetworkProvider _NetworkProvider; BTCPayNetworkProvider _NetworkProvider;
public InvoiceWatcher(ExplorerClient explorerClient, public InvoiceWatcher(
IHostingEnvironment env,
BTCPayNetworkProvider networkProvider, BTCPayNetworkProvider networkProvider,
InvoiceRepository invoiceRepository, InvoiceRepository invoiceRepository,
EventAggregator eventAggregator, EventAggregator eventAggregator,
BTCPayWallet wallet, BTCPayWallet wallet,
InvoiceWatcherAccessor accessor) InvoiceWatcherAccessor accessor)
{ {
LongPollingMode = explorerClient.Network == Network.RegTest; PollInterval = TimeSpan.FromMinutes(1.0);
PollInterval = explorerClient.Network == Network.RegTest ? TimeSpan.FromSeconds(10.0) : TimeSpan.FromMinutes(1.0);
_Wallet = wallet ?? throw new ArgumentNullException(nameof(wallet)); _Wallet = wallet ?? throw new ArgumentNullException(nameof(wallet));
_ExplorerClient = explorerClient ?? throw new ArgumentNullException(nameof(explorerClient));
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository)); _InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
_EventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator)); _EventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_NetworkProvider = networkProvider; _NetworkProvider = networkProvider;
@@ -48,11 +68,6 @@ namespace BTCPayServer.Services.Invoices
} }
CompositeDisposable leases = new CompositeDisposable(); CompositeDisposable leases = new CompositeDisposable();
public bool LongPollingMode
{
get; set;
}
async Task NotifyReceived(Script scriptPubKey) async Task NotifyReceived(Script scriptPubKey)
{ {
var invoice = await _InvoiceRepository.GetInvoiceIdFromScriptPubKey(scriptPubKey); var invoice = await _InvoiceRepository.GetInvoiceIdFromScriptPubKey(scriptPubKey);
@@ -70,7 +85,7 @@ namespace BTCPayServer.Services.Invoices
private async Task UpdateInvoice(string invoiceId) private async Task UpdateInvoice(string invoiceId)
{ {
UTXOChanges changes = null; Dictionary<BTCPayNetwork, KnownState> changes = new Dictionary<BTCPayNetwork, KnownState>();
while (true) while (true)
{ {
try try
@@ -79,22 +94,30 @@ namespace BTCPayServer.Services.Invoices
if (invoice == null) if (invoice == null)
break; break;
var stateBefore = invoice.Status; var stateBefore = invoice.Status;
var postSaveActions = new List<Action>(); var updateContext = new UpdateInvoiceContext()
var result = await UpdateInvoice(changes, invoice, postSaveActions).ConfigureAwait(false); {
changes = result.Changes; Invoice = invoice,
if (result.NeedSave) KnownStates = changes
{ };
await UpdateInvoice(updateContext).ConfigureAwait(false);
if (updateContext.Dirty)
{
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.Status, invoice.ExceptionStatus).ConfigureAwait(false); await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.Status, invoice.ExceptionStatus).ConfigureAwait(false);
_EventAggregator.Publish(new InvoiceDataChangedEvent() { InvoiceId = invoice.Id }); _EventAggregator.Publish(new InvoiceDataChangedEvent() { InvoiceId = invoice.Id });
} }
var changed = stateBefore != invoice.Status; var changed = stateBefore != invoice.Status;
foreach(var saveAction in postSaveActions) foreach (var evt in updateContext.Events)
{ {
saveAction(); _EventAggregator.Publish(evt, evt.GetType());
} }
foreach(var modifiedKnownState in updateContext.ModifiedKnownStates)
{
changes.AddOrReplace(modifiedKnownState.Key, modifiedKnownState.Value);
}
if (invoice.Status == "complete" || if (invoice.Status == "complete" ||
((invoice.Status == "invalid" || invoice.Status == "expired") && invoice.MonitoringExpiration < DateTimeOffset.UtcNow)) ((invoice.Status == "invalid" || invoice.Status == "expired") && invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
{ {
@@ -119,28 +142,34 @@ namespace BTCPayServer.Services.Invoices
} }
private async Task<(bool NeedSave, UTXOChanges Changes)> UpdateInvoice(UTXOChanges changes, InvoiceEntity invoice, List<Action> postSaveActions) private async Task UpdateInvoice(UpdateInvoiceContext context)
{ {
bool needSave = false; var invoice = context.Invoice;
//Fetch unknown payments //Fetch unknown payments
var strategy = invoice.GetDerivationStrategies(_NetworkProvider).First(s => s.Network.IsBTC); var strategies = invoice.GetDerivationStrategies(_NetworkProvider).ToArray();
changes = await _ExplorerClient.SyncAsync(strategy.DerivationStrategyBase, changes, !LongPollingMode, _Cts.Token).ConfigureAwait(false); var getCoinsResponsesAsync = strategies
.Select(d => _Wallet.GetCoins(d, context.KnownStates.TryGet(d.Network), _Cts.Token))
.ToArray();
var utxos = changes.Confirmed.UTXOs.Concat(changes.Unconfirmed.UTXOs).ToArray(); await Task.WhenAll(getCoinsResponsesAsync);
List<Coin> receivedCoins = new List<Coin>(); var getCoinsResponses = getCoinsResponsesAsync.Select(g => g.Result).ToArray();
foreach (var received in utxos) foreach (var response in getCoinsResponses)
if (invoice.AvailableAddressHashes.Contains(received.ScriptPubKey.Hash.ToString()))
receivedCoins.Add(received.AsCoin());
var alreadyAccounted = new HashSet<OutPoint>(invoice.Payments.Select(p => p.Outpoint));
bool dirtyAddress = false;
foreach (var coin in receivedCoins.Where(c => !alreadyAccounted.Contains(c.Outpoint)))
{ {
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin).ConfigureAwait(false); response.Coins = response.Coins.Where(c => invoice.AvailableAddressHashes.Contains(c.ScriptPubKey.Hash.ToString())).ToArray();
invoice.Payments.Add(payment); }
postSaveActions.Add(() => _EventAggregator.Publish(new InvoicePaymentEvent(invoice.Id))); var coins = getCoinsResponses.Where(s => s.Coins.Length != 0).FirstOrDefault();
dirtyAddress = true;
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).ConfigureAwait(false);
invoice.Payments.Add(payment);
context.Events.Add(new InvoicePaymentEvent(invoice.Id));
dirtyAddress = true;
}
} }
////// //////
var network = _NetworkProvider.GetNetwork("BTC"); var network = _NetworkProvider.GetNetwork("BTC");
@@ -149,10 +178,10 @@ namespace BTCPayServer.Services.Invoices
var accounting = cryptoData.Calculate(); var accounting = cryptoData.Calculate();
if (invoice.Status == "new" && invoice.ExpirationTime < DateTimeOffset.UtcNow) if (invoice.Status == "new" && invoice.ExpirationTime < DateTimeOffset.UtcNow)
{ {
needSave = true; context.MarkDirty();
await _InvoiceRepository.UnaffectAddress(invoice.Id); await _InvoiceRepository.UnaffectAddress(invoice.Id);
postSaveActions.Add(() => _EventAggregator.Publish(new InvoiceStatusChangedEvent(invoice, "expired"))); context.Events.Add(new InvoiceStatusChangedEvent(invoice, "expired"));
invoice.Status = "expired"; invoice.Status = "expired";
} }
@@ -163,16 +192,16 @@ namespace BTCPayServer.Services.Invoices
{ {
if (invoice.Status == "new") if (invoice.Status == "new")
{ {
postSaveActions.Add(() => _EventAggregator.Publish(new InvoiceStatusChangedEvent(invoice, "paid"))); context.Events.Add(new InvoiceStatusChangedEvent(invoice, "paid"));
invoice.Status = "paid"; invoice.Status = "paid";
invoice.ExceptionStatus = null; invoice.ExceptionStatus = null;
await _InvoiceRepository.UnaffectAddress(invoice.Id); await _InvoiceRepository.UnaffectAddress(invoice.Id);
needSave = true; context.MarkDirty();
} }
else if (invoice.Status == "expired") else if (invoice.Status == "expired")
{ {
invoice.ExceptionStatus = "paidLate"; invoice.ExceptionStatus = "paidLate";
needSave = true; context.MarkDirty();
} }
} }
@@ -180,17 +209,17 @@ namespace BTCPayServer.Services.Invoices
{ {
invoice.ExceptionStatus = "paidOver"; invoice.ExceptionStatus = "paidOver";
await _InvoiceRepository.UnaffectAddress(invoice.Id); await _InvoiceRepository.UnaffectAddress(invoice.Id);
needSave = true; context.MarkDirty();
} }
if (totalPaid < accounting.TotalDue && invoice.Payments.Count != 0 && invoice.ExceptionStatus != "paidPartial") if (totalPaid < accounting.TotalDue && invoice.Payments.Count != 0 && invoice.ExceptionStatus != "paidPartial")
{ {
Logs.PayServer.LogInformation("Paid to " + cryptoData.DepositAddress); Logs.PayServer.LogInformation("Paid to " + cryptoData.DepositAddress);
invoice.ExceptionStatus = "paidPartial"; invoice.ExceptionStatus = "paidPartial";
needSave = true; context.MarkDirty();
if (dirtyAddress) if (dirtyAddress)
{ {
var address = await _Wallet.ReserveAddressAsync(strategy); var address = await _Wallet.ReserveAddressAsync(coins.Strategy);
Logs.PayServer.LogInformation("Generate new " + address); Logs.PayServer.LogInformation("Generate new " + address);
await _InvoiceRepository.NewAddress(invoice.Id, address, network); await _InvoiceRepository.NewAddress(invoice.Id, address, network);
} }
@@ -223,9 +252,9 @@ namespace BTCPayServer.Services.Invoices
(chainTotalConfirmed < accounting.TotalDue)) (chainTotalConfirmed < accounting.TotalDue))
{ {
await _InvoiceRepository.UnaffectAddress(invoice.Id); await _InvoiceRepository.UnaffectAddress(invoice.Id);
postSaveActions.Add(() => _EventAggregator.Publish(new InvoiceStatusChangedEvent(invoice, "invalid"))); context.Events.Add(new InvoiceStatusChangedEvent(invoice, "invalid"));
invoice.Status = "invalid"; invoice.Status = "invalid";
needSave = true; context.MarkDirty();
} }
else else
{ {
@@ -233,9 +262,9 @@ namespace BTCPayServer.Services.Invoices
if (totalConfirmed >= accounting.TotalDue) if (totalConfirmed >= accounting.TotalDue)
{ {
await _InvoiceRepository.UnaffectAddress(invoice.Id); await _InvoiceRepository.UnaffectAddress(invoice.Id);
postSaveActions.Add(() => _EventAggregator.Publish(new InvoiceStatusChangedEvent(invoice, "confirmed"))); context.Events.Add(new InvoiceStatusChangedEvent(invoice, "confirmed"));
invoice.Status = "confirmed"; invoice.Status = "confirmed";
needSave = true; context.MarkDirty();
} }
} }
} }
@@ -247,17 +276,16 @@ namespace BTCPayServer.Services.Invoices
var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum(); var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
if (totalConfirmed >= accounting.TotalDue) if (totalConfirmed >= accounting.TotalDue)
{ {
postSaveActions.Add(() => _EventAggregator.Publish(new InvoiceStatusChangedEvent(invoice, "complete"))); context.Events.Add(new InvoiceStatusChangedEvent(invoice, "complete"));
invoice.Status = "complete"; invoice.Status = "complete";
needSave = true; context.MarkDirty();
} }
} }
return (needSave, changes);
} }
private async Task<IEnumerable<AccountedPaymentEntity>> GetPaymentsWithTransaction(InvoiceEntity invoice) private async Task<IEnumerable<AccountedPaymentEntity>> GetPaymentsWithTransaction(InvoiceEntity invoice)
{ {
var transactions = await _ExplorerClient.GetTransactions(invoice.Payments.Select(t => t.Outpoint.Hash).ToArray()); var transactions = await _Wallet.GetTransactions(invoice.Payments.Select(t => t.Outpoint.Hash).ToArray());
var spentTxIn = new Dictionary<OutPoint, AccountedPaymentEntity>(); var spentTxIn = new Dictionary<OutPoint, AccountedPaymentEntity>();
var result = invoice.Payments.Select(p => p.Outpoint).ToHashSet(); var result = invoice.Payments.Select(p => p.Outpoint).ToHashSet();

View File

@@ -7,9 +7,22 @@ using System.Text;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Data; using BTCPayServer.Data;
using System.Threading;
using NBXplorer.Models;
namespace BTCPayServer.Services.Wallets namespace BTCPayServer.Services.Wallets
{ {
public class KnownState
{
public uint256 UnconfirmedHash { get; set; }
public uint256 ConfirmedHash { get; set; }
}
public class GetCoinsResult
{
public Coin[] Coins { get; set; }
public KnownState State { get; set; }
public DerivationStrategy Strategy { get; set; }
}
public class BTCPayWallet public class BTCPayWallet
{ {
private ExplorerClient _Client; private ExplorerClient _Client;
@@ -25,6 +38,7 @@ namespace BTCPayServer.Services.Wallets
_Client = client; _Client = client;
_DBFactory = factory; _DBFactory = factory;
_Serializer = new NBXplorer.Serializer(_Client.Network); _Serializer = new NBXplorer.Serializer(_Client.Network);
LongPollingMode = client.Network == Network.RegTest;
} }
@@ -39,6 +53,24 @@ namespace BTCPayServer.Services.Wallets
await _Client.TrackAsync(derivationStrategy.DerivationStrategyBase); await _Client.TrackAsync(derivationStrategy.DerivationStrategyBase);
} }
public Task<TransactionResult> GetTransactionAsync(uint256 txId, CancellationToken cancellation = default(CancellationToken))
{
return _Client.GetTransactionAsync(txId, cancellation);
}
public bool LongPollingMode { get; set; }
public async Task<GetCoinsResult> GetCoins(DerivationStrategy strategy, KnownState state, CancellationToken cancellation = default(CancellationToken))
{
var changes = await _Client.SyncAsync(strategy.DerivationStrategyBase, state?.ConfirmedHash, state?.UnconfirmedHash, !LongPollingMode, cancellation).ConfigureAwait(false);
var utxos = changes.Confirmed.UTXOs.Concat(changes.Unconfirmed.UTXOs).Select(c => c.AsCoin()).ToArray();
return new GetCoinsResult()
{
Coins = utxos,
State = new KnownState() { ConfirmedHash = changes.Confirmed.Hash, UnconfirmedHash = changes.Unconfirmed.Hash },
Strategy = strategy,
};
}
private byte[] ToBytes<T>(T obj) private byte[] ToBytes<T>(T obj)
{ {
return ZipUtils.Zip(_Serializer.ToString(obj)); return ZipUtils.Zip(_Serializer.ToString(obj));