Compare commits
291 Commits
qwintq
...
afrobitcoi
Author | SHA1 | Date | |
---|---|---|---|
d7644c0fe7 | |||
bf9e2f47a3 | |||
a32257e5a2 | |||
f56ea60317 | |||
5bb7f158db | |||
4f49b5f1a0 | |||
25c30512ec | |||
52df7a5b89 | |||
1a85da27db | |||
10326a822e | |||
d1bf47a5c0 | |||
ccf9cfa332 | |||
773f8a9aea | |||
dd62e166a1 | |||
2fb72d5aa6 | |||
46f0818765 | |||
96569ae4aa | |||
f2b1e5f93e | |||
2326894a2b | |||
c15f02ddbf | |||
7708084331 | |||
696a414e95 | |||
c16dfb2dcb | |||
c979c4774c | |||
e82281d273 | |||
27c22d5e33 | |||
6acc545b66 | |||
609ec0989f | |||
b702621a04 | |||
89041a6744 | |||
c485c109e6 | |||
29a49d5f71 | |||
a5fafc4864 | |||
a921504bcf | |||
027154a4d3 | |||
bf1a1368ff | |||
097ffbf8a3 | |||
ec076d1560 | |||
8dadfa2111 | |||
c8ee6ead0b | |||
018e4c501d | |||
99a0b70cfa | |||
314a1352ec | |||
901e6be21e | |||
d58dde950e | |||
8ac18b74df | |||
2846c38ff5 | |||
d44efce225 | |||
d3dca7e808 | |||
41e3828eea | |||
9e76b4d28e | |||
ef03497350 | |||
e5a2aeb145 | |||
229a4ea56c | |||
f20e6d3768 | |||
1d210eb6e3 | |||
d8422a979f | |||
0cf6d39f02 | |||
076c20a3b7 | |||
0cfb0ba890 | |||
44a7e9387e | |||
e71954ee34 | |||
9cd9e84be6 | |||
25af9c4227 | |||
72a99bf9a6 | |||
f1228523cb | |||
a45d368115 | |||
16433dc183 | |||
0a956fdc73 | |||
75396f491b | |||
66a064e78b | |||
c4f8c4c7b4 | |||
22bbafa659 | |||
8cdfaba20c | |||
7da82826fb | |||
9a46a64cad | |||
33198d693d | |||
eaeb7021d5 | |||
b443acd43b | |||
616883648f | |||
7873f94848 | |||
17d1832dad | |||
f034e2cd65 | |||
19de73f9da | |||
00acbccd7f | |||
77d8e202d3 | |||
44df8cf0c5 | |||
e694568674 | |||
163f805f3b | |||
492512f527 | |||
73a4ac599c | |||
4aedf76f1f | |||
2d38113c66 | |||
445e1b7bd9 | |||
019ac7ae31 | |||
2b3b025bd8 | |||
57bc90ad03 | |||
089e16020e | |||
0c4f31794d | |||
cdffe9b355 | |||
5a28cf9e87 | |||
3b05de7f30 | |||
79b2f1652b | |||
b32e0e7cce | |||
1f9fbbee22 | |||
8c9f325c9f | |||
9bf1e35bf4 | |||
32e830a1c5 | |||
561bae071f | |||
08b6942c59 | |||
4564f9a46c | |||
58a1c6d2c8 | |||
97acec340c | |||
52790a6954 | |||
af6249a741 | |||
17064ab3c8 | |||
1487bf4ff5 | |||
e8c0858558 | |||
56fa3fe8f2 | |||
583813883c | |||
c69f95bdce | |||
b3df403980 | |||
90ce75ee21 | |||
1c5fcfe094 | |||
45c1fb42ee | |||
64bd493996 | |||
ec6029409e | |||
c0fc31c69a | |||
b5d0188f21 | |||
0ccbaf4bd6 | |||
ed43fb2071 | |||
d67ebd957e | |||
19d360a543 | |||
7dc41ebcea | |||
1eb7c727f3 | |||
ede8171408 | |||
2538f3d8f6 | |||
ac64f5e395 | |||
1a7a731b54 | |||
86f4d48bcb | |||
83536bee88 | |||
abfd6ea1dc | |||
688e873f7a | |||
c88df08350 | |||
82586590a7 | |||
88c66f30f2 | |||
9132592717 | |||
c0ffab768a | |||
69190081c8 | |||
093206cf1e | |||
a0110b7570 | |||
6d65feca4c | |||
95be0242b6 | |||
79e121c3af | |||
676ac2fe46 | |||
8eabdab53a | |||
957fb09ffc | |||
4bffe117a9 | |||
05b01a13c8 | |||
08e21c1a5d | |||
4d5245605d | |||
453548d614 | |||
95a0614ae1 | |||
36ea17a6b7 | |||
dc986959fd | |||
845e2881fa | |||
2e4be9310c | |||
a2faa6fd59 | |||
0a78846e8d | |||
4063a5aaee | |||
b1c81b696f | |||
0017f236a7 | |||
19d5e64063 | |||
22435a2bf5 | |||
a7def63137 | |||
3703a170e7 | |||
73fbfbd7cb | |||
acae3b8753 | |||
a618f901fc | |||
6d4918f0ab | |||
7f2c4d2e7a | |||
fd6d361e1a | |||
b5f0924651 | |||
1600dd4759 | |||
c777746b69 | |||
9f5466a41f | |||
4d1e4801bf | |||
5e469ff9c0 | |||
2f3eedea5b | |||
5c5d6dc1e2 | |||
fbe31ce64f | |||
0b082138c8 | |||
966e598f10 | |||
e998340387 | |||
f6b27cc5f9 | |||
f3dbf1e139 | |||
627d84fc91 | |||
8cde8c01df | |||
983b8c1f54 | |||
d666d8ea1a | |||
3ed81c3a78 | |||
4afec2e2b6 | |||
db83d238d5 | |||
fdcf7b3b7a | |||
53aafcf86b | |||
aec84f6d67 | |||
01e9f82d24 | |||
2eff45e65c | |||
13203c3e2b | |||
82c5e0e43d | |||
a1575f404b | |||
e1509506dc | |||
0c1d0d7b05 | |||
ad70856af0 | |||
8615f120ce | |||
0d0477d661 | |||
b31dc30878 | |||
6e392f4cfb | |||
cc3bdc331e | |||
76faf77a1c | |||
d8c0e5bf3a | |||
28c4c320cc | |||
e81403ec3f | |||
f11424f73a | |||
fa8b977016 | |||
d181846339 | |||
1956919886 | |||
0f66498965 | |||
918cd152b1 | |||
d3222df396 | |||
a84ffd8c7e | |||
6d0f9120b8 | |||
aafb4a7f2a | |||
ae432ff237 | |||
cdc318c71a | |||
94d1cec8a9 | |||
c0bc19ea59 | |||
6f07714cd9 | |||
a9d2cac23c | |||
693b46126b | |||
bbff9710bf | |||
358e122775 | |||
3818468932 | |||
3d2554fbe1 | |||
4309603317 | |||
f733c9ea77 | |||
775ee01171 | |||
33ec790137 | |||
0c575c888c | |||
24f7e51e3a | |||
0a0cf97c55 | |||
16b988d097 | |||
5edc0ff6ef | |||
375b96e508 | |||
1e72b12074 | |||
4a6d52f78e | |||
35dd580e74 | |||
79836ef1de | |||
8cb06f9c6c | |||
215a36e7a9 | |||
247f6b86a5 | |||
a9d42f1e6a | |||
4e03c2523a | |||
418b476725 | |||
783e4ccb35 | |||
6b7fb55658 | |||
3d5361cd11 | |||
2c4349c630 | |||
3589417b58 | |||
55203e0b64 | |||
a918288e3b | |||
e183138d2c | |||
d3e42862ed | |||
8860eec254 | |||
97e7e60cea | |||
44aaf7acbb | |||
9b721fae27 | |||
c3f412e3bb | |||
ee738a29f0 | |||
6c6544bf9b | |||
3d57b944ca | |||
acf003b1b4 | |||
52e108d32f | |||
7b96f96025 | |||
8db5e7e043 | |||
25fb5c1293 | |||
37f0498def | |||
02110f93d7 | |||
195dfc2c47 | |||
541b6cf9eb | |||
2c26b77afc |
.github
BTCPayServer.Abstractions
BTCPayServer.Client
BTCPayServer.Client.csprojBTCPayServerClient.OnChainWallet.csBTCPayServerClient.ServerInfo.csBTCPayServerClient.StoreUsers.csBTCPayServerClient.cs
Models
CreateAppRequest.csCreatePayoutThroughStoreRequest.csInvoiceExceptionStatus.csLNURLPayPaymentMethodBaseData.csLNURLPayPaymentMethodData.csLightningAutomatedPayoutSettings.csOnChainAutomatedPayoutSettings.csPaymentRequestData.csPayoutData.csPointOfSaleAppData.csRefundInvoiceRequest.csStoreBaseData.csStoreData.csStoreReportRequest.csStoreReportsResponse.csWebhookEvent.cs
Permissions.csBTCPayServer.Common
Altcoins
BTCPayNetworkProvider.BGold.csBTCPayNetworkProvider.Bitcore.csBTCPayNetworkProvider.Chaincoin.cs
BTCPayNetwork.csBTCPayNetworkProvider.csBTCPayServer.Common.csprojLiquid
Monero/RPC
BTCPayServer.Data
ApplicationDbContext.csApplicationDbContextFactory.cs
Data
AppData.csLightingAddressData.csPayoutProcessorData.csStoreData.csStoreRole.csUserStore.csWebhookDeliveryData.cs
Migrations
BTCPayServer.Rating
AvailableRateProvider.csBTCPayServer.Rating.csprojCurrencyNameTable.csCurrencyPair.csExtensions.cs
Providers
ArgoneumRateProvider.csCoinGeckoRateProvider.csFreeCurrencyRatesRateProvider.csRipioExchangeProvider.cs
RateSourceInfo.csServices
BTCPayServer.Tests
AltcoinTests
BTCPayServer.Tests.csprojCheckoutv2Tests.csCrowdfundTests.csExtensions.csFastTests.csFormTests.csGreenfieldAPITests.csPOSTests.csPaymentRequestTests.csSeleniumTester.csSeleniumTests.csServerTester.csTestAccount.csThirdPartyTests.csUnitTest1.csdocker-compose.altcoins.ymldocker-compose.ymlBTCPayServer
BTCPayServer.csprojProgram.csSearchString.csStorePolicies.cs_ViewImports.cshtml
Blazor
ColorPalette.csComponents
AppSales
AppTopItems
InvoiceStatus
LabelManager
MainLogo
MainNav
Notifications
QRCode
StoreLightningBalance
StoreNumbers
StoreRecentInvoices
StoreRecentTransactions
StoreSelector
StoreWalletBalance
TruncateCenter
WalletNav
Controllers
BitpayInvoiceController.cs
GreenField
GreenfieldAppsController.csGreenfieldInvoiceController.csGreenfieldLightningNodeApiController.csGreenfieldPaymentRequestsController.csGreenfieldPullPaymentController.csGreenfieldReportsController.csGreenfieldServerRolesController.csGreenfieldStoreAutomatedLightningPayoutProcessorsController.csGreenfieldStoreAutomatedOnChainPayoutProcessorsController.csGreenfieldStoreLNURLPayPaymentMethodsController.csGreenfieldStoreOnChainWalletsController.csGreenfieldStoreRolesController.csGreenfieldStoreUsersController.csGreenfieldStoreWebhooksController.csGreenfieldStoresController.csGreenfieldUsersController.csLocalBTCPayServerClient.cs
LightningAddressService.csUIAppsController.csUICustodianAccountsController.csUIHomeController.csUIInvoiceController.Testing.csUIInvoiceController.UI.csUIInvoiceController.csUILNURLController.csUIManageController.APIKeys.csUINotificationsController.csUIPaymentRequestController.csUIPublicController.csUIPullPaymentController.csUIReportsController.CheatMode.csUIReportsController.csUIServerController.Roles.csUIServerController.csUIStorePullPaymentsController.PullPayments.csUIStoresController.Email.csUIStoresController.Onchain.csUIStoresController.Roles.csUIStoresController.csUIUserStoresController.csUIVaultController.csUIWalletsController.PSBT.csUIWalletsController.csData
AddressInvoiceDataExtensions.cs
EventAggregator.csExtensions.csPayouts
StoreBlob.csStoreDataExtensions.csWebhookDataExtensions.csExtensions
FileTypeDetector.csForms
FieldValueMirror.csFormDataExtensions.csFormDataService.csHtmlFieldsetFormProvider.csHtmlInputFormProvider.csHtmlSelectFormProvider.csHtmlTextareaFormProvider.csIFormComponentProvider.csUIFormsController.cs
HostedServices
AppInventoryUpdaterHostedService.csBitpayIPNSender.csCleanupWebhookDeliveriesTask.csIPeriodicTask.csInvoiceWatcher.csPeriodicTaskLauncherHostedService.csPullPaymentHostedService.csScheduledTask.csTransactionLabelMarkerHostedService.csWebhookSender.cs
Hosting
BTCPayServerServices.csBTCpayMiddleware.csMigrationStartupTask.csStartup.csToPostgresMigrationStartupTask.cs
HwiWebSocketTransport.csModels
AccountViewModels
AppViewModels
InvoicingModels
PaymentRequestViewModels
ServerViewModels
StoreReportsViewModels
StoreViewModels
CheckoutAppearanceViewModel.csCreateStoreViewModel.csEditWebhookViewModel.csGeneralSettingsViewModel.csListStoresViewModel.csRatesViewModel.csStoreUsersViewModel.csWalletSetupRequest.cs
ViewPullPaymentModel.csWalletViewModels
PaymentRequest
Payments
PayoutProcessors
AfterPayoutActionData.csAfterPayoutFilterData.csBaseAutomatedPayoutProcessor.csBeforePayoutActionData.csBeforePayoutFilterData.csIPayoutProcessorFactory.cs
Lightning
LightningAutomatedPayoutBlob.csLightningAutomatedPayoutProcessor.csLightningAutomatedPayoutSenderFactory.csUILightningAutomatedPayoutProcessorsController.cs
OnChain
OnChainAutomatedPayoutBlob.csOnChainAutomatedPayoutProcessor.csUIOnChainAutomatedPayoutProcessorsController.cs
PayoutProcessorService.csPlugins
Crowdfund
LightningAddresssResolver.csNFC
PayButton/Controllers
PluginHookService.csPluginManager.csPointOfSale
Security
Services
Altcoins
Apps
Cheater.csDisplayFormatter.csInvoices
MigrationSettings.csNotifications
PoliciesSettings.csReportService.csReporting
FormattedAmount.csOnChainWalletReportProvider.csPaymentsReportProvider.csPayoutsReportProvider.csProductsReportProvider.csQueryContext.csReportProvider.csViewDefinition.cs
Stores
UserService.csWalletRepository.csWallets
Storage
Services/Providers/FileSystemStorage
StorageExtensions.csTagHelpers
VaultClient.csVaultHWITransport.csViews
Shared
Bitcoin
BitcoinLikeMethodCheckout-v2.cshtmlBitcoinLikeMethodCheckout.cshtmlViewBitcoinLikePaymentData.cshtml
CreateOrEditRole.cshtmlCrowdfund
Forms
LNURL
LayoutFoot.cshtmlLayoutHeadStoreBranding.cshtmlLayoutHeadTheme.cshtmlLightning
LightningLikeMethodCheckout-v2.cshtmlLightningLikeMethodCheckout.cshtmlViewLightningLikePaymentData.cshtml
ListRoles.cshtmlMonero
NFC
PayButton
PointOfSale
PosData.cshtmlPostRedirect.cshtmlShopify
ShowQR.cshtmlTemplateEditor.cshtmlVaultElements.cshtmlZcash
_BTCPaySupporters.cshtml_Footer.cshtml_Layout.cshtml_LayoutWizard.cshtml_StoreFooterLogo.cshtmlUIAccount
UIApps
UICustodianAccounts
UIForms
UIHome
UIInvoice
Checkout-Body.cshtmlCheckout-Cheating.cshtmlCheckout.cshtmlCheckoutV2.cshtmlCreateInvoice.cshtmlInvoice.cshtmlInvoiceReceipt.cshtmlInvoiceReceiptPrint.cshtmlListInvoices.cshtmlListInvoicesPaymentsPartial.cshtml_RefundModal.cshtml
UILNURL
UILightningAutomatedPayoutProcessors
UIManage
UIMoneroLikeStore
UINotifications
UIOnChainAutomatedPayoutProcessors
UIPaymentRequest
UIPayoutProcessors
UIPullPayment
UIReports
UIServer
DynamicDnsServices.cshtmlFiles.cshtmlListPlugins.cshtmlListUsers.cshtmlPolicies.cshtmlServerNavPages.cs_Nav.cshtml
UIStorePullPayments
UIStores
CheckoutAppearance.cshtmlDashboard.cshtmlGeneralSettings.cshtml
ImportWallet
ListTokens.cshtmlModifyWebhook.cshtmlStoreEmailSettings.cshtml~refs/remotes/Kukks
StoreEmails.cshtmlStoreNavPages.csStoreUsers.cshtmlWalletSettings.cshtmlWebhooks.cshtml_GenerateWalletForm.cshtml_LayoutWalletSetup.cshtml_Nav.cshtmlUIUserStores
UIWallets
wwwroot
cart
checkout-v2
checkout/js
crowdfund
img
js
copy-to-clipboard.jsdatatable.jsform-editor.jsstore-reports.jstemplate-editor.jsvaultbridge.jsvaultbridge.ui.jsvue-utils.js
light-pos
locales
main
modal
paybutton
payment-request
pos
swagger/v1
swagger.template.apps.jsonswagger.template.custodians.jsonswagger.template.invoices.jsonswagger.template.jsonswagger.template.lightning.common.jsonswagger.template.payment-requests.jsonswagger.template.payout-processors.jsonswagger.template.pull-payments.jsonswagger.template.serverinfo.jsonswagger.template.stores-payment-methods.lnurl.jsonswagger.template.stores-users.jsonswagger.template.stores-wallet.on-chain.jsonswagger.template.stores.jsonswagger.template.users.jsonswagger.template.webhooks.json
vendor
FileSaver
bootstrap-vue
bootstrap-vue.cssbootstrap-vue.jsbootstrap-vue.min.cssbootstrap-vue.min.jsbootstrap-vue.min.min.css.mapbootstrap-vue.min.min.js.map
bootstrap
decimal.js
papaparse
tom-select
vue-sanitize-directive
vue-sortable
Build
Changelog.mdREADME.mdbtcpayserver.slnbtcpayserver.sln.DotSettings
2
.github/codeql/codeql-config.yml
vendored
Normal file
2
.github/codeql/codeql-config.yml
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
paths-ignore:
|
||||
- 'BTCPayServer/wwwroot/vendor/**/*.js'
|
80
.github/workflows/codeql.yml
vendored
Normal file
80
.github/workflows/codeql.yml
vendored
Normal file
@ -0,0 +1,80 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
# Allow running tests manually. Usefull if scan failure, or need to rescan before next scheduled date.
|
||||
workflow_dispatch:
|
||||
# We scan only on a schedule for now, can uncomment the following to scan on commit or PR merge later on if deemed appropriate.
|
||||
# push:
|
||||
# branches: [ "master" ]
|
||||
# pull_request:
|
||||
# branches: [ "master" ]
|
||||
schedule:
|
||||
# Scan every Monday 06:00 UTC.
|
||||
- cron: '0 6 * * 1'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript', 'csharp' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
# Use only 'java' to analyze code written in Java, Kotlin or both
|
||||
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
|
||||
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
config-file: ./.github/codeql/codeql-config.yml
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
|
||||
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
||||
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
||||
|
||||
# - run: |
|
||||
# echo "Run, Build Application using script"
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
@ -31,7 +31,7 @@
|
||||
<None Include="icon.png" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="HtmlSanitizer" Version="5.0.372" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="8.0.723" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.9" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.9" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.7" />
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Data.Common;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
@ -21,7 +22,6 @@ namespace BTCPayServer.Abstractions.Contracts
|
||||
}
|
||||
|
||||
public abstract T CreateContext();
|
||||
|
||||
class CustomNpgsqlMigrationsSqlGenerator : NpgsqlMigrationsSqlGenerator
|
||||
{
|
||||
#pragma warning disable EF1001 // Internal EF Core API usage.
|
||||
|
@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Abstractions.Contracts
|
||||
@ -6,5 +7,8 @@ namespace BTCPayServer.Abstractions.Contracts
|
||||
{
|
||||
Task ApplyAction(string hook, object args);
|
||||
Task<object> ApplyFilter(string hook, object args);
|
||||
|
||||
event EventHandler<(string hook, object args)> ActionInvoked;
|
||||
event EventHandler<(string hook, object args)> FilterInvoked;
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,15 @@ namespace BTCPayServer.Abstractions.Extensions
|
||||
Relative
|
||||
}
|
||||
|
||||
public static void SetBlazorAllowed(this ViewDataDictionary viewData, bool allowed)
|
||||
{
|
||||
viewData["BlazorAllowed"] = allowed;
|
||||
}
|
||||
public static bool IsBlazorAllowed(this ViewDataDictionary viewData)
|
||||
{
|
||||
return viewData["BlazorAllowed"] is not false;
|
||||
}
|
||||
|
||||
public static void SetActivePage<T>(this ViewDataDictionary viewData, T activePage, string title = null, string activeId = null)
|
||||
where T : IConvertible
|
||||
{
|
||||
|
@ -69,7 +69,6 @@ public class Form
|
||||
if (!nameReturned.Add(fullName))
|
||||
{
|
||||
errors.Add($"Form contains duplicate field names '{fullName}'");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return errors.Count == 0;
|
||||
@ -86,15 +85,10 @@ public class Form
|
||||
thisPath.Add(field.Name);
|
||||
yield return (thisPath, field);
|
||||
}
|
||||
|
||||
foreach (var child in field.Fields)
|
||||
foreach (var descendant in GetAllFieldsCore(thisPath, field.Fields))
|
||||
{
|
||||
if (field.Constant)
|
||||
child.Constant = true;
|
||||
foreach (var descendant in GetAllFieldsCore(thisPath, field.Fields))
|
||||
{
|
||||
yield return descendant;
|
||||
}
|
||||
descendant.Field.Constant = field.Constant || descendant.Field.Constant;
|
||||
yield return descendant;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -111,31 +105,7 @@ public class Form
|
||||
}
|
||||
}
|
||||
|
||||
public void SetValues(JObject values)
|
||||
{
|
||||
var fields = GetAllFields().ToDictionary(k => k.FullName, k => k.Field);
|
||||
SetValues(fields, new List<string>(), values);
|
||||
}
|
||||
|
||||
private void SetValues(Dictionary<string, Field> fields, List<string> path, JObject values)
|
||||
{
|
||||
foreach (var prop in values.Properties())
|
||||
{
|
||||
List<string> propPath = new List<string>(path.Count + 1);
|
||||
propPath.AddRange(path);
|
||||
propPath.Add(prop.Name);
|
||||
if (prop.Value.Type == JTokenType.Object)
|
||||
{
|
||||
SetValues(fields, propPath, (JObject)prop.Value);
|
||||
}
|
||||
else if (prop.Value.Type == JTokenType.String)
|
||||
{
|
||||
var fullName = string.Join('_', propPath.Where(s => !string.IsNullOrEmpty(s)));
|
||||
if (fields.TryGetValue(fullName, out var f) && !f.Constant)
|
||||
f.Value = prop.Value.Value<string>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Ganss.XSS;
|
||||
using System.Web;
|
||||
using Ganss.Xss;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
||||
@ -21,6 +22,11 @@ namespace BTCPayServer.Abstractions.Services
|
||||
{
|
||||
return _htmlHelper.Raw(_htmlSanitizer.Sanitize(value));
|
||||
}
|
||||
|
||||
public IHtmlContent RawEncode(string value)
|
||||
{
|
||||
return _htmlHelper.Raw(HttpUtility.HtmlEncode(_htmlSanitizer.Sanitize(value)));
|
||||
}
|
||||
|
||||
public IHtmlContent Json(object model)
|
||||
{
|
||||
|
@ -6,31 +6,33 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BTCPayServer.Abstractions.TagHelpers;
|
||||
|
||||
[HtmlTargetElement(Attributes = nameof(Permission))]
|
||||
[HtmlTargetElement(Attributes = "[permission]")]
|
||||
[HtmlTargetElement(Attributes = "[not-permission]" )]
|
||||
public class PermissionTagHelper : TagHelper
|
||||
{
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly ILogger<PermissionTagHelper> _logger;
|
||||
|
||||
public PermissionTagHelper(IAuthorizationService authorizationService, IHttpContextAccessor httpContextAccessor, ILogger<PermissionTagHelper> logger)
|
||||
public PermissionTagHelper(IAuthorizationService authorizationService, IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_authorizationService = authorizationService;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public string Permission { get; set; }
|
||||
public string NotPermission { get; set; }
|
||||
public string PermissionResource { get; set; }
|
||||
|
||||
|
||||
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
if (string.IsNullOrEmpty(Permission))
|
||||
if (string.IsNullOrEmpty(Permission) && string.IsNullOrEmpty(NotPermission))
|
||||
return;
|
||||
if (_httpContextAccessor.HttpContext is null)
|
||||
return;
|
||||
|
||||
var key = $"{Permission}_{PermissionResource}";
|
||||
var expectedResult = !string.IsNullOrEmpty(Permission);
|
||||
var key = $"{Permission??NotPermission}_{PermissionResource}";
|
||||
if (!_httpContextAccessor.HttpContext.Items.TryGetValue(key, out var o) ||
|
||||
o is not AuthorizationResult res)
|
||||
{
|
||||
@ -39,7 +41,7 @@ public class PermissionTagHelper : TagHelper
|
||||
Permission);
|
||||
_httpContextAccessor.HttpContext.Items.Add(key, res);
|
||||
}
|
||||
if (!res.Succeeded)
|
||||
if (expectedResult != res.Succeeded)
|
||||
{
|
||||
output.SuppressOutput();
|
||||
}
|
||||
|
@ -12,9 +12,11 @@
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<RepositoryUrl>https://github.com/btcpayserver/btcpayserver</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<Configurations>Debug;Release;Altcoins-Debug;Altcoins-Release</Configurations>
|
||||
<Platforms>AnyCPU</Platforms>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<Version Condition=" '$(Version)' == '' ">1.7.2</Version>
|
||||
<Version Condition=" '$(Version)' == '' ">1.7.3</Version>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
@ -30,7 +32,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.3.21" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.24" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="icon.png" Pack="true" PackagePath="\" />
|
||||
|
@ -55,7 +55,7 @@ namespace BTCPayServer.Client
|
||||
}
|
||||
|
||||
public virtual async Task<IEnumerable<OnChainWalletTransactionData>> ShowOnChainWalletTransactions(
|
||||
string storeId, string cryptoCode, TransactionStatus[] statusFilter = null, string labelFilter = null,
|
||||
string storeId, string cryptoCode, TransactionStatus[] statusFilter = null, string labelFilter = null, int skip = 0,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var query = new Dictionary<string, object>();
|
||||
@ -67,6 +67,10 @@ namespace BTCPayServer.Client
|
||||
{
|
||||
query.Add(nameof(labelFilter), labelFilter);
|
||||
}
|
||||
if (skip != 0)
|
||||
{
|
||||
query.Add(nameof(skip), skip);
|
||||
}
|
||||
var response =
|
||||
await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions", query), token);
|
||||
|
@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
@ -11,5 +12,11 @@ namespace BTCPayServer.Client
|
||||
var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/server/info"), token);
|
||||
return await HandleResponse<ServerInfoData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<List<RoleData>> GetServerRoles(CancellationToken token = default)
|
||||
{
|
||||
using var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/server/roles"), token);
|
||||
return await HandleResponse<List<RoleData>>(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,13 @@ namespace BTCPayServer.Client
|
||||
{
|
||||
public partial class BTCPayServerClient
|
||||
{
|
||||
public virtual async Task<List<RoleData>> GetStoreRoles(string storeId,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
using var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/roles"), token);
|
||||
return await HandleResponse<List<RoleData>>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<IEnumerable<StoreUserData>> GetStoreUsers(string storeId,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
|
@ -51,7 +51,8 @@ namespace BTCPayServer.Client
|
||||
{
|
||||
if (message.StatusCode == System.Net.HttpStatusCode.UnprocessableEntity)
|
||||
{
|
||||
var err = JsonConvert.DeserializeObject<Models.GreenfieldValidationError[]>(await message.Content.ReadAsStringAsync());
|
||||
var aa = await message.Content.ReadAsStringAsync();
|
||||
var err = JsonConvert.DeserializeObject<Models.GreenfieldValidationError[]>(aa);
|
||||
throw new GreenfieldValidationException(err);
|
||||
}
|
||||
if (message.StatusCode == System.Net.HttpStatusCode.Forbidden)
|
||||
|
@ -28,6 +28,8 @@ namespace BTCPayServer.Client.Models
|
||||
public PosViewType DefaultView { get; set; }
|
||||
public bool ShowCustomAmount { get; set; } = false;
|
||||
public bool ShowDiscount { get; set; } = true;
|
||||
public bool ShowSearch { get; set; } = true;
|
||||
public bool ShowCategories { get; set; } = true;
|
||||
public bool EnableTips { get; set; } = true;
|
||||
public string CustomAmountPayButtonText { get; set; } = null;
|
||||
public string FixedAmountPayButtonText { get; set; } = null;
|
||||
@ -37,6 +39,7 @@ namespace BTCPayServer.Client.Models
|
||||
public string RedirectUrl { get; set; } = null;
|
||||
public bool? RedirectAutomatically { get; set; } = null;
|
||||
public bool? RequiresRefundEmail { get; set; } = null;
|
||||
public bool? Archived { get; set; } = null;
|
||||
public string FormId { get; set; } = null;
|
||||
public string EmbeddedCSS { get; set; } = null;
|
||||
public CheckoutType? CheckoutType { get; set; } = null;
|
||||
@ -78,6 +81,7 @@ namespace BTCPayServer.Client.Models
|
||||
public bool? DisplayPerksValue { get; set; } = null;
|
||||
public bool? DisplayPerksRanking { get; set; } = null;
|
||||
public bool? SortPerksByPopularity { get; set; } = null;
|
||||
public bool? Archived { get; set; } = null;
|
||||
public string[] Sounds { get; set; } = null;
|
||||
public string[] AnimationColors { get; set; } = null;
|
||||
}
|
||||
|
@ -1,8 +1,11 @@
|
||||
#nullable enable
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class CreatePayoutThroughStoreRequest : CreatePayoutRequest
|
||||
{
|
||||
public string? PullPaymentId { get; set; }
|
||||
public bool? Approved { get; set; }
|
||||
public JObject? Metadata { get; set; }
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
namespace BTCPayServer.Client.Models
|
||||
namespace BTCPayServer.Client.Models;
|
||||
public enum InvoiceExceptionStatus
|
||||
{
|
||||
public enum InvoiceExceptionStatus
|
||||
{
|
||||
None,
|
||||
PaidLate,
|
||||
PaidPartial,
|
||||
Marked,
|
||||
Invalid,
|
||||
PaidOver
|
||||
}
|
||||
None,
|
||||
PaidLate,
|
||||
PaidPartial,
|
||||
Marked,
|
||||
Invalid,
|
||||
PaidOver
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,8 +1,12 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class LNURLPayPaymentMethodBaseData
|
||||
{
|
||||
public bool UseBech32Scheme { get; set; }
|
||||
|
||||
[JsonProperty("lud12Enabled")]
|
||||
public bool LUD12Enabled { get; set; }
|
||||
|
||||
public LNURLPayPaymentMethodBaseData()
|
||||
|
@ -16,11 +16,12 @@ namespace BTCPayServer.Client.Models
|
||||
{
|
||||
}
|
||||
|
||||
public LNURLPayPaymentMethodData(string cryptoCode, bool enabled, bool useBech32Scheme)
|
||||
public LNURLPayPaymentMethodData(string cryptoCode, bool enabled, bool useBech32Scheme, bool lud12Enabled)
|
||||
{
|
||||
Enabled = enabled;
|
||||
CryptoCode = cryptoCode;
|
||||
UseBech32Scheme = useBech32Scheme;
|
||||
LUD12Enabled = lud12Enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,4 +10,9 @@ public class LightningAutomatedPayoutSettings
|
||||
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
|
||||
public TimeSpan IntervalSeconds { get; set; }
|
||||
|
||||
public int? CancelPayoutAfterFailures { get; set; }
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
public bool ProcessNewPayoutsInstantly { get; set; }
|
||||
|
||||
}
|
||||
|
@ -12,4 +12,8 @@ public class OnChainAutomatedPayoutSettings
|
||||
public TimeSpan IntervalSeconds { get; set; }
|
||||
|
||||
public int? FeeBlockTarget { get; set; }
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
public decimal Threshold { get; set; }
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
public bool ProcessNewPayoutsInstantly { get; set; }
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ namespace BTCPayServer.Client.Models
|
||||
public class PaymentRequestData : PaymentRequestBaseData
|
||||
{
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public PaymentRequestData.PaymentRequestStatus Status { get; set; }
|
||||
public PaymentRequestStatus Status { get; set; }
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset CreatedTime { get; set; }
|
||||
public string Id { get; set; }
|
||||
@ -16,7 +16,8 @@ namespace BTCPayServer.Client.Models
|
||||
{
|
||||
Pending = 0,
|
||||
Completed = 1,
|
||||
Expired = 2
|
||||
Expired = 2,
|
||||
Processing = 3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,5 +31,6 @@ namespace BTCPayServer.Client.Models
|
||||
public PayoutState State { get; set; }
|
||||
public int Revision { get; set; }
|
||||
public JObject PaymentProof { get; set; }
|
||||
public JObject Metadata { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,8 @@ namespace BTCPayServer.Client.Models
|
||||
public string AppType { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string StoreId { get; set; }
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public bool? Archived { get; set; }
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset Created { get; set; }
|
||||
}
|
||||
@ -19,6 +21,8 @@ namespace BTCPayServer.Client.Models
|
||||
public string DefaultView { get; set; }
|
||||
public bool ShowCustomAmount { get; set; }
|
||||
public bool ShowDiscount { get; set; }
|
||||
public bool ShowSearch { get; set; }
|
||||
public bool ShowCategories { get; set; }
|
||||
public bool EnableTips { get; set; }
|
||||
public string Currency { get; set; }
|
||||
public object Items { get; set; }
|
||||
|
@ -9,6 +9,7 @@ namespace BTCPayServer.Client.Models
|
||||
{
|
||||
RateThen,
|
||||
CurrentRate,
|
||||
OverpaidAmount,
|
||||
Fiat,
|
||||
Custom
|
||||
}
|
||||
@ -18,8 +19,13 @@ namespace BTCPayServer.Client.Models
|
||||
public string? Name { get; set; } = null;
|
||||
public string? PaymentMethod { get; set; }
|
||||
public string? Description { get; set; } = null;
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public RefundVariant? RefundVariant { get; set; }
|
||||
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal SubtractPercentage { get; set; }
|
||||
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal? CustomAmount { get; set; }
|
||||
public string? CustomCurrency { get; set; }
|
||||
|
@ -16,6 +16,8 @@ namespace BTCPayServer.Client.Models
|
||||
|
||||
public string Website { get; set; }
|
||||
|
||||
public string SupportUrl { get; set; }
|
||||
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public TimeSpan InvoiceExpiration { get; set; } = TimeSpan.FromMinutes(15);
|
||||
@ -43,6 +45,9 @@ namespace BTCPayServer.Client.Models
|
||||
public bool LazyPaymentMethods { get; set; }
|
||||
public bool RedirectAutomatically { get; set; }
|
||||
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public bool Archived { get; set; }
|
||||
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public bool ShowRecommendedFee { get; set; } = true;
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
@ -68,6 +73,17 @@ namespace BTCPayServer.Client.Models
|
||||
|
||||
public bool PayJoinEnabled { get; set; }
|
||||
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public bool? AutoDetectLanguage { get; set; }
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public bool? ShowPayInWalletButton { get; set; }
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public bool? ShowStoreHeader { get; set; }
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public bool? CelebratePayment { get; set; }
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public bool? PlaySoundOnPayment { get; set; }
|
||||
|
||||
public InvoiceData.ReceiptOptions Receipt { get; set; }
|
||||
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class StoreData : StoreBaseData
|
||||
@ -17,4 +19,12 @@ namespace BTCPayServer.Client.Models
|
||||
|
||||
public string Role { get; set; }
|
||||
}
|
||||
|
||||
public class RoleData
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public List<string> Permissions { get; set; }
|
||||
public string Role { get; set; }
|
||||
public bool IsServerRole { get; set; }
|
||||
}
|
||||
}
|
||||
|
62
BTCPayServer.Client/Models/StoreReportRequest.cs
Normal file
62
BTCPayServer.Client/Models/StoreReportRequest.cs
Normal file
@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class StoreReportRequest
|
||||
{
|
||||
public string ViewName { get; set; }
|
||||
public TimePeriod TimePeriod { get; set; }
|
||||
}
|
||||
public class StoreReportResponse
|
||||
{
|
||||
public class Field
|
||||
{
|
||||
public Field()
|
||||
{
|
||||
|
||||
}
|
||||
public Field(string name, string type)
|
||||
{
|
||||
Name = name;
|
||||
Type = type;
|
||||
}
|
||||
public string Name { get; set; }
|
||||
public string Type { get; set; }
|
||||
}
|
||||
public IList<Field> Fields { get; set; } = new List<Field>();
|
||||
public List<JArray> Data { get; set; }
|
||||
public DateTimeOffset From { get; set; }
|
||||
public DateTimeOffset To { get; set; }
|
||||
public List<ChartDefinition> Charts { get; set; }
|
||||
|
||||
public int GetIndex(string fieldName)
|
||||
{
|
||||
return Fields.ToList().FindIndex(f => f.Name == fieldName);
|
||||
}
|
||||
}
|
||||
|
||||
public class ChartDefinition
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public List<string> Groups { get; set; } = new List<string>();
|
||||
public List<string> Totals { get; set; } = new List<string>();
|
||||
public bool HasGrandTotal { get; set; }
|
||||
public List<string> Aggregates { get; set; } = new List<string>();
|
||||
public List<string> Filters { get; set; } = new List<string>();
|
||||
}
|
||||
|
||||
public class TimePeriod
|
||||
{
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset? From { get; set; }
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset? To { get; set; }
|
||||
}
|
16
BTCPayServer.Client/Models/StoreReportsResponse.cs
Normal file
16
BTCPayServer.Client/Models/StoreReportsResponse.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class StoreReportsResponse
|
||||
{
|
||||
public string ViewName { get; set; }
|
||||
public StoreReportResponse.Field[] Fields
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
@ -51,6 +51,10 @@ namespace BTCPayServer.Client.Models
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
[JsonExtensionData]
|
||||
public IDictionary<string, JToken> AdditionalData { get; set; }
|
||||
public bool IsPruned()
|
||||
{
|
||||
return DeliveryId is null;
|
||||
}
|
||||
public T ReadAs<T>()
|
||||
{
|
||||
var str = JsonConvert.SerializeObject(this, DefaultSerializerSettings);
|
||||
|
@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace BTCPayServer.Client
|
||||
{
|
||||
@ -31,6 +33,7 @@ namespace BTCPayServer.Client
|
||||
public const string CanManageUsers = "btcpay.server.canmanageusers";
|
||||
public const string CanDeleteUser = "btcpay.user.candeleteuser";
|
||||
public const string CanManagePullPayments = "btcpay.store.canmanagepullpayments";
|
||||
public const string CanArchivePullPayments = "btcpay.store.canarchivepullpayments";
|
||||
public const string CanCreatePullPayments = "btcpay.store.cancreatepullpayments";
|
||||
public const string CanCreateNonApprovedPullPayments = "btcpay.store.cancreatenonapprovedpullpayments";
|
||||
public const string CanViewCustodianAccounts = "btcpay.store.canviewcustodianaccounts";
|
||||
@ -67,6 +70,7 @@ namespace BTCPayServer.Client
|
||||
yield return CanViewLightningInvoiceInStore;
|
||||
yield return CanCreateLightningInvoiceInStore;
|
||||
yield return CanManagePullPayments;
|
||||
yield return CanArchivePullPayments;
|
||||
yield return CanCreatePullPayments;
|
||||
yield return CanCreateNonApprovedPullPayments;
|
||||
yield return CanViewCustodianAccounts;
|
||||
@ -134,7 +138,7 @@ namespace BTCPayServer.Client
|
||||
{
|
||||
static Permission()
|
||||
{
|
||||
Init();
|
||||
PolicyMap = Init();
|
||||
}
|
||||
|
||||
public static Permission Create(string policy, string scope = null)
|
||||
@ -235,11 +239,13 @@ namespace BTCPayServer.Client
|
||||
return subPolicies.Contains(subpolicy) || subPolicies.Any(s => ContainsPolicy(s, subpolicy));
|
||||
}
|
||||
|
||||
private static Dictionary<string, HashSet<string>> PolicyMap = new();
|
||||
public static ReadOnlyDictionary<string, HashSet<string>> PolicyMap { get; private set; }
|
||||
|
||||
|
||||
private static void Init()
|
||||
private static ReadOnlyDictionary<string, HashSet<string>> Init()
|
||||
{
|
||||
PolicyHasChild(Policies.CanModifyStoreSettings,
|
||||
var policyMap = new Dictionary<string, HashSet<string>>();
|
||||
PolicyHasChild(policyMap, Policies.CanModifyStoreSettings,
|
||||
Policies.CanManageCustodianAccounts,
|
||||
Policies.CanManagePullPayments,
|
||||
Policies.CanModifyInvoices,
|
||||
@ -248,25 +254,42 @@ namespace BTCPayServer.Client
|
||||
Policies.CanModifyPaymentRequests,
|
||||
Policies.CanUseLightningNodeInStore);
|
||||
|
||||
PolicyHasChild(Policies.CanManageUsers, Policies.CanCreateUser);
|
||||
PolicyHasChild(Policies.CanManagePullPayments, Policies.CanCreatePullPayments);
|
||||
PolicyHasChild(Policies.CanCreatePullPayments, Policies.CanCreateNonApprovedPullPayments);
|
||||
PolicyHasChild(Policies.CanModifyPaymentRequests, Policies.CanViewPaymentRequests);
|
||||
PolicyHasChild(Policies.CanModifyProfile, Policies.CanViewProfile);
|
||||
PolicyHasChild(Policies.CanUseLightningNodeInStore, Policies.CanViewLightningInvoiceInStore, Policies.CanCreateLightningInvoiceInStore);
|
||||
PolicyHasChild(Policies.CanManageNotificationsForUser, Policies.CanViewNotificationsForUser);
|
||||
PolicyHasChild(Policies.CanModifyServerSettings,
|
||||
PolicyHasChild(policyMap,Policies.CanManageUsers, Policies.CanCreateUser);
|
||||
PolicyHasChild(policyMap,Policies.CanManagePullPayments, Policies.CanCreatePullPayments, Policies.CanArchivePullPayments);
|
||||
PolicyHasChild(policyMap,Policies.CanCreatePullPayments, Policies.CanCreateNonApprovedPullPayments);
|
||||
PolicyHasChild(policyMap,Policies.CanModifyPaymentRequests, Policies.CanViewPaymentRequests);
|
||||
PolicyHasChild(policyMap,Policies.CanModifyProfile, Policies.CanViewProfile);
|
||||
PolicyHasChild(policyMap,Policies.CanUseLightningNodeInStore, Policies.CanViewLightningInvoiceInStore, Policies.CanCreateLightningInvoiceInStore);
|
||||
PolicyHasChild(policyMap,Policies.CanManageNotificationsForUser, Policies.CanViewNotificationsForUser);
|
||||
PolicyHasChild(policyMap,Policies.CanModifyServerSettings,
|
||||
Policies.CanUseInternalLightningNode,
|
||||
Policies.CanManageUsers);
|
||||
PolicyHasChild(Policies.CanUseInternalLightningNode, Policies.CanCreateLightningInvoiceInternalNode, Policies.CanViewLightningInvoiceInternalNode);
|
||||
PolicyHasChild(Policies.CanManageCustodianAccounts, Policies.CanViewCustodianAccounts);
|
||||
PolicyHasChild(Policies.CanModifyInvoices, Policies.CanViewInvoices, Policies.CanCreateInvoice, Policies.CanCreateLightningInvoiceInStore);
|
||||
PolicyHasChild(Policies.CanViewStoreSettings, Policies.CanViewInvoices, Policies.CanViewPaymentRequests);
|
||||
PolicyHasChild(policyMap, Policies.CanUseInternalLightningNode, Policies.CanCreateLightningInvoiceInternalNode, Policies.CanViewLightningInvoiceInternalNode);
|
||||
PolicyHasChild(policyMap, Policies.CanManageCustodianAccounts, Policies.CanViewCustodianAccounts);
|
||||
PolicyHasChild(policyMap, Policies.CanModifyInvoices, Policies.CanViewInvoices, Policies.CanCreateInvoice, Policies.CanCreateLightningInvoiceInStore);
|
||||
PolicyHasChild(policyMap, Policies.CanViewStoreSettings, Policies.CanViewInvoices, Policies.CanViewPaymentRequests);
|
||||
|
||||
var missingPolicies = Policies.AllPolicies.ToHashSet();
|
||||
//recurse through the tree to see which policies are not included in the tree
|
||||
foreach (var policy in policyMap)
|
||||
{
|
||||
missingPolicies.Remove(policy.Key);
|
||||
foreach (var subPolicy in policy.Value)
|
||||
{
|
||||
missingPolicies.Remove(subPolicy);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var missingPolicy in missingPolicies)
|
||||
{
|
||||
policyMap.Add(missingPolicy, new HashSet<string>());
|
||||
}
|
||||
return new ReadOnlyDictionary<string, HashSet<string>>(policyMap);
|
||||
}
|
||||
|
||||
private static void PolicyHasChild(string policy, params string[] subPolicies)
|
||||
private static void PolicyHasChild(Dictionary<string, HashSet<string>>policyMap, string policy, params string[] subPolicies)
|
||||
{
|
||||
if (PolicyMap.TryGetValue(policy, out var existingSubPolicies))
|
||||
if (policyMap.TryGetValue(policy, out var existingSubPolicies))
|
||||
{
|
||||
foreach (string subPolicy in subPolicies)
|
||||
{
|
||||
@ -275,7 +298,7 @@ namespace BTCPayServer.Client
|
||||
}
|
||||
else
|
||||
{
|
||||
PolicyMap.Add(policy, subPolicies.ToHashSet());
|
||||
policyMap.Add(policy, subPolicies.ToHashSet());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,7 @@ namespace BTCPayServer
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"BTG_X = BTG_BTC * BTC_X",
|
||||
"BTG_BTC = bitfinex(BTG_BTC)",
|
||||
"BTG_BTC = gate(BTG_BTC)",
|
||||
},
|
||||
CryptoImagePath = "imlegacy/btg.svg",
|
||||
LightningImagePath = "imlegacy/btg-lightning.svg",
|
||||
|
@ -17,7 +17,7 @@ namespace BTCPayServer
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"BTX_X = BTX_BTC * BTC_X",
|
||||
"BTX_BTC = hitbtc(BTX_BTC)"
|
||||
"BTX_BTC = graviex(BTX_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/bitcore.svg",
|
||||
LightningImagePath = "imlegacy/bitcore-lightning.svg",
|
||||
|
@ -1,32 +0,0 @@
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitChaincoin()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("CHC");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Chaincoin",
|
||||
BlockExplorerLink = NetworkType == ChainName.Mainnet
|
||||
? "https://explorer.chaincoin.org/Explorer/Transaction/{0}"
|
||||
: "https://test.explorer.chaincoin.org/Explorer/Transaction/tx/{0}",
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"CHC_X = CHC_BTC * BTC_X",
|
||||
"CHC_BTC = txbit(CHC_X)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/chaincoin.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
//https://github.com/satoshilabs/slips/blob/master/slip-0044.md
|
||||
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("711'")
|
||||
: new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ namespace BTCPayServer
|
||||
"USDT_X = USDT_BTC * BTC_X",
|
||||
"USDT_BTC = bitfinex(UST_BTC)",
|
||||
},
|
||||
AssetId = new uint256("ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2"),
|
||||
AssetId = NetworkType == ChainName.Regtest? null: new uint256("ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2"),
|
||||
DisplayName = "Liquid Tether",
|
||||
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://liquid.network/tx/{0}" : "https://liquid.network/testnet/tx/{0}",
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
@ -42,7 +42,7 @@ namespace BTCPayServer
|
||||
"ETB_BTC = bitpay(ETB_BTC)"
|
||||
},
|
||||
Divisibility = 2,
|
||||
AssetId = new uint256("aa775044c32a7df391902b3659f46dfe004ccb2644ce2ddc7dba31e889391caf"),
|
||||
AssetId = NetworkType == ChainName.Regtest? null: new uint256("aa775044c32a7df391902b3659f46dfe004ccb2644ce2ddc7dba31e889391caf"),
|
||||
DisplayName = "Ethiopian Birr",
|
||||
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://liquid.network/tx/{0}" : "https://liquid.network/testnet/tx/{0}",
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
@ -63,8 +63,9 @@ namespace BTCPayServer
|
||||
"LCAD_CAD = 1",
|
||||
"LCAD_X = CAD_BTC * BTC_X",
|
||||
"LCAD_BTC = bylls(CAD_BTC)",
|
||||
"CAD_BTC = LCAD_BTC"
|
||||
},
|
||||
AssetId = new uint256("0e99c1a6da379d1f4151fb9df90449d40d0608f6cb33a5bcbfc8c265f42bab0a"),
|
||||
AssetId = NetworkType == ChainName.Regtest? null: new uint256("0e99c1a6da379d1f4151fb9df90449d40d0608f6cb33a5bcbfc8c265f42bab0a"),
|
||||
DisplayName = "Liquid CAD",
|
||||
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://liquid.network/tx/{0}" : "https://liquid.network/testnet/tx/{0}",
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
|
@ -1,4 +1,5 @@
|
||||
#if ALTCOINS
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Common;
|
||||
@ -18,7 +19,8 @@ namespace BTCPayServer
|
||||
NewTransactionEvent evtOutputs)
|
||||
{
|
||||
return evtOutputs.Outputs.Where(output =>
|
||||
output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId).Select(output =>
|
||||
(output.Value is not AssetMoney && NetworkCryptoCode.Equals(evtOutputs.CryptoCode, StringComparison.InvariantCultureIgnoreCase)) ||
|
||||
(output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId)).Select(output =>
|
||||
{
|
||||
var outpoint = new OutPoint(evtOutputs.TransactionData.TransactionHash, output.Index);
|
||||
return (output, outpoint);
|
||||
@ -34,12 +36,12 @@ namespace BTCPayServer
|
||||
output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId));
|
||||
}
|
||||
|
||||
public override PaymentUrlBuilder GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue)
|
||||
public override PaymentUrlBuilder GenerateBIP21(string cryptoInfoAddress, decimal? cryptoInfoDue)
|
||||
{
|
||||
//precision 0: 10 = 0.00000010
|
||||
//precision 2: 10 = 0.00001000
|
||||
//precision 8: 10 = 10
|
||||
var money = cryptoInfoDue is null ? null : new Money(cryptoInfoDue.ToDecimal(MoneyUnit.BTC) / decimal.Parse("1".PadRight(1 + 8 - Divisibility, '0')), MoneyUnit.BTC);
|
||||
var money = cryptoInfoDue / (decimal)Math.Pow(10, 8 - Divisibility);
|
||||
var builder = base.GenerateBIP21(cryptoInfoAddress, money);
|
||||
builder.QueryParams.Add("assetid", AssetId.ToString());
|
||||
return builder;
|
||||
|
@ -45,10 +45,10 @@ namespace BTCPayServer.Services.Altcoins.Monero.RPC
|
||||
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Basic",
|
||||
Convert.ToBase64String(Encoding.Default.GetBytes($"{_username}:{_password}")));
|
||||
|
||||
var rawResult = await _httpClient.SendAsync(httpRequest, cts);
|
||||
|
||||
var rawJson = await rawResult.Content.ReadAsStringAsync();
|
||||
HttpResponseMessage rawResult = await _httpClient.SendAsync(httpRequest, cts);
|
||||
rawResult.EnsureSuccessStatusCode();
|
||||
var rawJson = await rawResult.Content.ReadAsStringAsync();
|
||||
|
||||
JsonRpcResult<TResponse> response;
|
||||
try
|
||||
{
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Common;
|
||||
@ -87,13 +88,13 @@ namespace BTCPayServer
|
||||
});
|
||||
}
|
||||
|
||||
public virtual PaymentUrlBuilder GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue)
|
||||
public virtual PaymentUrlBuilder GenerateBIP21(string cryptoInfoAddress, decimal? cryptoInfoDue)
|
||||
{
|
||||
var builder = new PaymentUrlBuilder(this.NBitcoinNetwork.UriScheme);
|
||||
builder.Host = cryptoInfoAddress;
|
||||
if (cryptoInfoDue != null && cryptoInfoDue != Money.Zero)
|
||||
if (cryptoInfoDue is not null && cryptoInfoDue.Value != 0.0m)
|
||||
{
|
||||
builder.QueryParams.Add("amount", cryptoInfoDue.ToString(false, true));
|
||||
builder.QueryParams.Add("amount", cryptoInfoDue.Value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
return builder;
|
||||
}
|
||||
|
@ -56,7 +56,6 @@ namespace BTCPayServer
|
||||
InitViacoin();
|
||||
InitMonero();
|
||||
InitZcash();
|
||||
InitChaincoin();
|
||||
// InitArgoneum();//their rate source is down 9/15/20.
|
||||
// InitMonetaryUnit(); Not supported from Bittrex from 11/23/2022, dead shitcoin
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="4.2.3" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="4.2.5" />
|
||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="2.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(Altcoins)' != 'true'">
|
||||
|
@ -64,6 +64,7 @@ namespace BTCPayServer.Data
|
||||
public DbSet<U2FDevice> U2FDevices { get; set; }
|
||||
public DbSet<Fido2Credential> Fido2Credentials { get; set; }
|
||||
public DbSet<UserStore> UserStore { get; set; }
|
||||
public DbSet<StoreRole> StoreRoles { get; set; }
|
||||
[Obsolete]
|
||||
public DbSet<WalletData> Wallets { get; set; }
|
||||
public DbSet<WalletObjectData> WalletObjects { get; set; }
|
||||
@ -129,6 +130,7 @@ namespace BTCPayServer.Data
|
||||
PayoutProcessorData.OnModelCreating(builder, Database);
|
||||
WebhookData.OnModelCreating(builder, Database);
|
||||
FormData.OnModelCreating(builder, Database);
|
||||
StoreRole.OnModelCreating(builder, Database);
|
||||
|
||||
|
||||
if (Database.IsSqlite() && !_designTime)
|
||||
|
@ -1,3 +1,6 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
@ -14,6 +14,7 @@ namespace BTCPayServer.Data
|
||||
public DateTimeOffset Created { get; set; }
|
||||
public bool TagAllInvoices { get; set; }
|
||||
public string Settings { get; set; }
|
||||
public bool Archived { get; set; }
|
||||
|
||||
internal static void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
|
@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Data;
|
||||
@ -41,4 +43,7 @@ public class LightningAddressDataBlob
|
||||
public decimal? Max { get; set; }
|
||||
|
||||
public JObject InvoiceMetadata { get; set; }
|
||||
|
||||
[JsonExtensionData] public Dictionary<string, JToken> AdditionalData { get; set; }
|
||||
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ namespace BTCPayServer.Data;
|
||||
public class AutomatedPayoutBlob
|
||||
{
|
||||
public TimeSpan Interval { get; set; } = TimeSpan.FromHours(1);
|
||||
public bool ProcessNewPayoutsInstantly { get; set; }
|
||||
}
|
||||
public class PayoutProcessorData : IHasBlobUntyped
|
||||
{
|
||||
|
@ -1,13 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using PayoutProcessorData = BTCPayServer.Data.PayoutProcessorData;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
@ -37,8 +34,6 @@ namespace BTCPayServer.Data
|
||||
|
||||
public byte[] StoreCertificate { get; set; }
|
||||
|
||||
[NotMapped] public string Role { get; set; }
|
||||
|
||||
public string StoreBlob { get; set; }
|
||||
|
||||
[Obsolete("Use GetDefaultPaymentId instead")]
|
||||
@ -52,6 +47,8 @@ namespace BTCPayServer.Data
|
||||
public IEnumerable<CustodianAccountData> CustodianAccounts { get; set; }
|
||||
public IEnumerable<StoreSettingData> Settings { get; set; }
|
||||
public IEnumerable<FormData> Forms { get; set; }
|
||||
public IEnumerable<StoreRole> StoreRoles { get; set; }
|
||||
public bool Archived { get; set; }
|
||||
|
||||
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||
{
|
||||
|
50
BTCPayServer.Data/Data/StoreRole.cs
Normal file
50
BTCPayServer.Data/Data/StoreRole.cs
Normal file
@ -0,0 +1,50 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Linq;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Data;
|
||||
|
||||
public class StoreRole
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string StoreDataId { get; set; }
|
||||
public string Role { get; set; }
|
||||
public List<string> Permissions { get; set; }
|
||||
public List<UserStore> Users { get; set; }
|
||||
public StoreData StoreData { get; set; }
|
||||
|
||||
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||
{
|
||||
builder.Entity<StoreRole>(entity =>
|
||||
{
|
||||
entity.HasOne(e => e.StoreData)
|
||||
.WithMany(s => s.StoreRoles)
|
||||
.HasForeignKey(e => e.StoreDataId)
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired(false);
|
||||
|
||||
entity.HasIndex(entity => new {entity.StoreDataId, entity.Role}).IsUnique();
|
||||
});
|
||||
|
||||
|
||||
|
||||
if (!databaseFacade.IsNpgsql())
|
||||
{
|
||||
builder.Entity<StoreRole>()
|
||||
.Property(o => o.Permissions)
|
||||
.HasConversion(
|
||||
v => JsonConvert.SerializeObject(v),
|
||||
v => JsonConvert.DeserializeObject<List<string>>(v)?? new List<string>(),
|
||||
new ValueComparer<List<string>>(
|
||||
(c1, c2) => c1 ==c2 || c1 != null && c2 != null && c1.SequenceEqual(c2),
|
||||
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
|
||||
c => c.ToList()));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
@ -9,7 +10,10 @@ namespace BTCPayServer.Data
|
||||
|
||||
public string StoreDataId { get; set; }
|
||||
public StoreData StoreData { get; set; }
|
||||
public string Role { get; set; }
|
||||
[Column("Role")]
|
||||
public string StoreRoleId { get; set; }
|
||||
public StoreRole StoreRole { get; set; }
|
||||
|
||||
|
||||
|
||||
internal static void OnModelCreating(ModelBuilder builder)
|
||||
@ -32,6 +36,10 @@ namespace BTCPayServer.Data
|
||||
.HasOne(pt => pt.StoreData)
|
||||
.WithMany(t => t.UserStores)
|
||||
.HasForeignKey(pt => pt.StoreDataId);
|
||||
|
||||
builder.Entity<UserStore>().HasOne(e => e.StoreRole)
|
||||
.WithMany(role => role.Users)
|
||||
.HasForeignKey(e => e.StoreRoleId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class WebhookDeliveryData : IHasBlobUntyped
|
||||
public class WebhookDeliveryData
|
||||
{
|
||||
[Key]
|
||||
[MaxLength(25)]
|
||||
@ -17,10 +17,8 @@ namespace BTCPayServer.Data
|
||||
|
||||
[Required]
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
[Obsolete("Use Blob2 instead")]
|
||||
public byte[] Blob { get; set; }
|
||||
public string Blob2 { get; set; }
|
||||
|
||||
public string Blob { get; set; }
|
||||
public bool Pruned { get; set; }
|
||||
|
||||
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||
{
|
||||
@ -28,11 +26,11 @@ namespace BTCPayServer.Data
|
||||
.HasOne(o => o.Webhook)
|
||||
.WithMany(a => a.Deliveries).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Entity<WebhookDeliveryData>().HasIndex(o => o.WebhookId);
|
||||
|
||||
builder.Entity<WebhookDeliveryData>().HasIndex(o => o.Timestamp);
|
||||
if (databaseFacade.IsNpgsql())
|
||||
{
|
||||
builder.Entity<WebhookDeliveryData>()
|
||||
.Property(o => o.Blob2)
|
||||
.Property(o => o.Blob)
|
||||
.HasColumnType("JSONB");
|
||||
}
|
||||
}
|
||||
|
106
BTCPayServer.Data/Migrations/20230504125505_StoreRoles.cs
Normal file
106
BTCPayServer.Data/Migrations/20230504125505_StoreRoles.cs
Normal file
@ -0,0 +1,106 @@
|
||||
using System;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20230504125505_StoreRoles")]
|
||||
public partial class StoreRoles : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
var permissionsType = migrationBuilder.IsNpgsql() ? "TEXT[]" : "TEXT";
|
||||
migrationBuilder.CreateTable(
|
||||
name: "StoreRoles",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
StoreDataId = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Role = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Permissions = table.Column<string>(type: permissionsType, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_StoreRoles", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_StoreRoles_Stores_StoreDataId",
|
||||
column: x => x.StoreDataId,
|
||||
principalTable: "Stores",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_StoreRoles_StoreDataId_Role",
|
||||
table: "StoreRoles",
|
||||
columns: new[] { "StoreDataId", "Role" },
|
||||
unique: true);
|
||||
|
||||
object GetPermissionsData(string[] permissions)
|
||||
{
|
||||
if (migrationBuilder.IsNpgsql())
|
||||
return permissions;
|
||||
return JsonConvert.SerializeObject(permissions);
|
||||
}
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
"StoreRoles",
|
||||
columns: new[] { "Id", "Role", "Permissions" },
|
||||
columnTypes: new[] { "TEXT", "TEXT", permissionsType },
|
||||
values: new object[,]
|
||||
{
|
||||
{
|
||||
"Owner", "Owner", GetPermissionsData(new[]
|
||||
{
|
||||
"btcpay.store.canmodifystoresettings",
|
||||
"btcpay.store.cantradecustodianaccount",
|
||||
"btcpay.store.canwithdrawfromcustodianaccount",
|
||||
"btcpay.store.candeposittocustodianaccount"
|
||||
})
|
||||
},
|
||||
{
|
||||
"Guest", "Guest", GetPermissionsData(new[]
|
||||
{
|
||||
"btcpay.store.canviewstoresettings",
|
||||
"btcpay.store.canmodifyinvoices",
|
||||
"btcpay.store.canviewcustodianaccounts",
|
||||
"btcpay.store.candeposittocustodianaccount"
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
if (this.SupportAddForeignKey(migrationBuilder.ActiveProvider))
|
||||
{
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_UserStore_StoreRoles_Role",
|
||||
table: "UserStore",
|
||||
column: "Role",
|
||||
principalTable: "StoreRoles",
|
||||
principalColumn: "Id");
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
if (this.SupportDropForeignKey(migrationBuilder.ActiveProvider))
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_UserStore_StoreRoles_Role",
|
||||
table: "UserStore");
|
||||
}
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "StoreRoles");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
using System;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20230529135505_WebhookDeliveriesCleanup")]
|
||||
public partial class WebhookDeliveriesCleanup : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
if (migrationBuilder.IsNpgsql())
|
||||
{
|
||||
migrationBuilder.Sql("DROP TABLE IF EXISTS \"InvoiceWebhookDeliveries\", \"WebhookDeliveries\";");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "WebhookDeliveries",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
WebhookId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Timestamp = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
Pruned = table.Column<bool>(type: "BOOLEAN", nullable: false),
|
||||
Blob = table.Column<string>(type: "JSONB", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_WebhookDeliveries", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_WebhookDeliveries_Webhooks_WebhookId",
|
||||
column: x => x.WebhookId,
|
||||
principalTable: "Webhooks",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_WebhookDeliveries_WebhookId",
|
||||
table: "WebhookDeliveries",
|
||||
column: "WebhookId");
|
||||
migrationBuilder.Sql("CREATE INDEX \"IX_WebhookDeliveries_Timestamp\" ON \"WebhookDeliveries\"(\"Timestamp\") WHERE \"Pruned\" IS FALSE");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "InvoiceWebhookDeliveries",
|
||||
columns: table => new
|
||||
{
|
||||
InvoiceId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
DeliveryId = table.Column<string>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_InvoiceWebhookDeliveries", x => new { x.InvoiceId, x.DeliveryId });
|
||||
table.ForeignKey(
|
||||
name: "FK_InvoiceWebhookDeliveries_WebhookDeliveries_DeliveryId",
|
||||
column: x => x.DeliveryId,
|
||||
principalTable: "WebhookDeliveries",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_InvoiceWebhookDeliveries_Invoices_InvoiceId",
|
||||
column: x => x.InvoiceId,
|
||||
principalTable: "Invoices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20230906135844_AddArchivedFlagForStoresAndApps")]
|
||||
public partial class AddArchivedFlagForStoresAndApps : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "Archived",
|
||||
table: "Stores",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "Archived",
|
||||
table: "Apps",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Archived",
|
||||
table: "Stores");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Archived",
|
||||
table: "Apps");
|
||||
}
|
||||
}
|
||||
}
|
@ -79,6 +79,9 @@ namespace BTCPayServer.Migrations
|
||||
b.Property<string>("AppType")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Archived")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset>("Created")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@ -214,56 +217,6 @@ namespace BTCPayServer.Migrations
|
||||
b.ToTable("CustodianAccount");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.FormData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Config")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Public")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("StoreId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreId");
|
||||
|
||||
b.ToTable("Forms");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PayoutProcessorData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<byte[]>("Blob")
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<string>("PaymentMethod")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Processor")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("StoreId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreId");
|
||||
|
||||
b.ToTable("PayoutProcessors");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.Fido2Credential", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
@ -292,6 +245,31 @@ namespace BTCPayServer.Migrations
|
||||
b.ToTable("Fido2Credentials");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.FormData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Config")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Public")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("StoreId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreId");
|
||||
|
||||
b.ToTable("Forms");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
@ -655,6 +633,34 @@ namespace BTCPayServer.Migrations
|
||||
b.ToTable("Payouts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PayoutProcessorData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<byte[]>("Blob")
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<string>("Blob2")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PaymentMethod")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Processor")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("StoreId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreId");
|
||||
|
||||
b.ToTable("PayoutProcessors");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
@ -748,6 +754,9 @@ namespace BTCPayServer.Migrations
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Archived")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("DefaultCrypto")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@ -802,6 +811,28 @@ namespace BTCPayServer.Migrations
|
||||
b.ToTable("Files");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoreRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Permissions")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Role")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("StoreDataId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreDataId", "Role")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("StoreRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoreSettingData", b =>
|
||||
{
|
||||
b.Property<string>("StoreId")
|
||||
@ -878,13 +909,16 @@ namespace BTCPayServer.Migrations
|
||||
b.Property<string>("StoreDataId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Role")
|
||||
.HasColumnType("TEXT");
|
||||
b.Property<string>("StoreRoleId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("Role");
|
||||
|
||||
b.HasKey("ApplicationUserId", "StoreDataId");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.HasIndex("StoreRoleId");
|
||||
|
||||
b.ToTable("UserStore");
|
||||
});
|
||||
|
||||
@ -991,12 +1025,12 @@ namespace BTCPayServer.Migrations
|
||||
.HasMaxLength(25)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<byte[]>("Blob")
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<string>("Blob2")
|
||||
b.Property<string>("Blob")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Pruned")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset>("Timestamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@ -1007,6 +1041,8 @@ namespace BTCPayServer.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Timestamp");
|
||||
|
||||
b.HasIndex("WebhookId");
|
||||
|
||||
b.ToTable("WebhookDeliveries");
|
||||
@ -1188,26 +1224,6 @@ namespace BTCPayServer.Migrations
|
||||
b.Navigation("StoreData");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.FormData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "Store")
|
||||
.WithMany("Forms")
|
||||
.HasForeignKey("StoreId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.Navigation("Store");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PayoutProcessorData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "Store")
|
||||
.WithMany("PayoutProcessors")
|
||||
.HasForeignKey("StoreId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.Navigation("Store");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.Fido2Credential", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser")
|
||||
@ -1218,6 +1234,16 @@ namespace BTCPayServer.Migrations
|
||||
b.Navigation("ApplicationUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.FormData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "Store")
|
||||
.WithMany("Forms")
|
||||
.HasForeignKey("StoreId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.Navigation("Store");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
@ -1343,6 +1369,16 @@ namespace BTCPayServer.Migrations
|
||||
b.Navigation("StoreData");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PayoutProcessorData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "Store")
|
||||
.WithMany("PayoutProcessors")
|
||||
.HasForeignKey("StoreId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.Navigation("Store");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
@ -1392,6 +1428,16 @@ namespace BTCPayServer.Migrations
|
||||
b.Navigation("ApplicationUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoreRole", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("StoreRoles")
|
||||
.HasForeignKey("StoreDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.Navigation("StoreData");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoreSettingData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "Store")
|
||||
@ -1446,9 +1492,15 @@ namespace BTCPayServer.Migrations
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("BTCPayServer.Data.StoreRole", "StoreRole")
|
||||
.WithMany("Users")
|
||||
.HasForeignKey("StoreRoleId");
|
||||
|
||||
b.Navigation("ApplicationUser");
|
||||
|
||||
b.Navigation("StoreData");
|
||||
|
||||
b.Navigation("StoreRole");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WalletObjectLinkData", b =>
|
||||
@ -1606,9 +1658,16 @@ namespace BTCPayServer.Migrations
|
||||
|
||||
b.Navigation("Settings");
|
||||
|
||||
b.Navigation("StoreRoles");
|
||||
|
||||
b.Navigation("UserStores");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoreRole", b =>
|
||||
{
|
||||
b.Navigation("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WalletData", b =>
|
||||
{
|
||||
b.Navigation("WalletTransactions");
|
||||
|
@ -1,37 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace BTCPayServer.Rating
|
||||
{
|
||||
public enum RateSource
|
||||
{
|
||||
Coingecko,
|
||||
Direct
|
||||
}
|
||||
public class AvailableRateProvider
|
||||
{
|
||||
public string Name { get; }
|
||||
public string Url { get; }
|
||||
public string Id { get; }
|
||||
public RateSource Source { get; }
|
||||
|
||||
public AvailableRateProvider(string id, string name, string url) : this(id, name, url, RateSource.Direct)
|
||||
{
|
||||
|
||||
}
|
||||
public AvailableRateProvider(string id, string name, string url, RateSource source)
|
||||
{
|
||||
Id = id;
|
||||
Name = name;
|
||||
Url = url;
|
||||
Source = source;
|
||||
}
|
||||
|
||||
public string DisplayName =>
|
||||
Source switch
|
||||
{
|
||||
RateSource.Direct => Name,
|
||||
RateSource.Coingecko => $"{Name} (via CoinGecko)",
|
||||
_ => throw new NotSupportedException(Source.ToString())
|
||||
};
|
||||
}
|
||||
}
|
@ -7,8 +7,8 @@
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" />
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.9" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.24" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="1.0.2" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="1.0.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -20,13 +20,13 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
public class CurrencyNameTable
|
||||
{
|
||||
public static CurrencyNameTable Instance = new CurrencyNameTable();
|
||||
public static CurrencyNameTable Instance = new();
|
||||
public CurrencyNameTable()
|
||||
{
|
||||
_Currencies = LoadCurrency().ToDictionary(k => k.Code);
|
||||
_Currencies = LoadCurrency().ToDictionary(k => k.Code, StringComparer.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
static readonly Dictionary<string, IFormatProvider> _CurrencyProviders = new Dictionary<string, IFormatProvider>();
|
||||
static readonly Dictionary<string, IFormatProvider> _CurrencyProviders = new();
|
||||
|
||||
public NumberFormatInfo GetNumberFormatInfo(string currency, bool useFallback)
|
||||
{
|
||||
|
@ -19,7 +19,7 @@ namespace BTCPayServer.Rating
|
||||
public static CurrencyPair Parse(string str)
|
||||
{
|
||||
if (!TryParse(str, out var result))
|
||||
throw new FormatException("Invalid currency pair");
|
||||
throw new FormatException($"Invalid currency pair ({str})");
|
||||
return result;
|
||||
}
|
||||
public static bool TryParse(string str, out CurrencyPair value)
|
||||
|
@ -15,7 +15,7 @@ namespace BTCPayServer.Rating
|
||||
while (true)
|
||||
{
|
||||
var rounded = decimal.Round(value, divisibility, MidpointRounding.AwayFromZero);
|
||||
if ((Math.Abs(rounded - value) / value) < 0.001m)
|
||||
if ((Math.Abs(rounded - value) / value) < 0.01m)
|
||||
{
|
||||
value = rounded;
|
||||
break;
|
||||
|
@ -1,29 +0,0 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class ArgoneumRateProvider : IRateProvider
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
public ArgoneumRateProvider(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient ?? new HttpClient();
|
||||
}
|
||||
|
||||
public RateSourceInfo RateSourceInfo => new("argoneum", "Argoneum", "https://rates.argoneum.net/rates");
|
||||
|
||||
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// Example result: AGM to BTC rate: {"agm":5000000.000000}
|
||||
var response = await _httpClient.GetAsync("https://rates.argoneum.net/rates/btc", cancellationToken);
|
||||
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
|
||||
var value = jobj["agm"].Value<decimal>();
|
||||
return new[] { new PairRate(new CurrencyPair("BTC", "AGM"), new BidAsk(value)) };
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,36 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
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");
|
||||
private readonly HttpClient _httpClient;
|
||||
public FreeCurrencyRatesRateProvider(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient ?? new HttpClient();
|
||||
}
|
||||
|
||||
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await _httpClient.GetAsync(RateSourceInfo.Url, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
|
||||
var results = (JObject) jobj["btc"] ;
|
||||
//key value is currency code to rate value
|
||||
var list = new List<PairRate>();
|
||||
foreach (var item in results)
|
||||
{
|
||||
string name = item.Key;
|
||||
var value = item.Value.Value<decimal>();
|
||||
list.Add(new PairRate(new CurrencyPair("BTC", name), new BidAsk(value)));
|
||||
}
|
||||
|
||||
return list.ToArray();
|
||||
}
|
||||
}
|
@ -13,7 +13,7 @@ namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class RipioExchangeProvider : IRateProvider
|
||||
{
|
||||
public RateSourceInfo RateSourceInfo => new("ripio", "Ripio", "https://api.exchange.ripio.com/api/v1/rate/all/");
|
||||
public RateSourceInfo RateSourceInfo => new("ripio", "Ripio", "https://api.ripiotrade.co/v4/public/tickers");
|
||||
private readonly HttpClient _httpClient;
|
||||
public RipioExchangeProvider(HttpClient httpClient)
|
||||
{
|
||||
@ -21,9 +21,9 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await _httpClient.GetAsync("https://api.exchange.ripio.com/api/v1/rate/all/", cancellationToken);
|
||||
var response = await _httpClient.GetAsync("https://api.ripiotrade.co/v4/public/tickers", cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var jarray = (JArray)(await response.Content.ReadAsAsync<JArray>(cancellationToken));
|
||||
var jarray = (JArray)(await response.Content.ReadAsAsync<JObject>(cancellationToken))["data"];
|
||||
return jarray
|
||||
.Children<JObject>()
|
||||
.Select(jobj => ParsePair(jobj))
|
||||
|
@ -1,21 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Rating
|
||||
#nullable enable
|
||||
namespace BTCPayServer.Rating;
|
||||
public enum RateSource
|
||||
{
|
||||
public class RateSourceInfo
|
||||
{
|
||||
public RateSourceInfo(string id, string displayName, string url)
|
||||
{
|
||||
Id = id;
|
||||
DisplayName = displayName;
|
||||
Url = url;
|
||||
}
|
||||
public string Id { get; set; }
|
||||
public string DisplayName { get; set; }
|
||||
public string Url { get; set; }
|
||||
}
|
||||
Coingecko,
|
||||
Direct
|
||||
}
|
||||
public record RateSourceInfo(string Id, string DisplayName, string Url, RateSource Source = RateSource.Direct);
|
||||
|
@ -85,14 +85,13 @@ namespace BTCPayServer.Services.Rates
|
||||
bgFetcher.RefreshRate = TimeSpan.FromMinutes(1.0);
|
||||
bgFetcher.ValidatyTime = TimeSpan.FromMinutes(5.0);
|
||||
Providers.Add(supportedExchange.Id, bgFetcher);
|
||||
var rsi = coingecko.RateSourceInfo;
|
||||
AvailableRateProviders.Add(new(rsi.Id, rsi.DisplayName, rsi.Url, RateSource.Coingecko));
|
||||
AvailableRateProviders.Add(coingecko.RateSourceInfo);
|
||||
}
|
||||
}
|
||||
AvailableRateProviders.Sort((a, b) => StringComparer.Ordinal.Compare(a.DisplayName, b.DisplayName));
|
||||
}
|
||||
|
||||
public List<AvailableRateProvider> AvailableRateProviders { get; } = new List<AvailableRateProvider>();
|
||||
public List<RateSourceInfo> AvailableRateProviders { get; } = new List<RateSourceInfo>();
|
||||
|
||||
public async Task<QueryRateResult> QueryRates(string exchangeName, CancellationToken cancellationToken)
|
||||
{
|
||||
|
@ -6,6 +6,7 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Hosting;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
@ -245,7 +246,7 @@ namespace BTCPayServer.Tests
|
||||
await tester.EnsureChannelsSetup();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess(true);
|
||||
user.RegisterLightningNode("BTC", LightningConnectionType.Charge);
|
||||
user.RegisterLightningNode("BTC");
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
user.RegisterDerivationScheme("LTC");
|
||||
|
||||
@ -651,6 +652,7 @@ donation:
|
||||
price: 1.02
|
||||
custom: true
|
||||
";
|
||||
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
|
||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
Assert.Equal("hello", vmpos.Title);
|
||||
@ -661,14 +663,13 @@ donation:
|
||||
Assert.Equal(3, vmview.Items.Length);
|
||||
Assert.Equal("good apple", vmview.Items[0].Title);
|
||||
Assert.Equal("orange", vmview.Items[1].Title);
|
||||
Assert.Equal(10.0m, vmview.Items[1].Price.Value);
|
||||
Assert.Equal("$5.00", vmview.Items[0].Price.Formatted);
|
||||
Assert.Equal(10.0m, vmview.Items[1].Price);
|
||||
Assert.Equal("{0} Purchase", vmview.ButtonText);
|
||||
Assert.Equal("Nicolas Sexy Hair", vmview.CustomButtonText);
|
||||
Assert.Equal("Wanna tip?", vmview.CustomTipText);
|
||||
Assert.Equal("15,18,20", string.Join(',', vmview.CustomTipPercentages));
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, null, null, null, null, "orange").Result);
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, choiceKey: "orange").Result);
|
||||
|
||||
//
|
||||
var invoices = await user.BitPay.GetInvoicesAsync();
|
||||
@ -677,18 +678,18 @@ donation:
|
||||
Assert.Equal("CAD", orangeInvoice.Currency);
|
||||
Assert.Equal("orange", orangeInvoice.ItemDesc);
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, null, null, null, null, "apple").Result);
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, choiceKey: "apple").Result);
|
||||
|
||||
invoices = user.BitPay.GetInvoices();
|
||||
invoices = await user.BitPay.GetInvoicesAsync();
|
||||
var appleInvoice = invoices.SingleOrDefault(invoice => invoice.ItemCode.Equals("apple"));
|
||||
Assert.NotNull(appleInvoice);
|
||||
Assert.Equal("good apple", appleInvoice.ItemDesc);
|
||||
|
||||
// testing custom amount
|
||||
var action = Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 6.6m, null, null, null, null, "donation").Result);
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 6.6m, choiceKey: "donation").Result);
|
||||
Assert.Equal(nameof(UIInvoiceController.Checkout), action.ActionName);
|
||||
invoices = user.BitPay.GetInvoices();
|
||||
invoices = await user.BitPay.GetInvoicesAsync();
|
||||
var donationInvoice = invoices.Single(i => i.Price == 6.6m);
|
||||
Assert.NotNull(donationInvoice);
|
||||
Assert.Equal("CAD", donationInvoice.Currency);
|
||||
@ -723,6 +724,7 @@ donation:
|
||||
price: 1.02
|
||||
custom: true
|
||||
";
|
||||
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
|
||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
publicApps = user.GetController<UIPointOfSaleController>();
|
||||
vmview = await publicApps.ViewPointOfSale(app.Id, PosViewType.Cart).AssertViewModelAsync<ViewPointOfSaleViewModel>();
|
||||
@ -750,26 +752,28 @@ inventoryitem:
|
||||
inventory: 1
|
||||
noninventoryitem:
|
||||
price: 10.0";
|
||||
|
||||
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
|
||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
|
||||
//inventoryitem has 1 item available
|
||||
await tester.WaitForEvent<AppInventoryUpdaterHostedService.UpdateAppInventory>(() =>
|
||||
{
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, null, null, null, null, "inventoryitem").Result);
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "inventoryitem").Result);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
//we already bought all available stock so this should fail
|
||||
await Task.Delay(100);
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, null, null, null, null, "inventoryitem").Result);
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "inventoryitem").Result);
|
||||
|
||||
//inventoryitem has unlimited items available
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, null, null, null, null, "noninventoryitem").Result);
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "noninventoryitem").Result);
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, null, null, null, null, "noninventoryitem").Result);
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "noninventoryitem").Result);
|
||||
|
||||
//verify invoices where created
|
||||
invoices = user.BitPay.GetInvoices();
|
||||
@ -780,15 +784,13 @@ noninventoryitem:
|
||||
|
||||
//let's mark the inventoryitem invoice as invalid, this should return the item to back in stock
|
||||
var controller = tester.PayTester.GetController<UIInvoiceController>(user.UserId, user.StoreId);
|
||||
var appService = tester.PayTester.GetService<AppService>();
|
||||
var eventAggregator = tester.PayTester.GetService<EventAggregator>();
|
||||
Assert.IsType<JsonResult>(await controller.ChangeInvoiceState(inventoryItemInvoice.Id, "invalid"));
|
||||
//check that item is back in stock
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
Assert.Equal(1,
|
||||
appService.Parse(vmpos.Template, "BTC").Single(item => item.Id == "inventoryitem").Inventory);
|
||||
AppService.Parse(vmpos.Template).Single(item => item.Id == "inventoryitem").Inventory);
|
||||
}, 10000);
|
||||
|
||||
//test payment methods option
|
||||
@ -803,11 +805,13 @@ btconly:
|
||||
- BTC
|
||||
normal:
|
||||
price: 1.0";
|
||||
|
||||
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
|
||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, null, null, null, null, "btconly").Result);
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "btconly").Result);
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, null, null, null, null, "normal").Result);
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "normal").Result);
|
||||
invoices = user.BitPay.GetInvoices();
|
||||
var normalInvoice = invoices.Single(invoice => invoice.ItemCode == "normal");
|
||||
var btcOnlyInvoice = invoices.Single(invoice => invoice.ItemCode == "btconly");
|
||||
@ -847,20 +851,21 @@ g:
|
||||
custom: topup
|
||||
";
|
||||
|
||||
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
|
||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
Assert.DoesNotContain("custom", vmpos.Template);
|
||||
var items = appService.Parse(vmpos.Template, vmpos.Currency);
|
||||
Assert.Contains(items, item => item.Id == "a" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed);
|
||||
Assert.Contains(items, item => item.Id == "b" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed);
|
||||
Assert.Contains(items, item => item.Id == "c" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Minimum);
|
||||
Assert.Contains(items, item => item.Id == "d" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed);
|
||||
Assert.Contains(items, item => item.Id == "e" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Minimum);
|
||||
Assert.Contains(items, item => item.Id == "f" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup);
|
||||
Assert.Contains(items, item => item.Id == "g" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup);
|
||||
|
||||
var items = AppService.Parse(vmpos.Template);
|
||||
Assert.Contains(items, item => item.Id == "a" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed);
|
||||
Assert.Contains(items, item => item.Id == "b" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed);
|
||||
Assert.Contains(items, item => item.Id == "c" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Minimum);
|
||||
Assert.Contains(items, item => item.Id == "d" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed);
|
||||
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, null, null, null, null, null, "g").Result);
|
||||
.ViewPointOfSale(app.Id, PosViewType.Static, choiceKey: "g").Result);
|
||||
invoices = user.BitPay.GetInvoices();
|
||||
var topupInvoice = invoices.Single(invoice => invoice.ItemCode == "g");
|
||||
Assert.Equal(0, topupInvoice.Price);
|
||||
|
@ -52,11 +52,12 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
tester.ActivateLBTC();
|
||||
await tester.StartAsync();
|
||||
|
||||
//https://github.com/ElementsProject/elements/issues/956
|
||||
await tester.LBTCExplorerNode.SendCommandAsync("rescanblockchain");
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("LBTC");
|
||||
user.RegisterDerivationScheme("USDT");
|
||||
user.RegisterDerivationScheme("ETB");
|
||||
await user.GrantAccessAsync();
|
||||
|
||||
await tester.LBTCExplorerNode.GenerateAsync(4);
|
||||
//no tether on our regtest, lets create it and set it
|
||||
var tether = tester.NetworkProvider.GetNetwork<ElementsBTCPayNetwork>("USDT");
|
||||
@ -75,6 +76,10 @@ namespace BTCPayServer.Tests
|
||||
.AssetId = etb.AssetId;
|
||||
|
||||
|
||||
user.RegisterDerivationScheme("LBTC");
|
||||
user.RegisterDerivationScheme("USDT");
|
||||
user.RegisterDerivationScheme("ETB");
|
||||
|
||||
//test: register 2 assets on the same elements network and make sure paying an invoice on one does not affect the other in any way
|
||||
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(0.1m, "BTC"));
|
||||
Assert.Equal(3, invoice.SupportedTransactionCurrencies.Count);
|
||||
@ -82,7 +87,7 @@ namespace BTCPayServer.Tests
|
||||
//1 lbtc = 1 btc
|
||||
Assert.Equal(1, ci.Rate);
|
||||
var star = await tester.LBTCExplorerNode.SendCommandAsync("sendtoaddress", ci.Address, ci.Due, "", "", false, true,
|
||||
1, "UNSET", lbtc.AssetId);
|
||||
1, "UNSET",false, lbtc.AssetId.ToString());
|
||||
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
@ -95,8 +100,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
ci = invoice.CryptoInfo.Single(info => info.CryptoCode.Equals("USDT"));
|
||||
Assert.Equal(3, invoice.SupportedTransactionCurrencies.Count);
|
||||
star = await tester.LBTCExplorerNode.SendCommandAsync("sendtoaddress", ci.Address, ci.Due, "", "", false, true,
|
||||
1, "UNSET", tether.AssetId);
|
||||
star = tester.LBTCExplorerNode.SendCommand("sendtoaddress", ci.Address, decimal.Parse(ci.Due), "x", "z", false, true, 1, "unset", false, tether.AssetId.ToString());
|
||||
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
|
@ -23,7 +23,7 @@
|
||||
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14" />
|
||||
<PackageReference Include="Selenium.Support" Version="4.1.1" />
|
||||
<PackageReference Include="Selenium.WebDriver" Version="4.1.1" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="112.0.5615.4900" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="119.0.6045.10500" />
|
||||
<PackageReference Include="xunit" Version="2.4.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
@ -1,13 +1,9 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using BTCPayServer.Views.Stores;
|
||||
using NBitcoin;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Support.Extensions;
|
||||
using OpenQA.Selenium.Support.UI;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
@ -40,8 +36,10 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// Configure store url
|
||||
var storeUrl = "https://satoshisteaks.com/";
|
||||
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);
|
||||
|
||||
@ -140,8 +138,47 @@ namespace BTCPayServer.Tests
|
||||
var expiredSection = s.Driver.FindElement(By.Id("unpaid"));
|
||||
Assert.True(expiredSection.Displayed);
|
||||
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("receipt-btn")));
|
||||
Assert.True(s.Driver.ElementDoesNotExist(By.Id("ContactLink")));
|
||||
Assert.True(s.Driver.ElementDoesNotExist(By.Id("ReceiptLink")));
|
||||
Assert.Equal(storeUrl, s.Driver.FindElement(By.Id("StoreLink")).GetAttribute("href"));
|
||||
|
||||
// Expire paid partial
|
||||
s.GoToHome();
|
||||
invoiceId = s.CreateInvoice(2100, "EUR");
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
|
||||
|
||||
await Task.Delay(200);
|
||||
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
|
||||
var amountFraction = "0.00001";
|
||||
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(address, Network.RegTest),
|
||||
Money.Parse(amountFraction));
|
||||
await s.Server.ExplorerNode.GenerateAsync(1);
|
||||
|
||||
expirySeconds = s.Driver.FindElement(By.Id("ExpirySeconds"));
|
||||
expirySeconds.Clear();
|
||||
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 expiredSection = s.Driver.FindElement(By.Id("unpaid"));
|
||||
Assert.True(expiredSection.Displayed);
|
||||
Assert.Contains("Invoice Expired", expiredSection.Text);
|
||||
Assert.Contains("This invoice expired with partial payment", expiredSection.Text);
|
||||
Assert.DoesNotContain("resubmit a payment", expiredSection.Text);
|
||||
});
|
||||
var contactLink = s.Driver.FindElement(By.Id("ContactLink"));
|
||||
Assert.Equal("Contact us", contactLink.Text);
|
||||
Assert.Matches(supportUrl.Replace("{InvoiceId}", invoiceId), contactLink.GetAttribute("href"));
|
||||
Assert.True(s.Driver.ElementDoesNotExist(By.Id("ReceiptLink")));
|
||||
Assert.Equal(storeUrl, s.Driver.FindElement(By.Id("StoreLink")).GetAttribute("href"));
|
||||
|
||||
// Test payment
|
||||
@ -166,7 +203,7 @@ namespace BTCPayServer.Tests
|
||||
// Pay partial amount
|
||||
await Task.Delay(200);
|
||||
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
|
||||
var amountFraction = "0.00001";
|
||||
amountFraction = "0.00001";
|
||||
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(address, Network.RegTest),
|
||||
Money.Parse(amountFraction));
|
||||
await s.Server.ExplorerNode.GenerateAsync(1);
|
||||
@ -210,7 +247,8 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("Invoice Paid", settledSection.Text);
|
||||
});
|
||||
s.Driver.FindElement(By.Id("confetti"));
|
||||
s.Driver.FindElement(By.Id("receipt-btn"));
|
||||
s.Driver.FindElement(By.Id("ReceiptLink"));
|
||||
Assert.True(s.Driver.ElementDoesNotExist(By.Id("ContactLink")));
|
||||
Assert.Equal(storeUrl, s.Driver.FindElement(By.Id("StoreLink")).GetAttribute("href"));
|
||||
|
||||
// BIP21
|
||||
@ -358,6 +396,7 @@ namespace BTCPayServer.Tests
|
||||
s.GoToHome();
|
||||
s.GoToLightningSettings();
|
||||
s.Driver.SetCheckbox(By.Id("LNURLEnabled"), false);
|
||||
s.Driver.ScrollTo(By.Id("save"));
|
||||
s.Driver.FindElement(By.Id("save")).Click();
|
||||
Assert.Contains("BTC Lightning settings successfully updated", s.FindAlertMessage().Text);
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
@ -53,13 +54,33 @@ namespace BTCPayServer.Tests
|
||||
Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps2.ListApps(user2.StoreId).Result).Model);
|
||||
Assert.Single(appList.Apps);
|
||||
Assert.Empty(appList2.Apps);
|
||||
Assert.Equal("test", appList.Apps[0].AppName);
|
||||
Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id);
|
||||
Assert.True(appList.Apps[0].IsOwner);
|
||||
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
|
||||
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id));
|
||||
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id));
|
||||
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result);
|
||||
Assert.Equal("test", app.AppName);
|
||||
Assert.Equal(apps.CreatedAppId, app.Id);
|
||||
Assert.True(app.Role.ToPermissionSet(app.StoreId).Contains(Policies.CanModifyStoreSettings, app.StoreId));
|
||||
Assert.Equal(user.StoreId, app.StoreId);
|
||||
// Archive
|
||||
redirect = Assert.IsType<RedirectResult>(apps.ToggleArchive(app.Id).Result);
|
||||
Assert.EndsWith("/settings/crowdfund", redirect.Url);
|
||||
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||
Assert.Empty(appList.Apps);
|
||||
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId, archived: true).Result).Model);
|
||||
app = appList.Apps[0];
|
||||
Assert.True(app.Archived);
|
||||
Assert.IsType<NotFoundResult>(await crowdfund.ViewCrowdfund(app.Id));
|
||||
// Unarchive
|
||||
redirect = Assert.IsType<RedirectResult>(apps.ToggleArchive(app.Id).Result);
|
||||
Assert.EndsWith("/settings/crowdfund", redirect.Url);
|
||||
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||
app = appList.Apps[0];
|
||||
Assert.False(app.Archived);
|
||||
var crowdfundViewModel = await crowdfund.UpdateCrowdfund(app.Id).AssertViewModelAsync<UpdateCrowdfundViewModel>();
|
||||
crowdfundViewModel.Enabled = true;
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
Assert.IsType<ViewResult>(await crowdfund.ViewCrowdfund(app.Id));
|
||||
// Delete
|
||||
Assert.IsType<NotFoundResult>(apps2.DeleteApp(app.Id));
|
||||
Assert.IsType<ViewResult>(apps.DeleteApp(app.Id));
|
||||
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(app.Id).Result);
|
||||
Assert.Equal(nameof(UIStoresController.Dashboard), redirectToAction.ActionName);
|
||||
appList = await apps.ListApps(user.StoreId).AssertViewModelAsync<ListAppsViewModel>();
|
||||
Assert.Empty(appList.Apps);
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@ -122,6 +123,13 @@ retry:
|
||||
driver.ExecuteJavaScript($"document.getElementById('{element}').{funcName}()");
|
||||
}
|
||||
|
||||
public static void WaitWalletTransactionsLoaded(this IWebDriver driver)
|
||||
{
|
||||
var wait = new WebDriverWait(driver, SeleniumTester.ImplicitWait);
|
||||
wait.UntilJsIsReady();
|
||||
wait.Until(d => d.WaitForElement(By.CssSelector("#WalletTransactions[data-loaded='true']")));
|
||||
}
|
||||
|
||||
public static IWebElement WaitForElement(this IWebDriver driver, By selector)
|
||||
{
|
||||
var wait = new WebDriverWait(driver, SeleniumTester.ImplicitWait);
|
||||
@ -189,6 +197,7 @@ retry:
|
||||
driver.FindElement(selector).Click();
|
||||
}
|
||||
|
||||
[DebuggerHidden]
|
||||
public static bool ElementDoesNotExist(this IWebDriver driver, By selector)
|
||||
{
|
||||
Assert.Throws<NoSuchElementException>(() =>
|
||||
|
@ -23,9 +23,11 @@ using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Labels;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Validation;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
@ -345,165 +347,272 @@ namespace BTCPayServer.Tests
|
||||
Assert.True(Torrc.TryParse(input, out torrc));
|
||||
Assert.Equal(expected, torrc.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanParseCartItems()
|
||||
{
|
||||
Assert.True(AppService.TryParsePosCartItems(new JObject()
|
||||
{
|
||||
{"cart", new JArray()
|
||||
{
|
||||
new JObject()
|
||||
{
|
||||
{ "id", "ddd"},
|
||||
{"price", 4},
|
||||
{"count", 1}
|
||||
}
|
||||
}}
|
||||
}, out var items));
|
||||
Assert.Equal("ddd", items[0].Id);
|
||||
Assert.Equal(1, items[0].Count);
|
||||
Assert.Equal(4, items[0].Price);
|
||||
|
||||
// Using legacy parsing
|
||||
Assert.True(AppService.TryParsePosCartItems(new JObject()
|
||||
{
|
||||
{"cart", new JArray()
|
||||
{
|
||||
new JObject()
|
||||
{
|
||||
{ "id", "ddd"},
|
||||
{"price", new JObject()
|
||||
{
|
||||
{ "value", 8.49m }
|
||||
}
|
||||
},
|
||||
{"count", 1}
|
||||
}
|
||||
}}
|
||||
}, out items));
|
||||
Assert.Equal("ddd", items[0].Id);
|
||||
Assert.Equal(1, items[0].Count);
|
||||
Assert.Equal(8.49m, items[0].Price);
|
||||
|
||||
Assert.False(AppService.TryParsePosCartItems(new JObject()
|
||||
{
|
||||
{"cart", new JArray()
|
||||
{
|
||||
new JObject()
|
||||
{
|
||||
{ "id", "ddd"},
|
||||
{"price", new JObject()
|
||||
{
|
||||
{ "value", "nocrahs" }
|
||||
}
|
||||
},
|
||||
{"count", 1}
|
||||
}
|
||||
}}
|
||||
}, out items));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanCalculateDust()
|
||||
{
|
||||
var entity = new InvoiceEntity() { Currency = "USD" };
|
||||
entity.Networks = new BTCPayNetworkProvider(ChainName.Regtest);
|
||||
#pragma warning disable CS0618
|
||||
entity.Payments = new System.Collections.Generic.List<PaymentEntity>();
|
||||
entity.SetPaymentMethod(new PaymentMethod()
|
||||
{
|
||||
Currency = "BTC",
|
||||
Rate = 34_000m
|
||||
});
|
||||
entity.Price = 4000;
|
||||
entity.UpdateTotals();
|
||||
var accounting = entity.GetPaymentMethods().First().Calculate();
|
||||
// Exact price should be 0.117647059..., but the payment method round up to one sat
|
||||
Assert.Equal(0.11764706m, accounting.Due);
|
||||
entity.Payments.Add(new PaymentEntity()
|
||||
{
|
||||
Currency = "BTC",
|
||||
Output = new TxOut(Money.Coins(0.11764706m), new Key()),
|
||||
Accounted = true
|
||||
});
|
||||
entity.UpdateTotals();
|
||||
Assert.Equal(0.0m, entity.NetDue);
|
||||
// The dust's value is below 1 sat
|
||||
Assert.True(entity.Dust > 0.0m);
|
||||
Assert.True(Money.Satoshis(1.0m).ToDecimal(MoneyUnit.BTC) * entity.Rates["BTC"] > entity.Dust);
|
||||
Assert.True(!entity.IsOverPaid);
|
||||
Assert.True(!entity.IsUnderPaid);
|
||||
|
||||
// Now, imagine there is litecoin. It might seem from its
|
||||
// perspecitve that there has been a slight over payment.
|
||||
// However, Calculate() should just cap it to 0.0m
|
||||
entity.SetPaymentMethod(new PaymentMethod()
|
||||
{
|
||||
Currency = "LTC",
|
||||
Rate = 3400m
|
||||
});
|
||||
entity.UpdateTotals();
|
||||
var method = entity.GetPaymentMethods().First(p => p.Currency == "LTC");
|
||||
accounting = method.Calculate();
|
||||
Assert.Equal(0.0m, accounting.DueUncapped);
|
||||
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
#if ALTCOINS
|
||||
[Fact]
|
||||
public void CanCalculateCryptoDue()
|
||||
{
|
||||
var networkProvider = new BTCPayNetworkProvider(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();
|
||||
var entity = new InvoiceEntity() { Currency = "USD" };
|
||||
entity.Networks = networkProvider;
|
||||
#pragma warning disable CS0618
|
||||
entity.Payments = new System.Collections.Generic.List<PaymentEntity>();
|
||||
entity.SetPaymentMethod(new PaymentMethod()
|
||||
{
|
||||
CryptoCode = "BTC",
|
||||
Currency = "BTC",
|
||||
Rate = 5000,
|
||||
NextNetworkFee = Money.Coins(0.1m)
|
||||
});
|
||||
entity.Price = 5000;
|
||||
entity.UpdateTotals();
|
||||
|
||||
var paymentMethod = entity.GetPaymentMethods().TryGet("BTC", PaymentTypes.BTCLike);
|
||||
var accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(1.1m), accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.1m), accounting.TotalDue);
|
||||
Assert.Equal(1.0m, accounting.ToSmallestUnit(Money.Satoshis(1.0m).ToDecimal(MoneyUnit.BTC)));
|
||||
Assert.Equal(1.1m, accounting.Due);
|
||||
Assert.Equal(1.1m, accounting.TotalDue);
|
||||
|
||||
entity.Payments.Add(new PaymentEntity()
|
||||
{
|
||||
Currency = "BTC",
|
||||
Output = new TxOut(Money.Coins(0.5m), new Key()),
|
||||
Rate = 5000,
|
||||
Accounted = true,
|
||||
NetworkFee = 0.1m
|
||||
});
|
||||
|
||||
entity.UpdateTotals();
|
||||
accounting = paymentMethod.Calculate();
|
||||
//Since we need to spend one more txout, it should be 1.1 - 0,5 + 0.1
|
||||
Assert.Equal(Money.Coins(0.7m), accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.2m), accounting.TotalDue);
|
||||
Assert.Equal(0.7m, accounting.Due);
|
||||
Assert.Equal(1.2m, accounting.TotalDue);
|
||||
|
||||
entity.Payments.Add(new PaymentEntity()
|
||||
{
|
||||
Currency = "BTC",
|
||||
Output = new TxOut(Money.Coins(0.2m), new Key()),
|
||||
Accounted = true,
|
||||
NetworkFee = 0.1m
|
||||
});
|
||||
|
||||
entity.UpdateTotals();
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(0.6m), accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
|
||||
Assert.Equal(0.6m, accounting.Due);
|
||||
Assert.Equal(1.3m, accounting.TotalDue);
|
||||
|
||||
entity.Payments.Add(new PaymentEntity()
|
||||
{
|
||||
Currency = "BTC",
|
||||
Output = new TxOut(Money.Coins(0.6m), new Key()),
|
||||
Accounted = true,
|
||||
NetworkFee = 0.1m
|
||||
});
|
||||
|
||||
entity.UpdateTotals();
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Zero, accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
|
||||
Assert.Equal(0.0m, accounting.Due);
|
||||
Assert.Equal(1.3m, accounting.TotalDue);
|
||||
|
||||
entity.Payments.Add(
|
||||
new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true });
|
||||
|
||||
new PaymentEntity() { Currency = "BTC", Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true });
|
||||
entity.UpdateTotals();
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Zero, accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
|
||||
Assert.Equal(0.0m, accounting.Due);
|
||||
Assert.Equal(1.3m, accounting.TotalDue);
|
||||
|
||||
entity = new InvoiceEntity();
|
||||
entity.Networks = networkProvider;
|
||||
entity.Price = 5000;
|
||||
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
|
||||
paymentMethods.Add(
|
||||
new PaymentMethod() { CryptoCode = "BTC", Rate = 1000, NextNetworkFee = Money.Coins(0.1m) });
|
||||
new PaymentMethod() { Currency = "BTC", Rate = 1000, NextNetworkFee = Money.Coins(0.1m) });
|
||||
paymentMethods.Add(
|
||||
new PaymentMethod() { CryptoCode = "LTC", Rate = 500, NextNetworkFee = Money.Coins(0.01m) });
|
||||
new PaymentMethod() { Currency = "LTC", Rate = 500, NextNetworkFee = Money.Coins(0.01m) });
|
||||
entity.SetPaymentMethods(paymentMethods);
|
||||
entity.Payments = new List<PaymentEntity>();
|
||||
entity.UpdateTotals();
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(5.1m), accounting.Due);
|
||||
Assert.Equal(5.1m, accounting.Due);
|
||||
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
|
||||
accounting = paymentMethod.Calculate();
|
||||
|
||||
Assert.Equal(Money.Coins(10.01m), accounting.TotalDue);
|
||||
Assert.Equal(10.01m, accounting.TotalDue);
|
||||
|
||||
entity.Payments.Add(new PaymentEntity()
|
||||
{
|
||||
CryptoCode = "BTC",
|
||||
Currency = "BTC",
|
||||
Output = new TxOut(Money.Coins(1.0m), new Key()),
|
||||
Accounted = true,
|
||||
NetworkFee = 0.1m
|
||||
});
|
||||
|
||||
entity.UpdateTotals();
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(4.2m), accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
|
||||
Assert.Equal(Money.Coins(1.0m), accounting.Paid);
|
||||
Assert.Equal(Money.Coins(5.2m), accounting.TotalDue);
|
||||
Assert.Equal(4.2m, accounting.Due);
|
||||
Assert.Equal(1.0m, accounting.CryptoPaid);
|
||||
Assert.Equal(1.0m, accounting.Paid);
|
||||
Assert.Equal(5.2m, accounting.TotalDue);
|
||||
Assert.Equal(2, accounting.TxRequired);
|
||||
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 - 2.0m /* 8.21m */), accounting.Due);
|
||||
Assert.Equal(Money.Coins(0.0m), accounting.CryptoPaid);
|
||||
Assert.Equal(Money.Coins(2.0m), accounting.Paid);
|
||||
Assert.Equal(Money.Coins(10.01m + 0.1m * 2), accounting.TotalDue);
|
||||
Assert.Equal(10.01m + 0.1m * 2 - 2.0m /* 8.21m */, accounting.Due);
|
||||
Assert.Equal(0.0m, accounting.CryptoPaid);
|
||||
Assert.Equal(2.0m, accounting.Paid);
|
||||
Assert.Equal(10.01m + 0.1m * 2, accounting.TotalDue);
|
||||
|
||||
entity.Payments.Add(new PaymentEntity()
|
||||
{
|
||||
CryptoCode = "LTC",
|
||||
Currency = "LTC",
|
||||
Output = new TxOut(Money.Coins(1.0m), new Key()),
|
||||
Accounted = true,
|
||||
NetworkFee = 0.01m
|
||||
});
|
||||
|
||||
entity.UpdateTotals();
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(4.2m - 0.5m + 0.01m / 2), accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
|
||||
Assert.Equal(Money.Coins(1.5m), accounting.Paid);
|
||||
Assert.Equal(Money.Coins(5.2m + 0.01m / 2), accounting.TotalDue); // The fee for LTC added
|
||||
Assert.Equal(4.2m - 0.5m + 0.01m / 2, accounting.Due);
|
||||
Assert.Equal(1.0m, accounting.CryptoPaid);
|
||||
Assert.Equal(1.5m, accounting.Paid);
|
||||
Assert.Equal(5.2m + 0.01m / 2, accounting.TotalDue); // The fee for LTC added
|
||||
Assert.Equal(2, accounting.TxRequired);
|
||||
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(8.21m - 1.0m + 0.01m), accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
|
||||
Assert.Equal(Money.Coins(3.0m), accounting.Paid);
|
||||
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 + 0.01m), accounting.TotalDue);
|
||||
Assert.Equal(8.21m - 1.0m + 0.01m, accounting.Due);
|
||||
Assert.Equal(1.0m, accounting.CryptoPaid);
|
||||
Assert.Equal(3.0m, accounting.Paid);
|
||||
Assert.Equal(10.01m + 0.1m * 2 + 0.01m, accounting.TotalDue);
|
||||
Assert.Equal(2, accounting.TxRequired);
|
||||
|
||||
var remaining = Money.Coins(4.2m - 0.5m + 0.01m / 2);
|
||||
var remaining = Money.Coins(4.2m - 0.5m + 0.01m / 2.0m).ToDecimal(MoneyUnit.BTC);
|
||||
entity.Payments.Add(new PaymentEntity()
|
||||
{
|
||||
CryptoCode = "BTC",
|
||||
Output = new TxOut(remaining, new Key()),
|
||||
Currency = "BTC",
|
||||
Output = new TxOut(Money.Coins(remaining), new Key()),
|
||||
Accounted = true,
|
||||
NetworkFee = 0.1m
|
||||
});
|
||||
|
||||
entity.UpdateTotals();
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Zero, accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.0m) + remaining, accounting.CryptoPaid);
|
||||
Assert.Equal(Money.Coins(1.5m) + remaining, accounting.Paid);
|
||||
Assert.Equal(Money.Coins(5.2m + 0.01m / 2), accounting.TotalDue);
|
||||
Assert.Equal(0.0m, accounting.Due);
|
||||
Assert.Equal(1.0m + remaining, accounting.CryptoPaid);
|
||||
Assert.Equal(1.5m + remaining, accounting.Paid);
|
||||
Assert.Equal(5.2m + 0.01m / 2, accounting.TotalDue);
|
||||
Assert.Equal(accounting.Paid, accounting.TotalDue);
|
||||
Assert.Equal(2, accounting.TxRequired);
|
||||
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Zero, accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
|
||||
Assert.Equal(Money.Coins(3.0m) + remaining * 2, accounting.Paid);
|
||||
Assert.Equal(0.0m, accounting.Due);
|
||||
Assert.Equal(1.0m, accounting.CryptoPaid);
|
||||
Assert.Equal(3.0m + remaining * 2, accounting.Paid);
|
||||
// Paying 2 BTC fee, LTC fee removed because fully paid
|
||||
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 + 0.1m * 2 /* + 0.01m no need to pay this fee anymore */),
|
||||
Assert.Equal(10.01m + 0.1m * 2 + 0.1m * 2 /* + 0.01m no need to pay this fee anymore */,
|
||||
accounting.TotalDue);
|
||||
Assert.Equal(1, accounting.TxRequired);
|
||||
Assert.Equal(accounting.Paid, accounting.TotalDue);
|
||||
@ -547,27 +656,29 @@ namespace BTCPayServer.Tests
|
||||
entity.Payments = new List<PaymentEntity>();
|
||||
entity.SetPaymentMethod(new PaymentMethod()
|
||||
{
|
||||
CryptoCode = "BTC",
|
||||
Currency = "BTC",
|
||||
Rate = 5000,
|
||||
NextNetworkFee = Money.Coins(0.1m)
|
||||
});
|
||||
entity.Price = 5000;
|
||||
entity.PaymentTolerance = 0;
|
||||
|
||||
entity.UpdateTotals();
|
||||
|
||||
var paymentMethod = entity.GetPaymentMethods().TryGet("BTC", PaymentTypes.BTCLike);
|
||||
var accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(1.1m), accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.1m), accounting.TotalDue);
|
||||
Assert.Equal(Money.Coins(1.1m), accounting.MinimumTotalDue);
|
||||
Assert.Equal(1.1m, accounting.Due);
|
||||
Assert.Equal(1.1m, accounting.TotalDue);
|
||||
Assert.Equal(1.1m, accounting.MinimumTotalDue);
|
||||
|
||||
entity.PaymentTolerance = 10;
|
||||
entity.UpdateTotals();
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(0.99m), accounting.MinimumTotalDue);
|
||||
Assert.Equal(0.99m, accounting.MinimumTotalDue);
|
||||
|
||||
entity.PaymentTolerance = 100;
|
||||
entity.UpdateTotals();
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Satoshis(1), accounting.MinimumTotalDue);
|
||||
Assert.Equal(0.0000_0001m, accounting.MinimumTotalDue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -608,7 +719,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanDetectImage()
|
||||
public void CanDetectFileType()
|
||||
{
|
||||
Assert.True(FileTypeDetector.IsPicture(new byte[] { 0x42, 0x4D }, "test.bmp"));
|
||||
Assert.False(FileTypeDetector.IsPicture(new byte[] { 0x42, 0x4D }, ".bmp"));
|
||||
@ -621,6 +732,15 @@ namespace BTCPayServer.Tests
|
||||
Assert.False(FileTypeDetector.IsPicture(new byte[] { 0x3C, 0x73, 0x76, 0x67 }, "test.jpg"));
|
||||
Assert.False(FileTypeDetector.IsPicture(new byte[] { 0xFF }, "e.jpg"));
|
||||
Assert.False(FileTypeDetector.IsPicture(new byte[] { }, "empty.jpg"));
|
||||
|
||||
Assert.False(FileTypeDetector.IsPicture(new byte[] { 0x49, 0x44, 0x33, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23 }, "music.mp3"));
|
||||
Assert.True(FileTypeDetector.IsAudio(new byte[] { 0x49, 0x44, 0x33, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23 }, "music.mp3"));
|
||||
Assert.True(FileTypeDetector.IsAudio(new byte[] { 0x52, 0x49, 0x46, 0x46, 0x24, 0x9A, 0x08, 0x00, 0x57, 0x41 }, "music.wav"));
|
||||
Assert.True(FileTypeDetector.IsAudio(new byte[] { 0xFF, 0xF1, 0x50, 0x80, 0x1C, 0x3F, 0xFC, 0xDA, 0x00, 0x4C }, "music.aac"));
|
||||
Assert.True(FileTypeDetector.IsAudio(new byte[] { 0x66, 0x4C, 0x61, 0x43, 0x00, 0x00, 0x00, 0x22, 0x04, 0x80 }, "music.flac"));
|
||||
Assert.True(FileTypeDetector.IsAudio(new byte[] { 0x4F, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00 }, "music.ogg"));
|
||||
Assert.True(FileTypeDetector.IsAudio(new byte[] { 0x1A, 0x45, 0xDF, 0xA3, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00 }, "music.weba"));
|
||||
Assert.True(FileTypeDetector.IsAudio(new byte[] { 0xFF, 0xF3, 0xE4, 0x64, 0x00, 0x20, 0xAD, 0xBD, 0x04, 0x00 }, "music.mp3"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -1042,14 +1162,13 @@ namespace BTCPayServer.Tests
|
||||
[Fact]
|
||||
public void CanParseFilter()
|
||||
{
|
||||
var storeId = "6DehZnc9S7qC6TUTNWuzJ1pFsHTHvES6An21r3MjvLey";
|
||||
var filter = "storeid:abc, status:abed, blabhbalh ";
|
||||
var search = new SearchString(filter);
|
||||
Assert.Equal("storeid:abc, status:abed, blabhbalh", search.ToString());
|
||||
Assert.Equal("blabhbalh", search.TextSearch);
|
||||
Assert.Single(search.Filters["storeid"]);
|
||||
Assert.Single(search.Filters["status"]);
|
||||
Assert.Equal("abc", search.Filters["storeid"].First());
|
||||
Assert.Equal("abed", search.Filters["status"].First());
|
||||
Assert.Single(search.Filters["storeid"], "abc");
|
||||
Assert.Single(search.Filters["status"], "abed");
|
||||
|
||||
filter = "status:abed, status:abed2";
|
||||
search = new SearchString(filter);
|
||||
@ -1064,6 +1183,48 @@ namespace BTCPayServer.Tests
|
||||
search = new SearchString(filter);
|
||||
Assert.Equal("2019-04-25 01:00 AM", search.Filters["startdate"].First());
|
||||
Assert.Equal("hekki", search.TextSearch);
|
||||
|
||||
// modify search
|
||||
filter = $"status:settled,exceptionstatus:paidLate,unusual:true, fulltext searchterm, storeid:{storeId},startdate:2019-04-25 01:00:00";
|
||||
search = new SearchString(filter);
|
||||
Assert.Equal(filter, search.ToString());
|
||||
Assert.Equal("fulltext searchterm", search.TextSearch);
|
||||
Assert.Single(search.Filters["storeid"], storeId);
|
||||
Assert.Single(search.Filters["status"], "settled");
|
||||
Assert.Single(search.Filters["exceptionstatus"], "paidLate");
|
||||
Assert.Single(search.Filters["unusual"], "true");
|
||||
|
||||
// toggle off bool with same value
|
||||
var modified = new SearchString(search.Toggle("unusual", "true"));
|
||||
Assert.Null(modified.GetFilterBool("unusual"));
|
||||
|
||||
// add to array
|
||||
modified = new SearchString(modified.Toggle("status", "processing"));
|
||||
var statusArray = modified.GetFilterArray("status");
|
||||
Assert.Equal(2, statusArray.Length);
|
||||
Assert.Contains("processing", statusArray);
|
||||
Assert.Contains("settled", statusArray);
|
||||
|
||||
// toggle off array with same value
|
||||
modified = new SearchString(modified.Toggle("status", "settled"));
|
||||
statusArray = modified.GetFilterArray("status");
|
||||
Assert.Single(statusArray, "processing");
|
||||
|
||||
// toggle off array with null value
|
||||
modified = new SearchString(modified.Toggle("status", null));
|
||||
Assert.Null(modified.GetFilterArray("status"));
|
||||
|
||||
// toggle off date with null value
|
||||
modified = new SearchString(modified.Toggle("startdate", "-7d"));
|
||||
Assert.Single(modified.GetFilterArray("startdate"), "-7d");
|
||||
modified = new SearchString(modified.Toggle("startdate", null));
|
||||
Assert.Null(modified.GetFilterArray("startdate"));
|
||||
|
||||
// toggle off date with same value
|
||||
modified = new SearchString(modified.Toggle("enddate", "-7d"));
|
||||
Assert.Single(modified.GetFilterArray("enddate"), "-7d");
|
||||
modified = new SearchString(modified.Toggle("enddate", "-7d"));
|
||||
Assert.Null(modified.GetFilterArray("enddate"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -1103,6 +1264,45 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal("000000161", m.OrderId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanParseOldPosAppData()
|
||||
{
|
||||
var data = new JObject()
|
||||
{
|
||||
["price"] = 1.64m
|
||||
}.ToString();
|
||||
Assert.Equal(1.64m, JsonConvert.DeserializeObject<PosAppCartItem>(data).Price);
|
||||
|
||||
data = new JObject()
|
||||
{
|
||||
["price"] = new JObject()
|
||||
{
|
||||
["value"] = 1.65m
|
||||
}
|
||||
}.ToString();
|
||||
Assert.Equal(1.65m, JsonConvert.DeserializeObject<PosAppCartItem>(data).Price);
|
||||
data = new JObject()
|
||||
{
|
||||
["price"] = new JObject()
|
||||
{
|
||||
["value"] = "1.6305"
|
||||
}
|
||||
}.ToString();
|
||||
Assert.Equal(1.6305m, JsonConvert.DeserializeObject<PosAppCartItem>(data).Price);
|
||||
|
||||
data = new JObject()
|
||||
{
|
||||
["price"] = new JObject()
|
||||
{
|
||||
["value"] = null
|
||||
}
|
||||
}.ToString();
|
||||
Assert.Equal(0.0m, JsonConvert.DeserializeObject<PosAppCartItem>(data).Price);
|
||||
|
||||
var o = JObject.Parse(JsonConvert.SerializeObject(new PosAppCartItem() { Price = 1.356m }));
|
||||
Assert.Equal(1.356m, o["price"].Value<decimal>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanParseCurrencyValue()
|
||||
{
|
||||
@ -1341,6 +1541,24 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(cache.States[0].Rates[0].Pair, cache2.States[0].Rates[0].Pair);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanParseStoreRoleId()
|
||||
{
|
||||
var id = StoreRoleId.Parse("test::lol");
|
||||
Assert.Equal("test", id.StoreId);
|
||||
Assert.Equal("lol", id.Role);
|
||||
Assert.Equal("test::lol", id.ToString());
|
||||
Assert.Equal("test::lol", id.Id);
|
||||
Assert.False(id.IsServerRole);
|
||||
|
||||
id = StoreRoleId.Parse("lol");
|
||||
Assert.Null(id.StoreId);
|
||||
Assert.Equal("lol", id.Role);
|
||||
Assert.Equal("lol", id.ToString());
|
||||
Assert.Equal("lol", id.Id);
|
||||
Assert.True(id.IsServerRole);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KitchenSinkTest()
|
||||
{
|
||||
@ -1785,11 +2003,6 @@ namespace BTCPayServer.Tests
|
||||
#pragma warning disable CS0618
|
||||
var dummy = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, Network.RegTest).ToString();
|
||||
var networkProvider = new BTCPayNetworkProvider(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 networkBTC = networkProvider.GetNetwork("BTC");
|
||||
var networkLTC = networkProvider.GetNetwork("LTC");
|
||||
InvoiceEntity invoiceEntity = new InvoiceEntity();
|
||||
@ -1797,14 +2010,14 @@ namespace BTCPayServer.Tests
|
||||
invoiceEntity.Payments = new System.Collections.Generic.List<PaymentEntity>();
|
||||
invoiceEntity.Price = 100;
|
||||
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
|
||||
paymentMethods.Add(new PaymentMethod() { Network = networkBTC, CryptoCode = "BTC", Rate = 10513.44m, }
|
||||
paymentMethods.Add(new PaymentMethod() { Network = networkBTC, Currency = "BTC", Rate = 10513.44m, }
|
||||
.SetPaymentMethodDetails(
|
||||
new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
|
||||
{
|
||||
NextNetworkFee = Money.Coins(0.00000100m),
|
||||
DepositAddress = dummy
|
||||
}));
|
||||
paymentMethods.Add(new PaymentMethod() { Network = networkLTC, CryptoCode = "LTC", Rate = 216.79m }
|
||||
paymentMethods.Add(new PaymentMethod() { Network = networkLTC, Currency = "LTC", Rate = 216.79m }
|
||||
.SetPaymentMethodDetails(
|
||||
new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
|
||||
{
|
||||
@ -1820,7 +2033,7 @@ namespace BTCPayServer.Tests
|
||||
new PaymentEntity()
|
||||
{
|
||||
Accounted = true,
|
||||
CryptoCode = "BTC",
|
||||
Currency = "BTC",
|
||||
NetworkFee = 0.00000100m,
|
||||
Network = networkProvider.GetNetwork("BTC"),
|
||||
}
|
||||
@ -1829,34 +2042,33 @@ namespace BTCPayServer.Tests
|
||||
Network = networkProvider.GetNetwork("BTC"),
|
||||
Output = new TxOut() { Value = Money.Coins(0.00151263m) }
|
||||
}));
|
||||
invoiceEntity.UpdateTotals();
|
||||
accounting = btc.Calculate();
|
||||
invoiceEntity.Payments.Add(
|
||||
new PaymentEntity()
|
||||
{
|
||||
Accounted = true,
|
||||
CryptoCode = "BTC",
|
||||
Currency = "BTC",
|
||||
NetworkFee = 0.00000100m,
|
||||
Network = networkProvider.GetNetwork("BTC")
|
||||
}
|
||||
.SetCryptoPaymentData(new BitcoinLikePaymentData()
|
||||
{
|
||||
Network = networkProvider.GetNetwork("BTC"),
|
||||
Output = new TxOut() { Value = accounting.Due }
|
||||
Output = new TxOut() { Value = Money.Coins(accounting.Due) }
|
||||
}));
|
||||
invoiceEntity.UpdateTotals();
|
||||
accounting = btc.Calculate();
|
||||
Assert.Equal(Money.Zero, accounting.Due);
|
||||
Assert.Equal(Money.Zero, accounting.DueUncapped);
|
||||
Assert.Equal(0.0m, accounting.Due);
|
||||
Assert.Equal(0.0m, accounting.DueUncapped);
|
||||
|
||||
var ltc = invoiceEntity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
|
||||
accounting = ltc.Calculate();
|
||||
|
||||
Assert.Equal(Money.Zero, accounting.Due);
|
||||
// LTC might have over paid due to BTC paying above what it should (round 1 satoshi up)
|
||||
Assert.True(accounting.DueUncapped < Money.Zero);
|
||||
|
||||
var paymentMethod = InvoiceWatcher.GetNearestClearedPayment(paymentMethods, out var accounting2);
|
||||
Assert.Equal(btc.CryptoCode, paymentMethod.CryptoCode);
|
||||
#pragma warning restore CS0618
|
||||
Assert.Equal(0.0m, accounting.Due);
|
||||
// LTC might should be over paid due to BTC paying above what it should (round 1 satoshi up), but we handle this case
|
||||
// and set DueUncapped to zero.
|
||||
Assert.Equal(0.0m, accounting.DueUncapped);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
using BTCPayServer.Forms;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@ -10,16 +11,25 @@ using Xunit.Abstractions;
|
||||
|
||||
namespace BTCPayServer.Tests;
|
||||
|
||||
[Trait("Fast", "Fast")]
|
||||
[Collection(nameof(NonParallelizableCollectionDefinition))]
|
||||
[Trait("Integration", "Integration")]
|
||||
public class FormTests : UnitTestBase
|
||||
{
|
||||
public FormTests(ITestOutputHelper helper) : base(helper)
|
||||
{
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanParseForm()
|
||||
|
||||
[Fact(Timeout = TestUtils.TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanParseForm()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
var service = tester.PayTester.GetService<FormDataService>();
|
||||
|
||||
var form = new Form()
|
||||
{
|
||||
Fields = new List<Field>
|
||||
@ -40,8 +50,6 @@ public class FormTests : UnitTestBase
|
||||
}
|
||||
}
|
||||
};
|
||||
var providers = new FormComponentProviders(new List<IFormComponentProvider>());
|
||||
var service = new FormDataService(null, providers);
|
||||
Assert.False(service.IsFormSchemaValid(form.ToString(), out _, out _));
|
||||
form = new Form
|
||||
{
|
||||
@ -164,7 +172,7 @@ public class FormTests : UnitTestBase
|
||||
Assert.Equal("original", obj["invoice"]["test"].Value<string>());
|
||||
Assert.Equal("updated", obj["invoice_item3"].Value<string>());
|
||||
Clear(form);
|
||||
form.SetValues(obj);
|
||||
service.SetValues(form, obj);
|
||||
obj = service.GetValues(form);
|
||||
Assert.Equal("original", obj["invoice"]["test"].Value<string>());
|
||||
Assert.Equal("updated", obj["invoice_item3"].Value<string>());
|
||||
@ -182,10 +190,12 @@ public class FormTests : UnitTestBase
|
||||
}
|
||||
}
|
||||
};
|
||||
form.SetValues(obj);
|
||||
|
||||
service.SetValues(form, obj);
|
||||
obj = service.GetValues(form);
|
||||
Assert.Null(obj["test"].Value<string>());
|
||||
form.SetValues(new JObject { ["test"] = "hello" });
|
||||
|
||||
service.SetValues(form, new JObject { ["test"] = "hello" });
|
||||
obj = service.GetValues(form);
|
||||
Assert.Equal("hello", obj["test"].Value<string>());
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
@ -16,14 +15,13 @@ using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.PayoutProcessors;
|
||||
using BTCPayServer.PayoutProcessors.OnChain;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Custodian.Client.MockCustodian;
|
||||
using BTCPayServer.Services.Notifications;
|
||||
using BTCPayServer.Services.Notifications.Blobs;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json;
|
||||
@ -228,7 +226,7 @@ namespace BTCPayServer.Tests
|
||||
await Assert.ThrowsAsync<GreenfieldAPIException>(() => newUserClient.GetInvoices(store.Id));
|
||||
|
||||
// if user is a guest or owner, then it should be ok
|
||||
await unrestricted.AddStoreUser(store.Id, new StoreUserData() { UserId = newUser.Id, Role = "Guest" });
|
||||
await unrestricted.AddStoreUser(store.Id, new StoreUserData() { UserId = newUser.Id});
|
||||
await newUserClient.GetInvoices(store.Id);
|
||||
}
|
||||
|
||||
@ -299,6 +297,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(user.StoreId, app.StoreId);
|
||||
Assert.Equal("PointOfSale", app.AppType);
|
||||
Assert.Equal("test app title", app.Title);
|
||||
Assert.False(app.Archived);
|
||||
|
||||
// Make sure we return a 404 if we try to get an app that doesn't exist
|
||||
await AssertHttpError(404, async () =>
|
||||
@ -322,17 +321,20 @@ namespace BTCPayServer.Tests
|
||||
new CreatePointOfSaleAppRequest()
|
||||
{
|
||||
AppName = "new app name",
|
||||
Title = "new app title"
|
||||
Title = "new app title",
|
||||
Archived = true
|
||||
}
|
||||
);
|
||||
// Test generic GET app endpoint first
|
||||
retrievedApp = await client.GetApp(app.Id);
|
||||
Assert.Equal("new app name", retrievedApp.Name);
|
||||
Assert.True(retrievedApp.Archived);
|
||||
|
||||
// Test the POS-specific endpoint also
|
||||
var retrievedPosApp = await client.GetPosApp(app.Id);
|
||||
Assert.Equal("new app name", retrievedPosApp.Name);
|
||||
Assert.Equal("new app title", retrievedPosApp.Title);
|
||||
Assert.True(retrievedPosApp.Archived);
|
||||
|
||||
// Make sure we return a 404 if we try to delete an app that doesn't exist
|
||||
await AssertHttpError(404, async () =>
|
||||
@ -464,6 +466,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal("test app from API", app.Name);
|
||||
Assert.Equal(user.StoreId, app.StoreId);
|
||||
Assert.Equal("Crowdfund", app.AppType);
|
||||
Assert.False(app.Archived);
|
||||
|
||||
// Make sure we return a 404 if we try to get an app that doesn't exist
|
||||
await AssertHttpError(404, async () =>
|
||||
@ -480,11 +483,13 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(app.Name, retrievedApp.Name);
|
||||
Assert.Equal(app.StoreId, retrievedApp.StoreId);
|
||||
Assert.Equal(app.AppType, retrievedApp.AppType);
|
||||
Assert.False(retrievedApp.Archived);
|
||||
|
||||
// Test the crowdfund-specific endpoint also
|
||||
var retrievedPosApp = await client.GetCrowdfundApp(app.Id);
|
||||
Assert.Equal(app.Name, retrievedPosApp.Name);
|
||||
Assert.Equal(app.Title, retrievedPosApp.Title);
|
||||
var retrievedCfApp = await client.GetCrowdfundApp(app.Id);
|
||||
Assert.Equal(app.Name, retrievedCfApp.Name);
|
||||
Assert.Equal(app.Title, retrievedCfApp.Title);
|
||||
Assert.False(retrievedCfApp.Archived);
|
||||
|
||||
// Make sure we return a 404 if we try to delete an app that doesn't exist
|
||||
await AssertHttpError(404, async () =>
|
||||
@ -534,10 +539,12 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(posApp.Name, apps[0].Name);
|
||||
Assert.Equal(posApp.StoreId, apps[0].StoreId);
|
||||
Assert.Equal(posApp.AppType, apps[0].AppType);
|
||||
Assert.False(apps[0].Archived);
|
||||
|
||||
Assert.Equal(crowdfundApp.Name, apps[1].Name);
|
||||
Assert.Equal(crowdfundApp.StoreId, apps[1].StoreId);
|
||||
Assert.Equal(crowdfundApp.AppType, apps[1].AppType);
|
||||
Assert.False(apps[1].Archived);
|
||||
|
||||
// Get all apps for all store now
|
||||
apps = await client.GetAllApps();
|
||||
@ -547,15 +554,17 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(posApp.Name, apps[0].Name);
|
||||
Assert.Equal(posApp.StoreId, apps[0].StoreId);
|
||||
Assert.Equal(posApp.AppType, apps[0].AppType);
|
||||
Assert.False(apps[0].Archived);
|
||||
|
||||
Assert.Equal(crowdfundApp.Name, apps[1].Name);
|
||||
Assert.Equal(crowdfundApp.StoreId, apps[1].StoreId);
|
||||
Assert.Equal(crowdfundApp.AppType, apps[1].AppType);
|
||||
Assert.False(apps[1].Archived);
|
||||
|
||||
Assert.Equal(newApp.Name, apps[2].Name);
|
||||
Assert.Equal(newApp.StoreId, apps[2].StoreId);
|
||||
Assert.Equal(newApp.AppType, apps[2].AppType);
|
||||
|
||||
Assert.False(apps[2].Archived);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
@ -876,7 +885,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
TestLogs.LogInformation("Can't archive without knowing the walletId");
|
||||
var ex = await AssertAPIError("missing-permission", async () => await client.ArchivePullPayment("lol", result.Id));
|
||||
Assert.Equal("btcpay.store.canmanagepullpayments", ((GreenfieldPermissionAPIError)ex.APIError).MissingPermission);
|
||||
Assert.Equal("btcpay.store.canarchivepullpayments", ((GreenfieldPermissionAPIError)ex.APIError).MissingPermission);
|
||||
TestLogs.LogInformation("Can't archive without permission");
|
||||
await AssertAPIError("unauthenticated", async () => await unauthenticated.ArchivePullPayment(storeId, result.Id));
|
||||
await client.ArchivePullPayment(storeId, result.Id);
|
||||
@ -1073,6 +1082,22 @@ namespace BTCPayServer.Tests
|
||||
var lnrURLs = await unauthenticated.GetPullPaymentLNURL(test4.Id);
|
||||
Assert.IsType<string>(lnrURLs.LNURLBech32);
|
||||
Assert.IsType<string>(lnrURLs.LNURLUri);
|
||||
Assert.Equal(12.303228134m, test4.Amount);
|
||||
Assert.Equal("BTC", test4.Currency);
|
||||
|
||||
// Test with SATS denomination values
|
||||
var testSats = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest()
|
||||
{
|
||||
Name = "Test SATS",
|
||||
Amount = 21000,
|
||||
Currency = "SATS",
|
||||
PaymentMethods = new[] { "BTC", "BTC-LightningNetwork", "BTC_LightningLike" }
|
||||
});
|
||||
lnrURLs = await unauthenticated.GetPullPaymentLNURL(testSats.Id);
|
||||
Assert.IsType<string>(lnrURLs.LNURLBech32);
|
||||
Assert.IsType<string>(lnrURLs.LNURLUri);
|
||||
Assert.Equal(21000, testSats.Amount);
|
||||
Assert.Equal("SATS", testSats.Currency);
|
||||
|
||||
//permission test around auto approved pps and payouts
|
||||
var nonApproved = await acc.CreateClient(Policies.CanCreateNonApprovedPullPayments);
|
||||
@ -1134,7 +1159,8 @@ namespace BTCPayServer.Tests
|
||||
Approved = false,
|
||||
PaymentMethod = "BTC",
|
||||
Amount = 0.0001m,
|
||||
Destination = address.ToString()
|
||||
Destination = address.ToString(),
|
||||
|
||||
});
|
||||
await AssertAPIError("invalid-state", async () =>
|
||||
{
|
||||
@ -1253,7 +1279,7 @@ namespace BTCPayServer.Tests
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
await user.GrantAccessAsync();
|
||||
await user.MakeAdmin();
|
||||
var client = await user.CreateClient(Policies.Unrestricted);
|
||||
|
||||
@ -1319,7 +1345,8 @@ namespace BTCPayServer.Tests
|
||||
// We strip the user's Owner right, so the key should not work
|
||||
using var ctx = tester.PayTester.GetService<Data.ApplicationDbContextFactory>().CreateContext();
|
||||
var storeEntity = await ctx.UserStore.SingleAsync(u => u.ApplicationUserId == user.UserId && u.StoreDataId == newStore.Id);
|
||||
storeEntity.Role = "Guest";
|
||||
var roleId = (await tester.PayTester.GetService<StoreRepository>().GetStoreRoles(null)).Single(r => r.Role == "Guest").Id;
|
||||
storeEntity.StoreRoleId = roleId;
|
||||
await ctx.SaveChangesAsync();
|
||||
await AssertHttpError(403, async () => await client.UpdateStore(newStore.Id, new UpdateStoreRequest() { Name = "B" }));
|
||||
|
||||
@ -1331,6 +1358,13 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
tester.DeleteStore = false;
|
||||
Assert.Empty(await client.GetStores());
|
||||
|
||||
// Archive
|
||||
var archivableStore = await client.CreateStore(new CreateStoreRequest { Name = "Archivable" });
|
||||
Assert.False(archivableStore.Archived);
|
||||
archivableStore = await client.UpdateStore(archivableStore.Id, new UpdateStoreRequest { Name = "Archived", Archived = true });
|
||||
Assert.Equal("Archived", archivableStore.Name);
|
||||
Assert.True(archivableStore.Archived);
|
||||
}
|
||||
|
||||
private async Task<GreenfieldValidationException> AssertValidationError(string[] fields, Func<Task> act)
|
||||
@ -1423,14 +1457,14 @@ namespace BTCPayServer.Tests
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanUseWebhooks()
|
||||
{
|
||||
void AssertHook(FakeServer fakeServer, Client.Models.StoreWebhookData hook)
|
||||
void AssertHook(FakeServer fakeServer, StoreWebhookData hook)
|
||||
{
|
||||
Assert.True(hook.Enabled);
|
||||
Assert.True(hook.AuthorizedEvents.Everything);
|
||||
Assert.False(hook.AutomaticRedelivery);
|
||||
Assert.Equal(fakeServer.ServerUri.AbsoluteUri, hook.Url);
|
||||
}
|
||||
using var tester = CreateServerTester();
|
||||
using var tester = CreateServerTester(newDb: true);
|
||||
using var fakeServer = new FakeServer();
|
||||
await fakeServer.Start();
|
||||
await tester.StartAsync();
|
||||
@ -1507,6 +1541,14 @@ namespace BTCPayServer.Tests
|
||||
clientProfile = await user.CreateClient(Policies.CanModifyStoreSettings, Policies.CanCreateInvoice);
|
||||
await clientProfile.GetWebhookDeliveryRequest(user.StoreId, hook.Id, newDeliveryId);
|
||||
|
||||
|
||||
TestLogs.LogInformation("Can prune deliveries");
|
||||
var cleanup = tester.PayTester.GetService<HostedServices.CleanupWebhookDeliveriesTask>();
|
||||
cleanup.BatchSize = 1;
|
||||
cleanup.PruneAfter = TimeSpan.Zero;
|
||||
await cleanup.Do(default);
|
||||
await AssertHttpError(409, () => clientProfile.RedeliverWebhook(user.StoreId, hook.Id, delivery.Id));
|
||||
|
||||
TestLogs.LogInformation("Testing corner cases");
|
||||
Assert.Null(await clientProfile.GetWebhookDeliveryRequest(user.StoreId, "lol", newDeliveryId));
|
||||
Assert.Null(await clientProfile.GetWebhookDeliveryRequest(user.StoreId, hook.Id, "lol"));
|
||||
@ -1564,7 +1606,7 @@ namespace BTCPayServer.Tests
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
await user.GrantAccessAsync();
|
||||
await user.MakeAdmin();
|
||||
var client = await user.CreateClient(Policies.Unrestricted);
|
||||
var viewOnly = await user.CreateClient(Policies.CanViewPaymentRequests);
|
||||
@ -1646,11 +1688,18 @@ namespace BTCPayServer.Tests
|
||||
BitcoinAddress.Create(invoice.BitcoinAddress, tester.ExplorerNode.Network), invoice.BtcDue);
|
||||
});
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
Assert.Equal(Invoice.STATUS_PAID, user.BitPay.GetInvoice(invoiceId).Status);
|
||||
if (!partialPayment)
|
||||
Assert.Equal(PaymentRequestData.PaymentRequestStatus.Completed, (await client.GetPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id)).Status);
|
||||
});
|
||||
{
|
||||
Assert.Equal(Invoice.STATUS_PAID, (await user.BitPay.GetInvoiceAsync(invoiceId)).Status);
|
||||
if (!partialPayment)
|
||||
Assert.Equal(PaymentRequestData.PaymentRequestStatus.Processing, (await client.GetPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id)).Status);
|
||||
});
|
||||
await tester.ExplorerNode.GenerateAsync(1);
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
Assert.Equal(Invoice.STATUS_CONFIRMED, (await user.BitPay.GetInvoiceAsync(invoiceId)).Status);
|
||||
if (!partialPayment)
|
||||
Assert.Equal(PaymentRequestData.PaymentRequestStatus.Completed, (await client.GetPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id)).Status);
|
||||
});
|
||||
}
|
||||
await Pay(invoiceId);
|
||||
|
||||
@ -1950,6 +1999,82 @@ namespace BTCPayServer.Tests
|
||||
CustomCurrency = "BTC"
|
||||
});
|
||||
Assert.True(pp.AutoApproveClaims);
|
||||
|
||||
// test subtract percentage
|
||||
validationError = await AssertValidationError(new[] { "SubtractPercentage" }, async () =>
|
||||
{
|
||||
await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
|
||||
{
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
RefundVariant = RefundVariant.RateThen,
|
||||
SubtractPercentage = 101
|
||||
});
|
||||
});
|
||||
Assert.Contains("SubtractPercentage: Percentage must be a numeric value between 0 and 100", validationError.Message);
|
||||
|
||||
// should auto-approve
|
||||
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
|
||||
{
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
RefundVariant = RefundVariant.RateThen,
|
||||
SubtractPercentage = 6.15m
|
||||
});
|
||||
Assert.Equal("BTC", pp.Currency);
|
||||
Assert.True(pp.AutoApproveClaims);
|
||||
Assert.Equal(0.9385m, pp.Amount);
|
||||
|
||||
// test RefundVariant.OverpaidAmount
|
||||
validationError = await AssertValidationError(new[] { "RefundVariant" }, async () =>
|
||||
{
|
||||
await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
|
||||
{
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
RefundVariant = RefundVariant.OverpaidAmount
|
||||
});
|
||||
});
|
||||
Assert.Contains("Invoice is not overpaid", validationError.Message);
|
||||
|
||||
// should auto-approve
|
||||
invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest { Amount = 5000.0m, Currency = "USD" });
|
||||
methods = await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id);
|
||||
method = methods.First();
|
||||
|
||||
await tester.WaitForEvent<NewOnChainTransactionEvent>(async () =>
|
||||
{
|
||||
await tester.ExplorerNode.SendToAddressAsync(
|
||||
BitcoinAddress.Create(method.Destination, tester.NetworkProvider.BTC.NBitcoinNetwork),
|
||||
Money.Coins(method.Due * 2)
|
||||
);
|
||||
});
|
||||
|
||||
await tester.ExplorerNode.GenerateAsync(5);
|
||||
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
invoice = await client.GetInvoice(user.StoreId, invoice.Id);
|
||||
Assert.True(invoice.Status == InvoiceStatus.Settled);
|
||||
Assert.True(invoice.AdditionalStatus == InvoiceExceptionStatus.PaidOver);
|
||||
});
|
||||
|
||||
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
|
||||
{
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
RefundVariant = RefundVariant.OverpaidAmount
|
||||
});
|
||||
Assert.Equal("BTC", pp.Currency);
|
||||
Assert.True(pp.AutoApproveClaims);
|
||||
Assert.Equal(method.Due, pp.Amount);
|
||||
|
||||
// once more with subtract percentage
|
||||
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
|
||||
{
|
||||
PaymentMethod = method.PaymentMethod,
|
||||
RefundVariant = RefundVariant.OverpaidAmount,
|
||||
SubtractPercentage = 21m
|
||||
});
|
||||
Assert.Equal("BTC", pp.Currency);
|
||||
Assert.True(pp.AutoApproveClaims);
|
||||
Assert.Equal(0.79m, pp.Amount);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
@ -2366,27 +2491,10 @@ namespace BTCPayServer.Tests
|
||||
Assert.NotNull(merchantInvoice.PaymentHash);
|
||||
Assert.Equal(merchantInvoice.Id, merchantInvoice.PaymentHash);
|
||||
|
||||
// The default client is using charge, so we should not be able to query channels
|
||||
var chargeClient = await user.CreateClient(Policies.CanUseInternalLightningNode);
|
||||
|
||||
var info = await chargeClient.GetLightningNodeInfo("BTC");
|
||||
Assert.Single(info.NodeURIs);
|
||||
Assert.NotEqual(0, info.BlockHeight);
|
||||
Assert.NotNull(info.Alias);
|
||||
Assert.NotNull(info.Color);
|
||||
Assert.NotNull(info.Version);
|
||||
Assert.NotNull(info.PeersCount);
|
||||
Assert.NotNull(info.ActiveChannelsCount);
|
||||
Assert.NotNull(info.InactiveChannelsCount);
|
||||
Assert.NotNull(info.PendingChannelsCount);
|
||||
|
||||
var gex = await AssertAPIError("lightning-node-unavailable", () => chargeClient.ConnectToLightningNode("BTC", new ConnectToNodeRequest(NodeInfo.Parse($"{new Key().PubKey.ToHex()}@localhost:3827"))));
|
||||
Assert.Contains("NotSupported", gex.Message);
|
||||
|
||||
await AssertAPIError("lightning-node-unavailable", () => chargeClient.GetLightningNodeChannels("BTC"));
|
||||
var client = await user.CreateClient(Policies.CanUseInternalLightningNode);
|
||||
// Not permission for the store!
|
||||
await AssertAPIError("missing-permission", () => chargeClient.GetLightningNodeChannels(user.StoreId, "BTC"));
|
||||
var invoiceData = await chargeClient.CreateLightningInvoice("BTC", new CreateLightningInvoiceRequest()
|
||||
await AssertAPIError("missing-permission", () => client.GetLightningNodeChannels(user.StoreId, "BTC"));
|
||||
var invoiceData = await client.CreateLightningInvoice("BTC", new CreateLightningInvoiceRequest()
|
||||
{
|
||||
Amount = LightMoney.Satoshis(1000),
|
||||
Description = "lol",
|
||||
@ -2394,17 +2502,17 @@ namespace BTCPayServer.Tests
|
||||
PrivateRouteHints = false
|
||||
});
|
||||
var chargeInvoice = invoiceData;
|
||||
Assert.NotNull(await chargeClient.GetLightningInvoice("BTC", invoiceData.Id));
|
||||
Assert.NotNull(await client.GetLightningInvoice("BTC", invoiceData.Id));
|
||||
|
||||
// check list for internal node
|
||||
var invoices = await chargeClient.GetLightningInvoices("BTC");
|
||||
var pendingInvoices = await chargeClient.GetLightningInvoices("BTC", true);
|
||||
var invoices = await client.GetLightningInvoices("BTC");
|
||||
var pendingInvoices = await client.GetLightningInvoices("BTC", true);
|
||||
Assert.NotEmpty(invoices);
|
||||
Assert.Contains(invoices, i => i.Id == invoiceData.Id);
|
||||
Assert.NotEmpty(pendingInvoices);
|
||||
Assert.Contains(pendingInvoices, i => i.Id == invoiceData.Id);
|
||||
|
||||
var client = await user.CreateClient($"{Policies.CanUseLightningNodeInStore}:{user.StoreId}");
|
||||
client = await user.CreateClient($"{Policies.CanUseLightningNodeInStore}:{user.StoreId}");
|
||||
// Not permission for the server
|
||||
await AssertAPIError("missing-permission", () => client.GetLightningNodeChannels("BTC"));
|
||||
|
||||
@ -2483,7 +2591,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains(payments, i => i.BOLT11 == merchantInvoice.BOLT11);
|
||||
|
||||
// Node info
|
||||
info = await client.GetLightningNodeInfo(user.StoreId, "BTC");
|
||||
var info = await client.GetLightningNodeInfo(user.StoreId, "BTC");
|
||||
Assert.Single(info.NodeURIs);
|
||||
Assert.NotEqual(0, info.BlockHeight);
|
||||
|
||||
@ -2524,7 +2632,12 @@ namespace BTCPayServer.Tests
|
||||
user.RegisterLightningNode("BTC", LightningConnectionType.CLightning);
|
||||
|
||||
var client = await user.CreateClient(Policies.Unrestricted);
|
||||
var invoice = await client.CreateInvoice(user.StoreId,
|
||||
var invoices = new Task<Client.Models.InvoiceData>[5];
|
||||
|
||||
// Create invoices
|
||||
for (int i = 0; i < invoices.Length; i++)
|
||||
{
|
||||
invoices[i] = client.CreateInvoice(user.StoreId,
|
||||
new CreateInvoiceRequest
|
||||
{
|
||||
Currency = "USD",
|
||||
@ -2535,18 +2648,35 @@ namespace BTCPayServer.Tests
|
||||
DefaultPaymentMethod = "BTC_LightningLike"
|
||||
}
|
||||
});
|
||||
var pm = Assert.Single(await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id));
|
||||
Assert.False(pm.AdditionalData.HasValues);
|
||||
}
|
||||
|
||||
var resp = await tester.CustomerLightningD.Pay(pm.Destination);
|
||||
Assert.Equal(PayResult.Ok, resp.Result);
|
||||
Assert.NotNull(resp.Details.PaymentHash);
|
||||
Assert.NotNull(resp.Details.Preimage);
|
||||
var pm = new InvoicePaymentMethodDataModel[invoices.Length];
|
||||
for (int i = 0; i < invoices.Length; i++)
|
||||
{
|
||||
pm[i] = Assert.Single(await client.GetInvoicePaymentMethods(user.StoreId, (await invoices[i]).Id));
|
||||
Assert.True(pm[i].AdditionalData.HasValues);
|
||||
}
|
||||
|
||||
pm = Assert.Single(await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id));
|
||||
Assert.True(pm.AdditionalData.HasValues);
|
||||
Assert.Equal(resp.Details.PaymentHash.ToString(), pm.AdditionalData.GetValue("paymentHash"));
|
||||
Assert.Equal(resp.Details.Preimage.ToString(), pm.AdditionalData.GetValue("preimage"));
|
||||
// Pay them all at once
|
||||
Task<PayResponse>[] payResponses = new Task<PayResponse>[invoices.Length];
|
||||
for (int i = 0; i < invoices.Length; i++)
|
||||
{
|
||||
payResponses[i] = tester.CustomerLightningD.Pay(pm[i].Destination);
|
||||
}
|
||||
|
||||
// Checking the results
|
||||
for (int i = 0; i < invoices.Length; i++)
|
||||
{
|
||||
var resp = await payResponses[i];
|
||||
Assert.Equal(PayResult.Ok, resp.Result);
|
||||
Assert.NotNull(resp.Details.PaymentHash);
|
||||
Assert.NotNull(resp.Details.Preimage);
|
||||
|
||||
pm[i] = Assert.Single(await client.GetInvoicePaymentMethods(user.StoreId, (await invoices[i]).Id));
|
||||
Assert.True(pm[i].AdditionalData.HasValues);
|
||||
Assert.Equal(resp.Details.PaymentHash.ToString(), pm[i].AdditionalData.GetValue("paymentHash"));
|
||||
Assert.Equal(resp.Details.Preimage.ToString(), pm[i].AdditionalData.GetValue("preimage"));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 20 * 1000)]
|
||||
@ -3103,6 +3233,9 @@ namespace BTCPayServer.Tests
|
||||
});
|
||||
var transaction = await client.GetOnChainWalletTransaction(walletId.StoreId, walletId.CryptoCode, txdata.TransactionHash.ToString());
|
||||
|
||||
// Check skip doesn't crash
|
||||
await client.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode, skip: 1);
|
||||
|
||||
Assert.Equal(transaction.TransactionHash, txdata.TransactionHash);
|
||||
Assert.Equal(String.Empty, transaction.Comment);
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
@ -3284,11 +3417,16 @@ namespace BTCPayServer.Tests
|
||||
|
||||
var client = await user.CreateClient(Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings);
|
||||
|
||||
var roles = await client.GetServerRoles();
|
||||
Assert.Equal(2,roles.Count);
|
||||
#pragma warning disable CS0618
|
||||
var ownerRole = roles.Single(data => data.Role == StoreRoles.Owner);
|
||||
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(StoreRoles.Owner, storeuser.Role);
|
||||
|
||||
Assert.Equal(ownerRole.Id, storeuser.Role);
|
||||
var user2 = tester.NewAccount();
|
||||
await user2.GrantAccessAsync(false);
|
||||
|
||||
@ -3299,7 +3437,7 @@ namespace BTCPayServer.Tests
|
||||
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 client.AddStoreUser(user.StoreId, new StoreUserData() { Role = StoreRoles.Guest, UserId = user2.UserId });
|
||||
await client.AddStoreUser(user.StoreId, new StoreUserData() { Role = guestRole.Id, UserId = user2.UserId });
|
||||
|
||||
//test no access to api when only a guest
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.GetStoreUsers(user.StoreId));
|
||||
@ -3313,10 +3451,10 @@ namespace BTCPayServer.Tests
|
||||
await user2Client.GetStore(user.StoreId));
|
||||
|
||||
|
||||
await client.AddStoreUser(user.StoreId, new StoreUserData() { Role = StoreRoles.Owner, UserId = user2.UserId });
|
||||
await client.AddStoreUser(user.StoreId, new StoreUserData() { Role = ownerRole.Id, UserId = user2.UserId });
|
||||
await AssertAPIError("duplicate-store-user-role", async () =>
|
||||
await client.AddStoreUser(user.StoreId,
|
||||
new StoreUserData() { Role = StoreRoles.Owner, UserId = user2.UserId }));
|
||||
new StoreUserData() { Role = ownerRole.Id, UserId = user2.UserId }));
|
||||
await user2Client.RemoveStoreUser(user.StoreId, user.UserId);
|
||||
|
||||
|
||||
@ -3432,6 +3570,7 @@ namespace BTCPayServer.Tests
|
||||
PaymentMethod = "BTC_LightningNetwork",
|
||||
Destination = customerInvoice.BOLT11
|
||||
});
|
||||
Assert.Equal(payout.Metadata.ToString(), new JObject().ToString()); //empty
|
||||
Assert.Empty(await adminClient.GetStoreLightningAutomatedPayoutProcessors(admin.StoreId, "BTC_LightningNetwork"));
|
||||
await adminClient.UpdateStoreLightningAutomatedPayoutProcessors(admin.StoreId, "BTC_LightningNetwork",
|
||||
new LightningAutomatedPayoutSettings() { IntervalSeconds = TimeSpan.FromSeconds(600) });
|
||||
@ -3442,6 +3581,46 @@ namespace BTCPayServer.Tests
|
||||
(await adminClient.GetStorePayouts(admin.StoreId, false)).Single(data => data.Id == payout.Id);
|
||||
Assert.Equal(PayoutState.Completed, payoutC.State);
|
||||
});
|
||||
|
||||
payout = await adminClient.CreatePayout(admin.StoreId,
|
||||
new CreatePayoutThroughStoreRequest()
|
||||
{
|
||||
Approved = true,
|
||||
PaymentMethod = "BTC",
|
||||
Destination = (await tester.ExplorerNode.GetNewAddressAsync()).ToString(),
|
||||
Amount = 0.0001m,
|
||||
Metadata = JObject.FromObject(new
|
||||
{
|
||||
source ="apitest",
|
||||
sourceLink = "https://chocolate.com"
|
||||
})
|
||||
});
|
||||
Assert.Equal(payout.Metadata.ToString(), JObject.FromObject(new
|
||||
{
|
||||
source = "apitest",
|
||||
sourceLink = "https://chocolate.com"
|
||||
}).ToString());
|
||||
|
||||
payout =
|
||||
(await adminClient.GetStorePayouts(admin.StoreId, false)).Single(data => data.Id == payout.Id);
|
||||
|
||||
Assert.Equal(payout.Metadata.ToString(), JObject.FromObject(new
|
||||
{
|
||||
source = "apitest",
|
||||
sourceLink = "https://chocolate.com"
|
||||
}).ToString());
|
||||
|
||||
customerInvoice = await tester.CustomerLightningD.CreateInvoice(LightMoney.FromUnit(10, LightMoneyUnit.Satoshi),
|
||||
Guid.NewGuid().ToString(), TimeSpan.FromDays(40));
|
||||
var payout2 = await adminClient.CreatePayout(admin.StoreId,
|
||||
new CreatePayoutThroughStoreRequest()
|
||||
{
|
||||
Approved = true,
|
||||
Amount = new Money(100, MoneyUnit.Satoshi).ToDecimal(MoneyUnit.BTC),
|
||||
PaymentMethod = "BTC_LightningNetwork",
|
||||
Destination = customerInvoice.BOLT11
|
||||
});
|
||||
Assert.Equal(payout2.Amount, new Money(100, MoneyUnit.Satoshi).ToDecimal(MoneyUnit.BTC));
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
@ -3557,9 +3736,12 @@ namespace BTCPayServer.Tests
|
||||
Assert.Single(payouts.Where(data => data.State == PayoutState.InProgress));
|
||||
});
|
||||
|
||||
var txid = await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
|
||||
tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(0.01m) + fee);
|
||||
await tester.WaitForEvent<NewOnChainTransactionEvent>(null, correctEvent: ev => ev.NewTransactionEvent.TransactionData.TransactionHash == txid);
|
||||
uint256 txid = null;
|
||||
await tester.WaitForEvent<NewOnChainTransactionEvent>(async () =>
|
||||
{
|
||||
txid = await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
|
||||
tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(0.01m) + fee);
|
||||
}, correctEvent: ev => ev.NewTransactionEvent.TransactionData.TransactionHash == txid);
|
||||
await tester.PayTester.GetService<PayoutProcessorService>().Restart(new PayoutProcessorService.PayoutProcessorQuery(admin.StoreId, "BTC"));
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
@ -3567,6 +3749,122 @@ namespace BTCPayServer.Tests
|
||||
payouts = await adminClient.GetStorePayouts(admin.StoreId);
|
||||
Assert.Empty(payouts.Where(data => data.State != PayoutState.InProgress));
|
||||
});
|
||||
|
||||
// settings that were added later
|
||||
var settings =
|
||||
Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC"));
|
||||
Assert.False( settings.ProcessNewPayoutsInstantly);
|
||||
Assert.Equal(0m, settings.Threshold);
|
||||
|
||||
//let's use the ProcessNewPayoutsInstantly so that it will trigger instantly
|
||||
|
||||
settings.IntervalSeconds = TimeSpan.FromDays(1);
|
||||
settings.ProcessNewPayoutsInstantly = true;
|
||||
|
||||
await tester.WaitForEvent<NewOnChainTransactionEvent>(async () =>
|
||||
{
|
||||
txid = await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
|
||||
tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(1m) + fee);
|
||||
}, correctEvent: ev => ev.NewTransactionEvent.TransactionData.TransactionHash == txid);
|
||||
|
||||
await adminClient.UpdateStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC", settings);
|
||||
settings =
|
||||
Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC"));
|
||||
Assert.True( settings.ProcessNewPayoutsInstantly);
|
||||
|
||||
var pluginHookService = tester.PayTester.GetService<IPluginHookService>();
|
||||
var beforeHookTcs = new TaskCompletionSource();
|
||||
var afterHookTcs = new TaskCompletionSource();
|
||||
pluginHookService.ActionInvoked += (sender, tuple) =>
|
||||
{
|
||||
switch (tuple.hook)
|
||||
{
|
||||
case "before-automated-payout-processing":
|
||||
beforeHookTcs.TrySetResult();
|
||||
break;
|
||||
case "after-automated-payout-processing":
|
||||
afterHookTcs.TrySetResult();
|
||||
break;
|
||||
}
|
||||
};
|
||||
var payoutThatShouldBeProcessedStraightAway = await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest()
|
||||
{
|
||||
PullPaymentId = pullPayment.Id,
|
||||
Amount = 0.5m,
|
||||
Approved = true,
|
||||
PaymentMethod = "BTC",
|
||||
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
|
||||
});
|
||||
await beforeHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
payouts = await adminClient.GetStorePayouts(admin.StoreId);
|
||||
Assert.Single(payouts.Where(data => data.State == PayoutState.InProgress && data.Id == payoutThatShouldBeProcessedStraightAway.Id));
|
||||
|
||||
beforeHookTcs = new TaskCompletionSource();
|
||||
afterHookTcs = new TaskCompletionSource();
|
||||
//let's test the threshold limiter
|
||||
settings.Threshold = 0.5m;
|
||||
await adminClient.UpdateStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC", settings);
|
||||
|
||||
//quick test: when updating processor, it processes instantly
|
||||
await beforeHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
|
||||
settings =
|
||||
Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC"));
|
||||
Assert.Equal(0.5m, settings.Threshold);
|
||||
|
||||
//create a payout that should not be processed straight away due to threshold
|
||||
|
||||
beforeHookTcs = new TaskCompletionSource();
|
||||
afterHookTcs = new TaskCompletionSource();
|
||||
var payoutThatShouldNotBeProcessedStraightAway = await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest()
|
||||
{
|
||||
Amount = 0.1m,
|
||||
Approved = true,
|
||||
PaymentMethod = "BTC",
|
||||
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
|
||||
});
|
||||
|
||||
await beforeHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
|
||||
payouts = await adminClient.GetStorePayouts(admin.StoreId);
|
||||
Assert.Single(payouts.Where(data => data.State == PayoutState.AwaitingPayment && data.Id == payoutThatShouldNotBeProcessedStraightAway.Id));
|
||||
|
||||
beforeHookTcs = new TaskCompletionSource();
|
||||
afterHookTcs = new TaskCompletionSource();
|
||||
var payoutThatShouldNotBeProcessedStraightAway2 = await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest()
|
||||
{
|
||||
Amount = 0.3m,
|
||||
Approved = true,
|
||||
PaymentMethod = "BTC",
|
||||
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
|
||||
});
|
||||
|
||||
await beforeHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
|
||||
payouts = await adminClient.GetStorePayouts(admin.StoreId);
|
||||
Assert.Equal(2, payouts.Count(data => data.State == PayoutState.AwaitingPayment &&
|
||||
(data.Id == payoutThatShouldNotBeProcessedStraightAway.Id || data.Id == payoutThatShouldNotBeProcessedStraightAway2.Id)));
|
||||
|
||||
beforeHookTcs = new TaskCompletionSource();
|
||||
afterHookTcs = new TaskCompletionSource();
|
||||
var payoutThatShouldNotBeProcessedStraightAway3 = await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest()
|
||||
{
|
||||
Amount = 0.3m,
|
||||
Approved = true,
|
||||
PaymentMethod = "BTC",
|
||||
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
|
||||
});
|
||||
|
||||
await beforeHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
|
||||
payouts = await adminClient.GetStorePayouts(admin.StoreId);
|
||||
Assert.Empty(payouts.Where(data => data.State != PayoutState.InProgress));
|
||||
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
|
@ -1,10 +1,14 @@
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Hosting;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Plugins.Crowdfund.Models;
|
||||
using BTCPayServer.Plugins.PointOfSale;
|
||||
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
@ -19,6 +23,74 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanParseOldYmlCorrectly()
|
||||
{
|
||||
var testOriginalDefaultYmlTemplate = @"
|
||||
green tea:
|
||||
price: 1
|
||||
title: Green Tea
|
||||
description: Lovely, fresh and tender, Meng Ding Gan Lu ('sweet dew') is grown in the lush Meng Ding Mountains of the southwestern province of Sichuan where it has been cultivated for over a thousand years.
|
||||
image: ~/img/pos-sample/green-tea.jpg
|
||||
|
||||
black tea:
|
||||
price: 1
|
||||
title: Black Tea
|
||||
description: Tian Jian Tian Jian means 'heavenly tippy tea' in Chinese, and it describes the finest grade of dark tea. Our Tian Jian dark tea is from Hunan province which is famous for making some of the best dark teas available.
|
||||
image: ~/img/pos-sample/black-tea.jpg
|
||||
|
||||
rooibos:
|
||||
price: 1.2
|
||||
title: Rooibos
|
||||
description: Rooibos is a dramatic red tea made from a South African herb that contains polyphenols and flavonoids. Often called 'African redbush tea', Rooibos herbal tea delights the senses and delivers potential health benefits with each caffeine-free sip.
|
||||
image: ~/img/pos-sample/rooibos.jpg
|
||||
|
||||
pu erh:
|
||||
price: 2
|
||||
title: Pu Erh
|
||||
description: This loose pur-erh tea is produced in Yunnan Province, China. The process in a relatively high humidity environment has mellowed the elemental character of the tea when compared to young Pu-erh.
|
||||
image: ~/img/pos-sample/pu-erh.jpg
|
||||
|
||||
herbal tea:
|
||||
price: 1.8
|
||||
title: Herbal Tea
|
||||
description: Chamomile tea is made from the flower heads of the chamomile plant. The medicinal use of chamomile dates back to the ancient Egyptians, Romans and Greeks. Pay us what you want!
|
||||
image: ~/img/pos-sample/herbal-tea.jpg
|
||||
custom: true
|
||||
|
||||
fruit tea:
|
||||
price: 1.5
|
||||
title: Fruit Tea
|
||||
description: The Tibetan Himalayas, the land is majestic and beautiful—a spiritual place where, despite the perilous environment, many journey seeking enlightenment. Pay us what you want!
|
||||
image: ~/img/pos-sample/fruit-tea.jpg
|
||||
inventory: 5
|
||||
custom: true
|
||||
";
|
||||
var parsedDefault = MigrationStartupTask.ParsePOSYML(testOriginalDefaultYmlTemplate);
|
||||
Assert.Equal(6, parsedDefault.Length);
|
||||
Assert.Equal( "Green Tea" ,parsedDefault[0].Title);
|
||||
Assert.Equal( "green tea" ,parsedDefault[0].Id);
|
||||
Assert.Equal( "Lovely, fresh and tender, Meng Ding Gan Lu ('sweet dew') is grown in the lush Meng Ding Mountains of the southwestern province of Sichuan where it has been cultivated for over a thousand years." ,parsedDefault[0].Description);
|
||||
Assert.Null( parsedDefault[0].BuyButtonText);
|
||||
Assert.Equal( "~/img/pos-sample/green-tea.jpg" ,parsedDefault[0].Image);
|
||||
Assert.Equal( 1 ,parsedDefault[0].Price);
|
||||
Assert.Equal( ViewPointOfSaleViewModel.ItemPriceType.Fixed ,parsedDefault[0].PriceType);
|
||||
Assert.Null( parsedDefault[0].AdditionalData);
|
||||
Assert.Null( parsedDefault[0].PaymentMethods);
|
||||
|
||||
|
||||
Assert.Equal( "Herbal Tea" ,parsedDefault[4].Title);
|
||||
Assert.Equal( "herbal tea" ,parsedDefault[4].Id);
|
||||
Assert.Equal( "Chamomile tea is made from the flower heads of the chamomile plant. The medicinal use of chamomile dates back to the ancient Egyptians, Romans and Greeks. Pay us what you want!" ,parsedDefault[4].Description);
|
||||
Assert.Null( parsedDefault[4].BuyButtonText);
|
||||
Assert.Equal( "~/img/pos-sample/herbal-tea.jpg" ,parsedDefault[4].Image);
|
||||
Assert.Equal( 1.8m ,parsedDefault[4].Price);
|
||||
Assert.Equal( ViewPointOfSaleViewModel.ItemPriceType.Minimum ,parsedDefault[4].PriceType);
|
||||
Assert.Null( parsedDefault[4].AdditionalData);
|
||||
Assert.Null( parsedDefault[4].PaymentMethods);
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanUsePoSApp1()
|
||||
@ -53,21 +125,59 @@ donation:
|
||||
price: 1.02
|
||||
custom: true
|
||||
";
|
||||
vmpos.Currency = "EUR";
|
||||
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
|
||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
var publicApps = user.GetController<UIPointOfSaleController>();
|
||||
var vmview = await publicApps.ViewPointOfSale(app.Id, PosViewType.Cart).AssertViewModelAsync<ViewPointOfSaleViewModel>();
|
||||
|
||||
Assert.Equal("EUR", vmview.CurrencyCode);
|
||||
// apple shouldn't be available since we it's set to "disabled: true" above
|
||||
Assert.Equal(2, vmview.Items.Length);
|
||||
Assert.Equal("orange", vmview.Items[0].Title);
|
||||
Assert.Equal("donation", vmview.Items[1].Title);
|
||||
// orange is available
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, null, null, null, null, "orange").Result);
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, choiceKey: "orange").Result);
|
||||
// apple is not found
|
||||
Assert.IsType<NotFoundResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, null, null, null, null, "apple").Result);
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, choiceKey: "apple").Result);
|
||||
|
||||
// List
|
||||
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||
app = appList.Apps[0];
|
||||
apps = user.GetController<UIAppsController>();
|
||||
appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType, Settings = "{\"currency\":\"EUR\"}" };
|
||||
apps.HttpContext.SetAppData(appData);
|
||||
pos.HttpContext.SetAppData(appData);
|
||||
Assert.Single(appList.Apps);
|
||||
Assert.Equal("test", app.AppName);
|
||||
Assert.True(app.Role.ToPermissionSet(appList.Apps[0].StoreId).Contains(Policies.CanModifyStoreSettings, app.StoreId));
|
||||
Assert.Equal(user.StoreId, app.StoreId);
|
||||
Assert.False(app.Archived);
|
||||
// Archive
|
||||
redirect = Assert.IsType<RedirectResult>(apps.ToggleArchive(app.Id).Result);
|
||||
Assert.EndsWith("/settings/pos", redirect.Url);
|
||||
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||
Assert.Empty(appList.Apps);
|
||||
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId, archived: true).Result).Model);
|
||||
app = appList.Apps[0];
|
||||
Assert.True(app.Archived);
|
||||
Assert.IsType<NotFoundResult>(await publicApps.ViewPointOfSale(app.Id, PosViewType.Static));
|
||||
// Unarchive
|
||||
redirect = Assert.IsType<RedirectResult>(apps.ToggleArchive(app.Id).Result);
|
||||
Assert.EndsWith("/settings/pos", redirect.Url);
|
||||
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||
app = appList.Apps[0];
|
||||
Assert.False(app.Archived);
|
||||
Assert.IsType<ViewResult>(await publicApps.ViewPointOfSale(app.Id, PosViewType.Static));
|
||||
// Delete
|
||||
Assert.IsType<ViewResult>(apps.DeleteApp(app.Id));
|
||||
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(app.Id).Result);
|
||||
Assert.Equal(nameof(UIStoresController.Dashboard), redirectToAction.ActionName);
|
||||
appList = await apps.ListApps(user.StoreId).AssertViewModelAsync<ListAppsViewModel>();
|
||||
Assert.Empty(appList.Apps);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,6 @@ namespace BTCPayServer.Tests
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
var user2 = tester.NewAccount();
|
||||
|
||||
await user2.GrantAccessAsync();
|
||||
|
||||
var paymentRequestController = user.GetController<UIPaymentRequestController>();
|
||||
@ -162,7 +161,7 @@ namespace BTCPayServer.Tests
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
await user.GrantAccessAsync();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
var paymentRequestController = user.GetController<UIPaymentRequestController>();
|
||||
@ -170,7 +169,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.IsType<NotFoundResult>(await
|
||||
paymentRequestController.CancelUnpaidPendingInvoice(Guid.NewGuid().ToString(), false));
|
||||
|
||||
var request = new UpdatePaymentRequestViewModel()
|
||||
var request = new UpdatePaymentRequestViewModel
|
||||
{
|
||||
Title = "original juice",
|
||||
Currency = "BTC",
|
||||
|
@ -180,7 +180,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
Driver.FindElement(By.Id("StoreSelectorToggle")).Click();
|
||||
}
|
||||
Driver.WaitForElement(By.Id("StoreSelectorCreate")).Click();
|
||||
GoToUrl("/stores/create");
|
||||
var name = "Store" + RandomUtils.GetUInt64();
|
||||
TestLogs.LogInformation($"Created store {name}");
|
||||
Driver.WaitForElement(By.Id("Name")).SendKeys(name);
|
||||
@ -313,8 +313,6 @@ namespace BTCPayServer.Tests
|
||||
|
||||
var connectionString = connectionType switch
|
||||
{
|
||||
LightningConnectionType.Charge =>
|
||||
$"type=charge;server={Server.MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true",
|
||||
LightningConnectionType.CLightning =>
|
||||
$"type=clightning;server={((CLightningClient)Server.MerchantLightningD).Address.AbsoluteUri}",
|
||||
LightningConnectionType.LndREST =>
|
||||
@ -395,6 +393,10 @@ namespace BTCPayServer.Tests
|
||||
public void GoToHome()
|
||||
{
|
||||
Driver.Navigate().GoToUrl(ServerUri);
|
||||
if (Driver.PageSource.Contains("id=\"SkipWizard\""))
|
||||
{
|
||||
Driver.FindElement(By.Id("SkipWizard")).Click();
|
||||
}
|
||||
}
|
||||
|
||||
public void Logout()
|
||||
@ -563,7 +565,7 @@ namespace BTCPayServer.Tests
|
||||
walletId ??= WalletId;
|
||||
GoToWallet(walletId, WalletsNavPages.Receive);
|
||||
Driver.FindElement(By.Id("generateButton")).Click();
|
||||
var addressStr = Driver.FindElement(By.Id("Address")).GetAttribute("value");
|
||||
var addressStr = Driver.FindElement(By.Id("Address")).GetAttribute("data-text");
|
||||
var address = BitcoinAddress.Create(addressStr, ((BTCPayNetwork)Server.NetworkProvider.GetNetwork(walletId.CryptoCode)).NBitcoinNetwork);
|
||||
for (var i = 0; i < coins; i++)
|
||||
{
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -92,7 +92,7 @@ namespace BTCPayServer.Tests
|
||||
#endif
|
||||
public void ActivateLightning()
|
||||
{
|
||||
ActivateLightning(LightningConnectionType.Charge);
|
||||
ActivateLightning(LightningConnectionType.CLightning);
|
||||
}
|
||||
public void ActivateLightning(LightningConnectionType internalNode)
|
||||
{
|
||||
@ -109,14 +109,7 @@ namespace BTCPayServer.Tests
|
||||
string connectionString = null;
|
||||
if (connectionType is null)
|
||||
return LightningSupportedPaymentMethod.InternalNode;
|
||||
if (connectionType == LightningConnectionType.Charge)
|
||||
{
|
||||
if (isMerchant)
|
||||
connectionString = $"type=charge;server={MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true";
|
||||
else
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
else if (connectionType == LightningConnectionType.CLightning)
|
||||
if (connectionType == LightningConnectionType.CLightning)
|
||||
{
|
||||
if (isMerchant)
|
||||
connectionString = "type=clightning;server=" +
|
||||
|
@ -40,6 +40,7 @@ namespace BTCPayServer.Tests
|
||||
public class TestAccount
|
||||
{
|
||||
readonly ServerTester parent;
|
||||
public string LNAddress;
|
||||
|
||||
public TestAccount(ServerTester parent)
|
||||
{
|
||||
@ -242,7 +243,7 @@ namespace BTCPayServer.Tests
|
||||
policies.LockSubscription = false;
|
||||
await account.Register(RegisterDetails);
|
||||
}
|
||||
|
||||
TestLogs.LogInformation($"UserId: {account.RegisteredUserId} Password: {Password}");
|
||||
UserId = account.RegisteredUserId;
|
||||
Email = RegisterDetails.Email;
|
||||
IsAdmin = account.RegisteredAdmin;
|
||||
@ -277,7 +278,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
public bool IsAdmin { get; internal set; }
|
||||
|
||||
public void RegisterLightningNode(string cryptoCode, LightningConnectionType connectionType, bool isMerchant = true)
|
||||
public void RegisterLightningNode(string cryptoCode, LightningConnectionType? connectionType = null, bool isMerchant = true)
|
||||
{
|
||||
RegisterLightningNodeAsync(cryptoCode, connectionType, isMerchant).GetAwaiter().GetResult();
|
||||
}
|
||||
@ -309,8 +310,9 @@ namespace BTCPayServer.Tests
|
||||
Assert.False(true, storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage);
|
||||
}
|
||||
|
||||
public async Task<Coin> ReceiveUTXO(Money value, BTCPayNetwork network)
|
||||
public async Task<Coin> ReceiveUTXO(Money value, BTCPayNetwork network = null)
|
||||
{
|
||||
network ??= SupportedNetwork;
|
||||
var cashCow = parent.ExplorerNode;
|
||||
var btcPayWallet = parent.PayTester.GetService<BTCPayWalletProvider>().GetWallet(network);
|
||||
var address = (await btcPayWallet.ReserveAddressAsync(this.DerivationScheme)).Address;
|
||||
@ -470,7 +472,10 @@ namespace BTCPayServer.Tests
|
||||
var req = await _server.GetNextRequest(cancellation);
|
||||
var bytes = await req.Request.Body.ReadBytesAsync((int)req.Request.Headers.ContentLength);
|
||||
var callback = Encoding.UTF8.GetString(bytes);
|
||||
_webhookEvents.Add(JsonConvert.DeserializeObject<WebhookInvoiceEvent>(callback));
|
||||
lock (_webhookEvents)
|
||||
{
|
||||
_webhookEvents.Add(JsonConvert.DeserializeObject<WebhookInvoiceEvent>(callback));
|
||||
}
|
||||
req.Response.StatusCode = 200;
|
||||
_server.Done();
|
||||
}
|
||||
@ -487,18 +492,21 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
int retry = 0;
|
||||
retry:
|
||||
foreach (var evt in WebhookEvents)
|
||||
lock (WebhookEvents)
|
||||
{
|
||||
if (evt.Type == eventType)
|
||||
foreach (var evt in WebhookEvents)
|
||||
{
|
||||
var typedEvt = evt.ReadAs<TEvent>();
|
||||
try
|
||||
{
|
||||
assert(typedEvt);
|
||||
return typedEvt;
|
||||
}
|
||||
catch (XunitException)
|
||||
if (evt.Type == eventType)
|
||||
{
|
||||
var typedEvt = evt.ReadAs<TEvent>();
|
||||
try
|
||||
{
|
||||
assert(typedEvt);
|
||||
return typedEvt;
|
||||
}
|
||||
catch (XunitException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -540,12 +548,101 @@ retry:
|
||||
public async Task AddGuest(string userId)
|
||||
{
|
||||
var repo = this.parent.PayTester.GetService<StoreRepository>();
|
||||
await repo.AddStoreUser(StoreId, userId, "Guest");
|
||||
await repo.AddStoreUser(StoreId, userId, StoreRoleId.Guest);
|
||||
}
|
||||
public async Task AddOwner(string userId)
|
||||
{
|
||||
var repo = this.parent.PayTester.GetService<StoreRepository>();
|
||||
await repo.AddStoreUser(StoreId, userId, "Owner");
|
||||
await repo.AddStoreUser(StoreId, userId, StoreRoleId.Owner);
|
||||
}
|
||||
|
||||
public async Task<uint256> PayOnChain(string invoiceId)
|
||||
{
|
||||
var cryptoCode = "BTC";
|
||||
var client = await CreateClient();
|
||||
var methods = await client.GetInvoicePaymentMethods(StoreId, invoiceId);
|
||||
var method = methods.First(m => m.PaymentMethod == cryptoCode);
|
||||
var address = method.Destination;
|
||||
var tx = await client.CreateOnChainTransaction(StoreId, cryptoCode, new CreateOnChainTransactionRequest()
|
||||
{
|
||||
Destinations = new List<CreateOnChainTransactionRequest.CreateOnChainTransactionRequestDestination>()
|
||||
{
|
||||
new ()
|
||||
{
|
||||
Destination = address,
|
||||
Amount = method.Due
|
||||
}
|
||||
},
|
||||
FeeRate = new FeeRate(1.0m)
|
||||
});
|
||||
await WaitInvoicePaid(invoiceId);
|
||||
return tx.TransactionHash;
|
||||
}
|
||||
|
||||
public async Task PayOnBOLT11(string invoiceId)
|
||||
{
|
||||
var cryptoCode = "BTC";
|
||||
var client = await CreateClient();
|
||||
var methods = await client.GetInvoicePaymentMethods(StoreId, invoiceId);
|
||||
var method = methods.First(m => m.PaymentMethod == $"{cryptoCode}-LightningNetwork");
|
||||
var bolt11 = method.Destination;
|
||||
TestLogs.LogInformation("PAYING");
|
||||
await parent.CustomerLightningD.Pay(bolt11);
|
||||
TestLogs.LogInformation("PAID");
|
||||
await WaitInvoicePaid(invoiceId);
|
||||
}
|
||||
|
||||
public async Task PayOnLNUrl(string invoiceId)
|
||||
{
|
||||
var cryptoCode = "BTC";
|
||||
var network = SupportedNetwork.NBitcoinNetwork;
|
||||
var client = await CreateClient();
|
||||
var methods = await client.GetInvoicePaymentMethods(StoreId, invoiceId);
|
||||
var method = methods.First(m => m.PaymentMethod == $"{cryptoCode}-LNURLPAY");
|
||||
var lnurL = LNURL.LNURL.Parse(method.PaymentLink, out var tag);
|
||||
var http = new HttpClient();
|
||||
var payreq = (LNURL.LNURLPayRequest)await LNURL.LNURL.FetchInformation(lnurL, tag, http);
|
||||
var resp = await payreq.SendRequest(payreq.MinSendable, network, http);
|
||||
var bolt11 = resp.Pr;
|
||||
await parent.CustomerLightningD.Pay(bolt11);
|
||||
await WaitInvoicePaid(invoiceId);
|
||||
}
|
||||
|
||||
public Task WaitInvoicePaid(string invoiceId)
|
||||
{
|
||||
return TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
var client = await CreateClient();
|
||||
var invoice = await client.GetInvoice(StoreId, invoiceId);
|
||||
if (invoice.Status == InvoiceStatus.Settled)
|
||||
return;
|
||||
Assert.Equal(InvoiceStatus.Processing, invoice.Status);
|
||||
});
|
||||
}
|
||||
|
||||
public async Task PayOnLNAddress(string lnAddrUser = null)
|
||||
{
|
||||
lnAddrUser ??= LNAddress;
|
||||
var network = SupportedNetwork.NBitcoinNetwork;
|
||||
var payReqStr = await (await parent.PayTester.HttpClient.GetAsync($".well-known/lnurlp/{lnAddrUser}")).Content.ReadAsStringAsync();
|
||||
var payreq = JsonConvert.DeserializeObject<LNURL.LNURLPayRequest>(payReqStr);
|
||||
var resp = await payreq.SendRequest(payreq.MinSendable, network, parent.PayTester.HttpClient);
|
||||
var bolt11 = resp.Pr;
|
||||
await parent.CustomerLightningD.Pay(bolt11);
|
||||
}
|
||||
|
||||
public async Task<string> CreateLNAddress()
|
||||
{
|
||||
var lnAddrUser = Guid.NewGuid().ToString();
|
||||
var ctx = parent.PayTester.GetService<ApplicationDbContextFactory>().CreateContext();
|
||||
ctx.LightningAddresses.Add(new()
|
||||
{
|
||||
StoreDataId = StoreId,
|
||||
Username = lnAddrUser
|
||||
});
|
||||
await ctx.SaveChangesAsync();
|
||||
LNAddress = lnAddrUser;
|
||||
return lnAddrUser;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,12 +16,14 @@ using BTCPayServer.Storage.Models;
|
||||
using BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.FileSystemGlobbing;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using Xunit.Sdk;
|
||||
using static BTCPayServer.HostedServices.PullPaymentHostedService.PayoutApproval;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -177,7 +179,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains(rates, e => e.CurrencyPair == new CurrencyPair("XMR", "BTC") && e.BidAsk.Bid < 1.0m);
|
||||
|
||||
// Check we didn't skip too many exchanges
|
||||
Assert.InRange(skipped, 0, 3);
|
||||
Assert.InRange(skipped, 0, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -290,9 +292,43 @@ retry:
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanGetRateCryptoCurrenciesByDefault()
|
||||
public async Task CanGetRateFromRecommendedExchanges()
|
||||
{
|
||||
string[] brokenShitcoins = { "BTX_USD", "CHC_USD" };
|
||||
var factory = FastTests.CreateBTCPayRateFactory();
|
||||
var fetcher = new RateFetcher(factory);
|
||||
var provider = new BTCPayNetworkProvider(ChainName.Mainnet);
|
||||
var b = new StoreBlob();
|
||||
string[] temporarilyBroken = { "COP", "UGX" };
|
||||
foreach (var k in StoreBlob.RecommendedExchanges)
|
||||
{
|
||||
b.DefaultCurrency = k.Key;
|
||||
var rules = b.GetDefaultRateRules(provider);
|
||||
var pairs = new[] { CurrencyPair.Parse($"BTC_{k.Key}") }.ToHashSet();
|
||||
var result = fetcher.FetchRates(pairs, rules, default);
|
||||
foreach ((CurrencyPair key, Task<RateResult> value) in result)
|
||||
{
|
||||
TestLogs.LogInformation($"Testing {key} when default currency is {k.Key}");
|
||||
var rateResult = await value;
|
||||
var hasRate = rateResult.BidAsk != null;
|
||||
|
||||
if (temporarilyBroken.Contains(k.Key))
|
||||
{
|
||||
if (!hasRate)
|
||||
{
|
||||
TestLogs.LogInformation($"Skipping {key} because it is marked as temporarily broken");
|
||||
continue;
|
||||
}
|
||||
TestLogs.LogInformation($"Note: {key} is marked as temporarily broken, but the rate is available");
|
||||
}
|
||||
Assert.True(hasRate, $"Impossible to get the rate {rateResult.EvaluatedRule}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanGetRateCryptoCurrenciesByDefault()
|
||||
{
|
||||
using var cts = new CancellationTokenSource(60_000);
|
||||
var provider = new BTCPayNetworkProvider(ChainName.Mainnet);
|
||||
var factory = FastTests.CreateBTCPayRateFactory();
|
||||
var fetcher = new RateFetcher(factory);
|
||||
@ -301,19 +337,29 @@ retry:
|
||||
.Select(c => new CurrencyPair(c.CryptoCode, "USD"))
|
||||
.ToHashSet();
|
||||
|
||||
string[] brokenShitcoins = { "BTG", "BTX" };
|
||||
bool IsBrokenShitcoin(CurrencyPair p) => brokenShitcoins.Contains(p.Left) || brokenShitcoins.Contains(p.Right);
|
||||
foreach (var _ in brokenShitcoins)
|
||||
{
|
||||
foreach (var p in pairs.Where(IsBrokenShitcoin).ToArray())
|
||||
{
|
||||
TestLogs.LogInformation($"Skipping {p} because it is marked as broken");
|
||||
pairs.Remove(p);
|
||||
}
|
||||
}
|
||||
|
||||
var rules = new StoreBlob().GetDefaultRateRules(provider);
|
||||
var result = fetcher.FetchRates(pairs, rules, default);
|
||||
var result = fetcher.FetchRates(pairs, rules, cts.Token);
|
||||
foreach ((CurrencyPair key, Task<RateResult> value) in result)
|
||||
{
|
||||
var rateResult = value.GetAwaiter().GetResult();
|
||||
var rateResult = await value;
|
||||
TestLogs.LogInformation($"Testing {key}");
|
||||
if (brokenShitcoins.Contains(key.ToString()))
|
||||
continue;
|
||||
Assert.True(rateResult.BidAsk != null, $"Impossible to get the rate {rateResult.EvaluatedRule}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public async Task CheckJsContent()
|
||||
{
|
||||
// This test verify that no malicious js is added in the minified files.
|
||||
@ -322,42 +368,77 @@ retry:
|
||||
var actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "bootstrap", "bootstrap.bundle.min.js").Trim();
|
||||
var version = Regex.Match(actual, "Bootstrap v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
|
||||
var expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/bootstrap@{version}/dist/js/bootstrap.bundle.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "clipboard.js", "clipboard.js");
|
||||
expected = (await (await client.GetAsync("https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.8/clipboard.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vuejs", "vue.min.js").Trim();
|
||||
version = Regex.Match(actual, "Vue\\.js v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
|
||||
expected = (await (await client.GetAsync($"https://cdnjs.cloudflare.com/ajax/libs/vue/{version}/vue.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "i18next", "i18next.min.js").Trim();
|
||||
expected = (await (await client.GetAsync("https://cdnjs.cloudflare.com/ajax/libs/i18next/22.0.6/i18next.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "i18next", "i18nextHttpBackend.min.js").Trim();
|
||||
expected = (await (await client.GetAsync("https://cdnjs.cloudflare.com/ajax/libs/i18next-http-backend/2.0.1/i18nextHttpBackend.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "i18next", "vue-i18next.js").Trim();
|
||||
expected = (await (await client.GetAsync("https://unpkg.com/@panter/vue-i18next@0.15.2/dist/vue-i18next.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vue-qrcode", "vue-qrcode.min.js").Trim();
|
||||
version = Regex.Match(actual, "vue-qrcode v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
|
||||
expected = (await (await client.GetAsync($"https://unpkg.com/@chenfengyuan/vue-qrcode@{version}/dist/vue-qrcode.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "tom-select", "tom-select.complete.min.js").Trim();
|
||||
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/js/tom-select.complete.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
version = Regex.Match(actual, "Tom Select v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
|
||||
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/tom-select@{version}/dist/js/tom-select.complete.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
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();
|
||||
Assert.Equal(expected, actual);
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vue-sortable", "sortable.min.js").Trim();
|
||||
version = Regex.Match(actual, "Sortable ([0-9]+.[0-9]+.[0-9]+) ").Groups[1].Value;
|
||||
expected = (await (await client.GetAsync($"https://unpkg.com/sortablejs@{version}/Sortable.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "bootstrap-vue", "bootstrap-vue.min.js").Trim();
|
||||
version = Regex.Match(actual, "BootstrapVue ([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
|
||||
expected = (await (await client.GetAsync($"https://cdnjs.cloudflare.com/ajax/libs/bootstrap-vue/{version}/bootstrap-vue.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "FileSaver", "FileSaver.min.js").Trim();
|
||||
expected = (await (await client.GetAsync($"https://raw.githubusercontent.com/eligrey/FileSaver.js/43bbd2f0ae6794f8d452cd360e9d33aef6071234/dist/FileSaver.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "papaparse", "papaparse.min.js").Trim();
|
||||
expected = (await (await client.GetAsync($"https://raw.githubusercontent.com/mholt/PapaParse/5.4.1/papaparse.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vue-sanitize-directive", "vue-sanitize-directive.umd.min.js").Trim();
|
||||
version = Regex.Match(actual, "Original file: /npm/vue-sanitize-directive@([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
|
||||
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/vue-sanitize-directive@{version}/dist/vue-sanitize-directive.umd.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "decimal.js", "decimal.min.js").Trim();
|
||||
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);
|
||||
}
|
||||
|
||||
private void EqualJsContent(string expected, string actual)
|
||||
{
|
||||
if (expected != actual)
|
||||
Assert.Equal(expected, actual.ReplaceLineEndings("\n"));
|
||||
}
|
||||
|
||||
string GetFileContent(params string[] path)
|
||||
|
@ -39,6 +39,7 @@ using BTCPayServer.Plugins.PayButton;
|
||||
using BTCPayServer.Plugins.PointOfSale;
|
||||
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||
using BTCPayServer.Security.Bitpay;
|
||||
using BTCPayServer.Security.Greenfield;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
@ -385,11 +386,11 @@ namespace BTCPayServer.Tests
|
||||
var newBolt11 = newInvoice.CryptoInfo.First(o => o.PaymentUrls.BOLT11 != null).PaymentUrls.BOLT11;
|
||||
var oldBolt11 = invoice.CryptoInfo.First(o => o.PaymentUrls.BOLT11 != null).PaymentUrls.BOLT11;
|
||||
Assert.NotEqual(newBolt11, oldBolt11);
|
||||
Assert.Equal(newInvoice.BtcDue.GetValue(),
|
||||
Assert.Equal(newInvoice.BtcDue.ToDecimal(MoneyUnit.BTC),
|
||||
BOLT11PaymentRequest.Parse(newBolt11, Network.RegTest).MinimumAmount.ToDecimal(LightMoneyUnit.BTC));
|
||||
}, 40000);
|
||||
|
||||
TestLogs.LogInformation($"Paying invoice {newInvoice.Id} remaining due amount {newInvoice.BtcDue.GetValue()} 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);
|
||||
@ -466,14 +467,6 @@ namespace BTCPayServer.Tests
|
||||
await ProcessLightningPayment(LightningConnectionType.CLightning);
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
[Trait("Integration", "Integration")]
|
||||
[Trait("Lightning", "Lightning")]
|
||||
public async Task CanSendLightningPaymentCharge()
|
||||
{
|
||||
await ProcessLightningPayment(LightningConnectionType.Charge);
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
[Trait("Integration", "Integration")]
|
||||
[Trait("Lightning", "Lightning")]
|
||||
@ -726,7 +719,7 @@ namespace BTCPayServer.Tests
|
||||
btcDerivationScheme.GetDerivation(new KeyPath("0/90")).ScriptPubKey, Money.Coins(1.0m));
|
||||
tester.ExplorerNode.Generate(1);
|
||||
var transactions = Assert.IsType<ListTransactionsViewModel>(Assert
|
||||
.IsType<ViewResult>(walletController.WalletTransactions(walletId).Result).Model);
|
||||
.IsType<ViewResult>(walletController.WalletTransactions(walletId, loadTransactions: true).Result).Model);
|
||||
Assert.Empty(transactions.Transactions);
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(walletController.WalletRescan(walletId, rescan).Result);
|
||||
@ -755,7 +748,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.NotNull(rescan.TimeOfScan);
|
||||
Assert.Equal(1, rescan.LastSuccess.Found);
|
||||
transactions = Assert.IsType<ListTransactionsViewModel>(Assert
|
||||
.IsType<ViewResult>(walletController.WalletTransactions(walletId).Result).Model);
|
||||
.IsType<ViewResult>(walletController.WalletTransactions(walletId, loadTransactions: true).Result).Model);
|
||||
var tx = Assert.Single(transactions.Transactions);
|
||||
Assert.Equal(tx.Id, txId.ToString());
|
||||
|
||||
@ -770,7 +763,7 @@ namespace BTCPayServer.Tests
|
||||
await walletController.ModifyTransaction(walletId, tx.Id, addcomment: "hello"));
|
||||
|
||||
transactions = Assert.IsType<ListTransactionsViewModel>(Assert
|
||||
.IsType<ViewResult>(walletController.WalletTransactions(walletId).Result).Model);
|
||||
.IsType<ViewResult>(walletController.WalletTransactions(walletId, loadTransactions: true).Result).Model);
|
||||
tx = Assert.Single(transactions.Transactions);
|
||||
|
||||
Assert.Equal("hello", tx.Comment);
|
||||
@ -782,7 +775,7 @@ namespace BTCPayServer.Tests
|
||||
await walletController.ModifyTransaction(walletId, tx.Id, removelabel: "test2"));
|
||||
|
||||
transactions = Assert.IsType<ListTransactionsViewModel>(Assert
|
||||
.IsType<ViewResult>(walletController.WalletTransactions(walletId).Result).Model);
|
||||
.IsType<ViewResult>(walletController.WalletTransactions(walletId, loadTransactions: true).Result).Model);
|
||||
tx = Assert.Single(transactions.Transactions);
|
||||
|
||||
Assert.Equal("hello", tx.Comment);
|
||||
@ -1634,7 +1627,7 @@ namespace BTCPayServer.Tests
|
||||
var user = tester.NewAccount();
|
||||
var cryptoCode = "BTC";
|
||||
user.GrantAccess(true);
|
||||
user.RegisterLightningNode(cryptoCode, LightningConnectionType.Charge);
|
||||
user.RegisterLightningNode(cryptoCode);
|
||||
user.SetLNUrl(cryptoCode, false);
|
||||
var vm = user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModel<CheckoutAppearanceViewModel>();
|
||||
var criteria = Assert.Single(vm.PaymentMethodCriteria);
|
||||
@ -1654,7 +1647,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(PaymentTypes.LightningLike.ToString(), invoice.CryptoInfo[0].PaymentType);
|
||||
|
||||
// Activating LNUrl, we should still have only 1 payment criteria that can be set.
|
||||
user.RegisterLightningNode(cryptoCode, LightningConnectionType.Charge);
|
||||
user.RegisterLightningNode(cryptoCode);
|
||||
user.SetLNUrl(cryptoCode, true);
|
||||
vm = user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModel<CheckoutAppearanceViewModel>();
|
||||
criteria = Assert.Single(vm.PaymentMethodCriteria);
|
||||
@ -1707,109 +1700,6 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanExportInvoicesJson()
|
||||
{
|
||||
decimal GetFieldValue(string input, string fieldName)
|
||||
{
|
||||
var match = Regex.Match(input, $"\"{fieldName}\":([^,]*)");
|
||||
Assert.True(match.Success);
|
||||
return decimal.Parse(match.Groups[1].Value.Trim(), CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
async Task<object[]> GetExport(TestAccount account, string storeId = null)
|
||||
{
|
||||
var content = await account.GetController<UIInvoiceController>(false)
|
||||
.Export("json", storeId);
|
||||
var result = Assert.IsType<ContentResult>(content);
|
||||
Assert.Equal("application/json", result.ContentType);
|
||||
return JsonConvert.DeserializeObject<object[]>(result.Content ?? "[]");
|
||||
}
|
||||
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
await user.GrantAccessAsync();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
await user.SetNetworkFeeMode(NetworkFeeMode.Always);
|
||||
var invoice = await user.BitPay.CreateInvoiceAsync(
|
||||
new Invoice
|
||||
{
|
||||
Price = 10,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some \", description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
var networkFee = new FeeRate(invoice.MinerFees["BTC"].SatoshiPerBytes).GetFee(100);
|
||||
var result = await GetExport(user);
|
||||
Assert.Single(result);
|
||||
|
||||
var cashCow = tester.ExplorerNode;
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
|
||||
var firstPayment = invoice.CryptoInfo[0].TotalDue - 3 * networkFee;
|
||||
cashCow.SendToAddress(invoiceAddress, firstPayment);
|
||||
Thread.Sleep(1000); // prevent race conditions, ordering payments
|
||||
// look if you can reduce thread sleep, this was min value for me
|
||||
|
||||
// should reduce invoice due by 0 USD because payment = network fee
|
||||
cashCow.SendToAddress(invoiceAddress, networkFee);
|
||||
Thread.Sleep(1000);
|
||||
|
||||
// pay remaining amount
|
||||
cashCow.SendToAddress(invoiceAddress, 4 * networkFee);
|
||||
Thread.Sleep(1000);
|
||||
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
var parsedJson = await GetExport(user);
|
||||
Assert.Equal(3, parsedJson.Length);
|
||||
|
||||
var invoiceDueAfterFirstPayment = (3 * networkFee).ToDecimal(MoneyUnit.BTC) * invoice.Rate;
|
||||
var pay1str = parsedJson[0].ToString();
|
||||
Assert.Contains("\"InvoiceItemDesc\": \"Some \\\", description\"", pay1str);
|
||||
Assert.Equal(invoiceDueAfterFirstPayment, GetFieldValue(pay1str, "InvoiceDue"));
|
||||
Assert.Contains("\"InvoicePrice\": 10.0", pay1str);
|
||||
Assert.Contains("\"ConversionRate\": 5000.0", pay1str);
|
||||
Assert.Contains($"\"InvoiceId\": \"{invoice.Id}\",", pay1str);
|
||||
|
||||
var pay2str = parsedJson[1].ToString();
|
||||
Assert.Equal(invoiceDueAfterFirstPayment, GetFieldValue(pay2str, "InvoiceDue"));
|
||||
|
||||
var pay3str = parsedJson[2].ToString();
|
||||
Assert.Contains("\"InvoiceDue\": 0", pay3str);
|
||||
});
|
||||
|
||||
// create an invoice for a new store and check responses with and without store id
|
||||
var otherUser = tester.NewAccount();
|
||||
await otherUser.GrantAccessAsync();
|
||||
otherUser.RegisterDerivationScheme("BTC");
|
||||
await otherUser.SetNetworkFeeMode(NetworkFeeMode.Always);
|
||||
var newInvoice = await otherUser.BitPay.CreateInvoiceAsync(
|
||||
new Invoice
|
||||
{
|
||||
Price = 21,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some \", description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
await otherUser.PayInvoice(newInvoice.Id);
|
||||
Assert.Single(await GetExport(otherUser));
|
||||
Assert.Single(await GetExport(otherUser, otherUser.StoreId));
|
||||
Assert.Equal(3, (await GetExport(user, user.StoreId)).Length);
|
||||
Assert.Equal(3, (await GetExport(user)).Length);
|
||||
|
||||
await otherUser.AddOwner(user.UserId);
|
||||
Assert.Equal(4, (await GetExport(user)).Length);
|
||||
Assert.Single(await GetExport(user, otherUser.StoreId));
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanChangeNetworkFeeMode()
|
||||
@ -1899,45 +1789,6 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanExportInvoicesCsv()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
await user.GrantAccessAsync();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
await user.SetNetworkFeeMode(NetworkFeeMode.Always);
|
||||
var invoice = user.BitPay.CreateInvoice(
|
||||
new Invoice
|
||||
{
|
||||
Price = 500,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some \", description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
var cashCow = tester.ExplorerNode;
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
|
||||
var firstPayment = invoice.CryptoInfo[0].TotalDue - Money.Coins(0.001m);
|
||||
cashCow.SendToAddress(invoiceAddress, firstPayment);
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
var exportResultPaid =
|
||||
user.GetController<UIInvoiceController>().Export("csv").GetAwaiter().GetResult();
|
||||
var paidresult = Assert.IsType<ContentResult>(exportResultPaid);
|
||||
Assert.Equal("application/csv", paidresult.ContentType);
|
||||
Assert.Contains($",orderId,{invoice.Id},", paidresult.Content);
|
||||
Assert.Contains($",On-Chain,BTC,0.0991,0.0001,5000.0", paidresult.Content);
|
||||
Assert.Contains($",USD,5.00", paidresult.Content); // Seems hacky but some plateform does not render this decimal the same
|
||||
Assert.Contains("0,,\"Some \"\", description\",New (paidPartial),new,paidPartial",
|
||||
paidresult.Content);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanCreateAndDeleteApps()
|
||||
@ -1970,7 +1821,8 @@ 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.IsOwner);
|
||||
|
||||
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));
|
||||
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id));
|
||||
@ -1991,6 +1843,7 @@ namespace BTCPayServer.Tests
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess(true);
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
var btcpayClient = await user.CreateClient();
|
||||
|
||||
DateTimeOffset expiration = DateTimeOffset.UtcNow + TimeSpan.FromMinutes(21);
|
||||
|
||||
@ -2071,6 +1924,20 @@ namespace BTCPayServer.Tests
|
||||
|
||||
var zeroInvoicePM = await greenfield.GetInvoicePaymentMethods(user.StoreId, zeroInvoice.Id);
|
||||
Assert.Empty(zeroInvoicePM);
|
||||
|
||||
var invoice6 = await btcpayClient.CreateInvoice(user.StoreId,
|
||||
new CreateInvoiceRequest()
|
||||
{
|
||||
Amount = GreenfieldConstants.MaxAmount,
|
||||
Currency = "USD"
|
||||
});
|
||||
var repo = tester.PayTester.GetService<InvoiceRepository>();
|
||||
var entity = (await repo.GetInvoice(invoice6.Id));
|
||||
Assert.Equal((decimal)ulong.MaxValue, entity.Price);
|
||||
entity.GetPaymentMethods().First().Calculate();
|
||||
// Shouldn't be possible as we clamp the value, but existing invoice may have that
|
||||
entity.Price = decimal.MaxValue;
|
||||
entity.GetPaymentMethods().First().Calculate();
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
@ -2154,7 +2021,7 @@ namespace BTCPayServer.Tests
|
||||
txFee = localInvoice.BtcDue - invoice.BtcDue;
|
||||
Assert.Equal("paidPartial", localInvoice.ExceptionStatus.ToString());
|
||||
Assert.Equal(1, localInvoice.CryptoInfo[0].TxCount);
|
||||
Assert.NotEqual(localInvoice.BitcoinAddress, invoice.BitcoinAddress); //New address
|
||||
Assert.Equal(localInvoice.BitcoinAddress, invoice.BitcoinAddress); //Same address
|
||||
Assert.True(IsMapped(invoice, ctx));
|
||||
Assert.True(IsMapped(localInvoice, ctx));
|
||||
|
||||
@ -2850,7 +2717,7 @@ namespace BTCPayServer.Tests
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
await user.GrantAccessAsync();
|
||||
var controller = tester.PayTester.GetController<UIServerController>(user.UserId, user.StoreId);
|
||||
|
||||
var fileSystemStorageConfiguration = Assert.IsType<FileSystemStorageConfiguration>(Assert
|
||||
@ -2865,7 +2732,6 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(StorageProvider.FileSystem,
|
||||
shouldBeRedirectingToLocalStorageConfigPage.RouteValues["provider"]);
|
||||
|
||||
|
||||
await CanUploadRemoveFiles(controller);
|
||||
}
|
||||
|
||||
@ -2897,7 +2763,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
//create a temporary link to file
|
||||
var tmpLinkGenerate = Assert.IsType<RedirectToActionResult>(await controller.CreateTemporaryFileUrl(fileId,
|
||||
new UIServerController.CreateTemporaryFileUrlViewModel()
|
||||
new UIServerController.CreateTemporaryFileUrlViewModel
|
||||
{
|
||||
IsDownload = true,
|
||||
TimeAmount = 1,
|
||||
@ -2927,5 +2793,124 @@ namespace BTCPayServer.Tests
|
||||
Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files(new string[] { fileId })).Model);
|
||||
Assert.Null(viewFilesViewModel.DirectUrlByFiles);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Selenium", "Selenium")]
|
||||
public async Task CanCreateReports()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
tester.ActivateLightning();
|
||||
tester.DeleteStore = false;
|
||||
await tester.StartAsync();
|
||||
await tester.EnsureChannelsSetup();
|
||||
var acc = tester.NewAccount();
|
||||
await acc.GrantAccessAsync();
|
||||
await acc.MakeAdmin();
|
||||
acc.RegisterDerivationScheme("BTC", importKeysToNBX: true);
|
||||
acc.RegisterLightningNode("BTC");
|
||||
await acc.ReceiveUTXO(Money.Coins(1.0m));
|
||||
|
||||
var client = await acc.CreateClient();
|
||||
var posController = acc.GetController<UIPointOfSaleController>();
|
||||
|
||||
var app = await client.CreatePointOfSaleApp(acc.StoreId, new CreatePointOfSaleAppRequest()
|
||||
{
|
||||
AppName = "Static",
|
||||
DefaultView = Client.Models.PosViewType.Static,
|
||||
Template = new PointOfSaleSettings().Template
|
||||
});
|
||||
var resp = await posController.ViewPointOfSale(app.Id, choiceKey: "green-tea");
|
||||
var invoiceId = GetInvoiceId(resp);
|
||||
await acc.PayOnChain(invoiceId);
|
||||
|
||||
app = await client.CreatePointOfSaleApp(acc.StoreId, new CreatePointOfSaleAppRequest()
|
||||
{
|
||||
AppName = "Cart",
|
||||
DefaultView = Client.Models.PosViewType.Cart,
|
||||
Template = new PointOfSaleSettings().Template
|
||||
});
|
||||
resp = await posController.ViewPointOfSale(app.Id, posData: new JObject()
|
||||
{
|
||||
["cart"] = new JArray()
|
||||
{
|
||||
new JObject()
|
||||
{
|
||||
["id"] = "green-tea",
|
||||
["count"] = 2
|
||||
},
|
||||
new JObject()
|
||||
{
|
||||
["id"] = "black-tea",
|
||||
["count"] = 1
|
||||
},
|
||||
}
|
||||
}.ToString());
|
||||
invoiceId = GetInvoiceId(resp);
|
||||
await acc.PayOnBOLT11(invoiceId);
|
||||
|
||||
resp = await posController.ViewPointOfSale(app.Id, posData: new JObject()
|
||||
{
|
||||
["cart"] = new JArray()
|
||||
{
|
||||
new JObject()
|
||||
{
|
||||
["id"] = "green-tea",
|
||||
["count"] = 5
|
||||
}
|
||||
}
|
||||
}.ToString());
|
||||
invoiceId = GetInvoiceId(resp);
|
||||
await acc.PayOnLNUrl(invoiceId);
|
||||
|
||||
await acc.CreateLNAddress();
|
||||
await acc.PayOnLNAddress();
|
||||
|
||||
var report = await GetReport(acc, new() { ViewName = "Payments" });
|
||||
// 1 payment on LN Address
|
||||
// 1 payment on LNURL
|
||||
// 1 payment on BOLT11
|
||||
// 1 payment on chain
|
||||
Assert.Equal(4, report.Data.Count);
|
||||
var lnAddressIndex = report.GetIndex("LightningAddress");
|
||||
var paymentTypeIndex = report.GetIndex("PaymentType");
|
||||
Assert.Contains(report.Data, d => d[lnAddressIndex]?.Value<string>()?.Contains(acc.LNAddress) is true);
|
||||
var paymentTypes = report.Data
|
||||
.GroupBy(d => d[paymentTypeIndex].Value<string>())
|
||||
.ToDictionary(d => d.Key);
|
||||
Assert.Equal(3, paymentTypes["Lightning"].Count());
|
||||
Assert.Single(paymentTypes["On-Chain"]);
|
||||
|
||||
// 2 on-chain transactions: It received from the cashcow, then paid its own invoice
|
||||
report = await GetReport(acc, new() { ViewName = "Wallets" });
|
||||
var txIdIndex = report.GetIndex("TransactionId");
|
||||
var balanceIndex = report.GetIndex("BalanceChange");
|
||||
Assert.Equal(2, report.Data.Count);
|
||||
Assert.Equal(64, report.Data[0][txIdIndex].Value<string>().Length);
|
||||
Assert.Contains(report.Data, d => d[balanceIndex]["v"].Value<decimal>() == 1.0m);
|
||||
|
||||
// Items sold
|
||||
report = await GetReport(acc, new() { ViewName = "Sales" });
|
||||
var itemIndex = report.GetIndex("Product");
|
||||
var countIndex = report.GetIndex("Quantity");
|
||||
var itemsCount = report.Data.GroupBy(d => d[itemIndex].Value<string>())
|
||||
.ToDictionary(d => d.Key, r => r.Sum(d => d[countIndex].Value<int>()));
|
||||
Assert.Equal(8, itemsCount["green-tea"]);
|
||||
Assert.Equal(1, itemsCount["black-tea"]);
|
||||
}
|
||||
|
||||
private async Task<StoreReportResponse> GetReport(TestAccount acc, StoreReportRequest req)
|
||||
{
|
||||
var controller = acc.GetController<UIReportsController>();
|
||||
return (await controller.StoreReportsJson(acc.StoreId, req)).AssertType<JsonResult>()
|
||||
.Value
|
||||
.AssertType<StoreReportResponse>();
|
||||
}
|
||||
|
||||
private static string GetInvoiceId(IActionResult resp)
|
||||
{
|
||||
var redirect = resp.AssertType<RedirectToActionResult>();
|
||||
Assert.Equal("Checkout", redirect.ActionName);
|
||||
return (string)redirect.RouteValues["invoiceId"];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,6 @@ services:
|
||||
TESTS_AzureBlobStorageConnectionString: ${TESTS_AzureBlobStorageConnectionString:-none}
|
||||
TEST_MERCHANTLIGHTNINGD: "type=clightning;server=unix://etc/merchant_lightningd_datadir/lightning-rpc"
|
||||
TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=unix://etc/customer_lightningd_datadir/lightning-rpc"
|
||||
TEST_MERCHANTCHARGE: "type=charge;server=http://lightning-charged:9112/;api-token=foiewnccewuify;allowinsecure=true"
|
||||
TEST_MERCHANTLND: "http://lnd:lnd@merchant_lnd:8080/"
|
||||
TESTS_INCONTAINER: "true"
|
||||
TESTS_SSHCONNECTION: "root@sshd:22"
|
||||
@ -56,7 +55,6 @@ services:
|
||||
- postgres
|
||||
- customer_lightningd
|
||||
- merchant_lightningd
|
||||
- lightning-charged
|
||||
- customer_lnd
|
||||
- merchant_lnd
|
||||
- sshd
|
||||
@ -75,7 +73,7 @@ services:
|
||||
- "sshd_datadir:/root/.ssh"
|
||||
|
||||
devlnd:
|
||||
image: btcpayserver/bitcoin:24.0
|
||||
image: btcpayserver/bitcoin:25.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_WALLETDIR: "/data/wallets"
|
||||
@ -89,14 +87,19 @@ services:
|
||||
- postgres
|
||||
- customer_lnd
|
||||
- merchant_lnd
|
||||
|
||||
selenium:
|
||||
image: selenium/standalone-chrome:101.0
|
||||
extra_hosts:
|
||||
- "tests:172.23.0.18"
|
||||
expose:
|
||||
- "4444"
|
||||
extra_hosts:
|
||||
- "tests:172.18.0.18"
|
||||
networks:
|
||||
default:
|
||||
custom:
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.3.63
|
||||
image: nicolasdorier/nbxplorer:2.3.66
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
@ -132,7 +135,7 @@ services:
|
||||
|
||||
bitcoind:
|
||||
restart: unless-stopped
|
||||
image: btcpayserver/bitcoin:24.0
|
||||
image: btcpayserver/bitcoin:25.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_WALLETDIR: "/data/wallets"
|
||||
@ -160,7 +163,7 @@ services:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: btcpayserver/lightning:v23.02-1-dev
|
||||
image: btcpayserver/lightning:v23.08-dev
|
||||
stop_signal: SIGKILL
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@ -186,30 +189,8 @@ services:
|
||||
depends_on:
|
||||
- bitcoind
|
||||
|
||||
lightning-charged:
|
||||
image: shesek/lightning-charge:0.4.23-1-standalone
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
NETWORK: regtest
|
||||
API_TOKEN: foiewnccewuify
|
||||
BITCOIND_RPCCONNECT: bitcoind
|
||||
LN_NET_PATH: /etc/lightning
|
||||
LN_NET: /etc/lightning
|
||||
volumes:
|
||||
- "bitcoin_datadir:/etc/bitcoin"
|
||||
- "lightning_charge_datadir:/data"
|
||||
- "merchant_lightningd_datadir:/etc/lightning"
|
||||
expose:
|
||||
- "9112" # Charge
|
||||
- "9735" # Lightning
|
||||
ports:
|
||||
- "54938:9112" # Charge
|
||||
depends_on:
|
||||
- bitcoind
|
||||
- merchant_lightningd
|
||||
|
||||
merchant_lightningd:
|
||||
image: btcpayserver/lightning:v23.02-1-dev
|
||||
image: btcpayserver/lightning:v23.08-dev
|
||||
stop_signal: SIGKILL
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
@ -243,7 +224,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.16.2-beta
|
||||
image: btcpayserver/lnd:v0.17.2-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -278,7 +259,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.16.2-beta
|
||||
image: btcpayserver/lnd:v0.17.2-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -326,7 +307,7 @@ services:
|
||||
- "torrcdir:/usr/local/etc/tor"
|
||||
- "tor_servicesdir:/var/lib/tor/hidden_services"
|
||||
monerod:
|
||||
image: btcpayserver/monero:0.17.0.0-amd64
|
||||
image: btcpayserver/monero:0.18.2.2-5
|
||||
restart: unless-stopped
|
||||
container_name: xmr_monerod
|
||||
entrypoint: sleep 999999
|
||||
@ -336,7 +317,7 @@ services:
|
||||
ports:
|
||||
- "18081:18081"
|
||||
monero_wallet:
|
||||
image: btcpayserver/monero:0.17.0.0-amd64
|
||||
image: btcpayserver/monero:0.18.2.2-5
|
||||
restart: unless-stopped
|
||||
container_name: xmr_wallet_rpc
|
||||
entrypoint: monero-wallet-rpc --testnet --rpc-bind-ip=0.0.0.0 --disable-rpc-login --confirm-external-bind --rpc-bind-port=18082 --non-interactive --trusted-daemon --daemon-address=monerod:18081 --wallet-file=/wallet/wallet.keys --password-file=/wallet/password --tx-notify="/bin/sh ./scripts/notifier.sh -k -X GET https://host.docker.internal:14142/monerolikedaemoncallback/tx?cryptoCode=xmr&hash=%s"
|
||||
@ -368,7 +349,7 @@ services:
|
||||
elementsd-liquid:
|
||||
restart: always
|
||||
container_name: btcpayserver_elementsd_liquid
|
||||
image: btcpayserver/elements:0.21.0.1
|
||||
image: btcpayserver/elements:0.21.0.2-4
|
||||
environment:
|
||||
ELEMENTS_CHAIN: elementsregtest
|
||||
ELEMENTS_EXTRA_ARGS: |
|
||||
@ -383,11 +364,9 @@ services:
|
||||
whitelist=0.0.0.0/0
|
||||
rpcallowip=0.0.0.0/0
|
||||
validatepegin=0
|
||||
initialfreecoins=210000000000000
|
||||
initialfreecoins=2100000000000000
|
||||
con_dyna_deploy_signal=1
|
||||
con_dyna_deploy_start=0
|
||||
con_nminerconfirmationwindow=1
|
||||
con_nrulechangeactivationthreshold=1
|
||||
con_dyna_deploy_start=10
|
||||
expose:
|
||||
- "19332"
|
||||
- "19444"
|
||||
|
@ -22,7 +22,6 @@ services:
|
||||
TESTS_AzureBlobStorageConnectionString: ${TESTS_AzureBlobStorageConnectionString:-none}
|
||||
TEST_MERCHANTLIGHTNINGD: "type=clightning;server=unix://etc/merchant_lightningd_datadir/lightning-rpc"
|
||||
TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=unix://etc/customer_lightningd_datadir/lightning-rpc"
|
||||
TEST_MERCHANTCHARGE: "type=charge;server=http://lightning-charged:9112/;api-token=foiewnccewuify;allowinsecure=true"
|
||||
TEST_MERCHANTLND: "http://lnd:lnd@merchant_lnd:8080/"
|
||||
TESTS_INCONTAINER: "true"
|
||||
TESTS_SSHCONNECTION: "root@sshd:22"
|
||||
@ -54,7 +53,6 @@ services:
|
||||
- postgres
|
||||
- customer_lightningd
|
||||
- merchant_lightningd
|
||||
- lightning-charged
|
||||
- customer_lnd
|
||||
- merchant_lnd
|
||||
- sshd
|
||||
@ -72,28 +70,33 @@ services:
|
||||
- "sshd_datadir:/root/.ssh"
|
||||
|
||||
devlnd:
|
||||
image: btcpayserver/bitcoin:24.0
|
||||
image: btcpayserver/bitcoin:25.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_WALLETDIR: "/data/wallets"
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
deprecatedrpc=signrawtransaction
|
||||
connect=bitcoind:39388
|
||||
rpcallowip=0.0.0.0/0
|
||||
fallbackfee=0.0002
|
||||
rpcallowip=0.0.0.0/0
|
||||
depends_on:
|
||||
- nbxplorer
|
||||
- postgres
|
||||
- customer_lnd
|
||||
- merchant_lnd
|
||||
|
||||
selenium:
|
||||
image: selenium/standalone-chrome:101.0
|
||||
extra_hosts:
|
||||
- "tests:172.18.0.18"
|
||||
- "tests:172.23.0.18"
|
||||
expose:
|
||||
- "4444"
|
||||
networks:
|
||||
default:
|
||||
custom:
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.3.63
|
||||
image: nicolasdorier/nbxplorer:2.3.66
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
@ -118,7 +121,7 @@ services:
|
||||
|
||||
bitcoind:
|
||||
restart: unless-stopped
|
||||
image: btcpayserver/bitcoin:24.0
|
||||
image: btcpayserver/bitcoin:25.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_WALLETDIR: "/data/wallets"
|
||||
@ -146,7 +149,7 @@ services:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: btcpayserver/lightning:v23.02-1-dev
|
||||
image: btcpayserver/lightning:v23.08-dev
|
||||
stop_signal: SIGKILL
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@ -172,30 +175,8 @@ services:
|
||||
depends_on:
|
||||
- bitcoind
|
||||
|
||||
lightning-charged:
|
||||
image: shesek/lightning-charge:0.4.23-1-standalone
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
NETWORK: regtest
|
||||
API_TOKEN: foiewnccewuify
|
||||
BITCOIND_RPCCONNECT: bitcoind
|
||||
LN_NET_PATH: /etc/lightning
|
||||
LN_NET: /etc/lightning
|
||||
volumes:
|
||||
- "bitcoin_datadir:/etc/bitcoin"
|
||||
- "lightning_charge_datadir:/data"
|
||||
- "merchant_lightningd_datadir:/etc/lightning"
|
||||
expose:
|
||||
- "9112" # Charge
|
||||
- "9735" # Lightning
|
||||
ports:
|
||||
- "54938:9112" # Charge
|
||||
depends_on:
|
||||
- bitcoind
|
||||
- merchant_lightningd
|
||||
|
||||
merchant_lightningd:
|
||||
image: btcpayserver/lightning:v23.02-1-dev
|
||||
image: btcpayserver/lightning:v23.08-dev
|
||||
stop_signal: SIGKILL
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
@ -230,7 +211,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.16.2-beta
|
||||
image: btcpayserver/lnd:v0.17.2-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -267,7 +248,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.16.2-beta
|
||||
image: btcpayserver/lnd:v0.17.2-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
|
@ -45,21 +45,21 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<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.4.23" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.4.31" />
|
||||
<PackageReference Include="CsvHelper" Version="15.0.5" />
|
||||
<PackageReference Include="Dapper" Version="2.0.123" />
|
||||
<PackageReference Include="Fido2" Version="2.0.2" />
|
||||
<PackageReference Include="Fido2.AspNet" Version="2.0.2" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="5.0.372" />
|
||||
<PackageReference Include="LNURL" Version="0.0.29" />
|
||||
<PackageReference Include="LNURL" Version="0.0.34" />
|
||||
<PackageReference Include="MailKit" Version="3.3.0" />
|
||||
<PackageReference Include="BTCPayServer.NETCore.Plugins.Mvc" Version="1.4.4" />
|
||||
<PackageReference Include="QRCoder" Version="1.4.3" />
|
||||
<PackageReference Include="System.IO.Pipelines" Version="6.0.3" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.39" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine" Version="2.0.0" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="2.0.0" />
|
||||
<PackageReference Include="NicolasDorier.RateLimits" Version="1.2.3" />
|
||||
@ -75,12 +75,12 @@
|
||||
<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="YamlDotNet" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.9" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.9" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="Views\UIReports\StoreReports.cshtml" />
|
||||
<None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.svg" />
|
||||
<None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.woff2" />
|
||||
<None Include="wwwroot\vendor\font-awesome\less\animated.less" />
|
||||
@ -111,9 +111,6 @@
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_screen-reader.scss" />
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_stacked.scss" />
|
||||
<None Include="wwwroot\vendor\font-awesome\scss\_variables.scss" />
|
||||
<None Include="wwwroot\vendor\jquery-easing\jquery.easing.compatibility.js" />
|
||||
<None Include="wwwroot\vendor\jquery-easing\jquery.easing.js" />
|
||||
<None Include="wwwroot\vendor\jquery-easing\jquery.easing.min.js" />
|
||||
<None Include="wwwroot\vendor\jquery\jquery.js" />
|
||||
<None Include="wwwroot\vendor\jquery\jquery.min.js" />
|
||||
</ItemGroup>
|
||||
@ -122,6 +119,7 @@
|
||||
<Folder Include="wwwroot\vendor\bootstrap" />
|
||||
<Folder Include="wwwroot\vendor\clipboard.js\" />
|
||||
<Folder Include="wwwroot\vendor\highlightjs\" />
|
||||
<Folder Include="wwwroot\vendor\pivottable\" />
|
||||
<Folder Include="wwwroot\vendor\summernote" />
|
||||
<Folder Include="wwwroot\vendor\tom-select" />
|
||||
<Folder Include="wwwroot\vendor\ur-registry" />
|
||||
@ -139,6 +137,8 @@
|
||||
<ItemGroup>
|
||||
<Watch Include="Views\**\*.*"></Watch>
|
||||
<Watch Remove="Views\UIAccount\CheatPermissions.cshtml" />
|
||||
<Watch Remove="Views\UIPullPayment\ViewPullPaymentPrint.cshtml" />
|
||||
<Watch Remove="Views\UIReports\StoreReports.cshtml" />
|
||||
<Content Update="Views\UIApps\_ViewImports.cshtml">
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
|
13
BTCPayServer/Blazor/BlazorExtensions.cs
Normal file
13
BTCPayServer/Blazor/BlazorExtensions.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace BTCPayServer.Blazor
|
||||
{
|
||||
public static class BlazorExtensions
|
||||
{
|
||||
public static bool IsPreRendering(this IJSRuntime runtime)
|
||||
{
|
||||
// The peculiar thing in prerender is that Blazor circuit isn't yet created, so we can't use JSInterop
|
||||
return !(bool)runtime.GetType().GetProperty("IsInitialized").GetValue(runtime);
|
||||
}
|
||||
}
|
||||
}
|
22
BTCPayServer/Blazor/Icon.razor
Normal file
22
BTCPayServer/Blazor/Icon.razor
Normal file
@ -0,0 +1,22 @@
|
||||
@using BTCPayServer.Abstractions.Extensions;
|
||||
@using BTCPayServer.Configuration;
|
||||
@using Microsoft.AspNetCore.Hosting;
|
||||
@using Microsoft.AspNetCore.Mvc.Routing;
|
||||
@using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
@using Microsoft.AspNetCore.Mvc;
|
||||
@inject IFileVersionProvider FileVersionProvider
|
||||
@inject BTCPayServerOptions BTCPayServerOptions
|
||||
|
||||
<svg role="img" class="icon icon-@Symbol">
|
||||
<use href="@GetPathTo(Symbol)"></use>
|
||||
</svg>
|
||||
@code {
|
||||
public string GetPathTo(string symbol)
|
||||
{
|
||||
var versioned = FileVersionProvider.AddFileVersionToPath(default, "img/icon-sprite.svg");
|
||||
var rootPath = (BTCPayServerOptions.RootPath ?? "/").WithTrailingSlash();
|
||||
return $"{rootPath}{versioned}#{Symbol}";
|
||||
}
|
||||
[Parameter]
|
||||
public string Symbol { get; set; }
|
||||
}
|
156
BTCPayServer/Blazor/NotificationsDropDown.razor
Normal file
156
BTCPayServer/Blazor/NotificationsDropDown.razor
Normal file
@ -0,0 +1,156 @@
|
||||
@using BTCPayServer.Abstractions.Contracts;
|
||||
@using BTCPayServer.Configuration;
|
||||
@using BTCPayServer.Data;
|
||||
@using BTCPayServer.Services.Notifications;
|
||||
@using Microsoft.AspNetCore.Identity;
|
||||
@using Microsoft.AspNetCore.Routing;
|
||||
@implements IDisposable
|
||||
@inject AuthenticationStateProvider _AuthenticationStateProvider
|
||||
@inject NotificationManager _NotificationManager
|
||||
@inject UserManager<ApplicationUser> _UserManager
|
||||
@inject IJSRuntime _JSRuntime
|
||||
@inject LinkGenerator _LinkGenerator
|
||||
@inject BTCPayServerOptions _BTCPayServerOptions
|
||||
@inject EventAggregator _EventAggregator
|
||||
|
||||
<div id="Notifications">
|
||||
@if (UnseenCount == "0")
|
||||
{
|
||||
<a href="@NotificationsUrl" id="NotificationsHandle" class="mainMenuButton" title="Notifications">
|
||||
<Icon Symbol="notifications" />
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button id="NotificationsHandle" class="mainMenuButton" title="Notifications" type="button" data-bs-toggle="dropdown">
|
||||
<Icon Symbol="notifications" />
|
||||
<span class="badge rounded-pill bg-danger p-1 ms-1" id="NotificationsBadge">@UnseenCount</span>
|
||||
</button>
|
||||
}
|
||||
@if (UnseenCount != "0" && Last5 is not null)
|
||||
{
|
||||
<div class="dropdown-menu text-center" id="NotificationsDropdown" aria-labelledby="NotificationsHandle">
|
||||
<div class="d-flex gap-3 align-items-center justify-content-between py-3 px-4 border-bottom border-light">
|
||||
<h5 class="m-0">Notifications</h5>
|
||||
<a class="btn btn-link p-0" @onclick="MarkAllAsSeen" id="NotificationsMarkAllAsSeen">Mark all as seen</a>
|
||||
</div>
|
||||
<div id="NotificationsList" v-pre>
|
||||
@foreach (var n in Last5)
|
||||
{
|
||||
<a href="@NotificationUrl(n.Id)" class="notification d-flex align-items-center dropdown-item border-bottom border-light py-3 px-4">
|
||||
<div class="me-3">
|
||||
<Icon Symbol="@NotificationIcon(n.Identifier)" />
|
||||
</div>
|
||||
<div class="notification-item__content">
|
||||
<div class="text-start text-wrap">
|
||||
@n.Body
|
||||
</div>
|
||||
<div class="text-start d-flex">
|
||||
<small class="text-muted" data-timeago-unixms="@n.Created.ToUnixTimeMilliseconds()">@n.Created.ToTimeAgo()</small>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="p-3">
|
||||
<a href="@NotificationsUrl">View all</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
string NotificationsUrl => _LinkGenerator.GetPathByAction("Index", "UINotifications", pathBase: _BTCPayServerOptions.RootPath);
|
||||
string NotificationUrl(string notificationId) => _LinkGenerator.GetPathByAction("NotificationPassThrough", "UINotifications", values: new { id = notificationId }, pathBase: _BTCPayServerOptions.RootPath);
|
||||
string UnseenCount;
|
||||
List<NotificationViewModel> Last5;
|
||||
IDisposable _EventAggregatorListener;
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
if (_JSRuntime.IsPreRendering())
|
||||
return;
|
||||
_EventAggregatorListener = _EventAggregator.Subscribe<UserNotificationsUpdatedEvent>((s, evt) =>
|
||||
{
|
||||
_ = InvokeAsync(async () =>
|
||||
{
|
||||
if (await GetUserId() is string userId)
|
||||
{
|
||||
var res = await _NotificationManager.GetSummaryNotifications(userId, cachedOnly: false);
|
||||
UpdateState(res);
|
||||
StateHasChanged();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose() => _EventAggregatorListener?.Dispose();
|
||||
|
||||
static string SeenCount(int? count)
|
||||
{
|
||||
if (count is not int c)
|
||||
return "0";
|
||||
if (c >= NotificationManager.MaxUnseen)
|
||||
return $"{NotificationManager.MaxUnseen - 1}+";
|
||||
return c.ToString();
|
||||
}
|
||||
|
||||
void UpdateState((List<NotificationViewModel> Items, int? Count) res)
|
||||
{
|
||||
UnseenCount = SeenCount(res.Count);
|
||||
Last5 = res.Items;
|
||||
}
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
if (await GetUserId() is string userId)
|
||||
{
|
||||
// For prerendering and first rendering, always use the cached value
|
||||
var res = await _NotificationManager.GetSummaryNotifications(userId, cachedOnly: true);
|
||||
// If we forget to update the state here, the UI will flicker.
|
||||
// Because the first rendering will think there is 0 events, until the DB call ends and the second rendering happens.
|
||||
// By updating the state here, the first rendering will show the cached value until the second rendering happens
|
||||
UpdateState(res);
|
||||
// We don't want to block the pre-rendering, so we will render again when the costly request is over
|
||||
if (!_JSRuntime.IsPreRendering())
|
||||
{
|
||||
res = await _NotificationManager.GetSummaryNotifications(userId, cachedOnly: false);
|
||||
UpdateState(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async Task<string> GetUserId()
|
||||
{
|
||||
var state = await _AuthenticationStateProvider.GetAuthenticationStateAsync();
|
||||
if (!state.User.Identity.IsAuthenticated)
|
||||
return null;
|
||||
return _UserManager.GetUserId(state.User);
|
||||
}
|
||||
|
||||
private async Task MarkAllAsSeen()
|
||||
{
|
||||
if (await GetUserId() is string userId)
|
||||
{
|
||||
await _NotificationManager.ToggleSeen(new NotificationsQuery() { Seen = false, UserId = userId }, true);
|
||||
UnseenCount = "0";
|
||||
}
|
||||
}
|
||||
|
||||
private static string NotificationIcon(string type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
"invoice_expired" => "notifications-invoice-failure",
|
||||
"invoice_expiredpaidpartial" => "notifications-invoice-failure",
|
||||
"invoice_failedtoconfirm" => "notifications-invoice-failure",
|
||||
"invoice_confirmed" => "notifications-invoice-settled",
|
||||
"invoice_paidafterexpiration" => "notifications-invoice-settled",
|
||||
"external-payout-transaction" => "notifications-payout",
|
||||
"payout_awaitingapproval" => "notifications-payout",
|
||||
"payout_awaitingpayment" => "notifications-payout-approved",
|
||||
"newversion" => "notifications-new-version",
|
||||
_ => "note"
|
||||
};
|
||||
}
|
||||
}
|
9
BTCPayServer/Blazor/_Imports.razor
Normal file
9
BTCPayServer/Blazor/_Imports.razor
Normal file
@ -0,0 +1,9 @@
|
||||
@using System.Net.Http
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.JSInterop
|
||||
@using BTCPayServer.Blazor
|
||||
@using BTCPayServer.Abstractions.Extensions
|
@ -13,14 +13,20 @@ namespace BTCPayServer
|
||||
{
|
||||
return Regex.Match(color, Pattern).Success;
|
||||
}
|
||||
public string TextColor(string bgColor)
|
||||
|
||||
public Color TextColor(Color bg)
|
||||
{
|
||||
int nThreshold = 105;
|
||||
var bg = ColorTranslator.FromHtml(bgColor);
|
||||
int bgDelta = Convert.ToInt32((bg.R * 0.299) + (bg.G * 0.587) + (bg.B * 0.114));
|
||||
Color color = (255 - bgDelta < nThreshold) ? Color.Black : Color.White;
|
||||
int bgDelta = Convert.ToInt32(bg.R * 0.299 + bg.G * 0.587 + bg.B * 0.114);
|
||||
return 255 - bgDelta < nThreshold ? Color.Black : Color.White;
|
||||
}
|
||||
|
||||
public string TextColor(string bg)
|
||||
{
|
||||
var color = TextColor(FromHtml(bg));
|
||||
return ColorTranslator.ToHtml(color).ToLowerInvariant();
|
||||
}
|
||||
|
||||
// Borrowed from https://github.com/ManageIQ/guides/blob/master/labels.md
|
||||
public static readonly ColorPalette Default = new ColorPalette(new string[] {
|
||||
"#fbca04",
|
||||
@ -31,6 +37,7 @@ namespace BTCPayServer
|
||||
"#cdcdcd",
|
||||
"#cc317c",
|
||||
});
|
||||
|
||||
private ColorPalette(string[] labels)
|
||||
{
|
||||
Labels = labels;
|
||||
@ -98,5 +105,10 @@ namespace BTCPayServer
|
||||
var color = AdjustBrightness(ColorTranslator.FromHtml(html), correctionFactor);
|
||||
return ColorTranslator.ToHtml(color);
|
||||
}
|
||||
|
||||
public Color FromHtml(string html)
|
||||
{
|
||||
return ColorTranslator.FromHtml(html);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,27 +1,24 @@
|
||||
if (!window.appSales) {
|
||||
window.appSales =
|
||||
{
|
||||
dataLoaded: function (model) {
|
||||
const id = "AppSales-" + model.id;
|
||||
window.appSales = {
|
||||
dataLoaded (model) {
|
||||
const id = `AppSales-${model.id}`;
|
||||
const appId = model.id;
|
||||
const period = model.period;
|
||||
const baseUrl = model.url;
|
||||
const baseUrl = model.dataUrl;
|
||||
const data = model;
|
||||
|
||||
const render = (data, period) => {
|
||||
const series = data.series.map(s => s.salesCount);
|
||||
const labels = data.series.map((s, i) => period === model.period ? s.label : (i % 5 === 0 ? s.label : ''));
|
||||
const labels = data.series.map((s, i) => period === 'Month' ? (i % 5 === 0 ? s.label : '') : s.label);
|
||||
const min = Math.min(...series);
|
||||
const max = Math.max(...series);
|
||||
const low = min === max ? 0 : Math.max(min - ((max - min) / 5), 0);
|
||||
|
||||
document.querySelectorAll(`#${id} .sales-count`).innerText = data.salesCount;
|
||||
|
||||
new Chartist.Bar(`#${id} .ct-chart`, {
|
||||
labels,
|
||||
series: [series]
|
||||
}, {
|
||||
low,
|
||||
low
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -24,7 +24,7 @@ public class AppTopItems : ViewComponent
|
||||
public async Task<IViewComponentResult> InvokeAsync(string appId, string appType)
|
||||
{
|
||||
var type = _appService.GetAppType(appType);
|
||||
if (type is not IHasItemStatsAppType salesAppType || type is not AppBaseType appBaseType)
|
||||
if (type is not (IHasItemStatsAppType and AppBaseType appBaseType))
|
||||
return new HtmlContentViewComponentResult(new StringHtmlContent(string.Empty));
|
||||
|
||||
var vm = new AppTopItemsViewModel
|
||||
@ -40,7 +40,7 @@ public class AppTopItems : ViewComponent
|
||||
var app = HttpContext.GetAppData();
|
||||
var entries = await _appService.GetItemStats(app);
|
||||
vm.SalesCount = entries.Select(e => e.SalesCount).ToList();
|
||||
vm.Entries = entries.ToList();
|
||||
vm.Entries = entries.Take(5).ToList();
|
||||
vm.AppType = app.AppType;
|
||||
vm.AppUrl = await appBaseType.ConfigureLink(app);
|
||||
vm.Name = app.Name;
|
||||
|
@ -27,8 +27,8 @@
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
document.getElementById(`AppTopItems-${appId}`).outerHTML = await response.text();
|
||||
const data = document.querySelector(`#AppSales-${appId} template`);
|
||||
if (data) window.appSales.dataLoaded(JSON.parse(data.innerHTML));
|
||||
const data = document.querySelector(`#AppTopItems-${appId} template`);
|
||||
if (data) window.appTopItems.dataLoaded(JSON.parse(data.innerHTML));
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
@ -48,7 +48,7 @@
|
||||
<span class="app-item-point ct-point"></span>
|
||||
@entry.Title
|
||||
</span>
|
||||
<span class="app-item-value">
|
||||
<span class="app-item-value" data-sensitive>
|
||||
<span class="text-muted">@entry.SalesCount @($"{label}{(entry.SalesCount == 1 ? "" : "s")}"),</span>
|
||||
@entry.TotalFormatted
|
||||
</span>
|
||||
|
@ -1,8 +1,7 @@
|
||||
if (!window.appTopItems) {
|
||||
window.appTopItems =
|
||||
{
|
||||
dataLoaded: function (model) {
|
||||
const id = "AppTopItems-" + model.id;
|
||||
window.appTopItems = {
|
||||
dataLoaded (model) {
|
||||
const id = `AppTopItems-${model.id}`;
|
||||
const series = model.salesCount;
|
||||
new Chartist.Bar(`#${id} .ct-chart`, { series }, {
|
||||
distributeSeries: true,
|
||||
|
67
BTCPayServer/Components/InvoiceStatus/Default.cshtml
Normal file
67
BTCPayServer/Components/InvoiceStatus/Default.cshtml
Normal file
@ -0,0 +1,67 @@
|
||||
@using BTCPayServer.Services.Invoices
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@model BTCPayServer.Components.InvoiceStatus.InvoiceStatusViewModel
|
||||
@inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary
|
||||
|
||||
@{
|
||||
var state = Model.State.ToString();
|
||||
var badgeClass = Model.State.Status.ToModernStatus().ToString().ToLower();
|
||||
var canMark = !string.IsNullOrEmpty(Model.InvoiceId) && (Model.State.CanMarkComplete() || Model.State.CanMarkInvalid());
|
||||
}
|
||||
<div class="d-inline-flex align-items-center gap-2">
|
||||
@if (Model.IsArchived)
|
||||
{
|
||||
<span class="badge bg-warning">archived</span>
|
||||
}
|
||||
<div class="badge badge-@badgeClass" data-invoice-state-badge="@Model.InvoiceId">
|
||||
@if (canMark)
|
||||
{
|
||||
<span class="dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
@state
|
||||
</span>
|
||||
<div class="dropdown-menu">
|
||||
@if (Model.State.CanMarkInvalid())
|
||||
{
|
||||
<button type="button" class="dropdown-item lh-base" data-invoice-id="@Model.InvoiceId" data-new-state="invalid">
|
||||
Mark as invalid
|
||||
</button>
|
||||
}
|
||||
@if (Model.State.CanMarkComplete())
|
||||
{
|
||||
<button type="button" class="dropdown-item lh-base" data-invoice-id="@Model.InvoiceId" data-new-state="settled">
|
||||
Mark as settled
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@state
|
||||
}
|
||||
</div>
|
||||
@if (Model.Payments != null)
|
||||
{
|
||||
foreach (var paymentMethodId in Model.Payments.Select(payment => payment.GetPaymentMethodId()).Distinct())
|
||||
{
|
||||
var image = PaymentMethodHandlerDictionary[paymentMethodId]?.GetCryptoImage(paymentMethodId);
|
||||
var badge = paymentMethodId.PaymentType.GetBadge();
|
||||
if (!string.IsNullOrEmpty(image) || !string.IsNullOrEmpty(badge))
|
||||
{
|
||||
<span class="d-inline-flex align-items-center gap-1">
|
||||
@if (!string.IsNullOrEmpty(image))
|
||||
{
|
||||
<img src="@Context.Request.GetRelativePathOrAbsolute(image)" alt="@paymentMethodId.PaymentType.ToString()" style="height:1.5em" />
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(badge))
|
||||
{
|
||||
@badge
|
||||
}
|
||||
</span>
|
||||
}
|
||||
}
|
||||
}
|
||||
@if (Model.HasRefund)
|
||||
{
|
||||
<span class="badge bg-warning">Refund</span>
|
||||
}
|
||||
</div>
|
22
BTCPayServer/Components/InvoiceStatus/InvoiceStatus.cs
Normal file
22
BTCPayServer/Components/InvoiceStatus/InvoiceStatus.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Components.InvoiceStatus
|
||||
{
|
||||
public class InvoiceStatus : ViewComponent
|
||||
{
|
||||
public IViewComponentResult Invoke(InvoiceState state, List<PaymentEntity> payments, string invoiceId, bool isArchived = false, bool hasRefund = false)
|
||||
{
|
||||
var vm = new InvoiceStatusViewModel
|
||||
{
|
||||
State = state,
|
||||
Payments = payments,
|
||||
InvoiceId = invoiceId,
|
||||
IsArchived = isArchived,
|
||||
HasRefund = hasRefund
|
||||
};
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
||||
namespace BTCPayServer.Components.InvoiceStatus
|
||||
{
|
||||
public class InvoiceStatusViewModel
|
||||
{
|
||||
public InvoiceState State { get; set; }
|
||||
public List<PaymentEntity> Payments { get; set; }
|
||||
public string InvoiceId { get; set; }
|
||||
public bool IsArchived { get; set; }
|
||||
public bool HasRefund { get; set; }
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@
|
||||
@model BTCPayServer.Components.LabelManager.LabelViewModel
|
||||
@{
|
||||
var elementId = "a" + Encoders.Base58.EncodeData(RandomUtils.GetBytes(16));
|
||||
var fetchUrl = Url.Action("GetLabels", "UIWallets", new {
|
||||
var fetchUrl = Url.Action("LabelsJson", "UIWallets", new {
|
||||
walletId = Model.WalletObjectId.WalletId,
|
||||
excludeTypes = Safe.Json(Model.ExcludeTypes)
|
||||
});
|
||||
|
@ -1,7 +1,5 @@
|
||||
@using BTCPayServer.Services
|
||||
@using BTCPayServer.Abstractions.Contracts
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@inject ThemeSettings Theme
|
||||
@inject IFileService FileService
|
||||
@model BTCPayServer.Components.MainLogo.MainLogoViewModel
|
||||
|
@ -1,13 +1,14 @@
|
||||
@using BTCPayServer.Views.Server
|
||||
@using BTCPayServer.Views.Stores
|
||||
@using BTCPayServer.Views.Apps
|
||||
@using BTCPayServer.Views.Invoice
|
||||
@using BTCPayServer.Views.Manage
|
||||
@using BTCPayServer.Views.PaymentRequest
|
||||
@using BTCPayServer.Views.Wallets
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Components.ThemeSwitch
|
||||
@using BTCPayServer.Components.UIExtensionPoint
|
||||
@using BTCPayServer.Services
|
||||
@using BTCPayServer.Views.Apps
|
||||
@using BTCPayServer.Views.CustodianAccounts
|
||||
@inject Microsoft.AspNetCore.Http.IHttpContextAccessor HttpContext;
|
||||
@inject BTCPayServerEnvironment Env
|
||||
@ -91,7 +92,7 @@
|
||||
|
||||
</li>
|
||||
}
|
||||
<vc:ui-extension-point location="store-wallets-nav" model="@Model"/>
|
||||
<vc:ui-extension-point location="store-wallets-nav" model="@Model"/>
|
||||
@if (PoliciesSettings.Experimental)
|
||||
{
|
||||
@foreach (var custodianAccount in Model.CustodianAccounts)
|
||||
@ -132,6 +133,12 @@
|
||||
<span>Invoices</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" permission="@Policies.CanViewInvoices">
|
||||
<a asp-area="" asp-controller="UIReports" asp-action="StoreReports" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActivePage(StoreNavPages.Reporting)" id="SectionNav-Reporting">
|
||||
<vc:icon symbol="invoice" />
|
||||
<span>Reporting</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" permission="@Policies.CanModifyStoreSettings">
|
||||
<a asp-area="" asp-controller="UIPaymentRequest" asp-action="GetPaymentRequests" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActiveCategory(typeof(PaymentRequestsNavPages))" id="StoreNav-PaymentRequests">
|
||||
<vc:icon symbol="payment-requests"/>
|
||||
@ -178,10 +185,18 @@
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item" permission="@Policies.CanModifyServerSettings">
|
||||
<a asp-area="" asp-controller="UIServer" asp-action="ListPlugins" class="nav-link @ViewData.IsActivePage(ServerNavPages.Plugins)" id="Nav-ManagePlugins">
|
||||
<vc:icon symbol="plugin"/>
|
||||
<vc:icon symbol="manage-plugins"/>
|
||||
<span>Manage Plugins</span>
|
||||
</a>
|
||||
</li>
|
||||
@if (Model.Store != null && Model.ArchivedAppsCount > 0)
|
||||
{
|
||||
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyStoreSettings">
|
||||
<a asp-area="" asp-controller="UIApps" asp-action="ListApps" asp-route-storeId="@Model.Store.Id" asp-route-archived="true" class="nav-link @ViewData.IsActivePage(AppsNavPages.Index)" id="Nav-ArchivedApps">
|
||||
@Model.ArchivedAppsCount Archived App@(Model.ArchivedAppsCount == 1 ? "" : "s")
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@ -239,7 +254,7 @@
|
||||
<span>Account</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu py-0 w-100" aria-labelledby="Nav-Account">
|
||||
<li class="p-3">
|
||||
<li class="p-3 border-bottom">
|
||||
<strong class="d-block text-truncate" style="max-width:195px">@User.Identity.Name</strong>
|
||||
@if (User.IsInRole(Roles.ServerAdmin))
|
||||
{
|
||||
@ -248,10 +263,19 @@
|
||||
</li>
|
||||
@if (!Theme.CustomTheme)
|
||||
{
|
||||
<li class="border-top py-1 px-3">
|
||||
<vc:theme-switch css-class="nav-link"/>
|
||||
<li class="py-1 px-3">
|
||||
<vc:theme-switch css-class="nav-link pb-0"/>
|
||||
</li>
|
||||
}
|
||||
<li class="py-1 px-3">
|
||||
<label class="d-flex align-items-center justify-content-between gap-3 nav-link">
|
||||
<span class="fw-semibold">Hide Sensitive Info</span>
|
||||
<input id="HideSensitiveInfo" name="HideSensitiveInfo" type="checkbox" class="btcpay-toggle" />
|
||||
</label>
|
||||
<script>
|
||||
document.getElementById('HideSensitiveInfo').checked = window.localStorage.getItem('btcpay-hide-sensitive-info') === 'true';
|
||||
</script>
|
||||
</li>
|
||||
<li class="border-top py-1 px-3">
|
||||
<a asp-area="" asp-controller="UIManage" asp-action="Index" class="nav-link @ViewData.IsActiveCategory(typeof(ManageNavPages))" id="Nav-ManageAccount">
|
||||
<span>Manage Account</span>
|
||||
|
@ -68,14 +68,17 @@ namespace BTCPayServer.Components.MainNav
|
||||
vm.LightningNodes = lightningNodes;
|
||||
|
||||
// Apps
|
||||
var apps = await _appService.GetAllApps(UserId, false, store.Id);
|
||||
vm.Apps = apps.Select(a => new StoreApp
|
||||
{
|
||||
Id = a.Id,
|
||||
IsOwner = a.IsOwner,
|
||||
AppName = a.AppName,
|
||||
AppType = a.AppType
|
||||
}).ToList();
|
||||
var apps = await _appService.GetAllApps(UserId, false, store.Id, true);
|
||||
vm.Apps = apps
|
||||
.Where(a => !a.Archived)
|
||||
.Select(a => new StoreApp
|
||||
{
|
||||
Id = a.Id,
|
||||
AppName = a.AppName,
|
||||
AppType = a.AppType
|
||||
}).ToList();
|
||||
|
||||
vm.ArchivedAppsCount = apps.Count(a => a.Archived);
|
||||
|
||||
if (PoliciesSettings.Experimental)
|
||||
{
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user