Compare commits

..

76 Commits

Author SHA1 Message Date
2e2c3cdec4 bump 2018-04-14 00:06:00 +09:00
6f827c86a4 Update images and bump 2018-04-13 14:34:29 +09:00
5aced90a3f Merge pull request #115 from iamvinny/master
Fix Portuguese translation
2018-04-13 10:47:37 +09:00
4646f88e3a Fix Portuguese translation 2018-04-12 18:45:05 -03:00
2b11cc1077 Simplify root key path calculation 2018-04-12 11:48:33 +09:00
77b42eb085 Do not forget to pass expiry to createinvoice on clightning 2018-04-11 18:42:19 +09:00
7de067cd7a remove unused 2018-04-10 19:12:37 +09:00
9da6df50b7 Add DOGECOIN 2018-04-10 19:07:57 +09:00
66b1623109 Merge pull request #109 from lepipele/master
Fixing ForgotPassword, updating BundleMinifier
2018-04-10 13:22:36 +09:00
2432834f3d Updating BundleMinifier, now supporting CSS variables 2018-04-09 23:13:14 -05:00
01fa483f95 Improving styling of Forgot password page
Fixes: https://github.com/btcpayserver/btcpayserver/issues/108
2018-04-09 23:12:03 -05:00
1ddf47256f Show more invoices on the invoice page, better search button 2018-04-09 17:53:43 +09:00
25fe32c3f8 Add border to table 2018-04-09 17:43:33 +09:00
ac9b8d03d7 Fix slow invoice creation 2018-04-09 16:25:31 +09:00
8fdfb2c4f6 Fix Back to Website path for Hangfire 2018-04-09 14:41:52 +09:00
b1da136f77 Update packages and remove hangfire hack 2018-04-09 14:31:39 +09:00
9a6f85fa21 Prevent full crash if lightning crash 2018-04-09 10:48:16 +09:00
7308453a74 Merge branch 'lepipele-dev-bootstrap4' 2018-04-08 14:57:54 +09:00
b798a17ef8 Updating Bootstrap4 path on POS 2018-04-08 00:28:39 -05:00
4b8899860e Migrating btn-info to btn-secondary 2018-04-08 00:25:00 -05:00
f46c8a0a0f Migrating btn-success to btn-primary 2018-04-08 00:08:15 -05:00
48832f9ac3 Updating tables not to have top border and margin as requested 2018-04-08 00:06:47 -05:00
9c798fc2e2 Bootstrap 4 custom theme to address issues from #106
Including SCSS variable changes in case we eventually want to
generate results CSS from Bootstrap SCSS
2018-04-08 00:05:00 -05:00
4704587f0a Removing unused Bootstrap 4 flavors and versions 2018-04-07 23:53:08 -05:00
58e6b63fd7 Removing legacy btn-default style 2018-04-07 23:50:34 -05:00
3c76dfb584 Migrating to btn-primary
btn-default has been removed in Bootstrap4:
https://github.com/twbs/bootstrap/issues/25029
2018-04-07 23:49:36 -05:00
10055d987d Merge remote-tracking branch 'source/master' into dev-bootstrap4 2018-04-07 23:22:11 -05:00
be49c60e83 Update lightning-charge 2018-04-08 12:29:20 +09:00
14016e2f84 Fix grammar 2018-04-07 21:34:24 +09:00
d7cb6f1cca Add a way to customize lightning invoice description 2018-04-07 16:27:46 +09:00
0f63162254 Merge branch 'dev-bootstrap4' of https://github.com/lepipele/btcpayserver into lepipele-dev-bootstrap4 2018-04-07 12:05:26 +09:00
21215dc537 Make sure a too high expiration do not trigger "The value needs to translate in milliseconds to -1 (signifying an infinite timeout)" 2018-04-07 11:53:33 +09:00
20e147edfc Fix #103 2018-04-07 02:49:26 +09:00
1048dd516b Pass itemDesc to lightning invoice (Fix #104) 2018-04-07 02:43:35 +09:00
42f44327f0 Update NBitcoin and NBXplorer 2018-04-07 02:23:12 +09:00
49200a4a9c Removed old Bootstrap, no longer needed 2018-04-06 00:16:27 -05:00
7d71757de3 Merge remote-tracking branch 'source/master' into dev-bootstrap4 2018-04-06 00:14:18 -05:00
0fb492a70f Migrating to FontAwesome
Glyphicons dropped from Bootstrap4:
https://getbootstrap.com/docs/4.0/migration/#components
New version of Glyphicons not readily available in CSS format
Using FA since it's already in solution
2018-04-06 00:14:07 -05:00
7ccc1abb95 Moving checkout CSS and JS to dedicated folder 2018-04-05 23:56:17 -05:00
d61858e260 Cleaning up CSS and JS files used for main theme 2018-04-05 23:51:55 -05:00
0ecd40f299 Removing legacy css files no longer used 2018-04-05 23:33:43 -05:00
d9d4e74126 Preserving btn-default style that's removed from Bootstrap4 2018-04-05 23:31:53 -05:00
42d04bff61 Migrating table styles 2018-04-05 23:20:12 -05:00
f9cc29f014 Removing CSS variables until NUglify is merged for bundling
Ref: https://github.com/madskristensen/BundlerMinifier/issues/306
2018-04-05 22:56:18 -05:00
992d359e79 Add a --rootpath option 2018-04-05 15:50:23 +09:00
1cc5427cbb Improve error message if you can't create an invoice in the UI 2018-04-05 15:44:27 +09:00
6270a626fb Fix checkout experience custom logo and css 2018-04-05 11:34:25 +09:00
40092b60fa Migrating navigation pills 2018-04-03 23:30:28 -05:00
5356b74490 Switching bundling to point to Bootstrap 4 2018-04-03 23:30:19 -05:00
e832ce5b4a Adding Bootstrap 4 to solution 2018-04-03 23:29:59 -05:00
a845ed88a7 bump 2018-04-03 18:01:47 +09:00
560c1c3dc0 do not use long cache provider 2018-04-03 17:56:55 +09:00
ecc5032bb2 Fix error message if invalid input lightning max value / min value. Increase cache of currency to 15 min 2018-04-03 17:54:50 +09:00
325b359ff6 Add OnChainMinValue 2018-04-03 17:39:28 +09:00
10fcc84379 Properly test PoS 2018-04-03 16:58:47 +09:00
149c29963d Add Point of Sale feature to BTCPay 2018-04-03 16:58:47 +09:00
546c39a98e Merge pull request #98 from Saevar2000/master
Update Icelandic
2018-04-02 22:17:09 +09:00
13223817a1 Update Icelandic 2018-04-02 13:05:55 +00:00
1b92314eb2 Merge pull request #97 from pajasevi/cs_lightning
Added CS translations for lightning payments
2018-04-02 22:02:54 +09:00
2b97808f1f add .vscode to .gitignore 2018-04-02 12:48:13 +00:00
8650446dcd Added CS translations for lightning payments 2018-04-02 11:57:12 +02:00
7f24b89a80 fix french mistake 2018-04-02 15:27:16 +09:00
e56ca73046 Merge pull request #95 from lepipele/dev-bugfixtrans
Bugfixing translations, they were breaking bundling
2018-04-01 14:45:54 -05:00
bf5062086c Bugfixing translations, they were breaking bundling 2018-04-01 14:43:25 -05:00
aa12167a6d Merge pull request #90 from LinoxBE/dutch-translation-update
Added Lightning related translations for Dutch
2018-03-31 22:50:56 +09:00
9fa9f62d02 Added Lightning related translations for Dutch (v2) 2018-03-31 15:27:16 +02:00
c9615b660e Merge branch 'master' of https://github.com/btcpayserver/btcpayserver into dutch-translation-update 2018-03-31 15:23:27 +02:00
0ac51f479f Merge pull request #92 from junderw/fixJA2
Update JA
2018-03-30 22:20:47 -05:00
37649fc77b Update JA 2018-03-31 10:12:24 +09:00
ca4585eee9 Merge pull request #91 from marcosrdz/patch-1
Update es.js
2018-03-30 13:45:47 -05:00
83a1492cd4 Update es.js 2018-03-30 12:52:33 -04:00
a1af694acb Added Lightning related translations for Dutch 2018-03-30 13:08:46 +02:00
6330c0f0d7 Merge pull request #88 from felipehuicochea/patch-1
Update es.js
2018-03-30 17:36:18 +09:00
224c569ed1 French translation 2018-03-30 17:35:41 +09:00
c608987526 Rename peer info to node info 2018-03-30 17:34:46 +09:00
aca67d6eae Update es.js
Minor typos and grammar fixes
2018-03-30 01:28:55 -05:00
149 changed files with 18441 additions and 28384 deletions

2
.gitignore vendored
View File

@ -291,3 +291,5 @@ __pycache__/
# Bundling JS/CSS
BTCPayServer/wwwroot/bundles/*
!BTCPayServer/wwwroot/bundles/.gitignore
.vscode

View File

@ -8,7 +8,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.6.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.0" />
<PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
</ItemGroup>

View File

@ -102,7 +102,7 @@ namespace BTCPayServer.Tests
.ConfigureServices(s =>
{
var mockRates = new MockRateProviderFactory();
var btc = new MockRateProvider("BTC", new Rate("USD", 5000m));
var btc = new MockRateProvider("BTC", new Rate("USD", 5000m), new Rate("CAD", 4500m));
var ltc = new MockRateProvider("LTC", new Rate("USD", 500m));
mockRates.AddMock(btc);
mockRates.AddMock(ltc);
@ -120,7 +120,6 @@ namespace BTCPayServer.Tests
.Build();
_Host.Start();
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
((LightningLikePaymentHandler)_Host.Services.GetService(typeof(IPaymentMethodHandler<LightningSupportedPaymentMethod>))).SkipP2PTest = !InContainer;
}
public string HostName

View File

@ -1,4 +1,4 @@
FROM microsoft/dotnet:2.0.5-sdk-2.1.4
FROM microsoft/dotnet:2.0.6-sdk-2.1.101-stretch
WORKDIR /app
# caches restore result by copying csproj file separately
COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj
@ -9,4 +9,4 @@ RUN dotnet restore
# copies the rest of your code
COPY . ../.
ENTRYPOINT ["dotnet", "test"]
ENTRYPOINT ["dotnet", "test"]

View File

@ -54,6 +54,11 @@ namespace BTCPayServer.Tests
return CreateStoreAsync().GetAwaiter().GetResult();
}
public T GetController<T>() where T : Controller
{
return parent.PayTester.GetController<T>(UserId);
}
public async Task<StoresController> CreateStoreAsync()
{
var store = parent.PayTester.GetController<UserStoresController>(UserId);
@ -127,7 +132,8 @@ namespace BTCPayServer.Tests
{
Url = connectionType == LightningConnectionType.Charge ? parent.MerchantCharge.Client.Uri.AbsoluteUri :
connectionType == LightningConnectionType.CLightning ? parent.MerchantLightningD.Address.AbsoluteUri
: throw new NotSupportedException(connectionType.ToString())
: throw new NotSupportedException(connectionType.ToString()),
SkipPortTest = true
}, "save", "BTC");
if (storeController.ModelState.ErrorCount != 0)
Assert.False(true, storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage);

View File

@ -30,6 +30,8 @@ using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.HostedServices;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Services.Apps;
namespace BTCPayServer.Tests
{
@ -866,6 +868,125 @@ namespace BTCPayServer.Tests
Assert.Equal($"{tpub}-[p2sh]", result.ToString());
}
[Fact]
public void CanSetPaymentMethodLimits()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
user.RegisterLightningNode("BTC", LightningConnectionType.Charge);
var vm = Assert.IsType<CheckoutExperienceViewModel>(Assert.IsType<ViewResult>(user.GetController<StoresController>().CheckoutExperience(user.StoreId).Result).Model);
vm.LightningMaxValue = "2 USD";
vm.OnChainMinValue = "5 USD";
Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().CheckoutExperience(user.StoreId, vm).Result);
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 1.5,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
Assert.Single(invoice.CryptoInfo);
Assert.Equal(PaymentTypes.LightningLike.ToString(), invoice.CryptoInfo[0].PaymentType);
invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5.5,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
Assert.Single(invoice.CryptoInfo);
Assert.Equal(PaymentTypes.BTCLike.ToString(), invoice.CryptoInfo[0].PaymentType);
}
}
[Fact]
public void CanUsePoSApp()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var apps = user.GetController<AppsController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
vm.Name = "test";
vm.SelectedAppType = AppType.PointOfSale.ToString();
Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result);
var appId = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model).Apps[0].Id;
var vmpos = Assert.IsType<UpdatePointOfSaleViewModel>(Assert.IsType<ViewResult>(apps.UpdatePointOfSale(appId).Result).Model);
vmpos.Title = "hello";
vmpos.Currency = "CAD";
vmpos.Template =
"apple:\n" +
" price: 5.0\n" +
" title: good apple\n" +
"orange:\n" +
" price: 10.0\n";
Assert.IsType<RedirectToActionResult>(apps.UpdatePointOfSale(appId, vmpos).Result);
vmpos = Assert.IsType<UpdatePointOfSaleViewModel>(Assert.IsType<ViewResult>(apps.UpdatePointOfSale(appId).Result).Model);
Assert.Equal("hello", vmpos.Title);
var vmview = Assert.IsType<ViewPointOfSaleViewModel>(Assert.IsType<ViewResult>(apps.ViewPointOfSale(appId).Result).Model);
Assert.Equal("hello", vmview.Title);
Assert.Equal(2, vmview.Items.Length);
Assert.Equal("good apple", vmview.Items[0].Title);
Assert.Equal("orange", vmview.Items[1].Title);
Assert.Equal(10.0m, vmview.Items[1].Price.Value);
Assert.Equal("$5.00", vmview.Items[0].Price.Formatted);
Assert.IsType<RedirectResult>(apps.ViewPointOfSale(appId, "orange").Result);
var invoice = user.BitPay.GetInvoices().First();
Assert.Equal(10.00, invoice.Price);
Assert.Equal("CAD", invoice.Currency);
Assert.Equal("orange", invoice.ItemDesc);
}
}
[Fact]
public void CanCreateAndDeleteApps()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var user2 = tester.NewAccount();
user2.GrantAccess();
var apps = user.GetController<AppsController>();
var apps2 = user2.GetController<AppsController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
Assert.NotNull(vm.SelectedAppType);
Assert.Null(vm.Name);
vm.Name = "test";
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result);
Assert.Equal(nameof(apps.ListApps), redirectToAction.ActionName);
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model);
var appList2 = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps2.ListApps().Result).Model);
Assert.Single(appList.Apps);
Assert.Empty(appList2.Apps);
Assert.Equal("test", appList.Apps[0].AppName);
Assert.True(appList.Apps[0].IsOwner);
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id).Result);
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id).Result);
redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result);
Assert.Equal(nameof(apps.ListApps), redirectToAction.ActionName);
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model);
Assert.Empty(appList.Apps);
}
}
[Fact]
public void InvoiceFlowThroughDifferentStatesCorrectly()
{
@ -1043,7 +1164,7 @@ namespace BTCPayServer.Tests
private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx)
{
var h = BitcoinAddress.Create(invoice.BitcoinAddress).ScriptPubKey.Hash.ToString();
var h = BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest).ScriptPubKey.Hash.ToString();
return ctx.AddressInvoices.FirstOrDefault(i => i.InvoiceDataId == invoice.Id && i.GetAddress() == h) != null;
}

View File

@ -46,7 +46,7 @@ services:
- lightning-charged
nbxplorer:
image: nicolasdorier/nbxplorer:1.0.1.24
image: nicolasdorier/nbxplorer:1.0.1.32
ports:
- "32838:32838"
expose:
@ -89,7 +89,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: nicolasdorier/clightning
image: nicolasdorier/clightning:0.0.0.3
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_OPT: |
@ -110,11 +110,10 @@ services:
- bitcoind
lightning-charged:
image: shesek/lightning-charge:0.3.5
image: shesek/lightning-charge:0.3.9
environment:
NETWORK: regtest
API_TOKEN: foiewnccewuify
SKIP_BITCOIND: 1
BITCOIND_RPCCONNECT: bitcoind
volumes:
- "bitcoin_datadir:/etc/bitcoin"
@ -130,7 +129,7 @@ services:
- merchant_lightningd
merchant_lightningd:
image: nicolasdorier/clightning
image: nicolasdorier/clightning:0.0.0.5-dev
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_OPT: |
@ -139,6 +138,7 @@ services:
ipaddr=merchant_lightningd
network=regtest
log-level=debug
dev-broadcast-interval=1000
ports:
- "30993:9835" # api port
expose:

View File

@ -72,6 +72,12 @@ namespace BTCPayServer
public override string ToString()
{
return CryptoCode;
}
}
internal KeyPath GetRootKeyPath()
{
return new KeyPath(NBitcoinNetwork.Consensus.SupportSegwit ? "49'" : "44'")
.Derive(CoinType);
}
}
}

View File

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitDogecoin()
{
NBitcoin.Altcoins.Dogecoin.EnsureRegistered();
var ltcRate = new CoinAverageRateProvider("DOGE");
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("DOGE");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
BlockExplorerLink = NBXplorerNetworkProvider.ChainType == ChainType.Main ? "https://dogechain.info/tx/{0}" : "https://dogechain.info/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "dogecoin",
DefaultRateProvider = ltcRate,
CryptoImagePath = "imlegacy/dogecoin.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType),
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("3'") : new KeyPath("1'")
});
}
}
}

View File

@ -12,7 +12,7 @@ namespace BTCPayServer
{
public void InitLitecoin()
{
NBXplorer.Altcoins.Litecoin.Networks.EnsureRegistered();
NBitcoin.Altcoins.Litecoin.EnsureRegistered();
var ltcRate = new CoinAverageRateProvider("LTC");
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("LTC");
@ -27,7 +27,7 @@ namespace BTCPayServer
CryptoImagePath = "imlegacy/litecoin-symbol.svg",
LightningImagePath = "imlegacy/ltc-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType),
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("2'") : new KeyPath("3'")
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("2'") : new KeyPath("1'")
});
}
}

View File

@ -47,6 +47,7 @@ namespace BTCPayServer
ChainType = chainType;
InitBitcoin();
InitLitecoin();
InitDogecoin();
}
/// <summary>

View File

@ -2,21 +2,25 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.0.1.71</Version>
<Version>1.0.1.81</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Build\dockerfiles\**" />
<Compile Remove="wwwroot\bundles\jqueryvalidate\**" />
<Compile Remove="wwwroot\css\**" />
<Compile Remove="wwwroot\vendor\jquery-nice-select\**" />
<Content Remove="Build\dockerfiles\**" />
<Content Remove="wwwroot\bundles\jqueryvalidate\**" />
<Content Remove="wwwroot\css\**" />
<Content Remove="wwwroot\vendor\jquery-nice-select\**" />
<EmbeddedResource Remove="Build\dockerfiles\**" />
<EmbeddedResource Remove="wwwroot\bundles\jqueryvalidate\**" />
<EmbeddedResource Remove="wwwroot\css\**" />
<EmbeddedResource Remove="wwwroot\vendor\jquery-nice-select\**" />
<None Remove="Build\dockerfiles\**" />
<None Remove="wwwroot\bundles\jqueryvalidate\**" />
<None Remove="wwwroot\css\**" />
<None Remove="wwwroot\vendor\jquery-nice-select\**" />
</ItemGroup>
<ItemGroup>
@ -26,8 +30,8 @@
<EmbeddedResource Include="Currencies.txt" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BuildBundlerMinifier" Version="2.6.362" />
<PackageReference Include="Hangfire" Version="1.6.17" />
<PackageReference Include="BuildBundlerMinifier" Version="2.6.375" />
<PackageReference Include="Hangfire" Version="1.6.19" />
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.1" />
<PackageReference Include="LedgerWallet" Version="1.0.1.32" />
@ -35,10 +39,10 @@
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="1.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.0" />
<PackageReference Include="NBitcoin" Version="4.0.0.65" />
<PackageReference Include="NBitcoin" Version="4.1.0.10" />
<PackageReference Include="NBitpayClient" Version="1.0.0.18" />
<PackageReference Include="DBreeze" Version="1.87.0" />
<PackageReference Include="NBXplorer.Client" Version="1.0.1.16" />
<PackageReference Include="NBXplorer.Client" Version="1.0.1.23" />
<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" />
@ -49,8 +53,9 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.1" PrivateAssets="All" />
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.2" PrivateAssets="All" />
<PackageReference Include="YamlDotNet" Version="4.3.1" />
</ItemGroup>
<ItemGroup>
@ -60,13 +65,8 @@
</ItemGroup>
<ItemGroup>
<None Include="wwwroot\js\checkout\core.js" />
<None Include="wwwroot\js\creative.js" />
<None Include="wwwroot\js\creative.min.js" />
<None Include="wwwroot\js\site.js" />
<None Include="wwwroot\js\site.min.js" />
<None Include="wwwroot\vendor\bootstrap\js\bootstrap.js" />
<None Include="wwwroot\vendor\bootstrap\js\bootstrap.min.js" />
<None Include="wwwroot\checkout\js\core.js" />
<None Include="wwwroot\main\js\creative.js" />
<None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.svg" />
<None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.woff2" />
<None Include="wwwroot\vendor\font-awesome\less\animated.less" />

View File

@ -92,11 +92,15 @@ namespace BTCPayServer.Configuration
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
ExternalUrl = conf.GetOrDefault<Uri>("externalurl", null);
RootPath = conf.GetOrDefault<string>("rootpath", "/");
if (!RootPath.StartsWith("/", StringComparison.InvariantCultureIgnoreCase))
RootPath = "/" + RootPath;
var old = conf.GetOrDefault<Uri>("internallightningnode", null);
if(old != null)
throw new ConfigException($"internallightningnode should not be used anymore, use btclightning instead");
}
public string RootPath { get; set; }
public Dictionary<string, LightningConnectionString> InternalLightningByCryptoCode { get; set; } = new Dictionary<string, LightningConnectionString>();
public BTCPayNetworkProvider NetworkProvider { get; set; }
@ -115,5 +119,14 @@ namespace BTCPayServer.Configuration
get;
set;
}
internal string GetRootUri()
{
if (ExternalUrl == null)
return null;
UriBuilder builder = new UriBuilder(ExternalUrl);
builder.Path = RootPath;
return builder.ToString();
}
}
}

View File

@ -31,6 +31,9 @@ namespace BTCPayServer.Configuration
app.Option("--regtest | -regtest", $"Use regtest (Deprecated, use --network instead)", CommandOptionType.BoolValue);
app.Option("--chains | -c", $"Chains to support comma separated (default: btc, available: {chains})", CommandOptionType.SingleValue);
app.Option("--postgres", $"Connection string to postgres database (default: sqlite is used)", 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("--bundlejscss", $"Bundle javascript and css files for better performance (default: true)", CommandOptionType.SingleValue);
app.Option("--rootpath", "The root path in the URL to access BTCPay (default: /)", CommandOptionType.SingleValue);
foreach (var network in provider.GetAll())
{
var crypto = network.CryptoCode.ToLowerInvariant();
@ -38,8 +41,6 @@ namespace BTCPayServer.Configuration
app.Option($"--{crypto}explorercookiefile", $"Path to the cookie file (default: {network.NBXplorerNetwork.DefaultSettings.DefaultCookieFile})", CommandOptionType.SingleValue);
app.Option($"--{crypto}lightning", $"Easy configuration of lightning for the server adnistrator: Must be a unix socket of CLightning (lightning-rpc) or URL to a charge server (default: empty)", 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("--bundlejscss", $"Bundle javascript and css files for better performance (default: true)", CommandOptionType.SingleValue);
return app;
}

View File

@ -0,0 +1,198 @@
using System;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NBitcoin.DataEncoders;
using NBitcoin;
using BTCPayServer.Services.Apps;
using Newtonsoft.Json;
using YamlDotNet.RepresentationModel;
using System.IO;
using BTCPayServer.Services.Rates;
using System.Globalization;
namespace BTCPayServer.Controllers
{
public partial class AppsController
{
public class PointOfSaleSettings
{
public PointOfSaleSettings()
{
Title = "My awesome Point of Sale";
Currency = "USD";
Template =
"tea:\n" +
" price: 0.02\n" +
" title: Green Tea # title is optional, defaults to the keys\n\n" +
"coffee:\n" +
" price: 1\n\n" +
"bamba:\n" +
" price: 3\n\n" +
"beer:\n" +
" price: 7\n\n" +
"hat:\n" +
" price: 15\n\n" +
"tshirt:\n" +
" price: 25";
}
public string Title { get; set; }
public string Currency { get; set; }
public string Template { get; set; }
}
[HttpGet]
[Route("{appId}/settings/pos")]
public async Task<IActionResult> UpdatePointOfSale(string appId)
{
var app = await GetOwnedApp(appId, AppType.PointOfSale);
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
return View(new UpdatePointOfSaleViewModel() { Title = settings.Title, Currency = settings.Currency, Template = settings.Template });
}
[HttpPost]
[Route("{appId}/settings/pos")]
public async Task<IActionResult> UpdatePointOfSale(string appId, UpdatePointOfSaleViewModel vm)
{
if (_Currencies.GetCurrencyData(vm.Currency) == null)
ModelState.AddModelError(nameof(vm.Currency), "Invalid currency");
try
{
Parse(vm.Template, vm.Currency);
}
catch
{
ModelState.AddModelError(nameof(vm.Template), "Invalid template");
}
if (!ModelState.IsValid)
{
return View(vm);
}
var app = await GetOwnedApp(appId, AppType.PointOfSale);
if (app == null)
return NotFound();
app.SetSettings(new PointOfSaleSettings()
{
Title = vm.Title,
Currency = vm.Currency.ToUpperInvariant(),
Template = vm.Template
});
await UpdateAppSettings(app);
StatusMessage = "App updated";
return RedirectToAction(nameof(UpdatePointOfSale));
}
[HttpGet]
[Route("{appId}/pos")]
public async Task<IActionResult> ViewPointOfSale(string appId)
{
var app = await GetApp(appId, AppType.PointOfSale);
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
return View(new ViewPointOfSaleViewModel()
{
Title = settings.Title,
Items = Parse(settings.Template, settings.Currency)
});
}
private async Task<AppData> GetApp(string appId, AppType appType)
{
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.Apps
.Where(us => us.Id == appId && us.AppType == appType.ToString())
.FirstOrDefaultAsync();
}
}
private ViewPointOfSaleViewModel.Item[] Parse(string template, string currency)
{
var input = new StringReader(template);
YamlStream stream = new YamlStream();
stream.Load(input);
var root = (YamlMappingNode)stream.Documents[0].RootNode;
return root
.Children
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlMappingNode })
.Where(kv => kv.Value != null)
.Select(c => new ViewPointOfSaleViewModel.Item()
{
Id = c.Key,
Title = c.Value.Children
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlScalarNode })
.Where(kv => kv.Value != null)
.Where(cc => cc.Key == "title")
.FirstOrDefault()?.Value?.Value ?? c.Key,
Price = c.Value.Children
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlScalarNode })
.Where(kv => kv.Value != null)
.Where(cc => cc.Key == "price")
.Select(cc => new ViewPointOfSaleViewModel.Item.ItemPrice()
{
Value = decimal.Parse(cc.Value.Value, CultureInfo.InvariantCulture),
Formatted = FormatCurrency(cc.Value.Value, currency)
})
.Single()
})
.ToArray();
}
string FormatCurrency(string price, string currency)
{
return decimal.Parse(price, CultureInfo.InvariantCulture).ToString("C", _Currencies.GetCurrencyProvider(currency));
}
[HttpPost]
[Route("{appId}/pos")]
public async Task<IActionResult> ViewPointOfSale(string appId, string choiceKey)
{
var app = await GetApp(appId, AppType.PointOfSale);
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
var choices = Parse(settings.Template, settings.Currency);
var choice = choices.FirstOrDefault(c => c.Id == choiceKey);
if (choice == null)
return NotFound();
var store = await GetStore(app);
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
{
ItemDesc = choice.Title,
Currency = settings.Currency,
Price = (double)choice.Price.Value,
}, store, HttpContext.Request.GetAbsoluteRoot());
return Redirect(invoice.Data.Url);
}
private async Task<StoreData> GetStore(AppData app)
{
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.Stores.FirstOrDefaultAsync(s => s.Id == app.StoreDataId);
}
}
private async Task UpdateAppSettings(AppData app)
{
using (var ctx = _ContextFactory.CreateContext())
{
ctx.Apps.Add(app);
ctx.Entry<AppData>(app).State = EntityState.Modified;
ctx.Entry<AppData>(app).Property(a => a.Settings).IsModified = true;
await ctx.SaveChangesAsync();
}
}
}
}

View File

@ -0,0 +1,203 @@
using System;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NBitcoin.DataEncoders;
using NBitcoin;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Rates;
namespace BTCPayServer.Controllers
{
[AutoValidateAntiforgeryToken]
[Route("apps")]
public partial class AppsController : Controller
{
ApplicationDbContextFactory _ContextFactory;
UserManager<ApplicationUser> _UserManager;
CurrencyNameTable _Currencies;
InvoiceController _InvoiceController;
[TempData]
public string StatusMessage { get; set; }
public AppsController(
UserManager<ApplicationUser> userManager,
ApplicationDbContextFactory contextFactory,
CurrencyNameTable currencies,
InvoiceController invoiceController)
{
_InvoiceController = invoiceController;
_UserManager = userManager;
_ContextFactory = contextFactory;
_Currencies = currencies;
}
public async Task<IActionResult> ListApps()
{
var apps = await GetAllApps();
return View(new ListAppsViewModel()
{
Apps = apps
});
}
[HttpPost]
[Route("{appId}/delete")]
public async Task<IActionResult> DeleteAppPost(string appId)
{
var appData = await GetOwnedApp(appId);
if (appData == null)
return NotFound();
if (await DeleteApp(appData))
StatusMessage = "App removed successfully";
return RedirectToAction(nameof(ListApps));
}
[HttpGet]
[Route("create")]
public async Task<IActionResult> CreateApp()
{
var stores = await GetOwnedStores();
if (stores.Length == 0)
{
StatusMessage = "Error: You must have created at least one store";
return RedirectToAction(nameof(ListApps));
}
var vm = new CreateAppViewModel();
vm.SetStores(stores);
return View(vm);
}
[HttpPost]
[Route("create")]
public async Task<IActionResult> CreateApp(CreateAppViewModel vm)
{
var stores = await GetOwnedStores();
if (stores.Length == 0)
{
StatusMessage = "Error: You must own at least one store";
return RedirectToAction(nameof(ListApps));
}
var selectedStore = vm.SelectedStore;
vm.SetStores(stores);
vm.SelectedStore = selectedStore;
if (!Enum.TryParse<AppType>(vm.SelectedAppType, out AppType appType))
ModelState.AddModelError(nameof(vm.SelectedAppType), "Invalid App Type");
if (!ModelState.IsValid)
{
return View(vm);
}
if (!stores.Any(s => s.Id == selectedStore))
{
StatusMessage = "Error: You are not owner of this store";
return RedirectToAction(nameof(ListApps));
}
using (var ctx = _ContextFactory.CreateContext())
{
var id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32));
var appData = new AppData() { Id = id };
appData.StoreDataId = selectedStore;
appData.Name = vm.Name;
appData.AppType = appType.ToString();
ctx.Apps.Add(appData);
await ctx.SaveChangesAsync();
}
StatusMessage = "App successfully created";
return RedirectToAction(nameof(ListApps));
}
[HttpGet]
[Route("{appId}/delete")]
public async Task<IActionResult> DeleteApp(string appId)
{
var appData = await GetOwnedApp(appId);
if (appData == null)
return NotFound();
return View("Confirm", new ConfirmModel()
{
Title = $"Delete app {appData.Name} ({appData.AppType})",
Description = "This app will be removed from this store",
Action = "Delete"
});
}
private async Task<AppData> GetOwnedApp(string appId, AppType? type = null)
{
var userId = GetUserId();
using (var ctx = _ContextFactory.CreateContext())
{
var app = await ctx.UserStore
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
.SelectMany(us => us.StoreData.Apps.Where(a => a.Id == appId))
.FirstOrDefaultAsync();
if (type != null && type.Value.ToString() != app.AppType)
return null;
return app;
}
}
private async Task<StoreData[]> GetOwnedStores()
{
var userId = GetUserId();
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.UserStore
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
.Select(u => u.StoreData)
.ToArrayAsync();
}
}
private async Task<bool> DeleteApp(AppData appData)
{
using (var ctx = _ContextFactory.CreateContext())
{
ctx.Apps.Add(appData);
ctx.Entry<AppData>(appData).State = EntityState.Deleted;
return await ctx.SaveChangesAsync() == 1;
}
}
private async Task<ListAppsViewModel.ListAppViewModel[]> GetAllApps()
{
var userId = GetUserId();
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.UserStore
.Where(us => us.ApplicationUserId == userId)
.Select(us => new
{
IsOwner = us.Role == StoreRoles.Owner,
StoreId = us.StoreDataId,
StoreName = us.StoreData.StoreName,
Apps = us.StoreData.Apps
})
.SelectMany(us => us.Apps.Select(app => new ListAppsViewModel.ListAppViewModel()
{
IsOwner = us.IsOwner,
AppName = app.Name,
AppType = app.AppType,
Id = app.Id,
StoreId = us.StoreId,
StoreName = us.StoreName
}))
.ToArrayAsync();
}
}
private string GetUserId()
{
return _UserManager.GetUserId(User);
}
}
}

View File

@ -358,7 +358,7 @@ namespace BTCPayServer.Controllers
[Route("invoices")]
[Authorize(AuthenticationSchemes = "Identity.Application")]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 20)
public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 50)
{
var model = new InvoicesModel();
var filterString = new SearchString(searchTerm);
@ -443,11 +443,14 @@ namespace BTCPayServer.Controllers
StatusMessage = null;
if (store.Role != StoreRoles.Owner)
{
StatusMessage = "Error: You need to be owner of this store to create an invoice";
ModelState.AddModelError(nameof(model.StoreId), "You need to be owner of this store to create an invoice");
return View(model);
}
if (store.GetSupportedPaymentMethods(_NetworkProvider).Count() == 0)
{
StatusMessage = "Error: You need to configure the derivation scheme in order to create an invoice";
ModelState.AddModelError(nameof(model.StoreId), "You need to configure the derivation scheme in order to create an invoice");
return View(model);
}
if(StatusMessage != null)

View File

@ -119,7 +119,7 @@ namespace BTCPayServer.Controllers
.Where(c => c.Network != null)
.Select(o =>
(SupportedPaymentMethod: o.SupportedPaymentMethod,
PaymentMethod: CreatePaymentMethodAsync(o.Handler, o.SupportedPaymentMethod, o.Network, entity, storeBlob)))
PaymentMethod: CreatePaymentMethodAsync(o.Handler, o.SupportedPaymentMethod, o.Network, entity, store)))
.ToList();
List<string> paymentMethodErrors = new List<string>();
@ -149,7 +149,7 @@ namespace BTCPayServer.Controllers
{
StringBuilder errors = new StringBuilder();
errors.AppendLine("No payment method available for this store");
foreach(var error in paymentMethodErrors)
foreach (var error in paymentMethodErrors)
{
errors.AppendLine(error);
}
@ -183,34 +183,50 @@ namespace BTCPayServer.Controllers
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
}
private async Task<PaymentMethod> CreatePaymentMethodAsync(IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreBlob storeBlob)
private async Task<PaymentMethod> CreatePaymentMethodAsync(IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreData store)
{
var storeBlob = store.GetStoreBlob();
var rate = await storeBlob.ApplyRateRules(network, _RateProviders.GetRateProvider(network, false)).GetRateAsync(entity.ProductInformation.Currency);
PaymentMethod paymentMethod = new PaymentMethod();
paymentMethod.ParentEntity = entity;
paymentMethod.Network = network;
paymentMethod.SetId(supportedPaymentMethod.PaymentId);
paymentMethod.Rate = rate;
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, network);
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network);
if (storeBlob.NetworkFeeDisabled)
paymentDetails.SetNoTxFee();
paymentMethod.SetPaymentMethodDetails(paymentDetails);
// Check if Lightning Max value is exceeded
Func<Money, Money, bool> compare = null;
CurrencyValue limitValue = null;
string errorMessage = null;
if (supportedPaymentMethod.PaymentId.PaymentType == PaymentTypes.LightningLike &&
storeBlob.LightningMaxValue != null)
{
var lightningMaxValue = storeBlob.LightningMaxValue;
var lightningMaxValueRate = 0.0m;
if (lightningMaxValue.Currency == entity.ProductInformation.Currency)
lightningMaxValueRate = paymentMethod.Rate;
else
lightningMaxValueRate = await storeBlob.ApplyRateRules(network, _RateProviders.GetRateProvider(network, false)).GetRateAsync(lightningMaxValue.Currency);
compare = (a, b) => a > b;
limitValue = storeBlob.LightningMaxValue;
errorMessage = "The amount of the invoice is too high to be paid with lightning";
}
else if (supportedPaymentMethod.PaymentId.PaymentType == PaymentTypes.BTCLike &&
storeBlob.OnChainMinValue != null)
{
compare = (a, b) => a < b;
limitValue = storeBlob.OnChainMinValue;
errorMessage = "The amount of the invoice is too low to be paid on chain";
}
var lightningMaxValueCrypto = Money.Coins(lightningMaxValue.Value / lightningMaxValueRate);
if (paymentMethod.Calculate().Due > lightningMaxValueCrypto)
if (compare != null)
{
var limitValueRate = 0.0m;
if (limitValue.Currency == entity.ProductInformation.Currency)
limitValueRate = paymentMethod.Rate;
else
limitValueRate = await storeBlob.ApplyRateRules(network, _RateProviders.GetRateProvider(network, false)).GetRateAsync(limitValue.Currency);
var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate);
if (compare(paymentMethod.Calculate().Due, limitValueCrypto))
{
throw new PaymentMethodUnavailableException("Lightning max value exceeded");
throw new PaymentMethodUnavailableException(errorMessage);
}
}
///////////////
@ -262,11 +278,6 @@ namespace BTCPayServer.Controllers
buyerInformation.BuyerZip = buyerInformation.BuyerZip ?? buyer.zip;
}
private DerivationStrategyBase ParseDerivationStrategy(string derivationStrategy, BTCPayNetwork network)
{
return new DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationStrategy);
}
private TDest Map<TFrom, TDest>(TFrom data)
{
return JsonConvert.DeserializeObject<TDest>(JsonConvert.SerializeObject(data));

View File

@ -26,10 +26,16 @@ namespace BTCPayServer.Controllers
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
var network = cryptoCode == null ? null : _ExplorerProvider.GetNetwork(cryptoCode);
if (network == null)
{
return NotFound();
}
DerivationSchemeViewModel vm = new DerivationSchemeViewModel();
vm.ServerUrl = GetStoreUrl(storeId);
vm.CryptoCode = cryptoCode;
vm.RootKeyPath = network.GetRootKeyPath();
SetExistingValues(store, vm);
return View(vm);
}
@ -63,6 +69,7 @@ namespace BTCPayServer.Controllers
{
return NotFound();
}
vm.RootKeyPath = network.GetRootKeyPath();
var wallet = _WalletProvider.GetWallet(network);
if (wallet == null)
{
@ -204,7 +211,7 @@ namespace BTCPayServer.Controllers
{
try
{
destinationAddress = BitcoinAddress.Create(destination);
destinationAddress = BitcoinAddress.Create(destination, network.NBitcoinNetwork);
}
catch { }
if (destinationAddress == null)
@ -251,8 +258,6 @@ namespace BTCPayServer.Controllers
if (command == "getxpub")
{
var getxpubResult = await hw.GetExtPubKey(network, account);
;
getxpubResult.CoinType = (int)(getxpubResult.KeyPath.Indexes[1] - 0x80000000);
result = getxpubResult;
}
if (command == "getinfo")

View File

@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Mvc;
using BTCPayServer.Payments.Lightning;
using System.Net;
using BTCPayServer.Data;
using System.Threading;
namespace BTCPayServer.Controllers
{
@ -127,13 +128,20 @@ namespace BTCPayServer.Controllers
try
{
var info = await handler.Test(paymentMethod, network);
if (!vm.SkipPortTest)
{
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)))
{
await handler.TestConnection(info, cts.Token);
}
}
vm.StatusMessage = $"Connection to the lightning node succeed ({info})";
}
catch (Exception ex)
{
vm.StatusMessage = $"Error: {ex.Message}";
return View(vm);
}
}
return View(vm);
}
}

View File

@ -195,9 +195,10 @@ namespace BTCPayServer.Controllers
vm.SetCryptoCurrencies(_ExplorerProvider, store.GetDefaultCrypto());
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? "";
vm.AllowCoinConversion = storeBlob.AllowCoinConversion;
vm.CustomCSS = storeBlob.CustomCSS;
vm.CustomLogo = storeBlob.CustomLogo;
vm.CustomCSS = storeBlob.CustomCSS?.AbsoluteUri;
vm.CustomLogo = storeBlob.CustomLogo?.AbsoluteUri;
return View(vm);
}
@ -205,14 +206,24 @@ namespace BTCPayServer.Controllers
[Route("{storeId}/checkout")]
public async Task<IActionResult> CheckoutExperience(string storeId, CheckoutExperienceViewModel model)
{
CurrencyValue currencyValue = null;
CurrencyValue lightningMaxValue = null;
if (!string.IsNullOrWhiteSpace(model.LightningMaxValue))
{
if (!CurrencyValue.TryParse(model.LightningMaxValue, out currencyValue))
if (!CurrencyValue.TryParse(model.LightningMaxValue, out lightningMaxValue))
{
ModelState.AddModelError(nameof(model.LightningMaxValue), "Invalid currency value");
ModelState.AddModelError(nameof(model.LightningMaxValue), "Invalid lightning max value");
}
}
CurrencyValue onchainMinValue = null;
if (!string.IsNullOrWhiteSpace(model.OnChainMinValue))
{
if (!CurrencyValue.TryParse(model.OnChainMinValue, out onchainMinValue))
{
ModelState.AddModelError(nameof(model.OnChainMinValue), "Invalid on chain min value");
}
}
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
@ -225,11 +236,17 @@ namespace BTCPayServer.Controllers
}
model.SetCryptoCurrencies(_ExplorerProvider, model.DefaultCryptoCurrency);
model.SetLanguages(_LangService, model.DefaultLang);
if(!ModelState.IsValid)
{
return View(model);
}
blob.DefaultLang = model.DefaultLang;
blob.AllowCoinConversion = model.AllowCoinConversion;
blob.LightningMaxValue = currencyValue;
blob.CustomLogo = model.CustomLogo;
blob.CustomCSS = model.CustomCSS;
blob.LightningMaxValue = lightningMaxValue;
blob.OnChainMinValue = onchainMinValue;
blob.CustomLogo = string.IsNullOrWhiteSpace(model.CustomLogo) ? null : new Uri(model.CustomLogo, UriKind.Absolute);
blob.CustomCSS = string.IsNullOrWhiteSpace(model.CustomCSS) ? null : new Uri(model.CustomCSS, UriKind.Absolute);
if (store.SetStoreBlob(blob))
{
needUpdate = true;
@ -265,6 +282,7 @@ namespace BTCPayServer.Controllers
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
vm.RateMultiplier = (double)storeBlob.GetRateMultiplier();
vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate;
vm.PreferredExchange = storeBlob.PreferredExchange.IsCoinAverage() ? "coinaverage" : storeBlob.PreferredExchange;
return View(vm);
}
@ -339,6 +357,7 @@ namespace BTCPayServer.Controllers
blob.NetworkFeeDisabled = !model.NetworkFee;
blob.MonitoringExpiration = model.MonitoringExpiration;
blob.InvoiceExpiration = model.InvoiceExpiration;
blob.LightningDescriptionTemplate = model.LightningDescriptionTemplate ?? string.Empty;
bool newExchange = blob.PreferredExchange != model.PreferredExchange;
blob.PreferredExchange = model.PreferredExchange;

View File

@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace BTCPayServer.Data
{
public class AppData
{
public string Id { get; set; }
public string Name { get; set; }
public string StoreDataId
{
get; set;
}
public string AppType { get; set; }
public StoreData StoreData
{
get; set;
}
public DateTimeOffset Created
{
get; set;
}
public string Settings { get; set; }
public T GetSettings<T>() where T : class, new()
{
if (String.IsNullOrEmpty(Settings))
return new T();
return JsonConvert.DeserializeObject<T>(Settings);
}
public void SetSettings(object value)
{
Settings = value == null ? null : JsonConvert.SerializeObject(value);
}
}
}

View File

@ -26,6 +26,11 @@ namespace BTCPayServer.Data
get; set;
}
public DbSet<AppData> Apps
{
get; set;
}
public DbSet<InvoiceEventData> InvoiceEvents
{
get; set;
@ -107,6 +112,10 @@ namespace BTCPayServer.Data
t.StoreDataId
});
builder.Entity<AppData>()
.HasOne(a => a.StoreData);
builder.Entity<UserStore>()
.HasOne(pt => pt.ApplicationUser)
.WithMany(p => p.UserStores)

View File

@ -30,6 +30,11 @@ namespace BTCPayServer.Data
get; set;
}
public List<AppData> Apps
{
get; set;
}
[Obsolete("Use GetDerivationStrategies instead")]
public string DerivationStrategy
{
@ -257,12 +262,28 @@ namespace BTCPayServer.Data
[JsonConverter(typeof(CurrencyValueJsonConverter))]
public CurrencyValue LightningMaxValue { get; set; }
[JsonConverter(typeof(CurrencyValueJsonConverter))]
public CurrencyValue OnChainMinValue { get; set; }
[JsonConverter(typeof(UriJsonConverter))]
public Uri CustomLogo { get; set; }
[JsonConverter(typeof(UriJsonConverter))]
public Uri CustomCSS { get; set; }
string _LightningDescriptionTemplate;
public string LightningDescriptionTemplate
{
get
{
return _LightningDescriptionTemplate ?? "Paid to {StoreName} (Order ID: {OrderId})";
}
set
{
_LightningDescriptionTemplate = value;
}
}
public IRateProvider ApplyRateRules(BTCPayNetwork network, IRateProvider rateProvider)
{
if (!PreferredExchange.IsCoinAverage())

View File

@ -43,6 +43,9 @@ namespace BTCPayServer
}
}
if(!Network.Consensus.SupportSegwit)
hintedLabels.Add("legacy");
try
{
var result = new DerivationStrategyFactory(Network).Parse(str);

View File

@ -25,6 +25,9 @@ using System.Net.WebSockets;
using BTCPayServer.Services.Invoices;
using NBitpayClient;
using BTCPayServer.Payments;
using Microsoft.AspNetCore.Identity;
using BTCPayServer.Models;
using System.Security.Claims;
namespace BTCPayServer
{

View File

@ -209,16 +209,16 @@ namespace BTCPayServer.HostedServices
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
try
{
var now = DateTimeOffset.UtcNow;
if (invoice.ExpirationTime > now)
var delay = invoice.ExpirationTime - DateTimeOffset.UtcNow;
if (delay > TimeSpan.Zero)
{
await Task.Delay(invoice.ExpirationTime - now, _Cts.Token);
await Task.Delay(delay, _Cts.Token);
}
Watch(invoiceId);
now = DateTimeOffset.UtcNow;
if (invoice.MonitoringExpiration > now)
delay = invoice.MonitoringExpiration - DateTimeOffset.UtcNow;
if (delay > TimeSpan.Zero)
{
await Task.Delay(invoice.MonitoringExpiration - now, _Cts.Token);
await Task.Delay(delay, _Cts.Token);
}
Watch(invoiceId);
}

View File

@ -76,8 +76,6 @@ namespace BTCPayServer.Hosting
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// Big hack, tests fails because Hangfire fail at initializing at the second test run
AddHangfireFix(services);
services.AddBTCPayServer();
services.AddMvc(o =>
{
@ -93,6 +91,24 @@ namespace BTCPayServer.Hosting
options.Password.RequireUppercase = false;
});
services.AddHangfire((o) =>
{
var scope = AspNetCoreJobActivator.Current.BeginScope(null);
var options = (ApplicationDbContextFactory)scope.Resolve(typeof(ApplicationDbContextFactory));
options.ConfigureHangfireBuilder(o);
});
services.AddCors(o =>
{
o.AddPolicy("BitpayAPI", b =>
{
b.AllowAnyMethod().AllowAnyHeader().AllowAnyOrigin();
});
});
services.Configure<IOptions<ApplicationInsightsServiceOptions>>(o =>
{
o.Value.DeveloperMode = _Env.IsDevelopment();
});
// Needed to debug U2F for ledger support
//services.Configure<KestrelServerOptions>(kestrel =>
//{
@ -103,51 +119,29 @@ namespace BTCPayServer.Hosting
//});
}
// Big hack, tests fails if only call AddHangfire because Hangfire fail at initializing at the second test run
private void AddHangfireFix(IServiceCollection services)
{
Action<IGlobalConfiguration> configuration = o =>
{
var scope = AspNetCoreJobActivator.Current.BeginScope(null);
var options = (ApplicationDbContextFactory)scope.Resolve(typeof(ApplicationDbContextFactory));
options.ConfigureHangfireBuilder(o);
};
ServiceCollectionDescriptorExtensions.TryAddSingleton<Action<IGlobalConfiguration>>(services, (IServiceProvider serviceProvider) => new Action<IGlobalConfiguration>((config) =>
{
ILoggerFactory service = ServiceProviderServiceExtensions.GetService<ILoggerFactory>(serviceProvider);
if (service != null)
{
Hangfire.GlobalConfigurationExtensions.UseLogProvider<AspNetCoreLogProvider>(config, new AspNetCoreLogProvider(service));
}
IServiceScopeFactory service2 = ServiceProviderServiceExtensions.GetService<IServiceScopeFactory>(serviceProvider);
if (service2 != null)
{
Hangfire.GlobalConfigurationExtensions.UseActivator<AspNetCoreJobActivator>(config, new AspNetCoreJobActivator(service2));
}
configuration(config);
}));
services.AddHangfire(configuration);
services.AddCors(o =>
{
o.AddPolicy("BitpayAPI", b =>
{
b.AllowAnyMethod().AllowAnyHeader().AllowAnyOrigin();
});
});
services.Configure<IOptions<ApplicationInsightsServiceOptions>>(o =>
{
o.Value.DeveloperMode = _Env.IsDevelopment();
});
}
public void Configure(
IApplicationBuilder app,
IHostingEnvironment env,
IServiceProvider prov,
BTCPayServerOptions options,
ILoggerFactory loggerFactory)
{
Logs.Configure(loggerFactory);
Logs.Configuration.LogInformation($"Root Path: {options.RootPath}");
if (options.RootPath.Equals("/", StringComparison.OrdinalIgnoreCase))
{
ConfigureCore(app, env, prov, loggerFactory, options);
}
else
{
app.Map(options.RootPath, appChild =>
{
ConfigureCore(appChild, env, prov, loggerFactory, options);
});
}
}
private static void ConfigureCore(IApplicationBuilder app, IHostingEnvironment env, IServiceProvider prov, ILoggerFactory loggerFactory, BTCPayServerOptions options)
{
if (env.IsDevelopment())
{
@ -155,9 +149,6 @@ namespace BTCPayServer.Hosting
app.UseBrowserLink();
}
Logs.Configure(loggerFactory);
//App insight do not that by itself...
loggerFactory.AddApplicationInsights(prov, LogLevel.Information);
@ -165,7 +156,11 @@ namespace BTCPayServer.Hosting
app.UseStaticFiles();
app.UseAuthentication();
app.UseHangfireServer();
app.UseHangfireDashboard("/hangfire", new DashboardOptions() { Authorization = new[] { new NeedRole(Roles.ServerAdmin) } });
app.UseHangfireDashboard("/hangfire", new DashboardOptions()
{
AppPath = options.GetRootUri(),
Authorization = new[] { new NeedRole(Roles.ServerAdmin) }
});
app.UseWebSockets();
app.UseMvc(routes =>
{

View File

@ -0,0 +1,537 @@
// <auto-generated />
using BTCPayServer.Data;
using BTCPayServer.Services.Invoices;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using System;
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20180402095640_appdata")]
partial class appdata
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.0.1-rtm-125");
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.Property<string>("Address")
.ValueGeneratedOnAdd();
b.Property<DateTimeOffset?>("CreatedTime");
b.Property<string>("InvoiceDataId");
b.HasKey("Address");
b.HasIndex("InvoiceDataId");
b.ToTable("AddressInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("AppType");
b.Property<DateTimeOffset>("Created");
b.Property<string>("Name");
b.Property<string>("Settings");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("StoreDataId");
b.ToTable("Apps");
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{
b.Property<string>("InvoiceDataId");
b.Property<string>("Address");
b.Property<DateTimeOffset>("Assigned");
b.Property<string>("CryptoCode");
b.Property<DateTimeOffset?>("UnAssigned");
b.HasKey("InvoiceDataId", "Address");
b.ToTable("HistoricalAddressInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<DateTimeOffset>("Created");
b.Property<string>("CustomerEmail");
b.Property<string>("ExceptionStatus");
b.Property<string>("ItemCode");
b.Property<string>("OrderId");
b.Property<string>("Status");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("StoreDataId");
b.ToTable("Invoices");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
{
b.Property<string>("InvoiceDataId");
b.Property<string>("UniqueId");
b.Property<string>("Message");
b.Property<DateTimeOffset>("Timestamp");
b.HasKey("InvoiceDataId", "UniqueId");
b.ToTable("InvoiceEvents");
});
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Facade");
b.Property<string>("Label");
b.Property<DateTimeOffset>("PairingTime");
b.Property<string>("SIN");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("SIN");
b.HasIndex("StoreDataId");
b.ToTable("PairedSINData");
});
modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("DateCreated");
b.Property<DateTimeOffset>("Expiration");
b.Property<string>("Facade");
b.Property<string>("Label");
b.Property<string>("SIN");
b.Property<string>("StoreDataId");
b.Property<string>("TokenValue");
b.HasKey("Id");
b.ToTable("PairingCodes");
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<bool>("Accounted");
b.Property<byte[]>("Blob");
b.Property<string>("InvoiceDataId");
b.HasKey("Id");
b.HasIndex("InvoiceDataId");
b.ToTable("Payments");
});
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.HasKey("Id");
b.ToTable("PendingInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<string>("InvoiceDataId");
b.HasKey("Id");
b.HasIndex("InvoiceDataId");
b.ToTable("RefundAddresses");
});
modelBuilder.Entity("BTCPayServer.Data.SettingData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Value");
b.HasKey("Id");
b.ToTable("Settings");
});
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("DefaultCrypto");
b.Property<string>("DerivationStrategies");
b.Property<string>("DerivationStrategy");
b.Property<int>("SpeedPolicy");
b.Property<byte[]>("StoreBlob");
b.Property<byte[]>("StoreCertificate");
b.Property<string>("StoreName");
b.Property<string>("StoreWebsite");
b.HasKey("Id");
b.ToTable("Stores");
});
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
{
b.Property<string>("ApplicationUserId");
b.Property<string>("StoreDataId");
b.Property<string>("Role");
b.HasKey("ApplicationUserId", "StoreDataId");
b.HasIndex("StoreDataId");
b.ToTable("UserStore");
});
modelBuilder.Entity("BTCPayServer.Models.ApplicationUser", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("AccessFailedCount");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Email")
.HasMaxLength(256);
b.Property<bool>("EmailConfirmed");
b.Property<bool>("LockoutEnabled");
b.Property<DateTimeOffset?>("LockoutEnd");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256);
b.Property<string>("NormalizedUserName")
.HasMaxLength(256);
b.Property<string>("PasswordHash");
b.Property<string>("PhoneNumber");
b.Property<bool>("PhoneNumberConfirmed");
b.Property<bool>("RequiresEmailConfirmation");
b.Property<string>("SecurityStamp");
b.Property<bool>("TwoFactorEnabled");
b.Property<string>("UserName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasName("UserNameIndex");
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Name")
.HasMaxLength(256);
b.Property<string>("NormalizedName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasName("RoleNameIndex");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("RoleId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider");
b.Property<string>("ProviderKey");
b.Property<string>("ProviderDisplayName");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("RoleId");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("LoginProvider");
b.Property<string>("Name");
b.Property<string>("Value");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("AddressInvoices")
.HasForeignKey("InvoiceDataId");
});
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("Apps")
.HasForeignKey("StoreDataId");
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData")
.WithMany("HistoricalAddressInvoices")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany()
.HasForeignKey("StoreDataId");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData")
.WithMany("Events")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("Payments")
.HasForeignKey("InvoiceDataId");
});
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("RefundAddresses")
.HasForeignKey("InvoiceDataId");
});
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser")
.WithMany("UserStores")
.HasForeignKey("ApplicationUserId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("UserStores")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,45 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace BTCPayServer.Migrations
{
public partial class appdata : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Apps",
columns: table => new
{
Id = table.Column<string>(nullable: false),
AppType = table.Column<string>(nullable: true),
Created = table.Column<DateTimeOffset>(nullable: false),
Name = table.Column<string>(nullable: true),
Settings = table.Column<string>(nullable: true),
StoreDataId = table.Column<string>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Apps", x => x.Id);
table.ForeignKey(
name: "FK_Apps_Stores_StoreDataId",
column: x => x.StoreDataId,
principalTable: "Stores",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_Apps_StoreDataId",
table: "Apps",
column: "StoreDataId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Apps");
}
}
}

View File

@ -1,9 +1,12 @@
// <auto-generated />
using BTCPayServer.Data;
using BTCPayServer.Services.Invoices;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using System;
namespace BTCPayServer.Migrations
@ -33,6 +36,28 @@ namespace BTCPayServer.Migrations
b.ToTable("AddressInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("AppType");
b.Property<DateTimeOffset>("Created");
b.Property<string>("Name");
b.Property<string>("Settings");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("StoreDataId");
b.ToTable("Apps");
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{
b.Property<string>("InvoiceDataId");
@ -404,6 +429,13 @@ namespace BTCPayServer.Migrations
.HasForeignKey("InvoiceDataId");
});
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("Apps")
.HasForeignKey("StoreDataId");
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData")

View File

@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Services.Apps;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace BTCPayServer.Models.AppViewModels
{
public class CreateAppViewModel
{
public CreateAppViewModel()
{
SetApps();
}
class Format
{
public string Name { get; set; }
public string Value { get; set; }
}
[Required]
[MaxLength(50)]
[MinLength(1)]
public string Name { get; set; }
[Display(Name = "Store")]
public string SelectedStore { get; set; }
public void SetStores(StoreData[] stores)
{
var defaultStore = stores[0].Id;
var choices = stores.Select(o => new Format() { Name = o.StoreName, Value = o.Id }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Value == defaultStore) ?? choices.FirstOrDefault();
Stores = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
SelectedStore = chosen.Value;
}
public SelectList Stores { get; set; }
[Display(Name = "App type")]
public string SelectedAppType { get; set; }
public SelectList AppTypes { get; set; }
void SetApps()
{
var defaultAppType = AppType.PointOfSale.ToString();
var choices = typeof(AppType).GetEnumNames().Select(o => new Format() { Name = o, Value = o }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Value == defaultAppType) ?? choices.FirstOrDefault();
AppTypes = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
SelectedAppType = chosen.Value;
}
}
}

View File

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.AppViewModels
{
public class ListAppsViewModel
{
public class ListAppViewModel
{
public string Id { get; set; }
public string StoreName { get; set; }
public string StoreId { get; set; }
public string AppName { get; set; }
public string AppType { get; set; }
public bool IsOwner { get; set; }
public string UpdateAction { get { return "Update" + AppType; } }
public string ViewAction { get { return "View" + AppType; } }
}
public ListAppViewModel[] Apps { get; set; }
}
}

View File

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.AppViewModels
{
public class UpdatePointOfSaleViewModel
{
[Required]
[MaxLength(30)]
public string Title { get; set; }
[Required]
[MaxLength(5)]
public string Currency { get; set; }
[Required]
[MaxLength(5000)]
public string Template { get; set; }
}
}

View File

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.AppViewModels
{
public class ViewPointOfSaleViewModel
{
public class Item
{
public class ItemPrice
{
public string Formatted { get; set; }
public decimal Value { get; set; }
}
public string Id { get; set; }
public ItemPrice Price { get; set; }
public string Title { get; set; }
}
public string Title { get; set; }
public Item[] Items { get; set; }
}
}

View File

@ -42,7 +42,6 @@ namespace BTCPayServer.Models
{
//"url":"https://test.bitpay.com/invoice?id=9saCHtp1zyPcNoi3rDdBu8"
[JsonProperty("url")]
[Obsolete("Use CryptoInfo.Url instead")]
public string Url
{
get; set;

View File

@ -31,12 +31,17 @@ namespace BTCPayServer.Models.StoreViewModels
[MaxLength(20)]
public string LightningMaxValue { get; set; }
[Display(Name = "Do not propose on chain payment if the value of the invoice is below...")]
[MaxLength(20)]
public string OnChainMinValue { get; set; }
[Display(Name = "Link to a custom CSS stylesheet")]
[Url]
public Uri CustomCSS { get; set; }
public string CustomCSS { get; set; }
[Display(Name = "Link to a custom logo")]
[Url]
public Uri CustomLogo { get; set; }
public string CustomLogo { get; set; }
public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string defaultCrypto)
{
var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray();

View File

@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Rendering;
using NBitcoin;
namespace BTCPayServer.Models.StoreViewModels
{
@ -29,5 +30,6 @@ namespace BTCPayServer.Models.StoreViewModels
public string ServerUrl { get; set; }
public string StatusMessage { get; internal set; }
public KeyPath RootKeyPath { get; set; }
}
}

View File

@ -23,5 +23,6 @@ namespace BTCPayServer.Models.StoreViewModels
}
public string StatusMessage { get; set; }
public string InternalLightningNode { get; internal set; }
public bool SkipPortTest { get; set; }
}
}

View File

@ -64,7 +64,7 @@ namespace BTCPayServer.Models.StoreViewModels
}
[Display(Name = "Invoice expires if the full amount has not been paid after ... minutes")]
[Range(1, 60 * 24 * 31)]
[Range(1, 60 * 24 * 24)]
public int InvoiceExpiration
{
get;
@ -72,7 +72,7 @@ namespace BTCPayServer.Models.StoreViewModels
}
[Display(Name = "Payment invalid if transactions fails to confirm ... minutes after invoice expiration")]
[Range(10, 60 * 24 * 31)]
[Range(10, 60 * 24 * 24)]
public int MonitoringExpiration
{
get;
@ -91,6 +91,9 @@ namespace BTCPayServer.Models.StoreViewModels
get; set;
}
[Display(Name = "Description template of the lightning invoice")]
public string LightningDescriptionTemplate { get; set; }
public class LightningNode
{
public string CryptoCode { get; set; }
@ -100,6 +103,5 @@ namespace BTCPayServer.Models.StoreViewModels
{
get; set;
} = new List<LightningNode>();
}
}

View File

@ -27,7 +27,7 @@ namespace BTCPayServer.Payments.Bitcoin
_WalletProvider = walletProvider;
}
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(DerivationStrategy supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network)
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(DerivationStrategy supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network)
{
if (!_ExplorerProvider.IsAvailable(network))
throw new PaymentMethodUnavailableException($"Full node not available");

View File

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Services.Invoices;
namespace BTCPayServer.Payments
@ -16,25 +17,26 @@ namespace BTCPayServer.Payments
/// </summary>
/// <param name="supportedPaymentMethod"></param>
/// <param name="paymentMethod"></param>
/// <param name="store"></param>
/// <param name="network"></param>
/// <returns></returns>
Task<IPaymentMethodDetails> CreatePaymentMethodDetails(ISupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network);
Task<IPaymentMethodDetails> CreatePaymentMethodDetails(ISupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network);
}
public interface IPaymentMethodHandler<T> : IPaymentMethodHandler where T : ISupportedPaymentMethod
{
Task<IPaymentMethodDetails> CreatePaymentMethodDetails(T supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network);
Task<IPaymentMethodDetails> CreatePaymentMethodDetails(T supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network);
}
public abstract class PaymentMethodHandlerBase<T> : IPaymentMethodHandler<T> where T : ISupportedPaymentMethod
{
public abstract Task<IPaymentMethodDetails> CreatePaymentMethodDetails(T supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network);
public abstract Task<IPaymentMethodDetails> CreatePaymentMethodDetails(T supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network);
Task<IPaymentMethodDetails> IPaymentMethodHandler.CreatePaymentMethodDetails(ISupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network)
Task<IPaymentMethodDetails> IPaymentMethodHandler.CreatePaymentMethodDetails(ISupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network)
{
if (supportedPaymentMethod is T method)
{
return CreatePaymentMethodDetails(method, paymentMethod, network);
return CreatePaymentMethodDetails(method, paymentMethod, store, network);
}
throw new NotSupportedException("Invalid supportedPaymentMethod");
}

View File

@ -174,10 +174,10 @@ namespace BTCPayServer.Payments.Lightning.CLightning
}
static NBitcoin.DataEncoders.DataEncoder InvoiceIdEncoder = NBitcoin.DataEncoders.Encoders.Base58;
async Task<LightningInvoice> ILightningInvoiceClient.CreateInvoice(LightMoney amount, TimeSpan expiry, CancellationToken cancellation)
async Task<LightningInvoice> ILightningInvoiceClient.CreateInvoice(LightMoney amount, string description, TimeSpan expiry, CancellationToken cancellation)
{
var id = InvoiceIdEncoder.EncodeData(RandomUtils.GetBytes(20));
var invoice = await SendCommandAsync<CLightningInvoice>("invoice", new object[] { amount.MilliSatoshi, id, "" }, cancellation: cancellation);
var invoice = await SendCommandAsync<CLightningInvoice>("invoice", new object[] { amount.MilliSatoshi, id, description ?? "", Math.Max(0, (int)expiry.TotalSeconds) }, cancellation: cancellation);
invoice.Label = id;
invoice.MilliSatoshi = amount;
invoice.Status = "unpaid";

View File

@ -48,8 +48,10 @@ namespace BTCPayServer.Payments.Lightning.Charge
{
var message = CreateMessage(HttpMethod.Post, "invoice");
Dictionary<string, string> parameters = new Dictionary<string, string>();
parameters.Add("msatoshi", request.Amont.MilliSatoshi.ToString(CultureInfo.InvariantCulture));
parameters.Add("msatoshi", request.Amount.MilliSatoshi.ToString(CultureInfo.InvariantCulture));
parameters.Add("expiry", ((int)request.Expiry.TotalSeconds).ToString(CultureInfo.InvariantCulture));
if(request.Description != null)
parameters.Add("description", request.Description);
message.Content = new FormUrlEncodedContent(parameters);
var result = await _Client.SendAsync(message, cancellation);
result.EnsureSuccessStatusCode();
@ -130,6 +132,8 @@ namespace BTCPayServer.Payments.Lightning.Charge
async Task<LightningInvoice> ILightningInvoiceClient.GetInvoice(string invoiceId, CancellationToken cancellation)
{
var invoice = await GetInvoice(invoiceId, cancellation);
if (invoice == null)
return null;
return ChargeClient.ToLightningInvoice(invoice);
}
@ -150,9 +154,9 @@ namespace BTCPayServer.Payments.Lightning.Charge
};
}
async Task<LightningInvoice> ILightningInvoiceClient.CreateInvoice(LightMoney amount, TimeSpan expiry, CancellationToken cancellation)
async Task<LightningInvoice> ILightningInvoiceClient.CreateInvoice(LightMoney amount, string description, TimeSpan expiry, CancellationToken cancellation)
{
var invoice = await CreateInvoiceAsync(new CreateInvoiceRequest() { Amont = amount, Expiry = expiry });
var invoice = await CreateInvoiceAsync(new CreateInvoiceRequest() { Amount = amount, Expiry = expiry, Description = description ?? "" });
return new LightningInvoice() { Id = invoice.Id, Amount = amount, BOLT11 = invoice.PayReq, Status = "unpaid" };
}

View File

@ -7,7 +7,8 @@ namespace BTCPayServer.Payments.Lightning.Charge
{
public class CreateInvoiceRequest
{
public LightMoney Amont { get; set; }
public LightMoney Amount { get; set; }
public TimeSpan Expiry { get; set; }
public string Description { get; set; }
}
}

View File

@ -29,7 +29,7 @@ namespace BTCPayServer.Payments.Lightning
public interface ILightningInvoiceClient
{
Task<LightningInvoice> GetInvoice(string invoiceId, CancellationToken cancellation = default(CancellationToken));
Task<LightningInvoice> CreateInvoice(LightMoney amount, TimeSpan expiry, CancellationToken cancellation = default(CancellationToken));
Task<LightningInvoice> CreateInvoice(LightMoney amount, string description, TimeSpan expiry, CancellationToken cancellation = default(CancellationToken));
Task<ILightningListenInvoiceSession> Listen(CancellationToken cancellation = default(CancellationToken));
Task<LightningNodeInformation> GetInfo(CancellationToken cancellation = default(CancellationToken));
}

View File

@ -5,6 +5,7 @@ using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Payments.Lightning.Charge;
using BTCPayServer.Payments.Lightning.CLightning;
@ -23,8 +24,9 @@ namespace BTCPayServer.Payments.Lightning
_LightningClientFactory = lightningClientFactory;
_Dashboard = dashboard;
}
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network)
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network)
{
var storeBlob = store.GetStoreBlob();
var test = Test(supportedPaymentMethod, network);
var invoice = paymentMethod.ParentEntity;
var due = Extensions.RoundUp(invoice.ProductInformation.Price / paymentMethod.Rate, 8);
@ -36,9 +38,13 @@ namespace BTCPayServer.Payments.Lightning
LightningInvoice lightningInvoice = null;
try
{
lightningInvoice = await client.CreateInvoice(new LightMoney(due, LightMoneyUnit.BTC), expiry);
string description = storeBlob.LightningDescriptionTemplate;
description = description.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase)
.Replace("{ItemDescription}", invoice.ProductInformation.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase)
.Replace("{OrderId}", invoice.OrderId ?? "", StringComparison.OrdinalIgnoreCase);
lightningInvoice = await client.CreateInvoice(new LightMoney(due, LightMoneyUnit.BTC), description, expiry);
}
catch(Exception ex)
catch (Exception ex)
{
throw new PaymentMethodUnavailableException($"Impossible to create lightning invoice ({ex.Message})", ex);
}
@ -51,11 +57,6 @@ namespace BTCPayServer.Payments.Lightning
};
}
/// <summary>
/// Used for testing
/// </summary>
public bool SkipP2PTest { get; set; }
public async Task<NodeInfo> Test(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
{
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
@ -88,38 +89,34 @@ namespace BTCPayServer.Payments.Lightning
throw new PaymentMethodUnavailableException($"The lightning is not synched ({blocksGap} blocks)");
}
return new NodeInfo(info.NodeId, info.Address, info.P2PPort);
}
public async Task TestConnection(NodeInfo nodeInfo, CancellationToken cancellation)
{
try
{
if (!SkipP2PTest)
IPAddress address = null;
try
{
await TestConnection(info.Address, info.P2PPort, cts.Token);
address = IPAddress.Parse(nodeInfo.Host);
}
catch
{
address = (await Dns.GetHostAddressesAsync(nodeInfo.Host)).FirstOrDefault();
}
if (address == null)
throw new PaymentMethodUnavailableException($"DNS did not resolved {nodeInfo.Host}");
using (var tcp = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp))
{
await WithTimeout(tcp.ConnectAsync(new IPEndPoint(address, nodeInfo.Port)), cancellation);
}
}
catch (Exception ex)
{
throw new PaymentMethodUnavailableException($"Error while connecting to the lightning node via {info.Address}:{info.P2PPort} ({ex.Message})");
}
return new NodeInfo(info.NodeId, info.Address, info.P2PPort);
}
private async Task TestConnection(string addressStr, int port, CancellationToken cancellation)
{
IPAddress address = null;
try
{
address = IPAddress.Parse(addressStr);
}
catch
{
address = (await Dns.GetHostAddressesAsync(addressStr)).FirstOrDefault();
}
if (address == null)
throw new PaymentMethodUnavailableException($"DNS did not resolved {addressStr}");
using (var tcp = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp))
{
await WithTimeout(tcp.ConnectAsync(new IPEndPoint(address, port)), cancellation);
throw new PaymentMethodUnavailableException($"Error while connecting to the lightning node via {nodeInfo.Host}:{nodeInfo.Port} ({ex.Message})");
}
}

View File

@ -52,9 +52,20 @@ namespace BTCPayServer.Payments.Lightning
_ListenPoller = new Timer(async s =>
{
await Task.WhenAll((await _InvoiceRepository.GetPendingInvoices())
.Select(async invoiceId => await EnsureListening(invoiceId, true))
.ToArray());
try
{
await Task.WhenAll((await _InvoiceRepository.GetPendingInvoices())
.Select(async invoiceId => await EnsureListening(invoiceId, true))
.ToArray());
}
catch (AggregateException ex)
{
Logs.PayServer.LogError(ex.InnerException ?? ex.InnerExceptions.FirstOrDefault(), $"Lightning: Uncaught error");
}
catch (Exception ex)
{
Logs.PayServer.LogError(ex, $"Lightning: Uncaught error");
}
}, null, 0, (int)PollInterval.TotalMilliseconds);
leases.Add(_ListenPoller);
return Task.CompletedTask;
@ -90,7 +101,16 @@ namespace BTCPayServer.Payments.Lightning
if (poll)
{
var charge = _LightningClientFactory.CreateClient(lightningSupportedMethod, network);
var chargeInvoice = await charge.GetInvoice(lightningMethod.InvoiceId);
LightningInvoice chargeInvoice = null;
try
{
chargeInvoice = await charge.GetInvoice(lightningMethod.InvoiceId);
}
catch(Exception ex)
{
Logs.PayServer.LogError(ex, $"{lightningSupportedMethod.CryptoCode} (Lightning): Can't connect to the lightning server");
continue;
}
if (chargeInvoice == null)
continue;
if (chargeInvoice.Status == "paid")

View File

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Services.Apps
{
public enum AppType
{
PointOfSale
}
}

View File

@ -1,4 +1,5 @@
using System;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Net.WebSockets;
@ -82,12 +83,13 @@ namespace BTCPayServer.Services
if (network == null)
throw new ArgumentNullException(nameof(network));
var path = new KeyPath("49'").Derive(network.CoinType).Derive(account, true);
var segwit = network.NBitcoinNetwork.Consensus.SupportSegwit;
var path = network.GetRootKeyPath().Derive(account, true);
var pubkey = await GetExtPubKey(_Ledger, network, path, false);
var derivation = new DerivationStrategyFactory(network.NBitcoinNetwork).CreateDirectDerivationStrategy(pubkey, new DerivationStrategyOptions()
{
P2SH = true,
Legacy = false
P2SH = segwit,
Legacy = !segwit
});
return new GetXPubResult() { ExtPubKey = derivation.ToString(), KeyPath = path };
}
@ -125,9 +127,13 @@ namespace BTCPayServer.Services
private static async Task<KeyPath> GetKeyPath(LedgerClient ledger, BTCPayNetwork network, DirectDerivationStrategy directStrategy)
{
List<KeyPath> derivations = new List<KeyPath>();
if(network.NBitcoinNetwork.Consensus.SupportSegwit)
derivations.Add(new KeyPath("49'"));
derivations.Add(new KeyPath("44'"));
KeyPath foundKeyPath = null;
foreach (var account in
new[] { new KeyPath("49'"), new KeyPath("44'") }
derivations
.Select(purpose => purpose.Derive(network.CoinType))
.SelectMany(coinType => Enumerable.Range(0, 5).Select(i => coinType.Derive(i, true))))
{
@ -235,6 +241,5 @@ namespace BTCPayServer.Services
public string ExtPubKey { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.KeyPathJsonConverter))]
public KeyPath KeyPath { get; set; }
public int CoinType { get; internal set; }
}
}

View File

@ -26,11 +26,14 @@ namespace BTCPayServer.Services.Rates
if (cache == null)
throw new ArgumentNullException(nameof(cache));
_Cache = cache;
// Using same providers because they are both at 15 min actually...
_Providers = _LongCacheProviders;
}
public IRateProvider RateProvider { get; set; }
public TimeSpan CacheSpan { get; set; } = TimeSpan.FromMinutes(1.0);
// We use 15 min because of limits with free version of bitcoinaverage
public TimeSpan CacheSpan { get; set; } = TimeSpan.FromMinutes(15.0);
public TimeSpan LongCacheSpan { get; set; } = TimeSpan.FromMinutes(15.0);
public IRateProvider GetRateProvider(BTCPayNetwork network, bool longCache)
{

View File

@ -22,7 +22,7 @@
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-default">Register</button>
<button type="submit" class="btn btn-primary">Register</button>
</form>
</div>
</div>

View File

@ -3,23 +3,37 @@
ViewData["Title"] = "Forgot your password?";
}
<h2>@ViewData["Title"]</h2>
<h4>Enter your email.</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="ForgotPassword" method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Email"></label>
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
<section>
<div class="container">
<div class="row">
<div class="col-lg-12 text-center">
@Html.Partial("_StatusMessage", TempData["StatusMessage"])
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
</div>
<div class="row">
<div class="col-lg-12 text-center">
<h2 class="section-heading">@ViewData["Title"]</h2>
<hr class="primary">
</div>
<div class="col-md-4">
<form asp-action="ForgotPassword" method="post">
<h4>Start password reset</h4>
<hr />
<p>
We all forget passwords every now and then. Just provide email address tied to your account and we'll start the process of helping you recover your account.
</p>
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Email"></label>
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
</div>
</div>
</div>
</section>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}

View File

@ -1,8 +1,22 @@
@{
ViewData["Title"] = "Forgot password confirmation";
ViewData["Title"] = "Email sent!";
}
<h2>@ViewData["Title"]</h2>
<h2></h2>
<p>
Please check your email to reset your password.
</p>
<section>
<div class="container">
<div class="row">
<div class="col-md-4">
<h4>@ViewData["Title"]</h4>
<hr />
<p>
Please check your email to reset your password.
</p>
</div>
</div>
</div>
</section>

View File

@ -41,7 +41,7 @@
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-default">Log in</button>
<button type="submit" class="btn btn-primary">Log in</button>
</div>
<div class="form-group">
<p>
@ -76,7 +76,7 @@
<p>
@foreach(var provider in loginProviders)
{
<button type="submit" class="btn btn-default" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.Name</button>
<button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.Name</button>
}
</p>
</div>

View File

@ -31,7 +31,7 @@
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-default">Log in</button>
<button type="submit" class="btn btn-primary">Log in</button>
</div>
</form>
</div>

View File

@ -18,7 +18,7 @@
<input asp-for="RecoveryCode" class="form-control" autocomplete="off" />
<span asp-validation-for="RecoveryCode" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-default">Log in</button>
<button type="submit" class="btn btn-primary">Log in</button>
</form>
</div>
</div>

View File

@ -35,7 +35,7 @@
<input asp-for="ConfirmPassword" class="form-control" />
<span asp-validation-for="ConfirmPassword" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-default">Register</button>
<button type="submit" class="btn btn-primary">Register</button>
</form>
</div>
</div>

View File

@ -26,7 +26,7 @@
<input asp-for="ConfirmPassword" class="form-control" />
<span asp-validation-for="ConfirmPassword" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-default">Reset</button>
<button type="submit" class="btn btn-primary">Reset</button>
</form>
</div>
</div>

View File

@ -0,0 +1,38 @@
@model CreateAppViewModel
@{
ViewData["Title"] = "Create a new app";
}
<section>
<div class="container">
<div class="row">
<div class="col-lg-12 text-center">
<h2 class="section-heading">@ViewData["Title"]</h2>
<hr class="primary">
</div>
</div>
<div class="row">
<div class="col-lg-12">
<form asp-action="CreateApp">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Name" class="control-label"></label>*
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="SelectedAppType" class="control-label"></label>*
<select asp-for="SelectedAppType" asp-items="Model.AppTypes" class="form-control"></select>
</div>
<div class="form-group">
<label asp-for="SelectedStore" class="control-label"></label>*
<select asp-for="SelectedStore" asp-items="Model.Stores" class="form-control"></select>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
<a asp-action="ListApps">Back to the app list</a>
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,64 @@
@model ListAppsViewModel
@{
ViewData["Title"] = "Stores";
}
<section>
<div class="container">
<div class="row">
<div class="col-lg-12 text-center">
@Html.Partial("_StatusMessage", TempData["TempDataProperty-StatusMessage"])
</div>
</div>
<div class="row">
<div class="col-lg-12 text-center">
<h2 class="section-heading">@ViewData["Title"]</h2>
<hr class="primary">
<p>Create and manage apps.</p>
</div>
</div>
<div class="row">
<a asp-action="CreateApp" class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Create a new app</a>
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Store</th>
<th>Name</th>
<th>App type</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var app in Model.Apps)
{
<tr>
<td>
@if (app.IsOwner)
{
<span><a asp-action="UpdateStore" asp-controller="Stores" asp-route-storeId="@app.StoreId">@app.StoreName</a></span>
}
else
{
<span>@app.StoreName</span>
}
</td>
<td>@app.AppName</td>
<td>@app.AppType</td>
<td style="text-align:right">
@if (app.IsOwner)
{
<a asp-action="@app.UpdateAction" asp-controller="Apps" asp-route-appId="@app.Id">Settings</a><span> - </span>
}
<a asp-action="@app.ViewAction" asp-controller="Apps" asp-route-appId="@app.Id">View</a><span> - </span>
<a asp-action="DeleteApp" asp-route-appId="@app.Id">Remove</a>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</section>

View File

@ -0,0 +1,45 @@
@model UpdatePointOfSaleViewModel
@{
ViewData["Title"] = "Update Point of Sale";
}
<section>
<div class="container">
<div class="row">
<div class="col-lg-12 text-center">
<h2 class="section-heading">@ViewData["Title"]</h2>
<hr class="primary">
</div>
</div>
<div class="row">
<div class="col-lg-12 text-center">
@Html.Partial("_StatusMessage", TempData["TempDataProperty-StatusMessage"])
</div>
</div>
<div class="row">
<div class="col-lg-12">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Title" class="control-label"></label>*
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" 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="Template" class="control-label"></label>*
<textarea asp-for="Template" rows="20" cols="40" class="form-control"></textarea>
<span asp-validation-for="Template" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" class="btn btn-primary" />
</div>
</form>
<a asp-action="ListApps">Back to the app list</a>
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,48 @@
@model ViewPointOfSaleViewModel
@{
ViewData["Title"] = Model.Title;
Layout = null;
}
<!DOCTYPE html>
<html class="h-100">
<head>
<title>@Model.Title</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="stylesheet" href="~/vendor/bootstrap4/css/bootstrap.css" />
</head>
<body class="h-100">
<div class="container d-flex h-100">
<div class="justify-content-center align-self-center text-center mx-auto">
<h1 class="mb-4">@Model.Title</h1>
<form method="post">
<div class="row">
@foreach (var item in Model.Items)
{
<div class="col-sm-4 mb-3">
<h3>@item.Title</h3>
<button type="submit" name="choiceKey" class="btn btn-primary" value="@item.Id">Buy for @item.Price.Formatted</button>
</div>
}
</div>
</form>
@*<div class="row mt-4">
<div class="col-md-4 offset-md-4 col-sm-6 offset-sm-3">
<h3>Something else</h3>
<form data-buy>
<div class="input-group">
<input class="form-control" type="number" step="0.00001" name="amount" placeholder="undefined (optional)"><div class="input-group-append">
<button class="btn btn-primary" type="submit">Pay</button>
</div>
</div>
</form>
</div>
</div>*@
</div>
</div>
<script src="~/vendor/jquery/jquery.js"></script>
<script src="~/vendor/bootstrap4/js/bootstrap.js"></script>
</body>
</html>

View File

@ -0,0 +1 @@
@using BTCPayServer.Models.AppViewModels

View File

@ -0,0 +1,3 @@
@{
ViewBag.MainTitle = "Manage app";
}

View File

@ -156,7 +156,6 @@
<div class="payment__details__instruction__open-wallet">
<a class="payment__details__instruction__open-wallet__btn action-button" v-bind:href="srvModel.invoiceBitcoinUrl">
<span>{{$t("Open in wallet")}}</span>
<span class="glyphicon glyphicon-new-window"></span>
</a>
</div>
</div>
@ -178,7 +177,7 @@
</div>
<div class="separatorGem"></div>
<div class="copySectionBox">
<label>{{$t("Peer Info")}}</label>
<label>{{$t("Node Info")}}</label>
<div class="inputWithIcon _copyInput">
<input type="text" class="checkoutTextbox" v-bind:value="srvModel.peerInfo" readonly="readonly" />
<img v-bind:src="srvModel.cryptoImage" />
@ -296,7 +295,7 @@
{{srvModel.orderId}}
</div>
</div>
<a href="/invoices" class="action-button" style="margin-top: 20px;">
<a class="action-button" style="margin-top: 20px;">
<bp-done-text>
<span>{{$t("Return to StoreName", srvModel)}}</span>
</bp-done-text>

View File

@ -55,7 +55,7 @@
<span asp-validation-for="StoreId" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-default" />
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
<a asp-action="ListInvoices">Back to List</a>

View File

@ -35,7 +35,7 @@
<div class="row">
<div class="col-md-6">
<h3>Information</h3>
<table class="table">
<table class="table table-sm">
<tr>
<th>Store</th>
<td><a href="@Model.StoreLink">@Model.StoreName</a></td>
@ -93,7 +93,7 @@
<div class="col-md-6">
<h3>Buyer information</h3>
<table class="table">
<table class="table table-sm">
<tr>
<th>Name
<th>
@ -134,7 +134,7 @@
</table>
<h3>Product information</h3>
<table class="table">
<table class="table table-sm">
<tr>
<th>Item code</th>
<td>@Model.ProductInformation.ItemCode</td>
@ -153,7 +153,7 @@
<div class="row">
<div class="col-md-12">
<h3>Paid summary</h3>
<table class="table">
<table class="table table-sm">
<thead class="thead-inverse">
<tr>
<th style="white-space:nowrap;">Payment method</th>
@ -181,7 +181,7 @@
<div class="row">
<div class="col-md-12">
<h3>Payments</h3>
<table class="table">
<table class="table table-sm">
<thead class="thead-inverse">
<tr>
<th style="white-space:nowrap;">Payment method</th>
@ -208,7 +208,7 @@
<div class="row">
<div class="col-md-12">
<h3>Addresses</h3>
<table class="table">
<table class="table table-sm">
<thead class="thead-inverse">
<tr>
<th style="white-space:nowrap;">Payment method</th>
@ -232,7 +232,7 @@
<div class="row">
<div class="col-md-12">
<h3>Events</h3>
<table class="table">
<table class="table table-sm">
<thead class="thead-inverse">
<tr>
<th>Date</th>

View File

@ -18,8 +18,10 @@
<hr class="primary">
<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>
<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>
@ -27,21 +29,25 @@
</div>
<div class="form-group">
<form asp-action="SearchInvoice" method="post">
<input asp-for="SearchTerm" class="form-control" />
<div class="input-group">
<input asp-for="SearchTerm" class="form-control" />
<span class="input-group-btn">
<button type="submit" class="btn btn-primary" title="Search invoice">
<span class="fa fa-search"></span> Search
</button>
</span>
</div>
<input type="hidden" asp-for="Count" />
<span asp-validation-for="SearchTerm" class="text-danger"></span>
<button type="submit" class="btn btn-default" title="Search invoice">
<span class="glyphicon glyphicon-search"></span> Search
</button>
</form>
</div>
</div>
</div>
<div class="row">
<a asp-action="CreateInvoice" class="btn btn-success" role="button"><span class="glyphicon glyphicon-plus"></span> Create a new invoice</a>
<table class="table">
<thead class="thead-inverse">
<a asp-action="CreateInvoice" class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Create a new invoice</a>
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Date</th>
<th>OrderId</th>
@ -82,13 +88,13 @@
}
else
{
<td >@invoice.Status</td>
<td>@invoice.Status</td>
}
<td style="text-align:right">@invoice.AmountCurrency</td>
<td style="text-align:right">
@if(invoice.Status == "new")
{
<a asp-action="Checkout" asp-route-invoiceId="@invoice.InvoiceId">Checkout</a> <span>-</span>
<a asp-action="Checkout" asp-route-invoiceId="@invoice.InvoiceId">Checkout</a> <span>-</span>
}<a asp-action="Invoice" asp-route-invoiceId="@invoice.InvoiceId">Details</a>
</td>
</tr>
@ -134,7 +140,7 @@
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-danger">Yes, make invoice Invalid</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" data-dismiss="modal">Cancel</button>
</div>
</div>

View File

@ -25,7 +25,7 @@
<input asp-for="ConfirmPassword" class="form-control" />
<span asp-validation-for="ConfirmPassword" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-default">Update password</button>
<button type="submit" class="btn btn-primary">Update password</button>
</form>
</div>
</div>

View File

@ -7,7 +7,7 @@
<div class="alert alert-warning" role="alert">
<p>
<span class="glyphicon glyphicon-warning-sign"></span>
<span class="fa fa-warning"></span>
<strong>This action only disables 2FA.</strong>
</p>
<p>

View File

@ -37,7 +37,7 @@
<input asp-for="Code" class="form-control" autocomplete="off" />
<span asp-validation-for="Code" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-default">Verify</button>
<button type="submit" class="btn btn-primary">Verify</button>
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
</form>
</div>

View File

@ -5,23 +5,23 @@
}
@Html.Partial("_StatusMessage", Model.StatusMessage)
@if(Model.CurrentLogins?.Count > 0)
@if (Model.CurrentLogins?.Count > 0)
{
<h4>Registered Logins</h4>
<table class="table">
<table class="table table-sm table-responsive-md">
<tbody>
@foreach(var login in Model.CurrentLogins)
@foreach (var login in Model.CurrentLogins)
{
<tr>
<td>@login.LoginProvider</td>
<td>
@if(Model.ShowRemoveButton)
@if (Model.ShowRemoveButton)
{
<form asp-action="RemoveLogin" method="post">
<div>
<input asp-for="@login.LoginProvider" name="LoginProvider" type="hidden" />
<input asp-for="@login.ProviderKey" name="ProviderKey" type="hidden" />
<button type="submit" class="btn btn-default" title="Remove this @login.LoginProvider login from your account">Remove</button>
<button type="submit" class="btn btn-primary" title="Remove this @login.LoginProvider login from your account">Remove</button>
</div>
</form>
}
@ -35,16 +35,16 @@
</tbody>
</table>
}
@if(Model.OtherLogins?.Count > 0)
@if (Model.OtherLogins?.Count > 0)
{
<h4>Add another service to log in.</h4>
<hr />
<form asp-action="LinkLogin" method="post" class="form-horizontal">
<div id="socialLoginList">
<p>
@foreach(var provider in Model.OtherLogins)
@foreach (var provider in Model.OtherLogins)
{
<button type="submit" class="btn btn-default" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
<button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
}
</p>
</div>

View File

@ -7,7 +7,7 @@
<h4>@ViewData["Title"]</h4>
<div class="alert alert-warning" role="alert">
<p>
<span class="glyphicon glyphicon-warning-sign"></span>
<span class="fa fa-warning"></span>
<strong>Put these codes in a safe place.</strong>
</p>
<p>

View File

@ -25,7 +25,7 @@
{
<div class="input-group">
<input asp-for="Email" class="form-control" />
<span class="input-group-addon" aria-hidden="true"><span class="glyphicon glyphicon-ok text-success"></span></span>
<span class="input-group-addon" aria-hidden="true"><span class="fa fa-check text-success"></span></span>
</div>
}
else
@ -35,7 +35,7 @@
}
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-default">Save</button>
<button type="submit" class="btn btn-primary">Save</button>
</form>
</div>
</div>

View File

@ -6,7 +6,7 @@
<h4>@ViewData["Title"]</h4>
<div class="alert alert-warning" role="alert">
<p>
<span class="glyphicon glyphicon-warning-sign"></span>
<span class="fa fa-warning"></span>
<strong>If you reset your authenticator key your authenticator app will not work until you reconfigure it.</strong>
</p>
<p>

View File

@ -24,7 +24,7 @@
<input asp-for="ConfirmPassword" class="form-control" />
<span asp-validation-for="ConfirmPassword" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-default">Set password</button>
<button type="submit" class="btn btn-primary">Set password</button>
</form>
</div>
</div>

View File

@ -29,19 +29,19 @@
</div>
}
<a asp-action="Disable2faWarning" class="btn btn-default">Disable 2FA</a>
<a asp-action="GenerateRecoveryCodes" class="btn btn-default">Reset recovery codes</a>
<a asp-action="Disable2faWarning" class="btn btn-primary">Disable 2FA</a>
<a asp-action="GenerateRecoveryCodes" class="btn btn-primary">Reset recovery codes</a>
}
<h5>Authenticator app</h5>
@if(!Model.HasAuthenticator)
{
<a asp-action="EnableAuthenticator" class="btn btn-default">Add authenticator app</a>
<a asp-action="EnableAuthenticator" class="btn btn-primary">Add authenticator app</a>
}
else
{
<a asp-action="EnableAuthenticator" class="btn btn-default">Configure authenticator app</a>
<a asp-action="ResetAuthenticatorWarning" class="btn btn-default">Reset authenticator key</a>
<a asp-action="EnableAuthenticator" class="btn btn-primary">Configure authenticator app</a>
<a asp-action="ResetAuthenticatorWarning" class="btn btn-primary">Reset authenticator key</a>
}
@section Scripts {

View File

@ -4,13 +4,13 @@
var hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any();
}
<ul class="nav nav-pills nav-stacked">
<li class="@ManageNavPages.IndexNavClass(ViewContext)"><a asp-action="Index">Profile</a></li>
<li class="@ManageNavPages.ChangePasswordNavClass(ViewContext)"><a asp-action="ChangePassword">Password</a></li>
<div class="nav flex-column nav-pills">
<a class="nav-link @ManageNavPages.IndexNavClass(ViewContext)" asp-action="Index">Profile</a>
<a class="nav-link @ManageNavPages.ChangePasswordNavClass(ViewContext)" asp-action="ChangePassword">Password</a>
@if(hasExternalLogins)
{
<li class="@ManageNavPages.ExternalLoginsNavClass(ViewContext)"><a asp-action="ExternalLogins">External logins</a></li>
<a class="nav-link @ManageNavPages.ExternalLoginsNavClass(ViewContext)" asp-action="ExternalLogins">External logins</a>
}
<li class="@ManageNavPages.TwoFactorAuthenticationNavClass(ViewContext)"><a asp-action="TwoFactorAuthentication">Two-factor authentication</a></li>
</ul>
<a class="nav-link @ManageNavPages.TwoFactorAuthenticationNavClass(ViewContext)" asp-action="TwoFactorAuthentication">Two-factor authentication</a>
</div>

View File

@ -56,8 +56,8 @@
<input asp-for="TestEmail" class="form-control" />
</div>
<button type="submit" class="btn btn-success" name="command" value="Save">Save</button>
<button type="submit" class="btn btn-default" name="command" value="Test">Test</button>
<button type="submit" class="btn btn-primary" name="command" value="Save">Save</button>
<button type="submit" class="btn btn-primary" name="command" value="Test">Test</button>
</form>
</div>
</div>

View File

@ -9,8 +9,8 @@
@Html.Partial("_StatusMessage", Model.StatusMessage)
<table class="table">
<thead class="thead-inverse">
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
@ -18,7 +18,7 @@
</tr>
</thead>
<tbody>
@foreach(var user in Model.Users)
@foreach (var user in Model.Users)
{
<tr>
<td>@user.Name</td>

View File

@ -23,7 +23,7 @@
<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>
<button type="submit" class="btn btn-primary" name="command" value="Save">Save</button>
</form>
</div>
</div>

View File

@ -16,7 +16,7 @@
<label asp-for="IsAdmin"></label>
<input asp-for="IsAdmin" type="checkbox" class="form-check" />
</div>
<button name="command" type="submit" class="btn btn-success" value="Save">Save</button>
<button name="command" type="submit" class="btn btn-primary" value="Save">Save</button>
</form>
</div>
</div>

View File

@ -1,9 +1,9 @@
@using BTCPayServer.Views.Server
<ul class="nav nav-pills nav-stacked">
<li class="@ServerNavPages.UsersNavClass(ViewContext)"><a asp-action="Users">Users</a></li>
<li class="@ServerNavPages.EmailsNavClass(ViewContext)"><a asp-action="Emails">Email server</a></li>
<li class="@ServerNavPages.PoliciesNavClass(ViewContext)"><a asp-action="Policies">Policies</a></li>
<li class="@ServerNavPages.HangfireNavClass(ViewContext)"><a href="~/hangfire" target="_blank">Hangfire</a></li>
</ul>
<div class="nav flex-column nav-pills">
<a class="nav-link @ServerNavPages.UsersNavClass(ViewContext)" asp-action="Users">Users</a>
<a class="nav-link @ServerNavPages.EmailsNavClass(ViewContext)" asp-action="Emails">Email server</a>
<a class="nav-link @ServerNavPages.PoliciesNavClass(ViewContext)" asp-action="Policies">Policies</a>
<a class="nav-link @ServerNavPages.HangfireNavClass(ViewContext)" href="~/hangfire" target="_blank">Hangfire</a>
</div>

View File

@ -15,7 +15,7 @@
<div class="row">
<div class="col-lg-12 text-center">
<form method="post">
<button type="submit" class="btn btn-info btn-danger" title="Continue">@Model.Action</button>
<button type="submit" class="btn btn-secondary btn-danger" title="Continue">@Model.Action</button>
</form>
</div>
</div>

View File

@ -48,24 +48,25 @@
</button>
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ml-auto">
@if (SignInManager.IsSignedIn(User))
{
@if (User.IsInRole(Roles.ServerAdmin))
{
<li class="nav-item"><a asp-area="" asp-controller="Server" asp-action="ListUsers" class="nav-link js-scroll-trigger">Server settings</a></li>
}
<li class="nav-item"><a asp-area="" asp-controller="UserStores" asp-action="ListStores" class="nav-link js-scroll-trigger">Stores</a></li>
<li class="nav-item"><a asp-area="" asp-controller="Invoice" asp-action="ListInvoices" class="nav-link js-scroll-trigger">Invoices</a></li>
<li class="nav-item">
<a asp-area="" asp-controller="Manage" asp-action="Index" title="Manage" class="nav-link js-scroll-trigger">My settings</a>
</li>
<li class="nav-item">
<a asp-area="" asp-controller="Account" asp- asp-action="Logout" title="Manage" class="nav-link js-scroll-trigger">Log out</a>
</li>}
else
{
<li class="nav-item"><a asp-area="" asp-controller="Account" asp-action="Register" class="nav-link js-scroll-trigger">Register</a></li>
<li class="nav-item"><a asp-area="" asp-controller="Account" asp-action="Login" class="nav-link js-scroll-trigger">Log in</a></li>}
@if(SignInManager.IsSignedIn(User))
{
@if(User.IsInRole(Roles.ServerAdmin))
{
<li class="nav-item"><a asp-area="" asp-controller="Server" asp-action="ListUsers" class="nav-link js-scroll-trigger">Server settings</a></li>
}
<li class="nav-item"><a asp-area="" asp-controller="UserStores" asp-action="ListStores" class="nav-link js-scroll-trigger">Stores</a></li>
<li class="nav-item"><a asp-area="" asp-controller="Apps" asp-action="ListApps" class="nav-link js-scroll-trigger">Apps</a></li>
<li class="nav-item"><a asp-area="" asp-controller="Invoice" asp-action="ListInvoices" class="nav-link js-scroll-trigger">Invoices</a></li>
<li class="nav-item">
<a asp-area="" asp-controller="Manage" asp-action="Index" title="Manage" class="nav-link js-scroll-trigger">My settings</a>
</li>
<li class="nav-item">
<a asp-area="" asp-controller="Account" asp- asp-action="Logout" title="Manage" class="nav-link js-scroll-trigger">Log out</a>
</li>}
else
{
<li class="nav-item"><a asp-area="" asp-controller="Account" asp-action="Register" class="nav-link js-scroll-trigger">Register</a></li>
<li class="nav-item"><a asp-area="" asp-controller="Account" asp-action="Login" class="nav-link js-scroll-trigger">Log in</a></li>}
</ul>
</div>

View File

@ -16,7 +16,7 @@
<div class="row">
<div class="col-md-8">
<form method="post">
@if(!Model.Confirmation)
@if (!Model.Confirmation)
{
<div class="form-group">
<h5>Derivation Scheme</h5>
@ -33,17 +33,17 @@
<div id="ledger-info" class="form-text text-muted" style="display: none;">
<span>A ledger wallet is detected, which account do you want to use?</span>
<ul>
@for(int i = 0; i < 4; i++)
@for (int i = 0; i < 4; i++)
{
<li><a class="ledger-info-recommended" data-ledgeraccount="@i" href="#">Account @i (49'/<span class="ledger-info-cointype">0</span>'/@i')</a></li>
<li><a class="ledger-info-recommended" data-ledgeraccount="@i" href="#">Account @i (<span>@Model.RootKeyPath.Derive(i, true)</span>)</a></li>
}
</ul>
</div>
</div>
<div class="form-group">
<span>BTCPay format memo</span>
<table class="table">
<thead class="thead-inverse">
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Address type</th>
<th>Example</th>
@ -77,7 +77,7 @@
</tbody>
</table>
</div>
<button name="command" type="submit" class="btn btn-info">Continue</button>
<button name="command" type="submit" class="btn btn-primary">Continue</button>
}
else
{
@ -88,15 +88,15 @@
<input type="hidden" asp-for="Confirmation" />
<input type="hidden" asp-for="DerivationScheme" />
<div class="form-group">
<table class="table">
<thead class="thead-inverse">
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Key path</th>
<th>Address</th>
</tr>
</thead>
<tbody>
@foreach(var sample in Model.AddressSamples)
@foreach (var sample in Model.AddressSamples)
{
<tr>
<td>@sample.KeyPath</td>
@ -116,7 +116,7 @@
<input asp-for="HintAddress" class="form-control" />
<span asp-validation-for="HintAddress" class="text-danger"></span>
</div>
<button name="command" type="submit" class="btn btn-success">Confirm</button>
<button name="command" type="submit" class="btn btn-primary">Confirm</button>
}
</form>
</div>

View File

@ -46,8 +46,8 @@
</p>
}
</div>
<button name="command" type="submit" value="save" class="btn btn-success">Submit</button>
<button name="command" type="submit" value="test" class="btn btn-info">Test connection</button>
<button name="command" type="submit" value="save" class="btn btn-primary">Submit</button>
<button name="command" type="submit" value="test" class="btn btn-secondary">Test connection</button>
</form>
</div>
</div>

View File

@ -44,7 +44,13 @@
<span asp-validation-for="LightningMaxValue" class="text-danger"></span>
<p class="form-text text-muted">Example: 5.50 USD</p>
</div>
<button name="command" type="submit" class="btn btn-success" value="Save">Save</button>
<div class="form-group">
<label asp-for="OnChainMinValue"></label>
<input asp-for="OnChainMinValue" class="form-control" />
<span asp-validation-for="OnChainMinValue" class="text-danger"></span>
<p class="form-text text-muted">Example: 5.50 USD</p>
</div>
<button name="command" type="submit" class="btn btn-primary" value="Save">Save</button>
</form>
</div>
</div>

View File

@ -49,7 +49,7 @@
{
<input type="hidden" asp-for="StoreId" />}
<div class="form-group">
<input type="submit" value="Request pairing" class="btn btn-default" />
<input type="submit" value="Request pairing" class="btn btn-primary" />
</div>
</form>
</div>

View File

@ -8,9 +8,9 @@
<h4>@ViewData["Title"]</h4>
<p>You can allow a public key to access the API of this store</p>
@Html.Partial("_StatusMessage", Model.StatusMessage)
<a asp-action="CreateToken" class="btn btn-success" role="button"><span class="glyphicon glyphicon-plus"></span>Create a new token</a>
<table class="table">
<thead class="thead-inverse">
<a asp-action="CreateToken" class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Create a new token</a>
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Label</th>
<th>SIN</th>
@ -19,7 +19,7 @@
</tr>
</thead>
<tbody>
@foreach(var token in Model.Tokens)
@foreach (var token in Model.Tokens)
{
<tr>
<td>@token.Label</td>

View File

@ -15,7 +15,7 @@
<div class="row">
<div class="col-md-4"></div>
<div class="col-md-4">
<table class="table">
<table class="table table-sm table-responsive-md">
<tr>
<th>Label</th>
<td style="text-align:right;">@Model.Label</td>
@ -42,7 +42,7 @@
<span asp-validation-for="SelectedStore" class="text-danger"></span>
</div>
<input type="hidden" name="pairingCode" value="@Model.Id" />
<button type="submit" class="btn btn-info" title="Approve this pairing demand">Approve</button>
<button type="submit" class="btn btn-secondary" title="Approve this pairing demand">Approve</button>
</form>
</div>
<div class="col-md-4"></div>

View File

@ -17,8 +17,10 @@
<div class="col-md-8">
<div class="form-group">
<h5>Users</h5>
<span>Add access to your store to other users (Guest will not be able to see and modify the store settings)<br />
Note that the user must have a registered account on this BTCPay Server.</span>
<span>
Add access to your store to other users (Guest will not be able to see and modify the store settings)<br />
Note that the user must have a registered account on this BTCPay Server.
</span>
</div>
<div class="form-inline">
<form method="post">
@ -27,12 +29,12 @@
<option value="@StoreRoles.Owner">Owner</option>
<option value="@StoreRoles.Guest">Guest</option>
</select>
<button type="submit" role="button" class="form-control btn btn-success"><span class="glyphicon glyphicon-plus"></span>Add user</button>
<button type="submit" role="button" class="form-control btn btn-primary"><span class="fa fa-plus"></span> Add user</button>
</form>
</div>
<div class="form-group">
<table class="table">
<thead class="thead-inverse">
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Email</th>
<th>Role</th>
@ -40,7 +42,7 @@
</tr>
</thead>
<tbody>
@foreach(var user in Model.Users)
@foreach (var user in Model.Users)
{
<tr>
<td>@user.Email</td>

View File

@ -72,8 +72,8 @@
</div>
<div class="form-group">
<table class="table">
<thead class="thead-inverse">
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Crypto</th>
<th>Derivation Scheme</th>
@ -81,13 +81,13 @@
</tr>
</thead>
<tbody>
@foreach(var scheme in Model.DerivationSchemes)
@foreach (var scheme in Model.DerivationSchemes)
{
<tr>
<td>@scheme.Crypto</td>
<td style="max-width:300px;overflow:hidden;">@scheme.Value</td>
<td style="text-align:right">
@if(!string.IsNullOrWhiteSpace(scheme.Value))
@if (!string.IsNullOrWhiteSpace(scheme.Value))
{
<a asp-action="Wallet" asp-route-cryptoCode="@scheme.Crypto">Wallet</a><span> - </span>
}
@ -108,8 +108,8 @@
</p>
</div>
<div class="form-group">
<table class="table">
<thead class="thead-inverse">
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Crypto</th>
<th>Address</th>
@ -117,7 +117,7 @@
</tr>
</thead>
<tbody>
@foreach(var scheme in Model.LightningNodes)
@foreach (var scheme in Model.LightningNodes)
{
<tr>
<td>@scheme.CryptoCode</td>
@ -129,7 +129,15 @@
</table>
</div>
</div>
<button name="command" type="submit" class="btn btn-success" value="Save">Save</button>
<div class="form-group">
<label asp-for="LightningDescriptionTemplate"></label>
<input asp-for="LightningDescriptionTemplate" class="form-control" />
<span asp-validation-for="LightningDescriptionTemplate" class="text-danger"></span>
<p class="form-text text-muted">
Available placeholders are: {StoreName}, {ItemDescription} and {OrderId}
</p>
</div>
<button name="command" type="submit" class="btn btn-primary" value="Save">Save</button>
</form>
</div>
</div>

View File

@ -20,13 +20,13 @@
<li>Make sure you are running the Ledger Bitcoin or Litecoin app with version superior or equal to 1.2.4</li>
<li>Use a browser supporting the <a href="https://www.yubico.com/support/knowledge-base/categories/articles/browsers-support-u2f/">U2F protocol</a></li>
</ul>
<p id="hw-loading"><span class="glyphicon glyphicon-question-sign" style="color:orange"></span> <span>Detecting hardware wallet...</span></p>
<p id="hw-error" style="display:none;"><span class="glyphicon glyphicon-remove-sign" style="color:red;"></span> <span class="hw-label">An error happened</span></p>
<p id="hw-success" style="display:none;"><span class="glyphicon glyphicon-ok-sign" style="color:green;"></span> <span class="hw-label">Detecting hardware wallet...</span></p>
<p id="hw-loading"><span class="fa fa-question-circle" style="color:orange"></span> <span>Detecting hardware wallet...</span></p>
<p id="hw-error" style="display:none;"><span class="fa fa-times-circle" style="color:red;"></span> <span class="hw-label">An error happened</span></p>
<p id="hw-success" style="display:none;"><span class="fa fa-check-circle" style="color:green;"></span> <span class="hw-label">Detecting hardware wallet...</span></p>
<p id="check-loading" style="display:none;"><span class="glyphicon glyphicon-question-sign" style="color:orange"></span> <span class="check-label">Detecting hardware wallet...</span></p>
<p id="check-error" style="display:none;"><span class="glyphicon glyphicon-remove-sign" style="color:red;"></span> <span class="check-label">An error happened</span></p>
<p id="check-success" style="display:none;"><span class="glyphicon glyphicon-ok-sign" style="color:green;"></span> <span class="check-label">Detecting hardware wallet...</span></p>
<p id="check-loading" style="display:none;"><span class="fa fa-question-circle" style="color:orange"></span> <span class="check-label">Detecting hardware wallet...</span></p>
<p id="check-error" style="display:none;"><span class="fa fa-times-circle" style="color:red;"></span> <span class="check-label">An error happened</span></p>
<p id="check-success" style="display:none;"><span class="fa fa-check-circle" style="color:green;"></span> <span class="check-label">Detecting hardware wallet...</span></p>
</div>
</div>
<div class="row">
@ -58,7 +58,7 @@
<label>Subtract fees from amount</label>
<input id="substract-checkbox" name="SubstractFees" class="form-check" type="checkbox" />
</div>
<button id="confirm-button" name="command" type="submit" class="btn btn-success">Confirm</button>
<button id="confirm-button" name="command" type="submit" class="btn btn-primary">Confirm</button>
</form>
</div>
</div>

View File

@ -1,10 +1,10 @@
@using BTCPayServer.Views.Stores
@inject SignInManager<ApplicationUser> SignInManager
<ul class="nav nav-pills nav-stacked">
<li class="@StoreNavPages.IndexNavClass(ViewContext)"><a asp-action="UpdateStore">General settings</a></li>
<li class="@StoreNavPages.CheckoutNavClass(ViewContext)"><a asp-action="CheckoutExperience">Checkout experience</a></li>
<li class="@StoreNavPages.TokensNavClass(ViewContext)"><a asp-action="ListTokens">Access Tokens</a></li>
<li class="@StoreNavPages.UsersNavClass(ViewContext)"><a asp-action="StoreUsers">Users</a></li>
</ul>
<div class="nav flex-column nav-pills">
<a class="nav-link @StoreNavPages.IndexNavClass(ViewContext)" asp-action="UpdateStore">General settings</a>
<a class="nav-link @StoreNavPages.CheckoutNavClass(ViewContext)" asp-action="CheckoutExperience">Checkout experience</a>
<a class="nav-link @StoreNavPages.TokensNavClass(ViewContext)" asp-action="ListTokens">Access Tokens</a>
<a class="nav-link @StoreNavPages.UsersNavClass(ViewContext)" asp-action="StoreUsers">Users</a>
</div>

View File

@ -20,7 +20,7 @@
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-default" />
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
<a asp-action="ListStores">Back to List</a>

View File

@ -21,9 +21,9 @@
</div>
<div class="row">
<a asp-action="CreateStore" class="btn btn-success" role="button"><span class="glyphicon glyphicon-plus"></span> Create a new store</a>
<table class="table">
<thead class="thead-inverse">
<a asp-action="CreateStore" class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Create a new store</a>
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Name</th>
<th>Website</th>
@ -32,28 +32,28 @@
</tr>
</thead>
<tbody>
@foreach(var store in Model.Stores)
@foreach (var store in Model.Stores)
{
<tr>
<td>@store.Name</td>
<td>
@if(!string.IsNullOrEmpty(store.WebSite))
@if (!string.IsNullOrEmpty(store.WebSite))
{
<a href="@store.WebSite">@store.WebSite</a>
}
</td>
<td>
@for(int i = 0; i < store.Balances.Length; i++)
@for (int i = 0; i < store.Balances.Length; i++)
{
<span>@store.Balances[i]</span>
if(i != store.Balances.Length - 1)
if (i != store.Balances.Length - 1)
{
<br />
}
}
</td>
<td style="text-align:right">
@if(store.IsOwner)
@if (store.IsOwner)
{
<a asp-action="UpdateStore" asp-controller="Stores" asp-route-storeId="@store.Id">Settings</a><span> - </span>
}

View File

@ -2,10 +2,10 @@
{
"outputFileName": "wwwroot/bundles/main-bundle.min.css",
"inputFiles": [
"wwwroot/vendor/bootstrap/css/bootstrap.css",
"wwwroot/vendor/bootstrap4/css/bootstrap.css",
"wwwroot/vendor/magnific-popup/magnific-popup.css",
"wwwroot/css/creative.css",
"wwwroot/css/site.css",
"wwwroot/vendor/font-awesome/css/font-awesome.css",
"wwwroot/main/**/*.css",
"wwwroot/vendor/animatecss/animate.css"
]
},
@ -14,11 +14,11 @@
"inputFiles": [
"wwwroot/vendor/jquery/jquery.js",
"wwwroot/vendor/popper/popper.js",
"wwwroot/vendor/bootstrap/js/bootstrap.js",
"wwwroot/vendor/bootstrap4/js/bootstrap.js",
"wwwroot/vendor/jquery-easing/jquery.easing.js",
"wwwroot/vendor/scrollreveal/scrollreveal.min.js",
"wwwroot/vendor/magnific-popup/jquery.magnific-popup.js",
"wwwroot/js/creative.js"
"wwwroot/main/**/js/*.js"
]
},
{
@ -32,8 +32,7 @@
"outputFileName": "wwwroot/bundles/checkout-bundle.min.css",
"inputFiles": [
"wwwroot/vendor/font-awesome/css/font-awesome.css",
"wwwroot/css/css.css",
"wwwroot/css/normalizer.css",
"wwwroot/checkout/**/*.css",
"wwwroot/vendor/jquery-prettydropdowns/prettydropdowns.css"
]
},
@ -47,7 +46,7 @@
"wwwroot/vendor/i18next/i18next.js",
"wwwroot/vendor/i18next/vue-i18next.js",
"wwwroot/vendor/jquery-prettydropdowns/jquery.prettydropdowns.js",
"wwwroot/js/checkout/**/*.js"
"wwwroot/checkout/**/*.js"
]
}
]

Some files were not shown because too many files have changed in this diff Show More