Compare commits

..

22 Commits

Author SHA1 Message Date
bf7ae178ef Fix #18, fix electrum format not recognizing standard p2pkh on testnet 2017-12-17 02:28:37 +09:00
dc7f96c6da Show a modal when node is synching 2017-12-17 02:07:11 +09:00
c6959bb0bc Can start without NBXplorer being ready 2017-12-17 01:04:20 +09:00
d4dd6c84bc Auto detect NGinx X-Forwarded 2017-12-15 19:11:48 +09:00
e59678360c Update background 2017-12-13 22:38:07 +09:00
1b6fa0c7d8 Prepare Eclair integration 2017-12-13 15:49:19 +09:00
95a5936daf Update youtube links 2017-12-11 18:03:06 +09:00
477d4117ce update slack invite site 2017-12-08 22:04:52 +09:00
444f119e50 Add twitter link 2017-12-08 17:02:10 +09:00
fa13a2874e Estimate rate with BTCPay if BitcoinAverage stops works 2017-12-08 15:04:47 +09:00
24ce325e31 Support electrum segwit xpub format 2017-12-06 18:08:21 +09:00
a52a1901c4 Can delete user 2017-12-04 14:39:02 +09:00
45aee607e3 Can lock down registrations 2017-12-04 00:55:39 +09:00
c263016939 fix help 2017-12-03 23:42:10 +09:00
741915b1f8 Allow filtering of invoices over storeid and status 2017-12-03 23:35:52 +09:00
6f2534ba82 Can set currency in the create invoice form fix #15 2017-12-03 22:36:04 +09:00
43635071d9 Show ISO code in checkout page 2017-12-03 22:14:08 +09:00
22f06ecd4e Can set store policy to define how much time to wait before passing a transaction from paid to invalid. 2017-12-03 14:43:52 +09:00
7efe83eba8 notify on invalid in fullnotification is true 2017-12-03 13:42:12 +09:00
a5b732e197 Update NBitcoin, NBxplorer and bump 2017-12-03 01:56:26 +09:00
f404aaf768 bump 2017-12-02 23:22:47 +09:00
e1f8177834 Can configure externalurl in case BTCPay is behind a reverse proxy 2017-12-02 23:22:23 +09:00
49 changed files with 1986 additions and 431 deletions

View File

@ -7,9 +7,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.3.0-preview-20170720-02" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0" />
<PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
</ItemGroup>
<ItemGroup>

View File

@ -106,19 +106,21 @@ namespace BTCPayServer.Tests
.UseStartup<Startup>()
.Build();
_Host.Start();
Runtime = (BTCPayServerRuntime)_Host.Services.GetService(typeof(BTCPayServerRuntime));
var watcher = (InvoiceWatcher)_Host.Services.GetService(typeof(InvoiceWatcher));
}
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
public BTCPayServerRuntime Runtime
{
get; set;
var waiter = ((NBXplorerWaiterAccessor)_Host.Services.GetService(typeof(NBXplorerWaiterAccessor))).Instance;
while(waiter.State != NBXplorerState.Ready)
{
Thread.Sleep(10);
}
}
public string HostName
{
get;
internal set;
}
public InvoiceRepository InvoiceRepository { get; private set; }
public T GetService<T>()
{

View File

@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Eclair;
namespace BTCPayServer.Tests
{
public class EclairTester
{
ServerTester parent;
public EclairTester(ServerTester parent, string environmentName, string defaultRPC, string defaultHost)
{
this.parent = parent;
RPC = new EclairRPCClient(new Uri(parent.GetEnvironment(environmentName, defaultRPC)), parent.Network);
P2PHost = parent.GetEnvironment(environmentName + "_HOST", defaultHost);
}
public EclairRPCClient RPC { get; }
public string P2PHost { get; }
NodeInfo _NodeInfo;
public async Task<NodeInfo> GetNodeInfoAsync()
{
if (_NodeInfo != null)
return _NodeInfo;
var info = await RPC.GetInfoAsync();
_NodeInfo = new NodeInfo(info.NodeId, P2PHost, info.Port);
return _NodeInfo;
}
public NodeInfo GetNodeInfo()
{
return GetNodeInfoAsync().GetAwaiter().GetResult();
}
}
}

View File

@ -1,4 +1,5 @@
using BTCPayServer.Controllers;
using System.Linq;
using BTCPayServer.Models.AccountViewModels;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@ -16,6 +17,7 @@ using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using BTCPayServer.Eclair;
namespace BTCPayServer.Tests
{
@ -56,9 +58,42 @@ namespace BTCPayServer.Tests
PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString()));
PayTester.HostName = GetEnvironment("TESTS_HOSTNAME", "127.0.0.1");
PayTester.Start();
MerchantEclair = new EclairTester(this, "TEST_ECLAIR1", "http://127.0.0.1:30992/", "eclair1");
CustomerEclair = new EclairTester(this, "TEST_ECLAIR2", "http://127.0.0.1:30993/", "eclair2");
}
private string GetEnvironment(string variable, string defaultValue)
/// <summary>
/// This will setup a channel going from customer to merchant
/// </summary>
public void PrepareLightning()
{
PrepareLightningAsync().GetAwaiter().GetResult();
}
public async Task PrepareLightningAsync()
{
// Activate segwit
var blockCount = ExplorerNode.GetBlockCountAsync();
// Fetch node info, but that in cache
var merchant = MerchantEclair.GetNodeInfoAsync();
var customer = CustomerEclair.GetNodeInfoAsync();
var channels = CustomerEclair.RPC.ChannelsAsync();
var connect = CustomerEclair.RPC.ConnectAsync(merchant.Result);
await Task.WhenAll(blockCount, merchant, customer, channels, connect);
// Mine until segwit is activated
if (blockCount.Result <= 432)
{
ExplorerNode.Generate(433 - blockCount.Result);
}
}
public EclairTester MerchantEclair { get; set; }
public EclairTester CustomerEclair { get; set; }
internal string GetEnvironment(string variable, string defaultValue)
{
var var = Environment.GetEnvironmentVariable(variable);
return String.IsNullOrEmpty(var) ? defaultValue : var;

View File

@ -22,6 +22,7 @@ using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using BTCPayServer.Services.Rates;
using Microsoft.Extensions.Caching.Memory;
using BTCPayServer.Eclair;
namespace BTCPayServer.Tests
{
@ -115,6 +116,24 @@ namespace BTCPayServer.Tests
}
}
[Fact]
public void CanUseLightMoney()
{
var light = LightMoney.MilliSatoshis(1);
Assert.Equal("0.00000000001", light.ToString());
}
[Fact]
public void CanSendLightningPayment()
{
using (var tester = ServerTester.Create())
{
tester.Start();
tester.PrepareLightning();
}
}
[Fact]
public void CanUseServerInitiatedPairingCode()
{
@ -251,6 +270,17 @@ namespace BTCPayServer.Tests
}
}
[Fact]
public void CanParseFilter()
{
var filter = "storeid:abc status:abed blabhbalh ";
var search = new SearchString(filter);
Assert.Equal("storeid:abc status:abed blabhbalh", search.ToString());
Assert.Equal("blabhbalh", search.TextSearch);
Assert.Equal("abc", search.Filters["storeid"]);
Assert.Equal("abed", search.Filters["status"]);
}
[Fact]
public void InvoiceFlowThroughDifferentStatesCorrectly()
{
@ -277,13 +307,13 @@ namespace BTCPayServer.Tests
Eventually(() =>
{
var textSearchResult = tester.PayTester.Runtime.InvoiceRepository.GetInvoices(new InvoiceQuery()
var textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
{
StoreId = user.StoreId,
TextSearch = invoice.OrderId
}).GetAwaiter().GetResult();
Assert.Equal(1, textSearchResult.Length);
textSearchResult = tester.PayTester.Runtime.InvoiceRepository.GetInvoices(new InvoiceQuery()
textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
{
StoreId = user.StoreId,
TextSearch = invoice.Id

View File

@ -1,5 +1,8 @@
version: "3"
# Run `docker-compose up dev` for bootstrapping your development environment
# Doing so will expose eclair API, NBXplorer, Bitcoind RPC and postgres port to the host so that tests can Run,
# The Visual Studio launch setting `Docker-Regtest` is configured to use this environment.
services:
tests:
@ -10,18 +13,36 @@ services:
TESTS_RPCCONNECTION: server=http://bitcoind:43782;ceiwHEbqWI83:DwubwWsoo3
TESTS_NBXPLORERURL: http://nbxplorer:32838/
TESTS_POSTGRES: User ID=postgres;Host=postgres;Port=5432;Database=btcpayserver
TESTS_FAKECALLBACK: 'true'
TESTS_FAKECALLBACK: 'false'
TESTS_PORT: 80
TESTS_HOSTNAME: tests
TEST_ECLAIR1: http://eclair1:8080/
TEST_ECLAIR2: http://eclair2:8080/
expose:
- "80"
links:
- bitcoind
- nbxplorer
- eclair1
- eclair2
extra_hosts:
- "tests:127.0.0.1"
# The dev container is not actually used, it is just handy to run `docker-compose up dev` to start all services
dev:
image: nicolasdorier/docker-bitcoin:0.15.0.1
environment:
BITCOIN_EXTRA_ARGS: |
regtest=1
connect=bitcoind:39388
links:
- bitcoind
- nbxplorer
- eclair1
- eclair2
nbxplorer:
image: nicolasdorier/nbxplorer:1.0.0.28
image: nicolasdorier/nbxplorer:1.0.0.32
ports:
- "32838:32838"
expose:
@ -39,25 +60,73 @@ services:
- bitcoind
- postgres
eclair1:
image: acinq/eclair:latest
environment:
JAVA_OPTS: >
-Xmx512m
-Declair.printToConsole
-Declair.bitcoind.host=bitcoind
-Declair.bitcoind.rpcport=43782
-Declair.bitcoind.rpcuser=ceiwHEbqWI83
-Declair.bitcoind.rpcpassword=DwubwWsoo3
-Declair.bitcoind.zmq=tcp://bitcoind:29000
-Declair.chain=regtest
-Declair.api.binding-ip=0.0.0.0
links:
- bitcoind
ports:
- "30992:8080" # api port
expose:
- "9735" # server port
- "8080" # api port
eclair2:
image: acinq/eclair:latest
environment:
JAVA_OPTS: >
-Xmx512m
-Declair.printToConsole
-Declair.bitcoind.host=bitcoind
-Declair.bitcoind.rpcport=43782
-Declair.bitcoind.rpcuser=ceiwHEbqWI83
-Declair.bitcoind.rpcpassword=DwubwWsoo3
-Declair.bitcoind.zmq=tcp://bitcoind:29000
-Declair.chain=regtest
-Declair.api.binding-ip=0.0.0.0
links:
- bitcoind
ports:
- "30993:8080" # api port
expose:
- "9735" # server port
- "8080" # api port
bitcoind:
container_name: btcpayserver_dev_bitcoind
image: nicolasdorier/docker-bitcoin:0.15.0.1
ports:
- "43782:43782"
- "39388:39388"
environment:
BITCOIN_EXTRA_ARGS: |
rpcuser=ceiwHEbqWI83
rpcpassword=DwubwWsoo3
regtest=1
server=1
rpcport=43782
port=39388
whitelist=0.0.0.0/0
zmqpubrawblock=tcp://0.0.0.0:29000
zmqpubrawtx=tcp://0.0.0.0:29000
txindex=1
ports:
- "43782:43782" # RPC
expose:
- "43782"
- "39388"
- "43782" # RPC
- "39388" # P2P
- "29000" # zmq
postgres:
image: postgres:9.6.5
ports:
- "39372:5432"
expose:
- "5432"

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.0.0.30</Version>
<Version>1.0.0.40</Version>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Build\dockerfiles\**" />
@ -18,13 +18,13 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Hangfire" Version="1.6.17" />
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.1" />
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
<PackageReference Include="NBitcoin" Version="4.0.0.41" />
<PackageReference Include="NBitpayClient" Version="1.0.0.12" />
<PackageReference Include="NBitcoin" Version="4.0.0.50" />
<PackageReference Include="NBitpayClient" Version="1.0.0.13" />
<PackageReference Include="DBreeze" Version="1.87.0" />
<PackageReference Include="NBXplorer.Client" Version="1.0.0.18" />
<PackageReference Include="NBXplorer.Client" Version="1.0.0.20" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.1" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.2" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.13" />
@ -34,9 +34,9 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.1" PrivateAssets="All" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.1" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>

View File

@ -55,18 +55,20 @@ namespace BTCPayServer.Configuration
Explorer = conf.GetOrDefault<Uri>("explorer.url", networkInfo.DefaultExplorerUrl);
CookieFile = conf.GetOrDefault<string>("explorer.cookiefile", networkInfo.DefaultExplorerCookieFile);
RequireHttps = conf.GetOrDefault<bool>("requirehttps", false);
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
}
public bool RequireHttps
{
get; set;
ExternalUrl = conf.GetOrDefault<Uri>("externalurl", null);
InternalUrl = conf.GetOrDefault<Uri>("internalurl", null);
}
public string PostgresConnectionString
{
get;
set;
}
public Uri ExternalUrl
{
get;
set;
}
public Uri InternalUrl { get; private set; }
}
}

View File

@ -1,110 +0,0 @@
using BTCPayServer.Authentication;
using Microsoft.Extensions.Logging;
using BTCPayServer.Logging;
using DBreeze;
using NBitcoin;
using NBXplorer;
using NBXplorer.DerivationStrategy;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using BTCPayServer.Data;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Wallets;
namespace BTCPayServer.Configuration
{
public class BTCPayServerRuntime : IDisposable
{
public ExplorerClient Explorer
{
get;
private set;
}
public void Configure(BTCPayServerOptions opts)
{
ConfigureAsync(opts).GetAwaiter().GetResult();
}
public async Task ConfigureAsync(BTCPayServerOptions opts)
{
Network = opts.Network;
Explorer = new ExplorerClient(opts.Network, opts.Explorer);
if (!Explorer.SetCookieAuth(opts.CookieFile))
Explorer.SetNoAuth();
CancellationTokenSource cts = new CancellationTokenSource(30000);
try
{
Logs.Configuration.LogInformation("Trying to connect to explorer " + Explorer.Address.AbsoluteUri);
await Explorer.WaitServerStartedAsync(cts.Token).ConfigureAwait(false);
Logs.Configuration.LogInformation("Connection successfull");
}
catch (Exception ex)
{
throw new ConfigException($"Could not connect to NBXplorer, {ex.Message}");
}
ApplicationDbContextFactory dbContext = null;
if (opts.PostgresConnectionString == null)
{
var connStr = "Data Source=" + Path.Combine(opts.DataDir, "sqllite.db");
Logs.Configuration.LogInformation($"SQLite DB used ({connStr})");
dbContext = new ApplicationDbContextFactory(DatabaseType.Sqlite, connStr);
}
else
{
Logs.Configuration.LogInformation($"Postgres DB used ({opts.PostgresConnectionString})");
dbContext = new ApplicationDbContextFactory(DatabaseType.Postgres, opts.PostgresConnectionString);
}
DBFactory = dbContext;
InvoiceRepository = new InvoiceRepository(dbContext, CreateDBPath(opts, "InvoiceDB"), Network);
_Resources.Add(InvoiceRepository);
}
private static string CreateDBPath(BTCPayServerOptions opts, string name)
{
var dbpath = Path.Combine(opts.DataDir, name);
if (!Directory.Exists(dbpath))
Directory.CreateDirectory(dbpath);
return dbpath;
}
List<IDisposable> _Resources = new List<IDisposable>();
public void Dispose()
{
lock (_Resources)
{
foreach (var r in _Resources)
{
r.Dispose();
}
_Resources.Clear();
}
}
public Network Network
{
get;
private set;
}
public InvoiceRepository InvoiceRepository
{
get;
set;
}
public ApplicationDbContextFactory DBFactory
{
get;
set;
}
}
}

View File

@ -19,18 +19,18 @@ namespace BTCPayServer.Configuration
{
CommandLineApplication app = new CommandLineApplication(true)
{
FullName = "NBXplorer\r\nLightweight block explorer for tracking HD wallets",
Name = "NBXplorer"
FullName = "BTCPay\r\nOpen source, self-hosted payment processor.",
Name = "BTCPay"
};
app.HelpOption("-? | -h | --help");
app.Option("-n | --network", $"Set the network among ({NetworkInformation.ToStringAll()}) (default: {Network.Main.ToString()})", CommandOptionType.SingleValue);
app.Option("--testnet | -testnet", $"Use testnet", CommandOptionType.BoolValue);
app.Option("--regtest | -regtest", $"Use regtest", CommandOptionType.BoolValue);
app.Option("--requirehttps", $"Will redirect to https version of the website (default: false)", CommandOptionType.BoolValue);
app.Option("--postgres", $"Connection string to postgres database (default: sqlite is used)", CommandOptionType.SingleValue);
app.Option("--explorerurl", $"Url of the NBxplorer (default: : Default setting of NBXplorer for the network)", CommandOptionType.SingleValue);
app.Option("--explorercookiefile", $"Path to the cookie file (default: Default setting of NBXplorer for the network)", CommandOptionType.SingleValue);
app.Option("--externalurl", $"The expected external url of this service, to use if BTCPay is behind a reverse proxy (default: empty, use the incoming HTTP request to figure out)", CommandOptionType.SingleValue);
app.Option("--internalurl", $"The expected internal url of this service, this set NBXplorer callback addresses (default: empty, use the incoming HTTP request to figure out)", CommandOptionType.SingleValue);
return app;
}
@ -77,7 +77,6 @@ namespace BTCPayServer.Configuration
builder.AppendLine("#regtest=0");
builder.AppendLine();
builder.AppendLine("### Server settings ###");
builder.AppendLine("#requirehttps=0");
builder.AppendLine("#port=" + network.DefaultPort);
builder.AppendLine("#bind=127.0.0.1");
builder.AppendLine();

View File

@ -235,8 +235,11 @@ namespace BTCPayServer.Controllers
[HttpGet]
[AllowAnonymous]
public IActionResult Register(string returnUrl = null)
public async Task<IActionResult> Register(string returnUrl = null)
{
var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
if (policies.LockSubscription)
return RedirectToAction(nameof(HomeController.Index), "Home");
ViewData["ReturnUrl"] = returnUrl;
return View();
}
@ -247,9 +250,11 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
if (policies.LockSubscription)
return RedirectToAction(nameof(HomeController.Index), "Home");
if (ModelState.IsValid)
{
var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
var user = new ApplicationUser { UserName = model.Email, Email = model.Email, RequiresEmailConfirmation = policies.RequiresConfirmedEmail };
var result = await _userManager.CreateAsync(user, model.Password);
if (result.Succeeded)

View File

@ -15,6 +15,7 @@ using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Configuration;
namespace BTCPayServer.Controllers
{
@ -31,16 +32,19 @@ namespace BTCPayServer.Controllers
Network _Network;
InvoiceWatcher _Watcher;
ExplorerClient _Explorer;
BTCPayServerOptions _Options;
public CallbackController(SettingsRepository repo,
ExplorerClient explorer,
InvoiceWatcher watcher,
InvoiceWatcherAccessor watcher,
BTCPayServerOptions options,
Network network)
{
_Settings = repo;
_Network = network;
_Watcher = watcher;
_Watcher = watcher.Instance;
_Explorer = explorer;
_Options = options;
}
[Route("callbacks/transactions")]
@ -79,7 +83,7 @@ namespace BTCPayServer.Controllers
public async Task<Uri> GetCallbackUriAsync(HttpRequest request)
{
string token = await GetToken();
return new Uri(request.GetAbsoluteRoot() + "/callbacks/transactions?token=" + token);
return BuildCallbackUri(request, "callbacks/transactions?token=" + token);
}
public async Task RegisterCallbackUriAsync(DerivationStrategyBase derivationScheme, HttpRequest request)
@ -103,12 +107,18 @@ namespace BTCPayServer.Controllers
public async Task<Uri> GetCallbackBlockUriAsync(HttpRequest request)
{
string token = await GetToken();
return new Uri(request.GetAbsoluteRoot() + "/callbacks/blocks?token=" + token);
return BuildCallbackUri(request, "callbacks/blocks?token=" + token);
}
public async Task<Uri> RegisterCallbackBlockUriAsync(HttpRequest request)
private Uri BuildCallbackUri(HttpRequest request, string callbackPath)
{
string baseUrl = _Options.InternalUrl == null ? request.GetAbsoluteRoot() : _Options.InternalUrl.AbsolutePath;
baseUrl = baseUrl.WithTrailingSlash();
return new Uri(baseUrl + callbackPath);
}
public async Task<Uri> RegisterCallbackBlockUriAsync(Uri uri)
{
var uri = await GetCallbackBlockUriAsync(request);
await _Explorer.SubscribeToBlocksAsync(uri);
return uri;
}

View File

@ -15,6 +15,7 @@ using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
namespace BTCPayServer.Controllers
{
@ -118,8 +119,7 @@ namespace BTCPayServer.Controllers
return null;
var store = await _StoreRepository.FindStore(invoice.StoreId);
var dto = invoice.EntityToDTO();
var cryptoFormat = _CurrencyNameTable.GetCurrencyProvider("BTC");
var currency = invoice.ProductInformation.Currency;
var model = new PaymentModel()
{
ServerUrl = HttpContext.Request.GetAbsoluteRoot(),
@ -133,7 +133,7 @@ namespace BTCPayServer.Controllers
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds,
ItemDesc = invoice.ProductInformation.ItemDesc,
Rate = invoice.Rate.ToString("C", _CurrencyNameTable.GetCurrencyProvider(invoice.ProductInformation.Currency)),
Rate = invoice.Rate.ToString("C", _CurrencyNameTable.GetCurrencyProvider(currency)) + $" ({currency})",
MerchantRefLink = invoice.RedirectURL ?? "/",
StoreName = store.StoreName,
TxFees = invoice.TxFee.ToString(),
@ -188,12 +188,15 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 20)
{
var model = new InvoicesModel();
var filterString = new SearchString(searchTerm);
foreach (var invoice in await _InvoiceRepository.GetInvoices(new InvoiceQuery()
{
TextSearch = searchTerm,
TextSearch = filterString.TextSearch,
Count = count,
Skip = skip,
UserId = GetUserId()
UserId = GetUserId(),
Status = filterString.Filters.TryGet("status"),
StoreId = filterString.Filters.TryGet("storeid")
}))
{
model.SearchTerm = searchTerm;
@ -232,9 +235,9 @@ namespace BTCPayServer.Controllers
[BitpayAPIConstraint(false)]
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model)
{
model.Stores = await GetStores(GetUserId(), model.StoreId);
if (!ModelState.IsValid)
{
model.Stores = await GetStores(GetUserId(), model.StoreId);
return View(model);
}
var store = await _StoreRepository.FindStore(model.StoreId, GetUserId());
@ -246,21 +249,30 @@ namespace BTCPayServer.Controllers
storeId = store.Id
});
}
var result = await CreateInvoiceCore(new Invoice()
{
Price = model.Amount.Value,
Currency = "USD",
PosData = model.PosData,
OrderId = model.OrderId,
//RedirectURL = redirect + "redirect",
NotificationURL = model.NotificationUrl,
ItemDesc = model.ItemDesc,
FullNotifications = true,
BuyerEmail = model.BuyerEmail,
}, store, HttpContext.Request.GetAbsoluteRoot());
StatusMessage = $"Invoice {result.Data.Id} just created!";
return RedirectToAction(nameof(ListInvoices));
try
{
var result = await CreateInvoiceCore(new Invoice()
{
Price = model.Amount.Value,
Currency = model.Currency,
PosData = model.PosData,
OrderId = model.OrderId,
//RedirectURL = redirect + "redirect",
NotificationURL = model.NotificationUrl,
ItemDesc = model.ItemDesc,
FullNotifications = true,
BuyerEmail = model.BuyerEmail,
}, store, HttpContext.Request.GetAbsoluteRoot());
StatusMessage = $"Invoice {result.Data.Id} just created!";
return RedirectToAction(nameof(ListInvoices));
}
catch (RateUnavailableException)
{
ModelState.TryAddModelError(nameof(model.Currency), "Unsupported currency");
return View(model);
}
}
private async Task<SelectList> GetStores(string userId, string storeId = null)

View File

@ -62,7 +62,7 @@ namespace BTCPayServer.Controllers
BTCPayWallet wallet,
IRateProvider rateProvider,
StoreRepository storeRepository,
InvoiceWatcher watcher,
InvoiceWatcherAccessor watcher,
ExplorerClient explorerClient,
IFeeProvider feeProvider)
{
@ -73,12 +73,12 @@ namespace BTCPayServer.Controllers
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
_Wallet = wallet ?? throw new ArgumentNullException(nameof(wallet));
_RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider));
_Watcher = watcher ?? throw new ArgumentNullException(nameof(watcher));
_Watcher = (watcher ?? throw new ArgumentNullException(nameof(watcher))).Instance;
_UserManager = userManager;
_FeeProvider = feeProvider ?? throw new ArgumentNullException(nameof(feeProvider));
}
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl, double expiryMinutes = 15, double monitoringMinutes = 60)
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl, double expiryMinutes = 15)
{
//TODO: expiryMinutes (time before a new invoice can become paid) and monitoringMinutes (time before a paid invoice becomes invalid) should be configurable at store level
var derivationStrategy = store.DerivationStrategy;
@ -87,12 +87,13 @@ namespace BTCPayServer.Controllers
InvoiceTime = DateTimeOffset.UtcNow,
DerivationStrategy = derivationStrategy ?? throw new BitpayHttpException(400, "This store has not configured the derivation strategy")
};
var storeBlob = store.GetStoreBlob(_Network);
Uri notificationUri = Uri.IsWellFormedUriString(invoice.NotificationURL, UriKind.Absolute) ? new Uri(invoice.NotificationURL, UriKind.Absolute) : null;
if (notificationUri == null || (notificationUri.Scheme != "http" && notificationUri.Scheme != "https")) //TODO: Filer non routable addresses ?
notificationUri = null;
EmailAddressAttribute emailValidator = new EmailAddressAttribute();
entity.ExpirationTime = entity.InvoiceTime.AddMinutes(expiryMinutes);
entity.MonitoringExpiration = entity.InvoiceTime.AddMinutes(monitoringMinutes);
entity.MonitoringExpiration = entity.ExpirationTime + TimeSpan.FromMinutes(storeBlob.MonitoringExpiration);
entity.OrderId = invoice.OrderId;
entity.ServerUrl = serverUrl;
entity.FullNotifications = invoice.FullNotifications;
@ -114,7 +115,7 @@ namespace BTCPayServer.Controllers
var getFeeRate = _FeeProvider.GetFeeRateAsync();
var getRate = _RateProvider.GetRateAsync(invoice.Currency);
var getAddress = _Wallet.ReserveAddressAsync(ParseDerivationStrategy(derivationStrategy));
entity.TxFee = store.GetStoreBlob(_Network).NetworkFeeDisabled ? Money.Zero : (await getFeeRate).GetFee(100); // assume price for 100 bytes
entity.TxFee = storeBlob.NetworkFeeDisabled ? Money.Zero : (await getFeeRate).GetFee(100); // assume price for 100 bytes
entity.Rate = (double)await getRate;
entity.PosData = invoice.PosData;
entity.DepositAddress = await getAddress;

View File

@ -32,15 +32,50 @@ namespace BTCPayServer.Controllers
public IActionResult ListUsers()
{
var users = new UsersViewModel();
users.StatusMessage = StatusMessage;
users.Users
= _UserManager.Users.Select(u => new UsersViewModel.UserViewModel()
{
Name = u.UserName,
Email = u.Email
Email = u.Email,
Id = u.Id
}).ToList();
return View(users);
}
[Route("server/users/{userId}/delete")]
public async Task<IActionResult> DeleteUser(string userId)
{
var user = userId == null ? null : await _UserManager.FindByIdAsync(userId);
if (user == null)
return NotFound();
return View("Confirm", new ConfirmModel()
{
Title = "Delete user " + user.Email,
Description = "This user will be permanently deleted",
Action = "Delete"
});
}
[Route("server/users/{userId}/delete")]
[HttpPost]
public async Task<IActionResult> DeleteUserPost(string userId)
{
var user = userId == null ? null : await _UserManager.FindByIdAsync(userId);
if (user == null)
return NotFound();
await _UserManager.DeleteAsync(user);
StatusMessage = "User deleted";
return RedirectToAction(nameof(ListUsers));
}
[TempData]
public string StatusMessage
{
get; set;
}
[Route("server/emails")]
public async Task<IActionResult> Emails()
{

View File

@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitpayClient;
using NBXplorer.DerivationStrategy;
using System;
@ -92,7 +93,7 @@ namespace BTCPayServer.Controllers
StoresViewModel result = new StoresViewModel();
result.StatusMessage = StatusMessage;
var stores = await _Repo.GetStoresByUserId(GetUserId());
var balances = stores.Select(async s => string.IsNullOrEmpty(s.DerivationStrategy) ? Money.Zero : await _Wallet.GetBalance(ParseDerivationStrategy(s.DerivationStrategy))).ToArray();
var balances = stores.Select(async s => string.IsNullOrEmpty(s.DerivationStrategy) ? Money.Zero : await _Wallet.GetBalance(ParseDerivationStrategy(s.DerivationStrategy, null))).ToArray();
for (int i = 0; i < stores.Length; i++)
{
@ -144,13 +145,16 @@ namespace BTCPayServer.Controllers
if (store == null)
return NotFound();
var storeBlob = store.GetStoreBlob(_Network);
var vm = new StoreViewModel();
vm.Id = store.Id;
vm.StoreName = store.StoreName;
vm.StoreWebsite = store.StoreWebsite;
vm.NetworkFee = !store.GetStoreBlob(_Network).NetworkFeeDisabled;
vm.NetworkFee = !storeBlob.NetworkFeeDisabled;
vm.SpeedPolicy = store.SpeedPolicy;
vm.DerivationScheme = store.DerivationStrategy;
vm.StatusMessage = StatusMessage;
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
return View(vm);
}
@ -192,7 +196,7 @@ namespace BTCPayServer.Controllers
{
if (!string.IsNullOrEmpty(model.DerivationScheme))
{
var strategy = ParseDerivationStrategy(model.DerivationScheme);
var strategy = ParseDerivationStrategy(model.DerivationScheme, model.DerivationSchemeFormat);
await _Wallet.TrackAsync(strategy);
await _CallbackController.RegisterCallbackUriAsync(strategy, Request);
}
@ -205,11 +209,12 @@ namespace BTCPayServer.Controllers
}
}
if (store.GetStoreBlob(_Network).NetworkFeeDisabled != !model.NetworkFee)
var blob = store.GetStoreBlob(_Network);
blob.NetworkFeeDisabled = !model.NetworkFee;
blob.MonitoringExpiration = model.MonitoringExpiration;
if (store.SetStoreBlob(blob, _Network))
{
var blob = store.GetStoreBlob(_Network);
blob.NetworkFeeDisabled = !model.NetworkFee;
store.SetStoreBlob(blob, _Network);
needUpdate = true;
}
@ -226,21 +231,62 @@ namespace BTCPayServer.Controllers
}
else
{
var facto = new DerivationStrategyFactory(_Network);
var scheme = facto.Parse(model.DerivationScheme);
var line = scheme.GetLineFor(DerivationFeature.Deposit);
for (int i = 0; i < 10; i++)
if (!string.IsNullOrEmpty(model.DerivationScheme))
{
var address = line.Derive((uint)i);
model.AddressSamples.Add((line.Path.Derive((uint)i).ToString(), address.ScriptPubKey.GetDestinationAddress(_Network).ToString()));
try
{
var scheme = ParseDerivationStrategy(model.DerivationScheme, model.DerivationSchemeFormat);
var line = scheme.GetLineFor(DerivationFeature.Deposit);
for (int i = 0; i < 10; i++)
{
var address = line.Derive((uint)i);
model.AddressSamples.Add((line.Path.Derive((uint)i).ToString(), address.ScriptPubKey.GetDestinationAddress(_Network).ToString()));
}
}
catch
{
ModelState.AddModelError(nameof(model.DerivationScheme), "Invalid Derivation Scheme");
}
}
return View(model);
}
}
private DerivationStrategyBase ParseDerivationStrategy(string derivationScheme)
private DerivationStrategyBase ParseDerivationStrategy(string derivationScheme, string format)
{
if (format == "Electrum")
{
//Unsupported Electrum
//var p2wsh_p2sh = 0x295b43fU;
//var p2wsh = 0x2aa7ed3U;
Dictionary<uint, string[]> electrumMapping = new Dictionary<uint, string[]>();
//Source https://github.com/spesmilo/electrum/blob/9edffd17542de5773e7284a8c8a2673c766bb3c3/lib/bitcoin.py
var standard = _Network == Network.Main ? 0x0488b21eU : 0x043587cf;
electrumMapping.Add(standard, new[] { "legacy" });
var p2wpkh_p2sh = 0x049d7cb2U;
electrumMapping.Add(p2wpkh_p2sh, new string[] { "p2sh" });
var p2wpkh = 0x4b24746U;
electrumMapping.Add(p2wpkh, new string[] { });
var data = Encoders.Base58Check.DecodeData(derivationScheme);
if (data.Length < 4)
throw new FormatException("data.Length < 4");
var prefix = Utils.ToUInt32(data, false);
if (!electrumMapping.TryGetValue(prefix, out string[] labels))
throw new FormatException("!electrumMapping.TryGetValue(prefix, out string[] labels)");
var standardPrefix = Utils.ToBytes(standard, false);
for (int i = 0; i < 4; i++)
data[i] = standardPrefix[i];
derivationScheme = new BitcoinExtPubKey(Encoders.Base58Check.EncodeData(data), _Network).ToString();
foreach (var label in labels)
{
derivationScheme = derivationScheme + $"-[{label}]";
}
}
return new DerivationStrategyFactory(_Network).Parse(derivationScheme);
}

View File

@ -8,6 +8,8 @@ using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel;
using Newtonsoft.Json;
namespace BTCPayServer.Data
{
@ -65,17 +67,33 @@ namespace BTCPayServer.Data
return StoreBlob == null ? new StoreBlob() : new Serializer(network).ToObject<StoreBlob>(Encoding.UTF8.GetString(StoreBlob));
}
public void SetStoreBlob(StoreBlob storeBlob, Network network)
public bool SetStoreBlob(StoreBlob storeBlob, Network network)
{
StoreBlob = Encoding.UTF8.GetBytes(new Serializer(network).ToString(storeBlob));
var original = new Serializer(network).ToString(GetStoreBlob(network));
var newBlob = new Serializer(network).ToString(storeBlob);
if (original == newBlob)
return false;
StoreBlob = Encoding.UTF8.GetBytes(newBlob);
return true;
}
}
public class StoreBlob
{
public StoreBlob()
{
MonitoringExpiration = 60;
}
public bool NetworkFeeDisabled
{
get; set;
}
[DefaultValue(60)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public int MonitoringExpiration
{
get;
set;
}
}
}

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Eclair
{
public class AllChannelResponse
{
public string ShortChannelId { get; set; }
public string NodeId1 { get; set; }
public string NodeId2 { get; set; }
}
}

View File

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Eclair
{
public class ChannelResponse
{
public string NodeId { get; set; }
public string ChannelId { get; set; }
public string State { get; set; }
}
public static class ChannelStates
{
public const string WAIT_FOR_FUNDING_CONFIRMED = "WAIT_FOR_FUNDING_CONFIRMED";
public const string NORMAL = "NORMAL";
}
}

View File

@ -0,0 +1,230 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using NBitcoin;
using NBitcoin.JsonConverters;
using NBitcoin.RPC;
namespace BTCPayServer.Eclair
{
public class EclairRPCClient
{
public EclairRPCClient(Uri address, Network network)
{
if (address == null)
throw new ArgumentNullException(nameof(address));
if (network == null)
throw new ArgumentNullException(nameof(network));
Address = address;
Network = network;
}
public Network Network { get; private set; }
public GetInfoResponse GetInfo()
{
return GetInfoAsync().GetAwaiter().GetResult();
}
public Task<GetInfoResponse> GetInfoAsync()
{
return SendCommandAsync<GetInfoResponse>(new RPCRequest("getinfo", new object[] { }));
}
public async Task<T> SendCommandAsync<T>(RPCRequest request, bool throwIfRPCError = true)
{
var response = await SendCommandAsync(request, throwIfRPCError);
return Serializer.ToObject<T>(response.ResultString, Network);
}
public async Task<RPCResponse> SendCommandAsync(RPCRequest request, bool throwIfRPCError = true)
{
RPCResponse response = null;
HttpWebRequest webRequest = response == null ? CreateWebRequest() : null;
if (response == null)
{
var writer = new StringWriter();
request.WriteJSON(writer);
writer.Flush();
var json = writer.ToString();
var bytes = Encoding.UTF8.GetBytes(json);
#if !(PORTABLE || NETCORE)
webRequest.ContentLength = bytes.Length;
#endif
var dataStream = await webRequest.GetRequestStreamAsync().ConfigureAwait(false);
await dataStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
await dataStream.FlushAsync().ConfigureAwait(false);
dataStream.Dispose();
}
WebResponse webResponse = null;
WebResponse errorResponse = null;
try
{
webResponse = response == null ? await webRequest.GetResponseAsync().ConfigureAwait(false) : null;
response = response ?? RPCResponse.Load(await ToMemoryStreamAsync(webResponse.GetResponseStream()).ConfigureAwait(false));
if (throwIfRPCError)
response.ThrowIfError();
}
catch (WebException ex)
{
if (ex.Response == null || ex.Response.ContentLength == 0 ||
!ex.Response.ContentType.Equals("application/json", StringComparison.Ordinal))
throw;
errorResponse = ex.Response;
response = RPCResponse.Load(await ToMemoryStreamAsync(errorResponse.GetResponseStream()).ConfigureAwait(false));
if (throwIfRPCError)
response.ThrowIfError();
}
finally
{
if (errorResponse != null)
{
errorResponse.Dispose();
errorResponse = null;
}
if (webResponse != null)
{
webResponse.Dispose();
webResponse = null;
}
}
return response;
}
public AllChannelResponse[] AllChannels()
{
return AllChannelsAsync().GetAwaiter().GetResult();
}
public async Task<AllChannelResponse[]> AllChannelsAsync()
{
return await SendCommandAsync<AllChannelResponse[]>(new RPCRequest("allchannels", new object[] { })).ConfigureAwait(false);
}
public string[] Channels()
{
return ChannelsAsync().GetAwaiter().GetResult();
}
public async Task<string[]> ChannelsAsync()
{
return await SendCommandAsync<string[]>(new RPCRequest("channels", new object[] { })).ConfigureAwait(false);
}
public void Close(string channelId)
{
CloseAsync(channelId).GetAwaiter().GetResult();
}
public async Task CloseAsync(string channelId)
{
if (channelId == null)
throw new ArgumentNullException(nameof(channelId));
try
{
await SendCommandAsync(new RPCRequest("close", new object[] { channelId })).ConfigureAwait(false);
}
catch (RPCException ex) when (ex.Message == "closing already in progress")
{
}
}
public ChannelResponse Channel(string channelId)
{
return ChannelAsync(channelId).GetAwaiter().GetResult();
}
public async Task<ChannelResponse> ChannelAsync(string channelId)
{
if (channelId == null)
throw new ArgumentNullException(nameof(channelId));
return await SendCommandAsync<ChannelResponse>(new RPCRequest("channel", new object[] { channelId })).ConfigureAwait(false);
}
public string[] AllNodes()
{
return AllNodesAsync().GetAwaiter().GetResult();
}
public async Task<string[]> AllNodesAsync()
{
return await SendCommandAsync<string[]>(new RPCRequest("allnodes", new object[] { })).ConfigureAwait(false);
}
public Uri Address { get; private set; }
private HttpWebRequest CreateWebRequest()
{
var webRequest = (HttpWebRequest)WebRequest.Create(Address.AbsoluteUri);
webRequest.ContentType = "application/json";
webRequest.Method = "POST";
return webRequest;
}
private async Task<Stream> ToMemoryStreamAsync(Stream stream)
{
MemoryStream ms = new MemoryStream();
await stream.CopyToAsync(ms).ConfigureAwait(false);
ms.Position = 0;
return ms;
}
public string Open(NodeInfo node, Money fundingSatoshi, LightMoney pushAmount = null)
{
return OpenAsync(node, fundingSatoshi, pushAmount).GetAwaiter().GetResult();
}
public string Connect(NodeInfo node)
{
return ConnectAsync(node).GetAwaiter().GetResult();
}
public async Task<string> ConnectAsync(NodeInfo node)
{
if (node == null)
throw new ArgumentNullException(nameof(node));
return (await SendCommandAsync(new RPCRequest("connect", new object[] { node.NodeId, node.Host, node.Port })).ConfigureAwait(false)).ResultString;
}
public string Receive(LightMoney amount, string description = null)
{
return ReceiveAsync(amount, description).GetAwaiter().GetResult();
}
public async Task<string> ReceiveAsync(LightMoney amount, string description = null)
{
if (amount == null)
throw new ArgumentNullException(nameof(amount));
List<object> args = new List<object>();
args.Add(amount.MilliSatoshi);
if(description != null)
{
args.Add(description);
}
return (await SendCommandAsync(new RPCRequest("receive", args.ToArray())).ConfigureAwait(false)).ResultString;
}
public async Task<string> OpenAsync(NodeInfo node, Money fundingSatoshi, LightMoney pushAmount = null)
{
if (fundingSatoshi == null)
throw new ArgumentNullException(nameof(fundingSatoshi));
if (node == null)
throw new ArgumentNullException(nameof(node));
pushAmount = pushAmount ?? LightMoney.Zero;
var result = await SendCommandAsync(new RPCRequest("open", new object[] { node.NodeId, node.Host, node.Port, fundingSatoshi.Satoshi, pushAmount.MilliSatoshi }));
return result.ResultString;
}
}
}

View File

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
using Newtonsoft.Json;
namespace BTCPayServer.Eclair
{
public class GetInfoResponse
{
public string NodeId { get; set; }
public string Alias { get; set; }
public int Port { get; set; }
public uint256 ChainHash { get; set; }
public int BlockHeight { get; set; }
}
}

View File

@ -0,0 +1,569 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Eclair
{
public enum LightMoneyUnit : ulong
{
BTC = 100000000000,
MilliBTC = 100000000,
Bit = 100000,
Satoshi = 1000,
MilliSatoshi = 1
}
public class LightMoney : IComparable, IComparable<LightMoney>, IEquatable<LightMoney>
{
// for decimal.TryParse. None of the NumberStyles' composed values is useful for bitcoin style
private const NumberStyles BitcoinStyle =
NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite
| NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint;
/// <summary>
/// Parse a bitcoin amount (Culture Invariant)
/// </summary>
/// <param name="bitcoin"></param>
/// <param name="nRet"></param>
/// <returns></returns>
public static bool TryParse(string bitcoin, out LightMoney nRet)
{
nRet = null;
decimal value;
if (!decimal.TryParse(bitcoin, BitcoinStyle, CultureInfo.InvariantCulture, out value))
{
return false;
}
try
{
nRet = new LightMoney(value, LightMoneyUnit.BTC);
return true;
}
catch (OverflowException)
{
return false;
}
}
/// <summary>
/// Parse a bitcoin amount (Culture Invariant)
/// </summary>
/// <param name="bitcoin"></param>
/// <returns></returns>
public static LightMoney Parse(string bitcoin)
{
LightMoney result;
if (TryParse(bitcoin, out result))
{
return result;
}
throw new FormatException("Impossible to parse the string in a bitcoin amount");
}
long _MilliSatoshis;
public long MilliSatoshi
{
get
{
return _MilliSatoshis;
}
// used as a central point where long.MinValue checking can be enforced
private set
{
CheckLongMinValue(value);
_MilliSatoshis = value;
}
}
/// <summary>
/// Get absolute value of the instance
/// </summary>
/// <returns></returns>
public LightMoney Abs()
{
var a = this;
if (a < LightMoney.Zero)
a = -a;
return a;
}
public LightMoney(int satoshis)
{
MilliSatoshi = satoshis;
}
public LightMoney(uint satoshis)
{
MilliSatoshi = satoshis;
}
public LightMoney(long satoshis)
{
MilliSatoshi = satoshis;
}
public LightMoney(ulong satoshis)
{
// overflow check.
// ulong.MaxValue is greater than long.MaxValue
checked
{
MilliSatoshi = (long)satoshis;
}
}
public LightMoney(decimal amount, LightMoneyUnit unit)
{
// sanity check. Only valid units are allowed
CheckMoneyUnit(unit, "unit");
checked
{
var satoshi = amount * (long)unit;
MilliSatoshi = (long)satoshi;
}
}
/// <summary>
/// Split the Money in parts without loss
/// </summary>
/// <param name="parts">The number of parts (must be more than 0)</param>
/// <returns>The splitted money</returns>
public IEnumerable<LightMoney> Split(int parts)
{
if (parts <= 0)
throw new ArgumentOutOfRangeException("Parts should be more than 0", "parts");
long remain;
long result = DivRem(_MilliSatoshis, parts, out remain);
for (int i = 0; i < parts; i++)
{
yield return LightMoney.Satoshis(result + (remain > 0 ? 1 : 0));
remain--;
}
}
private static long DivRem(long a, long b, out long result)
{
result = a % b;
return a / b;
}
public static LightMoney FromUnit(decimal amount, LightMoneyUnit unit)
{
return new LightMoney(amount, unit);
}
/// <summary>
/// Convert Money to decimal (same as ToDecimal)
/// </summary>
/// <param name="unit"></param>
/// <returns></returns>
public decimal ToUnit(LightMoneyUnit unit)
{
CheckMoneyUnit(unit, "unit");
// overflow safe because (long / int) always fit in decimal
// decimal operations are checked by default
return (decimal)MilliSatoshi / (int)unit;
}
/// <summary>
/// Convert Money to decimal (same as ToUnit)
/// </summary>
/// <param name="unit"></param>
/// <returns></returns>
public decimal ToDecimal(LightMoneyUnit unit)
{
return ToUnit(unit);
}
public static LightMoney Coins(decimal coins)
{
// overflow safe.
// decimal operations are checked by default
return new LightMoney(coins * (ulong)LightMoneyUnit.BTC, LightMoneyUnit.MilliBTC);
}
public static LightMoney Bits(decimal bits)
{
// overflow safe.
// decimal operations are checked by default
return new LightMoney(bits * (ulong)LightMoneyUnit.Bit, LightMoneyUnit.MilliBTC);
}
public static LightMoney Cents(decimal cents)
{
// overflow safe.
// decimal operations are checked by default
return new LightMoney(cents * (ulong)LightMoneyUnit.Bit, LightMoneyUnit.MilliBTC);
}
public static LightMoney Satoshis(decimal sats)
{
return new LightMoney(sats * (ulong)LightMoneyUnit.Satoshi, LightMoneyUnit.MilliBTC);
}
public static LightMoney Satoshis(ulong sats)
{
return new LightMoney(sats);
}
public static LightMoney Satoshis(long sats)
{
return new LightMoney(sats);
}
public static LightMoney MilliSatoshis(long msats)
{
return new LightMoney(msats);
}
public static LightMoney MilliSatoshis(ulong msats)
{
return new LightMoney(msats);
}
#region IEquatable<Money> Members
public bool Equals(LightMoney other)
{
if (other == null)
return false;
return _MilliSatoshis.Equals(other._MilliSatoshis);
}
public int CompareTo(LightMoney other)
{
if (other == null)
return 1;
return _MilliSatoshis.CompareTo(other._MilliSatoshis);
}
#endregion
#region IComparable Members
public int CompareTo(object obj)
{
if (obj == null)
return 1;
LightMoney m = obj as LightMoney;
if (m != null)
return _MilliSatoshis.CompareTo(m._MilliSatoshis);
#if !(PORTABLE || NETCORE)
return _MilliSatoshis.CompareTo(obj);
#else
return _Satoshis.CompareTo((long)obj);
#endif
}
#endregion
public static LightMoney operator -(LightMoney left, LightMoney right)
{
if (left == null)
throw new ArgumentNullException("left");
if (right == null)
throw new ArgumentNullException("right");
return new LightMoney(checked(left._MilliSatoshis - right._MilliSatoshis));
}
public static LightMoney operator -(LightMoney left)
{
if (left == null)
throw new ArgumentNullException("left");
return new LightMoney(checked(-left._MilliSatoshis));
}
public static LightMoney operator +(LightMoney left, LightMoney right)
{
if (left == null)
throw new ArgumentNullException("left");
if (right == null)
throw new ArgumentNullException("right");
return new LightMoney(checked(left._MilliSatoshis + right._MilliSatoshis));
}
public static LightMoney operator *(int left, LightMoney right)
{
if (right == null)
throw new ArgumentNullException("right");
return LightMoney.Satoshis(checked(left * right._MilliSatoshis));
}
public static LightMoney operator *(LightMoney right, int left)
{
if (right == null)
throw new ArgumentNullException("right");
return LightMoney.Satoshis(checked(right._MilliSatoshis * left));
}
public static LightMoney operator *(long left, LightMoney right)
{
if (right == null)
throw new ArgumentNullException("right");
return LightMoney.Satoshis(checked(left * right._MilliSatoshis));
}
public static LightMoney operator *(LightMoney right, long left)
{
if (right == null)
throw new ArgumentNullException("right");
return LightMoney.Satoshis(checked(left * right._MilliSatoshis));
}
public static LightMoney operator /(LightMoney left, long right)
{
if (left == null)
throw new ArgumentNullException("left");
return new LightMoney(checked(left._MilliSatoshis / right));
}
public static bool operator <(LightMoney left, LightMoney right)
{
if (left == null)
throw new ArgumentNullException("left");
if (right == null)
throw new ArgumentNullException("right");
return left._MilliSatoshis < right._MilliSatoshis;
}
public static bool operator >(LightMoney left, LightMoney right)
{
if (left == null)
throw new ArgumentNullException("left");
if (right == null)
throw new ArgumentNullException("right");
return left._MilliSatoshis > right._MilliSatoshis;
}
public static bool operator <=(LightMoney left, LightMoney right)
{
if (left == null)
throw new ArgumentNullException("left");
if (right == null)
throw new ArgumentNullException("right");
return left._MilliSatoshis <= right._MilliSatoshis;
}
public static bool operator >=(LightMoney left, LightMoney right)
{
if (left == null)
throw new ArgumentNullException("left");
if (right == null)
throw new ArgumentNullException("right");
return left._MilliSatoshis >= right._MilliSatoshis;
}
public static implicit operator LightMoney(long value)
{
return new LightMoney(value);
}
public static implicit operator LightMoney(int value)
{
return new LightMoney(value);
}
public static implicit operator LightMoney(uint value)
{
return new LightMoney(value);
}
public static implicit operator LightMoney(ulong value)
{
return new LightMoney(checked((long)value));
}
public static implicit operator long(LightMoney value)
{
return value.MilliSatoshi;
}
public static implicit operator ulong(LightMoney value)
{
return checked((ulong)value.MilliSatoshi);
}
public static implicit operator LightMoney(string value)
{
return LightMoney.Parse(value);
}
public override bool Equals(object obj)
{
LightMoney item = obj as LightMoney;
if (item == null)
return false;
return _MilliSatoshis.Equals(item._MilliSatoshis);
}
public static bool operator ==(LightMoney a, LightMoney b)
{
if (Object.ReferenceEquals(a, b))
return true;
if (((object)a == null) || ((object)b == null))
return false;
return a._MilliSatoshis == b._MilliSatoshis;
}
public static bool operator !=(LightMoney a, LightMoney b)
{
return !(a == b);
}
public override int GetHashCode()
{
return _MilliSatoshis.GetHashCode();
}
/// <summary>
/// Returns a culture invariant string representation of Bitcoin amount
/// </summary>
/// <returns></returns>
public override string ToString()
{
return ToString(false, false);
}
/// <summary>
/// Returns a culture invariant string representation of Bitcoin amount
/// </summary>
/// <param name="fplus">True if show + for a positive amount</param>
/// <param name="trimExcessZero">True if trim excess zeroes</param>
/// <returns></returns>
public string ToString(bool fplus, bool trimExcessZero = true)
{
var fmt = string.Format("{{0:{0}{1}B}}",
(fplus ? "+" : null),
(trimExcessZero ? "2" : "11"));
return string.Format(BitcoinFormatter.Formatter, fmt, _MilliSatoshis);
}
static LightMoney _Zero = new LightMoney(0);
public static LightMoney Zero
{
get
{
return _Zero;
}
}
internal class BitcoinFormatter : IFormatProvider, ICustomFormatter
{
public static readonly BitcoinFormatter Formatter = new BitcoinFormatter();
public object GetFormat(Type formatType)
{
return formatType == typeof(ICustomFormatter) ? this : null;
}
public string Format(string format, object arg, IFormatProvider formatProvider)
{
if (!this.Equals(formatProvider))
{
return null;
}
var i = 0;
var plus = format[i] == '+';
if (plus)
i++;
int decPos = 0;
if (int.TryParse(format.Substring(i, 1), out decPos))
{
i++;
}
var unit = format[i];
var unitToUseInCalc = LightMoneyUnit.BTC;
switch (unit)
{
case 'B':
unitToUseInCalc = LightMoneyUnit.BTC;
break;
}
var val = Convert.ToDecimal(arg) / (long)unitToUseInCalc;
var zeros = new string('0', decPos);
var rest = new string('#', 11 - decPos);
var fmt = plus && val > 0 ? "+" : string.Empty;
fmt += "{0:0" + (decPos > 0 ? "." + zeros + rest : string.Empty) + "}";
return string.Format(CultureInfo.InvariantCulture, fmt, val);
}
}
/// <summary>
/// Tell if amount is almost equal to this instance
/// </summary>
/// <param name="amount"></param>
/// <param name="dust">more or less amount</param>
/// <returns>true if equals, else false</returns>
public bool Almost(LightMoney amount, LightMoney dust)
{
if (amount == null)
throw new ArgumentNullException("amount");
if (dust == null)
throw new ArgumentNullException("dust");
return (amount - this).Abs() <= dust;
}
/// <summary>
/// Tell if amount is almost equal to this instance
/// </summary>
/// <param name="amount"></param>
/// <param name="margin">error margin (between 0 and 1)</param>
/// <returns>true if equals, else false</returns>
public bool Almost(LightMoney amount, decimal margin)
{
if (amount == null)
throw new ArgumentNullException("amount");
if (margin < 0.0m || margin > 1.0m)
throw new ArgumentOutOfRangeException("margin", "margin should be between 0 and 1");
var dust = LightMoney.Satoshis((decimal)this.MilliSatoshi * margin);
return Almost(amount, dust);
}
public static LightMoney Min(LightMoney a, LightMoney b)
{
if (a == null)
throw new ArgumentNullException("a");
if (b == null)
throw new ArgumentNullException("b");
if (a <= b)
return a;
return b;
}
public static LightMoney Max(LightMoney a, LightMoney b)
{
if (a == null)
throw new ArgumentNullException("a");
if (b == null)
throw new ArgumentNullException("b");
if (a >= b)
return a;
return b;
}
private static void CheckLongMinValue(long value)
{
if (value == long.MinValue)
throw new OverflowException("satoshis amount should be greater than long.MinValue");
}
private static void CheckMoneyUnit(LightMoneyUnit value, string paramName)
{
var typeOfMoneyUnit = typeof(LightMoneyUnit);
if (!Enum.IsDefined(typeOfMoneyUnit, value))
{
throw new ArgumentException("Invalid value for MoneyUnit", paramName);
}
}
#region IComparable Members
int IComparable.CompareTo(object obj)
{
return this.CompareTo(obj);
}
#endregion
}
}

View File

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Eclair
{
public class NodeInfo
{
public NodeInfo(string nodeId, string host, int port)
{
if (host == null)
throw new ArgumentNullException(nameof(host));
if (nodeId == null)
throw new ArgumentNullException(nameof(nodeId));
Port = port;
Host = host;
NodeId = nodeId;
}
public string NodeId { get; private set; }
public string Host { get; private set; }
public int Port { get; private set; }
}
}

View File

@ -71,10 +71,10 @@ namespace BTCPayServer
return res;
}
public static HtmlString ToSrvModel(this object o)
public static HtmlString ToJSVariableModel(this object o, string variableName)
{
var encodedJson = JavaScriptEncoder.Default.Encode(o.ToJson());
return new HtmlString("var srvModel = JSON.parse('" + encodedJson + "');");
return new HtmlString($"var {variableName} = JSON.parse('" + encodedJson + "');");
}

View File

@ -1,4 +1,5 @@
using BTCPayServer.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Hosting;
using System;
using System.Collections.Generic;
@ -34,6 +35,7 @@ using System.Threading;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Authentication;
using Microsoft.Extensions.Caching.Memory;
using BTCPayServer.Logging;
namespace BTCPayServer.Hosting
{
@ -83,19 +85,6 @@ namespace BTCPayServer.Hosting
}
}
}
class BTCPayServerConfigureOptions : IConfigureOptions<MvcOptions>
{
BTCPayServerOptions _Options;
public BTCPayServerConfigureOptions(BTCPayServerOptions options)
{
_Options = options;
}
public void Configure(MvcOptions options)
{
if (_Options.RequireHttps)
options.Filters.Add(new RequireHttpsAttribute());
}
}
public static IServiceCollection AddBTCPayServer(this IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>((provider, o) =>
@ -106,18 +95,35 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton<SettingsRepository>();
services.TryAddSingleton<InvoicePaymentNotification>();
services.TryAddSingleton<BTCPayServerOptions>(o => o.GetRequiredService<IOptions<BTCPayServerOptions>>().Value);
services.TryAddSingleton<IConfigureOptions<MvcOptions>, BTCPayServerConfigureOptions>();
services.TryAddSingleton(o =>
services.TryAddSingleton<InvoiceRepository>(o =>
{
var runtime = new BTCPayServerRuntime();
runtime.Configure(o.GetRequiredService<BTCPayServerOptions>());
return runtime;
var opts = o.GetRequiredService<BTCPayServerOptions>();
var dbContext = o.GetRequiredService<ApplicationDbContextFactory>();
var dbpath = Path.Combine(opts.DataDir, "InvoiceDB");
if (!Directory.Exists(dbpath))
Directory.CreateDirectory(dbpath);
return new InvoiceRepository(dbContext, dbpath, opts.Network);
});
services.AddSingleton<BTCPayServerEnvironment>();
services.TryAddSingleton<TokenRepository>();
services.TryAddSingleton(o => o.GetRequiredService<BTCPayServerRuntime>().InvoiceRepository);
services.TryAddSingleton<Network>(o => o.GetRequiredService<BTCPayServerOptions>().Network);
services.TryAddSingleton<ApplicationDbContextFactory>(o => o.GetRequiredService<BTCPayServerRuntime>().DBFactory);
services.TryAddSingleton<ApplicationDbContextFactory>(o =>
{
var opts = o.GetRequiredService<BTCPayServerOptions>();
ApplicationDbContextFactory dbContext = null;
if (opts.PostgresConnectionString == null)
{
var connStr = "Data Source=" + Path.Combine(opts.DataDir, "sqllite.db");
Logs.Configuration.LogInformation($"SQLite DB used ({connStr})");
dbContext = new ApplicationDbContextFactory(DatabaseType.Sqlite, connStr);
}
else
{
Logs.Configuration.LogInformation($"Postgres DB used ({opts.PostgresConnectionString})");
dbContext = new ApplicationDbContextFactory(DatabaseType.Postgres, opts.PostgresConnectionString);
}
return dbContext;
});
services.TryAddSingleton<StoreRepository>();
services.TryAddSingleton<BTCPayWallet>();
services.TryAddSingleton<CurrencyNameTable>();
@ -127,10 +133,16 @@ namespace BTCPayServer.Hosting
BlockTarget = 20,
ExplorerClient = o.GetRequiredService<ExplorerClient>()
});
services.TryAddSingleton<NBXplorerWaiterAccessor>();
services.AddSingleton<IHostedService, NBXplorerWaiter>();
services.TryAddSingleton<ExplorerClient>(o =>
{
var runtime = o.GetRequiredService<BTCPayServerRuntime>();
return runtime.Explorer;
var opts = o.GetRequiredService<BTCPayServerOptions>();
var explorer = new ExplorerClient(opts.Network, opts.Explorer);
if (!explorer.SetCookieAuth(opts.CookieFile))
explorer.SetNoAuth();
return explorer;
});
services.TryAddSingleton<Bitpay>(o =>
{
@ -141,11 +153,16 @@ namespace BTCPayServer.Hosting
});
services.TryAddSingleton<IRateProvider>(o =>
{
return new CachedRateProvider(new CoinAverageRateProvider(), o.GetRequiredService<IMemoryCache>()) { CacheSpan = TimeSpan.FromMinutes(1.0) };
var coinaverage = new CoinAverageRateProvider();
var bitpay = new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/")));
return new CachedRateProvider(new FallbackRateProvider(new IRateProvider[] { coinaverage, bitpay }), o.GetRequiredService<IMemoryCache>()) { CacheSpan = TimeSpan.FromMinutes(1.0) };
});
services.TryAddSingleton<InvoiceWatcher>();
services.TryAddSingleton<InvoiceNotificationManager>();
services.TryAddSingleton<IHostedService>(o => o.GetRequiredService<InvoiceWatcher>());
services.TryAddSingleton<InvoiceWatcherAccessor>();
services.AddSingleton<IHostedService, InvoiceWatcher>();
services.TryAddScoped<IHttpContextAccessor, HttpContextAccessor>();
services.TryAddSingleton<IAuthorizationHandler, OwnStoreHandler>();
services.AddTransient<AccessTokenController>();
@ -172,12 +189,6 @@ namespace BTCPayServer.Hosting
public static IApplicationBuilder UsePayServer(this IApplicationBuilder app)
{
if (app.ApplicationServices.GetRequiredService<BTCPayServerOptions>().RequireHttps)
{
var options = new RewriteOptions().AddRedirectToHttps();
app.UseRewriter(options);
}
using (var scope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
{
//Wait the DB is ready

View File

@ -30,25 +30,29 @@ namespace BTCPayServer.Hosting
TokenRepository _TokenRepository;
RequestDelegate _Next;
CallbackController _CallbackController;
BTCPayServerOptions _Options;
private NBXplorerWaiterAccessor _NbxplorerAwaiter;
public BTCPayMiddleware(RequestDelegate next,
TokenRepository tokenRepo,
BTCPayServerOptions options,
NBXplorerWaiterAccessor nbxplorerAwaiter,
CallbackController callbackController)
{
_TokenRepository = tokenRepo ?? throw new ArgumentNullException(nameof(tokenRepo));
_Next = next ?? throw new ArgumentNullException(nameof(next));
_CallbackController = callbackController;
_Options = options ?? throw new ArgumentNullException(nameof(options));
_NbxplorerAwaiter = (nbxplorerAwaiter ?? throw new ArgumentNullException(nameof(nbxplorerAwaiter)));
}
bool _Registered;
public async Task Invoke(HttpContext httpContext)
{
if (!_Registered)
{
var callback = await _CallbackController.RegisterCallbackBlockUriAsync(httpContext.Request);
Logs.PayServer.LogInformation($"Registering block callback to " + callback);
_Registered = true;
}
RewriteHostIfNeeded(httpContext);
await EnsureBlockCallbackRegistered(httpContext);
httpContext.Request.Headers.TryGetValue("x-signature", out StringValues values);
var sig = values.FirstOrDefault();
@ -103,6 +107,75 @@ namespace BTCPayServer.Hosting
}
}
private void RewriteHostIfNeeded(HttpContext httpContext)
{
// Make sure that code executing after this point think that the external url has been hit.
if (_Options.ExternalUrl != null)
{
httpContext.Request.Scheme = _Options.ExternalUrl.Scheme;
if (_Options.ExternalUrl.IsDefaultPort)
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host);
else
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host, _Options.ExternalUrl.Port);
}
// NGINX pass X-Forwarded-Proto and X-Forwarded-Port, so let's use that to have better guess of the real domain
else
{
ushort? p = null;
if (httpContext.Request.Headers.TryGetValue("X-Forwarded-Proto", out StringValues proto))
{
var scheme = proto.SingleOrDefault();
if (scheme != null)
{
httpContext.Request.Scheme = scheme;
if (scheme == "http")
p = 80;
if (scheme == "https")
p = 443;
}
}
if (httpContext.Request.Headers.TryGetValue("X-Forwarded-Port", out StringValues port))
{
var portString = port.SingleOrDefault();
if (portString != null && ushort.TryParse(portString, out ushort pp))
{
p = pp;
}
}
if (p.HasValue)
{
bool isDefault = httpContext.Request.Scheme == "http" && p.Value == 80;
isDefault |= httpContext.Request.Scheme == "https" && p.Value == 443;
if (isDefault)
httpContext.Request.Host = new HostString(httpContext.Request.Host.Host);
else
httpContext.Request.Host = new HostString(httpContext.Request.Host.Host, p.Value);
}
}
}
private async Task EnsureBlockCallbackRegistered(HttpContext httpContext)
{
if (!_Registered)
{
var callback = await _CallbackController.GetCallbackBlockUriAsync(httpContext.Request);
var unused = _NbxplorerAwaiter.Instance.WhenReady(async c =>
{
try
{
await _CallbackController.RegisterCallbackBlockUriAsync(callback);
Logs.PayServer.LogInformation($"Registering block callback to " + callback);
}
catch (Exception ex)
{
Logs.PayServer.LogError(ex, "Could not register block callback");
}
return true;
});
_Registered = true;
}
}
private static async Task HandleBitpayHttpException(HttpContext httpContext, BitpayHttpException ex)
{
httpContext.Response.StatusCode = ex.StatusCode;

View File

@ -9,12 +9,22 @@ namespace BTCPayServer.Models.InvoicingModels
{
public class CreateInvoiceModel
{
public CreateInvoiceModel()
{
Currency = "USD";
}
[Required]
public double? Amount
{
get; set;
}
[Required]
public string Currency
{
get; set;
}
[Required]
public string StoreId
{

View File

@ -9,6 +9,7 @@ namespace BTCPayServer.Models.ServerViewModels
{
public class UserViewModel
{
public string Id { get; set; }
public string Name
{
get; set;

View File

@ -1,5 +1,6 @@
using BTCPayServer.Services.Invoices;
using BTCPayServer.Validations;
using Microsoft.AspNetCore.Mvc.Rendering;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
@ -10,6 +11,22 @@ namespace BTCPayServer.Models.StoreViewModels
{
public class StoreViewModel
{
class Format
{
public string Name { get; set; }
public string Value { get; set; }
}
public StoreViewModel()
{
var btcPay = new Format { Name = "BTCPay", Value = "BTCPay" };
DerivationSchemeFormat = btcPay.Value;
DerivationSchemeFormats = new SelectList(new Format[]
{
btcPay,
new Format { Name = "Electrum", Value = "Electrum" },
}, nameof(btcPay.Value), nameof(btcPay.Name), btcPay);
}
public string Id { get; set; }
[Display(Name = "Store Name")]
[Required]
[MaxLength(50)]
@ -28,12 +45,28 @@ namespace BTCPayServer.Models.StoreViewModels
set;
}
[DerivationStrategyValidator]
public string DerivationScheme
{
get; set;
}
[Display(Name = "Derivation Scheme format")]
public string DerivationSchemeFormat
{
get;
set;
}
public SelectList DerivationSchemeFormats { get; set; }
[Display(Name = "Payment invalid if transactions fails to confirm after ... minutes")]
[Range(10, 60 * 24 * 31)]
public int MonitoringExpiration
{
get;
set;
}
[Display(Name = "Consider the invoice confirmed when the payment transaction...")]
public SpeedPolicy SpeedPolicy
{

View File

@ -0,0 +1,184 @@
using System;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Logging;
using Microsoft.Extensions.Hosting;
using NBXplorer;
using NBXplorer.Models;
using System.Collections.Concurrent;
namespace BTCPayServer
{
public class NBXplorerWaiterAccessor
{
public NBXplorerWaiter Instance { get; set; }
}
public enum NBXplorerState
{
NotConnected,
Synching,
Ready
}
public class NBXplorerWaiter : IHostedService
{
public NBXplorerWaiter(ExplorerClient client, NBXplorerWaiterAccessor accessor)
{
_Client = client;
accessor.Instance = this;
}
ExplorerClient _Client;
Timer _Timer;
ManualResetEventSlim _Idle = new ManualResetEventSlim(true);
public Task StartAsync(CancellationToken cancellationToken)
{
_Timer = new Timer(Callback, null, 0, (int)TimeSpan.FromMinutes(1.0).TotalMilliseconds);
return Task.CompletedTask;
}
void Callback(object state)
{
if (!_Idle.IsSet)
return;
try
{
_Idle.Reset();
CheckStatus().GetAwaiter().GetResult();
}
catch (Exception ex)
{
Logs.PayServer.LogError(ex, "Error while checking NBXplorer state");
}
finally
{
_Idle.Set();
}
}
async Task CheckStatus()
{
while (await StepAsync())
{
}
List<Task> tasks = new List<Task>();
if (State == NBXplorerState.Ready)
{
while (_WhenReady.TryDequeue(out Func<ExplorerClient, Task> act))
{
tasks.Add(act(_Client));
}
}
await Task.WhenAll(tasks);
}
private async Task<bool> StepAsync()
{
var oldState = State;
StatusResult status = null;
switch (State)
{
case NBXplorerState.NotConnected:
status = await GetStatusWithTimeout();
if (status != null)
{
if (status.IsFullySynched())
{
State = NBXplorerState.Ready;
}
else
{
State = NBXplorerState.Synching;
}
}
break;
case NBXplorerState.Synching:
status = await GetStatusWithTimeout();
if (status == null)
{
State = NBXplorerState.NotConnected;
}
else if (status.IsFullySynched())
{
State = NBXplorerState.Ready;
}
break;
case NBXplorerState.Ready:
status = await GetStatusWithTimeout();
if (status == null)
{
State = NBXplorerState.NotConnected;
}
else if (!status.IsFullySynched())
{
State = NBXplorerState.Synching;
}
break;
}
LastStatus = status;
if (oldState != State)
{
Logs.PayServer.LogInformation($"NBXplorerWaiter status changed: {oldState} => {State}");
}
return oldState != State;
}
public Task<T> WhenReady<T>(Func<ExplorerClient, Task<T>> act)
{
if (State == NBXplorerState.Ready)
return act(_Client);
TaskCompletionSource<T> completion = new TaskCompletionSource<T>();
_WhenReady.Enqueue(async client =>
{
try
{
var result = await act(client);
completion.SetResult(result);
}
catch (Exception ex)
{
completion.SetException(ex);
}
});
return completion.Task;
}
ConcurrentQueue<Func<ExplorerClient, Task>> _WhenReady = new ConcurrentQueue<Func<ExplorerClient, Task>>();
private async Task<StatusResult> GetStatusWithTimeout()
{
CancellationTokenSource cts = new CancellationTokenSource();
using (cts)
{
var cancellation = cts.Token;
while (!cancellation.IsCancellationRequested)
{
try
{
var status = await _Client.GetStatusAsync(cancellation).ConfigureAwait(false);
return status;
}
catch (OperationCanceledException) { throw; }
catch { }
}
}
return null;
}
public NBXplorerState State { get; private set; }
public StatusResult LastStatus { get; private set; }
public Task StopAsync(CancellationToken cancellationToken)
{
_Timer.Dispose();
_Timer = null;
_Idle.Wait();
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace BTCPayServer
{
public class SearchString
{
string _OriginalString;
public SearchString(string str)
{
str = str ?? string.Empty;
str = str.Trim();
_OriginalString = str.Trim();
TextSearch = _OriginalString;
var splitted = str.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
Filters
= splitted
.Select(t => t.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries))
.Where(kv => kv.Length == 2)
.Select(kv => new KeyValuePair<string, string>(kv[0].ToLowerInvariant(), kv[1]))
.ToDictionary(o => o.Key, o => o.Value);
foreach(var filter in splitted)
{
if(filter.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries).Length == 2)
{
TextSearch = TextSearch.Replace(filter, string.Empty);
}
}
TextSearch = TextSearch.Trim();
}
public string TextSearch
{
get;
private set;
}
public Dictionary<string, string> Filters { get; private set; }
public override string ToString()
{
return _OriginalString;
}
}
}

View File

@ -251,7 +251,8 @@ namespace BTCPayServer.Services.Invoices
get;
set;
}
public DateTimeOffset? MonitoringExpiration
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public DateTimeOffset MonitoringExpiration
{
get;
set;

View File

@ -16,6 +16,10 @@ using BTCPayServer.Services.Wallets;
namespace BTCPayServer.Services.Invoices
{
public class InvoiceWatcherAccessor
{
public InvoiceWatcher Instance { get; set; }
}
public class InvoiceWatcher : IHostedService
{
InvoiceRepository _InvoiceRepository;
@ -23,11 +27,13 @@ namespace BTCPayServer.Services.Invoices
DerivationStrategyFactory _DerivationFactory;
InvoiceNotificationManager _NotificationManager;
BTCPayWallet _Wallet;
public InvoiceWatcher(ExplorerClient explorerClient,
InvoiceRepository invoiceRepository,
BTCPayWallet wallet,
InvoiceNotificationManager notificationManager)
InvoiceNotificationManager notificationManager,
InvoiceWatcherAccessor accessor)
{
LongPollingMode = explorerClient.Network == Network.RegTest;
PollInterval = explorerClient.Network == Network.RegTest ? TimeSpan.FromSeconds(10.0) : TimeSpan.FromMinutes(1.0);
@ -36,6 +42,7 @@ namespace BTCPayServer.Services.Invoices
_DerivationFactory = new DerivationStrategyFactory(_ExplorerClient.Network);
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
_NotificationManager = notificationManager ?? throw new ArgumentNullException(nameof(notificationManager));
accessor.Instance = this;
}
public bool LongPollingMode
@ -80,9 +87,8 @@ namespace BTCPayServer.Services.Invoices
Logs.PayServer.LogInformation($"Invoice {invoice.Id}: {stateBefore} => {invoice.Status}");
}
var expirationMonitoring = invoice.MonitoringExpiration.HasValue ? invoice.MonitoringExpiration.Value : invoice.InvoiceTime + TimeSpan.FromMinutes(60);
if (invoice.Status == "complete" ||
((invoice.Status == "invalid" || invoice.Status == "expired") && expirationMonitoring < DateTimeOffset.UtcNow))
((invoice.Status == "invalid" || invoice.Status == "expired") && invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
{
if (await _InvoiceRepository.RemovePendingInvoice(invoice.Id).ConfigureAwait(false))
Logs.PayServer.LogInformation("Stopped watching invoice " + invoiceId);
@ -133,6 +139,10 @@ namespace BTCPayServer.Services.Invoices
needSave = true;
await _InvoiceRepository.UnaffectAddress(invoice.Id);
invoice.Status = "expired";
if (invoice.FullNotifications)
{
_NotificationManager.Notify(invoice);
}
}
if (invoice.Status == "new" || invoice.Status == "expired")
@ -181,22 +191,39 @@ namespace BTCPayServer.Services.Invoices
if (invoice.Status == "paid")
{
if (!invoice.MonitoringExpiration.HasValue || invoice.MonitoringExpiration > DateTimeOffset.UtcNow)
var transactions = await GetPaymentsWithTransaction(invoice);
var chainConfirmedTransactions = transactions.Where(t => t.Confirmations >= 1);
if (invoice.SpeedPolicy == SpeedPolicy.HighSpeed)
{
var transactions = await GetPaymentsWithTransaction(invoice);
if (invoice.SpeedPolicy == SpeedPolicy.HighSpeed)
{
transactions = transactions.Where(t => !t.Transaction.RBF);
}
else if (invoice.SpeedPolicy == SpeedPolicy.MediumSpeed)
{
transactions = transactions.Where(t => t.Confirmations >= 1);
}
else if (invoice.SpeedPolicy == SpeedPolicy.LowSpeed)
{
transactions = transactions.Where(t => t.Confirmations >= 6);
}
transactions = transactions.Where(t => !t.Transaction.RBF);
}
else if (invoice.SpeedPolicy == SpeedPolicy.MediumSpeed)
{
transactions = transactions.Where(t => t.Confirmations >= 1);
}
else if (invoice.SpeedPolicy == SpeedPolicy.LowSpeed)
{
transactions = transactions.Where(t => t.Confirmations >= 6);
}
var chainTotalConfirmed = chainConfirmedTransactions.Select(t => t.Payment.Output.Value).Sum();
if (// Is after the monitoring deadline
(invoice.MonitoringExpiration < DateTimeOffset.UtcNow)
&&
// And not enough amount confirmed
(chainTotalConfirmed < invoice.GetTotalCryptoDue()))
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
invoice.Status = "invalid";
needSave = true;
if (invoice.FullNotifications)
{
_NotificationManager.Notify(invoice);
}
}
else
{
var totalConfirmed = transactions.Select(t => t.Payment.Output.Value).Sum();
if (totalConfirmed >= invoice.GetTotalCryptoDue())
{
@ -206,12 +233,6 @@ namespace BTCPayServer.Services.Invoices
needSave = true;
}
}
else
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
invoice.Status = "invalid";
needSave = true;
}
}
if (invoice.Status == "confirmed")

View File

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace BTCPayServer.Services
{
@ -11,5 +12,8 @@ namespace BTCPayServer.Services
{
get; set;
}
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public bool LockSubscription { get; set; }
}
}

View File

@ -17,61 +17,6 @@ namespace BTCPayServer.Services.Rates
}
public class CoinAverageRateProvider : IRateProvider
{
public class RatesJson
{
public class RateJson
{
public string Code
{
get; set;
}
public decimal Rate
{
get; set;
}
}
[JsonProperty("rates")]
public JObject RatesInternal
{
get; set;
}
[JsonIgnore]
public List<RateJson> Rates
{
get; set;
}
[JsonIgnore]
public Dictionary<string, decimal> RatesByCurrency
{
get; set;
}
public decimal GetRate(string currency)
{
if (!RatesByCurrency.TryGetValue(currency.ToUpperInvariant(), out decimal currUSD))
throw new RateUnavailableException(currency);
if (!RatesByCurrency.TryGetValue("BTC", out decimal btcUSD))
throw new RateUnavailableException(currency);
return currUSD / btcUSD;
}
public void CalculateDictionary()
{
RatesByCurrency = new Dictionary<string, decimal>();
Rates = new List<RateJson>();
foreach (var rate in RatesInternal.OfType<JProperty>())
{
var rateJson = new RateJson();
rateJson.Code = rate.Name;
rateJson.Rate = decimal.Parse(rate.Value["rate"].Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint);
RatesByCurrency.Add(rate.Name, rateJson.Rate);
Rates.Add(rateJson);
}
}
}
static HttpClient _Client = new HttpClient();
public string Market
@ -80,13 +25,20 @@ namespace BTCPayServer.Services.Rates
} = "global";
public async Task<decimal> GetRateAsync(string currency)
{
RatesJson rates = await GetRatesCore();
return rates.GetRate(currency);
var rates = await GetRatesCore();
return GetRate(rates, currency);
}
private async Task<RatesJson> GetRatesCore()
private decimal GetRate(Dictionary<string, decimal> rates, string currency)
{
var resp = await _Client.GetAsync("https://apiv2.bitcoinaverage.com/constants/exchangerates/" + Market);
if (rates.TryGetValue(currency, out decimal result))
return result;
throw new RateUnavailableException(currency);
}
private async Task<Dictionary<string, decimal>> GetRatesCore()
{
var resp = await _Client.GetAsync($"https://apiv2.bitcoinaverage.com/indices/{Market}/ticker/short");
using (resp)
{
@ -97,19 +49,25 @@ namespace BTCPayServer.Services.Rates
if ((int)resp.StatusCode == 403)
throw new CoinAverageException("Unauthorized access to the API, premium plan needed");
resp.EnsureSuccessStatusCode();
var rates = JsonConvert.DeserializeObject<RatesJson>(await resp.Content.ReadAsStringAsync());
rates.CalculateDictionary();
return rates;
var rates = JObject.Parse(await resp.Content.ReadAsStringAsync());
return rates.Properties()
.Where(p => p.Name.StartsWith("BTC", StringComparison.OrdinalIgnoreCase))
.ToDictionary(p => p.Name.Substring(3, 3), p => ToDecimal(p.Value["last"]));
}
}
private decimal ToDecimal(JToken token)
{
return decimal.Parse(token.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint);
}
public async Task<ICollection<Rate>> GetRatesAsync()
{
RatesJson rates = await GetRatesCore();
return rates.Rates.Select(o => new Rate()
var rates = await GetRatesCore();
return rates.Select(o => new Rate()
{
Currency = o.Code,
Value = rates.GetRate(o.Code)
Currency = o.Key,
Value = o.Value
}).ToList();
}
}

View File

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Services.Rates
{
public class FallbackRateProvider : IRateProvider
{
IRateProvider[] _Providers;
public FallbackRateProvider(IRateProvider[] providers)
{
if (providers == null)
throw new ArgumentNullException(nameof(providers));
_Providers = providers;
}
public async Task<decimal> GetRateAsync(string currency)
{
foreach(var p in _Providers)
{
try
{
return await p.GetRateAsync(currency).ConfigureAwait(false);
}
catch { }
}
throw new RateUnavailableException(currency);
}
public async Task<ICollection<Rate>> GetRatesAsync()
{
foreach (var p in _Providers)
{
try
{
return await p.GetRatesAsync().ConfigureAwait(false);
}
catch { }
}
throw new RateUnavailableException("ALL");
}
}
}

View File

@ -1,32 +0,0 @@
using NBitcoin;
using NBXplorer.DerivationStrategy;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text;
namespace BTCPayServer.Validations
{
public class DerivationStrategyValidatorAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value == null)
{
return ValidationResult.Success;
}
var network = (Network)validationContext.GetService(typeof(Network));
if (network == null)
return new ValidationResult("No Network specified");
try
{
new DerivationStrategyFactory(network).Parse((string)value);
return ValidationResult.Success;
}
catch (Exception ex)
{
return new ValidationResult(ex.Message);
}
}
}
}

View File

@ -57,10 +57,10 @@
<h2>Video tutorials</h2>
<div class="row">
<div class="col-md-6 text-center">
<iframe width="560" height="315" src="https://www.youtube.com/embed/npFMOu6tTpA" frameborder="0" allowfullscreen></iframe>
<iframe width="560" height="315" src="https://www.youtube.com/embed/xh3Eac66qc4" frameborder="0" allowfullscreen></iframe>
</div>
<div class="col-md-6 text-center">
<iframe width="560" height="315" src="https://www.youtube.com/embed/6rd8ZpLrz-4" frameborder="0" allowfullscreen></iframe>
<iframe width="560" height="315" src="https://www.youtube.com/embed/Xo_vApXTZBU" frameborder="0" allowfullscreen></iframe>
</div>
</div>
</div>
@ -122,10 +122,18 @@
</div>
<div class="row">
<div class="col-lg-4 ml-auto text-center">
<a href="http://13.79.159.103:3000/">
<a href="http://52.191.212.129:3000/">
<img src="~/img/slack.png" height="100" />
</a>
<p><a href="http://13.79.159.103:3000/">On Slack</a></p>
<p><a href="http://52.191.212.129:3000/">On Slack</a></p>
</div>
<div class="col-lg-4 mr-auto text-center">
<a href="https://twitter.com/BtcpayServer">
<img src="~/img/twitter.png" height="100" />
</a>
<p>
<a href="https://twitter.com/BtcpayServer">On Twitter</a>
</p>
</div>
<div class="col-lg-4 mr-auto text-center">
<a href="https://github.com/btcpayserver/btcpayserver">

View File

@ -26,7 +26,7 @@
<script src="https://code.jquery.com/jquery-3.2.1.min.js"
crossorigin="anonymous"></script>
<script type="text/javascript">
@Model.ToSrvModel()
@Model.ToJSVariableModel("srvModel")
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/1.7.1/clipboard.min.js"></script>
<script src="~/js/vue.js" type="text/javascript" defer="defer"></script>

View File

@ -19,6 +19,11 @@
<input asp-for="Amount" class="form-control" />
<span asp-validation-for="Amount" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Currency" class="control-label"></label>*
<input asp-for="Currency" class="form-control" />
<span asp-validation-for="Currency" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="OrderId" class="control-label"></label>
<input asp-for="OrderId" class="form-control" />

View File

@ -16,7 +16,15 @@
<div class="col-lg-12 text-center">
<h2 class="section-heading">@ViewData["Title"]</h2>
<hr class="primary">
<p>Create, search or pay an invoice.</p>
<p>Create, search or pay an invoice. (<a href="#help" data-toggle="collapse">Help</a>)</p>
<div id="help" class="collapse text-left">
<p>You can search for invoice Id, deposit address, price, order id, store id, any buyer information and any product information.</br>
You can also apply filters to your search by searching for `filtername:value`, here is a list of supported filters</p>
<ul>
<li><b>storeid:id</b> for filtering a specific store</li>
<li><b>status:(expired|invalid|complete|confirmed|paid|new)</b> for filtering a specific status</li>
</ul>
</div>
<div class="form-group">
<form asp-action="SearchInvoice" method="post">
<input asp-for="SearchTerm" class="form-control" />
@ -43,12 +51,12 @@
</tr>
</thead>
<tbody>
@foreach (var invoice in Model.Invoices)
@foreach(var invoice in Model.Invoices)
{
<tr>
<td>@invoice.Date</td>
<td>@invoice.InvoiceId</td>
@if (invoice.Status == "paid")
@if(invoice.Status == "paid")
{
<td>
<div class="btn-group">
@ -72,7 +80,7 @@
</tbody>
</table>
<span>
@if (Model.Skip != 0)
@if(Model.Skip != 0)
{
<a href="@Url.Action("ListInvoices", new
{

View File

@ -14,6 +14,7 @@
<tr>
<th>Name</th>
<th>Email</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@ -22,6 +23,7 @@
<tr>
<td>@user.Name</td>
<td>@user.Email</td>
<td><a asp-action="DeleteUser" asp-route-userId="@user.Id">Remove</a></td>
</tr>}
</tbody>
</table>

View File

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

View File

@ -2,6 +2,18 @@
@inject UserManager<ApplicationUser> UserManager
@inject RoleManager<IdentityRole> RoleManager
@inject BTCPayServer.Services.BTCPayServerEnvironment env
@inject BTCPayServer.NBXplorerWaiterAccessor waiter
@{
var waiterState = waiter.Instance.State;
var lastStatus = waiter.Instance.LastStatus;
var synching = waiterState == NBXplorerState.Synching &&
lastStatus.NodeBlocks.HasValue &&
lastStatus.NodeHeaders.HasValue &&
lastStatus.VerificationProgress.HasValue;
var verificationProgress = lastStatus.VerificationProgress.HasValue ? lastStatus.VerificationProgress.Value * 100 : 0.0;
}
<!DOCTYPE html>
<html lang="en">
@ -78,6 +90,63 @@
</nav>
@RenderBody()
@if(waiterState == NBXplorerState.NotConnected)
{
<!-- Modal -->
<div id="no-nbxplorer" class="modal fade" role="dialog">
<div class="modal-dialog">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">NBXplorer is not running</h4>
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
<div class="modal-body">
<p>NBXplorer is not running, BTCPay Server will not be functional.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
}
@if(synching)
{
<!-- Modal -->
<div id="synching-modal" class="modal fade" role="dialog">
<div class="modal-dialog">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Bitcoin Core node is synching...</h4>
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
<div class="modal-body">
<p>Bitcoin Core is synching (Chain height: @lastStatus.ChainHeight, Block height: @lastStatus.NodeBlocks)</p>
<p>BTCPay Server will not work correctly until it is over.</p>
<div class="progress">
<div class="progress-bar" role="progressbar" aria-valuenow="@((int)verificationProgress)"
aria-valuemin="0" aria-valuemax="100" style="width:@((int)verificationProgress)%">
@((int)verificationProgress)%
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
}
<footer class="bg-dark">
<div class="container text-right"><span style="font-size:8px;">@env.ToString()</span></div>
</footer>
@ -95,6 +164,23 @@
<!-- Custom scripts for this template -->
<script src="~/js/creative.js"></script>
@if(waiterState == NBXplorerState.NotConnected)
{
<script type="text/javascript">
$(function () {
$("#no-nbxplorer").modal();
});
</script>
}
@if(synching)
{
<script type="text/javascript">
$(function () {
$("#synching-modal").modal();
});
</script>
}
@RenderSection("Scripts", required: false)
</body>

View File

@ -16,6 +16,15 @@
<div class="row">
<div class="col-md-6">
<form method="post">
<div class="form-group">
<label asp-for="Id"></label>
<input asp-for="Id" readonly class="form-control" />
</div>
<div class="form-group">
<label asp-for="StoreName"></label>
<input asp-for="StoreName" class="form-control" />
<span asp-validation-for="StoreName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="StoreName"></label>
<input asp-for="StoreName" class="form-control" />
@ -30,6 +39,11 @@
<label asp-for="NetworkFee"></label>
<input asp-for="NetworkFee" type="checkbox" class="form-check" />
</div>
<div class="form-group">
<label asp-for="MonitoringExpiration"></label>
<input asp-for="MonitoringExpiration" class="form-control" />
<span asp-validation-for="MonitoringExpiration" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="SpeedPolicy"></label>
<select asp-for="SpeedPolicy" class="form-control">
@ -42,72 +56,77 @@
<div class="form-group">
<h5>Derivation Scheme</h5>
@if(Model.AddressSamples.Count == 0)
{
<span>The DerivationScheme represents the destination of the funds received by your invoice. It is generated by your wallet software. Please, verify that you are generating the right addresses by clicking on 'Check ExtPubKey'</span>
}
{
<span>The DerivationScheme represents the destination of the funds received by your invoice. It is generated by your wallet software. Please, verify that you are generating the right addresses by clicking on 'Check ExtPubKey'</span>
}
</div>
<div class="form-group">
<input asp-for="DerivationScheme" class="form-control" />
<span asp-validation-for="DerivationScheme" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="DerivationSchemeFormat"></label>
<select asp-for="DerivationSchemeFormat" asp-items="Model.DerivationSchemeFormats" class="form-control"></select>
</div>
<div class="form-group">
@if(Model.AddressSamples.Count == 0)
{
<table class="table">
<thead class="thead-inverse">
<tr>
<th>Address type</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>P2WPKH</td>
<td>xpub</td>
</tr>
<tr>
<td>P2SH-P2WPKH</td>
<td>xpub-[p2sh]</td>
</tr>
<tr>
<td>P2PKH</td>
<td>xpub-[legacy]</td>
</tr>
<tr>
<td>Multi-sig P2WSH</td>
<td>2-of-xpub1-xpub2</td>
</tr>
<tr>
<td>Multi-sig P2SH-P2WSH</td>
<td>2-of-xpub1-xpub2-[p2sh]</td>
</tr>
<tr>
<td>Multi-sig P2SH</td>
<td>2-of-xpub1-xpub2-[legacy]</td>
</tr>
</tbody>
</table>
}
else
{
<table class="table">
<thead class="thead-inverse">
<tr>
<th>Key path</th>
<th>Address</th>
</tr>
</thead>
<tbody>
@foreach(var sample in Model.AddressSamples)
{
<tr>
<td>@sample.KeyPath</td>
<td>@sample.Address</td>
</tr>
}
</tbody>
</table>
}
{
<span>BTCPay format memo</span>
<table class="table">
<thead class="thead-inverse">
<tr>
<th>Address type</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>P2WPKH</td>
<td>xpub</td>
</tr>
<tr>
<td>P2SH-P2WPKH</td>
<td>xpub-[p2sh]</td>
</tr>
<tr>
<td>P2PKH</td>
<td>xpub-[legacy]</td>
</tr>
<tr>
<td>Multi-sig P2WSH</td>
<td>2-of-xpub1-xpub2</td>
</tr>
<tr>
<td>Multi-sig P2SH-P2WSH</td>
<td>2-of-xpub1-xpub2-[p2sh]</td>
</tr>
<tr>
<td>Multi-sig P2SH</td>
<td>2-of-xpub1-xpub2-[legacy]</td>
</tr>
</tbody>
</table>
}
else
{
<table class="table">
<thead class="thead-inverse">
<tr>
<th>Key path</th>
<th>Address</th>
</tr>
</thead>
<tbody>
@foreach(var sample in Model.AddressSamples)
{
<tr>
<td>@sample.KeyPath</td>
<td>@sample.Address</td>
</tr>
}
</tbody>
</table>
}
</div>
<button name="command" type="submit" class="btn btn-success" value="Save">Save</button>
<button name="command" type="submit" class="btn btn-default" value="Check">Check ExtPubKey</button>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 286 KiB

After

Width:  |  Height:  |  Size: 1018 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -6,7 +6,7 @@ RUN dotnet restore
COPY BTCPayServer/. .
RUN dotnet publish --output /app/ --configuration Release
FROM microsoft/aspnetcore:2.0.0
FROM microsoft/aspnetcore:2.0.3
WORKDIR /app
RUN mkdir /datadir
@ -14,4 +14,4 @@ ENV BTCPAY_DATADIR=/datadir
VOLUME /datadir
COPY --from=builder "/app" .
ENTRYPOINT ["dotnet", "BTCPayServer.dll"]
ENTRYPOINT ["dotnet", "BTCPayServer.dll"]

View File

@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26730.16
VisualStudioVersion = 15.0.27004.2005
MinimumVisualStudioVersion = 15.0.26124.0
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BTCPayServer", "BTCPayServer\BTCPayServer.csproj", "{949A0870-8D8C-4DE5-8845-DDD560489177}"
EndProject