Compare commits

...

43 Commits

Author SHA1 Message Date
91c63a8ee6 bump 2019-03-24 13:37:14 +09:00
563882d30b Merge pull request #711 from rockstardev/bugfix/satround
Javascript floating point math fix, closes #701
2019-03-24 13:35:13 +09:00
9a5eeee794 Javascript floating point math fix, closes #701 2019-03-23 22:28:54 -05:00
0578a692db Updating bundler to new version that will also support .NET Core 2.2 2019-03-23 19:10:16 -05:00
f74f06338a Update bundle minifier, trying to fix (#710 with bundle on) 2019-03-23 23:24:29 +09:00
2a302ea346 Do not spam logs if we can't connect to lightning because lightning is not started 2019-03-20 14:35:33 +09:00
be90172840 bump Microsoft.AspNetCore.App 2019-03-20 14:13:12 +09:00
b85ee895f5 Update nbxplorer in the tests 2019-03-20 14:08:25 +09:00
93de408e07 bump 2019-03-20 12:37:53 +09:00
d3662ae734 fix dockerfile 2019-03-20 12:34:47 +09:00
132d7795ea bump .net 2019-03-20 12:22:46 +09:00
bf5a624209 Remove curl dependency on alpine image 2019-03-20 11:49:43 +09:00
abbdbda03a Add Bitbank provider 2019-03-20 00:49:44 +09:00
e203cada54 Merge pull request #675 from Kukks/css-grid-pos-fix
attempt fix pos css grid
2019-03-18 19:47:11 +09:00
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
5c618233cb fix singular item in pos stretched width 2019-03-18 07:39:45 +01:00
9e91259b9e attempt fix pos css grid 2019-03-18 07:07:31 +01: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
67 changed files with 1360 additions and 361 deletions

View File

@ -1,12 +1,9 @@
FROM microsoft/dotnet:2.1.500-sdk-alpine3.7 AS builder
FROM mcr.microsoft.com/dotnet/core/sdk:2.1.505-alpine3.7 AS builder
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT false
RUN apk add --no-cache icu-libs
ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8
# This should be removed soon https://github.com/dotnet/corefx/issues/30003
RUN apk add --no-cache curl
WORKDIR /source
COPY BTCPayServer/BTCPayServer.csproj BTCPayServer/BTCPayServer.csproj
COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj

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);
@ -2251,15 +2327,22 @@ donation:
Assert.NotNull(exchangeRates);
Assert.NotEmpty(exchangeRates);
Assert.NotEmpty(exchangeRates.ByExchange[result.ExpectedName]);
// This check if the currency pair is using right currency pair
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
if (result.ExpectedName == "bitbank")
{
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
e => e.CurrencyPair == new CurrencyPair("BTC", "JPY") && e.BidAsk.Bid > 100m); // 1BTC will always be more than 100JPY
}
else
{
// This check if the currency pair is using right currency pair
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
e => (e.CurrencyPair == new CurrencyPair("BTC", "USD") ||
e.CurrencyPair == new CurrencyPair("BTC", "EUR") ||
e.CurrencyPair == new CurrencyPair("BTC", "USDT") ||
e.CurrencyPair == new CurrencyPair("BTC", "CAD"))
&& e.BidAsk.Bid > 1.0m // 1BTC will always be more than 1USD
);
}
}
// Kraken emit one request only after first GetRates
factory.Providers["kraken"].GetRatesAsync(default).GetAwaiter().GetResult();
@ -2352,7 +2435,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

@ -69,7 +69,7 @@ services:
nbxplorer:
image: nicolasdorier/nbxplorer:2.0.0.15
image: nicolasdorier/nbxplorer:2.0.0.21
restart: unless-stopped
ports:
- "32838:32838"

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.3.82</Version>
<Version>1.0.3.89</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<PropertyGroup>
@ -30,15 +30,17 @@
<None Remove="Currencies.txt" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="bundleconfig.json" />
<EmbeddedResource Include="Currencies.txt" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.11" />
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.14" />
<PackageReference Include="BuildBundlerMinifier" Version="2.9.406" />
<PackageReference Include="BundlerMinifier.Core" Version="2.9.406" />
<PackageReference Include="BundlerMinifier.TagHelpers" Version="2.9.406" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.5.3" />
<PackageReference Include="HtmlSanitizer" Version="4.0.199" />
<PackageReference Include="LedgerWallet" Version="2.0.0.3" />
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.2">
@ -67,7 +69,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.6" />
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.9" />
<PackageReference Include="YamlDotNet" Version="5.2.1" />
</ItemGroup>

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

@ -40,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

@ -284,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(),

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"
});
}
@ -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(
@ -315,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

@ -68,7 +68,7 @@ namespace BTCPayServer.HostedServices
var exchanges = new CoinAverageExchanges();
foreach (var item in (await new CoinAverageRateProvider() { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync())
.Exchanges
.Select(c => new CoinAverageExchange(c.Name, c.DisplayName)))
.Select(c => new CoinAverageExchange(c.Name, c.DisplayName, $"https://apiv2.bitcoinaverage.com/exchanges/{c.Name}")))
{
exchanges.Add(item);
}

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

@ -36,7 +36,6 @@ using BTCPayServer.Authentication;
using Microsoft.Extensions.Caching.Memory;
using BTCPayServer.Logging;
using BTCPayServer.HostedServices;
using Meziantou.AspNetCore.BundleTagHelpers;
using System.Security.Claims;
using BTCPayServer.PaymentRequest;
using BTCPayServer.Payments.Changelly;
@ -48,6 +47,7 @@ using NBXplorer.DerivationStrategy;
using NicolasDorier.RateLimits;
using Npgsql;
using BTCPayServer.Services.Apps;
using BundlerMinifier.TagHelpers;
namespace BTCPayServer.Hosting
{
@ -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>();
@ -215,7 +218,7 @@ namespace BTCPayServer.Hosting
services.AddAuthorization(o => Policies.AddBTCPayPolicies(o));
BitpayAuthentication.AddAuthentication(services);
services.AddBundles();
services.AddSingleton<IBundleProvider, ResourceBundleProvider>();
services.AddTransient<BundleOptions>(provider =>
{
var opts = provider.GetRequiredService<BTCPayServerOptions>();

View File

@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using BundlerMinifier.TagHelpers;
using Microsoft.AspNetCore.Hosting;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Hosting
{
public class ResourceBundleProvider : IBundleProvider
{
BundleProvider _InnerProvider;
Lazy<Dictionary<string, Bundle>> _BundlesByName;
public ResourceBundleProvider(IHostingEnvironment hosting, BundleOptions options)
{
if (options.UseBundles)
{
_BundlesByName = new Lazy<Dictionary<string, Bundle>>(() =>
{
using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("BTCPayServer.bundleconfig.json"))
using (var reader = new StreamReader(stream, Encoding.UTF8))
{
var content = reader.ReadToEnd();
return JArray.Parse(content).OfType<JObject>()
.Select(jobj => new Bundle()
{
Name = jobj.Property("name")?.Value.Value<string>() ?? jobj.Property("outputFileName").Value.Value<string>(),
OutputFileUrl = Path.Combine(hosting.ContentRootPath, jobj.Property("outputFileName").Value.Value<string>())
}).ToDictionary(o => o.Name, o => o);
}
}, true);
}
else
{
_InnerProvider = new BundleProvider();
}
}
public Bundle GetBundle(string name)
{
if (_InnerProvider != null)
return _InnerProvider.GetBundle(name);
_BundlesByName.Value.TryGetValue(name, out var bundle);
return bundle;
}
}
}

View File

@ -34,7 +34,6 @@ using Microsoft.AspNetCore.Mvc.Cors.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using System.Net;
using BTCPayServer.PaymentRequest;
using Meziantou.AspNetCore.BundleTagHelpers;
using BTCPayServer.Security;
using BTCPayServer.Services.Apps;

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

@ -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; }

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

@ -21,14 +21,16 @@ namespace BTCPayServer.Models.StoreViewModels
{
public string Name { get; set; }
public string Value { get; set; }
public string Url { get; set; }
}
public void SetExchangeRates(CoinAverageExchange[] supportedList, string preferredExchange)
{
var defaultStore = preferredExchange ?? CoinAverageRateProvider.CoinAverageName;
var choices = supportedList.Select(o => new Format() { Name = o.Display, Value = o.Name }).ToArray();
var choices = supportedList.Select(o => new Format() { Name = o.Display, Value = o.Name, Url = o.Url }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Value == defaultStore) ?? choices.FirstOrDefault();
Exchanges = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
PreferredExchange = chosen.Value;
RateSource = chosen.Url;
}
public List<TestResultViewModel> TestRateRules { get; set; }
@ -42,6 +44,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 ... %")]
@ -57,10 +61,8 @@ namespace BTCPayServer.Models.StoreViewModels
public string RateSource
{
get
{
return PreferredExchange == CoinAverageRateProvider.CoinAverageName ? "https://apiv2.bitcoinaverage.com/indices/global/ticker/short" : $"https://apiv2.bitcoinaverage.com/exchanges/{PreferredExchange}";
}
get;
set;
}
}
}

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

@ -100,13 +100,25 @@ namespace BTCPayServer.Payments.Lightning
{
var charge = lightningSupportedMethod.CreateClient(network);
LightningInvoice chargeInvoice = null;
string errorMessage = $"{lightningSupportedMethod.CryptoCode} (Lightning): Can't connect to the lightning server";
try
{
chargeInvoice = await charge.GetInvoice(lightningMethod.InvoiceId);
}
catch (System.Net.Sockets.SocketException socketEx) when (socketEx.SocketErrorCode == System.Net.Sockets.SocketError.ConnectionRefused)
{
Logs.PayServer.LogError(errorMessage);
continue;
}
catch (Exception ex) when (ex.InnerException is System.Net.Sockets.SocketException socketEx
&& socketEx.SocketErrorCode == System.Net.Sockets.SocketError.ConnectionRefused)
{
Logs.PayServer.LogError(errorMessage);
continue;
}
catch (Exception ex)
{
Logs.PayServer.LogError(ex, $"{lightningSupportedMethod.CryptoCode} (Lightning): Can't connect to the lightning server");
Logs.PayServer.LogError(ex, errorMessage);
continue;
}
if (chargeInvoice == null)
@ -192,7 +204,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

@ -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

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Rates
{
public class BitbankRateProvider : IRateProvider, IHasExchangeName
{
private readonly HttpClient _httpClient;
public BitbankRateProvider(HttpClient httpClient)
{
_httpClient = httpClient ?? new HttpClient();
}
public string ExchangeName => "bitbank";
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
{
var response = await _httpClient.GetAsync("https://public.bitbank.cc/prices", cancellationToken);
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
return new ExchangeRates(((jobj["data"] as JObject) ?? new JObject())
.Properties()
.Select(p => new ExchangeRate(ExchangeName, CurrencyPair.Parse(p.Name), new BidAsk(p.Value["buy"].Value<decimal>(), p.Value["sell"].Value<decimal>())))
.ToArray());
}
}
}

View File

@ -23,20 +23,18 @@ namespace BTCPayServer.Services.Rates
public class CoinAverageExchange
{
public CoinAverageExchange(string name, string display)
public CoinAverageExchange(string name, string display, string url)
{
Name = name;
Display = display;
Url = url;
}
public string Name { get; set; }
public string Display { get; set; }
public string Url
{
get
{
return Name == CoinAverageRateProvider.CoinAverageName ? $"https://apiv2.bitcoinaverage.com/indices/global/ticker/short"
: $"https://apiv2.bitcoinaverage.com/exchanges/{Name}";
}
get;
set;
}
}
public class CoinAverageExchanges : Dictionary<string, CoinAverageExchange>
@ -47,7 +45,11 @@ namespace BTCPayServer.Services.Rates
public void Add(CoinAverageExchange exchange)
{
TryAdd(exchange.Name, exchange);
if (!TryAdd(exchange.Name, exchange))
{
this.Remove(exchange.Name);
this.Add(exchange.Name, exchange);
}
}
}
public class CoinAverageSettings : ICoinAverageAuthenticator
@ -84,7 +86,6 @@ namespace BTCPayServer.Services.Rates
(DisplayName: "Coincheck", Name: "coincheck"),
(DisplayName: "Bittylicious", Name: "bittylicious"),
(DisplayName: "Gemini", Name: "gemini"),
(DisplayName: "QuadrigaCX", Name: "quadrigacx"),
(DisplayName: "Bit2C", Name: "bit2c"),
(DisplayName: "Luno", Name: "luno"),
(DisplayName: "Negocie Coins", Name: "negociecoins"),
@ -122,7 +123,7 @@ namespace BTCPayServer.Services.Rates
(DisplayName: "Bitso", Name: "bitso"),
})
{
AvailableExchanges.TryAdd(item.Name, new CoinAverageExchange(item.Name, item.DisplayName));
AvailableExchanges.TryAdd(item.Name, new CoinAverageExchange(item.Name, item.DisplayName, $"https://apiv2.bitcoinaverage.com/exchanges/{item.Name}"));
}
}

View File

@ -113,6 +113,7 @@ namespace BTCPayServer.Services.Rates
Providers.Add(CoinAverageRateProvider.CoinAverageName, new CoinAverageRateProvider() { Exchange = CoinAverageRateProvider.CoinAverageName, HttpClient = _httpClientFactory?.CreateClient("EXCHANGE_COINAVERAGE"), Authenticator = _CoinAverageSettings });
Providers.Add("kraken", new KrakenExchangeRateProvider() { HttpClient = _httpClientFactory?.CreateClient("EXCHANGE_KRAKEN") });
Providers.Add("bylls", new ByllsRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BYLLS")));
Providers.Add("bitbank", new BitbankRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BITBANK")));
// Those exchanges make multiple requests when calling GetTickers so we remove them
//DirectProviders.Add("gdax", new ExchangeSharpRateProvider("gdax", new ExchangeGdaxAPI()));
@ -168,9 +169,9 @@ namespace BTCPayServer.Services.Rates
}
// Add other exchanges supported here
exchanges.Add(new CoinAverageExchange(CoinAverageRateProvider.CoinAverageName, "Coin Average"));
exchanges.Add(new CoinAverageExchange("bylls", "Bylls"));
exchanges.Add(new CoinAverageExchange("cryptopia", "Cryptopia"));
exchanges.Add(new CoinAverageExchange(CoinAverageRateProvider.CoinAverageName, "Coin Average", $"https://apiv2.bitcoinaverage.com/indices/global/ticker/short"));
exchanges.Add(new CoinAverageExchange("bylls", "Bylls", "https://bylls.com/api/price?from_currency=BTC&to_currency=CAD"));
exchanges.Add(new CoinAverageExchange("bitbank", "Bitbank", "https://public.bitbank.cc/prices"));
return exchanges;
}

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

@ -1,4 +1,4 @@
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
@addTagHelper *, BundlerMinifier.TagHelpers
@model UpdateCrowdfundViewModel
@{
ViewData["Title"] = "Update Crowdfund";

View File

@ -1,4 +1,4 @@
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
@addTagHelper *, BundlerMinifier.TagHelpers
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
@model BTCPayServer.Models.AppViewModels.ViewCrowdfundViewModel
@{
@ -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

@ -1,4 +1,4 @@
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
@addTagHelper *, BundlerMinifier.TagHelpers
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
@model BTCPayServer.Models.AppViewModels.ViewPointOfSaleViewModel
@ -41,6 +41,10 @@
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
grid-gap: .5rem;
}
.card-deck .card:only-of-type {
max-width: 320px;
margin: auto;
}
</style>
</head>
@ -239,42 +243,29 @@
</div>
<div id="js-pos-list" class="text-center mx-auto px-4">
<div class="row">
@{
var rowsJ = Model.Items.Length/4 + (Model.Items.Length % 4 > 0 ? 1 : 0);
}
@for (int x = 0; x < rowsJ; x++)
{
var startingIndex = x == 0 ? 0 : x * 4;
var endLoopIndex = startingIndex + Math.Min(4, Model.Items.Length - startingIndex);
<div class="row card-deck my-3 mx-auto">
@for (int i = startingIndex; i < endLoopIndex; i++)
{
var item = Model.Items[i];
var image = item.Image;
var description = item.Description;
<div class="row card-deck my-3 mx-auto">
<div class="js-add-cart card" data-id="@i">
@if (!String.IsNullOrWhiteSpace(image))
{
<img class="card-img-top" src="@image" alt="Card image cap">
}
<div class="card-body p-3">
<h6 class="card-title mb-0">@item.Title</h6>
@if (!String.IsNullOrWhiteSpace(description))
{
<p class="card-text">@description</p>
}
<span class="text-muted small">@String.Format(Model.ButtonText, @item.Price.Formatted)</span>
</div>
</div>
}
</div>
}
@for (var index = 0; index < Model.Items.Length; index++)
{
var item = Model.Items[index];
var image = item.Image;
var description = item.Description;
<div class="js-add-cart card my-2" data-id="@index">
@if (!String.IsNullOrWhiteSpace(image))
{
@:<img class="card-img-top" src="@image" alt="Card image cap">
}
<div class="card-body p-3">
<h6 class="card-title mb-0">@item.Title</h6>
@if (!String.IsNullOrWhiteSpace(description))
{
<p class="card-text">@description</p>
}
<span class="text-muted small">@String.Format(Model.ButtonText, @item.Price.Formatted)</span>
</div>
</div>
}
</div>
</div>
</div>
@ -314,82 +305,68 @@
<div class="container d-flex h-100">
<div class="justify-content-center align-self-center text-center mx-auto px-2 py-3 w-100" style="margin: auto;">
<h1 class="mb-4">@Model.Title</h1>
@{
var rows = Model.Items.Length/4 + (Model.Items.Length % 4 > 0 ? 1 : 0);
}
@for (int x = 0; x < rows; x++)
{
var startingIndex = x == 0 ? 0 : x * 4;
var endLoopIndex = startingIndex + Math.Min(4, Model.Items.Length - startingIndex);
<div class="row card-deck my-3 mx-auto">
@for (int i = startingIndex; i < endLoopIndex; i++)
<div class="row card-deck my-3 mx-auto">
@for (int x = 0; x < Model.Items.Length; x++)
{
var item = Model.Items[i];
var image = item.Image;
var description = item.Description;
<div class="card" data-id="@i">
@if (!String.IsNullOrWhiteSpace(image))
var item = Model.Items[x];
<div class="card my-2" data-id="@x">
@if (!String.IsNullOrWhiteSpace(item.Image))
{
<img class="card-img-top" src="@item.Image" alt="Card image cap">
}
<div class="card-body">
<h5 class="card-title">@item.Title</h5>
@if (!String.IsNullOrWhiteSpace(item.Description))
{
<img class="card-img-top" src="@image" alt="Card image cap">
<p class="card-text">@item.Description</p>
}
<div class="card-body">
<h5 class="card-title">@item.Title</h5>
@if (!String.IsNullOrWhiteSpace(description))
{
<p class="card-text">@description</p>
}
@if (item.Custom && !Model.EnableShoppingCart)
{
<form method="post" asp-antiforgery="false" data-buy>
<input type="hidden" name="choicekey" value="@item.Id" />
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">@Model.CurrencySymbol</span>
</div>
<input class="form-control" type="number" min="@item.Price.Value" step="@Model.Step" name="amount"
value="@item.Price.Value" placeholder="Amount">
<div class="input-group-append">
<button class="btn btn-primary" type="submit">@Model.CustomButtonText</button>
</div>
</div>
</form>
}
else
{
<form method="post" asp-antiforgery="false">
<button type="submit" name="choiceKey" class="js-add-cart btn btn-primary" value="@item.Id">
@String.Format(Model.ButtonText, @item.Price.Formatted)</button>
</form>
}
</div>
</div>
}
</div>
}
@if (Model.ShowCustomAmount)
{
<div class="row mt-2 mb-4">
<div class="col-lg-4 offset-lg-4 col-md-6 offset-md-3 px-2">
<div class="card">
<div class="card-body">
<h5 class="card-title">Custom Amount</h5>
<p class="card-text">Create invoice to pay custom amount</p>
@if (item.Custom && !Model.EnableShoppingCart)
{
<form method="post" asp-antiforgery="false" data-buy>
<input type="hidden" name="choicekey" value="@item.Id"/>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">@Model.CurrencySymbol</span>
</div>
<input class="form-control" type="number" min="0" step="@Model.Step" name="amount" placeholder="Amount">
<div class="input-group-append"><button class="btn btn-primary" type="submit">@Model.CustomButtonText</button></div>
<input class="form-control" type="number" min="@item.Price.Value" step="@Model.Step" name="amount"
value="@item.Price.Value" placeholder="Amount">
<div class="input-group-append">
<button class="btn btn-primary" type="submit">@Model.CustomButtonText</button>
</div>
</div>
</form>
</div>
}
else
{
<form method="post" asp-antiforgery="false">
<button type="submit" name="choiceKey" class="js-add-cart btn btn-primary" value="@item.Id">
@String.Format(Model.ButtonText, @item.Price.Formatted)</button>
</form>
}
</div>
</div>
</div>
}
}
@if (Model.ShowCustomAmount)
{
<div class="card h-100">
<div class="card-body my-auto">
<h5 class="card-title">Custom Amount</h5>
<p class="card-text">Create invoice to pay custom amount</p>
<form method="post" asp-antiforgery="false" data-buy>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">@Model.CurrencySymbol</span>
</div>
<input class="form-control" type="number" min="0" step="@Model.Step" name="amount" placeholder="Amount">
<div class="input-group-append"><button class="btn btn-primary" type="submit">@Model.CustomButtonText</button></div>
</div>
</form>
</div>
</div>
}
</div>
</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>
@ -207,7 +207,7 @@
<div class="payment__scan">
<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'} }"
<qrcode v-bind:value="scanDisplayQr" :options="{ width: 256, margin: 0, color: {dark:'#000', light:'#f5f5f7'} }" tag="svg"
v-if="scanDisplayQr">
</qrcode>

View File

@ -1,4 +1,4 @@
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
@addTagHelper *, BundlerMinifier.TagHelpers
@inject BTCPayServer.Services.LanguageService langService
@using Newtonsoft.Json
@using Newtonsoft.Json.Linq
@ -176,6 +176,7 @@
scanDisplayQr: "",
expiringSoon: false,
isModal: srvModel.isModal,
lightningAmountInSatoshi: srvModel.lightningAmountInSatoshi,
selectedThirdPartyProcessor: ""
}
});

View File

@ -1,6 +1,6 @@
@using BTCPayServer.Services.PaymentRequests
@model BTCPayServer.Models.PaymentRequestViewModels.UpdatePaymentRequestViewModel
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
@addTagHelper *, BundlerMinifier.TagHelpers
<section>
<div class="container">

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

@ -1,6 +1,6 @@
@model BTCPayServer.Models.PaymentRequestViewModels.ViewPaymentRequestViewModel
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
@addTagHelper *, BundlerMinifier.TagHelpers
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
@{
ViewData["Title"] = Model.Title;
@ -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

@ -1,4 +1,4 @@
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
@addTagHelper *, BundlerMinifier.TagHelpers
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
@model BTCPayServer.Controllers.ShowLightningNodeInfoViewModel
@ -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));
@ -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:value="srvModel.nodeInfo" :options="{ width: 256, margin: 0, color: {dark:'#000', light:'#f5f5f7'} }">
</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

@ -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

@ -5,7 +5,7 @@
@inject BTCPayServer.HostedServices.NBXplorerDashboard dashboard
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
@addTagHelper *, BundlerMinifier.TagHelpers
<!DOCTYPE html>
<html lang="en">
@ -55,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>

View File

@ -1,3 +1,3 @@
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
@addTagHelper *, BundlerMinifier.TagHelpers
<bundle name="wwwroot/bundles/jqueryvalidate-bundle.min.js" />

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

@ -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

@ -9448,6 +9448,11 @@ strong {
margin-bottom: 5px;
}
.payment__scan svg {
width: 256px;
height: 256px;
}
.payment__scan__checkmark-wrapper {
display: flex;
justify-content: center;

View File

@ -90,10 +90,28 @@ 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 rounded = Math.round(x);
var parts = rounded.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

@ -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

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

@ -24,16 +24,16 @@
"Copied": "Copiado",
"ConversionTab_BodyTop": "Você pode pagar {{btcDue}} {{cryptoCode}} utilizando outras altcoins além das que a loja aceita diretamente.",
"ConversionTab_BodyDesc": "Esse serviço é oferecido por terceiros. Por favor, tenha em mente que não temos nenhum controle sobre como seus fundos serão utilizados. A fatura apenas será marcada como paga quando os fundos forem recebidos na Blockchain {{cryptoCode}}.",
"ConversionTab_CalculateAmount_Error": "Retry",
"ConversionTab_LoadCurrencies_Error": "Retry",
"ConversionTab_CalculateAmount_Error": "Tentar novamente",
"ConversionTab_LoadCurrencies_Error": "Tentar novamente",
"ConversionTab_Lightning": "Não há provedores de conversão disponíveis para pagamentos via Lightning Network.",
"ConversionTab_CurrencyList_Select_Option": "Please select a currency to convert from",
"ConversionTab_CurrencyList_Select_Option": "Por favor selecione a moeda da qual pretende converter",
"Invoice expiring soon...": "A fatura está expirando...",
"Invoice expired": "Fatura expirada",
"What happened?": "O que aconteceu?",
"InvoiceExpired_Body_1": "Essa fatura expirou. Uma fatura é válida por apenas {{maxTimeMinutes}} minutos. \nVocê pode voltar para {{storeName}} se quiser enviar o seu pagamento novamente.",
"InvoiceExpired_Body_2": "Se você tentou enviar um pagamento, ele ainda não foi aceito pela rede Bitcoin. Nós ainda não recebemos o valor enviado.",
"InvoiceExpired_Body_3": "",
"InvoiceExpired_Body_3": "Se o recebermos mais tarde, vamos processar o pedido ou entrar em contato para combinarmos uma devolução...",
"Invoice ID": "Nº da Fatura",
"Order ID": "Nº do Pedido",
"Return to StoreName": "Voltar para {{storeName}}",
@ -41,10 +41,10 @@
"This invoice has been archived": "Essa fatura foi arquivada",
"Archived_Body": "Por favor, entre em contato com o estabelecimento para informações e suporte",
"BOLT 11 Invoice": "Fatura BOLT 11",
"Node Info": "Informação de nó",
"Node Info": "Informação do nó",
"txCount": "{{count}} transação",
"txCount_plural": "{{count}} transações",
"Pay with CoinSwitch": "Pay with CoinSwitch",
"Pay with Changelly": "Pay with Changelly",
"Close": "Close"
"Pay with CoinSwitch": "Pagar com CoinSwitch",
"Pay with Changelly": "Pagar com Changelly",
"Close": "Fechar"
}

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

@ -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,11 +1,11 @@
FROM microsoft/dotnet:2.1.500-sdk-alpine3.7 AS builder
FROM mcr.microsoft.com/dotnet/core/sdk:2.1.505-alpine3.7 AS builder
WORKDIR /source
COPY BTCPayServer/BTCPayServer.csproj BTCPayServer.csproj
RUN dotnet restore
COPY BTCPayServer/. .
RUN dotnet publish --output /app/ --configuration Release
FROM microsoft/dotnet:2.1.6-aspnetcore-runtime-alpine3.7
FROM mcr.microsoft.com/dotnet/core/aspnet:2.1.9-alpine3.7
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT false
RUN apk add --no-cache icu-libs
@ -14,8 +14,6 @@ ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8
WORKDIR /app
# This should be removed soon https://github.com/dotnet/corefx/issues/30003
RUN apk add --no-cache curl
RUN mkdir /datadir
ENV BTCPAY_DATADIR=/datadir
VOLUME /datadir

View File

@ -1,5 +1,5 @@
# This is a manifest image, will pull the image with the same arch as the builder machine
FROM microsoft/dotnet:2.1.500-sdk AS builder
FROM mcr.microsoft.com/dotnet/core/sdk:2.1.505 AS builder
WORKDIR /source
COPY BTCPayServer/BTCPayServer.csproj BTCPayServer.csproj
RUN dotnet restore
@ -7,7 +7,7 @@ COPY BTCPayServer/. .
RUN dotnet publish --output /app/ --configuration Release
# Force the builder machine to take make an arm runtime image. This is fine as long as the builder does not run any program
FROM microsoft/dotnet:2.1.6-aspnetcore-runtime-stretch-slim-arm32v7
FROM mcr.microsoft.com/dotnet/core/aspnet:2.1.9-stretch-slim-arm32v7
ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8