Compare commits
152 Commits
v1.4.3
...
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 | |||
631ddc0af2 | |||
1d593df5af | |||
1d3a8bb7bf | |||
0dc9c183b5 | |||
05ab43f309 | |||
f4153ade92 | |||
44e84b46b8 | |||
3fe71e7bdc | |||
5d4d8a3422 | |||
f06199230c | |||
da9a6b835a | |||
0afc2cd2cb | |||
9e2f7fb048 | |||
3fa694c65f | |||
300d84c5d8 | |||
efed00f58b | |||
99c4ebe046 | |||
699231fd92 | |||
c18f112f31 | |||
605d04580c | |||
143211f276 | |||
4ca152da7c | |||
1c1f69fa50 | |||
147ccd6c96 | |||
c8b9906ef3 | |||
cd94a9fac1 | |||
20a9472ee2 | |||
c652a2f122 | |||
ce174d507d | |||
f66b1b644f | |||
5b460f0b4e | |||
ab8d116f11 |
@ -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
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -299,3 +299,4 @@ BTCPayServer/wwwroot/bundles/*
|
||||
BTCPayServer/testpwd
|
||||
.DS_Store
|
||||
Packed Plugins
|
||||
Plugins/packed
|
||||
|
@ -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.2.8" />
|
||||
<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);
|
||||
}
|
||||
}
|
||||
}
|
37
BTCPayServer.Client/BTCPayServerClient.StoreUsers.cs
Normal file
37
BTCPayServer.Client/BTCPayServerClient.StoreUsers.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
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<IEnumerable<StoreUserData>> GetStoreUsers(string storeId,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
using var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/users"), token);
|
||||
return await HandleResponse<IEnumerable<StoreUserData>>(response);
|
||||
}
|
||||
|
||||
public virtual async Task RemoveStoreUser(string storeId, string userId, CancellationToken token = default)
|
||||
{
|
||||
using var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/users/{userId}", method: HttpMethod.Delete), token);
|
||||
await HandleResponse(response);
|
||||
}
|
||||
|
||||
public virtual async Task AddStoreUser(string storeId, StoreUserData request,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
using var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/users", bodyPayload: request, method: HttpMethod.Post),
|
||||
token);
|
||||
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; }
|
||||
|
@ -8,6 +8,7 @@ namespace BTCPayServer.Client.Models
|
||||
public class CreatePullPaymentRequest
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Description { get; set; }
|
||||
[JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))]
|
||||
public decimal Amount { get; set; }
|
||||
public string Currency { 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))]
|
||||
@ -13,6 +20,7 @@ namespace BTCPayServer.Client.Models
|
||||
public DateTimeOffset? ExpiresAt { get; set; }
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Currency { get; set; }
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal Amount { get; set; }
|
||||
|
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;
|
||||
}
|
||||
}
|
@ -7,4 +7,14 @@ namespace BTCPayServer.Client.Models
|
||||
/// </summary>
|
||||
public string Id { get; set; }
|
||||
}
|
||||
|
||||
public class StoreUserData
|
||||
{
|
||||
/// <summary>
|
||||
/// the id of the user
|
||||
/// </summary>
|
||||
public string UserId { get; set; }
|
||||
|
||||
public string Role { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -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'")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,29 @@
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
// Change this if you want another zcash coin
|
||||
public void InitZcash()
|
||||
{
|
||||
Add(new ZcashLikeSpecificBtcPayNetwork()
|
||||
{
|
||||
CryptoCode = "ZEC",
|
||||
DisplayName = "Zcash",
|
||||
Divisibility = 8,
|
||||
BlockExplorerLink =
|
||||
NetworkType == ChainName.Mainnet
|
||||
? "https://www.exploreZcash.com/transaction/{0}"
|
||||
: "https://testnet.xmrchain.net/tx/{0}",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"ZEC_X = ZEC_BTC * BTC_X",
|
||||
"ZEC_BTC = kraken(ZEC_BTC)"
|
||||
},
|
||||
CryptoImagePath = "/imlegacy/zcash.png",
|
||||
UriScheme = "zcash"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
102
BTCPayServer.Common/Altcoins/Zcash/RPC/JsonRpcClient.cs
Normal file
102
BTCPayServer.Common/Altcoins/Zcash/RPC/JsonRpcClient.cs
Normal file
@ -0,0 +1,102 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Zcash.RPC
|
||||
{
|
||||
public class JsonRpcClient
|
||||
{
|
||||
private readonly Uri _address;
|
||||
private readonly string _username;
|
||||
private readonly string _password;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public JsonRpcClient(Uri address, string username, string password, HttpClient client = null)
|
||||
{
|
||||
_address = address;
|
||||
_username = username;
|
||||
_password = password;
|
||||
_httpClient = client ?? new HttpClient();
|
||||
}
|
||||
|
||||
|
||||
public async Task<TResponse> SendCommandAsync<TRequest, TResponse>(string method, TRequest data,
|
||||
CancellationToken cts = default(CancellationToken))
|
||||
{
|
||||
var jsonSerializer = new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver()
|
||||
};
|
||||
var httpRequest = new HttpRequestMessage()
|
||||
{
|
||||
Method = HttpMethod.Post,
|
||||
RequestUri = new Uri(_address, method),
|
||||
Content = new StringContent(
|
||||
JsonConvert.SerializeObject(data, jsonSerializer),
|
||||
Encoding.UTF8, "application/json")
|
||||
};
|
||||
// httpRequest.Headers.Accept.Clear();
|
||||
// httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
// httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Basic",
|
||||
// Convert.ToBase64String(Encoding.Default.GetBytes($"{_username}:{_password}")));
|
||||
|
||||
var rawResult = await _httpClient.SendAsync(httpRequest, cts);
|
||||
|
||||
var rawJson = await rawResult.Content.ReadAsStringAsync();
|
||||
rawResult.EnsureSuccessStatusCode();
|
||||
var response = JsonConvert.DeserializeObject<TResponse>(rawJson, jsonSerializer);
|
||||
return response;
|
||||
}
|
||||
|
||||
public class NoRequestModel
|
||||
{
|
||||
public static NoRequestModel Instance = new NoRequestModel();
|
||||
}
|
||||
|
||||
internal class JsonRpcApiException : Exception
|
||||
{
|
||||
public JsonRpcResultError Error { get; set; }
|
||||
|
||||
public override string Message => Error?.Message;
|
||||
}
|
||||
|
||||
public class JsonRpcResultError
|
||||
{
|
||||
[JsonProperty("code")] public int Code { get; set; }
|
||||
[JsonProperty("message")] public string Message { get; set; }
|
||||
[JsonProperty("data")] dynamic Data { get; set; }
|
||||
}
|
||||
internal class JsonRpcResult<T>
|
||||
{
|
||||
|
||||
|
||||
[JsonProperty("result")] public T Result { get; set; }
|
||||
[JsonProperty("error")] public JsonRpcResultError Error { get; set; }
|
||||
[JsonProperty("id")] public string Id { get; set; }
|
||||
}
|
||||
|
||||
internal class JsonRpcCommand<T>
|
||||
{
|
||||
[JsonProperty("jsonRpc")] public string JsonRpc { get; set; } = "2.0";
|
||||
[JsonProperty("id")] public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||
[JsonProperty("method")] public string Method { get; set; }
|
||||
|
||||
[JsonProperty("params")] public T Parameters { get; set; }
|
||||
|
||||
public JsonRpcCommand()
|
||||
{
|
||||
}
|
||||
|
||||
public JsonRpcCommand(string method, T parameters)
|
||||
{
|
||||
Method = method;
|
||||
Parameters = parameters;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
|
||||
{
|
||||
public partial class CreateAccountRequest
|
||||
{
|
||||
[JsonProperty("label")] public string Label { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
|
||||
{
|
||||
public partial class CreateAccountResponse
|
||||
{
|
||||
[JsonProperty("account_index")] public long AccountIndex { get; set; }
|
||||
[JsonProperty("address")] public string Address { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
|
||||
{
|
||||
public partial class CreateAddressRequest
|
||||
{
|
||||
[JsonProperty("account_index")] public long AccountIndex { get; set; }
|
||||
[JsonProperty("label")] public string Label { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
|
||||
{
|
||||
public partial class CreateAddressResponse
|
||||
{
|
||||
[JsonProperty("address")] public string Address { get; set; }
|
||||
[JsonProperty("address_index")] public long AddressIndex { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
|
||||
{
|
||||
public partial class GetAccountsRequest
|
||||
{
|
||||
[JsonProperty("tag")] public string Tag { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
|
||||
{
|
||||
public partial class GetAccountsResponse
|
||||
{
|
||||
[JsonProperty("subaddress_accounts")] public List<SubaddressAccount> SubaddressAccounts { get; set; }
|
||||
[JsonProperty("total_balance")] public decimal TotalBalance { get; set; }
|
||||
|
||||
[JsonProperty("total_unlocked_balance")]
|
||||
public decimal TotalUnlockedBalance { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
|
||||
{
|
||||
public class GetFeeEstimateRequest
|
||||
{
|
||||
[JsonProperty("grace_blocks")] public int? GraceBlocks { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
|
||||
{
|
||||
public class GetFeeEstimateResponse
|
||||
{
|
||||
[JsonProperty("fee")] public long Fee { get; set; }
|
||||
[JsonProperty("status")] public string Status { get; set; }
|
||||
[JsonProperty("untrusted")] public bool Untrusted { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
|
||||
{
|
||||
public partial class GetHeightResponse
|
||||
{
|
||||
[JsonProperty("height")] public long Height { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
|
||||
{
|
||||
public class GetTransferByTransactionIdRequest
|
||||
{
|
||||
[JsonProperty("txid")] public string TransactionId { get; set; }
|
||||
|
||||
[JsonProperty("account_index")] public long AccountIndex { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
|
||||
{
|
||||
public partial class GetTransferByTransactionIdResponse
|
||||
{
|
||||
[JsonProperty("transfer")] public TransferItem Transfer { get; set; }
|
||||
[JsonProperty("transfers")] public IEnumerable<TransferItem> Transfers { get; set; }
|
||||
|
||||
public partial class TransferItem
|
||||
{
|
||||
[JsonProperty("address")] public string Address { get; set; }
|
||||
[JsonProperty("amount")] public long Amount { get; set; }
|
||||
[JsonProperty("confirmations")] public long Confirmations { get; set; }
|
||||
[JsonProperty("double_spend_seen")] public bool DoubleSpendSeen { get; set; }
|
||||
[JsonProperty("height")] public long Height { get; set; }
|
||||
[JsonProperty("note")] public string Note { get; set; }
|
||||
[JsonProperty("payment_id")] public string PaymentId { get; set; }
|
||||
[JsonProperty("subaddr_index")] public SubaddrIndex SubaddrIndex { get; set; }
|
||||
|
||||
[JsonProperty("suggested_confirmations_threshold")]
|
||||
public long SuggestedConfirmationsThreshold { get; set; }
|
||||
|
||||
[JsonProperty("timestamp")] public long Timestamp { get; set; }
|
||||
[JsonProperty("txid")] public string Txid { get; set; }
|
||||
[JsonProperty("type")] public string Type { get; set; }
|
||||
[JsonProperty("unlock_time")] public long UnlockTime { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
|
||||
{
|
||||
public partial class GetTransfersRequest
|
||||
{
|
||||
[JsonProperty("in")] public bool In { get; set; }
|
||||
[JsonProperty("out")] public bool Out { get; set; }
|
||||
[JsonProperty("pending")] public bool Pending { get; set; }
|
||||
[JsonProperty("failed")] public bool Failed { get; set; }
|
||||
[JsonProperty("pool")] public bool Pool { get; set; }
|
||||
[JsonProperty("filter_by_height ")] public bool FilterByHeight { get; set; }
|
||||
[JsonProperty("min_height")] public long MinHeight { get; set; }
|
||||
[JsonProperty("max_height")] public long MaxHeight { get; set; }
|
||||
[JsonProperty("account_index")] public long AccountIndex { get; set; }
|
||||
[JsonProperty("subaddr_indices")] public List<long> SubaddrIndices { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
|
||||
{
|
||||
public partial class GetTransfersResponse
|
||||
{
|
||||
[JsonProperty("in")] public List<GetTransfersResponseItem> In { get; set; }
|
||||
[JsonProperty("out")] public List<GetTransfersResponseItem> Out { get; set; }
|
||||
[JsonProperty("pending")] public List<GetTransfersResponseItem> Pending { get; set; }
|
||||
[JsonProperty("failed")] public List<GetTransfersResponseItem> Failed { get; set; }
|
||||
[JsonProperty("pool")] public List<GetTransfersResponseItem> Pool { get; set; }
|
||||
|
||||
public partial class GetTransfersResponseItem
|
||||
|
||||
{
|
||||
[JsonProperty("address")] public string Address { get; set; }
|
||||
[JsonProperty("amount")] public long Amount { get; set; }
|
||||
[JsonProperty("confirmations")] public long Confirmations { get; set; }
|
||||
[JsonProperty("double_spend_seen")] public bool DoubleSpendSeen { get; set; }
|
||||
[JsonProperty("height")] public long Height { get; set; }
|
||||
[JsonProperty("note")] public string Note { get; set; }
|
||||
[JsonProperty("payment_id")] public string PaymentId { get; set; }
|
||||
[JsonProperty("subaddr_index")] public SubaddrIndex SubaddrIndex { get; set; }
|
||||
|
||||
[JsonProperty("suggested_confirmations_threshold")]
|
||||
public long SuggestedConfirmationsThreshold { get; set; }
|
||||
|
||||
[JsonProperty("timestamp")] public long Timestamp { get; set; }
|
||||
[JsonProperty("txid")] public string Txid { get; set; }
|
||||
[JsonProperty("type")] public string Type { get; set; }
|
||||
[JsonProperty("unlock_time")] public long UnlockTime { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
33
BTCPayServer.Common/Altcoins/Zcash/RPC/Models/Info.cs
Normal file
33
BTCPayServer.Common/Altcoins/Zcash/RPC/Models/Info.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
|
||||
{
|
||||
public partial class Info
|
||||
{
|
||||
[JsonProperty("address")] public string Address { get; set; }
|
||||
[JsonProperty("avg_download")] public long AvgDownload { get; set; }
|
||||
[JsonProperty("avg_upload")] public long AvgUpload { get; set; }
|
||||
[JsonProperty("connection_id")] public string ConnectionId { get; set; }
|
||||
[JsonProperty("current_download")] public long CurrentDownload { get; set; }
|
||||
[JsonProperty("current_upload")] public long CurrentUpload { get; set; }
|
||||
[JsonProperty("height")] public long Height { get; set; }
|
||||
[JsonProperty("host")] public string Host { get; set; }
|
||||
[JsonProperty("incoming")] public bool Incoming { get; set; }
|
||||
[JsonProperty("ip")] public string Ip { get; set; }
|
||||
[JsonProperty("live_time")] public long LiveTime { get; set; }
|
||||
[JsonProperty("local_ip")] public bool LocalIp { get; set; }
|
||||
[JsonProperty("localhost")] public bool Localhost { get; set; }
|
||||
[JsonProperty("peer_id")] public string PeerId { get; set; }
|
||||
|
||||
[JsonProperty("port")]
|
||||
[JsonConverter(typeof(ParseStringConverter))]
|
||||
public long Port { get; set; }
|
||||
|
||||
[JsonProperty("recv_count")] public long RecvCount { get; set; }
|
||||
[JsonProperty("recv_idle_time")] public long RecvIdleTime { get; set; }
|
||||
[JsonProperty("send_count")] public long SendCount { get; set; }
|
||||
[JsonProperty("send_idle_time")] public long SendIdleTime { get; set; }
|
||||
[JsonProperty("state")] public string State { get; set; }
|
||||
[JsonProperty("support_flags")] public long SupportFlags { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
|
||||
{
|
||||
public partial class MakeUriRequest
|
||||
{
|
||||
[JsonProperty("address")] public string Address { get; set; }
|
||||
[JsonProperty("amount")] public long Amount { get; set; }
|
||||
[JsonProperty("payment_id")] public string PaymentId { get; set; }
|
||||
[JsonProperty("tx_description")] public string TxDescription { get; set; }
|
||||
[JsonProperty("recipient_name")] public string RecipientName { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
|
||||
{
|
||||
public partial class MakeUriResponse
|
||||
{
|
||||
[JsonProperty("uri")] public string Uri { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
|
||||
{
|
||||
internal class ParseStringConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type t) => t == typeof(long) || t == typeof(long?);
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.TokenType == JsonToken.Null)
|
||||
return null;
|
||||
var value = serializer.Deserialize<string>(reader);
|
||||
long l;
|
||||
if (Int64.TryParse(value, out l))
|
||||
{
|
||||
return l;
|
||||
}
|
||||
|
||||
throw new Exception("Cannot unmarshal type long");
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer)
|
||||
{
|
||||
if (untypedValue == null)
|
||||
{
|
||||
serializer.Serialize(writer, null);
|
||||
return;
|
||||
}
|
||||
|
||||
var value = (long)untypedValue;
|
||||
serializer.Serialize(writer, value.ToString(CultureInfo.InvariantCulture));
|
||||
return;
|
||||
}
|
||||
|
||||
public static readonly ParseStringConverter Singleton = new ParseStringConverter();
|
||||
}
|
||||
}
|
9
BTCPayServer.Common/Altcoins/Zcash/RPC/Models/Peer.cs
Normal file
9
BTCPayServer.Common/Altcoins/Zcash/RPC/Models/Peer.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
|
||||
{
|
||||
public partial class Peer
|
||||
{
|
||||
[JsonProperty("info")] public Info Info { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
|
||||
{
|
||||
public partial class SubaddrIndex
|
||||
{
|
||||
[JsonProperty("major")] public long Major { get; set; }
|
||||
[JsonProperty("minor")] public long Minor { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
|
||||
{
|
||||
public partial class SubaddressAccount
|
||||
{
|
||||
[JsonProperty("account_index")] public long AccountIndex { get; set; }
|
||||
[JsonProperty("balance")] public decimal Balance { get; set; }
|
||||
[JsonProperty("base_address")] public string BaseAddress { get; set; }
|
||||
[JsonProperty("label")] public string Label { get; set; }
|
||||
[JsonProperty("tag")] public string Tag { get; set; }
|
||||
[JsonProperty("unlocked_balance")] public decimal UnlockedBalance { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Zcash.RPC.Models
|
||||
{
|
||||
public partial class SyncInfoResponse
|
||||
{
|
||||
[JsonProperty("height")] public long Height { get; set; }
|
||||
[JsonProperty("peers")] public List<Peer> Peers { get; set; }
|
||||
[JsonProperty("status")] public string Status { get; set; }
|
||||
[JsonProperty("target_height")] public long? TargetHeight { get; set; }
|
||||
}
|
||||
}
|
20
BTCPayServer.Common/Altcoins/Zcash/Utils/ZcashMoney.cs
Normal file
20
BTCPayServer.Common/Altcoins/Zcash/Utils/ZcashMoney.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Zcash.Utils
|
||||
{
|
||||
public class ZcashMoney
|
||||
{
|
||||
public static decimal Convert(long zat)
|
||||
{
|
||||
var amt = zat.ToString(CultureInfo.InvariantCulture).PadLeft(8, '0');
|
||||
amt = amt.Length == 8 ? $"0.{amt}" : amt.Insert(amt.Length - 8, ".");
|
||||
|
||||
return decimal.Parse(amt, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
public static long Convert(decimal Zcash)
|
||||
{
|
||||
return System.Convert.ToInt64(Zcash * 100000000);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class ZcashLikeSpecificBtcPayNetwork : BTCPayNetworkBase
|
||||
{
|
||||
public int MaxTrackedConfirmation = 10;
|
||||
public string UriScheme { get; set; }
|
||||
}
|
||||
}
|
@ -127,7 +127,6 @@ namespace BTCPayServer
|
||||
public string BlockExplorerLinkDefault { get; set; }
|
||||
public string DisplayName { get; set; }
|
||||
public int Divisibility { get; set; } = 8;
|
||||
[Obsolete("Should not be needed")]
|
||||
public bool IsBTC
|
||||
{
|
||||
get
|
||||
|
@ -55,7 +55,7 @@ namespace BTCPayServer
|
||||
InitGroestlcoin();
|
||||
InitViacoin();
|
||||
InitMonero();
|
||||
InitPolis();
|
||||
InitZcash();
|
||||
InitChaincoin();
|
||||
// InitArgoneum();//their rate source is down 9/15/20.
|
||||
InitMonetaryUnit();
|
||||
@ -92,8 +92,8 @@ namespace BTCPayServer
|
||||
return new BTCPayNetworkProvider(this, cryptoCodes);
|
||||
}
|
||||
|
||||
[Obsolete("To use only for legacy stuff")]
|
||||
public BTCPayNetwork BTC => GetNetwork<BTCPayNetwork>("BTC");
|
||||
public BTCPayNetworkBase DefaultNetwork => BTC ?? GetAll().First();
|
||||
|
||||
public void Add(BTCPayNetwork network)
|
||||
{
|
||||
|
@ -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>
|
||||
|
@ -1260,14 +1260,21 @@
|
||||
"crypto":true
|
||||
},
|
||||
{
|
||||
"name":"POLIS",
|
||||
"code":"POLIS",
|
||||
"name":"YEC",
|
||||
"code":"YEC",
|
||||
"divisibility":8,
|
||||
"symbol":null,
|
||||
"crypto":true
|
||||
},
|
||||
{
|
||||
"name":"Althash",
|
||||
"name":"ZEC",
|
||||
"code":"ZEC",
|
||||
"divisibility":8,
|
||||
"symbol":null,
|
||||
"crypto":true
|
||||
},
|
||||
{
|
||||
"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")));
|
||||
|
||||
|
||||
|
@ -350,7 +350,7 @@ namespace BTCPayServer.Tests
|
||||
var user = s.Server.NewAccount();
|
||||
await user.GrantAccessAsync();
|
||||
s.GoToLogin();
|
||||
s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password);
|
||||
s.LogIn(user.RegisterDetails.Email, user.RegisterDetails.Password);
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
await s.Server.ExplorerNode.GenerateAsync(1);
|
||||
|
||||
@ -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)
|
||||
@ -43,50 +43,54 @@ namespace BTCPayServer.Tests
|
||||
await user.GrantAccessAsync();
|
||||
await user.MakeAdmin(false);
|
||||
s.GoToLogin();
|
||||
s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password);
|
||||
s.LogIn(user.RegisterDetails.Email, user.RegisterDetails.Password);
|
||||
s.GoToProfile(ManageNavPages.APIKeys);
|
||||
s.Driver.FindElement(By.Id("AddApiKey")).Click();
|
||||
|
||||
TestLogs.LogInformation("Checking admin permissions");
|
||||
//not an admin, so this permission should not show
|
||||
Assert.DoesNotContain("btcpay.server.canmodifyserversettings", s.Driver.PageSource);
|
||||
await user.MakeAdmin();
|
||||
s.Logout();
|
||||
s.GoToLogin();
|
||||
s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password);
|
||||
s.LogIn(user.RegisterDetails.Email, user.RegisterDetails.Password);
|
||||
s.GoToProfile(ManageNavPages.APIKeys);
|
||||
s.Driver.FindElement(By.Id("AddApiKey")).Click();
|
||||
Assert.Contains("btcpay.server.canmodifyserversettings", s.Driver.PageSource);
|
||||
|
||||
//server management should show now
|
||||
s.Driver.SetCheckbox(By.Id("btcpay.server.canmodifyserversettings"), true);
|
||||
s.Driver.SetCheckbox(By.Id("btcpay.store.canmodifystoresettings"), true);
|
||||
s.Driver.SetCheckbox(By.Id("btcpay.user.canviewprofile"), true);
|
||||
s.Driver.FindElement(By.Id("Generate")).Click();
|
||||
var superApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
|
||||
|
||||
TestLogs.LogInformation("Checking super admin key");
|
||||
|
||||
//this api key has access to everything
|
||||
await TestApiAgainstAccessToken(superApiKey, tester, user, Policies.CanModifyServerSettings, Policies.CanModifyStoreSettings, Policies.CanViewProfile);
|
||||
|
||||
|
||||
s.Driver.FindElement(By.Id("AddApiKey")).Click();
|
||||
s.Driver.SetCheckbox(By.Id("btcpay.server.canmodifyserversettings"), true);
|
||||
s.Driver.FindElement(By.Id("Generate")).Click();
|
||||
var serverOnlyApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
|
||||
|
||||
TestLogs.LogInformation("Checking CanModifyServerSettings permissions");
|
||||
|
||||
await TestApiAgainstAccessToken(serverOnlyApiKey, tester, user,
|
||||
Policies.CanModifyServerSettings);
|
||||
|
||||
|
||||
|
||||
s.Driver.FindElement(By.Id("AddApiKey")).Click();
|
||||
s.Driver.SetCheckbox(By.Id("btcpay.store.canmodifystoresettings"), true);
|
||||
s.Driver.FindElement(By.Id("Generate")).Click();
|
||||
var allStoreOnlyApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
|
||||
|
||||
TestLogs.LogInformation("Checking CanModifyStoreSettings permissions");
|
||||
|
||||
await TestApiAgainstAccessToken(allStoreOnlyApiKey, tester, user,
|
||||
Policies.CanModifyStoreSettings);
|
||||
|
||||
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")
|
||||
@ -95,14 +99,21 @@ 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");
|
||||
|
||||
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: {noPermissionsApiKey}");
|
||||
await TestApiAgainstAccessToken(noPermissionsApiKey, tester, user);
|
||||
|
||||
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
|
||||
@ -110,6 +121,8 @@ namespace BTCPayServer.Tests
|
||||
await TestApiAgainstAccessToken<bool>("incorrect key", $"{TestApiPath}/me/id",
|
||||
tester.PayTester.HttpClient);
|
||||
});
|
||||
|
||||
TestLogs.LogInformation("Checking authorize screen");
|
||||
|
||||
//let's test the authorized screen now
|
||||
//options for authorize are:
|
||||
@ -124,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());
|
||||
@ -153,45 +175,63 @@ 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();
|
||||
s.LogIn(user.RegisterDetails.Email, user.RegisterDetails.Password);
|
||||
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;
|
||||
var apikeydata = await TestApiAgainstAccessToken<ApiKeyData>(allAPIKey, $"api/v1/api-keys/current", tester.PayTester.HttpClient);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -202,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);
|
||||
@ -220,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 =
|
||||
@ -287,7 +332,6 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
else if (!permissions.Contains(unrestricted))
|
||||
{
|
||||
|
||||
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
|
||||
{
|
||||
await TestApiAgainstAccessToken<bool>(accessToken,
|
||||
@ -302,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 () =>
|
||||
@ -315,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,
|
||||
@ -331,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,14 +125,28 @@ 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
|
||||
try
|
||||
{
|
||||
driver.FindElement(selector).Click();
|
||||
return;
|
||||
}
|
||||
catch { }
|
||||
|
||||
// Sometimes, selenium complain, so we enter hack territory
|
||||
var wait = new WebDriverWait(driver, SeleniumTester.ImplicitWait);
|
||||
wait.UntilJsIsReady();
|
||||
|
||||
@ -158,22 +170,8 @@ retry:
|
||||
public static void SetCheckbox(this IWebDriver driver, By selector, bool value)
|
||||
{
|
||||
var element = driver.FindElement(selector);
|
||||
if ((value && !element.Selected) || (!value && element.Selected))
|
||||
{
|
||||
try
|
||||
{
|
||||
driver.WaitForAndClick(selector);
|
||||
}
|
||||
catch (ElementClickInterceptedException)
|
||||
{
|
||||
element.SendKeys(" ");
|
||||
}
|
||||
}
|
||||
|
||||
if (value != element.Selected)
|
||||
{
|
||||
driver.SetCheckbox(selector, value);
|
||||
}
|
||||
driver.WaitForAndClick(selector);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
{
|
||||
@ -204,8 +202,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
// Local link, this is fine
|
||||
}
|
||||
else if (attributeValue.StartsWith("http://") || attributeValue.StartsWith("https://") ||
|
||||
attributeValue.StartsWith("@"))
|
||||
else if (attributeValue.StartsWith("http://") || attributeValue.StartsWith("https://"))
|
||||
{
|
||||
// This can be an external link. Treating it as such.
|
||||
var rel = GetAttributeValue(node, "rel");
|
||||
@ -1766,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")]
|
||||
@ -361,6 +455,7 @@ namespace BTCPayServer.Tests
|
||||
var result = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest()
|
||||
{
|
||||
Name = "Test",
|
||||
Description = "Test description",
|
||||
Amount = 12.3m,
|
||||
Currency = "BTC",
|
||||
PaymentMethods = new[] { "BTC" }
|
||||
@ -369,6 +464,7 @@ namespace BTCPayServer.Tests
|
||||
void VerifyResult()
|
||||
{
|
||||
Assert.Equal("Test", result.Name);
|
||||
Assert.Equal("Test description", result.Description);
|
||||
Assert.Null(result.Period);
|
||||
// If it contains ? it means that we are resolving an unknown route with the link generator
|
||||
Assert.DoesNotContain("?", result.ViewLink);
|
||||
@ -1097,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,
|
||||
@ -1223,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 () =>
|
||||
@ -1230,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 () =>
|
||||
{
|
||||
@ -1435,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()
|
||||
@ -1591,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",
|
||||
@ -2014,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 () =>
|
||||
{
|
||||
@ -2111,5 +2246,96 @@ namespace BTCPayServer.Tests
|
||||
|
||||
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task StoreUsersAPITest()
|
||||
{
|
||||
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
|
||||
var user = tester.NewAccount();
|
||||
await user.GrantAccessAsync(true);
|
||||
|
||||
var client = await user.CreateClient(Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings);
|
||||
|
||||
var users = await client.GetStoreUsers(user.StoreId);
|
||||
var storeuser = Assert.Single(users);
|
||||
Assert.Equal(user.UserId,storeuser.UserId);
|
||||
Assert.Equal(StoreRoles.Owner,storeuser.Role);
|
||||
|
||||
var user2= tester.NewAccount();
|
||||
await user2.GrantAccessAsync(false);
|
||||
|
||||
var user2Client =await user2.CreateClient(Policies.CanModifyStoreSettings);
|
||||
|
||||
//test no access to api when unrelated to store at all
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.GetStoreUsers(user.StoreId));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.AddStoreUser(user.StoreId, new StoreUserData()));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.RemoveStoreUser(user.StoreId, user.UserId));
|
||||
|
||||
await client.AddStoreUser(user.StoreId, new StoreUserData() { Role = StoreRoles.Guest, UserId = user2.UserId });
|
||||
|
||||
//test no access to api when only a guest
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.GetStoreUsers(user.StoreId));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.AddStoreUser(user.StoreId, new StoreUserData()));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.RemoveStoreUser(user.StoreId, user.UserId));
|
||||
|
||||
await user2Client.GetStore(user.StoreId);
|
||||
|
||||
await client.RemoveStoreUser(user.StoreId, user2.UserId);
|
||||
await AssertHttpError(403, async () =>
|
||||
await user2Client.GetStore(user.StoreId));
|
||||
|
||||
|
||||
await client.AddStoreUser(user.StoreId, new StoreUserData() { Role = StoreRoles.Owner, UserId = user2.UserId });
|
||||
await AssertAPIError("duplicate-store-user-role",async ()=>
|
||||
await client.AddStoreUser(user.StoreId,
|
||||
new StoreUserData() { Role = StoreRoles.Owner, UserId = user2.UserId }));
|
||||
await user2Client.RemoveStoreUser(user.StoreId, user.UserId);
|
||||
|
||||
|
||||
//test no access to api when unrelated to store at all
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await client.GetStoreUsers(user.StoreId));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await client.AddStoreUser(user.StoreId, new StoreUserData()));
|
||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await client.RemoveStoreUser(user.StoreId, user.UserId));
|
||||
|
||||
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.Parameters.Single(p => p.Key == "psbt" || p.Key == "SigningContext.PSBT").Value;
|
||||
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;
|
||||
@ -85,6 +84,11 @@ namespace BTCPayServer.Tests
|
||||
Driver.AssertNoError();
|
||||
}
|
||||
|
||||
public void PayInvoice()
|
||||
{
|
||||
Driver.FindElement(By.Id("FakePayment")).Click();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use this ServerUri when trying to browse with selenium
|
||||
/// Because for some reason, the selenium container can't resolve the tests container domain name
|
||||
@ -139,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)
|
||||
{
|
||||
@ -151,6 +162,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
Driver.WaitForElement(By.Id("StoreSelectorCreate")).Click();
|
||||
var name = "Store" + RandomUtils.GetUInt64();
|
||||
TestLogs.LogInformation($"Created store {name}");
|
||||
Driver.WaitForElement(By.Id("Name")).SendKeys(name);
|
||||
Driver.WaitForElement(By.Id("Create")).Click();
|
||||
Driver.FindElement(By.Id("StoreNav-StoreSettings")).Click();
|
||||
@ -161,7 +173,7 @@ namespace BTCPayServer.Tests
|
||||
return (name, storeId);
|
||||
}
|
||||
|
||||
public Mnemonic GenerateWallet(string cryptoCode = "BTC", string seed = "", bool importkeys = false, bool privkeys = false, ScriptPubKeyType format = ScriptPubKeyType.Segwit)
|
||||
public Mnemonic GenerateWallet(string cryptoCode = "BTC", string seed = "", bool? importkeys = null, bool isHotWallet = false, ScriptPubKeyType format = ScriptPubKeyType.Segwit)
|
||||
{
|
||||
var isImport = !string.IsNullOrEmpty(seed);
|
||||
GoToWalletSettings(cryptoCode);
|
||||
@ -181,11 +193,11 @@ namespace BTCPayServer.Tests
|
||||
Driver.FindElement(By.Id("ImportWalletOptionsLink")).Click();
|
||||
Driver.FindElement(By.Id("ImportSeedLink")).Click();
|
||||
Driver.FindElement(By.Id("ExistingMnemonic")).SendKeys(seed);
|
||||
Driver.SetCheckbox(By.Id("SavePrivateKeys"), privkeys);
|
||||
Driver.SetCheckbox(By.Id("SavePrivateKeys"), isHotWallet);
|
||||
}
|
||||
else
|
||||
{
|
||||
var option = privkeys ? "Hotwallet" : "Watchonly";
|
||||
var option = isHotWallet ? "Hotwallet" : "Watchonly";
|
||||
TestLogs.LogInformation($"Generating new seed ({option})");
|
||||
Driver.FindElement(By.Id("GenerateWalletLink")).Click();
|
||||
Driver.FindElement(By.Id($"Generate{option}Link")).Click();
|
||||
@ -195,7 +207,8 @@ namespace BTCPayServer.Tests
|
||||
Driver.FindElement(By.CssSelector($"#ScriptPubKeyType option[value={format}]")).Click();
|
||||
|
||||
Driver.ToggleCollapse("AdvancedSettings");
|
||||
Driver.SetCheckbox(By.Id("ImportKeysToRPC"), importkeys);
|
||||
if (importkeys is bool v)
|
||||
Driver.SetCheckbox(By.Id("ImportKeysToRPC"), v);
|
||||
Driver.FindElement(By.Id("Continue")).Click();
|
||||
|
||||
if (isImport)
|
||||
@ -228,7 +241,10 @@ namespace BTCPayServer.Tests
|
||||
/// <param name="derivationScheme"></param>
|
||||
public void AddDerivationScheme(string cryptoCode = "BTC", string derivationScheme = "xpub661MyMwAqRbcGABgHMUXDzPzH1tU7eZaAaJQXhDXsSxsqyQzQeU6kznNfSuAyqAK9UaWSaZaMFdNiY5BCF4zBPAzSnwfUAwUhwttuAKwfRX-[legacy]")
|
||||
{
|
||||
GoToWalletSettings(cryptoCode);
|
||||
if (!Driver.PageSource.Contains($"Setup {cryptoCode} Wallet"))
|
||||
{
|
||||
GoToWalletSettings(cryptoCode);
|
||||
}
|
||||
|
||||
Driver.FindElement(By.Id("ImportWalletOptionsLink")).Click();
|
||||
Driver.FindElement(By.Id("ImportXpubLink")).Click();
|
||||
@ -251,10 +267,9 @@ namespace BTCPayServer.Tests
|
||||
public void AddLightningNode(string cryptoCode = null, LightningConnectionType? connectionType = null, bool test = true)
|
||||
{
|
||||
cryptoCode ??= "BTC";
|
||||
Driver.FindElement(By.Id($"StoreNav-Lightning{cryptoCode}")).Click();
|
||||
if (Driver.PageSource.Contains("id=\"SetupLightningNodeLink\""))
|
||||
if (!Driver.PageSource.Contains("Connect to a Lightning node"))
|
||||
{
|
||||
Driver.FindElement(By.Id("SetupLightningNodeLink")).Click();
|
||||
GoToLightningSettings();
|
||||
}
|
||||
|
||||
var connectionString = connectionType switch
|
||||
@ -349,7 +364,7 @@ namespace BTCPayServer.Tests
|
||||
Driver.FindElement(By.Id("Nav-Logout")).Click();
|
||||
}
|
||||
|
||||
public void Login(string user, string password)
|
||||
public void LogIn(string user, string password)
|
||||
{
|
||||
Driver.FindElement(By.Id("Email")).SendKeys(user);
|
||||
Driver.FindElement(By.Id("Password")).SendKeys(password);
|
||||
@ -364,7 +379,12 @@ namespace BTCPayServer.Tests
|
||||
public void GoToStore(string storeId, StoreNavPages storeNavPage = StoreNavPages.General)
|
||||
{
|
||||
if (storeId is not null)
|
||||
{
|
||||
GoToUrl($"/stores/{storeId}/");
|
||||
StoreId = storeId;
|
||||
if (WalletId != null)
|
||||
WalletId = new WalletId(storeId, WalletId.CryptoCode);
|
||||
}
|
||||
|
||||
Driver.FindElement(By.Id("StoreNav-StoreSettings")).Click();
|
||||
|
||||
@ -388,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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -410,8 +430,9 @@ namespace BTCPayServer.Tests
|
||||
Driver.FindElement(By.Id($"StoreSelectorMenuItem-{storeId}")).Click();
|
||||
}
|
||||
|
||||
public void GoToInvoiceCheckout(string invoiceId)
|
||||
public void GoToInvoiceCheckout(string invoiceId = null)
|
||||
{
|
||||
invoiceId ??= InvoiceId;
|
||||
Driver.FindElement(By.Id("StoreNav-Invoices")).Click();
|
||||
Driver.FindElement(By.Id($"invoice-checkout-{invoiceId}")).Click();
|
||||
CheckForJSErrors();
|
||||
@ -431,16 +452,17 @@ namespace BTCPayServer.Tests
|
||||
else
|
||||
{
|
||||
GoToUrl(storeId == null ? "/invoices/" : $"/stores/{storeId}/invoices/");
|
||||
StoreId = storeId;
|
||||
}
|
||||
}
|
||||
|
||||
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()}"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -471,6 +493,7 @@ namespace BTCPayServer.Tests
|
||||
)
|
||||
{
|
||||
GoToInvoices(storeId);
|
||||
|
||||
Driver.FindElement(By.Id("CreateNewInvoice")).Click();
|
||||
if (amount is decimal v)
|
||||
Driver.FindElement(By.Id("Amount")).SendKeys(v.ToString(CultureInfo.InvariantCulture));
|
||||
@ -485,10 +508,14 @@ namespace BTCPayServer.Tests
|
||||
Driver.FindElement(By.Id("Create")).Click();
|
||||
|
||||
var statusElement = FindAlertMessage(expectedSeverity);
|
||||
return expectedSeverity == StatusMessageModel.StatusSeverity.Success ? statusElement.Text.Split(" ")[1] : null;
|
||||
var inv = expectedSeverity == StatusMessageModel.StatusSeverity.Success ? statusElement.Text.Split(" ")[1] : null;
|
||||
InvoiceId = inv;
|
||||
TestLogs.LogInformation($"Created invoice {inv}");
|
||||
return inv;
|
||||
}
|
||||
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);
|
||||
@ -497,24 +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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void PayInvoice(WalletId walletId, string invoiceId)
|
||||
{
|
||||
GoToInvoiceCheckout(invoiceId);
|
||||
var bip21 = Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn"))
|
||||
.GetAttribute("href");
|
||||
Assert.Contains($"{PayjoinClient.BIP21EndpointKey}", bip21);
|
||||
|
||||
GoToWallet(walletId);
|
||||
Driver.FindElement(By.Id("bip21parse")).Click();
|
||||
Driver.SwitchTo().Alert().SendKeys(bip21);
|
||||
Driver.SwitchTo().Alert().Accept();
|
||||
Driver.FindElement(By.Id("SignTransaction")).Click();
|
||||
Driver.FindElement(By.Id("SignWithSeed")).Click();
|
||||
Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
|
||||
Driver.Navigate().Refresh();
|
||||
return addressStr;
|
||||
}
|
||||
|
||||
private void CheckForJSErrors()
|
||||
@ -544,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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -64,6 +64,48 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("Starting listening NBXplorer", s.Driver.PageSource);
|
||||
s.Driver.Quit();
|
||||
}
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
public async Task CanUseCPFP()
|
||||
{
|
||||
using var s = CreateSeleniumTester();
|
||||
await s.StartAsync();
|
||||
s.RegisterNewUser(true);
|
||||
s.CreateNewStore();
|
||||
s.GenerateWallet(isHotWallet: true);
|
||||
await s.FundStoreWallet();
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
s.CreateInvoice();
|
||||
s.GoToInvoiceCheckout();
|
||||
s.PayInvoice();
|
||||
s.GoToInvoices(s.StoreId);
|
||||
}
|
||||
// Let's CPFP from the invoices page
|
||||
s.Driver.SetCheckbox(By.Id("selectAllCheckbox"), true);
|
||||
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
|
||||
s.Driver.FindElement(By.Id("BumpFee")).Click();
|
||||
s.Driver.FindElement(By.Id("BroadcastTransaction")).Click();
|
||||
s.FindAlertMessage();
|
||||
Assert.Contains($"/stores/{s.StoreId}/invoices", s.Driver.Url);
|
||||
|
||||
// CPFP again should fail because all invoices got bumped
|
||||
s.GoToInvoices();
|
||||
s.Driver.SetCheckbox(By.Id("selectAllCheckbox"), true);
|
||||
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
|
||||
s.Driver.FindElement(By.Id("BumpFee")).Click();
|
||||
var err = s.FindAlertMessage(StatusMessageModel.StatusSeverity.Error);
|
||||
Assert.Contains("any UTXO available", err.Text);
|
||||
Assert.Contains($"/stores/{s.StoreId}/invoices", s.Driver.Url);
|
||||
|
||||
// But we should be able to bump from the wallet's page
|
||||
s.GoToWallet(navPages: WalletsNavPages.Transactions);
|
||||
s.Driver.SetCheckbox(By.Id("selectAllCheckbox"), true);
|
||||
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
|
||||
s.Driver.FindElement(By.Id("BumpFee")).Click();
|
||||
s.Driver.FindElement(By.Id("BroadcastTransaction")).Click();
|
||||
s.FindAlertMessage();
|
||||
Assert.Contains($"/wallets/{s.WalletId}", s.Driver.Url);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Lightning", "Lightning")]
|
||||
@ -112,7 +154,7 @@ namespace BTCPayServer.Tests
|
||||
await u2.MakeAdmin(false);
|
||||
|
||||
s.GoToLogin();
|
||||
s.Login(u1.RegisterDetails.Email, u1.RegisterDetails.Password);
|
||||
s.LogIn(u1.RegisterDetails.Email, u1.RegisterDetails.Password);
|
||||
s.GoToProfile();
|
||||
s.Driver.FindElement(By.Id("Email")).Clear();
|
||||
s.Driver.FindElement(By.Id("Email")).SendKeys(u2.RegisterDetails.Email);
|
||||
@ -344,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();
|
||||
@ -407,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);
|
||||
@ -420,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();
|
||||
@ -431,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();
|
||||
@ -576,15 +621,16 @@ namespace BTCPayServer.Tests
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
public async Task CanCreateAppPoS()
|
||||
{
|
||||
using var s = CreateSeleniumTester();
|
||||
using var s = CreateSeleniumTester(newDb: true);
|
||||
await s.StartAsync();
|
||||
s.RegisterNewUser();
|
||||
var userId = s.RegisterNewUser(true);
|
||||
s.CreateNewStore();
|
||||
|
||||
s.Driver.FindElement(By.Id("StoreNav-CreateApp")).Click();
|
||||
s.Driver.FindElement(By.Name("AppName")).SendKeys("PoS" + Guid.NewGuid());
|
||||
s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("Point of Sale");
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
Assert.Contains("App successfully created", s.FindAlertMessage().Text);
|
||||
|
||||
s.Driver.FindElement(By.CssSelector(".template-item:nth-of-type(1) .btn-primary")).Click();
|
||||
s.Driver.FindElement(By.Id("BuyButtonText")).SendKeys("Take my money");
|
||||
s.Driver.FindElement(By.Id("SaveItemChanges")).Click();
|
||||
@ -595,9 +641,14 @@ namespace BTCPayServer.Tests
|
||||
|
||||
s.Driver.FindElement(By.Id("DefaultView")).SendKeys("Item list and cart");
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).Click();
|
||||
s.Driver.FindElement(By.Id("ViewApp")).Click();
|
||||
Assert.Contains("App updated", s.FindAlertMessage().Text);
|
||||
|
||||
var posBaseUrl = s.Driver.Url.Replace("/Cart", "");
|
||||
s.Driver.FindElement(By.Id("ViewApp")).Click();
|
||||
var windows = s.Driver.WindowHandles;
|
||||
Assert.Equal(2, windows.Count);
|
||||
s.Driver.SwitchTo().Window(windows[1]);
|
||||
|
||||
var posBaseUrl = s.Driver.Url.Replace("/cart", "");
|
||||
Assert.True(s.Driver.PageSource.Contains("Tea shop"), "Unable to create PoS");
|
||||
Assert.True(s.Driver.PageSource.Contains("Cart"), "PoS not showing correct default view");
|
||||
Assert.True(s.Driver.PageSource.Contains("Take my money"), "PoS not showing correct default view");
|
||||
@ -607,6 +658,48 @@ namespace BTCPayServer.Tests
|
||||
|
||||
s.Driver.Url = posBaseUrl + "/cart";
|
||||
Assert.True(s.Driver.PageSource.Contains("Cart"), "Cart PoS not showing correct view");
|
||||
|
||||
// Let's set change the root app
|
||||
s.GoToHome();
|
||||
s.GoToServer(ServerNavPages.Policies);
|
||||
s.Driver.ScrollTo(By.Id("RootAppId"));
|
||||
var select = new SelectElement(s.Driver.FindElement(By.Id("RootAppId")));
|
||||
select.SelectByText("Point of", true);
|
||||
s.Driver.FindElement(By.Id("SaveButton")).Click();
|
||||
s.FindAlertMessage();
|
||||
|
||||
s.Logout();
|
||||
s.GoToLogin();
|
||||
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 explicitly going to /
|
||||
s.GoToUrl("/");
|
||||
Assert.Contains("Tea shop", s.Driver.PageSource);
|
||||
s.Driver.Navigate().Back();
|
||||
|
||||
// Let's check with domain mapping as well.
|
||||
s.GoToServer(ServerNavPages.Policies);
|
||||
s.Driver.ScrollTo(By.Id("RootAppId"));
|
||||
select = new SelectElement(s.Driver.FindElement(By.Id("RootAppId")));
|
||||
select.SelectByText("None", true);
|
||||
s.Driver.FindElement(By.Id("SaveButton")).Click();
|
||||
s.Driver.ScrollTo(By.Id("RootAppId"));
|
||||
s.Driver.FindElement(By.Id("AddDomainButton")).Click();
|
||||
s.Driver.FindElement(By.Id("DomainToAppMapping_0__Domain")).SendKeys(new Uri(s.Driver.Url, UriKind.Absolute).DnsSafeHost);
|
||||
select = new SelectElement(s.Driver.FindElement(By.Id("DomainToAppMapping_0__AppId")));
|
||||
select.SelectByText("Point of", true);
|
||||
s.Driver.FindElement(By.Id("SaveButton")).Click();
|
||||
|
||||
s.Logout();
|
||||
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 explicitly going to /
|
||||
s.GoToUrl("/");
|
||||
Assert.Contains("Tea shop", s.Driver.PageSource);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
@ -622,14 +715,26 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Name("AppName")).SendKeys("CF" + Guid.NewGuid());
|
||||
s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("Crowdfund");
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
Assert.Contains("App successfully created", s.FindAlertMessage().Text);
|
||||
|
||||
s.Driver.FindElement(By.Id("Title")).SendKeys("Kukkstarter");
|
||||
s.Driver.FindElement(By.CssSelector("div.note-editable.card-block")).SendKeys("1BTC = 1BTC");
|
||||
s.Driver.FindElement(By.Id("TargetCurrency")).Clear();
|
||||
s.Driver.FindElement(By.Id("TargetCurrency")).SendKeys("JPY");
|
||||
s.Driver.FindElement(By.Id("TargetAmount")).SendKeys("700");
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).Click();
|
||||
Assert.Contains("App updated", s.FindAlertMessage().Text);
|
||||
|
||||
s.Driver.FindElement(By.Id("ViewApp")).Click();
|
||||
var windows = s.Driver.WindowHandles;
|
||||
Assert.Equal(2, windows.Count);
|
||||
s.Driver.SwitchTo().Window(windows[1]);
|
||||
|
||||
Assert.Equal("currently active!",
|
||||
s.Driver.FindElement(By.CssSelector("[data-test='time-state']")).Text);
|
||||
|
||||
s.Driver.Close();
|
||||
s.Driver.SwitchTo().Window(windows[0]);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
@ -645,11 +750,14 @@ 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();
|
||||
var aaa = s.Driver.PageSource;
|
||||
var url = s.Driver.Url;
|
||||
s.Driver.FindElement(By.Id("ViewAppButton")).Click();
|
||||
s.Driver.FindElement(By.Id("ViewPaymentRequest")).Click();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
|
||||
Assert.Equal("Amount due", s.Driver.FindElement(By.CssSelector("[data-test='amount-due-title']")).Text);
|
||||
Assert.Equal("Pay Invoice",
|
||||
@ -672,6 +780,21 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.AssertElementNotFound(By.CssSelector("[data-test='status']"));
|
||||
Assert.Equal("Pay Invoice",
|
||||
s.Driver.FindElement(By.CssSelector("[data-test='pay-button']")).Text.Trim());
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
|
||||
|
||||
// archive (from details page)
|
||||
var payReqId = s.Driver.Url.Split('/').Last();
|
||||
s.Driver.FindElement(By.Id("ArchivePaymentRequest")).Click();
|
||||
Assert.Contains("The payment request has been archived", s.FindAlertMessage().Text);
|
||||
Assert.DoesNotContain("Pay123", s.Driver.PageSource);
|
||||
s.Driver.FindElement(By.Id("SearchDropdownToggle")).Click();
|
||||
s.Driver.FindElement(By.Id("SearchIncludeArchived")).Click();
|
||||
Assert.Contains("Pay123", s.Driver.PageSource);
|
||||
|
||||
// unarchive (from list)
|
||||
s.Driver.FindElement(By.Id($"ToggleArchival-{payReqId}")).Click();
|
||||
Assert.Contains("The payment request has been unarchived", s.FindAlertMessage().Text);
|
||||
Assert.Contains("Pay123", s.Driver.PageSource);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
@ -861,7 +984,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
var cryptoCode = "BTC";
|
||||
s.CreateNewStore();
|
||||
s.GenerateWallet(cryptoCode, "melody lizard phrase voice unique car opinion merge degree evil swift cargo", privkeys: isHotwallet);
|
||||
s.GenerateWallet(cryptoCode, "melody lizard phrase voice unique car opinion merge degree evil swift cargo", isHotWallet: isHotwallet);
|
||||
s.GoToWalletSettings(cryptoCode);
|
||||
if (isHotwallet)
|
||||
Assert.Contains("View seed", s.Driver.PageSource);
|
||||
@ -885,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
|
||||
@ -958,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();
|
||||
@ -983,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);
|
||||
@ -1000,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();
|
||||
@ -1351,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);
|
||||
@ -1363,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 () =>
|
||||
{
|
||||
@ -1383,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();
|
||||
@ -1491,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);
|
||||
@ -1529,10 +1663,19 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Name("AppName")).SendKeys("CF" + Guid.NewGuid());
|
||||
s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("Crowdfund");
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
Assert.Contains("App successfully created", s.FindAlertMessage().Text);
|
||||
|
||||
s.Driver.FindElement(By.Id("Title")).SendKeys("Kukkstarter");
|
||||
s.Driver.FindElement(By.CssSelector("div.note-editable.card-block")).SendKeys("1BTC = 1BTC");
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).Click();
|
||||
Assert.Contains("App updated", s.FindAlertMessage().Text);
|
||||
|
||||
s.Driver.FindElement(By.Id("ViewApp")).Click();
|
||||
|
||||
var windows = s.Driver.WindowHandles;
|
||||
Assert.Equal(2, windows.Count);
|
||||
s.Driver.SwitchTo().Window(windows[1]);
|
||||
|
||||
s.Driver.FindElement(By.CssSelector("#crowdfund-body-contribution-container .perk")).Click();
|
||||
s.Driver.FindElement(By.PartialLinkText("LNURL")).Click();
|
||||
lnurl = s.Driver.FindElement(By.ClassName("lnurl"))
|
||||
@ -1540,7 +1683,8 @@ namespace BTCPayServer.Tests
|
||||
|
||||
LNURL.LNURL.Parse(lnurl, out tag);
|
||||
|
||||
|
||||
s.Driver.Close();
|
||||
s.Driver.SwitchTo().Window(windows[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -1552,7 +1696,6 @@ namespace BTCPayServer.Tests
|
||||
s.Server.ActivateLightning();
|
||||
await s.StartAsync();
|
||||
await s.Server.EnsureChannelsSetup();
|
||||
var cryptoCode = "BTC";
|
||||
s.RegisterNewUser(true);
|
||||
//ln address tests
|
||||
s.CreateNewStore();
|
||||
@ -1687,7 +1830,7 @@ retry:
|
||||
TestUtils.Eventually(() => s.FindAlertMessage());
|
||||
|
||||
s.Logout();
|
||||
s.Login(user, "123456");
|
||||
s.LogIn(user, "123456");
|
||||
var section = s.Driver.FindElement(By.Id("lnurlauth-section"));
|
||||
links = section.FindElements(By.CssSelector(".tab-content a")).Select(element => element.GetAttribute("href"));
|
||||
Assert.Equal(2,links.Count());
|
||||
|
@ -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
|
||||
@ -191,7 +194,8 @@ namespace BTCPayServer.Tests
|
||||
private async Task AssertLinkNotDead(HttpClient httpClient, string url, string file)
|
||||
{
|
||||
var uri = new Uri(url);
|
||||
|
||||
int retryLeft = 3;
|
||||
retry:
|
||||
try
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, uri);
|
||||
@ -200,11 +204,6 @@ namespace BTCPayServer.Tests
|
||||
request.Headers.TryAddWithoutValidation("User-Agent",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:75.0) Gecko/20100101 Firefox/75.0");
|
||||
var response = await httpClient.SendAsync(request);
|
||||
if (response.StatusCode == HttpStatusCode.ServiceUnavailable) // Temporary issue
|
||||
{
|
||||
TestLogs.LogInformation($"Unavailable: {url} ({file})");
|
||||
return;
|
||||
}
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
if (uri.Fragment.Length != 0)
|
||||
{
|
||||
@ -222,8 +221,13 @@ namespace BTCPayServer.Tests
|
||||
|
||||
throw;
|
||||
}
|
||||
catch (Exception) when (retryLeft > 0)
|
||||
{
|
||||
goto retry;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
retryLeft--;
|
||||
var details = ex is EqualException ? (ex as EqualException).Actual : ex.Message;
|
||||
TestLogs.LogInformation($"FAILED: {url} ({file}) {details}");
|
||||
|
||||
@ -250,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);
|
||||
@ -260,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"
|
||||
@ -235,7 +236,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.14.1-beta
|
||||
image: btcpayserver/lnd:v0.14.2-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -269,7 +270,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.14.1-beta
|
||||
image: btcpayserver/lnd:v0.14.2-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -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
|
||||
@ -385,6 +388,7 @@ services:
|
||||
- "19444:19444"
|
||||
volumes:
|
||||
- "elementsd_liquid_datadir:/data"
|
||||
|
||||
volumes:
|
||||
sshd_datadir:
|
||||
bitcoin_datadir:
|
||||
|
@ -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,
|
||||
@ -33,7 +33,7 @@ services:
|
||||
links:
|
||||
- dev
|
||||
- selenium
|
||||
extra_hosts:
|
||||
extra_hosts:
|
||||
- "tests:127.0.0.1"
|
||||
volumes:
|
||||
- "sshd_datadir:/root/.ssh"
|
||||
@ -41,7 +41,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:
|
||||
@ -66,7 +66,7 @@ services:
|
||||
volumes:
|
||||
- "sshd_datadir:/root/.ssh"
|
||||
|
||||
devlnd:
|
||||
devlnd:
|
||||
image: btcpayserver/bitcoin:22.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
@ -86,11 +86,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
|
||||
@ -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:
|
||||
@ -127,7 +128,7 @@ services:
|
||||
zmqpubrawtx=tcp://0.0.0.0:28333
|
||||
deprecatedrpc=signrawtransaction
|
||||
fallbackfee=0.0002
|
||||
ports:
|
||||
ports:
|
||||
- "43782:43782"
|
||||
- "39388:39388"
|
||||
expose:
|
||||
@ -142,7 +143,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"
|
||||
@ -190,7 +191,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"
|
||||
@ -223,7 +224,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.14.1-beta
|
||||
image: btcpayserver/lnd:v0.14.2-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -249,7 +250,9 @@ services:
|
||||
ports:
|
||||
- "35531:8080"
|
||||
expose:
|
||||
- "8080"
|
||||
- "9735"
|
||||
- "10009"
|
||||
volumes:
|
||||
- "merchant_lnd_datadir:/data"
|
||||
- "bitcoin_datadir:/deps/.bitcoin"
|
||||
@ -257,7 +260,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.14.1-beta
|
||||
image: btcpayserver/lnd:v0.14.2-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -274,7 +277,7 @@ services:
|
||||
bitcoind.rpcpass=DwubwWsoo3
|
||||
bitcoind.zmqpubrawblock=tcp://bitcoind:28332
|
||||
bitcoind.zmqpubrawtx=tcp://bitcoind:28333
|
||||
externalip=customer_lnd:10009
|
||||
externalip=customer_lnd:9735
|
||||
bitcoin.defaultchanconfs=1
|
||||
no-macaroons=1
|
||||
debuglevel=debug
|
||||
@ -284,6 +287,7 @@ services:
|
||||
- "35532:8080"
|
||||
expose:
|
||||
- "8080"
|
||||
- "9735"
|
||||
- "10009"
|
||||
volumes:
|
||||
- "customer_lnd_datadir:/root/.lnd"
|
||||
|
4
BTCPayServer.Tests/docker-customer-lncli.sh
Executable file
4
BTCPayServer.Tests/docker-customer-lncli.sh
Executable file
@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
container_id="$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=customer_lnd)"
|
||||
docker exec -ti $container_id lncli --no-macaroons --rpcserver localhost:10008 "$@"
|
@ -3,67 +3,102 @@
|
||||
# Commands
|
||||
BCMD=./docker-bitcoin-cli.sh
|
||||
GCMD=./docker-bitcoin-generate.sh
|
||||
CCMD=./docker-customer-lightning-cli.sh
|
||||
MCMD=./docker-merchant-lightning-cli.sh
|
||||
C_LN=./docker-customer-lncli.sh
|
||||
M_LN=./docker-merchant-lncli.sh
|
||||
C_CL=./docker-customer-lightning-cli.sh
|
||||
M_CL=./docker-merchant-lightning-cli.sh
|
||||
|
||||
function channel_count () {
|
||||
local cmd=$1; local id=$2;
|
||||
local count=$($cmd listchannels | jq -r ".channels | map(select(.destination == \"$id\")) | length | tonumber") 2>/dev/null
|
||||
local cmd="$1"; local id="$2";
|
||||
if [[ $cmd =~ "lightning-cli" ]]; then
|
||||
local count=$($cmd listchannels | jq -r ".channels | map(select(.destination == \"$id\" and .active == true)) | length | tonumber") 2>/dev/null
|
||||
elif [[ $cmd =~ "lncli" ]]; then
|
||||
local count=$($cmd listchannels | jq -r ".channels | map(select(.remote_pubkey == \"$id\" and .active == true)) | length | tonumber") 2>/dev/null
|
||||
fi
|
||||
return $count
|
||||
}
|
||||
|
||||
function connect () {
|
||||
local cmd="$1"; local uri="$2"; local desc="$3";
|
||||
local connid=`$cmd connect $uri` 2>/dev/null
|
||||
if [[ $connid =~ "already connected" ]]; then
|
||||
printf "%s %s\n\r" "✅" "$desc"
|
||||
else
|
||||
printf "%s %s\n\r" $([[ $uri =~ ^$(echo $connid | jq -r '.id')* ]] && echo "✅" || echo "❌") "$desc"
|
||||
fi
|
||||
}
|
||||
|
||||
function create_channel () {
|
||||
local cmd=$1; local id=$2;
|
||||
local btcaddr=$($cmd newaddr | jq -r '.address')
|
||||
$BCMD sendtoaddress $btcaddr 0.15 >/dev/null
|
||||
$GCMD 10 >/dev/null
|
||||
local fundres=$($cmd fundchannel $id 14500000 5000 | jq -r '.channel_id')
|
||||
$GCMD 20 >/dev/null
|
||||
sleep 2
|
||||
channel_count $cmd $id
|
||||
local cmd="$1"; local id="$2"; local desc="$3"; local opts="$4";
|
||||
channel_count "$cmd" "$id"
|
||||
local count=$?
|
||||
return $count
|
||||
if [[ $count -eq 0 ]]; then
|
||||
# fund onchain wallet
|
||||
if [[ $cmd =~ "lightning-cli" ]]; then
|
||||
local btcaddr=$($cmd newaddr | jq -r '.bech32')
|
||||
elif [[ $cmd =~ "lncli" ]]; then
|
||||
local btcaddr=$($cmd newaddress p2wkh | jq -r '.address')
|
||||
fi
|
||||
$BCMD sendtoaddress $btcaddr 0.615 >/dev/null
|
||||
$GCMD 10 >/dev/null
|
||||
# open channel
|
||||
if [[ $cmd =~ "lightning-cli" ]]; then
|
||||
$cmd -k fundchannel id=$id amount=5000000 push_msat=2450000 $opts >/dev/null
|
||||
elif [[ $cmd =~ "lncli" ]]; then
|
||||
$cmd openchannel $id 5000000 2450000 $opts >/dev/null
|
||||
fi
|
||||
$GCMD 20 >/dev/null
|
||||
sleep 1
|
||||
channel_count "$cmd" "$id"
|
||||
local count=$?
|
||||
fi
|
||||
printf "%s %s\n\r" $([[ $count -gt 0 ]] && echo "✅" || echo "❌") "$desc"
|
||||
}
|
||||
|
||||
# General information
|
||||
cinfo=$($CCMD getinfo | jq '.' 2>/dev/null)
|
||||
minfo=$($MCMD getinfo | jq '.' 2>/dev/null)
|
||||
cid=$(echo $cinfo | jq -r '.id')
|
||||
mid=$(echo $minfo | jq -r '.id')
|
||||
caddr=$(echo $cinfo | jq -r '.address[] | "\(.address):\(.port)"')
|
||||
maddr=$(echo $minfo | jq -r '.address[] | "\(.address):\(.port)"')
|
||||
# Nodes
|
||||
c_cl_info=$($C_CL getinfo | jq '.' 2>/dev/null)
|
||||
m_cl_info=$($M_CL getinfo | jq '.' 2>/dev/null)
|
||||
c_cl_id=$(echo $c_cl_info | jq -r '.id')
|
||||
m_cl_id=$(echo $m_cl_info | jq -r '.id')
|
||||
c_cl_addr=$(echo $c_cl_info | jq -r '.address[] | "\(.address):\(.port)"')
|
||||
m_cl_addr=$(echo $m_cl_info | jq -r '.address[] | "\(.address):\(.port)"')
|
||||
c_cl_uri=$(echo "$c_cl_id@$c_cl_addr")
|
||||
m_cl_uri=$(echo "$m_cl_id@$m_cl_addr")
|
||||
|
||||
printf "Customer ID: %s@%s\n\r" $cid $caddr
|
||||
printf "Merchant ID: %s@%s\n\r" $mid $maddr
|
||||
c_ln_info=$($C_LN getinfo | jq '.' 2>/dev/null)
|
||||
m_ln_info=$($M_LN getinfo | jq '.' 2>/dev/null)
|
||||
c_ln_id=$(echo $c_ln_info | jq -r '.identity_pubkey' 2>/dev/null)
|
||||
m_ln_id=$(echo $m_ln_info | jq -r '.identity_pubkey' 2>/dev/null)
|
||||
c_ln_uri=$(echo $c_ln_info | jq -r '.uris[]' 2>/dev/null)
|
||||
m_ln_uri=$(echo $m_ln_info | jq -r '.uris[]' 2>/dev/null)
|
||||
|
||||
printf "\n\rNodes\n\r-----\n\r"
|
||||
printf "Merchant c-lightning: %s\n\r" $m_cl_uri
|
||||
printf "Merchant LND: %s\n\r" $m_ln_uri
|
||||
printf "Customer c-lightning: %s\n\r" $c_cl_uri
|
||||
printf "Customer LND: %s\n\r" $c_ln_uri
|
||||
|
||||
# Connections
|
||||
printf "\n\rConnecting both parties …\n\r"
|
||||
printf "\n\rConnecting all parties\n\r----------------------\n\r"
|
||||
|
||||
cconnid=$($CCMD connect "$mid@$maddr" | jq -r '.id' 2>/dev/null)
|
||||
mconnid=$($MCMD connect "$cid@$caddr" | jq -r '.id' 2>/dev/null)
|
||||
|
||||
printf "Customer to merchant %s\n\r" $([[ $cconnid == $mid ]] && echo "succeeded" || echo "failed")
|
||||
printf "Merchant to customer %s\n\r" $([[ $mconnid == $cid ]] && echo "succeeded" || echo "failed")
|
||||
connect $M_CL $c_cl_uri "Merchant (c-lightning) to Customer (c-lightning)"
|
||||
connect $M_CL $c_ln_uri "Merchant (c-lightning) to Customer (LND)"
|
||||
connect $M_CL $m_ln_uri "Merchant (c-lightning) to Merchant (LND)"
|
||||
connect $C_CL $m_cl_uri "Customer (c-lightning) to Merchant (c-lightning)"
|
||||
connect $C_CL $m_ln_uri "Customer (c-lightning) to Merchant (LND)"
|
||||
connect $C_CL $c_ln_uri "Customer (c-lightning) to Customer (LND)"
|
||||
connect $M_LN $c_cl_uri "Merchant (LND) to Customer (c-lightning)"
|
||||
connect $M_LN $c_cl_uri "Merchant (LND) to Customer (c-lightning)"
|
||||
connect $M_LN $c_ln_uri "Merchant (LND) to Customer (LND)"
|
||||
connect $C_LN $m_cl_uri "Customer (LND) to Merchant (c-lightning)"
|
||||
connect $C_LN $c_cl_uri "Customer (LND) to Customer (c-lightning)"
|
||||
connect $C_LN $m_ln_uri "Customer (LND) to Merchant (LND)"
|
||||
|
||||
# Channels
|
||||
printf "\n\rChecking channels …\n\r"
|
||||
channel_count $CCMD $mid
|
||||
cchanscount=$?
|
||||
channel_count $MCMD $cid
|
||||
mchanscount=$?
|
||||
printf "\n\rEstablishing channels\n\r----------------------\n\r"
|
||||
|
||||
printf "Customer channel count to merchant: %d\n\r" $cchanscount
|
||||
printf "Merchant channel count to customer: %d\n\r" $mchanscount
|
||||
|
||||
# Open channels if there are none, details: https://github.com/ElementsProject/lightning#opening-a-channel
|
||||
if [[ $cchanscount -eq 0 ]]; then
|
||||
create_channel $CCMD $mid
|
||||
cchanres=$?
|
||||
printf "Establishing channel from customer to merchant %s\n\r" $([[ $cchanres -gt 0 ]] && echo "succeeded" || echo "failed")
|
||||
fi
|
||||
|
||||
if [[ $mchanscount -eq 0 ]]; then
|
||||
create_channel $MCMD $cid
|
||||
mchanres=$?
|
||||
printf "Establishing channel from merchant to customer %s\n\r" $([[ $mchanres -gt 0 ]] && echo "succeeded" || echo "failed")
|
||||
fi
|
||||
create_channel $C_CL $m_cl_id "Customer (c-lightning) to Merchant (c-lightning)"
|
||||
create_channel $C_CL $m_ln_id "Customer (c-lightning) to Merchant (LND)"
|
||||
create_channel $C_LN $c_cl_id "Customer (LND) to Customer (c-lightning)"
|
||||
create_channel $M_CL $m_ln_id "Merchant (c-lightning) to Merchant (LND)" "announce=false"
|
||||
create_channel $C_LN $m_ln_id "Customer (LND) to Merchant (LND)" --private
|
||||
|
@ -1,12 +1,15 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
./docker-customer-lncli.sh closeallchannels > /dev/null
|
||||
./docker-merchant-lncli.sh closeallchannels > /dev/null
|
||||
./docker-bitcoin-generate.sh 10 > /dev/null
|
||||
|
||||
channels=$(./docker-merchant-lightning-cli.sh listchannels | jq -cr '.channels | map(.short_channel_id) | unique')
|
||||
printf "Channels: %s\n\r" $channels
|
||||
|
||||
for chanid in $(echo "${channels}" | jq -cr '.[]')
|
||||
do
|
||||
printf "Closing channel ID: %s\n\r" $chanid
|
||||
./docker-merchant-lightning-cli.sh close $chanid
|
||||
./docker-bitcoin-generate.sh 20 > /dev/null
|
||||
./docker-merchant-lightning-cli.sh close $chanid > /dev/null
|
||||
done
|
||||
./docker-bitcoin-generate.sh 10 > /dev/null
|
||||
|
||||
printf "All channels closed!\r\n"
|
||||
|
4
BTCPayServer.Tests/docker-merchant-lncli.sh
Executable file
4
BTCPayServer.Tests/docker-merchant-lncli.sh
Executable file
@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
container_id="$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=merchant_lnd)"
|
||||
docker exec -ti $container_id lncli --no-macaroons --rpcserver localhost:10008 "$@"
|
@ -40,17 +40,20 @@
|
||||
<ItemGroup Condition="'$(Altcoins)' != 'true'">
|
||||
<Content Remove="Services\Altcoins\**\*" />
|
||||
<Content Remove="Views\UIMoneroLikeStore\**\*" />
|
||||
<Content Remove="Views\UIZcashLikeStore\**\*" />
|
||||
<Content Remove="Views\Shared\Monero\**\*" />
|
||||
<Content Remove="Views\Shared\Zcash\**\*" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
|
||||
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.2.19" />
|
||||
<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" />
|
||||
@ -229,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">
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user