Compare commits
128 Commits
v1.6.9
...
pluginbuil
Author | SHA1 | Date | |
---|---|---|---|
50efcefe8f | |||
c8a1024e24 | |||
9a2d2e2d89 | |||
b7af234427 | |||
a374e351e2 | |||
562f88555c | |||
167c5297fa | |||
b281d09694 | |||
853a0ac5ea | |||
ea948cfc3f | |||
fdd13390fb | |||
b2f6f8b3c1 | |||
cd12162b6f | |||
79717d1d64 | |||
e56cbf0baa | |||
05232414ad | |||
4bbc7d9662 | |||
3805b7f287 | |||
63620409a9 | |||
ba423a79e3 | |||
8806ba76eb | |||
9e73260230 | |||
c0125b83d1 | |||
1fa297fb73 | |||
57557748e2 | |||
8b79212a6e | |||
f4af4ec4dc | |||
2e150f4bf4 | |||
4f4aa051c9 | |||
da1dd7448e | |||
0fd47eeee0 | |||
54c9d7283a | |||
848db5f7de | |||
5fb32fe0e9 | |||
adf5b4ca0c | |||
16bfb1dbfe | |||
e5421b8a9f | |||
f9f1a22e3b | |||
9533809631 | |||
6d7c11f1b1 | |||
0286c72256 | |||
763aaa2926 | |||
ae4af7dd13 | |||
4ae2ea32e9 | |||
434298cba6 | |||
a2fa688cde | |||
895462ac7f | |||
e883714446 | |||
e1a235b4e8 | |||
ffa2c59df7 | |||
3f19dc55fa | |||
66c2148a63 | |||
28850f534c | |||
c40c11a822 | |||
b334e1aa00 | |||
b48986bfd6 | |||
ced63baed6 | |||
880635d615 | |||
d9f8c8d3b1 | |||
8155841a1d | |||
30f83d8f3f | |||
96c86160df | |||
b7ea128132 | |||
4bee8e9bfe | |||
bc195e771e | |||
0bc3e94052 | |||
3eb3523b52 | |||
8e2f84a989 | |||
143ec7463f | |||
4a5fd08e51 | |||
0306635a45 | |||
0a4d32cdb5 | |||
d590992d1d | |||
e8766946dd | |||
8c35189b37 | |||
e6390cde97 | |||
db976a6408 | |||
031c3ed055 | |||
cb391f08b9 | |||
5387a6287e | |||
0e4544b2da | |||
e0cbb7bede | |||
ed45b73274 | |||
cadcb586a7 | |||
9e31270459 | |||
dc07f046f2 | |||
5032bbafb1 | |||
1540bfb3a1 | |||
f3f5851118 | |||
9810edcd1a | |||
e334b9162a | |||
836c676057 | |||
1abadd9c5d | |||
e8bd1d8237 | |||
8a7470500a | |||
75689c665d | |||
d84f4f676b | |||
c97b859963 | |||
6b8f4ee1d5 | |||
3532789c35 | |||
267905b5e7 | |||
1626bd7a18 | |||
7106830be9 | |||
3b1946d65c | |||
6dedf4d44f | |||
fe5e2584b1 | |||
8fae38deca | |||
7f8e322e9c | |||
4d0f76f9e8 | |||
5d2b42960b | |||
0098dacdff | |||
51666fbf0e | |||
defb9120fd | |||
11ec72ce8c | |||
f67bd69ecc | |||
db2c29a6e1 | |||
e22e522245 | |||
7c8f4c0405 | |||
01ab21e4c0 | |||
534a2912e1 | |||
1456f4e227 | |||
63e11451ba | |||
7f80674cf2 | |||
16f4ca5fbf | |||
701ba59bd8 | |||
8c6705bccb | |||
e4542c4ac4 | |||
45eea1d6de |
4
.gitignore
vendored
4
.gitignore
vendored
@ -288,10 +288,6 @@ __pycache__/
|
||||
*.xsd.cs
|
||||
/BTCPayServer/Build/dockerfiles
|
||||
|
||||
# Bundling JS/CSS
|
||||
BTCPayServer/wwwroot/bundles/*
|
||||
!BTCPayServer/wwwroot/bundles/.gitignore
|
||||
|
||||
.vscode/*
|
||||
!.vscode/launch.json
|
||||
!.vscode/tasks.json
|
||||
|
@ -32,9 +32,9 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="HtmlSanitizer" Version="5.0.372" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.7" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.9" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.9" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.7" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="6.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
|
||||
@ -10,6 +12,13 @@ namespace BTCPayServer.Abstractions.Extensions
|
||||
private const string ACTIVE_CATEGORY_KEY = "ActiveCategory";
|
||||
private const string ACTIVE_PAGE_KEY = "ActivePage";
|
||||
private const string ACTIVE_ID_KEY = "ActiveId";
|
||||
private const string ActivePageClass = "active";
|
||||
|
||||
public enum DateDisplayFormat
|
||||
{
|
||||
Localized,
|
||||
Relative
|
||||
}
|
||||
|
||||
public static void SetActivePage<T>(this ViewDataDictionary viewData, T activePage, string title = null, string activeId = null)
|
||||
where T : IConvertible
|
||||
@ -52,7 +61,7 @@ namespace BTCPayServer.Abstractions.Extensions
|
||||
var activeCategory = viewData[ACTIVE_CATEGORY_KEY]?.ToString();
|
||||
var categoryMatch = category.Equals(activeCategory, StringComparison.InvariantCultureIgnoreCase);
|
||||
var idMatch = id == null || activeId == null || id.Equals(activeId);
|
||||
return categoryMatch && idMatch ? "active" : null;
|
||||
return categoryMatch && idMatch ? ActivePageClass : null;
|
||||
}
|
||||
|
||||
public static string IsActivePage<T>(this ViewDataDictionary viewData, T page, object id = null)
|
||||
@ -60,6 +69,14 @@ namespace BTCPayServer.Abstractions.Extensions
|
||||
{
|
||||
return IsActivePage(viewData, page.ToString(), page.GetType().ToString(), id);
|
||||
}
|
||||
|
||||
public static string IsActivePage<T>(this ViewDataDictionary viewData, IEnumerable<T> pages, object id = null)
|
||||
where T : IConvertible
|
||||
{
|
||||
return pages.Any(page => IsActivePage(viewData, page.ToString(), page.GetType().ToString(), id) == ActivePageClass)
|
||||
? ActivePageClass
|
||||
: null;
|
||||
}
|
||||
|
||||
public static string IsActivePage(this ViewDataDictionary viewData, string page, string category, object id = null)
|
||||
{
|
||||
@ -72,29 +89,32 @@ namespace BTCPayServer.Abstractions.Extensions
|
||||
var activeCategory = viewData[ACTIVE_CATEGORY_KEY]?.ToString();
|
||||
var categoryAndPageMatch = (category == null || activeCategory.Equals(category, StringComparison.InvariantCultureIgnoreCase)) && page.Equals(activePage, StringComparison.InvariantCultureIgnoreCase);
|
||||
var idMatch = id == null || activeId == null || id.Equals(activeId);
|
||||
return categoryAndPageMatch && idMatch ? "active" : null;
|
||||
return categoryAndPageMatch && idMatch ? ActivePageClass : null;
|
||||
}
|
||||
|
||||
public static HtmlString ToBrowserDate(this DateTimeOffset date)
|
||||
public static HtmlString ToBrowserDate(this DateTimeOffset date, DateDisplayFormat format = DateDisplayFormat.Localized)
|
||||
{
|
||||
var displayDate = date.ToString("o", CultureInfo.InvariantCulture);
|
||||
return new HtmlString($"<span class='localizeDate'>{displayDate}</span>");
|
||||
var relative = date.ToTimeAgo();
|
||||
var initial = format.ToString().ToLower();
|
||||
var dateTime = date.ToString("o", CultureInfo.InvariantCulture);
|
||||
var displayDate = format == DateDisplayFormat.Relative ? relative : date.ToString("g", CultureInfo.InvariantCulture);
|
||||
return new HtmlString($"<time datetime=\"{dateTime}\" data-relative=\"{relative}\" data-initial=\"{initial}\">{displayDate}</time>");
|
||||
}
|
||||
|
||||
public static HtmlString ToBrowserDate(this DateTime date)
|
||||
public static HtmlString ToBrowserDate(this DateTime date, DateDisplayFormat format = DateDisplayFormat.Localized)
|
||||
{
|
||||
var displayDate = date.ToString("o", CultureInfo.InvariantCulture);
|
||||
return new HtmlString($"<span class='localizeDate'>{displayDate}</span>");
|
||||
var relative = date.ToTimeAgo();
|
||||
var initial = format.ToString().ToLower();
|
||||
var dateTime = date.ToString("o", CultureInfo.InvariantCulture);
|
||||
var displayDate = format == DateDisplayFormat.Relative ? relative : date.ToString("g", CultureInfo.InvariantCulture);
|
||||
return new HtmlString($"<time datetime=\"{dateTime}\" data-relative=\"{relative}\" data-initial=\"{initial}\">{displayDate}</time>");
|
||||
}
|
||||
|
||||
public static string ToTimeAgo(this DateTimeOffset date)
|
||||
{
|
||||
var diff = DateTimeOffset.UtcNow - date;
|
||||
var formatted = diff.TotalSeconds > 0
|
||||
? $"{diff.TimeString()} ago"
|
||||
: $"in {diff.Negate().TimeString()}";
|
||||
return formatted;
|
||||
}
|
||||
public static string ToTimeAgo(this DateTimeOffset date) => (DateTimeOffset.UtcNow - date).ToTimeAgo();
|
||||
|
||||
public static string ToTimeAgo(this DateTime date) => (DateTimeOffset.UtcNow - date).ToTimeAgo();
|
||||
|
||||
public static string ToTimeAgo(this TimeSpan diff) => diff.TotalSeconds > 0 ? $"{diff.TimeString()} ago" : $"in {diff.Negate().TimeString()}";
|
||||
|
||||
public static string TimeString(this TimeSpan timeSpan)
|
||||
{
|
||||
@ -106,16 +126,14 @@ namespace BTCPayServer.Abstractions.Extensions
|
||||
{
|
||||
return $"{(int)timeSpan.TotalMinutes} minute{Plural((int)timeSpan.TotalMinutes)}";
|
||||
}
|
||||
if (timeSpan.Days < 1)
|
||||
{
|
||||
return $"{(int)timeSpan.TotalHours} hour{Plural((int)timeSpan.TotalHours)}";
|
||||
}
|
||||
return $"{(int)timeSpan.TotalDays} day{Plural((int)timeSpan.TotalDays)}";
|
||||
return timeSpan.Days < 1
|
||||
? $"{(int)timeSpan.TotalHours} hour{Plural((int)timeSpan.TotalHours)}"
|
||||
: $"{(int)timeSpan.TotalDays} day{Plural((int)timeSpan.TotalDays)}";
|
||||
}
|
||||
|
||||
private static string Plural(int value)
|
||||
{
|
||||
return value > 1 ? "s" : string.Empty;
|
||||
return value == 1 ? string.Empty : "s";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,8 +28,8 @@
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.3.8" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.10" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.3.15" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.14" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
@ -20,6 +20,17 @@ namespace BTCPayServer.Client
|
||||
return await HandleResponse<PointOfSaleAppData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<PointOfSaleAppData> UpdatePointOfSaleApp(string appId,
|
||||
CreatePointOfSaleAppRequest request, CancellationToken token = default)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/apps/pos/{appId}", bodyPayload: request,
|
||||
method: HttpMethod.Put), token);
|
||||
return await HandleResponse<PointOfSaleAppData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<AppDataBase> GetApp(string appId, CancellationToken token = default)
|
||||
{
|
||||
if (appId == null)
|
||||
|
@ -95,6 +95,24 @@ namespace BTCPayServer.Client
|
||||
method: HttpMethod.Get), token);
|
||||
return await HandleResponse<LightningInvoiceData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<LightningInvoiceData[]> GetLightningInvoices(string cryptoCode,
|
||||
bool? pendingOnly = null, long? offsetIndex = null, CancellationToken token = default)
|
||||
{
|
||||
var queryPayload = new Dictionary<string, object>();
|
||||
if (pendingOnly is bool v)
|
||||
{
|
||||
queryPayload.Add("pendingOnly", v.ToString());
|
||||
}
|
||||
if (offsetIndex is > 0)
|
||||
{
|
||||
queryPayload.Add("offsetIndex", offsetIndex);
|
||||
}
|
||||
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/invoices", queryPayload), token);
|
||||
return await HandleResponse<LightningInvoiceData[]>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<LightningInvoiceData> CreateLightningInvoice(string cryptoCode, CreateLightningInvoiceRequest request,
|
||||
CancellationToken token = default)
|
||||
|
@ -65,7 +65,7 @@ namespace BTCPayServer.Client
|
||||
return await HandleResponse<string>(response);
|
||||
}
|
||||
|
||||
public virtual async Task PayLightningInvoice(string storeId, string cryptoCode, PayLightningInvoiceRequest request,
|
||||
public virtual async Task<LightningPaymentData> PayLightningInvoice(string storeId, string cryptoCode, PayLightningInvoiceRequest request,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
if (request == null)
|
||||
@ -73,7 +73,7 @@ namespace BTCPayServer.Client
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices/pay", bodyPayload: request,
|
||||
method: HttpMethod.Post), token);
|
||||
await HandleResponse(response);
|
||||
return await HandleResponse<LightningPaymentData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<LightningPaymentData> GetLightningPayment(string storeId, string cryptoCode,
|
||||
@ -97,6 +97,24 @@ namespace BTCPayServer.Client
|
||||
method: HttpMethod.Get), token);
|
||||
return await HandleResponse<LightningInvoiceData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<LightningInvoiceData[]> GetLightningInvoices(string storeId, string cryptoCode,
|
||||
bool? pendingOnly = null, long? offsetIndex = null, CancellationToken token = default)
|
||||
{
|
||||
var queryPayload = new Dictionary<string, object>();
|
||||
if (pendingOnly is bool v)
|
||||
{
|
||||
queryPayload.Add("pendingOnly", v.ToString());
|
||||
}
|
||||
if (offsetIndex is > 0)
|
||||
{
|
||||
queryPayload.Add("offsetIndex", offsetIndex);
|
||||
}
|
||||
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices", queryPayload), token);
|
||||
return await HandleResponse<LightningInvoiceData[]>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<LightningInvoiceData> CreateLightningInvoice(string storeId, string cryptoCode,
|
||||
CreateLightningInvoiceRequest request, CancellationToken token = default)
|
||||
|
@ -37,6 +37,20 @@ namespace BTCPayServer.Client
|
||||
await HandleResponse(response);
|
||||
}
|
||||
|
||||
public virtual async Task<Client.Models.InvoiceData> PayPaymentRequest(string storeId, string paymentRequestId, PayPaymentRequestRequest request, CancellationToken token = default)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
if (storeId is null)
|
||||
throw new ArgumentNullException(nameof(storeId));
|
||||
if (paymentRequestId is null)
|
||||
throw new ArgumentNullException(nameof(paymentRequestId));
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-requests/{paymentRequestId}/pay", bodyPayload: request,
|
||||
method: HttpMethod.Post), token);
|
||||
return await HandleResponse<Client.Models.InvoiceData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<PaymentRequestData> CreatePaymentRequest(string storeId,
|
||||
CreatePaymentRequestRequest request, CancellationToken token = default)
|
||||
{
|
||||
|
@ -0,0 +1,53 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
|
||||
namespace BTCPayServer.Client
|
||||
{
|
||||
public partial class BTCPayServerClient
|
||||
{
|
||||
public virtual async Task<StoreRateConfiguration> GetStoreRateConfiguration(string storeId,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
using var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/rates/configuration", method: HttpMethod.Get),
|
||||
token);
|
||||
return await HandleResponse<StoreRateConfiguration>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<List<RateSource>> GetRateSources(
|
||||
CancellationToken token = default)
|
||||
{
|
||||
using var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"misc/rate-sources", method: HttpMethod.Get),
|
||||
token);
|
||||
return await HandleResponse<List<RateSource>>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<StoreRateConfiguration> UpdateStoreRateConfiguration(string storeId,
|
||||
StoreRateConfiguration request,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
using var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/rates/configuration", bodyPayload: request,
|
||||
method: HttpMethod.Put),
|
||||
token);
|
||||
return await HandleResponse<StoreRateConfiguration>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<List<StoreRatePreviewResult>> PreviewUpdateStoreRateConfiguration(string storeId,
|
||||
StoreRateConfiguration request,
|
||||
string[] currencyPair,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
using var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/rates/configuration/preview", bodyPayload: request,
|
||||
queryPayload: new Dictionary<string, object>() {{"currencyPair", currencyPair}},
|
||||
method: HttpMethod.Post),
|
||||
token);
|
||||
return await HandleResponse<List<StoreRatePreviewResult>>(response);
|
||||
}
|
||||
}
|
||||
}
|
@ -25,7 +25,7 @@ namespace BTCPayServer.Client.Models
|
||||
public string Template { get; set; } = null;
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public PosViewType DefaultView { get; set; }
|
||||
public bool ShowCustomAmount { get; set; } = true;
|
||||
public bool ShowCustomAmount { get; set; } = false;
|
||||
public bool ShowDiscount { get; set; } = true;
|
||||
public bool EnableTips { get; set; } = true;
|
||||
public string CustomAmountPayButtonText { get; set; } = null;
|
||||
@ -36,6 +36,8 @@ namespace BTCPayServer.Client.Models
|
||||
public string RedirectUrl { get; set; } = null;
|
||||
public bool? RedirectAutomatically { get; set; } = null;
|
||||
public bool? RequiresRefundEmail { get; set; } = null;
|
||||
public string CheckoutFormId { get; set; } = null;
|
||||
public string EmbeddedCSS { get; set; } = null;
|
||||
public CheckoutType? CheckoutType { get; set; } = null;
|
||||
}
|
||||
}
|
||||
|
@ -85,6 +85,9 @@ namespace BTCPayServer.Client.Models
|
||||
public bool? RedirectAutomatically { get; set; }
|
||||
public bool? RequiresRefundEmail { get; set; } = null;
|
||||
public string DefaultLanguage { get; set; }
|
||||
[JsonProperty("checkoutFormId")]
|
||||
public string CheckoutFormId { get; set; }
|
||||
public CheckoutType? CheckoutType { get; set; }
|
||||
}
|
||||
}
|
||||
public class InvoiceData : InvoiceDataBase
|
||||
|
@ -1,9 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
[Obsolete]
|
||||
public class LabelData
|
||||
{
|
||||
public string Type { get; set; }
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Client.JsonConverters;
|
||||
using BTCPayServer.Lightning;
|
||||
using Newtonsoft.Json;
|
||||
@ -26,5 +27,8 @@ namespace BTCPayServer.Client.Models
|
||||
|
||||
[JsonConverter(typeof(LightMoneyJsonConverter))]
|
||||
public LightMoney AmountReceived { get; set; }
|
||||
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public Dictionary<ulong, string> CustomRecords { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,13 @@ namespace BTCPayServer.Client.Models
|
||||
[JsonProperty("nodeURIs", ItemConverterType = typeof(NodeUriJsonConverter))]
|
||||
public NodeInfo[] NodeURIs { get; set; }
|
||||
public int BlockHeight { get; set; }
|
||||
public string Alias { get; set; }
|
||||
public string Color { get; set; }
|
||||
public string Version { get; set; }
|
||||
public long? PeersCount { get; set; }
|
||||
public long? ActiveChannelsCount { get; set; }
|
||||
public long? InactiveChannelsCount { get; set; }
|
||||
public long? PendingChannelsCount { get; set; }
|
||||
}
|
||||
|
||||
public class LightningChannelData
|
||||
|
@ -10,4 +10,6 @@ public class OnChainAutomatedPayoutSettings
|
||||
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
|
||||
public TimeSpan IntervalSeconds { get; set; }
|
||||
|
||||
public int? FeeBlockTarget { get; set; }
|
||||
}
|
||||
|
@ -13,7 +13,9 @@ namespace BTCPayServer.Client.Models
|
||||
public uint256 TransactionHash { get; set; }
|
||||
|
||||
public string Comment { get; set; }
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
public Dictionary<string, LabelData> Labels { get; set; } = new Dictionary<string, LabelData>();
|
||||
#pragma warning restore CS0612 // Type or member is obsolete
|
||||
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal Amount { get; set; }
|
||||
|
@ -15,7 +15,9 @@ namespace BTCPayServer.Client.Models
|
||||
[JsonConverter(typeof(OutpointJsonConverter))]
|
||||
public OutPoint Outpoint { get; set; }
|
||||
public string Link { get; set; }
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
public Dictionary<string, LabelData> Labels { get; set; }
|
||||
#pragma warning restore CS0612 // Type or member is obsolete
|
||||
[JsonConverter(typeof(DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
[JsonConverter(typeof(KeyPathJsonConverter))]
|
||||
|
@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using BTCPayServer.Client.JsonConverters;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using BTCPayServer.Lightning;
|
||||
@ -19,5 +20,8 @@ namespace BTCPayServer.Client.Models
|
||||
|
||||
[JsonConverter(typeof(LightMoneyJsonConverter))]
|
||||
public LightMoney Amount { get; set; }
|
||||
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
|
||||
public TimeSpan? SendTimeout { get; set; }
|
||||
}
|
||||
}
|
||||
|
15
BTCPayServer.Client/Models/PayPaymentRequestRequest.cs
Normal file
15
BTCPayServer.Client/Models/PayPaymentRequestRequest.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class PayPaymentRequestRequest
|
||||
{
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal? Amount { get; set; }
|
||||
public bool? AllowPendingInvoiceReuse { get; set; }
|
||||
}
|
||||
}
|
7
BTCPayServer.Client/Models/RateSource.cs
Normal file
7
BTCPayServer.Client/Models/RateSource.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class RateSource
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
@ -31,6 +31,10 @@ namespace BTCPayServer.Client.Models
|
||||
public bool AnyoneCanCreateInvoice { get; set; }
|
||||
public string DefaultCurrency { get; set; }
|
||||
public bool RequiresRefundEmail { get; set; }
|
||||
|
||||
public string CheckoutFormId { get; set; }
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public CheckoutType CheckoutType { get; set; }
|
||||
public bool LightningAmountInSatoshi { get; set; }
|
||||
public bool LightningPrivateRouteHints { get; set; }
|
||||
public bool OnChainWithLnInvoiceFallback { get; set; }
|
||||
@ -66,6 +70,12 @@ namespace BTCPayServer.Client.Models
|
||||
public IDictionary<string, JToken> AdditionalData { get; set; }
|
||||
}
|
||||
|
||||
public enum CheckoutType
|
||||
{
|
||||
V1,
|
||||
V2
|
||||
}
|
||||
|
||||
public enum NetworkFeeMode
|
||||
{
|
||||
MultiplePaymentsOnly,
|
||||
|
10
BTCPayServer.Client/Models/StoreRateConfiguration.cs
Normal file
10
BTCPayServer.Client/Models/StoreRateConfiguration.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class StoreRateConfiguration
|
||||
{
|
||||
public decimal Spread { get; set; }
|
||||
public bool IsCustomScript { get; set; }
|
||||
public string EffectiveScript { get; set; }
|
||||
public string PreferredSource { get; set; }
|
||||
}
|
||||
}
|
10
BTCPayServer.Client/Models/StoreRatePreviewResult.cs
Normal file
10
BTCPayServer.Client/Models/StoreRatePreviewResult.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class StoreRatePreviewResult
|
||||
{
|
||||
public string CurrencyPair { get; set; }
|
||||
public decimal? Rate { get; set; }
|
||||
public List<string> Errors { get; set; }
|
||||
}
|
7
BTCPayServer.Client/Models/StoreRateResult.cs
Normal file
7
BTCPayServer.Client/Models/StoreRateResult.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public class StoreRateResult
|
||||
{
|
||||
public string CurrencyPair { get; set; }
|
||||
public decimal Rate { get; set; }
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitMonetaryUnit()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("MUE");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "MonetaryUnit",
|
||||
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://explorer.monetaryunit.org/#/MUE/mainnet/tx/{0}" : "https://explorer.monetaryunit.org/#/MUE/mainnet/tx/{0}",
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"MUE_X = MUE_BTC * BTC_X",
|
||||
"MUE_BTC = bittrex(MUE_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/monetaryunit.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("31'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -22,7 +22,7 @@ namespace BTCPayServer
|
||||
"LBTC_X = LBTC_BTC * BTC_X",
|
||||
"LBTC_BTC = 1",
|
||||
},
|
||||
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://blockstream.info/liquid/tx/{0}" : "https://blockstream.info/testnet/liquid/tx/{0}",
|
||||
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://liquid.network/tx/{0}" : "https://liquid.network/testnet/tx/{0}",
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
CryptoImagePath = "imlegacy/liquid.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
|
@ -21,7 +21,7 @@ namespace BTCPayServer
|
||||
},
|
||||
AssetId = new uint256("ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2"),
|
||||
DisplayName = "Liquid Tether",
|
||||
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://blockstream.info/liquid/tx/{0}" : "https://blockstream.info/testnet/liquid/tx/{0}",
|
||||
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://liquid.network/tx/{0}" : "https://liquid.network/testnet/tx/{0}",
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
CryptoImagePath = "imlegacy/liquid-tether.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
@ -44,7 +44,7 @@ namespace BTCPayServer
|
||||
Divisibility = 2,
|
||||
AssetId = new uint256("aa775044c32a7df391902b3659f46dfe004ccb2644ce2ddc7dba31e889391caf"),
|
||||
DisplayName = "Ethiopian Birr",
|
||||
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://blockstream.info/liquid/tx/{0}" : "https://blockstream.info/testnet/liquid/tx/{0}",
|
||||
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://liquid.network/tx/{0}" : "https://liquid.network/testnet/tx/{0}",
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
CryptoImagePath = "imlegacy/etb.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
@ -66,7 +66,7 @@ namespace BTCPayServer
|
||||
},
|
||||
AssetId = new uint256("0e99c1a6da379d1f4151fb9df90449d40d0608f6cb33a5bcbfc8c265f42bab0a"),
|
||||
DisplayName = "Liquid CAD",
|
||||
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://blockstream.info/liquid/tx/{0}" : "https://blockstream.info/testnet/liquid/tx/{0}",
|
||||
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://liquid.network/tx/{0}" : "https://liquid.network/testnet/tx/{0}",
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
CryptoImagePath = "imlegacy/lcad.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
|
@ -13,9 +13,9 @@ namespace BTCPayServer
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Bitcoin",
|
||||
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://blockstream.info/tx/{0}" :
|
||||
NetworkType == Bitcoin.Instance.Signet.ChainName ? "https://explorer.bc-2.jp/tx/{0}"
|
||||
: "https://blockstream.info/testnet/tx/{0}",
|
||||
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://mempool.space/tx/{0}" :
|
||||
NetworkType == Bitcoin.Instance.Signet.ChainName ? "https://mempool.space/signet/tx/{0}"
|
||||
: "https://mempool.space/testnet/tx/{0}",
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
CryptoImagePath = "imlegacy/bitcoin.svg",
|
||||
LightningImagePath = "imlegacy/bitcoin-lightning.svg",
|
||||
|
@ -58,7 +58,7 @@ namespace BTCPayServer
|
||||
InitZcash();
|
||||
InitChaincoin();
|
||||
// InitArgoneum();//their rate source is down 9/15/20.
|
||||
InitMonetaryUnit();
|
||||
// InitMonetaryUnit(); Not supported from Bittrex from 11/23/2022, dead shitcoin
|
||||
|
||||
// Assume that electrum mappings are same as BTC if not specified
|
||||
foreach (var network in _Networks.Values.OfType<BTCPayNetwork>())
|
||||
|
@ -4,7 +4,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="4.2.0" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="4.2.1" />
|
||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.18" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(Altcoins)' != 'true'">
|
||||
<Compile Remove="Altcoins\**\*.cs"></Compile>
|
||||
|
@ -59,7 +59,11 @@ namespace BTCPayServer.Data
|
||||
public DbSet<U2FDevice> U2FDevices { get; set; }
|
||||
public DbSet<Fido2Credential> Fido2Credentials { get; set; }
|
||||
public DbSet<UserStore> UserStore { get; set; }
|
||||
[Obsolete]
|
||||
public DbSet<WalletData> Wallets { get; set; }
|
||||
public DbSet<WalletObjectData> WalletObjects { get; set; }
|
||||
public DbSet<WalletObjectLinkData> WalletObjectLinks { get; set; }
|
||||
[Obsolete]
|
||||
public DbSet<WalletTransactionData> WalletTransactions { get; set; }
|
||||
public DbSet<WebhookDeliveryData> WebhookDeliveries { get; set; }
|
||||
public DbSet<WebhookData> Webhooks { get; set; }
|
||||
@ -109,7 +113,11 @@ namespace BTCPayServer.Data
|
||||
Fido2Credential.OnModelCreating(builder);
|
||||
BTCPayServer.Data.UserStore.OnModelCreating(builder);
|
||||
//WalletData.OnModelCreating(builder);
|
||||
WalletObjectData.OnModelCreating(builder, Database);
|
||||
WalletObjectLinkData.OnModelCreating(builder, Database);
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
WalletTransactionData.OnModelCreating(builder);
|
||||
#pragma warning restore CS0612 // Type or member is obsolete
|
||||
WebhookDeliveryData.OnModelCreating(builder);
|
||||
LightningAddressData.OnModelCreating(builder);
|
||||
PayoutProcessorData.OnModelCreating(builder);
|
||||
|
@ -3,11 +3,11 @@
|
||||
<Import Project="../Build/Common.csproj" />
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.7">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.9">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.9" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />
|
||||
|
@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
[Obsolete]
|
||||
public class WalletData
|
||||
{
|
||||
[System.ComponentModel.DataAnnotations.Key]
|
||||
|
44
BTCPayServer.Data/Data/WalletObjectData.cs
Normal file
44
BTCPayServer.Data/Data/WalletObjectData.cs
Normal file
@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class WalletObjectData
|
||||
{
|
||||
public class Types
|
||||
{
|
||||
public const string Label = "label";
|
||||
public const string Tx = "tx";
|
||||
}
|
||||
public string WalletId { get; set; }
|
||||
public string Type { get; set; }
|
||||
public string Id { get; set; }
|
||||
public string Data { get; set; }
|
||||
|
||||
public List<WalletObjectLinkData> ChildLinks { get; set; }
|
||||
public List<WalletObjectLinkData> ParentLinks { get; set; }
|
||||
|
||||
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||
{
|
||||
builder.Entity<WalletObjectData>().HasKey(o =>
|
||||
new
|
||||
{
|
||||
o.WalletId,
|
||||
o.Type,
|
||||
o.Id,
|
||||
});
|
||||
|
||||
if (databaseFacade.IsNpgsql())
|
||||
{
|
||||
builder.Entity<WalletObjectData>()
|
||||
.Property(o => o.Data)
|
||||
.HasColumnType("JSONB");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
61
BTCPayServer.Data/Data/WalletObjectLinkData.cs
Normal file
61
BTCPayServer.Data/Data/WalletObjectLinkData.cs
Normal file
@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class WalletObjectLinkData
|
||||
{
|
||||
public string WalletId { get; set; }
|
||||
public string ParentType { get; set; }
|
||||
public string ParentId { get; set; }
|
||||
public string ChildType { get; set; }
|
||||
public string ChildId { get; set; }
|
||||
public string Data { get; set; }
|
||||
|
||||
public WalletObjectData Parent { get; set; }
|
||||
public WalletObjectData Child { get; set; }
|
||||
|
||||
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||
{
|
||||
builder.Entity<WalletObjectLinkData>().HasKey(o =>
|
||||
new
|
||||
{
|
||||
o.WalletId,
|
||||
o.ParentType,
|
||||
o.ParentId,
|
||||
o.ChildType,
|
||||
o.ChildId,
|
||||
});
|
||||
builder.Entity<WalletObjectLinkData>().HasIndex(o => new
|
||||
{
|
||||
o.WalletId,
|
||||
o.ChildType,
|
||||
o.ChildId,
|
||||
});
|
||||
|
||||
builder.Entity<WalletObjectLinkData>()
|
||||
.HasOne(o => o.Parent)
|
||||
.WithMany(o => o.ChildLinks)
|
||||
.HasForeignKey(o => new { o.WalletId, o.ParentType, o.ParentId })
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.Entity<WalletObjectLinkData>()
|
||||
.HasOne(o => o.Child)
|
||||
.WithMany(o => o.ParentLinks)
|
||||
.HasForeignKey(o => new { o.WalletId, o.ChildType, o.ChildId })
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
if (databaseFacade.IsNpgsql())
|
||||
{
|
||||
builder.Entity<WalletObjectLinkData>()
|
||||
.Property(o => o.Data)
|
||||
.HasColumnType("JSONB");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
[Obsolete]
|
||||
public class WalletTransactionData
|
||||
{
|
||||
public string WalletDataId { get; set; }
|
||||
|
77
BTCPayServer.Data/Migrations/20220929132704_label.cs
Normal file
77
BTCPayServer.Data/Migrations/20220929132704_label.cs
Normal file
@ -0,0 +1,77 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20220929132704_label")]
|
||||
public partial class label : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "WalletObjects",
|
||||
columns: table => new
|
||||
{
|
||||
WalletId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Type = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Data = table.Column<string>(type: migrationBuilder.IsNpgsql() ? "JSONB" : "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_WalletObjects", x => new { x.WalletId, x.Type, x.Id });
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "WalletObjectLinks",
|
||||
columns: table => new
|
||||
{
|
||||
WalletId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ParentType = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ParentId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ChildType = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ChildId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Data = table.Column<string>(type: migrationBuilder.IsNpgsql() ? "JSONB" : "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_WalletObjectLinks", x => new { x.WalletId, x.ParentType, x.ParentId, x.ChildType, x.ChildId });
|
||||
table.ForeignKey(
|
||||
name: "FK_WalletObjectLinks_WalletObjects_WalletId_ChildType_ChildId",
|
||||
columns: x => new { x.WalletId, x.ChildType, x.ChildId },
|
||||
principalTable: "WalletObjects",
|
||||
principalColumns: new[] { "WalletId", "Type", "Id" },
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_WalletObjectLinks_WalletObjects_WalletId_ParentType_ParentId",
|
||||
columns: x => new { x.WalletId, x.ParentType, x.ParentId },
|
||||
principalTable: "WalletObjects",
|
||||
principalColumns: new[] { "WalletId", "Type", "Id" },
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_WalletObjectLinks_WalletId_ChildType_ChildId",
|
||||
table: "WalletObjectLinks",
|
||||
columns: new[] { "WalletId", "ChildType", "ChildId" });
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "WalletObjectLinks");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "WalletObjects");
|
||||
}
|
||||
}
|
||||
}
|
@ -16,7 +16,7 @@ namespace BTCPayServer.Migrations
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "6.0.1");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "6.0.7");
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
@ -189,6 +189,7 @@ namespace BTCPayServer.Migrations
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@ -845,6 +846,52 @@ namespace BTCPayServer.Migrations
|
||||
b.ToTable("Wallets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WalletObjectData", b =>
|
||||
{
|
||||
b.Property<string>("WalletId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("WalletId", "Type", "Id");
|
||||
|
||||
b.ToTable("WalletObjects");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WalletObjectLinkData", b =>
|
||||
{
|
||||
b.Property<string>("WalletId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ParentType")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ParentId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ChildType")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ChildId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("WalletId", "ParentType", "ParentId", "ChildType", "ChildId");
|
||||
|
||||
b.HasIndex("WalletId", "ChildType", "ChildId");
|
||||
|
||||
b.ToTable("WalletObjectLinks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WalletTransactionData", b =>
|
||||
{
|
||||
b.Property<string>("WalletDataId")
|
||||
@ -1333,6 +1380,25 @@ namespace BTCPayServer.Migrations
|
||||
b.Navigation("StoreData");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WalletObjectLinkData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.WalletObjectData", "Child")
|
||||
.WithMany("ParentLinks")
|
||||
.HasForeignKey("WalletId", "ChildType", "ChildId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("BTCPayServer.Data.WalletObjectData", "Parent")
|
||||
.WithMany("ChildLinks")
|
||||
.HasForeignKey("WalletId", "ParentType", "ParentId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Child");
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WalletTransactionData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.WalletData", "WalletData")
|
||||
@ -1475,6 +1541,13 @@ namespace BTCPayServer.Migrations
|
||||
b.Navigation("WalletTransactions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WalletObjectData", b =>
|
||||
{
|
||||
b.Navigation("ChildLinks");
|
||||
|
||||
b.Navigation("ParentLinks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.WebhookData", b =>
|
||||
{
|
||||
b.Navigation("Deliveries");
|
||||
|
@ -26,6 +26,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="McMaster.NETCore.Plugins.Mvc" Version="1.4.0" />
|
||||
<ProjectReference Include="..\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />
|
||||
<None Include="icon.png" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
|
@ -8,6 +8,7 @@ using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using McMaster.NETCore.Plugins;
|
||||
using NBitcoin.Crypto;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitcoin.Secp256k1;
|
||||
@ -33,7 +34,8 @@ namespace BTCPayServer.PluginPacker
|
||||
throw new Exception($"{rootDLLPath} could not be found");
|
||||
}
|
||||
|
||||
var assembly = Assembly.LoadFrom(rootDLLPath);
|
||||
var plugin = PluginLoader.CreateFromAssemblyFile(rootDLLPath, false, new[] { typeof(IBTCPayServerPlugin) });
|
||||
var assembly = plugin.LoadAssembly(name);
|
||||
var extension = GetAllExtensionTypesFromAssembly(assembly).FirstOrDefault();
|
||||
if (extension is null)
|
||||
{
|
||||
|
@ -13,7 +13,7 @@
|
||||
<EmbeddedResource Include="Resources\**" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.7">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.9">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
@ -4,9 +4,9 @@
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.2.0" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" />
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.9" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.10" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.14" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="1.0.2" />
|
||||
</ItemGroup>
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Services.Rates;
|
||||
|
||||
namespace BTCPayServer.Rating
|
||||
@ -56,6 +57,13 @@ namespace BTCPayServer.Rating
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (splitted.Length > 2)
|
||||
{
|
||||
// Some shitcoin have _ their own ticker name... Since we don't care about those, let's
|
||||
// parse it anyway assuming the first part is one currency.
|
||||
value = new CurrencyPair(splitted[0], string.Join("_", splitted.Skip(1).ToArray()));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
@ -44,9 +44,6 @@ namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
if (notFoundSymbols.TryGetValue(ticker.Key, out _))
|
||||
return null;
|
||||
if (ticker.Key.Contains("XMR"))
|
||||
{
|
||||
}
|
||||
try
|
||||
{
|
||||
CurrencyPair pair;
|
||||
|
@ -11,6 +11,8 @@ using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Models.WalletViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
@ -617,6 +619,7 @@ namespace BTCPayServer.Tests
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
user.RegisterDerivationScheme("LTC");
|
||||
var apps = user.GetController<UIAppsController>();
|
||||
var pos = user.GetController<UIPointOfSaleController>();
|
||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
|
||||
var appType = AppType.PointOfSale.ToString();
|
||||
vm.AppName = "test";
|
||||
@ -624,8 +627,10 @@ namespace BTCPayServer.Tests
|
||||
Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
|
||||
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||
var app = appList.Apps[0];
|
||||
apps.HttpContext.SetAppData(new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType });
|
||||
var vmpos = await apps.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
|
||||
apps.HttpContext.SetAppData(appData);
|
||||
pos.HttpContext.SetAppData(appData);
|
||||
var vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
vmpos.Title = "hello";
|
||||
vmpos.Currency = "CAD";
|
||||
vmpos.ButtonText = "{0} Purchase";
|
||||
@ -642,11 +647,11 @@ donation:
|
||||
price: 1.02
|
||||
custom: true
|
||||
";
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
vmpos = await apps.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
Assert.Equal("hello", vmpos.Title);
|
||||
|
||||
var publicApps = user.GetController<UIAppsPublicController>();
|
||||
var publicApps = user.GetController<UIPointOfSaleController>();
|
||||
var vmview = await publicApps.ViewPointOfSale(app.Id, PosViewType.Cart).AssertViewModelAsync<ViewPointOfSaleViewModel>();
|
||||
Assert.Equal("hello", vmview.Title);
|
||||
Assert.Equal(3, vmview.Items.Length);
|
||||
@ -698,7 +703,7 @@ donation:
|
||||
})
|
||||
{
|
||||
TestLogs.LogInformation($"Testing for {test.Code}");
|
||||
vmpos = await apps.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
vmpos.Title = "hello";
|
||||
vmpos.Currency = test.Item1;
|
||||
vmpos.ButtonText = "{0} Purchase";
|
||||
@ -714,8 +719,8 @@ donation:
|
||||
price: 1.02
|
||||
custom: true
|
||||
";
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
publicApps = user.GetController<UIAppsPublicController>();
|
||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
publicApps = user.GetController<UIPointOfSaleController>();
|
||||
vmview = await publicApps.ViewPointOfSale(app.Id, PosViewType.Cart).AssertViewModelAsync<ViewPointOfSaleViewModel>();
|
||||
Assert.Equal(test.Code, vmview.CurrencyCode);
|
||||
Assert.Equal(test.ExpectedSymbol,
|
||||
@ -731,7 +736,7 @@ donation:
|
||||
}
|
||||
|
||||
//test inventory related features
|
||||
vmpos = await apps.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
vmpos.Title = "hello";
|
||||
vmpos.Currency = "BTC";
|
||||
vmpos.Template = @"
|
||||
@ -741,7 +746,7 @@ inventoryitem:
|
||||
inventory: 1
|
||||
noninventoryitem:
|
||||
price: 10.0";
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
|
||||
//inventoryitem has 1 item available
|
||||
await tester.WaitForEvent<AppInventoryUpdaterHostedService.UpdateAppInventory>(() =>
|
||||
@ -777,13 +782,13 @@ noninventoryitem:
|
||||
//check that item is back in stock
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
vmpos = await apps.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
Assert.Equal(1,
|
||||
appService.Parse(vmpos.Template, "BTC").Single(item => item.Id == "inventoryitem").Inventory);
|
||||
}, 10000);
|
||||
|
||||
//test payment methods option
|
||||
vmpos = await apps.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
vmpos.Title = "hello";
|
||||
vmpos.Currency = "BTC";
|
||||
vmpos.Template = @"
|
||||
@ -794,7 +799,7 @@ btconly:
|
||||
- BTC
|
||||
normal:
|
||||
price: 1.0";
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, null, null, null, null, "btconly").Result);
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
@ -838,8 +843,8 @@ g:
|
||||
custom: topup
|
||||
";
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
vmpos = await apps.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
Assert.DoesNotContain("custom", vmpos.Template);
|
||||
var items = appService.Parse(vmpos.Template, vmpos.Currency);
|
||||
Assert.Contains(items, item => item.Id == "a" && item.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed);
|
||||
|
@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Import Project="../Build/Common.csproj" />
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
@ -19,12 +19,12 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
|
||||
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14" />
|
||||
<PackageReference Include="Selenium.Support" Version="4.1.1" />
|
||||
<PackageReference Include="Selenium.WebDriver" Version="4.1.1" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="104.0.5112.7900" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="105.0.5195.5200" />
|
||||
<PackageReference Include="xunit" Version="2.4.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
|
@ -4,6 +4,8 @@ using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Plugins.Crowdfund.Controllers;
|
||||
using BTCPayServer.Plugins.Crowdfund.Models;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
@ -35,6 +37,7 @@ namespace BTCPayServer.Tests
|
||||
var stores = user.GetController<UIStoresController>();
|
||||
var apps = user.GetController<UIAppsController>();
|
||||
var apps2 = user2.GetController<UIAppsController>();
|
||||
var crowdfund = user.GetController<UICrowdfundController>();
|
||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
|
||||
var appType = AppType.Crowdfund.ToString();
|
||||
Assert.NotNull(vm.SelectedAppType);
|
||||
@ -42,10 +45,12 @@ namespace BTCPayServer.Tests
|
||||
vm.AppName = "test";
|
||||
vm.SelectedAppType = appType;
|
||||
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
|
||||
Assert.Equal(nameof(apps.UpdateCrowdfund), redirectToAction.ActionName);
|
||||
Assert.Equal(nameof(crowdfund.UpdateCrowdfund), redirectToAction.ActionName);
|
||||
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||
var app = appList.Apps[0];
|
||||
apps.HttpContext.SetAppData(new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType});
|
||||
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
|
||||
apps.HttpContext.SetAppData(appData);
|
||||
crowdfund.HttpContext.SetAppData(appData);
|
||||
var appList2 =
|
||||
Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps2.ListApps(user2.StoreId).Result).Model);
|
||||
Assert.Single(appList.Apps);
|
||||
@ -72,6 +77,7 @@ namespace BTCPayServer.Tests
|
||||
await user.GrantAccessAsync();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
var apps = user.GetController<UIAppsController>();
|
||||
var crowdfund = user.GetController<UICrowdfundController>();
|
||||
var vm = apps.CreateApp(user.StoreId).AssertViewModel<CreateAppViewModel>();
|
||||
var appType = AppType.Crowdfund.ToString();
|
||||
vm.AppName = "test";
|
||||
@ -79,18 +85,20 @@ namespace BTCPayServer.Tests
|
||||
Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
|
||||
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||
var app = appList.Apps[0];
|
||||
apps.HttpContext.SetAppData(new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType });
|
||||
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
|
||||
apps.HttpContext.SetAppData(appData);
|
||||
crowdfund.HttpContext.SetAppData(appData);
|
||||
|
||||
//Scenario 1: Not Enabled - Not Allowed
|
||||
var crowdfundViewModel = await apps.UpdateCrowdfund(app.Id).AssertViewModelAsync<UpdateCrowdfundViewModel>();
|
||||
var crowdfundViewModel = await crowdfund.UpdateCrowdfund(app.Id).AssertViewModelAsync<UpdateCrowdfundViewModel>();
|
||||
crowdfundViewModel.TargetCurrency = "BTC";
|
||||
crowdfundViewModel.Enabled = false;
|
||||
crowdfundViewModel.EndDate = null;
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
|
||||
var anonAppPubsController = tester.PayTester.GetController<UIAppsPublicController>();
|
||||
var publicApps = user.GetController<UIAppsPublicController>();
|
||||
var anonAppPubsController = tester.PayTester.GetController<UICrowdfundController>();
|
||||
var crowdfundController = user.GetController<UICrowdfundController>();
|
||||
|
||||
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund()
|
||||
{
|
||||
@ -100,19 +108,19 @@ namespace BTCPayServer.Tests
|
||||
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(app.Id, string.Empty));
|
||||
|
||||
//Scenario 2: Not Enabled But Admin - Allowed
|
||||
Assert.IsType<OkObjectResult>(await publicApps.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund()
|
||||
Assert.IsType<OkObjectResult>(await crowdfundController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund()
|
||||
{
|
||||
RedirectToCheckout = false,
|
||||
Amount = new decimal(0.01)
|
||||
}, default));
|
||||
Assert.IsType<ViewResult>(await publicApps.ViewCrowdfund(app.Id, string.Empty));
|
||||
Assert.IsType<ViewResult>(await crowdfundController.ViewCrowdfund(app.Id, string.Empty));
|
||||
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(app.Id, string.Empty));
|
||||
|
||||
//Scenario 3: Enabled But Start Date > Now - Not Allowed
|
||||
crowdfundViewModel.StartDate = DateTime.Today.AddDays(2);
|
||||
crowdfundViewModel.Enabled = true;
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(0.01)
|
||||
@ -123,7 +131,7 @@ namespace BTCPayServer.Tests
|
||||
crowdfundViewModel.EndDate = DateTime.Today.AddDays(-1);
|
||||
crowdfundViewModel.Enabled = true;
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(0.01)
|
||||
@ -136,7 +144,7 @@ namespace BTCPayServer.Tests
|
||||
crowdfundViewModel.TargetAmount = 1;
|
||||
crowdfundViewModel.TargetCurrency = "BTC";
|
||||
crowdfundViewModel.EnforceTargetAmount = true;
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(1.01)
|
||||
@ -160,6 +168,7 @@ namespace BTCPayServer.Tests
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
await user.SetNetworkFeeMode(NetworkFeeMode.Never);
|
||||
var apps = user.GetController<UIAppsController>();
|
||||
var crowdfund = user.GetController<UICrowdfundController>();
|
||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
|
||||
var appType = AppType.Crowdfund.ToString();
|
||||
vm.AppName = "test";
|
||||
@ -167,20 +176,21 @@ namespace BTCPayServer.Tests
|
||||
Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
|
||||
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||
var app = appList.Apps[0];
|
||||
apps.HttpContext.SetAppData(new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType });
|
||||
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
|
||||
apps.HttpContext.SetAppData(appData);
|
||||
crowdfund.HttpContext.SetAppData(appData);
|
||||
|
||||
TestLogs.LogInformation("We create an invoice with a hardcap");
|
||||
var crowdfundViewModel = await apps.UpdateCrowdfund(app.Id).AssertViewModelAsync<UpdateCrowdfundViewModel>();
|
||||
var crowdfundViewModel = await crowdfund.UpdateCrowdfund(app.Id).AssertViewModelAsync<UpdateCrowdfundViewModel>();
|
||||
crowdfundViewModel.Enabled = true;
|
||||
crowdfundViewModel.EndDate = null;
|
||||
crowdfundViewModel.TargetAmount = 100;
|
||||
crowdfundViewModel.TargetCurrency = "BTC";
|
||||
crowdfundViewModel.UseAllStoreInvoices = true;
|
||||
crowdfundViewModel.EnforceTargetAmount = true;
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
|
||||
var anonAppPubsController = tester.PayTester.GetController<UIAppsPublicController>();
|
||||
var publicApps = user.GetController<UIAppsPublicController>();
|
||||
var publicApps = user.GetController<UICrowdfundController>();
|
||||
|
||||
var model = Assert.IsType<ViewCrowdfundViewModel>(Assert
|
||||
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id, String.Empty).Result).Model);
|
||||
@ -232,7 +242,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains(AppService.GetAppInternalTag(app.Id), invoiceEntity.InternalTags);
|
||||
|
||||
crowdfundViewModel.UseAllStoreInvoices = false;
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
|
||||
TestLogs.LogInformation("Because UseAllStoreInvoices is false, let's make sure the invoice is not tagged");
|
||||
invoice = await user.BitPay.CreateInvoiceAsync(new Invoice
|
||||
@ -251,7 +261,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>(apps.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
invoice = await user.BitPay.CreateInvoiceAsync(new Invoice
|
||||
{
|
||||
Buyer = new Buyer { email = "test@fwf.com" },
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:6.0.101-bullseye-slim AS builder
|
||||
FROM mcr.microsoft.com/dotnet/sdk:6.0.401-bullseye-slim AS builder
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends chromium-driver \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
@ -483,93 +483,6 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
#endif
|
||||
|
||||
[Fact]
|
||||
public void CanParseLegacyLabels()
|
||||
{
|
||||
static void AssertContainsRawLabel(WalletTransactionInfo info)
|
||||
{
|
||||
foreach (var item in new[] { "blah", "lol", "hello" })
|
||||
{
|
||||
Assert.True(info.Labels.ContainsKey(item));
|
||||
var rawLabel = Assert.IsType<RawLabel>(info.Labels[item]);
|
||||
Assert.Equal("raw", rawLabel.Type);
|
||||
Assert.Equal(item, rawLabel.Text);
|
||||
}
|
||||
}
|
||||
var data = new WalletTransactionData();
|
||||
data.Labels = "blah,lol,hello,lol";
|
||||
var info = data.GetBlobInfo();
|
||||
Assert.Equal(3, info.Labels.Count);
|
||||
AssertContainsRawLabel(info);
|
||||
data.SetBlobInfo(info);
|
||||
Assert.Contains("raw", data.Labels);
|
||||
Assert.Contains("{", data.Labels);
|
||||
Assert.Contains("[", data.Labels);
|
||||
info = data.GetBlobInfo();
|
||||
AssertContainsRawLabel(info);
|
||||
|
||||
|
||||
data = new WalletTransactionData()
|
||||
{
|
||||
Labels = "pos",
|
||||
Blob = Encoders.Hex.DecodeData("1f8b08000000000000037abf7b7fb592737e6e6e6a5e89929592522d000000ffff030036bc6ad911000000")
|
||||
};
|
||||
info = data.GetBlobInfo();
|
||||
var label = Assert.Single(info.Labels);
|
||||
Assert.Equal("raw", label.Value.Type);
|
||||
Assert.Equal("pos", label.Value.Text);
|
||||
Assert.Equal("pos", label.Key);
|
||||
|
||||
|
||||
static void AssertContainsLabel(WalletTransactionInfo info)
|
||||
{
|
||||
Assert.Equal(2, info.Labels.Count);
|
||||
var invoiceLabel = Assert.IsType<ReferenceLabel>(info.Labels["invoice"]);
|
||||
Assert.Equal("BFm1MCJPBCDeRoWXvPcwnM", invoiceLabel.Reference);
|
||||
Assert.Equal("invoice", invoiceLabel.Text);
|
||||
Assert.Equal("invoice", invoiceLabel.Type);
|
||||
|
||||
var appLabel = Assert.IsType<ReferenceLabel>(info.Labels["app"]);
|
||||
Assert.Equal("87kj5yKay8mB4UUZcJhZH5TqDKMD3CznjwLjiu1oYZXe", appLabel.Reference);
|
||||
Assert.Equal("app", appLabel.Text);
|
||||
Assert.Equal("app", appLabel.Type);
|
||||
}
|
||||
data = new WalletTransactionData()
|
||||
{
|
||||
Labels = "[\"{\\n \\\"value\\\": \\\"invoice\\\",\\n \\\"id\\\": \\\"BFm1MCJPBCDeRoWXvPcwnM\\\"\\n}\",\"{\\n \\\"value\\\": \\\"app\\\",\\n \\\"id\\\": \\\"87kj5yKay8mB4UUZcJhZH5TqDKMD3CznjwLjiu1oYZXe\\\"\\n}\"]",
|
||||
};
|
||||
info = data.GetBlobInfo();
|
||||
AssertContainsLabel(info);
|
||||
data.SetBlobInfo(info);
|
||||
info = data.GetBlobInfo();
|
||||
AssertContainsLabel(info);
|
||||
|
||||
static void AssertPayoutLabel(WalletTransactionInfo info)
|
||||
{
|
||||
Assert.Single(info.Labels);
|
||||
var l = Assert.IsType<PayoutLabel>(info.Labels["payout"]);
|
||||
Assert.Single(Assert.Single(l.PullPaymentPayouts, k => k.Key == "pullPaymentId").Value, "payoutId");
|
||||
Assert.Equal("walletId", l.WalletId);
|
||||
}
|
||||
|
||||
var payoutId = "payoutId";
|
||||
var pullPaymentId = "pullPaymentId";
|
||||
var walletId = "walletId";
|
||||
// How it was serialized before
|
||||
|
||||
data = new WalletTransactionData()
|
||||
{
|
||||
Labels = new JArray(JObject.FromObject(new { value = "payout", id = payoutId, pullPaymentId, walletId })).ToString()
|
||||
};
|
||||
info = data.GetBlobInfo();
|
||||
AssertPayoutLabel(info);
|
||||
data.SetBlobInfo(info);
|
||||
info = data.GetBlobInfo();
|
||||
AssertPayoutLabel(info);
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Fact]
|
||||
public void DeterministicUTXOSorter()
|
||||
{
|
||||
@ -1294,6 +1207,9 @@ namespace BTCPayServer.Tests
|
||||
[Fact]
|
||||
public void CanParseRateRules()
|
||||
{
|
||||
var pair = CurrencyPair.Parse("USD_EMAT_IC");
|
||||
Assert.Equal("USD", pair.Left);
|
||||
Assert.Equal("EMAT_IC", pair.Right);
|
||||
// Check happy path
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.AppendLine("// Some cool comments");
|
||||
@ -1389,7 +1305,7 @@ namespace BTCPayServer.Tests
|
||||
rule2.Reevaluate();
|
||||
Assert.False(rule2.HasError);
|
||||
Assert.Equal("5000 * 2000.4 * 1.1", rule2.ToString(true));
|
||||
Assert.Equal(rule2.BidAsk.Bid, 5000m * 2000.4m * 1.1m);
|
||||
Assert.Equal(5000m * 2000.4m * 1.1m, rule2.BidAsk.Bid);
|
||||
////////
|
||||
|
||||
// Make sure parenthesis are correctly calculated
|
||||
|
@ -19,6 +19,7 @@ using BTCPayServer.Services.Custodian.Client.MockCustodian;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Notifications;
|
||||
using BTCPayServer.Services.Notifications.Blobs;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -88,7 +89,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal("missing-permission", e.APIError.Code);
|
||||
Assert.NotNull(e.APIError.Message);
|
||||
GreenfieldPermissionAPIError permissionError = Assert.IsType<GreenfieldPermissionAPIError>(e.APIError);
|
||||
Assert.Equal(permissionError.MissingPermission, Policies.CanModifyStoreSettings);
|
||||
Assert.Equal(Policies.CanModifyStoreSettings, permissionError.MissingPermission);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
@ -195,7 +196,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanCreateReadAndDeletePointOfSaleApp()
|
||||
public async Task CanCreateReadUpdateAndDeletePointOfSaleApp()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
@ -203,8 +204,58 @@ namespace BTCPayServer.Tests
|
||||
await user.RegisterDerivationSchemeAsync("BTC");
|
||||
var client = await user.CreateClient();
|
||||
|
||||
// Test creating a POS app
|
||||
var app = await client.CreatePointOfSaleApp(user.StoreId, new CreatePointOfSaleAppRequest() { AppName = "test app from API" });
|
||||
// Test validation for creating the app
|
||||
await AssertValidationError(new[] { "AppName" },
|
||||
async () => await client.CreatePointOfSaleApp(user.StoreId, new CreatePointOfSaleAppRequest() {}));
|
||||
await AssertValidationError(new[] { "AppName" },
|
||||
async () => await client.CreatePointOfSaleApp(
|
||||
user.StoreId,
|
||||
new CreatePointOfSaleAppRequest()
|
||||
{
|
||||
AppName = "this is a really long app name this is a really long app name this is a really long app name",
|
||||
}
|
||||
)
|
||||
);
|
||||
await AssertValidationError(new[] { "Currency" },
|
||||
async () => await client.CreatePointOfSaleApp(
|
||||
user.StoreId,
|
||||
new CreatePointOfSaleAppRequest()
|
||||
{
|
||||
AppName = "good name",
|
||||
Currency = "fake currency"
|
||||
}
|
||||
)
|
||||
);
|
||||
await AssertValidationError(new[] { "Template" },
|
||||
async () => await client.CreatePointOfSaleApp(
|
||||
user.StoreId,
|
||||
new CreatePointOfSaleAppRequest()
|
||||
{
|
||||
AppName = "good name",
|
||||
Template = "lol invalid template"
|
||||
}
|
||||
)
|
||||
);
|
||||
await AssertValidationError(new[] { "AppName", "Currency", "Template" },
|
||||
async () => await client.CreatePointOfSaleApp(
|
||||
user.StoreId,
|
||||
new CreatePointOfSaleAppRequest()
|
||||
{
|
||||
Currency = "fake currency",
|
||||
Template = "lol invalid template"
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Test creating a POS app successfully
|
||||
var app = await client.CreatePointOfSaleApp(
|
||||
user.StoreId,
|
||||
new CreatePointOfSaleAppRequest()
|
||||
{
|
||||
AppName = "test app from API",
|
||||
Currency = "JPY"
|
||||
}
|
||||
);
|
||||
Assert.Equal("test app from API", app.Name);
|
||||
Assert.Equal(user.StoreId, app.StoreId);
|
||||
Assert.Equal("PointOfSale", app.AppType);
|
||||
@ -220,6 +271,11 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(app.StoreId, retrievedApp.StoreId);
|
||||
Assert.Equal(app.AppType, retrievedApp.AppType);
|
||||
|
||||
// Test that we can update the app data
|
||||
await client.UpdatePointOfSaleApp(app.Id, new CreatePointOfSaleAppRequest() { AppName = "new app name" });
|
||||
retrievedApp = await client.GetApp(app.Id);
|
||||
Assert.Equal("new app name", retrievedApp.Name);
|
||||
|
||||
// Make sure we return a 404 if we try to delete an app that doesn't exist
|
||||
await AssertHttpError(404, async () =>
|
||||
{
|
||||
@ -1113,25 +1169,93 @@ namespace BTCPayServer.Tests
|
||||
await client.ArchivePaymentRequest(user.StoreId, paymentRequest.Id);
|
||||
Assert.DoesNotContain(paymentRequest.Id,
|
||||
(await client.GetPaymentRequests(user.StoreId)).Select(data => data.Id));
|
||||
|
||||
//let's test some payment stuff
|
||||
var archivedPrId = paymentRequest.Id;
|
||||
//let's test some payment stuff with the UI
|
||||
await user.RegisterDerivationSchemeAsync("BTC");
|
||||
var paymentTestPaymentRequest = await client.CreatePaymentRequest(user.StoreId,
|
||||
new CreatePaymentRequestRequest() { Amount = 0.1m, Currency = "BTC", Title = "Payment test title" });
|
||||
|
||||
var invoiceId = Assert.IsType<string>(Assert.IsType<OkObjectResult>(await user.GetController<UIPaymentRequestController>()
|
||||
.PayPaymentRequest(paymentTestPaymentRequest.Id, false)).Value);
|
||||
var invoice = user.BitPay.GetInvoice(invoiceId);
|
||||
await tester.WaitForEvent<InvoiceDataChangedEvent>(async () =>
|
||||
|
||||
async Task Pay(string invoiceId, bool partialPayment = false)
|
||||
{
|
||||
await tester.ExplorerNode.SendToAddressAsync(
|
||||
BitcoinAddress.Create(invoice.BitcoinAddress, tester.ExplorerNode.Network), invoice.BtcDue);
|
||||
TestLogs.LogInformation($"Paying invoice {invoiceId}");
|
||||
var invoice = user.BitPay.GetInvoice(invoiceId);
|
||||
await tester.WaitForEvent<InvoiceDataChangedEvent>(async () =>
|
||||
{
|
||||
TestLogs.LogInformation($"Paying address {invoice.BitcoinAddress}");
|
||||
await tester.ExplorerNode.SendToAddressAsync(
|
||||
BitcoinAddress.Create(invoice.BitcoinAddress, tester.ExplorerNode.Network), invoice.BtcDue);
|
||||
});
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
Assert.Equal(Invoice.STATUS_PAID, user.BitPay.GetInvoice(invoiceId).Status);
|
||||
if (!partialPayment)
|
||||
Assert.Equal(PaymentRequestData.PaymentRequestStatus.Completed, (await client.GetPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id)).Status);
|
||||
});
|
||||
}
|
||||
await Pay(invoiceId);
|
||||
|
||||
//Same thing, but with the API
|
||||
paymentTestPaymentRequest = await client.CreatePaymentRequest(user.StoreId,
|
||||
new CreatePaymentRequestRequest() { Amount = 0.1m, Currency = "BTC", Title = "Payment test title" });
|
||||
var paidPrId = paymentTestPaymentRequest.Id;
|
||||
var invoiceData = await client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest());
|
||||
await Pay(invoiceData.Id);
|
||||
|
||||
// Let's tests some unhappy path
|
||||
paymentTestPaymentRequest = await client.CreatePaymentRequest(user.StoreId,
|
||||
new CreatePaymentRequestRequest() { Amount = 0.1m, AllowCustomPaymentAmounts = false, Currency = "BTC", Title = "Payment test title" });
|
||||
await AssertValidationError(new[] { "Amount" }, () => client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest() { Amount = -0.04m }));
|
||||
await AssertValidationError(new[] { "Amount" }, () => client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest() { Amount = 0.04m }));
|
||||
await client.UpdatePaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new UpdatePaymentRequestRequest()
|
||||
{
|
||||
Amount = 0.1m,
|
||||
AllowCustomPaymentAmounts = true,
|
||||
Currency = "BTC",
|
||||
Title = "Payment test title"
|
||||
});
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
Assert.Equal(Invoice.STATUS_PAID, user.BitPay.GetInvoice(invoiceId).Status);
|
||||
Assert.Equal(PaymentRequestData.PaymentRequestStatus.Completed, (await client.GetPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id)).Status);
|
||||
});
|
||||
await AssertValidationError(new[] { "Amount" }, () => client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest() { Amount = -0.04m }));
|
||||
invoiceData = await client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest() { Amount = 0.04m });
|
||||
Assert.Equal(0.04m, invoiceData.Amount);
|
||||
var firstPaymentId = invoiceData.Id;
|
||||
await AssertAPIError("archived", () => client.PayPaymentRequest(user.StoreId, archivedPrId, new PayPaymentRequestRequest()));
|
||||
|
||||
await client.UpdatePaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new UpdatePaymentRequestRequest()
|
||||
{
|
||||
Amount = 0.1m,
|
||||
AllowCustomPaymentAmounts = true,
|
||||
Currency = "BTC",
|
||||
Title = "Payment test title",
|
||||
ExpiryDate = DateTimeOffset.UtcNow - TimeSpan.FromDays(1.0)
|
||||
});
|
||||
|
||||
await AssertAPIError("expired", () => client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest()));
|
||||
await AssertAPIError("already-paid", () => client.PayPaymentRequest(user.StoreId, paidPrId, new PayPaymentRequestRequest()));
|
||||
|
||||
await client.UpdatePaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new UpdatePaymentRequestRequest()
|
||||
{
|
||||
Amount = 0.1m,
|
||||
AllowCustomPaymentAmounts = true,
|
||||
Currency = "BTC",
|
||||
Title = "Payment test title",
|
||||
ExpiryDate = null
|
||||
});
|
||||
|
||||
await Pay(firstPaymentId, true);
|
||||
invoiceData = await client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest());
|
||||
|
||||
Assert.Equal(0.06m, invoiceData.Amount);
|
||||
Assert.Equal("BTC", invoiceData.Currency);
|
||||
|
||||
var expectedInvoiceId = invoiceData.Id;
|
||||
invoiceData = await client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest() { AllowPendingInvoiceReuse = true });
|
||||
Assert.Equal(expectedInvoiceId, invoiceData.Id);
|
||||
|
||||
var notExpectedInvoiceId = invoiceData.Id;
|
||||
invoiceData = await client.PayPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id, new PayPaymentRequestRequest() { AllowPendingInvoiceReuse = false });
|
||||
Assert.NotEqual(notExpectedInvoiceId, invoiceData.Id);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
@ -1269,12 +1393,14 @@ namespace BTCPayServer.Tests
|
||||
Checkout = new CreateInvoiceRequest.CheckoutOptions()
|
||||
{
|
||||
RedirectAutomatically = true,
|
||||
RequiresRefundEmail = true
|
||||
RequiresRefundEmail = true,
|
||||
CheckoutFormId = GenericFormOption.Email.ToString()
|
||||
},
|
||||
AdditionalSearchTerms = new string[] { "Banana" }
|
||||
});
|
||||
Assert.True(newInvoice.Checkout.RedirectAutomatically);
|
||||
Assert.True(newInvoice.Checkout.RequiresRefundEmail);
|
||||
Assert.Equal(GenericFormOption.Email.ToString(), newInvoice.Checkout.CheckoutFormId);
|
||||
Assert.Equal(user.StoreId, newInvoice.StoreId);
|
||||
//list
|
||||
var invoices = await viewOnly.GetInvoices(user.StoreId);
|
||||
@ -1598,25 +1724,35 @@ namespace BTCPayServer.Tests
|
||||
await tester.StartAsync();
|
||||
await tester.EnsureChannelsSetup();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess(true);
|
||||
await user.GrantAccessAsync(true);
|
||||
user.RegisterLightningNode("BTC", LightningConnectionType.CLightning, false);
|
||||
|
||||
var merchant = tester.NewAccount();
|
||||
merchant.GrantAccess(true);
|
||||
await merchant.GrantAccessAsync(true);
|
||||
merchant.RegisterLightningNode("BTC", LightningConnectionType.LndREST);
|
||||
var merchantClient = await merchant.CreateClient($"{Policies.CanUseLightningNodeInStore}:{merchant.StoreId}");
|
||||
var merchantInvoice = await merchantClient.CreateLightningInvoice(merchant.StoreId, "BTC", new CreateLightningInvoiceRequest(LightMoney.Satoshis(1_000), "hey", TimeSpan.FromSeconds(60)));
|
||||
// The default client is using charge, so we should not be able to query channels
|
||||
var client = await user.CreateClient(Policies.CanUseInternalLightningNode);
|
||||
var chargeClient = await user.CreateClient(Policies.CanUseInternalLightningNode);
|
||||
|
||||
var info = await client.GetLightningNodeInfo("BTC");
|
||||
var info = await chargeClient.GetLightningNodeInfo("BTC");
|
||||
Assert.Single(info.NodeURIs);
|
||||
Assert.NotEqual(0, info.BlockHeight);
|
||||
Assert.NotNull(info.Alias);
|
||||
Assert.NotNull(info.Color);
|
||||
Assert.NotNull(info.Version);
|
||||
Assert.NotNull(info.PeersCount);
|
||||
Assert.NotNull(info.ActiveChannelsCount);
|
||||
Assert.NotNull(info.InactiveChannelsCount);
|
||||
Assert.NotNull(info.PendingChannelsCount);
|
||||
|
||||
await AssertAPIError("lightning-node-unavailable", () => client.GetLightningNodeChannels("BTC"));
|
||||
var gex = await AssertAPIError("lightning-node-unavailable", () => chargeClient.ConnectToLightningNode("BTC", new ConnectToNodeRequest(NodeInfo.Parse($"{new Key().PubKey.ToHex()}@localhost:3827"))));
|
||||
Assert.Contains("NotSupported", gex.Message);
|
||||
|
||||
await AssertAPIError("lightning-node-unavailable", () => chargeClient.GetLightningNodeChannels("BTC"));
|
||||
// Not permission for the store!
|
||||
await AssertAPIError("missing-permission", () => client.GetLightningNodeChannels(user.StoreId, "BTC"));
|
||||
var invoiceData = await client.CreateLightningInvoice("BTC", new CreateLightningInvoiceRequest()
|
||||
await AssertAPIError("missing-permission", () => chargeClient.GetLightningNodeChannels(user.StoreId, "BTC"));
|
||||
var invoiceData = await chargeClient.CreateLightningInvoice("BTC", new CreateLightningInvoiceRequest()
|
||||
{
|
||||
Amount = LightMoney.Satoshis(1000),
|
||||
Description = "lol",
|
||||
@ -1624,9 +1760,17 @@ namespace BTCPayServer.Tests
|
||||
PrivateRouteHints = false
|
||||
});
|
||||
var chargeInvoice = invoiceData;
|
||||
Assert.NotNull(await client.GetLightningInvoice("BTC", invoiceData.Id));
|
||||
Assert.NotNull(await chargeClient.GetLightningInvoice("BTC", invoiceData.Id));
|
||||
|
||||
client = await user.CreateClient($"{Policies.CanUseLightningNodeInStore}:{user.StoreId}");
|
||||
// check list for internal node
|
||||
var invoices = await chargeClient.GetLightningInvoices("BTC");
|
||||
var pendingInvoices = await chargeClient.GetLightningInvoices("BTC", true);
|
||||
Assert.NotEmpty(invoices);
|
||||
Assert.Contains(invoices, i => i.Id == invoiceData.Id);
|
||||
Assert.NotEmpty(pendingInvoices);
|
||||
Assert.Contains(pendingInvoices, i => i.Id == invoiceData.Id);
|
||||
|
||||
var client = await user.CreateClient($"{Policies.CanUseLightningNodeInStore}:{user.StoreId}");
|
||||
// Not permission for the server
|
||||
await AssertAPIError("missing-permission", () => client.GetLightningNodeChannels("BTC"));
|
||||
|
||||
@ -1644,10 +1788,22 @@ namespace BTCPayServer.Tests
|
||||
|
||||
Assert.NotNull(await client.GetLightningInvoice(user.StoreId, "BTC", invoiceData.Id));
|
||||
|
||||
await client.PayLightningInvoice(user.StoreId, "BTC", new PayLightningInvoiceRequest()
|
||||
// check pending list
|
||||
var merchantPendingInvoices = await merchantClient.GetLightningInvoices(merchant.StoreId, "BTC", true);
|
||||
Assert.NotEmpty(merchantPendingInvoices);
|
||||
Assert.Contains(merchantPendingInvoices, i => i.Id == merchantInvoice.Id);
|
||||
|
||||
var payResponse = await client.PayLightningInvoice(user.StoreId, "BTC", new PayLightningInvoiceRequest
|
||||
{
|
||||
BOLT11 = merchantInvoice.BOLT11
|
||||
});
|
||||
Assert.Equal(merchantInvoice.BOLT11, payResponse.BOLT11);
|
||||
Assert.Equal(LightningPaymentStatus.Complete, payResponse.Status);
|
||||
Assert.NotNull(payResponse.Preimage);
|
||||
Assert.NotNull(payResponse.FeeAmount);
|
||||
Assert.NotNull(payResponse.TotalAmount);
|
||||
Assert.NotNull(payResponse.PaymentHash);
|
||||
|
||||
await Assert.ThrowsAsync<GreenfieldValidationException>(async () => await client.PayLightningInvoice(user.StoreId, "BTC", new PayLightningInvoiceRequest()
|
||||
{
|
||||
BOLT11 = "lol"
|
||||
@ -1664,6 +1820,15 @@ namespace BTCPayServer.Tests
|
||||
var invoice = await merchantClient.GetLightningInvoice(merchant.StoreId, "BTC", merchantInvoice.Id);
|
||||
Assert.NotNull(invoice.PaidAt);
|
||||
Assert.Equal(LightMoney.Satoshis(1000), invoice.Amount);
|
||||
|
||||
// check list for store with paid invoice
|
||||
var merchantInvoices = await merchantClient.GetLightningInvoices(merchant.StoreId, "BTC");
|
||||
merchantPendingInvoices = await merchantClient.GetLightningInvoices(merchant.StoreId, "BTC", true);
|
||||
Assert.NotEmpty(merchantInvoices);
|
||||
Assert.Empty(merchantPendingInvoices);
|
||||
// if the test ran too many times the invoice might be on a later page
|
||||
if (merchantInvoices.Length < 100) Assert.Contains(merchantInvoices, i => i.Id == merchantInvoice.Id);
|
||||
|
||||
// Amount received might be bigger because of internal implementation shit from lightning
|
||||
Assert.True(LightMoney.Satoshis(1000) <= invoice.AmountReceived);
|
||||
|
||||
@ -1671,7 +1836,6 @@ namespace BTCPayServer.Tests
|
||||
Assert.Single(info.NodeURIs);
|
||||
Assert.NotEqual(0, info.BlockHeight);
|
||||
|
||||
|
||||
// As admin, can use the internal node through our store.
|
||||
await user.MakeAdmin(true);
|
||||
await user.RegisterInternalLightningNodeAsync("BTC");
|
||||
@ -1681,7 +1845,7 @@ namespace BTCPayServer.Tests
|
||||
await AssertPermissionError("btcpay.server.canuseinternallightningnode", () => client.GetLightningNodeInfo(user.StoreId, "BTC"));
|
||||
// However, even as a guest, you should be able to create an invoice
|
||||
var guest = tester.NewAccount();
|
||||
guest.GrantAccess(false);
|
||||
await guest.GrantAccessAsync();
|
||||
await user.AddGuest(guest.UserId);
|
||||
client = await guest.CreateClient(Policies.CanCreateLightningInvoiceInStore);
|
||||
await client.CreateLightningInvoice(user.StoreId, "BTC", new CreateLightningInvoiceRequest()
|
||||
@ -2208,6 +2372,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
Assert.Equal(transaction.TransactionHash, txdata.TransactionHash);
|
||||
Assert.Equal(String.Empty, transaction.Comment);
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
Assert.Equal(new Dictionary<string, LabelData>(), transaction.Labels);
|
||||
|
||||
// transaction patch tests
|
||||
@ -2228,7 +2393,7 @@ namespace BTCPayServer.Tests
|
||||
}.ToJson(),
|
||||
patchedTransaction.Labels.ToJson()
|
||||
);
|
||||
|
||||
#pragma warning restore CS0612 // Type or member is obsolete
|
||||
await AssertHttpError(403, async () =>
|
||||
{
|
||||
await viewOnlyClient.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode);
|
||||
@ -2638,8 +2803,67 @@ namespace BTCPayServer.Tests
|
||||
Assert.NotNull(custodians);
|
||||
Assert.NotEmpty(custodians);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task StoreRateConfigTests()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri);
|
||||
await AssertHttpError(401, async () => await unauthClient.GetRateSources());
|
||||
|
||||
var user = tester.NewAccount();
|
||||
await user.GrantAccessAsync();
|
||||
var clientBasic = await user.CreateClient();
|
||||
Assert.NotEmpty(await clientBasic.GetRateSources());
|
||||
var config = await clientBasic.GetStoreRateConfiguration(user.StoreId);
|
||||
Assert.NotNull(config);
|
||||
Assert.False(config.IsCustomScript);
|
||||
Assert.Equal("X_X = coingecko(X_X);", config.EffectiveScript);
|
||||
Assert.Equal("coingecko", config.PreferredSource);
|
||||
|
||||
Assert.Equal(0.9m,
|
||||
Assert.Single(await clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId,
|
||||
new StoreRateConfiguration() {IsCustomScript = true, EffectiveScript = "BTC_XYZ = 1;", Spread = 10m,},
|
||||
new[] {"BTC_XYZ"})).Rate);
|
||||
|
||||
Assert.True((await clientBasic.UpdateStoreRateConfiguration(user.StoreId,
|
||||
new StoreRateConfiguration() { IsCustomScript = true, EffectiveScript = "BTC_XYZ = 1", Spread = 10m,}))
|
||||
.IsCustomScript);
|
||||
|
||||
config = await clientBasic.GetStoreRateConfiguration(user.StoreId);
|
||||
Assert.NotNull(config);
|
||||
Assert.NotNull(config.EffectiveScript);
|
||||
Assert.Equal("BTC_XYZ = 1;", config.EffectiveScript);
|
||||
Assert.Equal(10m, config.Spread);
|
||||
Assert.Null(config.PreferredSource);
|
||||
|
||||
Assert.NotNull((await clientBasic.GetStoreRateConfiguration(user.StoreId)).EffectiveScript);
|
||||
Assert.NotNull((await clientBasic.UpdateStoreRateConfiguration(user.StoreId,
|
||||
new StoreRateConfiguration() { IsCustomScript = false, PreferredSource = "coingecko"}))
|
||||
.PreferredSource);
|
||||
|
||||
config = await clientBasic.GetStoreRateConfiguration(user.StoreId);
|
||||
Assert.Equal("X_X = coingecko(X_X);", config.EffectiveScript);
|
||||
|
||||
await AssertValidationError(new[] { "EffectiveScript", "PreferredSource" }, () =>
|
||||
clientBasic.UpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguration() { IsCustomScript = false, EffectiveScript = "BTC_XYZ = 1;" }));
|
||||
|
||||
await AssertValidationError(new[] { "EffectiveScript" }, () =>
|
||||
clientBasic.UpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguration() { IsCustomScript = true, EffectiveScript = "BTC_XYZ rg8w*# 1;" }));
|
||||
await AssertValidationError(new[] { "PreferredSource" }, () =>
|
||||
clientBasic.UpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguration() { IsCustomScript = true, EffectiveScript = "", PreferredSource = "coingecko" }));
|
||||
|
||||
await AssertValidationError(new[] { "PreferredSource", "Spread" }, () =>
|
||||
clientBasic.UpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguration() { IsCustomScript = false, PreferredSource = "coingeckoOOO", Spread = -1m }));
|
||||
|
||||
await AssertValidationError(new[] { "currencyPair" }, () =>
|
||||
clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguration() { IsCustomScript = false, PreferredSource = "coingecko" }, new[] { "BTC_USD_USD_BTC" }));
|
||||
await AssertValidationError(new[] { "PreferredSource", "currencyPair" }, () =>
|
||||
clientBasic.PreviewUpdateStoreRateConfiguration(user.StoreId, new StoreRateConfiguration() { IsCustomScript = false, PreferredSource = "coingeckoOOO" }, new[] { "BTC_USD_USD_BTC" }));
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CustodianAccountControllerTests()
|
||||
|
@ -2,6 +2,8 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@ -28,6 +30,7 @@ namespace BTCPayServer.Tests
|
||||
await user.GrantAccessAsync();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
var apps = user.GetController<UIAppsController>();
|
||||
var pos = user.GetController<UIPointOfSaleController>();
|
||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
|
||||
var appType = AppType.PointOfSale.ToString();
|
||||
vm.AppName = "test";
|
||||
@ -35,8 +38,10 @@ namespace BTCPayServer.Tests
|
||||
Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
|
||||
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||
var app = appList.Apps[0];
|
||||
apps.HttpContext.SetAppData(new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType });
|
||||
var vmpos = await apps.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
|
||||
apps.HttpContext.SetAppData(appData);
|
||||
pos.HttpContext.SetAppData(appData);
|
||||
var vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
vmpos.Template = @"
|
||||
apple:
|
||||
price: 5.0
|
||||
@ -48,9 +53,9 @@ donation:
|
||||
price: 1.02
|
||||
custom: true
|
||||
";
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
vmpos = await apps.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
var publicApps = user.GetController<UIAppsPublicController>();
|
||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
var publicApps = user.GetController<UIPointOfSaleController>();
|
||||
var vmview = await publicApps.ViewPointOfSale(app.Id, PosViewType.Cart).AssertViewModelAsync<ViewPointOfSaleViewModel>();
|
||||
|
||||
// apple shouldn't be available since we it's set to "disabled: true" above
|
||||
|
@ -447,9 +447,12 @@ namespace BTCPayServer.Tests
|
||||
s.AddDerivationScheme();
|
||||
s.GoToInvoices();
|
||||
var i = s.CreateInvoice();
|
||||
s.GoToInvoiceCheckout(i);
|
||||
s.PayInvoice(true);
|
||||
TestUtils.Eventually(() => s.Driver.FindElement(By.LinkText("View receipt")).Click());
|
||||
await s.Server.PayTester.InvoiceRepository.MarkInvoiceStatus(i, InvoiceStatus.Settled);
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
s.Driver.Navigate().Refresh();
|
||||
s.Driver.FindElement(By.Id($"Receipt")).Click();
|
||||
});
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
s.Driver.Navigate().Refresh();
|
||||
@ -472,7 +475,11 @@ namespace BTCPayServer.Tests
|
||||
s.GoToInvoiceCheckout(i);
|
||||
var checkouturi = s.Driver.Url;
|
||||
s.PayInvoice();
|
||||
TestUtils.Eventually(() => s.Driver.FindElement(By.LinkText("View receipt")).Click());
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
s.Driver.Navigate().Refresh();
|
||||
s.Driver.FindElement(By.Id("receipt-btn")).Click();
|
||||
});
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
s.Driver.Navigate().Refresh();
|
||||
@ -480,9 +487,10 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("invoice-processing", s.Driver.PageSource);
|
||||
});
|
||||
s.GoToUrl(checkouturi);
|
||||
s.MineBlockOnInvoiceCheckout();
|
||||
|
||||
await s.Server.PayTester.InvoiceRepository.MarkInvoiceStatus(i, InvoiceStatus.Settled);
|
||||
|
||||
TestUtils.Eventually(() => s.Driver.FindElement(By.LinkText("View receipt")).Click());
|
||||
TestUtils.Eventually(() => s.Driver.FindElement(By.Id("receipt-btn")).Click());
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
s.Driver.Navigate().Refresh();
|
||||
@ -636,15 +644,26 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("enable-pay-button")).Click();
|
||||
s.Driver.FindElement(By.Id("disable-pay-button")).Click();
|
||||
s.FindAlertMessage();
|
||||
s.GoToStore(StoreNavPages.General);
|
||||
s.GoToStore();
|
||||
Assert.False(s.Driver.FindElement(By.Id("AnyoneCanCreateInvoice")).Selected);
|
||||
s.Driver.SetCheckbox(By.Id("AnyoneCanCreateInvoice"), true);
|
||||
s.Driver.FindElement(By.Id("Save")).Click();
|
||||
s.FindAlertMessage();
|
||||
Assert.True(s.Driver.FindElement(By.Id("AnyoneCanCreateInvoice")).Selected);
|
||||
|
||||
// Store settings: Set and unset brand color
|
||||
s.GoToStore();
|
||||
s.Driver.FindElement(By.Id("BrandColor")).SendKeys("#f7931a");
|
||||
s.Driver.FindElement(By.Id("Save")).Click();
|
||||
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
|
||||
Assert.Equal("#f7931a", s.Driver.FindElement(By.Id("BrandColor")).GetAttribute("value"));
|
||||
s.Driver.FindElement(By.Id("BrandColor")).Clear();
|
||||
s.Driver.FindElement(By.Id("Save")).Click();
|
||||
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
|
||||
Assert.Equal(string.Empty, s.Driver.FindElement(By.Id("BrandColor")).GetAttribute("value"));
|
||||
|
||||
// Alice should be able to delete the store
|
||||
s.GoToStore(StoreNavPages.General);
|
||||
s.GoToStore();
|
||||
s.Driver.FindElement(By.Id("DeleteStore")).Click();
|
||||
s.Driver.WaitForElement(By.Id("ConfirmInput")).SendKeys("DELETE");
|
||||
s.Driver.FindElement(By.Id("ConfirmContinue")).Click();
|
||||
@ -800,6 +819,16 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("TargetCurrency")).Clear();
|
||||
s.Driver.FindElement(By.Id("TargetCurrency")).SendKeys("JPY");
|
||||
s.Driver.FindElement(By.Id("TargetAmount")).SendKeys("700");
|
||||
|
||||
// test wrong dates
|
||||
s.Driver.ExecuteJavaScript("const now = new Date();document.getElementById('StartDate').value = now.toISOString();" +
|
||||
"const yst = new Date(now.setDate(now.getDate() -1));document.getElementById('EndDate').value = yst.toISOString()");
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).Click();
|
||||
Assert.Contains("End date cannot be before start date", s.Driver.PageSource);
|
||||
Assert.DoesNotContain("App updated", s.Driver.PageSource);
|
||||
|
||||
// unset end date
|
||||
s.Driver.ExecuteJavaScript("document.getElementById('EndDate').value = ''");
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).Click();
|
||||
Assert.Contains("App updated", s.FindAlertMessage().Text);
|
||||
|
||||
@ -842,9 +871,8 @@ namespace BTCPayServer.Tests
|
||||
var viewUrl = s.Driver.Url;
|
||||
|
||||
Assert.Equal("Amount due", s.Driver.FindElement(By.CssSelector("[data-test='amount-due-title']")).Text);
|
||||
Assert.Equal("Pay Invoice",
|
||||
s.Driver.FindElement(By.CssSelector("[data-test='pay-button']")).Text.Trim());
|
||||
|
||||
Assert.Equal("Pay Invoice", s.Driver.FindElement(By.CssSelector("[data-test='pay-button']")).Text.Trim());
|
||||
|
||||
// expire
|
||||
s.GoToUrl(editUrl);
|
||||
s.Driver.ExecuteJavaScript("document.getElementById('ExpiryDate').value = '2021-01-21T21:00:00.000Z'");
|
||||
@ -862,8 +890,13 @@ namespace BTCPayServer.Tests
|
||||
|
||||
s.GoToUrl(viewUrl);
|
||||
s.Driver.AssertElementNotFound(By.CssSelector("[data-test='status']"));
|
||||
Assert.Equal("Pay Invoice",
|
||||
s.Driver.FindElement(By.CssSelector("[data-test='pay-button']")).Text.Trim());
|
||||
Assert.Equal("Pay Invoice", s.Driver.FindElement(By.CssSelector("[data-test='pay-button']")).Text.Trim());
|
||||
|
||||
// test invoice creation, click with JS, because the button is inside a sticky header
|
||||
s.Driver.ExecuteJavaScript("document.querySelector('[data-test=\"pay-button\"]').click()");
|
||||
// checkout v1
|
||||
s.Driver.WaitForElement(By.CssSelector("invoice"));
|
||||
Assert.Contains("Awaiting Payment", s.Driver.PageSource);
|
||||
|
||||
// archive (from details page)
|
||||
s.GoToUrl(editUrl);
|
||||
@ -992,10 +1025,10 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
// This one should be checked
|
||||
Assert.Contains($"value=\"InvoiceProcessing\" checked", s.Driver.PageSource);
|
||||
Assert.Contains($"value=\"InvoiceCreated\" checked", s.Driver.PageSource);
|
||||
Assert.Contains("value=\"InvoiceProcessing\" checked", s.Driver.PageSource);
|
||||
Assert.Contains("value=\"InvoiceCreated\" checked", s.Driver.PageSource);
|
||||
// This one never been checked
|
||||
Assert.DoesNotContain($"value=\"InvoiceReceivedPayment\" checked", s.Driver.PageSource);
|
||||
Assert.DoesNotContain("value=\"InvoiceReceivedPayment\" checked", s.Driver.PageSource);
|
||||
|
||||
s.Driver.FindElement(By.Name("update")).Click();
|
||||
s.FindAlertMessage();
|
||||
@ -1026,6 +1059,7 @@ namespace BTCPayServer.Tests
|
||||
s.GoToStore(StoreNavPages.Webhooks);
|
||||
s.Driver.FindElement(By.LinkText("Modify")).Click();
|
||||
var elements = s.Driver.FindElements(By.ClassName("redeliver"));
|
||||
|
||||
// One worked, one failed
|
||||
s.Driver.FindElement(By.ClassName("fa-times"));
|
||||
s.Driver.FindElement(By.ClassName("fa-check"));
|
||||
|
@ -75,11 +75,11 @@ namespace BTCPayServer.Tests
|
||||
public async Task CanQueryDirectProviders()
|
||||
{
|
||||
// TODO: Check once in a while whether or not they are working again
|
||||
string[] brokenShitcoinCasinos = { };
|
||||
string[] brokenShitcoinCasinos = {};
|
||||
var skipped = 0;
|
||||
var factory = FastTests.CreateBTCPayRateFactory();
|
||||
var directlySupported = factory.GetSupportedExchanges().Where(s => s.Source == RateSource.Direct)
|
||||
.Select(s => s.Id).ToHashSet();
|
||||
var all = string.Join("\r\n", factory.GetSupportedExchanges().Select(e => e.Id).ToArray());
|
||||
foreach (var result in factory
|
||||
.Providers
|
||||
.Where(p => p.Value is BackgroundFetcherRateProvider bf &&
|
||||
@ -91,14 +91,26 @@ namespace BTCPayServer.Tests
|
||||
var name = result.ExpectedName;
|
||||
if (brokenShitcoinCasinos.Contains(name))
|
||||
{
|
||||
TestLogs.LogInformation($"Skipping {name}");
|
||||
TestLogs.LogInformation($"Skipping {name}: Broken shitcoin casino");
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
TestLogs.LogInformation($"Testing {name}");
|
||||
|
||||
result.Fetcher.InvalidateCache();
|
||||
var exchangeRates = new ExchangeRates(name, result.ResultAsync.Result);
|
||||
|
||||
ExchangeRates exchangeRates = null;
|
||||
try
|
||||
{
|
||||
exchangeRates = new ExchangeRates(name, result.ResultAsync.Result);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
TestLogs.LogInformation($"Skipping {name}: {exception.Message}");
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
result.Fetcher.InvalidateCache();
|
||||
Assert.NotNull(exchangeRates);
|
||||
Assert.NotEmpty(exchangeRates);
|
||||
@ -160,11 +172,12 @@ namespace BTCPayServer.Tests
|
||||
// Kraken emit one request only after first GetRates
|
||||
factory.Providers["kraken"].GetRatesAsync(default).GetAwaiter().GetResult();
|
||||
|
||||
|
||||
var p = new KrakenExchangeRateProvider();
|
||||
var rates = await p.GetRatesAsync(default);
|
||||
Assert.Contains(rates, e => e.CurrencyPair == new CurrencyPair("XMR", "BTC") && e.BidAsk.Bid < 1.0m);
|
||||
|
||||
// Check we didn't skip too many exchanges
|
||||
Assert.InRange(skipped, 0, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -35,10 +35,12 @@ using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Payments.PayJoin.Sender;
|
||||
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||
using BTCPayServer.Security.Bitpay;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Labels;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Storage.Models;
|
||||
@ -50,6 +52,7 @@ using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitcoin.Payment;
|
||||
@ -651,7 +654,7 @@ namespace BTCPayServer.Tests
|
||||
(string)store2.TempData[WellKnownTempData.ErrorMessage], StringComparison.CurrentCultureIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Fact(Timeout = LongRunningTestTimeout * 2)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanUseTorClient()
|
||||
{
|
||||
@ -784,9 +787,9 @@ namespace BTCPayServer.Tests
|
||||
tx = Assert.Single(transactions.Transactions);
|
||||
|
||||
Assert.Equal("hello", tx.Comment);
|
||||
Assert.Contains("test", tx.Labels.Select(l => l.Text));
|
||||
Assert.Contains("test2", tx.Labels.Select(l => l.Text));
|
||||
Assert.Equal(2, tx.Labels.GroupBy(l => l.Color).Count());
|
||||
Assert.Contains("test", tx.Tags.Select(l => l.Text));
|
||||
Assert.Contains("test2", tx.Tags.Select(l => l.Text));
|
||||
Assert.Equal(2, tx.Tags.GroupBy(l => l.Color).Count());
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(
|
||||
await walletController.ModifyTransaction(walletId, tx.Id, removelabel: "test2"));
|
||||
@ -796,12 +799,9 @@ namespace BTCPayServer.Tests
|
||||
tx = Assert.Single(transactions.Transactions);
|
||||
|
||||
Assert.Equal("hello", tx.Comment);
|
||||
Assert.Contains("test", tx.Labels.Select(l => l.Text));
|
||||
Assert.DoesNotContain("test2", tx.Labels.Select(l => l.Text));
|
||||
Assert.Single(tx.Labels.GroupBy(l => l.Color));
|
||||
|
||||
var walletInfo = await tester.PayTester.GetService<WalletRepository>().GetWalletInfo(walletId);
|
||||
Assert.Single(walletInfo.LabelColors); // the test2 color should have been removed
|
||||
Assert.Contains("test", tx.Tags.Select(l => l.Text));
|
||||
Assert.DoesNotContain("test2", tx.Tags.Select(l => l.Text));
|
||||
Assert.Single(tx.Tags.GroupBy(l => l.Color));
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
@ -1752,7 +1752,7 @@ namespace BTCPayServer.Tests
|
||||
await user.GrantAccessAsync();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
await user.SetNetworkFeeMode(NetworkFeeMode.Always);
|
||||
var invoice = user.BitPay.CreateInvoice(
|
||||
var invoice = await user.BitPay.CreateInvoiceAsync(
|
||||
new Invoice
|
||||
{
|
||||
Price = 10,
|
||||
@ -1764,11 +1764,10 @@ namespace BTCPayServer.Tests
|
||||
}, Facade.Merchant);
|
||||
|
||||
var networkFee = new FeeRate(invoice.MinerFees["BTC"].SatoshiPerBytes).GetFee(100);
|
||||
// ensure 0 invoices exported because there are no payments yet
|
||||
var jsonResult = user.GetController<UIInvoiceController>().Export("json").GetAwaiter().GetResult();
|
||||
var result = Assert.IsType<ContentResult>(jsonResult);
|
||||
Assert.Equal("application/json", result.ContentType);
|
||||
Assert.Equal("[]", result.Content);
|
||||
Assert.Single(JArray.Parse(result.Content));
|
||||
|
||||
var cashCow = tester.ExplorerNode;
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
|
||||
@ -1953,6 +1952,7 @@ namespace BTCPayServer.Tests
|
||||
var stores = user.GetController<UIStoresController>();
|
||||
var apps = user.GetController<UIAppsController>();
|
||||
var apps2 = user2.GetController<UIAppsController>();
|
||||
var pos = user.GetController<UIPointOfSaleController>();
|
||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp(user.StoreId)).Model);
|
||||
var appType = AppType.PointOfSale.ToString();
|
||||
Assert.NotNull(vm.SelectedAppType);
|
||||
@ -1960,12 +1960,14 @@ namespace BTCPayServer.Tests
|
||||
vm.AppName = "test";
|
||||
vm.SelectedAppType = appType;
|
||||
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.CreateApp(user.StoreId, vm).Result);
|
||||
Assert.Equal(nameof(apps.UpdatePointOfSale), redirectToAction.ActionName);
|
||||
Assert.Equal(nameof(pos.UpdatePointOfSale), redirectToAction.ActionName);
|
||||
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||
var appList2 =
|
||||
Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps2.ListApps(user2.StoreId).Result).Model);
|
||||
var app = appList.Apps[0];
|
||||
apps.HttpContext.SetAppData(new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType });
|
||||
var appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType };
|
||||
apps.HttpContext.SetAppData(appData);
|
||||
pos.HttpContext.SetAppData(appData);
|
||||
Assert.Single(appList.Apps);
|
||||
Assert.Empty(appList2.Apps);
|
||||
Assert.Equal("test", appList.Apps[0].AppName);
|
||||
@ -2518,6 +2520,79 @@ namespace BTCPayServer.Tests
|
||||
Assert.True(lnMethod.IsInternalNode);
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
[Obsolete]
|
||||
public async Task CanDoLabelMigrations()
|
||||
{
|
||||
using var tester = CreateServerTester(newDb: true);
|
||||
await tester.StartAsync();
|
||||
var dbf = tester.PayTester.GetService<ApplicationDbContextFactory>();
|
||||
int walletCount = 1000;
|
||||
var wallet = "walletttttttttttttttttttttttttttt";
|
||||
using (var db = dbf.CreateContext())
|
||||
{
|
||||
for (int i = 0; i < walletCount; i++)
|
||||
{
|
||||
var walletData = new WalletData() { Id = $"S-{wallet}{i}-BTC" };
|
||||
walletData.Blob = ZipUtils.Zip("{\"LabelColors\": { \"label1\" : \"black\", \"payout\":\"green\" }}");
|
||||
db.Wallets.Add(walletData);
|
||||
}
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
uint256 firstTxId = null;
|
||||
using (var db = dbf.CreateContext())
|
||||
{
|
||||
int transactionCount = 10_000;
|
||||
for (int i = 0; i < transactionCount; i++)
|
||||
{
|
||||
var txId = RandomUtils.GetUInt256();
|
||||
var wt = new WalletTransactionData()
|
||||
{
|
||||
WalletDataId = $"S-{wallet}{i % walletCount}-BTC",
|
||||
TransactionId = txId.ToString(),
|
||||
};
|
||||
firstTxId ??= txId;
|
||||
if (i != 10)
|
||||
wt.Blob = ZipUtils.Zip("{\"Comment\":\"test\"}");
|
||||
if (i % 1240 != 0)
|
||||
{
|
||||
wt.Labels = "[{\"type\":\"raw\", \"text\":\"label1\"}]";
|
||||
}
|
||||
else if (i == 0)
|
||||
{
|
||||
wt.Labels = "[{\"type\":\"raw\", \"text\":\"label1\"},{\"type\":\"raw\", \"text\":\"labelo" + i + "\"}, " +
|
||||
"{\"type\":\"payout\", \"text\":\"payout\", \"pullPaymentPayouts\":{\"pp1\":[\"p1\",\"p2\"],\"pp2\":[\"p3\"]}}]";
|
||||
}
|
||||
else
|
||||
{
|
||||
wt.Labels = "[{\"type\":\"raw\", \"text\":\"label1\"},{\"type\":\"raw\", \"text\":\"labelo" + i + "\"}]";
|
||||
}
|
||||
db.WalletTransactions.Add(wt);
|
||||
}
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
await RestartMigration(tester);
|
||||
var migrator = tester.PayTester.GetService<IEnumerable<IHostedService>>().OfType<DbMigrationsHostedService>().First();
|
||||
await migrator.MigratedTransactionLabels(0);
|
||||
|
||||
var walletRepo = tester.PayTester.GetService<WalletRepository>();
|
||||
var wi1 = await walletRepo.GetWalletLabels(new WalletId($"{wallet}0", "BTC"));
|
||||
Assert.Equal(3, wi1.Length);
|
||||
Assert.Contains(wi1, o => o.Label == "label1" && o.Color == "black");
|
||||
Assert.Contains(wi1, o => o.Label == "labelo0" && o.Color == "#000");
|
||||
Assert.Contains(wi1, o => o.Label == "payout" && o.Color == "green");
|
||||
|
||||
var txInfo = await walletRepo.GetWalletTransactionsInfo(new WalletId($"{wallet}0", "BTC"), new[] { firstTxId.ToString() });
|
||||
Assert.Equal("test", txInfo.Values.First().Comment);
|
||||
// Should have the 2 raw labels, and one legacy label for payouts
|
||||
Assert.Equal(3, txInfo.Values.First().LegacyLabels.Count);
|
||||
var payoutLabel = txInfo.Values.First().LegacyLabels.Select(l => l.Value).OfType<PayoutLabel>().First();
|
||||
Assert.Equal(2, payoutLabel.PullPaymentPayouts.Count);
|
||||
Assert.Equal(2, payoutLabel.PullPaymentPayouts["pp1"].Count);
|
||||
Assert.Single(payoutLabel.PullPaymentPayouts["pp2"]);
|
||||
}
|
||||
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
|
67
BTCPayServer.Tests/docker-bitcoin-multisig-setup.sh
Executable file
67
BTCPayServer.Tests/docker-bitcoin-multisig-setup.sh
Executable file
@ -0,0 +1,67 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Creates a 2-of-3 multisig setup, following the procedure described here:
|
||||
# https://github.com/bitcoin/bitcoin/blob/master/doc/multisig-tutorial.md
|
||||
# https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md
|
||||
#
|
||||
# Usage:
|
||||
# ./docker-bitcoin-multisig-setup.sh custom-name
|
||||
#
|
||||
# The custom name/prefix is optional and defaults to "multisig".
|
||||
prefix="${1:-"multi_sig"}"
|
||||
|
||||
declare -A xpubs
|
||||
|
||||
printf "\n👛 Create descriptor wallets\n\n"
|
||||
for ((n=1;n<=3;n++)); do
|
||||
# Create descriptor wallets, surpress error output in case wallet already exists
|
||||
./docker-bitcoin-cli.sh -named createwallet wallet_name="${prefix}_part_${n}" descriptors=true > /dev/null 2>&1
|
||||
|
||||
# Collect xpubs
|
||||
./docker-bitcoin-cli.sh -rpcwallet="${prefix}_part_${n}" listdescriptors > /dev/null 2>&1
|
||||
xpubs["internal_xpub_${n}"]=$(./docker-bitcoin-cli.sh -rpcwallet="${prefix}_part_${n}" listdescriptors | jq '.descriptors | [.[] | select(.desc | startswith("wpkh") and contains("/1/*"))][0] | .desc' | grep -Po '(?<=\().*(?=\))')
|
||||
xpubs["external_xpub_${n}"]=$(./docker-bitcoin-cli.sh -rpcwallet="${prefix}_part_${n}" listdescriptors | jq '.descriptors | [.[] | select(.desc | startswith("wpkh") and contains("/0/*"))][0] | .desc' | grep -Po '(?<=\().*(?=\))')
|
||||
done
|
||||
|
||||
for x in "${!xpubs[@]}"; do
|
||||
printf "[%s]=%s\n" "$x" "${xpubs[$x]}";
|
||||
done
|
||||
|
||||
external_desc="wsh(sortedmulti(2,${xpubs["external_xpub_1"]},${xpubs["external_xpub_2"]},${xpubs["external_xpub_3"]}))"
|
||||
internal_desc="wsh(sortedmulti(2,${xpubs["internal_xpub_1"]},${xpubs["internal_xpub_2"]},${xpubs["internal_xpub_3"]}))"
|
||||
|
||||
external_desc_sum=$(./docker-bitcoin-cli.sh getdescriptorinfo $external_desc | jq '.descriptor')
|
||||
internal_desc_sum=$(./docker-bitcoin-cli.sh getdescriptorinfo $internal_desc | jq '.descriptor')
|
||||
|
||||
multisig_ext_desc="{\"desc\": $external_desc_sum, \"active\": true, \"internal\": false, \"timestamp\": \"now\"}"
|
||||
multisig_int_desc="{\"desc\": $internal_desc_sum, \"active\": true, \"internal\": true, \"timestamp\": \"now\"}"
|
||||
|
||||
multisig_desc="[$multisig_ext_desc, $multisig_int_desc]"
|
||||
|
||||
# Create multisig wallet, surpress error output in case wallet already exists
|
||||
|
||||
printf "\n🔐 Create multisig wallet\n"
|
||||
printf "\nExternal descriptor: $external_desc\n"
|
||||
printf "\nInternal descriptor: $internal_desc\n"
|
||||
|
||||
multisig_name="${prefix}_wallet"
|
||||
./docker-bitcoin-cli.sh -named createwallet wallet_name="$multisig_name" disable_private_keys=true blank=true descriptors=true > /dev/null 2>&1
|
||||
./docker-bitcoin-cli.sh -rpcwallet="$multisig_name" importdescriptors "$multisig_desc" > /dev/null 2>&1
|
||||
|
||||
# Fund the wallet from the default wallet
|
||||
printf "\n💰 Fund multisig wallet\n"
|
||||
|
||||
newaddress=$(./docker-bitcoin-cli.sh -rpcwallet="$multisig_name" getnewaddress "MultiSig Funding" | tr -d "[:cntrl:]")
|
||||
txid=$(./docker-bitcoin-cli.sh -rpcwallet="" sendtoaddress "$newaddress" 0.615)
|
||||
printf "\nReceiving address: $newaddress\n"
|
||||
printf "\nTransaction ID: $txid\n"
|
||||
|
||||
# Confirm everything worked
|
||||
printf "\nℹ️ Multisig wallet info\n\n"
|
||||
./docker-bitcoin-cli.sh -rpcwallet="$multisig_name" getwalletinfo
|
||||
|
||||
# Unload wallets to prevent having to specify which wallet to use in BTCPay, NBXplorer etc.
|
||||
for ((n=1;n<=3;n++)); do
|
||||
./docker-bitcoin-cli.sh unloadwallet "${prefix}_part_${n}" > /dev/null 2>&1
|
||||
done
|
||||
./docker-bitcoin-cli.sh unloadwallet "$multisig_name" > /dev/null 2>&1
|
@ -71,7 +71,7 @@ services:
|
||||
- "sshd_datadir:/root/.ssh"
|
||||
|
||||
devlnd:
|
||||
image: btcpayserver/bitcoin:22.0
|
||||
image: btcpayserver/bitcoin:23.0-1
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_WALLETDIR: "/data/wallets"
|
||||
@ -90,7 +90,7 @@ services:
|
||||
expose:
|
||||
- "4444"
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.3.14
|
||||
image: nicolasdorier/nbxplorer:2.3.40
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
@ -126,7 +126,7 @@ services:
|
||||
|
||||
bitcoind:
|
||||
restart: unless-stopped
|
||||
image: btcpayserver/bitcoin:22.0
|
||||
image: btcpayserver/bitcoin:23.0-1
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_WALLETDIR: "/data/wallets"
|
||||
@ -237,7 +237,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.15.0-beta
|
||||
image: btcpayserver/lnd:v0.15.4-beta-1
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -272,7 +272,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.15.0-beta
|
||||
image: btcpayserver/lnd:v0.15.4-beta-1
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
|
@ -68,7 +68,7 @@ services:
|
||||
- "sshd_datadir:/root/.ssh"
|
||||
|
||||
devlnd:
|
||||
image: btcpayserver/bitcoin:22.0
|
||||
image: btcpayserver/bitcoin:23.0-1
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_WALLETDIR: "/data/wallets"
|
||||
@ -87,7 +87,7 @@ services:
|
||||
expose:
|
||||
- "4444"
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.3.14
|
||||
image: nicolasdorier/nbxplorer:2.3.40
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
@ -113,7 +113,7 @@ services:
|
||||
|
||||
bitcoind:
|
||||
restart: unless-stopped
|
||||
image: btcpayserver/bitcoin:22.0
|
||||
image: btcpayserver/bitcoin:23.0-1
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_WALLETDIR: "/data/wallets"
|
||||
@ -225,7 +225,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.15.0-beta
|
||||
image: btcpayserver/lnd:v0.15.4-beta-1
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -262,7 +262,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.15.0-beta
|
||||
image: btcpayserver/lnd:v0.15.4-beta-1
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
|
@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
|
||||
<Import Project="../Build/Common.csproj" />
|
||||
|
||||
@ -9,16 +9,12 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="Build\**" />
|
||||
<Compile Remove="wwwroot\bundles\jqueryvalidate\**" />
|
||||
<Compile Remove="wwwroot\vendor\jquery-nice-select\**" />
|
||||
<Content Remove="Build\**" />
|
||||
<Content Remove="wwwroot\bundles\jqueryvalidate\**" />
|
||||
<Content Remove="wwwroot\vendor\jquery-nice-select\**" />
|
||||
<EmbeddedResource Remove="Build\**" />
|
||||
<EmbeddedResource Remove="wwwroot\bundles\jqueryvalidate\**" />
|
||||
<EmbeddedResource Remove="wwwroot\vendor\jquery-nice-select\**" />
|
||||
<None Remove="Build\**" />
|
||||
<None Remove="wwwroot\bundles\jqueryvalidate\**" />
|
||||
<None Remove="wwwroot\vendor\jquery-nice-select\**" />
|
||||
<Content Update="Views\UIStorePullPayments\NewPullPayment.cshtml">
|
||||
<Pack>false</Pack>
|
||||
@ -33,9 +29,6 @@
|
||||
<ItemGroup>
|
||||
<None Remove="Currencies.txt" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="bundleconfig.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(Altcoins)' != 'true'">
|
||||
<Content Remove="Services\Altcoins\**\*" />
|
||||
@ -48,10 +41,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
|
||||
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.3.13" />
|
||||
<PackageReference Include="BuildBundlerMinifier" Version="3.2.449" />
|
||||
<PackageReference Include="BundlerMinifier.Core" Version="3.2.435" />
|
||||
<PackageReference Include="BundlerMinifier.TagHelpers" Version="3.2.435" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.4.8" />
|
||||
<PackageReference Include="CsvHelper" Version="15.0.5" />
|
||||
<PackageReference Include="Dapper" Version="2.0.123" />
|
||||
<PackageReference Include="Fido2" Version="2.0.2" />
|
||||
@ -72,7 +62,6 @@
|
||||
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
|
||||
<PackageReference Include="NicolasDorier.RateLimits" Version="1.2.3" />
|
||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.18" />
|
||||
<PackageReference Include="Serilog" Version="2.9.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
|
||||
@ -90,8 +79,8 @@
|
||||
<PackageReference Include="TwentyTwenty.Storage.Google" Version="2.12.1" />
|
||||
<PackageReference Include="TwentyTwenty.Storage.Local" Version="2.12.1" />
|
||||
<PackageReference Include="YamlDotNet" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.9" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.9" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
63
BTCPayServer/ColorPalette.cs
Normal file
63
BTCPayServer/ColorPalette.cs
Normal file
@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using NBitcoin.Crypto;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class ColorPalette
|
||||
{
|
||||
public const string Pattern = "^#[0-9a-fA-F]{6}$";
|
||||
public static bool IsValid(string color)
|
||||
{
|
||||
return Regex.Match(color, Pattern).Success;
|
||||
}
|
||||
public string TextColor(string bgColor)
|
||||
{
|
||||
int nThreshold = 105;
|
||||
var bg = ColorTranslator.FromHtml(bgColor);
|
||||
int bgDelta = Convert.ToInt32((bg.R * 0.299) + (bg.G * 0.587) + (bg.B * 0.114));
|
||||
Color color = (255 - bgDelta < nThreshold) ? Color.Black : Color.White;
|
||||
return ColorTranslator.ToHtml(color);
|
||||
}
|
||||
// Borrowed from https://github.com/ManageIQ/guides/blob/master/labels.md
|
||||
public static readonly ColorPalette Default = new ColorPalette(new string[] {
|
||||
"#fbca04",
|
||||
"#0e8a16",
|
||||
"#ff7619",
|
||||
"#84b6eb",
|
||||
"#5319e7",
|
||||
"#cdcdcd",
|
||||
"#cc317c",
|
||||
});
|
||||
private ColorPalette(string[] labels)
|
||||
{
|
||||
Labels = labels;
|
||||
}
|
||||
|
||||
public readonly string[] Labels;
|
||||
|
||||
public string DeterministicColor(string label)
|
||||
{
|
||||
switch (label)
|
||||
{
|
||||
case "payjoin":
|
||||
return "#51b13e";
|
||||
case "invoice":
|
||||
return "#cedc21";
|
||||
case "payment-request":
|
||||
return "#489D77";
|
||||
case "app":
|
||||
return "#5093B6";
|
||||
case "pj-exposed":
|
||||
return "#51b13e";
|
||||
case "payout":
|
||||
return "#3F88AF";
|
||||
default:
|
||||
var num = NBitcoin.Utils.ToUInt32(Hashes.SHA256(Encoding.UTF8.GetBytes(label)), 0, true);
|
||||
return Labels[num % Labels.Length];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
@model BTCPayServer.Components.AppSales.AppSalesViewModel
|
||||
|
||||
@{
|
||||
var controller = $"UI{Model.App.AppType}";
|
||||
var action = $"Update{Model.App.AppType}";
|
||||
var label = Model.App.AppType == nameof(AppType.Crowdfund) ? "Contributions" : "Sales";
|
||||
}
|
||||
@ -10,7 +11,7 @@
|
||||
<div id="AppSales-@Model.App.Id" class="widget app-sales">
|
||||
<header class="mb-3">
|
||||
<h3>@Model.App.Name @label</h3>
|
||||
<a asp-controller="UIApps" asp-action="@action" asp-route-appId="@Model.App.Id">Manage</a>
|
||||
<a asp-controller="@controller" asp-action="@action" asp-route-appId="@Model.App.Id">Manage</a>
|
||||
</header>
|
||||
@if (Model.InitialRendering)
|
||||
{
|
||||
|
@ -2,6 +2,7 @@
|
||||
@model BTCPayServer.Components.AppTopItems.AppTopItemsViewModel
|
||||
|
||||
@{
|
||||
var controller = $"UI{Model.App.AppType}";
|
||||
var action = $"Update{Model.App.AppType}";
|
||||
var label = Model.App.AppType == nameof(AppType.Crowdfund) ? "contribution" : "sale";
|
||||
}
|
||||
@ -9,7 +10,7 @@
|
||||
<div id="AppTopItems-@Model.App.Id" class="widget app-top-items">
|
||||
<header class="mb-3">
|
||||
<h3>Top @(Model.App.AppType == nameof(AppType.Crowdfund) ? "Perks" : "Items")</h3>
|
||||
<a asp-controller="UIApps" asp-action="@action" asp-route-appId="@Model.App.Id">View All</a>
|
||||
<a asp-controller="@controller" asp-action="@action" asp-route-appId="@Model.App.Id">View All</a>
|
||||
</header>
|
||||
@if (Model.InitialRendering)
|
||||
{
|
||||
|
@ -7,9 +7,6 @@
|
||||
@using BTCPayServer.Views.Wallets
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Components.Icon
|
||||
@using BTCPayServer.Components.ThemeSwitch
|
||||
@using BTCPayServer.Components.UIExtensionPoint
|
||||
@using BTCPayServer.Services
|
||||
@using BTCPayServer.Views.CustodianAccounts
|
||||
@inject BTCPayServerEnvironment Env
|
||||
@ -35,7 +32,7 @@
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a asp-area="" asp-controller="UIStores" asp-action="GeneralSettings" asp-route-storeId="@Model.Store.Id" class="nav-link js-scroll-trigger @ViewData.IsActivePage(StoreNavPages.Rates) @ViewData.IsActivePage(StoreNavPages.CheckoutAppearance) @ViewData.IsActivePage(StoreNavPages.General) @ViewData.IsActivePage(StoreNavPages.Tokens) @ViewData.IsActivePage(StoreNavPages.Users) @ViewData.IsActivePage(StoreNavPages.Plugins) @ViewData.IsActivePage(StoreNavPages.Webhooks)" id="StoreNav-StoreSettings">
|
||||
<a asp-area="" asp-controller="UIStores" asp-action="GeneralSettings" asp-route-storeId="@Model.Store.Id" class="nav-link js-scroll-trigger @ViewData.IsActivePage(new [] {StoreNavPages.Rates, StoreNavPages.CheckoutAppearance, StoreNavPages.General, StoreNavPages.Tokens, StoreNavPages.Users, StoreNavPages.Plugins, StoreNavPages.Webhooks, StoreNavPages.PayoutProcessors, StoreNavPages.Emails})" id="StoreNav-StoreSettings">
|
||||
<vc:icon symbol="settings"/>
|
||||
<span>Settings</span>
|
||||
</a>
|
||||
@ -100,9 +97,7 @@
|
||||
{
|
||||
<li class="nav-item">
|
||||
<a asp-area="" asp-controller="UICustodianAccounts" asp-action="ViewCustodianAccount" asp-route-storeId="@custodianAccount.StoreId" asp-route-accountId="@custodianAccount.Id" class="nav-link js-scroll-trigger @ViewData.IsActivePage(CustodianAccountsNavPages.View, custodianAccount.Id)" id="@($"StoreNav-CustodianAccount-{custodianAccount.Id}")">
|
||||
<!--
|
||||
TODO which icon should we use?
|
||||
-->
|
||||
@* TODO which icon should we use? *@
|
||||
<span>@custodianAccount.Name</span>
|
||||
<span class="badge bg-warning ms-1" style="font-size:10px;">Experimental</span>
|
||||
</a>
|
||||
@ -171,12 +166,7 @@
|
||||
<ul class="navbar-nav">
|
||||
@foreach (var app in Model.Apps)
|
||||
{
|
||||
<li class="nav-item">
|
||||
<a asp-area="" asp-controller="UIApps" asp-action="@app.Action" asp-route-appId="@app.Id" class="nav-link js-scroll-trigger @ViewData.IsActivePage(AppsNavPages.Update, app.Id)" id="@($"StoreNav-App-{app.Id}")">
|
||||
<vc:icon symbol="@app.AppType.ToLower()"/>
|
||||
<span>@app.AppName</span>
|
||||
</a>
|
||||
</li>
|
||||
<vc:ui-extension-point location="apps-nav" model="@app"/>
|
||||
}
|
||||
<li class="nav-item">
|
||||
<a asp-area="" asp-controller="UIApps" asp-action="CreateApp" asp-route-storeId="@Model.Store.Id" class="nav-link js-scroll-trigger @ViewData.IsActivePage(AppsNavPages.Create)" id="StoreNav-CreateApp">
|
||||
|
@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
@ -71,12 +72,11 @@ namespace BTCPayServer.Components.MainNav
|
||||
vm.Apps = apps.Select(a => new StoreApp
|
||||
{
|
||||
Id = a.Id,
|
||||
IsOwner = a.IsOwner,
|
||||
AppName = a.AppName,
|
||||
AppType = a.AppType,
|
||||
IsOwner = a.IsOwner
|
||||
AppType = Enum.Parse<AppType>(a.AppType)
|
||||
}).ToList();
|
||||
|
||||
|
||||
|
||||
if (PoliciesSettings.Experimental)
|
||||
{
|
||||
// Custodian Accounts
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Services.Apps;
|
||||
|
||||
namespace BTCPayServer.Components.MainNav
|
||||
{
|
||||
@ -18,8 +19,7 @@ namespace BTCPayServer.Components.MainNav
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string AppName { get; set; }
|
||||
public string AppType { get; set; }
|
||||
public string Action { get => $"Update{AppType}"; }
|
||||
public AppType AppType { get; set; }
|
||||
public bool IsOwner { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -123,7 +123,10 @@
|
||||
</div>
|
||||
@if (Model.Balance.OffchainBalance != null && Model.Balance.OnchainBalance != null)
|
||||
{
|
||||
<a class="d-inline-block mt-3" role="button" data-bs-toggle="collapse" data-bs-target=".balance-details" aria-expanded="false" aria-controls="balanceDetailsOffchain balanceDetailsOnchain">Show details</a>
|
||||
<button class="d-inline-flex align-items-center btn btn-link text-primary fw-semibold p-0 mt-3 ms-n1" type="button" data-bs-toggle="collapse" data-bs-target=".balance-details" aria-expanded="false" aria-controls="balanceDetailsOffchain balanceDetailsOnchain">
|
||||
<vc:icon symbol="caret-down"/>
|
||||
<span class="ms-1">Details</span>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
else
|
||||
|
@ -1,5 +1,10 @@
|
||||
@inject BTCPayServer.Services.BTCPayServerEnvironment _env
|
||||
@inject SignInManager<ApplicationUser> _signInManager
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@using BTCPayServer.Abstractions.Contracts
|
||||
@inject BTCPayServer.Services.BTCPayServerEnvironment Env
|
||||
@inject SignInManager<ApplicationUser> SignInManager
|
||||
@inject IFileService FileService
|
||||
@model BTCPayServer.Components.StoreSelector.StoreSelectorViewModel
|
||||
@functions {
|
||||
@* ReSharper disable once CSharpWarnings::CS1998 *@
|
||||
@ -8,9 +13,9 @@
|
||||
{
|
||||
var logoSrc = $"{ViewContext.HttpContext.Request.PathBase}/img/logo.svg";
|
||||
<svg xmlns="http://www.w3.org/2000/svg" role="img" alt="BTCPay Server" class="logo"><use href="@logoSrc#small" class="logo-small" /><use href="@logoSrc#large" class="logo-large" /></svg>
|
||||
@if (_env.NetworkType != NBitcoin.ChainName.Mainnet)
|
||||
@if (Env.NetworkType != NBitcoin.ChainName.Mainnet)
|
||||
{
|
||||
<span class="badge bg-warning ms-1 ms-sm-0" style="font-size:10px;">@_env.NetworkType.ToString()</span>
|
||||
<span class="badge bg-warning ms-1 ms-sm-0" style="font-size:10px;">@Env.NetworkType.ToString()</span>
|
||||
}
|
||||
}
|
||||
private string StoreName(string title)
|
||||
@ -37,7 +42,14 @@ else
|
||||
{
|
||||
<div id="StoreSelectorDropdown" class="dropdown only-for-js">
|
||||
<button id="StoreSelectorToggle" class="btn btn-secondary dropdown-toggle rounded-pill px-3 @(Model.CurrentStoreId == null ? "text-secondary" : "")" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<vc:icon symbol="store"/>
|
||||
@if (!string.IsNullOrEmpty(Model.CurrentStoreLogoFileId))
|
||||
{
|
||||
<img class="logo" src="@(await FileService.GetFileUrl(Context.Request.GetAbsoluteRootUri(), Model.CurrentStoreLogoFileId))" alt="@Model.CurrentDisplayName" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<vc:icon symbol="store"/>
|
||||
}
|
||||
<span>@(Model.CurrentStoreId == null ? "Select Store" : Model.CurrentDisplayName)</span>
|
||||
<vc:icon symbol="caret-down"/>
|
||||
</button>
|
||||
@ -60,7 +72,7 @@ else
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
else if (_signInManager.IsSignedIn(User))
|
||||
else if (SignInManager.IsSignedIn(User))
|
||||
{
|
||||
<a asp-controller="UIUserStores" asp-action="CreateStore" class="btn btn-primary w-100 rounded-pill" id="StoreSelectorCreate">Create Store</a>
|
||||
}
|
||||
|
@ -50,12 +50,15 @@ namespace BTCPayServer.Components.StoreSelector
|
||||
.OrderBy(s => s.Text)
|
||||
.ToList();
|
||||
|
||||
var blob = currentStore?.GetStoreBlob();
|
||||
|
||||
var vm = new StoreSelectorViewModel
|
||||
{
|
||||
Options = options,
|
||||
CurrentStoreId = currentStore?.Id,
|
||||
CurrentDisplayName = currentStore?.StoreName,
|
||||
CurrentStoreIsOwner = currentStore?.Role == StoreRoles.Owner
|
||||
CurrentStoreIsOwner = currentStore?.Role == StoreRoles.Owner,
|
||||
CurrentStoreLogoFileId = blob?.LogoFileId
|
||||
};
|
||||
|
||||
return View(vm);
|
||||
|
@ -6,6 +6,7 @@ namespace BTCPayServer.Components.StoreSelector
|
||||
{
|
||||
public List<StoreSelectorOption> Options { get; set; }
|
||||
public string CurrentStoreId { get; set; }
|
||||
public string CurrentStoreLogoFileId { get; set; }
|
||||
public string CurrentDisplayName { get; set; }
|
||||
public bool CurrentStoreIsOwner { get; set; }
|
||||
}
|
||||
|
@ -65,11 +65,7 @@ namespace BTCPayServer.Configuration
|
||||
if (conf.GetOrDefault<bool>("launchsettings", false) && NetworkType != ChainName.Regtest)
|
||||
throw new ConfigException($"You need to run BTCPayServer with the run.sh or run.ps1 script");
|
||||
|
||||
|
||||
|
||||
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
|
||||
DockerDeployment = conf.GetOrDefault<bool>("dockerdeployment", true);
|
||||
|
||||
TorrcFile = conf.GetOrDefault<string>("torrcfile", null);
|
||||
TorServices = conf.GetOrDefault<string>("torservices", null)
|
||||
?.Split(new[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
@ -112,9 +108,9 @@ namespace BTCPayServer.Configuration
|
||||
{
|
||||
Logs.Configuration.LogWarning($"The SSH key is not supported ({ex.Message}), try to generate the key with ssh-keygen using \"-m PEM\". Skipping SSH configuration...");
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new ConfigException($"sshkeyfilepassword is invalid");
|
||||
Logs.Configuration.LogWarning(ex, "Error while loading SSH settings");
|
||||
}
|
||||
}
|
||||
|
||||
@ -144,14 +140,15 @@ namespace BTCPayServer.Configuration
|
||||
}
|
||||
|
||||
DisableRegistration = conf.GetOrDefault<bool>("disable-registration", true);
|
||||
PluginRemote = conf.GetOrDefault("plugin-remote", "btcpayserver/btcpayserver-plugins");
|
||||
var pluginRemote = conf.GetOrDefault<string>("plugin-remote", null);
|
||||
if (pluginRemote != null)
|
||||
Logs.Configuration.LogWarning("plugin-remote is an obsolete configuration setting, please remove it from configuration");
|
||||
RecommendedPlugins = conf.GetOrDefault("recommended-plugins", "").ToLowerInvariant().Split('\r', '\n', '\t', ' ').Where(s => !string.IsNullOrEmpty(s)).Distinct().ToArray();
|
||||
CheatMode = conf.GetOrDefault("cheatmode", false);
|
||||
if (CheatMode && this.NetworkType == ChainName.Mainnet)
|
||||
throw new ConfigException($"cheatmode can't be used on mainnet");
|
||||
}
|
||||
|
||||
public string PluginRemote { get; set; }
|
||||
public string[] RecommendedPlugins { get; set; }
|
||||
public bool CheatMode { get; set; }
|
||||
|
||||
@ -192,16 +189,7 @@ namespace BTCPayServer.Configuration
|
||||
|
||||
public string RootPath { get; set; }
|
||||
public bool DockerDeployment { get; set; }
|
||||
public bool BundleJsCss
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public SSHSettings SSHSettings
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public SSHSettings SSHSettings { get; set; }
|
||||
public string TorrcFile { get; set; }
|
||||
public string[] TorServices { get; set; }
|
||||
public Uri UpdateUrl { get; set; }
|
||||
|
@ -31,7 +31,6 @@ namespace BTCPayServer.Configuration
|
||||
app.Option("--nocsp", $"Disable CSP (default false)", CommandOptionType.BoolValue);
|
||||
app.Option("--sqlitefile", $"File name to an SQLite database file inside the data directory", CommandOptionType.SingleValue);
|
||||
app.Option("--externalservices", $"Links added to external services inside Server Settings / Services under the format service1:path2;service2:path2.(default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--bundlejscss", $"Bundle JavaScript and CSS files for better performance (default: true)", CommandOptionType.SingleValue);
|
||||
app.Option("--rootpath", "The root path in the URL to access BTCPay (default: /)", CommandOptionType.SingleValue);
|
||||
app.Option("--sshconnection", "SSH server to manage BTCPay under the form user@server:port (default: root@externalhost or empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--sshpassword", "SSH password to manage BTCPay (default: empty)", CommandOptionType.SingleValue);
|
||||
@ -46,7 +45,7 @@ namespace BTCPayServer.Configuration
|
||||
app.Option("--debuglog", "A rolling log file for debug messages.", CommandOptionType.SingleValue);
|
||||
app.Option("--debugloglevel", "The severity you log (default:information)", CommandOptionType.SingleValue);
|
||||
app.Option("--disable-registration", "Disables new user registrations (default:true)", CommandOptionType.SingleValue);
|
||||
app.Option("--plugin-remote", "Which github repository to fetch the available plugins list (default:btcpayserver/btcpayserver-plugins)", CommandOptionType.SingleValue);
|
||||
app.Option("--plugin-remote", "Obsolete, do not use", CommandOptionType.SingleValue);
|
||||
app.Option("--recommended-plugins", "Plugins which would be marked as recommended to be installed. Separated by newline or space", CommandOptionType.MultipleValue);
|
||||
app.Option("--xforwardedproto", "If specified, set X-Forwarded-Proto to the specified value, this may be useful if your reverse proxy handle https but is not configured to add X-Forwarded-Proto (example: --xforwardedproto https)", CommandOptionType.SingleValue);
|
||||
app.Option("--cheatmode", "Add some helper UI to facilitate dev-time testing (Default false)", CommandOptionType.BoolValue);
|
||||
@ -139,6 +138,7 @@ namespace BTCPayServer.Configuration
|
||||
{
|
||||
builder.AppendLine(CultureInfo.InvariantCulture, $"#{n.CryptoCode}.explorer.url={n.NBXplorerNetwork.DefaultSettings.DefaultUrl}");
|
||||
builder.AppendLine(CultureInfo.InvariantCulture, $"#{n.CryptoCode}.explorer.cookiefile={ n.NBXplorerNetwork.DefaultSettings.DefaultCookieFile}");
|
||||
builder.AppendLine(CultureInfo.InvariantCulture, $"#{n.CryptoCode}.blockexplorerlink=https://mempool.space/tx/{{0}}");
|
||||
if (n.SupportLightning)
|
||||
{
|
||||
builder.AppendLine(CultureInfo.InvariantCulture, $"#{n.CryptoCode}.lightning=/root/.lightning/lightning-rpc");
|
||||
|
@ -6,6 +6,7 @@ using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -22,33 +23,38 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
private readonly AppService _appService;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly CurrencyNameTable _currencies;
|
||||
|
||||
public GreenfieldAppsController(
|
||||
AppService appService,
|
||||
StoreRepository storeRepository,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
BTCPayNetworkProvider btcPayNetworkProvider
|
||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
CurrencyNameTable currencies
|
||||
)
|
||||
{
|
||||
_appService = appService;
|
||||
_storeRepository = storeRepository;
|
||||
_currencies = currencies;
|
||||
}
|
||||
|
||||
[HttpPost("~/api/v1/stores/{storeId}/apps/pos")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> CreatePointOfSaleApp(string storeId, CreatePointOfSaleAppRequest request)
|
||||
{
|
||||
var validationResult = Validate(request);
|
||||
var store = await _storeRepository.FindStore(storeId);
|
||||
if (store == null)
|
||||
return this.CreateAPIError(404, "store-not-found", "The store was not found");
|
||||
|
||||
// This is not obvious but we must have a non-null currency or else request validation may work incorrectly
|
||||
request.Currency = request.Currency ?? store.GetStoreBlob().DefaultCurrency;
|
||||
|
||||
var validationResult = ValidatePOSAppRequest(request);
|
||||
if (validationResult != null)
|
||||
{
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
var store = await _storeRepository.FindStore(storeId);
|
||||
if (store == null)
|
||||
return this.CreateAPIError(404, "store-not-found", "The store was not found");
|
||||
|
||||
var defaultCurrency = store.GetStoreBlob().DefaultCurrency;
|
||||
var appData = new AppData
|
||||
{
|
||||
StoreDataId = storeId,
|
||||
@ -56,36 +62,55 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
AppType = AppType.PointOfSale.ToString()
|
||||
};
|
||||
|
||||
appData.SetSettings(new PointOfSaleSettings
|
||||
{
|
||||
Title = request.Title,
|
||||
DefaultView = (Services.Apps.PosViewType)request.DefaultView,
|
||||
ShowCustomAmount = request.ShowCustomAmount,
|
||||
ShowDiscount = request.ShowDiscount,
|
||||
EnableTips = request.EnableTips,
|
||||
Currency = request.Currency ?? defaultCurrency,
|
||||
Template = request.Template,
|
||||
ButtonText = request.FixedAmountPayButtonText ?? PointOfSaleSettings.BUTTON_TEXT_DEF,
|
||||
CustomButtonText = request.CustomAmountPayButtonText ?? PointOfSaleSettings.CUSTOM_BUTTON_TEXT_DEF,
|
||||
CustomTipText = request.TipText ?? PointOfSaleSettings.CUSTOM_TIP_TEXT_DEF,
|
||||
CustomCSSLink = request.CustomCSSLink,
|
||||
NotificationUrl = request.NotificationUrl,
|
||||
RedirectUrl = request.RedirectUrl,
|
||||
Description = request.Description,
|
||||
EmbeddedCSS = request.EmbeddedCSS,
|
||||
RedirectAutomatically = request.RedirectAutomatically,
|
||||
RequiresRefundEmail = request.RequiresRefundEmail == true ?
|
||||
RequiresRefundEmail.On :
|
||||
request.RequiresRefundEmail == false ?
|
||||
RequiresRefundEmail.Off :
|
||||
RequiresRefundEmail.InheritFromStore,
|
||||
});
|
||||
appData.SetSettings(ToPointOfSaleSettings(request));
|
||||
|
||||
await _appService.UpdateOrCreateApp(appData);
|
||||
|
||||
return Ok(ToModel(appData));
|
||||
}
|
||||
|
||||
[HttpPut("~/api/v1/apps/pos/{appId}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> UpdatePointOfSaleApp(string appId, CreatePointOfSaleAppRequest request)
|
||||
{
|
||||
var app = await _appService.GetApp(appId, AppType.PointOfSale);
|
||||
if (app == null)
|
||||
{
|
||||
return AppNotFound();
|
||||
}
|
||||
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
|
||||
// This is not obvious but we must have a non-null currency or else request validation may work incorrectly
|
||||
request.Currency = request.Currency ?? settings.Currency;
|
||||
|
||||
var validationResult = ValidatePOSAppRequest(request);
|
||||
if (validationResult != null)
|
||||
{
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
app.Name = request.AppName;
|
||||
app.SetSettings(ToPointOfSaleSettings(request));
|
||||
|
||||
await _appService.UpdateOrCreateApp(app);
|
||||
|
||||
return Ok(ToModel(app));
|
||||
}
|
||||
|
||||
private RequiresRefundEmail? BoolToRequiresRefundEmail(bool? requiresRefundEmail)
|
||||
{
|
||||
switch (requiresRefundEmail)
|
||||
{
|
||||
case true:
|
||||
return RequiresRefundEmail.On;
|
||||
case false:
|
||||
return RequiresRefundEmail.Off;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("~/api/v1/apps/{appId}")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> GetApp(string appId)
|
||||
@ -118,19 +143,75 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return this.CreateAPIError(404, "app-not-found", "The app with specified ID was not found");
|
||||
}
|
||||
|
||||
private PointOfSaleSettings ToPointOfSaleSettings(CreatePointOfSaleAppRequest request)
|
||||
{
|
||||
return new PointOfSaleSettings()
|
||||
{
|
||||
Title = request.Title,
|
||||
DefaultView = (Services.Apps.PosViewType)request.DefaultView,
|
||||
ShowCustomAmount = request.ShowCustomAmount,
|
||||
ShowDiscount = request.ShowDiscount,
|
||||
EnableTips = request.EnableTips,
|
||||
Currency = request.Currency,
|
||||
Template = request.Template != null ? _appService.SerializeTemplate(_appService.Parse(request.Template, request.Currency)) : null,
|
||||
ButtonText = request.FixedAmountPayButtonText ?? PointOfSaleSettings.BUTTON_TEXT_DEF,
|
||||
CustomButtonText = request.CustomAmountPayButtonText ?? PointOfSaleSettings.CUSTOM_BUTTON_TEXT_DEF,
|
||||
CustomTipText = request.TipText ?? PointOfSaleSettings.CUSTOM_TIP_TEXT_DEF,
|
||||
CustomCSSLink = request.CustomCSSLink,
|
||||
NotificationUrl = request.NotificationUrl,
|
||||
RedirectUrl = request.RedirectUrl,
|
||||
Description = request.Description,
|
||||
EmbeddedCSS = request.EmbeddedCSS,
|
||||
RedirectAutomatically = request.RedirectAutomatically,
|
||||
RequiresRefundEmail = BoolToRequiresRefundEmail(request.RequiresRefundEmail) ?? RequiresRefundEmail.InheritFromStore,
|
||||
CheckoutFormId = request.CheckoutFormId,
|
||||
CheckoutType = request.CheckoutType ?? CheckoutType.V1
|
||||
};
|
||||
}
|
||||
|
||||
private PointOfSaleAppData ToModel(AppData appData)
|
||||
{
|
||||
var settings = appData.GetSettings<PointOfSaleSettings>();
|
||||
|
||||
return new PointOfSaleAppData
|
||||
{
|
||||
Id = appData.Id,
|
||||
AppType = appData.AppType,
|
||||
Name = appData.Name,
|
||||
StoreId = appData.StoreDataId,
|
||||
Created = appData.Created
|
||||
Created = appData.Created,
|
||||
};
|
||||
}
|
||||
|
||||
private IActionResult? Validate(CreateAppRequest request)
|
||||
private IActionResult? ValidatePOSAppRequest(CreatePointOfSaleAppRequest request)
|
||||
{
|
||||
var validationResult = ValidateCreateAppRequest(request);
|
||||
if (request.Currency != null && _currencies.GetCurrencyData(request.Currency, false) == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.Currency), "Invalid currency");
|
||||
}
|
||||
|
||||
if (request.Template != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_appService.SerializeTemplate(_appService.Parse(request.Template, request.Currency));
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.Template), "Invalid template");
|
||||
}
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
validationResult = this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
private IActionResult? ValidateCreateAppRequest(CreateAppRequest request)
|
||||
{
|
||||
if (request is null)
|
||||
{
|
||||
|
@ -437,6 +437,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
DefaultLanguage = entity.DefaultLanguage,
|
||||
RedirectAutomatically = entity.RedirectAutomatically,
|
||||
RequiresRefundEmail = entity.RequiresRefundEmail,
|
||||
CheckoutFormId = entity.CheckoutFormId,
|
||||
CheckoutType = entity.CheckoutType,
|
||||
RedirectURL = entity.RedirectURLTemplate
|
||||
},
|
||||
Receipt = entity.ReceiptOptions
|
||||
|
@ -101,6 +101,14 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return base.GetInvoice(cryptoCode, id, cancellationToken);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanUseInternalLightningNode,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/server/lightning/{cryptoCode}/invoices")]
|
||||
public override Task<IActionResult> GetInvoices(string cryptoCode, [FromQuery] bool? pendingOnly, [FromQuery] long? offsetIndex, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return base.GetInvoices(cryptoCode, pendingOnly, offsetIndex, cancellationToken);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanUseInternalLightningNode,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/server/lightning/{cryptoCode}/invoices/pay")]
|
||||
|
@ -111,6 +111,14 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return base.GetInvoice(cryptoCode, id, cancellationToken);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices")]
|
||||
public override Task<IActionResult> GetInvoices(string cryptoCode, [FromQuery] bool? pendingOnly, [FromQuery] long? offsetIndex, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return base.GetInvoices(cryptoCode, pendingOnly, offsetIndex, cancellationToken);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanCreateLightningInvoiceInStore,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices")]
|
||||
|
@ -2,7 +2,6 @@ using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
@ -46,7 +45,14 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return Ok(new LightningNodeInformationData
|
||||
{
|
||||
BlockHeight = info.BlockHeight,
|
||||
NodeURIs = info.NodeInfoList.Select(nodeInfo => nodeInfo).ToArray()
|
||||
NodeURIs = info.NodeInfoList.Select(nodeInfo => nodeInfo).ToArray(),
|
||||
Alias = info.Alias,
|
||||
Color = info.Color,
|
||||
Version = info.Version,
|
||||
PeersCount = info.PeersCount,
|
||||
ActiveChannelsCount = info.ActiveChannelsCount,
|
||||
InactiveChannelsCount = info.InactiveChannelsCount,
|
||||
PendingChannelsCount = info.PendingChannelsCount
|
||||
});
|
||||
}
|
||||
|
||||
@ -202,9 +208,10 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
var lightningClient = await GetLightningClient(cryptoCode, true);
|
||||
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
||||
|
||||
if (lightningInvoice?.BOLT11 is null ||
|
||||
!BOLT11PaymentRequest.TryParse(lightningInvoice.BOLT11, out _, network.NBitcoinNetwork))
|
||||
BOLT11PaymentRequest bolt11 = null;
|
||||
|
||||
if (string.IsNullOrEmpty(lightningInvoice.BOLT11) ||
|
||||
!BOLT11PaymentRequest.TryParse(lightningInvoice.BOLT11, out bolt11, network.NBitcoinNetwork))
|
||||
{
|
||||
ModelState.AddModelError(nameof(lightningInvoice.BOLT11), "The BOLT11 invoice was invalid.");
|
||||
}
|
||||
@ -214,21 +221,54 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
var param = lightningInvoice?.MaxFeeFlat != null || lightningInvoice?.MaxFeePercent != null || lightningInvoice?.Amount != null
|
||||
? new PayInvoiceParams { MaxFeePercent = lightningInvoice.MaxFeePercent, MaxFeeFlat = lightningInvoice.MaxFeeFlat, Amount = lightningInvoice.Amount }
|
||||
var param = lightningInvoice.MaxFeeFlat != null || lightningInvoice.MaxFeePercent != null
|
||||
|| lightningInvoice.Amount != null || lightningInvoice.SendTimeout != null
|
||||
? new PayInvoiceParams
|
||||
{
|
||||
MaxFeePercent = lightningInvoice.MaxFeePercent,
|
||||
MaxFeeFlat = lightningInvoice.MaxFeeFlat,
|
||||
Amount = lightningInvoice.Amount,
|
||||
SendTimeout = lightningInvoice.SendTimeout
|
||||
}
|
||||
: null;
|
||||
var result = await lightningClient.Pay(lightningInvoice.BOLT11, param, cancellationToken);
|
||||
|
||||
if (result.Result is PayResult.Ok or PayResult.Unknown && bolt11?.PaymentHash is not null)
|
||||
{
|
||||
// get a new instance of the LN client, because the old one might have disposed its HTTPClient
|
||||
lightningClient = await GetLightningClient(cryptoCode, true);
|
||||
|
||||
var paymentHash = bolt11.PaymentHash.ToString();
|
||||
var payment = await lightningClient.GetPayment(paymentHash, cancellationToken);
|
||||
var data = new LightningPaymentData
|
||||
{
|
||||
Id = payment.Id,
|
||||
PaymentHash = paymentHash,
|
||||
Status = payment.Status,
|
||||
BOLT11 = payment.BOLT11,
|
||||
Preimage = payment.Preimage,
|
||||
CreatedAt = payment.CreatedAt,
|
||||
TotalAmount = payment.AmountSent,
|
||||
FeeAmount = payment.Fee,
|
||||
};
|
||||
return result.Result is PayResult.Ok ? Ok(data) : Accepted(data);
|
||||
}
|
||||
|
||||
return result.Result switch
|
||||
{
|
||||
PayResult.CouldNotFindRoute => this.CreateAPIError("could-not-find-route", "Impossible to find a route to the peer"),
|
||||
PayResult.Error => this.CreateAPIError("generic-error", result.ErrorDetail),
|
||||
PayResult.Unknown => Accepted(new LightningPaymentData
|
||||
{
|
||||
Status = LightningPaymentStatus.Unknown
|
||||
}),
|
||||
PayResult.Ok => Ok(new LightningPaymentData
|
||||
{
|
||||
Status = LightningPaymentStatus.Complete,
|
||||
TotalAmount = result.Details?.TotalAmount,
|
||||
FeeAmount = result.Details?.FeeAmount
|
||||
}),
|
||||
_ => throw new NotSupportedException("Unsupported Payresult")
|
||||
_ => throw new NotSupportedException("Unsupported PayResult")
|
||||
};
|
||||
}
|
||||
|
||||
@ -239,6 +279,14 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return inv == null ? this.CreateAPIError(404, "invoice-not-found", "Impossible to find a lightning invoice with this id") : Ok(ToModel(inv));
|
||||
}
|
||||
|
||||
public virtual async Task<IActionResult> GetInvoices(string cryptoCode, [FromQuery] bool? pendingOnly, [FromQuery] long? offsetIndex, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var lightningClient = await GetLightningClient(cryptoCode, false);
|
||||
var param = new ListInvoicesParams { PendingOnly = pendingOnly, OffsetIndex = offsetIndex };
|
||||
var invoices = await lightningClient.ListInvoices(param, cancellationToken);
|
||||
return Ok(invoices.Select(ToModel));
|
||||
}
|
||||
|
||||
public virtual async Task<IActionResult> CreateInvoice(string cryptoCode, CreateLightningInvoiceRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var lightningClient = await GetLightningClient(cryptoCode, false);
|
||||
@ -296,7 +344,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
private LightningInvoiceData ToModel(LightningInvoice invoice)
|
||||
{
|
||||
return new LightningInvoiceData
|
||||
var data = new LightningInvoiceData
|
||||
{
|
||||
Amount = invoice.Amount,
|
||||
Id = invoice.Id,
|
||||
@ -306,6 +354,12 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
BOLT11 = invoice.BOLT11,
|
||||
ExpiresAt = invoice.ExpiresAt
|
||||
};
|
||||
|
||||
if (invoice.CustomRecords != null)
|
||||
{
|
||||
data.CustomRecords = invoice.CustomRecords;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private LightningPaymentData ToModel(LightningPayment payment)
|
||||
|
@ -1,18 +1,22 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.PaymentRequest;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.PaymentRequests;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using PaymentRequestData = BTCPayServer.Data.PaymentRequestData;
|
||||
|
||||
namespace BTCPayServer.Controllers.Greenfield
|
||||
@ -22,14 +26,26 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public class GreenfieldPaymentRequestsController : ControllerBase
|
||||
{
|
||||
private readonly InvoiceRepository _InvoiceRepository;
|
||||
private readonly UIInvoiceController _invoiceController;
|
||||
private readonly PaymentRequestRepository _paymentRequestRepository;
|
||||
private readonly CurrencyNameTable _currencyNameTable;
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
|
||||
public GreenfieldPaymentRequestsController(PaymentRequestRepository paymentRequestRepository,
|
||||
CurrencyNameTable currencyNameTable)
|
||||
public GreenfieldPaymentRequestsController(
|
||||
InvoiceRepository invoiceRepository,
|
||||
UIInvoiceController invoiceController,
|
||||
PaymentRequestRepository paymentRequestRepository,
|
||||
PaymentRequestService paymentRequestService,
|
||||
CurrencyNameTable currencyNameTable,
|
||||
LinkGenerator linkGenerator)
|
||||
{
|
||||
_InvoiceRepository = invoiceRepository;
|
||||
_invoiceController = invoiceController;
|
||||
_paymentRequestRepository = paymentRequestRepository;
|
||||
PaymentRequestService = paymentRequestService;
|
||||
_currencyNameTable = currencyNameTable;
|
||||
_linkGenerator = linkGenerator;
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanViewPaymentRequests, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
@ -56,6 +72,62 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return Ok(FromModel(pr.First()));
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanViewPaymentRequests, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/stores/{storeId}/payment-requests/{paymentRequestId}/pay")]
|
||||
public async Task<IActionResult> PayPaymentRequest(string storeId, string paymentRequestId, [FromBody] PayPaymentRequestRequest pay, CancellationToken cancellationToken)
|
||||
{
|
||||
var pr = await this.PaymentRequestService.GetPaymentRequest(paymentRequestId);
|
||||
if (pr is null || pr.StoreId != storeId)
|
||||
return PaymentRequestNotFound();
|
||||
|
||||
var amount = pay?.Amount;
|
||||
if (amount.HasValue && amount.Value <= 0)
|
||||
{
|
||||
ModelState.AddModelError(nameof(pay.Amount), "The amount should be more than 0");
|
||||
}
|
||||
if (amount.HasValue && !pr.AllowCustomPaymentAmounts && amount.Value != pr.AmountDue)
|
||||
{
|
||||
ModelState.AddModelError(nameof(pay.Amount), "This payment request doesn't allow custom payment amount");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
|
||||
if (pr.Archived)
|
||||
{
|
||||
return this.CreateAPIError("archived", "You cannot pay an archived payment request");
|
||||
}
|
||||
|
||||
if (pr.AmountDue <= 0)
|
||||
{
|
||||
return this.CreateAPIError("already-paid", "This payment request is already paid");
|
||||
}
|
||||
|
||||
if (pr.ExpiryDate.HasValue && DateTime.UtcNow >= pr.ExpiryDate)
|
||||
{
|
||||
return this.CreateAPIError("expired", "This payment request is expired");
|
||||
}
|
||||
|
||||
if (pay?.AllowPendingInvoiceReuse is true)
|
||||
{
|
||||
if (pr.Invoices.GetReusableInvoice(amount)?.Id is string invoiceId)
|
||||
{
|
||||
var inv = await _InvoiceRepository.GetInvoice(invoiceId);
|
||||
return Ok(GreenfieldInvoiceController.ToModel(inv, _linkGenerator, Request));
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var invoice = await _invoiceController.CreatePaymentRequestInvoice(pr, amount, this.StoreData, Request, cancellationToken);
|
||||
return Ok(GreenfieldInvoiceController.ToModel(invoice, _linkGenerator, Request));
|
||||
}
|
||||
catch (BitpayHttpException e)
|
||||
{
|
||||
return this.CreateAPIError(null, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyPaymentRequests,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpDelete("~/api/v1/stores/{storeId}/payment-requests/{paymentRequestId}")]
|
||||
@ -97,6 +169,9 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return Ok(FromModel(pr));
|
||||
}
|
||||
public Data.StoreData StoreData => HttpContext.GetStoreData();
|
||||
|
||||
public PaymentRequestService PaymentRequestService { get; }
|
||||
|
||||
[HttpPut("~/api/v1/stores/{storeId}/payment-requests/{paymentRequestId}")]
|
||||
[Authorize(Policy = Policies.CanModifyPaymentRequests,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
|
@ -52,16 +52,22 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
private static OnChainAutomatedPayoutSettings ToModel(PayoutProcessorData data)
|
||||
{
|
||||
var blob = BaseAutomatedPayoutProcessor<OnChainAutomatedPayoutBlob>.GetBlob(data);
|
||||
return new OnChainAutomatedPayoutSettings()
|
||||
{
|
||||
FeeBlockTarget = blob.FeeTargetBlock,
|
||||
PaymentMethod = data.PaymentMethod,
|
||||
IntervalSeconds = InvoiceRepository.FromBytes<AutomatedPayoutBlob>(data.Blob).Interval
|
||||
IntervalSeconds = blob.Interval
|
||||
};
|
||||
}
|
||||
|
||||
private static AutomatedPayoutBlob FromModel(OnChainAutomatedPayoutSettings data)
|
||||
private static OnChainAutomatedPayoutBlob FromModel(OnChainAutomatedPayoutSettings data)
|
||||
{
|
||||
return new AutomatedPayoutBlob() {Interval = data.IntervalSeconds};
|
||||
return new OnChainAutomatedPayoutBlob()
|
||||
{
|
||||
FeeTargetBlock = data.FeeBlockTarget ?? 1,
|
||||
Interval = data.IntervalSeconds
|
||||
};
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
|
@ -35,7 +35,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly ISettingsRepository _settingsRepository;
|
||||
|
||||
public GreenfieldStoreLightningNetworkPaymentMethodsController(
|
||||
StoreRepository storeRepository,
|
||||
@ -47,7 +46,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
_storeRepository = storeRepository;
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_authorizationService = authorizationService;
|
||||
_settingsRepository = settingsRepository;
|
||||
PoliciesSettings = policiesSettings;
|
||||
}
|
||||
|
||||
|
@ -52,7 +52,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
private readonly WalletReceiveService _walletReceiveService;
|
||||
private readonly IFeeProviderFactory _feeProviderFactory;
|
||||
private readonly LabelFactory _labelFactory;
|
||||
private readonly UTXOLocker _utxoLocker;
|
||||
|
||||
public GreenfieldStoreOnChainWalletsController(
|
||||
@ -69,7 +68,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
EventAggregator eventAggregator,
|
||||
WalletReceiveService walletReceiveService,
|
||||
IFeeProviderFactory feeProviderFactory,
|
||||
LabelFactory labelFactory,
|
||||
UTXOLocker utxoLocker
|
||||
)
|
||||
{
|
||||
@ -86,7 +84,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
_eventAggregator = eventAggregator;
|
||||
_walletReceiveService = walletReceiveService;
|
||||
_feeProviderFactory = feeProviderFactory;
|
||||
_labelFactory = labelFactory;
|
||||
_utxoLocker = utxoLocker;
|
||||
}
|
||||
|
||||
@ -202,7 +199,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
if (!string.IsNullOrWhiteSpace(labelFilter))
|
||||
{
|
||||
walletTransactionsInfoAsync.TryGetValue(t.TransactionId.ToString(), out var transactionInfo);
|
||||
if (transactionInfo?.Labels.ContainsKey(labelFilter) is true)
|
||||
if (transactionInfo?.LabelColors.ContainsKey(labelFilter) is true)
|
||||
filteredList.Add(t);
|
||||
}
|
||||
if (statusFilter?.Any() is true)
|
||||
@ -270,36 +267,18 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
}
|
||||
|
||||
var walletId = new WalletId(storeId, cryptoCode);
|
||||
var walletTransactionsInfoAsync = _walletRepository.GetWalletTransactionsInfo(walletId);
|
||||
if (!(await walletTransactionsInfoAsync).TryGetValue(transactionId, out var walletTransactionInfo))
|
||||
{
|
||||
walletTransactionInfo = new WalletTransactionInfo();
|
||||
}
|
||||
var txObjectId = new WalletObjectId(walletId, WalletObjectData.Types.Tx, transactionId);
|
||||
|
||||
if (request.Comment != null)
|
||||
{
|
||||
walletTransactionInfo.Comment = request.Comment.Trim().Truncate(WalletTransactionDataExtensions.MaxCommentSize);
|
||||
await _walletRepository.SetWalletObjectComment(txObjectId, request.Comment);
|
||||
}
|
||||
|
||||
if (request.Labels != null)
|
||||
{
|
||||
var walletBlobInfo = await _walletRepository.GetWalletInfo(walletId);
|
||||
|
||||
foreach (string label in request.Labels)
|
||||
{
|
||||
var rawLabel = await _labelFactory.BuildLabel(
|
||||
walletBlobInfo,
|
||||
Request,
|
||||
walletTransactionInfo,
|
||||
walletId,
|
||||
transactionId,
|
||||
label
|
||||
);
|
||||
walletTransactionInfo.Labels.TryAdd(rawLabel.Text, rawLabel);
|
||||
}
|
||||
await _walletRepository.AddWalletObjectLabels(txObjectId, request.Labels.ToArray());
|
||||
}
|
||||
|
||||
await _walletRepository.SetWalletTransactionInfo(walletId, transactionId, walletTransactionInfo);
|
||||
var walletTransactionsInfo =
|
||||
(await _walletRepository.GetWalletTransactionsInfo(walletId, new[] { transactionId }))
|
||||
.Values
|
||||
@ -319,19 +298,20 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
var wallet = _btcPayWalletProvider.GetWallet(network);
|
||||
|
||||
var walletId = new WalletId(storeId, cryptoCode);
|
||||
var walletTransactionsInfoAsync = await _walletRepository.GetWalletTransactionsInfo(walletId);
|
||||
var utxos = await wallet.GetUnspentCoins(derivationScheme.AccountDerivation);
|
||||
|
||||
var walletTransactionsInfoAsync = await _walletRepository.GetWalletTransactionsInfo(walletId, utxos.Select(u => u.OutPoint.Hash.ToString()).ToHashSet().ToArray());
|
||||
return Ok(utxos.Select(coin =>
|
||||
{
|
||||
walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.Hash.ToString(), out var info);
|
||||
var labels = info?.Labels ?? new Dictionary<string, LabelData>();
|
||||
|
||||
return new OnChainWalletUTXOData()
|
||||
{
|
||||
Outpoint = coin.OutPoint,
|
||||
Amount = coin.Value.GetValue(network),
|
||||
Comment = info?.Comment,
|
||||
Labels = info?.Labels,
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
Labels = info?.LegacyLabels ?? new Dictionary<string, LabelData>(),
|
||||
#pragma warning restore CS0612 // Type or member is obsolete
|
||||
Link = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink,
|
||||
coin.OutPoint.Hash.ToString()),
|
||||
Timestamp = coin.Timestamp,
|
||||
@ -592,8 +572,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
payjoinPSBT.Finalize();
|
||||
var payjoinTransaction = payjoinPSBT.ExtractTransaction();
|
||||
var hash = payjoinTransaction.GetHash();
|
||||
_eventAggregator.Publish(new UpdateTransactionLabel(new WalletId(Store.Id, cryptoCode), hash,
|
||||
UpdateTransactionLabel.PayjoinLabelTemplate()));
|
||||
await this._walletRepository.AddWalletTransactionAttachment(new WalletId(Store.Id, cryptoCode), hash, Attachment.Payjoin());
|
||||
broadcastResult = await explorerClient.BroadcastAsync(payjoinTransaction);
|
||||
if (broadcastResult.Success)
|
||||
{
|
||||
@ -676,7 +655,9 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
TransactionHash = tx.TransactionId,
|
||||
Comment = walletTransactionsInfoAsync?.Comment ?? string.Empty,
|
||||
Labels = walletTransactionsInfoAsync?.Labels ?? new Dictionary<string, LabelData>(),
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
Labels = walletTransactionsInfoAsync?.LegacyLabels ?? new Dictionary<string, LabelData>(),
|
||||
#pragma warning restore CS0612 // Type or member is obsolete
|
||||
Amount = tx.BalanceChange.GetValue(wallet.Network),
|
||||
BlockHash = tx.BlockHash,
|
||||
BlockHeight = tx.Height,
|
||||
|
@ -0,0 +1,204 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using RateSource = BTCPayServer.Client.Models.RateSource;
|
||||
|
||||
namespace BTCPayServer.Controllers.GreenField
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/v1/stores/{storeId}/rates/configuration")]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public class GreenfieldStoreRateConfigurationController : ControllerBase
|
||||
{
|
||||
private readonly RateFetcher _rateProviderFactory;
|
||||
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
|
||||
public GreenfieldStoreRateConfigurationController(
|
||||
RateFetcher rateProviderFactory,
|
||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
StoreRepository storeRepository)
|
||||
{
|
||||
_rateProviderFactory = rateProviderFactory;
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_storeRepository = storeRepository;
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public IActionResult GetStoreRateConfiguration()
|
||||
{
|
||||
var data = HttpContext.GetStoreData();
|
||||
var blob = data.GetStoreBlob();
|
||||
|
||||
return Ok(new StoreRateConfiguration()
|
||||
{
|
||||
EffectiveScript = blob.GetRateRules(_btcPayNetworkProvider, out var preferredExchange).ToString(),
|
||||
Spread = blob.Spread * 100.0m,
|
||||
IsCustomScript = blob.RateScripting,
|
||||
PreferredSource = preferredExchange ? blob.PreferredExchange : null
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("/misc/rate-sources")]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie + "," + AuthenticationSchemes.Greenfield)]
|
||||
public ActionResult<List<RateSource>> GetRateSources()
|
||||
{
|
||||
return Ok(_rateProviderFactory.RateProviderFactory.GetSupportedExchanges().Select(provider =>
|
||||
new RateSource() {Id = provider.Id, Name = provider.DisplayName}));
|
||||
}
|
||||
|
||||
[HttpPut("")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> UpdateStoreRateConfiguration(
|
||||
StoreRateConfiguration configuration)
|
||||
{
|
||||
var storeData = HttpContext.GetStoreData();
|
||||
var blob = storeData.GetStoreBlob();
|
||||
ValidateAndSanitizeConfiguration(configuration, blob);
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
|
||||
PopulateBlob(configuration, blob);
|
||||
|
||||
storeData.SetStoreBlob(blob);
|
||||
|
||||
await _storeRepository.UpdateStore(storeData);
|
||||
|
||||
|
||||
return GetStoreRateConfiguration();
|
||||
}
|
||||
|
||||
[HttpPost("preview")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> PreviewUpdateStoreRateConfiguration(
|
||||
StoreRateConfiguration configuration, [FromQuery] string[] currencyPair)
|
||||
{
|
||||
var data = HttpContext.GetStoreData();
|
||||
var blob = data.GetStoreBlob();
|
||||
var parsedCurrencyPairs = new HashSet<CurrencyPair>();
|
||||
|
||||
|
||||
foreach (var pair in currencyPair ?? Array.Empty<string>())
|
||||
{
|
||||
if (!CurrencyPair.TryParse(pair, out var currencyPairParsed))
|
||||
{
|
||||
ModelState.AddModelError(nameof(currencyPair),
|
||||
$"Invalid currency pair '{pair}' (it should be formatted like BTC_USD,BTC_CAD)");
|
||||
break;
|
||||
}
|
||||
|
||||
parsedCurrencyPairs.Add(currencyPairParsed);
|
||||
}
|
||||
ValidateAndSanitizeConfiguration(configuration, blob);
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
PopulateBlob(configuration, blob);
|
||||
|
||||
var rules = blob.GetRateRules(_btcPayNetworkProvider);
|
||||
|
||||
|
||||
var rateTasks = _rateProviderFactory.FetchRates(parsedCurrencyPairs, rules, CancellationToken.None);
|
||||
await Task.WhenAll(rateTasks.Values);
|
||||
var result = new List<StoreRatePreviewResult>();
|
||||
foreach (var rateTask in rateTasks)
|
||||
{
|
||||
var rateTaskResult = rateTask.Value.Result;
|
||||
|
||||
result.Add(new StoreRatePreviewResult()
|
||||
{
|
||||
CurrencyPair = rateTask.Key.ToString(),
|
||||
Errors = rateTaskResult.Errors.Select(errors => errors.ToString()).ToList(),
|
||||
Rate = rateTaskResult.Errors.Any() ? (decimal?)null : rateTaskResult.BidAsk.Bid
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
private void ValidateAndSanitizeConfiguration(StoreRateConfiguration? configuration, StoreBlob storeBlob)
|
||||
{
|
||||
if (configuration is null)
|
||||
{
|
||||
ModelState.AddModelError("", "Body required");
|
||||
return;
|
||||
}
|
||||
if (configuration.Spread < 0 || configuration.Spread > 100)
|
||||
{
|
||||
ModelState.AddModelError(nameof(configuration.Spread),
|
||||
$"Spread value must be in %, between 0 and 100");
|
||||
}
|
||||
|
||||
if (configuration.IsCustomScript)
|
||||
{
|
||||
if (string.IsNullOrEmpty(configuration.EffectiveScript))
|
||||
{
|
||||
configuration.EffectiveScript = storeBlob.GetDefaultRateRules(_btcPayNetworkProvider).ToString();
|
||||
}
|
||||
|
||||
if (!RateRules.TryParse(configuration.EffectiveScript, out var r))
|
||||
{
|
||||
ModelState.AddModelError(nameof(configuration.EffectiveScript),
|
||||
$"Script syntax is invalid");
|
||||
}
|
||||
else
|
||||
{
|
||||
configuration.EffectiveScript = r.ToString();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(configuration.PreferredSource))
|
||||
{
|
||||
ModelState.AddModelError(nameof(configuration.PreferredSource),
|
||||
$"You can't set the preferredSource if you are using custom scripts");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!string.IsNullOrEmpty(configuration.EffectiveScript))
|
||||
{
|
||||
ModelState.AddModelError(nameof(configuration.EffectiveScript),
|
||||
$"You can't set the effectiveScript if you aren't using custom scripts");
|
||||
}
|
||||
if (string.IsNullOrEmpty(configuration.PreferredSource))
|
||||
{
|
||||
ModelState.AddModelError(nameof(configuration.PreferredSource),
|
||||
$"The preferredSource is required if you aren't using custom scripts");
|
||||
}
|
||||
|
||||
configuration.PreferredSource = _rateProviderFactory
|
||||
.RateProviderFactory
|
||||
.GetSupportedExchanges()
|
||||
.FirstOrDefault(s =>
|
||||
s.Id.Equals(configuration.PreferredSource,
|
||||
StringComparison.InvariantCultureIgnoreCase))?.Id;
|
||||
|
||||
if (string.IsNullOrEmpty(configuration.PreferredSource))
|
||||
{
|
||||
ModelState.AddModelError(nameof(configuration.PreferredSource),
|
||||
$"Unsupported source, please check /misc/rate-sources to see valid values ({configuration.PreferredSource})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void PopulateBlob(StoreRateConfiguration configuration, StoreBlob storeBlob)
|
||||
{
|
||||
storeBlob.PreferredExchange = configuration.PreferredSource;
|
||||
storeBlob.Spread = configuration.Spread / 100.0m;
|
||||
storeBlob.RateScripting = configuration.IsCustomScript;
|
||||
storeBlob.RateScript = configuration.EffectiveScript;
|
||||
}
|
||||
}
|
||||
}
|
@ -127,6 +127,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
//we do not include PaymentMethodCriteria because moving the CurrencyValueJsonConverter to the Client csproj is hard and requires a refactor (#1571 & #1572)
|
||||
NetworkFeeMode = storeBlob.NetworkFeeMode,
|
||||
RequiresRefundEmail = storeBlob.RequiresRefundEmail,
|
||||
CheckoutFormId = storeBlob.CheckoutFormId,
|
||||
CheckoutType = storeBlob.CheckoutType,
|
||||
Receipt = InvoiceDataBase.ReceiptOptions.Merge(storeBlob.ReceiptOptions, null),
|
||||
LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi,
|
||||
LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints,
|
||||
@ -165,6 +167,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
blob.NetworkFeeMode = restModel.NetworkFeeMode;
|
||||
blob.DefaultCurrency = restModel.DefaultCurrency;
|
||||
blob.RequiresRefundEmail = restModel.RequiresRefundEmail;
|
||||
blob.CheckoutFormId = restModel.CheckoutFormId;
|
||||
blob.CheckoutType = restModel.CheckoutType;
|
||||
blob.ReceiptOptions = InvoiceDataBase.ReceiptOptions.Merge(restModel.Receipt, null);
|
||||
blob.LightningAmountInSatoshi = restModel.LightningAmountInSatoshi;
|
||||
blob.LightningPrivateRouteHints = restModel.LightningPrivateRouteHints;
|
||||
|
@ -351,7 +351,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return GetFromActionResult<LightningNodeBalanceData>(
|
||||
await GetController<GreenfieldStoreLightningNodeApiController>().GetBalance(cryptoCode));
|
||||
await GetController<GreenfieldStoreLightningNodeApiController>().GetBalance(cryptoCode, token));
|
||||
}
|
||||
|
||||
public override async Task ConnectToLightningNode(string storeId, string cryptoCode,
|
||||
@ -381,10 +381,11 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
await GetController<GreenfieldStoreLightningNodeApiController>().GetDepositAddress(cryptoCode, token));
|
||||
}
|
||||
|
||||
public override async Task PayLightningInvoice(string storeId, string cryptoCode,
|
||||
public override async Task<LightningPaymentData> PayLightningInvoice(string storeId, string cryptoCode,
|
||||
PayLightningInvoiceRequest request, CancellationToken token = default)
|
||||
{
|
||||
HandleActionResult(await GetController<GreenfieldStoreLightningNodeApiController>().PayInvoice(cryptoCode, request, token));
|
||||
return GetFromActionResult<LightningPaymentData>(
|
||||
await GetController<GreenfieldStoreLightningNodeApiController>().PayInvoice(cryptoCode, request, token));
|
||||
}
|
||||
|
||||
public override async Task<LightningInvoiceData> GetLightningInvoice(string storeId, string cryptoCode,
|
||||
@ -394,6 +395,13 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
await GetController<GreenfieldStoreLightningNodeApiController>().GetInvoice(cryptoCode, invoiceId, token));
|
||||
}
|
||||
|
||||
public override async Task<LightningInvoiceData[]> GetLightningInvoices(string storeId, string cryptoCode,
|
||||
bool? pendingOnly = null, long? offsetIndex = null, CancellationToken token = default)
|
||||
{
|
||||
return GetFromActionResult<LightningInvoiceData[]>(
|
||||
await GetController<GreenfieldStoreLightningNodeApiController>().GetInvoices(cryptoCode, pendingOnly, offsetIndex, token));
|
||||
}
|
||||
|
||||
public override async Task<LightningInvoiceData> CreateLightningInvoice(string storeId, string cryptoCode,
|
||||
CreateLightningInvoiceRequest request, CancellationToken token = default)
|
||||
{
|
||||
@ -455,6 +463,13 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
await GetController<GreenfieldInternalLightningNodeApiController>().GetInvoice(cryptoCode, invoiceId, token));
|
||||
}
|
||||
|
||||
public override async Task<LightningInvoiceData[]> GetLightningInvoices(string cryptoCode,
|
||||
bool? pendingOnly = null, long? offsetIndex = null, CancellationToken token = default)
|
||||
{
|
||||
return GetFromActionResult<LightningInvoiceData[]>(
|
||||
await GetController<GreenfieldInternalLightningNodeApiController>().GetInvoices(cryptoCode, pendingOnly, offsetIndex, token));
|
||||
}
|
||||
|
||||
public override async Task<LightningInvoiceData> CreateLightningInvoice(string cryptoCode,
|
||||
CreateLightningInvoiceRequest request,
|
||||
CancellationToken token = default)
|
||||
@ -577,6 +592,12 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
HandleActionResult(await GetController<GreenfieldPaymentRequestsController>().ArchivePaymentRequest(storeId, paymentRequestId));
|
||||
}
|
||||
|
||||
public override async Task<InvoiceData> PayPaymentRequest(string storeId, string paymentRequestId, PayPaymentRequestRequest request, CancellationToken token = default)
|
||||
{
|
||||
return GetFromActionResult<InvoiceData>(
|
||||
await GetController<GreenfieldPaymentRequestsController>().PayPaymentRequest(storeId, paymentRequestId, request, token));
|
||||
}
|
||||
|
||||
public override async Task<PaymentRequestData> CreatePaymentRequest(string storeId,
|
||||
CreatePaymentRequestRequest request, CancellationToken token = default)
|
||||
{
|
||||
@ -1103,6 +1124,14 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
await GetController<GreenfieldAppsController>().CreatePointOfSaleApp(storeId, request));
|
||||
}
|
||||
|
||||
public override async Task<PointOfSaleAppData> UpdatePointOfSaleApp(
|
||||
string appId,
|
||||
CreatePointOfSaleAppRequest request, CancellationToken token = default)
|
||||
{
|
||||
return GetFromActionResult<PointOfSaleAppData>(
|
||||
await GetController<GreenfieldAppsController>().UpdatePointOfSaleApp(appId, request));
|
||||
}
|
||||
|
||||
public override async Task<AppDataBase> GetApp(string appId, CancellationToken token = default)
|
||||
{
|
||||
return GetFromActionResult<AppDataBase>(
|
||||
@ -1113,5 +1142,31 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
HandleActionResult(await GetController<GreenfieldAppsController>().DeleteApp(appId));
|
||||
}
|
||||
|
||||
public override Task<List<RateSource>> GetRateSources(CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(GetFromActionResult(GetController<GreenfieldStoreRateConfigurationController>().GetRateSources()));
|
||||
}
|
||||
|
||||
public override Task<StoreRateConfiguration> GetStoreRateConfiguration(string storeId, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(GetFromActionResult<StoreRateConfiguration>(GetController<GreenfieldStoreRateConfigurationController>().GetStoreRateConfiguration()));
|
||||
}
|
||||
|
||||
public override async Task<List<StoreRatePreviewResult>> PreviewUpdateStoreRateConfiguration(string storeId,
|
||||
StoreRateConfiguration request,
|
||||
string[] currencyPair,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return GetFromActionResult<List<StoreRatePreviewResult>>(
|
||||
await GetController<GreenfieldStoreRateConfigurationController>().PreviewUpdateStoreRateConfiguration(request,
|
||||
currencyPair));
|
||||
}
|
||||
|
||||
public override async Task<StoreRateConfiguration> UpdateStoreRateConfiguration(string storeId, StoreRateConfiguration request, CancellationToken token = default)
|
||||
{
|
||||
return GetFromActionResult<StoreRateConfiguration>(await GetController<GreenfieldStoreRateConfigurationController>().UpdateStoreRateConfiguration(request));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,189 +0,0 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class UIAppsController
|
||||
{
|
||||
public class AppUpdated
|
||||
{
|
||||
public string AppId { get; set; }
|
||||
public object Settings { get; set; }
|
||||
public string StoreId { get; set; }
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[HttpGet("{appId}/settings/crowdfund")]
|
||||
public async Task<IActionResult> UpdateCrowdfund(string appId)
|
||||
{
|
||||
var app = GetCurrentApp();
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
|
||||
var settings = app.GetSettings<CrowdfundSettings>();
|
||||
var resetEvery = Enum.GetName(typeof(CrowdfundResetEvery), settings.ResetEvery);
|
||||
var vm = new UpdateCrowdfundViewModel
|
||||
{
|
||||
Title = settings.Title,
|
||||
StoreId = app.StoreDataId,
|
||||
StoreName = app.StoreData?.StoreName,
|
||||
StoreDefaultCurrency = await GetStoreDefaultCurrentIfEmpty(app.StoreDataId, settings.TargetCurrency),
|
||||
AppName = app.Name,
|
||||
Enabled = settings.Enabled,
|
||||
EnforceTargetAmount = settings.EnforceTargetAmount,
|
||||
StartDate = settings.StartDate,
|
||||
TargetCurrency = settings.TargetCurrency,
|
||||
Description = settings.Description,
|
||||
MainImageUrl = settings.MainImageUrl,
|
||||
EmbeddedCSS = settings.EmbeddedCSS,
|
||||
EndDate = settings.EndDate,
|
||||
TargetAmount = settings.TargetAmount,
|
||||
CustomCSSLink = settings.CustomCSSLink,
|
||||
NotificationUrl = settings.NotificationUrl,
|
||||
Tagline = settings.Tagline,
|
||||
PerksTemplate = settings.PerksTemplate,
|
||||
DisqusEnabled = settings.DisqusEnabled,
|
||||
SoundsEnabled = settings.SoundsEnabled,
|
||||
DisqusShortname = settings.DisqusShortname,
|
||||
AnimationsEnabled = settings.AnimationsEnabled,
|
||||
ResetEveryAmount = settings.ResetEveryAmount,
|
||||
ResetEvery = resetEvery,
|
||||
IsRecurring = resetEvery != nameof(CrowdfundResetEvery.Never),
|
||||
UseAllStoreInvoices = app.TagAllInvoices,
|
||||
AppId = appId,
|
||||
SearchTerm = app.TagAllInvoices ? $"storeid:{app.StoreDataId}" : $"orderid:{AppService.GetAppOrderId(app)}",
|
||||
DisplayPerksRanking = settings.DisplayPerksRanking,
|
||||
DisplayPerksValue = settings.DisplayPerksValue,
|
||||
SortPerksByPopularity = settings.SortPerksByPopularity,
|
||||
Sounds = string.Join(Environment.NewLine, settings.Sounds),
|
||||
AnimationColors = string.Join(Environment.NewLine, settings.AnimationColors)
|
||||
};
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost("{appId}/settings/crowdfund")]
|
||||
public async Task<IActionResult> UpdateCrowdfund(string appId, UpdateCrowdfundViewModel vm, string command)
|
||||
{
|
||||
var app = GetCurrentApp();
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
|
||||
vm.TargetCurrency = await GetStoreDefaultCurrentIfEmpty(app.StoreDataId, vm.TargetCurrency);
|
||||
if (_currencies.GetCurrencyData(vm.TargetCurrency, false) == null)
|
||||
ModelState.AddModelError(nameof(vm.TargetCurrency), "Invalid currency");
|
||||
|
||||
try
|
||||
{
|
||||
vm.PerksTemplate = _appService.SerializeTemplate(_appService.Parse(vm.PerksTemplate, vm.TargetCurrency));
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.PerksTemplate), "Invalid template");
|
||||
}
|
||||
if (vm.TargetAmount is decimal v && v == 0.0m)
|
||||
{
|
||||
vm.TargetAmount = null;
|
||||
}
|
||||
|
||||
if (!vm.IsRecurring)
|
||||
{
|
||||
vm.ResetEvery = nameof(CrowdfundResetEvery.Never);
|
||||
}
|
||||
|
||||
if (Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery) != CrowdfundResetEvery.Never && !vm.StartDate.HasValue)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.StartDate), "A start date is needed when the goal resets every X amount of time.");
|
||||
}
|
||||
|
||||
if (Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery) != CrowdfundResetEvery.Never && vm.ResetEveryAmount <= 0)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ResetEveryAmount), "You must reset the goal at a minimum of 1 ");
|
||||
}
|
||||
|
||||
if (vm.DisplayPerksRanking)
|
||||
{
|
||||
vm.SortPerksByPopularity = true;
|
||||
}
|
||||
|
||||
var parsedSounds = vm.Sounds?.Split(
|
||||
new[] { "\r\n", "\r", "\n" },
|
||||
StringSplitOptions.None
|
||||
).Select(s => s.Trim()).ToArray();
|
||||
if (vm.SoundsEnabled && (parsedSounds == null || !parsedSounds.Any()))
|
||||
{
|
||||
vm.SoundsEnabled = false;
|
||||
parsedSounds = new CrowdfundSettings().Sounds;
|
||||
}
|
||||
|
||||
var parsedAnimationColors = vm.AnimationColors?.Split(
|
||||
new[] { "\r\n", "\r", "\n" },
|
||||
StringSplitOptions.None
|
||||
).Select(s => s.Trim()).ToArray();
|
||||
if (vm.AnimationsEnabled && (parsedAnimationColors == null || !parsedAnimationColors.Any()))
|
||||
{
|
||||
vm.AnimationsEnabled = false;
|
||||
parsedAnimationColors = new CrowdfundSettings().AnimationColors;
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
app.Name = vm.AppName;
|
||||
var newSettings = new CrowdfundSettings
|
||||
{
|
||||
Title = vm.Title,
|
||||
Enabled = vm.Enabled,
|
||||
EnforceTargetAmount = vm.EnforceTargetAmount,
|
||||
StartDate = vm.StartDate?.ToUniversalTime(),
|
||||
TargetCurrency = vm.TargetCurrency,
|
||||
Description = vm.Description,
|
||||
EndDate = vm.EndDate?.ToUniversalTime(),
|
||||
TargetAmount = vm.TargetAmount,
|
||||
CustomCSSLink = vm.CustomCSSLink,
|
||||
MainImageUrl = vm.MainImageUrl,
|
||||
EmbeddedCSS = vm.EmbeddedCSS,
|
||||
NotificationUrl = vm.NotificationUrl,
|
||||
Tagline = vm.Tagline,
|
||||
PerksTemplate = vm.PerksTemplate,
|
||||
DisqusEnabled = vm.DisqusEnabled,
|
||||
SoundsEnabled = vm.SoundsEnabled,
|
||||
DisqusShortname = vm.DisqusShortname,
|
||||
AnimationsEnabled = vm.AnimationsEnabled,
|
||||
ResetEveryAmount = vm.ResetEveryAmount,
|
||||
ResetEvery = Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery),
|
||||
DisplayPerksValue = vm.DisplayPerksValue,
|
||||
DisplayPerksRanking = vm.DisplayPerksRanking,
|
||||
SortPerksByPopularity = vm.SortPerksByPopularity,
|
||||
Sounds = parsedSounds,
|
||||
AnimationColors = parsedAnimationColors
|
||||
};
|
||||
|
||||
app.TagAllInvoices = vm.UseAllStoreInvoices;
|
||||
app.SetSettings(newSettings);
|
||||
|
||||
await _appService.UpdateOrCreateApp(app);
|
||||
|
||||
_eventAggregator.Publish(new AppUpdated()
|
||||
{
|
||||
AppId = appId,
|
||||
StoreId = app.StoreDataId,
|
||||
Settings = newSettings
|
||||
});
|
||||
TempData[WellKnownTempData.SuccessMessage] = "App updated";
|
||||
return RedirectToAction(nameof(UpdateCrowdfund), new { appId });
|
||||
}
|
||||
}
|
||||
}
|
@ -1,13 +1,17 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Components.AppSales;
|
||||
using BTCPayServer.Components.AppTopItems;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class UIAppsController
|
||||
{
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[HttpGet("{appId}/dashboard/app-top-items")]
|
||||
public IActionResult AppTopItems(string appId)
|
||||
{
|
||||
@ -21,6 +25,7 @@ namespace BTCPayServer.Controllers
|
||||
return ViewComponent("AppTopItems", new { vm });
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[HttpGet("{appId}/dashboard/app-sales")]
|
||||
public IActionResult AppSales(string appId)
|
||||
{
|
||||
@ -34,6 +39,7 @@ namespace BTCPayServer.Controllers
|
||||
return ViewComponent("AppSales", new { vm });
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[HttpGet("{appId}/dashboard/app-sales/{period}")]
|
||||
public async Task<IActionResult> AppSales(string appId, AppSalesPeriod period)
|
||||
{
|
||||
|
@ -1,161 +0,0 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class UIAppsController
|
||||
{
|
||||
[HttpGet("{appId}/settings/pos")]
|
||||
public async Task<IActionResult> UpdatePointOfSale(string appId)
|
||||
{
|
||||
var app = GetCurrentApp();
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
settings.DefaultView = settings.EnableShoppingCart ? PosViewType.Cart : settings.DefaultView;
|
||||
settings.EnableShoppingCart = false;
|
||||
|
||||
var vm = new UpdatePointOfSaleViewModel
|
||||
{
|
||||
Id = appId,
|
||||
StoreId = app.StoreDataId,
|
||||
StoreName = app.StoreData?.StoreName,
|
||||
StoreDefaultCurrency = await GetStoreDefaultCurrentIfEmpty(app.StoreDataId, settings.Currency),
|
||||
AppName = app.Name,
|
||||
Title = settings.Title,
|
||||
DefaultView = settings.DefaultView,
|
||||
ShowCustomAmount = settings.ShowCustomAmount,
|
||||
ShowDiscount = settings.ShowDiscount,
|
||||
EnableTips = settings.EnableTips,
|
||||
Currency = settings.Currency,
|
||||
Template = settings.Template,
|
||||
ButtonText = settings.ButtonText ?? PointOfSaleSettings.BUTTON_TEXT_DEF,
|
||||
CustomButtonText = settings.CustomButtonText ?? PointOfSaleSettings.CUSTOM_BUTTON_TEXT_DEF,
|
||||
CustomTipText = settings.CustomTipText ?? PointOfSaleSettings.CUSTOM_TIP_TEXT_DEF,
|
||||
CustomTipPercentages = settings.CustomTipPercentages != null ? string.Join(",", settings.CustomTipPercentages) : string.Join(",", PointOfSaleSettings.CUSTOM_TIP_PERCENTAGES_DEF),
|
||||
CustomCSSLink = settings.CustomCSSLink,
|
||||
EmbeddedCSS = settings.EmbeddedCSS,
|
||||
Description = settings.Description,
|
||||
NotificationUrl = settings.NotificationUrl,
|
||||
RedirectUrl = settings.RedirectUrl,
|
||||
SearchTerm = app.TagAllInvoices ? $"storeid:{app.StoreDataId}" : $"orderid:{AppService.GetAppOrderId(app)}",
|
||||
RedirectAutomatically = settings.RedirectAutomatically.HasValue ? settings.RedirectAutomatically.Value ? "true" : "false" : "",
|
||||
RequiresRefundEmail = settings.RequiresRefundEmail
|
||||
};
|
||||
if (HttpContext?.Request != null)
|
||||
{
|
||||
var appUrl = HttpContext.Request.GetAbsoluteUri($"/apps/{appId}/pos");
|
||||
var encoder = HtmlEncoder.Default;
|
||||
if (settings.ShowCustomAmount)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.AppendLine(CultureInfo.InvariantCulture, $"<form method=\"POST\" action=\"{encoder.Encode(appUrl)}\">");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"amount\" value=\"100\" />");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"email\" value=\"customer@example.com\" />");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"orderId\" value=\"CustomOrderId\" />");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"notificationUrl\" value=\"https://example.com/callbacks\" />");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"redirectUrl\" value=\"https://example.com/thanksyou\" />");
|
||||
builder.AppendLine($" <button type=\"submit\">Buy now</button>");
|
||||
builder.AppendLine($"</form>");
|
||||
vm.Example1 = builder.ToString();
|
||||
}
|
||||
try
|
||||
{
|
||||
var items = _appService.Parse(settings.Template, settings.Currency);
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine(CultureInfo.InvariantCulture, $"<form method=\"POST\" action=\"{encoder.Encode(appUrl)}\">");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"email\" value=\"customer@example.com\" />");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"orderId\" value=\"CustomOrderId\" />");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"notificationUrl\" value=\"https://example.com/callbacks\" />");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"redirectUrl\" value=\"https://example.com/thanksyou\" />");
|
||||
builder.AppendLine(CultureInfo.InvariantCulture, $" <button type=\"submit\" name=\"choiceKey\" value=\"{items[0].Id}\">Buy now</button>");
|
||||
builder.AppendLine($"</form>");
|
||||
vm.Example2 = builder.ToString();
|
||||
}
|
||||
catch { }
|
||||
vm.InvoiceUrl = appUrl + "invoices/SkdsDghkdP3D3qkj7bLq3";
|
||||
}
|
||||
|
||||
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}";
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost("{appId}/settings/pos")]
|
||||
public async Task<IActionResult> UpdatePointOfSale(string appId, UpdatePointOfSaleViewModel vm)
|
||||
{
|
||||
var app = GetCurrentApp();
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
return View(vm);
|
||||
|
||||
vm.Currency = await GetStoreDefaultCurrentIfEmpty(app.StoreDataId, vm.Currency);
|
||||
if (_currencies.GetCurrencyData(vm.Currency, false) == null)
|
||||
ModelState.AddModelError(nameof(vm.Currency), "Invalid currency");
|
||||
try
|
||||
{
|
||||
vm.Template = _appService.SerializeTemplate(_appService.Parse(vm.Template, vm.Currency));
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Template), "Invalid template");
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
app.Name = vm.AppName;
|
||||
app.SetSettings(new PointOfSaleSettings
|
||||
{
|
||||
Title = vm.Title,
|
||||
DefaultView = vm.DefaultView,
|
||||
ShowCustomAmount = vm.ShowCustomAmount,
|
||||
ShowDiscount = vm.ShowDiscount,
|
||||
EnableTips = vm.EnableTips,
|
||||
Currency = vm.Currency,
|
||||
Template = vm.Template,
|
||||
ButtonText = vm.ButtonText,
|
||||
CustomButtonText = vm.CustomButtonText,
|
||||
CustomTipText = vm.CustomTipText,
|
||||
CustomTipPercentages = ListSplit(vm.CustomTipPercentages),
|
||||
CustomCSSLink = vm.CustomCSSLink,
|
||||
NotificationUrl = vm.NotificationUrl,
|
||||
RedirectUrl = vm.RedirectUrl,
|
||||
Description = vm.Description,
|
||||
EmbeddedCSS = vm.EmbeddedCSS,
|
||||
RedirectAutomatically = string.IsNullOrEmpty(vm.RedirectAutomatically) ? (bool?)null : bool.Parse(vm.RedirectAutomatically),
|
||||
RequiresRefundEmail = vm.RequiresRefundEmail,
|
||||
});
|
||||
await _appService.UpdateOrCreateApp(app);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "App updated";
|
||||
return RedirectToAction(nameof(UpdatePointOfSale), new { appId });
|
||||
}
|
||||
|
||||
private int[] ListSplit(string list, string separator = ",")
|
||||
{
|
||||
if (string.IsNullOrEmpty(list))
|
||||
{
|
||||
return Array.Empty<int>();
|
||||
}
|
||||
|
||||
// Remove all characters except numeric and comma
|
||||
Regex charsToDestroy = new Regex(@"[^\d|\" + separator + "]");
|
||||
list = charsToDestroy.Replace(list, "");
|
||||
|
||||
return list.Split(separator, StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
@ -5,10 +5,10 @@ using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Plugins.Crowdfund.Controllers;
|
||||
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
@ -16,33 +16,53 @@ using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
[Route("apps")]
|
||||
public partial class UIAppsController : Controller
|
||||
{
|
||||
public UIAppsController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
EventAggregator eventAggregator,
|
||||
CurrencyNameTable currencies,
|
||||
StoreRepository storeRepository,
|
||||
AppService appService)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_eventAggregator = eventAggregator;
|
||||
_currencies = currencies;
|
||||
_storeRepository = storeRepository;
|
||||
_appService = appService;
|
||||
}
|
||||
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
private readonly CurrencyNameTable _currencies;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly AppService _appService;
|
||||
|
||||
public string CreatedAppId { get; set; }
|
||||
|
||||
public class AppUpdated
|
||||
{
|
||||
public string AppId { get; set; }
|
||||
public object Settings { get; set; }
|
||||
public string StoreId { get; set; }
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("/apps/{appId}")]
|
||||
public async Task<IActionResult> RedirectToApp(string appId)
|
||||
{
|
||||
var app = await _appService.GetApp(appId, null);
|
||||
if (app is null)
|
||||
return NotFound();
|
||||
|
||||
return app.AppType switch
|
||||
{
|
||||
nameof(AppType.Crowdfund) => RedirectToAction(nameof(UICrowdfundController.ViewCrowdfund), "UICrowdfund", new { appId }),
|
||||
nameof(AppType.PointOfSale) => RedirectToAction(nameof(UIPointOfSaleController.ViewPointOfSale), "UIPointOfSale", new { appId }),
|
||||
_ => NotFound()
|
||||
};
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[HttpGet("/stores/{storeId}/apps")]
|
||||
public async Task<IActionResult> ListApps(
|
||||
string storeId,
|
||||
@ -88,6 +108,7 @@ namespace BTCPayServer.Controllers
|
||||
});
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[HttpGet("/stores/{storeId}/apps/create")]
|
||||
public IActionResult CreateApp(string storeId)
|
||||
{
|
||||
@ -97,6 +118,7 @@ namespace BTCPayServer.Controllers
|
||||
});
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[HttpPost("/stores/{storeId}/apps/create")]
|
||||
public async Task<IActionResult> CreateApp(string storeId, CreateAppViewModel vm)
|
||||
{
|
||||
@ -139,12 +161,13 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
return appType switch
|
||||
{
|
||||
AppType.PointOfSale => RedirectToAction(nameof(UpdatePointOfSale), new { appId = appData.Id }),
|
||||
AppType.Crowdfund => RedirectToAction(nameof(UpdateCrowdfund), new { appId = appData.Id }),
|
||||
AppType.PointOfSale => RedirectToAction(nameof(UIPointOfSaleController.UpdatePointOfSale), "UIPointOfSale", new { appId = appData.Id }),
|
||||
AppType.Crowdfund => RedirectToAction(nameof(UICrowdfundController.UpdateCrowdfund), "UICrowdfund", new { appId = appData.Id }),
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[HttpGet("{appId}/delete")]
|
||||
public IActionResult DeleteApp(string appId)
|
||||
{
|
||||
@ -155,6 +178,7 @@ namespace BTCPayServer.Controllers
|
||||
return View("Confirm", new ConfirmModel("Delete app", $"The app <strong>{app.Name}</strong> and its settings will be permanently deleted. Are you sure?", "Delete"));
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[HttpPost("{appId}/delete")]
|
||||
public async Task<IActionResult> DeleteAppPost(string appId)
|
||||
{
|
||||
|
@ -1,428 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.ModelBinders;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitpayClient;
|
||||
using NicolasDorier.RateLimits;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class UIAppsPublicController : Controller
|
||||
{
|
||||
public UIAppsPublicController(AppService appService,
|
||||
BTCPayServerOptions btcPayServerOptions,
|
||||
UIInvoiceController invoiceController,
|
||||
UserManager<ApplicationUser> userManager)
|
||||
{
|
||||
_AppService = appService;
|
||||
_BtcPayServerOptions = btcPayServerOptions;
|
||||
_InvoiceController = invoiceController;
|
||||
_UserManager = userManager;
|
||||
}
|
||||
|
||||
private readonly AppService _AppService;
|
||||
private readonly BTCPayServerOptions _BtcPayServerOptions;
|
||||
private readonly UIInvoiceController _InvoiceController;
|
||||
private readonly UserManager<ApplicationUser> _UserManager;
|
||||
|
||||
[HttpGet("/apps/{appId}")]
|
||||
public async Task<IActionResult> RedirectToApp(string appId)
|
||||
{
|
||||
var app = await _AppService.GetApp(appId, null);
|
||||
if (app is null)
|
||||
return NotFound();
|
||||
switch (app.AppType)
|
||||
{
|
||||
case nameof(AppType.Crowdfund):
|
||||
return RedirectToAction("ViewCrowdfund", new { appId });
|
||||
|
||||
case nameof(AppType.PointOfSale):
|
||||
return RedirectToAction("ViewPointOfSale", new { appId });
|
||||
}
|
||||
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("/")]
|
||||
[Route("/apps/{appId}/pos/{viewType?}")]
|
||||
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
|
||||
[DomainMappingConstraint(AppType.PointOfSale)]
|
||||
public async Task<IActionResult> ViewPointOfSale(string appId, PosViewType? viewType = null)
|
||||
{
|
||||
var app = await _AppService.GetApp(appId, AppType.PointOfSale);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
var numberFormatInfo = _AppService.Currencies.GetNumberFormatInfo(settings.Currency) ?? _AppService.Currencies.GetNumberFormatInfo("USD");
|
||||
double step = Math.Pow(10, -(numberFormatInfo.CurrencyDecimalDigits));
|
||||
viewType ??= settings.EnableShoppingCart ? PosViewType.Cart : settings.DefaultView;
|
||||
var store = await _AppService.GetStore(app);
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
|
||||
return View("PointOfSale/" + viewType, new ViewPointOfSaleViewModel()
|
||||
{
|
||||
Title = settings.Title,
|
||||
Step = step.ToString(CultureInfo.InvariantCulture),
|
||||
ViewType = (PosViewType)viewType,
|
||||
ShowCustomAmount = settings.ShowCustomAmount,
|
||||
ShowDiscount = settings.ShowDiscount,
|
||||
EnableTips = settings.EnableTips,
|
||||
CurrencyCode = settings.Currency,
|
||||
CurrencySymbol = numberFormatInfo.CurrencySymbol,
|
||||
CurrencyInfo = new ViewPointOfSaleViewModel.CurrencyInfoData()
|
||||
{
|
||||
CurrencySymbol = string.IsNullOrEmpty(numberFormatInfo.CurrencySymbol) ? settings.Currency : numberFormatInfo.CurrencySymbol,
|
||||
Divisibility = numberFormatInfo.CurrencyDecimalDigits,
|
||||
DecimalSeparator = numberFormatInfo.CurrencyDecimalSeparator,
|
||||
ThousandSeparator = numberFormatInfo.NumberGroupSeparator,
|
||||
Prefixed = new[] { 0, 2 }.Contains(numberFormatInfo.CurrencyPositivePattern),
|
||||
SymbolSpace = new[] { 2, 3 }.Contains(numberFormatInfo.CurrencyPositivePattern)
|
||||
},
|
||||
Items = _AppService.GetPOSItems(settings.Template, settings.Currency),
|
||||
ButtonText = settings.ButtonText,
|
||||
CustomButtonText = settings.CustomButtonText,
|
||||
CustomTipText = settings.CustomTipText,
|
||||
CustomTipPercentages = settings.CustomTipPercentages,
|
||||
CustomCSSLink = settings.CustomCSSLink,
|
||||
CustomLogoLink = storeBlob.CustomLogo,
|
||||
AppId = appId,
|
||||
StoreId = store.Id,
|
||||
Description = settings.Description,
|
||||
EmbeddedCSS = settings.EmbeddedCSS,
|
||||
RequiresRefundEmail = settings.RequiresRefundEmail
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("/")]
|
||||
[Route("/apps/{appId}/pos/{viewType?}")]
|
||||
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
|
||||
[IgnoreAntiforgeryToken]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
[DomainMappingConstraint(AppType.PointOfSale)]
|
||||
[RateLimitsFilter(ZoneLimits.PublicInvoices, Scope = RateLimitsScope.RemoteAddress)]
|
||||
public async Task<IActionResult> ViewPointOfSale(string appId,
|
||||
PosViewType viewType,
|
||||
[ModelBinder(typeof(InvariantDecimalModelBinder))] decimal? amount,
|
||||
string email,
|
||||
string orderId,
|
||||
string notificationUrl,
|
||||
string redirectUrl,
|
||||
string choiceKey,
|
||||
string posData = null,
|
||||
RequiresRefundEmail requiresRefundEmail = RequiresRefundEmail.InheritFromStore,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var app = await _AppService.GetApp(appId, AppType.PointOfSale);
|
||||
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
|
||||
{
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
|
||||
}
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
settings.DefaultView = settings.EnableShoppingCart ? PosViewType.Cart : settings.DefaultView;
|
||||
if (string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount && settings.DefaultView != PosViewType.Cart)
|
||||
{
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId, viewType = viewType });
|
||||
}
|
||||
string title = null;
|
||||
decimal? price = null;
|
||||
Dictionary<string, InvoiceSupportedTransactionCurrency> paymentMethods = null;
|
||||
ViewPointOfSaleViewModel.Item choice = null;
|
||||
if (!string.IsNullOrEmpty(choiceKey))
|
||||
{
|
||||
var choices = _AppService.GetPOSItems(settings.Template, settings.Currency);
|
||||
choice = choices.FirstOrDefault(c => c.Id == choiceKey);
|
||||
if (choice == null)
|
||||
return NotFound();
|
||||
title = choice.Title;
|
||||
if (choice.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup)
|
||||
{
|
||||
price = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
price = choice.Price.Value;
|
||||
if (amount > price)
|
||||
price = amount;
|
||||
}
|
||||
|
||||
|
||||
if (choice.Inventory.HasValue)
|
||||
{
|
||||
if (choice.Inventory <= 0)
|
||||
{
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
|
||||
}
|
||||
}
|
||||
|
||||
if (choice?.PaymentMethods?.Any() is true)
|
||||
{
|
||||
paymentMethods = choice?.PaymentMethods.ToDictionary(s => s,
|
||||
s => new InvoiceSupportedTransactionCurrency() { Enabled = true });
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!settings.ShowCustomAmount && settings.DefaultView != PosViewType.Cart)
|
||||
return NotFound();
|
||||
price = amount;
|
||||
title = settings.Title;
|
||||
|
||||
//if cart IS enabled and we detect posdata that matches the cart system's, check inventory for the items
|
||||
if (!string.IsNullOrEmpty(posData) &&
|
||||
settings.DefaultView == PosViewType.Cart &&
|
||||
AppService.TryParsePosCartItems(posData, out var cartItems))
|
||||
{
|
||||
|
||||
var choices = _AppService.GetPOSItems(settings.Template, settings.Currency);
|
||||
foreach (var cartItem in cartItems)
|
||||
{
|
||||
var itemChoice = choices.FirstOrDefault(c => c.Id == cartItem.Key);
|
||||
if (itemChoice == null)
|
||||
return NotFound();
|
||||
|
||||
if (itemChoice.Inventory.HasValue)
|
||||
{
|
||||
switch (itemChoice.Inventory)
|
||||
{
|
||||
case int i when i <= 0:
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId });
|
||||
case int inventory when inventory < cartItem.Value:
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var store = await _AppService.GetStore(app);
|
||||
try
|
||||
{
|
||||
var invoice = await _InvoiceController.CreateInvoiceCore(new BitpayCreateInvoiceRequest()
|
||||
{
|
||||
ItemCode = choice?.Id,
|
||||
ItemDesc = title,
|
||||
Currency = settings.Currency,
|
||||
Price = price,
|
||||
BuyerEmail = email,
|
||||
OrderId = orderId ?? AppService.GetAppOrderId(app),
|
||||
NotificationURL =
|
||||
string.IsNullOrEmpty(notificationUrl) ? settings.NotificationUrl : notificationUrl,
|
||||
RedirectURL = !string.IsNullOrEmpty(redirectUrl) ? redirectUrl
|
||||
: !string.IsNullOrEmpty(settings.RedirectUrl) ? settings.RedirectUrl
|
||||
: Request.GetDisplayUrl(),
|
||||
FullNotifications = true,
|
||||
ExtendedNotifications = true,
|
||||
PosData = string.IsNullOrEmpty(posData) ? null : posData,
|
||||
RedirectAutomatically = settings.RedirectAutomatically,
|
||||
SupportedTransactionCurrencies = paymentMethods,
|
||||
RequiresRefundEmail = requiresRefundEmail == RequiresRefundEmail.InheritFromStore
|
||||
? store.GetStoreBlob().RequiresRefundEmail
|
||||
: requiresRefundEmail == RequiresRefundEmail.On,
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot(),
|
||||
new List<string>() { AppService.GetAppInternalTag(appId) },
|
||||
cancellationToken, (entity) =>
|
||||
{
|
||||
entity.Metadata.OrderUrl = Request.GetDisplayUrl();
|
||||
} );
|
||||
return RedirectToAction(nameof(UIInvoiceController.Checkout), "UIInvoice", new { invoiceId = invoice.Data.Id });
|
||||
}
|
||||
catch (BitpayHttpException e)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Html = e.Message.Replace("\n", "<br />", StringComparison.OrdinalIgnoreCase),
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
AllowDismiss = true
|
||||
});
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("/")]
|
||||
[Route("/apps/{appId}/crowdfund")]
|
||||
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
|
||||
[DomainMappingConstraintAttribute(AppType.Crowdfund)]
|
||||
public async Task<IActionResult> ViewCrowdfund(string appId, string statusMessage)
|
||||
{
|
||||
var app = await _AppService.GetApp(appId, AppType.Crowdfund, true);
|
||||
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<CrowdfundSettings>();
|
||||
|
||||
var isAdmin = await _AppService.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
|
||||
|
||||
var hasEnoughSettingsToLoad = !string.IsNullOrEmpty(settings.TargetCurrency);
|
||||
if (!hasEnoughSettingsToLoad)
|
||||
{
|
||||
if (!isAdmin)
|
||||
return NotFound();
|
||||
|
||||
return NotFound("A Target Currency must be set for this app in order to be loadable.");
|
||||
}
|
||||
var appInfo = await GetAppInfo(appId);
|
||||
|
||||
if (settings.Enabled)
|
||||
return View(appInfo);
|
||||
if (!isAdmin)
|
||||
return NotFound();
|
||||
|
||||
return View(appInfo);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("/")]
|
||||
[Route("/apps/{appId}/crowdfund")]
|
||||
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
|
||||
[IgnoreAntiforgeryToken]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
[DomainMappingConstraintAttribute(AppType.Crowdfund)]
|
||||
[RateLimitsFilter(ZoneLimits.PublicInvoices, Scope = RateLimitsScope.RemoteAddress)]
|
||||
public async Task<IActionResult> ContributeToCrowdfund(string appId, ContributeToCrowdfund request, CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
var app = await _AppService.GetApp(appId, AppType.Crowdfund, true);
|
||||
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<CrowdfundSettings>();
|
||||
|
||||
var isAdmin = await _AppService.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
|
||||
|
||||
if (!settings.Enabled && !isAdmin)
|
||||
{
|
||||
return NotFound("Crowdfund is not currently active");
|
||||
}
|
||||
|
||||
var info = await GetAppInfo(appId);
|
||||
if (!isAdmin &&
|
||||
((settings.StartDate.HasValue && DateTime.UtcNow < settings.StartDate) ||
|
||||
(settings.EndDate.HasValue && DateTime.UtcNow > settings.EndDate) ||
|
||||
(settings.EnforceTargetAmount &&
|
||||
(info.Info.PendingProgressPercentage.GetValueOrDefault(0) +
|
||||
info.Info.ProgressPercentage.GetValueOrDefault(0)) >= 100)))
|
||||
{
|
||||
return NotFound("Crowdfund is not currently active");
|
||||
}
|
||||
|
||||
var store = await _AppService.GetStore(app);
|
||||
var title = settings.Title;
|
||||
decimal? price = request.Amount;
|
||||
Dictionary<string, InvoiceSupportedTransactionCurrency> paymentMethods = null;
|
||||
ViewPointOfSaleViewModel.Item choice = null;
|
||||
if (!string.IsNullOrEmpty(request.ChoiceKey))
|
||||
{
|
||||
var choices = _AppService.GetPOSItems(settings.PerksTemplate, settings.TargetCurrency);
|
||||
choice = choices.FirstOrDefault(c => c.Id == request.ChoiceKey);
|
||||
if (choice == null)
|
||||
return NotFound("Incorrect option provided");
|
||||
title = choice.Title;
|
||||
|
||||
if (choice.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup)
|
||||
{
|
||||
price = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
price = choice.Price.Value;
|
||||
if (request.Amount > price)
|
||||
price = request.Amount;
|
||||
}
|
||||
if (choice.Inventory.HasValue)
|
||||
{
|
||||
if (choice.Inventory <= 0)
|
||||
{
|
||||
return NotFound("Option was out of stock");
|
||||
}
|
||||
}
|
||||
if (choice?.PaymentMethods?.Any() is true)
|
||||
{
|
||||
paymentMethods = choice?.PaymentMethods.ToDictionary(s => s,
|
||||
s => new InvoiceSupportedTransactionCurrency() { Enabled = true });
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (request.Amount < 0)
|
||||
{
|
||||
return NotFound("Please provide an amount greater than 0");
|
||||
}
|
||||
|
||||
price = request.Amount;
|
||||
}
|
||||
|
||||
if (!isAdmin && (settings.EnforceTargetAmount && info.TargetAmount.HasValue && price >
|
||||
(info.TargetAmount - (info.Info.CurrentAmount + info.Info.CurrentPendingAmount))))
|
||||
{
|
||||
return NotFound("Contribution Amount is more than is currently allowed.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var invoice = await _InvoiceController.CreateInvoiceCore(new BitpayCreateInvoiceRequest()
|
||||
{
|
||||
OrderId = AppService.GetAppOrderId(app),
|
||||
Currency = settings.TargetCurrency,
|
||||
ItemCode = request.ChoiceKey ?? string.Empty,
|
||||
ItemDesc = title,
|
||||
BuyerEmail = request.Email,
|
||||
Price = price,
|
||||
NotificationURL = settings.NotificationUrl,
|
||||
FullNotifications = true,
|
||||
ExtendedNotifications = true,
|
||||
SupportedTransactionCurrencies = paymentMethods,
|
||||
RedirectURL = request.RedirectUrl ?? Request.GetDisplayUrl(),
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot(),
|
||||
new List<string>() {AppService.GetAppInternalTag(appId)},
|
||||
cancellationToken, (entity) =>
|
||||
{
|
||||
entity.Metadata.OrderUrl = Request.GetDisplayUrl();
|
||||
});
|
||||
|
||||
if (request.RedirectToCheckout)
|
||||
{
|
||||
return RedirectToAction(nameof(UIInvoiceController.Checkout), "UIInvoice",
|
||||
new {invoiceId = invoice.Data.Id});
|
||||
}
|
||||
|
||||
return Ok(invoice.Data.Id);
|
||||
}
|
||||
catch (BitpayHttpException e)
|
||||
{
|
||||
return BadRequest(e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ViewCrowdfundViewModel> GetAppInfo(string appId)
|
||||
{
|
||||
var info = (ViewCrowdfundViewModel)await _AppService.GetAppInfo(appId);
|
||||
info.HubPath = AppHub.GetHubPath(Request);
|
||||
info.SimpleDisplay = Request.Query.ContainsKey("simple");
|
||||
return info;
|
||||
}
|
||||
|
||||
private string GetUserId()
|
||||
{
|
||||
return _UserManager.GetUserId(User);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
@ -24,34 +25,35 @@ namespace BTCPayServer.Controllers
|
||||
public string CryptoCode { get; set; } = "BTC";
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("i/{invoiceId}/test-payment")]
|
||||
[HttpPost("i/{invoiceId}/test-payment")]
|
||||
[CheatModeRoute]
|
||||
public async Task<IActionResult> TestPayment(string invoiceId, FakePaymentRequest request, [FromServices] Cheater cheater)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
|
||||
var store = await _StoreRepository.FindStore(invoice.StoreId);
|
||||
|
||||
// TODO support altcoins, not just bitcoin
|
||||
var network = _NetworkProvider.GetNetwork<BTCPayNetwork>(request.CryptoCode);
|
||||
// TODO support altcoins, not just bitcoin - and make it work for LN-only invoices
|
||||
var isSats = request.CryptoCode.ToUpper(CultureInfo.InvariantCulture) == "SATS";
|
||||
var cryptoCode = isSats ? "BTC" : request.CryptoCode;
|
||||
var network = _NetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
||||
var paymentMethodId = new [] {store.GetDefaultPaymentId()}.Concat(store.GetEnabledPaymentIds(_NetworkProvider))
|
||||
.FirstOrDefault(p => p!= null && p.CryptoCode == request.CryptoCode && p.PaymentType == PaymentTypes.BTCLike);
|
||||
.FirstOrDefault(p => p != null && p.CryptoCode == cryptoCode && p.PaymentType == PaymentTypes.BTCLike);
|
||||
var bitcoinAddressString = invoice.GetPaymentMethod(paymentMethodId).GetPaymentMethodDetails().GetPaymentDestination();
|
||||
var bitcoinAddressObj = BitcoinAddress.Create(bitcoinAddressString, network.NBitcoinNetwork);
|
||||
var BtcAmount = request.Amount;
|
||||
var amount = new Money(request.Amount, isSats ? MoneyUnit.Satoshi : MoneyUnit.BTC);
|
||||
|
||||
try
|
||||
{
|
||||
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId);
|
||||
var rate = paymentMethod.Rate;
|
||||
var txid = cheater.CashCow.SendToAddress(bitcoinAddressObj, new Money(BtcAmount, MoneyUnit.BTC)).ToString();
|
||||
var txid = (await cheater.CashCow.SendToAddressAsync(bitcoinAddressObj, amount)).ToString();
|
||||
|
||||
// TODO The value of totalDue is wrong. How can we get the real total due? invoice.Price is only correct if this is the 2nd payment, not for a 3rd or 4th payment.
|
||||
var totalDue = invoice.Price;
|
||||
return Ok(new
|
||||
{
|
||||
Txid = txid,
|
||||
AmountRemaining = (totalDue - (BtcAmount * rate)) / rate,
|
||||
AmountRemaining = (totalDue - (amount.ToUnit(MoneyUnit.BTC) * rate)) / rate,
|
||||
SuccessMessage = "Created transaction " + txid
|
||||
});
|
||||
}
|
||||
@ -65,8 +67,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("i/{invoiceId}/mine-blocks")]
|
||||
[HttpPost("i/{invoiceId}/mine-blocks")]
|
||||
[CheatModeRoute]
|
||||
public IActionResult MineBlock(string invoiceId, MineBlocksRequest request, [FromServices] Cheater cheater)
|
||||
{
|
||||
@ -96,8 +97,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("i/{invoiceId}/expire")]
|
||||
[HttpPost("i/{invoiceId}/expire")]
|
||||
[CheatModeRoute]
|
||||
public async Task<IActionResult> TestExpireNow(string invoiceId, [FromServices] Cheater cheater)
|
||||
{
|
||||
|
@ -24,6 +24,7 @@ using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Invoices.Export;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
@ -147,6 +148,7 @@ namespace BTCPayServer.Controllers
|
||||
var details = InvoicePopulatePayments(invoice);
|
||||
model.CryptoPayments = details.CryptoPayments;
|
||||
model.Payments = details.Payments;
|
||||
model.Overpaid = details.Overpaid;
|
||||
|
||||
return View(model);
|
||||
}
|
||||
@ -176,7 +178,7 @@ namespace BTCPayServer.Controllers
|
||||
});
|
||||
}
|
||||
JToken? receiptData = null;
|
||||
i.Metadata?.AdditionalData.TryGetValue("receiptData", out receiptData);
|
||||
i.Metadata?.AdditionalData?.TryGetValue("receiptData", out receiptData);
|
||||
|
||||
return View(new InvoiceReceiptViewModel
|
||||
{
|
||||
@ -467,15 +469,25 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
private InvoiceDetailsModel InvoicePopulatePayments(InvoiceEntity invoice)
|
||||
{
|
||||
return new InvoiceDetailsModel
|
||||
|
||||
var overpaid = false;
|
||||
var model = new InvoiceDetailsModel
|
||||
{
|
||||
Archived = invoice.Archived,
|
||||
Payments = invoice.GetPayments(false),
|
||||
Overpaid = true,
|
||||
CryptoPayments = invoice.GetPaymentMethods().Select(
|
||||
data =>
|
||||
{
|
||||
var accounting = data.Calculate();
|
||||
var paymentMethodId = data.GetId();
|
||||
var overpaidAmount = accounting.OverpaidHelper.ToDecimal(MoneyUnit.BTC);
|
||||
|
||||
if (overpaidAmount > 0)
|
||||
{
|
||||
overpaid = true;
|
||||
}
|
||||
|
||||
return new InvoiceDetailsModel.CryptoPayment
|
||||
{
|
||||
PaymentMethodId = paymentMethodId,
|
||||
@ -486,13 +498,16 @@ namespace BTCPayServer.Controllers
|
||||
accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC),
|
||||
paymentMethodId.CryptoCode),
|
||||
Overpaid = _CurrencyNameTable.DisplayFormatCurrency(
|
||||
accounting.OverpaidHelper.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode),
|
||||
overpaidAmount, paymentMethodId.CryptoCode),
|
||||
Address = data.GetPaymentMethodDetails().GetPaymentDestination(),
|
||||
Rate = ExchangeRate(data),
|
||||
PaymentMethodRaw = data
|
||||
};
|
||||
}).ToList()
|
||||
};
|
||||
model.Overpaid = overpaid;
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
[HttpPost("invoices/{invoiceId}/archive")]
|
||||
@ -589,23 +604,26 @@ namespace BTCPayServer.Controllers
|
||||
[HttpGet("i/{invoiceId}/{paymentMethodId}")]
|
||||
[HttpGet("invoice")]
|
||||
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest", false)]
|
||||
[XFrameOptionsAttribute(null)]
|
||||
[ReferrerPolicyAttribute("origin")]
|
||||
[XFrameOptions(null)]
|
||||
[ReferrerPolicy("origin")]
|
||||
public async Task<IActionResult> Checkout(string? invoiceId, string? id = null, string? paymentMethodId = null,
|
||||
[FromQuery] string? view = null, [FromQuery] string? lang = null)
|
||||
{
|
||||
//Keep compatibility with Bitpay
|
||||
invoiceId = invoiceId ?? id;
|
||||
//
|
||||
// Keep compatibility with Bitpay
|
||||
invoiceId ??= id;
|
||||
|
||||
if (invoiceId is null)
|
||||
return NotFound();
|
||||
|
||||
var model = await GetInvoiceModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId), lang);
|
||||
if (model == null)
|
||||
return NotFound();
|
||||
|
||||
if (view == "modal")
|
||||
model.IsModal = true;
|
||||
return View(nameof(Checkout), model);
|
||||
|
||||
var viewName = model.CheckoutType == CheckoutType.V2 ? "CheckoutV2" : nameof(Checkout);
|
||||
return View(viewName, model);
|
||||
}
|
||||
|
||||
[HttpGet("invoice-noscript")]
|
||||
@ -717,15 +735,18 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var receiptEnabled = InvoiceDataBase.ReceiptOptions.Merge(storeBlob.ReceiptOptions, invoice.ReceiptOptions).Enabled is true;
|
||||
var receiptUrl = receiptEnabled? _linkGenerator.GetUriByAction(
|
||||
nameof(UIInvoiceController.InvoiceReceipt),
|
||||
nameof(InvoiceReceipt),
|
||||
"UIInvoice",
|
||||
new {invoiceId},
|
||||
Request.Scheme,
|
||||
Request.Host,
|
||||
Request.PathBase) : null;
|
||||
|
||||
|
||||
var model = new PaymentModel
|
||||
{
|
||||
#if ALTCOINS
|
||||
AltcoinsBuild = true,
|
||||
#endif
|
||||
Activated = paymentMethodDetails.Activated,
|
||||
CryptoCode = network.CryptoCode,
|
||||
RootPath = Request.PathBase.Value.WithTrailingSlash(),
|
||||
@ -734,6 +755,10 @@ namespace BTCPayServer.Controllers
|
||||
DefaultLang = lang ?? invoice.DefaultLanguage ?? storeBlob.DefaultLang ?? "en",
|
||||
CustomCSSLink = storeBlob.CustomCSS,
|
||||
CustomLogoLink = storeBlob.CustomLogo,
|
||||
LogoFileId = storeBlob.LogoFileId,
|
||||
BrandColor = storeBlob.BrandColor,
|
||||
CheckoutFormId = invoice.CheckoutFormId ?? storeBlob.CheckoutFormId,
|
||||
CheckoutType = invoice.CheckoutType ?? storeBlob.CheckoutType,
|
||||
HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice",
|
||||
CryptoImage = Request.GetRelativePathOrAbsolute(paymentMethodHandler.GetCryptoImage(paymentMethodId)),
|
||||
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
|
||||
@ -769,12 +794,19 @@ namespace BTCPayServer.Controllers
|
||||
IsMultiCurrency = invoice.GetPayments(false).Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1,
|
||||
StoreId = store.Id,
|
||||
AvailableCryptos = invoice.GetPaymentMethods()
|
||||
.Where(i => i.Network != null)
|
||||
.Where(i => i.Network != null &&
|
||||
// TODO: These cases and implementation need to be discussed
|
||||
(storeBlob.CheckoutType == CheckoutType.V1 ||
|
||||
// Exclude LNURL for non-topup invoices
|
||||
(invoice.IsUnsetTopUp() || i.GetId().PaymentType is not LNURLPayPaymentType)) &&
|
||||
// Exclude Lightning if OnChainWithLnInvoiceFallback is active
|
||||
(!storeBlob.OnChainWithLnInvoiceFallback || i.GetId().PaymentType is not LightningPaymentType)
|
||||
)
|
||||
.Select(kv =>
|
||||
{
|
||||
var availableCryptoPaymentMethodId = kv.GetId();
|
||||
var availableCryptoHandler = _paymentMethodHandlerDictionary[availableCryptoPaymentMethodId];
|
||||
return new PaymentModel.AvailableCrypto()
|
||||
return new PaymentModel.AvailableCrypto
|
||||
{
|
||||
PaymentMethodId = kv.GetId().ToString(),
|
||||
CryptoCode = kv.Network?.CryptoCode ?? kv.GetId().CryptoCode,
|
||||
@ -893,6 +925,14 @@ namespace BTCPayServer.Controllers
|
||||
return Ok("{}");
|
||||
}
|
||||
|
||||
[HttpPost("i/{invoiceId}/Form")]
|
||||
[HttpPost("invoice/Form")]
|
||||
public IActionResult UpdateForm(string invoiceId)
|
||||
{
|
||||
// TODO: Forms integration
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpGet("/stores/{storeId}/invoices")]
|
||||
[HttpGet("invoices")]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanViewInvoices)]
|
||||
@ -1039,10 +1079,12 @@ namespace BTCPayServer.Controllers
|
||||
return RedirectToAction(nameof(UIHomeController.Index), "UIHome");
|
||||
}
|
||||
|
||||
var storeBlob = HttpContext.GetStoreData()?.GetStoreBlob();
|
||||
var vm = new CreateInvoiceModel
|
||||
{
|
||||
StoreId = model.StoreId,
|
||||
Currency = HttpContext.GetStoreData()?.GetStoreBlob().DefaultCurrency,
|
||||
Currency = storeBlob?.DefaultCurrency,
|
||||
UseNewCheckout = storeBlob?.CheckoutType is CheckoutType.V2,
|
||||
AvailablePaymentMethods = GetPaymentMethodsSelectList()
|
||||
};
|
||||
|
||||
@ -1055,8 +1097,11 @@ namespace BTCPayServer.Controllers
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model, CancellationToken cancellationToken)
|
||||
{
|
||||
model.AvailablePaymentMethods = GetPaymentMethodsSelectList();
|
||||
var store = HttpContext.GetStoreData();
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
model.UseNewCheckout = storeBlob.CheckoutType == CheckoutType.V2;
|
||||
model.AvailablePaymentMethods = GetPaymentMethodsSelectList();
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
@ -1075,18 +1120,17 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
try
|
||||
{
|
||||
var result = await CreateInvoiceCore(new BitpayCreateInvoiceRequest()
|
||||
var result = await CreateInvoiceCore(new BitpayCreateInvoiceRequest
|
||||
{
|
||||
Price = model.Amount,
|
||||
Currency = model.Currency,
|
||||
PosData = model.PosData,
|
||||
OrderId = model.OrderId,
|
||||
//RedirectURL = redirect + "redirect",
|
||||
NotificationURL = model.NotificationUrl,
|
||||
ItemDesc = model.ItemDesc,
|
||||
FullNotifications = true,
|
||||
BuyerEmail = model.BuyerEmail,
|
||||
SupportedTransactionCurrencies = model.SupportedTransactionCurrencies?.ToDictionary(s => s, s => new InvoiceSupportedTransactionCurrency()
|
||||
SupportedTransactionCurrencies = model.SupportedTransactionCurrencies?.ToDictionary(s => s, s => new InvoiceSupportedTransactionCurrency
|
||||
{
|
||||
Enabled = true
|
||||
}),
|
||||
@ -1094,8 +1138,11 @@ namespace BTCPayServer.Controllers
|
||||
NotificationEmail = model.NotificationEmail,
|
||||
ExtendedNotifications = model.NotificationEmail != null,
|
||||
RequiresRefundEmail = model.RequiresRefundEmail == RequiresRefundEmail.InheritFromStore
|
||||
? store.GetStoreBlob().RequiresRefundEmail
|
||||
: model.RequiresRefundEmail == RequiresRefundEmail.On
|
||||
? storeBlob.RequiresRefundEmail
|
||||
: model.RequiresRefundEmail == RequiresRefundEmail.On,
|
||||
CheckoutFormId = model.CheckoutFormId == GenericFormOption.InheritFromStore.ToString()
|
||||
? storeBlob.CheckoutFormId
|
||||
: model.CheckoutFormId
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"Invoice {result.Data.Id} just created!";
|
||||
|
@ -5,21 +5,25 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.PaymentRequestViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.PaymentRequests;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Validation;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
@ -138,6 +142,7 @@ namespace BTCPayServer.Controllers
|
||||
entity.RedirectAutomatically =
|
||||
invoice.RedirectAutomatically.GetValueOrDefault(storeBlob.RedirectAutomatically);
|
||||
entity.RequiresRefundEmail = invoice.RequiresRefundEmail;
|
||||
entity.CheckoutFormId = invoice.CheckoutFormId;
|
||||
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
|
||||
|
||||
IPaymentFilter? excludeFilter = null;
|
||||
@ -167,6 +172,35 @@ namespace BTCPayServer.Controllers
|
||||
return await CreateInvoiceCoreRaw(entity, store, excludeFilter, null, cancellationToken, entityManipulator);
|
||||
}
|
||||
|
||||
internal async Task<InvoiceEntity> CreatePaymentRequestInvoice(ViewPaymentRequestViewModel pr, decimal? amount, StoreData storeData, HttpRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (pr.AllowCustomPaymentAmounts && amount != null)
|
||||
amount = Math.Min(pr.AmountDue, amount.Value);
|
||||
else
|
||||
amount = pr.AmountDue;
|
||||
var redirectUrl = _linkGenerator.PaymentRequestLink(pr.Id, request.Scheme, request.Host, request.PathBase);
|
||||
|
||||
var invoiceMetadata =
|
||||
new InvoiceMetadata
|
||||
{
|
||||
OrderId = PaymentRequestRepository.GetOrderIdForPaymentRequest(pr.Id),
|
||||
PaymentRequestId = pr.Id,
|
||||
BuyerEmail = pr.Email
|
||||
};
|
||||
|
||||
var invoiceRequest =
|
||||
new CreateInvoiceRequest
|
||||
{
|
||||
Metadata = invoiceMetadata.ToJObject(),
|
||||
Currency = pr.Currency,
|
||||
Amount = amount,
|
||||
Checkout = { RedirectURL = redirectUrl }
|
||||
};
|
||||
|
||||
var additionalTags = new List<string> { PaymentRequestRepository.GetInternalTag(pr.Id) };
|
||||
return await CreateInvoiceCoreRaw(invoiceRequest, storeData, request.GetAbsoluteRoot(), additionalTags, cancellationToken);
|
||||
}
|
||||
|
||||
internal async Task<InvoiceEntity> CreateInvoiceCoreRaw(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string>? additionalTags = null, CancellationToken cancellationToken = default, Action<InvoiceEntity>? entityManipulator = null)
|
||||
{
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
@ -193,6 +227,8 @@ namespace BTCPayServer.Controllers
|
||||
entity.DefaultLanguage = invoice.Checkout.DefaultLanguage;
|
||||
entity.DefaultPaymentMethod = invoice.Checkout.DefaultPaymentMethod;
|
||||
entity.RedirectAutomatically = invoice.Checkout.RedirectAutomatically ?? storeBlob.RedirectAutomatically;
|
||||
entity.CheckoutFormId = invoice.Checkout.CheckoutFormId;
|
||||
entity.CheckoutType = invoice.Checkout.CheckoutType;
|
||||
entity.RequiresRefundEmail = invoice.Checkout.RequiresRefundEmail;
|
||||
IPaymentFilter? excludeFilter = null;
|
||||
if (invoice.Checkout.PaymentMethods != null)
|
||||
@ -278,7 +314,6 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
if (!noNeedForMethods)
|
||||
{
|
||||
|
||||
// This loop ends with .ToList so we are querying all payment methods at once
|
||||
// instead of sequentially to improve response time
|
||||
var x1 = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
|
@ -19,6 +19,7 @@ using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
@ -408,7 +409,7 @@ namespace BTCPayServer
|
||||
if (i.Type != InvoiceType.TopUp)
|
||||
{
|
||||
min = i.GetPaymentMethod(pmi).Calculate().Due.ToDecimal(MoneyUnit.Satoshi);
|
||||
max = min;
|
||||
max = item?.Price?.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Minimum ? null : min;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(username))
|
||||
@ -559,9 +560,12 @@ namespace BTCPayServer
|
||||
LightningInvoice invoice;
|
||||
try
|
||||
{
|
||||
invoice = await client.CreateInvoice(new CreateInvoiceParams(amount.Value,
|
||||
descriptionHash,
|
||||
i.ExpirationTime.ToUniversalTime() - DateTimeOffset.UtcNow));
|
||||
var expiry = i.ExpirationTime.ToUniversalTime() - DateTimeOffset.UtcNow;
|
||||
var param = new CreateInvoiceParams(amount.Value, descriptionHash, expiry)
|
||||
{
|
||||
PrivateRouteHints = blob.LightningPrivateRouteHints
|
||||
};
|
||||
invoice = await client.CreateInvoice(param);
|
||||
if (!BOLT11PaymentRequest.Parse(invoice.BOLT11, network.NBitcoinNetwork)
|
||||
.VerifyDescriptionHash(metadata))
|
||||
{
|
||||
|
@ -35,7 +35,7 @@ namespace BTCPayServer.Controllers
|
||||
private readonly EventAggregator _EventAggregator;
|
||||
private readonly CurrencyNameTable _Currencies;
|
||||
private readonly InvoiceRepository _InvoiceRepository;
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
|
||||
public UIPaymentRequestController(
|
||||
UIInvoiceController invoiceController,
|
||||
@ -44,8 +44,8 @@ namespace BTCPayServer.Controllers
|
||||
PaymentRequestService paymentRequestService,
|
||||
EventAggregator eventAggregator,
|
||||
CurrencyNameTable currencies,
|
||||
InvoiceRepository invoiceRepository,
|
||||
LinkGenerator linkGenerator)
|
||||
StoreRepository storeRepository,
|
||||
InvoiceRepository invoiceRepository)
|
||||
{
|
||||
_InvoiceController = invoiceController;
|
||||
_UserManager = userManager;
|
||||
@ -53,8 +53,8 @@ namespace BTCPayServer.Controllers
|
||||
_PaymentRequestService = paymentRequestService;
|
||||
_EventAggregator = eventAggregator;
|
||||
_Currencies = currencies;
|
||||
_storeRepository = storeRepository;
|
||||
_InvoiceRepository = invoiceRepository;
|
||||
_linkGenerator = linkGenerator;
|
||||
}
|
||||
|
||||
[BitpayAPIConstraint(false)]
|
||||
@ -213,14 +213,7 @@ namespace BTCPayServer.Controllers
|
||||
return BadRequest("Payment Request has expired");
|
||||
}
|
||||
|
||||
var stateAllowedToDisplay = new HashSet<InvoiceState>
|
||||
{
|
||||
new InvoiceState(InvoiceStatusLegacy.New, InvoiceExceptionStatus.None),
|
||||
new InvoiceState(InvoiceStatusLegacy.New, InvoiceExceptionStatus.PaidPartial),
|
||||
};
|
||||
var currentInvoice = result
|
||||
.Invoices
|
||||
.FirstOrDefault(invoice => stateAllowedToDisplay.Contains(invoice.State));
|
||||
var currentInvoice = result.Invoices.GetReusableInvoice(amount);
|
||||
if (currentInvoice != null)
|
||||
{
|
||||
if (redirectToInvoice)
|
||||
@ -231,38 +224,10 @@ namespace BTCPayServer.Controllers
|
||||
return Ok(currentInvoice.Id);
|
||||
}
|
||||
|
||||
if (result.AllowCustomPaymentAmounts && amount != null)
|
||||
amount = Math.Min(result.AmountDue, amount.Value);
|
||||
else
|
||||
amount = result.AmountDue;
|
||||
|
||||
var pr = await _PaymentRequestRepository.FindPaymentRequest(payReqId, null, cancellationToken);
|
||||
var blob = pr.GetBlob();
|
||||
var store = pr.StoreData;
|
||||
try
|
||||
{
|
||||
var redirectUrl = _linkGenerator.PaymentRequestLink(payReqId, Request.Scheme, Request.Host, Request.PathBase);
|
||||
|
||||
var invoiceMetadata =
|
||||
new InvoiceMetadata
|
||||
{
|
||||
OrderId = PaymentRequestRepository.GetOrderIdForPaymentRequest(payReqId),
|
||||
PaymentRequestId = payReqId,
|
||||
BuyerEmail = result.Email
|
||||
};
|
||||
|
||||
var invoiceRequest =
|
||||
new CreateInvoiceRequest
|
||||
{
|
||||
Metadata = invoiceMetadata.ToJObject(),
|
||||
Currency = blob.Currency,
|
||||
Amount = amount.Value,
|
||||
Checkout = { RedirectURL = redirectUrl }
|
||||
};
|
||||
|
||||
var additionalTags = new List<string> { PaymentRequestRepository.GetInternalTag(payReqId) };
|
||||
var newInvoice = await _InvoiceController.CreateInvoiceCoreRaw(invoiceRequest, store, Request.GetAbsoluteRoot(), additionalTags, cancellationToken);
|
||||
|
||||
var store = await _storeRepository.FindStore(result.StoreId);
|
||||
var newInvoice = await _InvoiceController.CreatePaymentRequestInvoice(result, amount, store, Request, cancellationToken);
|
||||
if (redirectToInvoice)
|
||||
{
|
||||
return RedirectToAction("Checkout", "UIInvoice", new { invoiceId = newInvoice.Id });
|
||||
|
@ -13,7 +13,6 @@ using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
|
||||
[Route("embed/{storeId}/{cryptoCode}/ln")]
|
||||
[AllowAnonymous]
|
||||
public class UIPublicLightningNodeInfoController : Controller
|
||||
@ -43,11 +42,11 @@ namespace BTCPayServer.Controllers
|
||||
var paymentMethodDetails = GetExistingLightningSupportedPaymentMethod(cryptoCode, store);
|
||||
var network = _BtcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
||||
var nodeInfo =
|
||||
await _LightningLikePaymentHandler.GetNodeInfo(paymentMethodDetails, network, new InvoiceLogs());
|
||||
await _LightningLikePaymentHandler.GetNodeInfo(paymentMethodDetails, network, new InvoiceLogs(), throws: true);
|
||||
|
||||
return View(new ShowLightningNodeInfoViewModel
|
||||
{
|
||||
Available = nodeInfo.Any(),
|
||||
Available = true,
|
||||
NodeInfo = nodeInfo.Select(n => new ShowLightningNodeInfoViewModel.NodeData(n)).ToArray(),
|
||||
CryptoCode = cryptoCode,
|
||||
CryptoImage = GetImage(paymentMethodDetails.PaymentId, network),
|
||||
|
@ -83,11 +83,11 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpPost("server/plugins/install")]
|
||||
public async Task<IActionResult> InstallPlugin(
|
||||
[FromServices] PluginService pluginService, string plugin, bool update = false, string path ="")
|
||||
[FromServices] PluginService pluginService, string plugin, bool update = false, string version = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await pluginService.DownloadRemotePlugin(plugin, path);
|
||||
await pluginService.DownloadRemotePlugin(plugin, version);
|
||||
if (update)
|
||||
{
|
||||
pluginService.UpdatePlugin(plugin);
|
||||
|
@ -33,6 +33,7 @@ namespace BTCPayServer.Controllers
|
||||
StoreId = CurrentStore.Id,
|
||||
StoreName = CurrentStore.StoreName,
|
||||
CryptoCode = cryptoCode,
|
||||
Network = _NetworkProvider.DefaultNetwork,
|
||||
IsSetUp = walletEnabled || lightningEnabled
|
||||
};
|
||||
|
||||
|
@ -160,7 +160,16 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
CryptoCode = paymentMethodId.CryptoCode
|
||||
};
|
||||
paymentMethod.SetLightningUrl(connectionString);
|
||||
|
||||
try
|
||||
{
|
||||
paymentMethod.SetLightningUrl(connectionString);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), ex.Message);
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
switch (command)
|
||||
@ -185,12 +194,15 @@ namespace BTCPayServer.Controllers
|
||||
try
|
||||
{
|
||||
var info = await handler.GetNodeInfo(paymentMethod, network, new InvoiceLogs(), Request.IsOnion(), true);
|
||||
if (!vm.SkipPortTest)
|
||||
var hasPublicAddress = info.Any();
|
||||
if (!vm.SkipPortTest && hasPublicAddress)
|
||||
{
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20));
|
||||
await handler.TestConnection(info.First(), cts.Token);
|
||||
}
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"Connection to the Lightning node successful. Your node address: {info.First()}";
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Connection to the Lightning node successful" + (hasPublicAddress
|
||||
? $". Your node address: {info.First()}"
|
||||
: ", but no public address has been configured");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -216,7 +228,7 @@ namespace BTCPayServer.Controllers
|
||||
var lightning = GetExistingLightningSupportedPaymentMethod(cryptoCode, store);
|
||||
if (lightning == null)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = $"You need to connect to a Lightning node before adjusting its settings.";
|
||||
TempData[WellKnownTempData.ErrorMessage] = "You need to connect to a Lightning node before adjusting its settings.";
|
||||
|
||||
return RedirectToAction(nameof(SetupLightningNode), new { storeId, cryptoCode });
|
||||
}
|
||||
|
@ -119,17 +119,16 @@ namespace BTCPayServer.Controllers
|
||||
accountSettings.AccountKeyPath =
|
||||
vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath);
|
||||
accountSettings.RootFingerprint = string.IsNullOrEmpty(vm.RootFingerprint)
|
||||
? (HDFingerprint?)null
|
||||
: new HDFingerprint(
|
||||
NBitcoin.DataEncoders.Encoders.Hex.DecodeData(vm.RootFingerprint));
|
||||
? null
|
||||
: new HDFingerprint(Encoders.Hex.DecodeData(vm.RootFingerprint));
|
||||
}
|
||||
}
|
||||
vm.DerivationScheme = strategy.AccountDerivation.ToString();
|
||||
ModelState.Remove(nameof(vm.DerivationScheme));
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid wallet format");
|
||||
ModelState.AddModelError(nameof(vm.DerivationScheme), $"Invalid wallet format: {ex.Message}");
|
||||
return View(vm.ViewName, vm);
|
||||
}
|
||||
}
|
||||
|
@ -3,9 +3,11 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Configuration;
|
||||
@ -59,6 +61,7 @@ namespace BTCPayServer.Controllers
|
||||
IAuthorizationService authorizationService,
|
||||
EventAggregator eventAggregator,
|
||||
AppService appService,
|
||||
IFileService fileService,
|
||||
WebhookSender webhookNotificationManager,
|
||||
IDataProtectionProvider dataProtector,
|
||||
IOptions<ExternalServicesOptions> externalServiceOptions)
|
||||
@ -74,6 +77,7 @@ namespace BTCPayServer.Controllers
|
||||
_policiesSettings = policiesSettings;
|
||||
_authorizationService = authorizationService;
|
||||
_appService = appService;
|
||||
_fileService = fileService;
|
||||
DataProtector = dataProtector.CreateProtector("ConfigProtector");
|
||||
WebhookNotificationManager = webhookNotificationManager;
|
||||
_EventAggregator = eventAggregator;
|
||||
@ -101,9 +105,14 @@ namespace BTCPayServer.Controllers
|
||||
private readonly PoliciesSettings _policiesSettings;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly AppService _appService;
|
||||
private readonly IFileService _fileService;
|
||||
private readonly EventAggregator _EventAggregator;
|
||||
private readonly IOptions<ExternalServicesOptions> _externalServiceOptions;
|
||||
|
||||
public string? GeneratedPairingCode { get; set; }
|
||||
public WebhookSender WebhookNotificationManager { get; }
|
||||
public IDataProtector DataProtector { get; }
|
||||
|
||||
[TempData]
|
||||
public bool StoreNotConfigured
|
||||
{
|
||||
@ -356,20 +365,11 @@ namespace BTCPayServer.Controllers
|
||||
.Where(s => s.PaymentId.PaymentType != PaymentTypes.LNURLPay)
|
||||
.Select(method =>
|
||||
{
|
||||
var existing =
|
||||
storeBlob.PaymentMethodCriteria.SingleOrDefault(criteria =>
|
||||
var existing = storeBlob.PaymentMethodCriteria.SingleOrDefault(criteria =>
|
||||
criteria.PaymentMethod == method.PaymentId);
|
||||
if (existing is null)
|
||||
{
|
||||
return new PaymentMethodCriteriaViewModel()
|
||||
{
|
||||
PaymentMethod = method.PaymentId.ToString(),
|
||||
Value = ""
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
return new PaymentMethodCriteriaViewModel()
|
||||
return existing is null
|
||||
? new PaymentMethodCriteriaViewModel { PaymentMethod = method.PaymentId.ToString(), Value = "" }
|
||||
: new PaymentMethodCriteriaViewModel
|
||||
{
|
||||
PaymentMethod = existing.PaymentMethod.ToString(),
|
||||
Type = existing.Above
|
||||
@ -377,9 +377,11 @@ namespace BTCPayServer.Controllers
|
||||
: PaymentMethodCriteriaViewModel.CriteriaType.LessThan,
|
||||
Value = existing.Value?.ToString() ?? ""
|
||||
};
|
||||
}
|
||||
}).ToList();
|
||||
|
||||
vm.UseNewCheckout = storeBlob.CheckoutType == Client.Models.CheckoutType.V2;
|
||||
vm.CheckoutFormId = storeBlob.CheckoutFormId;
|
||||
vm.OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback;
|
||||
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
|
||||
vm.LazyPaymentMethods = storeBlob.LazyPaymentMethods;
|
||||
vm.RedirectAutomatically = storeBlob.RedirectAutomatically;
|
||||
@ -495,6 +497,14 @@ namespace BTCPayServer.Controllers
|
||||
PaymentMethod = paymentMethodId
|
||||
});
|
||||
}
|
||||
|
||||
blob.CheckoutType = model.UseNewCheckout ? Client.Models.CheckoutType.V2 : Client.Models.CheckoutType.V1;
|
||||
if (blob.CheckoutType == Client.Models.CheckoutType.V2)
|
||||
{
|
||||
blob.CheckoutFormId = model.CheckoutFormId;
|
||||
blob.OnChainWithLnInvoiceFallback = model.OnChainWithLnInvoiceFallback;
|
||||
}
|
||||
|
||||
blob.RequiresRefundEmail = model.RequiresRefundEmail;
|
||||
blob.LazyPaymentMethods = model.LazyPaymentMethods;
|
||||
blob.RedirectAutomatically = model.RedirectAutomatically;
|
||||
@ -592,6 +602,8 @@ namespace BTCPayServer.Controllers
|
||||
Id = store.Id,
|
||||
StoreName = store.StoreName,
|
||||
StoreWebsite = store.StoreWebsite,
|
||||
LogoFileId = storeBlob.LogoFileId,
|
||||
BrandColor = storeBlob.BrandColor,
|
||||
NetworkFeeMode = storeBlob.NetworkFeeMode,
|
||||
AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice,
|
||||
PaymentTolerance = storeBlob.PaymentTolerance,
|
||||
@ -619,7 +631,7 @@ namespace BTCPayServer.Controllers
|
||||
needUpdate = true;
|
||||
CurrentStore.StoreWebsite = model.StoreWebsite;
|
||||
}
|
||||
|
||||
|
||||
var blob = CurrentStore.GetStoreBlob();
|
||||
blob.AnyoneCanInvoice = model.AnyoneCanCreateInvoice;
|
||||
blob.NetworkFeeMode = model.NetworkFeeMode;
|
||||
@ -627,6 +639,43 @@ namespace BTCPayServer.Controllers
|
||||
blob.DefaultCurrency = model.DefaultCurrency;
|
||||
blob.InvoiceExpiration = TimeSpan.FromMinutes(model.InvoiceExpiration);
|
||||
blob.RefundBOLT11Expiration = TimeSpan.FromDays(model.BOLT11Expiration);
|
||||
if (!string.IsNullOrEmpty(model.BrandColor) && !ColorPalette.IsValid(model.BrandColor))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.BrandColor), "Invalid color");
|
||||
return View(model);
|
||||
}
|
||||
blob.BrandColor = model.BrandColor;
|
||||
|
||||
if (model.LogoFile != null)
|
||||
{
|
||||
if (model.LogoFile.ContentType.StartsWith("image/", StringComparison.InvariantCulture))
|
||||
{
|
||||
var userId = GetUserId();
|
||||
if (userId is null)
|
||||
return NotFound();
|
||||
|
||||
// delete existing image
|
||||
if (!string.IsNullOrEmpty(blob.LogoFileId))
|
||||
{
|
||||
await _fileService.RemoveFile(blob.LogoFileId, userId);
|
||||
}
|
||||
|
||||
// add new image
|
||||
try
|
||||
{
|
||||
var storedFile = await _fileService.AddFile(model.LogoFile, userId);
|
||||
blob.LogoFileId = storedFile.Id;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = $"Could not save logo: {e.Message}";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "The uploaded logo file needs to be an image";
|
||||
}
|
||||
}
|
||||
|
||||
if (CurrentStore.SetStoreBlob(blob))
|
||||
{
|
||||
@ -672,10 +721,10 @@ namespace BTCPayServer.Controllers
|
||||
private DerivationSchemeSettings ParseDerivationStrategy(string derivationScheme, BTCPayNetwork network)
|
||||
{
|
||||
var parser = new DerivationSchemeParser(network);
|
||||
try
|
||||
var isOD = Regex.Match(derivationScheme, @"\(.*?\)");
|
||||
if (isOD.Success)
|
||||
{
|
||||
var derivationSchemeSettings = new DerivationSchemeSettings();
|
||||
derivationSchemeSettings.Network = network;
|
||||
var derivationSchemeSettings = new DerivationSchemeSettings { Network = network };
|
||||
var result = parser.ParseOutputDescriptor(derivationScheme);
|
||||
derivationSchemeSettings.AccountOriginal = derivationScheme.Trim();
|
||||
derivationSchemeSettings.AccountDerivation = result.Item1;
|
||||
@ -687,16 +736,12 @@ namespace BTCPayServer.Controllers
|
||||
}).ToArray() ?? new AccountKeySettings[result.Item1.GetExtPubKeys().Count()];
|
||||
return derivationSchemeSettings;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
return new DerivationSchemeSettings(parser.Parse(derivationScheme), network);
|
||||
|
||||
var strategy = parser.Parse(derivationScheme);
|
||||
return new DerivationSchemeSettings(strategy, network);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/Tokens")]
|
||||
[HttpGet("{storeId}/tokens")]
|
||||
public async Task<IActionResult> ListTokens()
|
||||
{
|
||||
var model = new TokensViewModel();
|
||||
@ -739,8 +784,7 @@ namespace BTCPayServer.Controllers
|
||||
return RedirectToAction(nameof(ListTokens), new { storeId = token?.StoreId });
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/tokens/{tokenId}")]
|
||||
[HttpGet("{storeId}/tokens/{tokenId}")]
|
||||
public async Task<IActionResult> ShowToken(string tokenId)
|
||||
{
|
||||
var token = await _TokenRepository.GetToken(tokenId);
|
||||
@ -749,8 +793,18 @@ namespace BTCPayServer.Controllers
|
||||
return View(token);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/Tokens/Create")]
|
||||
[HttpGet("{storeId}/tokens/create")]
|
||||
public IActionResult CreateToken(string storeId)
|
||||
{
|
||||
var model = new CreateTokenViewModel();
|
||||
ViewBag.HidePublicKey = storeId == null;
|
||||
ViewBag.ShowStores = storeId == null;
|
||||
ViewBag.ShowMenu = storeId != null;
|
||||
model.StoreId = storeId;
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/tokens/create")]
|
||||
public async Task<IActionResult> CreateToken(string storeId, CreateTokenViewModel model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
@ -791,29 +845,12 @@ namespace BTCPayServer.Controllers
|
||||
GeneratedPairingCode = pairingCode;
|
||||
return RedirectToAction(nameof(RequestPairing), new
|
||||
{
|
||||
pairingCode = pairingCode,
|
||||
pairingCode,
|
||||
selectedStore = storeId
|
||||
});
|
||||
}
|
||||
|
||||
public string? GeneratedPairingCode { get; set; }
|
||||
public WebhookSender WebhookNotificationManager { get; }
|
||||
public IDataProtector DataProtector { get; }
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/Tokens/Create")]
|
||||
public IActionResult CreateToken(string storeId)
|
||||
{
|
||||
var model = new CreateTokenViewModel();
|
||||
ViewBag.HidePublicKey = storeId == null;
|
||||
ViewBag.ShowStores = storeId == null;
|
||||
ViewBag.ShowMenu = storeId != null;
|
||||
model.StoreId = storeId;
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("/api-tokens")]
|
||||
[HttpGet("/api-tokens")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> CreateToken()
|
||||
{
|
||||
@ -834,16 +871,14 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("/api-tokens")]
|
||||
[HttpPost("/api-tokens")]
|
||||
[AllowAnonymous]
|
||||
public Task<IActionResult> CreateToken2(CreateTokenViewModel model)
|
||||
{
|
||||
return CreateToken(model.StoreId, model);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/tokens/apikey")]
|
||||
[HttpPost("{storeId}/tokens/apikey")]
|
||||
public async Task<IActionResult> GenerateAPIKey(string storeId, string command = "")
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
@ -866,50 +901,48 @@ namespace BTCPayServer.Controllers
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("/api-access-request")]
|
||||
[HttpGet("/api-access-request")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> RequestPairing(string pairingCode, string? selectedStore = null)
|
||||
{
|
||||
var userId = GetUserId();
|
||||
if (userId == null)
|
||||
return Challenge(AuthenticationSchemes.Cookie);
|
||||
|
||||
if (pairingCode == null)
|
||||
return NotFound();
|
||||
|
||||
if (selectedStore != null)
|
||||
{
|
||||
var store = await _Repo.FindStore(selectedStore, userId);
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
HttpContext.SetStoreData(store);
|
||||
ViewBag.ShowStores = false;
|
||||
}
|
||||
|
||||
var pairing = await _TokenRepository.GetPairingAsync(pairingCode);
|
||||
if (pairing == null)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "Unknown pairing code";
|
||||
return RedirectToAction(nameof(UIHomeController.Index), "UIHome");
|
||||
}
|
||||
else
|
||||
|
||||
var stores = await _Repo.GetStoresByUserId(userId);
|
||||
return View(new PairingModel
|
||||
{
|
||||
var stores = await _Repo.GetStoresByUserId(userId);
|
||||
return View(new PairingModel
|
||||
Id = pairing.Id,
|
||||
Label = pairing.Label,
|
||||
SIN = pairing.SIN ?? "Server-Initiated Pairing",
|
||||
StoreId = selectedStore ?? stores.FirstOrDefault()?.Id,
|
||||
Stores = stores.Where(u => u.Role == StoreRoles.Owner).Select(s => new PairingModel.StoreViewModel
|
||||
{
|
||||
Id = pairing.Id,
|
||||
Label = pairing.Label,
|
||||
SIN = pairing.SIN ?? "Server-Initiated Pairing",
|
||||
StoreId = selectedStore ?? stores.FirstOrDefault()?.Id,
|
||||
Stores = stores.Where(u => u.Role == StoreRoles.Owner).Select(s => new PairingModel.StoreViewModel()
|
||||
{
|
||||
Id = s.Id,
|
||||
Name = string.IsNullOrEmpty(s.StoreName) ? s.Id : s.StoreName
|
||||
}).ToArray()
|
||||
});
|
||||
}
|
||||
Id = s.Id,
|
||||
Name = string.IsNullOrEmpty(s.StoreName) ? s.Id : s.StoreName
|
||||
}).ToArray()
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("/api-access-request")]
|
||||
[HttpPost("/api-access-request")]
|
||||
public async Task<IActionResult> Pair(string pairingCode, string storeId)
|
||||
{
|
||||
if (pairingCode == null)
|
||||
|
@ -460,7 +460,7 @@ namespace BTCPayServer.Controllers
|
||||
vm.SigningContext.OriginalPSBT = psbt.ToBase64();
|
||||
proposedPayjoin.Finalize();
|
||||
var hash = proposedPayjoin.ExtractTransaction().GetHash();
|
||||
_EventAggregator.Publish(new UpdateTransactionLabel(walletId, hash, UpdateTransactionLabel.PayjoinLabelTemplate()));
|
||||
await WalletRepository.AddWalletTransactionAttachment(walletId, hash, Attachment.Payjoin());
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
|
@ -39,6 +39,8 @@ using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Models;
|
||||
using Newtonsoft.Json;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -60,11 +62,10 @@ namespace BTCPayServer.Controllers
|
||||
private readonly IFeeProviderFactory _feeRateProvider;
|
||||
private readonly BTCPayWalletProvider _walletProvider;
|
||||
private readonly WalletReceiveService _walletReceiveService;
|
||||
private readonly EventAggregator _EventAggregator;
|
||||
private readonly SettingsRepository _settingsRepository;
|
||||
private readonly DelayedTransactionBroadcaster _broadcaster;
|
||||
private readonly PayjoinClient _payjoinClient;
|
||||
private readonly LabelFactory _labelFactory;
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
private readonly PullPaymentHostedService _pullPaymentHostedService;
|
||||
private readonly UTXOLocker _utxoLocker;
|
||||
private readonly WalletHistogramService _walletHistogramService;
|
||||
@ -84,16 +85,16 @@ namespace BTCPayServer.Controllers
|
||||
IFeeProviderFactory feeRateProvider,
|
||||
BTCPayWalletProvider walletProvider,
|
||||
WalletReceiveService walletReceiveService,
|
||||
EventAggregator eventAggregator,
|
||||
SettingsRepository settingsRepository,
|
||||
DelayedTransactionBroadcaster broadcaster,
|
||||
PayjoinClient payjoinClient,
|
||||
LabelFactory labelFactory,
|
||||
IServiceProvider serviceProvider,
|
||||
PullPaymentHostedService pullPaymentHostedService,
|
||||
UTXOLocker utxoLocker)
|
||||
UTXOLocker utxoLocker,
|
||||
LinkGenerator linkGenerator)
|
||||
{
|
||||
_currencyTable = currencyTable;
|
||||
_linkGenerator = linkGenerator;
|
||||
Repository = repo;
|
||||
WalletRepository = walletRepository;
|
||||
RateFetcher = rateProvider;
|
||||
@ -105,11 +106,9 @@ namespace BTCPayServer.Controllers
|
||||
_feeRateProvider = feeRateProvider;
|
||||
_walletProvider = walletProvider;
|
||||
_walletReceiveService = walletReceiveService;
|
||||
_EventAggregator = eventAggregator;
|
||||
_settingsRepository = settingsRepository;
|
||||
_broadcaster = broadcaster;
|
||||
_payjoinClient = payjoinClient;
|
||||
_labelFactory = labelFactory;
|
||||
_pullPaymentHostedService = pullPaymentHostedService;
|
||||
_utxoLocker = utxoLocker;
|
||||
ServiceProvider = serviceProvider;
|
||||
@ -146,58 +145,19 @@ namespace BTCPayServer.Controllers
|
||||
if (paymentMethod == null)
|
||||
return NotFound();
|
||||
|
||||
var walletBlobInfoAsync = WalletRepository.GetWalletInfo(walletId);
|
||||
var walletTransactionsInfoAsync = WalletRepository.GetWalletTransactionsInfo(walletId);
|
||||
var txObjId = new WalletObjectId(walletId, WalletObjectData.Types.Tx, transactionId);
|
||||
var wallet = _walletProvider.GetWallet(paymentMethod.Network);
|
||||
var walletBlobInfo = await walletBlobInfoAsync;
|
||||
var walletTransactionsInfo = await walletTransactionsInfoAsync;
|
||||
if (addlabel != null)
|
||||
{
|
||||
if (!walletTransactionsInfo.TryGetValue(transactionId, out var walletTransactionInfo))
|
||||
{
|
||||
walletTransactionInfo = new WalletTransactionInfo();
|
||||
}
|
||||
|
||||
var rawLabel = await _labelFactory.BuildLabel(
|
||||
walletBlobInfo,
|
||||
Request!,
|
||||
walletTransactionInfo,
|
||||
walletId,
|
||||
transactionId,
|
||||
addlabel
|
||||
);
|
||||
if (walletTransactionInfo.Labels.TryAdd(rawLabel.Text, rawLabel))
|
||||
{
|
||||
await WalletRepository.SetWalletTransactionInfo(walletId, transactionId, walletTransactionInfo);
|
||||
}
|
||||
await WalletRepository.AddWalletObjectLabels(txObjId, addlabel);
|
||||
}
|
||||
else if (removelabel != null)
|
||||
{
|
||||
removelabel = removelabel.Trim();
|
||||
if (walletTransactionsInfo.TryGetValue(transactionId, out var walletTransactionInfo))
|
||||
{
|
||||
if (walletTransactionInfo.Labels.Remove(removelabel))
|
||||
{
|
||||
var canDeleteColor =
|
||||
!walletTransactionsInfo.Any(txi => txi.Value.Labels.ContainsKey(removelabel));
|
||||
if (canDeleteColor)
|
||||
{
|
||||
walletBlobInfo.LabelColors.Remove(removelabel);
|
||||
await WalletRepository.SetWalletInfo(walletId, walletBlobInfo);
|
||||
}
|
||||
await WalletRepository.SetWalletTransactionInfo(walletId, transactionId, walletTransactionInfo);
|
||||
}
|
||||
}
|
||||
await WalletRepository.RemoveWalletObjectLabels(txObjId, removelabel);
|
||||
}
|
||||
else if (addcomment != null)
|
||||
{
|
||||
addcomment = addcomment.Trim().Truncate(WalletTransactionDataExtensions.MaxCommentSize);
|
||||
if (!walletTransactionsInfo.TryGetValue(transactionId, out var walletTransactionInfo))
|
||||
{
|
||||
walletTransactionInfo = new WalletTransactionInfo();
|
||||
}
|
||||
walletTransactionInfo.Comment = addcomment;
|
||||
await WalletRepository.SetWalletTransactionInfo(walletId, transactionId, walletTransactionInfo);
|
||||
await WalletRepository.SetWalletObjectComment(txObjId, addcomment);
|
||||
}
|
||||
return RedirectToAction(nameof(WalletTransactions), new { walletId = walletId.ToString() });
|
||||
}
|
||||
@ -267,15 +227,17 @@ namespace BTCPayServer.Controllers
|
||||
return NotFound();
|
||||
|
||||
var wallet = _walletProvider.GetWallet(paymentMethod.Network);
|
||||
var walletBlobAsync = WalletRepository.GetWalletInfo(walletId);
|
||||
var walletTransactionsInfoAsync = WalletRepository.GetWalletTransactionsInfo(walletId);
|
||||
|
||||
// We can't filter at the database level if we need to apply label filter
|
||||
var preFiltering = string.IsNullOrEmpty(labelFilter);
|
||||
var transactions = await wallet.FetchTransactionHistory(paymentMethod.AccountDerivation, preFiltering ? skip : null, preFiltering ? count : null);
|
||||
var walletBlob = await walletBlobAsync;
|
||||
var walletTransactionsInfo = await walletTransactionsInfoAsync;
|
||||
var walletTransactionsInfo = await WalletRepository.GetWalletTransactionsInfo(walletId, transactions.Select(t => t.TransactionId.ToString()).ToArray());
|
||||
var model = new ListTransactionsViewModel { Skip = skip, Count = count };
|
||||
model.Labels.AddRange(
|
||||
(await WalletRepository.GetWalletLabels(walletId))
|
||||
.Select(c => (c.Label, c.Color, ColorPalette.Default.TextColor(c.Color)))
|
||||
);
|
||||
|
||||
if (labelFilter != null)
|
||||
{
|
||||
model.PaginationQuery = new Dictionary<string, object> { { "labelFilter", labelFilter } };
|
||||
@ -305,25 +267,30 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
if (walletTransactionsInfo.TryGetValue(tx.TransactionId.ToString(), out var transactionInfo))
|
||||
{
|
||||
var labels = _labelFactory.ColorizeTransactionLabels(walletBlob, transactionInfo, Request);
|
||||
vm.Labels.AddRange(labels);
|
||||
model.Labels.AddRange(labels);
|
||||
var labels = CreateTransactionTagModels(transactionInfo);
|
||||
vm.Tags.AddRange(labels);
|
||||
vm.Comment = transactionInfo.Comment;
|
||||
}
|
||||
|
||||
if (labelFilter == null ||
|
||||
vm.Labels.Any(l => l.Text.Equals(labelFilter, StringComparison.OrdinalIgnoreCase)))
|
||||
vm.Tags.Any(l => l.Text.Equals(labelFilter, StringComparison.OrdinalIgnoreCase)))
|
||||
model.Transactions.Add(vm);
|
||||
}
|
||||
|
||||
model.Total = preFiltering ? null : model.Transactions.Count;
|
||||
model.Transactions = model.Transactions.Skip(skip).Take(count)
|
||||
.ToList();
|
||||
// if we couldn't filter at the db level, we need to apply skip and count
|
||||
if (!preFiltering)
|
||||
{
|
||||
model.Transactions = model.Transactions.Skip(skip).Take(count).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
model.CryptoCode = walletId.CryptoCode;
|
||||
|
||||
return View(model);
|
||||
//If ajax call then load the partial view
|
||||
return Request.Headers["X-Requested-With"] == "XMLHttpRequest"
|
||||
? PartialView("_WalletTransactionsList", model)
|
||||
: View(model);
|
||||
}
|
||||
|
||||
[HttpGet("{walletId}/histogram/{type}")]
|
||||
@ -607,17 +574,15 @@ namespace BTCPayServer.Controllers
|
||||
var schemeSettings = GetDerivationSchemeSettings(walletId);
|
||||
if (schemeSettings is null)
|
||||
return NotFound();
|
||||
var walletBlobAsync = await WalletRepository.GetWalletInfo(walletId);
|
||||
var walletTransactionsInfoAsync = await WalletRepository.GetWalletTransactionsInfo(walletId);
|
||||
|
||||
var utxos = await _walletProvider.GetWallet(network)
|
||||
.GetUnspentCoins(schemeSettings.AccountDerivation, false, cancellation);
|
||||
|
||||
var walletTransactionsInfoAsync = await this.WalletRepository.GetWalletTransactionsInfo(walletId, utxos.Select(u => u.OutPoint.Hash.ToString()).Distinct().ToArray());
|
||||
vm.InputsAvailable = utxos.Select(coin =>
|
||||
{
|
||||
walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.Hash.ToString(), out var info);
|
||||
var labels = info?.Labels == null
|
||||
? new List<ColoredLabel>()
|
||||
: _labelFactory.ColorizeTransactionLabels(walletBlobAsync, info, Request).ToList();
|
||||
var labels = CreateTransactionTagModels(info).ToList();
|
||||
return new WalletSendModel.InputSelectionOption()
|
||||
{
|
||||
Outpoint = coin.OutPoint.ToString(),
|
||||
@ -1353,6 +1318,117 @@ namespace BTCPayServer.Controllers
|
||||
private string GetUserId() => _userManager.GetUserId(User);
|
||||
|
||||
private StoreData GetCurrentStore() => HttpContext.GetStoreData();
|
||||
|
||||
public IEnumerable<TransactionTagModel> CreateTransactionTagModels(WalletTransactionInfo? transactionInfo)
|
||||
{
|
||||
if (transactionInfo is null)
|
||||
return Array.Empty<TransactionTagModel>();
|
||||
|
||||
string PayoutTooltip(IGrouping<string, string>? payoutsByPullPaymentId = null)
|
||||
{
|
||||
if (payoutsByPullPaymentId is null)
|
||||
{
|
||||
return "Paid a payout";
|
||||
}
|
||||
else if (payoutsByPullPaymentId.Count() == 1)
|
||||
{
|
||||
var pp = payoutsByPullPaymentId.Key;
|
||||
var payout = payoutsByPullPaymentId.First();
|
||||
if (!string.IsNullOrEmpty(pp))
|
||||
return $"Paid a payout ({payout}) of a pull payment ({pp})";
|
||||
else
|
||||
return $"Paid a payout {payout}";
|
||||
}
|
||||
else
|
||||
{
|
||||
var pp = payoutsByPullPaymentId.Key;
|
||||
if (!string.IsNullOrEmpty(pp))
|
||||
return $"Paid {payoutsByPullPaymentId.Count()} payouts of a pull payment ({pp})";
|
||||
else
|
||||
return $"Paid {payoutsByPullPaymentId.Count()} payouts";
|
||||
}
|
||||
}
|
||||
|
||||
var models = new Dictionary<string, TransactionTagModel>();
|
||||
foreach (var tag in transactionInfo.Attachments)
|
||||
{
|
||||
if (models.ContainsKey(tag.Type))
|
||||
continue;
|
||||
if (!transactionInfo.LabelColors.TryGetValue(tag.Type, out var color))
|
||||
continue;
|
||||
var model = new TransactionTagModel
|
||||
{
|
||||
Text = tag.Type,
|
||||
Color = color,
|
||||
TextColor = ColorPalette.Default.TextColor(color)
|
||||
};
|
||||
models.Add(tag.Type, model);
|
||||
if (tag.Type == "payout")
|
||||
{
|
||||
var payoutsByPullPaymentId =
|
||||
transactionInfo.Attachments.Where(t => t.Type == "payout")
|
||||
.GroupBy(t => t.Data?["pullPaymentId"]?.Value<string>() ?? "",
|
||||
k => k.Id).ToList();
|
||||
|
||||
model.Tooltip = payoutsByPullPaymentId.Count switch
|
||||
{
|
||||
0 => PayoutTooltip(),
|
||||
1 => PayoutTooltip(payoutsByPullPaymentId.First()),
|
||||
_ =>
|
||||
$"<ul>{string.Join(string.Empty, payoutsByPullPaymentId.Select(pair => $"<li>{PayoutTooltip(pair)}</li>"))}</ul>"
|
||||
};
|
||||
|
||||
model.Link = _linkGenerator.PayoutLink(transactionInfo.WalletId.ToString(), null, PayoutState.Completed, Request.Scheme, Request.Host,
|
||||
Request.PathBase);
|
||||
}
|
||||
else if (tag.Type == "payjoin")
|
||||
{
|
||||
model.Tooltip = $"This UTXO was part of a PayJoin transaction.";
|
||||
}
|
||||
else if (tag.Type == "invoice")
|
||||
{
|
||||
model.Tooltip = $"Received through an invoice {tag.Id}";
|
||||
model.Link = string.IsNullOrEmpty(tag.Id)
|
||||
? null
|
||||
: _linkGenerator.InvoiceLink(tag.Id, Request.Scheme, Request.Host, Request.PathBase);
|
||||
}
|
||||
else if (tag.Type == "payment-request")
|
||||
{
|
||||
model.Tooltip = $"Received through a payment request {tag.Id}";
|
||||
model.Link = _linkGenerator.PaymentRequestLink(tag.Id, Request.Scheme, Request.Host, Request.PathBase);
|
||||
}
|
||||
else if (tag.Type == "app")
|
||||
{
|
||||
model.Tooltip = $"Received through an app {tag.Id}";
|
||||
model.Link = _linkGenerator.AppLink(tag.Id, Request.Scheme, Request.Host, Request.PathBase);
|
||||
}
|
||||
else if (tag.Type == "pj-exposed")
|
||||
{
|
||||
|
||||
if (tag.Id.Length != 0)
|
||||
{
|
||||
model.Tooltip = $"This UTXO was exposed through a PayJoin proposal for an invoice ({tag.Id})";
|
||||
model.Link = _linkGenerator.InvoiceLink(tag.Id, Request.Scheme, Request.Host, Request.PathBase);
|
||||
}
|
||||
else
|
||||
{
|
||||
model.Tooltip = $"This UTXO was exposed through a PayJoin proposal";
|
||||
}
|
||||
}
|
||||
else if (tag.Type == "payjoin")
|
||||
{
|
||||
model.Tooltip = $"This UTXO was part of a PayJoin transaction.";
|
||||
}
|
||||
}
|
||||
foreach (var label in transactionInfo.LabelColors)
|
||||
models.TryAdd(label.Key, new TransactionTagModel
|
||||
{
|
||||
Text = label.Key,
|
||||
Color = label.Value,
|
||||
TextColor = ColorPalette.Default.TextColor(label.Value)
|
||||
});
|
||||
return models.Values.OrderBy(v => v.Text);
|
||||
}
|
||||
}
|
||||
|
||||
public class WalletReceiveViewModel
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user