Compare commits
30 Commits
v2.0.0-alp
...
v2.0.0-bet
Author | SHA1 | Date | |
---|---|---|---|
|
413a9b4269 | ||
|
a698aa8a5b | ||
|
0f79526566 | ||
|
1ffbab7338 | ||
|
3a71c45a89 | ||
|
8f062f918b | ||
|
2f05d00219 | ||
|
b48ca92675 | ||
|
4a31cf0a09 | ||
|
82620ee327 | ||
|
6d284b4124 | ||
|
83fa8cbf0f | ||
|
9ba4b030ed | ||
|
272cc3d3c9 | ||
|
b5590a38fe | ||
|
443a350bad | ||
|
7013e618de | ||
|
363b60385b | ||
|
90635ffc4e | ||
|
056f850268 | ||
|
336f2d88e9 | ||
|
e16b4062b5 | ||
|
747dacf3b1 | ||
|
f00a71922f | ||
|
c97c9d4ece | ||
|
9d3f8672d9 | ||
|
fe48cd4236 | ||
|
587d3aa612 | ||
|
8a951940fd | ||
|
b726ef8a2e |
@@ -2,6 +2,7 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
|
||||
namespace BTCPayServer.Abstractions.Contracts;
|
||||
|
||||
@@ -14,4 +15,5 @@ public interface IFileService
|
||||
Task<string?> GetTemporaryFileUrl(Uri baseUri, string fileId, DateTimeOffset expiry,
|
||||
bool isDownload);
|
||||
Task RemoveFile(string fileId, string userId);
|
||||
Task<UploadImageResultModel> UploadImage(IFormFile file, string userId, long maxFileSizeInBytes = 1_000_000);
|
||||
}
|
||||
|
@@ -55,7 +55,7 @@ namespace BTCPayServer.Abstractions.Extensions
|
||||
viewData[ACTIVE_CATEGORY_KEY] = activeCategory;
|
||||
}
|
||||
|
||||
public static bool IsActiveCategory(this ViewDataDictionary viewData, string category, object id = null)
|
||||
public static bool IsCategoryActive(this ViewDataDictionary viewData, string category, object id = null)
|
||||
{
|
||||
if (!viewData.ContainsKey(ACTIVE_CATEGORY_KEY)) return false;
|
||||
var activeId = viewData[ACTIVE_ID_KEY];
|
||||
@@ -65,12 +65,12 @@ namespace BTCPayServer.Abstractions.Extensions
|
||||
return categoryMatch && idMatch;
|
||||
}
|
||||
|
||||
public static bool IsActiveCategory<T>(this ViewDataDictionary viewData, T category, object id = null)
|
||||
public static bool IsCategoryActive<T>(this ViewDataDictionary viewData, T category, object id = null)
|
||||
{
|
||||
return IsActiveCategory(viewData, category.ToString(), id);
|
||||
return IsCategoryActive(viewData, category.ToString(), id);
|
||||
}
|
||||
|
||||
public static bool IsActivePage(this ViewDataDictionary viewData, string page, string category, object id = null)
|
||||
public static bool IsPageActive(this ViewDataDictionary viewData, string page, string category, object id = null)
|
||||
{
|
||||
if (!viewData.ContainsKey(ACTIVE_PAGE_KEY)) return false;
|
||||
var activeId = viewData[ACTIVE_ID_KEY];
|
||||
@@ -82,7 +82,7 @@ namespace BTCPayServer.Abstractions.Extensions
|
||||
return categoryAndPageMatch && idMatch;
|
||||
}
|
||||
|
||||
public static bool IsActivePage<T>(this ViewDataDictionary viewData, IEnumerable<T> pages, object id = null)
|
||||
public static bool IsPageActive<T>(this ViewDataDictionary viewData, IEnumerable<T> pages, object id = null)
|
||||
where T : IConvertible
|
||||
{
|
||||
return pages.Any(page => ActivePageClass(viewData, page.ToString(), page.GetType().ToString(), id) == ACTIVE_CLASS);
|
||||
@@ -95,7 +95,7 @@ namespace BTCPayServer.Abstractions.Extensions
|
||||
|
||||
public static string ActiveCategoryClass(this ViewDataDictionary viewData, string category, object id = null)
|
||||
{
|
||||
return IsActiveCategory(viewData, category, id) ? ACTIVE_CLASS : null;
|
||||
return IsCategoryActive(viewData, category, id) ? ACTIVE_CLASS : null;
|
||||
}
|
||||
|
||||
public static string ActivePageClass<T>(this ViewDataDictionary viewData, T page, object id = null)
|
||||
@@ -106,12 +106,42 @@ namespace BTCPayServer.Abstractions.Extensions
|
||||
|
||||
public static string ActivePageClass(this ViewDataDictionary viewData, string page, string category, object id = null)
|
||||
{
|
||||
return IsActivePage(viewData, page, category, id) ? ACTIVE_CLASS : null;
|
||||
return IsPageActive(viewData, page, category, id) ? ACTIVE_CLASS : null;
|
||||
}
|
||||
|
||||
|
||||
public static string ActivePageClass<T>(this ViewDataDictionary viewData, IEnumerable<T> pages, object id = null) where T : IConvertible
|
||||
{
|
||||
return IsActivePage(viewData, pages, id) ? ACTIVE_CLASS : null;
|
||||
return IsPageActive(viewData, pages, id) ? ACTIVE_CLASS : null;
|
||||
}
|
||||
|
||||
[Obsolete("Use ActiveCategoryClass instead")]
|
||||
public static string IsActiveCategory<T>(this ViewDataDictionary viewData, T category, object id = null)
|
||||
{
|
||||
return ActiveCategoryClass(viewData, category, id);
|
||||
}
|
||||
|
||||
[Obsolete("Use ActiveCategoryClass instead")]
|
||||
public static string IsActiveCategory(this ViewDataDictionary viewData, string category, object id = null)
|
||||
{
|
||||
return ActiveCategoryClass(viewData, category, id);
|
||||
}
|
||||
|
||||
[Obsolete("Use ActivePageClass instead")]
|
||||
public static string IsActivePage<T>(this ViewDataDictionary viewData, T page, object id = null) where T : IConvertible
|
||||
{
|
||||
return ActivePageClass(viewData, page, id);
|
||||
}
|
||||
|
||||
[Obsolete("Use ActivePageClass instead")]
|
||||
public static string IsActivePage<T>(this ViewDataDictionary viewData, IEnumerable<T> pages, object id = null) where T : IConvertible
|
||||
{
|
||||
return ActivePageClass(viewData, pages, id);
|
||||
}
|
||||
|
||||
[Obsolete("Use ActivePageClass instead")]
|
||||
public static string IsActivePage(this ViewDataDictionary viewData, string page, string category, object id = null)
|
||||
{
|
||||
return ActivePageClass(viewData, page, category, id);
|
||||
}
|
||||
|
||||
public static HtmlString ToBrowserDate(this DateTimeOffset date, string netFormat, string jsDateFormat = "short", string jsTimeFormat = "short")
|
||||
|
15
BTCPayServer.Abstractions/Models/UploadImageResultModel.cs
Normal file
15
BTCPayServer.Abstractions/Models/UploadImageResultModel.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
|
||||
namespace BTCPayServer.Abstractions.Models;
|
||||
|
||||
public class UploadImageResultModel
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string Response { get; set; } = string.Empty;
|
||||
public IStoredFile? StoredFile { get; set; }
|
||||
}
|
@@ -46,9 +46,15 @@ public partial class BTCPayServerClient
|
||||
return await SendHttpRequest<InvoiceData>($"api/v1/stores/{storeId}/invoices/{invoiceId}", null, HttpMethod.Get, token);
|
||||
}
|
||||
public virtual async Task<InvoicePaymentMethodDataModel[]> GetInvoicePaymentMethods(string storeId, string invoiceId,
|
||||
bool onlyAccountedPayments = true, bool includeSensitive = false,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return await SendHttpRequest<InvoicePaymentMethodDataModel[]>($"api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods", null, HttpMethod.Get, token);
|
||||
var queryPayload = new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(onlyAccountedPayments), onlyAccountedPayments },
|
||||
{ nameof(includeSensitive), includeSensitive }
|
||||
};
|
||||
return await SendHttpRequest<InvoicePaymentMethodDataModel[]>($"api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods", queryPayload, HttpMethod.Get, token);
|
||||
}
|
||||
|
||||
public virtual async Task ArchiveInvoice(string storeId, string invoiceId,
|
||||
|
@@ -19,7 +19,7 @@ namespace BTCPayServer.Client.Models
|
||||
public DateTimeOffset? ExpiresAt { get; set; }
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset? StartsAt { get; set; }
|
||||
public string[] PaymentMethods { get; set; }
|
||||
public string[] PayoutMethods { get; set; }
|
||||
public bool AutoApproveClaims { get; set; }
|
||||
}
|
||||
}
|
||||
|
@@ -22,11 +22,12 @@ namespace BTCPayServer.Client.Models
|
||||
public string PullPaymentId { get; set; }
|
||||
public string Destination { get; set; }
|
||||
public string PayoutMethodId { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal Amount { get; set; }
|
||||
public decimal OriginalAmount { get; set; }
|
||||
public string OriginalCurrency { get; set; }
|
||||
public string PayoutCurrency { get; set; }
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal? PaymentMethodAmount { get; set; }
|
||||
public decimal? PayoutAmount { get; set; }
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public PayoutState State { get; set; }
|
||||
public int Revision { get; set; }
|
||||
|
@@ -4,6 +4,6 @@ namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string FriendlyName { get; set; }
|
||||
public string[] PaymentMethods { get; set; }
|
||||
public string[] PayoutMethods { get; set; }
|
||||
}
|
||||
}
|
||||
|
@@ -22,7 +22,5 @@
|
||||
<None Remove="DBScripts\002.RefactorPayouts.sql" />
|
||||
<None Remove="DBScripts\003.RefactorPendingInvoicesPayments.sql" />
|
||||
<None Remove="DBScripts\004.MonitoredInvoices.sql" />
|
||||
<None Remove="DBScripts\005.PaymentsRenaming.sql" />
|
||||
<None Remove="DBScripts\006.PaymentsRenaming.sql" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@@ -4,19 +4,20 @@ RETURNS JSONB AS $$
|
||||
$$ LANGUAGE sql IMMUTABLE;
|
||||
|
||||
|
||||
CREATE OR REPLACE FUNCTION get_monitored_invoices(payment_method_id TEXT)
|
||||
RETURNS TABLE (invoice_id TEXT, payment_id TEXT) AS $$
|
||||
CREATE OR REPLACE FUNCTION get_monitored_invoices(arg_payment_method_id TEXT, include_non_activated BOOLEAN)
|
||||
RETURNS TABLE (invoice_id TEXT, payment_id TEXT, payment_method_id TEXT) AS $$
|
||||
WITH cte AS (
|
||||
-- Get all the invoices which are pending. Even if no payments.
|
||||
SELECT i."Id" invoice_id, p."Id" payment_id FROM "Invoices" i LEFT JOIN "Payments" p ON i."Id" = p."InvoiceDataId"
|
||||
SELECT i."Id" invoice_id, p."Id" payment_id, p."PaymentMethodId" payment_method_id FROM "Invoices" i LEFT JOIN "Payments" p ON i."Id" = p."InvoiceDataId"
|
||||
WHERE is_pending(i."Status")
|
||||
UNION ALL
|
||||
-- For invoices not pending, take all of those which have pending payments
|
||||
SELECT i."Id", p."Id" FROM "Invoices" i INNER JOIN "Payments" p ON i."Id" = p."InvoiceDataId"
|
||||
SELECT i."Id" invoice_id, p."Id" payment_id, p."PaymentMethodId" payment_method_id FROM "Invoices" i INNER JOIN "Payments" p ON i."Id" = p."InvoiceDataId"
|
||||
WHERE is_pending(p."Status") AND NOT is_pending(i."Status"))
|
||||
SELECT cte.* FROM cte
|
||||
LEFT JOIN "Payments" p ON cte.payment_id=p."Id"
|
||||
LEFT JOIN "Invoices" i ON cte.invoice_id=i."Id"
|
||||
WHERE (p."Type" IS NOT NULL AND p."Type" = payment_method_id) OR
|
||||
(p."Type" IS NULL AND get_prompt(i."Blob2", payment_method_id) IS NOT NULL AND (get_prompt(i."Blob2", payment_method_id)->'activated')::BOOLEAN IS NOT FALSE);
|
||||
JOIN "Invoices" i ON cte.invoice_id=i."Id"
|
||||
LEFT JOIN "Payments" p ON cte.payment_id=p."Id" AND cte.payment_method_id=p."PaymentMethodId"
|
||||
WHERE (p."PaymentMethodId" IS NOT NULL AND p."PaymentMethodId" = arg_payment_method_id) OR
|
||||
(p."PaymentMethodId" IS NULL AND get_prompt(i."Blob2", arg_payment_method_id) IS NOT NULL AND
|
||||
(include_non_activated IS TRUE OR (get_prompt(i."Blob2", arg_payment_method_id)->'inactive')::BOOLEAN IS NOT TRUE));
|
||||
$$ LANGUAGE SQL STABLE;
|
||||
|
@@ -1,17 +0,0 @@
|
||||
DROP FUNCTION get_monitored_invoices;
|
||||
CREATE OR REPLACE FUNCTION get_monitored_invoices(payment_method_id TEXT)
|
||||
RETURNS TABLE (invoice_id TEXT, payment_id TEXT, payment_method_id TEXT) AS $$
|
||||
WITH cte AS (
|
||||
-- Get all the invoices which are pending. Even if no payments.
|
||||
SELECT i."Id" invoice_id, p."Id" payment_id, p."PaymentMethodId" payment_method_id FROM "Invoices" i LEFT JOIN "Payments" p ON i."Id" = p."InvoiceDataId"
|
||||
WHERE is_pending(i."Status")
|
||||
UNION ALL
|
||||
-- For invoices not pending, take all of those which have pending payments
|
||||
SELECT i."Id", p."Id", p."PaymentMethodId" payment_method_id FROM "Invoices" i INNER JOIN "Payments" p ON i."Id" = p."InvoiceDataId"
|
||||
WHERE is_pending(p."Status") AND NOT is_pending(i."Status"))
|
||||
SELECT cte.* FROM cte
|
||||
LEFT JOIN "Payments" p ON cte.payment_id=p."Id" AND cte.payment_id=p."PaymentMethodId"
|
||||
LEFT JOIN "Invoices" i ON cte.invoice_id=i."Id"
|
||||
WHERE (p."PaymentMethodId" IS NOT NULL AND p."PaymentMethodId" = payment_method_id) OR
|
||||
(p."PaymentMethodId" IS NULL AND get_prompt(i."Blob2", payment_method_id) IS NOT NULL AND (get_prompt(i."Blob2", payment_method_id)->'activated')::BOOLEAN IS NOT FALSE);
|
||||
$$ LANGUAGE SQL STABLE;
|
@@ -1,18 +0,0 @@
|
||||
DROP FUNCTION get_monitored_invoices;
|
||||
CREATE OR REPLACE FUNCTION get_monitored_invoices(payment_method_id TEXT, include_non_activated BOOLEAN)
|
||||
RETURNS TABLE (invoice_id TEXT, payment_id TEXT, payment_method_id TEXT) AS $$
|
||||
WITH cte AS (
|
||||
-- Get all the invoices which are pending. Even if no payments.
|
||||
SELECT i."Id" invoice_id, p."Id" payment_id, p."PaymentMethodId" payment_method_id FROM "Invoices" i LEFT JOIN "Payments" p ON i."Id" = p."InvoiceDataId"
|
||||
WHERE is_pending(i."Status")
|
||||
UNION ALL
|
||||
-- For invoices not pending, take all of those which have pending payments
|
||||
SELECT i."Id", p."Id", p."PaymentMethodId" payment_method_id FROM "Invoices" i INNER JOIN "Payments" p ON i."Id" = p."InvoiceDataId"
|
||||
WHERE is_pending(p."Status") AND NOT is_pending(i."Status"))
|
||||
SELECT cte.* FROM cte
|
||||
LEFT JOIN "Payments" p ON cte.payment_id=p."Id" AND cte.payment_id=p."PaymentMethodId"
|
||||
LEFT JOIN "Invoices" i ON cte.invoice_id=i."Id"
|
||||
WHERE (p."PaymentMethodId" IS NOT NULL AND p."PaymentMethodId" = payment_method_id) OR
|
||||
(p."PaymentMethodId" IS NULL AND get_prompt(i."Blob2", payment_method_id) IS NOT NULL AND
|
||||
(include_non_activated IS TRUE OR (get_prompt(i."Blob2", payment_method_id)->'activated')::BOOLEAN IS NOT FALSE));
|
||||
$$ LANGUAGE SQL STABLE;
|
39
BTCPayServer.Data/Data/PaymentRequestData.Migration.cs
Normal file
39
BTCPayServer.Data/Data/PaymentRequestData.Migration.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Linq;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public partial class PaymentRequestData : MigrationInterceptor.IHasMigration
|
||||
{
|
||||
[NotMapped]
|
||||
public bool Migrated { get; set; }
|
||||
|
||||
public bool TryMigrate()
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
if (Blob is null && Blob2 is not null)
|
||||
return false;
|
||||
if (Blob2 is null)
|
||||
{
|
||||
Blob2 = Blob is not (null or { Length: 0 }) ? MigrationExtensions.Unzip(Blob) : "{}";
|
||||
Blob2 = MigrationExtensions.SanitizeJSON(Blob2);
|
||||
}
|
||||
Blob = null;
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
var jobj = JObject.Parse(Blob2);
|
||||
// Fixup some legacy payment requests
|
||||
if (jobj["expiryDate"].Type == JTokenType.Date)
|
||||
{
|
||||
jobj["expiryDate"] = new JValue(NBitcoin.Utils.DateTimeToUnixTime(jobj["expiryDate"].Value<DateTime>()));
|
||||
Blob2 = jobj.ToString(Newtonsoft.Json.Formatting.None);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@@ -4,7 +4,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class PaymentRequestData : IHasBlobUntyped
|
||||
public partial class PaymentRequestData : IHasBlobUntyped
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public DateTimeOffset Created { get; set; }
|
||||
|
@@ -11,5 +11,30 @@ namespace BTCPayServer.Migrations
|
||||
[DBScript("002.RefactorPayouts.sql")]
|
||||
public partial class migratepayouts : DBScriptsMigration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
base.Up(migrationBuilder);
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "Destination",
|
||||
table: "Payouts",
|
||||
newName: "DedupId");
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_Payouts_Destination_State",
|
||||
table: "Payouts",
|
||||
newName: "IX_Payouts_DedupId_State");
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "PaymentMethod",
|
||||
table: "PayoutProcessors",
|
||||
newName: "PayoutMethodId");
|
||||
|
||||
migrationBuilder.Sql("""
|
||||
UPDATE "PayoutProcessors"
|
||||
SET
|
||||
"PayoutMethodId" = CASE WHEN STRPOS("PayoutMethodId", '_') = 0 THEN "PayoutMethodId" || '-CHAIN'
|
||||
WHEN STRPOS("PayoutMethodId", '_LightningLike') > 0 THEN split_part("PayoutMethodId", '_LightningLike', 1) || '-LN'
|
||||
WHEN STRPOS("PayoutMethodId", '_LNURLPAY') > 0 THEN split_part("PayoutMethodId",'_LNURLPAY', 1) || '-LN'
|
||||
ELSE "PayoutMethodId" END
|
||||
""");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,36 +0,0 @@
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20240906010127_renamecol")]
|
||||
public partial class renamecol : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "Destination",
|
||||
table: "Payouts",
|
||||
newName: "DedupId");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_Payouts_Destination_State",
|
||||
table: "Payouts",
|
||||
newName: "IX_Payouts_DedupId_State");
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "PaymentMethod",
|
||||
table: "PayoutProcessors",
|
||||
newName: "PayoutMethodId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@@ -9,7 +9,6 @@ namespace BTCPayServer.Migrations
|
||||
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20240923065254_refactorpayments")]
|
||||
[DBScript("005.PaymentsRenaming.sql")]
|
||||
public partial class refactorpayments : DBScriptsMigration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
@@ -1,31 +0,0 @@
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20240923071444_temprefactor2")]
|
||||
public partial class temprefactor2 : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_AddressInvoices",
|
||||
table: "AddressInvoices");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_AddressInvoices",
|
||||
table: "AddressInvoices",
|
||||
columns: new[] { "Address", "PaymentMethodId" });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@@ -7,7 +7,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20240919034505_monitoredinvoices")]
|
||||
[Migration("20240924065254_monitoredinvoices")]
|
||||
[DBScript("004.MonitoredInvoices.sql")]
|
||||
public partial class monitoredinvoices : DBScriptsMigration
|
||||
{
|
@@ -1,15 +0,0 @@
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20240924071444_temprefactor3")]
|
||||
[DBScript("006.PaymentsRenaming.sql")]
|
||||
public partial class temprefactor3 : DBScriptsMigration
|
||||
{
|
||||
}
|
||||
}
|
@@ -7,6 +7,15 @@
|
||||
<!--https://devblogs.microsoft.com/aspnet/testing-asp-net-core-mvc-web-apps-in-memory/-->
|
||||
<PreserveCompilationContext>true</PreserveCompilationContext>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="BTCPayServer.Tests.OutputPathAttribute">
|
||||
<!-- _Parameter1, _Parameter2, etc. correspond to the
|
||||
matching parameter of a constructor of that .NET attribute type -->
|
||||
<_Parameter1>$([System.IO.Path]::Combine('$(MSBuildProjectDirectory)', '$(OutputPath)'))</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
<!--https://devblogs.microsoft.com/aspnet/testing-asp-net-core-mvc-web-apps-in-memory/-->
|
||||
<Target Name="CopyAditionalFiles" AfterTargets="Build" Condition="'$(TargetFramework)'!=''">
|
||||
<ItemGroup>
|
||||
@@ -61,4 +70,7 @@
|
||||
<ProjectReference Include="..\BTCPayServer\BTCPayServer.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="obj\Debug\net8.0\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@@ -162,6 +162,8 @@ namespace BTCPayServer.Tests
|
||||
HttpClient.BaseAddress = ServerUri;
|
||||
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development");
|
||||
var confBuilder = new DefaultConfiguration() { Logger = LoggerProvider.CreateLogger("Console") }.CreateConfigurationBuilder(new[] { "--datadir", _Directory, "--conf", confPath, "--disable-registration", DisableRegistration ? "true" : "false" });
|
||||
// This make sure that tests work outside of this assembly (ie, test project it a plugin)
|
||||
confBuilder.SetBasePath(TestUtils.TestDirectory);
|
||||
#if DEBUG
|
||||
confBuilder.AddJsonFile("appsettings.dev.json", true, false);
|
||||
#endif
|
||||
@@ -265,7 +267,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
private string FindBTCPayServerDirectory()
|
||||
{
|
||||
var solutionDirectory = TestUtils.TryGetSolutionDirectoryInfo(Directory.GetCurrentDirectory());
|
||||
var solutionDirectory = TestUtils.TryGetSolutionDirectoryInfo();
|
||||
return Path.Combine(solutionDirectory.FullName, "BTCPayServer");
|
||||
}
|
||||
|
||||
|
@@ -39,6 +39,8 @@ namespace BTCPayServer.Tests
|
||||
await user.GrantAccessAsync();
|
||||
var user2 = tester.NewAccount();
|
||||
await user2.GrantAccessAsync();
|
||||
await user.RegisterDerivationSchemeAsync("BTC");
|
||||
await user2.RegisterDerivationSchemeAsync("BTC");
|
||||
var apps = user.GetController<UIAppsController>();
|
||||
var apps2 = user2.GetController<UIAppsController>();
|
||||
var crowdfund = user.GetController<UICrowdfundController>();
|
||||
@@ -79,7 +81,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.False(app.Archived);
|
||||
var crowdfundViewModel = await crowdfund.UpdateCrowdfund(app.Id).AssertViewModelAsync<UpdateCrowdfundViewModel>();
|
||||
crowdfundViewModel.Enabled = true;
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
|
||||
Assert.IsType<ViewResult>(await crowdfund.ViewCrowdfund(app.Id));
|
||||
// Delete
|
||||
Assert.IsType<NotFoundResult>(apps2.DeleteApp(app.Id));
|
||||
@@ -119,7 +121,7 @@ namespace BTCPayServer.Tests
|
||||
crowdfundViewModel.Enabled = false;
|
||||
crowdfundViewModel.EndDate = null;
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
|
||||
|
||||
var anonAppPubsController = tester.PayTester.GetController<UICrowdfundController>();
|
||||
var crowdfundController = user.GetController<UICrowdfundController>();
|
||||
@@ -144,7 +146,7 @@ namespace BTCPayServer.Tests
|
||||
crowdfundViewModel.StartDate = DateTime.Today.AddDays(2);
|
||||
crowdfundViewModel.Enabled = true;
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
|
||||
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(0.01)
|
||||
@@ -155,7 +157,7 @@ namespace BTCPayServer.Tests
|
||||
crowdfundViewModel.EndDate = DateTime.Today.AddDays(-1);
|
||||
crowdfundViewModel.Enabled = true;
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
|
||||
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(0.01)
|
||||
@@ -168,7 +170,7 @@ namespace BTCPayServer.Tests
|
||||
crowdfundViewModel.TargetAmount = 1;
|
||||
crowdfundViewModel.TargetCurrency = "BTC";
|
||||
crowdfundViewModel.EnforceTargetAmount = true;
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
|
||||
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(1.01)
|
||||
@@ -212,7 +214,7 @@ namespace BTCPayServer.Tests
|
||||
crowdfundViewModel.TargetCurrency = "BTC";
|
||||
crowdfundViewModel.UseAllStoreInvoices = true;
|
||||
crowdfundViewModel.EnforceTargetAmount = true;
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
|
||||
|
||||
var publicApps = user.GetController<UICrowdfundController>();
|
||||
|
||||
@@ -266,7 +268,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains(AppService.GetAppInternalTag(app.Id), invoiceEntity.InternalTags);
|
||||
|
||||
crowdfundViewModel.UseAllStoreInvoices = false;
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
|
||||
|
||||
TestLogs.LogInformation("Because UseAllStoreInvoices is false, let's make sure the invoice is not tagged");
|
||||
invoice = await user.BitPay.CreateInvoiceAsync(new Invoice
|
||||
@@ -285,7 +287,7 @@ namespace BTCPayServer.Tests
|
||||
TestLogs.LogInformation("After turning setting a softcap, let's check that only actual payments are counted");
|
||||
crowdfundViewModel.EnforceTargetAmount = false;
|
||||
crowdfundViewModel.UseAllStoreInvoices = true;
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
|
||||
invoice = await user.BitPay.CreateInvoiceAsync(new Invoice
|
||||
{
|
||||
Buyer = new Buyer { email = "test@fwf.com" },
|
||||
@@ -354,7 +356,7 @@ namespace BTCPayServer.Tests
|
||||
crowdfundViewModel.FormId = lstForms[0].Id;
|
||||
crowdfundViewModel.TargetCurrency = "BTC";
|
||||
crowdfundViewModel.Enabled = true;
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
|
||||
|
||||
var vm2 = await crowdfund.CrowdfundForm(app.Id, (decimal?)0.01).AssertViewModelAsync<FormViewModel>();
|
||||
var res = await crowdfund.CrowdfundFormSubmit(app.Id, (decimal)0.01, "", vm2);
|
||||
@@ -409,7 +411,7 @@ namespace BTCPayServer.Tests
|
||||
crowdfundViewModel.TargetCurrency = "BTC";
|
||||
crowdfundViewModel.Enabled = true;
|
||||
crowdfundViewModel.PerksTemplate = "[{\"id\": \"xxx\",\"title\": \"Perk 1\",\"priceType\": \"Fixed\",\"price\": \"0.001\",\"image\": \"\",\"description\": \"\",\"categories\": [],\"disabled\": false}]";
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
|
||||
|
||||
var vm2 = await crowdfund.CrowdfundForm(app.Id, (decimal?)0.01, "xxx").AssertViewModelAsync<FormViewModel>();
|
||||
var res = await crowdfund.CrowdfundFormSubmit(app.Id, (decimal)0.01, "xxx", vm2);
|
||||
|
@@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Dapper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -42,6 +43,13 @@ namespace BTCPayServer.Tests
|
||||
}), _loggerFactory);
|
||||
}
|
||||
|
||||
public InvoiceRepository GetInvoiceRepository()
|
||||
{
|
||||
var logs = new BTCPayServer.Logging.Logs();
|
||||
logs.Configure(_loggerFactory);
|
||||
return new InvoiceRepository(CreateContextFactory(), new EventAggregator(logs));
|
||||
}
|
||||
|
||||
public ApplicationDbContext CreateContext() => CreateContextFactory().CreateContext();
|
||||
|
||||
public async Task MigrateAsync()
|
||||
@@ -59,18 +67,21 @@ namespace BTCPayServer.Tests
|
||||
await conn.ExecuteAsync($"CREATE DATABASE \"{dbname}\";");
|
||||
}
|
||||
|
||||
public async Task MigrateUntil(string migration)
|
||||
public async Task MigrateUntil(string migration = null)
|
||||
{
|
||||
using var ctx = CreateContext();
|
||||
var db = ctx.Database.GetDbConnection();
|
||||
await EnsureCreatedAsync();
|
||||
var migrations = ctx.Database.GetMigrations().ToArray();
|
||||
var untilMigrationIdx = Array.IndexOf(migrations, migration);
|
||||
if (untilMigrationIdx == -1)
|
||||
throw new InvalidOperationException($"Migration {migration} not found");
|
||||
notAppliedMigrations = migrations[untilMigrationIdx..];
|
||||
await db.ExecuteAsync("CREATE TABLE IF NOT EXISTS \"__EFMigrationsHistory\" (\"MigrationId\" TEXT, \"ProductVersion\" TEXT)");
|
||||
await db.ExecuteAsync("INSERT INTO \"__EFMigrationsHistory\" VALUES (@migration, '8.0.0')", notAppliedMigrations.Select(m => new { migration = m }).ToArray());
|
||||
if (migration is not null)
|
||||
{
|
||||
var untilMigrationIdx = Array.IndexOf(migrations, migration);
|
||||
if (untilMigrationIdx == -1)
|
||||
throw new InvalidOperationException($"Migration {migration} not found");
|
||||
notAppliedMigrations = migrations[untilMigrationIdx..];
|
||||
await db.ExecuteAsync("CREATE TABLE IF NOT EXISTS \"__EFMigrationsHistory\" (\"MigrationId\" TEXT, \"ProductVersion\" TEXT)");
|
||||
await db.ExecuteAsync("INSERT INTO \"__EFMigrationsHistory\" VALUES (@migration, '8.0.0')", notAppliedMigrations.Select(m => new { migration = m }).ToArray());
|
||||
}
|
||||
await ctx.Database.MigrateAsync();
|
||||
}
|
||||
|
||||
|
@@ -3,7 +3,9 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Payments;
|
||||
using Dapper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Altcoins;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
@@ -18,6 +20,110 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanQueryMonitoredInvoices()
|
||||
{
|
||||
var tester = CreateDBTester();
|
||||
await tester.MigrateUntil();
|
||||
var invoiceRepository = tester.GetInvoiceRepository();
|
||||
using var ctx = tester.CreateContext();
|
||||
var conn = ctx.Database.GetDbConnection();
|
||||
|
||||
async Task AddPrompt(string invoiceId, string paymentMethodId, bool activated = true)
|
||||
{
|
||||
JObject prompt = new JObject();
|
||||
if (!activated)
|
||||
prompt["inactive"] = true;
|
||||
prompt["currency"] = "USD";
|
||||
var query = """
|
||||
UPDATE "Invoices" SET "Blob2" = jsonb_set('{"prompts": {}}'::JSONB || COALESCE("Blob2",'{}'), ARRAY['prompts','@paymentMethodId'], '@prompt'::JSONB)
|
||||
WHERE "Id" = '@invoiceId'
|
||||
""";
|
||||
query = query.Replace("@paymentMethodId", paymentMethodId);
|
||||
query = query.Replace("@prompt", prompt.ToString());
|
||||
query = query.Replace("@invoiceId", invoiceId);
|
||||
Assert.Equal(1, await conn.ExecuteAsync(query));
|
||||
}
|
||||
|
||||
await conn.ExecuteAsync("""
|
||||
INSERT INTO "Invoices" ("Id", "Created", "Status","Currency") VALUES
|
||||
('BTCOnly', NOW(), 'New', 'USD'),
|
||||
('LTCOnly', NOW(), 'New', 'USD'),
|
||||
('LTCAndBTC', NOW(), 'New', 'USD'),
|
||||
('LTCAndBTCLazy', NOW(), 'New', 'USD')
|
||||
""");
|
||||
foreach (var invoiceId in new string[] { "LTCOnly", "LTCAndBTCLazy", "LTCAndBTC" })
|
||||
{
|
||||
await AddPrompt(invoiceId, "LTC-CHAIN", true);
|
||||
}
|
||||
foreach (var invoiceId in new string[] { "BTCOnly", "LTCAndBTC" })
|
||||
{
|
||||
await AddPrompt(invoiceId, "BTC-CHAIN", true);
|
||||
}
|
||||
await AddPrompt("LTCAndBTCLazy", "BTC-CHAIN", false);
|
||||
|
||||
var btc = PaymentMethodId.Parse("BTC-CHAIN");
|
||||
var ltc = PaymentMethodId.Parse("LTC-CHAIN");
|
||||
var invoices = await invoiceRepository.GetMonitoredInvoices(btc);
|
||||
Assert.Equal(2, invoices.Length);
|
||||
foreach (var invoiceId in new[] { "BTCOnly", "LTCAndBTC" })
|
||||
{
|
||||
Assert.Contains(invoices, i => i.Id == invoiceId);
|
||||
}
|
||||
invoices = await invoiceRepository.GetMonitoredInvoices(btc, true);
|
||||
Assert.Equal(3, invoices.Length);
|
||||
foreach (var invoiceId in new[] { "BTCOnly", "LTCAndBTC", "LTCAndBTCLazy" })
|
||||
{
|
||||
Assert.Contains(invoices, i => i.Id == invoiceId);
|
||||
}
|
||||
|
||||
invoices = await invoiceRepository.GetMonitoredInvoices(ltc);
|
||||
Assert.Equal(3, invoices.Length);
|
||||
foreach (var invoiceId in new[] { "LTCAndBTC", "LTCAndBTC", "LTCAndBTCLazy" })
|
||||
{
|
||||
Assert.Contains(invoices, i => i.Id == invoiceId);
|
||||
}
|
||||
|
||||
await conn.ExecuteAsync("""
|
||||
INSERT INTO "Payments" ("Id", "InvoiceDataId", "PaymentMethodId", "Status", "Blob2", "Created", "Amount", "Currency") VALUES
|
||||
('1','LTCAndBTC', 'LTC-CHAIN', 'Processing', '{}'::JSONB, NOW(), 123, 'USD'),
|
||||
('2','LTCAndBTC', 'BTC-CHAIN', 'Processing', '{}'::JSONB, NOW(), 123, 'USD'),
|
||||
('3','LTCAndBTC', 'BTC-CHAIN', 'Processing', '{}'::JSONB, NOW(), 123, 'USD'),
|
||||
('4','LTCAndBTC', 'BTC-CHAIN', 'Settled', '{}'::JSONB, NOW(), 123, 'USD');
|
||||
|
||||
INSERT INTO "AddressInvoices" ("InvoiceDataId", "Address", "PaymentMethodId") VALUES
|
||||
('LTCAndBTC', 'BTC1', 'BTC-CHAIN'),
|
||||
('LTCAndBTC', 'BTC2', 'BTC-CHAIN'),
|
||||
('LTCAndBTC', 'LTC1', 'LTC-CHAIN');
|
||||
""");
|
||||
|
||||
var invoice = Assert.Single(await invoiceRepository.GetMonitoredInvoices(ltc), i => i.Id == "LTCAndBTC");
|
||||
var payment = Assert.Single(invoice.GetPayments(false));
|
||||
Assert.Equal("1", payment.Id);
|
||||
|
||||
foreach (var includeNonActivated in new[] { true, false })
|
||||
{
|
||||
invoices = await invoiceRepository.GetMonitoredInvoices(btc, includeNonActivated);
|
||||
invoice = Assert.Single(invoices, i => i.Id == "LTCAndBTC");
|
||||
var payments = invoice.GetPayments(false);
|
||||
Assert.Equal(3, payments.Count);
|
||||
|
||||
foreach (var paymentId in new[] { "2", "3", "4" })
|
||||
{
|
||||
Assert.Contains(payments, p => p.Id == paymentId);
|
||||
}
|
||||
Assert.Equal(2, invoice.Addresses.Count);
|
||||
foreach (var addr in new[] { "BTC1", "BTC2" })
|
||||
{
|
||||
Assert.Contains(invoice.Addresses, p => p.Address == addr);
|
||||
}
|
||||
if (!includeNonActivated)
|
||||
Assert.DoesNotContain(invoices, i => i.Id == "LTCAndBTCLazy");
|
||||
else
|
||||
Assert.Contains(invoices, i => i.Id == "LTCAndBTCLazy");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanMigrateInvoiceAddresses()
|
||||
{
|
||||
|
@@ -97,6 +97,9 @@ namespace BTCPayServer.Tests
|
||||
Assert.NotNull(e.APIError.Message);
|
||||
GreenfieldPermissionAPIError permissionError = Assert.IsType<GreenfieldPermissionAPIError>(e.APIError);
|
||||
Assert.Equal(Policies.CanModifyStoreSettings, permissionError.MissingPermission);
|
||||
|
||||
var client = await user.CreateClient(Policies.CanViewStoreSettings);
|
||||
await AssertAPIError("unsupported-in-v2", () => client.SendHttpRequest<object>($"api/v1/stores/{user.StoreId}/payment-methods/LightningNetwork"));
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
@@ -368,6 +371,27 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
)
|
||||
);
|
||||
var template = @"[
|
||||
{
|
||||
""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."",
|
||||
""id"": ""green-tea"",
|
||||
""image"": ""~/img/pos-sample/green-tea.jpg"",
|
||||
""priceType"": ""Fixed"",
|
||||
""price"": ""1"",
|
||||
""title"": ""Green Tea"",
|
||||
""disabled"": false
|
||||
}
|
||||
]";
|
||||
await AssertValidationError(new[] { "Template" },
|
||||
async () => await client.CreatePointOfSaleApp(
|
||||
user.StoreId,
|
||||
new PointOfSaleAppRequest
|
||||
{
|
||||
AppName = "good name",
|
||||
Template = template.Replace(@"""id"": ""green-tea"",", "")
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Test creating a POS app successfully
|
||||
var app = await client.CreatePointOfSaleApp(
|
||||
@@ -376,7 +400,8 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
AppName = "test app from API",
|
||||
Currency = "JPY",
|
||||
Title = "test app title"
|
||||
Title = "test app title",
|
||||
Template = template
|
||||
}
|
||||
);
|
||||
Assert.Equal("test app from API", app.AppName);
|
||||
@@ -559,6 +584,27 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
)
|
||||
);
|
||||
var template = @"[
|
||||
{
|
||||
""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."",
|
||||
""id"": ""green-tea"",
|
||||
""image"": ""~/img/pos-sample/green-tea.jpg"",
|
||||
""priceType"": ""Fixed"",
|
||||
""price"": ""1"",
|
||||
""title"": ""Green Tea"",
|
||||
""disabled"": false
|
||||
}
|
||||
]";
|
||||
await AssertValidationError(new[] { "PerksTemplate" },
|
||||
async () => await client.CreateCrowdfundApp(
|
||||
user.StoreId,
|
||||
new CrowdfundAppRequest
|
||||
{
|
||||
AppName = "good name",
|
||||
PerksTemplate = template.Replace(@"""id"": ""green-tea"",", "")
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Test creating a crowdfund app
|
||||
var app = await client.CreateCrowdfundApp(
|
||||
@@ -566,7 +612,8 @@ namespace BTCPayServer.Tests
|
||||
new CrowdfundAppRequest
|
||||
{
|
||||
AppName = "test app from API",
|
||||
Title = "test app title"
|
||||
Title = "test app title",
|
||||
PerksTemplate = template
|
||||
}
|
||||
);
|
||||
Assert.Equal("test app from API", app.AppName);
|
||||
@@ -1104,7 +1151,7 @@ namespace BTCPayServer.Tests
|
||||
Description = "Test description",
|
||||
Amount = 12.3m,
|
||||
Currency = "BTC",
|
||||
PaymentMethods = new[] { "BTC" }
|
||||
PayoutMethods = new[] { "BTC" }
|
||||
});
|
||||
|
||||
void VerifyResult()
|
||||
@@ -1135,7 +1182,7 @@ namespace BTCPayServer.Tests
|
||||
Name = "Test 2",
|
||||
Amount = 12.3m,
|
||||
Currency = "BTC",
|
||||
PaymentMethods = new[] { "BTC" },
|
||||
PayoutMethods = new[] { "BTC" },
|
||||
BOLT11Expiration = TimeSpan.FromDays(31.0)
|
||||
});
|
||||
Assert.Equal(TimeSpan.FromDays(31.0), test2.BOLT11Expiration);
|
||||
@@ -1182,13 +1229,13 @@ namespace BTCPayServer.Tests
|
||||
|
||||
payouts = await unauthenticated.GetPayouts(pps[0].Id);
|
||||
var payout2 = Assert.Single(payouts);
|
||||
Assert.Equal(payout.Amount, payout2.Amount);
|
||||
Assert.Equal(payout.OriginalAmount, payout2.OriginalAmount);
|
||||
Assert.Equal(payout.Id, payout2.Id);
|
||||
Assert.Equal(destination, payout2.Destination);
|
||||
Assert.Equal(PayoutState.AwaitingApproval, payout.State);
|
||||
Assert.Equal("BTC-CHAIN", payout2.PayoutMethodId);
|
||||
Assert.Equal("BTC", payout2.CryptoCode);
|
||||
Assert.Null(payout.PaymentMethodAmount);
|
||||
Assert.Equal("BTC", payout2.PayoutCurrency);
|
||||
Assert.Null(payout.PayoutAmount);
|
||||
|
||||
TestLogs.LogInformation("Can't overdraft");
|
||||
|
||||
@@ -1230,7 +1277,7 @@ namespace BTCPayServer.Tests
|
||||
Amount = 12.3m,
|
||||
StartsAt = start,
|
||||
Currency = "BTC",
|
||||
PaymentMethods = new[] { "BTC" }
|
||||
PayoutMethods = new[] { "BTC" }
|
||||
});
|
||||
Assert.Equal(start, inFuture.StartsAt);
|
||||
Assert.Null(inFuture.ExpiresAt);
|
||||
@@ -1248,7 +1295,7 @@ namespace BTCPayServer.Tests
|
||||
Amount = 12.3m,
|
||||
ExpiresAt = expires,
|
||||
Currency = "BTC",
|
||||
PaymentMethods = new[] { "BTC" }
|
||||
PayoutMethods = new[] { "BTC" }
|
||||
});
|
||||
await this.AssertAPIError("expired", async () => await unauthenticated.CreatePayout(inPast.Id, new CreatePayoutRequest()
|
||||
{
|
||||
@@ -1272,7 +1319,7 @@ namespace BTCPayServer.Tests
|
||||
Name = "Test USD",
|
||||
Amount = 5000m,
|
||||
Currency = "USD",
|
||||
PaymentMethods = new[] { "BTC" }
|
||||
PayoutMethods = new[] { "BTC" }
|
||||
});
|
||||
|
||||
await this.AssertAPIError("lnurl-not-supported", async () => await unauthenticated.GetPullPaymentLNURL(pp.Id));
|
||||
@@ -1297,8 +1344,8 @@ namespace BTCPayServer.Tests
|
||||
Revision = payout.Revision
|
||||
});
|
||||
Assert.Equal(PayoutState.AwaitingPayment, payout.State);
|
||||
Assert.NotNull(payout.PaymentMethodAmount);
|
||||
Assert.Equal(1.0m, payout.PaymentMethodAmount); // 1 BTC == 5000 USD in tests
|
||||
Assert.NotNull(payout.PayoutAmount);
|
||||
Assert.Equal(1.0m, payout.PayoutAmount); // 1 BTC == 5000 USD in tests
|
||||
await this.AssertAPIError("invalid-state", async () => await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest()
|
||||
{
|
||||
Revision = payout.Revision
|
||||
@@ -1310,7 +1357,7 @@ namespace BTCPayServer.Tests
|
||||
Name = "Test 2",
|
||||
Amount = 12.303228134m,
|
||||
Currency = "BTC",
|
||||
PaymentMethods = new[] { "BTC" }
|
||||
PayoutMethods = new[] { "BTC" }
|
||||
});
|
||||
destination = (await tester.ExplorerNode.GetNewAddressAsync()).ToString();
|
||||
payout = await unauthenticated.CreatePayout(test3.Id, new CreatePayoutRequest()
|
||||
@@ -1320,8 +1367,8 @@ namespace BTCPayServer.Tests
|
||||
});
|
||||
payout = await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest());
|
||||
// The payout should round the value of the payment down to the network of the payment method
|
||||
Assert.Equal(12.30322814m, payout.PaymentMethodAmount);
|
||||
Assert.Equal(12.303228134m, payout.Amount);
|
||||
Assert.Equal(12.30322814m, payout.PayoutAmount);
|
||||
Assert.Equal(12.303228134m, payout.OriginalAmount);
|
||||
|
||||
await client.MarkPayoutPaid(storeId, payout.Id);
|
||||
payout = (await client.GetPayouts(payout.PullPaymentId)).First(data => data.Id == payout.Id);
|
||||
@@ -1334,7 +1381,7 @@ namespace BTCPayServer.Tests
|
||||
Name = "Test 3",
|
||||
Amount = 12.303228134m,
|
||||
Currency = "BTC",
|
||||
PaymentMethods = new[] { "BTC", "BTC-LightningNetwork", "BTC_LightningLike" }
|
||||
PayoutMethods = new[] { "BTC", "BTC-LightningNetwork", "BTC_LightningLike" }
|
||||
});
|
||||
var lnrURLs = await unauthenticated.GetPullPaymentLNURL(test4.Id);
|
||||
Assert.IsType<string>(lnrURLs.LNURLBech32);
|
||||
@@ -1409,7 +1456,7 @@ namespace BTCPayServer.Tests
|
||||
Name = "Test SATS",
|
||||
Amount = 21000,
|
||||
Currency = "SATS",
|
||||
PaymentMethods = new[] { "BTC", "BTC-LightningNetwork", "BTC_LightningLike" }
|
||||
PayoutMethods = new[] { "BTC", "BTC-LightningNetwork", "BTC_LightningLike" }
|
||||
});
|
||||
lnrURLs = await unauthenticated.GetPullPaymentLNURL(testSats.Id);
|
||||
Assert.IsType<string>(lnrURLs.LNURLBech32);
|
||||
@@ -1427,7 +1474,7 @@ namespace BTCPayServer.Tests
|
||||
Amount = 100,
|
||||
Currency = "USD",
|
||||
Name = "pull payment",
|
||||
PaymentMethods = new[] { "BTC" },
|
||||
PayoutMethods = new[] { "BTC" },
|
||||
AutoApproveClaims = true
|
||||
});
|
||||
});
|
||||
@@ -1447,7 +1494,7 @@ namespace BTCPayServer.Tests
|
||||
Amount = 100,
|
||||
Currency = "USD",
|
||||
Name = "pull payment",
|
||||
PaymentMethods = new[] { "BTC" },
|
||||
PayoutMethods = new[] { "BTC" },
|
||||
AutoApproveClaims = true
|
||||
});
|
||||
|
||||
@@ -2374,6 +2421,14 @@ namespace BTCPayServer.Tests
|
||||
invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest { Amount = 5000.0m, Currency = "USD" });
|
||||
methods = await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id);
|
||||
method = methods.First();
|
||||
Assert.Equal(JTokenType.Null, method.AdditionalData["accountDerivation"].Type);
|
||||
Assert.NotNull(method.AdditionalData["keyPath"]);
|
||||
|
||||
methods = await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id, includeSensitive: true);
|
||||
method = methods.First();
|
||||
Assert.Equal(JTokenType.String, method.AdditionalData["accountDerivation"].Type);
|
||||
var clientViewOnly = await user.CreateClient(Policies.CanViewInvoices);
|
||||
await AssertApiError(403, "missing-permission", () => clientViewOnly.GetInvoicePaymentMethods(user.StoreId, invoice.Id, includeSensitive: true));
|
||||
|
||||
await tester.WaitForEvent<NewOnChainTransactionEvent>(async () =>
|
||||
{
|
||||
@@ -2717,8 +2772,8 @@ namespace BTCPayServer.Tests
|
||||
var includeNonActivated = true;
|
||||
Assert.Single(await invoiceRepo.GetMonitoredInvoices(PaymentMethodId.Parse("BTC-CHAIN"), includeNonActivated), i => i.Id == invoice.Id);
|
||||
includeNonActivated = false;
|
||||
Assert.Single(await invoiceRepo.GetMonitoredInvoices(PaymentMethodId.Parse("BTC-CHAIN"), includeNonActivated), i => i.Id == invoice.Id);
|
||||
Assert.Single(await invoiceRepo.GetMonitoredInvoices(PaymentMethodId.Parse("BTC-CHAIN")), i => i.Id == invoice.Id);
|
||||
Assert.DoesNotContain(await invoiceRepo.GetMonitoredInvoices(PaymentMethodId.Parse("BTC-CHAIN"), includeNonActivated), i => i.Id == invoice.Id);
|
||||
Assert.DoesNotContain(await invoiceRepo.GetMonitoredInvoices(PaymentMethodId.Parse("BTC-CHAIN")), i => i.Id == invoice.Id);
|
||||
//
|
||||
|
||||
paymentMethods = await client.GetInvoicePaymentMethods(store.Id, invoice.Id);
|
||||
@@ -4122,7 +4177,12 @@ namespace BTCPayServer.Tests
|
||||
var resp = await tester.CustomerLightningD.Pay(inv.BOLT11);
|
||||
Assert.Equal(PayResult.Ok, resp.Result);
|
||||
|
||||
|
||||
var store = tester.PayTester.GetService<StoreRepository>();
|
||||
Assert.True(await store.InternalNodePayoutAuthorized(admin.StoreId));
|
||||
Assert.False(await store.InternalNodePayoutAuthorized("blah"));
|
||||
await admin.MakeAdmin(false);
|
||||
Assert.False(await store.InternalNodePayoutAuthorized(admin.StoreId));
|
||||
await admin.MakeAdmin(true);
|
||||
|
||||
var customerInvoice = await tester.CustomerLightningD.CreateInvoice(LightMoney.FromUnit(10, LightMoneyUnit.Satoshi),
|
||||
Guid.NewGuid().ToString(), TimeSpan.FromDays(40));
|
||||
@@ -4183,7 +4243,7 @@ namespace BTCPayServer.Tests
|
||||
PayoutMethodId = "BTC_LightningNetwork",
|
||||
Destination = customerInvoice.BOLT11
|
||||
});
|
||||
Assert.Equal(payout2.Amount, new Money(100, MoneyUnit.Satoshi).ToDecimal(MoneyUnit.BTC));
|
||||
Assert.Equal(payout2.OriginalAmount, new Money(100, MoneyUnit.Satoshi).ToDecimal(MoneyUnit.BTC));
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
@@ -4227,7 +4287,7 @@ namespace BTCPayServer.Tests
|
||||
Amount = 100,
|
||||
Currency = "USD",
|
||||
Name = "pull payment",
|
||||
PaymentMethods = new[] { "BTC" }
|
||||
PayoutMethods = new[] { "BTC" }
|
||||
});
|
||||
|
||||
var notapprovedPayoutWithPullPayment = await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest()
|
||||
@@ -4253,7 +4313,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
Assert.Equal(3, payouts.Length);
|
||||
Assert.Empty(payouts.Where(data => data.State == PayoutState.AwaitingApproval));
|
||||
Assert.Empty(payouts.Where(data => data.PaymentMethodAmount is null));
|
||||
Assert.Empty(payouts.Where(data => data.PayoutAmount is null));
|
||||
|
||||
Assert.Empty(await adminClient.ShowOnChainWalletTransactions(admin.StoreId, "BTC"));
|
||||
|
||||
@@ -4266,12 +4326,12 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(3600, Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC")).IntervalSeconds.TotalSeconds);
|
||||
|
||||
var tpGen = Assert.Single(await adminClient.GetPayoutProcessors(admin.StoreId));
|
||||
Assert.Equal("BTC-CHAIN", Assert.Single(tpGen.PaymentMethods));
|
||||
Assert.Equal("BTC-CHAIN", Assert.Single(tpGen.PayoutMethods));
|
||||
//still too poor to process any payouts
|
||||
Assert.Empty(await adminClient.ShowOnChainWalletTransactions(admin.StoreId, "BTC"));
|
||||
|
||||
|
||||
await adminClient.RemovePayoutProcessor(admin.StoreId, tpGen.Name, tpGen.PaymentMethods.First());
|
||||
await adminClient.RemovePayoutProcessor(admin.StoreId, tpGen.Name, tpGen.PayoutMethods.First());
|
||||
|
||||
Assert.Empty(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC"));
|
||||
Assert.Empty(await adminClient.GetPayoutProcessors(admin.StoreId));
|
||||
|
13
BTCPayServer.Tests/OutputPathAttribute.cs
Normal file
13
BTCPayServer.Tests/OutputPathAttribute.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class OutputPathAttribute : Attribute
|
||||
{
|
||||
public OutputPathAttribute(string builtPath)
|
||||
{
|
||||
BuiltPath = builtPath;
|
||||
}
|
||||
public string BuiltPath { get; }
|
||||
}
|
||||
}
|
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Controllers;
|
||||
@@ -90,6 +91,54 @@ fruit tea:
|
||||
Assert.Null( parsedDefault[4].AdditionalData);
|
||||
Assert.Null( parsedDefault[4].PaymentMethods);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanParseAppTemplate()
|
||||
{
|
||||
var template = @"[
|
||||
{
|
||||
""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."",
|
||||
""id"": ""green-tea"",
|
||||
""image"": ""~/img/pos-sample/green-tea.jpg"",
|
||||
""priceType"": ""Fixed"",
|
||||
""price"": ""1"",
|
||||
""title"": ""Green Tea"",
|
||||
""disabled"": false
|
||||
},
|
||||
{
|
||||
""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."",
|
||||
""id"": ""black-tea"",
|
||||
""image"": ""~/img/pos-sample/black-tea.jpg"",
|
||||
""priceType"": ""Fixed"",
|
||||
""price"": ""1"",
|
||||
""title"": ""Black Tea"",
|
||||
""disabled"": false
|
||||
}
|
||||
]";
|
||||
|
||||
var items = AppService.Parse(template);
|
||||
Assert.Equal(2, items.Length);
|
||||
Assert.Equal("green-tea", items[0].Id);
|
||||
Assert.Equal("black-tea", items[1].Id);
|
||||
|
||||
// Fails gracefully for missing ID
|
||||
var missingId = template.Replace(@"""id"": ""green-tea"",", "");
|
||||
items = AppService.Parse(missingId);
|
||||
Assert.Single(items);
|
||||
Assert.Equal("black-tea", items[0].Id);
|
||||
|
||||
// Throws for missing ID
|
||||
Assert.Throws<ArgumentException>(() => AppService.Parse(missingId, true, true));
|
||||
|
||||
// Fails gracefully for duplicate IDs
|
||||
var duplicateId = template.Replace(@"""id"": ""green-tea"",", @"""id"": ""black-tea"",");
|
||||
items = AppService.Parse(duplicateId);
|
||||
Assert.Empty(items);
|
||||
|
||||
// Throws for duplicate IDs
|
||||
Assert.Throws<ArgumentException>(() => AppService.Parse(duplicateId, true, true));
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
|
@@ -45,7 +45,7 @@ namespace BTCPayServer.Tests
|
||||
var runInBrowser = config["RunSeleniumInBrowser"] == "true";
|
||||
// Reset this using `dotnet user-secrets remove RunSeleniumInBrowser`
|
||||
|
||||
var chromeDriverPath = config["ChromeDriverDirectory"] ?? (Server.PayTester.InContainer ? "/usr/bin" : Directory.GetCurrentDirectory());
|
||||
var chromeDriverPath = config["ChromeDriverDirectory"] ?? (Server.PayTester.InContainer ? "/usr/bin" : TestUtils.TestDirectory);
|
||||
|
||||
var options = new ChromeOptions();
|
||||
if (!runInBrowser)
|
||||
@@ -132,11 +132,11 @@ retry:
|
||||
/// Because for some reason, the selenium container can't resolve the tests container domain name
|
||||
/// </summary>
|
||||
public Uri ServerUri;
|
||||
internal IWebElement FindAlertMessage(StatusMessageModel.StatusSeverity severity = StatusMessageModel.StatusSeverity.Success)
|
||||
public IWebElement FindAlertMessage(StatusMessageModel.StatusSeverity severity = StatusMessageModel.StatusSeverity.Success)
|
||||
{
|
||||
return FindAlertMessage(new[] { severity });
|
||||
}
|
||||
internal IWebElement FindAlertMessage(params StatusMessageModel.StatusSeverity[] severity)
|
||||
public IWebElement FindAlertMessage(params StatusMessageModel.StatusSeverity[] severity)
|
||||
{
|
||||
var className = string.Join(", ", severity.Select(statusSeverity => $".alert-{StatusMessageModel.ToString(statusSeverity)}"));
|
||||
IWebElement el;
|
||||
@@ -182,10 +182,13 @@ retry:
|
||||
Driver.FindElement(By.Id("RegisterButton")).Click();
|
||||
Driver.AssertNoError();
|
||||
CreatedUser = usr;
|
||||
Password = "123456";
|
||||
return usr;
|
||||
}
|
||||
string CreatedUser;
|
||||
|
||||
public string Password { get; private set; }
|
||||
|
||||
public TestAccount AsTestAccount()
|
||||
{
|
||||
return new TestAccount(Server) { RegisterDetails = new Models.AccountViewModels.RegisterViewModel() { Password = "123456", Email = CreatedUser } };
|
||||
|
@@ -1238,6 +1238,7 @@ namespace BTCPayServer.Tests
|
||||
await s.StartAsync();
|
||||
var userId = s.RegisterNewUser(true);
|
||||
s.CreateNewStore();
|
||||
s.GenerateWallet();
|
||||
(_, string appId) = s.CreateApp("PointOfSale");
|
||||
s.Driver.FindElement(By.Id("Title")).Clear();
|
||||
s.Driver.FindElement(By.Id("Title")).SendKeys("Tea shop");
|
||||
@@ -1249,10 +1250,20 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("CodeTabButton")).Click();
|
||||
var template = s.Driver.FindElement(By.Id("TemplateConfig")).GetAttribute("value");
|
||||
Assert.Contains("\"buyButtonText\": \"Take my money\"", template);
|
||||
Assert.Matches("\"categories\": \\[\n\\s+\"Drinks\"\n\\s+\\]", template);
|
||||
Assert.Matches("\"categories\": \\[\r?\n\\s*\"Drinks\"\\s*\\]", template);
|
||||
|
||||
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("App updated", s.FindAlertMessage().Text);
|
||||
|
||||
s.Driver.ScrollTo(By.Id("CodeTabButton"));
|
||||
s.Driver.FindElement(By.Id("CodeTabButton")).Click();
|
||||
template = s.Driver.FindElement(By.Id("TemplateConfig")).GetAttribute("value");
|
||||
s.Driver.FindElement(By.Id("TemplateConfig")).Clear();
|
||||
s.Driver.FindElement(By.Id("TemplateConfig")).SendKeys(template.Replace(@"""id"": ""green-tea"",", ""));
|
||||
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("Invalid template: Missing ID for item \"Green Tea\".", s.Driver.FindElement(By.CssSelector(".validation-summary-errors")).Text);
|
||||
|
||||
s.Driver.FindElement(By.Id("ViewApp")).Click();
|
||||
var windows = s.Driver.WindowHandles;
|
||||
@@ -1973,6 +1984,7 @@ namespace BTCPayServer.Tests
|
||||
var jack = new Key().PubKey.Hash.GetAddress(Network.RegTest);
|
||||
SetTransactionOutput(s, 0, jack, 0.01m);
|
||||
s.Driver.FindElement(By.Id("SignTransaction")).Click();
|
||||
s.Driver.WaitForElement(By.CssSelector("button[value=broadcast]"));
|
||||
Assert.Contains(jack.ToString(), s.Driver.PageSource);
|
||||
Assert.Contains("0.01000000", s.Driver.PageSource);
|
||||
Assert.EndsWith("psbt/ready", s.Driver.Url);
|
||||
@@ -3416,11 +3428,14 @@ namespace BTCPayServer.Tests
|
||||
var user = s.RegisterNewUser();
|
||||
s.GoToHome();
|
||||
s.GoToProfile(ManageNavPages.LoginCodes);
|
||||
var code = s.Driver.FindElement(By.Id("logincode")).GetAttribute("value");
|
||||
s.ClickPagePrimary();
|
||||
Assert.NotEqual(code, s.Driver.FindElement(By.Id("logincode")).GetAttribute("value"));
|
||||
|
||||
code = s.Driver.FindElement(By.Id("logincode")).GetAttribute("value");
|
||||
string code = null;
|
||||
TestUtils.Eventually(() => { code = s.Driver.FindElement(By.CssSelector("#LoginCode .qr-code")).GetAttribute("alt"); });
|
||||
string prevCode = code;
|
||||
await s.Driver.Navigate().RefreshAsync();
|
||||
TestUtils.Eventually(() => { code = s.Driver.FindElement(By.CssSelector("#LoginCode .qr-code")).GetAttribute("alt"); });
|
||||
Assert.NotEqual(prevCode, code);
|
||||
TestUtils.Eventually(() => { code = s.Driver.FindElement(By.CssSelector("#LoginCode .qr-code")).GetAttribute("alt"); });
|
||||
s.Logout();
|
||||
s.GoToLogin();
|
||||
s.Driver.SetAttribute("LoginCode", "value", "bad code");
|
||||
|
@@ -146,19 +146,27 @@ namespace BTCPayServer.Tests
|
||||
public async Task ModifyPayment(Action<GeneralSettingsViewModel> modify)
|
||||
{
|
||||
var storeController = GetController<UIStoresController>();
|
||||
var response = await storeController.GeneralSettings();
|
||||
GeneralSettingsViewModel settings = (GeneralSettingsViewModel)((ViewResult)response).Model;
|
||||
var response = await storeController.GeneralSettings(StoreId);
|
||||
GeneralSettingsViewModel settings = (GeneralSettingsViewModel)((ViewResult)response).Model!;
|
||||
modify(settings);
|
||||
await storeController.GeneralSettings(settings);
|
||||
}
|
||||
|
||||
public async Task ModifyGeneralSettings(Action<GeneralSettingsViewModel> modify)
|
||||
{
|
||||
var storeController = GetController<UIStoresController>();
|
||||
var response = await storeController.GeneralSettings(StoreId);
|
||||
GeneralSettingsViewModel settings = (GeneralSettingsViewModel)((ViewResult)response).Model!;
|
||||
modify(settings);
|
||||
storeController.GeneralSettings(settings).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task ModifyOnchainPaymentSettings(Action<WalletSettingsViewModel> modify)
|
||||
{
|
||||
var storeController = GetController<UIStoresController>();
|
||||
var response = await storeController.WalletSettings(StoreId, "BTC");
|
||||
WalletSettingsViewModel walletSettings = (WalletSettingsViewModel)((ViewResult)response).Model;
|
||||
modify(walletSettings);
|
||||
storeController.UpdatePaymentSettings(walletSettings).GetAwaiter().GetResult();
|
||||
storeController.UpdateWalletSettings(walletSettings).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
|
@@ -1,3 +1,5 @@
|
||||
Password => Cyphercode
|
||||
Email address => Cypher ID
|
||||
Welcome to {0} => Yo at {0}
|
||||
{
|
||||
"Password" : "Cyphercode",
|
||||
"Email address" : "Cypher ID",
|
||||
"Welcome to {0}" : "Yo at {0}"
|
||||
}
|
||||
|
@@ -16,14 +16,13 @@ namespace BTCPayServer.Tests
|
||||
public static class TestUtils
|
||||
{
|
||||
#if DEBUG && !SHORT_TIMEOUT
|
||||
public const int TestTimeout = 60_000;
|
||||
public const int TestTimeout = 600_000;
|
||||
#else
|
||||
public const int TestTimeout = 90_000;
|
||||
#endif
|
||||
public static DirectoryInfo TryGetSolutionDirectoryInfo(string currentPath = null)
|
||||
public static DirectoryInfo TryGetSolutionDirectoryInfo()
|
||||
{
|
||||
var directory = new DirectoryInfo(
|
||||
currentPath ?? Directory.GetCurrentDirectory());
|
||||
var directory = new DirectoryInfo(TestDirectory);
|
||||
while (directory != null && !directory.GetFiles("*.sln").Any())
|
||||
{
|
||||
directory = directory.Parent;
|
||||
@@ -31,10 +30,15 @@ namespace BTCPayServer.Tests
|
||||
return directory;
|
||||
}
|
||||
|
||||
static TestUtils()
|
||||
{
|
||||
TestDirectory = ((OutputPathAttribute)typeof(TestUtils).Assembly.GetCustomAttributes(typeof(OutputPathAttribute), true)[0]).BuiltPath;
|
||||
}
|
||||
public readonly static string TestDirectory;
|
||||
|
||||
public static string GetTestDataFullPath(string relativeFilePath)
|
||||
{
|
||||
var directory = new DirectoryInfo(Directory.GetCurrentDirectory());
|
||||
var directory = new DirectoryInfo(TestDirectory);
|
||||
while (directory != null && !directory.GetFiles("*.csproj").Any())
|
||||
{
|
||||
directory = directory.Parent;
|
||||
|
@@ -303,7 +303,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// Set tolerance to 50%
|
||||
var stores = user.GetController<UIStoresController>();
|
||||
var response = await stores.GeneralSettings();
|
||||
var response = await stores.GeneralSettings(user.StoreId);
|
||||
var vm = Assert.IsType<GeneralSettingsViewModel>(Assert.IsType<ViewResult>(response).Model);
|
||||
Assert.Equal(0.0, vm.PaymentTolerance);
|
||||
vm.PaymentTolerance = 50.0;
|
||||
@@ -385,7 +385,7 @@ namespace BTCPayServer.Tests
|
||||
await user.RegisterDerivationSchemeAsync("BTC");
|
||||
await user.RegisterLightningNodeAsync("BTC", LightningConnectionType.CLightning);
|
||||
await user.SetNetworkFeeMode(NetworkFeeMode.Never);
|
||||
await user.ModifyOnchainPaymentSettings(p => p.SpeedPolicy = SpeedPolicy.HighSpeed);
|
||||
await user.ModifyGeneralSettings(p => p.SpeedPolicy = SpeedPolicy.HighSpeed);
|
||||
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(0.0001m, "BTC"));
|
||||
await tester.WaitForEvent<InvoiceNewPaymentDetailsEvent>(async () =>
|
||||
{
|
||||
@@ -445,7 +445,7 @@ namespace BTCPayServer.Tests
|
||||
var user = tester.NewAccount();
|
||||
await user.GrantAccessAsync(true);
|
||||
var storeController = user.GetController<UIStoresController>();
|
||||
var storeResponse = await storeController.GeneralSettings();
|
||||
var storeResponse = await storeController.GeneralSettings(user.StoreId);
|
||||
Assert.IsType<ViewResult>(storeResponse);
|
||||
Assert.IsType<ViewResult>(storeController.SetupLightningNode(user.StoreId, "BTC"));
|
||||
|
||||
@@ -568,10 +568,10 @@ namespace BTCPayServer.Tests
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var acc = tester.NewAccount();
|
||||
acc.GrantAccess();
|
||||
await acc.GrantAccessAsync();
|
||||
acc.RegisterDerivationScheme("BTC");
|
||||
await acc.ModifyOnchainPaymentSettings(p => p.SpeedPolicy = SpeedPolicy.LowSpeed);
|
||||
var invoice = acc.BitPay.CreateInvoice(new Invoice
|
||||
await acc.ModifyGeneralSettings(p => p.SpeedPolicy = SpeedPolicy.LowSpeed);
|
||||
var invoice = await acc.BitPay.CreateInvoiceAsync(new Invoice
|
||||
{
|
||||
Price = 5.0m,
|
||||
Currency = "USD",
|
||||
@@ -1463,7 +1463,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
Currency = "BTC",
|
||||
Amount = 1.0m,
|
||||
PaymentMethods = [ "BTC-CHAIN" ]
|
||||
PayoutMethods = [ "BTC-CHAIN" ]
|
||||
});
|
||||
var controller = user.GetController<UIInvoiceController>();
|
||||
var invoice = await controller.CreateInvoiceCoreRaw(new()
|
||||
@@ -1479,7 +1479,7 @@ namespace BTCPayServer.Tests
|
||||
var payout = Assert.Single(payouts);
|
||||
Assert.Equal("TOPUP", payout.PayoutMethodId);
|
||||
Assert.Equal(invoice.Id, payout.Destination);
|
||||
Assert.Equal(-0.5m, payout.Amount);
|
||||
Assert.Equal(-0.5m, payout.OriginalAmount);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1844,6 +1844,8 @@ namespace BTCPayServer.Tests
|
||||
await user.GrantAccessAsync();
|
||||
var user2 = tester.NewAccount();
|
||||
await user2.GrantAccessAsync();
|
||||
await user.RegisterDerivationSchemeAsync("BTC");
|
||||
await user2.RegisterDerivationSchemeAsync("BTC");
|
||||
var stores = user.GetController<UIStoresController>();
|
||||
var apps = user.GetController<UIAppsController>();
|
||||
var apps2 = user2.GetController<UIAppsController>();
|
||||
@@ -2617,7 +2619,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
var controller = tester.PayTester.GetController<UIStoresController>(user.UserId, user.StoreId);
|
||||
var vm = await controller.GeneralSettings().AssertViewModelAsync<GeneralSettingsViewModel>();
|
||||
var vm = await controller.GeneralSettings(user.StoreId).AssertViewModelAsync<GeneralSettingsViewModel>();
|
||||
Assert.Equal(tester.PayTester.ServerUriWithIP + "LocalStorage/8f890691-87f9-4c65-80e5-3b7ffaa3551f-store.png", vm.LogoUrl);
|
||||
Assert.Equal(tester.PayTester.ServerUriWithIP + "LocalStorage/2a51c49a-9d54-4013-80a2-3f6e69d08523-store.css", vm.CssUrl);
|
||||
|
||||
|
@@ -352,7 +352,7 @@ retry:
|
||||
}
|
||||
|
||||
// Go through all cshtml file, search for text-translate or ViewLocalizer usage
|
||||
using (var tester = CreateServerTester())
|
||||
using (var tester = CreateServerTester(newDb: true))
|
||||
{
|
||||
await tester.StartAsync();
|
||||
var engine = tester.PayTester.GetService<RazorProjectEngine>();
|
||||
@@ -360,12 +360,15 @@ retry:
|
||||
{
|
||||
var filePath = file.FullName;
|
||||
var txt = File.ReadAllText(file.FullName);
|
||||
if (txt.Contains("ViewLocalizer"))
|
||||
foreach (string localizer in new[] { "ViewLocalizer", "StringLocalizer" })
|
||||
{
|
||||
var matches = Regex.Matches(txt, "ViewLocalizer\\[\"(.*?)\"[\\],]");
|
||||
foreach (Match match in matches)
|
||||
if (txt.Contains(localizer))
|
||||
{
|
||||
defaultTranslatedKeys.Add(match.Groups[1].Value);
|
||||
var matches = Regex.Matches(txt, localizer + "\\[\"(.*?)\"[\\],]");
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
defaultTranslatedKeys.Add(match.Groups[1].Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,12 +382,18 @@ retry:
|
||||
|
||||
}
|
||||
defaultTranslatedKeys = defaultTranslatedKeys.Select(d => d.Trim()).Distinct().OrderBy(o => o).ToList();
|
||||
JObject obj = new JObject();
|
||||
foreach (var v in defaultTranslatedKeys)
|
||||
{
|
||||
obj.Add(v, "");
|
||||
}
|
||||
|
||||
var path = Path.Combine(soldir.FullName, "BTCPayServer/Services/Translations.Default.cs");
|
||||
var defaultTranslation = File.ReadAllText(path);
|
||||
var startIdx = defaultTranslation.IndexOf("\"\"\"");
|
||||
var endIdx = defaultTranslation.LastIndexOf("\"\"\"");
|
||||
var content = defaultTranslation.Substring(0, startIdx + 3);
|
||||
content += "\n" + String.Join('\n', defaultTranslatedKeys) + "\n";
|
||||
content += "\n" + obj.ToString(Formatting.Indented) + "\n";
|
||||
content += defaultTranslation.Substring(endIdx);
|
||||
File.WriteAllText(path, content);
|
||||
}
|
||||
|
@@ -12,7 +12,6 @@ services:
|
||||
args:
|
||||
CONFIGURATION_NAME: Release
|
||||
environment:
|
||||
TESTS_EXPERIMENTALV2_CONFIRM: "true"
|
||||
TESTS_BTCRPCCONNECTION: server=http://bitcoind:43782;ceiwHEbqWI83:DwubwWsoo3
|
||||
TESTS_LTCRPCCONNECTION: server=http://litecoind:43782;ceiwHEbqWI83:DwubwWsoo3
|
||||
TESTS_BTCNBXPLORERURL: http://nbxplorer:32838/
|
||||
|
@@ -12,7 +12,6 @@ services:
|
||||
args:
|
||||
CONFIGURATION_NAME: Release
|
||||
environment:
|
||||
TESTS_EXPERIMENTALV2_CONFIRM: "true"
|
||||
TESTS_BTCRPCCONNECTION: server=http://bitcoind:43782;ceiwHEbqWI83:DwubwWsoo3
|
||||
TESTS_BTCNBXPLORERURL: http://nbxplorer:32838/
|
||||
TESTS_POSTGRES: User ID=postgres;Include Error Detail=true;Host=postgres;Port=5432;Database=btcpayserver
|
||||
|
@@ -7,12 +7,13 @@
|
||||
<use href="@GetPathTo(Symbol)"></use>
|
||||
</svg>
|
||||
@code {
|
||||
public string GetPathTo(string symbol)
|
||||
[Parameter, EditorRequired]
|
||||
public string Symbol { get; set; }
|
||||
|
||||
private string GetPathTo(string symbol)
|
||||
{
|
||||
var versioned = FileVersionProvider.AddFileVersionToPath(default, "img/icon-sprite.svg");
|
||||
var rootPath = (BTCPayServerOptions.RootPath ?? "/").WithTrailingSlash();
|
||||
return $"{rootPath}{versioned}#{Symbol}";
|
||||
return $"{rootPath}{versioned}#{symbol}";
|
||||
}
|
||||
[Parameter]
|
||||
public string Symbol { get; set; }
|
||||
}
|
||||
|
42
BTCPayServer/Blazor/PosLoginCode.razor
Normal file
42
BTCPayServer/Blazor/PosLoginCode.razor
Normal file
@@ -0,0 +1,42 @@
|
||||
@using Microsoft.AspNetCore.Http
|
||||
|
||||
@inject IHttpContextAccessor HttpContextAccessor;
|
||||
|
||||
@if (Users?.Any() is true)
|
||||
{
|
||||
<div @attributes="Attrs" class="@CssClass">
|
||||
<label for="SignedInUser" class="form-label">Signed in user</label>
|
||||
<select id="SignedInUser" class="form-select" value="@_userId" @onchange="@(e => _userId = e.Value?.ToString())">
|
||||
<option value="">None, just open the URL</option>
|
||||
@foreach (var u in Users)
|
||||
{
|
||||
<option value="@u.Key">@u.Value</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (string.IsNullOrEmpty(_userId))
|
||||
{
|
||||
<QrCode Data="@PosUrl" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<UserLoginCode UserId="@_userId" RedirectUrl="@PosPath" />
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired]
|
||||
public string PosPath { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Dictionary<string,string> Users { get; set; }
|
||||
|
||||
[Parameter(CaptureUnmatchedValues = true)]
|
||||
public Dictionary<string, object> Attrs { get; set; }
|
||||
|
||||
private string _userId;
|
||||
private string PosUrl => Request.GetAbsoluteRoot() + PosPath;
|
||||
private HttpRequest Request => HttpContextAccessor.HttpContext?.Request;
|
||||
private string CssClass => $"form-group {(Attrs?.ContainsKey("class") is true ? Attrs["class"] : "")}".Trim();
|
||||
}
|
29
BTCPayServer/Blazor/QrCode.razor
Normal file
29
BTCPayServer/Blazor/QrCode.razor
Normal file
@@ -0,0 +1,29 @@
|
||||
@using QRCoder
|
||||
|
||||
@if (!string.IsNullOrEmpty(Data))
|
||||
{
|
||||
<img @attributes="Attrs" style="image-rendering:pixelated;image-rendering:-moz-crisp-edges;min-width:@(Size)px;min-height:@(Size)px" src="data:image/png;base64,@(GetBase64(Data))" class="@CssClass" alt="@Data" />
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired]
|
||||
public string Data { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public int Size { get; set; } = 256;
|
||||
|
||||
[Parameter(CaptureUnmatchedValues = true)]
|
||||
public Dictionary<string, object> Attrs { get; set; }
|
||||
|
||||
private static readonly QRCodeGenerator QrGenerator = new();
|
||||
|
||||
private string GetBase64(string data)
|
||||
{
|
||||
var qrCodeData = QrGenerator.CreateQrCode(data, QRCodeGenerator.ECCLevel.Q);
|
||||
var qrCode = new PngByteQRCode(qrCodeData);
|
||||
var bytes = qrCode.GetGraphic(5, [0, 0, 0, 255], [0xf5, 0xf5, 0xf7, 255]);
|
||||
return Convert.ToBase64String(bytes);
|
||||
}
|
||||
|
||||
private string CssClass => $"qr-code {(Attrs?.ContainsKey("class") is true ? Attrs["class"] : "")}".Trim();
|
||||
}
|
100
BTCPayServer/Blazor/UserLoginCode.razor
Normal file
100
BTCPayServer/Blazor/UserLoginCode.razor
Normal file
@@ -0,0 +1,100 @@
|
||||
@using System.Timers
|
||||
@using BTCPayServer.Data
|
||||
@using BTCPayServer.Fido2
|
||||
@using Microsoft.AspNetCore.Http
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@using Microsoft.AspNetCore.Mvc
|
||||
@using Microsoft.AspNetCore.Routing
|
||||
@inject AuthenticationStateProvider AuthenticationStateProvider
|
||||
@inject UserManager<ApplicationUser> UserManager;
|
||||
@inject UserLoginCodeService UserLoginCodeService;
|
||||
@inject LinkGenerator LinkGenerator;
|
||||
@inject IHttpContextAccessor HttpContextAccessor;
|
||||
@implements IDisposable
|
||||
|
||||
@if (!string.IsNullOrEmpty(_data))
|
||||
{
|
||||
<div @attributes="Attrs" class="@CssClass" style="width:@(Size)px">
|
||||
<div class="qr-container mb-2">
|
||||
<QrCode Data="@_data" Size="Size"/>
|
||||
</div>
|
||||
<p class="text-center text-muted mb-1" id="progress">Valid for @_seconds seconds</p>
|
||||
<div class="progress only-for-js" data-bs-toggle="tooltip" data-bs-placement="top">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated @(Percent < 15 ? "bg-warning" : null)" role="progressbar" style="width:@Percent%" id="progressbar"></div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public string UserId { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string RedirectUrl { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public int Size { get; set; } = 256;
|
||||
|
||||
[Parameter(CaptureUnmatchedValues = true)]
|
||||
public Dictionary<string, object> Attrs { get; set; }
|
||||
|
||||
private static readonly double Seconds = UserLoginCodeService.ExpirationTime.TotalSeconds;
|
||||
private double _seconds = Seconds;
|
||||
private string _data;
|
||||
private ApplicationUser _user;
|
||||
private Timer _timer;
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
UserId ??= await GetUserId();
|
||||
if (!string.IsNullOrEmpty(UserId)) _user = await UserManager.FindByIdAsync(UserId);
|
||||
if (_user == null) return;
|
||||
|
||||
GenerateCodeAndStartTimer();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_timer?.Dispose();
|
||||
}
|
||||
|
||||
private void GenerateCodeAndStartTimer()
|
||||
{
|
||||
var loginCode = UserLoginCodeService.GetOrGenerate(_user.Id);
|
||||
_data = GetData(loginCode);
|
||||
_seconds = Seconds;
|
||||
_timer?.Dispose();
|
||||
_timer = new Timer(1000);
|
||||
_timer.Elapsed += CountDownTimer;
|
||||
_timer.Enabled = true;
|
||||
}
|
||||
|
||||
private void CountDownTimer(object source, ElapsedEventArgs e)
|
||||
{
|
||||
if (_seconds > 0)
|
||||
_seconds -= 1;
|
||||
else
|
||||
GenerateCodeAndStartTimer();
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task<string> GetUserId()
|
||||
{
|
||||
var state = await AuthenticationStateProvider.GetAuthenticationStateAsync();
|
||||
return state.User.Identity?.IsAuthenticated is true
|
||||
? UserManager.GetUserId(state.User)
|
||||
: null;
|
||||
}
|
||||
|
||||
private string GetData(string loginCode)
|
||||
{
|
||||
var req = HttpContextAccessor.HttpContext?.Request;
|
||||
if (req == null) return loginCode;
|
||||
return !string.IsNullOrEmpty(RedirectUrl)
|
||||
? LinkGenerator.LoginCodeLink(loginCode, RedirectUrl, req.Scheme, req.Host, req.PathBase)
|
||||
: $"{loginCode};{LinkGenerator.IndexLink(req.Scheme, req.Host, req.PathBase)};{_user.Email}";
|
||||
}
|
||||
|
||||
private double Percent => Math.Round(_seconds / Seconds * 100);
|
||||
private string CssClass => $"user-login-code d-inline-flex flex-column {(Attrs?.ContainsKey("class") is true ? Attrs["class"] : "")}".Trim();
|
||||
}
|
@@ -43,7 +43,7 @@
|
||||
<span text-translate="true">Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
@if (ViewData.IsActivePage([StoreNavPages.General, StoreNavPages.Rates, StoreNavPages.CheckoutAppearance, StoreNavPages.Tokens, StoreNavPages.Users, StoreNavPages.Roles, StoreNavPages.Webhooks, StoreNavPages.PayoutProcessors, StoreNavPages.Emails, StoreNavPages.Forms]))
|
||||
@if (ViewData.IsPageActive([StoreNavPages.General, StoreNavPages.Rates, StoreNavPages.CheckoutAppearance, StoreNavPages.Tokens, StoreNavPages.Users, StoreNavPages.Roles, StoreNavPages.Webhooks, StoreNavPages.PayoutProcessors, StoreNavPages.Emails, StoreNavPages.Forms]))
|
||||
{
|
||||
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyStoreSettings">
|
||||
<a id="StoreNav-@(nameof(StoreNavPages.Rates))" class="nav-link @ViewData.ActivePageClass(StoreNavPages.Rates)" asp-controller="UIStores" asp-action="Rates" asp-route-storeId="@Model.Store.Id" text-translate="true">Rates</a>
|
||||
@@ -104,7 +104,7 @@
|
||||
</a>
|
||||
}
|
||||
</li>
|
||||
@if (ViewData.IsActiveCategory(typeof(WalletsNavPages), scheme.WalletId.ToString()) || ViewData.IsActivePage([WalletsNavPages.Settings], scheme.WalletId.ToString()) || ViewData.IsActivePage([StoreNavPages.OnchainSettings], categoryId))
|
||||
@if (ViewData.IsCategoryActive(typeof(WalletsNavPages), scheme.WalletId.ToString()) || ViewData.IsPageActive([WalletsNavPages.Settings], scheme.WalletId.ToString()) || ViewData.IsPageActive([StoreNavPages.OnchainSettings], categoryId))
|
||||
{
|
||||
<li class="nav-item nav-item-sub">
|
||||
<a id="WalletNav-Send" class="nav-link @ViewData.ActivePageClass([WalletsNavPages.Send, WalletsNavPages.PSBT], scheme.WalletId.ToString())" asp-area="" asp-controller="UIWallets" asp-action="WalletSend" asp-route-walletId="@scheme.WalletId">Send</a>
|
||||
@@ -140,7 +140,7 @@
|
||||
</a>
|
||||
}
|
||||
</li>
|
||||
@if (ViewData.IsActivePage([StoreNavPages.Lightning, StoreNavPages.LightningSettings], $"{Model.Store.Id}-{scheme.CryptoCode}"))
|
||||
@if (ViewData.IsPageActive([StoreNavPages.Lightning, StoreNavPages.LightningSettings], $"{Model.Store.Id}-{scheme.CryptoCode}"))
|
||||
{
|
||||
<li class="nav-item nav-item-sub">
|
||||
<a id="StoreNav-@(nameof(StoreNavPages.LightningSettings))" class="nav-link @ViewData.ActivePageClass(StoreNavPages.LightningSettings)" asp-controller="UIStores" asp-action="LightningSettings" asp-route-storeId="@Model.Store.Id" asp-route-cryptoCode="@scheme.CryptoCode">Settings</a>
|
||||
@@ -290,7 +290,7 @@
|
||||
<span text-translate="true">Server Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
@if (ViewData.IsActiveCategory(typeof(ServerNavPages)) && !ViewData.IsActivePage([ServerNavPages.Plugins]))
|
||||
@if (ViewData.IsCategoryActive(typeof(ServerNavPages)) && !ViewData.IsPageActive([ServerNavPages.Plugins]))
|
||||
{
|
||||
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyServerSettings">
|
||||
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Users" class="nav-link @ViewData.ActivePageClass(ServerNavPages.Users)" asp-action="ListUsers" text-translate="true">Users</a>
|
||||
@@ -379,7 +379,7 @@
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
@if (ViewData.IsActiveCategory(typeof(ManageNavPages)) || ViewData.IsActivePage([ManageNavPages.ChangePassword]))
|
||||
@if (ViewData.IsCategoryActive(typeof(ManageNavPages)) || ViewData.IsPageActive([ManageNavPages.ChangePassword]))
|
||||
{
|
||||
<li class="nav-item nav-item-sub">
|
||||
<a id="SectionNav-@ManageNavPages.ChangePassword.ToString()" class="nav-link @ViewData.ActivePageClass(ManageNavPages.ChangePassword)" asp-controller="UIManage" asp-action="ChangePassword" text-translate="true">Password</a>
|
||||
|
@@ -9,6 +9,7 @@ using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Plugins.Crowdfund;
|
||||
using BTCPayServer.Plugins.PointOfSale;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
@@ -28,12 +29,14 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
public class GreenfieldAppsController : ControllerBase
|
||||
{
|
||||
private readonly AppService _appService;
|
||||
private readonly UriResolver _uriResolver;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly CurrencyNameTable _currencies;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
|
||||
public GreenfieldAppsController(
|
||||
AppService appService,
|
||||
UriResolver uriResolver,
|
||||
StoreRepository storeRepository,
|
||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
CurrencyNameTable currencies,
|
||||
@@ -41,6 +44,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
)
|
||||
{
|
||||
_appService = appService;
|
||||
_uriResolver = uriResolver;
|
||||
_storeRepository = storeRepository;
|
||||
_currencies = currencies;
|
||||
_userManager = userManager;
|
||||
@@ -72,12 +76,12 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
Archived = request.Archived ?? false
|
||||
};
|
||||
|
||||
var settings = ToCrowdfundSettings(request, new CrowdfundSettings { Title = request.Title ?? request.AppName });
|
||||
var settings = ToCrowdfundSettings(request);
|
||||
appData.SetSettings(settings);
|
||||
|
||||
await _appService.UpdateOrCreateApp(appData);
|
||||
|
||||
return Ok(ToCrowdfundModel(appData));
|
||||
var model = await ToCrowdfundModel(appData);
|
||||
return Ok(model);
|
||||
}
|
||||
|
||||
[HttpPost("~/api/v1/stores/{storeId}/apps/pos")]
|
||||
@@ -208,7 +212,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return AppNotFound();
|
||||
}
|
||||
|
||||
return Ok(ToCrowdfundModel(app));
|
||||
var model = await ToCrowdfundModel(app);
|
||||
return Ok(model);
|
||||
}
|
||||
|
||||
[HttpDelete("~/api/v1/apps/{appId}")]
|
||||
@@ -255,7 +260,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return this.CreateAPIError(404, "app-not-found", "The app with specified ID was not found");
|
||||
}
|
||||
|
||||
private CrowdfundSettings ToCrowdfundSettings(CrowdfundAppRequest request, CrowdfundSettings settings)
|
||||
private CrowdfundSettings ToCrowdfundSettings(CrowdfundAppRequest request)
|
||||
{
|
||||
var parsedSounds = ValidateStringArray(request.Sounds);
|
||||
var parsedColors = ValidateStringArray(request.AnimationColors);
|
||||
@@ -271,7 +276,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
Description = request.Description?.Trim(),
|
||||
EndDate = request.EndDate?.UtcDateTime,
|
||||
TargetAmount = request.TargetAmount,
|
||||
MainImageUrl = request.MainImageUrl?.Trim(),
|
||||
MainImageUrl = request.MainImageUrl == null ? null : UnresolvedUri.Create(request.MainImageUrl),
|
||||
NotificationUrl = request.NotificationUrl?.Trim(),
|
||||
Tagline = request.Tagline?.Trim(),
|
||||
PerksTemplate = request.PerksTemplate is not null ? AppService.SerializeTemplate(AppService.Parse(request.PerksTemplate.Trim())) : null,
|
||||
@@ -402,16 +407,16 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
try
|
||||
{
|
||||
// Just checking if we can serialize
|
||||
AppService.SerializeTemplate(AppService.Parse(request.Template));
|
||||
AppService.SerializeTemplate(AppService.Parse(request.Template, true, true));
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.Template), "Invalid template");
|
||||
ModelState.AddModelError(nameof(request.Template), ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private CrowdfundAppData ToCrowdfundModel(AppData appData)
|
||||
private async Task<CrowdfundAppData> ToCrowdfundModel(AppData appData)
|
||||
{
|
||||
var settings = appData.GetSettings<CrowdfundSettings>();
|
||||
Enum.TryParse<CrowdfundResetEvery>(settings.ResetEvery.ToString(), true, out var resetEvery);
|
||||
@@ -432,7 +437,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
Description = settings.Description,
|
||||
EndDate = settings.EndDate,
|
||||
TargetAmount = settings.TargetAmount,
|
||||
MainImageUrl = settings.MainImageUrl,
|
||||
MainImageUrl = settings.MainImageUrl == null ? null : await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), settings.MainImageUrl),
|
||||
NotificationUrl = settings.NotificationUrl,
|
||||
Tagline = settings.Tagline,
|
||||
DisqusEnabled = settings.DisqusEnabled,
|
||||
@@ -486,11 +491,11 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
try
|
||||
{
|
||||
// Just checking if we can serialize
|
||||
AppService.SerializeTemplate(AppService.Parse(request.PerksTemplate));
|
||||
AppService.SerializeTemplate(AppService.Parse(request.PerksTemplate, true, true));
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.PerksTemplate), "Invalid template");
|
||||
ModelState.AddModelError(nameof(request.PerksTemplate), $"Invalid template: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -14,6 +15,7 @@ using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.Payouts;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Security.Greenfield;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
@@ -25,6 +27,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using CreateInvoiceRequest = BTCPayServer.Client.Models.CreateInvoiceRequest;
|
||||
using InvoiceData = BTCPayServer.Client.Models.InvoiceData;
|
||||
@@ -96,11 +99,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[FromQuery] int? take = null
|
||||
)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return StoreNotFound();
|
||||
}
|
||||
var store = HttpContext.GetStoreData()!;
|
||||
if (startDate is DateTimeOffset s &&
|
||||
endDate is DateTimeOffset e &&
|
||||
s > e)
|
||||
@@ -133,17 +132,9 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[HttpGet("~/api/v1/stores/{storeId}/invoices/{invoiceId}")]
|
||||
public async Task<IActionResult> GetInvoice(string storeId, string invoiceId)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return InvoiceNotFound();
|
||||
}
|
||||
|
||||
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
|
||||
if (invoice?.StoreId != store.Id)
|
||||
{
|
||||
if (!BelongsToThisStore(invoice))
|
||||
return InvoiceNotFound();
|
||||
}
|
||||
|
||||
return Ok(ToModel(invoice));
|
||||
}
|
||||
@@ -153,16 +144,9 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[HttpDelete("~/api/v1/stores/{storeId}/invoices/{invoiceId}")]
|
||||
public async Task<IActionResult> ArchiveInvoice(string storeId, string invoiceId)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return InvoiceNotFound();
|
||||
}
|
||||
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
|
||||
if (invoice?.StoreId != store.Id)
|
||||
{
|
||||
if (!BelongsToThisStore(invoice))
|
||||
return InvoiceNotFound();
|
||||
}
|
||||
await _invoiceRepository.ToggleInvoiceArchival(invoiceId, true, storeId);
|
||||
return Ok();
|
||||
}
|
||||
@@ -172,19 +156,10 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[HttpPut("~/api/v1/stores/{storeId}/invoices/{invoiceId}")]
|
||||
public async Task<IActionResult> UpdateInvoice(string storeId, string invoiceId, UpdateInvoiceRequest request)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return InvoiceNotFound();
|
||||
}
|
||||
|
||||
var result = await _invoiceRepository.UpdateInvoiceMetadata(invoiceId, storeId, request.Metadata);
|
||||
if (result != null)
|
||||
{
|
||||
return Ok(ToModel(result));
|
||||
}
|
||||
|
||||
return InvoiceNotFound();
|
||||
if (!BelongsToThisStore(result))
|
||||
return InvoiceNotFound();
|
||||
return Ok(ToModel(result));
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanCreateInvoice,
|
||||
@@ -192,12 +167,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[HttpPost("~/api/v1/stores/{storeId}/invoices")]
|
||||
public async Task<IActionResult> CreateInvoice(string storeId, CreateInvoiceRequest request)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return StoreNotFound();
|
||||
}
|
||||
|
||||
var store = HttpContext.GetStoreData()!;
|
||||
if (request.Amount < 0.0m)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.Amount), "The amount should be 0 or more.");
|
||||
@@ -271,17 +241,9 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
public async Task<IActionResult> MarkInvoiceStatus(string storeId, string invoiceId,
|
||||
MarkInvoiceStatusRequest request)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return InvoiceNotFound();
|
||||
}
|
||||
|
||||
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
|
||||
if (invoice.StoreId != store.Id)
|
||||
{
|
||||
if (!BelongsToThisStore(invoice))
|
||||
return InvoiceNotFound();
|
||||
}
|
||||
|
||||
if (!await _invoiceRepository.MarkInvoiceStatus(invoice.Id, request.Status))
|
||||
{
|
||||
@@ -300,17 +262,9 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[HttpPost("~/api/v1/stores/{storeId}/invoices/{invoiceId}/unarchive")]
|
||||
public async Task<IActionResult> UnarchiveInvoice(string storeId, string invoiceId)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return InvoiceNotFound();
|
||||
}
|
||||
|
||||
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
|
||||
if (invoice.StoreId != store.Id)
|
||||
{
|
||||
if (!BelongsToThisStore(invoice))
|
||||
return InvoiceNotFound();
|
||||
}
|
||||
|
||||
if (!invoice.Archived)
|
||||
{
|
||||
@@ -328,21 +282,23 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[Authorize(Policy = Policies.CanViewInvoices,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods")]
|
||||
public async Task<IActionResult> GetInvoicePaymentMethods(string storeId, string invoiceId, bool onlyAccountedPayments = true)
|
||||
public async Task<IActionResult> GetInvoicePaymentMethods(string storeId, string invoiceId, bool onlyAccountedPayments = true, bool includeSensitive = false)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return InvoiceNotFound();
|
||||
}
|
||||
|
||||
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
|
||||
if (invoice?.StoreId != store.Id)
|
||||
{
|
||||
if (!BelongsToThisStore(invoice))
|
||||
return InvoiceNotFound();
|
||||
}
|
||||
|
||||
return Ok(ToPaymentMethodModels(invoice, onlyAccountedPayments));
|
||||
if (includeSensitive && !await _authorizationService.CanModifyStore(User))
|
||||
return this.CreateAPIPermissionError(Policies.CanModifyStoreSettings);
|
||||
|
||||
return Ok(ToPaymentMethodModels(invoice, onlyAccountedPayments, includeSensitive));
|
||||
}
|
||||
|
||||
bool BelongsToThisStore([NotNullWhen(true)] InvoiceEntity invoice) => BelongsToThisStore(invoice, out _);
|
||||
private bool BelongsToThisStore([NotNullWhen(true)] InvoiceEntity invoice, [MaybeNullWhen(false)] out Data.StoreData store)
|
||||
{
|
||||
store = this.HttpContext.GetStoreData();
|
||||
return invoice?.StoreId is not null && store.Id == invoice.StoreId;
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanViewInvoices,
|
||||
@@ -350,17 +306,9 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[HttpPost("~/api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods/{paymentMethod}/activate")]
|
||||
public async Task<IActionResult> ActivateInvoicePaymentMethod(string storeId, string invoiceId, string paymentMethod)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return InvoiceNotFound();
|
||||
}
|
||||
|
||||
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
|
||||
if (invoice?.StoreId != store.Id)
|
||||
{
|
||||
if (!BelongsToThisStore(invoice))
|
||||
return InvoiceNotFound();
|
||||
}
|
||||
|
||||
if (PaymentMethodId.TryParse(paymentMethod, out var paymentMethodId))
|
||||
{
|
||||
@@ -381,22 +329,9 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return StoreNotFound();
|
||||
}
|
||||
|
||||
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
|
||||
if (invoice == null)
|
||||
{
|
||||
if (!BelongsToThisStore(invoice, out var store))
|
||||
return InvoiceNotFound();
|
||||
}
|
||||
|
||||
if (invoice.StoreId != store.Id)
|
||||
{
|
||||
return InvoiceNotFound();
|
||||
}
|
||||
if (!invoice.GetInvoiceState().CanRefund())
|
||||
{
|
||||
return this.CreateAPIError("non-refundable", "Cannot refund this invoice");
|
||||
@@ -446,7 +381,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
Name = request.Name ?? $"Refund {invoice.Id}",
|
||||
Description = request.Description,
|
||||
StoreId = storeId,
|
||||
PayoutMethodIds = new[] { payoutMethodId },
|
||||
PayoutMethods = new[] { payoutMethodId },
|
||||
};
|
||||
|
||||
if (request.RefundVariant != RefundVariant.Custom)
|
||||
@@ -588,12 +523,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
return this.CreateAPIError(404, "invoice-not-found", "The invoice was not found");
|
||||
}
|
||||
private IActionResult StoreNotFound()
|
||||
{
|
||||
return this.CreateAPIError(404, "store-not-found", "The store was not found");
|
||||
}
|
||||
|
||||
private InvoicePaymentMethodDataModel[] ToPaymentMethodModels(InvoiceEntity entity, bool includeAccountedPaymentOnly)
|
||||
private InvoicePaymentMethodDataModel[] ToPaymentMethodModels(InvoiceEntity entity, bool includeAccountedPaymentOnly, bool includeSensitive)
|
||||
{
|
||||
return entity.GetPaymentPrompts().Select(
|
||||
prompt =>
|
||||
@@ -606,7 +537,12 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
var details = prompt.Details;
|
||||
if (handler is not null && prompt.Activated)
|
||||
details = JToken.FromObject(handler.ParsePaymentPromptDetails(details), handler.Serializer.ForAPI());
|
||||
{
|
||||
var detailsObj = handler.ParsePaymentPromptDetails(details);
|
||||
if (!includeSensitive)
|
||||
handler.StripDetailsForNonOwner(detailsObj);
|
||||
details = JToken.FromObject(detailsObj, handler.Serializer.ForAPI());
|
||||
}
|
||||
return new InvoicePaymentMethodDataModel
|
||||
{
|
||||
Activated = prompt.Activated,
|
||||
@@ -621,7 +557,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
PaymentMethodFee = accounting?.PaymentMethodFee ?? 0m,
|
||||
PaymentLink = (prompt.Activated ? paymentLinkExtension?.GetPaymentLink(prompt, Url) : null) ?? string.Empty,
|
||||
Payments = payments.Select(paymentEntity => ToPaymentModel(entity, paymentEntity)).ToList(),
|
||||
AdditionalData = prompt.Details
|
||||
AdditionalData = details
|
||||
};
|
||||
}).ToArray();
|
||||
}
|
||||
|
@@ -0,0 +1,43 @@
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers.GreenField
|
||||
{
|
||||
[ApiController]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public class GreenfieldObsoleteController : ControllerBase
|
||||
{
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/LNURL")]
|
||||
public IActionResult Obsolete1(string storeId)
|
||||
{
|
||||
return Obsolete();
|
||||
}
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/LNURLPay/{cryptoCode}")]
|
||||
[HttpDelete("~/api/v1/stores/{storeId}/payment-methods/LNURLPay/{cryptoCode}")]
|
||||
[HttpPut("~/api/v1/stores/{storeId}/payment-methods/LNURLPay/{cryptoCode}")]
|
||||
public IActionResult Obsolete2(string storeId, string cryptoCode)
|
||||
{
|
||||
return Obsolete();
|
||||
}
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/LightningNetwork")]
|
||||
public IActionResult Obsolete3(string storeId)
|
||||
{
|
||||
return Obsolete();
|
||||
}
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/LightningNetwork/{cryptoCode}")]
|
||||
[HttpDelete("~/api/v1/stores/{storeId}/payment-methods/LightningNetwork/{cryptoCode}")]
|
||||
[HttpPut("~/api/v1/stores/{storeId}/payment-methods/LightningNetwork/{cryptoCode}")]
|
||||
public IActionResult Obsolete4(string storeId, string cryptoCode)
|
||||
{
|
||||
return Obsolete();
|
||||
}
|
||||
private IActionResult Obsolete()
|
||||
{
|
||||
return this.CreateAPIError(410, "unsupported-in-v2", "This route isn't supported by BTCPay Server 2.0 and newer. Please update your integration.");
|
||||
}
|
||||
}
|
||||
}
|
@@ -36,7 +36,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
Name = factory.Processor,
|
||||
FriendlyName = factory.FriendlyName,
|
||||
PaymentMethods = factory.GetSupportedPayoutMethods().Select(id => id.ToString())
|
||||
PayoutMethods = factory.GetSupportedPayoutMethods().Select(id => id.ToString())
|
||||
.ToArray()
|
||||
}));
|
||||
}
|
||||
|
@@ -132,7 +132,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
ModelState.AddModelError(nameof(request.BOLT11Expiration), $"The BOLT11 expiration should be positive");
|
||||
}
|
||||
PayoutMethodId?[]? payoutMethods = null;
|
||||
if (request.PaymentMethods is { } payoutMethodsStr)
|
||||
if (request.PayoutMethods is { } payoutMethodsStr)
|
||||
{
|
||||
payoutMethods = payoutMethodsStr.Select(s =>
|
||||
{
|
||||
@@ -144,13 +144,13 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
if (!supported.Contains(payoutMethods[i]))
|
||||
{
|
||||
request.AddModelError(paymentRequest => paymentRequest.PaymentMethods[i], "Invalid or unsupported payment method", this);
|
||||
request.AddModelError(paymentRequest => paymentRequest.PayoutMethods[i], "Invalid or unsupported payment method", this);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.PaymentMethods), "This field is required");
|
||||
ModelState.AddModelError(nameof(request.PayoutMethods), "This field is required");
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
@@ -364,16 +364,17 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
Id = p.Id,
|
||||
PullPaymentId = p.PullPaymentDataId,
|
||||
Date = p.Date,
|
||||
Amount = p.OriginalAmount,
|
||||
PaymentMethodAmount = p.Amount,
|
||||
OriginalCurrency = p.OriginalCurrency,
|
||||
OriginalAmount = p.OriginalAmount,
|
||||
PayoutCurrency = p.Currency,
|
||||
PayoutAmount = p.Amount,
|
||||
Revision = blob.Revision,
|
||||
State = p.State,
|
||||
PayoutMethodId = p.PayoutMethodId,
|
||||
PaymentProof = p.GetProofBlobJson(),
|
||||
Destination = blob.Destination,
|
||||
Metadata = blob.Metadata?? new JObject(),
|
||||
};
|
||||
model.Destination = blob.Destination;
|
||||
model.PayoutMethodId = p.PayoutMethodId;
|
||||
model.CryptoCode = p.Currency;
|
||||
model.PaymentProof = p.GetProofBlobJson();
|
||||
return model;
|
||||
}
|
||||
|
||||
|
@@ -28,7 +28,7 @@ public class GreenfieldServerRolesController : ControllerBase
|
||||
[HttpGet("~/api/v1/server/roles")]
|
||||
public async Task<IActionResult> GetServerRoles()
|
||||
{
|
||||
return Ok(FromModel(await _storeRepository.GetStoreRoles(null, false, false)));
|
||||
return Ok(FromModel(await _storeRepository.GetStoreRoles(null, false)));
|
||||
}
|
||||
private List<RoleData> FromModel(StoreRepository.StoreRole[] data)
|
||||
{
|
||||
|
@@ -46,7 +46,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
Stores = new[] { storeId },
|
||||
Processors = new[] { LightningAutomatedPayoutSenderFactory.ProcessorName },
|
||||
PayoutMethodIds = paymentMethodId is null ? null : new[] { paymentMethodId }
|
||||
PayoutMethods = paymentMethodId is null ? null : new[] { paymentMethodId }
|
||||
});
|
||||
|
||||
return Ok(configured.Select(ToModel).ToArray());
|
||||
@@ -88,7 +88,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
Stores = new[] { storeId },
|
||||
Processors = new[] { LightningAutomatedPayoutSenderFactory.ProcessorName },
|
||||
PayoutMethodIds = new[] { pmi }
|
||||
PayoutMethods = new[] { pmi }
|
||||
}))
|
||||
.FirstOrDefault();
|
||||
activeProcessor ??= new PayoutProcessorData();
|
||||
|
@@ -47,7 +47,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
Stores = new[] { storeId },
|
||||
Processors = new[] { OnChainAutomatedPayoutSenderFactory.ProcessorName },
|
||||
PayoutMethodIds = paymentMethodId is null ? null : new[] { paymentMethodId }
|
||||
PayoutMethods = paymentMethodId is null ? null : new[] { paymentMethodId }
|
||||
});
|
||||
|
||||
return Ok(configured.Select(ToModel).ToArray());
|
||||
@@ -94,7 +94,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
Stores = new[] { storeId },
|
||||
Processors = new[] { OnChainAutomatedPayoutSenderFactory.ProcessorName },
|
||||
PayoutMethodIds = new[] { payoutMethodId }
|
||||
PayoutMethods = new[] { payoutMethodId }
|
||||
}))
|
||||
.FirstOrDefault();
|
||||
activeProcessor ??= new PayoutProcessorData();
|
||||
|
@@ -145,9 +145,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
if (includeConfig is true)
|
||||
{
|
||||
var canModifyStore = (await _authorizationService.AuthorizeAsync(User, null,
|
||||
new PolicyRequirement(Policies.CanModifyStoreSettings))).Succeeded;
|
||||
if (!canModifyStore)
|
||||
if (!await _authorizationService.CanModifyStore(User))
|
||||
return this.CreateAPIPermissionError(Policies.CanModifyStoreSettings);
|
||||
}
|
||||
|
||||
|
@@ -39,7 +39,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
Name = datas.Key,
|
||||
FriendlyName = _factories.FirstOrDefault(factory => factory.Processor == datas.Key)?.FriendlyName,
|
||||
PaymentMethods = datas.Select(data => data.PayoutMethodId).ToArray()
|
||||
PayoutMethods = datas.Select(data => data.PayoutMethodId).ToArray()
|
||||
});
|
||||
return Ok(configured);
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
Stores = new[] { storeId },
|
||||
Processors = new[] { processor },
|
||||
PayoutMethodIds = new[] { PayoutMethodId.Parse(paymentMethod) }
|
||||
PayoutMethods = new[] { PayoutMethodId.Parse(paymentMethod) }
|
||||
})).FirstOrDefault();
|
||||
if (matched is null)
|
||||
{
|
||||
|
@@ -31,7 +31,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
var store = HttpContext.GetStoreData();
|
||||
return store == null
|
||||
? StoreNotFound()
|
||||
: Ok(FromModel(await _storeRepository.GetStoreRoles(storeId, false, false)));
|
||||
: Ok(FromModel(await _storeRepository.GetStoreRoles(storeId, false)));
|
||||
}
|
||||
|
||||
private List<RoleData> FromModel(StoreRepository.StoreRole[] data)
|
||||
|
@@ -831,10 +831,12 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
}
|
||||
|
||||
public override async Task<InvoicePaymentMethodDataModel[]> GetInvoicePaymentMethods(string storeId,
|
||||
string invoiceId, CancellationToken token = default)
|
||||
string invoiceId,
|
||||
bool onlyAccountedPayments = true, bool includeSensitive = false,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return GetFromActionResult<InvoicePaymentMethodDataModel[]>(
|
||||
await GetController<GreenfieldInvoiceController>().GetInvoicePaymentMethods(storeId, invoiceId));
|
||||
await GetController<GreenfieldInvoiceController>().GetInvoicePaymentMethods(storeId, invoiceId, onlyAccountedPayments, includeSensitive));
|
||||
}
|
||||
|
||||
public override async Task ArchiveInvoice(string storeId, string invoiceId, CancellationToken token = default)
|
||||
|
@@ -123,15 +123,30 @@ namespace BTCPayServer.Controllers
|
||||
return View(nameof(Login), new LoginViewModel { Email = email });
|
||||
}
|
||||
|
||||
// GET is for signin via the POS backend
|
||||
[HttpGet("/login/code")]
|
||||
[AllowAnonymous]
|
||||
[RateLimitsFilter(ZoneLimits.Login, Scope = RateLimitsScope.RemoteAddress)]
|
||||
public async Task<IActionResult> LoginUsingCode(string loginCode, string returnUrl = null)
|
||||
{
|
||||
return await LoginCodeResult(loginCode, returnUrl);
|
||||
}
|
||||
|
||||
[HttpPost("/login/code")]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
[RateLimitsFilter(ZoneLimits.Login, Scope = RateLimitsScope.RemoteAddress)]
|
||||
public async Task<IActionResult> LoginWithCode(string loginCode, string returnUrl = null)
|
||||
{
|
||||
return await LoginCodeResult(loginCode, returnUrl);
|
||||
}
|
||||
|
||||
private async Task<IActionResult> LoginCodeResult(string loginCode, string returnUrl)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(loginCode))
|
||||
{
|
||||
var userId = _userLoginCodeService.Verify(loginCode);
|
||||
var code = loginCode.Split(';').First();
|
||||
var userId = _userLoginCodeService.Verify(code);
|
||||
if (userId is null)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "Login code was invalid";
|
||||
|
@@ -24,12 +24,14 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
public UIAppsController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
StoreRepository storeRepository,
|
||||
IFileService fileService,
|
||||
AppService appService,
|
||||
IHtmlHelper html)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_networkProvider = networkProvider;
|
||||
_storeRepository = storeRepository;
|
||||
_fileService = fileService;
|
||||
_appService = appService;
|
||||
@@ -37,6 +39,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly BTCPayNetworkProvider _networkProvider;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly IFileService _fileService;
|
||||
private readonly AppService _appService;
|
||||
@@ -133,6 +136,20 @@ namespace BTCPayServer.Controllers
|
||||
public async Task<IActionResult> CreateApp(string storeId, CreateAppViewModel vm)
|
||||
{
|
||||
var store = GetCurrentStore();
|
||||
if (store == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
if (!store.AnyPaymentMethodAvailable())
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Html = $"To create a {vm.AppType} app, you need to <a href='{Url.Action(nameof(UIStoresController.SetupWallet), "UIStores", new { cryptoCode = _networkProvider.DefaultNetwork.CryptoCode, storeId })}' class='alert-link'>set up a wallet</a> first",
|
||||
AllowDismiss = false
|
||||
});
|
||||
return View(vm);
|
||||
}
|
||||
vm.StoreId = store.Id;
|
||||
var type = _appService.GetAppType(vm.AppType ?? vm.SelectedAppType);
|
||||
if (type is null)
|
||||
|
@@ -413,7 +413,7 @@ namespace BTCPayServer.Controllers
|
||||
createPullPayment = new CreatePullPayment
|
||||
{
|
||||
Name = $"Refund {invoice.Id}",
|
||||
PayoutMethodIds = new[] { pmi },
|
||||
PayoutMethods = new[] { pmi },
|
||||
StoreId = invoice.StoreId,
|
||||
BOLT11Expiration = store.GetStoreBlob().RefundBOLT11Expiration
|
||||
};
|
||||
|
@@ -1,21 +1,12 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class UIManageController
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> LoginCodes()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
namespace BTCPayServer.Controllers;
|
||||
|
||||
return View(nameof(LoginCodes), _userLoginCodeService.GetOrGenerate(user.Id));
|
||||
}
|
||||
public partial class UIManageController
|
||||
{
|
||||
[HttpGet]
|
||||
public ActionResult LoginCodes()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
}
|
||||
|
@@ -40,7 +40,6 @@ namespace BTCPayServer.Controllers
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly Fido2Service _fido2Service;
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
private readonly UserLoginCodeService _userLoginCodeService;
|
||||
private readonly IHtmlHelper Html;
|
||||
private readonly UserService _userService;
|
||||
private readonly UriResolver _uriResolver;
|
||||
@@ -62,7 +61,6 @@ namespace BTCPayServer.Controllers
|
||||
UserService userService,
|
||||
UriResolver uriResolver,
|
||||
IFileService fileService,
|
||||
UserLoginCodeService userLoginCodeService,
|
||||
IHtmlHelper htmlHelper
|
||||
)
|
||||
{
|
||||
@@ -76,7 +74,6 @@ namespace BTCPayServer.Controllers
|
||||
_authorizationService = authorizationService;
|
||||
_fido2Service = fido2Service;
|
||||
_linkGenerator = linkGenerator;
|
||||
_userLoginCodeService = userLoginCodeService;
|
||||
Html = htmlHelper;
|
||||
_userService = userService;
|
||||
_uriResolver = uriResolver;
|
||||
|
@@ -19,7 +19,7 @@ namespace BTCPayServer.Controllers
|
||||
string sortOrder = null
|
||||
)
|
||||
{
|
||||
var roles = await _StoreRepository.GetStoreRoles(null, true);
|
||||
var roles = await _StoreRepository.GetStoreRoles(null);
|
||||
var defaultRole = (await _StoreRepository.GetDefaultRole()).Role;
|
||||
model ??= new RolesViewModel();
|
||||
model.DefaultRole = defaultRole;
|
||||
|
@@ -150,7 +150,7 @@ namespace BTCPayServer.Controllers
|
||||
Amount = model.Amount,
|
||||
Currency = model.Currency,
|
||||
StoreId = storeId,
|
||||
PayoutMethodIds = selectedPaymentMethodIds,
|
||||
PayoutMethods = selectedPaymentMethodIds,
|
||||
BOLT11Expiration = TimeSpan.FromDays(model.BOLT11Expiration),
|
||||
AutoApproveClaims = model.AutoApproveClaims
|
||||
});
|
||||
@@ -586,7 +586,7 @@ namespace BTCPayServer.Controllers
|
||||
private async Task<bool> HasPayoutProcessor(string storeId, PayoutMethodId payoutMethodId)
|
||||
{
|
||||
var processors = await _payoutProcessorService.GetProcessors(
|
||||
new PayoutProcessorService.PayoutProcessorQuery { Stores = [storeId], PayoutMethodIds = [payoutMethodId] });
|
||||
new PayoutProcessorService.PayoutProcessorQuery { Stores = [storeId], PayoutMethods = [payoutMethodId] });
|
||||
return _payoutProcessorFactories.Any(factory => factory.GetSupportedPayoutMethods().Contains(payoutMethodId)) && processors.Any();
|
||||
}
|
||||
private async Task<bool> HasPayoutProcessor(string storeId, string payoutMethodId)
|
||||
|
@@ -437,15 +437,10 @@ public partial class UIStoresController
|
||||
}).ToList(),
|
||||
Config = ProtectString(JToken.FromObject(derivation, handler.Serializer).ToString()),
|
||||
PayJoinEnabled = storeBlob.PayJoinEnabled,
|
||||
MonitoringExpiration = (int)storeBlob.MonitoringExpiration.TotalMinutes,
|
||||
SpeedPolicy = store.SpeedPolicy,
|
||||
ShowRecommendedFee = storeBlob.ShowRecommendedFee,
|
||||
RecommendedFeeBlockTarget = storeBlob.RecommendedFeeBlockTarget,
|
||||
CanUsePayJoin = canUseHotWallet && network.SupportPayJoin && derivation.IsHotWallet,
|
||||
CanUseHotWallet = canUseHotWallet,
|
||||
CanUseRPCImport = rpcImport,
|
||||
CanUsePayJoin = canUseHotWallet && network.SupportPayJoin && derivation.IsHotWallet,
|
||||
StoreName = store.StoreName,
|
||||
|
||||
StoreName = store.StoreName
|
||||
};
|
||||
|
||||
ViewData["ReplaceDescription"] = WalletReplaceWarning(derivation.IsHotWallet);
|
||||
@@ -473,15 +468,14 @@ public partial class UIStoresController
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var excludeFilters = storeBlob.GetExcludedPaymentMethods();
|
||||
var currentlyEnabled = !excludeFilters.Match(handler.PaymentMethodId);
|
||||
bool enabledChanged = currentlyEnabled != vm.Enabled;
|
||||
bool needUpdate = enabledChanged;
|
||||
var enabledChanged = currentlyEnabled != vm.Enabled;
|
||||
var payjoinChanged = storeBlob.PayJoinEnabled != vm.PayJoinEnabled;
|
||||
var needUpdate = enabledChanged || payjoinChanged;
|
||||
string errorMessage = null;
|
||||
|
||||
if (enabledChanged)
|
||||
{
|
||||
storeBlob.SetExcluded(handler.PaymentMethodId, !vm.Enabled);
|
||||
store.SetStoreBlob(storeBlob);
|
||||
}
|
||||
if (enabledChanged) storeBlob.SetExcluded(handler.PaymentMethodId, !vm.Enabled);
|
||||
if (payjoinChanged && network.SupportPayJoin) storeBlob.PayJoinEnabled = vm.PayJoinEnabled;
|
||||
if (needUpdate) store.SetStoreBlob(storeBlob);
|
||||
|
||||
if (derivation.Label != vm.Label)
|
||||
{
|
||||
@@ -552,6 +546,15 @@ public partial class UIStoresController
|
||||
_eventAggregator.Publish(new WalletChangedEvent { WalletId = new WalletId(vm.StoreId, vm.CryptoCode) });
|
||||
successMessage += $" {vm.CryptoCode} on-chain payments are now {(vm.Enabled ? "enabled" : "disabled")} for this store.";
|
||||
}
|
||||
|
||||
if (payjoinChanged && storeBlob.PayJoinEnabled && network.SupportPayJoin)
|
||||
{
|
||||
var config = store.GetPaymentMethodConfig<DerivationSchemeSettings>(PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode), _handlers);
|
||||
if (config?.IsHotWallet is not true)
|
||||
{
|
||||
successMessage += " However, PayJoin will not work, as this isn't a <a href='https://docs.btcpayserver.org/HotWallet/' class='alert-link' target='_blank'>hot wallet</a>.";
|
||||
}
|
||||
}
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = successMessage;
|
||||
}
|
||||
@@ -564,65 +567,6 @@ public partial class UIStoresController
|
||||
return RedirectToAction(nameof(WalletSettings), new { vm.StoreId, vm.CryptoCode });
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/onchain/{cryptoCode}/settings/payment")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> UpdatePaymentSettings(WalletSettingsViewModel vm)
|
||||
{
|
||||
var checkResult = IsAvailable(vm.CryptoCode, out var store, out var network);
|
||||
if (checkResult != null)
|
||||
{
|
||||
return checkResult;
|
||||
}
|
||||
|
||||
var derivation = GetExistingDerivationStrategy(vm.CryptoCode, store);
|
||||
if (derivation == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
bool needUpdate = false;
|
||||
var blob = store.GetStoreBlob();
|
||||
var payjoinChanged = blob.PayJoinEnabled != vm.PayJoinEnabled;
|
||||
blob.MonitoringExpiration = TimeSpan.FromMinutes(vm.MonitoringExpiration);
|
||||
blob.ShowRecommendedFee = vm.ShowRecommendedFee;
|
||||
blob.RecommendedFeeBlockTarget = vm.RecommendedFeeBlockTarget;
|
||||
blob.PayJoinEnabled = vm.PayJoinEnabled;
|
||||
|
||||
if (store.SetStoreBlob(blob))
|
||||
{
|
||||
needUpdate = true;
|
||||
}
|
||||
|
||||
if (store.SpeedPolicy != vm.SpeedPolicy)
|
||||
{
|
||||
store.SpeedPolicy = vm.SpeedPolicy;
|
||||
needUpdate = true;
|
||||
}
|
||||
|
||||
if (needUpdate)
|
||||
{
|
||||
await _storeRepo.UpdateStore(store);
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Payment settings successfully updated";
|
||||
|
||||
if (payjoinChanged && blob.PayJoinEnabled && network.SupportPayJoin)
|
||||
{
|
||||
var config = store.GetPaymentMethodConfig<DerivationSchemeSettings>(PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode), _handlers);
|
||||
if (config?.IsHotWallet is not true)
|
||||
{
|
||||
TempData.Remove(WellKnownTempData.SuccessMessage);
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Warning,
|
||||
Html = "The payment settings were updated successfully. However, PayJoin will not work, as this isn't a <a href='https://docs.btcpayserver.org/HotWallet/' class='alert-link' target='_blank'>hot wallet</a>."
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(WalletSettings), new { vm.StoreId, vm.CryptoCode });
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/onchain/{cryptoCode}/seed")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> WalletSeed(string storeId, string cryptoCode, CancellationToken cancellationToken = default)
|
||||
|
@@ -21,7 +21,7 @@ public partial class UIStoresController
|
||||
string sortOrder = null
|
||||
)
|
||||
{
|
||||
var roles = await storeRepository.GetStoreRoles(storeId, true);
|
||||
var roles = await storeRepository.GetStoreRoles(storeId);
|
||||
var defaultRole = (await storeRepository.GetDefaultRole()).Role;
|
||||
model ??= new RolesViewModel();
|
||||
model.DefaultRole = defaultRole;
|
||||
|
@@ -20,11 +20,11 @@ namespace BTCPayServer.Controllers;
|
||||
public partial class UIStoresController
|
||||
{
|
||||
[HttpGet("{storeId}/settings")]
|
||||
public async Task<IActionResult> GeneralSettings()
|
||||
public async Task<IActionResult> GeneralSettings(string storeId)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
if (store == null) return NotFound();
|
||||
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var vm = new GeneralSettingsViewModel
|
||||
{
|
||||
@@ -41,7 +41,11 @@ public partial class UIStoresController
|
||||
InvoiceExpiration = (int)storeBlob.InvoiceExpiration.TotalMinutes,
|
||||
DefaultCurrency = storeBlob.DefaultCurrency,
|
||||
BOLT11Expiration = (long)storeBlob.RefundBOLT11Expiration.TotalDays,
|
||||
Archived = store.Archived
|
||||
Archived = store.Archived,
|
||||
MonitoringExpiration = (int)storeBlob.MonitoringExpiration.TotalMinutes,
|
||||
SpeedPolicy = store.SpeedPolicy,
|
||||
ShowRecommendedFee = storeBlob.ShowRecommendedFee,
|
||||
RecommendedFeeBlockTarget = storeBlob.RecommendedFeeBlockTarget
|
||||
};
|
||||
|
||||
return View(vm);
|
||||
@@ -67,13 +71,22 @@ public partial class UIStoresController
|
||||
CurrentStore.StoreWebsite = model.StoreWebsite;
|
||||
}
|
||||
|
||||
if (CurrentStore.SpeedPolicy != model.SpeedPolicy)
|
||||
{
|
||||
CurrentStore.SpeedPolicy = model.SpeedPolicy;
|
||||
needUpdate = true;
|
||||
}
|
||||
|
||||
var blob = CurrentStore.GetStoreBlob();
|
||||
blob.AnyoneCanInvoice = model.AnyoneCanCreateInvoice;
|
||||
blob.NetworkFeeMode = model.NetworkFeeMode;
|
||||
blob.PaymentTolerance = model.PaymentTolerance;
|
||||
blob.DefaultCurrency = model.DefaultCurrency;
|
||||
blob.ShowRecommendedFee = model.ShowRecommendedFee;
|
||||
blob.RecommendedFeeBlockTarget = model.RecommendedFeeBlockTarget;
|
||||
blob.InvoiceExpiration = TimeSpan.FromMinutes(model.InvoiceExpiration);
|
||||
blob.RefundBOLT11Expiration = TimeSpan.FromDays(model.BOLT11Expiration);
|
||||
blob.MonitoringExpiration = TimeSpan.FromMinutes(model.MonitoringExpiration);
|
||||
if (!string.IsNullOrEmpty(model.BrandColor) && !ColorPalette.IsValid(model.BrandColor))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.BrandColor), "The brand color needs to be a valid hex color code");
|
||||
|
@@ -32,7 +32,7 @@ namespace BTCPayServer.Data
|
||||
#nullable enable
|
||||
public static PayoutMethodId? GetClosestPayoutMethodId(this InvoiceData invoice, IEnumerable<PayoutMethodId> pmids)
|
||||
{
|
||||
var paymentMethodIds = invoice.Payments.Select(o => o.GetPaymentMethodId()).ToArray();
|
||||
var paymentMethodIds = invoice.Payments.Select(o => PaymentMethodId.Parse(o.PaymentMethodId)).ToArray();
|
||||
if (paymentMethodIds.Length == 0)
|
||||
paymentMethodIds = invoice.GetBlob().GetPaymentPrompts().Select(p => p.PaymentMethodId).ToArray();
|
||||
return PaymentMethodId.GetSimilarities(pmids, paymentMethodIds)
|
||||
|
@@ -38,16 +38,12 @@ namespace BTCPayServer.Data
|
||||
paymentData.Blob2 = JToken.FromObject(blob, InvoiceDataExtensions.DefaultSerializer).ToString(Newtonsoft.Json.Formatting.None);
|
||||
return paymentData;
|
||||
}
|
||||
public static PaymentMethodId GetPaymentMethodId(this PaymentData paymentData)
|
||||
{
|
||||
return PaymentMethodId.Parse(paymentData.PaymentMethodId);
|
||||
}
|
||||
public static PaymentEntity GetBlob(this PaymentData paymentData)
|
||||
{
|
||||
var entity = JToken.Parse(paymentData.Blob2).ToObject<PaymentEntity>(InvoiceDataExtensions.DefaultSerializer) ?? throw new FormatException($"Invalid {nameof(PaymentEntity)}");
|
||||
entity.Status = paymentData.Status!.Value;
|
||||
entity.Currency = paymentData.Currency;
|
||||
entity.PaymentMethodId = GetPaymentMethodId(paymentData);
|
||||
entity.PaymentMethodId = PaymentMethodId.Parse(paymentData.PaymentMethodId);
|
||||
entity.Value = paymentData.Amount!.Value;
|
||||
entity.Id = paymentData.Id;
|
||||
entity.ReceivedTime = paymentData.Created!.Value;
|
||||
|
@@ -9,26 +9,7 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
public static PaymentRequestBaseData GetBlob(this PaymentRequestData paymentRequestData)
|
||||
{
|
||||
if (paymentRequestData.Blob2 is not null)
|
||||
{
|
||||
return paymentRequestData.HasTypedBlob<PaymentRequestBaseData>().GetBlob();
|
||||
}
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
else if (paymentRequestData.Blob is not null)
|
||||
{
|
||||
return ParseBlob(paymentRequestData.Blob);
|
||||
}
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
return new PaymentRequestBaseData();
|
||||
}
|
||||
|
||||
static PaymentRequestBaseData ParseBlob(byte[] blob)
|
||||
{
|
||||
var jobj = JObject.Parse(ZipUtils.Unzip(blob));
|
||||
// Fixup some legacy payment requests
|
||||
if (jobj["expiryDate"].Type == JTokenType.Date)
|
||||
jobj["expiryDate"] = new JValue(NBitcoin.Utils.DateTimeToUnixTime(jobj["expiryDate"].Value<DateTime>()));
|
||||
return jobj.ToObject<PaymentRequestBaseData>();
|
||||
return paymentRequestData.HasTypedBlob<PaymentRequestBaseData>().GetBlob() ?? new PaymentRequestBaseData();
|
||||
}
|
||||
|
||||
public static void SetBlob(this PaymentRequestData paymentRequestData, PaymentRequestBaseData blob)
|
||||
|
@@ -2,6 +2,7 @@ using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Security.Bitpay;
|
||||
using BTCPayServer.Security.Greenfield;
|
||||
using BTCPayServer.Services;
|
||||
@@ -12,6 +13,11 @@ namespace BTCPayServer
|
||||
{
|
||||
public static class AuthorizationExtensions
|
||||
{
|
||||
public static async Task<bool> CanModifyStore(this IAuthorizationService authorizationService, ClaimsPrincipal user)
|
||||
{
|
||||
return (await authorizationService.AuthorizeAsync(user, null,
|
||||
new PolicyRequirement(Policies.CanModifyStoreSettings))).Succeeded;
|
||||
}
|
||||
public static async Task<(bool HotWallet, bool RPCImport)> CanUseHotWallet(
|
||||
this IAuthorizationService authorizationService,
|
||||
PoliciesSettings policiesSettings,
|
||||
|
@@ -23,7 +23,7 @@ public static class SettingsRepositoryExtensions
|
||||
}
|
||||
return new IssuerKey(issuerKey);
|
||||
}
|
||||
internal static AESKey FixedKey()
|
||||
public static AESKey FixedKey()
|
||||
{
|
||||
byte[] v = new byte[16];
|
||||
v[0] = 1;
|
||||
|
@@ -3,7 +3,6 @@ using System;
|
||||
using BTCPayServer;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
|
||||
@@ -52,6 +51,12 @@ namespace Microsoft.AspNetCore.Mvc
|
||||
{
|
||||
return urlHelper.GetUriByAction(nameof(UIAccountController.Login), "UIAccount", null , scheme, host, pathbase);
|
||||
}
|
||||
|
||||
public static string LoginCodeLink(this LinkGenerator urlHelper, string loginCode, string returnUrl, string scheme, HostString host, string pathbase)
|
||||
{
|
||||
return urlHelper.GetUriByAction(nameof(UIAccountController.LoginUsingCode), "UIAccount", new { loginCode, returnUrl }, scheme, host, pathbase);
|
||||
}
|
||||
|
||||
public static string ResetPasswordLink(this LinkGenerator urlHelper, string userId, string code, string scheme, HostString host, string pathbase)
|
||||
{
|
||||
return urlHelper.GetUriByAction(
|
||||
@@ -109,5 +114,14 @@ namespace Microsoft.AspNetCore.Mvc
|
||||
values: new { storeId = wallet?.StoreId ?? walletIdOrStoreId, pullPaymentId, payoutState },
|
||||
scheme, host, pathbase);
|
||||
}
|
||||
|
||||
public static string IndexLink(this LinkGenerator urlHelper, string scheme, HostString host, string pathbase)
|
||||
{
|
||||
return urlHelper.GetUriByAction(
|
||||
action: nameof(UIHomeController.Index),
|
||||
controller: "UIHome",
|
||||
values: null,
|
||||
scheme, host, pathbase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -8,6 +8,7 @@ namespace BTCPayServer.Fido2
|
||||
public class UserLoginCodeService
|
||||
{
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
public static readonly TimeSpan ExpirationTime = TimeSpan.FromSeconds(60);
|
||||
|
||||
public UserLoginCodeService(IMemoryCache memoryCache)
|
||||
{
|
||||
@@ -29,10 +30,10 @@ namespace BTCPayServer.Fido2
|
||||
}
|
||||
return _memoryCache.GetOrCreate(GetCacheKey(userId), entry =>
|
||||
{
|
||||
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1);
|
||||
entry.AbsoluteExpirationRelativeToNow = ExpirationTime;
|
||||
var code = Encoders.Hex.EncodeData(RandomUtils.GetBytes(20));
|
||||
using var newEntry = _memoryCache.CreateEntry(code);
|
||||
newEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1);
|
||||
newEntry.AbsoluteExpirationRelativeToNow = ExpirationTime;
|
||||
newEntry.Value = userId;
|
||||
|
||||
return code;
|
||||
|
@@ -192,13 +192,20 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Wait(string invoiceId) => await Wait(await _invoiceRepository.GetInvoice(invoiceId));
|
||||
private async Task Wait(InvoiceEntity invoice)
|
||||
private async Task Wait(string invoiceId, bool startup) => await Wait(await _invoiceRepository.GetInvoice(invoiceId), startup);
|
||||
private async Task Wait(InvoiceEntity invoice, bool startup)
|
||||
{
|
||||
var startupOffset = TimeSpan.Zero;
|
||||
|
||||
// This give some times for the pollers in the listeners to catch payments which happened
|
||||
// while the server was down.
|
||||
if (startup)
|
||||
startupOffset += TimeSpan.FromMinutes(2.0);
|
||||
|
||||
try
|
||||
{
|
||||
// add 1 second to ensure watch won't trigger moments before invoice expires
|
||||
var delay = invoice.ExpirationTime.AddSeconds(1) - DateTimeOffset.UtcNow;
|
||||
var delay = (invoice.ExpirationTime.AddSeconds(1) + startupOffset) - DateTimeOffset.UtcNow;
|
||||
if (delay > TimeSpan.Zero)
|
||||
{
|
||||
await Task.Delay(delay, _Cts.Token);
|
||||
@@ -243,7 +250,7 @@ namespace BTCPayServer.HostedServices
|
||||
if (b.Name == InvoiceEvent.Created)
|
||||
{
|
||||
Watch(b.Invoice.Id);
|
||||
_ = Wait(b.Invoice.Id);
|
||||
_ = Wait(b.Invoice.Id, false);
|
||||
}
|
||||
|
||||
if (b.Name == InvoiceEvent.ReceivedPayment)
|
||||
@@ -257,7 +264,7 @@ namespace BTCPayServer.HostedServices
|
||||
private async Task WaitPendingInvoices()
|
||||
{
|
||||
await Task.WhenAll((await GetPendingInvoices(_Cts.Token))
|
||||
.Select(i => Wait(i)).ToArray());
|
||||
.Select(i => Wait(i, true)).ToArray());
|
||||
}
|
||||
|
||||
private async Task<InvoiceEntity[]> GetPendingInvoices(CancellationToken cancellationToken)
|
||||
|
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Migrations;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public class PaymentRequestsMigratorHostedService : BlobMigratorHostedService<PaymentRequestData>
|
||||
{
|
||||
public PaymentRequestsMigratorHostedService(
|
||||
ILogger<PaymentRequestsMigratorHostedService> logs,
|
||||
ISettingsRepository settingsRepository,
|
||||
ApplicationDbContextFactory applicationDbContextFactory) : base(logs, settingsRepository, applicationDbContextFactory)
|
||||
{
|
||||
}
|
||||
public override string SettingsKey => "PaymentRequestsMigration";
|
||||
|
||||
protected override IQueryable<PaymentRequestData> GetQuery(ApplicationDbContext ctx, DateTimeOffset? progress)
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
var query = progress is DateTimeOffset last2 ?
|
||||
ctx.PaymentRequests.Where(i => i.Created < last2 && !(i.Blob == null && i.Blob2 != null)) :
|
||||
ctx.PaymentRequests.Where(i => !(i.Blob == null && i.Blob2 != null));
|
||||
return query.OrderByDescending(i => i.Created);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
|
||||
protected override async Task PostMigrationCleanup(ApplicationDbContext ctx, CancellationToken cancellationToken)
|
||||
{
|
||||
Logs.LogInformation("Post-migration VACUUM (FULL, ANALYZE)");
|
||||
await ctx.Database.ExecuteSqlRawAsync("VACUUM (FULL, ANALYZE) \"PaymentRequests\"", cancellationToken);
|
||||
Logs.LogInformation("Post-migration VACUUM (FULL, ANALYZE) finished");
|
||||
}
|
||||
|
||||
protected override DateTimeOffset ProcessEntities(ApplicationDbContext ctx, List<PaymentRequestData> entities)
|
||||
{
|
||||
// The PaymentRequestData.Migrate() is automatically called by EF.
|
||||
// But Modified isn't set as it happens before the ctx is bound to the entity.
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
ctx.PaymentRequests.Entry(entity).State = EntityState.Modified;
|
||||
}
|
||||
return entities[^1].Created;
|
||||
}
|
||||
|
||||
protected override Task Reindex(ApplicationDbContext ctx, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
@@ -39,7 +39,7 @@ namespace BTCPayServer.HostedServices
|
||||
public string Description { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
public string Currency { get; set; }
|
||||
public PayoutMethodId[] PayoutMethodIds { get; set; }
|
||||
public PayoutMethodId[] PayoutMethods { get; set; }
|
||||
public bool AutoApproveClaims { get; set; }
|
||||
public TimeSpan? BOLT11Expiration { get; set; }
|
||||
}
|
||||
@@ -119,7 +119,7 @@ namespace BTCPayServer.HostedServices
|
||||
Amount = request.Amount,
|
||||
Currency = request.Currency,
|
||||
StoreId = storeId,
|
||||
PayoutMethodIds = request.PaymentMethods.Select(p => PayoutMethodId.Parse(p)).ToArray(),
|
||||
PayoutMethods = request.PayoutMethods.Select(p => PayoutMethodId.Parse(p)).ToArray(),
|
||||
AutoApproveClaims = request.AutoApproveClaims
|
||||
});
|
||||
}
|
||||
@@ -143,7 +143,7 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
Name = create.Name ?? string.Empty,
|
||||
Description = create.Description ?? string.Empty,
|
||||
SupportedPayoutMethods = create.PayoutMethodIds,
|
||||
SupportedPayoutMethods = create.PayoutMethods,
|
||||
AutoApproveClaims = create.AutoApproveClaims,
|
||||
View = new PullPaymentBlob.PullPaymentView
|
||||
{
|
||||
|
@@ -89,6 +89,7 @@ namespace BTCPayServer.Hosting
|
||||
services.TryAddSingleton<IHtmlLocalizerFactory, LocalizerFactory>();
|
||||
services.TryAddSingleton<LocalizerService>();
|
||||
services.TryAddSingleton<ViewLocalizer>();
|
||||
services.TryAddSingleton<IStringLocalizer>(o => o.GetRequiredService<IStringLocalizerFactory>().Create("",""));
|
||||
|
||||
services.AddSingleton<MvcNewtonsoftJsonOptions>(o => o.GetRequiredService<IOptions<MvcNewtonsoftJsonOptions>>().Value);
|
||||
services.AddSingleton<JsonSerializerSettings>(o => o.GetRequiredService<IOptions<MvcNewtonsoftJsonOptions>>().Value.SerializerSettings);
|
||||
@@ -578,6 +579,9 @@ o.GetRequiredService<IEnumerable<IPaymentLinkExtension>>().ToDictionary(o => o.P
|
||||
services.AddSingleton<InvoiceBlobMigratorHostedService>();
|
||||
services.AddSingleton<IHostedService, InvoiceBlobMigratorHostedService>(o => o.GetRequiredService<InvoiceBlobMigratorHostedService>());
|
||||
|
||||
services.AddSingleton<PaymentRequestsMigratorHostedService>();
|
||||
services.AddSingleton<IHostedService, PaymentRequestsMigratorHostedService>(o => o.GetRequiredService<PaymentRequestsMigratorHostedService>());
|
||||
|
||||
// Broken
|
||||
// Providers.Add("argoneum", new ArgoneumRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_ARGONEUM")));
|
||||
|
||||
|
@@ -64,7 +64,7 @@ namespace BTCPayServer.Hosting
|
||||
continue;
|
||||
}
|
||||
var savedHash = dictionary.Metadata.ToObject<DictionaryFileMetadata>().Hash;
|
||||
var translations = Translations.CreateFromText(File.ReadAllText(file));
|
||||
var translations = Translations.CreateFromJson(File.ReadAllText(file));
|
||||
var currentHash = new uint256(SHA256.HashData(Encoding.UTF8.GetBytes(translations.ToJsonFormat())));
|
||||
|
||||
if (savedHash != currentHash)
|
||||
|
@@ -211,12 +211,6 @@ namespace BTCPayServer.Hosting
|
||||
settings.MigrateToStoreConfig = true;
|
||||
await _Settings.UpdateSetting(settings);
|
||||
}
|
||||
if (!settings.MigratePayoutProcessors)
|
||||
{
|
||||
await MigratePayoutProcessors();
|
||||
settings.MigratePayoutProcessors = true;
|
||||
await _Settings.UpdateSetting(settings);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -225,17 +219,6 @@ namespace BTCPayServer.Hosting
|
||||
}
|
||||
}
|
||||
|
||||
private async Task MigratePayoutProcessors()
|
||||
{
|
||||
await using var ctx = _DBContextFactory.CreateContext();
|
||||
var processors = await ctx.PayoutProcessors.ToArrayAsync();
|
||||
foreach (var processor in processors)
|
||||
{
|
||||
processor.PayoutMethodId = processor.GetPayoutMethodId().ToString();
|
||||
}
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private async Task MigrateToStoreConfig()
|
||||
{
|
||||
await using var ctx = _DBContextFactory.CreateContext();
|
||||
|
@@ -59,5 +59,19 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
[Display(Name = "Minimum acceptable expiration time for BOLT11 for refunds")]
|
||||
[Range(0, 365 * 10)]
|
||||
public long BOLT11Expiration { get; set; }
|
||||
|
||||
[Display(Name = "Show recommended fee")]
|
||||
public bool ShowRecommendedFee { get; set; }
|
||||
|
||||
[Display(Name = "Recommended fee confirmation target blocks")]
|
||||
[Range(1, double.PositiveInfinity)]
|
||||
public int RecommendedFeeBlockTarget { get; set; }
|
||||
|
||||
[Display(Name = "Payment invalid if transactions fails to confirm … after invoice expiration")]
|
||||
[Range(10, 60 * 24 * 24)]
|
||||
public int MonitoringExpiration { get; set; }
|
||||
|
||||
[Display(Name = "Consider the invoice settled when the payment transaction …")]
|
||||
public SpeedPolicy SpeedPolicy { get; set; }
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using BTCPayServer.Client.Models;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Models.StoreViewModels
|
||||
@@ -16,20 +15,6 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
[Display(Name = "Enable Payjoin/P2EP")]
|
||||
public bool PayJoinEnabled { get; set; }
|
||||
|
||||
[Display(Name = "Show recommended fee")]
|
||||
public bool ShowRecommendedFee { get; set; }
|
||||
|
||||
[Display(Name = "Recommended fee confirmation target blocks")]
|
||||
[Range(1, double.PositiveInfinity)]
|
||||
public int RecommendedFeeBlockTarget { get; set; }
|
||||
|
||||
[Display(Name = "Payment invalid if transactions fails to confirm … after invoice expiration")]
|
||||
[Range(10, 60 * 24 * 24)]
|
||||
public int MonitoringExpiration { get; set; }
|
||||
|
||||
[Display(Name = "Consider the invoice settled when the payment transaction …")]
|
||||
public SpeedPolicy SpeedPolicy { get; set; }
|
||||
|
||||
public string Label { get; set; }
|
||||
|
||||
public string DerivationSchemeInput { get; set; }
|
||||
|
@@ -37,8 +37,6 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
|
||||
public List<PullPaymentModel> PullPayments { get; set; } = new List<PullPaymentModel>();
|
||||
public override int CurrentPageCount => PullPayments.Count;
|
||||
public string PaymentMethodId { get; set; }
|
||||
public IEnumerable<PaymentMethodId> PaymentMethods { get; set; }
|
||||
public PullPaymentState ActiveState { get; set; } = PullPaymentState.Active;
|
||||
}
|
||||
|
||||
|
@@ -89,7 +89,10 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
{
|
||||
return ParsePaymentMethodConfig(config);
|
||||
}
|
||||
|
||||
public void StripDetailsForNonOwner(object details)
|
||||
{
|
||||
((BitcoinPaymentPromptDetails)details).AccountDerivation = null;
|
||||
}
|
||||
public async Task AfterSavingInvoice(PaymentMethodContext paymentMethodContext)
|
||||
{
|
||||
var paymentPrompt = paymentMethodContext.Prompt;
|
||||
|
@@ -14,6 +14,9 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public NetworkFeeMode FeeMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The fee rate charged to the user as `PaymentMethodFee`.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.FeeRateJsonConverter))]
|
||||
public FeeRate PaymentMethodFeeRate
|
||||
{
|
||||
@@ -21,6 +24,10 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
set;
|
||||
}
|
||||
public bool PayjoinEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The recommended fee rate for this payment method.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.FeeRateJsonConverter))]
|
||||
public FeeRate RecommendedFeeRate { get; set; }
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.KeyPathJsonConverter))]
|
||||
|
@@ -67,6 +67,12 @@ namespace BTCPayServer.Payments
|
||||
/// <param name="details"></param>
|
||||
/// <returns></returns>
|
||||
object ParsePaymentPromptDetails(JToken details);
|
||||
/// <summary>
|
||||
/// Remove properties from the details which shouldn't appear to non-store owner.
|
||||
/// </summary>
|
||||
/// <param name="details">Prompt details</param>
|
||||
void StripDetailsForNonOwner(object details) { }
|
||||
|
||||
/// <summary>
|
||||
/// Parse the configuration of the payment method in the store
|
||||
/// </summary>
|
||||
@@ -115,9 +121,11 @@ namespace BTCPayServer.Payments
|
||||
}
|
||||
foreach (var paymentMethodConfig in store.GetPaymentMethodConfigs())
|
||||
{
|
||||
var ctx = new PaymentMethodContext(store, storeBlob, paymentMethodConfig.Value, handlers[paymentMethodConfig.Key], invoiceEntity, invoiceLogs);
|
||||
if (!handlers.TryGetValue(paymentMethodConfig.Key, out var handler))
|
||||
continue;
|
||||
var ctx = new PaymentMethodContext(store, storeBlob, paymentMethodConfig.Value, handler, invoiceEntity, invoiceLogs);
|
||||
PaymentMethodContexts.Add(paymentMethodConfig.Key, ctx);
|
||||
if (excludeFilter.Match(paymentMethodConfig.Key) || !handlers.Support(paymentMethodConfig.Key))
|
||||
if (excludeFilter.Match(paymentMethodConfig.Key))
|
||||
ctx.Status = PaymentMethodContext.ContextStatus.Excluded;
|
||||
}
|
||||
}
|
||||
|
@@ -121,12 +121,7 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Li
|
||||
var processorBlob = GetBlob(PayoutProcessorSettings);
|
||||
var lightningSupportedPaymentMethod = (LightningPaymentMethodConfig)paymentMethodConfig;
|
||||
if (lightningSupportedPaymentMethod.IsInternalNode &&
|
||||
!(await Task.WhenAll((await _storeRepository.GetStoreUsers(PayoutProcessorSettings.StoreId))
|
||||
.Where(user =>
|
||||
user.StoreRole.ToPermissionSet(PayoutProcessorSettings.StoreId)
|
||||
.Contains(Policies.CanModifyStoreSettings, PayoutProcessorSettings.StoreId))
|
||||
.Select(user => user.Id)
|
||||
.Select(s => _userService.IsAdminUser(s)))).Any(b => b))
|
||||
!await _storeRepository.InternalNodePayoutAuthorized(PayoutProcessorSettings.StoreId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
@@ -54,7 +54,7 @@ public class UILightningAutomatedPayoutProcessorsController : Controller
|
||||
{
|
||||
Stores = new[] { storeId },
|
||||
Processors = new[] { _lightningAutomatedPayoutSenderFactory.Processor },
|
||||
PayoutMethodIds = new[]
|
||||
PayoutMethods = new[]
|
||||
{
|
||||
PayoutTypes.LN.GetPayoutMethodId(cryptoCode)
|
||||
}
|
||||
@@ -88,7 +88,7 @@ public class UILightningAutomatedPayoutProcessorsController : Controller
|
||||
{
|
||||
Stores = new[] { storeId },
|
||||
Processors = new[] { _lightningAutomatedPayoutSenderFactory.Processor },
|
||||
PayoutMethodIds = new[]
|
||||
PayoutMethods = new[]
|
||||
{
|
||||
PayoutTypes.LN.GetPayoutMethodId(cryptoCode)
|
||||
}
|
||||
|
@@ -65,7 +65,7 @@ public class UIOnChainAutomatedPayoutProcessorsController : Controller
|
||||
{
|
||||
Stores = new[] { storeId },
|
||||
Processors = new[] { _onChainAutomatedPayoutSenderFactory.Processor },
|
||||
PayoutMethodIds = new[]
|
||||
PayoutMethods = new[]
|
||||
{
|
||||
PayoutTypes.CHAIN.GetPayoutMethodId(cryptoCode)
|
||||
}
|
||||
@@ -98,7 +98,7 @@ public class UIOnChainAutomatedPayoutProcessorsController : Controller
|
||||
{
|
||||
Stores = new[] { storeId },
|
||||
Processors = new[] { OnChainAutomatedPayoutSenderFactory.ProcessorName },
|
||||
PayoutMethodIds = new[]
|
||||
PayoutMethods = new[]
|
||||
{
|
||||
PayoutTypes.CHAIN.GetPayoutMethodId(cryptoCode)
|
||||
}
|
||||
|
@@ -53,11 +53,11 @@ public class PayoutProcessorService : EventHostedServiceBase
|
||||
public PayoutProcessorQuery(string storeId, PayoutMethodId payoutMethodId)
|
||||
{
|
||||
Stores = new[] { storeId };
|
||||
PayoutMethodIds = new[] { payoutMethodId };
|
||||
PayoutMethods = new[] { payoutMethodId };
|
||||
}
|
||||
public string[] Stores { get; set; }
|
||||
public string[] Processors { get; set; }
|
||||
public PayoutMethodId[] PayoutMethodIds { get; set; }
|
||||
public PayoutMethodId[] PayoutMethods { get; set; }
|
||||
}
|
||||
|
||||
public async Task<List<PayoutProcessorData>> GetProcessors(PayoutProcessorQuery query)
|
||||
@@ -73,9 +73,9 @@ public class PayoutProcessorService : EventHostedServiceBase
|
||||
{
|
||||
queryable = queryable.Where(data => query.Stores.Contains(data.StoreId));
|
||||
}
|
||||
if (query.PayoutMethodIds is not null)
|
||||
if (query.PayoutMethods is not null)
|
||||
{
|
||||
var paymentMethods = query.PayoutMethodIds.Select(d => d.ToString()).Distinct().ToArray();
|
||||
var paymentMethods = query.PayoutMethods.Select(d => d.ToString()).Distinct().ToArray();
|
||||
queryable = queryable.Where(data => paymentMethods.Contains(data.PayoutMethodId));
|
||||
}
|
||||
|
||||
|
@@ -4,8 +4,10 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
@@ -44,6 +46,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
|
||||
EventAggregator eventAggregator,
|
||||
UriResolver uriResolver,
|
||||
StoreRepository storeRepository,
|
||||
IFileService fileService,
|
||||
UIInvoiceController invoiceController,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
FormDataService formDataService,
|
||||
@@ -53,6 +56,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
|
||||
_appService = appService;
|
||||
_userManager = userManager;
|
||||
_app = app;
|
||||
_fileService = fileService;
|
||||
_storeRepository = storeRepository;
|
||||
_eventAggregator = eventAggregator;
|
||||
_uriResolver = uriResolver;
|
||||
@@ -61,6 +65,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
|
||||
}
|
||||
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
private readonly IFileService _fileService;
|
||||
private readonly UriResolver _uriResolver;
|
||||
private readonly CurrencyNameTable _currencies;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
@@ -393,6 +398,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
|
||||
|
||||
var settings = app.GetSettings<CrowdfundSettings>();
|
||||
var resetEvery = Enum.GetName(typeof(CrowdfundResetEvery), settings.ResetEvery);
|
||||
|
||||
var vm = new UpdateCrowdfundViewModel
|
||||
{
|
||||
Title = settings.Title,
|
||||
@@ -405,8 +411,8 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
|
||||
EnforceTargetAmount = settings.EnforceTargetAmount,
|
||||
StartDate = settings.StartDate,
|
||||
TargetCurrency = settings.TargetCurrency,
|
||||
MainImageUrl = settings.MainImageUrl == null ? null : await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), settings.MainImageUrl),
|
||||
Description = settings.Description,
|
||||
MainImageUrl = settings.MainImageUrl,
|
||||
EndDate = settings.EndDate,
|
||||
TargetAmount = settings.TargetAmount,
|
||||
NotificationUrl = settings.NotificationUrl,
|
||||
@@ -434,8 +440,13 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[HttpPost("{appId}/settings/crowdfund")]
|
||||
public async Task<IActionResult> UpdateCrowdfund(string appId, UpdateCrowdfundViewModel vm, string command)
|
||||
public async Task<IActionResult> UpdateCrowdfund(string appId, UpdateCrowdfundViewModel vm,
|
||||
[FromForm] bool RemoveLogoFile = false)
|
||||
{
|
||||
var userId = GetUserId();
|
||||
if (userId is null)
|
||||
return NotFound();
|
||||
|
||||
var app = GetCurrentApp();
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
@@ -447,11 +458,11 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
|
||||
|
||||
try
|
||||
{
|
||||
vm.PerksTemplate = AppService.SerializeTemplate(AppService.Parse(vm.PerksTemplate));
|
||||
vm.PerksTemplate = AppService.SerializeTemplate(AppService.Parse(vm.PerksTemplate, true, true));
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.PerksTemplate), "Invalid template");
|
||||
ModelState.AddModelError(nameof(vm.PerksTemplate), $"Invalid template: {ex.Message}");
|
||||
}
|
||||
if (vm.TargetAmount is decimal v && v == 0.0m)
|
||||
{
|
||||
@@ -503,6 +514,16 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
|
||||
parsedAnimationColors = new CrowdfundSettings().AnimationColors;
|
||||
}
|
||||
|
||||
UploadImageResultModel imageUpload = null;
|
||||
if (vm.MainImageFile != null)
|
||||
{
|
||||
imageUpload = await _fileService.UploadImage(vm.MainImageFile, userId);
|
||||
if (!imageUpload.Success)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.MainImageFile), imageUpload.Response);
|
||||
}
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View("Crowdfund/UpdateCrowdfund", vm);
|
||||
@@ -520,7 +541,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
|
||||
Description = vm.Description,
|
||||
EndDate = vm.EndDate?.ToUniversalTime(),
|
||||
TargetAmount = vm.TargetAmount,
|
||||
MainImageUrl = vm.MainImageUrl,
|
||||
MainImageUrl = app.GetSettings<CrowdfundSettings>()?.MainImageUrl,
|
||||
NotificationUrl = vm.NotificationUrl,
|
||||
Tagline = vm.Tagline,
|
||||
PerksTemplate = vm.PerksTemplate,
|
||||
@@ -538,6 +559,17 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
|
||||
FormId = vm.FormId
|
||||
};
|
||||
|
||||
if (imageUpload?.Success is true)
|
||||
{
|
||||
newSettings.MainImageUrl = new UnresolvedUri.FileIdUri(imageUpload.StoredFile.Id);
|
||||
}
|
||||
else if (RemoveLogoFile)
|
||||
{
|
||||
newSettings.MainImageUrl = null;
|
||||
vm.MainImageUrl = null;
|
||||
vm.MainImageFile = null;
|
||||
}
|
||||
|
||||
app.TagAllInvoices = vm.UseAllStoreInvoices;
|
||||
app.SetSettings(newSettings);
|
||||
|
||||
|
@@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Abstractions.Services;
|
||||
using BTCPayServer.Client.Models;
|
||||
@@ -49,22 +50,28 @@ namespace BTCPayServer.Plugins.Crowdfund
|
||||
private readonly IOptions<BTCPayServerOptions> _options;
|
||||
private readonly DisplayFormatter _displayFormatter;
|
||||
private readonly CurrencyNameTable _currencyNameTable;
|
||||
private readonly UriResolver _uriResolver;
|
||||
private readonly InvoiceRepository _invoiceRepository;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly PrettyNameProvider _prettyNameProvider;
|
||||
public const string AppType = "Crowdfund";
|
||||
|
||||
public CrowdfundAppType(
|
||||
LinkGenerator linkGenerator,
|
||||
IOptions<BTCPayServerOptions> options,
|
||||
UriResolver uriResolver,
|
||||
InvoiceRepository invoiceRepository,
|
||||
PrettyNameProvider prettyNameProvider,
|
||||
DisplayFormatter displayFormatter,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
CurrencyNameTable currencyNameTable)
|
||||
{
|
||||
Description = Type = AppType;
|
||||
_linkGenerator = linkGenerator;
|
||||
_options = options;
|
||||
_uriResolver = uriResolver;
|
||||
_displayFormatter = displayFormatter;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_currencyNameTable = currencyNameTable;
|
||||
_invoiceRepository = invoiceRepository;
|
||||
_prettyNameProvider = prettyNameProvider;
|
||||
@@ -186,12 +193,11 @@ namespace BTCPayServer.Plugins.Crowdfund
|
||||
? _linkGenerator.GetPathByAction(nameof(UICrowdfundController.CrowdfundForm), "UICrowdfund",
|
||||
new { appId = appData.Id }, _options.Value.RootPath)
|
||||
: null;
|
||||
return new ViewCrowdfundViewModel
|
||||
var vm = new ViewCrowdfundViewModel
|
||||
{
|
||||
Title = settings.Title,
|
||||
Tagline = settings.Tagline,
|
||||
Description = settings.Description,
|
||||
MainImageUrl = settings.MainImageUrl,
|
||||
StoreName = store.StoreName,
|
||||
StoreId = appData.StoreDataId,
|
||||
AppId = appData.Id,
|
||||
@@ -230,6 +236,12 @@ namespace BTCPayServer.Plugins.Crowdfund
|
||||
CurrentAmount = currentPayments.TotalCurrency
|
||||
}
|
||||
};
|
||||
var httpContext = _httpContextAccessor.HttpContext;
|
||||
if (httpContext != null && settings.MainImageUrl != null)
|
||||
{
|
||||
vm.MainImageUrl = await _uriResolver.Resolve(httpContext.Request.GetAbsoluteRootUri(), settings.MainImageUrl);
|
||||
}
|
||||
return vm;
|
||||
}
|
||||
|
||||
private Dictionary<string, PaymentStat> GetPaymentStats(InvoiceStatistics stats)
|
||||
|
@@ -4,6 +4,8 @@ using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Validation;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Plugins.Crowdfund.Models
|
||||
{
|
||||
@@ -32,6 +34,10 @@ namespace BTCPayServer.Plugins.Crowdfund.Models
|
||||
[Display(Name = "Featured Image URL")]
|
||||
public string MainImageUrl { get; set; }
|
||||
|
||||
[Display(Name = "Featured Image URL")]
|
||||
[JsonIgnore]
|
||||
public IFormFile MainImageFile { get; set; }
|
||||
|
||||
[Display(Name = "Callback Notification URL")]
|
||||
[Uri]
|
||||
public string NotificationUrl { get; set; }
|
||||
|
@@ -627,6 +627,8 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
}
|
||||
|
||||
vm.ExampleCallback = "{\n \"id\":\"SkdsDghkdP3D3qkj7bLq3\",\n \"url\":\"https://btcpay.example.com/invoice?id=SkdsDghkdP3D3qkj7bLq3\",\n \"status\":\"paid\",\n \"price\":10,\n \"currency\":\"EUR\",\n \"invoiceTime\":1520373130312,\n \"expirationTime\":1520374030312,\n \"currentTime\":1520373179327,\n \"exceptionStatus\":false,\n \"buyerFields\":{\n \"buyerEmail\":\"customer@example.com\",\n \"buyerNotify\":false\n },\n \"paymentSubtotals\": {\n \"BTC\":114700\n },\n \"paymentTotals\": {\n \"BTC\":118400\n },\n \"transactionCurrency\": \"BTC\",\n \"amountPaid\": \"1025900\",\n \"exchangeRates\": {\n \"BTC\": {\n \"EUR\": 8721.690715789999,\n \"USD\": 10817.99\n }\n }\n}";
|
||||
|
||||
await FillUsers(vm);
|
||||
return View("PointOfSale/UpdatePointOfSale", vm);
|
||||
}
|
||||
|
||||
@@ -647,14 +649,15 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
ModelState.AddModelError(nameof(vm.Currency), "Invalid currency");
|
||||
try
|
||||
{
|
||||
vm.Template = AppService.SerializeTemplate(AppService.Parse(vm.Template));
|
||||
vm.Template = AppService.SerializeTemplate(AppService.Parse(vm.Template, true, true));
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Template), "Invalid template");
|
||||
ModelState.AddModelError(nameof(vm.Template), $"Invalid template: {ex.Message}");
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
await FillUsers(vm);
|
||||
return View("PointOfSale/UpdatePointOfSale", vm);
|
||||
}
|
||||
|
||||
@@ -715,5 +718,11 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
private StoreData GetCurrentStore() => HttpContext.GetStoreData();
|
||||
|
||||
private AppData GetCurrentApp() => HttpContext.GetAppData();
|
||||
|
||||
private async Task FillUsers(UpdatePointOfSaleViewModel vm)
|
||||
{
|
||||
var users = await _storeRepository.GetStoreUsers(GetCurrentStore().Id);
|
||||
vm.StoreUsers = users.Select(u => (u.Id, u.Email, u.StoreRole.Role)).ToDictionary(u => u.Id, u => $"{u.Email} ({u.Role})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -68,6 +68,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Models
|
||||
public string CustomTipPercentages { get; set; }
|
||||
|
||||
public string Id { get; set; }
|
||||
public Dictionary<string, string> StoreUsers { get; set; }
|
||||
|
||||
[Display(Name = "Redirect invoice to redirect url automatically after paid")]
|
||||
public string RedirectAutomatically { get; set; } = string.Empty;
|
||||
|
@@ -44,13 +44,6 @@ namespace BTCPayServer
|
||||
confBuilder.AddJsonFile("appsettings.dev.json", true, false);
|
||||
#endif
|
||||
conf = confBuilder.Build();
|
||||
|
||||
|
||||
var confirm = conf.GetOrDefault<bool>("EXPERIMENTALV2_CONFIRM", false);
|
||||
if(!confirm)
|
||||
{
|
||||
throw new ConfigException("You are running an experimental version of BTCPay Server that is the basis for v2. Many things will change and break, including irreversible database migrations. THERE IS NO WAY BACK. Please confirm you understand this by setting the setting EXPERIMENTALV2_CONFIRM=true");
|
||||
}
|
||||
var builder = new WebHostBuilder()
|
||||
.UseKestrel()
|
||||
.UseConfiguration(conf)
|
||||
|
@@ -4,7 +4,6 @@
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"BTCPAY_EXPERIMENTALV2_CONFIRM": "true",
|
||||
"BTCPAY_NETWORK": "regtest",
|
||||
"BTCPAY_LAUNCHSETTINGS": "true",
|
||||
"BTCPAY_BTCLIGHTNING": "type=clightning;server=tcp://127.0.0.1:30993/",
|
||||
@@ -38,7 +37,6 @@
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"BTCPAY_EXPERIMENTALV2_CONFIRM": "true",
|
||||
"BTCPAY_NETWORK": "regtest",
|
||||
"BTCPAY_LAUNCHSETTINGS": "true",
|
||||
"BTCPAY_PORT": "14142",
|
||||
@@ -76,7 +74,6 @@
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"BTCPAY_EXPERIMENTALV2_CONFIRM": "true",
|
||||
"BTCPAY_NETWORK": "regtest",
|
||||
"BTCPAY_LAUNCHSETTINGS": "true",
|
||||
"BTCPAY_PORT": "14142",
|
||||
@@ -117,7 +114,6 @@
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"BTCPAY_EXPERIMENTALV2_CONFIRM": "true",
|
||||
"BTCPAY_NETWORK": "regtest",
|
||||
"BTCPAY_LAUNCHSETTINGS": "true",
|
||||
"BTCPAY_PORT": "14142",
|
||||
|
@@ -106,15 +106,15 @@ namespace BTCPayServer.Services.Apps
|
||||
Date = entities.Key,
|
||||
Label = entities.Key.ToString("MMM dd", CultureInfo.InvariantCulture),
|
||||
SalesCount = entities.Count()
|
||||
});
|
||||
}).ToList();
|
||||
|
||||
// fill up the gaps
|
||||
foreach (var i in Enumerable.Range(0, numberOfDays))
|
||||
{
|
||||
var date = (DateTimeOffset.UtcNow - TimeSpan.FromDays(i)).Date;
|
||||
if (!series.Any(e => e.Date == date))
|
||||
if (series.All(e => e.Date != date))
|
||||
{
|
||||
series = series.Append(new AppSalesStatsItem
|
||||
series.Add(new AppSalesStatsItem
|
||||
{
|
||||
Date = date,
|
||||
Label = date.ToString("MMM dd", CultureInfo.InvariantCulture)
|
||||
@@ -125,7 +125,7 @@ namespace BTCPayServer.Services.Apps
|
||||
return Task.FromResult(new AppSalesStats
|
||||
{
|
||||
SalesCount = series.Sum(i => i.SalesCount),
|
||||
Series = series.OrderBy(i => i.Label)
|
||||
Series = series.OrderBy(i => i.Date)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -348,12 +348,17 @@ namespace BTCPayServer.Services.Apps
|
||||
{
|
||||
return JsonConvert.SerializeObject(items, Formatting.Indented, _defaultSerializer);
|
||||
}
|
||||
public static ViewPointOfSaleViewModel.Item[] Parse(string template, bool includeDisabled = true)
|
||||
public static ViewPointOfSaleViewModel.Item[] Parse(string template, bool includeDisabled = true, bool throws = false)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(template))
|
||||
return Array.Empty<ViewPointOfSaleViewModel.Item>();
|
||||
|
||||
return JsonConvert.DeserializeObject<ViewPointOfSaleViewModel.Item[]>(template, _defaultSerializer)!.Where(item => includeDisabled || !item.Disabled).ToArray();
|
||||
if (string.IsNullOrWhiteSpace(template)) return [];
|
||||
var allItems = JsonConvert.DeserializeObject<ViewPointOfSaleViewModel.Item[]>(template, _defaultSerializer)!;
|
||||
// ensure all items have an id, which is also unique
|
||||
var itemsWithoutId = allItems.Where(i => string.IsNullOrEmpty(i.Id)).ToList();
|
||||
if (itemsWithoutId.Any() && throws) throw new ArgumentException($"Missing ID for item \"{itemsWithoutId.First().Title}\".");
|
||||
// find items with duplicate IDs
|
||||
var duplicateIds = allItems.GroupBy(i => i.Id).Where(g => g.Count() > 1).Select(g => g.Key).ToList();
|
||||
if (duplicateIds.Any() && throws) throw new ArgumentException($"Duplicate ID \"{duplicateIds.First()}\".");
|
||||
return allItems.Where(item => (includeDisabled || !item.Disabled) && !itemsWithoutId.Contains(item) && !duplicateIds.Contains(item.Id)).ToArray();
|
||||
}
|
||||
#nullable restore
|
||||
#nullable enable
|
||||
|
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Services.Apps
|
||||
{
|
||||
@@ -27,7 +29,8 @@ namespace BTCPayServer.Services.Apps
|
||||
}
|
||||
|
||||
public bool EnforceTargetAmount { get; set; }
|
||||
public string MainImageUrl { get; set; }
|
||||
[JsonConverter(typeof(UnresolvedUriJsonConverter))]
|
||||
public UnresolvedUri MainImageUrl { get; set; }
|
||||
public string NotificationUrl { get; set; }
|
||||
public string Tagline { get; set; }
|
||||
public string PerksTemplate { get; set; }
|
||||
|
@@ -871,14 +871,18 @@ namespace BTCPayServer.Services.Invoices
|
||||
[JsonIgnore]
|
||||
public decimal Rate => Currency is null ? throw new InvalidOperationException("Currency of the payment prompt isn't set") : ParentEntity.GetInvoiceRate(Currency);
|
||||
public int Divisibility { get; set; }
|
||||
/// <summary>
|
||||
/// Total additional fee imposed by this specific payment method.
|
||||
/// It includes the <see cref="TweakFee"/>.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal PaymentMethodFee { get; set; }
|
||||
/// <summary>
|
||||
/// A fee, hidden from UI, meant to be used when a payment method has a service provider which
|
||||
/// An additional fee, hidden from UI, meant to be used when a payment method has a service provider which
|
||||
/// have a different way of converting the invoice's amount into the currency of the payment method.
|
||||
/// This fee can avoid under/over payments when this case happens.
|
||||
///
|
||||
/// Please use <see cref="AddTweakFee(decimal)"/> so that the tweak fee is also added to the <see cref="PaymentMethodFee"/>.
|
||||
/// You need to increment it with <see cref="AddTweakFee(decimal)"/> so that the tweak fee is also added to the <see cref="PaymentMethodFee"/>.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal TweakFee { get; set; }
|
||||
|
@@ -104,27 +104,31 @@ namespace BTCPayServer.Services.Invoices
|
||||
/// <returns></returns>
|
||||
public async Task<InvoiceEntity[]> GetMonitoredInvoices(PaymentMethodId paymentMethodId, bool includeNonActivated, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var pmi = paymentMethodId.ToString();
|
||||
using var ctx = _applicationDbContextFactory.CreateContext();
|
||||
var conn = ctx.Database.GetDbConnection();
|
||||
|
||||
string includeNonActivateQuery = String.Empty;
|
||||
if (includeNonActivated)
|
||||
includeNonActivateQuery = " AND (get_prompt(i.\"Blob2\", @pmi)->'activated')::BOOLEAN IS NOT FALSE)";
|
||||
|
||||
var rows = await conn.QueryAsync<(string Id, uint xmin, string[] addresses, string[] payments, string invoice)>(new("""
|
||||
WITH invoices_payments AS (
|
||||
SELECT
|
||||
i."Id",
|
||||
i.xmin,
|
||||
array_agg(ai."Address") addresses,
|
||||
COALESCE(array_agg(to_jsonb(p)) FILTER (WHERE p."Id" IS NOT NULL), '{}') as payments,
|
||||
(array_agg(to_jsonb(i)))[1] as invoice
|
||||
m.invoice_id,
|
||||
array_agg(to_jsonb(p)) FILTER (WHERE p."Id" IS NOT NULL) as payments
|
||||
FROM get_monitored_invoices(@pmi, @includeNonActivated) m
|
||||
LEFT JOIN "Payments" p ON p."Id" = m.payment_id AND p."PaymentMethodId" = m.payment_method_id
|
||||
LEFT JOIN "Invoices" i ON i."Id" = m.invoice_id
|
||||
LEFT JOIN "AddressInvoices" ai ON i."Id" = ai."InvoiceDataId"
|
||||
GROUP BY 1
|
||||
),
|
||||
invoices_addresses AS (
|
||||
SELECT m.invoice_id,
|
||||
array_agg(ai."Address") addresses
|
||||
FROM get_monitored_invoices(@pmi, @includeNonActivated) m
|
||||
JOIN "AddressInvoices" ai ON ai."InvoiceDataId" = m.invoice_id
|
||||
WHERE ai."PaymentMethodId" = @pmi
|
||||
GROUP BY i."Id";
|
||||
GROUP BY 1
|
||||
)
|
||||
SELECT
|
||||
ip.invoice_id, i.xmin, COALESCE(ia.addresses, '{}'), COALESCE(ip.payments, '{}'), to_jsonb(i)
|
||||
FROM invoices_payments ip
|
||||
JOIN "Invoices" i ON i."Id" = ip.invoice_id
|
||||
LEFT JOIN invoices_addresses ia ON ia.invoice_id = ip.invoice_id;
|
||||
"""
|
||||
, new { pmi = paymentMethodId.ToString(), includeNonActivated }));
|
||||
if (Enumerable.TryGetNonEnumeratedCount(rows, out var c) && c == 0)
|
||||
|
@@ -31,6 +31,5 @@ namespace BTCPayServer.Services
|
||||
public bool FixMappedDomainAppType { get; set; }
|
||||
public bool MigrateAppYmlToJson { get; set; }
|
||||
public bool MigrateToStoreConfig { get; set; }
|
||||
public bool MigratePayoutProcessors { get; internal set; }
|
||||
}
|
||||
}
|
||||
|
@@ -9,10 +9,12 @@ using BTCPayServer.Client;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Migrations;
|
||||
using Dapper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Newtonsoft.Json;
|
||||
using static BTCPayServer.Services.Stores.StoreRepository;
|
||||
|
||||
namespace BTCPayServer.Services.Stores
|
||||
{
|
||||
@@ -80,14 +82,20 @@ namespace BTCPayServer.Services.Stores
|
||||
public bool? IsUsed { get; set; }
|
||||
}
|
||||
#nullable enable
|
||||
public async Task<StoreRole[]> GetStoreRoles(string? storeId, bool includeUsers = false, bool storeOnly = false)
|
||||
public async Task<StoreRole[]> GetStoreRoles(string? storeId, bool storeOnly = false)
|
||||
{
|
||||
await using var ctx = _ContextFactory.CreateContext();
|
||||
var query = ctx.StoreRoles.Where(u => (storeOnly && u.StoreDataId == storeId) || (!storeOnly && (u.StoreDataId == null || u.StoreDataId == storeId)));
|
||||
if (includeUsers)
|
||||
{
|
||||
query = query.Include(u => u.Users);
|
||||
}
|
||||
var query = ctx.StoreRoles
|
||||
.Where(u => (storeOnly && u.StoreDataId == storeId) || (!storeOnly && (u.StoreDataId == null || u.StoreDataId == storeId)))
|
||||
// Not calling ToStoreRole here because we don't want to load users in the DB query
|
||||
.Select(u => new StoreRole()
|
||||
{
|
||||
Id = u.Id,
|
||||
Role = u.Role,
|
||||
Permissions = u.Permissions,
|
||||
IsServerRole = u.StoreDataId == null,
|
||||
IsUsed = u.Users.Any()
|
||||
});
|
||||
|
||||
var roles = await query.ToArrayAsync();
|
||||
// return ordered: default role comes first, then server-wide roles in specified order, followed by store roles
|
||||
@@ -98,7 +106,7 @@ namespace BTCPayServer.Services.Stores
|
||||
if (role.Role == defaultRole.Role) return -1;
|
||||
int index = Array.IndexOf(defaultOrder, role.Role);
|
||||
return index == -1 ? int.MaxValue : index;
|
||||
}).Select(ToStoreRole).ToArray();
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
public async Task<StoreRoleId> GetDefaultRole()
|
||||
@@ -637,6 +645,23 @@ retry:
|
||||
{
|
||||
return ex.InnerException is Npgsql.PostgresException postgres && postgres.SqlState == "40P01";
|
||||
}
|
||||
|
||||
public async Task<bool> InternalNodePayoutAuthorized(string storeId)
|
||||
{
|
||||
using var ctx = _ContextFactory.CreateContext();
|
||||
return (await ctx.Database.GetDbConnection().ExecuteScalarAsync<bool?>("""
|
||||
SELECT TRUE
|
||||
FROM "UserStore" us
|
||||
JOIN "StoreRoles" sr ON sr."Id" = us."Role"
|
||||
JOIN "AspNetUserRoles" ur ON us."ApplicationUserId" = ur."UserId"
|
||||
JOIN "AspNetRoles" r ON ur."RoleId" = r."Id"
|
||||
WHERE
|
||||
us."StoreDataId"=@storeId AND
|
||||
r."NormalizedName"='SERVERADMIN' AND
|
||||
'btcpay.store.canmodifystoresettings' = ANY(sr."Permissions")
|
||||
LIMIT 1;
|
||||
""", new { storeId })) is true;
|
||||
}
|
||||
}
|
||||
|
||||
public record StoreRoleId
|
||||
|
@@ -11,307 +11,377 @@ namespace BTCPayServer.Services
|
||||
// Please run it before release.
|
||||
var knownTranslations =
|
||||
"""
|
||||
Access Tokens
|
||||
Account
|
||||
Account key
|
||||
Account key path
|
||||
Add additional fee (network fee) to invoice …
|
||||
Add Address
|
||||
Add Exchange Rate Spread
|
||||
Add hop hints for private channels to the Lightning invoice
|
||||
Add Role
|
||||
Add Service
|
||||
Add User
|
||||
Add Webhook
|
||||
Additional Actions
|
||||
Admin API access token
|
||||
Admin must approve new users
|
||||
Administrator
|
||||
Allow anyone to create invoice
|
||||
Allow form for public use
|
||||
Allow payee to create invoices with custom amounts
|
||||
Allow payee to pass a comment
|
||||
Allow Stores use the Server's SMTP email settings as their default
|
||||
Always include non-witness UTXO if available
|
||||
Amazon S3
|
||||
Amount
|
||||
API Key
|
||||
API Keys
|
||||
App
|
||||
App Name
|
||||
App Type
|
||||
Application
|
||||
Approve
|
||||
Archive this store
|
||||
Authenticator code
|
||||
Auto-detect language on checkout
|
||||
Automatically approve claims
|
||||
Available Payment Methods
|
||||
Azure Blob Storage
|
||||
Backend's language
|
||||
Batch size
|
||||
BIP39 Seed (12/24 word mnemonic phrase) or HD private key (xprv...)
|
||||
Brand Color
|
||||
Branding
|
||||
Buyer Email
|
||||
Callback Notification URL
|
||||
Can use hot wallet
|
||||
Can use RPC import
|
||||
Celebrate payment with confetti
|
||||
Check releases on GitHub and notify when new BTCPay Server version is available
|
||||
Checkout Appearance
|
||||
Clone
|
||||
Colors to rotate between with animation when a payment is made. One color per line.
|
||||
Confirm new password
|
||||
Confirm password
|
||||
Connection string
|
||||
Consider the invoice paid even if the paid amount is … % less than expected
|
||||
Consider the invoice settled when the payment transaction …
|
||||
Contact URL
|
||||
Contact Us
|
||||
Contribution Perks Template
|
||||
Count all invoices created on the store as part of the goal
|
||||
Create
|
||||
Create a new app
|
||||
Create a new dictionary
|
||||
Create Account
|
||||
Create Form
|
||||
Create Invoice
|
||||
Create Pull Payment
|
||||
Create Request
|
||||
Create Store
|
||||
Create Webhook
|
||||
Create your account
|
||||
Crowdfund
|
||||
Currency
|
||||
Current password
|
||||
Custom CSS
|
||||
Custom HTML title to display on Checkout page
|
||||
Custom sound file for successful payment
|
||||
Custom Theme Extension Type
|
||||
Custom Theme File
|
||||
Dashboard
|
||||
Default currency
|
||||
Default language on checkout
|
||||
Default payment method on checkout
|
||||
Default role for users on a new store
|
||||
Delete this store
|
||||
Derivation scheme
|
||||
Derivation scheme format
|
||||
Description
|
||||
Description template of the lightning invoice
|
||||
Destination Address
|
||||
Dictionaries
|
||||
Dictionaries enable you to translate the BTCPay Server backend into different languages.
|
||||
Dictionary
|
||||
Disable public user registration
|
||||
Disable stores from using the server's email settings as backup
|
||||
Discourage search engines from indexing this site
|
||||
Display app on website root
|
||||
Display contribution ranking
|
||||
Display contribution value
|
||||
Display item selection for keypad
|
||||
Display Lightning payment amounts in Satoshis
|
||||
Display the category list
|
||||
Display the search bar
|
||||
Display Title
|
||||
Disqus Shortname
|
||||
Do not allow additional contributions after target has been reached
|
||||
Does not extend a BTCPay Server theme, fully custom
|
||||
Domain
|
||||
Domain name
|
||||
Don't create UTXO change
|
||||
Email
|
||||
Email address
|
||||
Email confirmation required
|
||||
Email confirmed?
|
||||
Emails
|
||||
Enable background animations on new payments
|
||||
Enable Disqus Comments
|
||||
Enable experimental features
|
||||
Enable LNURL
|
||||
Enable Payjoin/P2EP
|
||||
Enable public receipt page for settled invoices
|
||||
Enable public user registration
|
||||
Enable sounds on checkout page
|
||||
Enable sounds on new payments
|
||||
Enable tips
|
||||
End date
|
||||
Error
|
||||
Expiration Date
|
||||
Export
|
||||
Extends the BTCPay Server Dark theme
|
||||
Extends the BTCPay Server Light theme
|
||||
Fallback
|
||||
Featured Image URL
|
||||
Fee rate (sat/vB)
|
||||
Files
|
||||
Forgot password?
|
||||
Form configuration (JSON)
|
||||
Forms
|
||||
Gap limit
|
||||
Generate
|
||||
Generate API Key
|
||||
Generate Key
|
||||
Google Cloud Storage
|
||||
GRPC SSL Cipher suite (GRPC_SSL_CIPHER_SUITES)
|
||||
Hide Sensitive Info
|
||||
If a translation isn’t available in the new dictionary, it will be searched in the fallback.
|
||||
Image
|
||||
Invoice currency
|
||||
Invoice expires if the full amount has not been paid after …
|
||||
Invoice metadata
|
||||
Invoices
|
||||
Is administrator?
|
||||
Is signing key
|
||||
Item Description
|
||||
Keypad
|
||||
Lightning node (LNURL Auth)
|
||||
LNURL Classic Mode
|
||||
Local File System
|
||||
Log in
|
||||
Login Codes
|
||||
Logo
|
||||
Logout
|
||||
Logs
|
||||
Maintenance
|
||||
Make Crowdfund Public
|
||||
Manage Account
|
||||
Manage Plugins
|
||||
Master fingerprint
|
||||
Max sats
|
||||
Memo
|
||||
Metadata
|
||||
Min sats
|
||||
Minimum acceptable expiration time for BOLT11 for refunds
|
||||
Name
|
||||
New password
|
||||
Next
|
||||
Non-admins can access the User Creation API Endpoint
|
||||
Non-admins can create Hot Wallets for their Store
|
||||
Non-admins can import Hot Wallets for their Store
|
||||
Non-admins can use the Internal Lightning Node for their Store
|
||||
Non-admins cannot access the User Creation API Endpoint
|
||||
Notification Email
|
||||
Notification URL
|
||||
Notifications
|
||||
Only enable the payment method after user explicitly chooses it
|
||||
Optional seed passphrase
|
||||
Order Id
|
||||
Override the block explorers used
|
||||
Pair to
|
||||
Password
|
||||
Password (leave blank to generate invite-link)
|
||||
Pay Button
|
||||
PayJoin BIP21
|
||||
Payment
|
||||
Payment invalid if transactions fails to confirm … after invoice expiration
|
||||
Payments
|
||||
Payout Methods
|
||||
Payout Processors
|
||||
Payouts
|
||||
Plugin server
|
||||
Plugins
|
||||
Point of Sale
|
||||
Point of Sale Style
|
||||
Policies
|
||||
Preferred Price Source
|
||||
Print display
|
||||
Product list
|
||||
Product list with cart
|
||||
Profile Picture
|
||||
PSBT content
|
||||
PSBT to combine with…
|
||||
Public Key
|
||||
Pull Payments
|
||||
Rate Rules
|
||||
Rates
|
||||
Recommended fee confirmation target blocks
|
||||
Recovery Code
|
||||
Redirect invoice to redirect url automatically after paid
|
||||
Redirect URL
|
||||
Regenerate code
|
||||
Register
|
||||
Remember me
|
||||
Remember this machine
|
||||
Remove
|
||||
Reporting
|
||||
Request contributor data on checkout
|
||||
Request customer data on checkout
|
||||
Request Pairing
|
||||
Requests
|
||||
Reset goal every
|
||||
REST Uri
|
||||
Role
|
||||
Roles
|
||||
Root fingerprint
|
||||
Save
|
||||
Scope
|
||||
Search engines can index this site
|
||||
Security device (FIDO2)
|
||||
Select
|
||||
Select the Default Currency during Store Creation
|
||||
Select the payout method used for refund
|
||||
Send test webhook
|
||||
Server Name
|
||||
Server Settings
|
||||
Services
|
||||
Set Password
|
||||
Settings
|
||||
Shop Name
|
||||
Shopify
|
||||
Show "Pay in wallet" button
|
||||
Show a timer … minutes before invoice expiration
|
||||
Show plugins in pre-release
|
||||
Show recommended fee
|
||||
Show the payment list in the public receipt page
|
||||
Show the QR code of the receipt in the public receipt page
|
||||
Show the store header
|
||||
Sign in
|
||||
Sort contribution perks by popularity
|
||||
Sounds to play when a payment is made. One sound per line
|
||||
Specify the amount and currency for the refund
|
||||
Start date
|
||||
Starting index
|
||||
Store
|
||||
Store Id
|
||||
Store Name
|
||||
Store Settings
|
||||
Store Website
|
||||
Submit
|
||||
Subtract fees from amount
|
||||
Support URL
|
||||
Supported Transaction Currencies
|
||||
Target Amount
|
||||
Test Email
|
||||
Text to display in the tip input
|
||||
Text to display on buttons allowing the user to enter a custom amount
|
||||
Text to display on each button for items with a specific price
|
||||
Theme
|
||||
Tip percentage amounts (comma separated)
|
||||
Translations
|
||||
Two-Factor Authentication
|
||||
Unarchive this store
|
||||
Unify on-chain and lightning payment URL/QR code
|
||||
Update Password
|
||||
Update Webhook
|
||||
Upload PSBT from file…
|
||||
Url of the Dynamic DNS service you are using
|
||||
Use custom theme
|
||||
Use SSL
|
||||
User can input custom amount
|
||||
User can input discount in %
|
||||
Users
|
||||
UTXOs to spend from
|
||||
Verification Code
|
||||
Wallet file
|
||||
Wallet file content
|
||||
Wallets
|
||||
Webhooks
|
||||
Welcome to {0}
|
||||
Your dynamic DNS hostname
|
||||
{
|
||||
"... on every payment": "",
|
||||
"... only if the customer makes more than one payment for the invoice": "",
|
||||
"A given currency pair match the most specific rule. If two rules are matching and are as specific, the first rule will be chosen.": "",
|
||||
"Access Tokens": "",
|
||||
"Account": "",
|
||||
"Account key": "",
|
||||
"Account key path": "",
|
||||
"Add additional fee (network fee) to invoice …": "",
|
||||
"Add Address": "",
|
||||
"Add Exchange Rate Spread": "",
|
||||
"Add hop hints for private channels to the Lightning invoice": "",
|
||||
"Add Role": "",
|
||||
"Add Service": "",
|
||||
"Add User": "",
|
||||
"Add Webhook": "",
|
||||
"Additional Actions": "",
|
||||
"Admin API access token": "",
|
||||
"Admin must approve new users": "",
|
||||
"Administrator": "",
|
||||
"Advanced rate rule scripting": "",
|
||||
"Allow anyone to create invoice": "",
|
||||
"Allow form for public use": "",
|
||||
"Allow payee to create invoices with custom amounts": "",
|
||||
"Allow payee to pass a comment": "",
|
||||
"Allow Stores use the Server's SMTP email settings as their default": "",
|
||||
"Always include non-witness UTXO if available": "",
|
||||
"Amazon S3": "",
|
||||
"Amount": "",
|
||||
"API Key": "",
|
||||
"API Keys": "",
|
||||
"App": "",
|
||||
"App Name": "",
|
||||
"App Type": "",
|
||||
"Application": "",
|
||||
"Apply the brand color to the store's backend as well": "",
|
||||
"Approve": "",
|
||||
"Archive this store": "",
|
||||
"At Least One": "",
|
||||
"At Least Ten": "",
|
||||
"Authenticator code": "",
|
||||
"Auto-detect language on checkout": "",
|
||||
"Automatically approve claims": "",
|
||||
"Available Payment Methods": "",
|
||||
"Azure Blob Storage": "",
|
||||
"Backend's language": "",
|
||||
"Batch size": "",
|
||||
"BIP39 Seed (12/24 word mnemonic phrase) or HD private key (xprv...)": "",
|
||||
"blocks": "",
|
||||
"Brand Color": "",
|
||||
"Branding": "",
|
||||
"But now, what if you want to support <code>DOGE</code>? The problem with <code>DOGE</code> is that most exchange do not have any pair for it. But <code>bitpay</code> has a <code>DOGE_BTC</code> pair. <br />\r\n Luckily, the rule engine allow you to reference rules:": "",
|
||||
"Buyer Email": "",
|
||||
"Callback Notification URL": "",
|
||||
"Can use hot wallet": "",
|
||||
"Can use RPC import": "",
|
||||
"Celebrate payment with confetti": "",
|
||||
"Check releases on GitHub and notify when new BTCPay Server version is available": "",
|
||||
"Checkout Appearance": "",
|
||||
"Choose your import method": "",
|
||||
"Choose your wallet option": "",
|
||||
"Clone": "",
|
||||
"Coingecko integration": "",
|
||||
"Colors to rotate between with animation when a payment is made. One color per line.": "",
|
||||
"Confirm new password": "",
|
||||
"Confirm password": "",
|
||||
"Connect an existing wallet": "",
|
||||
"Connect hardware wallet": "",
|
||||
"Connection string": "",
|
||||
"Consider the invoice paid even if the paid amount is … % less than expected": "",
|
||||
"Consider the invoice settled when the payment transaction …": "",
|
||||
"Contact URL": "",
|
||||
"Contact Us": "",
|
||||
"Contribution Perks Template": "",
|
||||
"Count all invoices created on the store as part of the goal": "",
|
||||
"Create": "",
|
||||
"Create a new app": "",
|
||||
"Create a new wallet": "",
|
||||
"Create Account": "",
|
||||
"Create Form": "",
|
||||
"Create Invoice": "",
|
||||
"Create Pull Payment": "",
|
||||
"Create Request": "",
|
||||
"Create Store": "",
|
||||
"Create Webhook": "",
|
||||
"Create your account": "",
|
||||
"Crowdfund": "",
|
||||
"Currency": "",
|
||||
"Current password": "",
|
||||
"Custom": "",
|
||||
"Custom CSS": "",
|
||||
"Custom HTML title to display on Checkout page": "",
|
||||
"Custom sound file for successful payment": "",
|
||||
"Custom Theme Extension Type": "",
|
||||
"Custom Theme File": "",
|
||||
"Dashboard": "",
|
||||
"days": "",
|
||||
"Default currency": "",
|
||||
"Default Currency Pairs": "",
|
||||
"Default language on checkout": "",
|
||||
"Default payment method on checkout": "",
|
||||
"Default role for users on a new store": "",
|
||||
"Delete this store": "",
|
||||
"Derivation scheme": "",
|
||||
"Derivation scheme format": "",
|
||||
"Description": "",
|
||||
"Description template of the lightning invoice": "",
|
||||
"Destination Address": "",
|
||||
"Dictionaries": "",
|
||||
"Dictionaries enable you to translate the BTCPay Server backend into different languages.": "",
|
||||
"Dictionary": "",
|
||||
"Direct integration": "",
|
||||
"Disable public user registration": "",
|
||||
"Disable stores from using the server's email settings as backup": "",
|
||||
"Discourage search engines from indexing this site": "",
|
||||
"Display app on website root": "",
|
||||
"Display contribution ranking": "",
|
||||
"Display contribution value": "",
|
||||
"Display item selection for keypad": "",
|
||||
"Display Lightning payment amounts in Satoshis": "",
|
||||
"Display the category list": "",
|
||||
"Display the search bar": "",
|
||||
"Display Title": "",
|
||||
"Disqus Shortname": "",
|
||||
"Do not allow additional contributions after target has been reached": "",
|
||||
"Does not extend a BTCPay Server theme, fully custom": "",
|
||||
"Domain": "",
|
||||
"Domain name": "",
|
||||
"Don't create UTXO change": "",
|
||||
"Email": "",
|
||||
"Email address": "",
|
||||
"Email confirmation required": "",
|
||||
"Email confirmed?": "",
|
||||
"Emails": "",
|
||||
"Enable background animations on new payments": "",
|
||||
"Enable Disqus Comments": "",
|
||||
"Enable experimental features": "",
|
||||
"Enable LNURL": "",
|
||||
"Enable Payjoin/P2EP": "",
|
||||
"Enable public receipt page for settled invoices": "",
|
||||
"Enable public user registration": "",
|
||||
"Enable sounds on checkout page": "",
|
||||
"Enable sounds on new payments": "",
|
||||
"Enable tips": "",
|
||||
"End date": "",
|
||||
"Enter extended public key": "",
|
||||
"Enter wallet seed": "",
|
||||
"Error": "",
|
||||
"Expiration Date": "",
|
||||
"Export": "",
|
||||
"Extends the BTCPay Server Dark theme": "",
|
||||
"Extends the BTCPay Server Light theme": "",
|
||||
"Fallback": "",
|
||||
"Featured Image URL": "",
|
||||
"Fee rate (sat/vB)": "",
|
||||
"Fee will be shown for BTC and LTC onchain payments only.": "",
|
||||
"Files": "",
|
||||
"Forgot password?": "",
|
||||
"Form configuration (JSON)": "",
|
||||
"Forms": "",
|
||||
"Gap limit": "",
|
||||
"Generate": "",
|
||||
"Generate {0} Wallet": "",
|
||||
"Generate a brand-new wallet to use": "",
|
||||
"Generate API Key": "",
|
||||
"Generate Key": "",
|
||||
"Google Cloud Storage": "",
|
||||
"GRPC SSL Cipher suite (GRPC_SSL_CIPHER_SUITES)": "",
|
||||
"Has at least 1 confirmation": "",
|
||||
"Has at least 2 confirmations": "",
|
||||
"Has at least 6 confirmations": "",
|
||||
"Hide Sensitive Info": "",
|
||||
"Hot wallet": "",
|
||||
"However, <code>kraken</code> does not support the <code>BTC_CAD</code> pair. For this reason you can add a rule mapping all <code>X_CAD</code> to <code>ndax</code>, a Canadian exchange.": "",
|
||||
"However, explicitely setting specific pairs like this can be a bit difficult. Instead, you can define a rule <code>X_X</code> which will match any currency pair. The following example will use <code>kraken</code> for getting the rate of any currency pair.": "",
|
||||
"I don't have a wallet": "",
|
||||
"I have a wallet": "",
|
||||
"If a translation isn’t available in the new dictionary, it will be searched in the fallback.": "",
|
||||
"Image": "",
|
||||
"Import {0} Wallet": "",
|
||||
"Import an existing hardware or software wallet": "",
|
||||
"Import wallet file": "",
|
||||
"Import your public keys using our Vault application": "",
|
||||
"Input the key string manually": "",
|
||||
"Invitation URL": "",
|
||||
"Invoice currency": "",
|
||||
"Invoice expires if the full amount has not been paid after …": "",
|
||||
"Invoice metadata": "",
|
||||
"Invoices": "",
|
||||
"Is administrator?": "",
|
||||
"Is signing key": "",
|
||||
"Is unconfirmed": "",
|
||||
"It is worth noting that the inverses of those pairs are automatically supported as well.<br />\r\n It means that the rule <code>USD_DOGE = 1 / DOGE_USD</code> implicitely exists.": "",
|
||||
"Item Description": "",
|
||||
"Keypad": "",
|
||||
"Let's get started": "",
|
||||
"Lightning node (LNURL Auth)": "",
|
||||
"LNURL Classic Mode": "",
|
||||
"Local File System": "",
|
||||
"Log in": "",
|
||||
"Login Codes": "",
|
||||
"Logo": "",
|
||||
"Logout": "",
|
||||
"Logs": "",
|
||||
"Maintenance": "",
|
||||
"Make Crowdfund Public": "",
|
||||
"Manage Account": "",
|
||||
"Manage Plugins": "",
|
||||
"Master fingerprint": "",
|
||||
"Max sats": "",
|
||||
"Memo": "",
|
||||
"Metadata": "",
|
||||
"Min sats": "",
|
||||
"Minimum acceptable expiration time for BOLT11 for refunds": "",
|
||||
"Never add network fee": "",
|
||||
"New password": "",
|
||||
"Next": "",
|
||||
"Non-admins can access the User Creation API Endpoint": "",
|
||||
"Non-admins can create Hot Wallets for their Store": "",
|
||||
"Non-admins can import Hot Wallets for their Store": "",
|
||||
"Non-admins can use the Internal Lightning Node for their Store": "",
|
||||
"Non-admins cannot access the User Creation API Endpoint": "",
|
||||
"Not recommended": "",
|
||||
"Notification Email": "",
|
||||
"Notification URL": "",
|
||||
"Notifications": "",
|
||||
"Only enable the payment method after user explicitly chooses it": "",
|
||||
"Optional seed passphrase": "",
|
||||
"Order Id": "",
|
||||
"Override the block explorers used": "",
|
||||
"Pair to": "",
|
||||
"Password": "",
|
||||
"Password (leave blank to generate invite-link)": "",
|
||||
"Pay Button": "",
|
||||
"PayJoin BIP21": "",
|
||||
"Payment": "",
|
||||
"Payment invalid if transactions fails to confirm … after invoice expiration": "",
|
||||
"Payments": "",
|
||||
"Payout Methods": "",
|
||||
"Payout Processors": "",
|
||||
"Payouts": "",
|
||||
"Please enable JavaScript for this option to be available": "",
|
||||
"Please note that creating a hot wallet is not supported by this instance for non administrators.": "",
|
||||
"Plugin server": "",
|
||||
"Plugins": "",
|
||||
"Point of Sale": "",
|
||||
"Point of Sale Style": "",
|
||||
"Policies": "",
|
||||
"Preferred Price Source": "",
|
||||
"Print display": "",
|
||||
"Product list": "",
|
||||
"Product list with cart": "",
|
||||
"Profile Picture": "",
|
||||
"Provide the 12 or 24 word recovery seed": "",
|
||||
"PSBT content": "",
|
||||
"PSBT to combine with…": "",
|
||||
"Public Key": "",
|
||||
"Pull Payments": "",
|
||||
"Rate Rules": "",
|
||||
"Rate script allows you to express precisely how you want to calculate rates for currency pairs.": "",
|
||||
"Rates": "",
|
||||
"Recommended": "",
|
||||
"Recommended fee confirmation target blocks": "",
|
||||
"Recovery Code": "",
|
||||
"Redirect invoice to redirect url automatically after paid": "",
|
||||
"Redirect URL": "",
|
||||
"Register": "",
|
||||
"Remember me": "",
|
||||
"Remember this machine": "",
|
||||
"Remove": "",
|
||||
"Reporting": "",
|
||||
"Request contributor data on checkout": "",
|
||||
"Request customer data on checkout": "",
|
||||
"Request Pairing": "",
|
||||
"Requests": "",
|
||||
"Required Confirmations": "",
|
||||
"Reset goal every": "",
|
||||
"Reset Password": "",
|
||||
"REST Uri": "",
|
||||
"Role": "",
|
||||
"Roles": "",
|
||||
"Root fingerprint": "",
|
||||
"Save": "",
|
||||
"Scan wallet QR code": "",
|
||||
"Scope": "",
|
||||
"Scripting": "",
|
||||
"Search engines can index this site": "",
|
||||
"Security device (FIDO2)": "",
|
||||
"Select": "",
|
||||
"Select the Default Currency during Store Creation": "",
|
||||
"Select the payout method used for refund": "",
|
||||
"Send invitation email": "",
|
||||
"Send test webhook": "",
|
||||
"Server Name": "",
|
||||
"Server Settings": "",
|
||||
"Services": "",
|
||||
"Set Password": "",
|
||||
"Set to default settings": "",
|
||||
"Settings": "",
|
||||
"Setup {0} Wallet": "",
|
||||
"Shop Name": "",
|
||||
"Shopify": "",
|
||||
"Show \"Pay in wallet\" button": "",
|
||||
"Show a timer … minutes before invoice expiration": "",
|
||||
"Show plugins in pre-release": "",
|
||||
"Show recommended fee": "",
|
||||
"Show the payment list in the public receipt page": "",
|
||||
"Show the QR code of the receipt in the public receipt page": "",
|
||||
"Show the store header": "",
|
||||
"Sign in": "",
|
||||
"Sort contribution perks by popularity": "",
|
||||
"Sounds to play when a payment is made. One sound per line": "",
|
||||
"Specify the amount and currency for the refund": "",
|
||||
"Start date": "",
|
||||
"Starting index": "",
|
||||
"Store": "",
|
||||
"Store Id": "",
|
||||
"Store Name": "",
|
||||
"Store Settings": "",
|
||||
"Store Speed Policy": "",
|
||||
"Store Website": "",
|
||||
"Submit": "",
|
||||
"Subtract fees from amount": "",
|
||||
"Support URL": "",
|
||||
"Supported by BlueWallet, Cobo Vault, Passport and Specter DIY": "",
|
||||
"Supported Transaction Currencies": "",
|
||||
"Target Amount": "",
|
||||
"Test Email": "",
|
||||
"Test Results:": "",
|
||||
"Testing": "",
|
||||
"Text to display in the tip input": "",
|
||||
"Text to display on buttons allowing the user to enter a custom amount": "",
|
||||
"Text to display on each button for items with a specific price": "",
|
||||
"The following methods assume that you already have an existing wallet created and backed up.": "",
|
||||
"The script language is composed of several rules composed of a currency pair and a mathematic expression.\r\n The example below will use <code>kraken</code> for both <code>LTC_USD</code> and <code>BTC_USD</code> pairs.": "",
|
||||
"Theme": "",
|
||||
"Tip percentage amounts (comma separated)": "",
|
||||
"Translations": "",
|
||||
"Two-Factor Authentication": "",
|
||||
"Unarchive this store": "",
|
||||
"Unify on-chain and lightning payment URL/QR code": "",
|
||||
"Update Password": "",
|
||||
"Update Webhook": "",
|
||||
"Upload a file exported from your wallet": "",
|
||||
"Upload PSBT from file…": "",
|
||||
"Url of the Dynamic DNS service you are using": "",
|
||||
"Use custom theme": "",
|
||||
"Use SSL": "",
|
||||
"User can input custom amount": "",
|
||||
"User can input discount in %": "",
|
||||
"Users": "",
|
||||
"UTXOs to spend from": "",
|
||||
"Verification Code": "",
|
||||
"View-Only Wallet File": "",
|
||||
"Wallet file": "",
|
||||
"Wallet file content": "",
|
||||
"Wallet Keys File": "",
|
||||
"Wallet Password": "",
|
||||
"Wallet's private key is erased from the server. Higher security. To spend, you have to manually input the private key or import it into an external wallet.": "",
|
||||
"Wallet's private key is stored on the server. Spending the funds you received is convenient. To minimize the risk of theft, regularly withdraw funds to a different wallet.": "",
|
||||
"Wallets": "",
|
||||
"Watch-only wallet": "",
|
||||
"Webhooks": "",
|
||||
"Welcome to {0}": "",
|
||||
"With <code>DOGE_USD</code> will be expanded to <code>bitpay(DOGE_BTC) * kraken(BTC_USD)</code>. And <code>DOGE_CAD</code> will be expanded to <code>bitpay(DOGE_BTC) * ndax(BTC_CAD)</code>. <br />\r\n However, we advise you to write it that way to increase coverage so that <code>DOGE_BTC</code> is also supported:": "",
|
||||
"You really should not type your seed into a device that is connected to the internet.": "",
|
||||
"Your dynamic DNS hostname": "",
|
||||
"Zero Confirmation": ""
|
||||
}
|
||||
""";
|
||||
Default = Translations.CreateFromText(knownTranslations);
|
||||
Default = Translations.CreateFromJson(knownTranslations);
|
||||
Default = new Translations(new KeyValuePair<string, string>[]
|
||||
{
|
||||
// You can add additional hard coded default here
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user