Compare commits

...

29 Commits

Author SHA1 Message Date
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
48 changed files with 1135 additions and 293 deletions

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()
@ -881,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",
@ -2264,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();
@ -2365,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

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.3.84</Version>
<Version>1.0.3.87</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<PropertyGroup>
@ -33,7 +33,7 @@
<EmbeddedResource Include="Currencies.txt" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.13" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.14" />
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.5.3" />
<PackageReference Include="HtmlSanitizer" Version="4.0.199" />

View File

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

View File

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

View File

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

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

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

@ -348,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)
@ -411,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

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

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

View File

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

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

@ -1,4 +1,4 @@
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
@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

@ -176,6 +176,7 @@
scanDisplayQr: "",
expiringSoon: false,
isModal: srvModel.isModal,
lightningAmountInSatoshi: srvModel.lightningAmountInSatoshi,
selectedThirdPartyProcessor: ""
}
});

View File

@ -10,7 +10,7 @@
<html>
<head>
<title>@Model.CryptoCode LN Node Info</title>
<meta charset="utf-8"/>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="apple-touch-icon" href="~/img/icons/icon-512x512.png">
@ -18,10 +18,10 @@
<link rel="manifest" href="~/manifest.json">
<link href="@this.Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet"/>
<link href="~/vendor/font-awesome/css/font-awesome.min.css" rel="stylesheet"/>
<link href="@this.Context.Request.GetRelativePathOrAbsolute(themeManager.BootstrapUri)" rel="stylesheet" />
<link href="~/vendor/font-awesome/css/font-awesome.min.css" rel="stylesheet" />
<bundle name="wwwroot/bundles/lightning-node-info-bundle.min.js"/>
<bundle name="wwwroot/bundles/lightning-node-info-bundle.min.js" />
<script type="text/javascript">
var srvModel = @Html.Raw(Json.Serialize(Model));
@ -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

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

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

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

View File

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

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

View File

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

View File

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