Compare commits
168 Commits
Author | SHA1 | Date | |
---|---|---|---|
0136977359 | |||
0acd3e20b0 | |||
30bdfeee37 | |||
7ea665d884 | |||
073edcfb12 | |||
a645366a25 | |||
12aa0b7abd | |||
3f98a50410 | |||
24c8c076d5 | |||
37e6931d33 | |||
86493568e9 | |||
bb51436ae3 | |||
854a55ac1a | |||
cfb4b080d3 | |||
00aa2e4e17 | |||
69c67d99f6 | |||
65596ec8c1 | |||
49643cb00e | |||
35b0faee57 | |||
88ef4d69b2 | |||
575b6ca222 | |||
b5a0e844d2 | |||
2642e11ce2 | |||
b4fe655efe | |||
ffb761909a | |||
b443e1ac6e | |||
a4792f54a7 | |||
686ae029e0 | |||
f3fd2e7d0f | |||
7efd9ba0a5 | |||
1c2a6bb8a1 | |||
7bcf1cbdd5 | |||
2aaa2544bd | |||
d85f03ba20 | |||
cfb51a6be4 | |||
c9d778c94b | |||
fd62f882de | |||
adc050f190 | |||
2d551b9fc5 | |||
884acdde32 | |||
8f896de794 | |||
5e4e26d2fd | |||
ae688e6615 | |||
c4c812bdf6 | |||
e620fc0283 | |||
c333902468 | |||
4c83ecd06a | |||
b28a547dc4 | |||
6bc17e05bd | |||
0903350d30 | |||
6c0f19b457 | |||
e119dc823f | |||
43295c9c57 | |||
ded8b54042 | |||
50a3178d51 | |||
393c226032 | |||
f2630df387 | |||
abcd2c1750 | |||
cc95f3b5b5 | |||
a08ee93b43 | |||
4b90f873d5 | |||
419ab8e0b1 | |||
c95ef27998 | |||
63dfd93834 | |||
57610881de | |||
7469faf296 | |||
55a884a559 | |||
ee2b3c3d10 | |||
e5819a260b | |||
a3ecf48702 | |||
1c0b904cd2 | |||
072d8a1728 | |||
964e541c32 | |||
78fec4ed22 | |||
ef111d36c9 | |||
4f64193e85 | |||
89bb6d1268 | |||
9f4226bf0f | |||
a87c2a3374 | |||
d7294ba5a0 | |||
82d286dc6f | |||
1fa18ab997 | |||
afc90f32c9 | |||
e9cfb7c21e | |||
1af8ea3769 | |||
9f7af190f1 | |||
9c703fe94d | |||
a7a11a4f13 | |||
c32c3bb62b | |||
e29d1480a6 | |||
8f299d7791 | |||
65fb2e992e | |||
41f5d677d5 | |||
a2b78b8cd9 | |||
c93f217033 | |||
82c47b6e9a | |||
94fb738c67 | |||
89071e40fc | |||
95a90c410e | |||
59fc371cd5 | |||
caadfc8641 | |||
bffc2e70c1 | |||
ca28c34be0 | |||
196bc3ea00 | |||
04cba61888 | |||
a41e2e1ceb | |||
d1d03c98ba | |||
3e48a54ab5 | |||
f6e389ff62 | |||
561ec57cc8 | |||
c63feb488c | |||
f2ccc4d963 | |||
a92d48efdd | |||
b633206b45 | |||
b6f3d2af5e | |||
5ca4494eed | |||
de7e419ef4 | |||
20a6b3fc33 | |||
e6357d2ac8 | |||
1eecd85ceb | |||
d63176da19 | |||
887da5aa9a | |||
6e7f1151bc | |||
fb6d852827 | |||
ba17612461 | |||
a15c7a0213 | |||
7e321d4016 | |||
2ccf007b9a | |||
895b8c2c80 | |||
493466683c | |||
761c342c51 | |||
5341da28d9 | |||
7768f41849 | |||
c52a49f747 | |||
e4b9895ba7 | |||
92a2bb4d32 | |||
bfec722312 | |||
5a3f7b5b70 | |||
2aa097be46 | |||
8a646d85c6 | |||
890b3eaa00 | |||
cda28ebf15 | |||
2245027ca3 | |||
3dc250f801 | |||
66e786a1b0 | |||
8bd7ea5bbc | |||
6eb36abe2e | |||
6fced3fab2 | |||
774e456e54 | |||
519859e1c5 | |||
26f0c488e5 | |||
1b0b53fbd0 | |||
35f4ea29f9 | |||
d9426d301d | |||
8bcf7109a3 | |||
b11f8acba1 | |||
ef9a633aa4 | |||
c7e2f979dd | |||
e97bb9c933 | |||
2b84791391 | |||
9a488c60f2 | |||
9cb50446f4 | |||
8c00a2359e | |||
d1ff34d16d | |||
8e8615dab8 | |||
27bde55f54 | |||
b5d360594a | |||
7fa1b65af0 |
252
BTCPayServer.Tests/CrowdfundTests.cs
Normal file
252
BTCPayServer.Tests/CrowdfundTests.cs
Normal 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);
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
64
BTCPayServer.Tests/MockDelay.cs
Normal file
64
BTCPayServer.Tests/MockDelay.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
35
BTCPayServer/BTCPayNetworkProvider.Bitcoinplus.cs
Normal file
35
BTCPayServer/BTCPayNetworkProvider.Bitcoinplus.cs
Normal 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'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -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",
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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))
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
171
BTCPayServer/Controllers/AppsController.Crowdfund.cs
Normal file
171
BTCPayServer/Controllers/AppsController.Crowdfund.cs
Normal 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});
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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());
|
||||
|
@ -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());
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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)
|
||||
|
65
BTCPayServer/Controllers/StoresController.Email.cs
Normal file
65
BTCPayServer/Controllers/StoresController.Email.cs
Normal 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});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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>();
|
||||
|
66
BTCPayServer/Crowdfund/CrowdfundHub.cs
Normal file
66
BTCPayServer/Crowdfund/CrowdfundHub.cs
Normal 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>());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
371
BTCPayServer/Crowdfund/CrowdfundHubStreamer.cs
Normal file
371
BTCPayServer/Crowdfund/CrowdfundHubStreamer.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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>");
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
}));
|
||||
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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));
|
||||
|
@ -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
33
BTCPayServer/IDelay.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
{
|
||||
|
69
BTCPayServer/Models/AppViewModels/ViewCrowdfundViewModel.cs
Normal file
69
BTCPayServer/Models/AppViewModels/ViewCrowdfundViewModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,10 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public int Total
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string SearchTerm
|
||||
{
|
||||
get; set;
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
|
@ -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))
|
||||
{
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -7,6 +7,7 @@ namespace BTCPayServer.Services.Apps
|
||||
{
|
||||
public enum AppType
|
||||
{
|
||||
PointOfSale
|
||||
PointOfSale,
|
||||
Crowdfund
|
||||
}
|
||||
}
|
||||
|
12
BTCPayServer/Services/IBackgroundJobClient.cs
Normal file
12
BTCPayServer/Services/IBackgroundJobClient.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
31
BTCPayServer/Services/Mails/EmailSenderFactory.cs
Normal file
31
BTCPayServer/Services/Mails/EmailSenderFactory.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
25
BTCPayServer/Services/Mails/ServerEmailSender.cs
Normal file
25
BTCPayServer/Services/Mails/ServerEmailSender.cs
Normal 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>();
|
||||
}
|
||||
}
|
||||
}
|
38
BTCPayServer/Services/Mails/StoreEmailSender.cs
Normal file
38
BTCPayServer/Services/Mails/StoreEmailSender.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
|
@ -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
|
||||
|
244
BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml
Normal file
244
BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml
Normal 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">×</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>
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
154
BTCPayServer/Views/AppsPublic/Crowdfund/MinimalCrowdfund.cshtml
Normal file
154
BTCPayServer/Views/AppsPublic/Crowdfund/MinimalCrowdfund.cshtml
Normal 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>
|
319
BTCPayServer/Views/AppsPublic/Crowdfund/VueCrowdfund.cshtml
Normal file
319
BTCPayServer/Views/AppsPublic/Crowdfund/VueCrowdfund.cshtml
Normal 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>
|
58
BTCPayServer/Views/AppsPublic/ViewCrowdfund.cshtml
Normal file
58
BTCPayServer/Views/AppsPublic/ViewCrowdfund.cshtml
Normal 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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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>
|
||||
|
37
BTCPayServer/Views/Invoice/PosData.cshtml
Normal file
37
BTCPayServer/Views/Invoice/PosData.cshtml
Normal 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>
|
@ -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>
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
||||
|
68
BTCPayServer/Views/Stores/Emails.cshtml
Normal file
68
BTCPayServer/Views/Stores/Emails.cshtml
Normal 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")
|
||||
}
|
@ -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">×</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>
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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 />
|
||||
|
@ -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">
|
||||
|
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
8
BTCPayServer/wwwroot/crowdfund-admin/main.js
Normal file
8
BTCPayServer/wwwroot/crowdfund-admin/main.js
Normal file
@ -0,0 +1,8 @@
|
||||
hljs.initHighlightingOnLoad();
|
||||
$(document).ready(function() {
|
||||
|
||||
$(".richtext").summernote();
|
||||
$(".datetime").flatpickr({
|
||||
enableTime: true
|
||||
});
|
||||
});
|
323
BTCPayServer/wwwroot/crowdfund/app.js
Normal file
323
BTCPayServer/wwwroot/crowdfund/app.js
Normal 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
17
BTCPayServer/wwwroot/crowdfund/helpers/math.js
Normal file
17
BTCPayServer/wwwroot/crowdfund/helpers/math.js
Normal 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;
|
||||
};
|
65
BTCPayServer/wwwroot/crowdfund/services/audioplayer.js
Normal file
65
BTCPayServer/wwwroot/crowdfund/services/audioplayer.js
Normal 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"];
|
231
BTCPayServer/wwwroot/crowdfund/services/fireworks.js
Normal file
231
BTCPayServer/wwwroot/crowdfund/services/fireworks.js
Normal 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)
|
||||
};
|
||||
|
||||
|
||||
});
|
50
BTCPayServer/wwwroot/crowdfund/services/listener.js
Normal file
50
BTCPayServer/wwwroot/crowdfund/services/listener.js
Normal 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
|
||||
};
|
||||
}();
|
||||
|
56
BTCPayServer/wwwroot/crowdfund/styles/main.css
Normal file
56
BTCPayServer/wwwroot/crowdfund/styles/main.css
Normal 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
|
||||
}
|
BIN
BTCPayServer/wwwroot/imlegacy/bitcoinplus.png
Normal file
BIN
BTCPayServer/wwwroot/imlegacy/bitcoinplus.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.8 KiB |
@ -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
@ -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"
|
||||
}
|
@ -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"
|
||||
}
|
@ -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"
|
||||
}
|
@ -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"
|
||||
}
|
@ -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"
|
||||
}
|
@ -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"
|
||||
}
|
@ -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"
|
||||
}
|
@ -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"
|
||||
}
|
@ -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"
|
||||
}
|
@ -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でのお支払い"
|
||||
}
|
@ -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"
|
||||
}
|
@ -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
Reference in New Issue
Block a user