mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 22:14:26 +01:00
Pending Transaction Webhook Providers foundation
This commit is contained in:
@@ -1356,7 +1356,7 @@ namespace BTCPayServer.Controllers
|
|||||||
if (vm.SigningContext.PendingTransactionId is not null)
|
if (vm.SigningContext.PendingTransactionId is not null)
|
||||||
{
|
{
|
||||||
var psbt = PSBT.Parse(vm.SigningContext.PSBT, NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode).NBitcoinNetwork);
|
var psbt = PSBT.Parse(vm.SigningContext.PSBT, NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode).NBitcoinNetwork);
|
||||||
var pendingTransaction = await _pendingTransactionService.CollectSignature(walletId.CryptoCode, psbt, false, CancellationToken.None);
|
var pendingTransaction = await _pendingTransactionService.CollectSignature(walletId.CryptoCode, psbt, CancellationToken.None);
|
||||||
|
|
||||||
if (pendingTransaction != null)
|
if (pendingTransaction != null)
|
||||||
return RedirectToAction(nameof(WalletTransactions), new { walletId = walletId.ToString() });
|
return RedirectToAction(nameof(WalletTransactions), new { walletId = walletId.ToString() });
|
||||||
|
|||||||
@@ -20,12 +20,10 @@ using WebhookDeliveryData = BTCPayServer.Data.WebhookDeliveryData;
|
|||||||
namespace BTCPayServer.HostedServices;
|
namespace BTCPayServer.HostedServices;
|
||||||
|
|
||||||
public class PendingTransactionService(
|
public class PendingTransactionService(
|
||||||
DelayedTransactionBroadcaster broadcaster,
|
|
||||||
BTCPayNetworkProvider networkProvider,
|
BTCPayNetworkProvider networkProvider,
|
||||||
ApplicationDbContextFactory dbContextFactory,
|
ApplicationDbContextFactory dbContextFactory,
|
||||||
EventAggregator eventAggregator,
|
EventAggregator eventAggregator,
|
||||||
ILogger<PendingTransactionService> logger,
|
ILogger<PendingTransactionService> logger)
|
||||||
ExplorerClientProvider explorerClientProvider)
|
|
||||||
: EventHostedServiceBase(eventAggregator, logger), IPeriodicTask
|
: EventHostedServiceBase(eventAggregator, logger), IPeriodicTask
|
||||||
{
|
{
|
||||||
protected override void SubscribeToEvents()
|
protected override void SubscribeToEvents()
|
||||||
@@ -114,11 +112,15 @@ public class PendingTransactionService(
|
|||||||
pendingTransaction.SetBlob(new PendingTransactionBlob { PSBT = psbt.ToBase64() });
|
pendingTransaction.SetBlob(new PendingTransactionBlob { PSBT = psbt.ToBase64() });
|
||||||
ctx.PendingTransactions.Add(pendingTransaction);
|
ctx.PendingTransactions.Add(pendingTransaction);
|
||||||
await ctx.SaveChangesAsync(cancellationToken);
|
await ctx.SaveChangesAsync(cancellationToken);
|
||||||
|
EventAggregator.Publish(new PendingTransactionEvent
|
||||||
|
{
|
||||||
|
Data = pendingTransaction,
|
||||||
|
Type = PendingTransactionEvent.Created
|
||||||
|
});
|
||||||
return pendingTransaction;
|
return pendingTransaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PendingTransaction?> CollectSignature(string cryptoCode, PSBT psbt, bool broadcastIfComplete,
|
public async Task<PendingTransaction?> CollectSignature(string cryptoCode, PSBT psbt, CancellationToken cancellationToken)
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
{
|
||||||
var network = networkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
var network = networkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
||||||
if (network is null)
|
if (network is null)
|
||||||
@@ -187,23 +189,11 @@ public class PendingTransactionService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
await ctx.SaveChangesAsync(cancellationToken);
|
await ctx.SaveChangesAsync(cancellationToken);
|
||||||
|
EventAggregator.Publish(new PendingTransactionEvent
|
||||||
if (broadcastIfComplete && pendingTransaction.State == PendingTransactionState.Signed)
|
|
||||||
{
|
{
|
||||||
var explorerClient = explorerClientProvider.GetExplorerClient(network);
|
Data = pendingTransaction,
|
||||||
var tx = originalPsbtWorkingCopyWithNewPsbt.ExtractTransaction();
|
Type = PendingTransactionEvent.SignatureCollected
|
||||||
var result = await explorerClient.BroadcastAsync(tx, cancellationToken);
|
});
|
||||||
if (result.Success)
|
|
||||||
{
|
|
||||||
pendingTransaction.State = PendingTransactionState.Broadcast;
|
|
||||||
await ctx.SaveChangesAsync(cancellationToken);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await broadcaster.Schedule(DateTimeOffset.Now, tx, network);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return pendingTransaction;
|
return pendingTransaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,6 +235,21 @@ public class PendingTransactionService(
|
|||||||
if (pt is null) return;
|
if (pt is null) return;
|
||||||
pt.State = PendingTransactionState.Broadcast;
|
pt.State = PendingTransactionState.Broadcast;
|
||||||
await ctx.SaveChangesAsync();
|
await ctx.SaveChangesAsync();
|
||||||
|
EventAggregator.Publish(new PendingTransactionEvent
|
||||||
|
{
|
||||||
|
Data = pt,
|
||||||
|
Type = PendingTransactionEvent.Broadcast
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public record PendingTransactionEvent
|
||||||
|
{
|
||||||
|
public const string Created = nameof(Created);
|
||||||
|
public const string SignatureCollected = nameof(SignatureCollected);
|
||||||
|
public const string Broadcast = nameof(Broadcast);
|
||||||
|
|
||||||
|
public PendingTransaction Data { get; set; } = null!;
|
||||||
|
public string Type { get; set; } = null!;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Client.Models;
|
||||||
|
using BTCPayServer.Controllers;
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.Services.Invoices;
|
||||||
|
using WebhookDeliveryData = BTCPayServer.Data.WebhookDeliveryData;
|
||||||
|
|
||||||
|
namespace BTCPayServer.HostedServices.Webhooks;
|
||||||
|
|
||||||
|
public class PendingTransactionDeliveryRequest(
|
||||||
|
PendingTransaction pendingTransaction,
|
||||||
|
string webhookId,
|
||||||
|
WebhookEvent webhookEvent,
|
||||||
|
WebhookDeliveryData delivery,
|
||||||
|
WebhookBlob webhookBlob)
|
||||||
|
: WebhookSender.WebhookDeliveryRequest(webhookId, webhookEvent, delivery, webhookBlob)
|
||||||
|
{
|
||||||
|
public override Task<SendEmailRequest> Interpolate(SendEmailRequest req,
|
||||||
|
UIStoresController.StoreEmailRule storeEmailRule)
|
||||||
|
{
|
||||||
|
// if (storeEmailRule.CustomerEmail &&
|
||||||
|
// MailboxAddressValidator.TryParse(Invoice.Metadata.BuyerEmail, out var bmb))
|
||||||
|
// {
|
||||||
|
// req.Email ??= string.Empty;
|
||||||
|
// req.Email += $",{bmb}";
|
||||||
|
// }
|
||||||
|
|
||||||
|
req.Subject = Interpolate(req.Subject);
|
||||||
|
req.Body = Interpolate(req.Body);
|
||||||
|
return Task.FromResult(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string Interpolate(string str)
|
||||||
|
{
|
||||||
|
var res = str.Replace("{PendingTransaction.Id}", pendingTransaction.TransactionId);
|
||||||
|
// .Replace("{Invoice.StoreId}", Invoice.StoreId)
|
||||||
|
// .Replace("{Invoice.Price}", Invoice.Price.ToString(CultureInfo.InvariantCulture))
|
||||||
|
// .Replace("{Invoice.Currency}", Invoice.Currency)
|
||||||
|
// .Replace("{Invoice.Status}", Invoice.Status.ToString())
|
||||||
|
// .Replace("{Invoice.AdditionalStatus}", Invoice.ExceptionStatus.ToString())
|
||||||
|
// .Replace("{Invoice.OrderId}", Invoice.Metadata.OrderId);
|
||||||
|
|
||||||
|
// res = InterpolateJsonField(res, "Invoice.Metadata", Invoice.Metadata.ToJObject());
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using BTCPayServer.Client.Models;
|
||||||
|
using BTCPayServer.Controllers.Greenfield;
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.Events;
|
||||||
|
using BTCPayServer.Services.Invoices;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Converters;
|
||||||
|
using WebhookDeliveryData = BTCPayServer.Data.WebhookDeliveryData;
|
||||||
|
|
||||||
|
namespace BTCPayServer.HostedServices.Webhooks;
|
||||||
|
|
||||||
|
public class PendingTransactionWebhookProvider : WebhookProvider<PendingTransactionService.PendingTransactionEvent>
|
||||||
|
{
|
||||||
|
public PendingTransactionWebhookProvider(WebhookSender webhookSender, EventAggregator eventAggregator,
|
||||||
|
ILogger<InvoiceWebhookProvider> logger) : base(
|
||||||
|
eventAggregator, logger, webhookSender)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public const string PendingTransactionCreated = nameof(PendingTransactionCreated);
|
||||||
|
public const string PendingTransactionSignatureCollected = nameof(PendingTransactionSignatureCollected);
|
||||||
|
public const string PendingTransactionBroadcast = nameof(PendingTransactionBroadcast);
|
||||||
|
|
||||||
|
public override Dictionary<string, string> GetSupportedWebhookTypes()
|
||||||
|
{
|
||||||
|
return new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{PendingTransactionCreated, "Pending Transaction - Created"},
|
||||||
|
{PendingTransactionSignatureCollected, "Pending Transaction - Signature Collected"},
|
||||||
|
{PendingTransactionBroadcast, "Pending Transaction - Broadcast"}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override WebhookSender.WebhookDeliveryRequest CreateDeliveryRequest(PendingTransactionService.PendingTransactionEvent evt,
|
||||||
|
WebhookData webhook)
|
||||||
|
{
|
||||||
|
var webhookBlob = webhook?.GetBlob();
|
||||||
|
|
||||||
|
var webhookEvent = GetWebhookEvent(evt)!;
|
||||||
|
webhookEvent.StoreId = evt.Data.StoreId;
|
||||||
|
webhookEvent.WebhookId = webhook?.Id;
|
||||||
|
webhookEvent.IsRedelivery = false;
|
||||||
|
WebhookDeliveryData delivery = webhook is null? null: WebhookExtensions.NewWebhookDelivery(webhook.Id);
|
||||||
|
if (delivery is not null)
|
||||||
|
{
|
||||||
|
webhookEvent.DeliveryId = delivery.Id;
|
||||||
|
webhookEvent.OriginalDeliveryId = delivery.Id;
|
||||||
|
webhookEvent.Timestamp = delivery.Timestamp;
|
||||||
|
}
|
||||||
|
return new PendingTransactionDeliveryRequest(evt.Data, webhook?.Id, webhookEvent, delivery, webhookBlob);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override WebhookPendingTransactionEvent GetWebhookEvent(PendingTransactionService.PendingTransactionEvent evt)
|
||||||
|
{
|
||||||
|
return evt.Type switch
|
||||||
|
{
|
||||||
|
PendingTransactionService.PendingTransactionEvent.Created => new WebhookPendingTransactionEvent(
|
||||||
|
PendingTransactionCreated, evt.Data.StoreId),
|
||||||
|
PendingTransactionService.PendingTransactionEvent.SignatureCollected => new WebhookPendingTransactionEvent(
|
||||||
|
PendingTransactionSignatureCollected, evt.Data.StoreId),
|
||||||
|
PendingTransactionService.PendingTransactionEvent.Broadcast => new WebhookPendingTransactionEvent(
|
||||||
|
PendingTransactionBroadcast, evt.Data.StoreId),
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override WebhookEvent CreateTestEvent(string type, params object[] args)
|
||||||
|
{
|
||||||
|
var storeId = args[0].ToString();
|
||||||
|
return new WebhookInvoiceEvent(type, storeId)
|
||||||
|
{
|
||||||
|
InvoiceId = "__test__" + Guid.NewGuid() + "__test__"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class WebhookPendingTransactionEvent : StoreWebhookEvent
|
||||||
|
{
|
||||||
|
public WebhookPendingTransactionEvent(string type, string storeId)
|
||||||
|
{
|
||||||
|
if (!type.StartsWith(PendingTransactionCreated.Replace("Created", "").ToLower(), StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
throw new ArgumentException("Invalid event type", nameof(type));
|
||||||
|
Type = type;
|
||||||
|
StoreId = storeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonProperty(Order = 2)] public string PendingTransactionId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -354,6 +354,7 @@ namespace BTCPayServer.Hosting
|
|||||||
services.TryAddSingleton<PaymentRequestRepository>();
|
services.TryAddSingleton<PaymentRequestRepository>();
|
||||||
services.TryAddSingleton<BTCPayWalletProvider>();
|
services.TryAddSingleton<BTCPayWalletProvider>();
|
||||||
services.AddSingleton<PendingTransactionService>();
|
services.AddSingleton<PendingTransactionService>();
|
||||||
|
services.AddSingleton<IWebhookProvider>(o => o.GetRequiredService<PendingTransactionWebhookProvider>());
|
||||||
services.AddScheduledTask<PendingTransactionService>(TimeSpan.FromMinutes(10));
|
services.AddScheduledTask<PendingTransactionService>(TimeSpan.FromMinutes(10));
|
||||||
services.TryAddSingleton<WalletReceiveService>();
|
services.TryAddSingleton<WalletReceiveService>();
|
||||||
services.AddSingleton<IHostedService>(provider => provider.GetService<WalletReceiveService>());
|
services.AddSingleton<IHostedService>(provider => provider.GetService<WalletReceiveService>());
|
||||||
|
|||||||
Reference in New Issue
Block a user