Compare commits

...

30 Commits

Author SHA1 Message Date
018e4c501d Changelog 1.11.7 (#5394)
* Changelog 1.11.7

* Apply suggestions from code review

---------

Co-authored-by: d11n <mail@dennisreimann.de>
2023-10-13 23:17:17 +09:00
99a0b70cfa Fix form value setter (#5387)
* Fix form value setter

* Fix test parallelization

---------

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2023-10-13 10:08:16 +09:00
314a1352ec Design system updates (#5397) 2023-10-13 09:06:22 +09:00
901e6be21e Fix processing badge color 2023-10-12 14:52:26 +02:00
d58dde950e Fix pay report (#5388)
* Fix pay report

* Make sure we use 11 decimals in reports for lightning payments

---------

Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
2023-10-12 13:51:50 +09:00
8ac18b74df Checkout: Prevent re-rendering of payment details rows (#5392)
Potentially fixes #5390.
2023-10-12 09:35:47 +09:00
2846c38ff5 Invoice: Unify status display and functionality (#5360)
* Invoice: Unify status display and functionality

Consolidates the invoice status display and functionality (mark setted or invalid) across the dashboard, list and details pages.

* Test fix

---------

Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
2023-10-11 23:12:45 +09:00
d44efce225 Simplify code 2023-10-11 21:49:51 +09:00
d3dca7e808 fix lq errors and tests (#5371)
* fix lq errors and tests

* more fixes

* more fixes

* fix

* fix xmr
2023-10-11 21:12:33 +09:00
41e3828eea Reporting: Improve rounding and display (#5363)
* Reporting: Improve rounding and display

* Fix test

* Refactor

---------

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2023-10-11 20:48:40 +09:00
9e76b4d28e Fix swagger (#5380) 2023-10-10 14:15:07 +09:00
ef03497350 Fix build warning (#5355)
Removes unused `string payoutSource` and shortens return with to ternary operator.
2023-10-10 12:30:48 +09:00
e5a2aeb145 Pull Payment: Add QR scanner for destination and infer payment method (#5358)
* Pull Payment: Add QR scanner for destination and infer payment method

Closes #4754.

* Test fix
2023-10-10 12:30:09 +09:00
229a4ea56c Invoice: Improve payment details (#5362)
* Invoice: Improve payment details

Clearer description and display, especially for overpayments. Closes #5207.

* Further refinements

* Test fix
2023-10-10 12:28:00 +09:00
f20e6d3768 Greenfield: allow delete user by email too (#5372) 2023-10-10 12:26:23 +09:00
1d210eb6e3 Crowdfund: Improve no perks case (#5378)
If there are no perks configured, do not display the perks sidebar and contribute custom amount directly, when the main CTA "Contribute" is clicked.

Before it opened a mopdal, where one had to select the only option (custom amount) manually — so this gets rid of the extra step.

Closes #5376.
2023-10-06 22:58:02 +09:00
d8422a979f Fix number of rates (#5365)
* Ripio had api changed
* Exchange rate host now requires an api key so removed
* Removed unused argoneum rate provider code
* switched cop and ugx to yadio
* bumped exchange sharp lib as poloniex api changed and rate source was not working
2023-10-06 16:08:50 +09:00
0cf6d39f02 If shitcoins are removed, dont try to hash its cryptocode for nbx (#5373) 2023-10-06 16:06:17 +09:00
076c20a3b7 attempt to fix different casing in cryptocode of payments 2023-09-29 13:03:18 +02:00
0cfb0ba890 Email Rules: Require either recipients or customer email option (#5357) 2023-09-28 08:36:12 +02:00
44a7e9387e bump 2023-09-27 17:02:49 +09:00
e71954ee34 update lnurl 2023-09-27 09:13:12 +02:00
9cd9e84be6 Fix: After a while, a busy server would send error HTTP 500 (#5354)
This was due to Blazor which attempt to reconnect when the connection
is broken.

Before this, it would try again indefinitely, with this PR, it tries
only for around 3 minutes.

After this, the Blazor circuit should be dead anyway, so it's useless
to try again.
2023-09-27 16:05:57 +09:00
25af9c4227 Improve receipt info display (#5350)
* Improve receipt info display

Displays the info in correct order and adds optional info if tip was given with a percentage.

* Test fix

---------

Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
2023-09-26 22:50:04 +09:00
72a99bf9a6 Recommend Yadio for ARS currency (see #5347) 2023-09-26 22:21:53 +09:00
f1228523cb Try fix flackiness of CanUsePullPaymentsViaUI 2023-09-26 22:20:25 +09:00
a45d368115 Use exchangeratehost as recommended rates for COP 2023-09-26 21:19:42 +09:00
16433dc183 Hide 'Connection established' when connection to server come back (#5352) 2023-09-26 16:40:02 +09:00
0a956fdc73 Remove some useless intermediary type from Rate Source (#5351) 2023-09-26 16:37:40 +09:00
75396f491b Fix: Exchangerate.host falsly appear as Yadio in the UI (Fix #5347) 2023-09-26 14:45:46 +09:00
109 changed files with 1318 additions and 1061 deletions

View File

@ -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>();
}
}
}
}

View File

@ -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
}

View File

@ -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,

View File

@ -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);

View File

@ -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())
};
}
}

View File

@ -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>

View File

@ -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)
{

View File

@ -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)

View File

@ -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

View File

@ -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();
}
}

View File

@ -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))

View File

@ -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);

View File

@ -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)
{

View File

@ -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(() =>
{

View File

@ -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>());
}

View File

@ -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();

View File

@ -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)

View File

@ -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" });

View File

@ -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"

View File

@ -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"

View File

@ -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" />

View 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>

View 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);
}
}
}

View File

@ -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; }
}
}

View File

@ -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>

View File

@ -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; }
}

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}

View File

@ -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())

View File

@ -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);

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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; }

View File

@ -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);
}

View File

@ -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);
}
}
}

View File

@ -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>();

View File

@ -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
}

View File

@ -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)

View File

@ -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"}
};

View File

@ -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)
{

View File

@ -31,4 +31,9 @@ public class FieldValueMirror : IFormComponentProvider
return rawValue;
}
public void SetValue(Field field, JToken value)
{
//ignored
}
}

View File

@ -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);
}
}
}
}
}
}

View File

@ -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)
{
}

View File

@ -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()

View File

@ -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")));

View File

@ -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; }
}
}

View File

@ -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; }
}

View File

@ -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)]

View File

@ -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; }

View File

@ -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));
}

View File

@ -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)

View File

@ -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)

View File

@ -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();
}
}

View File

@ -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)",
_ => ""
};
}
}

View File

@ -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)
{

View File

@ -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; }

View 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);
}
}
}

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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" },

View File

@ -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 =
{

View File

@ -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>

View File

@ -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" />

View File

@ -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>
}

View File

@ -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>
}
}

View File

@ -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>
}

View File

@ -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>

View File

@ -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>

View File

@ -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>
}

View File

@ -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">

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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();

View File

@ -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">

View File

@ -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";
}

View File

@ -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>
&nbsp;
<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>
&nbsp;
<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>

View File

@ -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>

View File

@ -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>

View File

@ -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()" />

View File

@ -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"

View File

@ -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>

View File

@ -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" />

View File

@ -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

View File

@ -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,

View File

@ -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]]);
}
}

View File

@ -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 {

View File

@ -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 */

View File

@ -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
});

View File

@ -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)

View File

@ -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
},

View File

@ -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"

View File

@ -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",

View File

@ -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"
}
]
}
}
},

View File

@ -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