Compare commits
193 Commits
v1.12.0
...
fix/fast-t
Author | SHA1 | Date | |
---|---|---|---|
fde4b00d39 | |||
d6970e6750 | |||
a026d244fe | |||
5884850e22 | |||
b341536e42 | |||
8356c0d5e5 | |||
6d2f886717 | |||
f9aae4ab3d | |||
1ecf0d25a9 | |||
c7231fe092 | |||
8922c3de59 | |||
fefb99dfa2 | |||
a3b0bbe861 | |||
3dd562ffdc | |||
b19db7291d | |||
70253cbd9f | |||
887803a328 | |||
42da90f7dc | |||
1152f68aed | |||
9124aeb1ee | |||
a35c5d8289 | |||
e24b42ef95 | |||
e10937c253 | |||
96b90d2444 | |||
600bbb9ce0 | |||
fe9e5eb9c9 | |||
cb136cba82 | |||
b3240f28b5 | |||
fe32cbd8be | |||
51fcf52da1 | |||
3f02c0d30a | |||
bae1f4e20b | |||
3fbc717cd4 | |||
958a348fed | |||
57226fc97f | |||
ca55e1f300 | |||
8b02c0bd82 | |||
b92ff7c27b | |||
d24761a498 | |||
c78ee24d0a | |||
172dd507bd | |||
fdd4790023 | |||
4ebe46830b | |||
a2df9ed44c | |||
6ae474d214 | |||
5b31d4de20 | |||
14f8c73b08 | |||
529075f64c | |||
dba102e74f | |||
0f3f8b6bf9 | |||
83028b9b73 | |||
1fe766cb16 | |||
6b45eb0d3d | |||
6b0087ab69 | |||
e60fd8d6ab | |||
88a1d83323 | |||
e21a8df0f3 | |||
93f37b506b | |||
fca3480e37 | |||
966547db54 | |||
09dbe44bca | |||
b7ce6b7400 | |||
78f169cd24 | |||
d0e11f1ec4 | |||
912a706de9 | |||
9b5c8a8254 | |||
e5adc630af | |||
c56c6401d6 | |||
0e64df3bbf | |||
e497903bf4 | |||
f1ff913cbe | |||
3a00d32ce0 | |||
f0f698f411 | |||
22c6468a5d | |||
a60072a431 | |||
dcc6f17c9c | |||
15ce148b99 | |||
3b73d5a5cb | |||
1fd3054006 | |||
2db1434929 | |||
a171671fe5 | |||
9160a1d71e | |||
a896560a3c | |||
e43b4ed540 | |||
8b446e2791 | |||
d72b0e4cee | |||
22996ea21e | |||
d55770cc16 | |||
5c98ca180a | |||
10bb75ce0e | |||
b9e3686fcf | |||
4ae1046571 | |||
147c6c4548 | |||
354338180b | |||
5939e19f72 | |||
f72a6df55a | |||
9c95b98f3a | |||
55a8ba0905 | |||
04037b3d2d | |||
4943c84655 | |||
42a8160768 | |||
33d3a25928 | |||
c2acff81c6 | |||
214d4b0c3f | |||
bd4cf61c2b | |||
b592ee2fed | |||
c57e1cca25 | |||
335f345ce3 | |||
b7be93c569 | |||
cd01a7b727 | |||
b96e73a002 | |||
0bf22ddf29 | |||
1c4dc382a8 | |||
71c5566f2b | |||
6621859567 | |||
6437967e60 | |||
c5a926c50c | |||
85ab691b68 | |||
4d3e0ab599 | |||
02663a149e | |||
a8fdc4798d | |||
6290b0f3bf | |||
411e0334d0 | |||
b174977bc7 | |||
2111b67e2c | |||
b96cfcd14d | |||
086f713752 | |||
fd67e09cf0 | |||
6f4ca47532 | |||
f97f23c8a5 | |||
b62985faf4 | |||
09c761aa31 | |||
8089a938f3 | |||
35b3fef7c5 | |||
f31aa43c6a | |||
b03f8db06b | |||
27e70a169e | |||
6a1d17dda2 | |||
95bf60c252 | |||
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 | |||
f9a43b537f | |||
9f54074d03 | |||
f23078df1c | |||
a35bf54a02 | |||
4867698ac9 | |||
e84e575017 | |||
c585a0b276 | |||
ad89139e07 | |||
ebc053aca5 | |||
96da7f0322 | |||
8ae9e59d9d | |||
c94dc87cb8 | |||
20512a59b3 | |||
b3f9216c54 | |||
1cda0360e9 | |||
7f75117bfa | |||
5a70345499 | |||
5114a3a2ea | |||
93ab219124 | |||
61bf6d33b2 | |||
3fc687a2d4 | |||
8da04fd7e2 | |||
cb54f8f6d1 |
.circleci
.gitignore.vscode
BTCPayServer.Abstractions
BTCPayServer.Client
BTCPayServer.Common
BTCPayServer.Data
BTCPayServer.Rating
BTCPayServer.Rating.csprojCurrencies.jsonCurrencyNameTable.cs
Providers
BackgroundFetcherRateProvider.csBitnobRateProvider.csFreeCurrencyRatesRateProvider.csKrakenExchangeRateProvider.cs
Services
BTCPayServer.Tests
AltcoinTests
BTCPayServer.Tests.csprojBTCPayServerTester.csCheckoutv2Tests.csCrowdfundTests.csDockerfileFastTests.csGreenfieldAPITests.csPSBTTests.csPayJoinTests.csSeleniumTester.csSeleniumTests.csServerTester.csTestAccount.csTestData
ThirdPartyTests.csUnitTest1.csUtilitiesTests.csdocker-compose.altcoins.ymldocker-compose.ymldocker-customer-lncli.ps1docker-merchant-lncli.ps1BTCPayServer
BTCPayServer.csprojSearchString.csStorePolicies.csUserManagerExtensions.csWalletId.cs
Components
AppSales
MainNav
StoreLightningBalance
StoreRecentInvoices
StoreRecentTransactions
StoreSelector
StoreWalletBalance
Controllers
BitpayRateController.cs
GreenField
GreenfieldAppsController.csGreenfieldCustodianAccountController.csGreenfieldPaymentRequestsController.csGreenfieldPullPaymentController.csGreenfieldStoreOnChainPaymentMethodsController.WalletGeneration.csGreenfieldStoreOnChainPaymentMethodsController.csGreenfieldStoreOnChainWalletsController.csGreenfieldStoreRolesController.csGreenfieldStoreUsersController.csGreenfieldStoreWebhooksController.csGreenfieldStoresController.csGreenfieldUsersController.csLocalBTCPayServerClient.cs
LightningAddressService.csUIAccountController.csUIBoltcardController.csUICustodianAccountsController.csUIHomeController.csUIInvoiceController.UI.csUIInvoiceController.csUILNURLController.csUIManageController.2FA.csUIManageController.Notifications.csUIPaymentRequestController.csUIPullPaymentController.Boltcard.csUIPullPaymentController.csUIReportsController.CheatMode.csUIServerController.Plugins.csUIServerController.Roles.csUIServerController.Users.csUIServerController.csUIStorePullPaymentsController.PullPayments.csUIStoresController.Dashboard.csUIStoresController.Email.csUIStoresController.Integrations.csUIStoresController.LightningLike.csUIStoresController.Onchain.csUIStoresController.Roles.csUIStoresController.Users.csUIStoresController.csUIUserStoresController.csUIVaultController.csUIWalletsController.PSBT.csUIWalletsController.csData
BoltcardDataExtensions.cs
DerivationSchemeParser.csDerivationSchemeSettings.csPayouts
PullPayments
StoreBlob.csStoreDataExtensions.csEvents
Extensions.csExtensions
ActionLogicExtensions.csEmailSenderExtensions.csMoneyExtensions.csStoreExtensions.csUrlHelperExtensions.cs
Filters
Forms
HostedServices
BitpayIPNSender.csInvoiceWatcher.csPluginUpdateFetcher.csPullPaymentHostedService.csStoreEmailRuleProcessorSender.csTransactionLabelMarkerHostedService.csUserEventHostedService.cs
Webhooks
Hosting
Models
AccountViewModels
EmailsViewModel.csInvoicingModels
ServerViewModels
StoreViewModels
ViewPullPaymentModel.csWalletViewModels
PaymentRequest
Payments
Bitcoin
LNURLPay
Lightning
PayoutProcessors
Plugins
Altcoins
Bitcoin
Crowdfund
LightningAddressResolver.csLightningAddresssResolver.csPluginManager.csPluginService.csPointOfSale
Shopify
Security
Services
Altcoins/Monero
MoneroLikeExtensions.cs
Payments
MoneroLikeOnChainPaymentMethodDetails.csMoneroLikePaymentData.csMoneroLikePaymentMethodHandler.csMoneroSupportedPaymentMethod.cs
Services
UI
Apps
Fees
Invoices
Mails
Notifications
PoliciesSettings.csReportService.csReporting
ServerSettings.csStores
ThemesSettings.csUserService.csWalletFileParsers.csWalletFileParsing
BSMSWalletFileParser.csElectrumWalletFileParser.csIWalletFileParser.csNBXDerivGenericWalletFileParser.csOutputDescriptorJsonWalletFileParser.csOutputDescriptorWalletFileParser.csSpecterWalletFileParser.csWasabiWalletFileParser.cs
WalletRepository.csWallets/Export
Views
Shared
Bitcoin
CameraScanner.cshtmlCreateOrEditRole.cshtmlCrowdfund
EmailsBody.cshtmlEmailsTest.cshtmlLayoutHead.cshtmlLightning
ListRoles.cshtmlPointOfSale
PosData.cshtmlTemplateEditor.cshtml_BTCPaySupporters.cshtml_Confirm.cshtml_Layout.cshtml_LayoutSignedOut.cshtml_LayoutSimple.cshtmlUIAccount
ForgotPassword.cshtmlLockout.cshtmlLogin.cshtmlLoginWith2fa.cshtmlRegister.cshtmlSecondaryLogin.cshtmlSetPassword.cshtml
UIApps
UICustodianAccounts
UIError
UIForms
UIHome
UIInvoice
CreateInvoice.cshtmlInvoice.cshtmlInvoiceReceipt.cshtmlInvoiceReceiptPrint.cshtmlListInvoices.cshtml_RefundModal.cshtml
UILightningAutomatedPayoutProcessors
UIManage
AddApiKey.cshtmlAuthorizeAPIKey.cshtmlChangePassword.cshtmlConfirmAPIKey.cshtmlEnableAuthenticator.cshtmlIndex.cshtmlNotificationSettings.cshtmlSetPassword.cshtml
UIMoneroLikeStore
UIOnChainAutomatedPayoutProcessors
UIPaymentRequest
UIPayoutProcessors
UIPullPayment
UIReports
UIServer
Branding.cshtmlCLightningRestServices.cshtmlConfiguratorService.cshtmlCreateTemporaryFileUrl.cshtmlCreateUser.cshtmlDynamicDnsService.cshtmlEmails.cshtmlLightningChargeServices.cshtmlLightningWalletServices.cshtmlListPlugins.cshtmlListStores.cshtmlListUsers.cshtmlLndSeedBackup.cshtmlMaintenance.cshtmlP2PService.cshtmlPolicies.cshtmlRPCService.cshtmlSSHService.cshtmlServerNavPages.csStorage.cshtmlUser.cshtml_Nav.cshtml
UIShopify
UIStorePullPayments
UIStores
CheckoutAppearance.cshtmlDashboard.cshtmlGeneralSettings.cshtml
ImportWallet
Index.cshtmlLightning.cshtmlLightningSettings.cshtmlListTokens.cshtmlRates.cshtmlSetupLightningNode.cshtmlStoreEmailSettings.cshtmlStoreEmails.cshtmlStoreUsers.cshtmlWalletSettings.cshtmlWebhooks.cshtml_GenerateWalletForm.cshtml_Nav.cshtmlUIUserStores
UIWallets
wwwroot
crowdfund
img
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.jsonmain
pos
swagger/v1
swagger.template.apps.jsonswagger.template.jsonswagger.template.pull-payments.jsonswagger.template.users.jsonswagger.template.webhooks.json
vendor
Build
Changelog.mdDockerfileREADME.mdRELEASE-CYCLES.mdarm32v7.Dockerfilearm64v8.Dockerfilebtcpayserver.slndocs
@ -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-]+)/
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -300,3 +300,4 @@ Plugins/packed
|
||||
BTCPayServer/wwwroot/swagger/v1/openapi.json
|
||||
BTCPayServer/appsettings.dev.json
|
||||
BTCPayServer.Tests/monero_wallet
|
||||
/BTCPayServer.Tests/NewBlocks.bat
|
||||
|
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@ -10,7 +10,7 @@
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
// If you have changed target frameworks, make sure to update the program path.
|
||||
"program": "${workspaceFolder}/BTCPayServer/bin/Debug/net6.0/BTCPayServer.dll",
|
||||
"program": "${workspaceFolder}/BTCPayServer/bin/Debug/net8.0/BTCPayServer.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/BTCPayServer",
|
||||
"stopAtEntry": false,
|
||||
|
@ -31,9 +31,9 @@
|
||||
<None Include="icon.png" Pack="true" PackagePath="\" />
|
||||
</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="HtmlSanitizer" Version="8.0.838" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.5" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.0-beta.2" />
|
||||
</ItemGroup>
|
||||
|
@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Npgsql;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations.Operations;
|
||||
|
||||
@ -85,10 +86,9 @@ namespace BTCPayServer.Abstractions.Contracts
|
||||
{
|
||||
o.EnableRetryOnFailure(10);
|
||||
o.SetPostgresVersion(12, 0);
|
||||
if (!string.IsNullOrEmpty(_schemaPrefix))
|
||||
{
|
||||
o.MigrationsHistoryTable(_schemaPrefix);
|
||||
}
|
||||
var mainSearchPath = GetSearchPath(_options.Value.ConnectionString);
|
||||
var schemaPrefix = string.IsNullOrEmpty(_schemaPrefix) ? "__EFMigrationsHistory" : _schemaPrefix;
|
||||
o.MigrationsHistoryTable(schemaPrefix, mainSearchPath);
|
||||
})
|
||||
.ReplaceService<IMigrationsSqlGenerator, CustomNpgsqlMigrationsSqlGenerator>();
|
||||
break;
|
||||
@ -108,5 +108,11 @@ namespace BTCPayServer.Abstractions.Contracts
|
||||
}
|
||||
}
|
||||
|
||||
private string GetSearchPath(string connectionString)
|
||||
{
|
||||
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString);
|
||||
var searchPaths = connectionStringBuilder.SearchPath?.Split(',');
|
||||
return searchPaths is not { Length: > 0 } ? null : searchPaths[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -36,6 +36,17 @@ public static class HttpRequestExtensions
|
||||
request.Path.ToUriComponent());
|
||||
}
|
||||
|
||||
public static string GetCurrentUrlWithQueryString(this HttpRequest request)
|
||||
{
|
||||
return string.Concat(
|
||||
request.Scheme,
|
||||
"://",
|
||||
request.Host.ToUriComponent(),
|
||||
request.PathBase.ToUriComponent(),
|
||||
request.Path.ToUriComponent(),
|
||||
request.QueryString.ToUriComponent());
|
||||
}
|
||||
|
||||
public static string GetCurrentPath(this HttpRequest request)
|
||||
{
|
||||
return string.Concat(
|
||||
|
@ -101,6 +101,14 @@ namespace BTCPayServer.Abstractions.Extensions
|
||||
return categoryAndPageMatch && idMatch ? ActivePageClass : null;
|
||||
}
|
||||
|
||||
public static HtmlString ToBrowserDate(this DateTimeOffset date, string netFormat, string jsDateFormat = "short", string jsTimeFormat = "short")
|
||||
{
|
||||
var dateTime = date.ToString("o", CultureInfo.InvariantCulture);
|
||||
var displayDate = date.ToString(netFormat, CultureInfo.InvariantCulture);
|
||||
var tooltip = dateTime.Replace("T", " ");
|
||||
return new HtmlString($"<time datetime=\"{dateTime}\" data-date-style=\"{jsDateFormat}\" data-time-style=\"{jsTimeFormat}\" data-initial=\"localized\" data-bs-toggle=\"tooltip\" data-bs-title=\"{tooltip}\">{displayDate}</time>");
|
||||
}
|
||||
|
||||
public static HtmlString ToBrowserDate(this DateTimeOffset date, DateDisplayFormat format = DateDisplayFormat.Localized)
|
||||
{
|
||||
var relative = date.ToTimeAgo();
|
||||
|
@ -0,0 +1,35 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Razor.TagHelpers;
|
||||
|
||||
namespace BTCPayServer.Abstractions.TagHelpers;
|
||||
|
||||
[HtmlTargetElement("form", Attributes = "[permissioned]")]
|
||||
public partial class PermissionedFormTagHelper(
|
||||
IAuthorizationService authorizationService,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
: TagHelper
|
||||
{
|
||||
public string Permissioned { get; set; }
|
||||
public string PermissionResource { get; set; }
|
||||
|
||||
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
if (httpContextAccessor.HttpContext is null || string.IsNullOrEmpty(Permissioned))
|
||||
return;
|
||||
|
||||
var res = await authorizationService.AuthorizeAsync(httpContextAccessor.HttpContext.User,
|
||||
PermissionResource, Permissioned);
|
||||
if (!res.Succeeded)
|
||||
{
|
||||
var content = await output.GetChildContentAsync();
|
||||
var html = SubmitButtonRegex().Replace(content.GetContent(), "");
|
||||
output.Content.SetHtmlContent($"<fieldset disabled>{html}</fieldset>");
|
||||
}
|
||||
}
|
||||
|
||||
[GeneratedRegex("<(button|input).*?type=\"submit\".*?>.*?</\\1>")]
|
||||
private static partial Regex SubmitButtonRegex();
|
||||
}
|
@ -16,7 +16,7 @@
|
||||
<Platforms>AnyCPU</Platforms>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<Version Condition=" '$(Version)' == '' ">1.7.3</Version>
|
||||
<Version Condition=" '$(Version)' == '' ">1.7.4</Version>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
@ -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.37" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
@ -41,6 +41,14 @@ namespace BTCPayServer.Client
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
|
||||
public virtual async Task<bool> ApproveUser(string idOrEmail, bool approved, CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/users/{idOrEmail}/approve", null,
|
||||
new ApproveUserRequest { Approved = approved }, HttpMethod.Post), token);
|
||||
await HandleResponse(response);
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
|
||||
public virtual async Task<ApplicationUserData[]> GetUsers(CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/users/", null, HttpMethod.Get), token);
|
||||
|
@ -25,6 +25,16 @@ namespace BTCPayServer.Client.Models
|
||||
/// </summary>
|
||||
public bool RequiresEmailConfirmation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the user was approved by an admin
|
||||
/// </summary>
|
||||
public bool Approved { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// whether the user needed approval on account creation
|
||||
/// </summary>
|
||||
public bool RequiresApproval { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// the roles of the user
|
||||
/// </summary>
|
||||
|
6
BTCPayServer.Client/Models/ApproveUserRequest.cs
Normal file
6
BTCPayServer.Client/Models/ApproveUserRequest.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace BTCPayServer.Client;
|
||||
|
||||
public class ApproveUserRequest
|
||||
{
|
||||
public bool Approved { get; set; }
|
||||
}
|
@ -26,11 +26,12 @@ namespace BTCPayServer.Client.Models
|
||||
public string Template { get; set; } = null;
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public PosViewType DefaultView { get; set; }
|
||||
public bool ShowItems { get; set; } = false;
|
||||
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;
|
||||
|
@ -1,3 +1,5 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class EmailSettingsData
|
||||
@ -26,4 +28,11 @@ public class EmailSettingsData
|
||||
get; set;
|
||||
}
|
||||
public bool DisableCertificateCheck { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public bool EnabledCertificateCheck
|
||||
{
|
||||
get => !DisableCertificateCheck;
|
||||
set { DisableCertificateCheck = !value; }
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public string DefaultView { get; set; }
|
||||
public bool ShowItems { get; set; }
|
||||
public bool ShowCustomAmount { get; set; }
|
||||
public bool ShowDiscount { get; set; }
|
||||
public bool ShowSearch { get; set; }
|
||||
|
@ -4,6 +4,7 @@ using System.Text;
|
||||
using NBitcoin.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
@ -14,11 +15,15 @@ namespace BTCPayServer.Client.Models
|
||||
}
|
||||
public class RegisterBoltcardRequest
|
||||
{
|
||||
[JsonProperty("LNURLW")]
|
||||
public string LNURLW { get; set; }
|
||||
[JsonConverter(typeof(HexJsonConverter))]
|
||||
[JsonProperty("UID")]
|
||||
public byte[] UID { get; set; }
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public OnExistingBehavior? OnExisting { get; set; }
|
||||
[JsonExtensionData]
|
||||
public IDictionary<string, JToken> AdditionalData { get; set; } = new Dictionary<string, JToken>();
|
||||
}
|
||||
public class RegisterBoltcardResponse
|
||||
{
|
||||
|
@ -7,11 +7,11 @@ namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class WebhookPayoutEvent : StoreWebhookEvent
|
||||
{
|
||||
public WebhookPayoutEvent(string evtType, string storeId)
|
||||
public WebhookPayoutEvent(string type, string storeId)
|
||||
{
|
||||
if (!evtType.StartsWith("payout", StringComparison.InvariantCultureIgnoreCase))
|
||||
throw new ArgumentException("Invalid event type", nameof(evtType));
|
||||
Type = evtType;
|
||||
if (!type.StartsWith("payout", StringComparison.InvariantCultureIgnoreCase))
|
||||
throw new ArgumentException("Invalid event type", nameof(type));
|
||||
Type = type;
|
||||
StoreId = storeId;
|
||||
}
|
||||
|
||||
@ -21,11 +21,11 @@ namespace BTCPayServer.Client.Models
|
||||
}
|
||||
public class WebhookPaymentRequestEvent : StoreWebhookEvent
|
||||
{
|
||||
public WebhookPaymentRequestEvent(string evtType, string storeId)
|
||||
public WebhookPaymentRequestEvent(string type, string storeId)
|
||||
{
|
||||
if (!evtType.StartsWith("paymentrequest", StringComparison.InvariantCultureIgnoreCase))
|
||||
throw new ArgumentException("Invalid event type", nameof(evtType));
|
||||
Type = evtType;
|
||||
if (!type.StartsWith("paymentrequest", StringComparison.InvariantCultureIgnoreCase))
|
||||
throw new ArgumentException("Invalid event type", nameof(type));
|
||||
Type = type;
|
||||
StoreId = storeId;
|
||||
}
|
||||
|
||||
|
@ -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.1" />
|
||||
<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(": ");
|
||||
|
@ -107,10 +107,10 @@ namespace BTCPayServer.Data
|
||||
//PayjoinLock.OnModelCreating(builder);
|
||||
PaymentRequestData.OnModelCreating(builder, Database);
|
||||
PaymentData.OnModelCreating(builder, Database);
|
||||
PayoutData.OnModelCreating(builder);
|
||||
PayoutData.OnModelCreating(builder, Database);
|
||||
PendingInvoiceData.OnModelCreating(builder);
|
||||
//PlannedTransaction.OnModelCreating(builder);
|
||||
PullPaymentData.OnModelCreating(builder);
|
||||
PullPaymentData.OnModelCreating(builder, Database);
|
||||
RefundData.OnModelCreating(builder);
|
||||
SettingData.OnModelCreating(builder, Database);
|
||||
StoreSettingData.OnModelCreating(builder, Database);
|
||||
|
@ -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.5">
|
||||
<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.5" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />
|
||||
|
@ -11,6 +11,8 @@ namespace BTCPayServer.Data
|
||||
public class ApplicationUser : IdentityUser, IHasBlob<UserBlob>
|
||||
{
|
||||
public bool RequiresEmailConfirmation { get; set; }
|
||||
public bool RequiresApproval { get; set; }
|
||||
public bool Approved { get; set; }
|
||||
public List<StoredFile> StoredFiles { get; set; }
|
||||
[Obsolete("U2F support has been replace with FIDO2")]
|
||||
public List<U2FDevice> U2FDevices { get; set; }
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@ -31,7 +32,9 @@ namespace BTCPayServer.Data
|
||||
public List<InvoiceSearchData> InvoiceSearchData { get; set; }
|
||||
public List<RefundData> Refunds { get; set; }
|
||||
|
||||
|
||||
[Timestamp]
|
||||
// With this, update of InvoiceData will fail if the row was modified by another process
|
||||
public uint XMin { get; set; }
|
||||
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||
{
|
||||
builder.Entity<InvoiceData>()
|
||||
|
@ -40,7 +40,6 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
return Severity switch
|
||||
{
|
||||
EventSeverity.Info => "info",
|
||||
EventSeverity.Error => "danger",
|
||||
EventSeverity.Success => "success",
|
||||
EventSeverity.Warning => "warning",
|
||||
|
@ -1,8 +1,11 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text;
|
||||
using BTCPayServer.Client.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
@ -21,14 +24,14 @@ namespace BTCPayServer.Data
|
||||
[MaxLength(20)]
|
||||
[Required]
|
||||
public string PaymentMethodId { get; set; }
|
||||
public byte[] Blob { get; set; }
|
||||
public byte[] Proof { get; set; }
|
||||
public string Blob { get; set; }
|
||||
public string Proof { get; set; }
|
||||
#nullable enable
|
||||
public string? Destination { get; set; }
|
||||
#nullable restore
|
||||
public StoreData StoreData { get; set; }
|
||||
|
||||
internal static void OnModelCreating(ModelBuilder builder)
|
||||
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||
{
|
||||
builder.Entity<PayoutData>()
|
||||
.HasOne(o => o.PullPaymentData)
|
||||
@ -43,6 +46,33 @@ namespace BTCPayServer.Data
|
||||
.HasIndex(o => o.State);
|
||||
builder.Entity<PayoutData>()
|
||||
.HasIndex(x => new { DestinationId = x.Destination, x.State });
|
||||
|
||||
if (databaseFacade.IsNpgsql())
|
||||
{
|
||||
builder.Entity<PayoutData>()
|
||||
.Property(o => o.Blob)
|
||||
.HasColumnType("JSONB");
|
||||
builder.Entity<PayoutData>()
|
||||
.Property(o => o.Proof)
|
||||
.HasColumnType("JSONB");
|
||||
}
|
||||
else if (databaseFacade.IsMySql())
|
||||
{
|
||||
builder.Entity<PayoutData>()
|
||||
.Property(o => o.Blob)
|
||||
.HasConversion(new ValueConverter<string, byte[]>
|
||||
(
|
||||
convertToProviderExpression: (str) => Encoding.UTF8.GetBytes(str),
|
||||
convertFromProviderExpression: (bytes) => Encoding.UTF8.GetString(bytes)
|
||||
));
|
||||
builder.Entity<PayoutData>()
|
||||
.Property(o => o.Proof)
|
||||
.HasConversion(new ValueConverter<string, byte[]>
|
||||
(
|
||||
convertToProviderExpression: (str) => Encoding.UTF8.GetBytes(str),
|
||||
convertFromProviderExpression: (bytes) => Encoding.UTF8.GetString(bytes)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// utility methods
|
||||
|
@ -3,8 +3,11 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using BTCPayServer.Client.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
@ -24,16 +27,33 @@ namespace BTCPayServer.Data
|
||||
public DateTimeOffset? EndDate { get; set; }
|
||||
public bool Archived { get; set; }
|
||||
public List<PayoutData> Payouts { get; set; }
|
||||
public byte[] Blob { get; set; }
|
||||
public string Blob { get; set; }
|
||||
|
||||
|
||||
internal static void OnModelCreating(ModelBuilder builder)
|
||||
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||
{
|
||||
builder.Entity<PullPaymentData>()
|
||||
.HasIndex(o => o.StoreId);
|
||||
builder.Entity<PullPaymentData>()
|
||||
.HasOne(o => o.StoreData)
|
||||
.HasOne(o => o.StoreData)
|
||||
.WithMany(o => o.PullPayments).OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
if (databaseFacade.IsNpgsql())
|
||||
{
|
||||
builder.Entity<PullPaymentData>()
|
||||
.Property(o => o.Blob)
|
||||
.HasColumnType("JSONB");
|
||||
}
|
||||
else if (databaseFacade.IsMySql())
|
||||
{
|
||||
builder.Entity<PullPaymentData>()
|
||||
.Property(o => o.Blob)
|
||||
.HasConversion(new ValueConverter<string, byte[]>
|
||||
(
|
||||
convertToProviderExpression: (str) => Encoding.UTF8.GetBytes(str),
|
||||
convertFromProviderExpression: (bytes) => Encoding.UTF8.GetString(bytes)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
public (DateTimeOffset Start, DateTimeOffset? End)? GetPeriod(DateTimeOffset now)
|
||||
|
@ -0,0 +1,39 @@
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20240104155620_AddApprovalToApplicationUser")]
|
||||
public partial class AddApprovalToApplicationUser : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "Approved",
|
||||
table: "AspNetUsers",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "RequiresApproval",
|
||||
table: "AspNetUsers",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Approved",
|
||||
table: "AspNetUsers");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RequiresApproval",
|
||||
table: "AspNetUsers");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20240220000000_FixWalletObjectsWithEmptyWalletId")]
|
||||
public partial class FixWalletObjectsWithEmptyWalletId : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
if (migrationBuilder.IsNpgsql())
|
||||
{
|
||||
migrationBuilder.Sql("DELETE FROM \"WalletObjects\" WHERE \"WalletId\"='';");
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20240229000000_PayoutAndPullPaymentToJsonBlob")]
|
||||
public partial class PayoutAndPullPaymentToJsonBlob : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
if (migrationBuilder.IsNpgsql())
|
||||
{
|
||||
migrationBuilder.Sql("ALTER TABLE \"Payouts\" ALTER COLUMN \"Blob\" TYPE JSONB USING regexp_replace(convert_from(\"Blob\",'UTF8'), '\\\\u0000', '', 'g')::JSONB");
|
||||
migrationBuilder.Sql("ALTER TABLE \"Payouts\" ALTER COLUMN \"Proof\" TYPE JSONB USING regexp_replace(convert_from(\"Proof\",'UTF8'), '\\\\u0000', '', 'g')::JSONB");
|
||||
migrationBuilder.Sql("ALTER TABLE \"PullPayments\" ALTER COLUMN \"Blob\" TYPE JSONB USING regexp_replace(convert_from(\"Blob\",'UTF8'), '\\\\u0000', '', 'g')::JSONB");
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20240229092905_AddManagerAndEmployeeToStoreRoles")]
|
||||
public partial class AddManagerAndEmployeeToStoreRoles : Migration
|
||||
{
|
||||
object GetPermissionsData(MigrationBuilder migrationBuilder, string[] permissions)
|
||||
{
|
||||
return migrationBuilder.IsNpgsql()
|
||||
? permissions
|
||||
: JsonConvert.SerializeObject(permissions);
|
||||
}
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
var permissionsType = migrationBuilder.IsNpgsql() ? "TEXT[]" : "TEXT";
|
||||
migrationBuilder.InsertData(
|
||||
"StoreRoles",
|
||||
columns: new[] { "Id", "Role", "Permissions" },
|
||||
columnTypes: new[] { "TEXT", "TEXT", permissionsType },
|
||||
values: new object[,]
|
||||
{
|
||||
{
|
||||
"Manager", "Manager", GetPermissionsData(migrationBuilder, new[]
|
||||
{
|
||||
"btcpay.store.canviewstoresettings",
|
||||
"btcpay.store.canmodifyinvoices",
|
||||
"btcpay.store.webhooks.canmodifywebhooks",
|
||||
"btcpay.store.canmodifypaymentrequests",
|
||||
"btcpay.store.canmanagepullpayments",
|
||||
"btcpay.store.canmanagepayouts"
|
||||
})
|
||||
},
|
||||
{
|
||||
"Employee", "Employee", GetPermissionsData(migrationBuilder, new[]
|
||||
{
|
||||
"btcpay.store.canmodifyinvoices",
|
||||
"btcpay.store.canmodifypaymentrequests",
|
||||
"btcpay.store.cancreatenonapprovedpullpayments",
|
||||
"btcpay.store.canviewpayouts",
|
||||
"btcpay.store.canviewpullpayments"
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
"StoreRoles",
|
||||
keyColumns: new[] { "Id" },
|
||||
keyColumnTypes: new[] { "TEXT" },
|
||||
keyValues: new[] { "Guest" },
|
||||
columns: new[] { "Permissions" },
|
||||
columnTypes: new[] { permissionsType },
|
||||
values: new object[]
|
||||
{
|
||||
GetPermissionsData(migrationBuilder, new[]
|
||||
{
|
||||
"btcpay.store.canmodifyinvoices",
|
||||
"btcpay.store.canviewpaymentrequests",
|
||||
"btcpay.store.canviewpullpayments",
|
||||
"btcpay.store.canviewpayouts"
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DeleteData("StoreRoles", "Id", "Manager");
|
||||
migrationBuilder.DeleteData("StoreRoles", "Id", "Employee");
|
||||
|
||||
var permissionsType = migrationBuilder.IsNpgsql() ? "TEXT[]" : "TEXT";
|
||||
migrationBuilder.UpdateData(
|
||||
"StoreRoles",
|
||||
keyColumns: new[] { "Id" },
|
||||
keyColumnTypes: new[] { "TEXT" },
|
||||
keyValues: new[] { "Guest" },
|
||||
columns: new[] { "Permissions" },
|
||||
columnTypes: new[] { permissionsType },
|
||||
values: new object[]
|
||||
{
|
||||
GetPermissionsData(migrationBuilder, new[]
|
||||
{
|
||||
"btcpay.store.canviewstoresettings",
|
||||
"btcpay.store.canmodifyinvoices",
|
||||
"btcpay.store.canviewcustodianaccounts",
|
||||
"btcpay.store.candeposittocustodianaccount"
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -16,25 +16,7 @@ namespace BTCPayServer.Migrations
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "6.0.9");
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Address")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset?>("CreatedTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("InvoiceDataId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Address");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("AddressInvoices");
|
||||
});
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.1");
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
|
||||
{
|
||||
@ -71,6 +53,24 @@ namespace BTCPayServer.Migrations
|
||||
b.ToTable("ApiKeys");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Address")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset?>("CreatedTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("InvoiceDataId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Address");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("AddressInvoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
@ -89,7 +89,7 @@ namespace BTCPayServer.Migrations
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Settings")
|
||||
.HasColumnType("JSONB");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("StoreDataId")
|
||||
.HasColumnType("TEXT");
|
||||
@ -112,6 +112,9 @@ namespace BTCPayServer.Migrations
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Approved")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<byte[]>("Blob")
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
@ -158,6 +161,9 @@ namespace BTCPayServer.Migrations
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("RequiresApproval")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("RequiresEmailConfirmation")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@ -305,6 +311,11 @@ namespace BTCPayServer.Migrations
|
||||
b.Property<string>("StoreDataId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<uint>("XMin")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Created");
|
||||
@ -588,7 +599,7 @@ namespace BTCPayServer.Migrations
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<byte[]>("Blob")
|
||||
.HasColumnType("BLOB");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("Date")
|
||||
.HasColumnType("TEXT");
|
||||
@ -602,7 +613,7 @@ namespace BTCPayServer.Migrations
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<byte[]>("Proof")
|
||||
.HasColumnType("BLOB");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PullPaymentDataId")
|
||||
.HasColumnType("TEXT");
|
||||
@ -693,7 +704,7 @@ namespace BTCPayServer.Migrations
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<byte[]>("Blob")
|
||||
.HasColumnType("BLOB");
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset?>("EndDate")
|
||||
.HasColumnType("TEXT");
|
||||
@ -781,31 +792,6 @@ namespace BTCPayServer.Migrations
|
||||
b.ToTable("Stores");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoredFile", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ApplicationUserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FileName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("StorageFileName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ApplicationUserId");
|
||||
|
||||
b.ToTable("Files");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoreRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
@ -863,6 +849,31 @@ namespace BTCPayServer.Migrations
|
||||
b.ToTable("StoreWebhooks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoredFile", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ApplicationUserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FileName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("StorageFileName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ApplicationUserId");
|
||||
|
||||
b.ToTable("Files");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.U2FDevice", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
@ -1171,16 +1182,6 @@ namespace BTCPayServer.Migrations
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("AddressInvoices")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.Navigation("InvoiceData");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
@ -1198,6 +1199,16 @@ namespace BTCPayServer.Migrations
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("AddressInvoices")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.Navigation("InvoiceData");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
@ -1408,15 +1419,6 @@ namespace BTCPayServer.Migrations
|
||||
b.Navigation("PullPaymentData");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoredFile", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser")
|
||||
.WithMany("StoredFiles")
|
||||
.HasForeignKey("ApplicationUserId");
|
||||
|
||||
b.Navigation("ApplicationUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoreRole", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
@ -1457,6 +1459,15 @@ namespace BTCPayServer.Migrations
|
||||
b.Navigation("Webhook");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoredFile", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser")
|
||||
.WithMany("StoredFiles")
|
||||
.HasForeignKey("ApplicationUserId");
|
||||
|
||||
b.Navigation("ApplicationUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.U2FDevice", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser")
|
||||
|
@ -4,9 +4,9 @@
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" />
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.9" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.32" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.37" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="1.0.4" />
|
||||
</ItemGroup>
|
||||
|
@ -1,4 +1,4 @@
|
||||
[
|
||||
[
|
||||
{
|
||||
"name":"Afghan Afghani",
|
||||
"code":"AFN",
|
||||
@ -58,7 +58,7 @@
|
||||
{
|
||||
"name":"Argentine Peso",
|
||||
"code":"ARS",
|
||||
"divisibility":2,
|
||||
"divisibility":0,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
@ -289,7 +289,7 @@
|
||||
{
|
||||
"name":"Colombian Peso",
|
||||
"code":"COP",
|
||||
"divisibility":2,
|
||||
"divisibility":0,
|
||||
"symbol":null,
|
||||
"crypto":false
|
||||
},
|
||||
|
@ -64,11 +64,28 @@ 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);
|
||||
var symbol = new RegionInfo(culture.LCID).ISOCurrencySymbol;
|
||||
var c = symbol switch
|
||||
{
|
||||
// ARS and COP are officially 2 digits, but due to depreciation,
|
||||
// nobody really use those anymore. (See https://github.com/btcpayserver/btcpayserver/issues/5708)
|
||||
"ARS" or "COP" => ModifyCurrencyDecimalDigit(culture, 0),
|
||||
_ => culture
|
||||
};
|
||||
_CurrencyProviders.TryAdd(symbol, c);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
@ -82,6 +99,15 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
}
|
||||
|
||||
private CultureInfo ModifyCurrencyDecimalDigit(CultureInfo culture, int decimals)
|
||||
{
|
||||
var modifiedCulture = new CultureInfo(culture.Name);
|
||||
NumberFormatInfo modifiedNumberFormat = (NumberFormatInfo)modifiedCulture.NumberFormat.Clone();
|
||||
modifiedNumberFormat.CurrencyDecimalDigits = decimals;
|
||||
modifiedCulture.NumberFormat = modifiedNumberFormat;
|
||||
return modifiedCulture;
|
||||
}
|
||||
|
||||
private void AddCurrency(Dictionary<string, IFormatProvider> currencyProviders, string code, int divisibility, string symbol)
|
||||
{
|
||||
var culture = new CultureInfo("en-US");
|
||||
|
@ -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)
|
||||
{
|
||||
|
40
BTCPayServer.Rating/Providers/BitnobRateProvider.cs
Normal file
40
BTCPayServer.Rating/Providers/BitnobRateProvider.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Rating.Providers
|
||||
{
|
||||
public class BitnobRateProvider : IRateProvider
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
public BitnobRateProvider(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient ?? new HttpClient();
|
||||
}
|
||||
public RateSourceInfo RateSourceInfo => new("bitnob", "Bitnob", "https://api.bitnob.co/api/v1/rates/bitcoin/price");
|
||||
|
||||
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using var response = await _httpClient.GetAsync("https://api.bitnob.co/api/v1/rates/bitcoin/price", cancellationToken);
|
||||
JObject jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
|
||||
var dataObject = jobj["data"] as JObject;
|
||||
|
||||
if (dataObject == null)
|
||||
{
|
||||
return Array.Empty<PairRate>();
|
||||
}
|
||||
var pairRates = new List<PairRate>();
|
||||
foreach (var property in dataObject.Properties())
|
||||
{
|
||||
string[] parts = property.Name.Split('_');
|
||||
decimal value = property.Value.Value<decimal>();
|
||||
pairRates.Add(new PairRate(new CurrencyPair("BTC", parts[1]), new BidAsk(value)));
|
||||
}
|
||||
return pairRates.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@ namespace BTCPayServer.Services.Rates;
|
||||
|
||||
public class FreeCurrencyRatesRateProvider : IRateProvider
|
||||
{
|
||||
public RateSourceInfo RateSourceInfo => new("free-currency-rates", "Free Currency Rates", "https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api@1/latest/currencies/btc.min.json");
|
||||
public RateSourceInfo RateSourceInfo => new("free-currency-rates", "Free Currency Rates", "https://currency-api.pages.dev/v1/currencies/btc.min.json");
|
||||
private readonly HttpClient _httpClient;
|
||||
public FreeCurrencyRatesRateProvider(HttpClient httpClient)
|
||||
{
|
||||
|
@ -8,6 +8,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using ExchangeSharp;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
@ -16,7 +17,7 @@ namespace BTCPayServer.Services.Rates
|
||||
// Make sure that only one request is sent to kraken in general
|
||||
public class KrakenExchangeRateProvider : IRateProvider
|
||||
{
|
||||
public RateSourceInfo RateSourceInfo => new("kraken", "Kraken", "https://api.kraken.com/0/public/Ticker?pair=ATOMETH,ATOMEUR,ATOMUSD,ATOMXBT,BATETH,BATEUR,BATUSD,BATXBT,BCHEUR,BCHUSD,BCHXBT,DAIEUR,DAIUSD,DAIUSDT,DASHEUR,DASHUSD,DASHXBT,EOSETH,EOSXBT,ETHCHF,ETHDAI,ETHUSDC,ETHUSDT,GNOETH,GNOXBT,ICXETH,ICXEUR,ICXUSD,ICXXBT,LINKETH,LINKEUR,LINKUSD,LINKXBT,LSKETH,LSKEUR,LSKUSD,LSKXBT,NANOETH,NANOEUR,NANOUSD,NANOXBT,OMGETH,OMGEUR,OMGUSD,OMGXBT,PAXGETH,PAXGEUR,PAXGUSD,PAXGXBT,SCETH,SCEUR,SCUSD,SCXBT,USDCEUR,USDCUSD,USDCUSDT,USDTCAD,USDTEUR,USDTGBP,USDTZUSD,WAVESETH,WAVESEUR,WAVESUSD,WAVESXBT,XBTCHF,XBTDAI,XBTUSDC,XBTUSDT,XDGEUR,XDGUSD,XETCXETH,XETCXXBT,XETCZEUR,XETCZUSD,XETHXXBT,XETHZCAD,XETHZEUR,XETHZGBP,XETHZJPY,XETHZUSD,XLTCXXBT,XLTCZEUR,XLTCZUSD,XMLNXETH,XMLNXXBT,XMLNZEUR,XMLNZUSD,XREPXETH,XREPXXBT,XREPZEUR,XXBTZCAD,XXBTZEUR,XXBTZGBP,XXBTZJPY,XXBTZUSD,XXDGXXBT,XXLMXXBT,XXMRXXBT,XXMRZEUR,XXMRZUSD,XXRPXXBT,XXRPZEUR,XXRPZUSD,XZECXXBT,XZECZEUR,XZECZUSD");
|
||||
public RateSourceInfo RateSourceInfo => new("kraken", "Kraken", "https://api.kraken.com/0/public/Ticker");
|
||||
public HttpClient HttpClient
|
||||
{
|
||||
get
|
||||
@ -31,39 +32,6 @@ namespace BTCPayServer.Services.Rates
|
||||
|
||||
HttpClient _LocalClient;
|
||||
static readonly HttpClient _Client = new HttpClient();
|
||||
|
||||
// ExchangeSymbolToGlobalSymbol throws exception which would kill perf
|
||||
readonly ConcurrentDictionary<string, string> notFoundSymbols = new ConcurrentDictionary<string, string>(new Dictionary<string, string>()
|
||||
{
|
||||
{"ADAXBT","ADAXBT"},
|
||||
{ "BSVUSD","BSVUSD"},
|
||||
{ "QTUMEUR","QTUMEUR"},
|
||||
{ "QTUMXBT","QTUMXBT"},
|
||||
{ "EOSUSD","EOSUSD"},
|
||||
{ "XTZUSD","XTZUSD"},
|
||||
{ "XREPZUSD","XREPZUSD"},
|
||||
{ "ADAEUR","ADAEUR"},
|
||||
{ "ADAUSD","ADAUSD"},
|
||||
{ "GNOEUR","GNOEUR"},
|
||||
{ "XTZETH","XTZETH"},
|
||||
{ "XXRPZJPY","XXRPZJPY"},
|
||||
{ "XXRPZCAD","XXRPZCAD"},
|
||||
{ "XTZEUR","XTZEUR"},
|
||||
{ "QTUMETH","QTUMETH"},
|
||||
{ "XXLMZUSD","XXLMZUSD"},
|
||||
{ "QTUMCAD","QTUMCAD"},
|
||||
{ "QTUMUSD","QTUMUSD"},
|
||||
{ "XTZXBT","XTZXBT"},
|
||||
{ "GNOUSD","GNOUSD"},
|
||||
{ "ADAETH","ADAETH"},
|
||||
{ "ADACAD","ADACAD"},
|
||||
{ "XTZCAD","XTZCAD"},
|
||||
{ "BSVEUR","BSVEUR"},
|
||||
{ "XZECZJPY","XZECZJPY"},
|
||||
{ "XXLMZEUR","XXLMZEUR"},
|
||||
{"EOSEUR","EOSEUR"},
|
||||
{"BSVXBT","BSVXBT"}
|
||||
});
|
||||
string[] _Symbols = Array.Empty<string>();
|
||||
DateTimeOffset? _LastSymbolUpdate = null;
|
||||
readonly Dictionary<string, string> _TickerMapping = new Dictionary<string, string>()
|
||||
@ -76,48 +44,57 @@ namespace BTCPayServer.Services.Rates
|
||||
{ "ZEUR", "EUR" },
|
||||
{ "ZJPY", "JPY" },
|
||||
{ "ZCAD", "CAD" },
|
||||
{ "ZGBP", "GBP" }
|
||||
{ "ZGBP", "GBP" },
|
||||
{ "XXMR", "XMR" },
|
||||
{ "XETH", "ETH" },
|
||||
{ "USDC", "USDC" }, // On A=A purpose
|
||||
{ "XZEC", "ZEC" },
|
||||
{ "XLTC", "LTC" },
|
||||
{ "XXRP", "XRP" },
|
||||
};
|
||||
|
||||
string Normalize(string ticker)
|
||||
{
|
||||
_TickerMapping.TryGetValue(ticker, out var normalized);
|
||||
return normalized ?? ticker;
|
||||
}
|
||||
|
||||
readonly ConcurrentDictionary<string, CurrencyPair> CachedCurrencyPairs = new ConcurrentDictionary<string, CurrencyPair>();
|
||||
private CurrencyPair GetCurrencyPair(string symbol)
|
||||
{
|
||||
if (CachedCurrencyPairs.TryGetValue(symbol, out var pair))
|
||||
return pair;
|
||||
var found = _TickerMapping.Where(t => symbol.StartsWith(t.Key, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(t => new { KrakenTicker = t.Key, PayTicker = t.Value }).FirstOrDefault();
|
||||
if (found is not null)
|
||||
{
|
||||
pair = new CurrencyPair(found.PayTicker, Normalize(symbol.Substring(found.KrakenTicker.Length)));
|
||||
}
|
||||
if (pair is null)
|
||||
{
|
||||
found = _TickerMapping.Where(t => symbol.EndsWith(t.Key, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(t => new { KrakenTicker = t.Key, PayTicker = t.Value }).FirstOrDefault();
|
||||
if (found is not null)
|
||||
pair = new CurrencyPair(Normalize(symbol.Substring(0, symbol.Length - found.KrakenTicker.Length)), found.PayTicker);
|
||||
}
|
||||
if (pair is null)
|
||||
CurrencyPair.TryParse(symbol, out pair);
|
||||
CachedCurrencyPairs.TryAdd(symbol, pair);
|
||||
return pair;
|
||||
}
|
||||
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var result = new List<PairRate>();
|
||||
var symbols = await GetSymbolsAsync(cancellationToken);
|
||||
var helper = (ExchangeKrakenAPI)await ExchangeAPI.GetExchangeAPIAsync<ExchangeKrakenAPI>();
|
||||
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>>();
|
||||
JToken apiTickers = await MakeJsonRequestAsync<JToken>("/0/public/Ticker", null, null, cancellationToken: cancellationToken);
|
||||
foreach (string symbol in symbols)
|
||||
{
|
||||
var ticker = ConvertToExchangeTicker(symbol, apiTickers[symbol]);
|
||||
if (ticker != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
string global = null;
|
||||
var mapped1 = _TickerMapping.Where(t => symbol.StartsWith(t.Key, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(t => new { KrakenTicker = t.Key, PayTicker = t.Value }).SingleOrDefault();
|
||||
if (mapped1 != null)
|
||||
{
|
||||
var p2 = symbol.Substring(mapped1.KrakenTicker.Length);
|
||||
if (_TickerMapping.TryGetValue(p2, out var mapped2))
|
||||
p2 = mapped2;
|
||||
global = $"{mapped1.PayTicker}_{p2}";
|
||||
}
|
||||
else
|
||||
{
|
||||
global = await helper.ExchangeMarketSymbolToGlobalMarketSymbolAsync(symbol);
|
||||
}
|
||||
if (CurrencyPair.TryParse(global, out var pair))
|
||||
result.Add(new PairRate(pair, new BidAsk(ticker.Bid, ticker.Ask)));
|
||||
else
|
||||
notFoundSymbols.TryAdd(symbol, symbol);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
notFoundSymbols.TryAdd(symbol, symbol);
|
||||
}
|
||||
var pair = GetCurrencyPair(symbol);
|
||||
if (pair is not null)
|
||||
result.Add(new PairRate(pair, new BidAsk(ticker.Bid, ticker.Ask)));
|
||||
}
|
||||
}
|
||||
return result.ToArray();
|
||||
|
@ -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))))
|
||||
{
|
||||
|
@ -125,7 +125,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.IsType<RedirectToActionResult>(response);
|
||||
|
||||
// Setting it again should show the confirmation page
|
||||
response = await controller.UpdateWallet(new WalletSetupViewModel {StoreId = storeId, CryptoCode = cryptoCode, DerivationScheme = oldScheme });
|
||||
response = await controller.UpdateWallet(new WalletSetupViewModel { StoreId = storeId, CryptoCode = cryptoCode, DerivationScheme = oldScheme });
|
||||
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
|
||||
Assert.True(setupVm.Confirmation);
|
||||
|
||||
@ -133,7 +133,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// cobo vault file
|
||||
var content = "{\"ExtPubKey\":\"xpub6CEqRFZ7yZxCFXuEWZBAdnC8bdvu9SRHevaoU2SsW9ZmKhrCShmbpGZWwaR15hdLURf8hg47g4TpPGaqEU8hw5LEJCE35AUhne67XNyFGBk\",\"MasterFingerprint\":\"7a7563b5\",\"DerivationPath\":\"M\\/84'\\/0'\\/0'\",\"CoboVaultFirmwareVersion\":\"1.2.0(BTC-Only)\"}";
|
||||
response = await controller.UpdateWallet(new WalletSetupViewModel {StoreId = storeId, CryptoCode = cryptoCode, WalletFile = TestUtils.GetFormFile("cobovault.json", content)});
|
||||
response = await controller.UpdateWallet(new WalletSetupViewModel { StoreId = storeId, CryptoCode = cryptoCode, WalletFile = TestUtils.GetFormFile("cobovault.json", content) });
|
||||
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
|
||||
Assert.True(setupVm.Confirmation);
|
||||
response = await controller.UpdateWallet(setupVm);
|
||||
@ -144,7 +144,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// wasabi wallet file
|
||||
content = "{\r\n \"EncryptedSecret\": \"6PYWBQ1zsukowsnTNA57UUx791aBuJusm7E4egXUmF5WGw3tcdG3cmTL57\",\r\n \"ChainCode\": \"waSIVbn8HaoovoQg/0t8IS1+ZCxGsJRGFT21i06nWnc=\",\r\n \"MasterFingerprint\": \"7a7563b5\",\r\n \"ExtPubKey\": \"xpub6CEqRFZ7yZxCFXuEWZBAdnC8bdvu9SRHevaoU2SsW9ZmKhrCShmbpGZWwaR15hdLURf8hg47g4TpPGaqEU8hw5LEJCE35AUhne67XNyFGBk\",\r\n \"PasswordVerified\": false,\r\n \"MinGapLimit\": 21,\r\n \"AccountKeyPath\": \"84'/0'/0'\",\r\n \"BlockchainState\": {\r\n \"Network\": \"RegTest\",\r\n \"Height\": \"0\"\r\n },\r\n \"HdPubKeys\": []\r\n}";
|
||||
response = await controller.UpdateWallet(new WalletSetupViewModel {StoreId = storeId, CryptoCode = cryptoCode, WalletFile = TestUtils.GetFormFile("wasabi.json", content)});
|
||||
response = await controller.UpdateWallet(new WalletSetupViewModel { StoreId = storeId, CryptoCode = cryptoCode, WalletFile = TestUtils.GetFormFile("wasabi.json", content) });
|
||||
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
|
||||
Assert.True(setupVm.Confirmation);
|
||||
response = await controller.UpdateWallet(setupVm);
|
||||
@ -155,13 +155,13 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// Can we upload coldcard settings? (Should fail, we are giving a mainnet file to a testnet network)
|
||||
content = "{\"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}";
|
||||
response = await controller.UpdateWallet(new WalletSetupViewModel {StoreId = storeId, CryptoCode = cryptoCode, WalletFile = TestUtils.GetFormFile("coldcard-ypub.json", content)});
|
||||
response = await controller.UpdateWallet(new WalletSetupViewModel { StoreId = storeId, CryptoCode = cryptoCode, WalletFile = TestUtils.GetFormFile("coldcard-ypub.json", content) });
|
||||
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
|
||||
Assert.False(setupVm.Confirmation); // Should fail, we are giving a mainnet file to a testnet network
|
||||
|
||||
// And with a good file? (upub)
|
||||
content = "{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"upub5DBYp1qGgsTrkzCptMGZc2x18pquLwGrBw6nS59T4NViZ4cni1mGowQzziy85K8vzkp1jVtWrSkLhqk9KDfvrGeB369wGNYf39kX8rQfiLn\", \"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}";
|
||||
response = await controller.UpdateWallet(new WalletSetupViewModel {StoreId = storeId, CryptoCode = cryptoCode, WalletFile = TestUtils.GetFormFile("coldcard-upub.json", content)});
|
||||
response = await controller.UpdateWallet(new WalletSetupViewModel { StoreId = storeId, CryptoCode = cryptoCode, WalletFile = TestUtils.GetFormFile("coldcard-upub.json", content) });
|
||||
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
|
||||
Assert.True(setupVm.Confirmation);
|
||||
response = await controller.UpdateWallet(setupVm);
|
||||
@ -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);
|
||||
|
||||
@ -430,6 +430,7 @@ namespace BTCPayServer.Tests
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Altcoins", "Altcoins")]
|
||||
[Trait("Lightning", "Lightning")]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanUsePaymentMethodDropdown()
|
||||
{
|
||||
using (var s = CreateSeleniumTester())
|
||||
@ -438,10 +439,10 @@ namespace BTCPayServer.Tests
|
||||
s.Server.ActivateLightning();
|
||||
await s.StartAsync();
|
||||
s.GoToRegister();
|
||||
s.RegisterNewUser();
|
||||
s.RegisterNewUser(true);
|
||||
s.CreateNewStore();
|
||||
s.AddDerivationScheme("BTC");
|
||||
|
||||
s.EnableCheckout(Client.Models.CheckoutType.V1);
|
||||
//check that there is no dropdown since only one payment method is set
|
||||
var invoiceId = s.CreateInvoice(10, "USD", "a@g.com");
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
@ -454,18 +455,25 @@ namespace BTCPayServer.Tests
|
||||
invoiceId = s.CreateInvoice(10, "USD", "a@g.com");
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
var currencyDropdownButton = s.Driver.FindElement(By.ClassName("payment__currencies"));
|
||||
Assert.Contains("BTC", currencyDropdownButton.Text);
|
||||
Assert.Contains("Bitcoin", currencyDropdownButton.Text);
|
||||
currencyDropdownButton.Click();
|
||||
|
||||
var elements = s.Driver.FindElement(By.ClassName("vex-content")).FindElements(By.ClassName("vexmenuitem"));
|
||||
Assert.Equal(3, elements.Count);
|
||||
elements.Single(element => element.Text.Contains("LTC")).Click();
|
||||
IEnumerable<IWebElement> elements = null;
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
elements = s.Driver.FindElement(By.ClassName("vex-content")).FindElements(By.ClassName("vexmenuitem"));
|
||||
Assert.Equal(3, elements.Count());
|
||||
elements.Single(element => element.Text.Contains("Litecoin")).Click();
|
||||
});
|
||||
currencyDropdownButton = s.Driver.FindElement(By.ClassName("payment__currencies"));
|
||||
Assert.Contains("LTC", currencyDropdownButton.Text);
|
||||
Assert.Contains("Litecoin", currencyDropdownButton.Text);
|
||||
currencyDropdownButton.Click();
|
||||
|
||||
elements = s.Driver.FindElement(By.ClassName("vex-content")).FindElements(By.ClassName("vexmenuitem"));
|
||||
elements.Single(element => element.Text.Contains("Lightning")).Click();
|
||||
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
elements = s.Driver.FindElement(By.ClassName("vex-content")).FindElements(By.ClassName("vexmenuitem"));
|
||||
elements.Single(element => element.Text.Contains("Lightning")).Click();
|
||||
});
|
||||
|
||||
currencyDropdownButton = s.Driver.FindElement(By.ClassName("payment__currencies"));
|
||||
Assert.Contains("Lightning", currencyDropdownButton.Text);
|
||||
@ -754,7 +762,7 @@ inventoryitem:
|
||||
inventory: 1
|
||||
noninventoryitem:
|
||||
price: 10.0";
|
||||
|
||||
|
||||
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
|
||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
|
||||
@ -866,7 +874,7 @@ g:
|
||||
Assert.Contains(items, item => item.Id == "e" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Minimum);
|
||||
Assert.Contains(items, item => item.Id == "f" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup);
|
||||
Assert.Contains(items, item => item.Id == "g" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup);
|
||||
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Static, choiceKey: "g").Result);
|
||||
invoices = user.BitPay.GetInvoices();
|
||||
|
@ -24,9 +24,9 @@
|
||||
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.15" />
|
||||
<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="Selenium.WebDriver.ChromeDriver" Version="121.0.6167.8500" />
|
||||
<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>
|
||||
|
@ -245,6 +245,9 @@ namespace BTCPayServer.Tests
|
||||
rateProvider.Providers.Add("kraken", kraken);
|
||||
}
|
||||
|
||||
// reset test server policies
|
||||
var settings = GetService<SettingsRepository>();
|
||||
await settings.UpdateSetting(new PoliciesSettings { LockSubscription = false, RequiresUserApproval = false });
|
||||
|
||||
TestLogs.LogInformation("Waiting site is operational...");
|
||||
await WaitSiteIsOperational();
|
||||
|
@ -39,7 +39,6 @@ namespace BTCPayServer.Tests
|
||||
var supportUrl = "https://support.satoshisteaks.com/{InvoiceId}/";
|
||||
s.GoToStore();
|
||||
s.Driver.FindElement(By.Id("StoreWebsite")).SendKeys(storeUrl);
|
||||
s.Driver.FindElement(By.Id("StoreSupportUrl")).SendKeys(supportUrl);
|
||||
s.Driver.FindElement(By.Id("Save")).Click();
|
||||
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
|
||||
|
||||
@ -47,6 +46,7 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.WaitForAndClick(By.Id("Presets"));
|
||||
s.Driver.WaitForAndClick(By.Id("Presets_InStore"));
|
||||
Assert.True(s.Driver.SetCheckbox(By.Id("ShowPayInWalletButton"), true));
|
||||
s.Driver.FindElement(By.Id("SupportUrl")).SendKeys(supportUrl);
|
||||
s.Driver.FindElement(By.Id("Save")).SendKeys(Keys.Enter);
|
||||
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
|
||||
|
||||
@ -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"));
|
||||
|
||||
@ -130,9 +130,12 @@ namespace BTCPayServer.Tests
|
||||
expirySeconds.SendKeys("3");
|
||||
s.Driver.FindElement(By.Id("Expire")).Click();
|
||||
|
||||
var paymentInfo = s.Driver.WaitForElement(By.Id("PaymentInfo"));
|
||||
Assert.Contains("This invoice will expire in", paymentInfo.Text);
|
||||
Assert.DoesNotContain("Please send", paymentInfo.Text);
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
var paymentInfo = s.Driver.WaitForElement(By.Id("PaymentInfo"));
|
||||
Assert.Contains("This invoice will expire in", paymentInfo.Text);
|
||||
Assert.DoesNotContain("Please send", paymentInfo.Text);
|
||||
});
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
var expiredSection = s.Driver.FindElement(By.Id("unpaid"));
|
||||
@ -140,7 +143,6 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("Invoice Expired", expiredSection.Text);
|
||||
Assert.Contains("resubmit a payment", expiredSection.Text);
|
||||
Assert.DoesNotContain("This invoice expired with partial payment", expiredSection.Text);
|
||||
|
||||
});
|
||||
Assert.True(s.Driver.ElementDoesNotExist(By.Id("ContactLink")));
|
||||
Assert.True(s.Driver.ElementDoesNotExist(By.Id("ReceiptLink")));
|
||||
@ -153,7 +155,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));
|
||||
@ -164,9 +166,12 @@ namespace BTCPayServer.Tests
|
||||
expirySeconds.SendKeys("3");
|
||||
s.Driver.FindElement(By.Id("Expire")).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);
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
var 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);
|
||||
});
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
var expiredSection = s.Driver.FindElement(By.Id("unpaid"));
|
||||
@ -202,16 +207,15 @@ 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(() =>
|
||||
{
|
||||
paymentInfo = s.Driver.WaitForElement(By.Id("PaymentInfo"));
|
||||
s.Driver.FindElement(By.Id("FakePayment")).Click();
|
||||
s.Driver.FindElement(By.Id("mine-block")).Click();
|
||||
var 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 +269,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 +338,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");
|
||||
@ -358,11 +364,13 @@ namespace BTCPayServer.Tests
|
||||
expirySeconds.Clear();
|
||||
expirySeconds.SendKeys("5");
|
||||
s.Driver.FindElement(By.Id("Expire")).Click();
|
||||
|
||||
paymentInfo = s.Driver.WaitForElement(By.Id("PaymentInfo"));
|
||||
Assert.Contains("This invoice will expire in", paymentInfo.Text);
|
||||
Assert.Contains("00:0", paymentInfo.Text);
|
||||
Assert.DoesNotContain("Please send", paymentInfo.Text);
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
var paymentInfo = s.Driver.WaitForElement(By.Id("PaymentInfo"));
|
||||
Assert.Contains("This invoice will expire in", paymentInfo.Text);
|
||||
Assert.Contains("00:0", paymentInfo.Text);
|
||||
Assert.DoesNotContain("Please send", paymentInfo.Text);
|
||||
});
|
||||
|
||||
// Configure countdown timer
|
||||
s.GoToHome();
|
||||
@ -378,7 +386,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
|
||||
paymentInfo = s.Driver.FindElement(By.Id("PaymentInfo"));
|
||||
var paymentInfo = s.Driver.FindElement(By.Id("PaymentInfo"));
|
||||
Assert.False(paymentInfo.Displayed);
|
||||
Assert.DoesNotContain("This invoice will expire in", paymentInfo.Text);
|
||||
|
||||
@ -386,11 +394,13 @@ namespace BTCPayServer.Tests
|
||||
expirySeconds.Clear();
|
||||
expirySeconds.SendKeys("599");
|
||||
s.Driver.FindElement(By.Id("Expire")).Click();
|
||||
|
||||
paymentInfo = s.Driver.WaitForElement(By.Id("PaymentInfo"));
|
||||
Assert.True(paymentInfo.Displayed);
|
||||
Assert.Contains("This invoice will expire in", paymentInfo.Text);
|
||||
Assert.Contains("09:5", paymentInfo.Text);
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
paymentInfo = s.Driver.WaitForElement(By.Id("PaymentInfo"));
|
||||
Assert.True(paymentInfo.Displayed);
|
||||
Assert.Contains("This invoice will expire in", paymentInfo.Text);
|
||||
Assert.Contains("09:5", paymentInfo.Text);
|
||||
});
|
||||
|
||||
// Disable LNURL again
|
||||
s.GoToHome();
|
||||
@ -456,13 +466,12 @@ namespace BTCPayServer.Tests
|
||||
.GetPaymentMethodDetails().GetPaymentDestination(), Network.RegTest),
|
||||
new Money(0.001m, MoneyUnit.BTC));
|
||||
|
||||
IWebElement closebutton = null;
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
closebutton = iframe.FindElement(By.Id("close"));
|
||||
Assert.True(closebutton.Displayed);
|
||||
var closeButton = iframe.FindElement(By.Id("close"));
|
||||
Assert.True(closeButton.Displayed);
|
||||
closeButton.Click();
|
||||
});
|
||||
closebutton.Click();
|
||||
s.Driver.AssertElementNotFound(By.Name("btcpay"));
|
||||
Assert.Equal(s.Driver.Url,
|
||||
new Uri(s.ServerUri, $"tests/index.html?invoice={invoiceId}").ToString());
|
||||
|
@ -1,15 +1,19 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Forms;
|
||||
using BTCPayServer.Forms.Models;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Plugins.Crowdfund;
|
||||
using BTCPayServer.Plugins.Crowdfund.Controllers;
|
||||
using BTCPayServer.Plugins.Crowdfund.Models;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
@ -303,5 +307,114 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(0.7m, model.Info.CurrentPendingAmount);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CrowdfundWithFormNoPerk()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
await user.GrantAccessAsync();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
await user.SetNetworkFeeMode(NetworkFeeMode.Never);
|
||||
|
||||
var frmService = tester.PayTester.GetService<FormDataService>();
|
||||
var appService = tester.PayTester.GetService<AppService>();
|
||||
var crowdfund = user.GetController<UICrowdfundController>();
|
||||
var apps = user.GetController<UIAppsController>();
|
||||
var appData = new AppData { StoreDataId = user.StoreId, Name = "test", AppType = CrowdfundAppType.AppType };
|
||||
await appService.UpdateOrCreateApp(appData);
|
||||
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||
var app = appList.Apps[0];
|
||||
apps.HttpContext.SetAppData(appData);
|
||||
crowdfund.HttpContext.SetAppData(appData);
|
||||
|
||||
var form = new Form
|
||||
{
|
||||
Fields =
|
||||
[
|
||||
Field.Create("Enter your email", "item1", "test@toto.com", true, null, "email"),
|
||||
Field.Create("Name", "item2", 2.ToString(), true, null),
|
||||
Field.Create("Item3", "invoice_item3", 3.ToString(), true, null)
|
||||
]
|
||||
};
|
||||
var frmData = new FormData
|
||||
{
|
||||
StoreId = user.StoreId,
|
||||
Name = "frmTest",
|
||||
Config = form.ToString()
|
||||
};
|
||||
await frmService.AddOrUpdateForm(frmData);
|
||||
|
||||
var lstForms = await frmService.GetForms(user.StoreId);
|
||||
Assert.NotEmpty(lstForms);
|
||||
|
||||
var crowdfundViewModel = await crowdfund.UpdateCrowdfund(app.Id).AssertViewModelAsync<UpdateCrowdfundViewModel>();
|
||||
crowdfundViewModel.FormId = lstForms[0].Id;
|
||||
crowdfundViewModel.TargetCurrency = "BTC";
|
||||
crowdfundViewModel.Enabled = true;
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
|
||||
var vm2 = await crowdfund.CrowdfundForm(app.Id, (decimal?)0.01).AssertViewModelAsync<FormViewModel>();
|
||||
var res = await crowdfund.CrowdfundFormSubmit(app.Id, (decimal)0.01, "", vm2);
|
||||
Assert.IsNotType<NotFoundObjectResult>(res);
|
||||
Assert.IsNotType<BadRequest>(res);
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CrowdfundWithFormAndPerk()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
await user.GrantAccessAsync();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
await user.SetNetworkFeeMode(NetworkFeeMode.Never);
|
||||
|
||||
var frmService = tester.PayTester.GetService<FormDataService>();
|
||||
var appService = tester.PayTester.GetService<AppService>();
|
||||
var crowdfund = user.GetController<UICrowdfundController>();
|
||||
var apps = user.GetController<UIAppsController>();
|
||||
var appData = new AppData { StoreDataId = user.StoreId, Name = "test", AppType = CrowdfundAppType.AppType };
|
||||
await appService.UpdateOrCreateApp(appData);
|
||||
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||
var app = appList.Apps[0];
|
||||
apps.HttpContext.SetAppData(appData);
|
||||
crowdfund.HttpContext.SetAppData(appData);
|
||||
|
||||
var form = new Form
|
||||
{
|
||||
Fields =
|
||||
[
|
||||
Field.Create("Enter your email", "item1", "test@toto.com", true, null, "email"),
|
||||
Field.Create("Name", "item2", 2.ToString(), true, null),
|
||||
Field.Create("Item3", "invoice_item3", 3.ToString(), true, null)
|
||||
]
|
||||
};
|
||||
var frmData = new FormData
|
||||
{
|
||||
StoreId = user.StoreId,
|
||||
Name = "frmTest",
|
||||
Config = form.ToString()
|
||||
};
|
||||
await frmService.AddOrUpdateForm(frmData);
|
||||
|
||||
var lstForms = await frmService.GetForms(user.StoreId);
|
||||
Assert.NotEmpty(lstForms);
|
||||
|
||||
var crowdfundViewModel = await crowdfund.UpdateCrowdfund(app.Id).AssertViewModelAsync<UpdateCrowdfundViewModel>();
|
||||
crowdfundViewModel.FormId = lstForms[0].Id;
|
||||
crowdfundViewModel.TargetCurrency = "BTC";
|
||||
crowdfundViewModel.Enabled = true;
|
||||
crowdfundViewModel.PerksTemplate = "[{\"id\": \"xxx\",\"title\": \"Perk 1\",\"priceType\": \"Fixed\",\"price\": \"0.001\",\"image\": \"\",\"description\": \"\",\"categories\": [],\"disabled\": false}]";
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
|
||||
var vm2 = await crowdfund.CrowdfundForm(app.Id, (decimal?)0.01, "xxx").AssertViewModelAsync<FormViewModel>();
|
||||
var res = await crowdfund.CrowdfundFormSubmit(app.Id, (decimal)0.01, "xxx", vm2);
|
||||
Assert.IsNotType<NotFoundObjectResult>(res);
|
||||
Assert.IsNotType<BadRequest>(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
@ -756,13 +790,15 @@ namespace BTCPayServer.Tests
|
||||
(0.0005m, "0.0005 USD", "USD"), (0.001m, "0.001 USD", "USD"), (0.01m, "0.01 USD", "USD"),
|
||||
(0.1m, "0.10 USD", "USD"), (0.1m, "0,10 EUR", "EUR"), (1000m, "1,000 JPY", "JPY"),
|
||||
(1000.0001m, "1,000.00 INR", "INR"),
|
||||
(0.0m, "0.00 USD", "USD")
|
||||
(0.0m, "0.00 USD", "USD"), (1m, "1 COP", "COP"), (1m, "1 ARS", "ARS")
|
||||
})
|
||||
{
|
||||
var actual = displayFormatter.Currency(test.Item1, test.Item3);
|
||||
actual = actual.Replace("¥", "¥"); // Hack so JPY test pass on linux as well
|
||||
Assert.Equal(test.Item2, actual);
|
||||
}
|
||||
Assert.Equal(0, CurrencyNameTable.Instance.GetNumberFormatInfo("ARS").CurrencyDecimalDigits);
|
||||
Assert.Equal(0, CurrencyNameTable.Instance.GetNumberFormatInfo("COP").CurrencyDecimalDigits);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -882,6 +918,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 +934,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 +949,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 +974,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]
|
||||
@ -1209,17 +1347,22 @@ Assert.Equal("2b0e251e", nunchuk.AccountKeySettings[1].RootFingerprint.ToString(
|
||||
|
||||
filter = "status:abed, status:abed2";
|
||||
search = new SearchString(filter);
|
||||
Assert.Equal("", search.TextSearch);
|
||||
Assert.Null(search.TextSearch);
|
||||
Assert.Null(search.TextFilters);
|
||||
Assert.Equal("status:abed, status:abed2", search.ToString());
|
||||
Assert.Throws<KeyNotFoundException>(() => search.Filters["test"]);
|
||||
Assert.Equal(2, search.Filters["status"].Count);
|
||||
Assert.Equal("abed", search.Filters["status"].First());
|
||||
Assert.Equal("abed2", search.Filters["status"].Skip(1).First());
|
||||
|
||||
filter = "StartDate:2019-04-25 01:00 AM, hekki";
|
||||
filter = "StartDate:2019-04-25 01:00 AM, hekki,orderid:MYORDERID,orderid:MYORDERID_2";
|
||||
search = new SearchString(filter);
|
||||
Assert.Equal("2019-04-25 01:00 AM", search.Filters["startdate"].First());
|
||||
Assert.Equal("hekki", search.TextSearch);
|
||||
Assert.Equal("orderid:MYORDERID,orderid:MYORDERID_2", search.TextFilters);
|
||||
Assert.Equal("orderid:MYORDERID,orderid:MYORDERID_2,hekki", search.TextCombined);
|
||||
Assert.Equal("StartDate:2019-04-25 01:00 AM", search.WithoutSearchText());
|
||||
Assert.Equal(filter, search.ToString());
|
||||
|
||||
// modify search
|
||||
filter = $"status:settled,exceptionstatus:paidLate,unusual:true, fulltext searchterm, storeid:{storeId},startdate:2019-04-25 01:00:00";
|
||||
@ -1601,7 +1744,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 +2253,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 +2300,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()
|
||||
|
@ -13,6 +13,7 @@ using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.NTag424;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.PayoutProcessors;
|
||||
@ -24,6 +25,7 @@ using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
@ -58,8 +60,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");
|
||||
@ -301,6 +303,16 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal("test app title", app.Title);
|
||||
Assert.False(app.Archived);
|
||||
|
||||
// Test title falls back to name
|
||||
app = await client.CreatePointOfSaleApp(
|
||||
user.StoreId,
|
||||
new CreatePointOfSaleAppRequest
|
||||
{
|
||||
AppName = "test app name"
|
||||
}
|
||||
);
|
||||
Assert.Equal("test app name", app.Title);
|
||||
|
||||
// Make sure we return a 404 if we try to get an app that doesn't exist
|
||||
await AssertHttpError(404, async () =>
|
||||
{
|
||||
@ -470,6 +482,16 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal("Crowdfund", app.AppType);
|
||||
Assert.False(app.Archived);
|
||||
|
||||
// Test title falls back to name
|
||||
app = await client.CreateCrowdfundApp(
|
||||
user.StoreId,
|
||||
new CreateCrowdfundAppRequest
|
||||
{
|
||||
AppName = "test app name"
|
||||
}
|
||||
);
|
||||
Assert.Equal("test app name", app.Title);
|
||||
|
||||
// Make sure we return a 404 if we try to get an app that doesn't exist
|
||||
await AssertHttpError(404, async () =>
|
||||
{
|
||||
@ -694,14 +716,10 @@ namespace BTCPayServer.Tests
|
||||
// Try loading 1 user by email. Loading myself.
|
||||
await AssertHttpError(403, async () => await badClient.GetUserByIdOrEmail(badUser.Email));
|
||||
|
||||
|
||||
|
||||
|
||||
// Why is this line needed? I saw it in "CanDeleteUsersViaApi" as well. Is this part of the cleanup?
|
||||
tester.Stores.Remove(adminUser.StoreId);
|
||||
}
|
||||
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanCreateUsersViaAPI()
|
||||
@ -1119,6 +1137,35 @@ namespace BTCPayServer.Tests
|
||||
OnExisting = OnExistingBehavior.KeepVersion
|
||||
});
|
||||
Assert.Equal(card2.Version, card3.Version);
|
||||
var p = new byte[] { 0xc7 }.Concat(uid).Concat(new byte[8]).ToArray();
|
||||
var card4 = await client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
|
||||
{
|
||||
OnExisting = OnExistingBehavior.KeepVersion,
|
||||
LNURLW = card2.LNURLW + $"?p={Encoders.Hex.EncodeData(AESKey.Parse(card2.K1).Encrypt(p))}"
|
||||
});
|
||||
Assert.Equal(card2.Version, card4.Version);
|
||||
Assert.Equal(card2.K4, card4.K4);
|
||||
// Can't define both properties
|
||||
await AssertValidationError(["LNURLW"], () => client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
|
||||
{
|
||||
OnExisting = OnExistingBehavior.KeepVersion,
|
||||
UID = uid,
|
||||
LNURLW = card2.LNURLW + $"?p={Encoders.Hex.EncodeData(AESKey.Parse(card2.K1).Encrypt(p))}"
|
||||
}));
|
||||
// p is malformed
|
||||
await AssertValidationError(["LNURLW"], () => client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
|
||||
{
|
||||
OnExisting = OnExistingBehavior.KeepVersion,
|
||||
UID = uid,
|
||||
LNURLW = card2.LNURLW + $"?p=lol"
|
||||
}));
|
||||
// p is invalid
|
||||
p[0] = 0;
|
||||
await AssertValidationError(["LNURLW"], () => client.RegisterBoltcard(test4.Id, new RegisterBoltcardRequest()
|
||||
{
|
||||
OnExisting = OnExistingBehavior.KeepVersion,
|
||||
LNURLW = card2.LNURLW + $"?p={Encoders.Hex.EncodeData(AESKey.Parse(card2.K1).Encrypt(p))}"
|
||||
}));
|
||||
// Test with SATS denomination values
|
||||
var testSats = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest()
|
||||
{
|
||||
@ -1138,7 +1185,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 +1196,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 +1205,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 +1214,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",
|
||||
@ -2291,7 +2338,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal("updated", invoice.Metadata["itemCode"].Value<string>());
|
||||
Assert.Equal(15, ((JArray)invoice.Metadata["newstuff"]).Values<int>().Sum());
|
||||
|
||||
//also test the the metadata actually got saved
|
||||
//also test the metadata actually got saved
|
||||
invoice = await client.GetInvoice(user.StoreId, invoice.Id);
|
||||
Assert.Equal(newOrderId, invoice.Metadata["orderId"].Value<string>());
|
||||
Assert.Equal("updated", invoice.Metadata["itemCode"].Value<string>());
|
||||
@ -2335,7 +2382,7 @@ namespace BTCPayServer.Tests
|
||||
if (marked == InvoiceStatus.Settled)
|
||||
{
|
||||
Assert.Equal(InvoiceStatus.Settled, result.Status);
|
||||
user.AssertHasWebhookEvent<WebhookInvoiceSettledEvent>(WebhookEventType.InvoiceSettled,
|
||||
await user.AssertHasWebhookEvent<WebhookInvoiceSettledEvent>(WebhookEventType.InvoiceSettled,
|
||||
o =>
|
||||
{
|
||||
Assert.Equal(inv.Id, o.InvoiceId);
|
||||
@ -2345,7 +2392,7 @@ namespace BTCPayServer.Tests
|
||||
if (marked == InvoiceStatus.Invalid)
|
||||
{
|
||||
Assert.Equal(InvoiceStatus.Invalid, result.Status);
|
||||
var evt = user.AssertHasWebhookEvent<WebhookInvoiceInvalidEvent>(WebhookEventType.InvoiceInvalid,
|
||||
var evt = await user.AssertHasWebhookEvent<WebhookInvoiceInvalidEvent>(WebhookEventType.InvoiceInvalid,
|
||||
o =>
|
||||
{
|
||||
Assert.Equal(inv.Id, o.InvoiceId);
|
||||
@ -2537,7 +2584,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
|
||||
@ -3444,7 +3490,6 @@ namespace BTCPayServer.Tests
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task StoreUsersAPITest()
|
||||
{
|
||||
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
|
||||
@ -3454,52 +3499,83 @@ namespace BTCPayServer.Tests
|
||||
var client = await user.CreateClient(Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings);
|
||||
|
||||
var roles = await client.GetServerRoles();
|
||||
Assert.Equal(2,roles.Count);
|
||||
Assert.Equal(4, roles.Count);
|
||||
#pragma warning disable CS0618
|
||||
var ownerRole = roles.Single(data => data.Role == StoreRoles.Owner);
|
||||
var managerRole = roles.Single(data => data.Role == StoreRoles.Manager);
|
||||
var employeeRole = roles.Single(data => data.Role == StoreRoles.Employee);
|
||||
var guestRole = roles.Single(data => data.Role == StoreRoles.Guest);
|
||||
#pragma warning restore CS0618
|
||||
var users = await client.GetStoreUsers(user.StoreId);
|
||||
var storeuser = Assert.Single(users);
|
||||
Assert.Equal(user.UserId, storeuser.UserId);
|
||||
Assert.Equal(ownerRole.Id, storeuser.Role);
|
||||
var user2 = tester.NewAccount();
|
||||
await user2.GrantAccessAsync(false);
|
||||
var storeUser = Assert.Single(users);
|
||||
Assert.Equal(user.UserId, storeUser.UserId);
|
||||
Assert.Equal(ownerRole.Id, storeUser.Role);
|
||||
var manager = tester.NewAccount();
|
||||
await manager.GrantAccessAsync();
|
||||
var employee = tester.NewAccount();
|
||||
await employee.GrantAccessAsync();
|
||||
var guest = tester.NewAccount();
|
||||
await guest.GrantAccessAsync();
|
||||
|
||||
var user2Client = await user2.CreateClient(Policies.CanModifyStoreSettings);
|
||||
var managerClient = await manager.CreateClient(Policies.CanModifyStoreSettings);
|
||||
var employeeClient = await employee.CreateClient(Policies.CanModifyStoreSettings);
|
||||
var guestClient = await guest.CreateClient(Policies.CanModifyStoreSettings);
|
||||
|
||||
//test no access to api when unrelated to store at all
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.GetStoreUsers(user.StoreId));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.AddStoreUser(user.StoreId, new StoreUserData()));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.RemoveStoreUser(user.StoreId, user.UserId));
|
||||
await AssertPermissionError(Policies.CanViewStoreSettings, async () => await managerClient.GetStore(user.StoreId));
|
||||
await AssertPermissionError(Policies.CanViewStoreSettings, async () => await managerClient.GetStoreUsers(user.StoreId));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await managerClient.AddStoreUser(user.StoreId, new StoreUserData()));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await managerClient.RemoveStoreUser(user.StoreId, user.UserId));
|
||||
|
||||
await AssertPermissionError(Policies.CanViewStoreSettings, async () => await employeeClient.GetStore(user.StoreId));
|
||||
await AssertPermissionError(Policies.CanViewStoreSettings, async () => await employeeClient.GetStoreUsers(user.StoreId));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await employeeClient.AddStoreUser(user.StoreId, new StoreUserData()));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await employeeClient.RemoveStoreUser(user.StoreId, user.UserId));
|
||||
|
||||
await AssertPermissionError(Policies.CanViewStoreSettings, async () => await guestClient.GetStore(user.StoreId));
|
||||
await AssertPermissionError(Policies.CanViewStoreSettings, async () => await guestClient.GetStoreUsers(user.StoreId));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await guestClient.AddStoreUser(user.StoreId, new StoreUserData()));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await guestClient.RemoveStoreUser(user.StoreId, user.UserId));
|
||||
|
||||
await client.AddStoreUser(user.StoreId, new StoreUserData() { Role = guestRole.Id, UserId = user2.UserId });
|
||||
// add users to store
|
||||
await client.AddStoreUser(user.StoreId, new StoreUserData { Role = managerRole.Id, UserId = manager.UserId });
|
||||
await client.AddStoreUser(user.StoreId, new StoreUserData { Role = employeeRole.Id, UserId = employee.UserId });
|
||||
await client.AddStoreUser(user.StoreId, new StoreUserData { Role = guestRole.Id, UserId = guest.UserId });
|
||||
|
||||
//test no access to api when only a guest
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.GetStoreUsers(user.StoreId));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.AddStoreUser(user.StoreId, new StoreUserData()));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.RemoveStoreUser(user.StoreId, user.UserId));
|
||||
//test no access to api for employee
|
||||
await AssertPermissionError(Policies.CanViewStoreSettings, async () => await employeeClient.GetStore(user.StoreId));
|
||||
await AssertPermissionError(Policies.CanViewStoreSettings, async () => await employeeClient.GetStoreUsers(user.StoreId));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await employeeClient.AddStoreUser(user.StoreId, new StoreUserData()));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await employeeClient.RemoveStoreUser(user.StoreId, user.UserId));
|
||||
|
||||
//test no access to api for guest
|
||||
await AssertPermissionError(Policies.CanViewStoreSettings, async () => await guestClient.GetStore(user.StoreId));
|
||||
await AssertPermissionError(Policies.CanViewStoreSettings, async () => await guestClient.GetStoreUsers(user.StoreId));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await guestClient.AddStoreUser(user.StoreId, new StoreUserData()));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await guestClient.RemoveStoreUser(user.StoreId, user.UserId));
|
||||
|
||||
//test access to api for manager
|
||||
await managerClient.GetStore(user.StoreId);
|
||||
await managerClient.GetStoreUsers(user.StoreId);
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await managerClient.AddStoreUser(user.StoreId, new StoreUserData()));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await managerClient.RemoveStoreUser(user.StoreId, user.UserId));
|
||||
|
||||
await user2Client.GetStore(user.StoreId);
|
||||
// updates
|
||||
await client.RemoveStoreUser(user.StoreId, employee.UserId);
|
||||
await AssertHttpError(403, async () => await employeeClient.GetStore(user.StoreId));
|
||||
|
||||
await client.RemoveStoreUser(user.StoreId, user2.UserId);
|
||||
await AssertHttpError(403, async () =>
|
||||
await user2Client.GetStore(user.StoreId));
|
||||
|
||||
|
||||
await client.AddStoreUser(user.StoreId, new StoreUserData() { Role = ownerRole.Id, UserId = user2.UserId });
|
||||
await client.AddStoreUser(user.StoreId, new StoreUserData { Role = ownerRole.Id, UserId = employee.UserId });
|
||||
await AssertAPIError("duplicate-store-user-role", async () =>
|
||||
await client.AddStoreUser(user.StoreId,
|
||||
new StoreUserData() { Role = ownerRole.Id, UserId = user2.UserId }));
|
||||
await user2Client.RemoveStoreUser(user.StoreId, user.UserId);
|
||||
|
||||
await client.AddStoreUser(user.StoreId, new StoreUserData { Role = ownerRole.Id, UserId = employee.UserId }));
|
||||
await employeeClient.RemoveStoreUser(user.StoreId, user.UserId);
|
||||
|
||||
//test no access to api when unrelated to store at all
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await client.GetStoreUsers(user.StoreId));
|
||||
await AssertPermissionError(Policies.CanViewStoreSettings, async () => await client.GetStore(user.StoreId));
|
||||
await AssertPermissionError(Policies.CanViewStoreSettings, async () => await client.GetStoreUsers(user.StoreId));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await client.AddStoreUser(user.StoreId, new StoreUserData()));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await client.RemoveStoreUser(user.StoreId, user.UserId));
|
||||
|
||||
await AssertAPIError("store-user-role-orphaned", async () => await user2Client.RemoveStoreUser(user.StoreId, user2.UserId));
|
||||
await AssertAPIError("store-user-role-orphaned", async () => await employeeClient.RemoveStoreUser(user.StoreId, employee.UserId));
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
@ -3572,6 +3648,78 @@ namespace BTCPayServer.Tests
|
||||
await newUserBasicClient.GetCurrentUser();
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task ApproveUserTests()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var admin = tester.NewAccount();
|
||||
await admin.GrantAccessAsync(true);
|
||||
var adminClient = await admin.CreateClient(Policies.Unrestricted);
|
||||
Assert.False((await adminClient.GetUserByIdOrEmail(admin.UserId)).RequiresApproval);
|
||||
Assert.Empty(await adminClient.GetNotifications());
|
||||
|
||||
// require approval
|
||||
var settings = tester.PayTester.GetService<SettingsRepository>();
|
||||
await settings.UpdateSetting(new PoliciesSettings { LockSubscription = false, RequiresUserApproval = true });
|
||||
|
||||
// new user needs approval
|
||||
var unapprovedUser = tester.NewAccount();
|
||||
await unapprovedUser.GrantAccessAsync();
|
||||
var unapprovedUserBasicAuthClient = await unapprovedUser.CreateClient();
|
||||
await AssertAPIError("unauthenticated", async () =>
|
||||
{
|
||||
await unapprovedUserBasicAuthClient.GetCurrentUser();
|
||||
});
|
||||
var unapprovedUserApiKeyClient = await unapprovedUser.CreateClient(Policies.Unrestricted);
|
||||
await AssertAPIError("unauthenticated", async () =>
|
||||
{
|
||||
await unapprovedUserApiKeyClient.GetCurrentUser();
|
||||
});
|
||||
Assert.True((await adminClient.GetUserByIdOrEmail(unapprovedUser.UserId)).RequiresApproval);
|
||||
Assert.False((await adminClient.GetUserByIdOrEmail(unapprovedUser.UserId)).Approved);
|
||||
Assert.Single(await adminClient.GetNotifications(false));
|
||||
|
||||
// approve
|
||||
Assert.True(await adminClient.ApproveUser(unapprovedUser.UserId, true, CancellationToken.None));
|
||||
Assert.True((await adminClient.GetUserByIdOrEmail(unapprovedUser.UserId)).Approved);
|
||||
Assert.True((await unapprovedUserApiKeyClient.GetCurrentUser()).Approved);
|
||||
Assert.True((await unapprovedUserBasicAuthClient.GetCurrentUser()).Approved);
|
||||
|
||||
// un-approve
|
||||
Assert.True(await adminClient.ApproveUser(unapprovedUser.UserId, false, CancellationToken.None));
|
||||
Assert.False((await adminClient.GetUserByIdOrEmail(unapprovedUser.UserId)).Approved);
|
||||
await AssertAPIError("unauthenticated", async () =>
|
||||
{
|
||||
await unapprovedUserApiKeyClient.GetCurrentUser();
|
||||
});
|
||||
await AssertAPIError("unauthenticated", async () =>
|
||||
{
|
||||
await unapprovedUserBasicAuthClient.GetCurrentUser();
|
||||
});
|
||||
|
||||
// reset policies to not require approval
|
||||
await settings.UpdateSetting(new PoliciesSettings { LockSubscription = false, RequiresUserApproval = false });
|
||||
|
||||
// new user does not need approval
|
||||
var newUser = tester.NewAccount();
|
||||
await newUser.GrantAccessAsync();
|
||||
var newUserBasicAuthClient = await newUser.CreateClient();
|
||||
var newUserApiKeyClient = await newUser.CreateClient(Policies.Unrestricted);
|
||||
Assert.False((await newUserApiKeyClient.GetCurrentUser()).RequiresApproval);
|
||||
Assert.False((await newUserApiKeyClient.GetCurrentUser()).Approved);
|
||||
Assert.False((await newUserBasicAuthClient.GetCurrentUser()).RequiresApproval);
|
||||
Assert.False((await newUserBasicAuthClient.GetCurrentUser()).Approved);
|
||||
Assert.Single(await adminClient.GetNotifications(false));
|
||||
|
||||
// try unapproving user which does not have the RequiresApproval flag
|
||||
await AssertAPIError("invalid-state", async () =>
|
||||
{
|
||||
await adminClient.ApproveUser(newUser.UserId, false, CancellationToken.None);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
[Trait("Integration", "Integration")]
|
||||
[Trait("Lightning", "Lightning")]
|
||||
@ -3679,7 +3827,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,
|
||||
@ -3809,8 +3957,9 @@ namespace BTCPayServer.Tests
|
||||
Assert.True( settings.ProcessNewPayoutsInstantly);
|
||||
|
||||
var pluginHookService = tester.PayTester.GetService<IPluginHookService>();
|
||||
var beforeHookTcs = new TaskCompletionSource();
|
||||
var afterHookTcs = new TaskCompletionSource();
|
||||
var beforeHookTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var afterHookTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
TestLogs.LogInformation("Adding hook...");
|
||||
pluginHookService.ActionInvoked += (sender, tuple) =>
|
||||
{
|
||||
switch (tuple.hook)
|
||||
@ -3841,7 +3990,9 @@ namespace BTCPayServer.Tests
|
||||
PaymentMethod = "BTC",
|
||||
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
|
||||
});
|
||||
TestLogs.LogInformation("Waiting before hook...");
|
||||
await beforeHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
TestLogs.LogInformation("Waiting before after...");
|
||||
await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
payouts = await adminClient.GetStorePayouts(admin.StoreId);
|
||||
try
|
||||
@ -3911,7 +4062,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 +4482,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();
|
||||
|
@ -2,6 +2,7 @@ using System;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.BIP78.Sender;
|
||||
@ -68,7 +69,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 +190,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 +219,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 +237,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 +251,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,13 +306,13 @@ 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.
|
||||
var invoiceId = s.CreateInvoice(receiver.storeId);
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
var bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn"))
|
||||
var bip21 = s.Driver.WaitForElement(By.ClassName("payment__details__instruction__open-wallet__btn"))
|
||||
.GetAttribute("href");
|
||||
Assert.Contains($"{PayjoinClient.BIP21EndpointKey}=", bip21);
|
||||
|
||||
@ -320,14 +321,14 @@ 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);
|
||||
|
||||
invoiceId = s.CreateInvoice(receiver.storeId);
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn"))
|
||||
bip21 = s.Driver.WaitForElement(By.ClassName("payment__details__instruction__open-wallet__btn"))
|
||||
.GetAttribute("href");
|
||||
Assert.Contains($"{PayjoinClient.BIP21EndpointKey}=", bip21);
|
||||
|
||||
@ -361,7 +362,7 @@ namespace BTCPayServer.Tests
|
||||
//let's do it all again, except now the receiver has funds and is able to payjoin
|
||||
invoiceId = s.CreateInvoice();
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn"))
|
||||
bip21 = s.Driver.WaitForElement(By.ClassName("payment__details__instruction__open-wallet__btn"))
|
||||
.GetAttribute("href");
|
||||
Assert.Contains($"{PayjoinClient.BIP21EndpointKey}", bip21);
|
||||
|
||||
@ -374,7 +375,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 +407,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);
|
||||
@ -415,13 +415,13 @@ namespace BTCPayServer.Tests
|
||||
Assert.False(paymentValueRowColumn.Text.Contains("payjoin",
|
||||
StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
s.GoToWallet(receiverWalletId, WalletsNavPages.Transactions);
|
||||
s.Driver.WaitForElement(By.CssSelector("#WalletTransactionsList tr"));
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
s.GoToWallet(receiverWalletId, WalletsNavPages.Transactions);
|
||||
Assert.Contains(invoiceId, s.Driver.PageSource);
|
||||
Assert.Contains("payjoin", s.Driver.PageSource);
|
||||
//this label does not always show since input gets used
|
||||
// Assert.Contains("payjoin-exposed", s.Driver.PageSource);
|
||||
// Either the invoice id or the payjoin-exposed label, depending on the input having been used
|
||||
Assert.Matches(new Regex($"({invoiceId}|payjoin-exposed)"), s.Driver.PageSource);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -875,7 +875,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 +962,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 +970,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 +1130,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);
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@ -9,6 +8,7 @@ using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Lightning.CLightning;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Views.Manage;
|
||||
using BTCPayServer.Views.Server;
|
||||
using BTCPayServer.Views.Stores;
|
||||
@ -77,6 +77,7 @@ namespace BTCPayServer.Tests
|
||||
// A bit less than test timeout
|
||||
TimeSpan.FromSeconds(50));
|
||||
}
|
||||
|
||||
ServerUri = Server.PayTester.ServerUri;
|
||||
Driver.Manage().Window.Maximize();
|
||||
|
||||
@ -89,7 +90,6 @@ namespace BTCPayServer.Tests
|
||||
|
||||
public void PayInvoice(bool mine = false, decimal? amount = null)
|
||||
{
|
||||
|
||||
if (amount is not null)
|
||||
{
|
||||
Driver.FindElement(By.Id("test-payment-amount")).Clear();
|
||||
@ -97,6 +97,10 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
Driver.WaitUntilAvailable(By.Id("FakePayment"));
|
||||
Driver.FindElement(By.Id("FakePayment")).Click();
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
Driver.WaitForElement(By.Id("CheatSuccessMessage"));
|
||||
});
|
||||
if (mine)
|
||||
{
|
||||
MineBlockOnInvoiceCheckout();
|
||||
@ -105,8 +109,15 @@ namespace BTCPayServer.Tests
|
||||
|
||||
public void MineBlockOnInvoiceCheckout()
|
||||
{
|
||||
Driver.FindElement(By.CssSelector("#mine-block button")).Click();
|
||||
|
||||
retry:
|
||||
try
|
||||
{
|
||||
Driver.FindElement(By.CssSelector("#mine-block button")).Click();
|
||||
}
|
||||
catch (StaleElementReferenceException)
|
||||
{
|
||||
goto retry;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -278,7 +289,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"))
|
||||
{
|
||||
@ -396,15 +407,12 @@ namespace BTCPayServer.Tests
|
||||
|
||||
public void Logout()
|
||||
{
|
||||
if (!Driver.PageSource.Contains("id=\"Nav-Logout\""))
|
||||
{
|
||||
Driver.Navigate().GoToUrl(ServerUri);
|
||||
}
|
||||
if (!Driver.PageSource.Contains("id=\"Nav-Logout\"")) GoToUrl("/account");
|
||||
Driver.FindElement(By.Id("Nav-Account")).Click();
|
||||
Driver.FindElement(By.Id("Nav-Logout")).Click();
|
||||
}
|
||||
|
||||
public void LogIn(string user, string password)
|
||||
public void LogIn(string user, string password = "123456")
|
||||
{
|
||||
Driver.FindElement(By.Id("Email")).SendKeys(user);
|
||||
Driver.FindElement(By.Id("Password")).SendKeys(password);
|
||||
@ -633,5 +641,38 @@ retry:
|
||||
Driver.FindElement(By.Id($"SectionNav-{navPages}")).Click();
|
||||
}
|
||||
}
|
||||
|
||||
public void AddUserToStore(string storeId, string email, string role)
|
||||
{
|
||||
if (Driver.FindElements(By.Id("AddUser")).Count == 0)
|
||||
{
|
||||
GoToStore(storeId, StoreNavPages.Users);
|
||||
}
|
||||
Driver.FindElement(By.Id("Email")).SendKeys(email);
|
||||
new SelectElement(Driver.FindElement(By.Id("Role"))).SelectByValue(role);
|
||||
Driver.FindElement(By.Id("AddUser")).Click();
|
||||
Assert.Contains("User added successfully", FindAlertMessage().Text);
|
||||
}
|
||||
|
||||
public void AssertPageAccess(bool shouldHaveAccess, string url)
|
||||
{
|
||||
GoToUrl(url);
|
||||
Assert.DoesNotMatch("404 - Page not found</h", Driver.PageSource);
|
||||
if (shouldHaveAccess)
|
||||
Assert.DoesNotMatch("- Denied</h", Driver.PageSource);
|
||||
else
|
||||
Assert.Contains("- Denied</h", Driver.PageSource);
|
||||
}
|
||||
|
||||
public (string appName, string appId) CreateApp(string type, string name = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name)) name = $"{type}-{Guid.NewGuid().ToString()[..14]}";
|
||||
Driver.FindElement(By.Id($"StoreNav-Create{type}")).Click();
|
||||
Driver.FindElement(By.Name("AppName")).SendKeys(name);
|
||||
Driver.FindElement(By.Id("Create")).Click();
|
||||
Assert.Contains("App successfully created", FindAlertMessage().Text);
|
||||
var appId = Driver.Url.Split('/')[4];
|
||||
return (name, appId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||
|
@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
@ -25,6 +27,7 @@ using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.CodeAnalysis.Operations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Payment;
|
||||
using NBitpayClient;
|
||||
@ -32,6 +35,7 @@ using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Models;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Npgsql;
|
||||
using Xunit;
|
||||
using Xunit.Sdk;
|
||||
|
||||
@ -73,7 +77,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 +343,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 +350,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 +447,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;
|
||||
}
|
||||
@ -453,9 +456,9 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
private Client.Models.StoreWebhookData _wh;
|
||||
private FakeServer _server;
|
||||
private readonly List<WebhookInvoiceEvent> _webhookEvents;
|
||||
private readonly List<StoreWebhookEvent> _webhookEvents;
|
||||
private CancellationTokenSource _cts;
|
||||
public WebhookListener(Client.Models.StoreWebhookData wh, FakeServer server, List<WebhookInvoiceEvent> webhookEvents)
|
||||
public WebhookListener(Client.Models.StoreWebhookData wh, FakeServer server, List<StoreWebhookEvent> webhookEvents)
|
||||
{
|
||||
_wh = wh;
|
||||
_server = server;
|
||||
@ -473,7 +476,7 @@ namespace BTCPayServer.Tests
|
||||
var callback = Encoding.UTF8.GetString(bytes);
|
||||
lock (_webhookEvents)
|
||||
{
|
||||
_webhookEvents.Add(JsonConvert.DeserializeObject<WebhookInvoiceEvent>(callback));
|
||||
_webhookEvents.Add(JsonConvert.DeserializeObject<DummyStoreWebhookEvent>(callback));
|
||||
}
|
||||
req.Response.StatusCode = 200;
|
||||
_server.Done();
|
||||
@ -486,8 +489,13 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
public List<WebhookInvoiceEvent> WebhookEvents { get; set; } = new List<WebhookInvoiceEvent>();
|
||||
public TEvent AssertHasWebhookEvent<TEvent>(string eventType, Action<TEvent> assert) where TEvent : class
|
||||
public class DummyStoreWebhookEvent : StoreWebhookEvent
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public List<StoreWebhookEvent> WebhookEvents { get; set; } = new List<StoreWebhookEvent>();
|
||||
public async Task<TEvent> AssertHasWebhookEvent<TEvent>(string eventType, Action<TEvent> assert) where TEvent : class
|
||||
{
|
||||
int retry = 0;
|
||||
retry:
|
||||
@ -511,7 +519,7 @@ retry:
|
||||
}
|
||||
if (retry < 3)
|
||||
{
|
||||
Thread.Sleep(1000);
|
||||
await Task.Delay(1000);
|
||||
retry++;
|
||||
goto retry;
|
||||
}
|
||||
@ -546,13 +554,23 @@ retry:
|
||||
|
||||
public async Task AddGuest(string userId)
|
||||
{
|
||||
var repo = this.parent.PayTester.GetService<StoreRepository>();
|
||||
await repo.AddStoreUser(StoreId, userId, StoreRoleId.Guest);
|
||||
var repo = parent.PayTester.GetService<StoreRepository>();
|
||||
await repo.AddOrUpdateStoreUser(StoreId, userId, StoreRoleId.Guest);
|
||||
}
|
||||
public async Task AddOwner(string userId)
|
||||
{
|
||||
var repo = this.parent.PayTester.GetService<StoreRepository>();
|
||||
await repo.AddStoreUser(StoreId, userId, StoreRoleId.Owner);
|
||||
var repo = parent.PayTester.GetService<StoreRepository>();
|
||||
await repo.AddOrUpdateStoreUser(StoreId, userId, StoreRoleId.Owner);
|
||||
}
|
||||
public async Task AddManager(string userId)
|
||||
{
|
||||
var repo = parent.PayTester.GetService<StoreRepository>();
|
||||
await repo.AddOrUpdateStoreUser(StoreId, userId, StoreRoleId.Manager);
|
||||
}
|
||||
public async Task AddEmployee(string userId)
|
||||
{
|
||||
var repo = parent.PayTester.GetService<StoreRepository>();
|
||||
await repo.AddOrUpdateStoreUser(StoreId, userId, StoreRoleId.Employee);
|
||||
}
|
||||
|
||||
public async Task<uint256> PayOnChain(string invoiceId)
|
||||
@ -643,5 +661,33 @@ retry:
|
||||
LNAddress = lnAddrUser;
|
||||
return lnAddrUser;
|
||||
}
|
||||
|
||||
public async Task ImportOldInvoices(string storeId = null)
|
||||
{
|
||||
storeId ??= StoreId;
|
||||
var oldInvoices = File.ReadAllLines(TestUtils.GetTestDataFullPath("OldInvoices.csv"));
|
||||
var oldPayments = File.ReadAllLines(TestUtils.GetTestDataFullPath("OldPayments.csv"));
|
||||
var dbContext = this.parent.PayTester.GetService<ApplicationDbContextFactory>().CreateContext();
|
||||
var db = (NpgsqlConnection)dbContext.Database.GetDbConnection();
|
||||
await db.OpenAsync();
|
||||
using (var writer = db.BeginTextImport("COPY \"Invoices\" (\"Id\",\"Blob\",\"Created\",\"CustomerEmail\",\"ExceptionStatus\",\"ItemCode\",\"OrderId\",\"Status\",\"StoreDataId\",\"Archived\",\"Blob2\") FROM STDIN DELIMITER ',' CSV HEADER"))
|
||||
{
|
||||
foreach (var invoice in oldInvoices)
|
||||
{
|
||||
var localInvoice = invoice.Replace("3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd", storeId);
|
||||
await writer.WriteLineAsync(localInvoice);
|
||||
}
|
||||
await writer.FlushAsync();
|
||||
}
|
||||
using (var writer = db.BeginTextImport("COPY \"Payments\" (\"Id\",\"Blob\",\"InvoiceDataId\",\"Accounted\",\"Blob2\",\"Type\") FROM STDIN DELIMITER ',' CSV HEADER"))
|
||||
{
|
||||
foreach (var invoice in oldPayments)
|
||||
{
|
||||
var localPayment = invoice.Replace("3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd", storeId);
|
||||
await writer.WriteLineAsync(localPayment);
|
||||
}
|
||||
await writer.FlushAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
11
BTCPayServer.Tests/TestData/OldInvoices.csv
Normal file
11
BTCPayServer.Tests/TestData/OldInvoices.csv
Normal file
@ -0,0 +1,11 @@
|
||||
Id,Blob,Created,CustomerEmail,ExceptionStatus,ItemCode,OrderId,Status,StoreDataId,Archived,Blob2
|
||||
Q7RqoHLngK9svM4MgRyi9y,\x1f8b0800000000000003c454cb76a24010dde72b7258cf180d08929d0a89899ae323ea4962163c0ae808ddd8344426c72f9bc57cd2fcc2340d31ea38b398cd2cfb5657d5add7fdf9fdc7fbd9f9b9845ce9ea5c1a6b9335e90db0dfd7936ca80cfd498ef45cfa52fc4818a1702bbec9893feb76d9ace3abd3d6e06e456dd76b2fece46eb8eee4d7032f9bae5f6fd44dbfb330ddd2995017a870c6691896f16200774442e4e41cae0b8c5a0cf8436dea6a4d56344d5135596e2ac286704690030f282abe349a724bd6e5a67c298cb089117746041fd815a5b2bb109304b1b6eb524812514347ef1bb2a6cd0686663e7a8b4779dcf3bd45eb5ae9bcb181e10f50c93ca6c44d1d768b3d422391817b172d2b2831880c489c229e0d4085478577890b7be51691823c418e1572d4b3c2043e60caabe29856ab578893520a58b4459a4d0d89a35bc1c54e73dec5534c84e5de8a8e520ad88c2c149ec0bb24c58ce6272c4f283e814e59399ddfe220762a48d5ebcb3f9b1a274ca380e08f24bbbaf9ec0c8b59fbdbc3d70965a20953566c8d2fbab589535b35dab61f78e9a4bb50c76b2af7bda9f18a56caca9ca1acbdf202e7a6375ccd4d50eca775539e8fbf69fa4a514c326e0e8d7870d7efb747fd66369faba38d366b4c641dc6c8c67660240b3d35c78dbe1be85dc38efcc9d7e7f832095ea4d38c1088457b5f4a9d87ee52ba5afe277a4b69fb71c1164b05270c6f5275370ec425e7cab6eb706ce511605660cf2fe575829762d7b243d85fe10a1e1e2e19475d44c161b3c9a0c818301627571717919530a0981f0767b342d8af39242ab9b0cd3588d3ad9762e0f150f784218f1f4d41b160c2685a26c57b8632c5a7b000cd80ce68789817610cace642446a36737875e5bf1aa17e99dfa179cc48b568d55df1c9ed1e7fd7a7f296cb9e0d8105a410bbf7edcee4014c4aefc60e3baa5860ffa454c279bbbb978860c4d59a77d7dce9e24e135bf54a130354487aa14855323898bf95f18916c3aeac3d2b090e7fc0860176c13d1ed2e76a40566dd0f1563d9010a88585f0356af5b3ed2f000000ffff030035140a5d88060000,2018-10-01 11:32:12+00,,,,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
Ka6GHBrFJPRwFRga1RD6Yz,\x1f8b0800000000000003c454cd769a4014dee7297258b74682a066a7427e24e6284a729a9fc5001798083364188826c727eba28fd457e830101353db45375dcef7cdbdf7bbbf3fbfff783d383c5470a09c1c2a3632cece87ec743c759e4f9d08a98e697c7b51be543f724e195cc86f5a1eb9a31177879131ef5d8e97cc0bc2c18d978f274fc3f5e96558ce9f1ecf8c953dbcb182da98b2009834264592d4fe3280604a13ecaf05dc9618431cc44337ba46abad6aed76db50b5635d72989414fbb0c069f545d5b59ed6eff4f4da10561916c698921d5eef367c0019cd311f0401833c973998b7a5e742ef7110a7fd65e9f62889c7b6f5726b777d3fc25c9fd5ca334683c2e71724a42c9511847555b24a1287d484dcaffc9d310072b80024cd1a724403f89073e52e5ee7d84789404394e4f00633915a558656fbb881fc823120b2388ae53a8a4037529157ac452df7e991cc154a3fc594b095229cecc147b4209cadf730b738db83ce79dda3dffc60becf4953f1e33f53ea1e6a1a53f216649bb7e8a08938fa384362a870298b30e7d5ec44b25aabacf00c73e045715838a31b63f6c4343b9c9b8f78d9595a2e2e07cb30f6cfce27cb6b0b3adeed93ae5dcf5ebafd65a763d1993e31b3cbb16d0fa6b65e5e5f1bd355d7551dad0f33ec112f36f39b7e61cd543b88fb23d34b23e7eb5d769cc70fca7e4518e4b8bdde2bc3c5e85e39b9ff4ff2ee95cddb1e235e484d049e95667b7cc86acd0db7ad7086d629105e61770ff58e4258900079097c9ce1069eec0e994003ccc0e7ae7359458c39cff293a3a314e51c1811db21d42c31895a3e4d6b2d7c750a7281dbf5e686c2d515e538145b5349ac947056d441c907a20ef17e5e8095c05c96ecc6c584006f0590d296c77d915dfdaf455954c7f7d93ae3b419b466af44e7b68fbf5fa97a99eb9a4d80c7b43a79af9b2d150238b5b5bac53e652cb17fba57d278b3dd9794122c6eb6a8aeb5bd8edbcbd8d79acb18e3eab05727a909063bfd47a5e868d5ec863d4779bcfb03561c4800c1e726bd8f0694cd047d9eaa054d8021222f9fda6a1f6c7e010000ffff0300ddecc9e08e060000,2018-10-01 11:54:10+00,,,,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
Q3kZ3F8cUD57WUqcc8QLs2,\x1f8b0800000000000003c4544972da4014ddfb1494d6099110a377806463a662b0c085f142c3176a23758b564b86b83859163952ae90564b96812259649365bff77ffff9fdfaf1f3fda654929023dd96a4a9ba5da9774ddbd06a8da5b1b3ede6741855a42fa945c408850761a6461ba3db654667539f3787fd2db51cb7bdb4a2fe68d739dc0ddd64be7bbdafef079da5ee64ce843a4085338e7d3ffb2f047026c447f681c3b2c0a8c9803f6af546bd2c2baa2ccb7545add404877042900d8f28484d949ada545bd566a32248d887883b2382cff85a23e71d08498458db71284491a861921876af3f1ec361bc5bb4d427dadb57bcfba145f4fd66fe36795a6599879438b1cd1eb04b68202270efb465694a0c020d223bfd6f64460c28260e94e6ccdc22bc11feb95597e327c5a7ff7a8708d9a6cf51d7f423f88029af916395b29c23764c2960d12449376612478f22332b3ef09e5ecb4b306333b80829603d30917f05ef9218337ab8c2ac507805e545b26bff7711bbf649def9ca9f29e50a35f108fe0852d4cd27a999cc3cdd25be5c28114d98b3748736a25bfb30b6ea5adbda786e3ceb2eebd31d5507ee5c7b45dbea563750d2deba9e7ddf1b6d173a54add5aea62ea6df1bad6db5aa93696da485c3fe60d09e0c6ac962519fec1b8632535b304516b63c2d5ab6627daa0c1cafd5d5ac6033fbfa1c5622ef45ba9e1102b176ef6ba9f3d85d4bb7ebff94de5a3a7edcb3c9629113863729bf221bc22ce79c2b3a1c9a8700304bb1e797ec56c18db1635a3e9cae700e8fce978ca30ea2603363364c237a8c85d1edb76f417134517633659b04592e6c7f07e290e54c1a5cfed59830e4f2a349534c336134ce82e213220bf129334013a006f5cfe3228c81951d0848d96236af2eb32b139addad64d343c848be68f95df1c9158fbfab5576cb59cf46c03c924adffbb1a05c8059e6ad14d845c502fb27dd12cec7e25e028211d76ede5dbd50c942215b6aae901e4a053e55a43c189ccddf4cf844d361e76ccf8cbc730bd833c00e389743fa5c0d48f20dbadcaa47e20335b1103ea52cdf1c7f030000ffff03003e6b8efb96060000,2018-10-01 11:54:32+00,,,,,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
FSktP1Nrxu7arh7TUFAgTZ,\x1f8b0800000000000003c4554b72e33610ddcf295c5c27b23e16297a1599923c63591efd55f1781620d1143124011a0035545c3a59163952ae10008415d9a54caab2c992af5f773ff48f7ffefec7cb878b0b8760e7fac2192d52396d3df0aaf4104fbce56ad4df2e1f9d9f344348c6e193a175c47615047275b37517bdfbbb948738ee6f42713779bed98feee3dde2f9dbad5b8d6f36435c3b338e81d7ce41a922e59f2d50872e00f0946524da2b46d3601c49501f5dd76b377a6dbfedf7bc96e7f9c646e88e91089624d79456b7d3ebf8dd66a76b8c501544391346dfda7d6bc7503041641f630e4298e70c969bd093de7886d63de1a2ea7b67ddc28fb76595dcfdba1db9eeb296597086cb487ea231e3b9c9a0bc75f5b42409f90044a4e34d9090c029c370b1902825746bfc2d2b50b862d132cb2c5a247b41229429344699805798ab376afdcd66a369b1a8e41ca82993335ccd1d851e8cb6b0dcab7a9e53662c0f287f97d4c0c31c119d56c5d54d01fe0b54282f3268442c774e99012ba9e4fb33311e497106550f97e73206449e0b62bbd1fe6753eb8c699a30fa9ae45809d5dd0192e884ae5acec9ce946521f55c6d4dfdaaa20cdd413fdc2671390f36eeec9977c6f162f08da457e9704576fd344ea2db8f93743d84abf0f1b9db59cf7ef3fcf4ea6ac866ddc9a0b8bf1b8ffbd37177b75ebbd3ca5bb5e61d1f6624a46132101bbf1cce5a639cf8c120ccb7f39fbf146d917c75ce2b226046f1e5c9b959064fcef5d3ff24efc939bcae3b92a5d144e1bb63372b82a2d66c6dc70a17689f03951afbf2b5de5f884b8a5198c1e9585b78f26f63a778987088e46a7eaf89899485b8bebc3ce15dca04d154ec59597bc86a04765dcc77acb43d304962b55a5ab4d6267959cba027861fa4504b9985284a85ad09f01df015cf4ef96a852805d9c090b3462823558a9ad760bc5e7c27e2fb42323b95762d559b8f1f3f3e77f531a80b3c0199307d465f0e47530c30afbd5b47ec5d310cf69f0e9f713e1c972b6794a8ff803a69c3e3993d9e58bf6b4f6c42f4cf429f349b0cde0c0bdaa9f6ebc9b0d68f48246f195049a018f0fbfefd3d47b0b3e3f67e04972c038e687d391bcd0f87bf000000ffff030075db901fe2060000,2018-10-01 11:57:15+00,customer@example.com,,,CustomOrderId,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
HuzCsv9hghew2FD6zVqUyY,\x1f8b0800000000000003c455cb76d33010ddf3153d5e439aa75377456aa794268126a913286521dbe358b52d3992ecc6edc997b1e093f80564590d694f8073d8b0f49d3b3357f3f28f6fdf1f5f1d1d1938304e8f8c8bfcc1e68515ad22b86f9f3be6c362ed969f8dd715830bcae0bda275f8cab56de19eadccf9c9f832665e100e961ebf9caccfcaf37158ccd777efcccde86c390c6a67ca0260b5b39dcb48e9470dd4a13380e08a26d82f25a3a9308604c88f9ed96f374eda56db3ae9b7fa7d4bd9302928f6e11aa715a5d5eb9c74ac5edfea28236c322c9d3125cfeca6a9ed01649463310802069cabe758e9f2be701d7ad3b3379f9069a72be406a865ad3fdf59e36ceab46a9919a341ee8bf724a42c5519a47755bd4a9280d401ee57f126880b60840670341728c664a5fc35cb96b864913c49349a4525c73e4a241aa284c313cce41b2bfdcd66a3a9313f670c882a933174678644b74a9b9797b29e879429cb0794be48aae0618a709556c6ad9a02ec2d6c509a25d0f0696aec336d9a13c1ca03316e707600950f178732da581c0aa2bbd1febda975c0741551f294645709d95d0709b447972d67b85065998b6aae56aa7e9b2cf74c67e0ada2309fd94b73ba669d513877ee70dc8d872e2e067118f9ef2e26f162085def66ddeb2ca60f7d2bee768774da9b38d9f872341a5c8d7ac562615e6dfa6e6bd6b1608a3de2450e5f5af970da1a0591653b5eba9abdf992b579f4d538ac08831ac5c75be3ecdabe354e6fff93bc5b63fbb4ee48e44a13817b436f960f59ad59db7615ce5099021115f6e56bbdbf10e624405e02fb63ade1c9dfc64ef202ccc017ee6c5c112321327e7a7cbcc73b161122312f695e7b88cd39e87551dfa1d4f6810a1ccad5aa4457da04cb6b1964cff087147229130ff931d7350156007359b2cf972b4408884600296d78c297a5a8790dcaeac5377c566682eaa9d46b29dbbcfbf8f3b9ab8f415de00988885667f471bb338500b3dabbb5c35e144361ff74f894f376b75c292558fe07e4491beeceac3eb1dd66b3ad4f6c84ab9f4575d2743278362ca890edaf26435b2f108f9e336023800410bcecdfaf3982428fdbcb11bca6093044eacbd968bedafe040000ffff030069b828dae2060000,2018-10-01 12:09:53+00,customer@example.com,,,CustomOrderId,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
YZV1PQasUKEXdSkSV69XDL,\x1f8b0800000000000003c455cb76db3610dde72b7cb86e65911429cbabca1463c792523d2c39759c05480c4584244003202bd5475fd6453e29bf50108455d9474dcfe9a64bdeb93373312f7efff3dbf3bbb3338b60ebf2ccfaed616dcfe648acc6e127bccc966b7ff06934b17e6a1842320e1f34cd159b5510c8d5d5c65f5e4c6e331ee164781f89dbe9d3d5eefd24a9974f5faffdedf8ea3ec4ad33e31878eb1c542a52f1ab01dad025009eb19cc43bc5e86a8c2309eac3f37da7e3f56cd7f55dcf751c6d23b46624863b523414db732fdc81ef38b636c2b624ca9930facadeb78d1d43c90491438c3908a19f33b85927f2b62f2807a7866c7ce1f5c49dbc76275e9a08e7a2ba6e65969ce12a961f68c278a13328efa67a8d2409c50844dcc49b2221815386e16c295146e846fb1b56a070c5a2559e1bb44c7782c42857688272012f30576f6cf477bb9daec1e28a73a0ba4c56b85a580add6b6d51b553f53ca54c5b3ea2e24d520d8705224d5a15b7690af05f608b8a32874ecc0aeb9819b08a4abe3b11e381942750f570792a6340e4a920a61bce3f9bec13a659cae84b92432554774748a223ba6a3927b52ecb523673b5d1f5db9655e48f86d1264daa4570efcf9fb83b4e96a3af24eb65e18ad4c32c49e3eb9b69b60ea1173d3c79ee7afe477f90f57a219b7bd35139b91d8f87b3b157afd7fe6cdb5fd90b77007312d1281d89fb4115ceed314e07c1282a368b9f3f978e48bf58a71511d0a3f8fc685ddd058fd6e5e3ff24efd1dabfac3b9295d644e177cb6c560c65abd9d80e152ed1ae002a1becf397767f21a92846510ec7636de0e9bf8d9de261c22196abc5a421a65296e2f2fcfc88772e534433b16355eb21b7efc1ac8bfe4e94b68f4c9244ad5623bad12679d5caa047861fa4504b994728ce84a909f01af88ae7c77cb5429482ec6028582792b12a45cbeb30de2ebe15f35d2999994ab396aacd878f1f9fbbf618b4059e824c5973469ff7075302b068bded03f6a6181afb4f874f3bef0fcb55304ad47f409db4f07066cd89ed75bb7d736253d2fc2c9a936692c1ab6141b56a7f3319c67a8344fa9a015b0914037edbbfbfe7086a336e6f47f08ee5c0116d2f67a7fb6eff17000000ffff03003a3b8e5ae2060000,2018-10-01 12:17:01+00,customer@example.com,,,CustomOrderId,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
3qtdXmycQwhWEPs91MExB2,\x1f8b0800000000000003c455cb769b4810dde72b7c5827b21002455e4502f2b0a48c1e96e538cea2a10bd106ba5177238bf8e8cb66319f34bf90a6c18aeca324e7cc6696dcba5575bb5efcfbf73f8fafcece0c828d8b33c3da487c9395e1ec215ef953d13727fe6ed8315e570c2119874f354dac97ae2b97c3b5b3783bbe4c7880a3c12a109793cdb07c3f8eb68bcdfd0767371aae7c5c3b338e81d7ce6ea122657f35401d3a07c0539692b0548cb6c63892a03e6cc7e9b4ecae69598e655b9d8eb611ba6524842b925514d3b6de5a7dc7b11d6d845d4e943361f499bd6737760c3913440e30e620847e8ef9b0bcf70a6bf6ddc3e3e48bdfe38e7f39ef4d2837931b424641af969973868b507ea211e399cea0bcabea559224641e88b08a37414202a70cc3d942a284d0b5f66f58aec2158b1669daa0795c0a12a254a1114a053cc15cbd51eb373bad76038605e740759d0c7f393714bad7e282a254053d254d5b3ea3ec45560dfb1922555e15b7ea0af077b043599e422b649971cc745941252f4fc4b825f90954bd5c9ecae812792a48d38eceaf4de609d33466f429c9a112aabd1e92e888ae7acec956976521abc15aebfaedf22270bc41b08ea362eeae9cd9865ba368e1dd93a49bf84bb21d24511c7ef83849ae7de806b71bdbba9e7deff5936ed767337be2e5e3cbd168301dd9dbeb6b67baeb2dcdb9d587190968107b62d52ffc9939c271dff5826c3d7ff335ef88f89b715a11013d8b8f77c6f0cabd332eeefe277977c6fe69df912cb4260a0f46b35a21e4b5e6c676a8708eca0ca8acb0afdfea0586a8a01805291ccf75034ffe34768a870987502ee7e38a184b998b8bf3f323deb98c114d44c98ada43eede83de97767d4122a5ed33932452bb5589aeb4495ed432e891e13729d456a6010a13d1d404f816f892a7c77cb54294826c61c8582b90a12a45cd6b315e6fbe11f23297ac99ca662d559b0f1fbfbf77f535a80b3c0119b3ea8e3eee0fa608605e7b9b07ec453134f69f2e9f76de1f962b6394a81f81ba69fee1ce3637b6db36cde6c6c6a4fa5b5437ad4906cf86056d55fbabc968ac1f91889f3360278162c02ffbf7738e60db8cdbcb11bc62297044ebd3d96abfdaff000000ffff0300fd61e9cae3060000,2018-10-01 12:24:16+00,customer@example.com,,,CustomOrderId,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
BtJGr7kt52QkZe8RdhKz42,\x1f8b0800000000000003c455c172da4810bde72b5c3a6f301208249f168462078cd780c115e21c469a161a4b9a9167465a888b2fcb219f945fc86824b3d8c566abf692a3de7b3dfda6a7bbf5e3dbf7e777676706c1c6c5993194e34bde4fa46dcd923538731c4fbe762de38f4a2124e3f051cb3a62b3f43cb91c6e7a0be77a9cf0004783fb408ca74fc3dd87eba85c3c3d5ef6b693e1bd8feb60c631f03ad82bd449d95f0d501f9d03e05b969270a7146d8d7124417dd83dc76a5996d9711dc771db3dcd115a3212c21dc92a8969779c8edb6ff72d4dc236272a9830fa9a771b1e43ce0491038c3908a1af73152d3ee55bfea9fb784978de21ebf286925537598ef12258737f52dbcc39c345283fd288f14c6750d155f52a4b12b21188b03a6f8a84044e1986b3854409a11b1ddfa83c852b152dd2b441f3782748885285462815f002737547eddf6cb79c7ebf6d9b96dd7061c139505d2ec35fce0d85eeb5c7a0d8a9ba9e72a8991b94bd49ae613f43a44a6fa4880b4168028efbe7a6025b21cb8c63a9c70a2af9eec4216b929f405505e4a9941e91a70e699ec5fa77ca3c41ddc68cbe243994423df308497424576fcf49a9ebb29055836d7401b77911f44683601347c5dcbbefcd9e7867122d468f24e926fe929483248ac3cbab69b2f2a11bac9fecce6af6b5ef26ddaecf66f674945f8f2793c1edc42e57abdeedb6bf34e71d176624a0413c12f76ee1cfcc098e5d6f14649bf9fbcfb925e22fc6694704744f3e3f18c33befc1b878f84df61e8cfdcbdc2359684f14fe369a110b21af3d37dca1c239da654065857dfe520f324405c52848e1b8bf1b78fa9f7da784987008e5727e5d2963297371717e0e5b94e52954ba7319239a881d2bea08b9fd007a70daf52a8994b91b2649a486ac725d9993bca87dd023e21729d478a6010a13d1140578097cc9d363bdf24d29c816868cb50219aa5ad4ba16e3f50a3042becb256bdab2194cf5ce878f5f2fbe7a2dd4159e828c59b5509ff7072a0298d7d1e6017b530c8dfdaf15a883f787e9ca1825ea8fa0969b7f58b8cdb2edb64dbb59b631a97e1bd5726b92c1ab6e41a57aeaaa351af60a89f8b502b6122806fcf6fdfe6924289b7e7bdb83772c058e68bd435bed77fb9f000000ffff030034f07389ec060000,2018-10-01 12:31:12+00,customer@gmail.com,,,CustomOrderId,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
VWB3riH9WuCv9NtSedaGE6,\x1f8b0800000000000003c455c1729b4810bde72b5c9cb3b21012029f222162c792bc9664a494631f0668c404984133032bc5a52fcb219f945fc83060457629deaabdec91f75e4fbfe9e96e7e7efff1f4eeec4cc3a17671a62d574383e12b7b5538a57d231610a24bd7d4de570a2e28834f4a66f0b5e738c21baecd8535b94e981f468395cfafa79be1eee3242a179baf97e6763c5cb9611d4c5908ac0e760a7952f67703d447e700e12d4d71b0938ab6c21812203f7aa6d569753aba615b9665b74dc56152521cc01dce2a89de332cc3eeebb6ae48d8e65806634a5ef056bbe143c829c76210860c3857d7e96fd6b6037d4f1457c10adb9fbd9bfb8531b059d17326cbcbd5106a9b39a36111884f24a22c5319647455bdca92806c043ca8ce9b222e80111ac2d942a00493b58a6f548ec4a58a1469daa079bce33840a944239472788699bca3f2afb75b56bfdfeee99d5ec30505634054b934d79b6b12dd2b8f7eb193753de5503137287b955cc16e8670955e4b11e31c93042cfbc3ba025b01cdb463a9430b22d8eec421f7383f81ca0a8853291d2c4e1dd23c4be7cf947e82ba8d29794e7228857ce61112e8482edf9ee152d56521aa065bab026ef3c23747037f1d47c5dc5999b30d33c6d162f41527ddc4f5703948a238b8bc9a264b17bafefda6672c67dffa76d2edba74d69b8ef2c9f5783cb81df7cae5d2bcddf63d7d6ed830c33ef1e3115fd9853bd3c7616c3b233f5bcffffa927778fca89d768441f5e4d38336bc731eb48b87ffc9de83b67f9e7b240ae589c03f5a336201e4b5e7863b543847bb0c88a8b02f8ff52043549010f9291cf777034fffb5efa430c40c02e1cd2795321622e717e7e7b045599e42a53b17312209dfd1a28e10db8fa006a75daf92489abba1024772c82ad79539c18ada073922de4821c733f55190f0a628c04a601e4b8ff5d23721205a2164b4e58b40d6a2d6b528ab578016b05d2e68d396cd60ca773e7cbcbdf8eab55057780a22a6d5427dda1fa808605e47eb07ec553114f69f56a00ade1fa62ba304cb3f825c6eee61e136cbb6dbd6cd66d9c6b8fa6d54cbad49062fba0595f2a9abd668d82bc4e3970ad80a202184afdfef772341d9f4dbeb1ebca3293044ea1dda6abfdbff020000ffff0300e1a26ae9ec060000,2018-10-01 12:33:11+00,customer@gmail.com,,,CustomOrderId,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
||||
At4CDM5vfewV2WDHEPKRbt,\x1f8b0800000000000003c4554d73a33810bdcfaf4871de758c0db6c9696d20c9c471c61fb15d35933908688c06908824183329ffb23dec4fdabfb092205e27e59daddacb1e79efb5faa9d5ddfcf9fb1f2f1f2e2e0c1c195717c65858ae37b3ab18be6f7a5befd69f4f9781307e510a2e28838f5ad6e7bbb5eb8af56437588deeef521644f1781bf0bbd9f3a4bebe8fabd5f3b79bc17e3ad9fa51134c5904ac09764b7952fea9059aa30b80684e331cd652d1d5184302e4873d18f53b3db33f1c398edd1d3a9ac3a4a23884479c2b8969f74756d71c0d4c4dc2bec0321853f286ef0d5b3e8282722cc651c480737d9db1fde361b901320bf8c4ab83bb38c2dc22d73dd7de06f5eac6228dcd82d1a80cc547125396eb0c325a554f5912907bc04375de0c71018cd0082e5602a598ec747cab72252e55a4ccb2162d929ae31065128d51c6e11566f28ecabf351a76468ee90c7bc3960a4bc680e86a19fe7a6948f4a02d06652dcb7acea0661e50fe2eb786fd1c6195ddc810e31c931446ce6f3b0576429a1ba752979644b0facc219f717106950510e752ba589c3ba47d95de3f53e6196a9e50f29ae4580af9ca1e12e8442e9f9ee14ad76525547fed7401f745190cbc71b04be272e96e078b67d69fc62bef1b4eadd45fe36a9cc64978733b4b373e58c1e767bbbf59fc183aa965f97461cfbce2fe6e3a1dcfa776b5d90ce6fbe1da5cf61d58e0800489c7b74ee92fcc699438ae17e4bbe5af5f8a1e4fbe1ae71d61d02df9f2644c1edd27e3eae97fb2f7641c5ec71e89527b22f0dd68272c84a2f1dc72c70a17a8ce8108857df9dacc31c425895090c1697bb7f0ec5ffb4e0a23cc2014ebe5bd52264214fceaf212f6282f3250ba4b912092f29a964d84d85f839e9b6eb3496269ee810a1ccb1953ae9539c1cac60739217e92424e6716a030e56d518055c0d62c3bd54bdf8480e84490d34e2042598b46d7a1acd90046c8ea42d0b62ddbc194ef7cfcf8f9de6bb64253e1198884aa7dfa72385231c0b289368fd8bb6268ec3f6d401d7c384e574e09963f04b9dbfce3be3deeda41bfddb509567f0db5dbda64f0a65b50259f5ab546cbde229ebc55c05e0089207aff7e7f3712546dbfbdefc1479a0143a459a19dee87c35f000000ffff0300011a2ad8eb060000,2018-10-01 13:51:01+00,customer@gmail.com,,,CustomOrderId,expired,3sgUCCtUBg6S8LJkrbdfAWbsJMqByFLfvSqjG6xKBWEd,f,
|
|
11
BTCPayServer.Tests/TestData/OldPayments.csv
Normal file
11
BTCPayServer.Tests/TestData/OldPayments.csv
Normal file
@ -0,0 +1,11 @@
|
||||
Id,Blob,InvoiceDataId,Accounted,Blob2,Type
|
||||
a8be47ca93a1583f60b25fb1279b6c162af731024d169f8d1cd0d29d1d391132-0,\x1f8b0800000000000003748f396ec33010457b9f4260ed82c39d2ead20958b2050a966c425201289824205100c9f2c458e942b447216b8c9af66fe03de603edf3fcebbaa22537021bd05dfa43e904305921b4139a36cbfd13c9731a7a1ac847006c02d78eb3df3149cf13682129e320e513304a59ced98ee40c68e29caa33408963b14ba0b68e84fc89f79be7a9d1692de04345a10d26b0116c0482339a008144da7509a2e4aca95d7511923b534fadb87cee57928c1afca32cde15aba69194baeb3df5e23c7a62637f5032e7d18ca1d16dce8b925751e629a7a2c290ff5666bc901ecbe258fc7fb75bc6a5b720a4fe896758ff8f21a2eff289b65fc3d7a4acf81ec2e5f000000ffff03002eb110c572010000,Q7RqoHLngK9svM4MgRyi9y,t,,
|
||||
c703dd8ae34622dfb96d677260631c39af483918d9e63676839be0db1b8a9268-1,\x1f8b0800000000000003748f3d6ec3300c85f79cc2d09c413fb644658c8b4e198ac2a3175aa20ba1b565b8720123c8c93af448bd42e5040dbaf44de47bc0f7c8efcfaff3ae28d84c8ec207f9260cc40e85a84a59951c14df6f695cd214c39872c2345809283adf11b71d28a39526edad00654bc05e59e1b2c58dd446fbcefa5eca529302f49e2b67b8e057b13b79b971bdecace37709835694d0a1e08012786572a3b2d944aba4ef2de81e205f50213907e6c643e7e23226f21999e685aea69bd729c53afaed35766c6af6c77ec275a0313d60c22d3db7ac8e631fe601538863bdd15a76a8a4d8b7ecf9f898e72bb765277a41b7e6bdc7b777bafcc36cd6e9b7f5145e89ed2e3f000000ffff03000f0bf2f373010000,Q7RqoHLngK9svM4MgRyi9y,t,,
|
||||
f237a3d75018f4435927f5b1be1a8902045d88ddda39c25254214c3e484bd383-1,\x1f8b080000000000000374904d6ac3301046f73945d03a14fdc6a32ce3925528a564e9cd581a1593da0a8e9c62424ed6458fd42b547669e8a6b39bf7c193e6fbfaf8bc2e964b76a1fedcc48e6d967c35ed3d396a2ee40f4d4b190aa3a5e5609599d38ed27bec8f3b9a32fec0a791731287748a4d973267a0bcd2b50645dac96c30d249653d7a0f603cd75c821558532d829185b15a052dc0705fa05245905ccc62ceeee661f66ac36bedf97d44815668a851704009dc14be26653344aba40f16d601c0d660909c83e2c787cec5a14be4b332f503cdd0f5e329c532fae930b63d94ec0f7ec6b1a52e3d62c229bd56ac8c5d68fa1653aeae9c6c15db88756e6a55b197ed2e6fb3b9624ff7c232cb8565b4a75774635e03be9de9f6cf4387f1f4fb957d7324b6b87d030000ffff0300eef4b353b2010000,Q7RqoHLngK9svM4MgRyi9y,t,,
|
||||
aedd36b5c976e56543cd7a5e207ea13dc801764cb3682474ab75a54399f20c27-9,\x1f8b08000000000000037490bf6ec2301087779e02794695ff3b66241513aaaa8a91e5629fab8812a3e050458827ebd047ea2bd44e0bead29b7cdfcffacebeaf8fcfcb6c3e2767ec4f6decc872ce16a5efd1617b46bf6d0f58a052dc68658d9cd20ed37becf76b2c197da0a5f894c4211d63dba5cc0937d4056ead14a08c82c6482e75d508e98ca6cc55c20333c8a94203ce4ba1152aa39d6d94d0de03523b8929b99b87c99b6779c128a3bfc50c582621d0c02b69a571ba623a7068b4e516adb0547b45196f022aea2bf3e303e7e2d025f45999fa0127e8faf198621d7df918596d6bf2073fc378c02e3d4282925e76a48e5d68fb03a4bcbabad87664c9163bf2b25ae75380b713e6eee9bead0cf3b632dae02bb8f176e7facf94ed78bcbd63d3ee91ccaedf000000ffff030012c428b2af010000,Q7RqoHLngK9svM4MgRyi9y,t,,
|
||||
3358197dcb30420d3487516e8ec9dcc0f6d03f16b4f41b77cd3ad429b1e529ee-0,\x1f8b0800000000000003748fb14ec3301086f73e45e4b9839db3e3b86383983a209431cbe57c4116248e828314557d32061e8957202914b170d3ddff49dfe9ff7cff38efb24c4c4c1cded8d7a16771c894d18536e024ec371ae734c630a49508e6dcb16955eebc06246fad6a3bdd6a5540e7655790f444aee4828d2a2d68e9750e925aeb95332580fc19f16b9eaf5e50680dc8fcc69545a774993b7285c1429adc90266d491a8980502290b2d2b80ebd85d27efb9028ce4362bf2ad334f335a4691953aca2dfaa89635d893ff1032e3d0fe90e136ef4dc882a0e5d987a4c210ed5666bc441ed1bf178bc5fb70e5f5e79bd4efc84b4dc82cb3fca7a196f4f4fe199c5eef2050000ffff0300b173d7d272010000,Q7RqoHLngK9svM4MgRyi9y,t,,
|
||||
03d66d2d633dac96cc45546a0555cb917f5d2f0d37dddfdf67e48f88ae728aa1-5,\x1f8b08000000000000037490bd6ec2301446779e027946951dff33928a095555c598e5625fa38812a3e050458827ebd047ea2bd40e2aead2ebe99e4f3ab6bfefcfafeb6c3e2717eccf6decc872ce1665efd1617b41bf6d8f58a0144652a3f93ded307dc4feb0c692d1275aa69a9238a4536cbb94390166405780c6988042691ff2f15c535f05e975b0cceda4a4528114423a671538ee15afbcf25e514ee524a6e4611e26af50a085a68f611a2c1312ad1396a3715c280f2cd8c0b50dc2535571b6432a0ce74aed8cbefbc0b93874097d56a67ec009ba7e3ca558475f3e4656db9afcc1af301eb14bcf90a0a4d786d4b10b6d7f8494abab8bad214bc9b45a34e46db5cecb246ec8cba3afcc725f196d700f6ecc6b80f733defeb9673b9e7e5fb2690f4866b71f000000ffff03007bb33a2fb1010000,Q7RqoHLngK9svM4MgRyi9y,t,,
|
||||
dc130d025a4bd2e7ee83b7707d850e7c1a52872c222a5bc2e05d3357e79aa762-1,\x1f8b08000000000000037490bf6ec2301087779e027946e8fc2f761849c584aaaa6264b9d817145162141caa08f1641dfa487d85daa98abaf426dfefb3beb3efebe3f3369bcfd995fa4b1b3ab69af345ee7b72d45ec9efda13e5506bae1557d64eb4a3f81efae386328325e4121309433c87b68b296785405322196da4d49ec0095d0b1442386bb4e0681c90d5c61ba88d954464bc50b54610e0b9f40ef82406f6300f9317ea74c5c1a3b8c1922b4da553a524eba42a3cf2a66ca4291be5a11092d704ca4a5914b5353f3e742e0c5d249f940dbe5d684a5d3f9e63a882cf3f63eb5dc5fec42f389ea88b4f1831d3db9e55a16bdafe8431edaecaba3d5bc162cf5ed79b748afd40a9797e6c2bd365e65b3aa01b533b4dbeff3364379e7f9fb16d8fc466f76f000000ffff0300f93b039faf010000,Q7RqoHLngK9svM4MgRyi9y,f,,
|
||||
afc39884e024cbb3a48ca997b7e878b199d85fe97f90f94471364cc866ba83ad-1,\x1f8b080000000000000374904d6ac3301085f73945d03a14c992f593655cb20aa5942cbd1949a362d258c191534cc8c9bae8917a854a2e0ddd7476ef3de6d3e87d7d7c5e17cb25b9e070ee624fd64bb62a7a4087dd05fdbe3b6231eb9ad5a21246ce698fe93d0e872d968c3ed032d59cc4319d62d7a7ec13f09a5b90d269e1b8544c88600c55014d1dbc36c632a5515b651418ed4058ee6c25906a91258740d90ca6e44e1e672eb5402b47efc3f23e13351a270cc7bc2aa407164ce0ca04e1a9ac38b34885e65c4aabd50f0f9c8b639fd067641a469c4d374ca7149be8cbc7c866df903ff6334c47ecd3232428e9b5254dec43371c21e5ea9a426bc95ae59e562d79d96cb398c12d79baf795bddc57b676f80a6eca32c0db196fffbcb39f4ebf97ecba0392c5ed1b0000ffff030018cc54a1b1010000,Q7RqoHLngK9svM4MgRyi9y,t,,
|
||||
6f2b8513ebfdb14b41d4de235f050cd028400cb01fde700d72c6bedc2ad8af1e-5,\x1f8b08000000000000037490bf6ac3301087f73c45d01cc249966425635c3285528a472ffa732a26b5151c39c5843c59873e525fa192434397de22ddef83efa4fbfefcba2e964b72c1e1dc869e6c977495fb012db6177475db610e85908a6d24c04c7b8c1f6138ee313358432e369330c65368fb987242517ba79876d6a0952503578243ea0d80e5c0944b2708e159e1d0714eb931d43b34b4508219e941cc62200ff378f752c719c2a368a937942b445158855e6967b8b05c0a2e9934c0b59585960c0b55d272a3cabb4f5b1bc63ea24b4aafdfcf38a776984e3154c1e59f915d5d913ff18b9e3aece3938e3ad36b43aad0fb76e8744cbbabb2ae215b5835e475b74fb7388c989ae7c7b6325d677ec0376da7d4ce936fff0ca9a7d3ef330eed11c9e2f6030000ffff0300b09cf40daf010000,Q7RqoHLngK9svM4MgRyi9y,f,,
|
||||
3a6659e189f83f649c0010305def1b8fb78efdbe8c0ce44d56c8c22fff742b55-61,\x1f8b08000000000000037490bd6ec3201485f73c45c41c5580f973c6b8ca14555595d10be65e222b8d891c9cca8af2641dfa487d8582ab585d7a27ce39f001e7fbf3ebb6582ec915fb4b1b3ab25eb255d63d3a6caf08fbf684d994525121a85653da61fc08fd718b39a34f340f9f9230c47368bb987c22256fb4f09e7bc79d914a000aea8c6bd083c1461bcfd2524241593a5f3a250aef8d2991c952a9c21630812999c9c3c4d55615cad37998b625131eb8e5c080496c941525958c97b2708c838286022ac5b56060f42fcf3a17862e222464ec079c4cd78fe718aa00f96364b3afc81ffbd58e27ece2b38d36a7b79a54a1f36d7fb2315557655a4dd64c30b1aac9db669b84b7ef174cea652e2c99a9b064edf060ddf8d873ffe7a2fd787e3c65d71e912cee3f000000ffff0300a991d8c4b2010000,Q7RqoHLngK9svM4MgRyi9y,t,,
|
|
@ -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]
|
||||
@ -156,6 +191,12 @@ namespace BTCPayServer.Tests
|
||||
// Ripio keeps changing their pair, so anything is fine...
|
||||
Assert.NotEmpty(exchangeRates.ByExchange[name]);
|
||||
}
|
||||
else if (name == "bitnob")
|
||||
{
|
||||
Assert.Contains(exchangeRates.ByExchange[name],
|
||||
e => e.CurrencyPair == new CurrencyPair("BTC", "NGN") &&
|
||||
e.BidAsk.Bid > 1.0m); // 1 BTC will always be more than 1 NGN
|
||||
}
|
||||
else if (name == "cryptomarket")
|
||||
{
|
||||
Assert.Contains(exchangeRates.ByExchange[name],
|
||||
@ -231,7 +272,8 @@ namespace BTCPayServer.Tests
|
||||
"https://www.btse.com", // not allowing to be hit from circleci
|
||||
"https://www.bitpay.com", // not allowing to be hit from circleci
|
||||
"https://support.bitpay.com",
|
||||
"https://www.coingecko.com" // unhappy service
|
||||
"https://www.coingecko.com", // unhappy service
|
||||
"https://www.wasabiwallet.io/" // returning Forbidden
|
||||
};
|
||||
|
||||
foreach (var match in regex.Matches(text).OfType<Match>())
|
||||
@ -278,7 +320,6 @@ retry:
|
||||
}
|
||||
catch (Exception ex) when (ex is MatchesException)
|
||||
{
|
||||
var details = ex.Message;
|
||||
TestLogs.LogInformation($"FAILED: {url} ({file}) – anchor not found: {uri.Fragment}");
|
||||
|
||||
throw;
|
||||
@ -424,7 +465,7 @@ retry:
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "dom-confetti", "dom-confetti.min.js").Trim();
|
||||
version = Regex.Match(actual, "Original file: /npm/dom-confetti@([0-9]+.[0-9]+.[0-9]+)/lib/main.js").Groups[1].Value;
|
||||
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/dom-confetti@{version}/lib/main.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/dom-confetti@{version}")).Content.ReadAsStringAsync()).Trim();
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vue-sortable", "sortable.min.js").Trim();
|
||||
@ -454,6 +495,10 @@ retry:
|
||||
version = Regex.Match(actual, "Original file: /npm/decimal\\.js@([0-9]+.[0-9]+.[0-9]+)/decimal\\.js").Groups[1].Value;
|
||||
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/decimal.js@{version}/decimal.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "bbqr", "bbqr.iife.js").Trim();
|
||||
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/bbqr@1.0.0/dist/bbqr.iife.js")).Content.ReadAsStringAsync()).Trim();
|
||||
EqualJsContent(expected, actual);
|
||||
}
|
||||
|
||||
private void EqualJsContent(string expected, string actual)
|
||||
|
@ -73,6 +73,9 @@ using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using Xunit.Sdk;
|
||||
using CreateInvoiceRequest = BTCPayServer.Client.Models.CreateInvoiceRequest;
|
||||
using CreatePaymentRequestRequest = BTCPayServer.Client.Models.CreatePaymentRequestRequest;
|
||||
using MarkPayoutRequest = BTCPayServer.Client.Models.MarkPayoutRequest;
|
||||
using PaymentRequestData = BTCPayServer.Client.Models.PaymentRequestData;
|
||||
using RatesViewModel = BTCPayServer.Models.StoreViewModels.RatesViewModel;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
@ -267,7 +270,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 +349,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
try
|
||||
{
|
||||
var throwsBitpay404Error = user.BitPay.GetInvoice(invoice.Id + "123");
|
||||
user.BitPay.GetInvoice(invoice.Id + "123");
|
||||
}
|
||||
catch (BitPayException ex)
|
||||
{
|
||||
@ -395,7 +397,7 @@ namespace BTCPayServer.Tests
|
||||
BOLT11PaymentRequest.Parse(newBolt11, Network.RegTest).MinimumAmount.ToDecimal(LightMoneyUnit.BTC));
|
||||
}, 40000);
|
||||
|
||||
TestLogs.LogInformation($"Paying invoice {newInvoice.Id} remaining due amount {newInvoice.BtcDue.GetValue((BTCPayNetwork) tester.DefaultNetwork)} via lightning");
|
||||
TestLogs.LogInformation($"Paying invoice {newInvoice.Id} remaining due amount {newInvoice.BtcDue.GetValue((BTCPayNetwork)tester.DefaultNetwork)} via lightning");
|
||||
var evt = await tester.WaitForEvent<InvoiceDataChangedEvent>(async () =>
|
||||
{
|
||||
await tester.SendLightningPaymentAsync(newInvoice);
|
||||
@ -885,7 +887,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 +1074,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 +1150,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 +1178,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",
|
||||
@ -1301,11 +1310,8 @@ namespace BTCPayServer.Tests
|
||||
var user = tester.NewAccount();
|
||||
await user.GrantAccessAsync();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
await tester.ExplorerNode.EnsureGenerateAsync(1);
|
||||
var rng = new Random();
|
||||
var seed = rng.Next();
|
||||
rng = new Random(seed);
|
||||
TestLogs.LogInformation("Seed: " + seed);
|
||||
foreach (var networkFeeMode in Enum.GetValues(typeof(NetworkFeeMode)).Cast<NetworkFeeMode>())
|
||||
{
|
||||
await user.SetNetworkFeeMode(networkFeeMode);
|
||||
@ -1318,7 +1324,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task AssertTopUpBtcPrice(ServerTester tester, TestAccount user, Money btcSent, decimal expectedPriceWithoutNetworkFee, NetworkFeeMode networkFeeMode)
|
||||
private async Task AssertTopUpBtcPrice(ServerTester tester, TestAccount user, Money btcSent, decimal expectedPriceWithoutNetworkFee, NetworkFeeMode networkFeeMode)
|
||||
{
|
||||
var cashCow = tester.ExplorerNode;
|
||||
// First we try payment with a merchant having only BTC
|
||||
@ -1343,7 +1349,6 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
networkFee = 0.0m;
|
||||
}
|
||||
|
||||
await cashCow.SendToAddressAsync(invoiceAddress, paid);
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
@ -1461,7 +1466,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);
|
||||
@ -1475,6 +1480,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Lightning", "Lightning")]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanSetPaymentMethodLimits()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
@ -1510,9 +1516,10 @@ namespace BTCPayServer.Tests
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
Assert.Single(invoice.CryptoInfo);
|
||||
Assert.Equal(PaymentTypes.LightningLike.ToString(), invoice.CryptoInfo[0].PaymentType);
|
||||
// LN and LNURL
|
||||
Assert.Equal(2, invoice.CryptoInfo.Length);
|
||||
Assert.Contains(invoice.CryptoInfo, c => c.PaymentType == PaymentTypes.LNURLPay.ToString());
|
||||
Assert.Contains(invoice.CryptoInfo, c => c.PaymentType == PaymentTypes.LightningLike.ToString());
|
||||
|
||||
// Let's replicate https://github.com/btcpayserver/btcpayserver/issues/2963
|
||||
// We allow BTC for more than 5 USD, and LN for less than 150. The default is LN, so the default
|
||||
@ -1822,7 +1829,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Empty(appList2.Apps);
|
||||
Assert.Equal("test", appList.Apps[0].AppName);
|
||||
Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id);
|
||||
|
||||
|
||||
Assert.True(app.Role.ToPermissionSet(app.StoreId).Contains(Policies.CanModifyStoreSettings, app.StoreId));
|
||||
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
|
||||
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id));
|
||||
@ -1941,6 +1948,173 @@ namespace BTCPayServer.Tests
|
||||
entity.GetPaymentMethods().First().Calculate();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
[Fact()]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task EnsureWebhooksTrigger()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
await user.GrantAccessAsync();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
await user.SetupWebhook();
|
||||
var client = await user.CreateClient();
|
||||
|
||||
|
||||
var invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest()
|
||||
{
|
||||
Amount = 0.00m,
|
||||
Currency = "BTC"
|
||||
});;
|
||||
await user.AssertHasWebhookEvent(WebhookEventType.InvoiceCreated, (WebhookInvoiceEvent x)=> Assert.Equal(invoice.Id, x.InvoiceId));
|
||||
|
||||
//invoice payment webhooks
|
||||
invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest()
|
||||
{
|
||||
Amount = 0.01m,
|
||||
Currency = "BTC"
|
||||
});
|
||||
|
||||
var invoicePaymentRequest = new BitcoinUrlBuilder((await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id)).Single(model =>
|
||||
PaymentMethodId.Parse(model.PaymentMethod) ==
|
||||
new PaymentMethodId("BTC", BitcoinPaymentType.Instance))
|
||||
.PaymentLink, tester.ExplorerNode.Network);
|
||||
var halfPaymentTx = await tester.ExplorerNode.SendToAddressAsync(invoicePaymentRequest.Address, Money.Coins(invoicePaymentRequest.Amount.ToDecimal(MoneyUnit.BTC)/2m));
|
||||
|
||||
invoicePaymentRequest = new BitcoinUrlBuilder((await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id)).Single(model =>
|
||||
PaymentMethodId.Parse(model.PaymentMethod) ==
|
||||
new PaymentMethodId("BTC", BitcoinPaymentType.Instance))
|
||||
.PaymentLink, tester.ExplorerNode.Network);
|
||||
var remainingPaymentTx = await tester.ExplorerNode.SendToAddressAsync(invoicePaymentRequest.Address, Money.Coins(invoicePaymentRequest.Amount.ToDecimal(MoneyUnit.BTC)));
|
||||
|
||||
await user.AssertHasWebhookEvent(WebhookEventType.InvoiceCreated, (WebhookInvoiceEvent x)=> Assert.Equal(invoice.Id, x.InvoiceId));
|
||||
await user.AssertHasWebhookEvent(WebhookEventType.InvoiceProcessing, (WebhookInvoiceEvent x)=> Assert.Equal(invoice.Id, x.InvoiceId));
|
||||
await user.AssertHasWebhookEvent(WebhookEventType.InvoiceReceivedPayment,
|
||||
(WebhookInvoiceReceivedPaymentEvent x) =>
|
||||
{
|
||||
Assert.Equal(invoice.Id, x.InvoiceId);
|
||||
Assert.Contains(halfPaymentTx.ToString(), x.Payment.Id);
|
||||
});
|
||||
await user.AssertHasWebhookEvent(WebhookEventType.InvoiceReceivedPayment,
|
||||
(WebhookInvoiceReceivedPaymentEvent x) =>
|
||||
{
|
||||
Assert.Equal(invoice.Id, x.InvoiceId);
|
||||
Assert.Contains(remainingPaymentTx.ToString(), x.Payment.Id);
|
||||
});
|
||||
|
||||
await tester.ExplorerNode.GenerateAsync(1);
|
||||
|
||||
await user.AssertHasWebhookEvent(WebhookEventType.InvoicePaymentSettled,
|
||||
(WebhookInvoiceReceivedPaymentEvent x) =>
|
||||
{
|
||||
Assert.Equal(invoice.Id, x.InvoiceId);
|
||||
Assert.Contains(halfPaymentTx.ToString(), x.Payment.Id);
|
||||
});
|
||||
await user.AssertHasWebhookEvent(WebhookEventType.InvoicePaymentSettled,
|
||||
(WebhookInvoiceReceivedPaymentEvent x) =>
|
||||
{
|
||||
Assert.Equal(invoice.Id, x.InvoiceId);
|
||||
Assert.Contains(remainingPaymentTx.ToString(), x.Payment.Id);
|
||||
});
|
||||
|
||||
await user.AssertHasWebhookEvent(WebhookEventType.InvoiceSettled, (WebhookInvoiceEvent x)=> Assert.Equal(invoice.Id, x.InvoiceId));
|
||||
|
||||
invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest()
|
||||
{
|
||||
Amount = 0.01m,
|
||||
Currency = "BTC",
|
||||
});
|
||||
invoicePaymentRequest = new BitcoinUrlBuilder((await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id)).Single(model =>
|
||||
PaymentMethodId.Parse(model.PaymentMethod) ==
|
||||
new PaymentMethodId("BTC", BitcoinPaymentType.Instance))
|
||||
.PaymentLink, tester.ExplorerNode.Network);
|
||||
halfPaymentTx = await tester.ExplorerNode.SendToAddressAsync(invoicePaymentRequest.Address, Money.Coins(invoicePaymentRequest.Amount.ToDecimal(MoneyUnit.BTC)/2m));
|
||||
|
||||
|
||||
await user.AssertHasWebhookEvent(WebhookEventType.InvoiceCreated, (WebhookInvoiceEvent x)=> Assert.Equal(invoice.Id, x.InvoiceId));
|
||||
await user.AssertHasWebhookEvent(WebhookEventType.InvoiceReceivedPayment,
|
||||
(WebhookInvoiceReceivedPaymentEvent x) =>
|
||||
{
|
||||
Assert.Equal(invoice.Id, x.InvoiceId);
|
||||
Assert.Contains(halfPaymentTx.ToString(), x.Payment.Id);
|
||||
});
|
||||
|
||||
invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest()
|
||||
{
|
||||
Amount = 0.01m,
|
||||
Currency = "BTC"
|
||||
});
|
||||
|
||||
await user.AssertHasWebhookEvent(WebhookEventType.InvoiceCreated, (WebhookInvoiceEvent x)=> Assert.Equal(invoice.Id, x.InvoiceId));
|
||||
await client.MarkInvoiceStatus(user.StoreId, invoice.Id, new MarkInvoiceStatusRequest() { Status = InvoiceStatus.Invalid});
|
||||
await user.AssertHasWebhookEvent(WebhookEventType.InvoiceInvalid, (WebhookInvoiceEvent x)=> Assert.Equal(invoice.Id, x.InvoiceId));
|
||||
|
||||
//payment request webhook test
|
||||
var pr = await client.CreatePaymentRequest(user.StoreId, new CreatePaymentRequestRequest()
|
||||
{
|
||||
Amount = 100m,
|
||||
Currency = "USD",
|
||||
Title = "test pr",
|
||||
//TODO: this is a bug, we should not have these props in create request
|
||||
StoreId = user.StoreId,
|
||||
FormResponse = new JObject(),
|
||||
//END todo
|
||||
Description = "lala baba"
|
||||
});
|
||||
await user.AssertHasWebhookEvent(WebhookEventType.PaymentRequestCreated, (WebhookPaymentRequestEvent x)=> Assert.Equal(pr.Id, x.PaymentRequestId));
|
||||
pr = await client.UpdatePaymentRequest(user.StoreId, pr.Id,
|
||||
new UpdatePaymentRequestRequest() { Title = "test pr updated", Amount = 100m,
|
||||
Currency = "USD",
|
||||
//TODO: this is a bug, we should not have these props in create request
|
||||
StoreId = user.StoreId,
|
||||
FormResponse = new JObject(),
|
||||
//END todo
|
||||
Description = "lala baba"});
|
||||
await user.AssertHasWebhookEvent(WebhookEventType.PaymentRequestUpdated, (WebhookPaymentRequestEvent x)=> Assert.Equal(pr.Id, x.PaymentRequestId));
|
||||
var inv = await client.PayPaymentRequest(user.StoreId, pr.Id, new PayPaymentRequestRequest() {});
|
||||
|
||||
await client.MarkInvoiceStatus(user.StoreId, inv.Id, new MarkInvoiceStatusRequest() { Status = InvoiceStatus.Settled});
|
||||
await user.AssertHasWebhookEvent(WebhookEventType.PaymentRequestStatusChanged, (WebhookPaymentRequestEvent x)=>
|
||||
{
|
||||
Assert.Equal(PaymentRequestData.PaymentRequestStatus.Completed, x.Status);
|
||||
Assert.Equal(pr.Id, x.PaymentRequestId);
|
||||
});
|
||||
await client.ArchivePaymentRequest(user.StoreId, pr.Id);
|
||||
await user.AssertHasWebhookEvent(WebhookEventType.PaymentRequestArchived, (WebhookPaymentRequestEvent x)=> Assert.Equal(pr.Id, x.PaymentRequestId));
|
||||
//payoyt webhooks test
|
||||
var payout = await client.CreatePayout(user.StoreId,
|
||||
new CreatePayoutThroughStoreRequest()
|
||||
{
|
||||
Amount = 0.0001m,
|
||||
Destination = (await tester.ExplorerNode.GetNewAddressAsync()).ToString(),
|
||||
Approved = true,
|
||||
PaymentMethod = "BTC"
|
||||
});
|
||||
await user.AssertHasWebhookEvent(WebhookEventType.PayoutCreated, (WebhookPayoutEvent x)=> Assert.Equal(payout.Id, x.PayoutId));
|
||||
await client.MarkPayout(user.StoreId, payout.Id, new MarkPayoutRequest(){ State = PayoutState.AwaitingApproval});
|
||||
await user.AssertHasWebhookEvent(WebhookEventType.PayoutUpdated, (WebhookPayoutEvent x)=>
|
||||
{
|
||||
Assert.Equal(payout.Id, x.PayoutId);
|
||||
Assert.Equal(PayoutState.AwaitingApproval, x.PayoutState);
|
||||
});
|
||||
|
||||
await client.ApprovePayout(user.StoreId, payout.Id, new ApprovePayoutRequest(){});
|
||||
await user.AssertHasWebhookEvent(WebhookEventType.PayoutApproved, (WebhookPayoutEvent x)=>
|
||||
{
|
||||
Assert.Equal(payout.Id, x.PayoutId);
|
||||
Assert.Equal(PayoutState.AwaitingPayment, x.PayoutState);
|
||||
});
|
||||
await client.CancelPayout(user.StoreId, payout.Id );
|
||||
await user.AssertHasWebhookEvent(WebhookEventType.PayoutUpdated, (WebhookPayoutEvent x)=>
|
||||
{
|
||||
Assert.Equal(payout.Id, x.PayoutId);
|
||||
Assert.Equal(PayoutState.Cancelled, x.PayoutState);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task InvoiceFlowThroughDifferentStatesCorrectly()
|
||||
@ -2100,18 +2274,18 @@ namespace BTCPayServer.Tests
|
||||
});
|
||||
|
||||
// Test on the webhooks
|
||||
user.AssertHasWebhookEvent<WebhookInvoiceSettledEvent>(WebhookEventType.InvoiceSettled,
|
||||
await user.AssertHasWebhookEvent<WebhookInvoiceSettledEvent>(WebhookEventType.InvoiceSettled,
|
||||
c =>
|
||||
{
|
||||
Assert.False(c.ManuallyMarked);
|
||||
Assert.True(c.OverPaid);
|
||||
});
|
||||
user.AssertHasWebhookEvent<WebhookInvoiceProcessingEvent>(WebhookEventType.InvoiceProcessing,
|
||||
await user.AssertHasWebhookEvent<WebhookInvoiceProcessingEvent>(WebhookEventType.InvoiceProcessing,
|
||||
c =>
|
||||
{
|
||||
Assert.True(c.OverPaid);
|
||||
});
|
||||
user.AssertHasWebhookEvent<WebhookInvoiceReceivedPaymentEvent>(WebhookEventType.InvoiceReceivedPayment,
|
||||
await user.AssertHasWebhookEvent<WebhookInvoiceReceivedPaymentEvent>(WebhookEventType.InvoiceReceivedPayment,
|
||||
c =>
|
||||
{
|
||||
Assert.False(c.AfterExpiration);
|
||||
@ -2121,7 +2295,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.StartsWith(txId.ToString(), c.Payment.Id);
|
||||
|
||||
});
|
||||
user.AssertHasWebhookEvent<WebhookInvoicePaymentSettledEvent>(WebhookEventType.InvoicePaymentSettled,
|
||||
await user.AssertHasWebhookEvent<WebhookInvoicePaymentSettledEvent>(WebhookEventType.InvoicePaymentSettled,
|
||||
c =>
|
||||
{
|
||||
Assert.False(c.AfterExpiration);
|
||||
@ -2143,8 +2317,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)]
|
||||
@ -2324,17 +2497,21 @@ namespace BTCPayServer.Tests
|
||||
using var tester = CreateServerTester(newDb: true);
|
||||
await tester.StartAsync();
|
||||
var f = tester.PayTester.GetService<ApplicationDbContextFactory>();
|
||||
const string id = "BTCPayServer.Services.PoliciesSettings";
|
||||
using (var ctx = f.CreateContext())
|
||||
{
|
||||
var setting = new SettingData() { Id = "BTCPayServer.Services.PoliciesSettings" };
|
||||
setting.Value = JObject.Parse("{\"RootAppId\": null, \"RootAppType\": 1, \"Experimental\": false, \"PluginSource\": null, \"LockSubscription\": false, \"DisableSSHService\": false, \"PluginPreReleases\": false, \"BlockExplorerLinks\": [],\"DomainToAppMapping\": [{\"AppId\": \"87kj5yKay8mB4UUZcJhZH5TqDKMD3CznjwLjiu1oYZXe\", \"Domain\": \"donate.nicolas-dorier.com\", \"AppType\": 0}], \"CheckForNewVersions\": false, \"AllowHotWalletForAll\": false, \"RequiresConfirmedEmail\": false, \"DiscourageSearchEngines\": false, \"DisableInstantNotifications\": false, \"DisableNonAdminCreateUserApi\": false, \"AllowHotWalletRPCImportForAll\": false, \"AllowLightningInternalNodeForAll\": false, \"DisableStoresToUseServerEmailSettings\": false}").ToString();
|
||||
// remove existing policies setting
|
||||
var setting = await ctx.Settings.FirstOrDefaultAsync(c => c.Id == id);
|
||||
if (setting != null) ctx.Settings.Remove(setting);
|
||||
// create legacy policies setting that needs migration
|
||||
setting = new SettingData { Id = id, Value = JObject.Parse("{\"RootAppId\": null, \"RootAppType\": 1, \"Experimental\": false, \"PluginSource\": null, \"LockSubscription\": false, \"DisableSSHService\": false, \"PluginPreReleases\": false, \"BlockExplorerLinks\": [],\"DomainToAppMapping\": [{\"AppId\": \"87kj5yKay8mB4UUZcJhZH5TqDKMD3CznjwLjiu1oYZXe\", \"Domain\": \"donate.nicolas-dorier.com\", \"AppType\": 0}], \"CheckForNewVersions\": false, \"AllowHotWalletForAll\": false, \"RequiresConfirmedEmail\": false, \"DiscourageSearchEngines\": false, \"DisableNonAdminCreateUserApi\": false, \"AllowHotWalletRPCImportForAll\": false, \"AllowLightningInternalNodeForAll\": false, \"DisableStoresToUseServerEmailSettings\": false}").ToString() };
|
||||
ctx.Settings.Add(setting);
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
await RestartMigration(tester);
|
||||
using (var ctx = f.CreateContext())
|
||||
{
|
||||
var setting = await ctx.Settings.FirstOrDefaultAsync(c => c.Id == "BTCPayServer.Services.PoliciesSettings");
|
||||
var setting = await ctx.Settings.FirstOrDefaultAsync(c => c.Id == id);
|
||||
var o = JObject.Parse(setting.Value);
|
||||
Assert.Equal("Crowdfund", o["RootAppType"].Value<string>());
|
||||
o = (JObject)((JArray)o["DomainToAppMapping"])[0];
|
||||
@ -2398,12 +2575,12 @@ namespace BTCPayServer.Tests
|
||||
Assert.NotNull(lnMethod.GetExternalLightningUrl());
|
||||
|
||||
var url = lnMethod.GetExternalLightningUrl();
|
||||
var kv = LightningConnectionStringHelper.ExtractValues(url, out var connType);
|
||||
Assert.Equal(LightningConnectionType.Charge,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")));
|
||||
var auth = Assert.IsType<ChargeAuthentication.UserPasswordAuthentication>(client.ChargeAuthentication);
|
||||
|
||||
|
||||
Assert.Equal("pass", auth.NetworkCredential.Password);
|
||||
Assert.Equal("usr", auth.NetworkCredential.UserName);
|
||||
|
||||
@ -2605,7 +2782,7 @@ namespace BTCPayServer.Tests
|
||||
await tester.StartAsync();
|
||||
|
||||
var acc = tester.NewAccount();
|
||||
acc.GrantAccess(true);
|
||||
await acc.GrantAccessAsync(true);
|
||||
|
||||
var settings = tester.PayTester.GetService<SettingsRepository>();
|
||||
var emailSenderFactory = tester.PayTester.GetService<EmailSenderFactory>();
|
||||
@ -2630,14 +2807,14 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal("admin@admin.com", (await Assert.IsType<ServerEmailSender>(await emailSenderFactory.GetEmailSender()).GetEmailSettings()).Login);
|
||||
Assert.Null(await Assert.IsType<StoreEmailSender>(await emailSenderFactory.GetEmailSender(acc.StoreId)).GetEmailSettings());
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(await acc.GetController<UIStoresController>().StoreEmailSettings(acc.StoreId, new EmailsViewModel(new EmailSettings()
|
||||
Assert.IsType<RedirectToActionResult>(await acc.GetController<UIStoresController>().StoreEmailSettings(acc.StoreId, new EmailsViewModel(new EmailSettings
|
||||
{
|
||||
From = "store@store.com",
|
||||
Login = "store@store.com",
|
||||
Password = "store@store.com",
|
||||
Port = 1234,
|
||||
Server = "store.com"
|
||||
}), ""));
|
||||
}), "", true));
|
||||
|
||||
Assert.Equal("store@store.com", (await Assert.IsType<StoreEmailSender>(await emailSenderFactory.GetEmailSender(acc.StoreId)).GetEmailSettings()).Login);
|
||||
}
|
||||
@ -2775,7 +2952,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,
|
||||
@ -2808,10 +2985,10 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Selenium", "Selenium")]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanCreateReports()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
using var tester = CreateServerTester(newDb: true);
|
||||
tester.ActivateLightning();
|
||||
tester.DeleteStore = false;
|
||||
await tester.StartAsync();
|
||||
@ -2829,7 +3006,7 @@ namespace BTCPayServer.Tests
|
||||
var app = await client.CreatePointOfSaleApp(acc.StoreId, new CreatePointOfSaleAppRequest()
|
||||
{
|
||||
AppName = "Static",
|
||||
DefaultView = Client.Models.PosViewType.Static,
|
||||
DefaultView = Client.Models.PosViewType.Static,
|
||||
Template = new PointOfSaleSettings().Template
|
||||
});
|
||||
var resp = await posController.ViewPointOfSale(app.Id, choiceKey: "green-tea");
|
||||
@ -2839,7 +3016,7 @@ namespace BTCPayServer.Tests
|
||||
app = await client.CreatePointOfSaleApp(acc.StoreId, new CreatePointOfSaleAppRequest()
|
||||
{
|
||||
AppName = "Cart",
|
||||
DefaultView = Client.Models.PosViewType.Cart,
|
||||
DefaultView = Client.Models.PosViewType.Cart,
|
||||
Template = new PointOfSaleSettings().Template
|
||||
});
|
||||
resp = await posController.ViewPointOfSale(app.Id, posData: new JObject()
|
||||
@ -2909,6 +3086,55 @@ namespace BTCPayServer.Tests
|
||||
.ToDictionary(d => d.Key, r => r.Sum(d => d[countIndex].Value<int>()));
|
||||
Assert.Equal(8, itemsCount["green-tea"]);
|
||||
Assert.Equal(1, itemsCount["black-tea"]);
|
||||
|
||||
await acc.ImportOldInvoices();
|
||||
var date2018 = new DateTimeOffset(2018, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
report = await GetReport(acc, new() { ViewName = "Payments", TimePeriod = new TimePeriod() { From = date2018, To = date2018 + TimeSpan.FromDays(365) } });
|
||||
var invoiceIdIndex = report.GetIndex("InvoiceId");
|
||||
var oldPaymentsCount = report.Data.Count(d => d[invoiceIdIndex].Value<string>() == "Q7RqoHLngK9svM4MgRyi9y");
|
||||
Assert.Equal(8, oldPaymentsCount); // 10 payments, but 2 unaccounted
|
||||
|
||||
var addr = await tester.ExplorerNode.GetNewAddressAsync();
|
||||
// Two invoices get refunded
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
var inv = await client.CreateInvoice(acc.StoreId, new CreateInvoiceRequest() { Amount = 10m, Currency = "USD" });
|
||||
await acc.PayInvoice(inv.Id);
|
||||
await client.MarkInvoiceStatus(acc.StoreId, inv.Id, new MarkInvoiceStatusRequest() { Status = InvoiceStatus.Settled });
|
||||
var refund = await client.RefundInvoice(acc.StoreId, inv.Id, new RefundInvoiceRequest() { RefundVariant = RefundVariant.Fiat, PaymentMethod = "BTC" });
|
||||
|
||||
async Task AssertData(string currency, decimal awaiting, decimal limit, decimal completed, bool fullyPaid)
|
||||
{
|
||||
report = await GetReport(acc, new() { ViewName = "Refunds" });
|
||||
var currencyIndex = report.GetIndex("Currency");
|
||||
var awaitingIndex = report.GetIndex("Awaiting");
|
||||
var fullyPaidIndex = report.GetIndex("FullyPaid");
|
||||
var completedIndex = report.GetIndex("Completed");
|
||||
var limitIndex = report.GetIndex("Limit");
|
||||
var d = Assert.Single(report.Data.Where(d => d[report.GetIndex("InvoiceId")].Value<string>() == inv.Id));
|
||||
Assert.Equal(fullyPaid, (bool)d[fullyPaidIndex]);
|
||||
Assert.Equal(currency, d[currencyIndex].Value<string>());
|
||||
Assert.Equal(completed, (((JObject)d[completedIndex])["v"]).Value<decimal>());
|
||||
Assert.Equal(awaiting, (((JObject)d[awaitingIndex])["v"]).Value<decimal>());
|
||||
Assert.Equal(limit, (((JObject)d[limitIndex])["v"]).Value<decimal>());
|
||||
}
|
||||
|
||||
await AssertData("USD", awaiting: 0.0m, limit: 10.0m, completed: 0.0m, fullyPaid: false);
|
||||
var payout = await client.CreatePayout(refund.Id, new CreatePayoutRequest() { Destination = addr.ToString(), PaymentMethod = "BTC" });
|
||||
await AssertData("USD", awaiting: 10.0m, limit: 10.0m, completed: 0.0m, fullyPaid: false);
|
||||
await client.ApprovePayout(acc.StoreId, payout.Id, new ApprovePayoutRequest());
|
||||
await AssertData("USD", awaiting: 10.0m, limit: 10.0m, completed: 0.0m, fullyPaid: false);
|
||||
if (i == 0)
|
||||
{
|
||||
await client.MarkPayoutPaid(acc.StoreId, payout.Id);
|
||||
await AssertData("USD", awaiting: 0.0m, limit: 10.0m, completed: 10.0m, fullyPaid: true);
|
||||
}
|
||||
if (i == 1)
|
||||
{
|
||||
await client.CancelPayout(acc.StoreId, payout.Id);
|
||||
await AssertData("USD", awaiting: 0.0m, limit: 10.0m, completed: 0.0m, fullyPaid: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<StoreReportResponse> GetReport(TestAccount acc, StoreReportRequest req)
|
||||
|
@ -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.4
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
@ -163,7 +163,7 @@ services:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: btcpayserver/lightning:v23.08-dev
|
||||
image: btcpayserver/lightning:v24.02.2
|
||||
stop_signal: SIGKILL
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@ -171,6 +171,7 @@ services:
|
||||
LIGHTNINGD_CHAIN: "btc"
|
||||
LIGHTNINGD_NETWORK: "regtest"
|
||||
LIGHTNINGD_OPT: |
|
||||
developer
|
||||
bitcoin-datadir=/etc/bitcoin
|
||||
bitcoin-rpcconnect=bitcoind
|
||||
announce-addr=customer_lightningd:9735
|
||||
@ -190,13 +191,14 @@ services:
|
||||
- bitcoind
|
||||
|
||||
merchant_lightningd:
|
||||
image: btcpayserver/lightning:v23.08-dev
|
||||
image: btcpayserver/lightning:v24.02.2
|
||||
stop_signal: SIGKILL
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
LIGHTNINGD_CHAIN: "btc"
|
||||
LIGHTNINGD_NETWORK: "regtest"
|
||||
LIGHTNINGD_OPT: |
|
||||
developer
|
||||
bitcoin-datadir=/etc/bitcoin
|
||||
bitcoin-rpcconnect=bitcoind
|
||||
announce-addr=merchant_lightningd:9735
|
||||
@ -224,7 +226,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.17.2-beta
|
||||
image: btcpayserver/lnd:v0.18.0-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -259,7 +261,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.17.2-beta
|
||||
image: btcpayserver/lnd:v0.18.0-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.4
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
@ -149,7 +149,7 @@ services:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: btcpayserver/lightning:v23.08-dev
|
||||
image: btcpayserver/lightning:v24.02.2
|
||||
stop_signal: SIGKILL
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@ -157,6 +157,7 @@ services:
|
||||
LIGHTNINGD_CHAIN: "btc"
|
||||
LIGHTNINGD_NETWORK: "regtest"
|
||||
LIGHTNINGD_OPT: |
|
||||
developer
|
||||
bitcoin-datadir=/etc/bitcoin
|
||||
bitcoin-rpcconnect=bitcoind
|
||||
announce-addr=customer_lightningd:9735
|
||||
@ -176,13 +177,14 @@ services:
|
||||
- bitcoind
|
||||
|
||||
merchant_lightningd:
|
||||
image: btcpayserver/lightning:v23.08-dev
|
||||
image: btcpayserver/lightning:v24.02.2
|
||||
stop_signal: SIGKILL
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
LIGHTNINGD_CHAIN: "btc"
|
||||
LIGHTNINGD_NETWORK: "regtest"
|
||||
LIGHTNINGD_OPT: |
|
||||
developer
|
||||
bitcoin-datadir=/etc/bitcoin
|
||||
bitcoin-rpcconnect=bitcoind
|
||||
announce-addr=merchant_lightningd:9735
|
||||
@ -211,7 +213,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.17.2-beta
|
||||
image: btcpayserver/lnd:v0.18.0-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -248,7 +250,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.17.2-beta
|
||||
image: btcpayserver/lnd:v0.18.0-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
|
2
BTCPayServer.Tests/docker-customer-lncli.ps1
Normal file
2
BTCPayServer.Tests/docker-customer-lncli.ps1
Normal file
@ -0,0 +1,2 @@
|
||||
$container_id="$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=customer_lnd)"
|
||||
docker exec -ti $container_id lncli --no-macaroons --rpcserver localhost:10008 $args
|
2
BTCPayServer.Tests/docker-merchant-lncli.ps1
Normal file
2
BTCPayServer.Tests/docker-merchant-lncli.ps1
Normal file
@ -0,0 +1,2 @@
|
||||
$container_id="$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=merchant_lnd)"
|
||||
docker exec -ti $container_id lncli --no-macaroons --rpcserver localhost:10008 $args
|
@ -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.18" />
|
||||
<PackageReference Include="BTCPayServer.NTag424" Version="1.0.23" />
|
||||
<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.2" />
|
||||
<PackageReference Include="CsvHelper" Version="15.0.5" />
|
||||
<PackageReference Include="Dapper" Version="2.1.24" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.6.0" />
|
||||
<PackageReference Include="CsvHelper" Version="32.0.3" />
|
||||
<PackageReference Include="Dapper" Version="2.1.35" />
|
||||
<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.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.5" />
|
||||
</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
|
||||
{
|
||||
|
@ -26,16 +26,16 @@
|
||||
{
|
||||
@if (Model.Store != null)
|
||||
{
|
||||
<div class="accordion-item" permission="@Policies.CanModifyStoreSettings">
|
||||
<div class="accordion-item" permission="@Policies.CanViewStoreSettings">
|
||||
<div class="accordion-body">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<li class="nav-item" permission="@Policies.CanModifyStoreSettings">
|
||||
<a asp-area="" asp-controller="UIStores" asp-action="Dashboard" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActivePage(StoreNavPages.Dashboard)" id="StoreNav-Dashboard">
|
||||
<vc:icon symbol="home"/>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<li class="nav-item" permission="@Policies.CanViewStoreSettings">
|
||||
<a asp-area="" asp-controller="UIStores" asp-action="GeneralSettings" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActivePage(new [] {StoreNavPages.Rates, StoreNavPages.CheckoutAppearance, StoreNavPages.General, StoreNavPages.Tokens, StoreNavPages.Users, StoreNavPages.Webhooks, StoreNavPages.PayoutProcessors, StoreNavPages.Emails})" id="StoreNav-StoreSettings">
|
||||
<vc:icon symbol="settings"/>
|
||||
<span>Settings</span>
|
||||
@ -115,7 +115,6 @@
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@ -252,7 +251,7 @@
|
||||
{
|
||||
<ul id="mainNavSettings" class="navbar-nav border-top p-3 px-lg-4">
|
||||
<li class="nav-item" permission="@Policies.CanModifyServerSettings">
|
||||
<a asp-area="" asp-controller="UIServer" asp-action="ListUsers" class="nav-link @ViewData.IsActivePage(ServerNavPages.Users) @ViewData.IsActivePage(ServerNavPages.Emails) @ViewData.IsActivePage(ServerNavPages.Policies) @ViewData.IsActivePage(ServerNavPages.Services) @ViewData.IsActivePage(ServerNavPages.Theme) @ViewData.IsActivePage(ServerNavPages.Maintenance) @ViewData.IsActivePage(ServerNavPages.Logs) @ViewData.IsActivePage(ServerNavPages.Files)" id="Nav-ServerSettings">
|
||||
<a asp-area="" asp-controller="UIServer" asp-action="ListUsers" class="nav-link @ViewData.IsActivePage(ServerNavPages.Users) @ViewData.IsActivePage(ServerNavPages.Emails) @ViewData.IsActivePage(ServerNavPages.Policies) @ViewData.IsActivePage(ServerNavPages.Services) @ViewData.IsActivePage(ServerNavPages.Branding) @ViewData.IsActivePage(ServerNavPages.Maintenance) @ViewData.IsActivePage(ServerNavPages.Logs) @ViewData.IsActivePage(ServerNavPages.Files)" id="Nav-ServerSettings">
|
||||
<vc:icon symbol="server-settings"/>
|
||||
<span>Server Settings</span>
|
||||
</a>
|
||||
@ -297,6 +296,15 @@
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
@if (!string.IsNullOrWhiteSpace(Model.ContactUrl))
|
||||
{
|
||||
<li class="nav-item">
|
||||
<a href="@Model.ContactUrl" class="nav-link" id="Nav-ContactUs">
|
||||
<vc:icon symbol="contact"/>
|
||||
<span>Contact Us</span>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</nav>
|
||||
|
@ -29,6 +29,8 @@ namespace BTCPayServer.Components.MainNav
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary;
|
||||
private readonly CustodianAccountRepository _custodianAccountRepository;
|
||||
private readonly SettingsRepository _settingsRepository;
|
||||
public PoliciesSettings PoliciesSettings { get; }
|
||||
|
||||
public MainNav(
|
||||
AppService appService,
|
||||
@ -38,6 +40,7 @@ namespace BTCPayServer.Components.MainNav
|
||||
UserManager<ApplicationUser> userManager,
|
||||
PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
|
||||
CustodianAccountRepository custodianAccountRepository,
|
||||
SettingsRepository settingsRepository,
|
||||
PoliciesSettings policiesSettings)
|
||||
{
|
||||
_storeRepo = storeRepo;
|
||||
@ -47,13 +50,19 @@ namespace BTCPayServer.Components.MainNav
|
||||
_storesController = storesController;
|
||||
_paymentMethodHandlerDictionary = paymentMethodHandlerDictionary;
|
||||
_custodianAccountRepository = custodianAccountRepository;
|
||||
_settingsRepository = settingsRepository;
|
||||
PoliciesSettings = policiesSettings;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync()
|
||||
{
|
||||
var store = ViewContext.HttpContext.GetStoreData();
|
||||
var vm = new MainNavViewModel { Store = store };
|
||||
var serverSettings = await _settingsRepository.GetSettingAsync<ServerSettings>() ?? new ServerSettings();
|
||||
var vm = new MainNavViewModel
|
||||
{
|
||||
Store = store,
|
||||
ContactUrl = serverSettings.ContactUrl
|
||||
};
|
||||
#if ALTCOINS
|
||||
vm.AltcoinsBuild = true;
|
||||
#endif
|
||||
@ -92,7 +101,5 @@ namespace BTCPayServer.Components.MainNav
|
||||
}
|
||||
|
||||
private string UserId => _userManager.GetUserId(HttpContext.User);
|
||||
|
||||
public PoliciesSettings PoliciesSettings { get; }
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ namespace BTCPayServer.Components.MainNav
|
||||
public CustodianAccountData[] CustodianAccounts { get; set; }
|
||||
public bool AltcoinsBuild { get; set; }
|
||||
public int ArchivedAppsCount { get; set; }
|
||||
public string ContactUrl { get; set; }
|
||||
}
|
||||
|
||||
public class StoreApp
|
||||
|
@ -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;
|
||||
|
@ -34,34 +34,36 @@
|
||||
}
|
||||
else if (Model.Invoices.Any())
|
||||
{
|
||||
<table class="table table-hover mt-3 mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-125px">Date</th>
|
||||
<th class="text-nowrap">Invoice Id</th>
|
||||
<th>Status</th>
|
||||
<th class="text-end">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var invoice in Model.Invoices)
|
||||
{
|
||||
<div class="table-responsive mt-3 mb-0">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>@invoice.Date.ToTimeAgo()</td>
|
||||
<td>
|
||||
<a asp-controller="UIInvoice" asp-action="Invoice" asp-route-invoiceId="@invoice.InvoiceId" class="text-break">@invoice.InvoiceId</a>
|
||||
</td>
|
||||
<td>
|
||||
<vc:invoice-status state="invoice.Status" payments="invoice.Details.Payments" invoice-id="@invoice.InvoiceId"
|
||||
is-archived="invoice.Details.Archived" has-refund="invoice.HasRefund" />
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span data-sensitive>@DisplayFormatter.Currency(invoice.Amount, invoice.Currency)</span>
|
||||
</td>
|
||||
<th class="w-125px">Date</th>
|
||||
<th class="text-nowrap">Invoice Id</th>
|
||||
<th>Status</th>
|
||||
<th class="text-end">Amount</th>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var invoice in Model.Invoices)
|
||||
{
|
||||
<tr>
|
||||
<td>@invoice.Date.ToTimeAgo()</td>
|
||||
<td>
|
||||
<a asp-controller="UIInvoice" asp-action="Invoice" asp-route-invoiceId="@invoice.InvoiceId" class="text-break">@invoice.InvoiceId</a>
|
||||
</td>
|
||||
<td>
|
||||
<vc:invoice-status state="invoice.Status" payments="invoice.Details.Payments" invoice-id="@invoice.InvoiceId"
|
||||
is-archived="invoice.Details.Archived" has-refund="invoice.HasRefund" />
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span data-sensitive>@DisplayFormatter.Currency(invoice.Amount, invoice.Currency)</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -31,61 +31,63 @@
|
||||
}
|
||||
else if (Model.Transactions.Any())
|
||||
{
|
||||
<table class="table table-hover mt-3 mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-125px">Date</th>
|
||||
<th>Transaction</th>
|
||||
<th>Labels</th>
|
||||
<th class="text-end">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var tx in Model.Transactions)
|
||||
{
|
||||
<div class="table-responsive mt-3 mb-0">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>@tx.Timestamp.ToTimeAgo()</td>
|
||||
<td>
|
||||
<vc:truncate-center text="@tx.Id" link="@tx.Link" classes="truncate-center-id" />
|
||||
</td>
|
||||
<td>
|
||||
@if (tx.Labels.Any())
|
||||
{
|
||||
<div class="d-flex flex-wrap gap-2 align-items-center">
|
||||
@foreach (var label in tx.Labels)
|
||||
{
|
||||
<div class="transaction-label" style="--label-bg:@label.Color;--label-fg:@label.TextColor">
|
||||
<span>@label.Text</span>
|
||||
@if (!string.IsNullOrEmpty(label.Link))
|
||||
{
|
||||
<a class="transaction-label-info transaction-details-icon" href="@label.Link"
|
||||
target="_blank" rel="noreferrer noopener" title="@label.Tooltip" data-bs-html="true"
|
||||
data-bs-toggle="tooltip" data-bs-custom-class="transaction-label-tooltip">
|
||||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
</td>
|
||||
@if (tx.Positive)
|
||||
{
|
||||
<td class="text-end text-success">
|
||||
<span data-sensitive>@DisplayFormatter.Currency(tx.Balance, tx.Currency)</span>
|
||||
</td>
|
||||
}
|
||||
else
|
||||
{
|
||||
<td class="text-end text-danger">
|
||||
<span data-sensitive>@DisplayFormatter.Currency(tx.Balance, tx.Currency)</span>
|
||||
</td>
|
||||
}
|
||||
<th class="w-125px">Date</th>
|
||||
<th>Transaction</th>
|
||||
<th>Labels</th>
|
||||
<th class="text-end">Amount</th>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var tx in Model.Transactions)
|
||||
{
|
||||
<tr>
|
||||
<td>@tx.Timestamp.ToTimeAgo()</td>
|
||||
<td>
|
||||
<vc:truncate-center text="@tx.Id" link="@tx.Link" classes="truncate-center-id" />
|
||||
</td>
|
||||
<td>
|
||||
@if (tx.Labels.Any())
|
||||
{
|
||||
<div class="d-flex flex-wrap gap-2 align-items-center">
|
||||
@foreach (var label in tx.Labels)
|
||||
{
|
||||
<div class="transaction-label" style="--label-bg:@label.Color;--label-fg:@label.TextColor">
|
||||
<span>@label.Text</span>
|
||||
@if (!string.IsNullOrEmpty(label.Link))
|
||||
{
|
||||
<a class="transaction-label-info transaction-details-icon" href="@label.Link"
|
||||
target="_blank" rel="noreferrer noopener" title="@label.Tooltip" data-bs-html="true"
|
||||
data-bs-toggle="tooltip" data-bs-custom-class="transaction-label-tooltip">
|
||||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
</td>
|
||||
@if (tx.Positive)
|
||||
{
|
||||
<td class="text-end text-success">
|
||||
<span data-sensitive>@DisplayFormatter.Currency(tx.Balance, tx.Currency)</span>
|
||||
</td>
|
||||
}
|
||||
else
|
||||
{
|
||||
<td class="text-end text-danger">
|
||||
<span data-sensitive>@DisplayFormatter.Currency(tx.Balance, tx.Currency)</span>
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -2,6 +2,7 @@
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Components.MainLogo
|
||||
@using BTCPayServer.Services
|
||||
@using BTCPayServer.Views.Server
|
||||
@using BTCPayServer.Views.Stores
|
||||
@inject BTCPayServerEnvironment Env
|
||||
@inject IFileService FileService
|
||||
@ -53,19 +54,23 @@ else
|
||||
@foreach (var option in Model.Options)
|
||||
{
|
||||
<li>
|
||||
<a asp-controller="UIStores" asp-action="Index" asp-route-storeId="@option.Value" class="dropdown-item@(option.Selected ? " active" : "")" id="StoreSelectorMenuItem-@option.Value">@StoreName(option.Text)</a>
|
||||
<a asp-controller="UIStores" asp-action="Index" asp-route-storeId="@option.Value" class="dropdown-item@(option.Selected && ViewData.IsActivePage(ServerNavPages.Stores) != "active" ? " active" : "")" id="StoreSelectorMenuItem-@option.Value">@StoreName(option.Text)</a>
|
||||
</li>
|
||||
}
|
||||
@if (Model.Options.Any())
|
||||
{
|
||||
<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>
|
||||
<li><a asp-controller="UIUserStores" asp-action="ListStores" asp-route-archived="true" class="dropdown-item @ViewData.IsActivePage(StoreNavPages.Index)" id="StoreSelectorArchived">@Model.ArchivedCount Archived Store@(Model.ArchivedCount == 1 ? "" : "s")</a></li>
|
||||
}
|
||||
@*
|
||||
<li permission="@Policies.CanModifyServerSettings"><hr class="dropdown-divider"></li>
|
||||
<li permission="@Policies.CanModifyServerSettings"><a asp-controller="UIServer" asp-action="ListStores" class="dropdown-item @ViewData.IsActivePage(ServerNavPages.Stores)" id="StoreSelectorAdminStores">Admin Store Overview</a></li>
|
||||
*@
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -41,14 +41,13 @@ namespace BTCPayServer.Components.StoreSelector
|
||||
.FirstOrDefault()?
|
||||
.Network.CryptoCode;
|
||||
var walletId = cryptoCode != null ? new WalletId(store.Id, cryptoCode) : null;
|
||||
var role = store.GetStoreRoleOfUser(userId);
|
||||
return new StoreSelectorOption
|
||||
{
|
||||
Text = store.StoreName,
|
||||
Value = store.Id,
|
||||
Selected = store.Id == currentStore?.Id,
|
||||
WalletId = walletId,
|
||||
Store = store,
|
||||
Store = store
|
||||
};
|
||||
})
|
||||
.OrderBy(s => s.Text)
|
||||
|
@ -64,13 +64,14 @@
|
||||
|
||||
const id = `StoreWalletBalance-${storeId}`;
|
||||
const baseUrl = @Safe.Json(Url.Action("WalletHistogram", "UIWallets", new { walletId = Model.WalletId, type = WalletHistogramType.Week }));
|
||||
const valueTransform = value => rate
|
||||
? DashboardUtils.displayDefaultCurrency(value, rate, defaultCurrency, divisibility).toString()
|
||||
: value
|
||||
const chartOpts = {
|
||||
fullWidth: true,
|
||||
showArea: true,
|
||||
axisY: {
|
||||
labelInterpolationFnc: value => rate
|
||||
? DashboardUtils.displayDefaultCurrency(value, rate, defaultCurrency, divisibility).toString()
|
||||
: value
|
||||
labelInterpolationFnc: valueTransform
|
||||
}
|
||||
};
|
||||
|
||||
@ -80,16 +81,22 @@
|
||||
document.querySelectorAll(`#${id} .currency`).forEach(c => c.innerText = currency)
|
||||
document.querySelectorAll(`#${id} [data-balance]`).forEach(c => {
|
||||
const value = Number.parseFloat(c.dataset.balance);
|
||||
c.innerText = rate
|
||||
? DashboardUtils.displayDefaultCurrency(value, rate, currency, divisibility)
|
||||
: value
|
||||
c.innerText = valueTransform(value)
|
||||
});
|
||||
if (!series) return;
|
||||
|
||||
const min = Math.min(...series);
|
||||
const max = Math.max(...series);
|
||||
const low = Math.max(min - ((max - min) / 5), 0);
|
||||
const renderOpts = Object.assign({}, chartOpts, { low });
|
||||
const tooltip = Chartist.plugins.tooltip2({
|
||||
template: '{{value}}',
|
||||
offset: {
|
||||
x: 0,
|
||||
y: -16
|
||||
},
|
||||
valueTransformFunction: valueTransform
|
||||
})
|
||||
const renderOpts = Object.assign({}, chartOpts, { low, plugins: [tooltip] });
|
||||
const chart = new Chartist.Line(`#${id} .ct-chart`, {
|
||||
labels,
|
||||
series: [series]
|
||||
|
@ -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)." });
|
||||
|
@ -238,7 +238,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
return new CrowdfundSettings
|
||||
{
|
||||
Title = request.Title?.Trim(),
|
||||
Title = request.Title?.Trim() ?? request.AppName,
|
||||
Enabled = request.Enabled ?? true,
|
||||
EnforceTargetAmount = request.EnforceTargetAmount ?? false,
|
||||
StartDate = request.StartDate?.UtcDateTime,
|
||||
@ -272,8 +272,9 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
return new PointOfSaleSettings
|
||||
{
|
||||
Title = request.Title,
|
||||
Title = request.Title ?? request.AppName,
|
||||
DefaultView = (PosViewType)request.DefaultView,
|
||||
ShowItems = request.ShowItems,
|
||||
ShowCustomAmount = request.ShowCustomAmount,
|
||||
ShowDiscount = request.ShowDiscount,
|
||||
ShowSearch = request.ShowSearch,
|
||||
@ -335,6 +336,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
Created = appData.Created,
|
||||
Title = settings.Title,
|
||||
DefaultView = settings.DefaultView.ToString(),
|
||||
ShowItems = settings.ShowItems,
|
||||
ShowCustomAmount = settings.ShowCustomAmount,
|
||||
ShowDiscount = settings.ShowDiscount,
|
||||
ShowSearch = settings.ShowSearch,
|
||||
|
@ -326,7 +326,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
if (result == null)
|
||||
{
|
||||
return this.CreateAPIError(404, "trade-not-found",
|
||||
$"Could not find the the trade with ID {tradeId} on {custodianAccount.Name}");
|
||||
$"Could not find the trade with ID {tradeId} on {custodianAccount.Name}");
|
||||
}
|
||||
return Ok(ToModel(result, accountId, custodianAccount.CustodianCode));
|
||||
}
|
||||
|
@ -168,6 +168,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
Status = Client.Models.PaymentRequestData.PaymentRequestStatus.Pending,
|
||||
Created = DateTimeOffset.UtcNow
|
||||
};
|
||||
request.FormResponse = null;
|
||||
request.StoreId = storeId;
|
||||
pr.SetBlob(request);
|
||||
pr = await _paymentRequestRepository.CreateOrUpdatePaymentRequest(pr);
|
||||
return Ok(FromModel(pr));
|
||||
@ -196,6 +198,9 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
}
|
||||
|
||||
var updatedPr = pr.First();
|
||||
var blob = updatedPr.GetBlob();
|
||||
request.FormResponse = blob.FormResponse;
|
||||
request.StoreId = storeId;
|
||||
updatedPr.SetBlob(request);
|
||||
|
||||
return Ok(FromModel(await _paymentRequestRepository.CreateOrUpdatePaymentRequest(updatedPr)));
|
||||
|
@ -1,6 +1,9 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO.IsolatedStorage;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
@ -21,7 +24,9 @@ using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MarkPayoutRequest = BTCPayServer.HostedServices.MarkPayoutRequest;
|
||||
|
||||
@ -152,20 +157,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
var ppId = await _pullPaymentService.CreatePullPayment(new CreatePullPayment()
|
||||
{
|
||||
StartsAt = request.StartsAt,
|
||||
ExpiresAt = request.ExpiresAt,
|
||||
Period = request.Period,
|
||||
BOLT11Expiration = request.BOLT11Expiration,
|
||||
Name = request.Name,
|
||||
Description = request.Description,
|
||||
Amount = request.Amount,
|
||||
Currency = request.Currency,
|
||||
StoreId = storeId,
|
||||
PaymentMethodIds = paymentMethods,
|
||||
AutoApproveClaims = request.AutoApproveClaims
|
||||
});
|
||||
var ppId = await _pullPaymentService.CreatePullPayment(storeId, request);
|
||||
var pp = await _pullPaymentService.GetPullPayment(ppId, false);
|
||||
return this.Ok(CreatePullPaymentData(pp));
|
||||
}
|
||||
@ -199,13 +191,37 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[HttpPost]
|
||||
[Route("~/api/v1/pull-payments/{pullPaymentId}/boltcards")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> RegisterBoltcard(string pullPaymentId, RegisterBoltcardRequest request)
|
||||
public async Task<IActionResult> RegisterBoltcard(string pullPaymentId, RegisterBoltcardRequest request, string? onExisting = null)
|
||||
{
|
||||
if (pullPaymentId is null)
|
||||
return PullPaymentNotFound();
|
||||
var pp = await _pullPaymentService.GetPullPayment(pullPaymentId, false);
|
||||
if (pp is null)
|
||||
return PullPaymentNotFound();
|
||||
var issuerKey = await _settingsRepository.GetIssuerKey(_env);
|
||||
|
||||
// LNURLW is used by deeplinks
|
||||
if (request?.LNURLW is not null)
|
||||
{
|
||||
if (request.UID is not null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.LNURLW), "You should pass either LNURLW or UID but not both");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
var p = ExtractP(request.LNURLW);
|
||||
if (p is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.LNURLW), "The LNURLW should contains a 'p=' parameter");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
if (issuerKey.TryDecrypt(p) is not BoltcardPICCData picc)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.LNURLW), "The LNURLW 'p=' parameter cannot be decrypted");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
request.UID = picc.Uid;
|
||||
}
|
||||
|
||||
if (request?.UID is null || request.UID.Length != 7)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.UID), "The UID is required and should be 7 bytes");
|
||||
@ -216,15 +232,22 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return this.CreateAPIError(400, "lnurl-not-supported", "This pull payment currency should be BTC or SATS and accept lightning");
|
||||
}
|
||||
|
||||
var issuerKey = await _settingsRepository.GetIssuerKey(_env);
|
||||
// Passing onExisting as a query parameter is used by deeplink
|
||||
request.OnExisting = onExisting switch
|
||||
{
|
||||
nameof(OnExistingBehavior.UpdateVersion) => OnExistingBehavior.UpdateVersion,
|
||||
nameof(OnExistingBehavior.KeepVersion) => OnExistingBehavior.KeepVersion,
|
||||
_ => request.OnExisting
|
||||
};
|
||||
|
||||
var version = await _dbContextFactory.LinkBoltcardToPullPayment(pullPaymentId, issuerKey, request.UID, request.OnExisting);
|
||||
var keys = issuerKey.CreateCardKey(request.UID, version).DeriveBoltcardKeys(issuerKey);
|
||||
var keys = issuerKey.CreatePullPaymentCardKey(request.UID, version, pullPaymentId).DeriveBoltcardKeys(issuerKey);
|
||||
|
||||
var boltcardUrl = Url.Action(nameof(UIBoltcardController.GetWithdrawRequest), "UIBoltcard");
|
||||
boltcardUrl = Request.GetAbsoluteUri(boltcardUrl);
|
||||
boltcardUrl = Regex.Replace(boltcardUrl, "^https?://", "lnurlw://");
|
||||
|
||||
return Ok(new RegisterBoltcardResponse()
|
||||
var resp = new RegisterBoltcardResponse()
|
||||
{
|
||||
LNURLW = boltcardUrl,
|
||||
Version = version,
|
||||
@ -233,7 +256,22 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
K2 = Encoders.Hex.EncodeData(keys.AuthenticationKey.ToBytes()).ToUpperInvariant(),
|
||||
K3 = Encoders.Hex.EncodeData(keys.K3.ToBytes()).ToUpperInvariant(),
|
||||
K4 = Encoders.Hex.EncodeData(keys.K4.ToBytes()).ToUpperInvariant(),
|
||||
});
|
||||
};
|
||||
return Ok(resp);
|
||||
}
|
||||
|
||||
private string? ExtractP(string? url)
|
||||
{
|
||||
if (url is null || !Uri.TryCreate(url, UriKind.Absolute, out var uri))
|
||||
return null;
|
||||
int num = uri.AbsoluteUri.IndexOf('?');
|
||||
if (num == -1)
|
||||
return null;
|
||||
string input = uri.AbsoluteUri.Substring(num);
|
||||
Match match = Regex.Match(input, "p=([a-f0-9A-F]{32})");
|
||||
if (!match.Success)
|
||||
return null;
|
||||
return match.Groups[1].Value;
|
||||
}
|
||||
|
||||
[HttpGet("~/api/v1/pull-payments/{pullPaymentId}")]
|
||||
@ -385,7 +423,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
Destination = destination.destination,
|
||||
PullPaymentId = pullPaymentId,
|
||||
Value = request.Amount,
|
||||
PaymentMethodId = paymentMethodId
|
||||
PaymentMethodId = paymentMethodId,
|
||||
StoreId = pp.StoreId
|
||||
});
|
||||
|
||||
return HandleClaimResult(result);
|
||||
|
@ -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));
|
||||
@ -518,10 +518,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
Outputs = outputs,
|
||||
AlwaysIncludeNonWitnessUTXO = true,
|
||||
InputSelection = request.SelectedInputs?.Any() is true,
|
||||
AllowFeeBump =
|
||||
!request.RBF.HasValue ? WalletSendModel.ThreeStateBool.Maybe :
|
||||
request.RBF.Value ? WalletSendModel.ThreeStateBool.Yes :
|
||||
WalletSendModel.ThreeStateBool.No,
|
||||
FeeSatoshiPerByte = request.FeeRate?.SatoshiPerByte,
|
||||
NoChange = request.NoChange
|
||||
},
|
||||
|
@ -24,7 +24,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
_storeRepository = storeRepository;
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/roles")]
|
||||
public async Task<IActionResult> GetStoreRoles(string storeId)
|
||||
{
|
||||
@ -34,7 +34,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
: Ok(FromModel(await _storeRepository.GetStoreRoles(storeId, false, false)));
|
||||
}
|
||||
|
||||
|
||||
private List<RoleData> FromModel(StoreRepository.StoreRole[] data)
|
||||
{
|
||||
return data.Select(r => new RoleData() {Role = r.Role, Id = r.Id, Permissions = r.Permissions, IsServerRole = r.IsServerRole}).ToList();
|
||||
|
@ -27,14 +27,15 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
_storeRepository = storeRepository;
|
||||
_userManager = userManager;
|
||||
}
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/users")]
|
||||
public IActionResult GetStoreUsers()
|
||||
{
|
||||
|
||||
var store = HttpContext.GetStoreData();
|
||||
return store == null ? StoreNotFound() : Ok(FromModel(store));
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpDelete("~/api/v1/stores/{storeId}/users/{idOrEmail}")]
|
||||
public async Task<IActionResult> RemoveStoreUser(string storeId, string idOrEmail)
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -92,6 +92,26 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
$"{(request.Locked ? "Locking" : "Unlocking")} user failed");
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/users/{idOrEmail}/approve")]
|
||||
public async Task<IActionResult> ApproveUser(string idOrEmail, ApproveUserRequest request)
|
||||
{
|
||||
var user = await _userManager.FindByIdOrEmail(idOrEmail);
|
||||
if (user is null)
|
||||
{
|
||||
return this.UserNotFound();
|
||||
}
|
||||
|
||||
var success = false;
|
||||
if (user.RequiresApproval)
|
||||
{
|
||||
success = await _userService.SetUserApproval(user.Id, request.Approved, Request.GetAbsoluteRootUri());
|
||||
}
|
||||
|
||||
return success ? Ok() : this.CreateAPIError("invalid-state",
|
||||
$"{(request.Approved ? "Approving" : "Unapproving")} user failed");
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanViewUsers, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/users/")]
|
||||
public async Task<ActionResult<ApplicationUserData[]>> GetUsers()
|
||||
@ -163,7 +183,9 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
UserName = request.Email,
|
||||
Email = request.Email,
|
||||
RequiresEmailConfirmation = policies.RequiresConfirmedEmail,
|
||||
RequiresApproval = policies.RequiresUserApproval,
|
||||
Created = DateTimeOffset.UtcNow,
|
||||
Approved = isAdmin // auto-approve first admin and users created by an admin
|
||||
};
|
||||
var passwordValidation = await this._passwordValidator.ValidateAsync(_userManager, user, request.Password);
|
||||
if (!passwordValidation.Succeeded)
|
||||
@ -192,7 +214,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
if (request.IsAdministrator is true)
|
||||
var isNewAdmin = request.IsAdministrator is true;
|
||||
if (isNewAdmin)
|
||||
{
|
||||
if (!anyAdmin)
|
||||
{
|
||||
@ -211,7 +234,21 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
await _settingsRepository.FirstAdminRegistered(policies, _options.UpdateUrl != null, _options.DisableRegistration, Logs);
|
||||
}
|
||||
}
|
||||
_eventAggregator.Publish(new UserRegisteredEvent() { RequestUri = Request.GetAbsoluteRootUri(), User = user, Admin = request.IsAdministrator is true });
|
||||
|
||||
var currentUser = await _userManager.GetUserAsync(User);
|
||||
var userEvent = new UserRegisteredEvent
|
||||
{
|
||||
RequestUri = Request.GetAbsoluteRootUri(),
|
||||
Admin = isNewAdmin,
|
||||
User = user
|
||||
};
|
||||
if (currentUser is not null)
|
||||
{
|
||||
userEvent.Kind = UserRegisteredEventKind.Invite;
|
||||
userEvent.InvitedByUser = currentUser;
|
||||
};
|
||||
_eventAggregator.Publish(userEvent);
|
||||
|
||||
var model = await FromModel(user);
|
||||
return CreatedAtAction(string.Empty, model);
|
||||
}
|
||||
|
@ -1084,6 +1084,13 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
new LockUserRequest { Locked = disabled }));
|
||||
}
|
||||
|
||||
public override async Task<bool> ApproveUser(string idOrEmail, bool approved, CancellationToken token = default)
|
||||
{
|
||||
return GetFromActionResult<bool>(
|
||||
await GetController<GreenfieldUsersController>().ApproveUser(idOrEmail,
|
||||
new ApproveUserRequest { Approved = approved }));
|
||||
}
|
||||
|
||||
public override async Task<OnChainWalletTransactionData> PatchOnChainWalletTransaction(string storeId,
|
||||
string cryptoCode, string transactionId,
|
||||
PatchOnChainTransactionRequest request, bool force = false, CancellationToken token = default)
|
||||
|
@ -48,16 +48,11 @@ public class LightningAddressService
|
||||
{
|
||||
return await _memoryCache.GetOrCreateAsync(GetKey(username), async entry =>
|
||||
{
|
||||
var result = await Get(new LightningAddressQuery { Usernames = new[] { username } });
|
||||
var result = await Get(new LightningAddressQuery { Usernames = new[] { NormalizeUsername(username) } });
|
||||
return result.FirstOrDefault();
|
||||
});
|
||||
}
|
||||
|
||||
private string NormalizeUsername(string username)
|
||||
{
|
||||
return username.ToLowerInvariant();
|
||||
}
|
||||
|
||||
public async Task<bool> Set(LightningAddressData data)
|
||||
{
|
||||
data.Username = NormalizeUsername(data.Username);
|
||||
@ -115,8 +110,12 @@ public class LightningAddressService
|
||||
await context.AddAsync(data);
|
||||
}
|
||||
|
||||
public static string NormalizeUsername(string username)
|
||||
{
|
||||
return username.ToLowerInvariant();
|
||||
}
|
||||
|
||||
private string GetKey(string username)
|
||||
private static string GetKey(string username)
|
||||
{
|
||||
username = NormalizeUsername(username);
|
||||
return $"{nameof(LightningAddressService)}_{username}";
|
||||
|
@ -2,7 +2,6 @@ using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
@ -109,6 +108,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (User.Identity.IsAuthenticated && string.IsNullOrEmpty(returnUrl))
|
||||
return RedirectToLocal();
|
||||
|
||||
// Clear the existing external cookie to ensure a clean login process
|
||||
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
|
||||
|
||||
@ -118,15 +118,13 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
return View(nameof(Login), new LoginViewModel() { Email = email });
|
||||
return View(nameof(Login), new LoginViewModel { Email = email });
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("/login/code")]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
[RateLimitsFilter(ZoneLimits.Login, Scope = RateLimitsScope.RemoteAddress)]
|
||||
|
||||
public async Task<IActionResult> LoginWithCode(string loginCode, string returnUrl = null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(loginCode))
|
||||
@ -134,17 +132,26 @@ namespace BTCPayServer.Controllers
|
||||
var userId = _userLoginCodeService.Verify(loginCode);
|
||||
if (userId is null)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty,
|
||||
"Login code was invalid");
|
||||
return await Login(returnUrl, null);
|
||||
TempData[WellKnownTempData.ErrorMessage] = "Login code was invalid";
|
||||
return await Login(returnUrl);
|
||||
}
|
||||
var user = await _userManager.FindByIdAsync(userId);
|
||||
|
||||
_logger.LogInformation("User with ID {UserId} logged in with a login code.", user.Id);
|
||||
var user = await _userManager.FindByIdAsync(userId);
|
||||
if (!UserService.TryCanLogin(user, out var message))
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = user == null ? StatusMessageModel.StatusSeverity.Error : StatusMessageModel.StatusSeverity.Warning,
|
||||
Message = message
|
||||
});
|
||||
return await Login(returnUrl);
|
||||
}
|
||||
|
||||
_logger.LogInformation("User {Email} logged in with a login code", user!.Email);
|
||||
await _signInManager.SignInAsync(user, false, "LoginCode");
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
return await Login(returnUrl, null);
|
||||
return await Login(returnUrl);
|
||||
}
|
||||
|
||||
[HttpPost("/login")]
|
||||
@ -161,24 +168,20 @@ namespace BTCPayServer.Controllers
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
// Require the user to have a confirmed email before they can log on.
|
||||
// Require the user to pass basic checks (approval, confirmed email, not disabled) before they can log on
|
||||
var user = await _userManager.FindByEmailAsync(model.Email);
|
||||
if (user != null)
|
||||
const string errorMessage = "Invalid login attempt.";
|
||||
if (!UserService.TryCanLogin(user, out var message))
|
||||
{
|
||||
if (user.RequiresEmailConfirmation && !await _userManager.IsEmailConfirmedAsync(user))
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
ModelState.AddModelError(string.Empty,
|
||||
"You must have a confirmed email to log in.");
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
|
||||
Severity = user == null ? StatusMessageModel.StatusSeverity.Error : StatusMessageModel.StatusSeverity.Warning,
|
||||
Message = message
|
||||
});
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var fido2Devices = await _fido2Service.HasCredentials(user.Id);
|
||||
var fido2Devices = await _fido2Service.HasCredentials(user!.Id);
|
||||
var lnurlAuthCredentials = await _lnurlAuthService.HasCredentials(user.Id);
|
||||
if (!await _userManager.IsLockedOutAsync(user) && (fido2Devices || lnurlAuthCredentials))
|
||||
{
|
||||
@ -196,33 +199,30 @@ namespace BTCPayServer.Controllers
|
||||
};
|
||||
}
|
||||
|
||||
return View("SecondaryLogin", new SecondaryLoginViewModel()
|
||||
return View("SecondaryLogin", new SecondaryLoginViewModel
|
||||
{
|
||||
LoginWith2FaViewModel = twoFModel,
|
||||
LoginWithFido2ViewModel = fido2Devices ? await BuildFido2ViewModel(model.RememberMe, user) : null,
|
||||
LoginWithLNURLAuthViewModel = lnurlAuthCredentials ? await BuildLNURLAuthViewModel(model.RememberMe, user) : null,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
var incrementAccessFailedResult = await _userManager.AccessFailedAsync(user);
|
||||
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
|
||||
return View(model);
|
||||
|
||||
}
|
||||
await _userManager.AccessFailedAsync(user);
|
||||
ModelState.AddModelError(string.Empty, errorMessage!);
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: true);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation($"User '{user.Id}' logged in.");
|
||||
_logger.LogInformation("User {Email} logged in", user.Email);
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
if (result.RequiresTwoFactor)
|
||||
{
|
||||
return View("SecondaryLogin", new SecondaryLoginViewModel()
|
||||
return View("SecondaryLogin", new SecondaryLoginViewModel
|
||||
{
|
||||
LoginWith2FaViewModel = new LoginWith2faViewModel()
|
||||
LoginWith2FaViewModel = new LoginWith2faViewModel
|
||||
{
|
||||
RememberMe = model.RememberMe
|
||||
}
|
||||
@ -230,14 +230,12 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
if (result.IsLockedOut)
|
||||
{
|
||||
_logger.LogWarning($"User '{user.Id}' account locked out.");
|
||||
_logger.LogWarning("User {Email} tried to log in, but is locked out", user.Email);
|
||||
return RedirectToAction(nameof(Lockout), new { user.LockoutEnd });
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
|
||||
return View(model);
|
||||
}
|
||||
|
||||
ModelState.AddModelError(string.Empty, errorMessage);
|
||||
return View(model);
|
||||
}
|
||||
|
||||
// If we got this far, something failed, redisplay form
|
||||
@ -253,7 +251,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return new LoginWithFido2ViewModel()
|
||||
return new LoginWithFido2ViewModel
|
||||
{
|
||||
Data = r,
|
||||
UserId = user.Id,
|
||||
@ -263,7 +261,6 @@ namespace BTCPayServer.Controllers
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
private async Task<LoginWithLNURLAuthViewModel> BuildLNURLAuthViewModel(bool rememberMe, ApplicationUser user)
|
||||
{
|
||||
if (_btcPayServerEnvironment.IsSecure(HttpContext))
|
||||
@ -273,15 +270,14 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return new LoginWithLNURLAuthViewModel()
|
||||
return new LoginWithLNURLAuthViewModel
|
||||
{
|
||||
|
||||
RememberMe = rememberMe,
|
||||
UserId = user.Id,
|
||||
LNURLEndpoint = new Uri(_linkGenerator.GetUriByAction(
|
||||
action: nameof(UILNURLAuthController.LoginResponse),
|
||||
controller: "UILNURLAuth",
|
||||
values: new { userId = user.Id, action = "login", tag = "login", k1 = Encoders.Hex.EncodeData(r) }, Request.Scheme, Request.Host, Request.PathBase))
|
||||
action: nameof(UILNURLAuthController.LoginResponse),
|
||||
controller: "UILNURLAuth",
|
||||
values: new { userId = user.Id, action = "login", tag = "login", k1 = Encoders.Hex.EncodeData(r) }, Request.Scheme, Request.Host, Request.PathBase) ?? string.Empty)
|
||||
};
|
||||
}
|
||||
return null;
|
||||
@ -298,14 +294,18 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
var errorMessage = "Invalid login attempt.";
|
||||
var user = await _userManager.FindByIdAsync(viewModel.UserId);
|
||||
|
||||
if (user == null)
|
||||
if (!UserService.TryCanLogin(user, out var message))
|
||||
{
|
||||
return NotFound();
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = user == null ? StatusMessageModel.StatusSeverity.Error : StatusMessageModel.StatusSeverity.Warning,
|
||||
Message = message
|
||||
});
|
||||
return RedirectToAction("Login");
|
||||
}
|
||||
|
||||
var errorMessage = string.Empty;
|
||||
try
|
||||
{
|
||||
var k1 = Encoders.Hex.DecodeData(viewModel.LNURLEndpoint.ParseQueryString().Get("k1"));
|
||||
@ -313,34 +313,33 @@ namespace BTCPayServer.Controllers
|
||||
storedk1.SequenceEqual(k1))
|
||||
{
|
||||
_lnurlAuthService.FinalLoginStore.TryRemove(viewModel.UserId, out _);
|
||||
await _signInManager.SignInAsync(user, viewModel.RememberMe, "FIDO2");
|
||||
_logger.LogInformation("User logged in.");
|
||||
await _signInManager.SignInAsync(user!, viewModel.RememberMe, "FIDO2");
|
||||
_logger.LogInformation("User logged in");
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
|
||||
errorMessage = "Invalid login attempt.";
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
errorMessage = e.Message;
|
||||
}
|
||||
|
||||
ModelState.AddModelError(string.Empty, errorMessage);
|
||||
return View("SecondaryLogin", new SecondaryLoginViewModel()
|
||||
if (!string.IsNullOrEmpty(errorMessage))
|
||||
{
|
||||
|
||||
LoginWithFido2ViewModel = (await _fido2Service.HasCredentials(user.Id)) ? await BuildFido2ViewModel(viewModel.RememberMe, user) : null,
|
||||
ModelState.AddModelError(string.Empty, errorMessage);
|
||||
}
|
||||
return View("SecondaryLogin", new SecondaryLoginViewModel
|
||||
{
|
||||
LoginWithFido2ViewModel = await _fido2Service.HasCredentials(user!.Id) ? await BuildFido2ViewModel(viewModel.RememberMe, user) : null,
|
||||
LoginWithLNURLAuthViewModel = viewModel,
|
||||
LoginWith2FaViewModel = !user.TwoFactorEnabled
|
||||
? null
|
||||
: new LoginWith2faViewModel()
|
||||
: new LoginWith2faViewModel
|
||||
{
|
||||
RememberMe = viewModel.RememberMe
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("/login/fido2")]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
@ -352,44 +351,50 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
var errorMessage = "Invalid login attempt.";
|
||||
var user = await _userManager.FindByIdAsync(viewModel.UserId);
|
||||
|
||||
if (user == null)
|
||||
if (!UserService.TryCanLogin(user, out var message))
|
||||
{
|
||||
return NotFound();
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = user == null ? StatusMessageModel.StatusSeverity.Error : StatusMessageModel.StatusSeverity.Warning,
|
||||
Message = message
|
||||
});
|
||||
return RedirectToAction("Login");
|
||||
}
|
||||
|
||||
var errorMessage = string.Empty;
|
||||
try
|
||||
{
|
||||
if (await _fido2Service.CompleteLogin(viewModel.UserId, JObject.Parse(viewModel.Response).ToObject<AuthenticatorAssertionRawResponse>()))
|
||||
{
|
||||
await _signInManager.SignInAsync(user, viewModel.RememberMe, "FIDO2");
|
||||
_logger.LogInformation("User logged in.");
|
||||
await _signInManager.SignInAsync(user!, viewModel.RememberMe, "FIDO2");
|
||||
_logger.LogInformation("User {Email} logged in with FIDO2", user.Email);
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
|
||||
errorMessage = "Invalid login attempt.";
|
||||
}
|
||||
catch (Fido2VerificationException e)
|
||||
{
|
||||
errorMessage = e.Message;
|
||||
}
|
||||
|
||||
ModelState.AddModelError(string.Empty, errorMessage);
|
||||
if (!string.IsNullOrEmpty(errorMessage))
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, errorMessage);
|
||||
}
|
||||
viewModel.Response = null;
|
||||
return View("SecondaryLogin", new SecondaryLoginViewModel()
|
||||
return View("SecondaryLogin", new SecondaryLoginViewModel
|
||||
{
|
||||
LoginWithFido2ViewModel = viewModel,
|
||||
LoginWithLNURLAuthViewModel = (await _lnurlAuthService.HasCredentials(user.Id)) ? await BuildLNURLAuthViewModel(viewModel.RememberMe, user) : null,
|
||||
LoginWithLNURLAuthViewModel = await _lnurlAuthService.HasCredentials(user!.Id) ? await BuildLNURLAuthViewModel(viewModel.RememberMe, user) : null,
|
||||
LoginWith2FaViewModel = !user.TwoFactorEnabled
|
||||
? null
|
||||
: new LoginWith2faViewModel()
|
||||
: new LoginWith2faViewModel
|
||||
{
|
||||
RememberMe = viewModel.RememberMe
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("/login/2fa")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> LoginWith2fa(bool rememberMe, string returnUrl = null)
|
||||
@ -401,7 +406,6 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
// Ensure the user has gone through the username & password screen first
|
||||
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load two-factor authentication user.");
|
||||
@ -409,11 +413,11 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
|
||||
return View("SecondaryLogin", new SecondaryLoginViewModel()
|
||||
return View("SecondaryLogin", new SecondaryLoginViewModel
|
||||
{
|
||||
LoginWith2FaViewModel = new LoginWith2faViewModel { RememberMe = rememberMe },
|
||||
LoginWithFido2ViewModel = (await _fido2Service.HasCredentials(user.Id)) ? await BuildFido2ViewModel(rememberMe, user) : null,
|
||||
LoginWithLNURLAuthViewModel = (await _lnurlAuthService.HasCredentials(user.Id)) ? await BuildLNURLAuthViewModel(rememberMe, user) : null,
|
||||
LoginWithFido2ViewModel = await _fido2Service.HasCredentials(user.Id) ? await BuildFido2ViewModel(rememberMe, user) : null,
|
||||
LoginWithLNURLAuthViewModel = await _lnurlAuthService.HasCredentials(user.Id) ? await BuildLNURLAuthViewModel(rememberMe, user) : null,
|
||||
});
|
||||
}
|
||||
|
||||
@ -437,32 +441,32 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
if (!UserService.TryCanLogin(user, out var message))
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Warning,
|
||||
Message = message
|
||||
});
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var authenticatorCode = model.TwoFactorCode.Replace(" ", string.Empty, StringComparison.InvariantCulture).Replace("-", string.Empty, StringComparison.InvariantCulture);
|
||||
|
||||
var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe, model.RememberMachine);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User with ID {UserId} logged in with 2fa.", user.Id);
|
||||
_logger.LogInformation("User {Email} logged in with 2FA", user.Email);
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
else if (result.IsLockedOut)
|
||||
|
||||
_logger.LogWarning("User {Email} entered invalid authenticator code", user.Email);
|
||||
ModelState.AddModelError(string.Empty, "Invalid authenticator code.");
|
||||
return View("SecondaryLogin", new SecondaryLoginViewModel
|
||||
{
|
||||
_logger.LogWarning("User with ID {UserId} account locked out.", user.Id);
|
||||
return RedirectToAction(nameof(Lockout), new { user.LockoutEnd });
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Invalid authenticator code entered for user with ID {UserId}.", user.Id);
|
||||
ModelState.AddModelError(string.Empty, "Invalid authenticator code.");
|
||||
return View("SecondaryLogin", new SecondaryLoginViewModel()
|
||||
{
|
||||
LoginWith2FaViewModel = model,
|
||||
LoginWithFido2ViewModel = (await _fido2Service.HasCredentials(user.Id)) ? await BuildFido2ViewModel(rememberMe, user) : null,
|
||||
LoginWithLNURLAuthViewModel = (await _lnurlAuthService.HasCredentials(user.Id)) ? await BuildLNURLAuthViewModel(rememberMe, user) : null,
|
||||
});
|
||||
}
|
||||
LoginWith2FaViewModel = model,
|
||||
LoginWithFido2ViewModel = await _fido2Service.HasCredentials(user.Id) ? await BuildFido2ViewModel(rememberMe, user) : null,
|
||||
LoginWithLNURLAuthViewModel = await _lnurlAuthService.HasCredentials(user.Id) ? await BuildLNURLAuthViewModel(rememberMe, user) : null,
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("/login/recovery-code")]
|
||||
@ -504,30 +508,35 @@ namespace BTCPayServer.Controllers
|
||||
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load two-factor authentication user.");
|
||||
throw new ApplicationException("Unable to load two-factor authentication user.");
|
||||
}
|
||||
if (!UserService.TryCanLogin(user, out var message))
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Warning,
|
||||
Message = message
|
||||
});
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var recoveryCode = model.RecoveryCode.Replace(" ", string.Empty, StringComparison.InvariantCulture);
|
||||
|
||||
var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User with ID {UserId} logged in with a recovery code.", user.Id);
|
||||
_logger.LogInformation("User {Email} logged in with a recovery code", user.Email);
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
if (result.IsLockedOut)
|
||||
{
|
||||
_logger.LogWarning("User with ID {UserId} account locked out.", user.Id);
|
||||
_logger.LogWarning("User {Email} account locked out", user.Email);
|
||||
|
||||
return RedirectToAction(nameof(Lockout), new { user.LockoutEnd });
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Invalid recovery code entered for user with ID {UserId}", user.Id);
|
||||
ModelState.AddModelError(string.Empty, "Invalid recovery code entered.");
|
||||
return View();
|
||||
}
|
||||
|
||||
_logger.LogWarning("User {Email} entered invalid recovery code", user.Email);
|
||||
ModelState.AddModelError(string.Empty, "Invalid recovery code entered.");
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpGet("/login/lockout")]
|
||||
@ -540,7 +549,7 @@ namespace BTCPayServer.Controllers
|
||||
[HttpGet("/register")]
|
||||
[AllowAnonymous]
|
||||
[RateLimitsFilter(ZoneLimits.Register, Scope = RateLimitsScope.RemoteAddress)]
|
||||
public IActionResult Register(string returnUrl = null, bool logon = true)
|
||||
public IActionResult Register(string returnUrl = null)
|
||||
{
|
||||
if (!CanLoginOrRegister())
|
||||
{
|
||||
@ -567,32 +576,35 @@ namespace BTCPayServer.Controllers
|
||||
var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
|
||||
if (policies.LockSubscription && !User.IsInRole(Roles.ServerAdmin))
|
||||
return RedirectToAction(nameof(UIHomeController.Index), "UIHome");
|
||||
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
var anyAdmin = (await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin)).Any();
|
||||
var isFirstAdmin = !anyAdmin || (model.IsAdmin && _Options.CheatMode);
|
||||
var user = new ApplicationUser
|
||||
{
|
||||
UserName = model.Email,
|
||||
Email = model.Email,
|
||||
RequiresEmailConfirmation = policies.RequiresConfirmedEmail,
|
||||
Created = DateTimeOffset.UtcNow
|
||||
RequiresApproval = policies.RequiresUserApproval,
|
||||
Created = DateTimeOffset.UtcNow,
|
||||
Approved = isFirstAdmin // auto-approve first admin
|
||||
};
|
||||
var result = await _userManager.CreateAsync(user, model.Password);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
var admin = await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin);
|
||||
if (admin.Count == 0 || (model.IsAdmin && _Options.CheatMode))
|
||||
if (isFirstAdmin)
|
||||
{
|
||||
await _RoleManager.CreateAsync(new IdentityRole(Roles.ServerAdmin));
|
||||
await _userManager.AddToRoleAsync(user, Roles.ServerAdmin);
|
||||
var settings = await _SettingsRepository.GetSettingAsync<ThemeSettings>();
|
||||
var settings = await _SettingsRepository.GetSettingAsync<ThemeSettings>() ?? new ThemeSettings();
|
||||
settings.FirstRun = false;
|
||||
await _SettingsRepository.UpdateSetting<ThemeSettings>(settings);
|
||||
|
||||
await _SettingsRepository.UpdateSetting(settings);
|
||||
await _SettingsRepository.FirstAdminRegistered(policies, _Options.UpdateUrl != null, _Options.DisableRegistration, Logs);
|
||||
RegisteredAdmin = true;
|
||||
}
|
||||
|
||||
_eventAggregator.Publish(new UserRegisteredEvent()
|
||||
_eventAggregator.Publish(new UserRegisteredEvent
|
||||
{
|
||||
RequestUri = Request.GetAbsoluteRootUri(),
|
||||
User = user,
|
||||
@ -600,19 +612,31 @@ namespace BTCPayServer.Controllers
|
||||
});
|
||||
RegisteredUserId = user.Id;
|
||||
|
||||
if (!policies.RequiresConfirmedEmail)
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Account created.";
|
||||
var requiresConfirmedEmail = policies.RequiresConfirmedEmail && !user.EmailConfirmed;
|
||||
var requiresUserApproval = policies.RequiresUserApproval && !user.Approved;
|
||||
if (requiresConfirmedEmail)
|
||||
{
|
||||
if (logon)
|
||||
await _signInManager.SignInAsync(user, isPersistent: false);
|
||||
TempData[WellKnownTempData.SuccessMessage] += " Please confirm your email.";
|
||||
}
|
||||
if (requiresUserApproval)
|
||||
{
|
||||
TempData[WellKnownTempData.SuccessMessage] += " The new account requires approval by an admin before you can log in.";
|
||||
}
|
||||
if (requiresConfirmedEmail || requiresUserApproval)
|
||||
{
|
||||
return RedirectToAction(nameof(Login));
|
||||
}
|
||||
if (logon)
|
||||
{
|
||||
await _signInManager.SignInAsync(user, isPersistent: false);
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Account created, please confirm your email";
|
||||
return View();
|
||||
}
|
||||
}
|
||||
AddErrors(result);
|
||||
else
|
||||
{
|
||||
AddErrors(result);
|
||||
}
|
||||
}
|
||||
|
||||
// If we got this far, something failed, redisplay form
|
||||
@ -626,10 +650,12 @@ namespace BTCPayServer.Controllers
|
||||
[HttpGet("/logout")]
|
||||
public async Task<IActionResult> Logout()
|
||||
{
|
||||
var userId = _signInManager.UserManager.GetUserId(HttpContext.User);
|
||||
var user = await _userManager.FindByIdAsync(userId);
|
||||
await _signInManager.SignOutAsync();
|
||||
HttpContext.DeleteUserPrefsCookie();
|
||||
_logger.LogInformation("User logged out.");
|
||||
return RedirectToAction(nameof(UIAccountController.Login));
|
||||
_logger.LogInformation("User {Email} logged out", user!.Email);
|
||||
return RedirectToAction(nameof(Login));
|
||||
}
|
||||
|
||||
[HttpGet("/register/confirm-email")]
|
||||
@ -647,25 +673,31 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
var result = await _userManager.ConfirmEmailAsync(user, code);
|
||||
if (!await _userManager.HasPasswordAsync(user))
|
||||
{
|
||||
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Info,
|
||||
Message = "Your email has been confirmed but you still need to set your password."
|
||||
});
|
||||
return RedirectToAction("SetPassword", new { email = user.Email, code = await _userManager.GeneratePasswordResetTokenAsync(user) });
|
||||
}
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
_eventAggregator.Publish(new UserConfirmedEmailEvent
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = "Your email has been confirmed."
|
||||
User = user,
|
||||
RequestUri = Request.GetAbsoluteRootUri()
|
||||
});
|
||||
return RedirectToAction("Login", new { email = user.Email });
|
||||
|
||||
var hasPassword = await _userManager.HasPasswordAsync(user);
|
||||
if (hasPassword)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = "Your email has been confirmed."
|
||||
});
|
||||
return RedirectToAction(nameof(Login), new { email = user.Email });
|
||||
}
|
||||
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Info,
|
||||
Message = "Your email has been confirmed. Please set your password."
|
||||
});
|
||||
return await RedirectToSetPassword(user);
|
||||
}
|
||||
|
||||
return View("Error");
|
||||
@ -687,12 +719,12 @@ namespace BTCPayServer.Controllers
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
var user = await _userManager.FindByEmailAsync(model.Email);
|
||||
if (user == null || (user.RequiresEmailConfirmation && !(await _userManager.IsEmailConfirmedAsync(user))))
|
||||
if (!UserService.TryCanLogin(user, out _))
|
||||
{
|
||||
// Don't reveal that the user does not exist or is not confirmed
|
||||
return RedirectToAction(nameof(ForgotPasswordConfirmation));
|
||||
}
|
||||
_eventAggregator.Publish(new UserPasswordResetRequestedEvent()
|
||||
_eventAggregator.Publish(new UserPasswordResetRequestedEvent
|
||||
{
|
||||
User = user,
|
||||
RequestUri = Request.GetAbsoluteRootUri()
|
||||
@ -717,17 +749,23 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (code == null)
|
||||
{
|
||||
throw new ApplicationException("A code must be supplied for password reset.");
|
||||
throw new ApplicationException("A code must be supplied for this action.");
|
||||
}
|
||||
|
||||
var user = string.IsNullOrEmpty(userId) ? null : await _userManager.FindByIdAsync(userId);
|
||||
var hasPassword = user != null && await _userManager.HasPasswordAsync(user);
|
||||
if (!string.IsNullOrEmpty(userId))
|
||||
{
|
||||
var user = await _userManager.FindByIdAsync(userId);
|
||||
email = user?.Email;
|
||||
}
|
||||
|
||||
var model = new SetPasswordViewModel { Code = code, Email = email, EmailSetInternally = !string.IsNullOrEmpty(email) };
|
||||
return View(model);
|
||||
return View(new SetPasswordViewModel
|
||||
{
|
||||
Code = code,
|
||||
Email = email,
|
||||
EmailSetInternally = !string.IsNullOrEmpty(email),
|
||||
HasPassword = hasPassword
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("/login/set-password")]
|
||||
@ -739,28 +777,91 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var user = await _userManager.FindByEmailAsync(model.Email);
|
||||
if (user == null)
|
||||
var hasPassword = user != null && await _userManager.HasPasswordAsync(user);
|
||||
if (!UserService.TryCanLogin(user, out _))
|
||||
{
|
||||
// Don't reveal that the user does not exist
|
||||
return RedirectToAction(nameof(Login));
|
||||
}
|
||||
|
||||
var result = await _userManager.ResetPasswordAsync(user, model.Code, model.Password);
|
||||
var result = await _userManager.ResetPasswordAsync(user!, model.Code, model.Password);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = "Password successfully set."
|
||||
Message = hasPassword ? "Password successfully set." : "Account successfully created."
|
||||
});
|
||||
return RedirectToAction(nameof(Login));
|
||||
}
|
||||
|
||||
AddErrors(result);
|
||||
model.HasPassword = await _userManager.HasPasswordAsync(user);
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpGet("/invite/{userId}/{code}")]
|
||||
public async Task<IActionResult> AcceptInvite(string userId, string code)
|
||||
{
|
||||
if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(code))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var user = await _userManager.FindByInvitationTokenAsync(userId, Uri.UnescapeDataString(code));
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var requiresEmailConfirmation = user.RequiresEmailConfirmation && !user.EmailConfirmed;
|
||||
var requiresSetPassword = !await _userManager.HasPasswordAsync(user);
|
||||
|
||||
_eventAggregator.Publish(new UserInviteAcceptedEvent
|
||||
{
|
||||
User = user,
|
||||
RequestUri = Request.GetAbsoluteRootUri()
|
||||
});
|
||||
|
||||
if (requiresEmailConfirmation)
|
||||
{
|
||||
return await RedirectToConfirmEmail(user);
|
||||
}
|
||||
if (requiresSetPassword)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Info,
|
||||
Message = "Invitation accepted. Please set your password."
|
||||
});
|
||||
return await RedirectToSetPassword(user);
|
||||
}
|
||||
|
||||
// Inform user that a password has been set on account creation
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Info,
|
||||
Message = "Your password has been set by the user who invited you."
|
||||
});
|
||||
|
||||
return RedirectToAction(nameof(Login), new { email = user.Email });
|
||||
}
|
||||
|
||||
private async Task<IActionResult> RedirectToConfirmEmail(ApplicationUser user)
|
||||
{
|
||||
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
return RedirectToAction(nameof(ConfirmEmail), new { userId = user.Id, code });
|
||||
}
|
||||
|
||||
private async Task<IActionResult> RedirectToSetPassword(ApplicationUser user)
|
||||
{
|
||||
var code = await _userManager.GeneratePasswordResetTokenAsync(user);
|
||||
return RedirectToAction(nameof(SetPassword), new { userId = user.Id, email = user.Email, code });
|
||||
}
|
||||
|
||||
#region Helpers
|
||||
|
||||
private void AddErrors(IdentityResult result)
|
||||
@ -800,7 +901,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
private void SetInsecureFlags()
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = "You cannot login over an insecure connection. Please use HTTPS or Tor."
|
||||
|
@ -61,7 +61,7 @@ public class UIBoltcardController : Controller
|
||||
var registration = await ContextFactory.GetBoltcardRegistration(issuerKey, piccData, updateCounter: pr is not null);
|
||||
if (registration?.PullPaymentId is null)
|
||||
return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Replayed or expired query" });
|
||||
var cardKey = issuerKey.CreateCardKey(piccData.Uid, registration.Version);
|
||||
var cardKey = issuerKey.CreatePullPaymentCardKey(piccData.Uid, registration.Version, registration.PullPaymentId);
|
||||
if (!cardKey.CheckSunMac(c, piccData))
|
||||
return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Replayed or expired query" });
|
||||
LNURLController.ControllerContext.HttpContext = HttpContext;
|
||||
|
@ -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();
|
||||
|
||||
|
@ -29,7 +29,6 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
private readonly ThemeSettings _theme;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly BTCPayNetworkProvider _networkProvider;
|
||||
private IHttpClientFactory HttpClientFactory { get; }
|
||||
private SignInManager<ApplicationUser> SignInManager { get; }
|
||||
|
||||
@ -41,14 +40,12 @@ namespace BTCPayServer.Controllers
|
||||
ThemeSettings theme,
|
||||
LanguageService languageService,
|
||||
StoreRepository storeRepository,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
IWebHostEnvironment environment,
|
||||
SignInManager<ApplicationUser> signInManager)
|
||||
{
|
||||
_theme = theme;
|
||||
HttpClientFactory = httpClientFactory;
|
||||
LanguageService = languageService;
|
||||
_networkProvider = networkProvider;
|
||||
_storeRepository = storeRepository;
|
||||
SignInManager = signInManager;
|
||||
_WebRootFileProvider = environment.WebRootFileProvider;
|
||||
@ -76,16 +73,17 @@ namespace BTCPayServer.Controllers
|
||||
if (storeId != null)
|
||||
{
|
||||
// verify store exists and redirect to it
|
||||
var store = await _storeRepository.FindStore(storeId, userId);
|
||||
var store = await _storeRepository.FindStore(storeId);
|
||||
if (store != null)
|
||||
{
|
||||
return RedirectToStore(userId, store);
|
||||
return RedirectToAction(nameof(UIStoresController.Index), "UIStores", new { storeId });
|
||||
}
|
||||
}
|
||||
|
||||
var stores = await _storeRepository.GetStoresByUserId(userId);
|
||||
return stores.Any()
|
||||
? RedirectToStore(userId, stores.First())
|
||||
var stores = await _storeRepository.GetStoresByUserId(userId!);
|
||||
var activeStore = stores.FirstOrDefault(s => !s.Archived);
|
||||
return activeStore != null
|
||||
? RedirectToAction(nameof(UIStoresController.Index), "UIStores", new { storeId = activeStore.Id })
|
||||
: RedirectToAction(nameof(UIUserStoresController.CreateStore), "UIUserStores");
|
||||
}
|
||||
|
||||
@ -197,14 +195,5 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
|
||||
}
|
||||
public static RedirectToActionResult RedirectToStore(string userId, StoreData store)
|
||||
{
|
||||
var perms = store.GetPermissionSet(userId);
|
||||
if (perms.Contains(Policies.CanModifyStoreSettings, store.Id))
|
||||
return new RedirectToActionResult("Dashboard", "UIStores", new {storeId = store.Id});
|
||||
if (perms.Contains(Policies.CanViewInvoices, store.Id))
|
||||
return new RedirectToActionResult("ListInvoices", "UIInvoice", new { storeId = store.Id });
|
||||
return new RedirectToActionResult("Index", "UIStores", new {storeId = store.Id});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
@ -167,10 +162,10 @@ namespace BTCPayServer.Controllers
|
||||
model.Overpaid = details.Overpaid;
|
||||
model.StillDue = details.StillDue;
|
||||
model.HasRates = details.HasRates;
|
||||
|
||||
if (additionalData.ContainsKey("receiptData"))
|
||||
|
||||
if (additionalData.TryGetValue("receiptData", out object? receiptData))
|
||||
{
|
||||
model.ReceiptData = (Dictionary<string, object>)additionalData["receiptData"];
|
||||
model.ReceiptData = (Dictionary<string, object>)receiptData;
|
||||
additionalData.Remove("receiptData");
|
||||
}
|
||||
|
||||
@ -231,15 +226,40 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
JToken? receiptData = null;
|
||||
i.Metadata?.AdditionalData?.TryGetValue("receiptData", out receiptData);
|
||||
|
||||
var metaData = PosDataParser.ParsePosData(i.Metadata?.ToJObject());
|
||||
var additionalData = metaData
|
||||
.Where(dict => !InvoiceAdditionalDataExclude.Contains(dict.Key))
|
||||
.ToDictionary(dict => dict.Key, dict => dict.Value);
|
||||
|
||||
// Split receipt data into cart and additional data
|
||||
if (additionalData.TryGetValue("receiptData", out object? combinedReceiptData))
|
||||
{
|
||||
var receiptData = new Dictionary<string, object>((Dictionary<string, object>)combinedReceiptData, StringComparer.OrdinalIgnoreCase);
|
||||
string[] cartKeys = ["cart", "subtotal", "discount", "tip", "total"];
|
||||
// extract cart data and lowercase keys to handle data uniformly in PosData partial
|
||||
if (receiptData.Keys.Any(key => cartKeys.Contains(key.ToLowerInvariant())))
|
||||
{
|
||||
vm.CartData = new Dictionary<string, object>();
|
||||
foreach (var key in cartKeys)
|
||||
{
|
||||
if (!receiptData.ContainsKey(key)) continue;
|
||||
// add it to cart data and remove it from the general data
|
||||
vm.CartData.Add(key.ToLowerInvariant(), receiptData[key]);
|
||||
receiptData.Remove(key);
|
||||
}
|
||||
}
|
||||
// assign the rest to additional data
|
||||
if (receiptData.Any())
|
||||
{
|
||||
vm.AdditionalData = receiptData;
|
||||
}
|
||||
}
|
||||
|
||||
var payments = ViewPaymentRequestViewModel.PaymentRequestInvoicePayment.GetViewModels(i, _displayFormatter, _transactionLinkProviders);
|
||||
|
||||
vm.Amount = i.PaidAmount.Net;
|
||||
vm.Payments = receipt.ShowPayments is false ? null : payments;
|
||||
vm.AdditionalData = PosDataParser.ParsePosData(receiptData);
|
||||
|
||||
return View(print ? "InvoiceReceiptPrint" : "InvoiceReceipt", vm);
|
||||
}
|
||||
@ -617,7 +637,7 @@ namespace BTCPayServer.Controllers
|
||||
await _InvoiceRepository.MassArchive(selectedItems, false);
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"{selectedItems.Length} invoice{(selectedItems.Length == 1 ? "" : "s")} unarchived.";
|
||||
break;
|
||||
case "cpfp":
|
||||
case "cpfp" when storeId is not null:
|
||||
var network = _NetworkProvider.DefaultNetwork;
|
||||
var explorer = _ExplorerClients.GetExplorerClient(network);
|
||||
if (explorer is null)
|
||||
@ -856,10 +876,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",
|
||||
@ -1080,7 +1098,7 @@ namespace BTCPayServer.Controllers
|
||||
storeIds.Add(i);
|
||||
}
|
||||
model.Search = fs;
|
||||
model.SearchText = fs.TextSearch;
|
||||
model.SearchText = fs.TextCombined;
|
||||
|
||||
var apps = await _appService.GetAllApps(GetUserId(), false, storeId);
|
||||
InvoiceQuery invoiceQuery = GetInvoiceQuery(fs, apps, timezoneOffset);
|
||||
@ -1149,63 +1167,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)]
|
||||
[Authorize(Policy = Policies.CanCreateInvoice, 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);
|
||||
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 +1207,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 +1233,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 +1378,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)
|
||||
{
|
||||
|
@ -296,11 +296,11 @@ namespace BTCPayServer
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var createInvoice = new CreateInvoiceRequest()
|
||||
var createInvoice = new CreateInvoiceRequest
|
||||
{
|
||||
Amount = item?.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup? null: item?.Price,
|
||||
Amount = item?.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup ? null : item?.Price,
|
||||
Currency = currencyCode,
|
||||
Checkout = new InvoiceDataBase.CheckoutOptions()
|
||||
Checkout = new InvoiceDataBase.CheckoutOptions
|
||||
{
|
||||
RedirectURL = app.AppType switch
|
||||
{
|
||||
@ -312,6 +312,7 @@ namespace BTCPayServer
|
||||
AdditionalSearchTerms = new[] { AppService.GetAppSearchTerm(app) }
|
||||
};
|
||||
|
||||
var allowOverpay = item?.PriceType is not ViewPointOfSaleViewModel.ItemPriceType.Fixed;
|
||||
var invoiceMetadata = new InvoiceMetadata { OrderId = AppService.GetRandomOrderId() };
|
||||
if (item != null)
|
||||
{
|
||||
@ -326,7 +327,7 @@ namespace BTCPayServer
|
||||
store.GetStoreBlob(),
|
||||
createInvoice,
|
||||
additionalTags: new List<string> { AppService.GetAppInternalTag(appId) },
|
||||
allowOverpay: false);
|
||||
allowOverpay: allowOverpay);
|
||||
}
|
||||
|
||||
public class EditLightningAddressVM
|
||||
@ -373,7 +374,7 @@ namespace BTCPayServer
|
||||
if (string.IsNullOrEmpty(username))
|
||||
return NotFound("Unknown username");
|
||||
|
||||
LNURLPayRequest lnurlRequest = null;
|
||||
LNURLPayRequest lnurlRequest;
|
||||
|
||||
// Check core and fall back to lookup Lightning Address via plugins
|
||||
var lightningAddressSettings = await _lightningAddressService.ResolveByAddress(username);
|
||||
@ -495,7 +496,7 @@ namespace BTCPayServer
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<IActionResult> GetLNURLRequest(
|
||||
public async Task<IActionResult> GetLNURLRequest(
|
||||
string cryptoCode,
|
||||
Data.StoreData store,
|
||||
Data.StoreBlob blob,
|
||||
@ -522,7 +523,9 @@ namespace BTCPayServer
|
||||
return this.CreateAPIError(null, e.Message);
|
||||
}
|
||||
lnurlRequest = await CreateLNUrlRequestFromInvoice(cryptoCode, i, store, blob, lnurlRequest, lnUrlMetadata, allowOverpay);
|
||||
return lnurlRequest is null ? NotFound() : Ok(lnurlRequest);
|
||||
return lnurlRequest is null
|
||||
? BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Unable to create LNURL request." })
|
||||
: Ok(lnurlRequest);
|
||||
}
|
||||
|
||||
private async Task<LNURLPayRequest> CreateLNUrlRequestFromInvoice(
|
||||
|
@ -50,7 +50,7 @@ namespace BTCPayServer.Controllers
|
||||
$"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'.");
|
||||
}
|
||||
|
||||
_logger.LogInformation("User with ID {UserId} has disabled 2fa.", user.Id);
|
||||
_logger.LogInformation("User {Email} has disabled 2fa", user.Email);
|
||||
return RedirectToAction(nameof(TwoFactorAuthentication));
|
||||
}
|
||||
|
||||
@ -100,7 +100,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
await _userManager.SetTwoFactorEnabledAsync(user, true);
|
||||
_logger.LogInformation("User with ID {UserId} has enabled 2FA with an authenticator app.", user.Id);
|
||||
_logger.LogInformation("User {Email} has enabled 2FA with an authenticator app", user.Email);
|
||||
var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
|
||||
TempData[RecoveryCodesKey] = recoveryCodes.ToArray();
|
||||
|
||||
@ -117,7 +117,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
await _userManager.SetTwoFactorEnabledAsync(user, false);
|
||||
await _userManager.ResetAuthenticatorKeyAsync(user);
|
||||
_logger.LogInformation("User with id '{UserId}' has reset their authentication app key.", user.Id);
|
||||
_logger.LogInformation("User {Email} has reset their authentication app key", user.Email);
|
||||
|
||||
return RedirectToAction(nameof(EnableAuthenticator));
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ namespace BTCPayServer.Controllers
|
||||
new List<string>();
|
||||
var notifications = notificationHandlers.SelectMany(handler => handler.Meta.Select(tuple =>
|
||||
new SelectListItem(tuple.name, tuple.identifier,
|
||||
disabledNotifications.Contains(tuple.identifier, StringComparer.InvariantCultureIgnoreCase))))
|
||||
!disabledNotifications.Contains(tuple.identifier, StringComparer.InvariantCultureIgnoreCase))))
|
||||
.ToList();
|
||||
|
||||
return View(new NotificationSettingsViewModel { DisabledNotifications = notifications });
|
||||
@ -46,7 +46,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
else if (command == "update")
|
||||
{
|
||||
var disabled = vm.DisabledNotifications.Where(item => item.Selected).Select(item => item.Value)
|
||||
var disabled = vm.DisabledNotifications.Where(item => !item.Selected).Select(item => item.Value)
|
||||
.ToArray();
|
||||
user.DisabledNotifications = disabled.Any()
|
||||
? string.Join(';', disabled) + ";"
|
||||
|
@ -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")]
|
||||
@ -103,16 +108,24 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("/stores/{storeId}/payment-requests/edit/{payReqId?}")]
|
||||
[Authorize(Policy = Policies.CanViewPaymentRequests, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[Authorize(Policy = Policies.CanModifyPaymentRequests, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
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.");
|
||||
@ -260,6 +277,10 @@ namespace BTCPayServer.Controllers
|
||||
if (FormDataService.Validate(form, ModelState))
|
||||
{
|
||||
prBlob.FormResponse = FormDataService.GetValues(form);
|
||||
if(string.IsNullOrEmpty(prBlob.Email) && form.GetFieldByFullName("buyerEmail") is { } emailField)
|
||||
{
|
||||
prBlob.Email = emailField.Value;
|
||||
}
|
||||
result.SetBlob(prBlob);
|
||||
await _PaymentRequestRepository.CreateOrUpdatePaymentRequest(result);
|
||||
return RedirectToAction("PayPaymentRequest", new { payReqId });
|
||||
@ -441,5 +462,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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user