Compare commits
120 Commits
v1.4.5
...
user-disab
Author | SHA1 | Date | |
---|---|---|---|
597116bb26 | |||
64534efe71 | |||
1235ced355 | |||
23d383be67 | |||
fb90ff2fbb | |||
4706aa95e6 | |||
8981414705 | |||
7ec978fcdb | |||
d58803a058 | |||
fe6b7dc1e3 | |||
c9f0988b95 | |||
cd9a52706c | |||
bfdb1b4af9 | |||
e5174b4a29 | |||
8feb60c30d | |||
6bd7fb64ab | |||
b9602243d3 | |||
b7a930ef18 | |||
add206ae2d | |||
28ce095fb4 | |||
13952a4b79 | |||
05ec398346 | |||
dea2dd52be | |||
debe3cda4b | |||
5b5aa2c721 | |||
e201ddd74c | |||
4a1580169d | |||
001ca7de60 | |||
184be4e27b | |||
7652645dda | |||
ef6016857b | |||
e449ca2c95 | |||
b0f00773d6 | |||
451eee549b | |||
77da261fea | |||
e23c9ee608 | |||
37cb87a9c6 | |||
211db8e0f0 | |||
d074d60dad | |||
0bff5e2236 | |||
dca986eb2e | |||
326eb1135b | |||
b2f7b4e6b9 | |||
5129d6aa6b | |||
a8cf334616 | |||
3b4d06a1e5 | |||
c7969476b0 | |||
7bf24df03a | |||
5ef41294e4 | |||
2eb68655c7 | |||
23049439c0 | |||
ce6cd40b92 | |||
c36b0c16b0 | |||
c15f182377 | |||
165cb345b4 | |||
9b6d2beb4d | |||
722c39a6ff | |||
e344749d2f | |||
fe782bc3b6 | |||
d372cbad74 | |||
1e1198f4ec | |||
5effc96cff | |||
b8d4a1be05 | |||
36a25e6efa | |||
1240e7914d | |||
e4683b1ea1 | |||
749c22a0c3 | |||
6867774627 | |||
80944972e9 | |||
c1f608c0d8 | |||
4dfbb08db3 | |||
ead1dffd98 | |||
5a16e4d132 | |||
a89491e343 | |||
b1b00ae886 | |||
cde5bd87d8 | |||
3231d5d179 | |||
03e49ea2bf | |||
7d3eef092c | |||
30d0410b49 | |||
eb2a887f77 | |||
e77b8d29cf | |||
490ec299c5 | |||
e47c2aa24d | |||
3eb9fdca6a | |||
a4173a93b7 | |||
ad762cf239 | |||
5a478607dc | |||
4abc6eb387 | |||
c313bba288 | |||
73eaf97afb | |||
8d25df5d4e | |||
4a05f16050 | |||
3ef1423263 | |||
898652189b | |||
2976edf333 | |||
248be11e4d | |||
19ec8c36e2 | |||
90d989e358 | |||
19eea3a615 | |||
7b81b9786d | |||
292d302a3d | |||
557594e34d | |||
48393c3765 | |||
022cd666eb | |||
2d0eedb132 | |||
5d3d664ce6 | |||
d1c12d8294 | |||
947a67fcd2 | |||
9b9540b857 | |||
a3b748ffe3 | |||
9a3a7a3444 | |||
5c8ca15ee2 | |||
cd3807a3d8 | |||
2a884d6f38 | |||
6efeb60c41 | |||
dcdab5b218 | |||
288fbda54f | |||
03bc91fd1e | |||
1c5cf29540 |
@ -2,7 +2,7 @@ version: 2
|
||||
jobs:
|
||||
fast_tests:
|
||||
machine:
|
||||
enabled: true
|
||||
image: ubuntu-2004:202111-02
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
@ -10,7 +10,7 @@ jobs:
|
||||
cd .circleci && ./run-tests.sh "Fast=Fast|ThirdParty=ThirdParty" && ./can-build.sh
|
||||
selenium_tests:
|
||||
machine:
|
||||
enabled: true
|
||||
image: ubuntu-2004:202111-02
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
@ -18,7 +18,7 @@ jobs:
|
||||
cd .circleci && ./run-tests.sh "Selenium=Selenium"
|
||||
integration_tests:
|
||||
machine:
|
||||
enabled: true
|
||||
image: ubuntu-2004:202111-02
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
@ -26,19 +26,18 @@ jobs:
|
||||
cd .circleci && ./run-tests.sh "Integration=Integration"
|
||||
trigger_docs_build:
|
||||
machine:
|
||||
enabled: true
|
||||
image: circleci/classic:201808-01
|
||||
image: ubuntu-2004:202111-02
|
||||
steps:
|
||||
- run:
|
||||
command: |
|
||||
curl -X POST -H "Authorization: token $GH_PAT" -H "Accept: application/vnd.github.everest-preview+json" -H "Content-Type: application/json" https://api.github.com/repos/btcpayserver/btcpayserver-doc/dispatches --data '{"event_type": "build_docs"}'
|
||||
curl -X POST -H "Authorization: token $GH_PAT" -H "Accept: application/vnd.github.everest-preview+json" -H "Content-Type: application/json" https://api.github.com/repos/btcpayserver/btcpayserver-doc/dispatches --data '{"event_type": "build_docs"}'
|
||||
|
||||
# publish jobs require $DOCKERHUB_REPO, $DOCKERHUB_USER, $DOCKERHUB_PASS defined
|
||||
amd64:
|
||||
machine:
|
||||
enabled: true
|
||||
image: ubuntu-2004:202111-02
|
||||
steps:
|
||||
- checkout
|
||||
- checkout
|
||||
- run:
|
||||
command: |
|
||||
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
|
||||
@ -51,9 +50,9 @@ jobs:
|
||||
|
||||
arm32v7:
|
||||
machine:
|
||||
enabled: true
|
||||
image: ubuntu-2004:202111-02
|
||||
steps:
|
||||
- checkout
|
||||
- checkout
|
||||
- run:
|
||||
command: |
|
||||
sudo docker run --rm --privileged multiarch/qemu-user-static:register --reset
|
||||
@ -67,9 +66,9 @@ jobs:
|
||||
|
||||
arm64v8:
|
||||
machine:
|
||||
enabled: true
|
||||
image: ubuntu-2004:202111-02
|
||||
steps:
|
||||
- checkout
|
||||
- checkout
|
||||
- run:
|
||||
command: |
|
||||
sudo docker run --rm --privileged multiarch/qemu-user-static:register --reset
|
||||
@ -83,15 +82,10 @@ jobs:
|
||||
|
||||
multiarch:
|
||||
machine:
|
||||
enabled: true
|
||||
image: circleci/classic:201808-01
|
||||
image: ubuntu-2004:202201-02
|
||||
steps:
|
||||
- run:
|
||||
command: |
|
||||
# Turn on Experimental features
|
||||
sudo mkdir $HOME/.docker
|
||||
sudo sh -c 'echo "{ \"experimental\": \"enabled\" }" >> $HOME/.docker/config.json'
|
||||
#
|
||||
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
|
||||
#
|
||||
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
|
||||
@ -100,8 +94,7 @@ jobs:
|
||||
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 --os linux --arch arm --variant v7
|
||||
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-arm64v8 --os linux --arch arm64 --variant v8
|
||||
sudo docker manifest push $DOCKERHUB_REPO:$LATEST_TAG -p
|
||||
|
||||
|
||||
|
||||
sudo docker manifest create --amend $DOCKERHUB_REPO:$LATEST_TAG-altcoins $DOCKERHUB_REPO:$LATEST_TAG-altcoins-amd64 $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm32v7 $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm64v8
|
||||
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG-altcoins $DOCKERHUB_REPO:$LATEST_TAG-altcoins-amd64 --os linux --arch amd64
|
||||
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG-altcoins $DOCKERHUB_REPO:$LATEST_TAG-altcoins-arm32v7 --os linux --arch arm --variant v7
|
||||
|
@ -6,7 +6,6 @@ namespace BTCPayServer.Configuration
|
||||
public string PluginDir { get; set; }
|
||||
public string TempStorageDir { get; set; }
|
||||
public string StorageDir { get; set; }
|
||||
public string TempDir { get; set; }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
7
BTCPayServer.Abstractions/Constants/WellKnownTempData.cs
Normal file
7
BTCPayServer.Abstractions/Constants/WellKnownTempData.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace BTCPayServer.Abstractions.Constants;
|
||||
|
||||
public class WellKnownTempData
|
||||
{
|
||||
public const string SuccessMessage = nameof(SuccessMessage);
|
||||
public const string ErrorMessage = nameof(ErrorMessage);
|
||||
}
|
@ -1,10 +1,12 @@
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace BTCPayServer.Abstractions.Contracts
|
||||
{
|
||||
public interface IBTCPayServerClientFactory
|
||||
{
|
||||
Task<BTCPayServerClient> Create(string userId, params string[] storeIds);
|
||||
Task<BTCPayServerClient> Create(string userId, string[] storeIds, HttpContext httpRequest);
|
||||
}
|
||||
}
|
||||
|
17
BTCPayServer.Abstractions/Contracts/IFileService.cs
Normal file
17
BTCPayServer.Abstractions/Contracts/IFileService.cs
Normal file
@ -0,0 +1,17 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace BTCPayServer.Abstractions.Contracts;
|
||||
|
||||
public interface IFileService
|
||||
{
|
||||
Task<bool> IsAvailable();
|
||||
Task<IStoredFile> AddFile(IFormFile file, string userId);
|
||||
Task<IStoredFile> AddFile(Uri file, string userId);
|
||||
Task<string?> GetFileUrl(Uri baseUri, string fileId);
|
||||
Task<string?> GetTemporaryFileUrl(Uri baseUri, string fileId, DateTimeOffset expiry,
|
||||
bool isDownload);
|
||||
Task RemoveFile(string fileId, string userId);
|
||||
}
|
12
BTCPayServer.Abstractions/Contracts/IStoredFile.cs
Normal file
12
BTCPayServer.Abstractions/Contracts/IStoredFile.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace BTCPayServer.Abstractions.Contracts;
|
||||
|
||||
public interface IStoredFile
|
||||
{
|
||||
string Id { get; set; }
|
||||
string FileName { get; set; }
|
||||
string StorageFileName { get; set; }
|
||||
DateTime Timestamp { get; set; }
|
||||
string ApplicationUserId { get; set; }
|
||||
}
|
9
BTCPayServer.Abstractions/Contracts/ISwaggerProvider.cs
Normal file
9
BTCPayServer.Abstractions/Contracts/ISwaggerProvider.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Abstractions.Contracts;
|
||||
|
||||
public interface ISwaggerProvider
|
||||
{
|
||||
Task<JObject> Fetch();
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
|
||||
namespace BTCPayServer.Abstractions.Extensions
|
||||
{
|
||||
public static class SetStatusMessageModelExtensions
|
||||
{
|
||||
public static void SetStatusMessageModel(this ITempDataDictionary tempData, StatusMessageModel statusMessage)
|
||||
{
|
||||
if (statusMessage == null)
|
||||
{
|
||||
tempData.Remove("StatusMessageModel");
|
||||
return;
|
||||
}
|
||||
|
||||
tempData["StatusMessageModel"] = JsonSerializer.Serialize(statusMessage, new JsonSerializerOptions());
|
||||
}
|
||||
}
|
||||
}
|
43
BTCPayServer.Abstractions/Extensions/GreenfieldExtensions.cs
Normal file
43
BTCPayServer.Abstractions/Extensions/GreenfieldExtensions.cs
Normal file
@ -0,0 +1,43 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Client.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
namespace BTCPayServer.Abstractions.Extensions;
|
||||
|
||||
public static class GreenfieldExtensions
|
||||
{
|
||||
public static IActionResult CreateValidationError(this ControllerBase controller, ModelStateDictionary modelState)
|
||||
{
|
||||
return controller.UnprocessableEntity(modelState.ToGreenfieldValidationError());
|
||||
}
|
||||
|
||||
public static List<GreenfieldValidationError> ToGreenfieldValidationError(this ModelStateDictionary modelState)
|
||||
{
|
||||
List<GreenfieldValidationError> errors = new List<GreenfieldValidationError>();
|
||||
foreach (var error in modelState)
|
||||
{
|
||||
foreach (var errorMessage in error.Value.Errors)
|
||||
{
|
||||
errors.Add(new GreenfieldValidationError(error.Key, errorMessage.ErrorMessage));
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
public static IActionResult CreateAPIError(this ControllerBase controller, string errorCode, string errorMessage)
|
||||
{
|
||||
return controller.BadRequest(new GreenfieldAPIError(errorCode, errorMessage));
|
||||
}
|
||||
|
||||
public static IActionResult CreateAPIError(this ControllerBase controller, int httpCode, string errorCode, string errorMessage)
|
||||
{
|
||||
return controller.StatusCode(httpCode, new GreenfieldAPIError(errorCode, errorMessage));
|
||||
}
|
||||
|
||||
public static IActionResult CreateAPIPermissionError(this ControllerBase controller, string missingPermission, string message = null)
|
||||
{
|
||||
return controller.StatusCode(403, new GreenfieldPermissionAPIError(missingPermission, message));
|
||||
}
|
||||
}
|
120
BTCPayServer.Abstractions/Extensions/HttpRequestExtensions.cs
Normal file
120
BTCPayServer.Abstractions/Extensions/HttpRequestExtensions.cs
Normal file
@ -0,0 +1,120 @@
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace BTCPayServer.Abstractions.Extensions;
|
||||
|
||||
public static class HttpRequestExtensions
|
||||
{
|
||||
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(
|
||||
request.Scheme,
|
||||
"://",
|
||||
request.Host.ToUriComponent(),
|
||||
request.PathBase.ToUriComponent());
|
||||
}
|
||||
|
||||
public static Uri GetAbsoluteRootUri(this HttpRequest request)
|
||||
{
|
||||
return new Uri(request.GetAbsoluteRoot());
|
||||
}
|
||||
|
||||
public static string GetCurrentUrl(this HttpRequest request)
|
||||
{
|
||||
return string.Concat(
|
||||
request.Scheme,
|
||||
"://",
|
||||
request.Host.ToUriComponent(),
|
||||
request.PathBase.ToUriComponent(),
|
||||
request.Path.ToUriComponent());
|
||||
}
|
||||
|
||||
public static string GetCurrentPath(this HttpRequest request)
|
||||
{
|
||||
return string.Concat(
|
||||
request.PathBase.ToUriComponent(),
|
||||
request.Path.ToUriComponent());
|
||||
}
|
||||
|
||||
public static string GetCurrentPathWithQueryString(this HttpRequest request)
|
||||
{
|
||||
return request.PathBase + request.Path + request.QueryString;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If 'toto' and RootPath is 'rootpath' returns '/rootpath/toto'
|
||||
/// If 'toto' and RootPath is empty returns '/toto'
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
public static string GetRelativePath(this HttpRequest request, string path)
|
||||
{
|
||||
if (path.Length > 0 && path[0] != '/')
|
||||
path = $"/{path}";
|
||||
return string.Concat(
|
||||
request.PathBase.ToUriComponent(),
|
||||
path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If 'https://example.com/toto' returns 'https://example.com/toto'
|
||||
/// If 'toto' and RootPath is 'rootpath' returns '/rootpath/toto'
|
||||
/// If 'toto' and RootPath is empty returns '/toto'
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
public static string GetRelativePathOrAbsolute(this HttpRequest request, string path)
|
||||
{
|
||||
if (!Uri.TryCreate(path, UriKind.RelativeOrAbsolute, out var uri) ||
|
||||
uri.IsAbsoluteUri)
|
||||
return path;
|
||||
|
||||
if (path.Length > 0 && path[0] != '/')
|
||||
path = $"/{path}";
|
||||
return string.Concat(
|
||||
request.PathBase.ToUriComponent(),
|
||||
path);
|
||||
}
|
||||
|
||||
public static string GetAbsoluteUri(this HttpRequest request, string redirectUrl)
|
||||
{
|
||||
bool isRelative =
|
||||
(redirectUrl.Length > 0 && redirectUrl[0] == '/')
|
||||
|| !new Uri(redirectUrl, UriKind.RelativeOrAbsolute).IsAbsoluteUri;
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
using System.Text.Json;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Abstractions.Extensions;
|
||||
|
||||
public static class SetStatusMessageModelExtensions
|
||||
{
|
||||
public static void SetStatusMessageModel(this ITempDataDictionary tempData, StatusMessageModel statusMessage)
|
||||
{
|
||||
if (statusMessage == null)
|
||||
{
|
||||
tempData.Remove("StatusMessageModel");
|
||||
return;
|
||||
}
|
||||
|
||||
tempData["StatusMessageModel"] = JsonSerializer.Serialize(statusMessage, new JsonSerializerOptions());
|
||||
}
|
||||
|
||||
public static StatusMessageModel GetStatusMessageModel(this ITempDataDictionary tempData)
|
||||
{
|
||||
tempData.TryGetValue(WellKnownTempData.SuccessMessage, out var successMessage);
|
||||
tempData.TryGetValue(WellKnownTempData.ErrorMessage, out var errorMessage);
|
||||
tempData.TryGetValue("StatusMessageModel", out var model);
|
||||
if (successMessage != null || errorMessage != null)
|
||||
{
|
||||
var parsedModel = new StatusMessageModel();
|
||||
parsedModel.Message = (string)successMessage ?? (string)errorMessage;
|
||||
if (successMessage != null)
|
||||
{
|
||||
parsedModel.Severity = StatusMessageModel.StatusSeverity.Success;
|
||||
}
|
||||
else
|
||||
{
|
||||
parsedModel.Severity = StatusMessageModel.StatusSeverity.Error;
|
||||
}
|
||||
return parsedModel;
|
||||
}
|
||||
else if (model != null && model is string str)
|
||||
{
|
||||
return JObject.Parse(str).ToObject<StatusMessageModel>();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static bool HasStatusMessage(this ITempDataDictionary tempData)
|
||||
{
|
||||
return (tempData.Peek(WellKnownTempData.SuccessMessage) ??
|
||||
tempData.Peek(WellKnownTempData.ErrorMessage) ??
|
||||
tempData.Peek("StatusMessageModel")) != null;
|
||||
}
|
||||
|
||||
public static bool HasErrorMessage(this ITempDataDictionary tempData)
|
||||
{
|
||||
return GetStatusMessageModel(tempData)?.Severity == StatusMessageModel.StatusSeverity.Error;
|
||||
}
|
||||
}
|
45
BTCPayServer.Abstractions/Extensions/StringExtensions.cs
Normal file
45
BTCPayServer.Abstractions/Extensions/StringExtensions.cs
Normal file
@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace BTCPayServer.Abstractions.Extensions;
|
||||
|
||||
public static class StringExtensions
|
||||
{
|
||||
|
||||
public static bool IsValidFileName(this string fileName)
|
||||
{
|
||||
return !fileName.ToCharArray().Any(c => Path.GetInvalidFileNameChars().Contains(c)
|
||||
|| c == Path.AltDirectorySeparatorChar
|
||||
|| c == Path.DirectorySeparatorChar
|
||||
|| c == Path.PathSeparator
|
||||
|| c == '\\');
|
||||
}
|
||||
|
||||
public static string Truncate(this string value, int maxLength)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return value;
|
||||
return value.Length <= maxLength ? value : value.Substring(0, maxLength);
|
||||
}
|
||||
|
||||
public static string WithTrailingSlash(this string str)
|
||||
{
|
||||
if (str.EndsWith("/", StringComparison.InvariantCulture))
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
@ -28,8 +28,8 @@
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NBitcoin" Version="6.0.19" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.3.0" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.1" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.3.2" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
@ -54,8 +54,7 @@ namespace BTCPayServer.Client
|
||||
return await HandleResponse<string>(response);
|
||||
}
|
||||
|
||||
|
||||
public virtual async Task PayLightningInvoice(string cryptoCode, PayLightningInvoiceRequest request,
|
||||
public virtual async Task<LightningPaymentData> PayLightningInvoice(string cryptoCode, PayLightningInvoiceRequest request,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
if (request == null)
|
||||
@ -63,7 +62,18 @@ namespace BTCPayServer.Client
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/invoices/pay", bodyPayload: request,
|
||||
method: HttpMethod.Post), token);
|
||||
await HandleResponse(response);
|
||||
return await HandleResponse<LightningPaymentData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<LightningPaymentData> GetLightningPayment(string cryptoCode,
|
||||
string paymentHash, CancellationToken token = default)
|
||||
{
|
||||
if (paymentHash == null)
|
||||
throw new ArgumentNullException(nameof(paymentHash));
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/payments/{paymentHash}",
|
||||
method: HttpMethod.Get), token);
|
||||
return await HandleResponse<LightningPaymentData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<LightningInvoiceData> GetLightningInvoice(string cryptoCode,
|
||||
|
@ -67,6 +67,17 @@ namespace BTCPayServer.Client
|
||||
await HandleResponse(response);
|
||||
}
|
||||
|
||||
public virtual async Task<LightningPaymentData> GetLightningPayment(string storeId, string cryptoCode,
|
||||
string paymentHash, CancellationToken token = default)
|
||||
{
|
||||
if (paymentHash == null)
|
||||
throw new ArgumentNullException(nameof(paymentHash));
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/payments/{paymentHash}",
|
||||
method: HttpMethod.Get), token);
|
||||
return await HandleResponse<LightningPaymentData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<LightningInvoiceData> GetLightningInvoice(string storeId, string cryptoCode,
|
||||
string invoiceId, CancellationToken token = default)
|
||||
{
|
||||
|
@ -20,7 +20,7 @@ namespace BTCPayServer.Client
|
||||
|
||||
var response =
|
||||
await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain",
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain",
|
||||
query), token);
|
||||
return await HandleResponse<IEnumerable<OnChainPaymentMethodData>>(response);
|
||||
}
|
||||
@ -30,7 +30,7 @@ namespace BTCPayServer.Client
|
||||
{
|
||||
var response =
|
||||
await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}"), token);
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}"), token);
|
||||
return await HandleResponse<OnChainPaymentMethodData>(response);
|
||||
}
|
||||
|
||||
@ -39,7 +39,7 @@ namespace BTCPayServer.Client
|
||||
{
|
||||
var response =
|
||||
await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}",
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}",
|
||||
method: HttpMethod.Delete), token);
|
||||
await HandleResponse(response);
|
||||
}
|
||||
@ -49,7 +49,7 @@ namespace BTCPayServer.Client
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}",
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}",
|
||||
bodyPayload: paymentMethod, method: HttpMethod.Put), token);
|
||||
return await HandleResponse<OnChainPaymentMethodData>(response);
|
||||
}
|
||||
@ -61,7 +61,7 @@ namespace BTCPayServer.Client
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/preview",
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/preview",
|
||||
bodyPayload: paymentMethod,
|
||||
queryPayload: new Dictionary<string, object>() { { "offset", offset }, { "amount", amount } },
|
||||
method: HttpMethod.Post), token);
|
||||
@ -73,7 +73,7 @@ namespace BTCPayServer.Client
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/preview",
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/preview",
|
||||
queryPayload: new Dictionary<string, object>() { { "offset", offset }, { "amount", amount } },
|
||||
method: HttpMethod.Get), token);
|
||||
return await HandleResponse<OnChainPaymentMethodPreviewResultData>(response);
|
||||
@ -84,7 +84,7 @@ namespace BTCPayServer.Client
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/generate",
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/generate",
|
||||
bodyPayload: request,
|
||||
method: HttpMethod.Post), token);
|
||||
return await HandleResponse<OnChainPaymentMethodDataWithSensitiveData>(response);
|
||||
|
@ -16,7 +16,7 @@ namespace BTCPayServer.Client
|
||||
{
|
||||
var response =
|
||||
await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet"), token);
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet"), token);
|
||||
return await HandleResponse<OnChainWalletOverviewData>(response);
|
||||
}
|
||||
public virtual async Task<OnChainWalletFeeRateData> GetOnChainFeeRate(string storeId, string cryptoCode, int? blockTarget = null,
|
||||
@ -29,7 +29,7 @@ namespace BTCPayServer.Client
|
||||
}
|
||||
var response =
|
||||
await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/feeRate", queryParams), token);
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/feeRate", queryParams), token);
|
||||
return await HandleResponse<OnChainWalletFeeRateData>(response);
|
||||
}
|
||||
|
||||
@ -38,7 +38,7 @@ namespace BTCPayServer.Client
|
||||
{
|
||||
var response =
|
||||
await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/address", new Dictionary<string, object>()
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/address", new Dictionary<string, object>()
|
||||
{
|
||||
{"forceGenerate", forceGenerate}
|
||||
}), token);
|
||||
@ -50,7 +50,7 @@ namespace BTCPayServer.Client
|
||||
{
|
||||
var response =
|
||||
await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/address", method: HttpMethod.Delete), token);
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/address", method: HttpMethod.Delete), token);
|
||||
await HandleResponse(response);
|
||||
}
|
||||
|
||||
@ -65,7 +65,7 @@ namespace BTCPayServer.Client
|
||||
}
|
||||
var response =
|
||||
await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/transactions", query), token);
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions", query), token);
|
||||
return await HandleResponse<IEnumerable<OnChainWalletTransactionData>>(response);
|
||||
}
|
||||
|
||||
@ -75,7 +75,18 @@ namespace BTCPayServer.Client
|
||||
{
|
||||
var response =
|
||||
await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/transactions/{transactionId}"), token);
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions/{transactionId}"), token);
|
||||
return await HandleResponse<OnChainWalletTransactionData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<OnChainWalletTransactionData> PatchOnChainWalletTransaction(
|
||||
string storeId, string cryptoCode, string transactionId,
|
||||
PatchOnChainTransactionRequest request,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var response =
|
||||
await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions/{transactionId}", queryPayload: null, bodyPayload: request, HttpMethod.Patch), token);
|
||||
return await HandleResponse<OnChainWalletTransactionData>(response);
|
||||
}
|
||||
|
||||
@ -85,7 +96,7 @@ namespace BTCPayServer.Client
|
||||
{
|
||||
var response =
|
||||
await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/utxos"), token);
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/utxos"), token);
|
||||
return await HandleResponse<IEnumerable<OnChainWalletUTXOData>>(response);
|
||||
}
|
||||
|
||||
@ -100,7 +111,7 @@ namespace BTCPayServer.Client
|
||||
}
|
||||
var response =
|
||||
await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/transactions", null, request, HttpMethod.Post), token);
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions", null, request, HttpMethod.Post), token);
|
||||
return await HandleResponse<OnChainWalletTransactionData>(response);
|
||||
}
|
||||
|
||||
@ -115,7 +126,7 @@ namespace BTCPayServer.Client
|
||||
}
|
||||
var response =
|
||||
await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/transactions", null, request, HttpMethod.Post), token);
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions", null, request, HttpMethod.Post), token);
|
||||
return Transaction.Parse(await HandleResponse<string>(response), network);
|
||||
}
|
||||
}
|
||||
|
37
BTCPayServer.Client/BTCPayServerClient.StoreEmail.cs
Normal file
37
BTCPayServer.Client/BTCPayServerClient.StoreEmail.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
|
||||
namespace BTCPayServer.Client
|
||||
{
|
||||
public partial class BTCPayServerClient
|
||||
{
|
||||
public virtual async Task<EmailSettingsData> GetStoreEmailSettings(string storeId,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
using var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/email", method: HttpMethod.Get),
|
||||
token);
|
||||
return await HandleResponse<EmailSettingsData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<EmailSettingsData> UpdateStoreEmailSettings(string storeId, EmailSettingsData request,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
using var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/email", bodyPayload: request, method: HttpMethod.Put),
|
||||
token);
|
||||
return await HandleResponse<EmailSettingsData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task SendEmail(string storeId, SendEmailRequest request,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
using var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/email/send", bodyPayload: request, method: HttpMethod.Post),
|
||||
token);
|
||||
await HandleResponse(response);
|
||||
}
|
||||
}
|
||||
}
|
@ -23,7 +23,7 @@ namespace BTCPayServer.Client
|
||||
await HandleResponse(response);
|
||||
}
|
||||
|
||||
public virtual async Task<StoreData> AddStoreUser(string storeId, StoreUserData request,
|
||||
public virtual async Task AddStoreUser(string storeId, StoreUserData request,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
if (request == null)
|
||||
@ -31,7 +31,7 @@ namespace BTCPayServer.Client
|
||||
using var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/users", bodyPayload: request, method: HttpMethod.Post),
|
||||
token);
|
||||
return await HandleResponse<StoreData>(response);
|
||||
await HandleResponse(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,27 @@ namespace BTCPayServer.Client
|
||||
await HandleResponse(response);
|
||||
}
|
||||
|
||||
public virtual async Task<ApplicationUserData> GetUserByIdOrEmail(string idOrEmail, CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/users/{idOrEmail}", null, HttpMethod.Get), token);
|
||||
return await HandleResponse<ApplicationUserData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task ToggleUser(string idOrEmail, bool enabled, CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/users/{idOrEmail}/toggle", null,new
|
||||
{
|
||||
enabled
|
||||
} , HttpMethod.Post), token);
|
||||
await HandleResponse(response);
|
||||
}
|
||||
|
||||
public virtual async Task<ApplicationUserData[]> GetUsers( CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/users/", null, HttpMethod.Get), token);
|
||||
return await HandleResponse<ApplicationUserData[]>(response);
|
||||
}
|
||||
|
||||
public virtual async Task DeleteCurrentUser(CancellationToken token = default)
|
||||
{
|
||||
await DeleteUser("me", token);
|
||||
|
@ -35,5 +35,7 @@ namespace BTCPayServer.Client.Models
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset? Created { get; set; }
|
||||
|
||||
public bool Disabled { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Lightning.JsonConverters;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
@ -11,15 +11,19 @@ namespace BTCPayServer.Client.Models
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public CreateLightningInvoiceRequest(LightMoney amount, string description, TimeSpan expiry)
|
||||
{
|
||||
Amount = amount;
|
||||
Description = description;
|
||||
Expiry = expiry;
|
||||
}
|
||||
[JsonConverter(typeof(BTCPayServer.Client.JsonConverters.LightMoneyJsonConverter))]
|
||||
|
||||
[JsonConverter(typeof(JsonConverters.LightMoneyJsonConverter))]
|
||||
public LightMoney Amount { get; set; }
|
||||
public string Description { get; set; }
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))]
|
||||
public uint256 DescriptionHash { get; set; }
|
||||
[JsonConverter(typeof(JsonConverters.TimeSpanJsonConverter.Seconds))]
|
||||
public TimeSpan Expiry { get; set; }
|
||||
public bool PrivateRouteHints { get; set; }
|
||||
|
33
BTCPayServer.Client/Models/EmailSettingsData.cs
Normal file
33
BTCPayServer.Client/Models/EmailSettingsData.cs
Normal file
@ -0,0 +1,33 @@
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class EmailSettingsData
|
||||
{
|
||||
public string Server
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public int? Port
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string Login
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string Password
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string FromDisplay
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string From
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
@ -34,6 +35,7 @@ namespace BTCPayServer.Client.Models
|
||||
public string PaymentMethod { get; set; }
|
||||
|
||||
public string CryptoCode { get; set; }
|
||||
public JObject AdditionalData { get; set; }
|
||||
|
||||
public class Payment
|
||||
{
|
||||
|
32
BTCPayServer.Client/Models/LightningPaymentData.cs
Normal file
32
BTCPayServer.Client/Models/LightningPaymentData.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using BTCPayServer.Client.JsonConverters;
|
||||
using BTCPayServer.Lightning;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class LightningPaymentData
|
||||
{
|
||||
public string Id { get; set; }
|
||||
|
||||
public string PaymentHash { get; set; }
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public LightningPaymentStatus Status { get; set; }
|
||||
|
||||
[JsonProperty("BOLT11")]
|
||||
public string BOLT11 { get; set; }
|
||||
|
||||
public string Preimage { get; set; }
|
||||
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset? CreatedAt { get; set; }
|
||||
|
||||
[JsonConverter(typeof(LightMoneyJsonConverter))]
|
||||
public LightMoney TotalAmount { get; set; }
|
||||
|
||||
[JsonConverter(typeof(LightMoneyJsonConverter))]
|
||||
public LightMoney FeeAmount { get; set; }
|
||||
}
|
||||
}
|
@ -21,9 +21,9 @@ namespace BTCPayServer.Client.Models
|
||||
[JsonConverter(typeof(UInt256JsonConverter))]
|
||||
public uint256 BlockHash { get; set; }
|
||||
|
||||
public int? BlockHeight { get; set; }
|
||||
public long? BlockHeight { get; set; }
|
||||
|
||||
public int Confirmations { get; set; }
|
||||
public long Confirmations { get; set; }
|
||||
|
||||
[JsonConverter(typeof(DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
|
@ -21,6 +21,6 @@ namespace BTCPayServer.Client.Models
|
||||
[JsonConverter(typeof(KeyPathJsonConverter))]
|
||||
public KeyPath KeyPath { get; set; }
|
||||
public string Address { get; set; }
|
||||
public int Confirmations { get; set; }
|
||||
public long Confirmations { get; set; }
|
||||
}
|
||||
}
|
||||
|
12
BTCPayServer.Client/Models/PatchOnChainTransactionRequest.cs
Normal file
12
BTCPayServer.Client/Models/PatchOnChainTransactionRequest.cs
Normal file
@ -0,0 +1,12 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class PatchOnChainTransactionRequest
|
||||
{
|
||||
|
||||
public string? Comment { get; set; } = null;
|
||||
public List<string>? Labels { get; set; } = null;
|
||||
}
|
||||
}
|
@ -1,8 +1,20 @@
|
||||
#nullable enable
|
||||
using BTCPayServer.Client.JsonConverters;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class PayLightningInvoiceRequest
|
||||
{
|
||||
[Newtonsoft.Json.JsonProperty("BOLT11")]
|
||||
[JsonProperty("BOLT11")]
|
||||
public string BOLT11 { get; set; }
|
||||
|
||||
[JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))]
|
||||
public float? MaxFeePercent { get; set; }
|
||||
|
||||
[JsonConverter(typeof(MoneyJsonConverter))]
|
||||
public Money? MaxFeeFlat { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,13 @@ using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public enum PullPaymentState
|
||||
{
|
||||
Active,
|
||||
Expired,
|
||||
Archived,
|
||||
Future
|
||||
}
|
||||
public class PullPaymentData
|
||||
{
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
|
10
BTCPayServer.Client/Models/SendEmailRequest.cs
Normal file
10
BTCPayServer.Client/Models/SendEmailRequest.cs
Normal file
@ -0,0 +1,10 @@
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class SendEmailRequest
|
||||
{
|
||||
public string Email;
|
||||
public string Subject;
|
||||
public string Body;
|
||||
}
|
||||
}
|
@ -24,6 +24,7 @@ namespace BTCPayServer.Client
|
||||
public const string CanViewProfile = "btcpay.user.canviewprofile";
|
||||
public const string CanManageNotificationsForUser = "btcpay.user.canmanagenotificationsforuser";
|
||||
public const string CanViewNotificationsForUser = "btcpay.user.canviewnotificationsforuser";
|
||||
public const string CanViewUsers = "btcpay.server.canviewusers";
|
||||
public const string CanCreateUser = "btcpay.server.cancreateuser";
|
||||
public const string CanDeleteUser = "btcpay.user.candeleteuser";
|
||||
public const string CanManagePullPayments = "btcpay.store.canmanagepullpayments";
|
||||
@ -43,6 +44,7 @@ namespace BTCPayServer.Client
|
||||
yield return CanModifyPaymentRequests;
|
||||
yield return CanModifyProfile;
|
||||
yield return CanViewProfile;
|
||||
yield return CanViewUsers;
|
||||
yield return CanCreateUser;
|
||||
yield return CanDeleteUser;
|
||||
yield return CanManageNotificationsForUser;
|
||||
@ -175,7 +177,9 @@ namespace BTCPayServer.Client
|
||||
case Policies.CanViewProfile when this.Policy == Policies.CanModifyProfile:
|
||||
case Policies.CanModifyPaymentRequests when this.Policy == Policies.CanModifyStoreSettings:
|
||||
case Policies.CanViewPaymentRequests when this.Policy == Policies.CanModifyStoreSettings:
|
||||
case Policies.CanManagePullPayments when this.Policy == Policies.CanModifyStoreSettings:
|
||||
case Policies.CanViewPaymentRequests when this.Policy == Policies.CanViewStoreSettings:
|
||||
case Policies.CanViewPaymentRequests when this.Policy == Policies.CanModifyPaymentRequests:
|
||||
case Policies.CanCreateLightningInvoiceInternalNode when this.Policy == Policies.CanUseInternalLightningNode:
|
||||
case Policies.CanCreateLightningInvoiceInStore when this.Policy == Policies.CanUseLightningNodeInStore:
|
||||
case Policies.CanViewNotificationsForUser when this.Policy == Policies.CanManageNotificationsForUser:
|
||||
|
@ -11,7 +11,7 @@ namespace BTCPayServer
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Althash",
|
||||
DisplayName = "Htmlcoin",
|
||||
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://explorer.htmlcoin.com/api/tx/{0}" : "https://explorer.htmlcoin.com/api/tx/{0}",
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
DefaultRateRules = new[]
|
||||
@ -21,7 +21,7 @@ namespace BTCPayServer
|
||||
},
|
||||
CryptoImagePath = "imlegacy/althash.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("88'") : new KeyPath("1'")
|
||||
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("172'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -56,7 +56,6 @@ namespace BTCPayServer
|
||||
InitViacoin();
|
||||
InitMonero();
|
||||
InitZcash();
|
||||
InitPolis();
|
||||
InitChaincoin();
|
||||
// InitArgoneum();//their rate source is down 9/15/20.
|
||||
InitMonetaryUnit();
|
||||
|
@ -1,10 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
|
||||
<Import Project="../Build/Common.csproj" />
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="4.1.3" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="4.2.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(Altcoins)' != 'true'">
|
||||
<Compile Remove="Altcoins\**\*.cs"></Compile>
|
||||
|
@ -1,9 +1,10 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class StoredFile
|
||||
public class StoredFile : IStoredFile
|
||||
{
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public string Id { get; set; }
|
||||
|
@ -6,7 +6,7 @@
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" />
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
|
||||
<PackageReference Include="NBitcoin" Version="6.0.19" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.6.3" />
|
||||
</ItemGroup>
|
||||
|
@ -1274,14 +1274,7 @@
|
||||
"crypto":true
|
||||
},
|
||||
{
|
||||
"name":"POLIS",
|
||||
"code":"POLIS",
|
||||
"divisibility":8,
|
||||
"symbol":null,
|
||||
"crypto":true
|
||||
},
|
||||
{
|
||||
"name":"Althash",
|
||||
"name":"Htmlcoin",
|
||||
"code":"HTML",
|
||||
"divisibility":8,
|
||||
"symbol":null,
|
||||
|
File diff suppressed because one or more lines are too long
@ -1,25 +0,0 @@
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class PolisRateProvider : IRateProvider
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
public PolisRateProvider(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient ?? new HttpClient();
|
||||
}
|
||||
|
||||
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await _httpClient.GetAsync("https://obol.polispay.com/complex/btc/polis", cancellationToken); //Returns complex rate from BTC to POLIS
|
||||
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
|
||||
var value = jobj["data"].Value<decimal>();
|
||||
return new[] { new PairRate(new CurrencyPair("BTC", "POLIS"), new BidAsk(value)) };
|
||||
}
|
||||
}
|
||||
}
|
@ -78,8 +78,6 @@ namespace BTCPayServer.Services.Rates
|
||||
yield return new AvailableRateProvider("ripio", "Ripio", "https://api.exchange.ripio.com/api/v1/rate/all/");
|
||||
yield return new AvailableRateProvider("cryptomarket", "CryptoMarket", "https://api.exchange.cryptomkt.com/api/3/public/ticker/");
|
||||
|
||||
yield return new AvailableRateProvider("polispay", "PolisPay", "https://obol.polispay.com/complex/btc/polis");
|
||||
|
||||
yield return new AvailableRateProvider("bitfinex", "Bitfinex", "https://api.bitfinex.com/v2/tickers?symbols=tBTCUSD,tLTCUSD,tLTCBTC,tETHUSD,tETHBTC,tETCBTC,tETCUSD,tRRTUSD,tRRTBTC,tZECUSD,tZECBTC,tXMRUSD,tXMRBTC,tDSHUSD,tDSHBTC,tBTCEUR,tBTCJPY,tXRPUSD,tXRPBTC,tIOTUSD,tIOTBTC,tIOTETH,tEOSUSD,tEOSBTC,tEOSETH,tSANUSD,tSANBTC,tSANETH,tOMGUSD,tOMGBTC,tOMGETH,tNEOUSD,tNEOBTC,tNEOETH,tETPUSD,tETPBTC,tETPETH,tQTMUSD,tQTMBTC,tQTMETH,tAVTUSD,tAVTBTC,tAVTETH,tEDOUSD,tEDOBTC,tEDOETH,tBTGUSD,tBTGBTC,tDATUSD,tDATBTC,tDATETH,tQSHUSD,tQSHBTC,tQSHETH,tYYWUSD,tYYWBTC,tYYWETH,tGNTUSD,tGNTBTC,tGNTETH,tSNTUSD,tSNTBTC,tSNTETH,tIOTEUR,tBATUSD,tBATBTC,tBATETH,tMNAUSD,tMNABTC,tMNAETH,tFUNUSD,tFUNBTC,tFUNETH,tZRXUSD,tZRXBTC,tZRXETH,tTNBUSD,tTNBBTC,tTNBETH,tSPKUSD,tSPKBTC,tSPKETH,tTRXUSD,tTRXBTC,tTRXETH,tRCNUSD,tRCNBTC,tRCNETH,tRLCUSD,tRLCBTC,tRLCETH,tAIDUSD,tAIDBTC,tAIDETH,tSNGUSD,tSNGBTC,tSNGETH,tREPUSD,tREPBTC,tREPETH,tELFUSD,tELFBTC,tELFETH,tNECUSD,tNECBTC,tNECETH,tBTCGBP,tETHEUR,tETHJPY,tETHGBP,tNEOEUR,tNEOJPY,tNEOGBP,tEOSEUR,tEOSJPY,tEOSGBP,tIOTJPY,tIOTGBP,tIOSUSD,tIOSBTC,tIOSETH,tAIOUSD,tAIOBTC,tAIOETH,tREQUSD,tREQBTC,tREQETH,tRDNUSD,tRDNBTC,tRDNETH,tLRCUSD,tLRCBTC,tLRCETH,tWAXUSD,tWAXBTC,tWAXETH,tDAIUSD,tDAIBTC,tDAIETH,tAGIUSD,tAGIBTC,tAGIETH,tBFTUSD,tBFTBTC,tBFTETH,tMTNUSD,tMTNBTC,tMTNETH,tODEUSD,tODEBTC,tODEETH,tANTUSD,tANTBTC,tANTETH,tDTHUSD,tDTHBTC,tDTHETH,tMITUSD,tMITBTC,tMITETH,tSTJUSD,tSTJBTC,tSTJETH,tXLMUSD,tXLMEUR,tXLMJPY,tXLMGBP,tXLMBTC,tXLMETH,tXVGUSD,tXVGEUR,tXVGJPY,tXVGGBP,tXVGBTC,tXVGETH,tBCIUSD,tBCIBTC,tMKRUSD,tMKRBTC,tMKRETH,tKNCUSD,tKNCBTC,tKNCETH,tPOAUSD,tPOABTC,tPOAETH,tEVTUSD,tLYMUSD,tLYMBTC,tLYMETH,tUTKUSD,tUTKBTC,tUTKETH,tVEEUSD,tVEEBTC,tVEEETH,tDADUSD,tDADBTC,tDADETH,tORSUSD,tORSBTC,tORSETH,tAUCUSD,tAUCBTC,tAUCETH,tPOYUSD,tPOYBTC,tPOYETH,tFSNUSD,tFSNBTC,tFSNETH,tCBTUSD,tCBTBTC,tCBTETH,tZCNUSD,tZCNBTC,tZCNETH,tSENUSD,tSENBTC,tSENETH,tNCAUSD,tNCABTC,tNCAETH,tCNDUSD,tCNDBTC,tCNDETH,tCTXUSD,tCTXBTC,tCTXETH,tPAIUSD,tPAIBTC,tSEEUSD,tSEEBTC,tSEEETH,tESSUSD,tESSBTC,tESSETH,tATMUSD,tATMBTC,tATMETH,tHOTUSD,tHOTBTC,tHOTETH,tDTAUSD,tDTABTC,tDTAETH,tIQXUSD,tIQXBTC,tIQXEOS,tWPRUSD,tWPRBTC,tWPRETH,tZILUSD,tZILBTC,tZILETH,tBNTUSD,tBNTBTC,tBNTETH,tABSUSD,tABSETH,tXRAUSD,tXRAETH,tMANUSD,tMANETH,tBBNUSD,tBBNETH,tNIOUSD,tNIOETH,tDGXUSD,tDGXETH,tVETUSD,tVETBTC,tVETETH,tUTNUSD,tUTNETH,tTKNUSD,tTKNETH,tGOTUSD,tGOTEUR,tGOTETH,tXTZUSD,tXTZBTC,tCNNUSD,tCNNETH,tBOXUSD,tBOXETH,tTRXEUR,tTRXGBP,tTRXJPY,tMGOUSD,tMGOETH,tRTEUSD,tRTEETH,tYGGUSD,tYGGETH,tMLNUSD,tMLNETH,tWTCUSD,tWTCETH,tCSXUSD,tCSXETH,tOMNUSD,tOMNBTC,tINTUSD,tINTETH,tDRNUSD,tDRNETH,tPNKUSD,tPNKETH,tDGBUSD,tDGBBTC,tBSVUSD,tBSVBTC,tBABUSD,tBABBTC,tWLOUSD,tWLOXLM,tVLDUSD,tVLDETH,tENJUSD,tENJETH,tONLUSD,tONLETH,tRBTUSD,tRBTBTC,tUSTUSD,tEUTEUR,tEUTUSD,tGSDUSD,tUDCUSD,tTSDUSD,tPAXUSD,tRIFUSD,tRIFBTC,tPASUSD,tPASETH,tVSYUSD,tVSYBTC,tZRXDAI,tMKRDAI,tOMGDAI,tBTTUSD,tBTTBTC,tBTCUST,tETHUST,tCLOUSD,tCLOBTC,tIMPUSD,tIMPETH,tLTCUST,tEOSUST,tBABUST,tSCRUSD,tSCRETH,tGNOUSD,tGNOETH,tGENUSD,tGENETH,tATOUSD,tATOBTC,tATOETH,tWBTUSD,tXCHUSD,tEUSUSD,tWBTETH,tXCHETH,tEUSETH,tLEOUSD,tLEOBTC,tLEOUST,tLEOEOS,tLEOETH,tASTUSD,tASTETH,tFOAUSD,tFOAETH,tUFRUSD,tUFRETH,tZBTUSD,tZBTUST,tOKBUSD,tUSKUSD,tGTXUSD,tKANUSD,tOKBUST,tOKBETH,tOKBBTC,tUSKUST,tUSKETH,tUSKBTC,tUSKEOS,tGTXUST,tKANUST,tAMPUSD,tALGUSD,tALGBTC,tALGUST,tBTCXCH,tSWMUSD,tSWMETH,tTRIUSD,tTRIETH,tLOOUSD,tLOOETH,tAMPUST,tDUSK:USD,tDUSK:BTC,tUOSUSD,tUOSBTC,tRRBUSD,tRRBUST,tDTXUSD,tDTXUST,tAMPBTC,tFTTUSD,tFTTUST,tPAXUST,tUDCUST,tTSDUST,tBTC:CNHT,tUST:CNHT,tCNH:CNHT,tCHZUSD,tCHZUST,tBTCF0:USTF0,tETHF0:USTF0");
|
||||
yield return new AvailableRateProvider("okex", "OKEx", "https://www.okex.com/api/futures/v3/instruments/ticker");
|
||||
yield return new AvailableRateProvider("coinbasepro", "Coinbase Pro", "https://api.pro.coinbase.com/products");
|
||||
@ -104,7 +102,6 @@ namespace BTCPayServer.Services.Rates
|
||||
Providers.Add("ripio", new RipioExchangeProvider(_httpClientFactory?.CreateClient("EXCHANGE_RIPIO")));
|
||||
Providers.Add("cryptomarket", new CryptoMarketExchangeRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_CRYPTOMARKET")));
|
||||
Providers.Add("bitflyer", new BitflyerRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BITFLYER")));
|
||||
Providers.Add("polispay", new PolisRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_POLIS")));
|
||||
// Providers.Add("argoneum", new ArgoneumRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_ARGONEUM")));
|
||||
|
||||
|
||||
|
@ -358,7 +358,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
if (multiCurrency)
|
||||
user.RegisterDerivationScheme("LTC");
|
||||
foreach (var rateSelection in new[] { "FiatText", "CurrentRateText", "RateThenText" })
|
||||
foreach (var rateSelection in new[] { "FiatTextRadio", "CurrentRateTextRadio", "RateThenTextRadio" })
|
||||
await CanCreateRefundsCore(s, user, multiCurrency, rateSelection);
|
||||
}
|
||||
}
|
||||
@ -402,11 +402,11 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id(rateSelection)).Click();
|
||||
s.Driver.FindElement(By.Id("ok")).Click();
|
||||
Assert.Contains("pull-payments", s.Driver.Url);
|
||||
if (rateSelection == "FiatText")
|
||||
if (rateSelection == "FiatTextRadio")
|
||||
Assert.Contains("$5,500.00", s.Driver.PageSource);
|
||||
if (rateSelection == "CurrentRateText")
|
||||
if (rateSelection == "CurrentRateTextRadio")
|
||||
Assert.Contains("2.20000000 ₿", s.Driver.PageSource);
|
||||
if (rateSelection == "RateThenText")
|
||||
if (rateSelection == "RateThenTextRadio")
|
||||
Assert.Contains("1.10000000 ₿", s.Driver.PageSource);
|
||||
s.GoToInvoice(invoice.Id);
|
||||
s.Driver.FindElement(By.Id("refundlink")).Click();
|
||||
@ -475,11 +475,11 @@ namespace BTCPayServer.Tests
|
||||
tester.ActivateLTC();
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
await user.GrantAccessAsync();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
// First we try payment with a merchant having only BTC
|
||||
var invoice = user.BitPay.CreateInvoice(
|
||||
new Invoice()
|
||||
var invoice = await user.BitPay.CreateInvoiceAsync(
|
||||
new Invoice
|
||||
{
|
||||
Price = 5000.0m,
|
||||
Currency = "USD",
|
||||
@ -490,10 +490,10 @@ namespace BTCPayServer.Tests
|
||||
}, Facade.Merchant);
|
||||
|
||||
var cashCow = tester.ExplorerNode;
|
||||
cashCow.Generate(2); // get some money in case
|
||||
await cashCow.GenerateAsync(2); // get some money in case
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
|
||||
var firstPayment = Money.Coins(0.04m);
|
||||
cashCow.SendToAddress(invoiceAddress, firstPayment);
|
||||
await cashCow.SendToAddressAsync(invoiceAddress, firstPayment);
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
@ -523,8 +523,8 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// Retry now with LTC enabled
|
||||
user.RegisterDerivationScheme("LTC");
|
||||
invoice = user.BitPay.CreateInvoice(
|
||||
new Invoice()
|
||||
invoice = await user.BitPay.CreateInvoiceAsync(
|
||||
new Invoice
|
||||
{
|
||||
Price = 5000.0m,
|
||||
Currency = "USD",
|
||||
@ -537,7 +537,7 @@ namespace BTCPayServer.Tests
|
||||
cashCow = tester.ExplorerNode;
|
||||
invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
|
||||
firstPayment = Money.Coins(0.04m);
|
||||
cashCow.SendToAddress(invoiceAddress, firstPayment);
|
||||
await cashCow.SendToAddressAsync(invoiceAddress, firstPayment);
|
||||
TestLogs.LogInformation("First payment sent to " + invoiceAddress);
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
@ -550,8 +550,8 @@ namespace BTCPayServer.Tests
|
||||
Assert.NotNull(ltcCryptoInfo);
|
||||
invoiceAddress = BitcoinAddress.Create(ltcCryptoInfo.Address, cashCow.Network);
|
||||
var secondPayment = Money.Coins(decimal.Parse(ltcCryptoInfo.Due, CultureInfo.InvariantCulture));
|
||||
cashCow.Generate(4); // LTC is not worth a lot, so just to make sure we have money...
|
||||
cashCow.SendToAddress(invoiceAddress, secondPayment);
|
||||
await cashCow.GenerateAsync(4); // LTC is not worth a lot, so just to make sure we have money...
|
||||
await cashCow.SendToAddressAsync(invoiceAddress, secondPayment);
|
||||
TestLogs.LogInformation("Second payment sent to " + invoiceAddress);
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
@ -570,7 +570,6 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(2, checkout.AvailableCryptos.Count);
|
||||
Assert.Equal("LTC", checkout.CryptoCode);
|
||||
|
||||
|
||||
Assert.Equal(2, invoice.PaymentCodes.Count());
|
||||
Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count());
|
||||
Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count());
|
||||
@ -581,11 +580,10 @@ namespace BTCPayServer.Tests
|
||||
Assert.True(invoice.SupportedTransactionCurrencies["LTC"].Enabled);
|
||||
Assert.True(invoice.PaymentSubtotals.ContainsKey("LTC"));
|
||||
Assert.True(invoice.PaymentTotals.ContainsKey("LTC"));
|
||||
|
||||
|
||||
|
||||
// Check if we can disable LTC
|
||||
invoice = user.BitPay.CreateInvoice(
|
||||
new Invoice()
|
||||
invoice = await user.BitPay.CreateInvoiceAsync(
|
||||
new Invoice
|
||||
{
|
||||
Price = 5000.0m,
|
||||
Currency = "USD",
|
||||
|
@ -7,11 +7,11 @@ using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Security.Greenfield;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using BTCPayServer.Views.Manage;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Support.Extensions;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
@ -20,7 +20,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
public class ApiKeysTests : UnitTestBase
|
||||
{
|
||||
public const int TestTimeout = TestUtils.TestTimeout;
|
||||
public const int TestTimeout = 120_000;
|
||||
|
||||
public const string TestApiPath = "api/test/apikey";
|
||||
public ApiKeysTests(ITestOutputHelper helper) : base(helper)
|
||||
@ -91,7 +91,6 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("AddApiKey")).Click();
|
||||
s.Driver.FindElement(By.CssSelector("button[value='btcpay.store.canmodifystoresettings:change-store-mode']")).Click();
|
||||
//there should be a store already by default in the dropdown
|
||||
var src = s.Driver.PageSource;
|
||||
var getPermissionValueIndex =
|
||||
s.Driver.FindElement(By.CssSelector("input[value='btcpay.store.canmodifystoresettings']"))
|
||||
.GetAttribute("name")
|
||||
@ -100,7 +99,7 @@ namespace BTCPayServer.Tests
|
||||
var option = dropdown.FindElement(By.TagName("option"));
|
||||
var storeId = option.GetAttribute("value");
|
||||
option.Click();
|
||||
s.Driver.FindElement(By.Id("Generate")).Click();
|
||||
s.Driver.WaitForAndClick(By.Id("Generate"));
|
||||
var selectiveStoreApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
|
||||
|
||||
TestLogs.LogInformation("Checking CanModifyStoreSettings with StoreId permissions");
|
||||
@ -108,12 +107,13 @@ namespace BTCPayServer.Tests
|
||||
await TestApiAgainstAccessToken(selectiveStoreApiKey, tester, user,
|
||||
Permission.Create(Policies.CanModifyStoreSettings, storeId).ToString());
|
||||
|
||||
s.Driver.FindElement(By.Id("AddApiKey")).Click();
|
||||
s.Driver.FindElement(By.Id("Generate")).Click();
|
||||
TestLogs.LogInformation("Adding API key for no permissions");
|
||||
s.Driver.WaitForAndClick(By.Id("AddApiKey"));
|
||||
TestLogs.LogInformation("Generating API key for no permissions");
|
||||
s.Driver.WaitForAndClick(By.Id("Generate"));
|
||||
var noPermissionsApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
|
||||
|
||||
TestLogs.LogInformation("Checking no permissions");
|
||||
|
||||
TestLogs.LogInformation($"Checking no permissions: {noPermissionsApiKey}");
|
||||
await TestApiAgainstAccessToken(noPermissionsApiKey, tester, user);
|
||||
|
||||
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
|
||||
@ -137,26 +137,35 @@ namespace BTCPayServer.Tests
|
||||
var callbackUrl = s.ServerUri + "postredirect-callback-test";
|
||||
var authUrl = BTCPayServerClient.GenerateAuthorizeUri(s.ServerUri,
|
||||
new[] { Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings }, applicationDetails: (appidentifier, new Uri(callbackUrl))).ToString();
|
||||
s.Driver.Navigate().GoToUrl(authUrl);
|
||||
TestLogs.LogInformation($"Going to auth URL {authUrl}");
|
||||
s.GoToUrl(authUrl);
|
||||
Assert.Contains(appidentifier, s.Driver.PageSource);
|
||||
Assert.Equal("hidden", s.Driver.FindElement(By.Id("btcpay.store.canmodifystoresettings")).GetAttribute("type").ToLowerInvariant());
|
||||
Assert.Equal("true", s.Driver.FindElement(By.Id("btcpay.store.canmodifystoresettings")).GetAttribute("value").ToLowerInvariant());
|
||||
Assert.Equal("hidden", s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("type").ToLowerInvariant());
|
||||
Assert.Equal("true", s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("value").ToLowerInvariant());
|
||||
Assert.DoesNotContain("change-store-mode", s.Driver.PageSource);
|
||||
s.Driver.FindElement(By.Id("consent-yes")).Click();
|
||||
|
||||
TestLogs.LogInformation("Going to callback URL");
|
||||
|
||||
s.Driver.WaitForAndClick(By.Id("consent-yes"));
|
||||
Assert.Equal(callbackUrl, s.Driver.Url);
|
||||
TestLogs.LogInformation("On callback URL");
|
||||
|
||||
var apiKeyRepo = s.Server.PayTester.GetService<APIKeyRepository>();
|
||||
var accessToken = GetAccessTokenFromCallbackResult(s.Driver);
|
||||
|
||||
TestLogs.LogInformation($"Access token: {accessToken}");
|
||||
|
||||
await TestApiAgainstAccessToken(accessToken, tester, user,
|
||||
(await apiKeyRepo.GetKey(accessToken)).GetBlob().Permissions);
|
||||
|
||||
authUrl = BTCPayServerClient.GenerateAuthorizeUri(s.ServerUri,
|
||||
new[] { Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings }, false, true, applicationDetails: (null, new Uri(callbackUrl))).ToString();
|
||||
|
||||
s.Driver.Navigate().GoToUrl(authUrl);
|
||||
TestLogs.LogInformation($"Going to auth URL 2 {authUrl}");
|
||||
s.GoToUrl(authUrl);
|
||||
TestLogs.LogInformation("On auth URL 2");
|
||||
Assert.DoesNotContain("kukksappname", s.Driver.PageSource);
|
||||
|
||||
Assert.Equal("checkbox", s.Driver.FindElement(By.Id("btcpay.store.canmodifystoresettings")).GetAttribute("type").ToLowerInvariant());
|
||||
@ -166,50 +175,62 @@ namespace BTCPayServer.Tests
|
||||
|
||||
s.Driver.SetCheckbox(By.Id("btcpay.server.canmodifyserversettings"), false);
|
||||
Assert.Contains("change-store-mode", s.Driver.PageSource);
|
||||
s.Driver.FindElement(By.Id("consent-yes")).Click();
|
||||
|
||||
TestLogs.LogInformation("Going to callback URL 2");
|
||||
s.Driver.WaitForAndClick(By.Id("consent-yes"));
|
||||
Assert.Equal(callbackUrl, s.Driver.Url);
|
||||
TestLogs.LogInformation("On callback URL 2");
|
||||
|
||||
accessToken = GetAccessTokenFromCallbackResult(s.Driver);
|
||||
|
||||
TestLogs.LogInformation($"Access token: {accessToken}");
|
||||
TestLogs.LogInformation("Checking authorized permissions");
|
||||
|
||||
await TestApiAgainstAccessToken(accessToken, tester, user,
|
||||
(await apiKeyRepo.GetKey(accessToken)).GetBlob().Permissions);
|
||||
|
||||
//let's test the app identifier system
|
||||
TestLogs.LogInformation("Checking app identifier system");
|
||||
authUrl = BTCPayServerClient.GenerateAuthorizeUri(s.ServerUri,
|
||||
new[] { Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings }, false, true, (appidentifier, new Uri(callbackUrl))).ToString();
|
||||
|
||||
//if it's the same, go to the confirm page
|
||||
s.Driver.Navigate().GoToUrl(authUrl);
|
||||
s.Driver.FindElement(By.Id("continue")).Click();
|
||||
TestLogs.LogInformation($"Going to auth URL 3 {authUrl}");
|
||||
s.GoToUrl(authUrl);
|
||||
TestLogs.LogInformation("On auth URL 3");
|
||||
s.Driver.WaitForAndClick(By.Id("continue"));
|
||||
TestLogs.LogInformation("Going to callback URL 3");
|
||||
Assert.Equal(callbackUrl, s.Driver.Url);
|
||||
TestLogs.LogInformation("On callback URL 3");
|
||||
|
||||
//same app but different redirect = nono
|
||||
authUrl = BTCPayServerClient.GenerateAuthorizeUri(s.ServerUri,
|
||||
new[] { Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings }, false, true, (appidentifier, new Uri("https://international.local/callback"))).ToString();
|
||||
|
||||
s.Driver.Navigate().GoToUrl(authUrl);
|
||||
TestLogs.LogInformation($"Going to auth URL 4 {authUrl}");
|
||||
s.GoToUrl(authUrl);
|
||||
TestLogs.LogInformation("On auth URL 4");
|
||||
Assert.False(s.Driver.Url.StartsWith("https://international.com/callback"));
|
||||
|
||||
// Make sure we can check all permissions when not an admin
|
||||
TestLogs.LogInformation("Make sure we can check all permissions when not an admin");
|
||||
await user.MakeAdmin(false);
|
||||
s.Logout();
|
||||
s.GoToLogin();
|
||||
s.LogIn(user.RegisterDetails.Email, user.RegisterDetails.Password);
|
||||
s.GoToProfile(ManageNavPages.APIKeys);
|
||||
s.Driver.FindElement(By.Id("AddApiKey")).Click();
|
||||
int checkedPermissionCount = 0;
|
||||
foreach (var checkbox in s.Driver.FindElements(By.ClassName("form-check-input")))
|
||||
{
|
||||
checkedPermissionCount++;
|
||||
checkbox.Click();
|
||||
}
|
||||
s.Driver.FindElement(By.Id("Generate")).Click();
|
||||
TestLogs.LogInformation("Go to API Keys page");
|
||||
s.GoToUrl("/account/apikeys");
|
||||
TestLogs.LogInformation("On API Keys page");
|
||||
s.Driver.WaitForAndClick(By.Id("AddApiKey"));
|
||||
int checkedPermissionCount = s.Driver.FindElements(By.ClassName("form-check-input")).Count;
|
||||
TestLogs.LogInformation($"Adding API key: {checkedPermissionCount} permissions");
|
||||
s.Driver.ExecuteJavaScript("document.querySelectorAll('#Permissions .form-check-input').forEach(i => i.click())");
|
||||
TestLogs.LogInformation($"Clicked {checkedPermissionCount}");
|
||||
|
||||
TestLogs.LogInformation("Generating API key");
|
||||
s.Driver.WaitForAndClick(By.Id("Generate"));
|
||||
var allAPIKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
|
||||
|
||||
TestLogs.LogInformation("Checking API key permissions");
|
||||
|
||||
TestLogs.LogInformation($"Checking API key permissions: {allAPIKey}");
|
||||
var apikeydata = await TestApiAgainstAccessToken<ApiKeyData>(allAPIKey, "api/v1/api-keys/current", tester.PayTester.HttpClient);
|
||||
Assert.Equal(checkedPermissionCount, apikeydata.Permissions.Length);
|
||||
}
|
||||
@ -221,12 +242,14 @@ namespace BTCPayServer.Tests
|
||||
expectedPermissions ??= new Permission[0];
|
||||
var apikeydata = await TestApiAgainstAccessToken<ApiKeyData>(accessToken, $"api/v1/api-keys/current", tester.PayTester.HttpClient);
|
||||
var permissions = apikeydata.Permissions;
|
||||
TestLogs.LogInformation($"TestApiAgainstAccessToken: Permissions {permissions.Length}");
|
||||
Assert.Equal(expectedPermissions.Length, permissions.Length);
|
||||
foreach (var expectPermission in expectedPermissions)
|
||||
{
|
||||
Assert.True(permissions.Any(p => p == expectPermission), $"Missing expected permission {expectPermission}");
|
||||
}
|
||||
|
||||
TestLogs.LogInformation("Testing CanViewProfile");
|
||||
if (permissions.Contains(Permission.Create(Policies.CanViewProfile)))
|
||||
{
|
||||
var resultUser = await TestApiAgainstAccessToken<string>(accessToken, $"{TestApiPath}/me/id", tester.PayTester.HttpClient);
|
||||
@ -239,14 +262,17 @@ namespace BTCPayServer.Tests
|
||||
await TestApiAgainstAccessToken<string>(accessToken, $"{TestApiPath}/me/id", tester.PayTester.HttpClient);
|
||||
});
|
||||
}
|
||||
//create a second user to see if any of its data gets messed upin our results.
|
||||
//create a second user to see if any of its data gets messed up in our results.
|
||||
TestLogs.LogInformation("Testing second user");
|
||||
var secondUser = tester.NewAccount();
|
||||
secondUser.GrantAccess();
|
||||
await secondUser.GrantAccessAsync();
|
||||
|
||||
var canModifyAllStores = Permission.Create(Policies.CanModifyStoreSettings, null);
|
||||
var canModifyServer = Permission.Create(Policies.CanModifyServerSettings, null);
|
||||
var unrestricted = Permission.Create(Policies.Unrestricted, null);
|
||||
var selectiveStorePermissions = permissions.Where(p => p.Scope != null && p.Policy == Policies.CanModifyStoreSettings);
|
||||
|
||||
TestLogs.LogInformation("Testing can edit store for first user");
|
||||
if (permissions.Contains(canModifyAllStores) || selectiveStorePermissions.Any())
|
||||
{
|
||||
var resultStores =
|
||||
@ -306,7 +332,6 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
else if (!permissions.Contains(unrestricted))
|
||||
{
|
||||
|
||||
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
|
||||
{
|
||||
await TestApiAgainstAccessToken<bool>(accessToken,
|
||||
@ -321,6 +346,7 @@ namespace BTCPayServer.Tests
|
||||
tester.PayTester.HttpClient);
|
||||
}
|
||||
|
||||
TestLogs.LogInformation("Testing can edit store for second user");
|
||||
if (!permissions.Contains(unrestricted))
|
||||
{
|
||||
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
|
||||
@ -334,7 +360,9 @@ namespace BTCPayServer.Tests
|
||||
await TestApiAgainstAccessToken<bool>(accessToken, $"{TestApiPath}/me/stores/{secondUser.StoreId}/can-edit",
|
||||
tester.PayTester.HttpClient);
|
||||
}
|
||||
TestLogs.LogInformation("Testing can edit store for second user expectation met");
|
||||
|
||||
TestLogs.LogInformation($"Testing CanModifyServer with {permissions.Contains(canModifyServer)}");
|
||||
if (permissions.Contains(canModifyServer))
|
||||
{
|
||||
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
|
||||
@ -350,17 +378,21 @@ namespace BTCPayServer.Tests
|
||||
tester.PayTester.HttpClient);
|
||||
});
|
||||
}
|
||||
TestLogs.LogInformation("Testing CanModifyServer expectation met");
|
||||
}
|
||||
|
||||
public async Task<T> TestApiAgainstAccessToken<T>(string apikey, string url, HttpClient client)
|
||||
{
|
||||
var httpRequest = new HttpRequestMessage(HttpMethod.Get,
|
||||
new Uri(client.BaseAddress, url));
|
||||
var uri = new Uri(client.BaseAddress, url);
|
||||
var httpRequest = new HttpRequestMessage(HttpMethod.Get, uri);
|
||||
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("token", apikey);
|
||||
TestLogs.LogInformation($"Testing {uri}");
|
||||
var result = await client.SendAsync(httpRequest);
|
||||
TestLogs.LogInformation($"Testing {uri} status: {result.StatusCode}");
|
||||
result.EnsureSuccessStatusCode();
|
||||
|
||||
var rawJson = await result.Content.ReadAsStringAsync();
|
||||
TestLogs.LogInformation($"Testing {uri} result: {rawJson}");
|
||||
if (typeof(T).IsPrimitive || typeof(T) == typeof(string))
|
||||
{
|
||||
return (T)Convert.ChangeType(rawJson, typeof(T));
|
||||
|
@ -23,7 +23,7 @@
|
||||
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14" />
|
||||
<PackageReference Include="Selenium.Support" Version="3.141.0" />
|
||||
<PackageReference Include="Selenium.WebDriver" Version="3.141.0" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="96.0.4664.4500" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="98.0.4758.10200" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
@ -106,8 +106,6 @@ retry:
|
||||
driver.ExecuteJavaScript($"document.getElementById('{collapseId}').classList.add('show')");
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static void SetAttribute(this IWebDriver driver, string element, string attribute, string value)
|
||||
{
|
||||
driver.ExecuteJavaScript($"document.getElementById('{element}').setAttribute('{attribute}', '{value}')");
|
||||
@ -127,12 +125,17 @@ retry:
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
public static void ScrollTo(this IWebDriver driver, IWebElement element)
|
||||
{
|
||||
driver.ExecuteJavaScript("arguments[0].scrollIntoView();", element);
|
||||
}
|
||||
|
||||
public static void ScrollTo(this IWebDriver driver, By selector)
|
||||
{
|
||||
var element = driver.FindElement(selector);
|
||||
driver.ExecuteJavaScript("arguments[0].scrollIntoView();", element);
|
||||
ScrollTo(driver, driver.FindElement(selector));
|
||||
}
|
||||
|
||||
public static void WaitForAndClick(this IWebDriver driver, By selector)
|
||||
{
|
||||
// Try fast path
|
||||
|
@ -3,14 +3,13 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Configuration;
|
||||
@ -40,7 +39,6 @@ using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -1765,5 +1763,18 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(btc.CryptoCode, paymentMethod.CryptoCode);
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllPoliciesShowInUI()
|
||||
{
|
||||
foreach (var policy in Policies.AllPolicies)
|
||||
{
|
||||
Assert.True( UIManageController.AddApiKeyViewModel.PermissionValueItem.PermissionDescriptions.ContainsKey(policy));
|
||||
if (Policies.IsStorePolicy(policy))
|
||||
{
|
||||
Assert.True( UIManageController.AddApiKeyViewModel.PermissionValueItem.PermissionDescriptions.ContainsKey($"{policy}:"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -212,6 +212,100 @@ namespace BTCPayServer.Tests
|
||||
|
||||
tester.Stores.Remove(user.StoreId);
|
||||
}
|
||||
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanViewUsersViaApi()
|
||||
{
|
||||
using var tester = CreateServerTester(newDb: true);
|
||||
await tester.StartAsync();
|
||||
|
||||
var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri);
|
||||
|
||||
// Should be 401 for all calls because we don't have permission
|
||||
await AssertHttpError(401, async () => await unauthClient.GetUsers());
|
||||
await AssertHttpError(401, async () => await unauthClient.GetUserByIdOrEmail("non_existing_id"));
|
||||
await AssertHttpError(401, async () => await unauthClient.GetUserByIdOrEmail("someone@example.com"));
|
||||
|
||||
|
||||
var adminUser = tester.NewAccount();
|
||||
await adminUser.GrantAccessAsync();
|
||||
await adminUser.MakeAdmin();
|
||||
var adminClient = await adminUser.CreateClient(Policies.Unrestricted);
|
||||
|
||||
// Should be 404 if user doesn't exist
|
||||
await AssertHttpError(404,async () => await adminClient.GetUserByIdOrEmail("non_existing_id"));
|
||||
await AssertHttpError(404,async () => await adminClient.GetUserByIdOrEmail("doesnotexist@example.com"));
|
||||
|
||||
// Try listing all users, should be fine
|
||||
await adminClient.GetUsers();
|
||||
|
||||
// Try loading 1 user by ID. Loading myself.
|
||||
await adminClient.GetUserByIdOrEmail(adminUser.UserId);
|
||||
|
||||
// Try loading 1 user by email. Loading myself.
|
||||
await adminClient.GetUserByIdOrEmail(adminUser.Email);
|
||||
|
||||
|
||||
// var badClient = await user.CreateClient(Policies.CanCreateInvoice);
|
||||
// await AssertHttpError(403,
|
||||
// async () => await badClient.DeleteCurrentUser());
|
||||
|
||||
var goodUser = tester.NewAccount();
|
||||
await goodUser.GrantAccessAsync();
|
||||
await goodUser.MakeAdmin();
|
||||
var goodClient = await goodUser.CreateClient(Policies.CanViewUsers);
|
||||
|
||||
// Try listing all users, should be fine
|
||||
await goodClient.GetUsers();
|
||||
|
||||
// Should be 404 if user doesn't exist
|
||||
await AssertHttpError(404,async () => await goodClient.GetUserByIdOrEmail("non_existing_id"));
|
||||
await AssertHttpError(404,async () => await goodClient.GetUserByIdOrEmail("doesnotexist@example.com"));
|
||||
|
||||
// Try listing all users, should be fine
|
||||
await goodClient.GetUsers();
|
||||
|
||||
// Try loading 1 user by ID. Loading myself.
|
||||
await goodClient.GetUserByIdOrEmail(goodUser.UserId);
|
||||
|
||||
// Try loading 1 user by email. Loading myself.
|
||||
await goodClient.GetUserByIdOrEmail(goodUser.Email);
|
||||
|
||||
|
||||
|
||||
|
||||
var badUser = tester.NewAccount();
|
||||
await badUser.GrantAccessAsync();
|
||||
await badUser.MakeAdmin();
|
||||
|
||||
// Bad user has a permission, but it's the wrong one.
|
||||
var badClient = await goodUser.CreateClient(Policies.CanCreateInvoice);
|
||||
|
||||
// Try listing all users, should be fine
|
||||
await AssertHttpError(403,async () => await badClient.GetUsers());
|
||||
|
||||
// Should be 404 if user doesn't exist
|
||||
await AssertHttpError(403,async () => await badClient.GetUserByIdOrEmail("non_existing_id"));
|
||||
await AssertHttpError(403,async () => await badClient.GetUserByIdOrEmail("doesnotexist@example.com"));
|
||||
|
||||
// Try listing all users, should be fine
|
||||
await AssertHttpError(403,async () => await badClient.GetUsers());
|
||||
|
||||
// Try loading 1 user by ID. Loading myself.
|
||||
await AssertHttpError(403,async () => await badClient.GetUserByIdOrEmail(badUser.UserId));
|
||||
|
||||
// Try loading 1 user by email. Loading myself.
|
||||
await AssertHttpError(403,async () => await badClient.GetUserByIdOrEmail(badUser.Email));
|
||||
|
||||
|
||||
|
||||
|
||||
// Why is this line needed? I saw it in "CanDeleteUsersViaApi" as well. Is this part of the cleanup?
|
||||
tester.Stores.Remove(adminUser.StoreId);
|
||||
}
|
||||
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
@ -1099,12 +1193,13 @@ namespace BTCPayServer.Tests
|
||||
new CreateInvoiceRequest() { Currency = "helloinvalid", Amount = 1 });
|
||||
});
|
||||
await user.RegisterDerivationSchemeAsync("BTC");
|
||||
string origOrderId = "testOrder";
|
||||
var newInvoice = await client.CreateInvoice(user.StoreId,
|
||||
new CreateInvoiceRequest()
|
||||
{
|
||||
Currency = "USD",
|
||||
Amount = 1,
|
||||
Metadata = JObject.Parse("{\"itemCode\": \"testitem\", \"orderId\": \"testOrder\"}"),
|
||||
Metadata = JObject.Parse($"{{\"itemCode\": \"testitem\", \"orderId\": \"{origOrderId}\"}}"),
|
||||
Checkout = new CreateInvoiceRequest.CheckoutOptions()
|
||||
{
|
||||
RedirectAutomatically = true,
|
||||
@ -1225,6 +1320,8 @@ namespace BTCPayServer.Tests
|
||||
|
||||
newInvoice = await client.GetInvoice(user.StoreId, newInvoice.Id);
|
||||
|
||||
const string newOrderId = "UPDATED-ORDER-ID";
|
||||
JObject metadataForUpdate = JObject.Parse($"{{\"orderId\": \"{newOrderId}\", \"itemCode\": \"updated\", newstuff: [1,2,3,4,5]}}");
|
||||
Assert.Contains(InvoiceStatus.Settled, newInvoice.AvailableStatusesForManualMarking);
|
||||
Assert.DoesNotContain(InvoiceStatus.Invalid, newInvoice.AvailableStatusesForManualMarking);
|
||||
await AssertHttpError(403, async () =>
|
||||
@ -1232,23 +1329,36 @@ namespace BTCPayServer.Tests
|
||||
await viewOnly.UpdateInvoice(user.StoreId, invoice.Id,
|
||||
new UpdateInvoiceRequest()
|
||||
{
|
||||
Metadata = JObject.Parse("{\"itemCode\": \"updated\", newstuff: [1,2,3,4,5]}")
|
||||
Metadata = metadataForUpdate
|
||||
});
|
||||
});
|
||||
invoice = await client.UpdateInvoice(user.StoreId, invoice.Id,
|
||||
new UpdateInvoiceRequest()
|
||||
{
|
||||
Metadata = JObject.Parse("{\"itemCode\": \"updated\", newstuff: [1,2,3,4,5]}")
|
||||
Metadata = metadataForUpdate
|
||||
});
|
||||
|
||||
Assert.Equal(newOrderId, invoice.Metadata["orderId"].Value<string>());
|
||||
Assert.Equal("updated", invoice.Metadata["itemCode"].Value<string>());
|
||||
Assert.Equal(15, ((JArray)invoice.Metadata["newstuff"]).Values<int>().Sum());
|
||||
|
||||
//also test the the metadata actually got saved
|
||||
invoice = await client.GetInvoice(user.StoreId, invoice.Id);
|
||||
Assert.Equal(newOrderId, invoice.Metadata["orderId"].Value<string>());
|
||||
Assert.Equal("updated", invoice.Metadata["itemCode"].Value<string>());
|
||||
Assert.Equal(15, ((JArray)invoice.Metadata["newstuff"]).Values<int>().Sum());
|
||||
|
||||
// test if we can find the updated invoice using the new orderId
|
||||
var invoicesWithOrderId = await client.GetInvoices(user.StoreId, new[] { newOrderId });
|
||||
Assert.NotNull(invoicesWithOrderId);
|
||||
Assert.Single(invoicesWithOrderId);
|
||||
Assert.Equal(invoice.Id, invoicesWithOrderId.First().Id);
|
||||
|
||||
// test if the old orderId does not yield any results anymore
|
||||
var invoicesWithOldOrderId = await client.GetInvoices(user.StoreId, new[] { origOrderId });
|
||||
Assert.NotNull(invoicesWithOldOrderId);
|
||||
Assert.Empty(invoicesWithOldOrderId);
|
||||
|
||||
//archive
|
||||
await AssertHttpError(403, async () =>
|
||||
{
|
||||
@ -1437,7 +1547,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Single(info.NodeURIs);
|
||||
Assert.NotEqual(0, info.BlockHeight);
|
||||
|
||||
await AssertAPIError("ligthning-node-unavailable", () => client.GetLightningNodeChannels("BTC"));
|
||||
await AssertAPIError("lightning-node-unavailable", () => client.GetLightningNodeChannels("BTC"));
|
||||
// Not permission for the store!
|
||||
await AssertAPIError("missing-permission", () => client.GetLightningNodeChannels(user.StoreId, "BTC"));
|
||||
var invoiceData = await client.CreateLightningInvoice("BTC", new CreateLightningInvoiceRequest()
|
||||
@ -1593,7 +1703,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(firstAddress, (await viewOnlyClient.PreviewProposedStoreOnChainPaymentMethodAddresses(store.Id, "BTC",
|
||||
new UpdateOnChainPaymentMethodRequest() { Enabled = true, DerivationScheme = xpub })).Addresses.First().Address);
|
||||
|
||||
await AssertValidationError(new[] { "accountKeyPath" }, () => viewOnlyClient.SendHttpRequest<GreenfieldValidationError[]>(path: $"api/v1/stores/{store.Id}/payment-methods/Onchain/BTC/preview", method: HttpMethod.Post,
|
||||
await AssertValidationError(new[] { "accountKeyPath" }, () => viewOnlyClient.SendHttpRequest<GreenfieldValidationError[]>(path: $"api/v1/stores/{store.Id}/payment-methods/onchain/BTC/preview", method: HttpMethod.Post,
|
||||
bodyPayload: JObject.Parse("{\"accountKeyPath\": \"0/1\"}")));
|
||||
|
||||
var method = await client.UpdateStoreOnChainPaymentMethod(store.Id, "BTC",
|
||||
@ -2016,7 +2126,30 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
await viewOnlyClient.GetOnChainWalletTransaction(walletId.StoreId, walletId.CryptoCode, txdata.TransactionHash.ToString());
|
||||
});
|
||||
await client.GetOnChainWalletTransaction(walletId.StoreId, walletId.CryptoCode, txdata.TransactionHash.ToString());
|
||||
var transaction = await client.GetOnChainWalletTransaction(walletId.StoreId, walletId.CryptoCode, txdata.TransactionHash.ToString());
|
||||
|
||||
Assert.Equal(transaction.TransactionHash, txdata.TransactionHash);
|
||||
Assert.Equal(String.Empty, transaction.Comment);
|
||||
Assert.Equal(new Dictionary<string, LabelData>(), transaction.Labels);
|
||||
|
||||
// transaction patch tests
|
||||
var patchedTransaction = await client.PatchOnChainWalletTransaction(
|
||||
walletId.StoreId, walletId.CryptoCode, txdata.TransactionHash.ToString(),
|
||||
new PatchOnChainTransactionRequest() {
|
||||
Comment = "test comment",
|
||||
Labels = new List<string>
|
||||
{
|
||||
"test label"
|
||||
}
|
||||
});
|
||||
Assert.Equal("test comment", patchedTransaction.Comment);
|
||||
Assert.Equal(
|
||||
new Dictionary<string, LabelData>()
|
||||
{
|
||||
{ "test label", new LabelData(){ Type = "raw", Text = "test label" } }
|
||||
}.ToJson(),
|
||||
patchedTransaction.Labels.ToJson()
|
||||
);
|
||||
|
||||
await AssertHttpError(403, async () =>
|
||||
{
|
||||
@ -2171,6 +2304,38 @@ namespace BTCPayServer.Tests
|
||||
await AssertAPIError("store-user-role-orphaned", async () => await user2Client.RemoveStoreUser(user.StoreId, user2.UserId));
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task StoreEmailTests()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var admin = tester.NewAccount();
|
||||
await admin.GrantAccessAsync(true);
|
||||
var adminClient = await admin.CreateClient(Policies.Unrestricted);
|
||||
await adminClient.UpdateStoreEmailSettings(admin.StoreId,
|
||||
new EmailSettingsData());
|
||||
|
||||
var data = new EmailSettingsData()
|
||||
{
|
||||
From = "admin@admin.com",
|
||||
Login = "admin@admin.com",
|
||||
Password = "admin@admin.com",
|
||||
Port = 1234,
|
||||
Server = "admin.com",
|
||||
};
|
||||
await adminClient.UpdateStoreEmailSettings(admin.StoreId, data);
|
||||
var s = await adminClient.GetStoreEmailSettings(admin.StoreId);
|
||||
Assert.Equal(JsonConvert.SerializeObject(s), JsonConvert.SerializeObject(data));
|
||||
await AssertValidationError(new[] { nameof(EmailSettingsData.From) },
|
||||
async () => await adminClient.UpdateStoreEmailSettings(admin.StoreId,
|
||||
new EmailSettingsData() { From = "ass" }));
|
||||
|
||||
|
||||
await adminClient.SendEmail(admin.StoreId,
|
||||
new SendEmailRequest() { Body = "lol", Subject = "subj", Email = "sdasdas" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Models;
|
||||
@ -8,140 +9,125 @@ using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Support.Extensions;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
[Collection(nameof(NonParallelizableCollectionDefinition))]
|
||||
public class PSBTTests : UnitTestBase
|
||||
{
|
||||
public PSBTTests(ITestOutputHelper helper) : base(helper)
|
||||
{
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
[Trait("Selenium", "Selenium")]
|
||||
public async Task CanPlayWithPSBT()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 10,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some \", description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
var cashCow = tester.ExplorerNode;
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
|
||||
cashCow.SendToAddress(invoiceAddress, Money.Coins(1.5m));
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.Equal("paid", invoice.Status);
|
||||
});
|
||||
using var s = CreateSeleniumTester(newDb: true);
|
||||
await s.StartAsync();
|
||||
|
||||
var walletController = user.GetController<UIWalletsController>();
|
||||
var walletId = new WalletId(user.StoreId, "BTC");
|
||||
var sendDestination = new Key().PubKey.Hash.GetAddress(user.SupportedNetwork.NBitcoinNetwork).ToString();
|
||||
var sendModel = new WalletSendModel()
|
||||
{
|
||||
Outputs = new List<WalletSendModel.TransactionOutput>()
|
||||
{
|
||||
new WalletSendModel.TransactionOutput()
|
||||
{
|
||||
DestinationAddress = sendDestination,
|
||||
Amount = 0.1m,
|
||||
}
|
||||
},
|
||||
FeeSatoshiPerByte = 1,
|
||||
CurrentBalance = 1.5m
|
||||
};
|
||||
var u1 = s.RegisterNewUser(true);
|
||||
var hot = s.CreateNewStore();
|
||||
var seed = s.GenerateWallet(isHotWallet: true);
|
||||
var cold = s.CreateNewStore();
|
||||
s.GenerateWallet(isHotWallet: false, seed: seed.ToString());
|
||||
|
||||
string redirectedPSBT = AssertRedirectedPSBT(await walletController.WalletSend(walletId, sendModel, command: "analyze-psbt"), nameof(walletController.WalletPSBT));
|
||||
var vmPSBT = await walletController.WalletPSBT(walletId, new WalletPSBTViewModel() { PSBT = redirectedPSBT }).AssertViewModelAsync<WalletPSBTViewModel>();
|
||||
var unsignedPSBT = PSBT.Parse(vmPSBT.PSBT, user.SupportedNetwork.NBitcoinNetwork);
|
||||
Assert.NotNull(vmPSBT.Decoded);
|
||||
// Scenario 1: one user has two stores sharing same seed
|
||||
// one store is hot wallet, the other not.
|
||||
|
||||
var filePSBT = (FileContentResult)(await walletController.WalletPSBT(walletId, vmPSBT, "save-psbt"));
|
||||
PSBT.Load(filePSBT.FileContents, user.SupportedNetwork.NBitcoinNetwork);
|
||||
// Here, the cold wallet create a PSBT, then we switch to hot wallet to sign
|
||||
// the PSBT and broadcast
|
||||
s.GoToStore(cold.storeId);
|
||||
var address = await s.FundStoreWallet();
|
||||
Thread.Sleep(1000);
|
||||
s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.Send);
|
||||
SendAllTo(s, address);
|
||||
s.Driver.FindElement(By.Id("SignWithPSBT")).Click();
|
||||
|
||||
var vmPSBT2 = await walletController.WalletPSBT(walletId, new WalletPSBTViewModel
|
||||
{
|
||||
SigningContext = new SigningContextModel
|
||||
{
|
||||
PSBT = AssertRedirectedPSBT(await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"), nameof(walletController.WalletPSBTReady))
|
||||
}
|
||||
}).AssertViewModelAsync<WalletPSBTViewModel>();
|
||||
Assert.NotEmpty(vmPSBT2.Inputs.Where(i => i.Error != null));
|
||||
Assert.Equal(vmPSBT.PSBT, vmPSBT2.SigningContext.PSBT);
|
||||
var psbt = ExtractPSBT(s);
|
||||
|
||||
var signedPSBT = unsignedPSBT.Clone();
|
||||
signedPSBT.SignAll(user.DerivationScheme, user.GenerateWalletResponseV.AccountHDKey, user.GenerateWalletResponseV.AccountKeyPath);
|
||||
vmPSBT.PSBT = signedPSBT.ToBase64();
|
||||
var psbtReady = await walletController.WalletPSBT(walletId, new WalletPSBTViewModel
|
||||
{
|
||||
SigningContext = new SigningContextModel
|
||||
{
|
||||
PSBT = AssertRedirectedPSBT(await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"), nameof(walletController.WalletPSBTReady))
|
||||
}
|
||||
}).AssertViewModelAsync<WalletPSBTViewModel>();
|
||||
Assert.Equal(2 + 1, psbtReady.Destinations.Count); // The fee is a destination
|
||||
Assert.Contains(psbtReady.Destinations, d => d.Destination == sendDestination && !d.Positive);
|
||||
Assert.Contains(psbtReady.Destinations, d => d.Positive);
|
||||
s.GoToStore(hot.storeId);
|
||||
s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.PSBT);
|
||||
s.Driver.FindElement(By.Name("PSBT")).SendKeys(psbt);
|
||||
s.Driver.FindElement(By.Id("Decode")).Click();
|
||||
s.Driver.FindElement(By.Id("SignTransaction")).Click();
|
||||
s.Driver.FindElement(By.Id("BroadcastTransaction")).Click();
|
||||
s.FindAlertMessage();
|
||||
|
||||
vmPSBT.PSBT = unsignedPSBT.ToBase64();
|
||||
var combineVM = await walletController.WalletPSBT(walletId, vmPSBT, "combine").AssertViewModelAsync<WalletPSBTCombineViewModel>();
|
||||
Assert.Equal(vmPSBT.PSBT, combineVM.OtherPSBT);
|
||||
combineVM.PSBT = signedPSBT.ToBase64();
|
||||
var psbt = AssertRedirectedPSBT(await walletController.WalletPSBTCombine(walletId, combineVM), nameof(walletController.WalletPSBT));
|
||||
// Scenario 2: Same as scenario 1, except we create a PSBT from hot wallet, then sign by manually
|
||||
// entering the seed on the cold wallet.
|
||||
s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.Send);
|
||||
SendAllTo(s, address);
|
||||
psbt = ExtractPSBT(s);
|
||||
|
||||
var signedPSBT2 = PSBT.Parse(psbt, user.SupportedNetwork.NBitcoinNetwork);
|
||||
Assert.True(signedPSBT.TryFinalize(out _));
|
||||
Assert.True(signedPSBT2.TryFinalize(out _));
|
||||
Assert.Equal(signedPSBT, signedPSBT2);
|
||||
// Let's check it has been signed, then remove the signature.
|
||||
// Also remove the hdkeys so we can test the update later
|
||||
var psbtParsed = PSBT.Parse(psbt, s.Server.NetworkProvider.BTC.NBitcoinNetwork);
|
||||
var signedPSBT = psbtParsed.Clone();
|
||||
Assert.True(psbtParsed.Clone().TryFinalize(out _));
|
||||
Assert.Single(psbtParsed.Inputs[0].PartialSigs);
|
||||
psbtParsed.Inputs[0].PartialSigs.Clear();
|
||||
Assert.Single(psbtParsed.Inputs[0].HDKeyPaths);
|
||||
psbtParsed.Inputs[0].HDKeyPaths.Clear();
|
||||
var skeletonPSBT = psbtParsed;
|
||||
|
||||
// Can use uploaded file?
|
||||
combineVM.PSBT = null;
|
||||
combineVM.UploadedPSBTFile = TestUtils.GetFormFile("signedPSBT", signedPSBT.ToBytes());
|
||||
psbt = AssertRedirectedPSBT(await walletController.WalletPSBTCombine(walletId, combineVM), nameof(walletController.WalletPSBT));
|
||||
signedPSBT2 = PSBT.Parse(psbt, user.SupportedNetwork.NBitcoinNetwork);
|
||||
Assert.True(signedPSBT.TryFinalize(out _));
|
||||
Assert.True(signedPSBT2.TryFinalize(out _));
|
||||
Assert.Equal(signedPSBT, signedPSBT2);
|
||||
s.GoToStore(cold.storeId);
|
||||
s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.PSBT);
|
||||
s.Driver.FindElement(By.Name("PSBT")).SendKeys(skeletonPSBT.ToBase64());
|
||||
s.Driver.FindElement(By.Id("Decode")).Click();
|
||||
s.Driver.FindElement(By.Id("SignTransaction")).Click();
|
||||
s.Driver.FindElement(By.Id("SignWithSeed")).Click();
|
||||
s.Driver.FindElement(By.Name("SeedOrKey")).SendKeys(seed.ToString());
|
||||
s.Driver.FindElement(By.Id("Submit")).Click();
|
||||
s.Driver.FindElement(By.Id("BroadcastTransaction")).Click();
|
||||
s.FindAlertMessage();
|
||||
|
||||
var ready = (await walletController.WalletPSBT(walletId, new WalletPSBTViewModel
|
||||
{
|
||||
SigningContext = new SigningContextModel(signedPSBT)
|
||||
})).AssertViewModel<WalletPSBTViewModel>();
|
||||
Assert.Equal(signedPSBT.ToBase64(), ready.SigningContext.PSBT);
|
||||
psbt = AssertRedirectedPSBT(await walletController.WalletPSBTReady(walletId, ready, command: "analyze-psbt"), nameof(walletController.WalletPSBT));
|
||||
Assert.Equal(signedPSBT.ToBase64(), psbt);
|
||||
var redirect = Assert.IsType<RedirectToActionResult>(await walletController.WalletPSBTReady(walletId, ready, command: "broadcast"));
|
||||
Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName);
|
||||
// Let's check if the update feature works
|
||||
s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.PSBT);
|
||||
s.Driver.FindElement(By.Name("PSBT")).SendKeys(skeletonPSBT.ToBase64());
|
||||
s.Driver.FindElement(By.Id("Decode")).Click();
|
||||
s.Driver.FindElement(By.Id("PSBTOptionsAdvancedHeader")).Click();
|
||||
s.Driver.WaitForElement(By.Id("update-psbt")).Click();
|
||||
|
||||
//test base64 psbt file
|
||||
Assert.False(string.IsNullOrEmpty(Assert.IsType<WalletPSBTViewModel>(
|
||||
Assert.IsType<ViewResult>(
|
||||
await walletController.WalletPSBT(walletId,
|
||||
new WalletPSBTViewModel
|
||||
{
|
||||
UploadedPSBTFile = TestUtils.GetFormFile("base64", signedPSBT.ToBase64())
|
||||
})).Model).PSBT));
|
||||
psbt = ExtractPSBT(s);
|
||||
psbtParsed = PSBT.Parse(psbt, s.Server.NetworkProvider.BTC.NBitcoinNetwork);
|
||||
Assert.Single(psbtParsed.Inputs[0].HDKeyPaths);
|
||||
Assert.Empty(psbtParsed.Inputs[0].PartialSigs);
|
||||
|
||||
// Let's if we can combine the updated psbt (which has hdkeys, but no sig)
|
||||
// with the signed psbt (which has sig, but no hdkeys)
|
||||
s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.PSBT);
|
||||
s.Driver.FindElement(By.Name("PSBT")).SendKeys(psbtParsed.ToBase64());
|
||||
s.Driver.FindElement(By.Id("Decode")).Click();
|
||||
s.Driver.FindElement(By.Id("PSBTOptionsAdvancedHeader")).Click();
|
||||
s.Driver.WaitForElement(By.Id("combine-psbt")).Click();
|
||||
signedPSBT.Inputs[0].HDKeyPaths.Clear();
|
||||
s.Driver.FindElement(By.Name("PSBT")).SendKeys(signedPSBT.ToBase64());
|
||||
s.Driver.WaitForElement(By.Id("Submit")).Click();
|
||||
|
||||
psbt = ExtractPSBT(s);
|
||||
psbtParsed = PSBT.Parse(psbt, s.Server.NetworkProvider.BTC.NBitcoinNetwork);
|
||||
Assert.Single(psbtParsed.Inputs[0].HDKeyPaths);
|
||||
Assert.Single(psbtParsed.Inputs[0].PartialSigs);
|
||||
}
|
||||
|
||||
private static string AssertRedirectedPSBT(IActionResult view, string actionName)
|
||||
private static void SendAllTo(SeleniumTester s, string address)
|
||||
{
|
||||
var postRedirectView = Assert.IsType<ViewResult>(view);
|
||||
var postRedirectViewModel = Assert.IsType<PostRedirectViewModel>(postRedirectView.Model);
|
||||
Assert.Equal(actionName, postRedirectViewModel.AspAction);
|
||||
var redirectedPSBT = postRedirectViewModel.FormParameters.Single(p => p.Key == "psbt" || p.Key == "SigningContext.PSBT").Value?.FirstOrDefault();
|
||||
return redirectedPSBT;
|
||||
s.Driver.FindElement(By.Name("Outputs[0].DestinationAddress")).SendKeys(address);
|
||||
s.Driver.FindElement(By.ClassName("crypto-balance-link")).Click();
|
||||
s.Driver.FindElement(By.Id("SignTransaction")).Click();
|
||||
}
|
||||
|
||||
private static string ExtractPSBT(SeleniumTester s)
|
||||
{
|
||||
var pageSource = s.Driver.PageSource;
|
||||
var start = pageSource.IndexOf("id=\"psbt-base64\">");
|
||||
start += "id=\"psbt-base64\">".Length;
|
||||
var end = pageSource.IndexOf("<", start);
|
||||
return pageSource[start..end];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Models;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Support.Extensions;
|
||||
using OpenQA.Selenium.Support.UI;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Tooling
|
||||
|
||||
This README describe some useful tooling that you may need during development and testing.
|
||||
To learn how to get started with your local development environment, read [our documentation](https://docs.btcpayserver.org/LocalDevelopment/).
|
||||
To learn how to get started with your local development environment, read [our documentation](https://docs.btcpayserver.org/Development/LocalDevelopment/).
|
||||
|
||||
## How to manually test payments
|
||||
|
||||
|
@ -1,12 +1,10 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.BIP78.Sender;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Lightning.CLightning;
|
||||
using BTCPayServer.Views.Manage;
|
||||
@ -15,6 +13,7 @@ using BTCPayServer.Views.Stores;
|
||||
using BTCPayServer.Views.Wallets;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using NBitcoin;
|
||||
using NBitcoin.RPC;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Chrome;
|
||||
using OpenQA.Selenium.Support.UI;
|
||||
@ -144,8 +143,15 @@ namespace BTCPayServer.Tests
|
||||
Driver.FindElement(By.Id("IsAdmin")).Click();
|
||||
Driver.FindElement(By.Id("RegisterButton")).Click();
|
||||
Driver.AssertNoError();
|
||||
CreatedUser = usr;
|
||||
return usr;
|
||||
}
|
||||
string CreatedUser;
|
||||
|
||||
public TestAccount AsTestAccount()
|
||||
{
|
||||
return new TestAccount(Server) { RegisterDetails = new Models.AccountViewModels.RegisterViewModel() { Password = "123456", Email = CreatedUser } };
|
||||
}
|
||||
|
||||
public (string storeName, string storeId) CreateNewStore(bool keepId = true)
|
||||
{
|
||||
@ -376,6 +382,8 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
GoToUrl($"/stores/{storeId}/");
|
||||
StoreId = storeId;
|
||||
if (WalletId != null)
|
||||
WalletId = new WalletId(storeId, WalletId.CryptoCode);
|
||||
}
|
||||
|
||||
Driver.FindElement(By.Id("StoreNav-StoreSettings")).Click();
|
||||
@ -400,9 +408,9 @@ namespace BTCPayServer.Tests
|
||||
public void GoToWalletSettings(string cryptoCode = "BTC")
|
||||
{
|
||||
Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
|
||||
if (Driver.PageSource.Contains("id=\"SectionNav-Settings\""))
|
||||
if (Driver.PageSource.Contains("id=\"WalletNav-Settings\""))
|
||||
{
|
||||
Driver.FindElement(By.Id("SectionNav-Settings")).Click();
|
||||
Driver.FindElement(By.Id("WalletNav-Settings")).Click();
|
||||
}
|
||||
}
|
||||
|
||||
@ -422,7 +430,7 @@ namespace BTCPayServer.Tests
|
||||
Driver.FindElement(By.Id($"StoreSelectorMenuItem-{storeId}")).Click();
|
||||
}
|
||||
|
||||
public void GoToInvoiceCheckout(string? invoiceId = null)
|
||||
public void GoToInvoiceCheckout(string invoiceId = null)
|
||||
{
|
||||
invoiceId ??= InvoiceId;
|
||||
Driver.FindElement(By.Id("StoreNav-Invoices")).Click();
|
||||
@ -450,11 +458,11 @@ namespace BTCPayServer.Tests
|
||||
|
||||
public void GoToProfile(ManageNavPages navPages = ManageNavPages.Index)
|
||||
{
|
||||
Driver.FindElement(By.Id("Nav-Account")).Click();
|
||||
Driver.FindElement(By.Id("Nav-ManageAccount")).Click();
|
||||
Driver.WaitForAndClick(By.Id("Nav-Account"));
|
||||
Driver.WaitForAndClick(By.Id("Nav-ManageAccount"));
|
||||
if (navPages != ManageNavPages.Index)
|
||||
{
|
||||
Driver.FindElement(By.Id($"SectionNav-{navPages.ToString()}")).Click();
|
||||
Driver.WaitForAndClick(By.Id($"SectionNav-{navPages.ToString()}"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -507,7 +515,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
string InvoiceId;
|
||||
|
||||
public async Task FundStoreWallet(WalletId walletId = null, int coins = 1, decimal denomination = 1m)
|
||||
public async Task<string> FundStoreWallet(WalletId walletId = null, int coins = 1, decimal denomination = 1m)
|
||||
{
|
||||
walletId ??= WalletId;
|
||||
GoToWallet(walletId, WalletsNavPages.Receive);
|
||||
@ -516,8 +524,21 @@ namespace BTCPayServer.Tests
|
||||
var address = BitcoinAddress.Create(addressStr, ((BTCPayNetwork)Server.NetworkProvider.GetNetwork(walletId.CryptoCode)).NBitcoinNetwork);
|
||||
for (var i = 0; i < coins; i++)
|
||||
{
|
||||
await Server.ExplorerNode.SendToAddressAsync(address, Money.Coins(denomination));
|
||||
bool mined = false;
|
||||
retry:
|
||||
try
|
||||
{
|
||||
await Server.ExplorerNode.SendToAddressAsync(address, Money.Coins(denomination));
|
||||
}
|
||||
catch (RPCException) when (!mined)
|
||||
{
|
||||
mined = true;
|
||||
await Server.ExplorerNode.GenerateAsync(1);
|
||||
goto retry;
|
||||
}
|
||||
}
|
||||
Driver.Navigate().Refresh();
|
||||
return addressStr;
|
||||
}
|
||||
|
||||
private void CheckForJSErrors()
|
||||
@ -547,9 +568,14 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
walletId ??= WalletId;
|
||||
Driver.Navigate().GoToUrl(new Uri(ServerUri, $"wallets/{walletId}"));
|
||||
if (navPages != WalletsNavPages.Transactions)
|
||||
if (navPages == WalletsNavPages.PSBT)
|
||||
{
|
||||
Driver.FindElement(By.Id($"SectionNav-{navPages}")).Click();
|
||||
Driver.FindElement(By.Id("WalletNav-Send")).Click();
|
||||
Driver.FindElement(By.Id("PSBT")).Click();
|
||||
}
|
||||
else if (navPages != WalletsNavPages.Transactions)
|
||||
{
|
||||
Driver.FindElement(By.Id($"WalletNav-{navPages}")).Click();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -386,6 +386,10 @@ namespace BTCPayServer.Tests
|
||||
await s.StartAsync();
|
||||
s.RegisterNewUser(true);
|
||||
s.CreateNewStore();
|
||||
s.GoToInvoices();
|
||||
s.Driver.FindElement(By.Id("CreateNewInvoice")).Click();
|
||||
// Should give us an error message if we try to create an invoice before adding a wallet
|
||||
Assert.Contains("To create an invoice, you need to", s.Driver.PageSource);
|
||||
s.AddDerivationScheme();
|
||||
s.GoToInvoices();
|
||||
s.CreateInvoice();
|
||||
@ -449,6 +453,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
s.GoToStore();
|
||||
Assert.Contains(storeName, s.Driver.PageSource);
|
||||
Assert.DoesNotContain("id=\"Dashboard\"", s.Driver.PageSource);
|
||||
|
||||
// verify steps for wallet setup are displayed correctly
|
||||
s.GoToStore(StoreNavPages.Dashboard);
|
||||
@ -462,10 +467,11 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.AssertNoError();
|
||||
|
||||
s.GoToStore(StoreNavPages.Dashboard);
|
||||
Assert.True(s.Driver.FindElement(By.Id("SetupGuide-WalletDone")).Displayed);
|
||||
Assert.DoesNotContain("id=\"SetupGuide\"", s.Driver.PageSource);
|
||||
Assert.True(s.Driver.FindElement(By.Id("Dashboard")).Displayed);
|
||||
|
||||
// setup offchain wallet
|
||||
s.Driver.FindElement(By.Id("SetupGuide-Lightning")).Click();
|
||||
s.Driver.FindElement(By.Id("StoreNav-LightningBTC")).Click();
|
||||
s.AddLightningNode();
|
||||
s.Driver.AssertNoError();
|
||||
var successAlert = s.FindAlertMessage();
|
||||
@ -473,9 +479,6 @@ namespace BTCPayServer.Tests
|
||||
|
||||
s.ClickOnAllSectionLinks();
|
||||
|
||||
s.GoToStore(StoreNavPages.Dashboard);
|
||||
Assert.True(s.Driver.FindElement(By.Id("SetupGuide-LightningDone")).Displayed);
|
||||
|
||||
s.GoToInvoices();
|
||||
Assert.Contains("There are no invoices matching your criteria.", s.Driver.PageSource);
|
||||
var invoiceId = s.CreateInvoice();
|
||||
@ -670,7 +673,8 @@ namespace BTCPayServer.Tests
|
||||
s.LogIn(userId);
|
||||
// Make sure after login, we are not redirected to the PoS
|
||||
Assert.DoesNotContain("Tea shop", s.Driver.PageSource);
|
||||
// We are only if explicitely going to /
|
||||
|
||||
// We are only if explicitly going to /
|
||||
s.GoToUrl("/");
|
||||
Assert.Contains("Tea shop", s.Driver.PageSource);
|
||||
s.Driver.Navigate().Back();
|
||||
@ -692,7 +696,8 @@ namespace BTCPayServer.Tests
|
||||
s.LogIn(userId);
|
||||
// Make sure after login, we are not redirected to the PoS
|
||||
Assert.DoesNotContain("Tea shop", s.Driver.PageSource);
|
||||
// We are only if explicitely going to /
|
||||
|
||||
// We are only if explicitly going to /
|
||||
s.GoToUrl("/");
|
||||
Assert.Contains("Tea shop", s.Driver.PageSource);
|
||||
}
|
||||
@ -745,7 +750,12 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("CreatePaymentRequest")).Click();
|
||||
s.Driver.FindElement(By.Id("Title")).SendKeys("Pay123");
|
||||
s.Driver.FindElement(By.Id("Amount")).SendKeys("700");
|
||||
s.Driver.FindElement(By.Id("Currency")).SendKeys("BTC");
|
||||
|
||||
var currencyInput = s.Driver.FindElement(By.Id("Currency"));
|
||||
Assert.Equal("USD", currencyInput.GetAttribute("value"));
|
||||
currencyInput.Clear();
|
||||
currencyInput.SendKeys("BTC");
|
||||
|
||||
s.Driver.FindElement(By.Id("SaveButton")).Click();
|
||||
s.Driver.FindElement(By.Id("ViewPaymentRequest")).Click();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
|
||||
@ -998,22 +1008,22 @@ namespace BTCPayServer.Tests
|
||||
|
||||
//let's test quickly the receive wallet page
|
||||
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
|
||||
s.Driver.FindElement(By.Id("SectionNav-Send")).Click();
|
||||
s.Driver.FindElement(By.Id("WalletNav-Send")).Click();
|
||||
s.Driver.FindElement(By.Id("SignTransaction")).Click();
|
||||
|
||||
//you cannot use the Sign with NBX option without saving private keys when generating the wallet.
|
||||
Assert.DoesNotContain("nbx-seed", s.Driver.PageSource);
|
||||
|
||||
s.Driver.FindElement(By.Id("SectionNav-Receive")).Click();
|
||||
s.Driver.FindElement(By.Id("WalletNav-Receive")).Click();
|
||||
//generate a receiving address
|
||||
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
|
||||
Assert.True(s.Driver.FindElement(By.ClassName("qr-container")).Displayed);
|
||||
Assert.True(s.Driver.FindElement(By.CssSelector("#address-tab .qr-container")).Displayed);
|
||||
var receiveAddr = s.Driver.FindElement(By.Id("address")).GetAttribute("value");
|
||||
//unreserve
|
||||
s.Driver.FindElement(By.CssSelector("button[value=unreserve-current-address]")).Click();
|
||||
//generate it again, should be the same one as before as nothing got used in the meantime
|
||||
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
|
||||
Assert.True(s.Driver.FindElement(By.ClassName("qr-container")).Displayed);
|
||||
Assert.True(s.Driver.FindElement(By.CssSelector("#address-tab .qr-container")).Displayed);
|
||||
Assert.Equal(receiveAddr, s.Driver.FindElement(By.Id("address")).GetAttribute("value"));
|
||||
|
||||
//send money to addr and ensure it changed
|
||||
@ -1071,20 +1081,19 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("m/84'/1'/0'",
|
||||
s.Driver.FindElement(By.Id("AccountKeys_0__AccountKeyPath")).GetAttribute("value"));
|
||||
|
||||
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
|
||||
|
||||
// Make sure we can rescan, because we are admin!
|
||||
s.Driver.FindElement(By.Id("SectionNav-Rescan")).Click();
|
||||
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
|
||||
s.Driver.FindElement(By.Id("Rescan")).Click();
|
||||
Assert.Contains("The batch size make sure", s.Driver.PageSource);
|
||||
|
||||
// Check the tx sent earlier arrived
|
||||
s.Driver.FindElement(By.Id("SectionNav-Transactions")).Click();
|
||||
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
|
||||
|
||||
var walletTransactionLink = s.Driver.Url;
|
||||
Assert.Contains(tx.ToString(), s.Driver.PageSource);
|
||||
|
||||
// Send to bob
|
||||
s.Driver.FindElement(By.Id("SectionNav-Send")).Click();
|
||||
s.Driver.FindElement(By.Id("WalletNav-Send")).Click();
|
||||
var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest);
|
||||
SetTransactionOutput(s, 0, bob, 1);
|
||||
s.Driver.FindElement(By.Id("SignTransaction")).Click();
|
||||
@ -1096,7 +1105,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(walletTransactionLink, s.Driver.Url);
|
||||
|
||||
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
|
||||
s.Driver.FindElement(By.Id("SectionNav-Send")).Click();
|
||||
s.Driver.FindElement(By.Id("WalletNav-Send")).Click();
|
||||
|
||||
var jack = new Key().PubKey.Hash.GetAddress(Network.RegTest);
|
||||
SetTransactionOutput(s, 0, jack, 0.01m);
|
||||
@ -1113,7 +1122,7 @@ namespace BTCPayServer.Tests
|
||||
bip21 += "&label=Solid Snake&message=Snake? Snake? SNAAAAKE!";
|
||||
var parsedBip21 = new BitcoinUrlBuilder(bip21, Network.RegTest);
|
||||
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
|
||||
s.Driver.FindElement(By.Id("SectionNav-Send")).Click();
|
||||
s.Driver.FindElement(By.Id("WalletNav-Send")).Click();
|
||||
s.Driver.FindElement(By.Id("bip21parse")).Click();
|
||||
s.Driver.SwitchTo().Alert().SendKeys(bip21);
|
||||
s.Driver.SwitchTo().Alert().Accept();
|
||||
@ -1464,7 +1473,9 @@ namespace BTCPayServer.Tests
|
||||
s.GoToLightningSettings();
|
||||
// LNURL is true by default
|
||||
Assert.True(s.Driver.FindElement(By.Id("LNURLEnabled")).Selected);
|
||||
|
||||
s.Driver.SetCheckbox(By.Name("LUD12Enabled"), true);
|
||||
s.Driver.FindElement(By.Id("save")).Click();
|
||||
|
||||
// Topup Invoice test
|
||||
var i = s.CreateInvoice(storeId, null, cryptoCode);
|
||||
s.GoToInvoiceCheckout(i);
|
||||
@ -1476,13 +1487,13 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(1m, fetchedReuqest.MinSendable.ToDecimal(LightMoneyUnit.Satoshi));
|
||||
Assert.NotEqual(1m, fetchedReuqest.MaxSendable.ToDecimal(LightMoneyUnit.Satoshi));
|
||||
var lnurlResponse = await fetchedReuqest.SendRequest(new LightMoney(0.000001m, LightMoneyUnit.BTC),
|
||||
network, new HttpClient());
|
||||
network, new HttpClient(), comment: "lol");
|
||||
|
||||
Assert.Equal(new LightMoney(0.000001m, LightMoneyUnit.BTC),
|
||||
lnurlResponse.GetPaymentRequest(network).MinimumAmount);
|
||||
|
||||
var lnurlResponse2 = await fetchedReuqest.SendRequest(new LightMoney(0.000002m, LightMoneyUnit.BTC),
|
||||
network, new HttpClient());
|
||||
network, new HttpClient(), comment: "lol2");
|
||||
Assert.Equal(new LightMoney(0.000002m, LightMoneyUnit.BTC), lnurlResponse2.GetPaymentRequest(network).MinimumAmount);
|
||||
await Assert.ThrowsAnyAsync<LightningRPCException>(async () =>
|
||||
{
|
||||
@ -1496,7 +1507,11 @@ namespace BTCPayServer.Tests
|
||||
var inv = await s.Server.PayTester.InvoiceRepository.GetInvoice(i);
|
||||
Assert.Equal(InvoiceStatusLegacy.Complete, inv.Status);
|
||||
});
|
||||
|
||||
var greenfield = await s.AsTestAccount().CreateClient();
|
||||
var paymentMethods = await greenfield.GetInvoicePaymentMethods(s.StoreId, i);
|
||||
Assert.Single(paymentMethods, p => {
|
||||
return p.AdditionalData["providedComment"].Value<string>() == "lol2";
|
||||
});
|
||||
// Standard invoice test
|
||||
s.GoToStore(storeId);
|
||||
s.GoToLightningSettings();
|
||||
@ -1604,6 +1619,12 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("Name")).SendKeys("PP1");
|
||||
s.Driver.FindElement(By.Id("Amount")).Clear();
|
||||
s.Driver.FindElement(By.Id("Amount")).SendKeys("0.0000001");
|
||||
|
||||
var currencyInput = s.Driver.FindElement(By.Id("Currency"));
|
||||
Assert.Equal("USD", currencyInput.GetAttribute("value"));
|
||||
currencyInput.Clear();
|
||||
currencyInput.SendKeys("BTC");
|
||||
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
s.Driver.FindElement(By.LinkText("View")).Click();
|
||||
s.Driver.FindElement(By.Id("Destination")).SendKeys(lnurl);
|
||||
|
@ -5,6 +5,7 @@ using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.BIP78.Sender;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
@ -236,6 +237,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
UserId = account.RegisteredUserId;
|
||||
Email = RegisterDetails.Email;
|
||||
IsAdmin = account.RegisteredAdmin;
|
||||
}
|
||||
|
||||
@ -252,6 +254,12 @@ namespace BTCPayServer.Tests
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public string Email
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public string StoreId
|
||||
{
|
||||
|
@ -74,6 +74,8 @@ namespace BTCPayServer.Tests
|
||||
[Fact]
|
||||
public void CanQueryDirectProviders()
|
||||
{
|
||||
// TODO: Check once in a while whether or not they are working again
|
||||
string[] brokenShitcoinCasinos = { "okex" };
|
||||
var factory = FastTests.CreateBTCPayRateFactory();
|
||||
var directlySupported = factory.GetSupportedExchanges().Where(s => s.Source == RateSource.Direct)
|
||||
.Select(s => s.Id).ToHashSet();
|
||||
@ -86,47 +88,48 @@ namespace BTCPayServer.Tests
|
||||
Fetcher: (BackgroundFetcherRateProvider)p.Value))
|
||||
.ToList())
|
||||
{
|
||||
TestLogs.LogInformation($"Testing {result.ExpectedName}");
|
||||
var name = result.ExpectedName;
|
||||
if (brokenShitcoinCasinos.Contains(name))
|
||||
{
|
||||
TestLogs.LogInformation($"Skipping {name}");
|
||||
continue;
|
||||
}
|
||||
|
||||
TestLogs.LogInformation($"Testing {name}");
|
||||
|
||||
result.Fetcher.InvalidateCache();
|
||||
var exchangeRates = new ExchangeRates(result.ExpectedName, result.ResultAsync.Result);
|
||||
var exchangeRates = new ExchangeRates(name, result.ResultAsync.Result);
|
||||
result.Fetcher.InvalidateCache();
|
||||
Assert.NotNull(exchangeRates);
|
||||
Assert.NotEmpty(exchangeRates);
|
||||
Assert.NotEmpty(exchangeRates.ByExchange[result.ExpectedName]);
|
||||
if (result.ExpectedName == "bitbank" || result.ExpectedName == "bitflyer")
|
||||
Assert.NotEmpty(exchangeRates.ByExchange[name]);
|
||||
if (name == "bitbank" || name == "bitflyer")
|
||||
{
|
||||
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
|
||||
Assert.Contains(exchangeRates.ByExchange[name],
|
||||
e => e.CurrencyPair == new CurrencyPair("BTC", "JPY") &&
|
||||
e.BidAsk.Bid > 100m); // 1BTC will always be more than 100JPY
|
||||
}
|
||||
else if (result.ExpectedName == "polispay")
|
||||
else if (name == "argoneum")
|
||||
{
|
||||
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
|
||||
e => e.CurrencyPair == new CurrencyPair("BTC", "POLIS") &&
|
||||
e.BidAsk.Bid > 1.0m); // 1BTC will always be more than 1 POLIS
|
||||
}
|
||||
else if (result.ExpectedName == "argoneum")
|
||||
{
|
||||
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
|
||||
Assert.Contains(exchangeRates.ByExchange[name],
|
||||
e => e.CurrencyPair == new CurrencyPair("BTC", "AGM") &&
|
||||
e.BidAsk.Bid > 1.0m); // 1 BTC will always be more than 1 AGM
|
||||
}
|
||||
else if (result.ExpectedName == "ripio")
|
||||
else if (name == "ripio")
|
||||
{
|
||||
// Ripio keeps changing their pair, so anything is fine...
|
||||
Assert.NotEmpty(exchangeRates.ByExchange[result.ExpectedName]);
|
||||
Assert.NotEmpty(exchangeRates.ByExchange[name]);
|
||||
}
|
||||
else if (result.ExpectedName == "cryptomarket")
|
||||
else if (name == "cryptomarket")
|
||||
{
|
||||
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
|
||||
Assert.Contains(exchangeRates.ByExchange[name],
|
||||
e => e.CurrencyPair == new CurrencyPair("BTC", "CLP") &&
|
||||
e.BidAsk.Bid > 1.0m); // 1 BTC will always be more than 1 CLP
|
||||
}
|
||||
else
|
||||
{
|
||||
// This check if the currency pair is using right currency pair
|
||||
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
|
||||
Assert.Contains(exchangeRates.ByExchange[name],
|
||||
e => (e.CurrencyPair == new CurrencyPair("BTC", "USD") ||
|
||||
e.CurrencyPair == new CurrencyPair("BTC", "EUR") ||
|
||||
e.CurrencyPair == new CurrencyPair("BTC", "USDT") ||
|
||||
@ -139,8 +142,8 @@ namespace BTCPayServer.Tests
|
||||
// we need to modify the AvailableRateProvider
|
||||
|
||||
// There are some exception we stopped supporting but don't want to break backward compat
|
||||
if (result.ExpectedName != "coinaverage" && result.ExpectedName != "gdax")
|
||||
Assert.Contains(result.ExpectedName, directlySupported);
|
||||
if (name != "coinaverage" && name != "gdax")
|
||||
Assert.Contains(name, directlySupported);
|
||||
}
|
||||
|
||||
// Kraken emit one request only after first GetRates
|
||||
@ -251,6 +254,7 @@ namespace BTCPayServer.Tests
|
||||
[Fact]
|
||||
public void CanGetRateCryptoCurrenciesByDefault()
|
||||
{
|
||||
string[] brokenShitcoins = { "BTX_USD", "CHC_USD" };
|
||||
var provider = new BTCPayNetworkProvider(ChainName.Mainnet);
|
||||
var factory = FastTests.CreateBTCPayRateFactory();
|
||||
var fetcher = new RateFetcher(factory);
|
||||
@ -261,11 +265,11 @@ namespace BTCPayServer.Tests
|
||||
|
||||
var rules = new StoreBlob().GetDefaultRateRules(provider);
|
||||
var result = fetcher.FetchRates(pairs, rules, default);
|
||||
foreach (var value in result)
|
||||
foreach ((CurrencyPair key, Task<RateResult> value) in result)
|
||||
{
|
||||
var rateResult = value.Value.GetAwaiter().GetResult();
|
||||
TestLogs.LogInformation($"Testing {value.Key.ToString()}");
|
||||
if (value.Key.ToString() == "BTX_USD") // Broken shitcoin
|
||||
var rateResult = value.GetAwaiter().GetResult();
|
||||
TestLogs.LogInformation($"Testing {key}");
|
||||
if (brokenShitcoins.Contains(key.ToString()))
|
||||
continue;
|
||||
Assert.True(rateResult.BidAsk != null, $"Impossible to get the rate {rateResult.EvaluatedRule}");
|
||||
}
|
||||
|
@ -11,7 +11,9 @@ using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
@ -140,7 +142,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
var sresp = Assert
|
||||
.IsType<JsonResult>(await tester.PayTester.GetController<UIHomeController>(acc.UserId, acc.StoreId)
|
||||
.Swagger()).Value.ToJson();
|
||||
.Swagger(tester.PayTester.GetService<IEnumerable<ISwaggerProvider>>())).Value.ToJson();
|
||||
JObject swagger = JObject.Parse(sresp);
|
||||
var schema = JSchema.Parse(File.ReadAllText(TestUtils.GetTestDataFullPath("OpenAPI-Specification-schema.json")));
|
||||
IList<ValidationError> errors;
|
||||
@ -189,7 +191,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
var sresp = Assert
|
||||
.IsType<JsonResult>(await tester.PayTester.GetController<UIHomeController>(acc.UserId, acc.StoreId)
|
||||
.Swagger()).Value.ToJson();
|
||||
.Swagger(tester.PayTester.GetService<IEnumerable<ISwaggerProvider>>())).Value.ToJson();
|
||||
|
||||
JObject json = JObject.Parse(sresp);
|
||||
|
||||
@ -925,7 +927,7 @@ namespace BTCPayServer.Tests
|
||||
private void AssertSearchInvoice(TestAccount acc, bool expected, string invoiceId, string filter, string storeId = null)
|
||||
{
|
||||
var result =
|
||||
(InvoicesModel)((ViewResult)acc.GetController<UIInvoiceController>()
|
||||
(InvoicesModel)((ViewResult)acc.GetController<UIInvoiceController>(storeId is not null)
|
||||
.ListInvoices(new InvoicesModel { SearchTerm = filter, StoreId = storeId }).Result).Model;
|
||||
Assert.Equal(expected, result.Invoices.Any(i => i.InvoiceId == invoiceId));
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
version: "3"
|
||||
version: "3"
|
||||
|
||||
# Run `docker-compose up dev` for bootstrapping your development environment
|
||||
# Doing so will expose NBXplorer, Bitcoind RPC and postgres port to the host so that tests can Run,
|
||||
@ -35,7 +35,7 @@ services:
|
||||
links:
|
||||
- dev
|
||||
- selenium
|
||||
extra_hosts:
|
||||
extra_hosts:
|
||||
- "tests:127.0.0.1"
|
||||
volumes:
|
||||
- "sshd_datadir:/root/.ssh"
|
||||
@ -43,7 +43,7 @@ services:
|
||||
- "merchant_lightningd_datadir:/etc/merchant_lightningd_datadir"
|
||||
|
||||
# The dev container is not actually used, it is just handy to run `docker-compose up dev` to start all services
|
||||
dev:
|
||||
dev:
|
||||
image: alpine:3.7
|
||||
command: [ "/bin/sh", "-c", "trap : TERM INT; while :; do echo Ready to code and debug like a rockstar!!!; sleep 2073600; done & wait" ]
|
||||
links:
|
||||
@ -69,7 +69,7 @@ services:
|
||||
volumes:
|
||||
- "sshd_datadir:/root/.ssh"
|
||||
|
||||
devlnd:
|
||||
devlnd:
|
||||
image: btcpayserver/bitcoin:22.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
@ -89,11 +89,11 @@ services:
|
||||
expose:
|
||||
- "4444"
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.2.18
|
||||
image: nicolasdorier/nbxplorer:2.3.14
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
expose:
|
||||
expose:
|
||||
- "32838"
|
||||
environment:
|
||||
NBXPLORER_NETWORK: regtest
|
||||
@ -114,6 +114,7 @@ services:
|
||||
NBXPLORER_MINGAPSIZE: 5
|
||||
NBXPLORER_MAXGAPSIZE: 10
|
||||
NBXPLORER_VERBOSE: 1
|
||||
NBXPLORER_POSTGRES: User ID=postgres;Include Error Detail=true;Host=postgres;Port=5432;Database=nbxplorer
|
||||
NBXPLORER_NOAUTH: 1
|
||||
NBXPLORER_EXPOSERPC: 1
|
||||
links:
|
||||
@ -140,7 +141,7 @@ services:
|
||||
zmqpubrawtx=tcp://0.0.0.0:28333
|
||||
deprecatedrpc=signrawtransaction
|
||||
fallbackfee=0.0002
|
||||
ports:
|
||||
ports:
|
||||
- "43782:43782"
|
||||
- "39388:39388"
|
||||
expose:
|
||||
@ -155,7 +156,7 @@ services:
|
||||
image: btcpayserver/lightning:v0.10.1-1-dev
|
||||
stop_signal: SIGKILL
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
LIGHTNINGD_CHAIN: "btc"
|
||||
LIGHTNINGD_NETWORK: "regtest"
|
||||
@ -203,7 +204,7 @@ services:
|
||||
merchant_lightningd:
|
||||
image: btcpayserver/lightning:v0.10.1-1-dev
|
||||
stop_signal: SIGKILL
|
||||
environment:
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
LIGHTNINGD_CHAIN: "btc"
|
||||
LIGHTNINGD_NETWORK: "regtest"
|
||||
@ -336,24 +337,26 @@ services:
|
||||
volumes:
|
||||
- "./monero_wallet:/wallet"
|
||||
depends_on:
|
||||
- monerod
|
||||
- monerod
|
||||
litecoind:
|
||||
restart: unless-stopped
|
||||
image: nicolasdorier/docker-litecoin:0.16.3
|
||||
image: btcpayserver/litecoin:0.18.1
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_EXTRA_ARGS: |-
|
||||
rpcuser=ceiwHEbqWI83
|
||||
rpcpassword=DwubwWsoo3
|
||||
regtest=1
|
||||
rpcport=43782
|
||||
rpcbind=0.0.0.0:43782
|
||||
port=39388
|
||||
whitelist=0.0.0.0/0
|
||||
ports:
|
||||
ports:
|
||||
- "43783:43782"
|
||||
expose:
|
||||
- "43782" # RPC
|
||||
- "39388" # P2P
|
||||
|
||||
|
||||
elementsd-liquid:
|
||||
restart: always
|
||||
container_name: btcpayserver_elementsd_liquid
|
||||
|
@ -86,7 +86,7 @@ services:
|
||||
expose:
|
||||
- "4444"
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.2.18
|
||||
image: nicolasdorier/nbxplorer:2.3.14
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
@ -103,6 +103,7 @@ services:
|
||||
NBXPLORER_MINGAPSIZE: 5
|
||||
NBXPLORER_MAXGAPSIZE: 10
|
||||
NBXPLORER_VERBOSE: 1
|
||||
NBXPLORER_POSTGRES: User ID=postgres;Include Error Detail=true;Host=postgres;Port=5432;Database=nbxplorer
|
||||
NBXPLORER_EXPOSERPC: 1
|
||||
NBXPLORER_NOAUTH: 1
|
||||
links:
|
||||
|
@ -48,11 +48,12 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
|
||||
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.3.0" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.3.4" />
|
||||
<PackageReference Include="BuildBundlerMinifier" Version="3.2.449" />
|
||||
<PackageReference Include="BundlerMinifier.Core" Version="3.2.435" />
|
||||
<PackageReference Include="BundlerMinifier.TagHelpers" Version="3.2.435" />
|
||||
<PackageReference Include="CsvHelper" Version="15.0.5" />
|
||||
<PackageReference Include="Dapper" Version="2.0.123" />
|
||||
<PackageReference Include="Fido2" Version="2.0.1" />
|
||||
<PackageReference Include="Fido2.AspNet" Version="2.0.1" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="5.0.372" />
|
||||
@ -231,10 +232,5 @@
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<_ContentIncludedByDefault Remove="Views\Authorization\Authorize.cshtml" />
|
||||
<_ContentIncludedByDefault Remove="Views\Components\NotificationsDropdown\Default.cshtml" />
|
||||
</ItemGroup>
|
||||
|
||||
<ProjectExtensions><VisualStudio><UserProperties wwwroot_4swagger_4v1_4swagger_1template_1invoices_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1misc_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1pull-payments_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1serverinfo_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1stores_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1users_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1webhooks_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" /></VisualStudio></ProjectExtensions>
|
||||
</Project>
|
||||
|
34
BTCPayServer/Components/AppSales/AppSales.cs
Normal file
34
BTCPayServer/Components/AppSales/AppSales.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Components.AppSales;
|
||||
|
||||
public class AppSales : ViewComponent
|
||||
{
|
||||
private readonly AppService _appService;
|
||||
private readonly StoreRepository _storeRepo;
|
||||
|
||||
public AppSales(AppService appService, StoreRepository storeRepo)
|
||||
{
|
||||
_appService = appService;
|
||||
_storeRepo = storeRepo;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(AppData app)
|
||||
{
|
||||
var stats = await _appService.GetSalesStats(app);
|
||||
var vm = new AppSalesViewModel
|
||||
{
|
||||
App = app,
|
||||
SalesCount = stats.SalesCount,
|
||||
Series = stats.Series
|
||||
};
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
}
|
13
BTCPayServer/Components/AppSales/AppSalesViewModel.cs
Normal file
13
BTCPayServer/Components/AppSales/AppSalesViewModel.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Apps;
|
||||
|
||||
namespace BTCPayServer.Components.AppSales;
|
||||
|
||||
public class AppSalesViewModel
|
||||
{
|
||||
public AppData App { get; set; }
|
||||
public int SalesCount { get; set; }
|
||||
public IEnumerable<SalesStatsItem> Series { get; set; }
|
||||
}
|
30
BTCPayServer/Components/AppSales/Default.cshtml
Normal file
30
BTCPayServer/Components/AppSales/Default.cshtml
Normal file
@ -0,0 +1,30 @@
|
||||
@model BTCPayServer.Components.AppSales.AppSalesViewModel
|
||||
|
||||
@{
|
||||
var action = $"Update{Model.App.AppType}";
|
||||
}
|
||||
|
||||
<div id="AppSales-@Model.App.Id" class="widget app-sales">
|
||||
<header class="mb-3">
|
||||
<h3>@Model.App.Name Contributions</h3>
|
||||
<a asp-controller="UIApps" asp-action="@action" asp-route-appId="@Model.App.Id">Manage</a>
|
||||
</header>
|
||||
<p>@Model.SalesCount Total Contributions</p>
|
||||
<div class="ct-chart ct-major-octave"></div>
|
||||
<script>
|
||||
(function () {
|
||||
const id = 'AppSales-@Model.App.Id';
|
||||
const labels = @Safe.Json(Model.Series.Select(i => i.Label));
|
||||
const series = @Safe.Json(Model.Series.Select(i => i.SalesCount));
|
||||
const min = Math.min(...series);
|
||||
const max = Math.max(...series);
|
||||
const low = min === max ? 0 : Math.max(min - ((max - min) / 5), 0);
|
||||
new Chartist.Bar(`#${id} .ct-chart`, {
|
||||
labels,
|
||||
series: [series]
|
||||
}, {
|
||||
low,
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</div>
|
32
BTCPayServer/Components/AppTopItems/AppTopItems.cs
Normal file
32
BTCPayServer/Components/AppTopItems/AppTopItems.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Components.AppTopItems;
|
||||
|
||||
public class AppTopItems : ViewComponent
|
||||
{
|
||||
private readonly AppService _appService;
|
||||
private readonly StoreRepository _storeRepo;
|
||||
|
||||
public AppTopItems(AppService appService, StoreRepository storeRepo)
|
||||
{
|
||||
_appService = appService;
|
||||
_storeRepo = storeRepo;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(AppData app)
|
||||
{
|
||||
var entries = await _appService.GetPerkStats(app);
|
||||
var vm = new AppTopItemsViewModel
|
||||
{
|
||||
App = app,
|
||||
Entries = entries
|
||||
};
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
}
|
11
BTCPayServer/Components/AppTopItems/AppTopItemsViewModel.cs
Normal file
11
BTCPayServer/Components/AppTopItems/AppTopItemsViewModel.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Apps;
|
||||
|
||||
namespace BTCPayServer.Components.AppTopItems;
|
||||
|
||||
public class AppTopItemsViewModel
|
||||
{
|
||||
public AppData App { get; set; }
|
||||
public IEnumerable<ItemStats> Entries { get; set; }
|
||||
}
|
33
BTCPayServer/Components/AppTopItems/Default.cshtml
Normal file
33
BTCPayServer/Components/AppTopItems/Default.cshtml
Normal file
@ -0,0 +1,33 @@
|
||||
@model BTCPayServer.Components.AppTopItems.AppTopItemsViewModel
|
||||
|
||||
@{
|
||||
var action = $"Update{Model.App.AppType}";
|
||||
}
|
||||
|
||||
<div class="widget app-top-items">
|
||||
<header class="mb-3">
|
||||
<h3>Top Perks</h3>
|
||||
<a asp-controller="UIApps" asp-action="@action" asp-route-appId="@Model.App.Id">View All</a>
|
||||
</header>
|
||||
@if (Model.Entries.Any())
|
||||
{
|
||||
<div class="app-items">
|
||||
@foreach (var entry in Model.Entries)
|
||||
{
|
||||
<div class="app-item">
|
||||
<span class="app-item-name">@entry.Title</span>
|
||||
<span class="app-item-value">
|
||||
@entry.SalesCount sale@(entry.SalesCount == 1 ? "" : "s"),
|
||||
@entry.TotalFormatted total
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-secondary mt-3">
|
||||
No contributions have been made yet.
|
||||
</p>
|
||||
}
|
||||
</div>
|
@ -60,14 +60,14 @@
|
||||
{
|
||||
<a asp-area="" asp-controller="UIWallets" asp-action="WalletTransactions" asp-route-walletId="@scheme.WalletId" class="nav-link js-scroll-trigger @ViewData.IsActiveCategory(typeof(WalletsNavPages), scheme.WalletId.ToString()) @ViewData.IsActivePage(StoreNavPages.OnchainSettings)" id="@($"StoreNav-Wallet{scheme.Crypto}")">
|
||||
<span class="me-2 btcpay-status btcpay-status--@(scheme.Enabled ? "enabled" : "pending")"></span>
|
||||
<span>@(Model.AltcoinsBuild ? $"{scheme.Crypto} " : "")Wallet</span>
|
||||
<span>@(Model.AltcoinsBuild ? $"{scheme.Crypto} Wallet" : "Bitcoin")</span>
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a asp-area="" asp-controller="UIStores" asp-action="SetupWallet" asp-route-cryptoCode="@scheme.Crypto" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActivePage(StoreNavPages.OnchainSettings)" id="@($"StoreNav-Wallet{scheme.Crypto}")">
|
||||
<span class="me-2 btcpay-status btcpay-status--@(scheme.Enabled ? "enabled" : "pending")"></span>
|
||||
<span>@(Model.AltcoinsBuild ? $"{scheme.Crypto} " : "")Wallet</span>
|
||||
<span>@(Model.AltcoinsBuild ? $"{scheme.Crypto} Wallet" : "Bitcoin")</span>
|
||||
</a>
|
||||
}
|
||||
</li>
|
||||
@ -187,9 +187,9 @@
|
||||
<vc:ui-extension-point location="store-integrations-nav" model="@Model"/>
|
||||
}
|
||||
<li class="nav-item" permission="@Policies.CanModifyServerSettings">
|
||||
<a asp-area="" asp-controller="UIServer" asp-action="ListPlugins" class="nav-link js-scroll-trigger @ViewData.IsActivePage(ServerNavPages.Plugins)" id="Nav-AddPlugin">
|
||||
<a asp-area="" asp-controller="UIServer" asp-action="ListPlugins" class="nav-link js-scroll-trigger @ViewData.IsActivePage(ServerNavPages.Plugins)" id="Nav-ManagePlugins">
|
||||
<vc:icon symbol="new"/>
|
||||
<span>Add Plugin</span>
|
||||
<span>Manage Plugins</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -19,7 +19,6 @@ namespace BTCPayServer.Components.MainNav
|
||||
{
|
||||
public class MainNav : ViewComponent
|
||||
{
|
||||
private const string RootName = "Global";
|
||||
private readonly AppService _appService;
|
||||
private readonly StoreRepository _storeRepo;
|
||||
private readonly UIStoresController _storesController;
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
@if (Model.Total > pageSizeOptions.Min())
|
||||
{
|
||||
<nav aria-label="..." class="w-100">
|
||||
<nav aria-label="..." class="w-100 clearfix">
|
||||
@if (Model.Total > Model.Count)
|
||||
{
|
||||
<ul class="pagination float-start">
|
||||
|
27
BTCPayServer/Components/StoreNumbers/Default.cshtml
Normal file
27
BTCPayServer/Components/StoreNumbers/Default.cshtml
Normal file
@ -0,0 +1,27 @@
|
||||
@model BTCPayServer.Components.StoreNumbers.StoreNumbersViewModel
|
||||
|
||||
<div class="widget store-numbers">
|
||||
<div class="store-number">
|
||||
<header>
|
||||
<h6>Payouts Pending</h6>
|
||||
<a asp-controller="UIStorePullPayments" asp-action="Payouts" asp-route-storeId="@Model.Store.Id">Manage</a>
|
||||
</header>
|
||||
<div class="h3">@Model.PayoutsPending</div>
|
||||
</div>
|
||||
<div class="store-number">
|
||||
<header>
|
||||
<h6>TXs in the last @Model.TransactionDays days</h6>
|
||||
@if (Model.Transactions > 0)
|
||||
{
|
||||
<a asp-controller="UIWallets" asp-action="WalletTransactions" asp-route-walletId="@Model.WalletId">View All</a>
|
||||
}
|
||||
</header>
|
||||
<div class="h3">@Model.Transactions</div>
|
||||
</div>
|
||||
<div class="store-number">
|
||||
<header>
|
||||
<h6>Refunds Issued</h6>
|
||||
</header>
|
||||
<div class="h3">@Model.RefundsIssued</div>
|
||||
</div>
|
||||
</div>
|
77
BTCPayServer/Components/StoreNumbers/StoreNumbers.cs
Normal file
77
BTCPayServer/Components/StoreNumbers/StoreNumbers.cs
Normal file
@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Components.StoreRecentTransactions;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Npgsql;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
namespace BTCPayServer.Components.StoreNumbers;
|
||||
|
||||
public class StoreNumbers : ViewComponent
|
||||
{
|
||||
private const string CryptoCode = "BTC";
|
||||
private const int TransactionDays = 7;
|
||||
|
||||
private readonly StoreRepository _storeRepo;
|
||||
private readonly ApplicationDbContextFactory _dbContextFactory;
|
||||
private readonly BTCPayWalletProvider _walletProvider;
|
||||
private readonly BTCPayNetworkProvider _networkProvider;
|
||||
|
||||
public StoreNumbers(
|
||||
StoreRepository storeRepo,
|
||||
ApplicationDbContextFactory dbContextFactory,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
BTCPayWalletProvider walletProvider)
|
||||
{
|
||||
_storeRepo = storeRepo;
|
||||
_walletProvider = walletProvider;
|
||||
_networkProvider = networkProvider;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(StoreData store)
|
||||
{
|
||||
|
||||
await using var ctx = _dbContextFactory.CreateContext();
|
||||
var payoutsCount = await ctx.Payouts
|
||||
.Where(p => p.PullPaymentData.StoreId == store.Id && !p.PullPaymentData.Archived && p.State == PayoutState.AwaitingApproval)
|
||||
.CountAsync();
|
||||
var refundsCount = await ctx.Invoices
|
||||
.Where(i => i.StoreData.Id == store.Id && !i.Archived && i.CurrentRefundId != null)
|
||||
.CountAsync();
|
||||
|
||||
var walletId = new WalletId(store.Id, CryptoCode);
|
||||
var derivation = store.GetDerivationSchemeSettings(_networkProvider, walletId.CryptoCode);
|
||||
var transactionsCount = 0;
|
||||
if (derivation != null)
|
||||
{
|
||||
var network = derivation.Network;
|
||||
var wallet = _walletProvider.GetWallet(network);
|
||||
var allTransactions = await wallet.FetchTransactions(derivation.AccountDerivation);
|
||||
var afterDate = DateTimeOffset.UtcNow - TimeSpan.FromDays(TransactionDays);
|
||||
transactionsCount = allTransactions.UnconfirmedTransactions.Transactions
|
||||
.Concat(allTransactions.ConfirmedTransactions.Transactions)
|
||||
.Count(t => t.Timestamp > afterDate);
|
||||
}
|
||||
|
||||
var vm = new StoreNumbersViewModel
|
||||
{
|
||||
Store = store,
|
||||
WalletId = walletId,
|
||||
PayoutsPending = payoutsCount,
|
||||
Transactions = transactionsCount,
|
||||
TransactionDays = TransactionDays,
|
||||
RefundsIssued = refundsCount
|
||||
};
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
using System.Collections;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Components.StoreNumbers;
|
||||
|
||||
public class StoreNumbersViewModel
|
||||
{
|
||||
public StoreData Store { get; set; }
|
||||
public WalletId WalletId { get; set; }
|
||||
public int PayoutsPending { get; set; }
|
||||
public int Transactions { get; set; }
|
||||
public int RefundsIssued { get; set; }
|
||||
public int TransactionDays { get; set; }
|
||||
}
|
57
BTCPayServer/Components/StoreRecentInvoices/Default.cshtml
Normal file
57
BTCPayServer/Components/StoreRecentInvoices/Default.cshtml
Normal file
@ -0,0 +1,57 @@
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Client.Models
|
||||
@using BTCPayServer.Services.Invoices
|
||||
@model BTCPayServer.Components.StoreRecentInvoices.StoreRecentInvoicesViewModel
|
||||
|
||||
<div class="widget store-recent-transactions">
|
||||
<header>
|
||||
<h3>Recent Invoices</h3>
|
||||
@if (Model.Invoices.Any())
|
||||
{
|
||||
<a asp-controller="UIInvoice" asp-action="ListInvoices" asp-route-storeId="@Model.Store.Id">View All</a>
|
||||
}
|
||||
</header>
|
||||
@if (Model.Invoices.Any())
|
||||
{
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-125px">Date</th>
|
||||
<th class="text-nowrap">Invoice Id</th>
|
||||
<th>Status</th>
|
||||
<th class="text-end">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var invoice in Model.Invoices)
|
||||
{
|
||||
<tr>
|
||||
<td>@invoice.Date.ToTimeAgo()</td>
|
||||
<td>
|
||||
<a asp-controller="UIInvoice" asp-action="Invoice" asp-route-invoiceId="@invoice.InvoiceId" class="text-break">@invoice.InvoiceId</a>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge badge-@invoice.Status.Status.ToModernStatus().ToString().ToLower()">
|
||||
@invoice.Status.Status.ToModernStatus().ToString()
|
||||
@if (invoice.Status.ExceptionStatus != InvoiceExceptionStatus.None)
|
||||
{
|
||||
@($"({invoice.Status.ExceptionStatus.ToString()})")
|
||||
}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-end">@invoice.AmountCurrency</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-secondary my-3">
|
||||
There are no recent invoices.
|
||||
</p>
|
||||
<a asp-controller="UIInvoice" asp-action="CreateInvoice" asp-route-storeId="@Model.Store.Id" class="fw-semibold">
|
||||
Create Invoice
|
||||
</a>
|
||||
}
|
||||
</div>
|
@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
||||
namespace BTCPayServer.Components.StoreRecentInvoices;
|
||||
|
||||
public class StoreRecentInvoiceViewModel
|
||||
{
|
||||
public string InvoiceId { get; set; }
|
||||
public string OrderId { get; set; }
|
||||
public string AmountCurrency { get; set; }
|
||||
public InvoiceState Status { get; set; }
|
||||
public DateTimeOffset Date { get; set; }
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Components.StoreRecentInvoices;
|
||||
|
||||
public class StoreRecentInvoices : ViewComponent
|
||||
{
|
||||
private readonly StoreRepository _storeRepo;
|
||||
private readonly InvoiceRepository _invoiceRepo;
|
||||
private readonly CurrencyNameTable _currencyNameTable;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly ApplicationDbContextFactory _dbContextFactory;
|
||||
|
||||
public StoreRecentInvoices(
|
||||
StoreRepository storeRepo,
|
||||
InvoiceRepository invoiceRepo,
|
||||
CurrencyNameTable currencyNameTable,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
ApplicationDbContextFactory dbContextFactory)
|
||||
{
|
||||
_storeRepo = storeRepo;
|
||||
_invoiceRepo = invoiceRepo;
|
||||
_userManager = userManager;
|
||||
_currencyNameTable = currencyNameTable;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(StoreData store)
|
||||
{
|
||||
var userId = _userManager.GetUserId(UserClaimsPrincipal);
|
||||
var invoiceEntities = await _invoiceRepo.GetInvoices(new InvoiceQuery
|
||||
{
|
||||
UserId = userId,
|
||||
StoreId = new [] { store.Id },
|
||||
Take = 5
|
||||
});
|
||||
var invoices = new List<StoreRecentInvoiceViewModel>();
|
||||
foreach (var invoice in invoiceEntities)
|
||||
{
|
||||
var state = invoice.GetInvoiceState();
|
||||
invoices.Add(new StoreRecentInvoiceViewModel
|
||||
{
|
||||
Date = invoice.InvoiceTime,
|
||||
Status = state,
|
||||
InvoiceId = invoice.Id,
|
||||
OrderId = invoice.Metadata.OrderId ?? string.Empty,
|
||||
AmountCurrency = _currencyNameTable.DisplayFormatCurrency(invoice.Price, invoice.Currency),
|
||||
});
|
||||
}
|
||||
var vm = new StoreRecentInvoicesViewModel
|
||||
{
|
||||
Store = store,
|
||||
Invoices = invoices
|
||||
};
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Components.StoreRecentInvoices;
|
||||
|
||||
public class StoreRecentInvoicesViewModel
|
||||
{
|
||||
public StoreData Store { get; set; }
|
||||
public IEnumerable<StoreRecentInvoiceViewModel> Invoices { get; set; }
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@model BTCPayServer.Components.StoreRecentTransactions.StoreRecentTransactionsViewModel
|
||||
|
||||
<div class="widget store-recent-transactions">
|
||||
<header>
|
||||
<h3>Recent Transactions</h3>
|
||||
@if (Model.Transactions.Any())
|
||||
{
|
||||
<a asp-controller="UIWallets" asp-action="WalletTransactions" asp-route-walletId="@Model.WalletId">View All</a>
|
||||
}
|
||||
</header>
|
||||
@if (Model.Transactions.Any())
|
||||
{
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-125px">Date</th>
|
||||
<th>Transaction</th>
|
||||
<th class="text-end">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var tx in Model.Transactions)
|
||||
{
|
||||
<tr>
|
||||
<td>@tx.Timestamp.ToTimeAgo()</td>
|
||||
<td>
|
||||
<a href="@tx.Link" target="_blank" rel="noreferrer noopener" class="text-break">
|
||||
@tx.Id
|
||||
</a>
|
||||
</td>
|
||||
@if (tx.Positive)
|
||||
{
|
||||
<td class="text-end text-success">@tx.Balance</td>
|
||||
}
|
||||
else
|
||||
{
|
||||
<td class="text-end text-danger">@tx.Balance</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-secondary mt-3 mb-0">
|
||||
There are no recent transactions.
|
||||
</p>
|
||||
}
|
||||
</div>
|
@ -0,0 +1,13 @@
|
||||
using System;
|
||||
|
||||
namespace BTCPayServer.Components.StoreRecentTransactions;
|
||||
|
||||
public class StoreRecentTransactionViewModel
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Balance { get; set; }
|
||||
public bool Positive { get; set; }
|
||||
public bool IsConfirmed { get; set; }
|
||||
public string Link { get; set; }
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Dapper;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBXplorer.Client;
|
||||
using static BTCPayServer.Components.StoreRecentTransactions.StoreRecentTransactionsViewModel;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Components.StoreRecentTransactions;
|
||||
|
||||
public class StoreRecentTransactions : ViewComponent
|
||||
{
|
||||
private const string CryptoCode = "BTC";
|
||||
private readonly StoreRepository _storeRepo;
|
||||
private readonly ApplicationDbContextFactory _dbContextFactory;
|
||||
private readonly BTCPayWalletProvider _walletProvider;
|
||||
|
||||
public BTCPayNetworkProvider NetworkProvider { get; }
|
||||
public NBXplorerConnectionFactory ConnectionFactory { get; }
|
||||
|
||||
public StoreRecentTransactions(
|
||||
StoreRepository storeRepo,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
NBXplorerConnectionFactory connectionFactory,
|
||||
BTCPayWalletProvider walletProvider,
|
||||
ApplicationDbContextFactory dbContextFactory)
|
||||
{
|
||||
_storeRepo = storeRepo;
|
||||
NetworkProvider = networkProvider;
|
||||
ConnectionFactory = connectionFactory;
|
||||
_walletProvider = walletProvider;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(StoreData store)
|
||||
{
|
||||
var walletId = new WalletId(store.Id, CryptoCode);
|
||||
var derivationSettings = store.GetDerivationSchemeSettings(NetworkProvider, walletId.CryptoCode);
|
||||
var transactions = new List<StoreRecentTransactionViewModel>();
|
||||
if (derivationSettings?.AccountDerivation is not null)
|
||||
{
|
||||
if (ConnectionFactory.Available)
|
||||
{
|
||||
var wallet_id = derivationSettings.GetNBXWalletId();
|
||||
await using var conn = await ConnectionFactory.OpenConnection();
|
||||
var rows = await conn.QueryAsync(
|
||||
"SELECT t.tx_id, t.seen_at, to_btc(balance_change::NUMERIC) balance_change, (t.blk_id IS NOT NULL) confirmed " +
|
||||
"FROM get_wallets_recent(@wallet_id, @code, @interval, 5, 0) " +
|
||||
"JOIN txs t USING (code, tx_id) " +
|
||||
"ORDER BY seen_at DESC;",
|
||||
new
|
||||
{
|
||||
wallet_id,
|
||||
code = CryptoCode,
|
||||
interval = TimeSpan.FromDays(31)
|
||||
});
|
||||
var network = derivationSettings.Network;
|
||||
foreach (var r in rows)
|
||||
{
|
||||
var seenAt = new DateTimeOffset(((DateTime)r.seen_at));
|
||||
var balanceChange = new Money((decimal)r.balance_change, MoneyUnit.BTC);
|
||||
transactions.Add(new StoreRecentTransactionViewModel()
|
||||
{
|
||||
Timestamp = seenAt,
|
||||
Id = r.tx_id,
|
||||
Balance = balanceChange.ShowMoney(network),
|
||||
IsConfirmed = r.confirmed,
|
||||
Link = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, r.tx_id),
|
||||
Positive = balanceChange.GetValue(network) >= 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var network = derivationSettings.Network;
|
||||
var wallet = _walletProvider.GetWallet(network);
|
||||
var allTransactions = await wallet.FetchTransactions(derivationSettings.AccountDerivation);
|
||||
transactions = allTransactions.UnconfirmedTransactions.Transactions
|
||||
.Concat(allTransactions.ConfirmedTransactions.Transactions).ToArray()
|
||||
.OrderByDescending(t => t.Timestamp)
|
||||
.Take(5)
|
||||
.Select(tx => new StoreRecentTransactionViewModel
|
||||
{
|
||||
Id = tx.TransactionId.ToString(),
|
||||
Positive = tx.BalanceChange.GetValue(network) >= 0,
|
||||
Balance = tx.BalanceChange.ShowMoney(network),
|
||||
IsConfirmed = tx.Confirmations != 0,
|
||||
Link = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, tx.TransactionId.ToString()),
|
||||
Timestamp = tx.Timestamp
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var vm = new StoreRecentTransactionsViewModel
|
||||
{
|
||||
Store = store,
|
||||
WalletId = walletId,
|
||||
Transactions = transactions
|
||||
};
|
||||
return View(vm);
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Components.StoreRecentTransactions;
|
||||
|
||||
public class StoreRecentTransactionsViewModel
|
||||
{
|
||||
public StoreData Store { get; set; }
|
||||
public IList<StoreRecentTransactionViewModel> Transactions { get; set; } = new List<StoreRecentTransactionViewModel>();
|
||||
public WalletId WalletId { get; set; }
|
||||
}
|
@ -47,11 +47,7 @@ else
|
||||
@foreach (var option in Model.Options)
|
||||
{
|
||||
<li>
|
||||
@if (option.IsOwner && option.WalletId != null)
|
||||
{
|
||||
<a asp-controller="UIWallets" asp-action="WalletTransactions" asp-route-walletId="@option.WalletId" class="dropdown-item@(option.Selected ? " active" : "")" id="StoreSelectorMenuItem-@option.Value">@StoreName(option.Text)</a>
|
||||
}
|
||||
else if (option.IsOwner)
|
||||
@if (option.IsOwner)
|
||||
{
|
||||
<a asp-controller="UIStores" asp-action="Dashboard" asp-route-storeId="@option.Value" class="dropdown-item@(option.Selected ? " active" : "")" id="StoreSelectorMenuItem-@option.Value">@StoreName(option.Text)</a>
|
||||
}
|
||||
|
@ -47,6 +47,7 @@ namespace BTCPayServer.Components.StoreSelector
|
||||
WalletId = walletId
|
||||
};
|
||||
})
|
||||
.OrderBy(s => s.Text)
|
||||
.ToList();
|
||||
|
||||
var vm = new StoreSelectorViewModel
|
||||
|
57
BTCPayServer/Components/StoreWalletBalance/Default.cshtml
Normal file
57
BTCPayServer/Components/StoreWalletBalance/Default.cshtml
Normal file
@ -0,0 +1,57 @@
|
||||
@using BTCPayServer.Services.Wallets
|
||||
@model BTCPayServer.Components.StoreWalletBalance.StoreWalletBalanceViewModel
|
||||
|
||||
<div id="StoreWalletBalance-@Model.Store.Id" class="widget store-wallet-balance">
|
||||
<h6 class="mb-2">Wallet Balance</h6>
|
||||
<header class="mb-3">
|
||||
<div class="balance">
|
||||
<h3 class="d-inline-block me-1">@Model.Balance</h3>
|
||||
<span class="text-secondary fw-semibold">@Model.CryptoCode</span>
|
||||
</div>
|
||||
<div class="btn-group mt-1" role="group" aria-label="Filter">
|
||||
<input type="radio" class="btn-check" name="filter" id="filter-week" value="week" @(Model.Type == WalletHistogramType.Week ? "checked" : "")>
|
||||
<label class="btn btn-link" for="filter-week">1W</label>
|
||||
<input type="radio" class="btn-check" name="filter" id="filter-month" value="month" @(Model.Type == WalletHistogramType.Month ? "checked" : "")>
|
||||
<label class="btn btn-link" for="filter-month">1M</label>
|
||||
<input type="radio" class="btn-check" name="filter" id="filter-year" value="year" @(Model.Type == WalletHistogramType.Year ? "checked" : "")>
|
||||
<label class="btn btn-link" for="filter-year">1Y</label>
|
||||
</div>
|
||||
</header>
|
||||
<div class="ct-chart ct-major-eleventh"></div>
|
||||
<script>
|
||||
(function () {
|
||||
const id = 'StoreWalletBalance-@Model.Store.Id';
|
||||
const baseUrl = @Safe.Json(Url.Action("WalletHistogram", "UIWallets", new { walletId = Model.WalletId, type = WalletHistogramType.Week }));
|
||||
const render = data => {
|
||||
const { series, labels, balance } = data;
|
||||
document.querySelector(`#${id} h3`).innerText = balance;
|
||||
const min = Math.min(...series);
|
||||
const max = Math.max(...series);
|
||||
const low = Math.max(min - ((max - min) / 5), 0);
|
||||
new Chartist.Line(`#${id} .ct-chart`, {
|
||||
labels,
|
||||
series: [series]
|
||||
}, {
|
||||
low,
|
||||
fullWidth: true,
|
||||
showArea: true
|
||||
});
|
||||
};
|
||||
const update = async type => {
|
||||
const url = baseUrl.replace(/\/week$/gi, `/${type}`);
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
const json = await response.json();
|
||||
render(json);
|
||||
}
|
||||
};
|
||||
render({ series: @Safe.Json(Model.Series), labels: @Safe.Json(Model.Labels), balance: @Safe.Json(Model.Balance) });
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
delegate('change', `#${id} [name="filter"]`, async e => {
|
||||
const type = e.target.value;
|
||||
await update(type);
|
||||
})
|
||||
})
|
||||
})();
|
||||
</script>
|
||||
</div>
|
@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.Common;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Dapper;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBXplorer;
|
||||
using NBXplorer.Client;
|
||||
|
||||
namespace BTCPayServer.Components.StoreWalletBalance;
|
||||
|
||||
public class StoreWalletBalance : ViewComponent
|
||||
{
|
||||
private const string CryptoCode = "BTC";
|
||||
private const WalletHistogramType DefaultType = WalletHistogramType.Week;
|
||||
|
||||
private readonly StoreRepository _storeRepo;
|
||||
private readonly WalletHistogramService _walletHistogramService;
|
||||
|
||||
public StoreWalletBalance(StoreRepository storeRepo, WalletHistogramService walletHistogramService)
|
||||
{
|
||||
_storeRepo = storeRepo;
|
||||
_walletHistogramService = walletHistogramService;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(StoreData store)
|
||||
{
|
||||
var walletId = new WalletId(store.Id, CryptoCode);
|
||||
var data = await _walletHistogramService.GetHistogram(store, walletId, DefaultType);
|
||||
|
||||
var vm = new StoreWalletBalanceViewModel
|
||||
{
|
||||
Store = store,
|
||||
CryptoCode = CryptoCode,
|
||||
WalletId = walletId,
|
||||
Series = data?.Series,
|
||||
Labels = data?.Labels,
|
||||
Balance = data?.Balance ?? 0,
|
||||
Type = DefaultType
|
||||
};
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
|
||||
namespace BTCPayServer.Components.StoreWalletBalance;
|
||||
|
||||
public class StoreWalletBalanceViewModel
|
||||
{
|
||||
public decimal Balance { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
public StoreData Store { get; set; }
|
||||
public WalletId WalletId { get; set; }
|
||||
public WalletHistogramType Type { get; set; }
|
||||
public IList<string> Labels { get; set; } = new List<string>();
|
||||
public IList<decimal> Series { get; set; } = new List<decimal>();
|
||||
}
|
29
BTCPayServer/Components/WalletNav/Default.cshtml
Normal file
29
BTCPayServer/Components/WalletNav/Default.cshtml
Normal file
@ -0,0 +1,29 @@
|
||||
@using BTCPayServer.Views.Stores
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Views.Wallets
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@inject BTCPayNetworkProvider _btcPayNetworkProvider
|
||||
|
||||
@model BTCPayServer.Components.WalletNav.WalletNavViewModel
|
||||
@addTagHelper *, BundlerMinifier.TagHelpers
|
||||
|
||||
<div class="d-sm-flex align-items-center justify-content-between">
|
||||
<a asp-controller="UIWallets" asp-action="WalletTransactions" asp-route-walletId="@Model.WalletId" class="unobtrusive-link">
|
||||
<h2 class="mb-1">@Model.Label</h2>
|
||||
<div class="text-muted fw-semibold">
|
||||
@Model.Balance @Model.Network.CryptoCode
|
||||
</div>
|
||||
</a>
|
||||
<div class="d-flex gap-3 mt-3 mt-sm-0" permission="@Policies.CanModifyStoreSettings">
|
||||
@if (!Model.Network.ReadonlyWallet)
|
||||
{
|
||||
<a class="btn btn-primary" asp-controller="UIWallets" asp-action="WalletSend" asp-route-walletId="@Model.WalletId" id="WalletNav-Send">Send</a>
|
||||
}
|
||||
<a class="btn btn-primary" asp-controller="UIWallets" asp-action="WalletReceive" asp-route-walletId="@Model.WalletId" id="WalletNav-Receive">Receive</a>
|
||||
<a class="btn btn-secondary @ViewData.IsActivePage(WalletsNavPages.Settings) @ViewData.IsActivePage(StoreNavPages.OnchainSettings)" asp-controller="UIStores" asp-action="WalletSettings" asp-route-cryptoCode="@Model.WalletId.CryptoCode" asp-route-storeId="@Model.WalletId.StoreId" title="Settings" id="WalletNav-Settings">
|
||||
<vc:icon symbol="settings"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<vc:ui-extension-point location="wallet-nav" model="@Model"/>
|
56
BTCPayServer/Components/WalletNav/WalletNav.cs
Normal file
56
BTCPayServer/Components/WalletNav/WalletNav.cs
Normal file
@ -0,0 +1,56 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Secp256k1;
|
||||
|
||||
namespace BTCPayServer.Components.WalletNav
|
||||
{
|
||||
public class WalletNav : ViewComponent
|
||||
{
|
||||
private readonly BTCPayWalletProvider _walletProvider;
|
||||
private readonly UIWalletsController _walletsController;
|
||||
private readonly BTCPayNetworkProvider _networkProvider;
|
||||
|
||||
public WalletNav(
|
||||
BTCPayWalletProvider walletProvider,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
UIWalletsController walletsController)
|
||||
{
|
||||
_walletProvider = walletProvider;
|
||||
_networkProvider = networkProvider;
|
||||
_walletsController = walletsController;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(WalletId walletId)
|
||||
{
|
||||
var store = ViewContext.HttpContext.GetStoreData();
|
||||
var network = _networkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
|
||||
var wallet = _walletProvider.GetWallet(network);
|
||||
var derivation = store.GetDerivationSchemeSettings(_networkProvider, walletId.CryptoCode);
|
||||
var balance = await _walletsController.GetBalanceString(wallet, derivation.AccountDerivation);
|
||||
|
||||
var vm = new WalletNavViewModel
|
||||
{
|
||||
WalletId = walletId,
|
||||
Network = network,
|
||||
Balance = balance,
|
||||
Label = derivation.Label ?? $"{store.StoreName} {walletId.CryptoCode} Wallet"
|
||||
};
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
}
|
10
BTCPayServer/Components/WalletNav/WalletNavViewModel.cs
Normal file
10
BTCPayServer/Components/WalletNav/WalletNavViewModel.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace BTCPayServer.Components.WalletNav
|
||||
{
|
||||
public class WalletNavViewModel
|
||||
{
|
||||
public WalletId WalletId { get; set; }
|
||||
public BTCPayNetwork Network { get; set; }
|
||||
public string Label { get; set; }
|
||||
public string Balance { get; set; }
|
||||
}
|
||||
}
|
@ -50,6 +50,8 @@ namespace BTCPayServer.Configuration
|
||||
app.Option("--recommended-plugins", "Plugins which would be marked as recommended to be installed. Separated by newline or space", CommandOptionType.MultipleValue);
|
||||
app.Option("--xforwardedproto", "If specified, set X-Forwarded-Proto to the specified value, this may be useful if your reverse proxy handle https but is not configured to add X-Forwarded-Proto (example: --xforwardedproto https)", CommandOptionType.SingleValue);
|
||||
app.Option("--cheatmode", "Add elements in the UI to facilitate dev-time testing (Default false)", CommandOptionType.BoolValue);
|
||||
|
||||
app.Option("--explorerpostgres", $"Connection string to the postgres database of NBXplorer. (optional, used for dashboard and reporting features)", CommandOptionType.SingleValue);
|
||||
foreach (var network in provider.GetAll().OfType<BTCPayNetwork>())
|
||||
{
|
||||
var crypto = network.CryptoCode.ToLowerInvariant();
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Controllers;
|
||||
using NBitcoin;
|
||||
|
||||
@ -81,26 +82,28 @@ namespace BTCPayServer.Configuration
|
||||
// Read access key from cookie file
|
||||
if (connectionString.CookieFilePath != null)
|
||||
{
|
||||
string cookieFileContent = null;
|
||||
bool isFake = false;
|
||||
try
|
||||
bool isFake = connectionString.CookieFilePath == "fake"; // Hacks for testing
|
||||
string cookieFileContent = isFake ? "fake" : null;
|
||||
if (!isFake)
|
||||
{
|
||||
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);
|
||||
try
|
||||
{
|
||||
cookieFileContent = await System.IO.File.ReadAllTextAsync(connectionString.CookieFilePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new System.IO.FileNotFoundException("Cookie file path not found", ex);
|
||||
}
|
||||
}
|
||||
connectionString.CookieFilePath = null;
|
||||
|
||||
if (serviceType == ExternalServiceTypes.RTL || serviceType == ExternalServiceTypes.Configurator || serviceType == ExternalServiceTypes.ThunderHub)
|
||||
{
|
||||
connectionString.AccessKey = cookieFileContent;
|
||||
}
|
||||
else if (serviceType == ExternalServiceTypes.Spark)
|
||||
{
|
||||
var cookie = (isFake ? "fake:fake:fake" // Hacks for testing
|
||||
: cookieFileContent).Split(':');
|
||||
var cookie = (isFake ? "fake:fake:fake" : cookieFileContent).Split(':');
|
||||
if (cookie.Length >= 3)
|
||||
{
|
||||
connectionString.AccessKey = cookie[2];
|
||||
|
@ -9,5 +9,6 @@ namespace BTCPayServer.Configuration
|
||||
get;
|
||||
set;
|
||||
} = new List<NBXplorerConnectionSetting>();
|
||||
public string ConnectionString { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.ModelBinders;
|
||||
|
@ -1,42 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Client.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
public static class GreenfieldUtils
|
||||
{
|
||||
public static IActionResult CreateValidationError(this ControllerBase controller, ModelStateDictionary modelState)
|
||||
{
|
||||
return controller.UnprocessableEntity(modelState.ToGreenfieldValidationError());
|
||||
}
|
||||
|
||||
public static List<GreenfieldValidationError> ToGreenfieldValidationError(this ModelStateDictionary modelState)
|
||||
{
|
||||
List<GreenfieldValidationError> errors = new List<GreenfieldValidationError>();
|
||||
foreach (var error in modelState)
|
||||
{
|
||||
foreach (var errorMessage in error.Value.Errors)
|
||||
{
|
||||
errors.Add(new GreenfieldValidationError(error.Key, errorMessage.ErrorMessage));
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
public static IActionResult CreateAPIError(this ControllerBase controller, string errorCode, string errorMessage)
|
||||
{
|
||||
return controller.BadRequest(new GreenfieldAPIError(errorCode, errorMessage));
|
||||
}
|
||||
public static IActionResult CreateAPIError(this ControllerBase controller, int httpCode, string errorCode, string errorMessage)
|
||||
{
|
||||
return controller.StatusCode(httpCode, new GreenfieldAPIError(errorCode, errorMessage));
|
||||
}
|
||||
public static IActionResult CreateAPIPermissionError(this ControllerBase controller, string missingPermission, string message = null)
|
||||
{
|
||||
return controller.StatusCode(403, new GreenfieldPermissionAPIError(missingPermission, message));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
|
@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Payments;
|
||||
@ -365,7 +366,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
PaymentLink =
|
||||
method.GetId().PaymentType.GetPaymentLink(method.Network, details, accounting.Due,
|
||||
Request.GetAbsoluteRoot()),
|
||||
Payments = payments.Select(paymentEntity => ToPaymentModel(entity, paymentEntity)).ToList()
|
||||
Payments = payments.Select(paymentEntity => ToPaymentModel(entity, paymentEntity)).ToList(),
|
||||
AdditionalData = details.GetAdditionalData()
|
||||
};
|
||||
}).ToArray();
|
||||
}
|
||||
|
@ -77,6 +77,14 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return base.GetDepositAddress(cryptoCode);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanUseInternalLightningNode,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/server/lightning/{cryptoCode}/payments/{paymentHash}")]
|
||||
public override Task<IActionResult> GetPayment(string cryptoCode, string paymentHash)
|
||||
{
|
||||
return base.GetPayment(cryptoCode, paymentHash);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanUseInternalLightningNode,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/server/lightning/{cryptoCode}/invoices/{id}")]
|
||||
|
@ -78,6 +78,14 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return base.GetDepositAddress(cryptoCode);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/payments/{paymentHash}")]
|
||||
public override Task<IActionResult> GetPayment(string cryptoCode, string paymentHash)
|
||||
{
|
||||
return base.GetPayment(cryptoCode, paymentHash);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices/pay")]
|
||||
|
@ -3,6 +3,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.HostedServices;
|
||||
@ -20,7 +21,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
public void OnException(ExceptionContext context)
|
||||
{
|
||||
context.Result = new ObjectResult(new GreenfieldAPIError("ligthning-node-unavailable", $"The lightning node is unavailable ({context.Exception.GetType().Name}: {context.Exception.Message})")) { StatusCode = 503 };
|
||||
context.Result = new ObjectResult(new GreenfieldAPIError("lightning-node-unavailable", $"The lightning node is unavailable ({context.Exception.GetType().Name}: {context.Exception.Message})")) { StatusCode = 503 };
|
||||
// Do not mark handled, it is possible filters above have better errors
|
||||
}
|
||||
}
|
||||
@ -163,6 +164,13 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return Ok(new JValue((await lightningClient.GetDepositAddress()).ToString()));
|
||||
}
|
||||
|
||||
public virtual async Task<IActionResult> GetPayment(string cryptoCode, string paymentHash)
|
||||
{
|
||||
var lightningClient = await GetLightningClient(cryptoCode, false);
|
||||
var payment = await lightningClient.GetPayment(paymentHash);
|
||||
return payment == null ? this.CreateAPIError(404, "payment-not-found", "Impossible to find a lightning payment with this payment hash") : Ok(ToModel(payment));
|
||||
}
|
||||
|
||||
public virtual async Task<IActionResult> PayInvoice(string cryptoCode, PayLightningInvoiceRequest lightningInvoice)
|
||||
{
|
||||
var lightningClient = await GetLightningClient(cryptoCode, true);
|
||||
@ -178,19 +186,23 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
var result = await lightningClient.Pay(lightningInvoice.BOLT11);
|
||||
switch (result.Result)
|
||||
|
||||
var param = lightningInvoice?.MaxFeeFlat != null || lightningInvoice?.MaxFeePercent != null
|
||||
? new PayInvoiceParams { MaxFeePercent = lightningInvoice.MaxFeePercent, MaxFeeFlat = lightningInvoice.MaxFeeFlat }
|
||||
: null;
|
||||
var result = await lightningClient.Pay(lightningInvoice.BOLT11, param);
|
||||
|
||||
return result.Result switch
|
||||
{
|
||||
case PayResult.CouldNotFindRoute:
|
||||
return this.CreateAPIError("could-not-find-route", "Impossible to find a route to the peer");
|
||||
case PayResult.Error:
|
||||
return this.CreateAPIError("generic-error", result.ErrorDetail);
|
||||
case PayResult.Ok:
|
||||
return Ok();
|
||||
default:
|
||||
throw new NotSupportedException("Unsupported Payresult");
|
||||
}
|
||||
PayResult.CouldNotFindRoute => this.CreateAPIError("could-not-find-route", "Impossible to find a route to the peer"),
|
||||
PayResult.Error => this.CreateAPIError("generic-error", result.ErrorDetail),
|
||||
PayResult.Ok => Ok(new LightningPaymentData
|
||||
{
|
||||
TotalAmount = result.Details?.TotalAmount,
|
||||
FeeAmount = result.Details?.FeeAmount
|
||||
}),
|
||||
_ => throw new NotSupportedException("Unsupported Payresult")
|
||||
};
|
||||
}
|
||||
|
||||
public virtual async Task<IActionResult> GetInvoice(string cryptoCode, string id)
|
||||
@ -220,12 +232,16 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
try
|
||||
{
|
||||
var invoice = await lightningClient.CreateInvoice(
|
||||
new CreateInvoiceParams(request.Amount, request.Description, request.Expiry)
|
||||
var param = request.DescriptionHash != null
|
||||
? new CreateInvoiceParams(request.Amount, request.DescriptionHash, request.Expiry)
|
||||
{
|
||||
PrivateRouteHints = request.PrivateRouteHints
|
||||
},
|
||||
CancellationToken.None);
|
||||
PrivateRouteHints = request.PrivateRouteHints, Description = request.Description
|
||||
}
|
||||
: new CreateInvoiceParams(request.Amount, request.Description, request.Expiry)
|
||||
{
|
||||
PrivateRouteHints = request.PrivateRouteHints, DescriptionHash = request.DescriptionHash
|
||||
};
|
||||
var invoice = await lightningClient.CreateInvoice(param, CancellationToken.None);
|
||||
return Ok(ToModel(invoice));
|
||||
}
|
||||
catch (Exception ex)
|
||||
@ -253,7 +269,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
private LightningInvoiceData ToModel(LightningInvoice invoice)
|
||||
{
|
||||
return new LightningInvoiceData()
|
||||
return new LightningInvoiceData
|
||||
{
|
||||
Amount = invoice.Amount,
|
||||
Id = invoice.Id,
|
||||
@ -265,6 +281,21 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
};
|
||||
}
|
||||
|
||||
private LightningPaymentData ToModel(LightningPayment payment)
|
||||
{
|
||||
return new LightningPaymentData
|
||||
{
|
||||
TotalAmount = payment.AmountSent,
|
||||
FeeAmount = payment.Amount != null && payment.AmountSent != null ? payment.AmountSent - payment.Amount : null,
|
||||
Id = payment.Id,
|
||||
Status = payment.Status,
|
||||
CreatedAt = payment.CreatedAt,
|
||||
BOLT11 = payment.BOLT11,
|
||||
PaymentHash = payment.PaymentHash,
|
||||
Preimage = payment.Preimage
|
||||
};
|
||||
}
|
||||
|
||||
protected async Task<bool> CanUseInternalLightning(bool doingAdminThings)
|
||||
{
|
||||
|
||||
|
@ -3,6 +3,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
|
@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
|
@ -0,0 +1,95 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Validation;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers.GreenField
|
||||
{
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public class GreenfieldStoreEmailController : Controller
|
||||
{
|
||||
private readonly EmailSenderFactory _emailSenderFactory;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
|
||||
public GreenfieldStoreEmailController(EmailSenderFactory emailSenderFactory, StoreRepository storeRepository)
|
||||
{
|
||||
_emailSenderFactory = emailSenderFactory;
|
||||
_storeRepository = storeRepository;
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/stores/{storeId}/email/send")]
|
||||
public async Task<IActionResult> SendEmailFromStore(string storeId,
|
||||
[FromBody] SendEmailRequest request)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return this.CreateAPIError(404, "store-not-found", "The store was not found");
|
||||
}
|
||||
var emailSender = await _emailSenderFactory.GetEmailSender(storeId);
|
||||
if (emailSender is null )
|
||||
{
|
||||
return this.CreateAPIError(404,"smtp-not-configured", "Store does not have an SMTP server configured.");
|
||||
}
|
||||
|
||||
emailSender.SendEmail(request.Email, request.Subject, request.Body);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/email")]
|
||||
public IActionResult GetStoreEmailSettings()
|
||||
{
|
||||
|
||||
var store = HttpContext.GetStoreData();
|
||||
return store == null ? StoreNotFound() : Ok(FromModel(store));
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPut("~/api/v1/stores/{storeId}/email")]
|
||||
public async Task<IActionResult> UpdateStoreEmailSettings(string storeId, EmailSettings request)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return StoreNotFound();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(request.From) && !EmailValidator.IsEmail(request.From))
|
||||
{
|
||||
request.AddModelError(e => e.From,
|
||||
"Invalid email address", this);
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
var blob = store.GetStoreBlob();
|
||||
blob.EmailSettings = request;
|
||||
if (store.SetStoreBlob(blob))
|
||||
{
|
||||
await _storeRepository.UpdateStore(store);
|
||||
}
|
||||
|
||||
return Ok(FromModel(store));
|
||||
}
|
||||
private EmailSettings FromModel(Data.StoreData data)
|
||||
{
|
||||
return data.GetStoreBlob().EmailSettings??new();
|
||||
}
|
||||
private IActionResult StoreNotFound()
|
||||
{
|
||||
return this.CreateAPIError(404, "store-not-found", "The store was not found");
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user