Compare commits

...

70 Commits

Author SHA1 Message Date
d7644c0fe7 Adding ability to print vouchers for Pull Payments if it's supported 2023-11-26 22:26:24 -06:00
bf9e2f47a3 Destination and Pay Proof use space on receipt more efficiently
Allowing more space for the break, that is useful especially on Lightning invoices (Destination)
2023-11-26 22:26:11 -06:00
a32257e5a2 Scroll NFC error into view 2023-11-26 22:06:15 -06:00
f56ea60317 Save a bit of space 2023-11-26 22:05:50 -06:00
5bb7f158db Update NFC result handling and display 2023-11-26 22:05:43 -06:00
4f49b5f1a0 Move NFC code on Vue app level 2023-11-26 22:03:15 -06:00
25c30512ec Receipt fixes and improvements (#5505)
* Fix additional div

* Don't show payment number if there is only one

* Bump max-width to prevent wrapping in top container

* Fix colspan

* Re-add POS data

Closes #5498.

* Right-align amounts

* Re-order

* Don't show redundant receive date if there is only one payment

* Table improvements

* Unify crypto amount display

* More formatting improvements

* Only show Subtotal if there are calculations applicable to it

* Making margin on the bottom smaller to reduce expansion on Bitcoinize machines

---------

Co-authored-by: rockstardev <5191402+rockstardev@users.noreply.github.com>
2023-11-23 12:58:57 -06:00
52df7a5b89 Print button 2023-11-21 08:39:31 -06:00
1a85da27db Cleanup receipt print template 2023-11-21 08:39:25 -06:00
10326a822e Optimizing receipt printing, now works on POS terminal 2023-11-21 08:39:16 -06:00
d1bf47a5c0 Bumping LND to 0.17.2-beta 2023-11-20 13:28:20 -08:00
ccf9cfa332 Minor cleanups (#5460) 2023-11-20 11:18:19 +01:00
773f8a9aea Apps: Filter list lookups by available app types (#5482)
* Apps: Filter list lookups by available app types

Uniunstalling a plugin might lead to then unavailable app types, as the entries remain in the database. The list lookups need to account for that, otherwise unavailable apps cause crashes and misbehaviour.

Fixes #5480.

* Make a hashset

---------

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2023-11-20 10:48:56 +09:00
dd62e166a1 BIP329: Use application/jsonl as MIME type (#5489)
There's an [ongoing discussion](https://github.com/wardi/jsonlines/issues/19) about what the MIME type for [JSONL](https://jsonlines.org/) files should be. Making it `application/jsonl` leads to the file being downloaded according to my testing, which prevents browsers from opening them in a new window and parsing them as JSON, which fixes #5488.
2023-11-20 10:46:36 +09:00
2fb72d5aa6 Payment Request: Improve public view (#5413)
* Payment Request: Improve public view

Closes #4450.

* Test fix

* Extract Vue utils

* Improve payment history

* Fix amount display

* Unify receipt and payment request tables

* Re-add text confirmation for copying to clipboard

* Minor print optimizations

* Wording: Rename Description to Memo

* Open view links in new window

* View updates
2023-11-20 10:45:43 +09:00
46f0818765 Bumping LND to 0.17.1-beta 2023-11-14 19:45:41 -08:00
96569ae4aa POS Cart: Add options for search and categories display (#5438) 2023-11-13 13:59:14 +01:00
f2b1e5f93e fix report crash when some values are null 2023-11-10 12:30:12 +01:00
2326894a2b Responsive editor improvements (#5449) 2023-11-09 10:27:33 +01:00
c15f02ddbf Reporting: UI improvements (#5432) 2023-11-09 10:26:00 +01:00
7708084331 Pull payment improvements (#5453) 2023-11-09 10:17:52 +01:00
696a414e95 POS Keypad: Add plus and change clear functionality (#5396)
Closes #5299.
2023-11-02 20:03:34 +01:00
c16dfb2dcb POS and Crowdfund: Improve item editor (#5418)
* POS and Crowdfund: Improve item editor

Makes it work the same way as the form editor: Drag and drop for reordering and inline editing without modal.

* Upload component
2023-11-02 19:58:03 +01:00
c979c4774c POS Cart: Horizontal scrollable filters (#5391) 2023-11-02 08:36:27 +01:00
e82281d273 switch pos to metadata in invoice create view (#5412)
Co-authored-by: d11n <mail@dennisreimann.de>
2023-11-02 08:13:48 +01:00
27c22d5e33 Unify list views (#5399) 2023-11-02 08:12:28 +01:00
6acc545b66 Greenfield: LNURLPay store payment method fixes (#5446) 2023-11-02 08:11:32 +01:00
609ec0989f Do not activate Blazor in Wizard screens (#5435) 2023-10-27 10:16:36 +02:00
b702621a04 Simplify vault logic by introducing a VaultClient (#5434) 2023-10-27 11:54:15 +09:00
89041a6744 Wallet Send: Fill label from BIP21 (#5428)
Fixes #5426.
2023-10-27 09:59:12 +09:00
c485c109e6 Bumping LND to 0.17.0-beta (#5429) 2023-10-26 10:47:03 +02:00
29a49d5f71 Fix: In pull payment page, the amount of claims wasn't displayed (#5427) 2023-10-25 13:51:27 +02:00
a5fafc4864 Update Passport tooltips (#5423) 2023-10-24 13:23:10 +02:00
a921504bcf Bump HtmlSanitizer 2023-10-18 19:33:43 +09:00
027154a4d3 Update changelog 2023-10-18 19:27:20 +09:00
bf1a1368ff Forms: Make zip code a required field in predefined address form (#5405)
Closes #5401.
2023-10-18 19:21:56 +09:00
097ffbf8a3 Greenfield: Add missing checkout (V2) settings (#5406)
* Greenfield: Add missing checkout (V2) settings

Closes #5403.

* Fix swagger
2023-10-18 19:20:05 +09:00
ec076d1560 Fix: BTCPayServer.HostedServices.BitpayIPNSender fail to send notifications on some locale (Fix #5361) 2023-10-18 19:07:30 +09:00
8dadfa2111 Reporting fixes (#5410) 2023-10-18 10:09:03 +02:00
c8ee6ead0b Adjust swagger doc to latest change in Greenfield API 2023-10-18 05:31:00 +02:00
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
233 changed files with 4787 additions and 3374 deletions

View File

@ -31,7 +31,7 @@
<None Include="icon.png" Pack="true" PackagePath="\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="HtmlSanitizer" Version="5.0.372" />
<PackageReference Include="HtmlSanitizer" Version="8.0.723" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.9" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.7" />

View File

@ -20,6 +20,15 @@ namespace BTCPayServer.Abstractions.Extensions
Relative
}
public static void SetBlazorAllowed(this ViewDataDictionary viewData, bool allowed)
{
viewData["BlazorAllowed"] = allowed;
}
public static bool IsBlazorAllowed(this ViewDataDictionary viewData)
{
return viewData["BlazorAllowed"] is not false;
}
public static void SetActivePage<T>(this ViewDataDictionary viewData, T activePage, string title = null, string activeId = null)
where T : IConvertible
{

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,5 +1,5 @@
using System.Web;
using Ganss.XSS;
using Ganss.Xss;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;

View File

@ -28,6 +28,8 @@ namespace BTCPayServer.Client.Models
public PosViewType DefaultView { get; set; }
public bool ShowCustomAmount { get; set; } = false;
public bool ShowDiscount { get; set; } = true;
public bool ShowSearch { get; set; } = true;
public bool ShowCategories { get; set; } = true;
public bool EnableTips { get; set; } = true;
public string CustomAmountPayButtonText { get; set; } = null;
public string FixedAmountPayButtonText { get; set; } = null;

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

@ -1,8 +1,12 @@
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class LNURLPayPaymentMethodBaseData
{
public bool UseBech32Scheme { get; set; }
[JsonProperty("lud12Enabled")]
public bool LUD12Enabled { get; set; }
public LNURLPayPaymentMethodBaseData()

View File

@ -16,11 +16,12 @@ namespace BTCPayServer.Client.Models
{
}
public LNURLPayPaymentMethodData(string cryptoCode, bool enabled, bool useBech32Scheme)
public LNURLPayPaymentMethodData(string cryptoCode, bool enabled, bool useBech32Scheme, bool lud12Enabled)
{
Enabled = enabled;
CryptoCode = cryptoCode;
UseBech32Scheme = useBech32Scheme;
LUD12Enabled = lud12Enabled;
}
}
}

View File

@ -21,6 +21,8 @@ namespace BTCPayServer.Client.Models
public string DefaultView { get; set; }
public bool ShowCustomAmount { get; set; }
public bool ShowDiscount { get; set; }
public bool ShowSearch { get; set; }
public bool ShowCategories { get; set; }
public bool EnableTips { get; set; }
public string Currency { get; set; }
public object Items { get; set; }

View File

@ -73,6 +73,17 @@ namespace BTCPayServer.Client.Models
public bool PayJoinEnabled { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public bool? AutoDetectLanguage { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public bool? ShowPayInWalletButton { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public bool? ShowStoreHeader { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public bool? CelebratePayment { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public bool? PlaySoundOnPayment { get; set; }
public InvoiceData.ReceiptOptions Receipt { get; set; }

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

@ -23,7 +23,7 @@
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14" />
<PackageReference Include="Selenium.Support" Version="4.1.1" />
<PackageReference Include="Selenium.WebDriver" Version="4.1.1" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="116.0.5845.9600" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="119.0.6045.10500" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets>

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

@ -1457,7 +1457,7 @@ namespace BTCPayServer.Tests
[Trait("Integration", "Integration")]
public async Task CanUseWebhooks()
{
void AssertHook(FakeServer fakeServer, Client.Models.StoreWebhookData hook)
void AssertHook(FakeServer fakeServer, StoreWebhookData hook)
{
Assert.True(hook.Enabled);
Assert.True(hook.AuthorizedEvents.Everything);

View File

@ -92,9 +92,8 @@ namespace BTCPayServer.Tests
Assert.Contains("App updated", s.FindAlertMessage().Text);
s.Driver.FindElement(By.Id("ViewApp")).Click();
var windows = s.Driver.WindowHandles;
Assert.Equal(2, windows.Count);
s.Driver.SwitchTo().Window(windows[1]);
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
s.Driver.FindElement(By.CssSelector("button[type='submit']")).Click();
Assert.Contains("Enter your email", s.Driver.PageSource);
@ -103,6 +102,9 @@ namespace BTCPayServer.Tests
s.PayInvoice(true);
var invoiceId = s.Driver.Url[(s.Driver.Url.LastIndexOf("/", StringComparison.Ordinal) + 1)..];
s.Driver.Close();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
s.GoToInvoice(invoiceId);
Assert.Contains("aa@aa.com", s.Driver.PageSource);
@ -116,13 +118,19 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.XPath("//a[starts-with(@id, 'Edit-')]")).Click();
var editUrl = s.Driver.Url;
s.Driver.FindElement(By.Id("ViewPaymentRequest")).Click();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
s.Driver.FindElement(By.CssSelector("[data-test='form-button']")).Click();
Assert.Contains("Enter your email", s.Driver.PageSource);
s.Driver.FindElement(By.Name("buyerEmail")).SendKeys("aa@aa.com");
s.Driver.FindElement(By.CssSelector("input[type='submit']")).Click();
invoiceId = s.Driver.Url.Split('/').Last();
s.Driver.Close();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
s.Driver.Navigate().GoToUrl(editUrl);
Assert.Contains("aa@aa.com", s.Driver.PageSource);
var invoice = await s.Server.PayTester.GetService<InvoiceRepository>().GetInvoice(invoiceId);
@ -196,7 +204,6 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("StoreNav-PaymentRequests")).Click();
s.Driver.FindElement(By.Id("CreatePaymentRequest")).Click();
Assert.Equal(4, new SelectElement(s.Driver.FindElement(By.Id("FormId"))).Options.Count);
}
[Fact(Timeout = TestTimeout)]
@ -216,8 +223,7 @@ namespace BTCPayServer.Tests
s.GoToInvoices(s.StoreId);
}
// Let's CPFP from the invoices page
s.Driver.SetCheckbox(By.Id("selectAllCheckbox"), true);
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
s.Driver.SetCheckbox(By.CssSelector(".mass-action-select-all"), true);
s.Driver.FindElement(By.Id("BumpFee")).Click();
s.Driver.FindElement(By.Id("BroadcastTransaction")).Click();
s.FindAlertMessage();
@ -225,16 +231,14 @@ namespace BTCPayServer.Tests
// CPFP again should fail because all invoices got bumped
s.GoToInvoices();
s.Driver.SetCheckbox(By.Id("selectAllCheckbox"), true);
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
s.Driver.SetCheckbox(By.CssSelector(".mass-action-select-all"), true);
s.Driver.FindElement(By.Id("BumpFee")).Click();
Assert.Contains($"/stores/{s.StoreId}/invoices", s.Driver.Url);
Assert.Contains("any UTXO available", s.FindAlertMessage(StatusMessageModel.StatusSeverity.Error).Text);
// But we should be able to bump from the wallet's page
s.GoToWallet(navPages: WalletsNavPages.Transactions);
s.Driver.SetCheckbox(By.Id("selectAllCheckbox"), true);
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
s.Driver.SetCheckbox(By.CssSelector(".mass-action-select-all"), true);
s.Driver.FindElement(By.Id("BumpFee")).Click();
s.Driver.FindElement(By.Id("BroadcastTransaction")).Click();
Assert.Contains($"/wallets/{s.WalletId}", s.Driver.Url);
@ -556,24 +560,24 @@ namespace BTCPayServer.Tests
s.AddDerivationScheme();
s.GoToInvoices();
s.CreateInvoice();
s.Driver.FindElement(By.Id("markStatusDropdownMenuButton")).Click();
s.Driver.FindElements(By.ClassName("changeInvoiceState"))[0].Click();
s.Driver.FindElement(By.CssSelector("[data-invoice-state-badge] .dropdown-toggle")).Click();
s.Driver.FindElements(By.CssSelector("[data-invoice-state-badge] .dropdown-menu button"))[0].Click();
TestUtils.Eventually(() => Assert.Contains("Invalid (marked)", s.Driver.PageSource));
s.Driver.Navigate().Refresh();
s.Driver.FindElement(By.Id("markStatusDropdownMenuButton")).Click();
s.Driver.FindElements(By.ClassName("changeInvoiceState"))[0].Click();
s.Driver.FindElement(By.CssSelector("[data-invoice-state-badge] .dropdown-toggle")).Click();
s.Driver.FindElements(By.CssSelector("[data-invoice-state-badge] .dropdown-menu button"))[0].Click();
TestUtils.Eventually(() => Assert.Contains("Settled (marked)", s.Driver.PageSource));
s.Driver.Navigate().Refresh();
s.Driver.FindElement(By.Id("markStatusDropdownMenuButton")).Click();
s.Driver.FindElements(By.ClassName("changeInvoiceState"))[0].Click();
s.Driver.FindElement(By.CssSelector("[data-invoice-state-badge] .dropdown-toggle")).Click();
s.Driver.FindElements(By.CssSelector("[data-invoice-state-badge] .dropdown-menu button"))[0].Click();
TestUtils.Eventually(() => Assert.Contains("Invalid (marked)", s.Driver.PageSource));
s.Driver.Navigate().Refresh();
s.Driver.FindElement(By.Id("markStatusDropdownMenuButton")).Click();
s.Driver.FindElements(By.ClassName("changeInvoiceState"))[0].Click();
s.Driver.FindElement(By.CssSelector("[data-invoice-state-badge] .dropdown-toggle")).Click();
s.Driver.FindElements(By.CssSelector("[data-invoice-state-badge] .dropdown-menu button"))[0].Click();
TestUtils.Eventually(() => Assert.Contains("Settled (marked)", s.Driver.PageSource));
}
@ -730,9 +734,8 @@ namespace BTCPayServer.Tests
Assert.Contains(invoiceId, s.Driver.PageSource);
// archive via list
s.Driver.FindElement(By.CssSelector($".selector[value=\"{invoiceId}\"]")).Click();
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
s.Driver.FindElement(By.Id("ActionsDropdownArchive")).Click();
s.Driver.FindElement(By.CssSelector($".mass-action-select[value=\"{invoiceId}\"]")).Click();
s.Driver.FindElement(By.Id("ArchiveSelected")).Click();
Assert.Contains("1 invoice archived", s.FindAlertMessage().Text);
Assert.DoesNotContain(invoiceId, s.Driver.PageSource);
@ -740,9 +743,8 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("StatusOptionsToggle")).Click();
s.Driver.FindElement(By.Id("StatusOptionsIncludeArchived")).Click();
Assert.Contains(invoiceId, s.Driver.PageSource);
s.Driver.FindElement(By.CssSelector($".selector[value=\"{invoiceId}\"]")).Click();
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
s.Driver.FindElement(By.Id("ActionsDropdownUnarchive")).Click();
s.Driver.FindElement(By.CssSelector($".mass-action-select[value=\"{invoiceId}\"]")).Click();
s.Driver.FindElement(By.Id("UnarchiveSelected")).Click();
Assert.Contains("1 invoice unarchived", s.FindAlertMessage().Text);
Assert.Contains(invoiceId, s.Driver.PageSource);
@ -989,13 +991,12 @@ namespace BTCPayServer.Tests
Assert.Contains("App successfully created", s.FindAlertMessage().Text);
s.Driver.FindElement(By.CssSelector("label[for='DefaultView_Cart']")).Click();
s.Driver.FindElement(By.CssSelector(".template-item:nth-of-type(1) .btn-primary")).Click();
s.Driver.FindElement(By.CssSelector(".template-item:nth-of-type(1)")).Click();
s.Driver.FindElement(By.Id("BuyButtonText")).SendKeys("Take my money");
s.Driver.FindElement(By.Id("EditorCategories-ts-control")).SendKeys("Drinks");
s.Driver.FindElement(By.Id("SaveItemChanges")).Click();
s.Driver.FindElement(By.Id("ToggleRawEditor")).Click();
s.Driver.FindElement(By.Id("ApplyItemChanges")).Click();
var template = s.Driver.FindElement(By.Id("Template")).GetAttribute("value");
var template = s.Driver.FindElement(By.Id("TemplateConfig")).GetAttribute("value");
Assert.Contains("\"buyButtonText\": \"Take my money\"", template);
Assert.Matches("\"categories\": \\[\n\\s+\"Drinks\"\n\\s+\\]", template);
@ -1107,7 +1108,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("Title")).SendKeys("Kukkstarter");
s.Driver.FindElement(By.CssSelector("div.note-editable.card-block")).SendKeys("1BTC = 1BTC");
s.Driver.FindElement(By.Id("TargetCurrency")).Clear();
s.Driver.FindElement(By.Id("TargetCurrency")).SendKeys("JPY");
s.Driver.FindElement(By.Id("TargetCurrency")).SendKeys("EUR");
s.Driver.FindElement(By.Id("TargetAmount")).SendKeys("700");
// test wrong dates
@ -1122,7 +1123,8 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("SaveSettings")).Click();
Assert.Contains("App updated", s.FindAlertMessage().Text);
var appId = s.Driver.Url.Split('/')[4];
// CHeck public page
s.Driver.FindElement(By.Id("ViewApp")).Click();
var windows = s.Driver.WindowHandles;
Assert.Equal(2, windows.Count);
@ -1132,6 +1134,26 @@ namespace BTCPayServer.Tests
Assert.Equal("Currently active!",
s.Driver.FindElement(By.CssSelector("[data-test='time-state']")).Text);
// Contribute
s.Driver.FindElement(By.Id("crowdfund-body-header-cta")).Click();
Thread.Sleep(1000);
s.Driver.WaitUntilAvailable(By.Name("btcpay"));
var frameElement = s.Driver.FindElement(By.Name("btcpay"));
Assert.True(frameElement.Displayed);
var iframe = s.Driver.SwitchTo().Frame(frameElement);
iframe.WaitUntilAvailable(By.Id("Checkout-v2"));
IWebElement closebutton = null;
TestUtils.Eventually(() =>
{
closebutton = iframe.FindElement(By.Id("close"));
Assert.True(closebutton.Displayed);
});
closebutton.Click();
s.Driver.AssertElementNotFound(By.Name("btcpay"));
// Back to admin view
s.Driver.Close();
s.Driver.SwitchTo().Window(windows[0]);
@ -1179,13 +1201,13 @@ namespace BTCPayServer.Tests
var editUrl = s.Driver.Url;
s.Driver.FindElement(By.Id("ViewPaymentRequest")).Click();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
var viewUrl = s.Driver.Url;
Assert.Equal("Amount due", s.Driver.FindElement(By.CssSelector("[data-test='amount-due-title']")).Text);
Assert.Equal("Pay Invoice", s.Driver.FindElement(By.CssSelector("[data-test='pay-button']")).Text.Trim());
Assert.Equal("Pay Invoice", s.Driver.FindElement(By.Id("PayInvoice")).Text.Trim());
s.Driver.Close();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
// expire
s.GoToUrl(editUrl);
s.Driver.ExecuteJavaScript("document.getElementById('ExpiryDate').value = '2021-01-21T21:00:00.000Z'");
s.Driver.FindElement(By.Id("SaveButton")).Click();
s.Driver.FindElement(By.XPath("//a[starts-with(@id, 'Edit-')]")).Click();
@ -1205,12 +1227,28 @@ namespace BTCPayServer.Tests
Assert.True(s.Driver.FindElement(By.Id("Currency")).Enabled);
s.GoToUrl(viewUrl);
s.Driver.AssertElementNotFound(By.CssSelector("[data-test='status']"));
Assert.Equal("Pay Invoice", s.Driver.FindElement(By.CssSelector("[data-test='pay-button']")).Text.Trim());
Assert.Equal("Pay Invoice", s.Driver.FindElement(By.Id("PayInvoice")).Text.Trim());
// test invoice creation, click with JS, because the button is inside a sticky header
s.Driver.ExecuteJavaScript("document.querySelector('[data-test=\"pay-button\"]').click()");
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
// test invoice creation
s.Driver.FindElement(By.Id("PayInvoice")).Click();
TestUtils.Eventually(() =>
{
s.Driver.WaitUntilAvailable(By.Name("btcpay"));
var frameElement = s.Driver.FindElement(By.Name("btcpay"));
Assert.True(frameElement.Displayed);
var iframe = s.Driver.SwitchTo().Frame(frameElement);
iframe.WaitUntilAvailable(By.Id("Checkout-v2"));
IWebElement closebutton = null;
TestUtils.Eventually(() =>
{
closebutton = iframe.FindElement(By.Id("close"));
Assert.True(closebutton.Displayed);
});
closebutton.Click();
s.Driver.AssertElementNotFound(By.Name("btcpay"));
});
// amount and currency should not be editable, because invoice exists
s.GoToUrl(editUrl);
@ -1234,32 +1272,45 @@ namespace BTCPayServer.Tests
// payment
s.GoToUrl(viewUrl);
s.Driver.ExecuteJavaScript("document.querySelector('[data-test=\"pay-button\"]').click()");
// Pay full amount
s.PayInvoice();
// Processing
s.Driver.FindElement(By.Id("PayInvoice")).Click();
TestUtils.Eventually(() =>
{
var processingSection = s.Driver.WaitForElement(By.Id("processing"));
Assert.True(processingSection.Displayed);
Assert.Contains("Payment Received", processingSection.Text);
Assert.Contains("Your payment has been received and is now processing", processingSection.Text);
});
s.Driver.WaitUntilAvailable(By.Name("btcpay"));
var frameElement = s.Driver.FindElement(By.Name("btcpay"));
Assert.True(frameElement.Displayed);
var iframe = s.Driver.SwitchTo().Frame(frameElement);
iframe.WaitUntilAvailable(By.Id("Checkout-v2"));
// Pay full amount
s.PayInvoice();
// Processing
TestUtils.Eventually(() =>
{
var processingSection = s.Driver.WaitForElement(By.Id("processing"));
Assert.True(processingSection.Displayed);
Assert.Contains("Payment Received", processingSection.Text);
Assert.Contains("Your payment has been received and is now processing", processingSection.Text);
});
s.GoToUrl(viewUrl);
Assert.Equal("Processing", s.Driver.WaitForElement(By.CssSelector("[data-test='status']")).Text);
s.Driver.Navigate().Back();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles[0]);
Assert.Equal("Processing", s.Driver.WaitForElement(By.CssSelector("[data-test='status']")).Text);
s.Driver.SwitchTo().Frame(frameElement);
// Mine
s.MineBlockOnInvoiceCheckout();
TestUtils.Eventually(() =>
{
Assert.Contains("Mined 1 block",
s.Driver.WaitForElement(By.Id("CheatSuccessMessage")).Text);
// Mine
s.MineBlockOnInvoiceCheckout();
TestUtils.Eventually(() =>
{
Assert.Contains("Mined 1 block",
s.Driver.WaitForElement(By.Id("CheatSuccessMessage")).Text);
});
s.Driver.FindElement(By.Id("close")).Click();
s.Driver.AssertElementNotFound(By.Name("btcpay"));
});
s.GoToUrl(viewUrl);
s.Driver.SwitchTo().Window(s.Driver.WindowHandles[0]);
Assert.Equal("Settled", s.Driver.WaitForElement(By.CssSelector("[data-test='status']")).Text);
}
@ -1356,7 +1407,7 @@ namespace BTCPayServer.Tests
TestLogs.LogInformation("Let's try to update one of them");
s.Driver.FindElement(By.LinkText("Modify")).Click();
using FakeServer server = new FakeServer();
using var server = new FakeServer();
await server.Start();
s.Driver.FindElement(By.Name("PayloadUrl")).Clear();
s.Driver.FindElement(By.Name("PayloadUrl")).SendKeys(server.ServerUri.AbsoluteUri);
@ -1398,7 +1449,7 @@ namespace BTCPayServer.Tests
server.Done();
TestLogs.LogInformation("Let's make a failed event");
s.CreateInvoice();
var invoiceId = s.CreateInvoice();
request = await server.GetNextRequest();
request.Response.StatusCode = 404;
server.Done();
@ -1423,7 +1474,7 @@ namespace BTCPayServer.Tests
CanBrowseContent(s);
s.GoToInvoices();
s.Driver.FindElement(By.LinkText("Details")).Click();
s.Driver.FindElement(By.LinkText(invoiceId)).Click();
CanBrowseContent(s);
var element = s.Driver.FindElement(By.ClassName("redeliver"));
element.Click();
@ -1676,13 +1727,14 @@ namespace BTCPayServer.Tests
// no previous page in the wizard, hence no back button
Assert.True(s.Driver.ElementDoesNotExist(By.Id("GoBack")));
s.Driver.FindElement(By.Id("CancelWizard")).Click();
Assert.Equal(settingsUri.ToString(), s.Driver.Url);
// Transactions list contains export and action, ensure functions are present.
Assert.Equal(settingsUri.ToString(), s.Driver.Url);
// Transactions list contains export, ensure functions are present.
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
s.Driver.FindElement(By.ClassName("mass-action-select-all")).Click();
s.Driver.FindElement(By.Id("BumpFee"));
// JSON export
s.Driver.FindElement(By.Id("ExportDropdownToggle")).Click();
s.Driver.FindElement(By.Id("ExportJSON")).Click();
@ -1693,20 +1745,15 @@ namespace BTCPayServer.Tests
Assert.Contains("\"Amount\": \"3.00000000\"", s.Driver.PageSource);
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
// BIP-329 export
s.Driver.FindElement(By.Id("ExportDropdownToggle")).Click();
s.Driver.FindElement(By.Id("ExportBIP329")).Click();
Thread.Sleep(1000);
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
Assert.Contains(s.WalletId.ToString(), s.Driver.Url);
Assert.EndsWith("export?format=bip329", s.Driver.Url);
Assert.Contains("{\"type\":\"tx\",\"ref\":\"", s.Driver.PageSource);
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
// CSV export
s.Driver.FindElement(By.Id("ExportDropdownToggle")).Click();
s.Driver.FindElement(By.Id("ExportCSV")).Click();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
// BIP-329 export
s.Driver.FindElement(By.Id("ExportDropdownToggle")).Click();
s.Driver.FindElement(By.Id("ExportBIP329")).Click();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
}
[Fact(Timeout = TestTimeout)]
@ -1754,7 +1801,12 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("Amount")).Clear();
s.Driver.FindElement(By.Id("Amount")).SendKeys("99.0");
s.Driver.FindElement(By.Id("Create")).Click();
s.Driver.FindElement(By.LinkText("View")).Click();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
Assert.Contains("PP1", s.Driver.PageSource);
s.Driver.Close();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
@ -1765,9 +1817,9 @@ namespace BTCPayServer.Tests
var description = s.Driver.FindElement(By.ClassName("card-block"));
description.SendKeys("Description Edit");
s.Driver.FindElement(By.Id("SaveButton")).Click();
s.Driver.FindElement(By.LinkText("PP1 Edited")).Click();
s.Driver.FindElement(By.LinkText("View")).Click();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
Assert.Contains("Description Edit", s.Driver.PageSource);
Assert.Contains("PP1 Edited", s.Driver.PageSource);
}
@ -1793,7 +1845,12 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("Amount")).Clear();
s.Driver.FindElement(By.Id("Amount")).SendKeys("99.0");
s.Driver.FindElement(By.Id("Create")).Click();
s.Driver.FindElement(By.LinkText("View")).Click();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
Assert.Contains("PP1", s.Driver.PageSource);
s.Driver.Close();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
@ -1805,6 +1862,7 @@ namespace BTCPayServer.Tests
// This should select the first View, ie, the last one PP2
s.Driver.FindElement(By.LinkText("View")).Click();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
var address = await s.Server.ExplorerNode.GetNewAddressAsync();
s.Driver.FindElement(By.Id("Destination")).SendKeys(address.ToString());
s.Driver.FindElement(By.Id("ClaimedAmount")).Clear();
@ -1826,6 +1884,9 @@ namespace BTCPayServer.Tests
Assert.Contains("Awaiting Approval", s.Driver.PageSource);
var viewPullPaymentUrl = s.Driver.Url;
s.Driver.Close();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
// This one should have nothing
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
var payouts = s.Driver.FindElements(By.ClassName("pp-payout"));
@ -1838,8 +1899,7 @@ namespace BTCPayServer.Tests
payouts[0].Click();
Assert.NotEmpty(s.Driver.FindElements(By.ClassName("payout")));
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-selectAllCheckbox")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-actions")).Click();
s.Driver.FindElement(By.ClassName("mass-action-select-all")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-approve-pay")).Click();
s.Driver.FindElement(By.Id("SignTransaction")).Click();
@ -1892,8 +1952,6 @@ namespace BTCPayServer.Tests
});
s.GoToHome();
//offline/external payout test
s.Driver.FindElement(By.Id("NotificationsHandle")).Click();
s.Driver.FindElement(By.Id("NotificationsMarkAllAsSeen")).Click();
var newStore = s.CreateNewStore();
s.GenerateWallet("BTC", "", true, true);
@ -1906,8 +1964,10 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("Currency")).Clear();
s.Driver.FindElement(By.Id("Currency")).SendKeys("BTC");
s.Driver.FindElement(By.Id("Create")).Click();
s.Driver.FindElement(By.LinkText("View")).Click();
s.Driver.FindElement(By.LinkText("View")).Click();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
address = await s.Server.ExplorerNode.GetNewAddressAsync();
s.Driver.FindElement(By.Id("Destination")).SendKeys(address.ToString());
s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys(Keys.Enter);
@ -1916,18 +1976,18 @@ namespace BTCPayServer.Tests
Assert.Contains(PayoutState.AwaitingApproval.GetStateString(), s.Driver.PageSource);
s.GoToStore(s.StoreId, StoreNavPages.Payouts);
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-view")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-selectAllCheckbox")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-actions")).Click();
s.Driver.FindElement(By.ClassName("mass-action-select-all")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-approve")).Click();
s.FindAlertMessage();
var tx = await s.Server.ExplorerNode.SendToAddressAsync(address, Money.FromUnit(0.001m, MoneyUnit.BTC));
s.Driver.Close();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
s.GoToStore(s.StoreId, StoreNavPages.Payouts);
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-view")).Click();
Assert.Contains(PayoutState.AwaitingPayment.GetStateString(), s.Driver.PageSource);
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-selectAllCheckbox")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-actions")).Click();
s.Driver.FindElement(By.ClassName("mass-action-select-all")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-mark-paid")).Click();
s.FindAlertMessage();
@ -1960,17 +2020,16 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("Currency")).SendKeys("BTC");
s.Driver.FindElement(By.Id("Create")).Click();
s.Driver.FindElement(By.LinkText("View")).Click();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
// Bitcoin-only, SelectedPaymentMethod should not be displayed
s.Driver.ElementDoesNotExist(By.Id("SelectedPaymentMethod"));
var bolt = (await s.Server.CustomerLightningD.CreateInvoice(
payoutAmount,
$"LN payout test {DateTime.UtcNow.Ticks}",
TimeSpan.FromHours(1), CancellationToken.None)).BOLT11;
s.Driver.FindElement(By.Id("Destination")).SendKeys(bolt);
s.Driver.FindElement(By.Id("SelectedPaymentMethod")).Click();
s.Driver.FindElement(By.CssSelector(
$"#SelectedPaymentMethod option[value={new PaymentMethodId("BTC", PaymentTypes.LightningLike)}]"))
.Click();
s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys(Keys.Enter);
//we do not allow short-life bolts.
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Error);
@ -1981,27 +2040,23 @@ namespace BTCPayServer.Tests
TimeSpan.FromDays(31), CancellationToken.None)).BOLT11;
s.Driver.FindElement(By.Id("Destination")).Clear();
s.Driver.FindElement(By.Id("Destination")).SendKeys(bolt);
s.Driver.FindElement(By.Id("SelectedPaymentMethod")).Click();
s.Driver.FindElement(By.CssSelector(
$"#SelectedPaymentMethod option[value={new PaymentMethodId("BTC", PaymentTypes.LightningLike)}]"))
.Click();
s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys(Keys.Enter);
s.FindAlertMessage();
Assert.Contains(PayoutState.AwaitingApproval.GetStateString(), s.Driver.PageSource);
s.Driver.Close();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
s.GoToStore(newStore.storeId, StoreNavPages.Payouts);
s.Driver.FindElement(By.Id($"{new PaymentMethodId("BTC", PaymentTypes.LightningLike)}-view")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-view")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-selectAllCheckbox")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-actions")).Click();
s.Driver.FindElement(By.ClassName("mass-action-select-all")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-approve-pay")).Click();
Assert.Contains(bolt, s.Driver.PageSource);
Assert.Contains($"{payoutAmount.ToString()} BTC", s.Driver.PageSource);
Assert.Contains($"{payoutAmount} BTC", s.Driver.PageSource);
s.Driver.FindElement(By.CssSelector("#pay-invoices-form")).Submit();
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
s.FindAlertMessage();
s.GoToStore(newStore.storeId, StoreNavPages.Payouts);
s.Driver.FindElement(By.Id($"{new PaymentMethodId("BTC", PaymentTypes.LightningLike)}-view")).Click();
@ -2011,8 +2066,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-view")).Click();
Assert.Contains(bolt, s.Driver.PageSource);
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-selectAllCheckbox")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-actions")).Click();
s.Driver.FindElement(By.ClassName("mass-action-select-all")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-mark-paid")).Click();
s.Driver.FindElement(By.Id($"{new PaymentMethodId("BTC", PaymentTypes.LightningLike)}-view")).Click();
@ -2027,16 +2081,21 @@ namespace BTCPayServer.Tests
s.Driver.SetCheckbox(By.Id("AutoApproveClaims"), true);
s.Driver.FindElement(By.Id("Amount")).Clear();
s.Driver.FindElement(By.Id("Amount")).SendKeys("99.0" + Keys.Enter);
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
s.FindAlertMessage();
s.Driver.FindElement(By.LinkText("View")).Click();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
address = await s.Server.ExplorerNode.GetNewAddressAsync();
s.Driver.FindElement(By.Id("Destination")).Clear();
s.Driver.FindElement(By.Id("Destination")).SendKeys(address.ToString());
s.Driver.FindElement(By.Id("ClaimedAmount")).Clear();
s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys("20" + Keys.Enter);
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
s.FindAlertMessage();
Assert.Contains(PayoutState.AwaitingPayment.GetStateString(), s.Driver.PageSource);
s.Driver.Close();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
// LNURL Withdraw support check with BTC denomination
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
@ -2047,8 +2106,11 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("Amount")).SendKeys("0.0000001");
s.Driver.FindElement(By.Id("Currency")).Clear();
s.Driver.FindElement(By.Id("Currency")).SendKeys("BTC" + Keys.Enter);
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
s.FindAlertMessage();
s.Driver.FindElement(By.LinkText("View")).Click();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
s.Driver.FindElement(By.CssSelector("#lnurlwithdraw-button")).Click();
s.Driver.WaitForElement(By.Id("qr-code-data-input"));
@ -2074,6 +2136,8 @@ namespace BTCPayServer.Tests
Assert.Contains(PayoutState.Completed.GetStateString(), s.Driver.PageSource);
Assert.Equal(LightningInvoiceStatus.Paid, (await s.Server.CustomerLightningD.GetInvoice(bolt2.Id)).Status);
});
s.Driver.Close();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
@ -2083,8 +2147,11 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("Amount")).SendKeys("0.0000001");
s.Driver.FindElement(By.Id("Currency")).Clear();
s.Driver.FindElement(By.Id("Currency")).SendKeys("BTC" + Keys.Enter);
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
s.FindAlertMessage();
s.Driver.FindElement(By.LinkText("View")).Click();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
s.Driver.FindElement(By.CssSelector("#lnurlwithdraw-button")).Click();
lnurl = new Uri(LNURL.LNURL.Parse(s.Driver.FindElement(By.Id("qr-code-data-input")).GetAttribute("value"), out _).ToString().Replace("https", "http"));
@ -2108,6 +2175,8 @@ namespace BTCPayServer.Tests
Assert.Contains(PayoutState.AwaitingApproval.GetStateString(), s.Driver.PageSource);
});
s.Driver.Close();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
// LNURL Withdraw support check with SATS denomination
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
@ -2118,8 +2187,11 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("Amount")).SendKeys("21021");
s.Driver.FindElement(By.Id("Currency")).Clear();
s.Driver.FindElement(By.Id("Currency")).SendKeys("SATS" + Keys.Enter);
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
s.FindAlertMessage();
s.Driver.FindElement(By.LinkText("View")).Click();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
s.Driver.FindElement(By.CssSelector("#lnurlwithdraw-button")).Click();
lnurl = new Uri(LNURL.LNURL.Parse(s.Driver.FindElement(By.Id("qr-code-data-input")).GetAttribute("value"), out _).ToString().Replace("https", "http"));
s.Driver.FindElement(By.CssSelector("button[data-bs-dismiss='modal']")).Click();
@ -2144,6 +2216,7 @@ namespace BTCPayServer.Tests
Assert.Contains(PayoutState.Completed.GetStateString(), s.Driver.PageSource);
Assert.Equal(LightningInvoiceStatus.Paid, (await s.Server.CustomerLightningD.GetInvoice(bolt2.Id)).Status);
});
s.Driver.Close();
}
[Fact]
@ -2216,7 +2289,7 @@ namespace BTCPayServer.Tests
Assert.Contains("EUR", s.Driver.FindElement(By.Id("Currency")).Text);
Assert.Contains("0,00", s.Driver.FindElement(By.Id("Amount")).Text);
Assert.Equal("", s.Driver.FindElement(By.Id("Calculation")).Text);
Assert.True(s.Driver.FindElement(By.Id("ModeTablist-amount")).Selected);
Assert.True(s.Driver.FindElement(By.Id("ModeTablist-amounts")).Selected);
Assert.False(s.Driver.FindElement(By.Id("ModeTablist-discount")).Enabled);
Assert.False(s.Driver.FindElement(By.Id("ModeTablist-tip")).Enabled);
@ -2225,13 +2298,17 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.CssSelector(".keypad [data-key='2']")).Click();
s.Driver.FindElement(By.CssSelector(".keypad [data-key='3']")).Click();
s.Driver.FindElement(By.CssSelector(".keypad [data-key='4']")).Click();
s.Driver.FindElement(By.CssSelector(".keypad [data-key='.']")).Click();
s.Driver.FindElement(By.CssSelector(".keypad [data-key='0']")).Click();
s.Driver.FindElement(By.CssSelector(".keypad [data-key='0']")).Click();
Assert.Equal("1.234,00", s.Driver.FindElement(By.Id("Amount")).Text);
Assert.Equal("", s.Driver.FindElement(By.Id("Calculation")).Text);
s.Driver.FindElement(By.CssSelector(".keypad [data-key='+']")).Click();
s.Driver.FindElement(By.CssSelector(".keypad [data-key='5']")).Click();
s.Driver.FindElement(By.CssSelector(".keypad [data-key='6']")).Click();
Assert.Equal("1.234,56", s.Driver.FindElement(By.Id("Amount")).Text);
Assert.True(s.Driver.FindElement(By.Id("ModeTablist-discount")).Enabled);
Assert.True(s.Driver.FindElement(By.Id("ModeTablist-tip")).Enabled);
Assert.Equal("", s.Driver.FindElement(By.Id("Calculation")).Text);
Assert.Equal("1.234,00 € + 0,56 €", s.Driver.FindElement(By.Id("Calculation")).Text);
// Discount: 10%
s.Driver.FindElement(By.CssSelector("label[for='ModeTablist-discount']")).Click();
@ -2239,14 +2316,14 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.CssSelector(".keypad [data-key='0']")).Click();
Assert.Contains("1.111,10", s.Driver.FindElement(By.Id("Amount")).Text);
Assert.Contains("10% discount", s.Driver.FindElement(By.Id("Discount")).Text);
Assert.Contains("1.234,56 € - 123,46 € (10%)", s.Driver.FindElement(By.Id("Calculation")).Text);
Assert.Contains("1.234,00 € + 0,56 € - 123,46 € (10%)", s.Driver.FindElement(By.Id("Calculation")).Text);
// Tip: 10%
s.Driver.FindElement(By.CssSelector("label[for='ModeTablist-tip']")).Click();
s.Driver.WaitForElement(By.Id("Tip-Custom"));
s.Driver.FindElement(By.Id("Tip-10")).Click();
Assert.Contains("1.222,21", s.Driver.FindElement(By.Id("Amount")).Text);
Assert.Contains("1.234,56 € - 123,46 € (10%) + 111,11 € (10%)", s.Driver.FindElement(By.Id("Calculation")).Text);
Assert.Contains("1.234,00 € + 0,56 € - 123,46 € (10%) + 111,11 € (10%)", s.Driver.FindElement(By.Id("Calculation")).Text);
// Pay
s.Driver.FindElement(By.Id("pay-button")).Click();
@ -2529,20 +2606,22 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("Create")).Click();
s.Driver.FindElement(By.LinkText("View")).Click();
s.Driver.FindElement(By.Id("Destination")).SendKeys(lnurl);
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
var pullPaymentId = s.Driver.Url.Split('/').Last();
s.Driver.FindElement(By.Id("Destination")).SendKeys(lnurl);
s.Driver.FindElement(By.Id("ClaimedAmount")).Clear();
s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys("0.0000001" + Keys.Enter);
s.FindAlertMessage();
s.Driver.Close();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
var payouts = s.Driver.FindElements(By.ClassName("pp-payout"));
payouts[0].Click();
s.Driver.FindElement(By.Id("BTC_LightningLike-view")).Click();
Assert.NotEmpty(s.Driver.FindElements(By.ClassName("payout")));
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-selectAllCheckbox")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-actions")).Click();
s.Driver.FindElement(By.ClassName("mass-action-select-all")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-approve-pay")).Click();
Assert.Contains(lnurl, s.Driver.PageSource);

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);
@ -2881,15 +2881,15 @@ namespace BTCPayServer.Tests
Assert.Single(paymentTypes["On-Chain"]);
// 2 on-chain transactions: It received from the cashcow, then paid its own invoice
report = await GetReport(acc, new() { ViewName = "On-Chain Wallets" });
report = await GetReport(acc, new() { ViewName = "Wallets" });
var txIdIndex = report.GetIndex("TransactionId");
var balanceIndex = report.GetIndex("BalanceChange");
Assert.Equal(2, report.Data.Count);
Assert.Equal(64, report.Data[0][txIdIndex].Value<string>().Length);
Assert.Contains(report.Data, d => d[balanceIndex].Value<decimal>() == 1.0m);
Assert.Contains(report.Data, d => d[balanceIndex]["v"].Value<decimal>() == 1.0m);
// Items sold
report = await GetReport(acc, new() { ViewName = "Products sold" });
report = await GetReport(acc, new() { ViewName = "Sales" });
var itemIndex = report.GetIndex("Product");
var countIndex = report.GetIndex("Quantity");
var itemsCount = report.Data.GroupBy(d => d[itemIndex].Value<string>())

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"
@ -224,7 +224,7 @@ services:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:v0.16.4-beta-1
image: btcpayserver/lnd:v0.17.2-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -259,7 +259,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.16.4-beta-1
image: btcpayserver/lnd:v0.17.2-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -307,7 +307,7 @@ services:
- "torrcdir:/usr/local/etc/tor"
- "tor_servicesdir:/var/lib/tor/hidden_services"
monerod:
image: btcpayserver/monero:0.17.0.0-amd64
image: btcpayserver/monero:0.18.2.2-5
restart: unless-stopped
container_name: xmr_monerod
entrypoint: sleep 999999
@ -317,7 +317,7 @@ services:
ports:
- "18081:18081"
monero_wallet:
image: btcpayserver/monero:0.17.0.0-amd64
image: btcpayserver/monero:0.18.2.2-5
restart: unless-stopped
container_name: xmr_wallet_rpc
entrypoint: monero-wallet-rpc --testnet --rpc-bind-ip=0.0.0.0 --disable-rpc-login --confirm-external-bind --rpc-bind-port=18082 --non-interactive --trusted-daemon --daemon-address=monerod:18081 --wallet-file=/wallet/wallet.keys --password-file=/wallet/password --tx-notify="/bin/sh ./scripts/notifier.sh -k -X GET https://host.docker.internal:14142/monerolikedaemoncallback/tx?cryptoCode=xmr&hash=%s"
@ -349,7 +349,7 @@ services:
elementsd-liquid:
restart: always
container_name: btcpayserver_elementsd_liquid
image: btcpayserver/elements:0.21.0.1
image: btcpayserver/elements:0.21.0.2-4
environment:
ELEMENTS_CHAIN: elementsregtest
ELEMENTS_EXTRA_ARGS: |
@ -364,11 +364,9 @@ services:
whitelist=0.0.0.0/0
rpcallowip=0.0.0.0/0
validatepegin=0
initialfreecoins=210000000000000
initialfreecoins=2100000000000000
con_dyna_deploy_signal=1
con_dyna_deploy_start=0
con_nminerconfirmationwindow=1
con_nrulechangeactivationthreshold=1
con_dyna_deploy_start=10
expose:
- "19332"
- "19444"

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"
@ -211,7 +211,7 @@ services:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:v0.16.4-beta-1
image: btcpayserver/lnd:v0.17.2-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -248,7 +248,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.16.4-beta-1
image: btcpayserver/lnd:v0.17.2-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"

View File

@ -53,8 +53,7 @@
<PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="Fido2" Version="2.0.2" />
<PackageReference Include="Fido2.AspNet" Version="2.0.2" />
<PackageReference Include="HtmlSanitizer" Version="5.0.372" />
<PackageReference Include="LNURL" Version="0.0.33" />
<PackageReference Include="LNURL" Version="0.0.34" />
<PackageReference Include="MailKit" Version="3.3.0" />
<PackageReference Include="BTCPayServer.NETCore.Plugins.Mvc" Version="1.4.4" />
<PackageReference Include="QRCoder" Version="1.4.3" />
@ -138,6 +137,7 @@
<ItemGroup>
<Watch Include="Views\**\*.*"></Watch>
<Watch Remove="Views\UIAccount\CheatPermissions.cshtml" />
<Watch Remove="Views\UIPullPayment\ViewPullPaymentPrint.cshtml" />
<Watch Remove="Views\UIReports\StoreReports.cshtml" />
<Content Update="Views\UIApps\_ViewImports.cshtml">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>

View File

@ -1,4 +1,3 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.JSInterop;
namespace BTCPayServer.Blazor

View File

@ -1,4 +1,3 @@
@using System.Security.Claims
@using BTCPayServer.Abstractions.Contracts;
@using BTCPayServer.Configuration;
@using BTCPayServer.Data;
@ -86,7 +85,8 @@
}
public void Dispose() => _EventAggregatorListener?.Dispose();
string SeenCount(int? count)
static string SeenCount(int? count)
{
if (count is not int c)
return "0";
@ -94,12 +94,14 @@
return $"{NotificationManager.MaxUnseen - 1}+";
return c.ToString();
}
void UpdateState((List<NotificationViewModel> Items, int? Count) res)
{
UnseenCount = SeenCount(res.Count);
Last5 = res.Items;
}
protected async override Task OnParametersSetAsync()
protected override async Task OnParametersSetAsync()
{
if (await GetUserId() is string userId)
{
@ -117,15 +119,16 @@
}
}
}
async Task<string>
GetUserId()
async Task<string> GetUserId()
{
var state = await _AuthenticationStateProvider.GetAuthenticationStateAsync();
if (!state.User.Identity.IsAuthenticated)
return null;
return _UserManager.GetUserId(state.User);
}
public async Task MarkAllAsSeen()
private async Task MarkAllAsSeen()
{
if (await GetUserId() is string userId)
{
@ -133,6 +136,7 @@
UnseenCount = "0";
}
}
private static string NotificationIcon(string type)
{
return type switch

View File

@ -13,14 +13,20 @@ namespace BTCPayServer
{
return Regex.Match(color, Pattern).Success;
}
public string TextColor(string bgColor)
public Color TextColor(Color bg)
{
int nThreshold = 105;
var bg = ColorTranslator.FromHtml(bgColor);
int bgDelta = Convert.ToInt32((bg.R * 0.299) + (bg.G * 0.587) + (bg.B * 0.114));
Color color = (255 - bgDelta < nThreshold) ? Color.Black : Color.White;
int bgDelta = Convert.ToInt32(bg.R * 0.299 + bg.G * 0.587 + bg.B * 0.114);
return 255 - bgDelta < nThreshold ? Color.Black : Color.White;
}
public string TextColor(string bg)
{
var color = TextColor(FromHtml(bg));
return ColorTranslator.ToHtml(color).ToLowerInvariant();
}
// Borrowed from https://github.com/ManageIQ/guides/blob/master/labels.md
public static readonly ColorPalette Default = new ColorPalette(new string[] {
"#fbca04",
@ -31,6 +37,7 @@ namespace BTCPayServer
"#cdcdcd",
"#cc317c",
});
private ColorPalette(string[] labels)
{
Labels = labels;
@ -98,5 +105,10 @@ namespace BTCPayServer
var color = AdjustBrightness(ColorTranslator.FromHtml(html), correctionFactor);
return ColorTranslator.ToHtml(color);
}
public Color FromHtml(string html)
{
return ColorTranslator.FromHtml(html);
}
}
}

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

@ -1,7 +1,5 @@
@using BTCPayServer.Services
@using BTCPayServer.Abstractions.Contracts
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Abstractions.TagHelpers
@inject ThemeSettings Theme
@inject IFileService FileService
@model BTCPayServer.Components.MainLogo.MainLogoViewModel

View File

@ -4,7 +4,6 @@
@using BTCPayServer.Views.Manage
@using BTCPayServer.Views.PaymentRequest
@using BTCPayServer.Views.Wallets
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Client
@using BTCPayServer.Components.ThemeSwitch
@using BTCPayServer.Components.UIExtensionPoint

View File

@ -11,13 +11,14 @@ namespace BTCPayServer.Components.QRCode
{
private static QRCodeGenerator _qrGenerator = new();
public IViewComponentResult Invoke(string data)
public IViewComponentResult Invoke(string data, int size=256)
{
var qrCodeData = _qrGenerator.CreateQrCode(data, QRCodeGenerator.ECCLevel.Q);
var qrCode = new PngByteQRCode(qrCodeData);
var bytes = qrCode.GetGraphic(5, new byte[] { 0, 0, 0, 255 }, new byte[] { 0xf5, 0xf5, 0xf7, 255 });
var b64 = Convert.ToBase64String(bytes);
return new HtmlContentViewComponentResult(new HtmlString($"<img style=\"image-rendering:pixelated;image-rendering:-moz-crisp-edges;min-width:256px;min-height:256px\" src=\"data:image/png;base64,{b64}\" class=\"qr-code\" />"));
return new HtmlContentViewComponentResult(new HtmlString(
$"<img style=\"image-rendering:pixelated;image-rendering:-moz-crisp-edges;min-width:{size}px;min-height:{size}px\" src=\"data:image/png;base64,{b64}\" class=\"qr-code\" />"));
}
}
}

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,4 +1,3 @@
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Abstractions.Contracts
@using BTCPayServer.Client
@using BTCPayServer.Components.MainLogo

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

@ -9,7 +9,15 @@
@if (Model.IsVue)
{
<span class="truncate-center-truncated" data-bs-toggle="tooltip" :title=@Safe.Json(Model.Text)>
<span class="truncate-center-start" v-text=@Safe.Json(Model.Text)></span>
@if (Model.Elastic)
{
<span class="truncate-center-start" v-text=@Safe.Json(Model.Text)></span>
}
else
{
<span class="truncate-center-start" v-text=@Safe.Json($"{Model.Text}.slice(0, {Model.Padding})")></span>
<span>…</span>
}
<span class="truncate-center-end" v-text=@Safe.Json($"{Model.Text}.slice(-{Model.Padding})")></span>
</span>
<span class="truncate-center-text" v-text=@Safe.Json(Model.Text)></span>
@ -33,7 +41,7 @@
}
@if (!string.IsNullOrEmpty(Model.Link))
{
<a href="@Model.Link" rel="noreferrer noopener" target="_blank">
<a @(Model.IsVue ? ":" : "")href="@Model.Link" rel="noreferrer noopener" target="_blank">
<vc:icon symbol="info" />
</a>
}

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

@ -270,12 +270,14 @@ namespace BTCPayServer.Controllers.Greenfield
private PointOfSaleSettings ToPointOfSaleSettings(CreatePointOfSaleAppRequest request)
{
return new PointOfSaleSettings()
return new PointOfSaleSettings
{
Title = request.Title,
DefaultView = (PosViewType)request.DefaultView,
ShowCustomAmount = request.ShowCustomAmount,
ShowDiscount = request.ShowDiscount,
ShowSearch = request.ShowSearch,
ShowCategories = request.ShowCategories,
EnableTips = request.EnableTips,
Currency = request.Currency,
Template = request.Template != null ? AppService.SerializeTemplate(AppService.Parse(request.Template)) : null,
@ -336,6 +338,8 @@ namespace BTCPayServer.Controllers.Greenfield
DefaultView = settings.DefaultView.ToString(),
ShowCustomAmount = settings.ShowCustomAmount,
ShowDiscount = settings.ShowDiscount,
ShowSearch = settings.ShowSearch,
ShowCategories = settings.ShowCategories,
EnableTips = settings.EnableTips,
Currency = settings.Currency,
Items = JsonConvert.DeserializeObject(

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

@ -54,7 +54,8 @@ namespace BTCPayServer.Controllers.Greenfield
new LNURLPayPaymentMethodData(
paymentMethod.PaymentId.CryptoCode,
!excludedPaymentMethods.Match(paymentMethod.PaymentId),
paymentMethod.UseBech32Scheme
paymentMethod.UseBech32Scheme,
paymentMethod.LUD12Enabled
)
)
.Where((result) => enabled is null || enabled == result.Enabled)
@ -121,10 +122,11 @@ namespace BTCPayServer.Controllers.Greenfield
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
LNURLPaySupportedPaymentMethod? paymentMethod = new LNURLPaySupportedPaymentMethod()
var paymentMethod = new LNURLPaySupportedPaymentMethod
{
CryptoCode = cryptoCode,
UseBech32Scheme = paymentMethodData.UseBech32Scheme
UseBech32Scheme = paymentMethodData.UseBech32Scheme,
LUD12Enabled = paymentMethodData.LUD12Enabled
};
var store = Store;
@ -153,7 +155,8 @@ namespace BTCPayServer.Controllers.Greenfield
: new LNURLPayPaymentMethodData(
paymentMethod.PaymentId.CryptoCode,
!excluded,
paymentMethod.UseBech32Scheme
paymentMethod.UseBech32Scheme,
paymentMethod.LUD12Enabled
);
}
private void AssertCryptoCodeWallet(string cryptoCode, out BTCPayNetwork network)

View File

@ -153,13 +153,18 @@ namespace BTCPayServer.Controllers.Greenfield
LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate,
PaymentTolerance = storeBlob.PaymentTolerance,
PayJoinEnabled = storeBlob.PayJoinEnabled,
PaymentMethodCriteria = storeBlob.PaymentMethodCriteria?.Where(criteria => criteria.Value is not null)?.Select(criteria => new PaymentMethodCriteriaData()
AutoDetectLanguage = storeBlob.AutoDetectLanguage,
ShowPayInWalletButton = storeBlob.ShowPayInWalletButton,
ShowStoreHeader = storeBlob.ShowStoreHeader,
CelebratePayment = storeBlob.CelebratePayment,
PlaySoundOnPayment = storeBlob.PlaySoundOnPayment,
PaymentMethodCriteria = storeBlob.PaymentMethodCriteria?.Where(criteria => criteria.Value is not null).Select(criteria => new PaymentMethodCriteriaData
{
Above = criteria.Above,
Amount = criteria.Value.Value,
CurrencyCode = criteria.Value.Currency,
PaymentMethod = criteria.PaymentMethod.ToStringNormalized()
})?.ToList() ?? new List<PaymentMethodCriteriaData>()
}).ToList() ?? new List<PaymentMethodCriteriaData>()
};
}
@ -201,11 +206,21 @@ namespace BTCPayServer.Controllers.Greenfield
blob.LightningDescriptionTemplate = restModel.LightningDescriptionTemplate;
blob.PaymentTolerance = restModel.PaymentTolerance;
blob.PayJoinEnabled = restModel.PayJoinEnabled;
if (restModel.AutoDetectLanguage.HasValue)
blob.AutoDetectLanguage = restModel.AutoDetectLanguage.Value;
if (restModel.ShowPayInWalletButton.HasValue)
blob.ShowPayInWalletButton = restModel.ShowPayInWalletButton.Value;
if (restModel.ShowStoreHeader.HasValue)
blob.ShowStoreHeader = restModel.ShowStoreHeader.Value;
if (restModel.CelebratePayment.HasValue)
blob.CelebratePayment = restModel.CelebratePayment.Value;
if (restModel.PlaySoundOnPayment.HasValue)
blob.PlaySoundOnPayment = restModel.PlaySoundOnPayment.Value;
blob.PaymentMethodCriteria = restModel.PaymentMethodCriteria?.Select(criteria =>
new PaymentMethodCriteria()
new PaymentMethodCriteria
{
Above = criteria.Above,
Value = new CurrencyValue()
Value = new CurrencyValue
{
Currency = criteria.CurrencyCode,
Value = criteria.Amount

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

@ -228,13 +228,17 @@ namespace BTCPayServer.Controllers
if (app is null || userId is null)
return NotFound();
if (!file.FileName.IsValidFileName())
{
return Json(new { error = "Invalid file name" });
}
if (!file.ContentType.StartsWith("image/", StringComparison.InvariantCulture))
{
return Json(new { error = "The file needs to be an image" });
}
if (file.Length > 500_000)
{
return Json(new { error = "The image file size should be less than 0.5MB" });
return Json(new { error = "The file size should be less than 0.5MB" });
}
var formFile = await file.Bufferize();
if (!FileTypeDetector.IsPicture(formFile.Buffer, formFile.FileName))

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"))
{
@ -253,6 +254,7 @@ namespace BTCPayServer.Controllers
Amount = paymentEntity.PaidAmount.Gross,
Paid = paymentEntity.InvoicePaidAmount.Net,
ReceivedDate = paymentEntity.ReceivedTime.DateTime,
AmountFormatted = _displayFormatter.Currency(paymentEntity.PaidAmount.Gross, paymentEntity.PaidAmount.Currency),
PaidFormatted = _displayFormatter.Currency(paymentEntity.InvoicePaidAmount.Net, i.Currency, DisplayFormatter.CurrencyFormat.Symbol),
RateFormatted = _displayFormatter.Currency(paymentEntity.Rate, i.Currency, DisplayFormatter.CurrencyFormat.Symbol),
PaymentMethod = paymentMethodId.ToPrettyString(),
@ -565,37 +567,42 @@ namespace BTCPayServer.Controllers
private InvoiceDetailsModel InvoicePopulatePayments(InvoiceEntity invoice)
{
var overpaid = false;
var stillDue = false;
var hasRates = false;
var model = new InvoiceDetailsModel
{
Archived = invoice.Archived,
Payments = invoice.GetPayments(false),
Overpaid = true,
CryptoPayments = invoice.GetPaymentMethods().Select(
data =>
{
var accounting = data.Calculate();
var paymentMethodId = data.GetId();
var hasPayment = accounting.CryptoPaid > 0;
var overpaidAmount = accounting.OverpaidHelper;
if (overpaidAmount > 0)
{
overpaid = true;
}
var rate = ExchangeRate(data.GetId().CryptoCode, data);
if (rate is not null) hasRates = true;
if (hasPayment && overpaidAmount > 0) overpaid = true;
if (hasPayment && accounting.Due > 0) stillDue = true;
return new InvoiceDetailsModel.CryptoPayment
{
Rate = rate,
PaymentMethodRaw = data,
PaymentMethodId = paymentMethodId,
PaymentMethod = paymentMethodId.ToPrettyString(),
Due = _displayFormatter.Currency(accounting.Due, paymentMethodId.CryptoCode),
Paid = _displayFormatter.Currency(accounting.CryptoPaid, paymentMethodId.CryptoCode),
Overpaid = _displayFormatter.Currency(overpaidAmount, paymentMethodId.CryptoCode),
Address = data.GetPaymentMethodDetails().GetPaymentDestination(),
Rate = ExchangeRate(data.GetId().CryptoCode, data),
PaymentMethodRaw = data
TotalDue = _displayFormatter.Currency(accounting.TotalDue, paymentMethodId.CryptoCode),
Due = hasPayment ? _displayFormatter.Currency(accounting.Due, paymentMethodId.CryptoCode) : null,
Paid = hasPayment ? _displayFormatter.Currency(accounting.CryptoPaid, paymentMethodId.CryptoCode) : null,
Overpaid = hasPayment ? _displayFormatter.Currency(overpaidAmount, paymentMethodId.CryptoCode) : null,
Address = data.GetPaymentMethodDetails().GetPaymentDestination()
};
}).ToList()
}).ToList(),
Overpaid = overpaid,
StillDue = stillDue,
HasRates = hasRates
};
model.Overpaid = overpaid;
return model;
}
@ -628,58 +635,56 @@ namespace BTCPayServer.Controllers
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanViewInvoices)]
public async Task<IActionResult> MassAction(string command, string[] selectedItems, string? storeId = null)
{
if (selectedItems != null)
IActionResult NotSupported(string err)
{
switch (command)
{
case "archive":
await _InvoiceRepository.MassArchive(selectedItems);
TempData[WellKnownTempData.SuccessMessage] = $"{selectedItems.Length} invoice{(selectedItems.Length == 1 ? "" : "s")} archived.";
break;
TempData[WellKnownTempData.ErrorMessage] = err;
return RedirectToAction(nameof(ListInvoices), new { storeId });
}
if (selectedItems.Length == 0)
return NotSupported("No invoice has been selected");
case "unarchive":
await _InvoiceRepository.MassArchive(selectedItems, false);
TempData[WellKnownTempData.SuccessMessage] = $"{selectedItems.Length} invoice{(selectedItems.Length == 1 ? "" : "s")} unarchived.";
break;
case "cpfp":
if (selectedItems.Length == 0)
return NotSupported("No invoice has been selected");
var network = _NetworkProvider.DefaultNetwork;
var explorer = _ExplorerClients.GetExplorerClient(network);
IActionResult NotSupported(string err)
{
TempData[WellKnownTempData.ErrorMessage] = err;
return RedirectToAction(nameof(ListInvoices), new { storeId });
}
if (explorer is null)
return NotSupported("This feature is only available to BTC wallets");
if (!GetCurrentStore().HasPermission(GetUserId(), Policies.CanModifyStoreSettings))
return Forbid();
switch (command)
{
case "archive":
await _InvoiceRepository.MassArchive(selectedItems);
TempData[WellKnownTempData.SuccessMessage] = $"{selectedItems.Length} invoice{(selectedItems.Length == 1 ? "" : "s")} archived.";
break;
var derivationScheme = (this.GetCurrentStore().GetDerivationSchemeSettings(_NetworkProvider, network.CryptoCode))?.AccountDerivation;
if (derivationScheme is null)
return NotSupported("This feature is only available to BTC wallets");
var bumpableAddresses = (await GetAddresses(selectedItems))
.Where(p => p.GetPaymentMethodId().IsBTCOnChain)
.Select(p => p.GetAddress()).ToHashSet();
var utxos = await explorer.GetUTXOsAsync(derivationScheme);
var bumpableUTXOs = utxos.GetUnspentUTXOs().Where(u => u.Confirmations == 0 && bumpableAddresses.Contains(u.ScriptPubKey.Hash.ToString())).ToArray();
var parameters = new MultiValueDictionary<string, string>();
foreach (var utxo in bumpableUTXOs)
{
parameters.Add($"outpoints[]", utxo.Outpoint.ToString());
}
return View("PostRedirect", new PostRedirectViewModel
{
AspController = "UIWallets",
AspAction = nameof(UIWalletsController.WalletCPFP),
RouteParameters = {
{ "walletId", new WalletId(storeId, network.CryptoCode).ToString() },
{ "returnUrl", Url.Action(nameof(ListInvoices), new { storeId }) }
},
FormParameters = parameters,
});
}
case "unarchive":
await _InvoiceRepository.MassArchive(selectedItems, false);
TempData[WellKnownTempData.SuccessMessage] = $"{selectedItems.Length} invoice{(selectedItems.Length == 1 ? "" : "s")} unarchived.";
break;
case "cpfp":
var network = _NetworkProvider.DefaultNetwork;
var explorer = _ExplorerClients.GetExplorerClient(network);
if (explorer is null)
return NotSupported("This feature is only available to BTC wallets");
if (!GetCurrentStore().HasPermission(GetUserId(), Policies.CanModifyStoreSettings))
return Forbid();
var derivationScheme = (this.GetCurrentStore().GetDerivationSchemeSettings(_NetworkProvider, network.CryptoCode))?.AccountDerivation;
if (derivationScheme is null)
return NotSupported("This feature is only available to BTC wallets");
var bumpableAddresses = (await GetAddresses(selectedItems))
.Where(p => p.GetPaymentMethodId().IsBTCOnChain)
.Select(p => p.GetAddress()).ToHashSet();
var utxos = await explorer.GetUTXOsAsync(derivationScheme);
var bumpableUTXOs = utxos.GetUnspentUTXOs().Where(u => u.Confirmations == 0 && bumpableAddresses.Contains(u.ScriptPubKey.Hash.ToString())).ToArray();
var parameters = new MultiValueDictionary<string, string>();
foreach (var utxo in bumpableUTXOs)
{
parameters.Add($"outpoints[]", utxo.Outpoint.ToString());
}
return View("PostRedirect", new PostRedirectViewModel
{
AspController = "UIWallets",
AspAction = nameof(UIWalletsController.WalletCPFP),
RouteParameters = {
{ "walletId", new WalletId(storeId, network.CryptoCode).ToString() },
{ "returnUrl", Url.Action(nameof(ListInvoices), new { storeId }) }
},
FormParameters = parameters,
});
}
return RedirectToAction(nameof(ListInvoices), new { storeId });
}
@ -1147,7 +1152,7 @@ namespace BTCPayServer.Controllers
CanMarkInvalid = state.CanMarkInvalid(),
CanMarkSettled = state.CanMarkComplete(),
Details = InvoicePopulatePayments(invoice),
HasRefund = invoice.Refunds.Any(data => !data.PullPaymentData.Archived)
HasRefund = invoice.Refunds.Any()
});
}
return View(model);
@ -1254,6 +1259,19 @@ namespace BTCPayServer.Controllers
model.CheckoutType = storeBlob.CheckoutType;
model.AvailablePaymentMethods = GetPaymentMethodsSelectList();
JObject? metadataObj = null;
if (!string.IsNullOrEmpty(model.Metadata))
{
try
{
metadataObj = JObject.Parse(model.Metadata);
}
catch (Exception e)
{
ModelState.AddModelError(nameof(model.Metadata), "Metadata was not valid JSON");
}
}
if (!ModelState.IsValid)
{
return View(model);
@ -1272,17 +1290,27 @@ namespace BTCPayServer.Controllers
try
{
var metadata = metadataObj is null ? new InvoiceMetadata() : InvoiceMetadata.FromJObject(metadataObj);
if (!string.IsNullOrEmpty(model.OrderId))
{
metadata.OrderId = model.OrderId;
}
if (!string.IsNullOrEmpty(model.ItemDesc))
{
metadata.ItemDesc = model.ItemDesc;
}
if (!string.IsNullOrEmpty(model.BuyerEmail))
{
metadata.BuyerEmail = model.BuyerEmail;
}
var result = await CreateInvoiceCoreRaw(new CreateInvoiceRequest()
{
Amount = model.Amount,
Currency = model.Currency,
Metadata = new InvoiceMetadata()
{
PosDataLegacy = model.PosData,
OrderId = model.OrderId,
ItemDesc = model.ItemDesc,
BuyerEmail = model.BuyerEmail,
}.ToJObject(),
Metadata = metadata.ToJObject(),
Checkout = new ()
{
RedirectURL = store.StoreWebsite,

View File

@ -92,7 +92,7 @@ namespace BTCPayServer
}
[HttpGet("withdraw/pp/{pullPaymentId}")]
public async Task<IActionResult> GetLNURLForPullPayment(string cryptoCode, string pullPaymentId, string pr, CancellationToken cancellationToken)
public async Task<IActionResult> GetLNURLForPullPayment(string cryptoCode, string pullPaymentId, [FromQuery] string pr, CancellationToken cancellationToken)
{
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
if (network is null || !network.SupportLightning)

View File

@ -205,6 +205,7 @@ namespace BTCPayServer.Controllers
var storeBlob = store.GetStoreBlob();
vm.StoreName = store.StoreName;
vm.StoreWebsite = store.StoreWebsite;
vm.BrandColor = storeBlob.BrandColor;
vm.LogoFileId = storeBlob.LogoFileId;
vm.CssFileId = storeBlob.CssFileId;

View File

@ -55,7 +55,7 @@ namespace BTCPayServer.Controllers
[AllowAnonymous]
[HttpGet("pull-payments/{pullPaymentId}")]
public async Task<IActionResult> ViewPullPayment(string pullPaymentId)
public async Task<IActionResult> ViewPullPayment(string pullPaymentId, [FromQuery] bool print = false)
{
using var ctx = _dbContextFactory.CreateContext();
var pp = await ctx.PullPayments.FindAsync(pullPaymentId);
@ -69,14 +69,14 @@ namespace BTCPayServer.Controllers
var storeBlob = store.GetStoreBlob();
var payouts = (await ctx.Payouts.GetPayoutInPeriod(pp)
.OrderByDescending(o => o.Date)
.ToListAsync())
.Select(o => new
{
Entity = o,
Blob = o.GetBlob(_serializerSettings),
ProofBlob = _payoutHandlers.FindPayoutHandler(o.GetPaymentMethodId())?.ParseProof(o)
});
.OrderByDescending(o => o.Date)
.ToListAsync())
.Select(o => new
{
Entity = o,
Blob = o.GetBlob(_serializerSettings),
ProofBlob = _payoutHandlers.FindPayoutHandler(o.GetPaymentMethodId())?.ParseProof(o)
});
var cd = _currencyNameTable.GetCurrencyData(blob.Currency, false);
var totalPaid = payouts.Where(p => p.Entity.State != PayoutState.Cancelled).Select(p => p.Blob.Amount).Sum();
var amountDue = blob.Limit - totalPaid;
@ -91,18 +91,18 @@ namespace BTCPayServer.Controllers
CurrencyData = cd,
StartDate = pp.StartDate,
LastRefreshed = DateTime.UtcNow,
Payouts = payouts
.Select(entity => new ViewPullPaymentModel.PayoutLine
{
Id = entity.Entity.Id,
Amount = entity.Blob.Amount,
Currency = blob.Currency,
Status = entity.Entity.State,
Destination = entity.Blob.Destination,
PaymentMethod = PaymentMethodId.Parse(entity.Entity.PaymentMethodId),
Link = entity.ProofBlob?.Link,
TransactionId = entity.ProofBlob?.Id
}).ToList()
Payouts = payouts.Select(entity => new ViewPullPaymentModel.PayoutLine
{
Id = entity.Entity.Id,
Amount = entity.Blob.Amount,
AmountFormatted = _displayFormatter.Currency(entity.Blob.Amount, blob.Currency),
Currency = blob.Currency,
Status = entity.Entity.State,
Destination = entity.Blob.Destination,
PaymentMethod = PaymentMethodId.Parse(entity.Entity.PaymentMethodId),
Link = entity.ProofBlob?.Link,
TransactionId = entity.ProofBlob?.Id
}).ToList()
};
vm.IsPending &= vm.AmountDue > 0.0m;
@ -111,8 +111,9 @@ namespace BTCPayServer.Controllers
var url = Url.Action("GetLNURLForPullPayment", "UILNURL", new { cryptoCode = _networkProvider.DefaultNetwork.CryptoCode, pullPaymentId = vm.Id }, Request.Scheme, Request.Host.ToString());
vm.LnurlEndpoint = url != null ? new Uri(url) : null;
}
return View(nameof(ViewPullPayment), vm);
return View(print ? "ViewPullPaymentPrint" : "ViewPullPayment", vm);
}
[HttpGet("stores/{storeId}/pull-payments/edit/{pullPaymentId}")]
@ -176,31 +177,53 @@ namespace BTCPayServer.Controllers
[HttpPost("pull-payments/{pullPaymentId}/claim")]
public async Task<IActionResult> ClaimPullPayment(string pullPaymentId, ViewPullPaymentModel vm, CancellationToken cancellationToken)
{
using var ctx = _dbContextFactory.CreateContext();
await using var ctx = _dbContextFactory.CreateContext();
var pp = await ctx.PullPayments.FindAsync(pullPaymentId);
if (pp is null)
{
ModelState.AddModelError(nameof(pullPaymentId), "This pull payment does not exists");
}
var ppBlob = pp.GetBlob();
var paymentMethodId = ppBlob.SupportedPaymentMethods.FirstOrDefault(id => vm.SelectedPaymentMethod == id.ToString());
var payoutHandler = paymentMethodId is null ? null : _payoutHandlers.FindPayoutHandler(paymentMethodId);
if (payoutHandler is null)
if (string.IsNullOrEmpty(vm.Destination))
{
ModelState.AddModelError(nameof(vm.SelectedPaymentMethod), "Invalid destination with selected payment method");
ModelState.AddModelError(nameof(vm.Destination), "Please provide a destination");
return await ViewPullPayment(pullPaymentId);
}
var destination = await payoutHandler.ParseAndValidateClaimDestination(paymentMethodId, vm.Destination, ppBlob, cancellationToken);
if (destination.destination is null)
var ppBlob = pp.GetBlob();
var supported = ppBlob.SupportedPaymentMethods;
PaymentMethodId paymentMethodId = null;
IClaimDestination destination = null;
if (string.IsNullOrEmpty(vm.SelectedPaymentMethod))
{
ModelState.AddModelError(nameof(vm.Destination), destination.error ?? "Invalid destination with selected payment method");
foreach (var pmId in supported)
{
var handler = _payoutHandlers.FindPayoutHandler(pmId);
(IClaimDestination dst, string err) = handler == null
? (null, "No payment handler found for this payment method")
: await handler.ParseAndValidateClaimDestination(pmId, vm.Destination, ppBlob, cancellationToken);
if (dst is not null && err is null)
{
paymentMethodId = pmId;
destination = dst;
break;
}
}
}
else
{
paymentMethodId = supported.FirstOrDefault(id => vm.SelectedPaymentMethod == id.ToString());
var payoutHandler = paymentMethodId is null ? null : _payoutHandlers.FindPayoutHandler(paymentMethodId);
destination = payoutHandler is null ? null : (await payoutHandler.ParseAndValidateClaimDestination(paymentMethodId, vm.Destination, ppBlob, cancellationToken)).destination;
}
if (destination is null)
{
ModelState.AddModelError(nameof(vm.Destination), "Invalid destination or payment method");
return await ViewPullPayment(pullPaymentId);
}
var amtError = ClaimRequest.IsPayoutAmountOk(destination.destination, vm.ClaimedAmount == 0? null: vm.ClaimedAmount, paymentMethodId.CryptoCode, ppBlob.Currency);
var amtError = ClaimRequest.IsPayoutAmountOk(destination, vm.ClaimedAmount == 0? null: vm.ClaimedAmount, paymentMethodId.CryptoCode, ppBlob.Currency);
if (amtError.error is not null)
{
ModelState.AddModelError(nameof(vm.ClaimedAmount), amtError.error );
@ -215,9 +238,9 @@ namespace BTCPayServer.Controllers
return await ViewPullPayment(pullPaymentId);
}
var result = await _pullPaymentHostedService.Claim(new ClaimRequest()
var result = await _pullPaymentHostedService.Claim(new ClaimRequest
{
Destination = destination.destination,
Destination = destination,
PullPaymentId = pullPaymentId,
Value = vm.ClaimedAmount,
PaymentMethodId = paymentMethodId
@ -225,14 +248,9 @@ namespace BTCPayServer.Controllers
if (result.Result != ClaimRequest.ClaimResult.Ok)
{
if (result.Result == ClaimRequest.ClaimResult.AmountTooLow)
{
ModelState.AddModelError(nameof(vm.ClaimedAmount), ClaimRequest.GetErrorMessage(result.Result));
}
else
{
ModelState.AddModelError(string.Empty, ClaimRequest.GetErrorMessage(result.Result));
}
ModelState.AddModelError(
result.Result == ClaimRequest.ClaimResult.AmountTooLow ? nameof(vm.ClaimedAmount) : string.Empty,
ClaimRequest.GetErrorMessage(result.Result));
return await ViewPullPayment(pullPaymentId);
}

View File

@ -66,6 +66,10 @@ public partial class UIReportsController
decimal randomValue = ((decimal)rand.NextDouble() * range) + from;
return decimal.Round(randomValue, precision);
}
var fiatCurrency = rand.NextSingle() > 0.2 ? "USD" : TakeOne("JPY", "EUR", "CHF");
var cryptoCurrency = rand.NextSingle() > 0.2 ? "BTC" : TakeOne("LTC", "DOGE", "DASH");
if (f.Type == "invoice_id")
return Encoders.Base58.EncodeData(GenerateBytes(20));
if (f.Type == "boolean")
@ -80,9 +84,9 @@ public partial class UIReportsController
if (f.Name == "Address")
return Encoders.Bech32("bc1").Encode(0, GenerateBytes(20));
if (f.Name == "Crypto")
return rand.NextSingle() > 0.2 ? "BTC" : TakeOne("LTC", "DOGE", "DASH");
return cryptoCurrency;
if (f.Name == "CryptoAmount")
return GenerateDecimal(0.1m, 5m, 8);
return DisplayFormatter.ToFormattedAmount(GenerateDecimal(0.1m, 5m, 8), cryptoCurrency);
if (f.Name == "LightningAddress")
return TakeOne("satoshi", "satosan", "satoichi") + "@bitcoin.org";
if (f.Name == "BalanceChange")
@ -98,24 +102,30 @@ public partial class UIReportsController
if (f.Name == "Quantity")
return TakeOne(1, 2, 3, 4, 5);
if (f.Name == "Currency")
return rand.NextSingle() > 0.2 ? "USD" : TakeOne("JPY", "EUR", "CHF");
return fiatCurrency;
if (f.Name == "CurrencyAmount")
return row[fi - 1] switch
{
var curr = row[fi - 1]?.ToString();
var value = curr switch
{
"USD" or "EUR" or "CHF" => GenerateDecimal(100.0m, 10_000m, 2),
"JPY" => GenerateDecimal(10_000m, 1000_0000, 0),
_ => GenerateDecimal(100.0m, 10_000m, 2)
};
return DisplayFormatter.ToFormattedAmount(value, curr);
}
if (f.Type == "tx_id")
return Encoders.Hex.EncodeData(GenerateBytes(32));
if (f.Name == "Rate")
{
return row[fi - 1] switch
var curr = row[fi - 1]?.ToString();
var value = curr switch
{
"USD" or "EUR" or "CHF" => GenerateDecimal(30_000m, 60_000, 2),
"JPY" => GenerateDecimal(400_0000m, 1000_0000m, 0),
_ => GenerateDecimal(30_000m, 60_000, 2)
};
return DisplayFormatter.ToFormattedAmount(value, curr);
}
return null;
}

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,16 +25,18 @@ public partial class UIReportsController : Controller
ApplicationDbContextFactory dbContextFactory,
GreenfieldReportsController api,
ReportService reportService,
BTCPayServerEnvironment env
)
DisplayFormatter displayFormatter,
BTCPayServerEnvironment env)
{
Api = api;
ReportService = reportService;
Env = env;
DBContextFactory = dbContextFactory;
NetworkProvider = networkProvider;
DisplayFormatter = displayFormatter;
}
private BTCPayNetworkProvider NetworkProvider { get; }
private DisplayFormatter DisplayFormatter { get; }
public GreenfieldReportsController Api { get; }
public ReportService ReportService { get; }
public BTCPayServerEnvironment Env { get; }
@ -72,20 +64,17 @@ public partial class UIReportsController : Controller
string storeId,
string ? viewName = null)
{
var vm = new StoreReportsViewModel()
var vm = new StoreReportsViewModel
{
InvoiceTemplateUrl = this.Url.Action(nameof(UIInvoiceController.Invoice), "UIInvoice", new { invoiceId = "INVOICE_ID" }),
InvoiceTemplateUrl = Url.Action(nameof(UIInvoiceController.Invoice), "UIInvoice", new { invoiceId = "INVOICE_ID" }),
ExplorerTemplateUrls = NetworkProvider.GetAll().ToDictionary(network => network.CryptoCode, network => network.BlockExplorerLink?.Replace("{0}", "TX_ID")),
Request = new StoreReportRequest()
{
ViewName = viewName ?? "Payments"
}
Request = new StoreReportRequest { ViewName = viewName ?? "Payments" },
AvailableViews = ReportService.ReportProviders
.Values
.Where(r => r.IsAvailable())
.Select(k => k.Name)
.OrderBy(k => k).ToList()
};
vm.AvailableViews = ReportService.ReportProviders
.Values
.Where(r => r.IsAvailable())
.Select(k => k.Name)
.OrderBy(k => k).ToList();
return View(vm);
}
}

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

@ -50,9 +50,10 @@ namespace BTCPayServer.Controllers
if (network == null)
return NotFound();
var websocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
var vaultClient = new VaultClient(websocket);
var hwi = new Hwi.HwiClient(network.NBitcoinNetwork)
{
Transport = new HwiWebSocketTransport(websocket)
Transport = new VaultHWITransport(vaultClient)
};
Hwi.HwiDeviceClient device = null;
HwiEnumerateEntry deviceEntry = null;

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>();
@ -869,8 +877,7 @@ namespace BTCPayServer.Controllers
try
{
var uriBuilder = new NBitcoin.Payment.BitcoinUrlBuilder(bip21, network.NBitcoinNetwork);
vm.Outputs.Add(new WalletSendModel.TransactionOutput()
var output = new WalletSendModel.TransactionOutput
{
Amount = uriBuilder.Amount?.ToDecimal(MoneyUnit.BTC),
DestinationAddress = uriBuilder.Address?.ToString(),
@ -878,15 +885,20 @@ namespace BTCPayServer.Controllers
PayoutId = uriBuilder.UnknownParameters.ContainsKey("payout")
? uriBuilder.UnknownParameters["payout"]
: null
});
};
if (!string.IsNullOrEmpty(uriBuilder.Label))
{
output.Labels = output.Labels.Append(uriBuilder.Label).ToArray();
}
vm.Outputs.Add(output);
address = uriBuilder.Address;
if (!string.IsNullOrEmpty(uriBuilder.Label) || !string.IsNullOrEmpty(uriBuilder.Message))
{
TempData.SetStatusMessageModel(new StatusMessageModel()
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Info,
Html =
$"Payment {(string.IsNullOrEmpty(uriBuilder.Label) ? string.Empty : $" to {uriBuilder.Label}")} {(string.IsNullOrEmpty(uriBuilder.Message) ? string.Empty : $" for {uriBuilder.Message}")}"
$"Payment {(string.IsNullOrEmpty(uriBuilder.Label) ? string.Empty : $" to <strong>{uriBuilder.Label}</strong>")} {(string.IsNullOrEmpty(uriBuilder.Message) ? string.Empty : $" for <strong>{uriBuilder.Message}</strong>")}"
});
}
@ -918,7 +930,7 @@ namespace BTCPayServer.Controllers
if (address is not null)
{
var addressLabels = await WalletRepository.GetWalletLabels(new WalletObjectId(walletId, WalletObjectData.Types.Address, address.ToString()));
vm.Outputs.Last().Labels = addressLabels.Select(tuple => tuple.Label).ToArray();
vm.Outputs.Last().Labels = vm.Outputs.Last().Labels.Concat(addressLabels.Select(tuple => tuple.Label)).ToArray();
}
}
@ -1336,7 +1348,7 @@ namespace BTCPayServer.Controllers
{
"csv" => "text/csv",
"json" => "application/json",
"bip329" => "text/jsonl", // https://stackoverflow.com/questions/59938644/what-is-the-mime-type-of-jsonl-files
"bip329" => "application/jsonl", // Ongoing discussion: https://github.com/wardi/jsonlines/issues/19
_ => throw new ArgumentOutOfRangeException(nameof(format), format, null)
};
var cd = new ContentDisposition

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

@ -289,7 +289,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
};
}
var proofBlob = new PayoutLightningBlob() { PaymentHash = bolt11PaymentRequest.PaymentHash.ToString() };
var proofBlob = new PayoutLightningBlob { PaymentHash = bolt11PaymentRequest.PaymentHash.ToString() };
try
{
var result = await lightningClient.Pay(bolt11PaymentRequest.ToString(),
@ -298,6 +298,8 @@ namespace BTCPayServer.Data.Payouts.LightningLike
// CLN does not support explicit amount param if it is the same as the invoice amount
Amount = payoutBlob.CryptoAmount == bolt11PaymentRequest.MinimumAmount.ToDecimal(LightMoneyUnit.BTC)? null: new LightMoney((decimal)payoutBlob.CryptoAmount, LightMoneyUnit.BTC)
}, cancellationToken);
if (result == null) throw new NoPaymentResultException();
string message = null;
if (result.Result == PayResult.Ok)
{
@ -330,7 +332,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
Message = message
};
}
catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException)
catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException or NoPaymentResultException)
{
// Timeout, potentially caused by hold invoices
// Payment will be saved as pending, the LightningPendingPayoutListener will handle settling/cancelling
@ -347,7 +349,6 @@ namespace BTCPayServer.Data.Payouts.LightningLike
}
}
private async Task SetStoreContext()
{
var storeId = HttpContext.GetUserPrefsCookie()?.CurrentStoreId;
@ -377,4 +378,8 @@ namespace BTCPayServer.Data.Payouts.LightningLike
public decimal Amount { get; set; }
}
}
public class NoPaymentResultException : Exception
{
}
}

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"}
};
@ -240,7 +241,7 @@ namespace BTCPayServer.Data
[DefaultValue(false)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public bool PlaySoundOnPayment { get; set; } = false;
public bool PlaySoundOnPayment { get; set; }
public string SoundFileId { get; set; }

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

@ -43,9 +43,9 @@ public class FormDataService
Field.Create("Address Line 1", "buyerAddress1", null, true, null),
Field.Create("Address Line 2", "buyerAddress2", null, false, null),
Field.Create("City", "buyerCity", null, true, null),
Field.Create("Postcode", "buyerZip", null, false, null),
Field.Create("Postcode", "buyerZip", null, true, null),
Field.Create("State", "buyerState", null, false, null),
new SelectField()
new SelectField
{
Name = "buyerCountry",
Label = "Country",
@ -218,4 +218,36 @@ public class FormDataService
}
return r;
}
public void SetValues(Form form, JObject values)
{
var fields = form.GetAllFields().ToDictionary(k => k.FullName, k => k.Field);
SetValues(fields, new List<string>(), values);
}
private void SetValues(Dictionary<string, Field> fields, List<string> path, JObject values)
{
foreach (var prop in values.Properties())
{
List<string> propPath = new List<string>(path.Count + 1);
propPath.AddRange(path);
propPath.Add(prop.Name);
if (prop.Value.Type == JTokenType.Object)
{
SetValues(fields, propPath, (JObject)prop.Value);
}
else if (prop.Value.Type == JTokenType.String)
{
var fullName = string.Join('_', propPath.Where(s => !string.IsNullOrEmpty(s)));
if (fields.TryGetValue(fullName, out var f) && !f.Constant)
{
if (_formProviders.TypeToComponentProvider.TryGetValue(f.Type, out var formComponentProvider))
{
formComponentProvider.SetValue(f, prop.Value);
}
}
}
}
}
}

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

@ -283,10 +283,10 @@ namespace BTCPayServer.Hosting
services.AddSingleton<IPluginHookService, PluginHookService>(provider => provider.GetService<PluginHookService>());
services.TryAddTransient<Safe>();
services.TryAddTransient<DisplayFormatter>();
services.TryAddSingleton<Ganss.XSS.HtmlSanitizer>(o =>
services.TryAddSingleton<Ganss.Xss.HtmlSanitizer>(o =>
{
var htmlSanitizer = new Ganss.XSS.HtmlSanitizer();
var htmlSanitizer = new Ganss.Xss.HtmlSanitizer();
htmlSanitizer.RemovingAtRule += (sender, args) =>
@ -309,7 +309,7 @@ namespace BTCPayServer.Hosting
{
if (args.Tag.TagName.Equals("img", StringComparison.InvariantCultureIgnoreCase) &&
args.Attribute.Name.Equals("src", StringComparison.InvariantCultureIgnoreCase) &&
args.Reason == Ganss.XSS.RemoveReason.NotAllowedUrlValue)
args.Reason == Ganss.Xss.RemoveReason.NotAllowedUrlValue)
{
args.Cancel = true;
}
@ -505,7 +505,7 @@ namespace BTCPayServer.Hosting
// We need to be careful to only add exchanges which OnGetTickers implementation make only 1 request
services.AddRateProviderExchangeSharp<ExchangeBinanceAPI>(new("binance", "Binance", "https://api.binance.com/api/v1/ticker/24hr"));
services.AddRateProviderExchangeSharp<ExchangeBittrexAPI>(new("bittrex", "Bittrex", "https://bittrex.com/api/v1.1/public/getmarketsummaries"));
services.AddRateProviderExchangeSharp<ExchangePoloniexAPI>(new("poloniex", "Poloniex", "https://poloniex.com/public?command=returnTicker"));
services.AddRateProviderExchangeSharp<ExchangePoloniexAPI>(new("poloniex", "Poloniex", " https://api.poloniex.com/markets/price"));
services.AddRateProviderExchangeSharp<ExchangeNDAXAPI>(new("ndax", "NDAX", "https://ndax.io/api/returnTicker"));
services.AddRateProviderExchangeSharp<ExchangeBitfinexAPI>(new("bitfinex", "Bitfinex", "https://api.bitfinex.com/v2/tickers?symbols=tBTCUSD,tLTCUSD,tLTCBTC,tETHUSD,tETHBTC,tETCBTC,tETCUSD,tRRTUSD,tRRTBTC,tZECUSD,tZECBTC,tXMRUSD,tXMRBTC,tDSHUSD,tDSHBTC,tBTCEUR,tBTCJPY,tXRPUSD,tXRPBTC,tIOTUSD,tIOTBTC,tIOTETH,tEOSUSD,tEOSBTC,tEOSETH,tSANUSD,tSANBTC,tSANETH,tOMGUSD,tOMGBTC,tOMGETH,tNEOUSD,tNEOBTC,tNEOETH,tETPUSD,tETPBTC,tETPETH,tQTMUSD,tQTMBTC,tQTMETH,tAVTUSD,tAVTBTC,tAVTETH,tEDOUSD,tEDOBTC,tEDOETH,tBTGUSD,tBTGBTC,tDATUSD,tDATBTC,tDATETH,tQSHUSD,tQSHBTC,tQSHETH,tYYWUSD,tYYWBTC,tYYWETH,tGNTUSD,tGNTBTC,tGNTETH,tSNTUSD,tSNTBTC,tSNTETH,tIOTEUR,tBATUSD,tBATBTC,tBATETH,tMNAUSD,tMNABTC,tMNAETH,tFUNUSD,tFUNBTC,tFUNETH,tZRXUSD,tZRXBTC,tZRXETH,tTNBUSD,tTNBBTC,tTNBETH,tSPKUSD,tSPKBTC,tSPKETH,tTRXUSD,tTRXBTC,tTRXETH,tRCNUSD,tRCNBTC,tRCNETH,tRLCUSD,tRLCBTC,tRLCETH,tAIDUSD,tAIDBTC,tAIDETH,tSNGUSD,tSNGBTC,tSNGETH,tREPUSD,tREPBTC,tREPETH,tELFUSD,tELFBTC,tELFETH,tNECUSD,tNECBTC,tNECETH,tBTCGBP,tETHEUR,tETHJPY,tETHGBP,tNEOEUR,tNEOJPY,tNEOGBP,tEOSEUR,tEOSJPY,tEOSGBP,tIOTJPY,tIOTGBP,tIOSUSD,tIOSBTC,tIOSETH,tAIOUSD,tAIOBTC,tAIOETH,tREQUSD,tREQBTC,tREQETH,tRDNUSD,tRDNBTC,tRDNETH,tLRCUSD,tLRCBTC,tLRCETH,tWAXUSD,tWAXBTC,tWAXETH,tDAIUSD,tDAIBTC,tDAIETH,tAGIUSD,tAGIBTC,tAGIETH,tBFTUSD,tBFTBTC,tBFTETH,tMTNUSD,tMTNBTC,tMTNETH,tODEUSD,tODEBTC,tODEETH,tANTUSD,tANTBTC,tANTETH,tDTHUSD,tDTHBTC,tDTHETH,tMITUSD,tMITBTC,tMITETH,tSTJUSD,tSTJBTC,tSTJETH,tXLMUSD,tXLMEUR,tXLMJPY,tXLMGBP,tXLMBTC,tXLMETH,tXVGUSD,tXVGEUR,tXVGJPY,tXVGGBP,tXVGBTC,tXVGETH,tBCIUSD,tBCIBTC,tMKRUSD,tMKRBTC,tMKRETH,tKNCUSD,tKNCBTC,tKNCETH,tPOAUSD,tPOABTC,tPOAETH,tEVTUSD,tLYMUSD,tLYMBTC,tLYMETH,tUTKUSD,tUTKBTC,tUTKETH,tVEEUSD,tVEEBTC,tVEEETH,tDADUSD,tDADBTC,tDADETH,tORSUSD,tORSBTC,tORSETH,tAUCUSD,tAUCBTC,tAUCETH,tPOYUSD,tPOYBTC,tPOYETH,tFSNUSD,tFSNBTC,tFSNETH,tCBTUSD,tCBTBTC,tCBTETH,tZCNUSD,tZCNBTC,tZCNETH,tSENUSD,tSENBTC,tSENETH,tNCAUSD,tNCABTC,tNCAETH,tCNDUSD,tCNDBTC,tCNDETH,tCTXUSD,tCTXBTC,tCTXETH,tPAIUSD,tPAIBTC,tSEEUSD,tSEEBTC,tSEEETH,tESSUSD,tESSBTC,tESSETH,tATMUSD,tATMBTC,tATMETH,tHOTUSD,tHOTBTC,tHOTETH,tDTAUSD,tDTABTC,tDTAETH,tIQXUSD,tIQXBTC,tIQXEOS,tWPRUSD,tWPRBTC,tWPRETH,tZILUSD,tZILBTC,tZILETH,tBNTUSD,tBNTBTC,tBNTETH,tABSUSD,tABSETH,tXRAUSD,tXRAETH,tMANUSD,tMANETH,tBBNUSD,tBBNETH,tNIOUSD,tNIOETH,tDGXUSD,tDGXETH,tVETUSD,tVETBTC,tVETETH,tUTNUSD,tUTNETH,tTKNUSD,tTKNETH,tGOTUSD,tGOTEUR,tGOTETH,tXTZUSD,tXTZBTC,tCNNUSD,tCNNETH,tBOXUSD,tBOXETH,tTRXEUR,tTRXGBP,tTRXJPY,tMGOUSD,tMGOETH,tRTEUSD,tRTEETH,tYGGUSD,tYGGETH,tMLNUSD,tMLNETH,tWTCUSD,tWTCETH,tCSXUSD,tCSXETH,tOMNUSD,tOMNBTC,tINTUSD,tINTETH,tDRNUSD,tDRNETH,tPNKUSD,tPNKETH,tDGBUSD,tDGBBTC,tBSVUSD,tBSVBTC,tBABUSD,tBABBTC,tWLOUSD,tWLOXLM,tVLDUSD,tVLDETH,tENJUSD,tENJETH,tONLUSD,tONLETH,tRBTUSD,tRBTBTC,tUSTUSD,tEUTEUR,tEUTUSD,tGSDUSD,tUDCUSD,tTSDUSD,tPAXUSD,tRIFUSD,tRIFBTC,tPASUSD,tPASETH,tVSYUSD,tVSYBTC,tZRXDAI,tMKRDAI,tOMGDAI,tBTTUSD,tBTTBTC,tBTCUST,tETHUST,tCLOUSD,tCLOBTC,tIMPUSD,tIMPETH,tLTCUST,tEOSUST,tBABUST,tSCRUSD,tSCRETH,tGNOUSD,tGNOETH,tGENUSD,tGENETH,tATOUSD,tATOBTC,tATOETH,tWBTUSD,tXCHUSD,tEUSUSD,tWBTETH,tXCHETH,tEUSETH,tLEOUSD,tLEOBTC,tLEOUST,tLEOEOS,tLEOETH,tASTUSD,tASTETH,tFOAUSD,tFOAETH,tUFRUSD,tUFRETH,tZBTUSD,tZBTUST,tOKBUSD,tUSKUSD,tGTXUSD,tKANUSD,tOKBUST,tOKBETH,tOKBBTC,tUSKUST,tUSKETH,tUSKBTC,tUSKEOS,tGTXUST,tKANUST,tAMPUSD,tALGUSD,tALGBTC,tALGUST,tBTCXCH,tSWMUSD,tSWMETH,tTRIUSD,tTRIETH,tLOOUSD,tLOOETH,tAMPUST,tDUSK:USD,tDUSK:BTC,tUOSUSD,tUOSBTC,tRRBUSD,tRRBUST,tDTXUSD,tDTXUST,tAMPBTC,tFTTUSD,tFTTUST,tPAXUST,tUDCUST,tTSDUST,tBTC:CNHT,tUST:CNHT,tCNH:CNHT,tCHZUSD,tCHZUST,tBTCF0:USTF0,tETHF0:USTF0"));
@ -527,7 +527,6 @@ namespace BTCPayServer.Hosting
services.AddRateProvider<YadioRateProvider>();
services.AddRateProvider<BtcTurkRateProvider>();
services.AddRateProvider<FreeCurrencyRatesRateProvider>();
services.AddRateProvider<ExchangeRateHostRateProvider>();
// Broken
// Providers.Add("argoneum", new ArgoneumRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_ARGONEUM")));

View File

@ -1,24 +0,0 @@
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
namespace BTCPayServer
{
public class HwiWebSocketTransport : Hwi.Transports.ITransport
{
private readonly WebSocketHelper _webSocket;
public HwiWebSocketTransport(WebSocket webSocket)
{
_webSocket = new WebSocketHelper(webSocket);
}
public async Task<string> SendCommandAsync(string[] arguments, CancellationToken cancel)
{
JObject request = new JObject();
request.Add("params", new JArray(arguments));
await _webSocket.Send(request.ToString(), cancel);
return await _webSocket.NextMessageAsync(cancel);
}
}
}

View File

@ -44,9 +44,8 @@ namespace BTCPayServer.Models.InvoicingModels
{
get; set;
}
[DisplayName("POS Data")]
public string PosData
[DisplayName("Metadata")]
public string Metadata
{
get; set;
}

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

@ -72,7 +72,11 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
[Display(Name = "Expiration Date")]
public DateTime? ExpiryDate { get; set; }
[Required] public string Title { get; set; }
[Required]
public string Title { get; set; }
[Display(Name = "Memo")]
public string Description { get; set; }
[Display(Name = "Store")]
@ -87,7 +91,8 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
[Display(Name = "Custom CSS Code")]
public string EmbeddedCSS { get; set; }
[Display(Name = "Allow payee to create invoices in their own denomination")]
[Display(Name = "Allow payee to create invoices with custom amounts")]
public bool AllowCustomPaymentAmounts { get; set; }
public Dictionary<string, object> FormResponse { get; set; }
@ -151,6 +156,7 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
public string CssFileId { get; set; }
public string BrandColor { get; set; }
public string StoreName { get; set; }
public string StoreWebsite { get; set; }
public string EmbeddedCSS { get; set; }
public string CustomCSSLink { get; set; }
@ -208,6 +214,7 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
{
public string PaymentMethod { get; set; }
public decimal Amount { get; set; }
public string AmountFormatted { get; set; }
public string RateFormatted { get; set; }
public decimal Paid { get; set; }
public string PaidFormatted { get; set; }

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

@ -98,9 +98,13 @@ namespace BTCPayServer.Models.WalletViewModels
[MaxLength(30)]
public string Name { get; set; }
[Display(Name = "Memo")]
public string Description { get; set; }
[Display(Name = "Custom CSS URL")]
public string CustomCSSLink { get; set; }
[Display(Name = "Custom CSS Code")]
public string EmbeddedCSS { get; set; }
}

View File

@ -135,6 +135,7 @@ namespace BTCPayServer.PaymentRequest
Amount = paymentEntity.PaidAmount.Gross,
Paid = paymentEntity.InvoicePaidAmount.Net,
ReceivedDate = paymentEntity.ReceivedTime.DateTime,
AmountFormatted = _displayFormatter.Currency(paymentEntity.PaidAmount.Gross, paymentEntity.PaidAmount.Currency),
PaidFormatted = _displayFormatter.Currency(paymentEntity.InvoicePaidAmount.Net, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol),
RateFormatted = _displayFormatter.Currency(paymentEntity.Rate, blob.Currency, DisplayFormatter.CurrencyFormat.Symbol),
PaymentMethod = paymentMethodId.ToPrettyString(),

View File

@ -15,7 +15,7 @@ using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using Ganss.XSS;
using Ganss.Xss;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

View File

@ -95,6 +95,8 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
ViewType = (PosViewType)viewType,
ShowCustomAmount = settings.ShowCustomAmount,
ShowDiscount = settings.ShowDiscount,
ShowSearch = settings.ShowSearch,
ShowCategories = settings.ShowCategories,
EnableTips = settings.EnableTips,
CurrencyCode = settings.Currency,
CurrencySymbol = numberFormatInfo.CurrencySymbol,
@ -277,7 +279,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
formResponseJObject = TryParseJObject(formResponse) ?? new JObject();
var form = Form.Parse(formData.Config);
form.SetValues(formResponseJObject);
FormDataService.SetValues(form, formResponseJObject);
if (!FormDataService.Validate(form, ModelState))
{
//someone tried to bypass validation
@ -382,7 +384,8 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
}
if (appPosData.Tip > 0)
{
receiptData.Add("Tip", _displayFormatter.Currency(appPosData.Tip, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol));
var tipFormatted = _displayFormatter.Currency(appPosData.Tip, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol);
receiptData.Add("Tip", appPosData.TipPercentage > 0 ? $"{appPosData.TipPercentage}% = {tipFormatted}" : tipFormatted);
}
receiptData.Add("Total", _displayFormatter.Currency(appPosData.Total, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol));
}
@ -542,6 +545,8 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
DefaultView = settings.DefaultView,
ShowCustomAmount = settings.ShowCustomAmount,
ShowDiscount = settings.ShowDiscount,
ShowSearch = settings.ShowSearch,
ShowCategories = settings.ShowCategories,
EnableTips = settings.EnableTips,
Currency = settings.Currency,
Template = settings.Template,
@ -630,6 +635,8 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
DefaultView = vm.DefaultView,
ShowCustomAmount = vm.ShowCustomAmount,
ShowDiscount = vm.ShowDiscount,
ShowSearch = vm.ShowSearch,
ShowCategories = vm.ShowCategories,
EnableTips = vm.EnableTips,
Currency = vm.Currency,
Template = vm.Template,

View File

@ -31,6 +31,10 @@ namespace BTCPayServer.Plugins.PointOfSale.Models
public bool ShowCustomAmount { get; set; }
[Display(Name = "User can input discount in %")]
public bool ShowDiscount { get; set; }
[Display(Name = "Display the search bar")]
public bool ShowSearch { get; set; }
[Display(Name = "Display the category list")]
public bool ShowCategories { get; set; }
[Display(Name = "Enable tips")]
public bool EnableTips { get; set; }
public string Example1 { get; internal set; }

View File

@ -65,6 +65,8 @@ namespace BTCPayServer.Plugins.PointOfSale.Models
public PosViewType ViewType { get; set; }
public bool ShowCustomAmount { get; set; }
public bool ShowDiscount { get; set; }
public bool ShowSearch { get; set; } = true;
public bool ShowCategories { get; set; } = true;
public bool EnableTips { get; set; }
public string Step { get; set; }
public string Title { get; set; }

View File

@ -13,7 +13,7 @@ using BTCPayServer.Plugins.PointOfSale.Controllers;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using Ganss.XSS;
using Ganss.Xss;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

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

@ -15,7 +15,7 @@ using BTCPayServer.Plugins.PointOfSale.Models;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using Ganss.XSS;
using Ganss.Xss;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using NBitcoin.DataEncoders;
@ -253,6 +253,7 @@ namespace BTCPayServer.Services.Apps
public async Task<ListAppsViewModel.ListAppViewModel[]> GetAllApps(string? userId, bool allowNoUser = false, string? storeId = null, bool includeArchived = false)
{
await using var ctx = _ContextFactory.CreateContext();
var types = GetAvailableAppTypes().Select(at => at.Key).ToHashSet();
var listApps = (await ctx.UserStore
.Where(us =>
(allowNoUser && string.IsNullOrEmpty(userId) || us.ApplicationUserId == userId) &&
@ -260,7 +261,7 @@ namespace BTCPayServer.Services.Apps
.Include(store => store.StoreRole)
.Include(store => store.StoreData)
.Join(ctx.Apps, us => us.StoreDataId, app => app.StoreDataId, (us, app) => new { us, app })
.Where(b => !b.app.Archived || b.app.Archived == includeArchived)
.Where(b => types.Contains(b.app.AppType) && (!b.app.Archived || b.app.Archived == includeArchived))
.OrderBy(b => b.app.Created)
.ToArrayAsync()).Select(arg => new ListAppsViewModel.ListAppViewModel
{
@ -311,9 +312,10 @@ namespace BTCPayServer.Services.Apps
public async Task<List<AppData>> GetApps(string[] appIds, bool includeStore = false, bool includeArchived = false)
{
await using var ctx = _ContextFactory.CreateContext();
var types = GetAvailableAppTypes().Select(at => at.Key);
var query = ctx.Apps
.Where(app => appIds.Contains(app.Id))
.Where(app => !app.Archived || app.Archived == includeArchived);
.Where(app => types.Contains(app.AppType) && (!app.Archived || app.Archived == includeArchived));
if (includeStore)
{
query = query.Include(data => data.StoreData);
@ -332,9 +334,10 @@ namespace BTCPayServer.Services.Apps
public async Task<AppData?> GetApp(string appId, string? appType, bool includeStore = false, bool includeArchived = false)
{
await using var ctx = _ContextFactory.CreateContext();
var types = GetAvailableAppTypes().Select(at => at.Key);
var query = ctx.Apps
.Where(us => us.Id == appId && (appType == null || us.AppType == appType))
.Where(app => !app.Archived || app.Archived == includeArchived);
.Where(app => types.Contains(app.AppType) && (!app.Archived || app.Archived == includeArchived));
if (includeStore)
{
query = query.Include(data => data.StoreData);

View File

@ -77,6 +77,8 @@ namespace BTCPayServer.Services.Apps
DefaultView = PosViewType.Static;
ShowCustomAmount = false;
ShowDiscount = true;
ShowSearch = true;
ShowCategories = true;
EnableTips = true;
RequiresRefundEmail = RequiresRefundEmail.InheritFromStore;
}
@ -87,6 +89,8 @@ namespace BTCPayServer.Services.Apps
public PosViewType DefaultView { get; set; }
public bool ShowCustomAmount { get; set; }
public bool ShowDiscount { get; set; }
public bool ShowSearch { get; set; } = true;
public bool ShowCategories { get; set; } = true;
public bool EnableTips { get; set; }
public RequiresRefundEmail RequiresRefundEmail { get; set; }

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

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Globalization;
using System.Linq;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client.Models;
@ -386,7 +387,7 @@ namespace BTCPayServer.Services.Invoices
}
public void UpdateTotals()
{
Rates = new Dictionary<string, decimal>();
Rates = new Dictionary<string, decimal>(StringComparer.InvariantCultureIgnoreCase);
foreach (var p in GetPaymentMethods())
{
Rates.TryAdd(p.Currency, p.Rate);
@ -591,14 +592,14 @@ namespace BTCPayServer.Services.Invoices
cryptoInfo.CryptoCode = cryptoCode;
cryptoInfo.PaymentType = info.GetId().PaymentType.ToString();
cryptoInfo.Rate = info.Rate;
cryptoInfo.Price = subtotalPrice.ToString();
cryptoInfo.Price = subtotalPrice.ToString(CultureInfo.InvariantCulture);
cryptoInfo.Due = accounting.Due.ToString();
cryptoInfo.Paid = accounting.Paid.ToString();
cryptoInfo.TotalDue = accounting.TotalDue.ToString();
cryptoInfo.NetworkFee = accounting.NetworkFee.ToString();
cryptoInfo.Due = accounting.Due.ToString(CultureInfo.InvariantCulture);
cryptoInfo.Paid = accounting.Paid.ToString(CultureInfo.InvariantCulture);
cryptoInfo.TotalDue = accounting.TotalDue.ToString(CultureInfo.InvariantCulture);
cryptoInfo.NetworkFee = accounting.NetworkFee.ToString(CultureInfo.InvariantCulture);
cryptoInfo.TxCount = accounting.TxCount;
cryptoInfo.CryptoPaid = accounting.CryptoPaid.ToString();
cryptoInfo.CryptoPaid = accounting.CryptoPaid.ToString(CultureInfo.InvariantCulture);
cryptoInfo.Address = address;
@ -978,7 +979,15 @@ namespace BTCPayServer.Services.Invoices
}
public override string ToString()
{
return Status.ToModernStatus() + (ExceptionStatus == InvoiceExceptionStatus.None ? string.Empty : $" ({ToString(ExceptionStatus)})");
return Status.ToModernStatus() + ExceptionStatus switch
{
InvoiceExceptionStatus.PaidOver => " (paid over)",
InvoiceExceptionStatus.PaidLate => " (paid late)",
InvoiceExceptionStatus.PaidPartial => " (paid partial)",
InvoiceExceptionStatus.Marked => " (marked)",
InvoiceExceptionStatus.Invalid => " (invalid)",
_ => ""
};
}
}

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

@ -3,14 +3,9 @@ using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using Dapper;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using NBXplorer.DerivationStrategy;
using static Microsoft.EntityFrameworkCore.DbLoggerCategory.Database;
namespace BTCPayServer.Services.Reporting;
@ -27,42 +22,42 @@ public class OnChainWalletReportProvider : ReportProvider
NetworkProvider = networkProvider;
WalletRepository = walletRepository;
}
public NBXplorerConnectionFactory NbxplorerConnectionFactory { get; }
public StoreRepository StoreRepository { get; }
public BTCPayNetworkProvider NetworkProvider { get; }
public WalletRepository WalletRepository { get; }
public override string Name => "On-Chain Wallets";
private NBXplorerConnectionFactory NbxplorerConnectionFactory { get; }
private StoreRepository StoreRepository { get; }
private BTCPayNetworkProvider NetworkProvider { get; }
private WalletRepository WalletRepository { get; }
public override string Name => "Wallets";
ViewDefinition CreateViewDefinition()
{
return
new()
return new()
{
Fields =
{
Fields =
new ("Date", "datetime"),
new ("Crypto", "string"),
// For proper rendering of explorer links, Crypto should always be before tx_id
new ("TransactionId", "tx_id"),
new ("InvoiceId", "invoice_id"),
new ("Confirmed", "boolean"),
new ("BalanceChange", "amount")
},
Charts =
{
new ()
{
new ("Date", "datetime"),
new ("Crypto", "string"),
// For proper rendering of explorer links, Crypto should always be before tx_id
new ("TransactionId", "tx_id"),
new ("InvoiceId", "invoice_id"),
new ("Confirmed", "boolean"),
new ("BalanceChange", "decimal")
},
Charts =
{
new ()
{
Name = "Group by Crypto",
Totals = { "Crypto" },
Groups = { "Crypto", "Confirmed" },
Aggregates = { "BalanceChange" }
}
Name = "Group by Crypto",
Totals = { "Crypto" },
Groups = { "Crypto", "Confirmed" },
Aggregates = { "BalanceChange" }
}
};
}
};
}
public override bool IsAvailable()
{
return this.NbxplorerConnectionFactory.Available;
return NbxplorerConnectionFactory.Available;
}
public override async Task Query(QueryContext queryContext, CancellationToken cancellation)
@ -79,14 +74,15 @@ public class OnChainWalletReportProvider : ReportProvider
var command = new CommandDefinition(
commandText:
"SELECT r.tx_id, r.seen_at, t.blk_id, t.blk_height, r.balance_change " +
"FROM get_wallets_recent(@wallet_id, @code, @interval, NULL, NULL) r " +
"FROM get_wallets_recent(@wallet_id, @code, @asset_id, @interval, NULL, NULL) r " +
"JOIN txs t USING (code, tx_id) " +
"ORDER BY r.seen_at",
parameters: new
{
asset_id = GetAssetId(settings.Network),
wallet_id = NBXplorer.Client.DBUtils.nbxv1_get_wallet_id(settings.Network.CryptoCode, settings.AccountDerivation.ToString()),
code = settings.Network.CryptoCode,
interval = interval
interval
},
cancellationToken: cancellation);
@ -97,14 +93,15 @@ public class OnChainWalletReportProvider : ReportProvider
if (date > queryContext.To)
continue;
var values = queryContext.AddData();
values.Add((DateTimeOffset)date);
var balanceChange = Money.Satoshis((long)r.balance_change).ToDecimal(MoneyUnit.BTC);
values.Add(date);
values.Add(settings.Network.CryptoCode);
values.Add((string)r.tx_id);
values.Add(null);
values.Add((long?)r.blk_height is not null);
values.Add(Money.Satoshis((long)r.balance_change).ToDecimal(MoneyUnit.BTC));
values.Add(new FormattedAmount(balanceChange, settings.Network.Divisibility).ToJObject());
}
var objects = await WalletRepository.GetWalletObjects(new GetWalletObjectsQuery()
var objects = await WalletRepository.GetWalletObjects(new GetWalletObjectsQuery
{
Ids = queryContext.Data.Select(d => (string)d[2]!).ToArray(),
WalletId = walletId,
@ -119,4 +116,17 @@ public class OnChainWalletReportProvider : ReportProvider
}
}
}
private string? GetAssetId(BTCPayNetwork network)
{
#if ALTCOINS
if (network is ElementsBTCPayNetwork elNetwork)
{
if (elNetwork.CryptoCode == elNetwork.NetworkCryptoCode)
return "";
return elNetwork.AssetId.ToString();
}
#endif
return null;
}
}

Some files were not shown because too many files have changed in this diff Show More