Compare commits
45 Commits
v1.0.3.101
...
v1.0.3.104
Author | SHA1 | Date | |
---|---|---|---|
2926865c1b | |||
20fb7fc188 | |||
461462eafc | |||
7aa6d1a8d7 | |||
d914fe2f48 | |||
e100edce24 | |||
a68915d6cf | |||
210d680b21 | |||
8dc4acdc34 | |||
eb54a18fcd | |||
cf436e11ae | |||
e22b7f74c7 | |||
f8fca7434c | |||
66d303c4ba | |||
186ed8beb2 | |||
fac546cc0b | |||
9d2d2d0d64 | |||
f58043b07f | |||
289c6fa10e | |||
00e24ab249 | |||
7b69e334d7 | |||
12507b6743 | |||
3750842833 | |||
1dcec3e1fb | |||
d8e1edd6d3 | |||
a1f1e90626 | |||
522d745883 | |||
8ffb81cdf3 | |||
ae5254c65e | |||
9d53888524 | |||
cd6dd78759 | |||
27fd49e61c | |||
a3a259556f | |||
803da75636 | |||
d1556eb6cd | |||
a7edbfe5e9 | |||
663b5beac1 | |||
7e164d2ec3 | |||
f9fb0bb477 | |||
702c7f2c30 | |||
8b348ade75 | |||
bf37f44795 | |||
698033b0cf | |||
10496363f5 | |||
14647d5778 |
@ -14,11 +14,12 @@ jobs:
|
||||
- run:
|
||||
command: |
|
||||
cd BTCPayServer.Tests
|
||||
docker-compose -v
|
||||
docker-compose down --v
|
||||
docker-compose build
|
||||
TESTS_RUN_EXTERNAL_INTEGRATION="true"
|
||||
if [ -n "$CIRCLE_PULL_REQUEST" ] || [ -n "$CIRCLE_PR_NUMBER" ]; then
|
||||
TESTS_RUN_EXTERNAL_INTEGRATION="false"
|
||||
TESTS_RUN_EXTERNAL_INTEGRATION="false"
|
||||
if [ "$CIRCLE_PROJECT_USERNAME" == "btcpayserver" ] && [ "$CIRCLE_PROJECT_REPONAME" == "btcpayserver" ]; then
|
||||
TESTS_RUN_EXTERNAL_INTEGRATION="true"
|
||||
fi
|
||||
docker-compose run -e TESTS_RUN_EXTERNAL_INTEGRATION=$TESTS_RUN_EXTERNAL_INTEGRATION tests
|
||||
|
||||
|
@ -11,6 +11,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.0.1" />
|
||||
<PackageReference Include="Selenium.WebDriver" Version="3.141.0" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="74.0.3729.6" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
@ -36,6 +36,8 @@ using System.Threading;
|
||||
using Xunit;
|
||||
using BTCPayServer.Services;
|
||||
using System.Net.Http;
|
||||
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -102,12 +104,16 @@ namespace BTCPayServer.Tests
|
||||
|
||||
StringBuilder config = new StringBuilder();
|
||||
config.AppendLine($"{chain.ToLowerInvariant()}=1");
|
||||
if (InContainer)
|
||||
{
|
||||
config.AppendLine($"bind=0.0.0.0");
|
||||
}
|
||||
config.AppendLine($"port={Port}");
|
||||
config.AppendLine($"chains=btc,ltc");
|
||||
|
||||
config.AppendLine($"btc.explorer.url={NBXplorerUri.AbsoluteUri}");
|
||||
config.AppendLine($"btc.explorer.cookiefile=0");
|
||||
|
||||
config.AppendLine("allow-admin-registration=1");
|
||||
config.AppendLine($"ltc.explorer.url={LTCNBXplorerUri.AbsoluteUri}");
|
||||
config.AppendLine($"ltc.explorer.cookiefile=0");
|
||||
config.AppendLine($"btc.lightning={IntegratedLightning.AbsoluteUri}");
|
||||
@ -142,14 +148,17 @@ namespace BTCPayServer.Tests
|
||||
.UseStartup<Startup>()
|
||||
.Build();
|
||||
_Host.Start();
|
||||
|
||||
var urls = _Host.ServerFeatures.Get<IServerAddressesFeature>().Addresses;
|
||||
foreach (var url in urls)
|
||||
{
|
||||
Logs.Tester.LogInformation("Listening on " + url);
|
||||
}
|
||||
Logs.Tester.LogInformation("Server URI " + ServerUri);
|
||||
|
||||
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
|
||||
StoreRepository = (StoreRepository)_Host.Services.GetService(typeof(StoreRepository));
|
||||
Networks = (BTCPayNetworkProvider)_Host.Services.GetService(typeof(BTCPayNetworkProvider));
|
||||
var dashBoard = (NBXplorerDashboard)_Host.Services.GetService(typeof(NBXplorerDashboard));
|
||||
while(!dashBoard.IsFullySynched())
|
||||
{
|
||||
Thread.Sleep(10);
|
||||
}
|
||||
|
||||
if (MockRates)
|
||||
{
|
||||
@ -210,6 +219,35 @@ namespace BTCPayServer.Tests
|
||||
});
|
||||
rateProvider.Providers.Add("bittrex", bittrex);
|
||||
}
|
||||
|
||||
|
||||
|
||||
WaitSiteIsOperational().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private async Task WaitSiteIsOperational()
|
||||
{
|
||||
var synching = WaitIsFullySynched();
|
||||
var accessingHomepage = WaitCanAccessHomepage();
|
||||
await Task.WhenAll(synching, accessingHomepage).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task WaitCanAccessHomepage()
|
||||
{
|
||||
var resp = await HttpClient.GetAsync("/").ConfigureAwait(false);
|
||||
while (resp.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
await Task.Delay(10).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task WaitIsFullySynched()
|
||||
{
|
||||
var dashBoard = GetService<NBXplorerDashboard>();
|
||||
while (!dashBoard.IsFullySynched())
|
||||
{
|
||||
await Task.Delay(10).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private string FindBTCPayServerDirectory()
|
||||
|
@ -27,7 +27,7 @@ namespace BTCPayServer.Tests
|
||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Fact(Timeout = 60000)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async void CanSetChangellyPaymentMethod()
|
||||
{
|
||||
|
@ -8,6 +8,17 @@ WORKDIR /source
|
||||
COPY BTCPayServer/BTCPayServer.csproj BTCPayServer/BTCPayServer.csproj
|
||||
COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj
|
||||
RUN dotnet restore BTCPayServer.Tests/BTCPayServer.Tests.csproj
|
||||
|
||||
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT false
|
||||
|
||||
ENV LC_ALL en_US.UTF-8
|
||||
ENV LANG en_US.UTF-8
|
||||
|
||||
RUN apk add --no-cache chromium chromium-chromedriver icu-libs
|
||||
|
||||
ENV SCREEN_HEIGHT 600 \
|
||||
SCREEN_WIDTH 1200
|
||||
|
||||
COPY . .
|
||||
RUN dotnet build
|
||||
WORKDIR /source/BTCPayServer.Tests
|
||||
|
@ -2,13 +2,61 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using OpenQA.Selenium;
|
||||
using Xunit;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static void ScrollTo(this IWebDriver driver, By by)
|
||||
{
|
||||
var element = driver.FindElement(by);
|
||||
((IJavaScriptExecutor)driver).ExecuteScript($"window.scrollBy({element.Location.X},{element.Location.Y});");
|
||||
}
|
||||
/// <summary>
|
||||
/// Sometimes the chrome driver is fucked up and we need some magic to click on the element.
|
||||
/// </summary>
|
||||
/// <param name="element"></param>
|
||||
public static void ForceClick(this IWebElement element)
|
||||
{
|
||||
element.SendKeys(Keys.Return);
|
||||
}
|
||||
public static void AssertNoError(this IWebDriver driver)
|
||||
{
|
||||
try
|
||||
{
|
||||
Assert.NotEmpty(driver.FindElements(By.ClassName("navbar-brand")));
|
||||
}
|
||||
catch
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.AppendLine();
|
||||
foreach (var logKind in new []{ LogType.Browser, LogType.Client, LogType.Driver, LogType.Server })
|
||||
{
|
||||
try
|
||||
{
|
||||
var logs = driver.Manage().Logs.GetLog(logKind);
|
||||
builder.AppendLine($"Selenium [{logKind}]:");
|
||||
foreach (var entry in logs)
|
||||
{
|
||||
builder.AppendLine($"[{entry.Level}]: {entry.Message}");
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
builder.AppendLine($"---------");
|
||||
}
|
||||
Logs.Tester.LogInformation(builder.ToString());
|
||||
builder = new StringBuilder();
|
||||
builder.AppendLine($"Selenium [Sources]:");
|
||||
builder.AppendLine(driver.PageSource);
|
||||
builder.AppendLine($"---------");
|
||||
Logs.Tester.LogInformation(builder.ToString());
|
||||
throw;
|
||||
}
|
||||
}
|
||||
public static T AssertViewModel<T>(this IActionResult result)
|
||||
{
|
||||
Assert.NotNull(result);
|
||||
|
@ -50,9 +50,10 @@ namespace BTCPayServer.Tests
|
||||
|
||||
var walletController = tester.PayTester.GetController<WalletsController>(user.UserId);
|
||||
var walletId = new WalletId(user.StoreId, "BTC");
|
||||
var sendDestination = new Key().PubKey.Hash.GetAddress(user.SupportedNetwork.NBitcoinNetwork).ToString();
|
||||
var sendModel = new WalletSendModel()
|
||||
{
|
||||
Destination = new Key().PubKey.Hash.GetAddress(user.SupportedNetwork.NBitcoinNetwork).ToString(),
|
||||
Destination = sendDestination,
|
||||
Amount = 0.1m,
|
||||
FeeSatoshiPerByte = 1,
|
||||
CurrentBalance = 1.5m
|
||||
@ -63,11 +64,12 @@ namespace BTCPayServer.Tests
|
||||
Assert.NotNull(vmLedger.SuccessPath);
|
||||
Assert.NotNull(vmLedger.WebsocketPath);
|
||||
|
||||
var vmPSBT = await walletController.WalletSend(walletId, sendModel, command: "analyze-psbt").AssertViewModelAsync<WalletPSBTViewModel>();
|
||||
var redirectedPSBT = (string)Assert.IsType<RedirectToActionResult>(await walletController.WalletSend(walletId, sendModel, command: "analyze-psbt")).RouteValues["psbt"];
|
||||
var vmPSBT = walletController.WalletPSBT(walletId, new WalletPSBTViewModel() { PSBT = redirectedPSBT }).AssertViewModel<WalletPSBTViewModel>();
|
||||
var unsignedPSBT = PSBT.Parse(vmPSBT.PSBT, user.SupportedNetwork.NBitcoinNetwork);
|
||||
Assert.NotNull(vmPSBT.Decoded);
|
||||
|
||||
var filePSBT = (FileContentResult)(await walletController.WalletSend(walletId, sendModel, command: "save-psbt"));
|
||||
var filePSBT = (FileContentResult)(await walletController.WalletPSBT(walletId, vmPSBT, "save-psbt"));
|
||||
PSBT.Load(filePSBT.FileContents, user.SupportedNetwork.NBitcoinNetwork);
|
||||
|
||||
await walletController.WalletPSBT(walletId, vmPSBT, "ledger").AssertViewModelAsync<WalletSendLedgerModel>();
|
||||
@ -79,7 +81,11 @@ namespace BTCPayServer.Tests
|
||||
var signedPSBT = unsignedPSBT.Clone();
|
||||
signedPSBT.SignAll(user.ExtKey);
|
||||
vmPSBT.PSBT = signedPSBT.ToBase64();
|
||||
var redirect = Assert.IsType<RedirectToActionResult>(await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"));
|
||||
var psbtReady = await walletController.WalletPSBT(walletId, vmPSBT, "broadcast").AssertViewModelAsync<WalletPSBTReadyViewModel>();
|
||||
Assert.Equal(2, psbtReady.Destinations.Count);
|
||||
Assert.Contains(psbtReady.Destinations, d => d.Destination == sendDestination && !d.Positive);
|
||||
Assert.Contains(psbtReady.Destinations, d => d.Positive);
|
||||
var redirect = Assert.IsType<RedirectToActionResult>(await walletController.WalletPSBTReady(walletId, psbtReady, command: "broadcast"));
|
||||
Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName);
|
||||
|
||||
vmPSBT.PSBT = unsignedPSBT.ToBase64();
|
||||
@ -102,10 +108,10 @@ namespace BTCPayServer.Tests
|
||||
Assert.True(signedPSBT2.TryFinalize(out _));
|
||||
Assert.Equal(signedPSBT, signedPSBT2);
|
||||
|
||||
var ready = walletController.WalletPSBTReady(walletId, signedPSBT.ToBase64()).AssertViewModel<WalletPSBTReadyViewModel>();
|
||||
var ready = (await walletController.WalletPSBTReady(walletId, signedPSBT.ToBase64())).AssertViewModel<WalletPSBTReadyViewModel>();
|
||||
Assert.Equal(signedPSBT.ToBase64(), ready.PSBT);
|
||||
vmPSBT = await walletController.WalletPSBTReady(walletId, ready, command: "analyze-psbt").AssertViewModelAsync<WalletPSBTViewModel>();
|
||||
Assert.Equal(signedPSBT.ToBase64(), vmPSBT.PSBT);
|
||||
redirect = Assert.IsType<RedirectToActionResult>(await walletController.WalletPSBTReady(walletId, ready, command: "analyze-psbt"));
|
||||
Assert.Equal(signedPSBT.ToBase64(), (string)redirect.RouteValues["psbt"]);
|
||||
redirect = Assert.IsType<RedirectToActionResult>(await walletController.WalletPSBTReady(walletId, ready, command: "broadcast"));
|
||||
Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName);
|
||||
}
|
||||
|
143
BTCPayServer.Tests/SeleniumTester.cs
Normal file
143
BTCPayServer.Tests/SeleniumTester.cs
Normal file
@ -0,0 +1,143 @@
|
||||
using System;
|
||||
using BTCPayServer;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using NBitcoin;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Chrome;
|
||||
using Xunit;
|
||||
using System.IO;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using System.Threading;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class SeleniumTester : IDisposable
|
||||
{
|
||||
public IWebDriver Driver { get; set; }
|
||||
public ServerTester Server { get; set; }
|
||||
|
||||
public static SeleniumTester Create([CallerMemberNameAttribute] string scope = null)
|
||||
{
|
||||
var server = ServerTester.Create(scope);
|
||||
return new SeleniumTester()
|
||||
{
|
||||
Server = server
|
||||
};
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
Server.Start();
|
||||
ChromeOptions options = new ChromeOptions();
|
||||
options.AddArguments("headless"); // Comment to view browser
|
||||
options.AddArguments("window-size=1200x600"); // Comment to view browser
|
||||
options.AddArgument("shm-size=2g");
|
||||
if (Server.PayTester.InContainer)
|
||||
{
|
||||
options.AddArgument("no-sandbox");
|
||||
}
|
||||
Driver = new ChromeDriver(Server.PayTester.InContainer ? "/usr/bin" : Directory.GetCurrentDirectory(), options);
|
||||
Logs.Tester.LogInformation("Selenium: Using chrome driver");
|
||||
Logs.Tester.LogInformation("Selenium: Browsing to " + Server.PayTester.ServerUri);
|
||||
Logs.Tester.LogInformation($"Selenium: Resolution {Driver.Manage().Window.Size}");
|
||||
Driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(10);
|
||||
Driver.Navigate().GoToUrl(Server.PayTester.ServerUri);
|
||||
Driver.AssertNoError();
|
||||
}
|
||||
|
||||
public string Link(string relativeLink)
|
||||
{
|
||||
return Server.PayTester.ServerUri.AbsoluteUri.WithoutEndingSlash() + relativeLink.WithStartingSlash();
|
||||
}
|
||||
|
||||
public string RegisterNewUser(bool isAdmin = false)
|
||||
{
|
||||
var usr = RandomUtils.GetUInt256().ToString() + "@a.com";
|
||||
Driver.FindElement(By.Id("Register")).Click();
|
||||
Driver.FindElement(By.Id("Email")).SendKeys(usr);
|
||||
Driver.FindElement(By.Id("Password")).SendKeys("123456");
|
||||
Driver.FindElement(By.Id("ConfirmPassword")).SendKeys("123456");
|
||||
if (isAdmin)
|
||||
Driver.FindElement(By.Id("IsAdmin")).Click();
|
||||
Driver.FindElement(By.Id("RegisterButton")).Click();
|
||||
Driver.AssertNoError();
|
||||
return usr;
|
||||
}
|
||||
|
||||
public string CreateNewStore()
|
||||
{
|
||||
var usr = "Store" + RandomUtils.GetUInt64().ToString();
|
||||
Driver.FindElement(By.Id("Stores")).Click();
|
||||
Driver.FindElement(By.Id("CreateStore")).Click();
|
||||
Driver.FindElement(By.Id("Name")).SendKeys(usr);
|
||||
Driver.FindElement(By.Id("Create")).Click();
|
||||
return usr;
|
||||
}
|
||||
|
||||
public void AddDerivationScheme(string derivationScheme = "xpub661MyMwAqRbcGABgHMUXDzPzH1tU7eZaAaJQXhDXsSxsqyQzQeU6kznNfSuAyqAK9UaWSaZaMFdNiY5BCF4zBPAzSnwfUAwUhwttuAKwfRX-[legacy]")
|
||||
{
|
||||
Driver.FindElement(By.Id("ModifyBTC")).ForceClick();
|
||||
Driver.FindElement(By.Id("DerivationScheme")).SendKeys(derivationScheme);
|
||||
Driver.FindElement(By.Id("Continue")).ForceClick();
|
||||
Driver.FindElement(By.Id("Confirm")).ForceClick();
|
||||
Driver.FindElement(By.Id("Save")).ForceClick();
|
||||
return;
|
||||
}
|
||||
|
||||
public void ClickOnAllSideMenus()
|
||||
{
|
||||
var links = Driver.FindElements(By.CssSelector(".nav-pills .nav-link")).Select(c => c.GetAttribute("href")).ToList();
|
||||
Driver.AssertNoError();
|
||||
Assert.NotEmpty(links);
|
||||
foreach (var l in links)
|
||||
{
|
||||
Driver.Navigate().GoToUrl(l);
|
||||
Driver.AssertNoError();
|
||||
}
|
||||
}
|
||||
|
||||
public void CreateInvoice(string random)
|
||||
{
|
||||
Driver.FindElement(By.Id("Invoices")).Click();
|
||||
Driver.FindElement(By.Id("CreateNewInvoice")).Click();
|
||||
Driver.FindElement(By.CssSelector("input#Amount.form-control")).SendKeys("100");
|
||||
Driver.FindElement(By.Name("StoreId")).SendKeys("Deriv" + random + Keys.Enter);
|
||||
Driver.FindElement(By.Id("Create")).Click();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Driver != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
Driver.Close();
|
||||
}
|
||||
catch { }
|
||||
Driver.Dispose();
|
||||
}
|
||||
if (Server != null)
|
||||
Server.Dispose();
|
||||
}
|
||||
|
||||
internal void AssertNotFound()
|
||||
{
|
||||
Assert.Contains("Status Code: 404; Not Found", Driver.PageSource);
|
||||
}
|
||||
|
||||
internal void GoToHome()
|
||||
{
|
||||
Driver.Navigate().GoToUrl(Server.PayTester.ServerUri);
|
||||
}
|
||||
|
||||
internal void Logout()
|
||||
{
|
||||
Driver.FindElement(By.Id("Logout")).Click();
|
||||
}
|
||||
}
|
||||
}
|
311
BTCPayServer.Tests/SeleniumTests.cs
Normal file
311
BTCPayServer.Tests/SeleniumTests.cs
Normal file
@ -0,0 +1,311 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Chrome;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Xunit.Abstractions;
|
||||
using OpenQA.Selenium.Interactions;
|
||||
using System.Linq;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
[Trait("Selenium", "Selenium")]
|
||||
public class ChromeTests
|
||||
{
|
||||
public ChromeTests(ITestOutputHelper helper)
|
||||
{
|
||||
Logs.Tester = new XUnitLog(helper) { Name = "Tests" };
|
||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanNavigateServerSettings()
|
||||
{
|
||||
using (var s = SeleniumTester.Create())
|
||||
{
|
||||
s.Start();
|
||||
s.RegisterNewUser(true);
|
||||
s.Driver.FindElement(By.Id("ServerSettings")).Click();
|
||||
s.Driver.AssertNoError();
|
||||
s.ClickOnAllSideMenus();
|
||||
s.Driver.Quit();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NewUserLogin()
|
||||
{
|
||||
using (var s = SeleniumTester.Create())
|
||||
{
|
||||
s.Start();
|
||||
//Register & Log Out
|
||||
var email = s.RegisterNewUser();
|
||||
s.Driver.FindElement(By.Id("Logout")).Click();
|
||||
s.Driver.AssertNoError();
|
||||
s.Driver.FindElement(By.Id("Login")).Click();
|
||||
s.Driver.AssertNoError();
|
||||
|
||||
s.Driver.Navigate().GoToUrl(s.Link("/invoices"));
|
||||
Assert.Contains("ReturnUrl=%2Finvoices", s.Driver.Url);
|
||||
|
||||
// We should be redirected to login
|
||||
//Same User Can Log Back In
|
||||
s.Driver.FindElement(By.Id("Email")).SendKeys(email);
|
||||
s.Driver.FindElement(By.Id("Password")).SendKeys("123456");
|
||||
s.Driver.FindElement(By.Id("LoginButton")).Click();
|
||||
|
||||
// We should be redirected to invoice
|
||||
Assert.EndsWith("/invoices", s.Driver.Url);
|
||||
|
||||
// Should not be able to reach server settings
|
||||
s.Driver.Navigate().GoToUrl(s.Link("/server/users"));
|
||||
Assert.Contains("ReturnUrl=%2Fserver%2Fusers", s.Driver.Url);
|
||||
|
||||
//Change Password & Log Out
|
||||
s.Driver.FindElement(By.Id("MySettings")).Click();
|
||||
s.Driver.FindElement(By.Id("ChangePassword")).Click();
|
||||
s.Driver.FindElement(By.Id("OldPassword")).SendKeys("123456");
|
||||
s.Driver.FindElement(By.Id("NewPassword")).SendKeys("abc???");
|
||||
s.Driver.FindElement(By.Id("ConfirmPassword")).SendKeys("abc???");
|
||||
s.Driver.FindElement(By.Id("UpdatePassword")).Click();
|
||||
s.Driver.FindElement(By.Id("Logout")).Click();
|
||||
s.Driver.AssertNoError();
|
||||
|
||||
//Log In With New Password
|
||||
s.Driver.FindElement(By.Id("Login")).Click();
|
||||
s.Driver.FindElement(By.Id("Email")).SendKeys(email);
|
||||
s.Driver.FindElement(By.Id("Password")).SendKeys("abc???");
|
||||
s.Driver.FindElement(By.Id("LoginButton")).Click();
|
||||
Assert.True(s.Driver.PageSource.Contains("Stores"), "Can't Access Stores");
|
||||
|
||||
s.Driver.FindElement(By.Id("MySettings")).Click();
|
||||
s.ClickOnAllSideMenus();
|
||||
|
||||
s.Driver.Quit();
|
||||
}
|
||||
}
|
||||
|
||||
private static void LogIn(SeleniumTester s, string email)
|
||||
{
|
||||
s.Driver.FindElement(By.Id("Login")).Click();
|
||||
s.Driver.FindElement(By.Id("Email")).SendKeys(email);
|
||||
s.Driver.FindElement(By.Id("Password")).SendKeys("123456");
|
||||
s.Driver.FindElement(By.Id("LoginButton")).Click();
|
||||
s.Driver.AssertNoError();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanCreateStores()
|
||||
{
|
||||
using (var s = SeleniumTester.Create())
|
||||
{
|
||||
s.Start();
|
||||
var alice = s.RegisterNewUser();
|
||||
var store = s.CreateNewStore();
|
||||
s.AddDerivationScheme();
|
||||
s.Driver.AssertNoError();
|
||||
Assert.Contains(store, s.Driver.PageSource);
|
||||
var storeUrl = s.Driver.Url;
|
||||
s.ClickOnAllSideMenus();
|
||||
|
||||
CreateInvoice(s, store);
|
||||
s.Driver.FindElement(By.ClassName("invoice-details-link")).Click();
|
||||
var invoiceUrl = s.Driver.Url;
|
||||
|
||||
// When logout we should not be able to access store and invoice details
|
||||
s.Driver.FindElement(By.Id("Logout")).Click();
|
||||
s.Driver.Navigate().GoToUrl(storeUrl);
|
||||
Assert.Contains("ReturnUrl", s.Driver.Url);
|
||||
s.Driver.Navigate().GoToUrl(invoiceUrl);
|
||||
Assert.Contains("ReturnUrl", s.Driver.Url);
|
||||
|
||||
// When logged we should not be able to access store and invoice details
|
||||
var bob = s.RegisterNewUser();
|
||||
s.Driver.Navigate().GoToUrl(storeUrl);
|
||||
Assert.Contains("ReturnUrl", s.Driver.Url);
|
||||
s.Driver.Navigate().GoToUrl(invoiceUrl);
|
||||
s.AssertNotFound();
|
||||
s.GoToHome();
|
||||
s.Logout();
|
||||
|
||||
// Let's add Bob as a guest to alice's store
|
||||
LogIn(s, alice);
|
||||
s.Driver.Navigate().GoToUrl(storeUrl + "/users");
|
||||
s.Driver.FindElement(By.Id("Email")).SendKeys(bob + Keys.Enter);
|
||||
Assert.Contains("User added successfully", s.Driver.PageSource);
|
||||
s.Logout();
|
||||
|
||||
// Bob should not have access to store, but should have access to invoice
|
||||
LogIn(s, bob);
|
||||
s.Driver.Navigate().GoToUrl(storeUrl);
|
||||
Assert.Contains("ReturnUrl", s.Driver.Url);
|
||||
s.Driver.Navigate().GoToUrl(invoiceUrl);
|
||||
s.Driver.AssertNoError();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanCreateInvoice()
|
||||
{
|
||||
using (var s = SeleniumTester.Create())
|
||||
{
|
||||
s.Start();
|
||||
s.RegisterNewUser();
|
||||
var store = s.CreateNewStore();
|
||||
s.AddDerivationScheme();
|
||||
|
||||
CreateInvoice(s, store);
|
||||
|
||||
s.Driver.FindElement(By.ClassName("invoice-details-link")).Click();
|
||||
s.Driver.AssertNoError();
|
||||
s.Driver.Navigate().Back();
|
||||
s.Driver.FindElement(By.ClassName("invoice-checkout-link")).Click();
|
||||
Assert.NotEmpty(s.Driver.FindElements(By.Id("checkoutCtrl")));
|
||||
s.Driver.Quit();
|
||||
}
|
||||
}
|
||||
|
||||
private static void CreateInvoice(SeleniumTester s, string store)
|
||||
{
|
||||
s.Driver.FindElement(By.Id("Invoices")).Click();
|
||||
s.Driver.FindElement(By.Id("CreateNewInvoice")).Click();
|
||||
s.Driver.FindElement(By.CssSelector("input#Amount.form-control")).SendKeys("100");
|
||||
s.Driver.FindElement(By.Name("StoreId")).SendKeys(store + Keys.Enter);
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
Assert.True(s.Driver.PageSource.Contains("just created!"), "Unable to create Invoice");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanCreateAppPoS()
|
||||
{
|
||||
using (var s = SeleniumTester.Create())
|
||||
{
|
||||
s.Start();
|
||||
s.RegisterNewUser();
|
||||
var store = s.CreateNewStore();
|
||||
|
||||
s.Driver.FindElement(By.Id("Apps")).Click();
|
||||
s.Driver.FindElement(By.Id("CreateNewApp")).Click();
|
||||
s.Driver.FindElement(By.Name("Name")).SendKeys("PoS" + store);
|
||||
s.Driver.FindElement(By.CssSelector("select#SelectedAppType.form-control")).SendKeys("PointOfSale" + Keys.Enter);
|
||||
s.Driver.FindElement(By.CssSelector("select#SelectedStore.form-control")).SendKeys(store + Keys.Enter);
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
s.Driver.FindElement(By.CssSelector("input#EnableShoppingCart.form-check")).Click();
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).ForceClick();
|
||||
Assert.True(s.Driver.PageSource.Contains("App updated"), "Unable to create PoS");
|
||||
s.Driver.Quit();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanCreateAppCF()
|
||||
{
|
||||
using (var s = SeleniumTester.Create())
|
||||
{
|
||||
s.Start();
|
||||
s.RegisterNewUser();
|
||||
var store = s.CreateNewStore();
|
||||
s.AddDerivationScheme();
|
||||
|
||||
s.Driver.FindElement(By.Id("Apps")).Click();
|
||||
s.Driver.FindElement(By.Id("CreateNewApp")).Click();
|
||||
s.Driver.FindElement(By.Name("Name")).SendKeys("CF" + store);
|
||||
s.Driver.FindElement(By.CssSelector("select#SelectedAppType.form-control")).SendKeys("Crowdfund" + Keys.Enter);
|
||||
s.Driver.FindElement(By.CssSelector("select#SelectedStore.form-control")).SendKeys(store + Keys.Enter);
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
s.Driver.FindElement(By.Id("Title")).SendKeys("Kukkstarter");
|
||||
s.Driver.FindElement(By.CssSelector("div.note-editable.card-block")).SendKeys("1BTC = 1BTC");
|
||||
s.Driver.FindElement(By.Id("TargetCurrency")).SendKeys("JPY");
|
||||
s.Driver.FindElement(By.Id("TargetAmount")).SendKeys("700");
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).Submit();
|
||||
s.Driver.FindElement(By.Id("ViewApp")).ForceClick();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
|
||||
Assert.True(s.Driver.PageSource.Contains("Currently Active!"), "Unable to create CF");
|
||||
s.Driver.Quit();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanCreatePayRequest()
|
||||
{
|
||||
using (var s = SeleniumTester.Create())
|
||||
{
|
||||
s.Start();
|
||||
s.RegisterNewUser();
|
||||
s.CreateNewStore();
|
||||
s.AddDerivationScheme();
|
||||
|
||||
s.Driver.FindElement(By.Id("PaymentRequests")).Click();
|
||||
s.Driver.FindElement(By.Id("CreatePaymentRequest")).Click();
|
||||
s.Driver.FindElement(By.Id("Title")).SendKeys("Pay123");
|
||||
s.Driver.FindElement(By.Id("Amount")).SendKeys("700");
|
||||
s.Driver.FindElement(By.Id("Currency")).SendKeys("BTC");
|
||||
s.Driver.FindElement(By.Id("SaveButton")).Submit();
|
||||
s.Driver.FindElement(By.Name("ViewAppButton")).SendKeys(Keys.Return);
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
|
||||
Assert.True(s.Driver.PageSource.Contains("Amount due"), "Unable to create Payment Request");
|
||||
s.Driver.Quit();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanManageWallet()
|
||||
{
|
||||
using (var s = SeleniumTester.Create())
|
||||
{
|
||||
s.Start();
|
||||
s.RegisterNewUser();
|
||||
s.CreateNewStore();
|
||||
|
||||
// In this test, we try to spend from a manual seed. We import the xpub 49'/0'/0', then try to use the seed
|
||||
// to sign the transaction
|
||||
var mnemonic = "usage fever hen zero slide mammal silent heavy donate budget pulse say brain thank sausage brand craft about save attract muffin advance illegal cabbage";
|
||||
var root = new Mnemonic(mnemonic).DeriveExtKey();
|
||||
s.AddDerivationScheme("ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD");
|
||||
var tx = s.Server.ExplorerNode.SendToAddress(BitcoinAddress.Create("bcrt1qmxg8fgnmkp354vhe78j6sr4ut64tyz2xyejel4", Network.RegTest), Money.Coins(3.0m));
|
||||
s.Server.ExplorerNode.Generate(1);
|
||||
|
||||
s.Driver.FindElement(By.Id("Wallets")).Click();
|
||||
s.Driver.FindElement(By.LinkText("Manage")).Click();
|
||||
|
||||
s.ClickOnAllSideMenus();
|
||||
|
||||
// We setup the fingerprint and the account key path
|
||||
s.Driver.FindElement(By.Id("WalletSettings")).ForceClick();
|
||||
s.Driver.FindElement(By.Id("AccountKeys_0__MasterFingerprint")).SendKeys("8bafd160");
|
||||
s.Driver.FindElement(By.Id("AccountKeys_0__AccountKeyPath")).SendKeys("m/49'/0'/0'" + Keys.Enter);
|
||||
|
||||
// Check the tx sent earlier arrived
|
||||
s.Driver.FindElement(By.Id("WalletTransactions")).ForceClick();
|
||||
var walletTransactionLink = s.Driver.Url;
|
||||
Assert.Contains(tx.ToString(), s.Driver.PageSource);
|
||||
|
||||
|
||||
void SignWith(string signingSource)
|
||||
{
|
||||
// Send to bob
|
||||
s.Driver.FindElement(By.Id("WalletSend")).Click();
|
||||
var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest);
|
||||
s.Driver.FindElement(By.Id("Destination")).SendKeys(bob.ToString());
|
||||
s.Driver.FindElement(By.Id("Amount")).SendKeys("1");
|
||||
s.Driver.ScrollTo(By.Id("SendMenu"));
|
||||
s.Driver.FindElement(By.Id("SendMenu")).ForceClick();
|
||||
s.Driver.FindElement(By.CssSelector("button[value=seed]")).Click();
|
||||
|
||||
// Input the seed
|
||||
s.Driver.FindElement(By.Id("SeedOrKey")).SendKeys(signingSource + Keys.Enter);
|
||||
|
||||
// Broadcast
|
||||
Assert.Contains(bob.ToString(), s.Driver.PageSource);
|
||||
Assert.Contains("1.00000000", s.Driver.PageSource);
|
||||
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
|
||||
Assert.Equal(walletTransactionLink, s.Driver.Url);
|
||||
}
|
||||
SignWith(mnemonic);
|
||||
var accountKey = root.Derive(new KeyPath("m/49'/0'/0'")).GetWif(Network.RegTest).ToString();
|
||||
SignWith(accountKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -316,6 +316,42 @@ namespace BTCPayServer.Tests
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task GetRedirectedToLoginPathOnChallenge()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var client = tester.PayTester.HttpClient;
|
||||
//Wallets endpoint is protected
|
||||
var response = await client.GetAsync("wallets");
|
||||
var urlPath = response.RequestMessage.RequestUri.ToString()
|
||||
.Replace(tester.PayTester.ServerUri.ToString(), "");
|
||||
//Cookie Challenge redirects you to login page
|
||||
Assert.StartsWith("Account/Login", urlPath, StringComparison.InvariantCultureIgnoreCase);
|
||||
|
||||
var queryString = response.RequestMessage.RequestUri.ParseQueryString();
|
||||
|
||||
Assert.NotNull(queryString["ReturnUrl"]);
|
||||
Assert.Equal("/wallets", queryString["ReturnUrl"]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanUseTestWebsiteUI()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var http = new HttpClient();
|
||||
var response = await http.GetAsync(tester.PayTester.ServerUri);
|
||||
Assert.True(response.IsSuccessStatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanAcceptInvoiceWithTolerance()
|
||||
@ -1644,12 +1680,13 @@ namespace BTCPayServer.Tests
|
||||
Assert.NotNull(psbt);
|
||||
|
||||
var root = new Mnemonic("usage fever hen zero slide mammal silent heavy donate budget pulse say brain thank sausage brand craft about save attract muffin advance illegal cabbage").DeriveExtKey().AsHDKeyCache();
|
||||
var account = root.Derive(new KeyPath("m/49'/0'/0'"));
|
||||
Assert.All(psbt.PSBT.Inputs, input =>
|
||||
{
|
||||
var keyPath = input.HDKeyPaths.Single();
|
||||
Assert.StartsWith(onchainBTC.AccountKeyPath.ToString(), keyPath.Value.Item2.ToString());
|
||||
Assert.Equal(root.Derive(keyPath.Value.Item2).GetPublicKey(), keyPath.Key);
|
||||
Assert.Equal(keyPath.Value.Item1, onchainBTC.RootFingerprint.Value);
|
||||
Assert.False(keyPath.Value.KeyPath.IsHardened);
|
||||
Assert.Equal(account.Derive(keyPath.Value.KeyPath).GetPublicKey(), keyPath.Key);
|
||||
Assert.Equal(keyPath.Value.MasterFingerprint, onchainBTC.AccountKeySettings[0].AccountKey.GetPublicKey().GetHDFingerPrint());
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -2669,9 +2706,10 @@ donation:
|
||||
var mainnet = new BTCPayNetworkProvider(NetworkType.Mainnet).GetNetwork("BTC");
|
||||
var root = new Mnemonic("usage fever hen zero slide mammal silent heavy donate budget pulse say brain thank sausage brand craft about save attract muffin advance illegal cabbage").DeriveExtKey();
|
||||
Assert.True(DerivationSchemeSettings.TryParseFromColdcard("{\"keystore\": {\"ckcc_xpub\": \"xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw\", \"xpub\": \"ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}", mainnet, out var settings));
|
||||
Assert.Equal(root.GetPublicKey().GetHDFingerPrint(), settings.RootFingerprint);
|
||||
Assert.Equal(root.GetPublicKey().GetHDFingerPrint(), settings.AccountKeySettings[0].RootFingerprint);
|
||||
Assert.Equal(settings.AccountKeySettings[0].RootFingerprint, HDFingerprint.TryParse("8bafd160", out var hd) ? hd : default);
|
||||
Assert.Equal("Coldcard Import 0x60d1af8b", settings.Label);
|
||||
Assert.Equal("49'/0'/0'", settings.AccountKeyPath.ToString());
|
||||
Assert.Equal("49'/0'/0'", settings.AccountKeySettings[0].AccountKeyPath.ToString());
|
||||
Assert.Equal("ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD", settings.AccountOriginal);
|
||||
Assert.Equal(root.Derive(new KeyPath("m/49'/0'/0'")).Neuter().PubKey.WitHash.ScriptPubKey.Hash.ScriptPubKey, settings.AccountDerivation.Derive(new KeyPath()).ScriptPubKey);
|
||||
|
||||
|
@ -71,7 +71,7 @@ services:
|
||||
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.0.0.40
|
||||
image: nicolasdorier/nbxplorer:2.0.0.41
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
|
@ -2,6 +2,7 @@
|
||||
set -e
|
||||
|
||||
dotnet test --filter Fast=Fast --no-build
|
||||
dotnet test --filter Selenium=Selenium --no-build -v n
|
||||
dotnet test --filter Integration=Integration --no-build -v n
|
||||
if [[ "$TESTS_RUN_EXTERNAL_INTEGRATION" == "true" ]]; then
|
||||
dotnet test --filter ExternalIntegration=ExternalIntegration --no-build -v n
|
||||
|
@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
<Version>1.0.3.101</Version>
|
||||
<Version>1.0.3.104</Version>
|
||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
@ -47,16 +47,18 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NBitcoin" Version="4.1.2.20" />
|
||||
<PackageReference Include="NBitcoin" Version="4.1.2.28" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.34" />
|
||||
<PackageReference Include="DBriize" Version="1.0.0.4" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="2.0.0.12" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="2.0.0.13" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
|
||||
<PackageReference Include="NicolasDorier.RateLimits" Version="1.0.0.3" />
|
||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.18" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.1.2" />
|
||||
<PackageReference Include="OpenIddict" Version="2.0.0" />
|
||||
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="2.0.0" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="2.1.2" />
|
||||
<PackageReference Include="Serilog" Version="2.7.1" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="2.1.1" />
|
||||
|
@ -6,13 +6,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using StandardConfiguration;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using NBXplorer;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using Renci.SshNet;
|
||||
using NBitcoin.DataEncoders;
|
||||
using BTCPayServer.SSH;
|
||||
using BTCPayServer.Lightning;
|
||||
using Serilog.Events;
|
||||
@ -49,7 +43,7 @@ namespace BTCPayServer.Configuration
|
||||
private set;
|
||||
}
|
||||
public EndPoint SocksEndpoint { get; set; }
|
||||
|
||||
|
||||
public List<NBXplorerConnectionSetting> NBXplorerConnectionSettings
|
||||
{
|
||||
get;
|
||||
@ -75,10 +69,12 @@ namespace BTCPayServer.Configuration
|
||||
public void LoadArgs(IConfiguration conf)
|
||||
{
|
||||
NetworkType = DefaultConfiguration.GetNetworkType(conf);
|
||||
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType);
|
||||
DataDir = conf.GetOrDefault<string>("datadir", defaultSettings.DefaultDataDirectory);
|
||||
DataDir = conf.GetDataDir(NetworkType);
|
||||
Logs.Configuration.LogInformation("Network: " + NetworkType.ToString());
|
||||
|
||||
if (conf.GetOrDefault<bool>("launchsettings", false) && NetworkType != NetworkType.Regtest)
|
||||
throw new ConfigException($"You need to run BTCPayServer with the run.sh or run.ps1 script");
|
||||
|
||||
var supportedChains = conf.GetOrDefault<string>("chains", "btc")
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(t => t.ToUpperInvariant());
|
||||
@ -145,6 +141,7 @@ namespace BTCPayServer.Configuration
|
||||
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
|
||||
MySQLConnectionString = conf.GetOrDefault<string>("mysql", null);
|
||||
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
|
||||
AllowAdminRegistration = conf.GetOrDefault<bool>("allow-admin-registration", false);
|
||||
TorrcFile = conf.GetOrDefault<string>("torrcfile", null);
|
||||
|
||||
var socksEndpointString = conf.GetOrDefault<string>("socksendpoint", null);
|
||||
@ -273,6 +270,7 @@ namespace BTCPayServer.Configuration
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public bool AllowAdminRegistration { get; set; }
|
||||
public List<SSHFingerprint> TrustedFingerprints { get; set; } = new List<SSHFingerprint>();
|
||||
public SSHSettings SSHSettings
|
||||
{
|
||||
|
@ -58,5 +58,17 @@ namespace BTCPayServer.Configuration
|
||||
throw new NotSupportedException("Configuration value does not support time " + typeof(T).Name);
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetDataDir(this IConfiguration configuration)
|
||||
{
|
||||
var networkType = DefaultConfiguration.GetNetworkType(configuration);
|
||||
return GetDataDir(configuration, networkType);
|
||||
}
|
||||
|
||||
public static string GetDataDir(this IConfiguration configuration, NetworkType networkType)
|
||||
{
|
||||
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(networkType);
|
||||
return configuration.GetOrDefault("datadir", defaultSettings.DefaultDataDirectory);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ namespace BTCPayServer.Configuration
|
||||
app.Option("-n | --network", $"Set the network among (mainnet,testnet,regtest) (default: mainnet)", CommandOptionType.SingleValue);
|
||||
app.Option("--testnet | -testnet", $"Use testnet (deprecated, use --network instead)", CommandOptionType.BoolValue);
|
||||
app.Option("--regtest | -regtest", $"Use regtest (deprecated, use --network instead)", CommandOptionType.BoolValue);
|
||||
app.Option("--allow-admin-registration", $"For debug only, will show a checkbox when a new user register to add himself as admin. (default: false)", CommandOptionType.BoolValue);
|
||||
app.Option("--chains | -c", $"Chains to support as a comma separated (default: btc; available: {chains})", CommandOptionType.SingleValue);
|
||||
app.Option("--postgres", $"Connection string to a PostgreSQL database (default: SQLite)", CommandOptionType.SingleValue);
|
||||
app.Option("--mysql", $"Connection string to a MySQL database (default: SQLite)", CommandOptionType.SingleValue);
|
||||
|
@ -368,6 +368,7 @@ namespace BTCPayServer.Controllers
|
||||
return RedirectToAction(nameof(HomeController.Index), "Home");
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
ViewData["Logon"] = logon.ToString(CultureInfo.InvariantCulture).ToLowerInvariant();
|
||||
ViewData["AllowIsAdmin"] = _Options.AllowAdminRegistration;
|
||||
return View();
|
||||
}
|
||||
|
||||
@ -378,6 +379,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
ViewData["Logon"] = logon.ToString(CultureInfo.InvariantCulture).ToLowerInvariant();
|
||||
ViewData["AllowIsAdmin"] = _Options.AllowAdminRegistration;
|
||||
var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
|
||||
if (policies.LockSubscription && !User.IsInRole(Roles.ServerAdmin))
|
||||
return RedirectToAction(nameof(HomeController.Index), "Home");
|
||||
@ -389,7 +391,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
var admin = await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin);
|
||||
Logs.PayServer.LogInformation($"A new user just registered {user.Email} {(admin.Count == 0 ? "(admin)" : "")}");
|
||||
if (admin.Count == 0)
|
||||
if (admin.Count == 0 || (model.IsAdmin && _Options.AllowAdminRegistration))
|
||||
{
|
||||
await _RoleManager.CreateAsync(new IdentityRole(Roles.ServerAdmin));
|
||||
await _userManager.AddToRoleAsync(user, Roles.ServerAdmin);
|
||||
|
@ -129,10 +129,10 @@ namespace BTCPayServer.Controllers
|
||||
Title = vm.Title,
|
||||
Enabled = vm.Enabled,
|
||||
EnforceTargetAmount = vm.EnforceTargetAmount,
|
||||
StartDate = vm.StartDate,
|
||||
StartDate = vm.StartDate?.ToUniversalTime(),
|
||||
TargetCurrency = vm.TargetCurrency,
|
||||
Description = _htmlSanitizer.Sanitize( vm.Description),
|
||||
EndDate = vm.EndDate,
|
||||
EndDate = vm.EndDate?.ToUniversalTime(),
|
||||
TargetAmount = vm.TargetAmount,
|
||||
CustomCSSLink = vm.CustomCSSLink,
|
||||
MainImageUrl = vm.MainImageUrl,
|
||||
|
@ -154,7 +154,7 @@ namespace BTCPayServer.Controllers
|
||||
blob.Email = viewModel.Email;
|
||||
blob.Description = _htmlSanitizer.Sanitize(viewModel.Description);
|
||||
blob.Amount = viewModel.Amount;
|
||||
blob.ExpiryDate = viewModel.ExpiryDate;
|
||||
blob.ExpiryDate = viewModel.ExpiryDate?.ToUniversalTime();
|
||||
blob.Currency = viewModel.Currency;
|
||||
blob.EmbeddedCSS = viewModel.EmbeddedCSS;
|
||||
blob.CustomCSSLink = viewModel.CustomCSSLink;
|
||||
|
@ -14,6 +14,7 @@ using BTCPayServer.Storage.Services.Providers.GoogleCloudStorage;
|
||||
using BTCPayServer.Storage.Services.Providers.GoogleCloudStorage.Configuration;
|
||||
using BTCPayServer.Storage.Services.Providers.Models;
|
||||
using BTCPayServer.Storage.ViewModels;
|
||||
using BTCPayServer.Views;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json.Linq;
|
||||
@ -96,7 +97,7 @@ namespace BTCPayServer.Controllers
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var expiry = DateTimeOffset.Now;
|
||||
var expiry = DateTimeOffset.UtcNow;
|
||||
switch (viewModel.TimeType)
|
||||
{
|
||||
case CreateTemporaryFileUrlViewModel.TmpFileTimeType.Seconds:
|
||||
@ -122,7 +123,7 @@ namespace BTCPayServer.Controllers
|
||||
StatusMessage = new StatusMessageModel()
|
||||
{
|
||||
Html =
|
||||
$"Generated Temporary Url for file {file.FileName} which expires at {expiry:G}. <a href='{url}' target='_blank'>{url}</a>"
|
||||
$"Generated Temporary Url for file {file.FileName} which expires at {expiry.ToBrowserDate()}. <a href='{url}' target='_blank'>{url}</a>"
|
||||
}.ToString(),
|
||||
fileId,
|
||||
});
|
||||
|
@ -202,10 +202,17 @@ namespace BTCPayServer.Controllers
|
||||
var newStrategy = ParseDerivationStrategy(vm.DerivationScheme, null, network);
|
||||
if (newStrategy.AccountDerivation != strategy?.AccountDerivation)
|
||||
{
|
||||
var accountKey = string.IsNullOrEmpty(vm.AccountKey) ? null : new BitcoinExtPubKey(vm.AccountKey, network.NBitcoinNetwork);
|
||||
if (accountKey != null)
|
||||
{
|
||||
var accountSettings = newStrategy.AccountKeySettings.FirstOrDefault(a => a.AccountKey == accountKey);
|
||||
if (accountSettings != null)
|
||||
{
|
||||
accountSettings.AccountKeyPath = vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath);
|
||||
accountSettings.RootFingerprint = string.IsNullOrEmpty(vm.RootFingerprint) ? (HDFingerprint?)null : new HDFingerprint(NBitcoin.DataEncoders.Encoders.Hex.DecodeData(vm.RootFingerprint));
|
||||
}
|
||||
}
|
||||
strategy = newStrategy;
|
||||
strategy.AccountKeyPath = vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath);
|
||||
strategy.RootFingerprint = string.IsNullOrEmpty(vm.RootFingerprint) ? (HDFingerprint?)null : new HDFingerprint(NBitcoin.DataEncoders.Encoders.Hex.DecodeData(vm.RootFingerprint));
|
||||
strategy.ExplicitAccountKey = string.IsNullOrEmpty(vm.AccountKey) ? null : new BitcoinExtPubKey(vm.AccountKey, network.NBitcoinNetwork);
|
||||
strategy.Source = vm.Source;
|
||||
vm.DerivationScheme = strategy.AccountDerivation.ToString();
|
||||
}
|
||||
|
@ -34,7 +34,6 @@ namespace BTCPayServer.Controllers
|
||||
psbtRequest.ExplicitChangeAddress = psbtDestination.Destination;
|
||||
}
|
||||
psbtDestination.SubstractFees = sendModel.SubstractFees;
|
||||
psbtRequest.RebaseKeyPaths = derivationSettings.GetPSBTRebaseKeyRules().ToList();
|
||||
var psbt = (await nbx.CreatePSBTAsync(derivationSettings.AccountDerivation, psbtRequest, cancellationToken));
|
||||
if (psbt == null)
|
||||
throw new NotSupportedException("You need to update your version of NBXplorer");
|
||||
@ -43,9 +42,15 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[Route("{walletId}/psbt")]
|
||||
public IActionResult WalletPSBT()
|
||||
public IActionResult WalletPSBT([ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, WalletPSBTViewModel vm)
|
||||
{
|
||||
return View(new WalletPSBTViewModel());
|
||||
var network = NetworkProvider.GetNetwork(walletId.CryptoCode);
|
||||
if (vm?.PSBT != null)
|
||||
{
|
||||
vm.Decoded = vm.GetPSBT(network.NBitcoinNetwork)?.ToString();
|
||||
}
|
||||
return View(vm ?? new WalletPSBTViewModel());
|
||||
}
|
||||
[HttpPost]
|
||||
[Route("{walletId}/psbt")]
|
||||
@ -62,55 +67,108 @@ namespace BTCPayServer.Controllers
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
if (command == null)
|
||||
switch (command)
|
||||
{
|
||||
vm.Decoded = psbt.ToString();
|
||||
return View(vm);
|
||||
}
|
||||
else if (command == "ledger")
|
||||
{
|
||||
return ViewWalletSendLedger(psbt);
|
||||
}
|
||||
else if (command == "broadcast")
|
||||
{
|
||||
if (!psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors))
|
||||
{
|
||||
case null:
|
||||
vm.Decoded = psbt.ToString();
|
||||
vm.FileName = string.Empty;
|
||||
return View(vm);
|
||||
case "ledger":
|
||||
return ViewWalletSendLedger(psbt);
|
||||
case "seed":
|
||||
return SignWithSeed(walletId, psbt.ToBase64());
|
||||
case "broadcast" when !psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors):
|
||||
return ViewPSBT(psbt, errors);
|
||||
}
|
||||
var transaction = psbt.ExtractTransaction();
|
||||
try
|
||||
case "broadcast":
|
||||
{
|
||||
var broadcastResult = await ExplorerClientProvider.GetExplorerClient(network).BroadcastAsync(transaction);
|
||||
if (!broadcastResult.Success)
|
||||
{
|
||||
return ViewPSBT(psbt, new[] { $"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}" });
|
||||
}
|
||||
return await WalletPSBTReady(walletId, psbt.ToBase64());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ViewPSBT(psbt, "Error while broadcasting: " + ex.Message);
|
||||
}
|
||||
return await RedirectToWalletTransaction(walletId, transaction);
|
||||
}
|
||||
else if (command == "combine")
|
||||
{
|
||||
ModelState.Remove(nameof(vm.PSBT));
|
||||
return View(nameof(WalletPSBTCombine), new WalletPSBTCombineViewModel() { OtherPSBT = psbt.ToBase64() });
|
||||
}
|
||||
else
|
||||
{
|
||||
(await GetDerivationSchemeSettings(walletId)).RebaseKeyPaths(psbt);
|
||||
return FilePSBT(psbt, "psbt-export.psbt");
|
||||
case "combine":
|
||||
ModelState.Remove(nameof(vm.PSBT));
|
||||
return View(nameof(WalletPSBTCombine), new WalletPSBTCombineViewModel() { OtherPSBT = psbt.ToBase64() });
|
||||
case "save-psbt":
|
||||
return FilePSBT(psbt, vm.FileName);
|
||||
default:
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{walletId}/psbt/ready")]
|
||||
public IActionResult WalletPSBTReady(
|
||||
public async Task<IActionResult> WalletPSBTReady(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, string psbt = null)
|
||||
WalletId walletId, string psbt = null,
|
||||
string signingKey = null,
|
||||
string signingKeyPath = null)
|
||||
{
|
||||
return View(new WalletPSBTReadyViewModel() { PSBT = psbt });
|
||||
var network = NetworkProvider.GetNetwork(walletId.CryptoCode);
|
||||
var vm = new WalletPSBTReadyViewModel() { PSBT = psbt };
|
||||
vm.SigningKey = signingKey;
|
||||
vm.SigningKeyPath = signingKeyPath;
|
||||
await FetchTransactionDetails(walletId, vm, network);
|
||||
return View(nameof(WalletPSBTReady), vm);
|
||||
}
|
||||
|
||||
private async Task FetchTransactionDetails(WalletId walletId, WalletPSBTReadyViewModel vm, BTCPayNetwork network)
|
||||
{
|
||||
var psbtObject = PSBT.Parse(vm.PSBT, network.NBitcoinNetwork);
|
||||
IHDKey signingKey = null;
|
||||
RootedKeyPath signingKeyPath = null;
|
||||
|
||||
try
|
||||
{
|
||||
signingKey = new BitcoinExtPubKey(vm.SigningKey, network.NBitcoinNetwork);
|
||||
}
|
||||
catch { }
|
||||
try
|
||||
{
|
||||
signingKey = signingKey ?? new BitcoinExtKey(vm.SigningKey, network.NBitcoinNetwork);
|
||||
}
|
||||
catch { }
|
||||
|
||||
try
|
||||
{
|
||||
signingKeyPath = RootedKeyPath.Parse(vm.SigningKeyPath);
|
||||
}
|
||||
catch { }
|
||||
|
||||
if (signingKey == null || signingKeyPath == null)
|
||||
{
|
||||
var signingKeySettings = (await GetDerivationSchemeSettings(walletId)).GetSigningAccountKeySettings();
|
||||
if (signingKey == null)
|
||||
{
|
||||
signingKey = signingKeySettings.AccountKey;
|
||||
vm.SigningKey = signingKey.ToString();
|
||||
}
|
||||
if (vm.SigningKeyPath == null)
|
||||
{
|
||||
signingKeyPath = signingKeySettings.GetRootedKeyPath();
|
||||
vm.SigningKeyPath = signingKeyPath?.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
var balanceChange = psbtObject.GetBalance(signingKey, signingKeyPath);
|
||||
|
||||
vm.BalanceChange = ValueToString(balanceChange, network);
|
||||
vm.Positive = balanceChange >= Money.Zero;
|
||||
|
||||
foreach (var output in psbtObject.Outputs)
|
||||
{
|
||||
var dest = new WalletPSBTReadyViewModel.DestinationViewModel();
|
||||
vm.Destinations.Add(dest);
|
||||
var mine = output.HDKeysFor(signingKey, signingKeyPath).Any();
|
||||
var balanceChange2 = output.Value;
|
||||
if (!mine)
|
||||
balanceChange2 = -balanceChange2;
|
||||
dest.Balance = ValueToString(balanceChange2, network);
|
||||
dest.Positive = balanceChange2 >= Money.Zero;
|
||||
dest.Destination = output.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork)?.ToString() ?? output.ScriptPubKey.ToString();
|
||||
}
|
||||
|
||||
if (psbtObject.TryGetFee(out var fee))
|
||||
{
|
||||
vm.Fee = ValueToString(fee, network);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@ -124,6 +182,7 @@ namespace BTCPayServer.Controllers
|
||||
try
|
||||
{
|
||||
psbt = PSBT.Parse(vm.PSBT, network.NBitcoinNetwork);
|
||||
await FetchTransactionDetails(walletId, vm, network);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@ -160,7 +219,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
else if (command == "analyze-psbt")
|
||||
{
|
||||
return ViewPSBT(psbt);
|
||||
return RedirectToAction(nameof(WalletPSBT), new { walletId = walletId, psbt = psbt.ToBase64() });
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -172,13 +231,18 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
private IActionResult ViewPSBT<T>(PSBT psbt, IEnumerable<T> errors = null)
|
||||
{
|
||||
return ViewPSBT(psbt, errors?.Select(e => e.ToString()).ToList());
|
||||
return ViewPSBT(psbt, null, errors?.Select(e => e.ToString()).ToList());
|
||||
}
|
||||
private IActionResult ViewPSBT(PSBT psbt, IEnumerable<string> errors = null)
|
||||
{
|
||||
return ViewPSBT(psbt, null, errors);
|
||||
}
|
||||
private IActionResult ViewPSBT(PSBT psbt, string fileName, IEnumerable<string> errors = null)
|
||||
{
|
||||
return View(nameof(WalletPSBT), new WalletPSBTViewModel()
|
||||
{
|
||||
Decoded = psbt.ToString(),
|
||||
FileName = fileName ?? string.Empty,
|
||||
PSBT = psbt.ToBase64(),
|
||||
Errors = errors?.ToList()
|
||||
});
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
@ -21,8 +22,10 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Internal;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Models;
|
||||
using Newtonsoft.Json;
|
||||
@ -227,31 +230,33 @@ namespace BTCPayServer.Controllers
|
||||
return View(vm);
|
||||
|
||||
DerivationSchemeSettings derivationScheme = await GetDerivationSchemeSettings(walletId);
|
||||
var psbt = await CreatePSBT(network, derivationScheme, vm, cancellation);
|
||||
|
||||
if (command == "ledger")
|
||||
CreatePSBTResponse psbt = null;
|
||||
try
|
||||
{
|
||||
return ViewWalletSendLedger(psbt.PSBT, psbt.ChangeAddress);
|
||||
psbt = await CreatePSBT(network, derivationScheme, vm, cancellation);
|
||||
}
|
||||
else
|
||||
catch (NBXplorerException ex)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (command == "analyze-psbt")
|
||||
return ViewPSBT(psbt.PSBT);
|
||||
derivationScheme.RebaseKeyPaths(psbt.PSBT);
|
||||
return FilePSBT(psbt.PSBT, $"Send-{vm.Amount.Value}-{network.CryptoCode}-to-{destination[0].ToString()}.psbt");
|
||||
}
|
||||
catch (NBXplorerException ex)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Amount), ex.Error.Message);
|
||||
ModelState.AddModelError(nameof(vm.Amount), ex.Error.Message);
|
||||
return View(vm);
|
||||
}
|
||||
catch (NotSupportedException)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Destination), "You need to update your version of NBXplorer");
|
||||
return View(vm);
|
||||
}
|
||||
derivationScheme.RebaseKeyPaths(psbt.PSBT);
|
||||
switch (command)
|
||||
{
|
||||
case "ledger":
|
||||
return ViewWalletSendLedger(psbt.PSBT, psbt.ChangeAddress);
|
||||
case "seed":
|
||||
return SignWithSeed(walletId, psbt.PSBT.ToBase64());
|
||||
case "analyze-psbt":
|
||||
return RedirectToAction(nameof(WalletPSBT), new { walletId = walletId, psbt = psbt.PSBT.ToBase64(), FileName= $"Send-{vm.Amount.Value}-{network.CryptoCode}-to-{destination[0].ToString()}.psbt" });
|
||||
default:
|
||||
return View(vm);
|
||||
}
|
||||
catch (NotSupportedException)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Destination), "You need to update your version of NBXplorer");
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -265,6 +270,82 @@ namespace BTCPayServer.Controllers
|
||||
SuccessPath = this.Url.Action(nameof(WalletPSBTReady))
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("{walletId}/psbt/seed")]
|
||||
public IActionResult SignWithSeed([ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId,string psbt)
|
||||
{
|
||||
return View(nameof(SignWithSeed), new SignWithSeedViewModel()
|
||||
{
|
||||
PSBT = psbt
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("{walletId}/psbt/seed")]
|
||||
public async Task<IActionResult> SignWithSeed([ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, SignWithSeedViewModel viewModel)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(viewModel);
|
||||
}
|
||||
var network = NetworkProvider.GetNetwork(walletId.CryptoCode);
|
||||
if (network == null)
|
||||
throw new FormatException("Invalid value for crypto code");
|
||||
|
||||
ExtKey extKey = viewModel.GetExtKey(network.NBitcoinNetwork);
|
||||
|
||||
if (extKey == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(viewModel.SeedOrKey),
|
||||
"Seed or Key was not in a valid format. It is either the 12/24 words or starts with xprv");
|
||||
}
|
||||
|
||||
var psbt = PSBT.Parse(viewModel.PSBT, network.NBitcoinNetwork);
|
||||
|
||||
if (!psbt.IsReadyToSign())
|
||||
{
|
||||
ModelState.AddModelError(nameof(viewModel.PSBT), "PSBT is not ready to be signed");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
ExtKey signingKey = null;
|
||||
var settings = (await GetDerivationSchemeSettings(walletId));
|
||||
var signingKeySettings = settings.GetSigningAccountKeySettings();
|
||||
if (signingKeySettings.RootFingerprint is null)
|
||||
signingKeySettings.RootFingerprint = extKey.GetPublicKey().GetHDFingerPrint();
|
||||
|
||||
RootedKeyPath rootedKeyPath = signingKeySettings.GetRootedKeyPath();
|
||||
// The user gave the root key, let's try to rebase the PSBT, and derive the account private key
|
||||
if (rootedKeyPath?.MasterFingerprint == extKey.GetPublicKey().GetHDFingerPrint())
|
||||
{
|
||||
psbt.RebaseKeyPaths(signingKeySettings.AccountKey, rootedKeyPath);
|
||||
signingKey = extKey.Derive(rootedKeyPath.KeyPath);
|
||||
}
|
||||
// The user maybe gave the account key, let's try to sign with it
|
||||
else
|
||||
{
|
||||
signingKey = extKey;
|
||||
}
|
||||
var balanceChange = psbt.GetBalance(signingKey, rootedKeyPath);
|
||||
if (balanceChange == Money.Zero)
|
||||
{
|
||||
ModelState.AddModelError(nameof(viewModel.SeedOrKey), "This seed does not seem to be able to sign this transaction. Either this is the wrong key, or Wallet Settings have not the correct account path in the wallet settings.");
|
||||
return View(viewModel);
|
||||
}
|
||||
psbt.SignAll(signingKey, rootedKeyPath);
|
||||
ModelState.Remove(nameof(viewModel.PSBT));
|
||||
return await WalletPSBTReady(walletId, psbt.ToBase64(), signingKey.GetWif(network.NBitcoinNetwork).ToString(), rootedKeyPath.ToString());
|
||||
}
|
||||
|
||||
private string ValueToString(Money v, BTCPayNetwork network)
|
||||
{
|
||||
return v.ToString() + " " + network.CryptoCode;
|
||||
}
|
||||
|
||||
private IDestination[] ParseDestination(string destination, Network network)
|
||||
{
|
||||
@ -452,14 +533,15 @@ namespace BTCPayServer.Controllers
|
||||
if (!_dashboard.IsFullySynched(network.CryptoCode, out var summary))
|
||||
throw new Exception($"{network.CryptoCode}: not started or fully synched");
|
||||
|
||||
var accountKey = derivationSettings.GetSigningAccountKeySettings();
|
||||
// Some deployment does not have the AccountKeyPath set, let's fix this...
|
||||
if (derivationSettings.AccountKeyPath == null)
|
||||
if (accountKey.AccountKeyPath == null)
|
||||
{
|
||||
// If the saved wallet key path is not present or incorrect, let's scan the wallet to see if it can sign strategy
|
||||
var foundKeyPath = await hw.FindKeyPathFromDerivation(network,
|
||||
derivationSettings.AccountDerivation,
|
||||
normalOperationTimeout.Token);
|
||||
derivationSettings.AccountKeyPath = foundKeyPath ?? throw new HardwareWalletException($"This store is not configured to use this ledger");
|
||||
accountKey.AccountKeyPath = foundKeyPath ?? throw new HardwareWalletException($"This store is not configured to use this ledger");
|
||||
storeData.SetSupportedPaymentMethod(derivationSettings);
|
||||
await Repository.UpdateStore(storeData);
|
||||
}
|
||||
@ -468,10 +550,10 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
// Checking if ledger is right with the RootFingerprint is faster as it does not need to make a query to the parent xpub,
|
||||
// but some deployment does not have it, so let's use AccountKeyPath instead
|
||||
if (derivationSettings.RootFingerprint == null)
|
||||
if (accountKey.RootFingerprint == null)
|
||||
{
|
||||
|
||||
var actualPubKey = await hw.GetExtPubKey(network, derivationSettings.AccountKeyPath, normalOperationTimeout.Token);
|
||||
var actualPubKey = await hw.GetExtPubKey(network, accountKey.AccountKeyPath, normalOperationTimeout.Token);
|
||||
if (!derivationSettings.AccountDerivation.GetExtPubKeys().Any(p => p.GetPublicKey() == actualPubKey.GetPublicKey()))
|
||||
throw new HardwareWalletException($"This store is not configured to use this ledger");
|
||||
}
|
||||
@ -479,15 +561,15 @@ namespace BTCPayServer.Controllers
|
||||
else
|
||||
{
|
||||
var actualPubKey = await hw.GetPubKey(network, new KeyPath(), normalOperationTimeout.Token);
|
||||
if (actualPubKey.GetHDFingerPrint() != derivationSettings.RootFingerprint.Value)
|
||||
if (actualPubKey.GetHDFingerPrint() != accountKey.RootFingerprint.Value)
|
||||
throw new HardwareWalletException($"This store is not configured to use this ledger");
|
||||
}
|
||||
}
|
||||
|
||||
// Some deployment does not have the RootFingerprint set, let's fix this...
|
||||
if (derivationSettings.RootFingerprint == null)
|
||||
if (accountKey.RootFingerprint == null)
|
||||
{
|
||||
derivationSettings.RootFingerprint = (await hw.GetPubKey(network, new KeyPath(), normalOperationTimeout.Token)).GetHDFingerPrint();
|
||||
accountKey.RootFingerprint = (await hw.GetPubKey(network, new KeyPath(), normalOperationTimeout.Token)).GetHDFingerPrint();
|
||||
storeData.SetSupportedPaymentMethod(derivationSettings);
|
||||
await Repository.UpdateStore(storeData);
|
||||
}
|
||||
@ -502,7 +584,7 @@ namespace BTCPayServer.Controllers
|
||||
derivationSettings.RebaseKeyPaths(psbtResponse.PSBT);
|
||||
|
||||
signTimeout.CancelAfter(TimeSpan.FromMinutes(5));
|
||||
psbtResponse.PSBT = await hw.SignTransactionAsync(psbtResponse.PSBT, psbtResponse.ChangeAddress?.ScriptPubKey, signTimeout.Token);
|
||||
psbtResponse.PSBT = await hw.SignTransactionAsync(psbtResponse.PSBT, accountKey.GetRootedKeyPath(), accountKey.AccountKey, psbtResponse.ChangeAddress?.ScriptPubKey, signTimeout.Token);
|
||||
result = new SendToAddressResult() { PSBT = psbtResponse.PSBT.ToBase64() };
|
||||
}
|
||||
}
|
||||
@ -528,6 +610,59 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
return new EmptyResult();
|
||||
}
|
||||
|
||||
[Route("{walletId}/settings")]
|
||||
public async Task<IActionResult> WalletSettings(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId)
|
||||
{
|
||||
var derivationSchemeSettings = await GetDerivationSchemeSettings(walletId);
|
||||
if (derivationSchemeSettings == null)
|
||||
return NotFound();
|
||||
var store = (await Repository.FindStore(walletId.StoreId, GetUserId()));
|
||||
var vm = new WalletSettingsViewModel()
|
||||
{
|
||||
Label = derivationSchemeSettings.Label,
|
||||
DerivationScheme = derivationSchemeSettings.AccountDerivation.ToString(),
|
||||
DerivationSchemeInput = derivationSchemeSettings.AccountOriginal,
|
||||
SelectedSigningKey = derivationSchemeSettings.SigningKey.ToString()
|
||||
};
|
||||
vm.AccountKeys = derivationSchemeSettings.AccountKeySettings
|
||||
.Select(e => new WalletSettingsAccountKeyViewModel()
|
||||
{
|
||||
AccountKey = e.AccountKey.ToString(),
|
||||
MasterFingerprint = e.RootFingerprint is HDFingerprint fp ? fp.ToString() : null,
|
||||
AccountKeyPath = e.AccountKeyPath == null ? "" : $"m/{e.AccountKeyPath}"
|
||||
}).ToList();
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[Route("{walletId}/settings")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> WalletSettings(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, WalletSettingsViewModel vm)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
return View(vm);
|
||||
var derivationScheme = await GetDerivationSchemeSettings(walletId);
|
||||
if (derivationScheme == null)
|
||||
return NotFound();
|
||||
derivationScheme.Label = vm.Label;
|
||||
derivationScheme.SigningKey = string.IsNullOrEmpty(vm.SelectedSigningKey) ? null : new BitcoinExtPubKey(vm.SelectedSigningKey, derivationScheme.Network.NBitcoinNetwork);
|
||||
for (int i = 0; i < derivationScheme.AccountKeySettings.Length; i++)
|
||||
{
|
||||
derivationScheme.AccountKeySettings[i].AccountKeyPath = string.IsNullOrWhiteSpace(vm.AccountKeys[i].AccountKeyPath) ? null
|
||||
: new KeyPath(vm.AccountKeys[i].AccountKeyPath);
|
||||
derivationScheme.AccountKeySettings[i].RootFingerprint = string.IsNullOrWhiteSpace(vm.AccountKeys[i].MasterFingerprint) ? (HDFingerprint?)null
|
||||
: new HDFingerprint(Encoders.Hex.DecodeData(vm.AccountKeys[i].MasterFingerprint));
|
||||
}
|
||||
var store = (await Repository.FindStore(walletId.StoreId, GetUserId()));
|
||||
store.SetSupportedPaymentMethod(derivationScheme);
|
||||
await Repository.UpdateStore(store);
|
||||
StatusMessage = "Wallet settings updated";
|
||||
return RedirectToAction(nameof(WalletSettings));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -65,6 +65,9 @@ namespace BTCPayServer
|
||||
{
|
||||
result.AccountOriginal = jobj["xpub"].Value<string>().Trim();
|
||||
result.AccountDerivation = derivationSchemeParser.ParseElectrum(result.AccountOriginal);
|
||||
result.AccountKeySettings = new AccountKeySettings[1];
|
||||
result.AccountKeySettings[0] = new AccountKeySettings();
|
||||
result.AccountKeySettings[0].AccountKey = result.AccountDerivation.GetExtPubKeys().Single().GetWif(network.NBitcoinNetwork);
|
||||
if (result.AccountDerivation is DirectDerivationStrategy direct && !direct.Segwit)
|
||||
result.AccountOriginal = null; // Saving this would be confusing for user, as xpub of electrum is legacy derivation, but for btcpay, it is segwit derivation
|
||||
}
|
||||
@ -91,7 +94,7 @@ namespace BTCPayServer
|
||||
{
|
||||
try
|
||||
{
|
||||
result.RootFingerprint = new HDFingerprint(jobj["ckcc_xfp"].Value<uint>());
|
||||
result.AccountKeySettings[0].RootFingerprint = new HDFingerprint(jobj["ckcc_xfp"].Value<uint>());
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
@ -100,7 +103,7 @@ namespace BTCPayServer
|
||||
{
|
||||
try
|
||||
{
|
||||
result.AccountKeyPath = new KeyPath(jobj["derivation"].Value<string>());
|
||||
result.AccountKeySettings[0].AccountKeyPath = new KeyPath(jobj["derivation"].Value<string>());
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
@ -113,6 +116,7 @@ namespace BTCPayServer
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public DerivationSchemeSettings(DerivationStrategyBase derivationStrategy, BTCPayNetwork network)
|
||||
{
|
||||
if (network == null)
|
||||
@ -121,20 +125,47 @@ namespace BTCPayServer
|
||||
throw new ArgumentNullException(nameof(derivationStrategy));
|
||||
AccountDerivation = derivationStrategy;
|
||||
Network = network;
|
||||
AccountKeySettings = derivationStrategy.GetExtPubKeys().Select(c => new AccountKeySettings()
|
||||
{
|
||||
AccountKey = c.GetWif(network.NBitcoinNetwork)
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
|
||||
BitcoinExtPubKey _SigningKey;
|
||||
public BitcoinExtPubKey SigningKey
|
||||
{
|
||||
get
|
||||
{
|
||||
return _SigningKey ?? AccountKeySettings?.Select(k => k.AccountKey).FirstOrDefault();
|
||||
}
|
||||
set
|
||||
{
|
||||
_SigningKey = value;
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public BTCPayNetwork Network { get; set; }
|
||||
public string Source { get; set; }
|
||||
|
||||
[Obsolete("Use GetAccountKeySettings().AccountKeyPath instead")]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public KeyPath AccountKeyPath { get; set; }
|
||||
|
||||
public DerivationStrategyBase AccountDerivation { get; set; }
|
||||
public string AccountOriginal { get; set; }
|
||||
|
||||
[Obsolete("Use GetAccountKeySettings().RootFingerprint instead")]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public HDFingerprint? RootFingerprint { get; set; }
|
||||
|
||||
[Obsolete("Use GetAccountKeySettings().AccountKey instead")]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public BitcoinExtPubKey ExplicitAccountKey { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
[Obsolete("Use GetAccountKeySettings().AccountKey instead")]
|
||||
public BitcoinExtPubKey AccountKey
|
||||
{
|
||||
get
|
||||
@ -143,16 +174,54 @@ namespace BTCPayServer
|
||||
}
|
||||
}
|
||||
|
||||
public AccountKeySettings GetSigningAccountKeySettings()
|
||||
{
|
||||
return AccountKeySettings.Single(a => a.AccountKey == SigningKey);
|
||||
}
|
||||
|
||||
AccountKeySettings[] _AccountKeySettings;
|
||||
public AccountKeySettings[] AccountKeySettings
|
||||
{
|
||||
get
|
||||
{
|
||||
// Legacy
|
||||
if (_AccountKeySettings == null)
|
||||
{
|
||||
if (this.Network == null)
|
||||
return null;
|
||||
_AccountKeySettings = AccountDerivation.GetExtPubKeys().Select(e => new AccountKeySettings()
|
||||
{
|
||||
AccountKey = e.GetWif(this.Network.NBitcoinNetwork),
|
||||
}).ToArray();
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
_AccountKeySettings[0].AccountKeyPath = AccountKeyPath;
|
||||
_AccountKeySettings[0].RootFingerprint = RootFingerprint;
|
||||
ExplicitAccountKey = null;
|
||||
AccountKeyPath = null;
|
||||
RootFingerprint = null;
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
return _AccountKeySettings;
|
||||
}
|
||||
set
|
||||
{
|
||||
_AccountKeySettings = value;
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<NBXplorer.Models.PSBTRebaseKeyRules> GetPSBTRebaseKeyRules()
|
||||
{
|
||||
if (AccountKey != null && AccountKeyPath != null && RootFingerprint is HDFingerprint fp)
|
||||
foreach (var accountKey in AccountKeySettings)
|
||||
{
|
||||
yield return new NBXplorer.Models.PSBTRebaseKeyRules()
|
||||
if (accountKey.AccountKeyPath != null && accountKey.RootFingerprint is HDFingerprint fp)
|
||||
{
|
||||
AccountKey = AccountKey,
|
||||
AccountKeyPath = AccountKeyPath,
|
||||
MasterFingerprint = fp
|
||||
};
|
||||
yield return new NBXplorer.Models.PSBTRebaseKeyRules()
|
||||
{
|
||||
AccountKey = accountKey.AccountKey,
|
||||
AccountKeyPath = accountKey.AccountKeyPath,
|
||||
MasterFingerprint = fp
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -181,8 +250,25 @@ namespace BTCPayServer
|
||||
{
|
||||
foreach (var rebase in GetPSBTRebaseKeyRules())
|
||||
{
|
||||
psbt.RebaseKeyPaths(rebase.AccountKey, rebase.AccountKeyPath, rebase.MasterFingerprint);
|
||||
psbt.RebaseKeyPaths(rebase.AccountKey, rebase.GetRootedKeyPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
public class AccountKeySettings
|
||||
{
|
||||
public HDFingerprint? RootFingerprint { get; set; }
|
||||
public KeyPath AccountKeyPath { get; set; }
|
||||
|
||||
public RootedKeyPath GetRootedKeyPath()
|
||||
{
|
||||
if (RootFingerprint is HDFingerprint fp && AccountKeyPath != null)
|
||||
return new RootedKeyPath(fp, AccountKeyPath);
|
||||
return null;
|
||||
}
|
||||
public BitcoinExtPubKey AccountKey { get; set; }
|
||||
public bool IsFullySetup()
|
||||
{
|
||||
return AccountKeyPath != null && RootFingerprint is HDFingerprint;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ namespace BTCPayServer.Hosting
|
||||
{
|
||||
public static class BTCPayServerServices
|
||||
{
|
||||
public static IServiceCollection AddBTCPayServer(this IServiceCollection services)
|
||||
public static IServiceCollection AddBTCPayServer(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddDbContext<ApplicationDbContext>((provider, o) =>
|
||||
{
|
||||
@ -67,7 +67,8 @@ namespace BTCPayServer.Hosting
|
||||
services.TryAddSingleton<SocketFactory>();
|
||||
services.TryAddSingleton<LightningClientFactoryService>();
|
||||
services.TryAddSingleton<InvoicePaymentNotification>();
|
||||
services.TryAddSingleton<BTCPayServerOptions>(o => o.GetRequiredService<IOptions<BTCPayServerOptions>>().Value);
|
||||
services.TryAddSingleton<BTCPayServerOptions>(o =>
|
||||
o.GetRequiredService<IOptions<BTCPayServerOptions>>().Value);
|
||||
services.TryAddSingleton<InvoiceRepository>(o =>
|
||||
{
|
||||
var opts = o.GetRequiredService<BTCPayServerOptions>();
|
||||
@ -219,7 +220,7 @@ namespace BTCPayServer.Hosting
|
||||
// bundling
|
||||
|
||||
services.AddAuthorization(o => Policies.AddBTCPayPolicies(o));
|
||||
BitpayAuthentication.AddAuthentication(services);
|
||||
services.AddBtcPayServerAuthenticationSchemes(configuration);
|
||||
|
||||
services.AddSingleton<IBundleProvider, ResourceBundleProvider>();
|
||||
services.AddTransient<BundleOptions>(provider =>
|
||||
@ -231,9 +232,9 @@ namespace BTCPayServer.Hosting
|
||||
return bundle;
|
||||
});
|
||||
|
||||
services.AddCors(options=>
|
||||
services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy(CorsPolicies.All, p=>p.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin());
|
||||
options.AddPolicy(CorsPolicies.All, p => p.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin());
|
||||
});
|
||||
|
||||
var rateLimits = new RateLimitService();
|
||||
@ -241,6 +242,13 @@ namespace BTCPayServer.Hosting
|
||||
services.AddSingleton(rateLimits);
|
||||
return services;
|
||||
}
|
||||
|
||||
private static void AddBtcPayServerAuthenticationSchemes(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddAuthentication()
|
||||
.AddCookie()
|
||||
.AddBitpayAuthentication();
|
||||
}
|
||||
|
||||
public static IApplicationBuilder UsePayServer(this IApplicationBuilder app)
|
||||
{
|
||||
|
@ -67,9 +67,9 @@ namespace BTCPayServer.Hosting
|
||||
.AddEntityFrameworkStores<ApplicationDbContext>()
|
||||
.AddDefaultTokenProviders();
|
||||
services.AddSignalR();
|
||||
services.AddBTCPayServer();
|
||||
services.AddProviderStorage();
|
||||
services.AddSession();
|
||||
services.AddBTCPayServer(Configuration);
|
||||
services.AddMvc(o =>
|
||||
{
|
||||
o.Filters.Add(new XFrameOptionsAttribute("DENY"));
|
||||
|
@ -23,5 +23,8 @@ namespace BTCPayServer.Models.AccountViewModels
|
||||
[Display(Name = "Confirm password")]
|
||||
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
|
||||
public string ConfirmPassword { get; set; }
|
||||
|
||||
[Display(Name = "Is administrator?")]
|
||||
public bool IsAdmin { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace BTCPayServer.Models
|
||||
{
|
||||
@ -7,5 +8,11 @@ namespace BTCPayServer.Models
|
||||
public string RequestId { get; set; }
|
||||
|
||||
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||
|
||||
[Display(Name = "Error")]
|
||||
public string Error { get; set; }
|
||||
|
||||
[Display(Name = "Description")]
|
||||
public string ErrorDescription { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Models.WalletViewModels
|
||||
{
|
||||
public class SignWithSeedViewModel
|
||||
{
|
||||
[Required]
|
||||
public string PSBT { get; set; }
|
||||
[Required][Display(Name = "Seed(12/24 word mnemonic seed) Or HD private key(xprv...)")]
|
||||
public string SeedOrKey { get; set; }
|
||||
|
||||
[Display(Name = "Optional seed passphrase")]
|
||||
public string Passphrase { get; set; }
|
||||
|
||||
public ExtKey GetExtKey(Network network)
|
||||
{
|
||||
ExtKey extKey = null;
|
||||
try
|
||||
{
|
||||
var mnemonic = new Mnemonic(SeedOrKey);
|
||||
extKey = mnemonic.DeriveExtKey(Passphrase);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
|
||||
if (extKey == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
extKey = ExtKey.Parse(SeedOrKey, network);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
return extKey;
|
||||
}
|
||||
}
|
||||
}
|
@ -8,6 +8,20 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
public class WalletPSBTReadyViewModel
|
||||
{
|
||||
public string PSBT { get; set; }
|
||||
public string SigningKey { get; set; }
|
||||
public string SigningKeyPath { get; set; }
|
||||
public List<string> Errors { get; set; }
|
||||
|
||||
public class DestinationViewModel
|
||||
{
|
||||
public bool Positive { get; set; }
|
||||
public string Destination { get; set; }
|
||||
public string Balance { get; set; }
|
||||
}
|
||||
|
||||
public string BalanceChange { get; set; }
|
||||
public bool Positive { get; set; }
|
||||
public List<DestinationViewModel> Destinations { get; set; } = new List<DestinationViewModel>();
|
||||
public string Fee { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,18 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
public class WalletPSBTViewModel
|
||||
{
|
||||
public string Decoded { get; set; }
|
||||
string _FileName;
|
||||
public string FileName
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.IsNullOrEmpty(_FileName) ? "psbt-export.psbt" : _FileName;
|
||||
}
|
||||
set
|
||||
{
|
||||
_FileName = value;
|
||||
}
|
||||
}
|
||||
public string PSBT { get; set; }
|
||||
public List<string> Errors { get; set; } = new List<string>();
|
||||
|
||||
|
@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Models.WalletViewModels
|
||||
{
|
||||
public class WalletSettingsViewModel
|
||||
{
|
||||
public string Label { get; set; }
|
||||
public string DerivationScheme { get; set; }
|
||||
public string DerivationSchemeInput { get; set; }
|
||||
[Display(Name = "Is signing key")]
|
||||
public string SelectedSigningKey { get; set; }
|
||||
public bool IsMultiSig => AccountKeys.Count > 1;
|
||||
|
||||
public List<WalletSettingsAccountKeyViewModel> AccountKeys { get; set; } = new List<WalletSettingsAccountKeyViewModel>();
|
||||
}
|
||||
|
||||
public class WalletSettingsAccountKeyViewModel
|
||||
{
|
||||
public string AccountKey { get; set; }
|
||||
[Validation.HDFingerPrintValidator]
|
||||
public string MasterFingerprint { get; set; }
|
||||
[Validation.KeyPathValidator]
|
||||
public string AccountKeyPath { get; set; }
|
||||
}
|
||||
}
|
@ -5,12 +5,14 @@
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"BTCPAY_NETWORK": "regtest",
|
||||
"BTCPAY_LAUNCHSETTINGS": "true",
|
||||
"BTCPAY_BUNDLEJSCSS": "false",
|
||||
"BTCPAY_LTCEXPLORERURL": "http://127.0.0.1:32838/",
|
||||
"BTCPAY_BTCLIGHTNING": "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify",
|
||||
"BTCPAY_BTCEXTERNALLNDGRPC": "type=lnd-grpc;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true",
|
||||
"BTCPAY_BTCEXTERNALLNDREST": "type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/lnd-rest/btc/;allowinsecure=true;macaroonfilepath=D:\\admin.macaroon",
|
||||
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
|
||||
"BTCPAY_ALLOW-ADMIN-REGISTRATION": "true",
|
||||
"BTCPAY_DISABLE-REGISTRATION": "false",
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"BTCPAY_CHAINS": "btc,ltc",
|
||||
@ -23,6 +25,7 @@
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"BTCPAY_NETWORK": "regtest",
|
||||
"BTCPAY_LAUNCHSETTINGS": "true",
|
||||
"BTCPAY_PORT": "14142",
|
||||
"BTCPAY_HttpsUseDefaultCertificate": "true",
|
||||
"BTCPAY_BUNDLEJSCSS": "false",
|
||||
@ -33,6 +36,7 @@
|
||||
"BTCPAY_BTCEXTERNALSPARK": "server=/spark/btc/;cookiefile=fake",
|
||||
"BTCPAY_BTCEXTERNALCHARGE": "server=https://127.0.0.1:53280/mycharge/btc/;cookiefilepath=fake",
|
||||
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
|
||||
"BTCPAY_ALLOW-ADMIN-REGISTRATION": "true",
|
||||
"BTCPAY_DISABLE-REGISTRATION": "false",
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"BTCPAY_CHAINS": "btc,ltc",
|
||||
|
@ -1,15 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace BTCPayServer.Security
|
||||
@ -17,13 +12,13 @@ namespace BTCPayServer.Security
|
||||
|
||||
public class BTCPayClaimsFilter : IAsyncAuthorizationFilter, IConfigureOptions<MvcOptions>
|
||||
{
|
||||
UserManager<ApplicationUser> _UserManager;
|
||||
UserManager<ApplicationUser> _userManager;
|
||||
StoreRepository _StoreRepository;
|
||||
public BTCPayClaimsFilter(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
StoreRepository storeRepository)
|
||||
{
|
||||
_UserManager = userManager;
|
||||
_userManager = userManager;
|
||||
_StoreRepository = storeRepository;
|
||||
}
|
||||
|
||||
@ -35,28 +30,34 @@ namespace BTCPayServer.Security
|
||||
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
|
||||
{
|
||||
var principal = context.HttpContext.User;
|
||||
if (!context.HttpContext.GetIsBitpayAPI())
|
||||
if (context.HttpContext.GetIsBitpayAPI())
|
||||
{
|
||||
var identity = ((ClaimsIdentity)principal.Identity);
|
||||
if (principal.IsInRole(Roles.ServerAdmin))
|
||||
return;
|
||||
}
|
||||
|
||||
var identity = ((ClaimsIdentity)principal.Identity);
|
||||
if (principal.IsInRole(Roles.ServerAdmin))
|
||||
{
|
||||
identity.AddClaim(new Claim(Policies.CanModifyServerSettings.Key, "true"));
|
||||
}
|
||||
|
||||
if (context.RouteData.Values.TryGetValue("storeId", out var storeId))
|
||||
{
|
||||
var userid = _userManager.GetUserId(principal);
|
||||
|
||||
if (!string.IsNullOrEmpty(userid))
|
||||
{
|
||||
identity.AddClaim(new Claim(Policies.CanModifyServerSettings.Key, "true"));
|
||||
}
|
||||
if (context.RouteData.Values.TryGetValue("storeId", out var storeId))
|
||||
{
|
||||
var claim = identity.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier);
|
||||
if (claim != null)
|
||||
var store = await _StoreRepository.FindStore((string)storeId, userid);
|
||||
if (store == null)
|
||||
{
|
||||
var store = await _StoreRepository.FindStore((string)storeId, claim.Value);
|
||||
if (store == null)
|
||||
context.Result = new ChallengeResult(Policies.CookieAuthentication);
|
||||
else
|
||||
context.Result = new ChallengeResult();
|
||||
}
|
||||
else
|
||||
{
|
||||
context.HttpContext.SetStoreData(store);
|
||||
if (store != null)
|
||||
{
|
||||
context.HttpContext.SetStoreData(store);
|
||||
if (store != null)
|
||||
{
|
||||
identity.AddClaims(store.GetClaims());
|
||||
}
|
||||
identity.AddClaims(store.GetClaims());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,5 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
@ -10,13 +8,9 @@ using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Authentication;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
@ -27,7 +21,6 @@ using BTCPayServer.Logging;
|
||||
using Microsoft.AspNetCore.Http.Internal;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using System.Text.Encodings.Web;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace BTCPayServer.Security
|
||||
{
|
||||
@ -185,10 +178,10 @@ namespace BTCPayServer.Security
|
||||
return await _TokenRepository.GetStoreIdFromAPIKey(apiKey);
|
||||
}
|
||||
}
|
||||
internal static void AddAuthentication(IServiceCollection services, Action<BitpayAuthOptions> bitpayAuth = null)
|
||||
public static void AddAuthentication(AuthenticationBuilder builder, Action<BitpayAuthOptions> bitpayAuth = null)
|
||||
{
|
||||
bitpayAuth = bitpayAuth ?? new Action<BitpayAuthOptions>((o) => { });
|
||||
services.AddAuthentication().AddScheme<BitpayAuthOptions, BitpayAuthHandler>(Policies.BitpayAuthentication, bitpayAuth);
|
||||
builder.AddScheme<BitpayAuthOptions, BitpayAuthHandler>(Policies.BitpayAuthentication, bitpayAuth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
15
BTCPayServer/Security/BitpayAuthenticationExtensions.cs
Normal file
15
BTCPayServer/Security/BitpayAuthenticationExtensions.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
|
||||
namespace BTCPayServer.Security
|
||||
{
|
||||
public static class BitpayAuthenticationExtensions
|
||||
{
|
||||
public static AuthenticationBuilder AddBitpayAuthentication(this AuthenticationBuilder builder,
|
||||
Action<BitpayAuthentication.BitpayAuthOptions> bitpayAuth = null)
|
||||
{
|
||||
BitpayAuthentication.AddAuthentication(builder,bitpayAuth);
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
}
|
@ -62,8 +62,7 @@ namespace BTCPayServer.Services
|
||||
return foundKeyPath;
|
||||
}
|
||||
|
||||
public abstract Task<PSBT> SignTransactionAsync(PSBT psbt, Script changeHint,
|
||||
CancellationToken cancellationToken);
|
||||
public abstract Task<PSBT> SignTransactionAsync(PSBT psbt, RootedKeyPath accountKeyPath, BitcoinExtPubKey accountKey, Script changeHint, CancellationToken cancellationToken);
|
||||
|
||||
public virtual void Dispose()
|
||||
{
|
||||
|
@ -115,29 +115,26 @@ namespace BTCPayServer.Services
|
||||
return extpubkey;
|
||||
}
|
||||
|
||||
public override async Task<PSBT> SignTransactionAsync(PSBT psbt, Script changeHint, CancellationToken cancellationToken)
|
||||
public override async Task<PSBT> SignTransactionAsync(PSBT psbt, RootedKeyPath accountKeyPath, BitcoinExtPubKey accountKey, Script changeHint, CancellationToken cancellationToken)
|
||||
{
|
||||
var unsigned = psbt.GetGlobalTransaction();
|
||||
var changeKeyPath = psbt.Outputs
|
||||
.Where(o => changeHint == null ? true : changeHint == o.ScriptPubKey)
|
||||
.Where(o => o.HDKeyPaths.Any())
|
||||
.Select(o => o.HDKeyPaths.First().Value.Item2)
|
||||
var changeKeyPath = psbt.Outputs.HDKeysFor(accountKey, accountKeyPath)
|
||||
.Where(o => changeHint == null ? true : changeHint == o.Coin.ScriptPubKey)
|
||||
.Select(o => o.RootedKeyPath.KeyPath)
|
||||
.FirstOrDefault();
|
||||
var signatureRequests = psbt
|
||||
.Inputs
|
||||
.Where(o => o.HDKeyPaths.Any())
|
||||
.Where(o => !o.PartialSigs.ContainsKey(o.HDKeyPaths.First().Key))
|
||||
.HDKeysFor(accountKey, accountKeyPath)
|
||||
.Where(hd => !hd.Coin.PartialSigs.ContainsKey(hd.PubKey)) // Don't want to sign something twice
|
||||
.GroupBy(hd => hd.Coin)
|
||||
.Select(i => new SignatureRequest()
|
||||
{
|
||||
InputCoin = i.GetSignableCoin(),
|
||||
InputTransaction = i.NonWitnessUtxo,
|
||||
KeyPath = i.HDKeyPaths.First().Value.Item2,
|
||||
PubKey = i.HDKeyPaths.First().Key
|
||||
InputCoin = i.Key.GetSignableCoin(),
|
||||
InputTransaction = i.Key.NonWitnessUtxo,
|
||||
KeyPath = i.First().RootedKeyPath.KeyPath,
|
||||
PubKey = i.First().PubKey
|
||||
}).ToArray();
|
||||
var signedTransaction = await Ledger.SignTransactionAsync(signatureRequests, unsigned, changeKeyPath, cancellationToken);
|
||||
if (signedTransaction == null)
|
||||
throw new HardwareWalletException("The ledger failed to sign the transaction");
|
||||
|
||||
await Ledger.SignTransactionAsync(signatureRequests, unsigned, changeKeyPath, cancellationToken);
|
||||
psbt = psbt.Clone();
|
||||
foreach (var signature in signatureRequests)
|
||||
{
|
||||
|
@ -57,10 +57,10 @@ namespace BTCPayServer.Storage.Services.Providers.FileSystemStorage
|
||||
StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
public override Task<string> GetTemporaryFileUrl(StoredFile storedFile, StorageSettings configuration, DateTimeOffset expiry, bool isDownload,
|
||||
public override async Task<string> GetTemporaryFileUrl(StoredFile storedFile, StorageSettings configuration, DateTimeOffset expiry, bool isDownload,
|
||||
BlobUrlAccess access = BlobUrlAccess.Read)
|
||||
{
|
||||
return GetFileUrl(storedFile, configuration);
|
||||
return $"{(await GetFileUrl(storedFile, configuration))}{(isDownload ? "?download" : string.Empty)}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,14 @@ namespace BTCPayServer.Storage
|
||||
{
|
||||
ServeUnknownFileTypes = true,
|
||||
RequestPath = new PathString($"/{FileSystemFileProviderService.LocalStorageDirectoryName}"),
|
||||
FileProvider = new PhysicalFileProvider(dirInfo.FullName)
|
||||
FileProvider = new PhysicalFileProvider(dirInfo.FullName),
|
||||
OnPrepareResponse = context =>
|
||||
{
|
||||
if (context.Context.Request.Query.ContainsKey("download"))
|
||||
{
|
||||
context.Context.Response.Headers["Content-Disposition"] = "attachment";
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
32
BTCPayServer/Validation/HDFingerPrintValidator.cs
Normal file
32
BTCPayServer/Validation/HDFingerPrintValidator.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
|
||||
namespace BTCPayServer.Validation
|
||||
{
|
||||
public class HDFingerPrintValidator : ValidationAttribute
|
||||
{
|
||||
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
|
||||
{
|
||||
var str = value as string;
|
||||
if (string.IsNullOrWhiteSpace(str))
|
||||
{
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
new HDFingerprint(Encoders.Hex.DecodeData(str));
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new ValidationResult("Invalid fingerprint");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
29
BTCPayServer/Validation/KeyPathValidator.cs
Normal file
29
BTCPayServer/Validation/KeyPathValidator.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Validation
|
||||
{
|
||||
public class KeyPathValidator : ValidationAttribute
|
||||
{
|
||||
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
|
||||
{
|
||||
var str = value as string;
|
||||
if (string.IsNullOrWhiteSpace(str))
|
||||
{
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
if (KeyPath.TryParse(str, out _))
|
||||
{
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ValidationResult("Invalid keypath");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -41,7 +41,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary">Log in</button>
|
||||
<button type="submit" class="btn btn-primary" id="LoginButton">Log in</button>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<p>
|
||||
|
@ -35,7 +35,15 @@
|
||||
<input asp-for="ConfirmPassword" class="form-control" />
|
||||
<span asp-validation-for="ConfirmPassword" class="text-danger"></span>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Register</button>
|
||||
@if (ViewData["AllowIsAdmin"] is true)
|
||||
{
|
||||
<div class="form-group">
|
||||
<label asp-for="IsAdmin"></label>
|
||||
<input asp-for="IsAdmin" class="form-check" />
|
||||
<span asp-validation-for="IsAdmin" class="text-danger"></span>
|
||||
</div>
|
||||
}
|
||||
<button type="submit" class="btn btn-primary" id="RegisterButton">Register</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -28,7 +28,7 @@
|
||||
<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" />
|
||||
<input type="submit" value="Create" class="btn btn-primary" id="Create" />
|
||||
</div>
|
||||
</form>
|
||||
<a asp-action="ListApps">Back to the app list</a>
|
||||
|
@ -23,7 +23,7 @@
|
||||
|
||||
<div class="row no-gutter" style="margin-bottom: 5px;">
|
||||
<div class="col-lg-6">
|
||||
<a asp-action="CreateApp" class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Create a new app</a>
|
||||
<a asp-action="CreateApp" class="btn btn-primary" role="button" id="CreateNewApp"><span class="fa fa-plus"></span> Create a new app</a>
|
||||
<a href="https://docs.btcpayserver.org/features/apps" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -211,9 +211,9 @@
|
||||
|
||||
<input type="hidden" asp-for="NotificationEmailWarning" />
|
||||
<div class="form-group">
|
||||
<input type="submit" class="btn btn-primary" value="Save Settings" />
|
||||
<input type="submit" class="btn btn-primary" value="Save Settings" id="SaveSettings" />
|
||||
<a class="btn btn-secondary" target="_blank" asp-action="ListInvoices" asp-controller="Invoice" asp-route-searchterm="@Model.SearchTerm">Invoices</a>
|
||||
<a class="btn btn-secondary" target="_blank" asp-action="ViewCrowdfund" asp-controller="AppsPublic" asp-route-appId="@Model.AppId">View App</a>
|
||||
<a class="btn btn-secondary" target="_blank" asp-action="ViewCrowdfund" asp-controller="AppsPublic" asp-route-appId="@Model.AppId" id="ViewApp">View App</a>
|
||||
<a class="btn btn-secondary" target="_blank" asp-action="ListApps">Back to the app list</a>
|
||||
</div>
|
||||
|
||||
|
@ -6,19 +6,19 @@
|
||||
<div class="modal" id="product-modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Product management</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Modal body text goes here.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="js-product-save btn btn-primary">Save Changes</button>
|
||||
</div>
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Product management</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Modal body text goes here.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="js-product-save btn btn-primary">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -119,7 +119,7 @@
|
||||
{
|
||||
<partial name="NotificationEmailWarning"></partial>
|
||||
}
|
||||
<input type="email" asp-for="NotificationEmail" class="form-control"/>
|
||||
<input type="email" asp-for="NotificationEmail" class="form-control" />
|
||||
<span asp-validation-for="NotificationEmail" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@ -127,9 +127,9 @@
|
||||
<select asp-for="RedirectAutomatically" asp-items="Model.RedirectAutomaticallySelectList" class="form-control"></select>
|
||||
<span asp-validation-for="RedirectAutomatically" class="text-danger"></span>
|
||||
</div>
|
||||
<input type="hidden" asp-for="NotificationEmailWarning"/>
|
||||
<input type="hidden" asp-for="NotificationEmailWarning" />
|
||||
<div class="form-group">
|
||||
<input type="submit" class="btn btn-primary" value="Save Settings" />
|
||||
<input type="submit" class="btn btn-primary" value="Save Settings" id="SaveSettings" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="accordion" id="accordian-dev-info">
|
||||
@ -163,7 +163,7 @@
|
||||
<h2 class="mb-0">
|
||||
<button class="btn btn-link collapsed" type="button" data-toggle="collapse" data-target="#accordian-dev-info-embed-pos-iframe" aria-expanded="false" aria-controls="accordian-dev-info-embed-pos-iframe">
|
||||
Embed POS with Iframe
|
||||
|
||||
|
||||
</button>
|
||||
</h2>
|
||||
</div>
|
||||
@ -171,17 +171,17 @@
|
||||
<div class="card-body">
|
||||
You can embed the POS using an iframe
|
||||
@{
|
||||
var iframe = $"<iframe src='{(Url.Action("ViewPointOfSale", "AppsPublic", new {appId = Model.Id}, Context.Request.Scheme))}' style='max-width: 100%; border: 0;'></iframe>";
|
||||
var iframe = $"<iframe src='{(Url.Action("ViewPointOfSale", "AppsPublic", new { appId = Model.Id }, Context.Request.Scheme))}' style='max-width: 100%; border: 0;'></iframe>";
|
||||
}
|
||||
<pre><code class="html">@iframe</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header" id="accordian-dev-info-notification-header">
|
||||
<h2 class="mb-0">
|
||||
<button class="btn btn-link collapsed" type="button" data-toggle="collapse" data-target="#accordian-dev-info-notification" aria-expanded="false" aria-controls="accordian-dev-info-notification">
|
||||
Notification Url Callbacks
|
||||
Notification Url Callbacks
|
||||
</button>
|
||||
</h2>
|
||||
</div>
|
||||
@ -196,7 +196,8 @@
|
||||
<li>Verify that the <code>orderId</code> is from your backend, that the <code>price</code> is correct and that <code>status</code> is either <code>confirmed</code> or <code>complete</code></li>
|
||||
<li>You can then ship your order</li>
|
||||
</ul>
|
||||
</p> </div>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -204,8 +205,8 @@
|
||||
</div>
|
||||
</form>
|
||||
<a asp-action="ListApps">Back to the app list</a>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -230,38 +231,38 @@
|
||||
</script>
|
||||
|
||||
<script id="template-product-content" type="text/template">
|
||||
<div class="mb-3">
|
||||
<input class="js-product-id" type="hidden" name="id" value="{id}">
|
||||
<input class="js-product-index" type="hidden" name="index" value="{index}">
|
||||
<div class="form-row">
|
||||
<div class="col-sm-6">
|
||||
<label>Title</label>*
|
||||
<input type="text" class="js-product-title form-control mb-2" value="{title}" autofocus />
|
||||
<div class="mb-3">
|
||||
<input class="js-product-id" type="hidden" name="id" value="{id}">
|
||||
<input class="js-product-index" type="hidden" name="index" value="{index}">
|
||||
<div class="form-row">
|
||||
<div class="col-sm-6">
|
||||
<label>Title</label>*
|
||||
<input type="text" class="js-product-title form-control mb-2" value="{title}" autofocus />
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<label>Price</label>*
|
||||
<input type="number" class="js-product-price form-control mb-2" value="{price}" />
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<label>Custom price</label>
|
||||
<select class="js-product-custom form-control">
|
||||
{custom}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<label>Price</label>*
|
||||
<input type="number" class="js-product-price form-control mb-2" value="{price}" />
|
||||
<div class="form-row">
|
||||
<div class="col">
|
||||
<label>Image</label>
|
||||
<input type="text" class="js-product-image form-control mb-2" value="{image}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<label>Custom price</label>
|
||||
<select class="js-product-custom form-control">
|
||||
{custom}
|
||||
</select>
|
||||
<div class="form-row">
|
||||
<div class="col">
|
||||
<label>Description</label>
|
||||
<textarea rows="3" cols="40" class="js-product-description form-control mb-2">{description}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col">
|
||||
<label>Image</label>
|
||||
<input type="text" class="js-product-image form-control mb-2" value="{image}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col">
|
||||
<label>Description</label>
|
||||
<textarea rows="3" cols="40" class="js-product-description form-control mb-2">{description}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script src="~/products/js/products.js"></script>
|
||||
|
@ -73,7 +73,7 @@
|
||||
<span asp-validation-for="SupportedTransactionCurrencies" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="submit" value="Create" class="btn btn-primary" />
|
||||
<input type="submit" value="Create" class="btn btn-primary" id="Create" />
|
||||
</div>
|
||||
</form>
|
||||
<a asp-action="ListInvoices">Back to List</a>
|
||||
|
@ -47,7 +47,7 @@
|
||||
|
||||
<div class="row no-gutter" style="margin-bottom: 5px;">
|
||||
<div class="col-lg-6">
|
||||
<a asp-action="CreateInvoice" class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Create a new invoice</a>
|
||||
<a asp-action="CreateInvoice" class="btn btn-primary" role="button" id="CreateNewInvoice"><span class="fa fa-plus"></span> Create a new invoice</a>
|
||||
|
||||
<a class="btn btn-primary dropdown-toggle" href="#" role="button" id="dropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Export
|
||||
@ -246,7 +246,7 @@
|
||||
@if (invoice.ShowCheckout)
|
||||
{
|
||||
<span>
|
||||
<a asp-action="Checkout" asp-route-invoiceId="@invoice.InvoiceId">Checkout</a>
|
||||
<a asp-action="Checkout" class="invoice-checkout-link" asp-route-invoiceId="@invoice.InvoiceId">Checkout</a>
|
||||
<a href="javascript:btcpay.showInvoice('@invoice.InvoiceId')">[^]</a>
|
||||
@if (!invoice.CanMarkStatus)
|
||||
{
|
||||
@ -255,7 +255,7 @@
|
||||
</span>
|
||||
}
|
||||
|
||||
<a asp-action="Invoice" asp-route-invoiceId="@invoice.InvoiceId">Details</a>
|
||||
<a asp-action="Invoice" class="invoice-details-link" asp-route-invoiceId="@invoice.InvoiceId">Details</a>
|
||||
@*<span title="Details" class="fa fa-list"></span>*@
|
||||
|
||||
<a href="javascript:void(0);" onclick="detailsToggle(this, '@invoice.InvoiceId')">
|
||||
|
@ -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-primary">Update password</button>
|
||||
<button type="submit" class="btn btn-primary" id="UpdatePassword">Update password</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -5,7 +5,7 @@
|
||||
|
||||
<div class="nav flex-column nav-pills">
|
||||
<a class="nav-link @ViewData.IsActivePage(ManageNavPages.Index)" asp-action="Index">Profile</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(ManageNavPages.ChangePassword)" asp-action="ChangePassword">Password</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(ManageNavPages.ChangePassword)" asp-action="ChangePassword" id="ChangePassword">Password</a>
|
||||
@if (hasExternalLogins)
|
||||
{
|
||||
<a class="nav-link @ViewData.IsActivePage(ManageNavPages.ExternalLogins)" asp-action="ExternalLogins">External logins</a>
|
||||
|
@ -100,10 +100,10 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
<button type="submit" class="btn btn-primary" id="SaveButton">Save</button>
|
||||
@if (!string.IsNullOrEmpty(Model.Id))
|
||||
{
|
||||
<a class="btn btn-secondary" target="_blank" asp-action="ViewPaymentRequest" id="@Model.Id">View</a>
|
||||
<a class="btn btn-secondary" target="_blank" asp-action="ViewPaymentRequest" id="@Model.Id" name="ViewAppButton">View</a>
|
||||
<a class="btn btn-secondary"
|
||||
target="_blank"
|
||||
asp-action="ListInvoices"
|
||||
|
@ -22,7 +22,7 @@
|
||||
|
||||
<div class="row no-gutter" style="margin-bottom: 5px;">
|
||||
<div class="col-lg-6">
|
||||
<a asp-action="EditPaymentRequest" class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Create a new payment request</a>
|
||||
<a asp-action="EditPaymentRequest" class="btn btn-primary" role="button" id="CreatePaymentRequest"><span class="fa fa-plus"></span> Create a new payment request</a>
|
||||
<a href="https://docs.btcpayserver.org/features/paymentrequests" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -22,7 +22,7 @@
|
||||
{
|
||||
<tr>
|
||||
<td>@file.FileName</td>
|
||||
<td>@file.Timestamp.ToString("g")</td>
|
||||
<td >@file.Timestamp.ToBrowserDate2()</td>
|
||||
<td>@file.ApplicationUser.UserName</td>
|
||||
<td>
|
||||
<a asp-action="Files" asp-route-fileId="@file.Id">Get Link</a>
|
||||
|
@ -3,8 +3,24 @@
|
||||
ViewData["Title"] = "Error";
|
||||
}
|
||||
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
<h1 class="text-danger">
|
||||
@if (!string.IsNullOrEmpty(Model.Error)) {
|
||||
<strong>@Model.Error</strong>
|
||||
}
|
||||
else
|
||||
{
|
||||
@:Error.
|
||||
}
|
||||
</h1>
|
||||
<h2 class="text-danger">
|
||||
@if (!string.IsNullOrEmpty(Model.ErrorDescription)) {
|
||||
<small>@Model.ErrorDescription</small>
|
||||
}
|
||||
else
|
||||
{
|
||||
@:An error occurred while processing your request.
|
||||
}
|
||||
</h2>
|
||||
|
||||
@if(Model != null && Model.ShowRequestId)
|
||||
{
|
||||
|
@ -72,26 +72,26 @@
|
||||
{
|
||||
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="Server" asp-action="ListUsers" class="nav-link js-scroll-trigger" id="ServerSettings">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="Wallets" asp-action="ListWallets" class="nav-link js-scroll-trigger">Wallets</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="PaymentRequest" asp-action="GetPaymentRequests" class="nav-link js-scroll-trigger">Payment Requests</a></li>
|
||||
<li class="nav-item"><a asp-area="" asp-controller="UserStores" asp-action="ListStores" class="nav-link js-scroll-trigger" id="Stores">Stores</a></li>
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Apps" asp-action="ListApps" class="nav-link js-scroll-trigger" id="Apps">Apps</a></li>
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Wallets" asp-action="ListWallets" class="nav-link js-scroll-trigger" id="Wallets">Wallets</a></li>
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Invoice" asp-action="ListInvoices" class="nav-link js-scroll-trigger" id="Invoices">Invoices</a></li>
|
||||
<li class="nav-item"><a asp-area="" asp-controller="PaymentRequest" asp-action="GetPaymentRequests" class="nav-link js-scroll-trigger" id="PaymentRequests">Payment Requests</a></li>
|
||||
<li class="nav-item">
|
||||
<a asp-area="" asp-controller="Manage" asp-action="Index" title="My settings" class="nav-link js-scroll-trigger"><i class="fa fa-user"></i></a>
|
||||
<a asp-area="" asp-controller="Manage" asp-action="Index" title="My settings" class="nav-link js-scroll-trigger" id="MySettings"><i class="fa fa-user"></i></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a asp-area="" asp-controller="Account" asp- asp-action="Logout" title="Logout" class="nav-link js-scroll-trigger"><i class="fa fa-sign-out"></i></a>
|
||||
<a asp-area="" asp-controller="Account" asp- asp-action="Logout" title="Logout" class="nav-link js-scroll-trigger" id="Logout"><i class="fa fa-sign-out"></i></a>
|
||||
</li>}
|
||||
else if (env.IsSecure)
|
||||
{
|
||||
if (themeManager.ShowRegister)
|
||||
{
|
||||
<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="Register" class="nav-link js-scroll-trigger" id="Register">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>
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Account" asp-action="Login" class="nav-link js-scroll-trigger" id="Login">Log in</a></li>
|
||||
}
|
||||
|
||||
</ul>
|
||||
|
@ -130,7 +130,7 @@
|
||||
<label asp-for="Enabled"></label>
|
||||
<input asp-for="Enabled" type="checkbox" class="form-check" />
|
||||
</div>
|
||||
<button name="command" type="submit" class="btn btn-primary" value="save">Continue</button>
|
||||
<button name="command" type="submit" class="btn btn-primary" value="save" id="Continue">Continue</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -175,7 +175,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-primary" value="save">Confirm</button>
|
||||
<button name="command" type="submit" class="btn btn-primary" value="save" id="Confirm">Confirm</button>
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
|
@ -121,7 +121,7 @@
|
||||
{
|
||||
<a asp-action="WalletTransactions" asp-controller="Wallets" asp-route-walletId="@scheme.WalletId">Wallet</a><span> - </span>
|
||||
}
|
||||
<a asp-action="AddDerivationScheme" asp-route-cryptoCode="@scheme.Crypto">Modify</a>
|
||||
<a asp-action="AddDerivationScheme" asp-route-cryptoCode="@scheme.Crypto" id="ModifyBTC">Modify</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@ -247,7 +247,7 @@
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<button name="command" type="submit" class="btn btn-primary" value="Save">Save</button>
|
||||
<button name="command" type="submit" class="btn btn-primary" value="Save" id="Save">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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-primary" />
|
||||
<input type="submit" value="Create" class="btn btn-primary" id="Create" />
|
||||
</div>
|
||||
</form>
|
||||
<a asp-action="ListStores">Back to List</a>
|
||||
|
@ -21,7 +21,7 @@
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<a asp-action="CreateStore" class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Create a new store</a>
|
||||
<a asp-action="CreateStore" class="btn btn-primary" role="button" id="CreateStore"><span class="fa fa-plus"></span> Create a new store</a>
|
||||
<table class="table table-sm table-responsive-md">
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -31,6 +31,11 @@ namespace BTCPayServer.Views
|
||||
return new HtmlString($"<span class='localizeDate'>{hello}</span>");
|
||||
}
|
||||
|
||||
public static HtmlString ToBrowserDate2(this DateTime date)
|
||||
{
|
||||
var hello = date.ToString("o", CultureInfo.InvariantCulture);
|
||||
return new HtmlString($"<span class='localizeDate'>{hello}</span>");
|
||||
}
|
||||
public static string ToTimeAgo(this DateTimeOffset date)
|
||||
{
|
||||
var formatted = (DateTimeOffset.UtcNow - date).TimeString() + " ago";
|
||||
|
27
BTCPayServer/Views/Wallets/SignWithSeed.cshtml
Normal file
27
BTCPayServer/Views/Wallets/SignWithSeed.cshtml
Normal file
@ -0,0 +1,27 @@
|
||||
@model SignWithSeedViewModel
|
||||
@{
|
||||
Layout = "../Shared/_NavLayout.cshtml";
|
||||
ViewData["Title"] = "Sign PSBT With an HD private key or mnemonic seed";
|
||||
ViewData.SetActivePageAndTitle(WalletsNavPages.Send);
|
||||
}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
<form method="post" asp-action="SignWithSeed">
|
||||
|
||||
<input type="hidden" asp-for="PSBT" />
|
||||
<div class="form-group">
|
||||
<label asp-for="SeedOrKey"></label>
|
||||
<input asp-for="SeedOrKey" class="form-control" />
|
||||
<span asp-validation-for="SeedOrKey" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Passphrase"></label>
|
||||
<input asp-for="Passphrase" class="form-control" />
|
||||
<span asp-validation-for="Passphrase" class="text-danger"></span>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Sign</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
@ -32,12 +32,14 @@
|
||||
Sign with...
|
||||
</button>
|
||||
<input type="hidden" asp-for="PSBT" />
|
||||
<input type="hidden" asp-for="FileName" />
|
||||
<div class="dropdown-menu" aria-labelledby="SendMenu">
|
||||
<button name="command" type="submit" class="dropdown-item" value="ledger">... your Ledger Wallet device</button>
|
||||
<button name="command" type="submit" class="dropdown-item" value="save-psbt">... a wallet supporting PSBT files</button>
|
||||
<button name="command" type="submit" class="dropdown-item" value="seed">... an HD private key or mnemonic seed</button>
|
||||
<button name="command" type="submit" class="dropdown-item" value="save-psbt">... a wallet supporting PSBT (save as file)</button>
|
||||
</div>
|
||||
<button name="command" type="submit" class="btn btn-primary" value="broadcast">Broadcast</button>
|
||||
<button name="command" type="submit" class="btn btn-primary" value="combine">Combine with another PSBT</button>
|
||||
<button name="command" type="submit" class="btn btn-secondary" value="broadcast">Broadcast</button>
|
||||
<button name="command" type="submit" class="btn btn-secondary" value="combine">Combine with another PSBT</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -16,18 +16,70 @@
|
||||
}
|
||||
<div class="row">
|
||||
<div class="col-lg-12 text-center">
|
||||
<h2 class="section-heading">Transaction signed</h2>
|
||||
<h2 class="section-heading">Transaction review</h2>
|
||||
<hr class="primary">
|
||||
<p>Your transaction has been signed, what do you want to do next?</p>
|
||||
<p>
|
||||
If you broadcast this transaction, your balance will change: @if (Model.Positive)
|
||||
{
|
||||
<span style="color:green;">@Model.BalanceChange</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span style="color:red;">@Model.BalanceChange</span>
|
||||
}, do you want to continue?
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-3 text-center"></div>
|
||||
<div class="col-lg-6 text-center">
|
||||
<table class="table table-sm table-responsive-lg">
|
||||
<thead class="thead-inverse">
|
||||
<tr>
|
||||
<th style="text-align:left" class="col-md-auto">
|
||||
Destination
|
||||
</th>
|
||||
<th style="text-align:right">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var destination in Model.Destinations)
|
||||
{
|
||||
<tr>
|
||||
<td style="text-align:left">@destination.Destination</td>
|
||||
@if (destination.Positive)
|
||||
{
|
||||
<td style="text-align:right; color:green;">@destination.Balance</td>
|
||||
}
|
||||
else
|
||||
{
|
||||
<td style="text-align:right; color:red;">@destination.Balance</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-lg-3 text-center"></div>
|
||||
</div>
|
||||
@if (Model.Fee != null)
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-lg-3 text-center"></div>
|
||||
<div class="col-lg-6 text-left">
|
||||
<p>Transaction fee: <span style="color: red">@Model.Fee</span></p>
|
||||
</div>
|
||||
<div class="col-lg-3 text-center"></div>
|
||||
</div>
|
||||
}
|
||||
<div class="row">
|
||||
<div class="col-lg-12 text-center">
|
||||
<form method="post" asp-action="WalletPSBTReady">
|
||||
<input type="hidden" asp-for="PSBT" />
|
||||
<input type="hidden" asp-for="SigningKey" />
|
||||
<input type="hidden" asp-for="SigningKeyPath" />
|
||||
<button type="submit" class="btn btn-primary" name="command" value="broadcast">Broadcast it</button> or
|
||||
<button type="submit" class="btn btn-primary" name="command" value="analyze-psbt">Export as PSBT</button>
|
||||
<button type="submit" class="btn btn-secondary" name="command" value="analyze-psbt">Export as PSBT</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -89,8 +89,8 @@
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="SendMenu">
|
||||
<button name="command" type="submit" class="dropdown-item" value="ledger">... your Ledger Wallet device</button>
|
||||
<button name="command" type="submit" class="dropdown-item" value="save-psbt">... a wallet supporting PSBT</button>
|
||||
<button name="command" type="submit" class="dropdown-item" value="analyze-psbt">... analyze the PSBT</button>
|
||||
<button name="command" type="submit" class="dropdown-item" value="seed">... an HD private key or mnemonic seed</button>
|
||||
<button name="command" type="submit" class="dropdown-item" value="analyze-psbt">... a wallet supporting PSBT</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
77
BTCPayServer/Views/Wallets/WalletSettings.cshtml
Normal file
77
BTCPayServer/Views/Wallets/WalletSettings.cshtml
Normal file
@ -0,0 +1,77 @@
|
||||
@model WalletSettingsViewModel
|
||||
@{
|
||||
Layout = "../Shared/_NavLayout.cshtml";
|
||||
ViewData["Title"] = "Wallet settings";
|
||||
ViewData.SetActivePageAndTitle(WalletsNavPages.Settings);
|
||||
}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-10 text-center">
|
||||
<partial name="_StatusMessage" for="@TempData["TempDataProperty-StatusMessage"]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<p>
|
||||
Additional information about your wallet
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form method="post" asp-action="WalletSettings">
|
||||
<div class="form-group">
|
||||
<label asp-for="Label"></label>
|
||||
<input asp-for="Label" class="form-control" />
|
||||
<span asp-validation-for="Label" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="DerivationScheme"></label>
|
||||
<input asp-for="DerivationScheme" class="form-control" readonly />
|
||||
<span asp-validation-for="DerivationScheme" class="text-danger"></span>
|
||||
</div>
|
||||
@if (!string.IsNullOrEmpty(Model.DerivationSchemeInput) && Model.DerivationSchemeInput != Model.DerivationScheme)
|
||||
{
|
||||
<div class="form-group">
|
||||
<label asp-for="DerivationSchemeInput"></label>
|
||||
<input asp-for="DerivationSchemeInput" class="form-control" readonly />
|
||||
<span asp-validation-for="DerivationSchemeInput" class="text-danger"></span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@for (int i = 0; i < Model.AccountKeys.Count; i++)
|
||||
{
|
||||
<hr />
|
||||
<h5>Account key @i</h5>
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.AccountKeys[i].AccountKey"></label>
|
||||
<input asp-for="@Model.AccountKeys[i].AccountKey" class="form-control" readonly />
|
||||
<span asp-validation-for="@Model.AccountKeys[i].AccountKey" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.AccountKeys[i].MasterFingerprint"></label>
|
||||
<input asp-for="@Model.AccountKeys[i].MasterFingerprint" class="form-control" />
|
||||
<span asp-validation-for="@Model.AccountKeys[i].MasterFingerprint" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="@Model.AccountKeys[i].AccountKeyPath"></label>
|
||||
<input asp-for="@Model.AccountKeys[i].AccountKeyPath" class="form-control" />
|
||||
<span asp-validation-for="@Model.AccountKeys[i].AccountKeyPath" class="text-danger"></span>
|
||||
</div>
|
||||
@if (Model.IsMultiSig)
|
||||
{
|
||||
<div class="form-group">
|
||||
<label asp-for="SelectedSigningKey"></label>
|
||||
<input asp-for="SelectedSigningKey" type="radio" value="@Model.AccountKeys[i].AccountKey" />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
<div class="form-group">
|
||||
<button name="command" type="submit" class="btn btn-primary">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
@ -10,6 +10,7 @@ namespace BTCPayServer.Views.Wallets
|
||||
Send,
|
||||
Transactions,
|
||||
Rescan,
|
||||
PSBT
|
||||
PSBT,
|
||||
Settings
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
@inject SignInManager<ApplicationUser> SignInManager
|
||||
|
||||
<div class="nav flex-column nav-pills">
|
||||
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Transactions)" asp-action="WalletTransactions">Transactions</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Send)" asp-action="WalletSend">Send</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Transactions)" asp-action="WalletTransactions" id="WalletTransactions">Transactions</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Send)" asp-action="WalletSend" id="WalletSend">Send</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Rescan)" asp-action="WalletRescan">Rescan</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.PSBT)" asp-action="WalletPSBT">PSBT</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Settings)" asp-action="WalletSettings" id="WalletSettings">Settings</a>
|
||||
</div>
|
||||
|
||||
|
@ -30,11 +30,7 @@
|
||||
maxDate: max,
|
||||
defaultDate: defaultDate,
|
||||
time_24hr: true,
|
||||
defaultHour: 0,
|
||||
parseDate: function (date) {
|
||||
// check with Kukks if this is still needed with new date format
|
||||
return moment(date).toDate();
|
||||
}
|
||||
defaultHour: 0
|
||||
});
|
||||
}
|
||||
});
|
||||
|
Reference in New Issue
Block a user