Compare commits
402 Commits
v1.0.3.63
...
v1.0.3.102
Author | SHA1 | Date | |
---|---|---|---|
14647d5778 | |||
560dde3396 | |||
7f9c2439c4 | |||
6de5d0bce8 | |||
c705a11aa7 | |||
45a196b407 | |||
07cb6adb69 | |||
5358f81ce0 | |||
5b7988be79 | |||
e6c794d68f | |||
de73fedd1b | |||
2719849a54 | |||
3011fecf0f | |||
6da0a9a201 | |||
572fe3eacb | |||
ff82f15246 | |||
b214e3f6df | |||
cb9130fdf9 | |||
925dc869a2 | |||
5f1aa619cd | |||
541c748ecb | |||
e853bddbc8 | |||
79d26b5d95 | |||
840f52a75b | |||
f955302c74 | |||
95e7d3dfc4 | |||
75f2749b19 | |||
01e5b319d1 | |||
e504163bc7 | |||
aba3f7d6bd | |||
8d74023d30 | |||
602625fc17 | |||
bbeb2d5009 | |||
51faa39636 | |||
f37bfbf9f9 | |||
ba9928831e | |||
2b6bd3d751 | |||
e96ca21c89 | |||
6ee10fe98b | |||
a567c19759 | |||
bb3a087d39 | |||
5a92fe736f | |||
88390402a4 | |||
538eb66672 | |||
0b6dfe0fd3 | |||
d5579ef2b5 | |||
836c3a5b3a | |||
f2da64adad | |||
e5704abfb3 | |||
3bf4eea1fe | |||
aa23222339 | |||
68c1670c70 | |||
914eaaaa51 | |||
5831ba2143 | |||
c167a24f09 | |||
a539d27c62 | |||
d7fc079376 | |||
3a05f7e294 | |||
03713f9bd8 | |||
2a145f4350 | |||
d049da696c | |||
5a46d0e80d | |||
926250a967 | |||
139b588795 | |||
909f18f9c7 | |||
f598495198 | |||
95d746504d | |||
9a2e1d43ea | |||
be844978c1 | |||
60a361f963 | |||
93f50451e6 | |||
0936812df0 | |||
50351f56f8 | |||
232ceed8b0 | |||
b6c37a73b1 | |||
5967666df6 | |||
bf035333cf | |||
f93d1173e2 | |||
08bf4faeee | |||
e2b2cf0175 | |||
d32a24004e | |||
1f04e4e6be | |||
778dcf97b1 | |||
957fbdb907 | |||
e169b851ee | |||
7fadb4c5ad | |||
a20db7f341 | |||
b5f4739ae5 | |||
4bc03fbf06 | |||
1d3ff143d2 | |||
6918b8a291 | |||
3cd37682d3 | |||
19a990b095 | |||
87a4f02f18 | |||
8a99fc0505 | |||
bac99deb6c | |||
e65850b1eb | |||
a6e52ed3df | |||
4a9eadf71a | |||
b8f6cf4f23 | |||
e8abc1137b | |||
8507688c50 | |||
5718096224 | |||
d76e61e6f4 | |||
232817c00d | |||
86af585df3 | |||
9e770ea484 | |||
9670f11554 | |||
dc369d52cb | |||
33c755fc54 | |||
c5adc0eb71 | |||
fcb1de8a86 | |||
6df83ad148 | |||
857a436677 | |||
c6091750b0 | |||
64e7324285 | |||
d5bd0ee781 | |||
3b91b38014 | |||
165d4e2732 | |||
098dfacce8 | |||
44d1419af9 | |||
d0d077642d | |||
dc04839fab | |||
4ce0cb4b35 | |||
5100c36c06 | |||
b184360eb7 | |||
02d79de17c | |||
cf27c66132 | |||
53d9ed5adb | |||
3fe5051098 | |||
f7c8a989b6 | |||
65dcfd3549 | |||
6976fc54ca | |||
0e077ff5c4 | |||
c2f171a729 | |||
fea38758e4 | |||
444733565b | |||
96d28f00cc | |||
70cc79a77f | |||
8d10186fdf | |||
6f7e0205f8 | |||
7ef11817c1 | |||
c387c84861 | |||
ae7ad9f667 | |||
c55f1185e6 | |||
1619666bef | |||
bf784f6fd7 | |||
13e330fa65 | |||
827b133534 | |||
4067d4b00f | |||
359d8c5c6a | |||
265b7364e8 | |||
dc2b8c9e4c | |||
37869fd049 | |||
d7ada4d493 | |||
f093f85dbf | |||
1cf17872ab | |||
c79751829b | |||
7a21c03896 | |||
54f07139db | |||
d78990fbd5 | |||
9ed7dbc838 | |||
9b12c7bc57 | |||
8973c75bbc | |||
f425df7b6d | |||
a44b600c5e | |||
7f0a42c2d5 | |||
60cd864226 | |||
71cf02915e | |||
327d2298fb | |||
2ca11ed692 | |||
0224815a60 | |||
df824c36d2 | |||
66e7777b1a | |||
7ff85a86bf | |||
7b3700c2c6 | |||
04679aefd6 | |||
5190639b77 | |||
0bf73abb39 | |||
7e0211924d | |||
a8a857a7ce | |||
1d18965a26 | |||
e020b86a3f | |||
1b80b90609 | |||
efc3512994 | |||
acb8ca982f | |||
adc42cbba4 | |||
7edcb7ef5f | |||
656017c6df | |||
35db6d4a8b | |||
2741187546 | |||
c3a7ab647c | |||
92da0ec2d2 | |||
c767a49f2d | |||
ea8196b532 | |||
58f138e854 | |||
b4b6939498 | |||
bc97c07670 | |||
cf27fe5a53 | |||
449066449b | |||
eb5e32a07f | |||
708cdbe23f | |||
333de52c33 | |||
6b9932fa14 | |||
6c45689e6a | |||
1e3307c84c | |||
d0eed9857d | |||
4221763f48 | |||
184c797b0e | |||
4853e15d8a | |||
6b4b903669 | |||
05da63f2a5 | |||
5b4b073fc8 | |||
4723a83dbb | |||
78350db62d | |||
e8b71f36b2 | |||
6db9061dd1 | |||
320826a4b9 | |||
ad0edb5f4c | |||
2856c10bc3 | |||
24aa18e9ed | |||
767eca97cb | |||
73d5415ea9 | |||
e5a26cfca8 | |||
e40cd1fc0c | |||
978b7d930e | |||
0f2e3ef957 | |||
275b590e80 | |||
5d9da82d8e | |||
1a122726b7 | |||
0bd02a9272 | |||
3cce7b8b35 | |||
e3ab1f5228 | |||
4c875d9c7c | |||
e79334a6f6 | |||
a09c6d51e6 | |||
312c7b7193 | |||
ee733fee28 | |||
4d7e9d3f8a | |||
873c0a183a | |||
ea53ae8f20 | |||
686bc3380d | |||
67da20bcea | |||
561644f75b | |||
1abc89858f | |||
91c63a8ee6 | |||
563882d30b | |||
9a5eeee794 | |||
0578a692db | |||
f74f06338a | |||
1281f348bf | |||
5e76d4bfc1 | |||
2a302ea346 | |||
be90172840 | |||
b85ee895f5 | |||
93de408e07 | |||
d3662ae734 | |||
132d7795ea | |||
bf5a624209 | |||
abbdbda03a | |||
8a8593437a | |||
e203cada54 | |||
00c11c7ee9 | |||
82126b85d2 | |||
4f428c8ed1 | |||
9868af4db8 | |||
0a5d7c5efa | |||
c2754b324d | |||
5c618233cb | |||
9e91259b9e | |||
014d08f38a | |||
7998ea142b | |||
a4051dac72 | |||
e3a8892d24 | |||
ea02d77e69 | |||
4f582a6712 | |||
4769b1d452 | |||
17b18d820f | |||
26f34e75c2 | |||
6f50ac50ec | |||
5261cfcdd3 | |||
675920697f | |||
24699bf2ba | |||
5ab92ed794 | |||
8e83f0faa1 | |||
30d5add2ea | |||
6e47babf45 | |||
9b95fa1f20 | |||
c67aa14a87 | |||
23f296ef34 | |||
c6ce676ad3 | |||
fafb02b0dc | |||
53cecc8c8a | |||
db0e9ee8f8 | |||
2fac794d96 | |||
ffcd716906 | |||
85f50724db | |||
808a995741 | |||
4deb853914 | |||
470ec3354e | |||
053c2da9f1 | |||
c0e28ce66e | |||
08dd94e267 | |||
7497865d1f | |||
1888e4fe2b | |||
6746a5cbd5 | |||
baecb7bb0c | |||
28bf4b42bb | |||
c73dc425ad | |||
2138b7dcb8 | |||
8b6c4a9383 | |||
344755cbd0 | |||
63a975267c | |||
3c7d93e88d | |||
75974037bc | |||
e8a346182b | |||
e96d34f741 | |||
7dad814f19 | |||
7e67ca1413 | |||
603263549b | |||
a82b971ce7 | |||
b58c8ef2f0 | |||
274533bfdf | |||
cd6ce401e1 | |||
0c0809101d | |||
4b342376a8 | |||
fd963b9ad0 | |||
06406c0695 | |||
2b567de5c1 | |||
ef46d03760 | |||
b174f299fa | |||
09837966b9 | |||
465dce1d02 | |||
067dbad546 | |||
522970fdb9 | |||
e67aa499a6 | |||
3b68d81507 | |||
051248f2fc | |||
9a239f99f4 | |||
86c431d66e | |||
b35fe0e8e3 | |||
54905f5ceb | |||
1c9b05d992 | |||
a89c71df38 | |||
3db1f2af12 | |||
5399ff2751 | |||
bcea6027e9 | |||
a9722df7e4 | |||
474be6f7be | |||
ada9a7264b | |||
e991b302d0 | |||
eef301c6ec | |||
3fdfd0adfd | |||
358f1ffc43 | |||
349c3409df | |||
0263a2950c | |||
0364a57cae | |||
e232dd7d7e | |||
fee936b569 | |||
420115c54d | |||
312e961098 | |||
223213857f | |||
c0da81557b | |||
81945c0737 | |||
9664e3d6a1 | |||
0a8bd38e76 | |||
013054fb82 | |||
d898f716d1 | |||
1d3f144d21 | |||
de29d87487 | |||
ebef085a9c | |||
2c1f159d72 | |||
1ef59e05a5 | |||
5e09992637 | |||
30448233b1 | |||
a1601a17aa | |||
7522f7d0f7 | |||
d04b9c4c09 | |||
1a24ff9a49 | |||
13d72de82d | |||
3728fdab3f | |||
2317e3d50c | |||
5f15976c02 | |||
7f592639c5 | |||
a98402af12 | |||
316ffa91d1 | |||
c24953b57e | |||
7a1b1b7e5e | |||
70f71f64c4 | |||
5bccd07d7d | |||
d818baa6d1 | |||
249b8abf03 | |||
c134277514 | |||
f5d366cf7f | |||
ad25a2ed08 | |||
1e7a2ffe97 | |||
dd52075ff1 | |||
0253e42bd5 | |||
d99774f8d9 | |||
d563a2ec89 | |||
b4b4523193 | |||
fbcb69f447 |
.circleci
.github/ISSUE_TEMPLATE
BTCPayServer.Tests
BTCPayServer.Tests.csprojBTCPayServerTester.csChangellyTests.csCrowdfundTests.csDockerfileExtensions.csMockDelay.cs
Mocks
PSBTTests.csPaymentRequestTests.csREADME.mdServerTester.csStorageTests.csTestAccount.csTestUtils.csUnitTest1.csdocker-bitcoin-generate.ps1docker-compose.ymldocker-entrypoint.shBTCPayServer
Authentication
BTCPayNetwork.csBTCPayNetworkProvider.Bitcoin.csBTCPayNetworkProvider.Dash.csBTCPayNetworkProvider.Dogecoin.csBTCPayNetworkProvider.Groestlcoin.csBTCPayNetworkProvider.Litecoin.csBTCPayNetworkProvider.csBTCPayServer.csprojConfiguration
BTCPayServerOptions.csConfigurationExtensions.csDefaultConfiguration.cs
External
ExternalConnectionString.csExternalService.csSparkConnectionString.csControllers
AccessTokenController.csAccountController.csAppsController.Crowdfund.csAppsController.PointOfSale.csAppsController.csAppsPublicController.csChangellyController.csHomeController.csInvoiceController.API.csInvoiceController.UI.csInvoiceController.csMacaroons.csManageController.2FA.csManageController.U2F.csManageController.csPaymentRequestController.csPublicController.csPublicLightningNodeInfoController.csRateController.csServerController.Storage.csServerController.csStorageController.csStoresController.BTCLike.csStoresController.Changelly.csStoresController.CoinSwitch.csStoresController.LightningLike.csStoresController.csWalletsController.PSBT.csWalletsController.cs
Data
DerivationSchemeParser.csDerivationSchemeSettings.csDerivationStrategy.csExplorerClientProvider.csExtensions.csHostedServices
BackgroundJobSchedulerHostedService.csBaseAsyncService.csCssThemeManager.csEventHostedServiceBase.csInvoiceNotificationManager.csInvoiceWatcher.csMigratorHostedService.csNBXplorerWaiter.csRatesHostedService.csTorServicesHostedService.cs
Hosting
JsonConverters
Logging
Migrations
20190121133309_AddPaymentRequests.Designer.cs20190121133309_AddPaymentRequests.cs20190324141717_AddFiles.Designer.cs20190324141717_AddFiles.cs20190425081749_AddU2fDevices.Designer.cs20190425081749_AddU2fDevices.csApplicationDbContextModelSnapshot.cs
ModelBinders
Models
AccountViewModels
AppViewModels
UpdateCrowdfundViewModel.csUpdatePointOfSaleViewModel.csViewCrowdfundViewModel.csViewPointOfSaleViewModel.cs
ApplicationUser.csCreateInvoiceRequest.csGetTokensResponse.csInvoiceResponse.csInvoicingModels
PaymentRequestViewModels
ServerViewModels
StoreViewModels
CheckoutExperienceViewModel.csDerivationSchemeViewModel.csPairingModel.csPayButtonViewModel.csRatesViewModel.csTokensViewModel.csUpdateCoinSwitchSettingsViewModel.cs
TokenRequest.csWalletViewModels
PaymentRequest
Payments
Program.csProperties
SearchString.csSecurity
Services
Apps
BTCPayServerEnvironment.csHardwareWalletService.csIBackgroundJobClient.csInvoices
LedgerHardwareWalletService.csLightningClientFactoryService.csMails
MigrationSettings.csPaymentRequests
PoliciesSettings.csRates
BackgroundFetcherRateProvider.csBitbankRateProvider.csBitpayRateProvider.csByllsRateProvider.csCachedRateProvider.csCoinAverageRateProvider.csCoinAverageSettings.csCurrencyNameTable.csExchangeSharpRateProvider.csFallbackRateProvider.csIRateProvider.csKrakenExchangeRateProvider.csNullRateProvider.csQuadrigacxRateProvider.csRateFetcher.csRateProviderFactory.cs
SocketFactory.csTorServices.csTorrc.csStorage
Models
Services
FileService.csStoredFileRepository.cs
StorageExtensions.csProviders
AmazonS3Storage
AzureBlobStorage
AzureBlobStorageFileProviderService.cs
BaseTwentyTwentyStorageFileProviderServiceBase.csConfiguration
FileSystemStorage
GoogleCloudStorage
IStorageProviderService.csModels
ViewModels
U2F
Models
AddU2FDeviceViewModel.csLoginWithU2FViewModel.csServerChallenge.csServerRegisterResponse.csU2FAuthenticationViewModel.csU2FDevice.csU2FDeviceAuthenticationRequest.cs
U2FService.csViews
Account
Apps
AppsPublic
Home
Invoice
Checkout-Body.cshtmlCheckout.cshtmlCheckoutNoScript.cshtmlCreateInvoice.cshtmlInvoice.cshtmlInvoicePaymentsPartial.cshtmlListInvoices.cshtml
Manage
PaymentRequest
EditPaymentRequest.cshtmlGetPaymentRequests.cshtmlMinimalPaymentRequest.cshtmlViewPaymentRequest.cshtml
PublicLightningNodeInfo
Server
CreateTemporaryFileUrl.cshtmlEditAmazonS3StorageProvider.cshtmlEditAzureBlobStorageStorageProvider.cshtmlEditFilesystemStorageProvider.cshtmlEditGoogleCloudStorageStorageProvider.cshtmlEmails.cshtmlFiles.cshtmlLightningWalletServices.cshtmlLndServices.cshtmlMaintenance.cshtmlP2PService.cshtmlPolicies.cshtmlServerNavPages.csServices.cshtmlStorage.cshtml_Nav.cshtml
Shared
EmailsBody.cshtmlNotificationEmailWarning.cshtmlSyncModal.cshtml_Layout.cshtml_ValidationScriptsPartial.cshtml
Stores
AddDerivationScheme.cshtmlCheckoutExperience.cshtmlCreateToken.cshtmlEmails.cshtmlListTokens.cshtmlPayButton.cshtmlRates.cshtmlRequestPairing.cshtmlShowToken.cshtmlUpdateCoinSwitchSettings.cshtml
Wallets
wwwroot
cart/js
checkout
crowdfund-admin
crowdfund
img
js
locales
main
modal
paybutton
payment-request-admin
payment-request
vendor
@ -16,7 +16,11 @@ jobs:
|
||||
cd BTCPayServer.Tests
|
||||
docker-compose down --v
|
||||
docker-compose build
|
||||
docker-compose run tests
|
||||
TESTS_RUN_EXTERNAL_INTEGRATION="true"
|
||||
if [ -n "$CIRCLE_PULL_REQUEST" ] || [ -n "$CIRCLE_PR_NUMBER" ]; then
|
||||
TESTS_RUN_EXTERNAL_INTEGRATION="false"
|
||||
fi
|
||||
docker-compose run -e TESTS_RUN_EXTERNAL_INTEGRATION=$TESTS_RUN_EXTERNAL_INTEGRATION tests
|
||||
|
||||
# publish jobs require $DOCKERHUB_REPO, $DOCKERHUB_USER, $DOCKERHUB_PASS defined
|
||||
publish_docker_linuxamd64:
|
||||
|
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
---
|
||||
name: Report a problem
|
||||
about: File a technical problem or report a bug
|
||||
---
|
||||
|
||||
**Describe the problem/bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Your environment**
|
||||
* Version of BTCPay Server:
|
||||
* Deployment method:
|
||||
* Other relevant environment details:
|
||||
|
||||
**Logs (if applicable)**
|
||||
Basic logs can be found in Server Settings > Logs.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Actual behavior**
|
||||
Tell us what happens instead
|
||||
|
||||
**Screenshots/Links**
|
||||
If applicable, add screenshots or links to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Ideas and feature requests
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Provide examples**
|
||||
If applicable provide examples, wireframes, sketches or images to better explain your idea.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
@ -10,7 +10,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.0.1" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
@ -33,4 +33,5 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BTCPayServer\BTCPayServer.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -35,6 +35,7 @@ using System.Text;
|
||||
using System.Threading;
|
||||
using Xunit;
|
||||
using BTCPayServer.Services;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -109,7 +110,6 @@ namespace BTCPayServer.Tests
|
||||
|
||||
config.AppendLine($"ltc.explorer.url={LTCNBXplorerUri.AbsoluteUri}");
|
||||
config.AppendLine($"ltc.explorer.cookiefile=0");
|
||||
|
||||
config.AppendLine($"btc.lightning={IntegratedLightning.AbsoluteUri}");
|
||||
|
||||
if (TestDatabase == TestDatabases.MySQL && !String.IsNullOrEmpty(MySQL))
|
||||
@ -120,15 +120,18 @@ namespace BTCPayServer.Tests
|
||||
File.WriteAllText(confPath, config.ToString());
|
||||
|
||||
ServerUri = new Uri("http://" + HostName + ":" + Port + "/");
|
||||
|
||||
HttpClient = new HttpClient();
|
||||
HttpClient.BaseAddress = ServerUri;
|
||||
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development");
|
||||
var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory, "--conf", confPath, "--disable-registration", "false" });
|
||||
_Host = new WebHostBuilder()
|
||||
.UseConfiguration(conf)
|
||||
.UseContentRoot(FindBTCPayServerDirectory())
|
||||
.ConfigureServices(s =>
|
||||
{
|
||||
s.AddLogging(l =>
|
||||
{
|
||||
l.AddFilter("System.Net.Http.HttpClient", LogLevel.Critical);
|
||||
l.SetMinimumLevel(LogLevel.Information)
|
||||
.AddFilter("Microsoft", LogLevel.Error)
|
||||
.AddFilter("Hangfire", LogLevel.Error)
|
||||
@ -141,6 +144,7 @@ namespace BTCPayServer.Tests
|
||||
_Host.Start();
|
||||
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
|
||||
StoreRepository = (StoreRepository)_Host.Services.GetService(typeof(StoreRepository));
|
||||
Networks = (BTCPayNetworkProvider)_Host.Services.GetService(typeof(BTCPayNetworkProvider));
|
||||
var dashBoard = (NBXplorerDashboard)_Host.Services.GetService(typeof(NBXplorerDashboard));
|
||||
while(!dashBoard.IsFullySynched())
|
||||
{
|
||||
@ -208,6 +212,14 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
private string FindBTCPayServerDirectory()
|
||||
{
|
||||
var solutionDirectory = LanguageService.TryGetSolutionDirectoryInfo(Directory.GetCurrentDirectory());
|
||||
return Path.Combine(solutionDirectory.FullName, "BTCPayServer");
|
||||
}
|
||||
|
||||
public HttpClient HttpClient { get; set; }
|
||||
|
||||
public string HostName
|
||||
{
|
||||
get;
|
||||
@ -215,6 +227,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
public InvoiceRepository InvoiceRepository { get; private set; }
|
||||
public StoreRepository StoreRepository { get; private set; }
|
||||
public BTCPayNetworkProvider Networks { get; private set; }
|
||||
public Uri IntegratedLightning { get; internal set; }
|
||||
public bool InContainer { get; internal set; }
|
||||
|
||||
|
@ -218,7 +218,7 @@ namespace BTCPayServer.Tests
|
||||
tester.NetworkProvider, fetcher);
|
||||
changellyController.IsTest = true;
|
||||
Assert.IsType<decimal>(Assert
|
||||
.IsType<OkObjectResult>(await changellyController.CalculateAmount(user.StoreId, "ltc", "btc", 1.0m))
|
||||
.IsType<OkObjectResult>(await changellyController.CalculateAmount(user.StoreId, "ltc", "btc", 1.0m, default))
|
||||
.Value);
|
||||
}
|
||||
}
|
||||
|
@ -109,7 +109,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(0.01)
|
||||
}));
|
||||
}, default));
|
||||
|
||||
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(appId, string.Empty));
|
||||
|
||||
@ -118,7 +118,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
RedirectToCheckout = false,
|
||||
Amount = new decimal(0.01)
|
||||
}));
|
||||
}, default));
|
||||
Assert.IsType<ViewResult>(await publicApps.ViewCrowdfund(appId, string.Empty));
|
||||
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(appId, string.Empty));
|
||||
|
||||
@ -130,7 +130,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(0.01)
|
||||
}));
|
||||
}, default));
|
||||
|
||||
//Scenario 4: Enabled But End Date < Now - Not Allowed
|
||||
|
||||
@ -142,7 +142,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(0.01)
|
||||
}));
|
||||
}, default));
|
||||
|
||||
|
||||
//Scenario 5: Enabled and within correct timeframe, however target is enforced and Amount is Over - Not Allowed
|
||||
@ -156,13 +156,13 @@ namespace BTCPayServer.Tests
|
||||
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(1.01)
|
||||
}));
|
||||
}, default));
|
||||
|
||||
//Scenario 6: Allowed
|
||||
Assert.IsType<OkObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(0.05)
|
||||
}));
|
||||
}, default));
|
||||
|
||||
}
|
||||
}
|
||||
@ -177,6 +177,7 @@ namespace BTCPayServer.Tests
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
user.ModifyStore(s => s.NetworkFeeMode = NetworkFeeMode.Never);
|
||||
var apps = user.GetController<AppsController>();
|
||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
|
||||
vm.Name = "test";
|
||||
|
@ -1,12 +1,9 @@
|
||||
FROM microsoft/dotnet:2.1.500-sdk-alpine3.7 AS builder
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:2.1.505-alpine3.7 AS builder
|
||||
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT false
|
||||
RUN apk add --no-cache icu-libs
|
||||
ENV LC_ALL en_US.UTF-8
|
||||
ENV LANG en_US.UTF-8
|
||||
|
||||
# This should be removed soon https://github.com/dotnet/corefx/issues/30003
|
||||
RUN apk add --no-cache curl
|
||||
|
||||
WORKDIR /source
|
||||
COPY BTCPayServer/BTCPayServer.csproj BTCPayServer/BTCPayServer.csproj
|
||||
COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj
|
||||
|
26
BTCPayServer.Tests/Extensions.cs
Normal file
26
BTCPayServer.Tests/Extensions.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Xunit;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static T AssertViewModel<T>(this IActionResult result)
|
||||
{
|
||||
Assert.NotNull(result);
|
||||
var vr = Assert.IsType<ViewResult>(result);
|
||||
return Assert.IsType<T>(vr.Model);
|
||||
}
|
||||
public static async Task<T> AssertViewModelAsync<T>(this Task<IActionResult> task)
|
||||
{
|
||||
var result = await task;
|
||||
Assert.NotNull(result);
|
||||
var vr = Assert.IsType<ViewResult>(result);
|
||||
return Assert.IsType<T>(vr.Model);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
@ -35,27 +36,29 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
public void Advance(TimeSpan time)
|
||||
public async Task Advance(TimeSpan time)
|
||||
{
|
||||
_Now += time;
|
||||
List<WaitObj> overdue = new List<WaitObj>();
|
||||
lock (waits)
|
||||
{
|
||||
foreach (var wait in waits.ToArray())
|
||||
{
|
||||
if (_Now >= wait.Expiration)
|
||||
{
|
||||
wait.CTS.TrySetResult(true);
|
||||
overdue.Add(wait);
|
||||
waits.Remove(wait);
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (var o in overdue)
|
||||
o.CTS.TrySetResult(true);
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(overdue.Select(o => o.CTS.Task).ToArray());
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
public void AdvanceMilliseconds(long milli)
|
||||
{
|
||||
Advance(TimeSpan.FromMilliseconds(milli));
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return _Now.Millisecond.ToString(CultureInfo.InvariantCulture);
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Rates;
|
||||
@ -10,7 +11,7 @@ namespace BTCPayServer.Tests.Mocks
|
||||
public class MockRateProvider : IRateProvider
|
||||
{
|
||||
public ExchangeRates ExchangeRates { get; set; } = new ExchangeRates();
|
||||
public Task<ExchangeRates> GetRatesAsync()
|
||||
public Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(ExchangeRates);
|
||||
}
|
||||
|
114
BTCPayServer.Tests/PSBTTests.cs
Normal file
114
BTCPayServer.Tests/PSBTTests.cs
Normal file
@ -0,0 +1,114 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Models.WalletViewModels;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class PSBTTests
|
||||
{
|
||||
public PSBTTests(ITestOutputHelper helper)
|
||||
{
|
||||
Logs.Tester = new XUnitLog(helper) { Name = "Tests" };
|
||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
}
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanPlayWithPSBT()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 10,
|
||||
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);
|
||||
cashCow.SendToAddress(invoiceAddress, Money.Coins(1.5m));
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.Equal("paid", invoice.Status);
|
||||
});
|
||||
|
||||
var walletController = tester.PayTester.GetController<WalletsController>(user.UserId);
|
||||
var walletId = new WalletId(user.StoreId, "BTC");
|
||||
var sendModel = new WalletSendModel()
|
||||
{
|
||||
Destination = new Key().PubKey.Hash.GetAddress(user.SupportedNetwork.NBitcoinNetwork).ToString(),
|
||||
Amount = 0.1m,
|
||||
FeeSatoshiPerByte = 1,
|
||||
CurrentBalance = 1.5m
|
||||
};
|
||||
var vmLedger = await walletController.WalletSend(walletId, sendModel, command: "ledger").AssertViewModelAsync<WalletSendLedgerModel>();
|
||||
PSBT.Parse(vmLedger.PSBT, user.SupportedNetwork.NBitcoinNetwork);
|
||||
BitcoinAddress.Create(vmLedger.HintChange, user.SupportedNetwork.NBitcoinNetwork);
|
||||
Assert.NotNull(vmLedger.SuccessPath);
|
||||
Assert.NotNull(vmLedger.WebsocketPath);
|
||||
|
||||
var vmPSBT = await walletController.WalletSend(walletId, sendModel, command: "analyze-psbt").AssertViewModelAsync<WalletPSBTViewModel>();
|
||||
var unsignedPSBT = PSBT.Parse(vmPSBT.PSBT, user.SupportedNetwork.NBitcoinNetwork);
|
||||
Assert.NotNull(vmPSBT.Decoded);
|
||||
|
||||
var filePSBT = (FileContentResult)(await walletController.WalletSend(walletId, sendModel, command: "save-psbt"));
|
||||
PSBT.Load(filePSBT.FileContents, user.SupportedNetwork.NBitcoinNetwork);
|
||||
|
||||
await walletController.WalletPSBT(walletId, vmPSBT, "ledger").AssertViewModelAsync<WalletSendLedgerModel>();
|
||||
var vmPSBT2 = await walletController.WalletPSBT(walletId, vmPSBT, "broadcast").AssertViewModelAsync<WalletPSBTViewModel>();
|
||||
Assert.NotEmpty(vmPSBT2.Errors);
|
||||
Assert.Equal(vmPSBT.Decoded, vmPSBT2.Decoded);
|
||||
Assert.Equal(vmPSBT.PSBT, vmPSBT2.PSBT);
|
||||
|
||||
var signedPSBT = unsignedPSBT.Clone();
|
||||
signedPSBT.SignAll(user.ExtKey);
|
||||
vmPSBT.PSBT = signedPSBT.ToBase64();
|
||||
var redirect = Assert.IsType<RedirectToActionResult>(await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"));
|
||||
Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName);
|
||||
|
||||
vmPSBT.PSBT = unsignedPSBT.ToBase64();
|
||||
var combineVM = await walletController.WalletPSBT(walletId, vmPSBT, "combine").AssertViewModelAsync<WalletPSBTCombineViewModel>();
|
||||
Assert.Equal(vmPSBT.PSBT, combineVM.OtherPSBT);
|
||||
combineVM.PSBT = signedPSBT.ToBase64();
|
||||
vmPSBT = await walletController.WalletPSBTCombine(walletId, combineVM).AssertViewModelAsync<WalletPSBTViewModel>();
|
||||
|
||||
var signedPSBT2 = PSBT.Parse(vmPSBT.PSBT, user.SupportedNetwork.NBitcoinNetwork);
|
||||
Assert.True(signedPSBT.TryFinalize(out _));
|
||||
Assert.True(signedPSBT2.TryFinalize(out _));
|
||||
Assert.Equal(signedPSBT, signedPSBT2);
|
||||
|
||||
// Can use uploaded file?
|
||||
combineVM.PSBT = null;
|
||||
combineVM.UploadedPSBTFile = TestUtils.GetFormFile("signedPSBT", signedPSBT.ToBytes());
|
||||
vmPSBT = await walletController.WalletPSBTCombine(walletId, combineVM).AssertViewModelAsync<WalletPSBTViewModel>();
|
||||
signedPSBT2 = PSBT.Parse(vmPSBT.PSBT, user.SupportedNetwork.NBitcoinNetwork);
|
||||
Assert.True(signedPSBT.TryFinalize(out _));
|
||||
Assert.True(signedPSBT2.TryFinalize(out _));
|
||||
Assert.Equal(signedPSBT, signedPSBT2);
|
||||
|
||||
var ready = walletController.WalletPSBTReady(walletId, signedPSBT.ToBase64()).AssertViewModel<WalletPSBTReadyViewModel>();
|
||||
Assert.Equal(signedPSBT.ToBase64(), ready.PSBT);
|
||||
vmPSBT = await walletController.WalletPSBTReady(walletId, ready, command: "analyze-psbt").AssertViewModelAsync<WalletPSBTViewModel>();
|
||||
Assert.Equal(signedPSBT.ToBase64(), vmPSBT.PSBT);
|
||||
redirect = Assert.IsType<RedirectToActionResult>(await walletController.WalletPSBTReady(walletId, ready, command: "broadcast"));
|
||||
Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
216
BTCPayServer.Tests/PaymentRequestTests.cs
Normal file
216
BTCPayServer.Tests/PaymentRequestTests.cs
Normal file
@ -0,0 +1,216 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Models.PaymentRequestViewModels;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Payments.Changelly.Models;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class PaymentRequestTests
|
||||
{
|
||||
public PaymentRequestTests(ITestOutputHelper helper)
|
||||
{
|
||||
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
|
||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanCreateViewUpdateAndDeletePaymentRequest()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
var user2 = tester.NewAccount();
|
||||
user2.GrantAccess();
|
||||
|
||||
var paymentRequestController = user.GetController<PaymentRequestController>();
|
||||
var guestpaymentRequestController = user2.GetController<PaymentRequestController>();
|
||||
|
||||
var request = new UpdatePaymentRequestViewModel()
|
||||
{
|
||||
Title = "original juice",
|
||||
Currency = "BTC",
|
||||
Amount = 1,
|
||||
StoreId = user.StoreId,
|
||||
Description = "description"
|
||||
};
|
||||
var id = (Assert
|
||||
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result).RouteValues.Values.First().ToString());
|
||||
|
||||
|
||||
|
||||
//permission guard for guests editing
|
||||
Assert
|
||||
.IsType<NotFoundResult>(guestpaymentRequestController.EditPaymentRequest(id).Result);
|
||||
|
||||
request.Title = "update";
|
||||
Assert.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(id, request).Result);
|
||||
|
||||
Assert.Equal(request.Title, Assert.IsType<ViewPaymentRequestViewModel>( Assert.IsType<ViewResult>(paymentRequestController.ViewPaymentRequest(id).Result).Model).Title);
|
||||
|
||||
Assert.False(string.IsNullOrEmpty(id));
|
||||
|
||||
Assert.IsType<ViewPaymentRequestViewModel>(Assert
|
||||
.IsType<ViewResult>(paymentRequestController.ViewPaymentRequest(id).Result).Model);
|
||||
|
||||
//Delete
|
||||
|
||||
Assert.IsType<ConfirmModel>(Assert
|
||||
.IsType<ViewResult>(paymentRequestController.RemovePaymentRequestPrompt(id).Result).Model);
|
||||
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(paymentRequestController.RemovePaymentRequest(id).Result);
|
||||
|
||||
Assert
|
||||
.IsType<NotFoundResult>(paymentRequestController.ViewPaymentRequest(id).Result);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanPayPaymentRequestWhenPossible()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
var paymentRequestController = user.GetController<PaymentRequestController>();
|
||||
|
||||
Assert.IsType<NotFoundResult>(await paymentRequestController.PayPaymentRequest(Guid.NewGuid().ToString()));
|
||||
|
||||
|
||||
var request = new UpdatePaymentRequestViewModel()
|
||||
{
|
||||
Title = "original juice",
|
||||
Currency = "BTC",
|
||||
Amount = 1,
|
||||
StoreId = user.StoreId,
|
||||
Description = "description"
|
||||
};
|
||||
var response = Assert
|
||||
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result)
|
||||
.RouteValues.First();
|
||||
|
||||
var invoiceId = Assert
|
||||
.IsType<OkObjectResult>(await paymentRequestController.PayPaymentRequest(response.Value.ToString(), false)).Value
|
||||
.ToString();
|
||||
|
||||
var actionResult = Assert
|
||||
.IsType<RedirectToActionResult>(await paymentRequestController.PayPaymentRequest(response.Value.ToString()));
|
||||
|
||||
Assert.Equal("Checkout", actionResult.ActionName);
|
||||
Assert.Equal("Invoice", actionResult.ControllerName);
|
||||
Assert.Contains(actionResult.RouteValues, pair => pair.Key == "Id" && pair.Value.ToString() == invoiceId);
|
||||
|
||||
var invoice = user.BitPay.GetInvoice(invoiceId, Facade.Merchant);
|
||||
Assert.Equal(1, invoice.Price);
|
||||
|
||||
request = new UpdatePaymentRequestViewModel()
|
||||
{
|
||||
Title = "original juice with expiry",
|
||||
Currency = "BTC",
|
||||
Amount = 1,
|
||||
ExpiryDate = DateTime.Today.Subtract( TimeSpan.FromDays(2)),
|
||||
StoreId = user.StoreId,
|
||||
Description = "description"
|
||||
};
|
||||
|
||||
response = Assert
|
||||
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result)
|
||||
.RouteValues.First();
|
||||
|
||||
Assert
|
||||
.IsType<BadRequestObjectResult>(await paymentRequestController.PayPaymentRequest(response.Value.ToString(), false));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanCancelPaymentWhenPossible()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
var paymentRequestController = user.GetController<PaymentRequestController>();
|
||||
|
||||
|
||||
Assert.IsType<NotFoundResult>(await
|
||||
paymentRequestController.CancelUnpaidPendingInvoice(Guid.NewGuid().ToString(), false));
|
||||
|
||||
|
||||
var request = new UpdatePaymentRequestViewModel()
|
||||
{
|
||||
Title = "original juice",
|
||||
Currency = "BTC",
|
||||
Amount = 1,
|
||||
StoreId = user.StoreId,
|
||||
Description = "description"
|
||||
};
|
||||
var response = Assert
|
||||
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result)
|
||||
.RouteValues.First();
|
||||
|
||||
var paymentRequestId = response.Value.ToString();
|
||||
|
||||
var invoiceId = Assert
|
||||
.IsType<OkObjectResult>(await paymentRequestController.PayPaymentRequest(paymentRequestId, false)).Value
|
||||
.ToString();
|
||||
|
||||
var actionResult = Assert
|
||||
.IsType<RedirectToActionResult>(await paymentRequestController.PayPaymentRequest(response.Value.ToString()));
|
||||
|
||||
Assert.Equal("Checkout", actionResult.ActionName);
|
||||
Assert.Equal("Invoice", actionResult.ControllerName);
|
||||
Assert.Contains(actionResult.RouteValues, pair => pair.Key == "Id" && pair.Value.ToString() == invoiceId);
|
||||
|
||||
var invoice = user.BitPay.GetInvoice(invoiceId, Facade.Merchant);
|
||||
Assert.Equal(InvoiceState.ToString(InvoiceStatus.New), invoice.Status);
|
||||
Assert.IsType<OkObjectResult>(await
|
||||
paymentRequestController.CancelUnpaidPendingInvoice(paymentRequestId, false));
|
||||
|
||||
invoice = user.BitPay.GetInvoice(invoiceId, Facade.Merchant);
|
||||
Assert.Equal(InvoiceState.ToString(InvoiceStatus.Invalid), invoice.Status);
|
||||
|
||||
|
||||
Assert.IsType<BadRequestObjectResult>(await
|
||||
paymentRequestController.CancelUnpaidPendingInvoice(paymentRequestId, false));
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -41,10 +41,15 @@ You can call bitcoin-cli inside the container with `docker exec`, for example, i
|
||||
```
|
||||
|
||||
If you are using Powershell:
|
||||
```
|
||||
```powershell
|
||||
.\docker-bitcoin-cli.ps1 sendtoaddress "mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf" 0.23111090
|
||||
```
|
||||
|
||||
You can also generate blocks:
|
||||
```powershell
|
||||
.\docker-bitcoin-generate.ps1 3
|
||||
```
|
||||
|
||||
### Using the test litecoin-cli
|
||||
|
||||
Same as bitcoin-cli, but with `.\docker-litecoin-cli.ps1` and `.\docker-litecoin-cli.sh` instead.
|
||||
|
@ -23,6 +23,7 @@ using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Lightning.CLightning;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -44,6 +45,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
NetworkProvider = new BTCPayNetworkProvider(NetworkType.Regtest);
|
||||
ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_BTCRPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("BTC").NBitcoinNetwork);
|
||||
ExplorerNode.ScanRPCCapabilities();
|
||||
LTCExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_LTCRPCCONNECTION", "server=http://127.0.0.1:43783;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("LTC").NBitcoinNetwork);
|
||||
|
||||
ExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("BTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_BTCNBXPLORERURL", "http://127.0.0.1:32838/")));
|
||||
@ -85,9 +87,11 @@ namespace BTCPayServer.Tests
|
||||
/// Connect a customer LN node to the merchant LN node
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Task EnsureChannelsSetup()
|
||||
public async Task EnsureChannelsSetup()
|
||||
{
|
||||
return BTCPayServer.Lightning.Tests.ConnectChannels.ConnectAll(ExplorerNode, GetLightningSenderClients(), GetLightningDestClients());
|
||||
Logs.Tester.LogInformation("Connecting channels");
|
||||
await BTCPayServer.Lightning.Tests.ConnectChannels.ConnectAll(ExplorerNode, GetLightningSenderClients(), GetLightningDestClients()).ConfigureAwait(false);
|
||||
Logs.Tester.LogInformation("Channels connected");
|
||||
}
|
||||
|
||||
private IEnumerable<ILightningClient> GetLightningSenderClients()
|
||||
|
237
BTCPayServer.Tests/StorageTests.cs
Normal file
237
BTCPayServer.Tests/StorageTests.cs
Normal file
@ -0,0 +1,237 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.ServerViewModels;
|
||||
using BTCPayServer.Storage.Models;
|
||||
using BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration;
|
||||
using BTCPayServer.Storage.Services.Providers.FileSystemStorage.Configuration;
|
||||
using BTCPayServer.Storage.ViewModels;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Internal;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Resources;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class StorageTests
|
||||
{
|
||||
public StorageTests(ITestOutputHelper helper)
|
||||
{
|
||||
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
|
||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async void CanConfigureStorage()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
var controller = tester.PayTester.GetController<ServerController>(user.UserId, user.StoreId);
|
||||
|
||||
// //For some reason, the tests cache something on circleci and this is set by default
|
||||
// //Initially, there is no configuration, make sure we display the choices available to configure
|
||||
// Assert.IsType<StorageSettings>(Assert.IsType<ViewResult>(await controller.Storage()).Model);
|
||||
//
|
||||
// //the file list should tell us it's not configured:
|
||||
// var viewFilesViewModelInitial =
|
||||
// Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files()).Model);
|
||||
// Assert.False(viewFilesViewModelInitial.StorageConfigured);
|
||||
|
||||
|
||||
//Once we select a provider, redirect to its view
|
||||
var localResult = Assert
|
||||
.IsType<RedirectToActionResult>(controller.Storage(new StorageSettings()
|
||||
{
|
||||
Provider = StorageProvider.FileSystem
|
||||
}));
|
||||
Assert.Equal(nameof(ServerController.StorageProvider), localResult.ActionName);
|
||||
Assert.Equal(StorageProvider.FileSystem.ToString(), localResult.RouteValues["provider"]);
|
||||
|
||||
|
||||
var AmazonS3result = Assert
|
||||
.IsType<RedirectToActionResult>(controller.Storage(new StorageSettings()
|
||||
{
|
||||
Provider = StorageProvider.AmazonS3
|
||||
}));
|
||||
Assert.Equal(nameof(ServerController.StorageProvider), AmazonS3result.ActionName);
|
||||
Assert.Equal(StorageProvider.AmazonS3.ToString(), AmazonS3result.RouteValues["provider"]);
|
||||
|
||||
var GoogleResult = Assert
|
||||
.IsType<RedirectToActionResult>(controller.Storage(new StorageSettings()
|
||||
{
|
||||
Provider = StorageProvider.GoogleCloudStorage
|
||||
}));
|
||||
Assert.Equal(nameof(ServerController.StorageProvider), GoogleResult.ActionName);
|
||||
Assert.Equal(StorageProvider.GoogleCloudStorage.ToString(), GoogleResult.RouteValues["provider"]);
|
||||
|
||||
|
||||
var AzureResult = Assert
|
||||
.IsType<RedirectToActionResult>(controller.Storage(new StorageSettings()
|
||||
{
|
||||
Provider = StorageProvider.AzureBlobStorage
|
||||
}));
|
||||
Assert.Equal(nameof(ServerController.StorageProvider), AzureResult.ActionName);
|
||||
Assert.Equal(StorageProvider.AzureBlobStorage.ToString(), AzureResult.RouteValues["provider"]);
|
||||
|
||||
//Cool, we get redirected to the config pages
|
||||
//Let's configure this stuff
|
||||
|
||||
//Let's try and cheat and go to an invalid storage provider config
|
||||
Assert.Equal(nameof(Storage), (Assert
|
||||
.IsType<RedirectToActionResult>(await controller.StorageProvider("I am not a real provider"))
|
||||
.ActionName));
|
||||
|
||||
//ok no more messing around, let's configure this shit.
|
||||
var fileSystemStorageConfiguration = Assert.IsType<FileSystemStorageConfiguration>(Assert
|
||||
.IsType<ViewResult>(await controller.StorageProvider(StorageProvider.FileSystem.ToString()))
|
||||
.Model);
|
||||
|
||||
//local file system does not need config, easy days!
|
||||
Assert.IsType<ViewResult>(
|
||||
await controller.EditFileSystemStorageProvider(fileSystemStorageConfiguration));
|
||||
|
||||
//ok cool, let's see if this got set right
|
||||
var shouldBeRedirectingToLocalStorageConfigPage =
|
||||
Assert.IsType<RedirectToActionResult>(await controller.Storage());
|
||||
Assert.Equal(nameof(StorageProvider), shouldBeRedirectingToLocalStorageConfigPage.ActionName);
|
||||
Assert.Equal(StorageProvider.FileSystem,
|
||||
shouldBeRedirectingToLocalStorageConfigPage.RouteValues["provider"]);
|
||||
|
||||
|
||||
//if we tell the settings page to force, it should allow us to select a new provider
|
||||
Assert.IsType<ChooseStorageViewModel>(Assert.IsType<ViewResult>(await controller.Storage(true)).Model);
|
||||
|
||||
//awesome, now let's see if the files result says we're all set up
|
||||
var viewFilesViewModel =
|
||||
Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files()).Model);
|
||||
Assert.True(viewFilesViewModel.StorageConfigured);
|
||||
Assert.Empty(viewFilesViewModel.Files);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async void CanUseLocalProviderFiles()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
var controller = tester.PayTester.GetController<ServerController>(user.UserId, user.StoreId);
|
||||
|
||||
var fileSystemStorageConfiguration = Assert.IsType<FileSystemStorageConfiguration>(Assert
|
||||
.IsType<ViewResult>(await controller.StorageProvider(StorageProvider.FileSystem.ToString()))
|
||||
.Model);
|
||||
Assert.IsType<ViewResult>(
|
||||
await controller.EditFileSystemStorageProvider(fileSystemStorageConfiguration));
|
||||
|
||||
var shouldBeRedirectingToLocalStorageConfigPage =
|
||||
Assert.IsType<RedirectToActionResult>(await controller.Storage());
|
||||
Assert.Equal(nameof(StorageProvider), shouldBeRedirectingToLocalStorageConfigPage.ActionName);
|
||||
Assert.Equal(StorageProvider.FileSystem,
|
||||
shouldBeRedirectingToLocalStorageConfigPage.RouteValues["provider"]);
|
||||
|
||||
|
||||
await CanUploadRemoveFiles(controller);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("ExternalIntegration", "ExternalIntegration")]
|
||||
public async Task CanUseAzureBlobStorage()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
var controller = tester.PayTester.GetController<ServerController>(user.UserId, user.StoreId);
|
||||
var azureBlobStorageConfiguration = Assert.IsType<AzureBlobStorageConfiguration>(Assert
|
||||
.IsType<ViewResult>(await controller.StorageProvider(StorageProvider.AzureBlobStorage.ToString()))
|
||||
.Model);
|
||||
|
||||
azureBlobStorageConfiguration.ConnectionString = GetFromSecrets("AzureBlobStorageConnectionString");
|
||||
azureBlobStorageConfiguration.ContainerName = "testscontainer";
|
||||
Assert.IsType<ViewResult>(
|
||||
await controller.EditAzureBlobStorageStorageProvider(azureBlobStorageConfiguration));
|
||||
|
||||
|
||||
var shouldBeRedirectingToAzureStorageConfigPage =
|
||||
Assert.IsType<RedirectToActionResult>(await controller.Storage());
|
||||
Assert.Equal(nameof(StorageProvider), shouldBeRedirectingToAzureStorageConfigPage.ActionName);
|
||||
Assert.Equal(StorageProvider.AzureBlobStorage,
|
||||
shouldBeRedirectingToAzureStorageConfigPage.RouteValues["provider"]);
|
||||
|
||||
//seems like azure config worked, let's see if the conn string was actually saved
|
||||
|
||||
Assert.Equal(azureBlobStorageConfiguration.ConnectionString, Assert
|
||||
.IsType<AzureBlobStorageConfiguration>(Assert
|
||||
.IsType<ViewResult>(
|
||||
await controller.StorageProvider(StorageProvider.AzureBlobStorage.ToString()))
|
||||
.Model).ConnectionString);
|
||||
|
||||
|
||||
|
||||
await CanUploadRemoveFiles(controller);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task CanUploadRemoveFiles(ServerController controller)
|
||||
{
|
||||
var fileContent = "content";
|
||||
var uploadFormFileResult = Assert.IsType<RedirectToActionResult>(await controller.CreateFile(TestUtils.GetFormFile("uploadtestfile.txt", fileContent)));
|
||||
Assert.True(uploadFormFileResult.RouteValues.ContainsKey("fileId"));
|
||||
var fileId = uploadFormFileResult.RouteValues["fileId"].ToString();
|
||||
Assert.Equal("Files", uploadFormFileResult.ActionName);
|
||||
|
||||
var viewFilesViewModel =
|
||||
Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files(fileId)).Model);
|
||||
|
||||
Assert.NotEmpty(viewFilesViewModel.Files);
|
||||
Assert.Equal(fileId, viewFilesViewModel.SelectedFileId);
|
||||
Assert.NotEmpty(viewFilesViewModel.DirectFileUrl);
|
||||
|
||||
|
||||
var net = new System.Net.WebClient();
|
||||
var data = await net.DownloadStringTaskAsync(new Uri(viewFilesViewModel.DirectFileUrl));
|
||||
Assert.Equal(fileContent, data);
|
||||
|
||||
Assert.Equal(StatusMessageModel.StatusSeverity.Success, new StatusMessageModel(Assert
|
||||
.IsType<RedirectToActionResult>(await controller.DeleteFile(fileId))
|
||||
.RouteValues["statusMessage"].ToString()).Severity);
|
||||
|
||||
viewFilesViewModel =
|
||||
Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files(fileId)).Model);
|
||||
|
||||
Assert.Null(viewFilesViewModel.DirectFileUrl);
|
||||
Assert.Null(viewFilesViewModel.SelectedFileId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static string GetFromSecrets(string key)
|
||||
{
|
||||
var connStr = Environment.GetEnvironmentVariable($"TESTS_{key}");
|
||||
if (!string.IsNullOrEmpty(connStr) && connStr != "none")
|
||||
return connStr;
|
||||
var builder = new ConfigurationBuilder();
|
||||
builder.AddUserSecrets("AB0AC1DD-9D26-485B-9416-56A33F268117");
|
||||
var config = builder.Build();
|
||||
var token = config[key];
|
||||
Assert.False(token == null, $"{key} is not set.\n Run \"dotnet user-secrets set {key} <value>\"");
|
||||
return token;
|
||||
}
|
||||
}
|
||||
}
|
@ -113,15 +113,18 @@ namespace BTCPayServer.Tests
|
||||
private async Task RegisterAsync()
|
||||
{
|
||||
var account = parent.PayTester.GetController<AccountController>();
|
||||
await account.Register(new RegisterViewModel()
|
||||
RegisterDetails = new RegisterViewModel()
|
||||
{
|
||||
Email = Guid.NewGuid() + "@toto.com",
|
||||
ConfirmPassword = "Kitten0@",
|
||||
Password = "Kitten0@",
|
||||
});
|
||||
};
|
||||
await account.Register(RegisterDetails);
|
||||
UserId = account.RegisteredUserId;
|
||||
}
|
||||
|
||||
public RegisterViewModel RegisterDetails{ get; set; }
|
||||
|
||||
public Bitpay BitPay
|
||||
{
|
||||
get; set;
|
||||
|
81
BTCPayServer.Tests/TestUtils.cs
Normal file
81
BTCPayServer.Tests/TestUtils.cs
Normal file
@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Internal;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public static class TestUtils
|
||||
{
|
||||
public static FormFile GetFormFile(string filename, string content)
|
||||
{
|
||||
File.WriteAllText(filename, content);
|
||||
|
||||
var fileInfo = new FileInfo(filename);
|
||||
FormFile formFile = new FormFile(
|
||||
new FileStream(filename, FileMode.OpenOrCreate),
|
||||
0,
|
||||
fileInfo.Length, fileInfo.Name, fileInfo.Name)
|
||||
{
|
||||
Headers = new HeaderDictionary()
|
||||
};
|
||||
formFile.ContentType = "text/plain";
|
||||
formFile.ContentDisposition = $"form-data; name=\"file\"; filename=\"{fileInfo.Name}\"";
|
||||
return formFile;
|
||||
}
|
||||
public static FormFile GetFormFile(string filename, byte[] content)
|
||||
{
|
||||
File.WriteAllBytes(filename, content);
|
||||
|
||||
var fileInfo = new FileInfo(filename);
|
||||
FormFile formFile = new FormFile(
|
||||
new FileStream(filename, FileMode.OpenOrCreate),
|
||||
0,
|
||||
fileInfo.Length, fileInfo.Name, fileInfo.Name)
|
||||
{
|
||||
Headers = new HeaderDictionary()
|
||||
};
|
||||
formFile.ContentType = "application/octet-stream";
|
||||
formFile.ContentDisposition = $"form-data; name=\"file\"; filename=\"{fileInfo.Name}\"";
|
||||
return formFile;
|
||||
}
|
||||
public static void Eventually(Action act)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource(20000);
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
act();
|
||||
break;
|
||||
}
|
||||
catch (XunitException) when (!cts.Token.IsCancellationRequested)
|
||||
{
|
||||
cts.Token.WaitHandle.WaitOne(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task EventuallyAsync(Func<Task> act)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource(20000);
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await act();
|
||||
break;
|
||||
}
|
||||
catch (XunitException) when (!cts.Token.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
3
BTCPayServer.Tests/docker-bitcoin-generate.ps1
Normal file
3
BTCPayServer.Tests/docker-bitcoin-generate.ps1
Normal file
@ -0,0 +1,3 @@
|
||||
$bitcoind_container_id=$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=bitcoind)
|
||||
$address=$(docker exec -ti $bitcoind_container_id bitcoin-cli -datadir="/data" getnewaddress)
|
||||
docker exec -ti $bitcoind_container_id bitcoin-cli -datadir="/data" generatetoaddress $args $address
|
@ -19,6 +19,8 @@ services:
|
||||
TESTS_MYSQL: User ID=root;Host=mysql;Port=3306;Database=btcpayserver
|
||||
TESTS_PORT: 80
|
||||
TESTS_HOSTNAME: tests
|
||||
TESTS_RUN_EXTERNAL_INTEGRATION: ${TESTS_RUN_EXTERNAL_INTEGRATION:-false}
|
||||
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"
|
||||
@ -36,7 +38,7 @@ services:
|
||||
|
||||
# The dev container is not actually used, it is just handy to run `docker-compose up dev` to start all services
|
||||
dev:
|
||||
image: btcpayserver/bitcoin:0.17.0
|
||||
image: btcpayserver/bitcoin:0.18.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
@ -53,7 +55,7 @@ services:
|
||||
- merchant_lnd
|
||||
|
||||
devlnd:
|
||||
image: btcpayserver/bitcoin:0.17.0
|
||||
image: btcpayserver/bitcoin:0.18.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
@ -69,7 +71,7 @@ services:
|
||||
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.0.0.8
|
||||
image: nicolasdorier/nbxplorer:2.0.0.40
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
@ -96,13 +98,14 @@ services:
|
||||
|
||||
bitcoind:
|
||||
restart: unless-stopped
|
||||
image: btcpayserver/bitcoin:0.17.0
|
||||
image: btcpayserver/bitcoin:0.18.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_EXTRA_ARGS: |-
|
||||
rpcuser=ceiwHEbqWI83
|
||||
rpcpassword=DwubwWsoo3
|
||||
rpcport=43782
|
||||
rpcbind=0.0.0.0:43782
|
||||
port=39388
|
||||
whitelist=0.0.0.0/0
|
||||
zmqpubrawblock=tcp://0.0.0.0:28332
|
||||
@ -119,7 +122,7 @@ services:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: btcpayserver/lightning:v0.6.2-dev
|
||||
image: btcpayserver/lightning:v0.7.0-1-dev
|
||||
stop_signal: SIGKILL
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@ -165,7 +168,7 @@ services:
|
||||
- merchant_lightningd
|
||||
|
||||
merchant_lightningd:
|
||||
image: btcpayserver/lightning:v0.6.2-dev
|
||||
image: btcpayserver/lightning:v0.7.0-1-dev
|
||||
stop_signal: SIGKILL
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
@ -222,7 +225,7 @@ services:
|
||||
- MYSQL_ALLOW_EMPTY_PASSWORD=yes
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.5.2-beta
|
||||
image: btcpayserver/lnd:v0.6-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -252,7 +255,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.5.2-beta
|
||||
image: btcpayserver/lnd:v0.6-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
|
@ -3,3 +3,6 @@ set -e
|
||||
|
||||
dotnet test --filter Fast=Fast --no-build
|
||||
dotnet test --filter Integration=Integration --no-build -v n
|
||||
if [[ "$TESTS_RUN_EXTERNAL_INTEGRATION" == "true" ]]; then
|
||||
dotnet test --filter ExternalIntegration=ExternalIntegration --no-build -v n
|
||||
fi
|
||||
|
@ -8,10 +8,6 @@ namespace BTCPayServer.Authentication
|
||||
{
|
||||
public class BitTokenEntity
|
||||
{
|
||||
public string Facade
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string Value
|
||||
{
|
||||
get; set;
|
||||
@ -39,7 +35,6 @@ namespace BTCPayServer.Authentication
|
||||
return new BitTokenEntity()
|
||||
{
|
||||
Label = Label,
|
||||
Facade = Facade,
|
||||
StoreId = StoreId,
|
||||
PairingTime = PairingTime,
|
||||
SIN = SIN,
|
||||
|
@ -11,11 +11,6 @@ namespace BTCPayServer.Authentication
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string Facade
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string Label
|
||||
{
|
||||
get;
|
||||
|
@ -1,5 +1,5 @@
|
||||
using BTCPayServer.Data;
|
||||
using DBreeze;
|
||||
using DBriize;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Newtonsoft.Json;
|
||||
@ -90,7 +90,6 @@ namespace BTCPayServer.Authentication
|
||||
return new BitTokenEntity()
|
||||
{
|
||||
Label = data.Label,
|
||||
Facade = data.Facade,
|
||||
Value = data.Id,
|
||||
SIN = data.SIN,
|
||||
PairingTime = data.PairingTime,
|
||||
@ -129,7 +128,6 @@ namespace BTCPayServer.Authentication
|
||||
{
|
||||
var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeEntity.Id);
|
||||
pairingCode.Label = pairingCodeEntity.Label;
|
||||
pairingCode.Facade = pairingCodeEntity.Facade;
|
||||
await ctx.SaveChangesAsync();
|
||||
return CreatePairingCodeEntity(pairingCode);
|
||||
}
|
||||
@ -178,7 +176,6 @@ namespace BTCPayServer.Authentication
|
||||
{
|
||||
Id = pairingCode.TokenValue,
|
||||
PairingTime = DateTime.UtcNow,
|
||||
Facade = pairingCode.Facade,
|
||||
Label = pairingCode.Label,
|
||||
StoreDataId = pairingCode.StoreDataId,
|
||||
SIN = pairingCode.SIN
|
||||
@ -213,7 +210,6 @@ namespace BTCPayServer.Authentication
|
||||
return null;
|
||||
return new PairingCodeEntity()
|
||||
{
|
||||
Facade = data.Facade,
|
||||
Id = data.Id,
|
||||
Label = data.Label,
|
||||
Expiration = data.Expiration,
|
||||
|
@ -10,6 +10,12 @@ using NBXplorer;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public enum DerivationType
|
||||
{
|
||||
Legacy,
|
||||
SegwitP2SH,
|
||||
Segwit
|
||||
}
|
||||
public class BTCPayDefaultSettings
|
||||
{
|
||||
static BTCPayDefaultSettings()
|
||||
@ -44,7 +50,6 @@ namespace BTCPayServer
|
||||
public string CryptoCode { get; internal set; }
|
||||
public string BlockExplorerLink { get; internal set; }
|
||||
public string UriScheme { get; internal set; }
|
||||
public Money MinFee { get; internal set; }
|
||||
public string DisplayName { get; set; }
|
||||
|
||||
[Obsolete("Should not be needed")]
|
||||
@ -64,7 +69,8 @@ namespace BTCPayServer
|
||||
public KeyPath CoinType { get; internal set; }
|
||||
public int MaxTrackedConfirmation { get; internal set; } = 6;
|
||||
public string[] DefaultRateRules { get; internal set; } = Array.Empty<string>();
|
||||
|
||||
public bool SupportRBF { get; internal set; }
|
||||
public Dictionary<uint, DerivationType> ElectrumMapping = new Dictionary<uint, DerivationType>();
|
||||
public override string ToString()
|
||||
{
|
||||
return CryptoCode;
|
||||
|
@ -25,7 +25,22 @@ namespace BTCPayServer
|
||||
CryptoImagePath = "imlegacy/bitcoin.svg",
|
||||
LightningImagePath = "imlegacy/bitcoin-lightning.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("0'") : new KeyPath("1'")
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("0'") : new KeyPath("1'"),
|
||||
SupportRBF = true,
|
||||
//https://github.com/spesmilo/electrum/blob/11733d6bc271646a00b69ff07657119598874da4/electrum/constants.py
|
||||
ElectrumMapping = NetworkType == NetworkType.Mainnet
|
||||
? new Dictionary<uint, DerivationType>()
|
||||
{
|
||||
{0x0488b21eU, DerivationType.Legacy }, // xpub
|
||||
{0x049d7cb2U, DerivationType.SegwitP2SH }, // ypub
|
||||
{0x4b24746U, DerivationType.Segwit }, //zpub
|
||||
}
|
||||
: new Dictionary<uint, DerivationType>()
|
||||
{
|
||||
{0x043587cfU, DerivationType.Legacy},
|
||||
{0x044a5262U, DerivationType.SegwitP2SH},
|
||||
{0x045f1cf6U, DerivationType.Segwit}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -27,8 +27,7 @@ namespace BTCPayServer
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
//https://github.com/satoshilabs/slips/blob/master/slip-0044.md
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("5'")
|
||||
: new KeyPath("1'"),
|
||||
MinFee = Money.Satoshis(1m)
|
||||
: new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -28,8 +28,7 @@ namespace BTCPayServer
|
||||
},
|
||||
CryptoImagePath = "imlegacy/dogecoin.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("3'") : new KeyPath("1'"),
|
||||
MinFee = Money.Coins(1m)
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("3'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -10,20 +10,21 @@ namespace BTCPayServer
|
||||
{
|
||||
public void InitGroestlcoin()
|
||||
{
|
||||
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("GRS");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Groestlcoin",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://chainz.cryptoid.info/grs/tx.dws?{0}.htm" : "https://chainz.cryptoid.info/grs-test/tx.dws?{0}.htm",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet
|
||||
? "https://chainz.cryptoid.info/grs/tx.dws?{0}.htm"
|
||||
: "https://chainz.cryptoid.info/grs-test/tx.dws?{0}.htm",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "groestlcoin",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"GRS_X = GRS_BTC * BTC_X",
|
||||
"GRS_BTC = bittrex(GRS_BTC)"
|
||||
"GRS_X = GRS_BTC * BTC_X",
|
||||
"GRS_BTC = bittrex(GRS_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/groestlcoin.png",
|
||||
LightningImagePath = "imlegacy/groestlcoin-lightning.svg",
|
||||
|
@ -17,14 +17,30 @@ namespace BTCPayServer
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Litecoin",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://live.blockcypher.com/ltc/tx/{0}/" : "http://explorer.litecointools.com/tx/{0}",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet
|
||||
? "https://live.blockcypher.com/ltc/tx/{0}/"
|
||||
: "http://explorer.litecointools.com/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "litecoin",
|
||||
CryptoImagePath = "imlegacy/litecoin.svg",
|
||||
LightningImagePath = "imlegacy/litecoin-lightning.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("2'") : new KeyPath("1'")
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("2'") : new KeyPath("1'"),
|
||||
//https://github.com/pooler/electrum-ltc/blob/0d6989a9d2fb2edbea421c116e49d1015c7c5a91/electrum_ltc/constants.py
|
||||
ElectrumMapping = NetworkType == NetworkType.Mainnet
|
||||
? new Dictionary<uint, DerivationType>()
|
||||
{
|
||||
{0x0488b21eU, DerivationType.Legacy },
|
||||
{0x049d7cb2U, DerivationType.SegwitP2SH },
|
||||
{0x04b24746U, DerivationType.Segwit },
|
||||
}
|
||||
: new Dictionary<uint, DerivationType>()
|
||||
{
|
||||
{0x043587cfU, DerivationType.Legacy },
|
||||
{0x044a5262U, DerivationType.SegwitP2SH },
|
||||
{0x045f1cf6U, DerivationType.Segwit }
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -56,6 +56,22 @@ namespace BTCPayServer
|
||||
InitGroestlcoin();
|
||||
InitViacoin();
|
||||
|
||||
// Assume that electrum mappings are same as BTC if not specified
|
||||
foreach (var network in _Networks)
|
||||
{
|
||||
if(network.Value.ElectrumMapping.Count == 0)
|
||||
{
|
||||
network.Value.ElectrumMapping = GetNetwork("BTC").ElectrumMapping;
|
||||
if (!network.Value.NBitcoinNetwork.Consensus.SupportSegwit)
|
||||
{
|
||||
network.Value.ElectrumMapping =
|
||||
network.Value.ElectrumMapping
|
||||
.Where(kv => kv.Value == DerivationType.Legacy)
|
||||
.ToDictionary(k => k.Key, k => k.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Disabled because of https://twitter.com/Cryptopia_NZ/status/1085084168852291586
|
||||
//InitPolis();
|
||||
//InitBitcoinplus();
|
||||
|
@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
<Version>1.0.3.63</Version>
|
||||
<Version>1.0.3.102</Version>
|
||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
@ -30,25 +30,28 @@
|
||||
<None Remove="Currencies.txt" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="bundleconfig.json" />
|
||||
<EmbeddedResource Include="Currencies.txt" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.9" />
|
||||
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.18" />
|
||||
<PackageReference Include="BuildBundlerMinifier" Version="2.9.406" />
|
||||
<PackageReference Include="BundlerMinifier.Core" Version="2.9.406" />
|
||||
<PackageReference Include="BundlerMinifier.TagHelpers" Version="2.9.406" />
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.5.3" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="4.0.199" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="4.0.207" />
|
||||
<PackageReference Include="LedgerWallet" Version="2.0.0.3" />
|
||||
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
|
||||
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NBitcoin" Version="4.1.1.78" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.31" />
|
||||
<PackageReference Include="DBreeze" Version="1.92.0" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="2.0.0.3" />
|
||||
<PackageReference Include="NBitcoin" Version="4.1.2.20" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.34" />
|
||||
<PackageReference Include="DBriize" Version="1.0.0.4" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="2.0.0.12" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
|
||||
<PackageReference Include="NicolasDorier.RateLimits" Version="1.0.0.3" />
|
||||
@ -59,7 +62,6 @@
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="2.1.1" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="4.0.0" />
|
||||
<PackageReference Include="SSH.NET" Version="2016.1.0" />
|
||||
<PackageReference Include="System.Xml.XmlSerializer" Version="4.3.0" />
|
||||
<PackageReference Include="Text.Analyzers" Version="2.6.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
@ -67,7 +69,13 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.6" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.9" />
|
||||
<PackageReference Include="TwentyTwenty.Storage" Version="2.10.1" />
|
||||
<PackageReference Include="TwentyTwenty.Storage.Amazon" Version="2.10.1" />
|
||||
<PackageReference Include="TwentyTwenty.Storage.Azure" Version="2.10.1" />
|
||||
<PackageReference Include="TwentyTwenty.Storage.Google" Version="2.10.1" />
|
||||
<PackageReference Include="TwentyTwenty.Storage.Local" Version="2.10.1" />
|
||||
<PackageReference Include="U2F.Core" Version="1.0.4" />
|
||||
<PackageReference Include="YamlDotNet" Version="5.2.1" />
|
||||
</ItemGroup>
|
||||
|
||||
@ -123,9 +131,11 @@
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Build\" />
|
||||
<Folder Include="U2F\Services" />
|
||||
<Folder Include="wwwroot\vendor\clipboard.js\" />
|
||||
<Folder Include="wwwroot\vendor\highlightjs\" />
|
||||
<Folder Include="wwwroot\vendor\summernote" />
|
||||
<Folder Include="wwwroot\vendor\u2f" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -142,6 +152,9 @@
|
||||
<Content Update="Views\Server\LightningWalletServices.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Server\P2PService.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Server\SSHService.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
@ -169,6 +182,15 @@
|
||||
<Content Update="Views\Wallets\ListWallets.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Wallets\WalletPSBTCombine.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Wallets\WalletPSBTReady.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Wallets\WalletPSBT.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Wallets\WalletRescan.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
|
@ -15,7 +15,6 @@ using Renci.SshNet;
|
||||
using NBitcoin.DataEncoders;
|
||||
using BTCPayServer.SSH;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Configuration.External;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
@ -49,11 +48,7 @@ namespace BTCPayServer.Configuration
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
public List<IPEndPoint> Listen
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public EndPoint SocksEndpoint { get; set; }
|
||||
|
||||
public List<NBXplorerConnectionSetting> NBXplorerConnectionSettings
|
||||
{
|
||||
@ -109,82 +104,26 @@ namespace BTCPayServer.Configuration
|
||||
{
|
||||
if (!LightningConnectionString.TryParse(lightning, true, out var connectionString, out var error))
|
||||
{
|
||||
throw new ConfigException($"Invalid setting {net.CryptoCode}.lightning, " + Environment.NewLine +
|
||||
Logs.Configuration.LogWarning($"Invalid setting {net.CryptoCode}.lightning, " + Environment.NewLine +
|
||||
$"If you have a c-lightning server use: 'type=clightning;server=/root/.lightning/lightning-rpc', " + Environment.NewLine +
|
||||
$"If you have a lightning charge server: 'type=charge;server=https://charge.example.com;api-token=yourapitoken'" + Environment.NewLine +
|
||||
$"If you have a lnd server: 'type=lnd-rest;server=https://lnd:lnd@lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
$" lnd server: 'type=lnd-rest;server=https://lnd:lnd@lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
error);
|
||||
$"Error: {error}" + Environment.NewLine +
|
||||
"This service will not be exposed through BTCPay Server");
|
||||
}
|
||||
if (connectionString.IsLegacy)
|
||||
else
|
||||
{
|
||||
Logs.Configuration.LogWarning($"Setting {net.CryptoCode}.lightning is a deprecated format, it will work now, but please replace it for future versions with '{connectionString.ToString()}'");
|
||||
if (connectionString.IsLegacy)
|
||||
{
|
||||
Logs.Configuration.LogWarning($"Setting {net.CryptoCode}.lightning is a deprecated format, it will work now, but please replace it for future versions with '{connectionString.ToString()}'");
|
||||
}
|
||||
InternalLightningByCryptoCode.Add(net.CryptoCode, connectionString);
|
||||
}
|
||||
InternalLightningByCryptoCode.Add(net.CryptoCode, connectionString);
|
||||
}
|
||||
}
|
||||
|
||||
void externalLnd<T>(string code, string lndType)
|
||||
{
|
||||
var lightning = conf.GetOrDefault<string>(code, string.Empty);
|
||||
if (lightning.Length != 0)
|
||||
{
|
||||
if (!LightningConnectionString.TryParse(lightning, false, out var connectionString, out var error))
|
||||
{
|
||||
throw new ConfigException($"Invalid setting {code}, " + Environment.NewLine +
|
||||
$"lnd server: 'type={lndType};server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
$"lnd server: 'type={lndType};server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
error);
|
||||
}
|
||||
var instanceType = typeof(T);
|
||||
ExternalServicesByCryptoCode.Add(net.CryptoCode, (ExternalService)Activator.CreateInstance(instanceType, connectionString));
|
||||
}
|
||||
};
|
||||
|
||||
externalLnd<ExternalLndGrpc>($"{net.CryptoCode}.external.lnd.grpc", "lnd-grpc");
|
||||
externalLnd<ExternalLndRest>($"{net.CryptoCode}.external.lnd.rest", "lnd-rest");
|
||||
|
||||
{
|
||||
var spark = conf.GetOrDefault<string>($"{net.CryptoCode}.external.spark", string.Empty);
|
||||
if (spark.Length != 0)
|
||||
{
|
||||
if (!SparkConnectionString.TryParse(spark, out var connectionString))
|
||||
{
|
||||
throw new ConfigException($"Invalid setting {net.CryptoCode}.external.spark, " + Environment.NewLine +
|
||||
$"Valid example: 'server=https://btcpay.example.com/spark/btc/;cookiefile=/etc/clightning_bitcoin_spark/.cookie'");
|
||||
}
|
||||
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalSpark(connectionString));
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
var rtl = conf.GetOrDefault<string>($"{net.CryptoCode}.external.rtl", string.Empty);
|
||||
if (rtl.Length != 0)
|
||||
{
|
||||
if (!SparkConnectionString.TryParse(rtl, out var connectionString))
|
||||
{
|
||||
throw new ConfigException($"Invalid setting {net.CryptoCode}.external.rtl, " + Environment.NewLine +
|
||||
$"Valid example: 'server=https://btcpay.example.com/rtl/btc/;cookiefile=/etc/clightning_bitcoin_rtl/.cookie'");
|
||||
}
|
||||
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalRTL(connectionString));
|
||||
}
|
||||
}
|
||||
|
||||
var charge = conf.GetOrDefault<string>($"{net.CryptoCode}.external.charge", string.Empty);
|
||||
if (charge.Length != 0)
|
||||
{
|
||||
if (!LightningConnectionString.TryParse(charge, false, out var chargeConnectionString, out var chargeError))
|
||||
LightningConnectionString.TryParse("type=charge;" + charge, false, out chargeConnectionString, out chargeError);
|
||||
|
||||
if(chargeConnectionString == null || chargeConnectionString.ConnectionType != LightningConnectionType.Charge)
|
||||
{
|
||||
throw new ConfigException($"Invalid setting {net.CryptoCode}.external.charge, " + Environment.NewLine +
|
||||
$"lightning charge server: 'type=charge;server=https://charge.example.com;api-token=2abdf302...'" + Environment.NewLine +
|
||||
$"lightning charge server: 'type=charge;server=https://charge.example.com;cookiefilepath=/root/.charge/.cookie'" + Environment.NewLine +
|
||||
chargeError ?? string.Empty);
|
||||
}
|
||||
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalCharge(chargeConnectionString));
|
||||
}
|
||||
ExternalServices.Load(net.CryptoCode, conf);
|
||||
}
|
||||
|
||||
Logs.Configuration.LogInformation("Supported chains: " + String.Join(',', supportedChains.ToArray()));
|
||||
@ -198,14 +137,24 @@ namespace BTCPayServer.Configuration
|
||||
.Select(p => (Name: p.p.Substring(0, p.SeparatorIndex),
|
||||
Link: p.p.Substring(p.SeparatorIndex + 1))))
|
||||
{
|
||||
ExternalServices.AddOrReplace(service.Name, service.Link);
|
||||
if (Uri.TryCreate(service.Link, UriKind.RelativeOrAbsolute, out var uri))
|
||||
OtherExternalServices.AddOrReplace(service.Name, uri);
|
||||
}
|
||||
}
|
||||
|
||||
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
|
||||
MySQLConnectionString = conf.GetOrDefault<string>("mysql", null);
|
||||
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
|
||||
ExternalUrl = conf.GetOrDefault<Uri>("externalurl", null);
|
||||
TorrcFile = conf.GetOrDefault<string>("torrcfile", null);
|
||||
|
||||
var socksEndpointString = conf.GetOrDefault<string>("socksendpoint", null);
|
||||
if(!string.IsNullOrEmpty(socksEndpointString))
|
||||
{
|
||||
if (!Utils.TryParseEndpoint(socksEndpointString, 9050, out var endpoint))
|
||||
throw new ConfigException("Invalid value for socksendpoint");
|
||||
SocksEndpoint = endpoint;
|
||||
}
|
||||
|
||||
|
||||
var sshSettings = ParseSSHConfiguration(conf);
|
||||
if ((!string.IsNullOrEmpty(sshSettings.Password) || !string.IsNullOrEmpty(sshSettings.KeyFile)) && !string.IsNullOrEmpty(sshSettings.Server))
|
||||
@ -265,7 +214,6 @@ namespace BTCPayServer.Configuration
|
||||
|
||||
private SSHSettings ParseSSHConfiguration(IConfiguration conf)
|
||||
{
|
||||
var externalUrl = conf.GetOrDefault<Uri>("externalurl", null);
|
||||
var settings = new SSHSettings();
|
||||
settings.Server = conf.GetOrDefault<string>("sshconnection", null);
|
||||
if (settings.Server != null)
|
||||
@ -292,12 +240,6 @@ namespace BTCPayServer.Configuration
|
||||
settings.Username = "root";
|
||||
}
|
||||
}
|
||||
else if (externalUrl != null)
|
||||
{
|
||||
settings.Port = 22;
|
||||
settings.Username = "root";
|
||||
settings.Server = externalUrl.DnsSafeHost;
|
||||
}
|
||||
settings.Password = conf.GetOrDefault<string>("sshpassword", "");
|
||||
settings.KeyFile = conf.GetOrDefault<string>("sshkeyfile", "");
|
||||
settings.KeyFilePassword = conf.GetOrDefault<string>("sshkeyfilepassword", "");
|
||||
@ -311,9 +253,9 @@ namespace BTCPayServer.Configuration
|
||||
|
||||
public string RootPath { get; set; }
|
||||
public Dictionary<string, LightningConnectionString> InternalLightningByCryptoCode { get; set; } = new Dictionary<string, LightningConnectionString>();
|
||||
public Dictionary<string, string> ExternalServices { get; set; } = new Dictionary<string, string>();
|
||||
|
||||
public ExternalServices ExternalServicesByCryptoCode { get; set; } = new ExternalServices();
|
||||
public Dictionary<string, Uri> OtherExternalServices { get; set; } = new Dictionary<string, Uri>();
|
||||
public ExternalServices ExternalServices { get; set; } = new ExternalServices();
|
||||
|
||||
public BTCPayNetworkProvider NetworkProvider { get; set; }
|
||||
public string PostgresConnectionString
|
||||
@ -326,11 +268,6 @@ namespace BTCPayServer.Configuration
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public Uri ExternalUrl
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public bool BundleJsCss
|
||||
{
|
||||
get;
|
||||
@ -342,14 +279,6 @@ namespace BTCPayServer.Configuration
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
internal string GetRootUri()
|
||||
{
|
||||
if (ExternalUrl == null)
|
||||
return null;
|
||||
UriBuilder builder = new UriBuilder(ExternalUrl);
|
||||
builder.Path = RootPath;
|
||||
return builder.ToString();
|
||||
}
|
||||
public string TorrcFile { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
|
@ -32,7 +32,6 @@ namespace BTCPayServer.Configuration
|
||||
app.Option("--chains | -c", $"Chains to support as a comma separated (default: btc; available: {chains})", CommandOptionType.SingleValue);
|
||||
app.Option("--postgres", $"Connection string to a PostgreSQL database (default: SQLite)", CommandOptionType.SingleValue);
|
||||
app.Option("--mysql", $"Connection string to a MySQL database (default: SQLite)", CommandOptionType.SingleValue);
|
||||
app.Option("--externalurl", $"The expected external URL of this service, to use if BTCPay is behind a reverse proxy (default: empty, use the incoming HTTP request to figure out)", CommandOptionType.SingleValue);
|
||||
app.Option("--externalservices", $"Links added to external services inside Server Settings / Services under the format service1:path2;service2:path2.(default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--bundlejscss", $"Bundle JavaScript and CSS files for better performance (default: true)", CommandOptionType.SingleValue);
|
||||
app.Option("--rootpath", "The root path in the URL to access BTCPay (default: /)", CommandOptionType.SingleValue);
|
||||
@ -41,6 +40,8 @@ namespace BTCPayServer.Configuration
|
||||
app.Option("--sshkeyfile", "SSH private key file to manage BTCPay (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--sshkeyfilepassword", "Password of the SSH keyfile (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--sshtrustedfingerprints", "SSH Host public key fingerprint or sha256 (default: empty, it will allow untrusted connections)", CommandOptionType.SingleValue);
|
||||
app.Option("--torrcfile", "Path to torrc file containing hidden services directories (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--socksendpoint", "Socks endpoint to connect to onion urls (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--debuglog", "A rolling log file for debug messages.", CommandOptionType.SingleValue);
|
||||
app.Option("--debugloglevel", "The severity you log (default:information)", CommandOptionType.SingleValue);
|
||||
app.Option("--disable-registration", "Disables new user registrations (default:true)", CommandOptionType.SingleValue);
|
||||
|
@ -1,19 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayServer.Configuration.External
|
||||
{
|
||||
public class ExternalCharge : ExternalService
|
||||
{
|
||||
public ExternalCharge(LightningConnectionString connectionString)
|
||||
{
|
||||
if (connectionString == null)
|
||||
throw new ArgumentNullException(nameof(connectionString));
|
||||
ConnectionString = connectionString;
|
||||
}
|
||||
public LightningConnectionString ConnectionString { get; }
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayServer.Configuration.External
|
||||
{
|
||||
public abstract class ExternalLnd : ExternalService
|
||||
{
|
||||
public ExternalLnd(LightningConnectionString connectionString, string type)
|
||||
{
|
||||
ConnectionString = connectionString;
|
||||
Type = type;
|
||||
}
|
||||
|
||||
public string Type { get; set; }
|
||||
public LightningConnectionString ConnectionString { get; set; }
|
||||
}
|
||||
|
||||
public class ExternalLndGrpc : ExternalLnd
|
||||
{
|
||||
public ExternalLndGrpc(LightningConnectionString connectionString) : base(connectionString, "lnd-grpc") { }
|
||||
}
|
||||
|
||||
public class ExternalLndRest : ExternalLnd
|
||||
{
|
||||
public ExternalLndRest(LightningConnectionString connectionString) : base(connectionString, "lnd-rest") { }
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Configuration.External
|
||||
{
|
||||
public class ExternalRTL : ExternalService, IAccessKeyService
|
||||
{
|
||||
public SparkConnectionString ConnectionString { get; }
|
||||
|
||||
public ExternalRTL(SparkConnectionString connectionString)
|
||||
{
|
||||
if (connectionString == null)
|
||||
throw new ArgumentNullException(nameof(connectionString));
|
||||
ConnectionString = connectionString;
|
||||
}
|
||||
|
||||
public async Task<string> ExtractAccessKey()
|
||||
{
|
||||
if (ConnectionString?.CookeFile == null)
|
||||
throw new FormatException("Invalid connection string");
|
||||
return await System.IO.File.ReadAllTextAsync(ConnectionString.CookeFile);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayServer.Configuration.External
|
||||
{
|
||||
public class ExternalServices : MultiValueDictionary<string, ExternalService>
|
||||
{
|
||||
public IEnumerable<T> GetServices<T>(string cryptoCode) where T : ExternalService
|
||||
{
|
||||
if (!this.TryGetValue(cryptoCode.ToUpperInvariant(), out var services))
|
||||
return Array.Empty<T>();
|
||||
return services.OfType<T>();
|
||||
}
|
||||
}
|
||||
|
||||
public class ExternalService
|
||||
{
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Configuration.External
|
||||
{
|
||||
public interface IAccessKeyService
|
||||
{
|
||||
SparkConnectionString ConnectionString { get; }
|
||||
Task<string> ExtractAccessKey();
|
||||
}
|
||||
public class ExternalSpark : ExternalService, IAccessKeyService
|
||||
{
|
||||
public SparkConnectionString ConnectionString { get; }
|
||||
|
||||
public ExternalSpark(SparkConnectionString connectionString)
|
||||
{
|
||||
if (connectionString == null)
|
||||
throw new ArgumentNullException(nameof(connectionString));
|
||||
ConnectionString = connectionString;
|
||||
}
|
||||
|
||||
public async Task<string> ExtractAccessKey()
|
||||
{
|
||||
if (ConnectionString?.CookeFile == null)
|
||||
throw new FormatException("Invalid connection string");
|
||||
var cookie = (ConnectionString.CookeFile == "fake"
|
||||
? "fake:fake:fake" // Hacks for testing
|
||||
: await System.IO.File.ReadAllTextAsync(ConnectionString.CookeFile)).Split(':');
|
||||
if (cookie.Length >= 3)
|
||||
{
|
||||
return cookie[2];
|
||||
}
|
||||
throw new FormatException("Invalid cookiefile format");
|
||||
}
|
||||
}
|
||||
}
|
200
BTCPayServer/Configuration/ExternalConnectionString.cs
Normal file
200
BTCPayServer/Configuration/ExternalConnectionString.cs
Normal file
@ -0,0 +1,200 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
public class ExternalConnectionString
|
||||
{
|
||||
public ExternalConnectionString()
|
||||
{
|
||||
|
||||
}
|
||||
public ExternalConnectionString(Uri server)
|
||||
{
|
||||
Server = server;
|
||||
}
|
||||
public Uri Server { get; set; }
|
||||
public byte[] Macaroon { get; set; }
|
||||
public Macaroons Macaroons { get; set; }
|
||||
public string MacaroonFilePath { get; set; }
|
||||
public string CertificateThumbprint { get; set; }
|
||||
public string MacaroonDirectoryPath { get; set; }
|
||||
public string APIToken { get; set; }
|
||||
public string CookieFilePath { get; set; }
|
||||
public string AccessKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Return a connectionString which does not depends on external resources or information like relative path or file path
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<ExternalConnectionString> Expand(Uri absoluteUrlBase, ExternalServiceTypes serviceType)
|
||||
{
|
||||
var connectionString = this.Clone();
|
||||
// Transform relative URI into absolute URI
|
||||
var serviceUri = connectionString.Server.IsAbsoluteUri ? connectionString.Server : ToRelative(absoluteUrlBase, connectionString.Server.ToString());
|
||||
if (!serviceUri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase) &&
|
||||
!serviceUri.DnsSafeHost.EndsWith(".onion", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new System.Security.SecurityException($"Insecure transport protocol to access this service, please use HTTPS or TOR");
|
||||
}
|
||||
connectionString.Server = serviceUri;
|
||||
|
||||
if (serviceType == ExternalServiceTypes.LNDGRPC || serviceType == ExternalServiceTypes.LNDRest)
|
||||
{
|
||||
// Read the MacaroonDirectory
|
||||
if (connectionString.MacaroonDirectoryPath != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
connectionString.Macaroons = await Macaroons.GetFromDirectoryAsync(connectionString.MacaroonDirectoryPath);
|
||||
connectionString.MacaroonDirectoryPath = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new System.IO.DirectoryNotFoundException("Macaroon directory path not found", ex);
|
||||
}
|
||||
}
|
||||
|
||||
// Read the MacaroonFilePath
|
||||
if (connectionString.MacaroonFilePath != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
connectionString.Macaroon = await System.IO.File.ReadAllBytesAsync(connectionString.MacaroonFilePath);
|
||||
connectionString.MacaroonFilePath = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new System.IO.FileNotFoundException("Macaroon not found", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (serviceType == ExternalServiceTypes.Charge || serviceType == ExternalServiceTypes.RTL || serviceType == ExternalServiceTypes.Spark)
|
||||
{
|
||||
// Read access key from cookie file
|
||||
if (connectionString.CookieFilePath != null)
|
||||
{
|
||||
string cookieFileContent = null;
|
||||
bool isFake = false;
|
||||
try
|
||||
{
|
||||
cookieFileContent = await System.IO.File.ReadAllTextAsync(connectionString.CookieFilePath);
|
||||
isFake = connectionString.CookieFilePath == "fake";
|
||||
connectionString.CookieFilePath = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new System.IO.FileNotFoundException("Cookie file path not found", ex);
|
||||
}
|
||||
if (serviceType == ExternalServiceTypes.RTL)
|
||||
{
|
||||
connectionString.AccessKey = cookieFileContent;
|
||||
}
|
||||
else if (serviceType == ExternalServiceTypes.Spark)
|
||||
{
|
||||
var cookie = (isFake ? "fake:fake:fake" // Hacks for testing
|
||||
: cookieFileContent).Split(':');
|
||||
if (cookie.Length >= 3)
|
||||
{
|
||||
connectionString.AccessKey = cookie[2];
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new FormatException("Invalid cookiefile format");
|
||||
}
|
||||
}
|
||||
else if (serviceType == ExternalServiceTypes.Charge)
|
||||
{
|
||||
connectionString.APIToken = isFake ? "fake" : cookieFileContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
return connectionString;
|
||||
}
|
||||
|
||||
private Uri ToRelative(Uri absoluteUrlBase, string path)
|
||||
{
|
||||
if (path.StartsWith('/'))
|
||||
path = path.Substring(1);
|
||||
return new Uri($"{absoluteUrlBase.AbsoluteUri.WithTrailingSlash()}{path}", UriKind.Absolute);
|
||||
}
|
||||
|
||||
public ExternalConnectionString Clone()
|
||||
{
|
||||
return new ExternalConnectionString()
|
||||
{
|
||||
MacaroonFilePath = MacaroonFilePath,
|
||||
CertificateThumbprint = CertificateThumbprint,
|
||||
Macaroon = Macaroon,
|
||||
MacaroonDirectoryPath = MacaroonDirectoryPath,
|
||||
Server = Server,
|
||||
APIToken = APIToken,
|
||||
CookieFilePath = CookieFilePath,
|
||||
AccessKey = AccessKey,
|
||||
Macaroons = Macaroons?.Clone()
|
||||
};
|
||||
}
|
||||
public static bool TryParse(string str, out ExternalConnectionString result, out string error)
|
||||
{
|
||||
if (str == null)
|
||||
throw new ArgumentNullException(nameof(str));
|
||||
error = null;
|
||||
result = null;
|
||||
var resultTemp = new ExternalConnectionString();
|
||||
foreach(var kv in str.Split(';')
|
||||
.Select(part => part.Split('='))
|
||||
.Where(kv => kv.Length == 2))
|
||||
{
|
||||
switch (kv[0].ToLowerInvariant())
|
||||
{
|
||||
case "server":
|
||||
if (resultTemp.Server != null)
|
||||
{
|
||||
error = "Duplicated server attribute";
|
||||
return false;
|
||||
}
|
||||
if (!Uri.IsWellFormedUriString(kv[1], UriKind.RelativeOrAbsolute))
|
||||
{
|
||||
error = "Invalid URI";
|
||||
return false;
|
||||
}
|
||||
resultTemp.Server = new Uri(kv[1], UriKind.RelativeOrAbsolute);
|
||||
if (!resultTemp.Server.IsAbsoluteUri && (kv[1].Length == 0 || kv[1][0] != '/'))
|
||||
resultTemp.Server = new Uri($"/{kv[1]}", UriKind.RelativeOrAbsolute);
|
||||
break;
|
||||
case "cookiefile":
|
||||
case "cookiefilepath":
|
||||
if (resultTemp.CookieFilePath != null)
|
||||
{
|
||||
error = "Duplicated cookiefile attribute";
|
||||
return false;
|
||||
}
|
||||
|
||||
resultTemp.CookieFilePath = kv[1];
|
||||
break;
|
||||
case "macaroondirectorypath":
|
||||
resultTemp.MacaroonDirectoryPath = kv[1];
|
||||
break;
|
||||
case "certthumbprint":
|
||||
resultTemp.CertificateThumbprint = kv[1];
|
||||
break;
|
||||
case "macaroonfilepath":
|
||||
resultTemp.MacaroonFilePath = kv[1];
|
||||
break;
|
||||
case "api-token":
|
||||
resultTemp.APIToken = kv[1];
|
||||
break;
|
||||
case "access-key":
|
||||
resultTemp.AccessKey = kv[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
result = resultTemp;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
81
BTCPayServer/Configuration/ExternalService.cs
Normal file
81
BTCPayServer/Configuration/ExternalService.cs
Normal file
@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Lightning;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
public class ExternalServices : List<ExternalService>
|
||||
{
|
||||
public void Load(string cryptoCode, IConfiguration configuration)
|
||||
{
|
||||
Load(configuration, cryptoCode, "lndgrpc", ExternalServiceTypes.LNDGRPC, "Invalid setting {0}, " + Environment.NewLine +
|
||||
"lnd server: 'server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
"lnd server: 'server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
"lnd server: 'server=https://lnd.example.com;macaroondirectorypath=/root/.lnd;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
"Error: {1}",
|
||||
"LND (gRPC server)");
|
||||
Load(configuration, cryptoCode, "lndrest", ExternalServiceTypes.LNDRest, "Invalid setting {0}, " + Environment.NewLine +
|
||||
"lnd server: 'server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
"lnd server: 'server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
"lnd server: 'server=https://lnd.example.com;macaroondirectorypath=/root/.lnd;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
"Error: {1}",
|
||||
"LND (REST server)");
|
||||
Load(configuration, cryptoCode, "spark", ExternalServiceTypes.Spark, "Invalid setting {0}, " + Environment.NewLine +
|
||||
$"Valid example: 'server=https://btcpay.example.com/spark/btc/;cookiefile=/etc/clightning_bitcoin_spark/.cookie'" + Environment.NewLine +
|
||||
"Error: {1}",
|
||||
"C-Lightning (Spark server)");
|
||||
Load(configuration, cryptoCode, "rtl", ExternalServiceTypes.RTL, "Invalid setting {0}, " + Environment.NewLine +
|
||||
$"Valid example: 'server=https://btcpay.example.com/rtl/btc/;cookiefile=/etc/clightning_bitcoin_rtl/.cookie'" + Environment.NewLine +
|
||||
"Error: {1}",
|
||||
"LND (Ride the Lightning server)");
|
||||
Load(configuration, cryptoCode, "charge", ExternalServiceTypes.Charge, "Invalid setting {0}, " + Environment.NewLine +
|
||||
$"lightning charge server: 'type=charge;server=https://charge.example.com;api-token=2abdf302...'" + Environment.NewLine +
|
||||
$"lightning charge server: 'type=charge;server=https://charge.example.com;cookiefilepath=/root/.charge/.cookie'" + Environment.NewLine +
|
||||
"Error: {1}",
|
||||
"C-Lightning (Charge server)");
|
||||
}
|
||||
|
||||
void Load(IConfiguration configuration, string cryptoCode, string serviceName, ExternalServiceTypes type, string errorMessage, string displayName)
|
||||
{
|
||||
var setting = $"{cryptoCode}.external.{serviceName}";
|
||||
var connStr = configuration.GetOrDefault<string>(setting, string.Empty);
|
||||
if (connStr.Length != 0)
|
||||
{
|
||||
if (!ExternalConnectionString.TryParse(connStr, out var connectionString, out var error))
|
||||
{
|
||||
throw new ConfigException(string.Format(CultureInfo.InvariantCulture, errorMessage, setting, error));
|
||||
}
|
||||
this.Add(new ExternalService() { Type = type, ConnectionString = connectionString, CryptoCode = cryptoCode, DisplayName = displayName, ServiceName = serviceName });
|
||||
}
|
||||
}
|
||||
|
||||
public ExternalService GetService(string serviceName, string cryptoCode)
|
||||
{
|
||||
return this.FirstOrDefault(o => o.CryptoCode.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase) &&
|
||||
o.ServiceName.Equals(serviceName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
|
||||
public class ExternalService
|
||||
{
|
||||
public string DisplayName { get; set; }
|
||||
public ExternalServiceTypes Type { get; set; }
|
||||
public ExternalConnectionString ConnectionString { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
public string ServiceName { get; set; }
|
||||
}
|
||||
|
||||
public enum ExternalServiceTypes
|
||||
{
|
||||
LNDRest,
|
||||
LNDGRPC,
|
||||
Spark,
|
||||
RTL,
|
||||
Charge,
|
||||
P2P
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
public class SparkConnectionString
|
||||
{
|
||||
public Uri Server { get; private set; }
|
||||
public string CookeFile { get; private set; }
|
||||
|
||||
public static bool TryParse(string str, out SparkConnectionString result)
|
||||
{
|
||||
if (str == null)
|
||||
throw new ArgumentNullException(nameof(str));
|
||||
|
||||
result = null;
|
||||
var resultTemp = new SparkConnectionString();
|
||||
foreach(var kv in str.Split(';')
|
||||
.Select(part => part.Split('='))
|
||||
.Where(kv => kv.Length == 2))
|
||||
{
|
||||
switch (kv[0].ToLowerInvariant())
|
||||
{
|
||||
case "server":
|
||||
if (resultTemp.Server != null)
|
||||
return false;
|
||||
if (!Uri.IsWellFormedUriString(kv[1], UriKind.Absolute))
|
||||
return false;
|
||||
resultTemp.Server = new Uri(kv[1], UriKind.Absolute);
|
||||
break;
|
||||
case "cookiefile":
|
||||
case "cookiefilepath":
|
||||
if (resultTemp.CookeFile != null)
|
||||
return false;
|
||||
resultTemp.CookeFile = kv[1];
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
result = resultTemp;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@ -35,20 +35,19 @@ namespace BTCPayServer.Controllers
|
||||
[AllowAnonymous]
|
||||
public async Task<DataWrapper<List<PairingCodeResponse>>> Tokens([FromBody] TokenRequest request)
|
||||
{
|
||||
if (request == null)
|
||||
throw new BitpayHttpException(400, "The request body is missing");
|
||||
PairingCodeEntity pairingEntity = null;
|
||||
if (string.IsNullOrEmpty(request.PairingCode))
|
||||
{
|
||||
if (string.IsNullOrEmpty(request.Id) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(request.Id))
|
||||
throw new BitpayHttpException(400, "'id' property is required");
|
||||
if (string.IsNullOrEmpty(request.Facade))
|
||||
throw new BitpayHttpException(400, "'facade' property is required");
|
||||
|
||||
var pairingCode = await _TokenRepository.CreatePairingCodeAsync();
|
||||
await _TokenRepository.PairWithSINAsync(pairingCode, request.Id);
|
||||
pairingEntity = await _TokenRepository.UpdatePairingCode(new PairingCodeEntity()
|
||||
{
|
||||
Id = pairingCode,
|
||||
Facade = request.Facade,
|
||||
Label = request.Label
|
||||
});
|
||||
|
||||
@ -84,7 +83,7 @@ namespace BTCPayServer.Controllers
|
||||
PairingCode = pairingEntity.Id,
|
||||
PairingExpiration = pairingEntity.Expiration,
|
||||
DateCreated = pairingEntity.CreatedTime,
|
||||
Facade = pairingEntity.Facade,
|
||||
Facade = "merchant",
|
||||
Token = pairingEntity.TokenValue,
|
||||
Label = pairingEntity.Label
|
||||
}
|
||||
|
@ -18,6 +18,9 @@ using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Security;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Services.U2F;
|
||||
using BTCPayServer.Services.U2F.Models;
|
||||
using Newtonsoft.Json;
|
||||
using NicolasDorier.RateLimits;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
@ -33,6 +36,8 @@ namespace BTCPayServer.Controllers
|
||||
RoleManager<IdentityRole> _RoleManager;
|
||||
SettingsRepository _SettingsRepository;
|
||||
Configuration.BTCPayServerOptions _Options;
|
||||
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
|
||||
private readonly U2FService _u2FService;
|
||||
ILogger _logger;
|
||||
|
||||
public AccountController(
|
||||
@ -42,7 +47,9 @@ namespace BTCPayServer.Controllers
|
||||
SignInManager<ApplicationUser> signInManager,
|
||||
EmailSenderFactory emailSenderFactory,
|
||||
SettingsRepository settingsRepository,
|
||||
Configuration.BTCPayServerOptions options)
|
||||
Configuration.BTCPayServerOptions options,
|
||||
BTCPayServerEnvironment btcPayServerEnvironment,
|
||||
U2FService u2FService)
|
||||
{
|
||||
this.storeRepository = storeRepository;
|
||||
_userManager = userManager;
|
||||
@ -51,6 +58,8 @@ namespace BTCPayServer.Controllers
|
||||
_RoleManager = roleManager;
|
||||
_SettingsRepository = settingsRepository;
|
||||
_Options = options;
|
||||
_btcPayServerEnvironment = btcPayServerEnvironment;
|
||||
_u2FService = u2FService;
|
||||
_logger = Logs.PayServer;
|
||||
}
|
||||
|
||||
@ -91,8 +100,44 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
// This doesn't count login failures towards account lockout
|
||||
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
|
||||
return View(model);
|
||||
}
|
||||
|
||||
if (!await _userManager.IsLockedOutAsync(user) && await _u2FService.HasDevices(user.Id))
|
||||
{
|
||||
if (await _userManager.CheckPasswordAsync(user, model.Password))
|
||||
{
|
||||
LoginWith2faViewModel twoFModel = null;
|
||||
|
||||
if (user.TwoFactorEnabled)
|
||||
{
|
||||
// we need to do an actual sign in attempt so that 2fa can function in next step
|
||||
await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: true);
|
||||
twoFModel = new LoginWith2faViewModel
|
||||
{
|
||||
RememberMe = model.RememberMe
|
||||
};
|
||||
}
|
||||
|
||||
return View("SecondaryLogin", new SecondaryLoginViewModel()
|
||||
{
|
||||
LoginWith2FaViewModel = twoFModel,
|
||||
LoginWithU2FViewModel = await BuildU2FViewModel(model.RememberMe, user)
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
var incrementAccessFailedResult = await _userManager.AccessFailedAsync(user);
|
||||
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
|
||||
return View(model);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: true);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
@ -101,10 +146,12 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
if (result.RequiresTwoFactor)
|
||||
{
|
||||
return RedirectToAction(nameof(LoginWith2fa), new
|
||||
return View("SecondaryLogin", new SecondaryLoginViewModel()
|
||||
{
|
||||
returnUrl,
|
||||
model.RememberMe
|
||||
LoginWith2FaViewModel = new LoginWith2faViewModel()
|
||||
{
|
||||
RememberMe = model.RememberMe
|
||||
}
|
||||
});
|
||||
}
|
||||
if (result.IsLockedOut)
|
||||
@ -123,6 +170,71 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private async Task<LoginWithU2FViewModel> BuildU2FViewModel(bool rememberMe, ApplicationUser user)
|
||||
{
|
||||
if (_btcPayServerEnvironment.IsSecure)
|
||||
{
|
||||
var u2fChallenge = await _u2FService.GenerateDeviceChallenges(user.Id,
|
||||
Request.GetAbsoluteUriNoPathBase().ToString().TrimEnd('/'));
|
||||
|
||||
return new LoginWithU2FViewModel()
|
||||
{
|
||||
Version = u2fChallenge[0].version,
|
||||
Challenge = u2fChallenge[0].challenge,
|
||||
Challenges = JsonConvert.SerializeObject(u2fChallenge),
|
||||
AppId = u2fChallenge[0].appId,
|
||||
UserId = user.Id,
|
||||
RememberMe = rememberMe
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> LoginWithU2F(LoginWithU2FViewModel viewModel, string returnUrl = null)
|
||||
{
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
var user = await _userManager.FindByIdAsync(viewModel.UserId);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var errorMessage = string.Empty;
|
||||
try
|
||||
{
|
||||
if (await _u2FService.AuthenticateUser(viewModel.UserId, viewModel.DeviceResponse))
|
||||
{
|
||||
await _signInManager.SignInAsync(user, viewModel.RememberMe, "U2F");
|
||||
_logger.LogInformation("User logged in.");
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
|
||||
errorMessage = "Invalid login attempt.";
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
||||
errorMessage = e.Message;
|
||||
}
|
||||
|
||||
ModelState.AddModelError(string.Empty, errorMessage);
|
||||
return View("SecondaryLogin", new SecondaryLoginViewModel()
|
||||
{
|
||||
LoginWithU2FViewModel = viewModel,
|
||||
LoginWith2FaViewModel = !user.TwoFactorEnabled
|
||||
? null
|
||||
: new LoginWith2faViewModel()
|
||||
{
|
||||
RememberMe = viewModel.RememberMe
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> LoginWith2fa(bool rememberMe, string returnUrl = null)
|
||||
@ -135,10 +247,13 @@ namespace BTCPayServer.Controllers
|
||||
throw new ApplicationException($"Unable to load two-factor authentication user.");
|
||||
}
|
||||
|
||||
var model = new LoginWith2faViewModel { RememberMe = rememberMe };
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
|
||||
return View(model);
|
||||
return View("SecondaryLogin", new SecondaryLoginViewModel()
|
||||
{
|
||||
LoginWith2FaViewModel = new LoginWith2faViewModel { RememberMe = rememberMe },
|
||||
LoginWithU2FViewModel = (await _u2FService.HasDevices(user.Id))? await BuildU2FViewModel(rememberMe, user): null
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@ -175,7 +290,11 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
_logger.LogWarning("Invalid authenticator code entered for user with ID {UserId}.", user.Id);
|
||||
ModelState.AddModelError(string.Empty, "Invalid authenticator code.");
|
||||
return View();
|
||||
return View("SecondaryLogin", new SecondaryLoginViewModel()
|
||||
{
|
||||
LoginWith2FaViewModel = model,
|
||||
LoginWithU2FViewModel = (await _u2FService.HasDevices(user.Id))? await BuildU2FViewModel(rememberMe, user): null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,11 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
@ -32,6 +34,7 @@ namespace BTCPayServer.Controllers
|
||||
var settings = app.GetSettings<CrowdfundSettings>();
|
||||
var vm = new UpdateCrowdfundViewModel()
|
||||
{
|
||||
NotificationEmailWarning = !await IsEmailConfigured(app.StoreDataId),
|
||||
Title = settings.Title,
|
||||
Enabled = settings.Enabled,
|
||||
EnforceTargetAmount = settings.EnforceTargetAmount,
|
||||
@ -56,7 +59,9 @@ namespace BTCPayServer.Controllers
|
||||
AppId = appId,
|
||||
SearchTerm = app.TagAllInvoices ? $"storeid:{app.StoreDataId}" : $"orderid:{AppService.GetCrowdfundOrderId(appId)}",
|
||||
DisplayPerksRanking = settings.DisplayPerksRanking,
|
||||
SortPerksByPopularity = settings.SortPerksByPopularity
|
||||
SortPerksByPopularity = settings.SortPerksByPopularity,
|
||||
Sounds = string.Join(Environment.NewLine, settings.Sounds),
|
||||
AnimationColors = string.Join(Environment.NewLine, settings.AnimationColors)
|
||||
};
|
||||
return View(vm);
|
||||
}
|
||||
@ -90,6 +95,24 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DisplayPerksRanking), "You must sort by popularity in order to display ranking.");
|
||||
}
|
||||
|
||||
var parsedSounds = vm.Sounds.Split(
|
||||
new[] {"\r\n", "\r", "\n"},
|
||||
StringSplitOptions.None
|
||||
).Select(s => s.Trim()).ToArray();
|
||||
if (vm.SoundsEnabled && !parsedSounds.Any())
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Sounds), "You must have at least one sound if you enable sounds");
|
||||
}
|
||||
|
||||
var parsedAnimationColors = vm.AnimationColors.Split(
|
||||
new[] { "\r\n", "\r", "\n" },
|
||||
StringSplitOptions.None
|
||||
).Select(s => s.Trim()).ToArray();
|
||||
if (vm.AnimationsEnabled && !parsedAnimationColors.Any())
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.AnimationColors), "You must have at least one animation color if you enable animations");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
@ -115,6 +138,7 @@ namespace BTCPayServer.Controllers
|
||||
MainImageUrl = vm.MainImageUrl,
|
||||
EmbeddedCSS = vm.EmbeddedCSS,
|
||||
NotificationUrl = vm.NotificationUrl,
|
||||
NotificationEmail = vm.NotificationEmail,
|
||||
Tagline = vm.Tagline,
|
||||
PerksTemplate = vm.PerksTemplate,
|
||||
DisqusEnabled = vm.DisqusEnabled,
|
||||
@ -124,7 +148,9 @@ namespace BTCPayServer.Controllers
|
||||
ResetEveryAmount = vm.ResetEveryAmount,
|
||||
ResetEvery = Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery),
|
||||
DisplayPerksRanking = vm.DisplayPerksRanking,
|
||||
SortPerksByPopularity = vm.SortPerksByPopularity
|
||||
SortPerksByPopularity = vm.SortPerksByPopularity,
|
||||
Sounds = parsedSounds,
|
||||
AnimationColors = parsedAnimationColors
|
||||
};
|
||||
|
||||
app.TagAllInvoices = vm.UseAllStoreInvoices;
|
||||
|
@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
@ -55,12 +56,16 @@ namespace BTCPayServer.Controllers
|
||||
" custom: true";
|
||||
EnableShoppingCart = false;
|
||||
ShowCustomAmount = true;
|
||||
ShowDiscount = true;
|
||||
EnableTips = true;
|
||||
}
|
||||
public string Title { get; set; }
|
||||
public string Currency { get; set; }
|
||||
public string Template { get; set; }
|
||||
public bool EnableShoppingCart { get; set; }
|
||||
public bool ShowCustomAmount { get; set; }
|
||||
public bool ShowDiscount { get; set; }
|
||||
public bool EnableTips { get; set; }
|
||||
|
||||
public const string BUTTON_TEXT_DEF = "Buy for {0}";
|
||||
public string ButtonText { get; set; } = BUTTON_TEXT_DEF;
|
||||
@ -73,6 +78,9 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
|
||||
public string CustomCSSLink { get; set; }
|
||||
public string NotificationEmail { get; set; }
|
||||
public string NotificationUrl { get; set; }
|
||||
public bool? RedirectAutomatically { get; set; }
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -83,19 +91,26 @@ namespace BTCPayServer.Controllers
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
|
||||
var vm = new UpdatePointOfSaleViewModel()
|
||||
{
|
||||
NotificationEmailWarning = !await IsEmailConfigured(app.StoreDataId),
|
||||
Id = appId,
|
||||
Title = settings.Title,
|
||||
EnableShoppingCart = settings.EnableShoppingCart,
|
||||
ShowCustomAmount = settings.ShowCustomAmount,
|
||||
ShowDiscount = settings.ShowDiscount,
|
||||
EnableTips = settings.EnableTips,
|
||||
Currency = settings.Currency,
|
||||
Template = settings.Template,
|
||||
ButtonText = settings.ButtonText ?? PointOfSaleSettings.BUTTON_TEXT_DEF,
|
||||
CustomButtonText = settings.CustomButtonText ?? PointOfSaleSettings.CUSTOM_BUTTON_TEXT_DEF,
|
||||
CustomTipText = settings.CustomTipText ?? PointOfSaleSettings.CUSTOM_TIP_TEXT_DEF,
|
||||
CustomTipPercentages = settings.CustomTipPercentages != null ? string.Join(",", settings.CustomTipPercentages) : string.Join(",", PointOfSaleSettings.CUSTOM_TIP_PERCENTAGES_DEF),
|
||||
CustomCSSLink = settings.CustomCSSLink
|
||||
CustomCSSLink = settings.CustomCSSLink,
|
||||
NotificationEmail = settings.NotificationEmail,
|
||||
NotificationUrl = settings.NotificationUrl,
|
||||
RedirectAutomatically = settings.RedirectAutomatically.HasValue? settings.RedirectAutomatically.Value? "true": "false" : ""
|
||||
};
|
||||
if (HttpContext?.Request != null)
|
||||
{
|
||||
@ -160,13 +175,19 @@ namespace BTCPayServer.Controllers
|
||||
Title = vm.Title,
|
||||
EnableShoppingCart = vm.EnableShoppingCart,
|
||||
ShowCustomAmount = vm.ShowCustomAmount,
|
||||
ShowDiscount = vm.ShowDiscount,
|
||||
EnableTips = vm.EnableTips,
|
||||
Currency = vm.Currency.ToUpperInvariant(),
|
||||
Template = vm.Template,
|
||||
ButtonText = vm.ButtonText,
|
||||
CustomButtonText = vm.CustomButtonText,
|
||||
CustomTipText = vm.CustomTipText,
|
||||
CustomTipPercentages = ListSplit(vm.CustomTipPercentages),
|
||||
CustomCSSLink = vm.CustomCSSLink
|
||||
CustomCSSLink = vm.CustomCSSLink,
|
||||
NotificationUrl = vm.NotificationUrl,
|
||||
NotificationEmail = vm.NotificationEmail,
|
||||
RedirectAutomatically = string.IsNullOrEmpty(vm.RedirectAutomatically)? (bool?) null: bool.Parse(vm.RedirectAutomatically)
|
||||
|
||||
});
|
||||
await UpdateAppSettings(app);
|
||||
StatusMessage = "App updated";
|
||||
|
@ -7,6 +7,7 @@ using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Ganss.XSS;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -30,6 +31,7 @@ namespace BTCPayServer.Controllers
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
CurrencyNameTable currencies,
|
||||
HtmlSanitizer htmlSanitizer,
|
||||
EmailSenderFactory emailSenderFactory,
|
||||
AppService AppService)
|
||||
{
|
||||
_UserManager = userManager;
|
||||
@ -38,6 +40,7 @@ namespace BTCPayServer.Controllers
|
||||
_NetworkProvider = networkProvider;
|
||||
_currencies = currencies;
|
||||
_htmlSanitizer = htmlSanitizer;
|
||||
_emailSenderFactory = emailSenderFactory;
|
||||
_AppService = AppService;
|
||||
}
|
||||
|
||||
@ -47,6 +50,7 @@ namespace BTCPayServer.Controllers
|
||||
private BTCPayNetworkProvider _NetworkProvider;
|
||||
private readonly CurrencyNameTable _currencies;
|
||||
private readonly HtmlSanitizer _htmlSanitizer;
|
||||
private readonly EmailSenderFactory _emailSenderFactory;
|
||||
private AppService _AppService;
|
||||
|
||||
[TempData]
|
||||
@ -84,7 +88,7 @@ namespace BTCPayServer.Controllers
|
||||
StatusMessage = new StatusMessageModel()
|
||||
{
|
||||
Html =
|
||||
$"Error: You must have created at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}'>Create store</a>",
|
||||
$"Error: You need to create at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}'>Create store</a>",
|
||||
Severity = StatusMessageModel.StatusSeverity.Error
|
||||
}.ToString();
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
@ -104,7 +108,7 @@ namespace BTCPayServer.Controllers
|
||||
StatusMessage = new StatusMessageModel()
|
||||
{
|
||||
Html =
|
||||
$"Error: You must have created at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}'>Create store</a>",
|
||||
$"Error: You need to create at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}'>Create store</a>",
|
||||
Severity = StatusMessageModel.StatusSeverity.Error
|
||||
}.ToString();
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
@ -176,5 +180,10 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
return _UserManager.GetUserId(User);
|
||||
}
|
||||
|
||||
private async Task<bool> IsEmailConfigured(string storeId)
|
||||
{
|
||||
return (await (_emailSenderFactory.GetEmailSender(storeId) as EmailSender)?.GetEmailSettings())?.IsComplete() is true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,9 +5,12 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.ModelBinders;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
@ -30,16 +33,19 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class AppsPublicController : Controller
|
||||
{
|
||||
public AppsPublicController(AppService AppService,
|
||||
public AppsPublicController(AppService AppService,
|
||||
BTCPayServerOptions btcPayServerOptions,
|
||||
InvoiceController invoiceController,
|
||||
UserManager<ApplicationUser> userManager)
|
||||
{
|
||||
_AppService = AppService;
|
||||
_BtcPayServerOptions = btcPayServerOptions;
|
||||
_InvoiceController = invoiceController;
|
||||
_UserManager = userManager;
|
||||
}
|
||||
|
||||
private AppService _AppService;
|
||||
private readonly BTCPayServerOptions _BtcPayServerOptions;
|
||||
private InvoiceController _InvoiceController;
|
||||
private readonly UserManager<ApplicationUser> _UserManager;
|
||||
|
||||
@ -62,6 +68,8 @@ namespace BTCPayServer.Controllers
|
||||
Step = step.ToString(CultureInfo.InvariantCulture),
|
||||
EnableShoppingCart = settings.EnableShoppingCart,
|
||||
ShowCustomAmount = settings.ShowCustomAmount,
|
||||
ShowDiscount = settings.ShowDiscount,
|
||||
EnableTips = settings.EnableTips,
|
||||
CurrencyCode = settings.Currency,
|
||||
CurrencySymbol = numberFormatInfo.CurrencySymbol,
|
||||
CurrencyInfo = new ViewPointOfSaleViewModel.CurrencyInfoData()
|
||||
@ -82,35 +90,37 @@ namespace BTCPayServer.Controllers
|
||||
AppId = appId
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
[HttpGet]
|
||||
[Route("/apps/{appId}/crowdfund")]
|
||||
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
|
||||
public async Task<IActionResult> ViewCrowdfund(string appId, string statusMessage)
|
||||
|
||||
{
|
||||
var app = await _AppService.GetApp(appId, AppType.Crowdfund, true);
|
||||
|
||||
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<CrowdfundSettings>();
|
||||
|
||||
|
||||
var isAdmin = await _AppService.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
|
||||
|
||||
var hasEnoughSettingsToLoad = !string.IsNullOrEmpty(settings.TargetCurrency );
|
||||
|
||||
var hasEnoughSettingsToLoad = !string.IsNullOrEmpty(settings.TargetCurrency);
|
||||
if (!hasEnoughSettingsToLoad)
|
||||
{
|
||||
if(!isAdmin)
|
||||
if (!isAdmin)
|
||||
return NotFound();
|
||||
|
||||
return NotFound("A Target Currency must be set for this app in order to be loadable.");
|
||||
}
|
||||
if (settings.Enabled) return View(await _AppService.GetAppInfo(appId));
|
||||
if(!isAdmin)
|
||||
var appInfo = (ViewCrowdfundViewModel)(await _AppService.GetAppInfo(appId));
|
||||
appInfo.HubPath = AppHub.GetHubPath(this.Request);
|
||||
if (settings.Enabled)
|
||||
return View(appInfo);
|
||||
if (!isAdmin)
|
||||
return NotFound();
|
||||
|
||||
return View(await _AppService.GetAppInfo(appId));
|
||||
return View(appInfo);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@ -118,7 +128,7 @@ namespace BTCPayServer.Controllers
|
||||
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
|
||||
[IgnoreAntiforgeryToken]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public async Task<IActionResult> ContributeToCrowdfund(string appId, ContributeToCrowdfund request)
|
||||
public async Task<IActionResult> ContributeToCrowdfund(string appId, ContributeToCrowdfund request, CancellationToken cancellationToken)
|
||||
{
|
||||
var app = await _AppService.GetApp(appId, AppType.Crowdfund, true);
|
||||
|
||||
@ -129,14 +139,13 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var isAdmin = await _AppService.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
|
||||
|
||||
if (!settings.Enabled)
|
||||
if (!settings.Enabled && !isAdmin)
|
||||
{
|
||||
if (!isAdmin)
|
||||
return NotFound("Crowdfund is not currently active");
|
||||
return NotFound("Crowdfund is not currently active");
|
||||
}
|
||||
|
||||
var info = (ViewCrowdfundViewModel)await _AppService.GetAppInfo(appId);
|
||||
|
||||
info.HubPath = AppHub.GetHubPath(this.Request);
|
||||
if (!isAdmin &&
|
||||
((settings.StartDate.HasValue && DateTime.Now < settings.StartDate) ||
|
||||
(settings.EndDate.HasValue && DateTime.Now > settings.EndDate) ||
|
||||
@ -173,22 +182,26 @@ namespace BTCPayServer.Controllers
|
||||
try
|
||||
{
|
||||
var invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
|
||||
{
|
||||
OrderId = AppService.GetCrowdfundOrderId(appId),
|
||||
Currency = settings.TargetCurrency,
|
||||
ItemCode = request.ChoiceKey ?? string.Empty,
|
||||
ItemDesc = title,
|
||||
BuyerEmail = request.Email,
|
||||
Price = price,
|
||||
NotificationURL = settings.NotificationUrl,
|
||||
FullNotifications = true,
|
||||
ExtendedNotifications = true,
|
||||
RedirectURL = request.RedirectUrl ?? Request.GetDisplayUrl()
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot(), new List<string> { AppService.GetAppInternalTag(appId) });
|
||||
{
|
||||
OrderId = AppService.GetCrowdfundOrderId(appId),
|
||||
Currency = settings.TargetCurrency,
|
||||
ItemCode = request.ChoiceKey ?? string.Empty,
|
||||
ItemDesc = title,
|
||||
BuyerEmail = request.Email,
|
||||
Price = price,
|
||||
NotificationURL = settings.NotificationUrl,
|
||||
NotificationEmail = settings.NotificationEmail,
|
||||
FullNotifications = true,
|
||||
ExtendedNotifications = true,
|
||||
RedirectURL = request.RedirectUrl ??
|
||||
new Uri(new Uri( new Uri(HttpContext.Request.GetAbsoluteRoot()), _BtcPayServerOptions.RootPath), $"apps/{appId}/crowdfund").ToString()
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot(),
|
||||
new List<string> {AppService.GetAppInternalTag(appId)},
|
||||
cancellationToken: cancellationToken);
|
||||
if (request.RedirectToCheckout)
|
||||
{
|
||||
return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice",
|
||||
new {invoiceId = invoice.Data.Id});
|
||||
new { invoiceId = invoice.Data.Id });
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -199,7 +212,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
return BadRequest(e.Message);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@ -208,13 +221,13 @@ namespace BTCPayServer.Controllers
|
||||
[IgnoreAntiforgeryToken]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public async Task<IActionResult> ViewPointOfSale(string appId,
|
||||
decimal amount,
|
||||
[ModelBinder(typeof(InvariantDecimalModelBinder))] decimal amount,
|
||||
string email,
|
||||
string orderId,
|
||||
string notificationUrl,
|
||||
string redirectUrl,
|
||||
string choiceKey,
|
||||
string posData = null)
|
||||
string posData = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var app = await _AppService.GetApp(appId, AppType.PointOfSale);
|
||||
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
|
||||
@ -259,15 +272,21 @@ namespace BTCPayServer.Controllers
|
||||
Price = price,
|
||||
BuyerEmail = email,
|
||||
OrderId = orderId,
|
||||
NotificationURL = notificationUrl,
|
||||
RedirectURL = redirectUrl ?? Request.GetDisplayUrl(),
|
||||
NotificationURL =
|
||||
string.IsNullOrEmpty(notificationUrl) ? settings.NotificationUrl : notificationUrl,
|
||||
NotificationEmail = settings.NotificationEmail,
|
||||
RedirectURL = redirectUrl ?? Request.GetDisplayUrl(),
|
||||
FullNotifications = true,
|
||||
PosData = string.IsNullOrEmpty(posData) ? null : posData
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot());
|
||||
ExtendedNotifications = true,
|
||||
PosData = string.IsNullOrEmpty(posData) ? null : posData,
|
||||
RedirectAutomatically = settings.RedirectAutomatically,
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot(),
|
||||
new List<string>() { AppService.GetAppInternalTag(appId) },
|
||||
cancellationToken);
|
||||
return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice", new { invoiceId = invoice.Data.Id });
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private string GetUserId()
|
||||
{
|
||||
return _UserManager.GetUserId(User);
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
@ -48,7 +49,7 @@ namespace BTCPayServer.Controllers
|
||||
[HttpGet]
|
||||
[Route("calculate")]
|
||||
public async Task<IActionResult> CalculateAmount(string storeId, string fromCurrency, string toCurrency,
|
||||
decimal toCurrencyAmount)
|
||||
decimal toCurrencyAmount, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -57,7 +58,7 @@ namespace BTCPayServer.Controllers
|
||||
if (fromCurrency.Equals("usd", StringComparison.InvariantCultureIgnoreCase)
|
||||
|| fromCurrency.Equals("eur", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return await HandleCalculateFiatAmount(fromCurrency, toCurrency, toCurrencyAmount);
|
||||
return await HandleCalculateFiatAmount(fromCurrency, toCurrency, toCurrencyAmount, cancellationToken);
|
||||
}
|
||||
|
||||
var callCounter = 0;
|
||||
@ -102,11 +103,11 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
private async Task<IActionResult> HandleCalculateFiatAmount(string fromCurrency, string toCurrency,
|
||||
decimal toCurrencyAmount)
|
||||
decimal toCurrencyAmount, CancellationToken cancellationToken)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
var rules = store.GetStoreBlob().GetRateRules(_btcPayNetworkProvider);
|
||||
var rate = await _RateProviderFactory.FetchRate(new CurrencyPair(toCurrency, fromCurrency), rules);
|
||||
var rate = await _RateProviderFactory.FetchRate(new CurrencyPair(toCurrency, fromCurrency), rules, cancellationToken);
|
||||
if (rate.BidAsk == null) return BadRequest();
|
||||
var flatRate = rate.BidAsk.Center;
|
||||
return Ok(flatRate * toCurrencyAmount);
|
||||
|
@ -1,29 +1,46 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using BTCPayServer.Models;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitcoin.Payment;
|
||||
using System.Net.Http;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.HostedServices;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class HomeController : Controller
|
||||
{
|
||||
private readonly CssThemeManager _cachedServerSettings;
|
||||
|
||||
public IHttpClientFactory HttpClientFactory { get; }
|
||||
|
||||
public HomeController(IHttpClientFactory httpClientFactory)
|
||||
public HomeController(IHttpClientFactory httpClientFactory, CssThemeManager cachedServerSettings)
|
||||
{
|
||||
HttpClientFactory = httpClientFactory;
|
||||
_cachedServerSettings = cachedServerSettings;
|
||||
}
|
||||
public IActionResult Index()
|
||||
|
||||
public async Task<IActionResult> Index()
|
||||
{
|
||||
if (_cachedServerSettings.RootAppType is Services.Apps.AppType.Crowdfund)
|
||||
{
|
||||
var serviceProvider = HttpContext.RequestServices;
|
||||
var controller = (AppsPublicController)serviceProvider.GetService(typeof(AppsPublicController));
|
||||
controller.Url = Url;
|
||||
controller.ControllerContext = ControllerContext;
|
||||
var res = await controller.ViewCrowdfund(_cachedServerSettings.RootAppId, null) as ViewResult;
|
||||
if (res != null)
|
||||
{
|
||||
res.ViewName = "/Views/AppsPublic/ViewCrowdfund.cshtml";
|
||||
return res; // return
|
||||
}
|
||||
}
|
||||
|
||||
return View("Home");
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models;
|
||||
@ -32,11 +33,11 @@ namespace BTCPayServer.Controllers
|
||||
[HttpPost]
|
||||
[Route("invoices")]
|
||||
[MediaTypeConstraint("application/json")]
|
||||
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] CreateInvoiceRequest invoice)
|
||||
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] CreateInvoiceRequest invoice, CancellationToken cancellationToken)
|
||||
{
|
||||
if (invoice == null)
|
||||
throw new BitpayHttpException(400, "Invalid invoice");
|
||||
return await _InvoiceController.CreateInvoiceCore(invoice, HttpContext.GetStoreData(), HttpContext.Request.GetAbsoluteRoot());
|
||||
return await _InvoiceController.CreateInvoiceCore(invoice, HttpContext.GetStoreData(), HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
|
@ -19,6 +19,7 @@ using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Invoices.Export;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.EntityFrameworkCore.Internal;
|
||||
@ -46,9 +47,9 @@ namespace BTCPayServer.Controllers
|
||||
if (invoice == null)
|
||||
return NotFound();
|
||||
|
||||
var dto = invoice.EntityToDTO(_NetworkProvider);
|
||||
var prodInfo = invoice.ProductInformation;
|
||||
var store = await _StoreRepository.FindStore(invoice.StoreId);
|
||||
InvoiceDetailsModel model = new InvoiceDetailsModel()
|
||||
var model = new InvoiceDetailsModel()
|
||||
{
|
||||
StoreName = store.StoreName,
|
||||
StoreLink = Url.Action(nameof(StoresController.UpdateStore), "Stores", new { storeId = store.Id }),
|
||||
@ -64,20 +65,39 @@ namespace BTCPayServer.Controllers
|
||||
MonitoringDate = invoice.MonitoringExpiration,
|
||||
OrderId = invoice.OrderId,
|
||||
BuyerInformation = invoice.BuyerInformation,
|
||||
Fiat = _CurrencyNameTable.DisplayFormatCurrency(dto.Price, dto.Currency),
|
||||
TaxIncluded = _CurrencyNameTable.DisplayFormatCurrency(invoice.ProductInformation.TaxIncluded, dto.Currency),
|
||||
Fiat = _CurrencyNameTable.DisplayFormatCurrency(prodInfo.Price, prodInfo.Currency),
|
||||
TaxIncluded = _CurrencyNameTable.DisplayFormatCurrency(prodInfo.TaxIncluded, prodInfo.Currency),
|
||||
NotificationEmail = invoice.NotificationEmail,
|
||||
NotificationUrl = invoice.NotificationURL,
|
||||
RedirectUrl = invoice.RedirectURL,
|
||||
ProductInformation = invoice.ProductInformation,
|
||||
StatusException = invoice.ExceptionStatus,
|
||||
Events = invoice.Events,
|
||||
PosData = PosDataParser.ParsePosData(dto.PosData)
|
||||
PosData = PosDataParser.ParsePosData(invoice.PosData),
|
||||
StatusMessage = StatusMessage
|
||||
};
|
||||
|
||||
model.Addresses = invoice.HistoricalAddresses.Select(h => new InvoiceDetailsModel.AddressModel
|
||||
{
|
||||
Destination = h.GetAddress(),
|
||||
PaymentMethod = ToString(h.GetPaymentMethodId()),
|
||||
Current = !h.UnAssigned.HasValue
|
||||
}).ToArray();
|
||||
|
||||
var details = InvoicePopulatePayments(invoice);
|
||||
model.CryptoPayments = details.CryptoPayments;
|
||||
model.OnChainPayments = details.OnChainPayments;
|
||||
model.OffChainPayments = details.OffChainPayments;
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private InvoiceDetailsModel InvoicePopulatePayments(InvoiceEntity invoice)
|
||||
{
|
||||
var model = new InvoiceDetailsModel();
|
||||
|
||||
foreach (var data in invoice.GetPaymentMethods(null))
|
||||
{
|
||||
var cryptoInfo = dto.CryptoInfo.First(o => o.GetpaymentMethodId() == data.GetId());
|
||||
var accounting = data.Calculate();
|
||||
var paymentMethodId = data.GetId();
|
||||
var cryptoPayment = new InvoiceDetailsModel.CryptoPayment();
|
||||
@ -92,72 +112,46 @@ namespace BTCPayServer.Controllers
|
||||
cryptoPayment.Address = onchainMethod.DepositAddress;
|
||||
}
|
||||
cryptoPayment.Rate = ExchangeRate(data);
|
||||
cryptoPayment.PaymentUrl = cryptoInfo.PaymentUrls.BIP21;
|
||||
model.CryptoPayments.Add(cryptoPayment);
|
||||
}
|
||||
|
||||
var onChainPayments = invoice
|
||||
.GetPayments()
|
||||
.Select<PaymentEntity, Task<object>>(async payment =>
|
||||
foreach (var payment in invoice.GetPayments())
|
||||
{
|
||||
var paymentNetwork = _NetworkProvider.GetNetwork(payment.GetCryptoCode());
|
||||
var paymentData = payment.GetCryptoPaymentData();
|
||||
if (paymentData is Payments.Bitcoin.BitcoinLikePaymentData onChainPaymentData)
|
||||
{
|
||||
var paymentNetwork = _NetworkProvider.GetNetwork(payment.GetCryptoCode());
|
||||
var paymentData = payment.GetCryptoPaymentData();
|
||||
if (paymentData is Payments.Bitcoin.BitcoinLikePaymentData onChainPaymentData)
|
||||
var m = new InvoiceDetailsModel.Payment();
|
||||
m.Crypto = payment.GetPaymentMethodId().CryptoCode;
|
||||
m.DepositAddress = onChainPaymentData.GetDestination(paymentNetwork);
|
||||
|
||||
int confirmationCount = onChainPaymentData.ConfirmationCount;
|
||||
if (confirmationCount >= paymentNetwork.MaxTrackedConfirmation)
|
||||
{
|
||||
var m = new InvoiceDetailsModel.Payment();
|
||||
m.Crypto = payment.GetPaymentMethodId().CryptoCode;
|
||||
m.DepositAddress = onChainPaymentData.GetDestination(paymentNetwork);
|
||||
|
||||
int confirmationCount = 0;
|
||||
if ((onChainPaymentData.ConfirmationCount < paymentNetwork.MaxTrackedConfirmation && payment.Accounted)
|
||||
&& (onChainPaymentData.Legacy || invoice.MonitoringExpiration < DateTimeOffset.UtcNow)) // The confirmation count in the paymentData is not up to date
|
||||
{
|
||||
confirmationCount = (await ((ExplorerClientProvider)_ServiceProvider.GetService(typeof(ExplorerClientProvider))).GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(onChainPaymentData.Outpoint.Hash))?.Confirmations ?? 0;
|
||||
onChainPaymentData.ConfirmationCount = confirmationCount;
|
||||
payment.SetCryptoPaymentData(onChainPaymentData);
|
||||
await _InvoiceRepository.UpdatePayments(new List<PaymentEntity> { payment });
|
||||
}
|
||||
else
|
||||
{
|
||||
confirmationCount = onChainPaymentData.ConfirmationCount;
|
||||
}
|
||||
if (confirmationCount >= paymentNetwork.MaxTrackedConfirmation)
|
||||
{
|
||||
m.Confirmations = "At least " + (paymentNetwork.MaxTrackedConfirmation);
|
||||
}
|
||||
else
|
||||
{
|
||||
m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
m.TransactionId = onChainPaymentData.Outpoint.Hash.ToString();
|
||||
m.ReceivedTime = payment.ReceivedTime;
|
||||
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, paymentNetwork.BlockExplorerLink, m.TransactionId);
|
||||
m.Replaced = !payment.Accounted;
|
||||
return m;
|
||||
m.Confirmations = "At least " + (paymentNetwork.MaxTrackedConfirmation);
|
||||
}
|
||||
else
|
||||
{
|
||||
var lightningPaymentData = (Payments.Lightning.LightningLikePaymentData)paymentData;
|
||||
return new InvoiceDetailsModel.OffChainPayment()
|
||||
{
|
||||
Crypto = paymentNetwork.CryptoCode,
|
||||
BOLT11 = lightningPaymentData.BOLT11
|
||||
};
|
||||
m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
})
|
||||
.ToArray();
|
||||
await Task.WhenAll(onChainPayments);
|
||||
model.Addresses = invoice.HistoricalAddresses.Select(h => new InvoiceDetailsModel.AddressModel
|
||||
{
|
||||
Destination = h.GetAddress(),
|
||||
PaymentMethod = ToString(h.GetPaymentMethodId()),
|
||||
Current = !h.UnAssigned.HasValue
|
||||
}).ToArray();
|
||||
model.OnChainPayments = onChainPayments.Select(p => p.GetAwaiter().GetResult()).OfType<InvoiceDetailsModel.Payment>().ToList();
|
||||
model.OffChainPayments = onChainPayments.Select(p => p.GetAwaiter().GetResult()).OfType<InvoiceDetailsModel.OffChainPayment>().ToList();
|
||||
model.StatusMessage = StatusMessage;
|
||||
return View(model);
|
||||
|
||||
m.TransactionId = onChainPaymentData.Outpoint.Hash.ToString();
|
||||
m.ReceivedTime = payment.ReceivedTime;
|
||||
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, paymentNetwork.BlockExplorerLink, m.TransactionId);
|
||||
m.Replaced = !payment.Accounted;
|
||||
model.OnChainPayments.Add(m);
|
||||
}
|
||||
else
|
||||
{
|
||||
var lightningPaymentData = (LightningLikePaymentData)paymentData;
|
||||
model.OffChainPayments.Add(new InvoiceDetailsModel.OffChainPayment()
|
||||
{
|
||||
Crypto = paymentNetwork.CryptoCode,
|
||||
BOLT11 = lightningPaymentData.BOLT11
|
||||
});
|
||||
}
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
private string ToString(PaymentMethodId paymentMethodId)
|
||||
@ -188,7 +182,7 @@ namespace BTCPayServer.Controllers
|
||||
//Keep compatibility with Bitpay
|
||||
invoiceId = invoiceId ?? id;
|
||||
id = invoiceId;
|
||||
////
|
||||
//
|
||||
|
||||
var model = await GetInvoiceModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId));
|
||||
if (model == null)
|
||||
@ -213,6 +207,23 @@ namespace BTCPayServer.Controllers
|
||||
return View(nameof(Checkout), model);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoice-noscript")]
|
||||
public async Task<IActionResult> CheckoutNoScript(string invoiceId, string id = null, string paymentMethodId = null)
|
||||
{
|
||||
//Keep compatibility with Bitpay
|
||||
invoiceId = invoiceId ?? id;
|
||||
id = invoiceId;
|
||||
//
|
||||
|
||||
var model = await GetInvoiceModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId));
|
||||
if (model == null)
|
||||
return NotFound();
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
|
||||
private async Task<PaymentModel> GetInvoiceModel(string invoiceId, PaymentMethodId paymentMethodId)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
|
||||
@ -258,7 +269,7 @@ namespace BTCPayServer.Controllers
|
||||
storeBlob.ChangellySettings.IsConfigured())
|
||||
? storeBlob.ChangellySettings
|
||||
: null;
|
||||
|
||||
|
||||
CoinSwitchSettings coinswitch = (storeBlob.CoinSwitchSettings != null && storeBlob.CoinSwitchSettings.Enabled &&
|
||||
storeBlob.CoinSwitchSettings.IsConfigured())
|
||||
? storeBlob.CoinSwitchSettings
|
||||
@ -273,6 +284,7 @@ namespace BTCPayServer.Controllers
|
||||
var model = new PaymentModel()
|
||||
{
|
||||
CryptoCode = network.CryptoCode,
|
||||
RootPath = this.Request.PathBase.Value.WithTrailingSlash(),
|
||||
PaymentMethodId = paymentMethodId.ToString(),
|
||||
PaymentMethodName = GetDisplayName(paymentMethodId, network),
|
||||
CryptoImage = GetImage(paymentMethodId, network),
|
||||
@ -283,6 +295,7 @@ namespace BTCPayServer.Controllers
|
||||
HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice",
|
||||
CustomCSSLink = storeBlob.CustomCSS?.AbsoluteUri,
|
||||
CustomLogoLink = storeBlob.CustomLogo?.AbsoluteUri,
|
||||
LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi,
|
||||
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
|
||||
BtcDue = accounting.Due.ToString(),
|
||||
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
|
||||
@ -295,6 +308,7 @@ namespace BTCPayServer.Controllers
|
||||
ItemDesc = invoice.ProductInformation.ItemDesc,
|
||||
Rate = ExchangeRate(paymentMethod),
|
||||
MerchantRefLink = invoice.RedirectURL ?? "/",
|
||||
RedirectAutomatically = invoice.RedirectAutomatically,
|
||||
StoreName = store.StoreName,
|
||||
InvoiceBitcoinUrl = paymentMethodId.PaymentType == PaymentTypes.BTCLike ? cryptoInfo.PaymentUrls.BIP21 :
|
||||
paymentMethodId.PaymentType == PaymentTypes.LightningLike ? cryptoInfo.PaymentUrls.BOLT11 :
|
||||
@ -314,6 +328,7 @@ namespace BTCPayServer.Controllers
|
||||
ChangellyMerchantId = changelly?.ChangellyMerchantId,
|
||||
ChangellyAmountDue = changellyAmountDue,
|
||||
CoinSwitchEnabled = coinswitch != null,
|
||||
CoinSwitchAmountMarkupPercentage = coinswitch?.AmountMarkupPercentage ?? 0,
|
||||
CoinSwitchMerchantId = coinswitch?.MerchantId,
|
||||
CoinSwitchMode = coinswitch?.Mode,
|
||||
StoreId = store.Id,
|
||||
@ -345,9 +360,8 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
|
||||
{
|
||||
var res = paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
|
||||
Url.Content(network.CryptoImagePath) : Url.Content(network.LightningImagePath);
|
||||
return "/" + res;
|
||||
return paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
|
||||
this.Request.GetRelativePathOrAbsolute(network.CryptoImagePath) : this.Request.GetRelativePathOrAbsolute(network.LightningImagePath);
|
||||
}
|
||||
|
||||
private string OrderAmountFromInvoice(string cryptoCode, ProductInformation productInformation)
|
||||
@ -389,7 +403,7 @@ namespace BTCPayServer.Controllers
|
||||
if (!HttpContext.WebSockets.IsWebSocketRequest)
|
||||
return NotFound();
|
||||
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
|
||||
if (invoice == null || invoice.Status == InvoiceStatus.Complete || invoice.Status == InvoiceStatus.Invalid || invoice.Status == InvoiceStatus.Expired)
|
||||
if (invoice == null || invoice.Status == InvoiceStatus.Complete || invoice.Status == InvoiceStatus.Invalid || invoice.Status == InvoiceStatus.Expired)
|
||||
return NotFound();
|
||||
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||
CompositeDisposable leases = new CompositeDisposable();
|
||||
@ -437,34 +451,36 @@ namespace BTCPayServer.Controllers
|
||||
return BadRequest(ModelState);
|
||||
}
|
||||
await _InvoiceRepository.UpdateInvoice(invoiceId, data).ConfigureAwait(false);
|
||||
return Ok();
|
||||
return Ok("{}");
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 50)
|
||||
public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 50, int timezoneOffset = 0)
|
||||
{
|
||||
var model = new InvoicesModel
|
||||
{
|
||||
SearchTerm = searchTerm,
|
||||
Skip = skip,
|
||||
Count = count,
|
||||
StatusMessage = StatusMessage
|
||||
StatusMessage = StatusMessage,
|
||||
TimezoneOffset = timezoneOffset
|
||||
};
|
||||
InvoiceQuery invoiceQuery = GetInvoiceQuery(searchTerm);
|
||||
InvoiceQuery invoiceQuery = GetInvoiceQuery(searchTerm, timezoneOffset);
|
||||
var counting = _InvoiceRepository.GetInvoicesTotal(invoiceQuery);
|
||||
invoiceQuery.Count = count;
|
||||
invoiceQuery.Skip = skip;
|
||||
var list = await _InvoiceRepository.GetInvoices(invoiceQuery);
|
||||
|
||||
|
||||
foreach (var invoice in list)
|
||||
{
|
||||
var state = invoice.GetInvoiceState();
|
||||
model.Invoices.Add(new InvoiceModel()
|
||||
{
|
||||
Status = state.ToString(),
|
||||
Status = invoice.Status,
|
||||
StatusString = state.ToString(),
|
||||
ShowCheckout = invoice.Status == InvoiceStatus.New,
|
||||
Date = invoice.InvoiceTime,
|
||||
InvoiceId = invoice.Id,
|
||||
@ -472,28 +488,29 @@ namespace BTCPayServer.Controllers
|
||||
RedirectUrl = invoice.RedirectURL ?? string.Empty,
|
||||
AmountCurrency = _CurrencyNameTable.DisplayFormatCurrency(invoice.ProductInformation.Price, invoice.ProductInformation.Currency),
|
||||
CanMarkInvalid = state.CanMarkInvalid(),
|
||||
CanMarkComplete = state.CanMarkComplete()
|
||||
CanMarkComplete = state.CanMarkComplete(),
|
||||
Details = InvoicePopulatePayments(invoice)
|
||||
});
|
||||
}
|
||||
model.Total = await counting;
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private InvoiceQuery GetInvoiceQuery(string searchTerm = null)
|
||||
private InvoiceQuery GetInvoiceQuery(string searchTerm = null, int timezoneOffset = 0)
|
||||
{
|
||||
var filterString = new SearchString(searchTerm);
|
||||
var fs = new SearchString(searchTerm);
|
||||
var invoiceQuery = new InvoiceQuery()
|
||||
{
|
||||
TextSearch = filterString.TextSearch,
|
||||
TextSearch = fs.TextSearch,
|
||||
UserId = GetUserId(),
|
||||
Unusual = !filterString.Filters.ContainsKey("unusual") ? null
|
||||
: !bool.TryParse(filterString.Filters["unusual"].First(), out var r) ? (bool?)null
|
||||
: r,
|
||||
Status = filterString.Filters.ContainsKey("status") ? filterString.Filters["status"].ToArray() : null,
|
||||
ExceptionStatus = filterString.Filters.ContainsKey("exceptionstatus") ? filterString.Filters["exceptionstatus"].ToArray() : null,
|
||||
StoreId = filterString.Filters.ContainsKey("storeid") ? filterString.Filters["storeid"].ToArray() : null,
|
||||
ItemCode = filterString.Filters.ContainsKey("itemcode") ? filterString.Filters["itemcode"].ToArray() : null,
|
||||
OrderId = filterString.Filters.ContainsKey("orderid") ? filterString.Filters["orderid"].ToArray() : null
|
||||
Unusual = fs.GetFilterBool("unusual"),
|
||||
Status = fs.GetFilterArray("status"),
|
||||
ExceptionStatus = fs.GetFilterArray("exceptionstatus"),
|
||||
StoreId = fs.GetFilterArray("storeid"),
|
||||
ItemCode = fs.GetFilterArray("itemcode"),
|
||||
OrderId = fs.GetFilterArray("orderid"),
|
||||
StartDate = fs.GetFilterDate("startdate", timezoneOffset),
|
||||
EndDate = fs.GetFilterDate("enddate", timezoneOffset)
|
||||
};
|
||||
return invoiceQuery;
|
||||
}
|
||||
@ -501,13 +518,13 @@ namespace BTCPayServer.Controllers
|
||||
[HttpGet]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> Export(string format, string searchTerm = null)
|
||||
public async Task<IActionResult> Export(string format, string searchTerm = null, int timezoneOffset = 0)
|
||||
{
|
||||
var model = new InvoiceExport(_NetworkProvider, _CurrencyNameTable);
|
||||
|
||||
InvoiceQuery invoiceQuery = GetInvoiceQuery(searchTerm);
|
||||
invoiceQuery.Count = int.MaxValue;
|
||||
InvoiceQuery invoiceQuery = GetInvoiceQuery(searchTerm, timezoneOffset);
|
||||
invoiceQuery.Skip = 0;
|
||||
invoiceQuery.Count = int.MaxValue;
|
||||
var invoices = await _InvoiceRepository.GetInvoices(invoiceQuery);
|
||||
var res = model.Process(invoices, format);
|
||||
|
||||
@ -535,17 +552,36 @@ namespace BTCPayServer.Controllers
|
||||
StatusMessage = "Error: You need to create at least one store before creating a transaction";
|
||||
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
|
||||
}
|
||||
return View(new CreateInvoiceModel() { Stores = stores });
|
||||
|
||||
var paymentMethods = new SelectList(_NetworkProvider.GetAll().SelectMany(network => new[]
|
||||
{
|
||||
new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike),
|
||||
new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike)
|
||||
}).Select(id => new SelectListItem(id.ToString(true), id.ToString(false))),
|
||||
nameof(SelectListItem.Value),
|
||||
nameof(SelectListItem.Text));
|
||||
|
||||
return View(new CreateInvoiceModel() { Stores = stores, AvailablePaymentMethods = paymentMethods });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("invoices/create")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model)
|
||||
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model, CancellationToken cancellationToken)
|
||||
{
|
||||
var stores = await _StoreRepository.GetStoresByUserId(GetUserId());
|
||||
model.Stores = new SelectList(stores, nameof(StoreData.Id), nameof(StoreData.StoreName), model.StoreId);
|
||||
|
||||
var paymentMethods = new SelectList(_NetworkProvider.GetAll().SelectMany(network => new[]
|
||||
{
|
||||
new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike),
|
||||
new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike)
|
||||
}).Select(id => new SelectListItem(id.ToString(true), id.ToString(false))),
|
||||
nameof(SelectListItem.Value),
|
||||
nameof(SelectListItem.Text));
|
||||
model.AvailablePaymentMethods = paymentMethods;
|
||||
|
||||
var store = stores.FirstOrDefault(s => s.Id == model.StoreId);
|
||||
if (store == null)
|
||||
{
|
||||
@ -568,6 +604,7 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
|
||||
|
||||
if (StatusMessage != null)
|
||||
{
|
||||
return RedirectToAction(nameof(StoresController.UpdateStore), "Stores", new
|
||||
@ -590,7 +627,11 @@ namespace BTCPayServer.Controllers
|
||||
ItemDesc = model.ItemDesc,
|
||||
FullNotifications = true,
|
||||
BuyerEmail = model.BuyerEmail,
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot());
|
||||
SupportedTransactionCurrencies = model.SupportedTransactionCurrencies?.ToDictionary(s => s, s => new InvoiceSupportedTransactionCurrency()
|
||||
{
|
||||
Enabled = true
|
||||
})
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
|
||||
|
||||
StatusMessage = $"Invoice {result.Data.Id} just created!";
|
||||
return RedirectToAction(nameof(ListInvoices));
|
||||
@ -603,73 +644,45 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public IActionResult SearchInvoice(InvoicesModel invoices)
|
||||
{
|
||||
return RedirectToAction(nameof(ListInvoices), new
|
||||
{
|
||||
searchTerm = invoices.SearchTerm,
|
||||
skip = invoices.Skip,
|
||||
count = invoices.Count,
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices/{invoiceId}/changestate/{newState}")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public IActionResult ChangeInvoiceState(string invoiceId, string newState)
|
||||
{
|
||||
if (newState == "invalid")
|
||||
{
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Action = "Make invoice invalid",
|
||||
Title = "Change invoice state",
|
||||
Description = $"You will transition the state of this invoice to \"invalid\", do you want to continue?",
|
||||
});
|
||||
}
|
||||
else if (newState == "complete")
|
||||
{
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Action = "Make invoice complete",
|
||||
Title = "Change invoice state",
|
||||
Description = $"You will transition the state of this invoice to \"complete\", do you want to continue?",
|
||||
ButtonClass = "btn-primary"
|
||||
});
|
||||
}
|
||||
else
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("invoices/{invoiceId}/changestate/{newState}")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> ChangeInvoiceStateConfirm(string invoiceId, string newState)
|
||||
public async Task<IActionResult> ChangeInvoiceState(string invoiceId, string newState)
|
||||
{
|
||||
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
InvoiceId = invoiceId,
|
||||
UserId = GetUserId()
|
||||
})).FirstOrDefault();
|
||||
|
||||
var model = new InvoiceStateChangeModel();
|
||||
if (invoice == null)
|
||||
return NotFound();
|
||||
{
|
||||
model.NotFound = true;
|
||||
return NotFound(model);
|
||||
}
|
||||
|
||||
|
||||
if (newState == "invalid")
|
||||
{
|
||||
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId);
|
||||
_EventAggregator.Publish(new InvoiceEvent(invoice, 1008, InvoiceEvent.MarkedInvalid));
|
||||
StatusMessage = "Invoice marked invalid";
|
||||
model.StatusString = new InvoiceState("invalid", "marked").ToString();
|
||||
}
|
||||
else if(newState == "complete")
|
||||
else if (newState == "complete")
|
||||
{
|
||||
await _InvoiceRepository.UpdatePaidInvoiceToComplete(invoiceId);
|
||||
_EventAggregator.Publish(new InvoiceEvent(invoice, 2008, InvoiceEvent.MarkedCompleted));
|
||||
StatusMessage = "Invoice marked complete";
|
||||
model.StatusString = new InvoiceState("complete", "marked").ToString();
|
||||
}
|
||||
return RedirectToAction(nameof(ListInvoices));
|
||||
|
||||
return Json(model);
|
||||
}
|
||||
|
||||
public class InvoiceStateChangeModel
|
||||
{
|
||||
public bool NotFound { get; set; }
|
||||
public string StatusString { get; set; }
|
||||
}
|
||||
|
||||
[TempData]
|
||||
@ -688,18 +701,18 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
public static Dictionary<string, object> ParsePosData(string posData)
|
||||
{
|
||||
var result = new Dictionary<string,object>();
|
||||
var result = new Dictionary<string, object>();
|
||||
if (string.IsNullOrEmpty(posData))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
var jObject =JObject.Parse(posData);
|
||||
var jObject = JObject.Parse(posData);
|
||||
foreach (var item in jObject)
|
||||
{
|
||||
|
||||
|
||||
switch (item.Value.Type)
|
||||
{
|
||||
case JTokenType.Array:
|
||||
@ -716,7 +729,7 @@ namespace BTCPayServer.Controllers
|
||||
result.Add(item.Key, item.Value.ToString());
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
catch
|
||||
@ -726,6 +739,6 @@ namespace BTCPayServer.Controllers
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
@ -62,7 +63,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
|
||||
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string> additionalTags = null)
|
||||
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string> additionalTags = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!store.HasClaim(Policies.CanCreateInvoice.Key))
|
||||
throw new UnauthorizedAccessException();
|
||||
@ -83,6 +84,7 @@ namespace BTCPayServer.Controllers
|
||||
entity.ServerUrl = serverUrl;
|
||||
entity.FullNotifications = invoice.FullNotifications || invoice.ExtendedNotifications;
|
||||
entity.ExtendedNotifications = invoice.ExtendedNotifications;
|
||||
|
||||
if (invoice.NotificationURL != null &&
|
||||
Uri.TryCreate(invoice.NotificationURL, UriKind.Absolute, out var notificationUri) &&
|
||||
(notificationUri.Scheme == "http" || notificationUri.Scheme == "https"))
|
||||
@ -104,12 +106,14 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
var taxIncluded = invoice.TaxIncluded.HasValue ? invoice.TaxIncluded.Value : 0m;
|
||||
|
||||
|
||||
var currencyInfo = _CurrencyNameTable.GetNumberFormatInfo(invoice.Currency, false);
|
||||
if (currencyInfo != null)
|
||||
{
|
||||
invoice.Price = Math.Round(invoice.Price, currencyInfo.CurrencyDecimalDigits);
|
||||
invoice.TaxIncluded = Math.Round(taxIncluded, currencyInfo.CurrencyDecimalDigits);
|
||||
int divisibility = currencyInfo.CurrencyDecimalDigits;
|
||||
invoice.Price = invoice.Price.RoundToSignificant(ref divisibility);
|
||||
divisibility = currencyInfo.CurrencyDecimalDigits;
|
||||
invoice.TaxIncluded = taxIncluded.RoundToSignificant(ref divisibility);
|
||||
}
|
||||
invoice.Price = Math.Max(0.0m, invoice.Price);
|
||||
invoice.TaxIncluded = Math.Max(0.0m, taxIncluded);
|
||||
@ -122,6 +126,9 @@ namespace BTCPayServer.Controllers
|
||||
if (!Uri.IsWellFormedUriString(entity.RedirectURL, UriKind.Absolute))
|
||||
entity.RedirectURL = null;
|
||||
|
||||
entity.RedirectAutomatically =
|
||||
invoice.RedirectAutomatically.GetValueOrDefault(storeBlob.RedirectAutomatically);
|
||||
|
||||
entity.Status = InvoiceStatus.New;
|
||||
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
|
||||
|
||||
@ -135,7 +142,7 @@ namespace BTCPayServer.Controllers
|
||||
.Where(c => c.Value.Enabled)
|
||||
.Select(c => PaymentMethodId.TryParse(c.Key, out var p) ? p : null)
|
||||
.ToHashSet();
|
||||
excludeFilter = PaymentFilter.Or(excludeFilter,
|
||||
excludeFilter = PaymentFilter.Or(excludeFilter,
|
||||
PaymentFilter.Where(p => !supportedTransactionCurrencies.Contains(p)));
|
||||
}
|
||||
|
||||
@ -152,8 +159,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
var rateRules = storeBlob.GetRateRules(_NetworkProvider);
|
||||
var fetchingByCurrencyPair = _RateProvider.FetchRates(currencyPairsToFetch, rateRules);
|
||||
|
||||
var fetchingByCurrencyPair = _RateProvider.FetchRates(currencyPairsToFetch, rateRules, cancellationToken);
|
||||
var fetchingAll = WhenAllFetched(logs, fetchingByCurrencyPair);
|
||||
var supportedPaymentMethods = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.Where(s => !excludeFilter.Match(s.PaymentId))
|
||||
@ -197,8 +203,22 @@ namespace BTCPayServer.Controllers
|
||||
entity.InternalTags.Add(AppService.GetAppInternalTag(app.Id));
|
||||
}
|
||||
|
||||
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, logs, _NetworkProvider);
|
||||
await fetchingAll;
|
||||
using (logs.Measure("Saving invoice"))
|
||||
{
|
||||
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, _NetworkProvider);
|
||||
}
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await fetchingAll;
|
||||
}
|
||||
catch (AggregateException ex)
|
||||
{
|
||||
ex.Handle(e => { logs.Write($"Error while fetching rates {ex}"); return true; });
|
||||
}
|
||||
await _InvoiceRepository.AddInvoiceLogs(entity.Id, logs);
|
||||
});
|
||||
_EventAggregator.Publish(new Events.InvoiceEvent(entity, 1001, InvoiceEvent.Created));
|
||||
var resp = entity.EntityToDTO(_NetworkProvider);
|
||||
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
|
||||
@ -227,6 +247,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
try
|
||||
{
|
||||
var logPrefix = $"{supportedPaymentMethod.PaymentId.ToString(true)}:";
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var preparePayment = handler.PreparePayment(supportedPaymentMethod, store, network);
|
||||
var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.ProductInformation.Currency)];
|
||||
@ -239,8 +260,13 @@ namespace BTCPayServer.Controllers
|
||||
paymentMethod.Network = network;
|
||||
paymentMethod.SetId(supportedPaymentMethod.PaymentId);
|
||||
paymentMethod.Rate = rate.BidAsk.Bid;
|
||||
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network, preparePayment);
|
||||
paymentMethod.SetPaymentMethodDetails(paymentDetails);
|
||||
paymentMethod.PreferOnion = this.Request.IsOnion();
|
||||
|
||||
using (logs.Measure($"{logPrefix} Payment method details creation"))
|
||||
{
|
||||
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network, preparePayment);
|
||||
paymentMethod.SetPaymentMethodDetails(paymentDetails);
|
||||
}
|
||||
|
||||
Func<Money, Money, bool> compare = null;
|
||||
CurrencyValue limitValue = null;
|
||||
@ -268,7 +294,7 @@ namespace BTCPayServer.Controllers
|
||||
var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate.BidAsk.Bid);
|
||||
if (compare(paymentMethod.Calculate().Due, limitValueCrypto))
|
||||
{
|
||||
logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: {errorMessage}");
|
||||
logs.Write($"{logPrefix} {errorMessage}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ namespace BTCPayServer.Controllers
|
||||
throw new ArgumentNullException(nameof(directoryPath));
|
||||
Macaroons macaroons = new Macaroons();
|
||||
if (!Directory.Exists(directoryPath))
|
||||
return macaroons;
|
||||
throw new DirectoryNotFoundException("Macaroons directory not found");
|
||||
foreach(var file in Directory.GetFiles(directoryPath, "*.macaroon"))
|
||||
{
|
||||
try
|
||||
@ -49,6 +49,17 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
return macaroons;
|
||||
}
|
||||
|
||||
public Macaroons Clone()
|
||||
{
|
||||
return new Macaroons()
|
||||
{
|
||||
AdminMacaroon = AdminMacaroon,
|
||||
InvoiceMacaroon = InvoiceMacaroon,
|
||||
ReadonlyMacaroon = ReadonlyMacaroon
|
||||
};
|
||||
}
|
||||
|
||||
public Macaroon ReadonlyMacaroon { get; set; }
|
||||
|
||||
public Macaroon InvoiceMacaroon { get; set; }
|
||||
|
205
BTCPayServer/Controllers/ManageController.2FA.cs
Normal file
205
BTCPayServer/Controllers/ManageController.2FA.cs
Normal file
@ -0,0 +1,205 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models.ManageViewModels;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class ManageController
|
||||
{
|
||||
private const string AuthenicatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6";
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> TwoFactorAuthentication()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var model = new TwoFactorAuthenticationViewModel
|
||||
{
|
||||
HasAuthenticator = await _userManager.GetAuthenticatorKeyAsync(user) != null,
|
||||
Is2faEnabled = user.TwoFactorEnabled,
|
||||
RecoveryCodesLeft = await _userManager.CountRecoveryCodesAsync(user),
|
||||
};
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Disable2faWarning()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
if (!user.TwoFactorEnabled)
|
||||
{
|
||||
throw new ApplicationException(
|
||||
$"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'.");
|
||||
}
|
||||
|
||||
return View(nameof(Disable2fa));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Disable2fa()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false);
|
||||
if (!disable2faResult.Succeeded)
|
||||
{
|
||||
throw new ApplicationException(
|
||||
$"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'.");
|
||||
}
|
||||
|
||||
_logger.LogInformation("User with ID {UserId} has disabled 2fa.", user.Id);
|
||||
return RedirectToAction(nameof(TwoFactorAuthentication));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> EnableAuthenticator()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
|
||||
if (string.IsNullOrEmpty(unformattedKey))
|
||||
{
|
||||
await _userManager.ResetAuthenticatorKeyAsync(user);
|
||||
unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
|
||||
}
|
||||
|
||||
var model = new EnableAuthenticatorViewModel
|
||||
{
|
||||
SharedKey = FormatKey(unformattedKey),
|
||||
AuthenticatorUri = GenerateQrCodeUri(user.Email, unformattedKey)
|
||||
};
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> EnableAuthenticator(EnableAuthenticatorViewModel model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
// Strip spaces and hypens
|
||||
var verificationCode = model.Code.Replace(" ", string.Empty, StringComparison.InvariantCulture)
|
||||
.Replace("-", string.Empty, StringComparison.InvariantCulture);
|
||||
|
||||
var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync(
|
||||
user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);
|
||||
|
||||
if (!is2faTokenValid)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.Code), "Verification code is invalid.");
|
||||
return View(model);
|
||||
}
|
||||
|
||||
await _userManager.SetTwoFactorEnabledAsync(user, true);
|
||||
_logger.LogInformation("User with ID {UserId} has enabled 2FA with an authenticator app.", user.Id);
|
||||
return RedirectToAction(nameof(GenerateRecoveryCodes));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult ResetAuthenticatorWarning()
|
||||
{
|
||||
return View(nameof(ResetAuthenticator));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> ResetAuthenticator()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
await _userManager.SetTwoFactorEnabledAsync(user, false);
|
||||
await _userManager.ResetAuthenticatorKeyAsync(user);
|
||||
_logger.LogInformation("User with id '{UserId}' has reset their authentication app key.", user.Id);
|
||||
|
||||
return RedirectToAction(nameof(EnableAuthenticator));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GenerateRecoveryCodes()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
if (!user.TwoFactorEnabled)
|
||||
{
|
||||
throw new ApplicationException(
|
||||
$"Cannot generate recovery codes for user with ID '{user.Id}' as they do not have 2FA enabled.");
|
||||
}
|
||||
|
||||
var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
|
||||
var model = new GenerateRecoveryCodesViewModel {RecoveryCodes = recoveryCodes.ToArray()};
|
||||
|
||||
_logger.LogInformation("User with ID {UserId} has generated new 2FA recovery codes.", user.Id);
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private string GenerateQrCodeUri(string email, string unformattedKey)
|
||||
{
|
||||
return string.Format(CultureInfo.InvariantCulture,
|
||||
AuthenicatorUriFormat,
|
||||
_urlEncoder.Encode("BTCPayServer"),
|
||||
_urlEncoder.Encode(email),
|
||||
unformattedKey);
|
||||
}
|
||||
|
||||
private string FormatKey(string unformattedKey)
|
||||
{
|
||||
var result = new StringBuilder();
|
||||
int currentPosition = 0;
|
||||
while (currentPosition + 4 < unformattedKey.Length)
|
||||
{
|
||||
result.Append(unformattedKey.Substring(currentPosition, 4)).Append(" ");
|
||||
currentPosition += 4;
|
||||
}
|
||||
|
||||
if (currentPosition < unformattedKey.Length)
|
||||
{
|
||||
result.Append(unformattedKey.Substring(currentPosition));
|
||||
}
|
||||
|
||||
return result.ToString().ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
}
|
89
BTCPayServer/Controllers/ManageController.U2F.cs
Normal file
89
BTCPayServer/Controllers/ManageController.U2F.cs
Normal file
@ -0,0 +1,89 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Services.U2F.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class ManageController
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> U2FAuthentication(string statusMessage = null)
|
||||
{
|
||||
return View(new U2FAuthenticationViewModel()
|
||||
{
|
||||
StatusMessage = statusMessage,
|
||||
Devices = await _u2FService.GetDevices(_userManager.GetUserId(User))
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> RemoveU2FDevice(string id)
|
||||
{
|
||||
await _u2FService.RemoveDevice(id, _userManager.GetUserId(User));
|
||||
return RedirectToAction("U2FAuthentication", new
|
||||
{
|
||||
StatusMessage = "Device removed"
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult AddU2FDevice(string name)
|
||||
{
|
||||
if (!_btcPayServerEnvironment.IsSecure)
|
||||
{
|
||||
return RedirectToAction("U2FAuthentication", new
|
||||
{
|
||||
StatusMessage = new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = "Cannot register U2F device while not on https or tor"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var serverRegisterResponse = _u2FService.StartDeviceRegistration(_userManager.GetUserId(User),
|
||||
Request.GetAbsoluteUriNoPathBase().ToString().TrimEnd('/'));
|
||||
|
||||
return View(new AddU2FDeviceViewModel()
|
||||
{
|
||||
AppId = serverRegisterResponse.AppId,
|
||||
Challenge = serverRegisterResponse.Challenge,
|
||||
Version = serverRegisterResponse.Version,
|
||||
Name = name
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> AddU2FDevice(AddU2FDeviceViewModel viewModel)
|
||||
{
|
||||
var errorMessage = string.Empty;
|
||||
try
|
||||
{
|
||||
if (await _u2FService.CompleteRegistration(_userManager.GetUserId(User), viewModel.DeviceResponse,
|
||||
string.IsNullOrEmpty(viewModel.Name) ? "Unlabelled U2F Device" : viewModel.Name))
|
||||
{
|
||||
return RedirectToAction("U2FAuthentication", new
|
||||
|
||||
{
|
||||
StatusMessage = "Device added!"
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
errorMessage = e.Message;
|
||||
}
|
||||
|
||||
return RedirectToAction("U2FAuthentication", new
|
||||
{
|
||||
StatusMessage = new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = string.IsNullOrEmpty(errorMessage) ? "Could not add device." : errorMessage
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
@ -9,25 +8,23 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.ManageViewModels;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Authentication;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using NBitpayClient;
|
||||
using NBitcoin;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.U2F;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
[Route("[controller]/[action]")]
|
||||
public class ManageController : Controller
|
||||
public partial class ManageController : Controller
|
||||
{
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||
@ -36,10 +33,11 @@ namespace BTCPayServer.Controllers
|
||||
private readonly UrlEncoder _urlEncoder;
|
||||
TokenRepository _TokenRepository;
|
||||
IHostingEnvironment _Env;
|
||||
private readonly U2FService _u2FService;
|
||||
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
|
||||
StoreRepository _StoreRepository;
|
||||
|
||||
|
||||
private const string AuthenicatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6";
|
||||
|
||||
public ManageController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
@ -50,7 +48,9 @@ namespace BTCPayServer.Controllers
|
||||
TokenRepository tokenRepository,
|
||||
BTCPayWalletProvider walletProvider,
|
||||
StoreRepository storeRepository,
|
||||
IHostingEnvironment env)
|
||||
IHostingEnvironment env,
|
||||
U2FService u2FService,
|
||||
BTCPayServerEnvironment btcPayServerEnvironment)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
@ -59,6 +59,8 @@ namespace BTCPayServer.Controllers
|
||||
_urlEncoder = urlEncoder;
|
||||
_TokenRepository = tokenRepository;
|
||||
_Env = env;
|
||||
_u2FService = u2FService;
|
||||
_btcPayServerEnvironment = btcPayServerEnvironment;
|
||||
_StoreRepository = storeRepository;
|
||||
}
|
||||
|
||||
@ -113,6 +115,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
throw new ApplicationException($"Unexpected error occurred setting email for user with ID '{user.Id}'.");
|
||||
}
|
||||
await _userManager.SetUserNameAsync(user, model.Username);
|
||||
}
|
||||
|
||||
var phoneNumber = user.PhoneNumber;
|
||||
@ -338,163 +341,6 @@ namespace BTCPayServer.Controllers
|
||||
return RedirectToAction(nameof(ExternalLogins));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> TwoFactorAuthentication()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var model = new TwoFactorAuthenticationViewModel
|
||||
{
|
||||
HasAuthenticator = await _userManager.GetAuthenticatorKeyAsync(user) != null,
|
||||
Is2faEnabled = user.TwoFactorEnabled,
|
||||
RecoveryCodesLeft = await _userManager.CountRecoveryCodesAsync(user),
|
||||
};
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Disable2faWarning()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
if (!user.TwoFactorEnabled)
|
||||
{
|
||||
throw new ApplicationException($"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'.");
|
||||
}
|
||||
|
||||
return View(nameof(Disable2fa));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Disable2fa()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false);
|
||||
if (!disable2faResult.Succeeded)
|
||||
{
|
||||
throw new ApplicationException($"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'.");
|
||||
}
|
||||
|
||||
_logger.LogInformation("User with ID {UserId} has disabled 2fa.", user.Id);
|
||||
return RedirectToAction(nameof(TwoFactorAuthentication));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> EnableAuthenticator()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
|
||||
if (string.IsNullOrEmpty(unformattedKey))
|
||||
{
|
||||
await _userManager.ResetAuthenticatorKeyAsync(user);
|
||||
unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
|
||||
}
|
||||
|
||||
var model = new EnableAuthenticatorViewModel
|
||||
{
|
||||
SharedKey = FormatKey(unformattedKey),
|
||||
AuthenticatorUri = GenerateQrCodeUri(user.Email, unformattedKey)
|
||||
};
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> EnableAuthenticator(EnableAuthenticatorViewModel model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
// Strip spaces and hypens
|
||||
var verificationCode = model.Code.Replace(" ", string.Empty, StringComparison.InvariantCulture).Replace("-", string.Empty, StringComparison.InvariantCulture);
|
||||
|
||||
var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync(
|
||||
user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);
|
||||
|
||||
if (!is2faTokenValid)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.Code), "Verification code is invalid.");
|
||||
return View(model);
|
||||
}
|
||||
|
||||
await _userManager.SetTwoFactorEnabledAsync(user, true);
|
||||
_logger.LogInformation("User with ID {UserId} has enabled 2FA with an authenticator app.", user.Id);
|
||||
return RedirectToAction(nameof(GenerateRecoveryCodes));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult ResetAuthenticatorWarning()
|
||||
{
|
||||
return View(nameof(ResetAuthenticator));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> ResetAuthenticator()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
await _userManager.SetTwoFactorEnabledAsync(user, false);
|
||||
await _userManager.ResetAuthenticatorKeyAsync(user);
|
||||
_logger.LogInformation("User with id '{UserId}' has reset their authentication app key.", user.Id);
|
||||
|
||||
return RedirectToAction(nameof(EnableAuthenticator));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GenerateRecoveryCodes()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
if (!user.TwoFactorEnabled)
|
||||
{
|
||||
throw new ApplicationException($"Cannot generate recovery codes for user with ID '{user.Id}' as they do not have 2FA enabled.");
|
||||
}
|
||||
|
||||
var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
|
||||
var model = new GenerateRecoveryCodesViewModel { RecoveryCodes = recoveryCodes.ToArray() };
|
||||
|
||||
_logger.LogInformation("User with ID {UserId} has generated new 2FA recovery codes.", user.Id);
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
#region Helpers
|
||||
|
||||
@ -505,33 +351,6 @@ namespace BTCPayServer.Controllers
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
}
|
||||
|
||||
private string FormatKey(string unformattedKey)
|
||||
{
|
||||
var result = new StringBuilder();
|
||||
int currentPosition = 0;
|
||||
while (currentPosition + 4 < unformattedKey.Length)
|
||||
{
|
||||
result.Append(unformattedKey.Substring(currentPosition, 4)).Append(" ");
|
||||
currentPosition += 4;
|
||||
}
|
||||
if (currentPosition < unformattedKey.Length)
|
||||
{
|
||||
result.Append(unformattedKey.Substring(currentPosition));
|
||||
}
|
||||
|
||||
return result.ToString().ToLowerInvariant();
|
||||
}
|
||||
|
||||
private string GenerateQrCodeUri(string email, string unformattedKey)
|
||||
{
|
||||
return string.Format(CultureInfo.InvariantCulture,
|
||||
AuthenicatorUriFormat,
|
||||
_urlEncoder.Encode("BTCPayServer"),
|
||||
_urlEncoder.Encode(email),
|
||||
unformattedKey);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
378
BTCPayServer/Controllers/PaymentRequestController.cs
Normal file
378
BTCPayServer/Controllers/PaymentRequestController.cs
Normal file
@ -0,0 +1,378 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.PaymentRequestViewModels;
|
||||
using BTCPayServer.PaymentRequest;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.PaymentRequests;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Ganss.XSS;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Internal;
|
||||
using NBitpayClient;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Route("payment-requests")]
|
||||
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||
public class PaymentRequestController : Controller
|
||||
{
|
||||
private readonly InvoiceController _InvoiceController;
|
||||
private readonly UserManager<ApplicationUser> _UserManager;
|
||||
private readonly StoreRepository _StoreRepository;
|
||||
private readonly PaymentRequestRepository _PaymentRequestRepository;
|
||||
private readonly PaymentRequestService _PaymentRequestService;
|
||||
private readonly EventAggregator _EventAggregator;
|
||||
private readonly CurrencyNameTable _Currencies;
|
||||
private readonly HtmlSanitizer _htmlSanitizer;
|
||||
private readonly InvoiceRepository _InvoiceRepository;
|
||||
|
||||
public PaymentRequestController(
|
||||
InvoiceController invoiceController,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
StoreRepository storeRepository,
|
||||
PaymentRequestRepository paymentRequestRepository,
|
||||
PaymentRequestService paymentRequestService,
|
||||
EventAggregator eventAggregator,
|
||||
CurrencyNameTable currencies,
|
||||
HtmlSanitizer htmlSanitizer,
|
||||
InvoiceRepository invoiceRepository)
|
||||
{
|
||||
_InvoiceController = invoiceController;
|
||||
_UserManager = userManager;
|
||||
_StoreRepository = storeRepository;
|
||||
_PaymentRequestRepository = paymentRequestRepository;
|
||||
_PaymentRequestService = paymentRequestService;
|
||||
_EventAggregator = eventAggregator;
|
||||
_Currencies = currencies;
|
||||
_htmlSanitizer = htmlSanitizer;
|
||||
_InvoiceRepository = invoiceRepository;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("")]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> GetPaymentRequests(int skip = 0, int count = 50, string statusMessage = null)
|
||||
{
|
||||
var result = await _PaymentRequestRepository.FindPaymentRequests(new PaymentRequestQuery()
|
||||
{
|
||||
UserId = GetUserId(), Skip = skip, Count = count
|
||||
});
|
||||
return View(new ListPaymentRequestsViewModel()
|
||||
{
|
||||
Skip = skip,
|
||||
StatusMessage = statusMessage,
|
||||
Count = count,
|
||||
Total = result.Total,
|
||||
Items = result.Items.Select(data => new ViewPaymentRequestViewModel(data)).ToList()
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("edit/{id?}")]
|
||||
public async Task<IActionResult> EditPaymentRequest(string id, string statusMessage = null)
|
||||
{
|
||||
SelectList stores = null;
|
||||
var data = await _PaymentRequestRepository.FindPaymentRequest(id, GetUserId());
|
||||
if (data == null && !string.IsNullOrEmpty(id))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
stores = new SelectList(await _StoreRepository.GetStoresByUserId(GetUserId()), nameof(StoreData.Id),
|
||||
nameof(StoreData.StoreName), data?.StoreDataId);
|
||||
if (!stores.Any())
|
||||
{
|
||||
return RedirectToAction("GetPaymentRequests",
|
||||
new
|
||||
{
|
||||
StatusMessage = new StatusMessageModel()
|
||||
{
|
||||
Html =
|
||||
$"Error: You need to create at least one store. <a href='{Url.Action("CreateStore", "UserStores")}'>Create store</a>",
|
||||
Severity = StatusMessageModel.StatusSeverity.Error
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return View(new UpdatePaymentRequestViewModel(data)
|
||||
{
|
||||
Stores = stores,
|
||||
StatusMessage = statusMessage
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("edit/{id?}")]
|
||||
public async Task<IActionResult> EditPaymentRequest(string id, UpdatePaymentRequestViewModel viewModel)
|
||||
{
|
||||
if (string.IsNullOrEmpty(viewModel.Currency) ||
|
||||
_Currencies.GetCurrencyData(viewModel.Currency, false) == null)
|
||||
ModelState.AddModelError(nameof(viewModel.Currency), "Invalid currency");
|
||||
|
||||
var data = await _PaymentRequestRepository.FindPaymentRequest(id, GetUserId());
|
||||
if (data == null && !string.IsNullOrEmpty(id))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
viewModel.Stores = new SelectList(await _StoreRepository.GetStoresByUserId(GetUserId()),
|
||||
nameof(StoreData.Id),
|
||||
nameof(StoreData.StoreName), data?.StoreDataId);
|
||||
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
if (data == null)
|
||||
{
|
||||
data = new PaymentRequestData();
|
||||
}
|
||||
|
||||
data.StoreDataId = viewModel.StoreId;
|
||||
var blob = data.GetBlob();
|
||||
|
||||
blob.Title = viewModel.Title;
|
||||
blob.Email = viewModel.Email;
|
||||
blob.Description = _htmlSanitizer.Sanitize(viewModel.Description);
|
||||
blob.Amount = viewModel.Amount;
|
||||
blob.ExpiryDate = viewModel.ExpiryDate;
|
||||
blob.Currency = viewModel.Currency;
|
||||
blob.EmbeddedCSS = viewModel.EmbeddedCSS;
|
||||
blob.CustomCSSLink = viewModel.CustomCSSLink;
|
||||
blob.AllowCustomPaymentAmounts = viewModel.AllowCustomPaymentAmounts;
|
||||
|
||||
data.SetBlob(blob);
|
||||
data = await _PaymentRequestRepository.CreateOrUpdatePaymentRequest(data);
|
||||
_EventAggregator.Publish(new PaymentRequestUpdated()
|
||||
{
|
||||
Data = data,
|
||||
PaymentRequestId = data.Id
|
||||
});
|
||||
|
||||
return RedirectToAction("EditPaymentRequest", new {id = data.Id, StatusMessage = "Saved"});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{id}/remove")]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> RemovePaymentRequestPrompt(string id)
|
||||
{
|
||||
var data = await _PaymentRequestRepository.FindPaymentRequest(id, GetUserId());
|
||||
if (data == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var blob = data.GetBlob();
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Title = $"Remove Payment Request",
|
||||
Description = $"Are you sure you want to remove access to the payment request '{blob.Title}' ?",
|
||||
Action = "Delete"
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{id}/remove")]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> RemovePaymentRequest(string id)
|
||||
{
|
||||
var result = await _PaymentRequestRepository.RemovePaymentRequest(id, GetUserId());
|
||||
if (result)
|
||||
{
|
||||
return RedirectToAction("GetPaymentRequests",
|
||||
new {StatusMessage = "Payment request successfully removed"});
|
||||
}
|
||||
else
|
||||
{
|
||||
return RedirectToAction("GetPaymentRequests",
|
||||
new
|
||||
{
|
||||
StatusMessage =
|
||||
"Error: Payment request could not be removed. Any request that has generated invoices cannot be removed."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{id}")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> ViewPaymentRequest(string id, string statusMessage = null)
|
||||
{
|
||||
var result = await _PaymentRequestService.GetPaymentRequest(id, GetUserId());
|
||||
if (result == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
result.HubPath = PaymentRequestHub.GetHubPath(this.Request);
|
||||
result.StatusMessage = statusMessage;
|
||||
|
||||
return View(result);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{id}/pay")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> PayPaymentRequest(string id, bool redirectToInvoice = true,
|
||||
decimal? amount = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await _PaymentRequestService.GetPaymentRequest(id, GetUserId());
|
||||
if (result == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
result.HubPath = PaymentRequestHub.GetHubPath(this.Request);
|
||||
if (result.AmountDue <= 0)
|
||||
{
|
||||
if (redirectToInvoice)
|
||||
{
|
||||
return RedirectToAction("ViewPaymentRequest", new {Id = id});
|
||||
}
|
||||
|
||||
return BadRequest("Payment Request has already been settled.");
|
||||
}
|
||||
|
||||
if (result.ExpiryDate.HasValue && DateTime.Now >= result.ExpiryDate)
|
||||
{
|
||||
if (redirectToInvoice)
|
||||
{
|
||||
return RedirectToAction("ViewPaymentRequest", new {Id = id});
|
||||
}
|
||||
|
||||
return BadRequest("Payment Request has expired");
|
||||
}
|
||||
|
||||
var statusesAllowedToDisplay = new List<InvoiceStatus>()
|
||||
{
|
||||
InvoiceStatus.New
|
||||
};
|
||||
var validInvoice = result.Invoices.FirstOrDefault(invoice =>
|
||||
Enum.TryParse<InvoiceStatus>(invoice.Status, true, out var status) &&
|
||||
statusesAllowedToDisplay.Contains(status));
|
||||
|
||||
if (validInvoice != null)
|
||||
{
|
||||
if (redirectToInvoice)
|
||||
{
|
||||
return RedirectToAction("Checkout", "Invoice", new {Id = validInvoice.Id});
|
||||
}
|
||||
|
||||
return Ok(validInvoice.Id);
|
||||
}
|
||||
|
||||
if (result.AllowCustomPaymentAmounts && amount != null)
|
||||
amount = Math.Min(result.AmountDue, amount.Value);
|
||||
else
|
||||
amount = result.AmountDue;
|
||||
|
||||
|
||||
var pr = await _PaymentRequestRepository.FindPaymentRequest(id, null);
|
||||
var blob = pr.GetBlob();
|
||||
var store = pr.StoreData;
|
||||
store.AdditionalClaims.Add(new Claim(Policies.CanCreateInvoice.Key, store.Id));
|
||||
try
|
||||
{
|
||||
var redirectUrl = Request.GetDisplayUrl().TrimEnd("/pay", StringComparison.InvariantCulture)
|
||||
.Replace("hub?id=", string.Empty, StringComparison.InvariantCultureIgnoreCase);
|
||||
var newInvoiceId = (await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
|
||||
{
|
||||
OrderId = $"{PaymentRequestRepository.GetOrderIdForPaymentRequest(id)}",
|
||||
Currency = blob.Currency,
|
||||
Price = amount.Value,
|
||||
FullNotifications = true,
|
||||
BuyerEmail = result.Email,
|
||||
RedirectURL = redirectUrl,
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot(),
|
||||
new List<string>() {PaymentRequestRepository.GetInternalTag(id)},
|
||||
cancellationToken: cancellationToken))
|
||||
.Data.Id;
|
||||
|
||||
if (redirectToInvoice)
|
||||
{
|
||||
return RedirectToAction("Checkout", "Invoice", new {Id = newInvoiceId});
|
||||
}
|
||||
|
||||
return Ok(newInvoiceId);
|
||||
}
|
||||
catch (BitpayHttpException e)
|
||||
{
|
||||
return BadRequest(e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{id}/cancel")]
|
||||
public async Task<IActionResult> CancelUnpaidPendingInvoice(string id, bool redirect = true)
|
||||
{
|
||||
var result = await _PaymentRequestService.GetPaymentRequest(id, GetUserId());
|
||||
if (result == null )
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var invoice = result.Invoices.SingleOrDefault(requestInvoice =>
|
||||
requestInvoice.Status.Equals(InvoiceState.ToString(InvoiceStatus.New),StringComparison.InvariantCulture) && !requestInvoice.Payments.Any());
|
||||
|
||||
if (invoice == null )
|
||||
{
|
||||
return BadRequest("No unpaid pending invoice to cancel");
|
||||
}
|
||||
|
||||
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoice.Id);
|
||||
_EventAggregator.Publish(new InvoiceEvent(await _InvoiceRepository.GetInvoice(invoice.Id), 1008, InvoiceEvent.MarkedInvalid));
|
||||
|
||||
if (redirect)
|
||||
{
|
||||
return RedirectToAction(nameof(ViewPaymentRequest), new
|
||||
{
|
||||
Id = id,
|
||||
StatusMessage = "Payment cancelled"
|
||||
});
|
||||
}
|
||||
|
||||
return Ok("Payment cancelled");
|
||||
}
|
||||
|
||||
private string GetUserId()
|
||||
{
|
||||
return _UserManager.GetUserId(User);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{id}/clone")]
|
||||
public async Task<IActionResult> ClonePaymentRequest(string id)
|
||||
{
|
||||
var result = await EditPaymentRequest(id);
|
||||
if (result is ViewResult viewResult)
|
||||
{
|
||||
var model = (UpdatePaymentRequestViewModel)viewResult.Model;
|
||||
model.Id = null;
|
||||
model.Title = $"Clone of {model.Title}";
|
||||
|
||||
return View("EditPaymentRequest", model);
|
||||
|
||||
}
|
||||
|
||||
return NotFound();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models;
|
||||
@ -28,7 +29,7 @@ namespace BTCPayServer.Controllers
|
||||
[MediaTypeAcceptConstraintAttribute("text/html")]
|
||||
[IgnoreAntiforgeryToken]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public async Task<IActionResult> PayButtonHandle([FromForm]PayButtonViewModel model)
|
||||
public async Task<IActionResult> PayButtonHandle([FromForm]PayButtonViewModel model, CancellationToken cancellationToken)
|
||||
{
|
||||
var store = await _StoreRepository.FindStore(model.StoreId);
|
||||
if (store == null)
|
||||
@ -56,7 +57,7 @@ namespace BTCPayServer.Controllers
|
||||
NotificationURL = model.ServerIpn,
|
||||
RedirectURL = model.BrowserRedirect,
|
||||
FullNotifications = true
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot());
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
|
||||
return Redirect(invoice.Data.Url);
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ namespace BTCPayServer.Controllers
|
||||
var paymentMethodDetails = GetExistingLightningSupportedPaymentMethod(cryptoCode, store);
|
||||
var network = _BtcPayNetworkProvider.GetNetwork(cryptoCode);
|
||||
var nodeInfo =
|
||||
await _LightningLikePaymentHandler.GetNodeInfo(paymentMethodDetails,
|
||||
await _LightningLikePaymentHandler.GetNodeInfo(this.Request.IsOnion(), paymentMethodDetails,
|
||||
network);
|
||||
|
||||
return View(new ShowLightningNodeInfoViewModel()
|
||||
|
@ -13,6 +13,7 @@ using Newtonsoft.Json;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using BTCPayServer.Authentication;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using System.Threading;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -45,7 +46,7 @@ namespace BTCPayServer.Controllers
|
||||
[Route("rates/{baseCurrency}")]
|
||||
[HttpGet]
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<IActionResult> GetBaseCurrencyRates(string baseCurrency, string storeId)
|
||||
public async Task<IActionResult> GetBaseCurrencyRates(string baseCurrency, string storeId, CancellationToken cancellationToken)
|
||||
{
|
||||
storeId = await GetStoreId(storeId);
|
||||
var store = this.HttpContext.GetStoreData();
|
||||
@ -64,7 +65,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var currencypairs = BuildCurrencyPairs(currencyCodes, baseCurrency);
|
||||
|
||||
var result = await GetRates2(currencypairs, store.Id);
|
||||
var result = await GetRates2(currencypairs, store.Id, cancellationToken);
|
||||
var rates = (result as JsonResult)?.Value as Rate[];
|
||||
if (rates == null)
|
||||
return result;
|
||||
@ -75,10 +76,10 @@ namespace BTCPayServer.Controllers
|
||||
[Route("rates/{baseCurrency}/{currency}")]
|
||||
[HttpGet]
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<IActionResult> GetCurrencyPairRate(string baseCurrency, string currency, string storeId)
|
||||
public async Task<IActionResult> GetCurrencyPairRate(string baseCurrency, string currency, string storeId, CancellationToken cancellationToken)
|
||||
{
|
||||
storeId = await GetStoreId(storeId);
|
||||
var result = await GetRates2($"{baseCurrency}_{currency}", storeId);
|
||||
var result = await GetRates2($"{baseCurrency}_{currency}", storeId, cancellationToken);
|
||||
var rates = (result as JsonResult)?.Value as Rate[];
|
||||
if (rates == null)
|
||||
return result;
|
||||
@ -88,10 +89,9 @@ namespace BTCPayServer.Controllers
|
||||
[Route("rates")]
|
||||
[HttpGet]
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<IActionResult> GetRates(string currencyPairs, string storeId)
|
||||
public async Task<IActionResult> GetRates(string currencyPairs, string storeId, CancellationToken cancellationToken)
|
||||
{
|
||||
storeId = await GetStoreId(storeId);
|
||||
var result = await GetRates2(currencyPairs, storeId);
|
||||
var result = await GetRates2(currencyPairs, storeId, cancellationToken);
|
||||
var rates = (result as JsonResult)?.Value as Rate[];
|
||||
if (rates == null)
|
||||
return result;
|
||||
@ -118,7 +118,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[Route("api/rates")]
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetRates2(string currencyPairs, string storeId)
|
||||
public async Task<IActionResult> GetRates2(string currencyPairs, string storeId, CancellationToken cancellationToken)
|
||||
{
|
||||
storeId = await GetStoreId(storeId);
|
||||
if (storeId == null)
|
||||
@ -139,15 +139,10 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
if (currencyPairs == null)
|
||||
{
|
||||
var supportedMethods = store.GetSupportedPaymentMethods(_NetworkProvider);
|
||||
var currencyCodes = supportedMethods.Select(method => method.PaymentId.CryptoCode).Distinct();
|
||||
var defaultPaymentId = store.GetDefaultPaymentId(_NetworkProvider);
|
||||
|
||||
currencyPairs = BuildCurrencyPairs(currencyCodes, defaultPaymentId.CryptoCode);
|
||||
|
||||
currencyPairs = store.GetStoreBlob().GetDefaultCurrencyPairString();
|
||||
if (string.IsNullOrEmpty(currencyPairs))
|
||||
{
|
||||
var result = Json(new BitpayErrorsModel() { Error = "You need to specify currencyPairs (eg. BTC_USD,LTC_CAD)" });
|
||||
var result = Json(new BitpayErrorsModel() { Error = "You need to setup the default currency pairs in 'Store Settings / Rates' or specify 'currencyPairs' query parameter (eg. BTC_USD,LTC_CAD)." });
|
||||
result.StatusCode = 400;
|
||||
return result;
|
||||
}
|
||||
@ -168,7 +163,7 @@ namespace BTCPayServer.Controllers
|
||||
pairs.Add(pair);
|
||||
}
|
||||
|
||||
var fetching = _RateProviderFactory.FetchRates(pairs, rules);
|
||||
var fetching = _RateProviderFactory.FetchRates(pairs, rules, cancellationToken);
|
||||
await Task.WhenAll(fetching.Select(f => f.Value).ToArray());
|
||||
return Json(pairs
|
||||
.Select(r => (Pair: r, Value: fetching[r].GetAwaiter().GetResult().BidAsk?.Bid))
|
||||
|
289
BTCPayServer/Controllers/ServerController.Storage.cs
Normal file
289
BTCPayServer/Controllers/ServerController.Storage.cs
Normal file
@ -0,0 +1,289 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.ServerViewModels;
|
||||
using BTCPayServer.Storage.Models;
|
||||
using BTCPayServer.Storage.Services.Providers.AmazonS3Storage;
|
||||
using BTCPayServer.Storage.Services.Providers.AmazonS3Storage.Configuration;
|
||||
using BTCPayServer.Storage.Services.Providers.AzureBlobStorage;
|
||||
using BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration;
|
||||
using BTCPayServer.Storage.Services.Providers.FileSystemStorage;
|
||||
using BTCPayServer.Storage.Services.Providers.FileSystemStorage.Configuration;
|
||||
using BTCPayServer.Storage.Services.Providers.GoogleCloudStorage;
|
||||
using BTCPayServer.Storage.Services.Providers.GoogleCloudStorage.Configuration;
|
||||
using BTCPayServer.Storage.Services.Providers.Models;
|
||||
using BTCPayServer.Storage.ViewModels;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class ServerController
|
||||
{
|
||||
[HttpGet("server/files/{fileId?}")]
|
||||
public async Task<IActionResult> Files(string fileId = null, string statusMessage = null)
|
||||
{
|
||||
TempData["StatusMessage"] = statusMessage;
|
||||
var fileUrl = string.IsNullOrEmpty(fileId) ? null : await _FileService.GetFileUrl(fileId);
|
||||
|
||||
return View(new ViewFilesViewModel()
|
||||
{
|
||||
Files = await _StoredFileRepository.GetFiles(),
|
||||
SelectedFileId = string.IsNullOrEmpty(fileUrl) ? null : fileId,
|
||||
DirectFileUrl = fileUrl,
|
||||
StorageConfigured = (await _SettingsRepository.GetSettingAsync<StorageSettings>()) != null
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("server/files/{fileId}/delete")]
|
||||
public async Task<IActionResult> DeleteFile(string fileId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _FileService.RemoveFile(fileId, null);
|
||||
return RedirectToAction(nameof(Files), new
|
||||
{
|
||||
fileId = "",
|
||||
statusMessage = "File removed"
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return RedirectToAction(nameof(Files), new
|
||||
{
|
||||
statusMessage = new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = e.Message
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("server/files/{fileId}/tmp")]
|
||||
public async Task<IActionResult> CreateTemporaryFileUrl(string fileId)
|
||||
{
|
||||
var file = await _StoredFileRepository.GetFile(fileId);
|
||||
|
||||
if (file == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return View(new CreateTemporaryFileUrlViewModel());
|
||||
}
|
||||
|
||||
[HttpPost("server/files/{fileId}/tmp")]
|
||||
public async Task<IActionResult> CreateTemporaryFileUrl(string fileId,
|
||||
CreateTemporaryFileUrlViewModel viewModel)
|
||||
{
|
||||
if (viewModel.TimeAmount <= 0)
|
||||
{
|
||||
ModelState.AddModelError(nameof(viewModel.TimeAmount), "Time must be at least 1");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
var file = await _StoredFileRepository.GetFile(fileId);
|
||||
|
||||
if (file == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var expiry = DateTimeOffset.Now;
|
||||
switch (viewModel.TimeType)
|
||||
{
|
||||
case CreateTemporaryFileUrlViewModel.TmpFileTimeType.Seconds:
|
||||
expiry =expiry.AddSeconds(viewModel.TimeAmount);
|
||||
break;
|
||||
case CreateTemporaryFileUrlViewModel.TmpFileTimeType.Minutes:
|
||||
expiry = expiry.AddMinutes(viewModel.TimeAmount);
|
||||
break;
|
||||
case CreateTemporaryFileUrlViewModel.TmpFileTimeType.Hours:
|
||||
expiry = expiry.AddHours(viewModel.TimeAmount);
|
||||
break;
|
||||
case CreateTemporaryFileUrlViewModel.TmpFileTimeType.Days:
|
||||
expiry = expiry.AddDays(viewModel.TimeAmount);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
var url = await _FileService.GetTemporaryFileUrl(fileId, expiry, viewModel.IsDownload);
|
||||
|
||||
return RedirectToAction(nameof(Files), new
|
||||
{
|
||||
StatusMessage = new StatusMessageModel()
|
||||
{
|
||||
Html =
|
||||
$"Generated Temporary Url for file {file.FileName} which expires at {expiry:G}. <a href='{url}' target='_blank'>{url}</a>"
|
||||
}.ToString(),
|
||||
fileId,
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public class CreateTemporaryFileUrlViewModel
|
||||
{
|
||||
public enum TmpFileTimeType
|
||||
{
|
||||
Seconds,
|
||||
Minutes,
|
||||
Hours,
|
||||
Days
|
||||
}
|
||||
public int TimeAmount { get; set; }
|
||||
public TmpFileTimeType TimeType { get; set; }
|
||||
public bool IsDownload { get; set; }
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("server/files/upload")]
|
||||
public async Task<IActionResult> CreateFile(IFormFile file)
|
||||
{
|
||||
var newFile = await _FileService.AddFile(file, GetUserId());
|
||||
return RedirectToAction(nameof(Files), new
|
||||
{
|
||||
statusMessage = "File added!",
|
||||
fileId = newFile.Id
|
||||
});
|
||||
}
|
||||
|
||||
private string GetUserId()
|
||||
{
|
||||
return _UserManager.GetUserId(ControllerContext.HttpContext.User);
|
||||
}
|
||||
|
||||
[HttpGet("server/storage")]
|
||||
public async Task<IActionResult> Storage(bool forceChoice = false, string statusMessage = null)
|
||||
{
|
||||
TempData["StatusMessage"] = statusMessage;
|
||||
var savedSettings = await _SettingsRepository.GetSettingAsync<StorageSettings>();
|
||||
if (forceChoice || savedSettings == null)
|
||||
{
|
||||
return View(new ChooseStorageViewModel()
|
||||
{
|
||||
ShowChangeWarning = savedSettings != null,
|
||||
Provider = savedSettings?.Provider ?? BTCPayServer.Storage.Models.StorageProvider.FileSystem
|
||||
});
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(StorageProvider), new
|
||||
{
|
||||
provider = savedSettings.Provider
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("server/storage")]
|
||||
public IActionResult Storage(StorageSettings viewModel)
|
||||
{
|
||||
return RedirectToAction("StorageProvider", "Server", new
|
||||
{
|
||||
provider = viewModel.Provider.ToString()
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("server/storage/{provider}")]
|
||||
public async Task<IActionResult> StorageProvider(string provider)
|
||||
{
|
||||
if (!Enum.TryParse(typeof(StorageProvider), provider, out var storageProvider))
|
||||
{
|
||||
return RedirectToAction(nameof(Storage), new
|
||||
{
|
||||
StatusMessage = new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = $"{provider} provider is not supported"
|
||||
}.ToString()
|
||||
});
|
||||
}
|
||||
|
||||
var data = (await _SettingsRepository.GetSettingAsync<StorageSettings>()) ?? new StorageSettings();
|
||||
|
||||
var storageProviderService =
|
||||
_StorageProviderServices.SingleOrDefault(service => service.StorageProvider().Equals(storageProvider));
|
||||
|
||||
switch (storageProviderService)
|
||||
{
|
||||
case null:
|
||||
return RedirectToAction(nameof(Storage), new
|
||||
{
|
||||
StatusMessage = new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = $"{storageProvider} is not supported"
|
||||
}.ToString()
|
||||
});
|
||||
case AzureBlobStorageFileProviderService fileProviderService:
|
||||
return View(nameof(EditAzureBlobStorageStorageProvider),
|
||||
fileProviderService.GetProviderConfiguration(data));
|
||||
|
||||
case AmazonS3FileProviderService fileProviderService:
|
||||
return View(nameof(EditAmazonS3StorageProvider),
|
||||
fileProviderService.GetProviderConfiguration(data));
|
||||
|
||||
case GoogleCloudStorageFileProviderService fileProviderService:
|
||||
return View(nameof(EditGoogleCloudStorageStorageProvider),
|
||||
fileProviderService.GetProviderConfiguration(data));
|
||||
|
||||
case FileSystemFileProviderService fileProviderService:
|
||||
return View(nameof(EditFileSystemStorageProvider),
|
||||
fileProviderService.GetProviderConfiguration(data));
|
||||
}
|
||||
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("server/storage/AzureBlobStorage")]
|
||||
public async Task<IActionResult> EditAzureBlobStorageStorageProvider(AzureBlobStorageConfiguration viewModel)
|
||||
{
|
||||
return await SaveStorageProvider(viewModel, BTCPayServer.Storage.Models.StorageProvider.AzureBlobStorage);
|
||||
}
|
||||
|
||||
[HttpPost("server/storage/AmazonS3")]
|
||||
public async Task<IActionResult> EditAmazonS3StorageProvider(AmazonS3StorageConfiguration viewModel)
|
||||
{
|
||||
return await SaveStorageProvider(viewModel, BTCPayServer.Storage.Models.StorageProvider.AmazonS3);
|
||||
}
|
||||
|
||||
[HttpPost("server/storage/GoogleCloudStorage")]
|
||||
public async Task<IActionResult> EditGoogleCloudStorageStorageProvider(
|
||||
GoogleCloudStorageConfiguration viewModel)
|
||||
{
|
||||
return await SaveStorageProvider(viewModel, BTCPayServer.Storage.Models.StorageProvider.GoogleCloudStorage);
|
||||
}
|
||||
|
||||
[HttpPost("server/storage/FileSystem")]
|
||||
public async Task<IActionResult> EditFileSystemStorageProvider(FileSystemStorageConfiguration viewModel)
|
||||
{
|
||||
return await SaveStorageProvider(viewModel, BTCPayServer.Storage.Models.StorageProvider.FileSystem);
|
||||
}
|
||||
|
||||
private async Task<IActionResult> SaveStorageProvider(IBaseStorageConfiguration viewModel,
|
||||
StorageProvider storageProvider)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
var data = (await _SettingsRepository.GetSettingAsync<StorageSettings>()) ?? new StorageSettings();
|
||||
data.Provider = storageProvider;
|
||||
data.Configuration = JObject.FromObject(viewModel);
|
||||
await _SettingsRepository.UpdateSetting(data);
|
||||
TempData["StatusMessage"] = new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = "Storage settings updated successfully"
|
||||
}.ToString();
|
||||
return View(viewModel);
|
||||
}
|
||||
}
|
||||
}
|
@ -26,13 +26,18 @@ using System.Threading.Tasks;
|
||||
using Renci.SshNet;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Configuration.External;
|
||||
using System.Runtime.CompilerServices;
|
||||
using BTCPayServer.Storage.Models;
|
||||
using BTCPayServer.Storage.Services;
|
||||
using BTCPayServer.Storage.Services.Providers;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Authorize(Policy = BTCPayServer.Security.Policies.CanModifyServerSettings.Key)]
|
||||
public class ServerController : Controller
|
||||
public partial class ServerController : Controller
|
||||
{
|
||||
private UserManager<ApplicationUser> _UserManager;
|
||||
SettingsRepository _SettingsRepository;
|
||||
@ -40,18 +45,31 @@ namespace BTCPayServer.Controllers
|
||||
private RateFetcher _RateProviderFactory;
|
||||
private StoreRepository _StoreRepository;
|
||||
LightningConfigurationProvider _LnConfigProvider;
|
||||
private readonly TorServices _torServices;
|
||||
BTCPayServerOptions _Options;
|
||||
ApplicationDbContextFactory _ContextFactory;
|
||||
private readonly StoredFileRepository _StoredFileRepository;
|
||||
private readonly FileService _FileService;
|
||||
private readonly IEnumerable<IStorageProviderService> _StorageProviderServices;
|
||||
|
||||
public ServerController(UserManager<ApplicationUser> userManager,
|
||||
Configuration.BTCPayServerOptions options,
|
||||
StoredFileRepository storedFileRepository,
|
||||
FileService fileService,
|
||||
IEnumerable<IStorageProviderService> storageProviderServices,
|
||||
BTCPayServerOptions options,
|
||||
RateFetcher rateProviderFactory,
|
||||
SettingsRepository settingsRepository,
|
||||
NBXplorerDashboard dashBoard,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
LightningConfigurationProvider lnConfigProvider,
|
||||
Services.Stores.StoreRepository storeRepository)
|
||||
TorServices torServices,
|
||||
StoreRepository storeRepository,
|
||||
ApplicationDbContextFactory contextFactory)
|
||||
{
|
||||
_Options = options;
|
||||
_StoredFileRepository = storedFileRepository;
|
||||
_FileService = fileService;
|
||||
_StorageProviderServices = storageProviderServices;
|
||||
_UserManager = userManager;
|
||||
_SettingsRepository = settingsRepository;
|
||||
_dashBoard = dashBoard;
|
||||
@ -59,6 +77,8 @@ namespace BTCPayServer.Controllers
|
||||
_RateProviderFactory = rateProviderFactory;
|
||||
_StoreRepository = storeRepository;
|
||||
_LnConfigProvider = lnConfigProvider;
|
||||
_torServices = torServices;
|
||||
_ContextFactory = contextFactory;
|
||||
}
|
||||
|
||||
[Route("server/rates")]
|
||||
@ -247,6 +267,13 @@ namespace BTCPayServer.Controllers
|
||||
return error;
|
||||
StatusMessage = $"The server might restart soon if an update is available...";
|
||||
}
|
||||
else if (command == "clean")
|
||||
{
|
||||
var error = RunSSH(vm, $"btcpay-clean.sh");
|
||||
if (error != null)
|
||||
return error;
|
||||
StatusMessage = $"The old docker images will be cleaned soon...";
|
||||
}
|
||||
else
|
||||
{
|
||||
return NotFound();
|
||||
@ -385,18 +412,17 @@ namespace BTCPayServer.Controllers
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
|
||||
var admins = await _UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
|
||||
if (admins.Count == 1)
|
||||
{
|
||||
// return
|
||||
return View("Confirm", new ConfirmModel("Unable to Delete Last Admin",
|
||||
"This is the last Admin, so it can't be removed"));
|
||||
}
|
||||
|
||||
|
||||
var roles = await _UserManager.GetRolesAsync(user);
|
||||
if (IsAdmin(roles))
|
||||
{
|
||||
var admins = await _UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
|
||||
if (admins.Count == 1)
|
||||
{
|
||||
// return
|
||||
return View("Confirm", new ConfirmModel("Unable to Delete Last Admin",
|
||||
"This is the last Admin, so it can't be removed"));
|
||||
}
|
||||
|
||||
return View("Confirm", new ConfirmModel("Delete Admin " + user.Email,
|
||||
"Are you sure you want to delete this Admin and delete all accounts, users and data associated with the server account?",
|
||||
"Delete"));
|
||||
@ -429,228 +455,243 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
public IHttpClientFactory HttpClientFactory { get; }
|
||||
|
||||
[Route("server/emails")]
|
||||
public async Task<IActionResult> Emails()
|
||||
{
|
||||
var data = (await _SettingsRepository.GetSettingAsync<EmailSettings>()) ?? new EmailSettings();
|
||||
return View(new EmailsViewModel() { Settings = data });
|
||||
}
|
||||
|
||||
[Route("server/policies")]
|
||||
public async Task<IActionResult> Policies()
|
||||
{
|
||||
var data = (await _SettingsRepository.GetSettingAsync<PoliciesSettings>()) ?? new PoliciesSettings();
|
||||
|
||||
// load display app dropdown
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
var userId = _UserManager.GetUserId(base.User);
|
||||
var selectList = ctx.Users.Where(user => user.Id == userId)
|
||||
.SelectMany(s => s.UserStores)
|
||||
.Select(s => s.StoreData)
|
||||
.SelectMany(s => s.Apps)
|
||||
.Select(a => new SelectListItem($"{a.AppType} - {a.Name}", a.Id)).ToList();
|
||||
selectList.Insert(0, new SelectListItem("(None)", null));
|
||||
ViewBag.AppsList = new SelectList(selectList, "Value", "Text", data.RootAppId);
|
||||
}
|
||||
|
||||
return View(data);
|
||||
}
|
||||
[Route("server/policies")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Policies(PoliciesSettings settings)
|
||||
{
|
||||
if (!String.IsNullOrEmpty(settings.RootAppId))
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
var app = ctx.Apps.SingleOrDefault(a => a.Id == settings.RootAppId);
|
||||
if (app != null)
|
||||
settings.RootAppType = Enum.Parse<AppType>(app.AppType);
|
||||
else
|
||||
settings.RootAppType = null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// not preserved on client side, but clearing it just in case
|
||||
settings.RootAppType = null;
|
||||
}
|
||||
|
||||
await _SettingsRepository.UpdateSetting(settings);
|
||||
TempData["StatusMessage"] = "Policies updated successfully";
|
||||
return View(settings);
|
||||
return RedirectToAction(nameof(Policies));
|
||||
}
|
||||
|
||||
[Route("server/services")]
|
||||
public IActionResult Services()
|
||||
public async Task<IActionResult> Services()
|
||||
{
|
||||
var result = new ServicesViewModel();
|
||||
foreach (var cryptoCode in _Options.ExternalServicesByCryptoCode.Keys)
|
||||
result.ExternalServices = _Options.ExternalServices.ToList();
|
||||
foreach (var externalService in _Options.OtherExternalServices)
|
||||
{
|
||||
int i = 0;
|
||||
foreach (var grpcService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalLnd>(cryptoCode))
|
||||
{
|
||||
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
|
||||
{
|
||||
Crypto = cryptoCode,
|
||||
Type = grpcService.Type,
|
||||
Action = nameof(LndServices),
|
||||
Index = i++,
|
||||
});
|
||||
}
|
||||
i = 0;
|
||||
foreach (var sparkService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalSpark>(cryptoCode))
|
||||
{
|
||||
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
|
||||
{
|
||||
Crypto = cryptoCode,
|
||||
Type = "Spark server",
|
||||
Action = nameof(SparkService),
|
||||
Index = i++,
|
||||
});
|
||||
}
|
||||
foreach (var rtlService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalRTL>(cryptoCode))
|
||||
{
|
||||
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
|
||||
{
|
||||
Crypto = cryptoCode,
|
||||
Type = "Ride the Lightning server (RTL)",
|
||||
Action = nameof(RTLService),
|
||||
Index = i++,
|
||||
});
|
||||
}
|
||||
foreach (var chargeService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalCharge>(cryptoCode))
|
||||
{
|
||||
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
|
||||
{
|
||||
Crypto = cryptoCode,
|
||||
Type = "Lightning charge server",
|
||||
Action = nameof(LightningChargeServices),
|
||||
Index = i++,
|
||||
});
|
||||
}
|
||||
}
|
||||
foreach (var externalService in _Options.ExternalServices)
|
||||
{
|
||||
result.ExternalServices.Add(new ServicesViewModel.ExternalService()
|
||||
result.OtherExternalServices.Add(new ServicesViewModel.OtherExternalService()
|
||||
{
|
||||
Name = externalService.Key,
|
||||
Link = this.Request.GetRelativePathOrAbsolute(externalService.Value)
|
||||
Link = this.Request.GetAbsoluteUriNoPathBase(externalService.Value).AbsoluteUri
|
||||
});
|
||||
}
|
||||
if (_Options.SSHSettings != null)
|
||||
{
|
||||
result.ExternalServices.Add(new ServicesViewModel.ExternalService()
|
||||
result.OtherExternalServices.Add(new ServicesViewModel.OtherExternalService()
|
||||
{
|
||||
Name = "SSH",
|
||||
Link = this.Url.Action(nameof(SSHService))
|
||||
});
|
||||
}
|
||||
foreach (var torService in _torServices.Services)
|
||||
{
|
||||
if (torService.VirtualPort == 80)
|
||||
{
|
||||
result.TorHttpServices.Add(new ServicesViewModel.OtherExternalService()
|
||||
{
|
||||
Name = torService.Name,
|
||||
Link = $"http://{torService.OnionHost}"
|
||||
});
|
||||
}
|
||||
else if (TryParseAsExternalService(torService, out var externalService))
|
||||
{
|
||||
result.ExternalServices.Add(externalService);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.TorOtherServices.Add(new ServicesViewModel.OtherExternalService()
|
||||
{
|
||||
Name = torService.Name,
|
||||
Link = $"{torService.OnionHost}:{torService.VirtualPort}"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var storageSettings = await _SettingsRepository.GetSettingAsync<StorageSettings>();
|
||||
result.ExternalStorageServices.Add(new ServicesViewModel.OtherExternalService()
|
||||
{
|
||||
Name = storageSettings == null? "Not set": storageSettings.Provider.ToString(),
|
||||
Link = Url.Action("Storage")
|
||||
});
|
||||
return View(result);
|
||||
}
|
||||
|
||||
[Route("server/services/lightning-charge/{cryptoCode}/{index}")]
|
||||
public async Task<IActionResult> LightningChargeServices(string cryptoCode, int index, bool showQR = false)
|
||||
private static bool TryParseAsExternalService(TorService torService, out ExternalService externalService)
|
||||
{
|
||||
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
|
||||
externalService = null;
|
||||
if (torService.ServiceType == TorServiceType.P2P)
|
||||
{
|
||||
StatusMessage = $"Error: {cryptoCode} is not fully synched";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
var lightningCharge = _Options.ExternalServicesByCryptoCode.GetServices<ExternalCharge>(cryptoCode).Select(c => c.ConnectionString).FirstOrDefault();
|
||||
if (lightningCharge == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
ChargeServiceViewModel vm = new ChargeServiceViewModel();
|
||||
vm.Uri = lightningCharge.ToUri(false).AbsoluteUri;
|
||||
vm.APIToken = lightningCharge.Password;
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(vm.APIToken) && lightningCharge.CookieFilePath != null)
|
||||
externalService = new ExternalService()
|
||||
{
|
||||
if (lightningCharge.CookieFilePath != "fake")
|
||||
vm.APIToken = await System.IO.File.ReadAllTextAsync(lightningCharge.CookieFilePath);
|
||||
else
|
||||
vm.APIToken = "fake";
|
||||
}
|
||||
var builder = new UriBuilder(lightningCharge.ToUri(false));
|
||||
builder.UserName = "api-token";
|
||||
builder.Password = vm.APIToken;
|
||||
vm.AuthenticatedUri = builder.ToString();
|
||||
CryptoCode = torService.Network.CryptoCode,
|
||||
DisplayName = "Full node P2P",
|
||||
Type = ExternalServiceTypes.P2P,
|
||||
ConnectionString = new ExternalConnectionString(new Uri($"bitcoin-p2p://{torService.OnionHost}:{torService.VirtualPort}", UriKind.Absolute)),
|
||||
ServiceName = torService.Name,
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Error: {ex.Message}";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
return View(vm);
|
||||
return externalService != null;
|
||||
}
|
||||
|
||||
[Route("server/services/spark/{cryptoCode}/{index}")]
|
||||
public async Task<IActionResult> SparkService(string cryptoCode, int index, bool showQR = false)
|
||||
private ExternalService GetService(string serviceName, string cryptoCode)
|
||||
{
|
||||
return await LightningWalletServicesCore<ExternalSpark>(cryptoCode, showQR, "Spark Wallet");
|
||||
var result = _Options.ExternalServices.GetService(serviceName, cryptoCode);
|
||||
if (result != null)
|
||||
return result;
|
||||
_torServices.Services.FirstOrDefault(s => TryParseAsExternalService(s, out result));
|
||||
return result;
|
||||
}
|
||||
[Route("server/services/rtl/{cryptoCode}/{index}")]
|
||||
public async Task<IActionResult> RTLService(string cryptoCode, int index, bool showQR = false)
|
||||
{
|
||||
return await LightningWalletServicesCore<ExternalRTL>(cryptoCode, showQR, "Ride the Lightning Wallet");
|
||||
}
|
||||
private async Task<IActionResult> LightningWalletServicesCore<T>(string cryptoCode, bool showQR, string walletName) where T : ExternalService, IAccessKeyService
|
||||
|
||||
[Route("server/services/{serviceName}/{cryptoCode}")]
|
||||
public async Task<IActionResult> Service(string serviceName, string cryptoCode, bool showQR = false, uint? nonce = null)
|
||||
{
|
||||
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
|
||||
{
|
||||
StatusMessage = $"Error: {cryptoCode} is not fully synched";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
var external = _Options.ExternalServicesByCryptoCode.GetServices<T>(cryptoCode).Where(c => c?.ConnectionString?.Server != null).FirstOrDefault();
|
||||
if (external == null)
|
||||
{
|
||||
var service = GetService(serviceName, cryptoCode);
|
||||
if (service == null)
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
LightningWalletServices vm = new LightningWalletServices();
|
||||
vm.ShowQR = showQR;
|
||||
vm.WalletName = walletName;
|
||||
try
|
||||
{
|
||||
vm.ServiceLink = $"{external.ConnectionString.Server.AbsoluteUri}?access-key={await external.ExtractAccessKey()}";
|
||||
if (service.Type == ExternalServiceTypes.P2P)
|
||||
{
|
||||
return View("P2PService", new LightningWalletServices()
|
||||
{
|
||||
ShowQR = showQR,
|
||||
WalletName = service.ServiceName,
|
||||
ServiceLink = service.ConnectionString.Server.AbsoluteUri.WithoutEndingSlash()
|
||||
});
|
||||
}
|
||||
var connectionString = await service.ConnectionString.Expand(this.Request.GetAbsoluteUriNoPathBase(), service.Type);
|
||||
switch (service.Type)
|
||||
{
|
||||
case ExternalServiceTypes.Charge:
|
||||
return LightningChargeServices(service, connectionString, showQR);
|
||||
case ExternalServiceTypes.RTL:
|
||||
case ExternalServiceTypes.Spark:
|
||||
if (connectionString.AccessKey == null)
|
||||
{
|
||||
StatusMessage = $"Error: The access key of the service is not set";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
LightningWalletServices vm = new LightningWalletServices();
|
||||
vm.ShowQR = showQR;
|
||||
vm.WalletName = service.DisplayName;
|
||||
vm.ServiceLink = $"{connectionString.Server}?access-key={connectionString.AccessKey}";
|
||||
return View("LightningWalletServices", vm);
|
||||
case ExternalServiceTypes.LNDGRPC:
|
||||
case ExternalServiceTypes.LNDRest:
|
||||
return LndServices(service, connectionString, nonce);
|
||||
default:
|
||||
throw new NotSupportedException(service.Type.ToString());
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Error: {ex.Message}";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
return View("LightningWalletServices", vm);
|
||||
}
|
||||
|
||||
[Route("server/services/lnd/{cryptoCode}/{index}")]
|
||||
public async Task<IActionResult> LndServices(string cryptoCode, int index, uint? nonce)
|
||||
private IActionResult LightningChargeServices(ExternalService service, ExternalConnectionString connectionString, bool showQR = false)
|
||||
{
|
||||
ChargeServiceViewModel vm = new ChargeServiceViewModel();
|
||||
vm.Uri = connectionString.Server.AbsoluteUri;
|
||||
vm.APIToken = connectionString.APIToken;
|
||||
var builder = new UriBuilder(connectionString.Server);
|
||||
builder.UserName = "api-token";
|
||||
builder.Password = vm.APIToken;
|
||||
vm.AuthenticatedUri = builder.ToString();
|
||||
return View(nameof(LightningChargeServices), vm);
|
||||
}
|
||||
|
||||
private IActionResult LndServices(ExternalService service, ExternalConnectionString connectionString, uint? nonce)
|
||||
{
|
||||
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
|
||||
{
|
||||
StatusMessage = $"Error: {cryptoCode} is not fully synched";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
var external = GetExternalLndConnectionString(cryptoCode, index);
|
||||
if (external == null)
|
||||
return NotFound();
|
||||
var model = new LndGrpcServicesViewModel();
|
||||
if (external.ConnectionType == LightningConnectionType.LndGRPC)
|
||||
if (service.Type == ExternalServiceTypes.LNDGRPC)
|
||||
{
|
||||
model.Host = $"{external.BaseUri.DnsSafeHost}:{external.BaseUri.Port}";
|
||||
model.SSL = external.BaseUri.Scheme == "https";
|
||||
model.Host = $"{connectionString.Server.DnsSafeHost}:{connectionString.Server.Port}";
|
||||
model.SSL = connectionString.Server.Scheme == "https";
|
||||
model.ConnectionType = "GRPC";
|
||||
model.GRPCSSLCipherSuites = "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256";
|
||||
}
|
||||
else if (external.ConnectionType == LightningConnectionType.LndREST)
|
||||
else if (service.Type == ExternalServiceTypes.LNDRest)
|
||||
{
|
||||
model.Uri = external.BaseUri.AbsoluteUri;
|
||||
model.Uri = connectionString.Server.AbsoluteUri;
|
||||
model.ConnectionType = "REST";
|
||||
}
|
||||
|
||||
if (external.CertificateThumbprint != null)
|
||||
if (connectionString.CertificateThumbprint != null)
|
||||
{
|
||||
model.CertificateThumbprint = Encoders.Hex.EncodeData(external.CertificateThumbprint);
|
||||
model.CertificateThumbprint = connectionString.CertificateThumbprint;
|
||||
}
|
||||
if (external.Macaroon != null)
|
||||
if (connectionString.Macaroon != null)
|
||||
{
|
||||
model.Macaroon = Encoders.Hex.EncodeData(external.Macaroon);
|
||||
model.Macaroon = Encoders.Hex.EncodeData(connectionString.Macaroon);
|
||||
}
|
||||
var macaroons = external.MacaroonDirectoryPath == null ? null : await Macaroons.GetFromDirectoryAsync(external.MacaroonDirectoryPath);
|
||||
model.AdminMacaroon = macaroons?.AdminMacaroon?.Hex;
|
||||
model.InvoiceMacaroon = macaroons?.InvoiceMacaroon?.Hex;
|
||||
model.ReadonlyMacaroon = macaroons?.ReadonlyMacaroon?.Hex;
|
||||
model.AdminMacaroon = connectionString.Macaroons?.AdminMacaroon?.Hex;
|
||||
model.InvoiceMacaroon = connectionString.Macaroons?.InvoiceMacaroon?.Hex;
|
||||
model.ReadonlyMacaroon = connectionString.Macaroons?.ReadonlyMacaroon?.Hex;
|
||||
|
||||
if (nonce != null)
|
||||
{
|
||||
var configKey = GetConfigKey("lnd", cryptoCode, index, nonce.Value);
|
||||
var configKey = GetConfigKey("lnd", service.ServiceName, service.CryptoCode, nonce.Value);
|
||||
var lnConfig = _LnConfigProvider.GetConfig(configKey);
|
||||
if (lnConfig != null)
|
||||
{
|
||||
model.QRCodeLink = $"{this.Request.GetAbsoluteRoot().WithTrailingSlash()}lnd-config/{configKey}/lnd.config";
|
||||
model.QRCodeLink = Request.GetAbsoluteUri(Url.Action(nameof(GetLNDConfig), new { configKey = configKey }));
|
||||
model.QRCode = $"config={model.QRCodeLink}";
|
||||
}
|
||||
}
|
||||
|
||||
return View(model);
|
||||
return View(nameof(LndServices), model);
|
||||
}
|
||||
|
||||
private static uint GetConfigKey(string type, string cryptoCode, int index, uint nonce)
|
||||
private static uint GetConfigKey(string type, string serviceName, string cryptoCode, uint nonce)
|
||||
{
|
||||
return (uint)HashCode.Combine(type, cryptoCode, index, nonce);
|
||||
return (uint)HashCode.Combine(type, serviceName, cryptoCode, nonce);
|
||||
}
|
||||
|
||||
[Route("lnd-config/{configKey}/lnd.config")]
|
||||
@ -663,68 +704,62 @@ namespace BTCPayServer.Controllers
|
||||
return Json(conf);
|
||||
}
|
||||
|
||||
[Route("server/services/lnd/{cryptoCode}/{index}")]
|
||||
[Route("server/services/{serviceName}/{cryptoCode}")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> LndServicesPost(string cryptoCode, int index)
|
||||
public async Task<IActionResult> ServicePost(string serviceName, string cryptoCode)
|
||||
{
|
||||
var external = GetExternalLndConnectionString(cryptoCode, index);
|
||||
if (external == null)
|
||||
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
|
||||
{
|
||||
StatusMessage = $"Error: {cryptoCode} is not fully synched";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
var service = GetService(serviceName, cryptoCode);
|
||||
if (service == null)
|
||||
return NotFound();
|
||||
|
||||
ExternalConnectionString connectionString = null;
|
||||
try
|
||||
{
|
||||
connectionString = await service.ConnectionString.Expand(this.Request.GetAbsoluteUriNoPathBase(), service.Type);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Error: {ex.Message}";
|
||||
return RedirectToAction(nameof(Services));
|
||||
}
|
||||
|
||||
LightningConfigurations confs = new LightningConfigurations();
|
||||
var macaroons = external.MacaroonDirectoryPath == null ? null : await Macaroons.GetFromDirectoryAsync(external.MacaroonDirectoryPath);
|
||||
if (external.ConnectionType == LightningConnectionType.LndGRPC)
|
||||
if (service.Type == ExternalServiceTypes.LNDGRPC)
|
||||
{
|
||||
LightningConfiguration grpcConf = new LightningConfiguration();
|
||||
grpcConf.Type = "grpc";
|
||||
grpcConf.Host = external.BaseUri.DnsSafeHost;
|
||||
grpcConf.Port = external.BaseUri.Port;
|
||||
grpcConf.SSL = external.BaseUri.Scheme == "https";
|
||||
grpcConf.Host = connectionString.Server.DnsSafeHost;
|
||||
grpcConf.Port = connectionString.Server.Port;
|
||||
grpcConf.SSL = connectionString.Server.Scheme == "https";
|
||||
confs.Configurations.Add(grpcConf);
|
||||
}
|
||||
else if (external.ConnectionType == LightningConnectionType.LndREST)
|
||||
else if (service.Type == ExternalServiceTypes.LNDRest)
|
||||
{
|
||||
var restconf = new LNDRestConfiguration();
|
||||
restconf.Type = "lnd-rest";
|
||||
restconf.Uri = external.BaseUri.AbsoluteUri;
|
||||
restconf.Uri = connectionString.Server.AbsoluteUri;
|
||||
confs.Configurations.Add(restconf);
|
||||
}
|
||||
else
|
||||
throw new NotSupportedException(external.ConnectionType.ToString());
|
||||
throw new NotSupportedException(service.Type.ToString());
|
||||
var commonConf = (LNDConfiguration)confs.Configurations[confs.Configurations.Count - 1];
|
||||
commonConf.ChainType = _Options.NetworkType.ToString();
|
||||
commonConf.CryptoCode = cryptoCode;
|
||||
commonConf.Macaroon = external.Macaroon == null ? null : Encoders.Hex.EncodeData(external.Macaroon);
|
||||
commonConf.CertificateThumbprint = external.CertificateThumbprint == null ? null : Encoders.Hex.EncodeData(external.CertificateThumbprint);
|
||||
commonConf.AdminMacaroon = macaroons?.AdminMacaroon?.Hex;
|
||||
commonConf.ReadonlyMacaroon = macaroons?.ReadonlyMacaroon?.Hex;
|
||||
commonConf.InvoiceMacaroon = macaroons?.InvoiceMacaroon?.Hex;
|
||||
commonConf.Macaroon = connectionString.Macaroon == null ? null : Encoders.Hex.EncodeData(connectionString.Macaroon);
|
||||
commonConf.CertificateThumbprint = connectionString.CertificateThumbprint == null ? null : connectionString.CertificateThumbprint;
|
||||
commonConf.AdminMacaroon = connectionString.Macaroons?.AdminMacaroon?.Hex;
|
||||
commonConf.ReadonlyMacaroon = connectionString.Macaroons?.ReadonlyMacaroon?.Hex;
|
||||
commonConf.InvoiceMacaroon = connectionString.Macaroons?.InvoiceMacaroon?.Hex;
|
||||
|
||||
var nonce = RandomUtils.GetUInt32();
|
||||
var configKey = GetConfigKey("lnd", cryptoCode, index, nonce);
|
||||
var configKey = GetConfigKey("lnd", serviceName, cryptoCode, nonce);
|
||||
_LnConfigProvider.KeepConfig(configKey, confs);
|
||||
return RedirectToAction(nameof(LndServices), new { cryptoCode = cryptoCode, nonce = nonce });
|
||||
}
|
||||
|
||||
private LightningConnectionString GetExternalLndConnectionString(string cryptoCode, int index)
|
||||
{
|
||||
var connectionString = _Options.ExternalServicesByCryptoCode.GetServices<ExternalLnd>(cryptoCode).Skip(index).Select(c => c.ConnectionString).FirstOrDefault();
|
||||
if (connectionString == null)
|
||||
return null;
|
||||
connectionString = connectionString.Clone();
|
||||
if (connectionString.MacaroonFilePath != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
connectionString.Macaroon = System.IO.File.ReadAllBytes(connectionString.MacaroonFilePath);
|
||||
connectionString.MacaroonFilePath = null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
Logs.Configuration.LogWarning($"{cryptoCode}: The macaroon file path of the external LND grpc config was not found ({connectionString.MacaroonFilePath})");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return connectionString;
|
||||
return RedirectToAction(nameof(Service), new { cryptoCode = cryptoCode, serviceName = serviceName, nonce = nonce });
|
||||
}
|
||||
|
||||
[Route("server/services/ssh")]
|
||||
@ -739,9 +774,11 @@ namespace BTCPayServer.Controllers
|
||||
return NotFound();
|
||||
return File(System.IO.File.ReadAllBytes(settings.KeyFile), "application/octet-stream", "id_rsa");
|
||||
}
|
||||
|
||||
var server = Extensions.IsLocalNetwork(settings.Server) ? this.Request.Host.Host : settings.Server;
|
||||
SSHServiceViewModel vm = new SSHServiceViewModel();
|
||||
string port = settings.Port == 22 ? "" : $" -p {settings.Port}";
|
||||
vm.CommandLine = $"ssh {settings.Username}@{settings.Server}{port}";
|
||||
vm.CommandLine = $"ssh {settings.Username}@{server}{port}";
|
||||
vm.Password = settings.Password;
|
||||
vm.KeyFilePassword = settings.KeyFilePassword;
|
||||
vm.HasKeyFile = !string.IsNullOrEmpty(settings.KeyFile);
|
||||
@ -763,19 +800,28 @@ namespace BTCPayServer.Controllers
|
||||
return View(settings);
|
||||
}
|
||||
|
||||
|
||||
[Route("server/emails")]
|
||||
public async Task<IActionResult> Emails()
|
||||
{
|
||||
var data = (await _SettingsRepository.GetSettingAsync<EmailSettings>()) ?? new EmailSettings();
|
||||
return View(new EmailsViewModel() { Settings = data });
|
||||
}
|
||||
|
||||
[Route("server/emails")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Emails(EmailsViewModel model, string command)
|
||||
{
|
||||
if (!model.Settings.IsComplete())
|
||||
{
|
||||
model.StatusMessage = "Error: Required fields missing";
|
||||
return View(model);
|
||||
}
|
||||
|
||||
if (command == "Test")
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!model.Settings.IsComplete())
|
||||
{
|
||||
model.StatusMessage = "Error: Required fields missing";
|
||||
return View(model);
|
||||
}
|
||||
var client = model.Settings.CreateSmtpClient();
|
||||
await client.SendMailAsync(model.Settings.From, model.TestEmail, "BTCPay test", "BTCPay test");
|
||||
model.StatusMessage = "Email sent to " + model.TestEmail + ", please, verify you received it";
|
||||
|
24
BTCPayServer/Controllers/StorageController.cs
Normal file
24
BTCPayServer/Controllers/StorageController.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Storage.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Storage
|
||||
{
|
||||
[Route("Storage")]
|
||||
public class StorageController
|
||||
{
|
||||
private readonly FileService _FileService;
|
||||
|
||||
public StorageController(FileService fileService)
|
||||
{
|
||||
_FileService = fileService;
|
||||
}
|
||||
|
||||
[HttpGet("{fileId}")]
|
||||
public async Task<IActionResult> GetFile(string fileId)
|
||||
{
|
||||
var url = await _FileService.GetFileUrl(fileId);
|
||||
return new RedirectResult(url);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,16 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services;
|
||||
using LedgerWallet;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
@ -40,6 +43,14 @@ namespace BTCPayServer.Controllers
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
class GetXPubs
|
||||
{
|
||||
public BitcoinExtPubKey ExtPubKey { get; set; }
|
||||
public DerivationStrategyBase DerivationScheme { get; set; }
|
||||
public HDFingerprint RootFingerprint { get; set; }
|
||||
public string Source { get; set; }
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/derivations/{cryptoCode}/ledger/ws")]
|
||||
public async Task<IActionResult> AddDerivationSchemeLedger(
|
||||
@ -52,7 +63,7 @@ namespace BTCPayServer.Controllers
|
||||
return NotFound();
|
||||
|
||||
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||
var hw = new HardwareWalletService(webSocket);
|
||||
var hw = new LedgerHardwareWalletService(webSocket);
|
||||
object result = null;
|
||||
var network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||
|
||||
@ -70,7 +81,18 @@ namespace BTCPayServer.Controllers
|
||||
var k = KeyPath.Parse(keyPath);
|
||||
if (k.Indexes.Length == 0)
|
||||
throw new FormatException("Invalid key path");
|
||||
var getxpubResult = await hw.GetExtPubKey(network, k, normalOperationTimeout.Token);
|
||||
|
||||
var getxpubResult = new GetXPubs();
|
||||
getxpubResult.ExtPubKey = await hw.GetExtPubKey(network, k, normalOperationTimeout.Token);
|
||||
var segwit = network.NBitcoinNetwork.Consensus.SupportSegwit;
|
||||
var derivation = new DerivationStrategyFactory(network.NBitcoinNetwork).CreateDirectDerivationStrategy(getxpubResult.ExtPubKey, new DerivationStrategyOptions()
|
||||
{
|
||||
P2SH = segwit,
|
||||
Legacy = !segwit
|
||||
});
|
||||
getxpubResult.DerivationScheme = derivation;
|
||||
getxpubResult.RootFingerprint = (await hw.GetExtPubKey(network, new KeyPath(), normalOperationTimeout.Token)).ExtPubKey.PubKey.GetHDFingerPrint();
|
||||
getxpubResult.Source = hw.Device;
|
||||
result = getxpubResult;
|
||||
}
|
||||
}
|
||||
@ -84,7 +106,7 @@ namespace BTCPayServer.Controllers
|
||||
if (result != null)
|
||||
{
|
||||
UTF8Encoding UTF8NOBOM = new UTF8Encoding(false);
|
||||
var bytes = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, MvcJsonOptions.Value.SerializerSettings));
|
||||
var bytes = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, network.NBXplorerNetwork.JsonSerializerSettings));
|
||||
await webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token);
|
||||
}
|
||||
}
|
||||
@ -99,22 +121,30 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
private void SetExistingValues(StoreData store, DerivationSchemeViewModel vm)
|
||||
{
|
||||
vm.DerivationScheme = GetExistingDerivationStrategy(vm.CryptoCode, store)?.DerivationStrategyBase.ToString();
|
||||
var derivation = GetExistingDerivationStrategy(vm.CryptoCode, store);
|
||||
if (derivation != null)
|
||||
{
|
||||
vm.DerivationScheme = derivation.AccountDerivation.ToString();
|
||||
vm.Config = derivation.ToJson();
|
||||
}
|
||||
vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.BTCLike));
|
||||
}
|
||||
|
||||
private DerivationStrategy GetExistingDerivationStrategy(string cryptoCode, StoreData store)
|
||||
private DerivationSchemeSettings GetExistingDerivationStrategy(string cryptoCode, StoreData store)
|
||||
{
|
||||
var id = new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike);
|
||||
var existing = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.OfType<DerivationStrategy>()
|
||||
.OfType<DerivationSchemeSettings>()
|
||||
.FirstOrDefault(d => d.PaymentId == id);
|
||||
return existing;
|
||||
}
|
||||
|
||||
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/derivations/{cryptoCode}")]
|
||||
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm, string cryptoCode)
|
||||
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm,
|
||||
string cryptoCode)
|
||||
{
|
||||
vm.CryptoCode = cryptoCode;
|
||||
var store = HttpContext.GetStoreData();
|
||||
@ -126,45 +156,92 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
vm.RootKeyPath = network.GetRootKeyPath();
|
||||
DerivationSchemeSettings strategy = null;
|
||||
|
||||
var wallet = _WalletProvider.GetWallet(network);
|
||||
if (wallet == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
|
||||
var exisingStrategy = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.Where(c => c.PaymentId == paymentMethodId)
|
||||
.OfType<DerivationStrategy>()
|
||||
.Select(c => c.DerivationStrategyBase.ToString())
|
||||
.FirstOrDefault();
|
||||
DerivationStrategy strategy = null;
|
||||
try
|
||||
if (!string.IsNullOrEmpty(vm.Config))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(vm.DerivationScheme))
|
||||
if (!DerivationSchemeSettings.TryParseFromJson(vm.Config, network, out strategy))
|
||||
{
|
||||
strategy = ParseDerivationStrategy(vm.DerivationScheme, null, network);
|
||||
vm.DerivationScheme = strategy.ToString();
|
||||
vm.StatusMessage = new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = "Config file was not in the correct format"
|
||||
}.ToString();
|
||||
vm.Confirmation = false;
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
catch
|
||||
|
||||
if (vm.ColdcardPublicFile != null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
|
||||
vm.Confirmation = false;
|
||||
return View(vm);
|
||||
if (!DerivationSchemeSettings.TryParseFromColdcard(await ReadAllText(vm.ColdcardPublicFile), network, out strategy))
|
||||
{
|
||||
vm.StatusMessage = new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = "Coldcard public file was not in the correct format"
|
||||
}.ToString();
|
||||
vm.Confirmation = false;
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(vm.DerivationScheme))
|
||||
{
|
||||
var newStrategy = ParseDerivationStrategy(vm.DerivationScheme, null, network);
|
||||
if (newStrategy.AccountDerivation != strategy?.AccountDerivation)
|
||||
{
|
||||
strategy = newStrategy;
|
||||
strategy.AccountKeyPath = vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath);
|
||||
strategy.RootFingerprint = string.IsNullOrEmpty(vm.RootFingerprint) ? (HDFingerprint?)null : new HDFingerprint(NBitcoin.DataEncoders.Encoders.Hex.DecodeData(vm.RootFingerprint));
|
||||
strategy.ExplicitAccountKey = string.IsNullOrEmpty(vm.AccountKey) ? null : new BitcoinExtPubKey(vm.AccountKey, network.NBitcoinNetwork);
|
||||
strategy.Source = vm.Source;
|
||||
vm.DerivationScheme = strategy.AccountDerivation.ToString();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
strategy = null;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
|
||||
vm.Confirmation = false;
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
var oldConfig = vm.Config;
|
||||
vm.Config = strategy == null ? null : strategy.ToJson();
|
||||
|
||||
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
|
||||
var exisingStrategy = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.Where(c => c.PaymentId == paymentMethodId)
|
||||
.OfType<DerivationSchemeSettings>()
|
||||
.FirstOrDefault();
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var wasExcluded = storeBlob.GetExcludedPaymentMethods().Match(paymentMethodId);
|
||||
var willBeExcluded = !vm.Enabled;
|
||||
|
||||
var showAddress = // Show addresses if:
|
||||
// - If the user is testing the hint address in confirmation screen
|
||||
(vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress)) ||
|
||||
// - The user is setting a new derivation scheme
|
||||
(!vm.Confirmation && strategy != null && exisingStrategy != strategy.DerivationStrategyBase.ToString()) ||
|
||||
// - The user is clicking on continue without changing anything
|
||||
(!vm.Confirmation && willBeExcluded == wasExcluded);
|
||||
// - If the user is testing the hint address in confirmation screen
|
||||
(vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress)) ||
|
||||
// - The user is clicking on continue after changing the config
|
||||
(!vm.Confirmation && oldConfig != vm.Config) ||
|
||||
// - The user is clickingon continue without changing config nor enabling/disabling
|
||||
(!vm.Confirmation && oldConfig == vm.Config && willBeExcluded == wasExcluded);
|
||||
|
||||
showAddress = showAddress && strategy != null;
|
||||
if (!showAddress)
|
||||
@ -172,10 +249,9 @@ namespace BTCPayServer.Controllers
|
||||
try
|
||||
{
|
||||
if (strategy != null)
|
||||
await wallet.TrackAsync(strategy.DerivationStrategyBase);
|
||||
await wallet.TrackAsync(strategy.AccountDerivation);
|
||||
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
|
||||
storeBlob.SetExcluded(paymentMethodId, willBeExcluded);
|
||||
storeBlob.SetWalletKeyPathRoot(paymentMethodId, vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath));
|
||||
storeBlob.SetExcluded(paymentMethodId, willBeExcluded);
|
||||
store.SetStoreBlob(storeBlob);
|
||||
}
|
||||
catch
|
||||
@ -185,8 +261,14 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
await _Repo.UpdateStore(store);
|
||||
StatusMessage = $"Derivation scheme for {network.CryptoCode} has been modified.";
|
||||
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
|
||||
if (oldConfig != vm.Config)
|
||||
StatusMessage = $"Derivation settings for {network.CryptoCode} has been modified.";
|
||||
if (willBeExcluded != wasExcluded)
|
||||
{
|
||||
var label = willBeExcluded ? "disabled" : "enabled";
|
||||
StatusMessage = $"On-Chain payments for {network.CryptoCode} has been {label}.";
|
||||
}
|
||||
return RedirectToAction(nameof(UpdateStore), new {storeId = storeId});
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(vm.HintAddress))
|
||||
{
|
||||
@ -203,27 +285,43 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
try
|
||||
{
|
||||
strategy = ParseDerivationStrategy(vm.DerivationScheme, address.ScriptPubKey, network);
|
||||
var newStrategy = ParseDerivationStrategy(vm.DerivationScheme, address.ScriptPubKey, network);
|
||||
if (newStrategy.AccountDerivation != strategy.AccountDerivation)
|
||||
{
|
||||
strategy.AccountDerivation = newStrategy.AccountDerivation;
|
||||
strategy.AccountOriginal = null;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.HintAddress), "Impossible to find a match with this address");
|
||||
return ShowAddresses(vm, strategy);
|
||||
}
|
||||
|
||||
vm.HintAddress = "";
|
||||
vm.StatusMessage = "Address successfully found, please verify that the rest is correct and click on \"Confirm\"";
|
||||
vm.StatusMessage =
|
||||
"Address successfully found, please verify that the rest is correct and click on \"Confirm\"";
|
||||
ModelState.Remove(nameof(vm.HintAddress));
|
||||
ModelState.Remove(nameof(vm.DerivationScheme));
|
||||
}
|
||||
|
||||
return ShowAddresses(vm, strategy);
|
||||
}
|
||||
|
||||
private IActionResult ShowAddresses(DerivationSchemeViewModel vm, DerivationStrategy strategy)
|
||||
private async Task<string> ReadAllText(IFormFile file)
|
||||
{
|
||||
vm.DerivationScheme = strategy.DerivationStrategyBase.ToString();
|
||||
using (var stream = new StreamReader(file.OpenReadStream()))
|
||||
{
|
||||
return await stream.ReadToEndAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private IActionResult ShowAddresses(DerivationSchemeViewModel vm, DerivationSchemeSettings strategy)
|
||||
{
|
||||
vm.DerivationScheme = strategy.AccountDerivation.ToString();
|
||||
if (!string.IsNullOrEmpty(vm.DerivationScheme))
|
||||
{
|
||||
var line = strategy.DerivationStrategyBase.GetLineFor(DerivationFeature.Deposit);
|
||||
var line = strategy.AccountDerivation.GetLineFor(DerivationFeature.Deposit);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
@ -77,7 +78,7 @@ namespace BTCPayServer.Controllers
|
||||
case "test":
|
||||
try
|
||||
{
|
||||
var client = new Changelly(_httpClientFactory, changellySettings.ApiKey, changellySettings.ApiSecret,
|
||||
var client = new Changelly(_httpClientFactory.CreateClient(), changellySettings.ApiKey, changellySettings.ApiSecret,
|
||||
changellySettings.ApiUrl);
|
||||
var result = await client.GetCurrenciesFull();
|
||||
vm.StatusMessage = "Test Successful";
|
||||
|
@ -28,6 +28,7 @@ namespace BTCPayServer.Controllers
|
||||
vm.MerchantId = existing.MerchantId;
|
||||
vm.Enabled = existing.Enabled;
|
||||
vm.Mode = existing.Mode;
|
||||
vm.AmountMarkupPercentage = existing.AmountMarkupPercentage;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@ -50,7 +51,8 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
MerchantId = vm.MerchantId,
|
||||
Enabled = vm.Enabled,
|
||||
Mode = vm.Mode
|
||||
Mode = vm.Mode,
|
||||
AmountMarkupPercentage = vm.AmountMarkupPercentage
|
||||
};
|
||||
|
||||
switch (command)
|
||||
|
@ -155,7 +155,7 @@ namespace BTCPayServer.Controllers
|
||||
var handler = (LightningLikePaymentHandler)_ServiceProvider.GetRequiredService<IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>>();
|
||||
try
|
||||
{
|
||||
var info = await handler.GetNodeInfo(paymentMethod, network);
|
||||
var info = await handler.GetNodeInfo(this.Request.IsOnion(), paymentMethod, network);
|
||||
if (!vm.SkipPortTest)
|
||||
{
|
||||
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)))
|
||||
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Authentication;
|
||||
using BTCPayServer.Configuration;
|
||||
@ -188,24 +189,39 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/rates")]
|
||||
public IActionResult Rates()
|
||||
public IActionResult Rates(string storeId)
|
||||
{
|
||||
var storeBlob = StoreData.GetStoreBlob();
|
||||
var vm = new RatesViewModel();
|
||||
vm.SetExchangeRates(GetSupportedExchanges(), storeBlob.PreferredExchange ?? CoinAverageRateProvider.CoinAverageName);
|
||||
vm.Spread = (double)(storeBlob.Spread * 100m);
|
||||
vm.StoreId = storeId;
|
||||
vm.Script = storeBlob.GetRateRules(_NetworkProvider).ToString();
|
||||
vm.DefaultScript = storeBlob.GetDefaultRateRules(_NetworkProvider).ToString();
|
||||
vm.AvailableExchanges = GetSupportedExchanges();
|
||||
vm.DefaultCurrencyPairs = storeBlob.GetDefaultCurrencyPairString();
|
||||
vm.ShowScripting = storeBlob.RateScripting;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/rates")]
|
||||
public async Task<IActionResult> Rates(RatesViewModel model, string command = null)
|
||||
public async Task<IActionResult> Rates(RatesViewModel model, string command = null, string storeId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
model.SetExchangeRates(GetSupportedExchanges(), model.PreferredExchange);
|
||||
model.StoreId = storeId ?? model.StoreId;
|
||||
CurrencyPair[] currencyPairs = null;
|
||||
try
|
||||
{
|
||||
currencyPairs = model.DefaultCurrencyPairs?
|
||||
.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(p => CurrencyPair.Parse(p))
|
||||
.ToArray();
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.DefaultCurrencyPairs), "Invalid currency pairs (should be for example: BTC_USD,BTC_CAD,BTC_JPY)");
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
@ -219,7 +235,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
blob.PreferredExchange = model.PreferredExchange;
|
||||
blob.Spread = (decimal)model.Spread / 100.0m;
|
||||
|
||||
blob.DefaultCurrencyPairs = currencyPairs;
|
||||
if (!model.ShowScripting)
|
||||
{
|
||||
if (!GetSupportedExchanges().Select(c => c.Name).Contains(blob.PreferredExchange, StringComparer.OrdinalIgnoreCase))
|
||||
@ -267,7 +283,7 @@ namespace BTCPayServer.Controllers
|
||||
pairs.Add(currencyPair);
|
||||
}
|
||||
|
||||
var fetchs = _RateFactory.FetchRates(pairs.ToHashSet(), rules);
|
||||
var fetchs = _RateFactory.FetchRates(pairs.ToHashSet(), rules, cancellationToken);
|
||||
var testResults = new List<RatesViewModel.TestResultViewModel>();
|
||||
foreach (var fetch in fetchs)
|
||||
{
|
||||
@ -332,13 +348,15 @@ namespace BTCPayServer.Controllers
|
||||
var storeBlob = StoreData.GetStoreBlob();
|
||||
var vm = new CheckoutExperienceViewModel();
|
||||
SetCryptoCurrencies(vm, StoreData);
|
||||
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
|
||||
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
|
||||
vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? "";
|
||||
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
|
||||
vm.CustomCSS = storeBlob.CustomCSS?.AbsoluteUri;
|
||||
vm.CustomLogo = storeBlob.CustomLogo?.AbsoluteUri;
|
||||
vm.HtmlTitle = storeBlob.HtmlTitle;
|
||||
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
|
||||
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
|
||||
vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? "";
|
||||
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
|
||||
vm.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi;
|
||||
vm.RedirectAutomatically = storeBlob.RedirectAutomatically;
|
||||
return View(vm);
|
||||
}
|
||||
void SetCryptoCurrencies(CheckoutExperienceViewModel vm, Data.StoreData storeData)
|
||||
@ -395,13 +413,15 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
blob.DefaultLang = model.DefaultLang;
|
||||
blob.RequiresRefundEmail = model.RequiresRefundEmail;
|
||||
blob.LightningMaxValue = lightningMaxValue;
|
||||
blob.OnChainMinValue = onchainMinValue;
|
||||
blob.CustomLogo = string.IsNullOrWhiteSpace(model.CustomLogo) ? null : new Uri(model.CustomLogo, UriKind.Absolute);
|
||||
blob.CustomCSS = string.IsNullOrWhiteSpace(model.CustomCSS) ? null : new Uri(model.CustomCSS, UriKind.Absolute);
|
||||
blob.HtmlTitle = string.IsNullOrWhiteSpace(model.HtmlTitle) ? null : model.HtmlTitle;
|
||||
blob.DefaultLang = model.DefaultLang;
|
||||
blob.RequiresRefundEmail = model.RequiresRefundEmail;
|
||||
blob.OnChainMinValue = onchainMinValue;
|
||||
blob.LightningMaxValue = lightningMaxValue;
|
||||
blob.LightningAmountInSatoshi = model.LightningAmountInSatoshi;
|
||||
blob.RedirectAutomatically = model.RedirectAutomatically;
|
||||
if (StoreData.SetStoreBlob(blob))
|
||||
{
|
||||
needUpdate = true;
|
||||
@ -450,7 +470,7 @@ namespace BTCPayServer.Controllers
|
||||
var derivationByCryptoCode =
|
||||
store
|
||||
.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.OfType<DerivationStrategy>()
|
||||
.OfType<DerivationSchemeSettings>()
|
||||
.ToDictionary(c => c.Network.CryptoCode);
|
||||
foreach (var network in _NetworkProvider.GetAll())
|
||||
{
|
||||
@ -458,7 +478,7 @@ namespace BTCPayServer.Controllers
|
||||
vm.DerivationSchemes.Add(new StoreViewModel.DerivationScheme()
|
||||
{
|
||||
Crypto = network.CryptoCode,
|
||||
Value = strategy?.DerivationStrategyBase?.ToString() ?? string.Empty,
|
||||
Value = strategy?.ToPrettyString() ?? string.Empty,
|
||||
WalletId = new WalletId(store.Id, network.CryptoCode),
|
||||
Enabled = !excludeFilters.Match(new Payments.PaymentMethodId(network.CryptoCode, Payments.PaymentTypes.BTCLike))
|
||||
});
|
||||
@ -489,7 +509,7 @@ namespace BTCPayServer.Controllers
|
||||
Action = nameof(UpdateChangellySettings),
|
||||
Provider = "Changelly"
|
||||
});
|
||||
|
||||
|
||||
var coinSwitchEnabled = storeBlob.CoinSwitchSettings != null && storeBlob.CoinSwitchSettings.Enabled;
|
||||
vm.ThirdPartyPaymentMethods.Add(new StoreViewModel.ThirdPartyPaymentMethod()
|
||||
{
|
||||
@ -576,11 +596,11 @@ namespace BTCPayServer.Controllers
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private DerivationStrategy ParseDerivationStrategy(string derivationScheme, Script hint, BTCPayNetwork network)
|
||||
private DerivationSchemeSettings ParseDerivationStrategy(string derivationScheme, Script hint, BTCPayNetwork network)
|
||||
{
|
||||
var parser = new DerivationSchemeParser(network.NBitcoinNetwork);
|
||||
var parser = new DerivationSchemeParser(network);
|
||||
parser.HintScriptPubKey = hint;
|
||||
return new DerivationStrategy(parser.Parse(derivationScheme), network);
|
||||
return new DerivationSchemeSettings(parser.Parse(derivationScheme), network);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -593,7 +613,6 @@ namespace BTCPayServer.Controllers
|
||||
model.StoreNotConfigured = StoreNotConfigured;
|
||||
model.Tokens = tokens.Select(t => new TokenViewModel()
|
||||
{
|
||||
Facade = t.Facade,
|
||||
Label = t.Label,
|
||||
SIN = t.SIN,
|
||||
Id = t.Value
|
||||
@ -678,7 +697,6 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var tokenRequest = new TokenRequest()
|
||||
{
|
||||
Facade = model.Facade,
|
||||
Label = model.Label,
|
||||
Id = model.PublicKey == null ? null : NBitpayClient.Extensions.BitIdExtensions.GetBitIDSIN(new PubKey(model.PublicKey))
|
||||
};
|
||||
@ -690,7 +708,6 @@ namespace BTCPayServer.Controllers
|
||||
await _TokenRepository.UpdatePairingCode(new PairingCodeEntity()
|
||||
{
|
||||
Id = tokenRequest.PairingCode,
|
||||
Facade = model.Facade,
|
||||
Label = model.Label,
|
||||
});
|
||||
await _TokenRepository.PairWithStoreAsync(tokenRequest.PairingCode, storeId);
|
||||
@ -730,7 +747,6 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
}
|
||||
var model = new CreateTokenViewModel();
|
||||
model.Facade = "merchant";
|
||||
ViewBag.HidePublicKey = storeId == null;
|
||||
ViewBag.ShowStores = storeId == null;
|
||||
ViewBag.ShowMenu = storeId != null;
|
||||
@ -782,7 +798,6 @@ namespace BTCPayServer.Controllers
|
||||
return View(new PairingModel()
|
||||
{
|
||||
Id = pairing.Id,
|
||||
Facade = pairing.Facade,
|
||||
Label = pairing.Label,
|
||||
SIN = pairing.SIN ?? "Server-Initiated Pairing",
|
||||
SelectedStore = selectedStore ?? stores.FirstOrDefault()?.Id,
|
||||
@ -873,7 +888,11 @@ namespace BTCPayServer.Controllers
|
||||
ButtonSize = 2,
|
||||
UrlRoot = appUrl,
|
||||
PayButtonImageUrl = appUrl + "img/paybutton/pay.png",
|
||||
StoreId = store.Id
|
||||
StoreId = store.Id,
|
||||
ButtonType = 0,
|
||||
Min = 1,
|
||||
Max = 20,
|
||||
Step = 1
|
||||
};
|
||||
return View(model);
|
||||
}
|
||||
|
215
BTCPayServer/Controllers/WalletsController.PSBT.cs
Normal file
215
BTCPayServer/Controllers/WalletsController.PSBT.cs
Normal file
@ -0,0 +1,215 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.ModelBinders;
|
||||
using BTCPayServer.Models.WalletViewModels;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBXplorer.Models;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class WalletsController
|
||||
{
|
||||
|
||||
[NonAction]
|
||||
public async Task<CreatePSBTResponse> CreatePSBT(BTCPayNetwork network, DerivationSchemeSettings derivationSettings, WalletSendModel sendModel, CancellationToken cancellationToken)
|
||||
{
|
||||
var nbx = ExplorerClientProvider.GetExplorerClient(network);
|
||||
CreatePSBTRequest psbtRequest = new CreatePSBTRequest();
|
||||
CreatePSBTDestination psbtDestination = new CreatePSBTDestination();
|
||||
psbtRequest.Destinations.Add(psbtDestination);
|
||||
if (network.SupportRBF)
|
||||
{
|
||||
psbtRequest.RBF = !sendModel.DisableRBF;
|
||||
}
|
||||
psbtDestination.Destination = BitcoinAddress.Create(sendModel.Destination, network.NBitcoinNetwork);
|
||||
psbtDestination.Amount = Money.Coins(sendModel.Amount.Value);
|
||||
psbtRequest.FeePreference = new FeePreference();
|
||||
psbtRequest.FeePreference.ExplicitFeeRate = new FeeRate(Money.Satoshis(sendModel.FeeSatoshiPerByte), 1);
|
||||
if (sendModel.NoChange)
|
||||
{
|
||||
psbtRequest.ExplicitChangeAddress = psbtDestination.Destination;
|
||||
}
|
||||
psbtDestination.SubstractFees = sendModel.SubstractFees;
|
||||
psbtRequest.RebaseKeyPaths = derivationSettings.GetPSBTRebaseKeyRules().ToList();
|
||||
var psbt = (await nbx.CreatePSBTAsync(derivationSettings.AccountDerivation, psbtRequest, cancellationToken));
|
||||
if (psbt == null)
|
||||
throw new NotSupportedException("You need to update your version of NBXplorer");
|
||||
return psbt;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{walletId}/psbt")]
|
||||
public IActionResult WalletPSBT()
|
||||
{
|
||||
return View(new WalletPSBTViewModel());
|
||||
}
|
||||
[HttpPost]
|
||||
[Route("{walletId}/psbt")]
|
||||
public async Task<IActionResult> WalletPSBT(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId,
|
||||
WalletPSBTViewModel vm, string command = null)
|
||||
{
|
||||
var network = NetworkProvider.GetNetwork(walletId.CryptoCode);
|
||||
var psbt = vm.GetPSBT(network.NBitcoinNetwork);
|
||||
if (psbt == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.PSBT), "Invalid PSBT");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
if (command == null)
|
||||
{
|
||||
vm.Decoded = psbt.ToString();
|
||||
return View(vm);
|
||||
}
|
||||
else if (command == "ledger")
|
||||
{
|
||||
return ViewWalletSendLedger(psbt);
|
||||
}
|
||||
else if (command == "broadcast")
|
||||
{
|
||||
if (!psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors))
|
||||
{
|
||||
return ViewPSBT(psbt, errors);
|
||||
}
|
||||
var transaction = psbt.ExtractTransaction();
|
||||
try
|
||||
{
|
||||
var broadcastResult = await ExplorerClientProvider.GetExplorerClient(network).BroadcastAsync(transaction);
|
||||
if (!broadcastResult.Success)
|
||||
{
|
||||
return ViewPSBT(psbt, new[] { $"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}" });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ViewPSBT(psbt, "Error while broadcasting: " + ex.Message);
|
||||
}
|
||||
return await RedirectToWalletTransaction(walletId, transaction);
|
||||
}
|
||||
else if (command == "combine")
|
||||
{
|
||||
ModelState.Remove(nameof(vm.PSBT));
|
||||
return View(nameof(WalletPSBTCombine), new WalletPSBTCombineViewModel() { OtherPSBT = psbt.ToBase64() });
|
||||
}
|
||||
else
|
||||
{
|
||||
(await GetDerivationSchemeSettings(walletId)).RebaseKeyPaths(psbt);
|
||||
return FilePSBT(psbt, "psbt-export.psbt");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{walletId}/psbt/ready")]
|
||||
public IActionResult WalletPSBTReady(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, string psbt = null)
|
||||
{
|
||||
return View(new WalletPSBTReadyViewModel() { PSBT = psbt });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{walletId}/psbt/ready")]
|
||||
public async Task<IActionResult> WalletPSBTReady(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, WalletPSBTReadyViewModel vm, string command = null)
|
||||
{
|
||||
PSBT psbt = null;
|
||||
var network = NetworkProvider.GetNetwork(walletId.CryptoCode);
|
||||
try
|
||||
{
|
||||
psbt = PSBT.Parse(vm.PSBT, network.NBitcoinNetwork);
|
||||
}
|
||||
catch
|
||||
{
|
||||
vm.Errors = new List<string>();
|
||||
vm.Errors.Add("Invalid PSBT");
|
||||
return View(vm);
|
||||
}
|
||||
if (command == "broadcast")
|
||||
{
|
||||
if (!psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors))
|
||||
{
|
||||
vm.Errors = new List<string>();
|
||||
vm.Errors.AddRange(errors.Select(e => e.ToString()));
|
||||
return View(vm);
|
||||
}
|
||||
var transaction = psbt.ExtractTransaction();
|
||||
try
|
||||
{
|
||||
var broadcastResult = await ExplorerClientProvider.GetExplorerClient(network).BroadcastAsync(transaction);
|
||||
if (!broadcastResult.Success)
|
||||
{
|
||||
vm.Errors = new List<string>();
|
||||
vm.Errors.Add($"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}");
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
vm.Errors = new List<string>();
|
||||
vm.Errors.Add("Error while broadcasting: " + ex.Message);
|
||||
return View(vm);
|
||||
}
|
||||
return await RedirectToWalletTransaction(walletId, transaction);
|
||||
}
|
||||
else if (command == "analyze-psbt")
|
||||
{
|
||||
return ViewPSBT(psbt);
|
||||
}
|
||||
else
|
||||
{
|
||||
vm.Errors = new List<string>();
|
||||
vm.Errors.Add("Unknown command");
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
private IActionResult ViewPSBT<T>(PSBT psbt, IEnumerable<T> errors = null)
|
||||
{
|
||||
return ViewPSBT(psbt, errors?.Select(e => e.ToString()).ToList());
|
||||
}
|
||||
private IActionResult ViewPSBT(PSBT psbt, IEnumerable<string> errors = null)
|
||||
{
|
||||
return View(nameof(WalletPSBT), new WalletPSBTViewModel()
|
||||
{
|
||||
Decoded = psbt.ToString(),
|
||||
PSBT = psbt.ToBase64(),
|
||||
Errors = errors?.ToList()
|
||||
});
|
||||
}
|
||||
|
||||
private IActionResult FilePSBT(PSBT psbt, string fileName)
|
||||
{
|
||||
return File(psbt.ToBytes(), "application/octet-stream", fileName);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{walletId}/psbt/combine")]
|
||||
public async Task<IActionResult> WalletPSBTCombine([ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, WalletPSBTCombineViewModel vm)
|
||||
{
|
||||
var network = NetworkProvider.GetNetwork(walletId.CryptoCode);
|
||||
var psbt = await vm.GetPSBT(network.NBitcoinNetwork);
|
||||
if (psbt == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.PSBT), "Invalid PSBT");
|
||||
return View(vm);
|
||||
}
|
||||
var sourcePSBT = vm.GetSourcePSBT(network.NBitcoinNetwork);
|
||||
if (sourcePSBT == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.OtherPSBT), "Invalid PSBT");
|
||||
return View(vm);
|
||||
}
|
||||
sourcePSBT = sourcePSBT.Combine(psbt);
|
||||
StatusMessage = "PSBT Successfully combined!";
|
||||
return ViewPSBT(sourcePSBT);
|
||||
}
|
||||
}
|
||||
}
|
@ -80,9 +80,9 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var onChainWallets = stores
|
||||
.SelectMany(s => s.GetSupportedPaymentMethods(NetworkProvider)
|
||||
.OfType<DerivationStrategy>()
|
||||
.OfType<DerivationSchemeSettings>()
|
||||
.Select(d => ((Wallet: _walletProvider.GetWallet(d.Network),
|
||||
DerivationStrategy: d.DerivationStrategyBase,
|
||||
DerivationStrategy: d.AccountDerivation,
|
||||
Network: d.Network)))
|
||||
.Where(_ => _.Wallet != null)
|
||||
.Select(_ => (Wallet: _.Wallet,
|
||||
@ -117,13 +117,12 @@ namespace BTCPayServer.Controllers
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId)
|
||||
{
|
||||
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
|
||||
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
|
||||
DerivationSchemeSettings paymentMethod = await GetDerivationSchemeSettings(walletId);
|
||||
if (paymentMethod == null)
|
||||
return NotFound();
|
||||
|
||||
var wallet = _walletProvider.GetWallet(paymentMethod.Network);
|
||||
var transactions = await wallet.FetchTransactions(paymentMethod.DerivationStrategyBase);
|
||||
var transactions = await wallet.FetchTransactions(paymentMethod.AccountDerivation);
|
||||
|
||||
var model = new ListTransactionsViewModel();
|
||||
foreach (var tx in transactions.UnconfirmedTransactions.Transactions.Concat(transactions.ConfirmedTransactions.Transactions))
|
||||
@ -146,12 +145,12 @@ namespace BTCPayServer.Controllers
|
||||
[Route("{walletId}/send")]
|
||||
public async Task<IActionResult> WalletSend(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, string defaultDestination = null, string defaultAmount = null, bool advancedMode = false)
|
||||
WalletId walletId, string defaultDestination = null, string defaultAmount = null)
|
||||
{
|
||||
if (walletId?.StoreId == null)
|
||||
return NotFound();
|
||||
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
|
||||
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
|
||||
DerivationSchemeSettings paymentMethod = GetDerivationSchemeSettings(walletId, store);
|
||||
if (paymentMethod == null)
|
||||
return NotFound();
|
||||
var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode);
|
||||
@ -171,17 +170,17 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var feeProvider = _feeRateProvider.CreateFeeProvider(network);
|
||||
var recommendedFees = feeProvider.GetFeeRateAsync();
|
||||
var balance = _walletProvider.GetWallet(network).GetBalance(paymentMethod.DerivationStrategyBase);
|
||||
var balance = _walletProvider.GetWallet(network).GetBalance(paymentMethod.AccountDerivation);
|
||||
model.CurrentBalance = (await balance).ToDecimal(MoneyUnit.BTC);
|
||||
model.RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi;
|
||||
model.FeeSatoshiPerByte = model.RecommendedSatoshiPerByte;
|
||||
|
||||
model.SupportRBF = network.SupportRBF;
|
||||
using (CancellationTokenSource cts = new CancellationTokenSource())
|
||||
{
|
||||
try
|
||||
{
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(5));
|
||||
var result = await RateFetcher.FetchRate(currencyPair, rateRules).WithCancellation(cts.Token);
|
||||
var result = await RateFetcher.FetchRate(currencyPair, rateRules, cts.Token).WithCancellation(cts.Token);
|
||||
if (result.BidAsk != null)
|
||||
{
|
||||
model.Rate = result.BidAsk.Center;
|
||||
@ -195,7 +194,6 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
catch (Exception ex) { model.RateError = ex.Message; }
|
||||
}
|
||||
model.AdvancedMode = advancedMode;
|
||||
return View(model);
|
||||
}
|
||||
|
||||
@ -203,7 +201,7 @@ namespace BTCPayServer.Controllers
|
||||
[Route("{walletId}/send")]
|
||||
public async Task<IActionResult> WalletSend(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, WalletSendModel vm, string command = null)
|
||||
WalletId walletId, WalletSendModel vm, string command = null, CancellationToken cancellation = default)
|
||||
{
|
||||
if (walletId?.StoreId == null)
|
||||
return NotFound();
|
||||
@ -213,14 +211,7 @@ namespace BTCPayServer.Controllers
|
||||
var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode);
|
||||
if (network == null)
|
||||
return NotFound();
|
||||
|
||||
if (command == "noob" || command == "expert")
|
||||
{
|
||||
ModelState.Clear();
|
||||
vm.AdvancedMode = command == "expert";
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
vm.SupportRBF = network.SupportRBF;
|
||||
var destination = ParseDestination(vm.Destination, network.NBitcoinNetwork);
|
||||
if (destination == null)
|
||||
ModelState.AddModelError(nameof(vm.Destination), "Invalid address");
|
||||
@ -235,32 +226,44 @@ namespace BTCPayServer.Controllers
|
||||
if (!ModelState.IsValid)
|
||||
return View(vm);
|
||||
|
||||
return RedirectToAction(nameof(WalletSendLedger), new WalletSendLedgerModel()
|
||||
DerivationSchemeSettings derivationScheme = await GetDerivationSchemeSettings(walletId);
|
||||
var psbt = await CreatePSBT(network, derivationScheme, vm, cancellation);
|
||||
|
||||
if (command == "ledger")
|
||||
{
|
||||
Destination = vm.Destination,
|
||||
Amount = vm.Amount.Value,
|
||||
SubstractFees = vm.SubstractFees,
|
||||
FeeSatoshiPerByte = vm.FeeSatoshiPerByte,
|
||||
NoChange = vm.NoChange
|
||||
});
|
||||
return ViewWalletSendLedger(psbt.PSBT, psbt.ChangeAddress);
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
if (command == "analyze-psbt")
|
||||
return ViewPSBT(psbt.PSBT);
|
||||
derivationScheme.RebaseKeyPaths(psbt.PSBT);
|
||||
return FilePSBT(psbt.PSBT, $"Send-{vm.Amount.Value}-{network.CryptoCode}-to-{destination[0].ToString()}.psbt");
|
||||
}
|
||||
catch (NBXplorerException ex)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Amount), ex.Error.Message);
|
||||
return View(vm);
|
||||
}
|
||||
catch (NotSupportedException)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Destination), "You need to update your version of NBXplorer");
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{walletId}/send/ledger")]
|
||||
public async Task<IActionResult> WalletSendLedger(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, WalletSendLedgerModel vm)
|
||||
private ViewResult ViewWalletSendLedger(PSBT psbt, BitcoinAddress hintChange = null)
|
||||
{
|
||||
if (walletId?.StoreId == null)
|
||||
return NotFound();
|
||||
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
|
||||
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
|
||||
if (paymentMethod == null)
|
||||
return NotFound();
|
||||
var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode);
|
||||
if (network == null)
|
||||
return NotFound();
|
||||
return View(vm);
|
||||
return View("WalletSendLedger", new WalletSendLedgerModel()
|
||||
{
|
||||
PSBT = psbt.ToBase64(),
|
||||
HintChange = hintChange?.ToString(),
|
||||
WebsocketPath = this.Url.Action(nameof(LedgerConnection)),
|
||||
SuccessPath = this.Url.Action(nameof(WalletPSBTReady))
|
||||
});
|
||||
}
|
||||
|
||||
private IDestination[] ParseDestination(string destination, Network network)
|
||||
@ -276,6 +279,19 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IActionResult> RedirectToWalletTransaction(WalletId walletId, Transaction transaction)
|
||||
{
|
||||
var network = NetworkProvider.GetNetwork(walletId.CryptoCode);
|
||||
if (transaction != null)
|
||||
{
|
||||
var wallet = _walletProvider.GetWallet(network);
|
||||
var derivationSettings = await GetDerivationSchemeSettings(walletId);
|
||||
wallet.InvalidateCache(derivationSettings.AccountDerivation);
|
||||
StatusMessage = $"Transaction broadcasted successfully ({transaction.GetHash().ToString()})";
|
||||
}
|
||||
return RedirectToAction(nameof(WalletTransactions));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{walletId}/rescan")]
|
||||
public async Task<IActionResult> WalletRescan(
|
||||
@ -284,20 +300,16 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (walletId?.StoreId == null)
|
||||
return NotFound();
|
||||
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
|
||||
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
|
||||
DerivationSchemeSettings paymentMethod = await GetDerivationSchemeSettings(walletId);
|
||||
if (paymentMethod == null)
|
||||
return NotFound();
|
||||
|
||||
var vm = new RescanWalletModel();
|
||||
vm.IsFullySync = _dashboard.IsFullySynched(walletId.CryptoCode, out var unused);
|
||||
// We need to ensure it is segwit,
|
||||
// because hardware wallet support need the parent transactions to sign, which NBXplorer don't have. (Nor does a pruned node)
|
||||
vm.IsSegwit = paymentMethod.DerivationStrategyBase.IsSegwit();
|
||||
vm.IsServerAdmin = User.Claims.Any(c => c.Type == Policies.CanModifyServerSettings.Key && c.Value == "true");
|
||||
vm.IsSupportedByCurrency = _dashboard.Get(walletId.CryptoCode)?.Status?.BitcoinStatus?.Capabilities?.CanScanTxoutSet == true;
|
||||
var explorer = ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode);
|
||||
var scanProgress = await explorer.GetScanUTXOSetInformationAsync(paymentMethod.DerivationStrategyBase);
|
||||
var scanProgress = await explorer.GetScanUTXOSetInformationAsync(paymentMethod.AccountDerivation);
|
||||
if (scanProgress != null)
|
||||
{
|
||||
vm.PreviousError = scanProgress.Error;
|
||||
@ -331,14 +343,13 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (walletId?.StoreId == null)
|
||||
return NotFound();
|
||||
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
|
||||
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
|
||||
DerivationSchemeSettings paymentMethod = await GetDerivationSchemeSettings(walletId);
|
||||
if (paymentMethod == null)
|
||||
return NotFound();
|
||||
var explorer = ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode);
|
||||
try
|
||||
{
|
||||
await explorer.ScanUTXOSetAsync(paymentMethod.DerivationStrategyBase, vm.BatchSize, vm.GapLimit, vm.StartingIndex);
|
||||
await explorer.ScanUTXOSetAsync(paymentMethod.AccountDerivation, vm.BatchSize, vm.GapLimit, vm.StartingIndex);
|
||||
}
|
||||
catch (NBXplorerException ex) when (ex.Error.Code == "scanutxoset-in-progress")
|
||||
{
|
||||
@ -360,18 +371,24 @@ namespace BTCPayServer.Controllers
|
||||
return null;
|
||||
}
|
||||
|
||||
private DerivationStrategy GetPaymentMethod(WalletId walletId, StoreData store)
|
||||
private DerivationSchemeSettings GetDerivationSchemeSettings(WalletId walletId, StoreData store)
|
||||
{
|
||||
if (store == null || !store.HasClaim(Policies.CanModifyStoreSettings.Key))
|
||||
return null;
|
||||
|
||||
var paymentMethod = store
|
||||
.GetSupportedPaymentMethods(NetworkProvider)
|
||||
.OfType<DerivationStrategy>()
|
||||
.OfType<DerivationSchemeSettings>()
|
||||
.FirstOrDefault(p => p.PaymentId.PaymentType == Payments.PaymentTypes.BTCLike && p.PaymentId.CryptoCode == walletId.CryptoCode);
|
||||
return paymentMethod;
|
||||
}
|
||||
|
||||
private async Task<DerivationSchemeSettings> GetDerivationSchemeSettings(WalletId walletId)
|
||||
{
|
||||
var store = (await Repository.FindStore(walletId.StoreId, GetUserId()));
|
||||
return GetDerivationSchemeSettings(walletId, store);
|
||||
}
|
||||
|
||||
private static async Task<string> GetBalanceString(BTCPayWallet wallet, DerivationStrategyBase derivationStrategy)
|
||||
{
|
||||
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
|
||||
@ -392,17 +409,6 @@ namespace BTCPayServer.Controllers
|
||||
return _userManager.GetUserId(User);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{walletId}/send/ledger/success")]
|
||||
public IActionResult WalletSendLedgerSuccess(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId,
|
||||
string txid)
|
||||
{
|
||||
StatusMessage = $"Transaction broadcasted ({txid})";
|
||||
return RedirectToAction(nameof(this.WalletTransactions), new { walletId = walletId.ToString() });
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{walletId}/send/ledger/ws")]
|
||||
public async Task<IActionResult> LedgerConnection(
|
||||
@ -413,16 +419,18 @@ namespace BTCPayServer.Controllers
|
||||
// getxpub
|
||||
int account = 0,
|
||||
// sendtoaddress
|
||||
bool noChange = false,
|
||||
string destination = null, string amount = null, string feeRate = null, string substractFees = null
|
||||
string psbt = null,
|
||||
string hintChange = null
|
||||
)
|
||||
{
|
||||
if (!HttpContext.WebSockets.IsWebSocketRequest)
|
||||
return NotFound();
|
||||
|
||||
var cryptoCode = walletId.CryptoCode;
|
||||
var network = NetworkProvider.GetNetwork(walletId.CryptoCode);
|
||||
if (network == null)
|
||||
throw new FormatException("Invalid value for crypto code");
|
||||
var storeData = (await Repository.FindStore(walletId.StoreId, GetUserId()));
|
||||
var derivationScheme = GetPaymentMethod(walletId, storeData).DerivationStrategyBase;
|
||||
var derivationSettings = GetDerivationSchemeSettings(walletId, storeData);
|
||||
|
||||
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||
|
||||
@ -430,63 +438,11 @@ namespace BTCPayServer.Controllers
|
||||
using (var signTimeout = new CancellationTokenSource())
|
||||
{
|
||||
normalOperationTimeout.CancelAfter(TimeSpan.FromMinutes(30));
|
||||
var hw = new HardwareWalletService(webSocket);
|
||||
var hw = new LedgerHardwareWalletService(webSocket);
|
||||
var model = new WalletSendLedgerModel();
|
||||
object result = null;
|
||||
try
|
||||
{
|
||||
BTCPayNetwork network = null;
|
||||
if (cryptoCode != null)
|
||||
{
|
||||
network = NetworkProvider.GetNetwork(cryptoCode);
|
||||
if (network == null)
|
||||
throw new FormatException("Invalid value for crypto code");
|
||||
}
|
||||
|
||||
BitcoinAddress destinationAddress = null;
|
||||
if (destination != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
destinationAddress = BitcoinAddress.Create(destination.Trim(), network.NBitcoinNetwork);
|
||||
}
|
||||
catch { }
|
||||
if (destinationAddress == null)
|
||||
throw new FormatException("Invalid value for destination");
|
||||
}
|
||||
|
||||
FeeRate feeRateValue = null;
|
||||
if (feeRate != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
feeRateValue = new FeeRate(Money.Satoshis(int.Parse(feeRate, CultureInfo.InvariantCulture)), 1);
|
||||
}
|
||||
catch { }
|
||||
if (feeRateValue == null || feeRateValue.FeePerK <= Money.Zero)
|
||||
throw new FormatException("Invalid value for fee rate");
|
||||
}
|
||||
|
||||
Money amountBTC = null;
|
||||
if (amount != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
amountBTC = Money.Parse(amount);
|
||||
}
|
||||
catch { }
|
||||
if (amountBTC == null || amountBTC <= Money.Zero)
|
||||
throw new FormatException("Invalid value for amount");
|
||||
}
|
||||
|
||||
bool subsctractFeesValue = false;
|
||||
if (substractFees != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
subsctractFeesValue = bool.Parse(substractFees);
|
||||
}
|
||||
catch { throw new FormatException("Invalid value for subtract fees"); }
|
||||
}
|
||||
if (command == "test")
|
||||
{
|
||||
result = await hw.Test(normalOperationTimeout.Token);
|
||||
@ -495,127 +451,59 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (!_dashboard.IsFullySynched(network.CryptoCode, out var summary))
|
||||
throw new Exception($"{network.CryptoCode}: not started or fully synched");
|
||||
var strategy = GetDirectDerivationStrategy(derivationScheme);
|
||||
var wallet = _walletProvider.GetWallet(network);
|
||||
var change = wallet.GetChangeAddressAsync(derivationScheme);
|
||||
var keypaths = new Dictionary<Script, KeyPath>();
|
||||
List<Coin> availableCoins = new List<Coin>();
|
||||
foreach (var c in await wallet.GetUnspentCoins(derivationScheme))
|
||||
{
|
||||
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
|
||||
availableCoins.Add(c.Coin);
|
||||
}
|
||||
|
||||
var changeAddress = await change;
|
||||
|
||||
var storeBlob = storeData.GetStoreBlob();
|
||||
var paymentId = new Payments.PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike);
|
||||
var foundKeyPath = storeBlob.GetWalletKeyPathRoot(paymentId);
|
||||
// Some deployment have the wallet root key path saved in the store blob
|
||||
// If it does, we only have to make 1 call to the hw to check if it can sign the given strategy,
|
||||
if (foundKeyPath == null || !await hw.CanSign(network, strategy, foundKeyPath, normalOperationTimeout.Token))
|
||||
// Some deployment does not have the AccountKeyPath set, let's fix this...
|
||||
if (derivationSettings.AccountKeyPath == null)
|
||||
{
|
||||
// If the saved wallet key path is not present or incorrect, let's scan the wallet to see if it can sign strategy
|
||||
foundKeyPath = await hw.FindKeyPath(network, strategy, normalOperationTimeout.Token);
|
||||
if (foundKeyPath == null)
|
||||
throw new HardwareWalletException($"This store is not configured to use this ledger");
|
||||
storeBlob.SetWalletKeyPathRoot(paymentId, foundKeyPath);
|
||||
storeData.SetStoreBlob(storeBlob);
|
||||
var foundKeyPath = await hw.FindKeyPathFromDerivation(network,
|
||||
derivationSettings.AccountDerivation,
|
||||
normalOperationTimeout.Token);
|
||||
derivationSettings.AccountKeyPath = foundKeyPath ?? throw new HardwareWalletException($"This store is not configured to use this ledger");
|
||||
storeData.SetSupportedPaymentMethod(derivationSettings);
|
||||
await Repository.UpdateStore(storeData);
|
||||
}
|
||||
retry:
|
||||
var send = new[] { (
|
||||
destination: destinationAddress as IDestination,
|
||||
amount: amountBTC,
|
||||
substractFees: subsctractFeesValue) };
|
||||
|
||||
foreach (var element in send)
|
||||
{
|
||||
if (element.destination == null)
|
||||
throw new ArgumentNullException(nameof(element.destination));
|
||||
if (element.amount == null)
|
||||
throw new ArgumentNullException(nameof(element.amount));
|
||||
if (element.amount <= Money.Zero)
|
||||
throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero");
|
||||
}
|
||||
|
||||
TransactionBuilder builder = network.NBitcoinNetwork.CreateTransactionBuilder();
|
||||
builder.StandardTransactionPolicy.MinRelayTxFee = summary.Status.BitcoinStatus.MinRelayTxFee;
|
||||
builder.AddCoins(availableCoins);
|
||||
|
||||
foreach (var element in send)
|
||||
{
|
||||
builder.Send(element.destination, element.amount);
|
||||
if (element.substractFees)
|
||||
builder.SubtractFees();
|
||||
}
|
||||
|
||||
builder.SetChange(changeAddress.Item1);
|
||||
|
||||
if (network.MinFee == null)
|
||||
{
|
||||
builder.SendEstimatedFees(feeRateValue);
|
||||
}
|
||||
// If it has already the AccountKeyPath, we did not looked up for it, so we need to check if we are on the right ledger
|
||||
else
|
||||
{
|
||||
var estimatedFee = builder.EstimateFees(feeRateValue);
|
||||
if (network.MinFee > estimatedFee)
|
||||
builder.SendFees(network.MinFee);
|
||||
else
|
||||
builder.SendEstimatedFees(feeRateValue);
|
||||
}
|
||||
var unsigned = builder.BuildTransaction(false);
|
||||
|
||||
var hasChange = unsigned.Outputs.Any(o => o.ScriptPubKey == changeAddress.Item1.ScriptPubKey);
|
||||
if (noChange && hasChange)
|
||||
{
|
||||
availableCoins = builder.FindSpentCoins(unsigned).Cast<Coin>().ToList();
|
||||
amountBTC = builder.FindSpentCoins(unsigned).Select(c => c.TxOut.Value).Sum();
|
||||
subsctractFeesValue = true;
|
||||
goto retry;
|
||||
}
|
||||
|
||||
var usedCoins = builder.FindSpentCoins(unsigned);
|
||||
|
||||
Dictionary<uint256, Transaction> parentTransactions = new Dictionary<uint256, Transaction>();
|
||||
|
||||
if (!strategy.Segwit)
|
||||
{
|
||||
var parentHashes = usedCoins.Select(c => c.Outpoint.Hash).ToHashSet();
|
||||
var explorer = ExplorerClientProvider.GetExplorerClient(network);
|
||||
var getTransactionAsyncs = parentHashes.Select(h => (Op: explorer.GetTransactionAsync(h), Hash: h)).ToList();
|
||||
foreach (var getTransactionAsync in getTransactionAsyncs)
|
||||
// Checking if ledger is right with the RootFingerprint is faster as it does not need to make a query to the parent xpub,
|
||||
// but some deployment does not have it, so let's use AccountKeyPath instead
|
||||
if (derivationSettings.RootFingerprint == null)
|
||||
{
|
||||
var tx = (await getTransactionAsync.Op);
|
||||
if (tx == null)
|
||||
throw new Exception($"Parent transaction {getTransactionAsync.Hash} not found");
|
||||
parentTransactions.Add(tx.Transaction.GetHash(), tx.Transaction);
|
||||
|
||||
var actualPubKey = await hw.GetExtPubKey(network, derivationSettings.AccountKeyPath, normalOperationTimeout.Token);
|
||||
if (!derivationSettings.AccountDerivation.GetExtPubKeys().Any(p => p.GetPublicKey() == actualPubKey.GetPublicKey()))
|
||||
throw new HardwareWalletException($"This store is not configured to use this ledger");
|
||||
}
|
||||
// We have the root fingerprint, we can check the root from it
|
||||
else
|
||||
{
|
||||
var actualPubKey = await hw.GetPubKey(network, new KeyPath(), normalOperationTimeout.Token);
|
||||
if (actualPubKey.GetHDFingerPrint() != derivationSettings.RootFingerprint.Value)
|
||||
throw new HardwareWalletException($"This store is not configured to use this ledger");
|
||||
}
|
||||
}
|
||||
|
||||
// Some deployment does not have the RootFingerprint set, let's fix this...
|
||||
if (derivationSettings.RootFingerprint == null)
|
||||
{
|
||||
derivationSettings.RootFingerprint = (await hw.GetPubKey(network, new KeyPath(), normalOperationTimeout.Token)).GetHDFingerPrint();
|
||||
storeData.SetSupportedPaymentMethod(derivationSettings);
|
||||
await Repository.UpdateStore(storeData);
|
||||
}
|
||||
|
||||
var psbtResponse = new CreatePSBTResponse()
|
||||
{
|
||||
PSBT = PSBT.Parse(psbt, network.NBitcoinNetwork),
|
||||
ChangeAddress = string.IsNullOrEmpty(hintChange) ? null : BitcoinAddress.Create(hintChange, network.NBitcoinNetwork)
|
||||
};
|
||||
|
||||
|
||||
derivationSettings.RebaseKeyPaths(psbtResponse.PSBT);
|
||||
|
||||
signTimeout.CancelAfter(TimeSpan.FromMinutes(5));
|
||||
var transaction = await hw.SignTransactionAsync(usedCoins.Select(c => new SignatureRequest
|
||||
{
|
||||
InputTransaction = parentTransactions.TryGet(c.Outpoint.Hash),
|
||||
InputCoin = c,
|
||||
KeyPath = foundKeyPath.Derive(keypaths[c.TxOut.ScriptPubKey]),
|
||||
PubKey = strategy.Root.Derive(keypaths[c.TxOut.ScriptPubKey]).PubKey
|
||||
}).ToArray(), unsigned, hasChange ? foundKeyPath.Derive(changeAddress.Item2) : null, signTimeout.Token);
|
||||
try
|
||||
{
|
||||
var broadcastResult = await wallet.BroadcastTransactionsAsync(new List<Transaction>() { transaction });
|
||||
if (!broadcastResult[0].Success)
|
||||
{
|
||||
throw new Exception($"RPC Error while broadcasting: {broadcastResult[0].RPCCode} {broadcastResult[0].RPCCodeMessage} {broadcastResult[0].RPCMessage}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception("Error while broadcasting: " + ex.Message);
|
||||
}
|
||||
wallet.InvalidateCache(derivationScheme);
|
||||
result = new SendToAddressResult() { TransactionId = transaction.GetHash().ToString() };
|
||||
psbtResponse.PSBT = await hw.SignTransactionAsync(psbtResponse.PSBT, psbtResponse.ChangeAddress?.ScriptPubKey, signTimeout.Token);
|
||||
result = new SendToAddressResult() { PSBT = psbtResponse.PSBT.ToBase64() };
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
@ -640,16 +528,6 @@ retry:
|
||||
}
|
||||
return new EmptyResult();
|
||||
}
|
||||
|
||||
private DirectDerivationStrategy GetDirectDerivationStrategy(DerivationStrategyBase strategy)
|
||||
{
|
||||
if (strategy == null)
|
||||
throw new Exception("The derivation scheme is not provided");
|
||||
var directStrategy = strategy as DirectDerivationStrategy;
|
||||
if (directStrategy == null)
|
||||
directStrategy = (strategy as P2SHDerivationStrategy).Inner as DirectDerivationStrategy;
|
||||
return directStrategy;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -659,6 +537,7 @@ retry:
|
||||
|
||||
public class SendToAddressResult
|
||||
{
|
||||
public string TransactionId { get; set; }
|
||||
[JsonProperty("psbt")]
|
||||
public string PSBT { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using BTCPayServer.Models;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure.Internal;
|
||||
using BTCPayServer.Services.PaymentRequests;
|
||||
using BTCPayServer.Services.U2F.Models;
|
||||
using BTCPayServer.Storage.Models;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
@ -54,6 +53,11 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DbSet<PaymentRequestData> PaymentRequests
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DbSet<StoreData> Stores
|
||||
{
|
||||
@ -87,10 +91,18 @@ namespace BTCPayServer.Data
|
||||
}
|
||||
|
||||
public DbSet<APIKeyData> ApiKeys
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DbSet<StoredFile> Files
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
|
||||
public DbSet<U2FDevice> U2FDevices { get; set; }
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
var isConfigured = optionsBuilder.Options.Extensions.OfType<RelationalOptionsExtension>().Any();
|
||||
@ -204,6 +216,16 @@ namespace BTCPayServer.Data
|
||||
o.UniqueId
|
||||
#pragma warning restore CS0618
|
||||
});
|
||||
|
||||
|
||||
builder.Entity<PaymentRequestData>()
|
||||
.HasOne(o => o.StoreData)
|
||||
.WithMany(i => i.PaymentRequests)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.Entity<PaymentRequestData>()
|
||||
.HasIndex(o => o.Status);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -12,11 +12,6 @@ namespace BTCPayServer.Data
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string Facade
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string StoreDataId
|
||||
{
|
||||
get; set;
|
||||
|
@ -11,7 +11,7 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Obsolete("Unused")]
|
||||
public string Facade
|
||||
{
|
||||
get; set;
|
||||
|
@ -21,6 +21,7 @@ using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Payments.CoinSwitch;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.PaymentRequests;
|
||||
using BTCPayServer.Services.Mails;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
@ -41,6 +42,11 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public List<PaymentRequestData> PaymentRequests
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public List<InvoiceData> Invoices { get; set; }
|
||||
|
||||
@ -67,7 +73,7 @@ namespace BTCPayServer.Data
|
||||
if (networks.BTC != null)
|
||||
{
|
||||
btcReturned = true;
|
||||
yield return BTCPayServer.DerivationStrategy.Parse(DerivationStrategy, networks.BTC);
|
||||
yield return DerivationSchemeSettings.Parse(DerivationStrategy, networks.BTC);
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,6 +98,11 @@ namespace BTCPayServer.Data
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
public void SetSupportedPaymentMethod(ISupportedPaymentMethod supportedPaymentMethod)
|
||||
{
|
||||
SetSupportedPaymentMethod(null, supportedPaymentMethod);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set or remove a new supported payment method for the store
|
||||
/// </summary>
|
||||
@ -99,8 +110,16 @@ namespace BTCPayServer.Data
|
||||
/// <param name="supportedPaymentMethod">The payment method, or null to remove</param>
|
||||
public void SetSupportedPaymentMethod(PaymentMethodId paymentMethodId, ISupportedPaymentMethod supportedPaymentMethod)
|
||||
{
|
||||
if (supportedPaymentMethod != null && paymentMethodId != supportedPaymentMethod.PaymentId)
|
||||
throw new InvalidOperationException("Argument mismatch");
|
||||
if (supportedPaymentMethod != null && paymentMethodId != null && paymentMethodId != supportedPaymentMethod.PaymentId)
|
||||
{
|
||||
throw new InvalidOperationException("Incoherent arguments, this should never happen");
|
||||
}
|
||||
if (supportedPaymentMethod == null && paymentMethodId == null)
|
||||
throw new ArgumentException($"{nameof(supportedPaymentMethod)} or {nameof(paymentMethodId)} should be specified");
|
||||
if (supportedPaymentMethod != null && paymentMethodId == null)
|
||||
{
|
||||
paymentMethodId = supportedPaymentMethod.PaymentId;
|
||||
}
|
||||
|
||||
#pragma warning disable CS0618
|
||||
JObject strategies = string.IsNullOrEmpty(DerivationStrategies) ? new JObject() : JObject.Parse(DerivationStrategies);
|
||||
@ -128,7 +147,7 @@ namespace BTCPayServer.Data
|
||||
}
|
||||
}
|
||||
|
||||
if (!existing && supportedPaymentMethod == null && paymentMethodId.IsBTCOnChain)
|
||||
if (!existing && supportedPaymentMethod == null && supportedPaymentMethod.PaymentId.IsBTCOnChain)
|
||||
{
|
||||
DerivationStrategy = null;
|
||||
}
|
||||
@ -301,6 +320,25 @@ namespace BTCPayServer.Data
|
||||
|
||||
public bool RequiresRefundEmail { get; set; }
|
||||
|
||||
CurrencyPair[] _DefaultCurrencyPairs;
|
||||
[JsonProperty("defaultCurrencyPairs", ItemConverterType = typeof(CurrencyPairJsonConverter))]
|
||||
public CurrencyPair[] DefaultCurrencyPairs
|
||||
{
|
||||
get
|
||||
{
|
||||
return _DefaultCurrencyPairs ?? Array.Empty<CurrencyPair>();
|
||||
}
|
||||
set
|
||||
{
|
||||
_DefaultCurrencyPairs = value;
|
||||
}
|
||||
}
|
||||
|
||||
public string GetDefaultCurrencyPairString()
|
||||
{
|
||||
return string.Join(',', DefaultCurrencyPairs.Select(c => c.ToString()));
|
||||
}
|
||||
|
||||
public string DefaultLang { get; set; }
|
||||
[DefaultValue(60)]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
@ -324,10 +362,11 @@ namespace BTCPayServer.Data
|
||||
public List<RateRule_Obsolete> RateRules { get; set; } = new List<RateRule_Obsolete>();
|
||||
public string PreferredExchange { get; set; }
|
||||
|
||||
[JsonConverter(typeof(CurrencyValueJsonConverter))]
|
||||
public CurrencyValue LightningMaxValue { get; set; }
|
||||
[JsonConverter(typeof(CurrencyValueJsonConverter))]
|
||||
public CurrencyValue OnChainMinValue { get; set; }
|
||||
[JsonConverter(typeof(CurrencyValueJsonConverter))]
|
||||
public CurrencyValue LightningMaxValue { get; set; }
|
||||
public bool LightningAmountInSatoshi { get; set; }
|
||||
|
||||
[JsonConverter(typeof(UriJsonConverter))]
|
||||
public Uri CustomLogo { get; set; }
|
||||
@ -404,25 +443,12 @@ namespace BTCPayServer.Data
|
||||
|
||||
[Obsolete("Use GetExcludedPaymentMethods instead")]
|
||||
public string[] ExcludedPaymentMethods { get; set; }
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
public void SetWalletKeyPathRoot(PaymentMethodId paymentMethodId, KeyPath keyPath)
|
||||
{
|
||||
if (keyPath == null)
|
||||
WalletKeyPathRoots.Remove(paymentMethodId.ToString());
|
||||
else
|
||||
WalletKeyPathRoots.AddOrReplace(paymentMethodId.ToString().ToLowerInvariant(), keyPath.ToString());
|
||||
}
|
||||
public KeyPath GetWalletKeyPathRoot(PaymentMethodId paymentMethodId)
|
||||
{
|
||||
if (WalletKeyPathRoots.TryGetValue(paymentMethodId.ToString().ToLowerInvariant(), out var k))
|
||||
return KeyPath.Parse(k);
|
||||
return null;
|
||||
}
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
[Obsolete("Use SetWalletKeyPathRoot/GetWalletKeyPathRoot instead")]
|
||||
public Dictionary<string, string> WalletKeyPathRoots { get; set; } = new Dictionary<string, string>();
|
||||
|
||||
[Obsolete("Use DerivationSchemeSettings instead")]
|
||||
public Dictionary<string, string> WalletKeyPathRoots { get; set; }
|
||||
|
||||
public EmailSettings EmailSettings { get; set; }
|
||||
public bool RedirectAutomatically { get; set; }
|
||||
|
||||
public IPaymentFilter GetExcludedPaymentMethods()
|
||||
{
|
||||
|
@ -12,14 +12,51 @@ namespace BTCPayServer
|
||||
{
|
||||
public class DerivationSchemeParser
|
||||
{
|
||||
public Network Network { get; set; }
|
||||
public BTCPayNetwork BtcPayNetwork { get; }
|
||||
|
||||
public Network Network => BtcPayNetwork.NBitcoinNetwork;
|
||||
|
||||
public Script HintScriptPubKey { get; set; }
|
||||
|
||||
public DerivationSchemeParser(Network expectedNetwork)
|
||||
Dictionary<uint, string[]> ElectrumMapping = new Dictionary<uint, string[]>();
|
||||
|
||||
public DerivationSchemeParser(BTCPayNetwork expectedNetwork)
|
||||
{
|
||||
Network = expectedNetwork;
|
||||
if (expectedNetwork == null)
|
||||
throw new ArgumentNullException(nameof(expectedNetwork));
|
||||
BtcPayNetwork = expectedNetwork;
|
||||
}
|
||||
|
||||
|
||||
public DerivationStrategyBase ParseElectrum(string str)
|
||||
{
|
||||
|
||||
if (str == null)
|
||||
throw new ArgumentNullException(nameof(str));
|
||||
str = str.Trim();
|
||||
var data = Network.GetBase58CheckEncoder().DecodeData(str);
|
||||
if (data.Length < 4)
|
||||
throw new FormatException();
|
||||
var prefix = Utils.ToUInt32(data, false);
|
||||
|
||||
var standardPrefix = Utils.ToBytes(0x0488b21eU, false);
|
||||
for (int ii = 0; ii < 4; ii++)
|
||||
data[ii] = standardPrefix[ii];
|
||||
var extPubKey = new BitcoinExtPubKey(Network.GetBase58CheckEncoder().EncodeData(data), Network.Main).ToNetwork(Network);
|
||||
if (!BtcPayNetwork.ElectrumMapping.TryGetValue(prefix, out var type))
|
||||
{
|
||||
throw new FormatException();
|
||||
}
|
||||
if (type == DerivationType.Segwit)
|
||||
return new DirectDerivationStrategy(extPubKey) { Segwit = true };
|
||||
if (type == DerivationType.Legacy)
|
||||
return new DirectDerivationStrategy(extPubKey) { Segwit = false };
|
||||
if (type == DerivationType.SegwitP2SH)
|
||||
return new DerivationStrategyFactory(Network).Parse(extPubKey.ToString() + "-[p2sh]");
|
||||
throw new FormatException();
|
||||
}
|
||||
|
||||
|
||||
public DerivationStrategyBase Parse(string str)
|
||||
{
|
||||
if (str == null)
|
||||
@ -41,7 +78,7 @@ namespace BTCPayServer
|
||||
}
|
||||
}
|
||||
|
||||
if(!Network.Consensus.SupportSegwit)
|
||||
if (!Network.Consensus.SupportSegwit)
|
||||
hintedLabels.Add("legacy");
|
||||
|
||||
try
|
||||
@ -53,15 +90,6 @@ namespace BTCPayServer
|
||||
{
|
||||
}
|
||||
|
||||
Dictionary<uint, string[]> electrumMapping = new Dictionary<uint, string[]>();
|
||||
//Source https://github.com/spesmilo/electrum/blob/9edffd17542de5773e7284a8c8a2673c766bb3c3/lib/bitcoin.py
|
||||
var standard = 0x0488b21eU;
|
||||
electrumMapping.Add(standard, new[] { "legacy" });
|
||||
var p2wpkh_p2sh = 0x049d7cb2U;
|
||||
electrumMapping.Add(p2wpkh_p2sh, new string[] { "p2sh" });
|
||||
var p2wpkh = 0x4b24746U;
|
||||
electrumMapping.Add(p2wpkh, Array.Empty<string>());
|
||||
|
||||
var parts = str.Split('-');
|
||||
bool hasLabel = false;
|
||||
for (int i = 0; i < parts.Length; i++)
|
||||
@ -84,17 +112,22 @@ namespace BTCPayServer
|
||||
if (data.Length < 4)
|
||||
continue;
|
||||
var prefix = Utils.ToUInt32(data, false);
|
||||
var standardPrefix = Utils.ToBytes(Network.NetworkType == NetworkType.Mainnet ? 0x0488b21eU : 0x043587cf, false);
|
||||
|
||||
var standardPrefix = Utils.ToBytes(0x0488b21eU, false);
|
||||
for (int ii = 0; ii < 4; ii++)
|
||||
data[ii] = standardPrefix[ii];
|
||||
var derivationScheme = new BitcoinExtPubKey(Network.GetBase58CheckEncoder().EncodeData(data), Network.Main).ToNetwork(Network).ToString();
|
||||
|
||||
var derivationScheme = new BitcoinExtPubKey(Network.GetBase58CheckEncoder().EncodeData(data), Network).ToString();
|
||||
electrumMapping.TryGetValue(prefix, out string[] labels);
|
||||
if (labels != null)
|
||||
if (BtcPayNetwork.ElectrumMapping.TryGetValue(prefix, out var type))
|
||||
{
|
||||
foreach (var label in labels)
|
||||
switch (type)
|
||||
{
|
||||
hintedLabels.Add(label.ToLowerInvariant());
|
||||
case DerivationType.Legacy:
|
||||
hintedLabels.Add("legacy");
|
||||
break;
|
||||
case DerivationType.SegwitP2SH:
|
||||
hintedLabels.Add("p2sh");
|
||||
break;
|
||||
}
|
||||
}
|
||||
parts[i] = derivationScheme;
|
||||
@ -136,7 +169,7 @@ namespace BTCPayServer
|
||||
resultNoLabels = string.Join('-', resultNoLabels.Split('-').Where(p => !IsLabel(p)));
|
||||
foreach (var labels in ItemCombinations(hintLabels.ToList()))
|
||||
{
|
||||
var hinted = facto.Parse(resultNoLabels + '-' + string.Join('-', labels.Select(l=>$"[{l}]").ToArray()));
|
||||
var hinted = facto.Parse(resultNoLabels + '-' + string.Join('-', labels.Select(l => $"[{l}]").ToArray()));
|
||||
if (HintScriptPubKey == hinted.Derive(firstKeyPath).ScriptPubKey)
|
||||
return hinted;
|
||||
}
|
||||
@ -149,20 +182,20 @@ namespace BTCPayServer
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method to create lists containing possible combinations of an input list of items. This is
|
||||
/// basically copied from code by user "jaolho" on this thread:
|
||||
/// http://stackoverflow.com/questions/7802822/all-possible-combinations-of-a-list-of-values
|
||||
/// </summary>
|
||||
/// <typeparam name="T">type of the items on the input list</typeparam>
|
||||
/// <param name="inputList">list of items</param>
|
||||
/// <param name="minimumItems">minimum number of items wanted in the generated combinations,
|
||||
/// if zero the empty combination is included,
|
||||
/// default is one</param>
|
||||
/// <param name="maximumItems">maximum number of items wanted in the generated combinations,
|
||||
/// default is no maximum limit</param>
|
||||
/// <returns>list of lists for possible combinations of the input items</returns>
|
||||
public static List<List<T>> ItemCombinations<T>(List<T> inputList, int minimumItems = 1,
|
||||
int maximumItems = int.MaxValue)
|
||||
/// Method to create lists containing possible combinations of an input list of items. This is
|
||||
/// basically copied from code by user "jaolho" on this thread:
|
||||
/// http://stackoverflow.com/questions/7802822/all-possible-combinations-of-a-list-of-values
|
||||
/// </summary>
|
||||
/// <typeparam name="T">type of the items on the input list</typeparam>
|
||||
/// <param name="inputList">list of items</param>
|
||||
/// <param name="minimumItems">minimum number of items wanted in the generated combinations,
|
||||
/// if zero the empty combination is included,
|
||||
/// default is one</param>
|
||||
/// <param name="maximumItems">maximum number of items wanted in the generated combinations,
|
||||
/// default is no maximum limit</param>
|
||||
/// <returns>list of lists for possible combinations of the input items</returns>
|
||||
public static List<List<T>> ItemCombinations<T>(List<T> inputList, int minimumItems = 1,
|
||||
int maximumItems = int.MaxValue)
|
||||
{
|
||||
int nonEmptyCombinations = (int)Math.Pow(2, inputList.Count) - 1;
|
||||
List<List<T>> listOfLists = new List<List<T>>(nonEmptyCombinations + 1);
|
||||
|
188
BTCPayServer/DerivationSchemeSettings.cs
Normal file
188
BTCPayServer/DerivationSchemeSettings.cs
Normal file
@ -0,0 +1,188 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Payments;
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class DerivationSchemeSettings : ISupportedPaymentMethod
|
||||
{
|
||||
public static DerivationSchemeSettings Parse(string derivationStrategy, BTCPayNetwork network)
|
||||
{
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
if (derivationStrategy == null)
|
||||
throw new ArgumentNullException(nameof(derivationStrategy));
|
||||
var result = new NBXplorer.DerivationStrategy.DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationStrategy);
|
||||
return new DerivationSchemeSettings(result, network) { AccountOriginal = derivationStrategy.Trim() };
|
||||
}
|
||||
|
||||
public static bool TryParseFromJson(string config, BTCPayNetwork network, out DerivationSchemeSettings strategy)
|
||||
{
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
if (config == null)
|
||||
throw new ArgumentNullException(nameof(config));
|
||||
strategy = null;
|
||||
try
|
||||
{
|
||||
strategy = network.NBXplorerNetwork.Serializer.ToObject<DerivationSchemeSettings>(config);
|
||||
strategy.Network = network;
|
||||
}
|
||||
catch { }
|
||||
return strategy != null;
|
||||
}
|
||||
|
||||
public static bool TryParseFromColdcard(string coldcardExport, BTCPayNetwork network, out DerivationSchemeSettings settings)
|
||||
{
|
||||
settings = null;
|
||||
if (coldcardExport == null)
|
||||
throw new ArgumentNullException(nameof(coldcardExport));
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
var result = new DerivationSchemeSettings();
|
||||
result.Source = "Coldcard";
|
||||
var derivationSchemeParser = new DerivationSchemeParser(network);
|
||||
JObject jobj = null;
|
||||
try
|
||||
{
|
||||
jobj = JObject.Parse(coldcardExport);
|
||||
jobj = (JObject)jobj["keystore"];
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (jobj.ContainsKey("xpub"))
|
||||
{
|
||||
try
|
||||
{
|
||||
result.AccountOriginal = jobj["xpub"].Value<string>().Trim();
|
||||
result.AccountDerivation = derivationSchemeParser.ParseElectrum(result.AccountOriginal);
|
||||
if (result.AccountDerivation is DirectDerivationStrategy direct && !direct.Segwit)
|
||||
result.AccountOriginal = null; // Saving this would be confusing for user, as xpub of electrum is legacy derivation, but for btcpay, it is segwit derivation
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (jobj.ContainsKey("label"))
|
||||
{
|
||||
try
|
||||
{
|
||||
result.Label = jobj["label"].Value<string>();
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
if (jobj.ContainsKey("ckcc_xfp"))
|
||||
{
|
||||
try
|
||||
{
|
||||
result.RootFingerprint = new HDFingerprint(jobj["ckcc_xfp"].Value<uint>());
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
if (jobj.ContainsKey("derivation"))
|
||||
{
|
||||
try
|
||||
{
|
||||
result.AccountKeyPath = new KeyPath(jobj["derivation"].Value<string>());
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
settings = result;
|
||||
settings.Network = network;
|
||||
return true;
|
||||
}
|
||||
|
||||
public DerivationSchemeSettings()
|
||||
{
|
||||
|
||||
}
|
||||
public DerivationSchemeSettings(DerivationStrategyBase derivationStrategy, BTCPayNetwork network)
|
||||
{
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
if (derivationStrategy == null)
|
||||
throw new ArgumentNullException(nameof(derivationStrategy));
|
||||
AccountDerivation = derivationStrategy;
|
||||
Network = network;
|
||||
}
|
||||
[JsonIgnore]
|
||||
public BTCPayNetwork Network { get; set; }
|
||||
public string Source { get; set; }
|
||||
public KeyPath AccountKeyPath { get; set; }
|
||||
|
||||
public DerivationStrategyBase AccountDerivation { get; set; }
|
||||
public string AccountOriginal { get; set; }
|
||||
|
||||
public HDFingerprint? RootFingerprint { get; set; }
|
||||
|
||||
public BitcoinExtPubKey ExplicitAccountKey { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public BitcoinExtPubKey AccountKey
|
||||
{
|
||||
get
|
||||
{
|
||||
return ExplicitAccountKey ?? new BitcoinExtPubKey(AccountDerivation.GetExtPubKeys().First(), Network.NBitcoinNetwork);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<NBXplorer.Models.PSBTRebaseKeyRules> GetPSBTRebaseKeyRules()
|
||||
{
|
||||
if (AccountKey != null && AccountKeyPath != null && RootFingerprint is HDFingerprint fp)
|
||||
{
|
||||
yield return new NBXplorer.Models.PSBTRebaseKeyRules()
|
||||
{
|
||||
AccountKey = AccountKey,
|
||||
AccountKeyPath = AccountKeyPath,
|
||||
MasterFingerprint = fp
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public string Label { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public PaymentMethodId PaymentId => new PaymentMethodId(Network.CryptoCode, PaymentTypes.BTCLike);
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return AccountDerivation.ToString();
|
||||
}
|
||||
public string ToPrettyString()
|
||||
{
|
||||
return !string.IsNullOrEmpty(Label) ? Label :
|
||||
!String.IsNullOrEmpty(AccountOriginal) ? AccountOriginal :
|
||||
ToString();
|
||||
}
|
||||
|
||||
public string ToJson()
|
||||
{
|
||||
return Network.NBXplorerNetwork.Serializer.ToString(this);
|
||||
}
|
||||
|
||||
public void RebaseKeyPaths(PSBT psbt)
|
||||
{
|
||||
foreach (var rebase in GetPSBTRebaseKeyRules())
|
||||
{
|
||||
psbt.RebaseKeyPaths(rebase.AccountKey, rebase.AccountKeyPath, rebase.MasterFingerprint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class DerivationStrategy : ISupportedPaymentMethod
|
||||
{
|
||||
private DerivationStrategyBase _DerivationStrategy;
|
||||
private BTCPayNetwork _Network;
|
||||
|
||||
public DerivationStrategy(DerivationStrategyBase result, BTCPayNetwork network)
|
||||
{
|
||||
this._DerivationStrategy = result;
|
||||
this._Network = network;
|
||||
}
|
||||
|
||||
public static DerivationStrategy Parse(string derivationStrategy, BTCPayNetwork network)
|
||||
{
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
if (derivationStrategy == null)
|
||||
throw new ArgumentNullException(nameof(derivationStrategy));
|
||||
var result = new NBXplorer.DerivationStrategy.DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationStrategy);
|
||||
return new DerivationStrategy(result, network);
|
||||
}
|
||||
|
||||
public BTCPayNetwork Network { get { return this._Network; } }
|
||||
|
||||
public DerivationStrategyBase DerivationStrategyBase => this._DerivationStrategy;
|
||||
|
||||
public PaymentMethodId PaymentId => new PaymentMethodId(Network.CryptoCode, PaymentTypes.BTCLike);
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return _DerivationStrategy.ToString();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@ -17,7 +18,7 @@ namespace BTCPayServer
|
||||
|
||||
public BTCPayNetworkProvider NetworkProviders => _NetworkProviders;
|
||||
NBXplorerDashboard _Dashboard;
|
||||
public ExplorerClientProvider(BTCPayNetworkProvider networkProviders, BTCPayServerOptions options, NBXplorerDashboard dashboard)
|
||||
public ExplorerClientProvider(IHttpClientFactory httpClientFactory, BTCPayNetworkProvider networkProviders, BTCPayServerOptions options, NBXplorerDashboard dashboard)
|
||||
{
|
||||
_Dashboard = dashboard;
|
||||
_NetworkProviders = networkProviders;
|
||||
@ -32,14 +33,15 @@ namespace BTCPayServer
|
||||
Logs.Configuration.LogInformation($"{setting.CryptoCode}: Cookie file is {(setting.CookieFile ?? "not set")}");
|
||||
if (setting.ExplorerUri != null)
|
||||
{
|
||||
_Clients.TryAdd(setting.CryptoCode, CreateExplorerClient(_NetworkProviders.GetNetwork(setting.CryptoCode), setting.ExplorerUri, setting.CookieFile));
|
||||
_Clients.TryAdd(setting.CryptoCode, CreateExplorerClient(httpClientFactory.CreateClient($"NBXPLORER_{setting.CryptoCode}"), _NetworkProviders.GetNetwork(setting.CryptoCode), setting.ExplorerUri, setting.CookieFile));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ExplorerClient CreateExplorerClient(BTCPayNetwork n, Uri uri, string cookieFile)
|
||||
private static ExplorerClient CreateExplorerClient(HttpClient httpClient, BTCPayNetwork n, Uri uri, string cookieFile)
|
||||
{
|
||||
var explorer = new ExplorerClient(n.NBXplorerNetwork, uri);
|
||||
explorer.SetClient(httpClient);
|
||||
if (cookieFile == null)
|
||||
{
|
||||
Logs.Configuration.LogWarning($"{n.CryptoCode}: Not using cookie authentication");
|
||||
|
@ -33,6 +33,7 @@ using BTCPayServer.Services;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using System.Net;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
@ -61,6 +62,23 @@ namespace BTCPayServer
|
||||
}
|
||||
return value;
|
||||
}
|
||||
public static decimal RoundToSignificant(this decimal value, ref int divisibility)
|
||||
{
|
||||
if (value != 0m)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var rounded = decimal.Round(value, divisibility, MidpointRounding.AwayFromZero);
|
||||
if ((Math.Abs(rounded - value) / value) < 0.001m)
|
||||
{
|
||||
value = rounded;
|
||||
break;
|
||||
}
|
||||
divisibility++;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
public static PaymentMethodId GetpaymentMethodId(this InvoiceCryptoInfo info)
|
||||
{
|
||||
return new PaymentMethodId(info.CryptoCode, Enum.Parse<PaymentTypes>(info.PaymentType));
|
||||
@ -108,6 +126,18 @@ namespace BTCPayServer
|
||||
return str;
|
||||
return str + "/";
|
||||
}
|
||||
public static string WithStartingSlash(this string str)
|
||||
{
|
||||
if (str.StartsWith("/", StringComparison.InvariantCulture))
|
||||
return str;
|
||||
return $"/{str}";
|
||||
}
|
||||
public static string WithoutEndingSlash(this string str)
|
||||
{
|
||||
if (str.EndsWith("/", StringComparison.InvariantCulture))
|
||||
return str.Substring(0, str.Length - 1);
|
||||
return str;
|
||||
}
|
||||
|
||||
public static void SetHeaderOnStarting(this HttpResponse resp, string name, string value)
|
||||
{
|
||||
@ -142,6 +172,31 @@ namespace BTCPayServer
|
||||
(derivationStrategyBase is DirectDerivationStrategy direct) && direct.Segwit;
|
||||
}
|
||||
|
||||
public static bool IsLocalNetwork(string server)
|
||||
{
|
||||
if (server == null)
|
||||
throw new ArgumentNullException(nameof(server));
|
||||
if (Uri.CheckHostName(server) == UriHostNameType.Dns)
|
||||
{
|
||||
return server.EndsWith(".internal", StringComparison.OrdinalIgnoreCase) ||
|
||||
server.EndsWith(".local", StringComparison.OrdinalIgnoreCase) ||
|
||||
server.EndsWith(".lan", StringComparison.OrdinalIgnoreCase) ||
|
||||
server.IndexOf('.', StringComparison.OrdinalIgnoreCase) == -1;
|
||||
}
|
||||
if(IPAddress.TryParse(server, out var ip))
|
||||
{
|
||||
return ip.IsLocal();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool IsOnion(this HttpRequest request)
|
||||
{
|
||||
if (request?.Host.Host == null)
|
||||
return false;
|
||||
return request.Host.Host.EndsWith(".onion", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static string GetAbsoluteRoot(this HttpRequest request)
|
||||
{
|
||||
return string.Concat(
|
||||
@ -194,8 +249,10 @@ namespace BTCPayServer
|
||||
/// <returns></returns>
|
||||
public static string GetRelativePathOrAbsolute(this HttpRequest request, string path)
|
||||
{
|
||||
if (Uri.TryCreate(path, UriKind.Absolute, out var unused))
|
||||
if (!Uri.TryCreate(path, UriKind.RelativeOrAbsolute, out var uri) ||
|
||||
uri.IsAbsoluteUri)
|
||||
return path;
|
||||
|
||||
if (path.Length > 0 && path[0] != '/')
|
||||
path = $"/{path}";
|
||||
return string.Concat(
|
||||
@ -211,6 +268,31 @@ namespace BTCPayServer
|
||||
return isRelative ? request.GetAbsoluteRoot() + redirectUrl : redirectUrl;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will return an absolute URL.
|
||||
/// If `relativeOrAsbolute` is absolute, returns it.
|
||||
/// If `relativeOrAsbolute` is relative, send absolute url based on the HOST of this request (without PathBase)
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="relativeOrAbsolte"></param>
|
||||
/// <returns></returns>
|
||||
public static Uri GetAbsoluteUriNoPathBase(this HttpRequest request, Uri relativeOrAbsolute = null)
|
||||
{
|
||||
if (relativeOrAbsolute == null)
|
||||
{
|
||||
return new Uri(string.Concat(
|
||||
request.Scheme,
|
||||
"://",
|
||||
request.Host.ToUriComponent()), UriKind.Absolute);
|
||||
}
|
||||
if (relativeOrAbsolute.IsAbsoluteUri)
|
||||
return relativeOrAbsolute;
|
||||
return new Uri(string.Concat(
|
||||
request.Scheme,
|
||||
"://",
|
||||
request.Host.ToUriComponent()) + relativeOrAbsolute.ToString().WithStartingSlash(), UriKind.Absolute);
|
||||
}
|
||||
|
||||
public static IServiceCollection ConfigureBTCPayServer(this IServiceCollection services, IConfiguration conf)
|
||||
{
|
||||
services.Configure<BTCPayServerOptions>(o =>
|
||||
@ -253,30 +335,6 @@ namespace BTCPayServer
|
||||
NBitcoin.Extensions.TryAdd(ctx.Items, "BitpayAuth", value);
|
||||
}
|
||||
|
||||
public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
|
||||
{
|
||||
using (var delayCTS = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
|
||||
{
|
||||
var waiting = Task.Delay(-1, delayCTS.Token);
|
||||
var doing = task;
|
||||
await Task.WhenAny(waiting, doing);
|
||||
delayCTS.Cancel();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return await doing;
|
||||
}
|
||||
}
|
||||
public static async Task WithCancellation(this Task task, CancellationToken cancellationToken)
|
||||
{
|
||||
using (var delayCTS = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
|
||||
{
|
||||
var waiting = Task.Delay(-1, delayCTS.Token);
|
||||
var doing = task;
|
||||
await Task.WhenAny(waiting, doing);
|
||||
delayCTS.Cancel();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
}
|
||||
|
||||
public static (string Signature, String Id, String Authorization) GetBitpayAuth(this HttpContext ctx)
|
||||
{
|
||||
ctx.Items.TryGetValue("BitpayAuth", out object obj);
|
||||
@ -298,5 +356,15 @@ namespace BTCPayServer
|
||||
var res = JsonConvert.SerializeObject(o, Formatting.None, jsonSettings);
|
||||
return res;
|
||||
}
|
||||
|
||||
public static string TrimEnd(this string input, string suffixToRemove,
|
||||
StringComparison comparisonType) {
|
||||
|
||||
if (input != null && suffixToRemove != null
|
||||
&& input.EndsWith(suffixToRemove, comparisonType)) {
|
||||
return input.Substring(0, input.Length - suffixToRemove.Length);
|
||||
}
|
||||
else return input;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using NBitcoin;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@ -34,6 +35,8 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_Stop == null)
|
||||
return;
|
||||
_Stop.Cancel();
|
||||
try
|
||||
{
|
||||
@ -43,7 +46,14 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
|
||||
}
|
||||
await BackgroundJobClient.WaitAllRunning(cancellationToken);
|
||||
try
|
||||
{
|
||||
await BackgroundJobClient.WaitAllRunning(cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,10 +61,10 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
class BackgroundJob
|
||||
{
|
||||
public Func<Task> Action;
|
||||
public Func<CancellationToken, Task> Action;
|
||||
public TimeSpan Delay;
|
||||
public IDelay DelayImplementation;
|
||||
public BackgroundJob(Func<Task> action, TimeSpan delay, IDelay delayImplementation)
|
||||
public BackgroundJob(Func<CancellationToken, Task> action, TimeSpan delay, IDelay delayImplementation)
|
||||
{
|
||||
this.Action = action;
|
||||
this.Delay = delay;
|
||||
@ -64,7 +74,7 @@ namespace BTCPayServer.HostedServices
|
||||
public async Task Run(CancellationToken cancellationToken)
|
||||
{
|
||||
await DelayImplementation.Wait(Delay, cancellationToken);
|
||||
await Action();
|
||||
await Action(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,9 +89,9 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
private Channel<BackgroundJob> _Jobs = Channel.CreateUnbounded<BackgroundJob>();
|
||||
HashSet<Task> _Processing = new HashSet<Task>();
|
||||
public void Schedule(Func<Task> action, TimeSpan delay)
|
||||
public void Schedule(Func<CancellationToken, Task> act, TimeSpan scheduledIn)
|
||||
{
|
||||
_Jobs.Writer.TryWrite(new BackgroundJob(action, delay, Delay));
|
||||
_Jobs.Writer.TryWrite(new BackgroundJob(act, scheduledIn, Delay));
|
||||
}
|
||||
|
||||
public async Task WaitAllRunning(CancellationToken cancellationToken)
|
||||
@ -89,6 +99,8 @@ namespace BTCPayServer.HostedServices
|
||||
Task[] processing = null;
|
||||
lock (_Processing)
|
||||
{
|
||||
if (_Processing.Count == 0)
|
||||
return;
|
||||
processing = _Processing.ToArray();
|
||||
}
|
||||
|
||||
@ -96,9 +108,8 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
await Task.WhenAll(processing).WithCancellation(cancellationToken);
|
||||
}
|
||||
catch (Exception) when (cancellationToken.IsCancellationRequested)
|
||||
catch (Exception) when (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,8 +124,7 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
_Processing.Add(processing);
|
||||
}
|
||||
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
processing.ContinueWith(t =>
|
||||
_ = processing.ContinueWith(t =>
|
||||
{
|
||||
if (t.IsFaulted)
|
||||
{
|
||||
@ -125,7 +135,6 @@ namespace BTCPayServer.HostedServices
|
||||
_Processing.Remove(processing);
|
||||
}
|
||||
}, default, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
|
||||
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -59,6 +59,8 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_Cts == null)
|
||||
return Task.CompletedTask;
|
||||
_Cts.Cancel();
|
||||
return Task.WhenAll(_Tasks);
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ using BTCPayServer.Events;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Apps;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
@ -44,11 +45,20 @@ namespace BTCPayServer.HostedServices
|
||||
get { return _creativeStartUri; }
|
||||
}
|
||||
|
||||
|
||||
public bool ShowRegister { get; set; }
|
||||
public bool DiscourageSearchEngines { get; set; }
|
||||
|
||||
public AppType? RootAppType { get; set; }
|
||||
public string RootAppId { get; set; }
|
||||
|
||||
internal void Update(PoliciesSettings data)
|
||||
{
|
||||
ShowRegister = !data.LockSubscription;
|
||||
DiscourageSearchEngines = data.DiscourageSearchEngines;
|
||||
|
||||
RootAppType = data.RootAppType;
|
||||
RootAppId = data.RootAppId;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,7 +60,7 @@ namespace BTCPayServer.HostedServices
|
||||
_Subscriptions.Add(_EventAggregator.Subscribe<T>(e => _Events.Writer.TryWrite(e)));
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
public virtual Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Subscriptions = new List<IEventAggregatorSubscription>();
|
||||
SubscibeToEvents();
|
||||
@ -70,7 +70,7 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
Task _ProcessingEvents = Task.CompletedTask;
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
public virtual async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Subscriptions?.ForEach(subscription => subscription.Dispose());
|
||||
_Cts?.Cancel();
|
||||
|
@ -24,7 +24,7 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public class InvoiceNotificationManager : IHostedService
|
||||
{
|
||||
public static HttpClient _Client = new HttpClient();
|
||||
HttpClient _Client;
|
||||
|
||||
public class ScheduledJob
|
||||
{
|
||||
@ -46,6 +46,7 @@ namespace BTCPayServer.HostedServices
|
||||
private readonly EmailSenderFactory _EmailSenderFactory;
|
||||
|
||||
public InvoiceNotificationManager(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IBackgroundJobClient jobClient,
|
||||
EventAggregator eventAggregator,
|
||||
InvoiceRepository invoiceRepository,
|
||||
@ -53,6 +54,7 @@ namespace BTCPayServer.HostedServices
|
||||
ILogger<InvoiceNotificationManager> logger,
|
||||
EmailSenderFactory emailSenderFactory)
|
||||
{
|
||||
_Client = httpClientFactory.CreateClient();
|
||||
_JobClient = jobClient;
|
||||
_EventAggregator = eventAggregator;
|
||||
_InvoiceRepository = invoiceRepository;
|
||||
@ -135,23 +137,29 @@ namespace BTCPayServer.HostedServices
|
||||
return;
|
||||
var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Notification = notification });
|
||||
if (!string.IsNullOrEmpty(invoice.NotificationURL))
|
||||
_JobClient.Schedule(() => NotifyHttp(invoiceStr), TimeSpan.Zero);
|
||||
_JobClient.Schedule((cancellation) => NotifyHttp(invoiceStr, cancellation), TimeSpan.Zero);
|
||||
}
|
||||
|
||||
public async Task NotifyHttp(string invoiceData)
|
||||
public async Task NotifyHttp(string invoiceData, CancellationToken cancellationToken)
|
||||
{
|
||||
var job = NBitcoin.JsonConverters.Serializer.ToObject<ScheduledJob>(invoiceData);
|
||||
bool reschedule = false;
|
||||
var aggregatorEvent = new InvoiceIPNEvent(job.Notification.Data.Id, job.Notification.Event.Code, job.Notification.Event.Name);
|
||||
CancellationTokenSource cts = new CancellationTokenSource(10000);
|
||||
try
|
||||
{
|
||||
HttpResponseMessage response = await SendNotification(job.Notification, cts.Token);
|
||||
HttpResponseMessage response = await SendNotification(job.Notification, cancellationToken);
|
||||
reschedule = !response.IsSuccessStatusCode;
|
||||
aggregatorEvent.Error = reschedule ? $"Unexpected return code: {(int)response.StatusCode}" : null;
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(aggregatorEvent);
|
||||
}
|
||||
catch (OperationCanceledException) when (cts.IsCancellationRequested)
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// When the JobClient will be persistent, this will reschedule the job for after reboot
|
||||
invoiceData = NBitcoin.JsonConverters.Serializer.ToString(job);
|
||||
_JobClient.Schedule((cancellation) => NotifyHttp(invoiceData, cancellation), TimeSpan.FromMinutes(10.0));
|
||||
return;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
aggregatorEvent.Error = "Timeout";
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(aggregatorEvent);
|
||||
@ -172,14 +180,13 @@ namespace BTCPayServer.HostedServices
|
||||
aggregatorEvent.Error = $"Unexpected error: {message}";
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(aggregatorEvent);
|
||||
}
|
||||
finally { cts?.Dispose(); }
|
||||
|
||||
job.TryCount++;
|
||||
|
||||
if (job.TryCount < MaxTry && reschedule)
|
||||
{
|
||||
invoiceData = NBitcoin.JsonConverters.Serializer.ToString(job);
|
||||
_JobClient.Schedule(() => NotifyHttp(invoiceData), TimeSpan.FromMinutes(10.0));
|
||||
_JobClient.Schedule((cancellation) => NotifyHttp(invoiceData, cancellation), TimeSpan.FromMinutes(10.0));
|
||||
}
|
||||
}
|
||||
|
||||
@ -203,7 +210,7 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
|
||||
Encoding UTF8 = new UTF8Encoding(false);
|
||||
private async Task<HttpResponseMessage> SendNotification(InvoicePaymentNotificationEventWrapper notification, CancellationToken cancellation)
|
||||
private async Task<HttpResponseMessage> SendNotification(InvoicePaymentNotificationEventWrapper notification, CancellationToken cancellationToken)
|
||||
{
|
||||
var request = new HttpRequestMessage();
|
||||
request.Method = HttpMethod.Post;
|
||||
@ -224,7 +231,14 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
request.RequestUri = new Uri(notification.NotificationURL, UriKind.Absolute);
|
||||
request.Content = new StringContent(notificationString, UTF8, "application/json");
|
||||
var response = await Enqueue(notification.Data.Id, async () => await _Client.SendAsync(request, cancellation));
|
||||
var response = await Enqueue(notification.Data.Id, async () =>
|
||||
{
|
||||
using (CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
|
||||
{
|
||||
cts.CancelAfter(TimeSpan.FromMinutes(1.0));
|
||||
return await _Client.SendAsync(request, cts.Token);
|
||||
}
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
|
@ -43,16 +43,18 @@ namespace BTCPayServer.HostedServices
|
||||
InvoiceRepository _InvoiceRepository;
|
||||
EventAggregator _EventAggregator;
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
ExplorerClientProvider _ExplorerClientProvider;
|
||||
|
||||
public InvoiceWatcher(
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
InvoiceRepository invoiceRepository,
|
||||
EventAggregator eventAggregator)
|
||||
EventAggregator eventAggregator,
|
||||
ExplorerClientProvider explorerClientProvider)
|
||||
{
|
||||
PollInterval = TimeSpan.FromMinutes(1.0);
|
||||
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
|
||||
_EventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
|
||||
_NetworkProvider = networkProvider;
|
||||
_ExplorerClientProvider = explorerClientProvider;
|
||||
}
|
||||
CompositeDisposable leases = new CompositeDisposable();
|
||||
|
||||
@ -99,8 +101,8 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
if (accounting.Paid < accounting.MinimumTotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidPartial)
|
||||
{
|
||||
invoice.ExceptionStatus = InvoiceExceptionStatus.PaidPartial;
|
||||
context.MarkDirty();
|
||||
invoice.ExceptionStatus = InvoiceExceptionStatus.PaidPartial;
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
@ -185,19 +187,6 @@ namespace BTCPayServer.HostedServices
|
||||
return result;
|
||||
}
|
||||
|
||||
TimeSpan _PollInterval;
|
||||
public TimeSpan PollInterval
|
||||
{
|
||||
get
|
||||
{
|
||||
return _PollInterval;
|
||||
}
|
||||
set
|
||||
{
|
||||
_PollInterval = value;
|
||||
}
|
||||
}
|
||||
|
||||
private void Watch(string invoiceId)
|
||||
{
|
||||
if (invoiceId == null)
|
||||
@ -231,25 +220,24 @@ namespace BTCPayServer.HostedServices
|
||||
BlockingCollection<string> _WatchRequests = new BlockingCollection<string>(new ConcurrentQueue<string>());
|
||||
|
||||
Task _Loop;
|
||||
Task _WaitingInvoices;
|
||||
CancellationTokenSource _Cts;
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
_Loop = StartLoop(_Cts.Token);
|
||||
_WaitingInvoices = WaitPendingInvoices();
|
||||
_ = WaitPendingInvoices();
|
||||
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceNeedUpdateEvent>(b =>
|
||||
{
|
||||
Watch(b.InvoiceId);
|
||||
}));
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async b =>
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(b =>
|
||||
{
|
||||
if (b.Name == InvoiceEvent.Created)
|
||||
{
|
||||
Watch(b.Invoice.Id);
|
||||
await Wait(b.Invoice.Id);
|
||||
_ = Wait(b.Invoice.Id);
|
||||
}
|
||||
|
||||
if (b.Name == InvoiceEvent.ReceivedPayment)
|
||||
@ -264,79 +252,99 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
await Task.WhenAll((await _InvoiceRepository.GetPendingInvoices())
|
||||
.Select(id => Wait(id)).ToArray());
|
||||
_WaitingInvoices = null;
|
||||
}
|
||||
|
||||
async Task StartLoop(CancellationToken cancellation)
|
||||
{
|
||||
Logs.PayServer.LogInformation("Start watching invoices");
|
||||
await Task.Delay(1).ConfigureAwait(false); // Small hack so that the caller does not block on GetConsumingEnumerable
|
||||
try
|
||||
|
||||
foreach (var invoiceId in _WatchRequests.GetConsumingEnumerable(cancellation))
|
||||
{
|
||||
foreach (var invoiceId in _WatchRequests.GetConsumingEnumerable(cancellation))
|
||||
int maxLoop = 5;
|
||||
int loopCount = -1;
|
||||
while (loopCount < maxLoop)
|
||||
{
|
||||
int maxLoop = 5;
|
||||
int loopCount = -1;
|
||||
while (!cancellation.IsCancellationRequested && loopCount < maxLoop)
|
||||
loopCount++;
|
||||
try
|
||||
{
|
||||
loopCount++;
|
||||
try
|
||||
cancellation.ThrowIfCancellationRequested();
|
||||
var invoice = await _InvoiceRepository.GetInvoice(invoiceId, true);
|
||||
if (invoice == null)
|
||||
break;
|
||||
var updateContext = new UpdateInvoiceContext(invoice);
|
||||
await UpdateInvoice(updateContext);
|
||||
if (updateContext.Dirty)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(invoiceId, true);
|
||||
if (invoice == null)
|
||||
break;
|
||||
var updateContext = new UpdateInvoiceContext(invoice);
|
||||
await UpdateInvoice(updateContext);
|
||||
if (updateContext.Dirty)
|
||||
{
|
||||
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.GetInvoiceState());
|
||||
updateContext.Events.Insert(0, new InvoiceDataChangedEvent(invoice));
|
||||
}
|
||||
|
||||
foreach (var evt in updateContext.Events)
|
||||
{
|
||||
_EventAggregator.Publish(evt, evt.GetType());
|
||||
}
|
||||
|
||||
if (invoice.Status == InvoiceStatus.Complete ||
|
||||
((invoice.Status == InvoiceStatus.Invalid || invoice.Status == InvoiceStatus.Expired) && invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
|
||||
{
|
||||
if (await _InvoiceRepository.RemovePendingInvoice(invoice.Id))
|
||||
_EventAggregator.Publish(new InvoiceStopWatchedEvent(invoice.Id));
|
||||
break;
|
||||
}
|
||||
|
||||
if (updateContext.Events.Count == 0 || cancellation.IsCancellationRequested)
|
||||
break;
|
||||
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.GetInvoiceState());
|
||||
updateContext.Events.Insert(0, new InvoiceDataChangedEvent(invoice));
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellation.IsCancellationRequested)
|
||||
|
||||
foreach (var evt in updateContext.Events)
|
||||
{
|
||||
_EventAggregator.Publish(evt, evt.GetType());
|
||||
}
|
||||
|
||||
if (invoice.Status == InvoiceStatus.Complete ||
|
||||
((invoice.Status == InvoiceStatus.Invalid || invoice.Status == InvoiceStatus.Expired) && invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
|
||||
{
|
||||
var updateConfirmationCountIfNeeded = invoice
|
||||
.GetPayments()
|
||||
.Select<PaymentEntity, Task>(async payment =>
|
||||
{
|
||||
var paymentNetwork = _NetworkProvider.GetNetwork(payment.GetCryptoCode());
|
||||
var paymentData = payment.GetCryptoPaymentData();
|
||||
if (paymentData is Payments.Bitcoin.BitcoinLikePaymentData onChainPaymentData)
|
||||
{
|
||||
// Do update if confirmation count in the paymentData is not up to date
|
||||
if ((onChainPaymentData.ConfirmationCount < paymentNetwork.MaxTrackedConfirmation && payment.Accounted)
|
||||
&& (onChainPaymentData.Legacy || invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
|
||||
{
|
||||
var transactionResult = await _ExplorerClientProvider.GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(onChainPaymentData.Outpoint.Hash);
|
||||
var confirmationCount = transactionResult?.Confirmations ?? 0;
|
||||
onChainPaymentData.ConfirmationCount = confirmationCount;
|
||||
payment.SetCryptoPaymentData(onChainPaymentData);
|
||||
await _InvoiceRepository.UpdatePayments(new List<PaymentEntity> { payment });
|
||||
}
|
||||
}
|
||||
})
|
||||
.ToArray();
|
||||
await Task.WhenAll(updateConfirmationCountIfNeeded);
|
||||
|
||||
if (await _InvoiceRepository.RemovePendingInvoice(invoice.Id))
|
||||
_EventAggregator.Publish(new InvoiceStopWatchedEvent(invoice.Id));
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogError(ex, "Unhandled error on watching invoice " + invoiceId);
|
||||
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
Task.Delay(10000, cancellation)
|
||||
.ContinueWith(t => _WatchRequests.Add(invoiceId), TaskScheduler.Default);
|
||||
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
|
||||
if (updateContext.Events.Count == 0)
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (!cancellation.IsCancellationRequested)
|
||||
{
|
||||
Logs.PayServer.LogError(ex, "Unhandled error on watching invoice " + invoiceId);
|
||||
_ = Task.Delay(10000, cancellation)
|
||||
.ContinueWith(t => Watch(invoiceId), TaskScheduler.Default);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch when (cancellation.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
Logs.PayServer.LogInformation("Stop watching invoices");
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_Cts == null)
|
||||
return;
|
||||
leases.Dispose();
|
||||
_Cts.Cancel();
|
||||
var waitingPendingInvoices = _WaitingInvoices ?? Task.CompletedTask;
|
||||
return Task.WhenAll(waitingPendingInvoices, _Loop);
|
||||
try
|
||||
{
|
||||
await _Loop;
|
||||
}
|
||||
catch { }
|
||||
finally
|
||||
{
|
||||
Logs.PayServer.LogInformation("Stop watching invoices");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -71,6 +71,12 @@ namespace BTCPayServer.HostedServices
|
||||
settings.ConvertCrowdfundOldSettings = true;
|
||||
await _Settings.UpdateSetting(settings);
|
||||
}
|
||||
if (!settings.ConvertWalletKeyPathRoots)
|
||||
{
|
||||
await ConvertConvertWalletKeyPathRoots();
|
||||
settings.ConvertWalletKeyPathRoots = true;
|
||||
await _Settings.UpdateSetting(settings);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -79,6 +85,35 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ConvertConvertWalletKeyPathRoots()
|
||||
{
|
||||
bool save = false;
|
||||
using (var ctx = _DBContextFactory.CreateContext())
|
||||
{
|
||||
foreach (var store in await ctx.Stores.ToArrayAsync())
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
var blob = store.GetStoreBlob();
|
||||
if (blob.WalletKeyPathRoots == null)
|
||||
continue;
|
||||
foreach (var scheme in store.GetSupportedPaymentMethods(_NetworkProvider).OfType<DerivationSchemeSettings>())
|
||||
{
|
||||
if (blob.WalletKeyPathRoots.TryGetValue(scheme.PaymentId.ToString().ToLowerInvariant(), out var root))
|
||||
{
|
||||
scheme.AccountKeyPath = new NBitcoin.KeyPath(root);
|
||||
store.SetSupportedPaymentMethod(scheme);
|
||||
save = true;
|
||||
}
|
||||
}
|
||||
blob.WalletKeyPathRoots = null;
|
||||
store.SetStoreBlob(blob);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
if (save)
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ConvertCrowdfundOldSettings()
|
||||
{
|
||||
using (var ctx = _DBContextFactory.CreateContext())
|
||||
@ -159,7 +194,7 @@ namespace BTCPayServer.HostedServices
|
||||
if (lightning.IsLegacy)
|
||||
{
|
||||
method.SetLightningUrl(lightning);
|
||||
store.SetSupportedPaymentMethod(method.PaymentId, method);
|
||||
store.SetSupportedPaymentMethod(method);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -43,13 +43,13 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
public bool IsFullySynched(string cryptoCode, out NBXplorerSummary summary)
|
||||
{
|
||||
return _Summaries.TryGetValue(cryptoCode, out summary) &&
|
||||
return _Summaries.TryGetValue(cryptoCode.ToUpperInvariant(), out summary) &&
|
||||
summary.Status != null &&
|
||||
summary.Status.IsFullySynched;
|
||||
}
|
||||
public NBXplorerSummary Get(string cryptoCode)
|
||||
{
|
||||
_Summaries.TryGetValue(cryptoCode, out var summary);
|
||||
_Summaries.TryGetValue(cryptoCode.ToUpperInvariant(), out var summary);
|
||||
return summary;
|
||||
}
|
||||
public IEnumerable<NBXplorerSummary> GetAll()
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using NBitcoin;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@ -47,7 +48,7 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
await Task.WhenAll(_RateProviderFactory.Providers
|
||||
.Select(p => (Fetcher: p.Value as BackgroundFetcherRateProvider, ExchangeName: p.Key)).Where(p => p.Fetcher != null)
|
||||
.Select(p => p.Fetcher.UpdateIfNecessary().ContinueWith(t =>
|
||||
.Select(p => p.Fetcher.UpdateIfNecessary(timeout.Token).ContinueWith(t =>
|
||||
{
|
||||
if (t.Result.Exception != null)
|
||||
{
|
||||
@ -68,7 +69,7 @@ namespace BTCPayServer.HostedServices
|
||||
var exchanges = new CoinAverageExchanges();
|
||||
foreach (var item in (await new CoinAverageRateProvider() { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync())
|
||||
.Exchanges
|
||||
.Select(c => new CoinAverageExchange(c.Name, c.DisplayName)))
|
||||
.Select(c => new CoinAverageExchange(c.Name, c.DisplayName, $"https://apiv2.bitcoinaverage.com/exchanges/{c.Name}")))
|
||||
{
|
||||
exchanges.Add(item);
|
||||
}
|
||||
|
36
BTCPayServer/HostedServices/TorServicesHostedService.cs
Normal file
36
BTCPayServer/HostedServices/TorServicesHostedService.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public class TorServicesHostedService : BaseAsyncService
|
||||
{
|
||||
private readonly BTCPayServerOptions _options;
|
||||
private readonly TorServices _torServices;
|
||||
|
||||
public TorServicesHostedService(BTCPayServerOptions options, TorServices torServices)
|
||||
{
|
||||
_options = options;
|
||||
_torServices = torServices;
|
||||
}
|
||||
|
||||
internal override Task[] InitializeTasks()
|
||||
{
|
||||
// TODO: We should report auto configured services (like bitcoind, lnd or clightning)
|
||||
if (string.IsNullOrEmpty(_options.TorrcFile))
|
||||
return Array.Empty<Task>();
|
||||
return new Task[] { CreateLoopTask(RefreshTorServices) };
|
||||
}
|
||||
|
||||
async Task RefreshTorServices()
|
||||
{
|
||||
await _torServices.Refresh();
|
||||
await Task.Delay(TimeSpan.FromSeconds(120), Cancellation);
|
||||
}
|
||||
}
|
||||
}
|
@ -36,16 +36,19 @@ using BTCPayServer.Authentication;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.HostedServices;
|
||||
using Meziantou.AspNetCore.BundleTagHelpers;
|
||||
using System.Security.Claims;
|
||||
using BTCPayServer.PaymentRequest;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.PaymentRequests;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NicolasDorier.RateLimits;
|
||||
using Npgsql;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.U2F;
|
||||
using BundlerMinifier.TagHelpers;
|
||||
|
||||
namespace BTCPayServer.Hosting
|
||||
{
|
||||
@ -60,6 +63,9 @@ namespace BTCPayServer.Hosting
|
||||
});
|
||||
services.AddHttpClient();
|
||||
services.TryAddSingleton<SettingsRepository>();
|
||||
services.TryAddSingleton<TorServices>();
|
||||
services.TryAddSingleton<SocketFactory>();
|
||||
services.TryAddSingleton<LightningClientFactoryService>();
|
||||
services.TryAddSingleton<InvoicePaymentNotification>();
|
||||
services.TryAddSingleton<BTCPayServerOptions>(o => o.GetRequiredService<IOptions<BTCPayServerOptions>>().Value);
|
||||
services.TryAddSingleton<InvoiceRepository>(o =>
|
||||
@ -74,6 +80,8 @@ namespace BTCPayServer.Hosting
|
||||
services.AddSingleton<BTCPayServerEnvironment>();
|
||||
services.TryAddSingleton<TokenRepository>();
|
||||
services.TryAddSingleton<EventAggregator>();
|
||||
services.TryAddSingleton<PaymentRequestService>();
|
||||
services.TryAddSingleton<U2FService>();
|
||||
services.TryAddSingleton<CoinAverageSettings>();
|
||||
services.TryAddSingleton<ApplicationDbContextFactory>(o =>
|
||||
{
|
||||
@ -150,6 +158,7 @@ namespace BTCPayServer.Hosting
|
||||
services.TryAddSingleton<LanguageService>();
|
||||
services.TryAddSingleton<NBXplorerDashboard>();
|
||||
services.TryAddSingleton<StoreRepository>();
|
||||
services.TryAddSingleton<PaymentRequestRepository>();
|
||||
services.TryAddSingleton<BTCPayWalletProvider>();
|
||||
services.TryAddSingleton<CurrencyNameTable>();
|
||||
services.TryAddSingleton<IFeeProviderFactory>(o => new NBXplorerFeeProviderFactory(o.GetRequiredService<ExplorerClientProvider>())
|
||||
@ -167,7 +176,7 @@ namespace BTCPayServer.Hosting
|
||||
services.AddSingleton<IHostedService, CssThemeManagerHostedService>();
|
||||
services.AddSingleton<IHostedService, MigratorHostedService>();
|
||||
|
||||
services.AddSingleton<Payments.IPaymentMethodHandler<DerivationStrategy>, Payments.Bitcoin.BitcoinLikePaymentHandler>();
|
||||
services.AddSingleton<Payments.IPaymentMethodHandler<DerivationSchemeSettings>, Payments.Bitcoin.BitcoinLikePaymentHandler>();
|
||||
services.AddSingleton<IHostedService, Payments.Bitcoin.NBXplorerListener>();
|
||||
|
||||
services.AddSingleton<IHostedService, HostedServices.CheckConfigurationHostedService>();
|
||||
@ -184,7 +193,8 @@ namespace BTCPayServer.Hosting
|
||||
services.AddSingleton<IHostedService, RatesHostedService>();
|
||||
services.AddSingleton<IHostedService, BackgroundJobSchedulerHostedService>();
|
||||
services.AddSingleton<IHostedService, AppHubStreamer>();
|
||||
|
||||
services.AddSingleton<IHostedService, TorServicesHostedService>();
|
||||
services.AddSingleton<IHostedService, PaymentRequestStreamer>();
|
||||
services.AddSingleton<IBackgroundJobClient, BackgroundJobClient>();
|
||||
services.AddTransient<IConfigureOptions<MvcOptions>, BTCPayClaimsFilter>();
|
||||
|
||||
@ -203,6 +213,7 @@ namespace BTCPayServer.Hosting
|
||||
services.AddTransient<AccessTokenController>();
|
||||
services.AddTransient<InvoiceController>();
|
||||
services.AddTransient<AppsPublicController>();
|
||||
services.AddTransient<PaymentRequestController>();
|
||||
// Add application services.
|
||||
services.AddSingleton<EmailSenderFactory>();
|
||||
// bundling
|
||||
@ -210,7 +221,7 @@ namespace BTCPayServer.Hosting
|
||||
services.AddAuthorization(o => Policies.AddBTCPayPolicies(o));
|
||||
BitpayAuthentication.AddAuthentication(services);
|
||||
|
||||
services.AddBundles();
|
||||
services.AddSingleton<IBundleProvider, ResourceBundleProvider>();
|
||||
services.AddTransient<BundleOptions>(provider =>
|
||||
{
|
||||
var opts = provider.GetRequiredService<BTCPayServerOptions>();
|
||||
|
@ -32,8 +32,6 @@ namespace BTCPayServer.Hosting
|
||||
|
||||
public async Task Invoke(HttpContext httpContext)
|
||||
{
|
||||
RewriteHostIfNeeded(httpContext);
|
||||
|
||||
try
|
||||
{
|
||||
var bitpayAuth = GetBitpayAuth(httpContext, out bool isBitpayAuth);
|
||||
@ -90,6 +88,9 @@ namespace BTCPayServer.Hosting
|
||||
if (!httpContext.Request.Path.HasValue)
|
||||
return false;
|
||||
|
||||
// In case of anyone can create invoice, the storeId can be set explicitely
|
||||
bitpayAuth |= httpContext.Request.Query.ContainsKey("storeid");
|
||||
|
||||
var isJson = (httpContext.Request.ContentType ?? string.Empty).StartsWith("application/json", StringComparison.OrdinalIgnoreCase);
|
||||
var path = httpContext.Request.Path.Value;
|
||||
var method = httpContext.Request.Method;
|
||||
@ -97,7 +98,7 @@ namespace BTCPayServer.Hosting
|
||||
|
||||
if (
|
||||
(isCors || bitpayAuth) &&
|
||||
(path == "/invoices" || path == "/invoices/") &&
|
||||
(path == "/invoices" || path == "/invoices/") &&
|
||||
(isCors || (method == "POST" && isJson)))
|
||||
return true;
|
||||
|
||||
@ -125,83 +126,6 @@ namespace BTCPayServer.Hosting
|
||||
return false;
|
||||
}
|
||||
|
||||
private void RewriteHostIfNeeded(HttpContext httpContext)
|
||||
{
|
||||
string reverseProxyScheme = null;
|
||||
if (httpContext.Request.Headers.TryGetValue("X-Forwarded-Proto", out StringValues proto))
|
||||
{
|
||||
var scheme = proto.SingleOrDefault();
|
||||
if (scheme != null)
|
||||
{
|
||||
reverseProxyScheme = scheme;
|
||||
}
|
||||
}
|
||||
|
||||
ushort? reverseProxyPort = null;
|
||||
if (httpContext.Request.Headers.TryGetValue("X-Forwarded-Port", out StringValues port))
|
||||
{
|
||||
var portString = port.SingleOrDefault();
|
||||
if (portString != null && ushort.TryParse(portString, out ushort pp))
|
||||
{
|
||||
reverseProxyPort = pp;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure that code executing after this point think that the external url has been hit.
|
||||
if (_Options.ExternalUrl != null)
|
||||
{
|
||||
if (reverseProxyScheme != null && _Options.ExternalUrl.Scheme != reverseProxyScheme)
|
||||
{
|
||||
if (reverseProxyScheme == "http" && _Options.ExternalUrl.Scheme == "https")
|
||||
Logs.PayServer.LogWarning($"BTCPay ExternalUrl setting expected to use scheme '{_Options.ExternalUrl.Scheme}' externally, but the reverse proxy uses scheme '{reverseProxyScheme}' (X-Forwarded-Port), forcing ExternalUrl");
|
||||
}
|
||||
httpContext.Request.Scheme = _Options.ExternalUrl.Scheme;
|
||||
if (_Options.ExternalUrl.IsDefaultPort)
|
||||
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host);
|
||||
else
|
||||
{
|
||||
if (reverseProxyPort != null && _Options.ExternalUrl.Port != reverseProxyPort.Value)
|
||||
{
|
||||
Logs.PayServer.LogWarning($"BTCPay ExternalUrl setting expected to use port '{_Options.ExternalUrl.Port}' externally, but the reverse proxy uses port '{reverseProxyPort.Value}'");
|
||||
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host, reverseProxyPort.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host, _Options.ExternalUrl.Port);
|
||||
}
|
||||
}
|
||||
}
|
||||
// NGINX pass X-Forwarded-Proto and X-Forwarded-Port, so let's use that to have better guess of the real domain
|
||||
else
|
||||
{
|
||||
ushort? p = null;
|
||||
if (reverseProxyScheme != null)
|
||||
{
|
||||
httpContext.Request.Scheme = reverseProxyScheme;
|
||||
if (reverseProxyScheme == "http")
|
||||
p = 80;
|
||||
if (reverseProxyScheme == "https")
|
||||
p = 443;
|
||||
}
|
||||
|
||||
|
||||
if (reverseProxyPort != null)
|
||||
{
|
||||
p = reverseProxyPort.Value;
|
||||
}
|
||||
|
||||
if (p.HasValue)
|
||||
{
|
||||
bool isDefault = httpContext.Request.Scheme == "http" && p.Value == 80;
|
||||
isDefault |= httpContext.Request.Scheme == "https" && p.Value == 443;
|
||||
if (isDefault)
|
||||
httpContext.Request.Host = new HostString(httpContext.Request.Host.Host);
|
||||
else
|
||||
httpContext.Request.Host = new HostString(httpContext.Request.Host.Host, p.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task HandleBitpayHttpException(HttpContext httpContext, BitpayHttpException ex)
|
||||
{
|
||||
httpContext.Response.StatusCode = ex.StatusCode;
|
||||
|
50
BTCPayServer/Hosting/ResourceBundleProvider.cs
Normal file
50
BTCPayServer/Hosting/ResourceBundleProvider.cs
Normal file
@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BundlerMinifier.TagHelpers;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Hosting
|
||||
{
|
||||
public class ResourceBundleProvider : IBundleProvider
|
||||
{
|
||||
BundleProvider _InnerProvider;
|
||||
Lazy<Dictionary<string, Bundle>> _BundlesByName;
|
||||
public ResourceBundleProvider(IHostingEnvironment hosting, BundleOptions options)
|
||||
{
|
||||
if (options.UseBundles)
|
||||
{
|
||||
_BundlesByName = new Lazy<Dictionary<string, Bundle>>(() =>
|
||||
{
|
||||
using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("BTCPayServer.bundleconfig.json"))
|
||||
using (var reader = new StreamReader(stream, Encoding.UTF8))
|
||||
{
|
||||
var content = reader.ReadToEnd();
|
||||
return JArray.Parse(content).OfType<JObject>()
|
||||
.Select(jobj => new Bundle()
|
||||
{
|
||||
Name = jobj.Property("name", StringComparison.OrdinalIgnoreCase)?.Value.Value<string>() ?? jobj.Property("outputFileName", StringComparison.OrdinalIgnoreCase).Value.Value<string>(),
|
||||
OutputFileUrl = Path.Combine(hosting.ContentRootPath, jobj.Property("outputFileName", StringComparison.OrdinalIgnoreCase).Value.Value<string>())
|
||||
}).ToDictionary(o => o.Name, o => o);
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
_InnerProvider = new BundleProvider();
|
||||
}
|
||||
}
|
||||
public Bundle GetBundle(string name)
|
||||
{
|
||||
if (_InnerProvider != null)
|
||||
return _InnerProvider.GetBundle(name);
|
||||
_BundlesByName.Value.TryGetValue(name, out var bundle);
|
||||
return bundle;
|
||||
}
|
||||
}
|
||||
}
|
@ -3,7 +3,6 @@ using System.Reflection;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
@ -14,9 +13,9 @@ using BTCPayServer.Authentication;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using BTCPayServer.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Models;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using BTCPayServer.Logging;
|
||||
@ -34,9 +33,13 @@ using Microsoft.Extensions.Options;
|
||||
using Microsoft.AspNetCore.Mvc.Cors.Internal;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
using System.Net;
|
||||
using Meziantou.AspNetCore.BundleTagHelpers;
|
||||
using BTCPayServer.PaymentRequest;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Storage;
|
||||
using BTCPayServer.Storage.Services.Providers.FileSystemStorage;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
|
||||
namespace BTCPayServer.Hosting
|
||||
{
|
||||
@ -65,6 +68,8 @@ namespace BTCPayServer.Hosting
|
||||
.AddDefaultTokenProviders();
|
||||
services.AddSignalR();
|
||||
services.AddBTCPayServer();
|
||||
services.AddProviderStorage();
|
||||
services.AddSession();
|
||||
services.AddMvc(o =>
|
||||
{
|
||||
o.Filters.Add(new XFrameOptionsAttribute("DENY"));
|
||||
@ -79,7 +84,7 @@ namespace BTCPayServer.Hosting
|
||||
// StyleSrc = "'self' 'unsafe-inline'",
|
||||
// ScriptSrc = "'self' 'unsafe-inline'"
|
||||
//});
|
||||
});
|
||||
}).AddControllersAsServices();
|
||||
services.TryAddScoped<ContentSecurityPolicies>();
|
||||
services.Configure<IdentityOptions>(options =>
|
||||
{
|
||||
@ -158,13 +163,24 @@ namespace BTCPayServer.Hosting
|
||||
app.UseDeveloperExceptionPage();
|
||||
}
|
||||
|
||||
var forwardingOptions = new ForwardedHeadersOptions()
|
||||
{
|
||||
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
|
||||
};
|
||||
forwardingOptions.KnownNetworks.Clear();
|
||||
forwardingOptions.KnownProxies.Clear();
|
||||
forwardingOptions.ForwardedHeaders = ForwardedHeaders.All;
|
||||
app.UseForwardedHeaders(forwardingOptions);
|
||||
app.UseCors();
|
||||
app.UsePayServer();
|
||||
app.UseStaticFiles();
|
||||
app.UseProviderStorage(options);
|
||||
app.UseAuthentication();
|
||||
app.UseSession();
|
||||
app.UseSignalR(route =>
|
||||
{
|
||||
route.MapHub<AppHub>("/apps/hub");
|
||||
AppHub.Register(route);
|
||||
PaymentRequestHub.Register(route);
|
||||
});
|
||||
app.UseWebSockets();
|
||||
app.UseStatusCodePages();
|
||||
|
39
BTCPayServer/JsonConverters/CurrencyPairJsonConverter.cs
Normal file
39
BTCPayServer/JsonConverters/CurrencyPairJsonConverter.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using NBitcoin.JsonConverters;
|
||||
using BTCPayServer.Rating;
|
||||
|
||||
namespace BTCPayServer.JsonConverters
|
||||
{
|
||||
public class CurrencyPairJsonConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return typeof(CurrencyValue).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
|
||||
}
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
try
|
||||
{
|
||||
return reader.TokenType == JsonToken.Null ? null :
|
||||
CurrencyPair.TryParse((string)reader.Value, out var result) ? result :
|
||||
throw new JsonObjectException("Invalid currency pair", reader);
|
||||
}
|
||||
catch (InvalidCastException)
|
||||
{
|
||||
throw new JsonObjectException("Invalid currency pair", reader);
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
if (value != null)
|
||||
writer.WriteValue(value.ToString());
|
||||
}
|
||||
}
|
||||
}
|
@ -33,5 +33,35 @@ namespace BTCPayServer.Logging
|
||||
return _InvoiceLogs.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
internal IDisposable Measure(string logs)
|
||||
{
|
||||
return new Mesuring(this, logs);
|
||||
}
|
||||
|
||||
class Mesuring : IDisposable
|
||||
{
|
||||
private readonly InvoiceLogs _logs;
|
||||
private readonly string _msg;
|
||||
private readonly DateTimeOffset _Before;
|
||||
public Mesuring(InvoiceLogs logs, string msg)
|
||||
{
|
||||
_logs = logs;
|
||||
_msg = msg;
|
||||
_Before = DateTimeOffset.UtcNow;
|
||||
}
|
||||
public void Dispose()
|
||||
{
|
||||
var timespan = DateTimeOffset.UtcNow - _Before;
|
||||
if (timespan.TotalSeconds >= 1.0)
|
||||
{
|
||||
_logs.Write($"{_msg} took {(int)timespan.TotalSeconds} seconds");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logs.Write($"{_msg} took {(int)timespan.TotalMilliseconds} milliseconds");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
606
BTCPayServer/Migrations/20190121133309_AddPaymentRequests.Designer.cs
generated
Normal file
606
BTCPayServer/Migrations/20190121133309_AddPaymentRequests.Designer.cs
generated
Normal file
@ -0,0 +1,606 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20190121133309_AddPaymentRequests")]
|
||||
partial class AddPaymentRequests
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "2.1.4-rtm-31024");
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Address")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<DateTimeOffset?>("CreatedTime");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Address");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("AddressInvoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.Property<string>("StoreId")
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreId");
|
||||
|
||||
b.ToTable("ApiKeys");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("AppType");
|
||||
|
||||
b.Property<DateTimeOffset>("Created");
|
||||
|
||||
b.Property<string>("Name");
|
||||
|
||||
b.Property<string>("Settings");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("Apps");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.Property<string>("Address");
|
||||
|
||||
b.Property<DateTimeOffset>("Assigned");
|
||||
|
||||
b.Property<string>("CryptoCode");
|
||||
|
||||
b.Property<DateTimeOffset?>("UnAssigned");
|
||||
|
||||
b.HasKey("InvoiceDataId", "Address");
|
||||
|
||||
b.ToTable("HistoricalAddressInvoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<DateTimeOffset>("Created");
|
||||
|
||||
b.Property<string>("CustomerEmail");
|
||||
|
||||
b.Property<string>("ExceptionStatus");
|
||||
|
||||
b.Property<string>("ItemCode");
|
||||
|
||||
b.Property<string>("OrderId");
|
||||
|
||||
b.Property<string>("Status");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("Invoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
|
||||
{
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.Property<string>("UniqueId");
|
||||
|
||||
b.Property<string>("Message");
|
||||
|
||||
b.Property<DateTimeOffset>("Timestamp");
|
||||
|
||||
b.HasKey("InvoiceDataId", "UniqueId");
|
||||
|
||||
b.ToTable("InvoiceEvents");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("Facade");
|
||||
|
||||
b.Property<string>("Label");
|
||||
|
||||
b.Property<DateTimeOffset>("PairingTime");
|
||||
|
||||
b.Property<string>("SIN");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SIN");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("PairedSINData");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<DateTime>("DateCreated");
|
||||
|
||||
b.Property<DateTimeOffset>("Expiration");
|
||||
|
||||
b.Property<string>("Facade");
|
||||
|
||||
b.Property<string>("Label");
|
||||
|
||||
b.Property<string>("SIN");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.Property<string>("TokenValue");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PairingCodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<bool>("Accounted");
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("Payments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Id");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PendingInvoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("RefundAddresses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.SettingData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("Value");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("DefaultCrypto");
|
||||
|
||||
b.Property<string>("DerivationStrategies");
|
||||
|
||||
b.Property<string>("DerivationStrategy");
|
||||
|
||||
b.Property<int>("SpeedPolicy");
|
||||
|
||||
b.Property<byte[]>("StoreBlob");
|
||||
|
||||
b.Property<byte[]>("StoreCertificate");
|
||||
|
||||
b.Property<string>("StoreName");
|
||||
|
||||
b.Property<string>("StoreWebsite");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Stores");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
|
||||
{
|
||||
b.Property<string>("ApplicationUserId");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.Property<string>("Role");
|
||||
|
||||
b.HasKey("ApplicationUserId", "StoreDataId");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("UserStore");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Models.ApplicationUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<int>("AccessFailedCount");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken();
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<bool>("EmailConfirmed");
|
||||
|
||||
b.Property<bool>("LockoutEnabled");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<string>("PasswordHash");
|
||||
|
||||
b.Property<string>("PhoneNumber");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed");
|
||||
|
||||
b.Property<bool>("RequiresEmailConfirmation");
|
||||
|
||||
b.Property<string>("SecurityStamp");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasName("UserNameIndex");
|
||||
|
||||
b.ToTable("AspNetUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Services.PaymentRequests.PaymentRequestData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<int>("Status");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("PaymentRequests");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken();
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasName("RoleNameIndex");
|
||||
|
||||
b.ToTable("AspNetRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("ClaimType");
|
||||
|
||||
b.Property<string>("ClaimValue");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.IsRequired();
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetRoleClaims");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("ClaimType");
|
||||
|
||||
b.Property<string>("ClaimValue");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired();
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserClaims");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider");
|
||||
|
||||
b.Property<string>("ProviderKey");
|
||||
|
||||
b.Property<string>("ProviderDisplayName");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired();
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserLogins");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId");
|
||||
|
||||
b.Property<string>("RoleId");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetUserRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId");
|
||||
|
||||
b.Property<string>("LoginProvider");
|
||||
|
||||
b.Property<string>("Name");
|
||||
|
||||
b.Property<string>("Value");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("AddressInvoices")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("APIKeys")
|
||||
.HasForeignKey("StoreId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("Apps")
|
||||
.HasForeignKey("StoreDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("HistoricalAddressInvoices")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("Invoices")
|
||||
.HasForeignKey("StoreDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("Events")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("PairedSINs")
|
||||
.HasForeignKey("StoreDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("Payments")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("PendingInvoices")
|
||||
.HasForeignKey("Id")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("RefundAddresses")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser")
|
||||
.WithMany("UserStores")
|
||||
.HasForeignKey("ApplicationUserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("UserStores")
|
||||
.HasForeignKey("StoreDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Services.PaymentRequests.PaymentRequestData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("PaymentRequests")
|
||||
.HasForeignKey("StoreDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
47
BTCPayServer/Migrations/20190121133309_AddPaymentRequests.cs
Normal file
47
BTCPayServer/Migrations/20190121133309_AddPaymentRequests.cs
Normal file
@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
public partial class AddPaymentRequests : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PaymentRequests",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(nullable: false),
|
||||
StoreDataId = table.Column<string>(nullable: true),
|
||||
Status = table.Column<int>(nullable: false),
|
||||
Blob = table.Column<byte[]>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PaymentRequests", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_PaymentRequests_Stores_StoreDataId",
|
||||
column: x => x.StoreDataId,
|
||||
principalTable: "Stores",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PaymentRequests_Status",
|
||||
table: "PaymentRequests",
|
||||
column: "Status");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PaymentRequests_StoreDataId",
|
||||
table: "PaymentRequests",
|
||||
column: "StoreDataId");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "PaymentRequests");
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user