Compare commits

...

101 Commits

Author SHA1 Message Date
215a36e7a9 Fix: Some multi path payment on LND wouldn't be detected 2023-05-30 12:26:30 +09:00
247f6b86a5 Changelog 1.10.0 () 2023-05-29 16:24:27 +09:00
a9d42f1e6a Add What's New in v1.10.0 ()
* Add What's New in v1.10.0

* Update BTCPayServer/Views/UIStores/Dashboard.cshtml

Co-authored-by: B <102448109+Bas02@users.noreply.github.com>

---------

Co-authored-by: B <102448109+Bas02@users.noreply.github.com>
2023-05-29 16:08:46 +09:00
4e03c2523a Prune webhook data from database 2023-05-29 09:02:47 +02:00
418b476725 Bug fix on StoreRoleId parsing 2023-05-27 12:51:48 +09:00
783e4ccb35 Store Custom Roles () 2023-05-26 23:49:32 +09:00
6b7fb55658 Fix: Payment not marked as settled even if the payment is successful with LNBank
Fix https://github.com/dennisreimann/btcpayserver-plugin-lnbank/issues/33
2023-05-25 21:09:13 +09:00
3d5361cd11 [Bug] If a altcoins is disabled from BTCPay and payout processor is used, it would crash at restart ()
Co-authored-by: Andrew Camilleri <evilkukka@gmail.com>
2023-05-25 12:42:23 +02:00
2c4349c630 Test concurrent payment of lightning invoices 2023-05-25 18:41:17 +09:00
3589417b58 Form Editor: Minor wording adjustments () 2023-05-25 08:51:03 +02:00
55203e0b64 Dashboard: Fix SATS denomination display ()
When the default currency of the store is SATS, the display was broken.
2023-05-25 10:08:00 +09:00
a918288e3b Fix codeql config to not scan vendor js, add it to solution 2023-05-23 10:38:59 +09:00
e183138d2c Remove vendor js from codeql scan 2023-05-23 10:07:08 +09:00
d3e42862ed Create codeql.yml ()
Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
2023-05-23 09:23:24 +09:00
8860eec254 Switch Apps to json not YML () 2023-05-23 09:18:57 +09:00
97e7e60cea Add minrelayfee to payjoin request
fixes 
2023-05-22 14:56:08 +02:00
44aaf7acbb Form editor ()
Co-authored-by: dstrukt <gfxdsign@gmail.com>
2023-05-22 13:30:28 +02:00
9b721fae27 Better handle postgres requests for wallet objects () 2023-05-20 23:26:16 +09:00
c3f412e3bb Bump tests to Bitcoin Core 24.1 () 2023-05-20 21:38:39 +09:00
ee738a29f0 Stop spamming logs with event aggregator logging 2023-05-19 15:24:20 +09:00
6c6544bf9b Improve invoice filtering UI ()
* Improve invoice filtering UI

Closes .

* UI updates

* Add app filter

* Add indicator for active filters

* updates text

* Improve selected filter display

* Apply suggestions from code review

---------

Co-authored-by: dstrukt <gfxdsign@gmail.com>
2023-05-19 10:42:09 +09:00
3d57b944ca Fix a bunch of minor bugs () 2023-05-19 08:41:21 +09:00
acf003b1b4 Do not generate new address when a new payment is detected ()
* Do not generate new address when a new payment is detected

* Update BTCPayServer.Tests/UnitTest1.cs

Co-authored-by: Andrew Camilleri <evilkukka@gmail.com>

---------

Co-authored-by: Andrew Camilleri <evilkukka@gmail.com>
2023-05-18 16:53:01 +09:00
52e108d32f Minor design system updates ()
- Update Manage Plugins icon
- Add ESC to supporters sprite
- Update body-text-active variable
2023-05-17 10:19:26 +02:00
7b96f96025 bump clightning ()
* bump clightning

* Remove Lightning Charge from our tests
2023-05-16 09:17:21 +09:00
8db5e7e043 Plugins: Allow payout processors to signal they cannot be removed through common UI 2023-05-15 09:49:13 +02:00
25fb5c1293 Checkout v2: Improve expired paid partial state ()
Co-authored-by: Andrew Camilleri <evilkukka@gmail.com>
2023-05-11 10:38:40 +02:00
37f0498def adds payouts settings button ()
Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
2023-05-11 10:37:28 +02:00
02110f93d7 Hide sensitive info () 2023-05-11 10:35:51 +02:00
195dfc2c47 Refund updates () 2023-05-11 10:33:33 +02:00
541b6cf9eb Improve create first store case () 2023-05-10 11:18:29 +02:00
2c26b77afc Forms: Add multiline input () 2023-05-10 11:14:19 +02:00
99bcec5597 bump nbx 2023-05-09 22:06:23 +09:00
781190a65d Bump to 1.9.3 ()
* Bump to 1.9.3

* Apply suggestions from code review

* Update Changelog.md

* Update Changelog.md

* Update Changelog.md

* Update Changelog.md

Co-authored-by: d11n <mail@dennisreimann.de>

* Update Changelog.md

Co-authored-by: d11n <mail@dennisreimann.de>

* Update Changelog.md

Co-authored-by: d11n <mail@dennisreimann.de>

---------

Co-authored-by: d11n <mail@dennisreimann.de>
2023-05-08 21:30:43 +09:00
3763480280 NFC: Handle HTTP-related exceptions () 2023-05-08 12:11:42 +02:00
6fad5ebedb Do not crash checkout when attempting lnurl checkout through non secure page ()
Co-authored-by: d11n <mail@dennisreimann.de>
2023-05-08 12:09:48 +02:00
0690194aa1 Fix posdata with primitive array ()
Co-authored-by: d11n <mail@dennisreimann.de>
2023-05-08 11:02:13 +02:00
03b94e2be3 Minor refactoring about DefaultPaymentMethod 2023-05-08 09:14:58 +09:00
18e34b3cbe Checkout v2: Improve truncation of displayed addresses () 2023-05-05 10:00:55 +02:00
a0bb3ace61 LN Settings: Show only node host name () 2023-05-05 09:59:33 +02:00
920ad67633 Rates: Fix advanced rules example formatting
Fixes .
2023-05-05 09:58:42 +02:00
8b8f72129c Crowdfund: Fix redirect URL fallback
As the request for invoice creation is issued via web socket, the display URL ends up being the hob connection URL. This replaces it with the actual app URL and fixes .
2023-05-05 09:57:44 +02:00
b9b11e722c Greenfield: Apply store default payment method on invoice creation
Fixes .
2023-05-05 09:56:23 +02:00
eddd458744 Fix missing shopify link
fixes 
2023-05-05 09:51:45 +02:00
439ea20a89 Resolving weird Firefox form autocomplete behavior on our POS () 2023-05-04 18:49:35 +02:00
cec223c8e7 Fix missing closing div () 2023-05-03 10:55:20 +02:00
25cb188d00 Bumping LND to 0.16.2-beta 2023-04-29 09:10:07 -05:00
31007a8d96 Merge pull request from dennisreimann/esc
Supporters: Add ESC
2023-04-27 18:30:09 +02:00
a4fa8db69b Supporters: Add ESC 2023-04-27 18:28:29 +02:00
6193835ea1 Make LightningListener nullable, fix some NRE 2023-04-27 13:37:32 +09:00
0c78e9e4ac Bumping LND to 0.16.1-beta () 2023-04-27 13:17:57 +09:00
58c409e7fa Fix tests 2023-04-27 13:00:25 +09:00
9577eed524 Validate input in greenfield for payout processors () 2023-04-27 10:59:19 +09:00
76f32cd064 Fix payment request overriding user provided email 2023-04-26 14:06:42 +02:00
92d9c17095 Fix payment request merge conflict marker in UI 2023-04-26 14:06:23 +02:00
b0396df33f Update changelog 2023-04-26 18:27:06 +09:00
c17572c76f Clip configuration values for payout processors 2023-04-26 18:24:46 +09:00
5c91e072a6 Prevent payout processors from stalling restart 2023-04-26 18:09:56 +09:00
4991d0f965 Update Changelog and bump version 2023-04-26 17:57:25 +09:00
45b74e1ce5 Fix cart receipt + fix pos email form forwarding ()
* Show correct array regardless of size

fixes 

* Email provided to pos form was not forwarded to form

fixes 

* Make invoice receipt url redirect to the invoice redirect url if receipt is not enabled

When setting up a default email rule upon invoice settlement, you would link to the receipt page naturally. However, if using the payment requests, receipts are disabled as the payment request itself is the receipt.  This commit makes the receipt url redirect to the invoice redirect url if available, and in the case of payment requests, it would mean the receipt url is the payment request url. fixes 

* Set the email address in the form when configured in the payment request

* fix pay request email copy

* fix payouts nav link

fixes 

* Update BTCPayServer/Views/UIPaymentRequest/EditPaymentRequest.cshtml

Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>

---------

Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
2023-04-26 16:45:35 +09:00
ccb4b9a9ba Merge pull request from dennisreimann/receipt-print
Receipt: Optimize print view
2023-04-26 15:41:41 +09:00
dd635071d6 Merge pull request from dennisreimann/fix-4889
POS: Fix choiceKey case
2023-04-26 15:16:54 +09:00
fe1448dfae Merge pull request from NicolasDorier/qiponbtq
Remove LNURLStandardInvoiceEnabled
2023-04-26 15:15:20 +09:00
56855bc54d Fix typo in PayJoinEndpointController.cs () 2023-04-25 22:29:06 +02:00
b51fa8df5a Fix indent 2023-04-25 21:03:12 +09:00
3aa979cb11 Test updates 2023-04-25 13:07:01 +02:00
c95f75bc6c Remove disable BOLT11 option 2023-04-25 12:21:27 +02:00
b13a636f89 Fix syntax error 2023-04-25 12:11:08 +02:00
8de55cef31 Unify checkout v1 behaviour with v2 2023-04-25 11:36:45 +02:00
cb781f42e3 POS: Fix choiceKey case
I came across this while debugging . This does not actually fix it, but it fixes an inconsistence in the casing of the parameter name.

However, I think the original issue is a caching problem in the browser. I was able to reproduce it on first load, after reloading the page once more it works as intended. The weird thing is: even though the values are correct on first load (verified via debugger), the `choiceKey` for the first item is set incorrectly to an integer value.
2023-04-25 09:16:09 +02:00
b59292dc24 Receipt: Optimize print view
Fixes 
2023-04-25 07:30:15 +02:00
03b793d7e2 Fix tests 2023-04-25 10:28:36 +09:00
bee18d1cfb Can set LazyPaymentMethod at the invoice creation level 2023-04-25 08:38:42 +09:00
d8698181f4 Remove LNURLStandardInvoiceEnabled 2023-04-24 23:52:40 +09:00
47f5d97eaf Prevent an NRE in LNURL ()
* Prevent an NRE in LNURL

In addition to f05a7f9f14e0a2f906c79951a15311085774db85. Fixes .

* Revert "Prevent an NRE in LNURL"

This reverts commit 0b241d61ab45b79297211e04da0e05c2cb10dada.

* Fix NRE

---------

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2023-04-24 21:50:31 +09:00
cb3c5e56fd Fix: Poll of Lightning Invoice status might fail on LND if LNURL is used () 2023-04-24 18:15:39 +09:00
39b76c08de Fix: Form in Payment Requests was not setting its values to the invoices metadata () 2023-04-24 18:04:46 +09:00
c3c8cc21ff Security Page Refactor ()
* Update SECURITY.md

* typo

* project vs product

* Suggestion Update - Docker Deployment

Co-authored-by: d11n <mail@dennisreimann.de>

* Suggestion - Email highlight

Co-authored-by: d11n <mail@dennisreimann.de>

---------

Co-authored-by: d11n <mail@dennisreimann.de>
2023-04-24 15:04:56 +09:00
6dba1b6d8b Nullable on LanguageService 2023-04-20 09:44:24 +09:00
381fe70a79 Add UserAgent to IPN and webhook HTTP requests (Fix ) 2023-04-19 21:13:31 +09:00
feb927c2e4 Add missing param, fix response code. () 2023-04-19 13:07:36 +09:00
e77bd4c188 Swagger: FIx typos in pull payments docs ()
Fixes .
2023-04-19 13:06:51 +09:00
43436fc49e Remove BTCPayServer.Plugins.Test ()
Remove the plugin, which is superseded with the plugin template and move the PluginPacker to the root of th solution (which matches its location in the file system).
2023-04-17 11:38:03 +09:00
d738f797ec bump 2023-04-17 11:11:00 +09:00
b5de97f785 Update changelog 2023-04-17 10:54:00 +09:00
b0c1b0895d Fix crash if auto detect language on checkout page, and the language couldn't be detected (Fix ) () 2023-04-17 10:53:45 +09:00
8e60932f81 Migrate reference to AppType in server settings (Fix ) 2023-04-17 10:34:41 +09:00
7d14cd74f2 LightningLikePaymentData: Null-check preimage ()
Fixes .
2023-04-17 00:08:40 +02:00
717f1610f5 Update changelog 2023-04-13 16:28:30 +09:00
f1abe6497f Fix wrong data mapping to store data instead of store user data. () 2023-04-13 16:27:40 +09:00
046129a57d Bumping LND to 0.16.0-beta () 2023-04-13 14:48:47 +09:00
90d300a490 Remove superflous category in API doc 2023-04-13 08:41:46 +09:00
a2d506c0db Checkout v2: Confetti for processing payments ()
Let's not spoil the fun for those paying on-chain.
2023-04-13 08:40:21 +09:00
58748a24da What's New entry for v1.9 ()
Copy is just a proposal from my side. Link check expected to fail, because the blog post isn't up yet.
2023-04-12 20:40:20 +09:00
639e8a4a1d Add links to invoice metadata in the docs () 2023-04-12 16:30:34 +09:00
48ebaf5c5a Do not put payRequest in the metadata () 2023-04-12 16:30:22 +09:00
1aaccb1e6b Do not show empty JObject in AddtionalData of views 2023-04-11 13:21:02 +09:00
f05a7f9f14 Prevent an NRE in LNURL 2023-04-11 13:08:14 +09:00
98ddb348b0 Update changelog 2023-04-10 20:21:13 +09:00
a4aa85ebab Fix: If connection to Lightning node was interrupted, payments would be missed () 2023-04-10 19:35:01 +09:00
516efe56f4 Refactor: Dot not make LNURLPaymentMethodDetails depends on BTCPayInvoiceId ()
* Refactor: Dot not make LNURLPaymentMethodDetails depends on BTCPayInvoiceId

* Abstract PaymentProof

* fix bug

* Make the selenium container resolves the btcpay host name
2023-04-10 16:38:49 +09:00
280 changed files with 5959 additions and 2821 deletions
.github
.run
BTCPayServer.Abstractions
BTCPayServer.Client
BTCPayServer.Data
BTCPayServer.Tests
BTCPayServer
BTCPayServer.csproj
Components
Controllers
Data
EventAggregator.csExtensions.cs
Extensions
Forms
HostedServices
Hosting
Models
Payments
PayoutProcessors
Plugins
Properties
SearchString.cs
Security
Services
StorePolicies.cs
Views
wwwroot
Build
Changelog.md
Plugins/BTCPayServer.Plugins.Test
README.mdSECURITY.mdbtcpayserver.sln

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

@ -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,21 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Pack Test Plugin" type="DotNetProject" factoryName=".NET Project" singleton="false">
<option name="EXE_PATH" value="$PROJECT_DIR$/BTCPayServer.PluginPacker/bin/Debug/netcoreapp3.1/BTCPayServer.PluginPacker.dll" />
<option name="PROGRAM_PARAMETERS" value="../../../../BTCPayServer.Plugins.Test\bin\Debug\netcoreapp3.1 BTCPayServer.Plugins.Test &quot;../../../../Packed Plugins&quot;" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/BTCPayServer.PluginPacker/bin/Debug/netcoreapp3.1" />
<option name="PASS_PARENT_ENVS" value="1" />
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" />
<option name="RUNTIME_ARGUMENTS" value="" />
<option name="PROJECT_PATH" value="$PROJECT_DIR$/BTCPayServer.PluginPacker/BTCPayServer.PluginPacker.csproj" />
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
<option name="PROJECT_KIND" value="DotNetCore" />
<option name="PROJECT_TFM" value=".NETCoreApp,Version=v3.1" />
<method v="2">
<option name="Build" default="false" projectName="BTCPayServer.Plugins.Test" projectPath="C:\Git\btcpayserver\Plugins\BTCPayServer.Plugins.Test\BTCPayServer.Plugins.Test.csproj" />
<option name="Build" />
</method>
</configuration>
</component>

@ -1,4 +1,5 @@
using System;
using System.Data.Common;
using BTCPayServer.Abstractions.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
@ -21,7 +22,6 @@ namespace BTCPayServer.Abstractions.Contracts
}
public abstract T CreateContext();
class CustomNpgsqlMigrationsSqlGenerator : NpgsqlMigrationsSqlGenerator
{
#pragma warning disable EF1001 // Internal EF Core API usage.

@ -69,7 +69,6 @@ public class Form
if (!nameReturned.Add(fullName))
{
errors.Add($"Form contains duplicate field names '{fullName}'");
continue;
}
}
return errors.Count == 0;
@ -86,15 +85,10 @@ public class Form
thisPath.Add(field.Name);
yield return (thisPath, field);
}
foreach (var child in field.Fields)
foreach (var descendant in GetAllFieldsCore(thisPath, field.Fields))
{
if (field.Constant)
child.Constant = true;
foreach (var descendant in GetAllFieldsCore(thisPath, field.Fields))
{
yield return descendant;
}
descendant.Field.Constant = field.Constant || descendant.Field.Constant;
yield return descendant;
}
}
}

@ -6,7 +6,8 @@ using Microsoft.Extensions.Logging;
namespace BTCPayServer.Abstractions.TagHelpers;
[HtmlTargetElement(Attributes = nameof(Permission))]
[HtmlTargetElement(Attributes = "[permission]")]
[HtmlTargetElement(Attributes = "[not-permission]" )]
public class PermissionTagHelper : TagHelper
{
private readonly IAuthorizationService _authorizationService;
@ -21,16 +22,19 @@ public class PermissionTagHelper : TagHelper
}
public string Permission { get; set; }
public string NotPermission { get; set; }
public string PermissionResource { get; set; }
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
if (string.IsNullOrEmpty(Permission))
if (string.IsNullOrEmpty(Permission) && string.IsNullOrEmpty(NotPermission))
return;
if (_httpContextAccessor.HttpContext is null)
return;
var key = $"{Permission}_{PermissionResource}";
var expectedResult = !string.IsNullOrEmpty(Permission);
var key = $"{Permission??NotPermission}_{PermissionResource}";
if (!_httpContextAccessor.HttpContext.Items.TryGetValue(key, out var o) ||
o is not AuthorizationResult res)
{
@ -39,7 +43,7 @@ public class PermissionTagHelper : TagHelper
Permission);
_httpContextAccessor.HttpContext.Items.Add(key, res);
}
if (!res.Succeeded)
if (expectedResult != res.Succeeded)
{
output.SuppressOutput();
}

@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
@ -11,5 +12,11 @@ namespace BTCPayServer.Client
var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/server/info"), token);
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 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,
CancellationToken token = default)
{

@ -51,7 +51,8 @@ namespace BTCPayServer.Client
{
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);
}
if (message.StatusCode == System.Net.HttpStatusCode.Forbidden)

@ -86,6 +86,7 @@ namespace BTCPayServer.Client.Models
public bool? RequiresRefundEmail { get; set; } = null;
public string DefaultLanguage { get; set; }
public CheckoutType? CheckoutType { get; set; }
public bool? LazyPaymentMethods { get; set; }
}
}
public class InvoiceData : InvoiceDataBase

@ -3,7 +3,6 @@ namespace BTCPayServer.Client.Models
public class LNURLPayPaymentMethodBaseData
{
public bool UseBech32Scheme { get; set; }
public bool EnableForStandardInvoices { get; set; }
public bool LUD12Enabled { get; set; }
public LNURLPayPaymentMethodBaseData()

@ -16,12 +16,11 @@ namespace BTCPayServer.Client.Models
{
}
public LNURLPayPaymentMethodData(string cryptoCode, bool enabled, bool useBech32Scheme, bool enableForStandardInvoices)
public LNURLPayPaymentMethodData(string cryptoCode, bool enabled, bool useBech32Scheme)
{
Enabled = enabled;
CryptoCode = cryptoCode;
UseBech32Scheme = useBech32Scheme;
EnableForStandardInvoices = enableForStandardInvoices;
}
}
}

@ -4,7 +4,6 @@ namespace BTCPayServer.Client.Models
{
public string ConnectionString { get; set; }
public bool DisableBOLT11PaymentOption { get; set; }
public LightningNetworkPaymentMethodBaseData()
{

@ -16,13 +16,12 @@ namespace BTCPayServer.Client.Models
{
}
public LightningNetworkPaymentMethodData(string cryptoCode, string connectionString, bool enabled, string paymentMethod, bool disableBOLT11PaymentOption)
public LightningNetworkPaymentMethodData(string cryptoCode, string connectionString, bool enabled, string paymentMethod)
{
Enabled = enabled;
CryptoCode = cryptoCode;
ConnectionString = connectionString;
PaymentMethod = paymentMethod;
DisableBOLT11PaymentOption = disableBOLT11PaymentOption;
}
public string PaymentMethod { get; set; }

@ -9,6 +9,7 @@ namespace BTCPayServer.Client.Models
{
RateThen,
CurrentRate,
OverpaidAmount,
Fiat,
Custom
}
@ -18,8 +19,13 @@ namespace BTCPayServer.Client.Models
public string? Name { get; set; } = null;
public string? PaymentMethod { get; set; }
public string? Description { get; set; } = null;
[JsonConverter(typeof(StringEnumConverter))]
public RefundVariant? RefundVariant { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal SubtractPercentage { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal? CustomAmount { get; set; }
public string? CustomCurrency { get; set; }

@ -16,6 +16,8 @@ namespace BTCPayServer.Client.Models
public string Website { get; set; }
public string SupportUrl { get; set; }
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public TimeSpan InvoiceExpiration { get; set; } = TimeSpan.FromMinutes(15);

@ -1,3 +1,5 @@
using System.Collections.Generic;
namespace BTCPayServer.Client.Models
{
public class StoreData : StoreBaseData
@ -17,4 +19,12 @@ namespace BTCPayServer.Client.Models
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; }
}
}

@ -51,6 +51,10 @@ namespace BTCPayServer.Client.Models
public DateTimeOffset Timestamp { get; set; }
[JsonExtensionData]
public IDictionary<string, JToken> AdditionalData { get; set; }
public bool IsPruned()
{
return DeliveryId is null;
}
public T ReadAs<T>()
{
var str = JsonConvert.SerializeObject(this, DefaultSerializerSettings);

@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Linq.Expressions;
namespace BTCPayServer.Client
{
@ -134,7 +136,7 @@ namespace BTCPayServer.Client
{
static Permission()
{
Init();
PolicyMap = Init();
}
public static Permission Create(string policy, string scope = null)
@ -235,11 +237,13 @@ namespace BTCPayServer.Client
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.CanManagePullPayments,
Policies.CanModifyInvoices,
@ -248,25 +252,42 @@ namespace BTCPayServer.Client
Policies.CanModifyPaymentRequests,
Policies.CanUseLightningNodeInStore);
PolicyHasChild(Policies.CanManageUsers, Policies.CanCreateUser);
PolicyHasChild(Policies.CanManagePullPayments, Policies.CanCreatePullPayments);
PolicyHasChild(Policies.CanCreatePullPayments, Policies.CanCreateNonApprovedPullPayments);
PolicyHasChild(Policies.CanModifyPaymentRequests, Policies.CanViewPaymentRequests);
PolicyHasChild(Policies.CanModifyProfile, Policies.CanViewProfile);
PolicyHasChild(Policies.CanUseLightningNodeInStore, Policies.CanViewLightningInvoiceInStore, Policies.CanCreateLightningInvoiceInStore);
PolicyHasChild(Policies.CanManageNotificationsForUser, Policies.CanViewNotificationsForUser);
PolicyHasChild(Policies.CanModifyServerSettings,
PolicyHasChild(policyMap,Policies.CanManageUsers, Policies.CanCreateUser);
PolicyHasChild(policyMap,Policies.CanManagePullPayments, Policies.CanCreatePullPayments);
PolicyHasChild(policyMap,Policies.CanCreatePullPayments, Policies.CanCreateNonApprovedPullPayments);
PolicyHasChild(policyMap,Policies.CanModifyPaymentRequests, Policies.CanViewPaymentRequests);
PolicyHasChild(policyMap,Policies.CanModifyProfile, Policies.CanViewProfile);
PolicyHasChild(policyMap,Policies.CanUseLightningNodeInStore, Policies.CanViewLightningInvoiceInStore, Policies.CanCreateLightningInvoiceInStore);
PolicyHasChild(policyMap,Policies.CanManageNotificationsForUser, Policies.CanViewNotificationsForUser);
PolicyHasChild(policyMap,Policies.CanModifyServerSettings,
Policies.CanUseInternalLightningNode,
Policies.CanManageUsers);
PolicyHasChild(Policies.CanUseInternalLightningNode, Policies.CanCreateLightningInvoiceInternalNode, Policies.CanViewLightningInvoiceInternalNode);
PolicyHasChild(Policies.CanManageCustodianAccounts, Policies.CanViewCustodianAccounts);
PolicyHasChild(Policies.CanModifyInvoices, Policies.CanViewInvoices, Policies.CanCreateInvoice, Policies.CanCreateLightningInvoiceInStore);
PolicyHasChild(Policies.CanViewStoreSettings, Policies.CanViewInvoices, Policies.CanViewPaymentRequests);
PolicyHasChild(policyMap, Policies.CanUseInternalLightningNode, Policies.CanCreateLightningInvoiceInternalNode, Policies.CanViewLightningInvoiceInternalNode);
PolicyHasChild(policyMap, Policies.CanManageCustodianAccounts, Policies.CanViewCustodianAccounts);
PolicyHasChild(policyMap, Policies.CanModifyInvoices, Policies.CanViewInvoices, Policies.CanCreateInvoice, Policies.CanCreateLightningInvoiceInStore);
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)
{
@ -275,7 +296,7 @@ namespace BTCPayServer.Client
}
else
{
PolicyMap.Add(policy, subPolicies.ToHashSet());
policyMap.Add(policy, subPolicies.ToHashSet());
}
}

@ -64,6 +64,7 @@ namespace BTCPayServer.Data
public DbSet<U2FDevice> U2FDevices { get; set; }
public DbSet<Fido2Credential> Fido2Credentials { get; set; }
public DbSet<UserStore> UserStore { get; set; }
public DbSet<StoreRole> StoreRoles { get; set; }
[Obsolete]
public DbSet<WalletData> Wallets { get; set; }
public DbSet<WalletObjectData> WalletObjects { get; set; }
@ -129,6 +130,7 @@ namespace BTCPayServer.Data
PayoutProcessorData.OnModelCreating(builder, Database);
WebhookData.OnModelCreating(builder, Database);
FormData.OnModelCreating(builder, Database);
StoreRole.OnModelCreating(builder, Database);
if (Database.IsSqlite() && !_designTime)

@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Data;
@ -41,4 +43,7 @@ public class LightningAddressDataBlob
public decimal? Max { get; set; }
public JObject InvoiceMetadata { get; set; }
[JsonExtensionData] public Dictionary<string, JToken> AdditionalData { get; set; }
}

@ -1,13 +1,10 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using PayoutProcessorData = BTCPayServer.Data.PayoutProcessorData;
namespace BTCPayServer.Data
{
@ -37,8 +34,6 @@ namespace BTCPayServer.Data
public byte[] StoreCertificate { get; set; }
[NotMapped] public string Role { get; set; }
public string StoreBlob { get; set; }
[Obsolete("Use GetDefaultPaymentId instead")]
@ -52,6 +47,7 @@ namespace BTCPayServer.Data
public IEnumerable<CustodianAccountData> CustodianAccounts { get; set; }
public IEnumerable<StoreSettingData> Settings { get; set; }
public IEnumerable<FormData> Forms { get; set; }
public IEnumerable<StoreRole> StoreRoles { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{

@ -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;
namespace BTCPayServer.Data
@ -9,7 +10,10 @@ namespace BTCPayServer.Data
public string StoreDataId { 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)
@ -32,6 +36,10 @@ namespace BTCPayServer.Data
.HasOne(pt => pt.StoreData)
.WithMany(t => t.UserStores)
.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
{
public class WebhookDeliveryData : IHasBlobUntyped
public class WebhookDeliveryData
{
[Key]
[MaxLength(25)]
@ -17,10 +17,8 @@ namespace BTCPayServer.Data
[Required]
public DateTimeOffset Timestamp { get; set; }
[Obsolete("Use Blob2 instead")]
public byte[] Blob { get; set; }
public string Blob2 { get; set; }
public string Blob { get; set; }
public bool Pruned { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{
@ -28,11 +26,11 @@ namespace BTCPayServer.Data
.HasOne(o => o.Webhook)
.WithMany(a => a.Deliveries).OnDelete(DeleteBehavior.Cascade);
builder.Entity<WebhookDeliveryData>().HasIndex(o => o.WebhookId);
builder.Entity<WebhookDeliveryData>().HasIndex(o => o.Timestamp);
if (databaseFacade.IsNpgsql())
{
builder.Entity<WebhookDeliveryData>()
.Property(o => o.Blob2)
.Property(o => o.Blob)
.HasColumnType("JSONB");
}
}

@ -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)
{
}
}
}

@ -214,56 +214,6 @@ namespace BTCPayServer.Migrations
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 =>
{
b.Property<string>("Id")
@ -292,6 +242,31 @@ namespace BTCPayServer.Migrations
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 =>
{
b.Property<string>("Id")
@ -655,6 +630,34 @@ namespace BTCPayServer.Migrations
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 =>
{
b.Property<string>("Id")
@ -802,6 +805,28 @@ namespace BTCPayServer.Migrations
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 =>
{
b.Property<string>("StoreId")
@ -878,13 +903,16 @@ namespace BTCPayServer.Migrations
b.Property<string>("StoreDataId")
.HasColumnType("TEXT");
b.Property<string>("Role")
.HasColumnType("TEXT");
b.Property<string>("StoreRoleId")
.HasColumnType("TEXT")
.HasColumnName("Role");
b.HasKey("ApplicationUserId", "StoreDataId");
b.HasIndex("StoreDataId");
b.HasIndex("StoreRoleId");
b.ToTable("UserStore");
});
@ -991,12 +1019,12 @@ namespace BTCPayServer.Migrations
.HasMaxLength(25)
.HasColumnType("TEXT");
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
b.Property<string>("Blob2")
b.Property<string>("Blob")
.HasColumnType("TEXT");
b.Property<bool>("Pruned")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Timestamp")
.HasColumnType("TEXT");
@ -1007,6 +1035,8 @@ namespace BTCPayServer.Migrations
b.HasKey("Id");
b.HasIndex("Timestamp");
b.HasIndex("WebhookId");
b.ToTable("WebhookDeliveries");
@ -1188,26 +1218,6 @@ namespace BTCPayServer.Migrations
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 =>
{
b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser")
@ -1218,6 +1228,16 @@ namespace BTCPayServer.Migrations
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 =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
@ -1343,6 +1363,16 @@ namespace BTCPayServer.Migrations
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 =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
@ -1392,6 +1422,16 @@ namespace BTCPayServer.Migrations
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 =>
{
b.HasOne("BTCPayServer.Data.StoreData", "Store")
@ -1446,9 +1486,15 @@ namespace BTCPayServer.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("BTCPayServer.Data.StoreRole", "StoreRole")
.WithMany("Users")
.HasForeignKey("StoreRoleId");
b.Navigation("ApplicationUser");
b.Navigation("StoreData");
b.Navigation("StoreRole");
});
modelBuilder.Entity("BTCPayServer.Data.WalletObjectLinkData", b =>
@ -1606,9 +1652,16 @@ namespace BTCPayServer.Migrations
b.Navigation("Settings");
b.Navigation("StoreRoles");
b.Navigation("UserStores");
});
modelBuilder.Entity("BTCPayServer.Data.StoreRole", b =>
{
b.Navigation("Users");
});
modelBuilder.Entity("BTCPayServer.Data.WalletData", b =>
{
b.Navigation("WalletTransactions");

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Hosting;
using BTCPayServer.Lightning;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Models.StoreViewModels;
@ -51,6 +52,7 @@ namespace BTCPayServer.Tests
user.RegisterDerivationScheme(cryptoCode);
user.RegisterDerivationScheme("LTC");
user.RegisterLightningNode(cryptoCode, LightningConnectionType.CLightning);
user.SetLNUrl("BTC", false);
var btcNetwork = tester.PayTester.Networks.GetNetwork<BTCPayNetwork>(cryptoCode);
var invoice = await user.BitPay.CreateInvoiceAsync(
new Invoice
@ -244,7 +246,7 @@ namespace BTCPayServer.Tests
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
user.GrantAccess(true);
user.RegisterLightningNode("BTC", LightningConnectionType.Charge);
user.RegisterLightningNode("BTC");
user.RegisterDerivationScheme("BTC");
user.RegisterDerivationScheme("LTC");
@ -650,6 +652,7 @@ donation:
price: 1.02
custom: true
";
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
Assert.Equal("hello", vmpos.Title);
@ -661,7 +664,6 @@ donation:
Assert.Equal("good apple", vmview.Items[0].Title);
Assert.Equal("orange", vmview.Items[1].Title);
Assert.Equal(10.0m, vmview.Items[1].Price.Value);
Assert.Equal("$5.00", vmview.Items[0].Price.Formatted);
Assert.Equal("{0} Purchase", vmview.ButtonText);
Assert.Equal("Nicolas Sexy Hair", vmview.CustomButtonText);
Assert.Equal("Wanna tip?", vmview.CustomTipText);
@ -722,6 +724,7 @@ donation:
price: 1.02
custom: true
";
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
publicApps = user.GetController<UIPointOfSaleController>();
vmview = await publicApps.ViewPointOfSale(app.Id, PosViewType.Cart).AssertViewModelAsync<ViewPointOfSaleViewModel>();
@ -749,6 +752,8 @@ inventoryitem:
inventory: 1
noninventoryitem:
price: 10.0";
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
//inventoryitem has 1 item available
@ -779,15 +784,13 @@ noninventoryitem:
//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 appService = tester.PayTester.GetService<AppService>();
var eventAggregator = tester.PayTester.GetService<EventAggregator>();
Assert.IsType<JsonResult>(await controller.ChangeInvoiceState(inventoryItemInvoice.Id, "invalid"));
//check that item is back in stock
await TestUtils.EventuallyAsync(async () =>
{
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
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);
//test payment methods option
@ -802,6 +805,8 @@ btconly:
- BTC
normal:
price: 1.0";
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, null, null, null, null, "btconly").Result);
@ -846,18 +851,19 @@ g:
custom: topup
";
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
Assert.DoesNotContain("custom", vmpos.Template);
var items = appService.Parse(vmpos.Template, vmpos.Currency);
Assert.Contains(items, item => item.Id == "a" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed);
Assert.Contains(items, item => item.Id == "b" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed);
Assert.Contains(items, item => item.Id == "c" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Minimum);
Assert.Contains(items, item => item.Id == "d" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed);
Assert.Contains(items, item => item.Id == "e" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Minimum);
Assert.Contains(items, item => item.Id == "f" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup);
Assert.Contains(items, item => item.Id == "g" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup);
var items = AppService.Parse(vmpos.Template);
Assert.Contains(items, item => item.Id == "a" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed);
Assert.Contains(items, item => item.Id == "b" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed);
Assert.Contains(items, item => item.Id == "c" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Minimum);
Assert.Contains(items, item => item.Id == "d" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed);
Assert.Contains(items, item => item.Id == "e" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Minimum);
Assert.Contains(items, item => item.Id == "f" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup);
Assert.Contains(items, item => item.Id == "g" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup);
Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(app.Id, PosViewType.Static, null, null, null, null, null, "g").Result);
invoices = user.BitPay.GetInvoices();

@ -23,7 +23,7 @@
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14" />
<PackageReference Include="Selenium.Support" Version="4.1.1" />
<PackageReference Include="Selenium.WebDriver" Version="4.1.1" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="110.0.5481.7700" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="112.0.5615.4900" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets>

@ -164,7 +164,7 @@ namespace BTCPayServer.Tests
var invoiceId = s.CreateInvoice(defaultPaymentMethod: "BTC_LightningLike");
s.GoToInvoiceCheckout(invoiceId);
Assert.Equal("Bitcoin (Lightning) (BTC)", s.Driver.FindElement(By.ClassName("payment__currencies")).Text);
Assert.Equal("Bitcoin (Lightning)", s.Driver.FindElement(By.ClassName("payment__currencies")).Text);
s.Driver.Quit();
}
@ -187,7 +187,7 @@ namespace BTCPayServer.Tests
var invoiceId = s.CreateInvoice(10, "USD", "a@g.com");
s.GoToInvoiceCheckout(invoiceId);
Assert.Contains("sats", s.Driver.FindElement(By.ClassName("payment__currencies_noborder")).Text);
Assert.Contains("sats", s.Driver.FindElement(By.ClassName("buyerTotalLine")).Text);
}
[Fact(Timeout = TestTimeout)]

@ -1,13 +1,9 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using BTCPayServer.Payments;
using BTCPayServer.Tests.Logging;
using BTCPayServer.Views.Stores;
using NBitcoin;
using OpenQA.Selenium;
using OpenQA.Selenium.Support.Extensions;
using OpenQA.Selenium.Support.UI;
using Xunit;
using Xunit.Abstractions;
@ -40,19 +36,13 @@ namespace BTCPayServer.Tests
// Configure store url
var storeUrl = "https://satoshisteaks.com/";
var supportUrl = "https://support.satoshisteaks.com/{InvoiceId}/";
s.GoToStore();
s.Driver.FindElement(By.Id("StoreWebsite")).SendKeys(storeUrl);
s.Driver.FindElement(By.Id("StoreSupportUrl")).SendKeys(supportUrl);
s.Driver.FindElement(By.Id("Save")).Click();
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
// Enable LNURL, which we will need for (non-)presence checks throughout this test
s.GoToHome();
s.GoToLightningSettings();
s.Driver.SetCheckbox(By.Id("LNURLEnabled"), true);
s.Driver.SetCheckbox(By.Id("LNURLStandardInvoiceEnabled"), true);
s.Driver.FindElement(By.Id("save")).Click();
Assert.Contains("BTC Lightning settings successfully updated", s.FindAlertMessage().Text);
s.GoToStore(StoreNavPages.CheckoutAppearance);
s.Driver.WaitForAndClick(By.Id("Presets"));
s.Driver.WaitForAndClick(By.Id("Presets_InStore"));
@ -72,9 +62,9 @@ namespace BTCPayServer.Tests
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 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.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.Equal(address, copyAddress);
Assert.Equal($"bitcoin:{address.ToUpperInvariant()}", qrValue);
@ -94,7 +84,7 @@ namespace BTCPayServer.Tests
{
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
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"));
});
@ -109,7 +99,7 @@ namespace BTCPayServer.Tests
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
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(address, copyAddress);
Assert.Equal($"lightning:{address.ToUpperInvariant()}", qrValue);
@ -148,8 +138,47 @@ namespace BTCPayServer.Tests
var expiredSection = s.Driver.FindElement(By.Id("unpaid"));
Assert.True(expiredSection.Displayed);
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"));
// Test payment
@ -174,7 +203,7 @@ namespace BTCPayServer.Tests
// Pay partial amount
await Task.Delay(200);
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),
Money.Parse(amountFraction));
await s.Server.ExplorerNode.GenerateAsync(1);
@ -188,8 +217,10 @@ namespace BTCPayServer.Tests
});
s.Driver.Navigate().Refresh();
// Pay full amount
s.PayInvoice();
// Processing
TestUtils.Eventually(() =>
{
@ -197,8 +228,9 @@ namespace BTCPayServer.Tests
Assert.True(processingSection.Displayed);
Assert.Contains("Payment Received", processingSection.Text);
Assert.Contains("Your payment has been received and is now processing", processingSection.Text);
Assert.True(s.Driver.ElementDoesNotExist(By.Id("confetti")));
});
s.Driver.FindElement(By.Id("confetti"));
// Mine
s.MineBlockOnInvoiceCheckout();
TestUtils.Eventually(() =>
@ -215,7 +247,8 @@ namespace BTCPayServer.Tests
Assert.Contains("Invoice Paid", settledSection.Text);
});
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"));
// BIP21
@ -234,8 +267,8 @@ namespace BTCPayServer.Tests
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
var copyAddressOnchain = s.Driver.FindElement(By.Id("Address_BTC")).GetAttribute("value");
var copyAddressLightning = s.Driver.FindElement(By.Id("Lightning_BTC")).GetAttribute("value");
var copyAddressOnchain = s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text;
var copyAddressLightning = s.Driver.FindElement(By.CssSelector("#Lightning_BTC .truncate-center-start")).Text;
Assert.StartsWith($"bitcoin:{address}?amount=", payUrl);
Assert.Contains("?amount=", payUrl);
Assert.Contains("&lightning=", payUrl);
@ -293,7 +326,6 @@ namespace BTCPayServer.Tests
s.GoToHome();
s.GoToLightningSettings();
Assert.True(s.Driver.FindElement(By.Id("LNURLEnabled")).Selected);
Assert.True(s.Driver.FindElement(By.Id("LNURLStandardInvoiceEnabled")).Selected);
// BIP21 with top-up invoice
invoiceId = s.CreateInvoice(amount: null);
@ -303,8 +335,8 @@ namespace BTCPayServer.Tests
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
payUrl = s.Driver.FindElement(By.Id("PayInWallet")).GetAttribute("href");
copyAddressOnchain = s.Driver.FindElement(By.Id("Address_BTC")).GetAttribute("value");
copyAddressLightning = s.Driver.FindElement(By.Id("Lightning_BTC")).GetAttribute("value");
copyAddressOnchain = s.Driver.FindElement(By.CssSelector("#Address_BTC .truncate-center-start")).Text;
copyAddressLightning = s.Driver.FindElement(By.CssSelector("#Lightning_BTC .truncate-center-start")).Text;
Assert.StartsWith($"bitcoin:{address}", payUrl);
Assert.Contains("?lightning=lnurl", payUrl);
Assert.DoesNotContain("amount=", payUrl);
@ -364,6 +396,7 @@ namespace BTCPayServer.Tests
s.GoToHome();
s.GoToLightningSettings();
s.Driver.SetCheckbox(By.Id("LNURLEnabled"), false);
s.Driver.ScrollTo(By.Id("save"));
s.Driver.FindElement(By.Id("save")).Click();
Assert.Contains("BTC Lightning settings successfully updated", s.FindAlertMessage().Text);
@ -389,7 +422,7 @@ namespace BTCPayServer.Tests
Assert.Contains("lang=de", s.Driver.Url);
s.Driver.Navigate().Refresh();
languageSelect = new SelectElement(s.Driver.FindElement(By.Id("DefaultLang")));
languageSelect = new SelectElement(s.Driver.WaitForElement(By.Id("DefaultLang")));
Assert.Equal("Deutsch", languageSelect.SelectedOption.Text);
Assert.Equal("Details anzeigen", s.Driver.FindElement(By.Id("DetailsToggle")).Text);
languageSelect.SelectByText("English");

@ -1,5 +1,6 @@
using System;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
@ -55,7 +56,7 @@ namespace BTCPayServer.Tests
Assert.Empty(appList2.Apps);
Assert.Equal("test", appList.Apps[0].AppName);
Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id);
Assert.True(appList.Apps[0].IsOwner);
Assert.True(appList.Apps[0].Role.ToPermissionSet(appList.Apps[0].StoreId).Contains(Policies.CanModifyStoreSettings, appList.Apps[0].StoreId));
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id));
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id));

@ -26,6 +26,7 @@ using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Labels;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Validation;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
@ -1042,14 +1043,13 @@ namespace BTCPayServer.Tests
[Fact]
public void CanParseFilter()
{
var storeId = "6DehZnc9S7qC6TUTNWuzJ1pFsHTHvES6An21r3MjvLey";
var filter = "storeid:abc, status:abed, blabhbalh ";
var search = new SearchString(filter);
Assert.Equal("storeid:abc, status:abed, blabhbalh", search.ToString());
Assert.Equal("blabhbalh", search.TextSearch);
Assert.Single(search.Filters["storeid"]);
Assert.Single(search.Filters["status"]);
Assert.Equal("abc", search.Filters["storeid"].First());
Assert.Equal("abed", search.Filters["status"].First());
Assert.Single(search.Filters["storeid"], "abc");
Assert.Single(search.Filters["status"], "abed");
filter = "status:abed, status:abed2";
search = new SearchString(filter);
@ -1064,6 +1064,48 @@ namespace BTCPayServer.Tests
search = new SearchString(filter);
Assert.Equal("2019-04-25 01:00 AM", search.Filters["startdate"].First());
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]
@ -1341,6 +1383,24 @@ namespace BTCPayServer.Tests
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]
public void KitchenSinkTest()
{

@ -15,12 +15,16 @@ using BTCPayServer.Lightning;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.PayoutProcessors;
using BTCPayServer.PayoutProcessors.OnChain;
using BTCPayServer.Services;
using BTCPayServer.Services.Custodian.Client.MockCustodian;
using BTCPayServer.Services.Notifications;
using BTCPayServer.Services.Notifications.Blobs;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
using NBitcoin;
using NBitpayClient;
using Newtonsoft.Json;
@ -225,7 +229,7 @@ namespace BTCPayServer.Tests
await Assert.ThrowsAsync<GreenfieldAPIException>(() => newUserClient.GetInvoices(store.Id));
// 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);
}
@ -1316,7 +1320,8 @@ namespace BTCPayServer.Tests
// We strip the user's Owner right, so the key should not work
using var ctx = tester.PayTester.GetService<Data.ApplicationDbContextFactory>().CreateContext();
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 AssertHttpError(403, async () => await client.UpdateStore(newStore.Id, new UpdateStoreRequest() { Name = "B" }));
@ -1427,7 +1432,7 @@ namespace BTCPayServer.Tests
Assert.False(hook.AutomaticRedelivery);
Assert.Equal(fakeServer.ServerUri.AbsoluteUri, hook.Url);
}
using var tester = CreateServerTester();
using var tester = CreateServerTester(newDb: true);
using var fakeServer = new FakeServer();
await fakeServer.Start();
await tester.StartAsync();
@ -1474,6 +1479,7 @@ namespace BTCPayServer.Tests
var newDeliveryId = await clientProfile.RedeliverWebhook(user.StoreId, hook.Id, delivery.Id);
req = await fakeServer.GetNextRequest();
req.Response.StatusCode = 404;
Assert.StartsWith("BTCPayServer", Assert.Single(req.Request.Headers.UserAgent));
await TestUtils.EventuallyAsync(async () =>
{
// Releasing semaphore several times may help making this test less flaky
@ -1503,6 +1509,14 @@ namespace BTCPayServer.Tests
clientProfile = await user.CreateClient(Policies.CanModifyStoreSettings, Policies.CanCreateInvoice);
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");
Assert.Null(await clientProfile.GetWebhookDeliveryRequest(user.StoreId, "lol", newDeliveryId));
Assert.Null(await clientProfile.GetWebhookDeliveryRequest(user.StoreId, hook.Id, "lol"));
@ -1946,6 +1960,82 @@ namespace BTCPayServer.Tests
CustomCurrency = "BTC"
});
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)]
@ -2253,7 +2343,7 @@ namespace BTCPayServer.Tests
Assert.Single(paymentMethods);
Assert.True(paymentMethods.First().Activated);
var invoiceWithdefaultPaymentMethodLN = await client.CreateInvoice(user.StoreId,
var invoiceWithDefaultPaymentMethodLN = await client.CreateInvoice(user.StoreId,
new CreateInvoiceRequest()
{
Currency = "USD",
@ -2264,9 +2354,9 @@ namespace BTCPayServer.Tests
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()
{
Currency = "USD",
@ -2277,13 +2367,35 @@ namespace BTCPayServer.Tests
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.LazyPaymentMethods = false;
store = await client.UpdateStore(store.Id,
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
invoice = await client.CreateInvoice(user.StoreId,
@ -2340,27 +2452,10 @@ namespace BTCPayServer.Tests
Assert.NotNull(merchantInvoice.PaymentHash);
Assert.Equal(merchantInvoice.Id, merchantInvoice.PaymentHash);
// The default client is using charge, so we should not be able to query channels
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"));
var client = await user.CreateClient(Policies.CanUseInternalLightningNode);
// Not permission for the store!
await AssertAPIError("missing-permission", () => chargeClient.GetLightningNodeChannels(user.StoreId, "BTC"));
var invoiceData = await chargeClient.CreateLightningInvoice("BTC", new CreateLightningInvoiceRequest()
await AssertAPIError("missing-permission", () => client.GetLightningNodeChannels(user.StoreId, "BTC"));
var invoiceData = await client.CreateLightningInvoice("BTC", new CreateLightningInvoiceRequest()
{
Amount = LightMoney.Satoshis(1000),
Description = "lol",
@ -2368,17 +2463,17 @@ namespace BTCPayServer.Tests
PrivateRouteHints = false
});
var chargeInvoice = invoiceData;
Assert.NotNull(await chargeClient.GetLightningInvoice("BTC", invoiceData.Id));
Assert.NotNull(await client.GetLightningInvoice("BTC", invoiceData.Id));
// check list for internal node
var invoices = await chargeClient.GetLightningInvoices("BTC");
var pendingInvoices = await chargeClient.GetLightningInvoices("BTC", true);
var invoices = await client.GetLightningInvoices("BTC");
var pendingInvoices = await client.GetLightningInvoices("BTC", true);
Assert.NotEmpty(invoices);
Assert.Contains(invoices, i => i.Id == invoiceData.Id);
Assert.NotEmpty(pendingInvoices);
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
await AssertAPIError("missing-permission", () => client.GetLightningNodeChannels("BTC"));
@ -2457,7 +2552,7 @@ namespace BTCPayServer.Tests
Assert.Contains(payments, i => i.BOLT11 == merchantInvoice.BOLT11);
// Node info
info = await client.GetLightningNodeInfo(user.StoreId, "BTC");
var info = await client.GetLightningNodeInfo(user.StoreId, "BTC");
Assert.Single(info.NodeURIs);
Assert.NotEqual(0, info.BlockHeight);
@ -2498,7 +2593,12 @@ namespace BTCPayServer.Tests
user.RegisterLightningNode("BTC", LightningConnectionType.CLightning);
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
{
Currency = "USD",
@ -2509,18 +2609,35 @@ namespace BTCPayServer.Tests
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);
Assert.Equal(PayResult.Ok, resp.Result);
Assert.NotNull(resp.Details.PaymentHash);
Assert.NotNull(resp.Details.Preimage);
var pm = new InvoicePaymentMethodDataModel[invoices.Length];
for (int i = 0; i < invoices.Length; i++)
{
pm[i] = Assert.Single(await client.GetInvoicePaymentMethods(user.StoreId, (await invoices[i]).Id));
Assert.False(pm[i].AdditionalData.HasValues);
}
pm = Assert.Single(await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id));
Assert.True(pm.AdditionalData.HasValues);
Assert.Equal(resp.Details.PaymentHash.ToString(), pm.AdditionalData.GetValue("paymentHash"));
Assert.Equal(resp.Details.Preimage.ToString(), pm.AdditionalData.GetValue("preimage"));
// Pay them all at once
Task<PayResponse>[] payResponses = new Task<PayResponse>[invoices.Length];
for (int i = 0; i < invoices.Length; i++)
{
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)]
@ -3258,11 +3375,16 @@ namespace BTCPayServer.Tests
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 storeuser = Assert.Single(users);
Assert.Equal(user.UserId, storeuser.UserId);
Assert.Equal(StoreRoles.Owner, storeuser.Role);
Assert.Equal(ownerRole.Id, storeuser.Role);
var user2 = tester.NewAccount();
await user2.GrantAccessAsync(false);
@ -3273,7 +3395,7 @@ namespace BTCPayServer.Tests
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.AddStoreUser(user.StoreId, new StoreUserData()));
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.RemoveStoreUser(user.StoreId, user.UserId));
await client.AddStoreUser(user.StoreId, new StoreUserData() { Role = StoreRoles.Guest, UserId = user2.UserId });
await client.AddStoreUser(user.StoreId, new StoreUserData() { Role = guestRole.Id, UserId = user2.UserId });
//test no access to api when only a guest
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.GetStoreUsers(user.StoreId));
@ -3287,10 +3409,10 @@ namespace BTCPayServer.Tests
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 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);
@ -3408,8 +3530,8 @@ namespace BTCPayServer.Tests
});
Assert.Empty(await adminClient.GetStoreLightningAutomatedPayoutProcessors(admin.StoreId, "BTC_LightningNetwork"));
await adminClient.UpdateStoreLightningAutomatedPayoutProcessors(admin.StoreId, "BTC_LightningNetwork",
new LightningAutomatedPayoutSettings() { IntervalSeconds = TimeSpan.FromSeconds(2) });
Assert.Equal(2, Assert.Single(await adminClient.GetStoreLightningAutomatedPayoutProcessors(admin.StoreId, "BTC_LightningNetwork")).IntervalSeconds.TotalSeconds);
new LightningAutomatedPayoutSettings() { IntervalSeconds = TimeSpan.FromSeconds(600) });
Assert.Equal(600, Assert.Single(await adminClient.GetStoreLightningAutomatedPayoutProcessors(admin.StoreId, "BTC_LightningNetwork")).IntervalSeconds.TotalSeconds);
await TestUtils.EventuallyAsync(async () =>
{
var payoutC =
@ -3494,8 +3616,8 @@ namespace BTCPayServer.Tests
Assert.Empty(await adminClient.GetPayoutProcessors(admin.StoreId));
await adminClient.UpdateStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC",
new OnChainAutomatedPayoutSettings() { IntervalSeconds = TimeSpan.FromSeconds(100000) });
Assert.Equal(100000, Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC")).IntervalSeconds.TotalSeconds);
new OnChainAutomatedPayoutSettings() { IntervalSeconds = TimeSpan.FromSeconds(3600) });
Assert.Equal(3600, Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC")).IntervalSeconds.TotalSeconds);
var tpGen = Assert.Single(await adminClient.GetPayoutProcessors(admin.StoreId));
Assert.Equal("BTC", Assert.Single(tpGen.PaymentMethods));
@ -3508,8 +3630,10 @@ namespace BTCPayServer.Tests
Assert.Empty(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC"));
Assert.Empty(await adminClient.GetPayoutProcessors(admin.StoreId));
// Send just enough money to cover the smallest of the payouts.
var fee = (await tester.PayTester.GetService<IFeeProviderFactory>().CreateFeeProvider(tester.DefaultNetwork).GetFeeRateAsync(100)).GetFee(150);
await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(0.000012m));
tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(0.00001m) + fee);
await tester.ExplorerNode.GenerateAsync(1);
await TestUtils.EventuallyAsync(async () =>
{
@ -3519,8 +3643,9 @@ namespace BTCPayServer.Tests
Assert.Equal(3, payouts.Length);
});
await adminClient.UpdateStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC",
new OnChainAutomatedPayoutSettings() { IntervalSeconds = TimeSpan.FromSeconds(5) });
Assert.Equal(5, Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC")).IntervalSeconds.TotalSeconds);
new OnChainAutomatedPayoutSettings() { IntervalSeconds = TimeSpan.FromSeconds(600), FeeBlockTarget = 1000 });
Assert.Equal(600, Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC")).IntervalSeconds.TotalSeconds);
await TestUtils.EventuallyAsync(async () =>
{
Assert.Equal(2, (await adminClient.ShowOnChainWalletTransactions(admin.StoreId, "BTC")).Count());
@ -3528,8 +3653,10 @@ namespace BTCPayServer.Tests
Assert.Single(payouts.Where(data => data.State == PayoutState.InProgress));
});
await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(0.01m));
var txid = await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(0.01m) + fee);
await tester.WaitForEvent<NewOnChainTransactionEvent>(null, correctEvent: ev => ev.NewTransactionEvent.TransactionData.TransactionHash == txid);
await tester.PayTester.GetService<PayoutProcessorService>().Restart(new PayoutProcessorService.PayoutProcessorQuery(admin.StoreId, "BTC"));
await TestUtils.EventuallyAsync(async () =>
{
Assert.Equal(4, (await adminClient.ShowOnChainWalletTransactions(admin.StoreId, "BTC")).Count());

@ -1,10 +1,12 @@
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Hosting;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Plugins.PointOfSale;
using BTCPayServer.Plugins.PointOfSale.Controllers;
using BTCPayServer.Plugins.PointOfSale.Models;
using BTCPayServer.Services.Apps;
using Microsoft.AspNetCore.Mvc;
using Xunit;
using Xunit.Abstractions;
@ -19,6 +21,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)]
[Trait("Integration", "Integration")]
public async Task CanUsePoSApp1()
@ -53,6 +123,7 @@ donation:
price: 1.02
custom: true
";
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
var publicApps = user.GetController<UIPointOfSaleController>();

@ -1,4 +1,5 @@
using System;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Linq;
@ -64,7 +65,6 @@ namespace BTCPayServer.Tests
var containerIp = File.ReadAllText("/etc/hosts").Split('\n', StringSplitOptions.RemoveEmptyEntries).Last()
.Split('\t', StringSplitOptions.RemoveEmptyEntries)[0].Trim();
TestLogs.LogInformation($"Selenium: Container's IP {containerIp}");
ServerUri = new Uri(Server.PayTester.ServerUri.AbsoluteUri.Replace($"http://{Server.PayTester.HostName}", $"http://{containerIp}", StringComparison.OrdinalIgnoreCase), UriKind.Absolute);
}
else
{
@ -76,8 +76,8 @@ namespace BTCPayServer.Tests
Driver = new ChromeDriver(cds, options,
// A bit less than test timeout
TimeSpan.FromSeconds(50));
ServerUri = Server.PayTester.ServerUri;
}
ServerUri = Server.PayTester.ServerUri;
Driver.Manage().Window.Maximize();
TestLogs.LogInformation($"Selenium: Using {Driver.GetType()}");
@ -180,7 +180,7 @@ namespace BTCPayServer.Tests
{
Driver.FindElement(By.Id("StoreSelectorToggle")).Click();
}
Driver.WaitForElement(By.Id("StoreSelectorCreate")).Click();
GoToUrl("/stores/create");
var name = "Store" + RandomUtils.GetUInt64();
TestLogs.LogInformation($"Created store {name}");
Driver.WaitForElement(By.Id("Name")).SendKeys(name);
@ -313,8 +313,6 @@ namespace BTCPayServer.Tests
var connectionString = connectionType switch
{
LightningConnectionType.Charge =>
$"type=charge;server={Server.MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true",
LightningConnectionType.CLightning =>
$"type=clightning;server={((CLightningClient)Server.MerchantLightningD).Address.AbsoluteUri}",
LightningConnectionType.LndREST =>

@ -1,4 +1,5 @@
using System;
using System.Buffers;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
@ -11,8 +12,10 @@ using System.Threading.Tasks;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Lightning;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Payments;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
@ -116,24 +119,31 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Name("buyerEmail")).SendKeys("aa@aa.com");
s.Driver.FindElement(By.CssSelector("input[type='submit']")).Click();
invoiceId = s.Driver.Url.Split('/').Last();
s.Driver.Navigate().GoToUrl(editUrl);
Assert.Contains("aa@aa.com", s.Driver.PageSource);
var invoice = await s.Server.PayTester.GetService<InvoiceRepository>().GetInvoice(invoiceId);
Assert.Equal("aa@aa.com", invoice.Metadata.BuyerEmail);
//Custom Forms
s.GoToStore(StoreNavPages.Forms);
Assert.Contains("There are no forms yet.", s.Driver.PageSource);
s.Driver.FindElement(By.Id("CreateForm")).Click();
s.Driver.FindElement(By.Name("Name")).SendKeys("Custom Form 1");
s.Driver.FindElement((By.CssSelector("[data-form-template='email']"))).Click();
var emailtemplate = s.Driver.FindElement(By.Name("FormConfig")).GetAttribute("value");
Assert.Contains("buyerEmail", emailtemplate);
s.Driver.FindElement(By.Id("ApplyEmailTemplate")).Click();
s.Driver.FindElement(By.Id("CodeTabButton")).Click();
s.Driver.WaitForElement(By.Id("CodeTabPane"));
var config = s.Driver.FindElement(By.Name("FormConfig")).GetAttribute("value");
Assert.Contains("buyerEmail", config);
s.Driver.FindElement(By.Name("FormConfig")).Clear();
s.Driver.FindElement(By.Name("FormConfig"))
.SendKeys(emailtemplate.Replace("Enter your email", "CustomFormInputTest"));
.SendKeys(config.Replace("Enter your email", "CustomFormInputTest"));
s.Driver.FindElement(By.Id("SaveButton")).Click();
s.Driver.FindElement(By.Id("ViewForm")).Click();
var formurl = s.Driver.Url;
Assert.Contains("CustomFormInputTest", s.Driver.PageSource);
s.Driver.FindElement(By.Name("buyerEmail")).SendKeys("aa@aa.com");
@ -152,12 +162,16 @@ namespace BTCPayServer.Tests
Assert.DoesNotContain("Custom Form 1", s.Driver.PageSource);
s.Driver.FindElement(By.Id("CreateForm")).Click();
s.Driver.FindElement(By.Name("Name")).SendKeys("Custom Form 2");
s.Driver.FindElement((By.CssSelector("[data-form-template='email']"))).Click();
s.Driver.FindElement(By.Id("ApplyEmailTemplate")).Click();
s.Driver.FindElement(By.Id("CodeTabButton")).Click();
s.Driver.WaitForElement(By.Id("CodeTabPane"));
s.Driver.SetCheckbox(By.Name("Public"), true);
s.Driver.FindElement(By.Name("FormConfig")).Clear();
s.Driver.FindElement(By.Name("FormConfig"))
.SendKeys(emailtemplate.Replace("Enter your email", "CustomFormInputTest2"));
.SendKeys(config.Replace("Enter your email", "CustomFormInputTest2"));
s.Driver.FindElement(By.Id("SaveButton")).Click();
s.Driver.FindElement(By.Id("ViewForm")).Click();
formurl = s.Driver.Url;
@ -595,7 +609,7 @@ namespace BTCPayServer.Tests
TestUtils.Eventually(() =>
{
s.Driver.Navigate().Refresh();
s.Driver.FindElement(By.Id("receipt-btn")).Click();
s.Driver.FindElement(By.Id("ReceiptLink")).Click();
});
TestUtils.Eventually(() =>
{
@ -607,14 +621,13 @@ namespace BTCPayServer.Tests
await s.Server.PayTester.InvoiceRepository.MarkInvoiceStatus(i, InvoiceStatus.Settled);
TestUtils.Eventually(() => s.Driver.FindElement(By.Id("receipt-btn")).Click());
TestUtils.Eventually(() => s.Driver.FindElement(By.Id("ReceiptLink")).Click());
TestUtils.Eventually(() =>
{
s.Driver.Navigate().Refresh();
Assert.DoesNotContain("invoice-unsettled", s.Driver.PageSource);
Assert.DoesNotContain("invoice-processing", s.Driver.PageSource);
});
}
[Fact(Timeout = TestTimeout)]
@ -625,21 +638,24 @@ namespace BTCPayServer.Tests
s.RegisterNewUser();
s.GoToUrl("/");
// verify redirected to create store page
Assert.EndsWith("/stores/create", s.Driver.Url);
Assert.Contains("Create your first store", s.Driver.PageSource);
Assert.Contains("To start accepting payments, set up a store.", s.Driver.PageSource);
Assert.False(s.Driver.PageSource.Contains("id=\"StoreSelectorDropdown\""), "Store selector dropdown should not be present");
Assert.True(s.Driver.PageSource.Contains("id=\"StoreSelectorCreate\""), "Store selector create button should be present");
// verify steps for store creation are displayed correctly
s.Driver.FindElement(By.Id("SetupGuide-Store")).Click();
Assert.Contains("/stores/create", s.Driver.Url);
(_, string storeId) = s.CreateNewStore();
// should redirect to store
// should redirect to first store
s.GoToUrl("/");
Assert.Contains($"/stores/{storeId}", s.Driver.Url);
Assert.True(s.Driver.PageSource.Contains("id=\"StoreSelectorDropdown\""), "Store selector dropdown should be present");
Assert.True(s.Driver.PageSource.Contains("id=\"SetupGuide\""), "Store setup guide should be present");
s.GoToUrl("/stores/create");
Assert.Contains("Create a new store", s.Driver.PageSource);
Assert.DoesNotContain("Create your first store", s.Driver.PageSource);
Assert.DoesNotContain("To start accepting payments, set up a store.", s.Driver.PageSource);
}
[Fact(Timeout = TestTimeout)]
@ -713,8 +729,8 @@ namespace BTCPayServer.Tests
Assert.DoesNotContain(invoiceId, s.Driver.PageSource);
// unarchive via list
s.Driver.FindElement(By.Id("SearchOptionsToggle")).Click();
s.Driver.FindElement(By.Id("SearchOptionsIncludeArchived")).Click();
s.Driver.FindElement(By.Id("StatusOptionsToggle")).Click();
s.Driver.FindElement(By.Id("StatusOptionsIncludeArchived")).Click();
Assert.Contains(invoiceId, s.Driver.PageSource);
s.Driver.FindElement(By.CssSelector($".selector[value=\"{invoiceId}\"]")).Click();
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
@ -949,7 +965,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("ToggleRawEditor")).Click();
var template = s.Driver.FindElement(By.Id("Template")).GetAttribute("value");
Assert.Contains("buyButtonText: Take my money", template);
Assert.Contains("\"buyButtonText\":\"Take my money\"", template);
s.Driver.FindElement(By.Id("SaveSettings")).Click();
Assert.Contains("App updated", s.FindAlertMessage().Text);
@ -1726,11 +1742,12 @@ namespace BTCPayServer.Tests
{
s.Driver.Navigate().Refresh();
Assert.Contains("transaction-label", s.Driver.PageSource);
var labels = s.Driver.FindElements(By.CssSelector("#WalletTransactionsList tr:first-child div.transaction-label"));
Assert.Equal(2, labels.Count);
Assert.Contains(labels, element => element.Text == "payout");
Assert.Contains(labels, element => element.Text == "pull-payment");
});
var labels = s.Driver.FindElements(By.CssSelector("#WalletTransactionsList tr:first-child div.transaction-label"));
Assert.Equal(2, labels.Count);
Assert.Contains(labels, element => element.Text == "payout");
Assert.Contains(labels, element => element.Text == "pull-payment");
s.GoToStore(s.StoreId, StoreNavPages.Payouts);
s.Driver.FindElement(By.Id($"{PayoutState.InProgress}-view")).Click();
@ -2085,22 +2102,23 @@ namespace BTCPayServer.Tests
});
// Standard invoice test
s.GoToStore(storeId);
s.GoToLightningSettings();
s.Driver.SetCheckbox(By.Id("LNURLStandardInvoiceEnabled"), true);
SudoForceSaveLightningSettingsRightNowAndFast(s, cryptoCode);
i = s.CreateInvoice(storeId, 0.0000001m, cryptoCode);
s.GoToInvoiceCheckout(i);
s.Driver.FindElement(By.ClassName("payment__currencies")).Click();
// BOLT11 is also available for standard invoices
Assert.Equal(2, s.Driver.FindElements(By.CssSelector(".vex.vex-theme-btcpay .vex-content .vexmenu li.vexmenuitem")).Count);
s.Driver.FindElement(By.CssSelector(".vex.vex-theme-btcpay .vex-content .vexmenu li.vexmenuitem")).Click();
s.Driver.FindElement(By.ClassName("payment__currencies_noborder")).Click();
// BOLT11 is also displayed for standard invoice (not LNURL, even if it is available)
s.Driver.FindElement(By.Id("copy-tab")).Click();
lnurl = s.Driver.FindElement(By.CssSelector("input.checkoutTextbox")).GetAttribute("value");
parsed = LNURL.LNURL.Parse(lnurl, out tag);
fetchedReuqest = Assert.IsType<LNURLPayRequest>(await LNURL.LNURL.FetchInformation(parsed, new HttpClient()));
var bolt11 = s.Driver.FindElement(By.CssSelector("input.checkoutTextbox")).GetAttribute("value");
var bolt11Parsed = Lightning.BOLT11PaymentRequest.Parse(bolt11, s.Server.ExplorerNode.Network);
var invoiceId = s.Driver.Url.Split('/').Last();
using (var resp = await s.Server.PayTester.HttpClient.GetAsync("BTC/lnurl/pay/i/" + invoiceId))
{
resp.EnsureSuccessStatusCode();
fetchedReuqest = JsonConvert.DeserializeObject<LNURLPayRequest>(await resp.Content.ReadAsStringAsync());
}
Assert.Equal(0.0000001m, fetchedReuqest.MaxSendable.ToDecimal(LightMoneyUnit.BTC));
Assert.Equal(0.0000001m, fetchedReuqest.MinSendable.ToDecimal(LightMoneyUnit.BTC));
await Assert.ThrowsAsync<LNUrlException>(async () =>
{
await fetchedReuqest.SendRequest(new LightMoney(0.0000002m, LightMoneyUnit.BTC),
@ -2122,13 +2140,6 @@ namespace BTCPayServer.Tests
Assert.Equal(new LightMoney(0.0000001m, LightMoneyUnit.BTC),
lnurlResponse2.GetPaymentRequest(network).MinimumAmount);
s.GoToHome();
s.GoToLightningSettings();
// LNURL is enabled and settings are expanded
Assert.True(s.Driver.FindElement(By.Id("LNURLEnabled")).Selected);
Assert.Contains("show", s.Driver.FindElement(By.Id("LNURLSettings")).GetAttribute("class"));
s.Driver.SetCheckbox(By.Id("LNURLStandardInvoiceEnabled"), false);
s.Driver.FindElement(By.Id("save")).Click();
Assert.Contains($"{cryptoCode} Lightning settings successfully updated", s.FindAlertMessage().Text);
i = s.CreateInvoice(storeId, 0.000001m, cryptoCode);
s.GoToInvoiceCheckout(i);
@ -2142,23 +2153,12 @@ namespace BTCPayServer.Tests
s.GoToHome();
s.GoToLightningSettings();
s.Driver.SetCheckbox(By.Id("LNURLBech32Mode"), false);
s.Driver.SetCheckbox(By.Id("LNURLStandardInvoiceEnabled"), false);
s.Driver.SetCheckbox(By.Id("DisableBolt11PaymentMethod"), true);
s.Driver.FindElement(By.Id("save")).Click();
Assert.Contains($"{cryptoCode} Lightning settings successfully updated", s.FindAlertMessage().Text);
// Ensure the toggles are set correctly
s.GoToLightningSettings();
//TODO: DisableBolt11PaymentMethod is actually disabled because LNURLStandardInvoiceEnabled is disabled
// checkboxes is not good choice here, in next release we should have multi choice instead
Assert.False(s.Driver.FindElement(By.Id("LNURLBech32Mode")).Selected);
Assert.False(s.Driver.FindElement(By.Id("LNURLStandardInvoiceEnabled")).Selected);
//even though we set DisableBolt11PaymentMethod to true, logic when saving it turns it back off as otherwise no lightning option is available at all!
Assert.False(s.Driver.FindElement(By.Id("DisableBolt11PaymentMethod")).Selected);
// Invoice creation should fail, because it is a standard invoice with amount, but DisableBolt11PaymentMethod = true and LNURLStandardInvoiceEnabled = false
s.CreateInvoice(storeId, 0.0000001m, cryptoCode, "", null, expectedSeverity: StatusMessageModel.StatusSeverity.Success);
i = s.CreateInvoice(storeId, null, cryptoCode);
s.GoToInvoiceCheckout(i);
@ -2174,15 +2174,13 @@ namespace BTCPayServer.Tests
s.AddLightningNode(LightningConnectionType.LndREST, false);
s.GoToLightningSettings();
s.Driver.SetCheckbox(By.Id("LNURLEnabled"), true);
s.Driver.SetCheckbox(By.Id("DisableBolt11PaymentMethod"), true);
s.Driver.SetCheckbox(By.Id("LNURLStandardInvoiceEnabled"), true);
s.Driver.FindElement(By.Id("save")).Click();
Assert.Contains($"{cryptoCode} Lightning settings successfully updated", s.FindAlertMessage().Text);
var invForPP = s.CreateInvoice(0.0000001m, cryptoCode);
var invForPP = s.CreateInvoice(null, cryptoCode);
s.GoToInvoiceCheckout(invForPP);
s.Driver.FindElement(By.Id("copy-tab")).Click();
lnurl = s.Driver.FindElement(By.CssSelector("input.checkoutTextbox")).GetAttribute("value");
parsed = LNURL.LNURL.Parse(lnurl, out tag);
LNURL.LNURL.Parse(lnurl, out tag);
// Check that pull payment has lightning option
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
@ -2306,7 +2304,6 @@ namespace BTCPayServer.Tests
var invoices = await repo.GetInvoices(new InvoiceQuery() { StoreId = new[] { s.StoreId } });
Assert.Equal(2, invoices.Length);
var emailSuffix = $"@{s.Server.PayTester.HostName}:{s.Server.PayTester.Port}";
foreach (var i in invoices)
{
var lightningPaymentMethod = i.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.LNURLPay));
@ -2323,6 +2320,8 @@ namespace BTCPayServer.Tests
}
var lnUsername = lnaddress1.Split('@')[0];
LNURLPayRequest req;
using (var resp = await s.Server.PayTester.HttpClient.GetAsync($"/.well-known/lnurlp/{lnUsername}"))
{
@ -2450,10 +2449,11 @@ retry:
_ = await request.SendChallenge(linkingKey, new HttpClient());
TestUtils.Eventually(() => s.FindAlertMessage());
s.CreateNewStore(); // create a store to prevent redirect after login
s.Logout();
s.LogIn(user, "123456");
var section = s.Driver.FindElement(By.Id("lnurlauth-section"));
links = section.FindElements(By.CssSelector(".tab-content a")).Select(element => element.GetAttribute("href"));
links = section.FindElements(By.CssSelector(".tab-content a")).Select(element => element.GetAttribute("href")).ToList();
Assert.Equal(2, links.Count());
prevEndpoint = null;
foreach (string link in links)
@ -2467,9 +2467,148 @@ retry:
_ = await request.SendChallenge(linkingKey, new HttpClient());
TestUtils.Eventually(() =>
{
Assert.Equal(s.Driver.Url, s.ServerUri.ToString());
Assert.StartsWith(s.ServerUri.ToString(), s.Driver.Url);
});
}
[Fact]
[Trait("Selenium", "Selenium")]
public async Task CanUseRoleManager()
{
using var s = CreateSeleniumTester(newDb: true);
await s.StartAsync();
var user = s.RegisterNewUser(true);
s.GoToServer(ServerNavPages.Roles);
var existingServerRoles = s.Driver.FindElement(By.CssSelector("table")).FindElements(By.CssSelector("tr"));
Assert.Equal(3, existingServerRoles.Count);
IWebElement ownerRow = null;
IWebElement guestRow = null;
foreach (var roleItem in existingServerRoles)
{
if (roleItem.Text.Contains("owner", StringComparison.InvariantCultureIgnoreCase))
{
ownerRow = roleItem;
}
else if (roleItem.Text.Contains("guest", StringComparison.InvariantCultureIgnoreCase))
{
guestRow = roleItem;
}
}
Assert.NotNull(ownerRow);
Assert.NotNull(guestRow);
var ownerBadges = ownerRow.FindElements(By.CssSelector(".badge"));
Assert.Contains(ownerBadges, element => element.Text.Equals("Default", StringComparison.InvariantCultureIgnoreCase));
Assert.Contains(ownerBadges, element => element.Text.Equals("Server-wide", StringComparison.InvariantCultureIgnoreCase));
var guestBadges = guestRow.FindElements(By.CssSelector(".badge"));
Assert.DoesNotContain(guestBadges, element => element.Text.Equals("Default", StringComparison.InvariantCultureIgnoreCase));
Assert.Contains(guestBadges, element => element.Text.Equals("Server-wide", StringComparison.InvariantCultureIgnoreCase));
guestRow.FindElement(By.Id("SetDefault")).Click();
s.FindAlertMessage();
existingServerRoles = s.Driver.FindElement(By.CssSelector("table")).FindElements(By.CssSelector("tr"));
foreach (var roleItem in existingServerRoles)
{
if (roleItem.Text.Contains("owner", StringComparison.InvariantCultureIgnoreCase))
{
ownerRow = roleItem;
}
else if (roleItem.Text.Contains("guest", StringComparison.InvariantCultureIgnoreCase))
{
guestRow = roleItem;
}
}
guestBadges = guestRow.FindElements(By.CssSelector(".badge"));
Assert.Contains(guestBadges, element => element.Text.Equals("Default", StringComparison.InvariantCultureIgnoreCase));
ownerBadges = ownerRow.FindElements(By.CssSelector(".badge"));
Assert.DoesNotContain(ownerBadges, element => element.Text.Equals("Default", StringComparison.InvariantCultureIgnoreCase));
ownerRow.FindElement(By.Id("SetDefault")).Click();
s.FindAlertMessage();
s.CreateNewStore();
s.GoToStore(StoreNavPages.Roles);
var existingStoreRoles = s.Driver.FindElement(By.CssSelector("table")).FindElements(By.CssSelector("tr"));
Assert.Equal(3, existingStoreRoles.Count);
Assert.Equal(2, existingStoreRoles.Count(element => element.Text.Contains("Server-wide", StringComparison.InvariantCultureIgnoreCase)));
foreach (var roleItem in existingStoreRoles)
{
if (roleItem.Text.Contains("owner", StringComparison.InvariantCultureIgnoreCase))
{
ownerRow = roleItem;
break;
}
}
ownerRow.FindElement(By.LinkText("Remove")).Click();
Assert.DoesNotContain("ConfirmContinue", s.Driver.PageSource);
s.Driver.Navigate().Back();
existingStoreRoles = s.Driver.FindElement(By.CssSelector("table")).FindElements(By.CssSelector("tr"));
foreach (var roleItem in existingStoreRoles)
{
if (roleItem.Text.Contains("guest", StringComparison.InvariantCultureIgnoreCase))
{
guestRow = roleItem;
break;
}
}
guestRow.FindElement(By.LinkText("Remove")).Click();
s.Driver.FindElement(By.Id("ConfirmContinue")).Click();
s.FindAlertMessage();
s.GoToStore(StoreNavPages.Roles);
s.Driver.FindElement(By.Id("CreateRole")).Click();
Assert.Contains("Create role", s.Driver.PageSource);
s.Driver.FindElement(By.Id("Save")).Click();
s.Driver.FindElement(By.Id("Role")).SendKeys("store role");
s.Driver.FindElement(By.Id("Save")).Click();
s.FindAlertMessage();
existingStoreRoles = s.Driver.FindElement(By.CssSelector("table")).FindElements(By.CssSelector("tr"));
foreach (var roleItem in existingStoreRoles)
{
if (roleItem.Text.Contains("store role", StringComparison.InvariantCultureIgnoreCase))
{
guestRow = roleItem;
break;
}
}
guestBadges = guestRow.FindElements(By.CssSelector(".badge"));
Assert.DoesNotContain(guestBadges, element => element.Text.Equals("server-wide", StringComparison.InvariantCultureIgnoreCase));
s.GoToStore(StoreNavPages.Users);
var options = s.Driver.FindElements(By.CssSelector("#Role option"));
Assert.Equal(2, options.Count);
Assert.Contains(options, element => element.Text.Equals("store role", StringComparison.InvariantCultureIgnoreCase));
s.CreateNewStore();
s.GoToStore(StoreNavPages.Roles);
existingStoreRoles = s.Driver.FindElement(By.CssSelector("table")).FindElements(By.CssSelector("tr"));
Assert.Equal(2, existingStoreRoles.Count);
Assert.Equal(1, existingStoreRoles.Count(element => element.Text.Contains("Server-wide", StringComparison.InvariantCultureIgnoreCase)));
Assert.Equal(0, existingStoreRoles.Count(element => element.Text.Contains("store role", StringComparison.InvariantCultureIgnoreCase)));
s.GoToStore(StoreNavPages.Users);
options = s.Driver.FindElements(By.CssSelector("#Role option"));
Assert.Single(options);
Assert.DoesNotContain(options, element => element.Text.Equals("store role", StringComparison.InvariantCultureIgnoreCase));
s.GoToStore(StoreNavPages.Roles);
s.Driver.FindElement(By.Id("CreateRole")).Click();
s.Driver.FindElement(By.Id("Role")).SendKeys("Malice");
s.Driver.ExecuteJavaScript($"document.getElementById('Policies')['{Policies.CanModifyServerSettings}']=new Option('{Policies.CanModifyServerSettings}', '{Policies.CanModifyServerSettings}', true,true);");
s.Driver.FindElement(By.Id("Save")).Click();
s.FindAlertMessage();
Assert.Contains("Malice",s.Driver.PageSource);
Assert.DoesNotContain(Policies.CanModifyServerSettings,s.Driver.PageSource);
}
private static void CanBrowseContent(SeleniumTester s)
{

@ -92,7 +92,7 @@ namespace BTCPayServer.Tests
#endif
public void ActivateLightning()
{
ActivateLightning(LightningConnectionType.Charge);
ActivateLightning(LightningConnectionType.CLightning);
}
public void ActivateLightning(LightningConnectionType internalNode)
{
@ -109,14 +109,7 @@ namespace BTCPayServer.Tests
string connectionString = null;
if (connectionType is null)
return LightningSupportedPaymentMethod.InternalNode;
if (connectionType == LightningConnectionType.Charge)
{
if (isMerchant)
connectionString = $"type=charge;server={MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true";
else
throw new NotSupportedException();
}
else if (connectionType == LightningConnectionType.CLightning)
if (connectionType == LightningConnectionType.CLightning)
{
if (isMerchant)
connectionString = "type=clightning;server=" +
@ -177,7 +170,7 @@ namespace BTCPayServer.Tests
public async Task<PayResponse> SendLightningPaymentAsync(Invoice invoice)
{
var bolt11 = invoice.CryptoInfo.Where(o => o.PaymentUrls.BOLT11 != null).First().PaymentUrls.BOLT11;
var bolt11 = invoice.CryptoInfo.Where(o => o.PaymentUrls?.BOLT11 != null).First().PaymentUrls.BOLT11;
bolt11 = bolt11.Replace("lightning:", "", StringComparison.OrdinalIgnoreCase);
return await CustomerLightningD.Pay(bolt11);
}
@ -194,7 +187,8 @@ namespace BTCPayServer.Tests
tcs.TrySetResult(evt);
}
});
await action.Invoke();
if (action != null)
await action.Invoke();
var result = await tcs.Task;
sub.Dispose();
return result;
@ -247,6 +241,8 @@ namespace BTCPayServer.Tests
public List<string> Stores { get; internal set; } = new List<string>();
public bool DeleteStore { get; set; } = true;
public BTCPayNetworkBase DefaultNetwork => NetworkProvider.DefaultNetwork;
public void Dispose()
{
foreach (var r in this.Resources)

@ -214,6 +214,13 @@ namespace BTCPayServer.Tests
get => GenerateWalletResponseV.DerivationScheme;
}
public void SetLNUrl(string cryptoCode, bool activated)
{
var lnSettingsVm = GetController<UIStoresController>().LightningSettings(StoreId, cryptoCode).AssertViewModel<LightningSettingsViewModel>();
lnSettingsVm.LNURLEnabled = activated;
Assert.IsType<RedirectToActionResult>(GetController<UIStoresController>().LightningSettings(lnSettingsVm).Result);
}
private async Task RegisterAsync(bool isAdmin = false)
{
var account = parent.PayTester.GetController<UIAccountController>();
@ -270,7 +277,7 @@ namespace BTCPayServer.Tests
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();
}
@ -463,7 +470,10 @@ namespace BTCPayServer.Tests
var req = await _server.GetNextRequest(cancellation);
var bytes = await req.Request.Body.ReadBytesAsync((int)req.Request.Headers.ContentLength);
var callback = Encoding.UTF8.GetString(bytes);
_webhookEvents.Add(JsonConvert.DeserializeObject<WebhookInvoiceEvent>(callback));
lock (_webhookEvents)
{
_webhookEvents.Add(JsonConvert.DeserializeObject<WebhookInvoiceEvent>(callback));
}
req.Response.StatusCode = 200;
_server.Done();
}
@ -480,18 +490,21 @@ namespace BTCPayServer.Tests
{
int retry = 0;
retry:
foreach (var evt in WebhookEvents)
lock (WebhookEvents)
{
if (evt.Type == eventType)
foreach (var evt in WebhookEvents)
{
var typedEvt = evt.ReadAs<TEvent>();
try
{
assert(typedEvt);
return typedEvt;
}
catch (XunitException)
if (evt.Type == eventType)
{
var typedEvt = evt.ReadAs<TEvent>();
try
{
assert(typedEvt);
return typedEvt;
}
catch (XunitException)
{
}
}
}
}
@ -533,12 +546,12 @@ retry:
public async Task AddGuest(string userId)
{
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)
{
var repo = this.parent.PayTester.GetService<StoreRepository>();
await repo.AddStoreUser(StoreId, userId, "Owner");
await repo.AddStoreUser(StoreId, userId, StoreRoleId.Owner);
}
}
}

@ -112,7 +112,14 @@ namespace BTCPayServer.Tests
}
catch (XunitException) when (!cts.Token.IsCancellationRequested)
{
await Task.Delay(500, cts.Token);
bool timeout =false;
try
{
await Task.Delay(500, cts.Token);
}
catch { timeout = true; }
if (timeout)
throw;
}
}
}

@ -358,6 +358,11 @@ retry:
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();
Assert.Equal(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();
Assert.Equal(expected, actual);
}
string GetFileContent(params string[] path)

@ -466,14 +466,6 @@ namespace BTCPayServer.Tests
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)]
[Trait("Integration", "Integration")]
[Trait("Lightning", "Lightning")]
@ -1634,7 +1626,8 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount();
var cryptoCode = "BTC";
user.GrantAccess(true);
user.RegisterLightningNode(cryptoCode, LightningConnectionType.Charge);
user.RegisterLightningNode(cryptoCode);
user.SetLNUrl(cryptoCode, false);
var vm = user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModel<CheckoutAppearanceViewModel>();
var criteria = Assert.Single(vm.PaymentMethodCriteria);
Assert.Equal(new PaymentMethodId(cryptoCode, LightningPaymentType.Instance).ToString(), criteria.PaymentMethod);
@ -1649,16 +1642,12 @@ namespace BTCPayServer.Tests
Price = 1.5m,
Currency = "USD"
}, Facade.Merchant);
Assert.Single(invoice.CryptoInfo);
Assert.Equal(PaymentTypes.LightningLike.ToString(), invoice.CryptoInfo[0].PaymentType);
// Activating LNUrl, we should still have only 1 payment criteria that can be set.
user.RegisterLightningNode(cryptoCode, LightningConnectionType.Charge);
var lnSettingsVm = user.GetController<UIStoresController>().LightningSettings(user.StoreId, cryptoCode).AssertViewModel<LightningSettingsViewModel>();
lnSettingsVm.LNURLEnabled = true;
lnSettingsVm.LNURLStandardInvoiceEnabled = true;
Assert.IsType<RedirectToActionResult>(user.GetController<UIStoresController>().LightningSettings(lnSettingsVm).Result);
user.RegisterLightningNode(cryptoCode);
user.SetLNUrl(cryptoCode, true);
vm = user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModel<CheckoutAppearanceViewModel>();
criteria = Assert.Single(vm.PaymentMethodCriteria);
Assert.Equal(new PaymentMethodId(cryptoCode, LightningPaymentType.Instance).ToString(), criteria.PaymentMethod);
@ -1973,7 +1962,8 @@ namespace BTCPayServer.Tests
Assert.Empty(appList2.Apps);
Assert.Equal("test", appList.Apps[0].AppName);
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.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id));
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id));
@ -2157,7 +2147,7 @@ namespace BTCPayServer.Tests
txFee = localInvoice.BtcDue - invoice.BtcDue;
Assert.Equal("paidPartial", localInvoice.ExceptionStatus.ToString());
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(localInvoice, ctx));
@ -2445,6 +2435,31 @@ namespace BTCPayServer.Tests
Assert.False(fn.Seen);
}
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanFixMappedDomainAppType()
{
using var tester = CreateServerTester(newDb: true);
await tester.StartAsync();
var f = tester.PayTester.GetService<ApplicationDbContextFactory>();
using (var ctx = f.CreateContext())
{
var setting = new SettingData() { Id = "BTCPayServer.Services.PoliciesSettings" };
setting.Value = JObject.Parse("{\"RootAppId\": null, \"RootAppType\": 1, \"Experimental\": false, \"PluginSource\": null, \"LockSubscription\": false, \"DisableSSHService\": false, \"PluginPreReleases\": false, \"BlockExplorerLinks\": [],\"DomainToAppMapping\": [{\"AppId\": \"87kj5yKay8mB4UUZcJhZH5TqDKMD3CznjwLjiu1oYZXe\", \"Domain\": \"donate.nicolas-dorier.com\", \"AppType\": 0}], \"CheckForNewVersions\": false, \"AllowHotWalletForAll\": false, \"RequiresConfirmedEmail\": false, \"DiscourageSearchEngines\": false, \"DisableInstantNotifications\": false, \"DisableNonAdminCreateUserApi\": false, \"AllowHotWalletRPCImportForAll\": false, \"AllowLightningInternalNodeForAll\": false, \"DisableStoresToUseServerEmailSettings\": false}").ToString();
ctx.Settings.Add(setting);
await ctx.SaveChangesAsync();
}
await RestartMigration(tester);
using (var ctx = f.CreateContext())
{
var setting = await ctx.Settings.FirstOrDefaultAsync(c => c.Id == "BTCPayServer.Services.PoliciesSettings");
var o = JObject.Parse(setting.Value);
Assert.Equal("Crowdfund", o["RootAppType"].Value<string>());
o = (JObject)((JArray)o["DomainToAppMapping"])[0];
Assert.Equal("PointOfSale", o["AppType"].Value<string>());
}
}
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanDoLightningInternalNodeMigration()

@ -24,7 +24,6 @@ services:
TESTS_AzureBlobStorageConnectionString: ${TESTS_AzureBlobStorageConnectionString:-none}
TEST_MERCHANTLIGHTNINGD: "type=clightning;server=unix://etc/merchant_lightningd_datadir/lightning-rpc"
TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=unix://etc/customer_lightningd_datadir/lightning-rpc"
TEST_MERCHANTCHARGE: "type=charge;server=http://lightning-charged:9112/;api-token=foiewnccewuify;allowinsecure=true"
TEST_MERCHANTLND: "http://lnd:lnd@merchant_lnd:8080/"
TESTS_INCONTAINER: "true"
TESTS_SSHCONNECTION: "root@sshd:22"
@ -38,6 +37,10 @@ services:
- selenium
extra_hosts:
- "tests:127.0.0.1"
networks:
default:
custom:
ipv4_address: 172.23.0.18
volumes:
- "sshd_datadir:/root/.ssh"
- "customer_lightningd_datadir:/etc/customer_lightningd_datadir"
@ -52,7 +55,6 @@ services:
- postgres
- customer_lightningd
- merchant_lightningd
- lightning-charged
- customer_lnd
- merchant_lnd
- sshd
@ -71,7 +73,7 @@ services:
- "sshd_datadir:/root/.ssh"
devlnd:
image: btcpayserver/bitcoin:24.0
image: btcpayserver/bitcoin:24.1-1
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"
@ -85,12 +87,19 @@ services:
- postgres
- customer_lnd
- merchant_lnd
selenium:
image: selenium/standalone-chrome:101.0
extra_hosts:
- "tests:172.23.0.18"
expose:
- "4444"
networks:
default:
custom:
nbxplorer:
image: nicolasdorier/nbxplorer:2.3.58
image: nicolasdorier/nbxplorer:2.3.63
restart: unless-stopped
ports:
- "32838:32838"
@ -126,7 +135,7 @@ services:
bitcoind:
restart: unless-stopped
image: btcpayserver/bitcoin:24.0
image: btcpayserver/bitcoin:24.1-1
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"
@ -154,7 +163,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: btcpayserver/lightning:v23.02-1-dev
image: btcpayserver/lightning:v23.05-dev
stop_signal: SIGKILL
restart: unless-stopped
environment:
@ -180,30 +189,8 @@ services:
depends_on:
- 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:
image: btcpayserver/lightning:v23.02-1-dev
image: btcpayserver/lightning:v23.05-dev
stop_signal: SIGKILL
environment:
EXPOSE_TCP: "true"
@ -237,7 +224,7 @@ services:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:v0.15.4-beta-1
image: btcpayserver/lnd:v0.16.2-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -272,7 +259,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.15.4-beta-1
image: btcpayserver/lnd:v0.16.2-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -404,3 +391,12 @@ volumes:
torrcdir:
tor_servicesdir:
monero_data:
networks:
default:
driver: bridge
custom:
driver: bridge
ipam:
config:
- subnet: 172.23.0.0/16

@ -22,7 +22,6 @@ services:
TESTS_AzureBlobStorageConnectionString: ${TESTS_AzureBlobStorageConnectionString:-none}
TEST_MERCHANTLIGHTNINGD: "type=clightning;server=unix://etc/merchant_lightningd_datadir/lightning-rpc"
TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=unix://etc/customer_lightningd_datadir/lightning-rpc"
TEST_MERCHANTCHARGE: "type=charge;server=http://lightning-charged:9112/;api-token=foiewnccewuify;allowinsecure=true"
TEST_MERCHANTLND: "http://lnd:lnd@merchant_lnd:8080/"
TESTS_INCONTAINER: "true"
TESTS_SSHCONNECTION: "root@sshd:22"
@ -36,6 +35,10 @@ services:
- selenium
extra_hosts:
- "tests:127.0.0.1"
networks:
default:
custom:
ipv4_address: 172.23.0.18
volumes:
- "sshd_datadir:/root/.ssh"
- "customer_lightningd_datadir:/etc/customer_lightningd_datadir"
@ -50,7 +53,6 @@ services:
- postgres
- customer_lightningd
- merchant_lightningd
- lightning-charged
- customer_lnd
- merchant_lnd
- sshd
@ -68,26 +70,33 @@ services:
- "sshd_datadir:/root/.ssh"
devlnd:
image: btcpayserver/bitcoin:24.0
image: btcpayserver/bitcoin:24.1-1
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"
BITCOIN_EXTRA_ARGS: |
deprecatedrpc=signrawtransaction
connect=bitcoind:39388
rpcallowip=0.0.0.0/0
fallbackfee=0.0002
rpcallowip=0.0.0.0/0
depends_on:
- nbxplorer
- postgres
- customer_lnd
- merchant_lnd
selenium:
image: selenium/standalone-chrome:101.0
extra_hosts:
- "tests:172.23.0.18"
expose:
- "4444"
networks:
default:
custom:
nbxplorer:
image: nicolasdorier/nbxplorer:2.3.58
image: nicolasdorier/nbxplorer:2.3.63
restart: unless-stopped
ports:
- "32838:32838"
@ -110,10 +119,9 @@ services:
depends_on:
- bitcoind
bitcoind:
restart: unless-stopped
image: btcpayserver/bitcoin:24.0
image: btcpayserver/bitcoin:24.1-1
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"
@ -141,7 +149,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: btcpayserver/lightning:v23.02-1-dev
image: btcpayserver/lightning:v23.05-dev
stop_signal: SIGKILL
restart: unless-stopped
environment:
@ -167,30 +175,8 @@ services:
depends_on:
- 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:
image: btcpayserver/lightning:v23.02-1-dev
image: btcpayserver/lightning:v23.05-dev
stop_signal: SIGKILL
environment:
EXPOSE_TCP: "true"
@ -225,7 +211,7 @@ services:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:v0.15.4-beta-1
image: btcpayserver/lnd:v0.16.2-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -262,7 +248,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.15.4-beta-1
image: btcpayserver/lnd:v0.16.2-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -323,3 +309,12 @@ volumes:
tor_datadir:
torrcdir:
tor_servicesdir:
networks:
default:
driver: bridge
custom:
driver: bridge
ipam:
config:
- subnet: 172.23.0.0/16

@ -45,9 +45,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="YamlDotNet" Version="8.0.0" />
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.4.22" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.4.26" />
<PackageReference Include="CsvHelper" Version="15.0.5" />
<PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="Fido2" Version="2.0.2" />
@ -75,7 +76,6 @@
<PackageReference Include="TwentyTwenty.Storage.Azure" Version="2.12.1" />
<PackageReference Include="TwentyTwenty.Storage.Google" 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.NewtonsoftJson" Version="6.0.9" />
</ItemGroup>
@ -111,9 +111,6 @@
<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\_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.min.js" />
</ItemGroup>

@ -48,7 +48,7 @@
<span class="app-item-point ct-point"></span>
@entry.Title
</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>
@entry.TotalFormatted
</span>

@ -1,6 +1,5 @@
@using BTCPayServer.Views.Server
@using BTCPayServer.Views.Stores
@using BTCPayServer.Views.Apps
@using BTCPayServer.Views.Invoice
@using BTCPayServer.Views.Manage
@using BTCPayServer.Views.PaymentRequest
@ -145,7 +144,10 @@
</a>
</li>
<li class="nav-item" permission="@Policies.CanModifyStoreSettings">
<a asp-area="" asp-controller="UIStorePullPayments" asp-action="Payouts" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActivePage(StoreNavPages.Payouts)" id="StoreNav-Payouts">
<a asp-area=""
asp-controller="UIStorePullPayments" asp-action="Payouts"
asp-route-pullPaymentId=""
asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActivePage(StoreNavPages.Payouts)" id="StoreNav-Payouts">
<vc:icon symbol="payouts"/>
<span>Payouts</span>
</a>
@ -175,7 +177,7 @@
<ul class="navbar-nav">
<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">
<vc:icon symbol="plugin"/>
<vc:icon symbol="manage-plugins"/>
<span>Manage Plugins</span>
</a>
</li>
@ -236,7 +238,7 @@
<span>Account</span>
</a>
<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>
@if (User.IsInRole(Roles.ServerAdmin))
{
@ -245,10 +247,19 @@
</li>
@if (!Theme.CustomTheme)
{
<li class="border-top py-1 px-3">
<vc:theme-switch css-class="nav-link"/>
<li class="py-1 px-3">
<vc:theme-switch css-class="nav-link pb-0"/>
</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">
<a asp-area="" asp-controller="UIManage" asp-action="Index" class="nav-link @ViewData.IsActiveCategory(typeof(ManageNavPages))" id="Nav-ManageAccount">
<span>Manage Account</span>

@ -72,7 +72,6 @@ namespace BTCPayServer.Components.MainNav
vm.Apps = apps.Select(a => new StoreApp
{
Id = a.Id,
IsOwner = a.IsOwner,
AppName = a.AppName,
AppType = a.AppType
}).ToList();

@ -20,6 +20,5 @@ namespace BTCPayServer.Components.MainNav
public string Id { get; set; }
public string AppName { get; set; }
public string AppType { get; set; }
public bool IsOwner { get; set; }
}
}

@ -23,7 +23,7 @@
@if (Model.Balance.OffchainBalance != null)
{
<div class="balance">
<h3 class="d-inline-block me-1" data-balance="@Model.TotalOffchain">@Model.TotalOffchain</h3>
<h3 class="d-inline-block me-1" data-balance="@Model.TotalOffchain" data-sensitive>@Model.TotalOffchain</h3>
<span class="text-secondary fw-semibold text-nowrap">
<span class="currency">@Model.CryptoCode</span> in channels
</span>
@ -32,7 +32,7 @@
@if (Model.Balance.OffchainBalance.Opening != null)
{
<div class="mt-2">
<span class="fw-semibold" data-balance="@Model.Balance.OffchainBalance.Opening">
<span class="fw-semibold" data-balance="@Model.Balance.OffchainBalance.Opening" data-sensitive>
@Model.Balance.OffchainBalance.Opening
</span>
<span class="text-secondary text-nowrap">
@ -43,7 +43,7 @@
@if (Model.Balance.OffchainBalance.Local != null)
{
<div class="mt-2">
<span class="fw-semibold" data-balance="@Model.Balance.OffchainBalance.Local">
<span class="fw-semibold" data-balance="@Model.Balance.OffchainBalance.Local" data-sensitive>
@Model.Balance.OffchainBalance.Local
</span>
<span class="text-secondary text-nowrap">
@ -54,7 +54,7 @@
@if (Model.Balance.OffchainBalance.Remote != null)
{
<div class="mt-2">
<span class="fw-semibold" data-balance="@Model.Balance.OffchainBalance.Remote">
<span class="fw-semibold" data-balance="@Model.Balance.OffchainBalance.Remote" data-sensitive>
@Model.Balance.OffchainBalance.Remote
</span>
<span class="text-secondary text-nowrap">
@ -65,7 +65,7 @@
@if (Model.Balance.OffchainBalance.Closing != null)
{
<div class="mt-2">
<span class="fw-semibold" data-balance="@Model.Balance.OffchainBalance.Closing">
<span class="fw-semibold" data-balance="@Model.Balance.OffchainBalance.Closing" data-sensitive>
@Model.Balance.OffchainBalance.Closing
</span>
<span class="text-secondary text-nowrap">
@ -79,7 +79,7 @@
@if (Model.Balance.OnchainBalance != null)
{
<div class="balance">
<h3 class="d-inline-block me-1" data-balance="@Model.TotalOnchain">@Model.TotalOnchain</h3>
<h3 class="d-inline-block me-1" data-balance="@Model.TotalOnchain" data-sensitive>@Model.TotalOnchain</h3>
<span class="text-secondary fw-semibold text-nowrap">
<span class="currency">@Model.CryptoCode</span> on-chain
</span>
@ -87,7 +87,7 @@
@if (Model.Balance.OnchainBalance.Confirmed != null)
{
<div class="mt-2">
<span class="fw-semibold" data-balance="@Model.Balance.OnchainBalance.Confirmed">
<span class="fw-semibold" data-balance="@Model.Balance.OnchainBalance.Confirmed" data-sensitive>
@Model.Balance.OnchainBalance.Confirmed
</span>
<span class="text-secondary text-nowrap">
@ -98,7 +98,7 @@
@if (Model.Balance.OnchainBalance.Unconfirmed != null)
{
<div class="mt-2">
<span class="fw-semibold" data-balance="@Model.Balance.OnchainBalance.Unconfirmed">
<span class="fw-semibold" data-balance="@Model.Balance.OnchainBalance.Unconfirmed" data-sensitive>
@Model.Balance.OnchainBalance.Unconfirmed
</span>
<span class="text-secondary text-nowrap">
@ -109,7 +109,7 @@
@if (Model.Balance.OnchainBalance.Reserved != null)
{
<div class="mt-2">
<span class="fw-semibold" data-balance="@Model.Balance.OnchainBalance.Reserved">
<span class="fw-semibold" data-balance="@Model.Balance.OnchainBalance.Reserved" data-sensitive>
@Model.Balance.OnchainBalance.Reserved
</span>
<span class="text-secondary text-nowrap">

@ -65,7 +65,9 @@
</span>
}
</td>
<td class="text-end">@DisplayFormatter.Currency(invoice.Amount, invoice.Currency)</td>
<td class="text-end">
<span data-sensitive>@DisplayFormatter.Currency(invoice.Amount, invoice.Currency)</span>
</td>
</tr>
}
</tbody>

@ -72,11 +72,15 @@
</td>
@if (tx.Positive)
{
<td class="text-end text-success">@DisplayFormatter.Currency(tx.Balance, tx.Currency)</td>
<td class="text-end text-success">
<span data-sensitive>@DisplayFormatter.Currency(tx.Balance, tx.Currency)</span>
</td>
}
else
{
<td class="text-end text-danger">@DisplayFormatter.Currency(tx.Balance, tx.Currency)</td>
<td class="text-end text-danger">
<span data-sensitive>@DisplayFormatter.Currency(tx.Balance, tx.Currency)</span>
</td>
}
</tr>
}

@ -1,6 +1,7 @@
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Abstractions.Contracts
@using BTCPayServer.Client
@using BTCPayServer.Services
@inject SignInManager<ApplicationUser> SignInManager
@inject BTCPayServerEnvironment Env
@ -29,18 +30,15 @@
{
<a asp-controller="UIHome" asp-action="Index" id="StoreSelectorHome" class="navbar-brand py-2">@{await LogoContent();}</a>
}
else if (Model.CurrentStoreIsOwner)
{
<a asp-controller="UIStores" asp-action="Dashboard" asp-route-storeId="@Model.CurrentStoreId" id="StoreSelectorHome" class="navbar-brand py-2">@{await LogoContent();}</a>
}
else
{
<a asp-controller="UIInvoice" asp-action="ListInvoices" asp-route-storeId="@Model.CurrentStoreId" id="StoreSelectorHome" class="navbar-brand py-2">@{await LogoContent();}</a>
}
<a asp-controller="UIStores" asp-action="Dashboard" permission="@Policies.CanModifyStoreSettings" asp-route-storeId="@Model.CurrentStoreId" id="StoreSelectorHome" class="navbar-brand py-2">@{await LogoContent();}</a>
<div id="StoreSelector">
@if (Model.Options.Any())
{
<a asp-controller="UIInvoice" asp-action="ListInvoices" not-permission="@Policies.CanModifyStoreSettings" asp-route-storeId="@Model.CurrentStoreId" id="StoreSelectorHome" class="navbar-brand py-2">@{await LogoContent();}</a>
}
@if (Model.Options.Any())
{
<div id="StoreSelector">
<div id="StoreSelectorDropdown" class="dropdown only-for-js">
<button id="StoreSelectorToggle" class="btn btn-secondary dropdown-toggle rounded-pill px-3 @(Model.CurrentStoreId == null ? "empty-state" : "")" type="button" data-bs-toggle="dropdown" aria-expanded="false">
@if (!string.IsNullOrEmpty(Model.CurrentStoreLogoFileId))
@ -72,9 +70,5 @@ else
<li><a asp-controller="UIUserStores" asp-action="CreateStore" class="dropdown-item" id="StoreSelectorCreate">Create Store</a></li>
</ul>
</div>
}
else if (SignInManager.IsSignedIn(User))
{
<a asp-controller="UIUserStores" asp-action="CreateStore" class="btn btn-primary w-100 rounded-pill text-nowrap" id="StoreSelectorCreate">Create Store</a>
}
</div>
</div>
}

@ -43,7 +43,6 @@ namespace BTCPayServer.Components.StoreSelector
Text = store.StoreName,
Value = store.Id,
Selected = store.Id == currentStore?.Id,
IsOwner = store.Role == StoreRoles.Owner,
WalletId = walletId
};
})
@ -57,7 +56,6 @@ namespace BTCPayServer.Components.StoreSelector
Options = options,
CurrentStoreId = currentStore?.Id,
CurrentDisplayName = currentStore?.StoreName,
CurrentStoreIsOwner = currentStore?.Role == StoreRoles.Owner,
CurrentStoreLogoFileId = blob?.LogoFileId
};

@ -8,7 +8,6 @@ namespace BTCPayServer.Components.StoreSelector
public string CurrentStoreId { get; set; }
public string CurrentStoreLogoFileId { get; set; }
public string CurrentDisplayName { get; set; }
public bool CurrentStoreIsOwner { get; set; }
}
public class StoreSelectorOption

@ -18,7 +18,7 @@
@if (Model.Balance != null)
{
<div class="balance">
<h3 class="d-inline-block me-1" data-balance="@Model.Balance">@Model.Balance</h3>
<h3 class="d-inline-block me-1" data-balance="@Model.Balance" data-sensitive>@Model.Balance</h3>
<span class="text-secondary fw-semibold currency">@Model.CryptoCode</span>
</div>
}
@ -92,8 +92,7 @@
window.setTimeout(() => {
const yLabels = [...document.querySelectorAll('.ct-label.ct-vertical.ct-start')];
if (yLabels) {
const factor = rate ? 6 : 8;
const width = Math.max(...(yLabels.map(l => l.innerText.length * factor)));
const width = Math.max(...(yLabels.map(l => l.innerText.length * 7.5)));
const opts = Object.assign({}, renderOpts, {
axisY: Object.assign({}, renderOpts.axisY, { offset: width })
});

@ -1,10 +1,29 @@
@model BTCPayServer.Components.TruncateCenter.TruncateCenterViewModel
<span class="truncate-center @Model.Classes">
<span class="truncate-center-truncated" @(Model.Truncated != Model.Text ? $"data-bs-toggle=tooltip title={Model.Text}" : "")>@Model.Truncated</span>
<span class="truncate-center-text">@Model.Text</span>
@{
var classes = string.IsNullOrEmpty(Model.Classes) ? string.Empty : Model.Classes.Trim();
@if (Model.Copy) classes += " truncate-center--copy";
@if (Model.Elastic) classes += " truncate-center--elastic";
}
<span class="truncate-center @classes">
@if (Model.IsVue)
{
<span class="truncate-center-truncated" data-bs-toggle="tooltip" :title=@Safe.Json(Model.Text)>
<span class="truncate-center-start" v-text=@Safe.Json(Model.Text)></span>
<span class="truncate-center-end" v-text=@Safe.Json($"{Model.Text}.slice(-{Model.Padding})")></span>
</span>
<span class="truncate-center-text" v-text=@Safe.Json(Model.Text)></span>
}
else
{
<span class="truncate-center-truncated" @(!string.IsNullOrEmpty(Model.Start) ? $"data-bs-toggle=tooltip title={Model.Text}" : "")>
<span class="truncate-center-start">@(Model.Elastic ? Model.Text : $"{Model.Start}…")</span>
<span class="truncate-center-end">@Model.End</span>
</span>
<span class="truncate-center-text">@Model.Text</span>
}
@if (Model.Copy)
{
<button type="button" class="btn btn-link p-0" data-clipboard="@Model.Text">
<button type="button" class="btn btn-link p-0" @(Model.IsVue ? ":" : string.Empty)data-clipboard=@Safe.Json(Model.Text)>
<vc:icon symbol="copy" />
</button>
}

@ -15,7 +15,7 @@ namespace BTCPayServer.Components.TruncateCenter;
/// <returns>HTML with truncated string</returns>
public class TruncateCenter : ViewComponent
{
public IViewComponentResult Invoke(string text, string link = null, string classes = null, int padding = 7, bool copy = true)
public IViewComponentResult Invoke(string text, string link = null, string classes = null, int padding = 7, bool copy = true, bool elastic = false, bool isVue = false)
{
if (string.IsNullOrEmpty(text))
return new HtmlContentViewComponentResult(new StringHtmlContent(string.Empty));
@ -23,11 +23,17 @@ public class TruncateCenter : ViewComponent
{
Classes = classes,
Padding = padding,
Elastic = elastic,
IsVue = isVue,
Copy = copy,
Text = text,
Link = link,
Truncated = text.Length > 2 * padding ? $"{text[..padding]}…{text[^padding..]}" : text
Link = link
};
if (!isVue && text.Length > 2 * padding)
{
vm.Start = text[..padding];
vm.End = text[^padding..];
}
return View(vm);
}
}

@ -3,10 +3,13 @@ namespace BTCPayServer.Components.TruncateCenter
public class TruncateCenterViewModel
{
public string Text { get; set; }
public string Truncated { get; set; }
public string Start { get; set; }
public string End { get; set; }
public string Classes { get; set; }
public string Link { get; set; }
public int Padding { get; set; }
public bool Copy { get; set; }
public bool Elastic { get; set; }
public bool IsVue { get; set; }
}
}

@ -8,7 +8,7 @@
<div class="d-sm-flex align-items-center justify-content-between">
<a asp-controller="UIWallets" asp-action="WalletTransactions" asp-route-walletId="@Model.WalletId" class="unobtrusive-link">
<h2 class="mb-1">@Model.Label</h2>
<div class="text-muted fw-semibold">
<div class="text-muted fw-semibold" data-sensitive>
@Model.Balance @Model.Network.CryptoCode
</div>
</a>

@ -245,7 +245,7 @@ namespace BTCPayServer.Controllers.Greenfield
EmbeddedCSS = request.EmbeddedCSS?.Trim(),
NotificationUrl = request.NotificationUrl?.Trim(),
Tagline = request.Tagline?.Trim(),
PerksTemplate = request.PerksTemplate is not null ? _appService.SerializeTemplate(_appService.Parse(request.PerksTemplate.Trim(), request.TargetCurrency!)) : null,
PerksTemplate = request.PerksTemplate is not null ? AppService.SerializeTemplate(AppService.Parse(request.PerksTemplate.Trim())) : null,
// If Disqus shortname is not null or empty we assume that Disqus should be enabled
DisqusEnabled = !string.IsNullOrEmpty(request.DisqusShortname?.Trim()),
DisqusShortname = request.DisqusShortname?.Trim(),
@ -272,7 +272,7 @@ namespace BTCPayServer.Controllers.Greenfield
ShowDiscount = request.ShowDiscount,
EnableTips = request.EnableTips,
Currency = request.Currency,
Template = request.Template != null ? _appService.SerializeTemplate(_appService.Parse(request.Template, request.Currency)) : null,
Template = request.Template != null ? AppService.SerializeTemplate(AppService.Parse(request.Template)) : null,
ButtonText = request.FixedAmountPayButtonText ?? PointOfSaleSettings.BUTTON_TEXT_DEF,
CustomButtonText = request.CustomAmountPayButtonText ?? PointOfSaleSettings.CUSTOM_BUTTON_TEXT_DEF,
CustomTipText = request.TipText ?? PointOfSaleSettings.CUSTOM_TIP_TEXT_DEF,
@ -331,7 +331,7 @@ namespace BTCPayServer.Controllers.Greenfield
Currency = settings.Currency,
Items = JsonConvert.DeserializeObject(
JsonConvert.SerializeObject(
_appService.Parse(settings.Template, settings.Currency),
AppService.Parse(settings.Template),
new JsonSerializerSettings
{
ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver()
@ -363,8 +363,8 @@ namespace BTCPayServer.Controllers.Greenfield
{
try
{
// Just checking if we can serialize, we don't care about the currency
_appService.SerializeTemplate(_appService.Parse(request.Template, "USD"));
// Just checking if we can serialize
AppService.SerializeTemplate(AppService.Parse(request.Template));
}
catch
{
@ -406,7 +406,7 @@ namespace BTCPayServer.Controllers.Greenfield
Tagline = settings.Tagline,
Perks = JsonConvert.DeserializeObject(
JsonConvert.SerializeObject(
_appService.Parse(settings.PerksTemplate, settings.TargetCurrency),
AppService.Parse(settings.PerksTemplate),
new JsonSerializerSettings
{
ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver()
@ -453,8 +453,8 @@ namespace BTCPayServer.Controllers.Greenfield
try
{
// Just checking if we can serialize, we don't care about the currency
_appService.SerializeTemplate(_appService.Parse(request.PerksTemplate, "USD"));
// Just checking if we can serialize
AppService.SerializeTemplate(AppService.Parse(request.PerksTemplate));
}
catch
{

@ -183,7 +183,7 @@ namespace BTCPayServer.Controllers.Greenfield
{
ModelState.AddModelError(nameof(request.Amount), "The amount should be 0 or more.");
}
request.Checkout = request.Checkout ?? new CreateInvoiceRequest.CheckoutOptions();
request.Checkout ??= new CreateInvoiceRequest.CheckoutOptions();
if (request.Checkout.PaymentMethods?.Any() is true)
{
for (int i = 0; i < request.Checkout.PaymentMethods.Length; i++)
@ -226,7 +226,7 @@ namespace BTCPayServer.Controllers.Greenfield
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
try
{
var invoice = await _invoiceController.CreateInvoiceCoreRaw(request, store,
@ -383,14 +383,15 @@ namespace BTCPayServer.Controllers.Greenfield
}
if (invoicePaymentMethod is null)
{
this.ModelState.AddModelError(nameof(request.PaymentMethod), "Please select one of the payment methods which were available for the original invoice");
ModelState.AddModelError(nameof(request.PaymentMethod), "Please select one of the payment methods which were available for the original invoice");
}
if (request.RefundVariant is null)
this.ModelState.AddModelError(nameof(request.RefundVariant), "`refundVariant` is mandatory");
ModelState.AddModelError(nameof(request.RefundVariant), "`refundVariant` is mandatory");
if (!ModelState.IsValid || invoicePaymentMethod is null || paymentMethodId is null)
return this.CreateValidationError(ModelState);
var cryptoPaid = invoicePaymentMethod.Calculate().Paid.ToDecimal(MoneyUnit.BTC);
var accounting = invoicePaymentMethod.Calculate();
var cryptoPaid = accounting.Paid.ToDecimal(MoneyUnit.BTC);
var cdCurrency = _currencyNameTable.GetCurrencyData(invoice.Currency, true);
var paidCurrency = Math.Round(cryptoPaid * invoicePaymentMethod.Rate, cdCurrency.Divisibility);
var rateResult = await _rateProvider.FetchRate(
@ -398,8 +399,10 @@ namespace BTCPayServer.Controllers.Greenfield
store.GetStoreBlob().GetRateRules(_networkProvider),
cancellationToken
);
var cryptoCode = invoicePaymentMethod.GetId().CryptoCode;
var paymentMethodDivisibility = _currencyNameTable.GetCurrencyData(paymentMethodId.CryptoCode, false)?.Divisibility ?? 8;
var createPullPayment = new HostedServices.CreatePullPayment()
var paidAmount = cryptoPaid.RoundToSignificant(paymentMethodDivisibility);
var createPullPayment = new CreatePullPayment
{
BOLT11Expiration = store.GetStoreBlob().RefundBOLT11Expiration,
Name = request.Name ?? $"Refund {invoice.Id}",
@ -411,37 +414,61 @@ namespace BTCPayServer.Controllers.Greenfield
if (request.RefundVariant != RefundVariant.Custom)
{
if (request.CustomAmount is not null)
this.ModelState.AddModelError(nameof(request.CustomAmount), "CustomAmount should only be set if the refundVariant is Custom");
ModelState.AddModelError(nameof(request.CustomAmount), "CustomAmount should only be set if the refundVariant is Custom");
if (request.CustomCurrency is not null)
this.ModelState.AddModelError(nameof(request.CustomCurrency), "CustomCurrency should only be set if the refundVariant is Custom");
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
ModelState.AddModelError(nameof(request.CustomCurrency), "CustomCurrency should only be set if the refundVariant is Custom");
}
if (request.SubtractPercentage is < 0 or > 100)
{
ModelState.AddModelError(nameof(request.SubtractPercentage), "Percentage must be a numeric value between 0 and 100");
}
if (!ModelState.IsValid)
{
return this.CreateValidationError(ModelState);
}
var appliedDivisibility = paymentMethodDivisibility;
switch (request.RefundVariant)
{
case RefundVariant.RateThen:
createPullPayment.Currency = invoicePaymentMethod.GetId().CryptoCode;
createPullPayment.Amount = cryptoPaid.RoundToSignificant(paymentMethodDivisibility);
createPullPayment.Currency = cryptoCode;
createPullPayment.Amount = paidAmount;
createPullPayment.AutoApproveClaims = true;
break;
case RefundVariant.CurrentRate:
createPullPayment.Currency = invoicePaymentMethod.GetId().CryptoCode;
createPullPayment.Amount = Math.Round(paidCurrency / rateResult.BidAsk.Bid, paymentMethodDivisibility);
createPullPayment.Currency = cryptoCode;
createPullPayment.Amount = Math.Round(paidCurrency / rateResult.BidAsk.Bid, appliedDivisibility);
createPullPayment.AutoApproveClaims = true;
break;
case RefundVariant.Fiat:
appliedDivisibility = cdCurrency.Divisibility;
createPullPayment.Currency = invoice.Currency;
createPullPayment.Amount = paidCurrency;
createPullPayment.AutoApproveClaims = false;
break;
case RefundVariant.OverpaidAmount:
if (invoice.ExceptionStatus != InvoiceExceptionStatus.PaidOver)
{
ModelState.AddModelError(nameof(request.RefundVariant), "Invoice is not overpaid");
}
if (!ModelState.IsValid)
{
return this.CreateValidationError(ModelState);
}
var dueAmount = accounting.TotalDue.ToDecimal(MoneyUnit.BTC);
createPullPayment.Currency = cryptoCode;
createPullPayment.Amount = Math.Round(paidAmount - dueAmount, appliedDivisibility);
createPullPayment.AutoApproveClaims = true;
break;
case RefundVariant.Custom:
if (request.CustomAmount is null || (request.CustomAmount is decimal v && v <= 0))
{
this.ModelState.AddModelError(nameof(request.CustomAmount), "Amount must be greater than 0");
ModelState.AddModelError(nameof(request.CustomAmount), "Amount must be greater than 0");
}
if (
@ -472,6 +499,13 @@ namespace BTCPayServer.Controllers.Greenfield
ModelState.AddModelError(nameof(request.RefundVariant), "Please select a valid refund option");
return this.CreateValidationError(ModelState);
}
// reduce by percentage
if (request.SubtractPercentage is > 0 and <= 100)
{
var reduceByAmount = createPullPayment.Amount * (request.SubtractPercentage / 100);
createPullPayment.Amount = Math.Round(createPullPayment.Amount - reduceByAmount, appliedDivisibility);
}
var ppId = await _pullPaymentService.CreatePullPayment(createPullPayment);
@ -547,7 +581,7 @@ namespace BTCPayServer.Controllers.Greenfield
Amount = accounting.TotalDue.ToDecimal(MoneyUnit.BTC),
NetworkFee = accounting.NetworkFee.ToDecimal(MoneyUnit.BTC),
PaymentLink =
method.GetId().PaymentType.GetPaymentLink(method.Network, details, accounting.Due,
method.GetId().PaymentType.GetPaymentLink(method.Network, entity, details, accounting.Due,
Request.GetAbsoluteRoot()),
Payments = payments.Select(paymentEntity => ToPaymentModel(entity, paymentEntity)).ToList(),
AdditionalData = details.GetAdditionalData()

@ -133,7 +133,7 @@ namespace BTCPayServer.Controllers.Greenfield
"A valid node info was not provided to open a channel with");
}
if (request.ChannelAmount == null)
if (request?.ChannelAmount is null)
{
ModelState.AddModelError(nameof(request.ChannelAmount), "ChannelAmount is missing");
}
@ -142,7 +142,7 @@ namespace BTCPayServer.Controllers.Greenfield
ModelState.AddModelError(nameof(request.ChannelAmount), "ChannelAmount must be more than 0");
}
if (request.FeeRate == null)
if (request?.FeeRate is null)
{
ModelState.AddModelError(nameof(request.FeeRate), "FeeRate is missing");
}

@ -124,7 +124,8 @@ namespace BTCPayServer.Controllers.Greenfield
try
{
var invoice = await _invoiceController.CreatePaymentRequestInvoice(pr, amount, this.StoreData, Request, cancellationToken);
var prData = await _paymentRequestRepository.FindPaymentRequest(pr.Id, null);
var invoice = await _invoiceController.CreatePaymentRequestInvoice(prData, amount, pr.AmountDue, this.StoreData, Request, cancellationToken);
return Ok(GreenfieldInvoiceController.ToModel(invoice, _linkGenerator, Request));
}
catch (BitpayHttpException e)

@ -352,7 +352,7 @@ namespace BTCPayServer.Controllers.Greenfield
[Authorize(Policy = Policies.CanCreateNonApprovedPullPayments, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> CreatePayoutThroughStore(string storeId, CreatePayoutThroughStoreRequest request)
{
if (request.Approved is true)
if (request?.Approved is true)
{
if (!(await _authorizationService.AuthorizeAsync(User, null,
new PolicyRequirement(Policies.CanCreatePullPayments))).Succeeded)

@ -0,0 +1,42 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers.Greenfield;
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[EnableCors(CorsPolicies.All)]
public class GreenfieldServerRolesController : ControllerBase
{
private readonly StoreRepository _storeRepository;
public GreenfieldServerRolesController(StoreRepository storeRepository)
{
_storeRepository = storeRepository;
}
[Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/server/roles")]
public async Task<IActionResult> GetServerRoles()
{
return Ok(FromModel(await _storeRepository.GetStoreRoles(null, false, false)));
}
private List<RoleData> FromModel(StoreRepository.StoreRole[] data)
{
return data.Select(r => new RoleData() {Role = r.Role, Id = r.Id, Permissions = r.Permissions, IsServerRole = true}).ToList();
}
private IActionResult StoreNotFound()
{
return this.CreateAPIError(404, "store-not-found", "The store was not found");
}
}

@ -2,6 +2,7 @@
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
@ -69,6 +70,9 @@ namespace BTCPayServer.Controllers.Greenfield
public async Task<IActionResult> UpdateStoreLightningAutomatedPayoutProcessor(
string storeId, string paymentMethod, LightningAutomatedPayoutSettings request)
{
AutomatedPayoutConstants.ValidateInterval(ModelState, request.IntervalSeconds, nameof(request.IntervalSeconds));
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
paymentMethod = PaymentMethodId.Parse(paymentMethod).ToString();
var activeProcessor =
(await _payoutProcessorService.GetProcessors(

@ -1,7 +1,9 @@
#nullable enable
using System;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
@ -75,6 +77,11 @@ namespace BTCPayServer.Controllers.Greenfield
public async Task<IActionResult> UpdateStoreOnchainAutomatedPayoutProcessor(
string storeId, string paymentMethod, OnChainAutomatedPayoutSettings request)
{
AutomatedPayoutConstants.ValidateInterval(ModelState, request.IntervalSeconds, nameof(request.IntervalSeconds));
if (request.FeeBlockTarget is int t && (t < 1 || t > 1000))
ModelState.AddModelError(nameof(request.FeeBlockTarget), "The feeBlockTarget should be between 1 and 1000");
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
paymentMethod = PaymentMethodId.Parse(paymentMethod).ToString();
var activeProcessor =
(await _payoutProcessorService.GetProcessors(

@ -54,7 +54,7 @@ namespace BTCPayServer.Controllers.Greenfield
new LNURLPayPaymentMethodData(
paymentMethod.PaymentId.CryptoCode,
!excludedPaymentMethods.Match(paymentMethod.PaymentId),
paymentMethod.UseBech32Scheme, paymentMethod.EnableForStandardInvoices
paymentMethod.UseBech32Scheme
)
)
.Where((result) => enabled is null || enabled == result.Enabled)
@ -124,8 +124,7 @@ namespace BTCPayServer.Controllers.Greenfield
LNURLPaySupportedPaymentMethod? paymentMethod = new LNURLPaySupportedPaymentMethod()
{
CryptoCode = cryptoCode,
UseBech32Scheme = paymentMethodData.UseBech32Scheme,
EnableForStandardInvoices = paymentMethodData.EnableForStandardInvoices
UseBech32Scheme = paymentMethodData.UseBech32Scheme
};
var store = Store;
@ -154,7 +153,7 @@ namespace BTCPayServer.Controllers.Greenfield
: new LNURLPayPaymentMethodData(
paymentMethod.PaymentId.CryptoCode,
!excluded,
paymentMethod.UseBech32Scheme, paymentMethod.EnableForStandardInvoices
paymentMethod.UseBech32Scheme
);
}
private void AssertCryptoCodeWallet(string cryptoCode, out BTCPayNetwork network)

@ -66,8 +66,7 @@ namespace BTCPayServer.Controllers.Greenfield
paymentMethod.GetExternalLightningUrl()?.ToString() ??
paymentMethod.GetDisplayableConnectionString(),
!excludedPaymentMethods.Match(paymentMethod.PaymentId),
paymentMethod.PaymentId.ToStringNormalized(),
paymentMethod.DisableBOLT11PaymentOption
paymentMethod.PaymentId.ToStringNormalized()
)
)
.Where((result) => enabled is null || enabled == result.Enabled)
@ -207,7 +206,7 @@ namespace BTCPayServer.Controllers.Greenfield
? null
: new LightningNetworkPaymentMethodData(paymentMethod.PaymentId.CryptoCode,
paymentMethod.GetDisplayableConnectionString(), !excluded,
paymentMethod.PaymentId.ToStringNormalized(), paymentMethod.DisableBOLT11PaymentOption);
paymentMethod.PaymentId.ToStringNormalized());
}
private BTCPayNetwork AssertSupportLightning(string cryptoCode)

@ -585,6 +585,7 @@ namespace BTCPayServer.Controllers.Greenfield
{
await _delayedTransactionBroadcaster.Schedule(DateTimeOffset.UtcNow + TimeSpan.FromMinutes(2.0),
transaction, network);
_payjoinClient.MinimumFeeRate = minRelayFee;
var payjoinPSBT = await _payjoinClient.RequestPayjoin(
new BitcoinUrlBuilder(signingContext.PayJoinBIP21, network.NBitcoinNetwork),
new PayjoinWallet(derivationScheme),

@ -0,0 +1,48 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers.Greenfield
{
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[EnableCors(CorsPolicies.All)]
public class GreenfieldStoreRolesController : ControllerBase
{
private readonly StoreRepository _storeRepository;
public GreenfieldStoreRolesController(StoreRepository storeRepository)
{
_storeRepository = storeRepository;
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/roles")]
public async Task<IActionResult> GetStoreRoles(string storeId)
{
var store = HttpContext.GetStoreData();
return store == null
? StoreNotFound()
: Ok(FromModel(await _storeRepository.GetStoreRoles(storeId, false, false)));
}
private List<RoleData> FromModel(StoreRepository.StoreRole[] data)
{
return data.Select(r => new RoleData() {Role = r.Role, Id = r.Id, Permissions = r.Permissions, IsServerRole = r.IsServerRole}).ToList();
}
private IActionResult StoreNotFound()
{
return this.CreateAPIError(404, "store-not-found", "The store was not found");
}
}
}

@ -63,8 +63,19 @@ namespace BTCPayServer.Controllers.Greenfield
{
return StoreNotFound();
}
//we do not need to validate the role string as any value other than `StoreRoles.Owner` is currently treated like a guest
if (await _storeRepository.AddStoreUser(storeId, request.UserId, request.Role))
StoreRoleId roleId = null;
if (request.Role is not null)
{
roleId = await _storeRepository.ResolveStoreRoleId(storeId, request.Role);
if (roleId is null)
ModelState.AddModelError(nameof(request.Role), "The role id provided does not exist");
}
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
if (await _storeRepository.AddStoreUser(storeId, request.UserId, roleId))
{
return Ok();
}
@ -74,7 +85,7 @@ namespace BTCPayServer.Controllers.Greenfield
private IEnumerable<StoreUserData> FromModel(Data.StoreData data)
{
return data.UserStores.Select(store => new StoreUserData() { UserId = store.ApplicationUserId, Role = store.Role });
return data.UserStores.Select(store => new StoreUserData() { UserId = store.ApplicationUserId, Role = store.StoreRoleId });
}
private IActionResult StoreNotFound()
{

@ -153,15 +153,24 @@ namespace BTCPayServer.Controllers.Greenfield
var delivery = await StoreRepository.GetWebhookDelivery(CurrentStoreId, webhookId, deliveryId);
if (delivery is null)
return WebhookDeliveryNotFound();
if (delivery.GetBlob().IsPruned())
return WebhookDeliveryPruned();
return this.Ok(new JValue(await WebhookSender.Redeliver(deliveryId)));
}
private IActionResult WebhookDeliveryPruned()
{
return this.CreateAPIError(409, "webhookdelivery-pruned", "This webhook delivery has been pruned, so it can't be redelivered");
}
[HttpGet("~/api/v1/stores/{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/request")]
public async Task<IActionResult> GetDeliveryRequest(string storeId, string webhookId, string deliveryId)
{
var delivery = await StoreRepository.GetWebhookDelivery(CurrentStoreId, webhookId, deliveryId);
if (delivery is null)
return WebhookDeliveryNotFound();
if (delivery.GetBlob().IsPruned())
return WebhookDeliveryPruned();
return File(delivery.GetBlob().Request, "application/json");
}

@ -115,11 +115,12 @@ namespace BTCPayServer.Controllers.Greenfield
internal static Client.Models.StoreData FromModel(Data.StoreData data)
{
var storeBlob = data.GetStoreBlob();
return new Client.Models.StoreData()
return new Client.Models.StoreData
{
Id = data.Id,
Name = data.StoreName,
Website = data.StoreWebsite,
SupportUrl = storeBlob.StoreSupportUrl,
SpeedPolicy = data.SpeedPolicy,
DefaultPaymentMethod = data.GetDefaultPaymentId()?.ToStringNormalized(),
//blob
@ -163,7 +164,6 @@ namespace BTCPayServer.Controllers.Greenfield
{
var blob = model.GetStoreBlob();
model.StoreName = restModel.Name;
model.StoreName = restModel.Name;
model.StoreWebsite = restModel.Website;
model.SpeedPolicy = restModel.SpeedPolicy;
model.SetDefaultPaymentId(defaultPaymentMethod);
@ -186,6 +186,7 @@ namespace BTCPayServer.Controllers.Greenfield
blob.ShowRecommendedFee = restModel.ShowRecommendedFee;
blob.RecommendedFeeBlockTarget = restModel.RecommendedFeeBlockTarget;
blob.DefaultLang = restModel.DefaultLang;
blob.StoreSupportUrl = restModel.SupportUrl;
blob.MonitoringExpiration = restModel.MonitoringExpiration;
blob.InvoiceExpiration = restModel.InvoiceExpiration;
blob.DisplayExpirationTimer = restModel.DisplayExpirationTimer;
@ -238,7 +239,7 @@ namespace BTCPayServer.Controllers.Greenfield
ModelState.AddModelError(nameof(request.DisplayExpirationTimer), "DisplayExpirationTimer can only be between 1 and 34560 mins");
if (request.MonitoringExpiration < TimeSpan.FromMinutes(10) && request.MonitoringExpiration > TimeSpan.FromMinutes(60 * 24 * 24))
ModelState.AddModelError(nameof(request.MonitoringExpiration), "MonitoringExpiration can only be between 10 and 34560 mins");
if (request.PaymentTolerance < 0 && request.PaymentTolerance > 100)
if (request.PaymentTolerance < 0 || request.PaymentTolerance > 100)
ModelState.AddModelError(nameof(request.PaymentTolerance), "PaymentTolerance can only be between 0 and 100 percent");
if (request.PaymentMethodCriteria?.Any() is true)

@ -1319,5 +1319,27 @@ namespace BTCPayServer.Controllers.Greenfield
{
return GetFromActionResult<CrowdfundAppData>(await GetController<GreenfieldAppsController>().GetCrowdfundApp(appId));
}
public override async Task<PullPaymentData> RefundInvoice(string storeId, string invoiceId, RefundInvoiceRequest request, CancellationToken token = default)
{
return GetFromActionResult<PullPaymentData>(await GetController<GreenfieldInvoiceController>().RefundInvoice(storeId, invoiceId, request, token));
}
public override async Task RevokeAPIKey(string userId, string apikey, CancellationToken token = default)
{
HandleActionResult(await GetController<GreenfieldApiKeysController>().RevokeAPIKey(userId, apikey));
}
public override async Task<ApiKeyData> CreateAPIKey(string userId, CreateApiKeyRequest request, CancellationToken token = default)
{
return GetFromActionResult<ApiKeyData>(await GetController<GreenfieldApiKeysController>().CreateUserAPIKey(userId, request));
}
public override async Task<List<RoleData>> GetServerRoles(CancellationToken token = default)
{
return GetFromActionResult<List<RoleData>>(await GetController<GreenfieldServerRolesController>().GetServerRoles());
}
public override async Task<List<RoleData>> GetStoreRoles(string storeId, CancellationToken token = default)
{
return GetFromActionResult<List<RoleData>>(await GetController<GreenfieldStoreRolesController>().GetStoreRoles(storeId));
}
}
}

@ -1,4 +1,3 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
@ -9,30 +8,18 @@ using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
using BTCPayServer.Components.StoreSelector;
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.HostedServices;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Security;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Stores;
using ExchangeSharp;
using Google.Apis.Auth.OAuth2;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBitcoin.Payment;
using NBitpayClient;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@ -92,23 +79,14 @@ namespace BTCPayServer.Controllers
var store = await _storeRepository.FindStore(storeId, userId);
if (store != null)
{
return RedirectToStore(store);
return RedirectToStore(userId, store);
}
}
var stores = await _storeRepository.GetStoresByUserId(userId);
if (stores.Any())
{
// redirect to first store
return RedirectToStore(stores.First());
}
var vm = new HomeViewModel
{
HasStore = stores.Any()
};
return View("Home", vm);
return stores.Any()
? RedirectToStore(userId, stores.First())
: RedirectToAction(nameof(UIUserStoresController.CreateStore), "UIUserStores");
}
return Challenge();
@ -220,9 +198,9 @@ namespace BTCPayServer.Controllers
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
public RedirectToActionResult RedirectToStore(StoreData store)
public RedirectToActionResult RedirectToStore(string userId, StoreData store)
{
return store.HasPermission(Policies.CanModifyStoreSettings)
return store.HasPermission(userId, Policies.CanModifyStoreSettings)
? RedirectToAction("Dashboard", "UIStores", new { storeId = store.Id })
: RedirectToAction("ListInvoices", "UIInvoice", new { storeId = store.Id });
}

@ -184,8 +184,14 @@ namespace BTCPayServer.Controllers
var receipt = InvoiceDataBase.ReceiptOptions.Merge(store.GetStoreBlob().ReceiptOptions, i.ReceiptOptions);
if (receipt.Enabled is not true)
{
if (i.RedirectURL is not null)
{
return Redirect(i.RedirectURL.ToString());
}
return NotFound();
}
var storeBlob = store.GetStoreBlob();
var vm = new InvoiceReceiptViewModel
{
@ -238,7 +244,7 @@ namespace BTCPayServer.Controllers
Link = link,
Id = txId,
Destination = paymentData.GetDestination(),
PaymentProof = GetPaymentProof(paymentData),
PaymentProof = paymentData.GetPaymentProof(),
PaymentType = paymentData.GetPaymentType()
};
})
@ -252,16 +258,6 @@ namespace BTCPayServer.Controllers
return View(vm);
}
private string? GetPaymentProof(CryptoPaymentData paymentData)
{
return paymentData switch
{
BitcoinLikePaymentData b => b.Outpoint.ToString(),
LightningLikePaymentData l => l.Preimage?.ToString(),
_ => null
};
}
private string? GetTransactionLink(PaymentMethodId paymentMethodId, string txId)
{
var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode);
@ -351,23 +347,39 @@ namespace BTCPayServer.Controllers
RateRules rules;
RateResult rateResult;
CreatePullPayment createPullPayment;
PaymentMethodAccounting accounting;
var pms = invoice.GetPaymentMethods();
var paymentMethod = pms.SingleOrDefault(method => method.GetId() == paymentMethodId);
var appliedDivisibility = paymentMethodDivisibility;
decimal dueAmount = default;
decimal paidAmount = default;
decimal cryptoPaid = default;
//TODO: Make this clean
if (paymentMethod is null && paymentMethodId.PaymentType == LightningPaymentType.Instance)
{
paymentMethod = pms[new PaymentMethodId(paymentMethodId.CryptoCode, PaymentTypes.LNURLPay)];
}
if (paymentMethod != null)
{
accounting = paymentMethod.Calculate();
cryptoPaid = accounting.Paid.ToDecimal(MoneyUnit.BTC);
dueAmount = accounting.TotalDue.ToDecimal(MoneyUnit.BTC);
paidAmount = cryptoPaid.RoundToSignificant(appliedDivisibility);
}
var isPaidOver = invoice.ExceptionStatus == InvoiceExceptionStatus.PaidOver;
decimal? overpaidAmount = isPaidOver ? Math.Round(paidAmount - dueAmount, appliedDivisibility) : null;
switch (model.RefundStep)
{
case RefundSteps.SelectPaymentMethod:
model.RefundStep = RefundSteps.SelectRate;
model.Title = "How much to refund?";
var pms = invoice.GetPaymentMethods();
var paymentMethod = pms.SingleOrDefault(method => method.GetId() == paymentMethodId);
//TODO: Make this clean
if (paymentMethod is null && paymentMethodId.PaymentType == LightningPaymentType.Instance)
if (paymentMethod != null && cryptoPaid != default)
{
paymentMethod = pms[new PaymentMethodId(paymentMethodId.CryptoCode, PaymentTypes.LNURLPay)];
}
if (paymentMethod != null)
{
var cryptoPaid = paymentMethod.Calculate().Paid.ToDecimal(MoneyUnit.BTC);
var paidCurrency = Math.Round(cryptoPaid * paymentMethod.Rate, cdCurrency.Divisibility);
model.CryptoAmountThen = cryptoPaid.RoundToSignificant(paymentMethodDivisibility);
model.RateThenText = _displayFormatter.Currency(model.CryptoAmountThen, paymentMethodId.CryptoCode);
@ -387,8 +399,15 @@ namespace BTCPayServer.Controllers
model.CurrentRateText = _displayFormatter.Currency(model.CryptoAmountNow, paymentMethodId.CryptoCode);
model.FiatAmount = paidCurrency;
}
model.CryptoCode = paymentMethodId.CryptoCode;
model.CryptoDivisibility = paymentMethodDivisibility;
model.InvoiceDivisibility = cdCurrency.Divisibility;
model.InvoiceCurrency = invoice.Currency;
model.CustomAmount = model.FiatAmount;
model.CustomCurrency = invoice.Currency;
model.SubtractPercentage = 0;
model.OverpaidAmount = overpaidAmount;
model.OverpaidAmountText = overpaidAmount != null ? _displayFormatter.Currency(overpaidAmount.Value, paymentMethodId.CryptoCode) : null;
model.FiatText = _displayFormatter.Currency(model.FiatAmount, invoice.Currency);
return View("_RefundModal", model);
@ -403,6 +422,15 @@ namespace BTCPayServer.Controllers
var authorizedForAutoApprove = (await
_authorizationService.AuthorizeAsync(User, invoice.StoreId, Policies.CanCreatePullPayments))
.Succeeded;
if (model.SubtractPercentage is < 0 or > 100)
{
ModelState.AddModelError(nameof(model.SubtractPercentage), "Percentage must be a numeric value between 0 and 100");
}
if (!ModelState.IsValid)
{
return View("_RefundModal", model);
}
switch (model.SelectedRefundOption)
{
case "RateThen":
@ -418,27 +446,47 @@ namespace BTCPayServer.Controllers
break;
case "Fiat":
appliedDivisibility = cdCurrency.Divisibility;
createPullPayment.Currency = invoice.Currency;
createPullPayment.Amount = model.FiatAmount;
createPullPayment.AutoApproveClaims = false;
break;
case "OverpaidAmount":
model.Title = "How much to refund?";
model.RefundStep = RefundSteps.SelectRate;
if (isPaidOver)
{
ModelState.AddModelError(nameof(model.SelectedRefundOption), "Invoice is not overpaid");
}
if (overpaidAmount == null)
{
ModelState.AddModelError(nameof(model.SelectedRefundOption), "Overpaid amount cannot be calculated");
}
if (!ModelState.IsValid)
{
return this.CreateValidationError(ModelState);
}
createPullPayment.Currency = paymentMethodId.CryptoCode;
createPullPayment.Amount = overpaidAmount!.Value;
createPullPayment.AutoApproveClaims = true;
break;
case "Custom":
model.Title = "How much to refund?";
model.RefundStep = RefundSteps.SelectRate;
if (model.CustomAmount <= 0)
{
model.AddModelError(refundModel => refundModel.CustomAmount, "Amount must be greater than 0", this);
}
if (string.IsNullOrEmpty(model.CustomCurrency) ||
_CurrencyNameTable.GetCurrencyData(model.CustomCurrency, false) == null)
{
ModelState.AddModelError(nameof(model.CustomCurrency), "Invalid currency");
}
if (!ModelState.IsValid)
{
return View("_RefundModal", model);
@ -472,6 +520,13 @@ namespace BTCPayServer.Controllers
throw new ArgumentOutOfRangeException();
}
// reduce by percentage
if (model.SubtractPercentage is > 0 and <= 100)
{
var reduceByAmount = createPullPayment.Amount * (model.SubtractPercentage / 100);
createPullPayment.Amount = Math.Round(createPullPayment.Amount - reduceByAmount, appliedDivisibility);
}
var ppId = await _paymentHostedService.CreatePullPayment(createPullPayment);
TempData.SetStatusMessageModel(new StatusMessageModel
{
@ -583,7 +638,7 @@ namespace BTCPayServer.Controllers
}
if (explorer is null)
return NotSupported("This feature is only available to BTC wallets");
if (this.GetCurrentStore().Role != StoreRoles.Owner)
if (!GetCurrentStore().HasPermission(GetUserId(), Policies.CanModifyStoreSettings))
return Forbid();
var derivationScheme = (this.GetCurrentStore().GetDerivationSchemeSettings(_NetworkProvider, network.CryptoCode))?.AccountDerivation;
@ -676,48 +731,55 @@ namespace BTCPayServer.Controllers
var btcId = PaymentMethodId.Parse("BTC");
var lnId = PaymentMethodId.Parse("BTC_LightningLike");
var lnurlId = PaymentMethodId.Parse("BTC_LNURLPAY");
var displayedPaymentMethods = invoice.GetPaymentMethods().Select(p => p.GetId()).ToList();
// Exclude Lightning if OnChainWithLnInvoiceFallback is active and we have both payment methods
if (storeBlob is { OnChainWithLnInvoiceFallback: true } &&
displayedPaymentMethods.Contains(btcId))
{
displayedPaymentMethods.Remove(lnId);
displayedPaymentMethods.Remove(lnurlId);
}
// BOLT11 doesn't really support payment without amount
if (invoice.IsUnsetTopUp())
displayedPaymentMethods.Remove(lnId);
// Exclude lnurl if bolt11 is available
if (displayedPaymentMethods.Contains(lnId) && displayedPaymentMethods.Contains(lnurlId))
displayedPaymentMethods.Remove(lnurlId);
if (paymentMethodId is not null && !displayedPaymentMethods.Contains(paymentMethodId))
paymentMethodId = null;
if (paymentMethodId is null)
{
var enabledPaymentIds = store.GetEnabledPaymentIds(_NetworkProvider).ToArray();
// Exclude Lightning if OnChainWithLnInvoiceFallback is active and we have both payment methods
if (storeBlob is { CheckoutType: CheckoutType.V2, OnChainWithLnInvoiceFallback: true })
{
if (enabledPaymentIds.Contains(btcId) && enabledPaymentIds.Contains(lnId))
{
enabledPaymentIds = enabledPaymentIds.Where(pmId => pmId != lnId).ToArray();
}
if (enabledPaymentIds.Contains(btcId) && enabledPaymentIds.Contains(lnurlId))
{
enabledPaymentIds = enabledPaymentIds.Where(pmId => pmId != lnurlId).ToArray();
}
}
PaymentMethodId? invoicePaymentId = invoice.GetDefaultPaymentMethod();
PaymentMethodId? storePaymentId = store.GetDefaultPaymentId();
if (invoicePaymentId is not null)
{
if (enabledPaymentIds.Contains(invoicePaymentId))
if (displayedPaymentMethods.Contains(invoicePaymentId))
paymentMethodId = invoicePaymentId;
}
if (paymentMethodId is null && storePaymentId is not null)
{
if (enabledPaymentIds.Contains(storePaymentId))
if (displayedPaymentMethods.Contains(storePaymentId))
paymentMethodId = storePaymentId;
}
if (paymentMethodId is null && invoicePaymentId is not null)
{
paymentMethodId = invoicePaymentId.FindNearest(enabledPaymentIds);
paymentMethodId = invoicePaymentId.FindNearest(displayedPaymentMethods);
}
if (paymentMethodId is null && storePaymentId is not null)
{
paymentMethodId = storePaymentId.FindNearest(enabledPaymentIds);
paymentMethodId = storePaymentId.FindNearest(displayedPaymentMethods);
}
if (paymentMethodId is null)
{
paymentMethodId = enabledPaymentIds.FirstOrDefault(e => e.CryptoCode == _NetworkProvider.DefaultNetwork.CryptoCode && e.PaymentType == PaymentTypes.BTCLike) ??
enabledPaymentIds.FirstOrDefault(e => e.CryptoCode == _NetworkProvider.DefaultNetwork.CryptoCode && e.PaymentType != PaymentTypes.LNURLPay) ??
enabledPaymentIds.FirstOrDefault();
paymentMethodId = displayedPaymentMethods.FirstOrDefault(e => e.CryptoCode == _NetworkProvider.DefaultNetwork.CryptoCode && e.PaymentType == PaymentTypes.BTCLike) ??
displayedPaymentMethods.FirstOrDefault(e => e.CryptoCode == _NetworkProvider.DefaultNetwork.CryptoCode && e.PaymentType != PaymentTypes.LNURLPay) ??
displayedPaymentMethods.FirstOrDefault();
}
isDefaultPaymentId = true;
}
@ -731,30 +793,37 @@ namespace BTCPayServer.Controllers
return null;
var paymentMethodTemp = invoice
.GetPaymentMethods()
.FirstOrDefault(pm =>
{
var pmId = pm.GetId();
return paymentMethodId.CryptoCode == pmId.CryptoCode &&
((invoice.IsUnsetTopUp() && !storeBlob.OnChainWithLnInvoiceFallback) || pmId != lnurlId);
});
if (paymentMethodTemp == null)
paymentMethodTemp = invoice.GetPaymentMethods().FirstOrDefault();
.Where(p => displayedPaymentMethods.Contains(p.GetId()))
.FirstOrDefault();
if (paymentMethodTemp is null)
return null;
network = paymentMethodTemp.Network;
paymentMethodId = paymentMethodTemp.GetId();
}
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId);
var paymentMethodDetails = paymentMethod.GetPaymentMethodDetails();
if (!paymentMethodDetails.Activated)
// We activate the default payment method, and also those which aren't displayed (as they can't be set as default)
bool activated = false;
foreach (var pm in invoice.GetPaymentMethods())
{
if (await _invoiceActivator.ActivateInvoicePaymentMethod(paymentMethod.GetId(), invoice, store))
var pmi = pm.GetId();
if (pmi != paymentMethodId || !displayedPaymentMethods.Contains(pmi))
continue;
var pmd = pm.GetPaymentMethodDetails();
if (!pmd.Activated)
{
return await GetInvoiceModel(invoiceId, paymentMethodId, lang);
if (await _invoiceActivator.ActivateInvoicePaymentMethod(pmi, invoice, store))
{
activated = true;
}
}
}
if (activated)
return await GetInvoiceModel(invoiceId, paymentMethodId, lang);
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId);
var paymentMethodDetails = paymentMethod.GetPaymentMethodDetails();
var dto = invoice.EntityToDTO();
var accounting = paymentMethod.Calculate();
var paymentMethodHandler = _paymentMethodHandlerDictionary[paymentMethodId];
@ -764,7 +833,7 @@ namespace BTCPayServer.Controllers
{
case "auto":
case null when storeBlob.AutoDetectLanguage:
lang = _languageService.AutoDetectLanguageUsingHeader(HttpContext.Request.Headers, null).Code;
lang = _languageService.AutoDetectLanguageUsingHeader(HttpContext.Request.Headers, null)?.Code;
break;
case { } langs when !string.IsNullOrEmpty(langs):
{
@ -783,16 +852,25 @@ namespace BTCPayServer.Controllers
Request.Host,
Request.PathBase) : null;
var isAltcoinsBuild = false;
#if ALTCOINS
isAltcoinsBuild = true;
#endif
var orderId = invoice.Metadata.OrderId;
var supportUrl = !string.IsNullOrEmpty(storeBlob.StoreSupportUrl)
? storeBlob.StoreSupportUrl
.Replace("{OrderId}", string.IsNullOrEmpty(orderId) ? string.Empty : Uri.EscapeDataString(orderId))
.Replace("{InvoiceId}", Uri.EscapeDataString(invoice.Id))
: null;
var model = new PaymentModel
{
#if ALTCOINS
AltcoinsBuild = true,
#endif
Activated = paymentMethodDetails.Activated,
CryptoCode = network.CryptoCode,
RootPath = Request.PathBase.Value.WithTrailingSlash(),
OrderId = invoice.Metadata.OrderId,
InvoiceId = invoice.Id,
OrderId = orderId,
InvoiceId = invoiceId,
DefaultLang = lang ?? invoice.DefaultLanguage ?? storeBlob.DefaultLang ?? "en",
ShowPayInWalletButton = storeBlob.ShowPayInWalletButton,
ShowStoreHeader = storeBlob.ShowStoreHeader,
@ -824,6 +902,7 @@ namespace BTCPayServer.Controllers
ReceiptLink = receiptUrl,
RedirectAutomatically = invoice.RedirectAutomatically,
StoreName = store.StoreName,
StoreSupportUrl = supportUrl,
TxCount = accounting.TxRequired,
TxCountForFee = storeBlob.NetworkFeeMode switch
{
@ -852,11 +931,15 @@ namespace BTCPayServer.Controllers
{
var availableCryptoPaymentMethodId = kv.GetId();
var availableCryptoHandler = _paymentMethodHandlerDictionary[availableCryptoPaymentMethodId];
var pmName = availableCryptoHandler.GetPaymentMethodName(availableCryptoPaymentMethodId);
return new PaymentModel.AvailableCrypto
{
Displayed = displayedPaymentMethods.Contains(kv.GetId()),
PaymentMethodId = kv.GetId().ToString(),
CryptoCode = kv.Network?.CryptoCode ?? kv.GetId().CryptoCode,
PaymentMethodName = availableCryptoHandler.GetPaymentMethodName(availableCryptoPaymentMethodId),
PaymentMethodName = isAltcoinsBuild
? pmName
: pmName.Replace("Bitcoin (", "").Replace(")", "").Replace("Lightning ", ""),
IsLightning =
kv.GetId().PaymentType == PaymentTypes.LightningLike,
CryptoImage = Request.GetRelativePathOrAbsolute(availableCryptoHandler.GetCryptoImage(availableCryptoPaymentMethodId)),
@ -872,17 +955,6 @@ namespace BTCPayServer.Controllers
.ToList()
};
// Exclude Lightning if OnChainWithLnInvoiceFallback is active and we have both payment methods
if (storeBlob is { CheckoutType: CheckoutType.V2, OnChainWithLnInvoiceFallback: true })
{
var onchainPM = model.AvailableCryptos.Find(c => c.PaymentMethodId == btcId.ToString());
var lightningPM = model.AvailableCryptos.Find(c => c.PaymentMethodId == lnId.ToString());
if (onchainPM != null && lightningPM != null)
{
model.AvailableCryptos.Remove(lightningPM);
}
}
paymentMethodHandler.PreparePaymentModel(model, dto, storeBlob, paymentMethod);
model.UISettings = paymentMethodHandler.GetCheckoutUISettings();
model.PaymentMethodId = paymentMethodId.ToString();
@ -1001,34 +1073,44 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> ListInvoices(InvoicesModel? model = null)
{
model = this.ParseListQuery(model ?? new InvoicesModel());
var fs = new SearchString(model.SearchTerm);
var timezoneOffset = model.TimezoneOffset ?? 0;
var searchTerm = string.IsNullOrEmpty(model.SearchText) ? model.SearchTerm : $"{model.SearchText},{model.SearchTerm}";
var fs = new SearchString(searchTerm, timezoneOffset);
string? storeId = model.StoreId;
var storeIds = new HashSet<string>();
if (fs.GetFilterArray("storeid") is string[] l)
if (storeId is not null)
{
storeIds.Add(storeId);
}
if (fs.GetFilterArray("storeid") is { } l)
{
foreach (var i in l)
storeIds.Add(i);
}
if (storeId is not null)
{
storeIds.Add(storeId);
model.StoreId = storeId;
}
model.StoreIds = storeIds.ToArray();
InvoiceQuery invoiceQuery = GetInvoiceQuery(model.SearchTerm, model.TimezoneOffset ?? 0);
invoiceQuery.StoreId = model.StoreIds;
model.Search = fs;
model.SearchText = fs.TextSearch;
InvoiceQuery invoiceQuery = GetInvoiceQuery(fs, timezoneOffset);
invoiceQuery.StoreId = storeIds.ToArray();
invoiceQuery.Take = model.Count;
invoiceQuery.Skip = model.Skip;
invoiceQuery.IncludeRefunds = true;
var list = await _InvoiceRepository.GetInvoices(invoiceQuery);
model.IncludeArchived = invoiceQuery.IncludeArchived;
// Apps
var apps = await _appService.GetAllApps(GetUserId(), false, storeId);
model.Apps = apps.Select(a => new InvoiceAppModel
{
Id = a.Id,
AppName = a.AppName,
AppType = a.AppType,
AppOrderId = AppService.GetAppOrderId(a.AppType, a.Id)
}).ToList();
foreach (var invoice in list)
{
var state = invoice.GetInvoiceState();
model.Invoices.Add(new InvoiceModel()
model.Invoices.Add(new InvoiceModel
{
Status = state,
ShowCheckout = invoice.Status == InvoiceStatusLegacy.New,
@ -1047,10 +1129,9 @@ namespace BTCPayServer.Controllers
return View(model);
}
private InvoiceQuery GetInvoiceQuery(string? searchTerm = null, int timezoneOffset = 0)
private InvoiceQuery GetInvoiceQuery(SearchString fs, int timezoneOffset = 0)
{
var fs = new SearchString(searchTerm);
var invoiceQuery = new InvoiceQuery()
return new InvoiceQuery
{
TextSearch = fs.TextSearch,
UserId = GetUserId(),
@ -1064,7 +1145,6 @@ namespace BTCPayServer.Controllers
StartDate = fs.GetFilterDate("startdate", timezoneOffset),
EndDate = fs.GetFilterDate("enddate", timezoneOffset)
};
return invoiceQuery;
}
[HttpGet]
@ -1075,17 +1155,17 @@ namespace BTCPayServer.Controllers
var model = new InvoiceExport(_CurrencyNameTable);
var fs = new SearchString(searchTerm);
var storeIds = new HashSet<string>();
if (fs.GetFilterArray("storeid") is string[] l)
{
foreach (var i in l)
storeIds.Add(i);
}
if (storeId is not null)
{
storeIds.Add(storeId);
}
if (fs.GetFilterArray("storeid") is { } l)
{
foreach (var i in l)
storeIds.Add(i);
}
InvoiceQuery invoiceQuery = GetInvoiceQuery(searchTerm, timezoneOffset);
InvoiceQuery invoiceQuery = GetInvoiceQuery(fs, timezoneOffset);
invoiceQuery.StoreId = storeIds.ToArray();
invoiceQuery.Skip = 0;
invoiceQuery.Take = int.MaxValue;
@ -1296,10 +1376,15 @@ namespace BTCPayServer.Controllers
{
case JTokenType.Array:
var items = item.Value.AsEnumerable().ToList();
var arrayResult = new List<object>();
for (var i = 0; i < items.Count; i++)
{
result.TryAdd($"{item.Key}[{i}]", ParsePosData(items[i]));
arrayResult.Add(items[i] is JObject
? ParsePosData(items[i])
: items[i].ToString());
}
result.TryAdd(item.Key, arrayResult);
break;
case JTokenType.Object:

@ -58,6 +58,7 @@ namespace BTCPayServer.Controllers
private readonly InvoiceActivator _invoiceActivator;
private readonly LinkGenerator _linkGenerator;
private readonly IAuthorizationService _authorizationService;
private readonly AppService _appService;
public WebhookSender WebhookNotificationManager { get; }
@ -81,6 +82,7 @@ namespace BTCPayServer.Controllers
UIWalletsController walletsController,
InvoiceActivator invoiceActivator,
LinkGenerator linkGenerator,
AppService appService,
IAuthorizationService authorizationService)
{
_displayFormatter = displayFormatter;
@ -102,6 +104,7 @@ namespace BTCPayServer.Controllers
_invoiceActivator = invoiceActivator;
_linkGenerator = linkGenerator;
_authorizationService = authorizationService;
_appService = appService;
}
@ -154,6 +157,7 @@ namespace BTCPayServer.Controllers
entity.Type = InvoiceType.TopUp;
}
entity.StoreSupportUrl = storeBlob.StoreSupportUrl;
entity.RedirectURLTemplate = invoice.RedirectURL ?? store.StoreWebsite;
entity.RedirectAutomatically =
invoice.RedirectAutomatically.GetValueOrDefault(storeBlob.RedirectAutomatically);
@ -187,33 +191,36 @@ namespace BTCPayServer.Controllers
return await CreateInvoiceCoreRaw(entity, store, excludeFilter, null, cancellationToken, entityManipulator);
}
internal async Task<InvoiceEntity> CreatePaymentRequestInvoice(ViewPaymentRequestViewModel pr, decimal? amount, StoreData storeData, HttpRequest request, CancellationToken cancellationToken)
internal async Task<InvoiceEntity> CreatePaymentRequestInvoice(Data.PaymentRequestData prData, decimal? amount, decimal amountDue, StoreData storeData, HttpRequest request, CancellationToken cancellationToken)
{
if (pr.AllowCustomPaymentAmounts && amount != null)
amount = Math.Min(pr.AmountDue, amount.Value);
var id = prData.Id;
var prBlob = prData.GetBlob();
if (prBlob.AllowCustomPaymentAmounts && amount != null)
amount = Math.Min(amountDue, amount.Value);
else
amount = pr.AmountDue;
var redirectUrl = _linkGenerator.PaymentRequestLink(pr.Id, request.Scheme, request.Host, request.PathBase);
amount = amountDue;
var redirectUrl = _linkGenerator.PaymentRequestLink(id, request.Scheme, request.Host, request.PathBase);
var invoiceMetadata =
new InvoiceMetadata
{
OrderId = PaymentRequestRepository.GetOrderIdForPaymentRequest(pr.Id),
PaymentRequestId = pr.Id,
BuyerEmail = pr.Email
};
JObject invoiceMetadata = prData.GetBlob()?.FormResponse ?? new JObject();
invoiceMetadata.Merge(new InvoiceMetadata
{
OrderId = PaymentRequestRepository.GetOrderIdForPaymentRequest(id),
PaymentRequestId = id,
BuyerEmail = invoiceMetadata.TryGetValue("buyerEmail", out var formEmail) && formEmail.Type == JTokenType.String ? formEmail.Value<string>():
string.IsNullOrEmpty(prBlob.Email) ? null : prBlob.Email
}.ToJObject(), new JsonMergeSettings() { MergeNullValueHandling = MergeNullValueHandling.Ignore });
var invoiceRequest =
new CreateInvoiceRequest
{
Metadata = invoiceMetadata.ToJObject(),
Currency = pr.Currency,
Metadata = invoiceMetadata,
Currency = prBlob.Currency,
Amount = amount,
Checkout = { RedirectURL = redirectUrl },
Receipt = new InvoiceDataBase.ReceiptOptions { Enabled = false }
};
var additionalTags = new List<string> { PaymentRequestRepository.GetInternalTag(pr.Id) };
var additionalTags = new List<string> { PaymentRequestRepository.GetInternalTag(id) };
return await CreateInvoiceCoreRaw(invoiceRequest, storeData, request.GetAbsoluteRoot(), additionalTags, cancellationToken);
}
@ -242,10 +249,11 @@ namespace BTCPayServer.Controllers
}
entity.SpeedPolicy = invoice.Checkout.SpeedPolicy ?? store.SpeedPolicy;
entity.DefaultLanguage = invoice.Checkout.DefaultLanguage;
entity.DefaultPaymentMethod = invoice.Checkout.DefaultPaymentMethod;
entity.DefaultPaymentMethod = invoice.Checkout.DefaultPaymentMethod ?? store.GetDefaultPaymentId()?.ToStringNormalized() ?? new PaymentMethodId(_NetworkProvider.DefaultNetwork.CryptoCode, PaymentTypes.BTCLike).ToStringNormalized();
entity.RedirectAutomatically = invoice.Checkout.RedirectAutomatically ?? storeBlob.RedirectAutomatically;
entity.CheckoutType = invoice.Checkout.CheckoutType;
entity.RequiresRefundEmail = invoice.Checkout.RequiresRefundEmail;
entity.LazyPaymentMethods = invoice.Checkout.LazyPaymentMethods ?? storeBlob.LazyPaymentMethods;
IPaymentFilter? excludeFilter = null;
if (invoice.Checkout.PaymentMethods != null)
{
@ -458,7 +466,7 @@ namespace BTCPayServer.Controllers
var storeBlob = store.GetStoreBlob();
// Checkout v2 does not show a payment method switch for Bitcoin-only + BIP21, so exclude that case
var preparePayment = storeBlob.LazyPaymentMethods && !storeBlob.OnChainWithLnInvoiceFallback
var preparePayment = entity.LazyPaymentMethods && !storeBlob.OnChainWithLnInvoiceFallback
? null
: handler.PreparePayment(supportedPaymentMethod, store, network);
var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.Currency)];

@ -59,6 +59,7 @@ namespace BTCPayServer
private readonly PullPaymentHostedService _pullPaymentHostedService;
private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings;
private readonly IPluginHookService _pluginHookService;
private readonly InvoiceActivator _invoiceActivator;
public UILNURLController(InvoiceRepository invoiceRepository,
EventAggregator eventAggregator,
@ -72,7 +73,8 @@ namespace BTCPayServer
LightningLikePayoutHandler lightningLikePayoutHandler,
PullPaymentHostedService pullPaymentHostedService,
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings,
IPluginHookService pluginHookService)
IPluginHookService pluginHookService,
InvoiceActivator invoiceActivator)
{
_invoiceRepository = invoiceRepository;
_eventAggregator = eventAggregator;
@ -87,6 +89,7 @@ namespace BTCPayServer
_pullPaymentHostedService = pullPaymentHostedService;
_btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings;
_pluginHookService = pluginHookService;
_invoiceActivator = invoiceActivator;
}
[HttpGet("withdraw/pp/{pullPaymentId}")]
@ -254,12 +257,12 @@ namespace BTCPayServer
case CrowdfundAppType.AppType:
var cfS = app.GetSettings<CrowdfundSettings>();
currencyCode = cfS.TargetCurrency;
items = _appService.Parse(cfS.PerksTemplate, cfS.TargetCurrency);
items = AppService.Parse(cfS.PerksTemplate);
break;
case PointOfSaleAppType.AppType:
posS = app.GetSettings<PointOfSaleSettings>();
currencyCode = posS.Currency;
items = _appService.Parse(posS.Template, posS.Currency);
items = AppService.Parse(posS.Template);
break;
default:
//TODO: Allow other apps to define lnurl support
@ -358,11 +361,6 @@ namespace BTCPayServer
public ConcurrentDictionary<string, LightningAddressItem> Items { get; } = new();
public ConcurrentDictionary<string, string[]> StoreToItemMap { get; } = new();
public override string ToString()
{
return null;
}
}
[HttpGet("~/.well-known/lnurlp/{username}")]
@ -436,12 +434,16 @@ namespace BTCPayServer
List<string> additionalTags = null,
bool allowOverpay = true)
{
if (GetLNUrlPaymentMethodId(cryptoCode, store, out _) is null)
var pmi = GetLNUrlPaymentMethodId(cryptoCode, store, out _);
if (pmi is null)
return NotFound("LNUrl or LN is disabled");
InvoiceEntity i;
try
{
createInvoice.Checkout ??= new InvoiceDataBase.CheckoutOptions();
createInvoice.Checkout.LazyPaymentMethods = false;
createInvoice.Checkout.PaymentMethods = new[] { pmi.ToStringNormalized() };
i = await _invoiceController.CreateInvoiceCoreRaw(createInvoice, store, Request.GetAbsoluteRoot(), additionalTags);
}
catch (Exception e)
@ -467,13 +469,15 @@ namespace BTCPayServer
lnurlRequest ??= new LNURLPayRequest();
lnUrlMetadata ??= new Dictionary<string, string>();
var pm = i.GetPaymentMethod(pmi);
if (pm is null)
return null;
var paymentMethodDetails = (LNURLPayPaymentMethodDetails)pm.GetPaymentMethodDetails();
bool updatePaymentMethodDetails = false;
if (lnUrlMetadata?.TryGetValue("text/identifier", out var lnAddress) is true && lnAddress is not null)
{
var pm = i.GetPaymentMethod(pmi);
var paymentMethodDetails = (LNURLPayPaymentMethodDetails)pm.GetPaymentMethodDetails();
paymentMethodDetails.ConsumedLightningAddress = lnAddress;
pm.SetPaymentMethodDetails(paymentMethodDetails);
await _invoiceRepository.UpdateInvoicePaymentMethod(i.Id, pm);
updatePaymentMethodDetails = true;
}
if (!lnUrlMetadata.ContainsKey("text/plain"))
@ -494,7 +498,7 @@ namespace BTCPayServer
lnurlRequest.Metadata = JsonConvert.SerializeObject(lnUrlMetadata.Select(kv => new[] { kv.Key, kv.Value }));
if (i.Type != InvoiceType.TopUp)
{
lnurlRequest.MinSendable = new LightMoney(i.GetPaymentMethod(pmi).Calculate().Due.ToDecimal(MoneyUnit.Satoshi), LightMoneyUnit.Satoshi);
lnurlRequest.MinSendable = new LightMoney(pm.Calculate().Due.ToDecimal(MoneyUnit.Satoshi), LightMoneyUnit.Satoshi);
if (!allowOverpay)
lnurlRequest.MaxSendable = lnurlRequest.MinSendable;
}
@ -507,15 +511,16 @@ namespace BTCPayServer
lnurlRequest.MaxSendable = LightMoney.FromUnit(6.12m, LightMoneyUnit.BTC);
lnurlRequest = await _pluginHookService.ApplyFilter("modify-lnurlp-request", lnurlRequest) as LNURLPayRequest;
i.Metadata ??= new InvoiceMetadata();
var metadata = i.Metadata.ToJObject();
if (metadata.Property("payRequest") is null)
if (paymentMethodDetails.PayRequest is null)
{
metadata.Add("payRequest", JToken.FromObject(lnurlRequest));
await _invoiceRepository.UpdateInvoiceMetadata(i.Id, i.StoreId, metadata);
paymentMethodDetails.PayRequest = lnurlRequest;
updatePaymentMethodDetails = true;
}
if (updatePaymentMethodDetails)
{
pm.SetPaymentMethodDetails(paymentMethodDetails);
await _invoiceRepository.UpdateInvoicePaymentMethod(i.Id, pm);
}
return lnurlRequest;
}
@ -568,17 +573,22 @@ namespace BTCPayServer
var lightningPaymentMethod = i.GetPaymentMethod(pmi);
var paymentMethodDetails =
lightningPaymentMethod.GetPaymentMethodDetails() as LNURLPayPaymentMethodDetails;
if (paymentMethodDetails.LightningSupportedPaymentMethod is null)
lightningPaymentMethod?.GetPaymentMethodDetails() as LNURLPayPaymentMethodDetails;
if (paymentMethodDetails is not null && !paymentMethodDetails.Activated)
{
if (!await _invoiceActivator.ActivateInvoicePaymentMethod(pmi, i, store))
return NotFound();
i = await _invoiceRepository.GetInvoice(invoiceId, true);
lightningPaymentMethod = i.GetPaymentMethod(pmi);
paymentMethodDetails = lightningPaymentMethod.GetPaymentMethodDetails() as LNURLPayPaymentMethodDetails;
}
if (paymentMethodDetails?.LightningSupportedPaymentMethod is null)
return NotFound();
LNURLPayRequest lnurlPayRequest;
LNURLPayRequest lnurlPayRequest = paymentMethodDetails.PayRequest;
var blob = store.GetStoreBlob();
if (i.Metadata.AdditionalData.TryGetValue("payRequest", out var t) && t is JObject jo)
{
lnurlPayRequest = jo.ToObject<LNURLPayRequest>();
}
else
if (paymentMethodDetails.PayRequest is null)
{
lnurlPayRequest = await CreateLNUrlRequestFromInvoice(cryptoCode, i, store, blob, allowOverpay: false);
if (lnurlPayRequest is null)

@ -78,7 +78,7 @@ namespace BTCPayServer.Controllers
model = this.ParseListQuery(model ?? new ListPaymentRequestsViewModel());
var store = GetCurrentStore();
var includeArchived = new SearchString(model.SearchTerm).GetFilterBool("includearchived") == true;
var includeArchived = new SearchString(model.SearchTerm, model.TimezoneOffset ?? 0).GetFilterBool("includearchived") == true;
var result = await _PaymentRequestRepository.FindPaymentRequests(new PaymentRequestQuery
{
UserId = GetUserId(),
@ -235,6 +235,14 @@ namespace BTCPayServer.Controllers
}
var form = Form.Parse(formData.Config);
if (!string.IsNullOrEmpty(prBlob.Email))
{
var emailField = form.GetFieldByFullName("buyerEmail");
if (emailField is not null)
{
emailField.Value = prBlob.Email;
}
}
if (Request.Method == "POST" && Request.HasFormContentType)
{
form.ApplyValuesFromForm(Request.Form);
@ -277,7 +285,6 @@ namespace BTCPayServer.Controllers
return BadRequest("Payment Request cannot be paid as it has been archived");
}
if (!result.FormSubmitted && !string.IsNullOrEmpty(result.FormId))
{
var formData = await FormDataService.GetForm(result.FormId);
@ -322,7 +329,8 @@ namespace BTCPayServer.Controllers
try
{
var store = await _storeRepository.FindStore(result.StoreId);
var newInvoice = await _InvoiceController.CreatePaymentRequestInvoice(result, amount, store, Request, cancellationToken);
var prData = await _PaymentRequestRepository.FindPaymentRequest(result.Id, null);
var newInvoice = await _InvoiceController.CreatePaymentRequestInvoice(prData, amount, result.AmountDue, store, Request, cancellationToken);
if (redirectToInvoice)
{
return RedirectToAction("Checkout", "UIInvoice", new { invoiceId = newInvoice.Id });

@ -0,0 +1,185 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Amazon.S3.Transfer;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Models.ServerViewModels;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers
{
public partial class UIServerController
{
[Route("server/roles")]
public async Task<IActionResult> ListRoles(
[FromServices] StoreRepository storeRepository,
RolesViewModel model,
string sortOrder = null
)
{
model ??= new RolesViewModel();
model.DefaultRole = (await storeRepository.GetDefaultRole()).Role;
var roles = await storeRepository.GetStoreRoles(null);
if (sortOrder != null)
{
switch (sortOrder)
{
case "desc":
ViewData["NextRoleSortOrder"] = "asc";
roles = roles.OrderByDescending(user => user.Role).ToArray();
break;
case "asc":
roles = roles.OrderBy(user => user.Role).ToArray();
ViewData["NextRoleSortOrder"] = "desc";
break;
}
}
model.Roles = roles.Skip(model.Skip).Take(model.Count).ToList();
return View(model);
}
[HttpGet("server/roles/{role}")]
public async Task<IActionResult> CreateOrEditRole(
[FromServices] StoreRepository storeRepository,
string role)
{
if (role == "create")
{
ModelState.Remove(nameof(role));
return View(new UpdateRoleViewModel());
}
else
{
var roleData = await storeRepository.GetStoreRole(new StoreRoleId(role));
if (roleData == null)
return NotFound();
return View(new UpdateRoleViewModel()
{
Policies = roleData.Permissions,
Role = roleData.Role
});
}
}
[HttpPost("server/roles/{role}")]
public async Task<IActionResult> CreateOrEditRole(
[FromServices] StoreRepository storeRepository,
[FromRoute] string role, UpdateRoleViewModel viewModel)
{
string successMessage = null;
if (role == "create")
{
successMessage = "Role created";
role = viewModel.Role;
}
else
{
successMessage = "Role updated";
var storeRole = await storeRepository.GetStoreRole(new StoreRoleId(role));
if (storeRole == null)
return NotFound();
}
if (!ModelState.IsValid)
{
return View(viewModel);
}
var r = await storeRepository.AddOrUpdateStoreRole(new StoreRoleId(role), viewModel.Policies);
if (r is null)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = "Role could not be updated"
});
return View(viewModel);
}
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Success,
Message = successMessage
});
return RedirectToAction(nameof(ListRoles));
}
[HttpGet("server/roles/{role}/delete")]
public async Task<IActionResult> DeleteRole(
[FromServices] StoreRepository storeRepository,
string role)
{
var roleData = await storeRepository.GetStoreRole(new StoreRoleId(role), true);
if (roleData == null)
return NotFound();
return View("Confirm",
roleData.IsUsed is true
? new ConfirmModel("Delete role",
$"Unable to proceed: The role <strong>{Html.Encode(roleData.Role)}</strong> is currently assigned to one or more users, it cannot be removed.")
: new ConfirmModel("Delete role",
$"The role <strong>{Html.Encode(roleData.Role)}</strong> will be permanently deleted. Are you sure?",
"Delete"));
}
[HttpPost("server/roles/{role}/delete")]
public async Task<IActionResult> DeleteRolePost(
[FromServices] StoreRepository storeRepository,
string role)
{
var roleId = new StoreRoleId(role);
var roleData = await storeRepository.GetStoreRole(roleId, true);
if (roleData == null)
return NotFound();
if (roleData.IsUsed is true)
{
return BadRequest();
}
var errorMessage = await storeRepository.RemoveStoreRole(roleId);
if (errorMessage is null)
{
TempData[WellKnownTempData.SuccessMessage] = "Role deleted";
}
else
{
TempData[WellKnownTempData.ErrorMessage] = errorMessage;
}
return RedirectToAction(nameof(ListRoles));
}
[HttpGet("server/roles/{role}/default")]
public async Task<IActionResult> SetDefaultRole(
[FromServices] StoreRepository storeRepository,
string role)
{
await storeRepository.SetDefaultRole(role);
TempData[WellKnownTempData.SuccessMessage] = "Role set default";
return RedirectToAction(nameof(ListRoles));
}
}
}
public class UpdateRoleViewModel
{
[Required]
[Display(Name = "Role")]
public string Role { get; set; }
[Display(Name = "Policies")] public List<string> Policies { get; set; } = new();
}

@ -181,7 +181,6 @@ namespace BTCPayServer.Controllers
{
CryptoCode = vm.CryptoCode,
UseBech32Scheme = true,
EnableForStandardInvoices = false,
LUD12Enabled = false
});
@ -245,26 +244,12 @@ namespace BTCPayServer.Controllers
};
SetExistingValues(store, vm);
if (lightning != null)
{
vm.DisableBolt11PaymentMethod = lightning.DisableBOLT11PaymentOption;
}
var lnurl = GetExistingLNURLSupportedPaymentMethod(vm.CryptoCode, store);
if (lnurl != null)
{
vm.LNURLEnabled = !store.GetStoreBlob().GetExcludedPaymentMethods().Match(lnurl.PaymentId);
vm.LNURLBech32Mode = lnurl.UseBech32Scheme;
vm.LNURLStandardInvoiceEnabled = lnurl.EnableForStandardInvoices;
vm.LUD12Enabled = lnurl.LUD12Enabled;
vm.DisableBolt11PaymentMethod =
vm.LNURLEnabled && vm.LNURLStandardInvoiceEnabled && vm.DisableBolt11PaymentMethod;
}
else
{
//disable by default for now
//vm.LNURLEnabled = !lnSet;
vm.DisableBolt11PaymentMethod = false;
}
return View(vm);
@ -290,22 +275,11 @@ namespace BTCPayServer.Controllers
blob.LightningAmountInSatoshi = vm.LightningAmountInSatoshi;
blob.LightningPrivateRouteHints = vm.LightningPrivateRouteHints;
blob.OnChainWithLnInvoiceFallback = vm.OnChainWithLnInvoiceFallback;
var disableBolt11PaymentMethod =
vm.LNURLEnabled && vm.LNURLStandardInvoiceEnabled && vm.DisableBolt11PaymentMethod;
var lnurlId = new PaymentMethodId(vm.CryptoCode, PaymentTypes.LNURLPay);
blob.SetExcluded(lnurlId, !vm.LNURLEnabled);
var lightning = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store);
// Going to mark "lightning" as non-null here assuming that if we are POSTing here it's because we have a Lightning Node set-up
if (lightning!.DisableBOLT11PaymentOption != disableBolt11PaymentMethod)
{
needUpdate = true;
lightning.DisableBOLT11PaymentOption = disableBolt11PaymentMethod;
store.SetSupportedPaymentMethod(lightning);
}
var lnurl = GetExistingLNURLSupportedPaymentMethod(vm.CryptoCode, store);
if (lnurl is null || (
lnurl.EnableForStandardInvoices != vm.LNURLStandardInvoiceEnabled ||
lnurl.UseBech32Scheme != vm.LNURLBech32Mode ||
lnurl.LUD12Enabled != vm.LUD12Enabled))
{
@ -315,7 +289,6 @@ namespace BTCPayServer.Controllers
store.SetSupportedPaymentMethod(new LNURLPaySupportedPaymentMethod
{
CryptoCode = vm.CryptoCode,
EnableForStandardInvoices = vm.LNURLStandardInvoiceEnabled,
UseBech32Scheme = vm.LNURLBech32Mode,
LUD12Enabled = vm.LUD12Enabled
});

@ -0,0 +1,164 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using Amazon.S3.Transfer;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Models.ServerViewModels;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers
{
public partial class UIStoresController
{
[Route("{storeId}/roles")]
public async Task<IActionResult> ListRoles(
string storeId,
[FromServices] StoreRepository storeRepository,
RolesViewModel model,
string sortOrder = null
)
{
model ??= new RolesViewModel();
model.DefaultRole = (await storeRepository.GetDefaultRole()).Role;
var roles = await storeRepository.GetStoreRoles(storeId, false, false);
if (sortOrder != null)
{
switch (sortOrder)
{
case "desc":
ViewData["NextRoleSortOrder"] = "asc";
roles = roles.OrderByDescending(user => user.Role).ToArray();
break;
case "asc":
roles = roles.OrderBy(user => user.Role).ToArray();
ViewData["NextRoleSortOrder"] = "desc";
break;
}
}
model.Roles = roles.Skip(model.Skip).Take(model.Count).ToList();
return View(model);
}
[HttpGet("{storeId}/roles/{role}")]
public async Task<IActionResult> CreateOrEditRole(
string storeId,
[FromServices] StoreRepository storeRepository,
string role)
{
if (role == "create")
{
ModelState.Remove(nameof(role));
return View(new UpdateRoleViewModel());
}
else
{
var roleData = await storeRepository.GetStoreRole(new StoreRoleId(storeId, role));
if (roleData == null)
return NotFound();
return View(new UpdateRoleViewModel()
{
Policies = roleData.Permissions,
Role = roleData.Role
});
}
}
[HttpPost("{storeId}/roles/{role}")]
public async Task<IActionResult> CreateOrEditRole(
string storeId,
[FromServices] StoreRepository storeRepository,
[FromRoute] string role, UpdateRoleViewModel viewModel)
{
string successMessage = null;
StoreRoleId roleId;
if (role == "create")
{
successMessage = "Role created";
role = viewModel.Role;
roleId = new StoreRoleId(storeId, role);
}
else
{
successMessage = "Role updated";
roleId = new StoreRoleId(storeId, role);
var storeRole = await storeRepository.GetStoreRole(roleId);
if (storeRole == null)
return NotFound();
}
if (!ModelState.IsValid)
{
return View(viewModel);
}
var r = await storeRepository.AddOrUpdateStoreRole(roleId, viewModel.Policies);
if (r is null)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = "Role could not be updated"
});
return View(viewModel);
}
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Success,
Message = successMessage
});
return RedirectToAction(nameof(ListRoles), new { storeId });
}
[HttpGet("{storeId}/roles/{role}/delete")]
public async Task<IActionResult> DeleteRole(
string storeId,
[FromServices] StoreRepository storeRepository,
string role)
{
var roleData = await storeRepository.GetStoreRole(new StoreRoleId(storeId, role), true);;
if (roleData == null)
return NotFound();
return View("Confirm",
roleData.IsUsed is true
? new ConfirmModel("Delete role",
$"Unable to proceed: The role <strong>{Html.Encode(roleData.Role)}</strong> is currently assigned to one or more users, it cannot be removed.")
: new ConfirmModel("Delete role",
$"The role <strong>{Html.Encode(roleData.Role)}</strong> will be permanently deleted. Are you sure?",
"Delete"));
}
[HttpPost("{storeId}/roles/{roleId}/delete")]
public async Task<IActionResult> DeleteRolePost(
string storeId,
[FromServices] StoreRepository storeRepository,
string role)
{
var roleId = new StoreRoleId(storeId, role);
var roleData = await storeRepository.GetStoreRole(roleId, true);
if (roleData == null)
return NotFound();
if (roleData.IsUsed is true)
{
return BadRequest();
}
await storeRepository.RemoveStoreRole(roleId);
TempData[WellKnownTempData.SuccessMessage] = "Role deleted";
return RedirectToAction(nameof(ListRoles), new { storeId });
}
}
}

@ -130,6 +130,7 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> StoreUsers()
{
StoreUsersViewModel vm = new StoreUsersViewModel();
vm.Role = StoreRoleId.Guest.Role;
await FillUsers(vm);
return View(vm);
}
@ -142,7 +143,7 @@ namespace BTCPayServer.Controllers
{
Email = u.Email,
Id = u.Id,
Role = u.Role
Role = u.StoreRole.Role
}).ToList();
}
@ -150,7 +151,7 @@ namespace BTCPayServer.Controllers
[HttpPost]
[Route("{storeId}/users")]
public async Task<IActionResult> StoreUsers(StoreUsersViewModel vm)
public async Task<IActionResult> StoreUsers(string storeId, StoreUsersViewModel vm)
{
await FillUsers(vm);
if (!ModelState.IsValid)
@ -163,12 +164,16 @@ namespace BTCPayServer.Controllers
ModelState.AddModelError(nameof(vm.Email), "User not found");
return View(vm);
}
if (!StoreRoles.AllRoles.Contains(vm.Role))
var roles = await _Repo.GetStoreRoles(CurrentStore.Id);
if (roles.All(role => role.Id != vm.Role))
{
ModelState.AddModelError(nameof(vm.Role), "Invalid role");
return View(vm);
}
if (!await _Repo.AddStoreUser(CurrentStore.Id, user.Id, vm.Role))
var roleId = await _Repo.ResolveStoreRoleId(storeId, vm.Role);
if (!await _Repo.AddStoreUser(CurrentStore.Id, user.Id, roleId))
{
ModelState.AddModelError(nameof(vm.Email), "The user already has access to this store");
return View(vm);
@ -611,6 +616,7 @@ namespace BTCPayServer.Controllers
Id = store.Id,
StoreName = store.StoreName,
StoreWebsite = store.StoreWebsite,
StoreSupportUrl = storeBlob.StoreSupportUrl,
LogoFileId = storeBlob.LogoFileId,
CssFileId = storeBlob.CssFileId,
BrandColor = storeBlob.BrandColor,
@ -646,6 +652,7 @@ namespace BTCPayServer.Controllers
}
var blob = CurrentStore.GetStoreBlob();
blob.StoreSupportUrl = model.StoreSupportUrl;
blob.AnyoneCanInvoice = model.AnyoneCanCreateInvoice;
blob.NetworkFeeMode = model.NetworkFeeMode;
blob.PaymentTolerance = model.PaymentTolerance;
@ -887,8 +894,11 @@ namespace BTCPayServer.Controllers
var userId = GetUserId();
if (userId == null)
return Challenge(AuthenticationSchemes.Cookie);
storeId = model.StoreId;
var store = CurrentStore ?? await _Repo.FindStore(storeId, userId);
var store = model.StoreId switch
{
null => CurrentStore,
string id => await _Repo.FindStore(storeId, userId)
};
if (store == null)
return Challenge(AuthenticationSchemes.Cookie);
var tokenRequest = new TokenRequest()
@ -906,7 +916,7 @@ namespace BTCPayServer.Controllers
Id = tokenRequest.PairingCode,
Label = model.Label,
});
await _TokenRepository.PairWithStoreAsync(tokenRequest.PairingCode, storeId);
await _TokenRepository.PairWithStoreAsync(tokenRequest.PairingCode, store.Id);
pairingCode = tokenRequest.PairingCode;
}
else
@ -933,8 +943,9 @@ namespace BTCPayServer.Controllers
ViewBag.HidePublicKey = true;
ViewBag.ShowStores = true;
ViewBag.ShowMenu = false;
var stores = await _Repo.GetStoresByUserId(userId);
model.Stores = new SelectList(stores.Where(s => s.Role == StoreRoles.Owner), nameof(CurrentStore.Id), nameof(CurrentStore.StoreName));
var stores = (await _Repo.GetStoresByUserId(userId)).Where(data => data.HasPermission(userId, Policies.CanModifyStoreSettings)).ToArray();
model.Stores = new SelectList(stores, nameof(CurrentStore.Id), nameof(CurrentStore.StoreName));
if (!model.Stores.Any())
{
TempData[WellKnownTempData.ErrorMessage] = "You need to be owner of at least one store before pairing";
@ -999,14 +1010,14 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(UIHomeController.Index), "UIHome");
}
var stores = await _Repo.GetStoresByUserId(userId);
var stores = (await _Repo.GetStoresByUserId(userId)).Where(data => data.HasPermission(userId, Policies.CanModifyStoreSettings)).ToArray();
return View(new PairingModel
{
Id = pairing.Id,
Label = pairing.Label,
SIN = pairing.SIN ?? "Server-Initiated Pairing",
StoreId = selectedStore ?? stores.FirstOrDefault()?.Id,
Stores = stores.Where(u => u.Role == StoreRoles.Owner).Select(s => new PairingModel.StoreViewModel
Stores = stores.Select(s => new PairingModel.StoreViewModel
{
Id = s.Id,
Name = string.IsNullOrEmpty(s.StoreName) ? s.Id : s.StoreName

@ -39,10 +39,12 @@ namespace BTCPayServer.Controllers
[HttpGet("create")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanModifyStoreSettingsUnscoped)]
public IActionResult CreateStore()
public async Task<IActionResult> CreateStore()
{
var stores = await _repo.GetStoresByUserId(GetUserId());
var vm = new CreateStoreViewModel
{
IsFirstStore = !stores.Any(),
DefaultCurrency = StoreBlob.StandardDefaultCurrency,
Exchanges = GetExchangesSelectList(null)
};
@ -56,6 +58,8 @@ namespace BTCPayServer.Controllers
{
if (!ModelState.IsValid)
{
var stores = await _repo.GetStoresByUserId(GetUserId());
vm.IsFirstStore = !stores.Any();
vm.Exchanges = GetExchangesSelectList(vm.PreferredExchange);
return View(vm);
}

@ -318,6 +318,8 @@ namespace BTCPayServer.Controllers
await _broadcaster.Schedule(DateTimeOffset.UtcNow + TimeSpan.FromMinutes(2.0), cloned.ExtractTransaction(), btcPayNetwork);
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(TimeSpan.FromSeconds(30));
var minRelayFee = _dashboard.Get(btcPayNetwork.CryptoCode).Status.BitcoinStatus?.MinRelayTxFee;
_payjoinClient.MinimumFeeRate = minRelayFee;
return await _payjoinClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, cts.Token);
}

@ -189,11 +189,7 @@ namespace BTCPayServer.Controllers
ListWalletsViewModel.WalletViewModel walletVm = new ListWalletsViewModel.WalletViewModel();
wallets.Wallets.Add(walletVm);
walletVm.Balance = await wallet.Balance + " " + wallet.Wallet.Network.CryptoCode;
walletVm.IsOwner = wallet.Store.Role == StoreRoles.Owner;
if (!walletVm.IsOwner)
{
walletVm.Balance = "";
}
walletVm.CryptoCode = wallet.Network.CryptoCode;
walletVm.StoreId = wallet.Store.Id;

@ -74,6 +74,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
.Include(data => data.PullPaymentData)
.ThenInclude(data => data.StoreData)
.ThenInclude(data => data.UserStores)
.ThenInclude(data => data.StoreRole)
.Where(data =>
payoutIds.Contains(data.Id) &&
data.State == PayoutState.AwaitingPayment &&
@ -84,7 +85,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
if (approvedStores.TryGetValue(payout.PullPaymentData.StoreId, out var value))
return value;
value = payout.PullPaymentData.StoreData.UserStores
.Any(store => store.Role == StoreRoles.Owner && store.ApplicationUserId == userId);
.Any(store => store.ApplicationUserId == userId && store.StoreRole.Permissions.Contains(Policies.CanModifyStoreSettings));
approvedStores.Add(payout.PullPaymentData.StoreId, value);
return value;
}).ToList();

@ -65,6 +65,8 @@ namespace BTCPayServer.Data
_DefaultCurrency = _DefaultCurrency.Trim().ToUpperInvariant();
}
}
public string StoreSupportUrl { get; set; }
CurrencyPair[] _DefaultCurrencyPairs;
[JsonProperty("defaultCurrencyPairs", ItemConverterType = typeof(CurrencyPairJsonConverter))]

@ -16,32 +16,6 @@ namespace BTCPayServer.Data
{
public static class StoreDataExtensions
{
public static PermissionSet GetPermissionSet(this StoreData store)
{
ArgumentNullException.ThrowIfNull(store);
if (store.Role is null)
return new PermissionSet();
return new PermissionSet(store.Role == StoreRoles.Owner
? new[]
{
Permission.Create(Policies.CanModifyStoreSettings, store.Id),
Permission.Create(Policies.CanTradeCustodianAccount, store.Id),
Permission.Create(Policies.CanWithdrawFromCustodianAccounts, store.Id),
Permission.Create(Policies.CanDepositToCustodianAccounts, store.Id)
}
: new[]
{
Permission.Create(Policies.CanViewStoreSettings, store.Id),
Permission.Create(Policies.CanModifyInvoices, store.Id),
Permission.Create(Policies.CanViewCustodianAccounts, store.Id),
Permission.Create(Policies.CanDepositToCustodianAccounts, store.Id)
});
}
public static bool HasPermission(this StoreData store, string permission)
{
ArgumentNullException.ThrowIfNull(store);
return store.GetPermissionSet().Contains(permission, store.Id);
}
#pragma warning disable CS0618
public static PaymentMethodId? GetDefaultPaymentId(this StoreData storeData)

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
@ -29,12 +30,30 @@ namespace BTCPayServer.Data
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
public WebhookDeliveryStatus Status { get; set; }
public int? HttpCode { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string ErrorMessage { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public byte[] Request { get; set; }
public void Prune()
{
var request = JObject.Parse(UTF8Encoding.UTF8.GetString(Request));
foreach (var prop in request.Properties().ToList())
{
if (prop.Name == "type")
continue;
prop.Remove();
}
Request = UTF8Encoding.UTF8.GetBytes(request.ToString(Formatting.None));
}
public T ReadRequestAs<T>()
{
return JsonConvert.DeserializeObject<T>(UTF8Encoding.UTF8.GetString(Request), HostedServices.WebhookSender.DefaultSerializerSettings);
}
public bool IsPruned()
{
return ReadRequestAs<WebhookEvent>().IsPruned();
}
}
public class WebhookBlob
{
@ -56,11 +75,17 @@ namespace BTCPayServer.Data
}
public static WebhookDeliveryBlob GetBlob(this WebhookDeliveryData webhook)
{
return webhook.HasTypedBlob<WebhookDeliveryBlob>().GetBlob(HostedServices.WebhookSender.DefaultSerializerSettings);
if (webhook.Blob is null)
return null;
else
return JsonConvert.DeserializeObject<WebhookDeliveryBlob>(webhook.Blob, HostedServices.WebhookSender.DefaultSerializerSettings);
}
public static void SetBlob(this WebhookDeliveryData webhook, WebhookDeliveryBlob blob)
{
webhook.HasTypedBlob<WebhookDeliveryBlob>().SetBlob(blob, HostedServices.WebhookSender.DefaultSerializerSettings);
if (blob is null)
webhook.Blob = null;
else
webhook.Blob = JsonConvert.SerializeObject(blob, HostedServices.WebhookSender.DefaultSerializerSettings);
}
}
}

@ -4,6 +4,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using BTCPayServer.Events;
using BTCPayServer.Logging;
using Microsoft.Extensions.Logging;
@ -86,9 +87,6 @@ namespace BTCPayServer
}
}
var log = evt.ToString();
if (!String.IsNullOrEmpty(log))
Logs.Events.LogInformation(log);
foreach (var sub in actionList)
{
try

@ -13,6 +13,7 @@ using System.Threading.Tasks;
using BTCPayServer.BIP78.Sender;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Lightning;
using BTCPayServer.Logging;
using BTCPayServer.Models;
@ -25,6 +26,7 @@ using BTCPayServer.Services.Wallets;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBitcoin.Payment;
@ -115,6 +117,14 @@ namespace BTCPayServer
return value;
}
public static IServiceCollection AddScheduledTask<T>(this IServiceCollection services, TimeSpan every)
where T : class, IPeriodicTask
{
services.AddSingleton<T>();
services.AddTransient<ScheduledTask>(o => new ScheduledTask(typeof(T), every));
return services;
}
public static PaymentMethodId GetpaymentMethodId(this InvoiceCryptoInfo info)
{
return new PaymentMethodId(info.CryptoCode, PaymentTypes.Parse(info.PaymentType));

@ -1,11 +1,40 @@
#nullable enable
using System.Linq;
using BTCPayServer.Client;
using BTCPayServer.Data;
namespace BTCPayServer
{
public static class StoreExtensions
{
public static StoreRole? GetStoreRoleOfUser(this StoreData store, string userId)
{
return store.UserStores.FirstOrDefault(r => r.ApplicationUserId == userId)?.StoreRole;
}
public static PermissionSet GetPermissionSet(this StoreRole storeRole, string storeId)
{
return new PermissionSet(storeRole.Permissions
.Select(s => Permission.TryCreatePermission(s, storeId, out var permission) ? permission : null)
.Where(s => s != null).ToArray());
}
public static PermissionSet GetPermissionSet(this StoreData store, string userId)
{
return store.GetStoreRoleOfUser(userId)?.GetPermissionSet(store.Id)?? new PermissionSet();
}
public static bool HasPermission(this StoreData store, string userId, string permission)
{
return GetPermissionSet(store, userId).HasPermission(permission, store.Id);
}
public static bool HasPermission(this PermissionSet permissionSet, string permission, string storeId)
{
return permissionSet.Contains(permission, storeId);
}
public static DerivationSchemeSettings? GetDerivationSchemeSettings(this StoreData store, BTCPayNetworkProvider networkProvider, string cryptoCode)
{
var paymentMethod = store

@ -0,0 +1,26 @@
using System.Collections.Generic;
using BTCPayServer.Abstractions.Form;
namespace BTCPayServer.Forms;
public class FieldValueMirror : IFormComponentProvider
{
public string View { get; } = null;
public void Validate(Form form, Field field)
{
if (form.GetFieldByFullName(field.Value) is null)
{
field.ValidationErrors = new List<string> { $"{field.Name} requires {field.Value} to be present" };
}
}
public void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider)
{
typeToComponentProvider.Add("mirror", this);
}
public string GetValue(Form form, Field field)
{
return form.GetFieldByFullName(field.Value)?.Value;
}
}

@ -12,6 +12,7 @@ public static class FormDataExtensions
serviceCollection.AddSingleton<FormDataService>();
serviceCollection.AddSingleton<FormComponentProviders>();
serviceCollection.AddSingleton<IFormComponentProvider, HtmlInputFormProvider>();
serviceCollection.AddSingleton<IFormComponentProvider, HtmlTextareaFormProvider>();
serviceCollection.AddSingleton<IFormComponentProvider, HtmlFieldsetFormProvider>();
serviceCollection.AddSingleton<IFormComponentProvider, HtmlSelectFormProvider>();
serviceCollection.AddSingleton<IFormComponentProvider, FieldValueMirror>();

@ -5,27 +5,6 @@ using BTCPayServer.Validation;
namespace BTCPayServer.Forms;
public class FieldValueMirror : IFormComponentProvider
{
public string View { get; } = null;
public void Validate(Form form, Field field)
{
if (form.GetFieldByFullName(field.Value) is null)
{
field.ValidationErrors = new List<string>() { $"{field.Name} requires {field.Value} to be present" };
}
}
public void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider)
{
typeToComponentProvider.Add("mirror", this);
}
public string GetValue(Form form, Field field)
{
return form.GetFieldByFullName(field.Value)?.Value;
}
}
public class HtmlInputFormProvider : FormComponentProviderBase
{
public override void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider)

@ -9,10 +9,9 @@ public class HtmlSelectFormProvider : FormComponentProviderBase
{
public override void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider)
{
foreach (var t in new[] {
"select"})
typeToComponentProvider.Add(t, this);
typeToComponentProvider.Add("select", this);
}
public override string View => "Forms/SelectElement";
public override void Validate(Form form, Field field)

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