Allow BTCPay Server to start without any on-chain payment method
What changed, and why it matters
This commit lets BTCPay Server start without any Bitcoin-like blockchain payment method enabled. It adds a new --nodefaultchain option and updates many parts of the code so the app no longer assumes Bitcoin is always available. Most changes are defensive null checks and UI message adjustments, but the broad scope means there is a risk of missed edge cases where the code still expects a default network and could crash or behave unexpectedly.
Treat this as a feature change with hardening. Review that all code paths consuming DefaultNetwork or GetNetwork<T> handle the null case, especially in background services, API controllers, and third-party plugins. Run integration tests with --nodefaultchain enabled and verify no unhandled InvalidOperationException or NullReferenceException occurs.
Security signals we found
Broad nullability propagation after making a core dependency optional
Defensive checks added in wallet/PSBT controllers to return NotFound when network is missing
StatusMessageModel null-reference guard added
New configuration option changes runtime availability of a core payment method
Evidence from the diff
The patch removes the hard-coded BTC default from SelectedChains when –nodefaultchain is set, makes BTCPayNetworkProvider.DefaultNetwork nullable, and propagates that nullability through controllers, view components, model binders, and hosted services. It also fixes a null-reference issue in StatusMessageModel and updates several status messages to be localized. The change is a feature/refactoring patch with defensive hardening rather than a targeted security fix.
Changed components
BTCPayNetworkProviderSelectedChains configurationDefaultConfiguration CLI optionsUIStoresController dashboard and settingsUIInvoiceControllerUIWalletsController and PSBT handlingUIPaymentRequestControllerUIPullPaymentControllerGreenfieldPullPaymentControllerPullPaymentHostedServiceDerivationSchemeModelBinderStoreRecentInvoices and StoreWalletBalance view componentsStatusMessageModelInspect captured patch +139 / −74
diff --git a/BTCPayServer.Abstractions/Models/StatusMessageModel.cs b/BTCPayServer.Abstractions/Models/StatusMessageModel.cs
index 335d62e..b1d66cd 100644
--- a/BTCPayServer.Abstractions/Models/StatusMessageModel.cs
+++ b/BTCPayServer.Abstractions/Models/StatusMessageModel.cs
@@ -19,7 +19,7 @@ namespace BTCPayServer.Abstractions.Models
{
set
{
- Message = value.Value;
+ Message = value?.Value;
}
}
@@ -29,9 +29,16 @@ namespace BTCPayServer.Abstractions.Models
{
set
{
- StringWriter w = new();
- value.WriteTo(w, HtmlEncoder.Default);
- Html = w.ToString();
+ if (value is null)
+ {
+ Html = null;
+ }
+ else
+ {
+ StringWriter w = new();
+ value.WriteTo(w, HtmlEncoder.Default);
+ Html = w.ToString();
+ }
}
}
public StatusSeverity Severity { get; set; }
diff --git a/BTCPayServer.Common/BTCPayNetworkProvider.cs b/BTCPayServer.Common/BTCPayNetworkProvider.cs
index 7f6e021..7134774 100644
--- a/BTCPayServer.Common/BTCPayNetworkProvider.cs
+++ b/BTCPayServer.Common/BTCPayNetworkProvider.cs
@@ -1,7 +1,8 @@
+#nullable enable
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
-using BTCPayServer.Configuration;
using BTCPayServer.Logging;
using Microsoft.Extensions.Logging;
using NBitcoin;
@@ -9,28 +10,20 @@ using NBXplorer;
namespace BTCPayServer
{
- public partial class BTCPayNetworkProvider
+ public class BTCPayNetworkProvider
{
protected readonly Dictionary<string, BTCPayNetworkBase> _Networks = new Dictionary<string, BTCPayNetworkBase>();
- private readonly NBXplorerNetworkProvider _NBXplorerNetworkProvider;
- public NBXplorerNetworkProvider NBXplorerNetworkProvider
- {
- get
- {
- return _NBXplorerNetworkProvider;
- }
- }
+ public NBXplorerNetworkProvider NBXplorerNetworkProvider { get; }
public ChainName NetworkType { get; private set; }
public BTCPayNetworkProvider(
IEnumerable<BTCPayNetworkBase> networks,
- SelectedChains selectedChains,
NBXplorerNetworkProvider nbxplorerNetworkProvider,
Logs logs)
{
var networksList = networks.ToList();
- _NBXplorerNetworkProvider = nbxplorerNetworkProvider;
+ NBXplorerNetworkProvider = nbxplorerNetworkProvider;
NetworkType = nbxplorerNetworkProvider.NetworkType;
foreach (var network in networksList)
{
@@ -40,8 +33,12 @@ namespace BTCPayServer
logs.Configuration.LogInformation("Supported chains: {Chains}", string.Join(',', _Networks.Select(n => n.Key).ToArray()));
}
- public BTCPayNetwork BTC => GetNetwork<BTCPayNetwork>("BTC");
- public BTCPayNetworkBase DefaultNetwork => BTC ?? GetAll().First();
+ public BTCPayNetwork BTC => GetNetwork<BTCPayNetwork>("BTC") ?? throw new InvalidOperationException("BTC network is required");
+ public BTCPayNetworkBase? DefaultNetwork => GetNetwork<BTCPayNetwork>("BTC") ?? GetAll().FirstOrDefault();
+ /// <summary>
+ /// Returns the default network crypto code (BTC) or NONE if no default network is set
+ /// </summary>
+ public string DefaultCryptoCode => DefaultNetwork?.CryptoCode ?? "NONE";
public IEnumerable<BTCPayNetworkBase> GetAll()
{
return _Networks.Values.ToArray();
@@ -51,21 +48,21 @@ namespace BTCPayServer
{
return _Networks.ContainsKey(cryptoCode.ToUpperInvariant());
}
- public BTCPayNetworkBase GetNetwork(string cryptoCode)
+ public BTCPayNetworkBase? GetNetwork(string cryptoCode)
{
return GetNetwork<BTCPayNetworkBase>(cryptoCode.ToUpperInvariant());
}
- public T GetNetwork<T>(string cryptoCode) where T : BTCPayNetworkBase
+ public T? GetNetwork<T>(string cryptoCode) where T : BTCPayNetworkBase
{
ArgumentNullException.ThrowIfNull(cryptoCode);
- if (!_Networks.TryGetValue(cryptoCode.ToUpperInvariant(), out BTCPayNetworkBase network))
+ if (!_Networks.TryGetValue(cryptoCode.ToUpperInvariant(), out var network))
{
if (cryptoCode == "XBT")
return GetNetwork<T>("BTC");
}
return network as T;
}
- public bool TryGetNetwork<T>(string cryptoCode, out T network) where T : BTCPayNetworkBase
+ public bool TryGetNetwork<T>(string cryptoCode, [MaybeNullWhen(false)] out T network) where T : BTCPayNetworkBase
{
network = GetNetwork<T>(cryptoCode);
return network != null;
diff --git a/BTCPayServer.Common/SelectedChains.cs b/BTCPayServer.Common/SelectedChains.cs
index 3867a1a..1ec4231 100644
--- a/BTCPayServer.Common/SelectedChains.cs
+++ b/BTCPayServer.Common/SelectedChains.cs
@@ -13,7 +13,7 @@ namespace BTCPayServer
bool all = false;
public SelectedChains(IConfiguration configuration)
{
- foreach (var chain in (configuration["chains"] ?? "btc")
+ foreach (var chain in (configuration["chains"] ?? string.Empty)
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(t => t.ToUpperInvariant()))
{
@@ -24,12 +24,24 @@ namespace BTCPayServer
}
chains.Add(chain);
}
- if (chains.Count == 0)
+ if (chains.Count == 0 && !(GetBool(configuration["nodefaultchain"]) ?? false))
chains.Add("BTC");
if (all)
chains.Clear();
}
+ private bool? GetBool(string val)
+ {
+ if (string.IsNullOrWhiteSpace(val))
+ return null;
+ return val.ToLowerInvariant() switch
+ {
+ "1" or "true" => true,
+ "0" or "false" => false,
+ _ => (bool?)null
+ };
+ }
+
public bool Contains(string cryptoCode)
{
return all || chains.Contains(cryptoCode);
diff --git a/BTCPayServer/Components/StoreRecentInvoices/Default.cshtml b/BTCPayServer/Components/StoreRecentInvoices/Default.cshtml
index 46ba89a..63d3f2d 100644
--- a/BTCPayServer/Components/StoreRecentInvoices/Default.cshtml
+++ b/BTCPayServer/Components/StoreRecentInvoices/Default.cshtml
@@ -20,7 +20,7 @@
</div>
<script>
(async () => {
- const url = @Safe.Json(Url.Action("RecentInvoices", "UIStores", new { storeId = Model.StoreId, cryptoCode = Model.CryptoCode }));
+ const url = @Safe.Json(Url.Action("RecentInvoices", "UIStores", new { storeId = Model.StoreId }));
const storeId = @Safe.Json(Model.StoreId);
const response = await fetch(url);
if (response.ok) {
diff --git a/BTCPayServer/Components/StoreRecentInvoices/StoreRecentInvoices.cs b/BTCPayServer/Components/StoreRecentInvoices/StoreRecentInvoices.cs
index 6d01c23..bf2b520 100644
--- a/BTCPayServer/Components/StoreRecentInvoices/StoreRecentInvoices.cs
+++ b/BTCPayServer/Components/StoreRecentInvoices/StoreRecentInvoices.cs
@@ -32,12 +32,11 @@ public class StoreRecentInvoices : ViewComponent
_dbContextFactory = dbContextFactory;
}
- public async Task<IViewComponentResult> InvokeAsync(StoreData store, string cryptoCode, bool initialRendering)
+ public async Task<IViewComponentResult> InvokeAsync(StoreData store, bool initialRendering)
{
var vm = new StoreRecentInvoicesViewModel
{
StoreId = store.Id,
- CryptoCode = cryptoCode,
InitialRendering = initialRendering
};
@@ -58,11 +57,11 @@ public class StoreRecentInvoices : ViewComponent
let state = invoice.GetInvoiceState()
select new StoreRecentInvoiceViewModel
{
- Date = invoice.InvoiceTime,
- Status = state,
- HasRefund = invoice.Refunds.Any(),
+ Date = invoice.InvoiceTime,
+ Status = state,
+ HasRefund = invoice.Refunds.Any(),
InvoiceId = invoice.Id,
- OrderId = invoice.Metadata.OrderId ?? string.Empty,
+ OrderId = invoice.Metadata.OrderId ?? string.Empty,
Amount = invoice.Price,
Currency = invoice.Currency,
Details = new InvoiceDetailsModel
diff --git a/BTCPayServer/Components/StoreWalletBalance/StoreWalletBalance.cs b/BTCPayServer/Components/StoreWalletBalance/StoreWalletBalance.cs
index 7106989..b27cae1 100644
--- a/BTCPayServer/Components/StoreWalletBalance/StoreWalletBalance.cs
+++ b/BTCPayServer/Components/StoreWalletBalance/StoreWalletBalance.cs
@@ -40,11 +40,8 @@ public class StoreWalletBalance : ViewComponent
_handlers = handlers;
}
- public async Task<IViewComponentResult> InvokeAsync(StoreData store, string? cryptoCode = null)
+ public async Task<IViewComponentResult> InvokeAsync(StoreData store, string cryptoCode)
{
- cryptoCode = string.IsNullOrEmpty(cryptoCode)
- ? _networkProvider.DefaultNetwork.CryptoCode
- : cryptoCode;
var walletId = new WalletId(store.Id, cryptoCode);
var data = await _walletHistogramService.GetHistogram(store, walletId, DefaultType);
var defaultCurrency = store.GetStoreBlob().DefaultCurrency;
diff --git a/BTCPayServer/Configuration/DefaultConfiguration.cs b/BTCPayServer/Configuration/DefaultConfiguration.cs
index e1c7244..7b4682d 100644
--- a/BTCPayServer/Configuration/DefaultConfiguration.cs
+++ b/BTCPayServer/Configuration/DefaultConfiguration.cs
@@ -34,7 +34,8 @@ namespace BTCPayServer.Configuration
app.Option("--testnet | -testnet", $"Use testnet (deprecated, use --network instead)", CommandOptionType.BoolValue);
app.Option("--regtest | -regtest", $"Use regtest (deprecated, use --network instead)", CommandOptionType.BoolValue);
app.Option("--signet | -signet", $"Use signet (deprecated, use --network instead)", CommandOptionType.BoolValue);
- app.Option("--chains | -c", $"Chains to support as a comma separated (default: btc; available: {chains})", CommandOptionType.SingleValue);
+ app.Option("--chains | -c", $"Chains to support as a comma separated. Default to empty if --nodefaultchain is set (default: btc; available: {chains})", CommandOptionType.SingleValue);
+ app.Option("--nodefaultchain | -nodefaultchain", "Allow BTCPay to start without any chain enabled (default: false)", CommandOptionType.BoolValue);
app.Option("--postgres", $"Connection string to a PostgreSQL database", CommandOptionType.SingleValue);
app.Option("--nocsp", $"Disable CSP (default false)", CommandOptionType.BoolValue);
app.Option("--deprecated", $"Allow deprecated settings (default:false)", CommandOptionType.BoolValue);
diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldPullPaymentController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldPullPaymentController.cs
index c9e49dc..799aa41 100644
--- a/BTCPayServer/Controllers/GreenField/GreenfieldPullPaymentController.cs
+++ b/BTCPayServer/Controllers/GreenField/GreenfieldPullPaymentController.cs
@@ -375,14 +375,14 @@ retry:
public async Task<IActionResult> GetPullPaymentLNURL(string pullPaymentId)
{
var pp = await _pullPaymentService.GetPullPayment(pullPaymentId, false);
- if (pp is null)
+ if (pp is null || _networkProvider.DefaultNetwork?.CryptoCode is not {} cryptoCode)
return PullPaymentNotFound();
if (_pullPaymentService.SupportsLNURL(pp))
{
var lnurlEndpoint = new Uri(Url.Action("GetLNURLForPullPayment", "UILNURL", new
{
- cryptoCode = _networkProvider.DefaultNetwork.CryptoCode,
+ cryptoCode,
pullPaymentId
}, Request.Scheme, Request.Host.ToString())!);
@@ -446,7 +446,7 @@ retry:
ModelState.AddModelError(nameof(request.Destination), destination.error ?? "The destination is invalid for the payment specified");
return this.CreateValidationError(ModelState);
}
-
+
var amt = ClaimRequest.GetClaimedAmount(destination.destination, request.Amount, payoutHandler.Currency, pp.Currency);
if (amt is ClaimRequest.ClaimedAmountResult.Error err)
{
diff --git a/BTCPayServer/Controllers/UIAppsController.cs b/BTCPayServer/Controllers/UIAppsController.cs
index d753548..781f729 100644
--- a/BTCPayServer/Controllers/UIAppsController.cs
+++ b/BTCPayServer/Controllers/UIAppsController.cs
@@ -15,6 +15,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Localization;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Extensions.Localization;
@@ -32,6 +33,7 @@ namespace BTCPayServer.Controllers
IFileService fileService,
AppService appService,
IStringLocalizer stringLocalizer,
+ ViewLocalizer viewLocalizer,
IHtmlHelper html)
{
_userManager = userManager;
@@ -42,6 +44,7 @@ namespace BTCPayServer.Controllers
_appService = appService;
Html = html;
StringLocalizer = stringLocalizer;
+ ViewLocalizer = viewLocalizer;
}
private readonly UserManager<ApplicationUser> _userManager;
@@ -54,6 +57,7 @@ namespace BTCPayServer.Controllers
public string CreatedAppId { get; set; }
public IHtmlHelper Html { get; }
public IStringLocalizer StringLocalizer { get; }
+ public ViewLocalizer ViewLocalizer { get; }
public class AppUpdated
{
@@ -146,10 +150,16 @@ namespace BTCPayServer.Controllers
}
if (!store.AnyPaymentMethodAvailable(_handlers))
{
+ object text = _networkProvider.DefaultNetwork?.CryptoCode switch
+ {
+ null => StringLocalizer["To create a {0} app, you need to set up a wallet first", vm.AppType],
+ {} cryptoCode => ViewLocalizer["To create a {0} app, you need to <a href='{1}' class='alert-link'>set up a wallet</a> first", vm.AppType, Url.Action(nameof(UIStoresController.SetupWallet), "UIStores", new { cryptoCode, storeId })!]
+ };
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Error,
- Html = $"To create a {vm.AppType} app, you need to <a href='{Url.Action(nameof(UIStoresController.SetupWallet), "UIStores", new { cryptoCode = _networkProvider.DefaultNetwork.CryptoCode, storeId })}' class='alert-link'>set up a wallet</a> first",
+ LocalizedHtml = text as LocalizedHtmlString,
+ LocalizedMessage = text as LocalizedString,
AllowDismiss = false
});
return View(vm);
diff --git a/BTCPayServer/Controllers/UIInvoiceController.UI.cs b/BTCPayServer/Controllers/UIInvoiceController.UI.cs
index f188f39..e66a517 100644
--- a/BTCPayServer/Controllers/UIInvoiceController.UI.cs
+++ b/BTCPayServer/Controllers/UIInvoiceController.UI.cs
@@ -28,9 +28,11 @@ using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Localization;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Routing;
using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Localization;
using NBitcoin;
using NBXplorer;
using Newtonsoft.Json.Linq;
@@ -650,8 +652,8 @@ namespace BTCPayServer.Controllers
break;
case "cpfp" when storeId is not null:
var network = _NetworkProvider.DefaultNetwork;
- var explorer = _ExplorerClients.GetExplorerClient(network);
- if (explorer is null)
+ var explorer = network is null ? null : _ExplorerClients.GetExplorerClient(network);
+ if (explorer is null || network is null)
return NotSupported(StringLocalizer["This feature is only available to BTC wallets"]);
if (!GetCurrentStore().HasPermission(GetUserId(), Policies.CanModifyStoreSettings))
return Forbid();
@@ -912,8 +914,8 @@ namespace BTCPayServer.Controllers
PaymentMethodName = _prettyName.PrettyName(kv.PaymentMethodId, true),
Order = kv.PaymentMethodId switch
{
- _ when PaymentTypes.CHAIN.GetPaymentMethodId(_NetworkProvider.DefaultNetwork.CryptoCode) == kv.PaymentMethodId => 0,
- _ when PaymentTypes.LN.GetPaymentMethodId(_NetworkProvider.DefaultNetwork.CryptoCode) == kv.PaymentMethodId => 1,
+ _ when PaymentTypes.CHAIN.GetPaymentMethodId(_NetworkProvider.DefaultCryptoCode) == kv.PaymentMethodId => 0,
+ _ when PaymentTypes.LN.GetPaymentMethodId(_NetworkProvider.DefaultCryptoCode) == kv.PaymentMethodId => 1,
_ when handler is ILightningPaymentHandler => 2,
_ => 3
}
@@ -1316,10 +1318,16 @@ namespace BTCPayServer.Controllers
private IActionResult NoPaymentMethodResult(string storeId)
{
+ object text = _NetworkProvider.DefaultNetwork?.CryptoCode switch
+ {
+ null => StringLocalizer["To create an invoice, you need to setup a wallet first"],
+ {} cryptoCode => ViewLocalizer["To create an invoice, you need to <a href='{0}'>setup a wallet</a> first", Url.Action(nameof(UIStoresController.SetupWallet), "UIStores", new { cryptoCode, storeId })!]
+ };
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Error,
- Html = $"To create an invoice, you need to <a href='{Url.Action(nameof(UIStoresController.SetupWallet), "UIStores", new { cryptoCode = _NetworkProvider.DefaultNetwork.CryptoCode, storeId })}' class='alert-link'>set up a wallet</a> first",
+ LocalizedHtml = text as LocalizedHtmlString,
+ LocalizedMessage = text as LocalizedString,
AllowDismiss = false
});
return RedirectToAction(nameof(ListInvoices), new { storeId });
diff --git a/BTCPayServer/Controllers/UIInvoiceController.cs b/BTCPayServer/Controllers/UIInvoiceController.cs
index fee930c..3781e05 100644
--- a/BTCPayServer/Controllers/UIInvoiceController.cs
+++ b/BTCPayServer/Controllers/UIInvoiceController.cs
@@ -31,6 +31,7 @@ using Newtonsoft.Json.Linq;
using StoreData = BTCPayServer.Data.StoreData;
using BTCPayServer.Payouts;
using BTCPayServer.Plugins.Webhooks;
+using Microsoft.AspNetCore.Mvc.Localization;
using Microsoft.Extensions.Localization;
namespace BTCPayServer.Controllers
@@ -67,6 +68,7 @@ namespace BTCPayServer.Controllers
public WebhookSender WebhookNotificationManager { get; }
public IEnumerable<IGlobalCheckoutModelExtension> GlobalCheckoutModelExtensions { get; }
public IStringLocalizer StringLocalizer { get; }
+ public ViewLocalizer ViewLocalizer { get; }
public UIInvoiceController(
InvoiceRepository invoiceRepository,
@@ -96,6 +98,7 @@ namespace BTCPayServer.Controllers
Dictionary<PaymentMethodId, ICheckoutModelExtension> paymentModelExtensions,
IEnumerable<IGlobalCheckoutModelExtension> globalCheckoutModelExtensions,
IStringLocalizer stringLocalizer,
+ ViewLocalizer viewLocalizer,
PrettyNameProvider prettyName)
{
_displayFormatter = displayFormatter;
@@ -126,6 +129,7 @@ namespace BTCPayServer.Controllers
_defaultRules = defaultRules;
_appService = appService;
StringLocalizer = stringLocalizer;
+ ViewLocalizer = viewLocalizer;
}
internal async Task<InvoiceEntity> CreatePaymentRequestInvoice(Data.PaymentRequestData prData, decimal? amount, decimal amountDue, StoreData storeData, HttpRequest request, CancellationToken cancellationToken)
diff --git a/BTCPayServer/Controllers/UIPaymentRequestController.cs b/BTCPayServer/Controllers/UIPaymentRequestController.cs
index 487c0d2..83caf32 100644
--- a/BTCPayServer/Controllers/UIPaymentRequestController.cs
+++ b/BTCPayServer/Controllers/UIPaymentRequestController.cs
@@ -26,6 +26,7 @@ using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Localization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Localization;
using PaymentRequestData = BTCPayServer.Data.PaymentRequestData;
@@ -55,6 +56,7 @@ namespace BTCPayServer.Controllers
private FormComponentProviders FormProviders { get; }
public FormDataService FormDataService { get; }
public IStringLocalizer StringLocalizer { get; }
+ public ViewLocalizer ViewLocalizer { get; }
public UIPaymentRequestController(
UIInvoiceController invoiceController,
@@ -70,6 +72,7 @@ namespace BTCPayServer.Controllers
FormComponentProviders formProviders,
FormDataService formDataService,
IStringLocalizer stringLocalizer,
+ ViewLocalizer viewLocalizer,
ApplicationDbContextFactory dbContextFactory,
BTCPayNetworkProvider networkProvider,
StoreLabelRepository storeLabelRepository)
@@ -88,6 +91,7 @@ namespace BTCPayServer.Controllers
FormProviders = formProviders;
FormDataService = formDataService;
StringLocalizer = stringLocalizer;
+ ViewLocalizer = viewLocalizer;
_networkProvider = networkProvider;
_storeLabelRepository = storeLabelRepository;
}
@@ -677,11 +681,16 @@ namespace BTCPayServer.Controllers
private IActionResult NoPaymentMethodResult(string storeId)
{
+ object text = _networkProvider.DefaultNetwork?.CryptoCode switch
+ {
+ null => StringLocalizer["To create a payment request, you need to set up a wallet first"],
+ {} cryptoCode => ViewLocalizer["To create a payment request, you need to <a href='{0}'>setup a wallet</a> first", Url.Action(nameof(UIStoresController.SetupWallet), "UIStores", new { cryptoCode, storeId })!]
+ };
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Error,
- Html =
- $"To create a payment request, you need to <a href='{Url.Action(nameof(UIStoresController.SetupWallet), "UIStores", new { cryptoCode = _networkProvider.DefaultNetwork.CryptoCode, storeId })}' class='alert-link'>set up a wallet</a> first",
+ LocalizedHtml = text as LocalizedHtmlString,
+ LocalizedMessage = text as LocalizedString,
AllowDismiss = false
});
return RedirectToAction(nameof(GetPaymentRequests), new { storeId });
diff --git a/BTCPayServer/Controllers/UIPullPaymentController.cs b/BTCPayServer/Controllers/UIPullPaymentController.cs
index b08a63a..0faf2e4 100644
--- a/BTCPayServer/Controllers/UIPullPaymentController.cs
+++ b/BTCPayServer/Controllers/UIPullPaymentController.cs
@@ -72,7 +72,7 @@ namespace BTCPayServer.Controllers
{
using var ctx = _dbContextFactory.CreateContext();
var pp = await ctx.PullPayments.FindAsync(pullPaymentId);
- if (pp is null)
+ if (pp is null || _networkProvider.DefaultNetwork?.CryptoCode is not {} cryptoCode)
return NotFound();
var store = await _storeRepository.FindStore(pp.StoreId);
@@ -116,10 +116,10 @@ namespace BTCPayServer.Controllers
};
vm.IsPending &= vm.AmountDue > 0.0m;
vm.StoreBranding = await StoreBrandingViewModel.CreateAsync(Request, _uriResolver, storeBlob);
-
+
if (_pullPaymentHostedService.SupportsLNURL(pp))
{
- var url = Url.Action(nameof(UILNURLController.GetLNURLForPullPayment), "UILNURL", new { cryptoCode = _networkProvider.DefaultNetwork.CryptoCode, pullPaymentId = vm.Id }, Request.Scheme, Request.Host.ToString());
+ var url = Url.Action(nameof(UILNURLController.GetLNURLForPullPayment), "UILNURL", new { cryptoCode, pullPaymentId = vm.Id }, Request.Scheme, Request.Host.ToString());
vm.LnurlEndpoint = url != null ? new Uri(url) : null;
vm.SetupDeepLink = $"boltcard://program?url={GetBoltcardDeeplinkUrl(vm, OnExistingBehavior.UpdateVersion)}";
vm.ResetDeepLink = $"boltcard://reset?url={GetBoltcardDeeplinkUrl(vm, OnExistingBehavior.KeepVersion)}";
diff --git a/BTCPayServer/Controllers/UIStoresController.Dashboard.cs b/BTCPayServer/Controllers/UIStoresController.Dashboard.cs
index 207ff49..064d910 100644
--- a/BTCPayServer/Controllers/UIStoresController.Dashboard.cs
+++ b/BTCPayServer/Controllers/UIStoresController.Dashboard.cs
@@ -26,7 +26,7 @@ public partial class UIStoresController
var store = CurrentStore;
if (store is null)
return NotFound();
-
+
var storeBlob = store.GetStoreBlob();
AddPaymentMethods(store, storeBlob,
@@ -34,7 +34,7 @@ public partial class UIStoresController
var walletEnabled = derivationSchemes.Any(scheme => !string.IsNullOrEmpty(scheme.Value) && scheme.Enabled);
var lightningEnabled = lightningNodes.Any(ln => !string.IsNullOrEmpty(ln.Address) && ln.Enabled);
- var cryptoCode = _networkProvider.DefaultNetwork.CryptoCode;
+ var cryptoCode = _networkProvider.DefaultCryptoCode;
var vm = new StoreDashboardViewModel
{
WalletEnabled = walletEnabled,
@@ -46,7 +46,7 @@ public partial class UIStoresController
Network = _networkProvider.DefaultNetwork,
IsSetUp = walletEnabled || lightningEnabled,
EnabledWalletCryptos = derivationSchemes
- .Where(scheme => scheme.Enabled && scheme.WalletSupported)
+ .Where(scheme => scheme is { Enabled: true, WalletSupported: true })
.Select(scheme => scheme.Crypto)
.Distinct()
.ToList()
@@ -100,13 +100,13 @@ public partial class UIStoresController
: NotFound();
}
- [HttpGet("{storeId}/dashboard/{cryptoCode}/recent-invoices")]
+ [HttpGet("{storeId}/dashboard/recent-invoices")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
- public IActionResult RecentInvoices(string storeId, string cryptoCode)
+ public IActionResult RecentInvoices(string storeId)
{
var store = HttpContext.GetStoreData();
return store != null
- ? ViewComponent("StoreRecentInvoices", new { Store = store, CryptoCode = cryptoCode })
+ ? ViewComponent("StoreRecentInvoices", new { Store = store })
: NotFound();
}
diff --git a/BTCPayServer/Controllers/UIStoresController.Settings.cs b/BTCPayServer/Controllers/UIStoresController.Settings.cs
index f01ff9f..1873a00 100644
--- a/BTCPayServer/Controllers/UIStoresController.Settings.cs
+++ b/BTCPayServer/Controllers/UIStoresController.Settings.cs
@@ -417,8 +417,8 @@ public partial class UIStoresController
var defaultChoice = defaultPaymentId?.FindNearest(enabled);
if (defaultChoice is null)
{
- defaultChoice = enabled.FirstOrDefault(e => e == PaymentTypes.CHAIN.GetPaymentMethodId(_networkProvider.DefaultNetwork.CryptoCode)) ??
- enabled.FirstOrDefault(e => e == PaymentTypes.LN.GetPaymentMethodId(_networkProvider.DefaultNetwork.CryptoCode)) ??
+ defaultChoice = enabled.FirstOrDefault(e => e == PaymentTypes.CHAIN.GetPaymentMethodId(_networkProvider.DefaultCryptoCode)) ??
+ enabled.FirstOrDefault(e => e == PaymentTypes.LN.GetPaymentMethodId(_networkProvider.DefaultCryptoCode)) ??
enabled.FirstOrDefault();
}
var choices = GetEnabledPaymentMethodChoices(storeData);
diff --git a/BTCPayServer/Controllers/UIWalletsController.PSBT.cs b/BTCPayServer/Controllers/UIWalletsController.PSBT.cs
index 463d79d..82de020 100644
--- a/BTCPayServer/Controllers/UIWalletsController.PSBT.cs
+++ b/BTCPayServer/Controllers/UIWalletsController.PSBT.cs
@@ -75,7 +75,7 @@ namespace BTCPayServer.Controllers
WalletId walletId, WalletPSBTViewModel vm, string command = null)
{
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
- var psbt = await vm.GetPSBT(network.NBitcoinNetwork, ModelState);
+ var psbt = await vm.GetPSBT(network?.NBitcoinNetwork, ModelState);
if (psbt is null || vm.InvalidPSBT)
{
@@ -130,6 +130,8 @@ namespace BTCPayServer.Controllers
WalletId walletId, string returnUrl)
{
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
+ if (network is null)
+ return NotFound();
var referer = HttpContext.Request.GetTypedHeaders().Referer?.AbsolutePath;
var vm = new WalletPSBTViewModel
{
@@ -152,6 +154,8 @@ namespace BTCPayServer.Controllers
WalletPSBTViewModel vm, string command)
{
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
+ if (network is null)
+ return NotFound();
vm.CryptoCode = network.CryptoCode;
var derivationSchemeSettings = GetDerivationSchemeSettings(walletId);
@@ -441,6 +445,8 @@ namespace BTCPayServer.Controllers
WalletId walletId, WalletPSBTViewModel vm, string command, CancellationToken cancellationToken = default)
{
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
+ if (network is null)
+ return NotFound();
PSBT psbt = await vm.GetPSBT(network.NBitcoinNetwork, ModelState);
if (vm.InvalidPSBT || psbt is null)
{
@@ -608,12 +614,12 @@ namespace BTCPayServer.Controllers
WalletId walletId, WalletPSBTCombineViewModel vm)
{
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
- var psbt = await vm.GetPSBT(network.NBitcoinNetwork, ModelState);
+ var psbt = await vm.GetPSBT(network?.NBitcoinNetwork, ModelState);
if (psbt == null)
{
return View(vm);
}
- var sourcePSBT = vm.GetSourcePSBT(network.NBitcoinNetwork, ModelState);
+ var sourcePSBT = vm.GetSourcePSBT(network?.NBitcoinNetwork, ModelState);
if (sourcePSBT is null)
{
return View(vm);
diff --git a/BTCPayServer/Controllers/UIWalletsController.cs b/BTCPayServer/Controllers/UIWalletsController.cs
index 54e0981..da07d38 100644
--- a/BTCPayServer/Controllers/UIWalletsController.cs
+++ b/BTCPayServer/Controllers/UIWalletsController.cs
@@ -175,7 +175,7 @@ namespace BTCPayServer.Controllers
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
var pendingTransaction =
await _pendingTransactionService.GetPendingTransaction(GetPendingTxId(walletId, pendingTransactionId));
- if (pendingTransaction is null)
+ if (pendingTransaction is null || network is null)
return NotFound();
var blob = pendingTransaction.GetBlob();
if (blob?.PSBT is null)
@@ -1412,7 +1412,10 @@ namespace BTCPayServer.Controllers
{
if (vm.SigningContext.PendingTransactionId is not null)
{
- var psbt = PSBT.Parse(vm.SigningContext.PSBT, NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode).NBitcoinNetwork);
+ var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode)?.NBitcoinNetwork;
+ if (network is null)
+ return NotFound();
+ var psbt = PSBT.Parse(vm.SigningContext.PSBT, network);
var pendingTransaction = await _pendingTransactionService.CollectSignature(GetPendingTxId(walletId, vm.SigningContext.PendingTransactionId), psbt, CancellationToken.None);
if (pendingTransaction != null)
diff --git a/BTCPayServer/HostedServices/PullPaymentHostedService.cs b/BTCPayServer/HostedServices/PullPaymentHostedService.cs
index a013dbe..bb7d52b 100644
--- a/BTCPayServer/HostedServices/PullPaymentHostedService.cs
+++ b/BTCPayServer/HostedServices/PullPaymentHostedService.cs
@@ -430,9 +430,11 @@ namespace BTCPayServer.HostedServices
public bool SupportsLNURL(PullPaymentData pp, PullPaymentBlob blob = null)
{
+ var cryptoCode = _networkProvider.DefaultNetwork?.CryptoCode;
+ if (cryptoCode is null) return false;
blob ??= pp.GetBlob();
var pms = blob.SupportedPayoutMethods.FirstOrDefault(id =>
- PayoutTypes.LN.GetPayoutMethodId(_networkProvider.DefaultNetwork.CryptoCode)
+ PayoutTypes.LN.GetPayoutMethodId(cryptoCode)
== id);
return pms is not null && _lnurlSupportedCurrencies.Contains(pp.Currency);
}
diff --git a/BTCPayServer/ModelBinders/DerivationSchemeModelBinder.cs b/BTCPayServer/ModelBinders/DerivationSchemeModelBinder.cs
index 2aa15bb..5a576b0 100644
--- a/BTCPayServer/ModelBinders/DerivationSchemeModelBinder.cs
+++ b/BTCPayServer/ModelBinders/DerivationSchemeModelBinder.cs
@@ -32,7 +32,9 @@ namespace BTCPayServer.ModelBinders
var networkProvider = (BTCPayNetworkProvider)bindingContext.HttpContext.RequestServices.GetService(typeof(BTCPayNetworkProvider));
var cryptoCode = bindingContext.ValueProvider.GetValue("cryptoCode").FirstValue;
- var network = networkProvider.GetNetwork<BTCPayNetwork>(cryptoCode ?? networkProvider.DefaultNetwork.CryptoCode);
+ var network = networkProvider?.GetNetwork<BTCPayNetwork>(cryptoCode ?? networkProvider.DefaultCryptoCode);
+ if (network == null)
+ return Task.CompletedTask;
try
{
var data = network.NBXplorerNetwork.DerivationStrategyFactory.Parse(key);
diff --git a/BTCPayServer/Models/WalletViewModels/WalletPSBTCombineViewModel.cs b/BTCPayServer/Models/WalletViewModels/WalletPSBTCombineViewModel.cs
index 937d299..305bb64 100644
--- a/BTCPayServer/Models/WalletViewModels/WalletPSBTCombineViewModel.cs
+++ b/BTCPayServer/Models/WalletViewModels/WalletPSBTCombineViewModel.cs
@@ -20,6 +20,8 @@ namespace BTCPayServer.Models.WalletViewModels
public PSBT GetSourcePSBT(Network network, ModelStateDictionary modelState)
{
+ if (network is null)
+ return null;
if (!string.IsNullOrEmpty(OtherPSBT))
{
try
@@ -44,6 +46,8 @@ namespace BTCPayServer.Models.WalletViewModels
}
try
{
+ if (network is null)
+ return null;
return NBitcoin.PSBT.Load(bytes, network);
}
catch (FormatException ex)
@@ -56,6 +60,8 @@ namespace BTCPayServer.Models.WalletViewModels
{
try
{
+ if (network is null)
+ return null;
return NBitcoin.PSBT.Parse(PSBT, network);
}
catch (FormatException ex)
diff --git a/BTCPayServer/Models/WalletViewModels/WalletPSBTViewModel.cs b/BTCPayServer/Models/WalletViewModels/WalletPSBTViewModel.cs
index f52b7f0..bcea95d 100644
--- a/BTCPayServer/Models/WalletViewModels/WalletPSBTViewModel.cs
+++ b/BTCPayServer/Models/WalletViewModels/WalletPSBTViewModel.cs
@@ -36,7 +36,8 @@ namespace BTCPayServer.Models.WalletViewModels
public IFormFile UploadedPSBTFile { get; set; }
- public async Task<PSBT> GetPSBT(Network network, ModelStateDictionary modelState)
+ #nullable enable
+ public async Task<PSBT?> GetPSBT(Network? network, ModelStateDictionary modelState)
{
var psbt = await GetPSBTCore(network, modelState);
if (psbt != null)
@@ -53,7 +54,7 @@ namespace BTCPayServer.Models.WalletViewModels
}
public bool InvalidPSBT { get; set; }
- async Task<PSBT> GetPSBTCore(Network network, ModelStateDictionary modelState)
+ async Task<PSBT?> GetPSBTCore(Network? network, ModelStateDictionary modelState)
{
if (UploadedPSBTFile != null)
{
@@ -67,6 +68,8 @@ namespace BTCPayServer.Models.WalletViewModels
{
await stream.ReadAsync(bytes, 0, (int)UploadedPSBTFile.Length);
}
+ if (network is null)
+ return null;
return NBitcoin.PSBT.Load(bytes, network);
}
catch (Exception ex)
@@ -89,6 +92,8 @@ namespace BTCPayServer.Models.WalletViewModels
try
{
InvalidPSBT = false;
+ if (network is null)
+ return null;
return NBitcoin.PSBT.Parse(PSBT, network);
}
catch (Exception ex) when (!InvalidPSBT)
diff --git a/BTCPayServer/Plugins/PayButton/Views/PayButton.cshtml b/BTCPayServer/Plugins/PayButton/Views/PayButton.cshtml
index a9c0efb..3826db9 100644
--- a/BTCPayServer/Plugins/PayButton/Views/PayButton.cshtml
+++ b/BTCPayServer/Plugins/PayButton/Views/PayButton.cshtml
@@ -122,7 +122,7 @@
window.lnurlEndpoint = @Safe.Json(Url.Action("GetLNUrlForStore", "UILNURL", new
{
storeId = Model.StoreId,
- cryptoCode = NetworkProvider.DefaultNetwork.CryptoCode
+ cryptoCode = NetworkProvider.DefaultCryptoCode
}, "lnurlp", Context.Request.Host.ToString()));
const srvModel = @Safe.Json(Model);
const payButtonCtrl = new Vue({
diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs
index 6420261..0ccf97e 100644
--- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs
+++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs
@@ -372,8 +372,8 @@ namespace BTCPayServer.Services.Invoices
}
if (paymentMethodId is null)
{
- var defaultBTC = PaymentTypes.CHAIN.GetPaymentMethodId(networkProvider.DefaultNetwork.CryptoCode);
- var defaultLNURLPay = PaymentTypes.LNURL.GetPaymentMethodId(networkProvider.DefaultNetwork.CryptoCode);
+ var defaultBTC = PaymentTypes.CHAIN.GetPaymentMethodId(networkProvider.DefaultCryptoCode);
+ var defaultLNURLPay = PaymentTypes.LNURL.GetPaymentMethodId(networkProvider.DefaultCryptoCode);
paymentMethodId = authorized.FirstOrDefault(e => e == defaultBTC) ??
authorized.FirstOrDefault(e => e == defaultLNURLPay) ??
authorized.FirstOrDefault();
diff --git a/BTCPayServer/Views/UIPaymentRequest/EditPaymentRequest.cshtml b/BTCPayServer/Views/UIPaymentRequest/EditPaymentRequest.cshtml
index d883f10..7f5ba2a 100644
--- a/BTCPayServer/Views/UIPaymentRequest/EditPaymentRequest.cshtml
+++ b/BTCPayServer/Views/UIPaymentRequest/EditPaymentRequest.cshtml
@@ -10,13 +10,10 @@
@using BTCPayServer.Services
@inject FormDataService FormDataService
@inject LinkGenerator LinkGenerator
-@inject BTCPayNetworkProvider BTCPayNetworkProvider
@model BTCPayServer.Models.PaymentRequestViewModels.UpdatePaymentRequestViewModel
@{
var checkoutFormOptions = await FormDataService.GetSelect(Model.StoreId, Model.FormId);
ViewData.SetLayoutModel(new LayoutModel("PaymentRequests", string.IsNullOrEmpty(Model.Id) ? StringLocalizer["Create Payment Request"] : StringLocalizer["Edit Payment Request"]));
- var defaultNetwork = BTCPayNetworkProvider.DefaultNetwork;
- var wallet = new WalletId(Model.StoreId, defaultNetwork.CryptoCode);
}
@section PageHeadContent {
diff --git a/BTCPayServer/Views/UIStores/Dashboard.cshtml b/BTCPayServer/Views/UIStores/Dashboard.cshtml
index 5af068b..23b0a8b 100644
--- a/BTCPayServer/Views/UIStores/Dashboard.cshtml
+++ b/BTCPayServer/Views/UIStores/Dashboard.cshtml
@@ -96,7 +96,7 @@
}
</div>
<div class="dashboard-row dashboard-row-tertiary">
- <vc:store-recent-invoices store="store" crypto-code="@Model.CryptoCode" initial-rendering="true" />
+ <vc:store-recent-invoices store="store" initial-rendering="true" />
</div>
@if (Model.LightningEnabled)
{
Why this scored 28/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.