Compare commits

..

1 Commits

Author SHA1 Message Date
10eca1dc1c Fix a bunch of minor bugs 2023-05-18 09:51:42 +09:00
179 changed files with 1457 additions and 4577 deletions

View File

@ -1,2 +0,0 @@
paths-ignore:
- 'BTCPayServer/wwwroot/vendor/**/*.js'

View File

@ -1,80 +0,0 @@
# 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}}"

View File

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

View File

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

View File

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

View File

@ -1,4 +1,3 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
@ -12,11 +11,5 @@ 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);
}
}
}

View File

@ -9,13 +9,6 @@ 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)
{

View File

@ -51,8 +51,7 @@ namespace BTCPayServer.Client
{
if (message.StatusCode == System.Net.HttpStatusCode.UnprocessableEntity)
{
var aa = await message.Content.ReadAsStringAsync();
var err = JsonConvert.DeserializeObject<Models.GreenfieldValidationError[]>(aa);
var err = JsonConvert.DeserializeObject<Models.GreenfieldValidationError[]>(await message.Content.ReadAsStringAsync());
throw new GreenfieldValidationException(err);
}
if (message.StatusCode == System.Net.HttpStatusCode.Forbidden)

View File

@ -9,7 +9,6 @@ namespace BTCPayServer.Client.Models
{
RateThen,
CurrentRate,
OverpaidAmount,
Fiat,
Custom
}
@ -19,13 +18,8 @@ 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; }

View File

@ -16,8 +16,6 @@ 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);

View File

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

View File

@ -51,10 +51,6 @@ 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);

View File

@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Linq.Expressions;
namespace BTCPayServer.Client
{
@ -136,7 +134,7 @@ namespace BTCPayServer.Client
{
static Permission()
{
PolicyMap = Init();
Init();
}
public static Permission Create(string policy, string scope = null)
@ -237,13 +235,11 @@ namespace BTCPayServer.Client
return subPolicies.Contains(subpolicy) || subPolicies.Any(s => ContainsPolicy(s, subpolicy));
}
public static ReadOnlyDictionary<string, HashSet<string>> PolicyMap { get; private set; }
private static Dictionary<string, HashSet<string>> PolicyMap = new();
private static ReadOnlyDictionary<string, HashSet<string>> Init()
private static void Init()
{
var policyMap = new Dictionary<string, HashSet<string>>();
PolicyHasChild(policyMap, Policies.CanModifyStoreSettings,
PolicyHasChild(Policies.CanModifyStoreSettings,
Policies.CanManageCustodianAccounts,
Policies.CanManagePullPayments,
Policies.CanModifyInvoices,
@ -252,42 +248,25 @@ namespace BTCPayServer.Client
Policies.CanModifyPaymentRequests,
Policies.CanUseLightningNodeInStore);
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,
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,
Policies.CanUseInternalLightningNode,
Policies.CanManageUsers);
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);
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);
}
private static void PolicyHasChild(Dictionary<string, HashSet<string>>policyMap, string policy, params string[] subPolicies)
private static void PolicyHasChild(string policy, params string[] subPolicies)
{
if (policyMap.TryGetValue(policy, out var existingSubPolicies))
if (PolicyMap.TryGetValue(policy, out var existingSubPolicies))
{
foreach (string subPolicy in subPolicies)
{
@ -296,7 +275,7 @@ namespace BTCPayServer.Client
}
else
{
policyMap.Add(policy, subPolicies.ToHashSet());
PolicyMap.Add(policy, subPolicies.ToHashSet());
}
}

View File

@ -64,7 +64,6 @@ 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; }
@ -130,7 +129,6 @@ namespace BTCPayServer.Data
PayoutProcessorData.OnModelCreating(builder, Database);
WebhookData.OnModelCreating(builder, Database);
FormData.OnModelCreating(builder, Database);
StoreRole.OnModelCreating(builder, Database);
if (Database.IsSqlite() && !_designTime)

View File

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

View File

@ -1,10 +1,13 @@
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
{
@ -34,6 +37,8 @@ namespace BTCPayServer.Data
public byte[] StoreCertificate { get; set; }
[NotMapped] public string Role { get; set; }
public string StoreBlob { get; set; }
[Obsolete("Use GetDefaultPaymentId instead")]
@ -47,7 +52,6 @@ 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)
{

View File

@ -1,50 +0,0 @@
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()));
}
}
}

View File

@ -1,4 +1,3 @@
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace BTCPayServer.Data
@ -10,10 +9,7 @@ namespace BTCPayServer.Data
public string StoreDataId { get; set; }
public StoreData StoreData { get; set; }
[Column("Role")]
public string StoreRoleId { get; set; }
public StoreRole StoreRole { get; set; }
public string Role { get; set; }
internal static void OnModelCreating(ModelBuilder builder)
@ -36,10 +32,6 @@ 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);
}
}
}

View File

@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
namespace BTCPayServer.Data
{
public class WebhookDeliveryData
public class WebhookDeliveryData : IHasBlobUntyped
{
[Key]
[MaxLength(25)]
@ -17,8 +17,10 @@ namespace BTCPayServer.Data
[Required]
public DateTimeOffset Timestamp { get; set; }
public string Blob { get; set; }
public bool Pruned { get; set; }
[Obsolete("Use Blob2 instead")]
public byte[] Blob { get; set; }
public string Blob2 { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{
@ -26,11 +28,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.Blob)
.Property(o => o.Blob2)
.HasColumnType("JSONB");
}
}

View File

@ -1,106 +0,0 @@
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");
}
}
}

View File

@ -1,83 +0,0 @@
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)
{
}
}
}

View File

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

View File

@ -6,7 +6,6 @@ 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;
@ -246,7 +245,7 @@ namespace BTCPayServer.Tests
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
user.GrantAccess(true);
user.RegisterLightningNode("BTC");
user.RegisterLightningNode("BTC", LightningConnectionType.Charge);
user.RegisterDerivationScheme("BTC");
user.RegisterDerivationScheme("LTC");
@ -652,7 +651,6 @@ 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);
@ -664,6 +662,7 @@ 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);
@ -724,7 +723,6 @@ 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>();
@ -752,8 +750,6 @@ 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
@ -784,13 +780,15 @@ 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).Single(item => item.Id == "inventoryitem").Inventory);
appService.Parse(vmpos.Template, "BTC").Single(item => item.Id == "inventoryitem").Inventory);
}, 10000);
//test payment methods option
@ -805,8 +803,6 @@ 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);
@ -851,19 +847,18 @@ 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);
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);
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);
Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(app.Id, PosViewType.Static, null, null, null, null, null, "g").Result);
invoices = user.BitPay.GetInvoices();

View File

@ -1,9 +1,13 @@
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;
@ -36,10 +40,8 @@ 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);
@ -138,47 +140,8 @@ 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("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.True(s.Driver.ElementDoesNotExist(By.Id("receipt-btn")));
Assert.Equal(storeUrl, s.Driver.FindElement(By.Id("StoreLink")).GetAttribute("href"));
// Test payment
@ -203,7 +166,7 @@ namespace BTCPayServer.Tests
// Pay partial amount
await Task.Delay(200);
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
amountFraction = "0.00001";
var amountFraction = "0.00001";
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(address, Network.RegTest),
Money.Parse(amountFraction));
await s.Server.ExplorerNode.GenerateAsync(1);
@ -247,8 +210,7 @@ namespace BTCPayServer.Tests
Assert.Contains("Invoice Paid", settledSection.Text);
});
s.Driver.FindElement(By.Id("confetti"));
s.Driver.FindElement(By.Id("ReceiptLink"));
Assert.True(s.Driver.ElementDoesNotExist(By.Id("ContactLink")));
s.Driver.FindElement(By.Id("receipt-btn"));
Assert.Equal(storeUrl, s.Driver.FindElement(By.Id("StoreLink")).GetAttribute("href"));
// BIP21
@ -396,7 +358,6 @@ 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);

View File

@ -1,6 +1,5 @@
using System;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
@ -56,7 +55,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].Role.ToPermissionSet(appList.Apps[0].StoreId).Contains(Policies.CanModifyStoreSettings, appList.Apps[0].StoreId));
Assert.True(appList.Apps[0].IsOwner);
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));

View File

@ -26,7 +26,6 @@ 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;
@ -1043,13 +1042,14 @@ 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"], "abc");
Assert.Single(search.Filters["status"], "abed");
Assert.Single(search.Filters["storeid"]);
Assert.Single(search.Filters["status"]);
Assert.Equal("abc", search.Filters["storeid"].First());
Assert.Equal("abed", search.Filters["status"].First());
filter = "status:abed, status:abed2";
search = new SearchString(filter);
@ -1064,48 +1064,6 @@ 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]
@ -1383,24 +1341,6 @@ 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()
{

View File

@ -21,7 +21,6 @@ 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;
@ -229,7 +228,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});
await unrestricted.AddStoreUser(store.Id, new StoreUserData() { UserId = newUser.Id, Role = "Guest" });
await newUserClient.GetInvoices(store.Id);
}
@ -1320,8 +1319,7 @@ 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);
var roleId = (await tester.PayTester.GetService<StoreRepository>().GetStoreRoles(null)).Single(r => r.Role == "Guest").Id;
storeEntity.StoreRoleId = roleId;
storeEntity.Role = "Guest";
await ctx.SaveChangesAsync();
await AssertHttpError(403, async () => await client.UpdateStore(newStore.Id, new UpdateStoreRequest() { Name = "B" }));
@ -1432,7 +1430,7 @@ namespace BTCPayServer.Tests
Assert.False(hook.AutomaticRedelivery);
Assert.Equal(fakeServer.ServerUri.AbsoluteUri, hook.Url);
}
using var tester = CreateServerTester(newDb: true);
using var tester = CreateServerTester();
using var fakeServer = new FakeServer();
await fakeServer.Start();
await tester.StartAsync();
@ -1509,14 +1507,6 @@ 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"));
@ -1960,82 +1950,6 @@ 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)]
@ -2452,10 +2366,27 @@ namespace BTCPayServer.Tests
Assert.NotNull(merchantInvoice.PaymentHash);
Assert.Equal(merchantInvoice.Id, merchantInvoice.PaymentHash);
var client = await user.CreateClient(Policies.CanUseInternalLightningNode);
// 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"));
// Not permission for the store!
await AssertAPIError("missing-permission", () => client.GetLightningNodeChannels(user.StoreId, "BTC"));
var invoiceData = await client.CreateLightningInvoice("BTC", new CreateLightningInvoiceRequest()
await AssertAPIError("missing-permission", () => chargeClient.GetLightningNodeChannels(user.StoreId, "BTC"));
var invoiceData = await chargeClient.CreateLightningInvoice("BTC", new CreateLightningInvoiceRequest()
{
Amount = LightMoney.Satoshis(1000),
Description = "lol",
@ -2463,17 +2394,17 @@ namespace BTCPayServer.Tests
PrivateRouteHints = false
});
var chargeInvoice = invoiceData;
Assert.NotNull(await client.GetLightningInvoice("BTC", invoiceData.Id));
Assert.NotNull(await chargeClient.GetLightningInvoice("BTC", invoiceData.Id));
// check list for internal node
var invoices = await client.GetLightningInvoices("BTC");
var pendingInvoices = await client.GetLightningInvoices("BTC", true);
var invoices = await chargeClient.GetLightningInvoices("BTC");
var pendingInvoices = await chargeClient.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);
client = await user.CreateClient($"{Policies.CanUseLightningNodeInStore}:{user.StoreId}");
var client = await user.CreateClient($"{Policies.CanUseLightningNodeInStore}:{user.StoreId}");
// Not permission for the server
await AssertAPIError("missing-permission", () => client.GetLightningNodeChannels("BTC"));
@ -2552,7 +2483,7 @@ namespace BTCPayServer.Tests
Assert.Contains(payments, i => i.BOLT11 == merchantInvoice.BOLT11);
// Node info
var info = await client.GetLightningNodeInfo(user.StoreId, "BTC");
info = await client.GetLightningNodeInfo(user.StoreId, "BTC");
Assert.Single(info.NodeURIs);
Assert.NotEqual(0, info.BlockHeight);
@ -2593,12 +2524,7 @@ namespace BTCPayServer.Tests
user.RegisterLightningNode("BTC", LightningConnectionType.CLightning);
var client = await user.CreateClient(Policies.Unrestricted);
var invoices = new Task<Client.Models.InvoiceData>[5];
// Create invoices
for (int i = 0; i < invoices.Length; i++)
{
invoices[i] = client.CreateInvoice(user.StoreId,
var invoice = await client.CreateInvoice(user.StoreId,
new CreateInvoiceRequest
{
Currency = "USD",
@ -2609,35 +2535,18 @@ namespace BTCPayServer.Tests
DefaultPaymentMethod = "BTC_LightningLike"
}
});
}
var pm = Assert.Single(await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id));
Assert.False(pm.AdditionalData.HasValues);
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);
}
var resp = await tester.CustomerLightningD.Pay(pm.Destination);
Assert.Equal(PayResult.Ok, resp.Result);
Assert.NotNull(resp.Details.PaymentHash);
Assert.NotNull(resp.Details.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"));
}
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"));
}
[Fact(Timeout = 60 * 20 * 1000)]
@ -3375,16 +3284,11 @@ 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(ownerRole.Id, storeuser.Role);
Assert.Equal(StoreRoles.Owner, storeuser.Role);
var user2 = tester.NewAccount();
await user2.GrantAccessAsync(false);
@ -3395,7 +3299,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 = guestRole.Id, UserId = user2.UserId });
await client.AddStoreUser(user.StoreId, new StoreUserData() { Role = StoreRoles.Guest, UserId = user2.UserId });
//test no access to api when only a guest
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await user2Client.GetStoreUsers(user.StoreId));
@ -3409,10 +3313,10 @@ namespace BTCPayServer.Tests
await user2Client.GetStore(user.StoreId));
await client.AddStoreUser(user.StoreId, new StoreUserData() { Role = ownerRole.Id, UserId = user2.UserId });
await client.AddStoreUser(user.StoreId, new StoreUserData() { Role = StoreRoles.Owner, UserId = user2.UserId });
await AssertAPIError("duplicate-store-user-role", async () =>
await client.AddStoreUser(user.StoreId,
new StoreUserData() { Role = ownerRole.Id, UserId = user2.UserId }));
new StoreUserData() { Role = StoreRoles.Owner, UserId = user2.UserId }));
await user2Client.RemoveStoreUser(user.StoreId, user.UserId);

View File

@ -1,12 +1,10 @@
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;
@ -21,74 +19,6 @@ 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()
@ -123,7 +53,6 @@ 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>();

View File

@ -180,7 +180,7 @@ namespace BTCPayServer.Tests
{
Driver.FindElement(By.Id("StoreSelectorToggle")).Click();
}
GoToUrl("/stores/create");
Driver.WaitForElement(By.Id("StoreSelectorCreate")).Click();
var name = "Store" + RandomUtils.GetUInt64();
TestLogs.LogInformation($"Created store {name}");
Driver.WaitForElement(By.Id("Name")).SendKeys(name);
@ -313,6 +313,8 @@ 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 =>

View File

@ -1,5 +1,4 @@
using System;
using System.Buffers;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
@ -130,20 +129,16 @@ namespace BTCPayServer.Tests
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.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.CssSelector("[data-form-template='email']"))).Click();
var emailtemplate = s.Driver.FindElement(By.Name("FormConfig")).GetAttribute("value");
Assert.Contains("buyerEmail", emailtemplate);
s.Driver.FindElement(By.Name("FormConfig")).Clear();
s.Driver.FindElement(By.Name("FormConfig"))
.SendKeys(config.Replace("Enter your email", "CustomFormInputTest"));
.SendKeys(emailtemplate.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");
@ -162,16 +157,12 @@ 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.Id("ApplyEmailTemplate")).Click();
s.Driver.FindElement(By.Id("CodeTabButton")).Click();
s.Driver.WaitForElement(By.Id("CodeTabPane"));
s.Driver.FindElement((By.CssSelector("[data-form-template='email']"))).Click();
s.Driver.SetCheckbox(By.Name("Public"), true);
s.Driver.FindElement(By.Name("FormConfig")).Clear();
s.Driver.FindElement(By.Name("FormConfig"))
.SendKeys(config.Replace("Enter your email", "CustomFormInputTest2"));
.SendKeys(emailtemplate.Replace("Enter your email", "CustomFormInputTest2"));
s.Driver.FindElement(By.Id("SaveButton")).Click();
s.Driver.FindElement(By.Id("ViewForm")).Click();
formurl = s.Driver.Url;
@ -609,7 +600,7 @@ namespace BTCPayServer.Tests
TestUtils.Eventually(() =>
{
s.Driver.Navigate().Refresh();
s.Driver.FindElement(By.Id("ReceiptLink")).Click();
s.Driver.FindElement(By.Id("receipt-btn")).Click();
});
TestUtils.Eventually(() =>
{
@ -621,13 +612,14 @@ namespace BTCPayServer.Tests
await s.Server.PayTester.InvoiceRepository.MarkInvoiceStatus(i, InvoiceStatus.Settled);
TestUtils.Eventually(() => s.Driver.FindElement(By.Id("ReceiptLink")).Click());
TestUtils.Eventually(() => s.Driver.FindElement(By.Id("receipt-btn")).Click());
TestUtils.Eventually(() =>
{
s.Driver.Navigate().Refresh();
Assert.DoesNotContain("invoice-unsettled", s.Driver.PageSource);
Assert.DoesNotContain("invoice-processing", s.Driver.PageSource);
});
}
[Fact(Timeout = TestTimeout)]
@ -638,24 +630,21 @@ 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 first store
// should redirect to 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)]
@ -729,8 +718,8 @@ namespace BTCPayServer.Tests
Assert.DoesNotContain(invoiceId, s.Driver.PageSource);
// unarchive via list
s.Driver.FindElement(By.Id("StatusOptionsToggle")).Click();
s.Driver.FindElement(By.Id("StatusOptionsIncludeArchived")).Click();
s.Driver.FindElement(By.Id("SearchOptionsToggle")).Click();
s.Driver.FindElement(By.Id("SearchOptionsIncludeArchived")).Click();
Assert.Contains(invoiceId, s.Driver.PageSource);
s.Driver.FindElement(By.CssSelector($".selector[value=\"{invoiceId}\"]")).Click();
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
@ -965,7 +954,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);
@ -1742,12 +1731,11 @@ 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();
@ -2449,11 +2437,10 @@ 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")).ToList();
links = section.FindElements(By.CssSelector(".tab-content a")).Select(element => element.GetAttribute("href"));
Assert.Equal(2, links.Count());
prevEndpoint = null;
foreach (string link in links)
@ -2467,148 +2454,9 @@ retry:
_ = await request.SendChallenge(linkingKey, new HttpClient());
TestUtils.Eventually(() =>
{
Assert.StartsWith(s.ServerUri.ToString(), s.Driver.Url);
Assert.Equal(s.Driver.Url, s.ServerUri.ToString());
});
}
[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)
{

View File

@ -92,7 +92,7 @@ namespace BTCPayServer.Tests
#endif
public void ActivateLightning()
{
ActivateLightning(LightningConnectionType.CLightning);
ActivateLightning(LightningConnectionType.Charge);
}
public void ActivateLightning(LightningConnectionType internalNode)
{
@ -109,7 +109,14 @@ namespace BTCPayServer.Tests
string connectionString = null;
if (connectionType is null)
return LightningSupportedPaymentMethod.InternalNode;
if (connectionType == LightningConnectionType.CLightning)
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 (isMerchant)
connectionString = "type=clightning;server=" +

View File

@ -277,7 +277,7 @@ namespace BTCPayServer.Tests
public bool IsAdmin { get; internal set; }
public void RegisterLightningNode(string cryptoCode, LightningConnectionType? connectionType = null, bool isMerchant = true)
public void RegisterLightningNode(string cryptoCode, LightningConnectionType connectionType, bool isMerchant = true)
{
RegisterLightningNodeAsync(cryptoCode, connectionType, isMerchant).GetAwaiter().GetResult();
}
@ -470,10 +470,7 @@ 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);
lock (_webhookEvents)
{
_webhookEvents.Add(JsonConvert.DeserializeObject<WebhookInvoiceEvent>(callback));
}
_webhookEvents.Add(JsonConvert.DeserializeObject<WebhookInvoiceEvent>(callback));
req.Response.StatusCode = 200;
_server.Done();
}
@ -490,21 +487,18 @@ namespace BTCPayServer.Tests
{
int retry = 0;
retry:
lock (WebhookEvents)
foreach (var evt in WebhookEvents)
{
foreach (var evt in WebhookEvents)
if (evt.Type == eventType)
{
if (evt.Type == eventType)
var typedEvt = evt.ReadAs<TEvent>();
try
{
assert(typedEvt);
return typedEvt;
}
catch (XunitException)
{
var typedEvt = evt.ReadAs<TEvent>();
try
{
assert(typedEvt);
return typedEvt;
}
catch (XunitException)
{
}
}
}
}
@ -546,12 +540,12 @@ retry:
public async Task AddGuest(string userId)
{
var repo = this.parent.PayTester.GetService<StoreRepository>();
await repo.AddStoreUser(StoreId, userId, StoreRoleId.Guest);
await repo.AddStoreUser(StoreId, userId, "Guest");
}
public async Task AddOwner(string userId)
{
var repo = this.parent.PayTester.GetService<StoreRepository>();
await repo.AddStoreUser(StoreId, userId, StoreRoleId.Owner);
await repo.AddStoreUser(StoreId, userId, "Owner");
}
}
}

View File

@ -358,11 +358,6 @@ 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)

View File

@ -466,6 +466,14 @@ 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")]
@ -1626,7 +1634,7 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount();
var cryptoCode = "BTC";
user.GrantAccess(true);
user.RegisterLightningNode(cryptoCode);
user.RegisterLightningNode(cryptoCode, LightningConnectionType.Charge);
user.SetLNUrl(cryptoCode, false);
var vm = user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModel<CheckoutAppearanceViewModel>();
var criteria = Assert.Single(vm.PaymentMethodCriteria);
@ -1646,7 +1654,7 @@ namespace BTCPayServer.Tests
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);
user.RegisterLightningNode(cryptoCode, LightningConnectionType.Charge);
user.SetLNUrl(cryptoCode, true);
vm = user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModel<CheckoutAppearanceViewModel>();
criteria = Assert.Single(vm.PaymentMethodCriteria);
@ -1962,8 +1970,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(app.Role.ToPermissionSet(app.StoreId).Contains(Policies.CanModifyStoreSettings, app.StoreId));
Assert.True(app.IsOwner);
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));
@ -2147,7 +2154,7 @@ namespace BTCPayServer.Tests
txFee = localInvoice.BtcDue - invoice.BtcDue;
Assert.Equal("paidPartial", localInvoice.ExceptionStatus.ToString());
Assert.Equal(1, localInvoice.CryptoInfo[0].TxCount);
Assert.Equal(localInvoice.BitcoinAddress, invoice.BitcoinAddress); //Same address
Assert.NotEqual(localInvoice.BitcoinAddress, invoice.BitcoinAddress); //New address
Assert.True(IsMapped(invoice, ctx));
Assert.True(IsMapped(localInvoice, ctx));

View File

@ -24,6 +24,7 @@ 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"
@ -55,6 +56,7 @@ services:
- postgres
- customer_lightningd
- merchant_lightningd
- lightning-charged
- customer_lnd
- merchant_lnd
- sshd
@ -73,7 +75,7 @@ services:
- "sshd_datadir:/root/.ssh"
devlnd:
image: btcpayserver/bitcoin:24.1-1
image: btcpayserver/bitcoin:24.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"
@ -87,17 +89,12 @@ 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:
extra_hosts:
- "tests:172.18.0.18"
nbxplorer:
image: nicolasdorier/nbxplorer:2.3.63
restart: unless-stopped
@ -135,7 +132,7 @@ services:
bitcoind:
restart: unless-stopped
image: btcpayserver/bitcoin:24.1-1
image: btcpayserver/bitcoin:24.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"
@ -163,7 +160,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: btcpayserver/lightning:v23.05-dev
image: btcpayserver/lightning:v23.02-1-dev
stop_signal: SIGKILL
restart: unless-stopped
environment:
@ -189,8 +186,30 @@ 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.05-dev
image: btcpayserver/lightning:v23.02-1-dev
stop_signal: SIGKILL
environment:
EXPOSE_TCP: "true"

View File

@ -22,6 +22,7 @@ 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"
@ -53,6 +54,7 @@ services:
- postgres
- customer_lightningd
- merchant_lightningd
- lightning-charged
- customer_lnd
- merchant_lnd
- sshd
@ -70,31 +72,26 @@ services:
- "sshd_datadir:/root/.ssh"
devlnd:
image: btcpayserver/bitcoin:24.1-1
image: btcpayserver/bitcoin:24.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"
BITCOIN_EXTRA_ARGS: |
deprecatedrpc=signrawtransaction
connect=bitcoind:39388
fallbackfee=0.0002
rpcallowip=0.0.0.0/0
fallbackfee=0.0002
depends_on:
- nbxplorer
- postgres
- customer_lnd
- merchant_lnd
selenium:
image: selenium/standalone-chrome:101.0
extra_hosts:
- "tests:172.23.0.18"
- "tests:172.18.0.18"
expose:
- "4444"
networks:
default:
custom:
nbxplorer:
image: nicolasdorier/nbxplorer:2.3.63
restart: unless-stopped
@ -121,7 +118,7 @@ services:
bitcoind:
restart: unless-stopped
image: btcpayserver/bitcoin:24.1-1
image: btcpayserver/bitcoin:24.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"
@ -149,7 +146,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: btcpayserver/lightning:v23.05-dev
image: btcpayserver/lightning:v23.02-1-dev
stop_signal: SIGKILL
restart: unless-stopped
environment:
@ -175,8 +172,30 @@ 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.05-dev
image: btcpayserver/lightning:v23.02-1-dev
stop_signal: SIGKILL
environment:
EXPOSE_TCP: "true"

View File

@ -45,10 +45,9 @@
</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.26" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.4.23" />
<PackageReference Include="CsvHelper" Version="15.0.5" />
<PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="Fido2" Version="2.0.2" />
@ -76,6 +75,7 @@
<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,6 +111,9 @@
<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>

View File

@ -48,7 +48,7 @@
<span class="app-item-point ct-point"></span>
@entry.Title
</span>
<span class="app-item-value" data-sensitive>
<span class="app-item-value">
<span class="text-muted">@entry.SalesCount @($"{label}{(entry.SalesCount == 1 ? "" : "s")}"),</span>
@entry.TotalFormatted
</span>

View File

@ -1,5 +1,6 @@
@using BTCPayServer.Views.Server
@using BTCPayServer.Views.Stores
@using BTCPayServer.Views.Apps
@using BTCPayServer.Views.Invoice
@using BTCPayServer.Views.Manage
@using BTCPayServer.Views.PaymentRequest
@ -177,7 +178,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="manage-plugins"/>
<vc:icon symbol="plugin"/>
<span>Manage Plugins</span>
</a>
</li>
@ -238,7 +239,7 @@
<span>Account</span>
</a>
<ul class="dropdown-menu py-0 w-100" aria-labelledby="Nav-Account">
<li class="p-3 border-bottom">
<li class="p-3">
<strong class="d-block text-truncate" style="max-width:195px">@User.Identity.Name</strong>
@if (User.IsInRole(Roles.ServerAdmin))
{
@ -247,19 +248,10 @@
</li>
@if (!Theme.CustomTheme)
{
<li class="py-1 px-3">
<vc:theme-switch css-class="nav-link pb-0"/>
<li class="border-top py-1 px-3">
<vc:theme-switch css-class="nav-link"/>
</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>

View File

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

View File

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

View File

@ -23,7 +23,7 @@
@if (Model.Balance.OffchainBalance != null)
{
<div class="balance">
<h3 class="d-inline-block me-1" data-balance="@Model.TotalOffchain" data-sensitive>@Model.TotalOffchain</h3>
<h3 class="d-inline-block me-1" data-balance="@Model.TotalOffchain">@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" data-sensitive>
<span class="fw-semibold" data-balance="@Model.Balance.OffchainBalance.Opening">
@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" data-sensitive>
<span class="fw-semibold" data-balance="@Model.Balance.OffchainBalance.Local">
@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" data-sensitive>
<span class="fw-semibold" data-balance="@Model.Balance.OffchainBalance.Remote">
@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" data-sensitive>
<span class="fw-semibold" data-balance="@Model.Balance.OffchainBalance.Closing">
@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" data-sensitive>@Model.TotalOnchain</h3>
<h3 class="d-inline-block me-1" data-balance="@Model.TotalOnchain">@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" data-sensitive>
<span class="fw-semibold" data-balance="@Model.Balance.OnchainBalance.Confirmed">
@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" data-sensitive>
<span class="fw-semibold" data-balance="@Model.Balance.OnchainBalance.Unconfirmed">
@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" data-sensitive>
<span class="fw-semibold" data-balance="@Model.Balance.OnchainBalance.Reserved">
@Model.Balance.OnchainBalance.Reserved
</span>
<span class="text-secondary text-nowrap">

View File

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

View File

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

View File

@ -1,7 +1,6 @@
@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
@ -30,15 +29,18 @@
{
<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="UIStores" asp-action="Dashboard" permission="@Policies.CanModifyStoreSettings" asp-route-storeId="@Model.CurrentStoreId" id="StoreSelectorHome" class="navbar-brand py-2">@{await LogoContent();}</a>
<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>
<a asp-controller="UIInvoice" asp-action="ListInvoices" asp-route-storeId="@Model.CurrentStoreId" id="StoreSelectorHome" class="navbar-brand py-2">@{await LogoContent();}</a>
}
@if (Model.Options.Any())
{
<div id="StoreSelector">
<div id="StoreSelector">
@if (Model.Options.Any())
{
<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))
@ -70,5 +72,9 @@ else
<li><a asp-controller="UIUserStores" asp-action="CreateStore" class="dropdown-item" id="StoreSelectorCreate">Create Store</a></li>
</ul>
</div>
</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>

View File

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

View File

@ -8,6 +8,7 @@ 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

View File

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

View File

@ -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" data-sensitive>
<div class="text-muted fw-semibold">
@Model.Balance @Model.Network.CryptoCode
</div>
</a>

View File

@ -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())) : null,
PerksTemplate = request.PerksTemplate is not null ? _appService.SerializeTemplate(_appService.Parse(request.PerksTemplate.Trim(), request.TargetCurrency!)) : 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)) : null,
Template = request.Template != null ? _appService.SerializeTemplate(_appService.Parse(request.Template, request.Currency)) : 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),
_appService.Parse(settings.Template, settings.Currency),
new JsonSerializerSettings
{
ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver()
@ -363,8 +363,8 @@ namespace BTCPayServer.Controllers.Greenfield
{
try
{
// Just checking if we can serialize
AppService.SerializeTemplate(AppService.Parse(request.Template));
// Just checking if we can serialize, we don't care about the currency
_appService.SerializeTemplate(_appService.Parse(request.Template, "USD"));
}
catch
{
@ -406,7 +406,7 @@ namespace BTCPayServer.Controllers.Greenfield
Tagline = settings.Tagline,
Perks = JsonConvert.DeserializeObject(
JsonConvert.SerializeObject(
AppService.Parse(settings.PerksTemplate),
_appService.Parse(settings.PerksTemplate, settings.TargetCurrency),
new JsonSerializerSettings
{
ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver()
@ -453,8 +453,8 @@ namespace BTCPayServer.Controllers.Greenfield
try
{
// Just checking if we can serialize
AppService.SerializeTemplate(AppService.Parse(request.PerksTemplate));
// Just checking if we can serialize, we don't care about the currency
_appService.SerializeTemplate(_appService.Parse(request.PerksTemplate, "USD"));
}
catch
{

View File

@ -383,15 +383,14 @@ namespace BTCPayServer.Controllers.Greenfield
}
if (invoicePaymentMethod is null)
{
ModelState.AddModelError(nameof(request.PaymentMethod), "Please select one of the payment methods which were available for the original invoice");
this.ModelState.AddModelError(nameof(request.PaymentMethod), "Please select one of the payment methods which were available for the original invoice");
}
if (request.RefundVariant is null)
ModelState.AddModelError(nameof(request.RefundVariant), "`refundVariant` is mandatory");
this.ModelState.AddModelError(nameof(request.RefundVariant), "`refundVariant` is mandatory");
if (!ModelState.IsValid || invoicePaymentMethod is null || paymentMethodId is null)
return this.CreateValidationError(ModelState);
var accounting = invoicePaymentMethod.Calculate();
var cryptoPaid = accounting.Paid.ToDecimal(MoneyUnit.BTC);
var cryptoPaid = invoicePaymentMethod.Calculate().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(
@ -399,10 +398,8 @@ namespace BTCPayServer.Controllers.Greenfield
store.GetStoreBlob().GetRateRules(_networkProvider),
cancellationToken
);
var cryptoCode = invoicePaymentMethod.GetId().CryptoCode;
var paymentMethodDivisibility = _currencyNameTable.GetCurrencyData(paymentMethodId.CryptoCode, false)?.Divisibility ?? 8;
var paidAmount = cryptoPaid.RoundToSignificant(paymentMethodDivisibility);
var createPullPayment = new CreatePullPayment
var createPullPayment = new HostedServices.CreatePullPayment()
{
BOLT11Expiration = store.GetStoreBlob().RefundBOLT11Expiration,
Name = request.Name ?? $"Refund {invoice.Id}",
@ -414,61 +411,37 @@ namespace BTCPayServer.Controllers.Greenfield
if (request.RefundVariant != RefundVariant.Custom)
{
if (request.CustomAmount is not null)
ModelState.AddModelError(nameof(request.CustomAmount), "CustomAmount should only be set if the refundVariant is Custom");
this.ModelState.AddModelError(nameof(request.CustomAmount), "CustomAmount should only be set if the refundVariant is Custom");
if (request.CustomCurrency is not null)
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);
this.ModelState.AddModelError(nameof(request.CustomCurrency), "CustomCurrency should only be set if the refundVariant is Custom");
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
}
var appliedDivisibility = paymentMethodDivisibility;
switch (request.RefundVariant)
{
case RefundVariant.RateThen:
createPullPayment.Currency = cryptoCode;
createPullPayment.Amount = paidAmount;
createPullPayment.Currency = invoicePaymentMethod.GetId().CryptoCode;
createPullPayment.Amount = cryptoPaid.RoundToSignificant(paymentMethodDivisibility);
createPullPayment.AutoApproveClaims = true;
break;
case RefundVariant.CurrentRate:
createPullPayment.Currency = cryptoCode;
createPullPayment.Amount = Math.Round(paidCurrency / rateResult.BidAsk.Bid, appliedDivisibility);
createPullPayment.Currency = invoicePaymentMethod.GetId().CryptoCode;
createPullPayment.Amount = Math.Round(paidCurrency / rateResult.BidAsk.Bid, paymentMethodDivisibility);
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))
{
ModelState.AddModelError(nameof(request.CustomAmount), "Amount must be greater than 0");
this.ModelState.AddModelError(nameof(request.CustomAmount), "Amount must be greater than 0");
}
if (
@ -499,13 +472,6 @@ 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);

View File

@ -1,42 +0,0 @@
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");
}
}

View File

@ -585,7 +585,6 @@ 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),

View File

@ -1,48 +0,0 @@
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");
}
}
}

View File

@ -63,19 +63,8 @@ namespace BTCPayServer.Controllers.Greenfield
{
return StoreNotFound();
}
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))
//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))
{
return Ok();
}
@ -85,7 +74,7 @@ namespace BTCPayServer.Controllers.Greenfield
private IEnumerable<StoreUserData> FromModel(Data.StoreData data)
{
return data.UserStores.Select(store => new StoreUserData() { UserId = store.ApplicationUserId, Role = store.StoreRoleId });
return data.UserStores.Select(store => new StoreUserData() { UserId = store.ApplicationUserId, Role = store.Role });
}
private IActionResult StoreNotFound()
{

View File

@ -153,24 +153,15 @@ 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");
}

View File

@ -115,12 +115,11 @@ 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
@ -186,7 +185,6 @@ 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;

View File

@ -1319,27 +1319,5 @@ 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));
}
}
}

View File

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
@ -8,18 +9,30 @@ 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;
@ -79,14 +92,23 @@ namespace BTCPayServer.Controllers
var store = await _storeRepository.FindStore(storeId, userId);
if (store != null)
{
return RedirectToStore(userId, store);
return RedirectToStore(store);
}
}
var stores = await _storeRepository.GetStoresByUserId(userId);
return stores.Any()
? RedirectToStore(userId, stores.First())
: RedirectToAction(nameof(UIUserStoresController.CreateStore), "UIUserStores");
if (stores.Any())
{
// redirect to first store
return RedirectToStore(stores.First());
}
var vm = new HomeViewModel
{
HasStore = stores.Any()
};
return View("Home", vm);
}
return Challenge();
@ -198,9 +220,9 @@ namespace BTCPayServer.Controllers
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
public RedirectToActionResult RedirectToStore(string userId, StoreData store)
public RedirectToActionResult RedirectToStore(StoreData store)
{
return store.HasPermission(userId, Policies.CanModifyStoreSettings)
return store.HasPermission(Policies.CanModifyStoreSettings)
? RedirectToAction("Dashboard", "UIStores", new { storeId = store.Id })
: RedirectToAction("ListInvoices", "UIInvoice", new { storeId = store.Id });
}

View File

@ -347,39 +347,23 @@ 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);
if (paymentMethod != null && 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)
{
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);
@ -399,15 +383,8 @@ 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);
@ -422,15 +399,6 @@ 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":
@ -446,47 +414,27 @@ 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);
@ -520,13 +468,6 @@ 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
{
@ -638,7 +579,7 @@ namespace BTCPayServer.Controllers
}
if (explorer is null)
return NotSupported("This feature is only available to BTC wallets");
if (!GetCurrentStore().HasPermission(GetUserId(), Policies.CanModifyStoreSettings))
if (this.GetCurrentStore().Role != StoreRoles.Owner)
return Forbid();
var derivationScheme = (this.GetCurrentStore().GetDerivationSchemeSettings(_NetworkProvider, network.CryptoCode))?.AccountDerivation;
@ -854,23 +795,16 @@ namespace BTCPayServer.Controllers
var isAltcoinsBuild = false;
#if ALTCOINS
isAltcoinsBuild = true;
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
{
Activated = paymentMethodDetails.Activated,
CryptoCode = network.CryptoCode,
RootPath = Request.PathBase.Value.WithTrailingSlash(),
OrderId = orderId,
InvoiceId = invoiceId,
OrderId = invoice.Metadata.OrderId,
InvoiceId = invoice.Id,
DefaultLang = lang ?? invoice.DefaultLanguage ?? storeBlob.DefaultLang ?? "en",
ShowPayInWalletButton = storeBlob.ShowPayInWalletButton,
ShowStoreHeader = storeBlob.ShowStoreHeader,
@ -902,7 +836,6 @@ namespace BTCPayServer.Controllers
ReceiptLink = receiptUrl,
RedirectAutomatically = invoice.RedirectAutomatically,
StoreName = store.StoreName,
StoreSupportUrl = supportUrl,
TxCount = accounting.TxRequired,
TxCountForFee = storeBlob.NetworkFeeMode switch
{
@ -1073,44 +1006,34 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> ListInvoices(InvoicesModel? model = null)
{
model = this.ParseListQuery(model ?? new InvoicesModel());
var timezoneOffset = model.TimezoneOffset ?? 0;
var searchTerm = string.IsNullOrEmpty(model.SearchText) ? model.SearchTerm : $"{model.SearchText},{model.SearchTerm}";
var fs = new SearchString(searchTerm, timezoneOffset);
var fs = new SearchString(model.SearchTerm);
string? storeId = model.StoreId;
var storeIds = new HashSet<string>();
if (storeId is not null)
{
storeIds.Add(storeId);
}
if (fs.GetFilterArray("storeid") is { } l)
if (fs.GetFilterArray("storeid") is string[] l)
{
foreach (var i in l)
storeIds.Add(i);
}
model.Search = fs;
model.SearchText = fs.TextSearch;
InvoiceQuery invoiceQuery = GetInvoiceQuery(fs, timezoneOffset);
invoiceQuery.StoreId = storeIds.ToArray();
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;
invoiceQuery.Take = model.Count;
invoiceQuery.Skip = model.Skip;
invoiceQuery.IncludeRefunds = true;
var list = await _InvoiceRepository.GetInvoices(invoiceQuery);
// 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();
model.IncludeArchived = invoiceQuery.IncludeArchived;
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,
@ -1129,9 +1052,10 @@ namespace BTCPayServer.Controllers
return View(model);
}
private InvoiceQuery GetInvoiceQuery(SearchString fs, int timezoneOffset = 0)
private InvoiceQuery GetInvoiceQuery(string? searchTerm = null, int timezoneOffset = 0)
{
return new InvoiceQuery
var fs = new SearchString(searchTerm);
var invoiceQuery = new InvoiceQuery()
{
TextSearch = fs.TextSearch,
UserId = GetUserId(),
@ -1145,6 +1069,7 @@ namespace BTCPayServer.Controllers
StartDate = fs.GetFilterDate("startdate", timezoneOffset),
EndDate = fs.GetFilterDate("enddate", timezoneOffset)
};
return invoiceQuery;
}
[HttpGet]
@ -1155,17 +1080,17 @@ namespace BTCPayServer.Controllers
var model = new InvoiceExport(_CurrencyNameTable);
var fs = new SearchString(searchTerm);
var storeIds = new HashSet<string>();
if (storeId is not null)
{
storeIds.Add(storeId);
}
if (fs.GetFilterArray("storeid") is { } l)
if (fs.GetFilterArray("storeid") is string[] l)
{
foreach (var i in l)
storeIds.Add(i);
}
if (storeId is not null)
{
storeIds.Add(storeId);
}
InvoiceQuery invoiceQuery = GetInvoiceQuery(fs, timezoneOffset);
InvoiceQuery invoiceQuery = GetInvoiceQuery(searchTerm, timezoneOffset);
invoiceQuery.StoreId = storeIds.ToArray();
invoiceQuery.Skip = 0;
invoiceQuery.Take = int.MaxValue;

View File

@ -58,7 +58,6 @@ namespace BTCPayServer.Controllers
private readonly InvoiceActivator _invoiceActivator;
private readonly LinkGenerator _linkGenerator;
private readonly IAuthorizationService _authorizationService;
private readonly AppService _appService;
public WebhookSender WebhookNotificationManager { get; }
@ -82,7 +81,6 @@ namespace BTCPayServer.Controllers
UIWalletsController walletsController,
InvoiceActivator invoiceActivator,
LinkGenerator linkGenerator,
AppService appService,
IAuthorizationService authorizationService)
{
_displayFormatter = displayFormatter;
@ -104,7 +102,6 @@ namespace BTCPayServer.Controllers
_invoiceActivator = invoiceActivator;
_linkGenerator = linkGenerator;
_authorizationService = authorizationService;
_appService = appService;
}
@ -157,7 +154,6 @@ namespace BTCPayServer.Controllers
entity.Type = InvoiceType.TopUp;
}
entity.StoreSupportUrl = storeBlob.StoreSupportUrl;
entity.RedirectURLTemplate = invoice.RedirectURL ?? store.StoreWebsite;
entity.RedirectAutomatically =
invoice.RedirectAutomatically.GetValueOrDefault(storeBlob.RedirectAutomatically);

View File

@ -257,12 +257,12 @@ namespace BTCPayServer
case CrowdfundAppType.AppType:
var cfS = app.GetSettings<CrowdfundSettings>();
currencyCode = cfS.TargetCurrency;
items = AppService.Parse(cfS.PerksTemplate);
items = _appService.Parse(cfS.PerksTemplate, cfS.TargetCurrency);
break;
case PointOfSaleAppType.AppType:
posS = app.GetSettings<PointOfSaleSettings>();
currencyCode = posS.Currency;
items = AppService.Parse(posS.Template);
items = _appService.Parse(posS.Template, posS.Currency);
break;
default:
//TODO: Allow other apps to define lnurl support

View File

@ -78,7 +78,7 @@ namespace BTCPayServer.Controllers
model = this.ParseListQuery(model ?? new ListPaymentRequestsViewModel());
var store = GetCurrentStore();
var includeArchived = new SearchString(model.SearchTerm, model.TimezoneOffset ?? 0).GetFilterBool("includearchived") == true;
var includeArchived = new SearchString(model.SearchTerm).GetFilterBool("includearchived") == true;
var result = await _PaymentRequestRepository.FindPaymentRequests(new PaymentRequestQuery
{
UserId = GetUserId(),

View File

@ -1,194 +0,0 @@
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.error is not null)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = r.error
});
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)
{
var resolved = await storeRepository.ResolveStoreRoleId(null, role);
if (resolved is null)
{
TempData[WellKnownTempData.ErrorMessage] = "Role could not be set as default";
}
else
{
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();
}

View File

@ -1,164 +0,0 @@
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.error is not null)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = r.error
});
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 });
}
}
}

View File

@ -130,7 +130,6 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> StoreUsers()
{
StoreUsersViewModel vm = new StoreUsersViewModel();
vm.Role = StoreRoleId.Guest.Role;
await FillUsers(vm);
return View(vm);
}
@ -143,7 +142,7 @@ namespace BTCPayServer.Controllers
{
Email = u.Email,
Id = u.Id,
Role = u.StoreRole.Role
Role = u.Role
}).ToList();
}
@ -151,7 +150,7 @@ namespace BTCPayServer.Controllers
[HttpPost]
[Route("{storeId}/users")]
public async Task<IActionResult> StoreUsers(string storeId, StoreUsersViewModel vm)
public async Task<IActionResult> StoreUsers(StoreUsersViewModel vm)
{
await FillUsers(vm);
if (!ModelState.IsValid)
@ -164,16 +163,12 @@ namespace BTCPayServer.Controllers
ModelState.AddModelError(nameof(vm.Email), "User not found");
return View(vm);
}
var roles = await _Repo.GetStoreRoles(CurrentStore.Id);
if (roles.All(role => role.Id != vm.Role))
if (!StoreRoles.AllRoles.Contains(vm.Role))
{
ModelState.AddModelError(nameof(vm.Role), "Invalid role");
return View(vm);
}
var roleId = await _Repo.ResolveStoreRoleId(storeId, vm.Role);
if (!await _Repo.AddStoreUser(CurrentStore.Id, user.Id, roleId))
if (!await _Repo.AddStoreUser(CurrentStore.Id, user.Id, vm.Role))
{
ModelState.AddModelError(nameof(vm.Email), "The user already has access to this store");
return View(vm);
@ -616,7 +611,6 @@ namespace BTCPayServer.Controllers
Id = store.Id,
StoreName = store.StoreName,
StoreWebsite = store.StoreWebsite,
StoreSupportUrl = storeBlob.StoreSupportUrl,
LogoFileId = storeBlob.LogoFileId,
CssFileId = storeBlob.CssFileId,
BrandColor = storeBlob.BrandColor,
@ -652,7 +646,6 @@ namespace BTCPayServer.Controllers
}
var blob = CurrentStore.GetStoreBlob();
blob.StoreSupportUrl = model.StoreSupportUrl;
blob.AnyoneCanInvoice = model.AnyoneCanCreateInvoice;
blob.NetworkFeeMode = model.NetworkFeeMode;
blob.PaymentTolerance = model.PaymentTolerance;
@ -943,9 +936,8 @@ namespace BTCPayServer.Controllers
ViewBag.HidePublicKey = true;
ViewBag.ShowStores = true;
ViewBag.ShowMenu = false;
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));
var stores = await _Repo.GetStoresByUserId(userId);
model.Stores = new SelectList(stores.Where(s => s.Role == StoreRoles.Owner), 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";
@ -1010,14 +1002,14 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(UIHomeController.Index), "UIHome");
}
var stores = (await _Repo.GetStoresByUserId(userId)).Where(data => data.HasPermission(userId, Policies.CanModifyStoreSettings)).ToArray();
var stores = await _Repo.GetStoresByUserId(userId);
return View(new PairingModel
{
Id = pairing.Id,
Label = pairing.Label,
SIN = pairing.SIN ?? "Server-Initiated Pairing",
StoreId = selectedStore ?? stores.FirstOrDefault()?.Id,
Stores = stores.Select(s => new PairingModel.StoreViewModel
Stores = stores.Where(u => u.Role == StoreRoles.Owner).Select(s => new PairingModel.StoreViewModel
{
Id = s.Id,
Name = string.IsNullOrEmpty(s.StoreName) ? s.Id : s.StoreName

View File

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

View File

@ -318,8 +318,6 @@ 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);
}

View File

@ -189,7 +189,11 @@ 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;

View File

@ -74,7 +74,6 @@ 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 &&
@ -85,7 +84,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.ApplicationUserId == userId && store.StoreRole.Permissions.Contains(Policies.CanModifyStoreSettings));
.Any(store => store.Role == StoreRoles.Owner && store.ApplicationUserId == userId);
approvedStores.Add(payout.PullPaymentData.StoreId, value);
return value;
}).ToList();

View File

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

View File

@ -16,6 +16,32 @@ 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)

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
@ -30,30 +29,12 @@ 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
{
@ -75,17 +56,11 @@ namespace BTCPayServer.Data
}
public static WebhookDeliveryBlob GetBlob(this WebhookDeliveryData webhook)
{
if (webhook.Blob is null)
return null;
else
return JsonConvert.DeserializeObject<WebhookDeliveryBlob>(webhook.Blob, HostedServices.WebhookSender.DefaultSerializerSettings);
return webhook.HasTypedBlob<WebhookDeliveryBlob>().GetBlob(HostedServices.WebhookSender.DefaultSerializerSettings);
}
public static void SetBlob(this WebhookDeliveryData webhook, WebhookDeliveryBlob blob)
{
if (blob is null)
webhook.Blob = null;
else
webhook.Blob = JsonConvert.SerializeObject(blob, HostedServices.WebhookSender.DefaultSerializerSettings);
webhook.HasTypedBlob<WebhookDeliveryBlob>().SetBlob(blob, HostedServices.WebhookSender.DefaultSerializerSettings);
}
}
}

View File

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

View File

@ -13,7 +13,6 @@ 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;
@ -26,7 +25,6 @@ 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;
@ -117,14 +115,6 @@ 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));

View File

@ -1,40 +1,11 @@
#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

View File

@ -1,26 +0,0 @@
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;
}
}

View File

@ -12,7 +12,6 @@ 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>();

View File

@ -5,6 +5,27 @@ 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)

View File

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

View File

@ -1,23 +0,0 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using BTCPayServer.Abstractions.Form;
namespace BTCPayServer.Forms;
public class HtmlTextareaFormProvider : FormComponentProviderBase
{
public override void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider)
{
typeToComponentProvider.Add("textarea", this);
}
public override string View => "Forms/TextareaElement";
public override void Validate(Form form, Field field)
{
if (field.Required)
{
ValidateField<RequiredAttribute>(field);
}
}
}

View File

@ -77,6 +77,7 @@ public class UIFormsController : Controller
if (!_formDataService.IsFormSchemaValid(modifyForm.FormConfig, out var form, out var error))
{
ModelState.AddModelError(nameof(modifyForm.FormConfig),
$"Form config was invalid: {error})");
}
@ -85,6 +86,7 @@ public class UIFormsController : Controller
modifyForm.FormConfig = form.ToString();
}
if (!ModelState.IsValid)
{
return View(modifyForm);

View File

@ -40,11 +40,11 @@ namespace BTCPayServer.HostedServices
case PointOfSaleAppType.AppType:
var possettings = data.GetSettings<PointOfSaleSettings>();
return (Data: data, Settings: (object)possettings,
Items: AppService.Parse(possettings.Template));
Items: _appService.Parse(possettings.Template, possettings.Currency));
case CrowdfundAppType.AppType:
var cfsettings = data.GetSettings<CrowdfundSettings>();
return (Data: data, Settings: (object)cfsettings,
Items: AppService.Parse(cfsettings.PerksTemplate));
Items: _appService.Parse(cfsettings.PerksTemplate, cfsettings.TargetCurrency));
default:
return (null, null, null);
}
@ -70,11 +70,11 @@ namespace BTCPayServer.HostedServices
{
case PointOfSaleAppType.AppType:
((PointOfSaleSettings)valueTuple.Settings).Template =
AppService.SerializeTemplate(valueTuple.Items);
_appService.SerializeTemplate(valueTuple.Items);
break;
case CrowdfundAppType.AppType:
((CrowdfundSettings)valueTuple.Settings).PerksTemplate =
AppService.SerializeTemplate(valueTuple.Items);
_appService.SerializeTemplate(valueTuple.Items);
break;
default:
throw new InvalidOperationException();

View File

@ -1,61 +0,0 @@
using System;
using Dapper;
using System.Data.Common;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
namespace BTCPayServer.HostedServices
{
public class CleanupWebhookDeliveriesTask : IPeriodicTask
{
public CleanupWebhookDeliveriesTask(ApplicationDbContextFactory dbContextFactory)
{
DbContextFactory = dbContextFactory;
}
public ApplicationDbContextFactory DbContextFactory { get; }
public int BatchSize { get; set; } = 500;
public TimeSpan PruneAfter { get; set; } = TimeSpan.FromDays(60);
public async Task Do(CancellationToken cancellationToken)
{
await using var ctx = DbContextFactory.CreateContext();
if (!ctx.Database.IsNpgsql())
return;
var conn = ctx.Database.GetDbConnection();
bool pruned = false;
int offset = 0;
retry:
var rows = await conn.QueryAsync<WebhookDeliveryData>(@"
SELECT ""Id"", ""Blob""
FROM ""WebhookDeliveries""
WHERE ((now() - ""Timestamp"") > @PruneAfter) AND ""Pruned"" IS FALSE
ORDER BY ""Timestamp""
LIMIT @BatchSize OFFSET @offset
", new { PruneAfter, BatchSize, offset });
foreach (var d in rows)
{
var blob = d.GetBlob();
blob.Prune();
d.SetBlob(blob);
d.Pruned = true;
pruned = true;
}
if (pruned)
{
pruned = false;
await conn.ExecuteAsync("UPDATE \"WebhookDeliveries\" SET \"Blob\"=@Blob::JSONB, \"Pruned\"=@Pruned WHERE \"Id\"=@Id", rows);
if (rows.Count() == BatchSize)
{
offset += BatchSize;
goto retry;
}
}
}
}
}

View File

@ -1,10 +0,0 @@
using System.Threading.Tasks;
using System.Threading;
namespace BTCPayServer.HostedServices
{
public interface IPeriodicTask
{
Task Do(CancellationToken cancellationToken);
}
}

View File

@ -1,92 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
namespace BTCPayServer.HostedServices
{
public class PeriodicTaskLauncherHostedService : IHostedService
{
public PeriodicTaskLauncherHostedService(IServiceProvider serviceProvider, ILoggerFactory loggerFactory)
{
ServiceProvider = serviceProvider;
Logger = loggerFactory.CreateLogger("BTCPayServer.PeriodicTasks");
}
public IServiceProvider ServiceProvider { get; }
public ILogger Logger { get; }
Channel<ScheduledTask> jobs = Channel.CreateBounded<ScheduledTask>(100);
CancellationTokenSource cts;
public Task StartAsync(CancellationToken cancellationToken)
{
cts = new CancellationTokenSource();
foreach (var task in ServiceProvider.GetServices<ScheduledTask>())
jobs.Writer.TryWrite(task);
loop = Task.WhenAll(Enumerable.Range(0, 3).Select(_ => Loop(cts.Token)).ToArray());
return Task.CompletedTask;
}
Task loop;
private async Task Loop(CancellationToken token)
{
try
{
await foreach (var job in jobs.Reader.ReadAllAsync(token))
{
if (job.NextScheduled <= DateTimeOffset.UtcNow)
{
var t = (IPeriodicTask)ServiceProvider.GetService(job.PeriodicTaskType);
try
{
await t.Do(token);
}
catch when (token.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
Logger.LogError(ex, $"Unhandled error in periodic task {job.PeriodicTaskType.Name}");
}
finally
{
job.NextScheduled = DateTimeOffset.UtcNow + job.Every;
}
}
_ = Wait(job, token);
}
}
catch when (token.IsCancellationRequested)
{
}
}
private async Task Wait(ScheduledTask job, CancellationToken token)
{
var timeToWait = job.NextScheduled - DateTimeOffset.UtcNow;
try
{
await Task.Delay(timeToWait, token);
}
catch { }
while (await jobs.Writer.WaitToWriteAsync())
{
if (jobs.Writer.TryWrite(job))
break;
}
}
public async Task StopAsync(CancellationToken cancellationToken)
{
cts?.Cancel();
jobs.Writer.TryComplete();
if (loop is not null)
await loop;
}
}
}

View File

@ -1,16 +0,0 @@
using System;
namespace BTCPayServer.HostedServices
{
public class ScheduledTask
{
public ScheduledTask(Type periodicTypeTask, TimeSpan every)
{
PeriodicTaskType = periodicTypeTask;
Every = every;
}
public Type PeriodicTaskType { get; set; }
public TimeSpan Every { get; set; } = TimeSpan.FromMinutes(5.0);
public DateTimeOffset NextScheduled { get; set; } = DateTimeOffset.UtcNow;
}
}

View File

@ -105,8 +105,6 @@ namespace BTCPayServer.HostedServices
var newDeliveryBlob = new WebhookDeliveryBlob();
newDeliveryBlob.Request = oldDeliveryBlob.Request;
var webhookEvent = newDeliveryBlob.ReadRequestAs<WebhookEvent>();
if (webhookEvent.IsPruned())
return null;
webhookEvent.DeliveryId = newDelivery.Id;
webhookEvent.WebhookId = webhookDelivery.Webhook.Id;
// if we redelivered a redelivery, we still want the initial delivery here

View File

@ -350,9 +350,6 @@ namespace BTCPayServer.Hosting
services.AddSingleton<HostedServices.WebhookSender>();
services.AddSingleton<IHostedService, WebhookSender>(o => o.GetRequiredService<WebhookSender>());
services.AddSingleton<IHostedService, StoreEmailRuleProcessorSender>();
services.AddSingleton<IHostedService, PeriodicTaskLauncherHostedService>();
services.AddScheduledTask<CleanupWebhookDeliveriesTask>(TimeSpan.FromHours(6.0));
services.AddHttpClient(WebhookSender.OnionNamedClient)
.ConfigurePrimaryHttpMessageHandler<Socks5HttpClientHandler>();
services.AddHttpClient(WebhookSender.LoopbackNamedClient)

View File

@ -1,17 +1,16 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.Fido2;
using BTCPayServer.Fido2.Models;
using BTCPayServer.Logging;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Plugins.Crowdfund;
@ -27,19 +26,17 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NBitcoin;
using NBXplorer;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PeterO.Cbor;
using YamlDotNet.RepresentationModel;
using YamlDotNet.Serialization;
using LightningAddressData = BTCPayServer.Data.LightningAddressData;
using Serializer = NBXplorer.Serializer;
namespace BTCPayServer.Hosting
{
public class MigrationStartupTask : IStartupTask
{
public Logs Logs { get; }
private readonly ApplicationDbContextFactory _DBContextFactory;
private readonly StoreRepository _StoreRepository;
@ -249,11 +246,6 @@ namespace BTCPayServer.Hosting
{
await FixMappedDomainAppType();
settings.FixMappedDomainAppType = true;
}
if (!settings.MigrateAppYmlToJson)
{
await MigrateAppYmlToJson();
settings.MigrateAppYmlToJson = true;
await _Settings.UpdateSetting(settings);
}
}
@ -312,134 +304,6 @@ namespace BTCPayServer.Hosting
return;
await ToPostgresMigrationStartupTask.UpdateSequenceInvoiceSearch(ctx);
}
private async Task MigrateAppYmlToJson()
{
await using var ctx = _DBContextFactory.CreateContext();
var apps = await ctx.Apps.Where(data => CrowdfundAppType.AppType == data.AppType || PointOfSaleAppType.AppType == data.AppType)
.ToListAsync();
foreach (var app in apps)
{
switch (app.AppType)
{
case CrowdfundAppType.AppType :
var cfSettings = app.GetSettings<CrowdfundSettings>();
if (!string.IsNullOrEmpty(cfSettings?.PerksTemplate))
{
cfSettings.PerksTemplate = AppService.SerializeTemplate(ParsePOSYML(cfSettings?.PerksTemplate));
app.SetSettings(cfSettings);
}
break;
case PointOfSaleAppType.AppType:
var pSettings = app.GetSettings<PointOfSaleSettings>();
if (!string.IsNullOrEmpty(pSettings?.Template))
{
pSettings.Template = AppService.SerializeTemplate(ParsePOSYML(pSettings?.Template));
app.SetSettings(pSettings);
}
break;
}
}
await ctx.SaveChangesAsync();
}
public static ViewPointOfSaleViewModel.Item[] ParsePOSYML(string yaml)
{
var items = new List<ViewPointOfSaleViewModel.Item>();
var stream = new YamlStream();
stream.Load(new StringReader(yaml));
var root = stream.Documents.First().RootNode as YamlMappingNode;
foreach (var posItem in root.Children)
{
var trimmedKey = ((YamlScalarNode)posItem.Key).Value?.Trim();
if (string.IsNullOrEmpty(trimmedKey))
{
continue;
}
var currentItem = new ViewPointOfSaleViewModel.Item
{
Id = trimmedKey, Title = trimmedKey, PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed
};
var itemSpecs = (YamlMappingNode)posItem.Value;
foreach (var spec in itemSpecs)
{
if (spec.Key is not YamlScalarNode {Value: string keyString} || string.IsNullOrEmpty(keyString))
continue;
var scalarValue = spec.Value as YamlScalarNode;
switch (keyString)
{
case "title":
currentItem.Title = scalarValue?.Value ?? trimmedKey;
break;
case "inventory":
if (int.TryParse(scalarValue?.Value, out var inv))
{
currentItem.Inventory = inv;
}
break;
case "description":
currentItem.Description = scalarValue?.Value;
break;
case "image":
currentItem.Image = scalarValue?.Value;
break;
case "payment_methods" when spec.Value is YamlSequenceNode pmSequenceNode:
currentItem.PaymentMethods = pmSequenceNode.Children
.Select(node => (node as YamlScalarNode)?.Value?.Trim())
.Where(node => !string.IsNullOrEmpty(node)).ToArray();
break;
case "price_type":
case "custom":
if (bool.TryParse(scalarValue?.Value, out var customBoolValue))
{
if (customBoolValue)
{
currentItem.PriceType = currentItem.Price is null or 0
? ViewPointOfSaleViewModel.ItemPriceType.Topup
: ViewPointOfSaleViewModel.ItemPriceType.Minimum;
}
else
{
currentItem.PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed;
}
}
else if (Enum.TryParse<ViewPointOfSaleViewModel.ItemPriceType>(scalarValue?.Value, true,
out var customPriceType))
{
currentItem.PriceType = customPriceType;
}
break;
case "price":
if (decimal.TryParse(scalarValue?.Value, out var price))
{
currentItem.Price = price;
}
break;
case "buybuttontext":
currentItem.BuyButtonText = scalarValue?.Value;
break;
case "disabled":
if (bool.TryParse(scalarValue?.Value, out var disabled))
{
currentItem.Disabled = disabled;
}
break;
}
}
items.Add(currentItem);
}
return items.ToArray();
}
#pragma warning disable CS0612 // Type or member is obsolete
@ -657,8 +521,8 @@ WHERE cte.""Id""=p.""Id""
settings1.TargetCurrency = app.StoreData.GetStoreBlob().DefaultCurrency;
app.SetSettings(settings1);
}
items = AppService.Parse(settings1.PerksTemplate);
newTemplate = AppService.SerializeTemplate(items);
items = _appService.Parse(settings1.PerksTemplate, settings1.TargetCurrency);
newTemplate = _appService.SerializeTemplate(items);
if (settings1.PerksTemplate != newTemplate)
{
settings1.PerksTemplate = newTemplate;
@ -674,8 +538,8 @@ WHERE cte.""Id""=p.""Id""
settings2.Currency = app.StoreData.GetStoreBlob().DefaultCurrency;
app.SetSettings(settings2);
}
items = AppService.Parse(settings2.Template);
newTemplate = AppService.SerializeTemplate(items);
items = _appService.Parse(settings2.Template, settings2.Currency);
newTemplate = _appService.SerializeTemplate(items);
if (settings2.Template != newTemplate)
{
settings2.Template = newTemplate;

View File

@ -213,9 +213,6 @@ namespace BTCPayServer.Hosting
{
var typeMapping = t.EntityTypeMappings.Single();
var query = (IQueryable<object>)otherContext.GetType().GetMethod("Set", new Type[0])!.MakeGenericMethod(typeMapping.EntityType.ClrType).Invoke(otherContext, null)!;
if (t.Name == "WebhookDeliveries" ||
t.Name == "InvoiceWebhookDeliveries")
continue;
Logger.LogInformation($"Migrating table: " + t.Name);
List<PropertyInfo> datetimeProperties = new List<PropertyInfo>();
foreach (var col in t.Columns)

View File

@ -1,6 +1,5 @@
using System;
using BTCPayServer.Data;
using BTCPayServer.Services.Stores;
namespace BTCPayServer.Models.AppViewModels
@ -15,12 +14,12 @@ namespace BTCPayServer.Models.AppViewModels
public string AppName { get; set; }
public string AppType { get; set; }
public string ViewStyle { get; set; }
public bool IsOwner { get; set; }
public string UpdateAction { get { return "Update" + AppType; } }
public string ViewAction { get { return "View" + AppType; } }
public DateTimeOffset Created { get; set; }
public AppData App { get; set; }
public StoreRepository.StoreRole Role { get; set; }
}
public ListAppViewModel[] Apps { get; set; }

View File

@ -1,18 +1,17 @@
using System;
using System.Collections.Generic;
using BTCPayServer.Client.Models;
using BTCPayServer.Services.Invoices;
namespace BTCPayServer.Models.InvoicingModels
{
public class InvoicesModel : BasePagingViewModel
{
public List<InvoiceModel> Invoices { get; set; } = new ();
public List<InvoiceModel> Invoices { get; set; } = new List<InvoiceModel>();
public override int CurrentPageCount => Invoices.Count;
public string[] StoreIds { get; set; }
public string StoreId { get; set; }
public string SearchText { get; set; }
public SearchString Search { get; set; }
public List<InvoiceAppModel> Apps { get; set; }
public bool IncludeArchived { get; set; }
}
public class InvoiceModel
@ -35,12 +34,4 @@ namespace BTCPayServer.Models.InvoicingModels
public InvoiceDetailsModel Details { get; set; }
public bool HasRefund { get; set; }
}
public class InvoiceAppModel
{
public string Id { get; set; }
public string AppName { get; set; }
public string AppType { get; set; }
public string AppOrderId { get; set; }
}
}

View File

@ -61,7 +61,7 @@ namespace BTCPayServer.Models.InvoicingModels
public int TxCount { get; set; }
public int TxCountForFee { get; set; }
public string BtcPaid { get; set; }
public string StoreSupportUrl { get; set; }
public string StoreEmail { get; set; }
public string OrderId { get; set; }
public decimal NetworkFee { get; set; }

View File

@ -24,16 +24,9 @@ namespace BTCPayServer.Models.InvoicingModels
public string RateThenText { get; set; }
public string FiatText { get; set; }
public decimal FiatAmount { get; set; }
public decimal? OverpaidAmount { get; set; }
public string OverpaidAmountText { get; set; }
public decimal SubtractPercentage { get; set; }
[Display(Name = "Specify the amount and currency for the refund")]
public decimal CustomAmount { get; set; }
public string CustomCurrency { get; set; }
public string InvoiceCurrency { get; set; }
public string CryptoCode { get; set; }
public int CryptoDivisibility { get; set; }
public int InvoiceDivisibility { get; set; }
}
}

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using BTCPayServer.Services.Stores;
namespace BTCPayServer.Models.ServerViewModels
{
@ -21,11 +20,5 @@ namespace BTCPayServer.Models.ServerViewModels
public override int CurrentPageCount => Users.Count;
public Dictionary<string, string> Roles { get; set; }
}
public class RolesViewModel : BasePagingViewModel
{
public List<StoreRepository.StoreRole> Roles { get; set; } = new List<StoreRepository.StoreRole>();
public string DefaultRole { get; set; }
public override int CurrentPageCount => Roles.Count;
}
}

View File

@ -5,8 +5,6 @@ namespace BTCPayServer.Models.StoreViewModels
{
public class CreateStoreViewModel
{
public bool IsFirstStore { get; set; }
[Required]
[MaxLength(50)]
[MinLength(1)]

View File

@ -22,16 +22,13 @@ namespace BTCPayServer.Models.StoreViewModels
Success = blob.Status == WebhookDeliveryStatus.HttpSuccess;
ErrorMessage = blob.ErrorMessage ?? "Success";
Time = s.Timestamp;
var evt = blob.ReadRequestAs<WebhookEvent>();
Type = evt.Type;
Pruned = evt.IsPruned();
Type = blob.ReadRequestAs<WebhookEvent>().Type;
WebhookId = s.Id;
PayloadUrl = s.Webhook?.GetBlob().Url;
}
public string Id { get; set; }
public DateTimeOffset Time { get; set; }
public WebhookEventType Type { get; private set; }
public bool Pruned { get; set; }
public string WebhookId { get; set; }
public bool Success { get; set; }
public string ErrorMessage { get; set; }

View File

@ -22,10 +22,6 @@ namespace BTCPayServer.Models.StoreViewModels
[MaxLength(500)]
public string StoreWebsite { get; set; }
[Display(Name = "Support URL")]
[MaxLength(500)]
public string StoreSupportUrl { get; set; }
[Display(Name = "Brand Color")]
public string BrandColor { get; set; }

View File

@ -11,6 +11,10 @@ namespace BTCPayServer.Models.StoreViewModels
public string Role { get; set; }
public string Id { get; set; }
}
public StoreUsersViewModel()
{
Role = StoreRoles.Guest;
}
[Required]
[EmailAddress]
public string Email { get; set; }

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