Compare commits
46 Commits
v1.12.1
...
v1.12.4-rc
Author | SHA1 | Date | |
---|---|---|---|
b62985faf4 | |||
09c761aa31 | |||
8089a938f3 | |||
35b3fef7c5 | |||
f31aa43c6a | |||
b03f8db06b | |||
27e70a169e | |||
6a1d17dda2 | |||
31bc6dd48c | |||
6054315d84 | |||
2a7059ddeb | |||
e2e7e59722 | |||
8b373bda8e | |||
d6806dc1f6 | |||
a753698ae7 | |||
3eec9cb0bb | |||
cd8ef0c1ff | |||
bd196ad963 | |||
1ad93838c9 | |||
a9252fd741 | |||
376067324b | |||
dd7ab2f647 | |||
1d6d146fb2 | |||
3ae1f13323 | |||
0b0a8f8218 | |||
f070b22355 | |||
c5a0e28420 | |||
70e9ea1d5e | |||
89d294524a | |||
5e25ee2996 | |||
5935dbf1d1 | |||
f7542c988d | |||
e90414bded | |||
78882dcff0 | |||
1ac1443070 | |||
b5405e9313 | |||
c7eef01fd5 | |||
26f61d35bb | |||
765776c429 | |||
9f54074d03 | |||
f23078df1c | |||
a35bf54a02 | |||
4867698ac9 | |||
e84e575017 | |||
c585a0b276 | |||
ad89139e07 |
.circleci
BTCPayServer.Abstractions
BTCPayServer.Client
BTCPayServer.Common
BTCPayServer.Data
BTCPayServer.Rating
BTCPayServer.Tests
AltcoinTests
BTCPayServer.Tests.csprojCheckoutv2Tests.csDockerfileFastTests.csGreenfieldAPITests.csPSBTTests.csPayJoinTests.csSeleniumTester.csSeleniumTests.csServerTester.csTestAccount.csThirdPartyTests.csUnitTest1.csUtilitiesTests.csdocker-compose.altcoins.ymldocker-compose.ymlBTCPayServer
BTCPayServer.csproj
Components
AppSales
StoreLightningBalance
StoreSelector
Controllers
BitpayRateController.cs
GreenField
GreenfieldStoreOnChainPaymentMethodsController.WalletGeneration.csGreenfieldStoreOnChainPaymentMethodsController.csGreenfieldStoreOnChainWalletsController.csGreenfieldStoreWebhooksController.csGreenfieldStoresController.cs
UIAccountController.csUICustodianAccountsController.csUIInvoiceController.UI.csUIInvoiceController.csUIPaymentRequestController.csUIServerController.Plugins.csUIServerController.csUIStoresController.Email.csUIStoresController.Onchain.csUIStoresController.csUIUserStoresController.csUIWalletsController.csData
DerivationSchemeParser.csDerivationSchemeSettings.csExtensions.csHostedServices
Hosting
PaymentRequest
Payments
PayoutProcessors
Plugins
Services
Views
wwwroot
js
locales
checkout
am-ET.jsonar.jsonaz.jsonbg-BG.jsonbs-BA.jsonca-ES.jsoncs-CZ.jsonda-DK.jsonde-DE.jsonel-GR.jsonen.jsones-ES.jsonfa.jsonfi-FI.jsonfr-FR.jsonhe.jsonhi.jsonhr-HR.jsonhu-HU.jsonhy.jsonid.jsonis-IS.jsonit-IT.jsonja-JP.jsonka.jsonkk-KZ.jsonko.jsonlv.jsonnl-NL.jsonno.jsonnp-NP.jsonpl.jsonpt-BR.jsonpt-PT.jsonro.jsonru-RU.jsonsk-SK.jsonsl-SI.jsonsr.jsonsv.jsonth-TH.jsontr.jsonuk-UA.jsonvi-VN.jsonzh-SG.jsonzh-SP.jsonzh-TW.jsonzu.json
el-GR.jsonswagger/v1
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());
|
||||
|
44
BTCPayServer/Services/WalletFileParsers.cs
Normal file
44
BTCPayServer/Services/WalletFileParsers.cs
Normal file
@ -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
Reference in New Issue
Block a user