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 Execute(object args) var xResult = await httpClient.PostAsync("https://opensats.org/api/btcpay",content).ConfigureAwait(false); var rawInvoice = JObject.Parse(await xResult.Content.ReadAsStringAsync().ConfigureAwait(false)); - var invoiceUrl = $"{rawInvoice.Value("checkoutLink").TrimEnd('/')}/{paymentMethod}/status"; + // checkoutLink comes from the third-party response; pin it to opensats.org over HTTPS so a + // hostile response can't turn this into a server-side request against an arbitrary/internal URL. + var checkoutLink = rawInvoice.Value("checkoutLink"); + if (!Uri.TryCreate(checkoutLink, UriKind.Absolute, out var checkoutUri) || + checkoutUri.Scheme != Uri.UriSchemeHttps || + !(checkoutUri.Host.Equals("opensats.org", StringComparison.OrdinalIgnoreCase) || + checkoutUri.Host.EndsWith(".opensats.org", StringComparison.OrdinalIgnoreCase))) + { + result.Success = false; + return result; + } + var invoiceUrl = $"{checkoutLink.TrimEnd('/')}/{paymentMethod}/status"; var invoiceBtcpayModel = JObject.Parse(await httpClient.GetStringAsync(invoiceUrl).ConfigureAwait(false)); var destination = invoiceBtcpayModel.Value("btcAddress"); diff --git a/Plugins/BTCPayServer.Plugins.Prism/OpenSatsPrismClaimCreate.cs b/Plugins/BTCPayServer.Plugins.Prism/OpenSatsPrismClaimCreate.cs index 9ed5c706..4a99935d 100644 --- a/Plugins/BTCPayServer.Plugins.Prism/OpenSatsPrismClaimCreate.cs +++ b/Plugins/BTCPayServer.Plugins.Prism/OpenSatsPrismClaimCreate.cs @@ -64,7 +64,17 @@ public async Task Execute(object args) var result = await httpClient.PostAsync("https://opensats.org/api/btcpay",content).ConfigureAwait(false); var rawInvoice = JObject.Parse(await result.Content.ReadAsStringAsync().ConfigureAwait(false)); - var invoiceUrl = $"{rawInvoice.Value("checkoutLink").TrimEnd('/')}/{paymentMethod}/status"; + // checkoutLink comes from the third-party response; pin it to opensats.org over HTTPS so a + // hostile response can't turn this into a server-side request against an arbitrary/internal URL. + var checkoutLink = rawInvoice.Value("checkoutLink"); + if (!Uri.TryCreate(checkoutLink, UriKind.Absolute, out var checkoutUri) || + checkoutUri.Scheme != Uri.UriSchemeHttps || + !(checkoutUri.Host.Equals("opensats.org", StringComparison.OrdinalIgnoreCase) || + checkoutUri.Host.EndsWith(".opensats.org", StringComparison.OrdinalIgnoreCase))) + { + return null; + } + var invoiceUrl = $"{checkoutLink.TrimEnd('/')}/{paymentMethod}/status"; var invoiceBtcpayModel = JObject.Parse(await httpClient.GetStringAsync(invoiceUrl).ConfigureAwait(false)); var destination = invoiceBtcpayModel.Value("btcAddress"); var receiptLink = invoiceBtcpayModel.Value("receiptLink"); diff --git a/Plugins/BTCPayServer.Plugins.Prism/PrismController.cs b/Plugins/BTCPayServer.Plugins.Prism/PrismController.cs index b98b85bd..5d8e58a6 100644 --- a/Plugins/BTCPayServer.Plugins.Prism/PrismController.cs +++ b/Plugins/BTCPayServer.Plugins.Prism/PrismController.cs @@ -351,6 +351,23 @@ public async Task EditDestination(string storeId, EditDestination } } + // A store-prism destination writes invoices into the target store on every payout, so require + // the caller to actually hold a role on that store (the dropdown is filtered, but the POST + // accepts arbitrary values). + if (destination.StartsWith("store-prism:", StringComparison.InvariantCultureIgnoreCase)) + { + var parts = destination.Split(':', StringSplitOptions.RemoveEmptyEntries); + var targetStoreId = parts.Length > 1 ? parts[1] : null; + var currentUser = await _userManager.GetUserAsync(User); + var appUser = currentUser is null + ? null + : await _userManager.Users.Where(c => c.Id == currentUser.Id).Include(u => u.UserStores).SingleOrDefaultAsync(); + if (targetStoreId is null || appUser?.UserStores.Any(us => us.StoreDataId == targetStoreId) != true) + { + ModelState.AddModelError(nameof(vm.SelectedStoreId), "You do not have access to the selected store."); + } + } + var currentSettings = await _satBreaker.Get(storeId) ?? new PrismSettings(); currentSettings.Destinations ??= new Dictionary(); diff --git a/Plugins/BTCPayServer.Plugins.Prism/SatBreaker.cs b/Plugins/BTCPayServer.Plugins.Prism/SatBreaker.cs index 71af64b1..0303cd63 100644 --- a/Plugins/BTCPayServer.Plugins.Prism/SatBreaker.cs +++ b/Plugins/BTCPayServer.Plugins.Prism/SatBreaker.cs @@ -274,6 +274,7 @@ private async Task CheckPayouts(CancellationToken cancellationToken) continue; } + var dirty = false; foreach (var payout in storePayouts) { @@ -347,9 +348,10 @@ private async Task CheckPayouts(CancellationToken cancellationToken) } prismSettings.PendingPayouts.Remove(payout.Id); + dirty = true; } - if (await CreatePayouts(storePayouts.Key, prismSettings, new())) + if (await CreatePayouts(storePayouts.Key, prismSettings, new()) || dirty) { await UpdatePrismSettingsForStore(storePayouts.Key, prismSettings, true); } @@ -633,8 +635,9 @@ public async Task UpdatePrismSettingsForStore(string storeId, PrismSetting var paymentGroup = payments.First(); if (!catchAlls.Remove(paymentGroup.Key, out catchAllSplit)) { - //shift the paymentgroup to bottom of the list - payments = payments.Where(grouping => grouping.Key != paymentGroup.Key).Append(paymentGroup).ToArray(); + //no catch-all matches this group and no null-key catch-all exists to drain it; + //drop it so the loop makes progress instead of rotating the same group forever. + payments = payments.Where(grouping => grouping.Key != paymentGroup.Key).ToArray(); continue; } diff --git a/Plugins/BTCPayServer.Plugins.SideShift/BTCPayServer.Plugins.SideShift.csproj b/Plugins/BTCPayServer.Plugins.SideShift/BTCPayServer.Plugins.SideShift.csproj index 1c6b0d0f..7b9e5bff 100644 --- a/Plugins/BTCPayServer.Plugins.SideShift/BTCPayServer.Plugins.SideShift.csproj +++ b/Plugins/BTCPayServer.Plugins.SideShift/BTCPayServer.Plugins.SideShift.csproj @@ -9,7 +9,7 @@ SideShift Allows you to embed a SideShift conversion screen to allow customers to pay with altcoins. - 1.1.17 + 1.1.18 true diff --git a/Plugins/BTCPayServer.Plugins.SideShift/SideShiftService.cs b/Plugins/BTCPayServer.Plugins.SideShift/SideShiftService.cs index 9b520457..2057e8c7 100644 --- a/Plugins/BTCPayServer.Plugins.SideShift/SideShiftService.cs +++ b/Plugins/BTCPayServer.Plugins.SideShift/SideShiftService.cs @@ -237,6 +237,16 @@ public async Task> GetDepositOptions() } var coins = await response.Content.ReadAsStringAsync().ContinueWith(t => JsonConvert.DeserializeObject(t.Result)); + static bool IsSafeIdentifier(string s) + { + if (string.IsNullOrEmpty(s)) return false; + foreach (var c in s) + { + var ok = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' || c == '-'; + if (!ok) return false; + } + return true; + } foreach (var asset in coins["depositMethods"].Children()) { if (asset.Value["enabled"].Value() is not true) @@ -248,6 +258,12 @@ public async Task> GetDepositOptions() var coinType = asset.Value["fixedOnly"].Value() ? CoinType.FixedOnly : asset.Value["variableOnly"].Value()? CoinType.VariableOnly : CoinType.Both; var network = asset.Value["network"].Value(); var cryptoCode = asset.Value["asset"].Value(); + // SideShift is untrusted and these ids are rendered into the checkout page's Vue + // expressions; skip any that aren't plain identifier characters. + if (!IsSafeIdentifier(id) || !IsSafeIdentifier(network) || !IsSafeIdentifier(cryptoCode)) + { + continue; + } result.Add(new SideshiftDepositCoin() { Id = id, diff --git a/Plugins/BTCPayServer.Plugins.TicketTailor/BTCPayServer.Plugins.TicketTailor.csproj b/Plugins/BTCPayServer.Plugins.TicketTailor/BTCPayServer.Plugins.TicketTailor.csproj index 50563fb9..21f396e8 100644 --- a/Plugins/BTCPayServer.Plugins.TicketTailor/BTCPayServer.Plugins.TicketTailor.csproj +++ b/Plugins/BTCPayServer.Plugins.TicketTailor/BTCPayServer.Plugins.TicketTailor.csproj @@ -9,7 +9,7 @@ TicketTailor Allows you to integrate with TicketTailor.com to sell tickets for Bitcoin - 2.0.9 + 2.0.10 true diff --git a/Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorPlugin.cs b/Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorPlugin.cs index 086dc517..5ce6798d 100644 --- a/Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorPlugin.cs +++ b/Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorPlugin.cs @@ -14,7 +14,7 @@ public class TicketTailorPlugin : 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.TicketTailor/TicketTailorWebhookProvider.cs b/Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorWebhookProvider.cs index a2f81e19..6c2f455b 100644 --- a/Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorWebhookProvider.cs +++ b/Plugins/BTCPayServer.Plugins.TicketTailor/TicketTailorWebhookProvider.cs @@ -154,6 +154,7 @@ await HandleIssueTicketError( InvoiceEventData.EventSeverity.Info); while (hold?.TotalOnHold > 0) { + var remainingBefore = hold.TotalOnHold; foreach (var tt in hold.Quantities.Where(quantity => quantity.Quantity > 0)) { @@ -179,7 +180,13 @@ await HandleIssueTicketError( } hold = await client.GetHold(holdId); + if (hold is null || ticketResult.error is not null) + break; } + + if (hold is null || errors.Count > 0 || tickets.Count >= holdOriginalAmount || + hold.TotalOnHold >= remainingBefore) + break; } diff --git a/plugin-builder.json b/plugin-builder.json index e2da1246..5347c392 100644 --- a/plugin-builder.json +++ b/plugin-builder.json @@ -1,28 +1,28 @@ { - "BTCPayServer.Plugins.BitcoinSwitch": { "slug": null, "deprecated": false }, - "BTCPayServer.Plugins.BitcoinWhitepaper": { "slug": null, "deprecated": false }, - "BTCPayServer.Plugins.Blink": { "slug": null, "deprecated": false }, - "BTCPayServer.Plugins.Breez": { "slug": null, "deprecated": true }, - "BTCPayServer.Plugins.Bringin": { "slug": null, "deprecated": false }, - "BTCPayServer.Plugins.Conference": { "slug": null, "deprecated": false }, - "BTCPayServer.Plugins.DataErasure": { "slug": null, "deprecated": false }, - "BTCPayServer.Plugins.DynamicRateLimits": { "slug": null, "deprecated": false }, - "BTCPayServer.Plugins.DynamicReports": { "slug": null, "deprecated": false }, - "BTCPayServer.Plugins.Electrum": { "slug": null, "deprecated": false }, - "BTCPayServer.Plugins.FileSeller": { "slug": null, "deprecated": false }, - "BTCPayServer.Plugins.FixedFloat": { "slug": null, "deprecated": false }, - "BTCPayServer.Plugins.LNURLVerify": { "slug": null, "deprecated": false }, - "BTCPayServer.Plugins.LiquidPlus": { "slug": null, "deprecated": true }, - "BTCPayServer.Plugins.MCP": { "slug": null, "deprecated": false }, + "BTCPayServer.Plugins.BitcoinSwitch": { "slug": "bitcoinswitch", "deprecated": false }, + "BTCPayServer.Plugins.BitcoinWhitepaper": { "slug": "bitcoin-whitepaper", "deprecated": false }, + "BTCPayServer.Plugins.Blink": { "slug": "blink", "deprecated": false }, + "BTCPayServer.Plugins.Breez": { "slug": "breez", "deprecated": true }, + "BTCPayServer.Plugins.Bringin": { "slug": "bringin", "deprecated": false }, + "BTCPayServer.Plugins.Conference": { "slug": "conferences", "deprecated": false }, + "BTCPayServer.Plugins.DataErasure": { "slug": "data-erasure", "deprecated": false }, + "BTCPayServer.Plugins.DynamicRateLimits": { "slug": "dynamic-rate-limiter", "deprecated": false }, + "BTCPayServer.Plugins.DynamicReports": { "slug": "dynamic-reports", "deprecated": false }, + "BTCPayServer.Plugins.Electrum": { "slug": "electrum", "deprecated": false }, + "BTCPayServer.Plugins.FileSeller": { "slug": "file-seller", "deprecated": false }, + "BTCPayServer.Plugins.FixedFloat": { "slug": "fixed-float", "deprecated": false }, + "BTCPayServer.Plugins.LNURLVerify": { "slug": "lnurl", "deprecated": false }, + "BTCPayServer.Plugins.LiquidPlus": { "slug": "liquid-plus", "deprecated": true }, + "BTCPayServer.Plugins.MCP": { "slug": "mcp-server", "deprecated": false }, "BTCPayServer.Plugins.MicroNode": { "slug": null, "deprecated": false }, - "BTCPayServer.Plugins.NIP05": { "slug": null, "deprecated": false, "tagPrefix": "Nostr" }, - "BTCPayServer.Plugins.Prism": { "slug": null, "deprecated": false }, - "BTCPayServer.Plugins.ServerMessage": { "slug": null, "deprecated": false }, - "BTCPayServer.Plugins.SideShift": { "slug": null, "deprecated": false }, - "BTCPayServer.Plugins.Stripe": { "slug": null, "deprecated": false }, - "BTCPayServer.Plugins.Subscriptions": { "slug": null, "deprecated": false }, + "BTCPayServer.Plugins.NIP05": { "slug": "nip5", "deprecated": false, "tagPrefix": "Nostr" }, + "BTCPayServer.Plugins.Prism": { "slug": "prism", "deprecated": false }, + "BTCPayServer.Plugins.ServerMessage": { "slug": "server-message", "deprecated": false }, + "BTCPayServer.Plugins.SideShift": { "slug": "sideshift", "deprecated": false }, + "BTCPayServer.Plugins.Stripe": { "slug": "stripe-payments", "deprecated": false }, + "BTCPayServer.Plugins.Subscriptions": { "slug": "subscriptions", "deprecated": true }, "BTCPayServer.Plugins.SwapMiddleware": { "slug": null, "deprecated": false }, - "BTCPayServer.Plugins.Terminal": { "slug": null, "deprecated": false }, - "BTCPayServer.Plugins.TicketTailor": { "slug": null, "deprecated": false }, - "BTCPayServer.Plugins.Wabisabi": { "slug": null, "deprecated": true } + "BTCPayServer.Plugins.Terminal": { "slug": "terminal", "deprecated": false }, + "BTCPayServer.Plugins.TicketTailor": { "slug": "ticket-tailor", "deprecated": false }, + "BTCPayServer.Plugins.Wabisabi": { "slug": "coinjoin", "deprecated": true } } diff --git a/submodules/btcpayserver b/submodules/btcpayserver index 03345c28..fbf761f3 160000 --- a/submodules/btcpayserver +++ b/submodules/btcpayserver @@ -1 +1 @@ -Subproject commit 03345c2886a58ea4d2f603cb6554b2e3f5d690a4 +Subproject commit fbf761f36a015b76c197889f174f124c348cd097