Compare commits

..

46 Commits

Author SHA1 Message Date
b62985faf4 Update changelog 2024-01-24 22:55:23 +09:00
09c761aa31 Fix: Sometimes importing a wallet file from Electrum would fail 2024-01-24 22:53:40 +09:00
8089a938f3 Guest role: Fix redirect after store creation ()
This ensures that guests land on the invoices list, which tehy are allowed to see — rather than the dashboard, which they don't have permissions for.

Fixes .
2024-01-24 11:34:16 +01:00
35b3fef7c5 Fix wallet import ()
* Fix wallet import

* Improve error message for import of wallet file
2024-01-24 17:49:15 +09:00
f31aa43c6a Wallet file parsing: Add Wasabi test case and re-add Electrum distinction ()
* Extend tests, add Wasabi file

* Re-add Electrum distinction

* Specter: Fix indentation

* Cleanups
2024-01-24 09:28:22 +09:00
b03f8db06b Refactor wallet file parsing (Fix: ) () 2024-01-23 21:33:45 +09:00
27e70a169e Do not show warning about browser compatibility to vault on confirm address 2024-01-23 21:30:29 +09:00
6a1d17dda2 Remove ESC as a supporter ()
Closes .
2024-01-22 09:00:10 +09:00
31bc6dd48c More tests on interpolation 2024-01-20 12:21:58 +09:00
6054315d84 Add changelog 1.12.4, bump ()
* Add changelog 1.12.4, bump

* Update Changelog.md

Co-authored-by: d11n <mail@dennisreimann.de>

* Update Changelog.md

---------

Co-authored-by: d11n <mail@dennisreimann.de>
2024-01-19 23:28:01 +09:00
2a7059ddeb Update languages updates from transifex ()
* Update languages

* Update ChatGPT translator script

* Update translations
2024-01-19 21:45:14 +09:00
e2e7e59722 Fix webhook test for payment requests ()
When testing the webhook for payment requests, we were incorrectly creating a payout webhook instead of a payment request. This would cause an error (but nothing fatal as it is only a test webhook(
2024-01-19 21:30:15 +09:00
8b373bda8e bump NBX 2024-01-18 17:21:15 +09:00
d6806dc1f6 Improve checkout page load time by fetching recommended fee in the background periodically () 2024-01-18 17:16:57 +09:00
a753698ae7 Various plugin fixes ()
* Fix: Plugin updates do not work

* Offer install on disabled plugins when different version

This will:
* Clear any previous pending actions of a plugin if you click uninstall
* Show the plugin version that was disabled
* Show an update button on disabled plugins instead of install
* if a plugin is scheduled to be installed/updated, it will show which version was scheduled to be updated. If a newer version if available than the scheduled one, it will show an option to switch to that

* Ensure disabled plugins don't get loaded

* View fixes

---------

Co-authored-by: d11n <mail@dennisreimann.de>
2024-01-18 17:15:16 +09:00
3eec9cb0bb Refactor fee provider ()
* Refactor fee provider

The fee provider ended up glued with a hardcoded factory. This PR:
* removes this glue and uses the DI to register fee provider for a network. (allows plugins to add their own fee providers, for any network
* Add a 10 second timeout to mempoolspace fee fetching as they are slow at times

* use linear interpolation for mempool space fee estimation

* fix upper bound

* Add tests, rollback pluginify FeeProvider

---------

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2024-01-18 15:27:19 +09:00
cd8ef0c1ff Fix: Bitpay's API rate route wasn't backward for some queries () 2024-01-18 14:08:07 +09:00
bd196ad963 fix build 2024-01-18 12:31:59 +09:00
1ad93838c9 Remove reliance on static field 2024-01-18 11:13:32 +09:00
a9252fd741 Fix: Partial Payment shows 'Could not update BTC (LNURL-Pay)' in invoice logs () 2024-01-18 09:57:25 +09:00
376067324b Remove unused variables () 2024-01-18 09:47:39 +09:00
dd7ab2f647 Avoid exception storm when currency provider is initialized () 2024-01-18 09:31:35 +09:00
1d6d146fb2 Revert "Remove unused variables" ()
This reverts commit f070b223552b92b1450a96f893883c488f985cea.
2024-01-18 00:05:50 +09:00
3ae1f13323 Bump libraries 2024-01-17 22:11:30 +09:00
0b0a8f8218 Fix: BTCPay Server fails to start the first time when installing a new plugin () 2024-01-17 19:26:22 +09:00
f070b22355 Remove unused variables 2024-01-17 18:46:28 +09:00
c5a0e28420 Refactor Wallet import code ()
* Refactor Wallet import code

The code for wallet import was incredibly messy as it evolved over time from various requests.

This PR:
* splits up each supported format into its own file
* Supports taproot descriptors (through a hack until NBitcoin supports it internally) fixes 
* Reduces different paths for handling electrum/non-electrum xpubs
* Allows plugins to add their own import support formats for onchain wallets.

* Update NBitcoin to parse tr descriptors

* Fix warnings

* Use dedicated type OnChainWalletParsers

---------

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2024-01-17 18:08:39 +09:00
70e9ea1d5e POS: Fix missing store branding property on form error case ()
When a POS has a form, which results in an error state, the store branding property was not set. This adds the missing property and also does not render the store branding partial, in case the model property isn't present.

Fixes .
2024-01-16 08:55:38 +01:00
89d294524a Checkout v2: Clicking QR code copies full payment URI ()
* Checkout v2: Clicking QR code copies full payment URI

Before it copied only the destination value (Bitcoin address or Lightning BOLT11). This didn't include the BOLT11 in case of the unified QR code. Now it will copy the full payment URI, which is the same as the QR represents:

- Unified: `bitcoin:ADDRESS?amount=AMOUNT&lightning=BOLT11`
- Bitcoin: `bitcoin:ADDRESS?amount=AMOUNT`
- Lightning: `lightning:BOLT11`

Fixes .

* Test fix
2024-01-16 08:54:59 +01:00
5e25ee2996 Checkout v1: Apply custom style ()
Applies the custom CSS in Checkout v1 and prevents that it interferes with the styling of Checkout v2.

Fixes  and fixes .
2024-01-15 13:30:39 +01:00
5935dbf1d1 Store Emails: Fix test email with multiple recipients ()
Fixes .
2024-01-15 13:30:10 +01:00
f7542c988d Prevent payment request to be created when a wallet is not set up ()
* Prevent payment request to be created when a wallet is not set up

* Created an extension method for store wallet checks

* fix for invoice and payment request selenium test

* refactoring payment request controller

* removing unused variable

* Unify behaviour across controllers

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2024-01-11 16:25:56 +01:00
e90414bded Hide LN Balance when using internal node and not server admin ()
* Hide LN Balance when using internal node and not server admin

* Minor updates

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2024-01-06 08:46:19 +01:00
78882dcff0 Propose linking Greenfield API information within the Legacy API view ()
* Propose linking Greenfield API information within the Legacy API view

* Propose linking Greenfield API information within the Legacy API view

* moved Greenfield API section up

* moved Greenfield API section up

* Fix link

* Wording

* Adjust button alignment

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2024-01-03 11:33:54 +01:00
1ac1443070 README.md: .NET is not called "Core" anymore ()
* README.md: .NET is not called "Core" anymore

Ever since version 5.x, the "Core" part of the name was removed.

* README.md: remove unneeded lang setting from URL
2024-01-02 12:29:40 +01:00
b5405e9313 Make tips and discount properties disabled in POS setting ()
* Make tips and discount properties disabled in POS setting

* Update discount and tips boolean properties in model and swagger json

* update pos tests to cater for default tip and discount state

* Remove custom IDs and unify tests

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2023-12-31 09:07:15 +01:00
c7eef01fd5 Removed what's new button and info () 2023-12-28 08:57:18 +01:00
26f61d35bb Bumping LND to 0.17.3-beta () 2023-12-25 00:27:46 -06:00
765776c429 Update .NET version in README.md ()
Update the version of .NET requirement from 6.0 to 8.0 in README.md
2023-12-24 15:40:38 +01:00
9f54074d03 Startup: List configured networks in non-altcoin build warning () 2023-12-22 17:25:04 +09:00
f23078df1c Use buildx for creating and pushing docker images () 2023-12-22 14:23:04 +09:00
a35bf54a02 Changelog and bump 2023-12-22 14:21:12 +09:00
4867698ac9 AppService: Update inventory only for known app types ()
There are apps, which do not have a template and hence no inventory. Accessing it via `settings[templatePath]!.Value` causes exceptions in those cases.
2023-12-22 14:21:01 +09:00
e84e575017 bump 2023-12-22 10:53:54 +09:00
c585a0b276 Webhooks: Fix invoice interpolation ()
* Webhooks: Fix invoice interpolation

Fixes .

* Syntax cleanups
2023-12-22 10:50:08 +09:00
ad89139e07 Plugins: Fix missing uninstall button ()
Fixes .
2023-12-22 10:49:40 +09:00
158 changed files with 1645 additions and 1137 deletions
.circleci
BTCPayServer.Abstractions
BTCPayServer.Client
BTCPayServer.Common
BTCPayServer.Data
BTCPayServer.Rating
BTCPayServer.Tests
BTCPayServer
BTCPayServer.csproj
Components
AppSales
StoreLightningBalance
StoreSelector
Controllers
Data
DerivationSchemeParser.csDerivationSchemeSettings.csExtensions.cs
HostedServices
Hosting
PaymentRequest
Payments
PayoutProcessors
Plugins
Services
Views
wwwroot
Build
Changelog.mdDockerfileREADME.mdarm32v7.Dockerfilearm64v8.Dockerfilebtcpayserver.sln

@ -31,79 +31,23 @@ jobs:
- run:
command: |
curl -X POST -H "Authorization: token $GH_PAT" -H "Accept: application/vnd.github.everest-preview+json" -H "Content-Type: application/json" https://api.github.com/repos/btcpayserver/btcpayserver-doc/dispatches --data '{"event_type": "build_docs"}'
# publish jobs require $DOCKERHUB_REPO, $DOCKERHUB_USER, $DOCKERHUB_PASS defined
amd64:
machine:
image: ubuntu-2004:202111-02
docker:
docker:
- image: cimg/base:stable
steps:
- setup_remote_docker
- checkout
- run:
command: |
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
GIT_COMMIT=$(git rev-parse HEAD)
#
sudo docker build --build-arg GIT_COMMIT=${GIT_COMMIT} --pull -t $DOCKERHUB_REPO:$LATEST_TAG-amd64 -f amd64.Dockerfile .
sudo docker build --build-arg GIT_COMMIT=${GIT_COMMIT} --pull --build-arg CONFIGURATION_NAME=Altcoins-Release -t $DOCKERHUB_REPO:$LATEST_TAG-altcoins-amd64 -f amd64.Dockerfile .
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-amd64
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-altcoins-amd64
arm32v7:
machine:
image: ubuntu-2004:202111-02
steps:
- checkout
- run:
command: |
sudo docker run --rm --privileged multiarch/qemu-user-static:register --reset
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
GIT_COMMIT=$(git rev-parse HEAD)
#
sudo docker build --build-arg GIT_COMMIT=${GIT_COMMIT} --pull -t $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 -f arm32v7.Dockerfile .
sudo docker build --build-arg GIT_COMMIT=${GIT_COMMIT} --pull --build-arg CONFIGURATION_NAME=Altcoins-Release -t $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm32v7 -f arm32v7.Dockerfile .
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-arm32v7
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm32v7
arm64v8:
machine:
image: ubuntu-2004:202111-02
steps:
- checkout
- run:
command: |
sudo docker run --rm --privileged multiarch/qemu-user-static:register --reset
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
GIT_COMMIT=$(git rev-parse HEAD)
#
sudo docker build --build-arg GIT_COMMIT=${GIT_COMMIT} --pull -t $DOCKERHUB_REPO:$LATEST_TAG-arm64v8 -f arm64v8.Dockerfile .
sudo docker build --build-arg GIT_COMMIT=${GIT_COMMIT} --build-arg CONFIGURATION_NAME=Altcoins-Release --pull -t $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm64v8 -f arm64v8.Dockerfile .
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-arm64v8
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm64v8
multiarch:
machine:
image: ubuntu-2004:202201-02
steps:
- run:
command: |
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
#
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
sudo docker manifest create --amend $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-amd64 $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 $DOCKERHUB_REPO:$LATEST_TAG-arm64v8
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-amd64 --os linux --arch amd64
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 --os linux --arch arm --variant v7
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-arm64v8 --os linux --arch arm64 --variant v8
sudo docker manifest push $DOCKERHUB_REPO:$LATEST_TAG -p
sudo docker manifest create --amend $DOCKERHUB_REPO:$LATEST_TAG-altcoins $DOCKERHUB_REPO:$LATEST_TAG-altcoins-amd64 $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm32v7 $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm64v8
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG-altcoins $DOCKERHUB_REPO:$LATEST_TAG-altcoins-amd64 --os linux --arch amd64
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG-altcoins $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm32v7 --os linux --arch arm --variant v7
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG-altcoins $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm64v8 --os linux --arch arm64 --variant v8
sudo docker manifest push $DOCKERHUB_REPO:$LATEST_TAG-altcoins -p
docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
docker buildx create --use
DOCKER_BUILDX_OPTS="--platform linux/amd64,linux/arm64,linux/arm/v7 --build-arg GIT_COMMIT=${GIT_COMMIT} --push"
docker buildx build $DOCKER_BUILDX_OPTS -t $DOCKERHUB_REPO:$LATEST_TAG .
docker buildx build $DOCKER_BUILDX_OPTS -t $DOCKERHUB_REPO:$LATEST_TAG-altcoins --build-arg CONFIGURATION_NAME=Altcoins-Release .
workflows:
version: 2
build_and_test:
@ -120,7 +64,7 @@ workflows:
# only act on version tags
tags:
only: /(v[1-9]+(\.[0-9]+)*(-[a-z0-9-]+)?)|(v[a-z0-9-]+)/
- amd64:
- docker:
filters:
# ignore any commit on any branch by default
branches:
@ -130,25 +74,3 @@ workflows:
# OR features on specific versions like v1.0.0.88-lndseedbackup-1
tags:
only: /(v[1-9]+(\.[0-9]+)*(-[a-z0-9-]+)?)|(v[a-z0-9-]+)/
- arm32v7:
filters:
branches:
ignore: /.*/
tags:
only: /(v[1-9]+(\.[0-9]+)*(-[a-z0-9-]+)?)|(v[a-z0-9-]+)/
- arm64v8:
filters:
branches:
ignore: /.*/
tags:
only: /(v[1-9]+(\.[0-9]+)*(-[a-z0-9-]+)?)|(v[a-z0-9-]+)/
- multiarch:
requires:
- amd64
- arm32v7
- arm64v8
filters:
branches:
ignore: /.*/
tags:
only: /(v[1-9]+(\.[0-9]+)*(-[a-z0-9-]+)?)|(v[a-z0-9-]+)/

@ -32,8 +32,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="HtmlSanitizer" Version="8.0.723" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.1" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.0-beta.2" />
</ItemGroup>

@ -31,7 +31,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.5.1" />
<PackageReference Include="NBitcoin" Version="7.0.32" />
<PackageReference Include="NBitcoin" Version="7.0.34" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>

@ -27,10 +27,10 @@ namespace BTCPayServer.Client.Models
[JsonConverter(typeof(StringEnumConverter))]
public PosViewType DefaultView { get; set; }
public bool ShowCustomAmount { get; set; } = false;
public bool ShowDiscount { get; set; } = true;
public bool ShowDiscount { get; set; } = false;
public bool ShowSearch { get; set; } = true;
public bool ShowCategories { get; set; } = true;
public bool EnableTips { get; set; } = true;
public bool EnableTips { get; set; } = false;
public string CustomAmountPayButtonText { get; set; } = null;
public string FixedAmountPayButtonText { get; set; } = null;
public string TipText { get; set; } = null;

@ -31,18 +31,17 @@ namespace BTCPayServer
IEnumerable<BTCPayNetworkBase> networks,
SelectedChains selectedChains,
NBXplorerNetworkProvider nbxplorerNetworkProvider,
Logs logs,
IConfiguration configuration)
Logs logs)
{
var networksList = networks.ToList();
#if !ALTCOINS
var onlyBTC = networks.Count() == 1 && networks.First().IsBTC;
var onlyBTC = networksList.Count == 1 && networksList.First().IsBTC;
if (!onlyBTC)
throw new ConfigException($"This build of BTCPay Server does not support altcoins");
throw new ConfigException($"This build of BTCPay Server does not support altcoins. Configured networks: {string.Join(',', networksList.Select(n => n.CryptoCode).ToArray())}");
#endif
_NBXplorerNetworkProvider = nbxplorerNetworkProvider;
NetworkType = nbxplorerNetworkProvider.NetworkType;
foreach (var network in networks)
foreach (var network in networksList)
{
_Networks.Add(network.CryptoCode.ToUpperInvariant(), network);
}
@ -53,8 +52,7 @@ namespace BTCPayServer
throw new ConfigException($"Invalid chains \"{chain}\"");
}
logs.Configuration.LogInformation(
"Supported chains: " + String.Join(',', _Networks.Select(n => n.Key).ToArray()));
logs.Configuration.LogInformation("Supported chains: {Chains}", string.Join(',', _Networks.Select(n => n.Key).ToArray()));
}
public BTCPayNetwork BTC => GetNetwork<BTCPayNetwork>("BTC");

@ -4,7 +4,7 @@
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="NBXplorer.Client" Version="4.2.6" />
<PackageReference Include="NBXplorer.Client" Version="4.3.0" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="2.0.1" />
</ItemGroup>
<ItemGroup Condition="'$(Altcoins)' != 'true'">

@ -174,7 +174,6 @@ namespace BTCPayServer.Logging
logLevelColors = GetLogLevelConsoleColors(logLevel);
logLevelString = GetLogLevelString(logLevel);
// category and event id
var lenBefore = logBuilder.ToString().Length;
logBuilder.Append(_loglevelPadding);
logBuilder.Append(logName);
logBuilder.Append(": ");

@ -3,11 +3,11 @@
<Import Project="../Build/Common.csproj" />
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />

@ -4,9 +4,9 @@
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.9" />
<PackageReference Include="NBitcoin" Version="7.0.32" />
<PackageReference Include="NBitcoin" Version="7.0.34" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="1.0.4" />
</ItemGroup>

@ -64,8 +64,17 @@ namespace BTCPayServer.Services.Rates
{
if (_CurrencyProviders.Count == 0)
{
foreach (var culture in CultureInfo.GetCultures(CultureTypes.AllCultures).Where(c => !c.IsNeutralCulture))
foreach (var culture in CultureInfo.GetCultures(CultureTypes.AllCultures))
{
// This avoid storms of exception throwing slowing up
// startup and debugging sessions
if (culture switch
{
{ LCID: 0x007F or 0x0000 or 0x0c00 or 0x1000 } => true,
{ IsNeutralCulture : true } => true,
_ => false
})
continue;
try
{
_CurrencyProviders.TryAdd(new RegionInfo(culture.LCID).ISOCurrencySymbol, culture);

@ -110,7 +110,7 @@ namespace BTCPayServer.Services.Rates
public void LoadState(BackgroundFetcherState state)
{
if (state.LastRequested is DateTimeOffset lastRequested)
if (state.LastRequested is DateTimeOffset)
this.LastRequested = state.LastRequested;
if (state.LastUpdated is DateTimeOffset updated && state.Rates is List<BackgroundFetcherRate> rates)
{

@ -87,7 +87,6 @@ namespace BTCPayServer.Services.Rates
var normalizedPairsList = symbols.Where(s => !notFoundSymbols.ContainsKey(s)).Select(s => helper.NormalizeMarketSymbol(s)).ToList();
var csvPairsList = string.Join(",", normalizedPairsList);
JToken apiTickers = await MakeJsonRequestAsync<JToken>("/0/public/Ticker", null, new Dictionary<string, object> { { "pair", csvPairsList } }, cancellationToken: cancellationToken);
var tickers = new List<KeyValuePair<string, ExchangeTicker>>();
foreach (string symbol in symbols)
{
var ticker = ConvertToExchangeTicker(symbol, apiTickers[symbol]);

@ -45,7 +45,6 @@ namespace BTCPayServer.Services.Rates
var fetchingRates = new Dictionary<CurrencyPair, Task<RateResult>>();
var fetchingExchanges = new Dictionary<string, Task<QueryRateResult>>();
var consolidatedRates = new ExchangeRates();
foreach (var i in pairs.Select(p => (Pair: p, RateRule: rules.GetRuleFor(p))))
{

@ -176,7 +176,7 @@ namespace BTCPayServer.Tests
#pragma warning disable CS0618 // Type or member is obsolete
.OfType<DerivationSchemeSettings>().First(o => o.PaymentId.IsBTCOnChain);
#pragma warning restore CS0618 // Type or member is obsolete
DerivationSchemeSettings.TryParseFromWalletFile(content, onchainBTC.Network, out var expected, out var error);
FastTests.GetParsers().TryParseWalletFile(content, onchainBTC.Network, out var expected, out var error);
Assert.Equal(expected.ToJson(), onchainBTC.ToJson());
Assert.Null(error);

@ -25,8 +25,8 @@
<PackageReference Include="Selenium.Support" Version="4.1.1" />
<PackageReference Include="Selenium.WebDriver" Version="4.1.1" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="119.0.6045.10500" />
<PackageReference Include="xunit" Version="2.6.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.5">
<PackageReference Include="xunit" Version="2.6.6" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>

@ -60,13 +60,13 @@ namespace BTCPayServer.Tests
Assert.Contains("Bitcoin", s.Driver.FindElement(By.CssSelector(".payment-method.active")).Text);
Assert.Contains("LNURL", s.Driver.FindElement(By.CssSelector(".payment-method:nth-child(2)")).Text);
var qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
var address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
var clipboard = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
var payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
var copyAddress = s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text;
Assert.Equal($"bitcoin:{address}", payUrl);
var address = s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text;
Assert.StartsWith("bcrt", s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text);
Assert.DoesNotContain("lightning=", payUrl);
Assert.Equal(address, copyAddress);
Assert.Equal($"bitcoin:{address}", payUrl);
Assert.Equal($"bitcoin:{address}", clipboard);
Assert.Equal($"bitcoin:{address.ToUpperInvariant()}", qrValue);
s.Driver.ElementDoesNotExist(By.Id("Lightning_BTC"));
@ -97,11 +97,11 @@ namespace BTCPayServer.Tests
Assert.Contains("Lightning", s.Driver.WaitForElement(By.CssSelector(".payment-method.active")).Text);
Assert.Contains("Bitcoin", s.Driver.WaitForElement(By.CssSelector(".payment-method")).Text);
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
clipboard = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
copyAddress = s.Driver.FindElement(By.CssSelector("#Lightning_BTC_LightningLike .truncate-center-start")).Text;
address = s.Driver.FindElement(By.CssSelector("#Lightning_BTC_LightningLike .truncate-center-start")).Text;
Assert.Equal($"lightning:{address}", payUrl);
Assert.Equal(address, copyAddress);
Assert.Equal($"lightning:{address}", clipboard);
Assert.Equal($"lightning:{address.ToUpperInvariant()}", qrValue);
s.Driver.ElementDoesNotExist(By.Id("Address_BTC"));
@ -153,7 +153,7 @@ namespace BTCPayServer.Tests
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
await Task.Delay(200);
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
address = s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text;
var amountFraction = "0.00001";
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(address, Network.RegTest),
Money.Parse(amountFraction));
@ -202,15 +202,14 @@ namespace BTCPayServer.Tests
// Pay partial amount
await Task.Delay(200);
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
amountFraction = "0.00001";
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(address, Network.RegTest),
Money.Parse(amountFraction));
await s.Server.ExplorerNode.GenerateAsync(1);
s.Driver.FindElement(By.Id("test-payment-amount")).Clear();
s.Driver.FindElement(By.Id("test-payment-amount")).SendKeys("0.00001");
// Fake Pay
TestUtils.Eventually(() =>
{
s.Driver.FindElement(By.Id("FakePayment")).Click();
s.Driver.FindElement(By.Id("mine-block")).Click();
paymentInfo = s.Driver.WaitForElement(By.Id("PaymentInfo"));
Assert.Contains("The invoice hasn't been paid in full", paymentInfo.Text);
Assert.Contains("Please send", paymentInfo.Text);
@ -265,18 +264,19 @@ namespace BTCPayServer.Tests
Assert.Empty(s.Driver.FindElements(By.CssSelector(".payment-method")));
Assert.Contains("BTC", s.Driver.FindElement(By.Id("AmountDue")).Text);
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
clipboard = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
var copyAddressOnchain = s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text;
var copyAddressLightning = s.Driver.FindElement(By.CssSelector("#Lightning_BTC .truncate-center-start")).Text;
Assert.StartsWith($"bitcoin:{address}?amount=", payUrl);
Assert.StartsWith($"bitcoin:{copyAddressOnchain}?amount=", payUrl);
Assert.Contains("?amount=", payUrl);
Assert.Contains("&lightning=", payUrl);
Assert.StartsWith("bcrt", copyAddressOnchain);
Assert.Equal(address, copyAddressOnchain);
Assert.StartsWith("lnbcrt", copyAddressLightning);
Assert.StartsWith($"bitcoin:{address.ToUpperInvariant()}?amount=", qrValue);
Assert.StartsWith($"bitcoin:{copyAddressOnchain.ToUpperInvariant()}?amount=", qrValue);
Assert.Contains("&lightning=LNBCRT", qrValue);
Assert.Contains("&lightning=lnbcrt", clipboard);
Assert.Equal(clipboard, payUrl);
// Check details
s.Driver.ToggleCollapse("PaymentDetails");
@ -333,17 +333,18 @@ namespace BTCPayServer.Tests
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
Assert.Empty(s.Driver.FindElements(By.CssSelector(".payment-method")));
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
clipboard = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
copyAddressOnchain = s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text;
copyAddressLightning = s.Driver.FindElement(By.CssSelector("#Lightning_BTC .truncate-center-start")).Text;
Assert.StartsWith($"bitcoin:{address}", payUrl);
Assert.StartsWith($"bitcoin:{copyAddressOnchain}", payUrl);
Assert.Contains("?lightning=lnurl", payUrl);
Assert.DoesNotContain("amount=", payUrl);
Assert.StartsWith("bcrt", copyAddressOnchain);
Assert.Equal(address, copyAddressOnchain);
Assert.StartsWith("lnurl", copyAddressLightning);
Assert.StartsWith($"bitcoin:{address.ToUpperInvariant()}?lightning=LNURL", qrValue);
Assert.StartsWith($"bitcoin:{copyAddressOnchain.ToUpperInvariant()}?lightning=LNURL", qrValue);
Assert.Contains($"bitcoin:{copyAddressOnchain}?lightning=lnurl", clipboard);
Assert.Equal(clipboard, payUrl);
// Check details
s.Driver.ToggleCollapse("PaymentDetails");

@ -1,4 +1,4 @@
FROM mcr.microsoft.com/dotnet/sdk:8.0.100-bookworm-slim AS builder
FROM mcr.microsoft.com/dotnet/sdk:8.0.101-bookworm-slim AS builder
RUN apt-get update && apt-get install -y --no-install-recommends chromium-driver \
&& rm -rf /var/lib/apt/lists/*

@ -28,6 +28,7 @@ using BTCPayServer.Plugins.Bitcoin;
using BTCPayServer.Rating;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Fees;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Labels;
using BTCPayServer.Services.Rates;
@ -46,6 +47,7 @@ using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OpenQA.Selenium.DevTools.V100.DOMSnapshot;
using Xunit;
using Xunit.Abstractions;
using StoreData = BTCPayServer.Data.StoreData;
@ -159,6 +161,43 @@ namespace BTCPayServer.Tests
Assert.Equal("Test", data.FromAsset);
}
[Fact]
public void CanInterpolateOrBound()
{
var testData = new ((int Blocks, decimal Fee)[] Data, int Target, decimal Expected) []
{
([(0, 0m), (10, 100m)], 5, 50m),
([(50, 0m), (100, 100m)], 5, 0.0m),
([(50, 0m), (100, 100m)], 101, 100.0m),
([(50, 100m), (50, 100m)], 101, 100.0m),
([(50, 0m), (100, 100m)], 75, 50m),
([(0, 0m), (50, 50m), (100, 100m)], 75, 75m),
([(0, 0m), (500, 50m), (1000, 100m)], 750, 75m),
([(0, 0m), (500, 50m), (1000, 100m)], 100, 10m),
([(0, 0m), (100, 100m)], 80, 80m),
([(0, 0m), (100, 100m)], 25, 25m),
([(0, 0m), (25, 25m), (50, 50m), (100, 100m), (110, 120m)], 75, 75m),
([(0, 0m), (25, 0m), (50, 50m), (100, 100m), (110, 0m)], 75, 75m),
([(0, 0m), (25, 0m), (50, 50m), (100, 100m), (110, 0m)], 50, 50m),
([(0, 0m), (25, 0m), (50, 50m), (100, 100m), (110, 0m)], 100, 100m),
([(0, 0m), (25, 0m), (50, 50m), (100, 100m), (110, 0m)], 102, 80m),
};
foreach (var t in testData)
{
var actual = MempoolSpaceFeeProvider.InterpolateOrBound(t.Data.Select(t => new MempoolSpaceFeeProvider.BlockFeeRate(t.Blocks, new FeeRate(t.Fee))).ToArray(), t.Target);
Assert.Equal(new FeeRate(t.Expected), actual);
}
}
[Fact]
public void CanRandomizeByPercentage()
{
var generated = Enumerable.Range(0, 1000).Select(_ => MempoolSpaceFeeProvider.RandomizeByPercentage(100.0m, 10.0m)).ToArray();
Assert.Empty(generated.Where(g => g < 90m));
Assert.Empty(generated.Where(g => g > 110m));
Assert.NotEmpty(generated.Where(g => g < 91m));
Assert.NotEmpty(generated.Where(g => g > 109m));
}
private void CanParseDecimalsCore(string str, decimal expected)
{
var d = JsonConvert.DeserializeObject<LedgerEntryData>(str);
@ -649,11 +688,6 @@ namespace BTCPayServer.Tests
public void CanAcceptInvoiceWithTolerance()
{
var networkProvider = CreateNetworkProvider(ChainName.Regtest);
var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[]
{
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null, null),
new LightningLikePaymentHandler(null, null, networkProvider, null, null, null),
});
var entity = new InvoiceEntity();
entity.Networks = networkProvider;
#pragma warning disable CS0618
@ -882,6 +916,14 @@ namespace BTCPayServer.Tests
Assert.Equal(TradeQuantity.Parse(qty2.ToString()), TradeQuantity.Parse(" 1.3 "));
}
public static WalletFileParsers GetParsers()
{
var service = new ServiceCollection();
BTCPayServerServices.AddOnchainWalletParsers(service);
return service.BuildServiceProvider().GetRequiredService<WalletFileParsers>();
}
[Fact]
public void ParseDerivationSchemeSettings()
{
@ -890,13 +932,14 @@ namespace BTCPayServer.Tests
var root = new Mnemonic(
"usage fever hen zero slide mammal silent heavy donate budget pulse say brain thank sausage brand craft about save attract muffin advance illegal cabbage")
.DeriveExtKey();
var parsers = GetParsers();
// xpub
var tpub = "tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS";
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(tpub, testnet, out var settings, out var error));
Assert.Null(error);
Assert.True(parsers.TryParseWalletFile(tpub, testnet, out var settings, out var error));
Assert.True(settings.AccountDerivation is DirectDerivationStrategy { Segwit: false });
Assert.Equal($"{tpub}-[legacy]", ((DirectDerivationStrategy)settings.AccountDerivation).ToString());
Assert.Equal("GenericFile", settings.Source);
Assert.Null(error);
// xpub with fingerprint and account
tpub = "tpubDCXK98mNrPWuoWweaoUkqwxQF5NMWpQLy7n7XJgDCpwYfoZRXGafPaVM7mYqD7UKhsbMxkN864JY2PniMkt1Uk4dNuAMnWFVqdquyvZNyca";
@ -904,16 +947,18 @@ namespace BTCPayServer.Tests
var fingerprint = "e5746fd9";
var account = "84'/1'/0'";
var str = $"[{fingerprint}/{account}]{vpub}";
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(str, testnet, out settings, out error));
Assert.True(parsers.TryParseWalletFile(str, testnet, out settings, out error));
Assert.Null(error);
Assert.True(settings.AccountDerivation is DirectDerivationStrategy { Segwit: true });
Assert.Equal(vpub, settings.AccountOriginal);
Assert.Equal(tpub, ((DirectDerivationStrategy)settings.AccountDerivation).ToString());
Assert.Equal(HDFingerprint.TryParse(fingerprint, out var hd) ? hd : default, settings.AccountKeySettings[0].RootFingerprint);
Assert.Equal(account, settings.AccountKeySettings[0].AccountKeyPath.ToString());
Assert.Equal("GenericFile", settings.Source);
Assert.Null(error);
// ColdCard
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
Assert.True(parsers.TryParseWalletFile(
"{\"keystore\": {\"ckcc_xpub\": \"xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw\", \"xpub\": \"ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
mainnet, out settings, out error));
Assert.Null(error);
@ -927,78 +972,169 @@ namespace BTCPayServer.Tests
settings.AccountOriginal);
Assert.Equal(root.Derive(new KeyPath("m/49'/0'/0'")).Neuter().PubKey.WitHash.ScriptPubKey.Hash.ScriptPubKey,
settings.AccountDerivation.GetDerivation().ScriptPubKey);
Assert.Equal("ElectrumFile", settings.Source);
Assert.Null(error);
// Should be legacy
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
Assert.True(parsers.TryParseWalletFile(
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"tpubDDWYqT3P24znfsaGX7kZcQhNc5LAjnQiKQvUCHF2jS6dsgJBRtymopEU5uGpMaR5YChjuiExZG1X2aTbqXkp82KqH5qnqwWHp6EWis9ZvKr\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/44'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
testnet, out settings, out error));
Assert.True(settings.AccountDerivation is DirectDerivationStrategy { Segwit: false });
Assert.Equal("ElectrumFile", settings.Source);
Assert.Null(error);
// Should be segwit p2sh
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
Assert.True(parsers.TryParseWalletFile(
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"upub5DSddA9NoRUyJrQ4p86nsCiTSY7kLHrSxx3joEJXjHd4HPARhdXUATuk585FdWPVC2GdjsMePHb6BMDmf7c6KG4K4RPX6LVqBLtDcWpQJmh\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
testnet, out settings, out error));
Assert.True(settings.AccountDerivation is P2SHDerivationStrategy { Inner: DirectDerivationStrategy { Segwit: true } });
Assert.Equal("ElectrumFile", settings.Source);
Assert.Null(error);
// Should be segwit
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
Assert.True(parsers.TryParseWalletFile(
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"vpub5YjYxTemJ39tFRnuAhwduyxG2tKGjoEpmvqVQRPqdYrqa6YGoeSzBtHXaJUYB19zDbXs3JjbEcVWERjQBPf9bEfUUMZNMv1QnMyHV8JPqyf\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/84'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
testnet, out settings, out error));
Assert.True(settings.AccountDerivation is DirectDerivationStrategy { Segwit: true });
Assert.Equal("ElectrumFile", settings.Source);
Assert.Null(error);
// Specter
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(
Assert.True(parsers.TryParseWalletFile(
"{\"label\": \"Specter\", \"blockheight\": 123456, \"descriptor\": \"wpkh([8bafd160/49h/0h/0h]xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw/0/*)#9x4vkw48\"}",
mainnet, out var specter, out error));
Assert.Equal(root.GetPublicKey().GetHDFingerPrint(), specter.AccountKeySettings[0].RootFingerprint);
Assert.Equal(specter.AccountKeySettings[0].RootFingerprint, hd);
Assert.Equal("49'/0'/0'", specter.AccountKeySettings[0].AccountKeyPath.ToString());
Assert.True(specter.AccountDerivation is DirectDerivationStrategy { Segwit: true });
Assert.Equal("Specter", specter.Label);
Assert.Null(error);
//BSMS BIP129, Nunchuk
// Wasabi
var wasabiJson = @"{""EncryptedSecret"": ""6PYNUAZZLS1ShkhHhm9ayiNwXPAPLN669fN5mY2WbGm1Hqc88tomqWXabU"",""ChainCode"": ""UoHIB+2mDbZSowo11TfDQbsYK6q1DrZ2H2yqQBxu6m8="",""MasterFingerprint"": ""0f215605"",""ExtPubKey"": ""xpub6DUXFa6fMrFpg7x4nEd8jBU6xDN3vkSXsVUrSbUB2dadbYaPE31czwVdv146JRStGsc2U6TywdKnGoVcP8Rtp2AZQyzXxQb7HrgmR9LrqLA"",""TaprootExtPubKey"": ""xpub6D2thLU5KwUk3axkJu1UT3yKFshCGU7TMuxhPgZMd91VvrcDwHdRwdzLk61cSHtZC6BkaipPgfFwjoDBY4m1WxyznxZLukYgM4dC6iRJVf8"",""SkipSynchronization"": true,""UseTurboSync"": true,""MinGapLimit"": 21,""AccountKeyPath"": ""84'/0'/0'"",""TaprootAccountKeyPath"": ""86'/0'/0'"",""BlockchainState"": {""Network"": ""Main"",""Height"": ""503723"",""TurboSyncHeight"": ""503723""},""PreferPsbtWorkflow"": false,""AutoCoinJoin"": true,""PlebStopThreshold"": ""0.01"",""AnonScoreTarget"": 5,""FeeRateMedianTimeFrameHours"": 0,""IsCoinjoinProfileSelected"": true,""RedCoinIsolation"": false,""ExcludedCoinsFromCoinJoin"": [],""HdPubKeys"": [{""PubKey"": ""03f88b9c3e16e40a5a9eaf8b36b9bcee7bbc93fd9eea640b541efb931ac55f7ff5"",""FullKeyPath"": ""84'/0'/0'/1/0"",""Label"": """",""KeyState"": 0},{""PubKey"": ""03e5241fc28aa556d7cb826b9a9f5ecee85287e7476746126263574a5e27fbf569"",""FullKeyPath"": ""84'/0'/0'/0/0"",""Label"": """",""KeyState"": 0}]}";
Assert.True(parsers.TryParseWalletFile(wasabiJson, mainnet, out var wasabi, out error));
Assert.Null(error);
Assert.Equal("WasabiFile", wasabi.Source);
Assert.Single(wasabi.AccountKeySettings);
Assert.Equal("84'/0'/0'", wasabi.AccountKeySettings[0].AccountKeyPath.ToString());
Assert.Equal("0f215605", wasabi.AccountKeySettings[0].RootFingerprint.ToString());
Assert.True(wasabi.AccountDerivation is DirectDerivationStrategy { Segwit: true });
// BSMS BIP129, Nunchuk
var bsms = @"BSMS 1.0
wsh(sortedmulti(1,[5c9e228d/48'/0'/0'/2']xpub6EgGHjcvovyN3nK921zAGPfuB41cJXkYRdt3tLGmiMyvbgHpss4X1eRZwShbEBb1znz2e2bCkCED87QZpin3sSYKbmCzQ9Sc7LaV98ngdeX/**,[2b0e251e/48'/0'/0'/2']xpub6DrimHB8KUSkPvmJ8Pk8RE769EdDm2VEoZ8MBz76w9QupP8Py4wexs4Pa3aRB1LUEhc9GyY6ypDWEFFRCgqeDQePcyWQfjtmintrehq3JCL/**))
/0/*,/1/*
bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
";
Assert.True(DerivationSchemeSettings.TryParseFromWalletFile(bsms,
Assert.True(parsers.TryParseWalletFile(bsms,
mainnet, out var nunchuk, out error));
Assert.Equal(2, nunchuk.AccountKeySettings.Length);
Assert.Equal(2, nunchuk.AccountKeySettings.Length);
//check that the account key settings match those in bsms string
Assert.Equal("5c9e228d", nunchuk.AccountKeySettings[0].RootFingerprint.ToString());
Assert.Equal("48'/0'/0'/2'", nunchuk.AccountKeySettings[0].AccountKeyPath.ToString());
Assert.Equal("2b0e251e", nunchuk.AccountKeySettings[1].RootFingerprint.ToString());
Assert.Equal("2b0e251e", nunchuk.AccountKeySettings[1].RootFingerprint.ToString());
Assert.Equal("48'/0'/0'/2'", nunchuk.AccountKeySettings[1].AccountKeyPath.ToString());
var multsig = Assert.IsType < MultisigDerivationStrategy >
var multsig = Assert.IsType<MultisigDerivationStrategy>
(Assert.IsType<P2WSHDerivationStrategy>(nunchuk.AccountDerivation).Inner);
Assert.True(multsig.LexicographicOrder);
Assert.Equal(1, multsig.RequiredSignatures);
var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
var line =nunchuk.AccountDerivation.GetLineFor(deposit).Derive(0);
Assert.Equal(BitcoinAddress.Create("bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku", Network.Main).ScriptPubKey,
line.ScriptPubKey);
Assert.Equal(1, multsig.RequiredSignatures);
var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
var line = nunchuk.AccountDerivation.GetLineFor(deposit).Derive(0);
Assert.Equal(BitcoinAddress.Create("bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku", Network.Main).ScriptPubKey,
line.ScriptPubKey);
Assert.Equal("BSMS", nunchuk.Source);
Assert.Null(error);
// Failure case
Assert.False(DerivationSchemeSettings.TryParseFromWalletFile(
Assert.False(parsers.TryParseWalletFile(
"{\"keystore\": {\"ckcc_xpub\": \"tpubFailure\", \"xpub\": \"tpubFailure\", \"label\": \"Failure\"}, \"wallet_type\": \"standard\"}",
testnet, out settings, out error));
Assert.Null(settings);
Assert.NotNull(error);
//passport
var passportText =
"{\"Source\": \"Passport\", \"Descriptor\": \"tr([5c9e228d/86'/0'/0']xpub6EgGHjcvovyN3nK921zAGPfuB41cJXkYRdt3tLGmiMyvbgHpss4X1eRZwShbEBb1znz2e2bCkCED87QZpin3sSYKbmCzQ9Sc7LaV98ngdeX/0/*)\", \"FirmwareVersion\": \"v1.0.0\"}";
Assert.True(parsers.TryParseWalletFile(passportText, mainnet, out var passport, out error));
Assert.Equal("Passport", passport.Source);
Assert.True(passport.AccountDerivation is TaprootDerivationStrategy);
Assert.Equal("5c9e228d", passport.AccountKeySettings[0].RootFingerprint.ToString());
Assert.Equal("86'/0'/0'", passport.AccountKeySettings[0].AccountKeyPath.ToString());
//electrum
var electrumText =
"""
{
"keystore": {
"xpub": "vpub5Z14bnDNoEQeFdwZYSpVHcpzRpH99CnvSemzqTAvhjcgBTzPUVnaA5GhjgZc9J46duUprxQRUVUuqchazanXD6bLuVyarviNHBFUu6fBZNj",
"xprv": "vprv9ENJcv8RKwqMTqyhLSuBz5bEV7hpdZjisjUBuV9K8azz1vpop6xJFEDRdfDwgWBpYgUUhEVxdvpxgV3f8NircysfebnBaPu5y2dcnSDAEEw",
"type": "bip32",
"pw_hash_version": 1
},
"wallet_type": "standard",
"use_encryption": false,
"seed_type": "bip39"
}
""";
Assert.True(parsers.TryParseWalletFile(electrumText, testnet, out var electrum, out _));
Assert.Equal("ElectrumFile", electrum.Source);
electrumText =
"""
{
"keystore": {
"derivation": "m/0h",
"pw_hash_version": 1,
"root_fingerprint": "fbb5b37d",
"seed": "tiger room acoustic bracket thing film umbrella rather pepper tired vault remain",
"seed_type": "segwit",
"type": "bip32",
"xprv": "zprvAaQyp6mTAX53zY4j2BbecRNtmTq2kSEKgy2y4yK3bFPKgPJLxrMmPxzZdRkWq5XvmtH2R4ko5YmJYH2MgnVkWr32pHi4Dc5627WyML32KTW",
"xpub": "zpub6oQLDcJLztdMD29C8D8eyZKdKVfX9txB4BxZsMif9avJZBdVWPg1wmK3Uh3VxU7KXon1wm1xzvjyqmKWguYMqyjKP5f5Cho9f7uLfmRt2Br"
},
"wallet_type": "standard",
"use_encryption": false,
"seed_type": "bip39"
}
""";
Assert.True(parsers.TryParseWalletFile(electrumText, mainnet, out electrum, out _));
Assert.Equal("ElectrumFile", electrum.Source);
Assert.Equal("0'", electrum.GetSigningAccountKeySettings().AccountKeyPath.ToString());
Assert.True(electrum.AccountDerivation is DirectDerivationStrategy { Segwit: true });
Assert.Equal("fbb5b37d", electrum.GetSigningAccountKeySettings().RootFingerprint.ToString());
Assert.Equal("zpub6oQLDcJLztdMD29C8D8eyZKdKVfX9txB4BxZsMif9avJZBdVWPg1wmK3Uh3VxU7KXon1wm1xzvjyqmKWguYMqyjKP5f5Cho9f7uLfmRt2Br", electrum.AccountOriginal);
Assert.Equal(((DirectDerivationStrategy)electrum.AccountDerivation).GetExtPubKeys().First().ParentFingerprint.ToString(), electrum.GetSigningAccountKeySettings().RootFingerprint.ToString());
// Electrum with strange garbage at the end caused by the lightning support
electrumText =
"""
{
"keystore": {
"derivation": "m/0h",
"pw_hash_version": 1,
"root_fingerprint": "fbb5b37d",
"seed": "tiger room acoustic bracket thing film umbrella rather pepper tired vault remain",
"seed_type": "segwit",
"type": "bip32",
"xprv": "zprvAaQyp6mTAX53zY4j2BbecRNtmTq2kSEKgy2y4yK3bFPKgPJLxrMmPxzZdRkWq5XvmtH2R4ko5YmJYH2MgnVkWr32pHi4Dc5627WyML32KTW",
"xpub": "zpub6oQLDcJLztdMD29C8D8eyZKdKVfX9txB4BxZsMif9avJZBdVWPg1wmK3Uh3VxU7KXon1wm1xzvjyqmKWguYMqyjKP5f5Cho9f7uLfmRt2Br"
},
"wallet_type": "standard",
"use_encryption": false,
"seed_type": "bip39"
},
{"op": "remove", "path": "/channels"}
""";
Assert.True(parsers.TryParseWalletFile(electrumText, mainnet, out electrum, out _));
}
[Fact]
@ -1601,7 +1737,7 @@ Assert.Equal("2b0e251e", nunchuk.AccountKeySettings[1].RootFingerprint.ToString(
{
var b = JsonConvert.DeserializeObject<PullPaymentBlob>("{}");
Assert.Equal(TimeSpan.FromDays(30.0), b.BOLT11Expiration);
var aaa = JsonConvert.SerializeObject(b);
JsonConvert.SerializeObject(b);
}
[Fact]
@ -2110,7 +2246,7 @@ Assert.Equal("2b0e251e", nunchuk.AccountKeySettings[1].RootFingerprint.ToString(
[Fact]
public void AllPoliciesShowInUI()
{
var a = new BitpayRateProvider(new System.Net.Http.HttpClient()).GetRatesAsync(default).Result;
new BitpayRateProvider(new System.Net.Http.HttpClient()).GetRatesAsync(default).GetAwaiter().GetResult();
foreach (var policy in Policies.AllPolicies)
{
Assert.True(UIManageController.AddApiKeyViewModel.PermissionValueItem.PermissionDescriptions.ContainsKey(policy));
@ -2157,7 +2293,7 @@ Assert.Equal("2b0e251e", nunchuk.AccountKeySettings[1].RootFingerprint.ToString(
["derivationStrategy"] = "tpubDDLQZ1WMdy5YJAJWmRNoTJ3uQkavEPXCXnmD4eAuo9BKbzFUBbJmVHys5M3ku4Qw1C165wGpVWH55gZpHjdsCyntwNzhmCAzGejSL6rzbyf"
};
var scheme = DerivationSchemeSettings.Parse("tpubDDLQZ1WMdy5YJAJWmRNoTJ3uQkavEPXCXnmD4eAuo9BKbzFUBbJmVHys5M3ku4Qw1C165wGpVWH55gZpHjdsCyntwNzhmCAzGejSL6rzbyf", CreateNetworkProvider(ChainName.Regtest).BTC);
Assert.True(scheme.AccountDerivation is DirectDerivationStrategy { Segwit: true });
scheme.Source = "ManualDerivationScheme";
scheme.AccountOriginal = "tpubDDLQZ1WMdy5YJAJWmRNoTJ3uQkavEPXCXnmD4eAuo9BKbzFUBbJmVHys5M3ku4Qw1C165wGpVWH55gZpHjdsCyntwNzhmCAzGejSL6rzbyf";
var legacy2 = new JObject()

@ -58,8 +58,8 @@ namespace BTCPayServer.Tests
var factory = tester.PayTester.GetService<IBTCPayServerClientFactory>();
Assert.NotNull(factory);
var client = await factory.Create(user.UserId, user.StoreId);
var u = await client.GetCurrentUser();
var s = await client.GetStores();
await client.GetCurrentUser();
await client.GetStores();
var store = await client.GetStore(user.StoreId);
Assert.NotNull(store);
var addr = await client.GetLightningDepositAddress(user.StoreId, "BTC");
@ -1138,7 +1138,7 @@ namespace BTCPayServer.Tests
var approved = await acc.CreateClient(Policies.CanCreatePullPayments);
await AssertPermissionError(Policies.CanCreatePullPayments, async () =>
{
var pullPayment = await nonApproved.CreatePullPayment(acc.StoreId, new CreatePullPaymentRequest()
await nonApproved.CreatePullPayment(acc.StoreId, new CreatePullPaymentRequest()
{
Amount = 100,
Currency = "USD",
@ -1149,7 +1149,7 @@ namespace BTCPayServer.Tests
});
await AssertPermissionError(Policies.CanCreatePullPayments, async () =>
{
var pullPayment = await nonApproved.CreatePayout(acc.StoreId, new CreatePayoutThroughStoreRequest()
await nonApproved.CreatePayout(acc.StoreId, new CreatePayoutThroughStoreRequest()
{
Amount = 100,
PaymentMethod = "BTC",
@ -1158,7 +1158,7 @@ namespace BTCPayServer.Tests
});
});
var pullPayment = await approved.CreatePullPayment(acc.StoreId, new CreatePullPaymentRequest()
await approved.CreatePullPayment(acc.StoreId, new CreatePullPaymentRequest()
{
Amount = 100,
Currency = "USD",
@ -1167,7 +1167,7 @@ namespace BTCPayServer.Tests
AutoApproveClaims = true
});
var p = await approved.CreatePayout(acc.StoreId, new CreatePayoutThroughStoreRequest()
await approved.CreatePayout(acc.StoreId, new CreatePayoutThroughStoreRequest()
{
Amount = 100,
PaymentMethod = "BTC",
@ -2537,7 +2537,6 @@ namespace BTCPayServer.Tests
Expiry = TimeSpan.FromSeconds(400),
PrivateRouteHints = false
});
var chargeInvoice = invoiceData;
Assert.NotNull(await client.GetLightningInvoice("BTC", invoiceData.Id));
// check list for internal node
@ -3679,7 +3678,7 @@ namespace BTCPayServer.Tests
SavePrivateKeys = true
});
var preApprovedPayoutWithoutPullPayment = await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest()
await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest()
{
Amount = 0.0001m,
Approved = true,
@ -3911,7 +3910,7 @@ namespace BTCPayServer.Tests
beforeHookTcs = new TaskCompletionSource();
afterHookTcs = new TaskCompletionSource();
var payoutThatShouldNotBeProcessedStraightAway3 = await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest()
await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest()
{
Amount = 0.3m,
Approved = true,
@ -4331,7 +4330,7 @@ clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfi
await admin.GrantAccessAsync(true);
var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri);
var authClientNoPermissions = await admin.CreateClient(Policies.CanViewInvoices);
await admin.CreateClient(Policies.CanViewInvoices);
var adminClient = await admin.CreateClient(Policies.Unrestricted);
var managerClient = await admin.CreateClient(Policies.CanManageCustodianAccounts);
var withdrawalClient = await admin.CreateClient(Policies.CanWithdrawFromCustodianAccounts);

@ -29,7 +29,7 @@ namespace BTCPayServer.Tests
using var s = CreateSeleniumTester(newDb: true);
await s.StartAsync();
var u1 = s.RegisterNewUser(true);
s.RegisterNewUser(true);
var hot = s.CreateNewStore();
var seed = s.GenerateWallet(isHotWallet: true);
var cold = s.CreateNewStore();

@ -68,7 +68,7 @@ namespace BTCPayServer.Tests
{
using var tester = CreateServerTester();
await tester.StartAsync();
var network = tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC");
tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC");
var repo = tester.PayTester.GetService<UTXOLocker>();
var outpoint = RandomOutpoint();
@ -189,10 +189,10 @@ namespace BTCPayServer.Tests
using var tester = CreateServerTester();
await tester.StartAsync();
var broadcaster = tester.PayTester.GetService<DelayedTransactionBroadcaster>();
var payjoinRepository = tester.PayTester.GetService<UTXOLocker>();
tester.PayTester.GetService<UTXOLocker>();
broadcaster.Disable();
var network = tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC");
var btcPayWallet = tester.PayTester.GetService<BTCPayWalletProvider>().GetWallet(network);
tester.PayTester.GetService<BTCPayWalletProvider>().GetWallet(network);
var cashCow = tester.ExplorerNode;
cashCow.Generate(2); // get some money in case
@ -218,7 +218,7 @@ namespace BTCPayServer.Tests
receiverUser.GrantAccess(true);
receiverUser.RegisterDerivationScheme("BTC", receiverAddressType, true);
await receiverUser.ModifyOnchainPaymentSettings(p => p.PayJoinEnabled = true);
var receiverCoin = await receiverUser.ReceiveUTXO(Money.Satoshis(810), network);
await receiverUser.ReceiveUTXO(Money.Satoshis(810), network);
string errorCode = receiverAddressType == senderAddressType ? null : "unavailable|any UTXO available";
var invoice = receiverUser.BitPay.CreateInvoice(new Invoice() { Price = 50000, Currency = "SATS", FullNotifications = true });
@ -236,7 +236,7 @@ namespace BTCPayServer.Tests
txBuilder.SendEstimatedFees(new FeeRate(50m));
var psbt = txBuilder.BuildPSBT(false);
psbt = await senderUser.Sign(psbt);
var pj = await senderUser.SubmitPayjoin(invoice, psbt, errorCode, false);
await senderUser.SubmitPayjoin(invoice, psbt, errorCode, false);
}
}
}
@ -250,11 +250,11 @@ namespace BTCPayServer.Tests
s.RegisterNewUser(true);
var receiver = s.CreateNewStore();
s.EnableCheckout(CheckoutType.V1);
var receiverSeed = s.GenerateWallet("BTC", "", true, true, ScriptPubKeyType.Segwit);
s.GenerateWallet("BTC", "", true, true, ScriptPubKeyType.Segwit);
var receiverWalletId = new WalletId(receiver.storeId, "BTC");
var sender = s.CreateNewStore();
var senderSeed = s.GenerateWallet("BTC", "", true, true, ScriptPubKeyType.Segwit);
s.GenerateWallet("BTC", "", true, true, ScriptPubKeyType.Segwit);
var senderWalletId = new WalletId(sender.storeId, "BTC");
await s.Server.ExplorerNode.GenerateAsync(1);
@ -305,7 +305,7 @@ namespace BTCPayServer.Tests
var cryptoCode = "BTC";
var receiver = s.CreateNewStore();
s.EnableCheckout(CheckoutType.V1);
var receiverSeed = s.GenerateWallet(cryptoCode, "", true, true, format);
s.GenerateWallet(cryptoCode, "", true, true, format);
var receiverWalletId = new WalletId(receiver.storeId, cryptoCode);
//payjoin is enabled by default.
@ -320,7 +320,7 @@ namespace BTCPayServer.Tests
Assert.True(s.Driver.FindElement(By.Id("PayJoinEnabled")).Selected);
var sender = s.CreateNewStore();
var senderSeed = s.GenerateWallet(cryptoCode, "", true, true, format);
s.GenerateWallet(cryptoCode, "", true, true, format);
var senderWalletId = new WalletId(sender.storeId, cryptoCode);
await s.Server.ExplorerNode.GenerateAsync(1);
await s.FundStoreWallet(senderWalletId);
@ -374,7 +374,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("FeeSatoshiPerByte")).Clear();
s.Driver.FindElement(By.Id("FeeSatoshiPerByte")).SendKeys("2");
s.Driver.FindElement(By.Id("SignTransaction")).Click();
var txId = await s.Server.WaitForEvent<NewOnChainTransactionEvent>(() =>
await s.Server.WaitForEvent<NewOnChainTransactionEvent>(() =>
{
s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).Click();
return Task.CompletedTask;
@ -406,7 +406,6 @@ namespace BTCPayServer.Tests
await TestUtils.EventuallyAsync(async () =>
{
var invoice = await s.Server.PayTester.GetService<InvoiceRepository>().GetInvoice(invoiceId);
var dto = invoice.EntityToDTO();
Assert.Equal(InvoiceStatusLegacy.Paid, invoice.Status);
});
s.GoToInvoices(receiver.storeId);
@ -875,7 +874,6 @@ retry:
new Invoice() { Price = 0.02m, Currency = "BTC", FullNotifications = true });
cashCow.SendToAddress(BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network),
Money.Coins(0.06m));
var receiverWalletId = new WalletId(receiverUser.StoreId, "BTC");
//give the cow some cash
await cashCow.GenerateAsync(1);
@ -963,8 +961,6 @@ retry:
senderUser.GenerateWalletResponseV.MasterHDKey.Derive(signingKeySettings.GetRootedKeyPath()
.KeyPath);
var n = tester.ExplorerClient.Network.NBitcoinNetwork;
var Invoice1Coin1 = tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder()
.SetChange(senderChange)
.Send(parsedBip21.Address, parsedBip21.Amount)
@ -973,7 +969,7 @@ retry:
.SendEstimatedFees(new FeeRate(100m))
.BuildTransaction(true);
var Invoice1Coin2 = tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder()
tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder()
.SetChange(senderChange)
.Send(parsedBip21.Address, parsedBip21.Amount)
.AddCoins(coin2.Coin)
@ -1133,8 +1129,7 @@ retry:
var invoice7Coin6Response1Tx = await senderUser.SubmitPayjoin(invoice7, invoice7Coin6Tx, btcPayNetwork);
var Invoice7Coin6Response1TxSigned = invoice7Coin6TxBuilder.SignTransaction(invoice7Coin6Response1Tx);
var contributedInputsInvoice7Coin6Response1TxSigned =
Invoice7Coin6Response1TxSigned.Inputs.Single(txin => coin6.OutPoint != txin.PrevOut);
Invoice7Coin6Response1TxSigned.Inputs.Single(txin => coin6.OutPoint != txin.PrevOut);
////var receiverWalletPayJoinState = payJoinStateProvider.Get(receiverWalletId);

@ -288,7 +288,7 @@ namespace BTCPayServer.Tests
/// </summary>
/// <param name="cryptoCode"></param>
/// <param name="derivationScheme"></param>
public void AddDerivationScheme(string cryptoCode = "BTC", string derivationScheme = "xpub661MyMwAqRbcGABgHMUXDzPzH1tU7eZaAaJQXhDXsSxsqyQzQeU6kznNfSuAyqAK9UaWSaZaMFdNiY5BCF4zBPAzSnwfUAwUhwttuAKwfRX-[legacy]")
public void AddDerivationScheme(string cryptoCode = "BTC", string derivationScheme = "tpubD6NzVbkrYhZ4XxNXjYTcRujMc8z8734diCthtFGgDMimbG5hUsKBuSTCuUyxWL7YwP7R4A5StMTRQiZnb6vE4pdHWPgy9hbiHuVJfBMumUu-[legacy]")
{
if (!Driver.PageSource.Contains($"Setup {cryptoCode} Wallet"))
{

@ -565,9 +565,11 @@ namespace BTCPayServer.Tests
s.RegisterNewUser(true);
s.CreateNewStore();
s.GoToInvoices();
s.Driver.FindElement(By.Id("CreateNewInvoice")).Click();
// Should give us an error message if we try to create an invoice before adding a wallet
s.Driver.FindElement(By.Id("CreateNewInvoice")).Click();
Assert.Contains("To create an invoice, you need to", s.Driver.PageSource);
s.AddDerivationScheme();
s.GoToInvoices();
s.CreateInvoice();
@ -1195,8 +1197,13 @@ namespace BTCPayServer.Tests
await s.StartAsync();
s.RegisterNewUser();
s.CreateNewStore();
s.AddDerivationScheme();
s.Driver.FindElement(By.Id("StoreNav-PaymentRequests")).Click();
// Should give us an error message if we try to create a payment request before adding a wallet
s.Driver.FindElement(By.Id("CreatePaymentRequest")).Click();
Assert.Contains("To create a payment request, you need to", s.Driver.PageSource);
s.AddDerivationScheme();
s.Driver.FindElement(By.Id("StoreNav-PaymentRequests")).Click();
s.Driver.FindElement(By.Id("CreatePaymentRequest")).Click();
s.Driver.FindElement(By.Id("Title")).SendKeys("Pay123");
@ -1626,7 +1633,6 @@ namespace BTCPayServer.Tests
var mnemonic = s.GenerateWallet(cryptoCode, "", true, true);
//lets import and save private keys
var root = mnemonic.DeriveExtKey();
invoiceId = s.CreateInvoice(storeId);
invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId);
address = invoice.EntityToDTO().Addresses["BTC"];
@ -2326,7 +2332,6 @@ namespace BTCPayServer.Tests
s.Server.ActivateLightning();
await s.StartAsync();
await s.Server.EnsureChannelsSetup();
s.RegisterNewUser(true);
s.CreateNewStore();
s.GoToStore();
@ -2337,8 +2342,15 @@ namespace BTCPayServer.Tests
TestUtils.Eventually(() => Assert.Contains("App successfully created", s.FindAlertMessage().Text));
s.Driver.FindElement(By.CssSelector("label[for='DefaultView_Light']")).Click();
s.Driver.FindElement(By.Id("Currency")).SendKeys("EUR");
Assert.False(s.Driver.FindElement(By.Id("EnableTips")).Selected);
s.Driver.FindElement(By.Id("EnableTips")).Click();
Assert.True(s.Driver.FindElement(By.Id("EnableTips")).Selected);
Thread.Sleep(250);
s.Driver.FindElement(By.Id("CustomTipPercentages")).Clear();
s.Driver.FindElement(By.Id("CustomTipPercentages")).SendKeys("10,21");
Assert.False(s.Driver.FindElement(By.Id("ShowDiscount")).Selected);
s.Driver.FindElement(By.Id("ShowDiscount")).Click();
s.Driver.FindElement(By.Id("SaveSettings")).Click();
Assert.Contains("App updated", s.FindAlertMessage().Text);
s.Driver.FindElement(By.Id("ViewApp")).Click();
@ -2346,7 +2358,7 @@ namespace BTCPayServer.Tests
Assert.Equal(2, windows.Count);
s.Driver.SwitchTo().Window(windows[1]);
s.Driver.WaitForElement(By.ClassName("keypad"));
// basic checks
Assert.Contains("EUR", s.Driver.FindElement(By.Id("Currency")).Text);
Assert.Contains("0,00", s.Driver.FindElement(By.Id("Amount")).Text);
@ -2354,7 +2366,7 @@ namespace BTCPayServer.Tests
Assert.True(s.Driver.FindElement(By.Id("ModeTablist-amounts")).Selected);
Assert.False(s.Driver.FindElement(By.Id("ModeTablist-discount")).Enabled);
Assert.False(s.Driver.FindElement(By.Id("ModeTablist-tip")).Enabled);
// Amount: 1234,56
s.Driver.FindElement(By.CssSelector(".keypad [data-key='1']")).Click();
s.Driver.FindElement(By.CssSelector(".keypad [data-key='2']")).Click();
@ -2371,7 +2383,7 @@ namespace BTCPayServer.Tests
Assert.True(s.Driver.FindElement(By.Id("ModeTablist-discount")).Enabled);
Assert.True(s.Driver.FindElement(By.Id("ModeTablist-tip")).Enabled);
Assert.Equal("1.234,00 € + 0,56 €", s.Driver.FindElement(By.Id("Calculation")).Text);
// Discount: 10%
s.Driver.FindElement(By.CssSelector("label[for='ModeTablist-discount']")).Click();
s.Driver.FindElement(By.CssSelector(".keypad [data-key='1']")).Click();
@ -2379,14 +2391,14 @@ namespace BTCPayServer.Tests
Assert.Contains("1.111,10", s.Driver.FindElement(By.Id("Amount")).Text);
Assert.Contains("10% discount", s.Driver.FindElement(By.Id("Discount")).Text);
Assert.Contains("1.234,00 € + 0,56 € - 123,46 € (10%)", s.Driver.FindElement(By.Id("Calculation")).Text);
// Tip: 10%
s.Driver.FindElement(By.CssSelector("label[for='ModeTablist-tip']")).Click();
s.Driver.WaitForElement(By.Id("Tip-Custom"));
s.Driver.FindElement(By.Id("Tip-10")).Click();
Assert.Contains("1.222,21", s.Driver.FindElement(By.Id("Amount")).Text);
Assert.Contains("1.234,00 € + 0,56 € - 123,46 € (10%) + 111,11 € (10%)", s.Driver.FindElement(By.Id("Calculation")).Text);
// Pay
s.Driver.FindElement(By.Id("pay-button")).Click();
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
@ -2415,8 +2427,14 @@ namespace BTCPayServer.Tests
Assert.Contains("App successfully created", s.FindAlertMessage().Text);
s.Driver.FindElement(By.CssSelector("label[for='DefaultView_Cart']")).Click();
s.Driver.FindElement(By.Id("Currency")).SendKeys("EUR");
Assert.False(s.Driver.FindElement(By.Id("EnableTips")).Selected);
s.Driver.FindElement(By.Id("EnableTips")).Click();
Assert.True(s.Driver.FindElement(By.Id("EnableTips")).Selected);
Thread.Sleep(250);
s.Driver.FindElement(By.Id("CustomTipPercentages")).Clear();
s.Driver.FindElement(By.Id("CustomTipPercentages")).SendKeys("10,21");
Assert.False(s.Driver.FindElement(By.Id("ShowDiscount")).Selected);
s.Driver.FindElement(By.Id("ShowDiscount")).Click();
s.Driver.FindElement(By.Id("SaveSettings")).Click();
Assert.Contains("App updated", s.FindAlertMessage().Text);
s.Driver.FindElement(By.Id("ViewApp")).Click();
@ -2483,19 +2501,19 @@ namespace BTCPayServer.Tests
Thread.Sleep(250);
Assert.Equal(7, s.Driver.FindElements(By.CssSelector("#CartItems tr")).Count);
Assert.Equal("10,00 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
// Discount: 10%
s.Driver.ElementDoesNotExist(By.Id("CartDiscount"));
s.Driver.FindElement(By.Id("Discount")).SendKeys("10");
Assert.Contains("10% = 1,00 €", s.Driver.FindElement(By.Id("CartDiscount")).Text);
Assert.Equal("9,00 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
// Tip: 10%
s.Driver.ElementDoesNotExist(By.Id("CartTip"));
s.Driver.FindElement(By.Id("Tip-10")).Click();
Assert.Contains("10% = 0,90 €", s.Driver.FindElement(By.Id("CartTip")).Text);
Assert.Equal("9,90 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
// Check values on checkout page
s.Driver.FindElement(By.Id("CartSubmit")).Click();
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
@ -2579,7 +2597,7 @@ namespace BTCPayServer.Tests
// BOLT11 is also displayed for standard invoice (not LNURL, even if it is available)
s.Driver.FindElement(By.Id("copy-tab")).Click();
var bolt11 = s.Driver.FindElement(By.CssSelector("input.checkoutTextbox")).GetAttribute("value");
var bolt11Parsed = Lightning.BOLT11PaymentRequest.Parse(bolt11, s.Server.ExplorerNode.Network);
Lightning.BOLT11PaymentRequest.Parse(bolt11, s.Server.ExplorerNode.Network);
var invoiceId = s.Driver.Url.Split('/').Last();
using (var resp = await s.Server.PayTester.HttpClient.GetAsync("BTC/lnurl/pay/i/" + invoiceId))
{
@ -2790,7 +2808,6 @@ namespace BTCPayServer.Tests
}
invoices = await repo.GetInvoices(new InvoiceQuery() { StoreId = new[] { s.StoreId } });
Assert.Equal(2, invoices.Length);
var emailSuffix = $"@{s.Server.PayTester.HostName}:{s.Server.PayTester.Port}";
foreach (var i in invoices)
{
var lightningPaymentMethod = i.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.LNURLPay));

@ -39,7 +39,7 @@ namespace BTCPayServer.Tests
if (!Directory.Exists(_Directory))
Directory.CreateDirectory(_Directory);
NetworkProvider = networkProvider;
_NetworkProvider = networkProvider;
ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_BTCRPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork<BTCPayNetwork>("BTC").NBitcoinNetwork);
ExplorerNode.ScanRPCCapabilities();
@ -214,8 +214,14 @@ namespace BTCPayServer.Tests
{
return new TestAccount(this);
}
public BTCPayNetworkProvider NetworkProvider { get; private set; }
BTCPayNetworkProvider _NetworkProvider;
public BTCPayNetworkProvider NetworkProvider
{
get
{
return PayTester?.Networks ?? _NetworkProvider;
}
}
public RPCClient ExplorerNode
{
get; set;

@ -73,7 +73,7 @@ namespace BTCPayServer.Tests
public async Task<BTCPayServerClient> CreateClient(params string[] permissions)
{
var manageController = parent.PayTester.GetController<UIManageController>(UserId, StoreId, IsAdmin);
var x = Assert.IsType<RedirectToActionResult>(await manageController.AddApiKey(
Assert.IsType<RedirectToActionResult>(await manageController.AddApiKey(
new UIManageController.AddApiKeyViewModel()
{
PermissionValues = permissions.Select(s =>
@ -339,7 +339,6 @@ namespace BTCPayServer.Tests
public async Task<BitcoinAddress> GetNewAddress(BTCPayNetwork network)
{
var cashCow = parent.ExplorerNode;
var btcPayWallet = parent.PayTester.GetService<BTCPayWalletProvider>().GetWallet(network);
var address = (await btcPayWallet.ReserveAddressAsync(this.DerivationScheme)).Address;
return address;
@ -347,7 +346,7 @@ namespace BTCPayServer.Tests
public async Task<PSBT> Sign(PSBT psbt)
{
var btcPayWallet = parent.PayTester.GetService<BTCPayWalletProvider>()
parent.PayTester.GetService<BTCPayWalletProvider>()
.GetWallet(psbt.Network.NetworkSet.CryptoCode);
var explorerClient = parent.PayTester.GetService<ExplorerClientProvider>()
.GetExplorerClient(psbt.Network.NetworkSet.CryptoCode);
@ -444,7 +443,7 @@ namespace BTCPayServer.Tests
var parsedBip21 = new BitcoinUrlBuilder(
invoice.CryptoInfo.First(c => c.CryptoCode == network.NetworkSet.CryptoCode).PaymentUrls.BIP21,
network);
if (!parsedBip21.TryGetPayjoinEndpoint(out var endpoint))
if (!parsedBip21.TryGetPayjoinEndpoint(out _))
return null;
return parsedBip21;
}

@ -10,6 +10,7 @@ using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Models.WalletViewModels;
using BTCPayServer.Rating;
using BTCPayServer.Services.Fees;
using BTCPayServer.Services.Rates;
@ -90,9 +91,43 @@ namespace BTCPayServer.Tests
"test" + isTestnet,
prov.GetService<IHttpClientFactory>(),
isTestnet);
mempoolSpaceFeeProvider.CachedOnly = true;
await Assert.ThrowsAsync<InvalidOperationException>(() => mempoolSpaceFeeProvider.GetFeeRateAsync());
mempoolSpaceFeeProvider.CachedOnly = false;
var rates = await mempoolSpaceFeeProvider.GetFeeRatesAsync();
mempoolSpaceFeeProvider.CachedOnly = true;
await mempoolSpaceFeeProvider.GetFeeRateAsync();
mempoolSpaceFeeProvider.CachedOnly = false;
Assert.NotEmpty(rates);
await mempoolSpaceFeeProvider.GetFeeRateAsync(20);
var recommendedFees =
await Task.WhenAll(new[]
{
TimeSpan.FromMinutes(10.0), TimeSpan.FromMinutes(60.0), TimeSpan.FromHours(6.0),
TimeSpan.FromHours(24.0),
}.Select(async time =>
{
try
{
var result = await mempoolSpaceFeeProvider.GetFeeRateAsync(
(int)Network.Main.Consensus.GetExpectedBlocksFor(time));
return new WalletSendModel.FeeRateOption()
{
Target = time,
FeeRate = result.SatoshiPerByte
};
}
catch (Exception)
{
return null;
}
})
.ToArray());
//ENSURE THESE ARE LOGICAL
Assert.True(recommendedFees[0].FeeRate >= recommendedFees[1].FeeRate, $"{recommendedFees[0].Target}:{recommendedFees[0].FeeRate} >= {recommendedFees[1].Target}:{recommendedFees[1].FeeRate}");
Assert.True(recommendedFees[1].FeeRate >= recommendedFees[2].FeeRate, $"{recommendedFees[1].Target}:{recommendedFees[1].FeeRate} >= {recommendedFees[2].Target}:{recommendedFees[2].FeeRate}");
Assert.True(recommendedFees[2].FeeRate >= recommendedFees[3].FeeRate, $"{recommendedFees[2].Target}:{recommendedFees[2].FeeRate} >= {recommendedFees[3].Target}:{recommendedFees[3].FeeRate}");
}
}
[Fact]
@ -278,7 +313,6 @@ retry:
}
catch (Exception ex) when (ex is MatchesException)
{
var details = ex.Message;
TestLogs.LogInformation($"FAILED: {url} ({file}) anchor not found: {uri.Fragment}");
throw;

@ -267,7 +267,6 @@ namespace BTCPayServer.Tests
}
catch (Exception ex) when (ex is MatchesException)
{
var details = ex.Message;
TestLogs.LogInformation($"FAILED: {url} ({file}) anchor not found: {uri.Fragment}");
throw;
@ -347,7 +346,7 @@ namespace BTCPayServer.Tests
try
{
var throwsBitpay404Error = user.BitPay.GetInvoice(invoice.Id + "123");
user.BitPay.GetInvoice(invoice.Id + "123");
}
catch (BitPayException ex)
{
@ -885,7 +884,7 @@ namespace BTCPayServer.Tests
Assert.Equal("LTC", GetCurrencyPairRateResult.Data.Code);
// Should be OK because the request is signed, so we can know the store
var rates = acc.BitPay.GetRates();
acc.BitPay.GetRates();
HttpClient client = new HttpClient();
// Unauthentified requests should also be ok
var response =
@ -1072,7 +1071,7 @@ namespace BTCPayServer.Tests
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(0.01m, "BTC"));
await tester.WaitForEvent<InvoiceEvent>(async () =>
{
var tx = await tester.ExplorerNode.SendToAddressAsync(
await tester.ExplorerNode.SendToAddressAsync(
BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest),
Money.Coins(0.01m));
});
@ -1148,6 +1147,14 @@ namespace BTCPayServer.Tests
bitpay = new Bitpay(k, tester.PayTester.ServerUri);
Assert.True(bitpay.TestAccess(Facade.Merchant));
Assert.True(bitpay.TestAccess(Facade.PointOfSale));
HttpClient client = new HttpClient();
var token = (await bitpay.GetAccessTokenAsync(Facade.Merchant)).Value;
var getRates = tester.PayTester.ServerUri.AbsoluteUri + $"rates/?cryptoCode=BTC&token={token}";
var req = new HttpRequestMessage(HttpMethod.Get, getRates);
req.Headers.Add("x-signature", NBitpayClient.Extensions.BitIdExtensions.GetBitIDSignature(k, getRates, null));
req.Headers.Add("x-identity", k.PubKey.ToHex());
var resp = await client.SendAsync(req);
resp.EnsureSuccessStatusCode();
// Can generate API Key
var repo = tester.PayTester.GetService<TokenRepository>();
@ -1168,7 +1175,6 @@ namespace BTCPayServer.Tests
apiKey = apiKey2;
// Can create an invoice with this new API Key
HttpClient client = new HttpClient();
HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Post,
tester.PayTester.ServerUri.AbsoluteUri + "invoices");
message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic",
@ -1457,7 +1463,7 @@ namespace BTCPayServer.Tests
// via UI
var controller = user.GetController<UIInvoiceController>();
var model = await controller.CreateInvoice();
await controller.CreateInvoice();
(await controller.CreateInvoice(new CreateInvoiceModel(), default)).AssertType<RedirectToActionResult>();
invoice = await client.GetInvoice(user.StoreId, controller.CreatedInvoiceId);
Assert.Equal("EUR", invoice.Currency);
@ -2139,8 +2145,7 @@ namespace BTCPayServer.Tests
user.RegisterDerivationScheme("BTC");
var serverController = user.GetController<UIServerController>();
var vm = Assert.IsType<LogsViewModel>(
Assert.IsType<ViewResult>(await serverController.LogsView()).Model);
Assert.IsType<LogsViewModel>(Assert.IsType<ViewResult>(await serverController.LogsView()).Model);
}
[Fact(Timeout = LongRunningTestTimeout)]
@ -2394,7 +2399,7 @@ namespace BTCPayServer.Tests
Assert.NotNull(lnMethod.GetExternalLightningUrl());
var url = lnMethod.GetExternalLightningUrl();
var kv = LightningConnectionStringHelper.ExtractValues(url, out var connType);
LightningConnectionStringHelper.ExtractValues(url, out var connType);
Assert.Equal(LightningConnectionType.Charge, connType);
var client = Assert.IsType<ChargeClient>(tester.PayTester.GetService<LightningClientFactoryService>()
.Create(url, tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC")));
@ -2771,7 +2776,7 @@ namespace BTCPayServer.Tests
Assert.Equal(fileContent, data);
//create a temporary link to file
var tmpLinkGenerate = Assert.IsType<RedirectToActionResult>(await controller.CreateTemporaryFileUrl(fileId,
Assert.IsType<RedirectToActionResult>(await controller.CreateTemporaryFileUrl(fileId,
new UIServerController.CreateTemporaryFileUrlViewModel
{
IsDownload = true,

@ -200,26 +200,33 @@ namespace BTCPayServer.Tests
if (!askedPrompt)
{
driver.FindElement(By.XPath("//a[contains(text(), \"New chat\")]")).Click();
driver.FindElements(By.XPath("//button[contains(@class,'text-token-text-primary')]")).Where(e => e.Displayed).First().Click();
Thread.Sleep(200);
var input = driver.FindElement(By.XPath("//textarea[@data-id]"));
input.SendKeys($"I am translating a checkout crypto payment page, and I want you to translate it from English (en-US) to {languageCurrent} ({jsonLangCode}).");
input.SendKeys(Keys.LeftShift + Keys.Enter);
input.SendKeys("Reply only with the translation of the sentences I will give you and nothing more." + Keys.Enter);
input.SendKeys("Reply only with the translation of the sentences I will give you and nothing more, and do not translate what is inside `{{` and `}}`." + Keys.Enter);
WaitCanWritePrompt(driver);
askedPrompt = true;
}
english = english.Replace('\n', ' ');
driver.FindElement(By.XPath("//textarea[@data-id]")).SendKeys(english + Keys.Enter);
WaitCanWritePrompt(driver);
var elements = driver.FindElements(By.XPath("//div[contains(@class,'markdown') and contains(@class,'prose')]//p"));
var result = elements.Last().Text;
string result = GetLastResponse(driver);
langFile.Words[translation.Key] = result;
}
langFile.Save();
}
}
private static string GetLastResponse(ChromeDriver driver)
{
var elements = driver.FindElements(By.XPath("//div[contains(@class,'markdown') and contains(@class,'prose')]//p"));
var result = elements.LastOrDefault()?.Text;
return result;
}
private static TransifexClient GetTransifexClient()
{
return new TransifexClient(FactWithSecretAttribute.GetFromSecrets("TransifexAPIToken"));
@ -227,12 +234,27 @@ namespace BTCPayServer.Tests
private void WaitCanWritePrompt(IWebDriver driver)
{
bool stopGenerating = false;
retry:
Thread.Sleep(200);
try
{
driver.FindElement(By.XPath("//*[contains(text(), \"Regenerate response\")]"));
var el = driver.FindElement(By.XPath("//button[contains(@aria-label, 'Stop generating')]"));
if (!el.Enabled)
goto retry;
stopGenerating = true;
goto retry;
}
catch
{
if (!stopGenerating)
goto retry;
}
try
{
var el = driver.FindElement(By.XPath("//button[contains(@data-testid, 'send-button')]"));
if (!el.Displayed)
goto retry;
}
catch
{
@ -409,7 +431,7 @@ retry:
content.Headers.TryAddWithoutValidation("Content-Type", "application/vnd.api+json;profile=\"bulk\"");
message.Content = content;
using var response = await Client.SendAsync(message);
var str = await response.Content.ReadAsStringAsync();
await response.Content.ReadAsStringAsync();
}).ToArray());
}

@ -99,7 +99,7 @@ services:
custom:
nbxplorer:
image: nicolasdorier/nbxplorer:2.4.0
image: nicolasdorier/nbxplorer:2.5.0
restart: unless-stopped
ports:
- "32838:32838"
@ -224,7 +224,7 @@ services:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:v0.17.2-beta
image: btcpayserver/lnd:v0.17.3-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -259,7 +259,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.17.2-beta
image: btcpayserver/lnd:v0.17.3-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"

@ -96,7 +96,7 @@ services:
custom:
nbxplorer:
image: nicolasdorier/nbxplorer:2.4.0
image: nicolasdorier/nbxplorer:2.5.0
restart: unless-stopped
ports:
- "32838:32838"
@ -211,7 +211,7 @@ services:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:v0.17.2-beta
image: btcpayserver/lnd:v0.17.3-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -248,7 +248,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.17.2-beta
image: btcpayserver/lnd:v0.17.3-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"

@ -1,4 +1,4 @@
 <Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
<Import Project="../Build/Common.csproj" />
@ -46,13 +46,13 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.NTag424" Version="1.0.19" />
<PackageReference Include="BTCPayServer.NTag424" Version="1.0.20" />
<PackageReference Include="YamlDotNet" Version="8.0.0" />
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.5.3" />
<PackageReference Include="CsvHelper" Version="15.0.5" />
<PackageReference Include="Dapper" Version="2.1.24" />
<PackageReference Include="Dapper" Version="2.1.28" />
<PackageReference Include="Fido2" Version="2.0.2" />
<PackageReference Include="Fido2.AspNet" Version="2.0.2" />
<PackageReference Include="LNURL" Version="0.0.34" />
@ -77,8 +77,8 @@
<PackageReference Include="TwentyTwenty.Storage.Azure" Version="2.12.1" />
<PackageReference Include="TwentyTwenty.Storage.Google" Version="2.12.1" />
<PackageReference Include="TwentyTwenty.Storage.Local" Version="2.12.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.1" />
</ItemGroup>
<ItemGroup>

@ -30,7 +30,7 @@ public class AppSales : ViewComponent
public async Task<IViewComponentResult> InvokeAsync(string appId, string appType)
{
var type = _appService.GetAppType(appType);
if (type is not IHasSaleStatsAppType salesAppType || type is not AppBaseType appBaseType)
if (type is not IHasSaleStatsAppType || type is not AppBaseType appBaseType)
return new HtmlContentViewComponentResult(new StringHtmlContent(string.Empty));
var vm = new AppSalesViewModel
{

@ -1,5 +1,8 @@
@model BTCPayServer.Components.StoreLightningBalance.StoreLightningBalanceViewModel
@if(!Model.InitialRendering && Model.Balance == null)
{
return;
}
<div id="StoreLightningBalance-@Model.Store.Id" class="widget store-lightning-balance">
<div class="d-flex gap-3 align-items-center justify-content-between mb-2">
<h6>Lightning Balance</h6>

@ -2,6 +2,7 @@ using System;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.Lightning;
@ -9,9 +10,11 @@ using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Security;
using BTCPayServer.Services;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
@ -26,6 +29,7 @@ public class StoreLightningBalance : ViewComponent
private readonly LightningClientFactoryService _lightningClientFactory;
private readonly IOptions<LightningNetworkOptions> _lightningNetworkOptions;
private readonly IOptions<ExternalServicesOptions> _externalServiceOptions;
private readonly IAuthorizationService _authorizationService;
public StoreLightningBalance(
StoreRepository storeRepo,
@ -34,13 +38,15 @@ public class StoreLightningBalance : ViewComponent
BTCPayServerOptions btcpayServerOptions,
LightningClientFactoryService lightningClientFactory,
IOptions<LightningNetworkOptions> lightningNetworkOptions,
IOptions<ExternalServicesOptions> externalServiceOptions)
IOptions<ExternalServicesOptions> externalServiceOptions,
IAuthorizationService authorizationService)
{
_storeRepo = storeRepo;
_currencies = currencies;
_networkProvider = networkProvider;
_btcpayServerOptions = btcpayServerOptions;
_externalServiceOptions = externalServiceOptions;
_authorizationService = authorizationService;
_lightningClientFactory = lightningClientFactory;
_lightningNetworkOptions = lightningNetworkOptions;
}
@ -54,13 +60,19 @@ public class StoreLightningBalance : ViewComponent
vm.DefaultCurrency = vm.Store.GetStoreBlob().DefaultCurrency;
vm.CurrencyData = _currencies.GetCurrencyData(vm.DefaultCurrency, true);
if (vm.InitialRendering)
return View(vm);
try
{
var lightningClient = GetLightningClient(vm.Store, vm.CryptoCode);
var lightningClient = await GetLightningClient(vm.Store, vm.CryptoCode);
if (lightningClient == null)
{
vm.InitialRendering = false;
return View(vm);
}
if (vm.InitialRendering)
return View(vm);
var balance = await lightningClient.GetBalance();
vm.Balance = balance;
vm.TotalOnchain = balance.OnchainBalance != null
@ -72,7 +84,8 @@ public class StoreLightningBalance : ViewComponent
(balance.OffchainBalance.Closing ?? 0)
: null;
}
catch (NotSupportedException)
catch (Exception ex) when (ex is NotImplementedException or NotSupportedException)
{
// not all implementations support balance fetching
vm.ProblemDescription = "Your node does not support balance fetching.";
@ -85,7 +98,7 @@ public class StoreLightningBalance : ViewComponent
return View(vm);
}
private ILightningClient GetLightningClient(StoreData store, string cryptoCode)
private async Task<ILightningClient> GetLightningClient(StoreData store, string cryptoCode )
{
var network = _networkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
@ -101,7 +114,9 @@ public class StoreLightningBalance : ViewComponent
}
if (existing.IsInternalNode && _lightningNetworkOptions.Value.InternalLightningByCryptoCode.TryGetValue(cryptoCode, out var internalLightningNode))
{
return internalLightningNode;
var result = await _authorizationService.AuthorizeAsync(HttpContext.User, null,
new PolicyRequirement(Policies.CanUseInternalLightningNode));
return result.Succeeded ? internalLightningNode : null;
}
return null;

@ -60,7 +60,7 @@ else
{
<li><hr class="dropdown-divider"></li>
}
<li ><a asp-controller="UIUserStores" asp-action="CreateStore" class="dropdown-item @ViewData.IsActivePage(StoreNavPages.Create)" id="StoreSelectorCreate">Create Store</a></li>
<li><a asp-controller="UIUserStores" asp-action="CreateStore" class="dropdown-item @ViewData.IsActivePage(StoreNavPages.Create)" id="StoreSelectorCreate">Create Store</a></li>
@if (Model.ArchivedCount > 0)
{
<li><hr class="dropdown-divider"></li>

@ -48,7 +48,7 @@ namespace BTCPayServer.Controllers
[Route("rates/{baseCurrency}")]
[HttpGet]
[BitpayAPIConstraint]
public async Task<IActionResult> GetBaseCurrencyRates(string baseCurrency, CancellationToken cancellationToken)
public async Task<IActionResult> GetBaseCurrencyRates(string baseCurrency, string cryptoCode = null, CancellationToken cancellationToken = default)
{
var supportedMethods = CurrentStore.GetSupportedPaymentMethods(_networkProvider);
@ -57,16 +57,16 @@ namespace BTCPayServer.Controllers
var currencypairs = BuildCurrencyPairs(currencyCodes, baseCurrency);
var result = await GetRates2(currencypairs, null, cancellationToken);
var result = await GetRates2(currencypairs, null, cryptoCode, cancellationToken);
var rates = (result as JsonResult)?.Value as Rate[];
return rates == null ? result : Json(new DataWrapper<Rate[]>(rates));
}
[HttpGet("rates/{baseCurrency}/{currency}")]
[BitpayAPIConstraint]
public async Task<IActionResult> GetCurrencyPairRate(string baseCurrency, string currency, CancellationToken cancellationToken)
public async Task<IActionResult> GetCurrencyPairRate(string baseCurrency, string currency, string cryptoCode = null, CancellationToken cancellationToken = default)
{
var result = await GetRates2($"{baseCurrency}_{currency}", null, cancellationToken);
var result = await GetRates2($"{baseCurrency}_{currency}", null, cryptoCode, cancellationToken);
return (result as JsonResult)?.Value is not Rate[] rates
? result
: Json(new DataWrapper<Rate>(rates.First()));
@ -74,9 +74,9 @@ namespace BTCPayServer.Controllers
[HttpGet("rates")]
[BitpayAPIConstraint]
public async Task<IActionResult> GetRates(string currencyPairs, string storeId = null, CancellationToken cancellationToken = default)
public async Task<IActionResult> GetRates(string currencyPairs, string storeId = null, string cryptoCode = null, CancellationToken cancellationToken = default)
{
var result = await GetRates2(currencyPairs, storeId, cancellationToken);
var result = await GetRates2(currencyPairs, storeId, cryptoCode, cancellationToken);
return (result as JsonResult)?.Value is not Rate[] rates
? result
: Json(new DataWrapper<Rate[]>(rates));
@ -84,7 +84,7 @@ namespace BTCPayServer.Controllers
[AllowAnonymous]
[HttpGet("api/rates")]
public async Task<IActionResult> GetRates2(string currencyPairs, string storeId, CancellationToken cancellationToken)
public async Task<IActionResult> GetRates2(string currencyPairs, string storeId, string cryptoCode = null, CancellationToken cancellationToken = default)
{
var store = CurrentStore ?? await _storeRepo.FindStore(storeId);
if (store == null)
@ -95,7 +95,12 @@ namespace BTCPayServer.Controllers
}
if (currencyPairs == null)
{
currencyPairs = store.GetStoreBlob().GetDefaultCurrencyPairString();
var blob = store.GetStoreBlob();
currencyPairs = blob.GetDefaultCurrencyPairString();
if (string.IsNullOrEmpty(currencyPairs) && !string.IsNullOrWhiteSpace(cryptoCode))
{
currencyPairs = $"{blob.DefaultCurrency}_{cryptoCode}".ToUpperInvariant();
}
if (string.IsNullOrEmpty(currencyPairs))
{
var result = Json(new BitpayErrorsModel() { Error = "You need to setup the default currency pairs in 'Store Settings / Rates' or specify 'currencyPairs' query parameter (eg. BTC_USD,LTC_CAD)." });

@ -23,7 +23,7 @@ namespace BTCPayServer.Controllers.Greenfield
GenerateWalletRequest request)
{
AssertCryptoCodeWallet(cryptoCode, out var network, out var wallet);
AssertCryptoCodeWallet(cryptoCode, out var network, out _);
if (!_walletProvider.IsAvailable(network))
{

@ -122,10 +122,11 @@ namespace BTCPayServer.Controllers.Greenfield
try
{
var strategy = DerivationSchemeSettings.Parse(paymentMethod.DerivationScheme, network);
var strategy = network.GetDerivationSchemeParser().Parse(paymentMethod.DerivationScheme);
var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
var line = strategy.AccountDerivation.GetLineFor(deposit);
var line = strategy.GetLineFor(deposit);
var result = new OnChainPaymentMethodPreviewResultData();
for (var i = offset; i < amount; i++)
{
@ -134,8 +135,9 @@ namespace BTCPayServer.Controllers.Greenfield
new OnChainPaymentMethodPreviewResultData.OnChainPaymentMethodPreviewResultAddressItem()
{
KeyPath = deposit.GetKeyPath((uint)i).ToString(),
Address = address.ScriptPubKey.GetDestinationAddress(strategy.Network.NBitcoinNetwork)
.ToString()
Address =
network.NBXplorerNetwork.CreateAddress(strategy,deposit.GetKeyPath((uint)i), address.ScriptPubKey)
.ToString()
});
}
@ -168,10 +170,10 @@ namespace BTCPayServer.Controllers.Greenfield
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
DerivationSchemeSettings strategy;
DerivationStrategyBase strategy;
try
{
strategy = DerivationSchemeSettings.Parse(paymentMethodData.DerivationScheme, network);
strategy = network.GetDerivationSchemeParser().Parse(paymentMethodData.DerivationScheme);
}
catch
{
@ -181,7 +183,7 @@ namespace BTCPayServer.Controllers.Greenfield
}
var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
var line = strategy.AccountDerivation.GetLineFor(deposit);
var line = strategy.GetLineFor(deposit);
var result = new OnChainPaymentMethodPreviewResultData();
for (var i = offset; i < amount; i++)
{
@ -192,9 +194,9 @@ namespace BTCPayServer.Controllers.Greenfield
OnChainPaymentMethodPreviewResultAddressItem()
{
KeyPath = deposit.GetKeyPath((uint)i).ToString(),
Address = strategy.Network.NBXplorerNetwork.CreateAddress(strategy.AccountDerivation,
line.KeyPathTemplate.GetKeyPath((uint)i),
derivation.ScriptPubKey).ToString()
Address =
network.NBXplorerNetwork.CreateAddress(strategy,deposit.GetKeyPath((uint)i), derivation.ScriptPubKey)
.ToString()
});
}
@ -244,12 +246,13 @@ namespace BTCPayServer.Controllers.Greenfield
{
var store = Store;
var storeBlob = store.GetStoreBlob();
var strategy = DerivationSchemeSettings.Parse(request.DerivationScheme, network);
var strategy = network.GetDerivationSchemeParser().Parse(request.DerivationScheme);
if (strategy != null)
await wallet.TrackAsync(strategy.AccountDerivation);
strategy.Label = request.Label;
var signing = strategy.GetSigningAccountKeySettings();
if (request.AccountKeyPath is RootedKeyPath r)
await wallet.TrackAsync(strategy);
var dss = new DerivationSchemeSettings(strategy, network) {Label = request.Label,};
var signing = dss.GetSigningAccountKeySettings();
if (request.AccountKeyPath is { } r)
{
signing.AccountKeyPath = r.KeyPath;
signing.RootFingerprint = r.MasterFingerprint;
@ -260,7 +263,7 @@ namespace BTCPayServer.Controllers.Greenfield
signing.RootFingerprint = null;
}
store.SetSupportedPaymentMethod(id, strategy);
store.SetSupportedPaymentMethod(id, dss);
storeBlob.SetExcluded(id, !request.Enabled);
store.SetStoreBlob(storeBlob);
await _storeRepository.UpdateStore(store);

@ -117,7 +117,7 @@ namespace BTCPayServer.Controllers.Greenfield
public async Task<IActionResult> GetOnChainFeeRate(string storeId, string cryptoCode, int? blockTarget = null)
{
if (IsInvalidWalletRequest(cryptoCode, out var network,
out var derivationScheme, out var actionResult))
out _, out var actionResult))
return actionResult;
var feeRateTarget = blockTarget ?? Store.GetStoreBlob().RecommendedFeeBlockTarget;
@ -164,8 +164,8 @@ namespace BTCPayServer.Controllers.Greenfield
[HttpDelete("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/address")]
public async Task<IActionResult> UnReserveOnChainWalletReceiveAddress(string storeId, string cryptoCode)
{
if (IsInvalidWalletRequest(cryptoCode, out var network,
out var derivationScheme, out var actionResult))
if (IsInvalidWalletRequest(cryptoCode, out _,
out _, out var actionResult))
return actionResult;
var addr = await _walletReceiveService.UnReserveAddress(new WalletId(storeId, cryptoCode));

@ -78,7 +78,7 @@ namespace BTCPayServer.Controllers.Greenfield
private void ValidateWebhookRequest(StoreWebhookBaseData create)
{
if (!Uri.TryCreate(create?.Url, UriKind.Absolute, out var uri))
if (!Uri.TryCreate(create?.Url, UriKind.Absolute, out _))
ModelState.AddModelError(nameof(Url), "Invalid Url");
}

@ -240,7 +240,7 @@ namespace BTCPayServer.Controllers.Greenfield
}
if (!string.IsNullOrEmpty(request.DefaultPaymentMethod) &&
!PaymentMethodId.TryParse(request.DefaultPaymentMethod, out var defaultPaymentMethodId))
!PaymentMethodId.TryParse(request.DefaultPaymentMethod, out _))
{
ModelState.AddModelError(nameof(request.Name), "DefaultPaymentMethod is invalid");
}

@ -205,7 +205,7 @@ namespace BTCPayServer.Controllers
}
else
{
var incrementAccessFailedResult = await _userManager.AccessFailedAsync(user);
await _userManager.AccessFailedAsync(user);
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return View(model);

@ -391,10 +391,6 @@ namespace BTCPayServer.Controllers
return NotFound();
}
var store = GetCurrentStore();
var storeBlob = BTCPayServer.Data.StoreDataExtensions.GetStoreBlob(store);
var defaultCurrency = storeBlob.DefaultCurrency;
try
{
var assetBalancesData =
@ -583,7 +579,7 @@ namespace BTCPayServer.Controllers
try
{
if (custodian is ICanWithdraw withdrawableCustodian)
if (custodian is ICanWithdraw)
{
var config = custodianAccount.GetBlob();

@ -1,11 +1,8 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Mime;
using System.Net.WebSockets;
using System.Reflection.Metadata;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
@ -32,10 +29,8 @@ using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Routing;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using NBitpayClient;
using NBXplorer;
using Newtonsoft.Json.Linq;
using BitpayCreateInvoiceRequest = BTCPayServer.Models.BitpayCreateInvoiceRequest;
using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Controllers
@ -856,10 +851,8 @@ namespace BTCPayServer.Controllers
DefaultLang = lang ?? invoice.DefaultLanguage ?? storeBlob.DefaultLang ?? "en",
ShowPayInWalletButton = storeBlob.ShowPayInWalletButton,
ShowStoreHeader = storeBlob.ShowStoreHeader,
StoreBranding = new StoreBrandingViewModel(storeBlob)
{
CustomCSSLink = storeBlob.CustomCSS
},
StoreBranding = new StoreBrandingViewModel(storeBlob),
CustomCSSLink = storeBlob.CustomCSS,
CustomLogoLink = storeBlob.CustomLogo,
CheckoutType = invoice.CheckoutType ?? storeBlob.CheckoutType,
HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice",
@ -1149,63 +1142,34 @@ namespace BTCPayServer.Controllers
};
}
private SelectList GetPaymentMethodsSelectList()
{
var store = GetCurrentStore();
var excludeFilter = store.GetStoreBlob().GetExcludedPaymentMethods();
return new SelectList(store.GetSupportedPaymentMethods(_NetworkProvider)
.Where(s => !excludeFilter.Match(s.PaymentId))
.Select(method => new SelectListItem(method.PaymentId.ToPrettyString(), method.PaymentId.ToString())),
nameof(SelectListItem.Value),
nameof(SelectListItem.Text));
}
private bool AnyPaymentMethodAvailable(StoreData store)
{
var storeBlob = store.GetStoreBlob();
var excludeFilter = storeBlob.GetExcludedPaymentMethods();
return store.GetSupportedPaymentMethods(_NetworkProvider).Where(s => !excludeFilter.Match(s.PaymentId)).Any();
}
[HttpGet("/stores/{storeId}/invoices/create")]
[HttpGet("invoices/create")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> CreateInvoice(InvoicesModel? model = null)
{
if (model?.StoreId != null)
{
var store = await _StoreRepository.FindStore(model.StoreId, GetUserId());
if (store == null)
return NotFound();
if (!AnyPaymentMethodAvailable(store))
{
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 = store.Id })}' class='alert-link'>set up a wallet</a> first",
AllowDismiss = false
});
}
HttpContext.SetStoreData(store);
}
else
if (string.IsNullOrEmpty(model?.StoreId))
{
TempData[WellKnownTempData.ErrorMessage] = "You need to select a store before creating an invoice.";
return RedirectToAction(nameof(UIHomeController.Index), "UIHome");
}
var storeBlob = HttpContext.GetStoreData()?.GetStoreBlob();
var store = await _StoreRepository.FindStore(model.StoreId, GetUserId());
if (store == null)
return NotFound();
if (!store.AnyPaymentMethodAvailable(_NetworkProvider))
{
return NoPaymentMethodResult(store.Id);
}
var storeBlob = store.GetStoreBlob();
var vm = new CreateInvoiceModel
{
StoreId = model.StoreId,
Currency = storeBlob?.DefaultCurrency,
CheckoutType = storeBlob?.CheckoutType ?? CheckoutType.V2,
AvailablePaymentMethods = GetPaymentMethodsSelectList()
Currency = storeBlob.DefaultCurrency,
CheckoutType = storeBlob.CheckoutType,
AvailablePaymentMethods = GetPaymentMethodsSelectList(store)
};
return View(vm);
@ -1218,9 +1182,14 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model, CancellationToken cancellationToken)
{
var store = HttpContext.GetStoreData();
if (!store.AnyPaymentMethodAvailable(_NetworkProvider))
{
return NoPaymentMethodResult(store.Id);
}
var storeBlob = store.GetStoreBlob();
model.CheckoutType = storeBlob.CheckoutType;
model.AvailablePaymentMethods = GetPaymentMethodsSelectList();
model.AvailablePaymentMethods = GetPaymentMethodsSelectList(store);
JObject? metadataObj = null;
if (!string.IsNullOrEmpty(model.Metadata))
@ -1239,18 +1208,6 @@ namespace BTCPayServer.Controllers
{
return View(model);
}
if (!AnyPaymentMethodAvailable(store))
{
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 = store.Id })}' class='alert-link'>set up a wallet</a> first",
AllowDismiss = false
});
return View(model);
}
try
{
var metadata = metadataObj is null ? new InvoiceMetadata() : InvoiceMetadata.FromJObject(metadataObj);
@ -1396,5 +1353,26 @@ namespace BTCPayServer.Controllers
}
}
}
private SelectList GetPaymentMethodsSelectList(StoreData store)
{
var excludeFilter = store.GetStoreBlob().GetExcludedPaymentMethods();
return new SelectList(store.GetSupportedPaymentMethods(_NetworkProvider)
.Where(s => !excludeFilter.Match(s.PaymentId))
.Select(method => new SelectListItem(method.PaymentId.ToPrettyString(), method.PaymentId.ToString())),
nameof(SelectListItem.Value),
nameof(SelectListItem.Text));
}
private IActionResult NoPaymentMethodResult(string 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",
AllowDismiss = false
});
return RedirectToAction(nameof(ListInvoices), new { storeId });
}
}
}

@ -227,7 +227,6 @@ namespace BTCPayServer.Controllers
entity.Status = InvoiceStatusLegacy.New;
entity.UpdateTotals();
HashSet<CurrencyPair> currencyPairsToFetch = new HashSet<CurrencyPair>();
var rules = storeBlob.GetRateRules(_NetworkProvider);
var excludeFilter = storeBlob.GetExcludedPaymentMethods(); // Here we can compose filters from other origin with PaymentFilter.Any()
if (invoicePaymentMethodFilter != null)
{

@ -3,7 +3,9 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Form;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
@ -39,6 +41,7 @@ namespace BTCPayServer.Controllers
private readonly DisplayFormatter _displayFormatter;
private readonly InvoiceRepository _InvoiceRepository;
private readonly StoreRepository _storeRepository;
private readonly BTCPayNetworkProvider _networkProvider;
private FormComponentProviders FormProviders { get; }
public FormDataService FormDataService { get; }
@ -54,7 +57,8 @@ namespace BTCPayServer.Controllers
StoreRepository storeRepository,
InvoiceRepository invoiceRepository,
FormComponentProviders formProviders,
FormDataService formDataService)
FormDataService formDataService,
BTCPayNetworkProvider networkProvider)
{
_InvoiceController = invoiceController;
_UserManager = userManager;
@ -67,6 +71,7 @@ namespace BTCPayServer.Controllers
_InvoiceRepository = invoiceRepository;
FormProviders = formProviders;
FormDataService = formDataService;
_networkProvider = networkProvider;
}
[HttpGet("/stores/{storeId}/payment-requests")]
@ -107,12 +112,20 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> EditPaymentRequest(string storeId, string payReqId)
{
var store = GetCurrentStore();
if (store == null)
{
return NotFound();
}
var paymentRequest = GetCurrentPaymentRequest();
if (paymentRequest == null && !string.IsNullOrEmpty(payReqId))
{
return NotFound();
}
if (!store.AnyPaymentMethodAvailable(_networkProvider))
{
return NoPaymentMethodResult(storeId);
}
var storeBlob = store.GetStoreBlob();
var prInvoices = payReqId is null ? null : (await _PaymentRequestService.GetPaymentRequest(payReqId, GetUserId())).Invoices;
var vm = new UpdatePaymentRequestViewModel(paymentRequest)
@ -143,7 +156,11 @@ namespace BTCPayServer.Controllers
{
return NotFound();
}
if (!store.AnyPaymentMethodAvailable(_networkProvider))
{
return NoPaymentMethodResult(store.Id);
}
if (paymentRequest?.Archived is true && viewModel.Archived)
{
ModelState.AddModelError(string.Empty, "You cannot edit an archived payment request.");
@ -441,5 +458,16 @@ namespace BTCPayServer.Controllers
private StoreData GetCurrentStore() => HttpContext.GetStoreData();
private PaymentRequestData GetCurrentPaymentRequest() => HttpContext.GetPaymentRequestData();
private IActionResult NoPaymentMethodResult(string 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",
AllowDismiss = false
});
return RedirectToAction(nameof(GetPaymentRequests), new { storeId });
}
}
}

@ -55,7 +55,7 @@ namespace BTCPayServer.Controllers
public IEnumerable<PluginService.AvailablePlugin> Available { get; set; }
public (string command, string plugin)[] Commands { get; set; }
public bool CanShowRestart { get; set; }
public string[] Disabled { get; set; }
public Dictionary<string, Version> Disabled { get; set; }
public Dictionary<string, AvailablePlugin> DownloadedPluginsByIdentifier { get; set; } = new Dictionary<string, AvailablePlugin>();
}

@ -168,37 +168,31 @@ namespace BTCPayServer.Controllers
return View(vm);
}
var builder = new UriBuilder();
using (var client = new HttpClient(new HttpClientHandler()
try
{
ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
}))
{
try
{
builder.Scheme = this.Request.Scheme;
builder.Host = vm.DNSDomain;
var addresses1 = GetAddressAsync(this.Request.Host.Host);
var addresses2 = GetAddressAsync(vm.DNSDomain);
await Task.WhenAll(addresses1, addresses2);
builder.Scheme = this.Request.Scheme;
builder.Host = vm.DNSDomain;
var addresses1 = GetAddressAsync(this.Request.Host.Host);
var addresses2 = GetAddressAsync(vm.DNSDomain);
await Task.WhenAll(addresses1, addresses2);
var addressesSet = addresses1.GetAwaiter().GetResult().Select(c => c.ToString()).ToHashSet();
var hasCommonAddress = addresses2.GetAwaiter().GetResult().Select(c => c.ToString()).Any(s => addressesSet.Contains(s));
if (!hasCommonAddress)
{
ModelState.AddModelError(nameof(vm.DNSDomain), $"Invalid host ({vm.DNSDomain} is not pointing to this BTCPay instance)");
return View(vm);
}
}
catch (Exception ex)
var addressesSet = addresses1.GetAwaiter().GetResult().Select(c => c.ToString()).ToHashSet();
var hasCommonAddress = addresses2.GetAwaiter().GetResult().Select(c => c.ToString()).Any(s => addressesSet.Contains(s));
if (!hasCommonAddress)
{
var messages = new List<object>();
messages.Add(ex.Message);
if (ex.InnerException != null)
messages.Add(ex.InnerException.Message);
ModelState.AddModelError(nameof(vm.DNSDomain), $"Invalid domain ({string.Join(", ", messages.ToArray())})");
ModelState.AddModelError(nameof(vm.DNSDomain), $"Invalid host ({vm.DNSDomain} is not pointing to this BTCPay instance)");
return View(vm);
}
}
catch (Exception ex)
{
var messages = new List<object>();
messages.Add(ex.Message);
if (ex.InnerException != null)
messages.Add(ex.InnerException.Message);
ModelState.AddModelError(nameof(vm.DNSDomain), $"Invalid domain ({string.Join(", ", messages.ToArray())})");
return View(vm);
}
var error = await RunSSH(vm, $"changedomain.sh {vm.DNSDomain}");
if (error != null)
@ -685,7 +679,7 @@ namespace BTCPayServer.Controllers
[HttpPost]
public async Task<IActionResult> ServicePost(string serviceName, string cryptoCode)
{
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
if (!_dashBoard.IsFullySynched(cryptoCode, out _))
{
TempData[WellKnownTempData.ErrorMessage] = $"{cryptoCode} is not fully synched";
return RedirectToAction(nameof(Services));

@ -74,11 +74,10 @@ namespace BTCPayServer.Controllers
var rule = vm.Rules[i];
if (!string.IsNullOrEmpty(rule.To) && (rule.To.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Any(s => !MailboxAddressValidator.TryParse(s, out var mb))))
.Any(s => !MailboxAddressValidator.TryParse(s, out _))))
{
ModelState.AddModelError($"{nameof(vm.Rules)}[{i}].{nameof(rule.To)}",
"Invalid mailbox address provided. Valid formats are: 'test@example.com' or 'Firstname Lastname <test@example.com>'");
}
else if (!rule.CustomerEmail && string.IsNullOrEmpty(rule.To))
ModelState.AddModelError($"{nameof(vm.Rules)}[{i}].{nameof(rule.To)}",
@ -114,8 +113,17 @@ namespace BTCPayServer.Controllers
var emailSender = await _emailSenderFactory.GetEmailSender(store.Id);
if (await IsSetupComplete(emailSender))
{
emailSender.SendEmail(MailboxAddress.Parse(rule.To), $"({store.StoreName} test) {rule.Subject}", rule.Body);
message += $"Test email sent to {rule.To} — please verify you received it.";
var recipients = rule.To.Split(",", StringSplitOptions.RemoveEmptyEntries)
.Select(o =>
{
MailboxAddressValidator.TryParse(o, out var mb);
return mb;
})
.Where(o => o != null)
.ToArray();
emailSender.SendEmail(recipients.ToArray(), null, null, $"({store.StoreName} test) {rule.Subject}", rule.Body);
message += "Test email sent — please verify you received it.";
}
else
{

@ -90,15 +90,23 @@ namespace BTCPayServer.Controllers
if (vm.WalletFile != null)
{
if (!DerivationSchemeSettings.TryParseFromWalletFile(await ReadAllText(vm.WalletFile), network, out strategy, out var error))
string fileContent = null;
try
{
ModelState.AddModelError(nameof(vm.WalletFile), $"Importing wallet failed: {error}");
fileContent = await ReadAllText(vm.WalletFile);
}
catch
{
}
if (fileContent is null || !_onChainWalletParsers.TryParseWalletFile(fileContent, network, out strategy, out _))
{
ModelState.AddModelError(nameof(vm.WalletFile), $"Import failed, make sure you import a compatible wallet format");
return View(vm.ViewName, vm);
}
}
else if (!string.IsNullOrEmpty(vm.WalletFileContent))
{
if (!DerivationSchemeSettings.TryParseFromWalletFile(vm.WalletFileContent, network, out strategy, out var error))
if (!_onChainWalletParsers.TryParseWalletFile(vm.WalletFileContent, network, out strategy, out var error))
{
ModelState.AddModelError(nameof(vm.WalletFileContent), $"QR import failed: {error}");
return View(vm.ViewName, vm);
@ -191,7 +199,7 @@ namespace BTCPayServer.Controllers
[HttpGet("{storeId}/onchain/{cryptoCode}/generate/{method?}")]
public async Task<IActionResult> GenerateWallet(WalletSetupViewModel vm)
{
var checkResult = IsAvailable(vm.CryptoCode, out var store, out var network);
var checkResult = IsAvailable(vm.CryptoCode, out _, out var network);
if (checkResult != null)
{
return checkResult;
@ -231,7 +239,7 @@ namespace BTCPayServer.Controllers
[HttpPost("{storeId}/onchain/{cryptoCode}/generate/{method}")]
public async Task<IActionResult> GenerateWallet(string storeId, string cryptoCode, WalletSetupMethod method, WalletSetupRequest request)
{
var checkResult = IsAvailable(cryptoCode, out var store, out var network);
var checkResult = IsAvailable(cryptoCode, out _, out var network);
if (checkResult != null)
{
return checkResult;

@ -71,7 +71,8 @@ namespace BTCPayServer.Controllers
IOptions<ExternalServicesOptions> externalServiceOptions,
IHtmlHelper html,
LightningClientFactoryService lightningClientFactoryService,
EmailSenderFactory emailSenderFactory)
EmailSenderFactory emailSenderFactory,
WalletFileParsers onChainWalletParsers)
{
_RateFactory = rateFactory;
_Repo = repo;
@ -97,6 +98,7 @@ namespace BTCPayServer.Controllers
_externalServiceOptions = externalServiceOptions;
_lightningClientFactoryService = lightningClientFactoryService;
_emailSenderFactory = emailSenderFactory;
_onChainWalletParsers = onChainWalletParsers;
Html = html;
}
@ -121,6 +123,7 @@ namespace BTCPayServer.Controllers
private readonly IOptions<ExternalServicesOptions> _externalServiceOptions;
private readonly LightningClientFactoryService _lightningClientFactoryService;
private readonly EmailSenderFactory _emailSenderFactory;
private readonly WalletFileParsers _onChainWalletParsers;
public string? GeneratedPairingCode { get; set; }
public WebhookSender WebhookNotificationManager { get; }
@ -501,7 +504,7 @@ namespace BTCPayServer.Controllers
var methodCriterion = model.PaymentMethodCriteria[index];
if (!string.IsNullOrWhiteSpace(methodCriterion.Value))
{
if (!CurrencyValue.TryParse(methodCriterion.Value, out var value))
if (!CurrencyValue.TryParse(methodCriterion.Value, out _))
{
model.AddModelError(viewModel => viewModel.PaymentMethodCriteria[index].Value,
$"{methodCriterion.PaymentMethod}: Invalid format. Make sure to enter a valid amount and currency code. Examples: '5 USD', '0.001 BTC'", this);
@ -671,7 +674,7 @@ namespace BTCPayServer.Controllers
});
break;
case LNURLPayPaymentType lnurlPayPaymentType:
case LNURLPayPaymentType:
break;
case LightningPaymentType _:
@ -997,7 +1000,7 @@ namespace BTCPayServer.Controllers
var store = model.StoreId switch
{
null => CurrentStore,
string id => await _Repo.FindStore(storeId, userId)
_ => await _Repo.FindStore(storeId, userId)
};
if (store == null)
return Challenge(AuthenticationSchemes.Cookie);

@ -90,7 +90,7 @@ namespace BTCPayServer.Controllers
await _repo.CreateStore(GetUserId(), store);
CreatedStoreId = store.Id;
TempData[WellKnownTempData.SuccessMessage] = "Store successfully created";
return RedirectToAction(nameof(UIStoresController.Dashboard), "UIStores", new
return RedirectToAction(nameof(UIStoresController.Index), "UIStores", new
{
storeId = store.Id
});

@ -146,7 +146,6 @@ namespace BTCPayServer.Controllers
return NotFound();
var txObjId = new WalletObjectId(walletId, WalletObjectData.Types.Tx, transactionId);
var wallet = _walletProvider.GetWallet(paymentMethod.Network);
if (addlabel != null)
{
await WalletRepository.AddWalletObjectLabels(txObjId, addlabel);
@ -414,7 +413,6 @@ namespace BTCPayServer.Controllers
await ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode).WaitServerStartedAsync();
await Task.Delay(1000);
await using var conn = await factory.OpenConnection();
var wallet_id = paymentMethod.GetNBXWalletId();
var txIds = sending.Select(s => s.Result.ToString()).ToArray();
await conn.ExecuteAsync(

@ -60,6 +60,14 @@ namespace BTCPayServer.Data
return result;
}
public static bool AnyPaymentMethodAvailable(this StoreData storeData, BTCPayNetworkProvider networkProvider)
{
var storeBlob = GetStoreBlob(storeData);
var excludeFilter = storeBlob.GetExcludedPaymentMethods();
return GetSupportedPaymentMethods(storeData, networkProvider).Where(s => !excludeFilter.Match(s.PaymentId)).Any();
}
public static bool SetStoreBlob(this StoreData storeData, StoreBlob storeBlob)
{
var original = new Serializer(null).ToString(storeData.GetStoreBlob());

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using NBitcoin;
using NBitcoin.Scripting;
@ -53,6 +54,8 @@ namespace BTCPayServer
ArgumentNullException.ThrowIfNull(str);
str = str.Trim();
//nbitcoin output descriptor does not support taproot, so let's check if it is a taproot descriptor and fake until it is supported
var outputDescriptor = OutputDescriptor.Parse(str, Network);
switch (outputDescriptor)
{
@ -81,8 +84,10 @@ namespace BTCPayServer
return (Parse(ds.Item1 + suffix), ds.Item2);
};
throw new FormatException("sh descriptors are only supported with multsig(legacy or p2wsh) and segwit(p2wpkh)");
case OutputDescriptor.Tr tr:
return ExtractFromPkProvider(tr.InnerPubkey, "-[taproot]");
case OutputDescriptor.WPKH wpkh:
return ExtractFromPkProvider(wpkh.PkProvider, "");
return ExtractFromPkProvider(wpkh.PkProvider);
case OutputDescriptor.WSH { Inner: OutputDescriptor.Multi multi }:
return ExtractFromMulti(multi);
case OutputDescriptor.WSH:
@ -91,42 +96,11 @@ namespace BTCPayServer
throw new ArgumentOutOfRangeException(nameof(outputDescriptor));
}
}
public DerivationStrategyBase ParseElectrum(string str)
{
ArgumentNullException.ThrowIfNull(str);
str = str.Trim();
var data = Network.GetBase58CheckEncoder().DecodeData(str);
if (data.Length < 4)
throw new FormatException();
var prefix = Utils.ToUInt32(data, false);
var standardPrefix = Utils.ToBytes(0x0488b21eU, false);
for (int ii = 0; ii < 4; ii++)
data[ii] = standardPrefix[ii];
var extPubKey = GetBitcoinExtPubKeyByNetwork(Network, data);
if (!BtcPayNetwork.ElectrumMapping.TryGetValue(prefix, out var type))
{
throw new FormatException();
}
if (type == DerivationType.Segwit)
return new DirectDerivationStrategy(extPubKey, true);
if (type == DerivationType.Legacy)
return new DirectDerivationStrategy(extPubKey, false);
if (type == DerivationType.SegwitP2SH)
return BtcPayNetwork.NBXplorerNetwork.DerivationStrategyFactory.Parse(extPubKey.ToString() + "-[p2sh]");
throw new FormatException();
}
public DerivationStrategyBase Parse(string str)
{
ArgumentNullException.ThrowIfNull(str);
str = str.Trim();
HashSet<string> hintedLabels = new HashSet<string>();
if (!Network.Consensus.SupportSegwit)
{
hintedLabels.Add("legacy");
@ -163,7 +137,6 @@ namespace BTCPayServer
if (data.Length < 4)
continue;
var prefix = Utils.ToUInt32(data, false);
var standardPrefix = Utils.ToBytes(0x0488b21eU, false);
for (int ii = 0; ii < 4; ii++)
data[ii] = standardPrefix[ii];
@ -192,10 +165,35 @@ namespace BTCPayServer
{
str = $"{str}-[{label}]";
}
return BtcPayNetwork.NBXplorerNetwork.DerivationStrategyFactory.Parse(str);
}
internal DerivationStrategyBase ParseElectrum(string str)
{
ArgumentNullException.ThrowIfNull(str);
str = str.Trim();
var data = Network.GetBase58CheckEncoder().DecodeData(str);
if (data.Length < 4)
throw new FormatException();
var prefix = Utils.ToUInt32(data, false);
var standardPrefix = Utils.ToBytes(0x0488b21eU, false);
for (int ii = 0; ii < 4; ii++)
data[ii] = standardPrefix[ii];
var extPubKey = GetBitcoinExtPubKeyByNetwork(Network, data);
if (!BtcPayNetwork.ElectrumMapping.TryGetValue(prefix, out var type))
{
throw new FormatException();
}
if (type == DerivationType.Segwit)
return new DirectDerivationStrategy(extPubKey, true);
if (type == DerivationType.Legacy)
return new DirectDerivationStrategy(extPubKey, false);
if (type == DerivationType.SegwitP2SH)
return BtcPayNetwork.NBXplorerNetwork.DerivationStrategyFactory.Parse(extPubKey.ToString() + "-[p2sh]");
throw new FormatException();
}
public static BitcoinExtPubKey GetBitcoinExtPubKeyByNetwork(Network network, byte[] data)
{
try

@ -1,15 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using BTCPayServer.Payments;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBXplorer.Client;
using NBXplorer.DerivationStrategy;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer
{
@ -17,18 +13,17 @@ namespace BTCPayServer
{
public static DerivationSchemeSettings Parse(string derivationStrategy, BTCPayNetwork network)
{
string error = null;
ArgumentNullException.ThrowIfNull(network);
ArgumentNullException.ThrowIfNull(derivationStrategy);
var result = new DerivationSchemeSettings();
result.Network = network;
var parser = new DerivationSchemeParser(network);
if (TryParseXpub(derivationStrategy, parser, ref result, ref error, false) || TryParseXpub(derivationStrategy, parser, ref result, ref error, true))
var result = new DerivationSchemeSettings { Network = network };
var parser = network.GetDerivationSchemeParser();
if (parser.TryParseXpub(derivationStrategy, ref result) ||
parser.TryParseXpub(derivationStrategy, ref result, electrum: true))
{
return result;
}
throw new FormatException($"Invalid Derivation Scheme: {error}");
throw new FormatException($"Invalid Derivation Scheme");
}
public static bool TryParseFromJson(string config, BTCPayNetwork network, out DerivationSchemeSettings strategy)
@ -50,299 +45,6 @@ namespace BTCPayServer
return AccountDerivation is null ? null : DBUtils.nbxv1_get_wallet_id(Network.CryptoCode, AccountDerivation.ToString());
}
private static bool TryParseXpub(string xpub, DerivationSchemeParser derivationSchemeParser, ref DerivationSchemeSettings derivationSchemeSettings, ref string error, bool electrum = true)
{
if (!electrum)
{
var isOD = Regex.Match(xpub, @"\(.*?\)").Success;
try
{
var result = derivationSchemeParser.ParseOutputDescriptor(xpub);
derivationSchemeSettings.AccountOriginal = xpub.Trim();
derivationSchemeSettings.AccountDerivation = result.Item1;
derivationSchemeSettings.AccountKeySettings = result.Item2.Select((path, i) => new AccountKeySettings()
{
RootFingerprint = path?.MasterFingerprint,
AccountKeyPath = path?.KeyPath,
AccountKey = result.Item1.GetExtPubKeys().ElementAt(i).GetWif(derivationSchemeParser.Network)
}).ToArray();
return true;
}
catch (Exception exception)
{
error = exception.Message;
if (isOD)
{
return false;
} // otherwise continue and try to parse input as xpub
}
}
try
{
// Extract fingerprint and account key path from export formats that contain them.
// Possible formats: [fingerprint/account_key_path]xpub, [fingerprint]xpub, xpub
HDFingerprint? rootFingerprint = null;
KeyPath accountKeyPath = null;
var derivationRegex = new Regex(@"^(?:\[(\w+)(?:\/(.*?))?\])?(\w+)$", RegexOptions.IgnoreCase);
var match = derivationRegex.Match(xpub.Trim());
if (match.Success)
{
if (!string.IsNullOrEmpty(match.Groups[1].Value))
rootFingerprint = HDFingerprint.Parse(match.Groups[1].Value);
if (!string.IsNullOrEmpty(match.Groups[2].Value))
accountKeyPath = KeyPath.Parse(match.Groups[2].Value);
if (!string.IsNullOrEmpty(match.Groups[3].Value))
xpub = match.Groups[3].Value;
}
derivationSchemeSettings.AccountOriginal = xpub.Trim();
derivationSchemeSettings.AccountDerivation = electrum ? derivationSchemeParser.ParseElectrum(derivationSchemeSettings.AccountOriginal) : derivationSchemeParser.Parse(derivationSchemeSettings.AccountOriginal);
derivationSchemeSettings.AccountKeySettings = derivationSchemeSettings.AccountDerivation.GetExtPubKeys()
.Select(key => new AccountKeySettings
{
AccountKey = key.GetWif(derivationSchemeParser.Network)
}).ToArray();
if (derivationSchemeSettings.AccountDerivation is DirectDerivationStrategy direct && !direct.Segwit)
derivationSchemeSettings.AccountOriginal = null; // Saving this would be confusing for user, as xpub of electrum is legacy derivation, but for btcpay, it is segwit derivation
// apply initial matches if there were no results from parsing
if (rootFingerprint != null && derivationSchemeSettings.AccountKeySettings[0].RootFingerprint == null)
{
derivationSchemeSettings.AccountKeySettings[0].RootFingerprint = rootFingerprint;
}
if (accountKeyPath != null && derivationSchemeSettings.AccountKeySettings[0].AccountKeyPath == null)
{
derivationSchemeSettings.AccountKeySettings[0].AccountKeyPath = accountKeyPath;
}
return true;
}
catch (Exception exception)
{
error = exception.Message;
return false;
}
}
public static bool TryParseBSMSFile(string filecontent, DerivationSchemeParser derivationSchemeParser, ref DerivationSchemeSettings derivationSchemeSettings,
out string error)
{
error = null;
try
{
string[] lines = filecontent.Split(
new[] {"\r\n", "\r", "\n"},
StringSplitOptions.None
);
if (!lines[0].Trim().Equals("BSMS 1.0"))
{;
return false;
}
var descriptor = lines[1];
var derivationPath = lines[2].Split(',', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault()?? "/0/*";
if (derivationPath == "No path restrictions")
{
derivationPath = "/0/*";
}
if(derivationPath != "/0/*")
{
error = "BTCPay Server can only derive address to the deposit and change paths";
return false;
}
descriptor = descriptor.Replace("/**", derivationPath);
var testAddress = BitcoinAddress.Create( lines[3], derivationSchemeParser.Network);
var result = derivationSchemeParser.ParseOutputDescriptor(descriptor);
var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
var line = result.Item1.GetLineFor(deposit).Derive(0);
if (testAddress.ScriptPubKey != line.ScriptPubKey)
{
error = "BSMS test address did not match our generated address";
return false;
}
derivationSchemeSettings.Source = "BSMS";
derivationSchemeSettings.AccountDerivation = result.Item1;
derivationSchemeSettings.AccountOriginal = descriptor.Trim();
derivationSchemeSettings.AccountKeySettings = result.Item2.Select((path, i) => new AccountKeySettings()
{
RootFingerprint = path?.MasterFingerprint,
AccountKeyPath = path?.KeyPath,
AccountKey = result.Item1.GetExtPubKeys().ElementAt(i).GetWif(derivationSchemeParser.Network)
}).ToArray();
return true;
}
catch (Exception e)
{
error = $"BSMS parse error: {e.Message}";
return false;
}
}
public static bool TryParseFromWalletFile(string fileContents, BTCPayNetwork network, out DerivationSchemeSettings settings, out string error)
{
settings = null;
error = null;
ArgumentNullException.ThrowIfNull(fileContents);
ArgumentNullException.ThrowIfNull(network);
var result = new DerivationSchemeSettings();
var derivationSchemeParser = new DerivationSchemeParser(network);
JObject jobj;
try
{
if (HexEncoder.IsWellFormed(fileContents))
{
fileContents = Encoding.UTF8.GetString(Encoders.Hex.DecodeData(fileContents));
}
jobj = JObject.Parse(fileContents);
}
catch
{
if (TryParseBSMSFile(fileContents, derivationSchemeParser,ref result, out var bsmsError))
{
settings = result;
settings.Network = network;
return true;
}
if (bsmsError is not null)
{
error = bsmsError;
return false;
}
result.Source = "GenericFile";
if (TryParseXpub(fileContents, derivationSchemeParser, ref result, ref error) ||
TryParseXpub(fileContents, derivationSchemeParser, ref result, ref error, false))
{
settings = result;
settings.Network = network;
return true;
}
return false;
}
// Electrum
if (jobj.ContainsKey("keystore"))
{
result.Source = "ElectrumFile";
jobj = (JObject)jobj["keystore"];
if (!jobj.ContainsKey("xpub") ||
!TryParseXpub(jobj["xpub"].Value<string>(), derivationSchemeParser, ref result, ref error))
{
return false;
}
if (jobj.ContainsKey("label"))
{
try
{
result.Label = jobj["label"].Value<string>();
}
catch { return false; }
}
if (jobj.ContainsKey("ckcc_xfp"))
{
try
{
result.AccountKeySettings[0].RootFingerprint = new HDFingerprint(jobj["ckcc_xfp"].Value<uint>());
}
catch { return false; }
}
if (jobj.ContainsKey("derivation"))
{
try
{
result.AccountKeySettings[0].AccountKeyPath = new KeyPath(jobj["derivation"].Value<string>());
}
catch { return false; }
}
}
// Specter
else if (jobj.ContainsKey("descriptor") && jobj.ContainsKey("blockheight"))
{
result.Source = "SpecterFile";
if (!TryParseXpub(jobj["descriptor"].Value<string>(), derivationSchemeParser, ref result, ref error, false))
{
return false;
}
if (jobj.ContainsKey("label"))
{
try
{
result.Label = jobj["label"].Value<string>();
}
catch { return false; }
}
}
// Wasabi
else
{
result.Source = "WasabiFile";
if (!jobj.ContainsKey("ExtPubKey") ||
!TryParseXpub(jobj["ExtPubKey"].Value<string>(), derivationSchemeParser, ref result, ref error, false))
{
return false;
}
if (jobj.ContainsKey("MasterFingerprint"))
{
try
{
var mfpString = jobj["MasterFingerprint"].ToString().Trim();
// https://github.com/zkSNACKs/WalletWasabi/pull/1663#issuecomment-508073066
if (uint.TryParse(mfpString, out var fingerprint))
{
result.AccountKeySettings[0].RootFingerprint = new HDFingerprint(fingerprint);
}
else
{
var shouldReverseMfp = jobj.ContainsKey("ColdCardFirmwareVersion") &&
jobj["ColdCardFirmwareVersion"].ToString() == "2.1.0";
var bytes = Encoders.Hex.DecodeData(mfpString);
result.AccountKeySettings[0].RootFingerprint = shouldReverseMfp ? new HDFingerprint(bytes.Reverse().ToArray()) : new HDFingerprint(bytes);
}
}
catch { return false; }
}
if (jobj.ContainsKey("AccountKeyPath"))
{
try
{
result.AccountKeySettings[0].AccountKeyPath = new KeyPath(jobj["AccountKeyPath"].Value<string>());
}
catch { return false; }
}
if (jobj.ContainsKey("DerivationPath"))
{
try
{
result.AccountKeySettings[0].AccountKeyPath = new KeyPath(jobj["DerivationPath"].Value<string>().ToLowerInvariant());
}
catch { return false; }
}
if (jobj.ContainsKey("ColdCardFirmwareVersion"))
{
result.Source = "ColdCard";
}
if (jobj.ContainsKey("CoboVaultFirmwareVersion"))
{
result.Source = "CoboVault";
}
}
settings = result;
settings.Network = network;
return true;
}
public DerivationSchemeSettings()
{

@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
@ -9,6 +10,7 @@ using System.Net.WebSockets;
using System.Reflection;
using System.Security.Claims;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.BIP78.Sender;
@ -16,7 +18,6 @@ using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Lightning;
using BTCPayServer.Logging;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.NTag424;
@ -30,10 +31,9 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBitcoin.Payment;
using NBitpayClient;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json;
using InvoiceCryptoInfo = BTCPayServer.Services.Invoices.InvoiceCryptoInfo;
@ -42,6 +42,81 @@ namespace BTCPayServer
{
public static class Extensions
{
public static DerivationSchemeParser GetDerivationSchemeParser(this BTCPayNetwork network)
{
return new DerivationSchemeParser(network);
}
public static bool TryParseXpub(this DerivationSchemeParser derivationSchemeParser, string xpub,
ref DerivationSchemeSettings derivationSchemeSettings, bool electrum = false)
{
if (!electrum)
{
var isOD = Regex.Match(xpub, @"\(.*?\)").Success;
try
{
var result = derivationSchemeParser.ParseOutputDescriptor(xpub);
derivationSchemeSettings.AccountOriginal = xpub.Trim();
derivationSchemeSettings.AccountDerivation = result.Item1;
derivationSchemeSettings.AccountKeySettings = result.Item2.Select((path, i) => new AccountKeySettings()
{
RootFingerprint = path?.MasterFingerprint,
AccountKeyPath = path?.KeyPath,
AccountKey = result.Item1.GetExtPubKeys().ElementAt(i).GetWif(derivationSchemeParser.Network)
}).ToArray();
return true;
}
catch (Exception)
{
if (isOD)
{
return false;
} // otherwise continue and try to parse input as xpub
}
}
try
{
// Extract fingerprint and account key path from export formats that contain them.
// Possible formats: [fingerprint/account_key_path]xpub, [fingerprint]xpub, xpub
HDFingerprint? rootFingerprint = null;
KeyPath accountKeyPath = null;
var derivationRegex = new Regex(@"^(?:\[(\w+)(?:\/(.*?))?\])?(\w+)$", RegexOptions.IgnoreCase);
var match = derivationRegex.Match(xpub.Trim());
if (match.Success)
{
if (!string.IsNullOrEmpty(match.Groups[1].Value))
rootFingerprint = HDFingerprint.Parse(match.Groups[1].Value);
if (!string.IsNullOrEmpty(match.Groups[2].Value))
accountKeyPath = KeyPath.Parse(match.Groups[2].Value);
if (!string.IsNullOrEmpty(match.Groups[3].Value))
xpub = match.Groups[3].Value;
}
derivationSchemeSettings.AccountOriginal = xpub.Trim();
derivationSchemeSettings.AccountDerivation = electrum ? derivationSchemeParser.ParseElectrum(derivationSchemeSettings.AccountOriginal) : derivationSchemeParser.Parse(derivationSchemeSettings.AccountOriginal);
derivationSchemeSettings.AccountKeySettings = derivationSchemeSettings.AccountDerivation.GetExtPubKeys()
.Select(key => new AccountKeySettings
{
AccountKey = key.GetWif(derivationSchemeParser.Network)
}).ToArray();
if (derivationSchemeSettings.AccountDerivation is DirectDerivationStrategy direct && !direct.Segwit)
derivationSchemeSettings.AccountOriginal = null; // Saving this would be confusing for user, as xpub of electrum is legacy derivation, but for btcpay, it is segwit derivation
// apply initial matches if there were no results from parsing
if (rootFingerprint != null && derivationSchemeSettings.AccountKeySettings[0].RootFingerprint == null)
{
derivationSchemeSettings.AccountKeySettings[0].RootFingerprint = rootFingerprint;
}
if (accountKeyPath != null && derivationSchemeSettings.AccountKeySettings[0].AccountKeyPath == null)
{
derivationSchemeSettings.AccountKeySettings[0].AccountKeyPath = accountKeyPath;
}
return true;
}
catch (Exception)
{
return false;
}
}
public static CardKey CreatePullPaymentCardKey(this IssuerKey issuerKey, byte[] uid, int version, string pullPaymentId)
{
var data = Encoding.UTF8.GetBytes(pullPaymentId);
@ -92,7 +167,7 @@ namespace BTCPayServer
public static Uri GetServerUri(this ILightningClient client)
{
var kv = LightningConnectionStringHelper.ExtractValues(client.ToString(), out var type);
var kv = LightningConnectionStringHelper.ExtractValues(client.ToString(), out _);
return !kv.TryGetValue("server", out var server) ? null : new Uri(server, UriKind.Absolute);
}
@ -111,7 +186,7 @@ namespace BTCPayServer
public static bool IsSafe(this ILightningClient client)
{
var kv = LightningConnectionStringHelper.ExtractValues(client.ToString(), out var type);
var kv = LightningConnectionStringHelper.ExtractValues(client.ToString(), out _);
if (kv.TryGetValue("cookiefilepath", out _) ||
kv.TryGetValue("macaroondirectorypath", out _) ||
kv.TryGetValue("macaroonfilepath", out _) )

@ -87,7 +87,7 @@ namespace BTCPayServer.HostedServices
var remotePluginsList = remotePlugins
.GroupBy(plugin => plugin.Identifier)
.Select(group => group.OrderByDescending(plugin => plugin.Version).First())
.Where(pair => installedPlugins.ContainsKey(pair.Identifier) || disabledPlugins.Contains(pair.Name))
.Where(pair => installedPlugins.ContainsKey(pair.Identifier) || disabledPlugins.ContainsKey(pair.Name))
.ToDictionary(plugin => plugin.Identifier, plugin => plugin.Version);
var notify = new HashSet<string>();
foreach (var pair in remotePluginsList)
@ -95,8 +95,10 @@ namespace BTCPayServer.HostedServices
if (dh.LastVersions.TryGetValue(pair.Key, out var lastVersion) && lastVersion >= pair.Value)
continue;
if (installedPlugins.TryGetValue(pair.Key, out var installedVersion) && installedVersion < pair.Value)
{
notify.Add(pair.Key);
if (disabledPlugins.Contains(pair.Key))
}
else if (disabledPlugins.TryGetValue(pair.Key, out var disabledVersion) && disabledVersion < pair.Value)
{
notify.Add(pair.Key);
}

@ -59,15 +59,12 @@ public class StoreEmailRuleProcessorSender : EventHostedServiceBase
var sender = await _emailSenderFactory.GetEmailSender(storeWebhookEvent.StoreId);
foreach (UIStoresController.StoreEmailRule actionableRule in actionableRules)
{
var request = new SendEmailRequest()
var request = new SendEmailRequest
{
Subject = actionableRule.Subject, Body = actionableRule.Body, Email = actionableRule.To
};
request = await webhookDeliveryRequest.Interpolate(request, actionableRule);
var recipients = (request?.Email?.Split(",", StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty<string>())
.Select(o =>
{
@ -77,7 +74,7 @@ public class StoreEmailRuleProcessorSender : EventHostedServiceBase
.Where(o => o != null)
.ToArray();
if(recipients.Length == 0)
if (recipients.Length == 0)
continue;
sender.SendEmail(recipients.ToArray(), null, null, request.Subject, request.Body);

@ -1,11 +1,9 @@
using System;
using System.Globalization;
using System.Globalization;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Services.Invoices;
using Newtonsoft.Json.Linq;
using WebhookDeliveryData = BTCPayServer.Data.WebhookDeliveryData;
namespace BTCPayServer.HostedServices.Webhooks;
@ -46,7 +44,7 @@ public class InvoiceWebhookDeliveryRequest : WebhookSender.WebhookDeliveryReques
.Replace("{Invoice.OrderId}", Invoice.Metadata.OrderId);
res = InterpolateJsonField(str, "Invoice.Metadata", Invoice.Metadata.ToJObject());
res = InterpolateJsonField(res, "Invoice.Metadata", Invoice.Metadata.ToJObject());
return res;
}

@ -37,14 +37,14 @@ public class PaymentRequestWebhookDeliveryRequest : WebhookSender.WebhookDeliver
private string Interpolate(string str, PaymentRequestBaseData blob)
{
var res= str.Replace("{PaymentRequest.Id}", _evt.Data.Id)
var res = str.Replace("{PaymentRequest.Id}", _evt.Data.Id)
.Replace("{PaymentRequest.Price}", blob.Amount.ToString(CultureInfo.InvariantCulture))
.Replace("{PaymentRequest.Currency}", blob.Currency)
.Replace("{PaymentRequest.Title}", blob.Title)
.Replace("{PaymentRequest.Description}", blob.Description)
.Replace("{PaymentRequest.Status}", _evt.Data.Status.ToString());
res= InterpolateJsonField(res, "PaymentRequest.FormResponse", blob.FormResponse);
res = InterpolateJsonField(res, "PaymentRequest.FormResponse", blob.FormResponse);
return res;
}
}

@ -28,9 +28,9 @@ public class PaymentRequestWebhookProvider: WebhookProvider<PaymentRequestEvent>
public override WebhookEvent CreateTestEvent(string type, object[] args)
{
var storeId = args[0].ToString();
return new WebhookPayoutEvent(type, storeId)
return new WebhookPaymentRequestEvent(type, storeId)
{
PayoutId = "__test__" + Guid.NewGuid() + "__test__"
PaymentRequestId = "__test__" + Guid.NewGuid() + "__test__"
};
}

@ -23,7 +23,7 @@ public class PayoutWebhookDeliveryRequest(PayoutEvent evt, string? webhookId, We
private string Interpolate(string str)
{
var res= str.Replace("{Payout.Id}", evt.Payout.Id)
var res = str.Replace("{Payout.Id}", evt.Payout.Id)
.Replace("{Payout.PullPaymentId}", evt.Payout.PullPaymentDataId)
.Replace("{Payout.Destination}", evt.Payout.Destination)
.Replace("{Payout.State}", evt.Payout.State.ToString());

@ -73,6 +73,8 @@ using Newtonsoft.Json;
using NicolasDorier.RateLimits;
using Serilog;
using BTCPayServer.Services.Reporting;
using BTCPayServer.Services.WalletFileParsing;
#if ALTCOINS
using BTCPayServer.Services.Altcoins.Monero;
using BTCPayServer.Services.Altcoins.Zcash;
@ -129,7 +131,7 @@ namespace BTCPayServer.Hosting
services.AddSingleton<IHostedService>(provider => provider.GetRequiredService<TorServices>());
services.AddSingleton<ISwaggerProvider, DefaultSwaggerProvider>();
services.TryAddSingleton<SocketFactory>();
services.AddSingleton<Func<HttpClient, ILightningConnectionStringHandler>>(client =>
new ChargeLightningConnectionStringHandler(client));
services.AddSingleton<Func<HttpClient, ILightningConnectionStringHandler>>(_ =>
@ -145,8 +147,8 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton<LightningClientFactoryService>();
services.AddHttpClient(LightningClientFactoryService.OnionNamedClient)
.ConfigurePrimaryHttpMessageHandler<Socks5HttpClientHandler>();
services.TryAddSingleton<InvoicePaymentNotification>();
services.TryAddSingleton<BTCPayServerOptions>(o =>
o.GetRequiredService<IOptions<BTCPayServerOptions>>().Value);
@ -159,6 +161,9 @@ namespace BTCPayServer.Hosting
AddSettingsAccessor<PoliciesSettings>(services);
AddSettingsAccessor<ThemeSettings>(services);
//
AddOnchainWalletParsers(services);
services.AddStartupTask<BlockExplorerLinkStartupTask>();
services.TryAddSingleton<InvoiceRepository>();
services.AddSingleton<PaymentService>();
@ -246,7 +251,7 @@ namespace BTCPayServer.Hosting
{
error = e.Message;
}
if (error is not null)
{
logs.Configuration.LogWarning($"Invalid setting {net.CryptoCode}.lightning, " +
@ -363,7 +368,8 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton<WalletReceiveService>();
services.AddSingleton<IHostedService>(provider => provider.GetService<WalletReceiveService>());
services.TryAddSingleton<CurrencyNameTable>(CurrencyNameTable.Instance);
services.TryAddSingleton<IFeeProviderFactory,FeeProviderFactory>();
services.AddScheduledTask<FeeProviderFactory>(TimeSpan.FromMinutes(3.0));
services.AddSingleton<IFeeProviderFactory, FeeProviderFactory>(f => f.GetRequiredService<FeeProviderFactory>());
services.Configure<MvcOptions>((o) =>
{
@ -412,7 +418,7 @@ namespace BTCPayServer.Hosting
services.AddSingleton<NotificationManager>();
services.AddScoped<NotificationSender>();
services.AddSingleton<IHostedService, NBXplorerWaiters>();
services.AddSingleton<IHostedService, InvoiceEventSaverService>();
services.AddSingleton<IHostedService, BitpayIPNSender>();
@ -513,6 +519,19 @@ namespace BTCPayServer.Hosting
return services;
}
public static void AddOnchainWalletParsers(IServiceCollection services)
{
services.AddSingleton<WalletFileParsers>();
services.AddSingleton<IWalletFileParser, BSMSWalletFileParser>();
services.AddSingleton<IWalletFileParser, NBXDerivGenericWalletFileParser>();
services.AddSingleton<IWalletFileParser, ElectrumWalletFileParser>();
services.AddSingleton<IWalletFileParser, OutputDescriptorWalletFileParser>(provider => provider.GetService<OutputDescriptorWalletFileParser>());
services.AddSingleton<OutputDescriptorWalletFileParser>();
services.AddSingleton<IWalletFileParser, SpecterWalletFileParser>();
services.AddSingleton<IWalletFileParser, OutputDescriptorJsonWalletFileParser>();
services.AddSingleton<IWalletFileParser, WasabiWalletFileParser>();
}
internal static void RegisterRateSources(IServiceCollection services)
{
// We need to be careful to only add exchanges which OnGetTickers implementation make only 1 request

@ -475,9 +475,6 @@ namespace BTCPayServer.Hosting
continue;
var obj = new JObject();
obj.Add("color", label.Value);
var labelObjId = new WalletObjectId(WalletId.Parse(wallet.Id),
WalletObjectData.Types.Label,
labelId);
ctx.WalletObjects.Add(new WalletObjectData()
{
WalletId = wallet.Id,

@ -139,7 +139,7 @@ namespace BTCPayServer.Hosting
services.AddSingleton<UserLoginCodeService>();
services.AddSingleton<LnurlAuthService>();
services.AddSingleton<LightningAddressService>();
var mvcBuilder = services.AddMvc(o =>
services.AddMvc(o =>
{
o.Filters.Add(new XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.Deny));
o.Filters.Add(new XContentTypeOptionsAttribute("nosniff"));

@ -172,7 +172,6 @@ namespace BTCPayServer.Hosting
if (await otherContext.Settings.FirstOrDefaultAsync() == null)
return;
{
var postgres = new NpgsqlConnectionStringBuilder(p);
using var postgresContext = new ApplicationDbContext(new DbContextOptionsBuilder<ApplicationDbContext>().UseNpgsql(p, o =>
{
o.CommandTimeout(60 * 60 * 10);

@ -80,7 +80,7 @@ namespace BTCPayServer.PaymentRequest
await _PaymentRequestController.CancelUnpaidPendingInvoice(prId, false);
switch (result)
{
case OkObjectResult okObjectResult:
case OkObjectResult:
await Clients.Group(prId).SendCoreAsync(InvoiceCancelled, System.Array.Empty<object>());
break;

@ -411,7 +411,6 @@ namespace BTCPayServer.Payments.Bitcoin
private async Task<InvoiceEntity> ReceivedPayment(BTCPayWallet wallet, InvoiceEntity invoice, PaymentEntity payment, DerivationStrategyBase strategy)
{
var paymentData = (BitcoinLikePaymentData)payment.GetCryptoPaymentData();
invoice = (await UpdatePaymentStates(wallet, invoice.Id));
if (invoice == null)
return null;

@ -57,8 +57,7 @@ namespace BTCPayServer.Payments.Lightning
{
throw new PaymentMethodUnavailableException("LNURL requires a lightning node to be configured for the store.");
}
var client = lnSupported.CreateLightningClient(network, Options.Value, _lightningClientFactoryService);
var nodeInfo = (await _lightningLikePaymentHandler.GetNodeInfo(lnSupported, _networkProvider.GetNetwork<BTCPayNetwork>(supportedPaymentMethod.CryptoCode), logs, paymentMethod.PreferOnion)).FirstOrDefault();
return new LNURLPayPaymentMethodDetails()

@ -251,7 +251,7 @@ namespace BTCPayServer.Payments.Lightning
{
lock (_InstanceListeners)
{
foreach ((var key, var instance) in _InstanceListeners.ToArray())
foreach ((_, var instance) in _InstanceListeners.ToArray())
{
instance.RemoveExpiredInvoices();
if (!instance.Empty)
@ -285,8 +285,12 @@ namespace BTCPayServer.Payments.Lightning
}
if (oldDetails is LNURLPayPaymentMethodDetails lnurlPayPaymentMethodDetails && !string.IsNullOrEmpty(lnurlPayPaymentMethodDetails.BOLT11))
if (oldDetails is LNURLPayPaymentMethodDetails lnurlPayPaymentMethodDetails)
{
// LNUrlPay doesn't create a BOLT11 until it's actually scanned.
// So if no BOLT11 already created, which is likely the case, do nothing
if (string.IsNullOrEmpty(lnurlPayPaymentMethodDetails.BOLT11))
continue;
try
{
var client = _lightningLikePaymentHandler.CreateLightningClient(lnurlPayPaymentMethodDetails.LightningSupportedPaymentMethod,
@ -375,7 +379,6 @@ namespace BTCPayServer.Payments.Lightning
InvoiceEventData.EventSeverity.Error);
}
}
await _InvoiceRepository.AddInvoiceLogs(invoice.Id, logs);
_CheckInvoices.Writer.TryWrite(invoice.Id);
}

@ -42,9 +42,6 @@ public class UIPayoutProcessorsController : Controller
new PayoutProcessorService.PayoutProcessorQuery() { Stores = new[] { storeId } }))
.GroupBy(data => data.Processor);
var paymentMethods = HttpContext.GetStoreData().GetEnabledPaymentMethods(_btcPayNetworkProvider)
.Select(method => method.PaymentId).ToList();
return View(_payoutProcessorFactories.Select(factory =>
{
var conf = activeProcessors.FirstOrDefault(datas => datas.Key == factory.Processor)

@ -25,7 +25,6 @@ namespace BTCPayServer.Plugins.Altcoins
public override void Execute(IServiceCollection applicationBuilder)
{
var services = (PluginServiceCollection)applicationBuilder;
var onChain = new Payments.PaymentMethodId("BTC", Payments.PaymentTypes.BTCLike);
NBXplorerNetworkProvider = services.BootstrapServices.GetRequiredService<NBXplorerNetworkProvider>();
ChainName = NBXplorerNetworkProvider.NetworkType;

@ -21,7 +21,6 @@ namespace BTCPayServer.Plugins.Bitcoin
var nbxplorerNetworkProvider = services.BootstrapServices.GetRequiredService<NBXplorerNetworkProvider>();
var nbxplorerNetwork = nbxplorerNetworkProvider.GetFromCryptoCode("BTC");
var chainName = nbxplorerNetwork.NBitcoinNetwork.ChainName;
var selectedChains = services.BootstrapServices.GetRequiredService<SelectedChains>();
if (!services.BootstrapServices.GetRequiredService<SelectedChains>().Contains("BTC"))
return;
var blockExplorerLink = chainName == ChainName.Mainnet ? "https://mempool.space/tx/{0}" :

@ -60,10 +60,11 @@ namespace BTCPayServer.Plugins
pluginName = null;
return false;
}
public static IMvcBuilder AddPlugins(this IMvcBuilder mvcBuilder, IServiceCollection serviceCollection,
IConfiguration config, ILoggerFactory loggerFactory, ServiceProvider bootstrapServiceProvider)
{
void LoadPluginsFromAssemblies(Assembly systemAssembly1, HashSet<string> hashSet, HashSet<string> loadedPluginIdentifiers1,
void LoadPluginsFromAssemblies(Assembly systemAssembly1, HashSet<string> exclude, HashSet<string> loadedPluginIdentifiers1,
List<IBTCPayServerPlugin> btcPayServerPlugins)
{
// Load the referenced assembly plugins
@ -73,7 +74,7 @@ namespace BTCPayServer.Plugins
{
var assemblyName = assembly.GetName().Name;
bool isSystemPlugin = assembly == systemAssembly1;
if (!isSystemPlugin && hashSet.Contains(assemblyName))
if (!isSystemPlugin && exclude.Contains(assemblyName))
continue;
foreach (var plugin in GetPluginInstancesFromAssembly(assembly))
@ -101,14 +102,15 @@ namespace BTCPayServer.Plugins
Directory.CreateDirectory(pluginsFolder);
ExecuteCommands(pluginsFolder);
var disabledPlugins = GetDisabledPlugins(pluginsFolder);
var disabledPluginIdentifiers = GetDisabledPluginIdentifiers(pluginsFolder);
var systemAssembly = typeof(Program).Assembly;
LoadPluginsFromAssemblies(systemAssembly, disabledPlugins, loadedPluginIdentifiers, plugins);
LoadPluginsFromAssemblies(systemAssembly, disabledPluginIdentifiers, loadedPluginIdentifiers, plugins);
if (ExecuteCommands(pluginsFolder, plugins.ToDictionary(plugin => plugin.Identifier, plugin => plugin.Version)))
{
plugins.Clear();
LoadPluginsFromAssemblies(systemAssembly, disabledPlugins, loadedPluginIdentifiers, plugins);
loadedPluginIdentifiers.Clear();
LoadPluginsFromAssemblies(systemAssembly, disabledPluginIdentifiers, loadedPluginIdentifiers, plugins);
}
var pluginsToLoad = new List<(string PluginIdentifier, string PluginFilePath)>();
@ -134,7 +136,7 @@ namespace BTCPayServer.Plugins
var pluginFilePath = Path.Combine(directory, pluginIdentifier + ".dll");
if (!File.Exists(pluginFilePath))
continue;
if (disabledPlugins.Contains(pluginIdentifier))
if (disabledPluginIdentifiers.Contains(pluginIdentifier))
continue;
pluginsToLoad.Add((pluginIdentifier, pluginFilePath));
}
@ -287,6 +289,31 @@ namespace BTCPayServer.Plugins
return remainingCommands.Count != pendingCommands.Length;
}
private static Dictionary<string, (Version, IBTCPayServerPlugin.PluginDependency[] Dependencies, bool Disabled)> TryGetInstalledInfo(
string pluginsFolder)
{
var disabled = GetDisabledPluginIdentifiers(pluginsFolder);
var installed = new Dictionary<string, (Version, IBTCPayServerPlugin.PluginDependency[] Dependencies, bool Disabled)>();
foreach (string pluginDir in Directory.EnumerateDirectories(pluginsFolder))
{
var plugin = Path.GetFileName(pluginDir);
var dirName = Path.Combine(pluginsFolder, plugin);
var isDisabled = disabled.Contains(plugin);
var manifestFileName = Path.Combine(dirName, plugin + ".json");
if (File.Exists(manifestFileName))
{
var pluginManifest = JObject.Parse(File.ReadAllText(manifestFileName)).ToObject<PluginService.AvailablePlugin>();
installed.TryAdd(pluginManifest.Identifier, (pluginManifest.Version, pluginManifest.Dependencies, isDisabled));
}
else if (isDisabled)
{
// Disabled plugin might not have a manifest, but we still need to include
// it in the list, so that it can be shown on the Manage Plugins page
installed.TryAdd(plugin, (null, null, true));
}
}
return installed;
}
private static bool DependenciesMet(string pluginsFolder, string plugin, Dictionary<string, Version> installed)
{
@ -298,7 +325,7 @@ namespace BTCPayServer.Plugins
}
private static bool ExecuteCommand((string command, string extension) command, string pluginsFolder,
bool ignoreOrder = false, Dictionary<string, Version> installed = null)
bool ignoreOrder, Dictionary<string, Version> installed)
{
var dirName = Path.Combine(pluginsFolder, command.extension);
switch (command.command)
@ -306,12 +333,12 @@ namespace BTCPayServer.Plugins
case "update":
if (!DependenciesMet(pluginsFolder, command.extension, installed))
return false;
ExecuteCommand(("delete", command.extension), pluginsFolder, true);
ExecuteCommand(("install", command.extension), pluginsFolder, true);
ExecuteCommand(("delete", command.extension), pluginsFolder, true, installed);
ExecuteCommand(("install", command.extension), pluginsFolder, true, installed);
break;
case "delete":
ExecuteCommand(("enable", command.extension), pluginsFolder, true);
ExecuteCommand(("enable", command.extension), pluginsFolder, true, installed);
if (File.Exists(dirName))
{
File.Delete(dirName);
@ -334,7 +361,7 @@ namespace BTCPayServer.Plugins
if (!DependenciesMet(pluginsFolder, command.extension, installed))
return false;
ExecuteCommand(("enable", command.extension), pluginsFolder, true);
ExecuteCommand(("enable", command.extension), pluginsFolder, true, installed);
if (File.Exists(fileName))
{
ZipFile.ExtractToDirectory(fileName, dirName, true);
@ -425,12 +452,18 @@ namespace BTCPayServer.Plugins
QueueCommands(pluginDir, ("disable", plugin));
}
public static HashSet<string> GetDisabledPlugins(string pluginsFolder)
// Loads the list of disabled plugins from the file
private static HashSet<string> GetDisabledPluginIdentifiers(string pluginsFolder)
{
var disabledFilePath = Path.Combine(pluginsFolder, "disabled");
return File.Exists(disabledFilePath)
? File.ReadLines(disabledFilePath).ToHashSet()
: [];
var disabledPath = Path.Combine(pluginsFolder, "disabled");
return File.Exists(disabledPath) ? File.ReadAllLines(disabledPath).ToHashSet() : [];
}
// List of disabled plugins with additional info, like the disabled version and its dependencies
public static Dictionary<string, Version> GetDisabledPlugins(string pluginsFolder)
{
return TryGetInstalledInfo(pluginsFolder).Where(pair => pair.Value.Disabled)
.ToDictionary(pair => pair.Key, pair => pair.Value.Item1);
}
public static bool DependencyMet(IBTCPayServerPlugin.PluginDependency dependency,

@ -108,6 +108,7 @@ namespace BTCPayServer.Plugins
public void UninstallPlugin(string plugin)
{
var dest = _dataDirectories.Value.PluginDir;
PluginManager.CancelCommands(dest, plugin);
PluginManager.QueueCommands(dest, ("delete", plugin));
}
@ -155,9 +156,9 @@ namespace BTCPayServer.Plugins
PluginManager.CancelCommands(_dataDirectories.Value.PluginDir, plugin);
}
public string[] GetDisabledPlugins()
public Dictionary<string, Version> GetDisabledPlugins()
{
return PluginManager.GetDisabledPlugins(_dataDirectories.Value.PluginDir).ToArray();
return PluginManager.GetDisabledPlugins(_dataDirectories.Value.PluginDir);
}
}
}

@ -516,10 +516,13 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
}
}
var store = await _appService.GetStore(app);
var storeBlob = store.GetStoreBlob();
viewModel.FormName = formData.Name;
viewModel.Form = form;
viewModel.FormParameters = formParameters;
viewModel.StoreBranding = new StoreBrandingViewModel(storeBlob);
return View("Views/UIForms/View", viewModel);
}

@ -73,7 +73,7 @@ namespace BTCPayServer.Plugins.Shopify
public async Task RemoveWebhook(string id)
{
var req = CreateRequest(_credentials.ShopName, HttpMethod.Delete, $"webhooks/{id}.json");
var strResp = await SendRequest(req);
await SendRequest(req);
}
public async Task<string[]> CheckScopes()

@ -401,11 +401,21 @@ retry:
_ => "Template"
};
var settings = JObject.Parse(row.settings);
var items = JArray.Parse(settings[templatePath]!.Value<string>()!);
if (!settings.TryGetValue(templatePath, out var template))
return;
var items = template.Type switch
{
JTokenType.String => JArray.Parse(template.Value<string>()!),
JTokenType.Array => (JArray)template,
_ => null
};
if (items is null)
return;
bool hasChange = false;
foreach (var change in changes)
{
var item = items.FirstOrDefault(i => i["id"]?.Value<string>() == change.ItemId && i["inventory"] is not null && i["inventory"]!.Type is JTokenType.Integer);
var item = items.FirstOrDefault(i => i["id"]?.Value<string>() == change.ItemId && i["inventory"]?.Type is JTokenType.Integer);
if (item is null)
continue;
var inventory = item["inventory"]!.Value<int>();

@ -76,10 +76,10 @@ namespace BTCPayServer.Services.Apps
});
DefaultView = PosViewType.Static;
ShowCustomAmount = false;
ShowDiscount = true;
ShowDiscount = false;
ShowSearch = true;
ShowCategories = true;
EnableTips = true;
EnableTips = false;
RequiresRefundEmail = RequiresRefundEmail.InheritFromStore;
}
public string Title { get; set; }

@ -1,14 +1,18 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.HostedServices;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.Caching.Memory;
using NBitcoin;
namespace BTCPayServer.Services.Fees;
public class FeeProviderFactory : IFeeProviderFactory
public class FeeProviderFactory : IFeeProviderFactory, IPeriodicTask
{
public FeeProviderFactory(
BTCPayServerEnvironment Environment,
@ -16,7 +20,7 @@ public class FeeProviderFactory : IFeeProviderFactory
IHttpClientFactory HttpClientFactory,
IMemoryCache MemoryCache)
{
_FeeProviders = new ();
_FeeProviders = new();
// TODO: Pluginify this
foreach ((var network, var client) in ExplorerClients.GetAll())
@ -29,7 +33,10 @@ public class FeeProviderFactory : IFeeProviderFactory
$"MempoolSpaceFeeProvider-{network.CryptoCode}",
HttpClientFactory,
network is BTCPayNetwork n &&
n.NBitcoinNetwork.ChainName == ChainName.Testnet));
n.NBitcoinNetwork.ChainName == ChainName.Testnet)
{
CachedOnly = true
});
}
providers.Add(new NBXplorerFeeProvider(client));
providers.Add(new StaticFeeProvider(new FeeRate(100L, 1)));
@ -42,4 +49,27 @@ public class FeeProviderFactory : IFeeProviderFactory
{
return _FeeProviders.TryGetValue(network, out var prov) ? prov : throw new NotSupportedException($"No fee provider for this network ({network.CryptoCode})");
}
public async Task Do(CancellationToken cancellationToken)
{
try
{
await RefreshCache(_FeeProviders.Values);
}
// Do not spam logs if mempoolspace is down
catch (TaskCanceledException)
{
}
catch (HttpRequestException)
{
}
}
private Task RefreshCache(IEnumerable<IFeeProvider> feeProviders) => Task.WhenAll(feeProviders.Select(fp => RefreshCache(fp)));
private Task RefreshCache(IFeeProvider fp) =>
fp switch
{
FallbackFeeProvider ffp => Task.WhenAll(ffp.Providers.Select(p => RefreshCache(p))),
MempoolSpaceFeeProvider mempool => mempool.RefreshCache(),
_ => Task.CompletedTask
};
}

@ -3,21 +3,26 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using AngleSharp.Dom;
using ExchangeSharp;
using Microsoft.Extensions.Caching.Memory;
using NBitcoin;
using Org.BouncyCastle.Asn1.X509;
using YamlDotNet.Core.Tokens;
namespace BTCPayServer.Services.Fees;
public class MempoolSpaceFeeProvider(
IMemoryCache MemoryCache,
string CacheKey,
IHttpClientFactory HttpClientFactory,
bool Testnet) : IFeeProvider
IMemoryCache memoryCache,
string cacheKey,
IHttpClientFactory httpClientFactory,
bool testnet) : IFeeProvider
{
private readonly string ExplorerLink = Testnet switch
private string ExplorerLink = testnet switch
{
true => "https://mempool.space/testnet/api/v1/fees/recommended",
false => "https://mempool.space/api/v1/fees/recommended"
@ -27,50 +32,95 @@ public class MempoolSpaceFeeProvider(
{
var result = await GetFeeRatesAsync();
return result.TryGetValue(blockTarget, out var feeRate)
? feeRate
:
//try get the closest one
result[result.Keys.MinBy(key => Math.Abs(key - blockTarget))];
}
public Task<Dictionary<int, FeeRate>> GetFeeRatesAsync()
{
return MemoryCache.GetOrCreateAsync(CacheKey, async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
var client = HttpClientFactory.CreateClient(nameof(MempoolSpaceFeeProvider));
using var result = await client.GetAsync(ExplorerLink);
result.EnsureSuccessStatusCode();
var recommendedFees = await result.Content.ReadAsAsync<Dictionary<string, decimal>>();
var feesByBlockTarget = new Dictionary<int, FeeRate>();
foreach ((var feeId, decimal value) in recommendedFees)
{
var target = feeId switch
{
"fastestFee" => 1,
"halfHourFee" => 3,
"hourFee" => 6,
"economyFee" when recommendedFees.TryGetValue("minimumFee", out var minFee) && minFee == value => 144,
"economyFee" => 72,
"minimumFee" => 144,
_ => -1
};
feesByBlockTarget.TryAdd(target, new FeeRate(RandomizeByPercentage(value, 10)));
}
return feesByBlockTarget;
})!;
return InterpolateOrBound(result, blockTarget);
}
static decimal RandomizeByPercentage(decimal value, int percentage)
internal static FeeRate InterpolateOrBound(BlockFeeRate[] ordered, int target)
{
decimal range = value * percentage / 100m;
var res = value + range * (Random.Shared.NextDouble() < 0.5 ? -1 : 1);
(BlockFeeRate lb, BlockFeeRate hb) = (ordered[0], ordered[^1]);
target = Math.Clamp(target, lb.Blocks, hb.Blocks);
for (int i = 0; i < ordered.Length; i++)
{
if (ordered[i].Blocks > lb.Blocks && ordered[i].Blocks <= target)
lb = ordered[i];
if (ordered[i].Blocks < hb.Blocks && ordered[i].Blocks >= target)
hb = ordered[i];
}
if (hb.Blocks == lb.Blocks)
return hb.FeeRate;
var a = (decimal)(target - lb.Blocks) / (decimal)(hb.Blocks - lb.Blocks);
return new FeeRate((1 - a) * lb.FeeRate.SatoshiPerByte + a * hb.FeeRate.SatoshiPerByte);
}
readonly TimeSpan Expiration = TimeSpan.FromMinutes(25);
public async Task RefreshCache()
{
var rate = await GetFeeRatesCore();
memoryCache.Set(cacheKey, rate, Expiration);
}
public bool CachedOnly { get; set; }
internal async Task<BlockFeeRate[]> GetFeeRatesAsync()
{
if (CachedOnly)
return memoryCache.Get(cacheKey) as BlockFeeRate[] ?? throw new InvalidOperationException("Fee rates unavailable");
try
{
return (await memoryCache.GetOrCreateAsync(cacheKey, async entry =>
{
entry.AbsoluteExpirationRelativeToNow = Expiration;
return await GetFeeRatesCore();
}))!;
}
catch (Exception)
{
memoryCache.Remove(cacheKey);
throw;
}
}
internal record BlockFeeRate(int Blocks, FeeRate FeeRate);
async Task<BlockFeeRate[]> GetFeeRatesCore()
{
var client = httpClientFactory.CreateClient(nameof(MempoolSpaceFeeProvider));
using var result = await client.GetAsync(ExplorerLink);
result.EnsureSuccessStatusCode();
var recommendedFees = await result.Content.ReadAsAsync<Dictionary<string, decimal>>();
var r = new List<BlockFeeRate>();
foreach ((var feeId, decimal value) in recommendedFees)
{
var target = feeId switch
{
"fastestFee" => 1,
"halfHourFee" => 3,
"hourFee" => 6,
"economyFee" when recommendedFees.TryGetValue("minimumFee", out var minFee) && minFee == value => 144,
"economyFee" => 72,
"minimumFee" => 144,
_ => -1
};
r.Add(new(target, new FeeRate(value)));
}
var ordered = r.OrderBy(k => k.Blocks).ToArray();
for (var i = 0; i < ordered.Length; i++)
{
// Randomize a bit
ordered[i] = ordered[i] with { FeeRate = new FeeRate(RandomizeByPercentage(ordered[i].FeeRate.SatoshiPerByte, 10m)) };
if (i > 0) // Make sure feerate always decrease
ordered[i] = ordered[i] with { FeeRate = FeeRate.Min(ordered[i - 1].FeeRate, ordered[i].FeeRate) };
}
return ordered;
}
internal static decimal RandomizeByPercentage(decimal value, decimal percentage)
{
if (value is 1)
return 1;
decimal range = (value * percentage) / 100m;
var res = value + (range * 2.0m) * ((decimal)(Random.Shared.NextDouble() - 0.5));
return res switch
{
< 1m => 1m,
> 850m => 2000m,
> 2000m => 2000m,
_ => res
};
}

@ -319,8 +319,6 @@ namespace BTCPayServer.Services.Invoices
var paymentMethod = invoiceEntity.GetPaymentMethod(network, paymentMethodDetails.GetPaymentType());
if (paymentMethod == null)
return false;
var existingPaymentMethod = paymentMethod.GetPaymentMethodDetails();
paymentMethod.SetPaymentMethodDetails(paymentMethodDetails);
#pragma warning disable CS0618
if (network.IsBTC)
@ -358,7 +356,6 @@ namespace BTCPayServer.Services.Invoices
var invoice = await context.Invoices.FindAsync(invoiceId);
if (invoice == null)
return;
var network = paymentMethod.Network;
var invoiceEntity = invoice.GetBlob(_btcPayNetworkProvider);
var newDetails = paymentMethod.GetPaymentMethodDetails();
var existing = invoiceEntity.GetPaymentMethod(paymentMethod.GetId());

@ -0,0 +1,44 @@
#nullable enable
using System;
using System.Collections.Generic;
using NBitcoin.DataEncoders;
using System.Text;
using System.Diagnostics.CodeAnalysis;
using BTCPayServer.Services.WalletFileParsing;
namespace BTCPayServer.Services;
public class WalletFileParsers
{
public WalletFileParsers(IEnumerable<IWalletFileParser> parsers)
{
Parsers = parsers;
}
public IEnumerable<IWalletFileParser> Parsers { get; }
public bool TryParseWalletFile(string fileContents, BTCPayNetwork network, [MaybeNullWhen(false)] out DerivationSchemeSettings settings, [MaybeNullWhen(true)] out string error)
{
settings = null;
error = null;
ArgumentNullException.ThrowIfNull(fileContents);
ArgumentNullException.ThrowIfNull(network);
if (HexEncoder.IsWellFormed(fileContents))
{
fileContents = Encoding.UTF8.GetString(Encoders.Hex.DecodeData(fileContents));
}
foreach (IWalletFileParser onChainWalletParser in Parsers)
{
try
{
if (onChainWalletParser.TryParse(network, fileContents, out settings))
return true;
}
catch (Exception)
{
}
}
error = "Unsupported file format";
return false;
}
}

@ -0,0 +1,61 @@
#nullable enable
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using BTCPayServer;
using NBitcoin;
using NBXplorer.DerivationStrategy;
using AccountKeySettings = BTCPayServer.AccountKeySettings;
using BTCPayNetwork = BTCPayServer.BTCPayNetwork;
namespace BTCPayServer.Services.WalletFileParsing;
public class BSMSWalletFileParser : IWalletFileParser
{
public bool TryParse(BTCPayNetwork network, string data, [MaybeNullWhen(false)] out DerivationSchemeSettings derivationSchemeSettings)
{
derivationSchemeSettings = null;
string[] lines = data.Split(
new[] { "\r\n", "\r", "\n" },
StringSplitOptions.None
);
if (lines.Length < 4 || !lines[0].Trim().Equals("BSMS 1.0"))
return false;
var descriptor = lines[1];
var derivationPath = lines[2].Split(',', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault() ??
"/0/*";
if (derivationPath == "No path restrictions")
derivationPath = "/0/*";
if (derivationPath != "/0/*")
return false;
descriptor = descriptor.Replace("/**", derivationPath);
var testAddress = BitcoinAddress.Create(lines[3], network.NBitcoinNetwork);
var result = network.GetDerivationSchemeParser().ParseOutputDescriptor(descriptor);
var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
var line = result.Item1.GetLineFor(deposit).Derive(0);
if (testAddress.ScriptPubKey != line.ScriptPubKey)
return false;
derivationSchemeSettings = new BTCPayServer.DerivationSchemeSettings()
{
Network = network,
Source = "BSMS",
AccountDerivation = result.Item1,
AccountOriginal = descriptor.Trim(),
AccountKeySettings = result.Item2.Select((path, i) => new AccountKeySettings()
{
RootFingerprint = path?.MasterFingerprint,
AccountKeyPath = path?.KeyPath,
AccountKey = result.Item1.GetExtPubKeys().ElementAt(i).GetWif(network.NBitcoinNetwork)
}).ToArray()
};
return true;
}
}

@ -0,0 +1,68 @@
#nullable enable
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using NBitcoin;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.WalletFileParsing;
public class ElectrumWalletFileParser : IWalletFileParser
{
class ElectrumFormat
{
internal class KeyStoreFormat
{
public string? xpub { get; set; }
public string? label { get; set; }
public string? root_fingerprint { get; set; }
public uint? ckcc_xfp { get; set; }
public string? derivation { get; set; }
public string? ColdCardFirmwareVersion { get; set; }
public string? CoboVaultFirmwareVersion { get; set; }
}
public KeyStoreFormat? keystore { get; set; }
}
public bool TryParse(BTCPayNetwork network, string data, [MaybeNullWhen(false)] out DerivationSchemeSettings derivationSchemeSettings)
{
derivationSchemeSettings = null;
var jobj = DeserializeObject<ElectrumFormat>(data);
if (jobj?.keystore is null)
return false;
var result = new DerivationSchemeSettings { Network = network };
var derivationSchemeParser = network.GetDerivationSchemeParser();
result.Source = "ElectrumFile";
if (jobj.keystore.xpub is null)
return false;
if (!derivationSchemeParser.TryParseXpub(jobj.keystore.xpub, ref result, true))
return false;
if (jobj.keystore.label is not null)
result.Label = jobj.keystore.label;
if (jobj.keystore.ckcc_xfp is not null)
result.AccountKeySettings[0].RootFingerprint = new HDFingerprint(jobj.keystore.ckcc_xfp.Value);
if (jobj.keystore.root_fingerprint is not null)
result.AccountKeySettings[0].RootFingerprint = HDFingerprint.Parse(jobj.keystore.root_fingerprint);
if (jobj.keystore.derivation is not null)
result.AccountKeySettings[0].AccountKeyPath = new KeyPath(jobj.keystore.derivation);
if (jobj.keystore.ColdCardFirmwareVersion is not null)
result.Source = "ColdCard";
else if (jobj.keystore.CoboVaultFirmwareVersion is not null)
result.Source = "CoboVault";
derivationSchemeSettings = result;
return true;
}
private T? DeserializeObject<T>(string data)
{
// We can't call JsonConvert.DeserializeObject directly
// because some export of Electrum file can have more than one
// JSON object separated by commas in the file
JsonTextReader reader = new JsonTextReader(new StringReader(data));
var o = JObject.ReadFrom(reader);
return JsonConvert.DeserializeObject<T>(o.ToString());
}
}

@ -0,0 +1,8 @@
#nullable enable
using System.Diagnostics.CodeAnalysis;
using BTCPayServer;
namespace BTCPayServer.Services.WalletFileParsing;
public interface IWalletFileParser
{
bool TryParse(BTCPayNetwork network, string data, [MaybeNullWhen(false)] out DerivationSchemeSettings derivationSchemeSettings);
}

@ -0,0 +1,20 @@
#nullable enable
using System.Diagnostics.CodeAnalysis;
namespace BTCPayServer.Services.WalletFileParsing;
public class NBXDerivGenericWalletFileParser : IWalletFileParser
{
public bool TryParse(BTCPayNetwork network, string data, [MaybeNullWhen(false)] out DerivationSchemeSettings derivationSchemeSettings)
{
var result = new DerivationSchemeSettings { Network = network };
var parser = network.GetDerivationSchemeParser();
if (parser.TryParseXpub(data, ref result, electrum: true) ||
parser.TryParseXpub(data, ref result))
{
derivationSchemeSettings = result;
derivationSchemeSettings.Source = "GenericFile";
return true;
}
derivationSchemeSettings = null;
return false;
}
}

@ -0,0 +1,34 @@
#nullable enable
using System;
using System.Diagnostics.CodeAnalysis;
using BTCPayServer;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.WalletFileParsing;
public class OutputDescriptorJsonWalletFileParser : IWalletFileParser
{
private readonly OutputDescriptorWalletFileParser _outputDescriptorOnChainWalletParser;
class OutputDescriptorJsonWalletFileFormat
{
public string? Descriptor { get; set; }
public string? Source { get; set; }
}
public OutputDescriptorJsonWalletFileParser(OutputDescriptorWalletFileParser outputDescriptorOnChainWalletParser)
{
_outputDescriptorOnChainWalletParser = outputDescriptorOnChainWalletParser;
}
public bool TryParse(BTCPayNetwork network, string data, [MaybeNullWhen(false)] out DerivationSchemeSettings derivationSchemeSettings)
{
derivationSchemeSettings = null;
var jobj = JsonConvert.DeserializeObject<OutputDescriptorJsonWalletFileFormat>(data);
if (jobj?.Descriptor is null)
return false;
if (!_outputDescriptorOnChainWalletParser.TryParse(network, jobj.Descriptor, out derivationSchemeSettings))
return false;
if (jobj.Source is not null)
derivationSchemeSettings.Source = jobj.Source;
return true;
}
}

@ -0,0 +1,33 @@
#nullable enable
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using BTCPayServer;
namespace BTCPayServer.Services.WalletFileParsing;
public class OutputDescriptorWalletFileParser : IWalletFileParser
{
public bool TryParse(BTCPayNetwork network, string data, [MaybeNullWhen(false)] out DerivationSchemeSettings derivationSchemeSettings)
{
derivationSchemeSettings = null;
var maybeOutputDesc = !data.Trim().StartsWith("{", StringComparison.OrdinalIgnoreCase);
if (!maybeOutputDesc)
return false;
var derivationSchemeParser = network.GetDerivationSchemeParser();
var descriptor = derivationSchemeParser.ParseOutputDescriptor(data);
derivationSchemeSettings = new DerivationSchemeSettings()
{
Network = network,
Source = "OutputDescriptor",
AccountOriginal = data.Trim(),
AccountDerivation = descriptor.Item1,
AccountKeySettings = descriptor.Item2.Select((path, i) => new AccountKeySettings()
{
RootFingerprint = path?.MasterFingerprint,
AccountKeyPath = path?.KeyPath,
AccountKey =
descriptor.Item1.GetExtPubKeys().ElementAt(i).GetWif(derivationSchemeParser.Network)
}).ToArray()
};
return true;
}
}

@ -0,0 +1,37 @@
#nullable enable
using System;
using System.Diagnostics.CodeAnalysis;
using BTCPayServer;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.WalletFileParsing;
public class SpecterWalletFileParser : IWalletFileParser
{
private readonly OutputDescriptorWalletFileParser _outputDescriptorOnChainWalletParser;
class SpecterFormat
{
public string? descriptor { get; set; }
public int? blockheight { get; set; }
public string? label { get; set; }
}
public SpecterWalletFileParser(OutputDescriptorWalletFileParser outputDescriptorOnChainWalletParser)
{
_outputDescriptorOnChainWalletParser = outputDescriptorOnChainWalletParser;
}
public bool TryParse(BTCPayNetwork network, string data, [MaybeNullWhen(false)] out DerivationSchemeSettings derivationSchemeSettings)
{
derivationSchemeSettings = null;
var jobj = JsonConvert.DeserializeObject<SpecterFormat>(data);
if (jobj?.descriptor is null || jobj.blockheight is null)
return false;
if (!_outputDescriptorOnChainWalletParser.TryParse(network, jobj.descriptor, out derivationSchemeSettings))
return false;
derivationSchemeSettings.Source = "Specter";
if (jobj.label is not null)
derivationSchemeSettings.Label = jobj.label;
return true;
}
}

@ -0,0 +1,70 @@
#nullable enable
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using BTCPayServer;
using NBitcoin;
using NBitcoin.DataEncoders;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.WalletFileParsing;
public class WasabiWalletFileParser : IWalletFileParser
{
class WasabiFormat
{
public string? ExtPubKey { get; set; }
public string? MasterFingerprint { get; set; }
public string? AccountKeyPath { get; set; }
public string? ColdCardFirmwareVersion { get; set; }
public string? CoboVaultFirmwareVersion { get; set; }
public string? DerivationPath { get; set; }
public string? Source { get; set; }
}
public bool TryParse(BTCPayNetwork network, string data, [MaybeNullWhen(false)] out DerivationSchemeSettings derivationSchemeSettings)
{
derivationSchemeSettings = null;
var jobj = JsonConvert.DeserializeObject<WasabiFormat>(data);
var derivationSchemeParser = network.GetDerivationSchemeParser();
var result = new DerivationSchemeSettings()
{
Network = network
};
if (jobj is null || !derivationSchemeParser.TryParseXpub(jobj.ExtPubKey, ref result))
return false;
if (jobj.MasterFingerprint is not null)
{
// https://github.com/zkSNACKs/WalletWasabi/pull/1663#issuecomment-508073066
if (uint.TryParse(jobj.MasterFingerprint, out var fingerprint))
{
result.AccountKeySettings[0].RootFingerprint = new HDFingerprint(fingerprint);
}
else
{
var bytes = Encoders.Hex.DecodeData(jobj.MasterFingerprint);
var shouldReverseMfp = jobj.ColdCardFirmwareVersion == "2.1.0";
if (shouldReverseMfp) // Bug in previous version of coldcard
bytes = bytes.Reverse().ToArray();
result.AccountKeySettings[0].RootFingerprint = new HDFingerprint(bytes);
}
}
if (jobj.AccountKeyPath is not null)
result.AccountKeySettings[0].AccountKeyPath = new KeyPath(jobj.AccountKeyPath);
if (jobj.ColdCardFirmwareVersion is not null)
{
result.Source = "ColdCard";
}
else if (jobj.CoboVaultFirmwareVersion is not null)
{
result.Source = "CoboVault";
}
else
result.Source = jobj.Source ?? "WasabiFile";
derivationSchemeSettings = result;
return true;
}
}

@ -6,7 +6,7 @@
<template id="bitcoin-method-checkout-template">
@await Component.InvokeAsync("UiExtensionPoint", new {location = "checkout-v2-bitcoin-pre-content", model = Model})
<div class="payment-box">
<div v-if="model.invoiceBitcoinUrlQR" class="qr-container" :data-qr-value="model.invoiceBitcoinUrlQR" :data-clipboard="model.btcAddress">
<div v-if="model.invoiceBitcoinUrlQR" class="qr-container" :data-qr-value="model.invoiceBitcoinUrlQR" :data-clipboard="model.invoiceBitcoinUrl" data-clipboard-confirm-element="#Address_@Model.PaymentMethodId [data-clipboard]">
<div>
<qrcode :value="model.invoiceBitcoinUrlQR" tag="div" :options="qrOptions" />
</div>

@ -14,7 +14,6 @@
role = null;
var storeId = Context.GetRouteValue("storeId") as string;
var controller = ViewContext.RouteData.Values["controller"].ToString().TrimEnd("Controller", StringComparison.InvariantCultureIgnoreCase);
if (storeId is null)
ViewData.SetActivePage(ServerNavPages.Roles, role is null ? "Create role" : "Update role");

@ -15,7 +15,7 @@
<link href="~/main/site.css" asp-append-version="true" rel="stylesheet" />
<partial name="LayoutHeadTheme" />
@if (ViewData.TryGetValue("StoreBranding", out var storeBranding))
@if (ViewData.TryGetValue("StoreBranding", out var storeBranding) && storeBranding != null)
{
<partial name="LayoutHeadStoreBranding" model="storeBranding" />
}

@ -3,7 +3,7 @@
<template id="lightning-method-checkout-template">
<div class="payment-box">
@await Component.InvokeAsync("UiExtensionPoint" , new { location="checkout-v2-lightning-pre-content", model = Model})
<div v-if="model.invoiceBitcoinUrlQR" class="qr-container" :data-qr-value="model.invoiceBitcoinUrlQR" :data-clipboard="model.btcAddress">
<div v-if="model.invoiceBitcoinUrlQR" class="qr-container" :data-qr-value="model.invoiceBitcoinUrlQR" :data-clipboard="model.invoiceBitcoinUrl" data-clipboard-confirm-element="#Lightning_@Model.PaymentMethodId [data-clipboard]">
<div>
<qrcode :value="model.invoiceBitcoinUrlQR" tag="div" :options="qrOptions" />
</div>

@ -7,13 +7,13 @@
Layout = "_Layout";
ViewData.SetActivePage(ServerNavPages.Plugins);
var installed = Model.Installed.ToDictionary(plugin => plugin.Identifier, plugin => plugin.Version);
var installedWithoutSystemPlugins = Model.Installed.Where(i => !i.SystemPlugin).ToList();
var availableAndNotInstalled = new List<PluginService.AvailablePlugin>();
var availableAndNotInstalledx = Model.Available
.Where(plugin => !installed.ContainsKey(plugin.Identifier))
.GroupBy(plugin => plugin.Identifier)
.ToList();
var availableAndNotInstalled = new List<PluginService.AvailablePlugin>();
foreach (var availableAndNotInstalledItem in availableAndNotInstalledx)
{
var ordered = availableAndNotInstalledItem.OrderByDescending(plugin => plugin.Version).ToArray();
@ -76,12 +76,18 @@
<div class="mb-5">
<h3 class="mb-4">Disabled Plugins</h3>
<ul class="list-group list-group-flush d-inline-block">
@foreach (var d in Model.Disabled)
@foreach (var (plugin, version) in Model.Disabled)
{
<li class="list-group-item px-0">
<div class="d-flex flex-wrap align-items-center justify-content-between gap-3">
<span>@d</span>
<form asp-action="UnInstallPlugin" asp-route-plugin="@d">
<span>
@plugin
@if (version != null)
{
<span>({version})</span>
}
</span>
<form asp-action="UnInstallPlugin" asp-route-plugin="@plugin">
<button type="submit" class="btn btn-sm btn-outline-danger">Uninstall</button>
</form>
</div>
@ -233,57 +239,55 @@
</div>
</div>
@{
var pendingAction = Model.Commands.Any(tuple => tuple.plugin.Equals(plugin.Identifier, StringComparison.InvariantCultureIgnoreCase));
}
@if (pendingAction || (updateAvailable && x != null && !DependentOn(plugin.Identifier)))
{
var pendingAction = Model.Commands.LastOrDefault(tuple => tuple.plugin.Equals(plugin.Identifier, StringComparison.InvariantCultureIgnoreCase)).command;
var exclusivePendingAction = true;
<div class="card-footer border-0 pb-3 d-flex gap-2">
@if (pendingAction && updateAvailable)
var versionOfPendingInstall = PluginService.GetVersionOfPendingInstall(plugin.Identifier);
}
<div class="card-footer border-0 pb-3 d-flex gap-2">
@if (pendingAction is not null && updateAvailable)
{
var isUpdateAction = Model.Commands.Last(tuple => tuple.plugin.Equals(plugin.Identifier, StringComparison.InvariantCultureIgnoreCase)).command == "update";
if (isUpdateAction)
{
var isUpdateAction = Model.Commands.Last(tuple => tuple.plugin.Equals(plugin.Identifier, StringComparison.InvariantCultureIgnoreCase)).command == "update";
if (isUpdateAction)
{
var version = PluginService.GetVersionOfPendingInstall(plugin.Identifier);
exclusivePendingAction = version == x.Version;
}
exclusivePendingAction = versionOfPendingInstall == x.Version;
}
@if (pendingAction)
}
@if (pendingAction is not null)
{
<form asp-action="CancelPluginCommands" asp-route-plugin="@plugin.Identifier">
<button type="submit" class="btn btn-outline-secondary">Cancel pending @pendingAction @(versionOfPendingInstall is null? "": $"of {versionOfPendingInstall}")</button>
</form>
}
@if (pendingAction is null || !exclusivePendingAction)
{
@if (updateAvailable && x != null)
{
<form asp-action="CancelPluginCommands" asp-route-plugin="@plugin.Identifier">
<button type="submit" class="btn btn-outline-secondary">Cancel pending action</button>
</form>
}
@if(!pendingAction || !exclusivePendingAction)
{
@if (updateAvailable && x != null)
if (PluginManager.DependenciesMet(x.Dependencies, installed))
{
if (PluginManager.DependenciesMet(x.Dependencies, installed))
{
<form asp-action="InstallPlugin" asp-route-plugin="@plugin.Identifier" asp-route-version="@x.Version" asp-route-update="true" class="me-3">
<button type="submit" class="btn btn-secondary">Update</button>
</form>
}
else
{
<form asp-action="InstallPlugin" asp-route-plugin="@plugin.Identifier" asp-route-version="@x.Version" asp-route-update="true" class="me-3">
<button title="Schedule upgrade for when the dependencies have been met to ensure a smooth update" data-bs-toggle="tooltip" type="submit" class="btn btn-secondary">Schedule update</button>
</form>
}
}
@if (DependentOn(plugin.Identifier))
{
<button type="button" class="btn btn-outline-danger" data-bs-toggle="tooltip" title="This plugin cannot be uninstalled as it is depended on by other plugins.">Uninstall <span class="fa fa-exclamation"></span></button>
<form asp-action="InstallPlugin" asp-route-plugin="@plugin.Identifier" asp-route-version="@x.Version" asp-route-update="true" class="me-3">
<button type="submit" class="btn btn-secondary">Update</button>
</form>
}
else
{
<form asp-action="UnInstallPlugin" asp-route-plugin="@plugin.Identifier">
<button type="submit" class="btn btn-outline-danger">Uninstall</button>
<form asp-action="InstallPlugin" asp-route-plugin="@plugin.Identifier" asp-route-version="@x.Version" asp-route-update="true" class="me-3">
<button title="Schedule upgrade for when the dependencies have been met to ensure a smooth update" data-bs-toggle="tooltip" type="submit" class="btn btn-secondary">Schedule update</button>
</form>
}
}
</div>
}
@if (DependentOn(plugin.Identifier))
{
<button type="button" class="btn btn-outline-danger" data-bs-toggle="tooltip" title="This plugin cannot be uninstalled as it is depended on by other plugins.">Uninstall <span class="fa fa-exclamation"></span></button>
}
else
{
<form asp-action="UnInstallPlugin" asp-route-plugin="@plugin.Identifier">
<button type="submit" class="btn btn-outline-danger">Uninstall</button>
</form>
}
}
</div>
</div>
</div>
}
@ -297,7 +301,7 @@
@foreach (var plugin in availableAndNotInstalled)
{
var recommended = BTCPayServerOptions.RecommendedPlugins.Any(id => string.Equals(id, plugin.Identifier, StringComparison.InvariantCultureIgnoreCase));
var disabled = Model.Disabled?.Contains(plugin.Identifier) ?? false;
Model.Disabled.TryGetValue(plugin.Identifier, out var disabled);
<div class="col col-12 col-md-6 col-lg-12 col-xl-6 col-xxl-4 mb-4">
<div class="card h-100" id="@plugin.Identifier">
@ -316,7 +320,11 @@
</div>
<h5 class="text-muted d-flex align-items-center mt-1 gap-2">
@plugin.Version
@if (disabled)
@if (disabled is { } && disabled != plugin.Version)
{
<div class="badge bg-light">Disabled (@disabled)</div>
}
else if (disabled is { } && disabled == plugin.Version)
{
<div class="badge bg-light">Disabled</div>
}
@ -387,27 +395,33 @@
</div>
<div class="card-footer border-0 pb-3 d-flex gap-2">
@{
var pendingAction = Model.Commands.LastOrDefault(tuple => tuple.plugin.Equals(plugin.Identifier, StringComparison.InvariantCultureIgnoreCase));
var version = PluginService.GetVersionOfPendingInstall(plugin.Identifier);
var exclusivePendingAction = version == plugin.Version;
var res = Model.Commands.LastOrDefault(tuple => tuple.plugin.Equals(plugin.Identifier, StringComparison.InvariantCultureIgnoreCase));
var pendingAction = res != default ? res.command : null;
var versionOfPendingInstall = PluginService.GetVersionOfPendingInstall(plugin.Identifier);
var exclusivePendingAction = pendingAction is not null && (pendingAction == "delete" || versionOfPendingInstall == plugin.Version);
}
@if (!pendingAction.Equals(default))
@if (pendingAction is not null)
{
<form asp-action="CancelPluginCommands" asp-route-plugin="@plugin.Identifier">
<button type="submit" class="btn btn-outline-secondary">Cancel pending @pendingAction.command</button>
<button type="submit" class="btn btn-outline-secondary">Cancel pending @pendingAction @(versionOfPendingInstall is null? "": $"of {versionOfPendingInstall}")</button>
</form>
}
@if (pendingAction.Equals(default) || !exclusivePendingAction)
@if (pendingAction is null|| !exclusivePendingAction)
{
if (PluginManager.DependenciesMet(plugin.Dependencies, installed))
{
@* Don't show the "Install" button if plugin has been disabled *@
@if (!disabled)
@if (disabled is null)
{
<form asp-action="InstallPlugin" asp-route-plugin="@plugin.Identifier" asp-route-version="@plugin.Version">
<button type="submit" class="btn btn-primary">Install</button>
</form>
}
else if (disabled != plugin.Version)
{
<form asp-action="InstallPlugin" asp-route-plugin="@plugin.Identifier" asp-route-version="@plugin.Version" asp-route-update="true">
<button type="submit" class="btn btn-primary">Update</button>
</form>
}
}
else
{
@ -416,7 +430,7 @@
</form>
}
}
@if (disabled)
@if (disabled is not null && pendingAction is null)
{
<form asp-action="UnInstallPlugin" asp-route-plugin="@plugin.Identifier">
<button type="submit" class="btn btn-sm btn-outline-danger">Uninstall</button>

@ -7,6 +7,7 @@
@using BTCPayServer.Components.AppSales
@using BTCPayServer.Components.AppTopItems
@using BTCPayServer.Services.Apps
@using BTCPayServer.Client
@model StoreDashboardViewModel
@{
ViewData.SetActivePage(StoreNavPages.Dashboard, Model.StoreName, Model.StoreId);
@ -17,36 +18,6 @@
<div class="d-flex align-items-center justify-content-between">
<h2 class="mb-0">@ViewData["Title"]</h2>
@if (Model.IsSetUp)
{
<button type="button" class="btn btn-secondary only-for-js" data-bs-toggle="modal" data-bs-target="#WhatsNew">What's New</button>
}
</div>
<div class="modal fade" id="WhatsNew" tabindex="-1" aria-labelledby="WhatsNewTitle" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="WhatsNewTitle">What's New</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
<vc:icon symbol="close"/>
</button>
</div>
<div class="modal-body">
<h5 class="alert-heading">Updated in <a href="https://blog.btcpayserver.org/btcpay-server-1-10-0/" target="_blank" rel="noreferrer noopener">v1.10.0</a></h5>
<p class="mb-2">We've added some more options for refunds and an editor for creating custom forms. You can now also define your own roles with customized access to store features.</p>
<p class="mb-0">The invoice filter got a facelift and you now have the option to hide sensitive data in the UI (such as amounts and balances).</p>
<hr style="height:1px;background-color:var(--btcpay-body-text-muted);margin:var(--btcpay-space-m) 0;" />
<h5 class="alert-heading">Updated in <a href="https://blog.btcpayserver.org/btcpay-server-1-9-0/" target="_blank" rel="noreferrer noopener">v1.9.0</a></h5>
<p class="mb-2">Lots of improvements for our new checkout experience, which is now also the default for new stores — better NFC support, improved receipts and … confetti! 🎉</p>
<p class="mb-0">You'll also notice that wallet labels are featured more prominently. And the invoice details page now contains all the information you'd expect.</p>
<hr style="height:1px;background-color:var(--btcpay-body-text-muted);margin:var(--btcpay-space-m) 0;" />
<h5 class="alert-heading">Updated in <a href="https://blog.btcpayserver.org/btcpay-server-1-8-0/" target="_blank" rel="noreferrer noopener">v1.8.0</a></h5>
<p class="mb-2">Bear markets are for building: This version brings custom checkout forms, store branding options, a redesigned Point of Sale keypad view, new notification icons and address labeling.</p>
<p class="mb-0">You like that? Consider <a href="https://opensats.org/projects/btcpayserver" target="_blank" rel="noreferrer noopener">supporting BTCPay Server via OpenSats</a>.</p>
</div>
</div>
</div>
</div>
@if (Model.IsSetUp)
@ -112,7 +83,7 @@
@if (Model.LightningEnabled)
{
<vc:store-lightning-balance vm="@(new StoreLightningBalanceViewModel { Store = store, CryptoCode = Model.CryptoCode, InitialRendering = true })"/>
<vc:store-lightning-services vm="@(new StoreLightningServicesViewModel { Store = store, CryptoCode = Model.CryptoCode })"/>
<vc:store-lightning-services vm="@(new StoreLightningServicesViewModel { Store = store, CryptoCode = Model.CryptoCode })" permission="@Policies.CanModifyServerSettings"/>
}
@if (Model.WalletEnabled)
{

@ -19,8 +19,10 @@
{
<div asp-validation-summary="All"></div>
}
<partial name="LocalhostBrowserSupport" />
@if (this.Model.Method is WalletSetupMethod.Hardware)
{
<partial name="LocalhostBrowserSupport" />
}
<template id="modal-template">
<div class="modal-dialog" role="document">

@ -11,20 +11,26 @@
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close">
<vc:icon symbol="close" />
</button>
Warning: No wallet has been linked to your BTCPay Store.<br/>
Warning: No wallet has been linked to your BTCPay Server Store.<br/>
See <a href="https://docs.btcpayserver.org/WalletSetup/" target="_blank" class="alert-link" rel="noreferrer noopener">this link</a> for more information on how to connect your store and wallet.
</div>
}
<div class="row">
<div class="col-xxl-constrain col-xl-8">
<div class="d-flex align-items-center justify-content-between mb-3">
<h3 class="mb-3">Greenfield API Keys
<a href="/docs" target="_blank" rel="noreferrer noopener" title="More information...">
<vc:icon symbol="info"/>
</a>
</h3>
<p>To generate Greenfield API keys, please <a asp-controller="UIManage" asp-action="APIKeys">click here</a>.</p>
<div class="d-flex align-items-center justify-content-between mt-5 mb-3">
<h3 class="mb-0">@ViewData["Title"]</h3>
<a id="CreateNewToken" asp-action="CreateToken" class="btn btn-primary" role="button" asp-route-storeId="@Context.GetRouteValue("storeId")">
Create Token
</a>
</div>
<p>Authorize a public key to access Bitpay compatible Invoice API.
<a href="https://support.bitpay.com/hc/en-us/articles/115003001183-How-do-I-pair-my-client-and-create-a-token-" target="_blank" rel="noreferrer noopener" title="More information...">
<vc:icon symbol="info"/>

Some files were not shown because too many files have changed in this diff Show More