diff --git a/Plugins/BTCPayServer.Plugins.BitcoinSwitch/BTCPayServer.Plugins.BitcoinSwitch.csproj b/Plugins/BTCPayServer.Plugins.BitcoinSwitch/BTCPayServer.Plugins.BitcoinSwitch.csproj
index 37eedf81..233371c3 100644
--- a/Plugins/BTCPayServer.Plugins.BitcoinSwitch/BTCPayServer.Plugins.BitcoinSwitch.csproj
+++ b/Plugins/BTCPayServer.Plugins.BitcoinSwitch/BTCPayServer.Plugins.BitcoinSwitch.csproj
@@ -9,7 +9,7 @@
Bitcoin Switch
Control harwdare using the POS as a switch
- 1.0.3
+ 1.0.4
true
diff --git a/Plugins/BTCPayServer.Plugins.BitcoinSwitch/BitcoinSwitchService.cs b/Plugins/BTCPayServer.Plugins.BitcoinSwitch/BitcoinSwitchService.cs
index 6eb11a29..63d7798f 100644
--- a/Plugins/BTCPayServer.Plugins.BitcoinSwitch/BitcoinSwitchService.cs
+++ b/Plugins/BTCPayServer.Plugins.BitcoinSwitch/BitcoinSwitchService.cs
@@ -174,11 +174,22 @@ private async Task HandleGPIOMessages(CancellationToken cancellationToken, Bitco
return;
foreach (var socket in sockets)
{
- await socket.SendAsync(
- new ArraySegment(buffer),
- WebSocketMessageType.Text,
- true,
- cancellationToken);
+ // Isolate each socket with its own timeout so one stalled or faulted
+ // listener can't block or abort delivery to the real device.
+ try
+ {
+ using var sendCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+ sendCts.CancelAfter(TimeSpan.FromSeconds(5));
+ await socket.SendAsync(
+ new ArraySegment(buffer),
+ WebSocketMessageType.Text,
+ true,
+ sendCts.Token);
+ }
+ catch (Exception ex)
+ {
+ Logs.PayServer.LogWarning(ex, "Failed to send BitcoinSwitch command to a socket; continuing");
+ }
}
}
diff --git a/Plugins/BTCPayServer.Plugins.Blink/BTCPayServer.Plugins.Blink.csproj b/Plugins/BTCPayServer.Plugins.Blink/BTCPayServer.Plugins.Blink.csproj
index 96da6c8d..9df09355 100644
--- a/Plugins/BTCPayServer.Plugins.Blink/BTCPayServer.Plugins.Blink.csproj
+++ b/Plugins/BTCPayServer.Plugins.Blink/BTCPayServer.Plugins.Blink.csproj
@@ -9,7 +9,7 @@
Blink
Blink Lightning support
- 1.1.1
+ 1.1.2
true
BTCPayServer.Plugins.Blink
diff --git a/Plugins/BTCPayServer.Plugins.Blink/BlinkLnAddressLightningClient.cs b/Plugins/BTCPayServer.Plugins.Blink/BlinkLnAddressLightningClient.cs
index 15fe2013..d4ca605d 100644
--- a/Plugins/BTCPayServer.Plugins.Blink/BlinkLnAddressLightningClient.cs
+++ b/Plugins/BTCPayServer.Plugins.Blink/BlinkLnAddressLightningClient.cs
@@ -622,7 +622,6 @@ internal enum VerifyOutcome
private LightningInvoice BuildInvoice(string paymentHash, TrackedInvoice tracked, bool settled, string? preimage)
{
var bolt11 = BOLT11PaymentRequest.Parse(tracked.Bolt11, _network);
- var status = DetermineStatus(settled, tracked.ExpiresAt, DateTimeOffset.UtcNow);
// The authoritative payment hash is the verify key (the `paymentHash` argument = BTCPay's
// invoice Id), NOT the hash parsed from the returned `pr`. blink-lnurl-server's verify
@@ -642,11 +641,13 @@ private LightningInvoice BuildInvoice(string paymentHash, TrackedInvoice tracked
var reportedBolt11 = bolt11Matches ? tracked.Bolt11 : null;
var reportedAmount = bolt11Matches ? bolt11.MinimumAmount : null;
- // BTCPay validates the preimage: it must be 64 hex chars and SHA256(preimage)==paymentHash.
- // If invalid, drop it (the payment is still recorded, just without a preimage).
+ // Require a valid preimage (64 hex chars, SHA256(preimage)==paymentHash) before reporting the
+ // invoice as paid; a settled flag without that proof is not trusted.
var validPreimage = settled ? ValidatePreimage(paymentHash, preimage) : null;
- if (settled && preimage is not null && validPreimage is null)
- _logger.LogWarning("Blink preimage for {PaymentHash} did not validate against the payment hash; discarding.", paymentHash);
+ if (settled && validPreimage is null)
+ _logger.LogWarning("Blink reported {PaymentHash} settled without a valid preimage; not marking paid.", paymentHash);
+ var paid = validPreimage is not null && reportedAmount is not null;
+ var status = DetermineStatus(paid, tracked.ExpiresAt, DateTimeOffset.UtcNow);
return new LightningInvoice
{
@@ -655,10 +656,10 @@ private LightningInvoice BuildInvoice(string paymentHash, TrackedInvoice tracked
BOLT11 = reportedBolt11,
// Amount is parsed from the BOLT11; BTCPay requires a non-null amount to record payment.
Amount = reportedAmount,
- AmountReceived = settled ? reportedAmount : null,
+ AmountReceived = paid ? reportedAmount : null,
Status = status,
Preimage = validPreimage,
- PaidAt = settled ? DateTimeOffset.UtcNow : null,
+ PaidAt = paid ? DateTimeOffset.UtcNow : null,
ExpiresAt = tracked.ExpiresAt
};
}
diff --git a/Plugins/BTCPayServer.Plugins.Blink/BlinkPlugin.cs b/Plugins/BTCPayServer.Plugins.Blink/BlinkPlugin.cs
index 4b0fd5c1..086cead1 100644
--- a/Plugins/BTCPayServer.Plugins.Blink/BlinkPlugin.cs
+++ b/Plugins/BTCPayServer.Plugins.Blink/BlinkPlugin.cs
@@ -12,7 +12,7 @@ public class BlinkPlugin : BaseBTCPayServerPlugin
{
public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } =
{
- new() { Identifier = nameof(BTCPayServer), Condition = ">=2.3.7" }
+ new() { Identifier = nameof(BTCPayServer), Condition = ">=2.4.2" }
};
diff --git a/Plugins/BTCPayServer.Plugins.Bringin/BTCPayServer.Plugins.Bringin.csproj b/Plugins/BTCPayServer.Plugins.Bringin/BTCPayServer.Plugins.Bringin.csproj
index fe890feb..3cd0d9e9 100644
--- a/Plugins/BTCPayServer.Plugins.Bringin/BTCPayServer.Plugins.Bringin.csproj
+++ b/Plugins/BTCPayServer.Plugins.Bringin/BTCPayServer.Plugins.Bringin.csproj
@@ -9,7 +9,7 @@
Bringin
Euro Offramp
- 1.0.6
+ 1.0.7
true
diff --git a/Plugins/BTCPayServer.Plugins.Bringin/BringinController.cs b/Plugins/BTCPayServer.Plugins.Bringin/BringinController.cs
index d5032b2c..bda61e30 100644
--- a/Plugins/BTCPayServer.Plugins.Bringin/BringinController.cs
+++ b/Plugins/BTCPayServer.Plugins.Bringin/BringinController.cs
@@ -54,9 +54,13 @@ public async Task Edit()
[IgnoreAntiforgeryToken]
public async Task Callback(string storeId, string code, [FromBody]BringinVerificationUpdate content)
{
+ // Authenticate the shared code against read-only settings BEFORE any state change: Update()
+ // takes the store edit lock, so validating first stops an anonymous caller from holding it.
+ var settings = await _bringinService.Get(storeId);
+ if (settings?.Code != code) return BadRequest();
+ if (content.verificationStatus != "APPROVED") return BadRequest("Verification not approved");
+
var vm = await _bringinService.Update(storeId);
- if(vm.Code != code) return BadRequest();
- if(content.verificationStatus != "APPROVED") return BadRequest("Verification not approved");
if (string.IsNullOrEmpty(vm.ApiKey) && !string.IsNullOrEmpty(content.apikey))
{
diff --git a/Plugins/BTCPayServer.Plugins.Bringin/BringinService.cs b/Plugins/BTCPayServer.Plugins.Bringin/BringinService.cs
index a3d0f77d..ef993dfa 100644
--- a/Plugins/BTCPayServer.Plugins.Bringin/BringinService.cs
+++ b/Plugins/BTCPayServer.Plugins.Bringin/BringinService.cs
@@ -223,6 +223,8 @@ private async Task CheckIfNewPayoutsNeeded(string storeId, BringinStoreSet
if (methodSetting.Value.FiatThreshold)
{
var rate = await bringinClient.GetRate();
+ if (rate.BringinPrice <= 0)
+ throw new Exception($"Bringin returned an invalid exchange rate ({rate.BringinPrice}).");
thresholdAmount = methodSetting.Value.Threshold / rate.BringinPrice;
}
@@ -279,6 +281,8 @@ await _storeRepository.UpdateSetting(storeId, BringinStoreSettings.BringinSettin
{
var rate = await bringinClient.GetRate();
+ if (rate.BringinPrice <= 0)
+ throw new Exception($"Bringin returned an invalid exchange rate ({rate.BringinPrice}).");
var thresholdAmount = supportedMethod.FiatMinimumAmount / rate.BringinPrice;
if (amountBtc.ToDecimal(MoneyUnit.BTC) <= thresholdAmount)
{
@@ -294,6 +298,11 @@ await _storeRepository.UpdateSetting(storeId, BringinStoreSettings.BringinSettin
PaymentMethod = supportedMethod.bringinMethod
};
var order = await bringinClient.PlaceOrder(request);
+ // The Bringin response is untrusted: reject a non-positive amount or one larger than requested
+ // before it becomes a pre-approved payout, so a hostile response can't claim more than intended.
+ if (order.Amount <= 0 || order.Amount > amountBtc.Satoshi)
+ throw new Exception(
+ $"Bringin returned an invalid order amount ({order.Amount} sats) for a {amountBtc.Satoshi} sats request.");
var orderMoney = Money.Satoshis(order.Amount);
if (!payout)
diff --git a/Plugins/BTCPayServer.Plugins.Conference/BTCPayServer.Plugins.Conference.csproj b/Plugins/BTCPayServer.Plugins.Conference/BTCPayServer.Plugins.Conference.csproj
index a1511652..2142b414 100644
--- a/Plugins/BTCPayServer.Plugins.Conference/BTCPayServer.Plugins.Conference.csproj
+++ b/Plugins/BTCPayServer.Plugins.Conference/BTCPayServer.Plugins.Conference.csproj
@@ -9,7 +9,7 @@
Conference
Manage conference merchant stores, POS terminals, and sales reporting from a single app
- 1.2.0
+ 1.2.1
true
diff --git a/Plugins/BTCPayServer.Plugins.Conference/ConferenceController.cs b/Plugins/BTCPayServer.Plugins.Conference/ConferenceController.cs
index e992509d..1f1e47c5 100644
--- a/Plugins/BTCPayServer.Plugins.Conference/ConferenceController.cs
+++ b/Plugins/BTCPayServer.Plugins.Conference/ConferenceController.cs
@@ -23,6 +23,7 @@ namespace BTCPayServer.Plugins.Conference;
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
+[AutoValidateAntiforgeryToken]
public class ConferenceController : Controller
{
private readonly AppService _appService;
diff --git a/Plugins/BTCPayServer.Plugins.DataErasure/BTCPayServer.Plugins.DataErasure.csproj b/Plugins/BTCPayServer.Plugins.DataErasure/BTCPayServer.Plugins.DataErasure.csproj
index 5557bc92..f3478122 100644
--- a/Plugins/BTCPayServer.Plugins.DataErasure/BTCPayServer.Plugins.DataErasure.csproj
+++ b/Plugins/BTCPayServer.Plugins.DataErasure/BTCPayServer.Plugins.DataErasure.csproj
@@ -9,7 +9,7 @@
Data Erasure
Allows you to erase user data from invoices after a period of time.
- 1.0.5
+ 1.0.6
true
diff --git a/Plugins/BTCPayServer.Plugins.DataErasure/DataErasurePlugin.cs b/Plugins/BTCPayServer.Plugins.DataErasure/DataErasurePlugin.cs
index 02edfb3c..711a5ca9 100644
--- a/Plugins/BTCPayServer.Plugins.DataErasure/DataErasurePlugin.cs
+++ b/Plugins/BTCPayServer.Plugins.DataErasure/DataErasurePlugin.cs
@@ -9,7 +9,7 @@ public class DataErasurePlugin : BaseBTCPayServerPlugin
{
public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } =
{
- new() { Identifier = nameof(BTCPayServer), Condition = ">=2.4.1" }
+ new() { Identifier = nameof(BTCPayServer), Condition = ">=2.4.2" }
};
public override void Execute(IServiceCollection applicationBuilder)
{
diff --git a/Plugins/BTCPayServer.Plugins.DataErasure/DataErasureService.cs b/Plugins/BTCPayServer.Plugins.DataErasure/DataErasureService.cs
index 861d8acb..3ca32cf0 100644
--- a/Plugins/BTCPayServer.Plugins.DataErasure/DataErasureService.cs
+++ b/Plugins/BTCPayServer.Plugins.DataErasure/DataErasureService.cs
@@ -34,14 +34,12 @@ public async Task Get(string storeId)
public async Task Set(string storeId, DataErasureSettings settings, bool clearDate = false)
{
- _cts?.Cancel();
- await _runningLock.WaitAsync();
var existing = await Get(storeId);
- settings.LastRunCutoff = clearDate? null: existing?.LastRunCutoff;
+ settings.LastRunCutoff = clearDate ? null : existing?.LastRunCutoff;
await SetCore(storeId, settings);
- _runningLock.Release();
- _cts = new CancellationTokenSource();
- _ = Run();
+ // Wake the single worker so new settings apply promptly; coalesced to one pending run.
+ try { _wake.Release(); }
+ catch (SemaphoreFullException) { }
}
private async Task SetCore(string storeId, DataErasureSettings settings)
@@ -50,114 +48,132 @@ private async Task SetCore(string storeId, DataErasureSettings settings)
}
public bool IsRunning { get; private set; }
- private readonly SemaphoreSlim _runningLock = new(1, 1);
- private async Task Run()
+ // Signals the worker to run a cycle immediately, bounded to a single pending wake.
+ private readonly SemaphoreSlim _wake = new(0, 1);
+
+ private async Task RunLoop(CancellationToken ct)
{
- while (!_cts.IsCancellationRequested)
+ while (!ct.IsCancellationRequested)
{
- await _runningLock.WaitAsync(_cts.Token);
IsRunning = true;
-
-
- var settings =
- await _storeRepository.GetSettingsAsync(nameof(DataErasureSettings));
- foreach (var setting in settings.Where(setting => setting.Value.Enabled))
+ try
{
- var count = 0;
- var cutoffDate = DateTimeOffset.UtcNow.Subtract(TimeSpan.FromDays(setting.Value.DaysToKeep));
- if (setting.Value.EntirelyEraseInvoice)
- {
- await using var db = _dbContextFactory.CreateContext();
- db.Invoices.RemoveRange(db.Invoices.Where(i => i.StoreDataId == setting.Key && i.Created < cutoffDate && (setting.Value.LastRunCutoff == null || i.Created > setting.Value.LastRunCutoff)));
- count = await db.SaveChangesAsync(_cts.Token);
- }
- else
+ var settings =
+ await _storeRepository.GetSettingsAsync(nameof(DataErasureSettings));
+ foreach (var setting in settings.Where(setting => setting.Value.Enabled))
{
-
-
-
- var skip = 0;
- while (true)
+ try
{
- var invoices = await _invoiceRepository.GetInvoices(new InvoiceQuery()
+ var count = 0;
+ var cutoffDate = DateTimeOffset.UtcNow.Subtract(TimeSpan.FromDays(setting.Value.DaysToKeep));
+ if (setting.Value.EntirelyEraseInvoice)
{
- StartDate = setting.Value.LastRunCutoff,
- EndDate = cutoffDate,
- StoreId = new[] {setting.Key},
- Skip = skip,
- Take = 100
- }, _cts.Token);
-
-
-
- foreach (var invoice in invoices)
+ await using var db = _dbContextFactory.CreateContext();
+ db.Invoices.RemoveRange(db.Invoices.Where(i => i.StoreDataId == setting.Key && i.Created < cutoffDate && (setting.Value.LastRunCutoff == null || i.Created > setting.Value.LastRunCutoff)));
+ count = await db.SaveChangesAsync(ct);
+ }
+ else
{
- //replace all buyer info with "erased"
- if (!string.IsNullOrEmpty(invoice.Metadata.BuyerAddress1) ||
- !string.IsNullOrEmpty(invoice.Metadata.BuyerAddress2) ||
- !string.IsNullOrEmpty(invoice.Metadata.BuyerCity) ||
- !string.IsNullOrEmpty(invoice.Metadata.BuyerCountry) ||
- !string.IsNullOrEmpty(invoice.Metadata.BuyerEmail) ||
- !string.IsNullOrEmpty(invoice.Metadata.BuyerName) ||
- !string.IsNullOrEmpty(invoice.Metadata.BuyerPhone) ||
- !string.IsNullOrEmpty(invoice.Metadata.BuyerState) ||
- !string.IsNullOrEmpty(invoice.Metadata.BuyerZip))
+ var skip = 0;
+ while (true)
{
- await _invoiceRepository.UpdateInvoiceMetadata(invoice.Id, metadata =>
+ var invoices = await _invoiceRepository.GetInvoices(new InvoiceQuery()
+ {
+ StartDate = setting.Value.LastRunCutoff,
+ EndDate = cutoffDate,
+ StoreId = new[] {setting.Key},
+ Skip = skip,
+ Take = 100
+ }, ct);
+
+ foreach (var invoice in invoices)
+ {
+ //replace all buyer info with "erased"
+ var metadata = invoice.Metadata;
+ if (!string.IsNullOrEmpty(metadata.BuyerAddress1) ||
+ !string.IsNullOrEmpty(metadata.BuyerAddress2) ||
+ !string.IsNullOrEmpty(metadata.BuyerCity) ||
+ !string.IsNullOrEmpty(metadata.BuyerCountry) ||
+ !string.IsNullOrEmpty(metadata.BuyerEmail) ||
+ !string.IsNullOrEmpty(metadata.BuyerName) ||
+ !string.IsNullOrEmpty(metadata.BuyerPhone) ||
+ !string.IsNullOrEmpty(metadata.BuyerState) ||
+ !string.IsNullOrEmpty(metadata.BuyerZip))
+ {
+ if (!string.IsNullOrEmpty(metadata.BuyerAddress1))
+ metadata.BuyerAddress1 = "erased";
+ if (!string.IsNullOrEmpty(metadata.BuyerAddress2))
+ metadata.BuyerAddress2 = "erased";
+ if (!string.IsNullOrEmpty(metadata.BuyerCity))
+ metadata.BuyerCity = "erased";
+ if (!string.IsNullOrEmpty(metadata.BuyerCountry))
+ metadata.BuyerCountry = "erased";
+ if (!string.IsNullOrEmpty(metadata.BuyerEmail))
+ metadata.BuyerEmail = "erased";
+ if (!string.IsNullOrEmpty(metadata.BuyerName))
+ metadata.BuyerName = "erased";
+ if (!string.IsNullOrEmpty(metadata.BuyerPhone))
+ metadata.BuyerPhone = "erased";
+ if (!string.IsNullOrEmpty(metadata.BuyerState))
+ metadata.BuyerState = "erased";
+ if (!string.IsNullOrEmpty(metadata.BuyerZip))
+ metadata.BuyerZip = "erased";
+ await _invoiceRepository.UpdateInvoiceMetadata(invoice.Id, invoice.StoreId, metadata.ToJObject());
+ }
+ count++;
+ }
+
+ if (invoices.Length < 100)
{
- if (!string.IsNullOrEmpty(metadata.BuyerAddress1))
- metadata.BuyerAddress1 = "erased";
- if (!string.IsNullOrEmpty(metadata.BuyerAddress2))
- metadata.BuyerAddress2 = "erased";
- if (!string.IsNullOrEmpty(metadata.BuyerCity))
- metadata.BuyerCity = "erased";
- if (!string.IsNullOrEmpty(metadata.BuyerCountry))
- metadata.BuyerCountry = "erased";
- if (!string.IsNullOrEmpty(metadata.BuyerEmail))
- metadata.BuyerEmail = "erased";
- if (!string.IsNullOrEmpty(metadata.BuyerName))
- metadata.BuyerName = "erased";
- if (!string.IsNullOrEmpty(metadata.BuyerPhone))
- metadata.BuyerPhone = "erased";
- if (!string.IsNullOrEmpty(metadata.BuyerState))
- metadata.BuyerState = "erased";
- if (!string.IsNullOrEmpty(metadata.BuyerZip))
- metadata.BuyerZip = "erased";
- return metadata;
- });
+ break;
+ }
+
+ skip += 100;
}
- count++;
}
- if (invoices.Length < 100)
+ if (count > 0)
+ _logger.LogInformation($"Erased {count} invoice data for store {setting.Key}");
+ // Persist only cutoff progress against the latest settings, so a concurrent
+ // user change (e.g. disabling erasure mid-cycle) is not clobbered.
+ var latest = await Get(setting.Key);
+ if (latest != null)
{
- break;
+ latest.LastRunCutoff = cutoffDate;
+ await SetCore(setting.Key, latest);
}
-
- skip += 100;
+ }
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Failed to erase data for store {StoreId}", setting.Key);
}
}
-
- if (count > 0)
- _logger.LogInformation($"Erased {count} invoice data for store {setting.Key}");
- setting.Value.LastRunCutoff = cutoffDate;
- await SetCore(setting.Key, setting.Value);
+ }
+ catch (OperationCanceledException)
+ {
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Data erasure cycle failed");
+ }
+ finally
+ {
+ IsRunning = false;
}
- IsRunning = false;
-
-
- _runningLock.Release();
- await Task.Delay(TimeSpan.FromHours(1), _cts.Token);
- }
-
- try
- {
- _runningLock.Release();
- }
- catch (Exception e)
- {
+ try
+ {
+ await _wake.WaitAsync(TimeSpan.FromHours(1), ct);
+ }
+ catch (OperationCanceledException)
+ {
+ break;
+ }
}
}
@@ -166,7 +182,7 @@ await _invoiceRepository.UpdateInvoiceMetadata(invoice.Id, metadata =>
public Task StartAsync(CancellationToken cancellationToken)
{
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
- _ = Run();
+ _ = RunLoop(_cts.Token);
return Task.CompletedTask;
}
diff --git a/Plugins/BTCPayServer.Plugins.DynamicRateLimits/BTCPayServer.Plugins.DynamicRateLimits.csproj b/Plugins/BTCPayServer.Plugins.DynamicRateLimits/BTCPayServer.Plugins.DynamicRateLimits.csproj
index 3744541e..e969b758 100644
--- a/Plugins/BTCPayServer.Plugins.DynamicRateLimits/BTCPayServer.Plugins.DynamicRateLimits.csproj
+++ b/Plugins/BTCPayServer.Plugins.DynamicRateLimits/BTCPayServer.Plugins.DynamicRateLimits.csproj
@@ -9,7 +9,7 @@
Dynamic Rate Limit
Allows you to override the default rate limiting.
- 1.0.2
+ 1.0.3
true
diff --git a/Plugins/BTCPayServer.Plugins.DynamicRateLimits/DynamicRateLimitsPlugin.cs b/Plugins/BTCPayServer.Plugins.DynamicRateLimits/DynamicRateLimitsPlugin.cs
index b128d6f0..3dcd7d54 100644
--- a/Plugins/BTCPayServer.Plugins.DynamicRateLimits/DynamicRateLimitsPlugin.cs
+++ b/Plugins/BTCPayServer.Plugins.DynamicRateLimits/DynamicRateLimitsPlugin.cs
@@ -10,7 +10,7 @@ public class DynamicRateLimitsPlugin : BaseBTCPayServerPlugin
{
public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } =
{
- new() { Identifier = nameof(BTCPayServer), Condition = ">=2.3.7" }
+ new() { Identifier = nameof(BTCPayServer), Condition = ">=2.4.2" }
};
public override void Execute(IServiceCollection applicationBuilder)
{
diff --git a/Plugins/BTCPayServer.Plugins.DynamicRateLimits/DynamicRatesLimiterController.cs b/Plugins/BTCPayServer.Plugins.DynamicRateLimits/DynamicRatesLimiterController.cs
index b855da9f..6556a2f2 100644
--- a/Plugins/BTCPayServer.Plugins.DynamicRateLimits/DynamicRatesLimiterController.cs
+++ b/Plugins/BTCPayServer.Plugins.DynamicRateLimits/DynamicRatesLimiterController.cs
@@ -31,6 +31,7 @@ public async Task Update()
[HttpPost("")]
+ [ValidateAntiForgeryToken]
public async Task Update(DynamicRateLimitSettings vm, string command)
{
switch (command)
diff --git a/Plugins/BTCPayServer.Plugins.Electrum/BTCPayServer.Plugins.Electrum.csproj b/Plugins/BTCPayServer.Plugins.Electrum/BTCPayServer.Plugins.Electrum.csproj
index f8f104d4..9c70405b 100644
--- a/Plugins/BTCPayServer.Plugins.Electrum/BTCPayServer.Plugins.Electrum.csproj
+++ b/Plugins/BTCPayServer.Plugins.Electrum/BTCPayServer.Plugins.Electrum.csproj
@@ -9,7 +9,7 @@
Electrum
Runs an Electrum (ElectrumX/Fulcrum) backend alongside NBXplorer — a bootstrap while NBXplorer syncs and an always-on fallback if it goes down — with automatic per-wallet failover. Mainnet only.
- 1.1.2
+ 1.1.3
true
diff --git a/Plugins/BTCPayServer.Plugins.Electrum/Electrum/ElectrumWalletTracker.cs b/Plugins/BTCPayServer.Plugins.Electrum/Electrum/ElectrumWalletTracker.cs
index d6c46530..7e6cbd93 100644
--- a/Plugins/BTCPayServer.Plugins.Electrum/Electrum/ElectrumWalletTracker.cs
+++ b/Plugins/BTCPayServer.Plugins.Electrum/Electrum/ElectrumWalletTracker.cs
@@ -250,6 +250,12 @@ public async Task> HandleScripthashNotificationAsync(
var addr = await ctx.TrackedAddresses.FindAsync(new object[] { scripthash }, ct);
if (addr == null) return newTxs;
+ // All of this wallet's tracked addresses, so a tx paying several of them is
+ // fully attributed in one pass (later notifications dedupe on txid).
+ var walletAddresses = await ctx.TrackedAddresses
+ .Where(a => a.WalletId == addr.WalletId)
+ .ToDictionaryAsync(a => a.Scripthash, ct);
+
// Fetch current history from Electrum
var history = await _client.ScripthashGetHistoryAsync(scripthash, ct);
var existingTxids = await ctx.Transactions
@@ -292,7 +298,7 @@ public async Task> HandleScripthashNotificationAsync(
ctx.Transactions.Add(trackedTx);
- var txInfo = BuildNewTransactionInfo(tx, addr, strategy, item);
+ var txInfo = BuildNewTransactionInfo(tx, strategy, item, walletAddresses);
if (txInfo != null)
newTxs.Add(txInfo);
}
@@ -308,6 +314,23 @@ public async Task> HandleScripthashNotificationAsync(
await ExtendGapIfNeeded(ctx, addr, ct);
}
+ // A single tx can pay several of this wallet's addresses; mark every co-paid address used
+ // too, so none is left eligible for reissue by GetNextUnusedAddressAsync.
+ var addressLookup = walletAddresses.Values.ToDictionary(a => a.Address);
+ foreach (var paidAddress in newTxs.SelectMany(t => t.Outputs).Select(o => o.Address).Distinct())
+ {
+ if (addressLookup.TryGetValue(paidAddress, out var coPaid) && !coPaid.IsUsed)
+ {
+ coPaid.IsUsed = true;
+ // Best-effort: a failed gap-extension subscription must not abort persisting the tx.
+ try { await ExtendGapIfNeeded(ctx, coPaid, ct); }
+ catch (Exception e) when (!ct.IsCancellationRequested)
+ {
+ _logger.LogWarning(e, "Failed to extend gap for co-paid address {Address}", coPaid.Address);
+ }
+ }
+ }
+
await ctx.SaveChangesAsync(ct);
}
finally
@@ -1311,8 +1334,8 @@ public class OutputInfo
}
private NewTransactionInfo BuildNewTransactionInfo(
- Transaction tx, TrackedAddress matchedAddr,
- DerivationStrategyBase strategy, ElectrumHistoryItem historyItem)
+ Transaction tx, DerivationStrategyBase strategy, ElectrumHistoryItem historyItem,
+ IReadOnlyDictionary walletAddresses)
{
var info = new NewTransactionInfo
{
@@ -1325,25 +1348,23 @@ private NewTransactionInfo BuildNewTransactionInfo(
Transaction = tx
};
- // Find outputs that match our tracked addresses
+ // Attribute each output to whichever of this wallet's tracked addresses it pays.
for (var i = 0; i < tx.Outputs.Count; i++)
{
var output = tx.Outputs[i];
var scriptHash = ScriptHashUtility.ComputeScriptHash(output.ScriptPubKey);
- // Check if this output goes to one of our addresses
- if (scriptHash == matchedAddr.Scripthash ||
- _subscribedScripthashes.ContainsKey(scriptHash))
+ if (walletAddresses.TryGetValue(scriptHash, out var outputAddr))
{
- var parts = matchedAddr.KeyPath.Split('/');
+ var parts = outputAddr.KeyPath.Split('/');
var keyIndex = parts.Length == 2 && int.TryParse(parts[1], out var idx) ? idx : 0;
info.Outputs.Add(new OutputInfo
{
- Address = matchedAddr.Address,
+ Address = outputAddr.Address,
Index = i,
Value = output.Value,
- KeyPath = matchedAddr.KeyPath,
+ KeyPath = outputAddr.KeyPath,
KeyIndex = keyIndex
});
}
@@ -1352,9 +1373,4 @@ private NewTransactionInfo BuildNewTransactionInfo(
return info.Outputs.Count > 0 ? info : null;
}
- private ConcurrentDictionary _subscribedScripthashes =>
- (ConcurrentDictionary)typeof(ElectrumClient)
- .GetField("_subscribedScripthashes", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
- ?.GetValue(_client) ?? new ConcurrentDictionary();
-
}
diff --git a/Plugins/BTCPayServer.Plugins.Electrum/ElectrumPlugin.cs b/Plugins/BTCPayServer.Plugins.Electrum/ElectrumPlugin.cs
index e1eb1614..67b394f3 100644
--- a/Plugins/BTCPayServer.Plugins.Electrum/ElectrumPlugin.cs
+++ b/Plugins/BTCPayServer.Plugins.Electrum/ElectrumPlugin.cs
@@ -21,7 +21,7 @@ public class ElectrumPlugin : BaseBTCPayServerPlugin
{
public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } =
{
- new() { Identifier = nameof(BTCPayServer), Condition = ">=2.3.7" }
+ new() { Identifier = nameof(BTCPayServer), Condition = ">=2.4.2" }
};
// Escape hatch for tests: allows the mainnet-only guard below to be bypassed
diff --git a/Plugins/BTCPayServer.Plugins.FileSeller/BTCPayServer.Plugins.FileSeller.csproj b/Plugins/BTCPayServer.Plugins.FileSeller/BTCPayServer.Plugins.FileSeller.csproj
index a09a8fb5..1727b335 100644
--- a/Plugins/BTCPayServer.Plugins.FileSeller/BTCPayServer.Plugins.FileSeller.csproj
+++ b/Plugins/BTCPayServer.Plugins.FileSeller/BTCPayServer.Plugins.FileSeller.csproj
@@ -9,7 +9,7 @@
File Seller
Allows you to sell files through the point of sale/crowdfund apps.
- 1.0.9
+ 1.0.10
true
diff --git a/Plugins/BTCPayServer.Plugins.FileSeller/FileSellerService.cs b/Plugins/BTCPayServer.Plugins.FileSeller/FileSellerService.cs
index b8d115ad..6680821d 100644
--- a/Plugins/BTCPayServer.Plugins.FileSeller/FileSellerService.cs
+++ b/Plugins/BTCPayServer.Plugins.FileSeller/FileSellerService.cs
@@ -48,8 +48,9 @@ protected override async Task ProcessEvent(object evt, CancellationToken cancell
{
if (evt is not InvoiceEvent invoiceEvent) return;
List cartItems = null;
- if (invoiceEvent.Name is not (InvoiceEvent.Completed or InvoiceEvent.MarkedCompleted
- or InvoiceEvent.Confirmed))
+ // Release downloads only on settled states, never on Confirmed (which fires before
+ // settlement and is reversible for 0-conf stores).
+ if (invoiceEvent.Name is not (InvoiceEvent.Completed or InvoiceEvent.MarkedCompleted))
{
return;
}
diff --git a/Plugins/BTCPayServer.Plugins.LNURLVerify/BTCPayServer.Plugins.LNURLVerify.csproj b/Plugins/BTCPayServer.Plugins.LNURLVerify/BTCPayServer.Plugins.LNURLVerify.csproj
index 34e978f8..3ddf21e2 100644
--- a/Plugins/BTCPayServer.Plugins.LNURLVerify/BTCPayServer.Plugins.LNURLVerify.csproj
+++ b/Plugins/BTCPayServer.Plugins.LNURLVerify/BTCPayServer.Plugins.LNURLVerify.csproj
@@ -9,7 +9,7 @@
LNURL Verify
Use any LNURL or Lightning address as a BTCPay Lightning backend (receive via LUD-21 verify; send via LNURL-withdraw).
- 1.0.0
+ 1.0.1
true
BTCPayServer.Plugins.LNURLVerify
diff --git a/Plugins/BTCPayServer.Plugins.LNURLVerify/LNURLReceiver.cs b/Plugins/BTCPayServer.Plugins.LNURLVerify/LNURLReceiver.cs
index 83e7e999..2fcd854a 100644
--- a/Plugins/BTCPayServer.Plugins.LNURLVerify/LNURLReceiver.cs
+++ b/Plugins/BTCPayServer.Plugins.LNURLVerify/LNURLReceiver.cs
@@ -177,20 +177,22 @@ public async Task CreateInvoice(LightMoney? amount, string? de
private static LightningInvoice BuildInvoice(TrackedInvoice t, bool settled, string? preimage)
{
var bolt11 = BOLT11PaymentRequest.Parse(t.Bolt11, InferNetwork(t.Bolt11));
- var status = settled ? LightningInvoiceStatus.Paid
+ var normalizedPreimage = preimage?.Trim();
+ string? valid = settled && normalizedPreimage is not null && IsValidPreimage(normalizedPreimage, t.PaymentHash) ? normalizedPreimage : null;
+ var paid = valid is not null;
+ var status = paid ? LightningInvoiceStatus.Paid
: t.ExpiresAt < DateTimeOffset.UtcNow ? LightningInvoiceStatus.Expired
: LightningInvoiceStatus.Unpaid;
- string? valid = settled && preimage is not null && IsValidPreimage(preimage, t.PaymentHash) ? preimage : null;
return new LightningInvoice
{
Id = t.PaymentHash,
PaymentHash = t.PaymentHash,
BOLT11 = t.Bolt11,
Amount = bolt11.MinimumAmount,
- AmountReceived = settled ? bolt11.MinimumAmount : null,
+ AmountReceived = paid ? bolt11.MinimumAmount : null,
Status = status,
Preimage = valid,
- PaidAt = settled ? DateTimeOffset.UtcNow : null,
+ PaidAt = paid ? DateTimeOffset.UtcNow : null,
ExpiresAt = t.ExpiresAt
};
}
diff --git a/Plugins/BTCPayServer.Plugins.LNURLVerify/LNURLVerifyPlugin.cs b/Plugins/BTCPayServer.Plugins.LNURLVerify/LNURLVerifyPlugin.cs
index 8f8fe3be..d55f15f5 100644
--- a/Plugins/BTCPayServer.Plugins.LNURLVerify/LNURLVerifyPlugin.cs
+++ b/Plugins/BTCPayServer.Plugins.LNURLVerify/LNURLVerifyPlugin.cs
@@ -12,7 +12,7 @@ public class LNURLVerifyPlugin : BaseBTCPayServerPlugin
{
public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } =
{
- new() { Identifier = nameof(BTCPayServer), Condition = ">=2.4.0" }
+ new() { Identifier = nameof(BTCPayServer), Condition = ">=2.4.2" }
};
public override void Execute(IServiceCollection services)
diff --git a/Plugins/BTCPayServer.Plugins.MCP/BTCPayServer.Plugins.MCP.csproj b/Plugins/BTCPayServer.Plugins.MCP/BTCPayServer.Plugins.MCP.csproj
index 40589e30..a1c2024f 100644
--- a/Plugins/BTCPayServer.Plugins.MCP/BTCPayServer.Plugins.MCP.csproj
+++ b/Plugins/BTCPayServer.Plugins.MCP/BTCPayServer.Plugins.MCP.csproj
@@ -9,7 +9,7 @@
MCP
Exposes a Model Context Protocol (MCP) server, allowing LLM clients to interact with BTCPay Server through natural language.
- 1.1.0
+ 1.1.1
true
diff --git a/Plugins/BTCPayServer.Plugins.MCP/McpPlugin.cs b/Plugins/BTCPayServer.Plugins.MCP/McpPlugin.cs
index 6878f617..df83e702 100644
--- a/Plugins/BTCPayServer.Plugins.MCP/McpPlugin.cs
+++ b/Plugins/BTCPayServer.Plugins.MCP/McpPlugin.cs
@@ -15,7 +15,7 @@ public class McpPlugin : BaseBTCPayServerPlugin
{
public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } =
[
- new() { Identifier = nameof(BTCPayServer), Condition = ">=2.3.7" }
+ new() { Identifier = nameof(BTCPayServer), Condition = ">=2.4.2" }
];
public override void Execute(IServiceCollection services)
@@ -25,11 +25,7 @@ public override void Execute(IServiceCollection services)
.WithHttpTransport(options => options.Stateless = true)
.WithToolsFromAssembly();
services.AddScoped();
- services.AddHttpClient("McpGreenfield")
- .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
- {
- ServerCertificateCustomValidationCallback = (_, _, _, _) => true
- });
+ services.AddHttpClient("McpGreenfield");
// Insert token middleware before authentication in the pipeline
services.AddTransient();
base.Execute(services);
diff --git a/Plugins/BTCPayServer.Plugins.MicroNode/BTCPayServer.Plugins.MicroNode.csproj b/Plugins/BTCPayServer.Plugins.MicroNode/BTCPayServer.Plugins.MicroNode.csproj
index 6f1d72a6..8df3d0ef 100644
--- a/Plugins/BTCPayServer.Plugins.MicroNode/BTCPayServer.Plugins.MicroNode.csproj
+++ b/Plugins/BTCPayServer.Plugins.MicroNode/BTCPayServer.Plugins.MicroNode.csproj
@@ -11,7 +11,7 @@
MicroNode
Micro ln node.
- 1.1.0
+ 1.1.1
true
diff --git a/Plugins/BTCPayServer.Plugins.MicroNode/MicroLightningClient.cs b/Plugins/BTCPayServer.Plugins.MicroNode/MicroLightningClient.cs
index 36f28c8a..f1e46a4f 100644
--- a/Plugins/BTCPayServer.Plugins.MicroNode/MicroLightningClient.cs
+++ b/Plugins/BTCPayServer.Plugins.MicroNode/MicroLightningClient.cs
@@ -228,7 +228,7 @@ private LightningPayment FromPayResponse(PayResponse payResponse, MicroTransacti
PayResult.Error => LightningPaymentStatus.Failed,
_ => LightningPaymentStatus.Unknown
},
- AmountSent = transaction.Amount
+ AmountSent = LightMoney.MilliSatoshis(Math.Abs(transaction.Amount))
};
}
diff --git a/Plugins/BTCPayServer.Plugins.MicroNode/MicroNodeController.cs b/Plugins/BTCPayServer.Plugins.MicroNode/MicroNodeController.cs
index d4216448..6b2a553f 100644
--- a/Plugins/BTCPayServer.Plugins.MicroNode/MicroNodeController.cs
+++ b/Plugins/BTCPayServer.Plugins.MicroNode/MicroNodeController.cs
@@ -72,9 +72,11 @@ public async Task Configure(string storeId, string command, Micro
}
var pmi = PaymentTypes.LN.GetPaymentMethodId(network.CryptoCode);
var existing = store.GetPaymentMethodConfig(pmi, _paymentMethodHandlerDictionary);
- var isSet = settings?.Key is not null;
+ // The account key is owned server-side: reuse the store's existing key or mint a new one.
+ var storeSettings = await _microNodeService.GetStoreSettings(storeId);
settings ??= new MicroNodeStoreSettings();
- settings.Key ??= Guid.NewGuid().ToString();
+ settings.Key = storeSettings?.Key ?? Guid.NewGuid().ToString();
+ var isSet = storeSettings is not null;
var mlc = new MicroLightningClient(null, _microNodeService, network.NBitcoinNetwork,settings.Key);
var isStoreSetToThisMicro = existing?.GetExternalLightningUrl() == mlc.ToString();
@@ -93,7 +95,7 @@ public async Task Configure(string storeId, string command, Micro
return View(settings);
}
- if (!isSet)
+ if (masterStoreId is not null)
{
var masterSettings = await _microNodeService.GetMasterSettings(masterStoreId);
if (masterSettings is null)
diff --git a/Plugins/BTCPayServer.Plugins.MicroNode/MicroNodePlugin.cs b/Plugins/BTCPayServer.Plugins.MicroNode/MicroNodePlugin.cs
index 74cf283a..2be19bb0 100644
--- a/Plugins/BTCPayServer.Plugins.MicroNode/MicroNodePlugin.cs
+++ b/Plugins/BTCPayServer.Plugins.MicroNode/MicroNodePlugin.cs
@@ -11,7 +11,7 @@ public class MicroNodePlugin:BaseBTCPayServerPlugin
{
public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } =
{
- new () { Identifier = nameof(BTCPayServer), Condition = ">=2.3.7" }
+ new () { Identifier = nameof(BTCPayServer), Condition = ">=2.4.2" }
};
diff --git a/Plugins/BTCPayServer.Plugins.NIP05/BTCPayServer.Plugins.NIP05.csproj b/Plugins/BTCPayServer.Plugins.NIP05/BTCPayServer.Plugins.NIP05.csproj
index bf359adc..7df5d4c1 100644
--- a/Plugins/BTCPayServer.Plugins.NIP05/BTCPayServer.Plugins.NIP05.csproj
+++ b/Plugins/BTCPayServer.Plugins.NIP05/BTCPayServer.Plugins.NIP05.csproj
@@ -11,7 +11,7 @@
Nostr
NIP5 addresses, Zap support, Nostr Wallet Connect Lightning support
- 1.1.20
+ 1.1.21
true
@@ -36,7 +36,7 @@
-
+
diff --git a/Plugins/BTCPayServer.Plugins.NIP05/Zapper.cs b/Plugins/BTCPayServer.Plugins.NIP05/Zapper.cs
index fbb56412..02d514d7 100644
--- a/Plugins/BTCPayServer.Plugins.NIP05/Zapper.cs
+++ b/Plugins/BTCPayServer.Plugins.NIP05/Zapper.cs
@@ -13,6 +13,7 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using NBitcoin;
+using Newtonsoft.Json.Linq;
using NNostr.Client;
using JsonSerializer = System.Text.Json.JsonSerializer;
@@ -200,12 +201,14 @@ private async Task Subscription(InvoiceEvent arg)
zapReceipt = await zapReceipt.ComputeIdAndSignAsync(key);
relays = relays.Concat(userNostrSettings?.Relays ?? Array.Empty()).Distinct().ToArray();
- await _invoiceRepository.UpdateInvoiceMetadata(arg.InvoiceId, "Nostr", new Dictionary()
+ var metadata = arg.Invoice.Metadata.ToJObject();
+ metadata["Nostr"] = new JObject
{
- {"Zap Request", zapRequestEvent.Id},
- {"Zap Receipt", zapReceipt.Id},
- {"Relays", string.Join(',', relays)}
- });
+ ["Zap Request"] = zapRequestEvent.Id,
+ ["Zap Receipt"] = zapReceipt.Id,
+ ["Relays"] = string.Join(',', relays)
+ };
+ await _invoiceRepository.UpdateInvoiceMetadata(arg.InvoiceId, arg.Invoice.StoreId, metadata);
_pendingZapEvents.Add(new PendingZapEvent(relays, zapReceipt));
}
diff --git a/Plugins/BTCPayServer.Plugins.Prism/BTCPayServer.Plugins.Prism.csproj b/Plugins/BTCPayServer.Plugins.Prism/BTCPayServer.Plugins.Prism.csproj
index 0f1f16c8..80048902 100644
--- a/Plugins/BTCPayServer.Plugins.Prism/BTCPayServer.Plugins.Prism.csproj
+++ b/Plugins/BTCPayServer.Plugins.Prism/BTCPayServer.Plugins.Prism.csproj
@@ -11,7 +11,7 @@
Prism
Automated value splits for Bitcoin.
- 1.3.2
+ 1.3.3
true
diff --git a/Plugins/BTCPayServer.Plugins.Prism/OpenSatsDestinationValidator.cs b/Plugins/BTCPayServer.Plugins.Prism/OpenSatsDestinationValidator.cs
index 559fb13d..661edcb1 100644
--- a/Plugins/BTCPayServer.Plugins.Prism/OpenSatsDestinationValidator.cs
+++ b/Plugins/BTCPayServer.Plugins.Prism/OpenSatsDestinationValidator.cs
@@ -59,7 +59,18 @@ public async Task