Compare commits

...

168 Commits

Author SHA1 Message Date
0136977359 update translations 2019-01-26 21:23:41 +09:00
0acd3e20b0 bump 2019-01-26 20:58:15 +09:00
30bdfeee37 Enhance PosData Viewer & add cart to posdata in POS app (#559) 2019-01-26 13:26:49 +09:00
7ea665d884 Merge pull request #557 from Kukks/master
Fix close invoice button for modal invoices #555
2019-01-25 20:48:45 +09:00
073edcfb12 Merge remote-tracking branch 'btcpayserver/master' 2019-01-25 12:41:20 +01:00
a645366a25 Fix close invoice button for modal invoices #555 2019-01-25 12:41:15 +01:00
12aa0b7abd Merge pull request #556 from ChekaZ/master
Support Bitcoinplus
2019-01-25 16:09:39 +09:00
3f98a50410 Support Bitcoinplus 2019-01-25 01:03:04 +01:00
24c8c076d5 Add taxIncluded field in invoice 2019-01-24 20:53:29 +09:00
37e6931d33 Improve help 2019-01-23 17:44:03 +09:00
86493568e9 Fix external services parsing 2019-01-23 13:31:00 +09:00
bb51436ae3 Accept absolute url for external services 2019-01-23 13:17:36 +09:00
854a55ac1a Merge branch 'store-level-email' 2019-01-22 21:39:55 +09:00
cfb4b080d3 Emails on store level 2019-01-22 21:38:39 +09:00
00aa2e4e17 Merge pull request #546 from britttttk/fix/message
Fix delete user message
2019-01-21 17:11:24 +09:00
69c67d99f6 Fix message for delete user 2019-01-20 21:19:01 -07:00
65596ec8c1 fix delete user message 2019-01-20 21:12:20 -07:00
49643cb00e Make CanScheduleBackgroundTasks more robust 2019-01-19 21:19:15 +09:00
35b0faee57 Merge pull request #541 from Horndev/patch-2
Improve exception messages in server configuration parsing.
2019-01-19 20:49:08 +09:00
88ef4d69b2 Improve help and exception messages.
Improve the messages passed to users and in exceptions when parsing the BTCPayServerOptions configuration.
2019-01-18 10:23:47 -04:00
575b6ca222 Improve error messages when the store has no payment method configured 2019-01-18 19:15:31 +09:00
b5a0e844d2 Cann GetInvoicesTotal in parallel 2019-01-17 23:40:47 +09:00
2642e11ce2 Fix amount format in wallet send 2019-01-17 23:37:39 +09:00
b4fe655efe Merge pull request #534 from sipsorcery/fixpaging
Small improvement to the paging buttons on the list invoices page
2019-01-17 11:14:31 +09:00
ffb761909a Merge pull request #535 from dalijolijo/master
Change default exchange for Bitcore
2019-01-17 11:10:55 +09:00
b443e1ac6e Change default exchange for Bitcore 2019-01-16 22:04:24 +00:00
a4792f54a7 Added bootstrap paging buttons to the invoice list page and fixed paging buttons. 2019-01-16 21:33:04 +01:00
686ae029e0 bump 2019-01-16 23:49:40 +09:00
f3fd2e7d0f Update translations 2019-01-16 23:08:48 +09:00
7efd9ba0a5 Fix ledger on firefox 2019-01-16 23:07:22 +09:00
1c2a6bb8a1 Delete unused code 2019-01-16 19:35:29 +09:00
7bcf1cbdd5 Remove references to hangfire 2019-01-16 19:30:03 +09:00
2aaa2544bd Do not send mail synchronously in InvoiceNotificationManager 2019-01-16 19:21:02 +09:00
d85f03ba20 Remove HangFire dependency 2019-01-16 19:15:09 +09:00
cfb51a6be4 Merge pull request #531 from Kukks/patch-2
fix merge bug
2019-01-16 15:58:29 +09:00
c9d778c94b Bump nbitcoin and nbxplorer 2019-01-16 15:16:41 +09:00
fd62f882de fix merge bug 2019-01-15 18:18:41 +01:00
adc050f190 Trim destination address 2019-01-16 01:19:37 +09:00
2d551b9fc5 bump 2019-01-16 00:13:03 +09:00
884acdde32 Disabled POLIS and Bitcoin because default exchange (cryptopedia) is down 2019-01-16 00:08:17 +09:00
8f896de794 Merge pull request #516 from Kukks/feature/crowdfund
Crowdfund Bug fixes
2019-01-15 23:57:29 +09:00
5e4e26d2fd Merge pull request #529 from Kukks/bugfix/app-proper-redirect
fix app redirect to app instead of root url
2019-01-15 23:54:54 +09:00
ae688e6615 Merge pull request #530 from Horndev/patch-1
Grammatical corrections in exception messages
2019-01-15 23:54:20 +09:00
c4c812bdf6 Remove cryptopia from directly queried exchanges 2019-01-15 23:53:32 +09:00
e620fc0283 Add expert mode to BTCPay with No Change UTXO option 2019-01-15 23:50:45 +09:00
c333902468 Round up invoice price 2019-01-15 22:12:29 +09:00
4c83ecd06a Remove unused code 2019-01-15 21:56:33 +09:00
b28a547dc4 Grammatical corrections in exception messages
Fixed a few grammatical errors in LightningLikePaymentHandler.
2019-01-15 12:21:31 +00:00
6bc17e05bd add ids for better styling possibilities 2019-01-15 13:12:19 +01:00
0903350d30 add more log 2019-01-15 12:12:17 +01:00
6c0f19b457 Merge branch 'master' into feature/crowdfund 2019-01-15 09:48:16 +01:00
e119dc823f fix app redirect to app instead of root url 2019-01-15 09:46:07 +01:00
43295c9c57 Merge pull request #528 from bolatovumar/master
Update .NET Core SDK version in documentation
2019-01-15 16:42:40 +09:00
ded8b54042 Merge pull request #524 from Kukks/escapedstorename
Fix Store name character escaping on paid invoice #522
2019-01-15 16:41:51 +09:00
50a3178d51 Update .NET Core SDK version in documentation
Address #523
2019-01-14 16:49:12 -08:00
393c226032 fix escaped store name in return 2019-01-14 09:32:22 +01:00
f2630df387 dispose streamer properly 2019-01-14 08:21:27 +01:00
abcd2c1750 add padding when disqus enabled 2019-01-14 08:01:07 +01:00
cc95f3b5b5 fix exponent numbers in contribution amounts 2019-01-14 07:53:03 +01:00
a08ee93b43 fix issue with perk ordering 2019-01-14 07:45:21 +01:00
4b90f873d5 Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-11 10:52:31 +01:00
419ab8e0b1 add loader and fix perk badge zindex 2019-01-11 10:52:21 +01:00
c95ef27998 bump 2019-01-11 00:17:58 +09:00
63dfd93834 Merge pull request #511 from Kukks/feature/crowdfund
crowdfund: change text
2019-01-11 00:17:37 +09:00
57610881de change text 2019-01-10 16:12:00 +01:00
7469faf296 Merge pull request #510 from Kukks/feature/crowdfund
fix notif number
2019-01-10 23:58:57 +09:00
55a884a559 fix js number 2019-01-10 15:57:30 +01:00
ee2b3c3d10 bump 2019-01-10 23:41:08 +09:00
e5819a260b Merge pull request #509 from Kukks/feature/crowdfund
missed commit crowdfund
2019-01-10 23:40:52 +09:00
a3ecf48702 fix pos update too 2019-01-10 15:37:50 +01:00
1c0b904cd2 Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-10 15:35:19 +01:00
072d8a1728 fix exponents in js product editor 2019-01-10 15:35:03 +01:00
964e541c32 Update translations 2019-01-10 23:33:12 +09:00
78fec4ed22 bump 2019-01-10 23:31:35 +09:00
ef111d36c9 Merge pull request #484 from Kukks/feature/crowdfund
New App: Crowdfunding 🎉 🎉
2019-01-10 23:30:31 +09:00
4f64193e85 add rank badge to minimal and fix css in minimal 2019-01-10 14:54:41 +01:00
89bb6d1268 add validation for ranking 2019-01-10 14:43:47 +01:00
9f4226bf0f remove inline styles and fix checkbox setting text 2019-01-10 14:19:06 +01:00
a87c2a3374 Merge remote-tracking branch 'origin/master' into feature/crowdfund 2019-01-10 09:50:22 +01:00
d7294ba5a0 fix product item template 2019-01-10 09:28:51 +01:00
82d286dc6f Fix test 2019-01-10 14:00:26 +09:00
1fa18ab997 Merge pull request #507 from hubiktomas/patch-1
Typo fix
2019-01-10 13:49:52 +09:00
afc90f32c9 Fix tests 2019-01-10 13:47:21 +09:00
e9cfb7c21e Update link to accounting doc 2019-01-10 13:08:25 +09:00
1af8ea3769 Typo fix 2019-01-09 17:35:32 +01:00
9f7af190f1 fix ranking style 2019-01-09 15:44:16 +01:00
9c703fe94d fix number issue 2019-01-09 12:55:02 +01:00
a7a11a4f13 Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-09 12:22:42 +01:00
c32c3bb62b add contribution ranking 2019-01-09 12:22:36 +01:00
e29d1480a6 Add link to doc for export 2019-01-09 17:28:30 +09:00
8f299d7791 Fix build 2019-01-09 17:25:46 +09:00
65fb2e992e Round InvoiceDue and PaidCurrency in export 2019-01-09 17:18:01 +09:00
41f5d677d5 Merge pull request #491 from bitcoinshirt/bitcoinshirt-patch-ny
Update 2019 license
2019-01-09 13:33:56 +09:00
a2b78b8cd9 Merge pull request #506 from britttttk/fix/PasswordLength
Fix registration password length
2019-01-09 13:33:41 +09:00
c93f217033 Fix minimum registration password length 2019-01-08 18:32:07 -07:00
82c47b6e9a fix margin on crowdfund 2019-01-08 21:42:11 +01:00
94fb738c67 fix choice key and currency data 2019-01-08 15:49:07 +01:00
89071e40fc oops 2019-01-08 15:14:06 +01:00
95a90c410e Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-08 15:10:13 +01:00
59fc371cd5 perk count + img fixer 2019-01-08 15:10:05 +01:00
caadfc8641 use bolt icon in view 2019-01-08 13:52:44 +01:00
bffc2e70c1 use summernote instead 2019-01-08 13:52:30 +01:00
ca28c34be0 fix ln payment calculator 2019-01-08 10:32:10 +01:00
196bc3ea00 Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-08 09:50:50 +01:00
04cba61888 add bundle helper 2019-01-07 14:40:51 +01:00
a41e2e1ceb Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-07 14:39:08 +01:00
d1d03c98ba pr changes 2019-01-07 14:39:04 +01:00
3e48a54ab5 Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-07 14:25:41 +01:00
f6e389ff62 fix issues 2019-01-07 14:25:35 +01:00
561ec57cc8 Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-07 11:13:02 +01:00
c63feb488c Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-07 10:58:52 +01:00
f2ccc4d963 Add sanity check in loading crowdfun 2019-01-06 14:44:51 +01:00
a92d48efdd move button below help text 2019-01-06 14:37:40 +01:00
b633206b45 add helpful texts 2019-01-06 14:28:53 +01:00
b6f3d2af5e Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-06 14:12:25 +01:00
5ca4494eed reorder options in update crowdfund 2019-01-06 13:51:40 +01:00
de7e419ef4 fix overflow of descriptions 2019-01-06 13:50:30 +01:00
20a6b3fc33 Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-06 10:25:24 +01:00
e6357d2ac8 fix build 2019-01-06 09:29:21 +01:00
1eecd85ceb Merge branch 'master' into feature/crowdfund 2019-01-06 09:26:58 +01:00
d63176da19 Update BTCPayServer.csproj 2019-01-05 22:38:44 +01:00
887da5aa9a new year 2019-01-05 22:22:19 +01:00
6e7f1151bc bug fixes and optimizations 2019-01-05 19:47:39 +01:00
fb6d852827 switc back to regtest 2019-01-05 10:18:01 +01:00
ba17612461 Link to associated invoices 2019-01-05 10:17:52 +01:00
a15c7a0213 change crowdfund app prefix to not break invoice searcher 2019-01-05 09:53:57 +01:00
7e321d4016 expand list invoices search 2019-01-05 09:49:06 +01:00
2ccf007b9a fix permissions 2019-01-05 09:38:27 +01:00
895b8c2c80 ux fixes 2019-01-05 09:18:15 +01:00
493466683c start adding UTs 2019-01-04 16:42:35 +01:00
761c342c51 add validation 2019-01-04 13:47:06 +01:00
5341da28d9 add date time picker 2019-01-04 12:58:29 +01:00
7768f41849 add reset every x amount of time feature 2019-01-04 11:42:37 +01:00
c52a49f747 add minimal crowdfund version 2019-01-02 14:08:30 +01:00
e4b9895ba7 add rich text and options 2019-01-02 12:47:06 +01:00
92a2bb4d32 fixes to computed goal result 2019-01-02 12:04:35 +01:00
bfec722312 protect contrib endpoint when needed 2019-01-02 11:29:47 +01:00
5a3f7b5b70 add in more info and simplify backend model 2019-01-02 10:41:54 +01:00
2aa097be46 fix cache expiration time 2019-01-02 09:45:04 +01:00
8a646d85c6 fix bundles 2019-01-02 09:03:20 +01:00
890b3eaa00 remove api key for disqus 2019-01-02 08:10:42 +01:00
cda28ebf15 ui fixes + toggle sound options 2018-12-31 13:20:00 +01:00
2245027ca3 ux fixes 2018-12-31 12:34:27 +01:00
3dc250f801 Add Disqus & fix ux 2018-12-31 11:38:05 +01:00
66e786a1b0 styles and perks 2018-12-30 20:28:36 +01:00
8bd7ea5bbc start ux for perks 2018-12-29 20:21:23 +01:00
6eb36abe2e start contrib perks 2018-12-29 11:52:07 +01:00
6fced3fab2 better tooltips and icons 2018-12-29 10:19:50 +01:00
774e456e54 cleanup 2018-12-28 23:57:39 +01:00
519859e1c5 ux fixwes 2018-12-28 23:31:41 +01:00
26f0c488e5 hook up proper payments to events and super crazy ux shit 2018-12-28 23:12:16 +01:00
1b0b53fbd0 small ux fixes 2018-12-28 18:23:32 +01:00
35f4ea29f9 more integration 2018-12-28 17:38:20 +01:00
d9426d301d Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2018-12-28 12:08:32 +01:00
8bcf7109a3 integrate invoice popup 2018-12-28 12:07:15 +01:00
b11f8acba1 wip 2018-12-28 00:10:03 +01:00
ef9a633aa4 fixes for hub 2018-12-27 20:55:46 +01:00
c7e2f979dd Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2018-12-27 20:19:29 +01:00
e97bb9c933 work on vue and signalr fro crowdfund 2018-12-27 20:19:21 +01:00
2b84791391 fix raw html 2018-12-22 21:03:43 +01:00
9a488c60f2 fix some styling 2018-12-22 17:30:54 +01:00
9cb50446f4 update bootstrap to 4.2.1 2018-12-22 16:55:24 +01:00
8c00a2359e better layout 2018-12-22 15:43:40 +01:00
d1ff34d16d add minimal crowdfund system and UI 2018-12-22 15:02:16 +01:00
8e8615dab8 Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2018-12-21 11:51:13 +01:00
27bde55f54 work on building the viewmodel for crowdfund 2018-12-18 16:27:03 +01:00
b5d360594a Merge remote-tracking branch 'origin/master' into feature/crowdfund 2018-12-18 13:29:22 +01:00
7fa1b65af0 initial commit 2018-12-11 16:36:25 +01:00
123 changed files with 11853 additions and 559 deletions

View File

@ -0,0 +1,252 @@
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.Crowdfund;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Hubs;
using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
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 CrowdfundTests
{
public CrowdfundTests(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact]
[Trait("Integration", "Integration")]
public void CanCreateAndDeleteCrowdfundApp()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var user2 = tester.NewAccount();
user2.GrantAccess();
var apps = user.GetController<AppsController>();
var apps2 = user2.GetController<AppsController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
Assert.NotNull(vm.SelectedAppType);
Assert.Null(vm.Name);
vm.Name = "test";
vm.SelectedAppType = AppType.Crowdfund.ToString();
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result);
Assert.Equal(nameof(apps.UpdateCrowdfund), redirectToAction.ActionName);
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model);
var appList2 =
Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps2.ListApps().Result).Model);
Assert.Single(appList.Apps);
Assert.Empty(appList2.Apps);
Assert.Equal("test", appList.Apps[0].AppName);
Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id);
Assert.True(appList.Apps[0].IsOwner);
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id).Result);
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id).Result);
redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result);
Assert.Equal(nameof(apps.ListApps), redirectToAction.ActionName);
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model);
Assert.Empty(appList.Apps);
}
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanContributeOnlyWhenAllowed()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var apps = user.GetController<AppsController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
vm.Name = "test";
vm.SelectedAppType = AppType.Crowdfund.ToString();
Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result);
var appId = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model)
.Apps[0].Id;
//Scenario 1: Not Enabled - Not Allowed
var crowdfundViewModel = Assert.IsType<UpdateCrowdfundViewModel>(Assert
.IsType<ViewResult>(apps.UpdateCrowdfund(appId).Result).Model);
crowdfundViewModel.TargetCurrency = "BTC";
crowdfundViewModel.Enabled = false;
crowdfundViewModel.EndDate = null;
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
var anonAppPubsController = tester.PayTester.GetController<AppsPublicController>();
var publicApps = user.GetController<AppsPublicController>();
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
{
Amount = new decimal(0.01)
}));
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(appId, string.Empty));
//Scenario 2: Not Enabled But Admin - Allowed
Assert.IsType<OkObjectResult>(await publicApps.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
{
RedirectToCheckout = false,
Amount = new decimal(0.01)
}));
Assert.IsType<ViewResult>(await publicApps.ViewCrowdfund(appId, string.Empty));
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(appId, string.Empty));
//Scenario 3: Enabled But Start Date > Now - Not Allowed
crowdfundViewModel.StartDate= DateTime.Today.AddDays(2);
crowdfundViewModel.Enabled = true;
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
{
Amount = new decimal(0.01)
}));
//Scenario 4: Enabled But End Date < Now - Not Allowed
crowdfundViewModel.StartDate= DateTime.Today.AddDays(-2);
crowdfundViewModel.EndDate= DateTime.Today.AddDays(-1);
crowdfundViewModel.Enabled = true;
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
{
Amount = new decimal(0.01)
}));
//Scenario 5: Enabled and within correct timeframe, however target is enforced and Amount is Over - Not Allowed
crowdfundViewModel.StartDate= DateTime.Today.AddDays(-2);
crowdfundViewModel.EndDate= DateTime.Today.AddDays(2);
crowdfundViewModel.Enabled = true;
crowdfundViewModel.TargetAmount = 1;
crowdfundViewModel.TargetCurrency = "BTC";
crowdfundViewModel.EnforceTargetAmount = true;
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
{
Amount = new decimal(1.01)
}));
//Scenario 6: Allowed
Assert.IsType<OkObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
{
Amount = new decimal(0.05)
}));
}
}
[Fact]
[Trait("Integration", "Integration")]
public void CanComputeCrowdfundModel()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var apps = user.GetController<AppsController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
vm.Name = "test";
vm.SelectedAppType = AppType.Crowdfund.ToString();
Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result);
var appId = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model)
.Apps[0].Id;
var crowdfundViewModel = Assert.IsType<UpdateCrowdfundViewModel>(Assert
.IsType<ViewResult>(apps.UpdateCrowdfund(appId).Result).Model);
crowdfundViewModel.Enabled = true;
crowdfundViewModel.EndDate = null;
crowdfundViewModel.TargetAmount = 100;
crowdfundViewModel.TargetCurrency = "BTC";
crowdfundViewModel.UseAllStoreInvoices = true;
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
var anonAppPubsController = tester.PayTester.GetController<AppsPublicController>();
var publicApps = user.GetController<AppsPublicController>();
var model = Assert.IsType<ViewCrowdfundViewModel>(Assert
.IsType<ViewResult>(publicApps.ViewCrowdfund(appId, String.Empty).Result).Model);
Assert.Equal(crowdfundViewModel.TargetAmount, model.TargetAmount );
Assert.Equal(crowdfundViewModel.EndDate, model.EndDate );
Assert.Equal(crowdfundViewModel.StartDate, model.StartDate );
Assert.Equal(crowdfundViewModel.TargetCurrency, model.TargetCurrency );
Assert.Equal(0m, model.Info.CurrentAmount );
Assert.Equal(0m, model.Info.CurrentPendingAmount);
Assert.Equal(0m, model.Info.ProgressPercentage);
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Buyer = new Buyer() { email = "test@fwf.com" },
Price = 1m,
Currency = "BTC",
PosData = "posData",
OrderId = $"{CrowdfundHubStreamer.CrowdfundInvoiceOrderIdPrefix}{appId}",
ItemDesc = "Some description",
TransactionSpeed = "high",
FullNotifications = true
}, Facade.Merchant);
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
.IsType<ViewResult>(publicApps.ViewCrowdfund(appId, String.Empty).Result).Model);
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network);
tester.ExplorerNode.SendToAddress(invoiceAddress,invoice.BtcDue);
Assert.Equal(0m ,model.Info.CurrentAmount );
Assert.Equal(1m, model.Info.CurrentPendingAmount);
Assert.Equal( 0m, model.Info.ProgressPercentage);
Assert.Equal(1m, model.Info.PendingProgressPercentage);
}
}
}
}

View File

@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace BTCPayServer.Tests
{
public class MockDelay : IDelay
{
class WaitObj
{
public DateTimeOffset Expiration;
public TaskCompletionSource<bool> CTS;
}
List<WaitObj> waits = new List<WaitObj>();
DateTimeOffset _Now = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero);
public async Task Wait(TimeSpan delay, CancellationToken cancellation)
{
WaitObj w = new WaitObj();
w.Expiration = _Now + delay;
w.CTS = new TaskCompletionSource<bool>();
using (cancellation.Register(() =>
{
w.CTS.TrySetCanceled();
}))
{
lock (waits)
{
waits.Add(w);
}
await w.CTS.Task;
}
}
public void Advance(TimeSpan time)
{
_Now += time;
lock (waits)
{
foreach (var wait in waits.ToArray())
{
if (_Now >= wait.Expiration)
{
wait.CTS.TrySetResult(true);
waits.Remove(wait);
}
}
}
}
public void AdvanceMilliseconds(long milli)
{
Advance(TimeSpan.FromMilliseconds(milli));
}
public override string ToString()
{
return _Now.Millisecond.ToString(CultureInfo.InvariantCulture);
}
}
}

View File

@ -329,7 +329,7 @@ namespace BTCPayServer.Tests
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network);
tester.ExplorerNode.SendToAddress(invoiceAddress, Money.Satoshis((decimal)invoice.BtcDue.Satoshi * 0.75m));
Eventually(() =>
TestUtils.Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("paid", localInvoice.Status);
@ -451,7 +451,7 @@ namespace BTCPayServer.Tests
});
await Task.Delay(TimeSpan.FromMilliseconds(1000)); // Give time to listen the new invoices
await tester.SendLightningPaymentAsync(invoice);
await EventuallyAsync(async () =>
await TestUtils.EventuallyAsync(async () =>
{
var localInvoice = await user.BitPay.GetInvoiceAsync(invoice.Id);
Assert.Equal("complete", localInvoice.Status);
@ -685,7 +685,7 @@ namespace BTCPayServer.Tests
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
var firstPayment = invoice.CryptoInfo[0].TotalDue - Money.Satoshis(10);
cashCow.SendToAddress(invoiceAddress, firstPayment);
Eventually(() =>
TestUtils.Eventually(() =>
{
invoice = acc.BitPay.GetInvoice(invoice.Id);
Assert.Equal(firstPayment, invoice.CryptoInfo[0].Paid);
@ -782,7 +782,7 @@ namespace BTCPayServer.Tests
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, user.SupportedNetwork.NBitcoinNetwork);
Logs.Tester.LogInformation($"The invoice should be paidOver");
Eventually(() =>
TestUtils.Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal(payment1, invoice.BtcPaid);
@ -811,7 +811,7 @@ namespace BTCPayServer.Tests
Assert.Equal(tx2, ((NewTransactionEvent)listener.NextEvent(cts.Token)).TransactionData.TransactionHash);
}
Logs.Tester.LogInformation($"The invoice should now not be paidOver anymore");
Eventually(() =>
TestUtils.Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal(payment2, invoice.BtcPaid);
@ -1058,7 +1058,7 @@ namespace BTCPayServer.Tests
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
var firstPayment = Money.Coins(0.1m);
cashCow.SendToAddress(invoiceAddress, firstPayment);
Eventually(() =>
TestUtils.Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal(firstPayment, invoice.CryptoInfo[0].Paid);
@ -1079,7 +1079,7 @@ namespace BTCPayServer.Tests
Assert.NotEqual(invoice.BtcDue, invoice.CryptoInfo[0].Due); // Should be BTC rate
cashCow.SendToAddress(invoiceAddress, invoice.CryptoInfo[0].Due);
Eventually(() =>
TestUtils.Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal("paid", invoice.Status);
@ -1177,7 +1177,7 @@ namespace BTCPayServer.Tests
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
var firstPayment = Money.Coins(0.04m);
cashCow.SendToAddress(invoiceAddress, firstPayment);
Eventually(() =>
TestUtils.Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.True(invoice.BtcPaid == firstPayment);
@ -1219,7 +1219,7 @@ namespace BTCPayServer.Tests
firstPayment = Money.Coins(0.04m);
cashCow.SendToAddress(invoiceAddress, firstPayment);
Logs.Tester.LogInformation("First payment sent to " + invoiceAddress);
Eventually(() =>
TestUtils.Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.True(invoice.BtcPaid == firstPayment);
@ -1233,7 +1233,7 @@ namespace BTCPayServer.Tests
cashCow.Generate(2); // LTC is not worth a lot, so just to make sure we have money...
cashCow.SendToAddress(invoiceAddress, secondPayment);
Logs.Tester.LogInformation("Second payment sent to " + invoiceAddress);
Eventually(() =>
TestUtils.Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal(Money.Zero, invoice.BtcDue);
@ -1549,23 +1549,96 @@ donation:
}
}
[Fact]
[Trait("Fast", "Fast")]
public void CanScheduleBackgroundTasks()
{
BackgroundJobClient client = new BackgroundJobClient();
MockDelay mockDelay = new MockDelay();
client.Delay = mockDelay;
bool[] jobs = new bool[4];
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
Logs.Tester.LogInformation("Start Job[0] in 5 sec");
client.Schedule(async () => { Logs.Tester.LogInformation("Job[0]"); jobs[0] = true; }, TimeSpan.FromSeconds(5.0));
Logs.Tester.LogInformation("Start Job[1] in 2 sec");
client.Schedule(async () => { Logs.Tester.LogInformation("Job[1]"); jobs[1] = true; }, TimeSpan.FromSeconds(2.0));
Logs.Tester.LogInformation("Start Job[2] fails in 6 sec");
client.Schedule(async () => { jobs[2] = true; throw new Exception("Job[2]"); }, TimeSpan.FromSeconds(6.0));
Logs.Tester.LogInformation("Start Job[3] starts in in 7 sec");
client.Schedule(async () => { Logs.Tester.LogInformation("Job[3]"); jobs[3] = true; }, TimeSpan.FromSeconds(7.0));
Assert.True(new[] { false, false, false, false }.SequenceEqual(jobs));
CancellationTokenSource cts = new CancellationTokenSource();
var processing = client.ProcessJobs(cts.Token);
Assert.Equal(4, client.GetExecutingCount());
var waitJobsFinish = client.WaitAllRunning(default);
mockDelay.Advance(TimeSpan.FromSeconds(2.0));
Assert.True(new[] { false, true, false, false }.SequenceEqual(jobs));
mockDelay.Advance(TimeSpan.FromSeconds(3.0));
Assert.True(new[] { true, true, false, false }.SequenceEqual(jobs));
mockDelay.Advance(TimeSpan.FromSeconds(1.0));
Assert.True(new[] { true, true, true, false }.SequenceEqual(jobs));
Assert.Equal(1, client.GetExecutingCount());
Assert.False(waitJobsFinish.Wait(100));
Assert.False(waitJobsFinish.IsCompletedSuccessfully);
mockDelay.Advance(TimeSpan.FromSeconds(1.0));
Assert.True(new[] { true, true, true, true }.SequenceEqual(jobs));
Assert.True(waitJobsFinish.Wait(100));
Assert.True(waitJobsFinish.IsCompletedSuccessfully);
Assert.True(!waitJobsFinish.IsFaulted);
Assert.Equal(0, client.GetExecutingCount());
bool jobExecuted = false;
Logs.Tester.LogInformation("This job will be cancelled");
client.Schedule(async () => { jobExecuted = true; }, TimeSpan.FromSeconds(1.0));
mockDelay.Advance(TimeSpan.FromSeconds(0.5));
Assert.False(jobExecuted);
Thread.Sleep(100);
Assert.Equal(1, client.GetExecutingCount());
waitJobsFinish = client.WaitAllRunning(default);
Assert.False(waitJobsFinish.Wait(100));
cts.Cancel();
Assert.True(waitJobsFinish.Wait(1000));
Assert.True(waitJobsFinish.IsCompletedSuccessfully);
Assert.True(!waitJobsFinish.IsFaulted);
Assert.False(jobExecuted);
mockDelay.Advance(TimeSpan.FromSeconds(1.0));
Thread.Sleep(100); // Make sure it get cancelled
Assert.False(jobExecuted);
Assert.Equal(0, client.GetExecutingCount());
Assert.True(processing.IsCanceled);
Assert.True(client.WaitAllRunning(default).Wait(100));
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
}
[Fact]
[Trait("Fast", "Fast")]
public void PosDataParser_ParsesCorrectly()
{
var testCases =
new List<(string input, Dictionary<string, string> expectedOutput)>()
new List<(string input, Dictionary<string, object> expectedOutput)>()
{
{ (null, new Dictionary<string, string>())},
{("", new Dictionary<string, string>())},
{("{}", new Dictionary<string, string>())},
{("non-json-content", new Dictionary<string, string>(){ {string.Empty, "non-json-content"}})},
{("[1,2,3]", new Dictionary<string, string>(){ {string.Empty, "[1,2,3]"}})},
{("{ \"key\": \"value\"}", new Dictionary<string, string>(){ {"key", "value"}})},
{("{ \"key\": true}", new Dictionary<string, string>(){ {"key", "True"}})},
{("{ \"key\": \"value\", \"key2\": [\"value\", \"value2\"]}",
new Dictionary<string, string>(){ {"key", "value"}, {"key2", "value,value2"}})},
{("{ invalidjson file here}", new Dictionary<string, string>(){ {String.Empty, "{ invalidjson file here}"}})}
{ (null, new Dictionary<string, object>())},
{("", new Dictionary<string, object>())},
{("{}", new Dictionary<string, object>())},
{("non-json-content", new Dictionary<string, object>(){ {string.Empty, "non-json-content"}})},
{("[1,2,3]", new Dictionary<string, object>(){ {string.Empty, "[1,2,3]"}})},
{("{ \"key\": \"value\"}", new Dictionary<string, object>(){ {"key", "value"}})},
{("{ \"key\": true}", new Dictionary<string, object>(){ {"key", "True"}})},
{("{ invalidjson file here}", new Dictionary<string, object>(){ {String.Empty, "{ invalidjson file here}"}})}
};
testCases.ForEach(tuple =>
@ -1588,18 +1661,16 @@ donation:
var controller = tester.PayTester.GetController<InvoiceController>(null);
var testCases =
new List<(string input, Dictionary<string, string> expectedOutput)>()
new List<(string input, Dictionary<string, object> expectedOutput)>()
{
{ (null, new Dictionary<string, string>())},
{("", new Dictionary<string, string>())},
{("{}", new Dictionary<string, string>())},
{("non-json-content", new Dictionary<string, string>(){ {string.Empty, "non-json-content"}})},
{("[1,2,3]", new Dictionary<string, string>(){ {string.Empty, "[1,2,3]"}})},
{("{ \"key\": \"value\"}", new Dictionary<string, string>(){ {"key", "value"}})},
{("{ \"key\": true}", new Dictionary<string, string>(){ {"key", "True"}})},
{("{ \"key\": \"value\", \"key2\": [\"value\", \"value2\"]}",
new Dictionary<string, string>(){ {"key", "value"}, {"key2", "value,value2"}})},
{("{ invalidjson file here}", new Dictionary<string, string>(){ {String.Empty, "{ invalidjson file here}"}})}
{ (null, new Dictionary<string, object>())},
{("", new Dictionary<string, object>())},
{("{}", new Dictionary<string, object>())},
{("non-json-content", new Dictionary<string, object>(){ {string.Empty, "non-json-content"}})},
{("[1,2,3]", new Dictionary<string, object>(){ {string.Empty, "[1,2,3]"}})},
{("{ \"key\": \"value\"}", new Dictionary<string, object>(){ {"key", "value"}})},
{("{ \"key\": true}", new Dictionary<string, object>(){ {"key", "True"}})},
{("{ invalidjson file here}", new Dictionary<string, object>(){ {String.Empty, "{ invalidjson file here}"}})}
};
var tasks = new List<Task>();
@ -1672,7 +1743,7 @@ donation:
cashCow.SendToAddress(invoiceAddress, 4 * networkFee);
Thread.Sleep(1000);
Eventually(() =>
TestUtils.Eventually(() =>
{
var jsonResultPaid = user.GetController<InvoiceController>().Export("json").GetAwaiter().GetResult();
var paidresult = Assert.IsType<ContentResult>(jsonResultPaid);
@ -1744,7 +1815,7 @@ donation:
var firstPayment = productPartDue - missingMoney;
cashCow.SendToAddress(invoiceAddress, Money.Coins(firstPayment));
Eventually(() =>
TestUtils.Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
// Check that for the second payment, network fee are included
@ -1769,7 +1840,7 @@ donation:
}
});
cashCow.SendToAddress(invoiceAddress, due);
Eventually(() =>
TestUtils.Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal("paid", invoice.Status);
@ -1801,17 +1872,18 @@ donation:
var cashCow = tester.ExplorerNode;
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
var firstPayment = invoice.CryptoInfo[0].TotalDue - Money.Satoshis(10);
var firstPayment = invoice.CryptoInfo[0].TotalDue - Money.Coins(0.001m);
cashCow.SendToAddress(invoiceAddress, firstPayment);
Eventually(() =>
TestUtils.Eventually(() =>
{
var exportResultPaid = user.GetController<InvoiceController>().Export("csv").GetAwaiter().GetResult();
var paidresult = Assert.IsType<ContentResult>(exportResultPaid);
Assert.Equal("application/csv", paidresult.ContentType);
Assert.Contains($",\"orderId\",\"{invoice.Id}\",", paidresult.Content);
Assert.Contains($",\"OnChain\",\"BTC\",\"0.1000999\",\"0.0001\",\"5000.0\"", paidresult.Content);
Assert.Contains($",\"USD\",\"0.00050000\",\"500.0\",\"\",\"Some ``, description\",\"new (paidPartial)\"", paidresult.Content);
Assert.Contains($",\"OnChain\",\"BTC\",\"0.0991\",\"0.0001\",\"5000.0\"", paidresult.Content);
Assert.Contains($",\"USD\",\"5.00", paidresult.Content); // Seems hacky but some plateform does not render this decimal the same
Assert.Contains($"0\",\"500.0\",\"\",\"Some ``, description\",\"new (paidPartial)\"", paidresult.Content);
});
}
}
@ -1855,6 +1927,50 @@ donation:
}
}
[Fact]
[Trait("Integration", "Integration")]
public void CanCreateStrangeInvoice()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var invoice1 = user.BitPay.CreateInvoice(new Invoice()
{
Price = 0.000000012m,
Currency = "BTC",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
{
Price = 0.000000019m,
Currency = "BTC",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
Assert.Equal(0.00000001m, invoice1.Price);
Assert.Equal(0.00000002m, invoice2.Price);
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = -0.1m,
Currency = "BTC",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
Assert.Equal(0.0m, invoice.Price);
}
}
[Fact]
[Trait("Integration", "Integration")]
public void InvoiceFlowThroughDifferentStatesCorrectly()
@ -1868,6 +1984,7 @@ donation:
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0m,
TaxIncluded = 1000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
@ -1879,7 +1996,7 @@ donation:
Assert.Equal(0, invoice.CryptoInfo[0].TxCount);
Assert.True(invoice.MinerFees.ContainsKey("BTC"));
Assert.Contains(invoice.MinerFees["BTC"].SatoshiPerBytes, new[] { 100.0m, 20.0m });
Eventually(() =>
TestUtils.Eventually(() =>
{
var textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
{
@ -1897,6 +2014,8 @@ donation:
});
invoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal(1000.0m, invoice.TaxIncluded);
Assert.Equal(5000.0m, invoice.Price);
Assert.Equal(Money.Coins(0), invoice.BtcPaid);
Assert.Equal("new", invoice.Status);
Assert.False((bool)((JValue)invoice.ExceptionStatus).Value);
@ -1925,7 +2044,7 @@ donation:
Money secondPayment = Money.Zero;
Eventually(() =>
TestUtils.Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("new", localInvoice.Status);
@ -1948,7 +2067,7 @@ donation:
cashCow.SendToAddress(invoiceAddress, secondPayment);
Eventually(() =>
TestUtils.Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("paid", localInvoice.Status);
@ -1962,7 +2081,7 @@ donation:
cashCow.Generate(1); //The user has medium speed settings, so 1 conf is enough to be confirmed
Eventually(() =>
TestUtils.Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("confirmed", localInvoice.Status);
@ -1970,7 +2089,7 @@ donation:
cashCow.Generate(5); //Now should be complete
Eventually(() =>
TestUtils.Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("complete", localInvoice.Status);
@ -1992,7 +2111,7 @@ donation:
var txId = cashCow.SendToAddress(invoiceAddress, invoice.BtcDue + Money.Coins(1));
Eventually(() =>
TestUtils.Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("paid", localInvoice.Status);
@ -2009,7 +2128,7 @@ donation:
cashCow.Generate(1);
Eventually(() =>
TestUtils.Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("confirmed", localInvoice.Status);
@ -2199,36 +2318,39 @@ donation:
return ctx.AddressInvoices.FirstOrDefault(i => i.InvoiceDataId == invoice.Id && i.GetAddress() == h) != null;
}
private void Eventually(Action act)
public static class TestUtils
{
CancellationTokenSource cts = new CancellationTokenSource(20000);
while (true)
public static void Eventually(Action act)
{
try
CancellationTokenSource cts = new CancellationTokenSource(20000);
while (true)
{
act();
break;
}
catch (XunitException) when (!cts.Token.IsCancellationRequested)
{
cts.Token.WaitHandle.WaitOne(500);
try
{
act();
break;
}
catch (XunitException) when (!cts.Token.IsCancellationRequested)
{
cts.Token.WaitHandle.WaitOne(500);
}
}
}
}
private async Task EventuallyAsync(Func<Task> act)
{
CancellationTokenSource cts = new CancellationTokenSource(20000);
while (true)
public static async Task EventuallyAsync(Func<Task> act)
{
try
CancellationTokenSource cts = new CancellationTokenSource(20000);
while (true)
{
await act();
break;
}
catch (XunitException) when (!cts.Token.IsCancellationRequested)
{
await Task.Delay(500);
try
{
await act();
break;
}
catch (XunitException) when (!cts.Token.IsCancellationRequested)
{
await Task.Delay(500);
}
}
}
}

View File

@ -2,7 +2,7 @@ version: "3"
# Run `docker-compose up dev` for bootstrapping your development environment
# Doing so will expose NBXplorer, Bitcoind RPC and postgres port to the host so that tests can Run,
# The Visual Studio launch setting `Docker-Regtest` is configured to use this environment.
# The Visual Studio launch setting `Docker-regtest` is configured to use this environment.
services:
tests:
@ -69,7 +69,7 @@ services:
nbxplorer:
image: nicolasdorier/nbxplorer:2.0.0.2
image: nicolasdorier/nbxplorer:2.0.0.8
restart: unless-stopped
ports:
- "32838:32838"

View File

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitBitcoinplus()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("XBC");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Bitcoinplus",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://chainz.cryptoid.info/xbc/tx.dws?{0}" : "https://chainz.cryptoid.info/xbc/tx.dws?{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "bitcoinplus",
DefaultRateRules = new[]
{
"XBC_X = XBC_BTC * BTC_X",
"XBC_BTC = cryptopia(XBC_BTC)"
},
CryptoImagePath = "imlegacy/bitcoinplus.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("65'") : new KeyPath("1'")
});
}
}
}

View File

@ -24,7 +24,7 @@ namespace BTCPayServer
DefaultRateRules = new[]
{
"BTX_X = BTX_BTC * BTC_X",
"BTX_BTC = cryptopia(BTX_BTC)"
"BTX_BTC = hitbtc(BTX_BTC)"
},
CryptoImagePath = "imlegacy/bitcore.svg",
LightningImagePath = "imlegacy/bitcore-lightning.svg",

View File

@ -52,10 +52,13 @@ namespace BTCPayServer
InitBitcoinGold();
InitMonacoin();
InitDash();
InitPolis();
InitFeathercoin();
InitGroestlcoin();
InitViacoin();
// Disabled because of https://twitter.com/Cryptopia_NZ/status/1085084168852291586
//InitPolis();
//InitBitcoinplus();
//InitUfo();
}

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.3.39</Version>
<Version>1.0.3.47</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<PropertyGroup>
@ -33,11 +33,10 @@
<EmbeddedResource Include="Currencies.txt" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.4" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.5" />
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.5.3" />
<PackageReference Include="Hangfire" Version="1.6.20" />
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
<PackageReference Include="HtmlSanitizer" Version="4.0.199" />
<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" />
@ -46,10 +45,10 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="NBitcoin" Version="4.1.1.73" />
<PackageReference Include="NBitpayClient" Version="1.0.0.30" />
<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.1" />
<PackageReference Include="NBXplorer.Client" Version="2.0.0.3" />
<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" />
@ -126,6 +125,7 @@
<Folder Include="Build\" />
<Folder Include="wwwroot\vendor\clipboard.js\" />
<Folder Include="wwwroot\vendor\highlightjs\" />
<Folder Include="wwwroot\vendor\summernote" />
</ItemGroup>
<ItemGroup>

View File

@ -110,7 +110,7 @@ namespace BTCPayServer.Configuration
if (!LightningConnectionString.TryParse(lightning, true, out var connectionString, out var error))
{
throw new ConfigException($"Invalid setting {net.CryptoCode}.lightning, " + Environment.NewLine +
$"If you have a lightning server use: 'type=clightning;server=/root/.lightning/lightning-rpc', " + 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 +
@ -118,7 +118,7 @@ namespace BTCPayServer.Configuration
}
if (connectionString.IsLegacy)
{
Logs.Configuration.LogWarning($"Setting {net.CryptoCode}.lightning will work but use an deprecated format, please replace it by '{connectionString.ToString()}'");
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);
}
@ -177,10 +177,11 @@ namespace BTCPayServer.Configuration
var services = conf.GetOrDefault<string>("externalservices", null);
if (services != null)
{
foreach (var service in services.Split(new[] { ';', ',' })
.Select(p => p.Split(':'))
.Where(p => p.Length == 2)
.Select(p => (Name: p[0], Link: p[1])))
foreach (var service in services.Split(new[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(p => (p, SeparatorIndex: p.IndexOf(':', StringComparison.OrdinalIgnoreCase)))
.Where(p => p.SeparatorIndex != -1)
.Select(p => (Name: p.p.Substring(0, p.SeparatorIndex),
Link: p.p.Substring(p.SeparatorIndex + 1))))
{
ExternalServices.AddOrReplace(service.Name, service.Link);
}
@ -235,7 +236,7 @@ namespace BTCPayServer.Configuration
RootPath = "/" + RootPath;
var old = conf.GetOrDefault<Uri>("internallightningnode", null);
if (old != null)
throw new ConfigException($"internallightningnode should not be used anymore, use btclightning instead");
throw new ConfigException($"internallightningnode is deprecated and should not be used anymore, use btclightning instead");
LogFile = GetDebugLog(conf);
if (!string.IsNullOrEmpty(LogFile))

View File

@ -28,7 +28,7 @@ namespace BTCPayServer.Controllers
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly IEmailSender _emailSender;
private readonly EmailSenderFactory _EmailSenderFactory;
StoreRepository storeRepository;
RoleManager<IdentityRole> _RoleManager;
SettingsRepository _SettingsRepository;
@ -40,14 +40,14 @@ namespace BTCPayServer.Controllers
RoleManager<IdentityRole> roleManager,
StoreRepository storeRepository,
SignInManager<ApplicationUser> signInManager,
IEmailSender emailSender,
EmailSenderFactory emailSenderFactory,
SettingsRepository settingsRepository,
Configuration.BTCPayServerOptions options)
{
this.storeRepository = storeRepository;
_userManager = userManager;
_signInManager = signInManager;
_emailSender = emailSender;
_EmailSenderFactory = emailSenderFactory;
_RoleManager = roleManager;
_SettingsRepository = settingsRepository;
_Options = options;
@ -286,7 +286,8 @@ namespace BTCPayServer.Controllers
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme);
RegisteredUserId = user.Id;
await _emailSender.SendEmailConfirmationAsync(model.Email, callbackUrl);
_EmailSenderFactory.GetEmailSender().SendEmailConfirmation(model.Email, callbackUrl);
if (!policies.RequiresConfirmedEmail)
{
if(logon)
@ -446,8 +447,9 @@ namespace BTCPayServer.Controllers
// visit https://go.microsoft.com/fwlink/?LinkID=532713
var code = await _userManager.GeneratePasswordResetTokenAsync(user);
var callbackUrl = Url.ResetPasswordCallbackLink(user.Id, code, Request.Scheme);
await _emailSender.SendEmailAsync(model.Email, "Reset Password",
$"Please reset your password by clicking here: <a href='{callbackUrl}'>link</a>");
_EmailSenderFactory.GetEmailSender().SendEmail(model.Email, "Reset Password",
$"Please reset your password by clicking here: <a href='{callbackUrl}'>link</a>");
return RedirectToAction(nameof(ForgotPasswordConfirmation));
}

View File

@ -0,0 +1,171 @@
using System;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Services.Apps;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers
{
public partial class AppsController
{
public class CrowdfundAppUpdated
{
public string AppId { get; set; }
public CrowdfundSettings Settings { get; set; }
public string StoreId { get; set; }
}
public class CrowdfundSettings
{
public string Title { get; set; }
public string Description { get; set; }
public bool Enabled { get; set; } = false;
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
public string TargetCurrency { get; set; }
public decimal? TargetAmount { get; set; }
public bool EnforceTargetAmount { get; set; }
public string CustomCSSLink { get; set; }
public string MainImageUrl { get; set; }
public string NotificationUrl { get; set; }
public string Tagline { get; set; }
public string EmbeddedCSS { get; set; }
public string PerksTemplate { get; set; }
public bool DisqusEnabled { get; set; }= false;
public bool SoundsEnabled { get; set; }= true;
public string DisqusShortname { get; set; }
public bool AnimationsEnabled { get; set; } = true;
public bool UseInvoiceAmount { get; set; } = true;
public int ResetEveryAmount { get; set; } = 1;
public CrowdfundResetEvery ResetEvery { get; set; } = CrowdfundResetEvery.Never;
public bool UseAllStoreInvoices { get; set; }
public bool DisplayPerksRanking { get; set; }
public bool SortPerksByPopularity { get; set; }
}
[HttpGet]
[Route("{appId}/settings/crowdfund")]
public async Task<IActionResult> UpdateCrowdfund(string appId)
{
var app = await GetOwnedApp(appId, AppType.Crowdfund);
if (app == null)
return NotFound();
var settings = app.GetSettings<CrowdfundSettings>();
var vm = new UpdateCrowdfundViewModel()
{
Title = settings.Title,
Enabled = settings.Enabled,
EnforceTargetAmount = settings.EnforceTargetAmount,
StartDate = settings.StartDate,
TargetCurrency = settings.TargetCurrency,
Description = settings.Description,
MainImageUrl = settings.MainImageUrl,
EmbeddedCSS = settings.EmbeddedCSS,
EndDate = settings.EndDate,
TargetAmount = settings.TargetAmount,
CustomCSSLink = settings.CustomCSSLink,
NotificationUrl = settings.NotificationUrl,
Tagline = settings.Tagline,
PerksTemplate = settings.PerksTemplate,
DisqusEnabled = settings.DisqusEnabled,
SoundsEnabled = settings.SoundsEnabled,
DisqusShortname = settings.DisqusShortname,
AnimationsEnabled = settings.AnimationsEnabled,
UseInvoiceAmount = settings.UseInvoiceAmount,
ResetEveryAmount = settings.ResetEveryAmount,
ResetEvery = Enum.GetName(typeof(CrowdfundResetEvery), settings.ResetEvery),
UseAllStoreInvoices = settings.UseAllStoreInvoices,
AppId = appId,
DisplayPerksRanking = settings.DisplayPerksRanking,
SortPerksByPopularity = settings.SortPerksByPopularity
};
return View(vm);
}
[HttpPost]
[Route("{appId}/settings/crowdfund")]
public async Task<IActionResult> UpdateCrowdfund(string appId, UpdateCrowdfundViewModel vm)
{
if (!string.IsNullOrEmpty( vm.TargetCurrency) && _AppsHelper.GetCurrencyData(vm.TargetCurrency, false) == null)
ModelState.AddModelError(nameof(vm.TargetCurrency), "Invalid currency");
try
{
_AppsHelper.Parse(vm.PerksTemplate, vm.TargetCurrency).ToString();
}
catch
{
ModelState.AddModelError(nameof(vm.PerksTemplate), "Invalid template");
}
if (Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery) != CrowdfundResetEvery.Never && !vm.StartDate.HasValue)
{
ModelState.AddModelError(nameof(vm.StartDate), "A start date is needed when the goal resets every X amount of time.");
}
if (Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery) != CrowdfundResetEvery.Never && vm.ResetEveryAmount <= 0)
{
ModelState.AddModelError(nameof(vm.ResetEveryAmount), "You must reset the goal at a minimum of 1 ");
}
if (vm.DisplayPerksRanking && !vm.SortPerksByPopularity)
{
ModelState.AddModelError(nameof(vm.DisplayPerksRanking), "You must sort by popularity in order to display ranking.");
}
if (!ModelState.IsValid)
{
return View(vm);
}
var app = await GetOwnedApp(appId, AppType.Crowdfund);
if (app == null)
return NotFound();
var newSettings = new CrowdfundSettings()
{
Title = vm.Title,
Enabled = vm.Enabled,
EnforceTargetAmount = vm.EnforceTargetAmount,
StartDate = vm.StartDate,
TargetCurrency = vm.TargetCurrency,
Description = _AppsHelper.Sanitize( vm.Description),
EndDate = vm.EndDate,
TargetAmount = vm.TargetAmount,
CustomCSSLink = vm.CustomCSSLink,
MainImageUrl = vm.MainImageUrl,
EmbeddedCSS = vm.EmbeddedCSS,
NotificationUrl = vm.NotificationUrl,
Tagline = vm.Tagline,
PerksTemplate = vm.PerksTemplate,
DisqusEnabled = vm.DisqusEnabled,
SoundsEnabled = vm.SoundsEnabled,
DisqusShortname = vm.DisqusShortname,
AnimationsEnabled = vm.AnimationsEnabled,
ResetEveryAmount = vm.ResetEveryAmount,
ResetEvery = Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery),
UseInvoiceAmount = vm.UseInvoiceAmount,
UseAllStoreInvoices = vm.UseAllStoreInvoices,
DisplayPerksRanking = vm.DisplayPerksRanking,
SortPerksByPopularity = vm.SortPerksByPopularity
};
app.SetSettings(newSettings);
await UpdateAppSettings(app);
_EventAggregator.Publish(new CrowdfundAppUpdated()
{
AppId = appId,
StoreId = app.StoreDataId,
Settings = newSettings
});
StatusMessage = "App updated";
return RedirectToAction(nameof(UpdateCrowdfund), new {appId});
}
}
}

View File

@ -24,17 +24,20 @@ namespace BTCPayServer.Controllers
public AppsController(
UserManager<ApplicationUser> userManager,
ApplicationDbContextFactory contextFactory,
EventAggregator eventAggregator,
BTCPayNetworkProvider networkProvider,
AppsHelper appsHelper)
{
_UserManager = userManager;
_ContextFactory = contextFactory;
_EventAggregator = eventAggregator;
_NetworkProvider = networkProvider;
_AppsHelper = appsHelper;
}
private UserManager<ApplicationUser> _UserManager;
private ApplicationDbContextFactory _ContextFactory;
private readonly EventAggregator _EventAggregator;
private BTCPayNetworkProvider _NetworkProvider;
private AppsHelper _AppsHelper;
@ -44,7 +47,7 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> ListApps()
{
var apps = await GetAllApps();
var apps = await _AppsHelper.GetAllApps(GetUserId());
return View(new ListAppsViewModel()
{
Apps = apps
@ -58,7 +61,7 @@ namespace BTCPayServer.Controllers
var appData = await GetOwnedApp(appId);
if (appData == null)
return NotFound();
if (await DeleteApp(appData))
if (await _AppsHelper.DeleteApp(appData))
StatusMessage = "App removed successfully";
return RedirectToAction(nameof(ListApps));
}
@ -67,7 +70,7 @@ namespace BTCPayServer.Controllers
[Route("create")]
public async Task<IActionResult> CreateApp()
{
var stores = await GetOwnedStores();
var stores = await _AppsHelper.GetOwnedStores(GetUserId());
if (stores.Length == 0)
{
StatusMessage = "Error: You must have created at least one store";
@ -82,7 +85,7 @@ namespace BTCPayServer.Controllers
[Route("create")]
public async Task<IActionResult> CreateApp(CreateAppViewModel vm)
{
var stores = await GetOwnedStores();
var stores = await _AppsHelper.GetOwnedStores(GetUserId());
if (stores.Length == 0)
{
StatusMessage = "Error: You must own at least one store";
@ -117,9 +120,17 @@ namespace BTCPayServer.Controllers
}
StatusMessage = "App successfully created";
CreatedAppId = id;
if (appType == AppType.PointOfSale)
return RedirectToAction(nameof(UpdatePointOfSale), new { appId = id });
return RedirectToAction(nameof(ListApps));
switch (appType)
{
case AppType.PointOfSale:
return RedirectToAction(nameof(UpdatePointOfSale), new { appId = id });
case AppType.Crowdfund:
return RedirectToAction(nameof(UpdateCrowdfund), new { appId = id });
default:
return RedirectToAction(nameof(ListApps));
}
}
[HttpGet]
@ -142,50 +153,7 @@ namespace BTCPayServer.Controllers
return _AppsHelper.GetAppDataIfOwner(GetUserId(), appId, type);
}
private async Task<StoreData[]> GetOwnedStores()
{
var userId = GetUserId();
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.UserStore
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
.Select(u => u.StoreData)
.ToArrayAsync();
}
}
private async Task<bool> DeleteApp(AppData appData)
{
using (var ctx = _ContextFactory.CreateContext())
{
ctx.Apps.Add(appData);
ctx.Entry<AppData>(appData).State = EntityState.Deleted;
return await ctx.SaveChangesAsync() == 1;
}
}
private async Task<ListAppsViewModel.ListAppViewModel[]> GetAllApps()
{
var userId = GetUserId();
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.UserStore
.Where(us => us.ApplicationUserId == userId)
.Join(ctx.Apps, us => us.StoreDataId, app => app.StoreDataId,
(us, app) =>
new ListAppsViewModel.ListAppViewModel()
{
IsOwner = us.Role == StoreRoles.Owner,
StoreId = us.StoreDataId,
StoreName = us.StoreData.StoreName,
AppName = app.Name,
AppType = app.AppType,
Id = app.Id
})
.ToArrayAsync();
}
}
private string GetUserId()
{
return _UserManager.GetUserId(User);

View File

@ -1,20 +1,31 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using BTCPayServer.Crowdfund;
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.Hubs;
using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Rating;
using BTCPayServer.Security;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using Ganss.XSS;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NBitpayClient;
using Newtonsoft.Json.Linq;
using YamlDotNet.RepresentationModel;
using static BTCPayServer.Controllers.AppsController;
@ -22,14 +33,20 @@ namespace BTCPayServer.Controllers
{
public class AppsPublicController : Controller
{
public AppsPublicController(AppsHelper appsHelper, InvoiceController invoiceController)
public AppsPublicController(AppsHelper appsHelper,
InvoiceController invoiceController,
CrowdfundHubStreamer crowdfundHubStreamer, UserManager<ApplicationUser> userManager)
{
_AppsHelper = appsHelper;
_InvoiceController = invoiceController;
_CrowdfundHubStreamer = crowdfundHubStreamer;
_UserManager = userManager;
}
private AppsHelper _AppsHelper;
private InvoiceController _InvoiceController;
private readonly CrowdfundHubStreamer _CrowdfundHubStreamer;
private readonly UserManager<ApplicationUser> _UserManager;
[HttpGet]
[Route("/apps/{appId}/pos")]
@ -70,6 +87,121 @@ namespace BTCPayServer.Controllers
AppId = appId
});
}
[HttpGet]
[Route("/apps/{appId}/crowdfund")]
[XFrameOptionsAttribute(null)]
public async Task<IActionResult> ViewCrowdfund(string appId, string statusMessage)
{
var app = await _AppsHelper.GetApp(appId, AppType.Crowdfund, true);
if (app == null)
return NotFound();
var settings = app.GetSettings<CrowdfundSettings>();
var isAdmin = await _AppsHelper.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
var hasEnoughSettingsToLoad = !string.IsNullOrEmpty(settings.TargetCurrency );
if (!hasEnoughSettingsToLoad)
{
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 _CrowdfundHubStreamer.GetCrowdfundInfo(appId));
if(!isAdmin)
return NotFound();
return View(await _CrowdfundHubStreamer.GetCrowdfundInfo(appId));
}
[HttpPost]
[Route("/apps/{appId}/crowdfund")]
[XFrameOptionsAttribute(null)]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
public async Task<IActionResult> ContributeToCrowdfund(string appId, ContributeToCrowdfund request)
{
var app = await _AppsHelper.GetApp(appId, AppType.Crowdfund, true);
if (app == null)
return NotFound();
var settings = app.GetSettings<CrowdfundSettings>();
var isAdmin = await _AppsHelper.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
if (!settings.Enabled)
{
if(!isAdmin)
return NotFound("Crowdfund is not currently active");
}
var info = await _CrowdfundHubStreamer.GetCrowdfundInfo(appId);
if(!isAdmin &&
((settings.StartDate.HasValue && DateTime.Now < settings.StartDate) ||
(settings.EndDate.HasValue && DateTime.Now > settings.EndDate) ||
(settings.EnforceTargetAmount &&
(info.Info.PendingProgressPercentage.GetValueOrDefault(0) +
info.Info.ProgressPercentage.GetValueOrDefault(0)) >= 100)))
{
return NotFound("Crowdfund is not currently active");
}
var store = await _AppsHelper.GetStore(app);
var title = settings.Title;
var price = request.Amount;
ViewPointOfSaleViewModel.Item choice = null;
if (!string.IsNullOrEmpty(request.ChoiceKey))
{
var choices = _AppsHelper.Parse(settings.PerksTemplate, settings.TargetCurrency);
choice = choices.FirstOrDefault(c => c.Id == request.ChoiceKey);
if (choice == null)
return NotFound("Incorrect option provided");
title = choice.Title;
price = choice.Price.Value;
if (request.Amount > price)
price = request.Amount;
}
if (!isAdmin && (settings.EnforceTargetAmount && info.TargetAmount.HasValue && price >
(info.TargetAmount - (info.Info.CurrentAmount + info.Info.CurrentPendingAmount))))
{
return NotFound("Contribution Amount is more than is currently allowed.");
}
store.AdditionalClaims.Add(new Claim(Policies.CanCreateInvoice.Key, store.Id));
var invoice = await _InvoiceController.CreateInvoiceCore(new Invoice()
{
OrderId = $"{CrowdfundHubStreamer.CrowdfundInvoiceOrderIdPrefix}{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());
if (request.RedirectToCheckout)
{
return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice",
new {invoiceId = invoice.Data.Id});
}
else
{
return Ok(invoice.Data.Id);
}
}
[HttpPost]
[Route("/apps/{appId}/pos")]
@ -81,7 +213,8 @@ namespace BTCPayServer.Controllers
string orderId,
string notificationUrl,
string redirectUrl,
string choiceKey)
string choiceKey,
string posData = null)
{
var app = await _AppsHelper.GetApp(appId, AppType.PointOfSale);
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
@ -97,10 +230,11 @@ namespace BTCPayServer.Controllers
}
string title = null;
var price = 0.0m;
ViewPointOfSaleViewModel.Item choice = null;
if (!string.IsNullOrEmpty(choiceKey))
{
var choices = _AppsHelper.Parse(settings.Template, settings.Currency);
var choice = choices.FirstOrDefault(c => c.Id == choiceKey);
choice = choices.FirstOrDefault(c => c.Id == choiceKey);
if (choice == null)
return NotFound();
title = choice.Title;
@ -119,41 +253,145 @@ namespace BTCPayServer.Controllers
store.AdditionalClaims.Add(new Claim(Policies.CanCreateInvoice.Key, store.Id));
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
{
ItemCode = choiceKey ?? string.Empty,
ItemCode = choice?.Id,
ItemDesc = title,
Currency = settings.Currency,
Price = price,
BuyerEmail = email,
OrderId = orderId,
NotificationURL = notificationUrl,
RedirectURL = redirectUrl,
FullNotifications = true
RedirectURL = redirectUrl ?? Request.GetDisplayUrl(),
FullNotifications = true,
PosData = string.IsNullOrEmpty(posData) ? null : posData
}, store, HttpContext.Request.GetAbsoluteRoot());
return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice", new { invoiceId = invoice.Data.Id });
}
private string GetUserId()
{
return _UserManager.GetUserId(User);
}
}
public class AppsHelper
{
ApplicationDbContextFactory _ContextFactory;
CurrencyNameTable _Currencies;
private HtmlSanitizer _HtmlSanitizer;
public CurrencyNameTable Currencies => _Currencies;
public AppsHelper(ApplicationDbContextFactory contextFactory, CurrencyNameTable currencies)
{
_ContextFactory = contextFactory;
_Currencies = currencies;
ConfigureSanitizer();
}
private void ConfigureSanitizer()
{
_HtmlSanitizer = new HtmlSanitizer();
_HtmlSanitizer.RemovingAtRule += (sender, args) =>
{
Debug.WriteLine("");
};
_HtmlSanitizer.RemovingTag += (sender, args) =>
{
Debug.WriteLine("");
if (args.Tag.TagName.Equals("img", StringComparison.InvariantCultureIgnoreCase))
{
if (!args.Tag.ClassList.Contains("img-fluid"))
{
args.Tag.ClassList.Add("img-fluid");
}
args.Cancel = true;
}
};
_HtmlSanitizer.RemovingAttribute += (sender, args) =>
{
if (args.Tag.TagName.Equals("img",StringComparison.InvariantCultureIgnoreCase) &&
args.Attribute.Name.Equals( "src", StringComparison.InvariantCultureIgnoreCase) &&
args.Reason == RemoveReason.NotAllowedUrlValue)
{
args.Cancel = true;
}
Debug.WriteLine("");
};
_HtmlSanitizer.RemovingStyle += (sender, args) => { args.Cancel = true; };
_HtmlSanitizer.AllowedAttributes.Add("class");
_HtmlSanitizer.AllowedTags.Add("iframe");
_HtmlSanitizer.AllowedTags.Remove("img");
_HtmlSanitizer.AllowedAttributes.Add("webkitallowfullscreen");
_HtmlSanitizer.AllowedAttributes.Add("allowfullscreen");
}
public async Task<AppData> GetApp(string appId, AppType appType)
public string Sanitize(string raw)
{
return _HtmlSanitizer.Sanitize(raw);
}
public async Task<StoreData[]> GetOwnedStores(string userId)
{
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.Apps
.Where(us => us.Id == appId &&
us.AppType == appType.ToString())
.FirstOrDefaultAsync();
return await ctx.UserStore
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
.Select(u => u.StoreData)
.ToArrayAsync();
}
}
public async Task<bool> DeleteApp(AppData appData)
{
using (var ctx = _ContextFactory.CreateContext())
{
ctx.Apps.Add(appData);
ctx.Entry<AppData>(appData).State = EntityState.Deleted;
return await ctx.SaveChangesAsync() == 1;
}
}
public async Task<ListAppsViewModel.ListAppViewModel[]> GetAllApps(string userId, bool allowNoUser = false)
{
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.UserStore
.Where(us => (allowNoUser && string.IsNullOrEmpty(userId) ) || us.ApplicationUserId == userId)
.Join(ctx.Apps, us => us.StoreDataId, app => app.StoreDataId,
(us, app) =>
new ListAppsViewModel.ListAppViewModel()
{
IsOwner = us.Role == StoreRoles.Owner,
StoreId = us.StoreDataId,
StoreName = us.StoreData.StoreName,
AppName = app.Name,
AppType = app.AppType,
Id = app.Id
})
.ToArrayAsync();
}
}
public async Task<AppData> GetApp(string appId, AppType appType, bool includeStore = false)
{
using (var ctx = _ContextFactory.CreateContext())
{
var query = ctx.Apps
.Where(us => us.Id == appId &&
us.AppType == appType.ToString());
if (includeStore)
{
query = query.Include(data => data.StoreData);
}
return await query.FirstOrDefaultAsync();
}
}
@ -180,10 +418,10 @@ namespace BTCPayServer.Controllers
.Where(kv => kv.Value != null)
.Select(c => new ViewPointOfSaleViewModel.Item()
{
Description = c.GetDetailString("description"),
Description = Sanitize(c.GetDetailString("description")),
Id = c.Key,
Image = c.GetDetailString("image"),
Title = c.GetDetailString("title") ?? c.Key,
Image = Sanitize(c.GetDetailString("image")),
Title = Sanitize(c.GetDetailString("title") ?? c.Key),
Price = c.GetDetail("price")
.Select(cc => new ViewPointOfSaleViewModel.Item.ItemPrice()
{
@ -211,6 +449,7 @@ namespace BTCPayServer.Controllers
public string GetDetailString(string field)
{
return GetDetail(field).FirstOrDefault()?.Value?.Value;
}
}
@ -246,5 +485,6 @@ namespace BTCPayServer.Controllers
return app;
}
}
}
}

View File

@ -66,7 +66,7 @@ namespace BTCPayServer.Controllers
{
if (dateEnd != null)
dateEnd = dateEnd.Value + TimeSpan.FromDays(1); //Should include the end day
var query = new InvoiceQuery()
{
Count = limit,

View File

@ -21,6 +21,7 @@ using BTCPayServer.Services.Invoices.Export;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore.Internal;
using NBitcoin;
using NBitpayClient;
using NBXplorer;
@ -447,8 +448,11 @@ namespace BTCPayServer.Controllers
Count = count,
StatusMessage = StatusMessage
};
var list = await ListInvoicesProcess(searchTerm, skip, count);
InvoiceQuery invoiceQuery = GetInvoiceQuery(searchTerm);
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();
@ -465,27 +469,27 @@ namespace BTCPayServer.Controllers
CanMarkComplete = state.CanMarkComplete()
});
}
model.Total = await counting;
return View(model);
}
private async Task<InvoiceEntity[]> ListInvoicesProcess(string searchTerm = null, int skip = 0, int count = 50)
private InvoiceQuery GetInvoiceQuery(string searchTerm = null)
{
var filterString = new SearchString(searchTerm);
var list = await _InvoiceRepository.GetInvoices(new InvoiceQuery()
var invoiceQuery = new InvoiceQuery()
{
TextSearch = filterString.TextSearch,
Count = count,
Skip = skip,
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
});
return list;
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
};
return invoiceQuery;
}
[HttpGet]
@ -493,9 +497,12 @@ namespace BTCPayServer.Controllers
[BitpayAPIConstraint(false)]
public async Task<IActionResult> Export(string format, string searchTerm = null)
{
var model = new InvoiceExport(_NetworkProvider);
var model = new InvoiceExport(_NetworkProvider, _CurrencyNameTable);
var invoices = await ListInvoicesProcess(searchTerm, 0, int.MaxValue);
InvoiceQuery invoiceQuery = GetInvoiceQuery(searchTerm);
invoiceQuery.Count = int.MaxValue;
invoiceQuery.Skip = 0;
var invoices = await _InvoiceRepository.GetInvoices(invoiceQuery);
var res = model.Process(invoices, format);
var cd = new ContentDisposition
@ -673,9 +680,9 @@ namespace BTCPayServer.Controllers
public class PosDataParser
{
public static Dictionary<string, string> ParsePosData(string posData)
public static Dictionary<string, object> ParsePosData(string posData)
{
var result = new Dictionary<string,string>();
var result = new Dictionary<string,object>();
if (string.IsNullOrEmpty(posData))
{
return result;
@ -683,7 +690,6 @@ namespace BTCPayServer.Controllers
try
{
var jObject =JObject.Parse(posData);
foreach (var item in jObject)
{
@ -691,7 +697,14 @@ namespace BTCPayServer.Controllers
switch (item.Value.Type)
{
case JTokenType.Array:
result.Add(item.Key, string.Join(',', item.Value.AsEnumerable()));
var items = item.Value.AsEnumerable().ToList();
for (var i = 0; i < items.Count(); i++)
{
result.Add($"{item.Key}[{i}]", ParsePosData(items[i].ToString()));
}
break;
case JTokenType.Object:
result.Add(item.Key, ParsePosData(item.Value.ToString()));
break;
default:
result.Add(item.Key, item.Value.ToString());

View File

@ -71,7 +71,6 @@ namespace BTCPayServer.Controllers
{
InvoiceTime = DateTimeOffset.UtcNow
};
var storeBlob = store.GetStoreBlob();
Uri notificationUri = Uri.IsWellFormedUriString(invoice.NotificationURL, UriKind.Absolute) ? new Uri(invoice.NotificationURL, UriKind.Absolute) : null;
if (notificationUri == null || (notificationUri.Scheme != "http" && notificationUri.Scheme != "https")) //TODO: Filer non routable addresses ?
@ -95,7 +94,20 @@ namespace BTCPayServer.Controllers
throw new BitpayHttpException(400, "Invalid email");
entity.RefundMail = entity.BuyerInformation.BuyerEmail;
}
var currencyInfo = _CurrencyNameTable.GetNumberFormatInfo(invoice.Currency, false);
if (currencyInfo != null)
{
invoice.Price = Math.Round(invoice.Price, currencyInfo.CurrencyDecimalDigits);
invoice.TaxIncluded = Math.Round(invoice.TaxIncluded, currencyInfo.CurrencyDecimalDigits);
}
invoice.Price = Math.Max(0.0m, invoice.Price);
invoice.TaxIncluded = Math.Max(0.0m, invoice.TaxIncluded);
invoice.TaxIncluded = Math.Min(invoice.TaxIncluded, invoice.Price);
entity.ProductInformation = Map<Invoice, ProductInformation>(invoice);
entity.RedirectURL = invoice.RedirectURL ?? store.StoreWebsite;
if (!Uri.IsWellFormedUriString(entity.RedirectURL, UriKind.Absolute))
entity.RedirectURL = null;
@ -147,7 +159,7 @@ namespace BTCPayServer.Controllers
if (supported.Count == 0)
{
StringBuilder errors = new StringBuilder();
errors.AppendLine("No payment method available for this store");
errors.AppendLine("Warning: No wallet has been linked to your BTCPay Store. See the following link for more information on how to connect your store and wallet. (https://docs.btcpayserver.org/btcpay-basics/gettingstarted#connecting-btcpay-store-to-your-wallet)");
foreach (var error in logs.ToList())
{
errors.AppendLine(error.ToString());

View File

@ -31,7 +31,7 @@ namespace BTCPayServer.Controllers
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly IEmailSender _emailSender;
private readonly EmailSenderFactory _EmailSenderFactory;
private readonly ILogger _logger;
private readonly UrlEncoder _urlEncoder;
TokenRepository _TokenRepository;
@ -44,7 +44,7 @@ namespace BTCPayServer.Controllers
public ManageController(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
IEmailSender emailSender,
EmailSenderFactory emailSenderFactory,
ILogger<ManageController> logger,
UrlEncoder urlEncoder,
TokenRepository tokenRepository,
@ -54,7 +54,7 @@ namespace BTCPayServer.Controllers
{
_userManager = userManager;
_signInManager = signInManager;
_emailSender = emailSender;
_EmailSenderFactory = emailSenderFactory;
_logger = logger;
_urlEncoder = urlEncoder;
_TokenRepository = tokenRepository;
@ -156,8 +156,7 @@ namespace BTCPayServer.Controllers
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme);
var email = user.Email;
await _emailSender.SendEmailConfirmationAsync(email, callbackUrl);
_EmailSenderFactory.GetEmailSender().SendEmailConfirmation(email, callbackUrl);
StatusMessage = "Verification email sent. Please check your email.";
return RedirectToAction(nameof(Index));
}

View File

@ -465,7 +465,7 @@ namespace BTCPayServer.Controllers
result.ExternalServices.Add(new ServicesViewModel.ExternalService()
{
Name = externalService.Key,
Link = this.Request.GetRelativePath(externalService.Value)
Link = this.Request.GetRelativePathOrAbsolute(externalService.Value)
});
}
if(_Options.SSHSettings != null)

View File

@ -0,0 +1,65 @@
using System;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models.ServerViewModels;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Services.Mails;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers
{
public partial class StoresController
{
[Route("{storeId}/emails")]
public IActionResult Emails()
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
var data = store.GetStoreBlob().EmailSettings ?? new EmailSettings();
return View(new EmailsViewModel() { Settings = data });
}
[Route("{storeId}/emails")]
[HttpPost]
public async Task<IActionResult> Emails(string storeId, EmailsViewModel model, string command)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
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";
}
catch (Exception ex)
{
model.StatusMessage = "Error: " + ex.Message;
}
return View(model);
}
else // if(command == "Save")
{
var storeBlob = store.GetStoreBlob();
storeBlob.EmailSettings = model.Settings;
store.SetStoreBlob(storeBlob);
await _Repo.UpdateStore(store);
StatusMessage = "Email settings modified";
return RedirectToAction(nameof(UpdateStore), new {
storeId});
}
}
}
}

View File

@ -96,6 +96,11 @@ namespace BTCPayServer.Controllers
{
get; set;
}
[TempData]
public bool StoreNotConfigured
{
get; set;
}
[HttpGet]
[Route("{storeId}/users")]
@ -167,7 +172,7 @@ namespace BTCPayServer.Controllers
return View("Confirm", new ConfirmModel()
{
Title = $"Remove store user",
Description = $"Are you sure to remove access to remove access to {user.Email}?",
Description = $"Are you sure you want to remove store access for {user.Email}?",
Action = "Delete"
});
}
@ -567,6 +572,7 @@ namespace BTCPayServer.Controllers
var model = new TokensViewModel();
var tokens = await _TokenRepository.GetTokensByStoreIdAsync(StoreData.Id);
model.StatusMessage = StatusMessage;
model.StoreNotConfigured = StoreNotConfigured;
model.Tokens = tokens.Select(t => new TokenViewModel()
{
Facade = t.Facade,
@ -794,6 +800,10 @@ namespace BTCPayServer.Controllers
var pairingResult = await _TokenRepository.PairWithStoreAsync(pairingCode, store.Id);
if (pairingResult == PairingResult.Complete || pairingResult == PairingResult.Partial)
{
var excludeFilter = store.GetStoreBlob().GetExcludedPaymentMethods();
StoreNotConfigured = store.GetSupportedPaymentMethods(_NetworkProvider)
.Where(p => !excludeFilter.Match(p.PaymentId))
.Count() == 0;
StatusMessage = "Pairing is successful";
if (pairingResult == PairingResult.Partial)
StatusMessage = "Server initiated pairing code: " + pairingCode;

View File

@ -146,7 +146,7 @@ namespace BTCPayServer.Controllers
[Route("{walletId}/send")]
public async Task<IActionResult> WalletSend(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, string defaultDestination = null, string defaultAmount = null)
WalletId walletId, string defaultDestination = null, string defaultAmount = null, bool advancedMode = false)
{
if (walletId?.StoreId == null)
return NotFound();
@ -195,6 +195,7 @@ namespace BTCPayServer.Controllers
}
catch (Exception ex) { model.RateError = ex.Message; }
}
model.AdvancedMode = advancedMode;
return View(model);
}
@ -202,7 +203,7 @@ namespace BTCPayServer.Controllers
[Route("{walletId}/send")]
public async Task<IActionResult> WalletSend(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletSendModel vm)
WalletId walletId, WalletSendModel vm, string command = null)
{
if (walletId?.StoreId == null)
return NotFound();
@ -212,6 +213,14 @@ 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);
}
var destination = ParseDestination(vm.Destination, network.NBitcoinNetwork);
if (destination == null)
ModelState.AddModelError(nameof(vm.Destination), "Invalid address");
@ -231,7 +240,8 @@ namespace BTCPayServer.Controllers
Destination = vm.Destination,
Amount = vm.Amount.Value,
SubstractFees = vm.SubstractFees,
FeeSatoshiPerByte = vm.FeeSatoshiPerByte
FeeSatoshiPerByte = vm.FeeSatoshiPerByte,
NoChange = vm.NoChange
});
}
@ -403,6 +413,7 @@ namespace BTCPayServer.Controllers
// getxpub
int account = 0,
// sendtoaddress
bool noChange = false,
string destination = null, string amount = null, string feeRate = null, string substractFees = null
)
{
@ -436,7 +447,7 @@ namespace BTCPayServer.Controllers
{
try
{
destinationAddress = BitcoinAddress.Create(destination, network.NBitcoinNetwork);
destinationAddress = BitcoinAddress.Create(destination.Trim(), network.NBitcoinNetwork);
}
catch { }
if (destinationAddress == null)
@ -487,24 +498,16 @@ namespace BTCPayServer.Controllers
var strategy = GetDirectDerivationStrategy(derivationScheme);
var wallet = _walletProvider.GetWallet(network);
var change = wallet.GetChangeAddressAsync(derivationScheme);
var unspentCoins = await wallet.GetUnspentCoins(derivationScheme);
var changeAddress = await change;
var send = new[] { (
destination: destinationAddress as IDestination,
amount: amountBTC,
substractFees: subsctractFeesValue) };
foreach (var element in send)
var keypaths = new Dictionary<Script, KeyPath>();
List<Coin> availableCoins = new List<Coin>();
foreach (var c in await wallet.GetUnspentCoins(derivationScheme))
{
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");
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);
@ -520,10 +523,25 @@ namespace BTCPayServer.Controllers
storeData.SetStoreBlob(storeBlob);
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(unspentCoins.Select(c => c.Coin).ToArray());
builder.AddCoins(availableCoins);
foreach (var element in send)
{
@ -531,6 +549,7 @@ namespace BTCPayServer.Controllers
if (element.substractFees)
builder.SubtractFees();
}
builder.SetChange(changeAddress.Item1);
if (network.MinFee == null)
@ -547,13 +566,15 @@ namespace BTCPayServer.Controllers
}
var unsigned = builder.BuildTransaction(false);
var keypaths = new Dictionary<Script, KeyPath>();
foreach (var c in unspentCoins)
var hasChange = unsigned.Outputs.Any(o => o.ScriptPubKey == changeAddress.Item1.ScriptPubKey);
if (noChange && hasChange)
{
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
availableCoins = builder.FindSpentCoins(unsigned).Cast<Coin>().ToList();
amountBTC = builder.FindSpentCoins(unsigned).Select(c => c.TxOut.Value).Sum();
subsctractFeesValue = true;
goto retry;
}
var hasChange = unsigned.Outputs.Count == 2;
var usedCoins = builder.FindSpentCoins(unsigned);
Dictionary<uint256, Transaction> parentTransactions = new Dictionary<uint256, Transaction>();

View File

@ -0,0 +1,66 @@
using System;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Models.AppViewModels;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
namespace BTCPayServer.Hubs
{
public class CrowdfundHub: Hub
{
public const string InvoiceCreated = "InvoiceCreated";
public const string PaymentReceived = "PaymentReceived";
public const string InfoUpdated = "InfoUpdated";
public const string InvoiceError = "InvoiceError";
private readonly AppsPublicController _AppsPublicController;
public CrowdfundHub(AppsPublicController appsPublicController)
{
_AppsPublicController = appsPublicController;
}
public async Task ListenToCrowdfundApp(string appId)
{
if (Context.Items.ContainsKey("app"))
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, Context.Items["app"].ToString());
Context.Items.Remove("app");
}
Context.Items.Add("app", appId);
await Groups.AddToGroupAsync(Context.ConnectionId, appId);
}
public async Task CreateInvoice(ContributeToCrowdfund model)
{
model.RedirectToCheckout = false;
_AppsPublicController.ControllerContext.HttpContext = Context.GetHttpContext();
try
{
var result =
await _AppsPublicController.ContributeToCrowdfund(Context.Items["app"].ToString(), model);
switch (result)
{
case OkObjectResult okObjectResult:
await Clients.Caller.SendCoreAsync(InvoiceCreated, new[] {okObjectResult.Value.ToString()});
break;
case ObjectResult objectResult:
await Clients.Caller.SendCoreAsync(InvoiceError, new[] {objectResult.Value});
break;
default:
await Clients.Caller.SendCoreAsync(InvoiceError, System.Array.Empty<object>());
break;
}
}
catch (Exception)
{
await Clients.Caller.SendCoreAsync(InvoiceError, System.Array.Empty<object>());
}
}
}
}

View File

@ -0,0 +1,371 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Hubs;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Rating;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using NBitcoin;
namespace BTCPayServer.Crowdfund
{
public class CrowdfundHubStreamer: IDisposable
{
public const string CrowdfundInvoiceOrderIdPrefix = "crowdfund-app_";
private readonly EventAggregator _EventAggregator;
private readonly IHubContext<CrowdfundHub> _HubContext;
private readonly IMemoryCache _MemoryCache;
private readonly AppsHelper _AppsHelper;
private readonly RateFetcher _RateFetcher;
private readonly BTCPayNetworkProvider _BtcPayNetworkProvider;
private readonly InvoiceRepository _InvoiceRepository;
private readonly ILogger<CrowdfundHubStreamer> _Logger;
private readonly ConcurrentDictionary<string,(string appId, bool useAllStoreInvoices,bool useInvoiceAmount)> _QuickAppInvoiceLookup =
new ConcurrentDictionary<string, (string appId, bool useAllStoreInvoices, bool useInvoiceAmount)>();
private List<IEventAggregatorSubscription> _Subscriptions;
public CrowdfundHubStreamer(EventAggregator eventAggregator,
IHubContext<CrowdfundHub> hubContext,
IMemoryCache memoryCache,
AppsHelper appsHelper,
RateFetcher rateFetcher,
BTCPayNetworkProvider btcPayNetworkProvider,
InvoiceRepository invoiceRepository,
ILogger<CrowdfundHubStreamer> logger)
{
_EventAggregator = eventAggregator;
_HubContext = hubContext;
_MemoryCache = memoryCache;
_AppsHelper = appsHelper;
_RateFetcher = rateFetcher;
_BtcPayNetworkProvider = btcPayNetworkProvider;
_InvoiceRepository = invoiceRepository;
_Logger = logger;
#pragma warning disable 4014
InitLookup();
#pragma warning restore 4014
SubscribeToEvents();
}
private async Task InitLookup()
{
var apps = await _AppsHelper.GetAllApps(null, true);
apps = apps.Where(model => Enum.Parse<AppType>(model.AppType) == AppType.Crowdfund).ToArray();
var tasks = new List<Task>();
tasks.AddRange(apps.Select(app => Task.Run(async () =>
{
var fullApp = await _AppsHelper.GetApp(app.Id, AppType.Crowdfund, false);
var settings = fullApp.GetSettings<AppsController.CrowdfundSettings>();
UpdateLookup(app.Id, app.StoreId, settings);
})));
await Task.WhenAll(tasks);
}
private void UpdateLookup(string appId, string storeId, AppsController.CrowdfundSettings settings)
{
_QuickAppInvoiceLookup.AddOrReplace(storeId,
(
appId: appId,
useAllStoreInvoices: settings?.UseAllStoreInvoices ?? false,
useInvoiceAmount: settings?.UseInvoiceAmount ?? false
));
}
public Task<ViewCrowdfundViewModel> GetCrowdfundInfo(string appId)
{
return _MemoryCache.GetOrCreateAsync(GetCacheKey(appId), async entry =>
{
_Logger.LogInformation($"GetCrowdfundInfo {appId}");
var app = await _AppsHelper.GetApp(appId, AppType.Crowdfund, true);
var result = await GetInfo(app);
entry.SetValue(result);
TimeSpan? expire = null;
if (result.StartDate.HasValue && result.StartDate < DateTime.Now)
{
expire = result.StartDate.Value.Subtract(DateTime.Now);
}
else if (result.EndDate.HasValue && result.EndDate > DateTime.Now)
{
expire = result.EndDate.Value.Subtract(DateTime.Now);
}
if(!expire.HasValue || expire?.TotalMinutes > 5 || expire?.TotalMilliseconds <= 0)
{
expire = TimeSpan.FromMinutes(5);
}
entry.AbsoluteExpirationRelativeToNow = expire;
return result;
});
}
private void SubscribeToEvents()
{
_Subscriptions = new List<IEventAggregatorSubscription>()
{
_EventAggregator.Subscribe<InvoiceEvent>(OnInvoiceEvent),
_EventAggregator.Subscribe<AppsController.CrowdfundAppUpdated>(updated =>
{
UpdateLookup(updated.AppId, updated.StoreId, updated.Settings);
InvalidateCacheForApp(updated.AppId);
})
};
}
private string GetCacheKey(string appId)
{
return $"{CrowdfundInvoiceOrderIdPrefix}:{appId}";
}
private void OnInvoiceEvent(InvoiceEvent invoiceEvent)
{
if (!_QuickAppInvoiceLookup.TryGetValue(invoiceEvent.Invoice.StoreId, out var quickLookup) ||
(!quickLookup.useAllStoreInvoices &&
!string.IsNullOrEmpty(invoiceEvent.Invoice.OrderId) &&
!invoiceEvent.Invoice.OrderId.Equals($"{CrowdfundInvoiceOrderIdPrefix}{quickLookup.appId}", StringComparison.InvariantCulture)
))
{
return;
}
switch (invoiceEvent.Name)
{
case InvoiceEvent.ReceivedPayment:
var data = invoiceEvent.Payment.GetCryptoPaymentData();
_HubContext.Clients.Group(quickLookup.appId).SendCoreAsync(CrowdfundHub.PaymentReceived, new object[]
{
data.GetValue(),
invoiceEvent.Payment.GetCryptoCode(),
Enum.GetName(typeof(PaymentTypes),
invoiceEvent.Payment.GetPaymentMethodId().PaymentType)
} );
_Logger.LogInformation($"App {quickLookup.appId}: Received Payment");
InvalidateCacheForApp(quickLookup.appId);
break;
case InvoiceEvent.Created:
case InvoiceEvent.MarkedInvalid:
case InvoiceEvent.MarkedCompleted:
if (quickLookup.useInvoiceAmount)
{
InvalidateCacheForApp(quickLookup.appId);
}
break;
case InvoiceEvent.Completed:
InvalidateCacheForApp(quickLookup.appId);
break;
}
}
private void InvalidateCacheForApp(string appId)
{
_Logger.LogInformation($"App {appId} cache invalidated");
_MemoryCache.Remove(GetCacheKey(appId));
GetCrowdfundInfo(appId).ContinueWith(task =>
{
_HubContext.Clients.Group(appId).SendCoreAsync(CrowdfundHub.InfoUpdated, new object[]{ task.Result} );
}, TaskScheduler.Current);
}
private async Task<decimal> GetCurrentContributionAmount(Dictionary<string, decimal> stats, string primaryCurrency, RateRules rateRules)
{
decimal result = 0;
var ratesTask = _RateFetcher .FetchRates(
stats.Keys
.Select((x) => new CurrencyPair( primaryCurrency, PaymentMethodId.Parse(x).CryptoCode))
.Distinct()
.ToHashSet(),
rateRules);
var finalTasks = new List<Task>();
foreach (var rateTask in ratesTask)
{
finalTasks.Add(Task.Run(async () =>
{
var tResult = await rateTask.Value;
var rate = tResult.BidAsk?.Bid;
if (rate == null) return;
foreach (var stat in stats)
{
if (string.Equals(PaymentMethodId.Parse(stat.Key).CryptoCode, rateTask.Key.Right,
StringComparison.InvariantCultureIgnoreCase))
{
result += (1m / rate.Value) * stat.Value;
}
}
}));
}
await Task.WhenAll(finalTasks);
return result;
}
private static Dictionary<string, decimal> GetCurrentContributionAmountStats(InvoiceEntity[] invoices, bool usePaymentData = true)
{
if(usePaymentData){
var payments = invoices.SelectMany(entity => entity.GetPayments());
var groupedByMethod = payments.GroupBy(entity => entity.GetPaymentMethodId());
return groupedByMethod.ToDictionary(entities => entities.Key.ToString(),
entities => entities.Sum(entity => entity.GetCryptoPaymentData().GetValue()));
}
else
{
return invoices
.GroupBy(entity => entity.ProductInformation.Currency)
.ToDictionary(
entities => entities.Key,
entities => entities.Sum(entity => entity.ProductInformation.Price));
}
}
private async Task<ViewCrowdfundViewModel> GetInfo(AppData appData, string statusMessage= null)
{
var settings = appData.GetSettings<AppsController.CrowdfundSettings>();
var resetEvery = settings.StartDate.HasValue? settings.ResetEvery : CrowdfundResetEvery.Never;
DateTime? lastResetDate = null;
DateTime? nextResetDate = null;
if (resetEvery != CrowdfundResetEvery.Never)
{
lastResetDate = settings.StartDate.Value;
nextResetDate = lastResetDate.Value;
while (DateTime.Now >= nextResetDate)
{
lastResetDate = nextResetDate;
switch (resetEvery)
{
case CrowdfundResetEvery.Hour:
nextResetDate = lastResetDate.Value.AddHours(settings.ResetEveryAmount);
break;
case CrowdfundResetEvery.Day:
nextResetDate = lastResetDate.Value.AddDays(settings.ResetEveryAmount);
break;
case CrowdfundResetEvery.Month:
nextResetDate = lastResetDate.Value.AddMonths(settings.ResetEveryAmount);
break;
case CrowdfundResetEvery.Year:
nextResetDate = lastResetDate.Value.AddYears(settings.ResetEveryAmount);
break;
}
}
}
var invoices = await GetInvoicesForApp(settings.UseAllStoreInvoices? null : appData.Id, lastResetDate);
var completeInvoices = invoices.Where(entity => entity.Status == InvoiceStatus.Complete).ToArray();
var pendingInvoices = invoices.Where(entity => entity.Status != InvoiceStatus.Complete).ToArray();
var rateRules = appData.StoreData.GetStoreBlob().GetRateRules(_BtcPayNetworkProvider);
var pendingPaymentStats = GetCurrentContributionAmountStats(pendingInvoices, !settings.UseInvoiceAmount);
var paymentStats = GetCurrentContributionAmountStats(completeInvoices, !settings.UseInvoiceAmount);
var currentAmount = await GetCurrentContributionAmount(
paymentStats,
settings.TargetCurrency, rateRules);
var currentPendingAmount = await GetCurrentContributionAmount(
pendingPaymentStats,
settings.TargetCurrency, rateRules);
var perkCount = invoices
.Where(entity => !string.IsNullOrEmpty( entity.ProductInformation.ItemCode))
.GroupBy(entity => entity.ProductInformation.ItemCode)
.ToDictionary(entities => entities.Key, entities => entities.Count());
var perks = _AppsHelper.Parse(settings.PerksTemplate, settings.TargetCurrency);
if (settings.SortPerksByPopularity)
{
var ordered = perkCount.OrderByDescending(pair => pair.Value);
var newPerksOrder = ordered
.Select(keyValuePair => perks.SingleOrDefault(item => item.Id == keyValuePair.Key))
.Where(matchingPerk => matchingPerk != null)
.ToList();
var remainingPerks = perks.Where(item => !newPerksOrder.Contains(item));
newPerksOrder.AddRange(remainingPerks);
perks = newPerksOrder.ToArray();
}
return new ViewCrowdfundViewModel()
{
Title = settings.Title,
Tagline = settings.Tagline,
Description = settings.Description,
CustomCSSLink = settings.CustomCSSLink,
MainImageUrl = settings.MainImageUrl,
EmbeddedCSS = settings.EmbeddedCSS,
StoreId = appData.StoreDataId,
AppId = appData.Id,
StartDate = settings.StartDate?.ToUniversalTime(),
EndDate = settings.EndDate?.ToUniversalTime(),
TargetAmount = settings.TargetAmount,
TargetCurrency = settings.TargetCurrency,
EnforceTargetAmount = settings.EnforceTargetAmount,
StatusMessage = statusMessage,
Perks = perks,
DisqusEnabled = settings.DisqusEnabled,
SoundsEnabled = settings.SoundsEnabled,
DisqusShortname = settings.DisqusShortname,
AnimationsEnabled = settings.AnimationsEnabled,
ResetEveryAmount = settings.ResetEveryAmount,
DisplayPerksRanking = settings.DisplayPerksRanking,
PerkCount = perkCount,
ResetEvery = Enum.GetName(typeof(CrowdfundResetEvery),settings.ResetEvery),
CurrencyData = _AppsHelper.GetCurrencyData(settings.TargetCurrency, true),
Info = new ViewCrowdfundViewModel.CrowdfundInfo()
{
TotalContributors = invoices.Length,
CurrentPendingAmount = currentPendingAmount,
CurrentAmount = currentAmount,
ProgressPercentage = (currentAmount/ settings.TargetAmount) * 100,
PendingProgressPercentage = ( currentPendingAmount/ settings.TargetAmount) * 100,
LastUpdated = DateTime.Now,
PaymentStats = paymentStats,
PendingPaymentStats = pendingPaymentStats,
LastResetDate = lastResetDate,
NextResetDate = nextResetDate
}
};
}
private async Task<InvoiceEntity[]> GetInvoicesForApp(string appId, DateTime? startDate = null)
{
return await _InvoiceRepository.GetInvoices(new InvoiceQuery()
{
OrderId = appId == null? null : new []{$"{CrowdfundInvoiceOrderIdPrefix}{appId}"},
Status = new string[]{
InvoiceState.ToString(InvoiceStatus.New),
InvoiceState.ToString(InvoiceStatus.Paid),
InvoiceState.ToString(InvoiceStatus.Confirmed),
InvoiceState.ToString(InvoiceStatus.Complete)},
StartDate = startDate
});
}
public void Dispose()
{
_Subscriptions.ForEach(subscription => subscription.Dispose());
}
}
}

View File

@ -3,8 +3,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Hangfire;
using Hangfire.MemoryStorage;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations;
using JetBrains.Annotations;
@ -98,15 +96,5 @@ namespace BTCPayServer.Data
else if (_Type == DatabaseType.MySQL)
builder.UseMySql(_ConnectionString);
}
public void ConfigureHangfireBuilder(IGlobalConfiguration builder)
{
builder.UseMemoryStorage();
//We always use memory storage because of incompatibilities with the latest postgres in 2.1
//if (_Type == DatabaseType.Sqlite)
// builder.UseMemoryStorage(); //Sqlite provider does not support multiple workers
//else if (_Type == DatabaseType.Postgres)
// builder.UsePostgreSqlStorage(_ConnectionString);
}
}
}

View File

@ -21,6 +21,7 @@ using BTCPayServer.Payments.Changelly;
using BTCPayServer.Payments.CoinSwitch;
using BTCPayServer.Security;
using BTCPayServer.Rating;
using BTCPayServer.Services.Mails;
namespace BTCPayServer.Data
{
@ -403,6 +404,8 @@ namespace BTCPayServer.Data
[Obsolete("Use SetWalletKeyPathRoot/GetWalletKeyPathRoot instead")]
public Dictionary<string, string> WalletKeyPathRoots { get; set; } = new Dictionary<string, string>();
public EmailSettings EmailSettings { get; set; }
public IPaymentFilter GetExcludedPaymentMethods()
{
#pragma warning disable CS0618 // Type or member is obsolete

View File

@ -10,9 +10,9 @@ namespace BTCPayServer.Services
{
public static class EmailSenderExtensions
{
public static Task SendEmailConfirmationAsync(this IEmailSender emailSender, string email, string link)
public static void SendEmailConfirmation(this IEmailSender emailSender, string email, string link)
{
return emailSender.SendEmailAsync(email, "Confirm your email",
emailSender.SendEmail(email, "Confirm your email",
$"Please confirm your account by clicking this link: <a href='{HtmlEncoder.Default.Encode(link)}'>link</a>");
}
}

View File

@ -0,0 +1,133 @@
using System;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using BTCPayServer.Logging;
using BTCPayServer.Services;
using Microsoft.Extensions.Hosting;
using NicolasDorier.RateLimits;
namespace BTCPayServer.HostedServices
{
public class BackgroundJobSchedulerHostedService : IHostedService
{
public BackgroundJobSchedulerHostedService(IBackgroundJobClient backgroundJobClient)
{
BackgroundJobClient = (BackgroundJobClient)backgroundJobClient;
}
public BackgroundJobClient BackgroundJobClient { get; }
Task _Loop;
public Task StartAsync(CancellationToken cancellationToken)
{
_Stop = new CancellationTokenSource();
_Loop = BackgroundJobClient.ProcessJobs(_Stop.Token);
return Task.CompletedTask;
}
CancellationTokenSource _Stop;
public async Task StopAsync(CancellationToken cancellationToken)
{
_Stop.Cancel();
try
{
await _Loop;
}
catch (OperationCanceledException)
{
}
await BackgroundJobClient.WaitAllRunning(cancellationToken);
}
}
public class BackgroundJobClient : IBackgroundJobClient
{
class BackgroundJob
{
public Func<Task> Action;
public TimeSpan Delay;
public IDelay DelayImplementation;
public BackgroundJob(Func<Task> action, TimeSpan delay, IDelay delayImplementation)
{
this.Action = action;
this.Delay = delay;
this.DelayImplementation = delayImplementation;
}
public async Task Run(CancellationToken cancellationToken)
{
await DelayImplementation.Wait(Delay, cancellationToken);
await Action();
}
}
public IDelay Delay { get; set; } = TaskDelay.Instance;
public int GetExecutingCount()
{
lock (_Processing)
{
return _Processing.Count();
}
}
private Channel<BackgroundJob> _Jobs = Channel.CreateUnbounded<BackgroundJob>();
HashSet<Task> _Processing = new HashSet<Task>();
public void Schedule(Func<Task> action, TimeSpan delay)
{
_Jobs.Writer.TryWrite(new BackgroundJob(action, delay, Delay));
}
public async Task WaitAllRunning(CancellationToken cancellationToken)
{
Task[] processing = null;
lock (_Processing)
{
processing = _Processing.ToArray();
}
try
{
await Task.WhenAll(processing).WithCancellation(cancellationToken);
}
catch (Exception) when (cancellationToken.IsCancellationRequested)
{
throw;
}
}
public async Task ProcessJobs(CancellationToken cancellationToken)
{
while (await _Jobs.Reader.WaitToReadAsync(cancellationToken))
{
if (_Jobs.Reader.TryRead(out var job))
{
var processing = job.Run(cancellationToken);
lock (_Processing)
{
_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 =>
{
if (t.IsFaulted)
{
Logs.PayServer.LogWarning(t.Exception, "Unhandled exception while job running");
}
lock (_Processing)
{
_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
}
}
}
}
}

View File

@ -1,10 +1,7 @@
using Hangfire;
using Hangfire.Common;
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Hangfire.Annotations;
using System.Reflection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
@ -21,6 +18,7 @@ using NBXplorer;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Payments;
using BTCPayServer.Services.Mails;
using BTCPayServer.Services;
namespace BTCPayServer.HostedServices
{
@ -44,16 +42,11 @@ namespace BTCPayServer.HostedServices
public string Message { get; set; }
}
public ILogger Logger
{
get; set;
}
IBackgroundJobClient _JobClient;
EventAggregator _EventAggregator;
InvoiceRepository _InvoiceRepository;
BTCPayNetworkProvider _NetworkProvider;
IEmailSender _EmailSender;
private readonly EmailSenderFactory _EmailSenderFactory;
public InvoiceNotificationManager(
IBackgroundJobClient jobClient,
@ -61,17 +54,16 @@ namespace BTCPayServer.HostedServices
InvoiceRepository invoiceRepository,
BTCPayNetworkProvider networkProvider,
ILogger<InvoiceNotificationManager> logger,
IEmailSender emailSender)
EmailSenderFactory emailSenderFactory)
{
Logger = logger as ILogger ?? NullLogger.Instance;
_JobClient = jobClient;
_EventAggregator = eventAggregator;
_InvoiceRepository = invoiceRepository;
_NetworkProvider = networkProvider;
_EmailSender = emailSender;
_EmailSenderFactory = emailSenderFactory;
}
async Task Notify(InvoiceEntity invoice, int? eventCode = null, string name = null)
void Notify(InvoiceEntity invoice, int? eventCode = null, string name = null)
{
CancellationTokenSource cts = new CancellationTokenSource(10000);
@ -85,58 +77,31 @@ namespace BTCPayServer.HostedServices
invoice.StoreId
};
// TODO: Consider adding info on ItemDesc and payment info (amount)
var emailBody = NBitcoin.JsonConverters.Serializer.ToString(ipn);
await _EmailSender.SendEmailAsync(
invoice.NotificationEmail, $"BtcPayServer Invoice Notification - ${invoice.StoreId}", emailBody);
}
try
{
if (string.IsNullOrEmpty(invoice.NotificationURL))
return;
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name));
var response = await SendNotification(invoice, eventCode, name, cts.Token);
response.EnsureSuccessStatusCode();
_EmailSenderFactory.GetEmailSender(invoice.StoreId).SendEmail(
invoice.NotificationEmail,
$"BtcPayServer Invoice Notification - ${invoice.StoreId}",
emailBody);
}
if (string.IsNullOrEmpty(invoice.NotificationURL))
return;
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name)
{
Error = "Timeout"
});
}
catch (Exception ex) // It fails, it is OK because we try with hangfire after
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name)
{
Error = ex.Message
});
}
var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Invoice = invoice, EventCode = eventCode, Message = name });
if (!string.IsNullOrEmpty(invoice.NotificationURL))
_JobClient.Schedule(() => NotifyHttp(invoiceStr), TimeSpan.Zero);
}
ConcurrentDictionary<string, string> _Executing = new ConcurrentDictionary<string, string>();
public async Task NotifyHttp(string invoiceData)
{
var job = NBitcoin.JsonConverters.Serializer.ToObject<ScheduledJob>(invoiceData);
var jobId = GetHttpJobId(job.Invoice);
if (!_Executing.TryAdd(jobId, jobId))
return; //For some reason, Hangfire fire the job several time
Logger.LogInformation("Running " + jobId);
bool reschedule = false;
CancellationTokenSource cts = new CancellationTokenSource(10000);
try
{
HttpResponseMessage response = await SendNotification(job.Invoice, job.EventCode, job.Message, cts.Token);
reschedule = !response.IsSuccessStatusCode;
Logger.LogInformation("Job " + jobId + " returned " + response.StatusCode);
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
{
Error = reschedule ? $"Unexpected return code: {(int)response.StatusCode}" : null
@ -149,9 +114,8 @@ namespace BTCPayServer.HostedServices
Error = "Timeout"
});
reschedule = true;
Logger.LogInformation("Job " + jobId + " timed out");
}
catch (Exception ex) // It fails, it is OK because we try with hangfire after
catch (Exception ex)
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
{
@ -166,21 +130,18 @@ namespace BTCPayServer.HostedServices
ex = ex.InnerException;
}
string message = String.Join(',', messages.ToArray());
Logger.LogInformation("Job " + jobId + " threw exception " + message);
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
{
Error = $"Unexpected error: {message}"
});
}
finally { cts.Dispose(); _Executing.TryRemove(jobId, out jobId); }
finally { cts?.Dispose(); }
job.TryCount++;
if (job.TryCount < MaxTry && reschedule)
{
Logger.LogInformation("Rescheduling " + jobId + " in 10 minutes, remaining try " + (MaxTry - job.TryCount));
invoiceData = NBitcoin.JsonConverters.Serializer.ToString(job);
_JobClient.Schedule(() => NotifyHttp(invoiceData), TimeSpan.FromMinutes(10.0));
}
@ -320,11 +281,6 @@ namespace BTCPayServer.HostedServices
int MaxTry = 6;
private static string GetHttpJobId(InvoiceEntity invoice)
{
return $"{invoice.Id}-{invoice.Status}-HTTP";
}
CompositeDisposable leases = new CompositeDisposable();
public Task StartAsync(CancellationToken cancellationToken)
{
@ -350,19 +306,18 @@ namespace BTCPayServer.HostedServices
e.Name == InvoiceEvent.Completed ||
e.Name == InvoiceEvent.ExpiredPaidPartial
)
tasks.Add(Notify(invoice));
Notify(invoice);
}
if (e.Name == "invoice_confirmed")
{
tasks.Add(Notify(invoice));
Notify(invoice);
}
if (invoice.ExtendedNotifications)
{
tasks.Add(Notify(invoice, e.EventCode, e.Name));
Notify(invoice, e.EventCode, e.Name);
}
await Task.WhenAll(tasks.ToArray());
}));

View File

@ -11,7 +11,6 @@ using BTCPayServer.Logging;
using System.Threading;
using Microsoft.Extensions.Hosting;
using System.Collections.Concurrent;
using Hangfire;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Controllers;
using BTCPayServer.Events;

View File

@ -38,6 +38,8 @@ using BTCPayServer.Logging;
using BTCPayServer.HostedServices;
using Meziantou.AspNetCore.BundleTagHelpers;
using System.Security.Claims;
using BTCPayServer.Crowdfund;
using BTCPayServer.Hubs;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Security;
@ -74,6 +76,7 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton<TokenRepository>();
services.TryAddSingleton<EventAggregator>();
services.TryAddSingleton<CoinAverageSettings>();
services.TryAddSingleton<CrowdfundHubStreamer>();
services.TryAddSingleton<ApplicationDbContextFactory>(o =>
{
var opts = o.GetRequiredService<BTCPayServerOptions>();
@ -114,7 +117,7 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton<CurrencyNameTable>();
services.TryAddSingleton<IFeeProviderFactory>(o => new NBXplorerFeeProviderFactory(o.GetRequiredService<ExplorerClientProvider>())
{
Fallback = new FeeRate(100, 1),
Fallback = new FeeRate(100L, 1),
BlockTarget = 20
});
@ -142,6 +145,8 @@ namespace BTCPayServer.Hosting
services.AddSingleton<IHostedService, InvoiceNotificationManager>();
services.AddSingleton<IHostedService, InvoiceWatcher>();
services.AddSingleton<IHostedService, RatesHostedService>();
services.AddSingleton<IHostedService, BackgroundJobSchedulerHostedService>();
services.AddSingleton<IBackgroundJobClient, BackgroundJobClient>();
services.AddTransient<IConfigureOptions<MvcOptions>, BTCPayClaimsFilter>();
services.TryAddSingleton<ExplorerClientProvider>();
@ -158,8 +163,9 @@ namespace BTCPayServer.Hosting
services.TryAddScoped<IHttpContextAccessor, HttpContextAccessor>();
services.AddTransient<AccessTokenController>();
services.AddTransient<InvoiceController>();
services.AddTransient<AppsPublicController>();
// Add application services.
services.AddTransient<IEmailSender, EmailSender>();
services.AddSingleton<EmailSenderFactory>();
// bundling
services.AddAuthorization(o => Policies.AddBTCPayPolicies(o));

View File

@ -19,7 +19,6 @@ using BTCPayServer.Models;
using Microsoft.AspNetCore.Identity;
using BTCPayServer.Data;
using Microsoft.Extensions.Logging;
using Hangfire;
using BTCPayServer.Logging;
using Microsoft.AspNetCore.Authorization;
using System.Threading.Tasks;
@ -27,17 +26,15 @@ using BTCPayServer.Controllers;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Mails;
using Microsoft.Extensions.Configuration;
using Hangfire.AspNetCore;
using BTCPayServer.Configuration;
using System.IO;
using Hangfire.Dashboard;
using Hangfire.Annotations;
using Microsoft.Extensions.DependencyInjection.Extensions;
using System.Threading;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Mvc.Cors.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using System.Net;
using BTCPayServer.Hubs;
using Meziantou.AspNetCore.BundleTagHelpers;
using BTCPayServer.Security;
@ -45,18 +42,6 @@ namespace BTCPayServer.Hosting
{
public class Startup
{
class NeedRole : IDashboardAuthorizationFilter
{
string _Role;
public NeedRole(string role)
{
_Role = role;
}
public bool Authorize([NotNull] DashboardContext context)
{
return context.GetHttpContext().User.IsInRole(_Role);
}
}
public Startup(IConfiguration conf, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
Configuration = conf;
@ -78,7 +63,7 @@ namespace BTCPayServer.Hosting
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddSignalR();
services.AddBTCPayServer();
services.AddMvc(o =>
{
@ -99,7 +84,7 @@ namespace BTCPayServer.Hosting
services.Configure<IdentityOptions>(options =>
{
options.Password.RequireDigit = false;
options.Password.RequiredLength = 7;
options.Password.RequiredLength = 6;
options.Password.RequireLowercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
@ -107,13 +92,6 @@ namespace BTCPayServer.Hosting
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;
});
services.AddHangfire((o) =>
{
var scope = AspNetCoreJobActivator.Current.BeginScope(null);
var options = (ApplicationDbContextFactory)scope.Resolve(typeof(ApplicationDbContextFactory));
options.ConfigureHangfireBuilder(o);
});
services.AddCors(o =>
{
o.AddPolicy("BitpayAPI", b =>
@ -192,11 +170,9 @@ namespace BTCPayServer.Hosting
app.UsePayServer();
app.UseStaticFiles();
app.UseAuthentication();
app.UseHangfireServer();
app.UseHangfireDashboard("/hangfire", new DashboardOptions()
app.UseSignalR(route =>
{
AppPath = options.GetRootUri(),
Authorization = new[] { new NeedRole(Roles.ServerAdmin) }
route.MapHub<CrowdfundHub>("/apps/crowdfund/hub");
});
app.UseWebSockets();
app.UseStatusCodePages();

33
BTCPayServer/IDelay.cs Normal file
View File

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace BTCPayServer
{
public interface IDelay
{
Task Wait(TimeSpan delay, CancellationToken cancellationToken);
}
public class TaskDelay : IDelay
{
TaskDelay()
{
}
private static readonly TaskDelay _Instance = new TaskDelay();
public static TaskDelay Instance
{
get
{
return _Instance;
}
}
public Task Wait(TimeSpan delay, CancellationToken cancellationToken)
{
return Task.Delay(delay, cancellationToken);
}
}
}

View File

@ -0,0 +1,91 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace BTCPayServer.Models.AppViewModels
{
public class UpdateCrowdfundViewModel
{
[Required] [MaxLength(30)] public string Title { get; set; }
[MaxLength(50)] public string Tagline { get; set; }
[Required] public string Description { get; set; }
[Display(Name = "Featured Image")]
public string MainImageUrl { get; set; }
public string NotificationUrl { get; set; }
[Required]
[Display(Name = "Allow crowdfund to be publicly visible (still visible to you)")]
public bool Enabled { get; set; } = false;
[Required]
[Display(Name = "Enable background animations on new payments")]
public bool AnimationsEnabled { get; set; } = true;
[Required]
[Display(Name = "Enable sounds on new payments")]
public bool SoundsEnabled { get; set; } = true;
[Required]
[Display(Name = "Enable Disqus Comments")]
public bool DisqusEnabled { get; set; } = true;
[Display(Name = "Disqus Shortname")] public string DisqusShortname { get; set; }
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
[Required]
[MaxLength(5)]
[Display(Name = "The primary currency used for targets and stats. (e.g. BTC, LTC, USD, etc.)")]
public string TargetCurrency { get; set; } = "BTC";
[Display(Name = "Set a Target amount ")]
public decimal? TargetAmount { get; set; }
public IEnumerable<string> ResetEveryValues = Enum.GetNames(typeof(CrowdfundResetEvery));
[Display(Name = "Reset goal every")] public string ResetEvery { get; set; } = nameof(CrowdfundResetEvery.Never);
public int ResetEveryAmount { get; set; } = 1;
[Display(Name = "Do not allow additional contributions after target has been reached")]
public bool EnforceTargetAmount { get; set; }
[Display(Name = "Contribution Perks Template")]
public string PerksTemplate { get; set; }
[MaxLength(500)]
[Display(Name = "Custom bootstrap CSS file")]
public string CustomCSSLink { get; set; }
[Display(Name = "Custom CSS Code")]
public string EmbeddedCSS { get; set; }
[Display(Name = "Base the contributed goal amount on the invoice amount and not actual payments")]
public bool UseInvoiceAmount { get; set; }
[Display(Name = "Count all invoices created on the store as part of the crowdfunding goal")]
public bool UseAllStoreInvoices { get; set; }
public string AppId { get; set; }
[Display(Name = "Sort contribution perks by popularity")]
public bool SortPerksByPopularity { get; set; }
[Display(Name = "Display contribution ranking")]
public bool DisplayPerksRanking { get; set; }
}
public enum CrowdfundResetEvery
{
Never,
Hour,
Day,
Month,
Year
}
}

View File

@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using System.ComponentModel.DataAnnotations;
namespace BTCPayServer.Models.AppViewModels
{

View File

@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
namespace BTCPayServer.Models.AppViewModels
{
public class ViewCrowdfundViewModel
{
public string StatusMessage{ get; set; }
public string StoreId { get; set; }
public string AppId { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string MainImageUrl { get; set; }
public string EmbeddedCSS { get; set; }
public string CustomCSSLink { get; set; }
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
public string TargetCurrency { get; set; }
public decimal? TargetAmount { get; set; }
public bool EnforceTargetAmount { get; set; }
public CrowdfundInfo Info { get; set; }
public string Tagline { get; set; }
public ViewPointOfSaleViewModel.Item[] Perks { get; set; }
public bool DisqusEnabled { get; set; }
public bool SoundsEnabled { get; set; }
public string DisqusShortname { get; set; }
public bool AnimationsEnabled { get; set; }
public int ResetEveryAmount { get; set; }
public string ResetEvery { get; set; }
public Dictionary<string, int> PerkCount { get; set; }
public CurrencyData CurrencyData { get; set; }
public class CrowdfundInfo
{
public int TotalContributors { get; set; }
public decimal CurrentPendingAmount { get; set; }
public decimal CurrentAmount { get; set; }
public decimal? ProgressPercentage { get; set; }
public decimal? PendingProgressPercentage { get; set; }
public DateTime LastUpdated { get; set; }
public Dictionary<string, decimal> PaymentStats { get; set; }
public Dictionary<string, decimal> PendingPaymentStats { get; set; }
public DateTime? LastResetDate { get; set; }
public DateTime? NextResetDate { get; set; }
}
public bool Started => !StartDate.HasValue || DateTime.Now.ToUniversalTime() > StartDate;
public bool Ended => !EndDate.HasValue || DateTime.Now.ToUniversalTime() > EndDate;
public bool DisplayPerksRanking { get; set; }
}
public class ContributeToCrowdfund
{
public ViewCrowdfundViewModel ViewCrowdfundViewModel { get; set; }
[Required] public decimal Amount { get; set; }
public string Email { get; set; }
public string ChoiceKey { get; set; }
public bool RedirectToCheckout { get; set; }
public string RedirectUrl { get; set; }
}
}

View File

@ -90,6 +90,12 @@ namespace BTCPayServer.Models
get; set;
}
[JsonProperty("taxIncluded", DefaultValueHandling = DefaultValueHandling.Ignore)]
public decimal TaxIncluded
{
get; set;
}
//"currency":"USD"
[JsonProperty("currency")]
public string Currency

View File

@ -143,6 +143,6 @@ namespace BTCPayServer.Models.InvoicingModels
public DateTimeOffset MonitoringDate { get; internal set; }
public List<Data.InvoiceEventData> Events { get; internal set; }
public string NotificationEmail { get; internal set; }
public Dictionary<string, string> PosData { get; set; }
public Dictionary<string, object> PosData { get; set; }
}
}

View File

@ -15,6 +15,10 @@ namespace BTCPayServer.Models.InvoicingModels
{
get; set;
}
public int Total
{
get; set;
}
public string SearchTerm
{
get; set;

View File

@ -72,5 +72,6 @@ namespace BTCPayServer.Models.StoreViewModels
[Display(Name = "API Key")]
public string ApiKey { get; set; }
public string EncodedApiKey { get; set; }
public bool StoreNotConfigured { get; set; }
}
}

View File

@ -11,5 +11,6 @@ namespace BTCPayServer.Models.WalletViewModels
public bool SubstractFees { get; set; }
public decimal Amount { get; set; }
public string Destination { get; set; }
public bool NoChange { get; set; }
}
}

View File

@ -28,6 +28,10 @@ namespace BTCPayServer.Models.WalletViewModels
[Display(Name = "Fee rate (satoshi per byte)")]
[Required]
public int FeeSatoshiPerByte { get; set; }
[Display(Name = "Make sure no change UTXO is created")]
public bool NoChange { get; set; }
public bool AdvancedMode { get; set; }
public decimal? Rate { get; set; }
public int Divisibility { get; set; }
public string Fiat { get; set; }

View File

@ -47,7 +47,7 @@ namespace BTCPayServer.Payments.Lightning
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
throw new PaymentMethodUnavailableException($"The lightning node did not replied in a timely maner");
throw new PaymentMethodUnavailableException($"The lightning node did not reply in a timely maner");
}
catch (Exception ex)
{
@ -78,7 +78,7 @@ namespace BTCPayServer.Payments.Lightning
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
throw new PaymentMethodUnavailableException($"The lightning node did not replied in a timely manner");
throw new PaymentMethodUnavailableException($"The lightning node did not reply in a timely manner");
}
catch (Exception ex)
{
@ -115,7 +115,7 @@ namespace BTCPayServer.Payments.Lightning
}
if (address == null)
throw new PaymentMethodUnavailableException($"DNS did not resolved {nodeInfo.Host}");
throw new PaymentMethodUnavailableException($"DNS did not resolve {nodeInfo.Host}");
using (var tcp = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp))
{

View File

@ -11,6 +11,7 @@
"BTCPAY_BTCEXTERNALLNDGRPC": "type=lnd-grpc;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true",
"BTCPAY_BTCEXTERNALLNDREST": "type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/lnd-rest/btc/;allowinsecure=true;macaroonfilepath=D:\\admin.macaroon",
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
"BTCPAY_DISABLE-REGISTRATION": "false",
"ASPNETCORE_ENVIRONMENT": "Development",
"BTCPAY_CHAINS": "btc,ltc",
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver"
@ -32,6 +33,7 @@
"BTCPAY_BTCEXTERNALSPARK": "server=https://127.0.0.1:53280/spark/btc/;cookiefile=fake",
"BTCPAY_BTCEXTERNALCHARGE": "server=https://127.0.0.1:53280/spark/btc/;cookiefilepath=fake",
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
"BTCPAY_DISABLE-REGISTRATION": "false",
"ASPNETCORE_ENVIRONMENT": "Development",
"BTCPAY_CHAINS": "btc,ltc",
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver",

View File

@ -198,44 +198,6 @@ namespace BTCPayServer.Security
}
yield return token;
}
private bool IsBitpayAPI(HttpContext httpContext, bool bitpayAuth)
{
if (!httpContext.Request.Path.HasValue)
return false;
var isJson = (httpContext.Request.ContentType ?? string.Empty).StartsWith("application/json", StringComparison.OrdinalIgnoreCase);
var path = httpContext.Request.Path.Value;
if (
bitpayAuth &&
(path == "/invoices" || path == "/invoices/") &&
httpContext.Request.Method == "POST" &&
isJson)
return true;
if (
bitpayAuth &&
(path == "/invoices" || path == "/invoices/") &&
httpContext.Request.Method == "GET")
return true;
if (
path.StartsWith("/invoices/", StringComparison.OrdinalIgnoreCase) &&
httpContext.Request.Method == "GET" &&
(isJson || httpContext.Request.Query.ContainsKey("token")))
return true;
if (path.Equals("/rates", StringComparison.OrdinalIgnoreCase) &&
httpContext.Request.Method == "GET")
return true;
if (
path.Equals("/tokens", StringComparison.Ordinal) &&
(httpContext.Request.Method == "GET" || httpContext.Request.Method == "POST"))
return true;
return false;
}
}
internal static void AddAuthentication(IServiceCollection services, Action<BitpayAuthOptions> bitpayAuth = null)
{

View File

@ -7,6 +7,7 @@ namespace BTCPayServer.Services.Apps
{
public enum AppType
{
PointOfSale
PointOfSale,
Crowdfund
}
}

View File

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Services
{
public interface IBackgroundJobClient
{
void Schedule(Func<Task> act, TimeSpan zero);
}
}

View File

@ -5,6 +5,7 @@ using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using Newtonsoft.Json;
namespace BTCPayServer.Services.Invoices.Export
@ -12,10 +13,12 @@ namespace BTCPayServer.Services.Invoices.Export
public class InvoiceExport
{
public BTCPayNetworkProvider Networks { get; }
public CurrencyNameTable Currencies { get; }
public InvoiceExport(BTCPayNetworkProvider networks)
public InvoiceExport(BTCPayNetworkProvider networks, CurrencyNameTable currencies)
{
Networks = networks;
Currencies = currencies;
}
public string Process(InvoiceEntity[] invoices, string fileFormat)
{
@ -52,6 +55,7 @@ namespace BTCPayServer.Services.Invoices.Export
private IEnumerable<ExportInvoiceHolder> convertFromDb(InvoiceEntity invoice)
{
var exportList = new List<ExportInvoiceHolder>();
var currency = Currencies.GetNumberFormatInfo(invoice.ProductInformation.Currency, true);
var invoiceDue = invoice.ProductInformation.Price;
// in this first version we are only exporting invoices that were paid
@ -60,7 +64,6 @@ namespace BTCPayServer.Services.Invoices.Export
// not accounted payments are payments which got double spent like RBfed
if (!payment.Accounted)
continue;
var cryptoCode = payment.GetPaymentMethodId().CryptoCode;
var pdata = payment.GetCryptoPaymentData();
@ -77,13 +80,13 @@ namespace BTCPayServer.Services.Invoices.Export
PaymentType = payment.GetPaymentMethodId().PaymentType == Payments.PaymentTypes.BTCLike ? "OnChain" : "OffChain",
Destination = payment.GetCryptoPaymentData().GetDestination(Networks.GetNetwork(cryptoCode)),
Paid = pdata.GetValue().ToString(CultureInfo.InvariantCulture),
PaidCurrency = (pdata.GetValue() * pmethod.Rate).ToString(CultureInfo.InvariantCulture),
PaidCurrency = Math.Round(pdata.GetValue() * pmethod.Rate, currency.NumberDecimalDigits).ToString(CultureInfo.InvariantCulture),
// Adding NetworkFee because Paid doesn't take into account network fees
// so if fee is 10000 satoshis, customer can essentially send infinite number of tx
// and merchant effectivelly would receive 0 BTC, invoice won't be paid
// while looking just at export you could sum Paid and assume merchant "received payments"
NetworkFee = payment.NetworkFee.ToString(CultureInfo.InvariantCulture),
InvoiceDue = invoiceDue,
InvoiceDue = Math.Round(invoiceDue, currency.NumberDecimalDigits),
OrderId = invoice.OrderId,
StoreId = invoice.StoreId,
InvoiceId = invoice.Id,

View File

@ -91,6 +91,12 @@ namespace BTCPayServer.Services.Invoices
get; set;
}
[JsonProperty(PropertyName = "taxIncluded", DefaultValueHandling = DefaultValueHandling.Ignore)]
public decimal TaxIncluded
{
get; set;
}
[JsonProperty(PropertyName = "currency")]
public string Currency
{
@ -344,6 +350,7 @@ namespace BTCPayServer.Services.Invoices
InvoiceResponse dto = new InvoiceResponse
{
Id = Id,
StoreId = StoreId,
OrderId = OrderId,
PosData = PosData,
CurrentTime = DateTimeOffset.UtcNow,

View File

@ -178,7 +178,6 @@ retry:
textSearch.Add(invoice.StoreId);
AddToTextSearch(invoice.Id, textSearch.ToArray());
return invoice;
}
@ -420,91 +419,108 @@ retry:
return entity;
}
private IQueryable<Data.InvoiceData> GetInvoiceQuery(ApplicationDbContext context, InvoiceQuery queryObject)
{
IQueryable<Data.InvoiceData> query = context.Invoices;
if (!string.IsNullOrEmpty(queryObject.InvoiceId))
{
query = query.Where(i => i.Id == queryObject.InvoiceId);
}
if (queryObject.StoreId != null && queryObject.StoreId.Length > 0)
{
var stores = queryObject.StoreId.ToHashSet();
query = query.Where(i => stores.Contains(i.StoreDataId));
}
if (queryObject.UserId != null)
{
query = query.Where(i => i.StoreData.UserStores.Any(u => u.ApplicationUserId == queryObject.UserId));
}
if (!string.IsNullOrEmpty(queryObject.TextSearch))
{
var ids = new HashSet<string>(SearchInvoice(queryObject.TextSearch));
if (ids.Count == 0)
{
// Hacky way to return an empty query object. The nice way is much too elaborate:
// https://stackoverflow.com/questions/33305495/how-to-return-empty-iqueryable-in-an-async-repository-method
return query.Where(x => false);
}
query = query.Where(i => ids.Contains(i.Id));
}
if (queryObject.StartDate != null)
query = query.Where(i => queryObject.StartDate.Value <= i.Created);
if (queryObject.EndDate != null)
query = query.Where(i => i.Created <= queryObject.EndDate.Value);
if (queryObject.OrderId != null && queryObject.OrderId.Length > 0)
{
var statusSet = queryObject.OrderId.ToHashSet();
query = query.Where(i => statusSet.Contains(i.OrderId));
}
if (queryObject.ItemCode != null && queryObject.ItemCode.Length > 0)
{
var statusSet = queryObject.ItemCode.ToHashSet();
query = query.Where(i => statusSet.Contains(i.ItemCode));
}
if (queryObject.Status != null && queryObject.Status.Length > 0)
{
var statusSet = queryObject.Status.ToHashSet();
query = query.Where(i => statusSet.Contains(i.Status));
}
if (queryObject.Unusual != null)
{
var unused = queryObject.Unusual.Value;
query = query.Where(i => unused == (i.Status == "invalid" || i.ExceptionStatus != null));
}
if (queryObject.ExceptionStatus != null && queryObject.ExceptionStatus.Length > 0)
{
var exceptionStatusSet = queryObject.ExceptionStatus.Select(s => NormalizeExceptionStatus(s)).ToHashSet();
query = query.Where(i => exceptionStatusSet.Contains(i.ExceptionStatus));
}
query = query.OrderByDescending(q => q.Created);
if (queryObject.Skip != null)
query = query.Skip(queryObject.Skip.Value);
if (queryObject.Count != null)
query = query.Take(queryObject.Count.Value);
return query;
}
public async Task<int> GetInvoicesTotal(InvoiceQuery queryObject)
{
using (var context = _ContextFactory.CreateContext())
{
var query = GetInvoiceQuery(context, queryObject);
return await query.CountAsync();
}
}
public async Task<InvoiceEntity[]> GetInvoices(InvoiceQuery queryObject)
{
using (var context = _ContextFactory.CreateContext())
{
IQueryable<Data.InvoiceData> query = context
.Invoices
.Include(o => o.Payments)
var query = GetInvoiceQuery(context, queryObject);
query = query.Include(o => o.Payments)
.Include(o => o.RefundAddresses);
if (queryObject.IncludeAddresses)
query = query.Include(o => o.HistoricalAddressInvoices).Include(o => o.AddressInvoices);
if (queryObject.IncludeEvents)
query = query.Include(o => o.Events);
if (!string.IsNullOrEmpty(queryObject.InvoiceId))
{
query = query.Where(i => i.Id == queryObject.InvoiceId);
}
if (queryObject.StoreId != null && queryObject.StoreId.Length > 0)
{
var stores = queryObject.StoreId.ToHashSet();
query = query.Where(i => stores.Contains(i.StoreDataId));
}
if (queryObject.UserId != null)
{
query = query.Where(i => i.StoreData.UserStores.Any(u => u.ApplicationUserId == queryObject.UserId));
}
if (!string.IsNullOrEmpty(queryObject.TextSearch))
{
var ids = new HashSet<string>(SearchInvoice(queryObject.TextSearch));
if (ids.Count == 0)
return Array.Empty<InvoiceEntity>();
query = query.Where(i => ids.Contains(i.Id));
}
if (queryObject.StartDate != null)
query = query.Where(i => queryObject.StartDate.Value <= i.Created);
if (queryObject.EndDate != null)
query = query.Where(i => i.Created <= queryObject.EndDate.Value);
if (queryObject.OrderId != null && queryObject.OrderId.Length > 0)
{
var statusSet = queryObject.OrderId.ToHashSet();
query = query.Where(i => statusSet.Contains(i.OrderId));
}
if (queryObject.ItemCode != null && queryObject.ItemCode.Length > 0)
{
var statusSet = queryObject.ItemCode.ToHashSet();
query = query.Where(i => statusSet.Contains(i.ItemCode));
}
if (queryObject.Status != null && queryObject.Status.Length > 0)
{
var statusSet = queryObject.Status.ToHashSet();
query = query.Where(i => statusSet.Contains(i.Status));
}
if (queryObject.Unusual != null)
{
var unused = queryObject.Unusual.Value;
query = query.Where(i => unused == (i.Status == "invalid" || i.ExceptionStatus != null));
}
if (queryObject.ExceptionStatus != null && queryObject.ExceptionStatus.Length > 0)
{
var exceptionStatusSet = queryObject.ExceptionStatus.Select(s => NormalizeExceptionStatus(s)).ToHashSet();
query = query.Where(i => exceptionStatusSet.Contains(i.ExceptionStatus));
}
query = query.OrderByDescending(q => q.Created);
if (queryObject.Skip != null)
query = query.Skip(queryObject.Skip.Value);
if (queryObject.Count != null)
query = query.Take(queryObject.Count.Value);
var data = await query.ToArrayAsync().ConfigureAwait(false);
return data.Select(ToEntity).ToArray();
}
}
private string NormalizeExceptionStatus(string status)

View File

@ -1,48 +1,40 @@
using BTCPayServer.Logging;
using Microsoft.Extensions.Logging;
using Hangfire;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Mail;
using System.Threading.Tasks;
namespace BTCPayServer.Services.Mails
{
// This class is used by the application to send email for account confirmation and password reset.
// For more details see https://go.microsoft.com/fwlink/?LinkID=532713
public class EmailSender : IEmailSender
public abstract class EmailSender : IEmailSender
{
IBackgroundJobClient _JobClient;
SettingsRepository _Repository;
public EmailSender(IBackgroundJobClient jobClient, SettingsRepository repository)
public EmailSender(IBackgroundJobClient jobClient)
{
if (jobClient == null)
throw new ArgumentNullException(nameof(jobClient));
_JobClient = jobClient;
_Repository = repository;
}
public async Task SendEmailAsync(string email, string subject, string message)
{
var settings = await _Repository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
if (!settings.IsComplete())
{
Logs.Configuration.LogWarning("Should have sent email, but email settings are not configured");
return;
}
_JobClient.Schedule(() => SendMailCore(email, subject, message), TimeSpan.Zero);
return;
_JobClient = jobClient ?? throw new ArgumentNullException(nameof(jobClient));
}
public async Task SendMailCore(string email, string subject, string message)
public void SendEmail(string email, string subject, string message)
{
var settings = await _Repository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
if (!settings.IsComplete())
throw new InvalidOperationException("Email settings not configured");
var smtp = settings.CreateSmtpClient();
MailMessage mail = new MailMessage(settings.From, email, subject, message);
mail.IsBodyHtml = true;
await smtp.SendMailAsync(mail);
_JobClient.Schedule(async () =>
{
var emailSettings = await GetEmailSettings();
if (emailSettings?.IsComplete() != true)
{
Logs.Configuration.LogWarning("Should have sent email, but email settings are not configured");
return;
}
var smtp = emailSettings.CreateSmtpClient();
var mail = new MailMessage(emailSettings.From, email, subject, message)
{
IsBodyHtml = true
};
await smtp.SendMailAsync(mail);
}, TimeSpan.Zero);
}
public abstract Task<EmailSettings> GetEmailSettings();
}
}

View File

@ -0,0 +1,31 @@
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
using BTCPayServer.Services.Stores;
namespace BTCPayServer.Services.Mails
{
public class EmailSenderFactory
{
private readonly IBackgroundJobClient _JobClient;
private readonly SettingsRepository _Repository;
private readonly StoreRepository _StoreRepository;
public EmailSenderFactory(IBackgroundJobClient jobClient,
SettingsRepository repository,
StoreRepository storeRepository)
{
_JobClient = jobClient;
_Repository = repository;
_StoreRepository = storeRepository;
}
public IEmailSender GetEmailSender(string storeId = null)
{
var serverSender = new ServerEmailSender(_Repository, _JobClient);
if (string.IsNullOrEmpty(storeId))
return serverSender;
return new StoreEmailSender(_StoreRepository, serverSender, _JobClient, storeId);
}
}
}

View File

@ -7,6 +7,6 @@ namespace BTCPayServer.Services.Mails
{
public interface IEmailSender
{
Task SendEmailAsync(string email, string subject, string message);
void SendEmail(string email, string subject, string message);
}
}

View File

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Services.Mails
{
class ServerEmailSender : EmailSender
{
public ServerEmailSender(SettingsRepository settingsRepository,
IBackgroundJobClient backgroundJobClient) : base(backgroundJobClient)
{
if (settingsRepository == null)
throw new ArgumentNullException(nameof(settingsRepository));
SettingsRepository = settingsRepository;
}
public SettingsRepository SettingsRepository { get; }
public override Task<EmailSettings> GetEmailSettings()
{
return SettingsRepository.GetSettingAsync<EmailSettings>();
}
}
}

View File

@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Stores;
namespace BTCPayServer.Services.Mails
{
class StoreEmailSender : EmailSender
{
public StoreEmailSender(StoreRepository storeRepository,
EmailSender fallback,
IBackgroundJobClient backgroundJobClient,
string storeId) : base(backgroundJobClient)
{
if (storeId == null)
throw new ArgumentNullException(nameof(storeId));
StoreRepository = storeRepository;
FallbackSender = fallback;
StoreId = storeId;
}
public StoreRepository StoreRepository { get; }
public EmailSender FallbackSender { get; }
public string StoreId { get; }
public override async Task<EmailSettings> GetEmailSettings()
{
var store = await StoreRepository.FindStore(StoreId);
var emailSettings = store.GetStoreBlob().EmailSettings;
if (emailSettings?.IsComplete() == true)
{
return emailSettings;
}
return await FallbackSender.GetEmailSettings();
}
}
}

View File

@ -104,7 +104,8 @@ namespace BTCPayServer.Services.Rates
Providers.Add("hitbtc", new ExchangeSharpRateProvider("hitbtc", new ExchangeHitbtcAPI(), false));
// Cryptopia is often not available
Providers.Add("cryptopia", new ExchangeSharpRateProvider("cryptopia", new ExchangeCryptopiaAPI(), false));
// Disabled because of https://twitter.com/Cryptopia_NZ/status/1085084168852291586
// Providers.Add("cryptopia", new ExchangeSharpRateProvider("cryptopia", new ExchangeCryptopiaAPI(), false));
// Handmade providers
Providers.Add(QuadrigacxRateProvider.QuadrigacxName, new QuadrigacxRateProvider());

View File

@ -162,6 +162,8 @@ namespace BTCPayServer.Services.Stores
{
if (string.IsNullOrEmpty(name))
throw new ArgumentException("name should not be empty", nameof(name));
if (ownerId == null)
throw new ArgumentNullException(nameof(ownerId));
using (var ctx = _ContextFactory.CreateContext())
{
StoreData store = new StoreData

View File

@ -0,0 +1,244 @@
@using BTCPayServer.Crowdfund
@using BTCPayServer.Hubs
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
@model UpdateCrowdfundViewModel
@{
ViewData["Title"] = "Update Crowdfund";
}
<section>
<div class="modal" id="product-modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Contribution Perks Management</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="js-product-save btn btn-primary">Save Changes</button>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-lg-12 text-center">
<h2 class="section-heading">@ViewData["Title"]</h2>
<hr class="primary">
</div>
</div>
<div class="row">
<div class="col-lg-12 text-center">
<partial name="_StatusMessage" for="@TempData["TempDataProperty-StatusMessage"]" />
</div>
</div>
<div class="row">
<div class="col-lg-12">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Title" class="control-label"></label>*
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Tagline" class="control-label"></label>
<input asp-for="Tagline" class="form-control" />
<span asp-validation-for="Tagline" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Description" class="control-label"></label>
<textarea asp-for="Description" rows="20" cols="40" class="form-control richtext"></textarea>
<span asp-validation-for="Description" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="TargetCurrency" class="control-label"></label>
<input asp-for="TargetCurrency" class="form-control" />
<span asp-validation-for="TargetCurrency" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="TargetAmount" class="control-label"></label>
<input asp-for="TargetAmount" class="form-control" />
<span asp-validation-for="TargetAmount" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="StartDate" class="control-label"></label>
<input asp-for="StartDate" class="form-control datetime " />
<span asp-validation-for="StartDate" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ResetEvery" class="control-label"></label>
<div class="input-group">
<input type="number" asp-for="ResetEveryAmount" placeholder="Amount" class="form-control">
<select class="custom-select" asp-for="ResetEvery">
@foreach (var opt in Model.ResetEveryValues)
{
<option value="@opt">@opt</option>
}
</select>
</div>
</div>
<div class="form-group">
<label asp-for="EndDate" class="control-label"></label>
<input asp-for="EndDate" class="form-control datetime" />
<span asp-validation-for="EndDate" class="text-danger"></span>
</div>
<div class="form-group">
<label class="control-label">Contribution Perks</label>
<div class="mb-3">
<a class="js-product-add btn btn-secondary" href="#" data-toggle="modal" data-target="#product-modal"><i class="fa fa-plus fa-fw"></i> Add Perk</a>
</div>
<div class="js-products bg-light row p-3">
</div>
</div>
<div class="form-group">
<label asp-for="PerksTemplate" class="control-label"></label>
<textarea asp-for="PerksTemplate" rows="10" cols="40" class="js-product-template form-control"></textarea>
<span asp-validation-for="PerksTemplate" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="CustomCSSLink" class="control-label"></label>
<a href="https://docs.btcpayserver.org/development/theme#bootstrap-themes" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
<input asp-for="CustomCSSLink" class="form-control" />
<span asp-validation-for="CustomCSSLink" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="MainImageUrl" class="control-label"></label>
<input asp-for="MainImageUrl" class="form-control" />
<span asp-validation-for="MainImageUrl" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="EmbeddedCSS" class="control-label"></label>
<textarea asp-for="EmbeddedCSS" rows="10" cols="40" class="form-control"></textarea>
<span asp-validation-for="EmbeddedCSS" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="NotificationUrl" class="control-label"></label>
<input asp-for="NotificationUrl" class="form-control" />
<span asp-validation-for="NotificationUrl" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Enabled"></label>
<input asp-for="Enabled" type="checkbox" class="form-check"/>
<span asp-validation-for="Enabled" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="SortPerksByPopularity"></label>
<input asp-for="SortPerksByPopularity" type="checkbox" class="form-check"/>
<span asp-validation-for="SortPerksByPopularity" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="DisplayPerksRanking"></label>
<input asp-for="DisplayPerksRanking" type="checkbox" class="form-check"/>
<span asp-validation-for="DisplayPerksRanking" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="EnforceTargetAmount"></label>
<input asp-for="EnforceTargetAmount" type="checkbox" class="form-check"/>
<span asp-validation-for="EnforceTargetAmount" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="UseInvoiceAmount"></label>
<input asp-for="UseInvoiceAmount" type="checkbox" class="form-check"/>
<span asp-validation-for="UseInvoiceAmount" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="UseAllStoreInvoices"></label>
<input asp-for="UseAllStoreInvoices" type="checkbox" class="form-check"/>
<span asp-validation-for="UseAllStoreInvoices" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="SoundsEnabled"></label>
<input asp-for="SoundsEnabled" type="checkbox" class="form-check"/>
<span asp-validation-for="SoundsEnabled" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="AnimationsEnabled"></label>
<input asp-for="AnimationsEnabled" type="checkbox" class="form-check"/>
<span asp-validation-for="AnimationsEnabled" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="DisqusEnabled"></label>
<input asp-for="DisqusEnabled" type="checkbox" class="form-check"/>
<span asp-validation-for="DisqusEnabled" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="DisqusShortname" class="control-label"></label>
<input asp-for="DisqusShortname" class="form-control" />
<span asp-validation-for="DisqusShortname" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" class="btn btn-primary" value="Save Settings" />
<a class="btn btn-secondary" target="_blank" asp-action="ListInvoices" asp-controller="Invoice" asp-route-searchterm="@($"orderid:{CrowdfundHubStreamer.CrowdfundInvoiceOrderIdPrefix}{Model.AppId}")">Invoices generated by app</a>
<a class="btn btn-secondary" target="_blank" asp-action="ViewCrowdfund" asp-controller="AppsPublic" asp-route-appId="@Model.AppId">View App</a>
<a class="btn btn-secondary" target="_blank" asp-action="ListApps">Back to the app list</a>
</div>
</form>
</div>
</div>
</div>
</section>
@section Scripts {
<bundle name="wwwroot/bundles/crowdfund-admin-bundle.min.js"></bundle>
<bundle name="wwwroot/bundles/crowdfund-admin-bundle.min.css"></bundle>
<script id="template-product-item" type="text/template">
<div class="col-sm-4 col-md-3 mb-3">
<div class="card">
{image}
<div class="card-body">
<h6 class="card-title">{title}</h6>
<a href="#" class="js-product-edit btn btn-primary" data-toggle="modal" data-target="#product-modal">Edit</a>
<a href="#" class="js-product-remove btn btn-danger"><i class="fa fa-trash"></i></a>
</div>
</div>
</div>
</script>
<script id="template-product-content" type="text/template">
<div class="mb-3">
<input class="js-product-id" type="hidden" name="id" value="{id}">
<input class="js-product-index" type="hidden" name="index" value="{index}">
<div class="form-row">
<div class="col-sm-6">
<label>Title</label>*
<input type="text" class="js-product-title form-control mb-2" value="{title}" autofocus />
</div>
<div class="col-sm-3">
<label>Price</label>*
<input type="number" step="any" class="js-product-price form-control mb-2" value="{price}" />
</div>
<div class="col-sm-3">
<label>Custom price</label>
<select class="js-product-custom form-control">
{custom}
</select>
</div>
</div>
<div class="form-row">
<div class="col">
<label>Image</label>
<input type="text" class="js-product-image form-control mb-2" value="{image}" />
</div>
</div>
<div class="form-row">
<div class="col">
<label>Description</label>
<textarea rows="3" cols="40" class="js-product-description form-control mb-2">{description}</textarea>
</div>
</div>
</div>
</script>
}

View File

@ -159,7 +159,7 @@
</div>
<div class="col-sm-3">
<label>Price</label>*
<input type="text" class="js-product-price form-control mb-2" value="{price}" />
<input type="number" class="js-product-price form-control mb-2" value="{price}" />
</div>
<div class="col-sm-3">
<label>Custom price</label>

View File

@ -0,0 +1,78 @@
@using Microsoft.EntityFrameworkCore.Internal
@model BTCPayServer.Models.AppViewModels.ContributeToCrowdfund
<form method="post">
@foreach (var item in Model.ViewCrowdfundViewModel.Perks)
{
<div class="card mb-4 perk expanded" id="@item.Id">
@if (Model.ViewCrowdfundViewModel.DisplayPerksRanking && Model.ViewCrowdfundViewModel.PerkCount.ContainsKey(item.Id))
{
<span class="btn btn-sm rounded-circle px-0 btn-primary perk-badge">#@(Model.ViewCrowdfundViewModel.Perks.IndexOf(item)+1)</span>
}
@if (!string.IsNullOrEmpty(item.Image))
{
<img class="card-img-top" src="@item.Image"/>
}
<div class="card-body">
<div class="card-title d-flex justify-content-between">
<label class="h5">
@if (Model.ViewCrowdfundViewModel.Started && !Model.ViewCrowdfundViewModel.Ended && (item.Price.Value > 0 || item.Custom))
{
<input type="radio" asp-for="ChoiceKey" value="@item.Id"/>
}
@(string.IsNullOrEmpty(item.Title) ? item.Id : item.Title)
</label>
<span class="text-muted">
@if (item.Price.Value > 0)
{
@item.Price.Value
if (item.Custom)
{
Html.Raw("or more");
}
}
else if (item.Custom)
{
Html.Raw("Any amount");
}
</span>
</div>
<p class="card-text overflow-hidden">@Html.Raw(item.Description)</p>
</div>
@if (Model.ViewCrowdfundViewModel.PerkCount.ContainsKey(item.Id))
{
<div class="card-footer d-flex justify-content-between">
<span></span>
<span> @Model.ViewCrowdfundViewModel.PerkCount[item.Id] Contributors</span>
</div>
}
</div>
}
@if (Model.ViewCrowdfundViewModel.Started && !Model.ViewCrowdfundViewModel.Ended)
{
<div class="form-group">
<label asp-for="Email"></label>
<input asp-for="Email" type="email" class="form-control"/>
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Amount"></label>
<div class="input-group mb-3">
<input asp-for="Amount" type="number" step="any" class="form-control"/>
<div class="input-group-append">
<span class="input-group-text">@Model.ViewCrowdfundViewModel.TargetCurrency.ToUpperInvariant()</span>
</div>
</div>
<span asp-validation-for="Amount" class="text-danger"></span>
</div>
<input type="hidden" asp-for="RedirectToCheckout"/>
<button type="submit" class="btn btn-primary" >Contribute</button>
}
</form>

View File

@ -0,0 +1,154 @@
@using BTCPayServer.Models.AppViewModels
@using Microsoft.CodeAnalysis.CSharp.Syntax
@model BTCPayServer.Models.AppViewModels.ViewCrowdfundViewModel
<div class="container p-0">
<div class="row h-100 w-100 py-sm-0 py-md-4 mx-0">
<div class="card w-100 p-0 mx-0">
@if (!string.IsNullOrEmpty(Model.MainImageUrl))
{
<img class="card-img-top" src="@Model.MainImageUrl"/>
}
<div class="d-flex justify-content-between px-2">
<h1>
@Model.Title
@if (!Model.Started && Model.StartDate.HasValue)
{
<span class="h6 text-muted" >
Starts @Model.StartDate.Value.Subtract(DateTime.Now.ToUniversalTime())
</span>
}
else if (Model.Started && !Model.Ended && Model.EndDate.HasValue)
{
<span class="h6 text-muted" >
Ends @Model.EndDate.Value.Subtract(DateTime.Now.ToUniversalTime())
</span>
}
else if (Model.Started && !Model.Ended && !Model.EndDate.HasValue)
{
<span class="h6 text-muted" :title="startDate" title="No set end date">
Currently Active!
</span>
}
</h1>
@if (Model.TargetAmount.HasValue)
{
<span class="mt-3">
<span class="h5">@Model.TargetAmount @Model.TargetCurrency</span>
@if (Model.ResetEveryAmount > 0 && Model.ResetEvery != nameof(CrowdfundResetEvery.Never))
{
<span> Dynamic</span>
}
@if (Model.EnforceTargetAmount)
{
<span >Hardcap Goal <span class="fa fa-question-circle" ></span></span>
}
else
{
<span >Softcap Goal <span class="fa fa-question-circle" ></span> </span>
}
</span>
}
</div>
@if (Model.TargetAmount.HasValue)
{
<div class="progress w-100 rounded-0 " >
<div class="progress-bar" role="progressbar"
style="width:@(Model.Info.ProgressPercentage + "%")"
aria-valuemax="100">
</div>
<div class="progress-bar bg-warning" role="progressbar"
style="width:@(Model.Info.PendingProgressPercentage + "%")"
aria-valuemin="0"
aria-valuemax="100">
</div>
</div>
}
<div class="card-body">
<div class="row py-2 text-center">
<div class="col-sm border-right" id="raised-amount">
<h5>@(Model.Info.CurrentAmount + Model.Info.CurrentPendingAmount) @Model.TargetCurrency </h5>
<h5 class="text-muted">Raised</h5>
</div>
<div class="col-sm border-right">
<h5>@(Model.Info.PendingProgressPercentage.GetValueOrDefault(0) + Model.Info.ProgressPercentage.GetValueOrDefault(0))%</h5>
<h5 class="text-muted">Of Goal</h5>
</div>
<div class="col-sm text-right">
<h5>
@Model.Info.TotalContributors
</h5>
<h5 class="text-muted">Contributors</h5>
</div>
@if (Model.Started && !Model.Ended)
{
<div class="col-sm">
<h5>
@TimeZoneInfo.ConvertTimeFromUtc(Model.EndDate.Value, TimeZoneInfo.Local)
</h5>
<h5 class="text-muted">Ends</h5>
</div>
} else if (!Model.Started)
{
<div class="col-sm">
<h5>
@TimeZoneInfo.ConvertTimeFromUtc(Model.StartDate.Value, TimeZoneInfo.Local)
</h5>
<h5 class="text-muted">Starts</h5>
</div>
}else if (Model.Ended)
{
<div class="col-sm" id="inactive-campaign">
<h5>
Campaign
</h5>
<h5 >not active</h5>
</div>
}
</div>
<div class="card-title">
<div class="row">
<div class="col-sm-12 ">
<h2 class="text-muted" >@Model.Tagline</h2>
</div>
</div>
</div>
<hr/>
<div class="row">
<div class="col-md-8 col-sm-12">
<div class="card-text overflow-hidden">@Html.Raw(Model.Description)</div>
</div>
<div class="col-md-4 col-sm-12">
<partial
name="Crowdfund/ContributeForm"
model="@(new ContributeToCrowdfund()
{
ViewCrowdfundViewModel = Model,
RedirectToCheckout = true,
})"></partial>
</div>
</div>
</div>
<div class="card-footer text-muted d-flex" >
<div class="align-self-end pr-4">Updated @Model.Info.LastUpdated</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,319 @@
<div class="container p-0" id="app" v-cloak>
<div class="row h-100 w-100 py-sm-0 py-md-4 mx-0">
<div class="card w-100 p-0 mx-0">
<img class="card-img-top" :src="srvModel.mainImageUrl" v-if="srvModel.mainImageUrl" id="crowdfund-main-image">
<div class="d-flex justify-content-between px-2" id="crowdfund-header-container">
<h1>
{{srvModel.title}}
<span class="h6 text-muted" v-if="!started && srvModel.startDate" v-b-tooltip :title="startDate" id="crowdfund-header-start-date">
Starts {{startDateRelativeTime}}
</span>
<span class="h6 text-muted" v-else-if="started && !ended && srvModel.endDate" v-b-tooltip :title="endDate" id="crowdfund-header-end-date">
Ends {{endDateRelativeTime}}
</span>
<span class="h6 text-muted" v-else-if="started && !ended && !srvModel.endDate" v-b-tooltip title="No set end date" id="crowdfund-header-active">
Currently Active!
</span>
</h1>
<span v-if="srvModel.targetAmount" class="mt-3" id="crowdfund-header-target">
<span class="h5" id="crowdfund-header-target-amount">{{srvModel.targetAmount}} {{targetCurrency}}</span>
<span v-if="srvModel.resetEvery !== 'Never'"
id="crowdfund-header-target-dynamic"
v-b-tooltip
:title="'Goal resets every ' + srvModel.resetEveryAmount + ' ' + srvModel.resetEvery + ((srvModel.resetEveryAmount>1)?'s': '')" >Dynamic </span>
<span v-if="srvModel.enforceTargetAmount"
id="crowdfund-header-target-softcap">Hardcap Goal <span class="fa fa-question-circle" v-b-tooltip title="No contributions allowed after the goal has been reached"></span></span>
<span v-else
id="crowdfund-header-target-hardcap">Softcap Goal <span class="fa fa-question-circle" v-b-tooltip title="Contributions allowed even after goal is reached"></span> </span>
</span>
</div>
<div class="progress w-100 rounded-0 " v-if="srvModel.targetAmount"
id="crowdfund-progress-bar">
<div class="progress-bar" role="progressbar"
:aria-valuenow="srvModel.info.progressPercentage"
v-bind:style="{ width: srvModel.info.progressPercentage + '%' }"
aria-valuemin="0"
id="crowdfund-progress-bar-confirmed-bar"
v-b-tooltip :title="parseFloat(srvModel.info.progressPercentage).toFixed(2) + '% contributions'"
aria-valuemax="100">
</div>
<div class="progress-bar bg-warning" role="progressbar"
id="crowdfund-progress-bar-pending-bar"
:aria-valuenow="srvModel.info.pendingProgressPercentage"
v-bind:style="{ width: srvModel.info.pendingProgressPercentage + '%' }"
v-b-tooltip :title="parseFloat(srvModel.info.pendingProgressPercentage).toFixed(2) + '% contributions pending confirmation'"
aria-valuemin="0"
aria-valuemax="100">
</div>
</div>
<div class="card-body">
<div class="row py-2 text-center">
<div class="col-sm border-right" id="crowdfund-body-raised-amount">
<h5>{{ raisedAmount }} {{targetCurrency}} </h5>
<h5 class="text-muted">Raised</h5>
</div>
<div class="col-sm border-right" id="crowdfund-body-goal-raised">
<h5>{{ percentageRaisedAmount }}%</h5>
<h5 class="text-muted">Of Goal</h5>
</div>
<div class="col-sm border-right" id="crowdfund-body-total-contributors">
<h5>
{{srvModel.info.totalContributors}}
</h5>
<h5 class="text-muted">Contributors</h5>
</div>
<div class="col-sm" v-if="endDiff" id="crowdfund-body-campaign-dates-started">
<h5>
{{endDiff}}
</h5>
<h5 class="text-muted">Left</h5>
<b-tooltip target="crowdfund-body-campaign-dates-started" >
<ul class="p-0">
<li v-if="startDate" class="list-unstyled">
{{started? "Started" : "Starts"}} {{startDate}}
</li>
<li v-if="endDate" class="list-unstyled">
{{ended? "Ended" : "Ends"}} {{endDate}}
</li>
</ul>
</b-tooltip>
</div>
<div class="col-sm" v-if="startDiff" id="crowdfund-body-campaign-dates-not-started">
<h5>
{{startDiff}}
</h5>
<h5 class="text-muted">Left to start</h5>
<b-tooltip target="crowdfund-body-campaign-dates-not-started" >
<ul class="p-0">
<li v-if="startDate" class="list-unstyled">
{{started? "Started" : "Starts"}} {{startDate}}
</li>
<li v-if="endDate" class="list-unstyled">
{{ended? "Ended" : "Ends"}} {{endDate}}
</li>
</ul>
</b-tooltip>
</div>
<div class="col-sm" v-if="ended" id="crowdfund-body-campaign-dates-not-active">
<h5>
Campaign
</h5>
<h5 >not active</h5>
<b-tooltip target="crowdfund-body-campaign-dates-not-active" >
<ul class="p-0">
<li v-if="startDate" class="list-unstyled">
{{started? "Started" : "Starts"}} {{startDate}}
</li>
<li v-if="endDate" class="list-unstyled">
{{ended? "Ended" : "Ends"}} {{endDate}}
</li>
</ul>
</b-tooltip>
</div>
</div>
<b-tooltip target="crowdfund-body-raised-amount" v-if="paymentStats && paymentStats.length > 0">
<ul class="p-0 text-uppercase">
<li v-for="stat of paymentStats" class="list-unstyled">
{{stat.label}} <span v-if="stat.lightning" class="fa fa-bolt"></span> {{stat.value}}
</li>
</ul>
</b-tooltip>
<b-tooltip target="crowdfund-body-goal-raised" v-if="srvModel.resetEvery !== 'Never'">
Goal resets every {{srvModel.resetEveryAmount}} {{srvModel.resetEvery}} {{srvModel.resetEveryAmount>1?'s': ''}}
</b-tooltip>
<div class="card-title" id="crowdfund-body-header">
<div class="row">
<div class="col-sm-12 col-md-8 col-lg-9" id="crowdfund-body-header-tagline-container">
<h2 class="text-muted" v-if="srvModel.tagline" id="crowdfund-body-header-tagline">{{srvModel.tagline}}</h2>
</div>
<div class="col-sm-12 col-md-4 col-lg-3" id="crowdfund-body-header-cta-container">
<button v-if="active" id="crowdfund-body-header-cta" class="btn btn-lg btn-primary w-100 font-weight-bold" v-on:click="contributeModalOpen = true">Contribute</button>
</div>
</div>
</div>
<template v-if="srvModel.disqusEnabled">
<b-tabs>
<b-tab title="Details"active>
<div class="row mt-2">
<div class="col-md-8 col-sm-12" id="crowdfund-body-description-container">
<div class="card-text overflow-hidden" v-html="srvModel.description" id="crowdfund-body-description"></div>
</div>
<div class="col-md-4 col-sm-12" id="crowdfund-body-contribution-container">
<contribute :target-currency="srvModel.targetCurrency"
:display-perks-ranking="srvModel.displayPerksRanking"
:active="active"
:loading="loading"
:in-modal="false"
:perks="perks">
</contribute>
</div>
</div>
</b-tab>
<b-tab title="Discussion" >
<div id="disqus_thread" class=" mt-2"></div>
</b-tab>
</b-tabs>
</template>
<template v-else>
<hr/>
<div class="row mt-2">
<div class="col-md-8 col-sm-12" id="crowdfund-body-description-container">
<div class="card-text overflow-hidden" v-html="srvModel.description" id="crowdfund-body-description"></div>
</div>
<div class="col-md-4 col-sm-12" id="crowdfund-body-contribution-container">
<contribute :target-currency="srvModel.targetCurrency"
:loading="loading"
:display-perks-ranking="srvModel.displayPerksRanking"
:active="active"
:in-modal="false"
:perks="perks">
</contribute>
</div>
</div>
</template>
</div>
<div class="card-footer text-muted d-flex" v-if="srvModel.animationsEnabled || srvModel.soundsEnabled">
<div class="align-self-end pr-4">Updated {{lastUpdated}}</div>
<div class="form-check mx-1" v-if="srvModel.animationsEnabled || animation">
<input class="form-check-input" type="checkbox" id="cbAnime" v-model="animation">
<label class="form-check-label" for="cbAnime">
Animations
</label>
</div>
<div class="form-check mx-1" v-if="srvModel.soundsEnabled|| sound">
<input class="form-check-input" type="checkbox" id="cbSounds" v-model="sound">
<label class="form-check-label" for="cbSounds">
Sounds
</label>
</div>
</div>
</div>
</div>
<b-modal title="Contribute" v-model="contributeModalOpen" size="lg" ok-only="true" ok-variant="secondary" ok-title="Close" ref="modalContribute">
<contribute v-if="contributeModalOpen"
:target-currency="srvModel.targetCurrency"
:active="active"
:perks="srvModel.perks"
:loading="loading"
:in-modal="true">
</contribute>
</b-modal>
</div>
<script type="text/x-template" id="perks-template">
<div class="perks-container">
<perk v-if="!perks || perks.length ===0"
:perk="{title: 'Donate Custom Amount', price: { value: null}, custom: true}"
:target-currency="targetCurrency"
:active="active"
:loading="loading"
:in-modal="inModal">
</perk>
<perk v-for="(perk, index) in perks" :perk="perk" :key="perk.id"
:target-currency="targetCurrency"
:active="active"
:display-perks-ranking="displayPerksRanking"
:index="index"
:loading="loading"
:in-modal="inModal">
</perk>
</div>
</script>
<script type="text/x-template" id="perk-template">
<div class="card mb-4 perk" v-bind:class="{ 'expanded': expanded, 'unexpanded': !expanded }" v-on:click="expand" :id="perk.id">
<span v-if="displayPerksRanking && perk.sold"
class="btn btn-sm rounded-circle px-0 perk-badge"
v-bind:class="{ 'btn-primary': index==0, 'btn-secondary': index!=0}">#{{index+1}}</span>
<div class="perk-zoom " v-if="canExpand">
<div class="perk-zoom-bg bg-primary"> </div>
<div class="perk-zoom-text w-100 text-center text-white font-weight-bold">
Select this contribution perk
</div>
</div>
<form v-on:submit='onContributeFormSubmit'>
<input type="hidden" :value="perk.id" id="choiceKey"/>
<img v-if="perk.image && perk.image != 'null' " class="card-img-top" :src="perk.image" />
<div class="card-body">
<div class="card-title d-flex justify-content-between" >
<span class="h5">{{perk.title? perk.title : perk.id}} </span>
<span class="text-muted" >
<template v-if="perk.price.value">{{perk.price.value.noExponents()}}
{{targetCurrency}}
<template v-if="perk.custom">or more</template>
</template>
<template v-else-if="!perk.price.value && perk.custom">
Any amount
</template>
</span>
</div>
<p class="card-text overflow-hidden" v-if="perk.description" v-html="perk.description"></p>
<div class="input-group" style="max-width: 500px;" v-if="expanded" :id="'perk-form'+ perk.id">
<input
:disabled="!active"
:readonly="!perk.custom"
class="form-control"
type="number"
v-model="amount"
:min="perk.price.value"
step="any"
placeholder="Contribution Amount"
required >
<div class="input-group-append">
<span class='input-group-text'>{{targetCurrency}}</span>
<button
class="btn btn-primary"
v-bind:class="{ 'btn-disabled': loading}"
:disabled="!active || loading"
type="submit">
<div v-if="loading" class="spinner-grow spinner-grow-sm" role="status">
<span class="sr-only">Loading...</span>
</div>
Continue
</button>
</div>
</div>
</div>
<div class="card-footer d-flex justify-content-between" v-if="perk.sold">
<span ></span>
<span x >{{perk.sold}} Contributor{{perk.sold > 1? "s": ""}}</span>
</div>
</form>
</div>
</script>
<script type="text/x-template" id="contribute-template">
<div>
<h3 v-if="!inModal" class="mb-3">Contribute</h3>
<perks
:perks="perks"
:loading="loading"
:in-modal="inModal"
:display-perks-ranking="displayPerksRanking"
:target-currency="targetCurrency"
:active="active">
</perks>
</div>
</script>

View File

@ -0,0 +1,58 @@
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
@model BTCPayServer.Models.AppViewModels.ViewCrowdfundViewModel
@{
ViewData["Title"] = Model.Title;
Layout = null;
}
<!DOCTYPE html>
<html class="h-100">
<head>
<title>@Model.Title</title>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes">
<link href="@Context.Request.GetAbsoluteUri(themeManager.BootstrapUri)" rel="stylesheet"/>
@if (Model.CustomCSSLink != null)
{
<link href="@Model.CustomCSSLink" rel="stylesheet"/>
}
@if (!Context.Request.Query.ContainsKey("simple"))
{
<script type="text/javascript">
var srvModel = @Html.Raw(Json.Serialize(Model));
</script>
<bundle name="wwwroot/bundles/crowdfund-bundle-1.min.js"></bundle>
<bundle name="wwwroot/bundles/crowdfund-bundle-2.min.js"></bundle>
}
<bundle name="wwwroot/bundles/crowdfund-bundle.min.css"></bundle>
@if (!string.IsNullOrEmpty(Model.EmbeddedCSS))
{
<style>
@Html.Raw(Model.EmbeddedCSS);
</style>
}
</head>
<body>
@if (Context.Request.Query.ContainsKey("simple"))
{
@await Html.PartialAsync("Crowdfund/MinimalCrowdfund", Model)
}
else
{
<noscript>
@await Html.PartialAsync("Crowdfund/MinimalCrowdfund", Model)
</noscript>
if (Model.AnimationsEnabled)
{
<canvas id="fireworks"></canvas>
}
@await Html.PartialAsync("Crowdfund/VueCrowdfund", Model)
}
</body>
</html>

View File

@ -194,6 +194,7 @@
<div class="modal-footer bg-light">
<form method="post" asp-antiforgery="false" data-buy>
<input id="js-cart-amount" class="form-control" type="hidden" name="amount">
<input id="js-cart-posdata" class="form-control" type="hidden" name="posdata">
<button id="js-cart-pay" class="btn btn-primary btn-lg" type="submit"><b>@Model.CustomButtonText</b></button>
</form>
</div>

View File

@ -387,10 +387,10 @@
</div>
<div class="success-message">{{$t("This invoice has been paid")}}</div>
<a class="action-button" :href="srvModel.merchantRefLink" v-show="!isModal">
<span>{{$t("Return to StoreName", srvModel)}}</span>
<span v-html="$t('Return to StoreName', srvModel)"></span>
</a>
<button class="action-button close-action" v-show="isModal">
<span>{{$t("Return to StoreName", srvModel)}}</span>
<span v-html="$t('Close')">{{$t("Return to StoreName", srvModel)}}</span>
</button>
</div>
</div>

View File

@ -157,6 +157,10 @@
<th>Price</th>
<td>@Model.ProductInformation.Price @Model.ProductInformation.Currency</td>
</tr>
<tr>
<th>Tax included</th>
<td>@Model.ProductInformation.TaxIncluded @Model.ProductInformation.Currency</td>
</tr>
</table>
}
</div>
@ -180,28 +184,15 @@
<th>Price</th>
<td>@Model.ProductInformation.Price @Model.ProductInformation.Currency</td>
</tr>
<tr>
<th>Tax included</th>
<td>@Model.ProductInformation.TaxIncluded @Model.ProductInformation.Currency</td>
</tr>
</table>
</div>
<div class="col-md-6">
<h3>Point of Sale Data</h3>
<table class="table table-sm table-responsive-md">
@foreach (var posDataItem in Model.PosData)
{
<tr>
@if (!string.IsNullOrEmpty(posDataItem.Key))
{
<th>@posDataItem.Key</th>
<td>@posDataItem.Value</td>
}
else
{
<td colspan="2">@posDataItem.Value</td>
}
</tr>
}
</table>
<partial name="PosData" model="@Model.PosData"></partial>
</div>
</div>
}

View File

@ -49,6 +49,7 @@
<a class="btn btn-primary dropdown-toggle" href="#" role="button" id="dropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Export
</a>
<a href="https://docs.btcpayserver.org/features/accounting" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
<div class="dropdown-menu" aria-labelledby="dropdownMenuLink">
<a asp-action="Export" asp-route-format="csv" asp-route-searchTerm="@Model.SearchTerm" class="dropdown-item" target="_blank">CSV</a>
<a asp-action="Export" asp-route-format="json" asp-route-searchTerm="@Model.SearchTerm" class="dropdown-item" target="_blank">JSON</a>
@ -141,23 +142,31 @@
}
</tbody>
</table>
<span>
@if (Model.Skip != 0)
{
<a href="@Url.Action("ListInvoices", new
<nav aria-label="...">
<ul class="pagination">
<li class="page-item @(Model.Skip == 0 ? "disabled" : null)">
<a class="page-link" tabindex="-1" href="@Url.Action("ListInvoices", new
{
searchTerm = Model.SearchTerm,
skip = Math.Max(0, Model.Skip - Model.Count),
count = Model.Count,
})"><<</a><span> - </span>
}
<a href="@Url.Action("ListInvoices", new
{
searchTerm = Model.SearchTerm,
skip = Model.Skip + Model.Count,
count = Model.Count,
})">>></a>
</span>
})">Previous</a>
</li>
<li class="page-item disabled">
<span class="page-link">@(Model.Skip + 1) to @(Model.Skip + Model.Invoices.Count) of @Model.Total</span>
</li>
<li class="page-item @(Model.Total > (Model.Skip + Model.Invoices.Count) ? null : "disabled")">
<a class="page-link" href="@Url.Action("ListInvoices", new
{
searchTerm = Model.SearchTerm,
skip = Model.Skip + Model.Count,
count = Model.Count,
})">Next</a>
</li>
</ul>
</nav>
</div>
</div>

View File

@ -0,0 +1,37 @@
@model Dictionary<string, object>
<table class="table table-sm table-responsive-md">
@foreach (var posDataItem in Model)
{
<tr>
@if (!string.IsNullOrEmpty(posDataItem.Key))
{
<th>@posDataItem.Key</th>
<td>
@if (posDataItem.Value is string)
{
@posDataItem.Value
}
else
{
<partial name="PosData" model="@posDataItem.Value"/>
}
</td>
}
else
{
<td colspan="2">
@if (posDataItem.Value is string)
{
@posDataItem.Value
}
else
{
<partial name="PosData" model="@posDataItem.Value"/>
}
</td>
}
</tr>
}
</table>

View File

@ -17,7 +17,7 @@
<div class="col-md-8">
<div class="form-group">
<p>
<span>SSH servies are used by the maintenance operations<br /></span>
<span>SSH services are used by the maintenance operations<br /></span>
</p>
</div>

View File

@ -7,6 +7,6 @@ namespace BTCPayServer.Views.Server
{
public enum ServerNavPages
{
Index, Users, Rates, Emails, Policies, Theme, Hangfire, Services, Maintenance, Logs
Index, Users, Rates, Emails, Policies, Theme, Services, Maintenance, Logs
}
}

View File

@ -7,6 +7,5 @@
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Theme)" asp-action="Theme">Theme</a>
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Maintenance)" asp-action="Maintenance">Maintenance</a>
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Logs)" asp-action="Logs">Logs</a>
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Hangfire)" href="~/hangfire" target="_blank">Hangfire</a>
</div>

View File

@ -0,0 +1,68 @@
@model BTCPayServer.Models.ServerViewModels.EmailsViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData.SetActivePageAndTitle(StoreNavPages.Index, "Update Store Email Settings");
}
<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="StatusMessage" />
<div class="row">
<div class="col-md-6">
<div asp-validation-summary="All" class="text-danger"></div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<form method="post">
<div class="form-group">
<label asp-for="Settings.Server"></label>
<input asp-for="Settings.Server" class="form-control" />
<span asp-validation-for="Settings.Server" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Settings.Port"></label>
<input asp-for="Settings.Port" class="form-control" />
<span asp-validation-for="Settings.Port" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Settings.From"></label>
<input asp-for="Settings.From" class="form-control" />
<span asp-validation-for="Settings.From" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Settings.Login"></label>
<input asp-for="Settings.Login" class="form-control" />
<span asp-validation-for="Settings.Login" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Settings.Password"></label>
<input asp-for="Settings.Password" type="password" class="form-control" />
<span asp-validation-for="Settings.Password" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Settings.EnableSSL"></label>
<input asp-for="Settings.EnableSSL" type="checkbox" class="form-check-inline" />
</div>
<div class="form-group">
<label asp-for="TestEmail"></label>
<input asp-for="TestEmail" class="form-control" />
<span asp-validation-for="TestEmail" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary" name="command" value="Save">Save</button>
<button type="submit" class="btn btn-primary" name="command" value="Test">Test</button>
</form>
</div>
</div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}

View File

@ -5,6 +5,13 @@
}
<partial name="_StatusMessage" for="StatusMessage" />
@if (Model.StoreNotConfigured)
{
<div class="alert alert-warning alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<span>Warning: No wallet has been linked to your BTCPay Store. See <a href="https://docs.btcpayserver.org/btcpay-basics/gettingstarted#connecting-btcpay-store-to-your-wallet" target="_blank">this link</a> for more information on how to connect your store and wallet.</span>
</div>
}
<h4>Access token</h4>
<div class="row">
<div class="col-md-8">
@ -23,7 +30,7 @@
</tr>
</thead>
<tbody>
@foreach(var token in Model.Tokens)
@foreach (var token in Model.Tokens)
{
<tr>
<td>@token.Label</td>

View File

@ -214,7 +214,29 @@
</table>
</div>
</div>
<div class="form-group">
<div class="form-group">
<h5>Services</h5>
</div>
<div class="form-group">
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Service</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td>
Email
</td>
<td style="text-align:right"><a asp-action="Emails" >Modify</a></td>
</tr>
</tbody>
</table>
</div>
</div>
@if(Model.CanDelete)
{
<div class="form-group">

View File

@ -33,7 +33,7 @@
<div class="form-group">
<label asp-for="Amount"></label>
<div class="input-group">
<input asp-for="Amount" class="form-control" onkeyup='updateFiatValue();' />
<input asp-for="Amount" asp-format="{0}" class="form-control" onkeyup='updateFiatValue();' />
<div class="input-group-prepend">
<span class="input-group-text text-muted" style="display:none;" id="fiatValue"></span>
</div>
@ -55,7 +55,23 @@
<label asp-for="SubstractFees"></label>
<input asp-for="SubstractFees" class="form-check" />
</div>
<button type="submit" class="btn btn-primary">Confirm</button>
@if (Model.AdvancedMode)
{
<div class="form-group">
<label asp-for="NoChange"></label>
<a href="https://docs.btcpayserver.org/features/wallet#make-sure-no-change-utxo-is-created-expert-mode" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
<input asp-for="NoChange" class="form-check" />
</div>
}
<button name="command" type="submit" class="btn btn-primary">Confirm</button>
@if (Model.AdvancedMode)
{
<button name="command" type="submit" value="noob" class="btn btn-secondary">Use noob mode</button>
}
else
{
<button name="command" type="submit" value="expert" class="btn btn-secondary">Use expert mode</button>
}
</form>
</div>
</div>

View File

@ -16,6 +16,7 @@
<input type="hidden" asp-for="Amount" />
<input type="hidden" asp-for="FeeSatoshiPerByte" />
<input type="hidden" asp-for="SubstractFees" />
<input type="hidden" asp-for="NoChange" />
<p>
You can send money received by this store to an address with the help of your Ledger Wallet. <br />
If you don't have a Ledger Wallet, use Electrum with your favorite hardware wallet to transfer crypto. <br />

View File

@ -29,7 +29,8 @@
<h4>@ViewData["Title"]</h4>
<div class="row">
<div class="col-md-10">
If this wallet got restored, should have received money but nothing is showing up, please <a asp-action="WalletRescan">Rescan it</a>.
If BTCPay Server shows you an invalid balance, <a asp-action="WalletRescan">rescan your wallet</a>. <br />
If some transactions appear in BTCPay Server, but are missing on Electrum or another wallet, <a href="https://docs.btcpayserver.org/faq-and-common-issues/faq-wallet#missing-payments-in-my-software-or-hardware-wallet">follow those instructions</a>.
</div>
</div>
<div class="row">

View File

@ -53,6 +53,7 @@
"wwwroot/checkout/**/*.js"
]
},
{
"outputFileName": "wwwroot/bundles/lightning-node-info-bundle.min.js",
"inputFiles": [
@ -71,5 +72,57 @@
"wwwroot/cart/js/cart.js",
"wwwroot/cart/js/cart.jquery.js"
]
},
{
"outputFileName": "wwwroot/bundles/crowdfund-bundle-1.min.js",
"inputFiles": [
"wwwroot/vendor/vuejs/vue.min.js",
"wwwroot/vendor/babel-polyfill/polyfill.min.js",
"wwwroot/vendor/vue-toasted/vue-toasted.min.js",
"wwwroot/vendor/bootstrap-vue/bootstrap-vue.js",
"wwwroot/vendor/signalr/signalr.js",
"wwwroot/vendor/animejs/anime.min.js",
"wwwroot/modal/btcpay.js",
"wwwroot/crowdfund/**/*.js"
]
},
{
"outputFileName": "wwwroot/bundles/crowdfund-bundle-2.min.js",
"inputFiles": [
"wwwroot/vendor/moment/moment.js"
],
"minify": {
"enabled": false
}
},
{
"outputFileName": "wwwroot/bundles/crowdfund-admin-bundle.min.js",
"inputFiles": [
"wwwroot/vendor/highlightjs/highlight.min.js",
"wwwroot/vendor/summernote/summernote-bs4.js",
"wwwroot/vendor/flatpickr/flatpickr.js",
"wwwroot/crowdfund-admin/**/*.js",
"wwwroot/products/js/products.js",
"wwwroot/products/js/products.jquery.js"
]
},
{
"outputFileName": "wwwroot/bundles/crowdfund-admin-bundle.min.css",
"inputFiles": [
"wwwroot/vendor/highlightjs/default.min.css",
"wwwroot/vendor/summernote/summernote-bs4.css",
"wwwroot/vendor/flatpickr/flatpickr.min.css",
"wwwroot/crowdfund-admin/**/*.js"
]
},
{
"outputFileName": "wwwroot/bundles/crowdfund-bundle.min.css",
"inputFiles": [
"wwwroot/vendor/font-awesome/css/font-awesome.min.css",
"wwwroot/vendor/bootstrap-vue/bootstrap-vue.css",
"wwwroot/crowdfund/**/*.css"
]
}
]

View File

@ -21,6 +21,7 @@ function Cart() {
this.updateItemsCount();
this.updateAmount();
this.updatePosData();
}
Cart.prototype.setCustomAmount = function(amount) {
@ -243,6 +244,7 @@ Cart.prototype.updateAll = function() {
this.updateSummaryTotal();
this.updateTotal();
this.updateAmount();
this.updatePosData();
}
// Update number of cart items
@ -290,6 +292,20 @@ Cart.prototype.updateTip = function(amount) {
Cart.prototype.updateAmount = function() {
$('#js-cart-amount').val(this.getTotal(true));
}
Cart.prototype.updatePosData = function() {
var result = {
cart: this.content,
customAmount: this.fromCents(this.getCustomAmount()),
discountPercentage: this.discount? parseFloat(this.discount): 0,
subTotal: this.fromCents(this.getTotalProducts()),
discountAmount: this.fromCents(this.getDiscountAmount(this.totalAmount)),
tip: this.tip? this.tip: 0,
total: this.getTotal(true)
};
console.warn(result);
$('#js-cart-posdata').val(JSON.stringify(result));
}
Cart.prototype.resetDiscount = function() {
this.setDiscount(0);
@ -644,6 +660,7 @@ $.fn.inputAmount = function(obj, type) {
obj.updateSummaryTotal();
obj.updateAmount();
obj.updatePosData();
obj.emptyCartToggle();
});
}
@ -668,4 +685,4 @@ $.fn.removeAmount = function(obj, type) {
obj.updateSummaryTotal();
obj.emptyCartToggle();
});
}
}

View File

@ -0,0 +1,8 @@
hljs.initHighlightingOnLoad();
$(document).ready(function() {
$(".richtext").summernote();
$(".datetime").flatpickr({
enableTime: true
});
});

View File

@ -0,0 +1,323 @@
var app = null;
var eventAggregator = new Vue();
function addLoadEvent(func) {
var oldonload = window.onload;
if (typeof window.onload != 'function') {
window.onload = func;
} else {
window.onload = function() {
if (oldonload) {
oldonload();
}
func();
}
}
}
addLoadEvent(function (ev) {
Vue.use(Toasted);
Vue.component('contribute', {
props: ["targetCurrency", "active", "perks", "inModal", "displayPerksRanking", "loading"],
template: "#contribute-template"
});
Vue.component('perks', {
props: ["perks", "targetCurrency", "active", "inModal","displayPerksRanking", "loading"],
template: "#perks-template"
});
Vue.component('perk', {
props: ["perk", "targetCurrency", "active", "inModal", "displayPerksRanking", "index", "loading"],
template: "#perk-template",
data: function () {
return {
amount: null,
expanded: false
}
},
computed: {
canExpand: function(){
return !this.expanded && this.active && (this.perk.price.value || this.perk.custom)
}
},
methods: {
onContributeFormSubmit: function (e) {
if (e) {
e.preventDefault();
}
if(!this.active || this.loading){
return;
}
eventAggregator.$emit("contribute", {amount: parseFloat(this.amount), choiceKey: this.perk.id});
},
expand: function(){
if(this.canExpand){
this.expanded = true;
}
},
setAmount: function (amount) {
this.amount = (amount || 0).noExponents();
this.expanded = false;
}
},
mounted: function () {
this.setAmount(this.perk.price.value);
},
watch: {
perk: function (newValue, oldValue) {
if (newValue.price.value != oldValue.price.value) {
this.setAmount(newValue.price.value);
}
}
}
});
app = new Vue({
el: '#app',
data: function(){
return {
srvModel: window.srvModel,
connectionStatus: "",
endDate: "",
startDate: "",
startDateRelativeTime: "",
endDateRelativeTime: "",
started: false,
ended: false,
contributeModalOpen: false,
endDiff: "",
startDiff: "",
active: true,
animation: true,
sound: true,
lastUpdated:"",
loading: false,
timeoutState: 0
}
},
computed: {
raisedAmount: function(){
return parseFloat(this.srvModel.info.currentAmount + this.srvModel.info.currentPendingAmount ).toFixed(this.srvModel.currencyData.divisibility) ;
},
percentageRaisedAmount: function(){
return parseFloat(this.srvModel.info.progressPercentage + this.srvModel.info.pendingProgressPercentage ).toFixed(2);
},
targetCurrency: function(){
return this.srvModel.targetCurrency.toUpperCase();
},
paymentStats: function(){
var result= [];
var combinedStats = {};
var keys = Object.keys(this.srvModel.info.paymentStats);
for (var i = 0; i < keys.length; i++) {
if(combinedStats[keys[i]]){
combinedStats[keys[i]] +=this.srvModel.info.paymentStats[keys[i]];
}else{
combinedStats[keys[i]] =this.srvModel.info.paymentStats[keys[i]];
}
}
keys = Object.keys(this.srvModel.info.pendingPaymentStats);
for (var i = 0; i < keys.length; i++) {
if(combinedStats[keys[i]]){
combinedStats[keys[i]] +=this.srvModel.info.pendingPaymentStats[keys[i]];
}else{
combinedStats[keys[i]] =this.srvModel.info.pendingPaymentStats[keys[i]];
}
}
keys = Object.keys(combinedStats);
for (var i = 0; i < keys.length; i++) {
var newItem = {key:keys[i], value: combinedStats[keys[i]], label: keys[i].replace("_","")};
result.push(newItem);
}
for (var i = 0; i < result.length; i++) {
var current = result[i];
if(current.label.endsWith("LightningLike")){
current.label = current.label.substr(0,current.label.indexOf("LightningLike"));
current.lightning = true;
}
}
return result;
},
perks: function(){
var result = [];
for (var i = 0; i < this.srvModel.perks.length; i++) {
var currentPerk = this.srvModel.perks[i];
if(this.srvModel.perkCount.hasOwnProperty(currentPerk.id)){
currentPerk.sold = this.srvModel.perkCount[currentPerk.id];
}
result.push(currentPerk);
}
return result;
}
},
methods: {
updateComputed: function () {
if (this.srvModel.endDate) {
var endDateM = moment(this.srvModel.endDate);
this.endDate = endDateM.format('MMMM Do YYYY');
this.endDateRelativeTime = endDateM.fromNow();
this.ended = endDateM.isBefore(moment());
}else{
this.ended = false;
}
if (this.srvModel.startDate) {
var startDateM = moment(this.srvModel.startDate);
this.startDate = startDateM.format('MMMM Do YYYY');
this.startDateRelativeTime = startDateM.fromNow();
this.started = startDateM.isBefore(moment());
}else{
this.started = true;
}
if(this.started && !this.ended && this.srvModel.endDate){
var mDiffD = moment(this.srvModel.endDate).diff(moment(), "days");
var mDiffH = moment(this.srvModel.endDate).diff(moment(), "hours");
var mDiffM = moment(this.srvModel.endDate).diff(moment(), "minutes");
var mDiffS = moment(this.srvModel.endDate).diff(moment(), "seconds");
this.endDiff = mDiffD > 0? mDiffD + " Days" : mDiffH> 0? mDiffH + " Hours" : mDiffM> 0? mDiffM+ " Minutes" : mDiffS> 0? mDiffS + " Seconds": "";
}
if(!this.started && this.srvModel.startDate){
var mDiffD = moment(this.srvModel.startDate).diff(moment(), "days");
var mDiffH = moment(this.srvModel.startDate).diff(moment(), "hours");
var mDiffM = moment(this.srvModel.startDate).diff(moment(), "minutes");
var mDiffS = moment(this.srvModel.startDate).diff(moment(), "seconds");
this.startDiff = mDiffD > 0? mDiffD + " Days" : mDiffH> 0? mDiffH + " Hours" : mDiffM> 0? mDiffM+ " Minutes" : mDiffS> 0? mDiffS + " Seconds": "";
}
this.lastUpdated = moment(this.srvModel.info.lastUpdated).calendar();
this.active = this.started && !this.ended;
setTimeout(this.updateComputed, 1000);
},
setLoading: function(val){
this.loading = val;
if(this.timeoutState){
clearTimeout(this.timeoutState);
}
}
},
mounted: function () {
hubListener.connect();
var self = this;
this.sound = this.srvModel.soundsEnabled;
this.animation = this.srvModel.animationsEnabled;
eventAggregator.$on("invoice-created", function (invoiceId) {
btcpay.setApiUrlPrefix(window.location.origin);
btcpay.showInvoice(invoiceId);
btcpay.showFrame();
self.contributeModalOpen = false;
self.setLoading(false);
});
eventAggregator.$on("contribute", function () {
self.setLoading(true);
self.timeoutState = setTimeout(function(){
self.setLoading(false);
},5000);
});
eventAggregator.$on("invoice-error", function(error){
self.setLoading(false);
var msg = "";
if(typeof error === "string"){
msg = error;
}else if(!error){
msg = "Unknown Error";
}else{
msg = JSON.stringify(error);
}
Vue.toasted.show("Error creating invoice: " + msg, {
iconPack: "fontawesome",
icon: "exclamation-triangle",
fullWidth: false,
theme: "bubble",
type: "error",
position: "top-center",
duration: 10000
} );
});
eventAggregator.$on("payment-received", function (amount, cryptoCode, type) {
var onChain = type.toLowerCase() === "btclike";
if(self.sound) {
playRandomQuakeSound();
}
if(self.animation) {
fireworks();
}
amount = parseFloat(amount).noExponents();
if(onChain){
Vue.toasted.show('New payment of ' + amount+ " "+ cryptoCode + " " + (onChain? "On Chain": "LN "), {
iconPack: "fontawesome",
icon: "plus",
duration: 10000
} );
}else{
Vue.toasted.show('New payment of ' + amount+ " "+ cryptoCode + " " + (onChain? "On Chain": "LN "), {
iconPack: "fontawesome",
icon: "bolt",
duration: 10000
} );
}
});
if(srvModel.disqusEnabled){
window.disqus_config = function () {
// Replace PAGE_URL with your page's canonical URL variable
this.page.url = window.location.href;
// Replace PAGE_IDENTIFIER with your page's unique identifier variable
this.page.identifier = self.srvModel.appId;
};
(function() { // REQUIRED CONFIGURATION VARIABLE: EDIT THE SHORTNAME BELOW
var d = document, s = d.createElement('script');
// IMPORTANT: Replace EXAMPLE with your forum shortname!
s.src = "https://"+self.srvModel.disqusShortname+".disqus.com/embed.js";
s.async= true;
s.setAttribute('data-timestamp', +new Date());
(d.head || d.body).appendChild(s);
var s2 = d.createElement('script');
s2.src="//"+self.srvModel.disqusShortname+".disqus.com/count.js";
s2.async= true;
s.setAttribute('data-timestamp', +new Date());
(d.head || d.body).appendChild(s);
})();
}
eventAggregator.$on("info-updated", function (model) {
console.warn("UPDATED", self.srvModel, arguments);
self.srvModel = model;
});
eventAggregator.$on("connection-pending", function () {
self.connectionStatus = "pending";
});
eventAggregator.$on("connection-failed", function () {
self.connectionStatus = "failed";
});
eventAggregator.$on("connection-lost", function () {
self.connectionStatus = "connection lost";
});
this.updateComputed();
}
});
});

View File

@ -0,0 +1,17 @@
Number.prototype.noExponents= function(){
var data= String(this).split(/[eE]/);
if(data.length== 1) return data[0];
var z= '', sign= this<0? '-':'',
str= data[0].replace('.', ''),
mag= Number(data[1])+ 1;
if(mag<0){
z= sign + '0.';
while(mag++) z += '0';
return z + str.replace(/^\-/,'');
}
mag -= str.length;
while(mag--) z += '0';
return str + z;
};

View File

@ -0,0 +1,65 @@
function playSound(path) {
// audio supported?
if (typeof window.Audio === 'function') {
var audioElem = new Audio(path);
audioElem.play().catch(function(){
debugger;
})
}
}
function playQuakeSound (name){
// var path = window.location.protocol +"://github.com/ClaudiuHKS/AdvancedQuakeSounds/blob/master/sound/QuakeSounds/"+name+"?raw=true"
var path = window.location.protocol + "//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/" + name;
playSound(path);
}
function playRandomQuakeSound(){
playQuakeSound(quake[Math.floor((Math.random() * quake.length) )]);
}
var quake = [
"dominating.wav"
,"doublekill.wav"
,"doublekill2.wav"
,"eagleeye.wav"
,"firstblood.wav"
,"firstblood2.wav"
,"firstblood3.wav"
,"flawless.wav"
,"godlike.wav"
,"hattrick.wav"
,"headhunter.wav"
,"headshot.wav"
,"headshot2.wav"
,"headshot3.wav"
,"holyshit.wav"
,"killingspree.wav"
,"knife.wav"
,"knife2.wav"
,"knife3.wav"
,"ludicrouskill.wav"
,"megakill.wav"
,"monsterkill.wav"
,"multikill.wav"
,"nade.wav"
,"ownage.wav"
,"payback.wav"
,"prepare.wav"
,"prepare2.wav"
,"prepare3.wav"
,"prepare4.wav"
,"rampage.wav"
,"suicide.wav"
,"suicide2.wav"
,"suicide3.wav"
,"suicide4.wav"
,"teamkiller.wav"
,"triplekill.wav"
,"ultrakill.wav"
,"unstoppable.wav"
,"whickedsick.wav"];

View File

@ -0,0 +1,231 @@
addLoadEvent(function(){
var c = document.getElementById("fireworks");
var ctx = c.getContext("2d");
var cH;
var cW;
var bgColor = "#FF6138";
var animations = [];
var circles = [];
var colorPicker = (function() {
var colors = ["#FF6138", "#FFBE53", "#2980B9", "#282741"];
var index = 0;
function next() {
index = index++ < colors.length-1 ? index : 0;
return colors[index];
}
function current() {
return colors[index]
}
return {
next: next,
current: current
}
})();
function removeAnimation(animation) {
var index = animations.indexOf(animation);
if (index > -1) animations.splice(index, 1);
}
function calcPageFillRadius(x, y) {
var l = Math.max(x - 0, cW - x);
var h = Math.max(y - 0, cH - y);
return Math.sqrt(Math.pow(l, 2) + Math.pow(h, 2));
}
function addClickListeners() {
document.addEventListener("touchstart", handleEvent);
document.addEventListener("mousedown", handleEvent);
};
function handleEvent(e) {
if (e.touches) {
e.preventDefault();
e = e.touches[0];
}
var currentColor = colorPicker.current();
var nextColor = colorPicker.next();
var targetR = calcPageFillRadius(e.pageX, e.pageY);
var rippleSize = Math.min(200, (cW * .4));
var minCoverDuration = 750;
var pageFill = new Circle({
x: e.pageX,
y: e.pageY,
r: 0,
fill: nextColor
});
var fillAnimation = anime({
targets: pageFill,
r: targetR,
duration: Math.max(targetR / 2 , minCoverDuration ),
easing: "easeOutQuart",
complete: function(){
bgColor = pageFill.fill;
removeAnimation(fillAnimation);
}
});
var ripple = new Circle({
x: e.pageX,
y: e.pageY,
r: 0,
fill: currentColor,
stroke: {
width: 3,
color: currentColor
},
opacity: 1
});
var rippleAnimation = anime({
targets: ripple,
r: rippleSize,
opacity: 0,
easing: "easeOutExpo",
duration: 900,
complete: removeAnimation
});
var particles = [];
for (var i=0; i<32; i++) {
var particle = new Circle({
x: e.pageX,
y: e.pageY,
fill: currentColor,
r: anime.random(24, 48)
})
particles.push(particle);
}
var particlesAnimation = anime({
targets: particles,
x: function(particle){
return particle.x + anime.random(rippleSize, -rippleSize);
},
y: function(particle){
return particle.y + anime.random(rippleSize * 1.15, -rippleSize * 1.15);
},
r: 0,
easing: "easeOutExpo",
duration: anime.random(1000,1300),
complete: removeAnimation
});
animations.push(fillAnimation, rippleAnimation, particlesAnimation);
}
function extend(a, b){
for(var key in b) {
if(b.hasOwnProperty(key)) {
a[key] = b[key];
}
}
return a;
}
var Circle = function(opts) {
extend(this, opts);
}
Circle.prototype.draw = function() {
ctx.globalAlpha = this.opacity || 1;
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI, false);
if (this.stroke) {
ctx.strokeStyle = this.stroke.color;
ctx.lineWidth = this.stroke.width;
ctx.stroke();
}
if (this.fill) {
ctx.fillStyle = this.fill;
ctx.fill();
}
ctx.closePath();
ctx.globalAlpha = 1;
}
var animate = anime({
duration: Infinity,
update: function() {
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, cW, cH);
animations.forEach(function(anim) {
anim.animatables.forEach(function(animatable) {
animatable.target.draw();
});
});
}
});
var resizeCanvas = function() {
cW = window.innerWidth;
cH = window.innerHeight;
c.width = cW * devicePixelRatio;
c.height = cH * devicePixelRatio;
ctx.scale(devicePixelRatio, devicePixelRatio);
};
(function init() {
resizeCanvas();
window.addEventListener("resize", resizeCanvas);
// addClickListeners();
// handleInactiveUser();
})();
function handleInactiveUser() {
var inactive = setTimeout(function(){
fauxClick(cW/2, cH/2);
}, 2000);
function clearInactiveTimeout() {
clearTimeout(inactive);
document.removeEventListener("mousedown", clearInactiveTimeout);
document.removeEventListener("touchstart", clearInactiveTimeout);
}
document.addEventListener("mousedown", clearInactiveTimeout);
document.addEventListener("touchstart", clearInactiveTimeout);
}
function startFauxClicking() {
setTimeout(function(){
fauxClick(anime.random( cW * .2, cW * .8), anime.random(cH * .2, cH * .8));
startFauxClicking();
}, anime.random(200, 900));
}
function fauxClick(x, y) {
var fauxClick = new Event("mousedown");
fauxClick.pageX = x;
fauxClick.pageY = y;
document.dispatchEvent(fauxClick);
}
window.fireworks = function(){
var fauxClick = new Event("mousedown");
fauxClick.pageX =anime.random( 0, cW );
fauxClick.pageY = anime.random(0, cH );
var middleSpaceX = cW * 0.6;
var middleSpaceY = cH * 0.6;
var middleSpaceX1 = middleSpaceX /2;
var middleSpaceX2 = middleSpaceX1 + middleSpaceX;
var middleSpaceY1 = middleSpaceY /2;
var middleSpaceY2 = middleSpaceY1 + middleSpaceY;
while(true){
if(fauxClick.pageX > middleSpaceX1 && fauxClick.pageX < middleSpaceX2){
fauxClick.pageX =anime.random( 0, cW );
continue;
}
if(fauxClick.pageY > middleSpaceY1 && fauxClick.pageY < middleSpaceY2){
fauxClick.pageY =anime.random( 0, cH );
continue;
}
break;
}
handleEvent(fauxClick)
};
});

View File

@ -0,0 +1,50 @@
var hubListener = function(){
var connection = new signalR.HubConnectionBuilder().withUrl("/apps/crowdfund/hub").build();
connection.onclose(function(){
eventAggregator.$emit("connection-lost");
console.error("Connection was closed. Attempting reconnect in 2s");
setTimeout(connect, 2000);
});
connection.on("PaymentReceived", function(amount, cryptoCode, type){
eventAggregator.$emit("payment-received", amount,cryptoCode, type);
});
connection.on("InvoiceCreated", function(invoiceId){
eventAggregator.$emit("invoice-created", invoiceId);
});
connection.on("InvoiceError", function(error){
eventAggregator.$emit("invoice-error", error);
});
connection.on("InfoUpdated", function(model){
eventAggregator.$emit("info-updated", model);
});
function connect(){
eventAggregator.$emit("connection-pending");
connection
.start()
.then(function(){
connection.invoke("ListenToCrowdfundApp", srvModel.appId);
})
.catch(function (err) {
eventAggregator.$emit("connection-failed");
console.error("Could not connect to backend. Retrying in 2s", err );
setTimeout(connect, 2000);
});
}
eventAggregator.$on("contribute", function(model){
connection.invoke("CreateInvoice", model);
});
return {
connect: connect
};
}();

View File

@ -0,0 +1,56 @@
[v-cloak] > * {
display: none
}
[v-cloak]::before {
content: "loading…"
}
canvas#fireworks {
width: 100vw;
height: 100vh;
position: fixed;
}
.perk-zoom {
display: none;
opacity: 0;
}
.perk-zoom-bg {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
opacity: 0.9;
}
.perk-zoom-text {
left: 50%;
top: 50%;
border-radius: 50%;
transform: translate(-50%, -50%);
position: absolute;
}
.perk:hover .perk-zoom {
cursor: pointer;
opacity: 1;
display: block;
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
}
.perk-badge {
width: 33px;
height: 33px;
position: absolute;
left: -15px;
top: -15px;
padding-top: 5px;
z-index: 1
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

@ -3,6 +3,7 @@
var amount = $("#Amount").val();
var fee = $("#FeeSatoshiPerByte").val();
var substractFee = $("#SubstractFees").val();
var noChange = $("#NoChange").val();
var loc = window.location, ws_uri;
if (loc.protocol === "https:") {
@ -48,8 +49,14 @@
args += "&amount=" + amount;
args += "&feeRate=" + fee;
args += "&substractFees=" + substractFee;
args += "&noChange=" + noChange;
WriteAlert("warning", 'Please validate the transaction on your ledger');
if (noChange === "True") {
WriteAlert("warning", 'WARNING: Because you want to make sure no change UTXO is created, you will end up sending more than the chosen amount to your destination. Please validate the transaction on your ledger');
}
else {
WriteAlert("warning", 'Please validate the transaction on your ledger');
}
var confirmButton = $("#confirm-button");
confirmButton.prop("disabled", true);

File diff suppressed because one or more lines are too long

View File

@ -43,5 +43,7 @@
"BOLT 11 Invoice": "BOLT 11 Faktura",
"Node Info": "Info o uzlu",
"txCount": "{{count}} transakce",
"txCount_plural": "{{count}} transakcí"
"txCount_plural": "{{count}} transakcí",
"Pay with CoinSwitch": "Pay with CoinSwitch",
"Pay with Changelly": "Pay with Changelly"
}

View File

@ -43,5 +43,7 @@
"BOLT 11 Invoice": "BOLT 11 Rechnung",
"Node Info": "Netzwerkknoten Info",
"txCount": "{{count}} transaktion",
"txCount_plural": "{{count}} transaktionen"
"txCount_plural": "{{count}} transaktionen",
"Pay with CoinSwitch": "Pay with CoinSwitch",
"Pay with Changelly": "Pay with Changelly"
}

View File

@ -43,5 +43,7 @@
"BOLT 11 Invoice": "BOLT 11 Invoice",
"Node Info": "Node Info",
"txCount": "{{count}} transaction",
"txCount_plural": "{{count}} transactions"
"txCount_plural": "{{count}} transactions",
"Pay with CoinSwitch": "Pay with CoinSwitch",
"Pay with Changelly": "Pay with Changelly"
}

View File

@ -23,10 +23,10 @@
"Address": "Dirección",
"Copied": "Copiado",
"ConversionTab_BodyTop": "Puedes pagar {{btcDue}} {{cryptoCode}} usando altcoins que este comercio no soporta directamente.",
"ConversionTab_BodyDesc": "Este servicio es provisto por terceros. Ten en cuenta que no tenemos control sobre cómo estos terceros envían los fondos. La factura solo se marcará como pagada una vez se reciban los fondos en la cadena de bloques de {{cryptoCode}} .",
"ConversionTab_BodyDesc": "Este servicio es provisto por terceros. Ten en cuenta que no tenemos control sobre cómo estos terceros re-enviarán tus fondos. La factura solo se marcará como pagada una vez se reciban los fondos en la cadena de bloques de {{cryptoCode}} .",
"ConversionTab_CalculateAmount_Error": "Reintentar",
"ConversionTab_LoadCurrencies_Error": "Reintentar",
"ConversionTab_Lightning": "No hay proveedores de conversión disponibles para los pagos de Lightning Network.",
"ConversionTab_Lightning": "No hay proveedores de conversión disponibles para los pagos con Lightning Network.",
"ConversionTab_CurrencyList_Select_Option": "Selecciona la moneda a convertir",
"Invoice expiring soon...": "La factura expira pronto...",
"Invoice expired": "La factura expiró",
@ -43,5 +43,7 @@
"BOLT 11 Invoice": "Factura BOLT 11",
"Node Info": "Información del nodo",
"txCount": "{{count}} transacción",
"txCount_plural": "{{count}} transacciones"
"txCount_plural": "{{count}} transacciones",
"Pay with CoinSwitch": "Pagar con CoinSwitch",
"Pay with Changelly": "Pagar con Changelly"
}

View File

@ -43,5 +43,7 @@
"BOLT 11 Invoice": "Facture BOLT 11",
"Node Info": "Informations sur le nœud",
"txCount": "{{count}} transaction",
"txCount_plural": "{{count}} transactions"
"txCount_plural": "{{count}} transactions",
"Pay with CoinSwitch": "Pay with CoinSwitch",
"Pay with Changelly": "Pay with Changelly"
}

View File

@ -43,5 +43,7 @@
"BOLT 11 Invoice": "BOLT 11 चालान",
"Node Info": "नोड जानकारी",
"txCount": "लेनदेन",
"txCount_plural": "लेनदेनों"
"txCount_plural": "लेनदेनों",
"Pay with CoinSwitch": "Pay with CoinSwitch",
"Pay with Changelly": "Pay with Changelly"
}

View File

@ -43,5 +43,7 @@
"BOLT 11 Invoice": "BOLT 11 Invoice",
"Node Info": "Node Info",
"txCount": "{{count}} transaction",
"txCount_plural": "{{count}} transactions"
"txCount_plural": "{{count}} transactions",
"Pay with CoinSwitch": "Pay with CoinSwitch",
"Pay with Changelly": "Pay with Changelly"
}

View File

@ -43,5 +43,7 @@
"BOLT 11 Invoice": "BOLT 11 Reikningur",
"Node Info": "Nótu upplýsingar",
"txCount": "{{count}} reikningur",
"txCount_plural": "{{count}} reikningar"
"txCount_plural": "{{count}} reikningar",
"Pay with CoinSwitch": "Pay with CoinSwitch",
"Pay with Changelly": "Pay with Changelly"
}

View File

@ -43,5 +43,7 @@
"BOLT 11 Invoice": "Fattura BOLT 11",
"Node Info": "Informazioni sul Nodo",
"txCount": "{{count}} transazione",
"txCount_plural": "{{count}} transazioni"
"txCount_plural": "{{count}} transazioni",
"Pay with CoinSwitch": "Pay with CoinSwitch",
"Pay with Changelly": "Pay with Changelly"
}

View File

@ -43,5 +43,7 @@
"BOLT 11 Invoice": "お支払いコード",
"Node Info": "接続情報",
"txCount": "取引 {{count}} 個",
"txCount_plural": "取引 {{count}} 個"
"txCount_plural": "取引 {{count}} 個",
"Pay with CoinSwitch": "CoinSwitchでのお支払い",
"Pay with Changelly": "Changellyでのお支払い"
}

View File

@ -43,5 +43,7 @@
"BOLT 11 Invoice": "BOLT 11 Шот-фактура",
"Node Info": "Node анықтамасы",
"txCount": "{{count}} Транзакция",
"txCount_plural": "{{count}} Транзакциялар"
"txCount_plural": "{{count}} Транзакциялар",
"Pay with CoinSwitch": "Pay with CoinSwitch",
"Pay with Changelly": "Pay with Changelly"
}

View File

@ -18,22 +18,22 @@
"Copy": "Kopiëren",
"Conversion": "Omzetting",
"Open in wallet": "Wallet openen",
"CompletePay_Body": "Om de betaling te vervoledigen, bedankt om {{btcDue}} {{cryptoCode}} naar het hieronder vemelde adres op te sturen.",
"CompletePay_Body": "Om de betaling af te ronden, stuur alstublieft {{btcDue}} {{cryptoCode}} naar het hieronder vemelde adres.",
"Amount": "Bedrag",
"Address": "Adres",
"Copied": "Gekopieerd",
"ConversionTab_BodyTop": "Je kan altcoins gebruiken die niet ondersteund zijn door de verkoper, om {{btcDue}} {{cryptoCode}} te betalen.",
"ConversionTab_BodyDesc": "Deze dienst wordt door een externe partij geleverd. Bijgevolg, hebben we geen zicht over jouw fondsen. De factuur wordt pas als betaald beschouwd, wanneer de fondsen door de blockchain aanvaard zijn {{ cryptoCode }}.",
"ConversionTab_CalculateAmount_Error": "Retry",
"ConversionTab_LoadCurrencies_Error": "Retry",
"ConversionTab_CalculateAmount_Error": "Opnieuw proberen",
"ConversionTab_LoadCurrencies_Error": "Opnieuw proberen",
"ConversionTab_Lightning": "Geen leverancier beschikbaar voor de betalingen op het Lightning Network",
"ConversionTab_CurrencyList_Select_Option": "Please select a currency to convert from",
"ConversionTab_CurrencyList_Select_Option": "Selecteer een valuta om te converteren",
"Invoice expiring soon...": "De factuur verloopt binnenkort...",
"Invoice expired": "Vervallen factuur",
"What happened?": "Wat gebeurde er?",
"InvoiceExpired_Body_1": "De factuur is vervallen. Een factuur is alleen geldig voor {{maxTimeMinutes}} minuten. \nJe kan terug komen naar {{storeName}} als je de betaling opnieuw wilt proberen",
"InvoiceExpired_Body_2": "Als je een betaling uitvoerde, dan werd dit nog niet bevestigd door het netwerk. We hebben je fondsen nog niet ontvangen.",
"InvoiceExpired_Body_3": "",
"InvoiceExpired_Body_3": "Indien we het later ontvangen, zullen we uw order verwerken of nemen we contact op om een terugbetaling te regelen...",
"Invoice ID": "Factuurnummer",
"Order ID": "Bestllingsnummer",
"Return to StoreName": "Terug naar {{storeName}}",
@ -43,5 +43,7 @@
"BOLT 11 Invoice": "BOLT 11 Factuur",
"Node Info": "Node Info",
"txCount": "{{count}} transactie",
"txCount_plural": "{{count}} transacties"
"txCount_plural": "{{count}} transacties",
"Pay with CoinSwitch": "Betalen met CoinSwitch",
"Pay with Changelly": "Betalen met Changelly"
}

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