Compare commits

...

120 Commits

Author SHA1 Message Date
334abdc9d1 bump 2019-02-27 20:31:00 +09:00
3728fdab3f improve warning message 2019-02-27 18:54:19 +09:00
2317e3d50c Make sure we rewrite the request scheme 2019-02-27 18:52:11 +09:00
5f15976c02 bump 2019-02-27 18:46:15 +09:00
7f592639c5 Remove URI rewritting and ExternalUri stuff 2019-02-27 18:38:11 +09:00
a98402af12 Making currency switching indicator more obvious with button style (#616) 2019-02-27 13:45:58 +09:00
316ffa91d1 bump 2019-02-26 23:10:58 +09:00
c24953b57e Making hamburger light to see it on dark background (#613) 2019-02-26 14:08:03 +09:00
7a1b1b7e5e Merge branch 'payment-requests' 2019-02-25 17:59:02 +09:00
70f71f64c4 Use internal tags, not order id in the streamer to know if the incoming invoice is for the payment request 2019-02-25 17:56:29 +09:00
5bccd07d7d Make sure the invoiceEvent is from a payment request. 2019-02-25 17:56:29 +09:00
d818baa6d1 Fix crowdfund test 2019-02-25 17:56:29 +09:00
249b8abf03 deduct network from contributions + removed unsued Enabled properties 2019-02-25 17:56:29 +09:00
c134277514 remove creating state from payment requests 2019-02-25 17:56:29 +09:00
f5d366cf7f Fix final bugs 2019-02-25 17:56:29 +09:00
ad25a2ed08 Add payment requests 2019-02-25 17:56:28 +09:00
1e7a2ffe97 Enable/Disable tips and discount. Fix custom amount. (#612) 2019-02-25 15:11:03 +09:00
dd52075ff1 Add backoff delay if fetching exchange rate is failing 2019-02-24 22:00:30 +09:00
0253e42bd5 Do not poll in the invoice page if websocket are working 2019-02-23 15:22:17 +09:00
d99774f8d9 fix tests 2019-02-22 22:52:43 +09:00
d563a2ec89 Fix tests 2019-02-22 22:48:39 +09:00
b4b4523193 Round currency up to significant decimal 2019-02-22 22:15:25 +09:00
fbcb69f447 Do not prevent btcpayserver from starting if using insecure protocol for lightning services 2019-02-22 18:24:27 +09:00
8ae5a9c1f7 Fix old crowdfunding invoices 2019-02-22 17:51:38 +09:00
3ef5bfb6eb bump 2019-02-22 17:30:54 +09:00
4016ded584 Affect orderId to crowdfund app invoices 2019-02-22 17:29:54 +09:00
5b0b4adb1c Fix service link for RTL 2019-02-22 15:46:43 +09:00
f1ec3b0c75 bump 2019-02-22 15:08:45 +09:00
b5d55a2066 Add RTL support 2019-02-22 15:06:52 +09:00
0d2c9fe377 Fix https://github.com/btcpayserver/btcpayserver/issues/585 2019-02-22 13:52:35 +09:00
2c7cc9a796 Fix: invoice Price was not being rounded if no taxIncluded present 2019-02-21 21:58:49 +09:00
2e1d623755 fix https://github.com/btcpayserver/btcpayserver/issues/596 2019-02-21 21:30:30 +09:00
52fee8f842 Make sure no nullreferenceexception is thrown if invalid invoice 2019-02-21 19:36:05 +09:00
6ba17e8e30 Can filter supported payment methods for an invoice 2019-02-21 19:34:11 +09:00
ac3432920a Fix build 2019-02-21 18:42:12 +09:00
63c88be533 Use CreateInvoiceRequest instead of NBitpay Invoice type 2019-02-21 18:40:27 +09:00
3cb577e6ba Add link back to official website 2019-02-21 14:04:03 +09:00
1e0d64c548 Improve homepage, document mattermost and point on the official website. 2019-02-21 13:50:46 +09:00
bc1b9ff59c update translations 2019-02-20 23:16:13 +09:00
7d73bed3be bump 2019-02-20 23:06:52 +09:00
126fbdfd60 Fix null reference exception if the NotificationUrl is not set 2019-02-20 23:03:04 +09:00
15094436fd bump lnd 2019-02-20 21:29:16 +09:00
010c653995 Create EventHostedServiceBase and make AppHubStreamer use this 2019-02-20 12:27:10 +09:00
119f82fd4e Properly aggregate contributions amount 2019-02-19 16:15:14 +09:00
3bbf4de5d2 Fix live update of crowdfunding, add tests, consider payments as confirmed if invoice is confirmed 2019-02-19 16:01:28 +09:00
0807f3b87b Remote internal tags at store level 2019-02-19 13:24:04 +09:00
4e9b3b40aa Fix crowdfunding-admin js file not being included 2019-02-19 13:20:06 +09:00
cc444811db Rename CrowdfundHubStream to AppHubSteamer 2019-02-19 13:18:30 +09:00
50c8525012 Moving CrowdfundSettings in its own file 2019-02-19 13:07:10 +09:00
aedad497e8 Rename AppsHelper to AppService 2019-02-19 13:04:58 +09:00
b1b231e645 Add tests on tagging 2019-02-19 12:59:12 +09:00
dc46fd225a Migrate old crowdfund deployment to the new tagging system 2019-02-19 12:53:24 +09:00
6226de7cff Refactor Crowdfund to use the tagging system 2019-02-19 12:48:48 +09:00
37327ec674 Apps can tag invoices 2019-02-19 12:48:08 +09:00
c071c81403 Pass the whole Entity object to internal InvoiceEvent 2019-02-19 12:08:07 +09:00
85d75a013a The invoices link of crowdfund show all invoices of the store if it is set to use all store's invoice 2019-02-19 11:45:04 +09:00
3816b36131 Add internal tags to invoice 2019-02-19 11:14:21 +09:00
dc7965267b Use GetRelativePathOrAbsolute in ViewCrowdfund and ViewPointOfSale 2019-02-19 00:28:44 +09:00
ce9a6bced7 Use GetRelativePathOrAbsolute in ShowLightningNodeInfo 2019-02-18 12:25:14 +09:00
85325dc710 Update translations 2019-02-18 12:24:55 +09:00
ac4050df70 Improve the UI of lightning node info 2019-02-17 19:40:39 +09:00
a16a53167b Can put lightning node info inside an XFrame 2019-02-17 19:30:16 +09:00
afab3cf847 Better Datetime picker picker in crowdfund page 2019-02-17 19:25:18 +09:00
8fdaeb7bac Fix race condition on calculation of contributions, refactor the methods to AppHelper 2019-02-17 19:17:59 +09:00
7e0f9f6e0d Inject HtmlSanitizer in AddBTCPayServer, remove AppHelpers deps when possible 2019-02-17 18:47:25 +09:00
5b1bf6cd88 add email to export (#583) 2019-02-17 18:33:40 +09:00
b1584c352b Free some memory 2019-02-17 16:13:16 +09:00
b06b83503c Better status message 2019-02-15 10:05:29 -06:00
b03d89c190 Different message for admin deletion, check not to delete last admin
Ref: #549, #550
2019-02-15 10:05:29 -06:00
f53548d10f Showing warning when user tries to delete last admin 2019-02-15 10:05:29 -06:00
5ec2f54d7f Merge pull request #593 from BenSanex/bugfix/591_FixValidationMessage
Custom validation message for Crowdfund form primary currency
2019-02-15 10:03:05 -06:00
db588ff961 I've added asterisk. Isn't that impressive? 2019-02-15 10:02:17 -06:00
2b7006a14c add asterisk, revert primary currency error message, remove the 2019-02-11 21:53:45 -06:00
8f5f07882f Custom validation message for Crowdfund form primary currency 2019-02-07 20:27:26 -06:00
0eee8e7464 Returns Access-Control-Allow-Origin * on all Bitpay GET and post requests. 2019-02-02 16:12:51 +09:00
3725a5b644 Correctly set Access-Control-Allow-Headers 2019-02-02 15:51:38 +09:00
c84c0ac64d set CORS headers 2019-02-02 15:22:00 +09:00
098e07988c Bypass MVC for replying to CORS requests if Bitpay API 2019-02-02 15:19:22 +09:00
66bb702aca Fix CORS for bitpay API again 2019-02-02 13:58:32 +09:00
03ff2fedf0 Update Translator grammar (#579) 2019-02-01 17:35:49 +09:00
c707f47b11 bump 2019-01-31 22:03:46 +09:00
585efa3ff5 Fix: Default payment method should not return a disabled one 2019-01-31 22:03:28 +09:00
07d0b98a23 Update language 2019-01-31 19:33:07 +09:00
c7c0f01010 bump 2019-01-31 19:24:36 +09:00
cf6b17250a Can set lightning network as default payment method (close #290) 2019-01-31 19:07:38 +09:00
90503a490c Add dots to make derivation examples clearer (#561) 2019-01-31 17:00:15 +09:00
ebdd53b99b fix unfairly long dropdown in ledger account selection (#574)
Closes #570
2019-01-31 16:56:39 +09:00
51a5d2e812 Refactor XFrames Attribute & simplify pos settings page (#576)
* Enable better error when invoice cannot be created on crowdfund

Closes #572

* Allow all public apps in iframe

* cleanup pos page dev info
2019-01-31 16:56:21 +09:00
2ad509d56a Update Readme.md (#577)
* Update readme

* Update README.md

* add apps link

* fix broken link
2019-01-31 16:55:27 +09:00
1a98bfba36 Fix formatting of currencies in Invoice detail page 2019-01-30 19:18:44 +09:00
d05bb6c60e Properly format currencies in Invoice list 2019-01-30 19:01:18 +09:00
ed81b6a6aa bump 2019-01-30 15:52:31 +09:00
264914588f fix bitpay API not having CORS 2019-01-30 14:57:10 +09:00
05df43b426 fix bitpay API not having CORS 2019-01-30 14:36:26 +09:00
0334a4e176 bump 2019-01-30 13:46:55 +09:00
38dca425da Fix repetitive IPN for lightning network payments (https://github.com/btcpayserver/btcpayserver/issues/564) 2019-01-30 13:40:08 +09:00
82d4a79dd4 Fix potential crash if the current host is an IP instead of DNS name, might fix https://github.com/btcpayserver/btcpayserver/issues/543 2019-01-30 12:52:34 +09:00
6725be8145 Remove warnings 2019-01-29 18:35:27 +09:00
f5b693f01b Disable quadricagx tests because exchange is down 2019-01-29 18:34:30 +09:00
f09f23e570 Enable better error when invoice cannot be created on crowdfund (#575)
Closes #572
2019-01-29 18:32:44 +09:00
4f4d05b8cd Make sure CORS is enabled on Bitpay's API 2019-01-29 18:20:53 +09:00
0c5b5ff49c Add link to no stores error (#558)
* add link

* safer status message

* refactor

* small view cleanup
2019-01-29 16:44:46 +09:00
a815fad3f1 Put back the list of ledger accounts to 5. 2019-01-29 13:06:43 +09:00
d8b1c7c10a Fix broken lightning payments on Checkout page 2019-01-28 18:50:26 +09:00
02e1aea80c add warning for third parties (#562)
* add warning for third parties

* Update UpdateCoinSwitchSettings.cshtml

* Update UpdateChangellySettings.cshtml

* Update UpdateChangellySettings.cshtml

* Update UpdateCoinSwitchSettings.cshtml
2019-01-28 17:40:23 +09:00
1892f7e0f4 rename field 2019-01-28 17:10:51 +09:00
b7b50349a7 Convert Ledger account list to dropdown and add more accounts to list (#560) 2019-01-28 17:07:01 +09:00
02d227ee02 Fix connection to checkout backend (bad links) 2019-01-28 16:24:11 +09:00
47f8938b89 Catch websocket connection issues 2019-01-28 15:12:40 +09:00
4945a640a7 Use PaymentHash of a lightning payment as PaymentId 2019-01-27 13:06:55 +09:00
0136977359 update translations 2019-01-26 21:23:41 +09:00
0acd3e20b0 bump 2019-01-26 20:58:15 +09:00
30bdfeee37 Enhance PosData Viewer & add cart to posdata in POS app (#559) 2019-01-26 13:26:49 +09:00
7ea665d884 Merge pull request #557 from Kukks/master
Fix close invoice button for modal invoices #555
2019-01-25 20:48:45 +09:00
073edcfb12 Merge remote-tracking branch 'btcpayserver/master' 2019-01-25 12:41:20 +01:00
a645366a25 Fix close invoice button for modal invoices #555 2019-01-25 12:41:15 +01:00
12aa0b7abd Merge pull request #556 from ChekaZ/master
Support Bitcoinplus
2019-01-25 16:09:39 +09:00
3f98a50410 Support Bitcoinplus 2019-01-25 01:03:04 +01:00
24c8c076d5 Add taxIncluded field in invoice 2019-01-24 20:53:29 +09:00
37e6931d33 Improve help 2019-01-23 17:44:03 +09:00
146 changed files with 6043 additions and 1466 deletions

View File

@ -5,10 +5,8 @@ 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;
@ -25,6 +23,7 @@ using NBitcoin;
using NBitpayClient;
using Xunit;
using Xunit.Abstractions;
using static BTCPayServer.Tests.UnitTest1;
namespace BTCPayServer.Tests
{
@ -178,6 +177,7 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
user.ModifyStore(s => s.NetworkFeeMode = NetworkFeeMode.Never);
var apps = user.GetController<AppsController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
vm.Name = "test";
@ -185,7 +185,8 @@ namespace BTCPayServer.Tests
Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result);
var appId = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model)
.Apps[0].Id;
Logs.Tester.LogInformation("We create an invoice with a hardcap");
var crowdfundViewModel = Assert.IsType<UpdateCrowdfundViewModel>(Assert
.IsType<ViewResult>(apps.UpdateCrowdfund(appId).Result).Model);
crowdfundViewModel.Enabled = true;
@ -193,6 +194,7 @@ namespace BTCPayServer.Tests
crowdfundViewModel.TargetAmount = 100;
crowdfundViewModel.TargetCurrency = "BTC";
crowdfundViewModel.UseAllStoreInvoices = true;
crowdfundViewModel.EnforceTargetAmount = true;
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
var anonAppPubsController = tester.PayTester.GetController<AppsPublicController>();
@ -209,16 +211,16 @@ namespace BTCPayServer.Tests
Assert.Equal(0m, model.Info.CurrentAmount );
Assert.Equal(0m, model.Info.CurrentPendingAmount);
Assert.Equal(0m, model.Info.ProgressPercentage);
Logs.Tester.LogInformation("Unpaid invoices should show as pending contribution because it is hardcap");
Logs.Tester.LogInformation("Because UseAllStoreInvoices is true, we can manually create an invoice and it should show as contribution");
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
@ -228,19 +230,69 @@ namespace BTCPayServer.Tests
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(0m ,model.Info.CurrentAmount);
Assert.Equal(1m, model.Info.CurrentPendingAmount);
Assert.Equal( 0m, model.Info.ProgressPercentage);
Assert.Equal(0m, model.Info.ProgressPercentage);
Assert.Equal(1m, model.Info.PendingProgressPercentage);
Logs.Tester.LogInformation("Let's check current amount change once payment is confirmed");
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network);
tester.ExplorerNode.SendToAddress(invoiceAddress, invoice.BtcDue);
tester.ExplorerNode.Generate(1); // By default invoice confirmed at 1 block
TestUtils.Eventually(() =>
{
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
.IsType<ViewResult>(publicApps.ViewCrowdfund(appId, String.Empty).Result).Model);
Assert.Equal(1m, model.Info.CurrentAmount);
Assert.Equal(0m, model.Info.CurrentPendingAmount);
});
Logs.Tester.LogInformation("Because UseAllStoreInvoices is true, let's make sure the invoice is tagged");
var invoiceEntity = tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id).GetAwaiter().GetResult();
Assert.True(invoiceEntity.Version >= InvoiceEntity.InternalTagSupport_Version);
Assert.Contains(AppService.GetAppInternalTag(appId), invoiceEntity.InternalTags);
crowdfundViewModel.UseAllStoreInvoices = false;
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
Logs.Tester.LogInformation("Because UseAllStoreInvoices is false, let's make sure the invoice is not tagged");
invoice = user.BitPay.CreateInvoice(new Invoice()
{
Buyer = new Buyer() { email = "test@fwf.com" },
Price = 1m,
Currency = "BTC",
PosData = "posData",
ItemDesc = "Some description",
TransactionSpeed = "high",
FullNotifications = true
}, Facade.Merchant);
invoiceEntity = tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id).GetAwaiter().GetResult();
Assert.DoesNotContain(AppService.GetAppInternalTag(appId), invoiceEntity.InternalTags);
Logs.Tester.LogInformation("After turning setting a softcap, let's check that only actual payments are counted");
crowdfundViewModel.EnforceTargetAmount = false;
crowdfundViewModel.UseAllStoreInvoices = true;
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
invoice = user.BitPay.CreateInvoice(new Invoice()
{
Buyer = new Buyer() { email = "test@fwf.com" },
Price = 1m,
Currency = "BTC",
PosData = "posData",
ItemDesc = "Some description",
TransactionSpeed = "high",
FullNotifications = true
}, Facade.Merchant);
Assert.Equal(0m, model.Info.CurrentPendingAmount);
invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network);
tester.ExplorerNode.SendToAddress(invoiceAddress, Money.Coins(0.5m));
tester.ExplorerNode.SendToAddress(invoiceAddress, Money.Coins(0.2m));
TestUtils.Eventually(() =>
{
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
.IsType<ViewResult>(publicApps.ViewCrowdfund(appId, String.Empty).Result).Model);
Assert.Equal(0.7m, model.Info.CurrentPendingAmount);
});
}

View File

@ -0,0 +1,157 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Models.PaymentRequestViewModels;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Payments.Changelly.Models;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Tests.Logging;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBitpayClient;
using Xunit;
using Xunit.Abstractions;
namespace BTCPayServer.Tests
{
public class PaymentRequestTests
{
public PaymentRequestTests(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact]
[Trait("Integration", "Integration")]
public void CanCreateViewUpdateAndDeletePaymentRequest()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var user2 = tester.NewAccount();
user2.GrantAccess();
var paymentRequestController = user.GetController<PaymentRequestController>();
var guestpaymentRequestController = user2.GetController<PaymentRequestController>();
var request = new UpdatePaymentRequestViewModel()
{
Title = "original juice",
Currency = "BTC",
Amount = 1,
StoreId = user.StoreId,
Description = "description"
};
var id = (Assert
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result).RouteValues.Values.First().ToString());
//permission guard for guests editing
Assert
.IsType<NotFoundResult>(guestpaymentRequestController.EditPaymentRequest(id).Result);
request.Title = "update";
Assert.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(id, request).Result);
Assert.Equal(request.Title, Assert.IsType<ViewPaymentRequestViewModel>( Assert.IsType<ViewResult>(paymentRequestController.ViewPaymentRequest(id).Result).Model).Title);
Assert.False(string.IsNullOrEmpty(id));
Assert.IsType<ViewPaymentRequestViewModel>(Assert
.IsType<ViewResult>(paymentRequestController.ViewPaymentRequest(id).Result).Model);
//Delete
Assert.IsType<ConfirmModel>(Assert
.IsType<ViewResult>(paymentRequestController.RemovePaymentRequestPrompt(id).Result).Model);
Assert.IsType<RedirectToActionResult>(paymentRequestController.RemovePaymentRequest(id).Result);
Assert
.IsType<NotFoundResult>(paymentRequestController.ViewPaymentRequest(id).Result);
}
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanPayPaymentRequestWhenPossible()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var paymentRequestController = user.GetController<PaymentRequestController>();
Assert.IsType<NotFoundResult>(await paymentRequestController.PayPaymentRequest(Guid.NewGuid().ToString()));
var request = new UpdatePaymentRequestViewModel()
{
Title = "original juice",
Currency = "BTC",
Amount = 1,
StoreId = user.StoreId,
Description = "description"
};
var response = Assert
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result)
.RouteValues.First();
var invoiceId = Assert
.IsType<OkObjectResult>(await paymentRequestController.PayPaymentRequest(response.Value.ToString(), false)).Value
.ToString();
var actionResult = Assert
.IsType<RedirectToActionResult>(await paymentRequestController.PayPaymentRequest(response.Value.ToString()));
Assert.Equal("Checkout", actionResult.ActionName);
Assert.Equal("Invoice", actionResult.ControllerName);
Assert.Contains(actionResult.RouteValues, pair => pair.Key == "Id" && pair.Value.ToString() == invoiceId);
var invoice = user.BitPay.GetInvoice(invoiceId, Facade.Merchant);
Assert.Equal(1, invoice.Price);
request = new UpdatePaymentRequestViewModel()
{
Title = "original juice with expiry",
Currency = "BTC",
Amount = 1,
ExpiryDate = DateTime.Today.Subtract( TimeSpan.FromDays(2)),
StoreId = user.StoreId,
Description = "description"
};
response = Assert
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result)
.RouteValues.First();
Assert
.IsType<BadRequestObjectResult>(await paymentRequestController.PayPaymentRequest(response.Value.ToString(), false));
}
}
}
}

View File

@ -859,6 +859,44 @@ namespace BTCPayServer.Tests
Assert.Equal(f1.ToString(), f2.ToString());
}
[Fact]
[Trait("Integration", "Integration")]
public async void CheckCORSSetOnBitpayAPI()
{
using (var tester = ServerTester.Create())
{
tester.Start();
foreach(var req in new[]
{
"invoices/",
"invoices",
"rates",
"tokens"
}.Select(async path =>
{
using (HttpClient client = new HttpClient())
{
HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Options, tester.PayTester.ServerUri.AbsoluteUri + path);
message.Headers.Add("Access-Control-Request-Headers", "test");
var response = await client.SendAsync(message);
response.EnsureSuccessStatusCode();
Assert.True(response.Headers.TryGetValues("Access-Control-Allow-Origin", out var val));
Assert.Equal("*", val.FirstOrDefault());
Assert.True(response.Headers.TryGetValues("Access-Control-Allow-Headers", out val));
Assert.Equal("test", val.FirstOrDefault());
}
}).ToList())
{
await req;
}
HttpClient client2 = new HttpClient();
HttpRequestMessage message2 = new HttpRequestMessage(HttpMethod.Options, tester.PayTester.ServerUri.AbsoluteUri + "rates");
var response2 = await client2.SendAsync(message2);
Assert.True(response2.Headers.TryGetValues("Access-Control-Allow-Origin", out var val2));
Assert.Equal("*", val2.FirstOrDefault());
}
}
[Fact]
[Trait("Integration", "Integration")]
public void TestAccessBitpayAPI()
@ -1260,6 +1298,25 @@ namespace BTCPayServer.Tests
Assert.True(invoice.SupportedTransactionCurrencies["LTC"].Enabled);
Assert.True(invoice.PaymentSubtotals.ContainsKey("LTC"));
Assert.True(invoice.PaymentTotals.ContainsKey("LTC"));
// Check if we can disable LTC
invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true,
SupportedTransactionCurrencies = new Dictionary<string, InvoiceSupportedTransactionCurrency>()
{
{ "BTC", new InvoiceSupportedTransactionCurrency() { Enabled = true } }
}
}, Facade.Merchant);
Assert.Single(invoice.CryptoInfo.Where(c => c.CryptoCode == "BTC"));
Assert.Empty(invoice.CryptoInfo.Where(c => c.CryptoCode == "LTC"));
}
}
@ -1629,18 +1686,16 @@ donation:
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 =>
@ -1663,18 +1718,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>();
@ -1944,31 +1997,48 @@ donation:
var invoice1 = user.BitPay.CreateInvoice(new Invoice()
{
Price = 0.000000012m,
Currency = "BTC",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
Currency = "USD",
FullNotifications = true
}, Facade.Merchant);
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
{
Price = 0.000000019m,
Currency = "BTC",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
Currency = "USD"
}, Facade.Merchant);
Assert.Equal(0.000000012m, invoice1.Price);
Assert.Equal(0.000000019m, invoice2.Price);
// Should round up to 1 because 0.000000019 is unsignificant
var invoice3 = user.BitPay.CreateInvoice(new Invoice()
{
Price = 1.000000019m,
Currency = "USD",
FullNotifications = true
}, Facade.Merchant);
Assert.Equal(0.00000001m, invoice1.Price);
Assert.Equal(0.00000002m, invoice2.Price);
Assert.Equal(1m, invoice3.Price);
// Should not round up at 8 digit because the 9th is insignificant
var invoice4 = user.BitPay.CreateInvoice(new Invoice()
{
Price = 1.000000019m,
Currency = "BTC",
FullNotifications = true
}, Facade.Merchant);
Assert.Equal(1.00000002m, invoice4.Price);
// But not if the 9th is insignificant
invoice4 = user.BitPay.CreateInvoice(new Invoice()
{
Price = 0.000000019m,
Currency = "BTC",
FullNotifications = true
}, Facade.Merchant);
Assert.Equal(0.000000019m, invoice4.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);
@ -1988,6 +2058,7 @@ donation:
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0m,
TaxIncluded = 1000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
@ -2017,6 +2088,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);
@ -2139,19 +2212,20 @@ donation:
}
}
[Fact]
[Trait("Integration", "Integration")]
public void CheckQuadrigacxRateProvider()
{
var quadri = new QuadrigacxRateProvider();
var rates = quadri.GetRatesAsync().GetAwaiter().GetResult();
Assert.NotEmpty(rates);
Assert.NotEqual(0.0m, rates.First().BidAsk.Bid);
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_CAD")).Bid);
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_USD")).Bid);
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("LTC_CAD")).Bid);
Assert.Null(rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("LTC_USD")));
}
//[Fact]
//[Trait("Integration", "Integration")]
// 29 january, the exchange is down
//public void CheckQuadrigacxRateProvider()
//{
// var quadri = new QuadrigacxRateProvider();
// var rates = quadri.GetRatesAsync().GetAwaiter().GetResult();
// Assert.NotEmpty(rates);
// Assert.NotEqual(0.0m, rates.First().BidAsk.Bid);
// Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_CAD")).Bid);
// Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_USD")).Bid);
// Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("LTC_CAD")).Bid);
// Assert.Null(rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("LTC_USD")));
//}
[Fact]
[Trait("Integration", "Integration")]
@ -2165,6 +2239,9 @@ donation:
.Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync(), Fetcher: (BackgroundFetcherRateProvider)p.Value))
.ToList())
{
Logs.Tester.LogInformation($"Testing {result.ExpectedName}");
if (result.ExpectedName == "quadrigacx")
continue; // 29 january, the exchange is down
result.Fetcher.InvalidateCache();
var exchangeRates = result.ResultAsync.Result;
result.Fetcher.InvalidateCache();
@ -2313,6 +2390,42 @@ donation:
Assert.Throws<InvalidOperationException>(() => fetch.GetRatesAsync().GetAwaiter().GetResult());
}
[Fact]
[Trait("Fast", "Fast")]
public void CheckParseStatusMessageModel()
{
var legacyStatus = "Error: some bad shit happened";
var parsed = new StatusMessageModel(legacyStatus);
Assert.Equal(legacyStatus, parsed.Message);
Assert.Equal(StatusMessageModel.StatusSeverity.Error, parsed.Severity);
var legacyStatus2 = "Some normal shit happened";
parsed = new StatusMessageModel(legacyStatus2);
Assert.Equal(legacyStatus2, parsed.Message);
Assert.Equal(StatusMessageModel.StatusSeverity.Success, parsed.Severity);
var newStatus = new StatusMessageModel()
{
Html = "<a href='xxx'>something new</a>",
Severity = StatusMessageModel.StatusSeverity.Info
};
parsed = new StatusMessageModel(newStatus.ToString());
Assert.Null(parsed.Message);
Assert.Equal(newStatus.Html, parsed.Html);
Assert.Equal(StatusMessageModel.StatusSeverity.Info, parsed.Severity);
var newStatus2 = new StatusMessageModel()
{
Message = "something new",
Severity = StatusMessageModel.StatusSeverity.Success
};
parsed = new StatusMessageModel(newStatus2.ToString());
Assert.Null(parsed.Html);
Assert.Equal(newStatus2.Message, parsed.Message);
Assert.Equal(StatusMessageModel.StatusSeverity.Success, parsed.Severity);
}
private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx)
{
var h = BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest).ScriptPubKey.Hash.ToString();

View File

@ -222,7 +222,7 @@ services:
- MYSQL_ALLOW_EMPTY_PASSWORD=yes
merchant_lnd:
image: btcpayserver/lnd:v0.5.1-beta
image: btcpayserver/lnd:v0.5.2-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -252,7 +252,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.5.1-beta
image: btcpayserver/lnd:v0.5.2-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"

View File

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

View File

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

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.3.45</Version>
<Version>1.0.3.68</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<PropertyGroup>
@ -33,7 +33,7 @@
<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.9" />
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.5.3" />
<PackageReference Include="HtmlSanitizer" Version="4.0.199" />
@ -45,8 +45,8 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="NBitcoin" Version="4.1.1.77" />
<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.3" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
@ -139,7 +139,7 @@
<Content Update="Views\Server\LightningChargeServices.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\SparkServices.cshtml">
<Content Update="Views\Server\LightningWalletServices.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\SSHService.cshtml">

View File

@ -109,18 +109,22 @@ namespace BTCPayServer.Configuration
{
if (!LightningConnectionString.TryParse(lightning, true, out var connectionString, out var error))
{
throw new ConfigException($"Invalid setting {net.CryptoCode}.lightning, " + Environment.NewLine +
Logs.Configuration.LogWarning($"Invalid setting {net.CryptoCode}.lightning, " + Environment.NewLine +
$"If you have a c-lightning server use: 'type=clightning;server=/root/.lightning/lightning-rpc', " + Environment.NewLine +
$"If you have a lightning charge server: 'type=charge;server=https://charge.example.com;api-token=yourapitoken'" + Environment.NewLine +
$"If you have a lnd server: 'type=lnd-rest;server=https://lnd:lnd@lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
$" lnd server: 'type=lnd-rest;server=https://lnd:lnd@lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
error);
$"Error: {error}" + Environment.NewLine +
"This service will not be exposed through BTCPay Server");
}
if (connectionString.IsLegacy)
else
{
Logs.Configuration.LogWarning($"Setting {net.CryptoCode}.lightning is a deprecated format, it will work now, but please replace it for future versions with '{connectionString.ToString()}'");
if (connectionString.IsLegacy)
{
Logs.Configuration.LogWarning($"Setting {net.CryptoCode}.lightning is a deprecated format, it will work now, but please replace it for future versions with '{connectionString.ToString()}'");
}
InternalLightningByCryptoCode.Add(net.CryptoCode, connectionString);
}
InternalLightningByCryptoCode.Add(net.CryptoCode, connectionString);
}
}
@ -131,28 +135,57 @@ namespace BTCPayServer.Configuration
{
if (!LightningConnectionString.TryParse(lightning, false, out var connectionString, out var error))
{
throw new ConfigException($"Invalid setting {code}, " + Environment.NewLine +
Logs.Configuration.LogWarning($"Invalid setting {code}, " + Environment.NewLine +
$"lnd server: 'type={lndType};server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
$"lnd server: 'type={lndType};server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
error);
$"Error: {error}" + Environment.NewLine +
"This service will not be exposed through BTCPay Server");
}
else
{
var instanceType = typeof(T);
ExternalServicesByCryptoCode.Add(net.CryptoCode, (ExternalService)Activator.CreateInstance(instanceType, connectionString));
}
var instanceType = typeof(T);
ExternalServicesByCryptoCode.Add(net.CryptoCode, (ExternalService)Activator.CreateInstance(instanceType, connectionString));
}
};
externalLnd<ExternalLndGrpc>($"{net.CryptoCode}.external.lnd.grpc", "lnd-grpc");
externalLnd<ExternalLndRest>($"{net.CryptoCode}.external.lnd.rest", "lnd-rest");
var spark = conf.GetOrDefault<string>($"{net.CryptoCode}.external.spark", string.Empty);
if (spark.Length != 0)
{
if (!SparkConnectionString.TryParse(spark, out var connectionString))
var spark = conf.GetOrDefault<string>($"{net.CryptoCode}.external.spark", string.Empty);
if (spark.Length != 0)
{
throw new ConfigException($"Invalid setting {net.CryptoCode}.external.spark, " + Environment.NewLine +
$"Valid example: 'server=https://btcpay.example.com/spark/btc/;cookiefile=/etc/clightning_bitcoin_spark/.cookie'");
if (!SparkConnectionString.TryParse(spark, out var connectionString, out var error))
{
Logs.Configuration.LogWarning($"Invalid setting {net.CryptoCode}.external.spark, " + Environment.NewLine +
$"Valid example: 'server=https://btcpay.example.com/spark/btc/;cookiefile=/etc/clightning_bitcoin_spark/.cookie'" + Environment.NewLine +
$"Error: {error}" + Environment.NewLine +
"This service will not be exposed through BTCPay Server");
}
else
{
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalSpark(connectionString));
}
}
}
{
var rtl = conf.GetOrDefault<string>($"{net.CryptoCode}.external.rtl", string.Empty);
if (rtl.Length != 0)
{
if (!SparkConnectionString.TryParse(rtl, out var connectionString, out var error))
{
Logs.Configuration.LogWarning($"Invalid setting {net.CryptoCode}.external.rtl, " + Environment.NewLine +
$"Valid example: 'server=https://btcpay.example.com/rtl/btc/;cookiefile=/etc/clightning_bitcoin_rtl/.cookie'" + Environment.NewLine +
$"Error: {error}" + Environment.NewLine +
"This service will not be exposed through BTCPay Server");
}
else
{
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalRTL(connectionString));
}
}
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalSpark(connectionString));
}
var charge = conf.GetOrDefault<string>($"{net.CryptoCode}.external.charge", string.Empty);
@ -161,14 +194,18 @@ namespace BTCPayServer.Configuration
if (!LightningConnectionString.TryParse(charge, false, out var chargeConnectionString, out var chargeError))
LightningConnectionString.TryParse("type=charge;" + charge, false, out chargeConnectionString, out chargeError);
if(chargeConnectionString == null || chargeConnectionString.ConnectionType != LightningConnectionType.Charge)
if (chargeConnectionString == null || chargeConnectionString.ConnectionType != LightningConnectionType.Charge)
{
throw new ConfigException($"Invalid setting {net.CryptoCode}.external.charge, " + Environment.NewLine +
Logs.Configuration.LogWarning($"Invalid setting {net.CryptoCode}.external.charge, " + Environment.NewLine +
$"lightning charge server: 'type=charge;server=https://charge.example.com;api-token=2abdf302...'" + Environment.NewLine +
$"lightning charge server: 'type=charge;server=https://charge.example.com;cookiefilepath=/root/.charge/.cookie'" + Environment.NewLine +
chargeError ?? string.Empty);
$"Error: {chargeError ?? string.Empty}" + Environment.NewLine +
$"This service will not be exposed through BTCPay Server");
}
else
{
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalCharge(chargeConnectionString));
}
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalCharge(chargeConnectionString));
}
}
@ -190,7 +227,6 @@ namespace BTCPayServer.Configuration
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
MySQLConnectionString = conf.GetOrDefault<string>("mysql", null);
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
ExternalUrl = conf.GetOrDefault<Uri>("externalurl", null);
var sshSettings = ParseSSHConfiguration(conf);
if ((!string.IsNullOrEmpty(sshSettings.Password) || !string.IsNullOrEmpty(sshSettings.KeyFile)) && !string.IsNullOrEmpty(sshSettings.Server))
@ -311,11 +347,6 @@ namespace BTCPayServer.Configuration
get;
set;
}
public Uri ExternalUrl
{
get;
set;
}
public bool BundleJsCss
{
get;
@ -327,14 +358,5 @@ namespace BTCPayServer.Configuration
get;
set;
}
internal string GetRootUri()
{
if (ExternalUrl == null)
return null;
UriBuilder builder = new UriBuilder(ExternalUrl);
builder.Path = RootPath;
return builder.ToString();
}
}
}

View File

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Configuration.External
{
public class ExternalRTL : ExternalService, IAccessKeyService
{
public SparkConnectionString ConnectionString { get; }
public ExternalRTL(SparkConnectionString connectionString)
{
if (connectionString == null)
throw new ArgumentNullException(nameof(connectionString));
ConnectionString = connectionString;
}
public async Task<string> ExtractAccessKey()
{
if (ConnectionString?.CookeFile == null)
throw new FormatException("Invalid connection string");
return await System.IO.File.ReadAllTextAsync(ConnectionString.CookeFile);
}
}
}

View File

@ -5,7 +5,12 @@ using System.Threading.Tasks;
namespace BTCPayServer.Configuration.External
{
public class ExternalSpark : ExternalService
public interface IAccessKeyService
{
SparkConnectionString ConnectionString { get; }
Task<string> ExtractAccessKey();
}
public class ExternalSpark : ExternalService, IAccessKeyService
{
public SparkConnectionString ConnectionString { get; }
@ -15,5 +20,19 @@ namespace BTCPayServer.Configuration.External
throw new ArgumentNullException(nameof(connectionString));
ConnectionString = connectionString;
}
public async Task<string> ExtractAccessKey()
{
if (ConnectionString?.CookeFile == null)
throw new FormatException("Invalid connection string");
var cookie = (ConnectionString.CookeFile == "fake"
? "fake:fake:fake" // Hacks for testing
: await System.IO.File.ReadAllTextAsync(ConnectionString.CookeFile)).Split(':');
if (cookie.Length >= 3)
{
return cookie[2];
}
throw new FormatException("Invalid cookiefile format");
}
}
}

View File

@ -10,11 +10,11 @@ namespace BTCPayServer.Configuration
public Uri Server { get; private set; }
public string CookeFile { get; private set; }
public static bool TryParse(string str, out SparkConnectionString result)
public static bool TryParse(string str, out SparkConnectionString result, out string error)
{
if (str == null)
throw new ArgumentNullException(nameof(str));
error = null;
result = null;
var resultTemp = new SparkConnectionString();
foreach(var kv in str.Split(';')
@ -25,15 +25,30 @@ namespace BTCPayServer.Configuration
{
case "server":
if (resultTemp.Server != null)
{
error = "Duplicated server attribute";
return false;
}
if (!Uri.IsWellFormedUriString(kv[1], UriKind.Absolute))
{
error = "Invalid URI";
return false;
}
resultTemp.Server = new Uri(kv[1], UriKind.Absolute);
if(resultTemp.Server.Scheme == "http")
{
error = "Insecure transport protocol (http)";
return false;
}
break;
case "cookiefile":
case "cookiefilepath":
if (resultTemp.CookeFile != null)
{
error = "Duplicated cookiefile attribute";
return false;
}
resultTemp.CookeFile = kv[1];
break;
default:

View File

@ -2,6 +2,7 @@
using BTCPayServer.Filters;
using BTCPayServer.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using NBitcoin.DataEncoders;
using NBitpayClient;
@ -13,7 +14,7 @@ using System.Threading.Tasks;
namespace BTCPayServer.Controllers
{
[Authorize(AuthenticationSchemes = Security.Policies.BitpayAuthentication)]
[BitpayAPIConstraint(true)]
[BitpayAPIConstraint()]
public class AccessTokenController : Controller
{
TokenRepository _TokenRepository;

View File

@ -10,42 +10,15 @@ namespace BTCPayServer.Controllers
{
public partial class AppsController
{
public class CrowdfundAppUpdated
public class AppUpdated
{
public string AppId { get; set; }
public CrowdfundSettings Settings { get; set; }
public object 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; }
public override string ToString()
{
return String.Empty;
}
}
@ -77,11 +50,11 @@ namespace BTCPayServer.Controllers
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,
UseAllStoreInvoices = app.TagAllInvoices,
AppId = appId,
SearchTerm = app.TagAllInvoices ? $"storeid:{app.StoreDataId}" : $"orderid:{AppService.GetCrowdfundOrderId(appId)}",
DisplayPerksRanking = settings.DisplayPerksRanking,
SortPerksByPopularity = settings.SortPerksByPopularity
};
@ -91,12 +64,12 @@ namespace BTCPayServer.Controllers
[Route("{appId}/settings/crowdfund")]
public async Task<IActionResult> UpdateCrowdfund(string appId, UpdateCrowdfundViewModel vm)
{
if (!string.IsNullOrEmpty( vm.TargetCurrency) && _AppsHelper.GetCurrencyData(vm.TargetCurrency, false) == null)
if (!string.IsNullOrEmpty( vm.TargetCurrency) && _currencies.GetCurrencyData(vm.TargetCurrency, false) == null)
ModelState.AddModelError(nameof(vm.TargetCurrency), "Invalid currency");
try
{
_AppsHelper.Parse(vm.PerksTemplate, vm.TargetCurrency).ToString();
_AppService.Parse(vm.PerksTemplate, vm.TargetCurrency).ToString();
}
catch
{
@ -135,7 +108,7 @@ namespace BTCPayServer.Controllers
EnforceTargetAmount = vm.EnforceTargetAmount,
StartDate = vm.StartDate,
TargetCurrency = vm.TargetCurrency,
Description = _AppsHelper.Sanitize( vm.Description),
Description = _htmlSanitizer.Sanitize( vm.Description),
EndDate = vm.EndDate,
TargetAmount = vm.TargetAmount,
CustomCSSLink = vm.CustomCSSLink,
@ -150,15 +123,15 @@ namespace BTCPayServer.Controllers
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.TagAllInvoices = vm.UseAllStoreInvoices;
app.SetSettings(newSettings);
await UpdateAppSettings(app);
_EventAggregator.Publish(new CrowdfundAppUpdated()
_EventAggregator.Publish(new AppUpdated()
{
AppId = appId,
StoreId = app.StoreDataId,

View File

@ -55,12 +55,16 @@ namespace BTCPayServer.Controllers
" custom: true";
EnableShoppingCart = false;
ShowCustomAmount = true;
ShowDiscount = true;
EnableTips = true;
}
public string Title { get; set; }
public string Currency { get; set; }
public string Template { get; set; }
public bool EnableShoppingCart { get; set; }
public bool ShowCustomAmount { get; set; }
public bool ShowDiscount { get; set; }
public bool EnableTips { get; set; }
public const string BUTTON_TEXT_DEF = "Buy for {0}";
public string ButtonText { get; set; } = BUTTON_TEXT_DEF;
@ -85,9 +89,12 @@ namespace BTCPayServer.Controllers
var settings = app.GetSettings<PointOfSaleSettings>();
var vm = new UpdatePointOfSaleViewModel()
{
Id = appId,
Title = settings.Title,
EnableShoppingCart = settings.EnableShoppingCart,
ShowCustomAmount = settings.ShowCustomAmount,
ShowDiscount = settings.ShowDiscount,
EnableTips = settings.EnableTips,
Currency = settings.Currency,
Template = settings.Template,
ButtonText = settings.ButtonText ?? PointOfSaleSettings.BUTTON_TEXT_DEF,
@ -115,7 +122,7 @@ namespace BTCPayServer.Controllers
}
try
{
var items = _AppsHelper.Parse(settings.Template, settings.Currency);
var items = _AppService.Parse(settings.Template, settings.Currency);
var builder = new StringBuilder();
builder.AppendLine($"<form method=\"POST\" action=\"{encoder.Encode(appUrl)}\">");
builder.AppendLine($" <input type=\"hidden\" name=\"email\" value=\"customer@example.com\" />");
@ -137,11 +144,11 @@ namespace BTCPayServer.Controllers
[Route("{appId}/settings/pos")]
public async Task<IActionResult> UpdatePointOfSale(string appId, UpdatePointOfSaleViewModel vm)
{
if (_AppsHelper.GetCurrencyData(vm.Currency, false) == null)
if (_currencies.GetCurrencyData(vm.Currency, false) == null)
ModelState.AddModelError(nameof(vm.Currency), "Invalid currency");
try
{
_AppsHelper.Parse(vm.Template, vm.Currency);
_AppService.Parse(vm.Template, vm.Currency);
}
catch
{
@ -159,6 +166,8 @@ namespace BTCPayServer.Controllers
Title = vm.Title,
EnableShoppingCart = vm.EnableShoppingCart,
ShowCustomAmount = vm.ShowCustomAmount,
ShowDiscount = vm.ShowDiscount,
EnableTips = vm.EnableTips,
Currency = vm.Currency.ToUpperInvariant(),
Template = vm.Template,
ButtonText = vm.ButtonText,
@ -179,6 +188,7 @@ namespace BTCPayServer.Controllers
ctx.Apps.Add(app);
ctx.Entry<AppData>(app).State = EntityState.Modified;
ctx.Entry<AppData>(app).Property(a => a.Settings).IsModified = true;
ctx.Entry<AppData>(app).Property(a => a.TagAllInvoices).IsModified = true;
await ctx.SaveChangesAsync();
}
}

View File

@ -7,6 +7,8 @@ using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Security;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Rates;
using Ganss.XSS;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
@ -26,20 +28,26 @@ namespace BTCPayServer.Controllers
ApplicationDbContextFactory contextFactory,
EventAggregator eventAggregator,
BTCPayNetworkProvider networkProvider,
AppsHelper appsHelper)
CurrencyNameTable currencies,
HtmlSanitizer htmlSanitizer,
AppService AppService)
{
_UserManager = userManager;
_ContextFactory = contextFactory;
_EventAggregator = eventAggregator;
_NetworkProvider = networkProvider;
_AppsHelper = appsHelper;
_currencies = currencies;
_htmlSanitizer = htmlSanitizer;
_AppService = AppService;
}
private UserManager<ApplicationUser> _UserManager;
private ApplicationDbContextFactory _ContextFactory;
private readonly EventAggregator _EventAggregator;
private BTCPayNetworkProvider _NetworkProvider;
private AppsHelper _AppsHelper;
private readonly CurrencyNameTable _currencies;
private readonly HtmlSanitizer _htmlSanitizer;
private AppService _AppService;
[TempData]
public string StatusMessage { get; set; }
@ -47,7 +55,7 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> ListApps()
{
var apps = await _AppsHelper.GetAllApps(GetUserId());
var apps = await _AppService.GetAllApps(GetUserId());
return View(new ListAppsViewModel()
{
Apps = apps
@ -61,7 +69,7 @@ namespace BTCPayServer.Controllers
var appData = await GetOwnedApp(appId);
if (appData == null)
return NotFound();
if (await _AppsHelper.DeleteApp(appData))
if (await _AppService.DeleteApp(appData))
StatusMessage = "App removed successfully";
return RedirectToAction(nameof(ListApps));
}
@ -70,10 +78,15 @@ namespace BTCPayServer.Controllers
[Route("create")]
public async Task<IActionResult> CreateApp()
{
var stores = await _AppsHelper.GetOwnedStores(GetUserId());
var stores = await _AppService.GetOwnedStores(GetUserId());
if (stores.Length == 0)
{
StatusMessage = "Error: You must have created at least one store";
StatusMessage = new StatusMessageModel()
{
Html =
$"Error: You must have created at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}'>Create store</a>",
Severity = StatusMessageModel.StatusSeverity.Error
}.ToString();
return RedirectToAction(nameof(ListApps));
}
var vm = new CreateAppViewModel();
@ -85,10 +98,15 @@ namespace BTCPayServer.Controllers
[Route("create")]
public async Task<IActionResult> CreateApp(CreateAppViewModel vm)
{
var stores = await _AppsHelper.GetOwnedStores(GetUserId());
var stores = await _AppService.GetOwnedStores(GetUserId());
if (stores.Length == 0)
{
StatusMessage = "Error: You must own at least one store";
StatusMessage = new StatusMessageModel()
{
Html =
$"Error: You must have created at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}'>Create store</a>",
Severity = StatusMessageModel.StatusSeverity.Error
}.ToString();
return RedirectToAction(nameof(ListApps));
}
var selectedStore = vm.SelectedStore;
@ -150,7 +168,7 @@ namespace BTCPayServer.Controllers
private Task<AppData> GetOwnedApp(string appId, AppType? type = null)
{
return _AppsHelper.GetAppDataIfOwner(GetUserId(), appId, type);
return _AppService.GetAppDataIfOwner(GetUserId(), appId, type);
}

View File

@ -6,19 +6,17 @@ 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.Payments;
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;
@ -32,32 +30,30 @@ namespace BTCPayServer.Controllers
{
public class AppsPublicController : Controller
{
public AppsPublicController(AppsHelper appsHelper,
InvoiceController invoiceController,
CrowdfundHubStreamer crowdfundHubStreamer, UserManager<ApplicationUser> userManager)
public AppsPublicController(AppService AppService,
InvoiceController invoiceController,
UserManager<ApplicationUser> userManager)
{
_AppsHelper = appsHelper;
_AppService = AppService;
_InvoiceController = invoiceController;
_CrowdfundHubStreamer = crowdfundHubStreamer;
_UserManager = userManager;
}
private AppsHelper _AppsHelper;
private AppService _AppService;
private InvoiceController _InvoiceController;
private readonly CrowdfundHubStreamer _CrowdfundHubStreamer;
private readonly UserManager<ApplicationUser> _UserManager;
[HttpGet]
[Route("/apps/{appId}/pos")]
[XFrameOptionsAttribute(null)]
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
public async Task<IActionResult> ViewPointOfSale(string appId)
{
var app = await _AppsHelper.GetApp(appId, AppType.PointOfSale);
var app = await _AppService.GetApp(appId, AppType.PointOfSale);
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
var numberFormatInfo = _AppsHelper.Currencies.GetNumberFormatInfo(settings.Currency) ?? _AppsHelper.Currencies.GetNumberFormatInfo("USD");
var numberFormatInfo = _AppService.Currencies.GetNumberFormatInfo(settings.Currency) ?? _AppService.Currencies.GetNumberFormatInfo("USD");
double step = Math.Pow(10, -(numberFormatInfo.CurrencyDecimalDigits));
return View(new ViewPointOfSaleViewModel()
@ -66,6 +62,8 @@ namespace BTCPayServer.Controllers
Step = step.ToString(CultureInfo.InvariantCulture),
EnableShoppingCart = settings.EnableShoppingCart,
ShowCustomAmount = settings.ShowCustomAmount,
ShowDiscount = settings.ShowDiscount,
EnableTips = settings.EnableTips,
CurrencyCode = settings.Currency,
CurrencySymbol = numberFormatInfo.CurrencySymbol,
CurrencyInfo = new ViewPointOfSaleViewModel.CurrencyInfoData()
@ -77,7 +75,7 @@ namespace BTCPayServer.Controllers
Prefixed = new[] { 0, 2 }.Contains(numberFormatInfo.CurrencyPositivePattern),
SymbolSpace = new[] { 2, 3 }.Contains(numberFormatInfo.CurrencyPositivePattern)
},
Items = _AppsHelper.Parse(settings.Template, settings.Currency),
Items = _AppService.Parse(settings.Template, settings.Currency),
ButtonText = settings.ButtonText,
CustomButtonText = settings.CustomButtonText,
CustomTipText = settings.CustomTipText,
@ -90,17 +88,17 @@ namespace BTCPayServer.Controllers
[HttpGet]
[Route("/apps/{appId}/crowdfund")]
[XFrameOptionsAttribute(null)]
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
public async Task<IActionResult> ViewCrowdfund(string appId, string statusMessage)
{
var app = await _AppsHelper.GetApp(appId, AppType.Crowdfund, true);
var app = await _AppService.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 isAdmin = await _AppService.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
var hasEnoughSettingsToLoad = !string.IsNullOrEmpty(settings.TargetCurrency );
if (!hasEnoughSettingsToLoad)
@ -110,55 +108,55 @@ namespace BTCPayServer.Controllers
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 (settings.Enabled) return View(await _AppService.GetAppInfo(appId));
if(!isAdmin)
return NotFound();
return View(await _CrowdfundHubStreamer.GetCrowdfundInfo(appId));
return View(await _AppService.GetAppInfo(appId));
}
[HttpPost]
[Route("/apps/{appId}/crowdfund")]
[XFrameOptionsAttribute(null)]
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
public async Task<IActionResult> ContributeToCrowdfund(string appId, ContributeToCrowdfund request)
{
var app = await _AppsHelper.GetApp(appId, AppType.Crowdfund, true);
var app = await _AppService.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 isAdmin = await _AppService.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
if (!settings.Enabled)
{
if(!isAdmin)
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)))
var info = (ViewCrowdfundViewModel)await _AppService.GetAppInfo(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 store = await _AppService.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);
var choice = choices.FirstOrDefault(c => c.Id == request.ChoiceKey);
var choices = _AppService.Parse(settings.PerksTemplate, settings.TargetCurrency);
choice = choices.FirstOrDefault(c => c.Id == request.ChoiceKey);
if (choice == null)
return NotFound("Incorrect option provided");
title = choice.Title;
@ -168,41 +166,47 @@ namespace BTCPayServer.Controllers
}
if (!isAdmin && (settings.EnforceTargetAmount && info.TargetAmount.HasValue && price >
(info.TargetAmount - (info.Info.CurrentAmount + info.Info.CurrentPendingAmount))))
(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()
try
{
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});
var invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
{
OrderId = AppService.GetCrowdfundOrderId(appId),
Currency = settings.TargetCurrency,
ItemCode = request.ChoiceKey ?? string.Empty,
ItemDesc = title,
BuyerEmail = request.Email,
Price = price,
NotificationURL = settings.NotificationUrl,
FullNotifications = true,
ExtendedNotifications = true,
RedirectURL = request.RedirectUrl ?? Request.GetDisplayUrl()
}, store, HttpContext.Request.GetAbsoluteRoot(), new List<string> { AppService.GetAppInternalTag(appId) });
if (request.RedirectToCheckout)
{
return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice",
new {invoiceId = invoice.Data.Id});
}
else
{
return Ok(invoice.Data.Id);
}
}
else
catch (BitpayHttpException e)
{
return Ok(invoice.Data.Id);
return BadRequest(e.Message);
}
}
[HttpPost]
[Route("/apps/{appId}/pos")]
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
public async Task<IActionResult> ViewPointOfSale(string appId,
@ -211,9 +215,10 @@ namespace BTCPayServer.Controllers
string orderId,
string notificationUrl,
string redirectUrl,
string choiceKey)
string choiceKey,
string posData = null)
{
var app = await _AppsHelper.GetApp(appId, AppType.PointOfSale);
var app = await _AppService.GetApp(appId, AppType.PointOfSale);
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
@ -227,10 +232,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);
var choices = _AppService.Parse(settings.Template, settings.Currency);
choice = choices.FirstOrDefault(c => c.Id == choiceKey);
if (choice == null)
return NotFound();
title = choice.Title;
@ -245,11 +251,11 @@ namespace BTCPayServer.Controllers
price = amount;
title = settings.Title;
}
var store = await _AppsHelper.GetStore(app);
var store = await _AppService.GetStore(app);
store.AdditionalClaims.Add(new Claim(Policies.CanCreateInvoice.Key, store.Id));
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
var invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
{
ItemCode = choiceKey ?? string.Empty,
ItemCode = choice?.Id,
ItemDesc = title,
Currency = settings.Currency,
Price = price,
@ -258,6 +264,7 @@ namespace BTCPayServer.Controllers
NotificationURL = notificationUrl,
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 });
}
@ -268,218 +275,4 @@ namespace BTCPayServer.Controllers
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 string Sanitize(string raw)
{
return _HtmlSanitizer.Sanitize(raw);
}
public async Task<StoreData[]> GetOwnedStores(string userId)
{
using (var ctx = _ContextFactory.CreateContext())
{
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();
}
}
public async Task<StoreData> GetStore(AppData app)
{
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.Stores.FirstOrDefaultAsync(s => s.Id == app.StoreDataId);
}
}
public ViewPointOfSaleViewModel.Item[] Parse(string template, string currency)
{
if (string.IsNullOrWhiteSpace(template))
return Array.Empty<ViewPointOfSaleViewModel.Item>();
var input = new StringReader(template);
YamlStream stream = new YamlStream();
stream.Load(input);
var root = (YamlMappingNode)stream.Documents[0].RootNode;
return root
.Children
.Select(kv => new PosHolder { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlMappingNode })
.Where(kv => kv.Value != null)
.Select(c => new ViewPointOfSaleViewModel.Item()
{
Description = Sanitize(c.GetDetailString("description")),
Id = c.Key,
Image = Sanitize(c.GetDetailString("image")),
Title = Sanitize(c.GetDetailString("title") ?? c.Key),
Price = c.GetDetail("price")
.Select(cc => new ViewPointOfSaleViewModel.Item.ItemPrice()
{
Value = decimal.Parse(cc.Value.Value, CultureInfo.InvariantCulture),
Formatted = FormatCurrency(cc.Value.Value, currency)
}).Single(),
Custom = c.GetDetailString("custom") == "true"
})
.ToArray();
}
private class PosHolder
{
public string Key { get; set; }
public YamlMappingNode Value { get; set; }
public IEnumerable<PosScalar> GetDetail(string field)
{
var res = Value.Children
.Where(kv => kv.Value != null)
.Select(kv => new PosScalar { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlScalarNode })
.Where(cc => cc.Key == field);
return res;
}
public string GetDetailString(string field)
{
return GetDetail(field).FirstOrDefault()?.Value?.Value;
}
}
private class PosScalar
{
public string Key { get; set; }
public YamlScalarNode Value { get; set; }
}
public string FormatCurrency(string price, string currency)
{
return decimal.Parse(price, CultureInfo.InvariantCulture).ToString("C", _Currencies.GetCurrencyProvider(currency));
}
public CurrencyData GetCurrencyData(string currency, bool useFallback)
{
return _Currencies.GetCurrencyData(currency, useFallback);
}
public async Task<AppData> GetAppDataIfOwner(string userId, string appId, AppType? type = null)
{
if (userId == null || appId == null)
return null;
using (var ctx = _ContextFactory.CreateContext())
{
var app = await ctx.UserStore
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
.SelectMany(us => us.StoreData.Apps.Where(a => a.Id == appId))
.FirstOrDefaultAsync();
if (app == null)
return null;
if (type != null && type.Value.ToString() != app.AppType)
return null;
return app;
}
}
}
}

View File

@ -12,7 +12,6 @@ using NBitpayClient;
namespace BTCPayServer.Controllers
{
[EnableCors("BitpayAPI")]
[BitpayAPIConstraint]
[Authorize(Policies.CanCreateInvoice.Key, AuthenticationSchemes = Policies.BitpayAuthentication)]
public class InvoiceControllerAPI : Controller
@ -33,8 +32,10 @@ namespace BTCPayServer.Controllers
[HttpPost]
[Route("invoices")]
[MediaTypeConstraint("application/json")]
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] Invoice invoice)
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] CreateInvoiceRequest invoice)
{
if (invoice == null)
throw new BitpayHttpException(400, "Invalid invoice");
return await _InvoiceController.CreateInvoiceCore(invoice, HttpContext.GetStoreData(), HttpContext.Request.GetAbsoluteRoot());
}

View File

@ -21,6 +21,7 @@ using BTCPayServer.Services.Invoices.Export;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore.Internal;
using NBitcoin;
using NBitpayClient;
using NBXplorer;
@ -64,6 +65,7 @@ namespace BTCPayServer.Controllers
OrderId = invoice.OrderId,
BuyerInformation = invoice.BuyerInformation,
Fiat = _CurrencyNameTable.DisplayFormatCurrency(dto.Price, dto.Currency),
TaxIncluded = _CurrencyNameTable.DisplayFormatCurrency(invoice.ProductInformation.TaxIncluded, dto.Currency),
NotificationEmail = invoice.NotificationEmail,
NotificationUrl = invoice.NotificationURL,
RedirectUrl = invoice.RedirectURL,
@ -80,9 +82,9 @@ namespace BTCPayServer.Controllers
var paymentMethodId = data.GetId();
var cryptoPayment = new InvoiceDetailsModel.CryptoPayment();
cryptoPayment.PaymentMethod = ToString(paymentMethodId);
cryptoPayment.Due = $"{accounting.Due} {paymentMethodId.CryptoCode}";
cryptoPayment.Paid = $"{accounting.CryptoPaid} {paymentMethodId.CryptoCode}";
cryptoPayment.Overpaid = $"{accounting.OverpaidHelper} {paymentMethodId.CryptoCode}";
cryptoPayment.Due = _CurrencyNameTable.DisplayFormatCurrency(accounting.Due.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode);
cryptoPayment.Paid = _CurrencyNameTable.DisplayFormatCurrency(accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode);
cryptoPayment.Overpaid = _CurrencyNameTable.DisplayFormatCurrency(accounting.OverpaidHelper.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode);
var onchainMethod = data.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod;
if (onchainMethod != null)
@ -188,7 +190,7 @@ namespace BTCPayServer.Controllers
id = invoiceId;
////
var model = await GetInvoiceModel(invoiceId, paymentMethodId);
var model = await GetInvoiceModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId));
if (model == null)
return NotFound();
@ -211,31 +213,29 @@ namespace BTCPayServer.Controllers
return View(nameof(Checkout), model);
}
private async Task<PaymentModel> GetInvoiceModel(string invoiceId, string paymentMethodIdStr)
private async Task<PaymentModel> GetInvoiceModel(string invoiceId, PaymentMethodId paymentMethodId)
{
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
if (invoice == null)
return null;
var store = await _StoreRepository.FindStore(invoice.StoreId);
bool isDefaultCrypto = false;
if (paymentMethodIdStr == null)
bool isDefaultPaymentId = false;
if (paymentMethodId == null)
{
paymentMethodIdStr = store.GetDefaultCrypto(_NetworkProvider);
isDefaultCrypto = true;
paymentMethodId = store.GetDefaultPaymentId(_NetworkProvider);
isDefaultPaymentId = true;
}
var paymentMethodId = PaymentMethodId.Parse(paymentMethodIdStr);
var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode);
if (network == null && isDefaultCrypto)
if (network == null && isDefaultPaymentId)
{
network = _NetworkProvider.GetAll().FirstOrDefault();
paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
paymentMethodIdStr = paymentMethodId.ToString();
}
if (invoice == null || network == null)
return null;
if (!invoice.Support(paymentMethodId))
{
if (!isDefaultCrypto)
if (!isDefaultPaymentId)
return null;
var paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider)
.Where(c => paymentMethodId.CryptoCode == c.GetId().CryptoCode)
@ -244,7 +244,6 @@ namespace BTCPayServer.Controllers
paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider).First();
network = paymentMethodTemp.Network;
paymentMethodId = paymentMethodTemp.GetId();
paymentMethodIdStr = paymentMethodId.ToString();
}
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId, _NetworkProvider);
@ -278,7 +277,6 @@ namespace BTCPayServer.Controllers
PaymentMethodName = GetDisplayName(paymentMethodId, network),
CryptoImage = GetImage(paymentMethodId, network),
IsLightning = paymentMethodId.PaymentType == PaymentTypes.LightningLike,
ServerUrl = HttpContext.Request.GetAbsoluteRoot(),
OrderId = invoice.OrderId,
InvoiceId = invoice.Id,
DefaultLang = storeBlob.DefaultLang ?? "en",
@ -369,9 +367,12 @@ namespace BTCPayServer.Controllers
[HttpGet]
[Route("i/{invoiceId}/status")]
[Route("i/{invoiceId}/{paymentMethodId}/status")]
[Route("invoice/{invoiceId}/status")]
[Route("invoice/{invoiceId}/{paymentMethodId}/status")]
[Route("invoice/status")]
public async Task<IActionResult> GetStatus(string invoiceId, string paymentMethodId = null)
{
var model = await GetInvoiceModel(invoiceId, paymentMethodId);
var model = await GetInvoiceModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId));
if (model == null)
return NotFound();
return Json(model);
@ -379,6 +380,10 @@ namespace BTCPayServer.Controllers
[HttpGet]
[Route("i/{invoiceId}/status/ws")]
[Route("i/{invoiceId}/{paymentMethodId}/status/ws")]
[Route("invoice/{invoiceId}/status/ws")]
[Route("invoice/{invoiceId}/{paymentMethodId}/status")]
[Route("invoice/status/ws")]
public async Task<IActionResult> GetStatusWebSocket(string invoiceId)
{
if (!HttpContext.WebSockets.IsWebSocketRequest)
@ -424,6 +429,7 @@ namespace BTCPayServer.Controllers
[HttpPost]
[Route("i/{invoiceId}/UpdateCustomer")]
[Route("invoice/UpdateCustomer")]
public async Task<IActionResult> UpdateCustomer(string invoiceId, [FromBody]UpdateCustomerModel data)
{
if (!ModelState.IsValid)
@ -452,6 +458,7 @@ namespace BTCPayServer.Controllers
invoiceQuery.Count = count;
invoiceQuery.Skip = skip;
var list = await _InvoiceRepository.GetInvoices(invoiceQuery);
foreach (var invoice in list)
{
var state = invoice.GetInvoiceState();
@ -463,7 +470,7 @@ namespace BTCPayServer.Controllers
InvoiceId = invoice.Id,
OrderId = invoice.OrderId ?? string.Empty,
RedirectUrl = invoice.RedirectURL ?? string.Empty,
AmountCurrency = $"{invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture)} {invoice.ProductInformation.Currency}",
AmountCurrency = _CurrencyNameTable.DisplayFormatCurrency(invoice.ProductInformation.Price, invoice.ProductInformation.Currency),
CanMarkInvalid = state.CanMarkInvalid(),
CanMarkComplete = state.CanMarkComplete()
});
@ -571,7 +578,7 @@ namespace BTCPayServer.Controllers
try
{
var result = await CreateInvoiceCore(new Invoice()
var result = await CreateInvoiceCore(new CreateInvoiceRequest()
{
Price = model.Amount.Value,
Currency = model.Currency,
@ -653,13 +660,13 @@ namespace BTCPayServer.Controllers
if (newState == "invalid")
{
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId);
_EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1008, InvoiceEvent.MarkedInvalid));
_EventAggregator.Publish(new InvoiceEvent(invoice, 1008, InvoiceEvent.MarkedInvalid));
StatusMessage = "Invoice marked invalid";
}
else if(newState == "complete")
{
await _InvoiceRepository.UpdatePaidInvoiceToComplete(invoiceId);
_EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 2008, InvoiceEvent.MarkedCompleted));
_EventAggregator.Publish(new InvoiceEvent(invoice, 2008, InvoiceEvent.MarkedCompleted));
StatusMessage = "Invoice marked complete";
}
return RedirectToAction(nameof(ListInvoices));
@ -679,9 +686,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;
@ -689,7 +696,6 @@ namespace BTCPayServer.Controllers
try
{
var jObject =JObject.Parse(posData);
foreach (var item in jObject)
{
@ -697,7 +703,14 @@ namespace BTCPayServer.Controllers
switch (item.Value.Type)
{
case JTokenType.Array:
result.Add(item.Key, string.Join(',', item.Value.AsEnumerable()));
var items = item.Value.AsEnumerable().ToList();
for (var i = 0; i < items.Count(); i++)
{
result.Add($"{item.Key}[{i}]", ParsePosData(items[i].ToString()));
}
break;
case JTokenType.Object:
result.Add(item.Key, ParsePosData(item.Value.ToString()));
break;
default:
result.Add(item.Key, item.Value.ToString());

View File

@ -11,6 +11,7 @@ using BTCPayServer.Models;
using BTCPayServer.Payments;
using BTCPayServer.Rating;
using BTCPayServer.Security;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
@ -61,7 +62,7 @@ namespace BTCPayServer.Controllers
}
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl)
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string> additionalTags = null)
{
if (!store.HasClaim(Policies.CanCreateInvoice.Key))
throw new UnauthorizedAccessException();
@ -69,12 +70,12 @@ namespace BTCPayServer.Controllers
logs.Write("Creation of invoice starting");
var entity = new InvoiceEntity
{
Version = InvoiceEntity.Lastest_Version,
InvoiceTime = DateTimeOffset.UtcNow
};
var getAppsTaggingStore = _InvoiceRepository.GetAppsTaggingStore(store.Id);
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 ?
notificationUri = null;
EmailAddressAttribute emailValidator = new EmailAddressAttribute();
entity.ExpirationTime = entity.InvoiceTime.AddMinutes(storeBlob.InvoiceExpiration);
entity.MonitoringExpiration = entity.ExpirationTime + TimeSpan.FromMinutes(storeBlob.MonitoringExpiration);
@ -82,10 +83,17 @@ namespace BTCPayServer.Controllers
entity.ServerUrl = serverUrl;
entity.FullNotifications = invoice.FullNotifications || invoice.ExtendedNotifications;
entity.ExtendedNotifications = invoice.ExtendedNotifications;
entity.NotificationURL = notificationUri?.AbsoluteUri;
if (invoice.NotificationURL != null &&
Uri.TryCreate(invoice.NotificationURL, UriKind.Absolute, out var notificationUri) &&
(notificationUri.Scheme == "http" || notificationUri.Scheme == "https"))
{
entity.NotificationURL = notificationUri.AbsoluteUri;
}
entity.NotificationEmail = invoice.NotificationEmail;
entity.BuyerInformation = Map<Invoice, BuyerInformation>(invoice);
entity.BuyerInformation = Map<CreateInvoiceRequest, BuyerInformation>(invoice);
entity.PaymentTolerance = storeBlob.PaymentTolerance;
if (additionalTags != null)
entity.InternalTags.AddRange(additionalTags);
//Another way of passing buyer info to support
FillBuyerInfo(invoice.Buyer, entity.BuyerInformation);
if (entity?.BuyerInformation?.BuyerEmail != null)
@ -95,13 +103,21 @@ namespace BTCPayServer.Controllers
entity.RefundMail = entity.BuyerInformation.BuyerEmail;
}
var taxIncluded = invoice.TaxIncluded.HasValue ? invoice.TaxIncluded.Value : 0m;
var currencyInfo = _CurrencyNameTable.GetNumberFormatInfo(invoice.Currency, false);
if (currencyInfo != null)
{
invoice.Price = Math.Round(invoice.Price, currencyInfo.CurrencyDecimalDigits);
int divisibility = currencyInfo.CurrencyDecimalDigits;
invoice.Price = invoice.Price.RoundToSignificant(ref divisibility);
divisibility = currencyInfo.CurrencyDecimalDigits;
invoice.TaxIncluded = taxIncluded.RoundToSignificant(ref divisibility);
}
invoice.Price = Math.Max(0.0m, invoice.Price);
entity.ProductInformation = Map<Invoice, ProductInformation>(invoice);
invoice.TaxIncluded = Math.Max(0.0m, taxIncluded);
invoice.TaxIncluded = Math.Min(taxIncluded, invoice.Price);
entity.ProductInformation = Map<CreateInvoiceRequest, ProductInformation>(invoice);
entity.RedirectURL = invoice.RedirectURL ?? store.StoreWebsite;
@ -114,6 +130,17 @@ namespace BTCPayServer.Controllers
HashSet<CurrencyPair> currencyPairsToFetch = new HashSet<CurrencyPair>();
var rules = storeBlob.GetRateRules(_NetworkProvider);
var excludeFilter = storeBlob.GetExcludedPaymentMethods(); // Here we can compose filters from other origin with PaymentFilter.Any()
if (invoice.SupportedTransactionCurrencies != null && invoice.SupportedTransactionCurrencies.Count != 0)
{
var supportedTransactionCurrencies = invoice.SupportedTransactionCurrencies
.Where(c => c.Value.Enabled)
.Select(c => PaymentMethodId.TryParse(c.Key, out var p) ? p : null)
.ToHashSet();
excludeFilter = PaymentFilter.Or(excludeFilter,
PaymentFilter.Where(p => !supportedTransactionCurrencies.Contains(p)));
}
foreach (var network in store.GetSupportedPaymentMethods(_NetworkProvider)
.Where(s => !excludeFilter.Match(s.PaymentId))
.Select(c => _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode))
@ -166,9 +193,15 @@ namespace BTCPayServer.Controllers
entity.SetSupportedPaymentMethods(supported);
entity.SetPaymentMethods(paymentMethods);
entity.PosData = invoice.PosData;
foreach (var app in await getAppsTaggingStore)
{
entity.InternalTags.Add(AppService.GetAppInternalTag(app.Id));
}
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, logs, _NetworkProvider);
await fetchingAll;
_EventAggregator.Publish(new Events.InvoiceEvent(entity.EntityToDTO(_NetworkProvider), 1001, InvoiceEvent.Created));
_EventAggregator.Publish(new Events.InvoiceEvent(entity, 1001, InvoiceEvent.Created));
var resp = entity.EntityToDTO(_NetworkProvider);
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
}

View File

@ -0,0 +1,319 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.Models;
using BTCPayServer.Models.PaymentRequestViewModels;
using BTCPayServer.PaymentRequest;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Rating;
using BTCPayServer.Security;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.PaymentRequests;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using Ganss.XSS;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using NBitpayClient;
namespace BTCPayServer.Controllers
{
[Route("payment-requests")]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
public class PaymentRequestController : Controller
{
private readonly InvoiceController _InvoiceController;
private readonly UserManager<ApplicationUser> _UserManager;
private readonly StoreRepository _StoreRepository;
private readonly PaymentRequestRepository _PaymentRequestRepository;
private readonly PaymentRequestService _PaymentRequestService;
private readonly EventAggregator _EventAggregator;
private readonly CurrencyNameTable _Currencies;
private readonly HtmlSanitizer _htmlSanitizer;
public PaymentRequestController(
InvoiceController invoiceController,
UserManager<ApplicationUser> userManager,
StoreRepository storeRepository,
PaymentRequestRepository paymentRequestRepository,
PaymentRequestService paymentRequestService,
EventAggregator eventAggregator,
CurrencyNameTable currencies,
HtmlSanitizer htmlSanitizer)
{
_InvoiceController = invoiceController;
_UserManager = userManager;
_StoreRepository = storeRepository;
_PaymentRequestRepository = paymentRequestRepository;
_PaymentRequestService = paymentRequestService;
_EventAggregator = eventAggregator;
_Currencies = currencies;
_htmlSanitizer = htmlSanitizer;
}
[HttpGet]
[Route("")]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> GetPaymentRequests(int skip = 0, int count = 50, string statusMessage = null)
{
var result = await _PaymentRequestRepository.FindPaymentRequests(new PaymentRequestQuery()
{
UserId = GetUserId(), Skip = skip, Count = count
});
return View(new ListPaymentRequestsViewModel()
{
Skip = skip,
StatusMessage = statusMessage,
Count = count,
Total = result.Total,
Items = result.Items.Select(data => new ViewPaymentRequestViewModel(data)).ToList()
});
}
[HttpGet]
[Route("edit/{id?}")]
public async Task<IActionResult> EditPaymentRequest(string id, string statusMessage = null)
{
SelectList stores = null;
var data = await _PaymentRequestRepository.FindPaymentRequest(id, GetUserId());
if (data == null && !string.IsNullOrEmpty(id))
{
return NotFound();
}
stores = new SelectList(await _StoreRepository.GetStoresByUserId(GetUserId()), nameof(StoreData.Id),
nameof(StoreData.StoreName), data?.StoreDataId);
if (!stores.Any())
{
return RedirectToAction("GetPaymentRequests",
new
{
StatusMessage = "Error: You need to create at least one store before creating a payment request"
});
}
return View(new UpdatePaymentRequestViewModel(data)
{
Stores = stores,
StatusMessage = statusMessage
});
}
[HttpPost]
[Route("edit/{id?}")]
public async Task<IActionResult> EditPaymentRequest(string id, UpdatePaymentRequestViewModel viewModel)
{
if (string.IsNullOrEmpty(viewModel.Currency) ||
_Currencies.GetCurrencyData(viewModel.Currency, false) == null)
ModelState.AddModelError(nameof(viewModel.Currency), "Invalid currency");
var data = await _PaymentRequestRepository.FindPaymentRequest(id, GetUserId());
if (data == null && !string.IsNullOrEmpty(id))
{
return NotFound();
}
if (!ModelState.IsValid)
{
viewModel.Stores = new SelectList(await _StoreRepository.GetStoresByUserId(GetUserId()),
nameof(StoreData.Id),
nameof(StoreData.StoreName), data?.StoreDataId);
return View(viewModel);
}
if (data == null)
{
data = new PaymentRequestData();
}
data.StoreDataId = viewModel.StoreId;
var blob = data.GetBlob();
blob.Title = viewModel.Title;
blob.Email = viewModel.Email;
blob.Description = _htmlSanitizer.Sanitize(viewModel.Description);
blob.Amount = viewModel.Amount;
blob.ExpiryDate = viewModel.ExpiryDate;
blob.Currency = viewModel.Currency;
blob.EmbeddedCSS = viewModel.EmbeddedCSS;
blob.CustomCSSLink = viewModel.CustomCSSLink;
blob.AllowCustomPaymentAmounts = viewModel.AllowCustomPaymentAmounts;
data.SetBlob(blob);
data = await _PaymentRequestRepository.CreateOrUpdatePaymentRequest(data);
_EventAggregator.Publish(new PaymentRequestUpdated()
{
Data = data,
PaymentRequestId = data.Id
});
return RedirectToAction("EditPaymentRequest", new {id = data.Id, StatusMessage = "Saved"});
}
[HttpGet]
[Route("{id}/remove")]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> RemovePaymentRequestPrompt(string id)
{
var data = await _PaymentRequestRepository.FindPaymentRequest(id, GetUserId());
if (data == null)
{
return NotFound();
}
var blob = data.GetBlob();
return View("Confirm", new ConfirmModel()
{
Title = $"Remove Payment Request",
Description = $"Are you sure to remove access to remove payment request '{blob.Title}' ?",
Action = "Delete"
});
}
[HttpPost]
[Route("{id}/remove")]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> RemovePaymentRequest(string id)
{
var result = await _PaymentRequestRepository.RemovePaymentRequest(id, GetUserId());
if (result)
{
return RedirectToAction("GetPaymentRequests",
new {StatusMessage = "Payment request successfully removed"});
}
else
{
return RedirectToAction("GetPaymentRequests",
new
{
StatusMessage =
"Payment request could not be removed. Any request that has generated invoices cannot be removed."
});
}
}
[HttpGet]
[Route("{id}")]
[AllowAnonymous]
public async Task<IActionResult> ViewPaymentRequest(string id)
{
var result = await _PaymentRequestService.GetPaymentRequest(id, GetUserId());
if (result == null)
{
return NotFound();
}
return View(result);
}
[HttpGet]
[Route("{id}/pay")]
[AllowAnonymous]
public async Task<IActionResult> PayPaymentRequest(string id, bool redirectToInvoice = true,
decimal? amount = null)
{
var result = ((await ViewPaymentRequest(id)) as ViewResult)?.Model as ViewPaymentRequestViewModel;
if (result == null)
{
return NotFound();
}
if (result.AmountDue <= 0)
{
if (redirectToInvoice)
{
return RedirectToAction("ViewPaymentRequest", new {Id = id});
}
return BadRequest("Payment Request has already been settled.");
}
if (result.ExpiryDate.HasValue && DateTime.Now >= result.ExpiryDate)
{
if (redirectToInvoice)
{
return RedirectToAction("ViewPaymentRequest", new {Id = id});
}
return BadRequest("Payment Request has expired");
}
var statusesAllowedToDisplay = new List<InvoiceStatus>()
{
InvoiceStatus.New
};
var validInvoice = result.Invoices.FirstOrDefault(invoice =>
Enum.TryParse<InvoiceStatus>(invoice.Status, true, out var status) &&
statusesAllowedToDisplay.Contains(status));
if (validInvoice != null)
{
if (redirectToInvoice)
{
return RedirectToAction("Checkout", "Invoice", new {Id = validInvoice.Id});
}
return Ok(validInvoice.Id);
}
if (result.AllowCustomPaymentAmounts && amount != null)
{
var invoiceAmount = result.AmountDue < amount ? result.AmountDue : amount;
return await CreateInvoiceForPaymentRequest(id, redirectToInvoice, result, invoiceAmount);
}
return await CreateInvoiceForPaymentRequest(id, redirectToInvoice, result);
}
private async Task<IActionResult> CreateInvoiceForPaymentRequest(string id,
bool redirectToInvoice,
ViewPaymentRequestViewModel result,
decimal? amount = null)
{
var pr = await _PaymentRequestRepository.FindPaymentRequest(id, null);
var blob = pr.GetBlob();
var store = pr.StoreData;
store.AdditionalClaims.Add(new Claim(Policies.CanCreateInvoice.Key, store.Id));
try
{
var newInvoiceId = (await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
{
OrderId = $"{PaymentRequestRepository.GetOrderIdForPaymentRequest(id)}",
Currency = blob.Currency,
Price = amount.GetValueOrDefault(result.AmountDue),
FullNotifications = true,
BuyerEmail = result.Email,
RedirectURL = Request.GetDisplayUrl().Replace("/pay", "", StringComparison.InvariantCulture),
}, store, HttpContext.Request.GetAbsoluteRoot(), new List<string>() { PaymentRequestRepository.GetInternalTag(id) })).Data.Id;
if (redirectToInvoice)
{
return RedirectToAction("Checkout", "Invoice", new {Id = newInvoiceId});
}
return Ok(newInvoiceId);
}
catch (BitpayHttpException e)
{
return BadRequest(e.Message);
}
}
private string GetUserId()
{
return _UserManager.GetUserId(User);
}
}
}

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Filters;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Cors;
@ -45,7 +46,7 @@ namespace BTCPayServer.Controllers
if (!ModelState.IsValid)
return View();
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
var invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
{
Price = model.Price,
Currency = model.Currency,

View File

@ -2,6 +2,7 @@
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.Lightning;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
@ -30,6 +31,7 @@ namespace BTCPayServer.Controllers
}
[HttpGet]
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
public async Task<IActionResult> ShowLightningNodeInfo(string storeId, string cryptoCode)
{
var store = await _StoreRepository.FindStore(storeId);

View File

@ -12,11 +12,13 @@ using BTCPayServer.Rating;
using Newtonsoft.Json;
using Microsoft.AspNetCore.Authorization;
using BTCPayServer.Authentication;
using Microsoft.AspNetCore.Cors;
namespace BTCPayServer.Controllers
{
[Authorize(AuthenticationSchemes = Security.Policies.BitpayAuthentication)]
[AllowAnonymous]
[EnableCors(CorsPolicies.All)]
public class RateController : Controller
{
RateFetcher _RateProviderFactory;
@ -139,9 +141,9 @@ namespace BTCPayServer.Controllers
{
var supportedMethods = store.GetSupportedPaymentMethods(_NetworkProvider);
var currencyCodes = supportedMethods.Select(method => method.PaymentId.CryptoCode).Distinct();
var defaultCrypto = store.GetDefaultCrypto(_NetworkProvider);
var defaultPaymentId = store.GetDefaultPaymentId(_NetworkProvider);
currencyPairs = BuildCurrencyPairs(currencyCodes, defaultCrypto);
currencyPairs = BuildCurrencyPairs(currencyCodes, defaultPaymentId.CryptoCode);
if (string.IsNullOrEmpty(currencyPairs))
{

View File

@ -27,6 +27,7 @@ using Renci.SshNet;
using BTCPayServer.Logging;
using BTCPayServer.Lightning;
using BTCPayServer.Configuration.External;
using System.Runtime.CompilerServices;
namespace BTCPayServer.Controllers
{
@ -170,7 +171,7 @@ namespace BTCPayServer.Controllers
vm.DNSDomain = null;
return View(vm);
}
[Route("server/maintenance")]
[HttpPost]
public async Task<IActionResult> Maintenance(MaintenanceViewModel vm, string command)
@ -208,8 +209,8 @@ namespace BTCPayServer.Controllers
{
builder.Scheme = this.Request.Scheme;
builder.Host = vm.DNSDomain;
var addresses1 = Dns.GetHostAddressesAsync(this.Request.Host.Host);
var addresses2 = Dns.GetHostAddressesAsync(vm.DNSDomain);
var addresses1 = GetAddressAsync(this.Request.Host.Host);
var addresses2 = GetAddressAsync(vm.DNSDomain);
await Task.WhenAll(addresses1, addresses2);
var addressesSet = addresses1.GetAwaiter().GetResult().Select(c => c.ToString()).ToHashSet();
@ -253,6 +254,13 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(Maintenance));
}
private Task<IPAddress[]> GetAddressAsync(string domainOrIP)
{
if (IPAddress.TryParse(domainOrIP, out var ip))
return Task.FromResult(new[] { ip });
return Dns.GetHostAddressesAsync(domainOrIP);
}
public static string RunId = Encoders.Hex.EncodeData(NBitcoin.RandomUtils.GetBytes(32));
[HttpGet]
[Route("runid")]
@ -345,22 +353,27 @@ namespace BTCPayServer.Controllers
var user = await _UserManager.FindByIdAsync(userId);
if (user == null)
return NotFound();
var roles = await _UserManager.GetRolesAsync(user);
var isAdmin = IsAdmin(roles);
bool updated = false;
if (isAdmin != viewModel.IsAdmin)
viewModel.StatusMessage = "";
var admins = await _UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
if (!viewModel.IsAdmin && admins.Count == 1)
{
viewModel.StatusMessage = "This is the only Admin, so their role can't be removed until another Admin is added.";
return View(viewModel); // return
}
var roles = await _UserManager.GetRolesAsync(user);
if (viewModel.IsAdmin != IsAdmin(roles))
{
if (viewModel.IsAdmin)
await _UserManager.AddToRoleAsync(user, Roles.ServerAdmin);
else
await _UserManager.RemoveFromRoleAsync(user, Roles.ServerAdmin);
updated = true;
}
if (updated)
{
viewModel.StatusMessage = "User successfully updated";
}
return View(viewModel);
}
@ -371,12 +384,29 @@ namespace BTCPayServer.Controllers
var user = userId == null ? null : await _UserManager.FindByIdAsync(userId);
if (user == null)
return NotFound();
return View("Confirm", new ConfirmModel()
var admins = await _UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
if (admins.Count == 1)
{
Title = "Delete user " + user.Email,
Description = "This user will be permanently deleted",
Action = "Delete"
});
// return
return View("Confirm", new ConfirmModel("Unable to Delete Last Admin",
"This is the last Admin, so it can't be removed"));
}
var roles = await _UserManager.GetRolesAsync(user);
if (IsAdmin(roles))
{
return View("Confirm", new ConfirmModel("Delete Admin " + user.Email,
"Are you sure you want to delete this Admin and delete all accounts, users and data associated with the server account?",
"Delete"));
}
else
{
return View("Confirm", new ConfirmModel("Delete user " + user.Email,
"This user will be permanently deleted",
"Delete"));
}
}
[Route("server/users/{userId}/delete")]
@ -445,7 +475,17 @@ namespace BTCPayServer.Controllers
{
Crypto = cryptoCode,
Type = "Spark server",
Action = nameof(SparkServices),
Action = nameof(SparkService),
Index = i++,
});
}
foreach (var rtlService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalRTL>(cryptoCode))
{
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
{
Crypto = cryptoCode,
Type = "Ride the Lightning server (RTL)",
Action = nameof(RTLService),
Index = i++,
});
}
@ -460,7 +500,7 @@ namespace BTCPayServer.Controllers
});
}
}
foreach(var externalService in _Options.ExternalServices)
foreach (var externalService in _Options.ExternalServices)
{
result.ExternalServices.Add(new ServicesViewModel.ExternalService()
{
@ -468,7 +508,7 @@ namespace BTCPayServer.Controllers
Link = this.Request.GetRelativePathOrAbsolute(externalService.Value)
});
}
if(_Options.SSHSettings != null)
if (_Options.SSHSettings != null)
{
result.ExternalServices.Add(new ServicesViewModel.ExternalService()
{
@ -519,37 +559,41 @@ namespace BTCPayServer.Controllers
}
[Route("server/services/spark/{cryptoCode}/{index}")]
public async Task<IActionResult> SparkServices(string cryptoCode, int index, bool showQR = false)
public async Task<IActionResult> SparkService(string cryptoCode, int index, bool showQR = false)
{
return await LightningWalletServicesCore<ExternalSpark>(cryptoCode, showQR, "Spark Wallet");
}
[Route("server/services/rtl/{cryptoCode}/{index}")]
public async Task<IActionResult> RTLService(string cryptoCode, int index, bool showQR = false)
{
return await LightningWalletServicesCore<ExternalRTL>(cryptoCode, showQR, "Ride the Lightning Wallet");
}
private async Task<IActionResult> LightningWalletServicesCore<T>(string cryptoCode, bool showQR, string walletName) where T : ExternalService, IAccessKeyService
{
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
{
StatusMessage = $"Error: {cryptoCode} is not fully synched";
return RedirectToAction(nameof(Services));
}
var spark = _Options.ExternalServicesByCryptoCode.GetServices<ExternalSpark>(cryptoCode).Select(c => c.ConnectionString).FirstOrDefault();
if(spark == null)
var external = _Options.ExternalServicesByCryptoCode.GetServices<T>(cryptoCode).Where(c => c?.ConnectionString?.Server != null).FirstOrDefault();
if (external == null)
{
return NotFound();
}
SparkServicesViewModel vm = new SparkServicesViewModel();
LightningWalletServices vm = new LightningWalletServices();
vm.ShowQR = showQR;
vm.WalletName = walletName;
try
{
var cookie = (spark.CookeFile == "fake"
? "fake:fake:fake" // If we are testing, it should not crash
: await System.IO.File.ReadAllTextAsync(spark.CookeFile)).Split(':');
if (cookie.Length >= 3)
{
vm.SparkLink = $"{spark.Server.AbsoluteUri}?access-key={cookie[2]}";
}
vm.ServiceLink = $"{external.ConnectionString.Server.AbsoluteUri}?access-key={await external.ExtractAccessKey()}";
}
catch(Exception ex)
catch (Exception ex)
{
StatusMessage = $"Error: {ex.Message}";
return RedirectToAction(nameof(Services));
}
return View(vm);
return View("LightningWalletServices", vm);
}
[Route("server/services/lnd/{cryptoCode}/{index}")]
@ -571,7 +615,7 @@ namespace BTCPayServer.Controllers
model.ConnectionType = "GRPC";
model.GRPCSSLCipherSuites = "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256";
}
else if(external.ConnectionType == LightningConnectionType.LndREST)
else if (external.ConnectionType == LightningConnectionType.LndREST)
{
model.Uri = external.BaseUri.AbsoluteUri;
model.ConnectionType = "REST";
@ -785,7 +829,8 @@ namespace BTCPayServer.Controllers
.ToList();
vm.LogFileOffset = offset;
if (string.IsNullOrEmpty(file)) return View("Logs", vm);
if (string.IsNullOrEmpty(file))
return View("Logs", vm);
vm.Log = "";
var path = Path.Combine(di.FullName, file);
try

View File

@ -331,7 +331,7 @@ namespace BTCPayServer.Controllers
{
var storeBlob = StoreData.GetStoreBlob();
var vm = new CheckoutExperienceViewModel();
vm.SetCryptoCurrencies(_ExplorerProvider, StoreData.GetDefaultCrypto(_NetworkProvider));
SetCryptoCurrencies(vm, StoreData);
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? "";
@ -341,6 +341,23 @@ namespace BTCPayServer.Controllers
vm.HtmlTitle = storeBlob.HtmlTitle;
return View(vm);
}
void SetCryptoCurrencies(CheckoutExperienceViewModel vm, Data.StoreData storeData)
{
var choices = storeData.GetEnabledPaymentIds(_NetworkProvider)
.Select(o => new CheckoutExperienceViewModel.Format() { Name = GetDisplayName(o), Value = o.ToString(), PaymentId = o }).ToArray();
var defaultPaymentId = storeData.GetDefaultPaymentId(_NetworkProvider);
var chosen = choices.FirstOrDefault(c => c.PaymentId == defaultPaymentId);
vm.CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen?.Value);
vm.DefaultPaymentMethod = chosen?.Value;
}
private string GetDisplayName(PaymentMethodId paymentMethodId)
{
var display = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode)?.DisplayName ?? paymentMethodId.CryptoCode;
return paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
display : $"{display} (Lightning)";
}
[HttpPost]
[Route("{storeId}/checkout")]
@ -365,12 +382,13 @@ namespace BTCPayServer.Controllers
}
bool needUpdate = false;
var blob = StoreData.GetStoreBlob();
if (StoreData.GetDefaultCrypto(_NetworkProvider) != model.DefaultCryptoCurrency)
var defaultPaymentMethodId = model.DefaultPaymentMethod == null ? null : PaymentMethodId.Parse(model.DefaultPaymentMethod);
if (StoreData.GetDefaultPaymentId(_NetworkProvider) != defaultPaymentMethodId)
{
needUpdate = true;
StoreData.SetDefaultCrypto(model.DefaultCryptoCurrency);
StoreData.SetDefaultPaymentId(defaultPaymentMethodId);
}
model.SetCryptoCurrencies(_ExplorerProvider, model.DefaultCryptoCurrency);
SetCryptoCurrencies(model, StoreData);
model.SetLanguages(_LangService, model.DefaultLang);
if (!ModelState.IsValid)

View File

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

View File

@ -23,6 +23,7 @@ namespace BTCPayServer.Data
{
get; set;
}
public bool TagAllInvoices { get; set; }
public string Settings { get; set; }
public T GetSettings<T>() where T : class, new()

View File

@ -5,6 +5,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using BTCPayServer.Models;
using BTCPayServer.Services.PaymentRequests;
using Microsoft.EntityFrameworkCore.Infrastructure.Internal;
using Microsoft.EntityFrameworkCore.Infrastructure;
@ -54,6 +55,11 @@ namespace BTCPayServer.Data
{
get; set;
}
public DbSet<PaymentRequestData> PaymentRequests
{
get; set;
}
public DbSet<StoreData> Stores
{
@ -204,6 +210,15 @@ namespace BTCPayServer.Data
o.UniqueId
#pragma warning restore CS0618
});
builder.Entity<PaymentRequestData>()
.HasOne(o => o.StoreData)
.WithMany(i => i.PaymentRequests)
.OnDelete(DeleteBehavior.Cascade);
builder.Entity<PaymentRequestData>()
.HasIndex(o => o.Status);
}
}
}

View File

@ -21,6 +21,7 @@ using BTCPayServer.Payments.Changelly;
using BTCPayServer.Payments.CoinSwitch;
using BTCPayServer.Security;
using BTCPayServer.Rating;
using BTCPayServer.Services.PaymentRequests;
using BTCPayServer.Services.Mails;
namespace BTCPayServer.Data
@ -41,6 +42,11 @@ namespace BTCPayServer.Data
{
get; set;
}
public List<PaymentRequestData> PaymentRequests
{
get; set;
}
public List<InvoiceData> Invoices { get; set; }
@ -56,7 +62,6 @@ namespace BTCPayServer.Data
get;
set;
}
public IEnumerable<ISupportedPaymentMethod> GetSupportedPaymentMethods(BTCPayNetworkProvider networks)
{
#pragma warning disable CS0618
@ -195,7 +200,7 @@ namespace BTCPayServer.Data
get;
set;
}
[Obsolete("Use GetDefaultCrypto instead")]
[Obsolete("Use GetDefaultPaymentId instead")]
public string DefaultCrypto { get; set; }
public List<PairedSINData> PairedSINs { get; set; }
public IEnumerable<APIKeyData> APIKeys { get; set; }
@ -204,13 +209,32 @@ namespace BTCPayServer.Data
public List<Claim> AdditionalClaims { get; set; } = new List<Claim>();
#pragma warning disable CS0618
public string GetDefaultCrypto(BTCPayNetworkProvider networkProvider = null)
public PaymentMethodId GetDefaultPaymentId(BTCPayNetworkProvider networks)
{
return DefaultCrypto ?? (networkProvider == null ? "BTC" : GetSupportedPaymentMethods(networkProvider).Select(p => p.PaymentId.CryptoCode).FirstOrDefault() ?? "BTC");
PaymentMethodId[] paymentMethodIds = GetEnabledPaymentIds(networks);
var defaultPaymentId = string.IsNullOrEmpty(DefaultCrypto) ? null : PaymentMethodId.Parse(DefaultCrypto);
var chosen = paymentMethodIds.FirstOrDefault(f => f == defaultPaymentId) ??
paymentMethodIds.FirstOrDefault(f => f.CryptoCode == defaultPaymentId?.CryptoCode) ??
paymentMethodIds.FirstOrDefault();
return chosen;
}
public void SetDefaultCrypto(string defaultCryptoCurrency)
public PaymentMethodId[] GetEnabledPaymentIds(BTCPayNetworkProvider networks)
{
DefaultCrypto = defaultCryptoCurrency;
var excludeFilter = GetStoreBlob().GetExcludedPaymentMethods();
var paymentMethodIds = GetSupportedPaymentMethods(networks).Select(p => p.PaymentId)
.Where(a => !excludeFilter.Match(a))
.OrderByDescending(a => a.CryptoCode == "BTC")
.ThenBy(a => a.CryptoCode)
.ThenBy(a => a.PaymentType == PaymentTypes.LightningLike ? 1 : 0)
.ToArray();
return paymentMethodIds;
}
public void SetDefaultPaymentId(PaymentMethodId defaultPaymentId)
{
DefaultCrypto = defaultPaymentId.ToString();
}
#pragma warning restore CS0618

View File

@ -20,14 +20,14 @@ namespace BTCPayServer.Events
public const string Confirmed= "invoice_confirmed";
public const string Completed= "invoice_completed";
public InvoiceEvent(Models.InvoiceResponse invoice, int code, string name)
public InvoiceEvent(InvoiceEntity invoice, int code, string name)
{
Invoice = invoice;
EventCode = code;
Name = name;
}
public Models.InvoiceResponse Invoice { get; set; }
public InvoiceEntity Invoice { get; set; }
public int EventCode { get; set; }
public string Name { get; set; }

View File

@ -61,6 +61,23 @@ namespace BTCPayServer
}
return value;
}
public static decimal RoundToSignificant(this decimal value, ref int divisibility)
{
if (value != 0m)
{
while (true)
{
var rounded = decimal.Round(value, divisibility, MidpointRounding.AwayFromZero);
if ((Math.Abs(rounded - value) / value) < 0.001m)
{
value = rounded;
break;
}
divisibility++;
}
}
return value;
}
public static PaymentMethodId GetpaymentMethodId(this InvoiceCryptoInfo info)
{
return new PaymentMethodId(info.CryptoCode, Enum.Parse<PaymentTypes>(info.PaymentType));

View File

@ -12,13 +12,32 @@ namespace BTCPayServer.Filters
{
Value = value;
}
public string Value
public XFrameOptionsAttribute(XFrameOptions type, string allowFrom = null)
{
get; set;
switch (type)
{
case XFrameOptions.Deny:
Value = "deny";
break;
case XFrameOptions.SameOrigin:
Value = "deny";
break;
case XFrameOptions.AllowFrom:
Value = $"allow-from {allowFrom}";
break;
case XFrameOptions.AllowAll:
Value = "allow-all";
break;
default:
throw new ArgumentOutOfRangeException(nameof(type), type, null);
}
}
public string Value { get; set; }
public void OnActionExecuted(ActionExecutedContext context)
{
}
public void OnActionExecuting(ActionExecutingContext context)
@ -28,5 +47,13 @@ namespace BTCPayServer.Filters
context.HttpContext.Response.SetHeaderOnStarting("X-Frame-Options", Value);
}
}
public enum XFrameOptions
{
Deny,
SameOrigin,
AllowFrom,
AllowAll
}
}
}

View File

@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using BTCPayServer.Logging;
using Microsoft.Extensions.Hosting;
namespace BTCPayServer.HostedServices
{
public class EventHostedServiceBase : IHostedService
{
private readonly EventAggregator _EventAggregator;
private List<IEventAggregatorSubscription> _Subscriptions;
private CancellationTokenSource _Cts;
public EventHostedServiceBase(EventAggregator eventAggregator)
{
_EventAggregator = eventAggregator;
}
Channel<object> _Events = Channel.CreateUnbounded<object>();
public async Task ProcessEvents(CancellationToken cancellationToken)
{
while (await _Events.Reader.WaitToReadAsync(cancellationToken))
{
if (_Events.Reader.TryRead(out var evt))
{
try
{
await ProcessEvent(evt, cancellationToken);
}
catch when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
Logs.PayServer.LogWarning(ex, $"Unhandled exception in {this.GetType().Name}");
}
}
}
}
protected virtual Task ProcessEvent(object evt, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
protected virtual void SubscibeToEvents()
{
}
protected void Subscribe<T>()
{
_Subscriptions.Add(_EventAggregator.Subscribe<T>(e => _Events.Writer.TryWrite(e)));
}
public virtual Task StartAsync(CancellationToken cancellationToken)
{
_Subscriptions = new List<IEventAggregatorSubscription>();
SubscibeToEvents();
_Cts = new CancellationTokenSource();
_ProcessingEvents = ProcessEvents(_Cts.Token);
return Task.CompletedTask;
}
Task _ProcessingEvents = Task.CompletedTask;
public virtual async Task StopAsync(CancellationToken cancellationToken)
{
_Subscriptions?.ForEach(subscription => subscription.Dispose());
_Cts?.Cancel();
try
{
await _ProcessingEvents;
}
catch (OperationCanceledException)
{ }
}
}
}

View File

@ -33,13 +33,10 @@ namespace BTCPayServer.HostedServices
get; set;
}
public InvoiceEntity Invoice
public InvoicePaymentNotificationEventWrapper Notification
{
get; set;
}
public int? EventCode { get; set; }
public string Message { get; set; }
}
IBackgroundJobClient _JobClient;
@ -63,22 +60,70 @@ namespace BTCPayServer.HostedServices
_EmailSenderFactory = emailSenderFactory;
}
void Notify(InvoiceEntity invoice, int? eventCode = null, string name = null)
void Notify(InvoiceEntity invoice, InvoiceEvent invoiceEvent, bool extendedNotification)
{
var dto = invoice.EntityToDTO(_NetworkProvider);
var notification = new InvoicePaymentNotificationEventWrapper()
{
Data = new InvoicePaymentNotification()
{
Id = dto.Id,
Currency = dto.Currency,
CurrentTime = dto.CurrentTime,
ExceptionStatus = dto.ExceptionStatus,
ExpirationTime = dto.ExpirationTime,
InvoiceTime = dto.InvoiceTime,
PosData = dto.PosData,
Price = dto.Price,
Status = dto.Status,
BuyerFields = invoice.RefundMail == null ? null : new Newtonsoft.Json.Linq.JObject() { new JProperty("buyerEmail", invoice.RefundMail) },
PaymentSubtotals = dto.PaymentSubtotals,
PaymentTotals = dto.PaymentTotals,
AmountPaid = dto.AmountPaid,
ExchangeRates = dto.ExchangeRates,
},
Event = new InvoicePaymentNotificationEvent()
{
Code = invoiceEvent.EventCode,
Name = invoiceEvent.Name
},
ExtendedNotification = extendedNotification,
NotificationURL = invoice.NotificationURL
};
// For lightning network payments, paid, confirmed and completed come all at once.
// So despite the event is "paid" or "confirmed" the Status of the invoice is technically complete
// This confuse loggers who think their endpoint get duplicated events
// So here, we just override the status expressed by the notification
if (invoiceEvent.Name == InvoiceEvent.Confirmed)
{
notification.Data.Status = InvoiceState.ToString(InvoiceStatus.Confirmed);
}
if (invoiceEvent.Name == InvoiceEvent.PaidInFull)
{
notification.Data.Status = InvoiceState.ToString(InvoiceStatus.Paid);
}
//////////////////
// We keep backward compatibility with bitpay by passing BTC info to the notification
// we don't pass other info, as it is a bad idea to use IPN data for logic processing (can be faked)
var btcCryptoInfo = dto.CryptoInfo.FirstOrDefault(c => c.GetpaymentMethodId() == new PaymentMethodId("BTC", Payments.PaymentTypes.BTCLike));
if (btcCryptoInfo != null)
{
#pragma warning disable CS0618
notification.Data.Rate = dto.Rate;
notification.Data.Url = dto.Url;
notification.Data.BTCDue = dto.BTCDue;
notification.Data.BTCPaid = dto.BTCPaid;
notification.Data.BTCPrice = dto.BTCPrice;
#pragma warning restore CS0618
}
CancellationTokenSource cts = new CancellationTokenSource(10000);
if (!String.IsNullOrEmpty(invoice.NotificationEmail))
{
// just extracting most important data for email body, merchant should query API back for full invoice based on Invoice.Id
var ipn = new
{
invoice.Id,
invoice.Status,
invoice.StoreId
};
// TODO: Consider adding info on ItemDesc and payment info (amount)
var emailBody = NBitcoin.JsonConverters.Serializer.ToString(ipn);
var emailBody = NBitcoin.JsonConverters.Serializer.ToString(notification);
_EmailSenderFactory.GetEmailSender(invoice.StoreId).SendEmail(
invoice.NotificationEmail,
@ -86,9 +131,9 @@ namespace BTCPayServer.HostedServices
emailBody);
}
if (string.IsNullOrEmpty(invoice.NotificationURL))
if (string.IsNullOrEmpty(invoice.NotificationURL) || !Uri.IsWellFormedUriString(invoice.NotificationURL, UriKind.Absolute))
return;
var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Invoice = invoice, EventCode = eventCode, Message = name });
var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Notification = notification });
if (!string.IsNullOrEmpty(invoice.NotificationURL))
_JobClient.Schedule(() => NotifyHttp(invoiceStr), TimeSpan.Zero);
}
@ -97,30 +142,23 @@ namespace BTCPayServer.HostedServices
{
var job = NBitcoin.JsonConverters.Serializer.ToObject<ScheduledJob>(invoiceData);
bool reschedule = false;
var aggregatorEvent = new InvoiceIPNEvent(job.Notification.Data.Id, job.Notification.Event.Code, job.Notification.Event.Name);
CancellationTokenSource cts = new CancellationTokenSource(10000);
try
{
HttpResponseMessage response = await SendNotification(job.Invoice, job.EventCode, job.Message, cts.Token);
HttpResponseMessage response = await SendNotification(job.Notification, cts.Token);
reschedule = !response.IsSuccessStatusCode;
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
{
Error = reschedule ? $"Unexpected return code: {(int)response.StatusCode}" : null
});
aggregatorEvent.Error = reschedule ? $"Unexpected return code: {(int)response.StatusCode}" : null;
_EventAggregator.Publish<InvoiceIPNEvent>(aggregatorEvent);
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
{
Error = "Timeout"
});
aggregatorEvent.Error = "Timeout";
_EventAggregator.Publish<InvoiceIPNEvent>(aggregatorEvent);
reschedule = true;
}
catch (Exception ex)
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
{
Error = ex.Message
});
reschedule = true;
List<string> messages = new List<string>();
@ -131,10 +169,8 @@ namespace BTCPayServer.HostedServices
}
string message = String.Join(',', messages.ToArray());
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
{
Error = $"Unexpected error: {message}"
});
aggregatorEvent.Error = $"Unexpected error: {message}";
_EventAggregator.Publish<InvoiceIPNEvent>(aggregatorEvent);
}
finally { cts?.Dispose(); }
@ -160,64 +196,35 @@ namespace BTCPayServer.HostedServices
public InvoicePaymentNotificationEvent Event { get; set; }
[JsonProperty("data")]
public InvoicePaymentNotification Data { get; set; }
[JsonProperty("extendedNotification")]
public bool ExtendedNotification { get; set; }
[JsonProperty(PropertyName = "notificationURL")]
public string NotificationURL { get; set; }
}
Encoding UTF8 = new UTF8Encoding(false);
private async Task<HttpResponseMessage> SendNotification(InvoiceEntity invoice, int? eventCode, string name, CancellationToken cancellation)
private async Task<HttpResponseMessage> SendNotification(InvoicePaymentNotificationEventWrapper notification, CancellationToken cancellation)
{
var request = new HttpRequestMessage();
request.Method = HttpMethod.Post;
var dto = invoice.EntityToDTO(_NetworkProvider);
InvoicePaymentNotification notification = new InvoicePaymentNotification()
{
Id = dto.Id,
Currency = dto.Currency,
CurrentTime = dto.CurrentTime,
ExceptionStatus = dto.ExceptionStatus,
ExpirationTime = dto.ExpirationTime,
InvoiceTime = dto.InvoiceTime,
PosData = dto.PosData,
Price = dto.Price,
Status = dto.Status,
BuyerFields = invoice.RefundMail == null ? null : new Newtonsoft.Json.Linq.JObject() { new JProperty("buyerEmail", invoice.RefundMail) },
PaymentSubtotals = dto.PaymentSubtotals,
PaymentTotals = dto.PaymentTotals,
AmountPaid = dto.AmountPaid,
ExchangeRates = dto.ExchangeRates,
var notificationString = NBitcoin.JsonConverters.Serializer.ToString(notification);
var jobj = JObject.Parse(notificationString);
};
// We keep backward compatibility with bitpay by passing BTC info to the notification
// we don't pass other info, as it is a bad idea to use IPN data for logic processing (can be faked)
var btcCryptoInfo = dto.CryptoInfo.FirstOrDefault(c => c.GetpaymentMethodId() == new PaymentMethodId("BTC", Payments.PaymentTypes.BTCLike));
if (btcCryptoInfo != null)
if (notification.ExtendedNotification)
{
#pragma warning disable CS0618
notification.Rate = dto.Rate;
notification.Url = dto.Url;
notification.BTCDue = dto.BTCDue;
notification.BTCPaid = dto.BTCPaid;
notification.BTCPrice = dto.BTCPrice;
#pragma warning restore CS0618
}
string notificationString = null;
if (eventCode.HasValue)
{
var wrapper = new InvoicePaymentNotificationEventWrapper();
wrapper.Data = notification;
wrapper.Event = new InvoicePaymentNotificationEvent() { Code = eventCode.Value, Name = name };
notificationString = JsonConvert.SerializeObject(wrapper);
jobj.Remove("extendedNotification");
jobj.Remove("notificationURL");
notificationString = jobj.ToString();
}
else
{
notificationString = JsonConvert.SerializeObject(notification);
notificationString = jobj["data"].ToString();
}
request.RequestUri = new Uri(invoice.NotificationURL, UriKind.Absolute);
request.RequestUri = new Uri(notification.NotificationURL, UriKind.Absolute);
request.Content = new StringContent(notificationString, UTF8, "application/json");
var response = await Enqueue(invoice.Id, async () => await _Client.SendAsync(request, cancellation));
var response = await Enqueue(notification.Data.Id, async () => await _Client.SendAsync(request, cancellation));
return response;
}
@ -306,17 +313,17 @@ namespace BTCPayServer.HostedServices
e.Name == InvoiceEvent.Completed ||
e.Name == InvoiceEvent.ExpiredPaidPartial
)
Notify(invoice);
Notify(invoice, e, false);
}
if (e.Name == "invoice_confirmed")
if (e.Name == InvoiceEvent.Confirmed)
{
Notify(invoice);
Notify(invoice, e, false);
}
if (invoice.ExtendedNotifications)
{
Notify(invoice, e.EventCode, e.Name);
Notify(invoice, e, true);
}
}));

View File

@ -65,10 +65,10 @@ namespace BTCPayServer.HostedServices
context.MarkDirty();
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1004, InvoiceEvent.Expired));
invoice.Status = InvoiceStatus.Expired;
if(invoice.ExceptionStatus == InvoiceExceptionStatus.PaidPartial)
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 2000, InvoiceEvent.ExpiredPaidPartial));
context.Events.Add(new InvoiceEvent(invoice, 1004, InvoiceEvent.Expired));
if (invoice.ExceptionStatus == InvoiceExceptionStatus.PaidPartial)
context.Events.Add(new InvoiceEvent(invoice, 2000, InvoiceEvent.ExpiredPaidPartial));
}
var payments = invoice.GetPayments().Where(p => p.Accounted).ToArray();
@ -83,7 +83,7 @@ namespace BTCPayServer.HostedServices
{
if (invoice.Status == InvoiceStatus.New)
{
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1003, InvoiceEvent.PaidInFull));
context.Events.Add(new InvoiceEvent(invoice, 1003, InvoiceEvent.PaidInFull));
invoice.Status = InvoiceStatus.Paid;
invoice.ExceptionStatus = accounting.Paid > accounting.TotalDue ? InvoiceExceptionStatus.PaidOver : InvoiceExceptionStatus.None;
await _InvoiceRepository.UnaffectAddress(invoice.Id);
@ -92,7 +92,7 @@ namespace BTCPayServer.HostedServices
else if (invoice.Status == InvoiceStatus.Expired && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidLate)
{
invoice.ExceptionStatus = InvoiceExceptionStatus.PaidLate;
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1009, InvoiceEvent.PaidAfterExpiration));
context.Events.Add(new InvoiceEvent(invoice, 1009, InvoiceEvent.PaidAfterExpiration));
context.MarkDirty();
}
}
@ -138,15 +138,15 @@ namespace BTCPayServer.HostedServices
(confirmedAccounting.Paid < accounting.MinimumTotalDue))
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1013, InvoiceEvent.FailedToConfirm));
context.Events.Add(new InvoiceEvent(invoice, 1013, InvoiceEvent.FailedToConfirm));
invoice.Status = InvoiceStatus.Invalid;
context.MarkDirty();
}
else if (confirmedAccounting.Paid >= accounting.MinimumTotalDue)
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1005, InvoiceEvent.Confirmed));
invoice.Status = InvoiceStatus.Confirmed;
context.Events.Add(new InvoiceEvent(invoice, 1005, InvoiceEvent.Confirmed));
context.MarkDirty();
}
}
@ -156,7 +156,7 @@ namespace BTCPayServer.HostedServices
var completedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p, network));
if (completedAccounting.Paid >= accounting.MinimumTotalDue)
{
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1006, InvoiceEvent.Completed));
context.Events.Add(new InvoiceEvent(invoice, 1006, InvoiceEvent.Completed));
invoice.Status = InvoiceStatus.Complete;
context.MarkDirty();
}

View File

@ -65,6 +65,12 @@ namespace BTCPayServer.HostedServices
settings.ConvertNetworkFeeProperty = true;
await _Settings.UpdateSetting(settings);
}
if (!settings.ConvertCrowdfundOldSettings)
{
await ConvertCrowdfundOldSettings();
settings.ConvertCrowdfundOldSettings = true;
await _Settings.UpdateSetting(settings);
}
}
catch (Exception ex)
{
@ -73,6 +79,24 @@ namespace BTCPayServer.HostedServices
}
}
private async Task ConvertCrowdfundOldSettings()
{
using (var ctx = _DBContextFactory.CreateContext())
{
foreach (var app in ctx.Apps.Where(a => a.AppType == "Crowdfund"))
{
var settings = app.GetSettings<Services.Apps.CrowdfundSettings>();
#pragma warning disable CS0618 // Type or member is obsolete
if (settings.UseAllStoreInvoices)
#pragma warning restore CS0618 // Type or member is obsolete
{
app.TagAllInvoices = true;
}
}
await ctx.SaveChangesAsync();
}
}
private async Task ConvertNetworkFeeProperty()
{
using (var ctx = _DBContextFactory.CreateContext())

View File

@ -65,9 +65,8 @@ namespace BTCPayServer.HostedServices
async Task RefreshCoinAverageSupportedExchanges()
{
var tickers = await new CoinAverageRateProvider() { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync();
var exchanges = new CoinAverageExchanges();
foreach (var item in tickers
foreach (var item in (await new CoinAverageRateProvider() { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync())
.Exchanges
.Select(c => new CoinAverageExchange(c.Name, c.DisplayName)))
{

View File

@ -38,15 +38,16 @@ using BTCPayServer.Logging;
using BTCPayServer.HostedServices;
using Meziantou.AspNetCore.BundleTagHelpers;
using System.Security.Claims;
using BTCPayServer.Crowdfund;
using BTCPayServer.Hubs;
using BTCPayServer.PaymentRequest;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Security;
using BTCPayServer.Services.PaymentRequests;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using NBXplorer.DerivationStrategy;
using NicolasDorier.RateLimits;
using Npgsql;
using BTCPayServer.Services.Apps;
namespace BTCPayServer.Hosting
{
@ -75,8 +76,8 @@ namespace BTCPayServer.Hosting
services.AddSingleton<BTCPayServerEnvironment>();
services.TryAddSingleton<TokenRepository>();
services.TryAddSingleton<EventAggregator>();
services.TryAddSingleton<PaymentRequestService>();
services.TryAddSingleton<CoinAverageSettings>();
services.TryAddSingleton<CrowdfundHubStreamer>();
services.TryAddSingleton<ApplicationDbContextFactory>(o =>
{
var opts = o.GetRequiredService<BTCPayServerOptions>();
@ -107,12 +108,52 @@ namespace BTCPayServer.Hosting
return opts.NetworkProvider;
});
services.TryAddSingleton<AppsHelper>();
services.TryAddSingleton<AppService>();
services.TryAddSingleton<Ganss.XSS.HtmlSanitizer>(o =>
{
var htmlSanitizer = new Ganss.XSS.HtmlSanitizer();
htmlSanitizer.RemovingAtRule += (sender, args) =>
{
};
htmlSanitizer.RemovingTag += (sender, args) =>
{
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 == Ganss.XSS.RemoveReason.NotAllowedUrlValue)
{
args.Cancel = true;
}
};
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");
return htmlSanitizer;
});
services.TryAddSingleton<LightningConfigurationProvider>();
services.TryAddSingleton<LanguageService>();
services.TryAddSingleton<NBXplorerDashboard>();
services.TryAddSingleton<StoreRepository>();
services.TryAddSingleton<PaymentRequestRepository>();
services.TryAddSingleton<BTCPayWalletProvider>();
services.TryAddSingleton<CurrencyNameTable>();
services.TryAddSingleton<IFeeProviderFactory>(o => new NBXplorerFeeProviderFactory(o.GetRequiredService<ExplorerClientProvider>())
@ -146,6 +187,8 @@ namespace BTCPayServer.Hosting
services.AddSingleton<IHostedService, InvoiceWatcher>();
services.AddSingleton<IHostedService, RatesHostedService>();
services.AddSingleton<IHostedService, BackgroundJobSchedulerHostedService>();
services.AddSingleton<IHostedService, AppHubStreamer>();
services.AddSingleton<IHostedService, PaymentRequestStreamer>();
services.AddSingleton<IBackgroundJobClient, BackgroundJobClient>();
services.AddTransient<IConfigureOptions<MvcOptions>, BTCPayClaimsFilter>();
@ -164,6 +207,7 @@ namespace BTCPayServer.Hosting
services.AddTransient<AccessTokenController>();
services.AddTransient<InvoiceController>();
services.AddTransient<AppsPublicController>();
services.AddTransient<PaymentRequestController>();
// Add application services.
services.AddSingleton<EmailSenderFactory>();
// bundling

View File

@ -38,9 +38,20 @@ namespace BTCPayServer.Hosting
{
var bitpayAuth = GetBitpayAuth(httpContext, out bool isBitpayAuth);
var isBitpayAPI = IsBitpayAPI(httpContext, isBitpayAuth);
if (isBitpayAPI && httpContext.Request.Method == "OPTIONS")
{
httpContext.Response.StatusCode = 200;
httpContext.Response.SetHeader("Access-Control-Allow-Origin", "*");
if (httpContext.Request.Headers.ContainsKey("Access-Control-Request-Headers"))
{
httpContext.Response.SetHeader("Access-Control-Allow-Headers", httpContext.Request.Headers["Access-Control-Request-Headers"].FirstOrDefault());
}
return; // We bypass MVC completely
}
httpContext.SetIsBitpayAPI(isBitpayAPI);
if (isBitpayAPI)
{
httpContext.Response.SetHeader("Access-Control-Allow-Origin", "*");
httpContext.SetBitpayAuth(bitpayAuth);
}
await _Next(httpContext);
@ -81,32 +92,34 @@ namespace BTCPayServer.Hosting
var isJson = (httpContext.Request.ContentType ?? string.Empty).StartsWith("application/json", StringComparison.OrdinalIgnoreCase);
var path = httpContext.Request.Path.Value;
var method = httpContext.Request.Method;
var isCors = method == "OPTIONS";
if (
bitpayAuth &&
(isCors || bitpayAuth) &&
(path == "/invoices" || path == "/invoices/") &&
httpContext.Request.Method == "POST" &&
isJson)
(isCors || (method == "POST" && isJson)))
return true;
if (
bitpayAuth &&
(isCors || bitpayAuth) &&
(path == "/invoices" || path == "/invoices/") &&
httpContext.Request.Method == "GET")
(isCors || method == "GET"))
return true;
if (
path.StartsWith("/invoices/", StringComparison.OrdinalIgnoreCase) &&
httpContext.Request.Method == "GET" &&
(isJson || httpContext.Request.Query.ContainsKey("token")))
path.StartsWith("/invoices/", StringComparison.OrdinalIgnoreCase) &&
(isCors || method == "GET") &&
(isCors || isJson || httpContext.Request.Query.ContainsKey("token")))
return true;
if (path.StartsWith("/rates", StringComparison.OrdinalIgnoreCase) &&
httpContext.Request.Method == "GET")
(isCors || method == "GET"))
return true;
if (
path.Equals("/tokens", StringComparison.Ordinal) &&
(httpContext.Request.Method == "GET" || httpContext.Request.Method == "POST"))
(isCors || method == "GET" || method == "POST"))
return true;
return false;
@ -134,59 +147,33 @@ namespace BTCPayServer.Hosting
}
}
// Make sure that code executing after this point think that the external url has been hit.
if (_Options.ExternalUrl != null)
{
if (reverseProxyScheme != null && _Options.ExternalUrl.Scheme != reverseProxyScheme)
{
if (reverseProxyScheme == "http" && _Options.ExternalUrl.Scheme == "https")
Logs.PayServer.LogWarning($"BTCPay ExternalUrl setting expected to use scheme '{_Options.ExternalUrl.Scheme}' externally, but the reverse proxy uses scheme '{reverseProxyScheme}' (X-Forwarded-Port), forcing ExternalUrl");
}
httpContext.Request.Scheme = _Options.ExternalUrl.Scheme;
if (_Options.ExternalUrl.IsDefaultPort)
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host);
else
{
if (reverseProxyPort != null && _Options.ExternalUrl.Port != reverseProxyPort.Value)
{
Logs.PayServer.LogWarning($"BTCPay ExternalUrl setting expected to use port '{_Options.ExternalUrl.Port}' externally, but the reverse proxy uses port '{reverseProxyPort.Value}'");
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host, reverseProxyPort.Value);
}
else
{
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host, _Options.ExternalUrl.Port);
}
}
}
// NGINX pass X-Forwarded-Proto and X-Forwarded-Port, so let's use that to have better guess of the real domain
else
ushort? p = null;
if (reverseProxyScheme != null)
{
ushort? p = null;
if (reverseProxyScheme != null)
{
httpContext.Request.Scheme = reverseProxyScheme;
if (reverseProxyScheme == "http")
p = 80;
if (reverseProxyScheme == "https")
p = 443;
}
if (reverseProxyPort != null)
{
p = reverseProxyPort.Value;
}
if (p.HasValue)
{
bool isDefault = httpContext.Request.Scheme == "http" && p.Value == 80;
isDefault |= httpContext.Request.Scheme == "https" && p.Value == 443;
if (isDefault)
httpContext.Request.Host = new HostString(httpContext.Request.Host.Host);
else
httpContext.Request.Host = new HostString(httpContext.Request.Host.Host, p.Value);
}
httpContext.Request.Scheme = reverseProxyScheme;
if (reverseProxyScheme == "http")
p = 80;
if (reverseProxyScheme == "https")
p = 443;
}
if (reverseProxyPort != null)
{
p = reverseProxyPort.Value;
}
if (p.HasValue)
{
bool isDefault = httpContext.Request.Scheme == "http" && p.Value == 80;
isDefault |= httpContext.Request.Scheme == "https" && p.Value == 443;
if (isDefault)
httpContext.Request.Host = new HostString(httpContext.Request.Host.Host);
else
httpContext.Request.Host = new HostString(httpContext.Request.Host.Host, p.Value);
}
}
private static async Task HandleBitpayHttpException(HttpContext httpContext, BitpayHttpException ex)

View File

@ -34,9 +34,10 @@ using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Mvc.Cors.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using System.Net;
using BTCPayServer.Hubs;
using BTCPayServer.PaymentRequest;
using Meziantou.AspNetCore.BundleTagHelpers;
using BTCPayServer.Security;
using BTCPayServer.Services.Apps;
namespace BTCPayServer.Hosting
{
@ -92,14 +93,6 @@ namespace BTCPayServer.Hosting
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;
});
services.AddCors(o =>
{
o.AddPolicy("BitpayAPI", b =>
{
b.AllowAnyMethod().AllowAnyHeader().AllowAnyOrigin();
});
});
// If the HTTPS certificate path is not set this logic will NOT be used and the default Kestrel binding logic will be.
string httpsCertificateFilePath = Configuration.GetOrDefault<string>("HttpsCertificateFilePath", null);
bool useDefaultCertificate = Configuration.GetOrDefault<bool>("HttpsUseDefaultCertificate", false);
@ -172,7 +165,8 @@ namespace BTCPayServer.Hosting
app.UseAuthentication();
app.UseSignalR(route =>
{
route.MapHub<CrowdfundHub>("/apps/crowdfund/hub");
route.MapHub<AppHub>("/apps/hub");
route.MapHub<PaymentRequestHub>("/payment-requests/hub");
});
app.UseWebSockets();
app.UseStatusCodePages();

View File

@ -0,0 +1,606 @@
// <auto-generated />
using System;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20190121133309_AddPaymentRequests")]
partial class AddPaymentRequests
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.1.4-rtm-31024");
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.Property<string>("Address")
.ValueGeneratedOnAdd();
b.Property<DateTimeOffset?>("CreatedTime");
b.Property<string>("InvoiceDataId");
b.HasKey("Address");
b.HasIndex("InvoiceDataId");
b.ToTable("AddressInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasMaxLength(50);
b.Property<string>("StoreId")
.HasMaxLength(50);
b.HasKey("Id");
b.HasIndex("StoreId");
b.ToTable("ApiKeys");
});
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("AppType");
b.Property<DateTimeOffset>("Created");
b.Property<string>("Name");
b.Property<string>("Settings");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("StoreDataId");
b.ToTable("Apps");
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{
b.Property<string>("InvoiceDataId");
b.Property<string>("Address");
b.Property<DateTimeOffset>("Assigned");
b.Property<string>("CryptoCode");
b.Property<DateTimeOffset?>("UnAssigned");
b.HasKey("InvoiceDataId", "Address");
b.ToTable("HistoricalAddressInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<DateTimeOffset>("Created");
b.Property<string>("CustomerEmail");
b.Property<string>("ExceptionStatus");
b.Property<string>("ItemCode");
b.Property<string>("OrderId");
b.Property<string>("Status");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("StoreDataId");
b.ToTable("Invoices");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
{
b.Property<string>("InvoiceDataId");
b.Property<string>("UniqueId");
b.Property<string>("Message");
b.Property<DateTimeOffset>("Timestamp");
b.HasKey("InvoiceDataId", "UniqueId");
b.ToTable("InvoiceEvents");
});
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Facade");
b.Property<string>("Label");
b.Property<DateTimeOffset>("PairingTime");
b.Property<string>("SIN");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("SIN");
b.HasIndex("StoreDataId");
b.ToTable("PairedSINData");
});
modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("DateCreated");
b.Property<DateTimeOffset>("Expiration");
b.Property<string>("Facade");
b.Property<string>("Label");
b.Property<string>("SIN");
b.Property<string>("StoreDataId");
b.Property<string>("TokenValue");
b.HasKey("Id");
b.ToTable("PairingCodes");
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<bool>("Accounted");
b.Property<byte[]>("Blob");
b.Property<string>("InvoiceDataId");
b.HasKey("Id");
b.HasIndex("InvoiceDataId");
b.ToTable("Payments");
});
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
{
b.Property<string>("Id");
b.HasKey("Id");
b.ToTable("PendingInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<string>("InvoiceDataId");
b.HasKey("Id");
b.HasIndex("InvoiceDataId");
b.ToTable("RefundAddresses");
});
modelBuilder.Entity("BTCPayServer.Data.SettingData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Value");
b.HasKey("Id");
b.ToTable("Settings");
});
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("DefaultCrypto");
b.Property<string>("DerivationStrategies");
b.Property<string>("DerivationStrategy");
b.Property<int>("SpeedPolicy");
b.Property<byte[]>("StoreBlob");
b.Property<byte[]>("StoreCertificate");
b.Property<string>("StoreName");
b.Property<string>("StoreWebsite");
b.HasKey("Id");
b.ToTable("Stores");
});
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
{
b.Property<string>("ApplicationUserId");
b.Property<string>("StoreDataId");
b.Property<string>("Role");
b.HasKey("ApplicationUserId", "StoreDataId");
b.HasIndex("StoreDataId");
b.ToTable("UserStore");
});
modelBuilder.Entity("BTCPayServer.Models.ApplicationUser", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("AccessFailedCount");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Email")
.HasMaxLength(256);
b.Property<bool>("EmailConfirmed");
b.Property<bool>("LockoutEnabled");
b.Property<DateTimeOffset?>("LockoutEnd");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256);
b.Property<string>("NormalizedUserName")
.HasMaxLength(256);
b.Property<string>("PasswordHash");
b.Property<string>("PhoneNumber");
b.Property<bool>("PhoneNumberConfirmed");
b.Property<bool>("RequiresEmailConfirmation");
b.Property<string>("SecurityStamp");
b.Property<bool>("TwoFactorEnabled");
b.Property<string>("UserName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasName("UserNameIndex");
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("BTCPayServer.Services.PaymentRequests.PaymentRequestData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<int>("Status");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("Status");
b.HasIndex("StoreDataId");
b.ToTable("PaymentRequests");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Name")
.HasMaxLength(256);
b.Property<string>("NormalizedName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasName("RoleNameIndex");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("RoleId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider");
b.Property<string>("ProviderKey");
b.Property<string>("ProviderDisplayName");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("RoleId");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("LoginProvider");
b.Property<string>("Name");
b.Property<string>("Value");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("AddressInvoices")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("APIKeys")
.HasForeignKey("StoreId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("Apps")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("HistoricalAddressInvoices")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("Invoices")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("Events")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("PairedSINs")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("Payments")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("PendingInvoices")
.HasForeignKey("Id")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("RefundAddresses")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser")
.WithMany("UserStores")
.HasForeignKey("ApplicationUserId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("UserStores")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Services.PaymentRequests.PaymentRequestData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("PaymentRequests")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,47 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace BTCPayServer.Migrations
{
public partial class AddPaymentRequests : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "PaymentRequests",
columns: table => new
{
Id = table.Column<string>(nullable: false),
StoreDataId = table.Column<string>(nullable: true),
Status = table.Column<int>(nullable: false),
Blob = table.Column<byte[]>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PaymentRequests", x => x.Id);
table.ForeignKey(
name: "FK_PaymentRequests_Stores_StoreDataId",
column: x => x.StoreDataId,
principalTable: "Stores",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_PaymentRequests_Status",
table: "PaymentRequests",
column: "Status");
migrationBuilder.CreateIndex(
name: "IX_PaymentRequests_StoreDataId",
table: "PaymentRequests",
column: "StoreDataId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "PaymentRequests");
}
}
}

View File

@ -0,0 +1,580 @@
// <auto-generated />
using System;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20190219032533_AppsTagging")]
partial class AppsTagging
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.1.8-servicing-32085");
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.Property<string>("Address")
.ValueGeneratedOnAdd();
b.Property<DateTimeOffset?>("CreatedTime");
b.Property<string>("InvoiceDataId");
b.HasKey("Address");
b.HasIndex("InvoiceDataId");
b.ToTable("AddressInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasMaxLength(50);
b.Property<string>("StoreId")
.HasMaxLength(50);
b.HasKey("Id");
b.HasIndex("StoreId");
b.ToTable("ApiKeys");
});
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("AppType");
b.Property<DateTimeOffset>("Created");
b.Property<string>("Name");
b.Property<string>("Settings");
b.Property<string>("StoreDataId");
b.Property<bool>("TagAllInvoices");
b.HasKey("Id");
b.HasIndex("StoreDataId");
b.ToTable("Apps");
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{
b.Property<string>("InvoiceDataId");
b.Property<string>("Address");
b.Property<DateTimeOffset>("Assigned");
b.Property<string>("CryptoCode");
b.Property<DateTimeOffset?>("UnAssigned");
b.HasKey("InvoiceDataId", "Address");
b.ToTable("HistoricalAddressInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<DateTimeOffset>("Created");
b.Property<string>("CustomerEmail");
b.Property<string>("ExceptionStatus");
b.Property<string>("ItemCode");
b.Property<string>("OrderId");
b.Property<string>("Status");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("StoreDataId");
b.ToTable("Invoices");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
{
b.Property<string>("InvoiceDataId");
b.Property<string>("UniqueId");
b.Property<string>("Message");
b.Property<DateTimeOffset>("Timestamp");
b.HasKey("InvoiceDataId", "UniqueId");
b.ToTable("InvoiceEvents");
});
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Facade");
b.Property<string>("Label");
b.Property<DateTimeOffset>("PairingTime");
b.Property<string>("SIN");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("SIN");
b.HasIndex("StoreDataId");
b.ToTable("PairedSINData");
});
modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("DateCreated");
b.Property<DateTimeOffset>("Expiration");
b.Property<string>("Facade");
b.Property<string>("Label");
b.Property<string>("SIN");
b.Property<string>("StoreDataId");
b.Property<string>("TokenValue");
b.HasKey("Id");
b.ToTable("PairingCodes");
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<bool>("Accounted");
b.Property<byte[]>("Blob");
b.Property<string>("InvoiceDataId");
b.HasKey("Id");
b.HasIndex("InvoiceDataId");
b.ToTable("Payments");
});
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
{
b.Property<string>("Id");
b.HasKey("Id");
b.ToTable("PendingInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<string>("InvoiceDataId");
b.HasKey("Id");
b.HasIndex("InvoiceDataId");
b.ToTable("RefundAddresses");
});
modelBuilder.Entity("BTCPayServer.Data.SettingData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Value");
b.HasKey("Id");
b.ToTable("Settings");
});
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("DefaultCrypto");
b.Property<string>("DerivationStrategies");
b.Property<string>("DerivationStrategy");
b.Property<int>("SpeedPolicy");
b.Property<byte[]>("StoreBlob");
b.Property<byte[]>("StoreCertificate");
b.Property<string>("StoreName");
b.Property<string>("StoreWebsite");
b.HasKey("Id");
b.ToTable("Stores");
});
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
{
b.Property<string>("ApplicationUserId");
b.Property<string>("StoreDataId");
b.Property<string>("Role");
b.HasKey("ApplicationUserId", "StoreDataId");
b.HasIndex("StoreDataId");
b.ToTable("UserStore");
});
modelBuilder.Entity("BTCPayServer.Models.ApplicationUser", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("AccessFailedCount");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Email")
.HasMaxLength(256);
b.Property<bool>("EmailConfirmed");
b.Property<bool>("LockoutEnabled");
b.Property<DateTimeOffset?>("LockoutEnd");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256);
b.Property<string>("NormalizedUserName")
.HasMaxLength(256);
b.Property<string>("PasswordHash");
b.Property<string>("PhoneNumber");
b.Property<bool>("PhoneNumberConfirmed");
b.Property<bool>("RequiresEmailConfirmation");
b.Property<string>("SecurityStamp");
b.Property<bool>("TwoFactorEnabled");
b.Property<string>("UserName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasName("UserNameIndex");
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Name")
.HasMaxLength(256);
b.Property<string>("NormalizedName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasName("RoleNameIndex");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("RoleId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider");
b.Property<string>("ProviderKey");
b.Property<string>("ProviderDisplayName");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("RoleId");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("LoginProvider");
b.Property<string>("Name");
b.Property<string>("Value");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("AddressInvoices")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("APIKeys")
.HasForeignKey("StoreId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("Apps")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("HistoricalAddressInvoices")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("Invoices")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("Events")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("PairedSINs")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("Payments")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("PendingInvoices")
.HasForeignKey("Id")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("RefundAddresses")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser")
.WithMany("UserStores")
.HasForeignKey("ApplicationUserId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("UserStores")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace BTCPayServer.Migrations
{
public partial class AppsTagging : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "TagAllInvoices",
table: "Apps",
nullable: false,
defaultValue: false);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "TagAllInvoices",
table: "Apps");
}
}
}

View File

@ -14,7 +14,7 @@ namespace BTCPayServer.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.1.0-rtm-30799");
.HasAnnotation("ProductVersion", "2.1.8-servicing-32085");
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
@ -63,6 +63,8 @@ namespace BTCPayServer.Migrations
b.Property<string>("StoreDataId");
b.Property<bool>("TagAllInvoices");
b.HasKey("Id");
b.HasIndex("StoreDataId");
@ -326,6 +328,26 @@ namespace BTCPayServer.Migrations
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("BTCPayServer.Services.PaymentRequests.PaymentRequestData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<int>("Status");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("Status");
b.HasIndex("StoreDataId");
b.ToTable("PaymentRequests");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
@ -526,6 +548,14 @@ namespace BTCPayServer.Migrations
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Services.PaymentRequests.PaymentRequestData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("PaymentRequests")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")

View File

@ -2,6 +2,7 @@
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using BTCPayServer.Services.Apps;
namespace BTCPayServer.Models.AppViewModels
{
@ -41,7 +42,7 @@ namespace BTCPayServer.Models.AppViewModels
[Required]
[MaxLength(5)]
[Display(Name = "The primary currency used for targets and stats. (e.g. BTC, LTC, USD, etc.)")]
[Display(Name = "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 ")]
@ -68,24 +69,15 @@ namespace BTCPayServer.Models.AppViewModels
[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; }
public string SearchTerm { get; set; }
[Display(Name = "Sort contribution perks by popularity")]
public bool SortPerksByPopularity { get; set; }
[Display(Name = "Display contribution ranking")]
public bool DisplayPerksRanking { get; set; }
}
public enum CrowdfundResetEvery
{
Never,
Hour,
Day,
Month,
Year
}
}

View File

@ -17,6 +17,10 @@ namespace BTCPayServer.Models.AppViewModels
public bool EnableShoppingCart { get; set; }
[Display(Name = "User can input custom amount")]
public bool ShowCustomAmount { get; set; }
[Display(Name = "User can input discount in %")]
public bool ShowDiscount { get; set; }
[Display(Name = "Enable tips")]
public bool EnableTips { get; set; }
public string Example1 { get; internal set; }
public string Example2 { get; internal set; }
public string ExampleCallback { get; internal set; }
@ -41,5 +45,7 @@ namespace BTCPayServer.Models.AppViewModels
[MaxLength(500)]
[Display(Name = "Custom bootstrap CSS file")]
public string CustomCSSLink { get; set; }
public string Id { get; set; }
}
}

View File

@ -31,7 +31,7 @@ namespace BTCPayServer.Models.AppViewModels
public string DisqusShortname { get; set; }
public bool AnimationsEnabled { get; set; }
public int ResetEveryAmount { get; set; }
public string ResetEvery { get; set; }
public bool NeverReset { get; set; }
public Dictionary<string, int> PerkCount { get; set; }

View File

@ -36,6 +36,8 @@ namespace BTCPayServer.Models.AppViewModels
public bool EnableShoppingCart { get; set; }
public bool ShowCustomAmount { get; set; }
public bool ShowDiscount { get; set; }
public bool EnableTips { get; set; }
public string Step { get; set; }
public string Title { get; set; }
public Item[] Items { get; set; }

View File

@ -7,6 +7,15 @@ namespace BTCPayServer.Models
{
public class ConfirmModel
{
public ConfirmModel() { }
public ConfirmModel(string title, string desc, string action = null)
{
Title = title;
Description = desc;
Action = action;
}
public string Title
{
get; set;

View File

@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
using NBitcoin.JsonConverters;
using NBitpayClient;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Models
{
public class CreateInvoiceRequest
{
[JsonProperty(PropertyName = "buyer")]
public Buyer Buyer { get; set; }
[JsonProperty(PropertyName = "buyerEmail", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string BuyerEmail { get; set; }
[JsonProperty(PropertyName = "buyerCountry", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string BuyerCountry { get; set; }
[JsonProperty(PropertyName = "buyerZip", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string BuyerZip { get; set; }
[JsonProperty(PropertyName = "buyerState", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string BuyerState { get; set; }
[JsonProperty(PropertyName = "buyerCity", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string BuyerCity { get; set; }
[JsonProperty(PropertyName = "buyerAddress2", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string BuyerAddress2 { get; set; }
[JsonProperty(PropertyName = "buyerAddress1", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string BuyerAddress1 { get; set; }
[JsonProperty(PropertyName = "buyerName", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string BuyerName { get; set; }
[JsonProperty(PropertyName = "physical", DefaultValueHandling = DefaultValueHandling.Ignore)]
public bool Physical { get; set; }
[JsonProperty(PropertyName = "redirectURL", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string RedirectURL { get; set; }
[JsonProperty(PropertyName = "notificationURL", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string NotificationURL { get; set; }
[JsonProperty(PropertyName = "extendedNotifications", DefaultValueHandling = DefaultValueHandling.Ignore)]
public bool ExtendedNotifications { get; set; }
[JsonProperty(PropertyName = "fullNotifications", DefaultValueHandling = DefaultValueHandling.Ignore)]
public bool FullNotifications { get; set; }
[JsonProperty(PropertyName = "transactionSpeed", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string TransactionSpeed { get; set; }
[JsonProperty(PropertyName = "buyerPhone", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string BuyerPhone { get; set; }
[JsonProperty(PropertyName = "posData", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string PosData { get; set; }
[JsonProperty(PropertyName = "itemCode", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string ItemCode { get; set; }
[JsonProperty(PropertyName = "itemDesc", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string ItemDesc { get; set; }
[JsonProperty(PropertyName = "orderId", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string OrderId { get; set; }
[JsonProperty(PropertyName = "currency", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Currency { get; set; }
[JsonProperty(PropertyName = "price", DefaultValueHandling = DefaultValueHandling.Ignore)]
public decimal Price { get; set; }
[JsonProperty(PropertyName = "notificationEmail", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string NotificationEmail { get; set; }
[JsonConverter(typeof(DateTimeJsonConverter))]
[JsonProperty(PropertyName = "expirationTime", DefaultValueHandling = DefaultValueHandling.Ignore)]
public DateTimeOffset? ExpirationTime { get; set; }
[JsonProperty(PropertyName = "status", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Status { get; set; }
[JsonProperty(PropertyName = "minerFees", DefaultValueHandling = DefaultValueHandling.Ignore)]
public Dictionary<string, MinerFeeInfo> MinerFees { get; set; }
[JsonProperty(PropertyName = "supportedTransactionCurrencies", DefaultValueHandling = DefaultValueHandling.Ignore)]
public Dictionary<string, InvoiceSupportedTransactionCurrency> SupportedTransactionCurrencies { get; set; }
[JsonProperty(PropertyName = "exchangeRates", DefaultValueHandling = DefaultValueHandling.Ignore)]
public Dictionary<string, Dictionary<string, decimal>> ExchangeRates { get; set; }
[JsonProperty(PropertyName = "refundable", DefaultValueHandling = DefaultValueHandling.Ignore)]
public bool Refundable { get; set; }
[JsonProperty(PropertyName = "taxIncluded", DefaultValueHandling = DefaultValueHandling.Ignore)]
public decimal? TaxIncluded { get; set; }
[JsonProperty(PropertyName = "nonce", DefaultValueHandling = DefaultValueHandling.Ignore)]
public long Nonce { get; set; }
[JsonProperty(PropertyName = "guid", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Guid { get; set; }
[JsonProperty(PropertyName = "token", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Token { get; set; }
}
}

View File

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

View File

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

View File

@ -24,7 +24,6 @@ namespace BTCPayServer.Models.InvoicingModels
public bool IsModal { get; set; }
public bool IsLightning { get; set; }
public string CryptoCode { get; set; }
public string ServerUrl { get; set; }
public string InvoiceId { get; set; }
public string BtcAddress { get; set; }
public string BtcDue { get; set; }

View File

@ -0,0 +1,157 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using BTCPayServer.Services.PaymentRequests;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace BTCPayServer.Models.PaymentRequestViewModels
{
public class ListPaymentRequestsViewModel
{
public int Skip { get; set; }
public int Count { get; set; }
public List<ViewPaymentRequestViewModel> Items { get; set; }
public string StatusMessage { get; set; }
public int Total { get; set; }
}
public class UpdatePaymentRequestViewModel
{
public UpdatePaymentRequestViewModel()
{
}
public UpdatePaymentRequestViewModel(PaymentRequestData data)
{
if (data == null)
{
return;
}
Id = data.Id;
StoreId = data.StoreDataId;
var blob = data.GetBlob();
Title = blob.Title;
Amount = blob.Amount;
Currency = blob.Currency;
Description = blob.Description;
ExpiryDate = blob.ExpiryDate;
Email = blob.Email;
CustomCSSLink = blob.CustomCSSLink;
EmbeddedCSS = blob.EmbeddedCSS;
AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts;
}
public string Id { get; set; }
[Required] public string StoreId { get; set; }
[Required] public decimal Amount { get; set; }
[Display(Name = "The currency used for payment request. (e.g. BTC, LTC, USD, etc.)")]
public string Currency { get; set; }
[Display(Name = "Expiration Date")]
public DateTime? ExpiryDate { get; set; }
[Required] public string Title { get; set; }
public string Description { get; set; }
public string StatusMessage { get; set; }
public SelectList Stores { get; set; }
[EmailAddress]
public string Email { 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 = "Allow payee to create invoices in their own denomination")]
public bool AllowCustomPaymentAmounts { get; set; }
}
public class ViewPaymentRequestViewModel
{
public ViewPaymentRequestViewModel(PaymentRequestData data)
{
Id = data.Id;
var blob = data.GetBlob();
Title = blob.Title;
Amount = blob.Amount;
Currency = blob.Currency;
Description = blob.Description;
ExpiryDate = blob.ExpiryDate;
Email = blob.Email;
EmbeddedCSS = blob.EmbeddedCSS;
CustomCSSLink = blob.CustomCSSLink;
AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts;
switch (data.Status)
{
case PaymentRequestData.PaymentRequestStatus.Pending:
Status = ExpiryDate.HasValue ? $"Expires on {ExpiryDate.Value:g}" : "Pending";
IsPending = true;
break;
case PaymentRequestData.PaymentRequestStatus.Completed:
Status = "Settled";
break;
case PaymentRequestData.PaymentRequestStatus.Expired:
Status = "Expired";
break;
default:
throw new ArgumentOutOfRangeException();
}
}
public bool AllowCustomPaymentAmounts { get; set; }
public string Email { get; set; }
public string Status { get; set; }
public bool IsPending { get; set; }
public decimal AmountCollected { get; set; }
public decimal AmountDue { get; set; }
public string AmountDueFormatted { get; set; }
public decimal Amount { get; set; }
public string Id { get; set; }
public string Currency { get; set; }
public DateTime? ExpiryDate { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string EmbeddedCSS { get; set; }
public string CustomCSSLink { get; set; }
public List<PaymentRequestInvoice> Invoices { get; set; } = new List<PaymentRequestInvoice>();
public DateTime LastUpdated { get; set; }
public CurrencyData CurrencyData { get; set; }
public string AmountCollectedFormatted { get; set; }
public string AmountFormatted { get; set; }
public bool AnyPendingInvoice { get; set; }
public class PaymentRequestInvoice
{
public string Id { get; set; }
public DateTime ExpiryDate { get; set; }
public decimal Amount { get; set; }
public string Status { get; set; }
public List<PaymentRequestInvoicePayment> Payments { get; set; }
public string Currency { get; set; }
}
public class PaymentRequestInvoicePayment
{
public string PaymentMethod { get; set; }
public decimal Amount { get; set; }
public string Link { get; set; }
public string Id { get; set; }
}
}
}

View File

@ -5,9 +5,10 @@ using System.Threading.Tasks;
namespace BTCPayServer.Models.ServerViewModels
{
public class SparkServicesViewModel
public class LightningWalletServices
{
public string SparkLink { get; set; }
public string ServiceLink { get; set; }
public bool ShowQR { get; set; }
public string WalletName { get; internal set; }
}
}

View File

@ -0,0 +1,80 @@
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Models
{
public class StatusMessageModel
{
public StatusMessageModel()
{
}
public StatusMessageModel(string s)
{
if (string.IsNullOrEmpty(s))
return;
try
{
if (s.StartsWith("{", StringComparison.InvariantCultureIgnoreCase) &&
s.EndsWith("}", StringComparison.InvariantCultureIgnoreCase))
{
var model = JObject.Parse(s).ToObject<StatusMessageModel>();
Html = model.Html;
Message = model.Message;
Severity = model.Severity;
}
else
{
ParseNonJsonStatus(s);
}
}
catch (Exception)
{
ParseNonJsonStatus(s);
}
}
public string Message { get; set; }
public string Html { get; set; }
public StatusSeverity Severity { get; set; }
public string SeverityCSS
{
get
{
switch (Severity)
{
case StatusSeverity.Info:
return "info";
case StatusSeverity.Error:
return "danger";
case StatusSeverity.Success:
return "success";
default:
throw new ArgumentOutOfRangeException();
}
}
}
public override string ToString()
{
return JObject.FromObject(this).ToString(Formatting.None);
}
private void ParseNonJsonStatus(string s)
{
Message = s;
Severity = s.StartsWith("Error", StringComparison.InvariantCultureIgnoreCase)
? StatusSeverity.Error
: StatusSeverity.Success;
}
public enum StatusSeverity
{
Info,
Error,
Success
}
}
}

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Payments;
using BTCPayServer.Services;
using BTCPayServer.Validation;
using Microsoft.AspNetCore.Mvc.Rendering;
@ -11,16 +12,17 @@ namespace BTCPayServer.Models.StoreViewModels
{
public class CheckoutExperienceViewModel
{
class Format
public class Format
{
public string Name { get; set; }
public string Value { get; set; }
public PaymentMethodId PaymentId { get; set; }
}
public SelectList CryptoCurrencies { get; set; }
public SelectList Languages { get; set; }
[Display(Name = "Default crypto currency on checkout")]
public string DefaultCryptoCurrency { get; set; }
[Display(Name = "Default the default payment method on checkout")]
public string DefaultPaymentMethod { get; set; }
[Display(Name = "Default language on checkout")]
public string DefaultLang { get; set; }
[Display(Name = "Do not propose lightning payment if value of the invoice is above...")]
@ -47,15 +49,6 @@ namespace BTCPayServer.Models.StoreViewModels
[Display(Name = "Custom HTML title to display on Checkout page")]
public string HtmlTitle { get; set; }
public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string defaultCrypto)
{
var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Value == defaultCrypto) ?? choices.FirstOrDefault();
CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
DefaultCryptoCurrency = chosen.Name;
}
public void SetLanguages(LanguageService langService, string defaultLang)
{
defaultLang = langService.GetLanguages().Any(language => language.Code == defaultLang)? defaultLang : "en";

View File

@ -0,0 +1,175 @@
using System;
using System.Linq;
using Microsoft.Extensions.Logging;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Events;
using BTCPayServer.HostedServices;
using BTCPayServer.Logging;
using BTCPayServer.Payments;
using BTCPayServer.Services.PaymentRequests;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using BTCPayServer.Services.Apps;
namespace BTCPayServer.PaymentRequest
{
public class PaymentRequestHub : Hub
{
private readonly PaymentRequestController _PaymentRequestController;
public const string InvoiceCreated = "InvoiceCreated";
public const string PaymentReceived = "PaymentReceived";
public const string InfoUpdated = "InfoUpdated";
public const string InvoiceError = "InvoiceError";
public PaymentRequestHub(PaymentRequestController paymentRequestController)
{
_PaymentRequestController = paymentRequestController;
}
public async Task ListenToPaymentRequest(string paymentRequestId)
{
if (Context.Items.ContainsKey("pr-id"))
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, Context.Items["pr-id"].ToString());
Context.Items.Remove("pr-id");
}
Context.Items.Add("pr-id", paymentRequestId);
await Groups.AddToGroupAsync(Context.ConnectionId, paymentRequestId);
}
public async Task Pay(decimal? amount = null)
{
_PaymentRequestController.ControllerContext.HttpContext = Context.GetHttpContext();
var result =
await _PaymentRequestController.PayPaymentRequest(Context.Items["pr-id"].ToString(), false, amount);
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;
}
}
}
public class PaymentRequestStreamer : EventHostedServiceBase
{
private readonly IHubContext<PaymentRequestHub> _HubContext;
private readonly PaymentRequestRepository _PaymentRequestRepository;
private readonly PaymentRequestService _PaymentRequestService;
public PaymentRequestStreamer(EventAggregator eventAggregator,
IHubContext<PaymentRequestHub> hubContext,
PaymentRequestRepository paymentRequestRepository,
PaymentRequestService paymentRequestService) : base(eventAggregator)
{
_HubContext = hubContext;
_PaymentRequestRepository = paymentRequestRepository;
_PaymentRequestService = paymentRequestService;
}
public override async Task StartAsync(CancellationToken cancellationToken)
{
await base.StartAsync(cancellationToken);
_CheckingPendingPayments = CheckingPendingPayments(cancellationToken)
.ContinueWith(_ => _CheckingPendingPayments = null, TaskScheduler.Default);
}
private async Task CheckingPendingPayments(CancellationToken cancellationToken)
{
Logs.PayServer.LogInformation("Starting payment request expiration watcher");
var (total, items) = await _PaymentRequestRepository.FindPaymentRequests(new PaymentRequestQuery()
{
Status = new[] {PaymentRequestData.PaymentRequestStatus.Pending}
}, cancellationToken);
Logs.PayServer.LogInformation($"{total} pending payment requests being checked since last run");
await Task.WhenAll(items.Select(i => _PaymentRequestService.UpdatePaymentRequestStateIfNeeded(i))
.ToArray());
}
Task _CheckingPendingPayments;
public override async Task StopAsync(CancellationToken cancellationToken)
{
await base.StopAsync(cancellationToken);
await (_CheckingPendingPayments ?? Task.CompletedTask);
}
protected override void SubscibeToEvents()
{
Subscribe<InvoiceEvent>();
Subscribe<PaymentRequestUpdated>();
}
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
{
if (evt is InvoiceEvent invoiceEvent)
{
foreach (var paymentId in PaymentRequestRepository.GetPaymentIdsFromInternalTags(invoiceEvent.Invoice))
{
if (invoiceEvent.Name == InvoiceEvent.ReceivedPayment)
{
await _PaymentRequestService.UpdatePaymentRequestStateIfNeeded(paymentId);
var data = invoiceEvent.Payment.GetCryptoPaymentData();
await _HubContext.Clients.Group(paymentId).SendCoreAsync(PaymentRequestHub.PaymentReceived,
new object[]
{
data.GetValue(),
invoiceEvent.Payment.GetCryptoCode(),
Enum.GetName(typeof(PaymentTypes),
invoiceEvent.Payment.GetPaymentMethodId().PaymentType)
});
}
await InfoUpdated(paymentId);
}
}
else if (evt is PaymentRequestUpdated updated)
{
await InfoUpdated(updated.PaymentRequestId);
var expiry = updated.Data.GetBlob().ExpiryDate;
if (updated.Data.Status == PaymentRequestData.PaymentRequestStatus.Pending &&
expiry.HasValue)
{
QueueExpiryTask(
updated.PaymentRequestId,
expiry.Value,
cancellationToken);
}
}
}
private void QueueExpiryTask(string paymentRequestId, DateTime expiry, CancellationToken cancellationToken)
{
Task.Run(async () =>
{
var delay = expiry - DateTime.Now;
if (delay > TimeSpan.Zero)
await Task.Delay(delay, cancellationToken);
await _PaymentRequestService.UpdatePaymentRequestStateIfNeeded(paymentRequestId);
}, cancellationToken);
}
private async Task InfoUpdated(string paymentRequestId)
{
var req = await _PaymentRequestService.GetPaymentRequest(paymentRequestId);
if (req != null)
{
await _HubContext.Clients.Group(paymentRequestId)
.SendCoreAsync(PaymentRequestHub.InfoUpdated, new object[] {req});
}
}
}
}

View File

@ -0,0 +1,136 @@
using System;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Models.PaymentRequestViewModels;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.PaymentRequests;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.SignalR;
namespace BTCPayServer.PaymentRequest
{
public class PaymentRequestService
{
private readonly PaymentRequestRepository _PaymentRequestRepository;
private readonly BTCPayNetworkProvider _BtcPayNetworkProvider;
private readonly AppService _AppService;
private readonly CurrencyNameTable _currencies;
public PaymentRequestService(
IHubContext<PaymentRequestHub> hubContext,
PaymentRequestRepository paymentRequestRepository,
BTCPayNetworkProvider btcPayNetworkProvider,
AppService appService,
CurrencyNameTable currencies)
{
_PaymentRequestRepository = paymentRequestRepository;
_BtcPayNetworkProvider = btcPayNetworkProvider;
_AppService = appService;
_currencies = currencies;
}
public async Task UpdatePaymentRequestStateIfNeeded(string id)
{
var pr = await _PaymentRequestRepository.FindPaymentRequest(id, null);
await UpdatePaymentRequestStateIfNeeded(pr);
}
public async Task UpdatePaymentRequestStateIfNeeded(PaymentRequestData pr)
{
var blob = pr.GetBlob();
var currentStatus = pr.Status;
if (blob.ExpiryDate.HasValue)
{
if (blob.ExpiryDate.Value <= DateTimeOffset.UtcNow)
currentStatus = PaymentRequestData.PaymentRequestStatus.Expired;
}
else if (pr.Status == PaymentRequestData.PaymentRequestStatus.Pending)
{
var rateRules = pr.StoreData.GetStoreBlob().GetRateRules(_BtcPayNetworkProvider);
var invoices = await _PaymentRequestRepository.GetInvoicesForPaymentRequest(pr.Id);
var paymentStats = _AppService.GetCurrentContributionAmountStats(invoices, true);
var amountCollected =
await _AppService.GetCurrentContributionAmount(paymentStats, blob.Currency, rateRules);
if (amountCollected >= blob.Amount)
{
currentStatus = PaymentRequestData.PaymentRequestStatus.Completed;
}
}
if (currentStatus != pr.Status)
{
pr.Status = currentStatus;
await _PaymentRequestRepository.UpdatePaymentRequestStatus(pr.Id, currentStatus);
}
}
public async Task<ViewPaymentRequestViewModel> GetPaymentRequest(string id, string userId = null)
{
var pr = await _PaymentRequestRepository.FindPaymentRequest(id, null);
if (pr == null)
{
return null;
}
var blob = pr.GetBlob();
var rateRules = pr.StoreData.GetStoreBlob().GetRateRules(_BtcPayNetworkProvider);
var invoices = await _PaymentRequestRepository.GetInvoicesForPaymentRequest(id);
var paymentStats = _AppService.GetCurrentContributionAmountStats(invoices, true);
var amountCollected =
await _AppService.GetCurrentContributionAmount(paymentStats, blob.Currency, rateRules);
var amountDue = blob.Amount - amountCollected;
return new ViewPaymentRequestViewModel(pr)
{
AmountFormatted = _currencies.FormatCurrency(blob.Amount, blob.Currency),
AmountCollected = amountCollected,
AmountCollectedFormatted = _currencies.FormatCurrency(amountCollected, blob.Currency),
AmountDue = amountDue,
AmountDueFormatted = _currencies.FormatCurrency(amountDue, blob.Currency),
CurrencyData = _currencies.GetCurrencyData(blob.Currency, true),
LastUpdated = DateTime.Now,
AnyPendingInvoice = invoices.Any(entity => entity.Status == InvoiceStatus.New),
Invoices = invoices.Select(entity => new ViewPaymentRequestViewModel.PaymentRequestInvoice()
{
Id = entity.Id,
Amount = entity.ProductInformation.Price,
Currency = entity.ProductInformation.Currency,
ExpiryDate = entity.ExpirationTime.DateTime,
Status = entity.GetInvoiceState().ToString(),
Payments = entity.GetPayments().Select(paymentEntity =>
{
var paymentNetwork = _BtcPayNetworkProvider.GetNetwork(paymentEntity.GetCryptoCode());
var paymentData = paymentEntity.GetCryptoPaymentData();
string link = null;
string txId = null;
switch (paymentData)
{
case Payments.Bitcoin.BitcoinLikePaymentData onChainPaymentData:
txId = onChainPaymentData.Outpoint.Hash.ToString();
link = string.Format(CultureInfo.InvariantCulture, paymentNetwork.BlockExplorerLink,
txId);
break;
case LightningLikePaymentData lightningLikePaymentData:
txId = lightningLikePaymentData.BOLT11;
break;
}
return new ViewPaymentRequestViewModel.PaymentRequestInvoicePayment()
{
Amount = paymentData.GetValue(),
PaymentMethod = paymentEntity.GetPaymentMethodId().ToString(),
Link = link,
Id = txId
};
}).ToList()
}).ToList()
};
}
}
}

View File

@ -33,12 +33,10 @@ namespace BTCPayServer.Payments.Bitcoin
private TaskCompletionSource<bool> _RunningTask;
private CancellationTokenSource _Cts;
BTCPayWalletProvider _Wallets;
BTCPayNetworkProvider _NetworkProvider;
public NBXplorerListener(ExplorerClientProvider explorerClients,
BTCPayWalletProvider wallets,
InvoiceRepository invoiceRepository,
BTCPayNetworkProvider networkProvider,
EventAggregator aggregator, Microsoft.Extensions.Hosting.IApplicationLifetime lifetime)
{
PollInterval = TimeSpan.FromMinutes(1.0);
@ -47,7 +45,6 @@ namespace BTCPayServer.Payments.Bitcoin
_ExplorerClients = explorerClients;
_Aggregator = aggregator;
_Lifetime = lifetime;
_NetworkProvider = networkProvider;
}
CompositeDisposable leases = new CompositeDisposable();
@ -373,7 +370,7 @@ namespace BTCPayServer.Payments.Bitcoin
invoice.SetPaymentMethod(paymentMethod);
}
wallet.InvalidateCache(strategy);
_Aggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1002, InvoiceEvent.ReceivedPayment){Payment = payment});
_Aggregator.Publish(new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment){Payment = payment});
return invoice;
}
public Task StopAsync(CancellationToken cancellationToken)

View File

@ -12,6 +12,21 @@ namespace BTCPayServer.Payments
public class PaymentFilter
{
class OrPaymentFilter : IPaymentFilter
{
private readonly IPaymentFilter _a;
private readonly IPaymentFilter _b;
public OrPaymentFilter(IPaymentFilter a, IPaymentFilter b)
{
_a = a;
_b = b;
}
public bool Match(PaymentMethodId paymentMethodId)
{
return _a.Match(paymentMethodId) || _b.Match(paymentMethodId);
}
}
class NeverPaymentFilter : IPaymentFilter
{
@ -54,6 +69,34 @@ namespace BTCPayServer.Payments
return paymentMethodId == _paymentMethodId;
}
}
class PredicateFilter : IPaymentFilter
{
private Func<PaymentMethodId, bool> predicate;
public PredicateFilter(Func<PaymentMethodId, bool> predicate)
{
this.predicate = predicate;
}
public bool Match(PaymentMethodId paymentMethodId)
{
return this.predicate(paymentMethodId);
}
}
public static IPaymentFilter Where(Func<PaymentMethodId, bool> predicate)
{
if (predicate == null)
throw new ArgumentNullException(nameof(predicate));
return new PredicateFilter(predicate);
}
public static IPaymentFilter Or(IPaymentFilter a, IPaymentFilter b)
{
if (a == null)
throw new ArgumentNullException(nameof(a));
if (b == null)
throw new ArgumentNullException(nameof(b));
return new OrPaymentFilter(a, b);
}
public static IPaymentFilter Never()
{
return NeverPaymentFilter.Instance;

View File

@ -7,6 +7,7 @@ using BTCPayServer.Lightning;
using BTCPayServer.Lightning.JsonConverters;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services.Invoices;
using NBitcoin;
using Newtonsoft.Json;
namespace BTCPayServer.Payments.Lightning
@ -16,17 +17,21 @@ namespace BTCPayServer.Payments.Lightning
[JsonConverter(typeof(LightMoneyJsonConverter))]
public LightMoney Amount { get; set; }
public string BOLT11 { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))]
public uint256 PaymentHash { get; set; }
public string GetDestination(BTCPayNetwork network)
{
return GetPaymentId();
return BOLT11;
}
public decimal NetworkFee { get; set; }
public string GetPaymentId()
{
return BOLT11;
// Legacy, some old payments don't have the PaymentHash set
return PaymentHash?.ToString() ?? BOLT11;
}
public PaymentTypes GetPaymentType()

View File

@ -191,13 +191,14 @@ namespace BTCPayServer.Payments.Lightning
var payment = await _InvoiceRepository.AddPayment(listenedInvoice.InvoiceId, notification.PaidAt.Value, new LightningLikePaymentData()
{
BOLT11 = notification.BOLT11,
PaymentHash = BOLT11PaymentRequest.Parse(notification.BOLT11, network.NBitcoinNetwork).PaymentHash,
Amount = notification.Amount
}, network, accounted: true);
if (payment != null)
{
var invoice = await _InvoiceRepository.GetInvoice(listenedInvoice.InvoiceId);
if (invoice != null)
_Aggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1002, InvoiceEvent.ReceivedPayment){Payment = payment});
_Aggregator.Publish(new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment){Payment = payment});
}
}

View File

@ -67,10 +67,37 @@ namespace BTCPayServer.Payments
return CryptoCode + "_" + PaymentType.ToString();
}
public static bool TryParse(string str, out PaymentMethodId paymentMethodId)
{
paymentMethodId = null;
var parts = str.Split('_', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 0 || parts.Length > 2)
return false;
PaymentTypes type = PaymentTypes.BTCLike;
if (parts.Length == 2)
{
switch (parts[1].ToLowerInvariant())
{
case "btclike":
case "onchain":
type = PaymentTypes.BTCLike;
break;
case "lightninglike":
case "offchain":
type = PaymentTypes.LightningLike;
break;
default:
return false;
}
}
paymentMethodId = new PaymentMethodId(parts[0], type);
return true;
}
public static PaymentMethodId Parse(string str)
{
var parts = str.Split('_');
return new PaymentMethodId(parts[0], parts.Length == 1 ? PaymentTypes.BTCLike : Enum.Parse<PaymentTypes>(parts[1]));
if (!TryParse(str, out var result))
throw new FormatException("Invalid PaymentMethodId");
return result;
}
}
}

View File

@ -6,9 +6,9 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
namespace BTCPayServer.Hubs
namespace BTCPayServer.Services.Apps
{
public class CrowdfundHub: Hub
public class AppHub: Hub
{
public const string InvoiceCreated = "InvoiceCreated";
public const string PaymentReceived = "PaymentReceived";
@ -16,7 +16,7 @@ namespace BTCPayServer.Hubs
public const string InvoiceError = "InvoiceError";
private readonly AppsPublicController _AppsPublicController;
public CrowdfundHub(AppsPublicController appsPublicController)
public AppHub(AppsPublicController appsPublicController)
{
_AppsPublicController = appsPublicController;
}

View File

@ -0,0 +1,78 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.HostedServices;
using BTCPayServer.Logging;
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.Hosting;
using Microsoft.Extensions.Logging;
using NBitcoin;
namespace BTCPayServer.Services.Apps
{
public class AppHubStreamer : EventHostedServiceBase
{
private readonly AppService _appService;
private IHubContext<AppHub> _HubContext;
public AppHubStreamer(EventAggregator eventAggregator,
IHubContext<AppHub> hubContext,
AppService appService) : base(eventAggregator)
{
_appService = appService;
_HubContext = hubContext;
}
protected override void SubscibeToEvents()
{
Subscribe<InvoiceEvent>();
Subscribe<AppsController.AppUpdated>();
}
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
{
if (evt is InvoiceEvent invoiceEvent)
{
foreach (var appId in AppService.GetAppInternalTags(invoiceEvent.Invoice))
{
if (invoiceEvent.Name == InvoiceEvent.ReceivedPayment)
{
var data = invoiceEvent.Payment.GetCryptoPaymentData();
await _HubContext.Clients.Group(appId).SendCoreAsync(AppHub.PaymentReceived, new object[]
{
data.GetValue(),
invoiceEvent.Payment.GetCryptoCode(),
Enum.GetName(typeof(PaymentTypes),
invoiceEvent.Payment.GetPaymentMethodId().PaymentType)
}, cancellationToken);
}
await InfoUpdated(appId);
}
}
else if (evt is AppsController.AppUpdated app)
{
await InfoUpdated(app.AppId);
}
}
private async Task InfoUpdated(string appId)
{
var info = await _appService.GetAppInfo(appId);
await _HubContext.Clients.Group(appId).SendCoreAsync(AppHub.InfoUpdated, new object[] { info });
}
}
}

View File

@ -0,0 +1,398 @@
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.Data;
using BTCPayServer.Filters;
using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Rating;
using BTCPayServer.Security;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using Ganss.XSS;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NBitpayClient;
using YamlDotNet.RepresentationModel;
using static BTCPayServer.Controllers.AppsController;
namespace BTCPayServer.Services.Apps
{
public class AppService
{
ApplicationDbContextFactory _ContextFactory;
private readonly InvoiceRepository _InvoiceRepository;
CurrencyNameTable _Currencies;
private readonly RateFetcher _RateFetcher;
private readonly HtmlSanitizer _HtmlSanitizer;
private readonly BTCPayNetworkProvider _Networks;
public CurrencyNameTable Currencies => _Currencies;
public AppService(ApplicationDbContextFactory contextFactory,
InvoiceRepository invoiceRepository,
BTCPayNetworkProvider networks,
CurrencyNameTable currencies,
RateFetcher rateFetcher,
HtmlSanitizer htmlSanitizer)
{
_ContextFactory = contextFactory;
_InvoiceRepository = invoiceRepository;
_Currencies = currencies;
_RateFetcher = rateFetcher;
_HtmlSanitizer = htmlSanitizer;
_Networks = networks;
}
public async Task<object> GetAppInfo(string appId)
{
var app = await GetApp(appId, AppType.Crowdfund, true);
return await GetInfo(app);
}
private async Task<ViewCrowdfundViewModel> GetInfo(AppData appData, string statusMessage = null)
{
var settings = appData.GetSettings<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(appData, lastResetDate);
var completeInvoices = invoices.Where(entity => entity.Status == InvoiceStatus.Complete || entity.Status == InvoiceStatus.Confirmed).ToArray();
var pendingInvoices = invoices.Where(entity => !(entity.Status == InvoiceStatus.Complete || entity.Status == InvoiceStatus.Confirmed)).ToArray();
var rateRules = appData.StoreData.GetStoreBlob().GetRateRules(_Networks);
var pendingPaymentStats = GetCurrentContributionAmountStats(pendingInvoices, !settings.EnforceTargetAmount);
var paymentStats = GetCurrentContributionAmountStats(completeInvoices, !settings.EnforceTargetAmount);
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 = 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,
NeverReset = settings.ResetEvery == CrowdfundResetEvery.Never,
CurrencyData = _Currencies.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
}
};
}
public static string GetCrowdfundOrderId(string appId) => $"crowdfund-app_{appId}";
public static string GetAppInternalTag(string appId) => $"APP#{appId}";
public static string[] GetAppInternalTags(InvoiceEntity invoice)
{
return invoice.GetInternalTags("APP#");
}
private async Task<InvoiceEntity[]> GetInvoicesForApp(AppData appData, DateTime? startDate = null)
{
var invoices = await _InvoiceRepository.GetInvoices(new InvoiceQuery()
{
StoreId = new[] { appData.StoreData.Id },
OrderId = appData.TagAllInvoices ? null : new[] { GetCrowdfundOrderId(appData.Id) },
Status = new string[]{
InvoiceState.ToString(InvoiceStatus.New),
InvoiceState.ToString(InvoiceStatus.Paid),
InvoiceState.ToString(InvoiceStatus.Confirmed),
InvoiceState.ToString(InvoiceStatus.Complete)},
StartDate = startDate
});
// Old invoices may have invoices which were not tagged
invoices = invoices.Where(inv => inv.Version < InvoiceEntity.InternalTagSupport_Version ||
inv.InternalTags.Contains(GetAppInternalTag(appData.Id))).ToArray();
return invoices;
}
public async Task<StoreData[]> GetOwnedStores(string userId)
{
using (var ctx = _ContextFactory.CreateContext())
{
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();
}
}
public async Task<StoreData> GetStore(AppData app)
{
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.Stores.FirstOrDefaultAsync(s => s.Id == app.StoreDataId);
}
}
public ViewPointOfSaleViewModel.Item[] Parse(string template, string currency)
{
if (string.IsNullOrWhiteSpace(template))
return Array.Empty<ViewPointOfSaleViewModel.Item>();
var input = new StringReader(template);
YamlStream stream = new YamlStream();
stream.Load(input);
var root = (YamlMappingNode)stream.Documents[0].RootNode;
return root
.Children
.Select(kv => new PosHolder { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlMappingNode })
.Where(kv => kv.Value != null)
.Select(c => new ViewPointOfSaleViewModel.Item()
{
Description = _HtmlSanitizer.Sanitize(c.GetDetailString("description")),
Id = c.Key,
Image = _HtmlSanitizer.Sanitize(c.GetDetailString("image")),
Title = _HtmlSanitizer.Sanitize(c.GetDetailString("title") ?? c.Key),
Price = c.GetDetail("price")
.Select(cc => new ViewPointOfSaleViewModel.Item.ItemPrice()
{
Value = decimal.Parse(cc.Value.Value, CultureInfo.InvariantCulture),
Formatted = Currencies.FormatCurrency(cc.Value.Value, currency)
}).Single(),
Custom = c.GetDetailString("custom") == "true"
})
.ToArray();
}
public async Task<decimal> GetCurrentContributionAmount(Dictionary<string, decimal> stats, string primaryCurrency, RateRules rateRules)
{
var result = new List<decimal>();
var ratesTask = _RateFetcher.FetchRates(
stats.Keys
.Select((x) => new CurrencyPair(primaryCurrency, PaymentMethodId.Parse(x).CryptoCode))
.Distinct()
.ToHashSet(),
rateRules).Select(async rateTask =>
{
var (key, value) = rateTask;
var tResult = await value;
var rate = tResult.BidAsk?.Bid;
if (rate == null)
return;
foreach (var stat in stats)
{
if (string.Equals(PaymentMethodId.Parse(stat.Key).CryptoCode, key.Right,
StringComparison.InvariantCultureIgnoreCase))
{
result.Add((1m / rate.Value) * stat.Value);
}
}
});
await Task.WhenAll(ratesTask);
return result.Sum();
}
public Dictionary<string, decimal> GetCurrentContributionAmountStats(InvoiceEntity[] invoices, bool softcap)
{
return invoices
.SelectMany(p =>
{
// For hardcap, we count newly created invoices as part of the contributions
if (!softcap && p.Status == InvoiceStatus.New)
return new[] { (Key: p.ProductInformation.Currency, Value: p.ProductInformation.Price) };
// If the user get a donation via other mean, he can register an invoice manually for such amount
// then mark the invoice as complete
var payments = p.GetPayments();
if (payments.Count == 0 &&
p.ExceptionStatus == InvoiceExceptionStatus.Marked &&
p.Status == InvoiceStatus.Complete)
return new[] { (Key: p.ProductInformation.Currency, Value: p.ProductInformation.Price) };
// If an invoice has been marked invalid, remove the contribution
if (p.ExceptionStatus == InvoiceExceptionStatus.Marked &&
p.Status == InvoiceStatus.Invalid)
return new[] { (Key: p.ProductInformation.Currency, Value: 0m) };
// Else, we just sum the payments
return payments
.Select(pay => (Key: pay.GetPaymentMethodId().ToString(), Value: pay.GetCryptoPaymentData().GetValue() - pay.NetworkFee))
.ToArray();
})
.GroupBy(p => p.Key)
.ToDictionary(p => p.Key, p => p.Select(v => v.Value).Sum());
}
private class PosHolder
{
public string Key { get; set; }
public YamlMappingNode Value { get; set; }
public IEnumerable<PosScalar> GetDetail(string field)
{
var res = Value.Children
.Where(kv => kv.Value != null)
.Select(kv => new PosScalar { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlScalarNode })
.Where(cc => cc.Key == field);
return res;
}
public string GetDetailString(string field)
{
return GetDetail(field).FirstOrDefault()?.Value?.Value;
}
}
private class PosScalar
{
public string Key { get; set; }
public YamlScalarNode Value { get; set; }
}
public async Task<AppData> GetAppDataIfOwner(string userId, string appId, AppType? type = null)
{
if (userId == null || appId == null)
return null;
using (var ctx = _ContextFactory.CreateContext())
{
var app = await ctx.UserStore
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
.SelectMany(us => us.StoreData.Apps.Where(a => a.Id == appId))
.FirstOrDefaultAsync();
if (app == null)
return null;
if (type != null && type.Value.ToString() != app.AppType)
return null;
return app;
}
}
}
}

View File

@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Services.Apps
{
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 int ResetEveryAmount { get; set; } = 1;
public CrowdfundResetEvery ResetEvery { get; set; } = CrowdfundResetEvery.Never;
[Obsolete("Use AppData.TagAllInvoices instead")]
public bool UseAllStoreInvoices { get; set; }
public bool DisplayPerksRanking { get; set; }
public bool SortPerksByPopularity { get; set; }
}
public enum CrowdfundResetEvery
{
Never,
Hour,
Day,
Month,
Year
}
}

View File

@ -102,6 +102,7 @@ namespace BTCPayServer.Services.Invoices.Export
InvoiceItemDesc = invoice.ProductInformation.ItemDesc,
InvoicePrice = invoice.ProductInformation.Price,
InvoiceCurrency = invoice.ProductInformation.Currency,
BuyerEmail = invoice.BuyerInformation?.BuyerEmail
};
exportList.Add(target);
@ -139,5 +140,6 @@ namespace BTCPayServer.Services.Invoices.Export
public string InvoiceFullStatus { get; set; }
public string InvoiceStatus { get; set; }
public string InvoiceExceptionStatus { get; set; }
public string BuyerEmail { get; set; }
}
}

View File

@ -91,6 +91,12 @@ namespace BTCPayServer.Services.Invoices
get; set;
}
[JsonProperty(PropertyName = "taxIncluded", DefaultValueHandling = DefaultValueHandling.Ignore)]
public decimal TaxIncluded
{
get; set;
}
[JsonProperty(PropertyName = "currency")]
public string Currency
{
@ -107,6 +113,9 @@ namespace BTCPayServer.Services.Invoices
}
public class InvoiceEntity
{
public const int InternalTagSupport_Version = 1;
public const int Lastest_Version = 1;
public int Version { get; set; }
public string Id
{
get; set;
@ -158,6 +167,16 @@ namespace BTCPayServer.Services.Invoices
set;
}
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public HashSet<string> InternalTags { get; set; } = new HashSet<string>();
public string[] GetInternalTags(string suffix)
{
return InternalTags == null ? Array.Empty<string>() : InternalTags
.Where(t => t.StartsWith(suffix, StringComparison.InvariantCulture))
.Select(t => t.Substring(suffix.Length)).ToArray();
}
[Obsolete("Use GetDerivationStrategies instead")]
public string DerivationStrategy
{

View File

@ -93,6 +93,16 @@ retry:
}
}
public async Task<AppData[]> GetAppsTaggingStore(string storeId)
{
if (storeId == null)
throw new ArgumentNullException(nameof(storeId));
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.Apps.Where(a => a.StoreDataId == storeId && a.TagAllInvoices).ToArrayAsync();
}
}
public async Task UpdateInvoice(string invoiceId, UpdateCustomerModel data)
{
using (var ctx = _ContextFactory.CreateContext())
@ -178,7 +188,6 @@ retry:
textSearch.Add(invoice.StoreId);
AddToTextSearch(invoice.Id, textSearch.ToArray());
return invoice;
}

View File

@ -11,5 +11,6 @@ namespace BTCPayServer.Services
public bool DeprecatedLightningConnectionStringCheck { get; set; }
public bool ConvertMultiplierToSpread { get; set; }
public bool ConvertNetworkFeeProperty { get; set; }
public bool ConvertCrowdfundOldSettings { get; set; }
}
}

View File

@ -0,0 +1,254 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Services.Invoices;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Internal;
using NBitcoin;
using NBXplorer;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.PaymentRequests
{
public class PaymentRequestRepository
{
private readonly ApplicationDbContextFactory _ContextFactory;
private readonly InvoiceRepository _InvoiceRepository;
public PaymentRequestRepository(ApplicationDbContextFactory contextFactory, InvoiceRepository invoiceRepository)
{
_ContextFactory = contextFactory;
_InvoiceRepository = invoiceRepository;
}
public async Task<PaymentRequestData> CreateOrUpdatePaymentRequest(PaymentRequestData entity)
{
using (var context = _ContextFactory.CreateContext())
{
if (string.IsNullOrEmpty(entity.Id))
{
await context.PaymentRequests.AddAsync(entity);
}
else
{
context.PaymentRequests.Update(entity);
}
await context.SaveChangesAsync();
return entity;
}
}
public async Task<PaymentRequestData> FindPaymentRequest(string id, string userId, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(id))
{
return null;
}
using (var context = _ContextFactory.CreateContext())
{
return await context.PaymentRequests.Include(x => x.StoreData)
.Where(data =>
string.IsNullOrEmpty(userId) ||
(data.StoreData != null && data.StoreData.UserStores.Any(u => u.ApplicationUserId == userId)))
.SingleOrDefaultAsync(x => x.Id == id, cancellationToken);
}
}
public async Task<bool> IsPaymentRequestAdmin(string paymentRequestId, string userId)
{
if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(paymentRequestId))
{
return false;
}
using (var context = _ContextFactory.CreateContext())
{
return await context.PaymentRequests.Include(x => x.StoreData)
.AnyAsync(data =>
data.Id == paymentRequestId &&
(data.StoreData != null && data.StoreData.UserStores.Any(u => u.ApplicationUserId == userId)));
}
}
public async Task UpdatePaymentRequestStatus(string paymentRequestId, PaymentRequestData.PaymentRequestStatus status, CancellationToken cancellationToken = default)
{
using (var context = _ContextFactory.CreateContext())
{
var invoiceData = await context.FindAsync<PaymentRequestData>(paymentRequestId);
if (invoiceData == null)
return;
invoiceData.Status = status;
await context.SaveChangesAsync(cancellationToken);
}
}
public async Task<(int Total, PaymentRequestData[] Items)> FindPaymentRequests(PaymentRequestQuery query, CancellationToken cancellationToken = default)
{
using (var context = _ContextFactory.CreateContext())
{
var queryable = context.PaymentRequests.Include(data => data.StoreData).AsQueryable();
if (!string.IsNullOrEmpty(query.StoreId))
{
queryable = queryable.Where(data =>
data.StoreDataId.Equals(query.StoreId, StringComparison.InvariantCulture));
}
if (query.Status != null && query.Status.Any())
{
queryable = queryable.Where(data =>
query.Status.Contains(data.Status));
}
if (!string.IsNullOrEmpty(query.UserId))
{
queryable = queryable.Where(i =>
i.StoreData != null && i.StoreData.UserStores.Any(u => u.ApplicationUserId == query.UserId));
}
var total = await queryable.CountAsync(cancellationToken);
if (query.Skip.HasValue)
{
queryable = queryable.Skip(query.Skip.Value);
}
if (query.Count.HasValue)
{
queryable = queryable.Take(query.Count.Value);
}
return (total, await queryable.ToArrayAsync(cancellationToken));
}
}
public async Task<bool> RemovePaymentRequest(string id, string userId)
{
using (var context = _ContextFactory.CreateContext())
{
var canDelete = !EnumerableExtensions.Any((await GetInvoicesForPaymentRequest(id)));
if (!canDelete) return false;
var pr = await FindPaymentRequest(id, userId);
if (pr == null)
{
return false;
}
context.PaymentRequests.Remove(pr);
await context.SaveChangesAsync();
return true;
}
}
public async Task<InvoiceEntity[]> GetInvoicesForPaymentRequest(string paymentRequestId,
InvoiceQuery invoiceQuery = null)
{
if (invoiceQuery == null)
{
invoiceQuery = new InvoiceQuery();
}
invoiceQuery.OrderId = new[] {GetOrderIdForPaymentRequest(paymentRequestId)};
return await _InvoiceRepository.GetInvoices(invoiceQuery);
}
public static string GetOrderIdForPaymentRequest(string paymentRequestId)
{
return $"PAY_REQUEST_{paymentRequestId}";
}
public static string GetPaymentRequestIdFromOrderId(string invoiceOrderId)
{
if (string.IsNullOrEmpty(invoiceOrderId) ||
!invoiceOrderId.StartsWith("PAY_REQUEST_", StringComparison.InvariantCulture))
{
return null;
}
return invoiceOrderId.Replace("PAY_REQUEST_", "", StringComparison.InvariantCulture);
}
public static string GetInternalTag(string id)
{
return $"PAYREQ#{id}";
}
public static string[] GetPaymentIdsFromInternalTags(InvoiceEntity invoiceEntity)
{
return invoiceEntity.GetInternalTags("PAYREQ#");
}
}
public class PaymentRequestUpdated
{
public string PaymentRequestId { get; set; }
public PaymentRequestData Data { get; set; }
}
public class PaymentRequestQuery
{
public string StoreId { get; set; }
public PaymentRequestData.PaymentRequestStatus[] Status{ get; set; }
public string UserId { get; set; }
public int? Skip { get; set; }
public int? Count { get; set; }
}
public class PaymentRequestData
{
public string Id { get; set; }
public string StoreDataId { get; set; }
public StoreData StoreData { get; set; }
public PaymentRequestStatus Status { get; set; }
public byte[] Blob { get; set; }
public PaymentRequestBlob GetBlob()
{
var result = Blob == null
? new PaymentRequestBlob()
: JObject.Parse(ZipUtils.Unzip(Blob)).ToObject<PaymentRequestBlob>();
return result;
}
public bool SetBlob(PaymentRequestBlob blob)
{
var original = new Serializer(Network.Main).ToString(GetBlob());
var newBlob = new Serializer(Network.Main).ToString(blob);
if (original == newBlob)
return false;
Blob = ZipUtils.Zip(newBlob);
return true;
}
public class PaymentRequestBlob
{
public decimal Amount { get; set; }
public string Currency { get; set; }
public DateTime? ExpiryDate { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string Email { get; set; }
public string EmbeddedCSS { get; set; }
public string CustomCSSLink { get; set; }
public bool AllowCustomPaymentAmounts { get; set; }
}
public enum PaymentRequestStatus
{
Pending = 0,
Completed = 1,
Expired = 2
}
}
}

View File

@ -16,6 +16,7 @@ namespace BTCPayServer.Services.Rates
{
public ExchangeRates Latest;
public DateTimeOffset NextRefresh;
public TimeSpan Backoff = TimeSpan.FromSeconds(5.0);
public DateTimeOffset Expiration;
public Exception Exception;
public string ExchangeName;
@ -90,6 +91,7 @@ namespace BTCPayServer.Services.Rates
}
public bool DoNotAutoFetchIfExpired { get; set; }
readonly static TimeSpan MaxBackoff = TimeSpan.FromMinutes(5.0);
public async Task<LatestFetch> UpdateIfNecessary()
{
@ -142,12 +144,15 @@ namespace BTCPayServer.Services.Rates
{
fetch.Latest = previous.Latest;
fetch.Expiration = previous.Expiration;
fetch.Backoff = previous.Backoff * 2;
if (fetch.Backoff > MaxBackoff)
fetch.Backoff = MaxBackoff;
}
else
{
fetch.Expiration = DateTimeOffset.UtcNow;
}
fetch.NextRefresh = DateTimeOffset.UtcNow;
fetch.NextRefresh = DateTimeOffset.UtcNow + fetch.Backoff;
fetch.Exception = ex;
}
_Latest = fetch;

View File

@ -42,6 +42,15 @@ namespace BTCPayServer.Services.Rates
static Dictionary<string, IFormatProvider> _CurrencyProviders = new Dictionary<string, IFormatProvider>();
public string FormatCurrency(string price, string currency)
{
return FormatCurrency(decimal.Parse(price, CultureInfo.InvariantCulture), currency);
}
public string FormatCurrency(decimal price, string currency)
{
return price.ToString("C", GetCurrencyProvider(currency));
}
public NumberFormatInfo GetNumberFormatInfo(string currency, bool useFallback)
{
var data = GetCurrencyProvider(currency);
@ -121,16 +130,7 @@ namespace BTCPayServer.Services.Rates
var provider = GetNumberFormatInfo(currency, true);
var currencyData = GetCurrencyData(currency, true);
var divisibility = currencyData.Divisibility;
while (true)
{
var rounded = decimal.Round(value, divisibility, MidpointRounding.AwayFromZero);
if ((Math.Abs(rounded - value) / value) < 0.001m)
{
value = rounded;
break;
}
divisibility++;
}
value = value.RoundToSignificant(ref divisibility);
if (divisibility != provider.CurrencyDecimalDigits)
{
provider = (NumberFormatInfo)provider.Clone();

View File

@ -1,7 +1,7 @@
@using BTCPayServer.Services.Apps
@model ListAppsViewModel
@{
ViewData["Title"] = "Stores";
ViewData["Title"] = "Apps";
}
<section>

View File

@ -1,6 +1,4 @@
@using BTCPayServer.Crowdfund
@using BTCPayServer.Hubs
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
@model UpdateCrowdfundViewModel
@{
ViewData["Title"] = "Update Crowdfund";
@ -9,18 +7,18 @@
<div class="modal" id="product-modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Contribution Perks Management</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="js-product-save btn btn-primary">Save Changes</button>
</div>
<div class="modal-header">
<h5 class="modal-title">Contribution Perks Management</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
</div>
<div class="modal-footer">PRS
<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>
@ -44,20 +42,20 @@
<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>
<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>
<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>
<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>
@ -68,13 +66,21 @@
</div>
<div class="form-group">
<label asp-for="StartDate" class="control-label"></label>
<input asp-for="StartDate" class="form-control datetime " />
<div class="input-group ">
<input asp-for="StartDate" class="form-control datetime"/>
<div class="input-group-append only-for-js">
<button class="btn btn-secondary input-group-clear" type="button" title="Clear">
<span class=" fa fa-times"></span>
</button>
</div>
</div>
<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)
@ -84,10 +90,17 @@
</select>
</div>
</div>
<div class="form-group">
<label asp-for="EndDate" class="control-label"></label>
<input asp-for="EndDate" class="form-control datetime" />
<div class="input-group ">
<input asp-for="EndDate" class="form-control datetime" />
<div class="input-group-append only-for-js">
<button class="btn btn-secondary input-group-clear" type="button" title="Clear">
<span class=" fa fa-times"></span>
</button>
</div>
</div>
<span asp-validation-for="EndDate" class="text-danger"></span>
</div>
<div class="form-group">
@ -108,18 +121,17 @@
<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>
<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>
<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>
<div class="form-group">
<label asp-for="NotificationUrl" class="control-label"></label>
<input asp-for="NotificationUrl" class="form-control" />
@ -134,7 +146,7 @@
<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>
<div class="form-group">
<label asp-for="DisplayPerksRanking"></label>
<input asp-for="DisplayPerksRanking" type="checkbox" class="form-check"/>
@ -144,11 +156,6 @@
<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>
@ -175,13 +182,13 @@
<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>
<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="@Model.SearchTerm">Invoices</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>
@ -207,38 +214,38 @@
</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 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="col-sm-3">
<label>Price</label>*
<input type="number" step="any" class="js-product-price form-control mb-2" value="{price}" />
<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="col-sm-3">
<label>Custom price</label>
<select class="js-product-custom form-control">
{custom}
</select>
<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>
<div class="form-row">
<div class="col">
<label>Image</label>
<input type="text" class="js-product-image form-control mb-2" value="{image}" />
</div>
</div>
<div class="form-row">
<div class="col">
<label>Description</label>
<textarea rows="3" cols="40" class="js-product-description form-control mb-2">{description}</textarea>
</div>
</div>
</div>
</script>
}

View File

@ -57,6 +57,14 @@
<label asp-for="ShowCustomAmount"></label>
<input asp-for="ShowCustomAmount" type="checkbox" class="form-check" />
</div>
<div class="form-group">
<label asp-for="ShowDiscount"></label>
<input asp-for="ShowDiscount" type="checkbox" class="form-check" />
</div>
<div class="form-group">
<label asp-for="EnableTips"></label>
<input asp-for="EnableTips" type="checkbox" class="form-check" />
</div>
<div class="form-group">
<label asp-for="ButtonText" class="control-label"></label>*
<input asp-for="ButtonText" class="form-control" />
@ -97,34 +105,83 @@
<span asp-validation-for="Template" class="text-danger"></span>
</div>
<div class="form-group">
<h5>Host button externally</h5>
<p>You can host point of sale buttons in an external website with the following code.</p>
@if (Model.Example1 != null)
{
<span>For anything with a custom amount</span>
<pre><code class="html">@Model.Example1</code></pre>
}
@if (Model.Example2 != null)
{
<span>For a specific item of your template</span>
<pre><code class="html">@Model.Example2</code></pre>
}
<p>A <code>POST</code> callback will be sent to notification with the following form will be sent to <code>notificationUrl</code> once the enough is paid and once again once there is enough confirmations to the payment:</p>
<pre><code class="json">@Model.ExampleCallback</code></pre>
<p><strong>Never</strong> trust anything but <code>id</code>, <strong>ignore</strong> the other fields completely, an attacker can spoof those, they are present only for backward compatibility reason:</p>
<p>
<ul>
<li>Send a <code>GET</code> request to <code>https://btcpay.example.com/invoices/{invoiceId}</code> with <code>Content-Type: application/json</code></li>
<li>Verify that the <code>orderId</code> is from your backend, that the <code>price</code> is correct and that <code>status</code> is either <code>confirmed</code> or <code>complete</code></li>
<li>You can then ship your order</li>
</ul>
</p>
<input type="submit" class="btn btn-primary" value="Save Settings" />
</div>
<div class="form-group">
<input type="submit" class="btn btn-primary" value="Save Settings" />
<div class="accordion" id="accordian-dev-info">
<div class="card">
<div class="card-header" id="accordian-dev-info-embed-payment-button-header">
<h2 class="mb-0">
<button class="btn btn-link" type="button" data-toggle="collapse" data-target="#accordian-dev-info-embed-payment-button" aria-expanded="true" aria-controls="accordian-dev-info-embed-payment-button">
Embed Payment Button linking to POS item
</button>
</h2>
</div>
<div id="accordian-dev-info-embed-payment-button" class="collapse" aria-labelledby="accordian-dev-info-embed-payment-button-header" data-parent="#accordian-dev-info">
<div class="card-body">
<p>You can host point of sale buttons in an external website with the following code.</p>
@if (Model.Example1 != null)
{
<span>For anything with a custom amount</span>
<pre><code class="html">@Model.Example1</code></pre>
}
@if (Model.Example2 != null)
{
<span>For a specific item of your template</span>
<pre><code class="html">@Model.Example2</code></pre>
}
</div>
</div>
</div>
<div class="card">
<div class="card-header" id="accordian-dev-info-embed-pos-iframe-header">
<h2 class="mb-0">
<button class="btn btn-link collapsed" type="button" data-toggle="collapse" data-target="#accordian-dev-info-embed-pos-iframe" aria-expanded="false" aria-controls="accordian-dev-info-embed-pos-iframe">
Embed POS with Iframe
</button>
</h2>
</div>
<div id="accordian-dev-info-embed-pos-iframe" class="collapse" aria-labelledby="accordian-dev-info-embed-pos-iframe-header" data-parent="#accordian-dev-info">
<div class="card-body">
You can embed the POS using an iframe
@{
var iframe = $"<iframe src='{(Url.Action("ViewPointOfSale", "AppsPublic", new {appId = Model.Id}, Context.Request.Scheme))}' style='max-width: 100%; border: 0;'></iframe>";
}
<pre><code class="html">@iframe</code></pre>
</div>
</div>
</div>
<div class="card">
<div class="card-header" id="accordian-dev-info-notification-header">
<h2 class="mb-0">
<button class="btn btn-link collapsed" type="button" data-toggle="collapse" data-target="#accordian-dev-info-notification" aria-expanded="false" aria-controls="accordian-dev-info-notification">
Notification Url Callbacks
</button>
</h2>
</div>
<div id="accordian-dev-info-notification" class="collapse" aria-labelledby="accordian-dev-info-notification-header" data-parent="#accordian-dev-info">
<div class="card-body">
<p>A <code>POST</code> callback will be sent to notification with the following form will be sent to <code>notificationUrl</code> once the enough is paid and once again once there is enough confirmations to the payment:</p>
<pre><code class="json">@Model.ExampleCallback</code></pre>
<p><strong>Never</strong> trust anything but <code>id</code>, <strong>ignore</strong> the other fields completely, an attacker can spoof those, they are present only for backward compatibility reason:</p>
<p>
<ul>
<li>Send a <code>GET</code> request to <code>https://btcpay.example.com/invoices/{invoiceId}</code> with <code>Content-Type: application/json</code></li>
<li>Verify that the <code>orderId</code> is from your backend, that the <code>price</code> is correct and that <code>status</code> is either <code>confirmed</code> or <code>complete</code></li>
<li>You can then ship your order</li>
</ul>
</p> </div>
</div>
</div>
</div>
</div>
</form>
<a asp-action="ListApps">Back to the app list</a>
</div>
</div>
</div>

View File

@ -37,7 +37,7 @@
{
<span class="mt-3">
<span class="h5">@Model.TargetAmount @Model.TargetCurrency</span>
@if (Model.ResetEveryAmount > 0 && Model.ResetEvery != nameof(CrowdfundResetEvery.Never))
@if (Model.ResetEveryAmount > 0 && !Model.NeverReset)
{
<span> Dynamic</span>
}

View File

@ -13,7 +13,7 @@
<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"/>
<link href="@Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet"/>
@if (Model.CustomCSSLink != null)
{
<link href="@Model.CustomCSSLink" rel="stylesheet"/>
@ -32,7 +32,7 @@
{
<style>
@Html.Raw(Model.EmbeddedCSS);
</style>
</style>
}
</head>

View File

@ -20,7 +20,7 @@
<link rel="manifest" href="~/manifest.json">
<link href="@this.Context.Request.GetAbsoluteUri(themeManager.BootstrapUri)" rel="stylesheet" />
<link href="@this.Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet" />
@if (Model.CustomCSSLink != null)
{
<link href="@Model.CustomCSSLink" rel="stylesheet" />
@ -93,6 +93,8 @@
</div>
</td>
</tr>
@if (Model.ShowDiscount)
{
<tr>
<td colspan="5" class="border-top-0">
<div class="input-group">
@ -106,9 +108,12 @@
</div>
</td>
</tr>
}
</script>
<script id="template-cart-tip" type="text/template">
@if (Model.EnableTips)
{
<tr class="h5">
<td colspan="5" class="border-top-0 pt-4">@Model.CustomTipText</td>
</tr>
@ -135,6 +140,7 @@
</div>
</td>
</tr>
}
</script>
<script id="template-cart-total" type="text/template">
@ -170,18 +176,24 @@
<span class="js-cart-summary-products text-nowrap"></span>
</td>
</tr>
@if (Model.ShowDiscount)
{
<tr class="h6">
<td class="border-0 pb-y">Discount</td>
<td align="right" class="border-0 pb-y">
<span class="js-cart-summary-discount text-nowrap"></span>
</td>
</tr>
}
@if (Model.EnableTips)
{
<tr class="h6">
<td class="border-top-0 pt-0">Tip</td>
<td align="right" class="border-top-0 pt-0">
<span class="js-cart-summary-tip text-nowrap"></span>
</td>
</tr>
}
<tr class="h3 table-light">
<td>Total</td>
<td align="right">
@ -194,6 +206,7 @@
<div class="modal-footer bg-light">
<form method="post" asp-antiforgery="false" data-buy>
<input id="js-cart-amount" class="form-control" type="hidden" name="amount">
<input id="js-cart-posdata" class="form-control" type="hidden" name="posdata">
<button id="js-cart-pay" class="btn btn-primary btn-lg" type="submit"><b>@Model.CustomButtonText</b></button>
</form>
</div>

View File

@ -24,7 +24,7 @@
<div class="col-lg-12 text-center">
<h2 class="section-heading">The Bitpay Translator</h2>
<hr class="primary">
<p>Bitpay is using deprecated standard in their invoices which multiple wallet do not support, use this transform their invoices to regular address/amount.</p>
<p>Bitpay is using a deprecated standard in their invoices that most wallets do not support. Use this tool to transform their invoices to a regular address/amount.</p>
</div>
</div>
<div class="row">

View File

@ -10,7 +10,7 @@
<h1>Welcome to BTCPay Server</h1>
<hr />
<p>BTCPay Server is a free and open source server for merchants wanting to accept Bitcoin for their business.</p>
<a style="background-color: #fff;color: #222;display:inline-block;text-align: center;white-space: nowrap;vertical-align: middle;user-select: none;line-height: 1.25;font-size: 1rem;text-decoration:none;font-weight: 700; text-transform: uppercase;border: none;border-radius: 300px;padding: 15px 30px;" href="https://docs.btcpayserver.org">Getting started</a>
<a style="background-color: #fff;color: #222;display:inline-block;text-align: center;white-space: nowrap;vertical-align: middle;user-select: none;line-height: 1.25;font-size: 1rem;text-decoration:none;font-weight: 700; text-transform: uppercase;border: none;border-radius: 300px;padding: 15px 30px;" href="https://btcpayserver.org" target="_blank">Official website</a>
</div>
</div>
</header>
@ -128,13 +128,19 @@
</div>
</div>
<div class="row">
<div class="col-lg-4 ml-auto text-center">
<a href="http://slack.forkbitpay.ninja/">
<div class="col-lg-3 ml-auto text-center">
<a href="https://chat.btcpayserver.org/">
<img src="~/img/mattermost.png" height="100" />
</a>
<p><a href="https://chat.btcpayserver.org/">On Mattermost</a></p>
</div>
<div class="col-lg-3 ml-auto text-center">
<a href="https://slack.btcpayserver.org/">
<img src="~/img/slack.png" height="100" />
</a>
<p><a href="http://slack.forkbitpay.ninja/">On Slack</a></p>
</div>
<div class="col-lg-4 mr-auto text-center">
<div class="col-lg-3 mr-auto text-center">
<a href="https://twitter.com/BtcpayServer">
<img src="~/img/twitter.png" height="100" />
</a>
@ -142,7 +148,7 @@
<a href="https://twitter.com/BtcpayServer">On Twitter</a>
</p>
</div>
<div class="col-lg-4 mr-auto text-center">
<div class="col-lg-3 mr-auto text-center">
<a href="https://github.com/btcpayserver/btcpayserver">
<img src="~/img/github.png" height="100" />
</a>

View File

@ -45,11 +45,13 @@
<div class="single-item-order__right">
@if (Model.AvailableCryptos.Count > 1)
{
<div class="payment__currencies cursorPointer" onclick="openPaymentMethodDialog()">
<img v-bind:src="srvModel.cryptoImage" />
<span class="clickable_underline">{{srvModel.paymentMethodName}} ({{srvModel.cryptoCode}})</span>
<span v-show="srvModel.isLightning">&#9889;</span>
<span class="clickable_indicator fa fa-angle-right"></span>
<div class="paywithRowRight cursorPointer" onclick="openPaymentMethodDialog()">
<span class="payment__currencies ">
<img v-bind:src="srvModel.cryptoImage" />
<span>{{srvModel.paymentMethodName}} ({{srvModel.cryptoCode}})</span>
<span v-show="srvModel.isLightning">&#9889;</span>
<span class="clickable_indicator fa fa-angle-right"></span>
</span>
</div>
<div id="vexPopupDialog">
<ul class="vexmenu">
@ -390,7 +392,7 @@
<span v-html="$t('Return to StoreName', srvModel)"></span>
</a>
<button class="action-button close-action" v-show="isModal">
<span v-html="$t('home.header.title')">{{$t("Return to StoreName", srvModel)}}</span>
<span v-html="$t('Close')">{{$t("Return to StoreName", srvModel)}}</span>
</button>
</div>
</div>

View File

@ -155,7 +155,11 @@
</tr>
<tr>
<th>Price</th>
<td>@Model.ProductInformation.Price @Model.ProductInformation.Currency</td>
<td>@Model.Fiat</td>
</tr>
<tr>
<th>Tax included</th>
<td>@Model.TaxIncluded</td>
</tr>
</table>
}
@ -178,30 +182,17 @@
</tr>
<tr>
<th>Price</th>
<td>@Model.ProductInformation.Price @Model.ProductInformation.Currency</td>
<td>@Model.Fiat</td>
</tr>
<tr>
<th>Tax included</th>
<td>@Model.TaxIncluded</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" />
</div>
</div>
}

View File

@ -1,7 +1,6 @@
@model InvoicesModel
@{
ViewData["Title"] = "Invoices";
var rootUrl = Context.Request.GetAbsoluteRoot();
}
@section HeadScripts {

View File

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

View File

@ -0,0 +1,122 @@
@using BTCPayServer.Services.PaymentRequests
@model BTCPayServer.Models.PaymentRequestViewModels.UpdatePaymentRequestViewModel
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
<section>
<div class="container">
<div class="row">
<div class="col-lg-12 text-center">
<h2 class="section-heading">@(string.IsNullOrEmpty(Model.Id) ? "Create" : "Edit") Payment Request</h2>
<hr class="primary">
</div>
</div>
<div class="row">
<div class="col-lg-12 text-center">
<partial name="_StatusMessage" for="StatusMessage"/>
</div>
</div>
<div class="row">
<div class="col-lg-12">
<form method="post">
<input type="hidden" asp-for="Id"/>
<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="Amount" class="control-label"></label>*
<input type="number" step="any" asp-for="Amount" class="form-control"/>
<span asp-validation-for="Amount" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Currency" class="control-label"></label>*
<input placeholder="BTC" asp-for="Currency" class="form-control"/>
<span asp-validation-for="Currency" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="AllowCustomPaymentAmounts"></label>
<input asp-for="AllowCustomPaymentAmounts" type="checkbox" class="form-check"/>
<span asp-validation-for="AllowCustomPaymentAmounts" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="StoreId" class="control-label"></label>
@if (string.IsNullOrEmpty(Model.Id))
{
<select asp-for="StoreId" asp-items="Model.Stores" class="form-control"></select>
}
else
{
<input type="hidden" asp-for="StoreId" value="@Model.StoreId"/>
<input type="text" class="form-control" value="@Model.Stores.Single(item => item.Value == Model.StoreId).Text" readonly/>
}
<span asp-validation-for="StoreId" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Email" class="control-label"></label>
<input type="email" asp-for="Email" class="form-control"></input>
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ExpiryDate" class="control-label"></label>
<div class="input-group ">
<input asp-for="ExpiryDate" class="form-control datetime" min="today"/>
<div class="input-group-append only-for-js">
<button class="btn btn-secondary input-group-clear" type="button" title="Clear">
<span class=" fa fa-times"></span>
</button>
</div>
</div>
<span asp-validation-for="ExpiryDate" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Description" class="control-label"></label>
<textarea asp-for="Description" class="form-control richtext"></textarea>
<span asp-validation-for="Description" 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="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">
<button type="submit" class="btn btn-primary">Save</button>
@if (!string.IsNullOrEmpty(Model.Id))
{
<a class="btn btn-secondary" target="_blank" asp-action="ViewPaymentRequest" id="@Model.Id">View</a>
<a class="btn btn-secondary"
target="_blank"
asp-action="ListInvoices"
asp-controller="Invoice"
asp-route-searchterm="@($"orderid:{PaymentRequestRepository.GetOrderIdForPaymentRequest(Model.Id)}")">Invoices</a>
}
<a class="btn btn-secondary" target="_blank" asp-action="GetPaymentRequests">Back to list</a>
</div>
</form>
</div>
</div>
</div>
</section>
@section Scripts {
<bundle name="wwwroot/bundles/payment-request-admin-bundle.min.js"></bundle>
<bundle name="wwwroot/bundles/payment-request-admin-bundle.min.css"></bundle>
}

View File

@ -0,0 +1,88 @@
@using BTCPayServer.Services.PaymentRequests
@model BTCPayServer.Models.PaymentRequestViewModels.ListPaymentRequestsViewModel
@{
Layout = "_Layout";
}
<section>
<div class="container">
<div class="row">
<div class="col-lg-12 text-center">
<partial name="_StatusMessage" for="StatusMessage"/>
</div>
</div>
<div class="row">
<div class="col-lg-12 text-center">
<h2 class="section-heading">Payment Requests</h2>
</div>
</div>
<div class="row no-gutter" style="margin-bottom: 5px;">
<div class="col-lg-6">
<a asp-action="EditPaymentRequest" class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Create a new payment request</a>
</div>
</div>
<div class="row">
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Title</th>
<th>Expiry</th>
<th class="text-right">Price</th>
<th class="text-right">Status</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Items)
{
<tr>
<td>@item.Title</td>
<td>@(item.ExpiryDate?.ToString("g") ?? "No Expiry")</td>
<td class="text-right">@item.Amount @item.Currency</td>
<td class="text-right">@item.Status</td>
<td class="text-right">
<a asp-action="EditPaymentRequest" asp-route-id="@item.Id">Edit</a>
<span> - </span>
<a asp-action="ViewPaymentRequest" asp-route-id="@item.Id">View</a>
<span> - </span>
<a target="_blank" asp-action="ListInvoices" asp-controller="Invoice" asp-route-searchterm="@($"orderid:{PaymentRequestRepository.GetOrderIdForPaymentRequest(item.Id)}")">Invoices</a>
<span> - </span>
<a target="_blank" asp-action="PayPaymentRequest" asp-route-id="@item.Id">Pay</a>
<span> - </span>
<a asp-action="RemovePaymentRequestPrompt" asp-route-id="@item.Id">Remove</a>
</td>
</tr>
}
</tbody>
</table>
<nav aria-label="...">
<ul class="pagination">
<li class="page-item @(Model.Skip == 0 ? "disabled" : null)">
<a class="page-link" tabindex="-1" href="@Url.Action("GetPaymentRequests", new
{
skip = Math.Max(0, Model.Skip - Model.Count),
count = Model.Count,
})">Previous</a>
</li>
<li class="page-item disabled">
<span class="page-link">@(Model.Skip + 1) to @(Model.Skip + Model.Count) of @Model.Total</span>
</li>
<li class="page-item @(Model.Total > (Model.Skip + Model.Count) ? null : "disabled")">
<a class="page-link" href="@Url.Action("GetPaymentRequests", new
{
skip = Model.Skip + Model.Count,
count = Model.Count,
})">Next</a>
</li>
</ul>
</nav>
</div>
</div>
</section>

View File

@ -0,0 +1,163 @@
@model BTCPayServer.Models.PaymentRequestViewModels.ViewPaymentRequestViewModel
<div class="container">
<div class="row w-100 p-0 m-0" style="height: 100vh">
<div class="mx-auto my-auto w-100">
<div class="card">
<h1 class="card-header">
@Model.Title
<span class="text-muted float-right text-center">@Model.Status</span>
</h1>
<div class="card-body px-0 pt-0">
<div class="row mb-4">
<div class="col-sm-12 col-md-12 col-lg-6 ">
<ul class="w-100 list-group list-group-flush">
<li class="list-group-item">
<div class="d-flex justify-content-between">
<span class="h2 text-muted">Request amount:</span>
<span class="h2">@Model.AmountFormatted</span>
</div>
</li>
<li class="list-group-item">
<div class="d-flex justify-content-between">
<span class="h2 text-muted">Paid so far:</span>
<span class="h2">@Model.AmountCollectedFormatted</span>
</div>
</li>
<li class="list-group-item">
<div class="d-flex justify-content-between">
<span class="h2 text-muted">Amount due:</span>
<span class="h2">@Model.AmountDueFormatted</span>
</div>
</li>
</ul>
<div class="w-100 p-2">@Html.Raw(Model.Description)</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="table-responsive">
<table class="table border-top-0 ">
<thead>
<tr>
<th class=" border-top-0" scope="col">Invoice #</th>
<th class=" border-top-0">Price</th>
<th class=" border-top-0">Expiry</th>
<th class=" border-top-0">Status</th>
</tr>
</thead>
<tbody>
@if (Model.Invoices == null && !Model.Invoices.Any())
{
<tr>
<td colspan="4" class="text-center">No payments made yet</td>
</tr>
}
else
{
foreach (var invoice in Model.Invoices)
{
<tr class="bg-light">
<td scope="row">@invoice.Id</td>
<td>@invoice.Amount @invoice.Currency</td>
<td>@invoice.ExpiryDate.ToString("g")</td>
<td>@invoice.Status</td>
</tr>
if (invoice.Payments != null && invoice.Payments.Any())
{
<tr class="bg-light">
<td colspan="4" class=" px-2 py-1 border-top-0">
<div class="table-responsive">
<table class="table table-bordered">
<tr>
<th class="p-1" style="max-width: 300px">Tx Id</th>
<th class="p-1">Payment Method</th>
<th class="p-1">Amount</th>
<th class="p-1">Link</th>
</tr>
@foreach (var payment in invoice.Payments)
{
<tr class="d-flex">
<td class="p-1 m-0 d-print-none d-block" style="max-width: 300px">
<div style="width: 100%; overflow-x: auto; overflow-wrap: initial;">@payment.Id</div>
</td>
<td class="p-1 m-0 d-none d-print-table-cell" style="max-width: 150px;">
@payment.Id
</td>
<td class="p-1">@payment.PaymentMethod</td>
<td class="p-1">@payment.Amount</td>
<td class="p-1 d-print-none">
@if (!string.IsNullOrEmpty(payment.Link))
{
<a :href="@payment.Link" target="_blank">Link</a>
}
</td>
<td class="p-1 d-none d-print-table-cell" style="max-width: 150px;">
@payment.Link
</td>
</tr>
}
</table>
</div>
</td>
</tr>
}
}
}
@if (Model.IsPending)
{
<tr>
<td colspan="4" class="text-center">
@if (Model.AllowCustomPaymentAmounts && !Model.AnyPendingInvoice)
{
<form method="get" asp-action="PayPaymentRequest">
<div class="input-group m-auto" style="max-width: 250px">
<input
class="form-control"
type="number"
name="amount"
value="@Model.AmountDue"
max="@Model.AmountDue"
step="any"
placeholder="Amount"
required>
<div class="input-group-append">
<span class='input-group-text'>@Model.Currency.ToUpper()</span>
<button
class="btn btn-primary"
type="submit">
Pay now
</button>
</div>
</div>
</form>
}
else
{
<a class="btn btn-primary btn-lg d-print-none" asp-action="PayPaymentRequest">
Pay now
</a>
}
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="card-footer text-muted d-flex justify-content-between">
<div >Updated @Model.LastUpdated.ToString("g")</div>
<div >
<span class="text-muted">Powered by </span><a href="https://btcpayserver.org" target="_blank">BTCPay Server</a>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,213 @@
@model BTCPayServer.Models.PaymentRequestViewModels.ViewPaymentRequestViewModel
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
@{
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.GetRelativePathOrAbsolute(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/payment-request-bundle-1.min.js"></bundle>
<bundle name="wwwroot/bundles/payment-request-bundle-2.min.js"></bundle>
}
<bundle name="wwwroot/bundles/payment-request-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("MinimalPaymentRequest", Model)
}
else
{
<noscript>
@await Html.PartialAsync("MinimalPaymentRequest", Model)
</noscript>
<div class="container" id="app" v-cloak>
<div class="row w-100 p-0 m-0" style="height: 100vh">
<div class="mx-auto my-auto w-100">
<div class="card">
<h1 class="card-header">
{{srvModel.title}}
<span class="text-muted float-right text-center">
<template v-if="settled">Settled</template>
<template v-else>
<template v-if="ended">Request Expired</template>
<template v-else-if="endDiff">Expires in {{endDiff}}</template>
</template>
</span>
</h1>
<div class="card-body px-0 pt-0">
<div class="row mb-4">
<div class="col-sm-12 col-md-12 col-lg-6 ">
<ul class="w-100 list-group list-group-flush">
<li class="list-group-item">
<div class="d-flex justify-content-between">
<span class="h2 text-muted">Request amount:</span>
<span class="h2">{{srvModel.amountFormatted}}</span>
</div>
</li>
<li class="list-group-item">
<div class="d-flex justify-content-between">
<span class="h2 text-muted">Paid so far:</span>
<span class="h2">{{srvModel.amountCollectedFormatted}}</span>
</div>
</li>
<li class="list-group-item">
<div class="d-flex justify-content-between">
<span class="h2 text-muted">Amount due:</span>
<span class="h2">{{srvModel.amountDueFormatted}}</span>
</div>
</li>
</ul>
<div v-html="srvModel.description" class="w-100 p-2"></div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="table-responsive">
<table class="table border-top-0 ">
<thead>
<tr>
<th class=" border-top-0" scope="col">Invoice #</th>
<th class=" border-top-0">Price</th>
<th class=" border-top-0">Expiry</th>
<th class=" border-top-0">Status</th>
</tr>
</thead>
<tbody>
<tr v-if="!srvModel.invoices || srvModel.invoices.length == 0">
<td colspan="4" class="text-center">No payments made yet</td>
</tr>
<template v-else v-for="invoice of srvModel.invoices" :key="invoice.id">
<tr class="bg-light">
<td scope="row">{{invoice.id}}</td>
<td>{{invoice.amount}} {{invoice.currency}}</td>
<td>{{moment(invoice.expiryDate).format('L HH:mm')}}</td>
<td>{{invoice.status}}</td>
</tr>
<tr class="bg-light" v-if="invoice.payments && invoice.payments.length > 0">
<td colspan="4" class=" px-2 py-1 border-top-0">
<div class="table-responsive">
<table class="table table-bordered">
<tr>
<th class="p-1" style="max-width: 300px">Tx Id</th>
<th class="p-1">Payment Method</th>
<th class="p-1">Amount</th>
<th class="p-1">Link</th>
</tr>
<tr v-for="payment of invoice.payments">
<td class="p-1 m-0 d-print-none d-block" style="max-width: 300px">
<div style="width: 100%; overflow-x: auto; overflow-wrap: initial;">{{payment.id}}</div>
</td>
<td class="p-1 m-0 d-none d-print-table-cell" style="max-width: 150px;">
{{payment.id}}
</td>
<td class="p-1">{{formatPaymentMethod(payment.paymentMethod)}}</td>
<td class="p-1">{{payment.amount.noExponents()}}</td>
<td class="p-1 d-print-none">
<a v-if="payment.link" :href="payment.link" target="_blank">Link</a>
</td>
<td class="p-1 d-none d-print-table-cell" style="max-width: 150px;">
{{payment.link}}
</td>
</tr>
</table>
</div>
</td>
</tr>
</template>
<tr v-if="!ended && (srvModel.amountDue) > 0" class="d-print-none">
<td colspan="4" class="text-center">
<template v-if="srvModel.allowCustomPaymentAmounts && !srvModel.anyPendingInvoice">
<form v-on:submit="submitCustomAmountForm">
<div class="input-group m-auto" style="max-width: 250px">
<input
:readonly="!srvModel.allowCustomPaymentAmounts"
class="form-control"
type="number"
v-model="customAmount"
:max="srvModel.amountDue"
step="any"
placeholder="Amount"
required>
<div class="input-group-append">
<span class='input-group-text'>{{currency}}</span>
<button
class="btn btn-primary"
v-bind:class="{ 'btn-disabled': loading}"
:disabled="loading"
type="submit">
<div v-if="loading" class="spinner-grow spinner-grow-sm" role="status">
<span class="sr-only">Loading...</span>
</div>
Pay now
</button>
</div>
</div>
</form>
</template>
<button v-else class="btn btn-primary btn-lg " v-on:click="pay(null)"
:disabled="loading">
<div v-if="loading" class="spinner-grow spinner-grow-sm" role="status">
<span class="sr-only">Loading...</span>
</div>
Pay now
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="card-footer text-muted d-flex justify-content-between">
<div >
<span v-on:click="print" class="btn-link d-print-none" style="cursor: pointer"> <span class="fa fa-print"></span> Print</span>
<span>Updated {{lastUpdated}}</span>
</div>
<div >
<span class="text-muted">Powered by </span><a href="https://btcpayserver.org" target="_blank">BTCPay Server</a>
</div>
</div>
</div>
</div>
</div>
</div>
}
</body>
</html>

View File

@ -1,4 +1,4 @@
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
@model BTCPayServer.Controllers.ShowLightningNodeInfoViewModel
@ -18,7 +18,7 @@
<link rel="manifest" href="~/manifest.json">
<link href="@this.Context.Request.GetAbsoluteUri(themeManager.BootstrapUri)" rel="stylesheet"/>
<link href="@this.Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet"/>
<link href="~/vendor/font-awesome/css/font-awesome.min.css" rel="stylesheet"/>
<bundle name="wwwroot/bundles/lightning-node-info-bundle.min.js"/>
@ -73,36 +73,39 @@
position: relative;
text-align: center;
}
.copy {
cursor: copy;
}
.copy { cursor: copy; }
</style>
</head>
<body >
<noscript>
<div class="container">
<div class="col-md-8 col-sm-12 col-lg-6 mx-auto my-auto ">
<div class="card border-0">
<div class="row"></div>
<h1 class="card-title text-center">
@Model.CryptoCode Lightning Node - @(Model.Available? "Online" : "Unavailable")
<small class="@(Model.Available? "text-success" : "text-danger")">
<span class="fa fa-circle "></span>
</small>
</h1>
@if (Model.Available)
{
<div class="card-body m-sm-0 p-sm-0" >
<div class="input-group">
<input type="text" class="form-control " readonly="readonly" asp-for="NodeInfo" id="peer-info"/>
<div class="input-group-append">
<span class="input-group-text fa fa-copy"> </span>
<div class="row " style="height: 100vh">
<div class="col-md-8 col-sm-12 col-lg-6 mx-auto my-auto ">
<div class="col-md-8 col-sm-12 col-lg-6 mx-auto my-auto ">
<div class="card border-0">
<div class="row"></div>
<h1 class="card-title text-center">
@Model.CryptoCode Lightning Node - @(Model.Available ? "Online" : "Unavailable")
<small class="@(Model.Available ? "text-success" : "text-danger")">
<span class="fa fa-circle "></span>
</small>
</h1>
@if (Model.Available)
{
<div class="card-body m-sm-0 p-sm-0">
<div class="input-group">
<input type="text" class="form-control " readonly="readonly" asp-for="NodeInfo" id="peer-info"/>
<div class="input-group-append">
<span class="input-group-text fa fa-copy"> </span>
</div>
</div>
</div>
</div>
}
</div>
}
</div>
</div>
</div>
</div>

View File

@ -1,10 +1,10 @@
@model SparkServicesViewModel
@model LightningWalletServices
@{
ViewData.SetActivePageAndTitle(ServerNavPages.Services);
}
<h4>Spark service</h4>
<h4>@Model.WalletName</h4>
<partial name="_StatusMessage" for="@TempData["StatusMessage"]" />
@if (Model.ShowQR)
@ -30,7 +30,7 @@
<div class="form-group">
<h5>Browser connection</h5>
<p>
<span>You can go to spark from your browser by <a href="@Model.SparkLink" target="_blank">clicking here</a><br /></span>
<span>You can go to @Model.WalletName from your browser by <a href="@Model.ServiceLink" target="_blank">clicking here</a><br /></span>
</p>
</div>
@ -54,7 +54,7 @@
{
<div class="form-group">
<div id="qrCode"></div>
<div id="qrCodeData" data-url="@Html.Raw(Model.SparkLink)"></div>
<div id="qrCodeData" data-url="@Html.Raw(Model.ServiceLink)"></div>
</div>
}
</div>
@ -70,7 +70,7 @@
<script type="text/javascript">
new QRCode(document.getElementById("qrCode"),
{
text: "@Html.Raw(Model.SparkLink)",
text: "@Html.Raw(Model.ServiceLink)",
width: 150,
height: 150
});

View File

@ -12,6 +12,8 @@
<p>@Model.Description</p>
</div>
</div>
@if (!String.IsNullOrEmpty(Model.Action))
{
<div class="row">
<div class="col-lg-12 text-center">
<form method="post">
@ -19,5 +21,6 @@
</form>
</div>
</div>
}
</div>
</section>

View File

@ -42,7 +42,7 @@
}
<!-- Navigation -->
<nav class='navbar navbar-expand-lg navbar-light fixed-top @additionalStyle' id="mainNav">
<nav class='navbar navbar-expand-lg navbar-dark fixed-top @additionalStyle' id="mainNav">
<div class="container">
<a class="navbar-brand js-scroll-trigger" href="~/">
<img src="~/img/logo.png" height="45">
@ -66,6 +66,7 @@
<li class="nav-item"><a asp-area="" asp-controller="Apps" asp-action="ListApps" class="nav-link js-scroll-trigger">Apps</a></li>
<li class="nav-item"><a asp-area="" asp-controller="Wallets" asp-action="ListWallets" class="nav-link js-scroll-trigger">Wallets</a></li>
<li class="nav-item"><a asp-area="" asp-controller="Invoice" asp-action="ListInvoices" class="nav-link js-scroll-trigger">Invoices</a></li>
<li class="nav-item"><a asp-area="" asp-controller="PaymentRequest" asp-action="GetPaymentRequests" class="nav-link js-scroll-trigger">Payment Requests</a></li>
<li class="nav-item">
<a asp-area="" asp-controller="Manage" asp-action="Index" title="Manage" class="nav-link js-scroll-trigger">My settings</a>
</li>
@ -84,12 +85,7 @@
</div>
<div id="badUrl" class="alert alert-danger alert-dismissible" style="display:none; position:absolute; top:75px;" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<span>BTCPay is expecting you to access this website from <b>@(env.ExpectedProtocol)://@(env.ExpectedHost)/</b>. If you want to change this expectation:</span>
<ul>
<li>Either starts BTCPay with <b>--externalurl @(env.ExpectedProtocol)://@(env.ExpectedHost)/</b></li>
<li>Or, if using docker-compose deployment, setting environment variable <b>BTCPAY_PROTOCOL=@(env.ExpectedProtocol)</b> and <b>BTCPAY_HOST=@(env.ExpectedDomain)</b></li>
</ul>
<span>BTCPay is expecting you to access this website from <b>@(env.ExpectedProtocol)://@(env.ExpectedHost)/</b>. If you use a reverse proxy, please set the <b>X-Forwarded-Proto</b> header to <b>@(env.ExpectedProtocol)</b>:</span>
</div>
</div>
</nav>

View File

@ -1,10 +1,17 @@
@model string
@if(!String.IsNullOrEmpty(Model))
@if(!string.IsNullOrEmpty(Model))
{
var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success";
<div class="alert alert-@statusMessageClass alert-dismissible" role="alert">
var parsedModel = new StatusMessageModel(Model);
<div class="alert alert-@parsedModel.SeverityCSS alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
@Model
@if (!string.IsNullOrEmpty(parsedModel.Message))
{
@parsedModel.Message
}
@if (!string.IsNullOrEmpty(parsedModel.Html))
{
@Html.Raw(parsedModel.Html)
}
</div>
}

View File

@ -38,12 +38,17 @@
</p>
<div id="ledger-info" class="form-text text-muted" style="display: none;">
<span>A ledger wallet is detected, which account do you want to use? No need to paste manually xpub if your ledger device was detected. Just select derivation scheme from the list bellow and xpub will automatically populate.</span>
<ul>
@for (int i = 0; i < 4; i++)
{
<li><a class="ledger-info-recommended" data-ledgerkeypath="@Model.RootKeyPath.Derive(i, true)" href="#">Account @i (<span>@Model.RootKeyPath.Derive(i, true)</span>)</a></li>
}
</ul>
<div class="dropdown">
<button class="btn btn-primary dropdown-toggle" type="button" id="ledgerAccountsDropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Select ledger wallet account
</button>
<div class="dropdown-menu overflow-auto" style="max-height: 200px;" aria-labelledby="ledgerAccountsDropdownMenuButton">
@for (var i = 0; i < 20; i++)
{
<a class="dropdown-item ledger-info-recommended" data-ledgerkeypath="@Model.RootKeyPath.Derive(i, true)" href="#">Account @i (<span>@Model.RootKeyPath.Derive(i, true)</span>)</a>
}
</div>
</div>
</div>
</div>
<div class="form-group">
@ -58,27 +63,27 @@
<tbody>
<tr>
<td>P2WPKH</td>
<td>xpub</td>
<td>xpub...</td>
</tr>
<tr>
<td>P2SH-P2WPKH</td>
<td>xpub-[p2sh]</td>
<td>xpub...-[p2sh]</td>
</tr>
<tr>
<td>P2PKH</td>
<td>xpub-[legacy]</td>
<td>xpub...-[legacy]</td>
</tr>
<tr>
<td>Multi-sig P2WSH</td>
<td>2-of-xpub1-xpub2</td>
<td>2-of-xpub1...-xpub2...</td>
</tr>
<tr>
<td>Multi-sig P2SH-P2WSH</td>
<td>2-of-xpub1-xpub2-[p2sh]</td>
<td>2-of-xpub1...-xpub2...-[p2sh]</td>
</tr>
<tr>
<td>Multi-sig P2SH</td>
<td>2-of-xpub1-xpub2-[legacy]</td>
<td>2-of-xpub1...-xpub2...-[legacy]</td>
</tr>
</tbody>
</table>

View File

@ -31,8 +31,8 @@
<span asp-validation-for="HtmlTitle" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="DefaultCryptoCurrency"></label>
<select asp-for="DefaultCryptoCurrency" asp-items="Model.CryptoCurrencies" class="form-control"></select>
<label asp-for="DefaultPaymentMethod"></label>
<select asp-for="DefaultPaymentMethod" asp-items="Model.CryptoCurrencies" class="form-control"></select>
</div>
<div class="form-group">
<label asp-for="DefaultLang"></label>

View File

@ -11,6 +11,9 @@
<div class="row">
<div class="col-md-10">
<form method="post">
<div class="alert alert-warning" role="alert">
If you are enabling Changelly support, we advise that you configure the invoice expiration to a minimum of 30 minutes as it may take longer than the default 15 minutes to convert the funds.
</div>
<p>
You can obtain API keys at
<a href="https://changelly.com/?ref_id=804298eb5753" target="_blank">

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