Compare commits
260 Commits
fix-array-
...
fix-form-v
Author | SHA1 | Date | |
---|---|---|---|
|
38da96257f | ||
|
21f73d436a | ||
|
d58dde950e | ||
|
8ac18b74df | ||
|
2846c38ff5 | ||
|
d44efce225 | ||
|
d3dca7e808 | ||
|
41e3828eea | ||
|
9e76b4d28e | ||
|
ef03497350 | ||
|
e5a2aeb145 | ||
|
229a4ea56c | ||
|
f20e6d3768 | ||
|
1d210eb6e3 | ||
|
d8422a979f | ||
|
0cf6d39f02 | ||
|
076c20a3b7 | ||
|
0cfb0ba890 | ||
|
44a7e9387e | ||
|
e71954ee34 | ||
|
9cd9e84be6 | ||
|
25af9c4227 | ||
|
72a99bf9a6 | ||
|
f1228523cb | ||
|
a45d368115 | ||
|
16433dc183 | ||
|
0a956fdc73 | ||
|
75396f491b | ||
|
66a064e78b | ||
|
c4f8c4c7b4 | ||
|
22bbafa659 | ||
|
8cdfaba20c | ||
|
7da82826fb | ||
|
9a46a64cad | ||
|
33198d693d | ||
|
eaeb7021d5 | ||
|
b443acd43b | ||
|
616883648f | ||
|
7873f94848 | ||
|
17d1832dad | ||
|
f034e2cd65 | ||
|
19de73f9da | ||
|
00acbccd7f | ||
|
77d8e202d3 | ||
|
44df8cf0c5 | ||
|
e694568674 | ||
|
163f805f3b | ||
|
492512f527 | ||
|
73a4ac599c | ||
|
4aedf76f1f | ||
|
2d38113c66 | ||
|
445e1b7bd9 | ||
|
019ac7ae31 | ||
|
2b3b025bd8 | ||
|
57bc90ad03 | ||
|
089e16020e | ||
|
0c4f31794d | ||
|
cdffe9b355 | ||
|
5a28cf9e87 | ||
|
3b05de7f30 | ||
|
79b2f1652b | ||
|
b32e0e7cce | ||
|
1f9fbbee22 | ||
|
8c9f325c9f | ||
|
9bf1e35bf4 | ||
|
32e830a1c5 | ||
|
561bae071f | ||
|
08b6942c59 | ||
|
4564f9a46c | ||
|
58a1c6d2c8 | ||
|
97acec340c | ||
|
52790a6954 | ||
|
af6249a741 | ||
|
17064ab3c8 | ||
|
1487bf4ff5 | ||
|
e8c0858558 | ||
|
56fa3fe8f2 | ||
|
583813883c | ||
|
c69f95bdce | ||
|
b3df403980 | ||
|
90ce75ee21 | ||
|
1c5fcfe094 | ||
|
45c1fb42ee | ||
|
64bd493996 | ||
|
ec6029409e | ||
|
c0fc31c69a | ||
|
b5d0188f21 | ||
|
0ccbaf4bd6 | ||
|
ed43fb2071 | ||
|
d67ebd957e | ||
|
19d360a543 | ||
|
7dc41ebcea | ||
|
1eb7c727f3 | ||
|
ede8171408 | ||
|
2538f3d8f6 | ||
|
ac64f5e395 | ||
|
1a7a731b54 | ||
|
86f4d48bcb | ||
|
83536bee88 | ||
|
abfd6ea1dc | ||
|
688e873f7a | ||
|
c88df08350 | ||
|
82586590a7 | ||
|
88c66f30f2 | ||
|
9132592717 | ||
|
c0ffab768a | ||
|
69190081c8 | ||
|
093206cf1e | ||
|
a0110b7570 | ||
|
6d65feca4c | ||
|
95be0242b6 | ||
|
79e121c3af | ||
|
676ac2fe46 | ||
|
8eabdab53a | ||
|
957fb09ffc | ||
|
4bffe117a9 | ||
|
05b01a13c8 | ||
|
08e21c1a5d | ||
|
4d5245605d | ||
|
453548d614 | ||
|
95a0614ae1 | ||
|
36ea17a6b7 | ||
|
dc986959fd | ||
|
845e2881fa | ||
|
2e4be9310c | ||
|
a2faa6fd59 | ||
|
0a78846e8d | ||
|
4063a5aaee | ||
|
b1c81b696f | ||
|
0017f236a7 | ||
|
19d5e64063 | ||
|
22435a2bf5 | ||
|
a7def63137 | ||
|
3703a170e7 | ||
|
73fbfbd7cb | ||
|
acae3b8753 | ||
|
a618f901fc | ||
|
6d4918f0ab | ||
|
7f2c4d2e7a | ||
|
fd6d361e1a | ||
|
b5f0924651 | ||
|
1600dd4759 | ||
|
c777746b69 | ||
|
9f5466a41f | ||
|
4d1e4801bf | ||
|
5e469ff9c0 | ||
|
2f3eedea5b | ||
|
5c5d6dc1e2 | ||
|
fbe31ce64f | ||
|
0b082138c8 | ||
|
966e598f10 | ||
|
e998340387 | ||
|
f6b27cc5f9 | ||
|
f3dbf1e139 | ||
|
627d84fc91 | ||
|
8cde8c01df | ||
|
983b8c1f54 | ||
|
d666d8ea1a | ||
|
3ed81c3a78 | ||
|
4afec2e2b6 | ||
|
db83d238d5 | ||
|
fdcf7b3b7a | ||
|
53aafcf86b | ||
|
aec84f6d67 | ||
|
01e9f82d24 | ||
|
2eff45e65c | ||
|
13203c3e2b | ||
|
82c5e0e43d | ||
|
a1575f404b | ||
|
e1509506dc | ||
|
0c1d0d7b05 | ||
|
ad70856af0 | ||
|
8615f120ce | ||
|
0d0477d661 | ||
|
b31dc30878 | ||
|
6e392f4cfb | ||
|
cc3bdc331e | ||
|
76faf77a1c | ||
|
d8c0e5bf3a | ||
|
28c4c320cc | ||
|
e81403ec3f | ||
|
f11424f73a | ||
|
fa8b977016 | ||
|
d181846339 | ||
|
1956919886 | ||
|
0f66498965 | ||
|
918cd152b1 | ||
|
d3222df396 | ||
|
a84ffd8c7e | ||
|
6d0f9120b8 | ||
|
aafb4a7f2a | ||
|
ae432ff237 | ||
|
cdc318c71a | ||
|
94d1cec8a9 | ||
|
c0bc19ea59 | ||
|
6f07714cd9 | ||
|
a9d2cac23c | ||
|
693b46126b | ||
|
bbff9710bf | ||
|
358e122775 | ||
|
3818468932 | ||
|
3d2554fbe1 | ||
|
4309603317 | ||
|
f733c9ea77 | ||
|
775ee01171 | ||
|
33ec790137 | ||
|
0c575c888c | ||
|
24f7e51e3a | ||
|
0a0cf97c55 | ||
|
16b988d097 | ||
|
5edc0ff6ef | ||
|
375b96e508 | ||
|
1e72b12074 | ||
|
4a6d52f78e | ||
|
35dd580e74 | ||
|
79836ef1de | ||
|
8cb06f9c6c | ||
|
215a36e7a9 | ||
|
247f6b86a5 | ||
|
a9d42f1e6a | ||
|
4e03c2523a | ||
|
418b476725 | ||
|
783e4ccb35 | ||
|
6b7fb55658 | ||
|
3d5361cd11 | ||
|
2c4349c630 | ||
|
3589417b58 | ||
|
55203e0b64 | ||
|
a918288e3b | ||
|
e183138d2c | ||
|
d3e42862ed | ||
|
8860eec254 | ||
|
97e7e60cea | ||
|
44aaf7acbb | ||
|
9b721fae27 | ||
|
c3f412e3bb | ||
|
ee738a29f0 | ||
|
6c6544bf9b | ||
|
3d57b944ca | ||
|
acf003b1b4 | ||
|
52e108d32f | ||
|
7b96f96025 | ||
|
8db5e7e043 | ||
|
25fb5c1293 | ||
|
37f0498def | ||
|
02110f93d7 | ||
|
195dfc2c47 | ||
|
541b6cf9eb | ||
|
2c26b77afc | ||
|
99bcec5597 | ||
|
781190a65d | ||
|
3763480280 | ||
|
6fad5ebedb | ||
|
0690194aa1 | ||
|
03b94e2be3 | ||
|
18e34b3cbe | ||
|
a0bb3ace61 | ||
|
920ad67633 | ||
|
8b8f72129c | ||
|
b9b11e722c |
2
.github/codeql/codeql-config.yml
vendored
Normal file
2
.github/codeql/codeql-config.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
paths-ignore:
|
||||||
|
- 'BTCPayServer/wwwroot/vendor/**/*.js'
|
80
.github/workflows/codeql.yml
vendored
Normal file
80
.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# For most projects, this workflow file will not need changing; you simply need
|
||||||
|
# to commit it to your repository.
|
||||||
|
#
|
||||||
|
# You may wish to alter this file to override the set of languages analyzed,
|
||||||
|
# or to provide custom queries or build logic.
|
||||||
|
#
|
||||||
|
# ******** NOTE ********
|
||||||
|
# We have attempted to detect the languages in your repository. Please check
|
||||||
|
# the `language` matrix defined below to confirm you have the correct set of
|
||||||
|
# supported CodeQL languages.
|
||||||
|
#
|
||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
on:
|
||||||
|
# Allow running tests manually. Usefull if scan failure, or need to rescan before next scheduled date.
|
||||||
|
workflow_dispatch:
|
||||||
|
# We scan only on a schedule for now, can uncomment the following to scan on commit or PR merge later on if deemed appropriate.
|
||||||
|
# push:
|
||||||
|
# branches: [ "master" ]
|
||||||
|
# pull_request:
|
||||||
|
# branches: [ "master" ]
|
||||||
|
schedule:
|
||||||
|
# Scan every Monday 06:00 UTC.
|
||||||
|
- cron: '0 6 * * 1'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: [ 'javascript', 'csharp' ]
|
||||||
|
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||||
|
# Use only 'java' to analyze code written in Java, Kotlin or both
|
||||||
|
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
|
||||||
|
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v2
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
config-file: ./.github/codeql/codeql-config.yml
|
||||||
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
|
# By default, queries listed here will override any specified in a config file.
|
||||||
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
|
|
||||||
|
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||||
|
# queries: security-extended,security-and-quality
|
||||||
|
|
||||||
|
|
||||||
|
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
|
||||||
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
|
||||||
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||||
|
|
||||||
|
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
||||||
|
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
||||||
|
|
||||||
|
# - run: |
|
||||||
|
# echo "Run, Build Application using script"
|
||||||
|
# ./location_of_script_within_repo/buildscript.sh
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v2
|
||||||
|
with:
|
||||||
|
category: "/language:${{matrix.language}}"
|
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Data.Common;
|
||||||
using BTCPayServer.Abstractions.Models;
|
using BTCPayServer.Abstractions.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata;
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
@@ -21,7 +22,6 @@ namespace BTCPayServer.Abstractions.Contracts
|
|||||||
}
|
}
|
||||||
|
|
||||||
public abstract T CreateContext();
|
public abstract T CreateContext();
|
||||||
|
|
||||||
class CustomNpgsqlMigrationsSqlGenerator : NpgsqlMigrationsSqlGenerator
|
class CustomNpgsqlMigrationsSqlGenerator : NpgsqlMigrationsSqlGenerator
|
||||||
{
|
{
|
||||||
#pragma warning disable EF1001 // Internal EF Core API usage.
|
#pragma warning disable EF1001 // Internal EF Core API usage.
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace BTCPayServer.Abstractions.Contracts
|
namespace BTCPayServer.Abstractions.Contracts
|
||||||
@@ -6,5 +7,8 @@ namespace BTCPayServer.Abstractions.Contracts
|
|||||||
{
|
{
|
||||||
Task ApplyAction(string hook, object args);
|
Task ApplyAction(string hook, object args);
|
||||||
Task<object> ApplyFilter(string hook, object args);
|
Task<object> ApplyFilter(string hook, object args);
|
||||||
|
|
||||||
|
event EventHandler<(string hook, object args)> ActionInvoked;
|
||||||
|
event EventHandler<(string hook, object args)> FilterInvoked;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -69,7 +69,6 @@ public class Form
|
|||||||
if (!nameReturned.Add(fullName))
|
if (!nameReturned.Add(fullName))
|
||||||
{
|
{
|
||||||
errors.Add($"Form contains duplicate field names '{fullName}'");
|
errors.Add($"Form contains duplicate field names '{fullName}'");
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return errors.Count == 0;
|
return errors.Count == 0;
|
||||||
@@ -86,15 +85,10 @@ public class Form
|
|||||||
thisPath.Add(field.Name);
|
thisPath.Add(field.Name);
|
||||||
yield return (thisPath, field);
|
yield return (thisPath, field);
|
||||||
}
|
}
|
||||||
|
foreach (var descendant in GetAllFieldsCore(thisPath, field.Fields))
|
||||||
foreach (var child in field.Fields)
|
|
||||||
{
|
{
|
||||||
if (field.Constant)
|
descendant.Field.Constant = field.Constant || descendant.Field.Constant;
|
||||||
child.Constant = true;
|
yield return descendant;
|
||||||
foreach (var descendant in GetAllFieldsCore(thisPath, field.Fields))
|
|
||||||
{
|
|
||||||
yield return descendant;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,31 +105,7 @@ public class Form
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetValues(JObject values)
|
|
||||||
{
|
|
||||||
var fields = GetAllFields().ToDictionary(k => k.FullName, k => k.Field);
|
|
||||||
SetValues(fields, new List<string>(), values);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetValues(Dictionary<string, Field> fields, List<string> path, JObject values)
|
|
||||||
{
|
|
||||||
foreach (var prop in values.Properties())
|
|
||||||
{
|
|
||||||
List<string> propPath = new List<string>(path.Count + 1);
|
|
||||||
propPath.AddRange(path);
|
|
||||||
propPath.Add(prop.Name);
|
|
||||||
if (prop.Value.Type == JTokenType.Object)
|
|
||||||
{
|
|
||||||
SetValues(fields, propPath, (JObject)prop.Value);
|
|
||||||
}
|
|
||||||
else if (prop.Value.Type == JTokenType.String)
|
|
||||||
{
|
|
||||||
var fullName = string.Join('_', propPath.Where(s => !string.IsNullOrEmpty(s)));
|
|
||||||
if (fields.TryGetValue(fullName, out var f) && !f.Constant)
|
|
||||||
f.Value = prop.Value.Value<string>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Web;
|
||||||
using Ganss.XSS;
|
using Ganss.XSS;
|
||||||
using Microsoft.AspNetCore.Html;
|
using Microsoft.AspNetCore.Html;
|
||||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
@@ -21,6 +22,11 @@ namespace BTCPayServer.Abstractions.Services
|
|||||||
{
|
{
|
||||||
return _htmlHelper.Raw(_htmlSanitizer.Sanitize(value));
|
return _htmlHelper.Raw(_htmlSanitizer.Sanitize(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IHtmlContent RawEncode(string value)
|
||||||
|
{
|
||||||
|
return _htmlHelper.Raw(HttpUtility.HtmlEncode(_htmlSanitizer.Sanitize(value)));
|
||||||
|
}
|
||||||
|
|
||||||
public IHtmlContent Json(object model)
|
public IHtmlContent Json(object model)
|
||||||
{
|
{
|
||||||
|
@@ -6,31 +6,33 @@ using Microsoft.Extensions.Logging;
|
|||||||
|
|
||||||
namespace BTCPayServer.Abstractions.TagHelpers;
|
namespace BTCPayServer.Abstractions.TagHelpers;
|
||||||
|
|
||||||
[HtmlTargetElement(Attributes = nameof(Permission))]
|
[HtmlTargetElement(Attributes = "[permission]")]
|
||||||
|
[HtmlTargetElement(Attributes = "[not-permission]" )]
|
||||||
public class PermissionTagHelper : TagHelper
|
public class PermissionTagHelper : TagHelper
|
||||||
{
|
{
|
||||||
private readonly IAuthorizationService _authorizationService;
|
private readonly IAuthorizationService _authorizationService;
|
||||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
private readonly ILogger<PermissionTagHelper> _logger;
|
|
||||||
|
|
||||||
public PermissionTagHelper(IAuthorizationService authorizationService, IHttpContextAccessor httpContextAccessor, ILogger<PermissionTagHelper> logger)
|
public PermissionTagHelper(IAuthorizationService authorizationService, IHttpContextAccessor httpContextAccessor)
|
||||||
{
|
{
|
||||||
_authorizationService = authorizationService;
|
_authorizationService = authorizationService;
|
||||||
_httpContextAccessor = httpContextAccessor;
|
_httpContextAccessor = httpContextAccessor;
|
||||||
_logger = logger;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Permission { get; set; }
|
public string Permission { get; set; }
|
||||||
|
public string NotPermission { get; set; }
|
||||||
public string PermissionResource { get; set; }
|
public string PermissionResource { get; set; }
|
||||||
|
|
||||||
|
|
||||||
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
|
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(Permission))
|
if (string.IsNullOrEmpty(Permission) && string.IsNullOrEmpty(NotPermission))
|
||||||
return;
|
return;
|
||||||
if (_httpContextAccessor.HttpContext is null)
|
if (_httpContextAccessor.HttpContext is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var key = $"{Permission}_{PermissionResource}";
|
var expectedResult = !string.IsNullOrEmpty(Permission);
|
||||||
|
var key = $"{Permission??NotPermission}_{PermissionResource}";
|
||||||
if (!_httpContextAccessor.HttpContext.Items.TryGetValue(key, out var o) ||
|
if (!_httpContextAccessor.HttpContext.Items.TryGetValue(key, out var o) ||
|
||||||
o is not AuthorizationResult res)
|
o is not AuthorizationResult res)
|
||||||
{
|
{
|
||||||
@@ -39,7 +41,7 @@ public class PermissionTagHelper : TagHelper
|
|||||||
Permission);
|
Permission);
|
||||||
_httpContextAccessor.HttpContext.Items.Add(key, res);
|
_httpContextAccessor.HttpContext.Items.Add(key, res);
|
||||||
}
|
}
|
||||||
if (!res.Succeeded)
|
if (expectedResult != res.Succeeded)
|
||||||
{
|
{
|
||||||
output.SuppressOutput();
|
output.SuppressOutput();
|
||||||
}
|
}
|
||||||
|
@@ -12,9 +12,11 @@
|
|||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
<RepositoryUrl>https://github.com/btcpayserver/btcpayserver</RepositoryUrl>
|
<RepositoryUrl>https://github.com/btcpayserver/btcpayserver</RepositoryUrl>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
|
<Configurations>Debug;Release;Altcoins-Debug;Altcoins-Release</Configurations>
|
||||||
|
<Platforms>AnyCPU</Platforms>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Version Condition=" '$(Version)' == '' ">1.7.2</Version>
|
<Version Condition=" '$(Version)' == '' ">1.7.3</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||||
@@ -30,7 +32,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.3.21" />
|
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.3.21" />
|
||||||
<PackageReference Include="NBitcoin" Version="7.0.24" />
|
<PackageReference Include="NBitcoin" Version="7.0.24" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="icon.png" Pack="true" PackagePath="\" />
|
<None Include="icon.png" Pack="true" PackagePath="\" />
|
||||||
|
@@ -55,7 +55,7 @@ namespace BTCPayServer.Client
|
|||||||
}
|
}
|
||||||
|
|
||||||
public virtual async Task<IEnumerable<OnChainWalletTransactionData>> ShowOnChainWalletTransactions(
|
public virtual async Task<IEnumerable<OnChainWalletTransactionData>> ShowOnChainWalletTransactions(
|
||||||
string storeId, string cryptoCode, TransactionStatus[] statusFilter = null, string labelFilter = null,
|
string storeId, string cryptoCode, TransactionStatus[] statusFilter = null, string labelFilter = null, int skip = 0,
|
||||||
CancellationToken token = default)
|
CancellationToken token = default)
|
||||||
{
|
{
|
||||||
var query = new Dictionary<string, object>();
|
var query = new Dictionary<string, object>();
|
||||||
@@ -67,6 +67,10 @@ namespace BTCPayServer.Client
|
|||||||
{
|
{
|
||||||
query.Add(nameof(labelFilter), labelFilter);
|
query.Add(nameof(labelFilter), labelFilter);
|
||||||
}
|
}
|
||||||
|
if (skip != 0)
|
||||||
|
{
|
||||||
|
query.Add(nameof(skip), skip);
|
||||||
|
}
|
||||||
var response =
|
var response =
|
||||||
await _httpClient.SendAsync(
|
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);
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Client.Models;
|
using BTCPayServer.Client.Models;
|
||||||
@@ -11,5 +12,11 @@ namespace BTCPayServer.Client
|
|||||||
var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/server/info"), token);
|
var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/server/info"), token);
|
||||||
return await HandleResponse<ServerInfoData>(response);
|
return await HandleResponse<ServerInfoData>(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public virtual async Task<List<RoleData>> GetServerRoles(CancellationToken token = default)
|
||||||
|
{
|
||||||
|
using var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/server/roles"), token);
|
||||||
|
return await HandleResponse<List<RoleData>>(response);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -9,6 +9,13 @@ namespace BTCPayServer.Client
|
|||||||
{
|
{
|
||||||
public partial class BTCPayServerClient
|
public partial class BTCPayServerClient
|
||||||
{
|
{
|
||||||
|
public virtual async Task<List<RoleData>> GetStoreRoles(string storeId,
|
||||||
|
CancellationToken token = default)
|
||||||
|
{
|
||||||
|
using var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/roles"), token);
|
||||||
|
return await HandleResponse<List<RoleData>>(response);
|
||||||
|
}
|
||||||
|
|
||||||
public virtual async Task<IEnumerable<StoreUserData>> GetStoreUsers(string storeId,
|
public virtual async Task<IEnumerable<StoreUserData>> GetStoreUsers(string storeId,
|
||||||
CancellationToken token = default)
|
CancellationToken token = default)
|
||||||
{
|
{
|
||||||
|
@@ -51,7 +51,8 @@ namespace BTCPayServer.Client
|
|||||||
{
|
{
|
||||||
if (message.StatusCode == System.Net.HttpStatusCode.UnprocessableEntity)
|
if (message.StatusCode == System.Net.HttpStatusCode.UnprocessableEntity)
|
||||||
{
|
{
|
||||||
var err = JsonConvert.DeserializeObject<Models.GreenfieldValidationError[]>(await message.Content.ReadAsStringAsync());
|
var aa = await message.Content.ReadAsStringAsync();
|
||||||
|
var err = JsonConvert.DeserializeObject<Models.GreenfieldValidationError[]>(aa);
|
||||||
throw new GreenfieldValidationException(err);
|
throw new GreenfieldValidationException(err);
|
||||||
}
|
}
|
||||||
if (message.StatusCode == System.Net.HttpStatusCode.Forbidden)
|
if (message.StatusCode == System.Net.HttpStatusCode.Forbidden)
|
||||||
|
@@ -37,6 +37,7 @@ namespace BTCPayServer.Client.Models
|
|||||||
public string RedirectUrl { get; set; } = null;
|
public string RedirectUrl { get; set; } = null;
|
||||||
public bool? RedirectAutomatically { get; set; } = null;
|
public bool? RedirectAutomatically { get; set; } = null;
|
||||||
public bool? RequiresRefundEmail { get; set; } = null;
|
public bool? RequiresRefundEmail { get; set; } = null;
|
||||||
|
public bool? Archived { get; set; } = null;
|
||||||
public string FormId { get; set; } = null;
|
public string FormId { get; set; } = null;
|
||||||
public string EmbeddedCSS { get; set; } = null;
|
public string EmbeddedCSS { get; set; } = null;
|
||||||
public CheckoutType? CheckoutType { get; set; } = null;
|
public CheckoutType? CheckoutType { get; set; } = null;
|
||||||
@@ -78,6 +79,7 @@ namespace BTCPayServer.Client.Models
|
|||||||
public bool? DisplayPerksValue { get; set; } = null;
|
public bool? DisplayPerksValue { get; set; } = null;
|
||||||
public bool? DisplayPerksRanking { get; set; } = null;
|
public bool? DisplayPerksRanking { get; set; } = null;
|
||||||
public bool? SortPerksByPopularity { get; set; } = null;
|
public bool? SortPerksByPopularity { get; set; } = null;
|
||||||
|
public bool? Archived { get; set; } = null;
|
||||||
public string[] Sounds { get; set; } = null;
|
public string[] Sounds { get; set; } = null;
|
||||||
public string[] AnimationColors { get; set; } = null;
|
public string[] AnimationColors { get; set; } = null;
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,11 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace BTCPayServer.Client.Models;
|
namespace BTCPayServer.Client.Models;
|
||||||
|
|
||||||
public class CreatePayoutThroughStoreRequest : CreatePayoutRequest
|
public class CreatePayoutThroughStoreRequest : CreatePayoutRequest
|
||||||
{
|
{
|
||||||
public string? PullPaymentId { get; set; }
|
public string? PullPaymentId { get; set; }
|
||||||
public bool? Approved { get; set; }
|
public bool? Approved { get; set; }
|
||||||
|
public JObject? Metadata { get; set; }
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
namespace BTCPayServer.Client.Models
|
namespace BTCPayServer.Client.Models;
|
||||||
|
public enum InvoiceExceptionStatus
|
||||||
{
|
{
|
||||||
public enum InvoiceExceptionStatus
|
None,
|
||||||
{
|
PaidLate,
|
||||||
None,
|
PaidPartial,
|
||||||
PaidLate,
|
Marked,
|
||||||
PaidPartial,
|
Invalid,
|
||||||
Marked,
|
PaidOver
|
||||||
Invalid,
|
|
||||||
PaidOver
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@@ -10,4 +10,9 @@ public class LightningAutomatedPayoutSettings
|
|||||||
|
|
||||||
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
|
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
|
||||||
public TimeSpan IntervalSeconds { get; set; }
|
public TimeSpan IntervalSeconds { get; set; }
|
||||||
|
|
||||||
|
public int? CancelPayoutAfterFailures { get; set; }
|
||||||
|
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||||
|
public bool ProcessNewPayoutsInstantly { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -12,4 +12,8 @@ public class OnChainAutomatedPayoutSettings
|
|||||||
public TimeSpan IntervalSeconds { get; set; }
|
public TimeSpan IntervalSeconds { get; set; }
|
||||||
|
|
||||||
public int? FeeBlockTarget { get; set; }
|
public int? FeeBlockTarget { get; set; }
|
||||||
|
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||||
|
public decimal Threshold { get; set; }
|
||||||
|
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||||
|
public bool ProcessNewPayoutsInstantly { get; set; }
|
||||||
}
|
}
|
||||||
|
@@ -7,7 +7,7 @@ namespace BTCPayServer.Client.Models
|
|||||||
public class PaymentRequestData : PaymentRequestBaseData
|
public class PaymentRequestData : PaymentRequestBaseData
|
||||||
{
|
{
|
||||||
[JsonConverter(typeof(StringEnumConverter))]
|
[JsonConverter(typeof(StringEnumConverter))]
|
||||||
public PaymentRequestData.PaymentRequestStatus Status { get; set; }
|
public PaymentRequestStatus Status { get; set; }
|
||||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||||
public DateTimeOffset CreatedTime { get; set; }
|
public DateTimeOffset CreatedTime { get; set; }
|
||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
@@ -16,7 +16,8 @@ namespace BTCPayServer.Client.Models
|
|||||||
{
|
{
|
||||||
Pending = 0,
|
Pending = 0,
|
||||||
Completed = 1,
|
Completed = 1,
|
||||||
Expired = 2
|
Expired = 2,
|
||||||
|
Processing = 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -31,5 +31,6 @@ namespace BTCPayServer.Client.Models
|
|||||||
public PayoutState State { get; set; }
|
public PayoutState State { get; set; }
|
||||||
public int Revision { get; set; }
|
public int Revision { get; set; }
|
||||||
public JObject PaymentProof { get; set; }
|
public JObject PaymentProof { get; set; }
|
||||||
|
public JObject Metadata { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -9,6 +9,8 @@ namespace BTCPayServer.Client.Models
|
|||||||
public string AppType { get; set; }
|
public string AppType { get; set; }
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public string StoreId { get; set; }
|
public string StoreId { get; set; }
|
||||||
|
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||||
|
public bool? Archived { get; set; }
|
||||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||||
public DateTimeOffset Created { get; set; }
|
public DateTimeOffset Created { get; set; }
|
||||||
}
|
}
|
||||||
|
@@ -9,6 +9,7 @@ namespace BTCPayServer.Client.Models
|
|||||||
{
|
{
|
||||||
RateThen,
|
RateThen,
|
||||||
CurrentRate,
|
CurrentRate,
|
||||||
|
OverpaidAmount,
|
||||||
Fiat,
|
Fiat,
|
||||||
Custom
|
Custom
|
||||||
}
|
}
|
||||||
@@ -18,8 +19,13 @@ namespace BTCPayServer.Client.Models
|
|||||||
public string? Name { get; set; } = null;
|
public string? Name { get; set; } = null;
|
||||||
public string? PaymentMethod { get; set; }
|
public string? PaymentMethod { get; set; }
|
||||||
public string? Description { get; set; } = null;
|
public string? Description { get; set; } = null;
|
||||||
|
|
||||||
[JsonConverter(typeof(StringEnumConverter))]
|
[JsonConverter(typeof(StringEnumConverter))]
|
||||||
public RefundVariant? RefundVariant { get; set; }
|
public RefundVariant? RefundVariant { get; set; }
|
||||||
|
|
||||||
|
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||||
|
public decimal SubtractPercentage { get; set; }
|
||||||
|
|
||||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||||
public decimal? CustomAmount { get; set; }
|
public decimal? CustomAmount { get; set; }
|
||||||
public string? CustomCurrency { get; set; }
|
public string? CustomCurrency { get; set; }
|
||||||
|
@@ -16,6 +16,8 @@ namespace BTCPayServer.Client.Models
|
|||||||
|
|
||||||
public string Website { get; set; }
|
public string Website { get; set; }
|
||||||
|
|
||||||
|
public string SupportUrl { get; set; }
|
||||||
|
|
||||||
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
|
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
|
||||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||||
public TimeSpan InvoiceExpiration { get; set; } = TimeSpan.FromMinutes(15);
|
public TimeSpan InvoiceExpiration { get; set; } = TimeSpan.FromMinutes(15);
|
||||||
@@ -43,6 +45,9 @@ namespace BTCPayServer.Client.Models
|
|||||||
public bool LazyPaymentMethods { get; set; }
|
public bool LazyPaymentMethods { get; set; }
|
||||||
public bool RedirectAutomatically { get; set; }
|
public bool RedirectAutomatically { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||||
|
public bool Archived { get; set; }
|
||||||
|
|
||||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||||
public bool ShowRecommendedFee { get; set; } = true;
|
public bool ShowRecommendedFee { get; set; } = true;
|
||||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace BTCPayServer.Client.Models
|
namespace BTCPayServer.Client.Models
|
||||||
{
|
{
|
||||||
public class StoreData : StoreBaseData
|
public class StoreData : StoreBaseData
|
||||||
@@ -17,4 +19,12 @@ namespace BTCPayServer.Client.Models
|
|||||||
|
|
||||||
public string Role { get; set; }
|
public string Role { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class RoleData
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
public List<string> Permissions { get; set; }
|
||||||
|
public string Role { get; set; }
|
||||||
|
public bool IsServerRole { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
62
BTCPayServer.Client/Models/StoreReportRequest.cs
Normal file
62
BTCPayServer.Client/Models/StoreReportRequest.cs
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using BTCPayServer.JsonConverters;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Converters;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Client.Models;
|
||||||
|
|
||||||
|
public class StoreReportRequest
|
||||||
|
{
|
||||||
|
public string ViewName { get; set; }
|
||||||
|
public TimePeriod TimePeriod { get; set; }
|
||||||
|
}
|
||||||
|
public class StoreReportResponse
|
||||||
|
{
|
||||||
|
public class Field
|
||||||
|
{
|
||||||
|
public Field()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
public Field(string name, string type)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
Type = type;
|
||||||
|
}
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Type { get; set; }
|
||||||
|
}
|
||||||
|
public IList<Field> Fields { get; set; } = new List<Field>();
|
||||||
|
public List<JArray> Data { get; set; }
|
||||||
|
public DateTimeOffset From { get; set; }
|
||||||
|
public DateTimeOffset To { get; set; }
|
||||||
|
public List<ChartDefinition> Charts { get; set; }
|
||||||
|
|
||||||
|
public int GetIndex(string fieldName)
|
||||||
|
{
|
||||||
|
return Fields.ToList().FindIndex(f => f.Name == fieldName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ChartDefinition
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
public List<string> Groups { get; set; } = new List<string>();
|
||||||
|
public List<string> Totals { get; set; } = new List<string>();
|
||||||
|
public bool HasGrandTotal { get; set; }
|
||||||
|
public List<string> Aggregates { get; set; } = new List<string>();
|
||||||
|
public List<string> Filters { get; set; } = new List<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TimePeriod
|
||||||
|
{
|
||||||
|
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||||
|
public DateTimeOffset? From { get; set; }
|
||||||
|
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||||
|
public DateTimeOffset? To { get; set; }
|
||||||
|
}
|
16
BTCPayServer.Client/Models/StoreReportsResponse.cs
Normal file
16
BTCPayServer.Client/Models/StoreReportsResponse.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Client.Models
|
||||||
|
{
|
||||||
|
public class StoreReportsResponse
|
||||||
|
{
|
||||||
|
public string ViewName { get; set; }
|
||||||
|
public StoreReportResponse.Field[] Fields
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -51,6 +51,10 @@ namespace BTCPayServer.Client.Models
|
|||||||
public DateTimeOffset Timestamp { get; set; }
|
public DateTimeOffset Timestamp { get; set; }
|
||||||
[JsonExtensionData]
|
[JsonExtensionData]
|
||||||
public IDictionary<string, JToken> AdditionalData { get; set; }
|
public IDictionary<string, JToken> AdditionalData { get; set; }
|
||||||
|
public bool IsPruned()
|
||||||
|
{
|
||||||
|
return DeliveryId is null;
|
||||||
|
}
|
||||||
public T ReadAs<T>()
|
public T ReadAs<T>()
|
||||||
{
|
{
|
||||||
var str = JsonConvert.SerializeObject(this, DefaultSerializerSettings);
|
var str = JsonConvert.SerializeObject(this, DefaultSerializerSettings);
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
|
||||||
namespace BTCPayServer.Client
|
namespace BTCPayServer.Client
|
||||||
{
|
{
|
||||||
@@ -31,6 +33,7 @@ namespace BTCPayServer.Client
|
|||||||
public const string CanManageUsers = "btcpay.server.canmanageusers";
|
public const string CanManageUsers = "btcpay.server.canmanageusers";
|
||||||
public const string CanDeleteUser = "btcpay.user.candeleteuser";
|
public const string CanDeleteUser = "btcpay.user.candeleteuser";
|
||||||
public const string CanManagePullPayments = "btcpay.store.canmanagepullpayments";
|
public const string CanManagePullPayments = "btcpay.store.canmanagepullpayments";
|
||||||
|
public const string CanArchivePullPayments = "btcpay.store.canarchivepullpayments";
|
||||||
public const string CanCreatePullPayments = "btcpay.store.cancreatepullpayments";
|
public const string CanCreatePullPayments = "btcpay.store.cancreatepullpayments";
|
||||||
public const string CanCreateNonApprovedPullPayments = "btcpay.store.cancreatenonapprovedpullpayments";
|
public const string CanCreateNonApprovedPullPayments = "btcpay.store.cancreatenonapprovedpullpayments";
|
||||||
public const string CanViewCustodianAccounts = "btcpay.store.canviewcustodianaccounts";
|
public const string CanViewCustodianAccounts = "btcpay.store.canviewcustodianaccounts";
|
||||||
@@ -67,6 +70,7 @@ namespace BTCPayServer.Client
|
|||||||
yield return CanViewLightningInvoiceInStore;
|
yield return CanViewLightningInvoiceInStore;
|
||||||
yield return CanCreateLightningInvoiceInStore;
|
yield return CanCreateLightningInvoiceInStore;
|
||||||
yield return CanManagePullPayments;
|
yield return CanManagePullPayments;
|
||||||
|
yield return CanArchivePullPayments;
|
||||||
yield return CanCreatePullPayments;
|
yield return CanCreatePullPayments;
|
||||||
yield return CanCreateNonApprovedPullPayments;
|
yield return CanCreateNonApprovedPullPayments;
|
||||||
yield return CanViewCustodianAccounts;
|
yield return CanViewCustodianAccounts;
|
||||||
@@ -134,7 +138,7 @@ namespace BTCPayServer.Client
|
|||||||
{
|
{
|
||||||
static Permission()
|
static Permission()
|
||||||
{
|
{
|
||||||
Init();
|
PolicyMap = Init();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Permission Create(string policy, string scope = null)
|
public static Permission Create(string policy, string scope = null)
|
||||||
@@ -235,11 +239,13 @@ namespace BTCPayServer.Client
|
|||||||
return subPolicies.Contains(subpolicy) || subPolicies.Any(s => ContainsPolicy(s, subpolicy));
|
return subPolicies.Contains(subpolicy) || subPolicies.Any(s => ContainsPolicy(s, subpolicy));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Dictionary<string, HashSet<string>> PolicyMap = new();
|
public static ReadOnlyDictionary<string, HashSet<string>> PolicyMap { get; private set; }
|
||||||
|
|
||||||
|
|
||||||
private static void Init()
|
private static ReadOnlyDictionary<string, HashSet<string>> Init()
|
||||||
{
|
{
|
||||||
PolicyHasChild(Policies.CanModifyStoreSettings,
|
var policyMap = new Dictionary<string, HashSet<string>>();
|
||||||
|
PolicyHasChild(policyMap, Policies.CanModifyStoreSettings,
|
||||||
Policies.CanManageCustodianAccounts,
|
Policies.CanManageCustodianAccounts,
|
||||||
Policies.CanManagePullPayments,
|
Policies.CanManagePullPayments,
|
||||||
Policies.CanModifyInvoices,
|
Policies.CanModifyInvoices,
|
||||||
@@ -248,25 +254,42 @@ namespace BTCPayServer.Client
|
|||||||
Policies.CanModifyPaymentRequests,
|
Policies.CanModifyPaymentRequests,
|
||||||
Policies.CanUseLightningNodeInStore);
|
Policies.CanUseLightningNodeInStore);
|
||||||
|
|
||||||
PolicyHasChild(Policies.CanManageUsers, Policies.CanCreateUser);
|
PolicyHasChild(policyMap,Policies.CanManageUsers, Policies.CanCreateUser);
|
||||||
PolicyHasChild(Policies.CanManagePullPayments, Policies.CanCreatePullPayments);
|
PolicyHasChild(policyMap,Policies.CanManagePullPayments, Policies.CanCreatePullPayments, Policies.CanArchivePullPayments);
|
||||||
PolicyHasChild(Policies.CanCreatePullPayments, Policies.CanCreateNonApprovedPullPayments);
|
PolicyHasChild(policyMap,Policies.CanCreatePullPayments, Policies.CanCreateNonApprovedPullPayments);
|
||||||
PolicyHasChild(Policies.CanModifyPaymentRequests, Policies.CanViewPaymentRequests);
|
PolicyHasChild(policyMap,Policies.CanModifyPaymentRequests, Policies.CanViewPaymentRequests);
|
||||||
PolicyHasChild(Policies.CanModifyProfile, Policies.CanViewProfile);
|
PolicyHasChild(policyMap,Policies.CanModifyProfile, Policies.CanViewProfile);
|
||||||
PolicyHasChild(Policies.CanUseLightningNodeInStore, Policies.CanViewLightningInvoiceInStore, Policies.CanCreateLightningInvoiceInStore);
|
PolicyHasChild(policyMap,Policies.CanUseLightningNodeInStore, Policies.CanViewLightningInvoiceInStore, Policies.CanCreateLightningInvoiceInStore);
|
||||||
PolicyHasChild(Policies.CanManageNotificationsForUser, Policies.CanViewNotificationsForUser);
|
PolicyHasChild(policyMap,Policies.CanManageNotificationsForUser, Policies.CanViewNotificationsForUser);
|
||||||
PolicyHasChild(Policies.CanModifyServerSettings,
|
PolicyHasChild(policyMap,Policies.CanModifyServerSettings,
|
||||||
Policies.CanUseInternalLightningNode,
|
Policies.CanUseInternalLightningNode,
|
||||||
Policies.CanManageUsers);
|
Policies.CanManageUsers);
|
||||||
PolicyHasChild(Policies.CanUseInternalLightningNode, Policies.CanCreateLightningInvoiceInternalNode, Policies.CanViewLightningInvoiceInternalNode);
|
PolicyHasChild(policyMap, Policies.CanUseInternalLightningNode, Policies.CanCreateLightningInvoiceInternalNode, Policies.CanViewLightningInvoiceInternalNode);
|
||||||
PolicyHasChild(Policies.CanManageCustodianAccounts, Policies.CanViewCustodianAccounts);
|
PolicyHasChild(policyMap, Policies.CanManageCustodianAccounts, Policies.CanViewCustodianAccounts);
|
||||||
PolicyHasChild(Policies.CanModifyInvoices, Policies.CanViewInvoices, Policies.CanCreateInvoice, Policies.CanCreateLightningInvoiceInStore);
|
PolicyHasChild(policyMap, Policies.CanModifyInvoices, Policies.CanViewInvoices, Policies.CanCreateInvoice, Policies.CanCreateLightningInvoiceInStore);
|
||||||
PolicyHasChild(Policies.CanViewStoreSettings, Policies.CanViewInvoices, Policies.CanViewPaymentRequests);
|
PolicyHasChild(policyMap, Policies.CanViewStoreSettings, Policies.CanViewInvoices, Policies.CanViewPaymentRequests);
|
||||||
|
|
||||||
|
var missingPolicies = Policies.AllPolicies.ToHashSet();
|
||||||
|
//recurse through the tree to see which policies are not included in the tree
|
||||||
|
foreach (var policy in policyMap)
|
||||||
|
{
|
||||||
|
missingPolicies.Remove(policy.Key);
|
||||||
|
foreach (var subPolicy in policy.Value)
|
||||||
|
{
|
||||||
|
missingPolicies.Remove(subPolicy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var missingPolicy in missingPolicies)
|
||||||
|
{
|
||||||
|
policyMap.Add(missingPolicy, new HashSet<string>());
|
||||||
|
}
|
||||||
|
return new ReadOnlyDictionary<string, HashSet<string>>(policyMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void PolicyHasChild(string policy, params string[] subPolicies)
|
private static void PolicyHasChild(Dictionary<string, HashSet<string>>policyMap, string policy, params string[] subPolicies)
|
||||||
{
|
{
|
||||||
if (PolicyMap.TryGetValue(policy, out var existingSubPolicies))
|
if (policyMap.TryGetValue(policy, out var existingSubPolicies))
|
||||||
{
|
{
|
||||||
foreach (string subPolicy in subPolicies)
|
foreach (string subPolicy in subPolicies)
|
||||||
{
|
{
|
||||||
@@ -275,7 +298,7 @@ namespace BTCPayServer.Client
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
PolicyMap.Add(policy, subPolicies.ToHashSet());
|
policyMap.Add(policy, subPolicies.ToHashSet());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -16,7 +16,7 @@ namespace BTCPayServer
|
|||||||
DefaultRateRules = new[]
|
DefaultRateRules = new[]
|
||||||
{
|
{
|
||||||
"BTG_X = BTG_BTC * BTC_X",
|
"BTG_X = BTG_BTC * BTC_X",
|
||||||
"BTG_BTC = bitfinex(BTG_BTC)",
|
"BTG_BTC = gate(BTG_BTC)",
|
||||||
},
|
},
|
||||||
CryptoImagePath = "imlegacy/btg.svg",
|
CryptoImagePath = "imlegacy/btg.svg",
|
||||||
LightningImagePath = "imlegacy/btg-lightning.svg",
|
LightningImagePath = "imlegacy/btg-lightning.svg",
|
||||||
|
@@ -17,7 +17,7 @@ namespace BTCPayServer
|
|||||||
DefaultRateRules = new[]
|
DefaultRateRules = new[]
|
||||||
{
|
{
|
||||||
"BTX_X = BTX_BTC * BTC_X",
|
"BTX_X = BTX_BTC * BTC_X",
|
||||||
"BTX_BTC = hitbtc(BTX_BTC)"
|
"BTX_BTC = graviex(BTX_BTC)"
|
||||||
},
|
},
|
||||||
CryptoImagePath = "imlegacy/bitcore.svg",
|
CryptoImagePath = "imlegacy/bitcore.svg",
|
||||||
LightningImagePath = "imlegacy/bitcore-lightning.svg",
|
LightningImagePath = "imlegacy/bitcore-lightning.svg",
|
||||||
|
@@ -1,32 +0,0 @@
|
|||||||
using NBitcoin;
|
|
||||||
|
|
||||||
namespace BTCPayServer
|
|
||||||
{
|
|
||||||
public partial class BTCPayNetworkProvider
|
|
||||||
{
|
|
||||||
public void InitChaincoin()
|
|
||||||
{
|
|
||||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("CHC");
|
|
||||||
Add(new BTCPayNetwork()
|
|
||||||
{
|
|
||||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
|
||||||
DisplayName = "Chaincoin",
|
|
||||||
BlockExplorerLink = NetworkType == ChainName.Mainnet
|
|
||||||
? "https://explorer.chaincoin.org/Explorer/Transaction/{0}"
|
|
||||||
: "https://test.explorer.chaincoin.org/Explorer/Transaction/tx/{0}",
|
|
||||||
NBXplorerNetwork = nbxplorerNetwork,
|
|
||||||
DefaultRateRules = new[]
|
|
||||||
{
|
|
||||||
"CHC_X = CHC_BTC * BTC_X",
|
|
||||||
"CHC_BTC = txbit(CHC_X)"
|
|
||||||
},
|
|
||||||
CryptoImagePath = "imlegacy/chaincoin.png",
|
|
||||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
|
||||||
//https://github.com/satoshilabs/slips/blob/master/slip-0044.md
|
|
||||||
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("711'")
|
|
||||||
: new KeyPath("1'")
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@@ -19,7 +19,7 @@ namespace BTCPayServer
|
|||||||
"USDT_X = USDT_BTC * BTC_X",
|
"USDT_X = USDT_BTC * BTC_X",
|
||||||
"USDT_BTC = bitfinex(UST_BTC)",
|
"USDT_BTC = bitfinex(UST_BTC)",
|
||||||
},
|
},
|
||||||
AssetId = new uint256("ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2"),
|
AssetId = NetworkType == ChainName.Regtest? null: new uint256("ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2"),
|
||||||
DisplayName = "Liquid Tether",
|
DisplayName = "Liquid Tether",
|
||||||
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://liquid.network/tx/{0}" : "https://liquid.network/testnet/tx/{0}",
|
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://liquid.network/tx/{0}" : "https://liquid.network/testnet/tx/{0}",
|
||||||
NBXplorerNetwork = nbxplorerNetwork,
|
NBXplorerNetwork = nbxplorerNetwork,
|
||||||
@@ -42,7 +42,7 @@ namespace BTCPayServer
|
|||||||
"ETB_BTC = bitpay(ETB_BTC)"
|
"ETB_BTC = bitpay(ETB_BTC)"
|
||||||
},
|
},
|
||||||
Divisibility = 2,
|
Divisibility = 2,
|
||||||
AssetId = new uint256("aa775044c32a7df391902b3659f46dfe004ccb2644ce2ddc7dba31e889391caf"),
|
AssetId = NetworkType == ChainName.Regtest? null: new uint256("aa775044c32a7df391902b3659f46dfe004ccb2644ce2ddc7dba31e889391caf"),
|
||||||
DisplayName = "Ethiopian Birr",
|
DisplayName = "Ethiopian Birr",
|
||||||
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://liquid.network/tx/{0}" : "https://liquid.network/testnet/tx/{0}",
|
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://liquid.network/tx/{0}" : "https://liquid.network/testnet/tx/{0}",
|
||||||
NBXplorerNetwork = nbxplorerNetwork,
|
NBXplorerNetwork = nbxplorerNetwork,
|
||||||
@@ -63,8 +63,9 @@ namespace BTCPayServer
|
|||||||
"LCAD_CAD = 1",
|
"LCAD_CAD = 1",
|
||||||
"LCAD_X = CAD_BTC * BTC_X",
|
"LCAD_X = CAD_BTC * BTC_X",
|
||||||
"LCAD_BTC = bylls(CAD_BTC)",
|
"LCAD_BTC = bylls(CAD_BTC)",
|
||||||
|
"CAD_BTC = LCAD_BTC"
|
||||||
},
|
},
|
||||||
AssetId = new uint256("0e99c1a6da379d1f4151fb9df90449d40d0608f6cb33a5bcbfc8c265f42bab0a"),
|
AssetId = NetworkType == ChainName.Regtest? null: new uint256("0e99c1a6da379d1f4151fb9df90449d40d0608f6cb33a5bcbfc8c265f42bab0a"),
|
||||||
DisplayName = "Liquid CAD",
|
DisplayName = "Liquid CAD",
|
||||||
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://liquid.network/tx/{0}" : "https://liquid.network/testnet/tx/{0}",
|
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://liquid.network/tx/{0}" : "https://liquid.network/testnet/tx/{0}",
|
||||||
NBXplorerNetwork = nbxplorerNetwork,
|
NBXplorerNetwork = nbxplorerNetwork,
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
#if ALTCOINS
|
#if ALTCOINS
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using BTCPayServer.Common;
|
using BTCPayServer.Common;
|
||||||
@@ -18,7 +19,8 @@ namespace BTCPayServer
|
|||||||
NewTransactionEvent evtOutputs)
|
NewTransactionEvent evtOutputs)
|
||||||
{
|
{
|
||||||
return evtOutputs.Outputs.Where(output =>
|
return evtOutputs.Outputs.Where(output =>
|
||||||
output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId).Select(output =>
|
(output.Value is not AssetMoney && NetworkCryptoCode.Equals(evtOutputs.CryptoCode, StringComparison.InvariantCultureIgnoreCase)) ||
|
||||||
|
(output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId)).Select(output =>
|
||||||
{
|
{
|
||||||
var outpoint = new OutPoint(evtOutputs.TransactionData.TransactionHash, output.Index);
|
var outpoint = new OutPoint(evtOutputs.TransactionData.TransactionHash, output.Index);
|
||||||
return (output, outpoint);
|
return (output, outpoint);
|
||||||
@@ -34,12 +36,12 @@ namespace BTCPayServer
|
|||||||
output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId));
|
output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override PaymentUrlBuilder GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue)
|
public override PaymentUrlBuilder GenerateBIP21(string cryptoInfoAddress, decimal? cryptoInfoDue)
|
||||||
{
|
{
|
||||||
//precision 0: 10 = 0.00000010
|
//precision 0: 10 = 0.00000010
|
||||||
//precision 2: 10 = 0.00001000
|
//precision 2: 10 = 0.00001000
|
||||||
//precision 8: 10 = 10
|
//precision 8: 10 = 10
|
||||||
var money = cryptoInfoDue is null ? null : new Money(cryptoInfoDue.ToDecimal(MoneyUnit.BTC) / decimal.Parse("1".PadRight(1 + 8 - Divisibility, '0')), MoneyUnit.BTC);
|
var money = cryptoInfoDue / (decimal)Math.Pow(10, 8 - Divisibility);
|
||||||
var builder = base.GenerateBIP21(cryptoInfoAddress, money);
|
var builder = base.GenerateBIP21(cryptoInfoAddress, money);
|
||||||
builder.QueryParams.Add("assetid", AssetId.ToString());
|
builder.QueryParams.Add("assetid", AssetId.ToString());
|
||||||
return builder;
|
return builder;
|
||||||
|
@@ -45,10 +45,10 @@ namespace BTCPayServer.Services.Altcoins.Monero.RPC
|
|||||||
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Basic",
|
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Basic",
|
||||||
Convert.ToBase64String(Encoding.Default.GetBytes($"{_username}:{_password}")));
|
Convert.ToBase64String(Encoding.Default.GetBytes($"{_username}:{_password}")));
|
||||||
|
|
||||||
var rawResult = await _httpClient.SendAsync(httpRequest, cts);
|
HttpResponseMessage rawResult = await _httpClient.SendAsync(httpRequest, cts);
|
||||||
|
|
||||||
var rawJson = await rawResult.Content.ReadAsStringAsync();
|
|
||||||
rawResult.EnsureSuccessStatusCode();
|
rawResult.EnsureSuccessStatusCode();
|
||||||
|
var rawJson = await rawResult.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
JsonRpcResult<TResponse> response;
|
JsonRpcResult<TResponse> response;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using BTCPayServer.Common;
|
using BTCPayServer.Common;
|
||||||
@@ -87,13 +88,13 @@ namespace BTCPayServer
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual PaymentUrlBuilder GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue)
|
public virtual PaymentUrlBuilder GenerateBIP21(string cryptoInfoAddress, decimal? cryptoInfoDue)
|
||||||
{
|
{
|
||||||
var builder = new PaymentUrlBuilder(this.NBitcoinNetwork.UriScheme);
|
var builder = new PaymentUrlBuilder(this.NBitcoinNetwork.UriScheme);
|
||||||
builder.Host = cryptoInfoAddress;
|
builder.Host = cryptoInfoAddress;
|
||||||
if (cryptoInfoDue != null && cryptoInfoDue != Money.Zero)
|
if (cryptoInfoDue is not null && cryptoInfoDue.Value != 0.0m)
|
||||||
{
|
{
|
||||||
builder.QueryParams.Add("amount", cryptoInfoDue.ToString(false, true));
|
builder.QueryParams.Add("amount", cryptoInfoDue.Value.ToString(CultureInfo.InvariantCulture));
|
||||||
}
|
}
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
@@ -56,7 +56,6 @@ namespace BTCPayServer
|
|||||||
InitViacoin();
|
InitViacoin();
|
||||||
InitMonero();
|
InitMonero();
|
||||||
InitZcash();
|
InitZcash();
|
||||||
InitChaincoin();
|
|
||||||
// InitArgoneum();//their rate source is down 9/15/20.
|
// InitArgoneum();//their rate source is down 9/15/20.
|
||||||
// InitMonetaryUnit(); Not supported from Bittrex from 11/23/2022, dead shitcoin
|
// InitMonetaryUnit(); Not supported from Bittrex from 11/23/2022, dead shitcoin
|
||||||
|
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
<PackageReference Include="NBXplorer.Client" Version="4.2.3" />
|
<PackageReference Include="NBXplorer.Client" Version="4.2.5" />
|
||||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="2.0.1" />
|
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="2.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Condition="'$(Altcoins)' != 'true'">
|
<ItemGroup Condition="'$(Altcoins)' != 'true'">
|
||||||
|
@@ -64,6 +64,7 @@ namespace BTCPayServer.Data
|
|||||||
public DbSet<U2FDevice> U2FDevices { get; set; }
|
public DbSet<U2FDevice> U2FDevices { get; set; }
|
||||||
public DbSet<Fido2Credential> Fido2Credentials { get; set; }
|
public DbSet<Fido2Credential> Fido2Credentials { get; set; }
|
||||||
public DbSet<UserStore> UserStore { get; set; }
|
public DbSet<UserStore> UserStore { get; set; }
|
||||||
|
public DbSet<StoreRole> StoreRoles { get; set; }
|
||||||
[Obsolete]
|
[Obsolete]
|
||||||
public DbSet<WalletData> Wallets { get; set; }
|
public DbSet<WalletData> Wallets { get; set; }
|
||||||
public DbSet<WalletObjectData> WalletObjects { get; set; }
|
public DbSet<WalletObjectData> WalletObjects { get; set; }
|
||||||
@@ -129,6 +130,7 @@ namespace BTCPayServer.Data
|
|||||||
PayoutProcessorData.OnModelCreating(builder, Database);
|
PayoutProcessorData.OnModelCreating(builder, Database);
|
||||||
WebhookData.OnModelCreating(builder, Database);
|
WebhookData.OnModelCreating(builder, Database);
|
||||||
FormData.OnModelCreating(builder, Database);
|
FormData.OnModelCreating(builder, Database);
|
||||||
|
StoreRole.OnModelCreating(builder, Database);
|
||||||
|
|
||||||
|
|
||||||
if (Database.IsSqlite() && !_designTime)
|
if (Database.IsSqlite() && !_designTime)
|
||||||
|
@@ -1,3 +1,6 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Abstractions.Contracts;
|
using BTCPayServer.Abstractions.Contracts;
|
||||||
using BTCPayServer.Abstractions.Models;
|
using BTCPayServer.Abstractions.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
@@ -14,6 +14,7 @@ namespace BTCPayServer.Data
|
|||||||
public DateTimeOffset Created { get; set; }
|
public DateTimeOffset Created { get; set; }
|
||||||
public bool TagAllInvoices { get; set; }
|
public bool TagAllInvoices { get; set; }
|
||||||
public string Settings { get; set; }
|
public string Settings { get; set; }
|
||||||
|
public bool Archived { get; set; }
|
||||||
|
|
||||||
internal static void OnModelCreating(ModelBuilder builder)
|
internal static void OnModelCreating(ModelBuilder builder)
|
||||||
{
|
{
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace BTCPayServer.Data;
|
namespace BTCPayServer.Data;
|
||||||
@@ -41,4 +43,7 @@ public class LightningAddressDataBlob
|
|||||||
public decimal? Max { get; set; }
|
public decimal? Max { get; set; }
|
||||||
|
|
||||||
public JObject InvoiceMetadata { get; set; }
|
public JObject InvoiceMetadata { get; set; }
|
||||||
|
|
||||||
|
[JsonExtensionData] public Dictionary<string, JToken> AdditionalData { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -8,6 +8,7 @@ namespace BTCPayServer.Data;
|
|||||||
public class AutomatedPayoutBlob
|
public class AutomatedPayoutBlob
|
||||||
{
|
{
|
||||||
public TimeSpan Interval { get; set; } = TimeSpan.FromHours(1);
|
public TimeSpan Interval { get; set; } = TimeSpan.FromHours(1);
|
||||||
|
public bool ProcessNewPayoutsInstantly { get; set; }
|
||||||
}
|
}
|
||||||
public class PayoutProcessorData : IHasBlobUntyped
|
public class PayoutProcessorData : IHasBlobUntyped
|
||||||
{
|
{
|
||||||
|
@@ -1,13 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using BTCPayServer.Client;
|
|
||||||
using BTCPayServer.Client.Models;
|
using BTCPayServer.Client.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
using PayoutProcessorData = BTCPayServer.Data.PayoutProcessorData;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Data
|
namespace BTCPayServer.Data
|
||||||
{
|
{
|
||||||
@@ -37,8 +34,6 @@ namespace BTCPayServer.Data
|
|||||||
|
|
||||||
public byte[] StoreCertificate { get; set; }
|
public byte[] StoreCertificate { get; set; }
|
||||||
|
|
||||||
[NotMapped] public string Role { get; set; }
|
|
||||||
|
|
||||||
public string StoreBlob { get; set; }
|
public string StoreBlob { get; set; }
|
||||||
|
|
||||||
[Obsolete("Use GetDefaultPaymentId instead")]
|
[Obsolete("Use GetDefaultPaymentId instead")]
|
||||||
@@ -52,6 +47,8 @@ namespace BTCPayServer.Data
|
|||||||
public IEnumerable<CustodianAccountData> CustodianAccounts { get; set; }
|
public IEnumerable<CustodianAccountData> CustodianAccounts { get; set; }
|
||||||
public IEnumerable<StoreSettingData> Settings { get; set; }
|
public IEnumerable<StoreSettingData> Settings { get; set; }
|
||||||
public IEnumerable<FormData> Forms { get; set; }
|
public IEnumerable<FormData> Forms { get; set; }
|
||||||
|
public IEnumerable<StoreRole> StoreRoles { get; set; }
|
||||||
|
public bool Archived { get; set; }
|
||||||
|
|
||||||
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||||
{
|
{
|
||||||
|
50
BTCPayServer.Data/Data/StoreRole.cs
Normal file
50
BTCPayServer.Data/Data/StoreRole.cs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Data;
|
||||||
|
|
||||||
|
public class StoreRole
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
public string StoreDataId { get; set; }
|
||||||
|
public string Role { get; set; }
|
||||||
|
public List<string> Permissions { get; set; }
|
||||||
|
public List<UserStore> Users { get; set; }
|
||||||
|
public StoreData StoreData { get; set; }
|
||||||
|
|
||||||
|
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||||
|
{
|
||||||
|
builder.Entity<StoreRole>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasOne(e => e.StoreData)
|
||||||
|
.WithMany(s => s.StoreRoles)
|
||||||
|
.HasForeignKey(e => e.StoreDataId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired(false);
|
||||||
|
|
||||||
|
entity.HasIndex(entity => new {entity.StoreDataId, entity.Role}).IsUnique();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (!databaseFacade.IsNpgsql())
|
||||||
|
{
|
||||||
|
builder.Entity<StoreRole>()
|
||||||
|
.Property(o => o.Permissions)
|
||||||
|
.HasConversion(
|
||||||
|
v => JsonConvert.SerializeObject(v),
|
||||||
|
v => JsonConvert.DeserializeObject<List<string>>(v)?? new List<string>(),
|
||||||
|
new ValueComparer<List<string>>(
|
||||||
|
(c1, c2) => c1 ==c2 || c1 != null && c2 != null && c1.SequenceEqual(c2),
|
||||||
|
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
|
||||||
|
c => c.ToList()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,3 +1,4 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace BTCPayServer.Data
|
namespace BTCPayServer.Data
|
||||||
@@ -9,7 +10,10 @@ namespace BTCPayServer.Data
|
|||||||
|
|
||||||
public string StoreDataId { get; set; }
|
public string StoreDataId { get; set; }
|
||||||
public StoreData StoreData { get; set; }
|
public StoreData StoreData { get; set; }
|
||||||
public string Role { get; set; }
|
[Column("Role")]
|
||||||
|
public string StoreRoleId { get; set; }
|
||||||
|
public StoreRole StoreRole { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
internal static void OnModelCreating(ModelBuilder builder)
|
internal static void OnModelCreating(ModelBuilder builder)
|
||||||
@@ -32,6 +36,10 @@ namespace BTCPayServer.Data
|
|||||||
.HasOne(pt => pt.StoreData)
|
.HasOne(pt => pt.StoreData)
|
||||||
.WithMany(t => t.UserStores)
|
.WithMany(t => t.UserStores)
|
||||||
.HasForeignKey(pt => pt.StoreDataId);
|
.HasForeignKey(pt => pt.StoreDataId);
|
||||||
|
|
||||||
|
builder.Entity<UserStore>().HasOne(e => e.StoreRole)
|
||||||
|
.WithMany(role => role.Users)
|
||||||
|
.HasForeignKey(e => e.StoreRoleId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
|
|||||||
|
|
||||||
namespace BTCPayServer.Data
|
namespace BTCPayServer.Data
|
||||||
{
|
{
|
||||||
public class WebhookDeliveryData : IHasBlobUntyped
|
public class WebhookDeliveryData
|
||||||
{
|
{
|
||||||
[Key]
|
[Key]
|
||||||
[MaxLength(25)]
|
[MaxLength(25)]
|
||||||
@@ -17,10 +17,8 @@ namespace BTCPayServer.Data
|
|||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
public DateTimeOffset Timestamp { get; set; }
|
public DateTimeOffset Timestamp { get; set; }
|
||||||
[Obsolete("Use Blob2 instead")]
|
public string Blob { get; set; }
|
||||||
public byte[] Blob { get; set; }
|
public bool Pruned { get; set; }
|
||||||
public string Blob2 { get; set; }
|
|
||||||
|
|
||||||
|
|
||||||
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||||
{
|
{
|
||||||
@@ -28,11 +26,11 @@ namespace BTCPayServer.Data
|
|||||||
.HasOne(o => o.Webhook)
|
.HasOne(o => o.Webhook)
|
||||||
.WithMany(a => a.Deliveries).OnDelete(DeleteBehavior.Cascade);
|
.WithMany(a => a.Deliveries).OnDelete(DeleteBehavior.Cascade);
|
||||||
builder.Entity<WebhookDeliveryData>().HasIndex(o => o.WebhookId);
|
builder.Entity<WebhookDeliveryData>().HasIndex(o => o.WebhookId);
|
||||||
|
builder.Entity<WebhookDeliveryData>().HasIndex(o => o.Timestamp);
|
||||||
if (databaseFacade.IsNpgsql())
|
if (databaseFacade.IsNpgsql())
|
||||||
{
|
{
|
||||||
builder.Entity<WebhookDeliveryData>()
|
builder.Entity<WebhookDeliveryData>()
|
||||||
.Property(o => o.Blob2)
|
.Property(o => o.Blob)
|
||||||
.HasColumnType("JSONB");
|
.HasColumnType("JSONB");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
106
BTCPayServer.Data/Migrations/20230504125505_StoreRoles.cs
Normal file
106
BTCPayServer.Data/Migrations/20230504125505_StoreRoles.cs
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
using System;
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using NBitcoin;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace BTCPayServer.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20230504125505_StoreRoles")]
|
||||||
|
public partial class StoreRoles : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
var permissionsType = migrationBuilder.IsNpgsql() ? "TEXT[]" : "TEXT";
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "StoreRoles",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
StoreDataId = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
Role = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Permissions = table.Column<string>(type: permissionsType, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_StoreRoles", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_StoreRoles_Stores_StoreDataId",
|
||||||
|
column: x => x.StoreDataId,
|
||||||
|
principalTable: "Stores",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_StoreRoles_StoreDataId_Role",
|
||||||
|
table: "StoreRoles",
|
||||||
|
columns: new[] { "StoreDataId", "Role" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
object GetPermissionsData(string[] permissions)
|
||||||
|
{
|
||||||
|
if (migrationBuilder.IsNpgsql())
|
||||||
|
return permissions;
|
||||||
|
return JsonConvert.SerializeObject(permissions);
|
||||||
|
}
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
"StoreRoles",
|
||||||
|
columns: new[] { "Id", "Role", "Permissions" },
|
||||||
|
columnTypes: new[] { "TEXT", "TEXT", permissionsType },
|
||||||
|
values: new object[,]
|
||||||
|
{
|
||||||
|
{
|
||||||
|
"Owner", "Owner", GetPermissionsData(new[]
|
||||||
|
{
|
||||||
|
"btcpay.store.canmodifystoresettings",
|
||||||
|
"btcpay.store.cantradecustodianaccount",
|
||||||
|
"btcpay.store.canwithdrawfromcustodianaccount",
|
||||||
|
"btcpay.store.candeposittocustodianaccount"
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Guest", "Guest", GetPermissionsData(new[]
|
||||||
|
{
|
||||||
|
"btcpay.store.canviewstoresettings",
|
||||||
|
"btcpay.store.canmodifyinvoices",
|
||||||
|
"btcpay.store.canviewcustodianaccounts",
|
||||||
|
"btcpay.store.candeposittocustodianaccount"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.SupportAddForeignKey(migrationBuilder.ActiveProvider))
|
||||||
|
{
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_UserStore_StoreRoles_Role",
|
||||||
|
table: "UserStore",
|
||||||
|
column: "Role",
|
||||||
|
principalTable: "StoreRoles",
|
||||||
|
principalColumn: "Id");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
|
||||||
|
if (this.SupportDropForeignKey(migrationBuilder.ActiveProvider))
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_UserStore_StoreRoles_Role",
|
||||||
|
table: "UserStore");
|
||||||
|
}
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "StoreRoles");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,83 @@
|
|||||||
|
using System;
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using NBitcoin;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace BTCPayServer.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20230529135505_WebhookDeliveriesCleanup")]
|
||||||
|
public partial class WebhookDeliveriesCleanup : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
if (migrationBuilder.IsNpgsql())
|
||||||
|
{
|
||||||
|
migrationBuilder.Sql("DROP TABLE IF EXISTS \"InvoiceWebhookDeliveries\", \"WebhookDeliveries\";");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "WebhookDeliveries",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
WebhookId = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Timestamp = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||||
|
Pruned = table.Column<bool>(type: "BOOLEAN", nullable: false),
|
||||||
|
Blob = table.Column<string>(type: "JSONB", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_WebhookDeliveries", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_WebhookDeliveries_Webhooks_WebhookId",
|
||||||
|
column: x => x.WebhookId,
|
||||||
|
principalTable: "Webhooks",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_WebhookDeliveries_WebhookId",
|
||||||
|
table: "WebhookDeliveries",
|
||||||
|
column: "WebhookId");
|
||||||
|
migrationBuilder.Sql("CREATE INDEX \"IX_WebhookDeliveries_Timestamp\" ON \"WebhookDeliveries\"(\"Timestamp\") WHERE \"Pruned\" IS FALSE");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "InvoiceWebhookDeliveries",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
InvoiceId = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
DeliveryId = table.Column<string>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_InvoiceWebhookDeliveries", x => new { x.InvoiceId, x.DeliveryId });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_InvoiceWebhookDeliveries_WebhookDeliveries_DeliveryId",
|
||||||
|
column: x => x.DeliveryId,
|
||||||
|
principalTable: "WebhookDeliveries",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_InvoiceWebhookDeliveries_Invoices_InvoiceId",
|
||||||
|
column: x => x.InvoiceId,
|
||||||
|
principalTable: "Invoices",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,39 @@
|
|||||||
|
using BTCPayServer.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace BTCPayServer.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20230906135844_AddArchivedFlagForStoresAndApps")]
|
||||||
|
public partial class AddArchivedFlagForStoresAndApps : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "Archived",
|
||||||
|
table: "Stores",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "Archived",
|
||||||
|
table: "Apps",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Archived",
|
||||||
|
table: "Stores");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Archived",
|
||||||
|
table: "Apps");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -79,6 +79,9 @@ namespace BTCPayServer.Migrations
|
|||||||
b.Property<string>("AppType")
|
b.Property<string>("AppType")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("Archived")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("Created")
|
b.Property<DateTimeOffset>("Created")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
@@ -214,56 +217,6 @@ namespace BTCPayServer.Migrations
|
|||||||
b.ToTable("CustodianAccount");
|
b.ToTable("CustodianAccount");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("BTCPayServer.Data.FormData", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Config")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<bool>("Public")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("StoreId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("StoreId");
|
|
||||||
|
|
||||||
b.ToTable("Forms");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("BTCPayServer.Data.PayoutProcessorData", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<byte[]>("Blob")
|
|
||||||
.HasColumnType("BLOB");
|
|
||||||
|
|
||||||
b.Property<string>("PaymentMethod")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Processor")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("StoreId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("StoreId");
|
|
||||||
|
|
||||||
b.ToTable("PayoutProcessors");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("BTCPayServer.Data.Fido2Credential", b =>
|
modelBuilder.Entity("BTCPayServer.Data.Fido2Credential", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Id")
|
b.Property<string>("Id")
|
||||||
@@ -292,6 +245,31 @@ namespace BTCPayServer.Migrations
|
|||||||
b.ToTable("Fido2Credentials");
|
b.ToTable("Fido2Credentials");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.FormData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Config")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("Public")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("StoreId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("StoreId");
|
||||||
|
|
||||||
|
b.ToTable("Forms");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Id")
|
b.Property<string>("Id")
|
||||||
@@ -655,6 +633,34 @@ namespace BTCPayServer.Migrations
|
|||||||
b.ToTable("Payouts");
|
b.ToTable("Payouts");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.PayoutProcessorData", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<byte[]>("Blob")
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<string>("Blob2")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PaymentMethod")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Processor")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("StoreId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("StoreId");
|
||||||
|
|
||||||
|
b.ToTable("PayoutProcessors");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Id")
|
b.Property<string>("Id")
|
||||||
@@ -748,6 +754,9 @@ namespace BTCPayServer.Migrations
|
|||||||
b.Property<string>("Id")
|
b.Property<string>("Id")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("Archived")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<string>("DefaultCrypto")
|
b.Property<string>("DefaultCrypto")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
@@ -802,6 +811,28 @@ namespace BTCPayServer.Migrations
|
|||||||
b.ToTable("Files");
|
b.ToTable("Files");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.StoreRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Permissions")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Role")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("StoreDataId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("StoreDataId", "Role")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("StoreRoles");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("BTCPayServer.Data.StoreSettingData", b =>
|
modelBuilder.Entity("BTCPayServer.Data.StoreSettingData", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("StoreId")
|
b.Property<string>("StoreId")
|
||||||
@@ -878,13 +909,16 @@ namespace BTCPayServer.Migrations
|
|||||||
b.Property<string>("StoreDataId")
|
b.Property<string>("StoreDataId")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("Role")
|
b.Property<string>("StoreRoleId")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("Role");
|
||||||
|
|
||||||
b.HasKey("ApplicationUserId", "StoreDataId");
|
b.HasKey("ApplicationUserId", "StoreDataId");
|
||||||
|
|
||||||
b.HasIndex("StoreDataId");
|
b.HasIndex("StoreDataId");
|
||||||
|
|
||||||
|
b.HasIndex("StoreRoleId");
|
||||||
|
|
||||||
b.ToTable("UserStore");
|
b.ToTable("UserStore");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -991,12 +1025,12 @@ namespace BTCPayServer.Migrations
|
|||||||
.HasMaxLength(25)
|
.HasMaxLength(25)
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<byte[]>("Blob")
|
b.Property<string>("Blob")
|
||||||
.HasColumnType("BLOB");
|
|
||||||
|
|
||||||
b.Property<string>("Blob2")
|
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("Pruned")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("Timestamp")
|
b.Property<DateTimeOffset>("Timestamp")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
@@ -1007,6 +1041,8 @@ namespace BTCPayServer.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Timestamp");
|
||||||
|
|
||||||
b.HasIndex("WebhookId");
|
b.HasIndex("WebhookId");
|
||||||
|
|
||||||
b.ToTable("WebhookDeliveries");
|
b.ToTable("WebhookDeliveries");
|
||||||
@@ -1188,26 +1224,6 @@ namespace BTCPayServer.Migrations
|
|||||||
b.Navigation("StoreData");
|
b.Navigation("StoreData");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("BTCPayServer.Data.FormData", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("BTCPayServer.Data.StoreData", "Store")
|
|
||||||
.WithMany("Forms")
|
|
||||||
.HasForeignKey("StoreId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
|
|
||||||
b.Navigation("Store");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("BTCPayServer.Data.PayoutProcessorData", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("BTCPayServer.Data.StoreData", "Store")
|
|
||||||
.WithMany("PayoutProcessors")
|
|
||||||
.HasForeignKey("StoreId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
|
|
||||||
b.Navigation("Store");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("BTCPayServer.Data.Fido2Credential", b =>
|
modelBuilder.Entity("BTCPayServer.Data.Fido2Credential", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser")
|
b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser")
|
||||||
@@ -1218,6 +1234,16 @@ namespace BTCPayServer.Migrations
|
|||||||
b.Navigation("ApplicationUser");
|
b.Navigation("ApplicationUser");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.FormData", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Data.StoreData", "Store")
|
||||||
|
.WithMany("Forms")
|
||||||
|
.HasForeignKey("StoreId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
b.Navigation("Store");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||||
@@ -1343,6 +1369,16 @@ namespace BTCPayServer.Migrations
|
|||||||
b.Navigation("StoreData");
|
b.Navigation("StoreData");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.PayoutProcessorData", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Data.StoreData", "Store")
|
||||||
|
.WithMany("PayoutProcessors")
|
||||||
|
.HasForeignKey("StoreId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
b.Navigation("Store");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||||
@@ -1392,6 +1428,16 @@ namespace BTCPayServer.Migrations
|
|||||||
b.Navigation("ApplicationUser");
|
b.Navigation("ApplicationUser");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.StoreRole", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||||
|
.WithMany("StoreRoles")
|
||||||
|
.HasForeignKey("StoreDataId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
b.Navigation("StoreData");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("BTCPayServer.Data.StoreSettingData", b =>
|
modelBuilder.Entity("BTCPayServer.Data.StoreSettingData", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("BTCPayServer.Data.StoreData", "Store")
|
b.HasOne("BTCPayServer.Data.StoreData", "Store")
|
||||||
@@ -1446,9 +1492,15 @@ namespace BTCPayServer.Migrations
|
|||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("BTCPayServer.Data.StoreRole", "StoreRole")
|
||||||
|
.WithMany("Users")
|
||||||
|
.HasForeignKey("StoreRoleId");
|
||||||
|
|
||||||
b.Navigation("ApplicationUser");
|
b.Navigation("ApplicationUser");
|
||||||
|
|
||||||
b.Navigation("StoreData");
|
b.Navigation("StoreData");
|
||||||
|
|
||||||
|
b.Navigation("StoreRole");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("BTCPayServer.Data.WalletObjectLinkData", b =>
|
modelBuilder.Entity("BTCPayServer.Data.WalletObjectLinkData", b =>
|
||||||
@@ -1606,9 +1658,16 @@ namespace BTCPayServer.Migrations
|
|||||||
|
|
||||||
b.Navigation("Settings");
|
b.Navigation("Settings");
|
||||||
|
|
||||||
|
b.Navigation("StoreRoles");
|
||||||
|
|
||||||
b.Navigation("UserStores");
|
b.Navigation("UserStores");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("BTCPayServer.Data.StoreRole", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Users");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("BTCPayServer.Data.WalletData", b =>
|
modelBuilder.Entity("BTCPayServer.Data.WalletData", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("WalletTransactions");
|
b.Navigation("WalletTransactions");
|
||||||
|
@@ -1,37 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Rating
|
|
||||||
{
|
|
||||||
public enum RateSource
|
|
||||||
{
|
|
||||||
Coingecko,
|
|
||||||
Direct
|
|
||||||
}
|
|
||||||
public class AvailableRateProvider
|
|
||||||
{
|
|
||||||
public string Name { get; }
|
|
||||||
public string Url { get; }
|
|
||||||
public string Id { get; }
|
|
||||||
public RateSource Source { get; }
|
|
||||||
|
|
||||||
public AvailableRateProvider(string id, string name, string url) : this(id, name, url, RateSource.Direct)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
public AvailableRateProvider(string id, string name, string url, RateSource source)
|
|
||||||
{
|
|
||||||
Id = id;
|
|
||||||
Name = name;
|
|
||||||
Url = url;
|
|
||||||
Source = source;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string DisplayName =>
|
|
||||||
Source switch
|
|
||||||
{
|
|
||||||
RateSource.Direct => Name,
|
|
||||||
RateSource.Coingecko => $"{Name} (via CoinGecko)",
|
|
||||||
_ => throw new NotSupportedException(Source.ToString())
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@@ -7,8 +7,8 @@
|
|||||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" />
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" />
|
||||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.9" />
|
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.9" />
|
||||||
<PackageReference Include="NBitcoin" Version="7.0.24" />
|
<PackageReference Include="NBitcoin" Version="7.0.24" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="1.0.2" />
|
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="1.0.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@@ -20,13 +20,13 @@ namespace BTCPayServer.Services.Rates
|
|||||||
}
|
}
|
||||||
public class CurrencyNameTable
|
public class CurrencyNameTable
|
||||||
{
|
{
|
||||||
public static CurrencyNameTable Instance = new CurrencyNameTable();
|
public static CurrencyNameTable Instance = new();
|
||||||
public CurrencyNameTable()
|
public CurrencyNameTable()
|
||||||
{
|
{
|
||||||
_Currencies = LoadCurrency().ToDictionary(k => k.Code);
|
_Currencies = LoadCurrency().ToDictionary(k => k.Code, StringComparer.InvariantCultureIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
static readonly Dictionary<string, IFormatProvider> _CurrencyProviders = new Dictionary<string, IFormatProvider>();
|
static readonly Dictionary<string, IFormatProvider> _CurrencyProviders = new();
|
||||||
|
|
||||||
public NumberFormatInfo GetNumberFormatInfo(string currency, bool useFallback)
|
public NumberFormatInfo GetNumberFormatInfo(string currency, bool useFallback)
|
||||||
{
|
{
|
||||||
|
@@ -19,7 +19,7 @@ namespace BTCPayServer.Rating
|
|||||||
public static CurrencyPair Parse(string str)
|
public static CurrencyPair Parse(string str)
|
||||||
{
|
{
|
||||||
if (!TryParse(str, out var result))
|
if (!TryParse(str, out var result))
|
||||||
throw new FormatException("Invalid currency pair");
|
throw new FormatException($"Invalid currency pair ({str})");
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
public static bool TryParse(string str, out CurrencyPair value)
|
public static bool TryParse(string str, out CurrencyPair value)
|
||||||
|
@@ -15,7 +15,7 @@ namespace BTCPayServer.Rating
|
|||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
var rounded = decimal.Round(value, divisibility, MidpointRounding.AwayFromZero);
|
var rounded = decimal.Round(value, divisibility, MidpointRounding.AwayFromZero);
|
||||||
if ((Math.Abs(rounded - value) / value) < 0.001m)
|
if ((Math.Abs(rounded - value) / value) < 0.01m)
|
||||||
{
|
{
|
||||||
value = rounded;
|
value = rounded;
|
||||||
break;
|
break;
|
||||||
|
@@ -1,29 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using BTCPayServer.Rating;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Services.Rates
|
|
||||||
{
|
|
||||||
public class ArgoneumRateProvider : IRateProvider
|
|
||||||
{
|
|
||||||
private readonly HttpClient _httpClient;
|
|
||||||
public ArgoneumRateProvider(HttpClient httpClient)
|
|
||||||
{
|
|
||||||
_httpClient = httpClient ?? new HttpClient();
|
|
||||||
}
|
|
||||||
|
|
||||||
public RateSourceInfo RateSourceInfo => new("argoneum", "Argoneum", "https://rates.argoneum.net/rates");
|
|
||||||
|
|
||||||
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
// Example result: AGM to BTC rate: {"agm":5000000.000000}
|
|
||||||
var response = await _httpClient.GetAsync("https://rates.argoneum.net/rates/btc", cancellationToken);
|
|
||||||
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
|
|
||||||
var value = jobj["agm"].Value<decimal>();
|
|
||||||
return new[] { new PairRate(new CurrencyPair("BTC", "AGM"), new BidAsk(value)) };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because one or more lines are too long
@@ -0,0 +1,36 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Rating;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Services.Rates;
|
||||||
|
|
||||||
|
public class FreeCurrencyRatesRateProvider : IRateProvider
|
||||||
|
{
|
||||||
|
public RateSourceInfo RateSourceInfo => new("free-currency-rates", "Free Currency Rates", "https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api@1/latest/currencies/btc.min.json");
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
public FreeCurrencyRatesRateProvider(HttpClient httpClient)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient ?? new HttpClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var response = await _httpClient.GetAsync(RateSourceInfo.Url, cancellationToken);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
|
||||||
|
var results = (JObject) jobj["btc"] ;
|
||||||
|
//key value is currency code to rate value
|
||||||
|
var list = new List<PairRate>();
|
||||||
|
foreach (var item in results)
|
||||||
|
{
|
||||||
|
string name = item.Key;
|
||||||
|
var value = item.Value.Value<decimal>();
|
||||||
|
list.Add(new PairRate(new CurrencyPair("BTC", name), new BidAsk(value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.ToArray();
|
||||||
|
}
|
||||||
|
}
|
@@ -13,7 +13,7 @@ namespace BTCPayServer.Services.Rates
|
|||||||
{
|
{
|
||||||
public class RipioExchangeProvider : IRateProvider
|
public class RipioExchangeProvider : IRateProvider
|
||||||
{
|
{
|
||||||
public RateSourceInfo RateSourceInfo => new("ripio", "Ripio", "https://api.exchange.ripio.com/api/v1/rate/all/");
|
public RateSourceInfo RateSourceInfo => new("ripio", "Ripio", "https://api.ripiotrade.co/v4/public/tickers");
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
public RipioExchangeProvider(HttpClient httpClient)
|
public RipioExchangeProvider(HttpClient httpClient)
|
||||||
{
|
{
|
||||||
@@ -21,9 +21,9 @@ namespace BTCPayServer.Services.Rates
|
|||||||
}
|
}
|
||||||
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
|
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var response = await _httpClient.GetAsync("https://api.exchange.ripio.com/api/v1/rate/all/", cancellationToken);
|
var response = await _httpClient.GetAsync("https://api.ripiotrade.co/v4/public/tickers", cancellationToken);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
var jarray = (JArray)(await response.Content.ReadAsAsync<JArray>(cancellationToken));
|
var jarray = (JArray)(await response.Content.ReadAsAsync<JObject>(cancellationToken))["data"];
|
||||||
return jarray
|
return jarray
|
||||||
.Children<JObject>()
|
.Children<JObject>()
|
||||||
.Select(jobj => ParsePair(jobj))
|
.Select(jobj => ParsePair(jobj))
|
||||||
|
@@ -1,21 +1,8 @@
|
|||||||
using System;
|
#nullable enable
|
||||||
using System.Collections.Generic;
|
namespace BTCPayServer.Rating;
|
||||||
using System.Linq;
|
public enum RateSource
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Rating
|
|
||||||
{
|
{
|
||||||
public class RateSourceInfo
|
Coingecko,
|
||||||
{
|
Direct
|
||||||
public RateSourceInfo(string id, string displayName, string url)
|
|
||||||
{
|
|
||||||
Id = id;
|
|
||||||
DisplayName = displayName;
|
|
||||||
Url = url;
|
|
||||||
}
|
|
||||||
public string Id { get; set; }
|
|
||||||
public string DisplayName { get; set; }
|
|
||||||
public string Url { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
public record RateSourceInfo(string Id, string DisplayName, string Url, RateSource Source = RateSource.Direct);
|
||||||
|
@@ -85,14 +85,13 @@ namespace BTCPayServer.Services.Rates
|
|||||||
bgFetcher.RefreshRate = TimeSpan.FromMinutes(1.0);
|
bgFetcher.RefreshRate = TimeSpan.FromMinutes(1.0);
|
||||||
bgFetcher.ValidatyTime = TimeSpan.FromMinutes(5.0);
|
bgFetcher.ValidatyTime = TimeSpan.FromMinutes(5.0);
|
||||||
Providers.Add(supportedExchange.Id, bgFetcher);
|
Providers.Add(supportedExchange.Id, bgFetcher);
|
||||||
var rsi = coingecko.RateSourceInfo;
|
AvailableRateProviders.Add(coingecko.RateSourceInfo);
|
||||||
AvailableRateProviders.Add(new(rsi.Id, rsi.DisplayName, rsi.Url, RateSource.Coingecko));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AvailableRateProviders.Sort((a, b) => StringComparer.Ordinal.Compare(a.DisplayName, b.DisplayName));
|
AvailableRateProviders.Sort((a, b) => StringComparer.Ordinal.Compare(a.DisplayName, b.DisplayName));
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<AvailableRateProvider> AvailableRateProviders { get; } = new List<AvailableRateProvider>();
|
public List<RateSourceInfo> AvailableRateProviders { get; } = new List<RateSourceInfo>();
|
||||||
|
|
||||||
public async Task<QueryRateResult> QueryRates(string exchangeName, CancellationToken cancellationToken)
|
public async Task<QueryRateResult> QueryRates(string exchangeName, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
@@ -6,6 +6,7 @@ using System.Threading.Tasks;
|
|||||||
using BTCPayServer.Controllers;
|
using BTCPayServer.Controllers;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.HostedServices;
|
using BTCPayServer.HostedServices;
|
||||||
|
using BTCPayServer.Hosting;
|
||||||
using BTCPayServer.Lightning;
|
using BTCPayServer.Lightning;
|
||||||
using BTCPayServer.Models.AppViewModels;
|
using BTCPayServer.Models.AppViewModels;
|
||||||
using BTCPayServer.Models.StoreViewModels;
|
using BTCPayServer.Models.StoreViewModels;
|
||||||
@@ -245,7 +246,7 @@ namespace BTCPayServer.Tests
|
|||||||
await tester.EnsureChannelsSetup();
|
await tester.EnsureChannelsSetup();
|
||||||
var user = tester.NewAccount();
|
var user = tester.NewAccount();
|
||||||
user.GrantAccess(true);
|
user.GrantAccess(true);
|
||||||
user.RegisterLightningNode("BTC", LightningConnectionType.Charge);
|
user.RegisterLightningNode("BTC");
|
||||||
user.RegisterDerivationScheme("BTC");
|
user.RegisterDerivationScheme("BTC");
|
||||||
user.RegisterDerivationScheme("LTC");
|
user.RegisterDerivationScheme("LTC");
|
||||||
|
|
||||||
@@ -651,6 +652,7 @@ donation:
|
|||||||
price: 1.02
|
price: 1.02
|
||||||
custom: true
|
custom: true
|
||||||
";
|
";
|
||||||
|
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
|
||||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||||
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||||
Assert.Equal("hello", vmpos.Title);
|
Assert.Equal("hello", vmpos.Title);
|
||||||
@@ -661,14 +663,13 @@ donation:
|
|||||||
Assert.Equal(3, vmview.Items.Length);
|
Assert.Equal(3, vmview.Items.Length);
|
||||||
Assert.Equal("good apple", vmview.Items[0].Title);
|
Assert.Equal("good apple", vmview.Items[0].Title);
|
||||||
Assert.Equal("orange", vmview.Items[1].Title);
|
Assert.Equal("orange", vmview.Items[1].Title);
|
||||||
Assert.Equal(10.0m, vmview.Items[1].Price.Value);
|
Assert.Equal(10.0m, vmview.Items[1].Price);
|
||||||
Assert.Equal("$5.00", vmview.Items[0].Price.Formatted);
|
|
||||||
Assert.Equal("{0} Purchase", vmview.ButtonText);
|
Assert.Equal("{0} Purchase", vmview.ButtonText);
|
||||||
Assert.Equal("Nicolas Sexy Hair", vmview.CustomButtonText);
|
Assert.Equal("Nicolas Sexy Hair", vmview.CustomButtonText);
|
||||||
Assert.Equal("Wanna tip?", vmview.CustomTipText);
|
Assert.Equal("Wanna tip?", vmview.CustomTipText);
|
||||||
Assert.Equal("15,18,20", string.Join(',', vmview.CustomTipPercentages));
|
Assert.Equal("15,18,20", string.Join(',', vmview.CustomTipPercentages));
|
||||||
Assert.IsType<RedirectToActionResult>(publicApps
|
Assert.IsType<RedirectToActionResult>(publicApps
|
||||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, null, null, null, null, "orange").Result);
|
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, choiceKey: "orange").Result);
|
||||||
|
|
||||||
//
|
//
|
||||||
var invoices = await user.BitPay.GetInvoicesAsync();
|
var invoices = await user.BitPay.GetInvoicesAsync();
|
||||||
@@ -677,18 +678,18 @@ donation:
|
|||||||
Assert.Equal("CAD", orangeInvoice.Currency);
|
Assert.Equal("CAD", orangeInvoice.Currency);
|
||||||
Assert.Equal("orange", orangeInvoice.ItemDesc);
|
Assert.Equal("orange", orangeInvoice.ItemDesc);
|
||||||
Assert.IsType<RedirectToActionResult>(publicApps
|
Assert.IsType<RedirectToActionResult>(publicApps
|
||||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, null, null, null, null, "apple").Result);
|
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, choiceKey: "apple").Result);
|
||||||
|
|
||||||
invoices = user.BitPay.GetInvoices();
|
invoices = await user.BitPay.GetInvoicesAsync();
|
||||||
var appleInvoice = invoices.SingleOrDefault(invoice => invoice.ItemCode.Equals("apple"));
|
var appleInvoice = invoices.SingleOrDefault(invoice => invoice.ItemCode.Equals("apple"));
|
||||||
Assert.NotNull(appleInvoice);
|
Assert.NotNull(appleInvoice);
|
||||||
Assert.Equal("good apple", appleInvoice.ItemDesc);
|
Assert.Equal("good apple", appleInvoice.ItemDesc);
|
||||||
|
|
||||||
// testing custom amount
|
// testing custom amount
|
||||||
var action = Assert.IsType<RedirectToActionResult>(publicApps
|
var action = Assert.IsType<RedirectToActionResult>(publicApps
|
||||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 6.6m, null, null, null, null, "donation").Result);
|
.ViewPointOfSale(app.Id, PosViewType.Cart, 6.6m, choiceKey: "donation").Result);
|
||||||
Assert.Equal(nameof(UIInvoiceController.Checkout), action.ActionName);
|
Assert.Equal(nameof(UIInvoiceController.Checkout), action.ActionName);
|
||||||
invoices = user.BitPay.GetInvoices();
|
invoices = await user.BitPay.GetInvoicesAsync();
|
||||||
var donationInvoice = invoices.Single(i => i.Price == 6.6m);
|
var donationInvoice = invoices.Single(i => i.Price == 6.6m);
|
||||||
Assert.NotNull(donationInvoice);
|
Assert.NotNull(donationInvoice);
|
||||||
Assert.Equal("CAD", donationInvoice.Currency);
|
Assert.Equal("CAD", donationInvoice.Currency);
|
||||||
@@ -723,6 +724,7 @@ donation:
|
|||||||
price: 1.02
|
price: 1.02
|
||||||
custom: true
|
custom: true
|
||||||
";
|
";
|
||||||
|
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
|
||||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||||
publicApps = user.GetController<UIPointOfSaleController>();
|
publicApps = user.GetController<UIPointOfSaleController>();
|
||||||
vmview = await publicApps.ViewPointOfSale(app.Id, PosViewType.Cart).AssertViewModelAsync<ViewPointOfSaleViewModel>();
|
vmview = await publicApps.ViewPointOfSale(app.Id, PosViewType.Cart).AssertViewModelAsync<ViewPointOfSaleViewModel>();
|
||||||
@@ -750,26 +752,28 @@ inventoryitem:
|
|||||||
inventory: 1
|
inventory: 1
|
||||||
noninventoryitem:
|
noninventoryitem:
|
||||||
price: 10.0";
|
price: 10.0";
|
||||||
|
|
||||||
|
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
|
||||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||||
|
|
||||||
//inventoryitem has 1 item available
|
//inventoryitem has 1 item available
|
||||||
await tester.WaitForEvent<AppInventoryUpdaterHostedService.UpdateAppInventory>(() =>
|
await tester.WaitForEvent<AppInventoryUpdaterHostedService.UpdateAppInventory>(() =>
|
||||||
{
|
{
|
||||||
Assert.IsType<RedirectToActionResult>(publicApps
|
Assert.IsType<RedirectToActionResult>(publicApps
|
||||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, null, null, null, null, "inventoryitem").Result);
|
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "inventoryitem").Result);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
});
|
});
|
||||||
|
|
||||||
//we already bought all available stock so this should fail
|
//we already bought all available stock so this should fail
|
||||||
await Task.Delay(100);
|
await Task.Delay(100);
|
||||||
Assert.IsType<RedirectToActionResult>(publicApps
|
Assert.IsType<RedirectToActionResult>(publicApps
|
||||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, null, null, null, null, "inventoryitem").Result);
|
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "inventoryitem").Result);
|
||||||
|
|
||||||
//inventoryitem has unlimited items available
|
//inventoryitem has unlimited items available
|
||||||
Assert.IsType<RedirectToActionResult>(publicApps
|
Assert.IsType<RedirectToActionResult>(publicApps
|
||||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, null, null, null, null, "noninventoryitem").Result);
|
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "noninventoryitem").Result);
|
||||||
Assert.IsType<RedirectToActionResult>(publicApps
|
Assert.IsType<RedirectToActionResult>(publicApps
|
||||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, null, null, null, null, "noninventoryitem").Result);
|
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "noninventoryitem").Result);
|
||||||
|
|
||||||
//verify invoices where created
|
//verify invoices where created
|
||||||
invoices = user.BitPay.GetInvoices();
|
invoices = user.BitPay.GetInvoices();
|
||||||
@@ -780,15 +784,13 @@ noninventoryitem:
|
|||||||
|
|
||||||
//let's mark the inventoryitem invoice as invalid, this should return the item to back in stock
|
//let's mark the inventoryitem invoice as invalid, this should return the item to back in stock
|
||||||
var controller = tester.PayTester.GetController<UIInvoiceController>(user.UserId, user.StoreId);
|
var controller = tester.PayTester.GetController<UIInvoiceController>(user.UserId, user.StoreId);
|
||||||
var appService = tester.PayTester.GetService<AppService>();
|
|
||||||
var eventAggregator = tester.PayTester.GetService<EventAggregator>();
|
|
||||||
Assert.IsType<JsonResult>(await controller.ChangeInvoiceState(inventoryItemInvoice.Id, "invalid"));
|
Assert.IsType<JsonResult>(await controller.ChangeInvoiceState(inventoryItemInvoice.Id, "invalid"));
|
||||||
//check that item is back in stock
|
//check that item is back in stock
|
||||||
await TestUtils.EventuallyAsync(async () =>
|
await TestUtils.EventuallyAsync(async () =>
|
||||||
{
|
{
|
||||||
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||||
Assert.Equal(1,
|
Assert.Equal(1,
|
||||||
appService.Parse(vmpos.Template, "BTC").Single(item => item.Id == "inventoryitem").Inventory);
|
AppService.Parse(vmpos.Template).Single(item => item.Id == "inventoryitem").Inventory);
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
//test payment methods option
|
//test payment methods option
|
||||||
@@ -803,11 +805,13 @@ btconly:
|
|||||||
- BTC
|
- BTC
|
||||||
normal:
|
normal:
|
||||||
price: 1.0";
|
price: 1.0";
|
||||||
|
|
||||||
|
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
|
||||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||||
Assert.IsType<RedirectToActionResult>(publicApps
|
Assert.IsType<RedirectToActionResult>(publicApps
|
||||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, null, null, null, null, "btconly").Result);
|
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "btconly").Result);
|
||||||
Assert.IsType<RedirectToActionResult>(publicApps
|
Assert.IsType<RedirectToActionResult>(publicApps
|
||||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, null, null, null, null, "normal").Result);
|
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "normal").Result);
|
||||||
invoices = user.BitPay.GetInvoices();
|
invoices = user.BitPay.GetInvoices();
|
||||||
var normalInvoice = invoices.Single(invoice => invoice.ItemCode == "normal");
|
var normalInvoice = invoices.Single(invoice => invoice.ItemCode == "normal");
|
||||||
var btcOnlyInvoice = invoices.Single(invoice => invoice.ItemCode == "btconly");
|
var btcOnlyInvoice = invoices.Single(invoice => invoice.ItemCode == "btconly");
|
||||||
@@ -847,20 +851,21 @@ g:
|
|||||||
custom: topup
|
custom: topup
|
||||||
";
|
";
|
||||||
|
|
||||||
|
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
|
||||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||||
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||||
Assert.DoesNotContain("custom", vmpos.Template);
|
Assert.DoesNotContain("custom", vmpos.Template);
|
||||||
var items = appService.Parse(vmpos.Template, vmpos.Currency);
|
var items = AppService.Parse(vmpos.Template);
|
||||||
Assert.Contains(items, item => item.Id == "a" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed);
|
Assert.Contains(items, item => item.Id == "a" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed);
|
||||||
Assert.Contains(items, item => item.Id == "b" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed);
|
Assert.Contains(items, item => item.Id == "b" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed);
|
||||||
Assert.Contains(items, item => item.Id == "c" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Minimum);
|
Assert.Contains(items, item => item.Id == "c" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Minimum);
|
||||||
Assert.Contains(items, item => item.Id == "d" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed);
|
Assert.Contains(items, item => item.Id == "d" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed);
|
||||||
Assert.Contains(items, item => item.Id == "e" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Minimum);
|
Assert.Contains(items, item => item.Id == "e" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Minimum);
|
||||||
Assert.Contains(items, item => item.Id == "f" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup);
|
Assert.Contains(items, item => item.Id == "f" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup);
|
||||||
Assert.Contains(items, item => item.Id == "g" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup);
|
Assert.Contains(items, item => item.Id == "g" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup);
|
||||||
|
|
||||||
Assert.IsType<RedirectToActionResult>(publicApps
|
Assert.IsType<RedirectToActionResult>(publicApps
|
||||||
.ViewPointOfSale(app.Id, PosViewType.Static, null, null, null, null, null, "g").Result);
|
.ViewPointOfSale(app.Id, PosViewType.Static, choiceKey: "g").Result);
|
||||||
invoices = user.BitPay.GetInvoices();
|
invoices = user.BitPay.GetInvoices();
|
||||||
var topupInvoice = invoices.Single(invoice => invoice.ItemCode == "g");
|
var topupInvoice = invoices.Single(invoice => invoice.ItemCode == "g");
|
||||||
Assert.Equal(0, topupInvoice.Price);
|
Assert.Equal(0, topupInvoice.Price);
|
||||||
|
@@ -52,11 +52,12 @@ namespace BTCPayServer.Tests
|
|||||||
{
|
{
|
||||||
tester.ActivateLBTC();
|
tester.ActivateLBTC();
|
||||||
await tester.StartAsync();
|
await tester.StartAsync();
|
||||||
|
|
||||||
|
//https://github.com/ElementsProject/elements/issues/956
|
||||||
|
await tester.LBTCExplorerNode.SendCommandAsync("rescanblockchain");
|
||||||
var user = tester.NewAccount();
|
var user = tester.NewAccount();
|
||||||
user.GrantAccess();
|
await user.GrantAccessAsync();
|
||||||
user.RegisterDerivationScheme("LBTC");
|
|
||||||
user.RegisterDerivationScheme("USDT");
|
|
||||||
user.RegisterDerivationScheme("ETB");
|
|
||||||
await tester.LBTCExplorerNode.GenerateAsync(4);
|
await tester.LBTCExplorerNode.GenerateAsync(4);
|
||||||
//no tether on our regtest, lets create it and set it
|
//no tether on our regtest, lets create it and set it
|
||||||
var tether = tester.NetworkProvider.GetNetwork<ElementsBTCPayNetwork>("USDT");
|
var tether = tester.NetworkProvider.GetNetwork<ElementsBTCPayNetwork>("USDT");
|
||||||
@@ -75,6 +76,10 @@ namespace BTCPayServer.Tests
|
|||||||
.AssetId = etb.AssetId;
|
.AssetId = etb.AssetId;
|
||||||
|
|
||||||
|
|
||||||
|
user.RegisterDerivationScheme("LBTC");
|
||||||
|
user.RegisterDerivationScheme("USDT");
|
||||||
|
user.RegisterDerivationScheme("ETB");
|
||||||
|
|
||||||
//test: register 2 assets on the same elements network and make sure paying an invoice on one does not affect the other in any way
|
//test: register 2 assets on the same elements network and make sure paying an invoice on one does not affect the other in any way
|
||||||
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(0.1m, "BTC"));
|
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(0.1m, "BTC"));
|
||||||
Assert.Equal(3, invoice.SupportedTransactionCurrencies.Count);
|
Assert.Equal(3, invoice.SupportedTransactionCurrencies.Count);
|
||||||
@@ -82,7 +87,7 @@ namespace BTCPayServer.Tests
|
|||||||
//1 lbtc = 1 btc
|
//1 lbtc = 1 btc
|
||||||
Assert.Equal(1, ci.Rate);
|
Assert.Equal(1, ci.Rate);
|
||||||
var star = await tester.LBTCExplorerNode.SendCommandAsync("sendtoaddress", ci.Address, ci.Due, "", "", false, true,
|
var star = await tester.LBTCExplorerNode.SendCommandAsync("sendtoaddress", ci.Address, ci.Due, "", "", false, true,
|
||||||
1, "UNSET", lbtc.AssetId);
|
1, "UNSET",false, lbtc.AssetId.ToString());
|
||||||
|
|
||||||
TestUtils.Eventually(() =>
|
TestUtils.Eventually(() =>
|
||||||
{
|
{
|
||||||
@@ -95,8 +100,7 @@ namespace BTCPayServer.Tests
|
|||||||
|
|
||||||
ci = invoice.CryptoInfo.Single(info => info.CryptoCode.Equals("USDT"));
|
ci = invoice.CryptoInfo.Single(info => info.CryptoCode.Equals("USDT"));
|
||||||
Assert.Equal(3, invoice.SupportedTransactionCurrencies.Count);
|
Assert.Equal(3, invoice.SupportedTransactionCurrencies.Count);
|
||||||
star = await tester.LBTCExplorerNode.SendCommandAsync("sendtoaddress", ci.Address, ci.Due, "", "", false, true,
|
star = tester.LBTCExplorerNode.SendCommand("sendtoaddress", ci.Address, decimal.Parse(ci.Due), "x", "z", false, true, 1, "unset", false, tether.AssetId.ToString());
|
||||||
1, "UNSET", tether.AssetId);
|
|
||||||
|
|
||||||
TestUtils.Eventually(() =>
|
TestUtils.Eventually(() =>
|
||||||
{
|
{
|
||||||
|
@@ -23,7 +23,7 @@
|
|||||||
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14" />
|
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14" />
|
||||||
<PackageReference Include="Selenium.Support" Version="4.1.1" />
|
<PackageReference Include="Selenium.Support" Version="4.1.1" />
|
||||||
<PackageReference Include="Selenium.WebDriver" Version="4.1.1" />
|
<PackageReference Include="Selenium.WebDriver" Version="4.1.1" />
|
||||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="112.0.5615.4900" />
|
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="116.0.5845.9600" />
|
||||||
<PackageReference Include="xunit" Version="2.4.2" />
|
<PackageReference Include="xunit" Version="2.4.2" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
@@ -1,13 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Client.Models;
|
|
||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
using BTCPayServer.Tests.Logging;
|
|
||||||
using BTCPayServer.Views.Stores;
|
using BTCPayServer.Views.Stores;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using OpenQA.Selenium;
|
using OpenQA.Selenium;
|
||||||
using OpenQA.Selenium.Support.Extensions;
|
|
||||||
using OpenQA.Selenium.Support.UI;
|
using OpenQA.Selenium.Support.UI;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
@@ -40,8 +36,10 @@ namespace BTCPayServer.Tests
|
|||||||
|
|
||||||
// Configure store url
|
// Configure store url
|
||||||
var storeUrl = "https://satoshisteaks.com/";
|
var storeUrl = "https://satoshisteaks.com/";
|
||||||
|
var supportUrl = "https://support.satoshisteaks.com/{InvoiceId}/";
|
||||||
s.GoToStore();
|
s.GoToStore();
|
||||||
s.Driver.FindElement(By.Id("StoreWebsite")).SendKeys(storeUrl);
|
s.Driver.FindElement(By.Id("StoreWebsite")).SendKeys(storeUrl);
|
||||||
|
s.Driver.FindElement(By.Id("StoreSupportUrl")).SendKeys(supportUrl);
|
||||||
s.Driver.FindElement(By.Id("Save")).Click();
|
s.Driver.FindElement(By.Id("Save")).Click();
|
||||||
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
|
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
|
||||||
|
|
||||||
@@ -64,9 +62,9 @@ namespace BTCPayServer.Tests
|
|||||||
var qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
|
var qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
|
||||||
var address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
|
var address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
|
||||||
var payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
|
var payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
|
||||||
var copyAddress = s.Driver.FindElement(By.Id("Address_BTC")).GetAttribute("value");
|
var copyAddress = s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text;
|
||||||
Assert.Equal($"bitcoin:{address}", payUrl);
|
Assert.Equal($"bitcoin:{address}", payUrl);
|
||||||
Assert.StartsWith("bcrt", s.Driver.FindElement(By.Id("Address_BTC")).GetAttribute("value"));
|
Assert.StartsWith("bcrt", s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text);
|
||||||
Assert.DoesNotContain("lightning=", payUrl);
|
Assert.DoesNotContain("lightning=", payUrl);
|
||||||
Assert.Equal(address, copyAddress);
|
Assert.Equal(address, copyAddress);
|
||||||
Assert.Equal($"bitcoin:{address.ToUpperInvariant()}", qrValue);
|
Assert.Equal($"bitcoin:{address.ToUpperInvariant()}", qrValue);
|
||||||
@@ -86,7 +84,7 @@ namespace BTCPayServer.Tests
|
|||||||
{
|
{
|
||||||
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
|
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
|
||||||
Assert.StartsWith("lightning:lnurl", payUrl);
|
Assert.StartsWith("lightning:lnurl", payUrl);
|
||||||
Assert.StartsWith("lnurl", s.Driver.WaitForElement(By.Id("Lightning_BTC")).GetAttribute("value"));
|
Assert.StartsWith("lnurl", s.Driver.WaitForElement(By.CssSelector("#Lightning_BTC .truncate-center-start")).Text);
|
||||||
s.Driver.ElementDoesNotExist(By.Id("Address_BTC"));
|
s.Driver.ElementDoesNotExist(By.Id("Address_BTC"));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -101,7 +99,7 @@ namespace BTCPayServer.Tests
|
|||||||
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
|
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
|
||||||
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
|
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
|
||||||
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
|
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
|
||||||
copyAddress = s.Driver.FindElement(By.Id("Lightning_BTC_LightningLike")).GetAttribute("value");
|
copyAddress = s.Driver.FindElement(By.CssSelector("#Lightning_BTC_LightningLike .truncate-center-start")).Text;
|
||||||
Assert.Equal($"lightning:{address}", payUrl);
|
Assert.Equal($"lightning:{address}", payUrl);
|
||||||
Assert.Equal(address, copyAddress);
|
Assert.Equal(address, copyAddress);
|
||||||
Assert.Equal($"lightning:{address.ToUpperInvariant()}", qrValue);
|
Assert.Equal($"lightning:{address.ToUpperInvariant()}", qrValue);
|
||||||
@@ -140,8 +138,47 @@ namespace BTCPayServer.Tests
|
|||||||
var expiredSection = s.Driver.FindElement(By.Id("unpaid"));
|
var expiredSection = s.Driver.FindElement(By.Id("unpaid"));
|
||||||
Assert.True(expiredSection.Displayed);
|
Assert.True(expiredSection.Displayed);
|
||||||
Assert.Contains("Invoice Expired", expiredSection.Text);
|
Assert.Contains("Invoice Expired", expiredSection.Text);
|
||||||
|
Assert.Contains("resubmit a payment", expiredSection.Text);
|
||||||
|
Assert.DoesNotContain("This invoice expired with partial payment", expiredSection.Text);
|
||||||
|
|
||||||
});
|
});
|
||||||
Assert.True(s.Driver.ElementDoesNotExist(By.Id("receipt-btn")));
|
Assert.True(s.Driver.ElementDoesNotExist(By.Id("ContactLink")));
|
||||||
|
Assert.True(s.Driver.ElementDoesNotExist(By.Id("ReceiptLink")));
|
||||||
|
Assert.Equal(storeUrl, s.Driver.FindElement(By.Id("StoreLink")).GetAttribute("href"));
|
||||||
|
|
||||||
|
// Expire paid partial
|
||||||
|
s.GoToHome();
|
||||||
|
invoiceId = s.CreateInvoice(2100, "EUR");
|
||||||
|
s.GoToInvoiceCheckout(invoiceId);
|
||||||
|
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
|
||||||
|
|
||||||
|
await Task.Delay(200);
|
||||||
|
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
|
||||||
|
var amountFraction = "0.00001";
|
||||||
|
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(address, Network.RegTest),
|
||||||
|
Money.Parse(amountFraction));
|
||||||
|
await s.Server.ExplorerNode.GenerateAsync(1);
|
||||||
|
|
||||||
|
expirySeconds = s.Driver.FindElement(By.Id("ExpirySeconds"));
|
||||||
|
expirySeconds.Clear();
|
||||||
|
expirySeconds.SendKeys("3");
|
||||||
|
s.Driver.FindElement(By.Id("Expire")).Click();
|
||||||
|
|
||||||
|
paymentInfo = s.Driver.WaitForElement(By.Id("PaymentInfo"));
|
||||||
|
Assert.Contains("The invoice hasn't been paid in full.", paymentInfo.Text);
|
||||||
|
Assert.Contains("Please send", paymentInfo.Text);
|
||||||
|
TestUtils.Eventually(() =>
|
||||||
|
{
|
||||||
|
var expiredSection = s.Driver.FindElement(By.Id("unpaid"));
|
||||||
|
Assert.True(expiredSection.Displayed);
|
||||||
|
Assert.Contains("Invoice Expired", expiredSection.Text);
|
||||||
|
Assert.Contains("This invoice expired with partial payment", expiredSection.Text);
|
||||||
|
Assert.DoesNotContain("resubmit a payment", expiredSection.Text);
|
||||||
|
});
|
||||||
|
var contactLink = s.Driver.FindElement(By.Id("ContactLink"));
|
||||||
|
Assert.Equal("Contact us", contactLink.Text);
|
||||||
|
Assert.Matches(supportUrl.Replace("{InvoiceId}", invoiceId), contactLink.GetAttribute("href"));
|
||||||
|
Assert.True(s.Driver.ElementDoesNotExist(By.Id("ReceiptLink")));
|
||||||
Assert.Equal(storeUrl, s.Driver.FindElement(By.Id("StoreLink")).GetAttribute("href"));
|
Assert.Equal(storeUrl, s.Driver.FindElement(By.Id("StoreLink")).GetAttribute("href"));
|
||||||
|
|
||||||
// Test payment
|
// Test payment
|
||||||
@@ -166,7 +203,7 @@ namespace BTCPayServer.Tests
|
|||||||
// Pay partial amount
|
// Pay partial amount
|
||||||
await Task.Delay(200);
|
await Task.Delay(200);
|
||||||
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
|
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
|
||||||
var amountFraction = "0.00001";
|
amountFraction = "0.00001";
|
||||||
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(address, Network.RegTest),
|
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(address, Network.RegTest),
|
||||||
Money.Parse(amountFraction));
|
Money.Parse(amountFraction));
|
||||||
await s.Server.ExplorerNode.GenerateAsync(1);
|
await s.Server.ExplorerNode.GenerateAsync(1);
|
||||||
@@ -210,7 +247,8 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.Contains("Invoice Paid", settledSection.Text);
|
Assert.Contains("Invoice Paid", settledSection.Text);
|
||||||
});
|
});
|
||||||
s.Driver.FindElement(By.Id("confetti"));
|
s.Driver.FindElement(By.Id("confetti"));
|
||||||
s.Driver.FindElement(By.Id("receipt-btn"));
|
s.Driver.FindElement(By.Id("ReceiptLink"));
|
||||||
|
Assert.True(s.Driver.ElementDoesNotExist(By.Id("ContactLink")));
|
||||||
Assert.Equal(storeUrl, s.Driver.FindElement(By.Id("StoreLink")).GetAttribute("href"));
|
Assert.Equal(storeUrl, s.Driver.FindElement(By.Id("StoreLink")).GetAttribute("href"));
|
||||||
|
|
||||||
// BIP21
|
// BIP21
|
||||||
@@ -229,8 +267,8 @@ namespace BTCPayServer.Tests
|
|||||||
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
|
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
|
||||||
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
|
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
|
||||||
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
|
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
|
||||||
var copyAddressOnchain = s.Driver.FindElement(By.Id("Address_BTC")).GetAttribute("value");
|
var copyAddressOnchain = s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text;
|
||||||
var copyAddressLightning = s.Driver.FindElement(By.Id("Lightning_BTC")).GetAttribute("value");
|
var copyAddressLightning = s.Driver.FindElement(By.CssSelector("#Lightning_BTC .truncate-center-start")).Text;
|
||||||
Assert.StartsWith($"bitcoin:{address}?amount=", payUrl);
|
Assert.StartsWith($"bitcoin:{address}?amount=", payUrl);
|
||||||
Assert.Contains("?amount=", payUrl);
|
Assert.Contains("?amount=", payUrl);
|
||||||
Assert.Contains("&lightning=", payUrl);
|
Assert.Contains("&lightning=", payUrl);
|
||||||
@@ -297,8 +335,8 @@ namespace BTCPayServer.Tests
|
|||||||
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
|
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
|
||||||
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
|
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
|
||||||
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
|
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
|
||||||
copyAddressOnchain = s.Driver.FindElement(By.Id("Address_BTC")).GetAttribute("value");
|
copyAddressOnchain = s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text;
|
||||||
copyAddressLightning = s.Driver.FindElement(By.Id("Lightning_BTC")).GetAttribute("value");
|
copyAddressLightning = s.Driver.FindElement(By.CssSelector("#Lightning_BTC .truncate-center-start")).Text;
|
||||||
Assert.StartsWith($"bitcoin:{address}", payUrl);
|
Assert.StartsWith($"bitcoin:{address}", payUrl);
|
||||||
Assert.Contains("?lightning=lnurl", payUrl);
|
Assert.Contains("?lightning=lnurl", payUrl);
|
||||||
Assert.DoesNotContain("amount=", payUrl);
|
Assert.DoesNotContain("amount=", payUrl);
|
||||||
@@ -358,6 +396,7 @@ namespace BTCPayServer.Tests
|
|||||||
s.GoToHome();
|
s.GoToHome();
|
||||||
s.GoToLightningSettings();
|
s.GoToLightningSettings();
|
||||||
s.Driver.SetCheckbox(By.Id("LNURLEnabled"), false);
|
s.Driver.SetCheckbox(By.Id("LNURLEnabled"), false);
|
||||||
|
s.Driver.ScrollTo(By.Id("save"));
|
||||||
s.Driver.FindElement(By.Id("save")).Click();
|
s.Driver.FindElement(By.Id("save")).Click();
|
||||||
Assert.Contains("BTC Lightning settings successfully updated", s.FindAlertMessage().Text);
|
Assert.Contains("BTC Lightning settings successfully updated", s.FindAlertMessage().Text);
|
||||||
|
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Client;
|
||||||
using BTCPayServer.Client.Models;
|
using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.Controllers;
|
using BTCPayServer.Controllers;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
@@ -53,13 +54,33 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps2.ListApps(user2.StoreId).Result).Model);
|
Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps2.ListApps(user2.StoreId).Result).Model);
|
||||||
Assert.Single(appList.Apps);
|
Assert.Single(appList.Apps);
|
||||||
Assert.Empty(appList2.Apps);
|
Assert.Empty(appList2.Apps);
|
||||||
Assert.Equal("test", appList.Apps[0].AppName);
|
Assert.Equal("test", app.AppName);
|
||||||
Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id);
|
Assert.Equal(apps.CreatedAppId, app.Id);
|
||||||
Assert.True(appList.Apps[0].IsOwner);
|
Assert.True(app.Role.ToPermissionSet(app.StoreId).Contains(Policies.CanModifyStoreSettings, app.StoreId));
|
||||||
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
|
Assert.Equal(user.StoreId, app.StoreId);
|
||||||
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id));
|
// Archive
|
||||||
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id));
|
redirect = Assert.IsType<RedirectResult>(apps.ToggleArchive(app.Id).Result);
|
||||||
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result);
|
Assert.EndsWith("/settings/crowdfund", redirect.Url);
|
||||||
|
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||||
|
Assert.Empty(appList.Apps);
|
||||||
|
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId, archived: true).Result).Model);
|
||||||
|
app = appList.Apps[0];
|
||||||
|
Assert.True(app.Archived);
|
||||||
|
Assert.IsType<NotFoundResult>(await crowdfund.ViewCrowdfund(app.Id));
|
||||||
|
// Unarchive
|
||||||
|
redirect = Assert.IsType<RedirectResult>(apps.ToggleArchive(app.Id).Result);
|
||||||
|
Assert.EndsWith("/settings/crowdfund", redirect.Url);
|
||||||
|
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||||
|
app = appList.Apps[0];
|
||||||
|
Assert.False(app.Archived);
|
||||||
|
var crowdfundViewModel = await crowdfund.UpdateCrowdfund(app.Id).AssertViewModelAsync<UpdateCrowdfundViewModel>();
|
||||||
|
crowdfundViewModel.Enabled = true;
|
||||||
|
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||||
|
Assert.IsType<ViewResult>(await crowdfund.ViewCrowdfund(app.Id));
|
||||||
|
// Delete
|
||||||
|
Assert.IsType<NotFoundResult>(apps2.DeleteApp(app.Id));
|
||||||
|
Assert.IsType<ViewResult>(apps.DeleteApp(app.Id));
|
||||||
|
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(app.Id).Result);
|
||||||
Assert.Equal(nameof(UIStoresController.Dashboard), redirectToAction.ActionName);
|
Assert.Equal(nameof(UIStoresController.Dashboard), redirectToAction.ActionName);
|
||||||
appList = await apps.ListApps(user.StoreId).AssertViewModelAsync<ListAppsViewModel>();
|
appList = await apps.ListApps(user.StoreId).AssertViewModelAsync<ListAppsViewModel>();
|
||||||
Assert.Empty(appList.Apps);
|
Assert.Empty(appList.Apps);
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -122,6 +123,13 @@ retry:
|
|||||||
driver.ExecuteJavaScript($"document.getElementById('{element}').{funcName}()");
|
driver.ExecuteJavaScript($"document.getElementById('{element}').{funcName}()");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void WaitWalletTransactionsLoaded(this IWebDriver driver)
|
||||||
|
{
|
||||||
|
var wait = new WebDriverWait(driver, SeleniumTester.ImplicitWait);
|
||||||
|
wait.UntilJsIsReady();
|
||||||
|
wait.Until(d => d.WaitForElement(By.CssSelector("#WalletTransactions[data-loaded='true']")));
|
||||||
|
}
|
||||||
|
|
||||||
public static IWebElement WaitForElement(this IWebDriver driver, By selector)
|
public static IWebElement WaitForElement(this IWebDriver driver, By selector)
|
||||||
{
|
{
|
||||||
var wait = new WebDriverWait(driver, SeleniumTester.ImplicitWait);
|
var wait = new WebDriverWait(driver, SeleniumTester.ImplicitWait);
|
||||||
@@ -189,6 +197,7 @@ retry:
|
|||||||
driver.FindElement(selector).Click();
|
driver.FindElement(selector).Click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[DebuggerHidden]
|
||||||
public static bool ElementDoesNotExist(this IWebDriver driver, By selector)
|
public static bool ElementDoesNotExist(this IWebDriver driver, By selector)
|
||||||
{
|
{
|
||||||
Assert.Throws<NoSuchElementException>(() =>
|
Assert.Throws<NoSuchElementException>(() =>
|
||||||
|
@@ -23,9 +23,11 @@ using BTCPayServer.Payments.Bitcoin;
|
|||||||
using BTCPayServer.Payments.Lightning;
|
using BTCPayServer.Payments.Lightning;
|
||||||
using BTCPayServer.Rating;
|
using BTCPayServer.Rating;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
|
using BTCPayServer.Services.Apps;
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
using BTCPayServer.Services.Labels;
|
using BTCPayServer.Services.Labels;
|
||||||
using BTCPayServer.Services.Rates;
|
using BTCPayServer.Services.Rates;
|
||||||
|
using BTCPayServer.Services.Stores;
|
||||||
using BTCPayServer.Validation;
|
using BTCPayServer.Validation;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
@@ -345,165 +347,272 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.True(Torrc.TryParse(input, out torrc));
|
Assert.True(Torrc.TryParse(input, out torrc));
|
||||||
Assert.Equal(expected, torrc.ToString());
|
Assert.Equal(expected, torrc.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanParseCartItems()
|
||||||
|
{
|
||||||
|
Assert.True(AppService.TryParsePosCartItems(new JObject()
|
||||||
|
{
|
||||||
|
{"cart", new JArray()
|
||||||
|
{
|
||||||
|
new JObject()
|
||||||
|
{
|
||||||
|
{ "id", "ddd"},
|
||||||
|
{"price", 4},
|
||||||
|
{"count", 1}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
}, out var items));
|
||||||
|
Assert.Equal("ddd", items[0].Id);
|
||||||
|
Assert.Equal(1, items[0].Count);
|
||||||
|
Assert.Equal(4, items[0].Price);
|
||||||
|
|
||||||
|
// Using legacy parsing
|
||||||
|
Assert.True(AppService.TryParsePosCartItems(new JObject()
|
||||||
|
{
|
||||||
|
{"cart", new JArray()
|
||||||
|
{
|
||||||
|
new JObject()
|
||||||
|
{
|
||||||
|
{ "id", "ddd"},
|
||||||
|
{"price", new JObject()
|
||||||
|
{
|
||||||
|
{ "value", 8.49m }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{"count", 1}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
}, out items));
|
||||||
|
Assert.Equal("ddd", items[0].Id);
|
||||||
|
Assert.Equal(1, items[0].Count);
|
||||||
|
Assert.Equal(8.49m, items[0].Price);
|
||||||
|
|
||||||
|
Assert.False(AppService.TryParsePosCartItems(new JObject()
|
||||||
|
{
|
||||||
|
{"cart", new JArray()
|
||||||
|
{
|
||||||
|
new JObject()
|
||||||
|
{
|
||||||
|
{ "id", "ddd"},
|
||||||
|
{"price", new JObject()
|
||||||
|
{
|
||||||
|
{ "value", "nocrahs" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{"count", 1}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
}, out items));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanCalculateDust()
|
||||||
|
{
|
||||||
|
var entity = new InvoiceEntity() { Currency = "USD" };
|
||||||
|
entity.Networks = new BTCPayNetworkProvider(ChainName.Regtest);
|
||||||
|
#pragma warning disable CS0618
|
||||||
|
entity.Payments = new System.Collections.Generic.List<PaymentEntity>();
|
||||||
|
entity.SetPaymentMethod(new PaymentMethod()
|
||||||
|
{
|
||||||
|
Currency = "BTC",
|
||||||
|
Rate = 34_000m
|
||||||
|
});
|
||||||
|
entity.Price = 4000;
|
||||||
|
entity.UpdateTotals();
|
||||||
|
var accounting = entity.GetPaymentMethods().First().Calculate();
|
||||||
|
// Exact price should be 0.117647059..., but the payment method round up to one sat
|
||||||
|
Assert.Equal(0.11764706m, accounting.Due);
|
||||||
|
entity.Payments.Add(new PaymentEntity()
|
||||||
|
{
|
||||||
|
Currency = "BTC",
|
||||||
|
Output = new TxOut(Money.Coins(0.11764706m), new Key()),
|
||||||
|
Accounted = true
|
||||||
|
});
|
||||||
|
entity.UpdateTotals();
|
||||||
|
Assert.Equal(0.0m, entity.NetDue);
|
||||||
|
// The dust's value is below 1 sat
|
||||||
|
Assert.True(entity.Dust > 0.0m);
|
||||||
|
Assert.True(Money.Satoshis(1.0m).ToDecimal(MoneyUnit.BTC) * entity.Rates["BTC"] > entity.Dust);
|
||||||
|
Assert.True(!entity.IsOverPaid);
|
||||||
|
Assert.True(!entity.IsUnderPaid);
|
||||||
|
|
||||||
|
// Now, imagine there is litecoin. It might seem from its
|
||||||
|
// perspecitve that there has been a slight over payment.
|
||||||
|
// However, Calculate() should just cap it to 0.0m
|
||||||
|
entity.SetPaymentMethod(new PaymentMethod()
|
||||||
|
{
|
||||||
|
Currency = "LTC",
|
||||||
|
Rate = 3400m
|
||||||
|
});
|
||||||
|
entity.UpdateTotals();
|
||||||
|
var method = entity.GetPaymentMethods().First(p => p.Currency == "LTC");
|
||||||
|
accounting = method.Calculate();
|
||||||
|
Assert.Equal(0.0m, accounting.DueUncapped);
|
||||||
|
|
||||||
|
#pragma warning restore CS0618
|
||||||
|
}
|
||||||
#if ALTCOINS
|
#if ALTCOINS
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CanCalculateCryptoDue()
|
public void CanCalculateCryptoDue()
|
||||||
{
|
{
|
||||||
var networkProvider = new BTCPayNetworkProvider(ChainName.Regtest);
|
var networkProvider = new BTCPayNetworkProvider(ChainName.Regtest);
|
||||||
var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[]
|
var entity = new InvoiceEntity() { Currency = "USD" };
|
||||||
{
|
|
||||||
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null, null),
|
|
||||||
new LightningLikePaymentHandler(null, null, networkProvider, null, null, null),
|
|
||||||
});
|
|
||||||
var entity = new InvoiceEntity();
|
|
||||||
entity.Networks = networkProvider;
|
entity.Networks = networkProvider;
|
||||||
#pragma warning disable CS0618
|
#pragma warning disable CS0618
|
||||||
entity.Payments = new System.Collections.Generic.List<PaymentEntity>();
|
entity.Payments = new System.Collections.Generic.List<PaymentEntity>();
|
||||||
entity.SetPaymentMethod(new PaymentMethod()
|
entity.SetPaymentMethod(new PaymentMethod()
|
||||||
{
|
{
|
||||||
CryptoCode = "BTC",
|
Currency = "BTC",
|
||||||
Rate = 5000,
|
Rate = 5000,
|
||||||
NextNetworkFee = Money.Coins(0.1m)
|
NextNetworkFee = Money.Coins(0.1m)
|
||||||
});
|
});
|
||||||
entity.Price = 5000;
|
entity.Price = 5000;
|
||||||
|
entity.UpdateTotals();
|
||||||
|
|
||||||
var paymentMethod = entity.GetPaymentMethods().TryGet("BTC", PaymentTypes.BTCLike);
|
var paymentMethod = entity.GetPaymentMethods().TryGet("BTC", PaymentTypes.BTCLike);
|
||||||
var accounting = paymentMethod.Calculate();
|
var accounting = paymentMethod.Calculate();
|
||||||
Assert.Equal(Money.Coins(1.1m), accounting.Due);
|
Assert.Equal(1.0m, accounting.ToSmallestUnit(Money.Satoshis(1.0m).ToDecimal(MoneyUnit.BTC)));
|
||||||
Assert.Equal(Money.Coins(1.1m), accounting.TotalDue);
|
Assert.Equal(1.1m, accounting.Due);
|
||||||
|
Assert.Equal(1.1m, accounting.TotalDue);
|
||||||
|
|
||||||
entity.Payments.Add(new PaymentEntity()
|
entity.Payments.Add(new PaymentEntity()
|
||||||
{
|
{
|
||||||
|
Currency = "BTC",
|
||||||
Output = new TxOut(Money.Coins(0.5m), new Key()),
|
Output = new TxOut(Money.Coins(0.5m), new Key()),
|
||||||
|
Rate = 5000,
|
||||||
Accounted = true,
|
Accounted = true,
|
||||||
NetworkFee = 0.1m
|
NetworkFee = 0.1m
|
||||||
});
|
});
|
||||||
|
entity.UpdateTotals();
|
||||||
accounting = paymentMethod.Calculate();
|
accounting = paymentMethod.Calculate();
|
||||||
//Since we need to spend one more txout, it should be 1.1 - 0,5 + 0.1
|
//Since we need to spend one more txout, it should be 1.1 - 0,5 + 0.1
|
||||||
Assert.Equal(Money.Coins(0.7m), accounting.Due);
|
Assert.Equal(0.7m, accounting.Due);
|
||||||
Assert.Equal(Money.Coins(1.2m), accounting.TotalDue);
|
Assert.Equal(1.2m, accounting.TotalDue);
|
||||||
|
|
||||||
entity.Payments.Add(new PaymentEntity()
|
entity.Payments.Add(new PaymentEntity()
|
||||||
{
|
{
|
||||||
|
Currency = "BTC",
|
||||||
Output = new TxOut(Money.Coins(0.2m), new Key()),
|
Output = new TxOut(Money.Coins(0.2m), new Key()),
|
||||||
Accounted = true,
|
Accounted = true,
|
||||||
NetworkFee = 0.1m
|
NetworkFee = 0.1m
|
||||||
});
|
});
|
||||||
|
entity.UpdateTotals();
|
||||||
accounting = paymentMethod.Calculate();
|
accounting = paymentMethod.Calculate();
|
||||||
Assert.Equal(Money.Coins(0.6m), accounting.Due);
|
Assert.Equal(0.6m, accounting.Due);
|
||||||
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
|
Assert.Equal(1.3m, accounting.TotalDue);
|
||||||
|
|
||||||
entity.Payments.Add(new PaymentEntity()
|
entity.Payments.Add(new PaymentEntity()
|
||||||
{
|
{
|
||||||
|
Currency = "BTC",
|
||||||
Output = new TxOut(Money.Coins(0.6m), new Key()),
|
Output = new TxOut(Money.Coins(0.6m), new Key()),
|
||||||
Accounted = true,
|
Accounted = true,
|
||||||
NetworkFee = 0.1m
|
NetworkFee = 0.1m
|
||||||
});
|
});
|
||||||
|
entity.UpdateTotals();
|
||||||
accounting = paymentMethod.Calculate();
|
accounting = paymentMethod.Calculate();
|
||||||
Assert.Equal(Money.Zero, accounting.Due);
|
Assert.Equal(0.0m, accounting.Due);
|
||||||
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
|
Assert.Equal(1.3m, accounting.TotalDue);
|
||||||
|
|
||||||
entity.Payments.Add(
|
entity.Payments.Add(
|
||||||
new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true });
|
new PaymentEntity() { Currency = "BTC", Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true });
|
||||||
|
entity.UpdateTotals();
|
||||||
accounting = paymentMethod.Calculate();
|
accounting = paymentMethod.Calculate();
|
||||||
Assert.Equal(Money.Zero, accounting.Due);
|
Assert.Equal(0.0m, accounting.Due);
|
||||||
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
|
Assert.Equal(1.3m, accounting.TotalDue);
|
||||||
|
|
||||||
entity = new InvoiceEntity();
|
entity = new InvoiceEntity();
|
||||||
entity.Networks = networkProvider;
|
entity.Networks = networkProvider;
|
||||||
entity.Price = 5000;
|
entity.Price = 5000;
|
||||||
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
|
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
|
||||||
paymentMethods.Add(
|
paymentMethods.Add(
|
||||||
new PaymentMethod() { CryptoCode = "BTC", Rate = 1000, NextNetworkFee = Money.Coins(0.1m) });
|
new PaymentMethod() { Currency = "BTC", Rate = 1000, NextNetworkFee = Money.Coins(0.1m) });
|
||||||
paymentMethods.Add(
|
paymentMethods.Add(
|
||||||
new PaymentMethod() { CryptoCode = "LTC", Rate = 500, NextNetworkFee = Money.Coins(0.01m) });
|
new PaymentMethod() { Currency = "LTC", Rate = 500, NextNetworkFee = Money.Coins(0.01m) });
|
||||||
entity.SetPaymentMethods(paymentMethods);
|
entity.SetPaymentMethods(paymentMethods);
|
||||||
entity.Payments = new List<PaymentEntity>();
|
entity.Payments = new List<PaymentEntity>();
|
||||||
|
entity.UpdateTotals();
|
||||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
|
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
|
||||||
accounting = paymentMethod.Calculate();
|
accounting = paymentMethod.Calculate();
|
||||||
Assert.Equal(Money.Coins(5.1m), accounting.Due);
|
Assert.Equal(5.1m, accounting.Due);
|
||||||
|
|
||||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
|
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
|
||||||
accounting = paymentMethod.Calculate();
|
accounting = paymentMethod.Calculate();
|
||||||
|
|
||||||
Assert.Equal(Money.Coins(10.01m), accounting.TotalDue);
|
Assert.Equal(10.01m, accounting.TotalDue);
|
||||||
|
|
||||||
entity.Payments.Add(new PaymentEntity()
|
entity.Payments.Add(new PaymentEntity()
|
||||||
{
|
{
|
||||||
CryptoCode = "BTC",
|
Currency = "BTC",
|
||||||
Output = new TxOut(Money.Coins(1.0m), new Key()),
|
Output = new TxOut(Money.Coins(1.0m), new Key()),
|
||||||
Accounted = true,
|
Accounted = true,
|
||||||
NetworkFee = 0.1m
|
NetworkFee = 0.1m
|
||||||
});
|
});
|
||||||
|
entity.UpdateTotals();
|
||||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
|
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
|
||||||
accounting = paymentMethod.Calculate();
|
accounting = paymentMethod.Calculate();
|
||||||
Assert.Equal(Money.Coins(4.2m), accounting.Due);
|
Assert.Equal(4.2m, accounting.Due);
|
||||||
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
|
Assert.Equal(1.0m, accounting.CryptoPaid);
|
||||||
Assert.Equal(Money.Coins(1.0m), accounting.Paid);
|
Assert.Equal(1.0m, accounting.Paid);
|
||||||
Assert.Equal(Money.Coins(5.2m), accounting.TotalDue);
|
Assert.Equal(5.2m, accounting.TotalDue);
|
||||||
Assert.Equal(2, accounting.TxRequired);
|
Assert.Equal(2, accounting.TxRequired);
|
||||||
|
|
||||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
|
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
|
||||||
accounting = paymentMethod.Calculate();
|
accounting = paymentMethod.Calculate();
|
||||||
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 - 2.0m /* 8.21m */), accounting.Due);
|
Assert.Equal(10.01m + 0.1m * 2 - 2.0m /* 8.21m */, accounting.Due);
|
||||||
Assert.Equal(Money.Coins(0.0m), accounting.CryptoPaid);
|
Assert.Equal(0.0m, accounting.CryptoPaid);
|
||||||
Assert.Equal(Money.Coins(2.0m), accounting.Paid);
|
Assert.Equal(2.0m, accounting.Paid);
|
||||||
Assert.Equal(Money.Coins(10.01m + 0.1m * 2), accounting.TotalDue);
|
Assert.Equal(10.01m + 0.1m * 2, accounting.TotalDue);
|
||||||
|
|
||||||
entity.Payments.Add(new PaymentEntity()
|
entity.Payments.Add(new PaymentEntity()
|
||||||
{
|
{
|
||||||
CryptoCode = "LTC",
|
Currency = "LTC",
|
||||||
Output = new TxOut(Money.Coins(1.0m), new Key()),
|
Output = new TxOut(Money.Coins(1.0m), new Key()),
|
||||||
Accounted = true,
|
Accounted = true,
|
||||||
NetworkFee = 0.01m
|
NetworkFee = 0.01m
|
||||||
});
|
});
|
||||||
|
entity.UpdateTotals();
|
||||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
|
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
|
||||||
accounting = paymentMethod.Calculate();
|
accounting = paymentMethod.Calculate();
|
||||||
Assert.Equal(Money.Coins(4.2m - 0.5m + 0.01m / 2), accounting.Due);
|
Assert.Equal(4.2m - 0.5m + 0.01m / 2, accounting.Due);
|
||||||
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
|
Assert.Equal(1.0m, accounting.CryptoPaid);
|
||||||
Assert.Equal(Money.Coins(1.5m), accounting.Paid);
|
Assert.Equal(1.5m, accounting.Paid);
|
||||||
Assert.Equal(Money.Coins(5.2m + 0.01m / 2), accounting.TotalDue); // The fee for LTC added
|
Assert.Equal(5.2m + 0.01m / 2, accounting.TotalDue); // The fee for LTC added
|
||||||
Assert.Equal(2, accounting.TxRequired);
|
Assert.Equal(2, accounting.TxRequired);
|
||||||
|
|
||||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
|
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
|
||||||
accounting = paymentMethod.Calculate();
|
accounting = paymentMethod.Calculate();
|
||||||
Assert.Equal(Money.Coins(8.21m - 1.0m + 0.01m), accounting.Due);
|
Assert.Equal(8.21m - 1.0m + 0.01m, accounting.Due);
|
||||||
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
|
Assert.Equal(1.0m, accounting.CryptoPaid);
|
||||||
Assert.Equal(Money.Coins(3.0m), accounting.Paid);
|
Assert.Equal(3.0m, accounting.Paid);
|
||||||
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 + 0.01m), accounting.TotalDue);
|
Assert.Equal(10.01m + 0.1m * 2 + 0.01m, accounting.TotalDue);
|
||||||
Assert.Equal(2, accounting.TxRequired);
|
Assert.Equal(2, accounting.TxRequired);
|
||||||
|
|
||||||
var remaining = Money.Coins(4.2m - 0.5m + 0.01m / 2);
|
var remaining = Money.Coins(4.2m - 0.5m + 0.01m / 2.0m).ToDecimal(MoneyUnit.BTC);
|
||||||
entity.Payments.Add(new PaymentEntity()
|
entity.Payments.Add(new PaymentEntity()
|
||||||
{
|
{
|
||||||
CryptoCode = "BTC",
|
Currency = "BTC",
|
||||||
Output = new TxOut(remaining, new Key()),
|
Output = new TxOut(Money.Coins(remaining), new Key()),
|
||||||
Accounted = true,
|
Accounted = true,
|
||||||
NetworkFee = 0.1m
|
NetworkFee = 0.1m
|
||||||
});
|
});
|
||||||
|
entity.UpdateTotals();
|
||||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
|
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
|
||||||
accounting = paymentMethod.Calculate();
|
accounting = paymentMethod.Calculate();
|
||||||
Assert.Equal(Money.Zero, accounting.Due);
|
Assert.Equal(0.0m, accounting.Due);
|
||||||
Assert.Equal(Money.Coins(1.0m) + remaining, accounting.CryptoPaid);
|
Assert.Equal(1.0m + remaining, accounting.CryptoPaid);
|
||||||
Assert.Equal(Money.Coins(1.5m) + remaining, accounting.Paid);
|
Assert.Equal(1.5m + remaining, accounting.Paid);
|
||||||
Assert.Equal(Money.Coins(5.2m + 0.01m / 2), accounting.TotalDue);
|
Assert.Equal(5.2m + 0.01m / 2, accounting.TotalDue);
|
||||||
Assert.Equal(accounting.Paid, accounting.TotalDue);
|
Assert.Equal(accounting.Paid, accounting.TotalDue);
|
||||||
Assert.Equal(2, accounting.TxRequired);
|
Assert.Equal(2, accounting.TxRequired);
|
||||||
|
|
||||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
|
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
|
||||||
accounting = paymentMethod.Calculate();
|
accounting = paymentMethod.Calculate();
|
||||||
Assert.Equal(Money.Zero, accounting.Due);
|
Assert.Equal(0.0m, accounting.Due);
|
||||||
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
|
Assert.Equal(1.0m, accounting.CryptoPaid);
|
||||||
Assert.Equal(Money.Coins(3.0m) + remaining * 2, accounting.Paid);
|
Assert.Equal(3.0m + remaining * 2, accounting.Paid);
|
||||||
// Paying 2 BTC fee, LTC fee removed because fully paid
|
// Paying 2 BTC fee, LTC fee removed because fully paid
|
||||||
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 + 0.1m * 2 /* + 0.01m no need to pay this fee anymore */),
|
Assert.Equal(10.01m + 0.1m * 2 + 0.1m * 2 /* + 0.01m no need to pay this fee anymore */,
|
||||||
accounting.TotalDue);
|
accounting.TotalDue);
|
||||||
Assert.Equal(1, accounting.TxRequired);
|
Assert.Equal(1, accounting.TxRequired);
|
||||||
Assert.Equal(accounting.Paid, accounting.TotalDue);
|
Assert.Equal(accounting.Paid, accounting.TotalDue);
|
||||||
@@ -547,27 +656,29 @@ namespace BTCPayServer.Tests
|
|||||||
entity.Payments = new List<PaymentEntity>();
|
entity.Payments = new List<PaymentEntity>();
|
||||||
entity.SetPaymentMethod(new PaymentMethod()
|
entity.SetPaymentMethod(new PaymentMethod()
|
||||||
{
|
{
|
||||||
CryptoCode = "BTC",
|
Currency = "BTC",
|
||||||
Rate = 5000,
|
Rate = 5000,
|
||||||
NextNetworkFee = Money.Coins(0.1m)
|
NextNetworkFee = Money.Coins(0.1m)
|
||||||
});
|
});
|
||||||
entity.Price = 5000;
|
entity.Price = 5000;
|
||||||
entity.PaymentTolerance = 0;
|
entity.PaymentTolerance = 0;
|
||||||
|
entity.UpdateTotals();
|
||||||
|
|
||||||
var paymentMethod = entity.GetPaymentMethods().TryGet("BTC", PaymentTypes.BTCLike);
|
var paymentMethod = entity.GetPaymentMethods().TryGet("BTC", PaymentTypes.BTCLike);
|
||||||
var accounting = paymentMethod.Calculate();
|
var accounting = paymentMethod.Calculate();
|
||||||
Assert.Equal(Money.Coins(1.1m), accounting.Due);
|
Assert.Equal(1.1m, accounting.Due);
|
||||||
Assert.Equal(Money.Coins(1.1m), accounting.TotalDue);
|
Assert.Equal(1.1m, accounting.TotalDue);
|
||||||
Assert.Equal(Money.Coins(1.1m), accounting.MinimumTotalDue);
|
Assert.Equal(1.1m, accounting.MinimumTotalDue);
|
||||||
|
|
||||||
entity.PaymentTolerance = 10;
|
entity.PaymentTolerance = 10;
|
||||||
|
entity.UpdateTotals();
|
||||||
accounting = paymentMethod.Calculate();
|
accounting = paymentMethod.Calculate();
|
||||||
Assert.Equal(Money.Coins(0.99m), accounting.MinimumTotalDue);
|
Assert.Equal(0.99m, accounting.MinimumTotalDue);
|
||||||
|
|
||||||
entity.PaymentTolerance = 100;
|
entity.PaymentTolerance = 100;
|
||||||
|
entity.UpdateTotals();
|
||||||
accounting = paymentMethod.Calculate();
|
accounting = paymentMethod.Calculate();
|
||||||
Assert.Equal(Money.Satoshis(1), accounting.MinimumTotalDue);
|
Assert.Equal(0.0000_0001m, accounting.MinimumTotalDue);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -608,7 +719,7 @@ namespace BTCPayServer.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CanDetectImage()
|
public void CanDetectFileType()
|
||||||
{
|
{
|
||||||
Assert.True(FileTypeDetector.IsPicture(new byte[] { 0x42, 0x4D }, "test.bmp"));
|
Assert.True(FileTypeDetector.IsPicture(new byte[] { 0x42, 0x4D }, "test.bmp"));
|
||||||
Assert.False(FileTypeDetector.IsPicture(new byte[] { 0x42, 0x4D }, ".bmp"));
|
Assert.False(FileTypeDetector.IsPicture(new byte[] { 0x42, 0x4D }, ".bmp"));
|
||||||
@@ -621,6 +732,15 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.False(FileTypeDetector.IsPicture(new byte[] { 0x3C, 0x73, 0x76, 0x67 }, "test.jpg"));
|
Assert.False(FileTypeDetector.IsPicture(new byte[] { 0x3C, 0x73, 0x76, 0x67 }, "test.jpg"));
|
||||||
Assert.False(FileTypeDetector.IsPicture(new byte[] { 0xFF }, "e.jpg"));
|
Assert.False(FileTypeDetector.IsPicture(new byte[] { 0xFF }, "e.jpg"));
|
||||||
Assert.False(FileTypeDetector.IsPicture(new byte[] { }, "empty.jpg"));
|
Assert.False(FileTypeDetector.IsPicture(new byte[] { }, "empty.jpg"));
|
||||||
|
|
||||||
|
Assert.False(FileTypeDetector.IsPicture(new byte[] { 0x49, 0x44, 0x33, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23 }, "music.mp3"));
|
||||||
|
Assert.True(FileTypeDetector.IsAudio(new byte[] { 0x49, 0x44, 0x33, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23 }, "music.mp3"));
|
||||||
|
Assert.True(FileTypeDetector.IsAudio(new byte[] { 0x52, 0x49, 0x46, 0x46, 0x24, 0x9A, 0x08, 0x00, 0x57, 0x41 }, "music.wav"));
|
||||||
|
Assert.True(FileTypeDetector.IsAudio(new byte[] { 0xFF, 0xF1, 0x50, 0x80, 0x1C, 0x3F, 0xFC, 0xDA, 0x00, 0x4C }, "music.aac"));
|
||||||
|
Assert.True(FileTypeDetector.IsAudio(new byte[] { 0x66, 0x4C, 0x61, 0x43, 0x00, 0x00, 0x00, 0x22, 0x04, 0x80 }, "music.flac"));
|
||||||
|
Assert.True(FileTypeDetector.IsAudio(new byte[] { 0x4F, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00 }, "music.ogg"));
|
||||||
|
Assert.True(FileTypeDetector.IsAudio(new byte[] { 0x1A, 0x45, 0xDF, 0xA3, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00 }, "music.weba"));
|
||||||
|
Assert.True(FileTypeDetector.IsAudio(new byte[] { 0xFF, 0xF3, 0xE4, 0x64, 0x00, 0x20, 0xAD, 0xBD, 0x04, 0x00 }, "music.mp3"));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -1042,14 +1162,13 @@ namespace BTCPayServer.Tests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void CanParseFilter()
|
public void CanParseFilter()
|
||||||
{
|
{
|
||||||
|
var storeId = "6DehZnc9S7qC6TUTNWuzJ1pFsHTHvES6An21r3MjvLey";
|
||||||
var filter = "storeid:abc, status:abed, blabhbalh ";
|
var filter = "storeid:abc, status:abed, blabhbalh ";
|
||||||
var search = new SearchString(filter);
|
var search = new SearchString(filter);
|
||||||
Assert.Equal("storeid:abc, status:abed, blabhbalh", search.ToString());
|
Assert.Equal("storeid:abc, status:abed, blabhbalh", search.ToString());
|
||||||
Assert.Equal("blabhbalh", search.TextSearch);
|
Assert.Equal("blabhbalh", search.TextSearch);
|
||||||
Assert.Single(search.Filters["storeid"]);
|
Assert.Single(search.Filters["storeid"], "abc");
|
||||||
Assert.Single(search.Filters["status"]);
|
Assert.Single(search.Filters["status"], "abed");
|
||||||
Assert.Equal("abc", search.Filters["storeid"].First());
|
|
||||||
Assert.Equal("abed", search.Filters["status"].First());
|
|
||||||
|
|
||||||
filter = "status:abed, status:abed2";
|
filter = "status:abed, status:abed2";
|
||||||
search = new SearchString(filter);
|
search = new SearchString(filter);
|
||||||
@@ -1064,6 +1183,48 @@ namespace BTCPayServer.Tests
|
|||||||
search = new SearchString(filter);
|
search = new SearchString(filter);
|
||||||
Assert.Equal("2019-04-25 01:00 AM", search.Filters["startdate"].First());
|
Assert.Equal("2019-04-25 01:00 AM", search.Filters["startdate"].First());
|
||||||
Assert.Equal("hekki", search.TextSearch);
|
Assert.Equal("hekki", search.TextSearch);
|
||||||
|
|
||||||
|
// modify search
|
||||||
|
filter = $"status:settled,exceptionstatus:paidLate,unusual:true, fulltext searchterm, storeid:{storeId},startdate:2019-04-25 01:00:00";
|
||||||
|
search = new SearchString(filter);
|
||||||
|
Assert.Equal(filter, search.ToString());
|
||||||
|
Assert.Equal("fulltext searchterm", search.TextSearch);
|
||||||
|
Assert.Single(search.Filters["storeid"], storeId);
|
||||||
|
Assert.Single(search.Filters["status"], "settled");
|
||||||
|
Assert.Single(search.Filters["exceptionstatus"], "paidLate");
|
||||||
|
Assert.Single(search.Filters["unusual"], "true");
|
||||||
|
|
||||||
|
// toggle off bool with same value
|
||||||
|
var modified = new SearchString(search.Toggle("unusual", "true"));
|
||||||
|
Assert.Null(modified.GetFilterBool("unusual"));
|
||||||
|
|
||||||
|
// add to array
|
||||||
|
modified = new SearchString(modified.Toggle("status", "processing"));
|
||||||
|
var statusArray = modified.GetFilterArray("status");
|
||||||
|
Assert.Equal(2, statusArray.Length);
|
||||||
|
Assert.Contains("processing", statusArray);
|
||||||
|
Assert.Contains("settled", statusArray);
|
||||||
|
|
||||||
|
// toggle off array with same value
|
||||||
|
modified = new SearchString(modified.Toggle("status", "settled"));
|
||||||
|
statusArray = modified.GetFilterArray("status");
|
||||||
|
Assert.Single(statusArray, "processing");
|
||||||
|
|
||||||
|
// toggle off array with null value
|
||||||
|
modified = new SearchString(modified.Toggle("status", null));
|
||||||
|
Assert.Null(modified.GetFilterArray("status"));
|
||||||
|
|
||||||
|
// toggle off date with null value
|
||||||
|
modified = new SearchString(modified.Toggle("startdate", "-7d"));
|
||||||
|
Assert.Single(modified.GetFilterArray("startdate"), "-7d");
|
||||||
|
modified = new SearchString(modified.Toggle("startdate", null));
|
||||||
|
Assert.Null(modified.GetFilterArray("startdate"));
|
||||||
|
|
||||||
|
// toggle off date with same value
|
||||||
|
modified = new SearchString(modified.Toggle("enddate", "-7d"));
|
||||||
|
Assert.Single(modified.GetFilterArray("enddate"), "-7d");
|
||||||
|
modified = new SearchString(modified.Toggle("enddate", "-7d"));
|
||||||
|
Assert.Null(modified.GetFilterArray("enddate"));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -1103,6 +1264,45 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.Equal("000000161", m.OrderId);
|
Assert.Equal("000000161", m.OrderId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanParseOldPosAppData()
|
||||||
|
{
|
||||||
|
var data = new JObject()
|
||||||
|
{
|
||||||
|
["price"] = 1.64m
|
||||||
|
}.ToString();
|
||||||
|
Assert.Equal(1.64m, JsonConvert.DeserializeObject<PosAppCartItem>(data).Price);
|
||||||
|
|
||||||
|
data = new JObject()
|
||||||
|
{
|
||||||
|
["price"] = new JObject()
|
||||||
|
{
|
||||||
|
["value"] = 1.65m
|
||||||
|
}
|
||||||
|
}.ToString();
|
||||||
|
Assert.Equal(1.65m, JsonConvert.DeserializeObject<PosAppCartItem>(data).Price);
|
||||||
|
data = new JObject()
|
||||||
|
{
|
||||||
|
["price"] = new JObject()
|
||||||
|
{
|
||||||
|
["value"] = "1.6305"
|
||||||
|
}
|
||||||
|
}.ToString();
|
||||||
|
Assert.Equal(1.6305m, JsonConvert.DeserializeObject<PosAppCartItem>(data).Price);
|
||||||
|
|
||||||
|
data = new JObject()
|
||||||
|
{
|
||||||
|
["price"] = new JObject()
|
||||||
|
{
|
||||||
|
["value"] = null
|
||||||
|
}
|
||||||
|
}.ToString();
|
||||||
|
Assert.Equal(0.0m, JsonConvert.DeserializeObject<PosAppCartItem>(data).Price);
|
||||||
|
|
||||||
|
var o = JObject.Parse(JsonConvert.SerializeObject(new PosAppCartItem() { Price = 1.356m }));
|
||||||
|
Assert.Equal(1.356m, o["price"].Value<decimal>());
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CanParseCurrencyValue()
|
public void CanParseCurrencyValue()
|
||||||
{
|
{
|
||||||
@@ -1341,6 +1541,24 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.Equal(cache.States[0].Rates[0].Pair, cache2.States[0].Rates[0].Pair);
|
Assert.Equal(cache.States[0].Rates[0].Pair, cache2.States[0].Rates[0].Pair);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanParseStoreRoleId()
|
||||||
|
{
|
||||||
|
var id = StoreRoleId.Parse("test::lol");
|
||||||
|
Assert.Equal("test", id.StoreId);
|
||||||
|
Assert.Equal("lol", id.Role);
|
||||||
|
Assert.Equal("test::lol", id.ToString());
|
||||||
|
Assert.Equal("test::lol", id.Id);
|
||||||
|
Assert.False(id.IsServerRole);
|
||||||
|
|
||||||
|
id = StoreRoleId.Parse("lol");
|
||||||
|
Assert.Null(id.StoreId);
|
||||||
|
Assert.Equal("lol", id.Role);
|
||||||
|
Assert.Equal("lol", id.ToString());
|
||||||
|
Assert.Equal("lol", id.Id);
|
||||||
|
Assert.True(id.IsServerRole);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void KitchenSinkTest()
|
public void KitchenSinkTest()
|
||||||
{
|
{
|
||||||
@@ -1785,11 +2003,6 @@ namespace BTCPayServer.Tests
|
|||||||
#pragma warning disable CS0618
|
#pragma warning disable CS0618
|
||||||
var dummy = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, Network.RegTest).ToString();
|
var dummy = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, Network.RegTest).ToString();
|
||||||
var networkProvider = new BTCPayNetworkProvider(ChainName.Regtest);
|
var networkProvider = new BTCPayNetworkProvider(ChainName.Regtest);
|
||||||
var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[]
|
|
||||||
{
|
|
||||||
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null, null),
|
|
||||||
new LightningLikePaymentHandler(null, null, networkProvider, null, null, null),
|
|
||||||
});
|
|
||||||
var networkBTC = networkProvider.GetNetwork("BTC");
|
var networkBTC = networkProvider.GetNetwork("BTC");
|
||||||
var networkLTC = networkProvider.GetNetwork("LTC");
|
var networkLTC = networkProvider.GetNetwork("LTC");
|
||||||
InvoiceEntity invoiceEntity = new InvoiceEntity();
|
InvoiceEntity invoiceEntity = new InvoiceEntity();
|
||||||
@@ -1797,14 +2010,14 @@ namespace BTCPayServer.Tests
|
|||||||
invoiceEntity.Payments = new System.Collections.Generic.List<PaymentEntity>();
|
invoiceEntity.Payments = new System.Collections.Generic.List<PaymentEntity>();
|
||||||
invoiceEntity.Price = 100;
|
invoiceEntity.Price = 100;
|
||||||
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
|
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
|
||||||
paymentMethods.Add(new PaymentMethod() { Network = networkBTC, CryptoCode = "BTC", Rate = 10513.44m, }
|
paymentMethods.Add(new PaymentMethod() { Network = networkBTC, Currency = "BTC", Rate = 10513.44m, }
|
||||||
.SetPaymentMethodDetails(
|
.SetPaymentMethodDetails(
|
||||||
new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
|
new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
|
||||||
{
|
{
|
||||||
NextNetworkFee = Money.Coins(0.00000100m),
|
NextNetworkFee = Money.Coins(0.00000100m),
|
||||||
DepositAddress = dummy
|
DepositAddress = dummy
|
||||||
}));
|
}));
|
||||||
paymentMethods.Add(new PaymentMethod() { Network = networkLTC, CryptoCode = "LTC", Rate = 216.79m }
|
paymentMethods.Add(new PaymentMethod() { Network = networkLTC, Currency = "LTC", Rate = 216.79m }
|
||||||
.SetPaymentMethodDetails(
|
.SetPaymentMethodDetails(
|
||||||
new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
|
new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
|
||||||
{
|
{
|
||||||
@@ -1820,7 +2033,7 @@ namespace BTCPayServer.Tests
|
|||||||
new PaymentEntity()
|
new PaymentEntity()
|
||||||
{
|
{
|
||||||
Accounted = true,
|
Accounted = true,
|
||||||
CryptoCode = "BTC",
|
Currency = "BTC",
|
||||||
NetworkFee = 0.00000100m,
|
NetworkFee = 0.00000100m,
|
||||||
Network = networkProvider.GetNetwork("BTC"),
|
Network = networkProvider.GetNetwork("BTC"),
|
||||||
}
|
}
|
||||||
@@ -1829,34 +2042,33 @@ namespace BTCPayServer.Tests
|
|||||||
Network = networkProvider.GetNetwork("BTC"),
|
Network = networkProvider.GetNetwork("BTC"),
|
||||||
Output = new TxOut() { Value = Money.Coins(0.00151263m) }
|
Output = new TxOut() { Value = Money.Coins(0.00151263m) }
|
||||||
}));
|
}));
|
||||||
|
invoiceEntity.UpdateTotals();
|
||||||
accounting = btc.Calculate();
|
accounting = btc.Calculate();
|
||||||
invoiceEntity.Payments.Add(
|
invoiceEntity.Payments.Add(
|
||||||
new PaymentEntity()
|
new PaymentEntity()
|
||||||
{
|
{
|
||||||
Accounted = true,
|
Accounted = true,
|
||||||
CryptoCode = "BTC",
|
Currency = "BTC",
|
||||||
NetworkFee = 0.00000100m,
|
NetworkFee = 0.00000100m,
|
||||||
Network = networkProvider.GetNetwork("BTC")
|
Network = networkProvider.GetNetwork("BTC")
|
||||||
}
|
}
|
||||||
.SetCryptoPaymentData(new BitcoinLikePaymentData()
|
.SetCryptoPaymentData(new BitcoinLikePaymentData()
|
||||||
{
|
{
|
||||||
Network = networkProvider.GetNetwork("BTC"),
|
Network = networkProvider.GetNetwork("BTC"),
|
||||||
Output = new TxOut() { Value = accounting.Due }
|
Output = new TxOut() { Value = Money.Coins(accounting.Due) }
|
||||||
}));
|
}));
|
||||||
|
invoiceEntity.UpdateTotals();
|
||||||
accounting = btc.Calculate();
|
accounting = btc.Calculate();
|
||||||
Assert.Equal(Money.Zero, accounting.Due);
|
Assert.Equal(0.0m, accounting.Due);
|
||||||
Assert.Equal(Money.Zero, accounting.DueUncapped);
|
Assert.Equal(0.0m, accounting.DueUncapped);
|
||||||
|
|
||||||
var ltc = invoiceEntity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
|
var ltc = invoiceEntity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
|
||||||
accounting = ltc.Calculate();
|
accounting = ltc.Calculate();
|
||||||
|
|
||||||
Assert.Equal(Money.Zero, accounting.Due);
|
Assert.Equal(0.0m, accounting.Due);
|
||||||
// LTC might have over paid due to BTC paying above what it should (round 1 satoshi up)
|
// LTC might should be over paid due to BTC paying above what it should (round 1 satoshi up), but we handle this case
|
||||||
Assert.True(accounting.DueUncapped < Money.Zero);
|
// and set DueUncapped to zero.
|
||||||
|
Assert.Equal(0.0m, accounting.DueUncapped);
|
||||||
var paymentMethod = InvoiceWatcher.GetNearestClearedPayment(paymentMethods, out var accounting2);
|
|
||||||
Assert.Equal(btc.CryptoCode, paymentMethod.CryptoCode);
|
|
||||||
#pragma warning restore CS0618
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Abstractions.Form;
|
using BTCPayServer.Abstractions.Form;
|
||||||
using BTCPayServer.Forms;
|
using BTCPayServer.Forms;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
@@ -10,16 +11,25 @@ using Xunit.Abstractions;
|
|||||||
|
|
||||||
namespace BTCPayServer.Tests;
|
namespace BTCPayServer.Tests;
|
||||||
|
|
||||||
[Trait("Fast", "Fast")]
|
[Collection(nameof(NonParallelizableCollectionDefinition))]
|
||||||
|
[Trait("Integration", "Integration")]
|
||||||
public class FormTests : UnitTestBase
|
public class FormTests : UnitTestBase
|
||||||
{
|
{
|
||||||
public FormTests(ITestOutputHelper helper) : base(helper)
|
public FormTests(ITestOutputHelper helper) : base(helper)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void CanParseForm()
|
[Fact(Timeout = TestUtils.TestTimeout)]
|
||||||
|
[Trait("Integration", "Integration")]
|
||||||
|
public async Task CanParseForm()
|
||||||
{
|
{
|
||||||
|
using var tester = CreateServerTester();
|
||||||
|
await tester.StartAsync();
|
||||||
|
var user = tester.NewAccount();
|
||||||
|
user.GrantAccess();
|
||||||
|
var service = tester.PayTester.GetService<FormDataService>();
|
||||||
|
|
||||||
var form = new Form()
|
var form = new Form()
|
||||||
{
|
{
|
||||||
Fields = new List<Field>
|
Fields = new List<Field>
|
||||||
@@ -40,8 +50,6 @@ public class FormTests : UnitTestBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var providers = new FormComponentProviders(new List<IFormComponentProvider>());
|
|
||||||
var service = new FormDataService(null, providers);
|
|
||||||
Assert.False(service.IsFormSchemaValid(form.ToString(), out _, out _));
|
Assert.False(service.IsFormSchemaValid(form.ToString(), out _, out _));
|
||||||
form = new Form
|
form = new Form
|
||||||
{
|
{
|
||||||
@@ -164,7 +172,7 @@ public class FormTests : UnitTestBase
|
|||||||
Assert.Equal("original", obj["invoice"]["test"].Value<string>());
|
Assert.Equal("original", obj["invoice"]["test"].Value<string>());
|
||||||
Assert.Equal("updated", obj["invoice_item3"].Value<string>());
|
Assert.Equal("updated", obj["invoice_item3"].Value<string>());
|
||||||
Clear(form);
|
Clear(form);
|
||||||
form.SetValues(obj);
|
service.SetValues(form, obj);
|
||||||
obj = service.GetValues(form);
|
obj = service.GetValues(form);
|
||||||
Assert.Equal("original", obj["invoice"]["test"].Value<string>());
|
Assert.Equal("original", obj["invoice"]["test"].Value<string>());
|
||||||
Assert.Equal("updated", obj["invoice_item3"].Value<string>());
|
Assert.Equal("updated", obj["invoice_item3"].Value<string>());
|
||||||
@@ -182,10 +190,12 @@ public class FormTests : UnitTestBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
form.SetValues(obj);
|
|
||||||
|
service.SetValues(form, obj);
|
||||||
obj = service.GetValues(form);
|
obj = service.GetValues(form);
|
||||||
Assert.Null(obj["test"].Value<string>());
|
Assert.Null(obj["test"].Value<string>());
|
||||||
form.SetValues(new JObject { ["test"] = "hello" });
|
|
||||||
|
service.SetValues(form, new JObject { ["test"] = "hello" });
|
||||||
obj = service.GetValues(form);
|
obj = service.GetValues(form);
|
||||||
Assert.Equal("hello", obj["test"].Value<string>());
|
Assert.Equal("hello", obj["test"].Value<string>());
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -16,14 +15,13 @@ using BTCPayServer.Models.InvoicingModels;
|
|||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
using BTCPayServer.Payments.Lightning;
|
using BTCPayServer.Payments.Lightning;
|
||||||
using BTCPayServer.PayoutProcessors;
|
using BTCPayServer.PayoutProcessors;
|
||||||
using BTCPayServer.PayoutProcessors.OnChain;
|
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
using BTCPayServer.Services.Custodian.Client.MockCustodian;
|
using BTCPayServer.Services.Custodian.Client.MockCustodian;
|
||||||
using BTCPayServer.Services.Notifications;
|
using BTCPayServer.Services.Notifications;
|
||||||
using BTCPayServer.Services.Notifications.Blobs;
|
using BTCPayServer.Services.Notifications.Blobs;
|
||||||
|
using BTCPayServer.Services.Stores;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using NBitpayClient;
|
using NBitpayClient;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
@@ -228,7 +226,7 @@ namespace BTCPayServer.Tests
|
|||||||
await Assert.ThrowsAsync<GreenfieldAPIException>(() => newUserClient.GetInvoices(store.Id));
|
await Assert.ThrowsAsync<GreenfieldAPIException>(() => newUserClient.GetInvoices(store.Id));
|
||||||
|
|
||||||
// if user is a guest or owner, then it should be ok
|
// if user is a guest or owner, then it should be ok
|
||||||
await unrestricted.AddStoreUser(store.Id, new StoreUserData() { UserId = newUser.Id, Role = "Guest" });
|
await unrestricted.AddStoreUser(store.Id, new StoreUserData() { UserId = newUser.Id});
|
||||||
await newUserClient.GetInvoices(store.Id);
|
await newUserClient.GetInvoices(store.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,6 +297,7 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.Equal(user.StoreId, app.StoreId);
|
Assert.Equal(user.StoreId, app.StoreId);
|
||||||
Assert.Equal("PointOfSale", app.AppType);
|
Assert.Equal("PointOfSale", app.AppType);
|
||||||
Assert.Equal("test app title", app.Title);
|
Assert.Equal("test app title", app.Title);
|
||||||
|
Assert.False(app.Archived);
|
||||||
|
|
||||||
// Make sure we return a 404 if we try to get an app that doesn't exist
|
// Make sure we return a 404 if we try to get an app that doesn't exist
|
||||||
await AssertHttpError(404, async () =>
|
await AssertHttpError(404, async () =>
|
||||||
@@ -322,17 +321,20 @@ namespace BTCPayServer.Tests
|
|||||||
new CreatePointOfSaleAppRequest()
|
new CreatePointOfSaleAppRequest()
|
||||||
{
|
{
|
||||||
AppName = "new app name",
|
AppName = "new app name",
|
||||||
Title = "new app title"
|
Title = "new app title",
|
||||||
|
Archived = true
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
// Test generic GET app endpoint first
|
// Test generic GET app endpoint first
|
||||||
retrievedApp = await client.GetApp(app.Id);
|
retrievedApp = await client.GetApp(app.Id);
|
||||||
Assert.Equal("new app name", retrievedApp.Name);
|
Assert.Equal("new app name", retrievedApp.Name);
|
||||||
|
Assert.True(retrievedApp.Archived);
|
||||||
|
|
||||||
// Test the POS-specific endpoint also
|
// Test the POS-specific endpoint also
|
||||||
var retrievedPosApp = await client.GetPosApp(app.Id);
|
var retrievedPosApp = await client.GetPosApp(app.Id);
|
||||||
Assert.Equal("new app name", retrievedPosApp.Name);
|
Assert.Equal("new app name", retrievedPosApp.Name);
|
||||||
Assert.Equal("new app title", retrievedPosApp.Title);
|
Assert.Equal("new app title", retrievedPosApp.Title);
|
||||||
|
Assert.True(retrievedPosApp.Archived);
|
||||||
|
|
||||||
// Make sure we return a 404 if we try to delete an app that doesn't exist
|
// Make sure we return a 404 if we try to delete an app that doesn't exist
|
||||||
await AssertHttpError(404, async () =>
|
await AssertHttpError(404, async () =>
|
||||||
@@ -464,6 +466,7 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.Equal("test app from API", app.Name);
|
Assert.Equal("test app from API", app.Name);
|
||||||
Assert.Equal(user.StoreId, app.StoreId);
|
Assert.Equal(user.StoreId, app.StoreId);
|
||||||
Assert.Equal("Crowdfund", app.AppType);
|
Assert.Equal("Crowdfund", app.AppType);
|
||||||
|
Assert.False(app.Archived);
|
||||||
|
|
||||||
// Make sure we return a 404 if we try to get an app that doesn't exist
|
// Make sure we return a 404 if we try to get an app that doesn't exist
|
||||||
await AssertHttpError(404, async () =>
|
await AssertHttpError(404, async () =>
|
||||||
@@ -480,11 +483,13 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.Equal(app.Name, retrievedApp.Name);
|
Assert.Equal(app.Name, retrievedApp.Name);
|
||||||
Assert.Equal(app.StoreId, retrievedApp.StoreId);
|
Assert.Equal(app.StoreId, retrievedApp.StoreId);
|
||||||
Assert.Equal(app.AppType, retrievedApp.AppType);
|
Assert.Equal(app.AppType, retrievedApp.AppType);
|
||||||
|
Assert.False(retrievedApp.Archived);
|
||||||
|
|
||||||
// Test the crowdfund-specific endpoint also
|
// Test the crowdfund-specific endpoint also
|
||||||
var retrievedPosApp = await client.GetCrowdfundApp(app.Id);
|
var retrievedCfApp = await client.GetCrowdfundApp(app.Id);
|
||||||
Assert.Equal(app.Name, retrievedPosApp.Name);
|
Assert.Equal(app.Name, retrievedCfApp.Name);
|
||||||
Assert.Equal(app.Title, retrievedPosApp.Title);
|
Assert.Equal(app.Title, retrievedCfApp.Title);
|
||||||
|
Assert.False(retrievedCfApp.Archived);
|
||||||
|
|
||||||
// Make sure we return a 404 if we try to delete an app that doesn't exist
|
// Make sure we return a 404 if we try to delete an app that doesn't exist
|
||||||
await AssertHttpError(404, async () =>
|
await AssertHttpError(404, async () =>
|
||||||
@@ -534,10 +539,12 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.Equal(posApp.Name, apps[0].Name);
|
Assert.Equal(posApp.Name, apps[0].Name);
|
||||||
Assert.Equal(posApp.StoreId, apps[0].StoreId);
|
Assert.Equal(posApp.StoreId, apps[0].StoreId);
|
||||||
Assert.Equal(posApp.AppType, apps[0].AppType);
|
Assert.Equal(posApp.AppType, apps[0].AppType);
|
||||||
|
Assert.False(apps[0].Archived);
|
||||||
|
|
||||||
Assert.Equal(crowdfundApp.Name, apps[1].Name);
|
Assert.Equal(crowdfundApp.Name, apps[1].Name);
|
||||||
Assert.Equal(crowdfundApp.StoreId, apps[1].StoreId);
|
Assert.Equal(crowdfundApp.StoreId, apps[1].StoreId);
|
||||||
Assert.Equal(crowdfundApp.AppType, apps[1].AppType);
|
Assert.Equal(crowdfundApp.AppType, apps[1].AppType);
|
||||||
|
Assert.False(apps[1].Archived);
|
||||||
|
|
||||||
// Get all apps for all store now
|
// Get all apps for all store now
|
||||||
apps = await client.GetAllApps();
|
apps = await client.GetAllApps();
|
||||||
@@ -547,15 +554,17 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.Equal(posApp.Name, apps[0].Name);
|
Assert.Equal(posApp.Name, apps[0].Name);
|
||||||
Assert.Equal(posApp.StoreId, apps[0].StoreId);
|
Assert.Equal(posApp.StoreId, apps[0].StoreId);
|
||||||
Assert.Equal(posApp.AppType, apps[0].AppType);
|
Assert.Equal(posApp.AppType, apps[0].AppType);
|
||||||
|
Assert.False(apps[0].Archived);
|
||||||
|
|
||||||
Assert.Equal(crowdfundApp.Name, apps[1].Name);
|
Assert.Equal(crowdfundApp.Name, apps[1].Name);
|
||||||
Assert.Equal(crowdfundApp.StoreId, apps[1].StoreId);
|
Assert.Equal(crowdfundApp.StoreId, apps[1].StoreId);
|
||||||
Assert.Equal(crowdfundApp.AppType, apps[1].AppType);
|
Assert.Equal(crowdfundApp.AppType, apps[1].AppType);
|
||||||
|
Assert.False(apps[1].Archived);
|
||||||
|
|
||||||
Assert.Equal(newApp.Name, apps[2].Name);
|
Assert.Equal(newApp.Name, apps[2].Name);
|
||||||
Assert.Equal(newApp.StoreId, apps[2].StoreId);
|
Assert.Equal(newApp.StoreId, apps[2].StoreId);
|
||||||
Assert.Equal(newApp.AppType, apps[2].AppType);
|
Assert.Equal(newApp.AppType, apps[2].AppType);
|
||||||
|
Assert.False(apps[2].Archived);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Timeout = TestTimeout)]
|
[Fact(Timeout = TestTimeout)]
|
||||||
@@ -876,7 +885,7 @@ namespace BTCPayServer.Tests
|
|||||||
|
|
||||||
TestLogs.LogInformation("Can't archive without knowing the walletId");
|
TestLogs.LogInformation("Can't archive without knowing the walletId");
|
||||||
var ex = await AssertAPIError("missing-permission", async () => await client.ArchivePullPayment("lol", result.Id));
|
var ex = await AssertAPIError("missing-permission", async () => await client.ArchivePullPayment("lol", result.Id));
|
||||||
Assert.Equal("btcpay.store.canmanagepullpayments", ((GreenfieldPermissionAPIError)ex.APIError).MissingPermission);
|
Assert.Equal("btcpay.store.canarchivepullpayments", ((GreenfieldPermissionAPIError)ex.APIError).MissingPermission);
|
||||||
TestLogs.LogInformation("Can't archive without permission");
|
TestLogs.LogInformation("Can't archive without permission");
|
||||||
await AssertAPIError("unauthenticated", async () => await unauthenticated.ArchivePullPayment(storeId, result.Id));
|
await AssertAPIError("unauthenticated", async () => await unauthenticated.ArchivePullPayment(storeId, result.Id));
|
||||||
await client.ArchivePullPayment(storeId, result.Id);
|
await client.ArchivePullPayment(storeId, result.Id);
|
||||||
@@ -1073,6 +1082,22 @@ namespace BTCPayServer.Tests
|
|||||||
var lnrURLs = await unauthenticated.GetPullPaymentLNURL(test4.Id);
|
var lnrURLs = await unauthenticated.GetPullPaymentLNURL(test4.Id);
|
||||||
Assert.IsType<string>(lnrURLs.LNURLBech32);
|
Assert.IsType<string>(lnrURLs.LNURLBech32);
|
||||||
Assert.IsType<string>(lnrURLs.LNURLUri);
|
Assert.IsType<string>(lnrURLs.LNURLUri);
|
||||||
|
Assert.Equal(12.303228134m, test4.Amount);
|
||||||
|
Assert.Equal("BTC", test4.Currency);
|
||||||
|
|
||||||
|
// Test with SATS denomination values
|
||||||
|
var testSats = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest()
|
||||||
|
{
|
||||||
|
Name = "Test SATS",
|
||||||
|
Amount = 21000,
|
||||||
|
Currency = "SATS",
|
||||||
|
PaymentMethods = new[] { "BTC", "BTC-LightningNetwork", "BTC_LightningLike" }
|
||||||
|
});
|
||||||
|
lnrURLs = await unauthenticated.GetPullPaymentLNURL(testSats.Id);
|
||||||
|
Assert.IsType<string>(lnrURLs.LNURLBech32);
|
||||||
|
Assert.IsType<string>(lnrURLs.LNURLUri);
|
||||||
|
Assert.Equal(21000, testSats.Amount);
|
||||||
|
Assert.Equal("SATS", testSats.Currency);
|
||||||
|
|
||||||
//permission test around auto approved pps and payouts
|
//permission test around auto approved pps and payouts
|
||||||
var nonApproved = await acc.CreateClient(Policies.CanCreateNonApprovedPullPayments);
|
var nonApproved = await acc.CreateClient(Policies.CanCreateNonApprovedPullPayments);
|
||||||
@@ -1134,7 +1159,8 @@ namespace BTCPayServer.Tests
|
|||||||
Approved = false,
|
Approved = false,
|
||||||
PaymentMethod = "BTC",
|
PaymentMethod = "BTC",
|
||||||
Amount = 0.0001m,
|
Amount = 0.0001m,
|
||||||
Destination = address.ToString()
|
Destination = address.ToString(),
|
||||||
|
|
||||||
});
|
});
|
||||||
await AssertAPIError("invalid-state", async () =>
|
await AssertAPIError("invalid-state", async () =>
|
||||||
{
|
{
|
||||||
@@ -1253,7 +1279,7 @@ namespace BTCPayServer.Tests
|
|||||||
using var tester = CreateServerTester();
|
using var tester = CreateServerTester();
|
||||||
await tester.StartAsync();
|
await tester.StartAsync();
|
||||||
var user = tester.NewAccount();
|
var user = tester.NewAccount();
|
||||||
user.GrantAccess();
|
await user.GrantAccessAsync();
|
||||||
await user.MakeAdmin();
|
await user.MakeAdmin();
|
||||||
var client = await user.CreateClient(Policies.Unrestricted);
|
var client = await user.CreateClient(Policies.Unrestricted);
|
||||||
|
|
||||||
@@ -1319,7 +1345,8 @@ namespace BTCPayServer.Tests
|
|||||||
// We strip the user's Owner right, so the key should not work
|
// We strip the user's Owner right, so the key should not work
|
||||||
using var ctx = tester.PayTester.GetService<Data.ApplicationDbContextFactory>().CreateContext();
|
using var ctx = tester.PayTester.GetService<Data.ApplicationDbContextFactory>().CreateContext();
|
||||||
var storeEntity = await ctx.UserStore.SingleAsync(u => u.ApplicationUserId == user.UserId && u.StoreDataId == newStore.Id);
|
var storeEntity = await ctx.UserStore.SingleAsync(u => u.ApplicationUserId == user.UserId && u.StoreDataId == newStore.Id);
|
||||||
storeEntity.Role = "Guest";
|
var roleId = (await tester.PayTester.GetService<StoreRepository>().GetStoreRoles(null)).Single(r => r.Role == "Guest").Id;
|
||||||
|
storeEntity.StoreRoleId = roleId;
|
||||||
await ctx.SaveChangesAsync();
|
await ctx.SaveChangesAsync();
|
||||||
await AssertHttpError(403, async () => await client.UpdateStore(newStore.Id, new UpdateStoreRequest() { Name = "B" }));
|
await AssertHttpError(403, async () => await client.UpdateStore(newStore.Id, new UpdateStoreRequest() { Name = "B" }));
|
||||||
|
|
||||||
@@ -1331,6 +1358,13 @@ namespace BTCPayServer.Tests
|
|||||||
}
|
}
|
||||||
tester.DeleteStore = false;
|
tester.DeleteStore = false;
|
||||||
Assert.Empty(await client.GetStores());
|
Assert.Empty(await client.GetStores());
|
||||||
|
|
||||||
|
// Archive
|
||||||
|
var archivableStore = await client.CreateStore(new CreateStoreRequest { Name = "Archivable" });
|
||||||
|
Assert.False(archivableStore.Archived);
|
||||||
|
archivableStore = await client.UpdateStore(archivableStore.Id, new UpdateStoreRequest { Name = "Archived", Archived = true });
|
||||||
|
Assert.Equal("Archived", archivableStore.Name);
|
||||||
|
Assert.True(archivableStore.Archived);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<GreenfieldValidationException> AssertValidationError(string[] fields, Func<Task> act)
|
private async Task<GreenfieldValidationException> AssertValidationError(string[] fields, Func<Task> act)
|
||||||
@@ -1430,7 +1464,7 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.False(hook.AutomaticRedelivery);
|
Assert.False(hook.AutomaticRedelivery);
|
||||||
Assert.Equal(fakeServer.ServerUri.AbsoluteUri, hook.Url);
|
Assert.Equal(fakeServer.ServerUri.AbsoluteUri, hook.Url);
|
||||||
}
|
}
|
||||||
using var tester = CreateServerTester();
|
using var tester = CreateServerTester(newDb: true);
|
||||||
using var fakeServer = new FakeServer();
|
using var fakeServer = new FakeServer();
|
||||||
await fakeServer.Start();
|
await fakeServer.Start();
|
||||||
await tester.StartAsync();
|
await tester.StartAsync();
|
||||||
@@ -1507,6 +1541,14 @@ namespace BTCPayServer.Tests
|
|||||||
clientProfile = await user.CreateClient(Policies.CanModifyStoreSettings, Policies.CanCreateInvoice);
|
clientProfile = await user.CreateClient(Policies.CanModifyStoreSettings, Policies.CanCreateInvoice);
|
||||||
await clientProfile.GetWebhookDeliveryRequest(user.StoreId, hook.Id, newDeliveryId);
|
await clientProfile.GetWebhookDeliveryRequest(user.StoreId, hook.Id, newDeliveryId);
|
||||||
|
|
||||||
|
|
||||||
|
TestLogs.LogInformation("Can prune deliveries");
|
||||||
|
var cleanup = tester.PayTester.GetService<HostedServices.CleanupWebhookDeliveriesTask>();
|
||||||
|
cleanup.BatchSize = 1;
|
||||||
|
cleanup.PruneAfter = TimeSpan.Zero;
|
||||||
|
await cleanup.Do(default);
|
||||||
|
await AssertHttpError(409, () => clientProfile.RedeliverWebhook(user.StoreId, hook.Id, delivery.Id));
|
||||||
|
|
||||||
TestLogs.LogInformation("Testing corner cases");
|
TestLogs.LogInformation("Testing corner cases");
|
||||||
Assert.Null(await clientProfile.GetWebhookDeliveryRequest(user.StoreId, "lol", newDeliveryId));
|
Assert.Null(await clientProfile.GetWebhookDeliveryRequest(user.StoreId, "lol", newDeliveryId));
|
||||||
Assert.Null(await clientProfile.GetWebhookDeliveryRequest(user.StoreId, hook.Id, "lol"));
|
Assert.Null(await clientProfile.GetWebhookDeliveryRequest(user.StoreId, hook.Id, "lol"));
|
||||||
@@ -1564,7 +1606,7 @@ namespace BTCPayServer.Tests
|
|||||||
using var tester = CreateServerTester();
|
using var tester = CreateServerTester();
|
||||||
await tester.StartAsync();
|
await tester.StartAsync();
|
||||||
var user = tester.NewAccount();
|
var user = tester.NewAccount();
|
||||||
user.GrantAccess();
|
await user.GrantAccessAsync();
|
||||||
await user.MakeAdmin();
|
await user.MakeAdmin();
|
||||||
var client = await user.CreateClient(Policies.Unrestricted);
|
var client = await user.CreateClient(Policies.Unrestricted);
|
||||||
var viewOnly = await user.CreateClient(Policies.CanViewPaymentRequests);
|
var viewOnly = await user.CreateClient(Policies.CanViewPaymentRequests);
|
||||||
@@ -1646,11 +1688,18 @@ namespace BTCPayServer.Tests
|
|||||||
BitcoinAddress.Create(invoice.BitcoinAddress, tester.ExplorerNode.Network), invoice.BtcDue);
|
BitcoinAddress.Create(invoice.BitcoinAddress, tester.ExplorerNode.Network), invoice.BtcDue);
|
||||||
});
|
});
|
||||||
await TestUtils.EventuallyAsync(async () =>
|
await TestUtils.EventuallyAsync(async () =>
|
||||||
{
|
{
|
||||||
Assert.Equal(Invoice.STATUS_PAID, user.BitPay.GetInvoice(invoiceId).Status);
|
Assert.Equal(Invoice.STATUS_PAID, (await user.BitPay.GetInvoiceAsync(invoiceId)).Status);
|
||||||
if (!partialPayment)
|
if (!partialPayment)
|
||||||
Assert.Equal(PaymentRequestData.PaymentRequestStatus.Completed, (await client.GetPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id)).Status);
|
Assert.Equal(PaymentRequestData.PaymentRequestStatus.Processing, (await client.GetPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id)).Status);
|
||||||
});
|
});
|
||||||
|
await tester.ExplorerNode.GenerateAsync(1);
|
||||||
|
await TestUtils.EventuallyAsync(async () =>
|
||||||
|
{
|
||||||
|
Assert.Equal(Invoice.STATUS_CONFIRMED, (await user.BitPay.GetInvoiceAsync(invoiceId)).Status);
|
||||||
|
if (!partialPayment)
|
||||||
|
Assert.Equal(PaymentRequestData.PaymentRequestStatus.Completed, (await client.GetPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id)).Status);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
await Pay(invoiceId);
|
await Pay(invoiceId);
|
||||||
|
|
||||||
@@ -1950,6 +1999,82 @@ namespace BTCPayServer.Tests
|
|||||||
CustomCurrency = "BTC"
|
CustomCurrency = "BTC"
|
||||||
});
|
});
|
||||||
Assert.True(pp.AutoApproveClaims);
|
Assert.True(pp.AutoApproveClaims);
|
||||||
|
|
||||||
|
// test subtract percentage
|
||||||
|
validationError = await AssertValidationError(new[] { "SubtractPercentage" }, async () =>
|
||||||
|
{
|
||||||
|
await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
|
||||||
|
{
|
||||||
|
PaymentMethod = method.PaymentMethod,
|
||||||
|
RefundVariant = RefundVariant.RateThen,
|
||||||
|
SubtractPercentage = 101
|
||||||
|
});
|
||||||
|
});
|
||||||
|
Assert.Contains("SubtractPercentage: Percentage must be a numeric value between 0 and 100", validationError.Message);
|
||||||
|
|
||||||
|
// should auto-approve
|
||||||
|
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
|
||||||
|
{
|
||||||
|
PaymentMethod = method.PaymentMethod,
|
||||||
|
RefundVariant = RefundVariant.RateThen,
|
||||||
|
SubtractPercentage = 6.15m
|
||||||
|
});
|
||||||
|
Assert.Equal("BTC", pp.Currency);
|
||||||
|
Assert.True(pp.AutoApproveClaims);
|
||||||
|
Assert.Equal(0.9385m, pp.Amount);
|
||||||
|
|
||||||
|
// test RefundVariant.OverpaidAmount
|
||||||
|
validationError = await AssertValidationError(new[] { "RefundVariant" }, async () =>
|
||||||
|
{
|
||||||
|
await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
|
||||||
|
{
|
||||||
|
PaymentMethod = method.PaymentMethod,
|
||||||
|
RefundVariant = RefundVariant.OverpaidAmount
|
||||||
|
});
|
||||||
|
});
|
||||||
|
Assert.Contains("Invoice is not overpaid", validationError.Message);
|
||||||
|
|
||||||
|
// should auto-approve
|
||||||
|
invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest { Amount = 5000.0m, Currency = "USD" });
|
||||||
|
methods = await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id);
|
||||||
|
method = methods.First();
|
||||||
|
|
||||||
|
await tester.WaitForEvent<NewOnChainTransactionEvent>(async () =>
|
||||||
|
{
|
||||||
|
await tester.ExplorerNode.SendToAddressAsync(
|
||||||
|
BitcoinAddress.Create(method.Destination, tester.NetworkProvider.BTC.NBitcoinNetwork),
|
||||||
|
Money.Coins(method.Due * 2)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await tester.ExplorerNode.GenerateAsync(5);
|
||||||
|
|
||||||
|
await TestUtils.EventuallyAsync(async () =>
|
||||||
|
{
|
||||||
|
invoice = await client.GetInvoice(user.StoreId, invoice.Id);
|
||||||
|
Assert.True(invoice.Status == InvoiceStatus.Settled);
|
||||||
|
Assert.True(invoice.AdditionalStatus == InvoiceExceptionStatus.PaidOver);
|
||||||
|
});
|
||||||
|
|
||||||
|
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
|
||||||
|
{
|
||||||
|
PaymentMethod = method.PaymentMethod,
|
||||||
|
RefundVariant = RefundVariant.OverpaidAmount
|
||||||
|
});
|
||||||
|
Assert.Equal("BTC", pp.Currency);
|
||||||
|
Assert.True(pp.AutoApproveClaims);
|
||||||
|
Assert.Equal(method.Due, pp.Amount);
|
||||||
|
|
||||||
|
// once more with subtract percentage
|
||||||
|
pp = await client.RefundInvoice(user.StoreId, invoice.Id, new RefundInvoiceRequest
|
||||||
|
{
|
||||||
|
PaymentMethod = method.PaymentMethod,
|
||||||
|
RefundVariant = RefundVariant.OverpaidAmount,
|
||||||
|
SubtractPercentage = 21m
|
||||||
|
});
|
||||||
|
Assert.Equal("BTC", pp.Currency);
|
||||||
|
Assert.True(pp.AutoApproveClaims);
|
||||||
|
Assert.Equal(0.79m, pp.Amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Timeout = TestTimeout)]
|
[Fact(Timeout = TestTimeout)]
|
||||||
@@ -2257,7 +2382,7 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.Single(paymentMethods);
|
Assert.Single(paymentMethods);
|
||||||
Assert.True(paymentMethods.First().Activated);
|
Assert.True(paymentMethods.First().Activated);
|
||||||
|
|
||||||
var invoiceWithdefaultPaymentMethodLN = await client.CreateInvoice(user.StoreId,
|
var invoiceWithDefaultPaymentMethodLN = await client.CreateInvoice(user.StoreId,
|
||||||
new CreateInvoiceRequest()
|
new CreateInvoiceRequest()
|
||||||
{
|
{
|
||||||
Currency = "USD",
|
Currency = "USD",
|
||||||
@@ -2268,9 +2393,9 @@ namespace BTCPayServer.Tests
|
|||||||
DefaultPaymentMethod = "BTC_LightningLike"
|
DefaultPaymentMethod = "BTC_LightningLike"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Assert.Equal("BTC_LightningLike", invoiceWithdefaultPaymentMethodLN.Checkout.DefaultPaymentMethod);
|
Assert.Equal("BTC_LightningLike", invoiceWithDefaultPaymentMethodLN.Checkout.DefaultPaymentMethod);
|
||||||
|
|
||||||
var invoiceWithdefaultPaymentMethodOnChain = await client.CreateInvoice(user.StoreId,
|
var invoiceWithDefaultPaymentMethodOnChain = await client.CreateInvoice(user.StoreId,
|
||||||
new CreateInvoiceRequest()
|
new CreateInvoiceRequest()
|
||||||
{
|
{
|
||||||
Currency = "USD",
|
Currency = "USD",
|
||||||
@@ -2281,13 +2406,35 @@ namespace BTCPayServer.Tests
|
|||||||
DefaultPaymentMethod = "BTC"
|
DefaultPaymentMethod = "BTC"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Assert.Equal("BTC", invoiceWithdefaultPaymentMethodOnChain.Checkout.DefaultPaymentMethod);
|
Assert.Equal("BTC", invoiceWithDefaultPaymentMethodOnChain.Checkout.DefaultPaymentMethod);
|
||||||
|
|
||||||
|
// reset lazy payment methods
|
||||||
store = await client.GetStore(user.StoreId);
|
store = await client.GetStore(user.StoreId);
|
||||||
store.LazyPaymentMethods = false;
|
store.LazyPaymentMethods = false;
|
||||||
store = await client.UpdateStore(store.Id,
|
store = await client.UpdateStore(store.Id,
|
||||||
JObject.FromObject(store).ToObject<UpdateStoreRequest>());
|
JObject.FromObject(store).ToObject<UpdateStoreRequest>());
|
||||||
|
Assert.False(store.LazyPaymentMethods);
|
||||||
|
|
||||||
|
// use store default payment method
|
||||||
|
store = await client.GetStore(user.StoreId);
|
||||||
|
Assert.Null(store.DefaultPaymentMethod);
|
||||||
|
var storeDefaultPaymentMethod = "BTC-LightningNetwork";
|
||||||
|
store.DefaultPaymentMethod = storeDefaultPaymentMethod;
|
||||||
|
store = await client.UpdateStore(store.Id,
|
||||||
|
JObject.FromObject(store).ToObject<UpdateStoreRequest>());
|
||||||
|
Assert.Equal(storeDefaultPaymentMethod, store.DefaultPaymentMethod);
|
||||||
|
|
||||||
|
var invoiceWithStoreDefaultPaymentMethod = await client.CreateInvoice(user.StoreId,
|
||||||
|
new CreateInvoiceRequest()
|
||||||
|
{
|
||||||
|
Currency = "USD",
|
||||||
|
Amount = 100,
|
||||||
|
Checkout = new CreateInvoiceRequest.CheckoutOptions()
|
||||||
|
{
|
||||||
|
PaymentMethods = new[] { "BTC", "BTC-LightningNetwork", "BTC_LightningLike" }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Assert.Equal(storeDefaultPaymentMethod, invoiceWithStoreDefaultPaymentMethod.Checkout.DefaultPaymentMethod);
|
||||||
|
|
||||||
//let's see the overdue amount
|
//let's see the overdue amount
|
||||||
invoice = await client.CreateInvoice(user.StoreId,
|
invoice = await client.CreateInvoice(user.StoreId,
|
||||||
@@ -2344,27 +2491,10 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.NotNull(merchantInvoice.PaymentHash);
|
Assert.NotNull(merchantInvoice.PaymentHash);
|
||||||
Assert.Equal(merchantInvoice.Id, merchantInvoice.PaymentHash);
|
Assert.Equal(merchantInvoice.Id, merchantInvoice.PaymentHash);
|
||||||
|
|
||||||
// The default client is using charge, so we should not be able to query channels
|
var client = await user.CreateClient(Policies.CanUseInternalLightningNode);
|
||||||
var chargeClient = await user.CreateClient(Policies.CanUseInternalLightningNode);
|
|
||||||
|
|
||||||
var info = await chargeClient.GetLightningNodeInfo("BTC");
|
|
||||||
Assert.Single(info.NodeURIs);
|
|
||||||
Assert.NotEqual(0, info.BlockHeight);
|
|
||||||
Assert.NotNull(info.Alias);
|
|
||||||
Assert.NotNull(info.Color);
|
|
||||||
Assert.NotNull(info.Version);
|
|
||||||
Assert.NotNull(info.PeersCount);
|
|
||||||
Assert.NotNull(info.ActiveChannelsCount);
|
|
||||||
Assert.NotNull(info.InactiveChannelsCount);
|
|
||||||
Assert.NotNull(info.PendingChannelsCount);
|
|
||||||
|
|
||||||
var gex = await AssertAPIError("lightning-node-unavailable", () => chargeClient.ConnectToLightningNode("BTC", new ConnectToNodeRequest(NodeInfo.Parse($"{new Key().PubKey.ToHex()}@localhost:3827"))));
|
|
||||||
Assert.Contains("NotSupported", gex.Message);
|
|
||||||
|
|
||||||
await AssertAPIError("lightning-node-unavailable", () => chargeClient.GetLightningNodeChannels("BTC"));
|
|
||||||
// Not permission for the store!
|
// Not permission for the store!
|
||||||
await AssertAPIError("missing-permission", () => chargeClient.GetLightningNodeChannels(user.StoreId, "BTC"));
|
await AssertAPIError("missing-permission", () => client.GetLightningNodeChannels(user.StoreId, "BTC"));
|
||||||
var invoiceData = await chargeClient.CreateLightningInvoice("BTC", new CreateLightningInvoiceRequest()
|
var invoiceData = await client.CreateLightningInvoice("BTC", new CreateLightningInvoiceRequest()
|
||||||
{
|
{
|
||||||
Amount = LightMoney.Satoshis(1000),
|
Amount = LightMoney.Satoshis(1000),
|
||||||
Description = "lol",
|
Description = "lol",
|
||||||
@@ -2372,17 +2502,17 @@ namespace BTCPayServer.Tests
|
|||||||
PrivateRouteHints = false
|
PrivateRouteHints = false
|
||||||
});
|
});
|
||||||
var chargeInvoice = invoiceData;
|
var chargeInvoice = invoiceData;
|
||||||
Assert.NotNull(await chargeClient.GetLightningInvoice("BTC", invoiceData.Id));
|
Assert.NotNull(await client.GetLightningInvoice("BTC", invoiceData.Id));
|
||||||
|
|
||||||
// check list for internal node
|
// check list for internal node
|
||||||
var invoices = await chargeClient.GetLightningInvoices("BTC");
|
var invoices = await client.GetLightningInvoices("BTC");
|
||||||
var pendingInvoices = await chargeClient.GetLightningInvoices("BTC", true);
|
var pendingInvoices = await client.GetLightningInvoices("BTC", true);
|
||||||
Assert.NotEmpty(invoices);
|
Assert.NotEmpty(invoices);
|
||||||
Assert.Contains(invoices, i => i.Id == invoiceData.Id);
|
Assert.Contains(invoices, i => i.Id == invoiceData.Id);
|
||||||
Assert.NotEmpty(pendingInvoices);
|
Assert.NotEmpty(pendingInvoices);
|
||||||
Assert.Contains(pendingInvoices, i => i.Id == invoiceData.Id);
|
Assert.Contains(pendingInvoices, i => i.Id == invoiceData.Id);
|
||||||
|
|
||||||
var client = await user.CreateClient($"{Policies.CanUseLightningNodeInStore}:{user.StoreId}");
|
client = await user.CreateClient($"{Policies.CanUseLightningNodeInStore}:{user.StoreId}");
|
||||||
// Not permission for the server
|
// Not permission for the server
|
||||||
await AssertAPIError("missing-permission", () => client.GetLightningNodeChannels("BTC"));
|
await AssertAPIError("missing-permission", () => client.GetLightningNodeChannels("BTC"));
|
||||||
|
|
||||||
@@ -2461,7 +2591,7 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.Contains(payments, i => i.BOLT11 == merchantInvoice.BOLT11);
|
Assert.Contains(payments, i => i.BOLT11 == merchantInvoice.BOLT11);
|
||||||
|
|
||||||
// Node info
|
// Node info
|
||||||
info = await client.GetLightningNodeInfo(user.StoreId, "BTC");
|
var info = await client.GetLightningNodeInfo(user.StoreId, "BTC");
|
||||||
Assert.Single(info.NodeURIs);
|
Assert.Single(info.NodeURIs);
|
||||||
Assert.NotEqual(0, info.BlockHeight);
|
Assert.NotEqual(0, info.BlockHeight);
|
||||||
|
|
||||||
@@ -2502,7 +2632,12 @@ namespace BTCPayServer.Tests
|
|||||||
user.RegisterLightningNode("BTC", LightningConnectionType.CLightning);
|
user.RegisterLightningNode("BTC", LightningConnectionType.CLightning);
|
||||||
|
|
||||||
var client = await user.CreateClient(Policies.Unrestricted);
|
var client = await user.CreateClient(Policies.Unrestricted);
|
||||||
var invoice = await client.CreateInvoice(user.StoreId,
|
var invoices = new Task<Client.Models.InvoiceData>[5];
|
||||||
|
|
||||||
|
// Create invoices
|
||||||
|
for (int i = 0; i < invoices.Length; i++)
|
||||||
|
{
|
||||||
|
invoices[i] = client.CreateInvoice(user.StoreId,
|
||||||
new CreateInvoiceRequest
|
new CreateInvoiceRequest
|
||||||
{
|
{
|
||||||
Currency = "USD",
|
Currency = "USD",
|
||||||
@@ -2513,18 +2648,35 @@ namespace BTCPayServer.Tests
|
|||||||
DefaultPaymentMethod = "BTC_LightningLike"
|
DefaultPaymentMethod = "BTC_LightningLike"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
var pm = Assert.Single(await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id));
|
}
|
||||||
Assert.False(pm.AdditionalData.HasValues);
|
|
||||||
|
|
||||||
var resp = await tester.CustomerLightningD.Pay(pm.Destination);
|
var pm = new InvoicePaymentMethodDataModel[invoices.Length];
|
||||||
Assert.Equal(PayResult.Ok, resp.Result);
|
for (int i = 0; i < invoices.Length; i++)
|
||||||
Assert.NotNull(resp.Details.PaymentHash);
|
{
|
||||||
Assert.NotNull(resp.Details.Preimage);
|
pm[i] = Assert.Single(await client.GetInvoicePaymentMethods(user.StoreId, (await invoices[i]).Id));
|
||||||
|
Assert.True(pm[i].AdditionalData.HasValues);
|
||||||
|
}
|
||||||
|
|
||||||
pm = Assert.Single(await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id));
|
// Pay them all at once
|
||||||
Assert.True(pm.AdditionalData.HasValues);
|
Task<PayResponse>[] payResponses = new Task<PayResponse>[invoices.Length];
|
||||||
Assert.Equal(resp.Details.PaymentHash.ToString(), pm.AdditionalData.GetValue("paymentHash"));
|
for (int i = 0; i < invoices.Length; i++)
|
||||||
Assert.Equal(resp.Details.Preimage.ToString(), pm.AdditionalData.GetValue("preimage"));
|
{
|
||||||
|
payResponses[i] = tester.CustomerLightningD.Pay(pm[i].Destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checking the results
|
||||||
|
for (int i = 0; i < invoices.Length; i++)
|
||||||
|
{
|
||||||
|
var resp = await payResponses[i];
|
||||||
|
Assert.Equal(PayResult.Ok, resp.Result);
|
||||||
|
Assert.NotNull(resp.Details.PaymentHash);
|
||||||
|
Assert.NotNull(resp.Details.Preimage);
|
||||||
|
|
||||||
|
pm[i] = Assert.Single(await client.GetInvoicePaymentMethods(user.StoreId, (await invoices[i]).Id));
|
||||||
|
Assert.True(pm[i].AdditionalData.HasValues);
|
||||||
|
Assert.Equal(resp.Details.PaymentHash.ToString(), pm[i].AdditionalData.GetValue("paymentHash"));
|
||||||
|
Assert.Equal(resp.Details.Preimage.ToString(), pm[i].AdditionalData.GetValue("preimage"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Timeout = 60 * 20 * 1000)]
|
[Fact(Timeout = 60 * 20 * 1000)]
|
||||||
@@ -3081,6 +3233,9 @@ namespace BTCPayServer.Tests
|
|||||||
});
|
});
|
||||||
var transaction = await client.GetOnChainWalletTransaction(walletId.StoreId, walletId.CryptoCode, txdata.TransactionHash.ToString());
|
var transaction = await client.GetOnChainWalletTransaction(walletId.StoreId, walletId.CryptoCode, txdata.TransactionHash.ToString());
|
||||||
|
|
||||||
|
// Check skip doesn't crash
|
||||||
|
await client.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode, skip: 1);
|
||||||
|
|
||||||
Assert.Equal(transaction.TransactionHash, txdata.TransactionHash);
|
Assert.Equal(transaction.TransactionHash, txdata.TransactionHash);
|
||||||
Assert.Equal(String.Empty, transaction.Comment);
|
Assert.Equal(String.Empty, transaction.Comment);
|
||||||
#pragma warning disable CS0612 // Type or member is obsolete
|
#pragma warning disable CS0612 // Type or member is obsolete
|
||||||
@@ -3262,11 +3417,16 @@ namespace BTCPayServer.Tests
|
|||||||
|
|
||||||
var client = await user.CreateClient(Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings);
|
var client = await user.CreateClient(Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings);
|
||||||
|
|
||||||
|
var roles = await client.GetServerRoles();
|
||||||
|
Assert.Equal(2,roles.Count);
|
||||||
|
#pragma warning disable CS0618
|
||||||
|
var ownerRole = roles.Single(data => data.Role == StoreRoles.Owner);
|
||||||
|
var guestRole = roles.Single(data => data.Role == StoreRoles.Guest);
|
||||||
|
#pragma warning restore CS0618
|
||||||
var users = await client.GetStoreUsers(user.StoreId);
|
var users = await client.GetStoreUsers(user.StoreId);
|
||||||
var storeuser = Assert.Single(users);
|
var storeuser = Assert.Single(users);
|
||||||
Assert.Equal(user.UserId, storeuser.UserId);
|
Assert.Equal(user.UserId, storeuser.UserId);
|
||||||
Assert.Equal(StoreRoles.Owner, storeuser.Role);
|
Assert.Equal(ownerRole.Id, storeuser.Role);
|
||||||
|
|
||||||
var user2 = tester.NewAccount();
|
var user2 = tester.NewAccount();
|
||||||
await user2.GrantAccessAsync(false);
|
await user2.GrantAccessAsync(false);
|
||||||
|
|
||||||
@@ -3277,7 +3437,7 @@ namespace BTCPayServer.Tests
|
|||||||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.AddStoreUser(user.StoreId, new StoreUserData()));
|
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 AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.RemoveStoreUser(user.StoreId, user.UserId));
|
||||||
|
|
||||||
await client.AddStoreUser(user.StoreId, new StoreUserData() { Role = StoreRoles.Guest, UserId = user2.UserId });
|
await client.AddStoreUser(user.StoreId, new StoreUserData() { Role = guestRole.Id, UserId = user2.UserId });
|
||||||
|
|
||||||
//test no access to api when only a guest
|
//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.GetStoreUsers(user.StoreId));
|
||||||
@@ -3291,10 +3451,10 @@ namespace BTCPayServer.Tests
|
|||||||
await user2Client.GetStore(user.StoreId));
|
await user2Client.GetStore(user.StoreId));
|
||||||
|
|
||||||
|
|
||||||
await client.AddStoreUser(user.StoreId, new StoreUserData() { Role = StoreRoles.Owner, UserId = user2.UserId });
|
await client.AddStoreUser(user.StoreId, new StoreUserData() { Role = ownerRole.Id, UserId = user2.UserId });
|
||||||
await AssertAPIError("duplicate-store-user-role", async () =>
|
await AssertAPIError("duplicate-store-user-role", async () =>
|
||||||
await client.AddStoreUser(user.StoreId,
|
await client.AddStoreUser(user.StoreId,
|
||||||
new StoreUserData() { Role = StoreRoles.Owner, UserId = user2.UserId }));
|
new StoreUserData() { Role = ownerRole.Id, UserId = user2.UserId }));
|
||||||
await user2Client.RemoveStoreUser(user.StoreId, user.UserId);
|
await user2Client.RemoveStoreUser(user.StoreId, user.UserId);
|
||||||
|
|
||||||
|
|
||||||
@@ -3410,6 +3570,7 @@ namespace BTCPayServer.Tests
|
|||||||
PaymentMethod = "BTC_LightningNetwork",
|
PaymentMethod = "BTC_LightningNetwork",
|
||||||
Destination = customerInvoice.BOLT11
|
Destination = customerInvoice.BOLT11
|
||||||
});
|
});
|
||||||
|
Assert.Equal(payout.Metadata.ToString(), new JObject().ToString()); //empty
|
||||||
Assert.Empty(await adminClient.GetStoreLightningAutomatedPayoutProcessors(admin.StoreId, "BTC_LightningNetwork"));
|
Assert.Empty(await adminClient.GetStoreLightningAutomatedPayoutProcessors(admin.StoreId, "BTC_LightningNetwork"));
|
||||||
await adminClient.UpdateStoreLightningAutomatedPayoutProcessors(admin.StoreId, "BTC_LightningNetwork",
|
await adminClient.UpdateStoreLightningAutomatedPayoutProcessors(admin.StoreId, "BTC_LightningNetwork",
|
||||||
new LightningAutomatedPayoutSettings() { IntervalSeconds = TimeSpan.FromSeconds(600) });
|
new LightningAutomatedPayoutSettings() { IntervalSeconds = TimeSpan.FromSeconds(600) });
|
||||||
@@ -3420,6 +3581,46 @@ namespace BTCPayServer.Tests
|
|||||||
(await adminClient.GetStorePayouts(admin.StoreId, false)).Single(data => data.Id == payout.Id);
|
(await adminClient.GetStorePayouts(admin.StoreId, false)).Single(data => data.Id == payout.Id);
|
||||||
Assert.Equal(PayoutState.Completed, payoutC.State);
|
Assert.Equal(PayoutState.Completed, payoutC.State);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
payout = await adminClient.CreatePayout(admin.StoreId,
|
||||||
|
new CreatePayoutThroughStoreRequest()
|
||||||
|
{
|
||||||
|
Approved = true,
|
||||||
|
PaymentMethod = "BTC",
|
||||||
|
Destination = (await tester.ExplorerNode.GetNewAddressAsync()).ToString(),
|
||||||
|
Amount = 0.0001m,
|
||||||
|
Metadata = JObject.FromObject(new
|
||||||
|
{
|
||||||
|
source ="apitest",
|
||||||
|
sourceLink = "https://chocolate.com"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
Assert.Equal(payout.Metadata.ToString(), JObject.FromObject(new
|
||||||
|
{
|
||||||
|
source = "apitest",
|
||||||
|
sourceLink = "https://chocolate.com"
|
||||||
|
}).ToString());
|
||||||
|
|
||||||
|
payout =
|
||||||
|
(await adminClient.GetStorePayouts(admin.StoreId, false)).Single(data => data.Id == payout.Id);
|
||||||
|
|
||||||
|
Assert.Equal(payout.Metadata.ToString(), JObject.FromObject(new
|
||||||
|
{
|
||||||
|
source = "apitest",
|
||||||
|
sourceLink = "https://chocolate.com"
|
||||||
|
}).ToString());
|
||||||
|
|
||||||
|
customerInvoice = await tester.CustomerLightningD.CreateInvoice(LightMoney.FromUnit(10, LightMoneyUnit.Satoshi),
|
||||||
|
Guid.NewGuid().ToString(), TimeSpan.FromDays(40));
|
||||||
|
var payout2 = await adminClient.CreatePayout(admin.StoreId,
|
||||||
|
new CreatePayoutThroughStoreRequest()
|
||||||
|
{
|
||||||
|
Approved = true,
|
||||||
|
Amount = new Money(100, MoneyUnit.Satoshi).ToDecimal(MoneyUnit.BTC),
|
||||||
|
PaymentMethod = "BTC_LightningNetwork",
|
||||||
|
Destination = customerInvoice.BOLT11
|
||||||
|
});
|
||||||
|
Assert.Equal(payout2.Amount, new Money(100, MoneyUnit.Satoshi).ToDecimal(MoneyUnit.BTC));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Timeout = 60 * 2 * 1000)]
|
[Fact(Timeout = 60 * 2 * 1000)]
|
||||||
@@ -3535,9 +3736,12 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.Single(payouts.Where(data => data.State == PayoutState.InProgress));
|
Assert.Single(payouts.Where(data => data.State == PayoutState.InProgress));
|
||||||
});
|
});
|
||||||
|
|
||||||
var txid = await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
|
uint256 txid = null;
|
||||||
tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(0.01m) + fee);
|
await tester.WaitForEvent<NewOnChainTransactionEvent>(async () =>
|
||||||
await tester.WaitForEvent<NewOnChainTransactionEvent>(null, correctEvent: ev => ev.NewTransactionEvent.TransactionData.TransactionHash == txid);
|
{
|
||||||
|
txid = await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
|
||||||
|
tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(0.01m) + fee);
|
||||||
|
}, correctEvent: ev => ev.NewTransactionEvent.TransactionData.TransactionHash == txid);
|
||||||
await tester.PayTester.GetService<PayoutProcessorService>().Restart(new PayoutProcessorService.PayoutProcessorQuery(admin.StoreId, "BTC"));
|
await tester.PayTester.GetService<PayoutProcessorService>().Restart(new PayoutProcessorService.PayoutProcessorQuery(admin.StoreId, "BTC"));
|
||||||
await TestUtils.EventuallyAsync(async () =>
|
await TestUtils.EventuallyAsync(async () =>
|
||||||
{
|
{
|
||||||
@@ -3545,6 +3749,122 @@ namespace BTCPayServer.Tests
|
|||||||
payouts = await adminClient.GetStorePayouts(admin.StoreId);
|
payouts = await adminClient.GetStorePayouts(admin.StoreId);
|
||||||
Assert.Empty(payouts.Where(data => data.State != PayoutState.InProgress));
|
Assert.Empty(payouts.Where(data => data.State != PayoutState.InProgress));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// settings that were added later
|
||||||
|
var settings =
|
||||||
|
Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC"));
|
||||||
|
Assert.False( settings.ProcessNewPayoutsInstantly);
|
||||||
|
Assert.Equal(0m, settings.Threshold);
|
||||||
|
|
||||||
|
//let's use the ProcessNewPayoutsInstantly so that it will trigger instantly
|
||||||
|
|
||||||
|
settings.IntervalSeconds = TimeSpan.FromDays(1);
|
||||||
|
settings.ProcessNewPayoutsInstantly = true;
|
||||||
|
|
||||||
|
await tester.WaitForEvent<NewOnChainTransactionEvent>(async () =>
|
||||||
|
{
|
||||||
|
txid = await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
|
||||||
|
tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(1m) + fee);
|
||||||
|
}, correctEvent: ev => ev.NewTransactionEvent.TransactionData.TransactionHash == txid);
|
||||||
|
|
||||||
|
await adminClient.UpdateStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC", settings);
|
||||||
|
settings =
|
||||||
|
Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC"));
|
||||||
|
Assert.True( settings.ProcessNewPayoutsInstantly);
|
||||||
|
|
||||||
|
var pluginHookService = tester.PayTester.GetService<IPluginHookService>();
|
||||||
|
var beforeHookTcs = new TaskCompletionSource();
|
||||||
|
var afterHookTcs = new TaskCompletionSource();
|
||||||
|
pluginHookService.ActionInvoked += (sender, tuple) =>
|
||||||
|
{
|
||||||
|
switch (tuple.hook)
|
||||||
|
{
|
||||||
|
case "before-automated-payout-processing":
|
||||||
|
beforeHookTcs.TrySetResult();
|
||||||
|
break;
|
||||||
|
case "after-automated-payout-processing":
|
||||||
|
afterHookTcs.TrySetResult();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var payoutThatShouldBeProcessedStraightAway = await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest()
|
||||||
|
{
|
||||||
|
PullPaymentId = pullPayment.Id,
|
||||||
|
Amount = 0.5m,
|
||||||
|
Approved = true,
|
||||||
|
PaymentMethod = "BTC",
|
||||||
|
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
|
||||||
|
});
|
||||||
|
await beforeHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||||
|
await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||||
|
payouts = await adminClient.GetStorePayouts(admin.StoreId);
|
||||||
|
Assert.Single(payouts.Where(data => data.State == PayoutState.InProgress && data.Id == payoutThatShouldBeProcessedStraightAway.Id));
|
||||||
|
|
||||||
|
beforeHookTcs = new TaskCompletionSource();
|
||||||
|
afterHookTcs = new TaskCompletionSource();
|
||||||
|
//let's test the threshold limiter
|
||||||
|
settings.Threshold = 0.5m;
|
||||||
|
await adminClient.UpdateStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC", settings);
|
||||||
|
|
||||||
|
//quick test: when updating processor, it processes instantly
|
||||||
|
await beforeHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||||
|
await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||||
|
|
||||||
|
settings =
|
||||||
|
Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC"));
|
||||||
|
Assert.Equal(0.5m, settings.Threshold);
|
||||||
|
|
||||||
|
//create a payout that should not be processed straight away due to threshold
|
||||||
|
|
||||||
|
beforeHookTcs = new TaskCompletionSource();
|
||||||
|
afterHookTcs = new TaskCompletionSource();
|
||||||
|
var payoutThatShouldNotBeProcessedStraightAway = await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest()
|
||||||
|
{
|
||||||
|
Amount = 0.1m,
|
||||||
|
Approved = true,
|
||||||
|
PaymentMethod = "BTC",
|
||||||
|
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
|
||||||
|
});
|
||||||
|
|
||||||
|
await beforeHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||||
|
await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||||
|
|
||||||
|
payouts = await adminClient.GetStorePayouts(admin.StoreId);
|
||||||
|
Assert.Single(payouts.Where(data => data.State == PayoutState.AwaitingPayment && data.Id == payoutThatShouldNotBeProcessedStraightAway.Id));
|
||||||
|
|
||||||
|
beforeHookTcs = new TaskCompletionSource();
|
||||||
|
afterHookTcs = new TaskCompletionSource();
|
||||||
|
var payoutThatShouldNotBeProcessedStraightAway2 = await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest()
|
||||||
|
{
|
||||||
|
Amount = 0.3m,
|
||||||
|
Approved = true,
|
||||||
|
PaymentMethod = "BTC",
|
||||||
|
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
|
||||||
|
});
|
||||||
|
|
||||||
|
await beforeHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||||
|
await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||||
|
|
||||||
|
payouts = await adminClient.GetStorePayouts(admin.StoreId);
|
||||||
|
Assert.Equal(2, payouts.Count(data => data.State == PayoutState.AwaitingPayment &&
|
||||||
|
(data.Id == payoutThatShouldNotBeProcessedStraightAway.Id || data.Id == payoutThatShouldNotBeProcessedStraightAway2.Id)));
|
||||||
|
|
||||||
|
beforeHookTcs = new TaskCompletionSource();
|
||||||
|
afterHookTcs = new TaskCompletionSource();
|
||||||
|
var payoutThatShouldNotBeProcessedStraightAway3 = await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest()
|
||||||
|
{
|
||||||
|
Amount = 0.3m,
|
||||||
|
Approved = true,
|
||||||
|
PaymentMethod = "BTC",
|
||||||
|
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
|
||||||
|
});
|
||||||
|
|
||||||
|
await beforeHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||||
|
await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||||
|
|
||||||
|
payouts = await adminClient.GetStorePayouts(admin.StoreId);
|
||||||
|
Assert.Empty(payouts.Where(data => data.State != PayoutState.InProgress));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Timeout = 60 * 2 * 1000)]
|
[Fact(Timeout = 60 * 2 * 1000)]
|
||||||
|
@@ -1,10 +1,14 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Client;
|
||||||
using BTCPayServer.Controllers;
|
using BTCPayServer.Controllers;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.Hosting;
|
||||||
using BTCPayServer.Models.AppViewModels;
|
using BTCPayServer.Models.AppViewModels;
|
||||||
|
using BTCPayServer.Plugins.Crowdfund.Models;
|
||||||
using BTCPayServer.Plugins.PointOfSale;
|
using BTCPayServer.Plugins.PointOfSale;
|
||||||
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||||
|
using BTCPayServer.Services.Apps;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
@@ -19,6 +23,74 @@ namespace BTCPayServer.Tests
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Fast", "Fast")]
|
||||||
|
public void CanParseOldYmlCorrectly()
|
||||||
|
{
|
||||||
|
var testOriginalDefaultYmlTemplate = @"
|
||||||
|
green tea:
|
||||||
|
price: 1
|
||||||
|
title: Green Tea
|
||||||
|
description: Lovely, fresh and tender, Meng Ding Gan Lu ('sweet dew') is grown in the lush Meng Ding Mountains of the southwestern province of Sichuan where it has been cultivated for over a thousand years.
|
||||||
|
image: ~/img/pos-sample/green-tea.jpg
|
||||||
|
|
||||||
|
black tea:
|
||||||
|
price: 1
|
||||||
|
title: Black Tea
|
||||||
|
description: Tian Jian Tian Jian means 'heavenly tippy tea' in Chinese, and it describes the finest grade of dark tea. Our Tian Jian dark tea is from Hunan province which is famous for making some of the best dark teas available.
|
||||||
|
image: ~/img/pos-sample/black-tea.jpg
|
||||||
|
|
||||||
|
rooibos:
|
||||||
|
price: 1.2
|
||||||
|
title: Rooibos
|
||||||
|
description: Rooibos is a dramatic red tea made from a South African herb that contains polyphenols and flavonoids. Often called 'African redbush tea', Rooibos herbal tea delights the senses and delivers potential health benefits with each caffeine-free sip.
|
||||||
|
image: ~/img/pos-sample/rooibos.jpg
|
||||||
|
|
||||||
|
pu erh:
|
||||||
|
price: 2
|
||||||
|
title: Pu Erh
|
||||||
|
description: This loose pur-erh tea is produced in Yunnan Province, China. The process in a relatively high humidity environment has mellowed the elemental character of the tea when compared to young Pu-erh.
|
||||||
|
image: ~/img/pos-sample/pu-erh.jpg
|
||||||
|
|
||||||
|
herbal tea:
|
||||||
|
price: 1.8
|
||||||
|
title: Herbal Tea
|
||||||
|
description: Chamomile tea is made from the flower heads of the chamomile plant. The medicinal use of chamomile dates back to the ancient Egyptians, Romans and Greeks. Pay us what you want!
|
||||||
|
image: ~/img/pos-sample/herbal-tea.jpg
|
||||||
|
custom: true
|
||||||
|
|
||||||
|
fruit tea:
|
||||||
|
price: 1.5
|
||||||
|
title: Fruit Tea
|
||||||
|
description: The Tibetan Himalayas, the land is majestic and beautiful—a spiritual place where, despite the perilous environment, many journey seeking enlightenment. Pay us what you want!
|
||||||
|
image: ~/img/pos-sample/fruit-tea.jpg
|
||||||
|
inventory: 5
|
||||||
|
custom: true
|
||||||
|
";
|
||||||
|
var parsedDefault = MigrationStartupTask.ParsePOSYML(testOriginalDefaultYmlTemplate);
|
||||||
|
Assert.Equal(6, parsedDefault.Length);
|
||||||
|
Assert.Equal( "Green Tea" ,parsedDefault[0].Title);
|
||||||
|
Assert.Equal( "green tea" ,parsedDefault[0].Id);
|
||||||
|
Assert.Equal( "Lovely, fresh and tender, Meng Ding Gan Lu ('sweet dew') is grown in the lush Meng Ding Mountains of the southwestern province of Sichuan where it has been cultivated for over a thousand years." ,parsedDefault[0].Description);
|
||||||
|
Assert.Null( parsedDefault[0].BuyButtonText);
|
||||||
|
Assert.Equal( "~/img/pos-sample/green-tea.jpg" ,parsedDefault[0].Image);
|
||||||
|
Assert.Equal( 1 ,parsedDefault[0].Price);
|
||||||
|
Assert.Equal( ViewPointOfSaleViewModel.ItemPriceType.Fixed ,parsedDefault[0].PriceType);
|
||||||
|
Assert.Null( parsedDefault[0].AdditionalData);
|
||||||
|
Assert.Null( parsedDefault[0].PaymentMethods);
|
||||||
|
|
||||||
|
|
||||||
|
Assert.Equal( "Herbal Tea" ,parsedDefault[4].Title);
|
||||||
|
Assert.Equal( "herbal tea" ,parsedDefault[4].Id);
|
||||||
|
Assert.Equal( "Chamomile tea is made from the flower heads of the chamomile plant. The medicinal use of chamomile dates back to the ancient Egyptians, Romans and Greeks. Pay us what you want!" ,parsedDefault[4].Description);
|
||||||
|
Assert.Null( parsedDefault[4].BuyButtonText);
|
||||||
|
Assert.Equal( "~/img/pos-sample/herbal-tea.jpg" ,parsedDefault[4].Image);
|
||||||
|
Assert.Equal( 1.8m ,parsedDefault[4].Price);
|
||||||
|
Assert.Equal( ViewPointOfSaleViewModel.ItemPriceType.Minimum ,parsedDefault[4].PriceType);
|
||||||
|
Assert.Null( parsedDefault[4].AdditionalData);
|
||||||
|
Assert.Null( parsedDefault[4].PaymentMethods);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact(Timeout = LongRunningTestTimeout)]
|
[Fact(Timeout = LongRunningTestTimeout)]
|
||||||
[Trait("Integration", "Integration")]
|
[Trait("Integration", "Integration")]
|
||||||
public async Task CanUsePoSApp1()
|
public async Task CanUsePoSApp1()
|
||||||
@@ -53,21 +125,59 @@ donation:
|
|||||||
price: 1.02
|
price: 1.02
|
||||||
custom: true
|
custom: true
|
||||||
";
|
";
|
||||||
|
vmpos.Currency = "EUR";
|
||||||
|
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
|
||||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||||
await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||||
var publicApps = user.GetController<UIPointOfSaleController>();
|
var publicApps = user.GetController<UIPointOfSaleController>();
|
||||||
var vmview = await publicApps.ViewPointOfSale(app.Id, PosViewType.Cart).AssertViewModelAsync<ViewPointOfSaleViewModel>();
|
var vmview = await publicApps.ViewPointOfSale(app.Id, PosViewType.Cart).AssertViewModelAsync<ViewPointOfSaleViewModel>();
|
||||||
|
|
||||||
|
Assert.Equal("EUR", vmview.CurrencyCode);
|
||||||
// apple shouldn't be available since we it's set to "disabled: true" above
|
// apple shouldn't be available since we it's set to "disabled: true" above
|
||||||
Assert.Equal(2, vmview.Items.Length);
|
Assert.Equal(2, vmview.Items.Length);
|
||||||
Assert.Equal("orange", vmview.Items[0].Title);
|
Assert.Equal("orange", vmview.Items[0].Title);
|
||||||
Assert.Equal("donation", vmview.Items[1].Title);
|
Assert.Equal("donation", vmview.Items[1].Title);
|
||||||
// orange is available
|
// orange is available
|
||||||
Assert.IsType<RedirectToActionResult>(publicApps
|
Assert.IsType<RedirectToActionResult>(publicApps
|
||||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, null, null, null, null, "orange").Result);
|
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, choiceKey: "orange").Result);
|
||||||
// apple is not found
|
// apple is not found
|
||||||
Assert.IsType<NotFoundResult>(publicApps
|
Assert.IsType<NotFoundResult>(publicApps
|
||||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, null, null, null, null, "apple").Result);
|
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, choiceKey: "apple").Result);
|
||||||
|
|
||||||
|
// List
|
||||||
|
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||||
|
app = appList.Apps[0];
|
||||||
|
apps = user.GetController<UIAppsController>();
|
||||||
|
appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType, Settings = "{\"currency\":\"EUR\"}" };
|
||||||
|
apps.HttpContext.SetAppData(appData);
|
||||||
|
pos.HttpContext.SetAppData(appData);
|
||||||
|
Assert.Single(appList.Apps);
|
||||||
|
Assert.Equal("test", app.AppName);
|
||||||
|
Assert.True(app.Role.ToPermissionSet(appList.Apps[0].StoreId).Contains(Policies.CanModifyStoreSettings, app.StoreId));
|
||||||
|
Assert.Equal(user.StoreId, app.StoreId);
|
||||||
|
Assert.False(app.Archived);
|
||||||
|
// Archive
|
||||||
|
redirect = Assert.IsType<RedirectResult>(apps.ToggleArchive(app.Id).Result);
|
||||||
|
Assert.EndsWith("/settings/pos", redirect.Url);
|
||||||
|
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||||
|
Assert.Empty(appList.Apps);
|
||||||
|
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId, archived: true).Result).Model);
|
||||||
|
app = appList.Apps[0];
|
||||||
|
Assert.True(app.Archived);
|
||||||
|
Assert.IsType<NotFoundResult>(await publicApps.ViewPointOfSale(app.Id, PosViewType.Static));
|
||||||
|
// Unarchive
|
||||||
|
redirect = Assert.IsType<RedirectResult>(apps.ToggleArchive(app.Id).Result);
|
||||||
|
Assert.EndsWith("/settings/pos", redirect.Url);
|
||||||
|
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||||
|
app = appList.Apps[0];
|
||||||
|
Assert.False(app.Archived);
|
||||||
|
Assert.IsType<ViewResult>(await publicApps.ViewPointOfSale(app.Id, PosViewType.Static));
|
||||||
|
// Delete
|
||||||
|
Assert.IsType<ViewResult>(apps.DeleteApp(app.Id));
|
||||||
|
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(app.Id).Result);
|
||||||
|
Assert.Equal(nameof(UIStoresController.Dashboard), redirectToAction.ActionName);
|
||||||
|
appList = await apps.ListApps(user.StoreId).AssertViewModelAsync<ListAppsViewModel>();
|
||||||
|
Assert.Empty(appList.Apps);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -31,7 +31,6 @@ namespace BTCPayServer.Tests
|
|||||||
user.RegisterDerivationScheme("BTC");
|
user.RegisterDerivationScheme("BTC");
|
||||||
|
|
||||||
var user2 = tester.NewAccount();
|
var user2 = tester.NewAccount();
|
||||||
|
|
||||||
await user2.GrantAccessAsync();
|
await user2.GrantAccessAsync();
|
||||||
|
|
||||||
var paymentRequestController = user.GetController<UIPaymentRequestController>();
|
var paymentRequestController = user.GetController<UIPaymentRequestController>();
|
||||||
@@ -162,7 +161,7 @@ namespace BTCPayServer.Tests
|
|||||||
using var tester = CreateServerTester();
|
using var tester = CreateServerTester();
|
||||||
await tester.StartAsync();
|
await tester.StartAsync();
|
||||||
var user = tester.NewAccount();
|
var user = tester.NewAccount();
|
||||||
user.GrantAccess();
|
await user.GrantAccessAsync();
|
||||||
user.RegisterDerivationScheme("BTC");
|
user.RegisterDerivationScheme("BTC");
|
||||||
|
|
||||||
var paymentRequestController = user.GetController<UIPaymentRequestController>();
|
var paymentRequestController = user.GetController<UIPaymentRequestController>();
|
||||||
@@ -170,7 +169,7 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.IsType<NotFoundResult>(await
|
Assert.IsType<NotFoundResult>(await
|
||||||
paymentRequestController.CancelUnpaidPendingInvoice(Guid.NewGuid().ToString(), false));
|
paymentRequestController.CancelUnpaidPendingInvoice(Guid.NewGuid().ToString(), false));
|
||||||
|
|
||||||
var request = new UpdatePaymentRequestViewModel()
|
var request = new UpdatePaymentRequestViewModel
|
||||||
{
|
{
|
||||||
Title = "original juice",
|
Title = "original juice",
|
||||||
Currency = "BTC",
|
Currency = "BTC",
|
||||||
|
@@ -180,7 +180,7 @@ namespace BTCPayServer.Tests
|
|||||||
{
|
{
|
||||||
Driver.FindElement(By.Id("StoreSelectorToggle")).Click();
|
Driver.FindElement(By.Id("StoreSelectorToggle")).Click();
|
||||||
}
|
}
|
||||||
Driver.WaitForElement(By.Id("StoreSelectorCreate")).Click();
|
GoToUrl("/stores/create");
|
||||||
var name = "Store" + RandomUtils.GetUInt64();
|
var name = "Store" + RandomUtils.GetUInt64();
|
||||||
TestLogs.LogInformation($"Created store {name}");
|
TestLogs.LogInformation($"Created store {name}");
|
||||||
Driver.WaitForElement(By.Id("Name")).SendKeys(name);
|
Driver.WaitForElement(By.Id("Name")).SendKeys(name);
|
||||||
@@ -313,8 +313,6 @@ namespace BTCPayServer.Tests
|
|||||||
|
|
||||||
var connectionString = connectionType switch
|
var connectionString = connectionType switch
|
||||||
{
|
{
|
||||||
LightningConnectionType.Charge =>
|
|
||||||
$"type=charge;server={Server.MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true",
|
|
||||||
LightningConnectionType.CLightning =>
|
LightningConnectionType.CLightning =>
|
||||||
$"type=clightning;server={((CLightningClient)Server.MerchantLightningD).Address.AbsoluteUri}",
|
$"type=clightning;server={((CLightningClient)Server.MerchantLightningD).Address.AbsoluteUri}",
|
||||||
LightningConnectionType.LndREST =>
|
LightningConnectionType.LndREST =>
|
||||||
@@ -395,6 +393,10 @@ namespace BTCPayServer.Tests
|
|||||||
public void GoToHome()
|
public void GoToHome()
|
||||||
{
|
{
|
||||||
Driver.Navigate().GoToUrl(ServerUri);
|
Driver.Navigate().GoToUrl(ServerUri);
|
||||||
|
if (Driver.PageSource.Contains("id=\"SkipWizard\""))
|
||||||
|
{
|
||||||
|
Driver.FindElement(By.Id("SkipWizard")).Click();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Logout()
|
public void Logout()
|
||||||
@@ -563,7 +565,7 @@ namespace BTCPayServer.Tests
|
|||||||
walletId ??= WalletId;
|
walletId ??= WalletId;
|
||||||
GoToWallet(walletId, WalletsNavPages.Receive);
|
GoToWallet(walletId, WalletsNavPages.Receive);
|
||||||
Driver.FindElement(By.Id("generateButton")).Click();
|
Driver.FindElement(By.Id("generateButton")).Click();
|
||||||
var addressStr = Driver.FindElement(By.Id("Address")).GetAttribute("value");
|
var addressStr = Driver.FindElement(By.Id("Address")).GetAttribute("data-text");
|
||||||
var address = BitcoinAddress.Create(addressStr, ((BTCPayNetwork)Server.NetworkProvider.GetNetwork(walletId.CryptoCode)).NBitcoinNetwork);
|
var address = BitcoinAddress.Create(addressStr, ((BTCPayNetwork)Server.NetworkProvider.GetNetwork(walletId.CryptoCode)).NBitcoinNetwork);
|
||||||
for (var i = 0; i < coins; i++)
|
for (var i = 0; i < coins; i++)
|
||||||
{
|
{
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -92,7 +92,7 @@ namespace BTCPayServer.Tests
|
|||||||
#endif
|
#endif
|
||||||
public void ActivateLightning()
|
public void ActivateLightning()
|
||||||
{
|
{
|
||||||
ActivateLightning(LightningConnectionType.Charge);
|
ActivateLightning(LightningConnectionType.CLightning);
|
||||||
}
|
}
|
||||||
public void ActivateLightning(LightningConnectionType internalNode)
|
public void ActivateLightning(LightningConnectionType internalNode)
|
||||||
{
|
{
|
||||||
@@ -109,14 +109,7 @@ namespace BTCPayServer.Tests
|
|||||||
string connectionString = null;
|
string connectionString = null;
|
||||||
if (connectionType is null)
|
if (connectionType is null)
|
||||||
return LightningSupportedPaymentMethod.InternalNode;
|
return LightningSupportedPaymentMethod.InternalNode;
|
||||||
if (connectionType == LightningConnectionType.Charge)
|
if (connectionType == LightningConnectionType.CLightning)
|
||||||
{
|
|
||||||
if (isMerchant)
|
|
||||||
connectionString = $"type=charge;server={MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true";
|
|
||||||
else
|
|
||||||
throw new NotSupportedException();
|
|
||||||
}
|
|
||||||
else if (connectionType == LightningConnectionType.CLightning)
|
|
||||||
{
|
{
|
||||||
if (isMerchant)
|
if (isMerchant)
|
||||||
connectionString = "type=clightning;server=" +
|
connectionString = "type=clightning;server=" +
|
||||||
|
@@ -40,6 +40,7 @@ namespace BTCPayServer.Tests
|
|||||||
public class TestAccount
|
public class TestAccount
|
||||||
{
|
{
|
||||||
readonly ServerTester parent;
|
readonly ServerTester parent;
|
||||||
|
public string LNAddress;
|
||||||
|
|
||||||
public TestAccount(ServerTester parent)
|
public TestAccount(ServerTester parent)
|
||||||
{
|
{
|
||||||
@@ -242,7 +243,7 @@ namespace BTCPayServer.Tests
|
|||||||
policies.LockSubscription = false;
|
policies.LockSubscription = false;
|
||||||
await account.Register(RegisterDetails);
|
await account.Register(RegisterDetails);
|
||||||
}
|
}
|
||||||
|
TestLogs.LogInformation($"UserId: {account.RegisteredUserId} Password: {Password}");
|
||||||
UserId = account.RegisteredUserId;
|
UserId = account.RegisteredUserId;
|
||||||
Email = RegisterDetails.Email;
|
Email = RegisterDetails.Email;
|
||||||
IsAdmin = account.RegisteredAdmin;
|
IsAdmin = account.RegisteredAdmin;
|
||||||
@@ -277,7 +278,7 @@ namespace BTCPayServer.Tests
|
|||||||
|
|
||||||
public bool IsAdmin { get; internal set; }
|
public bool IsAdmin { get; internal set; }
|
||||||
|
|
||||||
public void RegisterLightningNode(string cryptoCode, LightningConnectionType connectionType, bool isMerchant = true)
|
public void RegisterLightningNode(string cryptoCode, LightningConnectionType? connectionType = null, bool isMerchant = true)
|
||||||
{
|
{
|
||||||
RegisterLightningNodeAsync(cryptoCode, connectionType, isMerchant).GetAwaiter().GetResult();
|
RegisterLightningNodeAsync(cryptoCode, connectionType, isMerchant).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
@@ -309,8 +310,9 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.False(true, storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage);
|
Assert.False(true, storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Coin> ReceiveUTXO(Money value, BTCPayNetwork network)
|
public async Task<Coin> ReceiveUTXO(Money value, BTCPayNetwork network = null)
|
||||||
{
|
{
|
||||||
|
network ??= SupportedNetwork;
|
||||||
var cashCow = parent.ExplorerNode;
|
var cashCow = parent.ExplorerNode;
|
||||||
var btcPayWallet = parent.PayTester.GetService<BTCPayWalletProvider>().GetWallet(network);
|
var btcPayWallet = parent.PayTester.GetService<BTCPayWalletProvider>().GetWallet(network);
|
||||||
var address = (await btcPayWallet.ReserveAddressAsync(this.DerivationScheme)).Address;
|
var address = (await btcPayWallet.ReserveAddressAsync(this.DerivationScheme)).Address;
|
||||||
@@ -470,7 +472,10 @@ namespace BTCPayServer.Tests
|
|||||||
var req = await _server.GetNextRequest(cancellation);
|
var req = await _server.GetNextRequest(cancellation);
|
||||||
var bytes = await req.Request.Body.ReadBytesAsync((int)req.Request.Headers.ContentLength);
|
var bytes = await req.Request.Body.ReadBytesAsync((int)req.Request.Headers.ContentLength);
|
||||||
var callback = Encoding.UTF8.GetString(bytes);
|
var callback = Encoding.UTF8.GetString(bytes);
|
||||||
_webhookEvents.Add(JsonConvert.DeserializeObject<WebhookInvoiceEvent>(callback));
|
lock (_webhookEvents)
|
||||||
|
{
|
||||||
|
_webhookEvents.Add(JsonConvert.DeserializeObject<WebhookInvoiceEvent>(callback));
|
||||||
|
}
|
||||||
req.Response.StatusCode = 200;
|
req.Response.StatusCode = 200;
|
||||||
_server.Done();
|
_server.Done();
|
||||||
}
|
}
|
||||||
@@ -487,18 +492,21 @@ namespace BTCPayServer.Tests
|
|||||||
{
|
{
|
||||||
int retry = 0;
|
int retry = 0;
|
||||||
retry:
|
retry:
|
||||||
foreach (var evt in WebhookEvents)
|
lock (WebhookEvents)
|
||||||
{
|
{
|
||||||
if (evt.Type == eventType)
|
foreach (var evt in WebhookEvents)
|
||||||
{
|
{
|
||||||
var typedEvt = evt.ReadAs<TEvent>();
|
if (evt.Type == eventType)
|
||||||
try
|
|
||||||
{
|
|
||||||
assert(typedEvt);
|
|
||||||
return typedEvt;
|
|
||||||
}
|
|
||||||
catch (XunitException)
|
|
||||||
{
|
{
|
||||||
|
var typedEvt = evt.ReadAs<TEvent>();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
assert(typedEvt);
|
||||||
|
return typedEvt;
|
||||||
|
}
|
||||||
|
catch (XunitException)
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -540,12 +548,101 @@ retry:
|
|||||||
public async Task AddGuest(string userId)
|
public async Task AddGuest(string userId)
|
||||||
{
|
{
|
||||||
var repo = this.parent.PayTester.GetService<StoreRepository>();
|
var repo = this.parent.PayTester.GetService<StoreRepository>();
|
||||||
await repo.AddStoreUser(StoreId, userId, "Guest");
|
await repo.AddStoreUser(StoreId, userId, StoreRoleId.Guest);
|
||||||
}
|
}
|
||||||
public async Task AddOwner(string userId)
|
public async Task AddOwner(string userId)
|
||||||
{
|
{
|
||||||
var repo = this.parent.PayTester.GetService<StoreRepository>();
|
var repo = this.parent.PayTester.GetService<StoreRepository>();
|
||||||
await repo.AddStoreUser(StoreId, userId, "Owner");
|
await repo.AddStoreUser(StoreId, userId, StoreRoleId.Owner);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<uint256> PayOnChain(string invoiceId)
|
||||||
|
{
|
||||||
|
var cryptoCode = "BTC";
|
||||||
|
var client = await CreateClient();
|
||||||
|
var methods = await client.GetInvoicePaymentMethods(StoreId, invoiceId);
|
||||||
|
var method = methods.First(m => m.PaymentMethod == cryptoCode);
|
||||||
|
var address = method.Destination;
|
||||||
|
var tx = await client.CreateOnChainTransaction(StoreId, cryptoCode, new CreateOnChainTransactionRequest()
|
||||||
|
{
|
||||||
|
Destinations = new List<CreateOnChainTransactionRequest.CreateOnChainTransactionRequestDestination>()
|
||||||
|
{
|
||||||
|
new ()
|
||||||
|
{
|
||||||
|
Destination = address,
|
||||||
|
Amount = method.Due
|
||||||
|
}
|
||||||
|
},
|
||||||
|
FeeRate = new FeeRate(1.0m)
|
||||||
|
});
|
||||||
|
await WaitInvoicePaid(invoiceId);
|
||||||
|
return tx.TransactionHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PayOnBOLT11(string invoiceId)
|
||||||
|
{
|
||||||
|
var cryptoCode = "BTC";
|
||||||
|
var client = await CreateClient();
|
||||||
|
var methods = await client.GetInvoicePaymentMethods(StoreId, invoiceId);
|
||||||
|
var method = methods.First(m => m.PaymentMethod == $"{cryptoCode}-LightningNetwork");
|
||||||
|
var bolt11 = method.Destination;
|
||||||
|
TestLogs.LogInformation("PAYING");
|
||||||
|
await parent.CustomerLightningD.Pay(bolt11);
|
||||||
|
TestLogs.LogInformation("PAID");
|
||||||
|
await WaitInvoicePaid(invoiceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PayOnLNUrl(string invoiceId)
|
||||||
|
{
|
||||||
|
var cryptoCode = "BTC";
|
||||||
|
var network = SupportedNetwork.NBitcoinNetwork;
|
||||||
|
var client = await CreateClient();
|
||||||
|
var methods = await client.GetInvoicePaymentMethods(StoreId, invoiceId);
|
||||||
|
var method = methods.First(m => m.PaymentMethod == $"{cryptoCode}-LNURLPAY");
|
||||||
|
var lnurL = LNURL.LNURL.Parse(method.PaymentLink, out var tag);
|
||||||
|
var http = new HttpClient();
|
||||||
|
var payreq = (LNURL.LNURLPayRequest)await LNURL.LNURL.FetchInformation(lnurL, tag, http);
|
||||||
|
var resp = await payreq.SendRequest(payreq.MinSendable, network, http);
|
||||||
|
var bolt11 = resp.Pr;
|
||||||
|
await parent.CustomerLightningD.Pay(bolt11);
|
||||||
|
await WaitInvoicePaid(invoiceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task WaitInvoicePaid(string invoiceId)
|
||||||
|
{
|
||||||
|
return TestUtils.EventuallyAsync(async () =>
|
||||||
|
{
|
||||||
|
var client = await CreateClient();
|
||||||
|
var invoice = await client.GetInvoice(StoreId, invoiceId);
|
||||||
|
if (invoice.Status == InvoiceStatus.Settled)
|
||||||
|
return;
|
||||||
|
Assert.Equal(InvoiceStatus.Processing, invoice.Status);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PayOnLNAddress(string lnAddrUser = null)
|
||||||
|
{
|
||||||
|
lnAddrUser ??= LNAddress;
|
||||||
|
var network = SupportedNetwork.NBitcoinNetwork;
|
||||||
|
var payReqStr = await (await parent.PayTester.HttpClient.GetAsync($".well-known/lnurlp/{lnAddrUser}")).Content.ReadAsStringAsync();
|
||||||
|
var payreq = JsonConvert.DeserializeObject<LNURL.LNURLPayRequest>(payReqStr);
|
||||||
|
var resp = await payreq.SendRequest(payreq.MinSendable, network, parent.PayTester.HttpClient);
|
||||||
|
var bolt11 = resp.Pr;
|
||||||
|
await parent.CustomerLightningD.Pay(bolt11);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> CreateLNAddress()
|
||||||
|
{
|
||||||
|
var lnAddrUser = Guid.NewGuid().ToString();
|
||||||
|
var ctx = parent.PayTester.GetService<ApplicationDbContextFactory>().CreateContext();
|
||||||
|
ctx.LightningAddresses.Add(new()
|
||||||
|
{
|
||||||
|
StoreDataId = StoreId,
|
||||||
|
Username = lnAddrUser
|
||||||
|
});
|
||||||
|
await ctx.SaveChangesAsync();
|
||||||
|
LNAddress = lnAddrUser;
|
||||||
|
return lnAddrUser;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -16,12 +16,14 @@ using BTCPayServer.Storage.Models;
|
|||||||
using BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration;
|
using BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.FileSystemGlobbing;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using NBitpayClient;
|
using NBitpayClient;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
using Xunit.Sdk;
|
using Xunit.Sdk;
|
||||||
|
using static BTCPayServer.HostedServices.PullPaymentHostedService.PayoutApproval;
|
||||||
|
|
||||||
namespace BTCPayServer.Tests
|
namespace BTCPayServer.Tests
|
||||||
{
|
{
|
||||||
@@ -177,7 +179,7 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.Contains(rates, e => e.CurrencyPair == new CurrencyPair("XMR", "BTC") && e.BidAsk.Bid < 1.0m);
|
Assert.Contains(rates, e => e.CurrencyPair == new CurrencyPair("XMR", "BTC") && e.BidAsk.Bid < 1.0m);
|
||||||
|
|
||||||
// Check we didn't skip too many exchanges
|
// Check we didn't skip too many exchanges
|
||||||
Assert.InRange(skipped, 0, 3);
|
Assert.InRange(skipped, 0, 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -290,9 +292,43 @@ retry:
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CanGetRateCryptoCurrenciesByDefault()
|
public async Task CanGetRateFromRecommendedExchanges()
|
||||||
{
|
{
|
||||||
string[] brokenShitcoins = { "BTX_USD", "CHC_USD" };
|
var factory = FastTests.CreateBTCPayRateFactory();
|
||||||
|
var fetcher = new RateFetcher(factory);
|
||||||
|
var provider = new BTCPayNetworkProvider(ChainName.Mainnet);
|
||||||
|
var b = new StoreBlob();
|
||||||
|
string[] temporarilyBroken = { "COP", "UGX" };
|
||||||
|
foreach (var k in StoreBlob.RecommendedExchanges)
|
||||||
|
{
|
||||||
|
b.DefaultCurrency = k.Key;
|
||||||
|
var rules = b.GetDefaultRateRules(provider);
|
||||||
|
var pairs = new[] { CurrencyPair.Parse($"BTC_{k.Key}") }.ToHashSet();
|
||||||
|
var result = fetcher.FetchRates(pairs, rules, default);
|
||||||
|
foreach ((CurrencyPair key, Task<RateResult> value) in result)
|
||||||
|
{
|
||||||
|
TestLogs.LogInformation($"Testing {key} when default currency is {k.Key}");
|
||||||
|
var rateResult = await value;
|
||||||
|
var hasRate = rateResult.BidAsk != null;
|
||||||
|
|
||||||
|
if (temporarilyBroken.Contains(k.Key))
|
||||||
|
{
|
||||||
|
if (!hasRate)
|
||||||
|
{
|
||||||
|
TestLogs.LogInformation($"Skipping {key} because it is marked as temporarily broken");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
TestLogs.LogInformation($"Note: {key} is marked as temporarily broken, but the rate is available");
|
||||||
|
}
|
||||||
|
Assert.True(hasRate, $"Impossible to get the rate {rateResult.EvaluatedRule}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CanGetRateCryptoCurrenciesByDefault()
|
||||||
|
{
|
||||||
|
using var cts = new CancellationTokenSource(60_000);
|
||||||
var provider = new BTCPayNetworkProvider(ChainName.Mainnet);
|
var provider = new BTCPayNetworkProvider(ChainName.Mainnet);
|
||||||
var factory = FastTests.CreateBTCPayRateFactory();
|
var factory = FastTests.CreateBTCPayRateFactory();
|
||||||
var fetcher = new RateFetcher(factory);
|
var fetcher = new RateFetcher(factory);
|
||||||
@@ -301,19 +337,29 @@ retry:
|
|||||||
.Select(c => new CurrencyPair(c.CryptoCode, "USD"))
|
.Select(c => new CurrencyPair(c.CryptoCode, "USD"))
|
||||||
.ToHashSet();
|
.ToHashSet();
|
||||||
|
|
||||||
|
string[] brokenShitcoins = { "BTG", "BTX" };
|
||||||
|
bool IsBrokenShitcoin(CurrencyPair p) => brokenShitcoins.Contains(p.Left) || brokenShitcoins.Contains(p.Right);
|
||||||
|
foreach (var _ in brokenShitcoins)
|
||||||
|
{
|
||||||
|
foreach (var p in pairs.Where(IsBrokenShitcoin).ToArray())
|
||||||
|
{
|
||||||
|
TestLogs.LogInformation($"Skipping {p} because it is marked as broken");
|
||||||
|
pairs.Remove(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var rules = new StoreBlob().GetDefaultRateRules(provider);
|
var rules = new StoreBlob().GetDefaultRateRules(provider);
|
||||||
var result = fetcher.FetchRates(pairs, rules, default);
|
var result = fetcher.FetchRates(pairs, rules, cts.Token);
|
||||||
foreach ((CurrencyPair key, Task<RateResult> value) in result)
|
foreach ((CurrencyPair key, Task<RateResult> value) in result)
|
||||||
{
|
{
|
||||||
var rateResult = value.GetAwaiter().GetResult();
|
var rateResult = await value;
|
||||||
TestLogs.LogInformation($"Testing {key}");
|
TestLogs.LogInformation($"Testing {key}");
|
||||||
if (brokenShitcoins.Contains(key.ToString()))
|
|
||||||
continue;
|
|
||||||
Assert.True(rateResult.BidAsk != null, $"Impossible to get the rate {rateResult.EvaluatedRule}");
|
Assert.True(rateResult.BidAsk != null, $"Impossible to get the rate {rateResult.EvaluatedRule}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
[Trait("Fast", "Fast")]
|
||||||
public async Task CheckJsContent()
|
public async Task CheckJsContent()
|
||||||
{
|
{
|
||||||
// This test verify that no malicious js is added in the minified files.
|
// This test verify that no malicious js is added in the minified files.
|
||||||
@@ -322,42 +368,77 @@ retry:
|
|||||||
var actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "bootstrap", "bootstrap.bundle.min.js").Trim();
|
var actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "bootstrap", "bootstrap.bundle.min.js").Trim();
|
||||||
var version = Regex.Match(actual, "Bootstrap v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
|
var version = Regex.Match(actual, "Bootstrap v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
|
||||||
var expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/bootstrap@{version}/dist/js/bootstrap.bundle.min.js")).Content.ReadAsStringAsync()).Trim();
|
var expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/bootstrap@{version}/dist/js/bootstrap.bundle.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||||
Assert.Equal(expected, actual);
|
EqualJsContent(expected, actual);
|
||||||
|
|
||||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "clipboard.js", "clipboard.js");
|
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "clipboard.js", "clipboard.js");
|
||||||
expected = (await (await client.GetAsync("https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.8/clipboard.js")).Content.ReadAsStringAsync()).Trim();
|
expected = (await (await client.GetAsync("https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.8/clipboard.js")).Content.ReadAsStringAsync()).Trim();
|
||||||
Assert.Equal(expected, actual);
|
EqualJsContent(expected, actual);
|
||||||
|
|
||||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vuejs", "vue.min.js").Trim();
|
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vuejs", "vue.min.js").Trim();
|
||||||
version = Regex.Match(actual, "Vue\\.js v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
|
version = Regex.Match(actual, "Vue\\.js v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
|
||||||
expected = (await (await client.GetAsync($"https://cdnjs.cloudflare.com/ajax/libs/vue/{version}/vue.min.js")).Content.ReadAsStringAsync()).Trim();
|
expected = (await (await client.GetAsync($"https://cdnjs.cloudflare.com/ajax/libs/vue/{version}/vue.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||||
Assert.Equal(expected, actual);
|
EqualJsContent(expected, actual);
|
||||||
|
|
||||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "i18next", "i18next.min.js").Trim();
|
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "i18next", "i18next.min.js").Trim();
|
||||||
expected = (await (await client.GetAsync("https://cdnjs.cloudflare.com/ajax/libs/i18next/22.0.6/i18next.min.js")).Content.ReadAsStringAsync()).Trim();
|
expected = (await (await client.GetAsync("https://cdnjs.cloudflare.com/ajax/libs/i18next/22.0.6/i18next.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||||
Assert.Equal(expected, actual);
|
EqualJsContent(expected, actual);
|
||||||
|
|
||||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "i18next", "i18nextHttpBackend.min.js").Trim();
|
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "i18next", "i18nextHttpBackend.min.js").Trim();
|
||||||
expected = (await (await client.GetAsync("https://cdnjs.cloudflare.com/ajax/libs/i18next-http-backend/2.0.1/i18nextHttpBackend.min.js")).Content.ReadAsStringAsync()).Trim();
|
expected = (await (await client.GetAsync("https://cdnjs.cloudflare.com/ajax/libs/i18next-http-backend/2.0.1/i18nextHttpBackend.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||||
Assert.Equal(expected, actual);
|
EqualJsContent(expected, actual);
|
||||||
|
|
||||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "i18next", "vue-i18next.js").Trim();
|
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "i18next", "vue-i18next.js").Trim();
|
||||||
expected = (await (await client.GetAsync("https://unpkg.com/@panter/vue-i18next@0.15.2/dist/vue-i18next.js")).Content.ReadAsStringAsync()).Trim();
|
expected = (await (await client.GetAsync("https://unpkg.com/@panter/vue-i18next@0.15.2/dist/vue-i18next.js")).Content.ReadAsStringAsync()).Trim();
|
||||||
Assert.Equal(expected, actual);
|
EqualJsContent(expected, actual);
|
||||||
|
|
||||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vue-qrcode", "vue-qrcode.min.js").Trim();
|
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vue-qrcode", "vue-qrcode.min.js").Trim();
|
||||||
version = Regex.Match(actual, "vue-qrcode v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
|
version = Regex.Match(actual, "vue-qrcode v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
|
||||||
expected = (await (await client.GetAsync($"https://unpkg.com/@chenfengyuan/vue-qrcode@{version}/dist/vue-qrcode.min.js")).Content.ReadAsStringAsync()).Trim();
|
expected = (await (await client.GetAsync($"https://unpkg.com/@chenfengyuan/vue-qrcode@{version}/dist/vue-qrcode.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||||
Assert.Equal(expected, actual);
|
EqualJsContent(expected, actual);
|
||||||
|
|
||||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "tom-select", "tom-select.complete.min.js").Trim();
|
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "tom-select", "tom-select.complete.min.js").Trim();
|
||||||
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/js/tom-select.complete.min.js")).Content.ReadAsStringAsync()).Trim();
|
version = Regex.Match(actual, "Tom Select v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
|
||||||
Assert.Equal(expected, actual);
|
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/tom-select@{version}/dist/js/tom-select.complete.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||||
|
EqualJsContent(expected, actual);
|
||||||
|
|
||||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "dom-confetti", "dom-confetti.min.js").Trim();
|
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "dom-confetti", "dom-confetti.min.js").Trim();
|
||||||
version = Regex.Match(actual, "Original file: /npm/dom-confetti@([0-9]+.[0-9]+.[0-9]+)/lib/main.js").Groups[1].Value;
|
version = Regex.Match(actual, "Original file: /npm/dom-confetti@([0-9]+.[0-9]+.[0-9]+)/lib/main.js").Groups[1].Value;
|
||||||
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/dom-confetti@{version}/lib/main.min.js")).Content.ReadAsStringAsync()).Trim();
|
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/dom-confetti@{version}/lib/main.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||||
Assert.Equal(expected, actual);
|
EqualJsContent(expected, actual);
|
||||||
|
|
||||||
|
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vue-sortable", "sortable.min.js").Trim();
|
||||||
|
version = Regex.Match(actual, "Sortable ([0-9]+.[0-9]+.[0-9]+) ").Groups[1].Value;
|
||||||
|
expected = (await (await client.GetAsync($"https://unpkg.com/sortablejs@{version}/Sortable.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||||
|
EqualJsContent(expected, actual);
|
||||||
|
|
||||||
|
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "bootstrap-vue", "bootstrap-vue.min.js").Trim();
|
||||||
|
version = Regex.Match(actual, "BootstrapVue ([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
|
||||||
|
expected = (await (await client.GetAsync($"https://cdnjs.cloudflare.com/ajax/libs/bootstrap-vue/{version}/bootstrap-vue.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||||
|
EqualJsContent(expected, actual);
|
||||||
|
|
||||||
|
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "FileSaver", "FileSaver.min.js").Trim();
|
||||||
|
expected = (await (await client.GetAsync($"https://raw.githubusercontent.com/eligrey/FileSaver.js/43bbd2f0ae6794f8d452cd360e9d33aef6071234/dist/FileSaver.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||||
|
EqualJsContent(expected, actual);
|
||||||
|
|
||||||
|
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "papaparse", "papaparse.min.js").Trim();
|
||||||
|
expected = (await (await client.GetAsync($"https://raw.githubusercontent.com/mholt/PapaParse/5.4.1/papaparse.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||||
|
EqualJsContent(expected, actual);
|
||||||
|
|
||||||
|
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vue-sanitize-directive", "vue-sanitize-directive.umd.min.js").Trim();
|
||||||
|
version = Regex.Match(actual, "Original file: /npm/vue-sanitize-directive@([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
|
||||||
|
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/vue-sanitize-directive@{version}/dist/vue-sanitize-directive.umd.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||||
|
EqualJsContent(expected, actual);
|
||||||
|
|
||||||
|
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "decimal.js", "decimal.min.js").Trim();
|
||||||
|
version = Regex.Match(actual, "Original file: /npm/decimal\\.js@([0-9]+.[0-9]+.[0-9]+)/decimal\\.js").Groups[1].Value;
|
||||||
|
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/decimal.js@{version}/decimal.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||||
|
EqualJsContent(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EqualJsContent(string expected, string actual)
|
||||||
|
{
|
||||||
|
if (expected != actual)
|
||||||
|
Assert.Equal(expected, actual.ReplaceLineEndings("\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
string GetFileContent(params string[] path)
|
string GetFileContent(params string[] path)
|
||||||
|
@@ -39,6 +39,7 @@ using BTCPayServer.Plugins.PayButton;
|
|||||||
using BTCPayServer.Plugins.PointOfSale;
|
using BTCPayServer.Plugins.PointOfSale;
|
||||||
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||||
using BTCPayServer.Security.Bitpay;
|
using BTCPayServer.Security.Bitpay;
|
||||||
|
using BTCPayServer.Security.Greenfield;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
using BTCPayServer.Services.Apps;
|
using BTCPayServer.Services.Apps;
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
@@ -385,11 +386,11 @@ namespace BTCPayServer.Tests
|
|||||||
var newBolt11 = newInvoice.CryptoInfo.First(o => o.PaymentUrls.BOLT11 != null).PaymentUrls.BOLT11;
|
var newBolt11 = newInvoice.CryptoInfo.First(o => o.PaymentUrls.BOLT11 != null).PaymentUrls.BOLT11;
|
||||||
var oldBolt11 = invoice.CryptoInfo.First(o => o.PaymentUrls.BOLT11 != null).PaymentUrls.BOLT11;
|
var oldBolt11 = invoice.CryptoInfo.First(o => o.PaymentUrls.BOLT11 != null).PaymentUrls.BOLT11;
|
||||||
Assert.NotEqual(newBolt11, oldBolt11);
|
Assert.NotEqual(newBolt11, oldBolt11);
|
||||||
Assert.Equal(newInvoice.BtcDue.GetValue(),
|
Assert.Equal(newInvoice.BtcDue.ToDecimal(MoneyUnit.BTC),
|
||||||
BOLT11PaymentRequest.Parse(newBolt11, Network.RegTest).MinimumAmount.ToDecimal(LightMoneyUnit.BTC));
|
BOLT11PaymentRequest.Parse(newBolt11, Network.RegTest).MinimumAmount.ToDecimal(LightMoneyUnit.BTC));
|
||||||
}, 40000);
|
}, 40000);
|
||||||
|
|
||||||
TestLogs.LogInformation($"Paying invoice {newInvoice.Id} remaining due amount {newInvoice.BtcDue.GetValue()} via lightning");
|
TestLogs.LogInformation($"Paying invoice {newInvoice.Id} remaining due amount {newInvoice.BtcDue.GetValue((BTCPayNetwork) tester.DefaultNetwork)} via lightning");
|
||||||
var evt = await tester.WaitForEvent<InvoiceDataChangedEvent>(async () =>
|
var evt = await tester.WaitForEvent<InvoiceDataChangedEvent>(async () =>
|
||||||
{
|
{
|
||||||
await tester.SendLightningPaymentAsync(newInvoice);
|
await tester.SendLightningPaymentAsync(newInvoice);
|
||||||
@@ -466,14 +467,6 @@ namespace BTCPayServer.Tests
|
|||||||
await ProcessLightningPayment(LightningConnectionType.CLightning);
|
await ProcessLightningPayment(LightningConnectionType.CLightning);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Timeout = 60 * 2 * 1000)]
|
|
||||||
[Trait("Integration", "Integration")]
|
|
||||||
[Trait("Lightning", "Lightning")]
|
|
||||||
public async Task CanSendLightningPaymentCharge()
|
|
||||||
{
|
|
||||||
await ProcessLightningPayment(LightningConnectionType.Charge);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact(Timeout = 60 * 2 * 1000)]
|
[Fact(Timeout = 60 * 2 * 1000)]
|
||||||
[Trait("Integration", "Integration")]
|
[Trait("Integration", "Integration")]
|
||||||
[Trait("Lightning", "Lightning")]
|
[Trait("Lightning", "Lightning")]
|
||||||
@@ -726,7 +719,7 @@ namespace BTCPayServer.Tests
|
|||||||
btcDerivationScheme.GetDerivation(new KeyPath("0/90")).ScriptPubKey, Money.Coins(1.0m));
|
btcDerivationScheme.GetDerivation(new KeyPath("0/90")).ScriptPubKey, Money.Coins(1.0m));
|
||||||
tester.ExplorerNode.Generate(1);
|
tester.ExplorerNode.Generate(1);
|
||||||
var transactions = Assert.IsType<ListTransactionsViewModel>(Assert
|
var transactions = Assert.IsType<ListTransactionsViewModel>(Assert
|
||||||
.IsType<ViewResult>(walletController.WalletTransactions(walletId).Result).Model);
|
.IsType<ViewResult>(walletController.WalletTransactions(walletId, loadTransactions: true).Result).Model);
|
||||||
Assert.Empty(transactions.Transactions);
|
Assert.Empty(transactions.Transactions);
|
||||||
|
|
||||||
Assert.IsType<RedirectToActionResult>(walletController.WalletRescan(walletId, rescan).Result);
|
Assert.IsType<RedirectToActionResult>(walletController.WalletRescan(walletId, rescan).Result);
|
||||||
@@ -755,7 +748,7 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.NotNull(rescan.TimeOfScan);
|
Assert.NotNull(rescan.TimeOfScan);
|
||||||
Assert.Equal(1, rescan.LastSuccess.Found);
|
Assert.Equal(1, rescan.LastSuccess.Found);
|
||||||
transactions = Assert.IsType<ListTransactionsViewModel>(Assert
|
transactions = Assert.IsType<ListTransactionsViewModel>(Assert
|
||||||
.IsType<ViewResult>(walletController.WalletTransactions(walletId).Result).Model);
|
.IsType<ViewResult>(walletController.WalletTransactions(walletId, loadTransactions: true).Result).Model);
|
||||||
var tx = Assert.Single(transactions.Transactions);
|
var tx = Assert.Single(transactions.Transactions);
|
||||||
Assert.Equal(tx.Id, txId.ToString());
|
Assert.Equal(tx.Id, txId.ToString());
|
||||||
|
|
||||||
@@ -770,7 +763,7 @@ namespace BTCPayServer.Tests
|
|||||||
await walletController.ModifyTransaction(walletId, tx.Id, addcomment: "hello"));
|
await walletController.ModifyTransaction(walletId, tx.Id, addcomment: "hello"));
|
||||||
|
|
||||||
transactions = Assert.IsType<ListTransactionsViewModel>(Assert
|
transactions = Assert.IsType<ListTransactionsViewModel>(Assert
|
||||||
.IsType<ViewResult>(walletController.WalletTransactions(walletId).Result).Model);
|
.IsType<ViewResult>(walletController.WalletTransactions(walletId, loadTransactions: true).Result).Model);
|
||||||
tx = Assert.Single(transactions.Transactions);
|
tx = Assert.Single(transactions.Transactions);
|
||||||
|
|
||||||
Assert.Equal("hello", tx.Comment);
|
Assert.Equal("hello", tx.Comment);
|
||||||
@@ -782,7 +775,7 @@ namespace BTCPayServer.Tests
|
|||||||
await walletController.ModifyTransaction(walletId, tx.Id, removelabel: "test2"));
|
await walletController.ModifyTransaction(walletId, tx.Id, removelabel: "test2"));
|
||||||
|
|
||||||
transactions = Assert.IsType<ListTransactionsViewModel>(Assert
|
transactions = Assert.IsType<ListTransactionsViewModel>(Assert
|
||||||
.IsType<ViewResult>(walletController.WalletTransactions(walletId).Result).Model);
|
.IsType<ViewResult>(walletController.WalletTransactions(walletId, loadTransactions: true).Result).Model);
|
||||||
tx = Assert.Single(transactions.Transactions);
|
tx = Assert.Single(transactions.Transactions);
|
||||||
|
|
||||||
Assert.Equal("hello", tx.Comment);
|
Assert.Equal("hello", tx.Comment);
|
||||||
@@ -1634,7 +1627,7 @@ namespace BTCPayServer.Tests
|
|||||||
var user = tester.NewAccount();
|
var user = tester.NewAccount();
|
||||||
var cryptoCode = "BTC";
|
var cryptoCode = "BTC";
|
||||||
user.GrantAccess(true);
|
user.GrantAccess(true);
|
||||||
user.RegisterLightningNode(cryptoCode, LightningConnectionType.Charge);
|
user.RegisterLightningNode(cryptoCode);
|
||||||
user.SetLNUrl(cryptoCode, false);
|
user.SetLNUrl(cryptoCode, false);
|
||||||
var vm = user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModel<CheckoutAppearanceViewModel>();
|
var vm = user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModel<CheckoutAppearanceViewModel>();
|
||||||
var criteria = Assert.Single(vm.PaymentMethodCriteria);
|
var criteria = Assert.Single(vm.PaymentMethodCriteria);
|
||||||
@@ -1654,7 +1647,7 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.Equal(PaymentTypes.LightningLike.ToString(), invoice.CryptoInfo[0].PaymentType);
|
Assert.Equal(PaymentTypes.LightningLike.ToString(), invoice.CryptoInfo[0].PaymentType);
|
||||||
|
|
||||||
// Activating LNUrl, we should still have only 1 payment criteria that can be set.
|
// Activating LNUrl, we should still have only 1 payment criteria that can be set.
|
||||||
user.RegisterLightningNode(cryptoCode, LightningConnectionType.Charge);
|
user.RegisterLightningNode(cryptoCode);
|
||||||
user.SetLNUrl(cryptoCode, true);
|
user.SetLNUrl(cryptoCode, true);
|
||||||
vm = user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModel<CheckoutAppearanceViewModel>();
|
vm = user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModel<CheckoutAppearanceViewModel>();
|
||||||
criteria = Assert.Single(vm.PaymentMethodCriteria);
|
criteria = Assert.Single(vm.PaymentMethodCriteria);
|
||||||
@@ -1707,109 +1700,6 @@ namespace BTCPayServer.Tests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Timeout = LongRunningTestTimeout)]
|
|
||||||
[Trait("Integration", "Integration")]
|
|
||||||
public async Task CanExportInvoicesJson()
|
|
||||||
{
|
|
||||||
decimal GetFieldValue(string input, string fieldName)
|
|
||||||
{
|
|
||||||
var match = Regex.Match(input, $"\"{fieldName}\":([^,]*)");
|
|
||||||
Assert.True(match.Success);
|
|
||||||
return decimal.Parse(match.Groups[1].Value.Trim(), CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task<object[]> GetExport(TestAccount account, string storeId = null)
|
|
||||||
{
|
|
||||||
var content = await account.GetController<UIInvoiceController>(false)
|
|
||||||
.Export("json", storeId);
|
|
||||||
var result = Assert.IsType<ContentResult>(content);
|
|
||||||
Assert.Equal("application/json", result.ContentType);
|
|
||||||
return JsonConvert.DeserializeObject<object[]>(result.Content ?? "[]");
|
|
||||||
}
|
|
||||||
|
|
||||||
using var tester = CreateServerTester();
|
|
||||||
await tester.StartAsync();
|
|
||||||
var user = tester.NewAccount();
|
|
||||||
await user.GrantAccessAsync();
|
|
||||||
user.RegisterDerivationScheme("BTC");
|
|
||||||
await user.SetNetworkFeeMode(NetworkFeeMode.Always);
|
|
||||||
var invoice = await user.BitPay.CreateInvoiceAsync(
|
|
||||||
new Invoice
|
|
||||||
{
|
|
||||||
Price = 10,
|
|
||||||
Currency = "USD",
|
|
||||||
PosData = "posData",
|
|
||||||
OrderId = "orderId",
|
|
||||||
ItemDesc = "Some \", description",
|
|
||||||
FullNotifications = true
|
|
||||||
}, Facade.Merchant);
|
|
||||||
|
|
||||||
var networkFee = new FeeRate(invoice.MinerFees["BTC"].SatoshiPerBytes).GetFee(100);
|
|
||||||
var result = await GetExport(user);
|
|
||||||
Assert.Single(result);
|
|
||||||
|
|
||||||
var cashCow = tester.ExplorerNode;
|
|
||||||
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
|
|
||||||
var firstPayment = invoice.CryptoInfo[0].TotalDue - 3 * networkFee;
|
|
||||||
cashCow.SendToAddress(invoiceAddress, firstPayment);
|
|
||||||
Thread.Sleep(1000); // prevent race conditions, ordering payments
|
|
||||||
// look if you can reduce thread sleep, this was min value for me
|
|
||||||
|
|
||||||
// should reduce invoice due by 0 USD because payment = network fee
|
|
||||||
cashCow.SendToAddress(invoiceAddress, networkFee);
|
|
||||||
Thread.Sleep(1000);
|
|
||||||
|
|
||||||
// pay remaining amount
|
|
||||||
cashCow.SendToAddress(invoiceAddress, 4 * networkFee);
|
|
||||||
Thread.Sleep(1000);
|
|
||||||
|
|
||||||
await TestUtils.EventuallyAsync(async () =>
|
|
||||||
{
|
|
||||||
var parsedJson = await GetExport(user);
|
|
||||||
Assert.Equal(3, parsedJson.Length);
|
|
||||||
|
|
||||||
var invoiceDueAfterFirstPayment = (3 * networkFee).ToDecimal(MoneyUnit.BTC) * invoice.Rate;
|
|
||||||
var pay1str = parsedJson[0].ToString();
|
|
||||||
Assert.Contains("\"InvoiceItemDesc\": \"Some \\\", description\"", pay1str);
|
|
||||||
Assert.Equal(invoiceDueAfterFirstPayment, GetFieldValue(pay1str, "InvoiceDue"));
|
|
||||||
Assert.Contains("\"InvoicePrice\": 10.0", pay1str);
|
|
||||||
Assert.Contains("\"ConversionRate\": 5000.0", pay1str);
|
|
||||||
Assert.Contains($"\"InvoiceId\": \"{invoice.Id}\",", pay1str);
|
|
||||||
|
|
||||||
var pay2str = parsedJson[1].ToString();
|
|
||||||
Assert.Equal(invoiceDueAfterFirstPayment, GetFieldValue(pay2str, "InvoiceDue"));
|
|
||||||
|
|
||||||
var pay3str = parsedJson[2].ToString();
|
|
||||||
Assert.Contains("\"InvoiceDue\": 0", pay3str);
|
|
||||||
});
|
|
||||||
|
|
||||||
// create an invoice for a new store and check responses with and without store id
|
|
||||||
var otherUser = tester.NewAccount();
|
|
||||||
await otherUser.GrantAccessAsync();
|
|
||||||
otherUser.RegisterDerivationScheme("BTC");
|
|
||||||
await otherUser.SetNetworkFeeMode(NetworkFeeMode.Always);
|
|
||||||
var newInvoice = await otherUser.BitPay.CreateInvoiceAsync(
|
|
||||||
new Invoice
|
|
||||||
{
|
|
||||||
Price = 21,
|
|
||||||
Currency = "USD",
|
|
||||||
PosData = "posData",
|
|
||||||
OrderId = "orderId",
|
|
||||||
ItemDesc = "Some \", description",
|
|
||||||
FullNotifications = true
|
|
||||||
}, Facade.Merchant);
|
|
||||||
|
|
||||||
await otherUser.PayInvoice(newInvoice.Id);
|
|
||||||
Assert.Single(await GetExport(otherUser));
|
|
||||||
Assert.Single(await GetExport(otherUser, otherUser.StoreId));
|
|
||||||
Assert.Equal(3, (await GetExport(user, user.StoreId)).Length);
|
|
||||||
Assert.Equal(3, (await GetExport(user)).Length);
|
|
||||||
|
|
||||||
await otherUser.AddOwner(user.UserId);
|
|
||||||
Assert.Equal(4, (await GetExport(user)).Length);
|
|
||||||
Assert.Single(await GetExport(user, otherUser.StoreId));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact(Timeout = LongRunningTestTimeout)]
|
[Fact(Timeout = LongRunningTestTimeout)]
|
||||||
[Trait("Integration", "Integration")]
|
[Trait("Integration", "Integration")]
|
||||||
public async Task CanChangeNetworkFeeMode()
|
public async Task CanChangeNetworkFeeMode()
|
||||||
@@ -1899,45 +1789,6 @@ namespace BTCPayServer.Tests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Timeout = LongRunningTestTimeout)]
|
|
||||||
[Trait("Integration", "Integration")]
|
|
||||||
public async Task CanExportInvoicesCsv()
|
|
||||||
{
|
|
||||||
using var tester = CreateServerTester();
|
|
||||||
await tester.StartAsync();
|
|
||||||
var user = tester.NewAccount();
|
|
||||||
await user.GrantAccessAsync();
|
|
||||||
user.RegisterDerivationScheme("BTC");
|
|
||||||
await user.SetNetworkFeeMode(NetworkFeeMode.Always);
|
|
||||||
var invoice = user.BitPay.CreateInvoice(
|
|
||||||
new Invoice
|
|
||||||
{
|
|
||||||
Price = 500,
|
|
||||||
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);
|
|
||||||
var firstPayment = invoice.CryptoInfo[0].TotalDue - Money.Coins(0.001m);
|
|
||||||
cashCow.SendToAddress(invoiceAddress, firstPayment);
|
|
||||||
TestUtils.Eventually(() =>
|
|
||||||
{
|
|
||||||
var exportResultPaid =
|
|
||||||
user.GetController<UIInvoiceController>().Export("csv").GetAwaiter().GetResult();
|
|
||||||
var paidresult = Assert.IsType<ContentResult>(exportResultPaid);
|
|
||||||
Assert.Equal("application/csv", paidresult.ContentType);
|
|
||||||
Assert.Contains($",orderId,{invoice.Id},", paidresult.Content);
|
|
||||||
Assert.Contains($",On-Chain,BTC,0.0991,0.0001,5000.0", paidresult.Content);
|
|
||||||
Assert.Contains($",USD,5.00", paidresult.Content); // Seems hacky but some plateform does not render this decimal the same
|
|
||||||
Assert.Contains("0,,\"Some \"\", description\",New (paidPartial),new,paidPartial",
|
|
||||||
paidresult.Content);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact(Timeout = LongRunningTestTimeout)]
|
[Fact(Timeout = LongRunningTestTimeout)]
|
||||||
[Trait("Integration", "Integration")]
|
[Trait("Integration", "Integration")]
|
||||||
public async Task CanCreateAndDeleteApps()
|
public async Task CanCreateAndDeleteApps()
|
||||||
@@ -1970,7 +1821,8 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.Empty(appList2.Apps);
|
Assert.Empty(appList2.Apps);
|
||||||
Assert.Equal("test", appList.Apps[0].AppName);
|
Assert.Equal("test", appList.Apps[0].AppName);
|
||||||
Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id);
|
Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id);
|
||||||
Assert.True(app.IsOwner);
|
|
||||||
|
Assert.True(app.Role.ToPermissionSet(app.StoreId).Contains(Policies.CanModifyStoreSettings, app.StoreId));
|
||||||
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
|
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
|
||||||
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id));
|
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id));
|
||||||
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id));
|
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id));
|
||||||
@@ -1991,6 +1843,7 @@ namespace BTCPayServer.Tests
|
|||||||
var user = tester.NewAccount();
|
var user = tester.NewAccount();
|
||||||
user.GrantAccess(true);
|
user.GrantAccess(true);
|
||||||
user.RegisterDerivationScheme("BTC");
|
user.RegisterDerivationScheme("BTC");
|
||||||
|
var btcpayClient = await user.CreateClient();
|
||||||
|
|
||||||
DateTimeOffset expiration = DateTimeOffset.UtcNow + TimeSpan.FromMinutes(21);
|
DateTimeOffset expiration = DateTimeOffset.UtcNow + TimeSpan.FromMinutes(21);
|
||||||
|
|
||||||
@@ -2071,6 +1924,20 @@ namespace BTCPayServer.Tests
|
|||||||
|
|
||||||
var zeroInvoicePM = await greenfield.GetInvoicePaymentMethods(user.StoreId, zeroInvoice.Id);
|
var zeroInvoicePM = await greenfield.GetInvoicePaymentMethods(user.StoreId, zeroInvoice.Id);
|
||||||
Assert.Empty(zeroInvoicePM);
|
Assert.Empty(zeroInvoicePM);
|
||||||
|
|
||||||
|
var invoice6 = await btcpayClient.CreateInvoice(user.StoreId,
|
||||||
|
new CreateInvoiceRequest()
|
||||||
|
{
|
||||||
|
Amount = GreenfieldConstants.MaxAmount,
|
||||||
|
Currency = "USD"
|
||||||
|
});
|
||||||
|
var repo = tester.PayTester.GetService<InvoiceRepository>();
|
||||||
|
var entity = (await repo.GetInvoice(invoice6.Id));
|
||||||
|
Assert.Equal((decimal)ulong.MaxValue, entity.Price);
|
||||||
|
entity.GetPaymentMethods().First().Calculate();
|
||||||
|
// Shouldn't be possible as we clamp the value, but existing invoice may have that
|
||||||
|
entity.Price = decimal.MaxValue;
|
||||||
|
entity.GetPaymentMethods().First().Calculate();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Timeout = LongRunningTestTimeout)]
|
[Fact(Timeout = LongRunningTestTimeout)]
|
||||||
@@ -2154,7 +2021,7 @@ namespace BTCPayServer.Tests
|
|||||||
txFee = localInvoice.BtcDue - invoice.BtcDue;
|
txFee = localInvoice.BtcDue - invoice.BtcDue;
|
||||||
Assert.Equal("paidPartial", localInvoice.ExceptionStatus.ToString());
|
Assert.Equal("paidPartial", localInvoice.ExceptionStatus.ToString());
|
||||||
Assert.Equal(1, localInvoice.CryptoInfo[0].TxCount);
|
Assert.Equal(1, localInvoice.CryptoInfo[0].TxCount);
|
||||||
Assert.NotEqual(localInvoice.BitcoinAddress, invoice.BitcoinAddress); //New address
|
Assert.Equal(localInvoice.BitcoinAddress, invoice.BitcoinAddress); //Same address
|
||||||
Assert.True(IsMapped(invoice, ctx));
|
Assert.True(IsMapped(invoice, ctx));
|
||||||
Assert.True(IsMapped(localInvoice, ctx));
|
Assert.True(IsMapped(localInvoice, ctx));
|
||||||
|
|
||||||
@@ -2850,7 +2717,7 @@ namespace BTCPayServer.Tests
|
|||||||
using var tester = CreateServerTester();
|
using var tester = CreateServerTester();
|
||||||
await tester.StartAsync();
|
await tester.StartAsync();
|
||||||
var user = tester.NewAccount();
|
var user = tester.NewAccount();
|
||||||
user.GrantAccess();
|
await user.GrantAccessAsync();
|
||||||
var controller = tester.PayTester.GetController<UIServerController>(user.UserId, user.StoreId);
|
var controller = tester.PayTester.GetController<UIServerController>(user.UserId, user.StoreId);
|
||||||
|
|
||||||
var fileSystemStorageConfiguration = Assert.IsType<FileSystemStorageConfiguration>(Assert
|
var fileSystemStorageConfiguration = Assert.IsType<FileSystemStorageConfiguration>(Assert
|
||||||
@@ -2865,7 +2732,6 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.Equal(StorageProvider.FileSystem,
|
Assert.Equal(StorageProvider.FileSystem,
|
||||||
shouldBeRedirectingToLocalStorageConfigPage.RouteValues["provider"]);
|
shouldBeRedirectingToLocalStorageConfigPage.RouteValues["provider"]);
|
||||||
|
|
||||||
|
|
||||||
await CanUploadRemoveFiles(controller);
|
await CanUploadRemoveFiles(controller);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2897,7 +2763,7 @@ namespace BTCPayServer.Tests
|
|||||||
|
|
||||||
//create a temporary link to file
|
//create a temporary link to file
|
||||||
var tmpLinkGenerate = Assert.IsType<RedirectToActionResult>(await controller.CreateTemporaryFileUrl(fileId,
|
var tmpLinkGenerate = Assert.IsType<RedirectToActionResult>(await controller.CreateTemporaryFileUrl(fileId,
|
||||||
new UIServerController.CreateTemporaryFileUrlViewModel()
|
new UIServerController.CreateTemporaryFileUrlViewModel
|
||||||
{
|
{
|
||||||
IsDownload = true,
|
IsDownload = true,
|
||||||
TimeAmount = 1,
|
TimeAmount = 1,
|
||||||
@@ -2927,5 +2793,124 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files(new string[] { fileId })).Model);
|
Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files(new string[] { fileId })).Model);
|
||||||
Assert.Null(viewFilesViewModel.DirectUrlByFiles);
|
Assert.Null(viewFilesViewModel.DirectUrlByFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Selenium", "Selenium")]
|
||||||
|
public async Task CanCreateReports()
|
||||||
|
{
|
||||||
|
using var tester = CreateServerTester();
|
||||||
|
tester.ActivateLightning();
|
||||||
|
tester.DeleteStore = false;
|
||||||
|
await tester.StartAsync();
|
||||||
|
await tester.EnsureChannelsSetup();
|
||||||
|
var acc = tester.NewAccount();
|
||||||
|
await acc.GrantAccessAsync();
|
||||||
|
await acc.MakeAdmin();
|
||||||
|
acc.RegisterDerivationScheme("BTC", importKeysToNBX: true);
|
||||||
|
acc.RegisterLightningNode("BTC");
|
||||||
|
await acc.ReceiveUTXO(Money.Coins(1.0m));
|
||||||
|
|
||||||
|
var client = await acc.CreateClient();
|
||||||
|
var posController = acc.GetController<UIPointOfSaleController>();
|
||||||
|
|
||||||
|
var app = await client.CreatePointOfSaleApp(acc.StoreId, new CreatePointOfSaleAppRequest()
|
||||||
|
{
|
||||||
|
AppName = "Static",
|
||||||
|
DefaultView = Client.Models.PosViewType.Static,
|
||||||
|
Template = new PointOfSaleSettings().Template
|
||||||
|
});
|
||||||
|
var resp = await posController.ViewPointOfSale(app.Id, choiceKey: "green-tea");
|
||||||
|
var invoiceId = GetInvoiceId(resp);
|
||||||
|
await acc.PayOnChain(invoiceId);
|
||||||
|
|
||||||
|
app = await client.CreatePointOfSaleApp(acc.StoreId, new CreatePointOfSaleAppRequest()
|
||||||
|
{
|
||||||
|
AppName = "Cart",
|
||||||
|
DefaultView = Client.Models.PosViewType.Cart,
|
||||||
|
Template = new PointOfSaleSettings().Template
|
||||||
|
});
|
||||||
|
resp = await posController.ViewPointOfSale(app.Id, posData: new JObject()
|
||||||
|
{
|
||||||
|
["cart"] = new JArray()
|
||||||
|
{
|
||||||
|
new JObject()
|
||||||
|
{
|
||||||
|
["id"] = "green-tea",
|
||||||
|
["count"] = 2
|
||||||
|
},
|
||||||
|
new JObject()
|
||||||
|
{
|
||||||
|
["id"] = "black-tea",
|
||||||
|
["count"] = 1
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}.ToString());
|
||||||
|
invoiceId = GetInvoiceId(resp);
|
||||||
|
await acc.PayOnBOLT11(invoiceId);
|
||||||
|
|
||||||
|
resp = await posController.ViewPointOfSale(app.Id, posData: new JObject()
|
||||||
|
{
|
||||||
|
["cart"] = new JArray()
|
||||||
|
{
|
||||||
|
new JObject()
|
||||||
|
{
|
||||||
|
["id"] = "green-tea",
|
||||||
|
["count"] = 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.ToString());
|
||||||
|
invoiceId = GetInvoiceId(resp);
|
||||||
|
await acc.PayOnLNUrl(invoiceId);
|
||||||
|
|
||||||
|
await acc.CreateLNAddress();
|
||||||
|
await acc.PayOnLNAddress();
|
||||||
|
|
||||||
|
var report = await GetReport(acc, new() { ViewName = "Payments" });
|
||||||
|
// 1 payment on LN Address
|
||||||
|
// 1 payment on LNURL
|
||||||
|
// 1 payment on BOLT11
|
||||||
|
// 1 payment on chain
|
||||||
|
Assert.Equal(4, report.Data.Count);
|
||||||
|
var lnAddressIndex = report.GetIndex("LightningAddress");
|
||||||
|
var paymentTypeIndex = report.GetIndex("PaymentType");
|
||||||
|
Assert.Contains(report.Data, d => d[lnAddressIndex]?.Value<string>()?.Contains(acc.LNAddress) is true);
|
||||||
|
var paymentTypes = report.Data
|
||||||
|
.GroupBy(d => d[paymentTypeIndex].Value<string>())
|
||||||
|
.ToDictionary(d => d.Key);
|
||||||
|
Assert.Equal(3, paymentTypes["Lightning"].Count());
|
||||||
|
Assert.Single(paymentTypes["On-Chain"]);
|
||||||
|
|
||||||
|
// 2 on-chain transactions: It received from the cashcow, then paid its own invoice
|
||||||
|
report = await GetReport(acc, new() { ViewName = "On-Chain Wallets" });
|
||||||
|
var txIdIndex = report.GetIndex("TransactionId");
|
||||||
|
var balanceIndex = report.GetIndex("BalanceChange");
|
||||||
|
Assert.Equal(2, report.Data.Count);
|
||||||
|
Assert.Equal(64, report.Data[0][txIdIndex].Value<string>().Length);
|
||||||
|
Assert.Contains(report.Data, d => d[balanceIndex]["v"].Value<decimal>() == 1.0m);
|
||||||
|
|
||||||
|
// Items sold
|
||||||
|
report = await GetReport(acc, new() { ViewName = "Products sold" });
|
||||||
|
var itemIndex = report.GetIndex("Product");
|
||||||
|
var countIndex = report.GetIndex("Quantity");
|
||||||
|
var itemsCount = report.Data.GroupBy(d => d[itemIndex].Value<string>())
|
||||||
|
.ToDictionary(d => d.Key, r => r.Sum(d => d[countIndex].Value<int>()));
|
||||||
|
Assert.Equal(8, itemsCount["green-tea"]);
|
||||||
|
Assert.Equal(1, itemsCount["black-tea"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<StoreReportResponse> GetReport(TestAccount acc, StoreReportRequest req)
|
||||||
|
{
|
||||||
|
var controller = acc.GetController<UIReportsController>();
|
||||||
|
return (await controller.StoreReportsJson(acc.StoreId, req)).AssertType<JsonResult>()
|
||||||
|
.Value
|
||||||
|
.AssertType<StoreReportResponse>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetInvoiceId(IActionResult resp)
|
||||||
|
{
|
||||||
|
var redirect = resp.AssertType<RedirectToActionResult>();
|
||||||
|
Assert.Equal("Checkout", redirect.ActionName);
|
||||||
|
return (string)redirect.RouteValues["invoiceId"];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -24,7 +24,6 @@ services:
|
|||||||
TESTS_AzureBlobStorageConnectionString: ${TESTS_AzureBlobStorageConnectionString:-none}
|
TESTS_AzureBlobStorageConnectionString: ${TESTS_AzureBlobStorageConnectionString:-none}
|
||||||
TEST_MERCHANTLIGHTNINGD: "type=clightning;server=unix://etc/merchant_lightningd_datadir/lightning-rpc"
|
TEST_MERCHANTLIGHTNINGD: "type=clightning;server=unix://etc/merchant_lightningd_datadir/lightning-rpc"
|
||||||
TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=unix://etc/customer_lightningd_datadir/lightning-rpc"
|
TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=unix://etc/customer_lightningd_datadir/lightning-rpc"
|
||||||
TEST_MERCHANTCHARGE: "type=charge;server=http://lightning-charged:9112/;api-token=foiewnccewuify;allowinsecure=true"
|
|
||||||
TEST_MERCHANTLND: "http://lnd:lnd@merchant_lnd:8080/"
|
TEST_MERCHANTLND: "http://lnd:lnd@merchant_lnd:8080/"
|
||||||
TESTS_INCONTAINER: "true"
|
TESTS_INCONTAINER: "true"
|
||||||
TESTS_SSHCONNECTION: "root@sshd:22"
|
TESTS_SSHCONNECTION: "root@sshd:22"
|
||||||
@@ -56,7 +55,6 @@ services:
|
|||||||
- postgres
|
- postgres
|
||||||
- customer_lightningd
|
- customer_lightningd
|
||||||
- merchant_lightningd
|
- merchant_lightningd
|
||||||
- lightning-charged
|
|
||||||
- customer_lnd
|
- customer_lnd
|
||||||
- merchant_lnd
|
- merchant_lnd
|
||||||
- sshd
|
- sshd
|
||||||
@@ -75,7 +73,7 @@ services:
|
|||||||
- "sshd_datadir:/root/.ssh"
|
- "sshd_datadir:/root/.ssh"
|
||||||
|
|
||||||
devlnd:
|
devlnd:
|
||||||
image: btcpayserver/bitcoin:24.0
|
image: btcpayserver/bitcoin:25.0
|
||||||
environment:
|
environment:
|
||||||
BITCOIN_NETWORK: regtest
|
BITCOIN_NETWORK: regtest
|
||||||
BITCOIN_WALLETDIR: "/data/wallets"
|
BITCOIN_WALLETDIR: "/data/wallets"
|
||||||
@@ -89,14 +87,19 @@ services:
|
|||||||
- postgres
|
- postgres
|
||||||
- customer_lnd
|
- customer_lnd
|
||||||
- merchant_lnd
|
- merchant_lnd
|
||||||
|
|
||||||
selenium:
|
selenium:
|
||||||
image: selenium/standalone-chrome:101.0
|
image: selenium/standalone-chrome:101.0
|
||||||
|
extra_hosts:
|
||||||
|
- "tests:172.23.0.18"
|
||||||
expose:
|
expose:
|
||||||
- "4444"
|
- "4444"
|
||||||
extra_hosts:
|
networks:
|
||||||
- "tests:172.18.0.18"
|
default:
|
||||||
|
custom:
|
||||||
|
|
||||||
nbxplorer:
|
nbxplorer:
|
||||||
image: nicolasdorier/nbxplorer:2.3.58
|
image: nicolasdorier/nbxplorer:2.3.66
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "32838:32838"
|
- "32838:32838"
|
||||||
@@ -132,7 +135,7 @@ services:
|
|||||||
|
|
||||||
bitcoind:
|
bitcoind:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
image: btcpayserver/bitcoin:24.0
|
image: btcpayserver/bitcoin:25.0
|
||||||
environment:
|
environment:
|
||||||
BITCOIN_NETWORK: regtest
|
BITCOIN_NETWORK: regtest
|
||||||
BITCOIN_WALLETDIR: "/data/wallets"
|
BITCOIN_WALLETDIR: "/data/wallets"
|
||||||
@@ -160,7 +163,7 @@ services:
|
|||||||
- "bitcoin_datadir:/data"
|
- "bitcoin_datadir:/data"
|
||||||
|
|
||||||
customer_lightningd:
|
customer_lightningd:
|
||||||
image: btcpayserver/lightning:v23.02-1-dev
|
image: btcpayserver/lightning:v23.08-dev
|
||||||
stop_signal: SIGKILL
|
stop_signal: SIGKILL
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
@@ -186,30 +189,8 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- bitcoind
|
- bitcoind
|
||||||
|
|
||||||
lightning-charged:
|
|
||||||
image: shesek/lightning-charge:0.4.23-1-standalone
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
NETWORK: regtest
|
|
||||||
API_TOKEN: foiewnccewuify
|
|
||||||
BITCOIND_RPCCONNECT: bitcoind
|
|
||||||
LN_NET_PATH: /etc/lightning
|
|
||||||
LN_NET: /etc/lightning
|
|
||||||
volumes:
|
|
||||||
- "bitcoin_datadir:/etc/bitcoin"
|
|
||||||
- "lightning_charge_datadir:/data"
|
|
||||||
- "merchant_lightningd_datadir:/etc/lightning"
|
|
||||||
expose:
|
|
||||||
- "9112" # Charge
|
|
||||||
- "9735" # Lightning
|
|
||||||
ports:
|
|
||||||
- "54938:9112" # Charge
|
|
||||||
depends_on:
|
|
||||||
- bitcoind
|
|
||||||
- merchant_lightningd
|
|
||||||
|
|
||||||
merchant_lightningd:
|
merchant_lightningd:
|
||||||
image: btcpayserver/lightning:v23.02-1-dev
|
image: btcpayserver/lightning:v23.08-dev
|
||||||
stop_signal: SIGKILL
|
stop_signal: SIGKILL
|
||||||
environment:
|
environment:
|
||||||
EXPOSE_TCP: "true"
|
EXPOSE_TCP: "true"
|
||||||
@@ -243,7 +224,7 @@ services:
|
|||||||
- "5432"
|
- "5432"
|
||||||
|
|
||||||
merchant_lnd:
|
merchant_lnd:
|
||||||
image: btcpayserver/lnd:v0.16.2-beta
|
image: btcpayserver/lnd:v0.16.4-beta-1
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
LND_CHAIN: "btc"
|
LND_CHAIN: "btc"
|
||||||
@@ -278,7 +259,7 @@ services:
|
|||||||
- bitcoind
|
- bitcoind
|
||||||
|
|
||||||
customer_lnd:
|
customer_lnd:
|
||||||
image: btcpayserver/lnd:v0.16.2-beta
|
image: btcpayserver/lnd:v0.16.4-beta-1
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
LND_CHAIN: "btc"
|
LND_CHAIN: "btc"
|
||||||
@@ -326,7 +307,7 @@ services:
|
|||||||
- "torrcdir:/usr/local/etc/tor"
|
- "torrcdir:/usr/local/etc/tor"
|
||||||
- "tor_servicesdir:/var/lib/tor/hidden_services"
|
- "tor_servicesdir:/var/lib/tor/hidden_services"
|
||||||
monerod:
|
monerod:
|
||||||
image: btcpayserver/monero:0.17.0.0-amd64
|
image: btcpayserver/monero:0.18.2.2-5
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
container_name: xmr_monerod
|
container_name: xmr_monerod
|
||||||
entrypoint: sleep 999999
|
entrypoint: sleep 999999
|
||||||
@@ -336,7 +317,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "18081:18081"
|
- "18081:18081"
|
||||||
monero_wallet:
|
monero_wallet:
|
||||||
image: btcpayserver/monero:0.17.0.0-amd64
|
image: btcpayserver/monero:0.18.2.2-5
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
container_name: xmr_wallet_rpc
|
container_name: xmr_wallet_rpc
|
||||||
entrypoint: monero-wallet-rpc --testnet --rpc-bind-ip=0.0.0.0 --disable-rpc-login --confirm-external-bind --rpc-bind-port=18082 --non-interactive --trusted-daemon --daemon-address=monerod:18081 --wallet-file=/wallet/wallet.keys --password-file=/wallet/password --tx-notify="/bin/sh ./scripts/notifier.sh -k -X GET https://host.docker.internal:14142/monerolikedaemoncallback/tx?cryptoCode=xmr&hash=%s"
|
entrypoint: monero-wallet-rpc --testnet --rpc-bind-ip=0.0.0.0 --disable-rpc-login --confirm-external-bind --rpc-bind-port=18082 --non-interactive --trusted-daemon --daemon-address=monerod:18081 --wallet-file=/wallet/wallet.keys --password-file=/wallet/password --tx-notify="/bin/sh ./scripts/notifier.sh -k -X GET https://host.docker.internal:14142/monerolikedaemoncallback/tx?cryptoCode=xmr&hash=%s"
|
||||||
@@ -368,7 +349,7 @@ services:
|
|||||||
elementsd-liquid:
|
elementsd-liquid:
|
||||||
restart: always
|
restart: always
|
||||||
container_name: btcpayserver_elementsd_liquid
|
container_name: btcpayserver_elementsd_liquid
|
||||||
image: btcpayserver/elements:0.21.0.1
|
image: btcpayserver/elements:0.21.0.2-4
|
||||||
environment:
|
environment:
|
||||||
ELEMENTS_CHAIN: elementsregtest
|
ELEMENTS_CHAIN: elementsregtest
|
||||||
ELEMENTS_EXTRA_ARGS: |
|
ELEMENTS_EXTRA_ARGS: |
|
||||||
@@ -383,11 +364,9 @@ services:
|
|||||||
whitelist=0.0.0.0/0
|
whitelist=0.0.0.0/0
|
||||||
rpcallowip=0.0.0.0/0
|
rpcallowip=0.0.0.0/0
|
||||||
validatepegin=0
|
validatepegin=0
|
||||||
initialfreecoins=210000000000000
|
initialfreecoins=2100000000000000
|
||||||
con_dyna_deploy_signal=1
|
con_dyna_deploy_signal=1
|
||||||
con_dyna_deploy_start=0
|
con_dyna_deploy_start=10
|
||||||
con_nminerconfirmationwindow=1
|
|
||||||
con_nrulechangeactivationthreshold=1
|
|
||||||
expose:
|
expose:
|
||||||
- "19332"
|
- "19332"
|
||||||
- "19444"
|
- "19444"
|
||||||
|
@@ -22,7 +22,6 @@ services:
|
|||||||
TESTS_AzureBlobStorageConnectionString: ${TESTS_AzureBlobStorageConnectionString:-none}
|
TESTS_AzureBlobStorageConnectionString: ${TESTS_AzureBlobStorageConnectionString:-none}
|
||||||
TEST_MERCHANTLIGHTNINGD: "type=clightning;server=unix://etc/merchant_lightningd_datadir/lightning-rpc"
|
TEST_MERCHANTLIGHTNINGD: "type=clightning;server=unix://etc/merchant_lightningd_datadir/lightning-rpc"
|
||||||
TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=unix://etc/customer_lightningd_datadir/lightning-rpc"
|
TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=unix://etc/customer_lightningd_datadir/lightning-rpc"
|
||||||
TEST_MERCHANTCHARGE: "type=charge;server=http://lightning-charged:9112/;api-token=foiewnccewuify;allowinsecure=true"
|
|
||||||
TEST_MERCHANTLND: "http://lnd:lnd@merchant_lnd:8080/"
|
TEST_MERCHANTLND: "http://lnd:lnd@merchant_lnd:8080/"
|
||||||
TESTS_INCONTAINER: "true"
|
TESTS_INCONTAINER: "true"
|
||||||
TESTS_SSHCONNECTION: "root@sshd:22"
|
TESTS_SSHCONNECTION: "root@sshd:22"
|
||||||
@@ -54,7 +53,6 @@ services:
|
|||||||
- postgres
|
- postgres
|
||||||
- customer_lightningd
|
- customer_lightningd
|
||||||
- merchant_lightningd
|
- merchant_lightningd
|
||||||
- lightning-charged
|
|
||||||
- customer_lnd
|
- customer_lnd
|
||||||
- merchant_lnd
|
- merchant_lnd
|
||||||
- sshd
|
- sshd
|
||||||
@@ -72,28 +70,33 @@ services:
|
|||||||
- "sshd_datadir:/root/.ssh"
|
- "sshd_datadir:/root/.ssh"
|
||||||
|
|
||||||
devlnd:
|
devlnd:
|
||||||
image: btcpayserver/bitcoin:24.0
|
image: btcpayserver/bitcoin:25.0
|
||||||
environment:
|
environment:
|
||||||
BITCOIN_NETWORK: regtest
|
BITCOIN_NETWORK: regtest
|
||||||
BITCOIN_WALLETDIR: "/data/wallets"
|
BITCOIN_WALLETDIR: "/data/wallets"
|
||||||
BITCOIN_EXTRA_ARGS: |
|
BITCOIN_EXTRA_ARGS: |
|
||||||
deprecatedrpc=signrawtransaction
|
deprecatedrpc=signrawtransaction
|
||||||
connect=bitcoind:39388
|
connect=bitcoind:39388
|
||||||
rpcallowip=0.0.0.0/0
|
|
||||||
fallbackfee=0.0002
|
fallbackfee=0.0002
|
||||||
|
rpcallowip=0.0.0.0/0
|
||||||
depends_on:
|
depends_on:
|
||||||
- nbxplorer
|
- nbxplorer
|
||||||
- postgres
|
- postgres
|
||||||
- customer_lnd
|
- customer_lnd
|
||||||
- merchant_lnd
|
- merchant_lnd
|
||||||
|
|
||||||
selenium:
|
selenium:
|
||||||
image: selenium/standalone-chrome:101.0
|
image: selenium/standalone-chrome:101.0
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- "tests:172.18.0.18"
|
- "tests:172.23.0.18"
|
||||||
expose:
|
expose:
|
||||||
- "4444"
|
- "4444"
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
custom:
|
||||||
|
|
||||||
nbxplorer:
|
nbxplorer:
|
||||||
image: nicolasdorier/nbxplorer:2.3.58
|
image: nicolasdorier/nbxplorer:2.3.66
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "32838:32838"
|
- "32838:32838"
|
||||||
@@ -118,7 +121,7 @@ services:
|
|||||||
|
|
||||||
bitcoind:
|
bitcoind:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
image: btcpayserver/bitcoin:24.0
|
image: btcpayserver/bitcoin:25.0
|
||||||
environment:
|
environment:
|
||||||
BITCOIN_NETWORK: regtest
|
BITCOIN_NETWORK: regtest
|
||||||
BITCOIN_WALLETDIR: "/data/wallets"
|
BITCOIN_WALLETDIR: "/data/wallets"
|
||||||
@@ -146,7 +149,7 @@ services:
|
|||||||
- "bitcoin_datadir:/data"
|
- "bitcoin_datadir:/data"
|
||||||
|
|
||||||
customer_lightningd:
|
customer_lightningd:
|
||||||
image: btcpayserver/lightning:v23.02-1-dev
|
image: btcpayserver/lightning:v23.08-dev
|
||||||
stop_signal: SIGKILL
|
stop_signal: SIGKILL
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
@@ -172,30 +175,8 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- bitcoind
|
- bitcoind
|
||||||
|
|
||||||
lightning-charged:
|
|
||||||
image: shesek/lightning-charge:0.4.23-1-standalone
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
NETWORK: regtest
|
|
||||||
API_TOKEN: foiewnccewuify
|
|
||||||
BITCOIND_RPCCONNECT: bitcoind
|
|
||||||
LN_NET_PATH: /etc/lightning
|
|
||||||
LN_NET: /etc/lightning
|
|
||||||
volumes:
|
|
||||||
- "bitcoin_datadir:/etc/bitcoin"
|
|
||||||
- "lightning_charge_datadir:/data"
|
|
||||||
- "merchant_lightningd_datadir:/etc/lightning"
|
|
||||||
expose:
|
|
||||||
- "9112" # Charge
|
|
||||||
- "9735" # Lightning
|
|
||||||
ports:
|
|
||||||
- "54938:9112" # Charge
|
|
||||||
depends_on:
|
|
||||||
- bitcoind
|
|
||||||
- merchant_lightningd
|
|
||||||
|
|
||||||
merchant_lightningd:
|
merchant_lightningd:
|
||||||
image: btcpayserver/lightning:v23.02-1-dev
|
image: btcpayserver/lightning:v23.08-dev
|
||||||
stop_signal: SIGKILL
|
stop_signal: SIGKILL
|
||||||
environment:
|
environment:
|
||||||
EXPOSE_TCP: "true"
|
EXPOSE_TCP: "true"
|
||||||
@@ -230,7 +211,7 @@ services:
|
|||||||
- "5432"
|
- "5432"
|
||||||
|
|
||||||
merchant_lnd:
|
merchant_lnd:
|
||||||
image: btcpayserver/lnd:v0.16.2-beta
|
image: btcpayserver/lnd:v0.16.4-beta-1
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
LND_CHAIN: "btc"
|
LND_CHAIN: "btc"
|
||||||
@@ -267,7 +248,7 @@ services:
|
|||||||
- bitcoind
|
- bitcoind
|
||||||
|
|
||||||
customer_lnd:
|
customer_lnd:
|
||||||
image: btcpayserver/lnd:v0.16.2-beta
|
image: btcpayserver/lnd:v0.16.4-beta-1
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
LND_CHAIN: "btc"
|
LND_CHAIN: "btc"
|
||||||
|
@@ -45,21 +45,22 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="YamlDotNet" Version="8.0.0" />
|
||||||
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
|
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
|
||||||
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
|
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
|
||||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.4.23" />
|
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.4.31" />
|
||||||
<PackageReference Include="CsvHelper" Version="15.0.5" />
|
<PackageReference Include="CsvHelper" Version="15.0.5" />
|
||||||
<PackageReference Include="Dapper" Version="2.0.123" />
|
<PackageReference Include="Dapper" Version="2.0.123" />
|
||||||
<PackageReference Include="Fido2" Version="2.0.2" />
|
<PackageReference Include="Fido2" Version="2.0.2" />
|
||||||
<PackageReference Include="Fido2.AspNet" Version="2.0.2" />
|
<PackageReference Include="Fido2.AspNet" Version="2.0.2" />
|
||||||
<PackageReference Include="HtmlSanitizer" Version="5.0.372" />
|
<PackageReference Include="HtmlSanitizer" Version="5.0.372" />
|
||||||
<PackageReference Include="LNURL" Version="0.0.29" />
|
<PackageReference Include="LNURL" Version="0.0.34" />
|
||||||
<PackageReference Include="MailKit" Version="3.3.0" />
|
<PackageReference Include="MailKit" Version="3.3.0" />
|
||||||
<PackageReference Include="BTCPayServer.NETCore.Plugins.Mvc" Version="1.4.4" />
|
<PackageReference Include="BTCPayServer.NETCore.Plugins.Mvc" Version="1.4.4" />
|
||||||
<PackageReference Include="QRCoder" Version="1.4.3" />
|
<PackageReference Include="QRCoder" Version="1.4.3" />
|
||||||
<PackageReference Include="System.IO.Pipelines" Version="6.0.3" />
|
<PackageReference Include="System.IO.Pipelines" Version="6.0.3" />
|
||||||
<PackageReference Include="NBitpayClient" Version="1.0.0.39" />
|
<PackageReference Include="NBitpayClient" Version="1.0.0.39" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="NicolasDorier.CommandLine" Version="2.0.0" />
|
<PackageReference Include="NicolasDorier.CommandLine" Version="2.0.0" />
|
||||||
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="2.0.0" />
|
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="2.0.0" />
|
||||||
<PackageReference Include="NicolasDorier.RateLimits" Version="1.2.3" />
|
<PackageReference Include="NicolasDorier.RateLimits" Version="1.2.3" />
|
||||||
@@ -75,12 +76,12 @@
|
|||||||
<PackageReference Include="TwentyTwenty.Storage.Azure" Version="2.12.1" />
|
<PackageReference Include="TwentyTwenty.Storage.Azure" Version="2.12.1" />
|
||||||
<PackageReference Include="TwentyTwenty.Storage.Google" Version="2.12.1" />
|
<PackageReference Include="TwentyTwenty.Storage.Google" Version="2.12.1" />
|
||||||
<PackageReference Include="TwentyTwenty.Storage.Local" Version="2.12.1" />
|
<PackageReference Include="TwentyTwenty.Storage.Local" Version="2.12.1" />
|
||||||
<PackageReference Include="YamlDotNet" Version="8.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.9" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.9" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.9" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.9" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<None Include="Views\UIReports\StoreReports.cshtml" />
|
||||||
<None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.svg" />
|
<None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.svg" />
|
||||||
<None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.woff2" />
|
<None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.woff2" />
|
||||||
<None Include="wwwroot\vendor\font-awesome\less\animated.less" />
|
<None Include="wwwroot\vendor\font-awesome\less\animated.less" />
|
||||||
@@ -111,9 +112,6 @@
|
|||||||
<None Include="wwwroot\vendor\font-awesome\scss\_screen-reader.scss" />
|
<None Include="wwwroot\vendor\font-awesome\scss\_screen-reader.scss" />
|
||||||
<None Include="wwwroot\vendor\font-awesome\scss\_stacked.scss" />
|
<None Include="wwwroot\vendor\font-awesome\scss\_stacked.scss" />
|
||||||
<None Include="wwwroot\vendor\font-awesome\scss\_variables.scss" />
|
<None Include="wwwroot\vendor\font-awesome\scss\_variables.scss" />
|
||||||
<None Include="wwwroot\vendor\jquery-easing\jquery.easing.compatibility.js" />
|
|
||||||
<None Include="wwwroot\vendor\jquery-easing\jquery.easing.js" />
|
|
||||||
<None Include="wwwroot\vendor\jquery-easing\jquery.easing.min.js" />
|
|
||||||
<None Include="wwwroot\vendor\jquery\jquery.js" />
|
<None Include="wwwroot\vendor\jquery\jquery.js" />
|
||||||
<None Include="wwwroot\vendor\jquery\jquery.min.js" />
|
<None Include="wwwroot\vendor\jquery\jquery.min.js" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -122,6 +120,7 @@
|
|||||||
<Folder Include="wwwroot\vendor\bootstrap" />
|
<Folder Include="wwwroot\vendor\bootstrap" />
|
||||||
<Folder Include="wwwroot\vendor\clipboard.js\" />
|
<Folder Include="wwwroot\vendor\clipboard.js\" />
|
||||||
<Folder Include="wwwroot\vendor\highlightjs\" />
|
<Folder Include="wwwroot\vendor\highlightjs\" />
|
||||||
|
<Folder Include="wwwroot\vendor\pivottable\" />
|
||||||
<Folder Include="wwwroot\vendor\summernote" />
|
<Folder Include="wwwroot\vendor\summernote" />
|
||||||
<Folder Include="wwwroot\vendor\tom-select" />
|
<Folder Include="wwwroot\vendor\tom-select" />
|
||||||
<Folder Include="wwwroot\vendor\ur-registry" />
|
<Folder Include="wwwroot\vendor\ur-registry" />
|
||||||
@@ -139,6 +138,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Watch Include="Views\**\*.*"></Watch>
|
<Watch Include="Views\**\*.*"></Watch>
|
||||||
<Watch Remove="Views\UIAccount\CheatPermissions.cshtml" />
|
<Watch Remove="Views\UIAccount\CheatPermissions.cshtml" />
|
||||||
|
<Watch Remove="Views\UIReports\StoreReports.cshtml" />
|
||||||
<Content Update="Views\UIApps\_ViewImports.cshtml">
|
<Content Update="Views\UIApps\_ViewImports.cshtml">
|
||||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||||
|
14
BTCPayServer/Blazor/BlazorExtensions.cs
Normal file
14
BTCPayServer/Blazor/BlazorExtensions.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Blazor
|
||||||
|
{
|
||||||
|
public static class BlazorExtensions
|
||||||
|
{
|
||||||
|
public static bool IsPreRendering(this IJSRuntime runtime)
|
||||||
|
{
|
||||||
|
// The peculiar thing in prerender is that Blazor circuit isn't yet created, so we can't use JSInterop
|
||||||
|
return !(bool)runtime.GetType().GetProperty("IsInitialized").GetValue(runtime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
BTCPayServer/Blazor/Icon.razor
Normal file
22
BTCPayServer/Blazor/Icon.razor
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
@using BTCPayServer.Abstractions.Extensions;
|
||||||
|
@using BTCPayServer.Configuration;
|
||||||
|
@using Microsoft.AspNetCore.Hosting;
|
||||||
|
@using Microsoft.AspNetCore.Mvc.Routing;
|
||||||
|
@using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||||
|
@using Microsoft.AspNetCore.Mvc;
|
||||||
|
@inject IFileVersionProvider FileVersionProvider
|
||||||
|
@inject BTCPayServerOptions BTCPayServerOptions
|
||||||
|
|
||||||
|
<svg role="img" class="icon icon-@Symbol">
|
||||||
|
<use href="@GetPathTo(Symbol)"></use>
|
||||||
|
</svg>
|
||||||
|
@code {
|
||||||
|
public string GetPathTo(string symbol)
|
||||||
|
{
|
||||||
|
var versioned = FileVersionProvider.AddFileVersionToPath(default, "img/icon-sprite.svg");
|
||||||
|
var rootPath = (BTCPayServerOptions.RootPath ?? "/").WithTrailingSlash();
|
||||||
|
return $"{rootPath}{versioned}#{Symbol}";
|
||||||
|
}
|
||||||
|
[Parameter]
|
||||||
|
public string Symbol { get; set; }
|
||||||
|
}
|
152
BTCPayServer/Blazor/NotificationsDropDown.razor
Normal file
152
BTCPayServer/Blazor/NotificationsDropDown.razor
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
@using System.Security.Claims
|
||||||
|
@using BTCPayServer.Abstractions.Contracts;
|
||||||
|
@using BTCPayServer.Configuration;
|
||||||
|
@using BTCPayServer.Data;
|
||||||
|
@using BTCPayServer.Services.Notifications;
|
||||||
|
@using Microsoft.AspNetCore.Identity;
|
||||||
|
@using Microsoft.AspNetCore.Routing;
|
||||||
|
@implements IDisposable
|
||||||
|
@inject AuthenticationStateProvider _AuthenticationStateProvider
|
||||||
|
@inject NotificationManager _NotificationManager
|
||||||
|
@inject UserManager<ApplicationUser> _UserManager
|
||||||
|
@inject IJSRuntime _JSRuntime
|
||||||
|
@inject LinkGenerator _LinkGenerator
|
||||||
|
@inject BTCPayServerOptions _BTCPayServerOptions
|
||||||
|
@inject EventAggregator _EventAggregator
|
||||||
|
|
||||||
|
<div id="Notifications">
|
||||||
|
@if (UnseenCount == "0")
|
||||||
|
{
|
||||||
|
<a href="@NotificationsUrl" id="NotificationsHandle" class="mainMenuButton" title="Notifications">
|
||||||
|
<Icon Symbol="notifications" />
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<button id="NotificationsHandle" class="mainMenuButton" title="Notifications" type="button" data-bs-toggle="dropdown">
|
||||||
|
<Icon Symbol="notifications" />
|
||||||
|
<span class="badge rounded-pill bg-danger p-1 ms-1" id="NotificationsBadge">@UnseenCount</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
@if (UnseenCount != "0" && Last5 is not null)
|
||||||
|
{
|
||||||
|
<div class="dropdown-menu text-center" id="NotificationsDropdown" aria-labelledby="NotificationsHandle">
|
||||||
|
<div class="d-flex gap-3 align-items-center justify-content-between py-3 px-4 border-bottom border-light">
|
||||||
|
<h5 class="m-0">Notifications</h5>
|
||||||
|
<a class="btn btn-link p-0" @onclick="MarkAllAsSeen" id="NotificationsMarkAllAsSeen">Mark all as seen</a>
|
||||||
|
</div>
|
||||||
|
<div id="NotificationsList" v-pre>
|
||||||
|
@foreach (var n in Last5)
|
||||||
|
{
|
||||||
|
<a href="@NotificationUrl(n.Id)" class="notification d-flex align-items-center dropdown-item border-bottom border-light py-3 px-4">
|
||||||
|
<div class="me-3">
|
||||||
|
<Icon Symbol="@NotificationIcon(n.Identifier)" />
|
||||||
|
</div>
|
||||||
|
<div class="notification-item__content">
|
||||||
|
<div class="text-start text-wrap">
|
||||||
|
@n.Body
|
||||||
|
</div>
|
||||||
|
<div class="text-start d-flex">
|
||||||
|
<small class="text-muted" data-timeago-unixms="@n.Created.ToUnixTimeMilliseconds()">@n.Created.ToTimeAgo()</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-3">
|
||||||
|
<a href="@NotificationsUrl">View all</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
string NotificationsUrl => _LinkGenerator.GetPathByAction("Index", "UINotifications", pathBase: _BTCPayServerOptions.RootPath);
|
||||||
|
string NotificationUrl(string notificationId) => _LinkGenerator.GetPathByAction("NotificationPassThrough", "UINotifications", values: new { id = notificationId }, pathBase: _BTCPayServerOptions.RootPath);
|
||||||
|
string UnseenCount;
|
||||||
|
List<NotificationViewModel> Last5;
|
||||||
|
IDisposable _EventAggregatorListener;
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
if (_JSRuntime.IsPreRendering())
|
||||||
|
return;
|
||||||
|
_EventAggregatorListener = _EventAggregator.Subscribe<UserNotificationsUpdatedEvent>((s, evt) =>
|
||||||
|
{
|
||||||
|
_ = InvokeAsync(async () =>
|
||||||
|
{
|
||||||
|
if (await GetUserId() is string userId)
|
||||||
|
{
|
||||||
|
var res = await _NotificationManager.GetSummaryNotifications(userId, cachedOnly: false);
|
||||||
|
UpdateState(res);
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() => _EventAggregatorListener?.Dispose();
|
||||||
|
string SeenCount(int? count)
|
||||||
|
{
|
||||||
|
if (count is not int c)
|
||||||
|
return "0";
|
||||||
|
if (c >= NotificationManager.MaxUnseen)
|
||||||
|
return $"{NotificationManager.MaxUnseen - 1}+";
|
||||||
|
return c.ToString();
|
||||||
|
}
|
||||||
|
void UpdateState((List<NotificationViewModel> Items, int? Count) res)
|
||||||
|
{
|
||||||
|
UnseenCount = SeenCount(res.Count);
|
||||||
|
Last5 = res.Items;
|
||||||
|
}
|
||||||
|
protected async override Task OnParametersSetAsync()
|
||||||
|
{
|
||||||
|
if (await GetUserId() is string userId)
|
||||||
|
{
|
||||||
|
// For prerendering and first rendering, always use the cached value
|
||||||
|
var res = await _NotificationManager.GetSummaryNotifications(userId, cachedOnly: true);
|
||||||
|
// If we forget to update the state here, the UI will flicker.
|
||||||
|
// Because the first rendering will think there is 0 events, until the DB call ends and the second rendering happens.
|
||||||
|
// By updating the state here, the first rendering will show the cached value until the second rendering happens
|
||||||
|
UpdateState(res);
|
||||||
|
// We don't want to block the pre-rendering, so we will render again when the costly request is over
|
||||||
|
if (!_JSRuntime.IsPreRendering())
|
||||||
|
{
|
||||||
|
res = await _NotificationManager.GetSummaryNotifications(userId, cachedOnly: false);
|
||||||
|
UpdateState(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async Task<string>
|
||||||
|
GetUserId()
|
||||||
|
{
|
||||||
|
var state = await _AuthenticationStateProvider.GetAuthenticationStateAsync();
|
||||||
|
if (!state.User.Identity.IsAuthenticated)
|
||||||
|
return null;
|
||||||
|
return _UserManager.GetUserId(state.User);
|
||||||
|
}
|
||||||
|
public async Task MarkAllAsSeen()
|
||||||
|
{
|
||||||
|
if (await GetUserId() is string userId)
|
||||||
|
{
|
||||||
|
await _NotificationManager.ToggleSeen(new NotificationsQuery() { Seen = false, UserId = userId }, true);
|
||||||
|
UnseenCount = "0";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private static string NotificationIcon(string type)
|
||||||
|
{
|
||||||
|
return type switch
|
||||||
|
{
|
||||||
|
"invoice_expired" => "notifications-invoice-failure",
|
||||||
|
"invoice_expiredpaidpartial" => "notifications-invoice-failure",
|
||||||
|
"invoice_failedtoconfirm" => "notifications-invoice-failure",
|
||||||
|
"invoice_confirmed" => "notifications-invoice-settled",
|
||||||
|
"invoice_paidafterexpiration" => "notifications-invoice-settled",
|
||||||
|
"external-payout-transaction" => "notifications-payout",
|
||||||
|
"payout_awaitingapproval" => "notifications-payout",
|
||||||
|
"payout_awaitingpayment" => "notifications-payout-approved",
|
||||||
|
"newversion" => "notifications-new-version",
|
||||||
|
_ => "note"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
9
BTCPayServer/Blazor/_Imports.razor
Normal file
9
BTCPayServer/Blazor/_Imports.razor
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
@using System.Net.Http
|
||||||
|
@using Microsoft.AspNetCore.Authorization
|
||||||
|
@using Microsoft.AspNetCore.Components.Authorization
|
||||||
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
@using Microsoft.AspNetCore.Components.Routing
|
||||||
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
|
@using Microsoft.JSInterop
|
||||||
|
@using BTCPayServer.Blazor
|
||||||
|
@using BTCPayServer.Abstractions.Extensions
|
@@ -1,27 +1,24 @@
|
|||||||
if (!window.appSales) {
|
if (!window.appSales) {
|
||||||
window.appSales =
|
window.appSales = {
|
||||||
{
|
dataLoaded (model) {
|
||||||
dataLoaded: function (model) {
|
const id = `AppSales-${model.id}`;
|
||||||
const id = "AppSales-" + model.id;
|
|
||||||
const appId = model.id;
|
const appId = model.id;
|
||||||
const period = model.period;
|
const period = model.period;
|
||||||
const baseUrl = model.url;
|
const baseUrl = model.dataUrl;
|
||||||
const data = model;
|
const data = model;
|
||||||
|
|
||||||
const render = (data, period) => {
|
const render = (data, period) => {
|
||||||
const series = data.series.map(s => s.salesCount);
|
const series = data.series.map(s => s.salesCount);
|
||||||
const labels = data.series.map((s, i) => period === model.period ? s.label : (i % 5 === 0 ? s.label : ''));
|
const labels = data.series.map((s, i) => period === 'Month' ? (i % 5 === 0 ? s.label : '') : s.label);
|
||||||
const min = Math.min(...series);
|
const min = Math.min(...series);
|
||||||
const max = Math.max(...series);
|
const max = Math.max(...series);
|
||||||
const low = min === max ? 0 : Math.max(min - ((max - min) / 5), 0);
|
const low = min === max ? 0 : Math.max(min - ((max - min) / 5), 0);
|
||||||
|
|
||||||
document.querySelectorAll(`#${id} .sales-count`).innerText = data.salesCount;
|
document.querySelectorAll(`#${id} .sales-count`).innerText = data.salesCount;
|
||||||
|
|
||||||
new Chartist.Bar(`#${id} .ct-chart`, {
|
new Chartist.Bar(`#${id} .ct-chart`, {
|
||||||
labels,
|
labels,
|
||||||
series: [series]
|
series: [series]
|
||||||
}, {
|
}, {
|
||||||
low,
|
low
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -24,7 +24,7 @@ public class AppTopItems : ViewComponent
|
|||||||
public async Task<IViewComponentResult> InvokeAsync(string appId, string appType)
|
public async Task<IViewComponentResult> InvokeAsync(string appId, string appType)
|
||||||
{
|
{
|
||||||
var type = _appService.GetAppType(appType);
|
var type = _appService.GetAppType(appType);
|
||||||
if (type is not IHasItemStatsAppType salesAppType || type is not AppBaseType appBaseType)
|
if (type is not (IHasItemStatsAppType and AppBaseType appBaseType))
|
||||||
return new HtmlContentViewComponentResult(new StringHtmlContent(string.Empty));
|
return new HtmlContentViewComponentResult(new StringHtmlContent(string.Empty));
|
||||||
|
|
||||||
var vm = new AppTopItemsViewModel
|
var vm = new AppTopItemsViewModel
|
||||||
@@ -40,7 +40,7 @@ public class AppTopItems : ViewComponent
|
|||||||
var app = HttpContext.GetAppData();
|
var app = HttpContext.GetAppData();
|
||||||
var entries = await _appService.GetItemStats(app);
|
var entries = await _appService.GetItemStats(app);
|
||||||
vm.SalesCount = entries.Select(e => e.SalesCount).ToList();
|
vm.SalesCount = entries.Select(e => e.SalesCount).ToList();
|
||||||
vm.Entries = entries.ToList();
|
vm.Entries = entries.Take(5).ToList();
|
||||||
vm.AppType = app.AppType;
|
vm.AppType = app.AppType;
|
||||||
vm.AppUrl = await appBaseType.ConfigureLink(app);
|
vm.AppUrl = await appBaseType.ConfigureLink(app);
|
||||||
vm.Name = app.Name;
|
vm.Name = app.Name;
|
||||||
|
@@ -27,8 +27,8 @@
|
|||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
document.getElementById(`AppTopItems-${appId}`).outerHTML = await response.text();
|
document.getElementById(`AppTopItems-${appId}`).outerHTML = await response.text();
|
||||||
const data = document.querySelector(`#AppSales-${appId} template`);
|
const data = document.querySelector(`#AppTopItems-${appId} template`);
|
||||||
if (data) window.appSales.dataLoaded(JSON.parse(data.innerHTML));
|
if (data) window.appTopItems.dataLoaded(JSON.parse(data.innerHTML));
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
<span class="app-item-point ct-point"></span>
|
<span class="app-item-point ct-point"></span>
|
||||||
@entry.Title
|
@entry.Title
|
||||||
</span>
|
</span>
|
||||||
<span class="app-item-value">
|
<span class="app-item-value" data-sensitive>
|
||||||
<span class="text-muted">@entry.SalesCount @($"{label}{(entry.SalesCount == 1 ? "" : "s")}"),</span>
|
<span class="text-muted">@entry.SalesCount @($"{label}{(entry.SalesCount == 1 ? "" : "s")}"),</span>
|
||||||
@entry.TotalFormatted
|
@entry.TotalFormatted
|
||||||
</span>
|
</span>
|
||||||
|
@@ -1,8 +1,7 @@
|
|||||||
if (!window.appTopItems) {
|
if (!window.appTopItems) {
|
||||||
window.appTopItems =
|
window.appTopItems = {
|
||||||
{
|
dataLoaded (model) {
|
||||||
dataLoaded: function (model) {
|
const id = `AppTopItems-${model.id}`;
|
||||||
const id = "AppTopItems-" + model.id;
|
|
||||||
const series = model.salesCount;
|
const series = model.salesCount;
|
||||||
new Chartist.Bar(`#${id} .ct-chart`, { series }, {
|
new Chartist.Bar(`#${id} .ct-chart`, { series }, {
|
||||||
distributeSeries: true,
|
distributeSeries: true,
|
||||||
|
67
BTCPayServer/Components/InvoiceStatus/Default.cshtml
Normal file
67
BTCPayServer/Components/InvoiceStatus/Default.cshtml
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
@using BTCPayServer.Services.Invoices
|
||||||
|
@using BTCPayServer.Abstractions.Extensions
|
||||||
|
@model BTCPayServer.Components.InvoiceStatus.InvoiceStatusViewModel
|
||||||
|
@inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary
|
||||||
|
|
||||||
|
@{
|
||||||
|
var state = Model.State.ToString();
|
||||||
|
var badgeClass = Model.State.Status.ToModernStatus().ToString().ToLower();
|
||||||
|
var canMark = !string.IsNullOrEmpty(Model.InvoiceId) && (Model.State.CanMarkComplete() || Model.State.CanMarkInvalid());
|
||||||
|
}
|
||||||
|
<div class="d-inline-flex align-items-center gap-2">
|
||||||
|
@if (Model.IsArchived)
|
||||||
|
{
|
||||||
|
<span class="badge bg-warning">archived</span>
|
||||||
|
}
|
||||||
|
<div class="badge badge-@badgeClass" data-invoice-state-badge="@Model.InvoiceId">
|
||||||
|
@if (canMark)
|
||||||
|
{
|
||||||
|
<span class="dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
|
@state
|
||||||
|
</span>
|
||||||
|
<div class="dropdown-menu">
|
||||||
|
@if (Model.State.CanMarkInvalid())
|
||||||
|
{
|
||||||
|
<button type="button" class="dropdown-item lh-base" data-invoice-id="@Model.InvoiceId" data-new-state="invalid">
|
||||||
|
Mark as invalid
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
@if (Model.State.CanMarkComplete())
|
||||||
|
{
|
||||||
|
<button type="button" class="dropdown-item lh-base" data-invoice-id="@Model.InvoiceId" data-new-state="settled">
|
||||||
|
Mark as settled
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
@state
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
@if (Model.Payments != null)
|
||||||
|
{
|
||||||
|
foreach (var paymentMethodId in Model.Payments.Select(payment => payment.GetPaymentMethodId()).Distinct())
|
||||||
|
{
|
||||||
|
var image = PaymentMethodHandlerDictionary[paymentMethodId]?.GetCryptoImage(paymentMethodId);
|
||||||
|
var badge = paymentMethodId.PaymentType.GetBadge();
|
||||||
|
if (!string.IsNullOrEmpty(image) || !string.IsNullOrEmpty(badge))
|
||||||
|
{
|
||||||
|
<span class="d-inline-flex align-items-center gap-1">
|
||||||
|
@if (!string.IsNullOrEmpty(image))
|
||||||
|
{
|
||||||
|
<img src="@Context.Request.GetRelativePathOrAbsolute(image)" alt="@paymentMethodId.PaymentType.ToString()" style="height:1.5em" />
|
||||||
|
}
|
||||||
|
@if (!string.IsNullOrEmpty(badge))
|
||||||
|
{
|
||||||
|
@badge
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@if (Model.HasRefund)
|
||||||
|
{
|
||||||
|
<span class="badge bg-warning">Refund</span>
|
||||||
|
}
|
||||||
|
</div>
|
22
BTCPayServer/Components/InvoiceStatus/InvoiceStatus.cs
Normal file
22
BTCPayServer/Components/InvoiceStatus/InvoiceStatus.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using BTCPayServer.Services.Invoices;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Components.InvoiceStatus
|
||||||
|
{
|
||||||
|
public class InvoiceStatus : ViewComponent
|
||||||
|
{
|
||||||
|
public IViewComponentResult Invoke(InvoiceState state, List<PaymentEntity> payments, string invoiceId, bool isArchived = false, bool hasRefund = false)
|
||||||
|
{
|
||||||
|
var vm = new InvoiceStatusViewModel
|
||||||
|
{
|
||||||
|
State = state,
|
||||||
|
Payments = payments,
|
||||||
|
InvoiceId = invoiceId,
|
||||||
|
IsArchived = isArchived,
|
||||||
|
HasRefund = hasRefund
|
||||||
|
};
|
||||||
|
return View(vm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,14 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using BTCPayServer.Services.Invoices;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Components.InvoiceStatus
|
||||||
|
{
|
||||||
|
public class InvoiceStatusViewModel
|
||||||
|
{
|
||||||
|
public InvoiceState State { get; set; }
|
||||||
|
public List<PaymentEntity> Payments { get; set; }
|
||||||
|
public string InvoiceId { get; set; }
|
||||||
|
public bool IsArchived { get; set; }
|
||||||
|
public bool HasRefund { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@@ -3,7 +3,7 @@
|
|||||||
@model BTCPayServer.Components.LabelManager.LabelViewModel
|
@model BTCPayServer.Components.LabelManager.LabelViewModel
|
||||||
@{
|
@{
|
||||||
var elementId = "a" + Encoders.Base58.EncodeData(RandomUtils.GetBytes(16));
|
var elementId = "a" + Encoders.Base58.EncodeData(RandomUtils.GetBytes(16));
|
||||||
var fetchUrl = Url.Action("GetLabels", "UIWallets", new {
|
var fetchUrl = Url.Action("LabelsJson", "UIWallets", new {
|
||||||
walletId = Model.WalletObjectId.WalletId,
|
walletId = Model.WalletObjectId.WalletId,
|
||||||
excludeTypes = Safe.Json(Model.ExcludeTypes)
|
excludeTypes = Safe.Json(Model.ExcludeTypes)
|
||||||
});
|
});
|
||||||
|
@@ -1,13 +1,15 @@
|
|||||||
@using BTCPayServer.Views.Server
|
@using BTCPayServer.Views.Server
|
||||||
@using BTCPayServer.Views.Stores
|
@using BTCPayServer.Views.Stores
|
||||||
@using BTCPayServer.Views.Apps
|
|
||||||
@using BTCPayServer.Views.Invoice
|
@using BTCPayServer.Views.Invoice
|
||||||
@using BTCPayServer.Views.Manage
|
@using BTCPayServer.Views.Manage
|
||||||
@using BTCPayServer.Views.PaymentRequest
|
@using BTCPayServer.Views.PaymentRequest
|
||||||
@using BTCPayServer.Views.Wallets
|
@using BTCPayServer.Views.Wallets
|
||||||
@using BTCPayServer.Abstractions.Extensions
|
@using BTCPayServer.Abstractions.Extensions
|
||||||
@using BTCPayServer.Client
|
@using BTCPayServer.Client
|
||||||
|
@using BTCPayServer.Components.ThemeSwitch
|
||||||
|
@using BTCPayServer.Components.UIExtensionPoint
|
||||||
@using BTCPayServer.Services
|
@using BTCPayServer.Services
|
||||||
|
@using BTCPayServer.Views.Apps
|
||||||
@using BTCPayServer.Views.CustodianAccounts
|
@using BTCPayServer.Views.CustodianAccounts
|
||||||
@inject Microsoft.AspNetCore.Http.IHttpContextAccessor HttpContext;
|
@inject Microsoft.AspNetCore.Http.IHttpContextAccessor HttpContext;
|
||||||
@inject BTCPayServerEnvironment Env
|
@inject BTCPayServerEnvironment Env
|
||||||
@@ -91,7 +93,7 @@
|
|||||||
|
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
<vc:ui-extension-point location="store-wallets-nav" model="@Model"/>
|
<vc:ui-extension-point location="store-wallets-nav" model="@Model"/>
|
||||||
@if (PoliciesSettings.Experimental)
|
@if (PoliciesSettings.Experimental)
|
||||||
{
|
{
|
||||||
@foreach (var custodianAccount in Model.CustodianAccounts)
|
@foreach (var custodianAccount in Model.CustodianAccounts)
|
||||||
@@ -132,6 +134,12 @@
|
|||||||
<span>Invoices</span>
|
<span>Invoices</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item" permission="@Policies.CanViewInvoices">
|
||||||
|
<a asp-area="" asp-controller="UIReports" asp-action="StoreReports" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActivePage(StoreNavPages.Reporting)" id="SectionNav-Reporting">
|
||||||
|
<vc:icon symbol="invoice" />
|
||||||
|
<span>Reporting</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item" permission="@Policies.CanModifyStoreSettings">
|
<li class="nav-item" permission="@Policies.CanModifyStoreSettings">
|
||||||
<a asp-area="" asp-controller="UIPaymentRequest" asp-action="GetPaymentRequests" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActiveCategory(typeof(PaymentRequestsNavPages))" id="StoreNav-PaymentRequests">
|
<a asp-area="" asp-controller="UIPaymentRequest" asp-action="GetPaymentRequests" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActiveCategory(typeof(PaymentRequestsNavPages))" id="StoreNav-PaymentRequests">
|
||||||
<vc:icon symbol="payment-requests"/>
|
<vc:icon symbol="payment-requests"/>
|
||||||
@@ -178,10 +186,18 @@
|
|||||||
<ul class="navbar-nav">
|
<ul class="navbar-nav">
|
||||||
<li class="nav-item" permission="@Policies.CanModifyServerSettings">
|
<li class="nav-item" permission="@Policies.CanModifyServerSettings">
|
||||||
<a asp-area="" asp-controller="UIServer" asp-action="ListPlugins" class="nav-link @ViewData.IsActivePage(ServerNavPages.Plugins)" id="Nav-ManagePlugins">
|
<a asp-area="" asp-controller="UIServer" asp-action="ListPlugins" class="nav-link @ViewData.IsActivePage(ServerNavPages.Plugins)" id="Nav-ManagePlugins">
|
||||||
<vc:icon symbol="plugin"/>
|
<vc:icon symbol="manage-plugins"/>
|
||||||
<span>Manage Plugins</span>
|
<span>Manage Plugins</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
@if (Model.Store != null && Model.ArchivedAppsCount > 0)
|
||||||
|
{
|
||||||
|
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyStoreSettings">
|
||||||
|
<a asp-area="" asp-controller="UIApps" asp-action="ListApps" asp-route-storeId="@Model.Store.Id" asp-route-archived="true" class="nav-link @ViewData.IsActivePage(AppsNavPages.Index)" id="Nav-ArchivedApps">
|
||||||
|
@Model.ArchivedAppsCount Archived App@(Model.ArchivedAppsCount == 1 ? "" : "s")
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -239,7 +255,7 @@
|
|||||||
<span>Account</span>
|
<span>Account</span>
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu py-0 w-100" aria-labelledby="Nav-Account">
|
<ul class="dropdown-menu py-0 w-100" aria-labelledby="Nav-Account">
|
||||||
<li class="p-3">
|
<li class="p-3 border-bottom">
|
||||||
<strong class="d-block text-truncate" style="max-width:195px">@User.Identity.Name</strong>
|
<strong class="d-block text-truncate" style="max-width:195px">@User.Identity.Name</strong>
|
||||||
@if (User.IsInRole(Roles.ServerAdmin))
|
@if (User.IsInRole(Roles.ServerAdmin))
|
||||||
{
|
{
|
||||||
@@ -248,10 +264,19 @@
|
|||||||
</li>
|
</li>
|
||||||
@if (!Theme.CustomTheme)
|
@if (!Theme.CustomTheme)
|
||||||
{
|
{
|
||||||
<li class="border-top py-1 px-3">
|
<li class="py-1 px-3">
|
||||||
<vc:theme-switch css-class="nav-link"/>
|
<vc:theme-switch css-class="nav-link pb-0"/>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
|
<li class="py-1 px-3">
|
||||||
|
<label class="d-flex align-items-center justify-content-between gap-3 nav-link">
|
||||||
|
<span class="fw-semibold">Hide Sensitive Info</span>
|
||||||
|
<input id="HideSensitiveInfo" name="HideSensitiveInfo" type="checkbox" class="btcpay-toggle" />
|
||||||
|
</label>
|
||||||
|
<script>
|
||||||
|
document.getElementById('HideSensitiveInfo').checked = window.localStorage.getItem('btcpay-hide-sensitive-info') === 'true';
|
||||||
|
</script>
|
||||||
|
</li>
|
||||||
<li class="border-top py-1 px-3">
|
<li class="border-top py-1 px-3">
|
||||||
<a asp-area="" asp-controller="UIManage" asp-action="Index" class="nav-link @ViewData.IsActiveCategory(typeof(ManageNavPages))" id="Nav-ManageAccount">
|
<a asp-area="" asp-controller="UIManage" asp-action="Index" class="nav-link @ViewData.IsActiveCategory(typeof(ManageNavPages))" id="Nav-ManageAccount">
|
||||||
<span>Manage Account</span>
|
<span>Manage Account</span>
|
||||||
|
@@ -68,14 +68,17 @@ namespace BTCPayServer.Components.MainNav
|
|||||||
vm.LightningNodes = lightningNodes;
|
vm.LightningNodes = lightningNodes;
|
||||||
|
|
||||||
// Apps
|
// Apps
|
||||||
var apps = await _appService.GetAllApps(UserId, false, store.Id);
|
var apps = await _appService.GetAllApps(UserId, false, store.Id, true);
|
||||||
vm.Apps = apps.Select(a => new StoreApp
|
vm.Apps = apps
|
||||||
{
|
.Where(a => !a.Archived)
|
||||||
Id = a.Id,
|
.Select(a => new StoreApp
|
||||||
IsOwner = a.IsOwner,
|
{
|
||||||
AppName = a.AppName,
|
Id = a.Id,
|
||||||
AppType = a.AppType
|
AppName = a.AppName,
|
||||||
}).ToList();
|
AppType = a.AppType
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
vm.ArchivedAppsCount = apps.Count(a => a.Archived);
|
||||||
|
|
||||||
if (PoliciesSettings.Experimental)
|
if (PoliciesSettings.Experimental)
|
||||||
{
|
{
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Models.StoreViewModels;
|
using BTCPayServer.Models.StoreViewModels;
|
||||||
using BTCPayServer.Services.Apps;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Components.MainNav
|
namespace BTCPayServer.Components.MainNav
|
||||||
{
|
{
|
||||||
@@ -13,6 +12,7 @@ namespace BTCPayServer.Components.MainNav
|
|||||||
public List<StoreApp> Apps { get; set; }
|
public List<StoreApp> Apps { get; set; }
|
||||||
public CustodianAccountData[] CustodianAccounts { get; set; }
|
public CustodianAccountData[] CustodianAccounts { get; set; }
|
||||||
public bool AltcoinsBuild { get; set; }
|
public bool AltcoinsBuild { get; set; }
|
||||||
|
public int ArchivedAppsCount { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class StoreApp
|
public class StoreApp
|
||||||
@@ -20,6 +20,5 @@ namespace BTCPayServer.Components.MainNav
|
|||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
public string AppName { get; set; }
|
public string AppName { get; set; }
|
||||||
public string AppType { get; set; }
|
public string AppType { get; set; }
|
||||||
public bool IsOwner { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,31 +0,0 @@
|
|||||||
@using BTCPayServer.Views.Notifications
|
|
||||||
@using BTCPayServer.Abstractions.Extensions
|
|
||||||
@model BTCPayServer.Components.Notifications.NotificationsViewModel
|
|
||||||
|
|
||||||
<div id="Notifications">
|
|
||||||
@if (Model.UnseenCount > 0)
|
|
||||||
{
|
|
||||||
<button id="NotificationsHandle" class="mainMenuButton @ViewData.IsActiveCategory(typeof(NotificationsNavPages))" title="Notifications" type="button" data-bs-toggle="dropdown">
|
|
||||||
<vc:icon symbol="notifications" />
|
|
||||||
<span class="badge rounded-pill bg-danger p-1 ms-1" id="NotificationsBadge">@Model.UnseenCount</span>
|
|
||||||
</button>
|
|
||||||
<div class="dropdown-menu text-center" id="NotificationsDropdown" aria-labelledby="NotificationsHandle">
|
|
||||||
<div class="d-flex gap-3 align-items-center justify-content-between py-3 px-4 border-bottom border-light">
|
|
||||||
<h5 class="m-0">Notifications</h5>
|
|
||||||
<form id="notificationsForm" asp-controller="UINotifications" asp-action="MarkAllAsSeen" asp-route-returnUrl="@Model.ReturnUrl" method="post">
|
|
||||||
<button class="btn btn-link p-0" type="submit">Mark all as seen</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<partial name="Components/Notifications/List" model="Model"/>
|
|
||||||
<div class="p-3">
|
|
||||||
<a asp-controller="UINotifications" asp-action="Index">View all</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<a asp-controller="UINotifications" asp-action="Index" id="NotificationsHandle" class="mainMenuButton @ViewData.IsActiveCategory(typeof(NotificationsNavPages))" title="Notifications">
|
|
||||||
<vc:icon symbol="notifications" />
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
</div>
|
|
@@ -1,38 +0,0 @@
|
|||||||
@using BTCPayServer.Abstractions.Extensions
|
|
||||||
@model BTCPayServer.Components.Notifications.NotificationsViewModel
|
|
||||||
@functions {
|
|
||||||
private static string NotificationIcon(string type)
|
|
||||||
{
|
|
||||||
return type switch
|
|
||||||
{
|
|
||||||
"invoice_expired" => "notifications-invoice-failure",
|
|
||||||
"invoice_expiredpaidpartial" => "notifications-invoice-failure",
|
|
||||||
"invoice_failedtoconfirm" => "notifications-invoice-failure",
|
|
||||||
"invoice_confirmed" => "notifications-invoice-settled",
|
|
||||||
"invoice_paidafterexpiration" => "notifications-invoice-settled",
|
|
||||||
"external-payout-transaction" => "notifications-payout",
|
|
||||||
"payout_awaitingapproval" => "notifications-payout",
|
|
||||||
"payout_awaitingpayment" => "notifications-payout-approved",
|
|
||||||
"newversion" => "notifications-new-version",
|
|
||||||
_ => "note"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
<div id="NotificationsList">
|
|
||||||
@foreach (var n in Model.Last5)
|
|
||||||
{
|
|
||||||
<a asp-action="NotificationPassThrough" asp-controller="UINotifications" asp-route-id="@n.Id" class="notification d-flex align-items-center dropdown-item border-bottom border-light py-3 px-4">
|
|
||||||
<div class="me-3">
|
|
||||||
<vc:icon symbol="@NotificationIcon(n.Identifier)" />
|
|
||||||
</div>
|
|
||||||
<div class="notification-item__content">
|
|
||||||
<div class="text-start text-wrap">
|
|
||||||
@n.Body
|
|
||||||
</div>
|
|
||||||
<div class="text-start d-flex">
|
|
||||||
<small class="text-muted" data-timeago-unixms="@n.Created.ToUnixTimeMilliseconds()">@n.Created.ToTimeAgo()</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
</div>
|
|
@@ -1,27 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using BTCPayServer.Services.Notifications;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Components.Notifications
|
|
||||||
{
|
|
||||||
public class Notifications : ViewComponent
|
|
||||||
{
|
|
||||||
private readonly NotificationManager _notificationManager;
|
|
||||||
|
|
||||||
private static readonly string[] _views = { "List", "Dropdown", "Recent" };
|
|
||||||
|
|
||||||
public Notifications(NotificationManager notificationManager)
|
|
||||||
{
|
|
||||||
_notificationManager = notificationManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IViewComponentResult> InvokeAsync(string appearance, string returnUrl)
|
|
||||||
{
|
|
||||||
var vm = await _notificationManager.GetSummaryNotifications(UserClaimsPrincipal);
|
|
||||||
vm.ReturnUrl = returnUrl;
|
|
||||||
var viewName = _views.Contains(appearance) ? appearance : _views[0];
|
|
||||||
return View(viewName, vm);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,12 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using BTCPayServer.Abstractions.Contracts;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Components.Notifications
|
|
||||||
{
|
|
||||||
public class NotificationsViewModel
|
|
||||||
{
|
|
||||||
public string ReturnUrl { get; set; }
|
|
||||||
public int UnseenCount { get; set; }
|
|
||||||
public List<NotificationViewModel> Last5 { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,19 +0,0 @@
|
|||||||
@model BTCPayServer.Components.Notifications.NotificationsViewModel
|
|
||||||
|
|
||||||
<div id="NotificationsRecent">
|
|
||||||
@if (Model.Last5.Any())
|
|
||||||
{
|
|
||||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
|
||||||
<h4 class="mb-0">Recent Notifications</h4>
|
|
||||||
<a asp-controller="UINotifications" asp-action="Index">View all</a>
|
|
||||||
</div>
|
|
||||||
<partial name="Components/Notifications/List" model="Model"/>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<h4 class="mb-3">Notifications</h4>
|
|
||||||
<p class="text-secondary mt-3">
|
|
||||||
There are no recent unseen notifications.
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
</div>
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user