mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 22:44:29 +01:00
invoice watcher can watch several currencies
This commit is contained in:
@@ -71,13 +71,18 @@ namespace BTCPayServer
|
||||
}
|
||||
|
||||
public void Publish<T>(T evt) where T : class
|
||||
{
|
||||
Publish(evt, typeof(T));
|
||||
}
|
||||
|
||||
public void Publish(object evt, Type evtType)
|
||||
{
|
||||
if (evt == null)
|
||||
throw new ArgumentNullException(nameof(evt));
|
||||
List<Action<object>> actionList = new List<Action<object>>();
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ using NBXplorer;
|
||||
using NBXplorer.Models;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
@@ -27,7 +28,7 @@ namespace BTCPayServer
|
||||
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();
|
||||
var transactions = hashes
|
||||
|
||||
@@ -15,6 +15,7 @@ using Hangfire;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Events;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
|
||||
namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
@@ -24,23 +25,42 @@ namespace BTCPayServer.Services.Invoices
|
||||
}
|
||||
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;
|
||||
ExplorerClient _ExplorerClient;
|
||||
EventAggregator _EventAggregator;
|
||||
BTCPayWallet _Wallet;
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
|
||||
public InvoiceWatcher(ExplorerClient explorerClient,
|
||||
public InvoiceWatcher(
|
||||
IHostingEnvironment env,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
InvoiceRepository invoiceRepository,
|
||||
EventAggregator eventAggregator,
|
||||
BTCPayWallet wallet,
|
||||
InvoiceWatcherAccessor accessor)
|
||||
{
|
||||
LongPollingMode = explorerClient.Network == Network.RegTest;
|
||||
PollInterval = explorerClient.Network == Network.RegTest ? TimeSpan.FromSeconds(10.0) : TimeSpan.FromMinutes(1.0);
|
||||
PollInterval = TimeSpan.FromMinutes(1.0);
|
||||
_Wallet = wallet ?? throw new ArgumentNullException(nameof(wallet));
|
||||
_ExplorerClient = explorerClient ?? throw new ArgumentNullException(nameof(explorerClient));
|
||||
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
|
||||
_EventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
|
||||
_NetworkProvider = networkProvider;
|
||||
@@ -48,11 +68,6 @@ namespace BTCPayServer.Services.Invoices
|
||||
}
|
||||
CompositeDisposable leases = new CompositeDisposable();
|
||||
|
||||
public bool LongPollingMode
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
async Task NotifyReceived(Script scriptPubKey)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoiceIdFromScriptPubKey(scriptPubKey);
|
||||
@@ -70,7 +85,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
|
||||
private async Task UpdateInvoice(string invoiceId)
|
||||
{
|
||||
UTXOChanges changes = null;
|
||||
Dictionary<BTCPayNetwork, KnownState> changes = new Dictionary<BTCPayNetwork, KnownState>();
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
@@ -79,22 +94,30 @@ namespace BTCPayServer.Services.Invoices
|
||||
if (invoice == null)
|
||||
break;
|
||||
var stateBefore = invoice.Status;
|
||||
var postSaveActions = new List<Action>();
|
||||
var result = await UpdateInvoice(changes, invoice, postSaveActions).ConfigureAwait(false);
|
||||
changes = result.Changes;
|
||||
if (result.NeedSave)
|
||||
{
|
||||
var updateContext = new UpdateInvoiceContext()
|
||||
{
|
||||
Invoice = invoice,
|
||||
KnownStates = changes
|
||||
};
|
||||
await UpdateInvoice(updateContext).ConfigureAwait(false);
|
||||
if (updateContext.Dirty)
|
||||
{
|
||||
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.Status, invoice.ExceptionStatus).ConfigureAwait(false);
|
||||
_EventAggregator.Publish(new InvoiceDataChangedEvent() { InvoiceId = invoice.Id });
|
||||
}
|
||||
|
||||
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" ||
|
||||
((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
|
||||
var strategy = invoice.GetDerivationStrategies(_NetworkProvider).First(s => s.Network.IsBTC);
|
||||
changes = await _ExplorerClient.SyncAsync(strategy.DerivationStrategyBase, changes, !LongPollingMode, _Cts.Token).ConfigureAwait(false);
|
||||
|
||||
|
||||
var utxos = changes.Confirmed.UTXOs.Concat(changes.Unconfirmed.UTXOs).ToArray();
|
||||
List<Coin> receivedCoins = new List<Coin>();
|
||||
foreach (var received in utxos)
|
||||
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 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)
|
||||
{
|
||||
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin).ConfigureAwait(false);
|
||||
invoice.Payments.Add(payment);
|
||||
postSaveActions.Add(() => _EventAggregator.Publish(new InvoicePaymentEvent(invoice.Id)));
|
||||
dirtyAddress = true;
|
||||
response.Coins = response.Coins.Where(c => invoice.AvailableAddressHashes.Contains(c.ScriptPubKey.Hash.ToString())).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).ConfigureAwait(false);
|
||||
invoice.Payments.Add(payment);
|
||||
context.Events.Add(new InvoicePaymentEvent(invoice.Id));
|
||||
dirtyAddress = true;
|
||||
}
|
||||
}
|
||||
//////
|
||||
var network = _NetworkProvider.GetNetwork("BTC");
|
||||
@@ -149,10 +178,10 @@ namespace BTCPayServer.Services.Invoices
|
||||
var accounting = cryptoData.Calculate();
|
||||
if (invoice.Status == "new" && invoice.ExpirationTime < DateTimeOffset.UtcNow)
|
||||
{
|
||||
needSave = true;
|
||||
context.MarkDirty();
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
|
||||
postSaveActions.Add(() => _EventAggregator.Publish(new InvoiceStatusChangedEvent(invoice, "expired")));
|
||||
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "expired"));
|
||||
invoice.Status = "expired";
|
||||
}
|
||||
|
||||
@@ -163,16 +192,16 @@ namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
if (invoice.Status == "new")
|
||||
{
|
||||
postSaveActions.Add(() => _EventAggregator.Publish(new InvoiceStatusChangedEvent(invoice, "paid")));
|
||||
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "paid"));
|
||||
invoice.Status = "paid";
|
||||
invoice.ExceptionStatus = null;
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
needSave = true;
|
||||
context.MarkDirty();
|
||||
}
|
||||
else if (invoice.Status == "expired")
|
||||
{
|
||||
invoice.ExceptionStatus = "paidLate";
|
||||
needSave = true;
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,17 +209,17 @@ namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
invoice.ExceptionStatus = "paidOver";
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
needSave = true;
|
||||
context.MarkDirty();
|
||||
}
|
||||
|
||||
if (totalPaid < accounting.TotalDue && invoice.Payments.Count != 0 && invoice.ExceptionStatus != "paidPartial")
|
||||
{
|
||||
Logs.PayServer.LogInformation("Paid to " + cryptoData.DepositAddress);
|
||||
invoice.ExceptionStatus = "paidPartial";
|
||||
needSave = true;
|
||||
context.MarkDirty();
|
||||
if (dirtyAddress)
|
||||
{
|
||||
var address = await _Wallet.ReserveAddressAsync(strategy);
|
||||
var address = await _Wallet.ReserveAddressAsync(coins.Strategy);
|
||||
Logs.PayServer.LogInformation("Generate new " + address);
|
||||
await _InvoiceRepository.NewAddress(invoice.Id, address, network);
|
||||
}
|
||||
@@ -223,9 +252,9 @@ namespace BTCPayServer.Services.Invoices
|
||||
(chainTotalConfirmed < accounting.TotalDue))
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
postSaveActions.Add(() => _EventAggregator.Publish(new InvoiceStatusChangedEvent(invoice, "invalid")));
|
||||
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "invalid"));
|
||||
invoice.Status = "invalid";
|
||||
needSave = true;
|
||||
context.MarkDirty();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -233,9 +262,9 @@ namespace BTCPayServer.Services.Invoices
|
||||
if (totalConfirmed >= accounting.TotalDue)
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
postSaveActions.Add(() => _EventAggregator.Publish(new InvoiceStatusChangedEvent(invoice, "confirmed")));
|
||||
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "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();
|
||||
if (totalConfirmed >= accounting.TotalDue)
|
||||
{
|
||||
postSaveActions.Add(() => _EventAggregator.Publish(new InvoiceStatusChangedEvent(invoice, "complete")));
|
||||
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "complete"));
|
||||
invoice.Status = "complete";
|
||||
needSave = true;
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
return (needSave, changes);
|
||||
}
|
||||
|
||||
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 result = invoice.Payments.Select(p => p.Outpoint).ToHashSet();
|
||||
|
||||
@@ -7,9 +7,22 @@ using System.Text;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using System.Threading;
|
||||
using NBXplorer.Models;
|
||||
|
||||
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
|
||||
{
|
||||
private ExplorerClient _Client;
|
||||
@@ -25,6 +38,7 @@ namespace BTCPayServer.Services.Wallets
|
||||
_Client = client;
|
||||
_DBFactory = factory;
|
||||
_Serializer = new NBXplorer.Serializer(_Client.Network);
|
||||
LongPollingMode = client.Network == Network.RegTest;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +53,24 @@ namespace BTCPayServer.Services.Wallets
|
||||
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)
|
||||
{
|
||||
return ZipUtils.Zip(_Serializer.ToString(obj));
|
||||
|
||||
Reference in New Issue
Block a user