Compare commits

..

1 Commits

Author SHA1 Message Date
334abdc9d1 bump 2019-02-27 20:31:00 +09:00
313 changed files with 4924 additions and 21585 deletions

View File

@ -16,11 +16,7 @@ jobs:
cd BTCPayServer.Tests
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"
fi
docker-compose run -e TESTS_RUN_EXTERNAL_INTEGRATION=$TESTS_RUN_EXTERNAL_INTEGRATION tests
docker-compose run tests
# publish jobs require $DOCKERHUB_REPO, $DOCKERHUB_USER, $DOCKERHUB_PASS defined
publish_docker_linuxamd64:

View File

@ -1,34 +0,0 @@
---
name: Report a problem
about: File a technical problem or report a bug
---
**Describe the problem/bug**
A clear and concise description of what the bug is.
**Your environment**
* Version of BTCPay Server:
* Deployment method:
* Other relevant environment details:
**Logs (if applicable)**
Basic logs can be found in Server Settings > Logs.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Actual behavior**
Tell us what happens instead
**Screenshots/Links**
If applicable, add screenshots or links to help explain your problem.
**Additional context**
Add any other context about the problem here.

View File

@ -1,20 +0,0 @@
---
name: Feature request
about: Ideas and feature requests
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Provide examples**
If applicable provide examples, wireframes, sketches or images to better explain your idea.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -10,7 +10,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>
@ -33,5 +33,4 @@
<ItemGroup>
<ProjectReference Include="..\BTCPayServer\BTCPayServer.csproj" />
</ItemGroup>
</Project>

View File

@ -35,7 +35,6 @@ using System.Text;
using System.Threading;
using Xunit;
using BTCPayServer.Services;
using System.Net.Http;
namespace BTCPayServer.Tests
{
@ -110,6 +109,7 @@ namespace BTCPayServer.Tests
config.AppendLine($"ltc.explorer.url={LTCNBXplorerUri.AbsoluteUri}");
config.AppendLine($"ltc.explorer.cookiefile=0");
config.AppendLine($"btc.lightning={IntegratedLightning.AbsoluteUri}");
if (TestDatabase == TestDatabases.MySQL && !String.IsNullOrEmpty(MySQL))
@ -120,18 +120,15 @@ namespace BTCPayServer.Tests
File.WriteAllText(confPath, config.ToString());
ServerUri = new Uri("http://" + HostName + ":" + Port + "/");
HttpClient = new HttpClient();
HttpClient.BaseAddress = ServerUri;
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development");
var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory, "--conf", confPath, "--disable-registration", "false" });
_Host = new WebHostBuilder()
.UseConfiguration(conf)
.UseContentRoot(FindBTCPayServerDirectory())
.ConfigureServices(s =>
{
s.AddLogging(l =>
{
l.AddFilter("System.Net.Http.HttpClient", LogLevel.Critical);
l.SetMinimumLevel(LogLevel.Information)
.AddFilter("Microsoft", LogLevel.Error)
.AddFilter("Hangfire", LogLevel.Error)
@ -144,7 +141,6 @@ namespace BTCPayServer.Tests
_Host.Start();
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())
{
@ -212,14 +208,6 @@ namespace BTCPayServer.Tests
}
}
private string FindBTCPayServerDirectory()
{
var solutionDirectory = LanguageService.TryGetSolutionDirectoryInfo(Directory.GetCurrentDirectory());
return Path.Combine(solutionDirectory.FullName, "BTCPayServer");
}
public HttpClient HttpClient { get; set; }
public string HostName
{
get;
@ -227,7 +215,6 @@ namespace BTCPayServer.Tests
}
public InvoiceRepository InvoiceRepository { get; private set; }
public StoreRepository StoreRepository { get; private set; }
public BTCPayNetworkProvider Networks { get; private set; }
public Uri IntegratedLightning { get; internal set; }
public bool InContainer { get; internal set; }

View File

@ -218,7 +218,7 @@ namespace BTCPayServer.Tests
tester.NetworkProvider, fetcher);
changellyController.IsTest = true;
Assert.IsType<decimal>(Assert
.IsType<OkObjectResult>(await changellyController.CalculateAmount(user.StoreId, "ltc", "btc", 1.0m, default))
.IsType<OkObjectResult>(await changellyController.CalculateAmount(user.StoreId, "ltc", "btc", 1.0m))
.Value);
}
}

View File

@ -109,7 +109,7 @@ namespace BTCPayServer.Tests
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
{
Amount = new decimal(0.01)
}, default));
}));
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(appId, string.Empty));
@ -118,7 +118,7 @@ namespace BTCPayServer.Tests
{
RedirectToCheckout = false,
Amount = new decimal(0.01)
}, default));
}));
Assert.IsType<ViewResult>(await publicApps.ViewCrowdfund(appId, string.Empty));
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(appId, string.Empty));
@ -130,7 +130,7 @@ namespace BTCPayServer.Tests
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
{
Amount = new decimal(0.01)
}, default));
}));
//Scenario 4: Enabled But End Date < Now - Not Allowed
@ -142,7 +142,7 @@ namespace BTCPayServer.Tests
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
{
Amount = new decimal(0.01)
}, default));
}));
//Scenario 5: Enabled and within correct timeframe, however target is enforced and Amount is Over - Not Allowed
@ -156,13 +156,13 @@ namespace BTCPayServer.Tests
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
{
Amount = new decimal(1.01)
}, default));
}));
//Scenario 6: Allowed
Assert.IsType<OkObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
{
Amount = new decimal(0.05)
}, default));
}));
}
}

View File

@ -1,9 +1,12 @@
FROM mcr.microsoft.com/dotnet/core/sdk:2.1.505-alpine3.7 AS builder
FROM microsoft/dotnet:2.1.500-sdk-alpine3.7 AS builder
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT false
RUN apk add --no-cache icu-libs
ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8
# This should be removed soon https://github.com/dotnet/corefx/issues/30003
RUN apk add --no-cache curl
WORKDIR /source
COPY BTCPayServer/BTCPayServer.csproj BTCPayServer/BTCPayServer.csproj
COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj

View File

@ -1,5 +1,4 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
@ -36,29 +35,27 @@ namespace BTCPayServer.Tests
}
}
public async Task Advance(TimeSpan time)
public void Advance(TimeSpan time)
{
_Now += time;
List<WaitObj> overdue = new List<WaitObj>();
lock (waits)
{
foreach (var wait in waits.ToArray())
{
if (_Now >= wait.Expiration)
{
overdue.Add(wait);
wait.CTS.TrySetResult(true);
waits.Remove(wait);
}
}
}
foreach (var o in overdue)
o.CTS.TrySetResult(true);
try
{
await Task.WhenAll(overdue.Select(o => o.CTS.Task).ToArray());
}
catch { }
}
public void AdvanceMilliseconds(long milli)
{
Advance(TimeSpan.FromMilliseconds(milli));
}
public override string ToString()
{
return _Now.Millisecond.ToString(CultureInfo.InvariantCulture);

View File

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using BTCPayServer.Services.Rates;
@ -11,7 +10,7 @@ namespace BTCPayServer.Tests.Mocks
public class MockRateProvider : IRateProvider
{
public ExchangeRates ExchangeRates { get; set; } = new ExchangeRates();
public Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
public Task<ExchangeRates> GetRatesAsync()
{
return Task.FromResult(ExchangeRates);
}

View File

@ -92,7 +92,7 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = 60 * 2 * 1000)]
[Fact]
[Trait("Integration", "Integration")]
public async Task CanPayPaymentRequestWhenPossible()
{
@ -153,64 +153,5 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
public async Task CanCancelPaymentWhenPossible()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var paymentRequestController = user.GetController<PaymentRequestController>();
Assert.IsType<NotFoundResult>(await
paymentRequestController.CancelUnpaidPendingInvoice(Guid.NewGuid().ToString(), false));
var request = new UpdatePaymentRequestViewModel()
{
Title = "original juice",
Currency = "BTC",
Amount = 1,
StoreId = user.StoreId,
Description = "description"
};
var response = Assert
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result)
.RouteValues.First();
var paymentRequestId = response.Value.ToString();
var invoiceId = Assert
.IsType<OkObjectResult>(await paymentRequestController.PayPaymentRequest(paymentRequestId, false)).Value
.ToString();
var actionResult = Assert
.IsType<RedirectToActionResult>(await paymentRequestController.PayPaymentRequest(response.Value.ToString()));
Assert.Equal("Checkout", actionResult.ActionName);
Assert.Equal("Invoice", actionResult.ControllerName);
Assert.Contains(actionResult.RouteValues, pair => pair.Key == "Id" && pair.Value.ToString() == invoiceId);
var invoice = user.BitPay.GetInvoice(invoiceId, Facade.Merchant);
Assert.Equal(InvoiceState.ToString(InvoiceStatus.New), invoice.Status);
Assert.IsType<OkObjectResult>(await
paymentRequestController.CancelUnpaidPendingInvoice(paymentRequestId, false));
invoice = user.BitPay.GetInvoice(invoiceId, Facade.Merchant);
Assert.Equal(InvoiceState.ToString(InvoiceStatus.Invalid), invoice.Status);
Assert.IsType<BadRequestObjectResult>(await
paymentRequestController.CancelUnpaidPendingInvoice(paymentRequestId, false));
}
}
}
}

View File

@ -41,13 +41,8 @@ You can call bitcoin-cli inside the container with `docker exec`, for example, i
```
If you are using Powershell:
```powershell
.\docker-bitcoin-cli.ps1 sendtoaddress "mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf" 0.23111090
```
You can also generate blocks:
```powershell
.\docker-bitcoin-generate.ps1 3
.\docker-bitcoin-cli.ps1 sendtoaddress "mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf" 0.23111090
```
### Using the test litecoin-cli

View File

@ -23,7 +23,6 @@ using BTCPayServer.Payments.Lightning;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Lightning;
using BTCPayServer.Services;
using BTCPayServer.Tests.Logging;
namespace BTCPayServer.Tests
{
@ -45,7 +44,6 @@ namespace BTCPayServer.Tests
NetworkProvider = new BTCPayNetworkProvider(NetworkType.Regtest);
ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_BTCRPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("BTC").NBitcoinNetwork);
ExplorerNode.ScanRPCCapabilities();
LTCExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_LTCRPCCONNECTION", "server=http://127.0.0.1:43783;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("LTC").NBitcoinNetwork);
ExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("BTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_BTCNBXPLORERURL", "http://127.0.0.1:32838/")));
@ -87,11 +85,9 @@ namespace BTCPayServer.Tests
/// Connect a customer LN node to the merchant LN node
/// </summary>
/// <returns></returns>
public async Task EnsureChannelsSetup()
public Task EnsureChannelsSetup()
{
Logs.Tester.LogInformation("Connecting channels");
await BTCPayServer.Lightning.Tests.ConnectChannels.ConnectAll(ExplorerNode, GetLightningSenderClients(), GetLightningDestClients()).ConfigureAwait(false);
Logs.Tester.LogInformation("Channels connected");
return BTCPayServer.Lightning.Tests.ConnectChannels.ConnectAll(ExplorerNode, GetLightningSenderClients(), GetLightningDestClients());
}
private IEnumerable<ILightningClient> GetLightningSenderClients()

View File

@ -1,237 +0,0 @@
using System;
using System.IO;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Models;
using BTCPayServer.Models.ServerViewModels;
using BTCPayServer.Storage.Models;
using BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration;
using BTCPayServer.Storage.Services.Providers.FileSystemStorage.Configuration;
using BTCPayServer.Storage.ViewModels;
using BTCPayServer.Tests.Logging;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Resources;
using Xunit;
using Xunit.Abstractions;
namespace BTCPayServer.Tests
{
public class StorageTests
{
public StorageTests(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact]
[Trait("Integration", "Integration")]
public async void CanConfigureStorage()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var controller = tester.PayTester.GetController<ServerController>(user.UserId, user.StoreId);
// //For some reason, the tests cache something on circleci and this is set by default
// //Initially, there is no configuration, make sure we display the choices available to configure
// Assert.IsType<StorageSettings>(Assert.IsType<ViewResult>(await controller.Storage()).Model);
//
// //the file list should tell us it's not configured:
// var viewFilesViewModelInitial =
// Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files()).Model);
// Assert.False(viewFilesViewModelInitial.StorageConfigured);
//Once we select a provider, redirect to its view
var localResult = Assert
.IsType<RedirectToActionResult>(controller.Storage(new StorageSettings()
{
Provider = StorageProvider.FileSystem
}));
Assert.Equal(nameof(ServerController.StorageProvider), localResult.ActionName);
Assert.Equal(StorageProvider.FileSystem.ToString(), localResult.RouteValues["provider"]);
var AmazonS3result = Assert
.IsType<RedirectToActionResult>(controller.Storage(new StorageSettings()
{
Provider = StorageProvider.AmazonS3
}));
Assert.Equal(nameof(ServerController.StorageProvider), AmazonS3result.ActionName);
Assert.Equal(StorageProvider.AmazonS3.ToString(), AmazonS3result.RouteValues["provider"]);
var GoogleResult = Assert
.IsType<RedirectToActionResult>(controller.Storage(new StorageSettings()
{
Provider = StorageProvider.GoogleCloudStorage
}));
Assert.Equal(nameof(ServerController.StorageProvider), GoogleResult.ActionName);
Assert.Equal(StorageProvider.GoogleCloudStorage.ToString(), GoogleResult.RouteValues["provider"]);
var AzureResult = Assert
.IsType<RedirectToActionResult>(controller.Storage(new StorageSettings()
{
Provider = StorageProvider.AzureBlobStorage
}));
Assert.Equal(nameof(ServerController.StorageProvider), AzureResult.ActionName);
Assert.Equal(StorageProvider.AzureBlobStorage.ToString(), AzureResult.RouteValues["provider"]);
//Cool, we get redirected to the config pages
//Let's configure this stuff
//Let's try and cheat and go to an invalid storage provider config
Assert.Equal(nameof(Storage), (Assert
.IsType<RedirectToActionResult>(await controller.StorageProvider("I am not a real provider"))
.ActionName));
//ok no more messing around, let's configure this shit.
var fileSystemStorageConfiguration = Assert.IsType<FileSystemStorageConfiguration>(Assert
.IsType<ViewResult>(await controller.StorageProvider(StorageProvider.FileSystem.ToString()))
.Model);
//local file system does not need config, easy days!
Assert.IsType<ViewResult>(
await controller.EditFileSystemStorageProvider(fileSystemStorageConfiguration));
//ok cool, let's see if this got set right
var shouldBeRedirectingToLocalStorageConfigPage =
Assert.IsType<RedirectToActionResult>(await controller.Storage());
Assert.Equal(nameof(StorageProvider), shouldBeRedirectingToLocalStorageConfigPage.ActionName);
Assert.Equal(StorageProvider.FileSystem,
shouldBeRedirectingToLocalStorageConfigPage.RouteValues["provider"]);
//if we tell the settings page to force, it should allow us to select a new provider
Assert.IsType<ChooseStorageViewModel>(Assert.IsType<ViewResult>(await controller.Storage(true)).Model);
//awesome, now let's see if the files result says we're all set up
var viewFilesViewModel =
Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files()).Model);
Assert.True(viewFilesViewModel.StorageConfigured);
Assert.Empty(viewFilesViewModel.Files);
}
}
[Fact]
[Trait("Integration", "Integration")]
public async void CanUseLocalProviderFiles()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var controller = tester.PayTester.GetController<ServerController>(user.UserId, user.StoreId);
var fileSystemStorageConfiguration = Assert.IsType<FileSystemStorageConfiguration>(Assert
.IsType<ViewResult>(await controller.StorageProvider(StorageProvider.FileSystem.ToString()))
.Model);
Assert.IsType<ViewResult>(
await controller.EditFileSystemStorageProvider(fileSystemStorageConfiguration));
var shouldBeRedirectingToLocalStorageConfigPage =
Assert.IsType<RedirectToActionResult>(await controller.Storage());
Assert.Equal(nameof(StorageProvider), shouldBeRedirectingToLocalStorageConfigPage.ActionName);
Assert.Equal(StorageProvider.FileSystem,
shouldBeRedirectingToLocalStorageConfigPage.RouteValues["provider"]);
await CanUploadRemoveFiles(controller);
}
}
[Fact]
[Trait("ExternalIntegration", "ExternalIntegration")]
public async Task CanUseAzureBlobStorage()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var controller = tester.PayTester.GetController<ServerController>(user.UserId, user.StoreId);
var azureBlobStorageConfiguration = Assert.IsType<AzureBlobStorageConfiguration>(Assert
.IsType<ViewResult>(await controller.StorageProvider(StorageProvider.AzureBlobStorage.ToString()))
.Model);
azureBlobStorageConfiguration.ConnectionString = GetFromSecrets("AzureBlobStorageConnectionString");
azureBlobStorageConfiguration.ContainerName = "testscontainer";
Assert.IsType<ViewResult>(
await controller.EditAzureBlobStorageStorageProvider(azureBlobStorageConfiguration));
var shouldBeRedirectingToAzureStorageConfigPage =
Assert.IsType<RedirectToActionResult>(await controller.Storage());
Assert.Equal(nameof(StorageProvider), shouldBeRedirectingToAzureStorageConfigPage.ActionName);
Assert.Equal(StorageProvider.AzureBlobStorage,
shouldBeRedirectingToAzureStorageConfigPage.RouteValues["provider"]);
//seems like azure config worked, let's see if the conn string was actually saved
Assert.Equal(azureBlobStorageConfiguration.ConnectionString, Assert
.IsType<AzureBlobStorageConfiguration>(Assert
.IsType<ViewResult>(
await controller.StorageProvider(StorageProvider.AzureBlobStorage.ToString()))
.Model).ConnectionString);
await CanUploadRemoveFiles(controller);
}
}
private async Task CanUploadRemoveFiles(ServerController controller)
{
var fileContent = "content";
var uploadFormFileResult = Assert.IsType<RedirectToActionResult>(await controller.CreateFile(TestUtils.GetFormFile("uploadtestfile.txt", fileContent)));
Assert.True(uploadFormFileResult.RouteValues.ContainsKey("fileId"));
var fileId = uploadFormFileResult.RouteValues["fileId"].ToString();
Assert.Equal("Files", uploadFormFileResult.ActionName);
var viewFilesViewModel =
Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files(fileId)).Model);
Assert.NotEmpty(viewFilesViewModel.Files);
Assert.Equal(fileId, viewFilesViewModel.SelectedFileId);
Assert.NotEmpty(viewFilesViewModel.DirectFileUrl);
var net = new System.Net.WebClient();
var data = await net.DownloadStringTaskAsync(new Uri(viewFilesViewModel.DirectFileUrl));
Assert.Equal(fileContent, data);
Assert.Equal(StatusMessageModel.StatusSeverity.Success, new StatusMessageModel(Assert
.IsType<RedirectToActionResult>(await controller.DeleteFile(fileId))
.RouteValues["statusMessage"].ToString()).Severity);
viewFilesViewModel =
Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files(fileId)).Model);
Assert.Null(viewFilesViewModel.DirectFileUrl);
Assert.Null(viewFilesViewModel.SelectedFileId);
}
private static string GetFromSecrets(string key)
{
var connStr = Environment.GetEnvironmentVariable($"TESTS_{key}");
if (!string.IsNullOrEmpty(connStr) && connStr != "none")
return connStr;
var builder = new ConfigurationBuilder();
builder.AddUserSecrets("AB0AC1DD-9D26-485B-9416-56A33F268117");
var config = builder.Build();
var token = config[key];
Assert.False(token == null, $"{key} is not set.\n Run \"dotnet user-secrets set {key} <value>\"");
return token;
}
}
}

View File

@ -113,18 +113,15 @@ namespace BTCPayServer.Tests
private async Task RegisterAsync()
{
var account = parent.PayTester.GetController<AccountController>();
RegisterDetails = new RegisterViewModel()
await account.Register(new RegisterViewModel()
{
Email = Guid.NewGuid() + "@toto.com",
ConfirmPassword = "Kitten0@",
Password = "Kitten0@",
};
await account.Register(RegisterDetails);
});
UserId = account.RegisteredUserId;
}
public RegisterViewModel RegisterDetails{ get; set; }
public Bitpay BitPay
{
get; set;

View File

@ -1,65 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Internal;
using Xunit.Sdk;
namespace BTCPayServer.Tests
{
public static class TestUtils
{
public static FormFile GetFormFile(string filename, string content)
{
File.WriteAllText(filename, content);
var fileInfo = new FileInfo(filename);
FormFile formFile = new FormFile(
new FileStream(filename, FileMode.OpenOrCreate),
0,
fileInfo.Length, fileInfo.Name, fileInfo.Name)
{
Headers = new HeaderDictionary()
};
formFile.ContentType = "text/plain";
formFile.ContentDisposition = $"form-data; name=\"file\"; filename=\"{fileInfo.Name}\"";
return formFile;
}
public static void Eventually(Action act)
{
CancellationTokenSource cts = new CancellationTokenSource(20000);
while (true)
{
try
{
act();
break;
}
catch (XunitException) when (!cts.Token.IsCancellationRequested)
{
cts.Token.WaitHandle.WaitOne(500);
}
}
}
public static async Task EventuallyAsync(Func<Task> act)
{
CancellationTokenSource cts = new CancellationTokenSource(20000);
while (true)
{
try
{
await act();
break;
}
catch (XunitException) when (!cts.Token.IsCancellationRequested)
{
await Task.Delay(500);
}
}
}
}
}

View File

@ -52,15 +52,6 @@ using NBitpayClient.Extensions;
using BTCPayServer.Services;
using System.Text.RegularExpressions;
using BTCPayServer.Events;
using BTCPayServer.Configuration;
using System.Security;
using System.Runtime.CompilerServices;
using System.Net;
using BTCPayServer.Models.AccountViewModels;
using BTCPayServer.Services.U2F.Models;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Http;
using NBXplorer.DerivationStrategy;
namespace BTCPayServer.Tests
{
@ -152,45 +143,6 @@ namespace BTCPayServer.Tests
#pragma warning restore CS0618
}
[Fact]
[Trait("Fast", "Fast")]
public void CanParseTorrc()
{
var nl = "\n";
var input = "# For the hidden service BTCPayServer" + nl +
"HiddenServiceDir /var/lib/tor/hidden_services/BTCPayServer" + nl +
"# Redirecting to nginx" + nl +
"HiddenServicePort 80 172.19.0.10:81";
nl = Environment.NewLine;
var expected = "HiddenServiceDir /var/lib/tor/hidden_services/BTCPayServer" + nl +
"HiddenServicePort 80 172.19.0.10:81" + nl;
Assert.True(Torrc.TryParse(input, out var torrc));
Assert.Equal(expected, torrc.ToString());
nl = "\r\n";
input = "# For the hidden service BTCPayServer" + nl +
"HiddenServiceDir /var/lib/tor/hidden_services/BTCPayServer" + nl +
"# Redirecting to nginx" + nl +
"HiddenServicePort 80 172.19.0.10:81";
Assert.True(Torrc.TryParse(input, out torrc));
Assert.Equal(expected, torrc.ToString());
input = "# For the hidden service BTCPayServer" + nl +
"HiddenServiceDir /var/lib/tor/hidden_services/BTCPayServer" + nl +
"# Redirecting to nginx" + nl +
"HiddenServicePort 80 172.19.0.10:80" + nl +
"HiddenServiceDir /var/lib/tor/hidden_services/Woocommerce" + nl +
"# Redirecting to nginx" + nl +
"HiddenServicePort 80 172.19.0.11:80";
nl = Environment.NewLine;
expected = "HiddenServiceDir /var/lib/tor/hidden_services/BTCPayServer" + nl +
"HiddenServicePort 80 172.19.0.10:80" + nl +
"HiddenServiceDir /var/lib/tor/hidden_services/Woocommerce" + nl +
"HiddenServicePort 80 172.19.0.11:80" + nl;
Assert.True(Torrc.TryParse(input, out torrc));
Assert.Equal(expected, torrc.ToString());
}
[Fact]
[Trait("Fast", "Fast")]
public void CanCalculateCryptoDue()
@ -406,7 +358,7 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = 60 * 2 * 1000)]
[Fact(Timeout = 60 * 1000)]
[Trait("Integration", "Integration")]
public async Task CanSetLightningServer()
{
@ -444,21 +396,21 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = 60 * 2 * 1000)]
[Fact]
[Trait("Integration", "Integration")]
public async Task CanSendLightningPaymentCLightning()
{
await ProcessLightningPayment(LightningConnectionType.CLightning);
}
[Fact(Timeout = 60 * 2 * 1000)]
[Fact]
[Trait("Integration", "Integration")]
public async Task CanSendLightningPaymentCharge()
{
await ProcessLightningPayment(LightningConnectionType.Charge);
}
[Fact(Timeout = 60 * 2 * 1000)]
[Fact(Timeout = 60 * 1000)]
[Trait("Integration", "Integration")]
public async Task CanSendLightningPaymentLnd()
{
@ -498,9 +450,7 @@ namespace BTCPayServer.Tests
ItemDesc = "Some description"
});
await Task.Delay(TimeSpan.FromMilliseconds(1000)); // Give time to listen the new invoices
Logs.Tester.LogInformation($"Trying to send Lightning payment to {invoice.Id}");
await tester.SendLightningPaymentAsync(invoice);
Logs.Tester.LogInformation($"Lightning payment to {invoice.Id} is sent");
await TestUtils.EventuallyAsync(async () =>
{
var localInvoice = await user.BitPay.GetInvoiceAsync(invoice.Id);
@ -523,6 +473,7 @@ namespace BTCPayServer.Tests
var controller = acc.GetController<StoresController>();
var token = (RedirectToActionResult)controller.CreateToken(new Models.StoreViewModels.CreateTokenViewModel()
{
Facade = Facade.Merchant.ToString(),
Label = "bla",
PublicKey = null
}).GetAwaiter().GetResult();
@ -635,7 +586,7 @@ namespace BTCPayServer.Tests
Assert.True(RateRules.TryParse("X_X=kraken(X_BTC) * kraken(BTC_X)", out var rule));
foreach (var pair in new[] { "DOGE_USD", "DOGE_CAD", "DASH_CAD", "DASH_USD", "DASH_EUR" })
{
var result = fetcher.FetchRate(CurrencyPair.Parse(pair), rule, default).GetAwaiter().GetResult();
var result = fetcher.FetchRate(CurrencyPair.Parse(pair), rule).GetAwaiter().GetResult();
Assert.NotNull(result.BidAsk);
Assert.Empty(result.Errors);
}
@ -659,6 +610,7 @@ namespace BTCPayServer.Tests
var rescan = Assert.IsType<RescanWalletModel>(Assert.IsType<ViewResult>(walletController.WalletRescan(walletId).Result).Model);
Assert.False(rescan.Ok);
Assert.True(rescan.IsFullySync);
Assert.True(rescan.IsSegwit);
Assert.False(rescan.IsSupportedByCurrency);
Assert.False(rescan.IsServerAdmin);
@ -747,12 +699,6 @@ namespace BTCPayServer.Tests
AssertSearchInvoice(acc, false, invoice.Id, $"exceptionstatus:paidOver");
AssertSearchInvoice(acc, true, invoice.Id, $"unusual:true");
AssertSearchInvoice(acc, false, invoice.Id, $"unusual:false");
var time = invoice.InvoiceTime;
AssertSearchInvoice(acc, true, invoice.Id, $"startdate:{time.ToString("yyyy-MM-dd HH:mm:ss")}");
AssertSearchInvoice(acc, true, invoice.Id, $"enddate:{time.ToStringLowerInvariant()}");
AssertSearchInvoice(acc, false, invoice.Id, $"startdate:{time.AddSeconds(1).ToString("yyyy-MM-dd HH:mm:ss")}");
AssertSearchInvoice(acc, false, invoice.Id, $"enddate:{time.AddSeconds(-1).ToString("yyyy-MM-dd HH:mm:ss")}");
}
}
@ -769,32 +715,20 @@ namespace BTCPayServer.Tests
acc.RegisterDerivationScheme("LTC");
var rateController = acc.GetController<RateController>();
var GetBaseCurrencyRatesResult = JObject.Parse(((JsonResult)rateController.GetBaseCurrencyRates("BTC", acc.StoreId, default)
var GetBaseCurrencyRatesResult = JObject.Parse(((JsonResult)rateController.GetBaseCurrencyRates("BTC", acc.StoreId)
.GetAwaiter().GetResult()).Value.ToJson()).ToObject<DataWrapper<Rate[]>>();
Assert.NotNull(GetBaseCurrencyRatesResult);
Assert.NotNull(GetBaseCurrencyRatesResult.Data);
Assert.Equal(2, GetBaseCurrencyRatesResult.Data.Length);
Assert.Single(GetBaseCurrencyRatesResult.Data.Where(o => o.Code == "LTC"));
var GetRatesResult = JObject.Parse(((JsonResult)rateController.GetRates(null, acc.StoreId, default)
var GetRatesResult = JObject.Parse(((JsonResult)rateController.GetRates(null, acc.StoreId)
.GetAwaiter().GetResult()).Value.ToJson()).ToObject<DataWrapper<Rate[]>>();
// We don't have any default currencies, so this should be failing
Assert.Null(GetRatesResult?.Data);
var store = acc.GetController<StoresController>();
var ratesVM = (RatesViewModel)(Assert.IsType<ViewResult>(store.Rates(acc.StoreId)).Model);
ratesVM.DefaultCurrencyPairs = "BTC_USD,LTC_USD";
store.Rates(ratesVM).Wait();
store = acc.GetController<StoresController>();
rateController = acc.GetController<RateController>();
GetRatesResult = JObject.Parse(((JsonResult)rateController.GetRates(null, acc.StoreId, default)
.GetAwaiter().GetResult()).Value.ToJson()).ToObject<DataWrapper<Rate[]>>();
// Now we should have a result
Assert.NotNull(GetRatesResult);
Assert.NotNull(GetRatesResult.Data);
Assert.Equal(2, GetRatesResult.Data.Length);
var GetCurrencyPairRateResult = JObject.Parse(((JsonResult)rateController.GetCurrencyPairRate("BTC", "LTC", acc.StoreId, default)
var GetCurrencyPairRateResult = JObject.Parse(((JsonResult)rateController.GetCurrencyPairRate("BTC", "LTC", acc.StoreId)
.GetAwaiter().GetResult()).Value.ToJson()).ToObject<DataWrapper<Rate>>();
Assert.NotNull(GetCurrencyPairRateResult);
@ -890,28 +824,22 @@ namespace BTCPayServer.Tests
[Trait("Fast", "Fast")]
public void CanParseFilter()
{
var filter = "storeid:abc, status:abed, blabhbalh ";
var filter = "storeid:abc status:abed blabhbalh ";
var search = new SearchString(filter);
Assert.Equal("storeid:abc, status:abed, blabhbalh", search.ToString());
Assert.Equal("storeid:abc status:abed blabhbalh", search.ToString());
Assert.Equal("blabhbalh", search.TextSearch);
Assert.Single(search.Filters["storeid"]);
Assert.Single(search.Filters["status"]);
Assert.Equal("abc", search.Filters["storeid"].First());
Assert.Equal("abed", search.Filters["status"].First());
filter = "status:abed, status:abed2";
filter = "status:abed status:abed2";
search = new SearchString(filter);
Assert.Equal("", search.TextSearch);
Assert.Equal("status:abed, status:abed2", search.ToString());
Assert.Equal("status:abed status:abed2", search.ToString());
Assert.Throws<KeyNotFoundException>(() => search.Filters["test"]);
Assert.Equal(2, search.Filters["status"].Count);
Assert.Equal("abed", search.Filters["status"].First());
Assert.Equal("abed2", search.Filters["status"].Skip(1).First());
filter = "StartDate:2019-04-25 01:00 AM, hekki";
search = new SearchString(filter);
Assert.Equal("2019-04-25 01:00 AM", search.Filters["startdate"].First());
Assert.Equal("hekki", search.TextSearch);
}
[Fact]
@ -938,7 +866,7 @@ namespace BTCPayServer.Tests
using (var tester = ServerTester.Create())
{
tester.Start();
foreach (var req in new[]
foreach(var req in new[]
{
"invoices/",
"invoices",
@ -987,6 +915,7 @@ namespace BTCPayServer.Tests
var storeController = user.GetController<StoresController>();
storeController.CreateToken(new CreateTokenViewModel()
{
Facade = Facade.Merchant.ToString(),
Label = "test2",
StoreId = user.StoreId
}).GetAwaiter().GetResult();
@ -1074,7 +1003,7 @@ namespace BTCPayServer.Tests
private static decimal CreateInvoice(ServerTester tester, TestAccount user, string exchange, string currency = "USD")
{
var storeController = user.GetController<StoresController>();
var vm = (RatesViewModel)((ViewResult)storeController.Rates(user.StoreId)).Model;
var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model;
vm.PreferredExchange = exchange;
storeController.Rates(vm).Wait();
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
@ -1089,49 +1018,6 @@ namespace BTCPayServer.Tests
return invoice2.CryptoInfo[0].Rate;
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanUseAnyoneCanCreateInvoice()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
Logs.Tester.LogInformation("StoreId without anyone can create invoice = 401");
var response = await tester.PayTester.HttpClient.SendAsync(new HttpRequestMessage(HttpMethod.Post, $"invoices?storeId={user.StoreId}")
{
Content = new StringContent("{\"Price\": 5000, \"currency\": \"USD\"}", Encoding.UTF8, "application/json"),
});
Assert.Equal(401, (int)response.StatusCode);
Logs.Tester.LogInformation("No store without anyone can create invoice = 404 because the bitpay API can't know the storeid");
response = await tester.PayTester.HttpClient.SendAsync(new HttpRequestMessage(HttpMethod.Post, $"invoices")
{
Content = new StringContent("{\"Price\": 5000, \"currency\": \"USD\"}", Encoding.UTF8, "application/json"),
});
Assert.Equal(404, (int)response.StatusCode);
user.ModifyStore(s => s.AnyoneCanCreateInvoice = true);
Logs.Tester.LogInformation("Bad store with anyone can create invoice = 401");
response = await tester.PayTester.HttpClient.SendAsync(new HttpRequestMessage(HttpMethod.Post, $"invoices?storeId=badid")
{
Content = new StringContent("{\"Price\": 5000, \"currency\": \"USD\"}", Encoding.UTF8, "application/json"),
});
Assert.Equal(401, (int)response.StatusCode);
Logs.Tester.LogInformation("Good store with anyone can create invoice = 200");
response = await tester.PayTester.HttpClient.SendAsync(new HttpRequestMessage(HttpMethod.Post, $"invoices?storeId={user.StoreId}")
{
Content = new StringContent("{\"Price\": 5000, \"currency\": \"USD\"}", Encoding.UTF8, "application/json"),
});
Assert.Equal(200, (int)response.StatusCode);
}
}
[Fact]
[Trait("Integration", "Integration")]
public void CanTweakRate()
@ -1155,7 +1041,7 @@ namespace BTCPayServer.Tests
Assert.Equal(Money.Coins(1.0m), invoice1.BtcPrice);
var storeController = user.GetController<StoresController>();
var vm = (RatesViewModel)((ViewResult)storeController.Rates(user.StoreId)).Model;
var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model;
Assert.Equal(0.0, vm.Spread);
vm.Spread = 40;
storeController.Rates(vm).Wait();
@ -1254,7 +1140,7 @@ namespace BTCPayServer.Tests
user.RegisterDerivationScheme("BTC");
var store = user.GetController<StoresController>();
var rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(user.StoreId)).Model);
var rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
Assert.False(rateVm.ShowScripting);
Assert.Equal("coinaverage", rateVm.PreferredExchange);
Assert.Equal(0.0, rateVm.Spread);
@ -1262,7 +1148,7 @@ namespace BTCPayServer.Tests
rateVm.PreferredExchange = "bitflyer";
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(user.StoreId)).Model);
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
Assert.Equal("bitflyer", rateVm.PreferredExchange);
rateVm.ScriptTest = "BTC_JPY,BTC_CAD";
@ -1279,8 +1165,7 @@ namespace BTCPayServer.Tests
Assert.IsType<RedirectToActionResult>(store.ShowRateRulesPost(true).Result);
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
store = user.GetController<StoresController>();
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(user.StoreId)).Model);
Assert.Equal(rateVm.StoreId, user.StoreId);
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
Assert.Equal(rateVm.DefaultScript, rateVm.Script);
Assert.True(rateVm.ShowScripting);
rateVm.ScriptTest = "BTC_JPY";
@ -1297,7 +1182,7 @@ namespace BTCPayServer.Tests
Assert.True(rateVm.TestRateRules.All(t => !t.Error));
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
store = user.GetController<StoresController>();
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(user.StoreId)).Model);
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
Assert.Equal(50, rateVm.Spread);
Assert.True(rateVm.ShowScripting);
Assert.Contains("DOGE_X", rateVm.Script, StringComparison.OrdinalIgnoreCase);
@ -1460,70 +1345,59 @@ namespace BTCPayServer.Tests
[Trait("Fast", "Fast")]
public void CanParseDerivationScheme()
{
var testnetNetworkProvider = new BTCPayNetworkProvider(NetworkType.Testnet);
var regtestNetworkProvider = new BTCPayNetworkProvider(NetworkType.Regtest);
var mainnetNetworkProvider = new BTCPayNetworkProvider(NetworkType.Mainnet);
var testnetParser = new DerivationSchemeParser(testnetNetworkProvider.GetNetwork("BTC"));
var mainnetParser = new DerivationSchemeParser(mainnetNetworkProvider.GetNetwork("BTC"));
var parser = new DerivationSchemeParser(Network.TestNet);
NBXplorer.DerivationStrategy.DerivationStrategyBase result;
// Passing electrum stuff
// Passing a native segwit from mainnet to a testnet parser, means the testnet parser will try to convert it into segwit
result = testnetParser.Parse("zpub6nL6PUGurpU3DfPDSZaRS6WshpbNc9ctCFFzrCn54cssnheM31SZJZUcFHKtjJJNhAueMbh6ptFMfy1aeiMQJr3RJ4DDt1hAPx7sMTKV48t");
// Native
result = parser.Parse("zpub6nL6PUGurpU3DfPDSZaRS6WshpbNc9ctCFFzrCn54cssnheM31SZJZUcFHKtjJJNhAueMbh6ptFMfy1aeiMQJr3RJ4DDt1hAPx7sMTKV48t");
Assert.Equal("tpubD93CJNkmGjLXnsBqE2zGDqfEh1Q8iJ8wueordy3SeWt1RngbbuxXCsqASuVWFywmfoCwUE1rSfNJbaH4cBNcbp8WcyZgPiiRSTazLGL8U9w", result.ToString());
result = mainnetParser.Parse("zpub6nL6PUGurpU3DfPDSZaRS6WshpbNc9ctCFFzrCn54cssnheM31SZJZUcFHKtjJJNhAueMbh6ptFMfy1aeiMQJr3RJ4DDt1hAPx7sMTKV48t");
Assert.Equal("xpub68fZn8w5ZTP5X4zymr1B1vKsMtJUiudtN2DZHQzJJc87gW1tXh7S4SALCsQijUzXstg2reVyuZYFuPnTDKXNiNgDZNpNiC4BrVzaaGEaRHj", result.ToString());
// P2SH
result = testnetParser.Parse("upub57Wa4MvRPNyAipy1MCpERxcFpHR2ZatyikppkyeWkoRL6QJvLVMo39jYdcaJVxyvBURyRVmErBEA5oGicKBgk1j72GAXSPFH5tUDoGZ8nEu");
result = parser.Parse("ypub6QqdH2c5z79681jUgdxjGJzGW9zpL4ryPCuhtZE4GpvrJoZqM823XQN6iSQeVbbbp2uCRQ9UgpeMcwiyV6qjvxTWVcxDn2XEAnioMUwsrQ5");
Assert.Equal("tpubD6NzVbkrYhZ4YWjDJUACG9E8fJx2NqNY1iynTiPKEjJrzzRKAgha3nNnwGXr2BtvCJKJHW4nmG7rRqc2AGGy2AECgt16seMyV2FZivUmaJg-[p2sh]", result.ToString());
result = mainnetParser.Parse("ypub6QqdH2c5z79681jUgdxjGJzGW9zpL4ryPCuhtZE4GpvrJoZqM823XQN6iSQeVbbbp2uCRQ9UgpeMcwiyV6qjvxTWVcxDn2XEAnioMUwsrQ5");
Assert.Equal("xpub661MyMwAqRbcGiYMrHB74DtmLBrNPSsUU6PV7ALAtpYyFhkc6TrUuLhxhET4VgwgQPnPfvYvEAHojf7QmQRj8imudHFoC7hju4f9xxri8wR-[p2sh]", result.ToString());
// if prefix not recognize, assume it is segwit
result = testnetParser.Parse("xpub661MyMwAqRbcGeVGU5e5KBcau1HHEUGf9Wr7k4FyLa8yRPNQrrVa7Ndrgg8Afbe2UYXMSL6tJBFd2JewwWASsePPLjkcJFL1tTVEs3UQ23X");
Assert.Equal("tpubD6NzVbkrYhZ4YSg7vGdAX6wxE8NwDrmih9SR6cK7gUtsAg37w5LfFpJgviCxC6bGGT4G3uckqH5fiV9ZLN1gm5qgQLVuymzFUR5ed7U7ksu", result.ToString());
result = parser.Parse("xpub661MyMwAqRbcGeVGU5e5KBcau1HHEUGf9Wr7k4FyLa8yRPNQrrVa7Ndrgg8Afbe2UYXMSL6tJBFd2JewwWASsePPLjkcJFL1tTVEs3UQ23X");
Assert.Equal("tpubD6NzVbkrYhZ4YSg7vGdAX6wxE8NwDrmih9SR6cK7gUtsAg37w5LfFpJgviCxC6bGGT4G3uckqH5fiV9ZLN1gm5qgQLVuymzFUR5ed7U7ksu-[legacy]", result.ToString());
////////////////
var tpub = "tpubD6NzVbkrYhZ4Wc65tjhmcKdWFauAo7bGLRTxvggygkNyp6SMGutJp7iociwsinU33jyNBp1J9j2hJH5yQsayfiS3LEU2ZqXodAcnaygra8o";
result = testnetParser.Parse(tpub);
result = parser.Parse(tpub);
Assert.Equal(tpub, result.ToString());
testnetParser.HintScriptPubKey = BitcoinAddress.Create("tb1q4s33amqm8l7a07zdxcunqnn3gcsjcfz3xc573l", testnetParser.Network).ScriptPubKey;
result = testnetParser.Parse(tpub);
parser.HintScriptPubKey = BitcoinAddress.Create("tb1q4s33amqm8l7a07zdxcunqnn3gcsjcfz3xc573l", parser.Network).ScriptPubKey;
result = parser.Parse(tpub);
Assert.Equal(tpub, result.ToString());
testnetParser.HintScriptPubKey = BitcoinAddress.Create("2N2humNio3YTApSfY6VztQ9hQwDnhDvaqFQ", testnetParser.Network).ScriptPubKey;
result = testnetParser.Parse(tpub);
parser.HintScriptPubKey = BitcoinAddress.Create("2N2humNio3YTApSfY6VztQ9hQwDnhDvaqFQ", parser.Network).ScriptPubKey;
result = parser.Parse(tpub);
Assert.Equal($"{tpub}-[p2sh]", result.ToString());
testnetParser.HintScriptPubKey = BitcoinAddress.Create("mwD8bHS65cdgUf6rZUUSoVhi3wNQFu1Nfi", testnetParser.Network).ScriptPubKey;
result = testnetParser.Parse(tpub);
parser.HintScriptPubKey = BitcoinAddress.Create("mwD8bHS65cdgUf6rZUUSoVhi3wNQFu1Nfi", parser.Network).ScriptPubKey;
result = parser.Parse(tpub);
Assert.Equal($"{tpub}-[legacy]", result.ToString());
testnetParser.HintScriptPubKey = BitcoinAddress.Create("2N2humNio3YTApSfY6VztQ9hQwDnhDvaqFQ", testnetParser.Network).ScriptPubKey;
result = testnetParser.Parse($"{tpub}-[legacy]");
parser.HintScriptPubKey = BitcoinAddress.Create("2N2humNio3YTApSfY6VztQ9hQwDnhDvaqFQ", parser.Network).ScriptPubKey;
result = parser.Parse($"{tpub}-[legacy]");
Assert.Equal($"{tpub}-[p2sh]", result.ToString());
result = testnetParser.Parse(tpub);
result = parser.Parse(tpub);
Assert.Equal($"{tpub}-[p2sh]", result.ToString());
var regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork("BTC"));
var parsed = regtestParser.Parse("xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]");
parser = new DerivationSchemeParser(Network.RegTest);
var parsed = parser.Parse("xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]");
Assert.Equal("tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[p2sh]", parsed.ToString());
// Let's make sure we can't generate segwit with dogecoin
regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork("DOGE"));
parsed = regtestParser.Parse("xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]");
parser = new DerivationSchemeParser(NBitcoin.Altcoins.Dogecoin.Instance.Regtest);
parsed = parser.Parse("xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]");
Assert.Equal("tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[legacy]", parsed.ToString());
regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork("DOGE"));
parsed = regtestParser.Parse("tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[p2sh]");
parser = new DerivationSchemeParser(NBitcoin.Altcoins.Dogecoin.Instance.Regtest);
parsed = parser.Parse("tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[p2sh]");
Assert.Equal("tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[legacy]", parsed.ToString());
}
[Fact]
[Trait("Integration", "Integration")]
public void CanAddDerivationSchemes()
public void CanDisablePaymentMethods()
{
using (var tester = ServerTester.Create())
{
@ -1533,7 +1407,7 @@ namespace BTCPayServer.Tests
user.RegisterDerivationScheme("BTC");
user.RegisterDerivationScheme("LTC");
user.RegisterLightningNode("BTC", LightningConnectionType.CLightning);
var btcNetwork = tester.PayTester.Networks.GetNetwork("BTC");
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 1.5m,
@ -1554,18 +1428,16 @@ namespace BTCPayServer.Tests
lightningVM = (LightningNodeViewModel)Assert.IsType<ViewResult>(controller.AddLightningNode(user.StoreId, "BTC")).Model;
Assert.False(lightningVM.Enabled);
// Only Enabling/Disabling the payment method must redirect to store page
var derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
Assert.True(derivationVM.Enabled);
derivationVM.Enabled = false;
Assert.IsType<RedirectToActionResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult());
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
// Confirmation
controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult();
Assert.False(derivationVM.Enabled);
// Clicking next without changing anything should send to the confirmation screen
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
Assert.True(derivationVM.Confirmation);
Assert.False(derivationVM.Enabled);
invoice = user.BitPay.CreateInvoice(new Invoice()
{
@ -1579,82 +1451,10 @@ namespace BTCPayServer.Tests
Assert.Single(invoice.CryptoInfo);
Assert.Equal("LTC", invoice.CryptoInfo[0].CryptoCode);
// Removing the derivation scheme, should redirect to store page
var oldScheme = derivationVM.DerivationScheme;
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
derivationVM.DerivationScheme = null;
Assert.IsType<RedirectToActionResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult());
// Setting it again should redirect to the confirmation page
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
derivationVM.DerivationScheme = oldScheme;
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
Assert.True(derivationVM.Confirmation);
// Can we upload coldcard settings? (Should fail, we are giving a mainnet file to a testnet network)
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
string content = "{\"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}";
derivationVM.ColdcardPublicFile = TestUtils.GetFormFile("wallet.json", content);
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
Assert.False(derivationVM.Confirmation); // Should fail, we are giving a mainnet file to a testnet network
// And with a good file? (upub)
content = "{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"upub5DBYp1qGgsTrkzCptMGZc2x18pquLwGrBw6nS59T4NViZ4cni1mGowQzziy85K8vzkp1jVtWrSkLhqk9KDfvrGeB369wGNYf39kX8rQfiLn\", \"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}";
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
derivationVM.ColdcardPublicFile = TestUtils.GetFormFile("wallet2.json", content);
derivationVM.Enabled = true;
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
Assert.True(derivationVM.Confirmation);
Assert.IsType<RedirectToActionResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult());
// Now let's check that no data has been lost in the process
var store = tester.PayTester.StoreRepository.FindStore(user.StoreId).GetAwaiter().GetResult();
var onchainBTC = store.GetSupportedPaymentMethods(tester.PayTester.Networks).OfType<DerivationSchemeSettings>().First(o => o.PaymentId.IsBTCOnChain);
DerivationSchemeSettings.TryParseFromColdcard(content, onchainBTC.Network, out var expected);
Assert.Equal(expected.ToJson(), onchainBTC.ToJson());
// Let's check that the root hdkey and account key path are taken into account when making a PSBT
invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 1.5m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
tester.ExplorerNode.Generate(1);
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo.First(c => c.CryptoCode == "BTC").Address, tester.ExplorerNode.Network);
tester.ExplorerNode.SendToAddress(invoiceAddress, Money.Coins(1m));
TestUtils.Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal("paid", invoice.Status);
});
var wallet = tester.PayTester.GetController<WalletsController>();
var psbt = wallet.CreatePSBT(btcNetwork, onchainBTC, new WalletSendLedgerModel()
{
Amount = 0.5m,
Destination = new Key().PubKey.GetAddress(btcNetwork.NBitcoinNetwork).ToString(),
FeeSatoshiPerByte = 1
}, default).GetAwaiter().GetResult();
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();
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);
});
}
}
[Fact(Timeout = 60 * 2 * 1000)]
[Fact(Timeout = 60 * 1000)]
[Trait("Integration", "Integration")]
public async Task CanSetPaymentMethodLimits()
{
@ -1756,21 +1556,12 @@ donation:
Assert.Equal(10.00m, orangeInvoice.Price);
Assert.Equal("CAD", orangeInvoice.Currency);
Assert.Equal("orange", orangeInvoice.ItemDesc);
Assert.IsType<RedirectToActionResult>(publicApps.ViewPointOfSale(appId, 0, null, null, null, null, "apple").Result);
invoices = user.BitPay.GetInvoices();
var appleInvoice = invoices.SingleOrDefault(invoice => invoice.ItemCode.Equals("apple"));
Assert.NotNull(appleInvoice);
Assert.Equal("good apple", appleInvoice.ItemDesc);
// testing custom amount
var action = Assert.IsType<RedirectToActionResult>(publicApps.ViewPointOfSale(appId, 6.6m, null, null, null, null, "donation").Result);
var action = Assert.IsType<RedirectToActionResult>(publicApps.ViewPointOfSale(appId, 5, null, null, null, null, "donation").Result);
Assert.Equal(nameof(InvoiceController.Checkout), action.ActionName);
invoices = user.BitPay.GetInvoices();
var donationInvoice = invoices.Single(i => i.Price == 6.6m);
var donationInvoice = invoices.Single(i => i.Price == 5m);
Assert.NotNull(donationInvoice);
Assert.Equal("CAD", donationInvoice.Currency);
Assert.Equal("donation", donationInvoice.ItemDesc);
@ -1818,20 +1609,21 @@ donation:
[Fact]
[Trait("Fast", "Fast")]
public async Task CanScheduleBackgroundTasks()
public void CanScheduleBackgroundTasks()
{
BackgroundJobClient client = new BackgroundJobClient();
MockDelay mockDelay = new MockDelay();
client.Delay = mockDelay;
bool[] jobs = new bool[4];
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
Logs.Tester.LogInformation("Start Job[0] in 5 sec");
client.Schedule((_) => { Logs.Tester.LogInformation("Job[0]"); jobs[0] = true; return Task.CompletedTask; }, TimeSpan.FromSeconds(5.0));
client.Schedule(async () => { Logs.Tester.LogInformation("Job[0]"); jobs[0] = true; }, TimeSpan.FromSeconds(5.0));
Logs.Tester.LogInformation("Start Job[1] in 2 sec");
client.Schedule((_) => { Logs.Tester.LogInformation("Job[1]"); jobs[1] = true; return Task.CompletedTask; }, TimeSpan.FromSeconds(2.0));
client.Schedule(async () => { Logs.Tester.LogInformation("Job[1]"); jobs[1] = true; }, TimeSpan.FromSeconds(2.0));
Logs.Tester.LogInformation("Start Job[2] fails in 6 sec");
client.Schedule((_) => { jobs[2] = true; throw new Exception("Job[2]"); }, TimeSpan.FromSeconds(6.0));
client.Schedule(async () => { jobs[2] = true; throw new Exception("Job[2]"); }, TimeSpan.FromSeconds(6.0));
Logs.Tester.LogInformation("Start Job[3] starts in in 7 sec");
client.Schedule((_) => { Logs.Tester.LogInformation("Job[3]"); jobs[3] = true; return Task.CompletedTask; }, TimeSpan.FromSeconds(7.0));
client.Schedule(async () => { Logs.Tester.LogInformation("Job[3]"); jobs[3] = true; }, TimeSpan.FromSeconds(7.0));
Assert.True(new[] { false, false, false, false }.SequenceEqual(jobs));
CancellationTokenSource cts = new CancellationTokenSource();
@ -1841,52 +1633,52 @@ donation:
var waitJobsFinish = client.WaitAllRunning(default);
await mockDelay.Advance(TimeSpan.FromSeconds(2.0));
mockDelay.Advance(TimeSpan.FromSeconds(2.0));
Assert.True(new[] { false, true, false, false }.SequenceEqual(jobs));
await mockDelay.Advance(TimeSpan.FromSeconds(3.0));
mockDelay.Advance(TimeSpan.FromSeconds(3.0));
Assert.True(new[] { true, true, false, false }.SequenceEqual(jobs));
await mockDelay.Advance(TimeSpan.FromSeconds(1.0));
mockDelay.Advance(TimeSpan.FromSeconds(1.0));
Assert.True(new[] { true, true, true, false }.SequenceEqual(jobs));
Assert.Equal(1, client.GetExecutingCount());
Assert.False(waitJobsFinish.Wait(1));
Assert.False(waitJobsFinish.Wait(100));
Assert.False(waitJobsFinish.IsCompletedSuccessfully);
await mockDelay.Advance(TimeSpan.FromSeconds(1.0));
mockDelay.Advance(TimeSpan.FromSeconds(1.0));
Assert.True(new[] { true, true, true, true }.SequenceEqual(jobs));
await waitJobsFinish;
Assert.True(waitJobsFinish.Wait(100));
Assert.True(waitJobsFinish.IsCompletedSuccessfully);
Assert.True(!waitJobsFinish.IsFaulted);
Assert.Equal(0, client.GetExecutingCount());
bool jobExecuted = false;
Logs.Tester.LogInformation("This job will be cancelled");
client.Schedule((_) => { jobExecuted = true; return Task.CompletedTask; }, TimeSpan.FromSeconds(1.0));
await mockDelay.Advance(TimeSpan.FromSeconds(0.5));
client.Schedule(async () => { jobExecuted = true; }, TimeSpan.FromSeconds(1.0));
mockDelay.Advance(TimeSpan.FromSeconds(0.5));
Assert.False(jobExecuted);
TestUtils.Eventually(() => Assert.Equal(1, client.GetExecutingCount()));
Thread.Sleep(100);
Assert.Equal(1, client.GetExecutingCount());
waitJobsFinish = client.WaitAllRunning(default);
Assert.False(waitJobsFinish.Wait(100));
cts.Cancel();
await waitJobsFinish;
Assert.True(waitJobsFinish.Wait(1));
Assert.True(waitJobsFinish.Wait(1000));
Assert.True(waitJobsFinish.IsCompletedSuccessfully);
Assert.False(waitJobsFinish.IsFaulted);
Assert.True(!waitJobsFinish.IsFaulted);
Assert.False(jobExecuted);
await mockDelay.Advance(TimeSpan.FromSeconds(1.0));
mockDelay.Advance(TimeSpan.FromSeconds(1.0));
Thread.Sleep(100); // Make sure it get cancelled
Assert.False(jobExecuted);
Assert.Equal(0, client.GetExecutingCount());
await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => await processing);
Assert.True(processing.IsCanceled);
Assert.True(client.WaitAllRunning(default).Wait(100));
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
}
[Fact]
@ -2444,7 +2236,7 @@ donation:
foreach (var result in factory
.Providers
.Where(p => p.Value is BackgroundFetcherRateProvider)
.Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync(default), Fetcher: (BackgroundFetcherRateProvider)p.Value))
.Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync(), Fetcher: (BackgroundFetcherRateProvider)p.Value))
.ToList())
{
Logs.Tester.LogInformation($"Testing {result.ExpectedName}");
@ -2456,25 +2248,17 @@ donation:
Assert.NotNull(exchangeRates);
Assert.NotEmpty(exchangeRates);
Assert.NotEmpty(exchangeRates.ByExchange[result.ExpectedName]);
if (result.ExpectedName == "bitbank")
{
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
e => e.CurrencyPair == new CurrencyPair("BTC", "JPY") && e.BidAsk.Bid > 100m); // 1BTC will always be more than 100JPY
}
else
{
// This check if the currency pair is using right currency pair
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
// This check if the currency pair is using right currency pair
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
e => (e.CurrencyPair == new CurrencyPair("BTC", "USD") ||
e.CurrencyPair == new CurrencyPair("BTC", "EUR") ||
e.CurrencyPair == new CurrencyPair("BTC", "USDT") ||
e.CurrencyPair == new CurrencyPair("BTC", "CAD"))
e.CurrencyPair == new CurrencyPair("BTC", "USDT"))
&& e.BidAsk.Bid > 1.0m // 1BTC will always be more than 1USD
);
}
}
// Kraken emit one request only after first GetRates
factory.Providers["kraken"].GetRatesAsync(default).GetAwaiter().GetResult();
factory.Providers["kraken"].GetRatesAsync().GetAwaiter().GetResult();
}
[Fact]
@ -2490,7 +2274,7 @@ donation:
.ToHashSet();
var rules = new StoreBlob().GetDefaultRateRules(provider);
var result = fetcher.FetchRates(pairs, rules, default);
var result = fetcher.FetchRates(pairs, rules);
foreach (var value in result)
{
var rateResult = value.Value.GetAwaiter().GetResult();
@ -2512,7 +2296,7 @@ donation:
class SpyRateProvider : IRateProvider
{
public bool Hit { get; set; }
public Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
public Task<ExchangeRates> GetRatesAsync()
{
Hit = true;
var rates = new ExchangeRates();
@ -2548,62 +2332,6 @@ donation:
}
}
[Fact]
[Trait("Fast", "Fast")]
public async Task CanExpandExternalConnectionString()
{
var unusedUri = new Uri("https://toto.com");
Assert.True(ExternalConnectionString.TryParse("server=/test", out var connStr, out var error));
var expanded = await connStr.Expand(new Uri("https://toto.com"), ExternalServiceTypes.Charge);
Assert.Equal(new Uri("https://toto.com/test"), expanded.Server);
expanded = await connStr.Expand(new Uri("http://toto.onion"), ExternalServiceTypes.Charge);
Assert.Equal(new Uri("http://toto.onion/test"), expanded.Server);
await Assert.ThrowsAsync<SecurityException>(() => connStr.Expand(new Uri("http://toto.com"), ExternalServiceTypes.Charge));
// Make sure absolute paths are not expanded
Assert.True(ExternalConnectionString.TryParse("server=https://tow/test", out connStr, out error));
expanded = await connStr.Expand(new Uri("https://toto.com"), ExternalServiceTypes.Charge);
Assert.Equal(new Uri("https://tow/test"), expanded.Server);
// Error if directory not exists
Assert.True(ExternalConnectionString.TryParse($"server={unusedUri};macaroondirectorypath=pouet", out connStr, out error));
await Assert.ThrowsAsync<DirectoryNotFoundException>(() => connStr.Expand(unusedUri, ExternalServiceTypes.LNDGRPC));
await Assert.ThrowsAsync<DirectoryNotFoundException>(() => connStr.Expand(unusedUri, ExternalServiceTypes.LNDRest));
await connStr.Expand(unusedUri, ExternalServiceTypes.Charge);
var macaroonDirectory = CreateDirectory();
Assert.True(ExternalConnectionString.TryParse($"server={unusedUri};macaroondirectorypath={macaroonDirectory}", out connStr, out error));
await connStr.Expand(unusedUri, ExternalServiceTypes.LNDGRPC);
expanded = await connStr.Expand(unusedUri, ExternalServiceTypes.LNDRest);
Assert.NotNull(expanded.Macaroons);
Assert.Null(expanded.MacaroonFilePath);
Assert.Null(expanded.Macaroons.AdminMacaroon);
Assert.Null(expanded.Macaroons.InvoiceMacaroon);
Assert.Null(expanded.Macaroons.ReadonlyMacaroon);
File.WriteAllBytes($"{macaroonDirectory}/admin.macaroon", new byte[] { 0xaa });
File.WriteAllBytes($"{macaroonDirectory}/invoice.macaroon", new byte[] { 0xab });
File.WriteAllBytes($"{macaroonDirectory}/readonly.macaroon", new byte[] { 0xac });
expanded = await connStr.Expand(unusedUri, ExternalServiceTypes.LNDRest);
Assert.NotNull(expanded.Macaroons.AdminMacaroon);
Assert.NotNull(expanded.Macaroons.InvoiceMacaroon);
Assert.Equal("ab", expanded.Macaroons.InvoiceMacaroon.Hex);
Assert.Equal(0xab, expanded.Macaroons.InvoiceMacaroon.Bytes[0]);
Assert.NotNull(expanded.Macaroons.ReadonlyMacaroon);
Assert.True(ExternalConnectionString.TryParse($"server={unusedUri};cookiefilepath={macaroonDirectory}/charge.cookie", out connStr, out error));
File.WriteAllText($"{macaroonDirectory}/charge.cookie", "apitoken");
expanded = await connStr.Expand(unusedUri, ExternalServiceTypes.Charge);
Assert.Equal("apitoken", expanded.APIToken);
}
private string CreateDirectory([CallerMemberName] string caller = null)
{
var name = $"{caller}-{NBitcoin.RandomUtils.GetUInt32()}";
Directory.CreateDirectory(name);
return name;
}
[Fact]
[Trait("Fast", "Fast")]
public void CheckRatesProvider()
@ -2619,75 +2347,47 @@ donation:
var fetcher = new RateFetcher(factory);
var fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
var fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertHit();
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertNotHit();
Thread.Sleep(3000);
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertHit();
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertNotHit();
// Should cache at exchange level so this should hit the cache
var fetchedRate2 = fetcher.FetchRate(CurrencyPair.Parse("LTC_USD"), rateRules, default).GetAwaiter().GetResult();
var fetchedRate2 = fetcher.FetchRate(CurrencyPair.Parse("LTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertNotHit();
Assert.Null(fetchedRate2.BidAsk);
Assert.Equal(RateRulesErrors.RateUnavailable, fetchedRate2.Errors.First());
// Should cache at exchange level this should not hit the cache as it is different exchange
RateRules.TryParse("X_X = bittrex(X_X);", out rateRules);
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertHit();
factory.Providers.Clear();
var fetch = new BackgroundFetcherRateProvider(spy);
fetch.DoNotAutoFetchIfExpired = true;
factory.Providers.Add("bittrex", fetch);
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertHit();
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertNotHit();
fetch.UpdateIfNecessary(default).GetAwaiter().GetResult();
fetch.UpdateIfNecessary().GetAwaiter().GetResult();
spy.AssertNotHit();
fetch.RefreshRate = TimeSpan.FromSeconds(1.0);
Thread.Sleep(1020);
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertNotHit();
fetch.ValidatyTime = TimeSpan.FromSeconds(1.0);
fetch.UpdateIfNecessary(default).GetAwaiter().GetResult();
fetch.UpdateIfNecessary().GetAwaiter().GetResult();
spy.AssertHit();
fetch.GetRatesAsync(default).GetAwaiter().GetResult();
fetch.GetRatesAsync().GetAwaiter().GetResult();
Thread.Sleep(1000);
Assert.Throws<InvalidOperationException>(() => fetch.GetRatesAsync(default).GetAwaiter().GetResult());
}
[Fact]
[Trait("Fast", "Fast")]
public void ParseDerivationSchemeSettings()
{
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("Coldcard Import 0x60d1af8b", settings.Label);
Assert.Equal("49'/0'/0'", settings.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);
var testnet = new BTCPayNetworkProvider(NetworkType.Testnet).GetNetwork("BTC");
// Should be legacy
Assert.True(DerivationSchemeSettings.TryParseFromColdcard("{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"tpubDDWYqT3P24znfsaGX7kZcQhNc5LAjnQiKQvUCHF2jS6dsgJBRtymopEU5uGpMaR5YChjuiExZG1X2aTbqXkp82KqH5qnqwWHp6EWis9ZvKr\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/44'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}", testnet, out settings));
Assert.True(settings.AccountDerivation is DirectDerivationStrategy s && !s.Segwit);
// Should be segwit p2sh
Assert.True(DerivationSchemeSettings.TryParseFromColdcard("{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"upub5DSddA9NoRUyJrQ4p86nsCiTSY7kLHrSxx3joEJXjHd4HPARhdXUATuk585FdWPVC2GdjsMePHb6BMDmf7c6KG4K4RPX6LVqBLtDcWpQJmh\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}", testnet, out settings));
Assert.True(settings.AccountDerivation is P2SHDerivationStrategy p && p.Inner is DirectDerivationStrategy s2 && s2.Segwit);
// Should be segwit
Assert.True(DerivationSchemeSettings.TryParseFromColdcard("{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"vpub5YjYxTemJ39tFRnuAhwduyxG2tKGjoEpmvqVQRPqdYrqa6YGoeSzBtHXaJUYB19zDbXs3JjbEcVWERjQBPf9bEfUUMZNMv1QnMyHV8JPqyf\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/84'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}", testnet, out settings));
Assert.True(settings.AccountDerivation is DirectDerivationStrategy s3 && s3.Segwit);
Assert.Throws<InvalidOperationException>(() => fetch.GetRatesAsync().GetAwaiter().GetResult());
}
[Fact]
@ -2725,117 +2425,48 @@ donation:
Assert.Equal(StatusMessageModel.StatusSeverity.Success, parsed.Severity);
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanCreateInvoiceWithSpecificPaymentMethods()
{
using (var tester = ServerTester.Create())
{
tester.Start();
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterLightningNode("BTC", LightningConnectionType.Charge);
user.RegisterDerivationScheme("BTC");
user.RegisterDerivationScheme("LTC");
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(100, "BTC"));
Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count);
invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(100, "BTC")
{
SupportedTransactionCurrencies = new Dictionary<string, InvoiceSupportedTransactionCurrency>()
{
{"BTC", new InvoiceSupportedTransactionCurrency()
{
Enabled = true
}}
}
});
Assert.Single(invoice.SupportedTransactionCurrencies);
}
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanLoginWithNoSecondaryAuthSystemsOrRequestItWhenAdded()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var accountController = tester.PayTester.GetController<AccountController>();
//no 2fa or u2f enabled, login should work
Assert.Equal(nameof(HomeController.Index), Assert.IsType<RedirectToActionResult>(await accountController.Login(new LoginViewModel()
{
Email = user.RegisterDetails.Email,
Password = user.RegisterDetails.Password
})).ActionName);
var manageController = user.GetController<ManageController>();
//by default no u2f devices available
Assert.Empty(Assert.IsType<U2FAuthenticationViewModel>(Assert.IsType<ViewResult>(await manageController.U2FAuthentication()).Model).Devices);
var addRequest = Assert.IsType<AddU2FDeviceViewModel>(Assert.IsType<ViewResult>(manageController.AddU2FDevice("label")).Model);
//name should match the one provided in beginning
Assert.Equal("label",addRequest.Name);
//sending an invalid response model back to server, should error out
var statusMessage = Assert
.IsType<RedirectToActionResult>(await manageController.AddU2FDevice(addRequest))
.RouteValues["StatusMessage"].ToString();
Assert.NotNull(statusMessage);
Assert.Equal(StatusMessageModel.StatusSeverity.Error, new StatusMessageModel(statusMessage).Severity);
var contextFactory = tester.PayTester.GetService<ApplicationDbContextFactory>();
//add a fake u2f device in db directly since emulating a u2f device is hard and annoying
using (var context = contextFactory.CreateContext())
{
var newDevice = new U2FDevice()
{
Name = "fake",
Counter = 0,
KeyHandle = UTF8Encoding.UTF8.GetBytes("fake"),
PublicKey= UTF8Encoding.UTF8.GetBytes("fake"),
AttestationCert= UTF8Encoding.UTF8.GetBytes("fake"),
ApplicationUserId= user.UserId
};
await context.U2FDevices.AddAsync(newDevice);
await context.SaveChangesAsync();
Assert.NotNull(newDevice.Id);
Assert.NotEmpty(Assert.IsType<U2FAuthenticationViewModel>(Assert.IsType<ViewResult>(await manageController.U2FAuthentication()).Model).Devices);
}
//check if we are showing the u2f login screen now
var secondLoginResult = Assert.IsType<ViewResult>(await accountController.Login(new LoginViewModel()
{
Email = user.RegisterDetails.Email,
Password = user.RegisterDetails.Password
}));
Assert.Equal("SecondaryLogin", secondLoginResult.ViewName);
var vm = Assert.IsType<SecondaryLoginViewModel>(secondLoginResult.Model);
//2fa was never enabled for user so this should be empty
Assert.Null(vm.LoginWith2FaViewModel);
Assert.NotNull(vm.LoginWithU2FViewModel);
}
}
private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx)
{
var h = BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest).ScriptPubKey.Hash.ToString();
return ctx.AddressInvoices.FirstOrDefault(i => i.InvoiceDataId == invoice.Id && i.GetAddress() == h) != null;
}
public static class TestUtils
{
public static void Eventually(Action act)
{
CancellationTokenSource cts = new CancellationTokenSource(20000);
while (true)
{
try
{
act();
break;
}
catch (XunitException) when (!cts.Token.IsCancellationRequested)
{
cts.Token.WaitHandle.WaitOne(500);
}
}
}
public static async Task EventuallyAsync(Func<Task> act)
{
CancellationTokenSource cts = new CancellationTokenSource(20000);
while (true)
{
try
{
await act();
break;
}
catch (XunitException) when (!cts.Token.IsCancellationRequested)
{
await Task.Delay(500);
}
}
}
}
}
}

View File

@ -1,3 +0,0 @@
$bitcoind_container_id=$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=bitcoind)
$address=$(docker exec -ti $bitcoind_container_id bitcoin-cli -datadir="/data" getnewaddress)
docker exec -ti $bitcoind_container_id bitcoin-cli -datadir="/data" generatetoaddress $args $address

View File

@ -19,8 +19,6 @@ services:
TESTS_MYSQL: User ID=root;Host=mysql;Port=3306;Database=btcpayserver
TESTS_PORT: 80
TESTS_HOSTNAME: tests
TESTS_RUN_EXTERNAL_INTEGRATION: ${TESTS_RUN_EXTERNAL_INTEGRATION:-false}
TESTS_AzureBlobStorageConnectionString: ${TESTS_AzureBlobStorageConnectionString:-none}
TEST_MERCHANTLIGHTNINGD: "type=clightning;server=unix://etc/merchant_lightningd_datadir/lightning-rpc"
TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=unix://etc/customer_lightningd_datadir/lightning-rpc"
TEST_MERCHANTCHARGE: "type=charge;server=http://lightning-charged:9112/;api-token=foiewnccewuify"
@ -38,7 +36,7 @@ services:
# The dev container is not actually used, it is just handy to run `docker-compose up dev` to start all services
dev:
image: btcpayserver/bitcoin:0.18.0
image: btcpayserver/bitcoin:0.17.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_EXTRA_ARGS: |
@ -55,7 +53,7 @@ services:
- merchant_lnd
devlnd:
image: btcpayserver/bitcoin:0.18.0
image: btcpayserver/bitcoin:0.17.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_EXTRA_ARGS: |
@ -71,7 +69,7 @@ services:
nbxplorer:
image: nicolasdorier/nbxplorer:2.0.0.39
image: nicolasdorier/nbxplorer:2.0.0.8
restart: unless-stopped
ports:
- "32838:32838"
@ -98,14 +96,13 @@ services:
bitcoind:
restart: unless-stopped
image: btcpayserver/bitcoin:0.18.0
image: btcpayserver/bitcoin:0.17.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_EXTRA_ARGS: |-
rpcuser=ceiwHEbqWI83
rpcpassword=DwubwWsoo3
rpcport=43782
rpcbind=0.0.0.0:43782
port=39388
whitelist=0.0.0.0/0
zmqpubrawblock=tcp://0.0.0.0:28332
@ -122,7 +119,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: btcpayserver/lightning:v0.7.0-1-dev
image: btcpayserver/lightning:v0.6.2-dev
stop_signal: SIGKILL
restart: unless-stopped
environment:
@ -168,7 +165,7 @@ services:
- merchant_lightningd
merchant_lightningd:
image: btcpayserver/lightning:v0.7.0-1-dev
image: btcpayserver/lightning:v0.6.2-dev
stop_signal: SIGKILL
environment:
EXPOSE_TCP: "true"
@ -225,7 +222,7 @@ services:
- MYSQL_ALLOW_EMPTY_PASSWORD=yes
merchant_lnd:
image: btcpayserver/lnd:v0.6-beta
image: btcpayserver/lnd:v0.5.2-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -255,7 +252,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.6-beta
image: btcpayserver/lnd:v0.5.2-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"

View File

@ -3,6 +3,3 @@ set -e
dotnet test --filter Fast=Fast --no-build
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
fi

View File

@ -8,6 +8,10 @@ namespace BTCPayServer.Authentication
{
public class BitTokenEntity
{
public string Facade
{
get; set;
}
public string Value
{
get; set;
@ -35,6 +39,7 @@ namespace BTCPayServer.Authentication
return new BitTokenEntity()
{
Label = Label,
Facade = Facade,
StoreId = StoreId,
PairingTime = PairingTime,
SIN = SIN,

View File

@ -11,6 +11,11 @@ namespace BTCPayServer.Authentication
get;
set;
}
public string Facade
{
get;
set;
}
public string Label
{
get;

View File

@ -1,5 +1,5 @@
using BTCPayServer.Data;
using DBriize;
using DBreeze;
using NBitcoin;
using NBitcoin.DataEncoders;
using Newtonsoft.Json;
@ -90,6 +90,7 @@ namespace BTCPayServer.Authentication
return new BitTokenEntity()
{
Label = data.Label,
Facade = data.Facade,
Value = data.Id,
SIN = data.SIN,
PairingTime = data.PairingTime,
@ -128,6 +129,7 @@ namespace BTCPayServer.Authentication
{
var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeEntity.Id);
pairingCode.Label = pairingCodeEntity.Label;
pairingCode.Facade = pairingCodeEntity.Facade;
await ctx.SaveChangesAsync();
return CreatePairingCodeEntity(pairingCode);
}
@ -176,6 +178,7 @@ namespace BTCPayServer.Authentication
{
Id = pairingCode.TokenValue,
PairingTime = DateTime.UtcNow,
Facade = pairingCode.Facade,
Label = pairingCode.Label,
StoreDataId = pairingCode.StoreDataId,
SIN = pairingCode.SIN
@ -210,6 +213,7 @@ namespace BTCPayServer.Authentication
return null;
return new PairingCodeEntity()
{
Facade = data.Facade,
Id = data.Id,
Label = data.Label,
Expiration = data.Expiration,

View File

@ -10,12 +10,6 @@ using NBXplorer;
namespace BTCPayServer
{
public enum DerivationType
{
Legacy,
SegwitP2SH,
Segwit
}
public class BTCPayDefaultSettings
{
static BTCPayDefaultSettings()
@ -70,8 +64,7 @@ namespace BTCPayServer
public KeyPath CoinType { get; internal set; }
public int MaxTrackedConfirmation { get; internal set; } = 6;
public string[] DefaultRateRules { get; internal set; } = Array.Empty<string>();
public bool SupportRBF { get; internal set; }
public Dictionary<uint, DerivationType> ElectrumMapping = new Dictionary<uint, DerivationType>();
public override string ToString()
{
return CryptoCode;

View File

@ -25,22 +25,7 @@ namespace BTCPayServer
CryptoImagePath = "imlegacy/bitcoin.svg",
LightningImagePath = "imlegacy/bitcoin-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("0'") : new KeyPath("1'"),
SupportRBF = true,
//https://github.com/spesmilo/electrum/blob/11733d6bc271646a00b69ff07657119598874da4/electrum/constants.py
ElectrumMapping = NetworkType == NetworkType.Mainnet
? new Dictionary<uint, DerivationType>()
{
{0x0488b21eU, DerivationType.Legacy }, // xpub
{0x049d7cb2U, DerivationType.SegwitP2SH }, // ypub
{0x4b24746U, DerivationType.Segwit }, //zpub
}
: new Dictionary<uint, DerivationType>()
{
{0x043587cfU, DerivationType.Legacy},
{0x044a5262U, DerivationType.SegwitP2SH},
{0x045f1cf6U, DerivationType.Segwit}
}
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("0'") : new KeyPath("1'")
});
}
}

View File

@ -10,21 +10,20 @@ namespace BTCPayServer
{
public void InitGroestlcoin()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("GRS");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Groestlcoin",
BlockExplorerLink = NetworkType == NetworkType.Mainnet
? "https://chainz.cryptoid.info/grs/tx.dws?{0}.htm"
: "https://chainz.cryptoid.info/grs-test/tx.dws?{0}.htm",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://chainz.cryptoid.info/grs/tx.dws?{0}.htm" : "https://chainz.cryptoid.info/grs-test/tx.dws?{0}.htm",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "groestlcoin",
DefaultRateRules = new[]
{
"GRS_X = GRS_BTC * BTC_X",
"GRS_BTC = bittrex(GRS_BTC)"
"GRS_X = GRS_BTC * BTC_X",
"GRS_BTC = bittrex(GRS_BTC)"
},
CryptoImagePath = "imlegacy/groestlcoin.png",
LightningImagePath = "imlegacy/groestlcoin-lightning.svg",

View File

@ -17,30 +17,14 @@ namespace BTCPayServer
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Litecoin",
BlockExplorerLink = NetworkType == NetworkType.Mainnet
? "https://live.blockcypher.com/ltc/tx/{0}/"
: "http://explorer.litecointools.com/tx/{0}",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://live.blockcypher.com/ltc/tx/{0}/" : "http://explorer.litecointools.com/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "litecoin",
CryptoImagePath = "imlegacy/litecoin.svg",
LightningImagePath = "imlegacy/litecoin-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("2'") : new KeyPath("1'"),
//https://github.com/pooler/electrum-ltc/blob/0d6989a9d2fb2edbea421c116e49d1015c7c5a91/electrum_ltc/constants.py
ElectrumMapping = NetworkType == NetworkType.Mainnet
? new Dictionary<uint, DerivationType>()
{
{0x0488b21eU, DerivationType.Legacy },
{0x049d7cb2U, DerivationType.SegwitP2SH },
{0x04b24746U, DerivationType.Segwit },
}
: new Dictionary<uint, DerivationType>()
{
{0x043587cfU, DerivationType.Legacy },
{0x044a5262U, DerivationType.SegwitP2SH },
{0x045f1cf6U, DerivationType.Segwit }
}
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("2'") : new KeyPath("1'")
});
}
}

View File

@ -56,22 +56,6 @@ namespace BTCPayServer
InitGroestlcoin();
InitViacoin();
// Assume that electrum mappings are same as BTC if not specified
foreach (var network in _Networks)
{
if(network.Value.ElectrumMapping.Count == 0)
{
network.Value.ElectrumMapping = GetNetwork("BTC").ElectrumMapping;
if (!network.Value.NBitcoinNetwork.Consensus.SupportSegwit)
{
network.Value.ElectrumMapping =
network.Value.ElectrumMapping
.Where(kv => kv.Value == DerivationType.Legacy)
.ToDictionary(k => k.Key, k => k.Value);
}
}
}
// Disabled because of https://twitter.com/Cryptopia_NZ/status/1085084168852291586
//InitPolis();
//InitBitcoinplus();

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.3.98</Version>
<Version>1.0.3.68</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<PropertyGroup>
@ -30,28 +30,25 @@
<None Remove="Currencies.txt" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="bundleconfig.json" />
<EmbeddedResource Include="Currencies.txt" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.18" />
<PackageReference Include="BuildBundlerMinifier" Version="2.9.406" />
<PackageReference Include="BundlerMinifier.Core" Version="2.9.406" />
<PackageReference Include="BundlerMinifier.TagHelpers" Version="2.9.406" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.9" />
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.5.3" />
<PackageReference Include="HtmlSanitizer" Version="4.0.207" />
<PackageReference Include="HtmlSanitizer" Version="4.0.199" />
<PackageReference Include="LedgerWallet" Version="2.0.0.3" />
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="NBitcoin" Version="4.1.2.15" />
<PackageReference Include="NBitpayClient" Version="1.0.0.34" />
<PackageReference Include="DBriize" Version="1.0.0.4" />
<PackageReference Include="NBXplorer.Client" Version="2.0.0.11" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="NBitcoin" Version="4.1.1.78" />
<PackageReference Include="NBitpayClient" Version="1.0.0.31" />
<PackageReference Include="DBreeze" Version="1.92.0" />
<PackageReference Include="NBXplorer.Client" Version="2.0.0.3" />
<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" />
@ -62,6 +59,7 @@
<PackageReference Include="Serilog.AspNetCore" Version="2.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="4.0.0" />
<PackageReference Include="SSH.NET" Version="2016.1.0" />
<PackageReference Include="System.Xml.XmlSerializer" Version="4.3.0" />
<PackageReference Include="Text.Analyzers" Version="2.6.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
@ -69,13 +67,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.9" />
<PackageReference Include="TwentyTwenty.Storage" Version="2.10.1" />
<PackageReference Include="TwentyTwenty.Storage.Amazon" Version="2.10.1" />
<PackageReference Include="TwentyTwenty.Storage.Azure" Version="2.10.1" />
<PackageReference Include="TwentyTwenty.Storage.Google" Version="2.10.1" />
<PackageReference Include="TwentyTwenty.Storage.Local" Version="2.10.1" />
<PackageReference Include="U2F.Core" Version="1.0.4" />
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.6" />
<PackageReference Include="YamlDotNet" Version="5.2.1" />
</ItemGroup>
@ -131,11 +123,9 @@
<ItemGroup>
<Folder Include="Build\" />
<Folder Include="U2F\Services" />
<Folder Include="wwwroot\vendor\clipboard.js\" />
<Folder Include="wwwroot\vendor\highlightjs\" />
<Folder Include="wwwroot\vendor\summernote" />
<Folder Include="wwwroot\vendor\u2f" />
</ItemGroup>
<ItemGroup>
@ -152,9 +142,6 @@
<Content Update="Views\Server\LightningWalletServices.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\P2PService.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\SSHService.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>

View File

@ -15,6 +15,7 @@ using Renci.SshNet;
using NBitcoin.DataEncoders;
using BTCPayServer.SSH;
using BTCPayServer.Lightning;
using BTCPayServer.Configuration.External;
using Serilog.Events;
namespace BTCPayServer.Configuration
@ -48,7 +49,11 @@ namespace BTCPayServer.Configuration
get;
private set;
}
public EndPoint SocksEndpoint { get; set; }
public List<IPEndPoint> Listen
{
get;
set;
}
public List<NBXplorerConnectionSetting> NBXplorerConnectionSettings
{
@ -123,7 +128,85 @@ namespace BTCPayServer.Configuration
}
}
ExternalServices.Load(net.CryptoCode, conf);
void externalLnd<T>(string code, string lndType)
{
var lightning = conf.GetOrDefault<string>(code, string.Empty);
if (lightning.Length != 0)
{
if (!LightningConnectionString.TryParse(lightning, false, out var connectionString, out var error))
{
Logs.Configuration.LogWarning($"Invalid setting {code}, " + Environment.NewLine +
$"lnd server: 'type={lndType};server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
$"lnd server: 'type={lndType};server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
$"Error: {error}" + Environment.NewLine +
"This service will not be exposed through BTCPay Server");
}
else
{
var instanceType = typeof(T);
ExternalServicesByCryptoCode.Add(net.CryptoCode, (ExternalService)Activator.CreateInstance(instanceType, connectionString));
}
}
};
externalLnd<ExternalLndGrpc>($"{net.CryptoCode}.external.lnd.grpc", "lnd-grpc");
externalLnd<ExternalLndRest>($"{net.CryptoCode}.external.lnd.rest", "lnd-rest");
{
var spark = conf.GetOrDefault<string>($"{net.CryptoCode}.external.spark", string.Empty);
if (spark.Length != 0)
{
if (!SparkConnectionString.TryParse(spark, out var connectionString, out var error))
{
Logs.Configuration.LogWarning($"Invalid setting {net.CryptoCode}.external.spark, " + Environment.NewLine +
$"Valid example: 'server=https://btcpay.example.com/spark/btc/;cookiefile=/etc/clightning_bitcoin_spark/.cookie'" + Environment.NewLine +
$"Error: {error}" + Environment.NewLine +
"This service will not be exposed through BTCPay Server");
}
else
{
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalSpark(connectionString));
}
}
}
{
var rtl = conf.GetOrDefault<string>($"{net.CryptoCode}.external.rtl", string.Empty);
if (rtl.Length != 0)
{
if (!SparkConnectionString.TryParse(rtl, out var connectionString, out var error))
{
Logs.Configuration.LogWarning($"Invalid setting {net.CryptoCode}.external.rtl, " + Environment.NewLine +
$"Valid example: 'server=https://btcpay.example.com/rtl/btc/;cookiefile=/etc/clightning_bitcoin_rtl/.cookie'" + Environment.NewLine +
$"Error: {error}" + Environment.NewLine +
"This service will not be exposed through BTCPay Server");
}
else
{
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalRTL(connectionString));
}
}
}
var charge = conf.GetOrDefault<string>($"{net.CryptoCode}.external.charge", string.Empty);
if (charge.Length != 0)
{
if (!LightningConnectionString.TryParse(charge, false, out var chargeConnectionString, out var chargeError))
LightningConnectionString.TryParse("type=charge;" + charge, false, out chargeConnectionString, out chargeError);
if (chargeConnectionString == null || chargeConnectionString.ConnectionType != LightningConnectionType.Charge)
{
Logs.Configuration.LogWarning($"Invalid setting {net.CryptoCode}.external.charge, " + Environment.NewLine +
$"lightning charge server: 'type=charge;server=https://charge.example.com;api-token=2abdf302...'" + Environment.NewLine +
$"lightning charge server: 'type=charge;server=https://charge.example.com;cookiefilepath=/root/.charge/.cookie'" + Environment.NewLine +
$"Error: {chargeError ?? string.Empty}" + Environment.NewLine +
$"This service will not be exposed through BTCPay Server");
}
else
{
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalCharge(chargeConnectionString));
}
}
}
Logs.Configuration.LogInformation("Supported chains: " + String.Join(',', supportedChains.ToArray()));
@ -137,24 +220,13 @@ namespace BTCPayServer.Configuration
.Select(p => (Name: p.p.Substring(0, p.SeparatorIndex),
Link: p.p.Substring(p.SeparatorIndex + 1))))
{
if (Uri.TryCreate(service.Link, UriKind.RelativeOrAbsolute, out var uri))
OtherExternalServices.AddOrReplace(service.Name, uri);
ExternalServices.AddOrReplace(service.Name, service.Link);
}
}
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
MySQLConnectionString = conf.GetOrDefault<string>("mysql", null);
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
TorrcFile = conf.GetOrDefault<string>("torrcfile", null);
var socksEndpointString = conf.GetOrDefault<string>("socksendpoint", null);
if(!string.IsNullOrEmpty(socksEndpointString))
{
if (!Utils.TryParseEndpoint(socksEndpointString, 9050, out var endpoint))
throw new ConfigException("Invalid value for socksendpoint");
SocksEndpoint = endpoint;
}
var sshSettings = ParseSSHConfiguration(conf);
if ((!string.IsNullOrEmpty(sshSettings.Password) || !string.IsNullOrEmpty(sshSettings.KeyFile)) && !string.IsNullOrEmpty(sshSettings.Server))
@ -214,6 +286,7 @@ namespace BTCPayServer.Configuration
private SSHSettings ParseSSHConfiguration(IConfiguration conf)
{
var externalUrl = conf.GetOrDefault<Uri>("externalurl", null);
var settings = new SSHSettings();
settings.Server = conf.GetOrDefault<string>("sshconnection", null);
if (settings.Server != null)
@ -240,6 +313,12 @@ namespace BTCPayServer.Configuration
settings.Username = "root";
}
}
else if (externalUrl != null)
{
settings.Port = 22;
settings.Username = "root";
settings.Server = externalUrl.DnsSafeHost;
}
settings.Password = conf.GetOrDefault<string>("sshpassword", "");
settings.KeyFile = conf.GetOrDefault<string>("sshkeyfile", "");
settings.KeyFilePassword = conf.GetOrDefault<string>("sshkeyfilepassword", "");
@ -253,9 +332,9 @@ namespace BTCPayServer.Configuration
public string RootPath { get; set; }
public Dictionary<string, LightningConnectionString> InternalLightningByCryptoCode { get; set; } = new Dictionary<string, LightningConnectionString>();
public Dictionary<string, string> ExternalServices { get; set; } = new Dictionary<string, string>();
public Dictionary<string, Uri> OtherExternalServices { get; set; } = new Dictionary<string, Uri>();
public ExternalServices ExternalServices { get; set; } = new ExternalServices();
public ExternalServices ExternalServicesByCryptoCode { get; set; } = new ExternalServices();
public BTCPayNetworkProvider NetworkProvider { get; set; }
public string PostgresConnectionString
@ -279,6 +358,5 @@ namespace BTCPayServer.Configuration
get;
set;
}
public string TorrcFile { get; set; }
}
}

View File

@ -6,7 +6,6 @@ using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Microsoft.Extensions.Primitives;
using NBitcoin;
namespace BTCPayServer.Configuration
{

View File

@ -32,6 +32,7 @@ namespace BTCPayServer.Configuration
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);
app.Option("--externalurl", $"The expected external URL of this service, to use if BTCPay is behind a reverse proxy (default: empty, use the incoming HTTP request to figure out)", CommandOptionType.SingleValue);
app.Option("--externalservices", $"Links added to external services inside Server Settings / Services under the format service1:path2;service2:path2.(default: empty)", CommandOptionType.SingleValue);
app.Option("--bundlejscss", $"Bundle JavaScript and CSS files for better performance (default: true)", CommandOptionType.SingleValue);
app.Option("--rootpath", "The root path in the URL to access BTCPay (default: /)", CommandOptionType.SingleValue);
@ -40,8 +41,6 @@ namespace BTCPayServer.Configuration
app.Option("--sshkeyfile", "SSH private key file to manage BTCPay (default: empty)", CommandOptionType.SingleValue);
app.Option("--sshkeyfilepassword", "Password of the SSH keyfile (default: empty)", CommandOptionType.SingleValue);
app.Option("--sshtrustedfingerprints", "SSH Host public key fingerprint or sha256 (default: empty, it will allow untrusted connections)", CommandOptionType.SingleValue);
app.Option("--torrcfile", "Path to torrc file containing hidden services directories (default: empty)", CommandOptionType.SingleValue);
app.Option("--socksendpoint", "Socks endpoint to connect to onion urls (default: empty)", CommandOptionType.SingleValue);
app.Option("--debuglog", "A rolling log file for debug messages.", CommandOptionType.SingleValue);
app.Option("--debugloglevel", "The severity you log (default:information)", CommandOptionType.SingleValue);
app.Option("--disable-registration", "Disables new user registrations (default:true)", CommandOptionType.SingleValue);

View File

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Lightning;
namespace BTCPayServer.Configuration.External
{
public class ExternalCharge : ExternalService
{
public ExternalCharge(LightningConnectionString connectionString)
{
if (connectionString == null)
throw new ArgumentNullException(nameof(connectionString));
ConnectionString = connectionString;
}
public LightningConnectionString ConnectionString { get; }
}
}

View File

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Lightning;
namespace BTCPayServer.Configuration.External
{
public abstract class ExternalLnd : ExternalService
{
public ExternalLnd(LightningConnectionString connectionString, string type)
{
ConnectionString = connectionString;
Type = type;
}
public string Type { get; set; }
public LightningConnectionString ConnectionString { get; set; }
}
public class ExternalLndGrpc : ExternalLnd
{
public ExternalLndGrpc(LightningConnectionString connectionString) : base(connectionString, "lnd-grpc") { }
}
public class ExternalLndRest : ExternalLnd
{
public ExternalLndRest(LightningConnectionString connectionString) : base(connectionString, "lnd-rest") { }
}
}

View File

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Configuration.External
{
public class ExternalRTL : ExternalService, IAccessKeyService
{
public SparkConnectionString ConnectionString { get; }
public ExternalRTL(SparkConnectionString connectionString)
{
if (connectionString == null)
throw new ArgumentNullException(nameof(connectionString));
ConnectionString = connectionString;
}
public async Task<string> ExtractAccessKey()
{
if (ConnectionString?.CookeFile == null)
throw new FormatException("Invalid connection string");
return await System.IO.File.ReadAllTextAsync(ConnectionString.CookeFile);
}
}
}

View File

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Lightning;
namespace BTCPayServer.Configuration.External
{
public class ExternalServices : MultiValueDictionary<string, ExternalService>
{
public IEnumerable<T> GetServices<T>(string cryptoCode) where T : ExternalService
{
if (!this.TryGetValue(cryptoCode.ToUpperInvariant(), out var services))
return Array.Empty<T>();
return services.OfType<T>();
}
}
public class ExternalService
{
}
}

View File

@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Configuration.External
{
public interface IAccessKeyService
{
SparkConnectionString ConnectionString { get; }
Task<string> ExtractAccessKey();
}
public class ExternalSpark : ExternalService, IAccessKeyService
{
public SparkConnectionString ConnectionString { get; }
public ExternalSpark(SparkConnectionString connectionString)
{
if (connectionString == null)
throw new ArgumentNullException(nameof(connectionString));
ConnectionString = connectionString;
}
public async Task<string> ExtractAccessKey()
{
if (ConnectionString?.CookeFile == null)
throw new FormatException("Invalid connection string");
var cookie = (ConnectionString.CookeFile == "fake"
? "fake:fake:fake" // Hacks for testing
: await System.IO.File.ReadAllTextAsync(ConnectionString.CookeFile)).Split(':');
if (cookie.Length >= 3)
{
return cookie[2];
}
throw new FormatException("Invalid cookiefile format");
}
}
}

View File

@ -1,200 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
namespace BTCPayServer.Configuration
{
public class ExternalConnectionString
{
public ExternalConnectionString()
{
}
public ExternalConnectionString(Uri server)
{
Server = server;
}
public Uri Server { get; set; }
public byte[] Macaroon { get; set; }
public Macaroons Macaroons { get; set; }
public string MacaroonFilePath { get; set; }
public string CertificateThumbprint { get; set; }
public string MacaroonDirectoryPath { get; set; }
public string APIToken { get; set; }
public string CookieFilePath { get; set; }
public string AccessKey { get; set; }
/// <summary>
/// Return a connectionString which does not depends on external resources or information like relative path or file path
/// </summary>
/// <returns></returns>
public async Task<ExternalConnectionString> Expand(Uri absoluteUrlBase, ExternalServiceTypes serviceType)
{
var connectionString = this.Clone();
// Transform relative URI into absolute URI
var serviceUri = connectionString.Server.IsAbsoluteUri ? connectionString.Server : ToRelative(absoluteUrlBase, connectionString.Server.ToString());
if (!serviceUri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase) &&
!serviceUri.DnsSafeHost.EndsWith(".onion", StringComparison.OrdinalIgnoreCase))
{
throw new System.Security.SecurityException($"Insecure transport protocol to access this service, please use HTTPS or TOR");
}
connectionString.Server = serviceUri;
if (serviceType == ExternalServiceTypes.LNDGRPC || serviceType == ExternalServiceTypes.LNDRest)
{
// Read the MacaroonDirectory
if (connectionString.MacaroonDirectoryPath != null)
{
try
{
connectionString.Macaroons = await Macaroons.GetFromDirectoryAsync(connectionString.MacaroonDirectoryPath);
connectionString.MacaroonDirectoryPath = null;
}
catch (Exception ex)
{
throw new System.IO.DirectoryNotFoundException("Macaroon directory path not found", ex);
}
}
// Read the MacaroonFilePath
if (connectionString.MacaroonFilePath != null)
{
try
{
connectionString.Macaroon = await System.IO.File.ReadAllBytesAsync(connectionString.MacaroonFilePath);
connectionString.MacaroonFilePath = null;
}
catch (Exception ex)
{
throw new System.IO.FileNotFoundException("Macaroon not found", ex);
}
}
}
if (serviceType == ExternalServiceTypes.Charge || serviceType == ExternalServiceTypes.RTL || serviceType == ExternalServiceTypes.Spark)
{
// Read access key from cookie file
if (connectionString.CookieFilePath != null)
{
string cookieFileContent = null;
bool isFake = false;
try
{
cookieFileContent = await System.IO.File.ReadAllTextAsync(connectionString.CookieFilePath);
isFake = connectionString.CookieFilePath == "fake";
connectionString.CookieFilePath = null;
}
catch (Exception ex)
{
throw new System.IO.FileNotFoundException("Cookie file path not found", ex);
}
if (serviceType == ExternalServiceTypes.RTL)
{
connectionString.AccessKey = cookieFileContent;
}
else if (serviceType == ExternalServiceTypes.Spark)
{
var cookie = (isFake ? "fake:fake:fake" // Hacks for testing
: cookieFileContent).Split(':');
if (cookie.Length >= 3)
{
connectionString.AccessKey = cookie[2];
}
else
{
throw new FormatException("Invalid cookiefile format");
}
}
else if (serviceType == ExternalServiceTypes.Charge)
{
connectionString.APIToken = isFake ? "fake" : cookieFileContent;
}
}
}
return connectionString;
}
private Uri ToRelative(Uri absoluteUrlBase, string path)
{
if (path.StartsWith('/'))
path = path.Substring(1);
return new Uri($"{absoluteUrlBase.AbsoluteUri.WithTrailingSlash()}{path}", UriKind.Absolute);
}
public ExternalConnectionString Clone()
{
return new ExternalConnectionString()
{
MacaroonFilePath = MacaroonFilePath,
CertificateThumbprint = CertificateThumbprint,
Macaroon = Macaroon,
MacaroonDirectoryPath = MacaroonDirectoryPath,
Server = Server,
APIToken = APIToken,
CookieFilePath = CookieFilePath,
AccessKey = AccessKey,
Macaroons = Macaroons?.Clone()
};
}
public static bool TryParse(string str, out ExternalConnectionString result, out string error)
{
if (str == null)
throw new ArgumentNullException(nameof(str));
error = null;
result = null;
var resultTemp = new ExternalConnectionString();
foreach(var kv in str.Split(';')
.Select(part => part.Split('='))
.Where(kv => kv.Length == 2))
{
switch (kv[0].ToLowerInvariant())
{
case "server":
if (resultTemp.Server != null)
{
error = "Duplicated server attribute";
return false;
}
if (!Uri.IsWellFormedUriString(kv[1], UriKind.RelativeOrAbsolute))
{
error = "Invalid URI";
return false;
}
resultTemp.Server = new Uri(kv[1], UriKind.RelativeOrAbsolute);
if (!resultTemp.Server.IsAbsoluteUri && (kv[1].Length == 0 || kv[1][0] != '/'))
resultTemp.Server = new Uri($"/{kv[1]}", UriKind.RelativeOrAbsolute);
break;
case "cookiefile":
case "cookiefilepath":
if (resultTemp.CookieFilePath != null)
{
error = "Duplicated cookiefile attribute";
return false;
}
resultTemp.CookieFilePath = kv[1];
break;
case "macaroondirectorypath":
resultTemp.MacaroonDirectoryPath = kv[1];
break;
case "certthumbprint":
resultTemp.CertificateThumbprint = kv[1];
break;
case "macaroonfilepath":
resultTemp.MacaroonFilePath = kv[1];
break;
case "api-token":
resultTemp.APIToken = kv[1];
break;
case "access-key":
resultTemp.AccessKey = kv[1];
break;
}
}
result = resultTemp;
return true;
}
}
}

View File

@ -1,81 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Lightning;
using Microsoft.Extensions.Configuration;
namespace BTCPayServer.Configuration
{
public class ExternalServices : List<ExternalService>
{
public void Load(string cryptoCode, IConfiguration configuration)
{
Load(configuration, cryptoCode, "lndgrpc", ExternalServiceTypes.LNDGRPC, "Invalid setting {0}, " + Environment.NewLine +
"lnd server: 'server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
"lnd server: 'server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
"lnd server: 'server=https://lnd.example.com;macaroondirectorypath=/root/.lnd;certthumbprint=2abdf302...'" + Environment.NewLine +
"Error: {1}",
"LND (gRPC server)");
Load(configuration, cryptoCode, "lndrest", ExternalServiceTypes.LNDRest, "Invalid setting {0}, " + Environment.NewLine +
"lnd server: 'server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
"lnd server: 'server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
"lnd server: 'server=https://lnd.example.com;macaroondirectorypath=/root/.lnd;certthumbprint=2abdf302...'" + Environment.NewLine +
"Error: {1}",
"LND (REST server)");
Load(configuration, cryptoCode, "spark", ExternalServiceTypes.Spark, "Invalid setting {0}, " + Environment.NewLine +
$"Valid example: 'server=https://btcpay.example.com/spark/btc/;cookiefile=/etc/clightning_bitcoin_spark/.cookie'" + Environment.NewLine +
"Error: {1}",
"C-Lightning (Spark server)");
Load(configuration, cryptoCode, "rtl", ExternalServiceTypes.RTL, "Invalid setting {0}, " + Environment.NewLine +
$"Valid example: 'server=https://btcpay.example.com/rtl/btc/;cookiefile=/etc/clightning_bitcoin_rtl/.cookie'" + Environment.NewLine +
"Error: {1}",
"LND (Ride the Lightning server)");
Load(configuration, cryptoCode, "charge", ExternalServiceTypes.Charge, "Invalid setting {0}, " + Environment.NewLine +
$"lightning charge server: 'type=charge;server=https://charge.example.com;api-token=2abdf302...'" + Environment.NewLine +
$"lightning charge server: 'type=charge;server=https://charge.example.com;cookiefilepath=/root/.charge/.cookie'" + Environment.NewLine +
"Error: {1}",
"C-Lightning (Charge server)");
}
void Load(IConfiguration configuration, string cryptoCode, string serviceName, ExternalServiceTypes type, string errorMessage, string displayName)
{
var setting = $"{cryptoCode}.external.{serviceName}";
var connStr = configuration.GetOrDefault<string>(setting, string.Empty);
if (connStr.Length != 0)
{
if (!ExternalConnectionString.TryParse(connStr, out var connectionString, out var error))
{
throw new ConfigException(string.Format(CultureInfo.InvariantCulture, errorMessage, setting, error));
}
this.Add(new ExternalService() { Type = type, ConnectionString = connectionString, CryptoCode = cryptoCode, DisplayName = displayName, ServiceName = serviceName });
}
}
public ExternalService GetService(string serviceName, string cryptoCode)
{
return this.FirstOrDefault(o => o.CryptoCode.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase) &&
o.ServiceName.Equals(serviceName, StringComparison.OrdinalIgnoreCase));
}
}
public class ExternalService
{
public string DisplayName { get; set; }
public ExternalServiceTypes Type { get; set; }
public ExternalConnectionString ConnectionString { get; set; }
public string CryptoCode { get; set; }
public string ServiceName { get; set; }
}
public enum ExternalServiceTypes
{
LNDRest,
LNDGRPC,
Spark,
RTL,
Charge,
P2P
}
}

View File

@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Configuration
{
public class SparkConnectionString
{
public Uri Server { get; private set; }
public string CookeFile { get; private set; }
public static bool TryParse(string str, out SparkConnectionString result, out string error)
{
if (str == null)
throw new ArgumentNullException(nameof(str));
error = null;
result = null;
var resultTemp = new SparkConnectionString();
foreach(var kv in str.Split(';')
.Select(part => part.Split('='))
.Where(kv => kv.Length == 2))
{
switch (kv[0].ToLowerInvariant())
{
case "server":
if (resultTemp.Server != null)
{
error = "Duplicated server attribute";
return false;
}
if (!Uri.IsWellFormedUriString(kv[1], UriKind.Absolute))
{
error = "Invalid URI";
return false;
}
resultTemp.Server = new Uri(kv[1], UriKind.Absolute);
if(resultTemp.Server.Scheme == "http")
{
error = "Insecure transport protocol (http)";
return false;
}
break;
case "cookiefile":
case "cookiefilepath":
if (resultTemp.CookeFile != null)
{
error = "Duplicated cookiefile attribute";
return false;
}
resultTemp.CookeFile = kv[1];
break;
default:
return false;
}
}
result = resultTemp;
return true;
}
}
}

View File

@ -35,19 +35,20 @@ namespace BTCPayServer.Controllers
[AllowAnonymous]
public async Task<DataWrapper<List<PairingCodeResponse>>> Tokens([FromBody] TokenRequest request)
{
if (request == null)
throw new BitpayHttpException(400, "The request body is missing");
PairingCodeEntity pairingEntity = null;
if (string.IsNullOrEmpty(request.PairingCode))
{
if (string.IsNullOrEmpty(request.Id) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(request.Id))
throw new BitpayHttpException(400, "'id' property is required");
if (string.IsNullOrEmpty(request.Facade))
throw new BitpayHttpException(400, "'facade' property is required");
var pairingCode = await _TokenRepository.CreatePairingCodeAsync();
await _TokenRepository.PairWithSINAsync(pairingCode, request.Id);
pairingEntity = await _TokenRepository.UpdatePairingCode(new PairingCodeEntity()
{
Id = pairingCode,
Facade = request.Facade,
Label = request.Label
});
@ -83,7 +84,7 @@ namespace BTCPayServer.Controllers
PairingCode = pairingEntity.Id,
PairingExpiration = pairingEntity.Expiration,
DateCreated = pairingEntity.CreatedTime,
Facade = "merchant",
Facade = pairingEntity.Facade,
Token = pairingEntity.TokenValue,
Label = pairingEntity.Label
}

View File

@ -18,9 +18,6 @@ using BTCPayServer.Services.Stores;
using BTCPayServer.Logging;
using BTCPayServer.Security;
using System.Globalization;
using BTCPayServer.Services.U2F;
using BTCPayServer.Services.U2F.Models;
using Newtonsoft.Json;
using NicolasDorier.RateLimits;
namespace BTCPayServer.Controllers
@ -36,8 +33,6 @@ namespace BTCPayServer.Controllers
RoleManager<IdentityRole> _RoleManager;
SettingsRepository _SettingsRepository;
Configuration.BTCPayServerOptions _Options;
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
private readonly U2FService _u2FService;
ILogger _logger;
public AccountController(
@ -47,9 +42,7 @@ namespace BTCPayServer.Controllers
SignInManager<ApplicationUser> signInManager,
EmailSenderFactory emailSenderFactory,
SettingsRepository settingsRepository,
Configuration.BTCPayServerOptions options,
BTCPayServerEnvironment btcPayServerEnvironment,
U2FService u2FService)
Configuration.BTCPayServerOptions options)
{
this.storeRepository = storeRepository;
_userManager = userManager;
@ -58,8 +51,6 @@ namespace BTCPayServer.Controllers
_RoleManager = roleManager;
_SettingsRepository = settingsRepository;
_Options = options;
_btcPayServerEnvironment = btcPayServerEnvironment;
_u2FService = u2FService;
_logger = Logs.PayServer;
}
@ -100,44 +91,8 @@ namespace BTCPayServer.Controllers
return View(model);
}
}
else
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return View(model);
}
if (!await _userManager.IsLockedOutAsync(user) && await _u2FService.HasDevices(user.Id))
{
if (await _userManager.CheckPasswordAsync(user, model.Password))
{
LoginWith2faViewModel twoFModel = null;
if (user.TwoFactorEnabled)
{
// we need to do an actual sign in attempt so that 2fa can function in next step
await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: true);
twoFModel = new LoginWith2faViewModel
{
RememberMe = model.RememberMe
};
}
return View("SecondaryLogin", new SecondaryLoginViewModel()
{
LoginWith2FaViewModel = twoFModel,
LoginWithU2FViewModel = await BuildU2FViewModel(model.RememberMe, user)
});
}
else
{
var incrementAccessFailedResult = await _userManager.AccessFailedAsync(user);
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return View(model);
}
}
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: true);
if (result.Succeeded)
{
@ -146,12 +101,10 @@ namespace BTCPayServer.Controllers
}
if (result.RequiresTwoFactor)
{
return View("SecondaryLogin", new SecondaryLoginViewModel()
return RedirectToAction(nameof(LoginWith2fa), new
{
LoginWith2FaViewModel = new LoginWith2faViewModel()
{
RememberMe = model.RememberMe
}
returnUrl,
model.RememberMe
});
}
if (result.IsLockedOut)
@ -170,71 +123,6 @@ namespace BTCPayServer.Controllers
return View(model);
}
private async Task<LoginWithU2FViewModel> BuildU2FViewModel(bool rememberMe, ApplicationUser user)
{
if (_btcPayServerEnvironment.IsSecure)
{
var u2fChallenge = await _u2FService.GenerateDeviceChallenges(user.Id,
Request.GetAbsoluteUriNoPathBase().ToString().TrimEnd('/'));
return new LoginWithU2FViewModel()
{
Version = u2fChallenge[0].version,
Challenge = u2fChallenge[0].challenge,
Challenges = JsonConvert.SerializeObject(u2fChallenge),
AppId = u2fChallenge[0].appId,
UserId = user.Id,
RememberMe = rememberMe
};
}
return null;
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> LoginWithU2F(LoginWithU2FViewModel viewModel, string returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
var user = await _userManager.FindByIdAsync(viewModel.UserId);
if (user == null)
{
return NotFound();
}
var errorMessage = string.Empty;
try
{
if (await _u2FService.AuthenticateUser(viewModel.UserId, viewModel.DeviceResponse))
{
await _signInManager.SignInAsync(user, viewModel.RememberMe, "U2F");
_logger.LogInformation("User logged in.");
return RedirectToLocal(returnUrl);
}
errorMessage = "Invalid login attempt.";
}
catch (Exception e)
{
errorMessage = e.Message;
}
ModelState.AddModelError(string.Empty, errorMessage);
return View("SecondaryLogin", new SecondaryLoginViewModel()
{
LoginWithU2FViewModel = viewModel,
LoginWith2FaViewModel = !user.TwoFactorEnabled
? null
: new LoginWith2faViewModel()
{
RememberMe = viewModel.RememberMe
}
});
}
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> LoginWith2fa(bool rememberMe, string returnUrl = null)
@ -247,13 +135,10 @@ namespace BTCPayServer.Controllers
throw new ApplicationException($"Unable to load two-factor authentication user.");
}
var model = new LoginWith2faViewModel { RememberMe = rememberMe };
ViewData["ReturnUrl"] = returnUrl;
return View("SecondaryLogin", new SecondaryLoginViewModel()
{
LoginWith2FaViewModel = new LoginWith2faViewModel { RememberMe = rememberMe },
LoginWithU2FViewModel = (await _u2FService.HasDevices(user.Id))? await BuildU2FViewModel(rememberMe, user): null
});
return View(model);
}
[HttpPost]
@ -290,11 +175,7 @@ namespace BTCPayServer.Controllers
{
_logger.LogWarning("Invalid authenticator code entered for user with ID {UserId}.", user.Id);
ModelState.AddModelError(string.Empty, "Invalid authenticator code.");
return View("SecondaryLogin", new SecondaryLoginViewModel()
{
LoginWith2FaViewModel = model,
LoginWithU2FViewModel = (await _u2FService.HasDevices(user.Id))? await BuildU2FViewModel(rememberMe, user): null
});
return View();
}
}

View File

@ -1,11 +1,9 @@
using System;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Mails;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers
@ -34,7 +32,6 @@ namespace BTCPayServer.Controllers
var settings = app.GetSettings<CrowdfundSettings>();
var vm = new UpdateCrowdfundViewModel()
{
NotificationEmailWarning = !await IsEmailConfigured(app.StoreDataId),
Title = settings.Title,
Enabled = settings.Enabled,
EnforceTargetAmount = settings.EnforceTargetAmount,
@ -59,9 +56,7 @@ namespace BTCPayServer.Controllers
AppId = appId,
SearchTerm = app.TagAllInvoices ? $"storeid:{app.StoreDataId}" : $"orderid:{AppService.GetCrowdfundOrderId(appId)}",
DisplayPerksRanking = settings.DisplayPerksRanking,
SortPerksByPopularity = settings.SortPerksByPopularity,
Sounds = string.Join(Environment.NewLine, settings.Sounds),
AnimationColors = string.Join(Environment.NewLine, settings.AnimationColors)
SortPerksByPopularity = settings.SortPerksByPopularity
};
return View(vm);
}
@ -95,24 +90,6 @@ namespace BTCPayServer.Controllers
{
ModelState.AddModelError(nameof(vm.DisplayPerksRanking), "You must sort by popularity in order to display ranking.");
}
var parsedSounds = vm.Sounds.Split(
new[] {"\r\n", "\r", "\n"},
StringSplitOptions.None
).Select(s => s.Trim()).ToArray();
if (vm.SoundsEnabled && !parsedSounds.Any())
{
ModelState.AddModelError(nameof(vm.Sounds), "You must have at least one sound if you enable sounds");
}
var parsedAnimationColors = vm.AnimationColors.Split(
new[] { "\r\n", "\r", "\n" },
StringSplitOptions.None
).Select(s => s.Trim()).ToArray();
if (vm.AnimationsEnabled && !parsedAnimationColors.Any())
{
ModelState.AddModelError(nameof(vm.AnimationColors), "You must have at least one animation color if you enable animations");
}
if (!ModelState.IsValid)
{
@ -138,7 +115,6 @@ namespace BTCPayServer.Controllers
MainImageUrl = vm.MainImageUrl,
EmbeddedCSS = vm.EmbeddedCSS,
NotificationUrl = vm.NotificationUrl,
NotificationEmail = vm.NotificationEmail,
Tagline = vm.Tagline,
PerksTemplate = vm.PerksTemplate,
DisqusEnabled = vm.DisqusEnabled,
@ -148,9 +124,7 @@ namespace BTCPayServer.Controllers
ResetEveryAmount = vm.ResetEveryAmount,
ResetEvery = Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery),
DisplayPerksRanking = vm.DisplayPerksRanking,
SortPerksByPopularity = vm.SortPerksByPopularity,
Sounds = parsedSounds,
AnimationColors = parsedAnimationColors
SortPerksByPopularity = vm.SortPerksByPopularity
};
app.TagAllInvoices = vm.UseAllStoreInvoices;

View File

@ -7,7 +7,6 @@ using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Mails;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
@ -78,9 +77,6 @@ namespace BTCPayServer.Controllers
public string CustomCSSLink { get; set; }
public string NotificationEmail { get; set; }
public string NotificationUrl { get; set; }
public bool? RedirectAutomatically { get; set; }
}
[HttpGet]
@ -91,10 +87,8 @@ namespace BTCPayServer.Controllers
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
var vm = new UpdatePointOfSaleViewModel()
{
NotificationEmailWarning = !await IsEmailConfigured(app.StoreDataId),
Id = appId,
Title = settings.Title,
EnableShoppingCart = settings.EnableShoppingCart,
@ -107,10 +101,7 @@ namespace BTCPayServer.Controllers
CustomButtonText = settings.CustomButtonText ?? PointOfSaleSettings.CUSTOM_BUTTON_TEXT_DEF,
CustomTipText = settings.CustomTipText ?? PointOfSaleSettings.CUSTOM_TIP_TEXT_DEF,
CustomTipPercentages = settings.CustomTipPercentages != null ? string.Join(",", settings.CustomTipPercentages) : string.Join(",", PointOfSaleSettings.CUSTOM_TIP_PERCENTAGES_DEF),
CustomCSSLink = settings.CustomCSSLink,
NotificationEmail = settings.NotificationEmail,
NotificationUrl = settings.NotificationUrl,
RedirectAutomatically = settings.RedirectAutomatically.HasValue? settings.RedirectAutomatically.Value? "true": "false" : ""
CustomCSSLink = settings.CustomCSSLink
};
if (HttpContext?.Request != null)
{
@ -183,11 +174,7 @@ namespace BTCPayServer.Controllers
CustomButtonText = vm.CustomButtonText,
CustomTipText = vm.CustomTipText,
CustomTipPercentages = ListSplit(vm.CustomTipPercentages),
CustomCSSLink = vm.CustomCSSLink,
NotificationUrl = vm.NotificationUrl,
NotificationEmail = vm.NotificationEmail,
RedirectAutomatically = string.IsNullOrEmpty(vm.RedirectAutomatically)? (bool?) null: bool.Parse(vm.RedirectAutomatically)
CustomCSSLink = vm.CustomCSSLink
});
await UpdateAppSettings(app);
StatusMessage = "App updated";

View File

@ -7,7 +7,6 @@ using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Security;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Rates;
using Ganss.XSS;
using Microsoft.AspNetCore.Authorization;
@ -31,7 +30,6 @@ namespace BTCPayServer.Controllers
BTCPayNetworkProvider networkProvider,
CurrencyNameTable currencies,
HtmlSanitizer htmlSanitizer,
EmailSenderFactory emailSenderFactory,
AppService AppService)
{
_UserManager = userManager;
@ -40,7 +38,6 @@ namespace BTCPayServer.Controllers
_NetworkProvider = networkProvider;
_currencies = currencies;
_htmlSanitizer = htmlSanitizer;
_emailSenderFactory = emailSenderFactory;
_AppService = AppService;
}
@ -50,7 +47,6 @@ namespace BTCPayServer.Controllers
private BTCPayNetworkProvider _NetworkProvider;
private readonly CurrencyNameTable _currencies;
private readonly HtmlSanitizer _htmlSanitizer;
private readonly EmailSenderFactory _emailSenderFactory;
private AppService _AppService;
[TempData]
@ -88,7 +84,7 @@ namespace BTCPayServer.Controllers
StatusMessage = new StatusMessageModel()
{
Html =
$"Error: You need to create at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}'>Create store</a>",
$"Error: You must have created at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}'>Create store</a>",
Severity = StatusMessageModel.StatusSeverity.Error
}.ToString();
return RedirectToAction(nameof(ListApps));
@ -108,7 +104,7 @@ namespace BTCPayServer.Controllers
StatusMessage = new StatusMessageModel()
{
Html =
$"Error: You need to create at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}'>Create store</a>",
$"Error: You must have created at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}'>Create store</a>",
Severity = StatusMessageModel.StatusSeverity.Error
}.ToString();
return RedirectToAction(nameof(ListApps));
@ -180,10 +176,5 @@ namespace BTCPayServer.Controllers
{
return _UserManager.GetUserId(User);
}
private async Task<bool> IsEmailConfigured(string storeId)
{
return (await (_emailSenderFactory.GetEmailSender(storeId) as EmailSender)?.GetEmailSettings())?.IsComplete() is true;
}
}
}

View File

@ -5,12 +5,9 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.ModelBinders;
using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Payments;
@ -33,19 +30,16 @@ namespace BTCPayServer.Controllers
{
public class AppsPublicController : Controller
{
public AppsPublicController(AppService AppService,
BTCPayServerOptions btcPayServerOptions,
public AppsPublicController(AppService AppService,
InvoiceController invoiceController,
UserManager<ApplicationUser> userManager)
{
_AppService = AppService;
_BtcPayServerOptions = btcPayServerOptions;
_InvoiceController = invoiceController;
_UserManager = userManager;
}
private AppService _AppService;
private readonly BTCPayServerOptions _BtcPayServerOptions;
private InvoiceController _InvoiceController;
private readonly UserManager<ApplicationUser> _UserManager;
@ -90,37 +84,35 @@ namespace BTCPayServer.Controllers
AppId = appId
});
}
[HttpGet]
[Route("/apps/{appId}/crowdfund")]
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
public async Task<IActionResult> ViewCrowdfund(string appId, string statusMessage)
{
var app = await _AppService.GetApp(appId, AppType.Crowdfund, true);
if (app == null)
return NotFound();
var settings = app.GetSettings<CrowdfundSettings>();
var isAdmin = await _AppService.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
var hasEnoughSettingsToLoad = !string.IsNullOrEmpty(settings.TargetCurrency);
var hasEnoughSettingsToLoad = !string.IsNullOrEmpty(settings.TargetCurrency );
if (!hasEnoughSettingsToLoad)
{
if (!isAdmin)
if(!isAdmin)
return NotFound();
return NotFound("A Target Currency must be set for this app in order to be loadable.");
}
var appInfo = (ViewCrowdfundViewModel)(await _AppService.GetAppInfo(appId));
appInfo.HubPath = AppHub.GetHubPath(this.Request);
if (settings.Enabled)
return View(appInfo);
if (!isAdmin)
if (settings.Enabled) return View(await _AppService.GetAppInfo(appId));
if(!isAdmin)
return NotFound();
return View(appInfo);
return View(await _AppService.GetAppInfo(appId));
}
[HttpPost]
@ -128,7 +120,7 @@ namespace BTCPayServer.Controllers
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
public async Task<IActionResult> ContributeToCrowdfund(string appId, ContributeToCrowdfund request, CancellationToken cancellationToken)
public async Task<IActionResult> ContributeToCrowdfund(string appId, ContributeToCrowdfund request)
{
var app = await _AppService.GetApp(appId, AppType.Crowdfund, true);
@ -139,13 +131,14 @@ namespace BTCPayServer.Controllers
var isAdmin = await _AppService.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
if (!settings.Enabled && !isAdmin)
if (!settings.Enabled)
{
return NotFound("Crowdfund is not currently active");
if (!isAdmin)
return NotFound("Crowdfund is not currently active");
}
var info = (ViewCrowdfundViewModel)await _AppService.GetAppInfo(appId);
info.HubPath = AppHub.GetHubPath(this.Request);
if (!isAdmin &&
((settings.StartDate.HasValue && DateTime.Now < settings.StartDate) ||
(settings.EndDate.HasValue && DateTime.Now > settings.EndDate) ||
@ -182,26 +175,22 @@ namespace BTCPayServer.Controllers
try
{
var invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
{
OrderId = AppService.GetCrowdfundOrderId(appId),
Currency = settings.TargetCurrency,
ItemCode = request.ChoiceKey ?? string.Empty,
ItemDesc = title,
BuyerEmail = request.Email,
Price = price,
NotificationURL = settings.NotificationUrl,
NotificationEmail = settings.NotificationEmail,
FullNotifications = true,
ExtendedNotifications = true,
RedirectURL = request.RedirectUrl ??
new Uri(new Uri( new Uri(HttpContext.Request.GetAbsoluteRoot()), _BtcPayServerOptions.RootPath), $"apps/{appId}/crowdfund").ToString()
}, store, HttpContext.Request.GetAbsoluteRoot(),
new List<string> {AppService.GetAppInternalTag(appId)},
cancellationToken: cancellationToken);
{
OrderId = AppService.GetCrowdfundOrderId(appId),
Currency = settings.TargetCurrency,
ItemCode = request.ChoiceKey ?? string.Empty,
ItemDesc = title,
BuyerEmail = request.Email,
Price = price,
NotificationURL = settings.NotificationUrl,
FullNotifications = true,
ExtendedNotifications = true,
RedirectURL = request.RedirectUrl ?? Request.GetDisplayUrl()
}, store, HttpContext.Request.GetAbsoluteRoot(), new List<string> { AppService.GetAppInternalTag(appId) });
if (request.RedirectToCheckout)
{
return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice",
new { invoiceId = invoice.Data.Id });
new {invoiceId = invoice.Data.Id});
}
else
{
@ -212,7 +201,7 @@ namespace BTCPayServer.Controllers
{
return BadRequest(e.Message);
}
}
[HttpPost]
@ -221,13 +210,13 @@ namespace BTCPayServer.Controllers
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
public async Task<IActionResult> ViewPointOfSale(string appId,
[ModelBinder(typeof(InvariantDecimalModelBinder))] decimal amount,
decimal amount,
string email,
string orderId,
string notificationUrl,
string redirectUrl,
string choiceKey,
string posData = null, CancellationToken cancellationToken = default)
string posData = null)
{
var app = await _AppService.GetApp(appId, AppType.PointOfSale);
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
@ -272,21 +261,15 @@ namespace BTCPayServer.Controllers
Price = price,
BuyerEmail = email,
OrderId = orderId,
NotificationURL =
string.IsNullOrEmpty(notificationUrl) ? settings.NotificationUrl : notificationUrl,
NotificationEmail = settings.NotificationEmail,
RedirectURL = redirectUrl ?? Request.GetDisplayUrl(),
NotificationURL = notificationUrl,
RedirectURL = redirectUrl ?? Request.GetDisplayUrl(),
FullNotifications = true,
ExtendedNotifications = true,
PosData = string.IsNullOrEmpty(posData) ? null : posData,
RedirectAutomatically = settings.RedirectAutomatically,
}, store, HttpContext.Request.GetAbsoluteRoot(),
new List<string>() { AppService.GetAppInternalTag(appId) },
cancellationToken);
PosData = string.IsNullOrEmpty(posData) ? null : posData
}, store, HttpContext.Request.GetAbsoluteRoot());
return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice", new { invoiceId = invoice.Data.Id });
}
private string GetUserId()
{
return _UserManager.GetUserId(User);

View File

@ -1,5 +1,4 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Models;
using BTCPayServer.Payments.Changelly;
@ -49,7 +48,7 @@ namespace BTCPayServer.Controllers
[HttpGet]
[Route("calculate")]
public async Task<IActionResult> CalculateAmount(string storeId, string fromCurrency, string toCurrency,
decimal toCurrencyAmount, CancellationToken cancellationToken)
decimal toCurrencyAmount)
{
try
{
@ -58,7 +57,7 @@ namespace BTCPayServer.Controllers
if (fromCurrency.Equals("usd", StringComparison.InvariantCultureIgnoreCase)
|| fromCurrency.Equals("eur", StringComparison.InvariantCultureIgnoreCase))
{
return await HandleCalculateFiatAmount(fromCurrency, toCurrency, toCurrencyAmount, cancellationToken);
return await HandleCalculateFiatAmount(fromCurrency, toCurrency, toCurrencyAmount);
}
var callCounter = 0;
@ -103,11 +102,11 @@ namespace BTCPayServer.Controllers
}
private async Task<IActionResult> HandleCalculateFiatAmount(string fromCurrency, string toCurrency,
decimal toCurrencyAmount, CancellationToken cancellationToken)
decimal toCurrencyAmount)
{
var store = HttpContext.GetStoreData();
var rules = store.GetStoreBlob().GetRateRules(_btcPayNetworkProvider);
var rate = await _RateProviderFactory.FetchRate(new CurrencyPair(toCurrency, fromCurrency), rules, cancellationToken);
var rate = await _RateProviderFactory.FetchRate(new CurrencyPair(toCurrency, fromCurrency), rules);
if (rate.BidAsk == null) return BadRequest();
var flatRate = rate.BidAsk.Center;
return Ok(flatRate * toCurrencyAmount);

View File

@ -1,46 +1,29 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using BTCPayServer.Models;
using NBitcoin.DataEncoders;
using NBitcoin.Payment;
using System.Net.Http;
using Newtonsoft.Json.Linq;
using NBitcoin;
using Newtonsoft.Json;
using BTCPayServer.Services;
using BTCPayServer.HostedServices;
namespace BTCPayServer.Controllers
{
public class HomeController : Controller
{
private readonly CssThemeManager _cachedServerSettings;
public IHttpClientFactory HttpClientFactory { get; }
public HomeController(IHttpClientFactory httpClientFactory, CssThemeManager cachedServerSettings)
public HomeController(IHttpClientFactory httpClientFactory)
{
HttpClientFactory = httpClientFactory;
_cachedServerSettings = cachedServerSettings;
}
public async Task<IActionResult> Index()
public IActionResult Index()
{
if (_cachedServerSettings.RootAppType is Services.Apps.AppType.Crowdfund)
{
var serviceProvider = HttpContext.RequestServices;
var controller = (AppsPublicController)serviceProvider.GetService(typeof(AppsPublicController));
controller.Url = Url;
controller.ControllerContext = ControllerContext;
var res = await controller.ViewCrowdfund(_cachedServerSettings.RootAppId, null) as ViewResult;
if (res != null)
{
res.ViewName = "/Views/AppsPublic/ViewCrowdfund.cshtml";
return res; // return
}
}
return View("Home");
}

View File

@ -1,6 +1,5 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Filters;
using BTCPayServer.Models;
@ -33,11 +32,11 @@ namespace BTCPayServer.Controllers
[HttpPost]
[Route("invoices")]
[MediaTypeConstraint("application/json")]
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] CreateInvoiceRequest invoice, CancellationToken cancellationToken)
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] CreateInvoiceRequest invoice)
{
if (invoice == null)
throw new BitpayHttpException(400, "Invalid invoice");
return await _InvoiceController.CreateInvoiceCore(invoice, HttpContext.GetStoreData(), HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
return await _InvoiceController.CreateInvoiceCore(invoice, HttpContext.GetStoreData(), HttpContext.Request.GetAbsoluteRoot());
}
[HttpGet]

View File

@ -19,7 +19,6 @@ using BTCPayServer.Security;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Invoices.Export;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore.Internal;
@ -47,9 +46,9 @@ namespace BTCPayServer.Controllers
if (invoice == null)
return NotFound();
var prodInfo = invoice.ProductInformation;
var dto = invoice.EntityToDTO(_NetworkProvider);
var store = await _StoreRepository.FindStore(invoice.StoreId);
var model = new InvoiceDetailsModel()
InvoiceDetailsModel model = new InvoiceDetailsModel()
{
StoreName = store.StoreName,
StoreLink = Url.Action(nameof(StoresController.UpdateStore), "Stores", new { storeId = store.Id }),
@ -65,62 +64,20 @@ namespace BTCPayServer.Controllers
MonitoringDate = invoice.MonitoringExpiration,
OrderId = invoice.OrderId,
BuyerInformation = invoice.BuyerInformation,
Fiat = _CurrencyNameTable.DisplayFormatCurrency(prodInfo.Price, prodInfo.Currency),
TaxIncluded = _CurrencyNameTable.DisplayFormatCurrency(prodInfo.TaxIncluded, prodInfo.Currency),
Fiat = _CurrencyNameTable.DisplayFormatCurrency(dto.Price, dto.Currency),
TaxIncluded = _CurrencyNameTable.DisplayFormatCurrency(invoice.ProductInformation.TaxIncluded, dto.Currency),
NotificationEmail = invoice.NotificationEmail,
NotificationUrl = invoice.NotificationURL,
RedirectUrl = invoice.RedirectURL,
ProductInformation = invoice.ProductInformation,
StatusException = invoice.ExceptionStatus,
Events = invoice.Events,
PosData = PosDataParser.ParsePosData(invoice.PosData),
StatusMessage = StatusMessage
PosData = PosDataParser.ParsePosData(dto.PosData)
};
model.Addresses = invoice.HistoricalAddresses.Select(h => new InvoiceDetailsModel.AddressModel
{
Destination = h.GetAddress(),
PaymentMethod = ToString(h.GetPaymentMethodId()),
Current = !h.UnAssigned.HasValue
}).ToArray();
var updateConfirmationCountIfNeeded = invoice
.GetPayments()
.Select<PaymentEntity, Task>(async payment =>
{
var paymentNetwork = _NetworkProvider.GetNetwork(payment.GetCryptoCode());
var paymentData = payment.GetCryptoPaymentData();
if (paymentData is Payments.Bitcoin.BitcoinLikePaymentData onChainPaymentData)
{
int confirmationCount = 0;
if ((onChainPaymentData.ConfirmationCount < paymentNetwork.MaxTrackedConfirmation && payment.Accounted)
&& (onChainPaymentData.Legacy || invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
// The confirmation count in the paymentData is not up to date
{
confirmationCount = (await ((ExplorerClientProvider)_ServiceProvider.GetService(typeof(ExplorerClientProvider))).GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(onChainPaymentData.Outpoint.Hash))?.Confirmations ?? 0;
onChainPaymentData.ConfirmationCount = confirmationCount;
payment.SetCryptoPaymentData(onChainPaymentData);
await _InvoiceRepository.UpdatePayments(new List<PaymentEntity> { payment });
}
}
})
.ToArray();
await Task.WhenAll(updateConfirmationCountIfNeeded);
var details = InvoicePopulatePayments(invoice);
model.CryptoPayments = details.CryptoPayments;
model.OnChainPayments = details.OnChainPayments;
model.OffChainPayments = details.OffChainPayments;
return View(model);
}
private InvoiceDetailsModel InvoicePopulatePayments(InvoiceEntity invoice)
{
var model = new InvoiceDetailsModel();
foreach (var data in invoice.GetPaymentMethods(null))
{
var cryptoInfo = dto.CryptoInfo.First(o => o.GetpaymentMethodId() == data.GetId());
var accounting = data.Calculate();
var paymentMethodId = data.GetId();
var cryptoPayment = new InvoiceDetailsModel.CryptoPayment();
@ -135,46 +92,72 @@ namespace BTCPayServer.Controllers
cryptoPayment.Address = onchainMethod.DepositAddress;
}
cryptoPayment.Rate = ExchangeRate(data);
cryptoPayment.PaymentUrl = cryptoInfo.PaymentUrls.BIP21;
model.CryptoPayments.Add(cryptoPayment);
}
foreach (var payment in invoice.GetPayments())
{
var paymentNetwork = _NetworkProvider.GetNetwork(payment.GetCryptoCode());
var paymentData = payment.GetCryptoPaymentData();
if (paymentData is Payments.Bitcoin.BitcoinLikePaymentData onChainPaymentData)
var onChainPayments = invoice
.GetPayments()
.Select<PaymentEntity, Task<object>>(async payment =>
{
var m = new InvoiceDetailsModel.Payment();
m.Crypto = payment.GetPaymentMethodId().CryptoCode;
m.DepositAddress = onChainPaymentData.GetDestination(paymentNetwork);
int confirmationCount = onChainPaymentData.ConfirmationCount;
if (confirmationCount >= paymentNetwork.MaxTrackedConfirmation)
var paymentNetwork = _NetworkProvider.GetNetwork(payment.GetCryptoCode());
var paymentData = payment.GetCryptoPaymentData();
if (paymentData is Payments.Bitcoin.BitcoinLikePaymentData onChainPaymentData)
{
m.Confirmations = "At least " + (paymentNetwork.MaxTrackedConfirmation);
var m = new InvoiceDetailsModel.Payment();
m.Crypto = payment.GetPaymentMethodId().CryptoCode;
m.DepositAddress = onChainPaymentData.GetDestination(paymentNetwork);
int confirmationCount = 0;
if ((onChainPaymentData.ConfirmationCount < paymentNetwork.MaxTrackedConfirmation && payment.Accounted)
&& (onChainPaymentData.Legacy || invoice.MonitoringExpiration < DateTimeOffset.UtcNow)) // The confirmation count in the paymentData is not up to date
{
confirmationCount = (await ((ExplorerClientProvider)_ServiceProvider.GetService(typeof(ExplorerClientProvider))).GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(onChainPaymentData.Outpoint.Hash))?.Confirmations ?? 0;
onChainPaymentData.ConfirmationCount = confirmationCount;
payment.SetCryptoPaymentData(onChainPaymentData);
await _InvoiceRepository.UpdatePayments(new List<PaymentEntity> { payment });
}
else
{
confirmationCount = onChainPaymentData.ConfirmationCount;
}
if (confirmationCount >= paymentNetwork.MaxTrackedConfirmation)
{
m.Confirmations = "At least " + (paymentNetwork.MaxTrackedConfirmation);
}
else
{
m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture);
}
m.TransactionId = onChainPaymentData.Outpoint.Hash.ToString();
m.ReceivedTime = payment.ReceivedTime;
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, paymentNetwork.BlockExplorerLink, m.TransactionId);
m.Replaced = !payment.Accounted;
return m;
}
else
{
m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture);
var lightningPaymentData = (Payments.Lightning.LightningLikePaymentData)paymentData;
return new InvoiceDetailsModel.OffChainPayment()
{
Crypto = paymentNetwork.CryptoCode,
BOLT11 = lightningPaymentData.BOLT11
};
}
m.TransactionId = onChainPaymentData.Outpoint.Hash.ToString();
m.ReceivedTime = payment.ReceivedTime;
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, paymentNetwork.BlockExplorerLink, m.TransactionId);
m.Replaced = !payment.Accounted;
model.OnChainPayments.Add(m);
}
else
{
var lightningPaymentData = (LightningLikePaymentData)paymentData;
model.OffChainPayments.Add(new InvoiceDetailsModel.OffChainPayment()
{
Crypto = paymentNetwork.CryptoCode,
BOLT11 = lightningPaymentData.BOLT11
});
}
}
return model;
})
.ToArray();
await Task.WhenAll(onChainPayments);
model.Addresses = invoice.HistoricalAddresses.Select(h => new InvoiceDetailsModel.AddressModel
{
Destination = h.GetAddress(),
PaymentMethod = ToString(h.GetPaymentMethodId()),
Current = !h.UnAssigned.HasValue
}).ToArray();
model.OnChainPayments = onChainPayments.Select(p => p.GetAwaiter().GetResult()).OfType<InvoiceDetailsModel.Payment>().ToList();
model.OffChainPayments = onChainPayments.Select(p => p.GetAwaiter().GetResult()).OfType<InvoiceDetailsModel.OffChainPayment>().ToList();
model.StatusMessage = StatusMessage;
return View(model);
}
private string ToString(PaymentMethodId paymentMethodId)
@ -205,7 +188,7 @@ namespace BTCPayServer.Controllers
//Keep compatibility with Bitpay
invoiceId = invoiceId ?? id;
id = invoiceId;
//
////
var model = await GetInvoiceModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId));
if (model == null)
@ -230,23 +213,6 @@ namespace BTCPayServer.Controllers
return View(nameof(Checkout), model);
}
[HttpGet]
[Route("invoice-noscript")]
public async Task<IActionResult> CheckoutNoScript(string invoiceId, string id = null, string paymentMethodId = null)
{
//Keep compatibility with Bitpay
invoiceId = invoiceId ?? id;
id = invoiceId;
//
var model = await GetInvoiceModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId));
if (model == null)
return NotFound();
return View(model);
}
private async Task<PaymentModel> GetInvoiceModel(string invoiceId, PaymentMethodId paymentMethodId)
{
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
@ -292,7 +258,7 @@ namespace BTCPayServer.Controllers
storeBlob.ChangellySettings.IsConfigured())
? storeBlob.ChangellySettings
: null;
CoinSwitchSettings coinswitch = (storeBlob.CoinSwitchSettings != null && storeBlob.CoinSwitchSettings.Enabled &&
storeBlob.CoinSwitchSettings.IsConfigured())
? storeBlob.CoinSwitchSettings
@ -307,7 +273,6 @@ namespace BTCPayServer.Controllers
var model = new PaymentModel()
{
CryptoCode = network.CryptoCode,
RootPath = this.Request.PathBase.Value.WithTrailingSlash(),
PaymentMethodId = paymentMethodId.ToString(),
PaymentMethodName = GetDisplayName(paymentMethodId, network),
CryptoImage = GetImage(paymentMethodId, network),
@ -318,7 +283,6 @@ namespace BTCPayServer.Controllers
HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice",
CustomCSSLink = storeBlob.CustomCSS?.AbsoluteUri,
CustomLogoLink = storeBlob.CustomLogo?.AbsoluteUri,
LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi,
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
BtcDue = accounting.Due.ToString(),
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
@ -331,7 +295,6 @@ namespace BTCPayServer.Controllers
ItemDesc = invoice.ProductInformation.ItemDesc,
Rate = ExchangeRate(paymentMethod),
MerchantRefLink = invoice.RedirectURL ?? "/",
RedirectAutomatically = invoice.RedirectAutomatically,
StoreName = store.StoreName,
InvoiceBitcoinUrl = paymentMethodId.PaymentType == PaymentTypes.BTCLike ? cryptoInfo.PaymentUrls.BIP21 :
paymentMethodId.PaymentType == PaymentTypes.LightningLike ? cryptoInfo.PaymentUrls.BOLT11 :
@ -351,7 +314,6 @@ namespace BTCPayServer.Controllers
ChangellyMerchantId = changelly?.ChangellyMerchantId,
ChangellyAmountDue = changellyAmountDue,
CoinSwitchEnabled = coinswitch != null,
CoinSwitchAmountMarkupPercentage = coinswitch?.AmountMarkupPercentage ?? 0,
CoinSwitchMerchantId = coinswitch?.MerchantId,
CoinSwitchMode = coinswitch?.Mode,
StoreId = store.Id,
@ -383,8 +345,9 @@ namespace BTCPayServer.Controllers
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
{
return paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
this.Request.GetRelativePathOrAbsolute(network.CryptoImagePath) : this.Request.GetRelativePathOrAbsolute(network.LightningImagePath);
var res = paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
Url.Content(network.CryptoImagePath) : Url.Content(network.LightningImagePath);
return "/" + res;
}
private string OrderAmountFromInvoice(string cryptoCode, ProductInformation productInformation)
@ -426,7 +389,7 @@ namespace BTCPayServer.Controllers
if (!HttpContext.WebSockets.IsWebSocketRequest)
return NotFound();
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
if (invoice == null || invoice.Status == InvoiceStatus.Complete || invoice.Status == InvoiceStatus.Invalid || invoice.Status == InvoiceStatus.Expired)
if (invoice == null || invoice.Status == InvoiceStatus.Complete || invoice.Status == InvoiceStatus.Invalid || invoice.Status == InvoiceStatus.Expired)
return NotFound();
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
CompositeDisposable leases = new CompositeDisposable();
@ -474,36 +437,34 @@ namespace BTCPayServer.Controllers
return BadRequest(ModelState);
}
await _InvoiceRepository.UpdateInvoice(invoiceId, data).ConfigureAwait(false);
return Ok("{}");
return Ok();
}
[HttpGet]
[Route("invoices")]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 50, int timezoneOffset = 0)
public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 50)
{
var model = new InvoicesModel
{
SearchTerm = searchTerm,
Skip = skip,
Count = count,
StatusMessage = StatusMessage,
TimezoneOffset = timezoneOffset
StatusMessage = StatusMessage
};
InvoiceQuery invoiceQuery = GetInvoiceQuery(searchTerm, timezoneOffset);
InvoiceQuery invoiceQuery = GetInvoiceQuery(searchTerm);
var counting = _InvoiceRepository.GetInvoicesTotal(invoiceQuery);
invoiceQuery.Count = count;
invoiceQuery.Skip = skip;
var list = await _InvoiceRepository.GetInvoices(invoiceQuery);
foreach (var invoice in list)
{
var state = invoice.GetInvoiceState();
model.Invoices.Add(new InvoiceModel()
{
Status = invoice.Status,
StatusString = state.ToString(),
Status = state.ToString(),
ShowCheckout = invoice.Status == InvoiceStatus.New,
Date = invoice.InvoiceTime,
InvoiceId = invoice.Id,
@ -511,29 +472,28 @@ namespace BTCPayServer.Controllers
RedirectUrl = invoice.RedirectURL ?? string.Empty,
AmountCurrency = _CurrencyNameTable.DisplayFormatCurrency(invoice.ProductInformation.Price, invoice.ProductInformation.Currency),
CanMarkInvalid = state.CanMarkInvalid(),
CanMarkComplete = state.CanMarkComplete(),
Details = InvoicePopulatePayments(invoice)
CanMarkComplete = state.CanMarkComplete()
});
}
model.Total = await counting;
return View(model);
}
private InvoiceQuery GetInvoiceQuery(string searchTerm = null, int timezoneOffset = 0)
private InvoiceQuery GetInvoiceQuery(string searchTerm = null)
{
var fs = new SearchString(searchTerm);
var filterString = new SearchString(searchTerm);
var invoiceQuery = new InvoiceQuery()
{
TextSearch = fs.TextSearch,
TextSearch = filterString.TextSearch,
UserId = GetUserId(),
Unusual = fs.GetFilterBool("unusual"),
Status = fs.GetFilterArray("status"),
ExceptionStatus = fs.GetFilterArray("exceptionstatus"),
StoreId = fs.GetFilterArray("storeid"),
ItemCode = fs.GetFilterArray("itemcode"),
OrderId = fs.GetFilterArray("orderid"),
StartDate = fs.GetFilterDate("startdate", timezoneOffset),
EndDate = fs.GetFilterDate("enddate", timezoneOffset)
Unusual = !filterString.Filters.ContainsKey("unusual") ? null
: !bool.TryParse(filterString.Filters["unusual"].First(), out var r) ? (bool?)null
: r,
Status = filterString.Filters.ContainsKey("status") ? filterString.Filters["status"].ToArray() : null,
ExceptionStatus = filterString.Filters.ContainsKey("exceptionstatus") ? filterString.Filters["exceptionstatus"].ToArray() : null,
StoreId = filterString.Filters.ContainsKey("storeid") ? filterString.Filters["storeid"].ToArray() : null,
ItemCode = filterString.Filters.ContainsKey("itemcode") ? filterString.Filters["itemcode"].ToArray() : null,
OrderId = filterString.Filters.ContainsKey("orderid") ? filterString.Filters["orderid"].ToArray() : null
};
return invoiceQuery;
}
@ -541,13 +501,13 @@ namespace BTCPayServer.Controllers
[HttpGet]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> Export(string format, string searchTerm = null, int timezoneOffset = 0)
public async Task<IActionResult> Export(string format, string searchTerm = null)
{
var model = new InvoiceExport(_NetworkProvider, _CurrencyNameTable);
InvoiceQuery invoiceQuery = GetInvoiceQuery(searchTerm, timezoneOffset);
invoiceQuery.Skip = 0;
InvoiceQuery invoiceQuery = GetInvoiceQuery(searchTerm);
invoiceQuery.Count = int.MaxValue;
invoiceQuery.Skip = 0;
var invoices = await _InvoiceRepository.GetInvoices(invoiceQuery);
var res = model.Process(invoices, format);
@ -575,36 +535,17 @@ namespace BTCPayServer.Controllers
StatusMessage = "Error: You need to create at least one store before creating a transaction";
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
}
var paymentMethods = new SelectList(_NetworkProvider.GetAll().SelectMany(network => new[]
{
new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike),
new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike)
}).Select(id => new SelectListItem(id.ToString(true), id.ToString(false))),
nameof(SelectListItem.Value),
nameof(SelectListItem.Text));
return View(new CreateInvoiceModel() { Stores = stores, AvailablePaymentMethods = paymentMethods });
return View(new CreateInvoiceModel() { Stores = stores });
}
[HttpPost]
[Route("invoices/create")]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model, CancellationToken cancellationToken)
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model)
{
var stores = await _StoreRepository.GetStoresByUserId(GetUserId());
model.Stores = new SelectList(stores, nameof(StoreData.Id), nameof(StoreData.StoreName), model.StoreId);
var paymentMethods = new SelectList(_NetworkProvider.GetAll().SelectMany(network => new[]
{
new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike),
new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike)
}).Select(id => new SelectListItem(id.ToString(true), id.ToString(false))),
nameof(SelectListItem.Value),
nameof(SelectListItem.Text));
model.AvailablePaymentMethods = paymentMethods;
var store = stores.FirstOrDefault(s => s.Id == model.StoreId);
if (store == null)
{
@ -627,7 +568,6 @@ namespace BTCPayServer.Controllers
return View(model);
}
if (StatusMessage != null)
{
return RedirectToAction(nameof(StoresController.UpdateStore), "Stores", new
@ -650,11 +590,7 @@ namespace BTCPayServer.Controllers
ItemDesc = model.ItemDesc,
FullNotifications = true,
BuyerEmail = model.BuyerEmail,
SupportedTransactionCurrencies = model.SupportedTransactionCurrencies?.ToDictionary(s => s, s => new InvoiceSupportedTransactionCurrency()
{
Enabled = true
})
}, store, HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
}, store, HttpContext.Request.GetAbsoluteRoot());
StatusMessage = $"Invoice {result.Data.Id} just created!";
return RedirectToAction(nameof(ListInvoices));
@ -667,45 +603,73 @@ namespace BTCPayServer.Controllers
}
[HttpPost]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[BitpayAPIConstraint(false)]
public IActionResult SearchInvoice(InvoicesModel invoices)
{
return RedirectToAction(nameof(ListInvoices), new
{
searchTerm = invoices.SearchTerm,
skip = invoices.Skip,
count = invoices.Count,
});
}
[HttpGet]
[Route("invoices/{invoiceId}/changestate/{newState}")]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> ChangeInvoiceState(string invoiceId, string newState)
public IActionResult ChangeInvoiceState(string invoiceId, string newState)
{
if (newState == "invalid")
{
return View("Confirm", new ConfirmModel()
{
Action = "Make invoice invalid",
Title = "Change invoice state",
Description = $"You will transition the state of this invoice to \"invalid\", do you want to continue?",
});
}
else if (newState == "complete")
{
return View("Confirm", new ConfirmModel()
{
Action = "Make invoice complete",
Title = "Change invoice state",
Description = $"You will transition the state of this invoice to \"complete\", do you want to continue?",
ButtonClass = "btn-primary"
});
}
else
return NotFound();
}
[HttpPost]
[Route("invoices/{invoiceId}/changestate/{newState}")]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> ChangeInvoiceStateConfirm(string invoiceId, string newState)
{
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery()
{
InvoiceId = invoiceId,
UserId = GetUserId()
})).FirstOrDefault();
var model = new InvoiceStateChangeModel();
if (invoice == null)
{
model.NotFound = true;
return NotFound(model);
}
return NotFound();
if (newState == "invalid")
{
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId);
_EventAggregator.Publish(new InvoiceEvent(invoice, 1008, InvoiceEvent.MarkedInvalid));
model.StatusString = new InvoiceState("invalid", "marked").ToString();
StatusMessage = "Invoice marked invalid";
}
else if (newState == "complete")
else if(newState == "complete")
{
await _InvoiceRepository.UpdatePaidInvoiceToComplete(invoiceId);
_EventAggregator.Publish(new InvoiceEvent(invoice, 2008, InvoiceEvent.MarkedCompleted));
model.StatusString = new InvoiceState("complete", "marked").ToString();
StatusMessage = "Invoice marked complete";
}
return Json(model);
}
public class InvoiceStateChangeModel
{
public bool NotFound { get; set; }
public string StatusString { get; set; }
return RedirectToAction(nameof(ListInvoices));
}
[TempData]
@ -724,18 +688,18 @@ namespace BTCPayServer.Controllers
{
public static Dictionary<string, object> ParsePosData(string posData)
{
var result = new Dictionary<string, object>();
var result = new Dictionary<string,object>();
if (string.IsNullOrEmpty(posData))
{
return result;
}
try
{
var jObject = JObject.Parse(posData);
var jObject =JObject.Parse(posData);
foreach (var item in jObject)
{
switch (item.Value.Type)
{
case JTokenType.Array:
@ -752,7 +716,7 @@ namespace BTCPayServer.Controllers
result.Add(item.Key, item.Value.ToString());
break;
}
}
}
catch
@ -762,6 +726,6 @@ namespace BTCPayServer.Controllers
return result;
}
}
}
}

View File

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Events;
@ -63,7 +62,7 @@ namespace BTCPayServer.Controllers
}
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string> additionalTags = null, CancellationToken cancellationToken = default)
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string> additionalTags = null)
{
if (!store.HasClaim(Policies.CanCreateInvoice.Key))
throw new UnauthorizedAccessException();
@ -84,7 +83,6 @@ namespace BTCPayServer.Controllers
entity.ServerUrl = serverUrl;
entity.FullNotifications = invoice.FullNotifications || invoice.ExtendedNotifications;
entity.ExtendedNotifications = invoice.ExtendedNotifications;
if (invoice.NotificationURL != null &&
Uri.TryCreate(invoice.NotificationURL, UriKind.Absolute, out var notificationUri) &&
(notificationUri.Scheme == "http" || notificationUri.Scheme == "https"))
@ -106,7 +104,7 @@ namespace BTCPayServer.Controllers
}
var taxIncluded = invoice.TaxIncluded.HasValue ? invoice.TaxIncluded.Value : 0m;
var currencyInfo = _CurrencyNameTable.GetNumberFormatInfo(invoice.Currency, false);
if (currencyInfo != null)
{
@ -126,9 +124,6 @@ namespace BTCPayServer.Controllers
if (!Uri.IsWellFormedUriString(entity.RedirectURL, UriKind.Absolute))
entity.RedirectURL = null;
entity.RedirectAutomatically =
invoice.RedirectAutomatically.GetValueOrDefault(storeBlob.RedirectAutomatically);
entity.Status = InvoiceStatus.New;
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
@ -142,7 +137,7 @@ namespace BTCPayServer.Controllers
.Where(c => c.Value.Enabled)
.Select(c => PaymentMethodId.TryParse(c.Key, out var p) ? p : null)
.ToHashSet();
excludeFilter = PaymentFilter.Or(excludeFilter,
excludeFilter = PaymentFilter.Or(excludeFilter,
PaymentFilter.Where(p => !supportedTransactionCurrencies.Contains(p)));
}
@ -159,7 +154,8 @@ namespace BTCPayServer.Controllers
}
var rateRules = storeBlob.GetRateRules(_NetworkProvider);
var fetchingByCurrencyPair = _RateProvider.FetchRates(currencyPairsToFetch, rateRules, cancellationToken);
var fetchingByCurrencyPair = _RateProvider.FetchRates(currencyPairsToFetch, rateRules);
var fetchingAll = WhenAllFetched(logs, fetchingByCurrencyPair);
var supportedPaymentMethods = store.GetSupportedPaymentMethods(_NetworkProvider)
.Where(s => !excludeFilter.Match(s.PaymentId))
@ -203,22 +199,8 @@ namespace BTCPayServer.Controllers
entity.InternalTags.Add(AppService.GetAppInternalTag(app.Id));
}
using (logs.Measure("Saving invoice"))
{
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, _NetworkProvider);
}
_ = Task.Run(async () =>
{
try
{
await fetchingAll;
}
catch (AggregateException ex)
{
ex.Handle(e => { logs.Write($"Error while fetching rates {ex}"); return true; });
}
await _InvoiceRepository.AddInvoiceLogs(entity.Id, logs);
});
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, logs, _NetworkProvider);
await fetchingAll;
_EventAggregator.Publish(new Events.InvoiceEvent(entity, 1001, InvoiceEvent.Created));
var resp = entity.EntityToDTO(_NetworkProvider);
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
@ -247,7 +229,6 @@ namespace BTCPayServer.Controllers
{
try
{
var logPrefix = $"{supportedPaymentMethod.PaymentId.ToString(true)}:";
var storeBlob = store.GetStoreBlob();
var preparePayment = handler.PreparePayment(supportedPaymentMethod, store, network);
var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.ProductInformation.Currency)];
@ -260,13 +241,8 @@ namespace BTCPayServer.Controllers
paymentMethod.Network = network;
paymentMethod.SetId(supportedPaymentMethod.PaymentId);
paymentMethod.Rate = rate.BidAsk.Bid;
paymentMethod.PreferOnion = this.Request.IsOnion();
using (logs.Measure($"{logPrefix} Payment method details creation"))
{
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network, preparePayment);
paymentMethod.SetPaymentMethodDetails(paymentDetails);
}
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network, preparePayment);
paymentMethod.SetPaymentMethodDetails(paymentDetails);
Func<Money, Money, bool> compare = null;
CurrencyValue limitValue = null;
@ -294,7 +270,7 @@ namespace BTCPayServer.Controllers
var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate.BidAsk.Bid);
if (compare(paymentMethod.Calculate().Due, limitValueCrypto))
{
logs.Write($"{logPrefix} {errorMessage}");
logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: {errorMessage}");
return null;
}
}

View File

@ -25,7 +25,7 @@ namespace BTCPayServer.Controllers
throw new ArgumentNullException(nameof(directoryPath));
Macaroons macaroons = new Macaroons();
if (!Directory.Exists(directoryPath))
throw new DirectoryNotFoundException("Macaroons directory not found");
return macaroons;
foreach(var file in Directory.GetFiles(directoryPath, "*.macaroon"))
{
try
@ -49,17 +49,6 @@ namespace BTCPayServer.Controllers
}
return macaroons;
}
public Macaroons Clone()
{
return new Macaroons()
{
AdminMacaroon = AdminMacaroon,
InvoiceMacaroon = InvoiceMacaroon,
ReadonlyMacaroon = ReadonlyMacaroon
};
}
public Macaroon ReadonlyMacaroon { get; set; }
public Macaroon InvoiceMacaroon { get; set; }

View File

@ -1,205 +0,0 @@
using System;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Models.ManageViewModels;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace BTCPayServer.Controllers
{
public partial class ManageController
{
private const string AuthenicatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6";
[HttpGet]
public async Task<IActionResult> TwoFactorAuthentication()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var model = new TwoFactorAuthenticationViewModel
{
HasAuthenticator = await _userManager.GetAuthenticatorKeyAsync(user) != null,
Is2faEnabled = user.TwoFactorEnabled,
RecoveryCodesLeft = await _userManager.CountRecoveryCodesAsync(user),
};
return View(model);
}
[HttpGet]
public async Task<IActionResult> Disable2faWarning()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
if (!user.TwoFactorEnabled)
{
throw new ApplicationException(
$"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'.");
}
return View(nameof(Disable2fa));
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Disable2fa()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false);
if (!disable2faResult.Succeeded)
{
throw new ApplicationException(
$"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'.");
}
_logger.LogInformation("User with ID {UserId} has disabled 2fa.", user.Id);
return RedirectToAction(nameof(TwoFactorAuthentication));
}
[HttpGet]
public async Task<IActionResult> EnableAuthenticator()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
if (string.IsNullOrEmpty(unformattedKey))
{
await _userManager.ResetAuthenticatorKeyAsync(user);
unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
}
var model = new EnableAuthenticatorViewModel
{
SharedKey = FormatKey(unformattedKey),
AuthenticatorUri = GenerateQrCodeUri(user.Email, unformattedKey)
};
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EnableAuthenticator(EnableAuthenticatorViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
// Strip spaces and hypens
var verificationCode = model.Code.Replace(" ", string.Empty, StringComparison.InvariantCulture)
.Replace("-", string.Empty, StringComparison.InvariantCulture);
var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync(
user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);
if (!is2faTokenValid)
{
ModelState.AddModelError(nameof(model.Code), "Verification code is invalid.");
return View(model);
}
await _userManager.SetTwoFactorEnabledAsync(user, true);
_logger.LogInformation("User with ID {UserId} has enabled 2FA with an authenticator app.", user.Id);
return RedirectToAction(nameof(GenerateRecoveryCodes));
}
[HttpGet]
public IActionResult ResetAuthenticatorWarning()
{
return View(nameof(ResetAuthenticator));
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ResetAuthenticator()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
await _userManager.SetTwoFactorEnabledAsync(user, false);
await _userManager.ResetAuthenticatorKeyAsync(user);
_logger.LogInformation("User with id '{UserId}' has reset their authentication app key.", user.Id);
return RedirectToAction(nameof(EnableAuthenticator));
}
[HttpGet]
public async Task<IActionResult> GenerateRecoveryCodes()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
if (!user.TwoFactorEnabled)
{
throw new ApplicationException(
$"Cannot generate recovery codes for user with ID '{user.Id}' as they do not have 2FA enabled.");
}
var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
var model = new GenerateRecoveryCodesViewModel {RecoveryCodes = recoveryCodes.ToArray()};
_logger.LogInformation("User with ID {UserId} has generated new 2FA recovery codes.", user.Id);
return View(model);
}
private string GenerateQrCodeUri(string email, string unformattedKey)
{
return string.Format(CultureInfo.InvariantCulture,
AuthenicatorUriFormat,
_urlEncoder.Encode("BTCPayServer"),
_urlEncoder.Encode(email),
unformattedKey);
}
private string FormatKey(string unformattedKey)
{
var result = new StringBuilder();
int currentPosition = 0;
while (currentPosition + 4 < unformattedKey.Length)
{
result.Append(unformattedKey.Substring(currentPosition, 4)).Append(" ");
currentPosition += 4;
}
if (currentPosition < unformattedKey.Length)
{
result.Append(unformattedKey.Substring(currentPosition));
}
return result.ToString().ToLowerInvariant();
}
}
}

View File

@ -1,89 +0,0 @@
using System;
using System.Threading.Tasks;
using BTCPayServer.Models;
using BTCPayServer.Services.U2F.Models;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers
{
public partial class ManageController
{
[HttpGet]
public async Task<IActionResult> U2FAuthentication(string statusMessage = null)
{
return View(new U2FAuthenticationViewModel()
{
StatusMessage = statusMessage,
Devices = await _u2FService.GetDevices(_userManager.GetUserId(User))
});
}
[HttpGet]
public async Task<IActionResult> RemoveU2FDevice(string id)
{
await _u2FService.RemoveDevice(id, _userManager.GetUserId(User));
return RedirectToAction("U2FAuthentication", new
{
StatusMessage = "Device removed"
});
}
[HttpGet]
public IActionResult AddU2FDevice(string name)
{
if (!_btcPayServerEnvironment.IsSecure)
{
return RedirectToAction("U2FAuthentication", new
{
StatusMessage = new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = "Cannot register U2F device while not on https or tor"
}
});
}
var serverRegisterResponse = _u2FService.StartDeviceRegistration(_userManager.GetUserId(User),
Request.GetAbsoluteUriNoPathBase().ToString().TrimEnd('/'));
return View(new AddU2FDeviceViewModel()
{
AppId = serverRegisterResponse.AppId,
Challenge = serverRegisterResponse.Challenge,
Version = serverRegisterResponse.Version,
Name = name
});
}
[HttpPost]
public async Task<IActionResult> AddU2FDevice(AddU2FDeviceViewModel viewModel)
{
var errorMessage = string.Empty;
try
{
if (await _u2FService.CompleteRegistration(_userManager.GetUserId(User), viewModel.DeviceResponse,
string.IsNullOrEmpty(viewModel.Name) ? "Unlabelled U2F Device" : viewModel.Name))
{
return RedirectToAction("U2FAuthentication", new
{
StatusMessage = "Device added!"
});
}
}
catch (Exception e)
{
errorMessage = e.Message;
}
return RedirectToAction("U2FAuthentication", new
{
StatusMessage = new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = string.IsNullOrEmpty(errorMessage) ? "Could not add device." : errorMessage
}
});
}
}
}

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
@ -8,23 +9,25 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using BTCPayServer.Models;
using BTCPayServer.Models.ManageViewModels;
using BTCPayServer.Services;
using BTCPayServer.Authentication;
using Microsoft.AspNetCore.Hosting;
using NBitpayClient;
using NBitcoin;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Services.Mails;
using System.Globalization;
using BTCPayServer.Security;
using BTCPayServer.Services.U2F;
namespace BTCPayServer.Controllers
{
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[Route("[controller]/[action]")]
public partial class ManageController : Controller
public class ManageController : Controller
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
@ -33,11 +36,10 @@ namespace BTCPayServer.Controllers
private readonly UrlEncoder _urlEncoder;
TokenRepository _TokenRepository;
IHostingEnvironment _Env;
private readonly U2FService _u2FService;
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
StoreRepository _StoreRepository;
private const string AuthenicatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6";
public ManageController(
UserManager<ApplicationUser> userManager,
@ -48,9 +50,7 @@ namespace BTCPayServer.Controllers
TokenRepository tokenRepository,
BTCPayWalletProvider walletProvider,
StoreRepository storeRepository,
IHostingEnvironment env,
U2FService u2FService,
BTCPayServerEnvironment btcPayServerEnvironment)
IHostingEnvironment env)
{
_userManager = userManager;
_signInManager = signInManager;
@ -59,8 +59,6 @@ namespace BTCPayServer.Controllers
_urlEncoder = urlEncoder;
_TokenRepository = tokenRepository;
_Env = env;
_u2FService = u2FService;
_btcPayServerEnvironment = btcPayServerEnvironment;
_StoreRepository = storeRepository;
}
@ -115,7 +113,6 @@ namespace BTCPayServer.Controllers
{
throw new ApplicationException($"Unexpected error occurred setting email for user with ID '{user.Id}'.");
}
await _userManager.SetUserNameAsync(user, model.Username);
}
var phoneNumber = user.PhoneNumber;
@ -341,6 +338,163 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(ExternalLogins));
}
[HttpGet]
public async Task<IActionResult> TwoFactorAuthentication()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var model = new TwoFactorAuthenticationViewModel
{
HasAuthenticator = await _userManager.GetAuthenticatorKeyAsync(user) != null,
Is2faEnabled = user.TwoFactorEnabled,
RecoveryCodesLeft = await _userManager.CountRecoveryCodesAsync(user),
};
return View(model);
}
[HttpGet]
public async Task<IActionResult> Disable2faWarning()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
if (!user.TwoFactorEnabled)
{
throw new ApplicationException($"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'.");
}
return View(nameof(Disable2fa));
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Disable2fa()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false);
if (!disable2faResult.Succeeded)
{
throw new ApplicationException($"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'.");
}
_logger.LogInformation("User with ID {UserId} has disabled 2fa.", user.Id);
return RedirectToAction(nameof(TwoFactorAuthentication));
}
[HttpGet]
public async Task<IActionResult> EnableAuthenticator()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
if (string.IsNullOrEmpty(unformattedKey))
{
await _userManager.ResetAuthenticatorKeyAsync(user);
unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
}
var model = new EnableAuthenticatorViewModel
{
SharedKey = FormatKey(unformattedKey),
AuthenticatorUri = GenerateQrCodeUri(user.Email, unformattedKey)
};
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EnableAuthenticator(EnableAuthenticatorViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
// Strip spaces and hypens
var verificationCode = model.Code.Replace(" ", string.Empty, StringComparison.InvariantCulture).Replace("-", string.Empty, StringComparison.InvariantCulture);
var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync(
user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);
if (!is2faTokenValid)
{
ModelState.AddModelError(nameof(model.Code), "Verification code is invalid.");
return View(model);
}
await _userManager.SetTwoFactorEnabledAsync(user, true);
_logger.LogInformation("User with ID {UserId} has enabled 2FA with an authenticator app.", user.Id);
return RedirectToAction(nameof(GenerateRecoveryCodes));
}
[HttpGet]
public IActionResult ResetAuthenticatorWarning()
{
return View(nameof(ResetAuthenticator));
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ResetAuthenticator()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
await _userManager.SetTwoFactorEnabledAsync(user, false);
await _userManager.ResetAuthenticatorKeyAsync(user);
_logger.LogInformation("User with id '{UserId}' has reset their authentication app key.", user.Id);
return RedirectToAction(nameof(EnableAuthenticator));
}
[HttpGet]
public async Task<IActionResult> GenerateRecoveryCodes()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
if (!user.TwoFactorEnabled)
{
throw new ApplicationException($"Cannot generate recovery codes for user with ID '{user.Id}' as they do not have 2FA enabled.");
}
var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
var model = new GenerateRecoveryCodesViewModel { RecoveryCodes = recoveryCodes.ToArray() };
_logger.LogInformation("User with ID {UserId} has generated new 2FA recovery codes.", user.Id);
return View(model);
}
#region Helpers
@ -351,6 +505,33 @@ namespace BTCPayServer.Controllers
ModelState.AddModelError(string.Empty, error.Description);
}
}
private string FormatKey(string unformattedKey)
{
var result = new StringBuilder();
int currentPosition = 0;
while (currentPosition + 4 < unformattedKey.Length)
{
result.Append(unformattedKey.Substring(currentPosition, 4)).Append(" ");
currentPosition += 4;
}
if (currentPosition < unformattedKey.Length)
{
result.Append(unformattedKey.Substring(currentPosition));
}
return result.ToString().ToLowerInvariant();
}
private string GenerateQrCodeUri(string email, string unformattedKey)
{
return string.Format(CultureInfo.InvariantCulture,
AuthenicatorUriFormat,
_urlEncoder.Encode("BTCPayServer"),
_urlEncoder.Encode(email),
unformattedKey);
}
#endregion
}
}

View File

@ -3,10 +3,8 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Filters;
using BTCPayServer.Models;
using BTCPayServer.Models.PaymentRequestViewModels;
@ -25,7 +23,6 @@ using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using NBitpayClient;
namespace BTCPayServer.Controllers
@ -42,7 +39,6 @@ namespace BTCPayServer.Controllers
private readonly EventAggregator _EventAggregator;
private readonly CurrencyNameTable _Currencies;
private readonly HtmlSanitizer _htmlSanitizer;
private readonly InvoiceRepository _InvoiceRepository;
public PaymentRequestController(
InvoiceController invoiceController,
@ -52,8 +48,7 @@ namespace BTCPayServer.Controllers
PaymentRequestService paymentRequestService,
EventAggregator eventAggregator,
CurrencyNameTable currencies,
HtmlSanitizer htmlSanitizer,
InvoiceRepository invoiceRepository)
HtmlSanitizer htmlSanitizer)
{
_InvoiceController = invoiceController;
_UserManager = userManager;
@ -63,7 +58,6 @@ namespace BTCPayServer.Controllers
_EventAggregator = eventAggregator;
_Currencies = currencies;
_htmlSanitizer = htmlSanitizer;
_InvoiceRepository = invoiceRepository;
}
[HttpGet]
@ -103,12 +97,7 @@ namespace BTCPayServer.Controllers
return RedirectToAction("GetPaymentRequests",
new
{
StatusMessage = new StatusMessageModel()
{
Html =
$"Error: You need to create at least one store. <a href='{Url.Action("CreateStore", "UserStores")}'>Create store</a>",
Severity = StatusMessageModel.StatusSeverity.Error
}
StatusMessage = "Error: You need to create at least one store before creating a payment request"
});
}
@ -186,7 +175,7 @@ namespace BTCPayServer.Controllers
return View("Confirm", new ConfirmModel()
{
Title = $"Remove Payment Request",
Description = $"Are you sure you want to remove access to the payment request '{blob.Title}' ?",
Description = $"Are you sure to remove access to remove payment request '{blob.Title}' ?",
Action = "Delete"
});
}
@ -208,7 +197,7 @@ namespace BTCPayServer.Controllers
new
{
StatusMessage =
"Error: Payment request could not be removed. Any request that has generated invoices cannot be removed."
"Payment request could not be removed. Any request that has generated invoices cannot be removed."
});
}
}
@ -216,16 +205,14 @@ namespace BTCPayServer.Controllers
[HttpGet]
[Route("{id}")]
[AllowAnonymous]
public async Task<IActionResult> ViewPaymentRequest(string id, string statusMessage = null)
public async Task<IActionResult> ViewPaymentRequest(string id)
{
var result = await _PaymentRequestService.GetPaymentRequest(id, GetUserId());
if (result == null)
{
return NotFound();
}
result.HubPath = PaymentRequestHub.GetHubPath(this.Request);
result.StatusMessage = statusMessage;
return View(result);
}
@ -233,14 +220,14 @@ namespace BTCPayServer.Controllers
[Route("{id}/pay")]
[AllowAnonymous]
public async Task<IActionResult> PayPaymentRequest(string id, bool redirectToInvoice = true,
decimal? amount = null, CancellationToken cancellationToken = default)
decimal? amount = null)
{
var result = await _PaymentRequestService.GetPaymentRequest(id, GetUserId());
var result = ((await ViewPaymentRequest(id)) as ViewResult)?.Model as ViewPaymentRequestViewModel;
if (result == null)
{
return NotFound();
}
result.HubPath = PaymentRequestHub.GetHubPath(this.Request);
if (result.AmountDue <= 0)
{
if (redirectToInvoice)
@ -280,31 +267,36 @@ namespace BTCPayServer.Controllers
}
if (result.AllowCustomPaymentAmounts && amount != null)
amount = Math.Min(result.AmountDue, amount.Value);
else
amount = result.AmountDue;
{
var invoiceAmount = result.AmountDue < amount ? result.AmountDue : amount;
return await CreateInvoiceForPaymentRequest(id, redirectToInvoice, result, invoiceAmount);
}
return await CreateInvoiceForPaymentRequest(id, redirectToInvoice, result);
}
private async Task<IActionResult> CreateInvoiceForPaymentRequest(string id,
bool redirectToInvoice,
ViewPaymentRequestViewModel result,
decimal? amount = null)
{
var pr = await _PaymentRequestRepository.FindPaymentRequest(id, null);
var blob = pr.GetBlob();
var store = pr.StoreData;
store.AdditionalClaims.Add(new Claim(Policies.CanCreateInvoice.Key, store.Id));
try
{
var redirectUrl = Request.GetDisplayUrl().TrimEnd("/pay", StringComparison.InvariantCulture)
.Replace("hub?id=", string.Empty, StringComparison.InvariantCultureIgnoreCase);
var newInvoiceId = (await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
{
OrderId = $"{PaymentRequestRepository.GetOrderIdForPaymentRequest(id)}",
Currency = blob.Currency,
Price = amount.Value,
FullNotifications = true,
BuyerEmail = result.Email,
RedirectURL = redirectUrl,
}, store, HttpContext.Request.GetAbsoluteRoot(),
new List<string>() {PaymentRequestRepository.GetInternalTag(id)},
cancellationToken: cancellationToken))
.Data.Id;
{
OrderId = $"{PaymentRequestRepository.GetOrderIdForPaymentRequest(id)}",
Currency = blob.Currency,
Price = amount.GetValueOrDefault(result.AmountDue),
FullNotifications = true,
BuyerEmail = result.Email,
RedirectURL = Request.GetDisplayUrl().Replace("/pay", "", StringComparison.InvariantCulture),
}, store, HttpContext.Request.GetAbsoluteRoot(), new List<string>() { PaymentRequestRepository.GetInternalTag(id) })).Data.Id;
if (redirectToInvoice)
{
@ -319,60 +311,9 @@ namespace BTCPayServer.Controllers
}
}
[HttpGet]
[Route("{id}/cancel")]
public async Task<IActionResult> CancelUnpaidPendingInvoice(string id, bool redirect = true)
{
var result = await _PaymentRequestService.GetPaymentRequest(id, GetUserId());
if (result == null )
{
return NotFound();
}
var invoice = result.Invoices.SingleOrDefault(requestInvoice =>
requestInvoice.Status.Equals(InvoiceState.ToString(InvoiceStatus.New),StringComparison.InvariantCulture) && !requestInvoice.Payments.Any());
if (invoice == null )
{
return BadRequest("No unpaid pending invoice to cancel");
}
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoice.Id);
_EventAggregator.Publish(new InvoiceEvent(await _InvoiceRepository.GetInvoice(invoice.Id), 1008, InvoiceEvent.MarkedInvalid));
if (redirect)
{
return RedirectToAction(nameof(ViewPaymentRequest), new
{
Id = id,
StatusMessage = "Payment cancelled"
});
}
return Ok("Payment cancelled");
}
private string GetUserId()
{
return _UserManager.GetUserId(User);
}
[HttpGet]
[Route("{id}/clone")]
public async Task<IActionResult> ClonePaymentRequest(string id)
{
var result = await EditPaymentRequest(id);
if (result is ViewResult viewResult)
{
var model = (UpdatePaymentRequestViewModel)viewResult.Model;
model.Id = null;
model.Title = $"Clone of {model.Title}";
return View("EditPaymentRequest", model);
}
return NotFound();
}
}
}

View File

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Filters;
using BTCPayServer.Models;
@ -29,7 +28,7 @@ namespace BTCPayServer.Controllers
[MediaTypeAcceptConstraintAttribute("text/html")]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
public async Task<IActionResult> PayButtonHandle([FromForm]PayButtonViewModel model, CancellationToken cancellationToken)
public async Task<IActionResult> PayButtonHandle([FromForm]PayButtonViewModel model)
{
var store = await _StoreRepository.FindStore(model.StoreId);
if (store == null)
@ -57,7 +56,7 @@ namespace BTCPayServer.Controllers
NotificationURL = model.ServerIpn,
RedirectURL = model.BrowserRedirect,
FullNotifications = true
}, store, HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
}, store, HttpContext.Request.GetAbsoluteRoot());
return Redirect(invoice.Data.Url);
}
}

View File

@ -43,7 +43,7 @@ namespace BTCPayServer.Controllers
var paymentMethodDetails = GetExistingLightningSupportedPaymentMethod(cryptoCode, store);
var network = _BtcPayNetworkProvider.GetNetwork(cryptoCode);
var nodeInfo =
await _LightningLikePaymentHandler.GetNodeInfo(this.Request.IsOnion(), paymentMethodDetails,
await _LightningLikePaymentHandler.GetNodeInfo(paymentMethodDetails,
network);
return View(new ShowLightningNodeInfoViewModel()

View File

@ -13,7 +13,6 @@ using Newtonsoft.Json;
using Microsoft.AspNetCore.Authorization;
using BTCPayServer.Authentication;
using Microsoft.AspNetCore.Cors;
using System.Threading;
namespace BTCPayServer.Controllers
{
@ -46,7 +45,7 @@ namespace BTCPayServer.Controllers
[Route("rates/{baseCurrency}")]
[HttpGet]
[BitpayAPIConstraint]
public async Task<IActionResult> GetBaseCurrencyRates(string baseCurrency, string storeId, CancellationToken cancellationToken)
public async Task<IActionResult> GetBaseCurrencyRates(string baseCurrency, string storeId)
{
storeId = await GetStoreId(storeId);
var store = this.HttpContext.GetStoreData();
@ -65,7 +64,7 @@ namespace BTCPayServer.Controllers
var currencypairs = BuildCurrencyPairs(currencyCodes, baseCurrency);
var result = await GetRates2(currencypairs, store.Id, cancellationToken);
var result = await GetRates2(currencypairs, store.Id);
var rates = (result as JsonResult)?.Value as Rate[];
if (rates == null)
return result;
@ -76,10 +75,10 @@ namespace BTCPayServer.Controllers
[Route("rates/{baseCurrency}/{currency}")]
[HttpGet]
[BitpayAPIConstraint]
public async Task<IActionResult> GetCurrencyPairRate(string baseCurrency, string currency, string storeId, CancellationToken cancellationToken)
public async Task<IActionResult> GetCurrencyPairRate(string baseCurrency, string currency, string storeId)
{
storeId = await GetStoreId(storeId);
var result = await GetRates2($"{baseCurrency}_{currency}", storeId, cancellationToken);
var result = await GetRates2($"{baseCurrency}_{currency}", storeId);
var rates = (result as JsonResult)?.Value as Rate[];
if (rates == null)
return result;
@ -89,9 +88,10 @@ namespace BTCPayServer.Controllers
[Route("rates")]
[HttpGet]
[BitpayAPIConstraint]
public async Task<IActionResult> GetRates(string currencyPairs, string storeId, CancellationToken cancellationToken)
public async Task<IActionResult> GetRates(string currencyPairs, string storeId)
{
var result = await GetRates2(currencyPairs, storeId, cancellationToken);
storeId = await GetStoreId(storeId);
var result = await GetRates2(currencyPairs, storeId);
var rates = (result as JsonResult)?.Value as Rate[];
if (rates == null)
return result;
@ -118,7 +118,7 @@ namespace BTCPayServer.Controllers
[Route("api/rates")]
[HttpGet]
public async Task<IActionResult> GetRates2(string currencyPairs, string storeId, CancellationToken cancellationToken)
public async Task<IActionResult> GetRates2(string currencyPairs, string storeId)
{
storeId = await GetStoreId(storeId);
if (storeId == null)
@ -139,10 +139,15 @@ namespace BTCPayServer.Controllers
if (currencyPairs == null)
{
currencyPairs = store.GetStoreBlob().GetDefaultCurrencyPairString();
var supportedMethods = store.GetSupportedPaymentMethods(_NetworkProvider);
var currencyCodes = supportedMethods.Select(method => method.PaymentId.CryptoCode).Distinct();
var defaultPaymentId = store.GetDefaultPaymentId(_NetworkProvider);
currencyPairs = BuildCurrencyPairs(currencyCodes, defaultPaymentId.CryptoCode);
if (string.IsNullOrEmpty(currencyPairs))
{
var result = Json(new BitpayErrorsModel() { Error = "You need to setup the default currency pairs in 'Store Settings / Rates' or specify 'currencyPairs' query parameter (eg. BTC_USD,LTC_CAD)." });
var result = Json(new BitpayErrorsModel() { Error = "You need to specify currencyPairs (eg. BTC_USD,LTC_CAD)" });
result.StatusCode = 400;
return result;
}
@ -163,7 +168,7 @@ namespace BTCPayServer.Controllers
pairs.Add(pair);
}
var fetching = _RateProviderFactory.FetchRates(pairs, rules, cancellationToken);
var fetching = _RateProviderFactory.FetchRates(pairs, rules);
await Task.WhenAll(fetching.Select(f => f.Value).ToArray());
return Json(pairs
.Select(r => (Pair: r, Value: fetching[r].GetAwaiter().GetResult().BidAsk?.Bid))

View File

@ -1,289 +0,0 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Models;
using BTCPayServer.Models.ServerViewModels;
using BTCPayServer.Storage.Models;
using BTCPayServer.Storage.Services.Providers.AmazonS3Storage;
using BTCPayServer.Storage.Services.Providers.AmazonS3Storage.Configuration;
using BTCPayServer.Storage.Services.Providers.AzureBlobStorage;
using BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration;
using BTCPayServer.Storage.Services.Providers.FileSystemStorage;
using BTCPayServer.Storage.Services.Providers.FileSystemStorage.Configuration;
using BTCPayServer.Storage.Services.Providers.GoogleCloudStorage;
using BTCPayServer.Storage.Services.Providers.GoogleCloudStorage.Configuration;
using BTCPayServer.Storage.Services.Providers.Models;
using BTCPayServer.Storage.ViewModels;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Controllers
{
public partial class ServerController
{
[HttpGet("server/files/{fileId?}")]
public async Task<IActionResult> Files(string fileId = null, string statusMessage = null)
{
TempData["StatusMessage"] = statusMessage;
var fileUrl = string.IsNullOrEmpty(fileId) ? null : await _FileService.GetFileUrl(fileId);
return View(new ViewFilesViewModel()
{
Files = await _StoredFileRepository.GetFiles(),
SelectedFileId = string.IsNullOrEmpty(fileUrl) ? null : fileId,
DirectFileUrl = fileUrl,
StorageConfigured = (await _SettingsRepository.GetSettingAsync<StorageSettings>()) != null
});
}
[HttpGet("server/files/{fileId}/delete")]
public async Task<IActionResult> DeleteFile(string fileId)
{
try
{
await _FileService.RemoveFile(fileId, null);
return RedirectToAction(nameof(Files), new
{
fileId = "",
statusMessage = "File removed"
});
}
catch (Exception e)
{
return RedirectToAction(nameof(Files), new
{
statusMessage = new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = e.Message
}
});
}
}
[HttpGet("server/files/{fileId}/tmp")]
public async Task<IActionResult> CreateTemporaryFileUrl(string fileId)
{
var file = await _StoredFileRepository.GetFile(fileId);
if (file == null)
{
return NotFound();
}
return View(new CreateTemporaryFileUrlViewModel());
}
[HttpPost("server/files/{fileId}/tmp")]
public async Task<IActionResult> CreateTemporaryFileUrl(string fileId,
CreateTemporaryFileUrlViewModel viewModel)
{
if (viewModel.TimeAmount <= 0)
{
ModelState.AddModelError(nameof(viewModel.TimeAmount), "Time must be at least 1");
}
if (!ModelState.IsValid)
{
return View(viewModel);
}
var file = await _StoredFileRepository.GetFile(fileId);
if (file == null)
{
return NotFound();
}
var expiry = DateTimeOffset.Now;
switch (viewModel.TimeType)
{
case CreateTemporaryFileUrlViewModel.TmpFileTimeType.Seconds:
expiry =expiry.AddSeconds(viewModel.TimeAmount);
break;
case CreateTemporaryFileUrlViewModel.TmpFileTimeType.Minutes:
expiry = expiry.AddMinutes(viewModel.TimeAmount);
break;
case CreateTemporaryFileUrlViewModel.TmpFileTimeType.Hours:
expiry = expiry.AddHours(viewModel.TimeAmount);
break;
case CreateTemporaryFileUrlViewModel.TmpFileTimeType.Days:
expiry = expiry.AddDays(viewModel.TimeAmount);
break;
default:
throw new ArgumentOutOfRangeException();
}
var url = await _FileService.GetTemporaryFileUrl(fileId, expiry, viewModel.IsDownload);
return RedirectToAction(nameof(Files), new
{
StatusMessage = new StatusMessageModel()
{
Html =
$"Generated Temporary Url for file {file.FileName} which expires at {expiry:G}. <a href='{url}' target='_blank'>{url}</a>"
}.ToString(),
fileId,
});
}
public class CreateTemporaryFileUrlViewModel
{
public enum TmpFileTimeType
{
Seconds,
Minutes,
Hours,
Days
}
public int TimeAmount { get; set; }
public TmpFileTimeType TimeType { get; set; }
public bool IsDownload { get; set; }
}
[HttpPost("server/files/upload")]
public async Task<IActionResult> CreateFile(IFormFile file)
{
var newFile = await _FileService.AddFile(file, GetUserId());
return RedirectToAction(nameof(Files), new
{
statusMessage = "File added!",
fileId = newFile.Id
});
}
private string GetUserId()
{
return _UserManager.GetUserId(ControllerContext.HttpContext.User);
}
[HttpGet("server/storage")]
public async Task<IActionResult> Storage(bool forceChoice = false, string statusMessage = null)
{
TempData["StatusMessage"] = statusMessage;
var savedSettings = await _SettingsRepository.GetSettingAsync<StorageSettings>();
if (forceChoice || savedSettings == null)
{
return View(new ChooseStorageViewModel()
{
ShowChangeWarning = savedSettings != null,
Provider = savedSettings?.Provider ?? BTCPayServer.Storage.Models.StorageProvider.FileSystem
});
}
return RedirectToAction(nameof(StorageProvider), new
{
provider = savedSettings.Provider
});
}
[HttpPost("server/storage")]
public IActionResult Storage(StorageSettings viewModel)
{
return RedirectToAction("StorageProvider", "Server", new
{
provider = viewModel.Provider.ToString()
});
}
[HttpGet("server/storage/{provider}")]
public async Task<IActionResult> StorageProvider(string provider)
{
if (!Enum.TryParse(typeof(StorageProvider), provider, out var storageProvider))
{
return RedirectToAction(nameof(Storage), new
{
StatusMessage = new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = $"{provider} provider is not supported"
}.ToString()
});
}
var data = (await _SettingsRepository.GetSettingAsync<StorageSettings>()) ?? new StorageSettings();
var storageProviderService =
_StorageProviderServices.SingleOrDefault(service => service.StorageProvider().Equals(storageProvider));
switch (storageProviderService)
{
case null:
return RedirectToAction(nameof(Storage), new
{
StatusMessage = new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = $"{storageProvider} is not supported"
}.ToString()
});
case AzureBlobStorageFileProviderService fileProviderService:
return View(nameof(EditAzureBlobStorageStorageProvider),
fileProviderService.GetProviderConfiguration(data));
case AmazonS3FileProviderService fileProviderService:
return View(nameof(EditAmazonS3StorageProvider),
fileProviderService.GetProviderConfiguration(data));
case GoogleCloudStorageFileProviderService fileProviderService:
return View(nameof(EditGoogleCloudStorageStorageProvider),
fileProviderService.GetProviderConfiguration(data));
case FileSystemFileProviderService fileProviderService:
return View(nameof(EditFileSystemStorageProvider),
fileProviderService.GetProviderConfiguration(data));
}
return NotFound();
}
[HttpPost("server/storage/AzureBlobStorage")]
public async Task<IActionResult> EditAzureBlobStorageStorageProvider(AzureBlobStorageConfiguration viewModel)
{
return await SaveStorageProvider(viewModel, BTCPayServer.Storage.Models.StorageProvider.AzureBlobStorage);
}
[HttpPost("server/storage/AmazonS3")]
public async Task<IActionResult> EditAmazonS3StorageProvider(AmazonS3StorageConfiguration viewModel)
{
return await SaveStorageProvider(viewModel, BTCPayServer.Storage.Models.StorageProvider.AmazonS3);
}
[HttpPost("server/storage/GoogleCloudStorage")]
public async Task<IActionResult> EditGoogleCloudStorageStorageProvider(
GoogleCloudStorageConfiguration viewModel)
{
return await SaveStorageProvider(viewModel, BTCPayServer.Storage.Models.StorageProvider.GoogleCloudStorage);
}
[HttpPost("server/storage/FileSystem")]
public async Task<IActionResult> EditFileSystemStorageProvider(FileSystemStorageConfiguration viewModel)
{
return await SaveStorageProvider(viewModel, BTCPayServer.Storage.Models.StorageProvider.FileSystem);
}
private async Task<IActionResult> SaveStorageProvider(IBaseStorageConfiguration viewModel,
StorageProvider storageProvider)
{
if (!ModelState.IsValid)
{
return View(viewModel);
}
var data = (await _SettingsRepository.GetSettingAsync<StorageSettings>()) ?? new StorageSettings();
data.Provider = storageProvider;
data.Configuration = JObject.FromObject(viewModel);
await _SettingsRepository.UpdateSetting(data);
TempData["StatusMessage"] = new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Success,
Message = "Storage settings updated successfully"
}.ToString();
return View(viewModel);
}
}
}

View File

@ -26,18 +26,13 @@ using System.Threading.Tasks;
using Renci.SshNet;
using BTCPayServer.Logging;
using BTCPayServer.Lightning;
using BTCPayServer.Configuration.External;
using System.Runtime.CompilerServices;
using BTCPayServer.Storage.Models;
using BTCPayServer.Storage.Services;
using BTCPayServer.Storage.Services.Providers;
using BTCPayServer.Services.Apps;
using Microsoft.AspNetCore.Mvc.Rendering;
using BTCPayServer.Data;
namespace BTCPayServer.Controllers
{
[Authorize(Policy = BTCPayServer.Security.Policies.CanModifyServerSettings.Key)]
public partial class ServerController : Controller
public class ServerController : Controller
{
private UserManager<ApplicationUser> _UserManager;
SettingsRepository _SettingsRepository;
@ -45,31 +40,18 @@ namespace BTCPayServer.Controllers
private RateFetcher _RateProviderFactory;
private StoreRepository _StoreRepository;
LightningConfigurationProvider _LnConfigProvider;
private readonly TorServices _torServices;
BTCPayServerOptions _Options;
ApplicationDbContextFactory _ContextFactory;
private readonly StoredFileRepository _StoredFileRepository;
private readonly FileService _FileService;
private readonly IEnumerable<IStorageProviderService> _StorageProviderServices;
public ServerController(UserManager<ApplicationUser> userManager,
StoredFileRepository storedFileRepository,
FileService fileService,
IEnumerable<IStorageProviderService> storageProviderServices,
BTCPayServerOptions options,
Configuration.BTCPayServerOptions options,
RateFetcher rateProviderFactory,
SettingsRepository settingsRepository,
NBXplorerDashboard dashBoard,
IHttpClientFactory httpClientFactory,
LightningConfigurationProvider lnConfigProvider,
TorServices torServices,
StoreRepository storeRepository,
ApplicationDbContextFactory contextFactory)
Services.Stores.StoreRepository storeRepository)
{
_Options = options;
_StoredFileRepository = storedFileRepository;
_FileService = fileService;
_StorageProviderServices = storageProviderServices;
_UserManager = userManager;
_SettingsRepository = settingsRepository;
_dashBoard = dashBoard;
@ -77,8 +59,6 @@ namespace BTCPayServer.Controllers
_RateProviderFactory = rateProviderFactory;
_StoreRepository = storeRepository;
_LnConfigProvider = lnConfigProvider;
_torServices = torServices;
_ContextFactory = contextFactory;
}
[Route("server/rates")]
@ -267,13 +247,6 @@ namespace BTCPayServer.Controllers
return error;
StatusMessage = $"The server might restart soon if an update is available...";
}
else if (command == "clean")
{
var error = RunSSH(vm, $"btcpay-clean.sh");
if (error != null)
return error;
StatusMessage = $"The old docker images will be cleaned soon...";
}
else
{
return NotFound();
@ -412,17 +385,18 @@ namespace BTCPayServer.Controllers
if (user == null)
return NotFound();
var admins = await _UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
if (admins.Count == 1)
{
// return
return View("Confirm", new ConfirmModel("Unable to Delete Last Admin",
"This is the last Admin, so it can't be removed"));
}
var roles = await _UserManager.GetRolesAsync(user);
if (IsAdmin(roles))
{
var admins = await _UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
if (admins.Count == 1)
{
// return
return View("Confirm", new ConfirmModel("Unable to Delete Last Admin",
"This is the last Admin, so it can't be removed"));
}
return View("Confirm", new ConfirmModel("Delete Admin " + user.Email,
"Are you sure you want to delete this Admin and delete all accounts, users and data associated with the server account?",
"Delete"));
@ -455,243 +429,228 @@ namespace BTCPayServer.Controllers
}
public IHttpClientFactory HttpClientFactory { get; }
[Route("server/emails")]
public async Task<IActionResult> Emails()
{
var data = (await _SettingsRepository.GetSettingAsync<EmailSettings>()) ?? new EmailSettings();
return View(new EmailsViewModel() { Settings = data });
}
[Route("server/policies")]
public async Task<IActionResult> Policies()
{
var data = (await _SettingsRepository.GetSettingAsync<PoliciesSettings>()) ?? new PoliciesSettings();
// load display app dropdown
using (var ctx = _ContextFactory.CreateContext())
{
var userId = _UserManager.GetUserId(base.User);
var selectList = ctx.Users.Where(user => user.Id == userId)
.SelectMany(s => s.UserStores)
.Select(s => s.StoreData)
.SelectMany(s => s.Apps)
.Select(a => new SelectListItem($"{a.AppType} - {a.Name}", a.Id)).ToList();
selectList.Insert(0, new SelectListItem("(None)", null));
ViewBag.AppsList = new SelectList(selectList, "Value", "Text", data.RootAppId);
}
return View(data);
}
[Route("server/policies")]
[HttpPost]
public async Task<IActionResult> Policies(PoliciesSettings settings)
{
if (!String.IsNullOrEmpty(settings.RootAppId))
{
using (var ctx = _ContextFactory.CreateContext())
{
var app = ctx.Apps.SingleOrDefault(a => a.Id == settings.RootAppId);
if (app != null)
settings.RootAppType = Enum.Parse<AppType>(app.AppType);
else
settings.RootAppType = null;
}
}
else
{
// not preserved on client side, but clearing it just in case
settings.RootAppType = null;
}
await _SettingsRepository.UpdateSetting(settings);
TempData["StatusMessage"] = "Policies updated successfully";
return RedirectToAction(nameof(Policies));
return View(settings);
}
[Route("server/services")]
public async Task<IActionResult> Services()
public IActionResult Services()
{
var result = new ServicesViewModel();
result.ExternalServices = _Options.ExternalServices.ToList();
foreach (var externalService in _Options.OtherExternalServices)
foreach (var cryptoCode in _Options.ExternalServicesByCryptoCode.Keys)
{
result.OtherExternalServices.Add(new ServicesViewModel.OtherExternalService()
int i = 0;
foreach (var grpcService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalLnd>(cryptoCode))
{
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
{
Crypto = cryptoCode,
Type = grpcService.Type,
Action = nameof(LndServices),
Index = i++,
});
}
i = 0;
foreach (var sparkService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalSpark>(cryptoCode))
{
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
{
Crypto = cryptoCode,
Type = "Spark server",
Action = nameof(SparkService),
Index = i++,
});
}
foreach (var rtlService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalRTL>(cryptoCode))
{
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
{
Crypto = cryptoCode,
Type = "Ride the Lightning server (RTL)",
Action = nameof(RTLService),
Index = i++,
});
}
foreach (var chargeService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalCharge>(cryptoCode))
{
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
{
Crypto = cryptoCode,
Type = "Lightning charge server",
Action = nameof(LightningChargeServices),
Index = i++,
});
}
}
foreach (var externalService in _Options.ExternalServices)
{
result.ExternalServices.Add(new ServicesViewModel.ExternalService()
{
Name = externalService.Key,
Link = this.Request.GetAbsoluteUriNoPathBase(externalService.Value).AbsoluteUri
Link = this.Request.GetRelativePathOrAbsolute(externalService.Value)
});
}
if (_Options.SSHSettings != null)
{
result.OtherExternalServices.Add(new ServicesViewModel.OtherExternalService()
result.ExternalServices.Add(new ServicesViewModel.ExternalService()
{
Name = "SSH",
Link = this.Url.Action(nameof(SSHService))
});
}
foreach (var torService in _torServices.Services)
{
if (torService.VirtualPort == 80)
{
result.TorHttpServices.Add(new ServicesViewModel.OtherExternalService()
{
Name = torService.Name,
Link = $"http://{torService.OnionHost}"
});
}
else if (TryParseAsExternalService(torService, out var externalService))
{
result.ExternalServices.Add(externalService);
}
else
{
result.TorOtherServices.Add(new ServicesViewModel.OtherExternalService()
{
Name = torService.Name,
Link = $"{torService.OnionHost}:{torService.VirtualPort}"
});
}
}
var storageSettings = await _SettingsRepository.GetSettingAsync<StorageSettings>();
result.ExternalStorageServices.Add(new ServicesViewModel.OtherExternalService()
{
Name = storageSettings == null? "Not set": storageSettings.Provider.ToString(),
Link = Url.Action("Storage")
});
return View(result);
}
private static bool TryParseAsExternalService(TorService torService, out ExternalService externalService)
{
externalService = null;
if (torService.ServiceType == TorServiceType.P2P)
{
externalService = new ExternalService()
{
CryptoCode = torService.Network.CryptoCode,
DisplayName = "Full node P2P",
Type = ExternalServiceTypes.P2P,
ConnectionString = new ExternalConnectionString(new Uri($"bitcoin-p2p://{torService.OnionHost}:{torService.VirtualPort}", UriKind.Absolute)),
ServiceName = torService.Name,
};
}
return externalService != null;
}
private ExternalService GetService(string serviceName, string cryptoCode)
{
var result = _Options.ExternalServices.GetService(serviceName, cryptoCode);
if (result != null)
return result;
_torServices.Services.FirstOrDefault(s => TryParseAsExternalService(s, out result));
return result;
}
[Route("server/services/{serviceName}/{cryptoCode}")]
public async Task<IActionResult> Service(string serviceName, string cryptoCode, bool showQR = false, uint? nonce = null)
[Route("server/services/lightning-charge/{cryptoCode}/{index}")]
public async Task<IActionResult> LightningChargeServices(string cryptoCode, int index, bool showQR = false)
{
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
{
StatusMessage = $"Error: {cryptoCode} is not fully synched";
return RedirectToAction(nameof(Services));
}
var service = GetService(serviceName, cryptoCode);
if (service == null)
var lightningCharge = _Options.ExternalServicesByCryptoCode.GetServices<ExternalCharge>(cryptoCode).Select(c => c.ConnectionString).FirstOrDefault();
if (lightningCharge == null)
{
return NotFound();
}
ChargeServiceViewModel vm = new ChargeServiceViewModel();
vm.Uri = lightningCharge.ToUri(false).AbsoluteUri;
vm.APIToken = lightningCharge.Password;
try
{
if (service.Type == ExternalServiceTypes.P2P)
if (string.IsNullOrEmpty(vm.APIToken) && lightningCharge.CookieFilePath != null)
{
return View("P2PService", new LightningWalletServices()
{
ShowQR = showQR,
WalletName = service.ServiceName,
ServiceLink = service.ConnectionString.Server.AbsoluteUri.WithoutEndingSlash()
});
}
var connectionString = await service.ConnectionString.Expand(this.Request.GetAbsoluteUriNoPathBase(), service.Type);
switch (service.Type)
{
case ExternalServiceTypes.Charge:
return LightningChargeServices(service, connectionString, showQR);
case ExternalServiceTypes.RTL:
case ExternalServiceTypes.Spark:
if (connectionString.AccessKey == null)
{
StatusMessage = $"Error: The access key of the service is not set";
return RedirectToAction(nameof(Services));
}
LightningWalletServices vm = new LightningWalletServices();
vm.ShowQR = showQR;
vm.WalletName = service.DisplayName;
vm.ServiceLink = $"{connectionString.Server}?access-key={connectionString.AccessKey}";
return View("LightningWalletServices", vm);
case ExternalServiceTypes.LNDGRPC:
case ExternalServiceTypes.LNDRest:
return LndServices(service, connectionString, nonce);
default:
throw new NotSupportedException(service.Type.ToString());
if (lightningCharge.CookieFilePath != "fake")
vm.APIToken = await System.IO.File.ReadAllTextAsync(lightningCharge.CookieFilePath);
else
vm.APIToken = "fake";
}
var builder = new UriBuilder(lightningCharge.ToUri(false));
builder.UserName = "api-token";
builder.Password = vm.APIToken;
vm.AuthenticatedUri = builder.ToString();
}
catch (Exception ex)
{
StatusMessage = $"Error: {ex.Message}";
return RedirectToAction(nameof(Services));
}
return View(vm);
}
private IActionResult LightningChargeServices(ExternalService service, ExternalConnectionString connectionString, bool showQR = false)
[Route("server/services/spark/{cryptoCode}/{index}")]
public async Task<IActionResult> SparkService(string cryptoCode, int index, bool showQR = false)
{
ChargeServiceViewModel vm = new ChargeServiceViewModel();
vm.Uri = connectionString.Server.AbsoluteUri;
vm.APIToken = connectionString.APIToken;
var builder = new UriBuilder(connectionString.Server);
builder.UserName = "api-token";
builder.Password = vm.APIToken;
vm.AuthenticatedUri = builder.ToString();
return View(nameof(LightningChargeServices), vm);
return await LightningWalletServicesCore<ExternalSpark>(cryptoCode, showQR, "Spark Wallet");
}
private IActionResult LndServices(ExternalService service, ExternalConnectionString connectionString, uint? nonce)
[Route("server/services/rtl/{cryptoCode}/{index}")]
public async Task<IActionResult> RTLService(string cryptoCode, int index, bool showQR = false)
{
var model = new LndGrpcServicesViewModel();
if (service.Type == ExternalServiceTypes.LNDGRPC)
return await LightningWalletServicesCore<ExternalRTL>(cryptoCode, showQR, "Ride the Lightning Wallet");
}
private async Task<IActionResult> LightningWalletServicesCore<T>(string cryptoCode, bool showQR, string walletName) where T : ExternalService, IAccessKeyService
{
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
{
model.Host = $"{connectionString.Server.DnsSafeHost}:{connectionString.Server.Port}";
model.SSL = connectionString.Server.Scheme == "https";
StatusMessage = $"Error: {cryptoCode} is not fully synched";
return RedirectToAction(nameof(Services));
}
var external = _Options.ExternalServicesByCryptoCode.GetServices<T>(cryptoCode).Where(c => c?.ConnectionString?.Server != null).FirstOrDefault();
if (external == null)
{
return NotFound();
}
LightningWalletServices vm = new LightningWalletServices();
vm.ShowQR = showQR;
vm.WalletName = walletName;
try
{
vm.ServiceLink = $"{external.ConnectionString.Server.AbsoluteUri}?access-key={await external.ExtractAccessKey()}";
}
catch (Exception ex)
{
StatusMessage = $"Error: {ex.Message}";
return RedirectToAction(nameof(Services));
}
return View("LightningWalletServices", vm);
}
[Route("server/services/lnd/{cryptoCode}/{index}")]
public async Task<IActionResult> LndServices(string cryptoCode, int index, uint? nonce)
{
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
{
StatusMessage = $"Error: {cryptoCode} is not fully synched";
return RedirectToAction(nameof(Services));
}
var external = GetExternalLndConnectionString(cryptoCode, index);
if (external == null)
return NotFound();
var model = new LndGrpcServicesViewModel();
if (external.ConnectionType == LightningConnectionType.LndGRPC)
{
model.Host = $"{external.BaseUri.DnsSafeHost}:{external.BaseUri.Port}";
model.SSL = external.BaseUri.Scheme == "https";
model.ConnectionType = "GRPC";
model.GRPCSSLCipherSuites = "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256";
}
else if (service.Type == ExternalServiceTypes.LNDRest)
else if (external.ConnectionType == LightningConnectionType.LndREST)
{
model.Uri = connectionString.Server.AbsoluteUri;
model.Uri = external.BaseUri.AbsoluteUri;
model.ConnectionType = "REST";
}
if (connectionString.CertificateThumbprint != null)
if (external.CertificateThumbprint != null)
{
model.CertificateThumbprint = connectionString.CertificateThumbprint;
model.CertificateThumbprint = Encoders.Hex.EncodeData(external.CertificateThumbprint);
}
if (connectionString.Macaroon != null)
if (external.Macaroon != null)
{
model.Macaroon = Encoders.Hex.EncodeData(connectionString.Macaroon);
model.Macaroon = Encoders.Hex.EncodeData(external.Macaroon);
}
model.AdminMacaroon = connectionString.Macaroons?.AdminMacaroon?.Hex;
model.InvoiceMacaroon = connectionString.Macaroons?.InvoiceMacaroon?.Hex;
model.ReadonlyMacaroon = connectionString.Macaroons?.ReadonlyMacaroon?.Hex;
var macaroons = external.MacaroonDirectoryPath == null ? null : await Macaroons.GetFromDirectoryAsync(external.MacaroonDirectoryPath);
model.AdminMacaroon = macaroons?.AdminMacaroon?.Hex;
model.InvoiceMacaroon = macaroons?.InvoiceMacaroon?.Hex;
model.ReadonlyMacaroon = macaroons?.ReadonlyMacaroon?.Hex;
if (nonce != null)
{
var configKey = GetConfigKey("lnd", service.ServiceName, service.CryptoCode, nonce.Value);
var configKey = GetConfigKey("lnd", cryptoCode, index, nonce.Value);
var lnConfig = _LnConfigProvider.GetConfig(configKey);
if (lnConfig != null)
{
model.QRCodeLink = Request.GetAbsoluteUri(Url.Action(nameof(GetLNDConfig), new { configKey = configKey }));
model.QRCodeLink = $"{this.Request.GetAbsoluteRoot().WithTrailingSlash()}lnd-config/{configKey}/lnd.config";
model.QRCode = $"config={model.QRCodeLink}";
}
}
return View(nameof(LndServices), model);
return View(model);
}
private static uint GetConfigKey(string type, string serviceName, string cryptoCode, uint nonce)
private static uint GetConfigKey(string type, string cryptoCode, int index, uint nonce)
{
return (uint)HashCode.Combine(type, serviceName, cryptoCode, nonce);
return (uint)HashCode.Combine(type, cryptoCode, index, nonce);
}
[Route("lnd-config/{configKey}/lnd.config")]
@ -704,62 +663,68 @@ namespace BTCPayServer.Controllers
return Json(conf);
}
[Route("server/services/{serviceName}/{cryptoCode}")]
[Route("server/services/lnd/{cryptoCode}/{index}")]
[HttpPost]
public async Task<IActionResult> ServicePost(string serviceName, string cryptoCode)
public async Task<IActionResult> LndServicesPost(string cryptoCode, int index)
{
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
{
StatusMessage = $"Error: {cryptoCode} is not fully synched";
return RedirectToAction(nameof(Services));
}
var service = GetService(serviceName, cryptoCode);
if (service == null)
var external = GetExternalLndConnectionString(cryptoCode, index);
if (external == null)
return NotFound();
ExternalConnectionString connectionString = null;
try
{
connectionString = await service.ConnectionString.Expand(this.Request.GetAbsoluteUriNoPathBase(), service.Type);
}
catch (Exception ex)
{
StatusMessage = $"Error: {ex.Message}";
return RedirectToAction(nameof(Services));
}
LightningConfigurations confs = new LightningConfigurations();
if (service.Type == ExternalServiceTypes.LNDGRPC)
var macaroons = external.MacaroonDirectoryPath == null ? null : await Macaroons.GetFromDirectoryAsync(external.MacaroonDirectoryPath);
if (external.ConnectionType == LightningConnectionType.LndGRPC)
{
LightningConfiguration grpcConf = new LightningConfiguration();
grpcConf.Type = "grpc";
grpcConf.Host = connectionString.Server.DnsSafeHost;
grpcConf.Port = connectionString.Server.Port;
grpcConf.SSL = connectionString.Server.Scheme == "https";
grpcConf.Host = external.BaseUri.DnsSafeHost;
grpcConf.Port = external.BaseUri.Port;
grpcConf.SSL = external.BaseUri.Scheme == "https";
confs.Configurations.Add(grpcConf);
}
else if (service.Type == ExternalServiceTypes.LNDRest)
else if (external.ConnectionType == LightningConnectionType.LndREST)
{
var restconf = new LNDRestConfiguration();
restconf.Type = "lnd-rest";
restconf.Uri = connectionString.Server.AbsoluteUri;
restconf.Uri = external.BaseUri.AbsoluteUri;
confs.Configurations.Add(restconf);
}
else
throw new NotSupportedException(service.Type.ToString());
throw new NotSupportedException(external.ConnectionType.ToString());
var commonConf = (LNDConfiguration)confs.Configurations[confs.Configurations.Count - 1];
commonConf.ChainType = _Options.NetworkType.ToString();
commonConf.CryptoCode = cryptoCode;
commonConf.Macaroon = connectionString.Macaroon == null ? null : Encoders.Hex.EncodeData(connectionString.Macaroon);
commonConf.CertificateThumbprint = connectionString.CertificateThumbprint == null ? null : connectionString.CertificateThumbprint;
commonConf.AdminMacaroon = connectionString.Macaroons?.AdminMacaroon?.Hex;
commonConf.ReadonlyMacaroon = connectionString.Macaroons?.ReadonlyMacaroon?.Hex;
commonConf.InvoiceMacaroon = connectionString.Macaroons?.InvoiceMacaroon?.Hex;
commonConf.Macaroon = external.Macaroon == null ? null : Encoders.Hex.EncodeData(external.Macaroon);
commonConf.CertificateThumbprint = external.CertificateThumbprint == null ? null : Encoders.Hex.EncodeData(external.CertificateThumbprint);
commonConf.AdminMacaroon = macaroons?.AdminMacaroon?.Hex;
commonConf.ReadonlyMacaroon = macaroons?.ReadonlyMacaroon?.Hex;
commonConf.InvoiceMacaroon = macaroons?.InvoiceMacaroon?.Hex;
var nonce = RandomUtils.GetUInt32();
var configKey = GetConfigKey("lnd", serviceName, cryptoCode, nonce);
var configKey = GetConfigKey("lnd", cryptoCode, index, nonce);
_LnConfigProvider.KeepConfig(configKey, confs);
return RedirectToAction(nameof(Service), new { cryptoCode = cryptoCode, serviceName = serviceName, nonce = nonce });
return RedirectToAction(nameof(LndServices), new { cryptoCode = cryptoCode, nonce = nonce });
}
private LightningConnectionString GetExternalLndConnectionString(string cryptoCode, int index)
{
var connectionString = _Options.ExternalServicesByCryptoCode.GetServices<ExternalLnd>(cryptoCode).Skip(index).Select(c => c.ConnectionString).FirstOrDefault();
if (connectionString == null)
return null;
connectionString = connectionString.Clone();
if (connectionString.MacaroonFilePath != null)
{
try
{
connectionString.Macaroon = System.IO.File.ReadAllBytes(connectionString.MacaroonFilePath);
connectionString.MacaroonFilePath = null;
}
catch
{
Logs.Configuration.LogWarning($"{cryptoCode}: The macaroon file path of the external LND grpc config was not found ({connectionString.MacaroonFilePath})");
return null;
}
}
return connectionString;
}
[Route("server/services/ssh")]
@ -774,11 +739,9 @@ namespace BTCPayServer.Controllers
return NotFound();
return File(System.IO.File.ReadAllBytes(settings.KeyFile), "application/octet-stream", "id_rsa");
}
var server = Extensions.IsLocalNetwork(settings.Server) ? this.Request.Host.Host : settings.Server;
SSHServiceViewModel vm = new SSHServiceViewModel();
string port = settings.Port == 22 ? "" : $" -p {settings.Port}";
vm.CommandLine = $"ssh {settings.Username}@{server}{port}";
vm.CommandLine = $"ssh {settings.Username}@{settings.Server}{port}";
vm.Password = settings.Password;
vm.KeyFilePassword = settings.KeyFilePassword;
vm.HasKeyFile = !string.IsNullOrEmpty(settings.KeyFile);
@ -800,28 +763,19 @@ namespace BTCPayServer.Controllers
return View(settings);
}
[Route("server/emails")]
public async Task<IActionResult> Emails()
{
var data = (await _SettingsRepository.GetSettingAsync<EmailSettings>()) ?? new EmailSettings();
return View(new EmailsViewModel() { Settings = data });
}
[Route("server/emails")]
[HttpPost]
public async Task<IActionResult> Emails(EmailsViewModel model, string command)
{
if (!model.Settings.IsComplete())
{
model.StatusMessage = "Error: Required fields missing";
return View(model);
}
if (command == "Test")
{
try
{
if (!model.Settings.IsComplete())
{
model.StatusMessage = "Error: Required fields missing";
return View(model);
}
var client = model.Settings.CreateSmtpClient();
await client.SendMailAsync(model.Settings.From, model.TestEmail, "BTCPay test", "BTCPay test");
model.StatusMessage = "Email sent to " + model.TestEmail + ", please, verify you received it";

View File

@ -1,24 +0,0 @@
using System.Threading.Tasks;
using BTCPayServer.Storage.Services;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Storage
{
[Route("Storage")]
public class StorageController
{
private readonly FileService _FileService;
public StorageController(FileService fileService)
{
_FileService = fileService;
}
[HttpGet("{fileId}")]
public async Task<IActionResult> GetFile(string fileId)
{
var url = await _FileService.GetFileUrl(fileId);
return new RedirectResult(url);
}
}
}

View File

@ -1,19 +1,16 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Services;
using LedgerWallet;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBXplorer.DerivationStrategy;
@ -102,30 +99,22 @@ namespace BTCPayServer.Controllers
private void SetExistingValues(StoreData store, DerivationSchemeViewModel vm)
{
var derivation = GetExistingDerivationStrategy(vm.CryptoCode, store);
if (derivation != null)
{
vm.DerivationScheme = derivation.AccountDerivation.ToString();
vm.Config = derivation.ToJson();
}
vm.DerivationScheme = GetExistingDerivationStrategy(vm.CryptoCode, store)?.DerivationStrategyBase.ToString();
vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.BTCLike));
}
private DerivationSchemeSettings GetExistingDerivationStrategy(string cryptoCode, StoreData store)
private DerivationStrategy GetExistingDerivationStrategy(string cryptoCode, StoreData store)
{
var id = new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike);
var existing = store.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationSchemeSettings>()
.OfType<DerivationStrategy>()
.FirstOrDefault(d => d.PaymentId == id);
return existing;
}
[HttpPost]
[Route("{storeId}/derivations/{cryptoCode}")]
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm,
string cryptoCode)
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm, string cryptoCode)
{
vm.CryptoCode = cryptoCode;
var store = HttpContext.GetStoreData();
@ -137,89 +126,45 @@ namespace BTCPayServer.Controllers
{
return NotFound();
}
vm.RootKeyPath = network.GetRootKeyPath();
DerivationSchemeSettings strategy = null;
var wallet = _WalletProvider.GetWallet(network);
if (wallet == null)
{
return NotFound();
}
if (!string.IsNullOrEmpty(vm.Config))
{
if (!DerivationSchemeSettings.TryParseFromJson(vm.Config, network, out strategy))
{
vm.StatusMessage = new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = "Config file was not in the correct format"
}.ToString();
vm.Confirmation = false;
return View(vm);
}
}
if (vm.ColdcardPublicFile != null)
{
if (!DerivationSchemeSettings.TryParseFromColdcard(await ReadAllText(vm.ColdcardPublicFile), network, out strategy))
{
vm.StatusMessage = new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = "Coldcard public file was not in the correct format"
}.ToString();
vm.Confirmation = false;
return View(vm);
}
}
else
{
try
{
if (!string.IsNullOrEmpty(vm.DerivationScheme))
{
var newStrategy = ParseDerivationStrategy(vm.DerivationScheme, null, network);
if (newStrategy.AccountDerivation != strategy?.AccountDerivation)
{
strategy = newStrategy;
strategy.AccountKeyPath = vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath);
vm.DerivationScheme = strategy.AccountDerivation.ToString();
}
}
else
{
strategy = null;
}
}
catch
{
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
vm.Confirmation = false;
return View(vm);
}
}
var oldConfig = vm.Config;
vm.Config = strategy == null ? null : strategy.ToJson();
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
var exisingStrategy = store.GetSupportedPaymentMethods(_NetworkProvider)
.Where(c => c.PaymentId == paymentMethodId)
.OfType<DerivationSchemeSettings>()
.FirstOrDefault();
.Where(c => c.PaymentId == paymentMethodId)
.OfType<DerivationStrategy>()
.Select(c => c.DerivationStrategyBase.ToString())
.FirstOrDefault();
DerivationStrategy strategy = null;
try
{
if (!string.IsNullOrEmpty(vm.DerivationScheme))
{
strategy = ParseDerivationStrategy(vm.DerivationScheme, null, network);
vm.DerivationScheme = strategy.ToString();
}
}
catch
{
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
vm.Confirmation = false;
return View(vm);
}
var storeBlob = store.GetStoreBlob();
var wasExcluded = storeBlob.GetExcludedPaymentMethods().Match(paymentMethodId);
var willBeExcluded = !vm.Enabled;
var showAddress = // Show addresses if:
// - If the user is testing the hint address in confirmation screen
(vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress)) ||
// - The user is clicking on continue after changing the config
(!vm.Confirmation && oldConfig != vm.Config) ||
// - The user is clickingon continue without changing config nor enabling/disabling
(!vm.Confirmation && oldConfig == vm.Config && willBeExcluded == wasExcluded);
// - If the user is testing the hint address in confirmation screen
(vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress)) ||
// - The user is setting a new derivation scheme
(!vm.Confirmation && strategy != null && exisingStrategy != strategy.DerivationStrategyBase.ToString()) ||
// - The user is clicking on continue without changing anything
(!vm.Confirmation && willBeExcluded == wasExcluded);
showAddress = showAddress && strategy != null;
if (!showAddress)
@ -227,9 +172,10 @@ namespace BTCPayServer.Controllers
try
{
if (strategy != null)
await wallet.TrackAsync(strategy.AccountDerivation);
await wallet.TrackAsync(strategy.DerivationStrategyBase);
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
storeBlob.SetExcluded(paymentMethodId, willBeExcluded);
storeBlob.SetExcluded(paymentMethodId, willBeExcluded);
storeBlob.SetWalletKeyPathRoot(paymentMethodId, vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath));
store.SetStoreBlob(storeBlob);
}
catch
@ -239,14 +185,8 @@ namespace BTCPayServer.Controllers
}
await _Repo.UpdateStore(store);
if (oldConfig != vm.Config)
StatusMessage = $"Derivation settings for {network.CryptoCode} has been modified.";
if (willBeExcluded != wasExcluded)
{
var label = willBeExcluded ? "disabled" : "enabled";
StatusMessage = $"On-Chain payments for {network.CryptoCode} has been {label}.";
}
return RedirectToAction(nameof(UpdateStore), new {storeId = storeId});
StatusMessage = $"Derivation scheme for {network.CryptoCode} has been modified.";
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
}
else if (!string.IsNullOrEmpty(vm.HintAddress))
{
@ -263,43 +203,27 @@ namespace BTCPayServer.Controllers
try
{
var newStrategy = ParseDerivationStrategy(vm.DerivationScheme, address.ScriptPubKey, network);
if (newStrategy.AccountDerivation != strategy.AccountDerivation)
{
strategy.AccountDerivation = newStrategy.AccountDerivation;
strategy.AccountOriginal = null;
}
strategy = ParseDerivationStrategy(vm.DerivationScheme, address.ScriptPubKey, network);
}
catch
{
ModelState.AddModelError(nameof(vm.HintAddress), "Impossible to find a match with this address");
return ShowAddresses(vm, strategy);
}
vm.HintAddress = "";
vm.StatusMessage =
"Address successfully found, please verify that the rest is correct and click on \"Confirm\"";
vm.StatusMessage = "Address successfully found, please verify that the rest is correct and click on \"Confirm\"";
ModelState.Remove(nameof(vm.HintAddress));
ModelState.Remove(nameof(vm.DerivationScheme));
}
return ShowAddresses(vm, strategy);
}
private async Task<string> ReadAllText(IFormFile file)
private IActionResult ShowAddresses(DerivationSchemeViewModel vm, DerivationStrategy strategy)
{
using (var stream = new StreamReader(file.OpenReadStream()))
{
return await stream.ReadToEndAsync();
}
}
private IActionResult ShowAddresses(DerivationSchemeViewModel vm, DerivationSchemeSettings strategy)
{
vm.DerivationScheme = strategy.AccountDerivation.ToString();
vm.DerivationScheme = strategy.DerivationStrategyBase.ToString();
if (!string.IsNullOrEmpty(vm.DerivationScheme))
{
var line = strategy.AccountDerivation.GetLineFor(DerivationFeature.Deposit);
var line = strategy.DerivationStrategyBase.GetLineFor(DerivationFeature.Deposit);
for (int i = 0; i < 10; i++)
{

View File

@ -1,5 +1,4 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models.StoreViewModels;
@ -78,7 +77,7 @@ namespace BTCPayServer.Controllers
case "test":
try
{
var client = new Changelly(_httpClientFactory.CreateClient(), changellySettings.ApiKey, changellySettings.ApiSecret,
var client = new Changelly(_httpClientFactory, changellySettings.ApiKey, changellySettings.ApiSecret,
changellySettings.ApiUrl);
var result = await client.GetCurrenciesFull();
vm.StatusMessage = "Test Successful";

View File

@ -28,7 +28,6 @@ namespace BTCPayServer.Controllers
vm.MerchantId = existing.MerchantId;
vm.Enabled = existing.Enabled;
vm.Mode = existing.Mode;
vm.AmountMarkupPercentage = existing.AmountMarkupPercentage;
}
[HttpPost]
@ -51,8 +50,7 @@ namespace BTCPayServer.Controllers
{
MerchantId = vm.MerchantId,
Enabled = vm.Enabled,
Mode = vm.Mode,
AmountMarkupPercentage = vm.AmountMarkupPercentage
Mode = vm.Mode
};
switch (command)

View File

@ -155,7 +155,7 @@ namespace BTCPayServer.Controllers
var handler = (LightningLikePaymentHandler)_ServiceProvider.GetRequiredService<IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>>();
try
{
var info = await handler.GetNodeInfo(this.Request.IsOnion(), paymentMethod, network);
var info = await handler.GetNodeInfo(paymentMethod, network);
if (!vm.SkipPortTest)
{
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)))

View File

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Authentication;
using BTCPayServer.Configuration;
@ -189,39 +188,24 @@ namespace BTCPayServer.Controllers
[HttpGet]
[Route("{storeId}/rates")]
public IActionResult Rates(string storeId)
public IActionResult Rates()
{
var storeBlob = StoreData.GetStoreBlob();
var vm = new RatesViewModel();
vm.SetExchangeRates(GetSupportedExchanges(), storeBlob.PreferredExchange ?? CoinAverageRateProvider.CoinAverageName);
vm.Spread = (double)(storeBlob.Spread * 100m);
vm.StoreId = storeId;
vm.Script = storeBlob.GetRateRules(_NetworkProvider).ToString();
vm.DefaultScript = storeBlob.GetDefaultRateRules(_NetworkProvider).ToString();
vm.AvailableExchanges = GetSupportedExchanges();
vm.DefaultCurrencyPairs = storeBlob.GetDefaultCurrencyPairString();
vm.ShowScripting = storeBlob.RateScripting;
return View(vm);
}
[HttpPost]
[Route("{storeId}/rates")]
public async Task<IActionResult> Rates(RatesViewModel model, string command = null, string storeId = null, CancellationToken cancellationToken = default)
public async Task<IActionResult> Rates(RatesViewModel model, string command = null)
{
model.SetExchangeRates(GetSupportedExchanges(), model.PreferredExchange);
model.StoreId = storeId ?? model.StoreId;
CurrencyPair[] currencyPairs = null;
try
{
currencyPairs = model.DefaultCurrencyPairs?
.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(p => CurrencyPair.Parse(p))
.ToArray();
}
catch
{
ModelState.AddModelError(nameof(model.DefaultCurrencyPairs), "Invalid currency pairs (should be for example: BTC_USD,BTC_CAD,BTC_JPY)");
}
if (!ModelState.IsValid)
{
return View(model);
@ -235,7 +219,7 @@ namespace BTCPayServer.Controllers
blob.PreferredExchange = model.PreferredExchange;
blob.Spread = (decimal)model.Spread / 100.0m;
blob.DefaultCurrencyPairs = currencyPairs;
if (!model.ShowScripting)
{
if (!GetSupportedExchanges().Select(c => c.Name).Contains(blob.PreferredExchange, StringComparer.OrdinalIgnoreCase))
@ -283,7 +267,7 @@ namespace BTCPayServer.Controllers
pairs.Add(currencyPair);
}
var fetchs = _RateFactory.FetchRates(pairs.ToHashSet(), rules, cancellationToken);
var fetchs = _RateFactory.FetchRates(pairs.ToHashSet(), rules);
var testResults = new List<RatesViewModel.TestResultViewModel>();
foreach (var fetch in fetchs)
{
@ -348,15 +332,13 @@ namespace BTCPayServer.Controllers
var storeBlob = StoreData.GetStoreBlob();
var vm = new CheckoutExperienceViewModel();
SetCryptoCurrencies(vm, StoreData);
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? "";
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
vm.CustomCSS = storeBlob.CustomCSS?.AbsoluteUri;
vm.CustomLogo = storeBlob.CustomLogo?.AbsoluteUri;
vm.HtmlTitle = storeBlob.HtmlTitle;
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? "";
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
vm.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi;
vm.RedirectAutomatically = storeBlob.RedirectAutomatically;
return View(vm);
}
void SetCryptoCurrencies(CheckoutExperienceViewModel vm, Data.StoreData storeData)
@ -413,15 +395,13 @@ namespace BTCPayServer.Controllers
{
return View(model);
}
blob.DefaultLang = model.DefaultLang;
blob.RequiresRefundEmail = model.RequiresRefundEmail;
blob.LightningMaxValue = lightningMaxValue;
blob.OnChainMinValue = onchainMinValue;
blob.CustomLogo = string.IsNullOrWhiteSpace(model.CustomLogo) ? null : new Uri(model.CustomLogo, UriKind.Absolute);
blob.CustomCSS = string.IsNullOrWhiteSpace(model.CustomCSS) ? null : new Uri(model.CustomCSS, UriKind.Absolute);
blob.HtmlTitle = string.IsNullOrWhiteSpace(model.HtmlTitle) ? null : model.HtmlTitle;
blob.DefaultLang = model.DefaultLang;
blob.RequiresRefundEmail = model.RequiresRefundEmail;
blob.OnChainMinValue = onchainMinValue;
blob.LightningMaxValue = lightningMaxValue;
blob.LightningAmountInSatoshi = model.LightningAmountInSatoshi;
blob.RedirectAutomatically = model.RedirectAutomatically;
if (StoreData.SetStoreBlob(blob))
{
needUpdate = true;
@ -470,7 +450,7 @@ namespace BTCPayServer.Controllers
var derivationByCryptoCode =
store
.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationSchemeSettings>()
.OfType<DerivationStrategy>()
.ToDictionary(c => c.Network.CryptoCode);
foreach (var network in _NetworkProvider.GetAll())
{
@ -478,7 +458,7 @@ namespace BTCPayServer.Controllers
vm.DerivationSchemes.Add(new StoreViewModel.DerivationScheme()
{
Crypto = network.CryptoCode,
Value = strategy?.ToPrettyString() ?? string.Empty,
Value = strategy?.DerivationStrategyBase?.ToString() ?? string.Empty,
WalletId = new WalletId(store.Id, network.CryptoCode),
Enabled = !excludeFilters.Match(new Payments.PaymentMethodId(network.CryptoCode, Payments.PaymentTypes.BTCLike))
});
@ -509,7 +489,7 @@ namespace BTCPayServer.Controllers
Action = nameof(UpdateChangellySettings),
Provider = "Changelly"
});
var coinSwitchEnabled = storeBlob.CoinSwitchSettings != null && storeBlob.CoinSwitchSettings.Enabled;
vm.ThirdPartyPaymentMethods.Add(new StoreViewModel.ThirdPartyPaymentMethod()
{
@ -596,11 +576,11 @@ namespace BTCPayServer.Controllers
.ToArray();
}
private DerivationSchemeSettings ParseDerivationStrategy(string derivationScheme, Script hint, BTCPayNetwork network)
private DerivationStrategy ParseDerivationStrategy(string derivationScheme, Script hint, BTCPayNetwork network)
{
var parser = new DerivationSchemeParser(network);
var parser = new DerivationSchemeParser(network.NBitcoinNetwork);
parser.HintScriptPubKey = hint;
return new DerivationSchemeSettings(parser.Parse(derivationScheme), network);
return new DerivationStrategy(parser.Parse(derivationScheme), network);
}
[HttpGet]
@ -613,6 +593,7 @@ namespace BTCPayServer.Controllers
model.StoreNotConfigured = StoreNotConfigured;
model.Tokens = tokens.Select(t => new TokenViewModel()
{
Facade = t.Facade,
Label = t.Label,
SIN = t.SIN,
Id = t.Value
@ -697,6 +678,7 @@ namespace BTCPayServer.Controllers
var tokenRequest = new TokenRequest()
{
Facade = model.Facade,
Label = model.Label,
Id = model.PublicKey == null ? null : NBitpayClient.Extensions.BitIdExtensions.GetBitIDSIN(new PubKey(model.PublicKey))
};
@ -708,6 +690,7 @@ namespace BTCPayServer.Controllers
await _TokenRepository.UpdatePairingCode(new PairingCodeEntity()
{
Id = tokenRequest.PairingCode,
Facade = model.Facade,
Label = model.Label,
});
await _TokenRepository.PairWithStoreAsync(tokenRequest.PairingCode, storeId);
@ -747,6 +730,7 @@ namespace BTCPayServer.Controllers
}
}
var model = new CreateTokenViewModel();
model.Facade = "merchant";
ViewBag.HidePublicKey = storeId == null;
ViewBag.ShowStores = storeId == null;
ViewBag.ShowMenu = storeId != null;
@ -798,6 +782,7 @@ namespace BTCPayServer.Controllers
return View(new PairingModel()
{
Id = pairing.Id,
Facade = pairing.Facade,
Label = pairing.Label,
SIN = pairing.SIN ?? "Server-Initiated Pairing",
SelectedStore = selectedStore ?? stores.FirstOrDefault()?.Id,
@ -888,11 +873,7 @@ namespace BTCPayServer.Controllers
ButtonSize = 2,
UrlRoot = appUrl,
PayButtonImageUrl = appUrl + "img/paybutton/pay.png",
StoreId = store.Id,
ButtonType = 0,
Min = 1,
Max = 20,
Step = 1
StoreId = store.Id
};
return View(model);
}

View File

@ -80,9 +80,9 @@ namespace BTCPayServer.Controllers
var onChainWallets = stores
.SelectMany(s => s.GetSupportedPaymentMethods(NetworkProvider)
.OfType<DerivationSchemeSettings>()
.OfType<DerivationStrategy>()
.Select(d => ((Wallet: _walletProvider.GetWallet(d.Network),
DerivationStrategy: d.AccountDerivation,
DerivationStrategy: d.DerivationStrategyBase,
Network: d.Network)))
.Where(_ => _.Wallet != null)
.Select(_ => (Wallet: _.Wallet,
@ -118,12 +118,12 @@ namespace BTCPayServer.Controllers
WalletId walletId)
{
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
DerivationSchemeSettings paymentMethod = GetPaymentMethod(walletId, store);
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
if (paymentMethod == null)
return NotFound();
var wallet = _walletProvider.GetWallet(paymentMethod.Network);
var transactions = await wallet.FetchTransactions(paymentMethod.AccountDerivation);
var transactions = await wallet.FetchTransactions(paymentMethod.DerivationStrategyBase);
var model = new ListTransactionsViewModel();
foreach (var tx in transactions.UnconfirmedTransactions.Transactions.Concat(transactions.ConfirmedTransactions.Transactions))
@ -146,12 +146,12 @@ namespace BTCPayServer.Controllers
[Route("{walletId}/send")]
public async Task<IActionResult> WalletSend(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, string defaultDestination = null, string defaultAmount = null)
WalletId walletId, string defaultDestination = null, string defaultAmount = null, bool advancedMode = false)
{
if (walletId?.StoreId == null)
return NotFound();
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
DerivationSchemeSettings paymentMethod = GetPaymentMethod(walletId, store);
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
if (paymentMethod == null)
return NotFound();
var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode);
@ -171,17 +171,17 @@ namespace BTCPayServer.Controllers
var feeProvider = _feeRateProvider.CreateFeeProvider(network);
var recommendedFees = feeProvider.GetFeeRateAsync();
var balance = _walletProvider.GetWallet(network).GetBalance(paymentMethod.AccountDerivation);
var balance = _walletProvider.GetWallet(network).GetBalance(paymentMethod.DerivationStrategyBase);
model.CurrentBalance = (await balance).ToDecimal(MoneyUnit.BTC);
model.RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi;
model.FeeSatoshiPerByte = model.RecommendedSatoshiPerByte;
model.SupportRBF = network.SupportRBF;
using (CancellationTokenSource cts = new CancellationTokenSource())
{
try
{
cts.CancelAfter(TimeSpan.FromSeconds(5));
var result = await RateFetcher.FetchRate(currencyPair, rateRules, cts.Token).WithCancellation(cts.Token);
var result = await RateFetcher.FetchRate(currencyPair, rateRules).WithCancellation(cts.Token);
if (result.BidAsk != null)
{
model.Rate = result.BidAsk.Center;
@ -195,6 +195,7 @@ namespace BTCPayServer.Controllers
}
catch (Exception ex) { model.RateError = ex.Message; }
}
model.AdvancedMode = advancedMode;
return View(model);
}
@ -202,7 +203,7 @@ namespace BTCPayServer.Controllers
[Route("{walletId}/send")]
public async Task<IActionResult> WalletSend(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletSendModel vm, string command = null, CancellationToken cancellation = default)
WalletId walletId, WalletSendModel vm, string command = null)
{
if (walletId?.StoreId == null)
return NotFound();
@ -212,7 +213,14 @@ namespace BTCPayServer.Controllers
var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode);
if (network == null)
return NotFound();
vm.SupportRBF = network.SupportRBF;
if (command == "noob" || command == "expert")
{
ModelState.Clear();
vm.AdvancedMode = command == "expert";
return View(vm);
}
var destination = ParseDestination(vm.Destination, network.NBitcoinNetwork);
if (destination == null)
ModelState.AddModelError(nameof(vm.Destination), "Invalid address");
@ -227,93 +235,14 @@ namespace BTCPayServer.Controllers
if (!ModelState.IsValid)
return View(vm);
var sendModel = new WalletSendLedgerModel()
return RedirectToAction(nameof(WalletSendLedger), new WalletSendLedgerModel()
{
Destination = vm.Destination,
Amount = vm.Amount.Value,
SubstractFees = vm.SubstractFees,
FeeSatoshiPerByte = vm.FeeSatoshiPerByte,
NoChange = vm.NoChange,
DisableRBF = vm.DisableRBF
};
if (command == "ledger")
{
return RedirectToAction(nameof(WalletSendLedger), sendModel);
}
else
{
var storeData = (await Repository.FindStore(walletId.StoreId, GetUserId()));
var derivationScheme = GetPaymentMethod(walletId, storeData);
try
{
var psbt = await CreatePSBT(network, derivationScheme, sendModel, cancellation);
return File(psbt.PSBT.ToBytes(), "application/octet-stream", $"Send-{vm.Amount.Value}-{network.CryptoCode}-to-{destination[0].ToString()}.psbt");
}
catch (NBXplorerException ex)
{
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);
}
}
}
public async Task<CreatePSBTResponse> CreatePSBT(BTCPayNetwork network, DerivationSchemeSettings derivationSettings, WalletSendLedgerModel sendModel, CancellationToken cancellationToken)
{
var nbx = ExplorerClientProvider.GetExplorerClient(network);
CreatePSBTRequest psbtRequest = new CreatePSBTRequest();
CreatePSBTDestination psbtDestination = new CreatePSBTDestination();
psbtRequest.Destinations.Add(psbtDestination);
if (network.SupportRBF)
{
psbtRequest.RBF = !sendModel.DisableRBF;
}
psbtDestination.Destination = BitcoinAddress.Create(sendModel.Destination, network.NBitcoinNetwork);
psbtDestination.Amount = Money.Coins(sendModel.Amount);
psbtRequest.FeePreference = new FeePreference();
psbtRequest.FeePreference.ExplicitFeeRate = new FeeRate(Money.Satoshis(sendModel.FeeSatoshiPerByte), 1);
if (sendModel.NoChange)
{
psbtRequest.ExplicitChangeAddress = psbtDestination.Destination;
}
psbtDestination.SubstractFees = sendModel.SubstractFees;
var psbt = (await nbx.CreatePSBTAsync(derivationSettings.AccountDerivation, psbtRequest, cancellationToken));
if (psbt == null)
throw new NotSupportedException("You need to update your version of NBXplorer");
if (network.MinFee != null)
{
psbt.PSBT.TryGetFee(out var fee);
if (fee < network.MinFee)
{
psbtRequest.FeePreference = new FeePreference() { ExplicitFee = network.MinFee };
psbt = (await nbx.CreatePSBTAsync(derivationSettings.AccountDerivation, psbtRequest, cancellationToken));
}
}
if (derivationSettings.AccountKeyPath != null && derivationSettings.AccountKeyPath.Indexes.Length != 0)
{
// NBX only know the path relative to the account xpub.
// Here we rebase the hd_keys in the PSBT to have a keypath relative to the root HD so the wallet can sign
// Note that the fingerprint of the hd keys are now 0, which is wrong
// However, hardware wallets does not give a damn, and sometimes does not even allow us to get this fingerprint anyway.
foreach (var o in psbt.PSBT.Inputs.OfType<PSBTCoin>().Concat(psbt.PSBT.Outputs))
{
var rootFP = derivationSettings.RootFingerprint is HDFingerprint fp ? fp : default;
foreach (var keypath in o.HDKeyPaths.ToList())
{
var newKeyPath = derivationSettings.AccountKeyPath.Derive(keypath.Value.Item2);
o.HDKeyPaths.Remove(keypath.Key);
o.HDKeyPaths.Add(keypath.Key, Tuple.Create(rootFP, newKeyPath));
}
}
}
return psbt;
NoChange = vm.NoChange
});
}
[HttpGet]
@ -325,7 +254,7 @@ namespace BTCPayServer.Controllers
if (walletId?.StoreId == null)
return NotFound();
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
DerivationSchemeSettings paymentMethod = GetPaymentMethod(walletId, store);
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
if (paymentMethod == null)
return NotFound();
var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode);
@ -356,16 +285,19 @@ namespace BTCPayServer.Controllers
if (walletId?.StoreId == null)
return NotFound();
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
DerivationSchemeSettings paymentMethod = GetPaymentMethod(walletId, store);
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
if (paymentMethod == null)
return NotFound();
var vm = new RescanWalletModel();
vm.IsFullySync = _dashboard.IsFullySynched(walletId.CryptoCode, out var unused);
// We need to ensure it is segwit,
// because hardware wallet support need the parent transactions to sign, which NBXplorer don't have. (Nor does a pruned node)
vm.IsSegwit = paymentMethod.DerivationStrategyBase.IsSegwit();
vm.IsServerAdmin = User.Claims.Any(c => c.Type == Policies.CanModifyServerSettings.Key && c.Value == "true");
vm.IsSupportedByCurrency = _dashboard.Get(walletId.CryptoCode)?.Status?.BitcoinStatus?.Capabilities?.CanScanTxoutSet == true;
var explorer = ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode);
var scanProgress = await explorer.GetScanUTXOSetInformationAsync(paymentMethod.AccountDerivation);
var scanProgress = await explorer.GetScanUTXOSetInformationAsync(paymentMethod.DerivationStrategyBase);
if (scanProgress != null)
{
vm.PreviousError = scanProgress.Error;
@ -400,13 +332,13 @@ namespace BTCPayServer.Controllers
if (walletId?.StoreId == null)
return NotFound();
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
DerivationSchemeSettings paymentMethod = GetPaymentMethod(walletId, store);
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
if (paymentMethod == null)
return NotFound();
var explorer = ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode);
try
{
await explorer.ScanUTXOSetAsync(paymentMethod.AccountDerivation, vm.BatchSize, vm.GapLimit, vm.StartingIndex);
await explorer.ScanUTXOSetAsync(paymentMethod.DerivationStrategyBase, vm.BatchSize, vm.GapLimit, vm.StartingIndex);
}
catch (NBXplorerException ex) when (ex.Error.Code == "scanutxoset-in-progress")
{
@ -428,14 +360,14 @@ namespace BTCPayServer.Controllers
return null;
}
private DerivationSchemeSettings GetPaymentMethod(WalletId walletId, StoreData store)
private DerivationStrategy GetPaymentMethod(WalletId walletId, StoreData store)
{
if (store == null || !store.HasClaim(Policies.CanModifyStoreSettings.Key))
return null;
var paymentMethod = store
.GetSupportedPaymentMethods(NetworkProvider)
.OfType<DerivationSchemeSettings>()
.OfType<DerivationStrategy>()
.FirstOrDefault(p => p.PaymentId.PaymentType == Payments.PaymentTypes.BTCLike && p.PaymentId.CryptoCode == walletId.CryptoCode);
return paymentMethod;
}
@ -482,7 +414,7 @@ namespace BTCPayServer.Controllers
int account = 0,
// sendtoaddress
bool noChange = false,
string destination = null, string amount = null, string feeRate = null, bool substractFees = false, bool disableRBF = false
string destination = null, string amount = null, string feeRate = null, string substractFees = null
)
{
if (!HttpContext.WebSockets.IsWebSocketRequest)
@ -490,7 +422,7 @@ namespace BTCPayServer.Controllers
var cryptoCode = walletId.CryptoCode;
var storeData = (await Repository.FindStore(walletId.StoreId, GetUserId()));
var derivationSettings = GetPaymentMethod(walletId, storeData);
var derivationScheme = GetPaymentMethod(walletId, storeData).DerivationStrategyBase;
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
@ -499,7 +431,6 @@ namespace BTCPayServer.Controllers
{
normalOperationTimeout.CancelAfter(TimeSpan.FromMinutes(30));
var hw = new HardwareWalletService(webSocket);
var model = new WalletSendLedgerModel();
object result = null;
try
{
@ -511,42 +442,51 @@ namespace BTCPayServer.Controllers
throw new FormatException("Invalid value for crypto code");
}
BitcoinAddress destinationAddress = null;
if (destination != null)
{
try
{
BitcoinAddress.Create(destination.Trim(), network.NBitcoinNetwork);
model.Destination = destination.Trim();
destinationAddress = BitcoinAddress.Create(destination.Trim(), network.NBitcoinNetwork);
}
catch { }
if (destinationAddress == null)
throw new FormatException("Invalid value for destination");
}
FeeRate feeRateValue = null;
if (feeRate != null)
{
try
{
model.FeeSatoshiPerByte = int.Parse(feeRate, CultureInfo.InvariantCulture);
feeRateValue = new FeeRate(Money.Satoshis(int.Parse(feeRate, CultureInfo.InvariantCulture)), 1);
}
catch { }
if (model.FeeSatoshiPerByte <= 0)
if (feeRateValue == null || feeRateValue.FeePerK <= Money.Zero)
throw new FormatException("Invalid value for fee rate");
}
Money amountBTC = null;
if (amount != null)
{
try
{
model.Amount = Money.Parse(amount).ToDecimal(MoneyUnit.BTC);
amountBTC = Money.Parse(amount);
}
catch { }
if (model.Amount <= 0m)
if (amountBTC == null || amountBTC <= Money.Zero)
throw new FormatException("Invalid value for amount");
}
model.SubstractFees = substractFees;
model.NoChange = noChange;
model.DisableRBF = disableRBF;
bool subsctractFeesValue = false;
if (substractFees != null)
{
try
{
subsctractFeesValue = bool.Parse(substractFees);
}
catch { throw new FormatException("Invalid value for subtract fees"); }
}
if (command == "test")
{
result = await hw.Test(normalOperationTimeout.Token);
@ -555,46 +495,126 @@ namespace BTCPayServer.Controllers
{
if (!_dashboard.IsFullySynched(network.CryptoCode, out var summary))
throw new Exception($"{network.CryptoCode}: not started or fully synched");
var strategy = GetDirectDerivationStrategy(derivationScheme);
var wallet = _walletProvider.GetWallet(network);
var change = wallet.GetChangeAddressAsync(derivationScheme);
var keypaths = new Dictionary<Script, KeyPath>();
List<Coin> availableCoins = new List<Coin>();
foreach (var c in await wallet.GetUnspentCoins(derivationScheme))
{
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
availableCoins.Add(c.Coin);
}
var changeAddress = await change;
var strategy = GetDirectDerivationStrategy(derivationSettings.AccountDerivation);
var storeBlob = storeData.GetStoreBlob();
var paymentId = new Payments.PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike);
var foundKeyPath = storeBlob.GetWalletKeyPathRoot(paymentId);
// Some deployment have the wallet root key path saved in the store blob
// If it does, we only have to make 1 call to the hw to check if it can sign the given strategy,
if (derivationSettings.AccountKeyPath == null || !await hw.CanSign(network, strategy, derivationSettings.AccountKeyPath, normalOperationTimeout.Token))
if (foundKeyPath == null || !await hw.CanSign(network, strategy, foundKeyPath, normalOperationTimeout.Token))
{
// 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.FindKeyPath(network, strategy, normalOperationTimeout.Token);
foundKeyPath = await hw.FindKeyPath(network, strategy, normalOperationTimeout.Token);
if (foundKeyPath == null)
throw new HardwareWalletException($"This store is not configured to use this ledger");
derivationSettings.AccountKeyPath = foundKeyPath;
storeData.SetSupportedPaymentMethod(derivationSettings);
storeBlob.SetWalletKeyPathRoot(paymentId, foundKeyPath);
storeData.SetStoreBlob(storeBlob);
await Repository.UpdateStore(storeData);
}
retry:
var send = new[] { (
destination: destinationAddress as IDestination,
amount: amountBTC,
substractFees: subsctractFeesValue) };
var psbt = await CreatePSBT(network, derivationSettings, model, normalOperationTimeout.Token);
signTimeout.CancelAfter(TimeSpan.FromMinutes(5));
psbt.PSBT = await hw.SignTransactionAsync(psbt.PSBT, psbt.ChangeAddress?.ScriptPubKey, signTimeout.Token);
if(!psbt.PSBT.TryFinalize(out var errors))
foreach (var element in send)
{
throw new Exception($"Error while finalizing the transaction ({new PSBTException(errors).ToString()})");
if (element.destination == null)
throw new ArgumentNullException(nameof(element.destination));
if (element.amount == null)
throw new ArgumentNullException(nameof(element.amount));
if (element.amount <= Money.Zero)
throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero");
}
var transaction = psbt.PSBT.ExtractTransaction();
TransactionBuilder builder = network.NBitcoinNetwork.CreateTransactionBuilder();
builder.StandardTransactionPolicy.MinRelayTxFee = summary.Status.BitcoinStatus.MinRelayTxFee;
builder.AddCoins(availableCoins);
foreach (var element in send)
{
builder.Send(element.destination, element.amount);
if (element.substractFees)
builder.SubtractFees();
}
builder.SetChange(changeAddress.Item1);
if (network.MinFee == null)
{
builder.SendEstimatedFees(feeRateValue);
}
else
{
var estimatedFee = builder.EstimateFees(feeRateValue);
if (network.MinFee > estimatedFee)
builder.SendFees(network.MinFee);
else
builder.SendEstimatedFees(feeRateValue);
}
var unsigned = builder.BuildTransaction(false);
var hasChange = unsigned.Outputs.Any(o => o.ScriptPubKey == changeAddress.Item1.ScriptPubKey);
if (noChange && hasChange)
{
availableCoins = builder.FindSpentCoins(unsigned).Cast<Coin>().ToList();
amountBTC = builder.FindSpentCoins(unsigned).Select(c => c.TxOut.Value).Sum();
subsctractFeesValue = true;
goto retry;
}
var usedCoins = builder.FindSpentCoins(unsigned);
Dictionary<uint256, Transaction> parentTransactions = new Dictionary<uint256, Transaction>();
if (!strategy.Segwit)
{
var parentHashes = usedCoins.Select(c => c.Outpoint.Hash).ToHashSet();
var explorer = ExplorerClientProvider.GetExplorerClient(network);
var getTransactionAsyncs = parentHashes.Select(h => (Op: explorer.GetTransactionAsync(h), Hash: h)).ToList();
foreach (var getTransactionAsync in getTransactionAsyncs)
{
var tx = (await getTransactionAsync.Op);
if (tx == null)
throw new Exception($"Parent transaction {getTransactionAsync.Hash} not found");
parentTransactions.Add(tx.Transaction.GetHash(), tx.Transaction);
}
}
signTimeout.CancelAfter(TimeSpan.FromMinutes(5));
var transaction = await hw.SignTransactionAsync(usedCoins.Select(c => new SignatureRequest
{
InputTransaction = parentTransactions.TryGet(c.Outpoint.Hash),
InputCoin = c,
KeyPath = foundKeyPath.Derive(keypaths[c.TxOut.ScriptPubKey]),
PubKey = strategy.Root.Derive(keypaths[c.TxOut.ScriptPubKey]).PubKey
}).ToArray(), unsigned, hasChange ? foundKeyPath.Derive(changeAddress.Item2) : null, signTimeout.Token);
try
{
var broadcastResult = await ExplorerClientProvider.GetExplorerClient(network).BroadcastAsync(transaction);
if (!broadcastResult.Success)
var broadcastResult = await wallet.BroadcastTransactionsAsync(new List<Transaction>() { transaction });
if (!broadcastResult[0].Success)
{
throw new Exception($"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}");
throw new Exception($"RPC Error while broadcasting: {broadcastResult[0].RPCCode} {broadcastResult[0].RPCCodeMessage} {broadcastResult[0].RPCMessage}");
}
}
catch (Exception ex)
{
throw new Exception("Error while broadcasting: " + ex.Message);
}
var wallet = _walletProvider.GetWallet(network);
wallet.InvalidateCache(derivationSettings.AccountDerivation);
wallet.InvalidateCache(derivationScheme);
result = new SendToAddressResult() { TransactionId = transaction.GetHash().ToString() };
}
}

View File

@ -1,10 +1,12 @@
using System.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using BTCPayServer.Models;
using BTCPayServer.Services.PaymentRequests;
using BTCPayServer.Services.U2F.Models;
using BTCPayServer.Storage.Models;
using Microsoft.EntityFrameworkCore.Infrastructure.Internal;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace BTCPayServer.Data
@ -91,18 +93,10 @@ namespace BTCPayServer.Data
}
public DbSet<APIKeyData> ApiKeys
{
get; set;
}
public DbSet<StoredFile> Files
{
get; set;
}
public DbSet<U2FDevice> U2FDevices { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var isConfigured = optionsBuilder.Options.Extensions.OfType<RelationalOptionsExtension>().Any();
@ -227,5 +221,4 @@ namespace BTCPayServer.Data
.HasIndex(o => o.Status);
}
}
}

View File

@ -12,6 +12,11 @@ namespace BTCPayServer.Data
get; set;
}
public string Facade
{
get; set;
}
public string StoreDataId
{
get; set;

View File

@ -11,7 +11,7 @@ namespace BTCPayServer.Data
{
get; set;
}
[Obsolete("Unused")]
public string Facade
{
get; set;

View File

@ -73,7 +73,7 @@ namespace BTCPayServer.Data
if (networks.BTC != null)
{
btcReturned = true;
yield return DerivationSchemeSettings.Parse(DerivationStrategy, networks.BTC);
yield return BTCPayServer.DerivationStrategy.Parse(DerivationStrategy, networks.BTC);
}
}
@ -98,11 +98,6 @@ namespace BTCPayServer.Data
#pragma warning restore CS0618
}
public void SetSupportedPaymentMethod(ISupportedPaymentMethod supportedPaymentMethod)
{
SetSupportedPaymentMethod(null, supportedPaymentMethod);
}
/// <summary>
/// Set or remove a new supported payment method for the store
/// </summary>
@ -110,16 +105,8 @@ namespace BTCPayServer.Data
/// <param name="supportedPaymentMethod">The payment method, or null to remove</param>
public void SetSupportedPaymentMethod(PaymentMethodId paymentMethodId, ISupportedPaymentMethod supportedPaymentMethod)
{
if (supportedPaymentMethod != null && paymentMethodId != null && paymentMethodId != supportedPaymentMethod.PaymentId)
{
throw new InvalidOperationException("Incoherent arguments, this should never happen");
}
if (supportedPaymentMethod == null && paymentMethodId == null)
throw new ArgumentException($"{nameof(supportedPaymentMethod)} or {nameof(paymentMethodId)} should be specified");
if (supportedPaymentMethod != null && paymentMethodId == null)
{
paymentMethodId = supportedPaymentMethod.PaymentId;
}
if (supportedPaymentMethod != null && paymentMethodId != supportedPaymentMethod.PaymentId)
throw new InvalidOperationException("Argument mismatch");
#pragma warning disable CS0618
JObject strategies = string.IsNullOrEmpty(DerivationStrategies) ? new JObject() : JObject.Parse(DerivationStrategies);
@ -147,7 +134,7 @@ namespace BTCPayServer.Data
}
}
if (!existing && supportedPaymentMethod == null && supportedPaymentMethod.PaymentId.IsBTCOnChain)
if (!existing && supportedPaymentMethod == null && paymentMethodId.IsBTCOnChain)
{
DerivationStrategy = null;
}
@ -320,25 +307,6 @@ namespace BTCPayServer.Data
public bool RequiresRefundEmail { get; set; }
CurrencyPair[] _DefaultCurrencyPairs;
[JsonProperty("defaultCurrencyPairs", ItemConverterType = typeof(CurrencyPairJsonConverter))]
public CurrencyPair[] DefaultCurrencyPairs
{
get
{
return _DefaultCurrencyPairs ?? Array.Empty<CurrencyPair>();
}
set
{
_DefaultCurrencyPairs = value;
}
}
public string GetDefaultCurrencyPairString()
{
return string.Join(',', DefaultCurrencyPairs.Select(c => c.ToString()));
}
public string DefaultLang { get; set; }
[DefaultValue(60)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
@ -362,11 +330,10 @@ namespace BTCPayServer.Data
public List<RateRule_Obsolete> RateRules { get; set; } = new List<RateRule_Obsolete>();
public string PreferredExchange { get; set; }
[JsonConverter(typeof(CurrencyValueJsonConverter))]
public CurrencyValue OnChainMinValue { get; set; }
[JsonConverter(typeof(CurrencyValueJsonConverter))]
public CurrencyValue LightningMaxValue { get; set; }
public bool LightningAmountInSatoshi { get; set; }
[JsonConverter(typeof(CurrencyValueJsonConverter))]
public CurrencyValue OnChainMinValue { get; set; }
[JsonConverter(typeof(UriJsonConverter))]
public Uri CustomLogo { get; set; }
@ -443,12 +410,25 @@ namespace BTCPayServer.Data
[Obsolete("Use GetExcludedPaymentMethods instead")]
public string[] ExcludedPaymentMethods { get; set; }
[Obsolete("Use DerivationSchemeSettings instead")]
public Dictionary<string, string> WalletKeyPathRoots { get; set; }
#pragma warning disable CS0618 // Type or member is obsolete
public void SetWalletKeyPathRoot(PaymentMethodId paymentMethodId, KeyPath keyPath)
{
if (keyPath == null)
WalletKeyPathRoots.Remove(paymentMethodId.ToString());
else
WalletKeyPathRoots.AddOrReplace(paymentMethodId.ToString().ToLowerInvariant(), keyPath.ToString());
}
public KeyPath GetWalletKeyPathRoot(PaymentMethodId paymentMethodId)
{
if (WalletKeyPathRoots.TryGetValue(paymentMethodId.ToString().ToLowerInvariant(), out var k))
return KeyPath.Parse(k);
return null;
}
#pragma warning restore CS0618 // Type or member is obsolete
[Obsolete("Use SetWalletKeyPathRoot/GetWalletKeyPathRoot instead")]
public Dictionary<string, string> WalletKeyPathRoots { get; set; } = new Dictionary<string, string>();
public EmailSettings EmailSettings { get; set; }
public bool RedirectAutomatically { get; set; }
public IPaymentFilter GetExcludedPaymentMethods()
{

View File

@ -12,51 +12,14 @@ namespace BTCPayServer
{
public class DerivationSchemeParser
{
public BTCPayNetwork BtcPayNetwork { get; }
public Network Network => BtcPayNetwork.NBitcoinNetwork;
public Network Network { get; set; }
public Script HintScriptPubKey { get; set; }
Dictionary<uint, string[]> ElectrumMapping = new Dictionary<uint, string[]>();
public DerivationSchemeParser(BTCPayNetwork expectedNetwork)
public DerivationSchemeParser(Network expectedNetwork)
{
if (expectedNetwork == null)
throw new ArgumentNullException(nameof(expectedNetwork));
BtcPayNetwork = expectedNetwork;
Network = expectedNetwork;
}
public DerivationStrategyBase ParseElectrum(string str)
{
if (str == null)
throw new ArgumentNullException(nameof(str));
str = str.Trim();
var data = Network.GetBase58CheckEncoder().DecodeData(str);
if (data.Length < 4)
throw new FormatException();
var prefix = Utils.ToUInt32(data, false);
var standardPrefix = Utils.ToBytes(0x0488b21eU, false);
for (int ii = 0; ii < 4; ii++)
data[ii] = standardPrefix[ii];
var extPubKey = new BitcoinExtPubKey(Network.GetBase58CheckEncoder().EncodeData(data), Network.Main).ToNetwork(Network);
if (!BtcPayNetwork.ElectrumMapping.TryGetValue(prefix, out var type))
{
throw new FormatException();
}
if (type == DerivationType.Segwit)
return new DirectDerivationStrategy(extPubKey) { Segwit = true };
if (type == DerivationType.Legacy)
return new DirectDerivationStrategy(extPubKey) { Segwit = false };
if (type == DerivationType.SegwitP2SH)
return new DerivationStrategyFactory(Network).Parse(extPubKey.ToString() + "-[p2sh]");
throw new FormatException();
}
public DerivationStrategyBase Parse(string str)
{
if (str == null)
@ -78,7 +41,7 @@ namespace BTCPayServer
}
}
if (!Network.Consensus.SupportSegwit)
if(!Network.Consensus.SupportSegwit)
hintedLabels.Add("legacy");
try
@ -90,6 +53,15 @@ namespace BTCPayServer
{
}
Dictionary<uint, string[]> electrumMapping = new Dictionary<uint, string[]>();
//Source https://github.com/spesmilo/electrum/blob/9edffd17542de5773e7284a8c8a2673c766bb3c3/lib/bitcoin.py
var standard = 0x0488b21eU;
electrumMapping.Add(standard, new[] { "legacy" });
var p2wpkh_p2sh = 0x049d7cb2U;
electrumMapping.Add(p2wpkh_p2sh, new string[] { "p2sh" });
var p2wpkh = 0x4b24746U;
electrumMapping.Add(p2wpkh, Array.Empty<string>());
var parts = str.Split('-');
bool hasLabel = false;
for (int i = 0; i < parts.Length; i++)
@ -112,22 +84,17 @@ namespace BTCPayServer
if (data.Length < 4)
continue;
var prefix = Utils.ToUInt32(data, false);
var standardPrefix = Utils.ToBytes(0x0488b21eU, false);
var standardPrefix = Utils.ToBytes(Network.NetworkType == NetworkType.Mainnet ? 0x0488b21eU : 0x043587cf, false);
for (int ii = 0; ii < 4; ii++)
data[ii] = standardPrefix[ii];
var derivationScheme = new BitcoinExtPubKey(Network.GetBase58CheckEncoder().EncodeData(data), Network.Main).ToNetwork(Network).ToString();
if (BtcPayNetwork.ElectrumMapping.TryGetValue(prefix, out var type))
var derivationScheme = new BitcoinExtPubKey(Network.GetBase58CheckEncoder().EncodeData(data), Network).ToString();
electrumMapping.TryGetValue(prefix, out string[] labels);
if (labels != null)
{
switch (type)
foreach (var label in labels)
{
case DerivationType.Legacy:
hintedLabels.Add("legacy");
break;
case DerivationType.SegwitP2SH:
hintedLabels.Add("p2sh");
break;
hintedLabels.Add(label.ToLowerInvariant());
}
}
parts[i] = derivationScheme;
@ -169,7 +136,7 @@ namespace BTCPayServer
resultNoLabels = string.Join('-', resultNoLabels.Split('-').Where(p => !IsLabel(p)));
foreach (var labels in ItemCombinations(hintLabels.ToList()))
{
var hinted = facto.Parse(resultNoLabels + '-' + string.Join('-', labels.Select(l => $"[{l}]").ToArray()));
var hinted = facto.Parse(resultNoLabels + '-' + string.Join('-', labels.Select(l=>$"[{l}]").ToArray()));
if (HintScriptPubKey == hinted.Derive(firstKeyPath).ScriptPubKey)
return hinted;
}
@ -182,20 +149,20 @@ namespace BTCPayServer
}
/// <summary>
/// Method to create lists containing possible combinations of an input list of items. This is
/// basically copied from code by user "jaolho" on this thread:
/// http://stackoverflow.com/questions/7802822/all-possible-combinations-of-a-list-of-values
/// </summary>
/// <typeparam name="T">type of the items on the input list</typeparam>
/// <param name="inputList">list of items</param>
/// <param name="minimumItems">minimum number of items wanted in the generated combinations,
/// if zero the empty combination is included,
/// default is one</param>
/// <param name="maximumItems">maximum number of items wanted in the generated combinations,
/// default is no maximum limit</param>
/// <returns>list of lists for possible combinations of the input items</returns>
public static List<List<T>> ItemCombinations<T>(List<T> inputList, int minimumItems = 1,
int maximumItems = int.MaxValue)
/// Method to create lists containing possible combinations of an input list of items. This is
/// basically copied from code by user "jaolho" on this thread:
/// http://stackoverflow.com/questions/7802822/all-possible-combinations-of-a-list-of-values
/// </summary>
/// <typeparam name="T">type of the items on the input list</typeparam>
/// <param name="inputList">list of items</param>
/// <param name="minimumItems">minimum number of items wanted in the generated combinations,
/// if zero the empty combination is included,
/// default is one</param>
/// <param name="maximumItems">maximum number of items wanted in the generated combinations,
/// default is no maximum limit</param>
/// <returns>list of lists for possible combinations of the input items</returns>
public static List<List<T>> ItemCombinations<T>(List<T> inputList, int minimumItems = 1,
int maximumItems = int.MaxValue)
{
int nonEmptyCombinations = (int)Math.Pow(2, inputList.Count) - 1;
List<List<T>> listOfLists = new List<List<T>>(nonEmptyCombinations + 1);

View File

@ -1,156 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Payments;
using NBitcoin;
using NBXplorer.DerivationStrategy;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer
{
public class DerivationSchemeSettings : ISupportedPaymentMethod
{
public static DerivationSchemeSettings Parse(string derivationStrategy, BTCPayNetwork network)
{
if (network == null)
throw new ArgumentNullException(nameof(network));
if (derivationStrategy == null)
throw new ArgumentNullException(nameof(derivationStrategy));
var result = new NBXplorer.DerivationStrategy.DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationStrategy);
return new DerivationSchemeSettings(result, network) { AccountOriginal = derivationStrategy.Trim() };
}
public static bool TryParseFromJson(string config, BTCPayNetwork network, out DerivationSchemeSettings strategy)
{
if (network == null)
throw new ArgumentNullException(nameof(network));
if (config == null)
throw new ArgumentNullException(nameof(config));
strategy = null;
try
{
strategy = network.NBXplorerNetwork.Serializer.ToObject<DerivationSchemeSettings>(config);
strategy.Network = network;
}
catch { }
return strategy != null;
}
public static bool TryParseFromColdcard(string coldcardExport, BTCPayNetwork network, out DerivationSchemeSettings settings)
{
settings = null;
if (coldcardExport == null)
throw new ArgumentNullException(nameof(coldcardExport));
if (network == null)
throw new ArgumentNullException(nameof(network));
var result = new DerivationSchemeSettings();
result.Source = "Coldcard";
var derivationSchemeParser = new DerivationSchemeParser(network);
JObject jobj = null;
try
{
jobj = JObject.Parse(coldcardExport);
jobj = (JObject)jobj["keystore"];
}
catch
{
return false;
}
if (jobj.ContainsKey("xpub"))
{
try
{
result.AccountOriginal = jobj["xpub"].Value<string>().Trim();
result.AccountDerivation = derivationSchemeParser.ParseElectrum(result.AccountOriginal);
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
}
catch
{
return false;
}
}
else
{
return false;
}
if (jobj.ContainsKey("label"))
{
try
{
result.Label = jobj["label"].Value<string>();
}
catch { return false; }
}
if (jobj.ContainsKey("ckcc_xfp"))
{
try
{
result.RootFingerprint = new HDFingerprint(jobj["ckcc_xfp"].Value<uint>());
}
catch { return false; }
}
if (jobj.ContainsKey("derivation"))
{
try
{
result.AccountKeyPath = new KeyPath(jobj["derivation"].Value<string>());
}
catch { return false; }
}
settings = result;
settings.Network = network;
return true;
}
public DerivationSchemeSettings()
{
}
public DerivationSchemeSettings(DerivationStrategyBase derivationStrategy, BTCPayNetwork network)
{
if (network == null)
throw new ArgumentNullException(nameof(network));
if (derivationStrategy == null)
throw new ArgumentNullException(nameof(derivationStrategy));
AccountDerivation = derivationStrategy;
Network = network;
}
[JsonIgnore]
public BTCPayNetwork Network { get; set; }
public string Source { get; set; }
public KeyPath AccountKeyPath { get; set; }
public DerivationStrategyBase AccountDerivation { get; set; }
public string AccountOriginal { get; set; }
public HDFingerprint? RootFingerprint { get; set; }
public string Label { get; set; }
[JsonIgnore]
public PaymentMethodId PaymentId => new PaymentMethodId(Network.CryptoCode, PaymentTypes.BTCLike);
public override string ToString()
{
return AccountDerivation.ToString();
}
public string ToPrettyString()
{
return !string.IsNullOrEmpty(Label) ? Label :
!String.IsNullOrEmpty(AccountOriginal) ? AccountOriginal :
ToString();
}
public string ToJson()
{
return Network.NBXplorerNetwork.Serializer.ToString(this);
}
}
}

View File

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Payments;
using BTCPayServer.Services.Invoices;
using NBitcoin;
using NBXplorer.DerivationStrategy;
namespace BTCPayServer
{
public class DerivationStrategy : ISupportedPaymentMethod
{
private DerivationStrategyBase _DerivationStrategy;
private BTCPayNetwork _Network;
public DerivationStrategy(DerivationStrategyBase result, BTCPayNetwork network)
{
this._DerivationStrategy = result;
this._Network = network;
}
public static DerivationStrategy Parse(string derivationStrategy, BTCPayNetwork network)
{
if (network == null)
throw new ArgumentNullException(nameof(network));
if (derivationStrategy == null)
throw new ArgumentNullException(nameof(derivationStrategy));
var result = new NBXplorer.DerivationStrategy.DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationStrategy);
return new DerivationStrategy(result, network);
}
public BTCPayNetwork Network { get { return this._Network; } }
public DerivationStrategyBase DerivationStrategyBase => this._DerivationStrategy;
public PaymentMethodId PaymentId => new PaymentMethodId(Network.CryptoCode, PaymentTypes.BTCLike);
public override string ToString()
{
return _DerivationStrategy.ToString();
}
}
}

View File

@ -1,5 +1,4 @@
using System;
using System.Net.Http;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
@ -18,7 +17,7 @@ namespace BTCPayServer
public BTCPayNetworkProvider NetworkProviders => _NetworkProviders;
NBXplorerDashboard _Dashboard;
public ExplorerClientProvider(IHttpClientFactory httpClientFactory, BTCPayNetworkProvider networkProviders, BTCPayServerOptions options, NBXplorerDashboard dashboard)
public ExplorerClientProvider(BTCPayNetworkProvider networkProviders, BTCPayServerOptions options, NBXplorerDashboard dashboard)
{
_Dashboard = dashboard;
_NetworkProviders = networkProviders;
@ -33,15 +32,14 @@ namespace BTCPayServer
Logs.Configuration.LogInformation($"{setting.CryptoCode}: Cookie file is {(setting.CookieFile ?? "not set")}");
if (setting.ExplorerUri != null)
{
_Clients.TryAdd(setting.CryptoCode, CreateExplorerClient(httpClientFactory.CreateClient($"NBXPLORER_{setting.CryptoCode}"), _NetworkProviders.GetNetwork(setting.CryptoCode), setting.ExplorerUri, setting.CookieFile));
_Clients.TryAdd(setting.CryptoCode, CreateExplorerClient(_NetworkProviders.GetNetwork(setting.CryptoCode), setting.ExplorerUri, setting.CookieFile));
}
}
}
private static ExplorerClient CreateExplorerClient(HttpClient httpClient, BTCPayNetwork n, Uri uri, string cookieFile)
private static ExplorerClient CreateExplorerClient(BTCPayNetwork n, Uri uri, string cookieFile)
{
var explorer = new ExplorerClient(n.NBXplorerNetwork, uri);
explorer.SetClient(httpClient);
if (cookieFile == null)
{
Logs.Configuration.LogWarning($"{n.CryptoCode}: Not using cookie authentication");

View File

@ -33,7 +33,6 @@ using BTCPayServer.Services;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using NBXplorer.DerivationStrategy;
using System.Net;
namespace BTCPayServer
{
@ -126,18 +125,6 @@ namespace BTCPayServer
return str;
return str + "/";
}
public static string WithStartingSlash(this string str)
{
if (str.StartsWith("/", StringComparison.InvariantCulture))
return str;
return $"/{str}";
}
public static string WithoutEndingSlash(this string str)
{
if (str.EndsWith("/", StringComparison.InvariantCulture))
return str.Substring(0, str.Length - 1);
return str;
}
public static void SetHeaderOnStarting(this HttpResponse resp, string name, string value)
{
@ -172,31 +159,6 @@ namespace BTCPayServer
(derivationStrategyBase is DirectDerivationStrategy direct) && direct.Segwit;
}
public static bool IsLocalNetwork(string server)
{
if (server == null)
throw new ArgumentNullException(nameof(server));
if (Uri.CheckHostName(server) == UriHostNameType.Dns)
{
return server.EndsWith(".internal", StringComparison.OrdinalIgnoreCase) ||
server.EndsWith(".local", StringComparison.OrdinalIgnoreCase) ||
server.EndsWith(".lan", StringComparison.OrdinalIgnoreCase) ||
server.IndexOf('.', StringComparison.OrdinalIgnoreCase) == -1;
}
if(IPAddress.TryParse(server, out var ip))
{
return ip.IsLocal();
}
return false;
}
public static bool IsOnion(this HttpRequest request)
{
if (request?.Host.Host == null)
return false;
return request.Host.Host.EndsWith(".onion", StringComparison.OrdinalIgnoreCase);
}
public static string GetAbsoluteRoot(this HttpRequest request)
{
return string.Concat(
@ -249,10 +211,8 @@ namespace BTCPayServer
/// <returns></returns>
public static string GetRelativePathOrAbsolute(this HttpRequest request, string path)
{
if (!Uri.TryCreate(path, UriKind.RelativeOrAbsolute, out var uri) ||
uri.IsAbsoluteUri)
if (Uri.TryCreate(path, UriKind.Absolute, out var unused))
return path;
if (path.Length > 0 && path[0] != '/')
path = $"/{path}";
return string.Concat(
@ -268,31 +228,6 @@ namespace BTCPayServer
return isRelative ? request.GetAbsoluteRoot() + redirectUrl : redirectUrl;
}
/// <summary>
/// Will return an absolute URL.
/// If `relativeOrAsbolute` is absolute, returns it.
/// If `relativeOrAsbolute` is relative, send absolute url based on the HOST of this request (without PathBase)
/// </summary>
/// <param name="request"></param>
/// <param name="relativeOrAbsolte"></param>
/// <returns></returns>
public static Uri GetAbsoluteUriNoPathBase(this HttpRequest request, Uri relativeOrAbsolute = null)
{
if (relativeOrAbsolute == null)
{
return new Uri(string.Concat(
request.Scheme,
"://",
request.Host.ToUriComponent()), UriKind.Absolute);
}
if (relativeOrAbsolute.IsAbsoluteUri)
return relativeOrAbsolute;
return new Uri(string.Concat(
request.Scheme,
"://",
request.Host.ToUriComponent()) + relativeOrAbsolute.ToString().WithStartingSlash(), UriKind.Absolute);
}
public static IServiceCollection ConfigureBTCPayServer(this IServiceCollection services, IConfiguration conf)
{
services.Configure<BTCPayServerOptions>(o =>
@ -335,6 +270,30 @@ namespace BTCPayServer
NBitcoin.Extensions.TryAdd(ctx.Items, "BitpayAuth", value);
}
public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
{
using (var delayCTS = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
{
var waiting = Task.Delay(-1, delayCTS.Token);
var doing = task;
await Task.WhenAny(waiting, doing);
delayCTS.Cancel();
cancellationToken.ThrowIfCancellationRequested();
return await doing;
}
}
public static async Task WithCancellation(this Task task, CancellationToken cancellationToken)
{
using (var delayCTS = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
{
var waiting = Task.Delay(-1, delayCTS.Token);
var doing = task;
await Task.WhenAny(waiting, doing);
delayCTS.Cancel();
cancellationToken.ThrowIfCancellationRequested();
}
}
public static (string Signature, String Id, String Authorization) GetBitpayAuth(this HttpContext ctx)
{
ctx.Items.TryGetValue("BitpayAuth", out object obj);
@ -356,15 +315,5 @@ namespace BTCPayServer
var res = JsonConvert.SerializeObject(o, Formatting.None, jsonSettings);
return res;
}
public static string TrimEnd(this string input, string suffixToRemove,
StringComparison comparisonType) {
if (input != null && suffixToRemove != null
&& input.EndsWith(suffixToRemove, comparisonType)) {
return input.Substring(0, input.Length - suffixToRemove.Length);
}
else return input;
}
}
}

View File

@ -1,5 +1,4 @@
using System;
using NBitcoin;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
@ -35,8 +34,6 @@ namespace BTCPayServer.HostedServices
public async Task StopAsync(CancellationToken cancellationToken)
{
if (_Stop == null)
return;
_Stop.Cancel();
try
{
@ -46,14 +43,7 @@ namespace BTCPayServer.HostedServices
{
}
try
{
await BackgroundJobClient.WaitAllRunning(cancellationToken);
}
catch (OperationCanceledException)
{
}
await BackgroundJobClient.WaitAllRunning(cancellationToken);
}
}
@ -61,10 +51,10 @@ namespace BTCPayServer.HostedServices
{
class BackgroundJob
{
public Func<CancellationToken, Task> Action;
public Func<Task> Action;
public TimeSpan Delay;
public IDelay DelayImplementation;
public BackgroundJob(Func<CancellationToken, Task> action, TimeSpan delay, IDelay delayImplementation)
public BackgroundJob(Func<Task> action, TimeSpan delay, IDelay delayImplementation)
{
this.Action = action;
this.Delay = delay;
@ -74,7 +64,7 @@ namespace BTCPayServer.HostedServices
public async Task Run(CancellationToken cancellationToken)
{
await DelayImplementation.Wait(Delay, cancellationToken);
await Action(cancellationToken);
await Action();
}
}
@ -89,9 +79,9 @@ namespace BTCPayServer.HostedServices
private Channel<BackgroundJob> _Jobs = Channel.CreateUnbounded<BackgroundJob>();
HashSet<Task> _Processing = new HashSet<Task>();
public void Schedule(Func<CancellationToken, Task> act, TimeSpan scheduledIn)
public void Schedule(Func<Task> action, TimeSpan delay)
{
_Jobs.Writer.TryWrite(new BackgroundJob(act, scheduledIn, Delay));
_Jobs.Writer.TryWrite(new BackgroundJob(action, delay, Delay));
}
public async Task WaitAllRunning(CancellationToken cancellationToken)
@ -99,8 +89,6 @@ namespace BTCPayServer.HostedServices
Task[] processing = null;
lock (_Processing)
{
if (_Processing.Count == 0)
return;
processing = _Processing.ToArray();
}
@ -108,8 +96,9 @@ namespace BTCPayServer.HostedServices
{
await Task.WhenAll(processing).WithCancellation(cancellationToken);
}
catch (Exception) when (!cancellationToken.IsCancellationRequested)
catch (Exception) when (cancellationToken.IsCancellationRequested)
{
throw;
}
}
@ -124,7 +113,8 @@ namespace BTCPayServer.HostedServices
{
_Processing.Add(processing);
}
_ = processing.ContinueWith(t =>
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
processing.ContinueWith(t =>
{
if (t.IsFaulted)
{
@ -135,6 +125,7 @@ namespace BTCPayServer.HostedServices
_Processing.Remove(processing);
}
}, default, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
}
}
}

View File

@ -59,8 +59,6 @@ namespace BTCPayServer.HostedServices
public Task StopAsync(CancellationToken cancellationToken)
{
if (_Cts == null)
return Task.CompletedTask;
_Cts.Cancel();
return Task.WhenAll(_Tasks);
}

View File

@ -13,7 +13,6 @@ using BTCPayServer.Events;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Mvc.Filters;
using BTCPayServer.Security;
using BTCPayServer.Services.Apps;
namespace BTCPayServer.HostedServices
{
@ -45,20 +44,11 @@ namespace BTCPayServer.HostedServices
get { return _creativeStartUri; }
}
public bool ShowRegister { get; set; }
public bool DiscourageSearchEngines { get; set; }
public AppType? RootAppType { get; set; }
public string RootAppId { get; set; }
internal void Update(PoliciesSettings data)
{
ShowRegister = !data.LockSubscription;
DiscourageSearchEngines = data.DiscourageSearchEngines;
RootAppType = data.RootAppType;
RootAppId = data.RootAppId;
}
}

View File

@ -24,7 +24,7 @@ namespace BTCPayServer.HostedServices
{
public class InvoiceNotificationManager : IHostedService
{
HttpClient _Client;
public static HttpClient _Client = new HttpClient();
public class ScheduledJob
{
@ -46,7 +46,6 @@ namespace BTCPayServer.HostedServices
private readonly EmailSenderFactory _EmailSenderFactory;
public InvoiceNotificationManager(
IHttpClientFactory httpClientFactory,
IBackgroundJobClient jobClient,
EventAggregator eventAggregator,
InvoiceRepository invoiceRepository,
@ -54,7 +53,6 @@ namespace BTCPayServer.HostedServices
ILogger<InvoiceNotificationManager> logger,
EmailSenderFactory emailSenderFactory)
{
_Client = httpClientFactory.CreateClient();
_JobClient = jobClient;
_EventAggregator = eventAggregator;
_InvoiceRepository = invoiceRepository;
@ -137,29 +135,23 @@ namespace BTCPayServer.HostedServices
return;
var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Notification = notification });
if (!string.IsNullOrEmpty(invoice.NotificationURL))
_JobClient.Schedule((cancellation) => NotifyHttp(invoiceStr, cancellation), TimeSpan.Zero);
_JobClient.Schedule(() => NotifyHttp(invoiceStr), TimeSpan.Zero);
}
public async Task NotifyHttp(string invoiceData, CancellationToken cancellationToken)
public async Task NotifyHttp(string invoiceData)
{
var job = NBitcoin.JsonConverters.Serializer.ToObject<ScheduledJob>(invoiceData);
bool reschedule = false;
var aggregatorEvent = new InvoiceIPNEvent(job.Notification.Data.Id, job.Notification.Event.Code, job.Notification.Event.Name);
CancellationTokenSource cts = new CancellationTokenSource(10000);
try
{
HttpResponseMessage response = await SendNotification(job.Notification, cancellationToken);
HttpResponseMessage response = await SendNotification(job.Notification, cts.Token);
reschedule = !response.IsSuccessStatusCode;
aggregatorEvent.Error = reschedule ? $"Unexpected return code: {(int)response.StatusCode}" : null;
_EventAggregator.Publish<InvoiceIPNEvent>(aggregatorEvent);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
// When the JobClient will be persistent, this will reschedule the job for after reboot
invoiceData = NBitcoin.JsonConverters.Serializer.ToString(job);
_JobClient.Schedule((cancellation) => NotifyHttp(invoiceData, cancellation), TimeSpan.FromMinutes(10.0));
return;
}
catch (OperationCanceledException)
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
aggregatorEvent.Error = "Timeout";
_EventAggregator.Publish<InvoiceIPNEvent>(aggregatorEvent);
@ -180,13 +172,14 @@ namespace BTCPayServer.HostedServices
aggregatorEvent.Error = $"Unexpected error: {message}";
_EventAggregator.Publish<InvoiceIPNEvent>(aggregatorEvent);
}
finally { cts?.Dispose(); }
job.TryCount++;
if (job.TryCount < MaxTry && reschedule)
{
invoiceData = NBitcoin.JsonConverters.Serializer.ToString(job);
_JobClient.Schedule((cancellation) => NotifyHttp(invoiceData, cancellation), TimeSpan.FromMinutes(10.0));
_JobClient.Schedule(() => NotifyHttp(invoiceData), TimeSpan.FromMinutes(10.0));
}
}
@ -210,7 +203,7 @@ namespace BTCPayServer.HostedServices
}
Encoding UTF8 = new UTF8Encoding(false);
private async Task<HttpResponseMessage> SendNotification(InvoicePaymentNotificationEventWrapper notification, CancellationToken cancellationToken)
private async Task<HttpResponseMessage> SendNotification(InvoicePaymentNotificationEventWrapper notification, CancellationToken cancellation)
{
var request = new HttpRequestMessage();
request.Method = HttpMethod.Post;
@ -231,14 +224,7 @@ namespace BTCPayServer.HostedServices
request.RequestUri = new Uri(notification.NotificationURL, UriKind.Absolute);
request.Content = new StringContent(notificationString, UTF8, "application/json");
var response = await Enqueue(notification.Data.Id, async () =>
{
using (CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
{
cts.CancelAfter(TimeSpan.FromMinutes(1.0));
return await _Client.SendAsync(request, cts.Token);
}
});
var response = await Enqueue(notification.Data.Id, async () => await _Client.SendAsync(request, cancellation));
return response;
}

View File

@ -49,6 +49,7 @@ namespace BTCPayServer.HostedServices
InvoiceRepository invoiceRepository,
EventAggregator eventAggregator)
{
PollInterval = TimeSpan.FromMinutes(1.0);
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
_EventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_NetworkProvider = networkProvider;
@ -98,8 +99,8 @@ namespace BTCPayServer.HostedServices
if (accounting.Paid < accounting.MinimumTotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidPartial)
{
invoice.ExceptionStatus = InvoiceExceptionStatus.PaidPartial;
context.MarkDirty();
invoice.ExceptionStatus = InvoiceExceptionStatus.PaidPartial;
context.MarkDirty();
}
}
@ -184,6 +185,19 @@ namespace BTCPayServer.HostedServices
return result;
}
TimeSpan _PollInterval;
public TimeSpan PollInterval
{
get
{
return _PollInterval;
}
set
{
_PollInterval = value;
}
}
private void Watch(string invoiceId)
{
if (invoiceId == null)
@ -217,24 +231,25 @@ namespace BTCPayServer.HostedServices
BlockingCollection<string> _WatchRequests = new BlockingCollection<string>(new ConcurrentQueue<string>());
Task _Loop;
Task _WaitingInvoices;
CancellationTokenSource _Cts;
public Task StartAsync(CancellationToken cancellationToken)
{
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_Loop = StartLoop(_Cts.Token);
_ = WaitPendingInvoices();
_WaitingInvoices = WaitPendingInvoices();
leases.Add(_EventAggregator.Subscribe<Events.InvoiceNeedUpdateEvent>(b =>
{
Watch(b.InvoiceId);
}));
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(b =>
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async b =>
{
if (b.Name == InvoiceEvent.Created)
{
Watch(b.Invoice.Id);
_ = Wait(b.Invoice.Id);
await Wait(b.Invoice.Id);
}
if (b.Name == InvoiceEvent.ReceivedPayment)
@ -249,76 +264,79 @@ namespace BTCPayServer.HostedServices
{
await Task.WhenAll((await _InvoiceRepository.GetPendingInvoices())
.Select(id => Wait(id)).ToArray());
_WaitingInvoices = null;
}
async Task StartLoop(CancellationToken cancellation)
{
Logs.PayServer.LogInformation("Start watching invoices");
await Task.Delay(1).ConfigureAwait(false); // Small hack so that the caller does not block on GetConsumingEnumerable
foreach (var invoiceId in _WatchRequests.GetConsumingEnumerable(cancellation))
try
{
int maxLoop = 5;
int loopCount = -1;
while (loopCount < maxLoop)
foreach (var invoiceId in _WatchRequests.GetConsumingEnumerable(cancellation))
{
loopCount++;
try
int maxLoop = 5;
int loopCount = -1;
while (!cancellation.IsCancellationRequested && loopCount < maxLoop)
{
cancellation.ThrowIfCancellationRequested();
var invoice = await _InvoiceRepository.GetInvoice(invoiceId, true);
if (invoice == null)
break;
var updateContext = new UpdateInvoiceContext(invoice);
await UpdateInvoice(updateContext);
if (updateContext.Dirty)
loopCount++;
try
{
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.GetInvoiceState());
updateContext.Events.Insert(0, new InvoiceDataChangedEvent(invoice));
}
var invoice = await _InvoiceRepository.GetInvoice(invoiceId, true);
if (invoice == null)
break;
var updateContext = new UpdateInvoiceContext(invoice);
await UpdateInvoice(updateContext);
if (updateContext.Dirty)
{
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.GetInvoiceState());
updateContext.Events.Insert(0, new InvoiceDataChangedEvent(invoice));
}
foreach (var evt in updateContext.Events)
{
_EventAggregator.Publish(evt, evt.GetType());
}
foreach (var evt in updateContext.Events)
{
_EventAggregator.Publish(evt, evt.GetType());
}
if (invoice.Status == InvoiceStatus.Complete ||
((invoice.Status == InvoiceStatus.Invalid || invoice.Status == InvoiceStatus.Expired) && invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
if (invoice.Status == InvoiceStatus.Complete ||
((invoice.Status == InvoiceStatus.Invalid || invoice.Status == InvoiceStatus.Expired) && invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
{
if (await _InvoiceRepository.RemovePendingInvoice(invoice.Id))
_EventAggregator.Publish(new InvoiceStopWatchedEvent(invoice.Id));
break;
}
if (updateContext.Events.Count == 0 || cancellation.IsCancellationRequested)
break;
}
catch (OperationCanceledException) when (cancellation.IsCancellationRequested)
{
if (await _InvoiceRepository.RemovePendingInvoice(invoice.Id))
_EventAggregator.Publish(new InvoiceStopWatchedEvent(invoice.Id));
break;
}
if (updateContext.Events.Count == 0)
catch (Exception ex)
{
Logs.PayServer.LogError(ex, "Unhandled error on watching invoice " + invoiceId);
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
Task.Delay(10000, cancellation)
.ContinueWith(t => _WatchRequests.Add(invoiceId), TaskScheduler.Default);
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
break;
}
catch (Exception ex) when (!cancellation.IsCancellationRequested)
{
Logs.PayServer.LogError(ex, "Unhandled error on watching invoice " + invoiceId);
_ = Task.Delay(10000, cancellation)
.ContinueWith(t => Watch(invoiceId), TaskScheduler.Default);
break;
}
}
}
}
catch when (cancellation.IsCancellationRequested)
{
}
Logs.PayServer.LogInformation("Stop watching invoices");
}
public async Task StopAsync(CancellationToken cancellationToken)
public Task StopAsync(CancellationToken cancellationToken)
{
if (_Cts == null)
return;
leases.Dispose();
_Cts.Cancel();
try
{
await _Loop;
}
catch { }
finally
{
Logs.PayServer.LogInformation("Stop watching invoices");
}
var waitingPendingInvoices = _WaitingInvoices ?? Task.CompletedTask;
return Task.WhenAll(waitingPendingInvoices, _Loop);
}
}
}

View File

@ -71,12 +71,6 @@ namespace BTCPayServer.HostedServices
settings.ConvertCrowdfundOldSettings = true;
await _Settings.UpdateSetting(settings);
}
if (!settings.ConvertWalletKeyPathRoots)
{
await ConvertConvertWalletKeyPathRoots();
settings.ConvertWalletKeyPathRoots = true;
await _Settings.UpdateSetting(settings);
}
}
catch (Exception ex)
{
@ -85,35 +79,6 @@ namespace BTCPayServer.HostedServices
}
}
private async Task ConvertConvertWalletKeyPathRoots()
{
bool save = false;
using (var ctx = _DBContextFactory.CreateContext())
{
foreach (var store in await ctx.Stores.ToArrayAsync())
{
#pragma warning disable CS0618 // Type or member is obsolete
var blob = store.GetStoreBlob();
if (blob.WalletKeyPathRoots == null)
continue;
foreach (var scheme in store.GetSupportedPaymentMethods(_NetworkProvider).OfType<DerivationSchemeSettings>())
{
if (blob.WalletKeyPathRoots.TryGetValue(scheme.PaymentId.ToString().ToLowerInvariant(), out var root))
{
scheme.AccountKeyPath = new NBitcoin.KeyPath(root);
store.SetSupportedPaymentMethod(scheme);
save = true;
}
}
blob.WalletKeyPathRoots = null;
store.SetStoreBlob(blob);
#pragma warning restore CS0618 // Type or member is obsolete
}
if (save)
await ctx.SaveChangesAsync();
}
}
private async Task ConvertCrowdfundOldSettings()
{
using (var ctx = _DBContextFactory.CreateContext())
@ -194,7 +159,7 @@ namespace BTCPayServer.HostedServices
if (lightning.IsLegacy)
{
method.SetLightningUrl(lightning);
store.SetSupportedPaymentMethod(method);
store.SetSupportedPaymentMethod(method.PaymentId, method);
}
}
}

View File

@ -43,13 +43,13 @@ namespace BTCPayServer.HostedServices
public bool IsFullySynched(string cryptoCode, out NBXplorerSummary summary)
{
return _Summaries.TryGetValue(cryptoCode.ToUpperInvariant(), out summary) &&
return _Summaries.TryGetValue(cryptoCode, out summary) &&
summary.Status != null &&
summary.Status.IsFullySynched;
}
public NBXplorerSummary Get(string cryptoCode)
{
_Summaries.TryGetValue(cryptoCode.ToUpperInvariant(), out var summary);
_Summaries.TryGetValue(cryptoCode, out var summary);
return summary;
}
public IEnumerable<NBXplorerSummary> GetAll()

View File

@ -1,5 +1,4 @@
using System;
using NBitcoin;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
@ -48,7 +47,7 @@ namespace BTCPayServer.HostedServices
{
await Task.WhenAll(_RateProviderFactory.Providers
.Select(p => (Fetcher: p.Value as BackgroundFetcherRateProvider, ExchangeName: p.Key)).Where(p => p.Fetcher != null)
.Select(p => p.Fetcher.UpdateIfNecessary(timeout.Token).ContinueWith(t =>
.Select(p => p.Fetcher.UpdateIfNecessary().ContinueWith(t =>
{
if (t.Result.Exception != null)
{
@ -69,7 +68,7 @@ namespace BTCPayServer.HostedServices
var exchanges = new CoinAverageExchanges();
foreach (var item in (await new CoinAverageRateProvider() { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync())
.Exchanges
.Select(c => new CoinAverageExchange(c.Name, c.DisplayName, $"https://apiv2.bitcoinaverage.com/exchanges/{c.Name}")))
.Select(c => new CoinAverageExchange(c.Name, c.DisplayName)))
{
exchanges.Add(item);
}

View File

@ -1,36 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Configuration;
using BTCPayServer.Services;
using Microsoft.Extensions.Hosting;
namespace BTCPayServer.HostedServices
{
public class TorServicesHostedService : BaseAsyncService
{
private readonly BTCPayServerOptions _options;
private readonly TorServices _torServices;
public TorServicesHostedService(BTCPayServerOptions options, TorServices torServices)
{
_options = options;
_torServices = torServices;
}
internal override Task[] InitializeTasks()
{
// TODO: We should report auto configured services (like bitcoind, lnd or clightning)
if (string.IsNullOrEmpty(_options.TorrcFile))
return Array.Empty<Task>();
return new Task[] { CreateLoopTask(RefreshTorServices) };
}
async Task RefreshTorServices()
{
await _torServices.Refresh();
await Task.Delay(TimeSpan.FromSeconds(120), Cancellation);
}
}
}

View File

@ -36,6 +36,7 @@ using BTCPayServer.Authentication;
using Microsoft.Extensions.Caching.Memory;
using BTCPayServer.Logging;
using BTCPayServer.HostedServices;
using Meziantou.AspNetCore.BundleTagHelpers;
using System.Security.Claims;
using BTCPayServer.PaymentRequest;
using BTCPayServer.Payments.Changelly;
@ -47,8 +48,6 @@ using NBXplorer.DerivationStrategy;
using NicolasDorier.RateLimits;
using Npgsql;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.U2F;
using BundlerMinifier.TagHelpers;
namespace BTCPayServer.Hosting
{
@ -63,9 +62,6 @@ namespace BTCPayServer.Hosting
});
services.AddHttpClient();
services.TryAddSingleton<SettingsRepository>();
services.TryAddSingleton<TorServices>();
services.TryAddSingleton<SocketFactory>();
services.TryAddSingleton<LightningClientFactoryService>();
services.TryAddSingleton<InvoicePaymentNotification>();
services.TryAddSingleton<BTCPayServerOptions>(o => o.GetRequiredService<IOptions<BTCPayServerOptions>>().Value);
services.TryAddSingleton<InvoiceRepository>(o =>
@ -81,7 +77,6 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton<TokenRepository>();
services.TryAddSingleton<EventAggregator>();
services.TryAddSingleton<PaymentRequestService>();
services.TryAddSingleton<U2FService>();
services.TryAddSingleton<CoinAverageSettings>();
services.TryAddSingleton<ApplicationDbContextFactory>(o =>
{
@ -176,7 +171,7 @@ namespace BTCPayServer.Hosting
services.AddSingleton<IHostedService, CssThemeManagerHostedService>();
services.AddSingleton<IHostedService, MigratorHostedService>();
services.AddSingleton<Payments.IPaymentMethodHandler<DerivationSchemeSettings>, Payments.Bitcoin.BitcoinLikePaymentHandler>();
services.AddSingleton<Payments.IPaymentMethodHandler<DerivationStrategy>, Payments.Bitcoin.BitcoinLikePaymentHandler>();
services.AddSingleton<IHostedService, Payments.Bitcoin.NBXplorerListener>();
services.AddSingleton<IHostedService, HostedServices.CheckConfigurationHostedService>();
@ -193,7 +188,6 @@ namespace BTCPayServer.Hosting
services.AddSingleton<IHostedService, RatesHostedService>();
services.AddSingleton<IHostedService, BackgroundJobSchedulerHostedService>();
services.AddSingleton<IHostedService, AppHubStreamer>();
services.AddSingleton<IHostedService, TorServicesHostedService>();
services.AddSingleton<IHostedService, PaymentRequestStreamer>();
services.AddSingleton<IBackgroundJobClient, BackgroundJobClient>();
services.AddTransient<IConfigureOptions<MvcOptions>, BTCPayClaimsFilter>();
@ -221,7 +215,7 @@ namespace BTCPayServer.Hosting
services.AddAuthorization(o => Policies.AddBTCPayPolicies(o));
BitpayAuthentication.AddAuthentication(services);
services.AddSingleton<IBundleProvider, ResourceBundleProvider>();
services.AddBundles();
services.AddTransient<BundleOptions>(provider =>
{
var opts = provider.GetRequiredService<BTCPayServerOptions>();

View File

@ -32,6 +32,8 @@ namespace BTCPayServer.Hosting
public async Task Invoke(HttpContext httpContext)
{
RewriteHostIfNeeded(httpContext);
try
{
var bitpayAuth = GetBitpayAuth(httpContext, out bool isBitpayAuth);
@ -88,9 +90,6 @@ namespace BTCPayServer.Hosting
if (!httpContext.Request.Path.HasValue)
return false;
// In case of anyone can create invoice, the storeId can be set explicitely
bitpayAuth |= httpContext.Request.Query.ContainsKey("storeid");
var isJson = (httpContext.Request.ContentType ?? string.Empty).StartsWith("application/json", StringComparison.OrdinalIgnoreCase);
var path = httpContext.Request.Path.Value;
var method = httpContext.Request.Method;
@ -98,7 +97,7 @@ namespace BTCPayServer.Hosting
if (
(isCors || bitpayAuth) &&
(path == "/invoices" || path == "/invoices/") &&
(path == "/invoices" || path == "/invoices/") &&
(isCors || (method == "POST" && isJson)))
return true;
@ -126,6 +125,57 @@ namespace BTCPayServer.Hosting
return false;
}
private void RewriteHostIfNeeded(HttpContext httpContext)
{
string reverseProxyScheme = null;
if (httpContext.Request.Headers.TryGetValue("X-Forwarded-Proto", out StringValues proto))
{
var scheme = proto.SingleOrDefault();
if (scheme != null)
{
reverseProxyScheme = scheme;
}
}
ushort? reverseProxyPort = null;
if (httpContext.Request.Headers.TryGetValue("X-Forwarded-Port", out StringValues port))
{
var portString = port.SingleOrDefault();
if (portString != null && ushort.TryParse(portString, out ushort pp))
{
reverseProxyPort = pp;
}
}
// NGINX pass X-Forwarded-Proto and X-Forwarded-Port, so let's use that to have better guess of the real domain
ushort? p = null;
if (reverseProxyScheme != null)
{
httpContext.Request.Scheme = reverseProxyScheme;
if (reverseProxyScheme == "http")
p = 80;
if (reverseProxyScheme == "https")
p = 443;
}
if (reverseProxyPort != null)
{
p = reverseProxyPort.Value;
}
if (p.HasValue)
{
bool isDefault = httpContext.Request.Scheme == "http" && p.Value == 80;
isDefault |= httpContext.Request.Scheme == "https" && p.Value == 443;
if (isDefault)
httpContext.Request.Host = new HostString(httpContext.Request.Host.Host);
else
httpContext.Request.Host = new HostString(httpContext.Request.Host.Host, p.Value);
}
}
private static async Task HandleBitpayHttpException(HttpContext httpContext, BitpayHttpException ex)
{
httpContext.Response.StatusCode = ex.StatusCode;

View File

@ -1,50 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using BundlerMinifier.TagHelpers;
using Microsoft.AspNetCore.Hosting;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Hosting
{
public class ResourceBundleProvider : IBundleProvider
{
BundleProvider _InnerProvider;
Lazy<Dictionary<string, Bundle>> _BundlesByName;
public ResourceBundleProvider(IHostingEnvironment hosting, BundleOptions options)
{
if (options.UseBundles)
{
_BundlesByName = new Lazy<Dictionary<string, Bundle>>(() =>
{
using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("BTCPayServer.bundleconfig.json"))
using (var reader = new StreamReader(stream, Encoding.UTF8))
{
var content = reader.ReadToEnd();
return JArray.Parse(content).OfType<JObject>()
.Select(jobj => new Bundle()
{
Name = jobj.Property("name", StringComparison.OrdinalIgnoreCase)?.Value.Value<string>() ?? jobj.Property("outputFileName", StringComparison.OrdinalIgnoreCase).Value.Value<string>(),
OutputFileUrl = Path.Combine(hosting.ContentRootPath, jobj.Property("outputFileName", StringComparison.OrdinalIgnoreCase).Value.Value<string>())
}).ToDictionary(o => o.Name, o => o);
}
}, true);
}
else
{
_InnerProvider = new BundleProvider();
}
}
public Bundle GetBundle(string name)
{
if (_InnerProvider != null)
return _InnerProvider.GetBundle(name);
_BundlesByName.Value.TryGetValue(name, out var bundle);
return bundle;
}
}
}

View File

@ -3,6 +3,7 @@ using System.Reflection;
using System.Linq;
using Microsoft.AspNetCore.Builder;
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
@ -13,9 +14,9 @@ using BTCPayServer.Authentication;
using Microsoft.EntityFrameworkCore;
using BTCPayServer.Filters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using BTCPayServer.Services;
using BTCPayServer.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.HttpOverrides;
using BTCPayServer.Data;
using Microsoft.Extensions.Logging;
using BTCPayServer.Logging;
@ -34,12 +35,9 @@ using Microsoft.AspNetCore.Mvc.Cors.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using System.Net;
using BTCPayServer.PaymentRequest;
using Meziantou.AspNetCore.BundleTagHelpers;
using BTCPayServer.Security;
using BTCPayServer.Services.Apps;
using BTCPayServer.Storage;
using BTCPayServer.Storage.Services.Providers.FileSystemStorage;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.FileProviders;
namespace BTCPayServer.Hosting
{
@ -68,8 +66,6 @@ namespace BTCPayServer.Hosting
.AddDefaultTokenProviders();
services.AddSignalR();
services.AddBTCPayServer();
services.AddProviderStorage();
services.AddSession();
services.AddMvc(o =>
{
o.Filters.Add(new XFrameOptionsAttribute("DENY"));
@ -84,7 +80,7 @@ namespace BTCPayServer.Hosting
// StyleSrc = "'self' 'unsafe-inline'",
// ScriptSrc = "'self' 'unsafe-inline'"
//});
}).AddControllersAsServices();
});
services.TryAddScoped<ContentSecurityPolicies>();
services.Configure<IdentityOptions>(options =>
{
@ -163,24 +159,14 @@ namespace BTCPayServer.Hosting
app.UseDeveloperExceptionPage();
}
var forwardingOptions = new ForwardedHeadersOptions()
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
};
forwardingOptions.KnownNetworks.Clear();
forwardingOptions.KnownProxies.Clear();
forwardingOptions.ForwardedHeaders = ForwardedHeaders.All;
app.UseForwardedHeaders(forwardingOptions);
app.UseCors();
app.UsePayServer();
app.UseStaticFiles();
app.UseProviderStorage(options);
app.UseAuthentication();
app.UseSession();
app.UseSignalR(route =>
{
AppHub.Register(route);
PaymentRequestHub.Register(route);
route.MapHub<AppHub>("/apps/hub");
route.MapHub<PaymentRequestHub>("/payment-requests/hub");
});
app.UseWebSockets();
app.UseStatusCodePages();

View File

@ -1,39 +0,0 @@
using System;
using System.Reflection;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
using NBitcoin.JsonConverters;
using BTCPayServer.Rating;
namespace BTCPayServer.JsonConverters
{
public class CurrencyPairJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(CurrencyValue).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
try
{
return reader.TokenType == JsonToken.Null ? null :
CurrencyPair.TryParse((string)reader.Value, out var result) ? result :
throw new JsonObjectException("Invalid currency pair", reader);
}
catch (InvalidCastException)
{
throw new JsonObjectException("Invalid currency pair", reader);
}
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value != null)
writer.WriteValue(value.ToString());
}
}
}

View File

@ -33,35 +33,5 @@ namespace BTCPayServer.Logging
return _InvoiceLogs.ToList();
}
}
internal IDisposable Measure(string logs)
{
return new Mesuring(this, logs);
}
class Mesuring : IDisposable
{
private readonly InvoiceLogs _logs;
private readonly string _msg;
private readonly DateTimeOffset _Before;
public Mesuring(InvoiceLogs logs, string msg)
{
_logs = logs;
_msg = msg;
_Before = DateTimeOffset.UtcNow;
}
public void Dispose()
{
var timespan = DateTimeOffset.UtcNow - _Before;
if (timespan.TotalSeconds >= 1.0)
{
_logs.Write($"{_msg} took {(int)timespan.TotalSeconds} seconds");
}
else
{
_logs.Write($"{_msg} took {(int)timespan.TotalMilliseconds} milliseconds");
}
}
}
}
}

View File

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

View File

@ -1,43 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace BTCPayServer.Migrations
{
public partial class AddFiles : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Files",
columns: table => new
{
Id = table.Column<string>(nullable: false),
FileName = table.Column<string>(nullable: true),
StorageFileName = table.Column<string>(nullable: true),
Timestamp = table.Column<DateTime>(nullable: false),
ApplicationUserId = table.Column<string>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Files", x => x.Id);
table.ForeignKey(
name: "FK_Files_AspNetUsers_ApplicationUserId",
column: x => x.ApplicationUserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_Files_ApplicationUserId",
table: "Files",
column: "ApplicationUserId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Files");
}
}
}

View File

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

View File

@ -1,60 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace BTCPayServer.Migrations
{
public partial class AddU2fDevices : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
if (this.SupportDropColumn(migrationBuilder.ActiveProvider))
{
migrationBuilder.DropColumn(
name: "Facade",
table: "PairedSINData");
}
migrationBuilder.CreateTable(
name: "U2FDevices",
columns: table => new
{
Id = table.Column<string>(nullable: false),
Name = table.Column<string>(nullable: true),
KeyHandle = table.Column<byte[]>(nullable: false),
PublicKey = table.Column<byte[]>(nullable: false),
AttestationCert = table.Column<byte[]>(nullable: false),
Counter = table.Column<int>(nullable: false),
ApplicationUserId = table.Column<string>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_U2FDevices", x => x.Id);
table.ForeignKey(
name: "FK_U2FDevices_AspNetUsers_ApplicationUserId",
column: x => x.ApplicationUserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_U2FDevices_ApplicationUserId",
table: "U2FDevices",
column: "ApplicationUserId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "U2FDevices");
//if it did not support dropping it, then it is still here and re-adding it would throw
if (this.SupportDropColumn(migrationBuilder.ActiveProvider))
{
migrationBuilder.AddColumn<string>(
name: "Facade",
table: "PairedSINData",
nullable: true);
}
}
}
}

View File

@ -137,6 +137,8 @@ namespace BTCPayServer.Migrations
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Facade");
b.Property<string>("Label");
b.Property<DateTimeOffset>("PairingTime");
@ -346,53 +348,6 @@ namespace BTCPayServer.Migrations
b.ToTable("PaymentRequests");
});
modelBuilder.Entity("BTCPayServer.Services.U2F.Models.U2FDevice", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ApplicationUserId");
b.Property<byte[]>("AttestationCert")
.IsRequired();
b.Property<int>("Counter");
b.Property<byte[]>("KeyHandle")
.IsRequired();
b.Property<string>("Name");
b.Property<byte[]>("PublicKey")
.IsRequired();
b.HasKey("Id");
b.HasIndex("ApplicationUserId");
b.ToTable("U2FDevices");
});
modelBuilder.Entity("BTCPayServer.Storage.Models.StoredFile", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ApplicationUserId");
b.Property<string>("FileName");
b.Property<string>("StorageFileName");
b.Property<DateTime>("Timestamp");
b.HasKey("Id");
b.HasIndex("ApplicationUserId");
b.ToTable("Files");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
@ -601,20 +556,6 @@ namespace BTCPayServer.Migrations
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Services.U2F.Models.U2FDevice", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser")
.WithMany("U2FDevices")
.HasForeignKey("ApplicationUserId");
});
modelBuilder.Entity("BTCPayServer.Storage.Models.StoredFile", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser")
.WithMany("StoredFiles")
.HasForeignKey("ApplicationUserId");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")

View File

@ -1,99 +0,0 @@
// Copied and adjusted from https://github.com/aspnet/Mvc/blob/master/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/DecimalModelBinder.cs
using System;
using System.Globalization;
using System.Runtime.ExceptionServices;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace BTCPayServer.ModelBinders
{
/// <summary>
/// An <see cref="IModelBinder"/> for <see cref="decimal"/> and <see cref="Nullable{T}"/> where <c>T</c> is
/// <see cref="decimal"/>.
/// </summary>
public class InvariantDecimalModelBinder : IModelBinder
{
private readonly NumberStyles _supportedStyles;
public InvariantDecimalModelBinder()
{
_supportedStyles = NumberStyles.Any;
}
/// <inheritdoc />
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
var modelName = bindingContext.ModelName;
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
if (valueProviderResult == ValueProviderResult.None)
{
return Task.CompletedTask;
}
var modelState = bindingContext.ModelState;
modelState.SetModelValue(modelName, valueProviderResult);
var metadata = bindingContext.ModelMetadata;
var type = metadata.UnderlyingOrModelType;
try
{
var value = valueProviderResult.FirstValue;
var culture = CultureInfo.InvariantCulture;
object model;
if (string.IsNullOrWhiteSpace(value))
{
// Parse() method trims the value (with common NumberStyles) then throws if the result is empty.
model = null;
}
else if (type == typeof(decimal))
{
model = decimal.Parse(value, _supportedStyles, culture);
}
else
{
// unreachable
throw new NotSupportedException();
}
// When converting value, a null model may indicate a failed conversion for an otherwise required
// model (can't set a ValueType to null). This detects if a null model value is acceptable given the
// current bindingContext. If not, an error is logged.
if (model == null && !metadata.IsReferenceOrNullableType)
{
modelState.TryAddModelError(
modelName,
metadata.ModelBindingMessageProvider.ValueMustNotBeNullAccessor(
valueProviderResult.ToString()));
}
else
{
bindingContext.Result = ModelBindingResult.Success(model);
}
}
catch (Exception exception)
{
var isFormatException = exception is FormatException;
if (!isFormatException && exception.InnerException != null)
{
// Unlike TypeConverters, floating point types do not seem to wrap FormatExceptions. Preserve
// this code in case a cursory review of the CoreFx code missed something.
exception = ExceptionDispatchInfo.Capture(exception.InnerException).SourceException;
}
modelState.TryAddModelError(modelName, exception, metadata);
// Conversion failed.
}
return Task.CompletedTask;
}
}
}

View File

@ -1,10 +0,0 @@
using BTCPayServer.Services.U2F.Models;
namespace BTCPayServer.Models.AccountViewModels
{
public class SecondaryLoginViewModel
{
public LoginWith2faViewModel LoginWith2FaViewModel { get; set; }
public LoginWithU2FViewModel LoginWithU2FViewModel { get; set; }
}
}

View File

@ -3,7 +3,6 @@ using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using BTCPayServer.Services.Apps;
using BTCPayServer.Validation;
namespace BTCPayServer.Models.AppViewModels
{
@ -17,13 +16,8 @@ namespace BTCPayServer.Models.AppViewModels
[Display(Name = "Featured Image")]
public string MainImageUrl { get; set; }
[Display(Name = "Callback Notification Url")]
[Uri]
public string NotificationUrl { get; set; }
[Display(Name = "Invoice Email Notification")]
[EmailAddress]
public string NotificationEmail { get; set; }
[Required]
[Display(Name = "Allow crowdfund to be publicly visible (still visible to you)")]
@ -52,7 +46,6 @@ namespace BTCPayServer.Models.AppViewModels
public string TargetCurrency { get; set; } = "BTC";
[Display(Name = "Set a Target amount ")]
[Range(0, double.PositiveInfinity)]
public decimal? TargetAmount { get; set; }
@ -86,13 +79,5 @@ namespace BTCPayServer.Models.AppViewModels
public bool SortPerksByPopularity { get; set; }
[Display(Name = "Display contribution ranking")]
public bool DisplayPerksRanking { get; set; }
[Display(Name = "Sounds to play when a payment is made. One sound per line")]
public string Sounds{ get; set; }
[Display(Name = "Colors to rotate between with animation when a payment is made. First color is the default background. One color per line. Can be any valid css color value.")]
public string AnimationColors{ get; set; }
public bool NotificationEmailWarning { get; set; }
}
}

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