Compare commits
70 Commits
v1.11.5
...
afrobitcoi
Author | SHA1 | Date | |
---|---|---|---|
d7644c0fe7 | |||
bf9e2f47a3 | |||
a32257e5a2 | |||
f56ea60317 | |||
5bb7f158db | |||
4f49b5f1a0 | |||
25c30512ec | |||
52df7a5b89 | |||
1a85da27db | |||
10326a822e | |||
d1bf47a5c0 | |||
ccf9cfa332 | |||
773f8a9aea | |||
dd62e166a1 | |||
2fb72d5aa6 | |||
46f0818765 | |||
96569ae4aa | |||
f2b1e5f93e | |||
2326894a2b | |||
c15f02ddbf | |||
7708084331 | |||
696a414e95 | |||
c16dfb2dcb | |||
c979c4774c | |||
e82281d273 | |||
27c22d5e33 | |||
6acc545b66 | |||
609ec0989f | |||
b702621a04 | |||
89041a6744 | |||
c485c109e6 | |||
29a49d5f71 | |||
a5fafc4864 | |||
a921504bcf | |||
027154a4d3 | |||
bf1a1368ff | |||
097ffbf8a3 | |||
ec076d1560 | |||
8dadfa2111 | |||
c8ee6ead0b | |||
018e4c501d | |||
99a0b70cfa | |||
314a1352ec | |||
901e6be21e | |||
d58dde950e | |||
8ac18b74df | |||
2846c38ff5 | |||
d44efce225 | |||
d3dca7e808 | |||
41e3828eea | |||
9e76b4d28e | |||
ef03497350 | |||
e5a2aeb145 | |||
229a4ea56c | |||
f20e6d3768 | |||
1d210eb6e3 | |||
d8422a979f | |||
0cf6d39f02 | |||
076c20a3b7 | |||
0cfb0ba890 | |||
44a7e9387e | |||
e71954ee34 | |||
9cd9e84be6 | |||
25af9c4227 | |||
72a99bf9a6 | |||
f1228523cb | |||
a45d368115 | |||
16433dc183 | |||
0a956fdc73 | |||
75396f491b |
@ -31,7 +31,7 @@
|
||||
<None Include="icon.png" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="HtmlSanitizer" Version="5.0.372" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="8.0.723" />
|
||||
<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" />
|
||||
|
@ -20,6 +20,15 @@ namespace BTCPayServer.Abstractions.Extensions
|
||||
Relative
|
||||
}
|
||||
|
||||
public static void SetBlazorAllowed(this ViewDataDictionary viewData, bool allowed)
|
||||
{
|
||||
viewData["BlazorAllowed"] = allowed;
|
||||
}
|
||||
public static bool IsBlazorAllowed(this ViewDataDictionary viewData)
|
||||
{
|
||||
return viewData["BlazorAllowed"] is not false;
|
||||
}
|
||||
|
||||
public static void SetActivePage<T>(this ViewDataDictionary viewData, T activePage, string title = null, string activeId = null)
|
||||
where T : IConvertible
|
||||
{
|
||||
|
@ -105,31 +105,7 @@ public class Form
|
||||
}
|
||||
}
|
||||
|
||||
public void SetValues(JObject values)
|
||||
{
|
||||
var fields = GetAllFields().ToDictionary(k => k.FullName, k => k.Field);
|
||||
SetValues(fields, new List<string>(), values);
|
||||
}
|
||||
|
||||
private void SetValues(Dictionary<string, Field> fields, List<string> path, JObject values)
|
||||
{
|
||||
foreach (var prop in values.Properties())
|
||||
{
|
||||
List<string> propPath = new List<string>(path.Count + 1);
|
||||
propPath.AddRange(path);
|
||||
propPath.Add(prop.Name);
|
||||
if (prop.Value.Type == JTokenType.Object)
|
||||
{
|
||||
SetValues(fields, propPath, (JObject)prop.Value);
|
||||
}
|
||||
else if (prop.Value.Type == JTokenType.String)
|
||||
{
|
||||
var fullName = string.Join('_', propPath.Where(s => !string.IsNullOrEmpty(s)));
|
||||
if (fields.TryGetValue(fullName, out var f) && !f.Constant)
|
||||
f.Value = prop.Value.Value<string>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
using System.Web;
|
||||
using Ganss.XSS;
|
||||
using Ganss.Xss;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
||||
|
@ -28,6 +28,8 @@ namespace BTCPayServer.Client.Models
|
||||
public PosViewType DefaultView { get; set; }
|
||||
public bool ShowCustomAmount { get; set; } = false;
|
||||
public bool ShowDiscount { get; set; } = true;
|
||||
public bool ShowSearch { get; set; } = true;
|
||||
public bool ShowCategories { get; set; } = true;
|
||||
public bool EnableTips { get; set; } = true;
|
||||
public string CustomAmountPayButtonText { get; set; } = null;
|
||||
public string FixedAmountPayButtonText { get; set; } = null;
|
||||
|
@ -1,12 +1,12 @@
|
||||
namespace BTCPayServer.Client.Models
|
||||
namespace BTCPayServer.Client.Models;
|
||||
public enum InvoiceExceptionStatus
|
||||
{
|
||||
public enum InvoiceExceptionStatus
|
||||
{
|
||||
None,
|
||||
PaidLate,
|
||||
PaidPartial,
|
||||
Marked,
|
||||
Invalid,
|
||||
PaidOver
|
||||
}
|
||||
None,
|
||||
PaidLate,
|
||||
PaidPartial,
|
||||
Marked,
|
||||
Invalid,
|
||||
PaidOver
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,8 +1,12 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class LNURLPayPaymentMethodBaseData
|
||||
{
|
||||
public bool UseBech32Scheme { get; set; }
|
||||
|
||||
[JsonProperty("lud12Enabled")]
|
||||
public bool LUD12Enabled { get; set; }
|
||||
|
||||
public LNURLPayPaymentMethodBaseData()
|
||||
|
@ -16,11 +16,12 @@ namespace BTCPayServer.Client.Models
|
||||
{
|
||||
}
|
||||
|
||||
public LNURLPayPaymentMethodData(string cryptoCode, bool enabled, bool useBech32Scheme)
|
||||
public LNURLPayPaymentMethodData(string cryptoCode, bool enabled, bool useBech32Scheme, bool lud12Enabled)
|
||||
{
|
||||
Enabled = enabled;
|
||||
CryptoCode = cryptoCode;
|
||||
UseBech32Scheme = useBech32Scheme;
|
||||
LUD12Enabled = lud12Enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,8 @@ namespace BTCPayServer.Client.Models
|
||||
public string DefaultView { get; set; }
|
||||
public bool ShowCustomAmount { get; set; }
|
||||
public bool ShowDiscount { get; set; }
|
||||
public bool ShowSearch { get; set; }
|
||||
public bool ShowCategories { get; set; }
|
||||
public bool EnableTips { get; set; }
|
||||
public string Currency { get; set; }
|
||||
public object Items { get; set; }
|
||||
|
@ -73,6 +73,17 @@ namespace BTCPayServer.Client.Models
|
||||
|
||||
public bool PayJoinEnabled { get; set; }
|
||||
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public bool? AutoDetectLanguage { get; set; }
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public bool? ShowPayInWalletButton { get; set; }
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public bool? ShowStoreHeader { get; set; }
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public bool? CelebratePayment { get; set; }
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public bool? PlaySoundOnPayment { get; set; }
|
||||
|
||||
public InvoiceData.ReceiptOptions Receipt { get; set; }
|
||||
|
||||
|
||||
|
@ -19,7 +19,7 @@ namespace BTCPayServer
|
||||
"USDT_X = USDT_BTC * BTC_X",
|
||||
"USDT_BTC = bitfinex(UST_BTC)",
|
||||
},
|
||||
AssetId = new uint256("ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2"),
|
||||
AssetId = NetworkType == ChainName.Regtest? null: new uint256("ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2"),
|
||||
DisplayName = "Liquid Tether",
|
||||
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://liquid.network/tx/{0}" : "https://liquid.network/testnet/tx/{0}",
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
@ -42,7 +42,7 @@ namespace BTCPayServer
|
||||
"ETB_BTC = bitpay(ETB_BTC)"
|
||||
},
|
||||
Divisibility = 2,
|
||||
AssetId = new uint256("aa775044c32a7df391902b3659f46dfe004ccb2644ce2ddc7dba31e889391caf"),
|
||||
AssetId = NetworkType == ChainName.Regtest? null: new uint256("aa775044c32a7df391902b3659f46dfe004ccb2644ce2ddc7dba31e889391caf"),
|
||||
DisplayName = "Ethiopian Birr",
|
||||
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://liquid.network/tx/{0}" : "https://liquid.network/testnet/tx/{0}",
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
@ -65,7 +65,7 @@ namespace BTCPayServer
|
||||
"LCAD_BTC = bylls(CAD_BTC)",
|
||||
"CAD_BTC = LCAD_BTC"
|
||||
},
|
||||
AssetId = new uint256("0e99c1a6da379d1f4151fb9df90449d40d0608f6cb33a5bcbfc8c265f42bab0a"),
|
||||
AssetId = NetworkType == ChainName.Regtest? null: new uint256("0e99c1a6da379d1f4151fb9df90449d40d0608f6cb33a5bcbfc8c265f42bab0a"),
|
||||
DisplayName = "Liquid CAD",
|
||||
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://liquid.network/tx/{0}" : "https://liquid.network/testnet/tx/{0}",
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
|
@ -19,7 +19,8 @@ namespace BTCPayServer
|
||||
NewTransactionEvent evtOutputs)
|
||||
{
|
||||
return evtOutputs.Outputs.Where(output =>
|
||||
output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId).Select(output =>
|
||||
(output.Value is not AssetMoney && NetworkCryptoCode.Equals(evtOutputs.CryptoCode, StringComparison.InvariantCultureIgnoreCase)) ||
|
||||
(output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId)).Select(output =>
|
||||
{
|
||||
var outpoint = new OutPoint(evtOutputs.TransactionData.TransactionHash, output.Index);
|
||||
return (output, outpoint);
|
||||
|
@ -1,37 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace BTCPayServer.Rating
|
||||
{
|
||||
public enum RateSource
|
||||
{
|
||||
Coingecko,
|
||||
Direct
|
||||
}
|
||||
public class AvailableRateProvider
|
||||
{
|
||||
public string Name { get; }
|
||||
public string Url { get; }
|
||||
public string Id { get; }
|
||||
public RateSource Source { get; }
|
||||
|
||||
public AvailableRateProvider(string id, string name, string url) : this(id, name, url, RateSource.Direct)
|
||||
{
|
||||
|
||||
}
|
||||
public AvailableRateProvider(string id, string name, string url, RateSource source)
|
||||
{
|
||||
Id = id;
|
||||
Name = name;
|
||||
Url = url;
|
||||
Source = source;
|
||||
}
|
||||
|
||||
public string DisplayName =>
|
||||
Source switch
|
||||
{
|
||||
RateSource.Direct => Name,
|
||||
RateSource.Coingecko => $"{Name} (via CoinGecko)",
|
||||
_ => throw new NotSupportedException(Source.ToString())
|
||||
};
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.9" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.24" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="1.0.2" />
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="1.0.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -20,13 +20,13 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
public class CurrencyNameTable
|
||||
{
|
||||
public static CurrencyNameTable Instance = new CurrencyNameTable();
|
||||
public static CurrencyNameTable Instance = new();
|
||||
public CurrencyNameTable()
|
||||
{
|
||||
_Currencies = LoadCurrency().ToDictionary(k => k.Code);
|
||||
_Currencies = LoadCurrency().ToDictionary(k => k.Code, StringComparer.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
static readonly Dictionary<string, IFormatProvider> _CurrencyProviders = new Dictionary<string, IFormatProvider>();
|
||||
static readonly Dictionary<string, IFormatProvider> _CurrencyProviders = new();
|
||||
|
||||
public NumberFormatInfo GetNumberFormatInfo(string currency, bool useFallback)
|
||||
{
|
||||
|
@ -19,7 +19,7 @@ namespace BTCPayServer.Rating
|
||||
public static CurrencyPair Parse(string str)
|
||||
{
|
||||
if (!TryParse(str, out var result))
|
||||
throw new FormatException("Invalid currency pair");
|
||||
throw new FormatException($"Invalid currency pair ({str})");
|
||||
return result;
|
||||
}
|
||||
public static bool TryParse(string str, out CurrencyPair value)
|
||||
|
@ -1,29 +0,0 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class ArgoneumRateProvider : IRateProvider
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
public ArgoneumRateProvider(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient ?? new HttpClient();
|
||||
}
|
||||
|
||||
public RateSourceInfo RateSourceInfo => new("argoneum", "Argoneum", "https://rates.argoneum.net/rates");
|
||||
|
||||
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// Example result: AGM to BTC rate: {"agm":5000000.000000}
|
||||
var response = await _httpClient.GetAsync("https://rates.argoneum.net/rates/btc", cancellationToken);
|
||||
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
|
||||
var value = jobj["agm"].Value<decimal>();
|
||||
return new[] { new PairRate(new CurrencyPair("BTC", "AGM"), new BidAsk(value)) };
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
@ -1,40 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Services.Rates;
|
||||
|
||||
|
||||
public class ExchangeRateHostRateProvider : IRateProvider
|
||||
{
|
||||
public RateSourceInfo RateSourceInfo => new("exchangeratehost", "Yadio", "https://api.exchangerate.host/latest?base=BTC");
|
||||
private readonly HttpClient _httpClient;
|
||||
public ExchangeRateHostRateProvider(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient ?? new HttpClient();
|
||||
}
|
||||
|
||||
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await _httpClient.GetAsync(RateSourceInfo.Url, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
|
||||
if(jobj["success"].Value<bool>() is not true || !jobj["base"].Value<string>().Equals("BTC", StringComparison.InvariantCulture))
|
||||
throw new Exception("exchangerate.host returned a non success response or the base currency was not the requested one (BTC)");
|
||||
var results = (JObject) jobj["rates"] ;
|
||||
//key value is currency code to rate value
|
||||
var list = new List<PairRate>();
|
||||
foreach (var item in results)
|
||||
{
|
||||
string name = item.Key;
|
||||
var value = item.Value.Value<decimal>();
|
||||
list.Add(new PairRate(new CurrencyPair("BTC", name), new BidAsk(value)));
|
||||
}
|
||||
|
||||
return list.ToArray();
|
||||
}
|
||||
}
|
@ -13,7 +13,7 @@ namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class RipioExchangeProvider : IRateProvider
|
||||
{
|
||||
public RateSourceInfo RateSourceInfo => new("ripio", "Ripio", "https://api.exchange.ripio.com/api/v1/rate/all/");
|
||||
public RateSourceInfo RateSourceInfo => new("ripio", "Ripio", "https://api.ripiotrade.co/v4/public/tickers");
|
||||
private readonly HttpClient _httpClient;
|
||||
public RipioExchangeProvider(HttpClient httpClient)
|
||||
{
|
||||
@ -21,9 +21,9 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await _httpClient.GetAsync("https://api.exchange.ripio.com/api/v1/rate/all/", cancellationToken);
|
||||
var response = await _httpClient.GetAsync("https://api.ripiotrade.co/v4/public/tickers", cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var jarray = (JArray)(await response.Content.ReadAsAsync<JArray>(cancellationToken));
|
||||
var jarray = (JArray)(await response.Content.ReadAsAsync<JObject>(cancellationToken))["data"];
|
||||
return jarray
|
||||
.Children<JObject>()
|
||||
.Select(jobj => ParsePair(jobj))
|
||||
|
@ -1,21 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Rating
|
||||
#nullable enable
|
||||
namespace BTCPayServer.Rating;
|
||||
public enum RateSource
|
||||
{
|
||||
public class RateSourceInfo
|
||||
{
|
||||
public RateSourceInfo(string id, string displayName, string url)
|
||||
{
|
||||
Id = id;
|
||||
DisplayName = displayName;
|
||||
Url = url;
|
||||
}
|
||||
public string Id { get; set; }
|
||||
public string DisplayName { get; set; }
|
||||
public string Url { get; set; }
|
||||
}
|
||||
Coingecko,
|
||||
Direct
|
||||
}
|
||||
public record RateSourceInfo(string Id, string DisplayName, string Url, RateSource Source = RateSource.Direct);
|
||||
|
@ -85,14 +85,13 @@ namespace BTCPayServer.Services.Rates
|
||||
bgFetcher.RefreshRate = TimeSpan.FromMinutes(1.0);
|
||||
bgFetcher.ValidatyTime = TimeSpan.FromMinutes(5.0);
|
||||
Providers.Add(supportedExchange.Id, bgFetcher);
|
||||
var rsi = coingecko.RateSourceInfo;
|
||||
AvailableRateProviders.Add(new(rsi.Id, rsi.DisplayName, rsi.Url, RateSource.Coingecko));
|
||||
AvailableRateProviders.Add(coingecko.RateSourceInfo);
|
||||
}
|
||||
}
|
||||
AvailableRateProviders.Sort((a, b) => StringComparer.Ordinal.Compare(a.DisplayName, b.DisplayName));
|
||||
}
|
||||
|
||||
public List<AvailableRateProvider> AvailableRateProviders { get; } = new List<AvailableRateProvider>();
|
||||
public List<RateSourceInfo> AvailableRateProviders { get; } = new List<RateSourceInfo>();
|
||||
|
||||
public async Task<QueryRateResult> QueryRates(string exchangeName, CancellationToken cancellationToken)
|
||||
{
|
||||
|
@ -52,11 +52,12 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
tester.ActivateLBTC();
|
||||
await tester.StartAsync();
|
||||
|
||||
//https://github.com/ElementsProject/elements/issues/956
|
||||
await tester.LBTCExplorerNode.SendCommandAsync("rescanblockchain");
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("LBTC");
|
||||
user.RegisterDerivationScheme("USDT");
|
||||
user.RegisterDerivationScheme("ETB");
|
||||
await user.GrantAccessAsync();
|
||||
|
||||
await tester.LBTCExplorerNode.GenerateAsync(4);
|
||||
//no tether on our regtest, lets create it and set it
|
||||
var tether = tester.NetworkProvider.GetNetwork<ElementsBTCPayNetwork>("USDT");
|
||||
@ -75,6 +76,10 @@ namespace BTCPayServer.Tests
|
||||
.AssetId = etb.AssetId;
|
||||
|
||||
|
||||
user.RegisterDerivationScheme("LBTC");
|
||||
user.RegisterDerivationScheme("USDT");
|
||||
user.RegisterDerivationScheme("ETB");
|
||||
|
||||
//test: register 2 assets on the same elements network and make sure paying an invoice on one does not affect the other in any way
|
||||
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(0.1m, "BTC"));
|
||||
Assert.Equal(3, invoice.SupportedTransactionCurrencies.Count);
|
||||
@ -82,7 +87,7 @@ namespace BTCPayServer.Tests
|
||||
//1 lbtc = 1 btc
|
||||
Assert.Equal(1, ci.Rate);
|
||||
var star = await tester.LBTCExplorerNode.SendCommandAsync("sendtoaddress", ci.Address, ci.Due, "", "", false, true,
|
||||
1, "UNSET", lbtc.AssetId);
|
||||
1, "UNSET",false, lbtc.AssetId.ToString());
|
||||
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
@ -95,8 +100,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
ci = invoice.CryptoInfo.Single(info => info.CryptoCode.Equals("USDT"));
|
||||
Assert.Equal(3, invoice.SupportedTransactionCurrencies.Count);
|
||||
star = await tester.LBTCExplorerNode.SendCommandAsync("sendtoaddress", ci.Address, ci.Due, "", "", false, true,
|
||||
1, "UNSET", tether.AssetId);
|
||||
star = tester.LBTCExplorerNode.SendCommand("sendtoaddress", ci.Address, decimal.Parse(ci.Due), "x", "z", false, true, 1, "unset", false, tether.AssetId.ToString());
|
||||
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
|
@ -23,7 +23,7 @@
|
||||
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14" />
|
||||
<PackageReference Include="Selenium.Support" Version="4.1.1" />
|
||||
<PackageReference Include="Selenium.WebDriver" Version="4.1.1" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="116.0.5845.9600" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="119.0.6045.10500" />
|
||||
<PackageReference Include="xunit" Version="2.4.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
using BTCPayServer.Forms;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@ -10,16 +11,25 @@ using Xunit.Abstractions;
|
||||
|
||||
namespace BTCPayServer.Tests;
|
||||
|
||||
[Trait("Fast", "Fast")]
|
||||
[Collection(nameof(NonParallelizableCollectionDefinition))]
|
||||
[Trait("Integration", "Integration")]
|
||||
public class FormTests : UnitTestBase
|
||||
{
|
||||
public FormTests(ITestOutputHelper helper) : base(helper)
|
||||
{
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanParseForm()
|
||||
|
||||
[Fact(Timeout = TestUtils.TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanParseForm()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
var service = tester.PayTester.GetService<FormDataService>();
|
||||
|
||||
var form = new Form()
|
||||
{
|
||||
Fields = new List<Field>
|
||||
@ -40,8 +50,6 @@ public class FormTests : UnitTestBase
|
||||
}
|
||||
}
|
||||
};
|
||||
var providers = new FormComponentProviders(new List<IFormComponentProvider>());
|
||||
var service = new FormDataService(null, providers);
|
||||
Assert.False(service.IsFormSchemaValid(form.ToString(), out _, out _));
|
||||
form = new Form
|
||||
{
|
||||
@ -164,7 +172,7 @@ public class FormTests : UnitTestBase
|
||||
Assert.Equal("original", obj["invoice"]["test"].Value<string>());
|
||||
Assert.Equal("updated", obj["invoice_item3"].Value<string>());
|
||||
Clear(form);
|
||||
form.SetValues(obj);
|
||||
service.SetValues(form, obj);
|
||||
obj = service.GetValues(form);
|
||||
Assert.Equal("original", obj["invoice"]["test"].Value<string>());
|
||||
Assert.Equal("updated", obj["invoice_item3"].Value<string>());
|
||||
@ -182,10 +190,12 @@ public class FormTests : UnitTestBase
|
||||
}
|
||||
}
|
||||
};
|
||||
form.SetValues(obj);
|
||||
|
||||
service.SetValues(form, obj);
|
||||
obj = service.GetValues(form);
|
||||
Assert.Null(obj["test"].Value<string>());
|
||||
form.SetValues(new JObject { ["test"] = "hello" });
|
||||
|
||||
service.SetValues(form, new JObject { ["test"] = "hello" });
|
||||
obj = service.GetValues(form);
|
||||
Assert.Equal("hello", obj["test"].Value<string>());
|
||||
}
|
||||
|
@ -1457,7 +1457,7 @@ namespace BTCPayServer.Tests
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanUseWebhooks()
|
||||
{
|
||||
void AssertHook(FakeServer fakeServer, Client.Models.StoreWebhookData hook)
|
||||
void AssertHook(FakeServer fakeServer, StoreWebhookData hook)
|
||||
{
|
||||
Assert.True(hook.Enabled);
|
||||
Assert.True(hook.AuthorizedEvents.Everything);
|
||||
|
@ -92,9 +92,8 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("App updated", s.FindAlertMessage().Text);
|
||||
|
||||
s.Driver.FindElement(By.Id("ViewApp")).Click();
|
||||
var windows = s.Driver.WindowHandles;
|
||||
Assert.Equal(2, windows.Count);
|
||||
s.Driver.SwitchTo().Window(windows[1]);
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
|
||||
|
||||
s.Driver.FindElement(By.CssSelector("button[type='submit']")).Click();
|
||||
|
||||
Assert.Contains("Enter your email", s.Driver.PageSource);
|
||||
@ -103,6 +102,9 @@ namespace BTCPayServer.Tests
|
||||
|
||||
s.PayInvoice(true);
|
||||
var invoiceId = s.Driver.Url[(s.Driver.Url.LastIndexOf("/", StringComparison.Ordinal) + 1)..];
|
||||
s.Driver.Close();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
|
||||
|
||||
s.GoToInvoice(invoiceId);
|
||||
Assert.Contains("aa@aa.com", s.Driver.PageSource);
|
||||
|
||||
@ -116,13 +118,19 @@ namespace BTCPayServer.Tests
|
||||
|
||||
s.Driver.FindElement(By.XPath("//a[starts-with(@id, 'Edit-')]")).Click();
|
||||
var editUrl = s.Driver.Url;
|
||||
|
||||
s.Driver.FindElement(By.Id("ViewPaymentRequest")).Click();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
|
||||
|
||||
s.Driver.FindElement(By.CssSelector("[data-test='form-button']")).Click();
|
||||
Assert.Contains("Enter your email", s.Driver.PageSource);
|
||||
|
||||
s.Driver.FindElement(By.Name("buyerEmail")).SendKeys("aa@aa.com");
|
||||
s.Driver.FindElement(By.CssSelector("input[type='submit']")).Click();
|
||||
invoiceId = s.Driver.Url.Split('/').Last();
|
||||
s.Driver.Close();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
|
||||
|
||||
s.Driver.Navigate().GoToUrl(editUrl);
|
||||
Assert.Contains("aa@aa.com", s.Driver.PageSource);
|
||||
var invoice = await s.Server.PayTester.GetService<InvoiceRepository>().GetInvoice(invoiceId);
|
||||
@ -196,7 +204,6 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("StoreNav-PaymentRequests")).Click();
|
||||
s.Driver.FindElement(By.Id("CreatePaymentRequest")).Click();
|
||||
Assert.Equal(4, new SelectElement(s.Driver.FindElement(By.Id("FormId"))).Options.Count);
|
||||
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
@ -216,8 +223,7 @@ namespace BTCPayServer.Tests
|
||||
s.GoToInvoices(s.StoreId);
|
||||
}
|
||||
// Let's CPFP from the invoices page
|
||||
s.Driver.SetCheckbox(By.Id("selectAllCheckbox"), true);
|
||||
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
|
||||
s.Driver.SetCheckbox(By.CssSelector(".mass-action-select-all"), true);
|
||||
s.Driver.FindElement(By.Id("BumpFee")).Click();
|
||||
s.Driver.FindElement(By.Id("BroadcastTransaction")).Click();
|
||||
s.FindAlertMessage();
|
||||
@ -225,16 +231,14 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// CPFP again should fail because all invoices got bumped
|
||||
s.GoToInvoices();
|
||||
s.Driver.SetCheckbox(By.Id("selectAllCheckbox"), true);
|
||||
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
|
||||
s.Driver.SetCheckbox(By.CssSelector(".mass-action-select-all"), true);
|
||||
s.Driver.FindElement(By.Id("BumpFee")).Click();
|
||||
Assert.Contains($"/stores/{s.StoreId}/invoices", s.Driver.Url);
|
||||
Assert.Contains("any UTXO available", s.FindAlertMessage(StatusMessageModel.StatusSeverity.Error).Text);
|
||||
|
||||
// But we should be able to bump from the wallet's page
|
||||
s.GoToWallet(navPages: WalletsNavPages.Transactions);
|
||||
s.Driver.SetCheckbox(By.Id("selectAllCheckbox"), true);
|
||||
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
|
||||
s.Driver.SetCheckbox(By.CssSelector(".mass-action-select-all"), true);
|
||||
s.Driver.FindElement(By.Id("BumpFee")).Click();
|
||||
s.Driver.FindElement(By.Id("BroadcastTransaction")).Click();
|
||||
Assert.Contains($"/wallets/{s.WalletId}", s.Driver.Url);
|
||||
@ -556,24 +560,24 @@ namespace BTCPayServer.Tests
|
||||
s.AddDerivationScheme();
|
||||
s.GoToInvoices();
|
||||
s.CreateInvoice();
|
||||
s.Driver.FindElement(By.Id("markStatusDropdownMenuButton")).Click();
|
||||
s.Driver.FindElements(By.ClassName("changeInvoiceState"))[0].Click();
|
||||
s.Driver.FindElement(By.CssSelector("[data-invoice-state-badge] .dropdown-toggle")).Click();
|
||||
s.Driver.FindElements(By.CssSelector("[data-invoice-state-badge] .dropdown-menu button"))[0].Click();
|
||||
TestUtils.Eventually(() => Assert.Contains("Invalid (marked)", s.Driver.PageSource));
|
||||
s.Driver.Navigate().Refresh();
|
||||
|
||||
s.Driver.FindElement(By.Id("markStatusDropdownMenuButton")).Click();
|
||||
s.Driver.FindElements(By.ClassName("changeInvoiceState"))[0].Click();
|
||||
s.Driver.FindElement(By.CssSelector("[data-invoice-state-badge] .dropdown-toggle")).Click();
|
||||
s.Driver.FindElements(By.CssSelector("[data-invoice-state-badge] .dropdown-menu button"))[0].Click();
|
||||
TestUtils.Eventually(() => Assert.Contains("Settled (marked)", s.Driver.PageSource));
|
||||
|
||||
s.Driver.Navigate().Refresh();
|
||||
|
||||
s.Driver.FindElement(By.Id("markStatusDropdownMenuButton")).Click();
|
||||
s.Driver.FindElements(By.ClassName("changeInvoiceState"))[0].Click();
|
||||
s.Driver.FindElement(By.CssSelector("[data-invoice-state-badge] .dropdown-toggle")).Click();
|
||||
s.Driver.FindElements(By.CssSelector("[data-invoice-state-badge] .dropdown-menu button"))[0].Click();
|
||||
TestUtils.Eventually(() => Assert.Contains("Invalid (marked)", s.Driver.PageSource));
|
||||
s.Driver.Navigate().Refresh();
|
||||
|
||||
s.Driver.FindElement(By.Id("markStatusDropdownMenuButton")).Click();
|
||||
s.Driver.FindElements(By.ClassName("changeInvoiceState"))[0].Click();
|
||||
s.Driver.FindElement(By.CssSelector("[data-invoice-state-badge] .dropdown-toggle")).Click();
|
||||
s.Driver.FindElements(By.CssSelector("[data-invoice-state-badge] .dropdown-menu button"))[0].Click();
|
||||
TestUtils.Eventually(() => Assert.Contains("Settled (marked)", s.Driver.PageSource));
|
||||
}
|
||||
|
||||
@ -730,9 +734,8 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains(invoiceId, s.Driver.PageSource);
|
||||
|
||||
// archive via list
|
||||
s.Driver.FindElement(By.CssSelector($".selector[value=\"{invoiceId}\"]")).Click();
|
||||
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
|
||||
s.Driver.FindElement(By.Id("ActionsDropdownArchive")).Click();
|
||||
s.Driver.FindElement(By.CssSelector($".mass-action-select[value=\"{invoiceId}\"]")).Click();
|
||||
s.Driver.FindElement(By.Id("ArchiveSelected")).Click();
|
||||
Assert.Contains("1 invoice archived", s.FindAlertMessage().Text);
|
||||
Assert.DoesNotContain(invoiceId, s.Driver.PageSource);
|
||||
|
||||
@ -740,9 +743,8 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("StatusOptionsToggle")).Click();
|
||||
s.Driver.FindElement(By.Id("StatusOptionsIncludeArchived")).Click();
|
||||
Assert.Contains(invoiceId, s.Driver.PageSource);
|
||||
s.Driver.FindElement(By.CssSelector($".selector[value=\"{invoiceId}\"]")).Click();
|
||||
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
|
||||
s.Driver.FindElement(By.Id("ActionsDropdownUnarchive")).Click();
|
||||
s.Driver.FindElement(By.CssSelector($".mass-action-select[value=\"{invoiceId}\"]")).Click();
|
||||
s.Driver.FindElement(By.Id("UnarchiveSelected")).Click();
|
||||
Assert.Contains("1 invoice unarchived", s.FindAlertMessage().Text);
|
||||
Assert.Contains(invoiceId, s.Driver.PageSource);
|
||||
|
||||
@ -989,13 +991,12 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("App successfully created", s.FindAlertMessage().Text);
|
||||
|
||||
s.Driver.FindElement(By.CssSelector("label[for='DefaultView_Cart']")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(".template-item:nth-of-type(1) .btn-primary")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(".template-item:nth-of-type(1)")).Click();
|
||||
s.Driver.FindElement(By.Id("BuyButtonText")).SendKeys("Take my money");
|
||||
s.Driver.FindElement(By.Id("EditorCategories-ts-control")).SendKeys("Drinks");
|
||||
s.Driver.FindElement(By.Id("SaveItemChanges")).Click();
|
||||
s.Driver.FindElement(By.Id("ToggleRawEditor")).Click();
|
||||
s.Driver.FindElement(By.Id("ApplyItemChanges")).Click();
|
||||
|
||||
var template = s.Driver.FindElement(By.Id("Template")).GetAttribute("value");
|
||||
var template = s.Driver.FindElement(By.Id("TemplateConfig")).GetAttribute("value");
|
||||
Assert.Contains("\"buyButtonText\": \"Take my money\"", template);
|
||||
Assert.Matches("\"categories\": \\[\n\\s+\"Drinks\"\n\\s+\\]", template);
|
||||
|
||||
@ -1107,7 +1108,7 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("Title")).SendKeys("Kukkstarter");
|
||||
s.Driver.FindElement(By.CssSelector("div.note-editable.card-block")).SendKeys("1BTC = 1BTC");
|
||||
s.Driver.FindElement(By.Id("TargetCurrency")).Clear();
|
||||
s.Driver.FindElement(By.Id("TargetCurrency")).SendKeys("JPY");
|
||||
s.Driver.FindElement(By.Id("TargetCurrency")).SendKeys("EUR");
|
||||
s.Driver.FindElement(By.Id("TargetAmount")).SendKeys("700");
|
||||
|
||||
// test wrong dates
|
||||
@ -1122,7 +1123,8 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).Click();
|
||||
Assert.Contains("App updated", s.FindAlertMessage().Text);
|
||||
var appId = s.Driver.Url.Split('/')[4];
|
||||
|
||||
|
||||
// CHeck public page
|
||||
s.Driver.FindElement(By.Id("ViewApp")).Click();
|
||||
var windows = s.Driver.WindowHandles;
|
||||
Assert.Equal(2, windows.Count);
|
||||
@ -1132,6 +1134,26 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal("Currently active!",
|
||||
s.Driver.FindElement(By.CssSelector("[data-test='time-state']")).Text);
|
||||
|
||||
// Contribute
|
||||
s.Driver.FindElement(By.Id("crowdfund-body-header-cta")).Click();
|
||||
Thread.Sleep(1000);
|
||||
s.Driver.WaitUntilAvailable(By.Name("btcpay"));
|
||||
|
||||
var frameElement = s.Driver.FindElement(By.Name("btcpay"));
|
||||
Assert.True(frameElement.Displayed);
|
||||
var iframe = s.Driver.SwitchTo().Frame(frameElement);
|
||||
iframe.WaitUntilAvailable(By.Id("Checkout-v2"));
|
||||
|
||||
IWebElement closebutton = null;
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
closebutton = iframe.FindElement(By.Id("close"));
|
||||
Assert.True(closebutton.Displayed);
|
||||
});
|
||||
closebutton.Click();
|
||||
s.Driver.AssertElementNotFound(By.Name("btcpay"));
|
||||
|
||||
// Back to admin view
|
||||
s.Driver.Close();
|
||||
s.Driver.SwitchTo().Window(windows[0]);
|
||||
|
||||
@ -1179,13 +1201,13 @@ namespace BTCPayServer.Tests
|
||||
var editUrl = s.Driver.Url;
|
||||
|
||||
s.Driver.FindElement(By.Id("ViewPaymentRequest")).Click();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
|
||||
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.Id("PayInvoice")).Text.Trim());
|
||||
s.Driver.Close();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
|
||||
|
||||
// expire
|
||||
s.GoToUrl(editUrl);
|
||||
s.Driver.ExecuteJavaScript("document.getElementById('ExpiryDate').value = '2021-01-21T21:00:00.000Z'");
|
||||
s.Driver.FindElement(By.Id("SaveButton")).Click();
|
||||
s.Driver.FindElement(By.XPath("//a[starts-with(@id, 'Edit-')]")).Click();
|
||||
@ -1205,12 +1227,28 @@ namespace BTCPayServer.Tests
|
||||
Assert.True(s.Driver.FindElement(By.Id("Currency")).Enabled);
|
||||
|
||||
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.Id("PayInvoice")).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()");
|
||||
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
|
||||
// test invoice creation
|
||||
s.Driver.FindElement(By.Id("PayInvoice")).Click();
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
s.Driver.WaitUntilAvailable(By.Name("btcpay"));
|
||||
|
||||
var frameElement = s.Driver.FindElement(By.Name("btcpay"));
|
||||
Assert.True(frameElement.Displayed);
|
||||
var iframe = s.Driver.SwitchTo().Frame(frameElement);
|
||||
iframe.WaitUntilAvailable(By.Id("Checkout-v2"));
|
||||
|
||||
IWebElement closebutton = null;
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
closebutton = iframe.FindElement(By.Id("close"));
|
||||
Assert.True(closebutton.Displayed);
|
||||
});
|
||||
closebutton.Click();
|
||||
s.Driver.AssertElementNotFound(By.Name("btcpay"));
|
||||
});
|
||||
|
||||
// amount and currency should not be editable, because invoice exists
|
||||
s.GoToUrl(editUrl);
|
||||
@ -1234,32 +1272,45 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// payment
|
||||
s.GoToUrl(viewUrl);
|
||||
s.Driver.ExecuteJavaScript("document.querySelector('[data-test=\"pay-button\"]').click()");
|
||||
|
||||
// Pay full amount
|
||||
s.PayInvoice();
|
||||
|
||||
// Processing
|
||||
s.Driver.FindElement(By.Id("PayInvoice")).Click();
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
var processingSection = s.Driver.WaitForElement(By.Id("processing"));
|
||||
Assert.True(processingSection.Displayed);
|
||||
Assert.Contains("Payment Received", processingSection.Text);
|
||||
Assert.Contains("Your payment has been received and is now processing", processingSection.Text);
|
||||
});
|
||||
s.Driver.WaitUntilAvailable(By.Name("btcpay"));
|
||||
|
||||
var frameElement = s.Driver.FindElement(By.Name("btcpay"));
|
||||
Assert.True(frameElement.Displayed);
|
||||
var iframe = s.Driver.SwitchTo().Frame(frameElement);
|
||||
iframe.WaitUntilAvailable(By.Id("Checkout-v2"));
|
||||
|
||||
// Pay full amount
|
||||
s.PayInvoice();
|
||||
|
||||
// Processing
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
var processingSection = s.Driver.WaitForElement(By.Id("processing"));
|
||||
Assert.True(processingSection.Displayed);
|
||||
Assert.Contains("Payment Received", processingSection.Text);
|
||||
Assert.Contains("Your payment has been received and is now processing", processingSection.Text);
|
||||
});
|
||||
|
||||
s.GoToUrl(viewUrl);
|
||||
Assert.Equal("Processing", s.Driver.WaitForElement(By.CssSelector("[data-test='status']")).Text);
|
||||
s.Driver.Navigate().Back();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles[0]);
|
||||
Assert.Equal("Processing", s.Driver.WaitForElement(By.CssSelector("[data-test='status']")).Text);
|
||||
s.Driver.SwitchTo().Frame(frameElement);
|
||||
|
||||
// Mine
|
||||
s.MineBlockOnInvoiceCheckout();
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
Assert.Contains("Mined 1 block",
|
||||
s.Driver.WaitForElement(By.Id("CheatSuccessMessage")).Text);
|
||||
// Mine
|
||||
s.MineBlockOnInvoiceCheckout();
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
Assert.Contains("Mined 1 block",
|
||||
s.Driver.WaitForElement(By.Id("CheatSuccessMessage")).Text);
|
||||
});
|
||||
|
||||
s.Driver.FindElement(By.Id("close")).Click();
|
||||
s.Driver.AssertElementNotFound(By.Name("btcpay"));
|
||||
});
|
||||
s.GoToUrl(viewUrl);
|
||||
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles[0]);
|
||||
Assert.Equal("Settled", s.Driver.WaitForElement(By.CssSelector("[data-test='status']")).Text);
|
||||
}
|
||||
|
||||
@ -1356,7 +1407,7 @@ namespace BTCPayServer.Tests
|
||||
TestLogs.LogInformation("Let's try to update one of them");
|
||||
s.Driver.FindElement(By.LinkText("Modify")).Click();
|
||||
|
||||
using FakeServer server = new FakeServer();
|
||||
using var server = new FakeServer();
|
||||
await server.Start();
|
||||
s.Driver.FindElement(By.Name("PayloadUrl")).Clear();
|
||||
s.Driver.FindElement(By.Name("PayloadUrl")).SendKeys(server.ServerUri.AbsoluteUri);
|
||||
@ -1398,7 +1449,7 @@ namespace BTCPayServer.Tests
|
||||
server.Done();
|
||||
|
||||
TestLogs.LogInformation("Let's make a failed event");
|
||||
s.CreateInvoice();
|
||||
var invoiceId = s.CreateInvoice();
|
||||
request = await server.GetNextRequest();
|
||||
request.Response.StatusCode = 404;
|
||||
server.Done();
|
||||
@ -1423,7 +1474,7 @@ namespace BTCPayServer.Tests
|
||||
CanBrowseContent(s);
|
||||
|
||||
s.GoToInvoices();
|
||||
s.Driver.FindElement(By.LinkText("Details")).Click();
|
||||
s.Driver.FindElement(By.LinkText(invoiceId)).Click();
|
||||
CanBrowseContent(s);
|
||||
var element = s.Driver.FindElement(By.ClassName("redeliver"));
|
||||
element.Click();
|
||||
@ -1676,13 +1727,14 @@ namespace BTCPayServer.Tests
|
||||
// no previous page in the wizard, hence no back button
|
||||
Assert.True(s.Driver.ElementDoesNotExist(By.Id("GoBack")));
|
||||
s.Driver.FindElement(By.Id("CancelWizard")).Click();
|
||||
Assert.Equal(settingsUri.ToString(), s.Driver.Url);
|
||||
|
||||
// Transactions list contains export and action, ensure functions are present.
|
||||
Assert.Equal(settingsUri.ToString(), s.Driver.Url);
|
||||
|
||||
// Transactions list contains export, ensure functions are present.
|
||||
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
|
||||
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
|
||||
|
||||
s.Driver.FindElement(By.ClassName("mass-action-select-all")).Click();
|
||||
s.Driver.FindElement(By.Id("BumpFee"));
|
||||
|
||||
|
||||
// JSON export
|
||||
s.Driver.FindElement(By.Id("ExportDropdownToggle")).Click();
|
||||
s.Driver.FindElement(By.Id("ExportJSON")).Click();
|
||||
@ -1693,20 +1745,15 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("\"Amount\": \"3.00000000\"", s.Driver.PageSource);
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
|
||||
|
||||
// BIP-329 export
|
||||
s.Driver.FindElement(By.Id("ExportDropdownToggle")).Click();
|
||||
s.Driver.FindElement(By.Id("ExportBIP329")).Click();
|
||||
Thread.Sleep(1000);
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
|
||||
Assert.Contains(s.WalletId.ToString(), s.Driver.Url);
|
||||
Assert.EndsWith("export?format=bip329", s.Driver.Url);
|
||||
Assert.Contains("{\"type\":\"tx\",\"ref\":\"", s.Driver.PageSource);
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
|
||||
|
||||
// CSV export
|
||||
s.Driver.FindElement(By.Id("ExportDropdownToggle")).Click();
|
||||
s.Driver.FindElement(By.Id("ExportCSV")).Click();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
|
||||
|
||||
// BIP-329 export
|
||||
s.Driver.FindElement(By.Id("ExportDropdownToggle")).Click();
|
||||
s.Driver.FindElement(By.Id("ExportBIP329")).Click();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
@ -1754,7 +1801,12 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("Amount")).Clear();
|
||||
s.Driver.FindElement(By.Id("Amount")).SendKeys("99.0");
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
|
||||
s.Driver.FindElement(By.LinkText("View")).Click();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
|
||||
Assert.Contains("PP1", s.Driver.PageSource);
|
||||
s.Driver.Close();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
|
||||
|
||||
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
|
||||
|
||||
@ -1765,9 +1817,9 @@ namespace BTCPayServer.Tests
|
||||
var description = s.Driver.FindElement(By.ClassName("card-block"));
|
||||
description.SendKeys("Description Edit");
|
||||
s.Driver.FindElement(By.Id("SaveButton")).Click();
|
||||
|
||||
s.Driver.FindElement(By.LinkText("PP1 Edited")).Click();
|
||||
|
||||
|
||||
s.Driver.FindElement(By.LinkText("View")).Click();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
|
||||
Assert.Contains("Description Edit", s.Driver.PageSource);
|
||||
Assert.Contains("PP1 Edited", s.Driver.PageSource);
|
||||
}
|
||||
@ -1793,7 +1845,12 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("Amount")).Clear();
|
||||
s.Driver.FindElement(By.Id("Amount")).SendKeys("99.0");
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
|
||||
s.Driver.FindElement(By.LinkText("View")).Click();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
|
||||
Assert.Contains("PP1", s.Driver.PageSource);
|
||||
s.Driver.Close();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
|
||||
|
||||
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
|
||||
|
||||
@ -1805,6 +1862,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// This should select the first View, ie, the last one PP2
|
||||
s.Driver.FindElement(By.LinkText("View")).Click();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
|
||||
var address = await s.Server.ExplorerNode.GetNewAddressAsync();
|
||||
s.Driver.FindElement(By.Id("Destination")).SendKeys(address.ToString());
|
||||
s.Driver.FindElement(By.Id("ClaimedAmount")).Clear();
|
||||
@ -1826,6 +1884,9 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("Awaiting Approval", s.Driver.PageSource);
|
||||
|
||||
var viewPullPaymentUrl = s.Driver.Url;
|
||||
s.Driver.Close();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
|
||||
|
||||
// This one should have nothing
|
||||
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
|
||||
var payouts = s.Driver.FindElements(By.ClassName("pp-payout"));
|
||||
@ -1838,8 +1899,7 @@ namespace BTCPayServer.Tests
|
||||
payouts[0].Click();
|
||||
|
||||
Assert.NotEmpty(s.Driver.FindElements(By.ClassName("payout")));
|
||||
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-selectAllCheckbox")).Click();
|
||||
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-actions")).Click();
|
||||
s.Driver.FindElement(By.ClassName("mass-action-select-all")).Click();
|
||||
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-approve-pay")).Click();
|
||||
|
||||
s.Driver.FindElement(By.Id("SignTransaction")).Click();
|
||||
@ -1892,8 +1952,6 @@ namespace BTCPayServer.Tests
|
||||
});
|
||||
s.GoToHome();
|
||||
//offline/external payout test
|
||||
s.Driver.FindElement(By.Id("NotificationsHandle")).Click();
|
||||
s.Driver.FindElement(By.Id("NotificationsMarkAllAsSeen")).Click();
|
||||
|
||||
var newStore = s.CreateNewStore();
|
||||
s.GenerateWallet("BTC", "", true, true);
|
||||
@ -1906,8 +1964,10 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("Currency")).Clear();
|
||||
s.Driver.FindElement(By.Id("Currency")).SendKeys("BTC");
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
s.Driver.FindElement(By.LinkText("View")).Click();
|
||||
|
||||
s.Driver.FindElement(By.LinkText("View")).Click();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
|
||||
|
||||
address = await s.Server.ExplorerNode.GetNewAddressAsync();
|
||||
s.Driver.FindElement(By.Id("Destination")).SendKeys(address.ToString());
|
||||
s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys(Keys.Enter);
|
||||
@ -1916,18 +1976,18 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains(PayoutState.AwaitingApproval.GetStateString(), s.Driver.PageSource);
|
||||
s.GoToStore(s.StoreId, StoreNavPages.Payouts);
|
||||
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-view")).Click();
|
||||
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-selectAllCheckbox")).Click();
|
||||
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-actions")).Click();
|
||||
s.Driver.FindElement(By.ClassName("mass-action-select-all")).Click();
|
||||
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-approve")).Click();
|
||||
s.FindAlertMessage();
|
||||
var tx = await s.Server.ExplorerNode.SendToAddressAsync(address, Money.FromUnit(0.001m, MoneyUnit.BTC));
|
||||
s.Driver.Close();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
|
||||
|
||||
s.GoToStore(s.StoreId, StoreNavPages.Payouts);
|
||||
|
||||
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-view")).Click();
|
||||
Assert.Contains(PayoutState.AwaitingPayment.GetStateString(), s.Driver.PageSource);
|
||||
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-selectAllCheckbox")).Click();
|
||||
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-actions")).Click();
|
||||
s.Driver.FindElement(By.ClassName("mass-action-select-all")).Click();
|
||||
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-mark-paid")).Click();
|
||||
s.FindAlertMessage();
|
||||
|
||||
@ -1960,17 +2020,16 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("Currency")).SendKeys("BTC");
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
s.Driver.FindElement(By.LinkText("View")).Click();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
|
||||
|
||||
// Bitcoin-only, SelectedPaymentMethod should not be displayed
|
||||
s.Driver.ElementDoesNotExist(By.Id("SelectedPaymentMethod"));
|
||||
|
||||
var bolt = (await s.Server.CustomerLightningD.CreateInvoice(
|
||||
payoutAmount,
|
||||
$"LN payout test {DateTime.UtcNow.Ticks}",
|
||||
TimeSpan.FromHours(1), CancellationToken.None)).BOLT11;
|
||||
s.Driver.FindElement(By.Id("Destination")).SendKeys(bolt);
|
||||
s.Driver.FindElement(By.Id("SelectedPaymentMethod")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(
|
||||
$"#SelectedPaymentMethod option[value={new PaymentMethodId("BTC", PaymentTypes.LightningLike)}]"))
|
||||
.Click();
|
||||
|
||||
s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys(Keys.Enter);
|
||||
//we do not allow short-life bolts.
|
||||
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Error);
|
||||
@ -1981,27 +2040,23 @@ namespace BTCPayServer.Tests
|
||||
TimeSpan.FromDays(31), CancellationToken.None)).BOLT11;
|
||||
s.Driver.FindElement(By.Id("Destination")).Clear();
|
||||
s.Driver.FindElement(By.Id("Destination")).SendKeys(bolt);
|
||||
s.Driver.FindElement(By.Id("SelectedPaymentMethod")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(
|
||||
$"#SelectedPaymentMethod option[value={new PaymentMethodId("BTC", PaymentTypes.LightningLike)}]"))
|
||||
.Click();
|
||||
|
||||
s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys(Keys.Enter);
|
||||
s.FindAlertMessage();
|
||||
|
||||
Assert.Contains(PayoutState.AwaitingApproval.GetStateString(), s.Driver.PageSource);
|
||||
s.Driver.Close();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
|
||||
|
||||
s.GoToStore(newStore.storeId, StoreNavPages.Payouts);
|
||||
s.Driver.FindElement(By.Id($"{new PaymentMethodId("BTC", PaymentTypes.LightningLike)}-view")).Click();
|
||||
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-view")).Click();
|
||||
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-selectAllCheckbox")).Click();
|
||||
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-actions")).Click();
|
||||
s.Driver.FindElement(By.ClassName("mass-action-select-all")).Click();
|
||||
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-approve-pay")).Click();
|
||||
Assert.Contains(bolt, s.Driver.PageSource);
|
||||
Assert.Contains($"{payoutAmount.ToString()} BTC", s.Driver.PageSource);
|
||||
Assert.Contains($"{payoutAmount} BTC", s.Driver.PageSource);
|
||||
s.Driver.FindElement(By.CssSelector("#pay-invoices-form")).Submit();
|
||||
|
||||
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
|
||||
s.FindAlertMessage();
|
||||
s.GoToStore(newStore.storeId, StoreNavPages.Payouts);
|
||||
s.Driver.FindElement(By.Id($"{new PaymentMethodId("BTC", PaymentTypes.LightningLike)}-view")).Click();
|
||||
|
||||
@ -2011,8 +2066,7 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-view")).Click();
|
||||
Assert.Contains(bolt, s.Driver.PageSource);
|
||||
|
||||
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-selectAllCheckbox")).Click();
|
||||
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-actions")).Click();
|
||||
s.Driver.FindElement(By.ClassName("mass-action-select-all")).Click();
|
||||
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-mark-paid")).Click();
|
||||
s.Driver.FindElement(By.Id($"{new PaymentMethodId("BTC", PaymentTypes.LightningLike)}-view")).Click();
|
||||
|
||||
@ -2027,16 +2081,21 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.SetCheckbox(By.Id("AutoApproveClaims"), true);
|
||||
s.Driver.FindElement(By.Id("Amount")).Clear();
|
||||
s.Driver.FindElement(By.Id("Amount")).SendKeys("99.0" + Keys.Enter);
|
||||
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
|
||||
s.FindAlertMessage();
|
||||
|
||||
s.Driver.FindElement(By.LinkText("View")).Click();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
|
||||
|
||||
address = await s.Server.ExplorerNode.GetNewAddressAsync();
|
||||
s.Driver.FindElement(By.Id("Destination")).Clear();
|
||||
s.Driver.FindElement(By.Id("Destination")).SendKeys(address.ToString());
|
||||
s.Driver.FindElement(By.Id("ClaimedAmount")).Clear();
|
||||
s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys("20" + Keys.Enter);
|
||||
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
|
||||
s.FindAlertMessage();
|
||||
|
||||
Assert.Contains(PayoutState.AwaitingPayment.GetStateString(), s.Driver.PageSource);
|
||||
s.Driver.Close();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
|
||||
|
||||
// LNURL Withdraw support check with BTC denomination
|
||||
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
|
||||
@ -2047,8 +2106,11 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("Amount")).SendKeys("0.0000001");
|
||||
s.Driver.FindElement(By.Id("Currency")).Clear();
|
||||
s.Driver.FindElement(By.Id("Currency")).SendKeys("BTC" + Keys.Enter);
|
||||
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
|
||||
s.FindAlertMessage();
|
||||
|
||||
s.Driver.FindElement(By.LinkText("View")).Click();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
|
||||
|
||||
s.Driver.FindElement(By.CssSelector("#lnurlwithdraw-button")).Click();
|
||||
s.Driver.WaitForElement(By.Id("qr-code-data-input"));
|
||||
|
||||
@ -2074,6 +2136,8 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains(PayoutState.Completed.GetStateString(), s.Driver.PageSource);
|
||||
Assert.Equal(LightningInvoiceStatus.Paid, (await s.Server.CustomerLightningD.GetInvoice(bolt2.Id)).Status);
|
||||
});
|
||||
s.Driver.Close();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
|
||||
|
||||
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
|
||||
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
|
||||
@ -2083,8 +2147,11 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("Amount")).SendKeys("0.0000001");
|
||||
s.Driver.FindElement(By.Id("Currency")).Clear();
|
||||
s.Driver.FindElement(By.Id("Currency")).SendKeys("BTC" + Keys.Enter);
|
||||
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
|
||||
s.FindAlertMessage();
|
||||
|
||||
s.Driver.FindElement(By.LinkText("View")).Click();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
|
||||
|
||||
s.Driver.FindElement(By.CssSelector("#lnurlwithdraw-button")).Click();
|
||||
lnurl = new Uri(LNURL.LNURL.Parse(s.Driver.FindElement(By.Id("qr-code-data-input")).GetAttribute("value"), out _).ToString().Replace("https", "http"));
|
||||
|
||||
@ -2108,6 +2175,8 @@ namespace BTCPayServer.Tests
|
||||
|
||||
Assert.Contains(PayoutState.AwaitingApproval.GetStateString(), s.Driver.PageSource);
|
||||
});
|
||||
s.Driver.Close();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
|
||||
|
||||
// LNURL Withdraw support check with SATS denomination
|
||||
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
|
||||
@ -2118,8 +2187,11 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("Amount")).SendKeys("21021");
|
||||
s.Driver.FindElement(By.Id("Currency")).Clear();
|
||||
s.Driver.FindElement(By.Id("Currency")).SendKeys("SATS" + Keys.Enter);
|
||||
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
|
||||
s.FindAlertMessage();
|
||||
|
||||
s.Driver.FindElement(By.LinkText("View")).Click();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
|
||||
|
||||
s.Driver.FindElement(By.CssSelector("#lnurlwithdraw-button")).Click();
|
||||
lnurl = new Uri(LNURL.LNURL.Parse(s.Driver.FindElement(By.Id("qr-code-data-input")).GetAttribute("value"), out _).ToString().Replace("https", "http"));
|
||||
s.Driver.FindElement(By.CssSelector("button[data-bs-dismiss='modal']")).Click();
|
||||
@ -2144,6 +2216,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains(PayoutState.Completed.GetStateString(), s.Driver.PageSource);
|
||||
Assert.Equal(LightningInvoiceStatus.Paid, (await s.Server.CustomerLightningD.GetInvoice(bolt2.Id)).Status);
|
||||
});
|
||||
s.Driver.Close();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -2216,7 +2289,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("EUR", s.Driver.FindElement(By.Id("Currency")).Text);
|
||||
Assert.Contains("0,00", s.Driver.FindElement(By.Id("Amount")).Text);
|
||||
Assert.Equal("", s.Driver.FindElement(By.Id("Calculation")).Text);
|
||||
Assert.True(s.Driver.FindElement(By.Id("ModeTablist-amount")).Selected);
|
||||
Assert.True(s.Driver.FindElement(By.Id("ModeTablist-amounts")).Selected);
|
||||
Assert.False(s.Driver.FindElement(By.Id("ModeTablist-discount")).Enabled);
|
||||
Assert.False(s.Driver.FindElement(By.Id("ModeTablist-tip")).Enabled);
|
||||
|
||||
@ -2225,13 +2298,17 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.CssSelector(".keypad [data-key='2']")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(".keypad [data-key='3']")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(".keypad [data-key='4']")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(".keypad [data-key='.']")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(".keypad [data-key='0']")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(".keypad [data-key='0']")).Click();
|
||||
Assert.Equal("1.234,00", s.Driver.FindElement(By.Id("Amount")).Text);
|
||||
Assert.Equal("", s.Driver.FindElement(By.Id("Calculation")).Text);
|
||||
s.Driver.FindElement(By.CssSelector(".keypad [data-key='+']")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(".keypad [data-key='5']")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(".keypad [data-key='6']")).Click();
|
||||
Assert.Equal("1.234,56", s.Driver.FindElement(By.Id("Amount")).Text);
|
||||
Assert.True(s.Driver.FindElement(By.Id("ModeTablist-discount")).Enabled);
|
||||
Assert.True(s.Driver.FindElement(By.Id("ModeTablist-tip")).Enabled);
|
||||
Assert.Equal("", s.Driver.FindElement(By.Id("Calculation")).Text);
|
||||
Assert.Equal("1.234,00 € + 0,56 €", s.Driver.FindElement(By.Id("Calculation")).Text);
|
||||
|
||||
// Discount: 10%
|
||||
s.Driver.FindElement(By.CssSelector("label[for='ModeTablist-discount']")).Click();
|
||||
@ -2239,14 +2316,14 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.CssSelector(".keypad [data-key='0']")).Click();
|
||||
Assert.Contains("1.111,10", s.Driver.FindElement(By.Id("Amount")).Text);
|
||||
Assert.Contains("10% discount", s.Driver.FindElement(By.Id("Discount")).Text);
|
||||
Assert.Contains("1.234,56 € - 123,46 € (10%)", s.Driver.FindElement(By.Id("Calculation")).Text);
|
||||
Assert.Contains("1.234,00 € + 0,56 € - 123,46 € (10%)", s.Driver.FindElement(By.Id("Calculation")).Text);
|
||||
|
||||
// Tip: 10%
|
||||
s.Driver.FindElement(By.CssSelector("label[for='ModeTablist-tip']")).Click();
|
||||
s.Driver.WaitForElement(By.Id("Tip-Custom"));
|
||||
s.Driver.FindElement(By.Id("Tip-10")).Click();
|
||||
Assert.Contains("1.222,21", s.Driver.FindElement(By.Id("Amount")).Text);
|
||||
Assert.Contains("1.234,56 € - 123,46 € (10%) + 111,11 € (10%)", s.Driver.FindElement(By.Id("Calculation")).Text);
|
||||
Assert.Contains("1.234,00 € + 0,56 € - 123,46 € (10%) + 111,11 € (10%)", s.Driver.FindElement(By.Id("Calculation")).Text);
|
||||
|
||||
// Pay
|
||||
s.Driver.FindElement(By.Id("pay-button")).Click();
|
||||
@ -2529,20 +2606,22 @@ namespace BTCPayServer.Tests
|
||||
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
s.Driver.FindElement(By.LinkText("View")).Click();
|
||||
s.Driver.FindElement(By.Id("Destination")).SendKeys(lnurl);
|
||||
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
|
||||
var pullPaymentId = s.Driver.Url.Split('/').Last();
|
||||
|
||||
s.Driver.FindElement(By.Id("Destination")).SendKeys(lnurl);
|
||||
s.Driver.FindElement(By.Id("ClaimedAmount")).Clear();
|
||||
s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys("0.0000001" + Keys.Enter);
|
||||
s.FindAlertMessage();
|
||||
|
||||
|
||||
s.Driver.Close();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
|
||||
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
|
||||
var payouts = s.Driver.FindElements(By.ClassName("pp-payout"));
|
||||
payouts[0].Click();
|
||||
s.Driver.FindElement(By.Id("BTC_LightningLike-view")).Click();
|
||||
Assert.NotEmpty(s.Driver.FindElements(By.ClassName("payout")));
|
||||
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-selectAllCheckbox")).Click();
|
||||
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-actions")).Click();
|
||||
s.Driver.FindElement(By.ClassName("mass-action-select-all")).Click();
|
||||
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-approve-pay")).Click();
|
||||
|
||||
Assert.Contains(lnurl, s.Driver.PageSource);
|
||||
|
@ -298,7 +298,7 @@ retry:
|
||||
var fetcher = new RateFetcher(factory);
|
||||
var provider = new BTCPayNetworkProvider(ChainName.Mainnet);
|
||||
var b = new StoreBlob();
|
||||
string[] temporarilyBroken = { "UGX" };
|
||||
string[] temporarilyBroken = { "COP", "UGX" };
|
||||
foreach (var k in StoreBlob.RecommendedExchanges)
|
||||
{
|
||||
b.DefaultCurrency = k.Key;
|
||||
@ -307,14 +307,20 @@ retry:
|
||||
var result = fetcher.FetchRates(pairs, rules, default);
|
||||
foreach ((CurrencyPair key, Task<RateResult> value) in result)
|
||||
{
|
||||
TestLogs.LogInformation($"Testing {key} when default currency is {k.Key}");
|
||||
var rateResult = await value;
|
||||
var hasRate = rateResult.BidAsk != null;
|
||||
|
||||
if (temporarilyBroken.Contains(k.Key))
|
||||
{
|
||||
TestLogs.LogInformation($"Skipping {key} because it is marked as temporarily broken");
|
||||
continue;
|
||||
if (!hasRate)
|
||||
{
|
||||
TestLogs.LogInformation($"Skipping {key} because it is marked as temporarily broken");
|
||||
continue;
|
||||
}
|
||||
TestLogs.LogInformation($"Note: {key} is marked as temporarily broken, but the rate is available");
|
||||
}
|
||||
var rateResult = await value;
|
||||
TestLogs.LogInformation($"Testing {key} when default currency is {k.Key}");
|
||||
Assert.True(rateResult.BidAsk != null, $"Impossible to get the rate {rateResult.EvaluatedRule}");
|
||||
Assert.True(hasRate, $"Impossible to get the rate {rateResult.EvaluatedRule}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -422,6 +428,11 @@ retry:
|
||||
version = Regex.Match(actual, "Original file: /npm/vue-sanitize-directive@([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
|
||||
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/vue-sanitize-directive@{version}/dist/vue-sanitize-directive.umd.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "decimal.js", "decimal.min.js").Trim();
|
||||
version = Regex.Match(actual, "Original file: /npm/decimal\\.js@([0-9]+.[0-9]+.[0-9]+)/decimal\\.js").Groups[1].Value;
|
||||
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/decimal.js@{version}/decimal.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
EqualJsContent(expected, actual);
|
||||
}
|
||||
|
||||
private void EqualJsContent(string expected, string actual)
|
||||
|
@ -386,11 +386,11 @@ namespace BTCPayServer.Tests
|
||||
var newBolt11 = newInvoice.CryptoInfo.First(o => o.PaymentUrls.BOLT11 != null).PaymentUrls.BOLT11;
|
||||
var oldBolt11 = invoice.CryptoInfo.First(o => o.PaymentUrls.BOLT11 != null).PaymentUrls.BOLT11;
|
||||
Assert.NotEqual(newBolt11, oldBolt11);
|
||||
Assert.Equal(newInvoice.BtcDue.GetValue(),
|
||||
Assert.Equal(newInvoice.BtcDue.ToDecimal(MoneyUnit.BTC),
|
||||
BOLT11PaymentRequest.Parse(newBolt11, Network.RegTest).MinimumAmount.ToDecimal(LightMoneyUnit.BTC));
|
||||
}, 40000);
|
||||
|
||||
TestLogs.LogInformation($"Paying invoice {newInvoice.Id} remaining due amount {newInvoice.BtcDue.GetValue()} via lightning");
|
||||
TestLogs.LogInformation($"Paying invoice {newInvoice.Id} remaining due amount {newInvoice.BtcDue.GetValue((BTCPayNetwork) tester.DefaultNetwork)} via lightning");
|
||||
var evt = await tester.WaitForEvent<InvoiceDataChangedEvent>(async () =>
|
||||
{
|
||||
await tester.SendLightningPaymentAsync(newInvoice);
|
||||
@ -2881,15 +2881,15 @@ namespace BTCPayServer.Tests
|
||||
Assert.Single(paymentTypes["On-Chain"]);
|
||||
|
||||
// 2 on-chain transactions: It received from the cashcow, then paid its own invoice
|
||||
report = await GetReport(acc, new() { ViewName = "On-Chain Wallets" });
|
||||
report = await GetReport(acc, new() { ViewName = "Wallets" });
|
||||
var txIdIndex = report.GetIndex("TransactionId");
|
||||
var balanceIndex = report.GetIndex("BalanceChange");
|
||||
Assert.Equal(2, report.Data.Count);
|
||||
Assert.Equal(64, report.Data[0][txIdIndex].Value<string>().Length);
|
||||
Assert.Contains(report.Data, d => d[balanceIndex].Value<decimal>() == 1.0m);
|
||||
Assert.Contains(report.Data, d => d[balanceIndex]["v"].Value<decimal>() == 1.0m);
|
||||
|
||||
// Items sold
|
||||
report = await GetReport(acc, new() { ViewName = "Products sold" });
|
||||
report = await GetReport(acc, new() { ViewName = "Sales" });
|
||||
var itemIndex = report.GetIndex("Product");
|
||||
var countIndex = report.GetIndex("Quantity");
|
||||
var itemsCount = report.Data.GroupBy(d => d[itemIndex].Value<string>())
|
||||
|
@ -99,7 +99,7 @@ services:
|
||||
custom:
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.3.63
|
||||
image: nicolasdorier/nbxplorer:2.3.66
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
@ -224,7 +224,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.16.4-beta-1
|
||||
image: btcpayserver/lnd:v0.17.2-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -259,7 +259,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.16.4-beta-1
|
||||
image: btcpayserver/lnd:v0.17.2-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -307,7 +307,7 @@ services:
|
||||
- "torrcdir:/usr/local/etc/tor"
|
||||
- "tor_servicesdir:/var/lib/tor/hidden_services"
|
||||
monerod:
|
||||
image: btcpayserver/monero:0.17.0.0-amd64
|
||||
image: btcpayserver/monero:0.18.2.2-5
|
||||
restart: unless-stopped
|
||||
container_name: xmr_monerod
|
||||
entrypoint: sleep 999999
|
||||
@ -317,7 +317,7 @@ services:
|
||||
ports:
|
||||
- "18081:18081"
|
||||
monero_wallet:
|
||||
image: btcpayserver/monero:0.17.0.0-amd64
|
||||
image: btcpayserver/monero:0.18.2.2-5
|
||||
restart: unless-stopped
|
||||
container_name: xmr_wallet_rpc
|
||||
entrypoint: monero-wallet-rpc --testnet --rpc-bind-ip=0.0.0.0 --disable-rpc-login --confirm-external-bind --rpc-bind-port=18082 --non-interactive --trusted-daemon --daemon-address=monerod:18081 --wallet-file=/wallet/wallet.keys --password-file=/wallet/password --tx-notify="/bin/sh ./scripts/notifier.sh -k -X GET https://host.docker.internal:14142/monerolikedaemoncallback/tx?cryptoCode=xmr&hash=%s"
|
||||
@ -349,7 +349,7 @@ services:
|
||||
elementsd-liquid:
|
||||
restart: always
|
||||
container_name: btcpayserver_elementsd_liquid
|
||||
image: btcpayserver/elements:0.21.0.1
|
||||
image: btcpayserver/elements:0.21.0.2-4
|
||||
environment:
|
||||
ELEMENTS_CHAIN: elementsregtest
|
||||
ELEMENTS_EXTRA_ARGS: |
|
||||
@ -364,11 +364,9 @@ services:
|
||||
whitelist=0.0.0.0/0
|
||||
rpcallowip=0.0.0.0/0
|
||||
validatepegin=0
|
||||
initialfreecoins=210000000000000
|
||||
initialfreecoins=2100000000000000
|
||||
con_dyna_deploy_signal=1
|
||||
con_dyna_deploy_start=0
|
||||
con_nminerconfirmationwindow=1
|
||||
con_nrulechangeactivationthreshold=1
|
||||
con_dyna_deploy_start=10
|
||||
expose:
|
||||
- "19332"
|
||||
- "19444"
|
||||
|
@ -96,7 +96,7 @@ services:
|
||||
custom:
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.3.63
|
||||
image: nicolasdorier/nbxplorer:2.3.66
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
@ -211,7 +211,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.16.4-beta-1
|
||||
image: btcpayserver/lnd:v0.17.2-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -248,7 +248,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.16.4-beta-1
|
||||
image: btcpayserver/lnd:v0.17.2-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
|
@ -53,8 +53,7 @@
|
||||
<PackageReference Include="Dapper" Version="2.0.123" />
|
||||
<PackageReference Include="Fido2" Version="2.0.2" />
|
||||
<PackageReference Include="Fido2.AspNet" Version="2.0.2" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="5.0.372" />
|
||||
<PackageReference Include="LNURL" Version="0.0.33" />
|
||||
<PackageReference Include="LNURL" Version="0.0.34" />
|
||||
<PackageReference Include="MailKit" Version="3.3.0" />
|
||||
<PackageReference Include="BTCPayServer.NETCore.Plugins.Mvc" Version="1.4.4" />
|
||||
<PackageReference Include="QRCoder" Version="1.4.3" />
|
||||
@ -138,6 +137,7 @@
|
||||
<ItemGroup>
|
||||
<Watch Include="Views\**\*.*"></Watch>
|
||||
<Watch Remove="Views\UIAccount\CheatPermissions.cshtml" />
|
||||
<Watch Remove="Views\UIPullPayment\ViewPullPaymentPrint.cshtml" />
|
||||
<Watch Remove="Views\UIReports\StoreReports.cshtml" />
|
||||
<Content Update="Views\UIApps\_ViewImports.cshtml">
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
|
@ -1,4 +1,3 @@
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace BTCPayServer.Blazor
|
||||
|
@ -1,4 +1,3 @@
|
||||
@using System.Security.Claims
|
||||
@using BTCPayServer.Abstractions.Contracts;
|
||||
@using BTCPayServer.Configuration;
|
||||
@using BTCPayServer.Data;
|
||||
@ -86,7 +85,8 @@
|
||||
}
|
||||
|
||||
public void Dispose() => _EventAggregatorListener?.Dispose();
|
||||
string SeenCount(int? count)
|
||||
|
||||
static string SeenCount(int? count)
|
||||
{
|
||||
if (count is not int c)
|
||||
return "0";
|
||||
@ -94,12 +94,14 @@
|
||||
return $"{NotificationManager.MaxUnseen - 1}+";
|
||||
return c.ToString();
|
||||
}
|
||||
|
||||
void UpdateState((List<NotificationViewModel> Items, int? Count) res)
|
||||
{
|
||||
UnseenCount = SeenCount(res.Count);
|
||||
Last5 = res.Items;
|
||||
}
|
||||
protected async override Task OnParametersSetAsync()
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
if (await GetUserId() is string userId)
|
||||
{
|
||||
@ -117,15 +119,16 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
async Task<string>
|
||||
GetUserId()
|
||||
|
||||
async Task<string> GetUserId()
|
||||
{
|
||||
var state = await _AuthenticationStateProvider.GetAuthenticationStateAsync();
|
||||
if (!state.User.Identity.IsAuthenticated)
|
||||
return null;
|
||||
return _UserManager.GetUserId(state.User);
|
||||
}
|
||||
public async Task MarkAllAsSeen()
|
||||
|
||||
private async Task MarkAllAsSeen()
|
||||
{
|
||||
if (await GetUserId() is string userId)
|
||||
{
|
||||
@ -133,6 +136,7 @@
|
||||
UnseenCount = "0";
|
||||
}
|
||||
}
|
||||
|
||||
private static string NotificationIcon(string type)
|
||||
{
|
||||
return type switch
|
||||
|
@ -13,14 +13,20 @@ namespace BTCPayServer
|
||||
{
|
||||
return Regex.Match(color, Pattern).Success;
|
||||
}
|
||||
public string TextColor(string bgColor)
|
||||
|
||||
public Color TextColor(Color bg)
|
||||
{
|
||||
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;
|
||||
int bgDelta = Convert.ToInt32(bg.R * 0.299 + bg.G * 0.587 + bg.B * 0.114);
|
||||
return 255 - bgDelta < nThreshold ? Color.Black : Color.White;
|
||||
}
|
||||
|
||||
public string TextColor(string bg)
|
||||
{
|
||||
var color = TextColor(FromHtml(bg));
|
||||
return ColorTranslator.ToHtml(color).ToLowerInvariant();
|
||||
}
|
||||
|
||||
// Borrowed from https://github.com/ManageIQ/guides/blob/master/labels.md
|
||||
public static readonly ColorPalette Default = new ColorPalette(new string[] {
|
||||
"#fbca04",
|
||||
@ -31,6 +37,7 @@ namespace BTCPayServer
|
||||
"#cdcdcd",
|
||||
"#cc317c",
|
||||
});
|
||||
|
||||
private ColorPalette(string[] labels)
|
||||
{
|
||||
Labels = labels;
|
||||
@ -98,5 +105,10 @@ namespace BTCPayServer
|
||||
var color = AdjustBrightness(ColorTranslator.FromHtml(html), correctionFactor);
|
||||
return ColorTranslator.ToHtml(color);
|
||||
}
|
||||
|
||||
public Color FromHtml(string html)
|
||||
{
|
||||
return ColorTranslator.FromHtml(html);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
67
BTCPayServer/Components/InvoiceStatus/Default.cshtml
Normal file
67
BTCPayServer/Components/InvoiceStatus/Default.cshtml
Normal file
@ -0,0 +1,67 @@
|
||||
@using BTCPayServer.Services.Invoices
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@model BTCPayServer.Components.InvoiceStatus.InvoiceStatusViewModel
|
||||
@inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary
|
||||
|
||||
@{
|
||||
var state = Model.State.ToString();
|
||||
var badgeClass = Model.State.Status.ToModernStatus().ToString().ToLower();
|
||||
var canMark = !string.IsNullOrEmpty(Model.InvoiceId) && (Model.State.CanMarkComplete() || Model.State.CanMarkInvalid());
|
||||
}
|
||||
<div class="d-inline-flex align-items-center gap-2">
|
||||
@if (Model.IsArchived)
|
||||
{
|
||||
<span class="badge bg-warning">archived</span>
|
||||
}
|
||||
<div class="badge badge-@badgeClass" data-invoice-state-badge="@Model.InvoiceId">
|
||||
@if (canMark)
|
||||
{
|
||||
<span class="dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
@state
|
||||
</span>
|
||||
<div class="dropdown-menu">
|
||||
@if (Model.State.CanMarkInvalid())
|
||||
{
|
||||
<button type="button" class="dropdown-item lh-base" data-invoice-id="@Model.InvoiceId" data-new-state="invalid">
|
||||
Mark as invalid
|
||||
</button>
|
||||
}
|
||||
@if (Model.State.CanMarkComplete())
|
||||
{
|
||||
<button type="button" class="dropdown-item lh-base" data-invoice-id="@Model.InvoiceId" data-new-state="settled">
|
||||
Mark as settled
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@state
|
||||
}
|
||||
</div>
|
||||
@if (Model.Payments != null)
|
||||
{
|
||||
foreach (var paymentMethodId in Model.Payments.Select(payment => payment.GetPaymentMethodId()).Distinct())
|
||||
{
|
||||
var image = PaymentMethodHandlerDictionary[paymentMethodId]?.GetCryptoImage(paymentMethodId);
|
||||
var badge = paymentMethodId.PaymentType.GetBadge();
|
||||
if (!string.IsNullOrEmpty(image) || !string.IsNullOrEmpty(badge))
|
||||
{
|
||||
<span class="d-inline-flex align-items-center gap-1">
|
||||
@if (!string.IsNullOrEmpty(image))
|
||||
{
|
||||
<img src="@Context.Request.GetRelativePathOrAbsolute(image)" alt="@paymentMethodId.PaymentType.ToString()" style="height:1.5em" />
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(badge))
|
||||
{
|
||||
@badge
|
||||
}
|
||||
</span>
|
||||
}
|
||||
}
|
||||
}
|
||||
@if (Model.HasRefund)
|
||||
{
|
||||
<span class="badge bg-warning">Refund</span>
|
||||
}
|
||||
</div>
|
22
BTCPayServer/Components/InvoiceStatus/InvoiceStatus.cs
Normal file
22
BTCPayServer/Components/InvoiceStatus/InvoiceStatus.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Components.InvoiceStatus
|
||||
{
|
||||
public class InvoiceStatus : ViewComponent
|
||||
{
|
||||
public IViewComponentResult Invoke(InvoiceState state, List<PaymentEntity> payments, string invoiceId, bool isArchived = false, bool hasRefund = false)
|
||||
{
|
||||
var vm = new InvoiceStatusViewModel
|
||||
{
|
||||
State = state,
|
||||
Payments = payments,
|
||||
InvoiceId = invoiceId,
|
||||
IsArchived = isArchived,
|
||||
HasRefund = hasRefund
|
||||
};
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
||||
namespace BTCPayServer.Components.InvoiceStatus
|
||||
{
|
||||
public class InvoiceStatusViewModel
|
||||
{
|
||||
public InvoiceState State { get; set; }
|
||||
public List<PaymentEntity> Payments { get; set; }
|
||||
public string InvoiceId { get; set; }
|
||||
public bool IsArchived { get; set; }
|
||||
public bool HasRefund { get; set; }
|
||||
}
|
||||
}
|
@ -1,7 +1,5 @@
|
||||
@using BTCPayServer.Services
|
||||
@using BTCPayServer.Abstractions.Contracts
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@inject ThemeSettings Theme
|
||||
@inject IFileService FileService
|
||||
@model BTCPayServer.Components.MainLogo.MainLogoViewModel
|
||||
|
@ -4,7 +4,6 @@
|
||||
@using BTCPayServer.Views.Manage
|
||||
@using BTCPayServer.Views.PaymentRequest
|
||||
@using BTCPayServer.Views.Wallets
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Components.ThemeSwitch
|
||||
@using BTCPayServer.Components.UIExtensionPoint
|
||||
|
@ -11,13 +11,14 @@ namespace BTCPayServer.Components.QRCode
|
||||
{
|
||||
private static QRCodeGenerator _qrGenerator = new();
|
||||
|
||||
public IViewComponentResult Invoke(string data)
|
||||
public IViewComponentResult Invoke(string data, int size=256)
|
||||
{
|
||||
var qrCodeData = _qrGenerator.CreateQrCode(data, QRCodeGenerator.ECCLevel.Q);
|
||||
var qrCode = new PngByteQRCode(qrCodeData);
|
||||
var bytes = qrCode.GetGraphic(5, new byte[] { 0, 0, 0, 255 }, new byte[] { 0xf5, 0xf5, 0xf7, 255 });
|
||||
var b64 = Convert.ToBase64String(bytes);
|
||||
return new HtmlContentViewComponentResult(new HtmlString($"<img style=\"image-rendering:pixelated;image-rendering:-moz-crisp-edges;min-width:256px;min-height:256px\" src=\"data:image/png;base64,{b64}\" class=\"qr-code\" />"));
|
||||
return new HtmlContentViewComponentResult(new HtmlString(
|
||||
$"<img style=\"image-rendering:pixelated;image-rendering:-moz-crisp-edges;min-width:{size}px;min-height:{size}px\" src=\"data:image/png;base64,{b64}\" class=\"qr-code\" />"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -52,41 +52,8 @@
|
||||
<a asp-controller="UIInvoice" asp-action="Invoice" asp-route-invoiceId="@invoice.InvoiceId" class="text-break">@invoice.InvoiceId</a>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
@if (invoice.Details.Archived)
|
||||
{
|
||||
<span class="badge bg-warning">archived</span>
|
||||
}
|
||||
<span class="badge badge-@invoice.Status.Status.ToModernStatus().ToString().ToLower()">
|
||||
@invoice.Status.Status.ToModernStatus().ToString()
|
||||
@if (invoice.Status.ExceptionStatus != InvoiceExceptionStatus.None)
|
||||
{
|
||||
@($"({invoice.Status.ExceptionStatus.ToString()})")
|
||||
}
|
||||
</span>
|
||||
@foreach (var paymentMethodId in invoice.Details.Payments.Select(payment => payment.GetPaymentMethodId()).Distinct())
|
||||
{
|
||||
var image = PaymentMethodHandlerDictionary[paymentMethodId]?.GetCryptoImage(paymentMethodId);
|
||||
var badge = paymentMethodId.PaymentType.GetBadge();
|
||||
if (!string.IsNullOrEmpty(image) || !string.IsNullOrEmpty(badge))
|
||||
{
|
||||
<span class="d-inline-flex align-items-center gap-1">
|
||||
@if (!string.IsNullOrEmpty(image))
|
||||
{
|
||||
<img src="@Context.Request.GetRelativePathOrAbsolute(image)" alt="@paymentMethodId.PaymentType.ToString()" style="height:1.5em" />
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(badge))
|
||||
{
|
||||
@badge
|
||||
}
|
||||
</span>
|
||||
}
|
||||
}
|
||||
@if (invoice.HasRefund)
|
||||
{
|
||||
<span class="badge bg-warning">Refund</span>
|
||||
}
|
||||
</div>
|
||||
<vc:invoice-status state="invoice.Status" payments="invoice.Details.Payments" invoice-id="@invoice.InvoiceId"
|
||||
is-archived="invoice.Details.Archived" has-refund="invoice.HasRefund" />
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span data-sensitive>@DisplayFormatter.Currency(invoice.Amount, invoice.Currency)</span>
|
||||
|
@ -12,7 +12,6 @@ public class StoreRecentInvoiceViewModel
|
||||
public string Currency { get; set; }
|
||||
public InvoiceState Status { get; set; }
|
||||
public DateTimeOffset Date { get; set; }
|
||||
|
||||
public InvoiceDetailsModel Details { get; set; }
|
||||
public bool HasRefund { get; set; }
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Abstractions.Contracts
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Components.MainLogo
|
||||
|
@ -1,6 +1,7 @@
|
||||
@using BTCPayServer.Services.Wallets
|
||||
@using BTCPayServer.Payments
|
||||
@model BTCPayServer.Components.StoreWalletBalance.StoreWalletBalanceViewModel
|
||||
|
||||
@inject BTCPayNetworkProvider NetworkProvider
|
||||
<div id="StoreWalletBalance-@Model.Store.Id" class="widget store-wallet-balance">
|
||||
<div class="d-flex gap-3 align-items-center justify-content-between mb-2">
|
||||
<h6>Wallet Balance</h6>
|
||||
@ -38,6 +39,12 @@
|
||||
{
|
||||
<div class="ct-chart"></div>
|
||||
}
|
||||
else if (!Model.Store.GetSupportedPaymentMethods(NetworkProvider).Any(method => method.PaymentId.PaymentType == BitcoinPaymentType.Instance && method.PaymentId.CryptoCode == Model.CryptoCode))
|
||||
{
|
||||
<p>
|
||||
We would like to show you a chart of your balance but you have not yet <a href="@Url.Action("SetupWallet", "UIStores", new {storeId = Model.Store.Id, cryptoCode = Model.CryptoCode})">configured a wallet</a>.
|
||||
</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p>
|
||||
|
@ -75,7 +75,7 @@ public class StoreWalletBalance : ViewComponent
|
||||
if (derivation is not null)
|
||||
{
|
||||
var balance = await wallet.GetBalance(derivation.AccountDerivation, cts.Token);
|
||||
vm.Balance = balance.Available.GetValue();
|
||||
vm.Balance = balance.Available.GetValue(derivation.Network);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,15 @@
|
||||
@if (Model.IsVue)
|
||||
{
|
||||
<span class="truncate-center-truncated" data-bs-toggle="tooltip" :title=@Safe.Json(Model.Text)>
|
||||
<span class="truncate-center-start" v-text=@Safe.Json(Model.Text)></span>
|
||||
@if (Model.Elastic)
|
||||
{
|
||||
<span class="truncate-center-start" v-text=@Safe.Json(Model.Text)></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="truncate-center-start" v-text=@Safe.Json($"{Model.Text}.slice(0, {Model.Padding})")></span>
|
||||
<span>…</span>
|
||||
}
|
||||
<span class="truncate-center-end" v-text=@Safe.Json($"{Model.Text}.slice(-{Model.Padding})")></span>
|
||||
</span>
|
||||
<span class="truncate-center-text" v-text=@Safe.Json(Model.Text)></span>
|
||||
@ -33,7 +41,7 @@
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(Model.Link))
|
||||
{
|
||||
<a href="@Model.Link" rel="noreferrer noopener" target="_blank">
|
||||
<a @(Model.IsVue ? ":" : "")href="@Model.Link" rel="noreferrer noopener" target="_blank">
|
||||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
}
|
||||
|
@ -74,7 +74,7 @@ namespace BTCPayServer.Components.WalletNav
|
||||
if (bid is decimal b)
|
||||
{
|
||||
var currencyData = _currencies.GetCurrencyData(defaultCurrency, true);
|
||||
vm.BalanceDefaultCurrency = (balance.GetValue() * b).ShowMoney(currencyData.Divisibility);
|
||||
vm.BalanceDefaultCurrency = (balance.GetValue(network) * b).ShowMoney(currencyData.Divisibility);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -270,12 +270,14 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
private PointOfSaleSettings ToPointOfSaleSettings(CreatePointOfSaleAppRequest request)
|
||||
{
|
||||
return new PointOfSaleSettings()
|
||||
return new PointOfSaleSettings
|
||||
{
|
||||
Title = request.Title,
|
||||
DefaultView = (PosViewType)request.DefaultView,
|
||||
ShowCustomAmount = request.ShowCustomAmount,
|
||||
ShowDiscount = request.ShowDiscount,
|
||||
ShowSearch = request.ShowSearch,
|
||||
ShowCategories = request.ShowCategories,
|
||||
EnableTips = request.EnableTips,
|
||||
Currency = request.Currency,
|
||||
Template = request.Template != null ? AppService.SerializeTemplate(AppService.Parse(request.Template)) : null,
|
||||
@ -336,6 +338,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
DefaultView = settings.DefaultView.ToString(),
|
||||
ShowCustomAmount = settings.ShowCustomAmount,
|
||||
ShowDiscount = settings.ShowDiscount,
|
||||
ShowSearch = settings.ShowSearch,
|
||||
ShowCategories = settings.ShowCategories,
|
||||
EnableTips = settings.EnableTips,
|
||||
Currency = settings.Currency,
|
||||
Items = JsonConvert.DeserializeObject(
|
||||
|
@ -1,18 +1,11 @@
|
||||
#nullable enable
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore.Internal;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Dapper;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Client;
|
||||
@ -39,7 +32,6 @@ public class GreenfieldReportsController : Controller
|
||||
public ApplicationDbContextFactory DBContextFactory { get; }
|
||||
public ReportService ReportService { get; }
|
||||
|
||||
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/stores/{storeId}/reports")]
|
||||
[NonAction] // Disabling this endpoint as we still need to figure out the request/response model
|
||||
@ -60,7 +52,7 @@ public class GreenfieldReportsController : Controller
|
||||
|
||||
var ctx = new Services.Reporting.QueryContext(storeId, from, to);
|
||||
await report.Query(ctx, cancellationToken);
|
||||
var result = new StoreReportResponse()
|
||||
var result = new StoreReportResponse
|
||||
{
|
||||
Fields = ctx.ViewDefinition?.Fields ?? new List<StoreReportResponse.Field>(),
|
||||
Charts = ctx.ViewDefinition?.Charts ?? new List<ChartDefinition>(),
|
||||
@ -70,11 +62,9 @@ public class GreenfieldReportsController : Controller
|
||||
};
|
||||
return Json(result);
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ViewName), "View doesn't exist");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
ModelState.AddModelError(nameof(vm.ViewName), "View doesn't exist");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,7 +54,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
new LNURLPayPaymentMethodData(
|
||||
paymentMethod.PaymentId.CryptoCode,
|
||||
!excludedPaymentMethods.Match(paymentMethod.PaymentId),
|
||||
paymentMethod.UseBech32Scheme
|
||||
paymentMethod.UseBech32Scheme,
|
||||
paymentMethod.LUD12Enabled
|
||||
)
|
||||
)
|
||||
.Where((result) => enabled is null || enabled == result.Enabled)
|
||||
@ -121,10 +122,11 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
|
||||
LNURLPaySupportedPaymentMethod? paymentMethod = new LNURLPaySupportedPaymentMethod()
|
||||
var paymentMethod = new LNURLPaySupportedPaymentMethod
|
||||
{
|
||||
CryptoCode = cryptoCode,
|
||||
UseBech32Scheme = paymentMethodData.UseBech32Scheme
|
||||
UseBech32Scheme = paymentMethodData.UseBech32Scheme,
|
||||
LUD12Enabled = paymentMethodData.LUD12Enabled
|
||||
};
|
||||
|
||||
var store = Store;
|
||||
@ -153,7 +155,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
: new LNURLPayPaymentMethodData(
|
||||
paymentMethod.PaymentId.CryptoCode,
|
||||
!excluded,
|
||||
paymentMethod.UseBech32Scheme
|
||||
paymentMethod.UseBech32Scheme,
|
||||
paymentMethod.LUD12Enabled
|
||||
);
|
||||
}
|
||||
private void AssertCryptoCodeWallet(string cryptoCode, out BTCPayNetwork network)
|
||||
|
@ -153,13 +153,18 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate,
|
||||
PaymentTolerance = storeBlob.PaymentTolerance,
|
||||
PayJoinEnabled = storeBlob.PayJoinEnabled,
|
||||
PaymentMethodCriteria = storeBlob.PaymentMethodCriteria?.Where(criteria => criteria.Value is not null)?.Select(criteria => new PaymentMethodCriteriaData()
|
||||
AutoDetectLanguage = storeBlob.AutoDetectLanguage,
|
||||
ShowPayInWalletButton = storeBlob.ShowPayInWalletButton,
|
||||
ShowStoreHeader = storeBlob.ShowStoreHeader,
|
||||
CelebratePayment = storeBlob.CelebratePayment,
|
||||
PlaySoundOnPayment = storeBlob.PlaySoundOnPayment,
|
||||
PaymentMethodCriteria = storeBlob.PaymentMethodCriteria?.Where(criteria => criteria.Value is not null).Select(criteria => new PaymentMethodCriteriaData
|
||||
{
|
||||
Above = criteria.Above,
|
||||
Amount = criteria.Value.Value,
|
||||
CurrencyCode = criteria.Value.Currency,
|
||||
PaymentMethod = criteria.PaymentMethod.ToStringNormalized()
|
||||
})?.ToList() ?? new List<PaymentMethodCriteriaData>()
|
||||
}).ToList() ?? new List<PaymentMethodCriteriaData>()
|
||||
};
|
||||
}
|
||||
|
||||
@ -201,11 +206,21 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
blob.LightningDescriptionTemplate = restModel.LightningDescriptionTemplate;
|
||||
blob.PaymentTolerance = restModel.PaymentTolerance;
|
||||
blob.PayJoinEnabled = restModel.PayJoinEnabled;
|
||||
if (restModel.AutoDetectLanguage.HasValue)
|
||||
blob.AutoDetectLanguage = restModel.AutoDetectLanguage.Value;
|
||||
if (restModel.ShowPayInWalletButton.HasValue)
|
||||
blob.ShowPayInWalletButton = restModel.ShowPayInWalletButton.Value;
|
||||
if (restModel.ShowStoreHeader.HasValue)
|
||||
blob.ShowStoreHeader = restModel.ShowStoreHeader.Value;
|
||||
if (restModel.CelebratePayment.HasValue)
|
||||
blob.CelebratePayment = restModel.CelebratePayment.Value;
|
||||
if (restModel.PlaySoundOnPayment.HasValue)
|
||||
blob.PlaySoundOnPayment = restModel.PlaySoundOnPayment.Value;
|
||||
blob.PaymentMethodCriteria = restModel.PaymentMethodCriteria?.Select(criteria =>
|
||||
new PaymentMethodCriteria()
|
||||
new PaymentMethodCriteria
|
||||
{
|
||||
Above = criteria.Above,
|
||||
Value = new CurrencyValue()
|
||||
Value = new CurrencyValue
|
||||
{
|
||||
Currency = criteria.CurrencyCode,
|
||||
Value = criteria.Amount
|
||||
|
@ -216,12 +216,12 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return CreatedAtAction(string.Empty, model);
|
||||
}
|
||||
|
||||
[HttpDelete("~/api/v1/users/{userId}")]
|
||||
[HttpDelete("~/api/v1/users/{idOrEmail}")]
|
||||
[Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> DeleteUser(string userId)
|
||||
public async Task<IActionResult> DeleteUser(string idOrEmail)
|
||||
{
|
||||
var user = await _userManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
var user = await _userManager.FindByIdOrEmail(idOrEmail);
|
||||
if (user is null)
|
||||
{
|
||||
return this.UserNotFound();
|
||||
}
|
||||
|
@ -228,13 +228,17 @@ namespace BTCPayServer.Controllers
|
||||
if (app is null || userId is null)
|
||||
return NotFound();
|
||||
|
||||
if (!file.FileName.IsValidFileName())
|
||||
{
|
||||
return Json(new { error = "Invalid file name" });
|
||||
}
|
||||
if (!file.ContentType.StartsWith("image/", StringComparison.InvariantCulture))
|
||||
{
|
||||
return Json(new { error = "The file needs to be an image" });
|
||||
}
|
||||
if (file.Length > 500_000)
|
||||
{
|
||||
return Json(new { error = "The image file size should be less than 0.5MB" });
|
||||
return Json(new { error = "The file size should be less than 0.5MB" });
|
||||
}
|
||||
var formFile = await file.Bufferize();
|
||||
if (!FileTypeDetector.IsPicture(formFile.Buffer, formFile.FileName))
|
||||
|
@ -223,7 +223,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var blob = custodianAccount.GetBlob();
|
||||
var configForm = await custodian.GetConfigForm(blob, HttpContext.RequestAborted);
|
||||
configForm.SetValues(blob);
|
||||
_formDataService.SetValues(configForm, blob);
|
||||
|
||||
var vm = new EditCustodianAccountViewModel();
|
||||
vm.CustodianAccount = custodianAccount;
|
||||
@ -280,14 +280,14 @@ namespace BTCPayServer.Controllers
|
||||
// First, we restore the previous form based on the previous blob that was
|
||||
// stored in config
|
||||
var form = await custodian.GetConfigForm(b, HttpContext.RequestAborted);
|
||||
form.SetValues(b);
|
||||
_formDataService.SetValues(form, b);
|
||||
// Then we apply new values overriding the previous blob from the Form params
|
||||
form.ApplyValuesFromForm(Request.Form);
|
||||
// We extract the new resulting blob, and request what is the next form based on it
|
||||
b = _formDataService.GetValues(form);
|
||||
form = await custodian.GetConfigForm(_formDataService.GetValues(form), HttpContext.RequestAborted);
|
||||
// We set all the values to this blob, and validate the form
|
||||
form.SetValues(b);
|
||||
_formDataService.SetValues(form, b);
|
||||
_formDataService.Validate(form, ModelState);
|
||||
return form;
|
||||
}
|
||||
@ -600,7 +600,7 @@ namespace BTCPayServer.Controllers
|
||||
catch (BadConfigException e)
|
||||
{
|
||||
Form configForm = await custodian.GetConfigForm(config);
|
||||
configForm.SetValues(config);
|
||||
_formDataService.SetValues(configForm, config);
|
||||
string[] badConfigFields = new string[e.BadConfigKeys.Length];
|
||||
int i = 0;
|
||||
foreach (var oneField in configForm.GetAllFields())
|
||||
|
@ -150,21 +150,22 @@ namespace BTCPayServer.Controllers
|
||||
Events = invoice.Events,
|
||||
Metadata = metaData,
|
||||
Archived = invoice.Archived,
|
||||
HasRefund = invoice.Refunds.Any(),
|
||||
CanRefund = invoiceState.CanRefund(),
|
||||
Refunds = invoice.Refunds,
|
||||
ShowCheckout = invoice.Status == InvoiceStatusLegacy.New,
|
||||
ShowReceipt = invoice.Status.ToModernStatus() == InvoiceStatus.Settled && (invoice.ReceiptOptions?.Enabled ?? receipt.Enabled is true),
|
||||
Deliveries = (await _InvoiceRepository.GetWebhookDeliveries(invoiceId))
|
||||
.Select(c => new Models.StoreViewModels.DeliveryViewModel(c))
|
||||
.ToList(),
|
||||
CanMarkInvalid = invoiceState.CanMarkInvalid(),
|
||||
CanMarkSettled = invoiceState.CanMarkComplete(),
|
||||
.ToList()
|
||||
};
|
||||
|
||||
var details = InvoicePopulatePayments(invoice);
|
||||
model.CryptoPayments = details.CryptoPayments;
|
||||
model.Payments = details.Payments;
|
||||
model.Overpaid = details.Overpaid;
|
||||
model.StillDue = details.StillDue;
|
||||
model.HasRates = details.HasRates;
|
||||
|
||||
if (additionalData.ContainsKey("receiptData"))
|
||||
{
|
||||
@ -253,6 +254,7 @@ namespace BTCPayServer.Controllers
|
||||
Amount = paymentEntity.PaidAmount.Gross,
|
||||
Paid = paymentEntity.InvoicePaidAmount.Net,
|
||||
ReceivedDate = paymentEntity.ReceivedTime.DateTime,
|
||||
AmountFormatted = _displayFormatter.Currency(paymentEntity.PaidAmount.Gross, paymentEntity.PaidAmount.Currency),
|
||||
PaidFormatted = _displayFormatter.Currency(paymentEntity.InvoicePaidAmount.Net, i.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
||||
RateFormatted = _displayFormatter.Currency(paymentEntity.Rate, i.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
||||
PaymentMethod = paymentMethodId.ToPrettyString(),
|
||||
@ -565,37 +567,42 @@ namespace BTCPayServer.Controllers
|
||||
private InvoiceDetailsModel InvoicePopulatePayments(InvoiceEntity invoice)
|
||||
{
|
||||
var overpaid = false;
|
||||
var stillDue = false;
|
||||
var hasRates = 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 hasPayment = accounting.CryptoPaid > 0;
|
||||
var overpaidAmount = accounting.OverpaidHelper;
|
||||
|
||||
if (overpaidAmount > 0)
|
||||
{
|
||||
overpaid = true;
|
||||
}
|
||||
var rate = ExchangeRate(data.GetId().CryptoCode, data);
|
||||
|
||||
if (rate is not null) hasRates = true;
|
||||
if (hasPayment && overpaidAmount > 0) overpaid = true;
|
||||
if (hasPayment && accounting.Due > 0) stillDue = true;
|
||||
|
||||
return new InvoiceDetailsModel.CryptoPayment
|
||||
{
|
||||
Rate = rate,
|
||||
PaymentMethodRaw = data,
|
||||
PaymentMethodId = paymentMethodId,
|
||||
PaymentMethod = paymentMethodId.ToPrettyString(),
|
||||
Due = _displayFormatter.Currency(accounting.Due, paymentMethodId.CryptoCode),
|
||||
Paid = _displayFormatter.Currency(accounting.CryptoPaid, paymentMethodId.CryptoCode),
|
||||
Overpaid = _displayFormatter.Currency(overpaidAmount, paymentMethodId.CryptoCode),
|
||||
Address = data.GetPaymentMethodDetails().GetPaymentDestination(),
|
||||
Rate = ExchangeRate(data.GetId().CryptoCode, data),
|
||||
PaymentMethodRaw = data
|
||||
TotalDue = _displayFormatter.Currency(accounting.TotalDue, paymentMethodId.CryptoCode),
|
||||
Due = hasPayment ? _displayFormatter.Currency(accounting.Due, paymentMethodId.CryptoCode) : null,
|
||||
Paid = hasPayment ? _displayFormatter.Currency(accounting.CryptoPaid, paymentMethodId.CryptoCode) : null,
|
||||
Overpaid = hasPayment ? _displayFormatter.Currency(overpaidAmount, paymentMethodId.CryptoCode) : null,
|
||||
Address = data.GetPaymentMethodDetails().GetPaymentDestination()
|
||||
};
|
||||
}).ToList()
|
||||
}).ToList(),
|
||||
Overpaid = overpaid,
|
||||
StillDue = stillDue,
|
||||
HasRates = hasRates
|
||||
};
|
||||
model.Overpaid = overpaid;
|
||||
|
||||
return model;
|
||||
}
|
||||
@ -628,58 +635,56 @@ namespace BTCPayServer.Controllers
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanViewInvoices)]
|
||||
public async Task<IActionResult> MassAction(string command, string[] selectedItems, string? storeId = null)
|
||||
{
|
||||
if (selectedItems != null)
|
||||
IActionResult NotSupported(string err)
|
||||
{
|
||||
switch (command)
|
||||
{
|
||||
case "archive":
|
||||
await _InvoiceRepository.MassArchive(selectedItems);
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"{selectedItems.Length} invoice{(selectedItems.Length == 1 ? "" : "s")} archived.";
|
||||
break;
|
||||
TempData[WellKnownTempData.ErrorMessage] = err;
|
||||
return RedirectToAction(nameof(ListInvoices), new { storeId });
|
||||
}
|
||||
if (selectedItems.Length == 0)
|
||||
return NotSupported("No invoice has been selected");
|
||||
|
||||
case "unarchive":
|
||||
await _InvoiceRepository.MassArchive(selectedItems, false);
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"{selectedItems.Length} invoice{(selectedItems.Length == 1 ? "" : "s")} unarchived.";
|
||||
break;
|
||||
case "cpfp":
|
||||
if (selectedItems.Length == 0)
|
||||
return NotSupported("No invoice has been selected");
|
||||
var network = _NetworkProvider.DefaultNetwork;
|
||||
var explorer = _ExplorerClients.GetExplorerClient(network);
|
||||
IActionResult NotSupported(string err)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = err;
|
||||
return RedirectToAction(nameof(ListInvoices), new { storeId });
|
||||
}
|
||||
if (explorer is null)
|
||||
return NotSupported("This feature is only available to BTC wallets");
|
||||
if (!GetCurrentStore().HasPermission(GetUserId(), Policies.CanModifyStoreSettings))
|
||||
return Forbid();
|
||||
switch (command)
|
||||
{
|
||||
case "archive":
|
||||
await _InvoiceRepository.MassArchive(selectedItems);
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"{selectedItems.Length} invoice{(selectedItems.Length == 1 ? "" : "s")} archived.";
|
||||
break;
|
||||
|
||||
var derivationScheme = (this.GetCurrentStore().GetDerivationSchemeSettings(_NetworkProvider, network.CryptoCode))?.AccountDerivation;
|
||||
if (derivationScheme is null)
|
||||
return NotSupported("This feature is only available to BTC wallets");
|
||||
var bumpableAddresses = (await GetAddresses(selectedItems))
|
||||
.Where(p => p.GetPaymentMethodId().IsBTCOnChain)
|
||||
.Select(p => p.GetAddress()).ToHashSet();
|
||||
var utxos = await explorer.GetUTXOsAsync(derivationScheme);
|
||||
var bumpableUTXOs = utxos.GetUnspentUTXOs().Where(u => u.Confirmations == 0 && bumpableAddresses.Contains(u.ScriptPubKey.Hash.ToString())).ToArray();
|
||||
var parameters = new MultiValueDictionary<string, string>();
|
||||
foreach (var utxo in bumpableUTXOs)
|
||||
{
|
||||
parameters.Add($"outpoints[]", utxo.Outpoint.ToString());
|
||||
}
|
||||
return View("PostRedirect", new PostRedirectViewModel
|
||||
{
|
||||
AspController = "UIWallets",
|
||||
AspAction = nameof(UIWalletsController.WalletCPFP),
|
||||
RouteParameters = {
|
||||
{ "walletId", new WalletId(storeId, network.CryptoCode).ToString() },
|
||||
{ "returnUrl", Url.Action(nameof(ListInvoices), new { storeId }) }
|
||||
},
|
||||
FormParameters = parameters,
|
||||
});
|
||||
}
|
||||
case "unarchive":
|
||||
await _InvoiceRepository.MassArchive(selectedItems, false);
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"{selectedItems.Length} invoice{(selectedItems.Length == 1 ? "" : "s")} unarchived.";
|
||||
break;
|
||||
case "cpfp":
|
||||
var network = _NetworkProvider.DefaultNetwork;
|
||||
var explorer = _ExplorerClients.GetExplorerClient(network);
|
||||
if (explorer is null)
|
||||
return NotSupported("This feature is only available to BTC wallets");
|
||||
if (!GetCurrentStore().HasPermission(GetUserId(), Policies.CanModifyStoreSettings))
|
||||
return Forbid();
|
||||
|
||||
var derivationScheme = (this.GetCurrentStore().GetDerivationSchemeSettings(_NetworkProvider, network.CryptoCode))?.AccountDerivation;
|
||||
if (derivationScheme is null)
|
||||
return NotSupported("This feature is only available to BTC wallets");
|
||||
var bumpableAddresses = (await GetAddresses(selectedItems))
|
||||
.Where(p => p.GetPaymentMethodId().IsBTCOnChain)
|
||||
.Select(p => p.GetAddress()).ToHashSet();
|
||||
var utxos = await explorer.GetUTXOsAsync(derivationScheme);
|
||||
var bumpableUTXOs = utxos.GetUnspentUTXOs().Where(u => u.Confirmations == 0 && bumpableAddresses.Contains(u.ScriptPubKey.Hash.ToString())).ToArray();
|
||||
var parameters = new MultiValueDictionary<string, string>();
|
||||
foreach (var utxo in bumpableUTXOs)
|
||||
{
|
||||
parameters.Add($"outpoints[]", utxo.Outpoint.ToString());
|
||||
}
|
||||
return View("PostRedirect", new PostRedirectViewModel
|
||||
{
|
||||
AspController = "UIWallets",
|
||||
AspAction = nameof(UIWalletsController.WalletCPFP),
|
||||
RouteParameters = {
|
||||
{ "walletId", new WalletId(storeId, network.CryptoCode).ToString() },
|
||||
{ "returnUrl", Url.Action(nameof(ListInvoices), new { storeId }) }
|
||||
},
|
||||
FormParameters = parameters,
|
||||
});
|
||||
}
|
||||
return RedirectToAction(nameof(ListInvoices), new { storeId });
|
||||
}
|
||||
@ -1147,7 +1152,7 @@ namespace BTCPayServer.Controllers
|
||||
CanMarkInvalid = state.CanMarkInvalid(),
|
||||
CanMarkSettled = state.CanMarkComplete(),
|
||||
Details = InvoicePopulatePayments(invoice),
|
||||
HasRefund = invoice.Refunds.Any(data => !data.PullPaymentData.Archived)
|
||||
HasRefund = invoice.Refunds.Any()
|
||||
});
|
||||
}
|
||||
return View(model);
|
||||
@ -1254,6 +1259,19 @@ namespace BTCPayServer.Controllers
|
||||
model.CheckoutType = storeBlob.CheckoutType;
|
||||
model.AvailablePaymentMethods = GetPaymentMethodsSelectList();
|
||||
|
||||
JObject? metadataObj = null;
|
||||
if (!string.IsNullOrEmpty(model.Metadata))
|
||||
{
|
||||
try
|
||||
{
|
||||
metadataObj = JObject.Parse(model.Metadata);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.Metadata), "Metadata was not valid JSON");
|
||||
}
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
@ -1272,17 +1290,27 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
try
|
||||
{
|
||||
var metadata = metadataObj is null ? new InvoiceMetadata() : InvoiceMetadata.FromJObject(metadataObj);
|
||||
if (!string.IsNullOrEmpty(model.OrderId))
|
||||
{
|
||||
metadata.OrderId = model.OrderId;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(model.ItemDesc))
|
||||
{
|
||||
metadata.ItemDesc = model.ItemDesc;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(model.BuyerEmail))
|
||||
{
|
||||
metadata.BuyerEmail = model.BuyerEmail;
|
||||
}
|
||||
|
||||
var result = await CreateInvoiceCoreRaw(new CreateInvoiceRequest()
|
||||
{
|
||||
Amount = model.Amount,
|
||||
Currency = model.Currency,
|
||||
Metadata = new InvoiceMetadata()
|
||||
{
|
||||
PosDataLegacy = model.PosData,
|
||||
OrderId = model.OrderId,
|
||||
ItemDesc = model.ItemDesc,
|
||||
BuyerEmail = model.BuyerEmail,
|
||||
}.ToJObject(),
|
||||
Metadata = metadata.ToJObject(),
|
||||
Checkout = new ()
|
||||
{
|
||||
RedirectURL = store.StoreWebsite,
|
||||
|
@ -92,7 +92,7 @@ namespace BTCPayServer
|
||||
}
|
||||
|
||||
[HttpGet("withdraw/pp/{pullPaymentId}")]
|
||||
public async Task<IActionResult> GetLNURLForPullPayment(string cryptoCode, string pullPaymentId, string pr, CancellationToken cancellationToken)
|
||||
public async Task<IActionResult> GetLNURLForPullPayment(string cryptoCode, string pullPaymentId, [FromQuery] string pr, CancellationToken cancellationToken)
|
||||
{
|
||||
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
||||
if (network is null || !network.SupportLightning)
|
||||
|
@ -205,6 +205,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
vm.StoreName = store.StoreName;
|
||||
vm.StoreWebsite = store.StoreWebsite;
|
||||
vm.BrandColor = storeBlob.BrandColor;
|
||||
vm.LogoFileId = storeBlob.LogoFileId;
|
||||
vm.CssFileId = storeBlob.CssFileId;
|
||||
|
@ -55,7 +55,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpGet("pull-payments/{pullPaymentId}")]
|
||||
public async Task<IActionResult> ViewPullPayment(string pullPaymentId)
|
||||
public async Task<IActionResult> ViewPullPayment(string pullPaymentId, [FromQuery] bool print = false)
|
||||
{
|
||||
using var ctx = _dbContextFactory.CreateContext();
|
||||
var pp = await ctx.PullPayments.FindAsync(pullPaymentId);
|
||||
@ -69,14 +69,14 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var payouts = (await ctx.Payouts.GetPayoutInPeriod(pp)
|
||||
.OrderByDescending(o => o.Date)
|
||||
.ToListAsync())
|
||||
.Select(o => new
|
||||
{
|
||||
Entity = o,
|
||||
Blob = o.GetBlob(_serializerSettings),
|
||||
ProofBlob = _payoutHandlers.FindPayoutHandler(o.GetPaymentMethodId())?.ParseProof(o)
|
||||
});
|
||||
.OrderByDescending(o => o.Date)
|
||||
.ToListAsync())
|
||||
.Select(o => new
|
||||
{
|
||||
Entity = o,
|
||||
Blob = o.GetBlob(_serializerSettings),
|
||||
ProofBlob = _payoutHandlers.FindPayoutHandler(o.GetPaymentMethodId())?.ParseProof(o)
|
||||
});
|
||||
var cd = _currencyNameTable.GetCurrencyData(blob.Currency, false);
|
||||
var totalPaid = payouts.Where(p => p.Entity.State != PayoutState.Cancelled).Select(p => p.Blob.Amount).Sum();
|
||||
var amountDue = blob.Limit - totalPaid;
|
||||
@ -91,18 +91,18 @@ namespace BTCPayServer.Controllers
|
||||
CurrencyData = cd,
|
||||
StartDate = pp.StartDate,
|
||||
LastRefreshed = DateTime.UtcNow,
|
||||
Payouts = payouts
|
||||
.Select(entity => new ViewPullPaymentModel.PayoutLine
|
||||
{
|
||||
Id = entity.Entity.Id,
|
||||
Amount = entity.Blob.Amount,
|
||||
Currency = blob.Currency,
|
||||
Status = entity.Entity.State,
|
||||
Destination = entity.Blob.Destination,
|
||||
PaymentMethod = PaymentMethodId.Parse(entity.Entity.PaymentMethodId),
|
||||
Link = entity.ProofBlob?.Link,
|
||||
TransactionId = entity.ProofBlob?.Id
|
||||
}).ToList()
|
||||
Payouts = payouts.Select(entity => new ViewPullPaymentModel.PayoutLine
|
||||
{
|
||||
Id = entity.Entity.Id,
|
||||
Amount = entity.Blob.Amount,
|
||||
AmountFormatted = _displayFormatter.Currency(entity.Blob.Amount, blob.Currency),
|
||||
Currency = blob.Currency,
|
||||
Status = entity.Entity.State,
|
||||
Destination = entity.Blob.Destination,
|
||||
PaymentMethod = PaymentMethodId.Parse(entity.Entity.PaymentMethodId),
|
||||
Link = entity.ProofBlob?.Link,
|
||||
TransactionId = entity.ProofBlob?.Id
|
||||
}).ToList()
|
||||
};
|
||||
vm.IsPending &= vm.AmountDue > 0.0m;
|
||||
|
||||
@ -111,8 +111,9 @@ namespace BTCPayServer.Controllers
|
||||
var url = Url.Action("GetLNURLForPullPayment", "UILNURL", new { cryptoCode = _networkProvider.DefaultNetwork.CryptoCode, pullPaymentId = vm.Id }, Request.Scheme, Request.Host.ToString());
|
||||
vm.LnurlEndpoint = url != null ? new Uri(url) : null;
|
||||
}
|
||||
|
||||
return View(nameof(ViewPullPayment), vm);
|
||||
|
||||
|
||||
return View(print ? "ViewPullPaymentPrint" : "ViewPullPayment", vm);
|
||||
}
|
||||
|
||||
[HttpGet("stores/{storeId}/pull-payments/edit/{pullPaymentId}")]
|
||||
@ -176,31 +177,53 @@ namespace BTCPayServer.Controllers
|
||||
[HttpPost("pull-payments/{pullPaymentId}/claim")]
|
||||
public async Task<IActionResult> ClaimPullPayment(string pullPaymentId, ViewPullPaymentModel vm, CancellationToken cancellationToken)
|
||||
{
|
||||
using var ctx = _dbContextFactory.CreateContext();
|
||||
await using var ctx = _dbContextFactory.CreateContext();
|
||||
var pp = await ctx.PullPayments.FindAsync(pullPaymentId);
|
||||
if (pp is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(pullPaymentId), "This pull payment does not exists");
|
||||
}
|
||||
|
||||
var ppBlob = pp.GetBlob();
|
||||
|
||||
var paymentMethodId = ppBlob.SupportedPaymentMethods.FirstOrDefault(id => vm.SelectedPaymentMethod == id.ToString());
|
||||
|
||||
var payoutHandler = paymentMethodId is null ? null : _payoutHandlers.FindPayoutHandler(paymentMethodId);
|
||||
if (payoutHandler is null)
|
||||
if (string.IsNullOrEmpty(vm.Destination))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.SelectedPaymentMethod), "Invalid destination with selected payment method");
|
||||
ModelState.AddModelError(nameof(vm.Destination), "Please provide a destination");
|
||||
return await ViewPullPayment(pullPaymentId);
|
||||
}
|
||||
var destination = await payoutHandler.ParseAndValidateClaimDestination(paymentMethodId, vm.Destination, ppBlob, cancellationToken);
|
||||
if (destination.destination is null)
|
||||
|
||||
var ppBlob = pp.GetBlob();
|
||||
var supported = ppBlob.SupportedPaymentMethods;
|
||||
PaymentMethodId paymentMethodId = null;
|
||||
IClaimDestination destination = null;
|
||||
if (string.IsNullOrEmpty(vm.SelectedPaymentMethod))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Destination), destination.error ?? "Invalid destination with selected payment method");
|
||||
foreach (var pmId in supported)
|
||||
{
|
||||
var handler = _payoutHandlers.FindPayoutHandler(pmId);
|
||||
(IClaimDestination dst, string err) = handler == null
|
||||
? (null, "No payment handler found for this payment method")
|
||||
: await handler.ParseAndValidateClaimDestination(pmId, vm.Destination, ppBlob, cancellationToken);
|
||||
if (dst is not null && err is null)
|
||||
{
|
||||
paymentMethodId = pmId;
|
||||
destination = dst;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
paymentMethodId = supported.FirstOrDefault(id => vm.SelectedPaymentMethod == id.ToString());
|
||||
var payoutHandler = paymentMethodId is null ? null : _payoutHandlers.FindPayoutHandler(paymentMethodId);
|
||||
destination = payoutHandler is null ? null : (await payoutHandler.ParseAndValidateClaimDestination(paymentMethodId, vm.Destination, ppBlob, cancellationToken)).destination;
|
||||
}
|
||||
|
||||
if (destination is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Destination), "Invalid destination or payment method");
|
||||
return await ViewPullPayment(pullPaymentId);
|
||||
}
|
||||
|
||||
var amtError = ClaimRequest.IsPayoutAmountOk(destination.destination, vm.ClaimedAmount == 0? null: vm.ClaimedAmount, paymentMethodId.CryptoCode, ppBlob.Currency);
|
||||
var amtError = ClaimRequest.IsPayoutAmountOk(destination, vm.ClaimedAmount == 0? null: vm.ClaimedAmount, paymentMethodId.CryptoCode, ppBlob.Currency);
|
||||
if (amtError.error is not null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ClaimedAmount), amtError.error );
|
||||
@ -215,9 +238,9 @@ namespace BTCPayServer.Controllers
|
||||
return await ViewPullPayment(pullPaymentId);
|
||||
}
|
||||
|
||||
var result = await _pullPaymentHostedService.Claim(new ClaimRequest()
|
||||
var result = await _pullPaymentHostedService.Claim(new ClaimRequest
|
||||
{
|
||||
Destination = destination.destination,
|
||||
Destination = destination,
|
||||
PullPaymentId = pullPaymentId,
|
||||
Value = vm.ClaimedAmount,
|
||||
PaymentMethodId = paymentMethodId
|
||||
@ -225,14 +248,9 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
if (result.Result != ClaimRequest.ClaimResult.Ok)
|
||||
{
|
||||
if (result.Result == ClaimRequest.ClaimResult.AmountTooLow)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ClaimedAmount), ClaimRequest.GetErrorMessage(result.Result));
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, ClaimRequest.GetErrorMessage(result.Result));
|
||||
}
|
||||
ModelState.AddModelError(
|
||||
result.Result == ClaimRequest.ClaimResult.AmountTooLow ? nameof(vm.ClaimedAmount) : string.Empty,
|
||||
ClaimRequest.GetErrorMessage(result.Result));
|
||||
return await ViewPullPayment(pullPaymentId);
|
||||
}
|
||||
|
||||
|
@ -66,6 +66,10 @@ public partial class UIReportsController
|
||||
decimal randomValue = ((decimal)rand.NextDouble() * range) + from;
|
||||
return decimal.Round(randomValue, precision);
|
||||
}
|
||||
|
||||
var fiatCurrency = rand.NextSingle() > 0.2 ? "USD" : TakeOne("JPY", "EUR", "CHF");
|
||||
var cryptoCurrency = rand.NextSingle() > 0.2 ? "BTC" : TakeOne("LTC", "DOGE", "DASH");
|
||||
|
||||
if (f.Type == "invoice_id")
|
||||
return Encoders.Base58.EncodeData(GenerateBytes(20));
|
||||
if (f.Type == "boolean")
|
||||
@ -80,9 +84,9 @@ public partial class UIReportsController
|
||||
if (f.Name == "Address")
|
||||
return Encoders.Bech32("bc1").Encode(0, GenerateBytes(20));
|
||||
if (f.Name == "Crypto")
|
||||
return rand.NextSingle() > 0.2 ? "BTC" : TakeOne("LTC", "DOGE", "DASH");
|
||||
return cryptoCurrency;
|
||||
if (f.Name == "CryptoAmount")
|
||||
return GenerateDecimal(0.1m, 5m, 8);
|
||||
return DisplayFormatter.ToFormattedAmount(GenerateDecimal(0.1m, 5m, 8), cryptoCurrency);
|
||||
if (f.Name == "LightningAddress")
|
||||
return TakeOne("satoshi", "satosan", "satoichi") + "@bitcoin.org";
|
||||
if (f.Name == "BalanceChange")
|
||||
@ -98,24 +102,30 @@ public partial class UIReportsController
|
||||
if (f.Name == "Quantity")
|
||||
return TakeOne(1, 2, 3, 4, 5);
|
||||
if (f.Name == "Currency")
|
||||
return rand.NextSingle() > 0.2 ? "USD" : TakeOne("JPY", "EUR", "CHF");
|
||||
return fiatCurrency;
|
||||
if (f.Name == "CurrencyAmount")
|
||||
return row[fi - 1] switch
|
||||
{
|
||||
var curr = row[fi - 1]?.ToString();
|
||||
var value = curr switch
|
||||
{
|
||||
"USD" or "EUR" or "CHF" => GenerateDecimal(100.0m, 10_000m, 2),
|
||||
"JPY" => GenerateDecimal(10_000m, 1000_0000, 0),
|
||||
_ => GenerateDecimal(100.0m, 10_000m, 2)
|
||||
};
|
||||
return DisplayFormatter.ToFormattedAmount(value, curr);
|
||||
}
|
||||
if (f.Type == "tx_id")
|
||||
return Encoders.Hex.EncodeData(GenerateBytes(32));
|
||||
if (f.Name == "Rate")
|
||||
{
|
||||
return row[fi - 1] switch
|
||||
var curr = row[fi - 1]?.ToString();
|
||||
var value = curr switch
|
||||
{
|
||||
"USD" or "EUR" or "CHF" => GenerateDecimal(30_000m, 60_000, 2),
|
||||
"JPY" => GenerateDecimal(400_0000m, 1000_0000m, 0),
|
||||
_ => GenerateDecimal(30_000m, 60_000, 2)
|
||||
};
|
||||
return DisplayFormatter.ToFormattedAmount(value, curr);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -1,6 +1,4 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using Dapper;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
@ -10,19 +8,11 @@ using BTCPayServer.Controllers.GreenField;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models.StoreReportsViewModels;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Internal;
|
||||
using System.Text.Json.Nodes;
|
||||
using Org.BouncyCastle.Ocsp;
|
||||
using System.Threading;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NBitcoin.DataEncoders;
|
||||
|
||||
|
||||
namespace BTCPayServer.Controllers;
|
||||
|
||||
@ -35,16 +25,18 @@ public partial class UIReportsController : Controller
|
||||
ApplicationDbContextFactory dbContextFactory,
|
||||
GreenfieldReportsController api,
|
||||
ReportService reportService,
|
||||
BTCPayServerEnvironment env
|
||||
)
|
||||
DisplayFormatter displayFormatter,
|
||||
BTCPayServerEnvironment env)
|
||||
{
|
||||
Api = api;
|
||||
ReportService = reportService;
|
||||
Env = env;
|
||||
DBContextFactory = dbContextFactory;
|
||||
NetworkProvider = networkProvider;
|
||||
DisplayFormatter = displayFormatter;
|
||||
}
|
||||
private BTCPayNetworkProvider NetworkProvider { get; }
|
||||
private DisplayFormatter DisplayFormatter { get; }
|
||||
public GreenfieldReportsController Api { get; }
|
||||
public ReportService ReportService { get; }
|
||||
public BTCPayServerEnvironment Env { get; }
|
||||
@ -72,20 +64,17 @@ public partial class UIReportsController : Controller
|
||||
string storeId,
|
||||
string ? viewName = null)
|
||||
{
|
||||
var vm = new StoreReportsViewModel()
|
||||
var vm = new StoreReportsViewModel
|
||||
{
|
||||
InvoiceTemplateUrl = this.Url.Action(nameof(UIInvoiceController.Invoice), "UIInvoice", new { invoiceId = "INVOICE_ID" }),
|
||||
InvoiceTemplateUrl = Url.Action(nameof(UIInvoiceController.Invoice), "UIInvoice", new { invoiceId = "INVOICE_ID" }),
|
||||
ExplorerTemplateUrls = NetworkProvider.GetAll().ToDictionary(network => network.CryptoCode, network => network.BlockExplorerLink?.Replace("{0}", "TX_ID")),
|
||||
Request = new StoreReportRequest()
|
||||
{
|
||||
ViewName = viewName ?? "Payments"
|
||||
}
|
||||
Request = new StoreReportRequest { ViewName = viewName ?? "Payments" },
|
||||
AvailableViews = ReportService.ReportProviders
|
||||
.Values
|
||||
.Where(r => r.IsAvailable())
|
||||
.Select(k => k.Name)
|
||||
.OrderBy(k => k).ToList()
|
||||
};
|
||||
vm.AvailableViews = ReportService.ReportProviders
|
||||
.Values
|
||||
.Where(r => r.IsAvailable())
|
||||
.Select(k => k.Name)
|
||||
.OrderBy(k => k).ToList();
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
@ -62,6 +62,14 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
for (var i = 0; index < vm.Rules.Count; index++)
|
||||
{
|
||||
var rule = vm.Rules[i];
|
||||
if (!rule.CustomerEmail && string.IsNullOrEmpty(rule.To))
|
||||
ModelState.AddModelError($"{nameof(vm.Rules)}[{i}].{nameof(rule.To)}", "Either recipient or \"Send the email to the buyer\" is required");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(vm);
|
||||
@ -121,7 +129,6 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
public bool CustomerEmail { get; set; }
|
||||
|
||||
[Required]
|
||||
[MailboxAddress]
|
||||
public string To { get; set; }
|
||||
|
||||
|
@ -859,11 +859,10 @@ namespace BTCPayServer.Controllers
|
||||
return RedirectToAction(nameof(UIHomeController.Index), "UIHome");
|
||||
}
|
||||
|
||||
private IEnumerable<AvailableRateProvider> GetSupportedExchanges()
|
||||
private IEnumerable<RateSourceInfo> GetSupportedExchanges()
|
||||
{
|
||||
return _RateFactory.RateProviderFactory.AvailableRateProviders
|
||||
.Where(r => !string.IsNullOrWhiteSpace(r.Name))
|
||||
.OrderBy(s => s.Id, StringComparer.OrdinalIgnoreCase);
|
||||
.OrderBy(s => s.DisplayName, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
}
|
||||
|
||||
|
@ -125,12 +125,11 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
var exchanges = _rateFactory.RateProviderFactory
|
||||
.AvailableRateProviders
|
||||
.Where(r => !string.IsNullOrWhiteSpace(r.Name))
|
||||
.OrderBy(s => s.Id, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
exchanges.Insert(0, new AvailableRateProvider(null, "Recommended", ""));
|
||||
exchanges.Insert(0, new (null, "Recommended", ""));
|
||||
var chosen = exchanges.FirstOrDefault(f => f.Id == selected) ?? exchanges.First();
|
||||
return new SelectList(exchanges, nameof(chosen.Id), nameof(chosen.Name), chosen.Id);
|
||||
return new SelectList(exchanges, nameof(chosen.Id), nameof(chosen.DisplayName), chosen.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -50,9 +50,10 @@ namespace BTCPayServer.Controllers
|
||||
if (network == null)
|
||||
return NotFound();
|
||||
var websocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||
var vaultClient = new VaultClient(websocket);
|
||||
var hwi = new Hwi.HwiClient(network.NBitcoinNetwork)
|
||||
{
|
||||
Transport = new HwiWebSocketTransport(websocket)
|
||||
Transport = new VaultHWITransport(vaultClient)
|
||||
};
|
||||
Hwi.HwiDeviceClient device = null;
|
||||
HwiEnumerateEntry deviceEntry = null;
|
||||
|
@ -383,10 +383,18 @@ namespace BTCPayServer.Controllers
|
||||
private async Task SendFreeMoney(Cheater cheater, WalletId walletId, DerivationSchemeSettings paymentMethod)
|
||||
{
|
||||
var c = this.ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode);
|
||||
var cashCow = cheater.GetCashCow(walletId.CryptoCode);
|
||||
#if ALTCOINS
|
||||
if (walletId.CryptoCode == "LBTC")
|
||||
{
|
||||
await cashCow.SendCommandAsync("rescanblockchain");
|
||||
}
|
||||
#endif
|
||||
var addresses = Enumerable.Range(0, 200).Select(_ => c.GetUnusedAsync(paymentMethod.AccountDerivation, DerivationFeature.Deposit, reserve: true)).ToArray();
|
||||
|
||||
await Task.WhenAll(addresses);
|
||||
await cheater.CashCow.GenerateAsync(addresses.Length / 8);
|
||||
var b = cheater.CashCow.PrepareBatch();
|
||||
await cashCow.GenerateAsync(addresses.Length / 8);
|
||||
var b = cashCow.PrepareBatch();
|
||||
Random r = new Random();
|
||||
List<Task<uint256>> sending = new List<Task<uint256>>();
|
||||
foreach (var a in addresses)
|
||||
@ -394,7 +402,7 @@ namespace BTCPayServer.Controllers
|
||||
sending.Add(b.SendToAddressAsync((await a).Address, Money.Coins(0.1m) + Money.Satoshis(r.Next(0, 90_000_000))));
|
||||
}
|
||||
await b.SendBatchAsync();
|
||||
await cheater.CashCow.GenerateAsync(1);
|
||||
await cashCow.GenerateAsync(1);
|
||||
|
||||
var factory = ServiceProvider.GetRequiredService<NBXplorerConnectionFactory>();
|
||||
|
||||
@ -869,8 +877,7 @@ namespace BTCPayServer.Controllers
|
||||
try
|
||||
{
|
||||
var uriBuilder = new NBitcoin.Payment.BitcoinUrlBuilder(bip21, network.NBitcoinNetwork);
|
||||
|
||||
vm.Outputs.Add(new WalletSendModel.TransactionOutput()
|
||||
var output = new WalletSendModel.TransactionOutput
|
||||
{
|
||||
Amount = uriBuilder.Amount?.ToDecimal(MoneyUnit.BTC),
|
||||
DestinationAddress = uriBuilder.Address?.ToString(),
|
||||
@ -878,15 +885,20 @@ namespace BTCPayServer.Controllers
|
||||
PayoutId = uriBuilder.UnknownParameters.ContainsKey("payout")
|
||||
? uriBuilder.UnknownParameters["payout"]
|
||||
: null
|
||||
});
|
||||
};
|
||||
if (!string.IsNullOrEmpty(uriBuilder.Label))
|
||||
{
|
||||
output.Labels = output.Labels.Append(uriBuilder.Label).ToArray();
|
||||
}
|
||||
vm.Outputs.Add(output);
|
||||
address = uriBuilder.Address;
|
||||
if (!string.IsNullOrEmpty(uriBuilder.Label) || !string.IsNullOrEmpty(uriBuilder.Message))
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Info,
|
||||
Html =
|
||||
$"Payment {(string.IsNullOrEmpty(uriBuilder.Label) ? string.Empty : $" to {uriBuilder.Label}")} {(string.IsNullOrEmpty(uriBuilder.Message) ? string.Empty : $" for {uriBuilder.Message}")}"
|
||||
$"Payment {(string.IsNullOrEmpty(uriBuilder.Label) ? string.Empty : $" to <strong>{uriBuilder.Label}</strong>")} {(string.IsNullOrEmpty(uriBuilder.Message) ? string.Empty : $" for <strong>{uriBuilder.Message}</strong>")}"
|
||||
});
|
||||
}
|
||||
|
||||
@ -918,7 +930,7 @@ namespace BTCPayServer.Controllers
|
||||
if (address is not null)
|
||||
{
|
||||
var addressLabels = await WalletRepository.GetWalletLabels(new WalletObjectId(walletId, WalletObjectData.Types.Address, address.ToString()));
|
||||
vm.Outputs.Last().Labels = addressLabels.Select(tuple => tuple.Label).ToArray();
|
||||
vm.Outputs.Last().Labels = vm.Outputs.Last().Labels.Concat(addressLabels.Select(tuple => tuple.Label)).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1336,7 +1348,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
"csv" => "text/csv",
|
||||
"json" => "application/json",
|
||||
"bip329" => "text/jsonl", // https://stackoverflow.com/questions/59938644/what-is-the-mime-type-of-jsonl-files
|
||||
"bip329" => "application/jsonl", // Ongoing discussion: https://github.com/wardi/jsonlines/issues/19
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(format), format, null)
|
||||
};
|
||||
var cd = new ContentDisposition
|
||||
|
@ -29,7 +29,7 @@ namespace BTCPayServer.Data
|
||||
if (index == -1)
|
||||
return PaymentMethodId.Parse("BTC");
|
||||
/////////////////////////
|
||||
return PaymentMethodId.Parse(addressInvoiceData.Address.Substring(index + 1));
|
||||
return PaymentMethodId.TryParse(addressInvoiceData.Address.Substring(index + 1));
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
@ -289,7 +289,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
||||
};
|
||||
}
|
||||
|
||||
var proofBlob = new PayoutLightningBlob() { PaymentHash = bolt11PaymentRequest.PaymentHash.ToString() };
|
||||
var proofBlob = new PayoutLightningBlob { PaymentHash = bolt11PaymentRequest.PaymentHash.ToString() };
|
||||
try
|
||||
{
|
||||
var result = await lightningClient.Pay(bolt11PaymentRequest.ToString(),
|
||||
@ -298,6 +298,8 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
||||
// CLN does not support explicit amount param if it is the same as the invoice amount
|
||||
Amount = payoutBlob.CryptoAmount == bolt11PaymentRequest.MinimumAmount.ToDecimal(LightMoneyUnit.BTC)? null: new LightMoney((decimal)payoutBlob.CryptoAmount, LightMoneyUnit.BTC)
|
||||
}, cancellationToken);
|
||||
if (result == null) throw new NoPaymentResultException();
|
||||
|
||||
string message = null;
|
||||
if (result.Result == PayResult.Ok)
|
||||
{
|
||||
@ -330,7 +332,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
||||
Message = message
|
||||
};
|
||||
}
|
||||
catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException)
|
||||
catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException or NoPaymentResultException)
|
||||
{
|
||||
// Timeout, potentially caused by hold invoices
|
||||
// Payment will be saved as pending, the LightningPendingPayoutListener will handle settling/cancelling
|
||||
@ -347,7 +349,6 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task SetStoreContext()
|
||||
{
|
||||
var storeId = HttpContext.GetUserPrefsCookie()?.CurrentStoreId;
|
||||
@ -377,4 +378,8 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
||||
public decimal Amount { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
public class NoPaymentResultException : Exception
|
||||
{
|
||||
}
|
||||
}
|
||||
|
@ -37,16 +37,9 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
var ppBlob = data.PullPaymentData?.GetBlob();
|
||||
var payoutBlob = data.GetBlob(jsonSerializerSettings);
|
||||
string payoutSource;
|
||||
if (payoutBlob.Metadata?.TryGetValue("source", StringComparison.InvariantCultureIgnoreCase,
|
||||
out var source) is true)
|
||||
{
|
||||
return source.Value<string>();
|
||||
}
|
||||
else
|
||||
{
|
||||
return ppBlob?.Name ?? data.PullPaymentDataId;
|
||||
}
|
||||
return payoutBlob.Metadata?.TryGetValue("source", StringComparison.InvariantCultureIgnoreCase, out var source) is true
|
||||
? source.Value<string>()
|
||||
: ppBlob?.Name ?? data.PullPaymentDataId;
|
||||
}
|
||||
|
||||
public static PayoutBlob GetBlob(this PayoutData data, BTCPayNetworkJsonSerializerSettings serializers)
|
||||
|
@ -198,9 +198,10 @@ namespace BTCPayServer.Data
|
||||
{ "CHF", "kraken" },
|
||||
{ "GTQ", "bitpay" },
|
||||
{ "COP", "yadio" },
|
||||
{ "ARS", "yadio" },
|
||||
{ "JPY", "bitbank" },
|
||||
{ "TRY", "btcturk" },
|
||||
{ "UGX", "exchangeratehost"},
|
||||
{ "UGX", "yadio"},
|
||||
{ "RSD", "bitpay"}
|
||||
};
|
||||
|
||||
@ -240,7 +241,7 @@ namespace BTCPayServer.Data
|
||||
|
||||
[DefaultValue(false)]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
public bool PlaySoundOnPayment { get; set; } = false;
|
||||
public bool PlaySoundOnPayment { get; set; }
|
||||
|
||||
public string SoundFileId { get; set; }
|
||||
|
||||
|
@ -8,7 +8,7 @@ namespace BTCPayServer
|
||||
{
|
||||
public static class MoneyExtensions
|
||||
{
|
||||
public static decimal GetValue(this IMoney m, BTCPayNetwork network = null)
|
||||
public static decimal GetValue(this IMoney m, BTCPayNetwork network)
|
||||
{
|
||||
switch (m)
|
||||
{
|
||||
|
@ -31,4 +31,9 @@ public class FieldValueMirror : IFormComponentProvider
|
||||
|
||||
return rawValue;
|
||||
}
|
||||
|
||||
public void SetValue(Field field, JToken value)
|
||||
{
|
||||
//ignored
|
||||
}
|
||||
}
|
||||
|
@ -43,9 +43,9 @@ public class FormDataService
|
||||
Field.Create("Address Line 1", "buyerAddress1", null, true, null),
|
||||
Field.Create("Address Line 2", "buyerAddress2", null, false, null),
|
||||
Field.Create("City", "buyerCity", null, true, null),
|
||||
Field.Create("Postcode", "buyerZip", null, false, null),
|
||||
Field.Create("Postcode", "buyerZip", null, true, null),
|
||||
Field.Create("State", "buyerState", null, false, null),
|
||||
new SelectField()
|
||||
new SelectField
|
||||
{
|
||||
Name = "buyerCountry",
|
||||
Label = "Country",
|
||||
@ -218,4 +218,36 @@ public class FormDataService
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
public void SetValues(Form form, JObject values)
|
||||
{
|
||||
|
||||
var fields = form.GetAllFields().ToDictionary(k => k.FullName, k => k.Field);
|
||||
SetValues(fields, new List<string>(), values);
|
||||
}
|
||||
|
||||
private void SetValues(Dictionary<string, Field> fields, List<string> path, JObject values)
|
||||
{
|
||||
foreach (var prop in values.Properties())
|
||||
{
|
||||
List<string> propPath = new List<string>(path.Count + 1);
|
||||
propPath.AddRange(path);
|
||||
propPath.Add(prop.Name);
|
||||
if (prop.Value.Type == JTokenType.Object)
|
||||
{
|
||||
SetValues(fields, propPath, (JObject)prop.Value);
|
||||
}
|
||||
else if (prop.Value.Type == JTokenType.String)
|
||||
{
|
||||
var fullName = string.Join('_', propPath.Where(s => !string.IsNullOrEmpty(s)));
|
||||
if (fields.TryGetValue(fullName, out var f) && !f.Constant)
|
||||
{
|
||||
if (_formProviders.TypeToComponentProvider.TryGetValue(f.Type, out var formComponentProvider))
|
||||
{
|
||||
formComponentProvider.SetValue(f, prop.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Forms;
|
||||
|
||||
@ -17,6 +18,11 @@ public class HtmlFieldsetFormProvider : IFormComponentProvider
|
||||
return null;
|
||||
}
|
||||
|
||||
public void SetValue(Field field, JToken value)
|
||||
{
|
||||
//ignored
|
||||
}
|
||||
|
||||
public void Validate(Form form, Field field)
|
||||
{
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Forms;
|
||||
|
||||
@ -10,6 +11,7 @@ public interface IFormComponentProvider
|
||||
void Validate(Form form, Field field);
|
||||
void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider);
|
||||
string GetValue(Form form, Field field);
|
||||
void SetValue(Field field, JToken value);
|
||||
}
|
||||
|
||||
public abstract class FormComponentProviderBase : IFormComponentProvider
|
||||
@ -21,6 +23,11 @@ public abstract class FormComponentProviderBase : IFormComponentProvider
|
||||
return field.Value;
|
||||
}
|
||||
|
||||
public void SetValue(Field field, JToken value)
|
||||
{
|
||||
field.Value = value.ToString();
|
||||
}
|
||||
|
||||
public abstract void Validate(Form form, Field field);
|
||||
|
||||
public void ValidateField<T>(Field field) where T : ValidationAttribute, new()
|
||||
|
@ -283,10 +283,10 @@ namespace BTCPayServer.Hosting
|
||||
services.AddSingleton<IPluginHookService, PluginHookService>(provider => provider.GetService<PluginHookService>());
|
||||
services.TryAddTransient<Safe>();
|
||||
services.TryAddTransient<DisplayFormatter>();
|
||||
services.TryAddSingleton<Ganss.XSS.HtmlSanitizer>(o =>
|
||||
services.TryAddSingleton<Ganss.Xss.HtmlSanitizer>(o =>
|
||||
{
|
||||
|
||||
var htmlSanitizer = new Ganss.XSS.HtmlSanitizer();
|
||||
var htmlSanitizer = new Ganss.Xss.HtmlSanitizer();
|
||||
|
||||
|
||||
htmlSanitizer.RemovingAtRule += (sender, args) =>
|
||||
@ -309,7 +309,7 @@ namespace BTCPayServer.Hosting
|
||||
{
|
||||
if (args.Tag.TagName.Equals("img", StringComparison.InvariantCultureIgnoreCase) &&
|
||||
args.Attribute.Name.Equals("src", StringComparison.InvariantCultureIgnoreCase) &&
|
||||
args.Reason == Ganss.XSS.RemoveReason.NotAllowedUrlValue)
|
||||
args.Reason == Ganss.Xss.RemoveReason.NotAllowedUrlValue)
|
||||
{
|
||||
args.Cancel = true;
|
||||
}
|
||||
@ -505,7 +505,7 @@ namespace BTCPayServer.Hosting
|
||||
// We need to be careful to only add exchanges which OnGetTickers implementation make only 1 request
|
||||
services.AddRateProviderExchangeSharp<ExchangeBinanceAPI>(new("binance", "Binance", "https://api.binance.com/api/v1/ticker/24hr"));
|
||||
services.AddRateProviderExchangeSharp<ExchangeBittrexAPI>(new("bittrex", "Bittrex", "https://bittrex.com/api/v1.1/public/getmarketsummaries"));
|
||||
services.AddRateProviderExchangeSharp<ExchangePoloniexAPI>(new("poloniex", "Poloniex", "https://poloniex.com/public?command=returnTicker"));
|
||||
services.AddRateProviderExchangeSharp<ExchangePoloniexAPI>(new("poloniex", "Poloniex", " https://api.poloniex.com/markets/price"));
|
||||
services.AddRateProviderExchangeSharp<ExchangeNDAXAPI>(new("ndax", "NDAX", "https://ndax.io/api/returnTicker"));
|
||||
|
||||
services.AddRateProviderExchangeSharp<ExchangeBitfinexAPI>(new("bitfinex", "Bitfinex", "https://api.bitfinex.com/v2/tickers?symbols=tBTCUSD,tLTCUSD,tLTCBTC,tETHUSD,tETHBTC,tETCBTC,tETCUSD,tRRTUSD,tRRTBTC,tZECUSD,tZECBTC,tXMRUSD,tXMRBTC,tDSHUSD,tDSHBTC,tBTCEUR,tBTCJPY,tXRPUSD,tXRPBTC,tIOTUSD,tIOTBTC,tIOTETH,tEOSUSD,tEOSBTC,tEOSETH,tSANUSD,tSANBTC,tSANETH,tOMGUSD,tOMGBTC,tOMGETH,tNEOUSD,tNEOBTC,tNEOETH,tETPUSD,tETPBTC,tETPETH,tQTMUSD,tQTMBTC,tQTMETH,tAVTUSD,tAVTBTC,tAVTETH,tEDOUSD,tEDOBTC,tEDOETH,tBTGUSD,tBTGBTC,tDATUSD,tDATBTC,tDATETH,tQSHUSD,tQSHBTC,tQSHETH,tYYWUSD,tYYWBTC,tYYWETH,tGNTUSD,tGNTBTC,tGNTETH,tSNTUSD,tSNTBTC,tSNTETH,tIOTEUR,tBATUSD,tBATBTC,tBATETH,tMNAUSD,tMNABTC,tMNAETH,tFUNUSD,tFUNBTC,tFUNETH,tZRXUSD,tZRXBTC,tZRXETH,tTNBUSD,tTNBBTC,tTNBETH,tSPKUSD,tSPKBTC,tSPKETH,tTRXUSD,tTRXBTC,tTRXETH,tRCNUSD,tRCNBTC,tRCNETH,tRLCUSD,tRLCBTC,tRLCETH,tAIDUSD,tAIDBTC,tAIDETH,tSNGUSD,tSNGBTC,tSNGETH,tREPUSD,tREPBTC,tREPETH,tELFUSD,tELFBTC,tELFETH,tNECUSD,tNECBTC,tNECETH,tBTCGBP,tETHEUR,tETHJPY,tETHGBP,tNEOEUR,tNEOJPY,tNEOGBP,tEOSEUR,tEOSJPY,tEOSGBP,tIOTJPY,tIOTGBP,tIOSUSD,tIOSBTC,tIOSETH,tAIOUSD,tAIOBTC,tAIOETH,tREQUSD,tREQBTC,tREQETH,tRDNUSD,tRDNBTC,tRDNETH,tLRCUSD,tLRCBTC,tLRCETH,tWAXUSD,tWAXBTC,tWAXETH,tDAIUSD,tDAIBTC,tDAIETH,tAGIUSD,tAGIBTC,tAGIETH,tBFTUSD,tBFTBTC,tBFTETH,tMTNUSD,tMTNBTC,tMTNETH,tODEUSD,tODEBTC,tODEETH,tANTUSD,tANTBTC,tANTETH,tDTHUSD,tDTHBTC,tDTHETH,tMITUSD,tMITBTC,tMITETH,tSTJUSD,tSTJBTC,tSTJETH,tXLMUSD,tXLMEUR,tXLMJPY,tXLMGBP,tXLMBTC,tXLMETH,tXVGUSD,tXVGEUR,tXVGJPY,tXVGGBP,tXVGBTC,tXVGETH,tBCIUSD,tBCIBTC,tMKRUSD,tMKRBTC,tMKRETH,tKNCUSD,tKNCBTC,tKNCETH,tPOAUSD,tPOABTC,tPOAETH,tEVTUSD,tLYMUSD,tLYMBTC,tLYMETH,tUTKUSD,tUTKBTC,tUTKETH,tVEEUSD,tVEEBTC,tVEEETH,tDADUSD,tDADBTC,tDADETH,tORSUSD,tORSBTC,tORSETH,tAUCUSD,tAUCBTC,tAUCETH,tPOYUSD,tPOYBTC,tPOYETH,tFSNUSD,tFSNBTC,tFSNETH,tCBTUSD,tCBTBTC,tCBTETH,tZCNUSD,tZCNBTC,tZCNETH,tSENUSD,tSENBTC,tSENETH,tNCAUSD,tNCABTC,tNCAETH,tCNDUSD,tCNDBTC,tCNDETH,tCTXUSD,tCTXBTC,tCTXETH,tPAIUSD,tPAIBTC,tSEEUSD,tSEEBTC,tSEEETH,tESSUSD,tESSBTC,tESSETH,tATMUSD,tATMBTC,tATMETH,tHOTUSD,tHOTBTC,tHOTETH,tDTAUSD,tDTABTC,tDTAETH,tIQXUSD,tIQXBTC,tIQXEOS,tWPRUSD,tWPRBTC,tWPRETH,tZILUSD,tZILBTC,tZILETH,tBNTUSD,tBNTBTC,tBNTETH,tABSUSD,tABSETH,tXRAUSD,tXRAETH,tMANUSD,tMANETH,tBBNUSD,tBBNETH,tNIOUSD,tNIOETH,tDGXUSD,tDGXETH,tVETUSD,tVETBTC,tVETETH,tUTNUSD,tUTNETH,tTKNUSD,tTKNETH,tGOTUSD,tGOTEUR,tGOTETH,tXTZUSD,tXTZBTC,tCNNUSD,tCNNETH,tBOXUSD,tBOXETH,tTRXEUR,tTRXGBP,tTRXJPY,tMGOUSD,tMGOETH,tRTEUSD,tRTEETH,tYGGUSD,tYGGETH,tMLNUSD,tMLNETH,tWTCUSD,tWTCETH,tCSXUSD,tCSXETH,tOMNUSD,tOMNBTC,tINTUSD,tINTETH,tDRNUSD,tDRNETH,tPNKUSD,tPNKETH,tDGBUSD,tDGBBTC,tBSVUSD,tBSVBTC,tBABUSD,tBABBTC,tWLOUSD,tWLOXLM,tVLDUSD,tVLDETH,tENJUSD,tENJETH,tONLUSD,tONLETH,tRBTUSD,tRBTBTC,tUSTUSD,tEUTEUR,tEUTUSD,tGSDUSD,tUDCUSD,tTSDUSD,tPAXUSD,tRIFUSD,tRIFBTC,tPASUSD,tPASETH,tVSYUSD,tVSYBTC,tZRXDAI,tMKRDAI,tOMGDAI,tBTTUSD,tBTTBTC,tBTCUST,tETHUST,tCLOUSD,tCLOBTC,tIMPUSD,tIMPETH,tLTCUST,tEOSUST,tBABUST,tSCRUSD,tSCRETH,tGNOUSD,tGNOETH,tGENUSD,tGENETH,tATOUSD,tATOBTC,tATOETH,tWBTUSD,tXCHUSD,tEUSUSD,tWBTETH,tXCHETH,tEUSETH,tLEOUSD,tLEOBTC,tLEOUST,tLEOEOS,tLEOETH,tASTUSD,tASTETH,tFOAUSD,tFOAETH,tUFRUSD,tUFRETH,tZBTUSD,tZBTUST,tOKBUSD,tUSKUSD,tGTXUSD,tKANUSD,tOKBUST,tOKBETH,tOKBBTC,tUSKUST,tUSKETH,tUSKBTC,tUSKEOS,tGTXUST,tKANUST,tAMPUSD,tALGUSD,tALGBTC,tALGUST,tBTCXCH,tSWMUSD,tSWMETH,tTRIUSD,tTRIETH,tLOOUSD,tLOOETH,tAMPUST,tDUSK:USD,tDUSK:BTC,tUOSUSD,tUOSBTC,tRRBUSD,tRRBUST,tDTXUSD,tDTXUST,tAMPBTC,tFTTUSD,tFTTUST,tPAXUST,tUDCUST,tTSDUST,tBTC:CNHT,tUST:CNHT,tCNH:CNHT,tCHZUSD,tCHZUST,tBTCF0:USTF0,tETHF0:USTF0"));
|
||||
@ -527,7 +527,6 @@ namespace BTCPayServer.Hosting
|
||||
services.AddRateProvider<YadioRateProvider>();
|
||||
services.AddRateProvider<BtcTurkRateProvider>();
|
||||
services.AddRateProvider<FreeCurrencyRatesRateProvider>();
|
||||
services.AddRateProvider<ExchangeRateHostRateProvider>();
|
||||
|
||||
// Broken
|
||||
// Providers.Add("argoneum", new ArgoneumRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_ARGONEUM")));
|
||||
|
@ -1,24 +0,0 @@
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class HwiWebSocketTransport : Hwi.Transports.ITransport
|
||||
{
|
||||
private readonly WebSocketHelper _webSocket;
|
||||
|
||||
public HwiWebSocketTransport(WebSocket webSocket)
|
||||
{
|
||||
_webSocket = new WebSocketHelper(webSocket);
|
||||
}
|
||||
public async Task<string> SendCommandAsync(string[] arguments, CancellationToken cancel)
|
||||
{
|
||||
JObject request = new JObject();
|
||||
request.Add("params", new JArray(arguments));
|
||||
await _webSocket.Send(request.ToString(), cancel);
|
||||
return await _webSocket.NextMessageAsync(cancel);
|
||||
}
|
||||
}
|
||||
}
|
@ -44,9 +44,8 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[DisplayName("POS Data")]
|
||||
public string PosData
|
||||
[DisplayName("Metadata")]
|
||||
public string Metadata
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
@ -41,6 +41,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public class CryptoPayment
|
||||
{
|
||||
public string PaymentMethod { get; set; }
|
||||
public string TotalDue { get; set; }
|
||||
public string Due { get; set; }
|
||||
public string Paid { get; set; }
|
||||
public string Address { get; internal set; }
|
||||
@ -124,7 +125,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
}
|
||||
public InvoiceMetadata TypedMetadata { get; set; }
|
||||
public DateTimeOffset MonitoringDate { get; internal set; }
|
||||
public List<Data.InvoiceEventData> Events { get; internal set; }
|
||||
public List<InvoiceEventData> Events { get; internal set; }
|
||||
public string NotificationEmail { get; internal set; }
|
||||
public Dictionary<string, object> Metadata { get; set; }
|
||||
public Dictionary<string, object> ReceiptData { get; set; }
|
||||
@ -133,11 +134,11 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public bool Archived { get; set; }
|
||||
public bool CanRefund { get; set; }
|
||||
public bool ShowCheckout { get; set; }
|
||||
public bool CanMarkSettled { get; set; }
|
||||
public bool CanMarkInvalid { get; set; }
|
||||
public bool CanMarkStatus => CanMarkSettled || CanMarkInvalid;
|
||||
public List<RefundData> Refunds { get; set; }
|
||||
public bool ShowReceipt { get; set; }
|
||||
public bool Overpaid { get; set; } = false;
|
||||
public bool Overpaid { get; set; }
|
||||
public bool HasRefund { get; set; }
|
||||
public bool StillDue { get; set; }
|
||||
public bool HasRates { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public List<InvoiceModel> Invoices { get; set; } = new ();
|
||||
public override int CurrentPageCount => Invoices.Count;
|
||||
public string StoreId { get; set; }
|
||||
|
||||
public string SearchText { get; set; }
|
||||
public SearchString Search { get; set; }
|
||||
public List<InvoiceAppModel> Apps { get; set; }
|
||||
@ -22,16 +21,13 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public string OrderId { get; set; }
|
||||
public string RedirectUrl { get; set; }
|
||||
public string InvoiceId { get; set; }
|
||||
|
||||
public InvoiceState Status { get; set; }
|
||||
public bool CanMarkSettled { get; set; }
|
||||
public bool CanMarkInvalid { get; set; }
|
||||
public bool CanMarkStatus => CanMarkSettled || CanMarkInvalid;
|
||||
public bool ShowCheckout { get; set; }
|
||||
public string ExceptionStatus { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
public string Currency { get; set; }
|
||||
|
||||
public InvoiceDetailsModel Details { get; set; }
|
||||
public bool HasRefund { get; set; }
|
||||
}
|
||||
|
@ -72,7 +72,11 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
|
||||
|
||||
[Display(Name = "Expiration Date")]
|
||||
public DateTime? ExpiryDate { get; set; }
|
||||
[Required] public string Title { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Title { get; set; }
|
||||
|
||||
[Display(Name = "Memo")]
|
||||
public string Description { get; set; }
|
||||
|
||||
[Display(Name = "Store")]
|
||||
@ -87,7 +91,8 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
|
||||
|
||||
[Display(Name = "Custom CSS Code")]
|
||||
public string EmbeddedCSS { get; set; }
|
||||
[Display(Name = "Allow payee to create invoices in their own denomination")]
|
||||
|
||||
[Display(Name = "Allow payee to create invoices with custom amounts")]
|
||||
public bool AllowCustomPaymentAmounts { get; set; }
|
||||
|
||||
public Dictionary<string, object> FormResponse { get; set; }
|
||||
@ -151,6 +156,7 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
|
||||
public string CssFileId { get; set; }
|
||||
public string BrandColor { get; set; }
|
||||
public string StoreName { get; set; }
|
||||
public string StoreWebsite { get; set; }
|
||||
public string EmbeddedCSS { get; set; }
|
||||
public string CustomCSSLink { get; set; }
|
||||
|
||||
@ -208,6 +214,7 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
|
||||
{
|
||||
public string PaymentMethod { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
public string AmountFormatted { get; set; }
|
||||
public string RateFormatted { get; set; }
|
||||
public decimal Paid { get; set; }
|
||||
public string PaidFormatted { get; set; }
|
||||
|
@ -17,11 +17,11 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
public bool Error { get; set; }
|
||||
}
|
||||
|
||||
public void SetExchangeRates(IEnumerable<AvailableRateProvider> supportedList, string preferredExchange)
|
||||
public void SetExchangeRates(IEnumerable<RateSourceInfo> supportedList, string preferredExchange)
|
||||
{
|
||||
supportedList = supportedList.Select(a => new AvailableRateProvider(a.Id, a.DisplayName, a.Url, a.Source)).ToArray();
|
||||
supportedList = supportedList.ToArray();
|
||||
var chosen = supportedList.FirstOrDefault(f => f.Id == preferredExchange) ?? supportedList.FirstOrDefault();
|
||||
Exchanges = new SelectList(supportedList, nameof(chosen.Id), nameof(chosen.Name), chosen);
|
||||
Exchanges = new SelectList(supportedList, nameof(chosen.Id), nameof(chosen.DisplayName), chosen);
|
||||
PreferredExchange = chosen?.Id;
|
||||
RateSource = chosen?.Url;
|
||||
}
|
||||
@ -39,7 +39,7 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
public string ScriptTest { get; set; }
|
||||
public string DefaultCurrencyPairs { get; set; }
|
||||
public string StoreId { get; set; }
|
||||
public IEnumerable<AvailableRateProvider> AvailableExchanges { get; set; }
|
||||
public IEnumerable<RateSourceInfo> AvailableExchanges { get; set; }
|
||||
|
||||
[Display(Name = "Add Exchange Rate Spread")]
|
||||
[Range(0.0, 100.0)]
|
||||
|
@ -22,6 +22,7 @@ namespace BTCPayServer.Models
|
||||
StoreId = data.StoreId;
|
||||
var blob = data.GetBlob();
|
||||
PaymentMethods = blob.SupportedPaymentMethods;
|
||||
BitcoinOnly = blob.SupportedPaymentMethods.All(p => p.CryptoCode == "BTC");
|
||||
SelectedPaymentMethod = PaymentMethods.First().ToString();
|
||||
Archived = data.Archived;
|
||||
AutoApprove = blob.AutoApproveClaims;
|
||||
@ -66,6 +67,8 @@ namespace BTCPayServer.Models
|
||||
}
|
||||
}
|
||||
|
||||
public bool BitcoinOnly { get; set; }
|
||||
|
||||
public string StoreId { get; set; }
|
||||
|
||||
public string SelectedPaymentMethod { get; set; }
|
||||
|
@ -98,9 +98,13 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
|
||||
[MaxLength(30)]
|
||||
public string Name { get; set; }
|
||||
|
||||
[Display(Name = "Memo")]
|
||||
public string Description { get; set; }
|
||||
|
||||
[Display(Name = "Custom CSS URL")]
|
||||
public string CustomCSSLink { get; set; }
|
||||
|
||||
[Display(Name = "Custom CSS Code")]
|
||||
public string EmbeddedCSS { get; set; }
|
||||
}
|
||||
|
@ -135,6 +135,7 @@ namespace BTCPayServer.PaymentRequest
|
||||
Amount = paymentEntity.PaidAmount.Gross,
|
||||
Paid = paymentEntity.InvoicePaidAmount.Net,
|
||||
ReceivedDate = paymentEntity.ReceivedTime.DateTime,
|
||||
AmountFormatted = _displayFormatter.Currency(paymentEntity.PaidAmount.Gross, paymentEntity.PaidAmount.Currency),
|
||||
PaidFormatted = _displayFormatter.Currency(paymentEntity.InvoicePaidAmount.Net, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
||||
RateFormatted = _displayFormatter.Currency(paymentEntity.Rate, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
||||
PaymentMethod = paymentMethodId.ToPrettyString(),
|
||||
|
@ -15,7 +15,7 @@ using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Ganss.XSS;
|
||||
using Ganss.Xss;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
@ -95,6 +95,8 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
ViewType = (PosViewType)viewType,
|
||||
ShowCustomAmount = settings.ShowCustomAmount,
|
||||
ShowDiscount = settings.ShowDiscount,
|
||||
ShowSearch = settings.ShowSearch,
|
||||
ShowCategories = settings.ShowCategories,
|
||||
EnableTips = settings.EnableTips,
|
||||
CurrencyCode = settings.Currency,
|
||||
CurrencySymbol = numberFormatInfo.CurrencySymbol,
|
||||
@ -277,7 +279,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
|
||||
formResponseJObject = TryParseJObject(formResponse) ?? new JObject();
|
||||
var form = Form.Parse(formData.Config);
|
||||
form.SetValues(formResponseJObject);
|
||||
FormDataService.SetValues(form, formResponseJObject);
|
||||
if (!FormDataService.Validate(form, ModelState))
|
||||
{
|
||||
//someone tried to bypass validation
|
||||
@ -382,7 +384,8 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
}
|
||||
if (appPosData.Tip > 0)
|
||||
{
|
||||
receiptData.Add("Tip", _displayFormatter.Currency(appPosData.Tip, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol));
|
||||
var tipFormatted = _displayFormatter.Currency(appPosData.Tip, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
receiptData.Add("Tip", appPosData.TipPercentage > 0 ? $"{appPosData.TipPercentage}% = {tipFormatted}" : tipFormatted);
|
||||
}
|
||||
receiptData.Add("Total", _displayFormatter.Currency(appPosData.Total, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol));
|
||||
}
|
||||
@ -542,6 +545,8 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
DefaultView = settings.DefaultView,
|
||||
ShowCustomAmount = settings.ShowCustomAmount,
|
||||
ShowDiscount = settings.ShowDiscount,
|
||||
ShowSearch = settings.ShowSearch,
|
||||
ShowCategories = settings.ShowCategories,
|
||||
EnableTips = settings.EnableTips,
|
||||
Currency = settings.Currency,
|
||||
Template = settings.Template,
|
||||
@ -630,6 +635,8 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
DefaultView = vm.DefaultView,
|
||||
ShowCustomAmount = vm.ShowCustomAmount,
|
||||
ShowDiscount = vm.ShowDiscount,
|
||||
ShowSearch = vm.ShowSearch,
|
||||
ShowCategories = vm.ShowCategories,
|
||||
EnableTips = vm.EnableTips,
|
||||
Currency = vm.Currency,
|
||||
Template = vm.Template,
|
||||
|
@ -31,6 +31,10 @@ namespace BTCPayServer.Plugins.PointOfSale.Models
|
||||
public bool ShowCustomAmount { get; set; }
|
||||
[Display(Name = "User can input discount in %")]
|
||||
public bool ShowDiscount { get; set; }
|
||||
[Display(Name = "Display the search bar")]
|
||||
public bool ShowSearch { get; set; }
|
||||
[Display(Name = "Display the category list")]
|
||||
public bool ShowCategories { get; set; }
|
||||
[Display(Name = "Enable tips")]
|
||||
public bool EnableTips { get; set; }
|
||||
public string Example1 { get; internal set; }
|
||||
|
@ -65,6 +65,8 @@ namespace BTCPayServer.Plugins.PointOfSale.Models
|
||||
public PosViewType ViewType { get; set; }
|
||||
public bool ShowCustomAmount { get; set; }
|
||||
public bool ShowDiscount { get; set; }
|
||||
public bool ShowSearch { get; set; } = true;
|
||||
public bool ShowCategories { get; set; } = true;
|
||||
public bool EnableTips { get; set; }
|
||||
public string Step { get; set; }
|
||||
public string Title { get; set; }
|
||||
|
@ -13,7 +13,7 @@ using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Ganss.XSS;
|
||||
using Ganss.Xss;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
@ -55,21 +55,26 @@ namespace BTCPayServer.Services.Altcoins.Monero.UI
|
||||
[HttpGet()]
|
||||
public async Task<IActionResult> GetStoreMoneroLikePaymentMethods()
|
||||
{
|
||||
var monero = StoreData.GetSupportedPaymentMethods(_BtcPayNetworkProvider)
|
||||
return View(await GetVM(StoreData));
|
||||
}
|
||||
[NonAction]
|
||||
public async Task<MoneroLikePaymentMethodListViewModel> GetVM(StoreData storeData)
|
||||
{
|
||||
var monero = storeData.GetSupportedPaymentMethods(_BtcPayNetworkProvider)
|
||||
.OfType<MoneroSupportedPaymentMethod>();
|
||||
|
||||
var excludeFilters = StoreData.GetStoreBlob().GetExcludedPaymentMethods();
|
||||
var excludeFilters = storeData.GetStoreBlob().GetExcludedPaymentMethods();
|
||||
|
||||
var accountsList = _MoneroLikeConfiguration.MoneroLikeConfigurationItems.ToDictionary(pair => pair.Key,
|
||||
pair => GetAccounts(pair.Key));
|
||||
|
||||
await Task.WhenAll(accountsList.Values);
|
||||
return View(new MoneroLikePaymentMethodListViewModel()
|
||||
return new MoneroLikePaymentMethodListViewModel()
|
||||
{
|
||||
Items = _MoneroLikeConfiguration.MoneroLikeConfigurationItems.Select(pair =>
|
||||
GetMoneroLikePaymentMethodViewModel(monero, pair.Key, excludeFilters,
|
||||
accountsList[pair.Key].Result))
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private Task<GetAccountsResponse> GetAccounts(string cryptoCode)
|
||||
|
@ -15,7 +15,7 @@ using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Ganss.XSS;
|
||||
using Ganss.Xss;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
@ -253,6 +253,7 @@ namespace BTCPayServer.Services.Apps
|
||||
public async Task<ListAppsViewModel.ListAppViewModel[]> GetAllApps(string? userId, bool allowNoUser = false, string? storeId = null, bool includeArchived = false)
|
||||
{
|
||||
await using var ctx = _ContextFactory.CreateContext();
|
||||
var types = GetAvailableAppTypes().Select(at => at.Key).ToHashSet();
|
||||
var listApps = (await ctx.UserStore
|
||||
.Where(us =>
|
||||
(allowNoUser && string.IsNullOrEmpty(userId) || us.ApplicationUserId == userId) &&
|
||||
@ -260,7 +261,7 @@ namespace BTCPayServer.Services.Apps
|
||||
.Include(store => store.StoreRole)
|
||||
.Include(store => store.StoreData)
|
||||
.Join(ctx.Apps, us => us.StoreDataId, app => app.StoreDataId, (us, app) => new { us, app })
|
||||
.Where(b => !b.app.Archived || b.app.Archived == includeArchived)
|
||||
.Where(b => types.Contains(b.app.AppType) && (!b.app.Archived || b.app.Archived == includeArchived))
|
||||
.OrderBy(b => b.app.Created)
|
||||
.ToArrayAsync()).Select(arg => new ListAppsViewModel.ListAppViewModel
|
||||
{
|
||||
@ -311,9 +312,10 @@ namespace BTCPayServer.Services.Apps
|
||||
public async Task<List<AppData>> GetApps(string[] appIds, bool includeStore = false, bool includeArchived = false)
|
||||
{
|
||||
await using var ctx = _ContextFactory.CreateContext();
|
||||
var types = GetAvailableAppTypes().Select(at => at.Key);
|
||||
var query = ctx.Apps
|
||||
.Where(app => appIds.Contains(app.Id))
|
||||
.Where(app => !app.Archived || app.Archived == includeArchived);
|
||||
.Where(app => types.Contains(app.AppType) && (!app.Archived || app.Archived == includeArchived));
|
||||
if (includeStore)
|
||||
{
|
||||
query = query.Include(data => data.StoreData);
|
||||
@ -332,9 +334,10 @@ namespace BTCPayServer.Services.Apps
|
||||
public async Task<AppData?> GetApp(string appId, string? appType, bool includeStore = false, bool includeArchived = false)
|
||||
{
|
||||
await using var ctx = _ContextFactory.CreateContext();
|
||||
var types = GetAvailableAppTypes().Select(at => at.Key);
|
||||
var query = ctx.Apps
|
||||
.Where(us => us.Id == appId && (appType == null || us.AppType == appType))
|
||||
.Where(app => !app.Archived || app.Archived == includeArchived);
|
||||
.Where(app => types.Contains(app.AppType) && (!app.Archived || app.Archived == includeArchived));
|
||||
if (includeStore)
|
||||
{
|
||||
query = query.Include(data => data.StoreData);
|
||||
|
@ -77,6 +77,8 @@ namespace BTCPayServer.Services.Apps
|
||||
DefaultView = PosViewType.Static;
|
||||
ShowCustomAmount = false;
|
||||
ShowDiscount = true;
|
||||
ShowSearch = true;
|
||||
ShowCategories = true;
|
||||
EnableTips = true;
|
||||
RequiresRefundEmail = RequiresRefundEmail.InheritFromStore;
|
||||
}
|
||||
@ -87,6 +89,8 @@ namespace BTCPayServer.Services.Apps
|
||||
public PosViewType DefaultView { get; set; }
|
||||
public bool ShowCustomAmount { get; set; }
|
||||
public bool ShowDiscount { get; set; }
|
||||
public bool ShowSearch { get; set; } = true;
|
||||
public bool ShowCategories { get; set; } = true;
|
||||
public bool EnableTips { get; set; }
|
||||
public RequiresRefundEmail RequiresRefundEmail { get; set; }
|
||||
|
||||
|
@ -1,14 +1,17 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NBitcoin;
|
||||
using NBitcoin.RPC;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
public class Cheater : IHostedService
|
||||
{
|
||||
private readonly ExplorerClientProvider _prov;
|
||||
private readonly InvoiceRepository _invoiceRepository;
|
||||
public RPCClient CashCow { get; set; }
|
||||
|
||||
@ -17,18 +20,47 @@ namespace BTCPayServer.Services
|
||||
InvoiceRepository invoiceRepository)
|
||||
{
|
||||
CashCow = prov.GetExplorerClient("BTC")?.RPCClient;
|
||||
_prov = prov;
|
||||
_invoiceRepository = invoiceRepository;
|
||||
}
|
||||
|
||||
public RPCClient GetCashCow(string cryptoCode)
|
||||
{
|
||||
return _prov.GetExplorerClient(cryptoCode)?.RPCClient;
|
||||
}
|
||||
|
||||
public async Task UpdateInvoiceExpiry(string invoiceId, TimeSpan seconds)
|
||||
{
|
||||
await _invoiceRepository.UpdateInvoiceExpiry(invoiceId, seconds);
|
||||
}
|
||||
|
||||
Task IHostedService.StartAsync(CancellationToken cancellationToken)
|
||||
async Task IHostedService.StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_ = CashCow?.ScanRPCCapabilitiesAsync(cancellationToken);
|
||||
return Task.CompletedTask;
|
||||
#if ALTCOINS
|
||||
var liquid = _prov.GetNetwork("LBTC");
|
||||
if (liquid is not null)
|
||||
{
|
||||
var lbtcrpc = GetCashCow(liquid.CryptoCode);
|
||||
await lbtcrpc.SendCommandAsync("rescanblockchain");
|
||||
var elements = _prov.NetworkProviders.GetAll().OfType<ElementsBTCPayNetwork>();
|
||||
foreach (ElementsBTCPayNetwork element in elements)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (element.AssetId is null)
|
||||
{
|
||||
var issueAssetResult = await lbtcrpc.SendCommandAsync("issueasset", 100000, 0);
|
||||
element.AssetId = uint256.Parse(issueAssetResult.Result["asset"].ToString());
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
Task IHostedService.StopAsync(CancellationToken cancellationToken)
|
||||
|
@ -2,6 +2,8 @@ using System;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Reporting;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Services;
|
||||
|
||||
@ -18,7 +20,8 @@ public class DisplayFormatter
|
||||
{
|
||||
Code,
|
||||
Symbol,
|
||||
CodeAndSymbol
|
||||
CodeAndSymbol,
|
||||
None
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -43,6 +46,7 @@ public class DisplayFormatter
|
||||
|
||||
return format switch
|
||||
{
|
||||
CurrencyFormat.None => formatted.Replace(provider.CurrencySymbol, "").Trim(),
|
||||
CurrencyFormat.Code => $"{formatted.Replace(provider.CurrencySymbol, "").Trim()} {currency}",
|
||||
CurrencyFormat.Symbol => formatted,
|
||||
CurrencyFormat.CodeAndSymbol => $"{formatted} ({currency})",
|
||||
@ -54,4 +58,11 @@ public class DisplayFormatter
|
||||
{
|
||||
return Currency(decimal.Parse(value, CultureInfo.InvariantCulture), currency, format);
|
||||
}
|
||||
|
||||
public JObject ToFormattedAmount(decimal value, string currency)
|
||||
{
|
||||
var currencyData = _currencyNameTable.GetCurrencyData(currency, true);
|
||||
var divisibility = currencyData.Divisibility;
|
||||
return new FormattedAmount(value, divisibility).ToJObject();
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client.Models;
|
||||
@ -386,7 +387,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
}
|
||||
public void UpdateTotals()
|
||||
{
|
||||
Rates = new Dictionary<string, decimal>();
|
||||
Rates = new Dictionary<string, decimal>(StringComparer.InvariantCultureIgnoreCase);
|
||||
foreach (var p in GetPaymentMethods())
|
||||
{
|
||||
Rates.TryAdd(p.Currency, p.Rate);
|
||||
@ -591,14 +592,14 @@ namespace BTCPayServer.Services.Invoices
|
||||
cryptoInfo.CryptoCode = cryptoCode;
|
||||
cryptoInfo.PaymentType = info.GetId().PaymentType.ToString();
|
||||
cryptoInfo.Rate = info.Rate;
|
||||
cryptoInfo.Price = subtotalPrice.ToString();
|
||||
cryptoInfo.Price = subtotalPrice.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
cryptoInfo.Due = accounting.Due.ToString();
|
||||
cryptoInfo.Paid = accounting.Paid.ToString();
|
||||
cryptoInfo.TotalDue = accounting.TotalDue.ToString();
|
||||
cryptoInfo.NetworkFee = accounting.NetworkFee.ToString();
|
||||
cryptoInfo.Due = accounting.Due.ToString(CultureInfo.InvariantCulture);
|
||||
cryptoInfo.Paid = accounting.Paid.ToString(CultureInfo.InvariantCulture);
|
||||
cryptoInfo.TotalDue = accounting.TotalDue.ToString(CultureInfo.InvariantCulture);
|
||||
cryptoInfo.NetworkFee = accounting.NetworkFee.ToString(CultureInfo.InvariantCulture);
|
||||
cryptoInfo.TxCount = accounting.TxCount;
|
||||
cryptoInfo.CryptoPaid = accounting.CryptoPaid.ToString();
|
||||
cryptoInfo.CryptoPaid = accounting.CryptoPaid.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
cryptoInfo.Address = address;
|
||||
|
||||
@ -978,7 +979,15 @@ namespace BTCPayServer.Services.Invoices
|
||||
}
|
||||
public override string ToString()
|
||||
{
|
||||
return Status.ToModernStatus() + (ExceptionStatus == InvoiceExceptionStatus.None ? string.Empty : $" ({ToString(ExceptionStatus)})");
|
||||
return Status.ToModernStatus() + ExceptionStatus switch
|
||||
{
|
||||
InvoiceExceptionStatus.PaidOver => " (paid over)",
|
||||
InvoiceExceptionStatus.PaidLate => " (paid late)",
|
||||
InvoiceExceptionStatus.PaidPartial => " (paid partial)",
|
||||
InvoiceExceptionStatus.Marked => " (marked)",
|
||||
InvoiceExceptionStatus.Invalid => " (invalid)",
|
||||
_ => ""
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -584,7 +584,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
entity.RefundMail = invoice.CustomerEmail;
|
||||
if (invoice.AddressInvoices != null)
|
||||
{
|
||||
entity.AvailableAddressHashes = invoice.AddressInvoices.Select(a => a.GetAddress() + a.GetPaymentMethodId().ToString()).ToHashSet();
|
||||
entity.AvailableAddressHashes = invoice.AddressInvoices.Select(a => a.GetAddress() + a.GetPaymentMethodId()).ToHashSet();
|
||||
}
|
||||
if (invoice.Events != null)
|
||||
{
|
||||
|
@ -20,6 +20,9 @@ public class PosAppData
|
||||
[JsonProperty(PropertyName = "discountAmount")]
|
||||
public decimal DiscountAmount { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "tipPercentage")]
|
||||
public decimal TipPercentage { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "tip")]
|
||||
public decimal Tip { get; set; }
|
||||
|
||||
|
25
BTCPayServer/Services/Reporting/FormattedAmount.cs
Normal file
25
BTCPayServer/Services/Reporting/FormattedAmount.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using BTCPayServer.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Services.Reporting
|
||||
{
|
||||
public class FormattedAmount
|
||||
{
|
||||
public FormattedAmount(decimal value, int divisibility)
|
||||
{
|
||||
Value = value;
|
||||
Divisibility = divisibility;
|
||||
}
|
||||
[JsonProperty("v")]
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal Value { get; set; }
|
||||
[JsonProperty("d")]
|
||||
public int Divisibility { get; set; }
|
||||
|
||||
public JObject ToJObject()
|
||||
{
|
||||
return JObject.FromObject(this);
|
||||
}
|
||||
}
|
||||
}
|
@ -3,14 +3,9 @@ using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Dapper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using static Microsoft.EntityFrameworkCore.DbLoggerCategory.Database;
|
||||
|
||||
namespace BTCPayServer.Services.Reporting;
|
||||
|
||||
@ -27,42 +22,42 @@ public class OnChainWalletReportProvider : ReportProvider
|
||||
NetworkProvider = networkProvider;
|
||||
WalletRepository = walletRepository;
|
||||
}
|
||||
public NBXplorerConnectionFactory NbxplorerConnectionFactory { get; }
|
||||
public StoreRepository StoreRepository { get; }
|
||||
public BTCPayNetworkProvider NetworkProvider { get; }
|
||||
public WalletRepository WalletRepository { get; }
|
||||
public override string Name => "On-Chain Wallets";
|
||||
|
||||
private NBXplorerConnectionFactory NbxplorerConnectionFactory { get; }
|
||||
private StoreRepository StoreRepository { get; }
|
||||
private BTCPayNetworkProvider NetworkProvider { get; }
|
||||
private WalletRepository WalletRepository { get; }
|
||||
public override string Name => "Wallets";
|
||||
ViewDefinition CreateViewDefinition()
|
||||
{
|
||||
return
|
||||
new()
|
||||
return new()
|
||||
{
|
||||
Fields =
|
||||
{
|
||||
Fields =
|
||||
new ("Date", "datetime"),
|
||||
new ("Crypto", "string"),
|
||||
// For proper rendering of explorer links, Crypto should always be before tx_id
|
||||
new ("TransactionId", "tx_id"),
|
||||
new ("InvoiceId", "invoice_id"),
|
||||
new ("Confirmed", "boolean"),
|
||||
new ("BalanceChange", "amount")
|
||||
},
|
||||
Charts =
|
||||
{
|
||||
new ()
|
||||
{
|
||||
new ("Date", "datetime"),
|
||||
new ("Crypto", "string"),
|
||||
// For proper rendering of explorer links, Crypto should always be before tx_id
|
||||
new ("TransactionId", "tx_id"),
|
||||
new ("InvoiceId", "invoice_id"),
|
||||
new ("Confirmed", "boolean"),
|
||||
new ("BalanceChange", "decimal")
|
||||
},
|
||||
Charts =
|
||||
{
|
||||
new ()
|
||||
{
|
||||
Name = "Group by Crypto",
|
||||
Totals = { "Crypto" },
|
||||
Groups = { "Crypto", "Confirmed" },
|
||||
Aggregates = { "BalanceChange" }
|
||||
}
|
||||
Name = "Group by Crypto",
|
||||
Totals = { "Crypto" },
|
||||
Groups = { "Crypto", "Confirmed" },
|
||||
Aggregates = { "BalanceChange" }
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public override bool IsAvailable()
|
||||
{
|
||||
return this.NbxplorerConnectionFactory.Available;
|
||||
return NbxplorerConnectionFactory.Available;
|
||||
}
|
||||
|
||||
public override async Task Query(QueryContext queryContext, CancellationToken cancellation)
|
||||
@ -79,14 +74,15 @@ public class OnChainWalletReportProvider : ReportProvider
|
||||
var command = new CommandDefinition(
|
||||
commandText:
|
||||
"SELECT r.tx_id, r.seen_at, t.blk_id, t.blk_height, r.balance_change " +
|
||||
"FROM get_wallets_recent(@wallet_id, @code, @interval, NULL, NULL) r " +
|
||||
"FROM get_wallets_recent(@wallet_id, @code, @asset_id, @interval, NULL, NULL) r " +
|
||||
"JOIN txs t USING (code, tx_id) " +
|
||||
"ORDER BY r.seen_at",
|
||||
parameters: new
|
||||
{
|
||||
asset_id = GetAssetId(settings.Network),
|
||||
wallet_id = NBXplorer.Client.DBUtils.nbxv1_get_wallet_id(settings.Network.CryptoCode, settings.AccountDerivation.ToString()),
|
||||
code = settings.Network.CryptoCode,
|
||||
interval = interval
|
||||
interval
|
||||
},
|
||||
cancellationToken: cancellation);
|
||||
|
||||
@ -97,14 +93,15 @@ public class OnChainWalletReportProvider : ReportProvider
|
||||
if (date > queryContext.To)
|
||||
continue;
|
||||
var values = queryContext.AddData();
|
||||
values.Add((DateTimeOffset)date);
|
||||
var balanceChange = Money.Satoshis((long)r.balance_change).ToDecimal(MoneyUnit.BTC);
|
||||
values.Add(date);
|
||||
values.Add(settings.Network.CryptoCode);
|
||||
values.Add((string)r.tx_id);
|
||||
values.Add(null);
|
||||
values.Add((long?)r.blk_height is not null);
|
||||
values.Add(Money.Satoshis((long)r.balance_change).ToDecimal(MoneyUnit.BTC));
|
||||
values.Add(new FormattedAmount(balanceChange, settings.Network.Divisibility).ToJObject());
|
||||
}
|
||||
var objects = await WalletRepository.GetWalletObjects(new GetWalletObjectsQuery()
|
||||
var objects = await WalletRepository.GetWalletObjects(new GetWalletObjectsQuery
|
||||
{
|
||||
Ids = queryContext.Data.Select(d => (string)d[2]!).ToArray(),
|
||||
WalletId = walletId,
|
||||
@ -119,4 +116,17 @@ public class OnChainWalletReportProvider : ReportProvider
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string? GetAssetId(BTCPayNetwork network)
|
||||
{
|
||||
#if ALTCOINS
|
||||
if (network is ElementsBTCPayNetwork elNetwork)
|
||||
{
|
||||
if (elNetwork.CryptoCode == elNetwork.NetworkCryptoCode)
|
||||
return "";
|
||||
return elNetwork.AssetId.ToString();
|
||||
}
|
||||
#endif
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user