Compare commits

...

12 Commits

Author SHA1 Message Date
57bb980526 Update packages 2018-07-23 00:21:40 +09:00
e0c718f4ba Fix wallet alert 2018-07-23 00:21:40 +09:00
c58e015bfb Merge pull request from viacoin/master
Viacoin: add to README
2018-07-22 21:45:28 +09:00
648829644a Add QR code information 2018-07-22 21:28:21 +09:00
1b0f8c7aca Viacoin: add to README 2018-07-22 14:18:31 +02:00
4f8e0b0393 Can get lnd config without being logged 2018-07-22 18:43:11 +09:00
466f65d6cd bump 2018-07-22 18:39:22 +09:00
022b4f115d Expose LND gRPC settings 2018-07-22 18:38:14 +09:00
71f6aaabbd Merge pull request from rockstardev/master
Changing Lightning suffix per suggestion
2018-07-21 22:21:49 +09:00
79b06bce42 Changing Lightning suffix per suggestion 2018-07-20 23:33:54 -05:00
480afebcd9 bump 2018-07-20 15:25:45 +09:00
96721e95a2 Clean unreachable store if user is deleted 2018-07-20 15:24:19 +09:00
20 changed files with 649 additions and 252 deletions

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.2.52</Version>
<Version>1.0.2.56</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<ItemGroup>
@ -35,18 +35,18 @@
<PackageReference Include="Hangfire" Version="1.6.19" />
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.2" />
<PackageReference Include="LedgerWallet" Version="1.0.1.36" />
<PackageReference Include="LedgerWallet" Version="2.0.0" />
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.0" />
<PackageReference Include="NBitcoin" Version="4.1.1.27" />
<PackageReference Include="NBitcoin" Version="4.1.1.30" />
<PackageReference Include="NBitpayClient" Version="1.0.0.29" />
<PackageReference Include="DBreeze" Version="1.87.0" />
<PackageReference Include="NBXplorer.Client" Version="1.0.2.13" />
<PackageReference Include="NBXplorer.Client" Version="1.0.2.14" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.14" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.16" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.1.0" />
<PackageReference Include="System.Xml.XmlSerializer" Version="4.3.0" />
<PackageReference Include="Text.Analyzers" Version="2.6.0" />
@ -114,6 +114,15 @@
<Folder Include="wwwroot\vendor\highlightjs\" />
</ItemGroup>
<ItemGroup>
<Content Update="Views\Server\LNDGRPCServices.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\Services.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
</ItemGroup>
<ItemGroup>
<None Update="devtest.pfx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>

@ -307,7 +307,7 @@ namespace BTCPayServer.Controllers
private string GetDisplayName(PaymentMethodId paymentMethodId, BTCPayNetwork network)
{
return paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
network.DisplayName : network.DisplayName + " - Lightning";
network.DisplayName : network.DisplayName + " (via Lightning)";
}
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)

@ -1,13 +1,17 @@
using BTCPayServer.HostedServices;
using BTCPayServer.Configuration;
using BTCPayServer.HostedServices;
using BTCPayServer.Models;
using BTCPayServer.Models.ServerViewModels;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services;
using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Validations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NBitcoin.DataEncoders;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
@ -25,14 +29,23 @@ namespace BTCPayServer.Controllers
private UserManager<ApplicationUser> _UserManager;
SettingsRepository _SettingsRepository;
private BTCPayRateProviderFactory _RateProviderFactory;
private StoreRepository _StoreRepository;
LightningConfigurationProvider _LnConfigProvider;
BTCPayServerOptions _Options;
public ServerController(UserManager<ApplicationUser> userManager,
Configuration.BTCPayServerOptions options,
BTCPayRateProviderFactory rateProviderFactory,
SettingsRepository settingsRepository)
SettingsRepository settingsRepository,
LightningConfigurationProvider lnConfigProvider,
Services.Stores.StoreRepository storeRepository)
{
_Options = options;
_UserManager = userManager;
_SettingsRepository = settingsRepository;
_RateProviderFactory = rateProviderFactory;
_StoreRepository = storeRepository;
_LnConfigProvider = lnConfigProvider;
}
[Route("server/rates")]
@ -74,7 +87,7 @@ namespace BTCPayServer.Controllers
try
{
var service = GetCoinaverageService(vm, true);
if(service != null)
if (service != null)
await service.TestAuthAsync();
}
catch
@ -188,6 +201,7 @@ namespace BTCPayServer.Controllers
if (user == null)
return NotFound();
await _UserManager.DeleteAsync(user);
await _StoreRepository.CleanUnreachableStores();
StatusMessage = "User deleted";
return RedirectToAction(nameof(ListUsers));
}
@ -220,6 +234,122 @@ namespace BTCPayServer.Controllers
return View(settings);
}
[Route("server/services")]
public IActionResult Services()
{
var result = new ServicesViewModel();
foreach (var internalNode in _Options.InternalLightningByCryptoCode)
{
//Only BTC can be supported because gRPC does not allow http path rewriting.
if (internalNode.Key == "BTC" && GetExternalLNDConnectionString(internalNode.Value) != null)
{
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
{
Crypto = internalNode.Key,
Type = "gRPC"
});
}
}
return View(result);
}
private LightningConnectionString GetExternalLNDConnectionString(LightningConnectionString value)
{
if (value.ConnectionType != LightningConnectionType.LndREST)
return null;
var external = new LightningConnectionString();
external.ConnectionType = LightningConnectionType.LndREST;
external.BaseUri = _Options.ExternalUrl ?? value.BaseUri;
if (external.BaseUri.Scheme == "http" || value.AllowInsecure)
{
external.AllowInsecure = true;
}
try
{
if (value.MacaroonFilePath != null)
external.Macaroon = System.IO.File.ReadAllBytes(value.MacaroonFilePath);
}
catch
{
return null;
}
if (value.Macaroon != null)
external.Macaroon = value.Macaroon;
// If external url is provided, then we don't care about cert thumbprint
// because we override it at the reverse proxy level with a trusted certificate
if (_Options.ExternalUrl == null)
{
if (value.CertificateThumbprint != null)
{
external.CertificateThumbprint = value.CertificateThumbprint;
external.AllowInsecure = false;
}
}
return external;
}
[Route("server/services/lnd-grpc/{cryptoCode}")]
public IActionResult LNDGRPCServices(string cryptoCode, ulong? secret)
{
if (!_Options.InternalLightningByCryptoCode.TryGetValue(cryptoCode.ToUpperInvariant(), out var connectionString))
return NotFound();
var model = new LNDGRPCServicesViewModel();
var external = GetExternalLNDConnectionString(connectionString);
model.Host = $"{external.BaseUri.DnsSafeHost}:{external.BaseUri.Port}";
model.SSL = external.BaseUri.Scheme == "https";
if (external.CertificateThumbprint != null)
{
model.CertificateThumbprint = Encoders.Hex.EncodeData(external.CertificateThumbprint);
}
if (external.Macaroon != null)
{
model.Macaroon = Encoders.Hex.EncodeData(external.Macaroon);
}
if (secret != null)
{
var lnConfig = _LnConfigProvider.GetConfig(secret.Value);
if (lnConfig != null)
{
model.QRCodeLink = $"{this.Request.GetAbsoluteRoot().WithTrailingSlash()}lnd-config/{secret.Value}/lnd.config";
model.QRCode = $"config={model.QRCodeLink}";
}
}
return View(model);
}
[Route("lnd-config/{secret}/lnd.config")]
[AllowAnonymous]
public IActionResult GetLNDConfig(ulong secret)
{
var conf = _LnConfigProvider.GetConfig(secret);
if (conf == null)
return NotFound();
return Json(conf);
}
[Route("server/services/lnd-grpc/{cryptoCode}")]
[HttpPost]
public IActionResult LNDGRPCServicesPOST(string cryptoCode)
{
if (!_Options.InternalLightningByCryptoCode.TryGetValue(cryptoCode.ToUpperInvariant(), out var connectionString))
return NotFound();
var external = GetExternalLNDConnectionString(connectionString);
LightningConfigurations confs = new LightningConfigurations();
LightningConfiguration conf = new LightningConfiguration();
conf.Type = "grpc";
conf.CryptoCode = cryptoCode;
conf.Host = external.BaseUri.DnsSafeHost;
conf.Port = external.BaseUri.Port;
conf.SSL = external.BaseUri.Scheme == "https";
conf.Macaroon = external.Macaroon == null ? null : Encoders.Hex.EncodeData(external.Macaroon);
conf.CertificateThumbprint = external.CertificateThumbprint == null ? null : Encoders.Hex.EncodeData(external.CertificateThumbprint);
confs.Configurations.Add(conf);
var secret = _LnConfigProvider.KeepConfig(confs);
return RedirectToAction(nameof(LNDGRPCServices), new { cryptoCode = cryptoCode, secret = secret });
}
[Route("server/theme")]
public async Task<IActionResult> Theme()
{
@ -243,7 +373,7 @@ namespace BTCPayServer.Controllers
{
try
{
if(!model.Settings.IsComplete())
if (!model.Settings.IsComplete())
{
model.StatusMessage = "Error: Required fields missing";
return View(model);

@ -195,216 +195,221 @@ namespace BTCPayServer.Controllers
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
var hw = new HardwareWalletService(webSocket);
object result = null;
try
using (var normalOperationTimeout = new CancellationTokenSource())
using (var signTimeout = new CancellationTokenSource())
{
BTCPayNetwork network = null;
if (cryptoCode != null)
normalOperationTimeout.CancelAfter(TimeSpan.FromMinutes(30));
var hw = new HardwareWalletService(webSocket);
object result = null;
try
{
network = _NetworkProvider.GetNetwork(cryptoCode);
if (network == null)
throw new FormatException("Invalid value for crypto code");
}
BitcoinAddress destinationAddress = null;
if (destination != null)
{
try
BTCPayNetwork network = null;
if (cryptoCode != null)
{
destinationAddress = BitcoinAddress.Create(destination, network.NBitcoinNetwork);
}
catch { }
if (destinationAddress == null)
throw new FormatException("Invalid value for destination");
}
FeeRate feeRateValue = null;
if (feeRate != null)
{
try
{
feeRateValue = new FeeRate(Money.Satoshis(int.Parse(feeRate, CultureInfo.InvariantCulture)), 1);
}
catch { }
if (feeRateValue == null || feeRateValue.FeePerK <= Money.Zero)
throw new FormatException("Invalid value for fee rate");
}
Money amountBTC = null;
if (amount != null)
{
try
{
amountBTC = Money.Parse(amount);
}
catch { }
if (amountBTC == null || amountBTC <= Money.Zero)
throw new FormatException("Invalid value for amount");
}
bool subsctractFeesValue = false;
if (substractFees != null)
{
try
{
subsctractFeesValue = bool.Parse(substractFees);
}
catch { throw new FormatException("Invalid value for subtract fees"); }
}
if (command == "test")
{
result = await hw.Test();
}
if (command == "getxpub")
{
var getxpubResult = await hw.GetExtPubKey(network, account);
result = getxpubResult;
}
if (command == "getinfo")
{
var strategy = GetDirectDerivationStrategy(store, network);
var strategyBase = GetDerivationStrategy(store, network);
if (strategy == null || await hw.GetKeyPath(network, strategy) == null)
{
throw new Exception($"This store is not configured to use this ledger");
network = _NetworkProvider.GetNetwork(cryptoCode);
if (network == null)
throw new FormatException("Invalid value for crypto code");
}
var feeProvider = _FeeRateProvider.CreateFeeProvider(network);
var recommendedFees = feeProvider.GetFeeRateAsync();
var balance = _WalletProvider.GetWallet(network).GetBalance(strategyBase);
result = new GetInfoResult() { Balance = (double)(await balance).ToDecimal(MoneyUnit.BTC), RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi };
}
BitcoinAddress destinationAddress = null;
if (destination != null)
{
try
{
destinationAddress = BitcoinAddress.Create(destination, network.NBitcoinNetwork);
}
catch { }
if (destinationAddress == null)
throw new FormatException("Invalid value for destination");
}
if (command == "sendtoaddress")
{
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
throw new Exception($"{network.CryptoCode}: not started or fully synched");
var strategy = GetDirectDerivationStrategy(store, network);
var strategyBase = GetDerivationStrategy(store, network);
var wallet = _WalletProvider.GetWallet(network);
var change = wallet.GetChangeAddressAsync(strategyBase);
FeeRate feeRateValue = null;
if (feeRate != null)
{
try
{
feeRateValue = new FeeRate(Money.Satoshis(int.Parse(feeRate, CultureInfo.InvariantCulture)), 1);
}
catch { }
if (feeRateValue == null || feeRateValue.FeePerK <= Money.Zero)
throw new FormatException("Invalid value for fee rate");
}
var unspentCoins = await wallet.GetUnspentCoins(strategyBase);
var changeAddress = await change;
var send = new[] { (
Money amountBTC = null;
if (amount != null)
{
try
{
amountBTC = Money.Parse(amount);
}
catch { }
if (amountBTC == null || amountBTC <= Money.Zero)
throw new FormatException("Invalid value for amount");
}
bool subsctractFeesValue = false;
if (substractFees != null)
{
try
{
subsctractFeesValue = bool.Parse(substractFees);
}
catch { throw new FormatException("Invalid value for subtract fees"); }
}
if (command == "test")
{
result = await hw.Test(normalOperationTimeout.Token);
}
if (command == "getxpub")
{
var getxpubResult = await hw.GetExtPubKey(network, account, normalOperationTimeout.Token);
result = getxpubResult;
}
if (command == "getinfo")
{
var strategy = GetDirectDerivationStrategy(store, network);
var strategyBase = GetDerivationStrategy(store, network);
if (strategy == null || await hw.GetKeyPath(network, strategy, normalOperationTimeout.Token) == null)
{
throw new Exception($"This store is not configured to use this ledger");
}
var feeProvider = _FeeRateProvider.CreateFeeProvider(network);
var recommendedFees = feeProvider.GetFeeRateAsync();
var balance = _WalletProvider.GetWallet(network).GetBalance(strategyBase);
result = new GetInfoResult() { Balance = (double)(await balance).ToDecimal(MoneyUnit.BTC), RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi };
}
if (command == "sendtoaddress")
{
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
throw new Exception($"{network.CryptoCode}: not started or fully synched");
var strategy = GetDirectDerivationStrategy(store, network);
var strategyBase = GetDerivationStrategy(store, network);
var wallet = _WalletProvider.GetWallet(network);
var change = wallet.GetChangeAddressAsync(strategyBase);
var unspentCoins = await wallet.GetUnspentCoins(strategyBase);
var changeAddress = await change;
var send = new[] { (
destination: destinationAddress as IDestination,
amount: amountBTC,
substractFees: subsctractFeesValue) };
foreach (var element in send)
{
if (element.destination == null)
throw new ArgumentNullException(nameof(element.destination));
if (element.amount == null)
throw new ArgumentNullException(nameof(element.amount));
if (element.amount <= Money.Zero)
throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero");
}
foreach (var element in send)
{
if (element.destination == null)
throw new ArgumentNullException(nameof(element.destination));
if (element.amount == null)
throw new ArgumentNullException(nameof(element.amount));
if (element.amount <= Money.Zero)
throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero");
}
var foundKeyPath = await hw.GetKeyPath(network, strategy);
if (foundKeyPath == null)
{
throw new HardwareWalletException($"This store is not configured to use this ledger");
}
var foundKeyPath = await hw.GetKeyPath(network, strategy, normalOperationTimeout.Token);
if (foundKeyPath == null)
{
throw new HardwareWalletException($"This store is not configured to use this ledger");
}
TransactionBuilder builder = new TransactionBuilder();
builder.StandardTransactionPolicy.MinRelayTxFee = summary.Status.BitcoinStatus.MinRelayTxFee;
builder.SetConsensusFactory(network.NBitcoinNetwork);
builder.AddCoins(unspentCoins.Select(c => c.Coin).ToArray());
TransactionBuilder builder = new TransactionBuilder();
builder.StandardTransactionPolicy.MinRelayTxFee = summary.Status.BitcoinStatus.MinRelayTxFee;
builder.SetConsensusFactory(network.NBitcoinNetwork);
builder.AddCoins(unspentCoins.Select(c => c.Coin).ToArray());
foreach (var element in send)
{
builder.Send(element.destination, element.amount);
if (element.substractFees)
builder.SubtractFees();
}
builder.SetChange(changeAddress.Item1);
foreach (var element in send)
{
builder.Send(element.destination, element.amount);
if (element.substractFees)
builder.SubtractFees();
}
builder.SetChange(changeAddress.Item1);
if (network.MinFee == null)
{
builder.SendEstimatedFees(feeRateValue);
}
else
{
var estimatedFee = builder.EstimateFees(feeRateValue);
if (network.MinFee > estimatedFee)
builder.SendFees(network.MinFee);
else
if (network.MinFee == null)
{
builder.SendEstimatedFees(feeRateValue);
}
builder.Shuffle();
var unsigned = builder.BuildTransaction(false);
var keypaths = new Dictionary<Script, KeyPath>();
foreach (var c in unspentCoins)
{
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
}
var hasChange = unsigned.Outputs.Count == 2;
var usedCoins = builder.FindSpentCoins(unsigned);
Dictionary<uint256, Transaction> parentTransactions = new Dictionary<uint256, Transaction>();
if (!strategy.Segwit)
{
var parentHashes = usedCoins.Select(c => c.Outpoint.Hash).ToHashSet();
var explorer = _ExplorerProvider.GetExplorerClient(network);
var getTransactionAsyncs = parentHashes.Select(h => (Op: explorer.GetTransactionAsync(h), Hash: h)).ToList();
foreach (var getTransactionAsync in getTransactionAsyncs)
{
var tx = (await getTransactionAsync.Op);
if (tx == null)
throw new Exception($"Parent transaction {getTransactionAsync.Hash} not found");
parentTransactions.Add(tx.Transaction.GetHash(), tx.Transaction);
}
}
var transaction = await hw.SignTransactionAsync(usedCoins.Select(c => new SignatureRequest
{
InputTransaction = parentTransactions.TryGet(c.Outpoint.Hash),
InputCoin = c,
KeyPath = foundKeyPath.Derive(keypaths[c.TxOut.ScriptPubKey]),
PubKey = strategy.Root.Derive(keypaths[c.TxOut.ScriptPubKey]).PubKey
}).ToArray(), unsigned, hasChange ? foundKeyPath.Derive(changeAddress.Item2) : null);
try
{
var broadcastResult = await wallet.BroadcastTransactionsAsync(new List<Transaction>() { transaction });
if (!broadcastResult[0].Success)
else
{
throw new Exception($"RPC Error while broadcasting: {broadcastResult[0].RPCCode} {broadcastResult[0].RPCCodeMessage} {broadcastResult[0].RPCMessage}");
var estimatedFee = builder.EstimateFees(feeRateValue);
if (network.MinFee > estimatedFee)
builder.SendFees(network.MinFee);
else
builder.SendEstimatedFees(feeRateValue);
}
builder.Shuffle();
var unsigned = builder.BuildTransaction(false);
var keypaths = new Dictionary<Script, KeyPath>();
foreach (var c in unspentCoins)
{
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
}
var hasChange = unsigned.Outputs.Count == 2;
var usedCoins = builder.FindSpentCoins(unsigned);
Dictionary<uint256, Transaction> parentTransactions = new Dictionary<uint256, Transaction>();
if (!strategy.Segwit)
{
var parentHashes = usedCoins.Select(c => c.Outpoint.Hash).ToHashSet();
var explorer = _ExplorerProvider.GetExplorerClient(network);
var getTransactionAsyncs = parentHashes.Select(h => (Op: explorer.GetTransactionAsync(h), Hash: h)).ToList();
foreach (var getTransactionAsync in getTransactionAsyncs)
{
var tx = (await getTransactionAsync.Op);
if (tx == null)
throw new Exception($"Parent transaction {getTransactionAsync.Hash} not found");
parentTransactions.Add(tx.Transaction.GetHash(), tx.Transaction);
}
}
signTimeout.CancelAfter(TimeSpan.FromMinutes(5));
var transaction = await hw.SignTransactionAsync(usedCoins.Select(c => new SignatureRequest
{
InputTransaction = parentTransactions.TryGet(c.Outpoint.Hash),
InputCoin = c,
KeyPath = foundKeyPath.Derive(keypaths[c.TxOut.ScriptPubKey]),
PubKey = strategy.Root.Derive(keypaths[c.TxOut.ScriptPubKey]).PubKey
}).ToArray(), unsigned, hasChange ? foundKeyPath.Derive(changeAddress.Item2) : null, signTimeout.Token);
try
{
var broadcastResult = await wallet.BroadcastTransactionsAsync(new List<Transaction>() { transaction });
if (!broadcastResult[0].Success)
{
throw new Exception($"RPC Error while broadcasting: {broadcastResult[0].RPCCode} {broadcastResult[0].RPCCodeMessage} {broadcastResult[0].RPCMessage}");
}
}
catch (Exception ex)
{
throw new Exception("Error while broadcasting: " + ex.Message);
}
wallet.InvalidateCache(strategyBase);
result = new SendToAddressResult() { TransactionId = transaction.GetHash().ToString() };
}
catch (Exception ex)
{
throw new Exception("Error while broadcasting: " + ex.Message);
}
wallet.InvalidateCache(strategyBase);
result = new SendToAddressResult() { TransactionId = transaction.GetHash().ToString() };
}
}
catch (OperationCanceledException)
{ result = new LedgerTestResult() { Success = false, Error = "Timeout" }; }
catch (Exception ex)
{ result = new LedgerTestResult() { Success = false, Error = ex.Message }; }
finally { hw.Dispose(); }
try
{
if (result != null)
catch (OperationCanceledException)
{ result = new LedgerTestResult() { Success = false, Error = "Timeout" }; }
catch (Exception ex)
{ result = new LedgerTestResult() { Success = false, Error = ex.Message }; }
finally { hw.Dispose(); }
try
{
UTF8Encoding UTF8NOBOM = new UTF8Encoding(false);
var bytes = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, _MvcJsonOptions.SerializerSettings));
await webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token);
if (result != null)
{
UTF8Encoding UTF8NOBOM = new UTF8Encoding(false);
var bytes = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, _MvcJsonOptions.SerializerSettings));
await webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token);
}
}
catch { }
finally
{
await webSocket.CloseSocket();
}
}
catch { }
finally
{
await webSocket.CloseSocket();
}
return new EmptyResult();
}

@ -61,18 +61,9 @@ namespace BTCPayServer.HostedServices
}
}
private async Task UnreachableStoreCheck()
private Task UnreachableStoreCheck()
{
using (var ctx = _DBContextFactory.CreateContext())
{
if (!ctx.Database.SupportDropForeignKey())
return;
foreach (var store in await ctx.Stores.Where(s => s.UserStores.Where(u => u.Role == StoreRoles.Owner).Count() == 0).ToArrayAsync())
{
ctx.Stores.Remove(store);
}
await ctx.SaveChangesAsync();
}
return _StoreRepository.CleanUnreachableStores();
}
private async Task DeprecatedLightningConnectionStringCheck()

@ -92,6 +92,7 @@ namespace BTCPayServer.Hosting
return opts.NetworkProvider;
});
services.TryAddSingleton<LightningConfigurationProvider>();
services.TryAddSingleton<LanguageService>();
services.TryAddSingleton<NBXplorerDashboard>();
services.TryAddSingleton<StoreRepository>();

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.ServerViewModels
{
public class LNDGRPCServicesViewModel
{
public string Host { get; set; }
public bool SSL { get; set; }
public string Macaroon { get; set; }
public string CertificateThumbprint { get; set; }
public string QRCode { get; set; }
public string QRCodeLink { get; set; }
}
}

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.ServerViewModels
{
public class ServicesViewModel
{
public class LNDServiceViewModel
{
public string Crypto { get; set; }
public string Type { get; set; }
}
public List<LNDServiceViewModel> LNDServices { get; set; } = new List<LNDServiceViewModel>();
}
}

@ -353,7 +353,7 @@ namespace BTCPayServer.Payments.Lightning
public LightningConnectionType ConnectionType
{
get;
private set;
set;
}
public byte[] Macaroon { get; set; }
public string MacaroonFilePath { get; set; }

@ -3,16 +3,17 @@
"Docker-Regtest": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"BTCPAY_NETWORK": "regtest",
"BTCPAY_LTCEXPLORERURL": "http://127.0.0.1:32838/",
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
"ASPNETCORE_ENVIRONMENT": "Development",
"BTCPAY_CHAINS": "btc,ltc",
"BTCPAY_BTCLIGHTNING": "http://api-token:foiewnccewuify@127.0.0.1:54938/",
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver",
"BTCPAY_BUNDLEJSCSS": "false"
},
"environmentVariables": {
"BTCPAY_NETWORK": "regtest",
"BTCPAY_LTCEXPLORERURL": "http://127.0.0.1:32838/",
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
"ASPNETCORE_ENVIRONMENT": "Development",
"BTCPAY_CHAINS": "btc,ltc",
"BTCPAY_BTCLIGHTNING": "http://api-token:foiewnccewuify@127.0.0.1:54938/",
//"BTCPAY_BTCLIGHTNING": "type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true",
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver",
"BTCPAY_BUNDLEJSCSS": "false"
},
"applicationUrl": "http://127.0.0.1:14142/"
}
}

@ -34,26 +34,22 @@ namespace BTCPayServer.Services
}
SemaphoreSlim _Semaphore = new SemaphoreSlim(1, 1);
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(10);
public async Task<byte[][]> Exchange(byte[][] apdus)
public async Task<byte[][]> Exchange(byte[][] apdus, CancellationToken cancellationToken)
{
await _Semaphore.WaitAsync();
List<byte[]> responses = new List<byte[]>();
try
{
using (CancellationTokenSource cts = new CancellationTokenSource(Timeout))
foreach (var apdu in apdus)
{
foreach (var apdu in apdus)
{
await this.webSocket.SendAsync(new ArraySegment<byte>(apdu), WebSocketMessageType.Binary, true, cts.Token);
}
foreach (var apdu in apdus)
{
byte[] response = new byte[300];
var result = await this.webSocket.ReceiveAsync(new ArraySegment<byte>(response), cts.Token);
Array.Resize(ref response, result.Count);
responses.Add(response);
}
await this.webSocket.SendAsync(new ArraySegment<byte>(apdu), WebSocketMessageType.Binary, true, cancellationToken);
}
foreach (var apdu in apdus)
{
byte[] response = new byte[300];
var result = await this.webSocket.ReceiveAsync(new ArraySegment<byte>(response), cancellationToken);
Array.Resize(ref response, result.Count);
responses.Add(response);
}
}
finally
@ -86,20 +82,20 @@ namespace BTCPayServer.Services
_Ledger = new LedgerClient(_Transport);
}
public async Task<LedgerTestResult> Test()
public async Task<LedgerTestResult> Test(CancellationToken cancellation)
{
var version = await _Ledger.GetFirmwareVersionAsync();
var version = await Ledger.GetFirmwareVersionAsync(cancellation);
return new LedgerTestResult() { Success = true };
}
public async Task<GetXPubResult> GetExtPubKey(BTCPayNetwork network, int account)
public async Task<GetXPubResult> GetExtPubKey(BTCPayNetwork network, int account, CancellationToken cancellation)
{
if (network == null)
throw new ArgumentNullException(nameof(network));
var segwit = network.NBitcoinNetwork.Consensus.SupportSegwit;
var path = network.GetRootKeyPath().Derive(account, true);
var pubkey = await GetExtPubKey(_Ledger, network, path, false);
var pubkey = await GetExtPubKey(Ledger, network, path, false, cancellation);
var derivation = new DerivationStrategyFactory(network.NBitcoinNetwork).CreateDirectDerivationStrategy(pubkey, new DerivationStrategyOptions()
{
P2SH = segwit,
@ -108,11 +104,11 @@ namespace BTCPayServer.Services
return new GetXPubResult() { ExtPubKey = derivation.ToString(), KeyPath = path };
}
private static async Task<BitcoinExtPubKey> GetExtPubKey(LedgerClient ledger, BTCPayNetwork network, KeyPath account, bool onlyChaincode)
private static async Task<BitcoinExtPubKey> GetExtPubKey(LedgerClient ledger, BTCPayNetwork network, KeyPath account, bool onlyChaincode, CancellationToken cancellation)
{
try
{
var pubKey = await ledger.GetWalletPubKeyAsync(account);
var pubKey = await ledger.GetWalletPubKeyAsync(account, cancellation: cancellation);
try
{
pubKey.GetAddress(network.NBitcoinNetwork);
@ -122,7 +118,7 @@ namespace BTCPayServer.Services
if (network.NBitcoinNetwork.NetworkType == NetworkType.Mainnet)
throw new Exception($"The opened ledger app does not seems to support {network.NBitcoinNetwork.Name}.");
}
var fingerprint = onlyChaincode ? new byte[4] : (await ledger.GetWalletPubKeyAsync(account.Parent)).UncompressedPublicKey.Compress().Hash.ToBytes().Take(4).ToArray();
var fingerprint = onlyChaincode ? new byte[4] : (await ledger.GetWalletPubKeyAsync(account.Parent, cancellation: cancellation)).UncompressedPublicKey.Compress().Hash.ToBytes().Take(4).ToArray();
var extpubkey = new ExtPubKey(pubKey.UncompressedPublicKey.Compress(), pubKey.ChainCode, (byte)account.Indexes.Length, fingerprint, account.Indexes.Last()).GetWif(network.NBitcoinNetwork);
return extpubkey;
}
@ -132,7 +128,7 @@ namespace BTCPayServer.Services
}
}
public async Task<KeyPath> GetKeyPath(BTCPayNetwork network, DirectDerivationStrategy directStrategy)
public async Task<KeyPath> GetKeyPath(BTCPayNetwork network, DirectDerivationStrategy directStrategy, CancellationToken cancellation)
{
List<KeyPath> derivations = new List<KeyPath>();
if (network.NBitcoinNetwork.Consensus.SupportSegwit)
@ -146,7 +142,7 @@ namespace BTCPayServer.Services
{
try
{
var extpubkey = await GetExtPubKey(_Ledger, network, account, true);
var extpubkey = await GetExtPubKey(Ledger, network, account, true, cancellation);
if (directStrategy.Root.PubKey == extpubkey.ExtPubKey.PubKey)
{
foundKeyPath = account;
@ -164,15 +160,15 @@ namespace BTCPayServer.Services
public async Task<Transaction> SignTransactionAsync(SignatureRequest[] signatureRequests,
Transaction unsigned,
KeyPath changeKeyPath)
KeyPath changeKeyPath,
CancellationToken cancellationToken)
{
_Transport.Timeout = TimeSpan.FromMinutes(5);
return await Ledger.SignTransactionAsync(signatureRequests, unsigned, changeKeyPath);
return await Ledger.SignTransactionAsync(signatureRequests, unsigned, changeKeyPath, cancellationToken);
}
public void Dispose()
{
if(_Transport != null)
if (_Transport != null)
_Transport.Dispose();
}
}

@ -0,0 +1,55 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
namespace BTCPayServer.Services
{
public class LightningConfigurationProvider
{
ConcurrentDictionary<ulong, (DateTimeOffset expiration, LightningConfigurations config)> _Map = new ConcurrentDictionary<ulong, (DateTimeOffset expiration, LightningConfigurations config)>();
public ulong KeepConfig(LightningConfigurations configuration)
{
CleanExpired();
var secret = RandomUtils.GetUInt64();
_Map.AddOrReplace(secret, (DateTimeOffset.UtcNow + TimeSpan.FromMinutes(10), configuration));
return secret;
}
public LightningConfigurations GetConfig(ulong secret)
{
CleanExpired();
if (!_Map.TryGetValue(secret, out var value))
return null;
return value.config;
}
private void CleanExpired()
{
foreach(var item in _Map)
{
if(item.Value.expiration < DateTimeOffset.UtcNow)
{
_Map.TryRemove(item.Key, out var unused);
}
}
}
}
public class LightningConfigurations
{
public List<LightningConfiguration> Configurations { get; set; } = new List<LightningConfiguration>();
}
public class LightningConfiguration
{
public string Type { get; set; }
public string CryptoCode { get; set; }
public string Host { get; set; }
public int Port { get; set; }
public bool SSL { get; set; }
public string CertificateThumbprint { get; set; }
public string Macaroon { get; set; }
}
}

@ -112,6 +112,20 @@ namespace BTCPayServer.Services.Stores
}
}
public async Task CleanUnreachableStores()
{
using (var ctx = _ContextFactory.CreateContext())
{
if (!ctx.Database.SupportDropForeignKey())
return;
foreach (var store in await ctx.Stores.Where(s => s.UserStores.Where(u => u.Role == StoreRoles.Owner).Count() == 0).ToArrayAsync())
{
ctx.Stores.Remove(store);
}
await ctx.SaveChangesAsync();
}
}
public async Task RemoveStoreUser(string storeId, string userId)
{
using (var ctx = _ContextFactory.CreateContext())

@ -0,0 +1,106 @@
@model BTCPayServer.Models.ServerViewModels.LNDGRPCServicesViewModel
@{
ViewData.SetActivePageAndTitle(ServerNavPages.Services);
}
<h4>GRPC settings</h4>
<partial name="_StatusMessage" for="@TempData["StatusMessage"]" />
<div class="row">
<div class="col-md-6">
<div asp-validation-summary="All" class="text-danger"></div>
</div>
</div>
<div class="row">
<div class="col-md-8">
<div class="form-group">
<p>
<span>BTCPay exposes gRPC services for outside consumption, you will find connection informaiton here.<br /></span>
</p>
</div>
<div class="form-group">
<h5>QR Code connection</h5>
<p>
<span>You can use this QR Code to connect your Zap wallet to your LND instance.<br /></span>
<span>This QR Code is only valid for 10 minutes</span>
</p>
</div>
<div class="form-group">
@if(Model.QRCode == null)
{
<div class="form-group">
<form method="post">
<button type="submit" class="btn btn-primary">Show QR Code</button>
</form>
</div>
}
else
{
<div class="form-group">
<div id="qrCode"></div>
<div id="qrCodeData" data-url="@Html.Raw(Model.QRCode)"></div>
</div>
<p>See QR Code information by clicking <a href="#detailsQR" data-toggle="collapse">here</a></p>
<div id="detailsQR" class="collapse">
<div class="form-group">
<label>QR Code data</label>
<input asp-for="QRCode" readonly class="form-control" />
</div>
<div class="form-group">
Click <a href="@Model.QRCodeLink" target="_blank">here</a> to open the configuration file.
</div>
</div>
}
<div class="form-group">
<h5>More details...</h5>
<p>Alternatively, you can see the settings by clicking <a href="#details" data-toggle="collapse">here</a></p>
</div>
<div id="details" class="collapse">
<div class="form-group">
<label asp-for="Host"></label>
<input asp-for="Host" readonly class="form-control" />
</div>
<div class="form-group">
<label asp-for="SSL"></label>
<input asp-for="SSL" disabled type="checkbox" class="form-check-inline" />
</div>
@if(Model.Macaroon != null)
{
<div class="form-group">
<label asp-for="Macaroon"></label>
<input asp-for="Macaroon" readonly class="form-control" />
</div>
}
@if(Model.CertificateThumbprint != null)
{
<div class="form-group">
<label asp-for="CertificateThumbprint"></label>
<input asp-for="CertificateThumbprint" readonly class="form-control" />
</div>
}
</div>
</div>
</div>
</div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
@if(Model.QRCode != null)
{
<script type="text/javascript" src="~/js/qrcode.min.js"></script>
<script type="text/javascript">
new QRCode(document.getElementById("qrCode"),
{
text: "@Html.Raw(Model.QRCode)",
width: 150,
height: 150
});
</script>
}
}

@ -7,6 +7,6 @@ namespace BTCPayServer.Views.Server
{
public enum ServerNavPages
{
Index, Users, Rates, Emails, Policies, Theme, Hangfire
Index, Users, Rates, Emails, Policies, Theme, Hangfire, Services
}
}

@ -0,0 +1,53 @@
@model BTCPayServer.Models.ServerViewModels.ServicesViewModel
@{
ViewData.SetActivePageAndTitle(ServerNavPages.Services);
}
<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="@TempData["StatusMessage"]" />
<div class="row">
<div class="col-md-6">
<div asp-validation-summary="All" class="text-danger"></div>
</div>
</div>
<div class="row">
@if(Model.LNDServices.Count != 0)
{
<div class="col-md-8">
<div class="form-group">
<h5>LND nodes</h5>
<span>You can get access to internal LND services here. For gRPC, only BTC is supported.</span>
</div>
<div class="form-group">
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Crypto</th>
<th>Access Type</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
<tbody>
@foreach(var lnd in Model.LNDServices)
{
<tr>
<td>@lnd.Crypto</td>
<td>@lnd.Type</td>
<td style="text-align:right">
<a asp-action="LNDGRPCServices" asp-route-cryptoCode="@lnd.Crypto">See information</a>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
</div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}

@ -3,6 +3,7 @@
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Rates)" asp-action="Rates">Rates</a>
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Emails)" asp-action="Emails">Email server</a>
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Policies)" asp-action="Policies">Policies</a>
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Services)" asp-action="Services">Services</a>
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Theme)" asp-action="Theme">Theme</a>
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Hangfire)" href="~/hangfire" target="_blank">Hangfire</a>
</div>

@ -5,7 +5,7 @@
}
<h4>@ViewData["Title"]</h4>
<div class="alert alert-danger alert-dismissible" style="display:none;" role="alert">
<div id="walletAlert" class="alert alert-danger alert-dismissible" style="display:none;" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<span id="alertMessage"></span>
</div>

@ -6,11 +6,11 @@
var cryptoCode = $("#cryptoCode").val();
function WriteAlert(type, message) {
$(".alert").removeClass("alert-danger");
$(".alert").removeClass("alert-warning");
$(".alert").removeClass("alert-success");
$(".alert").addClass("alert-" + type);
$(".alert").css("display", "block");
$("#walletAlert").removeClass("alert-danger");
$("#walletAlert").removeClass("alert-warning");
$("#walletAlert").removeClass("alert-success");
$("#walletAlert").addClass("alert-" + type);
$("#walletAlert").css("display", "block");
$("#alertMessage").text(message);
}

@ -32,6 +32,7 @@ In addition to Bitcoin, we support the following crypto currencies:
* Monacoin
* Polis
* UFO
* Viacoin
## Documentation