Compare commits
30 Commits
v1.11.5
...
v1.11.7-rc
Author | SHA1 | Date | |
---|---|---|---|
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 |
@ -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,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
|
||||
}
|
||||
|
||||
|
||||
|
@ -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(() =>
|
||||
{
|
||||
|
@ -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>());
|
||||
}
|
||||
|
@ -556,24 +556,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));
|
||||
}
|
||||
|
||||
@ -1107,7 +1107,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 +1122,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 +1133,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]);
|
||||
|
||||
@ -1892,8 +1913,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);
|
||||
@ -1960,17 +1979,15 @@ 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();
|
||||
|
||||
// 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,11 +1998,6 @@ 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();
|
||||
|
||||
|
@ -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);
|
||||
@ -2886,7 +2886,7 @@ namespace BTCPayServer.Tests
|
||||
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" });
|
||||
|
@ -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"
|
||||
@ -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"
|
||||
|
@ -54,7 +54,7 @@
|
||||
<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" />
|
||||
|
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; }
|
||||
}
|
||||
}
|
@ -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,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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"))
|
||||
{
|
||||
@ -565,37 +566,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;
|
||||
}
|
||||
@ -1147,7 +1153,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);
|
||||
|
@ -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,17 @@ 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,
|
||||
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;
|
||||
|
||||
@ -176,31 +175,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 +236,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 +246,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);
|
||||
}
|
||||
|
||||
|
@ -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,8 +25,7 @@ public partial class UIReportsController : Controller
|
||||
ApplicationDbContextFactory dbContextFactory,
|
||||
GreenfieldReportsController api,
|
||||
ReportService reportService,
|
||||
BTCPayServerEnvironment env
|
||||
)
|
||||
BTCPayServerEnvironment env)
|
||||
{
|
||||
Api = api;
|
||||
ReportService = reportService;
|
||||
@ -72,20 +61,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>();
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"}
|
||||
};
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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")));
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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; }
|
||||
|
@ -277,7 +277,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 +382,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));
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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 e)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#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();
|
||||
}
|
||||
}
|
||||
|
@ -386,7 +386,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);
|
||||
@ -978,7 +978,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);
|
||||
}
|
||||
}
|
||||
}
|
@ -19,6 +19,7 @@ public class OnChainWalletReportProvider : ReportProvider
|
||||
public OnChainWalletReportProvider(
|
||||
NBXplorerConnectionFactory NbxplorerConnectionFactory,
|
||||
StoreRepository storeRepository,
|
||||
DisplayFormatter displayFormatter,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
WalletRepository walletRepository)
|
||||
{
|
||||
@ -26,43 +27,45 @@ public class OnChainWalletReportProvider : ReportProvider
|
||||
StoreRepository = storeRepository;
|
||||
NetworkProvider = networkProvider;
|
||||
WalletRepository = walletRepository;
|
||||
_displayFormatter = displayFormatter;
|
||||
}
|
||||
public NBXplorerConnectionFactory NbxplorerConnectionFactory { get; }
|
||||
public StoreRepository StoreRepository { get; }
|
||||
public BTCPayNetworkProvider NetworkProvider { get; }
|
||||
public WalletRepository WalletRepository { get; }
|
||||
|
||||
private readonly DisplayFormatter _displayFormatter;
|
||||
private NBXplorerConnectionFactory NbxplorerConnectionFactory { get; }
|
||||
private StoreRepository StoreRepository { get; }
|
||||
private BTCPayNetworkProvider NetworkProvider { get; }
|
||||
private WalletRepository WalletRepository { get; }
|
||||
public override string Name => "On-Chain 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 +82,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 +101,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 +124,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;
|
||||
}
|
||||
}
|
||||
|
@ -1,107 +1,103 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.Common;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Lightning.LND;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Dapper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Internal;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using static BTCPayServer.HostedServices.PullPaymentHostedService.PayoutApproval;
|
||||
|
||||
namespace BTCPayServer.Services.Reporting;
|
||||
|
||||
public class PaymentsReportProvider : ReportProvider
|
||||
{
|
||||
|
||||
public PaymentsReportProvider(ApplicationDbContextFactory dbContextFactory, CurrencyNameTable currencyNameTable)
|
||||
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||
public PaymentsReportProvider(
|
||||
ApplicationDbContextFactory dbContextFactory,
|
||||
DisplayFormatter displayFormatter,
|
||||
BTCPayNetworkProvider btcPayNetworkProvider)
|
||||
{
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
DbContextFactory = dbContextFactory;
|
||||
CurrencyNameTable = currencyNameTable;
|
||||
DisplayFormatter = displayFormatter;
|
||||
}
|
||||
public override string Name => "Payments";
|
||||
public ApplicationDbContextFactory DbContextFactory { get; }
|
||||
public CurrencyNameTable CurrencyNameTable { get; }
|
||||
private ApplicationDbContextFactory DbContextFactory { get; }
|
||||
private DisplayFormatter DisplayFormatter { get; }
|
||||
|
||||
ViewDefinition CreateViewDefinition()
|
||||
{
|
||||
return
|
||||
new()
|
||||
return new()
|
||||
{
|
||||
Fields =
|
||||
{
|
||||
Fields =
|
||||
new ("Date", "datetime"),
|
||||
new ("InvoiceId", "invoice_id"),
|
||||
new ("OrderId", "string"),
|
||||
new ("PaymentType", "string"),
|
||||
new ("PaymentId", "string"),
|
||||
new ("Confirmed", "boolean"),
|
||||
new ("Address", "string"),
|
||||
new ("Crypto", "string"),
|
||||
new ("CryptoAmount", "amount"),
|
||||
new ("NetworkFee", "amount"),
|
||||
new ("LightningAddress", "string"),
|
||||
new ("Currency", "string"),
|
||||
new ("CurrencyAmount", "amount"),
|
||||
new ("Rate", "amount")
|
||||
},
|
||||
Charts =
|
||||
{
|
||||
new ()
|
||||
{
|
||||
new ("Date", "datetime"),
|
||||
new ("InvoiceId", "invoice_id"),
|
||||
new ("OrderId", "string"),
|
||||
new ("PaymentType", "string"),
|
||||
new ("PaymentId", "string"),
|
||||
new ("Confirmed", "boolean"),
|
||||
new ("Address", "string"),
|
||||
new ("Crypto", "string"),
|
||||
new ("CryptoAmount", "decimal"),
|
||||
new ("NetworkFee", "decimal"),
|
||||
new ("LightningAddress", "string"),
|
||||
new ("Currency", "string"),
|
||||
new ("CurrencyAmount", "decimal"),
|
||||
new ("Rate", "decimal")
|
||||
Name = "Aggregated crypto amount",
|
||||
Groups = { "Crypto", "PaymentType" },
|
||||
Totals = { "Crypto" },
|
||||
HasGrandTotal = false,
|
||||
Aggregates = { "CryptoAmount" }
|
||||
},
|
||||
Charts =
|
||||
new ()
|
||||
{
|
||||
new ()
|
||||
{
|
||||
Name = "Aggregated crypto amount",
|
||||
Groups = { "Crypto", "PaymentType" },
|
||||
Totals = { "Crypto" },
|
||||
HasGrandTotal = false,
|
||||
Aggregates = { "CryptoAmount" }
|
||||
},
|
||||
new ()
|
||||
{
|
||||
Name = "Aggregated currency amount",
|
||||
Groups = { "Currency" },
|
||||
Totals = { "Currency" },
|
||||
HasGrandTotal = false,
|
||||
Aggregates = { "CurrencyAmount" }
|
||||
},
|
||||
new ()
|
||||
{
|
||||
Name = "Group by Lightning Address (Currency amount)",
|
||||
Filters = { "typeof this.LightningAddress === 'string' && this.Crypto == \"BTC\"" },
|
||||
Groups = { "LightningAddress", "Currency" },
|
||||
Aggregates = { "CurrencyAmount" },
|
||||
HasGrandTotal = true
|
||||
},
|
||||
new ()
|
||||
{
|
||||
Name = "Group by Lightning Address (Crypto amount)",
|
||||
Filters = { "typeof this.LightningAddress === 'string' && this.Crypto == \"BTC\"" },
|
||||
Groups = { "LightningAddress" },
|
||||
Aggregates = { "CryptoAmount" },
|
||||
HasGrandTotal = true
|
||||
}
|
||||
Name = "Aggregated currency amount",
|
||||
Groups = { "Currency" },
|
||||
Totals = { "Currency" },
|
||||
HasGrandTotal = false,
|
||||
Aggregates = { "CurrencyAmount" }
|
||||
},
|
||||
new ()
|
||||
{
|
||||
Name = "Group by Lightning Address (Currency amount)",
|
||||
Filters = { "typeof this.LightningAddress === 'string' && this.Crypto == \"BTC\"" },
|
||||
Groups = { "LightningAddress", "Currency" },
|
||||
Aggregates = { "CurrencyAmount" },
|
||||
HasGrandTotal = true
|
||||
},
|
||||
new ()
|
||||
{
|
||||
Name = "Group by Lightning Address (Crypto amount)",
|
||||
Filters = { "typeof this.LightningAddress === 'string' && this.Crypto == \"BTC\"" },
|
||||
Groups = { "LightningAddress", "Crypto" },
|
||||
Aggregates = { "CryptoAmount" },
|
||||
HasGrandTotal = true
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public override async Task Query(QueryContext queryContext, CancellationToken cancellation)
|
||||
{
|
||||
queryContext.ViewDefinition = CreateViewDefinition();
|
||||
await using var ctx = DbContextFactory.CreateContext();
|
||||
var conn = ctx.Database.GetDbConnection();
|
||||
string[] fields = new[]
|
||||
string[] fields =
|
||||
{
|
||||
$"i.\"Created\" created",
|
||||
"i.\"Created\" created",
|
||||
"i.\"Id\" invoice_id",
|
||||
"i.\"OrderId\" order_id",
|
||||
"p.\"Id\" payment_id",
|
||||
@ -113,7 +109,7 @@ public class PaymentsReportProvider : ReportProvider
|
||||
string body =
|
||||
"FROM \"Payments\" p " +
|
||||
"JOIN \"Invoices\" i ON i.\"Id\" = p.\"InvoiceDataId\" " +
|
||||
$"WHERE p.\"Accounted\" IS TRUE AND i.\"Created\" >= @from AND i.\"Created\" < @to AND i.\"StoreDataId\"=@storeId " +
|
||||
"WHERE p.\"Accounted\" IS TRUE AND i.\"Created\" >= @from AND i.\"Created\" < @to AND i.\"StoreDataId\"=@storeId " +
|
||||
"ORDER BY i.\"Created\"";
|
||||
var command = new CommandDefinition(
|
||||
commandText: select + body,
|
||||
@ -131,10 +127,14 @@ public class PaymentsReportProvider : ReportProvider
|
||||
values.Add((DateTime)r.created);
|
||||
values.Add((string)r.invoice_id);
|
||||
values.Add((string)r.order_id);
|
||||
bool isLightning = false;
|
||||
if (PaymentMethodId.TryParse((string)r.payment_type, out var paymentType))
|
||||
{
|
||||
if (paymentType.PaymentType == PaymentTypes.LightningLike || paymentType.PaymentType == PaymentTypes.LNURLPay)
|
||||
{
|
||||
isLightning = true;
|
||||
values.Add("Lightning");
|
||||
}
|
||||
else if (paymentType.PaymentType == PaymentTypes.BTCLike)
|
||||
values.Add("On-Chain");
|
||||
else
|
||||
@ -143,42 +143,56 @@ public class PaymentsReportProvider : ReportProvider
|
||||
else
|
||||
continue;
|
||||
values.Add((string)r.payment_id);
|
||||
var invoiceBlob = JObject.Parse((string)r.invoice_blob);
|
||||
var paymentBlob = JObject.Parse((string)r.payment_blob);
|
||||
//var invoiceBlob = JObject.Parse((string)r.invoice_blob);
|
||||
//var paymentBlob = JObject.Parse((string)r.payment_blob);
|
||||
|
||||
var pd = new PaymentData()
|
||||
{
|
||||
Blob2 = r.payment_blob,
|
||||
Accounted = true,
|
||||
Type = paymentType.ToStringNormalized()
|
||||
};
|
||||
var paymentEntity = pd.GetBlob(_btcPayNetworkProvider);
|
||||
var paymentData = paymentEntity?.GetCryptoPaymentData();
|
||||
if (paymentData is null)
|
||||
continue;
|
||||
|
||||
var data = JObject.Parse(paymentBlob.SelectToken("$.cryptoPaymentData")?.Value<string>()!);
|
||||
var conf = data.SelectToken("$.confirmationCount")?.Value<int>();
|
||||
values.Add(conf is int o ? o > 0 :
|
||||
paymentType.PaymentType != PaymentTypes.BTCLike ? true : null);
|
||||
values.Add(data.SelectToken("$.address")?.Value<string>());
|
||||
Data.InvoiceData invoiceData = new()
|
||||
{
|
||||
Blob2 = r.invoice_blob
|
||||
};
|
||||
var invoiceBlob = invoiceData.GetBlob(_btcPayNetworkProvider);
|
||||
invoiceBlob.UpdateTotals();
|
||||
|
||||
values.Add(paymentData.PaymentConfirmed(paymentEntity, SpeedPolicy.MediumSpeed));
|
||||
values.Add(paymentData.GetDestination());
|
||||
values.Add(paymentType.CryptoCode);
|
||||
|
||||
decimal cryptoAmount;
|
||||
if (data.SelectToken("$.amount")?.Value<long>() is long v)
|
||||
var cryptoAmount = paymentData.GetValue();
|
||||
|
||||
var divisibility = 8;
|
||||
if (_btcPayNetworkProvider.TryGetNetwork<BTCPayNetwork>(paymentType.CryptoCode, out var network))
|
||||
{
|
||||
cryptoAmount = LightMoney.MilliSatoshis(v).ToDecimal(LightMoneyUnit.BTC);
|
||||
divisibility = network.Divisibility;
|
||||
}
|
||||
else if (data.SelectToken("$.value")?.Value<long>() is long amount)
|
||||
if (isLightning)
|
||||
divisibility += 3;
|
||||
values.Add(new FormattedAmount(cryptoAmount, divisibility).ToJObject());
|
||||
values.Add(paymentEntity.NetworkFee);
|
||||
var consumerdLightningAddress = (invoiceBlob.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.LNURLPay))?
|
||||
.GetPaymentMethodDetails() as LNURLPayPaymentMethodDetails)?
|
||||
.ConsumedLightningAddress;
|
||||
values.Add(consumerdLightningAddress);
|
||||
values.Add(invoiceBlob.Currency);
|
||||
if (invoiceBlob.Rates.TryGetValue(paymentType.CryptoCode, out var rate))
|
||||
{
|
||||
cryptoAmount = Money.Satoshis(amount).ToDecimal(MoneyUnit.BTC);
|
||||
values.Add(DisplayFormatter.ToFormattedAmount(rate * cryptoAmount, invoiceBlob.Currency ?? "USD")); // Currency amount
|
||||
values.Add(DisplayFormatter.ToFormattedAmount(rate, invoiceBlob.Currency ?? "USD"));
|
||||
}
|
||||
else
|
||||
{
|
||||
continue;
|
||||
}
|
||||
values.Add(cryptoAmount);
|
||||
values.Add(paymentBlob.SelectToken("$.networkFee", false)?.Value<decimal>());
|
||||
values.Add(invoiceBlob.SelectToken("$.cryptoData.BTC_LNURLPAY.paymentMethod.ConsumedLightningAddress", false)?.Value<string>());
|
||||
var currency = invoiceBlob.SelectToken("$.currency")?.Value<string>();
|
||||
values.Add(currency);
|
||||
|
||||
values.Add(null); // Currency amount
|
||||
var rate = invoiceBlob.SelectToken($"$.cryptoData.{paymentType}.rate")?.Value<decimal>();
|
||||
values.Add(rate);
|
||||
if (rate is not null)
|
||||
{
|
||||
values[^2] = (rate.Value * cryptoAmount).RoundToSignificant(CurrencyNameTable.GetCurrencyData(currency ?? "USD", true).Divisibility);
|
||||
values.Add(null);
|
||||
values.Add(null);
|
||||
}
|
||||
|
||||
queryContext.Data.Add(values);
|
||||
|
@ -1,4 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@ -9,13 +9,18 @@ using BTCPayServer.Payments;
|
||||
|
||||
namespace BTCPayServer.Services.Reporting;
|
||||
|
||||
public class PayoutsReportProvider:ReportProvider
|
||||
public class PayoutsReportProvider : ReportProvider
|
||||
{
|
||||
private readonly PullPaymentHostedService _pullPaymentHostedService;
|
||||
private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings;
|
||||
private readonly DisplayFormatter _displayFormatter;
|
||||
|
||||
public PayoutsReportProvider(PullPaymentHostedService pullPaymentHostedService, BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings)
|
||||
public PayoutsReportProvider(
|
||||
PullPaymentHostedService pullPaymentHostedService,
|
||||
DisplayFormatter displayFormatter,
|
||||
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings)
|
||||
{
|
||||
_displayFormatter = displayFormatter;
|
||||
_pullPaymentHostedService = pullPaymentHostedService;
|
||||
_btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings;
|
||||
}
|
||||
@ -51,32 +56,32 @@ public class PayoutsReportProvider:ReportProvider
|
||||
}
|
||||
else
|
||||
continue;
|
||||
data.Add(paymentType.CryptoCode);
|
||||
data.Add(blob.CryptoAmount);
|
||||
|
||||
var ppBlob = payout.PullPaymentData?.GetBlob();
|
||||
data.Add(ppBlob?.Currency??paymentType.CryptoCode);
|
||||
data.Add(blob.Amount);
|
||||
var currency = ppBlob?.Currency ?? paymentType.CryptoCode;
|
||||
data.Add(paymentType.CryptoCode);
|
||||
data.Add(blob.CryptoAmount.HasValue ? _displayFormatter.ToFormattedAmount(blob.CryptoAmount.Value, paymentType.CryptoCode) : null);
|
||||
data.Add(currency);
|
||||
data.Add(_displayFormatter.ToFormattedAmount(blob.Amount, currency));
|
||||
data.Add(blob.Destination);
|
||||
queryContext.Data.Add(data);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private ViewDefinition CreateDefinition()
|
||||
{
|
||||
return new ViewDefinition()
|
||||
return new ViewDefinition
|
||||
{
|
||||
Fields = new List<StoreReportResponse.Field>()
|
||||
Fields = new List<StoreReportResponse.Field>
|
||||
{
|
||||
new("Date", "datetime"),
|
||||
new("Source", "string"),
|
||||
new("State", "string"),
|
||||
new("PaymentType", "string"),
|
||||
new("Crypto", "string"),
|
||||
new("CryptoAmount", "decimal"),
|
||||
new("CryptoAmount", "amount"),
|
||||
new("Currency", "string"),
|
||||
new("CurrencyAmount", "decimal"),
|
||||
new("CurrencyAmount", "amount"),
|
||||
new("Destination", "string")
|
||||
},
|
||||
Charts =
|
||||
@ -88,14 +93,16 @@ public class PayoutsReportProvider:ReportProvider
|
||||
Totals = { "Crypto" },
|
||||
HasGrandTotal = false,
|
||||
Aggregates = { "CryptoAmount" }
|
||||
},new ()
|
||||
},
|
||||
new ()
|
||||
{
|
||||
Name = "Aggregated amount",
|
||||
Groups = { "Currency", "State" },
|
||||
Totals = { "CurrencyAmount" },
|
||||
HasGrandTotal = false,
|
||||
Aggregates = { "CurrencyAmount" }
|
||||
},new ()
|
||||
},
|
||||
new ()
|
||||
{
|
||||
Name = "Aggregated amount by Source",
|
||||
Groups = { "Currency", "State", "Source" },
|
||||
|
@ -1,24 +1,26 @@
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
|
||||
namespace BTCPayServer.Services.Reporting;
|
||||
|
||||
public class ProductsReportProvider : ReportProvider
|
||||
{
|
||||
public ProductsReportProvider(InvoiceRepository invoiceRepository, CurrencyNameTable currencyNameTable, AppService apps)
|
||||
public ProductsReportProvider(
|
||||
InvoiceRepository invoiceRepository,
|
||||
DisplayFormatter displayFormatter,
|
||||
AppService apps)
|
||||
{
|
||||
InvoiceRepository = invoiceRepository;
|
||||
CurrencyNameTable = currencyNameTable;
|
||||
_displayFormatter = displayFormatter;
|
||||
Apps = apps;
|
||||
}
|
||||
|
||||
public InvoiceRepository InvoiceRepository { get; }
|
||||
public CurrencyNameTable CurrencyNameTable { get; }
|
||||
public AppService Apps { get; }
|
||||
|
||||
private readonly DisplayFormatter _displayFormatter;
|
||||
private InvoiceRepository InvoiceRepository { get; }
|
||||
private AppService Apps { get; }
|
||||
|
||||
public override string Name => "Products sold";
|
||||
|
||||
@ -27,7 +29,7 @@ public class ProductsReportProvider : ReportProvider
|
||||
var appsById = (await Apps.GetApps(queryContext.StoreId)).ToDictionary(o => o.Id);
|
||||
var tagAllinvoicesApps = appsById.Values.Where(a => a.TagAllInvoices).ToList();
|
||||
queryContext.ViewDefinition = CreateDefinition();
|
||||
foreach (var i in (await InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
foreach (var i in (await InvoiceRepository.GetInvoices(new InvoiceQuery
|
||||
{
|
||||
IncludeArchived = true,
|
||||
IncludeAddresses = false,
|
||||
@ -63,8 +65,8 @@ public class ProductsReportProvider : ReportProvider
|
||||
{
|
||||
values.Add(code);
|
||||
values.Add(1);
|
||||
values.Add(i.Currency);
|
||||
values.Add(i.Price);
|
||||
values.Add(i.Currency);
|
||||
queryContext.Data.Add(values);
|
||||
}
|
||||
else
|
||||
@ -76,8 +78,8 @@ public class ProductsReportProvider : ReportProvider
|
||||
var copy = values.ToList();
|
||||
copy.Add(item.Id);
|
||||
copy.Add(item.Count);
|
||||
copy.Add(i.Currency);
|
||||
copy.Add(item.Price * item.Count);
|
||||
copy.Add(i.Currency);
|
||||
queryContext.Data.Add(copy);
|
||||
}
|
||||
}
|
||||
@ -87,13 +89,15 @@ public class ProductsReportProvider : ReportProvider
|
||||
// Round the currency amount
|
||||
foreach (var r in queryContext.Data)
|
||||
{
|
||||
r[^1] = ((decimal)r[^1]).RoundToSignificant(CurrencyNameTable.GetCurrencyData((string)r[^2] ?? "USD", true).Divisibility);
|
||||
var amount = (decimal)r[^2];
|
||||
var currency = (string)r[^1] ?? "USD";
|
||||
r[^2] = _displayFormatter.ToFormattedAmount(amount, currency);
|
||||
}
|
||||
}
|
||||
|
||||
private ViewDefinition CreateDefinition()
|
||||
{
|
||||
return new ViewDefinition()
|
||||
return new ViewDefinition
|
||||
{
|
||||
Fields =
|
||||
{
|
||||
@ -102,9 +106,9 @@ public class ProductsReportProvider : ReportProvider
|
||||
new ("State", "string"),
|
||||
new ("AppId", "string"),
|
||||
new ("Product", "string"),
|
||||
new ("Quantity", "decimal"),
|
||||
new ("Currency", "string"),
|
||||
new ("CurrencyAmount", "decimal")
|
||||
new ("Quantity", "integer"),
|
||||
new ("CurrencyAmount", "amount"),
|
||||
new ("Currency", "string")
|
||||
},
|
||||
Charts =
|
||||
{
|
||||
|
@ -59,7 +59,7 @@
|
||||
<h5>On-Chain Payments</h5>
|
||||
<div class="invoice-payments table-responsive mt-0">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="thead-inverse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-75px">Crypto</th>
|
||||
<th class="w-100px">Index</th>
|
||||
@ -75,7 +75,7 @@
|
||||
</th>
|
||||
}
|
||||
<th class="text-end">Confirmations</th>
|
||||
<th class="w-150px text-end">Amount</th>
|
||||
<th class="w-150px text-end">Paid</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -96,7 +96,7 @@
|
||||
}
|
||||
<td class="text-end">@payment.Confirmations</td>
|
||||
<td class="payment-value text-end text-nowrap">
|
||||
<span data-sensitive>@DisplayFormatter.Currency(payment.CryptoPaymentData.GetValue(), payment.Crypto)</span>
|
||||
<span data-sensitive class="text-success">@DisplayFormatter.Currency(payment.CryptoPaymentData.GetValue(), payment.Crypto)</span>
|
||||
@if (!string.IsNullOrEmpty(payment.AdditionalInformation))
|
||||
{
|
||||
<div>(@payment.AdditionalInformation)</div>
|
||||
|
@ -211,11 +211,11 @@
|
||||
</div>
|
||||
|
||||
<div class="text-center mb-4" id="crowdfund-body-header">
|
||||
<button v-if="active" id="crowdfund-body-header-cta" class="btn btn-lg btn-primary py-2 px-5 only-for-js" v-on:click="contributeModalOpen = true">Contribute</button>
|
||||
<button v-if="active" id="crowdfund-body-header-cta" class="btn btn-lg btn-primary py-2 px-5 only-for-js" v-on:click="contribute">Contribute</button>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4 justify-content-between gap-5">
|
||||
<div class="col-lg-7 col-sm-12" id="crowdfund-body-description-container">
|
||||
<div :class="{ 'col-lg-7 col-sm-12': hasPerks, 'col-12': !hasPerks }" id="crowdfund-body-description-container">
|
||||
<template v-if="srvModel.disqusEnabled && srvModel.disqusShortname">
|
||||
<b-tabs>
|
||||
<b-tab title="Details" active>
|
||||
@ -232,7 +232,7 @@
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="col-lg-4 col-sm-12" id="crowdfund-body-contribution-container">
|
||||
<div class="col-lg-4 col-sm-12" id="crowdfund-body-contribution-container" v-if="hasPerks">
|
||||
<contribute :target-currency="srvModel.targetCurrency"
|
||||
:loading="loading"
|
||||
:display-perks-ranking="srvModel.displayPerksRanking"
|
||||
@ -266,7 +266,6 @@
|
||||
:in-modal="true">
|
||||
</contribute>
|
||||
</b-modal>
|
||||
|
||||
<footer class="store-footer">
|
||||
<a class="store-powered-by" href="https://btcpayserver.org" target="_blank" rel="noreferrer noopener">
|
||||
Powered by <partial name="_StoreFooterLogo" />
|
||||
|
@ -30,13 +30,13 @@
|
||||
<h5>Off-Chain Payments</h5>
|
||||
<div class="invoice-payments table-responsive mt-0">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="thead-inverse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-75px">Crypto</th>
|
||||
<th class="w-100px">Type</th>
|
||||
<th class="w-175px">Destination</th>
|
||||
<th class="text-nowrap">Payment Proof</th>
|
||||
<th class="w-150px text-end">Amount</th>
|
||||
<th class="w-150px text-end">Paid</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -52,7 +52,7 @@
|
||||
<vc:truncate-center text="@payment.PaymentProof" classes="truncate-center-id" />
|
||||
</td>
|
||||
<td class="payment-value text-end text-nowrap">
|
||||
<span data-sensitive>@payment.Amount</span>
|
||||
<span data-sensitive class="text-success">@payment.Amount</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
|
@ -1,22 +1,34 @@
|
||||
@using BTCPayServer.Services.Altcoins.Monero.Configuration
|
||||
@using BTCPayServer.Services.Altcoins.Monero.UI
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@using BTCPayServer.Abstractions.Contracts
|
||||
@inject SignInManager<ApplicationUser> SignInManager;
|
||||
@inject MoneroLikeConfiguration MoneroLikeConfiguration;
|
||||
@inject IScopeProvider ScopeProvider
|
||||
@inject UIMoneroLikeStoreController UIMoneroLikeStore;
|
||||
@{
|
||||
var storeId = ScopeProvider.GetCurrentStoreId();
|
||||
var isActive = !string.IsNullOrEmpty(storeId) && ViewContext.RouteData.Values.TryGetValue("Controller", out var controller) && controller is not null &&
|
||||
nameof(UIMoneroLikeStoreController).StartsWith(controller.ToString() ?? string.Empty, StringComparison.InvariantCultureIgnoreCase);
|
||||
|
||||
}
|
||||
@if (MoneroLikeConfiguration.MoneroLikeConfigurationItems.Any())
|
||||
@if (SignInManager.IsSignedIn(User) && User.IsInRole(Roles.ServerAdmin) && MoneroLikeConfiguration.MoneroLikeConfigurationItems.Any())
|
||||
{
|
||||
<li class="nav-item" permission="@Policies.CanModifyStoreSettings">
|
||||
<a asp-action="GetStoreMoneroLikePaymentMethods" asp-controller="UIMoneroLikeStore" asp-route-storeId="@storeId" class="nav-link @(isActive ? "active" : string.Empty)" id="StoreNav-Monero">
|
||||
<span class="me-2 btcpay-status"></span>
|
||||
<span>Monero</span>
|
||||
</a>
|
||||
</li>
|
||||
var store = Context.GetStoreData();
|
||||
var result = await UIMoneroLikeStore.GetVM(store);
|
||||
|
||||
foreach (var item in result.Items)
|
||||
{
|
||||
|
||||
var isActive = !string.IsNullOrEmpty(storeId) && ViewContext.RouteData.Values.TryGetValue("Controller", out var controller) && controller is not null &&
|
||||
nameof(UIMoneroLikeStoreController).StartsWith(controller.ToString() ?? string.Empty, StringComparison.InvariantCultureIgnoreCase) &&
|
||||
ViewContext.RouteData.Values.TryGetValue("cryptoCode", out var cryptoCode) && cryptoCode is not null && cryptoCode.ToString() == item.CryptoCode;
|
||||
<li class="nav-item">
|
||||
<a class="nav-link @(isActive? "active" : "")"
|
||||
asp-route-cryptoCode="@item.CryptoCode"
|
||||
asp-route-storeId="@storeId"
|
||||
asp-action="GetStoreMoneroLikePaymentMethod"
|
||||
asp-controller="UIMoneroLikeStore">
|
||||
<span class="me-2 btcpay-status btcpay-status--@(item.Enabled ? "enabled" : "pending")"></span>
|
||||
<span>@item.CryptoCode Wallet</span>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
@using System.Globalization
|
||||
@using BTCPayServer.Services
|
||||
@using BTCPayServer.Services.Altcoins.Monero.Payments
|
||||
@using BTCPayServer.Services.Altcoins.Monero.UI
|
||||
@inject DisplayFormatter DisplayFormatter
|
||||
@model IEnumerable<BTCPayServer.Services.Invoices.PaymentEntity>
|
||||
|
||||
@{
|
||||
var onchainPayments = Model.Where(entity => entity.GetPaymentMethodId().PaymentType == MoneroPaymentType.Instance).Select(payment =>
|
||||
var payments = Model.Where(entity => entity.GetPaymentMethodId().PaymentType == MoneroPaymentType.Instance).Select(payment =>
|
||||
{
|
||||
var m = new MoneroPaymentViewModel();
|
||||
var onChainPaymentData = payment.GetCryptoPaymentData() as MoneroLikePaymentData;
|
||||
@ -26,37 +28,37 @@
|
||||
m.ReceivedTime = payment.ReceivedTime;
|
||||
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, payment.Network.BlockExplorerLink, m.TransactionId);
|
||||
return m;
|
||||
});
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
@if (onchainPayments.Any())
|
||||
@if (payments.Any())
|
||||
{
|
||||
<h5>Monero Payments</h5>
|
||||
<table class="table table-hover">
|
||||
<thead class="thead-inverse">
|
||||
<tr>
|
||||
<th>Crypto</th>
|
||||
<th>Deposit address</th>
|
||||
<th>Amount</th>
|
||||
<th>Transaction Id</th>
|
||||
<th class="text-right">Confirmations</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var payment in onchainPayments)
|
||||
{
|
||||
<tr >
|
||||
<td>@payment.Crypto</td>
|
||||
<td>@payment.DepositAddress</td>
|
||||
<td>@payment.Amount</td>
|
||||
<td>
|
||||
<a href="@payment.TransactionLink" class="text-break" target="_blank" rel="noreferrer noopener">
|
||||
@payment.TransactionId
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-right">@payment.Confirmations</td>
|
||||
<section>
|
||||
<h5>Monero Payments</h5>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-75px">Crypto</th>
|
||||
<th class="w-175px">Destination</th>
|
||||
<th class="text-nowrap">Payment Proof</th>
|
||||
<th class="text-end">Confirmations</th>
|
||||
<th class="w-150px text-end">Paid</th>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var payment in payments)
|
||||
{
|
||||
<tr >
|
||||
<td>@payment.Crypto</td>
|
||||
<td><vc:truncate-center text="@payment.DepositAddress" classes="truncate-center-id" /></td>
|
||||
<td><vc:truncate-center text="@payment.TransactionId" link="@payment.TransactionLink" classes="truncate-center-id" /></td>
|
||||
<td class="text-end">@payment.Confirmations</td>
|
||||
<td class="payment-value text-end text-nowrap">
|
||||
<span data-sensitive class="text-success">@DisplayFormatter.Currency(payment.Amount, payment.Crypto)</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
}
|
||||
|
@ -10,19 +10,23 @@
|
||||
|
||||
@if (Model.Items.Any())
|
||||
{
|
||||
var hasCart = Model.Items.ContainsKey("Cart");
|
||||
<table class="table my-0" v-pre>
|
||||
@if (Model.Items.ContainsKey("Cart"))
|
||||
@if (hasCart || (Model.Items.ContainsKey("Subtotal") && Model.Items.ContainsKey("Total")))
|
||||
{
|
||||
<tbody>
|
||||
@foreach (var (key, value) in (Dictionary <string, object>)Model.Items["Cart"])
|
||||
@if (hasCart)
|
||||
{
|
||||
<tbody>
|
||||
@foreach (var (key, value) in (Dictionary<string, object>)Model.Items["Cart"])
|
||||
{
|
||||
<tr>
|
||||
<th>@key</th>
|
||||
<td class="text-end">@value</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot style="border-top-width:3px">
|
||||
</tbody>
|
||||
}
|
||||
<tfoot style="border-top-width:@(hasCart ? "3px" : "0")">
|
||||
@if (Model.Items.ContainsKey("Subtotal"))
|
||||
{
|
||||
<tr>
|
||||
|
@ -1,7 +1,5 @@
|
||||
@using BTCPayServer.Client
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@using BTCPayServer.Views.Stores
|
||||
@model BTCPayServer.Components.MainNav.MainNavViewModel
|
||||
@{
|
||||
@ -10,10 +8,9 @@
|
||||
|
||||
@if (store != null)
|
||||
{
|
||||
|
||||
<li class="nav-item" permission="@Policies.CanModifyStoreSettings">
|
||||
<a asp-area="" asp-controller="UIShopify" asp-action="EditShopify" asp-route-storeId="@store.Id" class="nav-link @ViewData.IsActivePage("shopify", nameof(StoreNavPages))" id="StoreNav-Shopify">
|
||||
<vc:icon symbol="shopify" />
|
||||
<vc:icon symbol="logo-shopify" />
|
||||
<span>Shopify</span>
|
||||
</a>
|
||||
</li>
|
||||
|
@ -1,10 +1,13 @@
|
||||
@using System.Globalization
|
||||
@using BTCPayServer.Components.TruncateCenter
|
||||
@using BTCPayServer.Services
|
||||
@using BTCPayServer.Services.Altcoins.Zcash.Payments
|
||||
@using BTCPayServer.Services.Altcoins.Zcash.UI
|
||||
@inject DisplayFormatter DisplayFormatter
|
||||
@model IEnumerable<BTCPayServer.Services.Invoices.PaymentEntity>
|
||||
|
||||
@{
|
||||
var onchainPayments = Model.Where(entity => entity.GetPaymentMethodId().PaymentType == ZcashPaymentType.Instance).Select(payment =>
|
||||
var payments = Model.Where(entity => entity.GetPaymentMethodId().PaymentType == ZcashPaymentType.Instance).Select(payment =>
|
||||
{
|
||||
var m = new ZcashPaymentViewModel();
|
||||
var onChainPaymentData = payment.GetCryptoPaymentData() as ZcashLikePaymentData;
|
||||
@ -26,37 +29,37 @@
|
||||
m.ReceivedTime = payment.ReceivedTime;
|
||||
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, payment.Network.BlockExplorerLink, m.TransactionId);
|
||||
return m;
|
||||
});
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
@if (onchainPayments.Any())
|
||||
@if (payments.Any())
|
||||
{
|
||||
<h5>Zcash Payments</h5>
|
||||
<table class="table table-hover">
|
||||
<thead class="thead-inverse">
|
||||
<tr>
|
||||
<th>Crypto</th>
|
||||
<th>Deposit address</th>
|
||||
<th>Amount</th>
|
||||
<th>Transaction Id</th>
|
||||
<th class="text-right">Confirmations</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var payment in onchainPayments)
|
||||
{
|
||||
<tr >
|
||||
<td>@payment.Crypto</td>
|
||||
<td>@payment.DepositAddress</td>
|
||||
<td>@payment.Amount</td>
|
||||
<td>
|
||||
<a href="@payment.TransactionLink" class="text-break" target="_blank" rel="noreferrer noopener">
|
||||
@payment.TransactionId
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-right">@payment.Confirmations</td>
|
||||
<section>
|
||||
<h5>Zcash Payments</h5>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-75px">Crypto</th>
|
||||
<th class="w-175px">Destination</th>
|
||||
<th class="text-nowrap">Payment Proof</th>
|
||||
<th class="text-end">Confirmations</th>
|
||||
<th class="w-150px text-end">Paid</th>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var payment in payments)
|
||||
{
|
||||
<tr >
|
||||
<td>@payment.Crypto</td>
|
||||
<td><vc:truncate-center text="@payment.DepositAddress" classes="truncate-center-id" /></td>
|
||||
<td><vc:truncate-center text="@payment.TransactionId" link="@payment.TransactionLink" classes="truncate-center-id" /></td>
|
||||
<td class="text-end">@payment.Confirmations</td>
|
||||
<td class="payment-value text-end text-nowrap">
|
||||
<span data-sensitive class="text-success">@DisplayFormatter.Currency(payment.Amount, payment.Crypto)</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
}
|
||||
|
@ -5,19 +5,19 @@
|
||||
<div class="d-flex flex-wrap flex-column justify-content-between flex-xl-row gap-3">
|
||||
<div class="d-flex flex-wrap justify-content-center justify-content-xl-start gap-4">
|
||||
<a href="https://github.com/btcpayserver/btcpayserver" target="_blank" rel="noreferrer noopener">
|
||||
<vc:icon symbol="github"/>
|
||||
<vc:icon symbol="social-github"/>
|
||||
<span>Github</span>
|
||||
</a>
|
||||
<a href="https://chat.btcpayserver.org/" target="_blank" rel="noreferrer noopener">
|
||||
<vc:icon symbol="mattermost"/>
|
||||
<vc:icon symbol="social-mattermost"/>
|
||||
<span>Mattermost</span>
|
||||
</a>
|
||||
<a href="https://twitter.com/BtcpayServer" target="_blank" rel="noreferrer noopener">
|
||||
<vc:icon symbol="twitter"/>
|
||||
<vc:icon symbol="social-twitter"/>
|
||||
<span>Twitter</span>
|
||||
</a>
|
||||
<a href="https://t.me/btcpayserver" target="_blank" rel="noreferrer noopener">
|
||||
<vc:icon symbol="telegram"/>
|
||||
<vc:icon symbol="social-telegram"/>
|
||||
<span>Telegram</span>
|
||||
</a>
|
||||
<a href="https://btcpayserver.org/donate/" target="_blank" rel="noreferrer noopener">
|
||||
|
@ -248,37 +248,37 @@
|
||||
</noscript>
|
||||
<script type="text/x-template" id="payment-details">
|
||||
<dl>
|
||||
<div v-if="orderAmount > 0" id="PaymentDetails-TotalPrice">
|
||||
<div v-if="orderAmount > 0" id="PaymentDetails-TotalPrice" key="TotalPrice">
|
||||
<dt v-t="'total_price'"></dt>
|
||||
<dd :data-clipboard="srvModel.orderAmount" data-clipboard-hover="start">{{srvModel.orderAmount}} {{ srvModel.cryptoCode }}</dd>
|
||||
</div>
|
||||
<div v-if="orderAmount > 0 && srvModel.orderAmountFiat" id="PaymentDetails-TotalFiat">
|
||||
<div v-if="orderAmount > 0 && srvModel.orderAmountFiat" id="PaymentDetails-TotalFiat" key="TotalFiat">
|
||||
<dt v-t="'total_fiat'"></dt>
|
||||
<dd :data-clipboard="srvModel.orderAmountFiat" data-clipboard-hover="start">{{srvModel.orderAmountFiat}}</dd>
|
||||
</div>
|
||||
<div v-if="srvModel.rate && srvModel.cryptoCode" id="PaymentDetails-ExchangeRate">
|
||||
<div v-if="srvModel.rate && srvModel.cryptoCode" id="PaymentDetails-ExchangeRate" key="ExchangeRate">
|
||||
<dt v-t="'exchange_rate'"></dt>
|
||||
<dd :data-clipboard="srvModel.rate" data-clipboard-hover="start">
|
||||
<template v-if="srvModel.cryptoCode === 'sats'">1 sat = {{ srvModel.rate }}</template>
|
||||
<template v-else>1 {{ srvModel.cryptoCode }} = {{ srvModel.rate }}</template>
|
||||
</dd>
|
||||
</div>
|
||||
<div v-if="srvModel.networkFee" id="PaymentDetails-NetworkCost">
|
||||
<div v-if="srvModel.networkFee" id="PaymentDetails-NetworkCost" key="NetworkCost">
|
||||
<dt v-t="'network_cost'"></dt>
|
||||
<dd :data-clipboard="srvModel.networkFee" data-clipboard-hover="start">
|
||||
<div v-if="srvModel.txCountForFee > 0" v-t="{ path: 'tx_count', args: { count: srvModel.txCount } }"></div>
|
||||
<div v-text="`${srvModel.networkFee} ${srvModel.cryptoCode}`"></div>
|
||||
</dd>
|
||||
</div>
|
||||
<div v-if="btcPaid > 0" id="PaymentDetails-AmountPaid">
|
||||
<div v-if="btcPaid > 0" id="PaymentDetails-AmountPaid" key="AmountPaid">
|
||||
<dt v-t="'amount_paid'"></dt>
|
||||
<dd :data-clipboard="srvModel.btcPaid" data-clipboard-hover="start" v-text="`${srvModel.btcPaid} ${srvModel.cryptoCode}`"></dd>
|
||||
</div>
|
||||
<div v-if="btcDue > 0" id="PaymentDetails-AmountDue">
|
||||
<div v-if="btcDue > 0" id="PaymentDetails-AmountDue" key="AmountDue">
|
||||
<dt v-t="'amount_due'"></dt>
|
||||
<dd :data-clipboard="srvModel.btcDue" data-clipboard-hover="start" v-text="`${srvModel.btcDue} ${srvModel.cryptoCode}`"></dd>
|
||||
</div>
|
||||
<div v-if="showRecommendedFee" id="PaymentDetails-RecommendedFee">
|
||||
<div v-if="showRecommendedFee" id="PaymentDetails-RecommendedFee" key="RecommendedFee">
|
||||
<dt v-t="'recommended_fee'"></dt>
|
||||
<dd :data-clipboard="srvModel.feeRate" data-clipboard-hover="start" v-t="{ path: 'fee_rate', args: { feeRate: srvModel.feeRate } }"></dd>
|
||||
</div>
|
||||
|
@ -34,30 +34,6 @@
|
||||
|
||||
@section PageFootContent {
|
||||
<script>
|
||||
const alertClasses = { "Settled (marked)": 'success', "Invalid (marked)": 'danger' }
|
||||
|
||||
function changeInvoiceState(invoiceId, newState) {
|
||||
console.log(invoiceId, newState)
|
||||
const toggleButton = $("#markStatusDropdownMenuButton");
|
||||
toggleButton.attr("disabled", "disabled");
|
||||
|
||||
$.post(`${invoiceId}/changestate/${newState}`)
|
||||
.done(({ statusString }) => {
|
||||
const alertClass = alertClasses[statusString];
|
||||
toggleButton.replaceWith(`<span class="fs-6 fw-normal badge bg-${alertClass}">${statusString} <span class="fa fa-check"></span></span>`);
|
||||
})
|
||||
.fail(function () {
|
||||
toggleButton.removeAttr("disabled");
|
||||
alert("Invoice state update failed");
|
||||
});
|
||||
}
|
||||
|
||||
delegate('click', '[data-change-invoice-status-button]', e => {
|
||||
const button = e.target.closest('[data-change-invoice-status-button]')
|
||||
const { id, status } = button.dataset
|
||||
changeInvoiceState(id, status)
|
||||
})
|
||||
|
||||
const handleRefundResponse = async response => {
|
||||
const modalBody = document.querySelector('#RefundModal .modal-body')
|
||||
if (response.ok && response.redirected) {
|
||||
@ -282,32 +258,8 @@
|
||||
<tr>
|
||||
<th>State</th>
|
||||
<td>
|
||||
@if (Model.CanMarkStatus)
|
||||
{
|
||||
<div class="dropdown changeInvoiceStateToggle">
|
||||
<button class="btn btn-secondary btn-sm dropdown-toggle py-1 px-2" type="button" id="markStatusDropdownMenuButton" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
@Model.State
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="markStatusDropdownMenuButton">
|
||||
@if (Model.CanMarkInvalid)
|
||||
{
|
||||
<button type="button" class="dropdown-item lh-base changeInvoiceState" data-id="@Model.Id" data-status="invalid" data-change-invoice-status-button>
|
||||
Mark as invalid
|
||||
</button>
|
||||
}
|
||||
@if (Model.CanMarkSettled)
|
||||
{
|
||||
<button type="button" class="dropdown-item lh-base changeInvoiceState" href="#" data-id="@Model.Id" data-status="settled" data-change-invoice-status-button>
|
||||
Mark as settled
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@Model.State
|
||||
}
|
||||
<vc:invoice-status invoice-id="@Model.Id" state="Model.State" payments="Model.Payments"
|
||||
is-archived="Model.Archived" has-refund="Model.HasRefund" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -327,7 +279,7 @@
|
||||
<td>@Model.TransactionSpeed</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Total Fiat Due</th>
|
||||
<th>Total Amount Due</th>
|
||||
<td><span data-sensitive>@Model.Fiat</span></td>
|
||||
</tr>
|
||||
@if (!string.IsNullOrEmpty(Model.RefundEmail))
|
||||
@ -506,7 +458,8 @@
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xxl-constrain">
|
||||
<h3 class="mb-3">Invoice Summary</h3>
|
||||
<partial name="ListInvoicesPaymentsPartial" model="(Model, true)" />
|
||||
|
||||
@ -638,20 +591,22 @@
|
||||
<h3 class="mb-0">Events</h3>
|
||||
<table class="table table-hover mt-3 mb-4">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Message</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var evt in Model.Events)
|
||||
{
|
||||
<tr class="text-@evt.GetCssClass()">
|
||||
<td>@evt.Timestamp.ToBrowserDate()</td>
|
||||
<td>@evt.Message</td>
|
||||
</tr>
|
||||
}
|
||||
@foreach (var evt in Model.Events)
|
||||
{
|
||||
<tr class="text-@evt.GetCssClass()">
|
||||
<td>@evt.Timestamp.ToBrowserDate()</td>
|
||||
<td>@evt.Message</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -32,8 +32,8 @@
|
||||
@section PageHeadContent
|
||||
{
|
||||
<style>
|
||||
.invoice-payments {
|
||||
padding-left: var(--btcpay-space-l);
|
||||
.invoice-details-row > td {
|
||||
padding: 1.5rem .5rem 0 2.65rem;
|
||||
}
|
||||
.dropdown > .btn {
|
||||
min-width: 7rem;
|
||||
@ -60,23 +60,6 @@
|
||||
});
|
||||
});
|
||||
|
||||
delegate('click', '.changeInvoiceState', e => {
|
||||
const { invoiceId, newState } = e.target.dataset;
|
||||
const pavpill = $("#pavpill_" + invoiceId);
|
||||
const originalHtml = pavpill.html();
|
||||
pavpill.html("<span class='fa fa-bitcoin fa-spin' style='margin-left:16px;'></span>");
|
||||
|
||||
$.post("invoices/" + invoiceId + "/changestate/" + newState)
|
||||
.done(function (data) {
|
||||
const statusHtml = "<span class='badge badge-" + newState + "'>" + data.statusString + " <span class='fa fa-check'></span></span>";
|
||||
pavpill.replaceWith(statusHtml);
|
||||
})
|
||||
.fail(function (data) {
|
||||
pavpill.html(originalHtml.replace("dropdown-menu show", "dropdown-menu"));
|
||||
alert("Invoice state update failed");
|
||||
});
|
||||
})
|
||||
|
||||
delegate('click', '.showInvoice', e => {
|
||||
e.preventDefault();
|
||||
const { invoiceId } = e.target.dataset;
|
||||
@ -386,66 +369,8 @@
|
||||
</td>
|
||||
<td class="text-break">@invoice.InvoiceId</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
@if (invoice.Details.Archived)
|
||||
{
|
||||
<span class="badge bg-warning">archived</span>
|
||||
}
|
||||
@if (invoice.CanMarkStatus)
|
||||
{
|
||||
<div id="pavpill_@invoice.InvoiceId" class="badge badge-@invoice.Status.Status.ToModernStatus().ToString().ToLower()">
|
||||
<span class="dropdown-toggle changeInvoiceStateToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
@invoice.Status.ToString()
|
||||
</span>
|
||||
<div class="dropdown-menu">
|
||||
@if (invoice.CanMarkInvalid)
|
||||
{
|
||||
<button type="button" class="dropdown-item lh-base changeInvoiceState" data-invoice-id="@invoice.InvoiceId" data-new-state="invalid">
|
||||
Mark as invalid
|
||||
</button>
|
||||
}
|
||||
@if (invoice.CanMarkSettled)
|
||||
{
|
||||
<button type="button" class="dropdown-item lh-base changeInvoiceState" data-invoice-id="@invoice.InvoiceId" data-new-state="settled">
|
||||
Mark as settled
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<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 text-nowrap">
|
||||
<span data-sensitive>@DisplayFormatter.Currency(invoice.Amount, invoice.Currency)</span>
|
||||
|
@ -6,22 +6,41 @@
|
||||
.Where(entities => entities.Key != null);
|
||||
}
|
||||
|
||||
@if (invoice.Overpaid)
|
||||
{
|
||||
var usedPaymentMethods = invoice.CryptoPayments.Count(p => p.Paid != null);
|
||||
<p class="d-flex align-items-center gap-2 mb-3 text-warning">
|
||||
<vc:icon symbol="warning"/>
|
||||
This invoice got overpaid.
|
||||
@if (usedPaymentMethods > 1)
|
||||
{
|
||||
@("Each payment method shows the total excess amount.")
|
||||
}
|
||||
</p>
|
||||
}
|
||||
<div class="invoice-payments table-responsive mt-0">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="thead-inverse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-nowrap w-175px">Payment method</th>
|
||||
@if (Model.ShowAddress)
|
||||
{
|
||||
<th>Destination</th>
|
||||
}
|
||||
<th class="w-150px text-end">Rate</th>
|
||||
<th class="w-150px text-end">Paid</th>
|
||||
<th class="w-150px text-end">Due</th>
|
||||
@if (invoice.Overpaid)
|
||||
@if (invoice.HasRates)
|
||||
{
|
||||
<th class="w-150px text-end">Rate</th>
|
||||
}
|
||||
<th class="w-150px text-end">Total due</th>
|
||||
@if (invoice.StillDue)
|
||||
{
|
||||
<th class="w-150px text-end">Still due</th>
|
||||
}
|
||||
else if (invoice.Overpaid)
|
||||
{
|
||||
<th class="w-150px text-end">Overpaid</th>
|
||||
}
|
||||
<th class="w-150px text-end">Paid</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -35,13 +54,39 @@
|
||||
<vc:truncate-center text="@payment.Address" classes="truncate-center-id" />
|
||||
</td>
|
||||
}
|
||||
<td class="text-nowrap text-end"><span data-sensitive>@payment.Rate</span></td>
|
||||
<td class="text-nowrap text-end"><span data-sensitive>@payment.Paid</span></td>
|
||||
<td class="text-nowrap text-end"><span data-sensitive>@payment.Due</span></td>
|
||||
@if (invoice.Overpaid)
|
||||
@if (invoice.HasRates)
|
||||
{
|
||||
<td class="text-nowrap text-end"><span data-sensitive>@payment.Overpaid</span></td>
|
||||
<td class="text-nowrap text-end">
|
||||
<span data-sensitive>@payment.Rate</span>
|
||||
</td>
|
||||
}
|
||||
<td class="text-nowrap text-end">
|
||||
<span data-sensitive>@payment.TotalDue</span>
|
||||
</td>
|
||||
@if (invoice.StillDue)
|
||||
{
|
||||
<td class="text-nowrap text-end">
|
||||
@if (payment.Due != null)
|
||||
{
|
||||
<span data-sensitive>@payment.Due</span>
|
||||
}
|
||||
</td>
|
||||
}
|
||||
else if (invoice.Overpaid)
|
||||
{
|
||||
<td class="text-nowrap text-end">
|
||||
@if (payment.Overpaid != null)
|
||||
{
|
||||
<span data-sensitive class="text-warning">@payment.Overpaid</span>
|
||||
}
|
||||
</td>
|
||||
}
|
||||
<td class="text-nowrap text-end">
|
||||
@if (payment.Paid != null)
|
||||
{
|
||||
<span data-sensitive class="text-success">@payment.Paid</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
var details = payment.PaymentMethodRaw.GetPaymentMethodDetails();
|
||||
var name = details.GetAdditionalDataPartialName();
|
||||
|
@ -5,7 +5,7 @@
|
||||
@{
|
||||
Layout = "../Shared/_NavLayout.cshtml";
|
||||
ViewData["NavPartialName"] = "../UIStores/_Nav";
|
||||
ViewData.SetActivePage(StoreNavPages.OnchainSettings, $"{Model.CryptoCode} Settings");
|
||||
ViewData.SetActivePage(Model.CryptoCode, $"{Model.CryptoCode} Settings", Model.CryptoCode);
|
||||
}
|
||||
|
||||
<div class="row">
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
@{
|
||||
Layout = "../Shared/_NavLayout.cshtml";
|
||||
ViewData.SetActivePage(StoreNavPages.OnchainSettings, "Monero Settings");
|
||||
ViewData.SetActivePage("Monero Settings", "Monero Settings", "Monero Settings");
|
||||
ViewData["NavPartialName"] = "../UIStores/_Nav";
|
||||
}
|
||||
|
||||
|
@ -30,6 +30,7 @@
|
||||
<partial name="LayoutHead" />
|
||||
<partial name="LayoutHeadStoreBranding" model="@(Model.BrandColor, Model.CssFileId, Model.CustomCSSLink, Model.EmbeddedCSS)" />
|
||||
<link href="~/vendor/bootstrap-vue/bootstrap-vue.min.css" asp-append-version="true" rel="stylesheet" />
|
||||
<link href="~/vendor/vue-qrcode-reader/vue-qrcode-reader.css" rel="stylesheet" asp-append-version="true"/>
|
||||
<style>
|
||||
.no-marker > ul { list-style-type: none; }
|
||||
</style>
|
||||
@ -46,7 +47,7 @@
|
||||
<div class="input-group">
|
||||
@if (Model.LnurlEndpoint is not null)
|
||||
{
|
||||
<button type="button" class="btn btn-secondary" id="lnurlwithdraw-button">
|
||||
<button type="button" class="btn btn-secondary only-for-js" id="lnurlwithdraw-button">
|
||||
<span class="fa fa-qrcode fa-2x" title="LNURL-Withdraw"></span>
|
||||
</button>
|
||||
}
|
||||
@ -56,13 +57,15 @@
|
||||
<input type="hidden" asp-for="SelectedPaymentMethod">
|
||||
<span class="input-group-text">@Model.PaymentMethods.First().ToPrettyString()</span>
|
||||
}
|
||||
else
|
||||
else if (!Model.BitcoinOnly)
|
||||
{
|
||||
<select class="form-select w-auto" asp-for="SelectedPaymentMethod" asp-items="Model.PaymentMethods.Select(id => new SelectListItem(id.ToPrettyString(), id.ToString()))"></select>
|
||||
}
|
||||
<button type="button" class="btn btn-secondary only-for-js" data-bs-toggle="modal" data-bs-target="#scanModal" title="Scan destination with camera" id="scandestination-button">
|
||||
<i class="fa fa-camera"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mb-3 col-sm-6 mb-sm-0 col-lg-3">
|
||||
<div class="input-group">
|
||||
<input type="number" inputmode="decimal" class="form-control form-control-lg text-end hide-number-spin" asp-for="ClaimedAmount" max="@Model.AmountDue" min="@Model.MinimumClaim" step="any" placeholder="Amount" required>
|
||||
@ -92,22 +95,21 @@
|
||||
{
|
||||
<h2 class="h4 mb-3">@Model.Title</h2>
|
||||
}
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="text-muted text-nowrap">Start Date</span>
|
||||
|
||||
<span class="text-nowrap">@Model.StartDate.ToString("g")</span>
|
||||
</div>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="text-muted text-nowrap">Last Updated</span>
|
||||
|
||||
<span class="text-nowrap">@Model.LastRefreshed.ToString("g")</span>
|
||||
<button type="button" class="btn btn-link fw-semibold d-none d-lg-inline-block d-print-none border-0 p-0 ms-4 only-for-js" id="copyLink">
|
||||
</div>
|
||||
<div class="d-flex align-items-center only-for-js gap-3 my-3">
|
||||
<button type="button" class="btn btn-link fw-semibold d-print-none p-0" id="copyLink">
|
||||
Copy Link
|
||||
</button>
|
||||
<button type="button" class="btn btn-link fw-semibold d-inline-block d-print-none border-0 p-0 ms-4 only-for-js" page-qr>
|
||||
<span class="fa fa-qrcode"></span> Show QR
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-link fw-semibold d-print-none p-0" page-qr>
|
||||
<span class="fa fa-qrcode"></span> Show QR
|
||||
</button>
|
||||
</div>
|
||||
@if (!string.IsNullOrEmpty(Model.ResetIn))
|
||||
{
|
||||
@ -207,10 +209,13 @@
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
<partial name="LayoutFoot" />
|
||||
<script src="~/vendor/vue-qrcode/vue-qrcode.min.js" asp-append-version="true"></script>
|
||||
<script src="~/vendor/vuejs/vue.min.js" asp-append-version="true"></script>
|
||||
<partial name="ShowQR" />
|
||||
<partial name="CameraScanner"/>
|
||||
<partial name="LayoutFoot" />
|
||||
<script src="~/vendor/vuejs/vue.min.js" asp-append-version="true"></script>
|
||||
<script src="~/vendor/vue-qrcode/vue-qrcode.min.js" asp-append-version="true"></script>
|
||||
<script src="~/vendor/vue-qrcode-reader/VueQrcodeReader.umd.min.js" asp-append-version="true"></script>
|
||||
<script src="~/vendor/ur-registry/urlib.min.js" asp-append-version="true"></script>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
window.qrApp = initQRShow({});
|
||||
@ -219,6 +224,12 @@
|
||||
qrApp.note = "Scan this QR code to open this page on your mobile device.";
|
||||
qrApp.showData(window.location.href);
|
||||
});
|
||||
|
||||
delegate('click', '#copyLink', window.copyUrlToClipboard);
|
||||
|
||||
initCameraScanningApp("Scan address/ payment link", data => {
|
||||
document.getElementById("Destination").value = data;
|
||||
}, "scanModal");
|
||||
});
|
||||
</script>
|
||||
@if (Model.LnurlEndpoint is not null)
|
||||
@ -246,11 +257,6 @@
|
||||
});
|
||||
</script>
|
||||
}
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
document.getElementById("copyLink").addEventListener("click", window.copyUrlToClipboard);
|
||||
});
|
||||
</script>
|
||||
<vc:ui-extension-point location="pullpayment-foot" model="@Model"></vc:ui-extension-point>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -61,28 +61,32 @@
|
||||
<div id="app" v-cloak class="w-100-fixed">
|
||||
<article v-for="chart in srv.charts" class="mb-5">
|
||||
<h3>{{ chart.name }}</h3>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover w-auto">
|
||||
<div class="table-responsive" v-if="chart.rows.length || chart.hasGrandTotal">
|
||||
<table class="table table-hover w-auto">
|
||||
<thead class="sticky-top bg-body">
|
||||
<tr>
|
||||
<th v-for="group in chart.groups">{{ group }}</th>
|
||||
<th v-for="agg in chart.aggregates">{{ agg }}</th>
|
||||
<th v-for="group in chart.groups">{{ titleCase(group) }}</th>
|
||||
<th v-for="agg in chart.aggregates" class="text-end">{{ titleCase(agg) }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in chart.rows">
|
||||
<td v-for="group in row.groups" :rowspan="group.rowCount">{{ group.name }}</td>
|
||||
<td v-if="row.isTotal" :colspan="row.rLevel">Total</td>
|
||||
<td v-for="value in row.values">{{ value }}</td>
|
||||
<td v-for="value in row.values" class="text-end">{{ displayValue(value) }}</td>
|
||||
</tr>
|
||||
<tr v-if="chart.hasGrandTotal"><td :colspan="chart.groups.length">Grand total</td><td v-for="value in chart.grandTotalValues">{{ value }}</td></tr>
|
||||
<tr v-if="chart.hasGrandTotal">
|
||||
<td :colspan="chart.groups.length">Grand Total</td>
|
||||
<td v-for="value in chart.grandTotalValues" class="text-end">{{ displayValue(value) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p v-else class="mt-3 mb-5 text-secondary">No data</p>
|
||||
</article>
|
||||
<article>
|
||||
<article v-if="srv.result.data">
|
||||
<h3 id="raw-data">Raw data</h3>
|
||||
<div class="table-responsive">
|
||||
<div class="table-responsive" v-if="srv.result.data.length">
|
||||
<table class="table table-hover w-auto">
|
||||
<thead class="sticky-top bg-body">
|
||||
<tr>
|
||||
@ -92,7 +96,7 @@
|
||||
:data-field="field.name"
|
||||
@@click.prevent="srv.sortBy(field.name)"
|
||||
:title="srv.fieldViews[field.name].sortByTitle">
|
||||
{{ field.name }}
|
||||
{{ titleCase(field.name) }}
|
||||
<span :class="srv.fieldViews[field.name].sortIconClass" />
|
||||
</a>
|
||||
</th>
|
||||
@ -100,25 +104,26 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, index) in srv.result.data" :key="index">
|
||||
<td class="text-nowrap" v-for="(value, columnIndex) in row" :key="columnIndex">
|
||||
<td class="text-nowrap" v-for="(value, columnIndex) in row" :key="columnIndex" :class="{ 'text-end': ['integer', 'decimal', 'amount'].includes(srv.result.fields[columnIndex].type) }">
|
||||
<a :href="getInvoiceUrl(value)"
|
||||
target="_blank"
|
||||
v-if="srv.result.fields[columnIndex].type === 'invoice_id'">{{ value }}</a>
|
||||
|
||||
v-if="srv.result.fields[columnIndex].type === 'invoice_id'">{{ displayValue(value) }}</a>
|
||||
<a :href="getExplorerUrl(value, row[columnIndex-1])"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
v-else-if="srv.result.fields[columnIndex].type === 'tx_id'">{{ value }}</a>
|
||||
<span v-else>{{ value }}</span>
|
||||
v-else-if="srv.result.fields[columnIndex].type === 'tx_id'">{{ displayValue(value) }}</a>
|
||||
<template v-else>{{ displayValue(value) }}</template>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p v-else class="mt-3 mb-5 text-secondary">No data</p>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
@section PageFootContent {
|
||||
<script src="~/vendor/decimal.js/decimal.min.js" asp-append-version="true"></script>
|
||||
<script src="~/vendor/FileSaver/FileSaver.min.js" asp-append-version="true"></script>
|
||||
<script src="~/vendor/papaparse/papaparse.min.js" asp-append-version="true"></script>
|
||||
<script src="~/vendor/vuejs/vue.min.js" asp-append-version="true"></script>
|
||||
|
@ -234,7 +234,7 @@
|
||||
{
|
||||
<li>
|
||||
<a href="@downloadInfo.Source" rel="noreferrer noopener" class="d-flex align-items-center" target="_blank">
|
||||
<vc:icon symbol="github" />
|
||||
<vc:icon symbol="social-github" />
|
||||
<span style="margin-left:.4rem">Sources</span>
|
||||
</a>
|
||||
</li>
|
||||
@ -363,7 +363,7 @@
|
||||
{
|
||||
<li>
|
||||
<a href="@plugin.Source" rel="noreferrer noopener" class="d-flex align-items-center" target="_blank">
|
||||
<vc:icon symbol="github" />
|
||||
<vc:icon symbol="social-github" />
|
||||
<span style="margin-left:.4rem">Sources</span>
|
||||
</a>
|
||||
</li>
|
||||
|
@ -164,7 +164,7 @@
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead class="thead-inverse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th permission="@Policies.CanModifyStoreSettings">
|
||||
<input id="@Model.PayoutState-selectAllCheckbox" type="checkbox" class="form-check-input selectAll" data-payout-state="@Model.PayoutState.ToString()" />
|
||||
|
@ -102,7 +102,7 @@
|
||||
}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead class="thead-inverse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
<a asp-action="PullPayments"
|
||||
|
@ -60,7 +60,7 @@
|
||||
<div class="form-text">Choose what event sends the email.</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Rules[index].To" class="form-label" data-required>Recipients</label>
|
||||
<label asp-for="Rules[index].To" class="form-label">Recipients</label>
|
||||
<input type="text" asp-for="Rules[index].To" class="form-control email-rule-to" />
|
||||
<span asp-validation-for="Rules[index].To" class="text-danger"></span>
|
||||
<div class="form-text">Who to send the email to. For multiple emails, separate with a comma.</div>
|
||||
|
@ -169,7 +169,7 @@
|
||||
|
||||
<div id="WalletTransactions" class="table-responsive-md">
|
||||
<table class="table table-hover">
|
||||
<thead class="thead-inverse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:2rem;" class="only-for-js">
|
||||
<input id="selectAllCheckbox" type="checkbox" class="form-check-input" />
|
||||
|
@ -38,7 +38,7 @@ document.addEventListener("DOMContentLoaded",function (ev) {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
if(!this.active || this.loading){
|
||||
if (!this.active || this.loading){
|
||||
return;
|
||||
}
|
||||
|
||||
@ -162,6 +162,9 @@ document.addEventListener("DOMContentLoaded",function (ev) {
|
||||
result.push(currentPerk);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
hasPerks() {
|
||||
return this.srvModel.perks && this.srvModel.perks.length > 0;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@ -214,6 +217,15 @@ document.addEventListener("DOMContentLoaded",function (ev) {
|
||||
},
|
||||
formatAmount: function(amount) {
|
||||
return formatAmount(amount, this.srvModel.currencyData.divisibility)
|
||||
},
|
||||
contribute() {
|
||||
if (!this.active || this.loading) return;
|
||||
|
||||
if (this.hasPerks){
|
||||
this.contributeModalOpen = true
|
||||
} else {
|
||||
eventAggregator.$emit("contribute", {amount: null, choiceKey: null});
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 75 KiB |
@ -1,11 +1,11 @@
|
||||
(function () {
|
||||
// Given sorted data, build a tabular data of given groups and aggregates.
|
||||
function groupBy(groupIndices, aggregatesIndices, data) {
|
||||
var summaryRows = [];
|
||||
var summaryRow = null;
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
const summaryRows = [];
|
||||
let summaryRow = null;
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
if (summaryRow) {
|
||||
for (var gi = 0; gi < groupIndices.length; gi++) {
|
||||
for (let gi = 0; gi < groupIndices.length; gi++) {
|
||||
if (summaryRow[gi] !== data[i][groupIndices[gi]]) {
|
||||
summaryRows.push(summaryRow);
|
||||
summaryRow = null;
|
||||
@ -15,16 +15,31 @@
|
||||
}
|
||||
if (!summaryRow) {
|
||||
summaryRow = new Array(groupIndices.length + aggregatesIndices.length);
|
||||
for (var gi = 0; gi < groupIndices.length; gi++) {
|
||||
for (let gi = 0; gi < groupIndices.length; gi++) {
|
||||
summaryRow[gi] = data[i][groupIndices[gi]];
|
||||
}
|
||||
summaryRow.fill(0, groupIndices.length);
|
||||
summaryRow.fill(new Decimal(0), groupIndices.length);
|
||||
}
|
||||
for (var ai = 0; ai < aggregatesIndices.length; ai++) {
|
||||
var v = data[i][aggregatesIndices[ai]];
|
||||
for (let ai = 0; ai < aggregatesIndices.length; ai++) {
|
||||
const v = data[i][aggregatesIndices[ai]];
|
||||
// TODO: support other aggregate functions
|
||||
if (v)
|
||||
summaryRow[groupIndices.length + ai] += v;
|
||||
if (typeof (v) === 'object' && v.v) {
|
||||
// Amount in the format of `{ v: "1.0000001", d: 8 }`, where v is decimal string and `d` is divisibility
|
||||
const agg = summaryRow[groupIndices.length + ai];
|
||||
let d = v.d;
|
||||
let val = new Decimal(v.v);
|
||||
if (typeof (agg) === 'object' && agg.v) {
|
||||
d = Math.max(d, agg.d);
|
||||
val = agg.v.plus(val);
|
||||
}
|
||||
summaryRow[groupIndices.length + ai] = {
|
||||
v: val,
|
||||
d: d
|
||||
};
|
||||
} else {
|
||||
const val = new Decimal(v);
|
||||
summaryRow[groupIndices.length + ai] = summaryRow[groupIndices.length + ai].plus(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (summaryRow) {
|
||||
@ -36,15 +51,12 @@
|
||||
// Sort tabular data by the column indices
|
||||
function byColumns(columnIndices) {
|
||||
return (a, b) => {
|
||||
for (var i = 0; i < columnIndices.length; i++) {
|
||||
var fieldIndex = columnIndices[i];
|
||||
for (let i = 0; i < columnIndices.length; i++) {
|
||||
const fieldIndex = columnIndices[i];
|
||||
if (!a[fieldIndex]) return 1;
|
||||
if (!b[fieldIndex]) return -1;
|
||||
|
||||
if (a[fieldIndex] < b[fieldIndex])
|
||||
return -1;
|
||||
if (a[fieldIndex] > b[fieldIndex])
|
||||
return 1;
|
||||
if (a[fieldIndex] < b[fieldIndex]) return -1;
|
||||
if (a[fieldIndex] > b[fieldIndex]) return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@ -53,23 +65,22 @@
|
||||
// Build a representation of the HTML table's data 'rows' from the tree of nodes.
|
||||
function buildRows(node, rows) {
|
||||
if (node.children.length === 0 && node.level !== 0) {
|
||||
var row =
|
||||
const row =
|
||||
{
|
||||
values: node.values,
|
||||
groups: [],
|
||||
isTotal: node.isTotal,
|
||||
rLevel: node.rLevel
|
||||
};
|
||||
// Round the nuber to 8 decimal to avoid weird decimal outputs
|
||||
for (var i = 0; i < row.values.length; i++) {
|
||||
if (typeof row.values[i] === 'number')
|
||||
row.values[i] = new Number(row.values[i].toFixed(8));
|
||||
for (let i = 0; i < row.values.length; i++) {
|
||||
if (typeof row.values[i] === 'number') {
|
||||
row.values[i] = new Decimal(row.values[i]);
|
||||
}
|
||||
}
|
||||
if (!node.isTotal)
|
||||
row.groups.push({ name: node.groups[node.groups.length - 1], rowCount: node.leafCount })
|
||||
var parent = node.parent;
|
||||
var n = node;
|
||||
while (parent && parent.level != 0 && parent.children[0] === n) {
|
||||
let parent = node.parent, n = node;
|
||||
while (parent && parent.level !== 0 && parent.children[0] === n) {
|
||||
row.groups.push({ name: parent.groups[parent.groups.length - 1], rowCount: parent.leafCount })
|
||||
n = parent;
|
||||
parent = parent.parent;
|
||||
@ -77,7 +88,7 @@
|
||||
row.groups.reverse();
|
||||
rows.push(row);
|
||||
}
|
||||
for (var i = 0; i < node.children.length; i++) {
|
||||
for (let i = 0; i < node.children.length; i++) {
|
||||
buildRows(node.children[i], rows);
|
||||
}
|
||||
}
|
||||
@ -90,12 +101,12 @@
|
||||
node.leafCount++;
|
||||
return;
|
||||
}
|
||||
for (var i = 0; i < node.children.length; i++) {
|
||||
for (let i = 0; i < node.children.length; i++) {
|
||||
visitTree(node.children[i]);
|
||||
node.leafCount += node.children[i].leafCount;
|
||||
}
|
||||
// Remove total if there is only one child outside of the total
|
||||
if (node.children.length == 2 && node.children[0].isTotal) {
|
||||
if (node.children.length === 2 && node.children[0].isTotal) {
|
||||
node.children.shift();
|
||||
node.leafCount--;
|
||||
}
|
||||
@ -114,12 +125,12 @@
|
||||
isTotal: true
|
||||
});
|
||||
}
|
||||
for (var i = 0; i < groupLevels[level].length; i++) {
|
||||
var foundFirst = false;
|
||||
var groupData = groupLevels[level][i];
|
||||
var gotoNextRow = false;
|
||||
var stop = false;
|
||||
for (var gi = 0; gi < parent.groups.length; gi++) {
|
||||
for (let i = 0; i < groupLevels[level].length; i++) {
|
||||
let foundFirst = false;
|
||||
let groupData = groupLevels[level][i];
|
||||
let gotoNextRow = false;
|
||||
let stop = false;
|
||||
for (let gi = 0; gi < parent.groups.length; gi++) {
|
||||
if (parent.groups[gi] !== groupData[gi]) {
|
||||
if (foundFirst) {
|
||||
stop = true;
|
||||
@ -135,7 +146,7 @@
|
||||
break;
|
||||
if (gotoNextRow)
|
||||
continue;
|
||||
var node =
|
||||
const node =
|
||||
{
|
||||
parent: parent,
|
||||
groups: groupData.slice(0, level),
|
||||
@ -179,7 +190,6 @@
|
||||
var groupIndices = summaryDefinition.groups.map(g => fields.findIndex((a) => a === g)).filter(g => g !== -1);
|
||||
var aggregatesIndices = summaryDefinition.aggregates.map(g => fields.findIndex((a) => a === g)).filter(g => g !== -1);
|
||||
aggregatesIndices = aggregatesIndices.filter(g => g !== -1);
|
||||
|
||||
// Filter rows
|
||||
rows = applyFilters(rows, fields, summaryDefinition.filters);
|
||||
|
||||
@ -190,7 +200,6 @@
|
||||
// [Region, Crypto, PaymentType]
|
||||
var groupRows = groupBy(groupIndices, aggregatesIndices, rows);
|
||||
|
||||
|
||||
// There will be several level of aggregation
|
||||
// For example, if you have 3 groups: [Region, Crypto, PaymentType] then you have 4 group data.
|
||||
// [Region, Crypto, PaymentType]
|
||||
@ -238,10 +247,8 @@
|
||||
// rlevel is the reverse. It starts from the highest level and goes down to 0
|
||||
rLevel: groupLevels.length
|
||||
};
|
||||
|
||||
|
||||
// Which levels will have a total row
|
||||
var totalLevels = [];
|
||||
let totalLevels = [];
|
||||
if (summaryDefinition.totals) {
|
||||
totalLevels = summaryDefinition.totals.map(g => summaryDefinition.groups.findIndex((a) => a === g) + 1).filter(a => a !== 0);
|
||||
}
|
||||
@ -251,10 +258,9 @@
|
||||
// Add a leafCount property to each node, it is the number of leaf below each nodes.
|
||||
visitTree(root);
|
||||
|
||||
// Create a representation that can easily be binded to VueJS
|
||||
// Create a representation that can easily be bound to VueJS
|
||||
var rows = [];
|
||||
buildRows(root, rows);
|
||||
|
||||
return {
|
||||
groups: summaryDefinition.groups,
|
||||
aggregates: summaryDefinition.aggregates,
|
||||
|
@ -129,7 +129,16 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
updateUIDateRange();
|
||||
app = new Vue({
|
||||
el: '#app',
|
||||
data() { return { srv } }
|
||||
data() { return { srv } },
|
||||
methods: {
|
||||
titleCase(str) {
|
||||
const result = str.replace(/([A-Z])/g, " $1");
|
||||
return result.charAt(0).toUpperCase() + result.slice(1);
|
||||
},
|
||||
displayValue(val) {
|
||||
return val && typeof (val) === "object" && val.d ? new Decimal(val.v).toFixed(val.d) : val;
|
||||
}
|
||||
}
|
||||
});
|
||||
fetchStoreReports();
|
||||
});
|
||||
@ -141,11 +150,14 @@ function updateUIDateRange() {
|
||||
|
||||
// This function modify all the fields of a given type
|
||||
function modifyFields(fields, data, type, action) {
|
||||
var fieldIndices = fields.map((f, i) => ({ i: i, type: f.type })).filter(f => f.type == type).map(f => f.i);
|
||||
const fieldIndices = fields
|
||||
.map((f, i) => ({ i: i, type: f.type }))
|
||||
.filter(f => f.type === type)
|
||||
.map(f => f.i);
|
||||
if (fieldIndices.length === 0)
|
||||
return;
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
for (var f = 0; f < fieldIndices.length; f++) {
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
for (let f = 0; f < fieldIndices.length; f++) {
|
||||
data[i][fieldIndices[f]] = action(data[i][fieldIndices[f]]);
|
||||
}
|
||||
}
|
||||
|
104
BTCPayServer/wwwroot/main/bootstrap/bootstrap.css
vendored
104
BTCPayServer/wwwroot/main/bootstrap/bootstrap.css
vendored
@ -1,5 +1,5 @@
|
||||
/*!
|
||||
* Bootstrap v5.3.0 (https://getbootstrap.com/)
|
||||
* Bootstrap v5.3.2 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2023 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
@ -13,7 +13,6 @@
|
||||
--btcpay-body-font-size: 0.875rem;
|
||||
--btcpay-body-font-weight: var(--btcpay-font-weight-normal);
|
||||
--btcpay-body-line-height: 1.6;
|
||||
--btcpay-body-color: var(--btcpay-body-text);
|
||||
--btcpay-secondary-bg: var(--btcpay-neutral-200);
|
||||
--btcpay-secondary-bg-rgb: 233, 236, 239;
|
||||
--btcpay-tertiary-color: rgba(var(--btcpay-dark-rgb), 0.5);
|
||||
@ -26,6 +25,7 @@
|
||||
--btcpay-body-link-accent-rgb: 10, 88, 202;
|
||||
--btcpay-link-hover-decoration: none;
|
||||
--btcpay-code-color: var(--btcpay-code-text);
|
||||
--btcpay-highlight-color: var(--btcpay-dark);
|
||||
--btcpay-highlight-bg: #fff3cd;
|
||||
--btcpay-border-width: 1px;
|
||||
--btcpay-border-style: solid;
|
||||
@ -66,7 +66,7 @@ body {
|
||||
font-size: var(--btcpay-body-font-size);
|
||||
font-weight: var(--btcpay-body-font-weight);
|
||||
line-height: var(--btcpay-body-line-height);
|
||||
color: var(--btcpay-body-color);
|
||||
color: var(--btcpay-body-text);
|
||||
text-align: var(--btcpay-body-text-align);
|
||||
background-color: var(--btcpay-body-bg);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
@ -199,6 +199,7 @@ small, .small {
|
||||
|
||||
mark, .mark {
|
||||
padding: 0.1875em;
|
||||
color: var(--btcpay-highlight-color);
|
||||
background-color: var(--btcpay-highlight-bg);
|
||||
}
|
||||
|
||||
@ -425,8 +426,8 @@ legend + * {
|
||||
}
|
||||
|
||||
[type="search"] {
|
||||
outline-offset: -2px;
|
||||
-webkit-appearance: textfield;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
/* rtl:raw:
|
||||
@ -1761,7 +1762,7 @@ progress {
|
||||
--btcpay-table-bg-type: initial;
|
||||
--btcpay-table-color-state: initial;
|
||||
--btcpay-table-bg-state: initial;
|
||||
--btcpay-table-color: var(--btcpay-body-color);
|
||||
--btcpay-table-color: var(--btcpay-body-text);
|
||||
--btcpay-table-bg: transparent;
|
||||
--btcpay-table-border-color: var(--btcpay-border-color);
|
||||
--btcpay-table-accent-bg: transparent;
|
||||
@ -1844,7 +1845,7 @@ progress {
|
||||
.table-primary {
|
||||
--btcpay-table-color: var(--btcpay-primary-dim-text);
|
||||
--btcpay-table-bg: var(--btcpay-primary-dim-bg);
|
||||
--btcpay-table-border-color: #bacbe6;
|
||||
--btcpay-table-border-color: var(--btcpay-primary-dim-border);
|
||||
--btcpay-table-striped-bg: var(--btcpay-primary-dim-bg-striped);
|
||||
--btcpay-table-striped-color: var(--btcpay-primary-dim-text-striped);
|
||||
--btcpay-table-active-bg: var(--btcpay-primary-dim-bg-active);
|
||||
@ -1858,7 +1859,7 @@ progress {
|
||||
.table-secondary {
|
||||
--btcpay-table-color: var(--btcpay-secondary-dim-text);
|
||||
--btcpay-table-bg: var(--btcpay-secondary-dim-bg);
|
||||
--btcpay-table-border-color: #cbccce;
|
||||
--btcpay-table-border-color: var(--btcpay-secondary-dim-border);
|
||||
--btcpay-table-striped-bg: var(--btcpay-secondary-dim-bg-striped);
|
||||
--btcpay-table-striped-color: var(--btcpay-secondary-dim-text-striped);
|
||||
--btcpay-table-active-bg: var(--btcpay-secondary-dim-bg-active);
|
||||
@ -1872,7 +1873,7 @@ progress {
|
||||
.table-success {
|
||||
--btcpay-table-color: var(--btcpay-success-dim-text);
|
||||
--btcpay-table-bg: var(--btcpay-success-dim-bg);
|
||||
--btcpay-table-border-color: #bcd0c7;
|
||||
--btcpay-table-border-color: var(--btcpay-success-dim-border);
|
||||
--btcpay-table-striped-bg: var(--btcpay-success-dim-bg-striped);
|
||||
--btcpay-table-striped-color: var(--btcpay-success-dim-text-striped);
|
||||
--btcpay-table-active-bg: var(--btcpay-success-dim-bg-active);
|
||||
@ -1886,7 +1887,7 @@ progress {
|
||||
.table-info {
|
||||
--btcpay-table-color: var(--btcpay-info-dim-text);
|
||||
--btcpay-table-bg: var(--btcpay-info-dim-bg);
|
||||
--btcpay-table-border-color: #badce3;
|
||||
--btcpay-table-border-color: var(--btcpay-info-dim-border);
|
||||
--btcpay-table-striped-bg: var(--btcpay-info-dim-bg-striped);
|
||||
--btcpay-table-striped-color: var(--btcpay-info-dim-text-striped);
|
||||
--btcpay-table-active-bg: var(--btcpay-info-dim-bg-active);
|
||||
@ -1900,7 +1901,7 @@ progress {
|
||||
.table-warning {
|
||||
--btcpay-table-color: var(--btcpay-warning-dim-text);
|
||||
--btcpay-table-bg: var(--btcpay-warning-dim-bg);
|
||||
--btcpay-table-border-color: #e6dbb9;
|
||||
--btcpay-table-border-color: var(--btcpay-warning-dim-border);
|
||||
--btcpay-table-striped-bg: var(--btcpay-warning-dim-bg-striped);
|
||||
--btcpay-table-striped-color: var(--btcpay-warning-dim-text-striped);
|
||||
--btcpay-table-active-bg: var(--btcpay-warning-dim-bg-active);
|
||||
@ -1914,7 +1915,7 @@ progress {
|
||||
.table-danger {
|
||||
--btcpay-table-color: var(--btcpay-danger-dim-text);
|
||||
--btcpay-table-bg: var(--btcpay-danger-dim-bg);
|
||||
--btcpay-table-border-color: #dfc2c4;
|
||||
--btcpay-table-border-color: var(--btcpay-danger-dim-border);
|
||||
--btcpay-table-striped-bg: var(--btcpay-danger-dim-bg-striped);
|
||||
--btcpay-table-striped-color: var(--btcpay-danger-dim-text-striped);
|
||||
--btcpay-table-active-bg: var(--btcpay-danger-dim-bg-active);
|
||||
@ -1928,7 +1929,7 @@ progress {
|
||||
.table-light {
|
||||
--btcpay-table-color: var(--btcpay-light-dim-text);
|
||||
--btcpay-table-bg: var(--btcpay-light-dim-bg);
|
||||
--btcpay-table-border-color: #dfe0e1;
|
||||
--btcpay-table-border-color: var(--btcpay-light-border-hover);
|
||||
--btcpay-table-striped-bg: var(--btcpay-light-dim-bg-striped);
|
||||
--btcpay-table-striped-color: var(--btcpay-light-dim-text-striped);
|
||||
--btcpay-table-active-bg: var(--btcpay-light-dim-bg-active);
|
||||
@ -1942,7 +1943,7 @@ progress {
|
||||
.table-dark {
|
||||
--btcpay-table-color: var(--btcpay-dark-dim-text);
|
||||
--btcpay-table-bg: var(--btcpay-dark-dim-bg);
|
||||
--btcpay-table-border-color: var(--btcpay-dark-border-hover);
|
||||
--btcpay-table-border-color: var(--btcpay-dark-dim-border);
|
||||
--btcpay-table-striped-bg: var(--btcpay-dark-dim-bg-striped);
|
||||
--btcpay-table-striped-color: var(--btcpay-dark-dim-text-striped);
|
||||
--btcpay-table-active-bg: var(--btcpay-dark-dim-bg-active);
|
||||
@ -2033,12 +2034,12 @@ progress {
|
||||
font-weight: var(--btcpay-font-weight-normal);
|
||||
line-height: 1.6;
|
||||
color: var(--btcpay-form-text);
|
||||
background-color: var(--btcpay-form-bg);
|
||||
background-clip: padding-box;
|
||||
border: var(--btcpay-border-width) solid var(--btcpay-form-border);
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
background-color: var(--btcpay-form-bg);
|
||||
background-clip: padding-box;
|
||||
border: var(--btcpay-border-width) solid var(--btcpay-form-border);
|
||||
border-radius: var(--btcpay-border-radius);
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
@ -2133,7 +2134,7 @@ progress {
|
||||
padding: 0.5rem 0;
|
||||
margin-bottom: 0;
|
||||
line-height: 1.6;
|
||||
color: var(--btcpay-body-color);
|
||||
color: var(--btcpay-body-text);
|
||||
background-color: transparent;
|
||||
border: solid transparent;
|
||||
border-width: var(--btcpay-border-width) 0;
|
||||
@ -2239,6 +2240,9 @@ textarea.form-control-lg {
|
||||
font-weight: var(--btcpay-font-weight-normal);
|
||||
line-height: 1.6;
|
||||
color: var(--btcpay-form-text);
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
background-color: var(--btcpay-form-bg);
|
||||
background-image: var(--btcpay-form-select-bg-img), var(--btcpay-form-select-bg-icon, none);
|
||||
background-repeat: no-repeat;
|
||||
@ -2247,9 +2251,6 @@ textarea.form-control-lg {
|
||||
border: var(--btcpay-border-width) solid var(--btcpay-form-border);
|
||||
border-radius: var(--btcpay-border-radius);
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.form-select:focus {
|
||||
@ -2315,19 +2316,20 @@ textarea.form-control-lg {
|
||||
|
||||
.form-check-input {
|
||||
--btcpay-form-check-bg: transparent;
|
||||
flex-shrink: 0;
|
||||
width: 1.25em;
|
||||
height: 1.25em;
|
||||
margin-top: 0.175em;
|
||||
vertical-align: top;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
background-color: var(--btcpay-form-check-bg);
|
||||
background-image: var(--btcpay-form-check-bg-image);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
border: 2px solid var(--btcpay-form-border-check);
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
-webkit-print-color-adjust: exact;
|
||||
color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
@ -2435,10 +2437,10 @@ textarea.form-control-lg {
|
||||
width: 100%;
|
||||
height: calc(1rem + 4px);
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.form-range:focus {
|
||||
@ -2461,13 +2463,13 @@ textarea.form-control-lg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-top: -0.25rem;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background-color: var(--btcpay-primary);
|
||||
border: 0;
|
||||
border-radius: 1rem;
|
||||
-webkit-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.form-range::-webkit-slider-thumb:active {
|
||||
@ -2487,13 +2489,13 @@ textarea.form-control-lg {
|
||||
.form-range::-moz-range-thumb {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
background-color: var(--btcpay-primary);
|
||||
border: 0;
|
||||
border-radius: 1rem;
|
||||
-moz-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.form-range::-moz-range-thumb:active {
|
||||
@ -2633,11 +2635,13 @@ textarea.form-control-lg {
|
||||
border-width: var(--btcpay-border-width) 0;
|
||||
}
|
||||
|
||||
.form-floating > :disabled ~ label {
|
||||
.form-floating > :disabled ~ label,
|
||||
.form-floating > .form-control:disabled ~ label {
|
||||
color: var(--btcpay-secondary);
|
||||
}
|
||||
|
||||
.form-floating > :disabled ~ label::after {
|
||||
.form-floating > :disabled ~ label::after,
|
||||
.form-floating > .form-control:disabled ~ label::after {
|
||||
background-color: var(--btcpay-form-bg-disabled);
|
||||
}
|
||||
|
||||
@ -2938,7 +2942,7 @@ textarea.form-control-lg {
|
||||
--btcpay-btn-font-size: 0.875rem;
|
||||
--btcpay-btn-font-weight: var(--btcpay-font-weight-semibold);
|
||||
--btcpay-btn-line-height: 1.6;
|
||||
--btcpay-btn-color: var(--btcpay-body-color);
|
||||
--btcpay-btn-color: var(--btcpay-body-text);
|
||||
--btcpay-btn-bg: transparent;
|
||||
--btcpay-btn-border-width: var(--btcpay-border-width);
|
||||
--btcpay-btn-border-color: transparent;
|
||||
@ -3394,7 +3398,7 @@ fieldset:disabled .btn {
|
||||
--btcpay-dropdown-inner-border-radius: calc(var(--btcpay-border-radius) - var(--btcpay-border-width));
|
||||
--btcpay-dropdown-divider-bg: var(--btcpay-body-border-medium);
|
||||
--btcpay-dropdown-divider-margin-y: 0.5rem;
|
||||
--btcpay-dropdown-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.05);
|
||||
--btcpay-dropdown-box-shadow: var(--btcpay-box-shadow);
|
||||
--btcpay-dropdown-link-color: var(--btcpay-body-text);
|
||||
--btcpay-dropdown-link-hover-color: var(--btcpay-body-text);
|
||||
--btcpay-dropdown-link-hover-bg: var(--btcpay-body-bg-hover);
|
||||
@ -3838,7 +3842,7 @@ fieldset:disabled .btn {
|
||||
box-shadow: 0 0 0 0.25rem rgba(var(--btcpay-primary-rgb), 0.25);
|
||||
}
|
||||
|
||||
.nav-link.disabled {
|
||||
.nav-link.disabled, .nav-link:disabled {
|
||||
color: var(--btcpay-nav-link-disabled-color);
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
@ -3867,12 +3871,6 @@ fieldset:disabled .btn {
|
||||
border-color: var(--btcpay-nav-tabs-link-hover-border-color);
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link.disabled, .nav-tabs .nav-link:disabled {
|
||||
color: var(--btcpay-nav-link-disabled-color);
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link.active,
|
||||
.nav-tabs .nav-item.show .nav-link {
|
||||
color: var(--btcpay-nav-tabs-link-active-color);
|
||||
@ -3896,12 +3894,6 @@ fieldset:disabled .btn {
|
||||
border-radius: var(--btcpay-nav-pills-border-radius);
|
||||
}
|
||||
|
||||
.nav-pills .nav-link:disabled {
|
||||
color: var(--btcpay-nav-link-disabled-color);
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.nav-pills .nav-link.active,
|
||||
.nav-pills .show > .nav-link {
|
||||
color: var(--btcpay-nav-pills-link-active-color);
|
||||
@ -4417,7 +4409,7 @@ fieldset:disabled .btn {
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
height: var(--btcpay-card-height);
|
||||
color: var(--btcpay-body-color);
|
||||
color: var(--btcpay-body-text);
|
||||
word-wrap: break-word;
|
||||
background-color: var(--btcpay-card-bg);
|
||||
background-clip: border-box;
|
||||
@ -4589,7 +4581,7 @@ fieldset:disabled .btn {
|
||||
}
|
||||
|
||||
.accordion {
|
||||
--btcpay-accordion-color: var(--btcpay-body-color);
|
||||
--btcpay-accordion-color: var(--btcpay-body-text);
|
||||
--btcpay-accordion-bg: transparent;
|
||||
--btcpay-accordion-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, border-radius 0.15s ease;
|
||||
--btcpay-accordion-border-color: transparent;
|
||||
@ -5525,7 +5517,7 @@ fieldset:disabled .btn {
|
||||
--btcpay-modal-border-color: var(--btcpay-border-color);
|
||||
--btcpay-modal-border-width: var(--btcpay-border-width);
|
||||
--btcpay-modal-border-radius: var(--btcpay-border-radius-lg);
|
||||
--btcpay-modal-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
|
||||
--btcpay-modal-box-shadow: var(--btcpay-box-shadow-sm);
|
||||
--btcpay-modal-inner-border-radius: calc(var(--btcpay-border-radius-lg) - (var(--btcpay-border-width)));
|
||||
--btcpay-modal-header-padding-x: 1rem;
|
||||
--btcpay-modal-header-padding-y: 1rem;
|
||||
@ -5670,7 +5662,7 @@ fieldset:disabled .btn {
|
||||
@media (min-width: 576px) {
|
||||
.modal {
|
||||
--btcpay-modal-margin: 1.75rem;
|
||||
--btcpay-modal-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.05);
|
||||
--btcpay-modal-box-shadow: var(--btcpay-box-shadow);
|
||||
}
|
||||
.modal-dialog {
|
||||
max-width: var(--btcpay-modal-width);
|
||||
@ -5940,7 +5932,7 @@ fieldset:disabled .btn {
|
||||
--btcpay-popover-border-color: var(--btcpay-body-border-medium);
|
||||
--btcpay-popover-border-radius: var(--btcpay-border-radius-lg);
|
||||
--btcpay-popover-inner-border-radius: calc(var(--btcpay-border-radius-lg) - var(--btcpay-border-width));
|
||||
--btcpay-popover-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.05);
|
||||
--btcpay-popover-box-shadow: var(--btcpay-box-shadow);
|
||||
--btcpay-popover-header-padding-x: 1rem;
|
||||
--btcpay-popover-header-padding-y: 0.5rem;
|
||||
--btcpay-popover-header-font-size: 0.875rem;
|
||||
@ -5948,7 +5940,7 @@ fieldset:disabled .btn {
|
||||
--btcpay-popover-header-bg: var(--btcpay-bg-tile);
|
||||
--btcpay-popover-body-padding-x: 1rem;
|
||||
--btcpay-popover-body-padding-y: 1rem;
|
||||
--btcpay-popover-body-color: var(--btcpay-body-color);
|
||||
--btcpay-popover-body-color: var(--btcpay-body-text);
|
||||
--btcpay-popover-arrow-width: 1rem;
|
||||
--btcpay-popover-arrow-height: 0.5rem;
|
||||
--btcpay-popover-arrow-border: var(--btcpay-popover-border-color);
|
||||
@ -6357,7 +6349,7 @@ fieldset:disabled .btn {
|
||||
--btcpay-offcanvas-height: auto;
|
||||
--btcpay-offcanvas-padding-x: 0;
|
||||
--btcpay-offcanvas-padding-y: 0.5rem;
|
||||
--btcpay-offcanvas-color: var(--btcpay-body-color);
|
||||
--btcpay-offcanvas-color: var(--btcpay-body-text);
|
||||
--btcpay-offcanvas-bg: var(--btcpay-bg-tile);
|
||||
--btcpay-offcanvas-border-width: var(--btcpay-border-width);
|
||||
--btcpay-offcanvas-border-color: var(--btcpay-border-color);
|
||||
@ -7250,7 +7242,7 @@ fieldset:disabled .btn {
|
||||
.vr {
|
||||
display: inline-block;
|
||||
align-self: stretch;
|
||||
width: 1px;
|
||||
width: var(--btcpay-border-width);
|
||||
min-height: 1em;
|
||||
background-color: currentcolor;
|
||||
opacity: 0.25;
|
||||
@ -7430,15 +7422,15 @@ fieldset:disabled .btn {
|
||||
}
|
||||
|
||||
.shadow {
|
||||
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.05) !important;
|
||||
box-shadow: var(--btcpay-box-shadow) !important;
|
||||
}
|
||||
|
||||
.shadow-sm {
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08) !important;
|
||||
box-shadow: var(--btcpay-box-shadow-sm) !important;
|
||||
}
|
||||
|
||||
.shadow-lg {
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1) !important;
|
||||
box-shadow: var(--btcpay-box-shadow-lg) !important;
|
||||
}
|
||||
|
||||
.shadow-none {
|
||||
|
@ -76,7 +76,7 @@ hr.primary {
|
||||
|
||||
@media (min-width: 1400px) {
|
||||
.col-xxl-constrain {
|
||||
max-width: 800px;
|
||||
max-width: 984px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -115,14 +115,11 @@ a.unobtrusive-link {
|
||||
background: var(--btcpay-danger);
|
||||
color: var(--btcpay-danger-text);
|
||||
}
|
||||
.badge-unusual {
|
||||
.badge-unusual,
|
||||
.badge-processing {
|
||||
background: var(--btcpay-warning);
|
||||
color: var(--btcpay-warning-text);
|
||||
}
|
||||
.badge-processing {
|
||||
background: var(--btcpay-info);
|
||||
color: var(--btcpay-info-text);
|
||||
}
|
||||
.badge-settled {
|
||||
background: var(--btcpay-success);
|
||||
color: var(--btcpay-success-text);
|
||||
@ -263,17 +260,6 @@ h2 svg.icon.icon-info {
|
||||
.card {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
#markStatusDropdownMenuButton {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
padding: 0 !important;
|
||||
color: inherit;
|
||||
font-weight: var(--btcpay-font-weight-normal);
|
||||
font-size: var(--btcpay-body-font-size);
|
||||
}
|
||||
#markStatusDropdownMenuButton::after {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Richtext editor */
|
||||
|
@ -1,3 +1,5 @@
|
||||
const baseUrl = Object.values(document.scripts).find(s => s.src.includes('/main/site.js')).src.split('/main/site.js').shift();
|
||||
|
||||
const flatpickrInstances = [];
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat
|
||||
@ -268,6 +270,23 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Invoice Status
|
||||
delegate('click', '[data-invoice-state-badge] [data-invoice-id][data-new-state]', async e => {
|
||||
const $button = e.target
|
||||
const $badge = $button.closest('[data-invoice-state-badge]')
|
||||
const { invoiceId, newState } = $button.dataset
|
||||
|
||||
$badge.classList.add('pe-none'); // disable further interaction
|
||||
const response = await fetch(`${baseUrl}/invoices/${invoiceId}/changestate/${newState}`, { method: 'POST' })
|
||||
if (response.ok) {
|
||||
const { statusString } = await response.json()
|
||||
$badge.outerHTML = `<div class="badge badge-${newState}" data-invoice-state-badge="${invoiceId}">${statusString}</div>`
|
||||
} else {
|
||||
$badge.classList.remove('pe-none');
|
||||
alert("Invoice state update failed");
|
||||
}
|
||||
})
|
||||
|
||||
// Time Format
|
||||
delegate('click', '.switch-time-format', switchTimeFormat);
|
||||
|
||||
@ -342,59 +361,79 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
if (window.Blazor) {
|
||||
let isUnloading = false;
|
||||
window.addEventListener("beforeunload", () => { isUnloading = true; });
|
||||
let brokenConnection = {
|
||||
isConnected: false,
|
||||
titleContent: 'Connection broken',
|
||||
innerHTML: 'Please <a href="">refresh the page</a>.'
|
||||
};
|
||||
let interruptedConnection = {
|
||||
isConnected: false,
|
||||
titleContent: 'Connection interrupted',
|
||||
innerHTML: 'Attempt to reestablish the connection in a few seconds...'
|
||||
};
|
||||
let successfulConnection = {
|
||||
isConnected: true,
|
||||
titleContent: 'Connection established',
|
||||
innerHTML: '' // use empty link on purpose
|
||||
};
|
||||
class BlazorReconnectionHandler {
|
||||
reconnecting = false;
|
||||
async onConnectionDown(options, _error) {
|
||||
if (this.reconnecting)
|
||||
return;
|
||||
this.setBlazorStatus(false);
|
||||
this.setBlazorStatus(interruptedConnection);
|
||||
this.reconnecting = true;
|
||||
console.debug('Blazor hub connection lost');
|
||||
await this.reconnect();
|
||||
}
|
||||
|
||||
async reconnect() {
|
||||
let delays = [500, 1000, 2000, 4000, 8000, 16000, 20000];
|
||||
let delays = [500, 1000, 2000, 4000, 8000, 16000, 20000, 40000];
|
||||
let i = 0;
|
||||
const lastDelay = delays.length - 1;
|
||||
while (true) {
|
||||
while (i < delays.length) {
|
||||
await this.delay(delays[i]);
|
||||
try {
|
||||
if (await Blazor.reconnect())
|
||||
break;
|
||||
|
||||
this.setBlazorStatus(false);
|
||||
return;
|
||||
console.warn('Error while reconnecting to Blazor hub (Broken circuit)');
|
||||
break;
|
||||
}
|
||||
catch (err) {
|
||||
this.setBlazorStatus(false);
|
||||
this.setBlazorStatus(interruptedConnection);
|
||||
console.warn(`Error while reconnecting to Blazor hub (${err})`);
|
||||
}
|
||||
i++;
|
||||
if (i > lastDelay)
|
||||
i = lastDelay;
|
||||
}
|
||||
this.setBlazorStatus(brokenConnection);
|
||||
}
|
||||
onConnectionUp() {
|
||||
this.reconnecting = false;
|
||||
console.debug('Blazor hub connected');
|
||||
this.setBlazorStatus(true);
|
||||
this.setBlazorStatus(successfulConnection);
|
||||
}
|
||||
|
||||
setBlazorStatus(isConnected) {
|
||||
setBlazorStatus(content) {
|
||||
document.querySelectorAll('.blazor-status').forEach($status => {
|
||||
const $state = $status.querySelector('.blazor-status__state');
|
||||
const $title = $status.querySelector('.blazor-status__title');
|
||||
const $body = $status.querySelector('.blazor-status__body');
|
||||
$state.classList.remove('btcpay-status--enabled');
|
||||
$state.classList.remove('btcpay-status--disabled');
|
||||
$state.classList.add('btcpay-status--' + (isConnected ? 'enabled' : 'disabled'));
|
||||
$title.textContent = isConnected ? 'Connection established' : 'Connection interrupted';
|
||||
$body.innerHTML = isConnected ? '' : 'Please <a href="">refresh the page</a>.'; // use empty link on purpose
|
||||
$body.classList.toggle('d-none', isConnected);
|
||||
if (!isConnected && !isUnloading) {
|
||||
$state.classList.add(content.isConnected ? 'btcpay-status--enabled' : 'btcpay-status--disabled');
|
||||
$title.textContent = content.titleContent;
|
||||
$body.innerHTML = content.innerHTML;
|
||||
$body.classList.toggle('d-none', content.isConnected);
|
||||
if (!isUnloading) {
|
||||
const toast = new bootstrap.Toast($status, { autohide: false });
|
||||
if (!toast.isShown())
|
||||
toast.show();
|
||||
if (content.isConnected) {
|
||||
if (toast.isShown())
|
||||
toast.hide();
|
||||
}
|
||||
else {
|
||||
if (!toast.isShown())
|
||||
toast.show();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -404,7 +443,7 @@ if (window.Blazor) {
|
||||
}
|
||||
|
||||
const handler = new BlazorReconnectionHandler();
|
||||
handler.setBlazorStatus(true);
|
||||
handler.setBlazorStatus(successfulConnection);
|
||||
Blazor.start({
|
||||
reconnectionHandler: handler
|
||||
});
|
||||
|
@ -58,6 +58,7 @@ const posCommon = {
|
||||
total: this.totalNumeric
|
||||
}
|
||||
if (this.tipNumeric > 0) data.tip = this.tipNumeric
|
||||
if (this.tipPercent > 0) data.tipPercentage = this.tipPercent
|
||||
if (this.discountNumeric > 0) data.discountAmount = this.discountNumeric
|
||||
if (this.discountPercentNumeric > 0) data.discountPercentage = this.discountPercentNumeric
|
||||
return JSON.stringify(data)
|
||||
|
@ -788,11 +788,17 @@
|
||||
},
|
||||
"depositablePaymentMethods": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "A list of payment methods (crypto code + network) you can deposit to the custodian.",
|
||||
"nullable": false
|
||||
},
|
||||
"withdrawablePaymentMethods": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "A list of payment methods (crypto code + network) you can withdraw from the custodian.",
|
||||
"nullable": false
|
||||
},
|
||||
|
@ -793,7 +793,11 @@
|
||||
"status": {
|
||||
"nullable": false,
|
||||
"description": "Mark an invoice as completed or invalid.",
|
||||
"$ref": "#/components/schemas/InvoiceStatusMark"
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/InvoiceStatusMark"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -811,7 +815,7 @@
|
||||
},
|
||||
"InvoiceStatus": {
|
||||
"type": "string",
|
||||
"description": "",
|
||||
"description": "The status of the invoice",
|
||||
"x-enumNames": [
|
||||
"New",
|
||||
"Processing",
|
||||
@ -854,13 +858,21 @@
|
||||
},
|
||||
"checkout": {
|
||||
"nullable": true,
|
||||
"$ref": "#/components/schemas/CheckoutOptions",
|
||||
"description": "Additional settings to customize the checkout flow"
|
||||
"description": "Additional settings to customize the checkout flow",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/CheckoutOptions"
|
||||
}
|
||||
]
|
||||
},
|
||||
"receipt": {
|
||||
"nullable": true,
|
||||
"$ref": "#/components/schemas/ReceiptOptions",
|
||||
"description": "Additional settings to customize the public receipt"
|
||||
"description": "Additional settings to customize the public receipt",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/ReceiptOptions"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -893,8 +905,7 @@
|
||||
"example": "USD"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/components/schemas/InvoiceType",
|
||||
"description": "The type of invoice"
|
||||
"$ref": "#/components/schemas/InvoiceType"
|
||||
},
|
||||
"checkoutLink": {
|
||||
"type": "string",
|
||||
@ -925,12 +936,10 @@
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/components/schemas/InvoiceStatus",
|
||||
"description": "The status of the invoice"
|
||||
"$ref": "#/components/schemas/InvoiceStatus"
|
||||
},
|
||||
"additionalStatus": {
|
||||
"$ref": "#/components/schemas/InvoiceAdditionalStatus",
|
||||
"description": "a secondary status of the invoice"
|
||||
"$ref": "#/components/schemas/InvoiceAdditionalStatus"
|
||||
},
|
||||
"availableStatusesForManualMarking": {
|
||||
"type": "array",
|
||||
@ -1167,8 +1176,11 @@
|
||||
"properties": {
|
||||
"speedPolicy": {
|
||||
"nullable": true,
|
||||
"$ref": "#/components/schemas/SpeedPolicy",
|
||||
"description": "This is a risk mitigation parameter for the merchant to configure how they want to fulfill orders depending on the number of block confirmations for the transaction made by the consumer on the selected cryptocurrency"
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/SpeedPolicy"
|
||||
}
|
||||
]
|
||||
},
|
||||
"paymentMethods": {
|
||||
"type": "array",
|
||||
@ -1239,11 +1251,8 @@
|
||||
"description": "`\"V1\"`: The original checkout form \n`\"V2\"`: The new experimental checkout form. \nIf `null` or unspecified, the store's settings will be used.",
|
||||
"nullable": true,
|
||||
"default": null,
|
||||
"x-enumNames": [
|
||||
"V1",
|
||||
"V2"
|
||||
],
|
||||
"enum": [
|
||||
null,
|
||||
"V1",
|
||||
"V2"
|
||||
]
|
||||
@ -1398,8 +1407,7 @@
|
||||
"description": "The fee paid for the payment"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/components/schemas/PaymentStatus",
|
||||
"description": "The status of the payment"
|
||||
"$ref": "#/components/schemas/PaymentStatus"
|
||||
},
|
||||
"destination": {
|
||||
"type": "string",
|
||||
@ -1409,7 +1417,7 @@
|
||||
},
|
||||
"PaymentStatus": {
|
||||
"type": "string",
|
||||
"description": "",
|
||||
"description": "The status of the payment",
|
||||
"x-enumNames": [
|
||||
"Invalid",
|
||||
"Processing",
|
||||
@ -1423,7 +1431,7 @@
|
||||
},
|
||||
"InvoiceType": {
|
||||
"type": "string",
|
||||
"description": "",
|
||||
"description": "The type of the invoice",
|
||||
"x-enumNames": [
|
||||
"Standard",
|
||||
"TopUp"
|
||||
|
@ -65,7 +65,7 @@
|
||||
},
|
||||
"SpeedPolicy": {
|
||||
"type": "string",
|
||||
"description": "`\"HighSpeed\"`: 0 confirmations (1 confirmation if RBF enabled in transaction) \n`\"MediumSpeed\"`: 1 confirmation \n`\"LowMediumSpeed\"`: 2 confirmations \n`\"LowSpeed\"`: 6 confirmations\n",
|
||||
"description": "This is a risk mitigation parameter for the merchant to configure how they want to fulfill orders depending on the number of block confirmations for the transaction made by the consumer on the selected cryptocurrency.\n`\"HighSpeed\"`: 0 confirmations (1 confirmation if RBF enabled in transaction) \n`\"MediumSpeed\"`: 1 confirmation \n`\"LowMediumSpeed\"`: 2 confirmations \n`\"LowSpeed\"`: 6 confirmations\n",
|
||||
"x-enumNames": [
|
||||
"HighSpeed",
|
||||
"MediumSpeed",
|
||||
|
@ -250,16 +250,22 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"onchain": {
|
||||
"type": "object",
|
||||
"description": "On-chain balance of the Lightning node",
|
||||
"nullable": true,
|
||||
"$ref": "#/components/schemas/OnchainBalanceData"
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/OnchainBalanceData"
|
||||
}
|
||||
]
|
||||
},
|
||||
"offchain": {
|
||||
"type": "object",
|
||||
"description": "Off-chain balance of the Lightning node",
|
||||
"nullable": true,
|
||||
"$ref": "#/components/schemas/OffchainBalanceData"
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/OffchainBalanceData"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -301,6 +301,7 @@
|
||||
"schema": { "type": "string" }
|
||||
}
|
||||
],
|
||||
"operationId": "PaymentRequests_Pay",
|
||||
"description": "Create a new invoice for the payment request, or reuse an existing one",
|
||||
"requestBody": {
|
||||
"description": "Invoice creation request",
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user