Compare commits

...

56 Commits

Author SHA1 Message Date
00c11c7ee9 bump 2019-03-18 18:58:15 +09:00
82126b85d2 Fix tests 2019-03-18 18:52:19 +09:00
4f428c8ed1 Fix other tor services display 2019-03-18 17:13:02 +09:00
9868af4db8 Show onion link in the navbar 2019-03-18 17:07:39 +09:00
0a5d7c5efa Add .local as a localnetwork link 2019-03-18 16:48:04 +09:00
c2754b324d Show other tor services 2019-03-18 16:45:46 +09:00
014d08f38a Add socks support on BTCPay Server to query onion endpoints 2019-03-18 00:03:02 +09:00
7998ea142b Fix tor services not appearing 2019-03-17 21:47:08 +09:00
a4051dac72 Make sure BTCPay show TOR lightning node info if the site is accessed through TOR 2019-03-17 21:28:47 +09:00
e3a8892d24 Check tor services in the background 2019-03-17 21:07:24 +09:00
ea02d77e69 Parse torrc file to know virtual port of hidden services 2019-03-17 20:49:26 +09:00
4f582a6712 Show onion hosts instead of url (can't know about ports) 2019-03-17 13:42:54 +09:00
4769b1d452 Show tor services inside hidden directories in Server Settings/Services 2019-03-17 13:01:47 +09:00
17b18d820f bump 2019-03-17 12:28:38 +09:00
26f34e75c2 Language update 2019-03-17 12:28:19 +09:00
6f50ac50ec Merge pull request #689 from rockstardev/rock-lnsats
Allowing for lightning payment amounts to be displayed in Satoshis
2019-03-17 12:26:13 +09:00
5261cfcdd3 Allowing for lightning payment amounts to be displayed in Satoshis 2019-03-15 22:43:57 -05:00
675920697f Merge pull request #685 from britttttk/fix/PayReqMessage
Improve delete payment request message
2019-03-16 12:36:24 +09:00
24699bf2ba Updating QR Code on public node info page to be SVG
Co-authored-by: Sebastian Kippe <sebastian@kip.pe>
2019-03-15 22:16:12 -05:00
5ab92ed794 Switching to SVG for QR code so it looks OK on higher density display
Co-authored-by: Sebastian Kippe <sebastian@kip.pe>
2019-03-15 21:45:55 -05:00
8e83f0faa1 Improve delete payment request message 2019-03-14 23:26:39 -06:00
30d5add2ea bump 2019-03-15 14:04:17 +09:00
6e47babf45 Overpayment of lightning invoice now properly appears (Fix #486) 2019-03-14 18:48:05 +09:00
9b95fa1f20 Add links for Payment Requests and Apps docs in UI 2019-03-12 21:50:55 +09:00
c67aa14a87 Fix payment requests and crowdfund app not working if ROOTPATH is specified (fix #659) 2019-03-12 15:48:24 +09:00
23f296ef34 The store owner can define default currency pairs when using rate API without parameter 2019-03-11 18:39:21 +09:00
c6ce676ad3 Fix exception on Payment Request (#672) 2019-03-11 16:06:27 +09:00
fafb02b0dc bump 2019-03-11 15:20:10 +09:00
53cecc8c8a Improve error message for POST /tokens without body 2019-03-10 15:10:30 +09:00
db0e9ee8f8 Merge pull request #654 from Kukks/robots-suck
Add policy to discourage search engines + build robots.txt dynamcally
2019-03-10 15:00:11 +09:00
2fac794d96 Merge pull request #667 from pavlenex/pavlenex-patch-issue-templates
Add Issue Templates
2019-03-10 14:58:58 +09:00
ffcd716906 Create feature_request.md 2019-03-09 23:21:03 +01:00
85f50724db Create Bug Report template 2019-03-09 23:16:29 +01:00
808a995741 Merge pull request #665 from rockstardev/rock-qrcodefix
Rendering QR code only if there is data, plus loading indicator
2019-03-10 00:40:07 +09:00
4deb853914 remo useless mode now 2019-03-09 16:35:53 +01:00
470ec3354e Rendering QR code only if there is data, plus loading indicator 2019-03-09 09:33:05 -06:00
053c2da9f1 use thememanager instead of view component 2019-03-09 16:29:04 +01:00
c0e28ce66e Revert "Remove error in console logs on checkout page"
This reverts commit 08dd94e267c4810837b2fccd33b9001a3e406b30.
2019-03-10 00:28:47 +09:00
08dd94e267 Remove error in console logs on checkout page 2019-03-10 00:18:34 +09:00
7497865d1f Pay button was not working properly if the server was not en-US (Fix #638) 2019-03-09 23:40:22 +09:00
1888e4fe2b remove robots and remove nofollow 2019-03-09 15:01:56 +01:00
6746a5cbd5 add meta for noindex,nofollow if policy set 2019-03-09 14:55:37 +01:00
baecb7bb0c Fix coinswitch issue (#664)
Coinswitch did a breaking change, this fixes it. Closes #660
2019-03-09 22:36:25 +09:00
28bf4b42bb Fix tests 2019-03-09 22:36:07 +09:00
c73dc425ad Remove externalurl in command line options --help (Fix #658) 2019-03-09 22:30:49 +09:00
2138b7dcb8 Fix invoice popup not showing up if btcpay has a rootpath 2019-03-09 22:28:20 +09:00
8b6c4a9383 simplifed robots generator 2019-03-09 14:23:55 +01:00
344755cbd0 Add policy to discourage search engines + build robots.txt dynamcally
closes #390
2019-03-09 14:13:10 +01:00
63a975267c Fix coinswitch issue
Coinswitch did a breaking change, this fixes it. Closes #660
2019-03-09 14:11:56 +01:00
3c7d93e88d No ROOTPATH in launchsettings 2019-03-09 18:40:41 +09:00
75974037bc Fix SignalR paths if RootPath is used 2019-03-09 16:08:31 +09:00
e8a346182b Fix vue-qrcode inside the lightning network info vue 2019-03-09 12:51:05 +09:00
e96d34f741 New QR Code component, fixes scanning of long lightning invoices 2019-03-08 16:19:39 -06:00
7dad814f19 Fix some checkout page if RootPath is set (#659) 2019-03-09 00:48:33 +09:00
7e67ca1413 Fix RootPath was not working correctly on Linux 2019-03-08 23:38:00 +09:00
603263549b Document LND supported wallets in services (#657)
* Document LND supported wallets in services

* Add Zeus

* Open links in new tab
2019-03-08 18:12:14 +09:00
84 changed files with 11852 additions and 1762 deletions

34
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,34 @@
---
name: Report a problem
about: File a technical problem or report a bug
---
**Describe the problem/bug**
A clear and concise description of what the bug is.
**Your environment**
* Version of BTCPay Server:
* Deployment method:
* Other relevant environment details:
**Logs (if applicable)**
Basic logs can be found in Server Settings > Logs.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Actual behavior**
Tell us what happens instead
**Screenshots/Links**
If applicable, add screenshots or links to help explain your problem.
**Additional context**
Add any other context about the problem here.

View File

@ -0,0 +1,20 @@
---
name: Feature request
about: Ideas and feature requests
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Provide examples**
If applicable provide examples, wireframes, sketches or images to better explain your idea.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -55,6 +55,8 @@ using BTCPayServer.Events;
using BTCPayServer.Configuration;
using System.Security;
using System.Runtime.CompilerServices;
using System.Net;
using BTCPayServer.Tor;
namespace BTCPayServer.Tests
{
@ -146,6 +148,67 @@ namespace BTCPayServer.Tests
#pragma warning restore CS0618
}
[Fact]
[Trait("Fast", "Fast")]
public void CanParseEndpoint()
{
Assert.False(EndPointParser.TryParse("126.2.2.2", out var endpoint));
Assert.True(EndPointParser.TryParse("126.2.2.2:20", out endpoint));
var ipEndpoint = Assert.IsType<IPEndPoint>(endpoint);
Assert.Equal("126.2.2.2", ipEndpoint.Address.ToString());
Assert.Equal(20, ipEndpoint.Port);
Assert.True(EndPointParser.TryParse("toto.com:20", out endpoint));
var dnsEndpoint = Assert.IsType<DnsEndPoint>(endpoint);
Assert.IsNotType<OnionEndpoint>(endpoint);
Assert.Equal("toto.com", dnsEndpoint.Host.ToString());
Assert.Equal(20, dnsEndpoint.Port);
Assert.False(EndPointParser.TryParse("toto invalid hostname:2029", out endpoint));
Assert.True(EndPointParser.TryParse("toto.onion:20", out endpoint));
var onionEndpoint = Assert.IsType<OnionEndpoint>(endpoint);
Assert.Equal("toto.onion", onionEndpoint.Host.ToString());
Assert.Equal(20, onionEndpoint.Port);
}
[Fact]
[Trait("Fast", "Fast")]
public void CanParseTorrc()
{
var nl = "\n";
var input = "# For the hidden service BTCPayServer" + nl +
"HiddenServiceDir /var/lib/tor/hidden_services/BTCPayServer" + nl +
"# Redirecting to nginx" + nl +
"HiddenServicePort 80 172.19.0.10:81";
nl = Environment.NewLine;
var expected = "HiddenServiceDir /var/lib/tor/hidden_services/BTCPayServer" + nl +
"HiddenServicePort 80 172.19.0.10:81" + nl;
Assert.True(Torrc.TryParse(input, out var torrc));
Assert.Equal(expected, torrc.ToString());
nl = "\r\n";
input = "# For the hidden service BTCPayServer" + nl +
"HiddenServiceDir /var/lib/tor/hidden_services/BTCPayServer" + nl +
"# Redirecting to nginx" + nl +
"HiddenServicePort 80 172.19.0.10:81";
Assert.True(Torrc.TryParse(input, out torrc));
Assert.Equal(expected, torrc.ToString());
input = "# For the hidden service BTCPayServer" + nl +
"HiddenServiceDir /var/lib/tor/hidden_services/BTCPayServer" + nl +
"# Redirecting to nginx" + nl +
"HiddenServicePort 80 172.19.0.10:80" + nl +
"HiddenServiceDir /var/lib/tor/hidden_services/Woocommerce" + nl +
"# Redirecting to nginx" + nl +
"HiddenServicePort 80 172.19.0.11:80";
nl = Environment.NewLine;
expected = "HiddenServiceDir /var/lib/tor/hidden_services/BTCPayServer" + nl +
"HiddenServicePort 80 172.19.0.10:80" + nl +
"HiddenServiceDir /var/lib/tor/hidden_services/Woocommerce" + nl +
"HiddenServicePort 80 172.19.0.11:80" + nl;
Assert.True(Torrc.TryParse(input, out torrc));
Assert.Equal(expected, torrc.ToString());
}
[Fact]
[Trait("Fast", "Fast")]
public void CanCalculateCryptoDue()
@ -727,6 +790,18 @@ namespace BTCPayServer.Tests
var GetRatesResult = JObject.Parse(((JsonResult)rateController.GetRates(null, acc.StoreId, default)
.GetAwaiter().GetResult()).Value.ToJson()).ToObject<DataWrapper<Rate[]>>();
// We don't have any default currencies, so this should be failing
Assert.Null(GetRatesResult?.Data);
var store = acc.GetController<StoresController>();
var ratesVM = (RatesViewModel)(Assert.IsType<ViewResult>(store.Rates(acc.StoreId)).Model);
ratesVM.DefaultCurrencyPairs = "BTC_USD,LTC_USD";
store.Rates(ratesVM).Wait();
store = acc.GetController<StoresController>();
rateController = acc.GetController<RateController>();
GetRatesResult = JObject.Parse(((JsonResult)rateController.GetRates(null, acc.StoreId, default)
.GetAwaiter().GetResult()).Value.ToJson()).ToObject<DataWrapper<Rate[]>>();
// Now we should have a result
Assert.NotNull(GetRatesResult);
Assert.NotNull(GetRatesResult.Data);
Assert.Equal(2, GetRatesResult.Data.Length);
@ -869,7 +944,7 @@ namespace BTCPayServer.Tests
using (var tester = ServerTester.Create())
{
tester.Start();
foreach(var req in new[]
foreach (var req in new[]
{
"invoices/",
"invoices",
@ -1006,7 +1081,7 @@ namespace BTCPayServer.Tests
private static decimal CreateInvoice(ServerTester tester, TestAccount user, string exchange, string currency = "USD")
{
var storeController = user.GetController<StoresController>();
var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model;
var vm = (RatesViewModel)((ViewResult)storeController.Rates(user.StoreId)).Model;
vm.PreferredExchange = exchange;
storeController.Rates(vm).Wait();
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
@ -1044,7 +1119,7 @@ namespace BTCPayServer.Tests
Assert.Equal(Money.Coins(1.0m), invoice1.BtcPrice);
var storeController = user.GetController<StoresController>();
var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model;
var vm = (RatesViewModel)((ViewResult)storeController.Rates(user.StoreId)).Model;
Assert.Equal(0.0, vm.Spread);
vm.Spread = 40;
storeController.Rates(vm).Wait();
@ -1143,7 +1218,7 @@ namespace BTCPayServer.Tests
user.RegisterDerivationScheme("BTC");
var store = user.GetController<StoresController>();
var rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
var rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(user.StoreId)).Model);
Assert.False(rateVm.ShowScripting);
Assert.Equal("coinaverage", rateVm.PreferredExchange);
Assert.Equal(0.0, rateVm.Spread);
@ -1151,7 +1226,7 @@ namespace BTCPayServer.Tests
rateVm.PreferredExchange = "bitflyer";
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(user.StoreId)).Model);
Assert.Equal("bitflyer", rateVm.PreferredExchange);
rateVm.ScriptTest = "BTC_JPY,BTC_CAD";
@ -1168,7 +1243,8 @@ namespace BTCPayServer.Tests
Assert.IsType<RedirectToActionResult>(store.ShowRateRulesPost(true).Result);
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
store = user.GetController<StoresController>();
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(user.StoreId)).Model);
Assert.Equal(rateVm.StoreId, user.StoreId);
Assert.Equal(rateVm.DefaultScript, rateVm.Script);
Assert.True(rateVm.ShowScripting);
rateVm.ScriptTest = "BTC_JPY";
@ -1185,7 +1261,7 @@ namespace BTCPayServer.Tests
Assert.True(rateVm.TestRateRules.All(t => !t.Error));
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
store = user.GetController<StoresController>();
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(user.StoreId)).Model);
Assert.Equal(50, rateVm.Spread);
Assert.True(rateVm.ShowScripting);
Assert.Contains("DOGE_X", rateVm.Script, StringComparison.OrdinalIgnoreCase);
@ -2352,7 +2428,7 @@ donation:
Assert.True(ExternalConnectionString.TryParse("server=https://tow/test", out connStr, out error));
expanded = await connStr.Expand(new Uri("https://toto.com"), ExternalServiceTypes.Charge);
Assert.Equal(new Uri("https://tow/test"), expanded.Server);
// Error if directory not exists
Assert.True(ExternalConnectionString.TryParse($"server={unusedUri};macaroondirectorypath=pouet", out connStr, out error));
await Assert.ThrowsAsync<DirectoryNotFoundException>(() => connStr.Expand(unusedUri, ExternalServiceTypes.LNDGRPC));

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.3.81</Version>
<Version>1.0.3.86</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.11" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.14" />
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.5.3" />
<PackageReference Include="HtmlSanitizer" Version="4.0.199" />

View File

@ -48,11 +48,7 @@ namespace BTCPayServer.Configuration
get;
private set;
}
public List<IPEndPoint> Listen
{
get;
set;
}
public EndPoint SocksEndpoint { get; set; }
public List<NBXplorerConnectionSetting> NBXplorerConnectionSettings
{
@ -149,6 +145,10 @@ namespace BTCPayServer.Configuration
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
MySQLConnectionString = conf.GetOrDefault<string>("mysql", null);
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
TorrcFile = conf.GetOrDefault<string>("torrcfile", null);
SocksEndpoint = conf.GetOrDefault<EndPoint>("socksendpoint", null);
if (SocksEndpoint is Tor.OnionEndpoint)
throw new ConfigException($"socksendpoint should not be a tor endpoint");
var sshSettings = ParseSSHConfiguration(conf);
if ((!string.IsNullOrEmpty(sshSettings.Password) || !string.IsNullOrEmpty(sshSettings.KeyFile)) && !string.IsNullOrEmpty(sshSettings.Server))
@ -273,5 +273,6 @@ namespace BTCPayServer.Configuration
get;
set;
}
public string TorrcFile { get; set; }
}
}

View File

@ -39,6 +39,12 @@ namespace BTCPayServer.Configuration
return (T)(object)str;
else if (typeof(T) == typeof(IPAddress))
return (T)(object)IPAddress.Parse(str);
else if (typeof(T) == typeof(EndPoint))
{
if (EndPointParser.TryParse(str, out var endpoint))
return (T)(object)endpoint;
throw new FormatException("Invalid endpoint");
}
else if (typeof(T) == typeof(IPEndPoint))
{
var separator = str.LastIndexOf(":", StringComparison.InvariantCulture);

View File

@ -32,7 +32,6 @@ namespace BTCPayServer.Configuration
app.Option("--chains | -c", $"Chains to support as a comma separated (default: btc; available: {chains})", CommandOptionType.SingleValue);
app.Option("--postgres", $"Connection string to a PostgreSQL database (default: SQLite)", CommandOptionType.SingleValue);
app.Option("--mysql", $"Connection string to a MySQL database (default: SQLite)", CommandOptionType.SingleValue);
app.Option("--externalurl", $"The expected external URL of this service, to use if BTCPay is behind a reverse proxy (default: empty, use the incoming HTTP request to figure out)", CommandOptionType.SingleValue);
app.Option("--externalservices", $"Links added to external services inside Server Settings / Services under the format service1:path2;service2:path2.(default: empty)", CommandOptionType.SingleValue);
app.Option("--bundlejscss", $"Bundle JavaScript and CSS files for better performance (default: true)", CommandOptionType.SingleValue);
app.Option("--rootpath", "The root path in the URL to access BTCPay (default: /)", CommandOptionType.SingleValue);
@ -41,6 +40,8 @@ namespace BTCPayServer.Configuration
app.Option("--sshkeyfile", "SSH private key file to manage BTCPay (default: empty)", CommandOptionType.SingleValue);
app.Option("--sshkeyfilepassword", "Password of the SSH keyfile (default: empty)", CommandOptionType.SingleValue);
app.Option("--sshtrustedfingerprints", "SSH Host public key fingerprint or sha256 (default: empty, it will allow untrusted connections)", CommandOptionType.SingleValue);
app.Option("--torrcfile", "Path to torrc file containing hidden services directories (default: empty)", CommandOptionType.SingleValue);
app.Option("--socksendpoint", "Socks endpoint to connect to onion urls (default: empty)", CommandOptionType.SingleValue);
app.Option("--debuglog", "A rolling log file for debug messages.", CommandOptionType.SingleValue);
app.Option("--debugloglevel", "The severity you log (default:information)", CommandOptionType.SingleValue);
app.Option("--disable-registration", "Disables new user registrations (default:true)", CommandOptionType.SingleValue);

View File

@ -35,6 +35,8 @@ namespace BTCPayServer.Controllers
[AllowAnonymous]
public async Task<DataWrapper<List<PairingCodeResponse>>> Tokens([FromBody] TokenRequest request)
{
if (request == null)
throw new BitpayHttpException(400, "The request body is missing");
PairingCodeEntity pairingEntity = null;
if (string.IsNullOrEmpty(request.PairingCode))
{

View File

@ -109,11 +109,13 @@ 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 _AppService.GetAppInfo(appId));
var appInfo = (ViewCrowdfundViewModel)(await _AppService.GetAppInfo(appId));
appInfo.HubPath = AppHub.GetHubPath(this.Request);
if (settings.Enabled) return View(appInfo);
if(!isAdmin)
return NotFound();
return View(await _AppService.GetAppInfo(appId));
return View(appInfo);
}
[HttpPost]
@ -137,7 +139,7 @@ namespace BTCPayServer.Controllers
}
var info = (ViewCrowdfundViewModel)await _AppService.GetAppInfo(appId);
info.HubPath = AppHub.GetHubPath(this.Request);
if (!isAdmin &&
((settings.StartDate.HasValue && DateTime.Now < settings.StartDate) ||
(settings.EndDate.HasValue && DateTime.Now > settings.EndDate) ||

View File

@ -1,11 +1,8 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using BTCPayServer.Models;
using NBitcoin.DataEncoders;
using NBitcoin.Payment;
using System.Net.Http;
using Newtonsoft.Json.Linq;

View File

@ -273,6 +273,7 @@ namespace BTCPayServer.Controllers
var model = new PaymentModel()
{
CryptoCode = network.CryptoCode,
RootPath = this.Request.PathBase.Value.WithTrailingSlash(),
PaymentMethodId = paymentMethodId.ToString(),
PaymentMethodName = GetDisplayName(paymentMethodId, network),
CryptoImage = GetImage(paymentMethodId, network),
@ -283,6 +284,7 @@ namespace BTCPayServer.Controllers
HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice",
CustomCSSLink = storeBlob.CustomCSS?.AbsoluteUri,
CustomLogoLink = storeBlob.CustomLogo?.AbsoluteUri,
LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi,
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
BtcDue = accounting.Due.ToString(),
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
@ -345,9 +347,8 @@ namespace BTCPayServer.Controllers
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
{
var res = paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
Url.Content(network.CryptoImagePath) : Url.Content(network.LightningImagePath);
return "/" + res;
return paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
this.Request.GetRelativePathOrAbsolute(network.CryptoImagePath) : this.Request.GetRelativePathOrAbsolute(network.LightningImagePath);
}
private string OrderAmountFromInvoice(string cryptoCode, ProductInformation productInformation)

View File

@ -242,6 +242,7 @@ namespace BTCPayServer.Controllers
paymentMethod.Network = network;
paymentMethod.SetId(supportedPaymentMethod.PaymentId);
paymentMethod.Rate = rate.BidAsk.Bid;
paymentMethod.PreferOnion = this.Request.IsOnion();
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network, preparePayment);
paymentMethod.SetPaymentMethodDetails(paymentDetails);

View File

@ -182,7 +182,7 @@ namespace BTCPayServer.Controllers
return View("Confirm", new ConfirmModel()
{
Title = $"Remove Payment Request",
Description = $"Are you sure to remove access to remove payment request '{blob.Title}' ?",
Description = $"Are you sure you want to remove access to the payment request '{blob.Title}' ?",
Action = "Delete"
});
}
@ -219,7 +219,7 @@ namespace BTCPayServer.Controllers
{
return NotFound();
}
result.HubPath = PaymentRequestHub.GetHubPath(this.Request);
return View(result);
}
@ -229,12 +229,12 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> PayPaymentRequest(string id, bool redirectToInvoice = true,
decimal? amount = null, CancellationToken cancellationToken = default)
{
var result = ((await ViewPaymentRequest(id)) as ViewResult)?.Model as ViewPaymentRequestViewModel;
var result = await _PaymentRequestService.GetPaymentRequest(id, GetUserId());
if (result == null)
{
return NotFound();
}
result.HubPath = PaymentRequestHub.GetHubPath(this.Request);
if (result.AmountDue <= 0)
{
if (redirectToInvoice)

View File

@ -43,7 +43,7 @@ namespace BTCPayServer.Controllers
var paymentMethodDetails = GetExistingLightningSupportedPaymentMethod(cryptoCode, store);
var network = _BtcPayNetworkProvider.GetNetwork(cryptoCode);
var nodeInfo =
await _LightningLikePaymentHandler.GetNodeInfo(paymentMethodDetails,
await _LightningLikePaymentHandler.GetNodeInfo(this.Request.IsOnion(), paymentMethodDetails,
network);
return View(new ShowLightningNodeInfoViewModel()

View File

@ -91,7 +91,6 @@ namespace BTCPayServer.Controllers
[BitpayAPIConstraint]
public async Task<IActionResult> GetRates(string currencyPairs, string storeId, CancellationToken cancellationToken)
{
storeId = await GetStoreId(storeId);
var result = await GetRates2(currencyPairs, storeId, cancellationToken);
var rates = (result as JsonResult)?.Value as Rate[];
if (rates == null)
@ -140,15 +139,10 @@ namespace BTCPayServer.Controllers
if (currencyPairs == null)
{
var supportedMethods = store.GetSupportedPaymentMethods(_NetworkProvider);
var currencyCodes = supportedMethods.Select(method => method.PaymentId.CryptoCode).Distinct();
var defaultPaymentId = store.GetDefaultPaymentId(_NetworkProvider);
currencyPairs = BuildCurrencyPairs(currencyCodes, defaultPaymentId.CryptoCode);
currencyPairs = store.GetStoreBlob().GetDefaultCurrencyPairString();
if (string.IsNullOrEmpty(currencyPairs))
{
var result = Json(new BitpayErrorsModel() { Error = "You need to specify currencyPairs (eg. BTC_USD,LTC_CAD)" });
var result = Json(new BitpayErrorsModel() { Error = "You need to setup the default currency pairs in 'Store Settings / Rates' or specify 'currencyPairs' query parameter (eg. BTC_USD,LTC_CAD)." });
result.StatusCode = 400;
return result;
}

View File

@ -39,6 +39,7 @@ namespace BTCPayServer.Controllers
private RateFetcher _RateProviderFactory;
private StoreRepository _StoreRepository;
LightningConfigurationProvider _LnConfigProvider;
private readonly TorServices _torServices;
BTCPayServerOptions _Options;
public ServerController(UserManager<ApplicationUser> userManager,
@ -48,6 +49,7 @@ namespace BTCPayServer.Controllers
NBXplorerDashboard dashBoard,
IHttpClientFactory httpClientFactory,
LightningConfigurationProvider lnConfigProvider,
TorServices torServices,
Services.Stores.StoreRepository storeRepository)
{
_Options = options;
@ -58,6 +60,7 @@ namespace BTCPayServer.Controllers
_RateProviderFactory = rateProviderFactory;
_StoreRepository = storeRepository;
_LnConfigProvider = lnConfigProvider;
_torServices = torServices;
}
[Route("server/rates")]
@ -463,6 +466,25 @@ namespace BTCPayServer.Controllers
Link = this.Url.Action(nameof(SSHService))
});
}
foreach(var torService in _torServices.Services)
{
if (torService.VirtualPort == 80)
{
result.TorHttpServices.Add(new ServicesViewModel.OtherExternalService()
{
Name = torService.Name,
Link = $"http://{torService.OnionHost}"
});
}
else
{
result.TorOtherServices.Add(new ServicesViewModel.OtherExternalService()
{
Name = torService.Name,
Link = $"{torService.OnionHost}:{torService.VirtualPort}"
});
}
}
return View(result);
}
@ -664,6 +686,7 @@ namespace BTCPayServer.Controllers
private static bool IsLocalNetwork(string server)
{
return server.EndsWith(".internal", StringComparison.OrdinalIgnoreCase) ||
server.EndsWith(".local", StringComparison.OrdinalIgnoreCase) ||
server.Equals("127.0.0.1", StringComparison.OrdinalIgnoreCase) ||
server.Equals("localhost", StringComparison.OrdinalIgnoreCase);
}

View File

@ -155,7 +155,7 @@ namespace BTCPayServer.Controllers
var handler = (LightningLikePaymentHandler)_ServiceProvider.GetRequiredService<IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>>();
try
{
var info = await handler.GetNodeInfo(paymentMethod, network);
var info = await handler.GetNodeInfo(this.Request.IsOnion(), paymentMethod, network);
if (!vm.SkipPortTest)
{
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)))

View File

@ -189,24 +189,39 @@ namespace BTCPayServer.Controllers
[HttpGet]
[Route("{storeId}/rates")]
public IActionResult Rates()
public IActionResult Rates(string storeId)
{
var storeBlob = StoreData.GetStoreBlob();
var vm = new RatesViewModel();
vm.SetExchangeRates(GetSupportedExchanges(), storeBlob.PreferredExchange ?? CoinAverageRateProvider.CoinAverageName);
vm.Spread = (double)(storeBlob.Spread * 100m);
vm.StoreId = storeId;
vm.Script = storeBlob.GetRateRules(_NetworkProvider).ToString();
vm.DefaultScript = storeBlob.GetDefaultRateRules(_NetworkProvider).ToString();
vm.AvailableExchanges = GetSupportedExchanges();
vm.DefaultCurrencyPairs = storeBlob.GetDefaultCurrencyPairString();
vm.ShowScripting = storeBlob.RateScripting;
return View(vm);
}
[HttpPost]
[Route("{storeId}/rates")]
public async Task<IActionResult> Rates(RatesViewModel model, string command = null, CancellationToken cancellationToken = default)
public async Task<IActionResult> Rates(RatesViewModel model, string command = null, string storeId = null, CancellationToken cancellationToken = default)
{
model.SetExchangeRates(GetSupportedExchanges(), model.PreferredExchange);
model.StoreId = storeId ?? model.StoreId;
CurrencyPair[] currencyPairs = null;
try
{
currencyPairs = model.DefaultCurrencyPairs?
.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(p => CurrencyPair.Parse(p))
.ToArray();
}
catch
{
ModelState.AddModelError(nameof(model.DefaultCurrencyPairs), "Invalid currency pairs (should be for example: BTC_USD,BTC_CAD,BTC_JPY)");
}
if (!ModelState.IsValid)
{
return View(model);
@ -220,7 +235,7 @@ namespace BTCPayServer.Controllers
blob.PreferredExchange = model.PreferredExchange;
blob.Spread = (decimal)model.Spread / 100.0m;
blob.DefaultCurrencyPairs = currencyPairs;
if (!model.ShowScripting)
{
if (!GetSupportedExchanges().Select(c => c.Name).Contains(blob.PreferredExchange, StringComparer.OrdinalIgnoreCase))
@ -333,13 +348,14 @@ namespace BTCPayServer.Controllers
var storeBlob = StoreData.GetStoreBlob();
var vm = new CheckoutExperienceViewModel();
SetCryptoCurrencies(vm, StoreData);
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? "";
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
vm.CustomCSS = storeBlob.CustomCSS?.AbsoluteUri;
vm.CustomLogo = storeBlob.CustomLogo?.AbsoluteUri;
vm.HtmlTitle = storeBlob.HtmlTitle;
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? "";
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
vm.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi;
return View(vm);
}
void SetCryptoCurrencies(CheckoutExperienceViewModel vm, Data.StoreData storeData)
@ -396,13 +412,14 @@ namespace BTCPayServer.Controllers
{
return View(model);
}
blob.DefaultLang = model.DefaultLang;
blob.RequiresRefundEmail = model.RequiresRefundEmail;
blob.LightningMaxValue = lightningMaxValue;
blob.OnChainMinValue = onchainMinValue;
blob.CustomLogo = string.IsNullOrWhiteSpace(model.CustomLogo) ? null : new Uri(model.CustomLogo, UriKind.Absolute);
blob.CustomCSS = string.IsNullOrWhiteSpace(model.CustomCSS) ? null : new Uri(model.CustomCSS, UriKind.Absolute);
blob.HtmlTitle = string.IsNullOrWhiteSpace(model.HtmlTitle) ? null : model.HtmlTitle;
blob.DefaultLang = model.DefaultLang;
blob.RequiresRefundEmail = model.RequiresRefundEmail;
blob.OnChainMinValue = onchainMinValue;
blob.LightningMaxValue = lightningMaxValue;
blob.LightningAmountInSatoshi = model.LightningAmountInSatoshi;
if (StoreData.SetStoreBlob(blob))
{
needUpdate = true;

View File

@ -307,6 +307,25 @@ namespace BTCPayServer.Data
public bool RequiresRefundEmail { get; set; }
CurrencyPair[] _DefaultCurrencyPairs;
[JsonProperty("defaultCurrencyPairs", ItemConverterType = typeof(CurrencyPairJsonConverter))]
public CurrencyPair[] DefaultCurrencyPairs
{
get
{
return _DefaultCurrencyPairs ?? Array.Empty<CurrencyPair>();
}
set
{
_DefaultCurrencyPairs = value;
}
}
public string GetDefaultCurrencyPairString()
{
return string.Join(',', DefaultCurrencyPairs.Select(c => c.ToString()));
}
public string DefaultLang { get; set; }
[DefaultValue(60)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
@ -330,10 +349,11 @@ namespace BTCPayServer.Data
public List<RateRule_Obsolete> RateRules { get; set; } = new List<RateRule_Obsolete>();
public string PreferredExchange { get; set; }
[JsonConverter(typeof(CurrencyValueJsonConverter))]
public CurrencyValue LightningMaxValue { get; set; }
[JsonConverter(typeof(CurrencyValueJsonConverter))]
public CurrencyValue OnChainMinValue { get; set; }
[JsonConverter(typeof(CurrencyValueJsonConverter))]
public CurrencyValue LightningMaxValue { get; set; }
public bool LightningAmountInSatoshi { get; set; }
[JsonConverter(typeof(UriJsonConverter))]
public Uri CustomLogo { get; set; }

View File

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using BTCPayServer.Tor;
namespace BTCPayServer
{
public static class EndPointParser
{
public static bool TryParse(string hostPort, out EndPoint endpoint)
{
if (hostPort == null)
throw new ArgumentNullException(nameof(hostPort));
endpoint = null;
var index = hostPort.LastIndexOf(':');
if (index == -1)
return false;
var portStr = hostPort.Substring(index + 1);
if (!ushort.TryParse(portStr, out var port))
return false;
return TryParse(hostPort.Substring(0, index), port, out endpoint);
}
public static bool TryParse(string host, int port, out EndPoint endpoint)
{
if (host == null)
throw new ArgumentNullException(nameof(host));
endpoint = null;
if (IPAddress.TryParse(host, out var address))
endpoint = new IPEndPoint(address, port);
else if (host.EndsWith(".onion", StringComparison.OrdinalIgnoreCase))
endpoint = new OnionEndpoint(host, port);
else
{
if (Uri.CheckHostName(host) != UriHostNameType.Dns)
return false;
endpoint = new DnsEndPoint(host, port);
}
return true;
}
}
}

View File

@ -33,6 +33,7 @@ using BTCPayServer.Services;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using NBXplorer.DerivationStrategy;
using System.Net;
namespace BTCPayServer
{
@ -165,6 +166,13 @@ namespace BTCPayServer
(derivationStrategyBase is DirectDerivationStrategy direct) && direct.Segwit;
}
public static bool IsOnion(this HttpRequest request)
{
if (request?.Host.Host == null)
return false;
return request.Host.Host.EndsWith(".onion", StringComparison.OrdinalIgnoreCase);
}
public static string GetAbsoluteRoot(this HttpRequest request)
{
return string.Concat(
@ -217,8 +225,10 @@ namespace BTCPayServer
/// <returns></returns>
public static string GetRelativePathOrAbsolute(this HttpRequest request, string path)
{
if (Uri.TryCreate(path, UriKind.Absolute, out var unused))
if (!Uri.TryCreate(path, UriKind.RelativeOrAbsolute, out var uri) ||
uri.IsAbsoluteUri)
return path;
if (path.Length > 0 && path[0] != '/')
path = $"/{path}";
return string.Concat(
@ -313,6 +323,7 @@ namespace BTCPayServer
return await doing;
}
}
public static async Task WithCancellation(this Task task, CancellationToken cancellationToken)
{
using (var delayCTS = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))

View File

@ -45,10 +45,12 @@ namespace BTCPayServer.HostedServices
}
public bool ShowRegister { get; set; }
public bool DiscourageSearchEngines { get; set; }
internal void Update(PoliciesSettings data)
{
ShowRegister = !data.LockSubscription;
DiscourageSearchEngines = data.DiscourageSearchEngines;
}
}

View File

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Configuration;
using BTCPayServer.Services;
using Microsoft.Extensions.Hosting;
namespace BTCPayServer.HostedServices
{
public class TorServicesHostedService : BaseAsyncService
{
private readonly BTCPayServerOptions _options;
private readonly TorServices _torServices;
public TorServicesHostedService(BTCPayServerOptions options, TorServices torServices)
{
_options = options;
_torServices = torServices;
}
internal override Task[] InitializeTasks()
{
// TODO: We should report auto configured services (like bitcoind, lnd or clightning)
if (string.IsNullOrEmpty(_options.TorrcFile))
return Array.Empty<Task>();
return new Task[] { CreateLoopTask(RefreshTorServices) };
}
async Task RefreshTorServices()
{
await _torServices.Refresh();
await Task.Delay(TimeSpan.FromSeconds(120), Cancellation);
}
}
}

View File

@ -62,6 +62,8 @@ namespace BTCPayServer.Hosting
});
services.AddHttpClient();
services.TryAddSingleton<SettingsRepository>();
services.TryAddSingleton<TorServices>();
services.TryAddSingleton<SocketFactory>();
services.TryAddSingleton<InvoicePaymentNotification>();
services.TryAddSingleton<BTCPayServerOptions>(o => o.GetRequiredService<IOptions<BTCPayServerOptions>>().Value);
services.TryAddSingleton<InvoiceRepository>(o =>
@ -188,6 +190,7 @@ namespace BTCPayServer.Hosting
services.AddSingleton<IHostedService, RatesHostedService>();
services.AddSingleton<IHostedService, BackgroundJobSchedulerHostedService>();
services.AddSingleton<IHostedService, AppHubStreamer>();
services.AddSingleton<IHostedService, TorServicesHostedService>();
services.AddSingleton<IHostedService, PaymentRequestStreamer>();
services.AddSingleton<IBackgroundJobClient, BackgroundJobClient>();
services.AddTransient<IConfigureOptions<MvcOptions>, BTCPayClaimsFilter>();

View File

@ -3,7 +3,6 @@ using System.Reflection;
using System.Linq;
using Microsoft.AspNetCore.Builder;
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
@ -14,7 +13,6 @@ using BTCPayServer.Authentication;
using Microsoft.EntityFrameworkCore;
using BTCPayServer.Filters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using BTCPayServer.Services;
using BTCPayServer.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.HttpOverrides;
@ -174,8 +172,8 @@ namespace BTCPayServer.Hosting
app.UseAuthentication();
app.UseSignalR(route =>
{
route.MapHub<AppHub>("/apps/hub");
route.MapHub<PaymentRequestHub>("/payment-requests/hub");
AppHub.Register(route);
PaymentRequestHub.Register(route);
});
app.UseWebSockets();
app.UseStatusCodePages();

View File

@ -0,0 +1,39 @@
using System;
using System.Reflection;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
using NBitcoin.JsonConverters;
using BTCPayServer.Rating;
namespace BTCPayServer.JsonConverters
{
public class CurrencyPairJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(CurrencyValue).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
try
{
return reader.TokenType == JsonToken.Null ? null :
CurrencyPair.TryParse((string)reader.Value, out var result) ? result :
throw new JsonObjectException("Invalid currency pair", reader);
}
catch (InvalidCastException)
{
throw new JsonObjectException("Invalid currency pair", reader);
}
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value != null)
writer.WriteValue(value.ToString());
}
}
}

View File

@ -0,0 +1,99 @@
// Copied and adjusted from https://github.com/aspnet/Mvc/blob/master/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/DecimalModelBinder.cs
using System;
using System.Globalization;
using System.Runtime.ExceptionServices;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace BTCPayServer.ModelBinders
{
/// <summary>
/// An <see cref="IModelBinder"/> for <see cref="decimal"/> and <see cref="Nullable{T}"/> where <c>T</c> is
/// <see cref="decimal"/>.
/// </summary>
public class InvariantDecimalModelBinder : IModelBinder
{
private readonly NumberStyles _supportedStyles;
public InvariantDecimalModelBinder()
{
_supportedStyles = NumberStyles.Any;
}
/// <inheritdoc />
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
var modelName = bindingContext.ModelName;
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
if (valueProviderResult == ValueProviderResult.None)
{
return Task.CompletedTask;
}
var modelState = bindingContext.ModelState;
modelState.SetModelValue(modelName, valueProviderResult);
var metadata = bindingContext.ModelMetadata;
var type = metadata.UnderlyingOrModelType;
try
{
var value = valueProviderResult.FirstValue;
var culture = CultureInfo.InvariantCulture;
object model;
if (string.IsNullOrWhiteSpace(value))
{
// Parse() method trims the value (with common NumberStyles) then throws if the result is empty.
model = null;
}
else if (type == typeof(decimal))
{
model = decimal.Parse(value, _supportedStyles, culture);
}
else
{
// unreachable
throw new NotSupportedException();
}
// When converting value, a null model may indicate a failed conversion for an otherwise required
// model (can't set a ValueType to null). This detects if a null model value is acceptable given the
// current bindingContext. If not, an error is logged.
if (model == null && !metadata.IsReferenceOrNullableType)
{
modelState.TryAddModelError(
modelName,
metadata.ModelBindingMessageProvider.ValueMustNotBeNullAccessor(
valueProviderResult.ToString()));
}
else
{
bindingContext.Result = ModelBindingResult.Success(model);
}
}
catch (Exception exception)
{
var isFormatException = exception is FormatException;
if (!isFormatException && exception.InnerException != null)
{
// Unlike TypeConverters, floating point types do not seem to wrap FormatExceptions. Preserve
// this code in case a cursory review of the CoreFx code missed something.
exception = ExceptionDispatchInfo.Capture(exception.InnerException).SourceException;
}
modelState.TryAddModelError(modelName, exception, metadata);
// Conversion failed.
}
return Task.CompletedTask;
}
}
}

View File

@ -10,6 +10,7 @@ namespace BTCPayServer.Models.AppViewModels
{
public class ViewCrowdfundViewModel
{
public string HubPath { get; set; }
public string StatusMessage{ get; set; }
public string StoreId { get; set; }
public string AppId { get; set; }

View File

@ -20,6 +20,7 @@ namespace BTCPayServer.Models.InvoicingModels
public string CustomCSSLink { get; set; }
public string CustomLogoLink { get; set; }
public string DefaultLang { get; set; }
public bool LightningAmountInSatoshi { get; set; }
public List<AvailableCrypto> AvailableCryptos { get; set; } = new List<AvailableCrypto>();
public bool IsModal { get; set; }
public bool IsLightning { get; set; }
@ -64,5 +65,6 @@ namespace BTCPayServer.Models.InvoicingModels
public bool CoinSwitchEnabled { get; set; }
public string CoinSwitchMode { get; set; }
public string CoinSwitchMerchantId { get; set; }
public string RootPath { get; set; }
}
}

View File

@ -134,6 +134,7 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
public string AmountCollectedFormatted { get; set; }
public string AmountFormatted { get; set; }
public bool AnyPendingInvoice { get; set; }
public string HubPath { get; set; }
public class PaymentRequestInvoice
{

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Configuration;
using BTCPayServer.Services;
namespace BTCPayServer.Models.ServerViewModels
{
@ -17,5 +18,7 @@ namespace BTCPayServer.Models.ServerViewModels
public List<ExternalService> ExternalServices { get; set; } = new List<ExternalService>();
public List<OtherExternalService> OtherExternalServices { get; set; } = new List<OtherExternalService>();
public List<OtherExternalService> TorHttpServices { get; set; } = new List<OtherExternalService>();
public List<OtherExternalService> TorOtherServices { get; set; } = new List<OtherExternalService>();
}
}

View File

@ -25,19 +25,6 @@ namespace BTCPayServer.Models.StoreViewModels
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...")]
[MaxLength(20)]
public string LightningMaxValue { get; set; }
[Display(Name = "Requires a refund email")]
public bool RequiresRefundEmail
{
get; set;
}
[Display(Name = "Do not propose on chain payment if the value of the invoice is below...")]
[MaxLength(20)]
public string OnChainMinValue { get; set; }
[Display(Name = "Link to a custom CSS stylesheet")]
[Uri]
@ -49,9 +36,23 @@ namespace BTCPayServer.Models.StoreViewModels
[Display(Name = "Custom HTML title to display on Checkout page")]
public string HtmlTitle { get; set; }
[Display(Name = "Requires a refund email")]
public bool RequiresRefundEmail { get; set; }
[Display(Name = "Do not propose on chain payment if the value of the invoice is below...")]
[MaxLength(20)]
public string OnChainMinValue { get; set; }
[Display(Name = "Do not propose lightning payment if value of the invoice is above...")]
[MaxLength(20)]
public string LightningMaxValue { get; set; }
[Display(Name = "Display lightning payment amounts in Satoshis")]
public bool LightningAmountInSatoshi { get; set; }
public void SetLanguages(LanguageService langService, string defaultLang)
{
defaultLang = langService.GetLanguages().Any(language => language.Code == defaultLang)? defaultLang : "en";
defaultLang = langService.GetLanguages().Any(language => language.Code == defaultLang) ? defaultLang : "en";
var choices = langService.GetLanguages().Select(o => new Format() { Name = o.DisplayName, Value = o.Code }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Value == defaultLang) ?? choices.FirstOrDefault();
Languages = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);

View File

@ -3,11 +3,14 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.ModelBinders;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Models.StoreViewModels
{
public class PayButtonViewModel
{
[ModelBinder(BinderType = typeof(InvariantDecimalModelBinder))]
public decimal Price { get; set; }
public string InvoiceId { get; set; }
[Required]

View File

@ -42,6 +42,8 @@ namespace BTCPayServer.Models.StoreViewModels
public string Script { get; set; }
public string DefaultScript { get; set; }
public string ScriptTest { get; set; }
public string DefaultCurrencyPairs { get; set; }
public string StoreId { get; set; }
public CoinAverageExchange[] AvailableExchanges { get; set; }
[Display(Name = "Add a spread on exchange rate of ... %")]

View File

@ -12,6 +12,7 @@ using BTCPayServer.Services.PaymentRequests;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using BTCPayServer.Services.Apps;
using Microsoft.AspNetCore.Http;
namespace BTCPayServer.PaymentRequest
{
@ -59,6 +60,15 @@ namespace BTCPayServer.PaymentRequest
break;
}
}
public static string GetHubPath(HttpRequest request)
{
return request.GetRelativePathOrAbsolute("/payment-requests/hub");
}
public static void Register(HubRouteBuilder route)
{
route.MapHub<PaymentRequestHub>("/payment-requests/hub");
}
}
public class PaymentRequestStreamer : EventHostedServiceBase

View File

@ -1,14 +1,16 @@
using System;
using System.Net.Sockets;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Lightning;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Tor;
using BTCPayServer.Services;
namespace BTCPayServer.Payments.Lightning
{
@ -17,15 +19,19 @@ namespace BTCPayServer.Payments.Lightning
public static int LIGHTNING_TIMEOUT = 5000;
NBXplorerDashboard _Dashboard;
private readonly SocketFactory _socketFactory;
public LightningLikePaymentHandler(
NBXplorerDashboard dashboard)
NBXplorerDashboard dashboard,
SocketFactory socketFactory)
{
_Dashboard = dashboard;
_socketFactory = socketFactory;
}
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network, object preparePaymentObject)
{
var storeBlob = store.GetStoreBlob();
var test = GetNodeInfo(supportedPaymentMethod, network);
var test = GetNodeInfo(paymentMethod.PreferOnion, supportedPaymentMethod, network);
var invoice = paymentMethod.ParentEntity;
var due = Extensions.RoundUp(invoice.ProductInformation.Price / paymentMethod.Rate, 8);
var client = supportedPaymentMethod.CreateClient(network);
@ -63,7 +69,7 @@ namespace BTCPayServer.Payments.Lightning
};
}
public async Task<NodeInfo> GetNodeInfo(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
public async Task<NodeInfo> GetNodeInfo(bool preferOnion, LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
{
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
throw new PaymentMethodUnavailableException($"Full node not available");
@ -84,8 +90,8 @@ namespace BTCPayServer.Payments.Lightning
{
throw new PaymentMethodUnavailableException($"Error while connecting to the API ({ex.Message})");
}
if (info.NodeInfo == null)
var nodeInfo = info.NodeInfoList.FirstOrDefault(i => i.IsTor == preferOnion) ?? info.NodeInfoList.FirstOrDefault();
if (nodeInfo == null)
{
throw new PaymentMethodUnavailableException($"No lightning node public address has been configured");
}
@ -96,7 +102,7 @@ namespace BTCPayServer.Payments.Lightning
throw new PaymentMethodUnavailableException($"The lightning node is not synched ({blocksGap} blocks left)");
}
return info.NodeInfo;
return nodeInfo;
}
}
@ -104,22 +110,11 @@ namespace BTCPayServer.Payments.Lightning
{
try
{
IPAddress address = null;
try
{
address = IPAddress.Parse(nodeInfo.Host);
}
catch
{
address = (await Dns.GetHostAddressesAsync(nodeInfo.Host)).FirstOrDefault();
}
if (!EndPointParser.TryParse(nodeInfo.Host, nodeInfo.Port, out var endpoint))
throw new PaymentMethodUnavailableException($"Could not parse the endpoint {nodeInfo.Host}");
if (address == null)
throw new PaymentMethodUnavailableException($"DNS did not resolve {nodeInfo.Host}");
using (var tcp = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp))
using (var tcp = await _socketFactory.ConnectAsync(endpoint, SocketType.Stream, ProtocolType.Tcp, cancellation))
{
await tcp.ConnectAsync(new IPEndPoint(address, nodeInfo.Port)).WithCancellation(cancellation);
}
}
catch (Exception ex)

View File

@ -192,7 +192,7 @@ namespace BTCPayServer.Payments.Lightning
{
BOLT11 = notification.BOLT11,
PaymentHash = BOLT11PaymentRequest.Parse(notification.BOLT11, network.NBitcoinNetwork).PaymentHash,
Amount = notification.Amount
Amount = notification.AmountReceived ?? notification.Amount, // if running old version amount received might be unavailable
}, network, accounted: true);
if (payment != null)
{

View File

@ -2,6 +2,7 @@ using System;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Models.AppViewModels;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
@ -62,5 +63,13 @@ namespace BTCPayServer.Services.Apps
}
public static string GetHubPath(HttpRequest request)
{
return request.GetRelativePathOrAbsolute("/apps/hub");
}
public static void Register(HubRouteBuilder route)
{
route.MapHub<AppHub>("/apps/hub");
}
}
}

View File

@ -14,7 +14,8 @@ namespace BTCPayServer.Services
public class BTCPayServerEnvironment
{
IHttpContextAccessor httpContext;
public BTCPayServerEnvironment(IHostingEnvironment env, BTCPayNetworkProvider provider, IHttpContextAccessor httpContext)
TorServices torServices;
public BTCPayServerEnvironment(IHostingEnvironment env, BTCPayNetworkProvider provider, IHttpContextAccessor httpContext, TorServices torServices)
{
this.httpContext = httpContext;
Version = typeof(BTCPayServerEnvironment).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyFileVersionAttribute>().Version;
@ -25,6 +26,7 @@ namespace BTCPayServer.Services
#endif
Environment = env;
NetworkType = provider.NetworkType;
this.torServices = torServices;
}
public IHostingEnvironment Environment
{
@ -34,6 +36,8 @@ namespace BTCPayServer.Services
public string ExpectedDomain => httpContext.HttpContext.Request.Host.Host;
public string ExpectedHost => httpContext.HttpContext.Request.Host.Value;
public string ExpectedProtocol => httpContext.HttpContext.Request.Scheme;
public string OnionUrl => this.torServices.Services.Where(s => s.ServiceType == TorServiceType.BTCPayServer)
.Select(s => $"http://{s.OnionHost}").FirstOrDefault();
public NetworkType NetworkType { get; set; }
public string Version

View File

@ -717,6 +717,12 @@ namespace BTCPayServer.Services.Invoices
[Obsolete("Use GetId().PaymentType instead")]
public string PaymentType { get; set; }
/// <summary>
/// We only use this to pass a singleton asking to the payment handler to prefer payments through TOR, we don't really
/// need to save this information
/// </summary>
[JsonIgnore]
public bool PreferOnion { get; set; }
public PaymentMethodId GetId()
{

View File

@ -18,5 +18,9 @@ namespace BTCPayServer.Services
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
[Display(Name = "Disable registration")]
public bool LockSubscription { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
[Display(Name = "Discourage search engines from indexing this site")]
public bool DiscourageSearchEngines { get; set; }
}
}

View File

@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Configuration;
using BTCPayServer.Tor;
namespace BTCPayServer.Services
{
public class SocketFactory
{
private readonly BTCPayServerOptions _options;
public SocketFactory(BTCPayServerOptions options)
{
_options = options;
}
public async Task<Socket> ConnectAsync(EndPoint endPoint, SocketType socketType, ProtocolType protocolType, CancellationToken cancellationToken)
{
Socket socket = null;
try
{
if (endPoint is IPEndPoint ipEndpoint)
{
socket = new Socket(ipEndpoint.Address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
await socket.ConnectAsync(ipEndpoint).WithCancellation(cancellationToken);
}
else if (endPoint is OnionEndpoint onionEndpoint)
{
if (_options.SocksEndpoint == null)
throw new NotSupportedException("It is impossible to connect to an onion address without btcpay's -socksendpoint configured");
socket = await Socks5Connect.ConnectSocksAsync(_options.SocksEndpoint, onionEndpoint, cancellationToken);
}
else if (endPoint is DnsEndPoint dnsEndPoint)
{
var address = (await Dns.GetHostAddressesAsync(dnsEndPoint.Host)).FirstOrDefault();
socket = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
await socket.ConnectAsync(dnsEndPoint).WithCancellation(cancellationToken);
}
else
throw new NotSupportedException("Endpoint type not supported");
}
catch
{
CloseSocket(ref socket);
throw;
}
return socket;
}
private void CloseSocket(ref Socket s)
{
if (s == null)
return;
try
{
s.Shutdown(SocketShutdown.Both);
}
catch
{
try
{
s.Dispose();
}
catch { }
}
finally
{
s = null;
}
}
}
}

View File

@ -0,0 +1,90 @@
using System;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Configuration;
using BTCPayServer.Logging;
namespace BTCPayServer.Services
{
public class TorServices
{
BTCPayServerOptions _Options;
public TorServices(BTCPayServerOptions options)
{
_Options = options;
}
public TorService[] Services { get; internal set; } = Array.Empty<TorService>();
internal async Task Refresh()
{
if (string.IsNullOrEmpty(_Options.TorrcFile) || !File.Exists(_Options.TorrcFile))
{
if (!string.IsNullOrEmpty(_Options.TorrcFile))
Logs.PayServer.LogWarning("Torrc file is not found");
Services = Array.Empty<TorService>();
return;
}
List<TorService> result = new List<TorService>();
try
{
var torrcContent = await File.ReadAllTextAsync(_Options.TorrcFile);
if (!Torrc.TryParse(torrcContent, out var torrc))
{
Logs.PayServer.LogWarning("Torrc file could not be parsed");
Services = Array.Empty<TorService>();
return;
}
var services = torrc.ServiceDirectories.SelectMany(d => d.ServicePorts.Select(p => (Directory: new DirectoryInfo(d.DirectoryPath), VirtualPort: p.VirtualPort)))
.Select(d => (ServiceName: d.Directory.Name,
ReadingLines: System.IO.File.ReadAllLinesAsync(Path.Combine(d.Directory.FullName, "hostname")),
VirtualPort: d.VirtualPort))
.ToArray();
foreach (var service in services)
{
try
{
var onionHost = (await service.ReadingLines)[0].Trim();
var torService = new TorService()
{
Name = service.ServiceName,
OnionHost = onionHost,
VirtualPort = service.VirtualPort
};
if (service.ServiceName.Equals("BTCPayServer", StringComparison.OrdinalIgnoreCase))
torService.ServiceType = TorServiceType.BTCPayServer;
result.Add(torService);
}
catch (Exception ex)
{
Logs.PayServer.LogWarning(ex, $"Error while reading hidden service {service.ServiceName} configuration");
}
}
}
catch (Exception ex)
{
Logs.PayServer.LogWarning(ex, $"Error while reading torrc file");
}
Services = result.ToArray();
}
}
public class TorService
{
public TorServiceType ServiceType { get; set; } = TorServiceType.Other;
public string Name { get; set; }
public string OnionHost { get; set; }
public int VirtualPort { get; set; }
}
public enum TorServiceType
{
BTCPayServer,
Other
}
}

View File

@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
namespace BTCPayServer.Services
{
public class Torrc
{
public static bool TryParse(string str, out Torrc value)
{
value = null;
List<HiddenServiceDir> serviceDirectories = new List<HiddenServiceDir>();
var lines = str.Split(new char[] { '\n' });
HiddenServiceDir currentDirectory = null;
foreach (var line in lines)
{
if (HiddenServiceDir.TryParse(line, out var dir))
{
serviceDirectories.Add(dir);
currentDirectory = dir;
}
else if (HiddenServicePortDefinition.TryParse(line, out var portDef) && currentDirectory != null)
{
currentDirectory.ServicePorts.Add(portDef);
}
}
value = new Torrc() { ServiceDirectories = serviceDirectories };
return true;
}
public List<HiddenServiceDir> ServiceDirectories { get; set; } = new List<HiddenServiceDir>();
public override string ToString()
{
StringBuilder builder = new StringBuilder();
foreach(var serviceDir in ServiceDirectories)
{
builder.AppendLine(serviceDir.ToString());
foreach (var port in serviceDir.ServicePorts)
builder.AppendLine(port.ToString());
}
return builder.ToString();
}
}
public class HiddenServiceDir
{
public static bool TryParse(string str, out HiddenServiceDir serviceDir)
{
serviceDir = null;
if (!str.Trim().StartsWith("HiddenServiceDir ", StringComparison.OrdinalIgnoreCase))
return false;
var parts = str.Split(new char[] { ' ', '\t' }, StringSplitOptions.None);
if (parts.Length != 2)
return false;
serviceDir = new HiddenServiceDir() { DirectoryPath = parts[1].Trim() };
return true;
}
public string DirectoryPath { get; set; }
public List<HiddenServicePortDefinition> ServicePorts { get; set; } = new List<HiddenServicePortDefinition>();
public override string ToString()
{
return $"HiddenServiceDir {DirectoryPath}";
}
}
public class HiddenServicePortDefinition
{
public static bool TryParse(string str, out HiddenServicePortDefinition portDefinition)
{
portDefinition = null;
if (!str.Trim().StartsWith("HiddenServicePort ", StringComparison.OrdinalIgnoreCase))
return false;
var parts = str.Split(new char[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length != 3)
return false;
if (!int.TryParse(parts[1].Trim(), out int virtualPort))
return false;
var addressPort = parts[2].Trim().Split(':', StringSplitOptions.RemoveEmptyEntries);
if (addressPort.Length != 2)
return false;
if (!int.TryParse(addressPort[1].Trim(), out int port))
return false;
if (!IPAddress.TryParse(addressPort[0].Trim(), out IPAddress address))
return false;
portDefinition = new HiddenServicePortDefinition() { VirtualPort = virtualPort, Endpoint = new IPEndPoint(address, port) };
return true;
}
public int VirtualPort { get; set; }
public IPEndPoint Endpoint { get; set; }
public override string ToString()
{
return $"HiddenServicePort {VirtualPort} {Endpoint}";
}
}
}

View File

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
namespace BTCPayServer.Tor
{
public class OnionEndpoint : DnsEndPoint
{
public OnionEndpoint(string host, int port): base(host, port)
{
}
}
}

View File

@ -0,0 +1,186 @@
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace BTCPayServer.Tor
{
public enum SocksErrorCode
{
Success = 0,
GeneralServerFailure = 1,
ConnectionNotAllowed = 2,
NetworkUnreachable = 3,
HostUnreachable = 4,
ConnectionRefused = 5,
TTLExpired = 6,
CommandNotSupported = 7,
AddressTypeNotSupported = 8,
}
public class SocksException : Exception
{
public SocksException(SocksErrorCode errorCode) : base(GetMessageForCode((int)errorCode))
{
SocksErrorCode = errorCode;
}
public SocksErrorCode SocksErrorCode
{
get; set;
}
private static string GetMessageForCode(int errorCode)
{
switch (errorCode)
{
case 0:
return "Success";
case 1:
return "general SOCKS server failure";
case 2:
return "connection not allowed by ruleset";
case 3:
return "Network unreachable";
case 4:
return "Host unreachable";
case 5:
return "Connection refused";
case 6:
return "TTL expired";
case 7:
return "Command not supported";
case 8:
return "Address type not supported";
default:
return "Unknown code";
}
}
public SocksException(string message) : base(message)
{
}
}
public class Socks5Connect
{
static readonly byte[] SelectionMessage = new byte[] { 5, 1, 0 };
public static async Task<Socket> ConnectSocksAsync(EndPoint socksEndpoint, DnsEndPoint endpoint, CancellationToken cancellation)
{
Socket s = null;
int maxTries = 3;
int retry = 0;
try
{
while (true)
{
s = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
await s.ConnectAsync(socksEndpoint).WithCancellation(cancellation).ConfigureAwait(false);
NetworkStream stream = new NetworkStream(s, false);
await stream.WriteAsync(SelectionMessage, 0, SelectionMessage.Length, cancellation).ConfigureAwait(false);
await stream.FlushAsync(cancellation).ConfigureAwait(false);
var selectionResponse = new byte[2];
await stream.ReadAsync(selectionResponse, 0, 2, cancellation);
if (selectionResponse[0] != 5)
throw new SocksException("Invalid version in selection reply");
if (selectionResponse[1] != 0)
throw new SocksException("Unsupported authentication method in selection reply");
var connectBytes = CreateConnectMessage(endpoint.Host, endpoint.Port);
await stream.WriteAsync(connectBytes, 0, connectBytes.Length, cancellation).ConfigureAwait(false);
await stream.FlushAsync(cancellation).ConfigureAwait(false);
var connectResponse = new byte[10];
await stream.ReadAsync(connectResponse, 0, 10, cancellation);
if (connectResponse[0] != 5)
throw new SocksException("Invalid version in connect reply");
if (connectResponse[1] != 0)
{
var code = (SocksErrorCode)connectResponse[1];
if (!IsTransient(code) || retry++ >= maxTries)
throw new SocksException(code);
CloseSocket(ref s);
await Task.Delay(1000, cancellation).ConfigureAwait(false);
continue;
}
if (connectResponse[2] != 0)
throw new SocksException("Invalid RSV in connect reply");
if (connectResponse[3] != 1)
throw new SocksException("Invalid ATYP in connect reply");
for (int i = 4; i < 4 + 4; i++)
{
if (connectResponse[i] != 0)
throw new SocksException("Invalid BIND address in connect reply");
}
if (connectResponse[8] != 0 || connectResponse[9] != 0)
throw new SocksException("Invalid PORT address connect reply");
return s;
}
}
catch
{
CloseSocket(ref s);
throw;
}
}
private static void CloseSocket(ref Socket s)
{
if (s == null)
return;
try
{
s.Shutdown(SocketShutdown.Both);
}
catch
{
try
{
s.Dispose();
}
catch { }
}
finally
{
s = null;
}
}
private static bool IsTransient(SocksErrorCode code)
{
return code == SocksErrorCode.GeneralServerFailure ||
code == SocksErrorCode.TTLExpired;
}
internal static byte[] CreateConnectMessage(string host, int port)
{
byte[] sendBuffer;
byte[] nameBytes = Encoding.ASCII.GetBytes(host);
var addressBytes =
Enumerable.Empty<byte>()
.Concat(new[] { (byte)nameBytes.Length })
.Concat(nameBytes).ToArray();
sendBuffer =
Enumerable.Empty<byte>()
.Concat(
new byte[]
{
(byte)5, (byte) 0x01, (byte) 0x00, (byte)0x03
})
.Concat(addressBytes)
.Concat(BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)port))).ToArray();
return sendBuffer;
}
}
}

View File

@ -21,8 +21,13 @@
</div>
</div>
<div class="row no-gutter" style="margin-bottom: 5px;">
<div class="col-lg-6">
<a asp-action="CreateApp" class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Create a new app</a>
<a href="https://docs.btcpayserver.org/features/apps" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
</div>
</div>
<div class="row">
<a asp-action="CreateApp" class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Create a new app</a>
<table class="table table-sm table-responsive-md">
<thead>
<tr>

View File

@ -10,13 +10,13 @@
<html class="h-100">
<head>
<title>@Model.Title</title>
<meta charset="utf-8"/>
<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"/>
<link href="@Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet" />
@if (Model.CustomCSSLink != null)
{
<link href="@Model.CustomCSSLink" rel="stylesheet"/>
<link href="@Model.CustomCSSLink" rel="stylesheet" />
}
@if (!Context.Request.Query.ContainsKey("simple"))
{
@ -25,13 +25,15 @@
</script>
<bundle name="wwwroot/bundles/crowdfund-bundle-1.min.js"></bundle>
<bundle name="wwwroot/bundles/crowdfund-bundle-2.min.js"></bundle>
@*We need to make sure btcpay.js is not bundled, else it will not work if there is a RootPath*@
<script src="~/modal/btcpay.js"></script>
}
<bundle name="wwwroot/bundles/crowdfund-bundle.min.css"></bundle>
@if (!string.IsNullOrEmpty(Model.EmbeddedCSS))
{
<style>
@Html.Raw(Model.EmbeddedCSS);
@Html.Raw(Model.EmbeddedCSS);
</style>
}

View File

@ -129,31 +129,31 @@
</div>
<div class="row">
<div class="col-lg-3 ml-auto text-center">
<a href="https://chat.btcpayserver.org/">
<a href="https://chat.btcpayserver.org/" target="_blank">
<img src="~/img/mattermost.png" height="100" />
</a>
<p><a href="https://chat.btcpayserver.org/">On Mattermost</a></p>
<p><a href="https://chat.btcpayserver.org/" target="_blank">On Mattermost</a></p>
</div>
<div class="col-lg-3 ml-auto text-center">
<a href="https://slack.btcpayserver.org/">
<a href="https://slack.btcpayserver.org/" target="_blank">
<img src="~/img/slack.png" height="100" />
</a>
<p><a href="http://slack.forkbitpay.ninja/">On Slack</a></p>
<p><a href="http://slack.forkbitpay.ninja/" target="_blank">On Slack</a></p>
</div>
<div class="col-lg-3 mr-auto text-center">
<a href="https://twitter.com/BtcpayServer">
<a href="https://twitter.com/BtcpayServer" target="_blank">
<img src="~/img/twitter.png" height="100" />
</a>
<p>
<a href="https://twitter.com/BtcpayServer">On Twitter</a>
<a href="https://twitter.com/BtcpayServer" target="_blank">On Twitter</a>
</p>
</div>
<div class="col-lg-3 mr-auto text-center">
<a href="https://github.com/btcpayserver/btcpayserver">
<a href="https://github.com/btcpayserver/btcpayserver" target="_blank">
<img src="~/img/github.png" height="100" />
</a>
<p>
<a href="https://github.com/btcpayserver/btcpayserver">On Github</a>
<a href="https://github.com/btcpayserver/btcpayserver" target="_blank">On Github</a>
</p>
</div>
</div>

View File

@ -48,7 +48,7 @@
<div class="paywithRowRight cursorPointer" onclick="openPaymentMethodDialog()">
<span class="payment__currencies ">
<img v-bind:src="srvModel.cryptoImage" />
<span>{{srvModel.paymentMethodName}} ({{srvModel.cryptoCode}})</span>
<span>{{srvModel.paymentMethodName}} ({{srvModel.cryptoCodeSrv}})</span>
<span v-show="srvModel.isLightning">&#9889;</span>
<span class="clickable_indicator fa fa-angle-right"></span>
</span>
@ -73,7 +73,7 @@
{
<div class="payment__currencies_noborder">
<img v-bind:src="srvModel.cryptoImage" />
<span>{{srvModel.paymentMethodName}} ({{srvModel.cryptoCode}})</span>
<span>{{srvModel.paymentMethodName}} ({{srvModel.cryptoCodeSrv}})</span>
<span v-show="srvModel.isLightning">&#9889;</span>
</div>
}
@ -101,7 +101,7 @@
</div>
<div class="single-item-order__right__ex-rate" v-if="srvModel.orderAmountFiat">
1 {{ srvModel.cryptoCode }} = {{ srvModel.rate }}
1 {{ srvModel.cryptoCodeSrv }} = {{ srvModel.rate }}
</div>
</div>
@ -192,7 +192,7 @@
</form>
</div>
<div class="bp-view payment scan" id="scan">
<div class="wrapBtnGroup" v-bind:class="{ invisible: lndModel === null }">
<div class="wrapBtnGroup" v-bind:class="{ invisible: lndModel === null || !scanDisplayQr }">
<div class="btnGroupLnd">
<button onclick="lndToggleBolt11()" v-bind:class="{ active: lndModel != null && lndModel.toggle === 0 }"
v-bind:title="$t('BOLT 11 Invoice')">
@ -205,11 +205,18 @@
</div>
</div>
<div class="payment__scan">
<img v-bind:src="srvModel.cryptoImage" class="qr_currency_icon" />
<qrcode v-bind:val="scanDisplayQr" v-bind:size="256" bg-color="#f5f5f7" fg-color="#000">
<img v-bind:src="srvModel.cryptoImage" class="qr_currency_icon"
v-if="scanDisplayQr" />
<qrcode v-bind:value="scanDisplayQr" :options="{ width: 256, margin: 0, color: {dark:'#000', light:'#f5f5f7'} }" tag="svg"
v-if="scanDisplayQr">
</qrcode>
<div class="payment__spinner qr_currency_icon" style="padding-right: 20px;">
<partial name="Checkout-Spinner" />
</div>
</div>
<div class="payment__details__instruction__open-wallet">
<div class="payment__details__instruction__open-wallet" v-if="scanDisplayQr">
<a class="payment__details__instruction__open-wallet__btn action-button" v-bind:href="srvModel.invoiceBitcoinUrl">
<span>{{$t("Open in wallet")}}</span>
</a>

View File

@ -82,15 +82,15 @@
@* Not working because of nsSeparator: false, keySeparator: false,
{{$t("nested.lang")}} >>
*@
<select asp-for="DefaultLang"
<select asp-for="DefaultLang"
class="cmblang reverse invisible"
onchange="changeLanguage($(this).val())"
asp-items="@langService.GetLanguages().Select((language) => new SelectListItem(language.DisplayName,language.Code, false))"></select>
onchange="changeLanguage($(this).val())"
asp-items="@langService.GetLanguages().Select((language) => new SelectListItem(language.DisplayName,language.Code, false))"></select>
<script>
var languageSelectorPrettyDropdown;
$(function() {
$(function () {
// REVIEW: don't use initDropdown method but rather directly initialize select whenever you are using it
$("#DefaultLang").val(startingLanguage);
languageSelectorPrettyDropdown = initDropdown("#DefaultLang");
@ -113,11 +113,8 @@
</div>
</div>
</invoice>
<script type="text/javascript">
var availableLanguages = @Html.Raw(Json.Serialize(
langService
.GetLanguages()
.Select((language) => language.Code)));;
<script type="text/javascript">
var availableLanguages = @Html.Raw(Json.Serialize(langService.GetLanguages().Select((language) => language.Code)));;
var storeDefaultLang = "@Model.DefaultLang";
var fallbackLanguage = "en";
startingLanguage = computeStartingLanguage();
@ -126,14 +123,14 @@
.use(window.i18nextXHRBackend)
.init({
backend: {
loadPath: '/locales/{{lng}}.json'
loadPath: '@(Model.RootPath)locales/{{lng}}.json'
},
lng: startingLanguage,
fallbackLng: fallbackLanguage,
nsSeparator: false,
keySeparator: false
});
function computeStartingLanguage() {
if (urlParams.lang && isLanguageAvailable(urlParams.lang)) {
return urlParams.lang;
@ -150,11 +147,11 @@
i18next.changeLanguage(lang);
}
}
function isLanguageAvailable(languageCode) {
return availableLanguages.indexOf(languageCode) >= 0;
}
const i18n = new VueI18next(i18next);
// TODO: Move all logic from core.js to Vue controller
@ -169,7 +166,7 @@
i18n: i18n,
el: '#checkoutCtrl',
components: {
qrcode: VueQr,
qrcode: VueQrcode,
changelly: ChangellyComponent,
coinswitch: CoinSwitchComponent
},
@ -179,9 +176,10 @@
scanDisplayQr: "",
expiringSoon: false,
isModal: srvModel.isModal,
lightningAmountInSatoshi: srvModel.lightningAmountInSatoshi,
selectedThirdPartyProcessor: ""
}
});
</script>
</script>
</body>
</html>

View File

@ -23,6 +23,7 @@
<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>
<a href="https://docs.btcpayserver.org/features/paymentrequests" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
</div>
</div>

View File

@ -11,13 +11,13 @@
<html class="h-100">
<head>
<title>@Model.Title</title>
<meta charset="utf-8"/>
<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"/>
<link href="@Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet" />
@if (Model.CustomCSSLink != null)
{
<link href="@Model.CustomCSSLink" rel="stylesheet"/>
<link href="@Model.CustomCSSLink" rel="stylesheet" />
}
@if (!Context.Request.Query.ContainsKey("simple"))
{
@ -26,13 +26,15 @@
</script>
<bundle name="wwwroot/bundles/payment-request-bundle-1.min.js"></bundle>
<bundle name="wwwroot/bundles/payment-request-bundle-2.min.js"></bundle>
@*We need to make sure btcpay.js is not bundled, else it will not work if there is a RootPath*@
<script src="~/modal/btcpay.js"></script>
}
<bundle name="wwwroot/bundles/payment-request-bundle.min.css"></bundle>
@if (!string.IsNullOrEmpty(Model.EmbeddedCSS))
{
<style>
@Html.Raw(Model.EmbeddedCSS);
@Html.Raw(Model.EmbeddedCSS);
</style>
}
</head>

View File

@ -10,7 +10,7 @@
<html>
<head>
<title>@Model.CryptoCode LN Node Info</title>
<meta charset="utf-8"/>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="apple-touch-icon" href="~/img/icons/icon-512x512.png">
@ -18,10 +18,10 @@
<link rel="manifest" href="~/manifest.json">
<link href="@this.Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet"/>
<link href="~/vendor/font-awesome/css/font-awesome.min.css" 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"/>
<bundle name="wwwroot/bundles/lightning-node-info-bundle.min.js" />
<script type="text/javascript">
var srvModel = @Html.Raw(Json.Serialize(Model));
@ -31,7 +31,7 @@
var app = new Vue({
el: '#app',
components: {
qrcode: VueQr
qrcode: VueQrcode
},
data: {
srvModel: srvModel
@ -74,70 +74,76 @@
text-align: center;
}
.copy { cursor: copy; }
.qr-container svg {
width: 256px;
height: 256px;
}
.copy {
cursor: copy;
}
</style>
</head>
<body >
<noscript>
<div class="container">
<div class="row " style="height: 100vh">
<div class="col-md-8 col-sm-12 col-lg-6 mx-auto my-auto ">
<body>
<noscript>
<div class="container">
<div class="row " style="height: 100vh">
<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="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>
</div>
</noscript>
<div id="app" class="container">
<div class="row " style="height: 100vh">
<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">
{{srvModel.cryptoCode}} Lightning Node
- {{srvModel.available? "Online" : "Unavailable"}}
<small v-bind:class="{ 'text-success': srvModel.available, 'text-danger': !srvModel.available }">
<span class="fa fa-circle "></span>
</small>
</h1>
<div class="card-body m-sm-0 p-sm-0" v-if="srvModel.available">
<div class="qr-container mb-2">
<img v-bind:src="srvModel.cryptoImage" class="qr-icon"/>
<qrcode v-bind:val="srvModel.nodeInfo" v-bind:size="256" bg-color="#f5f5f7" fg-color="#000">
</qrcode>
</div>
<div class="input-group copy" data-clipboard-target="#vue-peer-info">
<input type="text" class=" form-control " readonly="readonly" :value="srvModel.nodeInfo" id="vue-peer-info"/>
<div class="input-group-append">
<span class="input-group-text fa fa-copy"> </span>
</noscript>
<div id="app" class="container">
<div class="row " style="height: 100vh">
<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">
{{srvModel.cryptoCode}} Lightning Node
- {{srvModel.available? "Online" : "Unavailable"}}
<small v-bind:class="{ 'text-success': srvModel.available, 'text-danger': !srvModel.available }">
<span class="fa fa-circle "></span>
</small>
</h1>
<div class="card-body m-sm-0 p-sm-0" v-if="srvModel.available">
<div class="qr-container mb-2">
<img v-bind:src="srvModel.cryptoImage" class="qr-icon" />
<qrcode v-bind:value="srvModel.nodeInfo" :options="{ width: 256, margin: 0, color: {dark:'#000', light:'#fff'} }" tag="svg">
</qrcode>
</div>
<div class="input-group copy" data-clipboard-target="#vue-peer-info">
<input type="text" class=" form-control " readonly="readonly" :value="srvModel.nodeInfo" id="vue-peer-info" />
<div class="input-group-append">
<span class="input-group-text fa fa-copy"> </span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -21,6 +21,55 @@
</p>
</div>
<div class="form-group">
<h5>Compatible wallets</h5>
</div>
@if (Model.Uri == null) // if GRPC
{
<div class="row">
<div class="col-lg-3 ml-auto text-center">
<a href="https://www.pebble.indiesquare.me/ target="_blank"">
<img src="~/img/pebblewallet.jpg" height="100" />
</a>
<p><a href="https://www.pebble.indiesquare.me/" target="_blank">Pebble</a></p>
</div>
<div class="col-lg-3 ml-auto text-center">
<a href="https://zap.jackmallers.com/" target="_blank">
<img src="~/img/zapwallet.jpg" height="100" />
</a>
<p><a href="https://zap.jackmallers.com/" target="_blank">Zap</a></p>
</div>
<div class="col-lg-3 mr-auto text-center">
</div>
<div class="col-lg-3 mr-auto text-center">
</div>
</div>
}
else
{
<div class="row">
<div class="col-lg-3 ml-auto text-center">
<a href="https://lightningjoule.com/" target="_blank">
<img src="~/img/joule.png" height="100" />
</a>
<p><a href="https://lightningjoule.com/" target="_blank">Joule</a></p>
</div>
<div class="col-lg-3 ml-auto text-center">
<a href="https://github.com/ZeusLN/zeus" target="_blank">
<img src="~/img/zeus.jpg" height="100" />
</a>
<p><a href="https://github.com/ZeusLN/zeus" target="_blank">Zeus</a></p>
</div>
<div class="col-lg-3 mr-auto text-center">
</div>
<div class="col-lg-3 mr-auto text-center">
</div>
</div>
}
<div class="form-group">
<h5>QR Code connection</h5>
<p>
@ -29,7 +78,7 @@
</p>
</div>
<div class="form-group">
@if(Model.QRCode == null)
@if (Model.QRCode == null)
{
<div class="form-group">
<form method="post">

View File

@ -21,6 +21,10 @@
<div class="form-group">
<label asp-for="LockSubscription"></label>
<input asp-for="LockSubscription" type="checkbox" class="form-check-inline" />
</div>
<div class="form-group">
<label asp-for="DiscourageSearchEngines"></label>
<input asp-for="DiscourageSearchEngines" type="checkbox" class="form-check-inline" />
</div>
<button type="submit" class="btn btn-primary" name="command" value="Save">Save</button>
</form>

View File

@ -48,35 +48,99 @@
@if (Model.OtherExternalServices.Count != 0)
{
<div class="row">
<div class="col-md-8">
<h4>Other services</h4>
<div class="form-group">
<span>Other external services</span>
</div>
<div class="form-group">
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Name</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var s in Model.OtherExternalServices)
{
<div class="row">
<div class="col-md-8">
<h4>Other services</h4>
<div class="form-group">
<span>Other external services</span>
</div>
<div class="form-group">
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<td>@s.Name</td>
<td style="text-align:right">
<a href="@s.Link" target="_blank">See information</a>
</td>
<th>Name</th>
<th style="text-align:right">Actions</th>
</tr>
}
</tbody>
</table>
</thead>
<tbody>
@foreach (var s in Model.OtherExternalServices)
{
<tr>
<td>@s.Name</td>
<td style="text-align:right">
<a href="@s.Link" target="_blank">See information</a>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
}
@if (Model.TorHttpServices.Count != 0)
{
<div class="row">
<div class="col-md-8">
<h4>HTTP TOR hidden services</h4>
<div class="form-group">
<span>TOR services hosted on this server, only http servers are listed here.</span>
</div>
<div class="form-group">
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Name</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var s in Model.TorHttpServices)
{
<tr>
<td>@s.Name</td>
<td style="text-align:right">
<a href="@s.Link" target="_blank">See information</a>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
}
@if (Model.TorOtherServices.Count != 0)
{
<div class="row">
<div class="col-md-8">
<h4>Other TOR hidden services</h4>
<div class="form-group">
<span>TOR services hosted on this server which are not http based.</span>
</div>
<div class="form-group">
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Name</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var s in Model.TorOtherServices)
{
<tr>
<td>@s.Name</td>
<td style="text-align:right">@s.Link</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
</div>
}
@section Scripts {

View File

@ -15,6 +15,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
@if (themeManager.DiscourageSearchEngines)
{
<META NAME="robots" CONTENT="noindex">
}
<title>BTCPay Server</title>
@ -51,6 +55,14 @@
<span class="badge badge-warning" style="font-size:10px;">@env.NetworkType.ToString()</span>
}
</a>
@if (env.OnionUrl != null)
{
<span>
<a href="@env.OnionUrl" target="_blank">
<img src="~/img/icons/onion.svg" width="26" height="32" />
</a>
</span>
}
<button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
@ -58,7 +70,7 @@
<ul class="navbar-nav ml-auto">
@if (SignInManager.IsSignedIn(User))
{
@if (User.IsInRole(Roles.ServerAdmin))
if (User.IsInRole(Roles.ServerAdmin))
{
<li class="nav-item"><a asp-area="" asp-controller="Server" asp-action="ListUsers" class="nav-link js-scroll-trigger">Server settings</a></li>
}
@ -75,7 +87,7 @@
</li>}
else
{
@if (themeManager.ShowRegister)
if (themeManager.ShowRegister)
{
<li class="nav-item"><a asp-area="" asp-controller="Account" asp-action="Register" class="nav-link js-scroll-trigger">Register</a></li>
}

View File

@ -42,6 +42,12 @@
<label asp-for="RequiresRefundEmail"></label>
<input asp-for="RequiresRefundEmail" type="checkbox" class="form-check" />
</div>
<div class="form-group">
<label asp-for="OnChainMinValue"></label>
<input asp-for="OnChainMinValue" class="form-control" />
<span asp-validation-for="OnChainMinValue" class="text-danger"></span>
<p class="form-text text-muted">Example: 5.50 USD</p>
</div>
<div class="form-group">
<label asp-for="LightningMaxValue"></label>
<input asp-for="LightningMaxValue" class="form-control" />
@ -49,11 +55,10 @@
<p class="form-text text-muted">Example: 5.50 USD</p>
</div>
<div class="form-group">
<label asp-for="OnChainMinValue"></label>
<input asp-for="OnChainMinValue" class="form-control" />
<span asp-validation-for="OnChainMinValue" class="text-danger"></span>
<p class="form-text text-muted">Example: 5.50 USD</p>
<label asp-for="LightningAmountInSatoshi"></label>
<input asp-for="LightningAmountInSatoshi" type="checkbox" class="form-check" />
</div>
<br />
<button name="command" type="submit" class="btn btn-primary" value="Save">Save</button>
</form>
</div>

View File

@ -15,14 +15,14 @@
<div class="row">
<div class="col-md-8">
<form method="post">
@if(Model.ShowScripting)
@if (Model.ShowScripting)
{
<div class="form-group">
<h5>Scripting</h5>
<span>Rate script allows you to express precisely how you want to calculate rates for currency pairs.</span>
<p class="text-muted">
<b>Supported exchanges are</b>:
@for(int i = 0; i < Model.AvailableExchanges.Length; i++)
@for (int i = 0; i < Model.AvailableExchanges.Length; i++)
{
<a href="@Model.AvailableExchanges[i].Url">@Model.AvailableExchanges[i].Name</a><span>@(i == Model.AvailableExchanges.Length - 1 ? "" : ",")</span>
}
@ -30,16 +30,16 @@
<p><a href="#help" data-toggle="collapse"><b>Click here for more information</b></a></p>
</div>
}
@if(Model.TestRateRules != null)
@if (Model.TestRateRules != null)
{
<div class="form-group">
<h5>Test results:</h5>
<table class="table table-sm table-responsive-md">
<tbody>
@foreach(var result in Model.TestRateRules)
@foreach (var result in Model.TestRateRules)
{
<tr>
@if(result.Error)
@if (result.Error)
{
<th class="small"><span class="fa fa-times" style="color:red;"></span> @result.CurrencyPair</th>
}
@ -54,7 +54,7 @@
</table>
</div>
}
@if(Model.ShowScripting)
@if (Model.ShowScripting)
{
<div id="help" class="collapse text-left">
<p>
@ -157,6 +157,14 @@
</div>
<span asp-validation-for="ScriptTest" class="text-danger"></span>
</div>
<div class="form-group">
<h5>Default currency pairs</h5>
<span>You can query those pairs via REST by querying <a asp-controller="Rate" asp-action="GetRates2" asp-route-storeId="@Model.StoreId" target="_blank">this link</a> without the need to specify currencyPairs.</span>
<div class="input-group">
<input placeholder="BTC_USD, BTC_CAD" asp-for="DefaultCurrencyPairs" class="form-control" />
</div>
<span asp-validation-for="DefaultCurrencyPairs" class="text-danger"></span>
</div>
<button name="command" type="submit" class="btn btn-primary" value="Save">Save</button>
<input type="hidden" asp-for="ShowScripting" />
</form>

View File

@ -44,7 +44,7 @@
"wwwroot/vendor/clipboard.js/clipboard.js",
"wwwroot/vendor/jquery/jquery.js",
"wwwroot/vendor/vuejs/vue.min.js",
"wwwroot/vendor/vuejs/vue-qrcode.js",
"wwwroot/vendor/vue-qrcode/vue-qrcode.js",
"wwwroot/vendor/i18next/i18next.js",
"wwwroot/vendor/i18next/i18nextXHRBackend.js",
"wwwroot/vendor/i18next/vue-i18next.js",
@ -60,7 +60,7 @@
"wwwroot/vendor/clipboard.js/clipboard.js",
"wwwroot/vendor/jquery/jquery.js",
"wwwroot/vendor/vuejs/vue.min.js",
"wwwroot/vendor/vuejs/vue-qrcode.js",
"wwwroot/vendor/vue-qrcode/vue-qrcode.js",
"wwwroot/vendor/vue-toasted/vue-toasted.min.js"
]
},
@ -83,7 +83,6 @@
"wwwroot/vendor/bootstrap-vue/bootstrap-vue.js",
"wwwroot/vendor/signalr/signalr.js",
"wwwroot/vendor/animejs/anime.min.js",
"wwwroot/modal/btcpay.js",
"wwwroot/crowdfund/**/*.js"
]
},
@ -151,7 +150,6 @@
"wwwroot/vendor/bootstrap-vue/bootstrap-vue.js",
"wwwroot/vendor/signalr/signalr.js",
"wwwroot/vendor/animejs/anime.min.js",
"wwwroot/modal/btcpay.js",
"wwwroot/payment-request/**/*.js"
]
},

View File

@ -46,7 +46,9 @@
setTimeout(waitForCoinSwitch, 1000);
return;
}
payment.open(config);
payment.on("Exchange:Ready", function(){
payment.open(config);
});
payment.on("Exchange:Complete", function () {
if(window.localStorage){
window.localStorage.removeItem(toCurrencyAddress);

View File

@ -9444,6 +9444,13 @@ strong {
.payment__scan {
position: relative;
text-align: center;
height: 256px;
margin-bottom: 5px;
}
.payment__scan svg {
width: 256px;
height: 256px;
}
.payment__scan__checkmark-wrapper {

View File

@ -90,10 +90,27 @@ function onDataCallback(jsonData) {
checkoutCtrl.lndModel = null;
}
// displaying satoshis for lightning payments
jsonData.cryptoCodeSrv = jsonData.cryptoCode;
if (jsonData.isLightning && checkoutCtrl.lightningAmountInSatoshi && jsonData.cryptoCode === "BTC") {
var SATOSHIME = 100000000;
jsonData.cryptoCode = "Sats";
jsonData.btcDue = numberFormatted(jsonData.btcDue * SATOSHIME);
jsonData.btcPaid = numberFormatted(jsonData.btcPaid * SATOSHIME);
jsonData.networkFee = numberFormatted(jsonData.networkFee * SATOSHIME);
jsonData.orderAmount = numberFormatted(jsonData.orderAmount * SATOSHIME);
}
// updating ui
checkoutCtrl.srvModel = jsonData;
}
function numberFormatted(x) {
var parts = x.toString().split(".");
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, " ");
return parts.join(".");
}
function fetchStatus() {
$.ajax({
url: window.location.pathname + "/status?invoiceId=" + srvModel.invoiceId + "&paymentMethodId=" + srvModel.paymentMethodId,

View File

@ -216,7 +216,6 @@ addLoadEvent(function (ev) {
this.sound = this.srvModel.soundsEnabled;
this.animation = this.srvModel.animationsEnabled;
eventAggregator.$on("invoice-created", function (invoiceId) {
btcpay.setApiUrlPrefix(window.location.origin);
btcpay.showInvoice(invoiceId);
btcpay.showFrame();

View File

@ -1,7 +1,7 @@
var hubListener = function(){
var connection = new signalR.HubConnectionBuilder().withUrl("/apps/hub").build();
var connection = new signalR.HubConnectionBuilder().withUrl(srvModel.hubPath).build();
connection.onclose(function(){
eventAggregator.$emit("connection-lost");

View File

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 228.46 342.85"><defs><style>.cls-1{fill:#fefefe;}.cls-2{fill:#111316;}.cls-3{fill:#868686;}.cls-4{fill:#b1b1b1;}.cls-5{fill:#454545;}.cls-6{fill:#909090;}.cls-7{fill:#7a7a7a;}.cls-8{fill:#9e9e9e;}.cls-9{fill:#555;}.cls-10{fill:#5f5f5f;}.cls-11{fill:#878787;}.cls-12{fill:#5e5e5e;}</style></defs><title>Artboard 1</title><path class="cls-1" d="M174.18,1.15c-12.79,15.54-25.44,31.17-35,49a.81.81,0,0,0-.13,1.16c.37.29.62-.11.91-.34,14.89-18.65,34.39-30.35,56.88-37.71C168.44,39.75,143.05,68.71,121.8,102l13.66,2.14c-3.72,20.24,6.33,32.87,22.43,42.15,13.7,7.89,27.63,15.55,38.79,27,26.69,27.29,36.54,59.88,26.11,96.68S186.55,329,149.45,337c-29.9,6.44-60.06,6.49-88.73-5.95C31.47,318.33,11,297,4.13,265.19-4,227.54,6.39,195.42,35.89,170.07c12.79-11,27-20.09,41.32-28.79,15.76-9.57,16.88-22.58,13.4-36.47C89,98.35,86.13,92.39,83.17,86.64c.6-1.64,1.94-2,3-2.93l18.37,12.14C108.18,69.64,117.17,46.14,130.1,24l-7.71,30.83c4.68-6.78,9-13.78,14.15-20.21a172,172,0,0,1,17-18.29A193.73,193.73,0,0,1,173.11.09C174.12-.2,174.4.23,174.18,1.15Z"/><path class="cls-2" d="M105.39,142.67c-.54,2.26-.13,4.81-2,6.65-1.33,4.1-2.7,8.19-4,12.3-2.94,9.35-8.78,16-17.44,20.79a159.8,159.8,0,0,0-26.84,19c-12.21,10.49-20.24,23.62-24.85,39-2.89,9.63-.69,18.88,2.31,27.89,6.38,19.22,17,36,30.72,50.82,3.3,3.57,7.49,6,12.45,9.21a56.79,56.79,0,0,1-15.69-6c-26.8-14.88-45.19-36.45-49.11-67.45-3.61-28.6,3.69-54.2,26.61-73.69,10.91-9.27,21.8-18.54,34.25-25.77a86.33,86.33,0,0,0,17.76-13.65c10.31-10.3,11.58-22.34,7.13-35.46-1.58-4.66-3.8-9.1-6.16-14.64l19.65,8.58c1.5,2,1.45,4.36,1.45,6.7q0,65.25,0,130.49c0,3.35.14,6.77-3.22,8.92-1.79,3.64-4.57,6.52-7.27,9.44-11,11.89-13.48,26.43-10.71,41.39,2.42,13.05,7.1,25.75,19.42,33.65-12.07-2.3-23.29-6-31.89-15.45-9-9.84-15.93-20.73-20-33.49a35.89,35.89,0,0,1,2.74-29.35,67.11,67.11,0,0,1,25.28-26.26c8.75-5.15,15.45-12,18.34-22,.32-2,.84-4,1.31-6-.1-.69-1,0-.33-.12.29-.05.41.07.33.35-.48,1.72-.7,3.56-2.17,4.83-4,10-10.86,17.15-20.4,22.28-14,7.5-25.18,18.15-28.63,34.09-4.9,22.61,2.2,42,19.08,57.72,6.08,5.67,13.15,9.93,20.62,13.62-8.59-1.5-17.33-3.21-23.57-9.53-17-17.27-30.29-36.88-36.6-60.76C31.33,251,32.67,242,36.44,233a78,78,0,0,1,25.86-33c7.61-5.64,15.25-11.32,23.32-16.25,8.86-5.42,13.72-13.26,16.29-22.9.94-3.52,1.6-7.12,2.39-10.69.38-2.54.46-5.14,1.22-6.84"/><path class="cls-2" d="M115.56,151.29c0,5.4,1.52,10.67,1.43,16.08.34,5.26,2.4,9.88,4.89,14.45,7.68,14.11,14.61,28.47,18.4,44.31,5.06,21.13,2.58,41.88-1.53,62.66-1.29,6.49-2.28,13.07-4.72,19.28-1.91,0-1.21-1.16-1-2.12a211.41,211.41,0,0,0,4.22-30.08c1-19-4.07-36.53-13.21-52.88-4.44-8-8.47-15.77-8.49-25.11-1.24,8.49-.75,16.8,4.4,24,6.83,9.59,10.07,20.46,12.23,31.77,2.86,15,.79,29.79-1.72,44.59-1.84,10.82-5.17,20.95-13.32,28.85-.88.19-1.35,0-1-1,4-5.93,5.09-12.71,6.18-19.64,3.21-20.33.45-39.93-6.28-59.15-1.41-1.95-1.74-4.33-2.56-6.53a20.81,20.81,0,0,1,1.51,8.57c2.8,13.37,5,26.75,4.32,40.54-.54,11.72-2.26,23.12-6.2,34.17-3.21-1.21-2.25-4.06-2.26-6.31-.1-21-.31-42,0-62.94.72-49.27.15-98.55.39-147.82,0-2-.49-4,1-5.73a16.56,16.56,0,0,1,6.28,2.7c.81,5.24-.18,10.37-.34,15.5-.38,11.8,1.83,22.91,8.79,32.6,23.31,32.48,25.59,69.38,22.73,107.35a270.61,270.61,0,0,1-5.5,36.45,43.13,43.13,0,0,1-7,16.32.78.78,0,0,1-1.28,0c6-19.91,11.48-39.82,10.66-61-.77-20.13-5.41-38.94-14.9-56.75-4.51-8.45-10.14-16.38-12.92-25.71-.33-1.11-.52-2.26-.77-3.4-2.34-4.74-1.76-10-2.63-15-.92-4.13-.55-8.37-.89-12.55"/><path class="cls-2" d="M109.16,325.19c-15-15.28-24.62-43.24-7.89-65.34,3.17-4.18,6.84-8.2,7.79-13.7,2.54.21,2.07,2.24,2.07,3.74q.11,35.73,0,71.46C111.13,322.85,111.64,324.86,109.16,325.19Z"/><path class="cls-3" d="M109.16,325.19c1.44-.63,1-1.94,1-3q0-36.53,0-73.06c0-1.07.42-2.41-1.1-3l-.33.17-.36,0a24.68,24.68,0,0,0,1.86-10.15q-.12-68-.08-136l2.1,1q0,109.61,0,219.21c0,1.21-.67,2.7.86,3.59C112.42,326.62,111,326.7,109.16,325.19Z"/><path class="cls-4" d="M117,167.37c-1.63-5.25-1.87-10.68-2-16.12-.29-1,.11-1.27,1-1l1.92,15Z"/><path class="cls-5" d="M133,306c.39.68,0,1.73,1,2.12-1.4,4-2.43,8.17-5.91,11l.09.08Q130.61,312.56,133,306Z"/><path class="cls-6" d="M115,249.33c-1.36-3-1.61-6.19-1.77-10.22a20,20,0,0,1,2.82,8.18Z"/><path class="cls-7" d="M103.36,149.32c.16-2.64,1.35-4.95,2.66-8.31-.2,3.83-.25,6.64-1.72,9.21Z"/><path class="cls-8" d="M103.41,203.3l2.5-7.07a10.65,10.65,0,0,1-1.64,8Z"/><path class="cls-9" d="M135.91,312.14l1.28,0a7.25,7.25,0,0,1-2.71,3.84A4.33,4.33,0,0,1,135.91,312.14Z"/><path class="cls-10" d="M116.11,326.08l1,1a3.76,3.76,0,0,1-3,2.28C113.9,327.66,115.06,326.9,116.11,326.08Z"/><path class="cls-11" d="M127.18,321.13c.35,1.34-.49,2.07-1.67,2.95,0-1.49.12-2.6,1.7-2.92Z"/><path class="cls-5" d="M127.21,321.16c0-.84-.12-1.71,1-2l-.09-.08c.29,1,.23,1.76-.94,2Z"/><path class="cls-12" d="M116.06,150.27l-1,1c-1.91-4.67-.52-9.42-.2-14.16Q115.44,143.67,116.06,150.27Z"/></svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -46,5 +46,5 @@
"txCount_plural": "{{count}} transakcji",
"Pay with CoinSwitch": "Zapłać z CoinSwitch",
"Pay with Changelly": "Zapłać z Changelly",
"Close": "Close"
"Close": "Zamknij"
}

View File

@ -1,10 +1,10 @@
{
"NOTICE_WARN": "THIS CODE HAS BEEN AUTOMATICALLY GENERATED FROM TRANSIFEX, IF YOU WISH TO HELP TRANSLATION COME ON THE SLACK http://slack.btcpayserver.org TO REQUEST PERMISSION TO https://www.transifex.com/btcpayserver/btcpayserver/",
"code": "ru-RU",
"currentLanguage": "русский",
"lang": "язык",
"currentLanguage": "Русский",
"lang": "Язык",
"Awaiting Payment...": "Ожидание оплаты...",
"Pay with": "заплатить",
"Pay with": "Способ оплаты",
"Contact and Refund Email": "Контактный адрес электронной почты",
"Contact_Body": "Укажите ниже адрес электронной почты. Мы свяжемся с вами по этому адресу, если у вас возникла проблема с оплатой.",
"Your email": "Ваш адрес электронной почты",
@ -13,8 +13,8 @@
"Order Amount": "Сумма заказа",
"Network Cost": "Ценность сети",
"Already Paid": "Уже оплачено",
"Due": "является обязательной",
"Scan": "просканировать",
"Due": "К оплате",
"Scan": "Просканировать",
"Copy": "Скопировать",
"Conversion": "Конвертация",
"Open in wallet": "Открыть кошелек",
@ -22,29 +22,29 @@
"Amount": "Сумма",
"Address": "Адрес",
"Copied": "Скопировано",
"ConversionTab_BodyTop": "Вы можете заплатить {{btcDue}} {{cryptoCode}} используя Altcoins отличные от предпочитаемых продавцом.",
"ConversionTab_BodyDesc": "Эта услуга предоставляется третьим лицом. Пожалуйста, имейте в виду, что мы не контролируем, каким образом провайдеры переедут ваши фондовые средства. Счет будет закрыт только после {{cryptoCode}} Blockchain получения средств.",
"ConversionTab_CalculateAmount_Error": "Retry",
"ConversionTab_LoadCurrencies_Error": "Retry",
"ConversionTab_BodyTop": "Вы можете заплатить {{btcDue}} {{cryptoCode}}, используя альткойны, отличные от тех, которые поддерживает продавец.",
"ConversionTab_BodyDesc": "Эта услуга предоставляется третьей стороной. Пожалуйста, имейте в виду, что мы не можем контролировать, как провайдеры будут направлять ваши средства. Счет будет помечен как оплаченный только после получения средств в {{cryptoCode}} блокчейне.",
"ConversionTab_CalculateAmount_Error": "Повторить попытку",
"ConversionTab_LoadCurrencies_Error": "Повторить попытку",
"ConversionTab_Lightning": "Возможность конвертации Lightning Network платежей отсутствует.",
"ConversionTab_CurrencyList_Select_Option": "Please select a currency to convert from",
"ConversionTab_CurrencyList_Select_Option": "Пожалуйста, выберите валюту для конвертации",
"Invoice expiring soon...": "Время выставленного счета вскоре истечёт...",
"Invoice expired": "Срок действия выставленного счета истек",
"What happened?": "Что случилось?",
"InvoiceExpired_Body_1": "Срок действия этого счета истек. Счет действителен только {{maxTimeMinutes}} минут. \nвы можете посетить {{storeName}} опять, если захотите оплатить.",
"InvoiceExpired_Body_2": "ваша оплата пока еще не принята системой. Мы еще не получили ваши фонды.",
"InvoiceExpired_Body_3": "",
"InvoiceExpired_Body_2": "Если вы пытались отправить платеж, он еще не был принят сетью. Мы еще не получили ваши средства.",
"InvoiceExpired_Body_3": "Если мы получим его позднее, мы либо обработаем ваш заказ, либо свяжемся с вами, чтобы договориться о возврате средств...",
"Invoice ID": "Порядковый номер выставленного счета",
"Order ID": "Порядковый номер заказа",
"Return to StoreName": "возвратить в {{storeName}}",
"Return to StoreName": "Вернуться в {{storeName}}",
"This invoice has been paid": "Этот выставленный счет был оплачен",
"This invoice has been archived": "Этот счет был заархивирован.",
"Archived_Body": "ожалуйста, обратитесь в магазин для получения информации о заказе или помощи",
"BOLT 11 Invoice": "счет BOLT 11",
"This invoice has been archived": "Этот счет был заархивирован",
"Archived_Body": "Пожалуйста, обратитесь в магазин для получения информации о заказе или помощи",
"BOLT 11 Invoice": "Счет BOLT 11",
"Node Info": "Информация об устройстве",
"txCount": "{{count}} Сделка",
"txCount_plural": "{{count}} операции",
"Pay with CoinSwitch": "Pay with CoinSwitch",
"Pay with Changelly": "Pay with Changelly",
"Close": "Close"
"txCount": "{{count}} транзакция",
"txCount_plural": "{{count}} транзакций",
"Pay with CoinSwitch": "Оплатить с помощью CoinSwitch",
"Pay with Changelly": "Оплатить с помощью Changelly",
"Close": "Закрыть"
}

View File

@ -0,0 +1,50 @@
{
"NOTICE_WARN": "THIS CODE HAS BEEN AUTOMATICALLY GENERATED FROM TRANSIFEX, IF YOU WISH TO HELP TRANSLATION COME ON THE SLACK http://slack.btcpayserver.org TO REQUEST PERMISSION TO https://www.transifex.com/btcpayserver/btcpayserver/",
"code": "sv",
"currentLanguage": "Svenska",
"lang": "Språk",
"Awaiting Payment...": "Inväntar betalning...",
"Pay with": "Betala med",
"Contact and Refund Email": "E-postadress för kontakt och återbetalning",
"Contact_Body": "Vänligen uppge en e-postadress. Vi kontaktar dig på denna adress ifall det uppstår problem med din betalning.",
"Your email": "Din e-postadress",
"Continue": "Fortsätt",
"Please enter a valid email address": "Vänligen uppge en korrekt e-postadress",
"Order Amount": "Orderbelopp",
"Network Cost": "Nätverkskostnad",
"Already Paid": "Redan betald",
"Due": "Att betala",
"Scan": "Skanna",
"Copy": "Kopiera",
"Conversion": "Konvertering",
"Open in wallet": "Öppna i wallet",
"CompletePay_Body": "Vänligen skicka {{btcDue}} {{cryptoCode}} till adressen nedan för att fullfölja betalningen.",
"Amount": "Belopp",
"Address": "Adress",
"Copied": "Kopierat",
"ConversionTab_BodyTop": "Du kan betala {{btcDue}} {{cryptoCode}} med andra altcoins utöver de som handlaren stödjer.",
"ConversionTab_BodyDesc": "Denna tjänst tillhandahålls av tredje part. Vänligen kom ihåg att vi saknar kontroll över hur tjänstleverantörer behandlar din betalning. Fakturan markeras som betald först när betalningen har mottagits på {{cryptoCode}} Blockchain.",
"ConversionTab_CalculateAmount_Error": "Försök igen",
"ConversionTab_LoadCurrencies_Error": "Försök igen",
"ConversionTab_Lightning": "Inga tillgängliga tjänstleverantörer för konvertering av betalningar på Lightning Network.",
"ConversionTab_CurrencyList_Select_Option": "Vänligen välj valuta att konvertera från",
"Invoice expiring soon...": "Fakturan utgår snart...",
"Invoice expired": "Fakturan utgången",
"What happened?": "Vad hände?",
"InvoiceExpired_Body_1": "Denna faktura har utgått. En faktura är endast giltig i {{maxTimeMinutes}} minuter.\nDu kan återgå till {{storeName}} ifall du önskar skicka din betalning igen.",
"InvoiceExpired_Body_2": "Betalningsförsöket har ännu inte accepterats av nätverket. Vi har inte mottagit din betalning än.",
"InvoiceExpired_Body_3": "Om betalningen fördröjs och mottas vid ett senare tillfälle kommer orderprocessen att fortsätta behandlas annars kontaktar vi dig för att genomföra en återbetalning...",
"Invoice ID": "Fakturanummer",
"Order ID": "Ordernummer",
"Return to StoreName": "Återgå till {{storeName}}",
"This invoice has been paid": "Denna faktura har betalats",
"This invoice has been archived": "Denna faktura har arkiverats",
"Archived_Body": "Vänligen kontakta handlaren för orderinformation eller hjälp",
"BOLT 11 Invoice": "BOLT 11 Faktura",
"Node Info": "Nodinformation",
"txCount": "{{count}} transaktion",
"txCount_plural": "{{count}} transaktioner",
"Pay with CoinSwitch": "Betala med CoinSwitch",
"Pay with Changelly": "Betala med Changelly",
"Close": "Stäng"
}

View File

@ -1,5 +1,6 @@
/* jshint browser: true, strict: false, maxlen: false, maxstatements: false */
(function () {
var showingInvoice = false;
var supportsCurrentScript = ("currentScript" in document);
var thisScript = "";
if (supportsCurrentScript) {
@ -65,6 +66,7 @@
function hideFrame() {
onModalWillLeaveMethod();
iframe.style.display = 'none';
showingInvoice = false;
iframe = window.document.body.removeChild(iframe);
}
@ -79,7 +81,7 @@
function receiveMessage(event) {
var uri;
if (origin !== event.origin) {
if (!origin.startsWith(event.origin) || !showingInvoice) {
return;
}
if (event.data === 'close') {
@ -100,6 +102,7 @@
}
function showInvoice(invoiceId, params) {
showingInvoice = true;
window.document.body.appendChild(iframe);
var invoiceUrl = origin + '/invoice?id=' + invoiceId + '&view=modal';
if (params && params.animateEntrance === false) {

View File

@ -112,7 +112,6 @@ addLoadEvent(function (ev) {
var self = this;
eventAggregator.$on("invoice-created", function (invoiceId) {
self.setLoading(false);
btcpay.setApiUrlPrefix(window.location.origin);
btcpay.showInvoice(invoiceId);
btcpay.showFrame();
});

View File

@ -1,6 +1,6 @@
var hubListener = function () {
var connection = new signalR.HubConnectionBuilder().withUrl("/payment-requests/hub").build();
var connection = new signalR.HubConnectionBuilder().withUrl(srvModel.hubPath).build();
connection.onclose(function () {
eventAggregator.$emit("connection-lost");

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long