Compare commits

..

12 Commits

Author SHA1 Message Date
af44d6aeac Partially Revert "Adapt controller and partially fix tests"
This reverts commit d4828f8d0edb0878ed1cca5cbafc061b3a46e2f0.
2023-06-26 10:48:27 +02:00
d99bd28386 prevent explicit rate if modify invoice is not available 2023-06-26 10:40:20 +02:00
86a44b6f1e make sure i is not stale 2023-06-26 10:22:05 +02:00
f1e5f1b759 try fix 2023-06-26 10:22:05 +02:00
d4828f8d0e Adapt controller and partially fix tests 2023-06-23 17:53:01 +02:00
a512cb90d4 Fix test and build warnings 2023-06-23 15:53:52 +02:00
b9a96ebb63 Merge branch 'master' into better-lnurl 2023-06-23 12:18:36 +02:00
351702930c Merge branch 'master' into better-lnurl 2023-06-21 14:05:37 +02:00
7f26a97eab fixes 2023-06-21 10:17:46 +02:00
b021039d87 cleanup 2023-06-21 09:56:02 +02:00
623d7e3056 reduce code 2023-06-21 09:17:13 +02:00
33f1f956f7 Do not create an invoice on every lnurl query 2023-06-20 13:21:00 +02:00
377 changed files with 6259 additions and 11448 deletions
BTCPayServer.Abstractions
BTCPayServer.Client
BTCPayServer.Common
BTCPayServer.Data
BTCPayServer.Rating
BTCPayServer.Tests
BTCPayServer
BTCPayServer.csproj
Blazor
Components
Controllers
Data
Extensions.cs
Extensions
FileTypeDetector.cs
Forms
HostedServices
Hosting
Models
PaymentRequest
Payments
PayoutProcessors
Plugins
Program.csSearchString.cs
Services
Storage
TagHelpers
Views
Shared
UIAccount
UIApps
UIForms
UIInvoice
UILightningAutomatedPayoutProcessors
UIMoneroLikeStore
UIOnChainAutomatedPayoutProcessors
UIPaymentRequest
UIPullPayment
UIReports
UIServer
UIStorePullPayments
UIStores
UIUserStores
UIWallets
wwwroot
Build
Changelog.mdREADME.mdbtcpayserver.sln.DotSettings

@ -31,7 +31,7 @@
<None Include="icon.png" Pack="true" PackagePath="\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="HtmlSanitizer" Version="8.0.723" />
<PackageReference Include="HtmlSanitizer" Version="5.0.372" />
<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" />

@ -1,4 +1,3 @@
using System;
using System.Threading.Tasks;
namespace BTCPayServer.Abstractions.Contracts
@ -7,8 +6,5 @@ namespace BTCPayServer.Abstractions.Contracts
{
Task ApplyAction(string hook, object args);
Task<object> ApplyFilter(string hook, object args);
event EventHandler<(string hook, object args)> ActionInvoked;
event EventHandler<(string hook, object args)> FilterInvoked;
}
}

@ -105,7 +105,31 @@ public class Form
}
}
public void SetValues(JObject values)
{
var fields = GetAllFields().ToDictionary(k => k.FullName, k => k.Field);
SetValues(fields, new List<string>(), values);
}
private void SetValues(Dictionary<string, Field> fields, List<string> path, JObject values)
{
foreach (var prop in values.Properties())
{
List<string> propPath = new List<string>(path.Count + 1);
propPath.AddRange(path);
propPath.Add(prop.Name);
if (prop.Value.Type == JTokenType.Object)
{
SetValues(fields, propPath, (JObject)prop.Value);
}
else if (prop.Value.Type == JTokenType.String)
{
var fullName = string.Join('_', propPath.Where(s => !string.IsNullOrEmpty(s)));
if (fields.TryGetValue(fullName, out var f) && !f.Constant)
f.Value = prop.Value.Value<string>();
}
}
}
}

@ -1,5 +1,4 @@
using System.Web;
using Ganss.Xss;
using Ganss.XSS;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
@ -22,11 +21,6 @@ namespace BTCPayServer.Abstractions.Services
{
return _htmlHelper.Raw(_htmlSanitizer.Sanitize(value));
}
public IHtmlContent RawEncode(string value)
{
return _htmlHelper.Raw(HttpUtility.HtmlEncode(_htmlSanitizer.Sanitize(value)));
}
public IHtmlContent Json(object model)
{

@ -12,11 +12,13 @@ public class PermissionTagHelper : TagHelper
{
private readonly IAuthorizationService _authorizationService;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger<PermissionTagHelper> _logger;
public PermissionTagHelper(IAuthorizationService authorizationService, IHttpContextAccessor httpContextAccessor)
public PermissionTagHelper(IAuthorizationService authorizationService, IHttpContextAccessor httpContextAccessor, ILogger<PermissionTagHelper> logger)
{
_authorizationService = authorizationService;
_httpContextAccessor = httpContextAccessor;
_logger = logger;
}
public string Permission { get; set; }

@ -16,7 +16,7 @@
<Platforms>AnyCPU</Platforms>
</PropertyGroup>
<PropertyGroup>
<Version Condition=" '$(Version)' == '' ">1.7.3</Version>
<Version Condition=" '$(Version)' == '' ">1.7.2</Version>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<PublishRepositoryUrl>true</PublishRepositoryUrl>
@ -32,7 +32,7 @@
<ItemGroup>
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.3.21" />
<PackageReference Include="NBitcoin" Version="7.0.24" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
<ItemGroup>
<None Include="icon.png" Pack="true" PackagePath="\" />

@ -55,7 +55,7 @@ namespace BTCPayServer.Client
}
public virtual async Task<IEnumerable<OnChainWalletTransactionData>> ShowOnChainWalletTransactions(
string storeId, string cryptoCode, TransactionStatus[] statusFilter = null, string labelFilter = null, int skip = 0,
string storeId, string cryptoCode, TransactionStatus[] statusFilter = null, string labelFilter = null,
CancellationToken token = default)
{
var query = new Dictionary<string, object>();
@ -67,10 +67,6 @@ namespace BTCPayServer.Client
{
query.Add(nameof(labelFilter), labelFilter);
}
if (skip != 0)
{
query.Add(nameof(skip), skip);
}
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions", query), token);

@ -37,7 +37,6 @@ namespace BTCPayServer.Client.Models
public string RedirectUrl { get; set; } = null;
public bool? RedirectAutomatically { get; set; } = null;
public bool? RequiresRefundEmail { get; set; } = null;
public bool? Archived { get; set; } = null;
public string FormId { get; set; } = null;
public string EmbeddedCSS { get; set; } = null;
public CheckoutType? CheckoutType { get; set; } = null;
@ -79,7 +78,6 @@ namespace BTCPayServer.Client.Models
public bool? DisplayPerksValue { get; set; } = null;
public bool? DisplayPerksRanking { get; set; } = null;
public bool? SortPerksByPopularity { get; set; } = null;
public bool? Archived { get; set; } = null;
public string[] Sounds { get; set; } = null;
public string[] AnimationColors { get; set; } = null;
}

@ -1,11 +1,8 @@
#nullable enable
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models;
public class CreatePayoutThroughStoreRequest : CreatePayoutRequest
{
public string? PullPaymentId { get; set; }
public bool? Approved { get; set; }
public JObject? Metadata { get; set; }
}

@ -87,6 +87,7 @@ namespace BTCPayServer.Client.Models
public string DefaultLanguage { get; set; }
public CheckoutType? CheckoutType { get; set; }
public bool? LazyPaymentMethods { get; set; }
public string ExplicitRateScript { get; set; }
}
}
public class InvoiceData : InvoiceDataBase

@ -1,12 +1,12 @@
namespace BTCPayServer.Client.Models;
public enum InvoiceExceptionStatus
namespace BTCPayServer.Client.Models
{
None,
PaidLate,
PaidPartial,
Marked,
Invalid,
PaidOver
public enum InvoiceExceptionStatus
{
None,
PaidLate,
PaidPartial,
Marked,
Invalid,
PaidOver
}
}

@ -10,9 +10,4 @@ public class LightningAutomatedPayoutSettings
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
public TimeSpan IntervalSeconds { get; set; }
public int? CancelPayoutAfterFailures { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public bool ProcessNewPayoutsInstantly { get; set; }
}

@ -12,8 +12,4 @@ public class OnChainAutomatedPayoutSettings
public TimeSpan IntervalSeconds { get; set; }
public int? FeeBlockTarget { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public decimal Threshold { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public bool ProcessNewPayoutsInstantly { get; set; }
}

@ -7,7 +7,7 @@ namespace BTCPayServer.Client.Models
public class PaymentRequestData : PaymentRequestBaseData
{
[JsonConverter(typeof(StringEnumConverter))]
public PaymentRequestStatus Status { get; set; }
public PaymentRequestData.PaymentRequestStatus Status { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset CreatedTime { get; set; }
public string Id { get; set; }
@ -16,8 +16,7 @@ namespace BTCPayServer.Client.Models
{
Pending = 0,
Completed = 1,
Expired = 2,
Processing = 3
Expired = 2
}
}
}

@ -31,6 +31,5 @@ namespace BTCPayServer.Client.Models
public PayoutState State { get; set; }
public int Revision { get; set; }
public JObject PaymentProof { get; set; }
public JObject Metadata { get; set; }
}
}

@ -9,8 +9,6 @@ namespace BTCPayServer.Client.Models
public string AppType { get; set; }
public string Name { get; set; }
public string StoreId { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public bool? Archived { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset Created { get; set; }
}

@ -45,9 +45,6 @@ namespace BTCPayServer.Client.Models
public bool LazyPaymentMethods { get; set; }
public bool RedirectAutomatically { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public bool Archived { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public bool ShowRecommendedFee { get; set; } = true;
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
@ -73,17 +70,6 @@ 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; }

@ -1,62 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models;
public class StoreReportRequest
{
public string ViewName { get; set; }
public TimePeriod TimePeriod { get; set; }
}
public class StoreReportResponse
{
public class Field
{
public Field()
{
}
public Field(string name, string type)
{
Name = name;
Type = type;
}
public string Name { get; set; }
public string Type { get; set; }
}
public IList<Field> Fields { get; set; } = new List<Field>();
public List<JArray> Data { get; set; }
public DateTimeOffset From { get; set; }
public DateTimeOffset To { get; set; }
public List<ChartDefinition> Charts { get; set; }
public int GetIndex(string fieldName)
{
return Fields.ToList().FindIndex(f => f.Name == fieldName);
}
}
public class ChartDefinition
{
public string Name { get; set; }
public List<string> Groups { get; set; } = new List<string>();
public List<string> Totals { get; set; } = new List<string>();
public bool HasGrandTotal { get; set; }
public List<string> Aggregates { get; set; } = new List<string>();
public List<string> Filters { get; set; } = new List<string>();
}
public class TimePeriod
{
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset? From { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset? To { get; set; }
}

@ -1,16 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BTCPayServer.Client.Models
{
public class StoreReportsResponse
{
public string ViewName { get; set; }
public StoreReportResponse.Field[] Fields
{
get;
set;
}
}
}

@ -33,7 +33,6 @@ namespace BTCPayServer.Client
public const string CanManageUsers = "btcpay.server.canmanageusers";
public const string CanDeleteUser = "btcpay.user.candeleteuser";
public const string CanManagePullPayments = "btcpay.store.canmanagepullpayments";
public const string CanArchivePullPayments = "btcpay.store.canarchivepullpayments";
public const string CanCreatePullPayments = "btcpay.store.cancreatepullpayments";
public const string CanCreateNonApprovedPullPayments = "btcpay.store.cancreatenonapprovedpullpayments";
public const string CanViewCustodianAccounts = "btcpay.store.canviewcustodianaccounts";
@ -70,7 +69,6 @@ namespace BTCPayServer.Client
yield return CanViewLightningInvoiceInStore;
yield return CanCreateLightningInvoiceInStore;
yield return CanManagePullPayments;
yield return CanArchivePullPayments;
yield return CanCreatePullPayments;
yield return CanCreateNonApprovedPullPayments;
yield return CanViewCustodianAccounts;
@ -255,7 +253,7 @@ namespace BTCPayServer.Client
Policies.CanUseLightningNodeInStore);
PolicyHasChild(policyMap,Policies.CanManageUsers, Policies.CanCreateUser);
PolicyHasChild(policyMap,Policies.CanManagePullPayments, Policies.CanCreatePullPayments, Policies.CanArchivePullPayments);
PolicyHasChild(policyMap,Policies.CanManagePullPayments, Policies.CanCreatePullPayments);
PolicyHasChild(policyMap,Policies.CanCreatePullPayments, Policies.CanCreateNonApprovedPullPayments);
PolicyHasChild(policyMap,Policies.CanModifyPaymentRequests, Policies.CanViewPaymentRequests);
PolicyHasChild(policyMap,Policies.CanModifyProfile, Policies.CanViewProfile);

@ -16,7 +16,7 @@ namespace BTCPayServer
DefaultRateRules = new[]
{
"BTG_X = BTG_BTC * BTC_X",
"BTG_BTC = gate(BTG_BTC)",
"BTG_BTC = bitfinex(BTG_BTC)",
},
CryptoImagePath = "imlegacy/btg.svg",
LightningImagePath = "imlegacy/btg-lightning.svg",

@ -17,7 +17,7 @@ namespace BTCPayServer
DefaultRateRules = new[]
{
"BTX_X = BTX_BTC * BTC_X",
"BTX_BTC = graviex(BTX_BTC)"
"BTX_BTC = hitbtc(BTX_BTC)"
},
CryptoImagePath = "imlegacy/bitcore.svg",
LightningImagePath = "imlegacy/bitcore-lightning.svg",

@ -0,0 +1,32 @@
using NBitcoin;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitChaincoin()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("CHC");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Chaincoin",
BlockExplorerLink = NetworkType == ChainName.Mainnet
? "https://explorer.chaincoin.org/Explorer/Transaction/{0}"
: "https://test.explorer.chaincoin.org/Explorer/Transaction/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
DefaultRateRules = new[]
{
"CHC_X = CHC_BTC * BTC_X",
"CHC_BTC = txbit(CHC_X)"
},
CryptoImagePath = "imlegacy/chaincoin.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
//https://github.com/satoshilabs/slips/blob/master/slip-0044.md
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("711'")
: new KeyPath("1'")
});
}
}
}

@ -19,7 +19,7 @@ namespace BTCPayServer
"USDT_X = USDT_BTC * BTC_X",
"USDT_BTC = bitfinex(UST_BTC)",
},
AssetId = NetworkType == ChainName.Regtest? null: new uint256("ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2"),
AssetId = 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 = NetworkType == ChainName.Regtest? null: new uint256("aa775044c32a7df391902b3659f46dfe004ccb2644ce2ddc7dba31e889391caf"),
AssetId = new uint256("aa775044c32a7df391902b3659f46dfe004ccb2644ce2ddc7dba31e889391caf"),
DisplayName = "Ethiopian Birr",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://liquid.network/tx/{0}" : "https://liquid.network/testnet/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
@ -63,9 +63,8 @@ namespace BTCPayServer
"LCAD_CAD = 1",
"LCAD_X = CAD_BTC * BTC_X",
"LCAD_BTC = bylls(CAD_BTC)",
"CAD_BTC = LCAD_BTC"
},
AssetId = NetworkType == ChainName.Regtest? null: new uint256("0e99c1a6da379d1f4151fb9df90449d40d0608f6cb33a5bcbfc8c265f42bab0a"),
AssetId = new uint256("0e99c1a6da379d1f4151fb9df90449d40d0608f6cb33a5bcbfc8c265f42bab0a"),
DisplayName = "Liquid CAD",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://liquid.network/tx/{0}" : "https://liquid.network/testnet/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,

@ -1,5 +1,4 @@
#if ALTCOINS
using System;
using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Common;
@ -19,8 +18,7 @@ namespace BTCPayServer
NewTransactionEvent evtOutputs)
{
return evtOutputs.Outputs.Where(output =>
(output.Value is not AssetMoney && NetworkCryptoCode.Equals(evtOutputs.CryptoCode, StringComparison.InvariantCultureIgnoreCase)) ||
(output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId)).Select(output =>
output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId).Select(output =>
{
var outpoint = new OutPoint(evtOutputs.TransactionData.TransactionHash, output.Index);
return (output, outpoint);
@ -36,12 +34,12 @@ namespace BTCPayServer
output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId));
}
public override PaymentUrlBuilder GenerateBIP21(string cryptoInfoAddress, decimal? cryptoInfoDue)
public override PaymentUrlBuilder GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue)
{
//precision 0: 10 = 0.00000010
//precision 2: 10 = 0.00001000
//precision 8: 10 = 10
var money = cryptoInfoDue / (decimal)Math.Pow(10, 8 - Divisibility);
var money = cryptoInfoDue is null ? null : new Money(cryptoInfoDue.ToDecimal(MoneyUnit.BTC) / decimal.Parse("1".PadRight(1 + 8 - Divisibility, '0')), MoneyUnit.BTC);
var builder = base.GenerateBIP21(cryptoInfoAddress, money);
builder.QueryParams.Add("assetid", AssetId.ToString());
return builder;

@ -45,10 +45,10 @@ namespace BTCPayServer.Services.Altcoins.Monero.RPC
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Basic",
Convert.ToBase64String(Encoding.Default.GetBytes($"{_username}:{_password}")));
HttpResponseMessage rawResult = await _httpClient.SendAsync(httpRequest, cts);
rawResult.EnsureSuccessStatusCode();
var rawResult = await _httpClient.SendAsync(httpRequest, cts);
var rawJson = await rawResult.Content.ReadAsStringAsync();
rawResult.EnsureSuccessStatusCode();
JsonRpcResult<TResponse> response;
try
{

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using BTCPayServer.Common;
@ -88,13 +87,13 @@ namespace BTCPayServer
});
}
public virtual PaymentUrlBuilder GenerateBIP21(string cryptoInfoAddress, decimal? cryptoInfoDue)
public virtual PaymentUrlBuilder GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue)
{
var builder = new PaymentUrlBuilder(this.NBitcoinNetwork.UriScheme);
builder.Host = cryptoInfoAddress;
if (cryptoInfoDue is not null && cryptoInfoDue.Value != 0.0m)
if (cryptoInfoDue != null && cryptoInfoDue != Money.Zero)
{
builder.QueryParams.Add("amount", cryptoInfoDue.Value.ToString(CultureInfo.InvariantCulture));
builder.QueryParams.Add("amount", cryptoInfoDue.ToString(false, true));
}
return builder;
}

@ -56,6 +56,7 @@ namespace BTCPayServer
InitViacoin();
InitMonero();
InitZcash();
InitChaincoin();
// InitArgoneum();//their rate source is down 9/15/20.
// InitMonetaryUnit(); Not supported from Bittrex from 11/23/2022, dead shitcoin

@ -1,6 +1,3 @@
using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Models;
using Microsoft.EntityFrameworkCore;

@ -14,7 +14,6 @@ namespace BTCPayServer.Data
public DateTimeOffset Created { get; set; }
public bool TagAllInvoices { get; set; }
public string Settings { get; set; }
public bool Archived { get; set; }
internal static void OnModelCreating(ModelBuilder builder)
{

@ -8,7 +8,6 @@ namespace BTCPayServer.Data;
public class AutomatedPayoutBlob
{
public TimeSpan Interval { get; set; } = TimeSpan.FromHours(1);
public bool ProcessNewPayoutsInstantly { get; set; }
}
public class PayoutProcessorData : IHasBlobUntyped
{

@ -48,7 +48,6 @@ namespace BTCPayServer.Data
public IEnumerable<StoreSettingData> Settings { get; set; }
public IEnumerable<FormData> Forms { get; set; }
public IEnumerable<StoreRole> StoreRoles { get; set; }
public bool Archived { get; set; }
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
{

@ -1,39 +0,0 @@
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20230906135844_AddArchivedFlagForStoresAndApps")]
public partial class AddArchivedFlagForStoresAndApps : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "Archived",
table: "Stores",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "Archived",
table: "Apps",
nullable: false,
defaultValue: false);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Archived",
table: "Stores");
migrationBuilder.DropColumn(
name: "Archived",
table: "Apps");
}
}
}

@ -79,9 +79,6 @@ namespace BTCPayServer.Migrations
b.Property<string>("AppType")
.HasColumnType("TEXT");
b.Property<bool>("Archived")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Created")
.HasColumnType("TEXT");
@ -754,9 +751,6 @@ namespace BTCPayServer.Migrations
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<bool>("Archived")
.HasColumnType("INTEGER");
b.Property<string>("DefaultCrypto")
.HasColumnType("TEXT");

@ -0,0 +1,37 @@
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())
};
}
}

@ -7,8 +7,8 @@
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.9" />
<PackageReference Include="NBitcoin" Version="7.0.24" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="1.0.4" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="1.0.2" />
</ItemGroup>
<ItemGroup>

@ -20,13 +20,13 @@ namespace BTCPayServer.Services.Rates
}
public class CurrencyNameTable
{
public static CurrencyNameTable Instance = new();
public static CurrencyNameTable Instance = new CurrencyNameTable();
public CurrencyNameTable()
{
_Currencies = LoadCurrency().ToDictionary(k => k.Code, StringComparer.InvariantCultureIgnoreCase);
_Currencies = LoadCurrency().ToDictionary(k => k.Code);
}
static readonly Dictionary<string, IFormatProvider> _CurrencyProviders = new();
static readonly Dictionary<string, IFormatProvider> _CurrencyProviders = new Dictionary<string, IFormatProvider>();
public NumberFormatInfo GetNumberFormatInfo(string currency, bool useFallback)
{

@ -19,7 +19,7 @@ namespace BTCPayServer.Rating
public static CurrencyPair Parse(string str)
{
if (!TryParse(str, out var result))
throw new FormatException($"Invalid currency pair ({str})");
throw new FormatException("Invalid currency pair");
return result;
}
public static bool TryParse(string str, out CurrencyPair value)

@ -0,0 +1,29 @@
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Rates
{
public class ArgoneumRateProvider : IRateProvider
{
private readonly HttpClient _httpClient;
public ArgoneumRateProvider(HttpClient httpClient)
{
_httpClient = httpClient ?? new HttpClient();
}
public RateSourceInfo RateSourceInfo => new("argoneum", "Argoneum", "https://rates.argoneum.net/rates");
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
// Example result: AGM to BTC rate: {"agm":5000000.000000}
var response = await _httpClient.GetAsync("https://rates.argoneum.net/rates/btc", cancellationToken);
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
var value = jobj["agm"].Value<decimal>();
return new[] { new PairRate(new CurrencyPair("BTC", "AGM"), new BidAsk(value)) };
}
}
}

File diff suppressed because one or more lines are too long

@ -1,36 +0,0 @@
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 FreeCurrencyRatesRateProvider : IRateProvider
{
public RateSourceInfo RateSourceInfo => new("free-currency-rates", "Free Currency Rates", "https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api@1/latest/currencies/btc.min.json");
private readonly HttpClient _httpClient;
public FreeCurrencyRatesRateProvider(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);
var results = (JObject) jobj["btc"] ;
//key value is currency code to rate value
var list = new List<PairRate>();
foreach (var item in results)
{
string name = item.Key;
var value = item.Value.Value<decimal>();
list.Add(new PairRate(new CurrencyPair("BTC", name), new BidAsk(value)));
}
return list.ToArray();
}
}

@ -13,7 +13,7 @@ namespace BTCPayServer.Services.Rates
{
public class RipioExchangeProvider : IRateProvider
{
public RateSourceInfo RateSourceInfo => new("ripio", "Ripio", "https://api.ripiotrade.co/v4/public/tickers");
public RateSourceInfo RateSourceInfo => new("ripio", "Ripio", "https://api.exchange.ripio.com/api/v1/rate/all/");
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.ripiotrade.co/v4/public/tickers", cancellationToken);
var response = await _httpClient.GetAsync("https://api.exchange.ripio.com/api/v1/rate/all/", cancellationToken);
response.EnsureSuccessStatusCode();
var jarray = (JArray)(await response.Content.ReadAsAsync<JObject>(cancellationToken))["data"];
var jarray = (JArray)(await response.Content.ReadAsAsync<JArray>(cancellationToken));
return jarray
.Children<JObject>()
.Select(jobj => ParsePair(jobj))

@ -1,8 +1,21 @@
#nullable enable
namespace BTCPayServer.Rating;
public enum RateSource
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BTCPayServer.Rating
{
Coingecko,
Direct
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; }
}
}
public record RateSourceInfo(string Id, string DisplayName, string Url, RateSource Source = RateSource.Direct);

@ -85,13 +85,14 @@ namespace BTCPayServer.Services.Rates
bgFetcher.RefreshRate = TimeSpan.FromMinutes(1.0);
bgFetcher.ValidatyTime = TimeSpan.FromMinutes(5.0);
Providers.Add(supportedExchange.Id, bgFetcher);
AvailableRateProviders.Add(coingecko.RateSourceInfo);
var rsi = coingecko.RateSourceInfo;
AvailableRateProviders.Add(new(rsi.Id, rsi.DisplayName, rsi.Url, RateSource.Coingecko));
}
}
AvailableRateProviders.Sort((a, b) => StringComparer.Ordinal.Compare(a.DisplayName, b.DisplayName));
}
public List<RateSourceInfo> AvailableRateProviders { get; } = new List<RateSourceInfo>();
public List<AvailableRateProvider> AvailableRateProviders { get; } = new List<AvailableRateProvider>();
public async Task<QueryRateResult> QueryRates(string exchangeName, CancellationToken cancellationToken)
{

@ -663,7 +663,7 @@ donation:
Assert.Equal(3, vmview.Items.Length);
Assert.Equal("good apple", vmview.Items[0].Title);
Assert.Equal("orange", vmview.Items[1].Title);
Assert.Equal(10.0m, vmview.Items[1].Price);
Assert.Equal(10.0m, vmview.Items[1].Price.Value);
Assert.Equal("{0} Purchase", vmview.ButtonText);
Assert.Equal("Nicolas Sexy Hair", vmview.CustomButtonText);
Assert.Equal("Wanna tip?", vmview.CustomTipText);
@ -680,7 +680,7 @@ donation:
Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, choiceKey: "apple").Result);
invoices = await user.BitPay.GetInvoicesAsync();
invoices = user.BitPay.GetInvoices();
var appleInvoice = invoices.SingleOrDefault(invoice => invoice.ItemCode.Equals("apple"));
Assert.NotNull(appleInvoice);
Assert.Equal("good apple", appleInvoice.ItemDesc);
@ -689,7 +689,7 @@ donation:
var action = Assert.IsType<RedirectToActionResult>(publicApps
.ViewPointOfSale(app.Id, PosViewType.Cart, 6.6m, choiceKey: "donation").Result);
Assert.Equal(nameof(UIInvoiceController.Checkout), action.ActionName);
invoices = await user.BitPay.GetInvoicesAsync();
invoices = user.BitPay.GetInvoices();
var donationInvoice = invoices.Single(i => i.Price == 6.6m);
Assert.NotNull(donationInvoice);
Assert.Equal("CAD", donationInvoice.Currency);

@ -52,12 +52,11 @@ namespace BTCPayServer.Tests
{
tester.ActivateLBTC();
await tester.StartAsync();
//https://github.com/ElementsProject/elements/issues/956
await tester.LBTCExplorerNode.SendCommandAsync("rescanblockchain");
var user = tester.NewAccount();
await user.GrantAccessAsync();
user.GrantAccess();
user.RegisterDerivationScheme("LBTC");
user.RegisterDerivationScheme("USDT");
user.RegisterDerivationScheme("ETB");
await tester.LBTCExplorerNode.GenerateAsync(4);
//no tether on our regtest, lets create it and set it
var tether = tester.NetworkProvider.GetNetwork<ElementsBTCPayNetwork>("USDT");
@ -76,10 +75,6 @@ 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);
@ -87,7 +82,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",false, lbtc.AssetId.ToString());
1, "UNSET", lbtc.AssetId);
TestUtils.Eventually(() =>
{
@ -100,7 +95,8 @@ namespace BTCPayServer.Tests
ci = invoice.CryptoInfo.Single(info => info.CryptoCode.Equals("USDT"));
Assert.Equal(3, invoice.SupportedTransactionCurrencies.Count);
star = tester.LBTCExplorerNode.SendCommand("sendtoaddress", ci.Address, decimal.Parse(ci.Due), "x", "z", false, true, 1, "unset", false, tether.AssetId.ToString());
star = await tester.LBTCExplorerNode.SendCommandAsync("sendtoaddress", ci.Address, ci.Due, "", "", false, true,
1, "UNSET", tether.AssetId);
TestUtils.Eventually(() =>
{

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

@ -54,33 +54,13 @@ namespace BTCPayServer.Tests
Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps2.ListApps(user2.StoreId).Result).Model);
Assert.Single(appList.Apps);
Assert.Empty(appList2.Apps);
Assert.Equal("test", app.AppName);
Assert.Equal(apps.CreatedAppId, app.Id);
Assert.True(app.Role.ToPermissionSet(app.StoreId).Contains(Policies.CanModifyStoreSettings, app.StoreId));
Assert.Equal(user.StoreId, app.StoreId);
// Archive
redirect = Assert.IsType<RedirectResult>(apps.ToggleArchive(app.Id).Result);
Assert.EndsWith("/settings/crowdfund", redirect.Url);
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
Assert.Empty(appList.Apps);
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId, archived: true).Result).Model);
app = appList.Apps[0];
Assert.True(app.Archived);
Assert.IsType<NotFoundResult>(await crowdfund.ViewCrowdfund(app.Id));
// Unarchive
redirect = Assert.IsType<RedirectResult>(apps.ToggleArchive(app.Id).Result);
Assert.EndsWith("/settings/crowdfund", redirect.Url);
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
app = appList.Apps[0];
Assert.False(app.Archived);
var crowdfundViewModel = await crowdfund.UpdateCrowdfund(app.Id).AssertViewModelAsync<UpdateCrowdfundViewModel>();
crowdfundViewModel.Enabled = true;
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
Assert.IsType<ViewResult>(await crowdfund.ViewCrowdfund(app.Id));
// Delete
Assert.IsType<NotFoundResult>(apps2.DeleteApp(app.Id));
Assert.IsType<ViewResult>(apps.DeleteApp(app.Id));
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(app.Id).Result);
Assert.Equal("test", appList.Apps[0].AppName);
Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id);
Assert.True(appList.Apps[0].Role.ToPermissionSet(appList.Apps[0].StoreId).Contains(Policies.CanModifyStoreSettings, appList.Apps[0].StoreId));
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id));
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id));
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result);
Assert.Equal(nameof(UIStoresController.Dashboard), redirectToAction.ActionName);
appList = await apps.ListApps(user.StoreId).AssertViewModelAsync<ListAppsViewModel>();
Assert.Empty(appList.Apps);

@ -1,5 +1,4 @@
using System;
using System.Diagnostics;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@ -197,7 +196,6 @@ retry:
driver.FindElement(selector).Click();
}
[DebuggerHidden]
public static bool ElementDoesNotExist(this IWebDriver driver, By selector)
{
Assert.Throws<NoSuchElementException>(() =>

@ -23,7 +23,6 @@ using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Rating;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Labels;
using BTCPayServer.Services.Rates;
@ -347,272 +346,165 @@ namespace BTCPayServer.Tests
Assert.True(Torrc.TryParse(input, out torrc));
Assert.Equal(expected, torrc.ToString());
}
[Fact]
public void CanParseCartItems()
{
Assert.True(AppService.TryParsePosCartItems(new JObject()
{
{"cart", new JArray()
{
new JObject()
{
{ "id", "ddd"},
{"price", 4},
{"count", 1}
}
}}
}, out var items));
Assert.Equal("ddd", items[0].Id);
Assert.Equal(1, items[0].Count);
Assert.Equal(4, items[0].Price);
// Using legacy parsing
Assert.True(AppService.TryParsePosCartItems(new JObject()
{
{"cart", new JArray()
{
new JObject()
{
{ "id", "ddd"},
{"price", new JObject()
{
{ "value", 8.49m }
}
},
{"count", 1}
}
}}
}, out items));
Assert.Equal("ddd", items[0].Id);
Assert.Equal(1, items[0].Count);
Assert.Equal(8.49m, items[0].Price);
Assert.False(AppService.TryParsePosCartItems(new JObject()
{
{"cart", new JArray()
{
new JObject()
{
{ "id", "ddd"},
{"price", new JObject()
{
{ "value", "nocrahs" }
}
},
{"count", 1}
}
}}
}, out items));
}
[Fact]
public void CanCalculateDust()
{
var entity = new InvoiceEntity() { Currency = "USD" };
entity.Networks = new BTCPayNetworkProvider(ChainName.Regtest);
#pragma warning disable CS0618
entity.Payments = new System.Collections.Generic.List<PaymentEntity>();
entity.SetPaymentMethod(new PaymentMethod()
{
Currency = "BTC",
Rate = 34_000m
});
entity.Price = 4000;
entity.UpdateTotals();
var accounting = entity.GetPaymentMethods().First().Calculate();
// Exact price should be 0.117647059..., but the payment method round up to one sat
Assert.Equal(0.11764706m, accounting.Due);
entity.Payments.Add(new PaymentEntity()
{
Currency = "BTC",
Output = new TxOut(Money.Coins(0.11764706m), new Key()),
Accounted = true
});
entity.UpdateTotals();
Assert.Equal(0.0m, entity.NetDue);
// The dust's value is below 1 sat
Assert.True(entity.Dust > 0.0m);
Assert.True(Money.Satoshis(1.0m).ToDecimal(MoneyUnit.BTC) * entity.Rates["BTC"] > entity.Dust);
Assert.True(!entity.IsOverPaid);
Assert.True(!entity.IsUnderPaid);
// Now, imagine there is litecoin. It might seem from its
// perspecitve that there has been a slight over payment.
// However, Calculate() should just cap it to 0.0m
entity.SetPaymentMethod(new PaymentMethod()
{
Currency = "LTC",
Rate = 3400m
});
entity.UpdateTotals();
var method = entity.GetPaymentMethods().First(p => p.Currency == "LTC");
accounting = method.Calculate();
Assert.Equal(0.0m, accounting.DueUncapped);
#pragma warning restore CS0618
}
#if ALTCOINS
[Fact]
public void CanCalculateCryptoDue()
{
var networkProvider = new BTCPayNetworkProvider(ChainName.Regtest);
var entity = new InvoiceEntity() { Currency = "USD" };
var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[]
{
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null, null),
new LightningLikePaymentHandler(null, null, networkProvider, null, null, null),
});
var entity = new InvoiceEntity();
entity.Networks = networkProvider;
#pragma warning disable CS0618
entity.Payments = new System.Collections.Generic.List<PaymentEntity>();
entity.SetPaymentMethod(new PaymentMethod()
{
Currency = "BTC",
CryptoCode = "BTC",
Rate = 5000,
NextNetworkFee = Money.Coins(0.1m)
});
entity.Price = 5000;
entity.UpdateTotals();
var paymentMethod = entity.GetPaymentMethods().TryGet("BTC", PaymentTypes.BTCLike);
var accounting = paymentMethod.Calculate();
Assert.Equal(1.0m, accounting.ToSmallestUnit(Money.Satoshis(1.0m).ToDecimal(MoneyUnit.BTC)));
Assert.Equal(1.1m, accounting.Due);
Assert.Equal(1.1m, accounting.TotalDue);
Assert.Equal(Money.Coins(1.1m), accounting.Due);
Assert.Equal(Money.Coins(1.1m), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity()
{
Currency = "BTC",
Output = new TxOut(Money.Coins(0.5m), new Key()),
Rate = 5000,
Accounted = true,
NetworkFee = 0.1m
});
entity.UpdateTotals();
accounting = paymentMethod.Calculate();
//Since we need to spend one more txout, it should be 1.1 - 0,5 + 0.1
Assert.Equal(0.7m, accounting.Due);
Assert.Equal(1.2m, accounting.TotalDue);
Assert.Equal(Money.Coins(0.7m), accounting.Due);
Assert.Equal(Money.Coins(1.2m), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity()
{
Currency = "BTC",
Output = new TxOut(Money.Coins(0.2m), new Key()),
Accounted = true,
NetworkFee = 0.1m
});
entity.UpdateTotals();
accounting = paymentMethod.Calculate();
Assert.Equal(0.6m, accounting.Due);
Assert.Equal(1.3m, accounting.TotalDue);
Assert.Equal(Money.Coins(0.6m), accounting.Due);
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity()
{
Currency = "BTC",
Output = new TxOut(Money.Coins(0.6m), new Key()),
Accounted = true,
NetworkFee = 0.1m
});
entity.UpdateTotals();
accounting = paymentMethod.Calculate();
Assert.Equal(0.0m, accounting.Due);
Assert.Equal(1.3m, accounting.TotalDue);
Assert.Equal(Money.Zero, accounting.Due);
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
entity.Payments.Add(
new PaymentEntity() { Currency = "BTC", Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true });
entity.UpdateTotals();
new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true });
accounting = paymentMethod.Calculate();
Assert.Equal(0.0m, accounting.Due);
Assert.Equal(1.3m, accounting.TotalDue);
Assert.Equal(Money.Zero, accounting.Due);
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
entity = new InvoiceEntity();
entity.Networks = networkProvider;
entity.Price = 5000;
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
paymentMethods.Add(
new PaymentMethod() { Currency = "BTC", Rate = 1000, NextNetworkFee = Money.Coins(0.1m) });
new PaymentMethod() { CryptoCode = "BTC", Rate = 1000, NextNetworkFee = Money.Coins(0.1m) });
paymentMethods.Add(
new PaymentMethod() { Currency = "LTC", Rate = 500, NextNetworkFee = Money.Coins(0.01m) });
new PaymentMethod() { CryptoCode = "LTC", Rate = 500, NextNetworkFee = Money.Coins(0.01m) });
entity.SetPaymentMethods(paymentMethods);
entity.Payments = new List<PaymentEntity>();
entity.UpdateTotals();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
accounting = paymentMethod.Calculate();
Assert.Equal(5.1m, accounting.Due);
Assert.Equal(Money.Coins(5.1m), accounting.Due);
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
accounting = paymentMethod.Calculate();
Assert.Equal(10.01m, accounting.TotalDue);
Assert.Equal(Money.Coins(10.01m), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity()
{
Currency = "BTC",
CryptoCode = "BTC",
Output = new TxOut(Money.Coins(1.0m), new Key()),
Accounted = true,
NetworkFee = 0.1m
});
entity.UpdateTotals();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
accounting = paymentMethod.Calculate();
Assert.Equal(4.2m, accounting.Due);
Assert.Equal(1.0m, accounting.CryptoPaid);
Assert.Equal(1.0m, accounting.Paid);
Assert.Equal(5.2m, accounting.TotalDue);
Assert.Equal(Money.Coins(4.2m), accounting.Due);
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
Assert.Equal(Money.Coins(1.0m), accounting.Paid);
Assert.Equal(Money.Coins(5.2m), accounting.TotalDue);
Assert.Equal(2, accounting.TxRequired);
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
accounting = paymentMethod.Calculate();
Assert.Equal(10.01m + 0.1m * 2 - 2.0m /* 8.21m */, accounting.Due);
Assert.Equal(0.0m, accounting.CryptoPaid);
Assert.Equal(2.0m, accounting.Paid);
Assert.Equal(10.01m + 0.1m * 2, accounting.TotalDue);
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 - 2.0m /* 8.21m */), accounting.Due);
Assert.Equal(Money.Coins(0.0m), accounting.CryptoPaid);
Assert.Equal(Money.Coins(2.0m), accounting.Paid);
Assert.Equal(Money.Coins(10.01m + 0.1m * 2), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity()
{
Currency = "LTC",
CryptoCode = "LTC",
Output = new TxOut(Money.Coins(1.0m), new Key()),
Accounted = true,
NetworkFee = 0.01m
});
entity.UpdateTotals();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
accounting = paymentMethod.Calculate();
Assert.Equal(4.2m - 0.5m + 0.01m / 2, accounting.Due);
Assert.Equal(1.0m, accounting.CryptoPaid);
Assert.Equal(1.5m, accounting.Paid);
Assert.Equal(5.2m + 0.01m / 2, accounting.TotalDue); // The fee for LTC added
Assert.Equal(Money.Coins(4.2m - 0.5m + 0.01m / 2), accounting.Due);
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
Assert.Equal(Money.Coins(1.5m), accounting.Paid);
Assert.Equal(Money.Coins(5.2m + 0.01m / 2), accounting.TotalDue); // The fee for LTC added
Assert.Equal(2, accounting.TxRequired);
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
accounting = paymentMethod.Calculate();
Assert.Equal(8.21m - 1.0m + 0.01m, accounting.Due);
Assert.Equal(1.0m, accounting.CryptoPaid);
Assert.Equal(3.0m, accounting.Paid);
Assert.Equal(10.01m + 0.1m * 2 + 0.01m, accounting.TotalDue);
Assert.Equal(Money.Coins(8.21m - 1.0m + 0.01m), accounting.Due);
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
Assert.Equal(Money.Coins(3.0m), accounting.Paid);
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 + 0.01m), accounting.TotalDue);
Assert.Equal(2, accounting.TxRequired);
var remaining = Money.Coins(4.2m - 0.5m + 0.01m / 2.0m).ToDecimal(MoneyUnit.BTC);
var remaining = Money.Coins(4.2m - 0.5m + 0.01m / 2);
entity.Payments.Add(new PaymentEntity()
{
Currency = "BTC",
Output = new TxOut(Money.Coins(remaining), new Key()),
CryptoCode = "BTC",
Output = new TxOut(remaining, new Key()),
Accounted = true,
NetworkFee = 0.1m
});
entity.UpdateTotals();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike));
accounting = paymentMethod.Calculate();
Assert.Equal(0.0m, accounting.Due);
Assert.Equal(1.0m + remaining, accounting.CryptoPaid);
Assert.Equal(1.5m + remaining, accounting.Paid);
Assert.Equal(5.2m + 0.01m / 2, accounting.TotalDue);
Assert.Equal(Money.Zero, accounting.Due);
Assert.Equal(Money.Coins(1.0m) + remaining, accounting.CryptoPaid);
Assert.Equal(Money.Coins(1.5m) + remaining, accounting.Paid);
Assert.Equal(Money.Coins(5.2m + 0.01m / 2), accounting.TotalDue);
Assert.Equal(accounting.Paid, accounting.TotalDue);
Assert.Equal(2, accounting.TxRequired);
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
accounting = paymentMethod.Calculate();
Assert.Equal(0.0m, accounting.Due);
Assert.Equal(1.0m, accounting.CryptoPaid);
Assert.Equal(3.0m + remaining * 2, accounting.Paid);
Assert.Equal(Money.Zero, accounting.Due);
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
Assert.Equal(Money.Coins(3.0m) + remaining * 2, accounting.Paid);
// Paying 2 BTC fee, LTC fee removed because fully paid
Assert.Equal(10.01m + 0.1m * 2 + 0.1m * 2 /* + 0.01m no need to pay this fee anymore */,
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 + 0.1m * 2 /* + 0.01m no need to pay this fee anymore */),
accounting.TotalDue);
Assert.Equal(1, accounting.TxRequired);
Assert.Equal(accounting.Paid, accounting.TotalDue);
@ -656,29 +548,27 @@ namespace BTCPayServer.Tests
entity.Payments = new List<PaymentEntity>();
entity.SetPaymentMethod(new PaymentMethod()
{
Currency = "BTC",
CryptoCode = "BTC",
Rate = 5000,
NextNetworkFee = Money.Coins(0.1m)
});
entity.Price = 5000;
entity.PaymentTolerance = 0;
entity.UpdateTotals();
var paymentMethod = entity.GetPaymentMethods().TryGet("BTC", PaymentTypes.BTCLike);
var accounting = paymentMethod.Calculate();
Assert.Equal(1.1m, accounting.Due);
Assert.Equal(1.1m, accounting.TotalDue);
Assert.Equal(1.1m, accounting.MinimumTotalDue);
Assert.Equal(Money.Coins(1.1m), accounting.Due);
Assert.Equal(Money.Coins(1.1m), accounting.TotalDue);
Assert.Equal(Money.Coins(1.1m), accounting.MinimumTotalDue);
entity.PaymentTolerance = 10;
entity.UpdateTotals();
accounting = paymentMethod.Calculate();
Assert.Equal(0.99m, accounting.MinimumTotalDue);
Assert.Equal(Money.Coins(0.99m), accounting.MinimumTotalDue);
entity.PaymentTolerance = 100;
entity.UpdateTotals();
accounting = paymentMethod.Calculate();
Assert.Equal(0.0000_0001m, accounting.MinimumTotalDue);
Assert.Equal(Money.Satoshis(1), accounting.MinimumTotalDue);
}
[Fact]
@ -719,7 +609,7 @@ namespace BTCPayServer.Tests
}
[Fact]
public void CanDetectFileType()
public void CanDetectImage()
{
Assert.True(FileTypeDetector.IsPicture(new byte[] { 0x42, 0x4D }, "test.bmp"));
Assert.False(FileTypeDetector.IsPicture(new byte[] { 0x42, 0x4D }, ".bmp"));
@ -732,15 +622,6 @@ namespace BTCPayServer.Tests
Assert.False(FileTypeDetector.IsPicture(new byte[] { 0x3C, 0x73, 0x76, 0x67 }, "test.jpg"));
Assert.False(FileTypeDetector.IsPicture(new byte[] { 0xFF }, "e.jpg"));
Assert.False(FileTypeDetector.IsPicture(new byte[] { }, "empty.jpg"));
Assert.False(FileTypeDetector.IsPicture(new byte[] { 0x49, 0x44, 0x33, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23 }, "music.mp3"));
Assert.True(FileTypeDetector.IsAudio(new byte[] { 0x49, 0x44, 0x33, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23 }, "music.mp3"));
Assert.True(FileTypeDetector.IsAudio(new byte[] { 0x52, 0x49, 0x46, 0x46, 0x24, 0x9A, 0x08, 0x00, 0x57, 0x41 }, "music.wav"));
Assert.True(FileTypeDetector.IsAudio(new byte[] { 0xFF, 0xF1, 0x50, 0x80, 0x1C, 0x3F, 0xFC, 0xDA, 0x00, 0x4C }, "music.aac"));
Assert.True(FileTypeDetector.IsAudio(new byte[] { 0x66, 0x4C, 0x61, 0x43, 0x00, 0x00, 0x00, 0x22, 0x04, 0x80 }, "music.flac"));
Assert.True(FileTypeDetector.IsAudio(new byte[] { 0x4F, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00 }, "music.ogg"));
Assert.True(FileTypeDetector.IsAudio(new byte[] { 0x1A, 0x45, 0xDF, 0xA3, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00 }, "music.weba"));
Assert.True(FileTypeDetector.IsAudio(new byte[] { 0xFF, 0xF3, 0xE4, 0x64, 0x00, 0x20, 0xAD, 0xBD, 0x04, 0x00 }, "music.mp3"));
}
[Fact]
@ -1183,7 +1064,7 @@ namespace BTCPayServer.Tests
search = new SearchString(filter);
Assert.Equal("2019-04-25 01:00 AM", search.Filters["startdate"].First());
Assert.Equal("hekki", search.TextSearch);
// modify search
filter = $"status:settled,exceptionstatus:paidLate,unusual:true, fulltext searchterm, storeid:{storeId},startdate:2019-04-25 01:00:00";
search = new SearchString(filter);
@ -1193,33 +1074,33 @@ namespace BTCPayServer.Tests
Assert.Single(search.Filters["status"], "settled");
Assert.Single(search.Filters["exceptionstatus"], "paidLate");
Assert.Single(search.Filters["unusual"], "true");
// toggle off bool with same value
var modified = new SearchString(search.Toggle("unusual", "true"));
Assert.Null(modified.GetFilterBool("unusual"));
// add to array
modified = new SearchString(modified.Toggle("status", "processing"));
var statusArray = modified.GetFilterArray("status");
Assert.Equal(2, statusArray.Length);
Assert.Contains("processing", statusArray);
Assert.Contains("settled", statusArray);
// toggle off array with same value
modified = new SearchString(modified.Toggle("status", "settled"));
statusArray = modified.GetFilterArray("status");
Assert.Single(statusArray, "processing");
// toggle off array with null value
modified = new SearchString(modified.Toggle("status", null));
Assert.Null(modified.GetFilterArray("status"));
// toggle off date with null value
modified = new SearchString(modified.Toggle("startdate", "-7d"));
Assert.Single(modified.GetFilterArray("startdate"), "-7d");
modified = new SearchString(modified.Toggle("startdate", null));
Assert.Null(modified.GetFilterArray("startdate"));
// toggle off date with same value
modified = new SearchString(modified.Toggle("enddate", "-7d"));
Assert.Single(modified.GetFilterArray("enddate"), "-7d");
@ -1264,45 +1145,6 @@ namespace BTCPayServer.Tests
Assert.Equal("000000161", m.OrderId);
}
[Fact]
public void CanParseOldPosAppData()
{
var data = new JObject()
{
["price"] = 1.64m
}.ToString();
Assert.Equal(1.64m, JsonConvert.DeserializeObject<PosAppCartItem>(data).Price);
data = new JObject()
{
["price"] = new JObject()
{
["value"] = 1.65m
}
}.ToString();
Assert.Equal(1.65m, JsonConvert.DeserializeObject<PosAppCartItem>(data).Price);
data = new JObject()
{
["price"] = new JObject()
{
["value"] = "1.6305"
}
}.ToString();
Assert.Equal(1.6305m, JsonConvert.DeserializeObject<PosAppCartItem>(data).Price);
data = new JObject()
{
["price"] = new JObject()
{
["value"] = null
}
}.ToString();
Assert.Equal(0.0m, JsonConvert.DeserializeObject<PosAppCartItem>(data).Price);
var o = JObject.Parse(JsonConvert.SerializeObject(new PosAppCartItem() { Price = 1.356m }));
Assert.Equal(1.356m, o["price"].Value<decimal>());
}
[Fact]
public void CanParseCurrencyValue()
{
@ -2003,6 +1845,11 @@ namespace BTCPayServer.Tests
#pragma warning disable CS0618
var dummy = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, Network.RegTest).ToString();
var networkProvider = new BTCPayNetworkProvider(ChainName.Regtest);
var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[]
{
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null, null),
new LightningLikePaymentHandler(null, null, networkProvider, null, null, null),
});
var networkBTC = networkProvider.GetNetwork("BTC");
var networkLTC = networkProvider.GetNetwork("LTC");
InvoiceEntity invoiceEntity = new InvoiceEntity();
@ -2010,14 +1857,14 @@ namespace BTCPayServer.Tests
invoiceEntity.Payments = new System.Collections.Generic.List<PaymentEntity>();
invoiceEntity.Price = 100;
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
paymentMethods.Add(new PaymentMethod() { Network = networkBTC, Currency = "BTC", Rate = 10513.44m, }
paymentMethods.Add(new PaymentMethod() { Network = networkBTC, CryptoCode = "BTC", Rate = 10513.44m, }
.SetPaymentMethodDetails(
new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
{
NextNetworkFee = Money.Coins(0.00000100m),
DepositAddress = dummy
}));
paymentMethods.Add(new PaymentMethod() { Network = networkLTC, Currency = "LTC", Rate = 216.79m }
paymentMethods.Add(new PaymentMethod() { Network = networkLTC, CryptoCode = "LTC", Rate = 216.79m }
.SetPaymentMethodDetails(
new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
{
@ -2033,7 +1880,7 @@ namespace BTCPayServer.Tests
new PaymentEntity()
{
Accounted = true,
Currency = "BTC",
CryptoCode = "BTC",
NetworkFee = 0.00000100m,
Network = networkProvider.GetNetwork("BTC"),
}
@ -2042,33 +1889,34 @@ namespace BTCPayServer.Tests
Network = networkProvider.GetNetwork("BTC"),
Output = new TxOut() { Value = Money.Coins(0.00151263m) }
}));
invoiceEntity.UpdateTotals();
accounting = btc.Calculate();
invoiceEntity.Payments.Add(
new PaymentEntity()
{
Accounted = true,
Currency = "BTC",
CryptoCode = "BTC",
NetworkFee = 0.00000100m,
Network = networkProvider.GetNetwork("BTC")
}
.SetCryptoPaymentData(new BitcoinLikePaymentData()
{
Network = networkProvider.GetNetwork("BTC"),
Output = new TxOut() { Value = Money.Coins(accounting.Due) }
Output = new TxOut() { Value = accounting.Due }
}));
invoiceEntity.UpdateTotals();
accounting = btc.Calculate();
Assert.Equal(0.0m, accounting.Due);
Assert.Equal(0.0m, accounting.DueUncapped);
Assert.Equal(Money.Zero, accounting.Due);
Assert.Equal(Money.Zero, accounting.DueUncapped);
var ltc = invoiceEntity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike));
accounting = ltc.Calculate();
Assert.Equal(0.0m, accounting.Due);
// LTC might should be over paid due to BTC paying above what it should (round 1 satoshi up), but we handle this case
// and set DueUncapped to zero.
Assert.Equal(0.0m, accounting.DueUncapped);
Assert.Equal(Money.Zero, accounting.Due);
// LTC might have over paid due to BTC paying above what it should (round 1 satoshi up)
Assert.True(accounting.DueUncapped < Money.Zero);
var paymentMethod = InvoiceWatcher.GetNearestClearedPayment(paymentMethods, out var accounting2);
Assert.Equal(btc.CryptoCode, paymentMethod.CryptoCode);
#pragma warning restore CS0618
}
[Fact]

@ -1,6 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Form;
using BTCPayServer.Forms;
using Microsoft.AspNetCore.Http;
@ -11,25 +10,16 @@ using Xunit.Abstractions;
namespace BTCPayServer.Tests;
[Collection(nameof(NonParallelizableCollectionDefinition))]
[Trait("Integration", "Integration")]
[Trait("Fast", "Fast")]
public class FormTests : UnitTestBase
{
public FormTests(ITestOutputHelper helper) : base(helper)
{
}
[Fact(Timeout = TestUtils.TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanParseForm()
[Fact]
public void 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>
@ -50,6 +40,8 @@ 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
{
@ -172,7 +164,7 @@ public class FormTests : UnitTestBase
Assert.Equal("original", obj["invoice"]["test"].Value<string>());
Assert.Equal("updated", obj["invoice_item3"].Value<string>());
Clear(form);
service.SetValues(form, obj);
form.SetValues(obj);
obj = service.GetValues(form);
Assert.Equal("original", obj["invoice"]["test"].Value<string>());
Assert.Equal("updated", obj["invoice_item3"].Value<string>());
@ -190,12 +182,10 @@ public class FormTests : UnitTestBase
}
}
};
service.SetValues(form, obj);
form.SetValues(obj);
obj = service.GetValues(form);
Assert.Null(obj["test"].Value<string>());
service.SetValues(form, new JObject { ["test"] = "hello" });
form.SetValues(new JObject { ["test"] = "hello" });
obj = service.GetValues(form);
Assert.Equal("hello", obj["test"].Value<string>());
}

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading;
@ -15,6 +16,7 @@ using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.PayoutProcessors;
using BTCPayServer.PayoutProcessors.OnChain;
using BTCPayServer.Services;
using BTCPayServer.Services.Custodian.Client.MockCustodian;
using BTCPayServer.Services.Notifications;
@ -22,6 +24,7 @@ using BTCPayServer.Services.Notifications.Blobs;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
using NBitcoin;
using NBitpayClient;
using Newtonsoft.Json;
@ -297,7 +300,6 @@ namespace BTCPayServer.Tests
Assert.Equal(user.StoreId, app.StoreId);
Assert.Equal("PointOfSale", app.AppType);
Assert.Equal("test app title", app.Title);
Assert.False(app.Archived);
// Make sure we return a 404 if we try to get an app that doesn't exist
await AssertHttpError(404, async () =>
@ -321,20 +323,17 @@ namespace BTCPayServer.Tests
new CreatePointOfSaleAppRequest()
{
AppName = "new app name",
Title = "new app title",
Archived = true
Title = "new app title"
}
);
// Test generic GET app endpoint first
retrievedApp = await client.GetApp(app.Id);
Assert.Equal("new app name", retrievedApp.Name);
Assert.True(retrievedApp.Archived);
// Test the POS-specific endpoint also
var retrievedPosApp = await client.GetPosApp(app.Id);
Assert.Equal("new app name", retrievedPosApp.Name);
Assert.Equal("new app title", retrievedPosApp.Title);
Assert.True(retrievedPosApp.Archived);
// Make sure we return a 404 if we try to delete an app that doesn't exist
await AssertHttpError(404, async () =>
@ -466,7 +465,6 @@ namespace BTCPayServer.Tests
Assert.Equal("test app from API", app.Name);
Assert.Equal(user.StoreId, app.StoreId);
Assert.Equal("Crowdfund", app.AppType);
Assert.False(app.Archived);
// Make sure we return a 404 if we try to get an app that doesn't exist
await AssertHttpError(404, async () =>
@ -483,13 +481,11 @@ namespace BTCPayServer.Tests
Assert.Equal(app.Name, retrievedApp.Name);
Assert.Equal(app.StoreId, retrievedApp.StoreId);
Assert.Equal(app.AppType, retrievedApp.AppType);
Assert.False(retrievedApp.Archived);
// Test the crowdfund-specific endpoint also
var retrievedCfApp = await client.GetCrowdfundApp(app.Id);
Assert.Equal(app.Name, retrievedCfApp.Name);
Assert.Equal(app.Title, retrievedCfApp.Title);
Assert.False(retrievedCfApp.Archived);
var retrievedPosApp = await client.GetCrowdfundApp(app.Id);
Assert.Equal(app.Name, retrievedPosApp.Name);
Assert.Equal(app.Title, retrievedPosApp.Title);
// Make sure we return a 404 if we try to delete an app that doesn't exist
await AssertHttpError(404, async () =>
@ -539,12 +535,10 @@ namespace BTCPayServer.Tests
Assert.Equal(posApp.Name, apps[0].Name);
Assert.Equal(posApp.StoreId, apps[0].StoreId);
Assert.Equal(posApp.AppType, apps[0].AppType);
Assert.False(apps[0].Archived);
Assert.Equal(crowdfundApp.Name, apps[1].Name);
Assert.Equal(crowdfundApp.StoreId, apps[1].StoreId);
Assert.Equal(crowdfundApp.AppType, apps[1].AppType);
Assert.False(apps[1].Archived);
// Get all apps for all store now
apps = await client.GetAllApps();
@ -554,17 +548,15 @@ namespace BTCPayServer.Tests
Assert.Equal(posApp.Name, apps[0].Name);
Assert.Equal(posApp.StoreId, apps[0].StoreId);
Assert.Equal(posApp.AppType, apps[0].AppType);
Assert.False(apps[0].Archived);
Assert.Equal(crowdfundApp.Name, apps[1].Name);
Assert.Equal(crowdfundApp.StoreId, apps[1].StoreId);
Assert.Equal(crowdfundApp.AppType, apps[1].AppType);
Assert.False(apps[1].Archived);
Assert.Equal(newApp.Name, apps[2].Name);
Assert.Equal(newApp.StoreId, apps[2].StoreId);
Assert.Equal(newApp.AppType, apps[2].AppType);
Assert.False(apps[2].Archived);
}
[Fact(Timeout = TestTimeout)]
@ -885,7 +877,7 @@ namespace BTCPayServer.Tests
TestLogs.LogInformation("Can't archive without knowing the walletId");
var ex = await AssertAPIError("missing-permission", async () => await client.ArchivePullPayment("lol", result.Id));
Assert.Equal("btcpay.store.canarchivepullpayments", ((GreenfieldPermissionAPIError)ex.APIError).MissingPermission);
Assert.Equal("btcpay.store.canmanagepullpayments", ((GreenfieldPermissionAPIError)ex.APIError).MissingPermission);
TestLogs.LogInformation("Can't archive without permission");
await AssertAPIError("unauthenticated", async () => await unauthenticated.ArchivePullPayment(storeId, result.Id));
await client.ArchivePullPayment(storeId, result.Id);
@ -1159,8 +1151,7 @@ namespace BTCPayServer.Tests
Approved = false,
PaymentMethod = "BTC",
Amount = 0.0001m,
Destination = address.ToString(),
Destination = address.ToString()
});
await AssertAPIError("invalid-state", async () =>
{
@ -1279,7 +1270,7 @@ namespace BTCPayServer.Tests
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
await user.GrantAccessAsync();
user.GrantAccess();
await user.MakeAdmin();
var client = await user.CreateClient(Policies.Unrestricted);
@ -1358,13 +1349,6 @@ namespace BTCPayServer.Tests
}
tester.DeleteStore = false;
Assert.Empty(await client.GetStores());
// Archive
var archivableStore = await client.CreateStore(new CreateStoreRequest { Name = "Archivable" });
Assert.False(archivableStore.Archived);
archivableStore = await client.UpdateStore(archivableStore.Id, new UpdateStoreRequest { Name = "Archived", Archived = true });
Assert.Equal("Archived", archivableStore.Name);
Assert.True(archivableStore.Archived);
}
private async Task<GreenfieldValidationException> AssertValidationError(string[] fields, Func<Task> act)
@ -1606,7 +1590,7 @@ namespace BTCPayServer.Tests
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
await user.GrantAccessAsync();
user.GrantAccess();
await user.MakeAdmin();
var client = await user.CreateClient(Policies.Unrestricted);
var viewOnly = await user.CreateClient(Policies.CanViewPaymentRequests);
@ -1688,18 +1672,11 @@ namespace BTCPayServer.Tests
BitcoinAddress.Create(invoice.BitcoinAddress, tester.ExplorerNode.Network), invoice.BtcDue);
});
await TestUtils.EventuallyAsync(async () =>
{
Assert.Equal(Invoice.STATUS_PAID, (await user.BitPay.GetInvoiceAsync(invoiceId)).Status);
if (!partialPayment)
Assert.Equal(PaymentRequestData.PaymentRequestStatus.Processing, (await client.GetPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id)).Status);
});
await tester.ExplorerNode.GenerateAsync(1);
await TestUtils.EventuallyAsync(async () =>
{
Assert.Equal(Invoice.STATUS_CONFIRMED, (await user.BitPay.GetInvoiceAsync(invoiceId)).Status);
if (!partialPayment)
Assert.Equal(PaymentRequestData.PaymentRequestStatus.Completed, (await client.GetPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id)).Status);
});
{
Assert.Equal(Invoice.STATUS_PAID, user.BitPay.GetInvoice(invoiceId).Status);
if (!partialPayment)
Assert.Equal(PaymentRequestData.PaymentRequestStatus.Completed, (await client.GetPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id)).Status);
});
}
await Pay(invoiceId);
@ -3233,9 +3210,6 @@ namespace BTCPayServer.Tests
});
var transaction = await client.GetOnChainWalletTransaction(walletId.StoreId, walletId.CryptoCode, txdata.TransactionHash.ToString());
// Check skip doesn't crash
await client.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode, skip: 1);
Assert.Equal(transaction.TransactionHash, txdata.TransactionHash);
Assert.Equal(String.Empty, transaction.Comment);
#pragma warning disable CS0612 // Type or member is obsolete
@ -3570,7 +3544,6 @@ namespace BTCPayServer.Tests
PaymentMethod = "BTC_LightningNetwork",
Destination = customerInvoice.BOLT11
});
Assert.Equal(payout.Metadata.ToString(), new JObject().ToString()); //empty
Assert.Empty(await adminClient.GetStoreLightningAutomatedPayoutProcessors(admin.StoreId, "BTC_LightningNetwork"));
await adminClient.UpdateStoreLightningAutomatedPayoutProcessors(admin.StoreId, "BTC_LightningNetwork",
new LightningAutomatedPayoutSettings() { IntervalSeconds = TimeSpan.FromSeconds(600) });
@ -3581,46 +3554,6 @@ namespace BTCPayServer.Tests
(await adminClient.GetStorePayouts(admin.StoreId, false)).Single(data => data.Id == payout.Id);
Assert.Equal(PayoutState.Completed, payoutC.State);
});
payout = await adminClient.CreatePayout(admin.StoreId,
new CreatePayoutThroughStoreRequest()
{
Approved = true,
PaymentMethod = "BTC",
Destination = (await tester.ExplorerNode.GetNewAddressAsync()).ToString(),
Amount = 0.0001m,
Metadata = JObject.FromObject(new
{
source ="apitest",
sourceLink = "https://chocolate.com"
})
});
Assert.Equal(payout.Metadata.ToString(), JObject.FromObject(new
{
source = "apitest",
sourceLink = "https://chocolate.com"
}).ToString());
payout =
(await adminClient.GetStorePayouts(admin.StoreId, false)).Single(data => data.Id == payout.Id);
Assert.Equal(payout.Metadata.ToString(), JObject.FromObject(new
{
source = "apitest",
sourceLink = "https://chocolate.com"
}).ToString());
customerInvoice = await tester.CustomerLightningD.CreateInvoice(LightMoney.FromUnit(10, LightMoneyUnit.Satoshi),
Guid.NewGuid().ToString(), TimeSpan.FromDays(40));
var payout2 = await adminClient.CreatePayout(admin.StoreId,
new CreatePayoutThroughStoreRequest()
{
Approved = true,
Amount = new Money(100, MoneyUnit.Satoshi).ToDecimal(MoneyUnit.BTC),
PaymentMethod = "BTC_LightningNetwork",
Destination = customerInvoice.BOLT11
});
Assert.Equal(payout2.Amount, new Money(100, MoneyUnit.Satoshi).ToDecimal(MoneyUnit.BTC));
}
[Fact(Timeout = 60 * 2 * 1000)]
@ -3736,12 +3669,9 @@ namespace BTCPayServer.Tests
Assert.Single(payouts.Where(data => data.State == PayoutState.InProgress));
});
uint256 txid = null;
await tester.WaitForEvent<NewOnChainTransactionEvent>(async () =>
{
txid = await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(0.01m) + fee);
}, correctEvent: ev => ev.NewTransactionEvent.TransactionData.TransactionHash == txid);
var txid = await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(0.01m) + fee);
await tester.WaitForEvent<NewOnChainTransactionEvent>(null, correctEvent: ev => ev.NewTransactionEvent.TransactionData.TransactionHash == txid);
await tester.PayTester.GetService<PayoutProcessorService>().Restart(new PayoutProcessorService.PayoutProcessorQuery(admin.StoreId, "BTC"));
await TestUtils.EventuallyAsync(async () =>
{
@ -3749,122 +3679,6 @@ namespace BTCPayServer.Tests
payouts = await adminClient.GetStorePayouts(admin.StoreId);
Assert.Empty(payouts.Where(data => data.State != PayoutState.InProgress));
});
// settings that were added later
var settings =
Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC"));
Assert.False( settings.ProcessNewPayoutsInstantly);
Assert.Equal(0m, settings.Threshold);
//let's use the ProcessNewPayoutsInstantly so that it will trigger instantly
settings.IntervalSeconds = TimeSpan.FromDays(1);
settings.ProcessNewPayoutsInstantly = true;
await tester.WaitForEvent<NewOnChainTransactionEvent>(async () =>
{
txid = await tester.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create((await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
tester.ExplorerClient.Network.NBitcoinNetwork), Money.Coins(1m) + fee);
}, correctEvent: ev => ev.NewTransactionEvent.TransactionData.TransactionHash == txid);
await adminClient.UpdateStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC", settings);
settings =
Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC"));
Assert.True( settings.ProcessNewPayoutsInstantly);
var pluginHookService = tester.PayTester.GetService<IPluginHookService>();
var beforeHookTcs = new TaskCompletionSource();
var afterHookTcs = new TaskCompletionSource();
pluginHookService.ActionInvoked += (sender, tuple) =>
{
switch (tuple.hook)
{
case "before-automated-payout-processing":
beforeHookTcs.TrySetResult();
break;
case "after-automated-payout-processing":
afterHookTcs.TrySetResult();
break;
}
};
var payoutThatShouldBeProcessedStraightAway = await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest()
{
PullPaymentId = pullPayment.Id,
Amount = 0.5m,
Approved = true,
PaymentMethod = "BTC",
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
});
await beforeHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
payouts = await adminClient.GetStorePayouts(admin.StoreId);
Assert.Single(payouts.Where(data => data.State == PayoutState.InProgress && data.Id == payoutThatShouldBeProcessedStraightAway.Id));
beforeHookTcs = new TaskCompletionSource();
afterHookTcs = new TaskCompletionSource();
//let's test the threshold limiter
settings.Threshold = 0.5m;
await adminClient.UpdateStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC", settings);
//quick test: when updating processor, it processes instantly
await beforeHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
settings =
Assert.Single(await adminClient.GetStoreOnChainAutomatedPayoutProcessors(admin.StoreId, "BTC"));
Assert.Equal(0.5m, settings.Threshold);
//create a payout that should not be processed straight away due to threshold
beforeHookTcs = new TaskCompletionSource();
afterHookTcs = new TaskCompletionSource();
var payoutThatShouldNotBeProcessedStraightAway = await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest()
{
Amount = 0.1m,
Approved = true,
PaymentMethod = "BTC",
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
});
await beforeHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
payouts = await adminClient.GetStorePayouts(admin.StoreId);
Assert.Single(payouts.Where(data => data.State == PayoutState.AwaitingPayment && data.Id == payoutThatShouldNotBeProcessedStraightAway.Id));
beforeHookTcs = new TaskCompletionSource();
afterHookTcs = new TaskCompletionSource();
var payoutThatShouldNotBeProcessedStraightAway2 = await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest()
{
Amount = 0.3m,
Approved = true,
PaymentMethod = "BTC",
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
});
await beforeHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
payouts = await adminClient.GetStorePayouts(admin.StoreId);
Assert.Equal(2, payouts.Count(data => data.State == PayoutState.AwaitingPayment &&
(data.Id == payoutThatShouldNotBeProcessedStraightAway.Id || data.Id == payoutThatShouldNotBeProcessedStraightAway2.Id)));
beforeHookTcs = new TaskCompletionSource();
afterHookTcs = new TaskCompletionSource();
var payoutThatShouldNotBeProcessedStraightAway3 = await adminClient.CreatePayout(admin.StoreId, new CreatePayoutThroughStoreRequest()
{
Amount = 0.3m,
Approved = true,
PaymentMethod = "BTC",
Destination = (await adminClient.GetOnChainWalletReceiveAddress(admin.StoreId, "BTC", true)).Address,
});
await beforeHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
await afterHookTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
payouts = await adminClient.GetStorePayouts(admin.StoreId);
Assert.Empty(payouts.Where(data => data.State != PayoutState.InProgress));
}
[Fact(Timeout = 60 * 2 * 1000)]

@ -1,10 +1,8 @@
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Hosting;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Plugins.Crowdfund.Models;
using BTCPayServer.Plugins.PointOfSale;
using BTCPayServer.Plugins.PointOfSale.Controllers;
using BTCPayServer.Plugins.PointOfSale.Models;
@ -125,14 +123,12 @@ donation:
price: 1.02
custom: true
";
vmpos.Currency = "EUR";
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
var publicApps = user.GetController<UIPointOfSaleController>();
var vmview = await publicApps.ViewPointOfSale(app.Id, PosViewType.Cart).AssertViewModelAsync<ViewPointOfSaleViewModel>();
Assert.Equal("EUR", vmview.CurrencyCode);
// apple shouldn't be available since we it's set to "disabled: true" above
Assert.Equal(2, vmview.Items.Length);
Assert.Equal("orange", vmview.Items[0].Title);
@ -143,41 +139,6 @@ donation:
// apple is not found
Assert.IsType<NotFoundResult>(publicApps
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, choiceKey: "apple").Result);
// List
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
app = appList.Apps[0];
apps = user.GetController<UIAppsController>();
appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType, Settings = "{\"currency\":\"EUR\"}" };
apps.HttpContext.SetAppData(appData);
pos.HttpContext.SetAppData(appData);
Assert.Single(appList.Apps);
Assert.Equal("test", app.AppName);
Assert.True(app.Role.ToPermissionSet(appList.Apps[0].StoreId).Contains(Policies.CanModifyStoreSettings, app.StoreId));
Assert.Equal(user.StoreId, app.StoreId);
Assert.False(app.Archived);
// Archive
redirect = Assert.IsType<RedirectResult>(apps.ToggleArchive(app.Id).Result);
Assert.EndsWith("/settings/pos", redirect.Url);
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
Assert.Empty(appList.Apps);
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId, archived: true).Result).Model);
app = appList.Apps[0];
Assert.True(app.Archived);
Assert.IsType<NotFoundResult>(await publicApps.ViewPointOfSale(app.Id, PosViewType.Static));
// Unarchive
redirect = Assert.IsType<RedirectResult>(apps.ToggleArchive(app.Id).Result);
Assert.EndsWith("/settings/pos", redirect.Url);
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
app = appList.Apps[0];
Assert.False(app.Archived);
Assert.IsType<ViewResult>(await publicApps.ViewPointOfSale(app.Id, PosViewType.Static));
// Delete
Assert.IsType<ViewResult>(apps.DeleteApp(app.Id));
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(app.Id).Result);
Assert.Equal(nameof(UIStoresController.Dashboard), redirectToAction.ActionName);
appList = await apps.ListApps(user.StoreId).AssertViewModelAsync<ListAppsViewModel>();
Assert.Empty(appList.Apps);
}
}
}

@ -31,6 +31,7 @@ namespace BTCPayServer.Tests
user.RegisterDerivationScheme("BTC");
var user2 = tester.NewAccount();
await user2.GrantAccessAsync();
var paymentRequestController = user.GetController<UIPaymentRequestController>();
@ -161,7 +162,7 @@ namespace BTCPayServer.Tests
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
await user.GrantAccessAsync();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var paymentRequestController = user.GetController<UIPaymentRequestController>();
@ -169,7 +170,7 @@ namespace BTCPayServer.Tests
Assert.IsType<NotFoundResult>(await
paymentRequestController.CancelUnpaidPendingInvoice(Guid.NewGuid().ToString(), false));
var request = new UpdatePaymentRequestViewModel
var request = new UpdatePaymentRequestViewModel()
{
Title = "original juice",
Currency = "BTC",

@ -393,10 +393,6 @@ namespace BTCPayServer.Tests
public void GoToHome()
{
Driver.Navigate().GoToUrl(ServerUri);
if (Driver.PageSource.Contains("id=\"SkipWizard\""))
{
Driver.FindElement(By.Id("SkipWizard")).Click();
}
}
public void Logout()
@ -565,7 +561,7 @@ namespace BTCPayServer.Tests
walletId ??= WalletId;
GoToWallet(walletId, WalletsNavPages.Receive);
Driver.FindElement(By.Id("generateButton")).Click();
var addressStr = Driver.FindElement(By.Id("Address")).GetAttribute("data-text");
var addressStr = Driver.FindElement(By.Id("Address")).GetAttribute("value");
var address = BitcoinAddress.Create(addressStr, ((BTCPayNetwork)Server.NetworkProvider.GetNetwork(walletId.CryptoCode)).NBitcoinNetwork);
for (var i = 0; i < coins; i++)
{

@ -1,6 +1,5 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
@ -38,7 +37,6 @@ using OpenQA.Selenium.Support.Extensions;
using OpenQA.Selenium.Support.UI;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace BTCPayServer.Tests
{
@ -58,11 +56,10 @@ namespace BTCPayServer.Tests
using var s = CreateSeleniumTester();
await s.StartAsync();
s.RegisterNewUser(true);
s.GoToHome();
s.GoToServer();
s.Driver.FindElement(By.Id("Nav-ServerSettings")).Click();
s.Driver.AssertNoError();
s.ClickOnAllSectionLinks();
s.GoToServer();
s.Driver.FindElement(By.Id("Nav-ServerSettings")).Click();
s.Driver.FindElement(By.LinkText("Services")).Click();
TestLogs.LogInformation("Let's check if we can access the logs");
@ -249,8 +246,7 @@ namespace BTCPayServer.Tests
s.Server.ActivateLightning();
await s.StartAsync();
s.RegisterNewUser(true);
s.GoToHome();
s.GoToServer();
s.Driver.FindElement(By.Id("Nav-ServerSettings")).Click();
s.Driver.AssertNoError();
s.Driver.FindElement(By.LinkText("Services")).Click();
@ -317,7 +313,6 @@ namespace BTCPayServer.Tests
await s.StartAsync();
//Register & Log Out
var email = s.RegisterNewUser();
s.GoToHome();
s.Logout();
s.Driver.AssertNoError();
Assert.Contains("/login", s.Driver.Url);
@ -353,7 +348,6 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("Password")).SendKeys("abc???");
s.Driver.FindElement(By.Id("LoginButton")).Click();
s.GoToHome();
s.GoToProfile();
s.ClickOnAllSectionLinks();
@ -361,7 +355,6 @@ namespace BTCPayServer.Tests
s.Logout();
s.GoToRegister();
s.RegisterNewUser(true);
s.GoToHome();
s.GoToServer(ServerNavPages.Users);
s.Driver.FindElement(By.Id("CreateUser")).Click();
@ -384,7 +377,6 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("LoginButton")).Click();
// We should be logged in now
s.GoToHome();
s.Driver.FindElement(By.Id("mainNav"));
//let's test delete user quickly while we're at it
@ -556,24 +548,24 @@ namespace BTCPayServer.Tests
s.AddDerivationScheme();
s.GoToInvoices();
s.CreateInvoice();
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();
s.Driver.FindElement(By.Id("markStatusDropdownMenuButton")).Click();
s.Driver.FindElements(By.ClassName("changeInvoiceState"))[0].Click();
TestUtils.Eventually(() => Assert.Contains("Invalid (marked)", s.Driver.PageSource));
s.Driver.Navigate().Refresh();
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();
s.Driver.FindElement(By.Id("markStatusDropdownMenuButton")).Click();
s.Driver.FindElements(By.ClassName("changeInvoiceState"))[0].Click();
TestUtils.Eventually(() => Assert.Contains("Settled (marked)", s.Driver.PageSource));
s.Driver.Navigate().Refresh();
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();
s.Driver.FindElement(By.Id("markStatusDropdownMenuButton")).Click();
s.Driver.FindElements(By.ClassName("changeInvoiceState"))[0].Click();
TestUtils.Eventually(() => Assert.Contains("Invalid (marked)", s.Driver.PageSource));
s.Driver.Navigate().Refresh();
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();
s.Driver.FindElement(By.Id("markStatusDropdownMenuButton")).Click();
s.Driver.FindElements(By.ClassName("changeInvoiceState"))[0].Click();
TestUtils.Eventually(() => Assert.Contains("Settled (marked)", s.Driver.PageSource));
}
@ -649,7 +641,7 @@ namespace BTCPayServer.Tests
// verify redirected to create store page
Assert.EndsWith("/stores/create", s.Driver.Url);
Assert.Contains("Create your first store", s.Driver.PageSource);
Assert.Contains("Create a store to begin accepting payments", s.Driver.PageSource);
Assert.Contains("To start accepting payments, set up a store.", s.Driver.PageSource);
Assert.False(s.Driver.PageSource.Contains("id=\"StoreSelectorDropdown\""), "Store selector dropdown should not be present");
(_, string storeId) = s.CreateNewStore();
@ -810,27 +802,6 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("ConfirmContinue")).Click();
s.GoToUrl(storeUrl);
Assert.Contains("ReturnUrl", s.Driver.Url);
// Archive store
(storeName, storeId) = s.CreateNewStore();
s.Driver.FindElement(By.Id("StoreSelectorToggle")).Click();
Assert.Contains(storeName, s.Driver.FindElement(By.Id("StoreSelectorMenu")).Text);
s.Driver.FindElement(By.Id($"StoreSelectorMenuItem-{storeId}")).Click();
s.GoToStore();
s.Driver.FindElement(By.Id("btn-archive-toggle")).Click();
Assert.Contains("The store has been archived and will no longer appear in the stores list by default.", s.FindAlertMessage().Text);
s.Driver.FindElement(By.Id("StoreSelectorToggle")).Click();
Assert.DoesNotContain(storeName, s.Driver.FindElement(By.Id("StoreSelectorMenu")).Text);
Assert.Contains("1 Archived Store", s.Driver.FindElement(By.Id("StoreSelectorMenu")).Text);
s.Driver.FindElement(By.Id("StoreSelectorArchived")).Click();
var storeLink = s.Driver.FindElement(By.Id($"Store-{storeId}"));
Assert.Contains(storeName, storeLink.Text);
storeLink.Click();
s.Driver.FindElement(By.Id("btn-archive-toggle")).Click();
Assert.Contains("The store has been unarchived and will appear in the stores list by default again.", s.FindAlertMessage().Text);
}
[Fact(Timeout = TestTimeout)]
@ -927,8 +898,7 @@ namespace BTCPayServer.Tests
Policies.CanModifyStoreSettings,
Policies.CanCreateNonApprovedPullPayments,
Policies.CanCreatePullPayments,
Policies.CanManagePullPayments,
Policies.CanArchivePullPayments,
Policies.CanManagePullPayments
});
AssertPermissions(pageSource, false,
new[]
@ -991,17 +961,14 @@ namespace BTCPayServer.Tests
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.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();
var template = s.Driver.FindElement(By.Id("Template")).GetAttribute("value");
Assert.Contains("\"buyButtonText\": \"Take my money\"", template);
Assert.Matches("\"categories\": \\[\n\\s+\"Drinks\"\n\\s+\\]", template);
s.Driver.FindElement(By.Id("SaveSettings")).Click();
Assert.Contains("App updated", s.FindAlertMessage().Text);
var appId = s.Driver.Url.Split('/')[4];
s.Driver.FindElement(By.Id("ViewApp")).Click();
var windows = s.Driver.WindowHandles;
@ -1012,14 +979,6 @@ namespace BTCPayServer.Tests
Assert.True(s.Driver.PageSource.Contains("Tea shop"), "Unable to create PoS");
Assert.True(s.Driver.PageSource.Contains("Cart"), "PoS not showing correct default view");
Assert.True(s.Driver.PageSource.Contains("Take my money"), "PoS not showing correct default view");
Assert.Equal(6, s.Driver.FindElements(By.CssSelector(".posItem.posItem--displayed")).Count);
var drinks = s.Driver.FindElement(By.CssSelector("label[for='Category-Drinks']"));
Assert.Equal("Drinks", drinks.Text);
drinks.Click();
Assert.Single(s.Driver.FindElements(By.CssSelector(".posItem.posItem--displayed")));
s.Driver.FindElement(By.CssSelector("label[for='Category-*']")).Click();
Assert.Equal(6, s.Driver.FindElements(By.CssSelector(".posItem.posItem--displayed")).Count);
s.Driver.Url = posBaseUrl + "/static";
Assert.False(s.Driver.PageSource.Contains("Cart"), "Static PoS not showing correct view");
@ -1070,24 +1029,6 @@ namespace BTCPayServer.Tests
// We are only if explicitly going to /
s.GoToUrl("/");
Assert.Contains("Tea shop", s.Driver.PageSource);
// Archive
Assert.True(s.Driver.ElementDoesNotExist(By.Id("Nav-ArchivedApps")));
s.Driver.SwitchTo().Window(windows[0]);
s.Driver.FindElement(By.Id("btn-archive-toggle")).Click();
Assert.Contains("The app has been archived and will no longer appear in the apps list by default.", s.FindAlertMessage().Text);
Assert.True(s.Driver.ElementDoesNotExist(By.Id("ViewApp")));
Assert.Contains("1 Archived App", s.Driver.FindElement(By.Id("Nav-ArchivedApps")).Text);
s.Driver.Navigate().GoToUrl(posBaseUrl);
Assert.Contains("Page not found", s.Driver.Title, StringComparison.OrdinalIgnoreCase);
s.Driver.Navigate().Back();
s.Driver.FindElement(By.Id("Nav-ArchivedApps")).Click();
// Unarchive
s.Driver.FindElement(By.Id($"App-{appId}")).Click();
s.Driver.FindElement(By.Id("btn-archive-toggle")).Click();
Assert.Contains("The app has been unarchived and will appear in the apps list by default again.", s.FindAlertMessage().Text);
}
[Fact(Timeout = TestTimeout)]
@ -1107,7 +1048,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("EUR");
s.Driver.FindElement(By.Id("TargetCurrency")).SendKeys("JPY");
s.Driver.FindElement(By.Id("TargetAmount")).SendKeys("700");
// test wrong dates
@ -1121,58 +1062,17 @@ namespace BTCPayServer.Tests
s.Driver.ExecuteJavaScript("document.getElementById('EndDate').value = ''");
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);
s.Driver.SwitchTo().Window(windows[1]);
var cfUrl = s.Driver.Url;
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]);
// Archive
Assert.True(s.Driver.ElementDoesNotExist(By.Id("Nav-ArchivedApps")));
s.Driver.SwitchTo().Window(windows[0]);
s.Driver.FindElement(By.Id("btn-archive-toggle")).Click();
Assert.Contains("The app has been archived and will no longer appear in the apps list by default.", s.FindAlertMessage().Text);
Assert.True(s.Driver.ElementDoesNotExist(By.Id("ViewApp")));
Assert.Contains("1 Archived App", s.Driver.FindElement(By.Id("Nav-ArchivedApps")).Text);
s.Driver.Navigate().GoToUrl(cfUrl);
Assert.Contains("Page not found", s.Driver.Title, StringComparison.OrdinalIgnoreCase);
s.Driver.Navigate().Back();
s.Driver.FindElement(By.Id("Nav-ArchivedApps")).Click();
// Unarchive
s.Driver.FindElement(By.Id($"App-{appId}")).Click();
s.Driver.FindElement(By.Id("btn-archive-toggle")).Click();
Assert.Contains("The app has been unarchived and will appear in the apps list by default again.", s.FindAlertMessage().Text);
}
[Fact(Timeout = TestTimeout)]
@ -1182,13 +1082,13 @@ namespace BTCPayServer.Tests
await s.StartAsync();
s.RegisterNewUser();
s.CreateNewStore();
s.EnableCheckout(CheckoutType.V1);
s.AddDerivationScheme();
s.Driver.FindElement(By.Id("StoreNav-PaymentRequests")).Click();
s.Driver.FindElement(By.Id("CreatePaymentRequest")).Click();
s.Driver.FindElement(By.Id("Title")).SendKeys("Pay123");
s.Driver.FindElement(By.Id("Amount")).Clear();
s.Driver.FindElement(By.Id("Amount")).SendKeys(".01");
s.Driver.FindElement(By.Id("Amount")).SendKeys("700");
var currencyInput = s.Driver.FindElement(By.Id("Currency"));
Assert.Equal("USD", currencyInput.GetAttribute("value"));
@ -1231,7 +1131,9 @@ namespace BTCPayServer.Tests
// 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"));
// checkout v1
s.Driver.WaitForElement(By.CssSelector("invoice"));
Assert.Contains("Awaiting Payment", s.Driver.PageSource);
// amount and currency should not be editable, because invoice exists
s.GoToUrl(editUrl);
@ -1243,45 +1145,14 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("ArchivePaymentRequest")).Click();
Assert.Contains("The payment request has been archived", s.FindAlertMessage().Text);
Assert.DoesNotContain("Pay123", s.Driver.PageSource);
s.Driver.FindElement(By.Id("StatusOptionsToggle")).Click();
s.Driver.WaitForElement(By.Id("StatusOptionsIncludeArchived")).Click();
s.Driver.FindElement(By.Id("SearchDropdownToggle")).Click();
s.Driver.FindElement(By.Id("SearchIncludeArchived")).Click();
Assert.Contains("Pay123", s.Driver.PageSource);
// unarchive (from list)
s.Driver.FindElement(By.Id($"ToggleActions-{payReqId}")).Click();
s.Driver.WaitForElement(By.Id($"ToggleArchival-{payReqId}")).Click();
s.Driver.FindElement(By.Id($"ToggleArchival-{payReqId}")).Click();
Assert.Contains("The payment request has been unarchived", s.FindAlertMessage().Text);
Assert.Contains("Pay123", s.Driver.PageSource);
// payment
s.GoToUrl(viewUrl);
s.Driver.ExecuteJavaScript("document.querySelector('[data-test=\"pay-button\"]').click()");
// 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();
// Mine
s.MineBlockOnInvoiceCheckout();
TestUtils.Eventually(() =>
{
Assert.Contains("Mined 1 block",
s.Driver.WaitForElement(By.Id("CheatSuccessMessage")).Text);
});
s.GoToUrl(viewUrl);
Assert.Equal("Settled", s.Driver.WaitForElement(By.CssSelector("[data-test='status']")).Text);
}
[Fact(Timeout = TestTimeout)]
@ -1295,7 +1166,7 @@ namespace BTCPayServer.Tests
var walletId = new WalletId(storeId, "BTC");
s.GoToWallet(walletId, WalletsNavPages.Receive);
s.Driver.FindElement(By.Id("generateButton")).Click();
var addressStr = s.Driver.FindElement(By.Id("Address")).GetAttribute("data-text");
var addressStr = s.Driver.FindElement(By.Id("Address")).GetAttribute("value");
var address = BitcoinAddress.Create(addressStr,
((BTCPayNetwork)s.Server.NetworkProvider.GetNetwork("BTC")).NBitcoinNetwork);
await s.Server.ExplorerNode.GenerateAsync(1);
@ -1511,7 +1382,7 @@ namespace BTCPayServer.Tests
Assert.True(s.Driver.FindElement(By.CssSelector("#address-tab .qr-container")).Displayed);
// no previous page in the wizard, hence no back button
Assert.True(s.Driver.ElementDoesNotExist(By.Id("GoBack")));
var receiveAddr = s.Driver.FindElement(By.Id("Address")).GetAttribute("data-text");
var receiveAddr = s.Driver.FindElement(By.Id("Address")).GetAttribute("value");
// Can add a label?
await TestUtils.EventuallyAsync(async () =>
@ -1534,7 +1405,7 @@ namespace BTCPayServer.Tests
//generate it again, should be the same one as before as nothing got used in the meantime
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
Assert.True(s.Driver.FindElement(By.CssSelector("#address-tab .qr-container")).Displayed);
Assert.Equal(receiveAddr, s.Driver.FindElement(By.Id("Address")).GetAttribute("data-text"));
Assert.Equal(receiveAddr, s.Driver.FindElement(By.Id("Address")).GetAttribute("value"));
TestUtils.Eventually(() =>
{
Assert.Contains("test-label", s.Driver.PageSource);
@ -1565,8 +1436,8 @@ namespace BTCPayServer.Tests
await Task.Delay(200);
s.Driver.Navigate().Refresh();
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
Assert.NotEqual(receiveAddr, s.Driver.FindElement(By.Id("Address")).GetAttribute("data-text"));
receiveAddr = s.Driver.FindElement(By.Id("Address")).GetAttribute("data-text");
Assert.NotEqual(receiveAddr, s.Driver.FindElement(By.Id("Address")).GetAttribute("value"));
receiveAddr = s.Driver.FindElement(By.Id("Address")).GetAttribute("value");
s.Driver.FindElement(By.Id("CancelWizard")).Click();
// Check the label is applied to the tx
@ -1577,7 +1448,7 @@ namespace BTCPayServer.Tests
s.GenerateWallet(cryptoCode, "", true);
s.GoToWallet(null, WalletsNavPages.Receive);
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
Assert.NotEqual(receiveAddr, s.Driver.FindElement(By.Id("Address")).GetAttribute("data-text"));
Assert.NotEqual(receiveAddr, s.Driver.FindElement(By.Id("Address")).GetAttribute("value"));
var invoiceId = s.CreateInvoice(storeId);
var invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId);
@ -1871,7 +1742,6 @@ namespace BTCPayServer.Tests
TestUtils.Eventually(() =>
{
s.Driver.Navigate().Refresh();
s.Driver.WaitWalletTransactionsLoaded();
Assert.Contains("transaction-label", s.Driver.PageSource);
var labels = s.Driver.FindElements(By.CssSelector("#WalletTransactionsList tr:first-child div.transaction-label"));
Assert.Equal(2, labels.Count);
@ -1913,6 +1783,8 @@ namespace BTCPayServer.Tests
});
s.GoToHome();
//offline/external payout test
s.Driver.FindElement(By.Id("NotificationsHandle")).Click();
s.Driver.FindElement(By.CssSelector("#notificationsForm button")).Click();
var newStore = s.CreateNewStore();
s.GenerateWallet("BTC", "", true, true);
@ -1979,15 +1851,17 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("Currency")).SendKeys("BTC");
s.Driver.FindElement(By.Id("Create")).Click();
s.Driver.FindElement(By.LinkText("View")).Click();
// Bitcoin-only, SelectedPaymentMethod should not be displayed
s.Driver.ElementDoesNotExist(By.Id("SelectedPaymentMethod"));
var bolt = (await s.Server.CustomerLightningD.CreateInvoice(
payoutAmount,
$"LN payout test {DateTime.UtcNow.Ticks}",
TimeSpan.FromHours(1), CancellationToken.None)).BOLT11;
s.Driver.FindElement(By.Id("Destination")).SendKeys(bolt);
s.Driver.FindElement(By.Id("SelectedPaymentMethod")).Click();
s.Driver.FindElement(By.CssSelector(
$"#SelectedPaymentMethod option[value={new PaymentMethodId("BTC", PaymentTypes.LightningLike)}]"))
.Click();
s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys(Keys.Enter);
//we do not allow short-life bolts.
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Error);
@ -1998,6 +1872,11 @@ 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();
@ -2032,7 +1911,10 @@ namespace BTCPayServer.Tests
Assert.Contains(bolt, s.Driver.PageSource);
}
//auto-approve pull payments
s.GoToStore(StoreNavPages.PullPayments);
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
s.Driver.FindElement(By.Id("Name")).SendKeys("PP1");
@ -2062,8 +1944,6 @@ namespace BTCPayServer.Tests
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
s.Driver.FindElement(By.LinkText("View")).Click();
s.Driver.FindElement(By.CssSelector("#lnurlwithdraw-button")).Click();
s.Driver.WaitForElement(By.Id("qr-code-data-input"));
var 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();
var info = Assert.IsType<LNURLWithdrawRequest>(await LNURL.LNURL.FetchInformation(lnurl, s.Server.PayTester.HttpClient));
@ -2077,7 +1957,7 @@ namespace BTCPayServer.Tests
new LightMoney(0.0000001m, LightMoneyUnit.BTC),
$"LNurl w payout test {DateTime.UtcNow.Ticks}",
TimeSpan.FromHours(1), CancellationToken.None));
var response = await info.SendRequest(bolt2.BOLT11, s.Server.PayTester.HttpClient, null,null);
var response = await info.SendRequest(bolt2.BOLT11, s.Server.PayTester.HttpClient);
await TestUtils.EventuallyAsync(async () =>
{
s.Driver.Navigate().Refresh();
@ -2112,7 +1992,7 @@ namespace BTCPayServer.Tests
new LightMoney(0.0000001m, LightMoneyUnit.BTC),
$"LNurl w payout test {DateTime.UtcNow.Ticks}",
TimeSpan.FromHours(1), CancellationToken.None));
response = await info.SendRequest(bolt2.BOLT11, s.Server.PayTester.HttpClient, null,null);
response = await info.SendRequest(bolt2.BOLT11, s.Server.PayTester.HttpClient);
TestUtils.Eventually(() =>
{
s.Driver.Navigate().Refresh();
@ -2147,7 +2027,7 @@ namespace BTCPayServer.Tests
amount,
$"LNurl w payout test {DateTime.UtcNow.Ticks}",
TimeSpan.FromHours(1), CancellationToken.None));
response = await info.SendRequest(bolt2.BOLT11, s.Server.PayTester.HttpClient, null,null);
response = await info.SendRequest(bolt2.BOLT11, s.Server.PayTester.HttpClient);
await TestUtils.EventuallyAsync(async () =>
{
s.Driver.Navigate().Refresh();
@ -2166,6 +2046,7 @@ namespace BTCPayServer.Tests
using var s = CreateSeleniumTester();
s.Server.ActivateLightning();
await s.StartAsync();
await s.Server.EnsureChannelsSetup();
s.RegisterNewUser(true);
@ -2202,6 +2083,7 @@ namespace BTCPayServer.Tests
using var s = CreateSeleniumTester();
s.Server.ActivateLightning();
await s.StartAsync();
await s.Server.EnsureChannelsSetup();
s.RegisterNewUser(true);
@ -2268,122 +2150,6 @@ namespace BTCPayServer.Tests
Assert.Contains("1 222,21 €", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text);
}
[Fact]
[Trait("Selenium", "Selenium")]
[Trait("Lightning", "Lightning")]
public async Task CanUsePOSCart()
{
using var s = CreateSeleniumTester();
s.Server.ActivateLightning();
await s.StartAsync();
await s.Server.EnsureChannelsSetup();
s.RegisterNewUser(true);
s.CreateNewStore();
s.GoToStore();
s.AddLightningNode(LightningConnectionType.CLightning, false);
s.Driver.FindElement(By.Id("StoreNav-CreatePointOfSale")).Click();
s.Driver.FindElement(By.Id("AppName")).SendKeys(Guid.NewGuid().ToString());
s.Driver.FindElement(By.Id("Create")).Click();
Assert.Contains("App successfully created", s.FindAlertMessage().Text);
s.Driver.FindElement(By.CssSelector("label[for='DefaultView_Cart']")).Click();
s.Driver.FindElement(By.Id("Currency")).SendKeys("EUR");
s.Driver.FindElement(By.Id("CustomTipPercentages")).Clear();
s.Driver.FindElement(By.Id("CustomTipPercentages")).SendKeys("10,21");
s.Driver.FindElement(By.Id("SaveSettings")).Click();
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.WaitForElement(By.Id("PosItems"));
Assert.Empty(s.Driver.FindElements(By.CssSelector("#CartItems tr")));
var posUrl = s.Driver.Url;
// Select and clear
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(1) .btn-primary")).Click();
Assert.Single(s.Driver.FindElements(By.CssSelector("#CartItems tr")));
s.Driver.FindElement(By.Id("CartClear")).Click();
Thread.Sleep(250);
Assert.Empty(s.Driver.FindElements(By.CssSelector("#CartItems tr")));
// Select simple items
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(1) .btn-primary")).Click();
Thread.Sleep(250);
Assert.Single(s.Driver.FindElements(By.CssSelector("#CartItems tr")));
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(2) .btn-primary")).Click();
Thread.Sleep(250);
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(2) .btn-primary")).Click();
Thread.Sleep(250);
Assert.Equal(2, s.Driver.FindElements(By.CssSelector("#CartItems tr")).Count);
Assert.Equal("3,00 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
// Select item with inventory - two of it
Assert.Equal("5 left", s.Driver.FindElement(By.CssSelector(".posItem:nth-child(3) .badge.inventory")).Text);
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(3) .btn-primary")).Click();
Thread.Sleep(250);
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(3) .btn-primary")).Click();
Thread.Sleep(250);
Assert.Equal(3, s.Driver.FindElements(By.CssSelector("#CartItems tr")).Count);
Assert.Equal("5,40 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
// Select items with minimum amount
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(5) .btn-primary")).Click();
Thread.Sleep(250);
Assert.Equal(4, s.Driver.FindElements(By.CssSelector("#CartItems tr")).Count);
Assert.Equal("7,20 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
// Select items with adjusted minimum amount
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(5) input[name='amount']")).Clear();
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(5) input[name='amount']")).SendKeys("2.3");
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(5) .btn-primary")).Click();
Thread.Sleep(250);
Assert.Equal(5, s.Driver.FindElements(By.CssSelector("#CartItems tr")).Count);
Assert.Equal("9,50 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
// Select items with custom amount
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(6) input[name='amount']")).Clear();
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(6) input[name='amount']")).SendKeys(".2");
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(6) .btn-primary")).Click();
Thread.Sleep(250);
Assert.Equal(6, s.Driver.FindElements(By.CssSelector("#CartItems tr")).Count);
Assert.Equal("9,70 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
// Select items with another custom amount
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(6) input[name='amount']")).Clear();
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(6) input[name='amount']")).SendKeys(".3");
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(6) .btn-primary")).Click();
Thread.Sleep(250);
Assert.Equal(7, s.Driver.FindElements(By.CssSelector("#CartItems tr")).Count);
Assert.Equal("10,00 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
// Discount: 10%
s.Driver.ElementDoesNotExist(By.Id("CartDiscount"));
s.Driver.FindElement(By.Id("Discount")).SendKeys("10");
Assert.Contains("10% = 1,00 €", s.Driver.FindElement(By.Id("CartDiscount")).Text);
Assert.Equal("9,00 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
// Tip: 10%
s.Driver.ElementDoesNotExist(By.Id("CartTip"));
s.Driver.FindElement(By.Id("Tip-10")).Click();
Assert.Contains("10% = 0,90 €", s.Driver.FindElement(By.Id("CartTip")).Text);
Assert.Equal("9,90 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
// Check values on checkout page
s.Driver.FindElement(By.Id("CartSubmit")).Click();
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
s.Driver.FindElement(By.Id("DetailsToggle")).Click();
s.Driver.WaitForElement(By.Id("PaymentDetails-TotalFiat"));
Assert.Contains("9,90 €", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text);
// Pay
s.PayInvoice();
// Check inventory got updated and is now 3 instead of 5
s.Driver.Navigate().GoToUrl(posUrl);
Assert.Equal("3 left", s.Driver.FindElement(By.CssSelector(".posItem:nth-child(3) .badge.inventory")).Text);
}
[Fact]
[Trait("Selenium", "Selenium")]
[Trait("Lightning", "Lightning")]
@ -2595,11 +2361,11 @@ namespace BTCPayServer.Tests
var lnaddress1 = Guid.NewGuid().ToString();
s.Driver.FindElement(By.Id("Add_Username")).SendKeys(lnaddress1);
s.Driver.FindElement(By.CssSelector("button[value='add']")).Click();
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
s.FindAlertMessage();
s.Driver.ToggleCollapse("AddAddress");
var lnaddress2 = "EUR" + Guid.NewGuid().ToString();
var lnaddress2 = "EUR" + Guid.NewGuid();
s.Driver.FindElement(By.Id("Add_Username")).SendKeys(lnaddress2);
lnaddress2 = lnaddress2.ToLowerInvariant();
@ -2609,20 +2375,22 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("Add_Max")).SendKeys("10");
s.Driver.FindElement(By.Id("Add_InvoiceMetadata")).SendKeys("{\"test\":\"lol\"}");
s.Driver.FindElement(By.CssSelector("button[value='add']")).Click();
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
s.FindAlertMessage();
var addresses = s.Driver.FindElements(By.ClassName("lightning-address-value"));
var emailSuffix = $"@{s.Server.PayTester.HostName}:{s.Server.PayTester.Port}";
Assert.Equal(2, addresses.Count);
var callbacks = new List<Uri>();
LNURLPayRequest.LNURLPayRequestCallbackResponse lnAddressOneResponse = null;
LNURLPayRequest.LNURLPayRequestCallbackResponse lnAddressTwoResponse = null;
foreach (IWebElement webElement in addresses)
{
var value = webElement.GetAttribute("value");
//cannot test this directly as https is not supported on our e2e tests
// var request = await LNURL.LNURL.FetchPayRequestViaInternetIdentifier(value, new HttpClient());
var lnurl = new Uri(LNURL.LNURL.ExtractUriFromInternetIdentifier(value).ToString()
.Replace("https", "http"));
var request = (LNURL.LNURLPayRequest)await LNURL.LNURL.FetchInformation(lnurl, new HttpClient());
var lnurl = new Uri(LNURL.LNURL.ExtractUriFromInternetIdentifier(value).ToString().Replace("https", "http"));
var request = (LNURLPayRequest)await LNURL.LNURL.FetchInformation(lnurl, new HttpClient());
var m = request.ParsedMetadata.ToDictionary(o => o.Key, o => o.Value);
switch (value)
{
@ -2631,7 +2399,8 @@ namespace BTCPayServer.Tests
lnaddress2 = m["text/identifier"];
Assert.Equal(2, request.MinSendable.ToDecimal(LightMoneyUnit.Satoshi));
Assert.Equal(10, request.MaxSendable.ToDecimal(LightMoneyUnit.Satoshi));
callbacks.Add(request.Callback);
lnAddressTwoResponse = await request.SendRequest(request.MinSendable, ((BTCPayNetwork)s.Server.DefaultNetwork).NBitcoinNetwork,
new HttpClient());
break;
case { } v when v.StartsWith(lnaddress1):
@ -2639,47 +2408,29 @@ namespace BTCPayServer.Tests
lnaddress1 = m["text/identifier"];
Assert.Equal(1, request.MinSendable.ToDecimal(LightMoneyUnit.Satoshi));
Assert.Equal(6.12m, request.MaxSendable.ToDecimal(LightMoneyUnit.BTC));
callbacks.Add(request.Callback);
lnAddressOneResponse = await request.SendRequest(request.MinSendable, ((BTCPayNetwork)s.Server.DefaultNetwork).NBitcoinNetwork,
new HttpClient());
break;
case not null when value.Equals($"{lnaddress2}{emailSuffix}"):
lnaddress2 = m["text/identifier"];
Assert.Equal(2, request.MinSendable.ToDecimal(LightMoneyUnit.Satoshi));
Assert.Equal(10, request.MaxSendable.ToDecimal(LightMoneyUnit.Satoshi));
break;
default:
Assert.False(true, "Should have matched");
Assert.False(true, "Should have matched one of the Lightning addresses");
break;
}
}
// Check that no BTCPay invoice got generated on initial LNURL request
var repo = s.Server.PayTester.GetService<InvoiceRepository>();
var invoices = await repo.GetInvoices(new InvoiceQuery() { StoreId = new[] { s.StoreId } });
// Resolving a ln address shouldn't create any btcpay invoice.
// This must be done because some NOST clients resolve ln addresses preemptively without user interaction
var invoices = await repo.GetInvoices(new InvoiceQuery { StoreId = new[] { s.StoreId } });
Assert.Empty(invoices);
// Calling the callbacks should create the invoices
foreach (var callback in callbacks)
{
using var r = await s.Server.PayTester.HttpClient.GetAsync(callback);
await r.Content.ReadAsStringAsync();
}
invoices = await repo.GetInvoices(new InvoiceQuery() { StoreId = new[] { s.StoreId } });
Assert.Equal(2, invoices.Length);
var emailSuffix = $"@{s.Server.PayTester.HostName}:{s.Server.PayTester.Port}";
foreach (var i in invoices)
{
var lightningPaymentMethod = i.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.LNURLPay));
var paymentMethodDetails =
lightningPaymentMethod.GetPaymentMethodDetails() as LNURLPayPaymentMethodDetails;
Assert.Contains(
paymentMethodDetails.ConsumedLightningAddress,
new[] { lnaddress1, lnaddress2 });
if (paymentMethodDetails.ConsumedLightningAddress == lnaddress2)
{
Assert.Equal("lol", i.Metadata.AdditionalData["test"].Value<string>());
}
}
var lnUsername = lnaddress1.Split('@')[0];
LNURLPayRequest req;
using (var resp = await s.Server.PayTester.HttpClient.GetAsync($"/.well-known/lnurlp/{lnUsername}"))
{
@ -2732,6 +2483,25 @@ namespace BTCPayServer.Tests
Assert.NotNull(succ.Pr);
Assert.Equal(new LightMoney(2001), BOLT11PaymentRequest.Parse(succ.Pr, Network.RegTest).MinimumAmount);
}
// Again, check the invoices
invoices = await repo.GetInvoices(new InvoiceQuery { StoreId = new[] { s.StoreId } });
Assert.Equal(2, invoices.Length);
foreach (var i in invoices)
{
var lightningPaymentMethod = i.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.LNURLPay));
var paymentMethodDetails =
lightningPaymentMethod.GetPaymentMethodDetails() as LNURLPayPaymentMethodDetails;
Assert.Contains(
paymentMethodDetails.ConsumedLightningAddress,
new[] { lnaddress1, lnaddress2 });
if (paymentMethodDetails.ConsumedLightningAddress == lnaddress2)
{
Assert.Equal("lol", i.Metadata.AdditionalData["test"].Value<string>());
}
}
}
[Fact]
@ -2741,7 +2511,6 @@ namespace BTCPayServer.Tests
using var s = CreateSeleniumTester();
await s.StartAsync();
var user = s.RegisterNewUser();
s.GoToHome();
s.GoToProfile(ManageNavPages.LoginCodes);
var code = s.Driver.FindElement(By.Id("logincode")).GetAttribute("value");
s.Driver.FindElement(By.Id("regeneratecode")).Click();
@ -2753,12 +2522,14 @@ namespace BTCPayServer.Tests
s.Driver.SetAttribute("LoginCode", "value", "bad code");
s.Driver.InvokeJSFunction("logincode-form", "submit");
s.Driver.SetAttribute("LoginCode", "value", code);
s.Driver.InvokeJSFunction("logincode-form", "submit");
s.GoToHome();
s.GoToProfile();
Assert.Contains(user, s.Driver.PageSource);
}
// For god know why, selenium have problems clicking on the save button, resulting in ultimate hacks
// to make it works.
private void SudoForceSaveLightningSettingsRightNowAndFast(SeleniumTester s, string cryptoCode)
@ -2777,6 +2548,7 @@ retry:
}
}
[Fact]
[Trait("Selenium", "Selenium")]
public async Task CanUseLNURLAuth()
@ -2784,7 +2556,6 @@ retry:
using var s = CreateSeleniumTester();
await s.StartAsync();
var user = s.RegisterNewUser(true);
s.GoToHome();
s.GoToProfile(ManageNavPages.TwoFactorAuthentication);
s.Driver.FindElement(By.Name("Name")).SendKeys("ln wallet");
s.Driver.FindElement(By.Name("type"))
@ -2833,8 +2604,7 @@ retry:
{
using var s = CreateSeleniumTester(newDb: true);
await s.StartAsync();
s.RegisterNewUser(true);
s.GoToHome();
var user = s.RegisterNewUser(true);
s.GoToServer(ServerNavPages.Roles);
var existingServerRoles = s.Driver.FindElement(By.CssSelector("table")).FindElements(By.CssSelector("tr"));
Assert.Equal(3, existingServerRoles.Count);

@ -40,7 +40,6 @@ namespace BTCPayServer.Tests
public class TestAccount
{
readonly ServerTester parent;
public string LNAddress;
public TestAccount(ServerTester parent)
{
@ -243,7 +242,7 @@ namespace BTCPayServer.Tests
policies.LockSubscription = false;
await account.Register(RegisterDetails);
}
TestLogs.LogInformation($"UserId: {account.RegisteredUserId} Password: {Password}");
UserId = account.RegisteredUserId;
Email = RegisterDetails.Email;
IsAdmin = account.RegisteredAdmin;
@ -310,9 +309,8 @@ namespace BTCPayServer.Tests
Assert.False(true, storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage);
}
public async Task<Coin> ReceiveUTXO(Money value, BTCPayNetwork network = null)
public async Task<Coin> ReceiveUTXO(Money value, BTCPayNetwork network)
{
network ??= SupportedNetwork;
var cashCow = parent.ExplorerNode;
var btcPayWallet = parent.PayTester.GetService<BTCPayWalletProvider>().GetWallet(network);
var address = (await btcPayWallet.ReserveAddressAsync(this.DerivationScheme)).Address;
@ -555,94 +553,5 @@ retry:
var repo = this.parent.PayTester.GetService<StoreRepository>();
await repo.AddStoreUser(StoreId, userId, StoreRoleId.Owner);
}
public async Task<uint256> PayOnChain(string invoiceId)
{
var cryptoCode = "BTC";
var client = await CreateClient();
var methods = await client.GetInvoicePaymentMethods(StoreId, invoiceId);
var method = methods.First(m => m.PaymentMethod == cryptoCode);
var address = method.Destination;
var tx = await client.CreateOnChainTransaction(StoreId, cryptoCode, new CreateOnChainTransactionRequest()
{
Destinations = new List<CreateOnChainTransactionRequest.CreateOnChainTransactionRequestDestination>()
{
new ()
{
Destination = address,
Amount = method.Due
}
},
FeeRate = new FeeRate(1.0m)
});
await WaitInvoicePaid(invoiceId);
return tx.TransactionHash;
}
public async Task PayOnBOLT11(string invoiceId)
{
var cryptoCode = "BTC";
var client = await CreateClient();
var methods = await client.GetInvoicePaymentMethods(StoreId, invoiceId);
var method = methods.First(m => m.PaymentMethod == $"{cryptoCode}-LightningNetwork");
var bolt11 = method.Destination;
TestLogs.LogInformation("PAYING");
await parent.CustomerLightningD.Pay(bolt11);
TestLogs.LogInformation("PAID");
await WaitInvoicePaid(invoiceId);
}
public async Task PayOnLNUrl(string invoiceId)
{
var cryptoCode = "BTC";
var network = SupportedNetwork.NBitcoinNetwork;
var client = await CreateClient();
var methods = await client.GetInvoicePaymentMethods(StoreId, invoiceId);
var method = methods.First(m => m.PaymentMethod == $"{cryptoCode}-LNURLPAY");
var lnurL = LNURL.LNURL.Parse(method.PaymentLink, out var tag);
var http = new HttpClient();
var payreq = (LNURL.LNURLPayRequest)await LNURL.LNURL.FetchInformation(lnurL, tag, http);
var resp = await payreq.SendRequest(payreq.MinSendable, network, http);
var bolt11 = resp.Pr;
await parent.CustomerLightningD.Pay(bolt11);
await WaitInvoicePaid(invoiceId);
}
public Task WaitInvoicePaid(string invoiceId)
{
return TestUtils.EventuallyAsync(async () =>
{
var client = await CreateClient();
var invoice = await client.GetInvoice(StoreId, invoiceId);
if (invoice.Status == InvoiceStatus.Settled)
return;
Assert.Equal(InvoiceStatus.Processing, invoice.Status);
});
}
public async Task PayOnLNAddress(string lnAddrUser = null)
{
lnAddrUser ??= LNAddress;
var network = SupportedNetwork.NBitcoinNetwork;
var payReqStr = await (await parent.PayTester.HttpClient.GetAsync($".well-known/lnurlp/{lnAddrUser}")).Content.ReadAsStringAsync();
var payreq = JsonConvert.DeserializeObject<LNURL.LNURLPayRequest>(payReqStr);
var resp = await payreq.SendRequest(payreq.MinSendable, network, parent.PayTester.HttpClient);
var bolt11 = resp.Pr;
await parent.CustomerLightningD.Pay(bolt11);
}
public async Task<string> CreateLNAddress()
{
var lnAddrUser = Guid.NewGuid().ToString();
var ctx = parent.PayTester.GetService<ApplicationDbContextFactory>().CreateContext();
ctx.LightningAddresses.Add(new()
{
StoreDataId = StoreId,
Username = lnAddrUser
});
await ctx.SaveChangesAsync();
LNAddress = lnAddrUser;
return lnAddrUser;
}
}
}

@ -16,14 +16,12 @@ using BTCPayServer.Storage.Models;
using BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.FileSystemGlobbing;
using NBitcoin;
using NBitpayClient;
using Newtonsoft.Json;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
using static BTCPayServer.HostedServices.PullPaymentHostedService.PayoutApproval;
namespace BTCPayServer.Tests
{
@ -179,7 +177,7 @@ namespace BTCPayServer.Tests
Assert.Contains(rates, e => e.CurrencyPair == new CurrencyPair("XMR", "BTC") && e.BidAsk.Bid < 1.0m);
// Check we didn't skip too many exchanges
Assert.InRange(skipped, 0, 5);
Assert.InRange(skipped, 0, 3);
}
[Fact]
@ -292,43 +290,9 @@ retry:
}
[Fact]
public async Task CanGetRateFromRecommendedExchanges()
public void CanGetRateCryptoCurrenciesByDefault()
{
var factory = FastTests.CreateBTCPayRateFactory();
var fetcher = new RateFetcher(factory);
var provider = new BTCPayNetworkProvider(ChainName.Mainnet);
var b = new StoreBlob();
string[] temporarilyBroken = { "COP", "UGX" };
foreach (var k in StoreBlob.RecommendedExchanges)
{
b.DefaultCurrency = k.Key;
var rules = b.GetDefaultRateRules(provider);
var pairs = new[] { CurrencyPair.Parse($"BTC_{k.Key}") }.ToHashSet();
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))
{
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");
}
Assert.True(hasRate, $"Impossible to get the rate {rateResult.EvaluatedRule}");
}
}
}
[Fact]
public async Task CanGetRateCryptoCurrenciesByDefault()
{
using var cts = new CancellationTokenSource(60_000);
string[] brokenShitcoins = { "BTX_USD", "CHC_USD" };
var provider = new BTCPayNetworkProvider(ChainName.Mainnet);
var factory = FastTests.CreateBTCPayRateFactory();
var fetcher = new RateFetcher(factory);
@ -337,29 +301,21 @@ retry:
.Select(c => new CurrencyPair(c.CryptoCode, "USD"))
.ToHashSet();
string[] brokenShitcoins = { "BTG", "BTX" };
bool IsBrokenShitcoin(CurrencyPair p) => brokenShitcoins.Contains(p.Left) || brokenShitcoins.Contains(p.Right);
foreach (var _ in brokenShitcoins)
{
foreach (var p in pairs.Where(IsBrokenShitcoin).ToArray())
{
TestLogs.LogInformation($"Skipping {p} because it is marked as broken");
pairs.Remove(p);
}
}
var rules = new StoreBlob().GetDefaultRateRules(provider);
var result = fetcher.FetchRates(pairs, rules, cts.Token);
var result = fetcher.FetchRates(pairs, rules, default);
foreach ((CurrencyPair key, Task<RateResult> value) in result)
{
var rateResult = await value;
var rateResult = value.GetAwaiter().GetResult();
if (key.ToString() == "BTG_USD")
continue; // shitcoin not supported by bitfinex anymore
TestLogs.LogInformation($"Testing {key}");
if (brokenShitcoins.Contains(key.ToString()))
continue;
Assert.True(rateResult.BidAsk != null, $"Impossible to get the rate {rateResult.EvaluatedRule}");
}
}
[Fact]
[Trait("Fast", "Fast")]
public async Task CheckJsContent()
{
// This test verify that no malicious js is added in the minified files.
@ -368,77 +324,52 @@ retry:
var actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "bootstrap", "bootstrap.bundle.min.js").Trim();
var version = Regex.Match(actual, "Bootstrap v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
var expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/bootstrap@{version}/dist/js/bootstrap.bundle.min.js")).Content.ReadAsStringAsync()).Trim();
EqualJsContent(expected, actual);
Assert.Equal(expected, actual);
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "clipboard.js", "clipboard.js");
expected = (await (await client.GetAsync("https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.8/clipboard.js")).Content.ReadAsStringAsync()).Trim();
EqualJsContent(expected, actual);
Assert.Equal(expected, actual);
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vuejs", "vue.min.js").Trim();
version = Regex.Match(actual, "Vue\\.js v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
expected = (await (await client.GetAsync($"https://cdnjs.cloudflare.com/ajax/libs/vue/{version}/vue.min.js")).Content.ReadAsStringAsync()).Trim();
EqualJsContent(expected, actual);
Assert.Equal(expected, actual);
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "i18next", "i18next.min.js").Trim();
expected = (await (await client.GetAsync("https://cdnjs.cloudflare.com/ajax/libs/i18next/22.0.6/i18next.min.js")).Content.ReadAsStringAsync()).Trim();
EqualJsContent(expected, actual);
Assert.Equal(expected, actual);
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "i18next", "i18nextHttpBackend.min.js").Trim();
expected = (await (await client.GetAsync("https://cdnjs.cloudflare.com/ajax/libs/i18next-http-backend/2.0.1/i18nextHttpBackend.min.js")).Content.ReadAsStringAsync()).Trim();
EqualJsContent(expected, actual);
Assert.Equal(expected, actual);
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "i18next", "vue-i18next.js").Trim();
expected = (await (await client.GetAsync("https://unpkg.com/@panter/vue-i18next@0.15.2/dist/vue-i18next.js")).Content.ReadAsStringAsync()).Trim();
EqualJsContent(expected, actual);
Assert.Equal(expected, actual);
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vue-qrcode", "vue-qrcode.min.js").Trim();
version = Regex.Match(actual, "vue-qrcode v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
expected = (await (await client.GetAsync($"https://unpkg.com/@chenfengyuan/vue-qrcode@{version}/dist/vue-qrcode.min.js")).Content.ReadAsStringAsync()).Trim();
EqualJsContent(expected, actual);
Assert.Equal(expected, actual);
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "tom-select", "tom-select.complete.min.js").Trim();
version = Regex.Match(actual, "Tom Select v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/tom-select@{version}/dist/js/tom-select.complete.min.js")).Content.ReadAsStringAsync()).Trim();
EqualJsContent(expected, actual);
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/js/tom-select.complete.min.js")).Content.ReadAsStringAsync()).Trim();
Assert.Equal(expected, actual);
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "dom-confetti", "dom-confetti.min.js").Trim();
version = Regex.Match(actual, "Original file: /npm/dom-confetti@([0-9]+.[0-9]+.[0-9]+)/lib/main.js").Groups[1].Value;
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/dom-confetti@{version}/lib/main.min.js")).Content.ReadAsStringAsync()).Trim();
EqualJsContent(expected, actual);
Assert.Equal(expected, actual);
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vue-sortable", "sortable.min.js").Trim();
version = Regex.Match(actual, "Sortable ([0-9]+.[0-9]+.[0-9]+) ").Groups[1].Value;
expected = (await (await client.GetAsync($"https://unpkg.com/sortablejs@{version}/Sortable.min.js")).Content.ReadAsStringAsync()).Trim();
EqualJsContent(expected, actual);
Assert.Equal(expected, actual);
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "bootstrap-vue", "bootstrap-vue.min.js").Trim();
version = Regex.Match(actual, "BootstrapVue ([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
expected = (await (await client.GetAsync($"https://cdnjs.cloudflare.com/ajax/libs/bootstrap-vue/{version}/bootstrap-vue.min.js")).Content.ReadAsStringAsync()).Trim();
EqualJsContent(expected, actual);
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "FileSaver", "FileSaver.min.js").Trim();
expected = (await (await client.GetAsync($"https://raw.githubusercontent.com/eligrey/FileSaver.js/43bbd2f0ae6794f8d452cd360e9d33aef6071234/dist/FileSaver.min.js")).Content.ReadAsStringAsync()).Trim();
EqualJsContent(expected, actual);
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "papaparse", "papaparse.min.js").Trim();
expected = (await (await client.GetAsync($"https://raw.githubusercontent.com/mholt/PapaParse/5.4.1/papaparse.min.js")).Content.ReadAsStringAsync()).Trim();
EqualJsContent(expected, actual);
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vue-sanitize-directive", "vue-sanitize-directive.umd.min.js").Trim();
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)
{
if (expected != actual)
Assert.Equal(expected, actual.ReplaceLineEndings("\n"));
Assert.Equal(expected, actual);
}
string GetFileContent(params string[] path)

@ -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.ToDecimal(MoneyUnit.BTC),
Assert.Equal(newInvoice.BtcDue.GetValue(),
BOLT11PaymentRequest.Parse(newBolt11, Network.RegTest).MinimumAmount.ToDecimal(LightMoneyUnit.BTC));
}, 40000);
TestLogs.LogInformation($"Paying invoice {newInvoice.Id} remaining due amount {newInvoice.BtcDue.GetValue((BTCPayNetwork) tester.DefaultNetwork)} via lightning");
TestLogs.LogInformation($"Paying invoice {newInvoice.Id} remaining due amount {newInvoice.BtcDue.GetValue()} via lightning");
var evt = await tester.WaitForEvent<InvoiceDataChangedEvent>(async () =>
{
await tester.SendLightningPaymentAsync(newInvoice);
@ -1700,6 +1700,109 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanExportInvoicesJson()
{
decimal GetFieldValue(string input, string fieldName)
{
var match = Regex.Match(input, $"\"{fieldName}\":([^,]*)");
Assert.True(match.Success);
return decimal.Parse(match.Groups[1].Value.Trim(), CultureInfo.InvariantCulture);
}
async Task<object[]> GetExport(TestAccount account, string storeId = null)
{
var content = await account.GetController<UIInvoiceController>(false)
.Export("json", storeId);
var result = Assert.IsType<ContentResult>(content);
Assert.Equal("application/json", result.ContentType);
return JsonConvert.DeserializeObject<object[]>(result.Content ?? "[]");
}
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
await user.GrantAccessAsync();
user.RegisterDerivationScheme("BTC");
await user.SetNetworkFeeMode(NetworkFeeMode.Always);
var invoice = await user.BitPay.CreateInvoiceAsync(
new Invoice
{
Price = 10,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some \", description",
FullNotifications = true
}, Facade.Merchant);
var networkFee = new FeeRate(invoice.MinerFees["BTC"].SatoshiPerBytes).GetFee(100);
var result = await GetExport(user);
Assert.Single(result);
var cashCow = tester.ExplorerNode;
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
var firstPayment = invoice.CryptoInfo[0].TotalDue - 3 * networkFee;
cashCow.SendToAddress(invoiceAddress, firstPayment);
Thread.Sleep(1000); // prevent race conditions, ordering payments
// look if you can reduce thread sleep, this was min value for me
// should reduce invoice due by 0 USD because payment = network fee
cashCow.SendToAddress(invoiceAddress, networkFee);
Thread.Sleep(1000);
// pay remaining amount
cashCow.SendToAddress(invoiceAddress, 4 * networkFee);
Thread.Sleep(1000);
await TestUtils.EventuallyAsync(async () =>
{
var parsedJson = await GetExport(user);
Assert.Equal(3, parsedJson.Length);
var invoiceDueAfterFirstPayment = (3 * networkFee).ToDecimal(MoneyUnit.BTC) * invoice.Rate;
var pay1str = parsedJson[0].ToString();
Assert.Contains("\"InvoiceItemDesc\": \"Some \\\", description\"", pay1str);
Assert.Equal(invoiceDueAfterFirstPayment, GetFieldValue(pay1str, "InvoiceDue"));
Assert.Contains("\"InvoicePrice\": 10.0", pay1str);
Assert.Contains("\"ConversionRate\": 5000.0", pay1str);
Assert.Contains($"\"InvoiceId\": \"{invoice.Id}\",", pay1str);
var pay2str = parsedJson[1].ToString();
Assert.Equal(invoiceDueAfterFirstPayment, GetFieldValue(pay2str, "InvoiceDue"));
var pay3str = parsedJson[2].ToString();
Assert.Contains("\"InvoiceDue\": 0", pay3str);
});
// create an invoice for a new store and check responses with and without store id
var otherUser = tester.NewAccount();
await otherUser.GrantAccessAsync();
otherUser.RegisterDerivationScheme("BTC");
await otherUser.SetNetworkFeeMode(NetworkFeeMode.Always);
var newInvoice = await otherUser.BitPay.CreateInvoiceAsync(
new Invoice
{
Price = 21,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some \", description",
FullNotifications = true
}, Facade.Merchant);
await otherUser.PayInvoice(newInvoice.Id);
Assert.Single(await GetExport(otherUser));
Assert.Single(await GetExport(otherUser, otherUser.StoreId));
Assert.Equal(3, (await GetExport(user, user.StoreId)).Length);
Assert.Equal(3, (await GetExport(user)).Length);
await otherUser.AddOwner(user.UserId);
Assert.Equal(4, (await GetExport(user)).Length);
Assert.Single(await GetExport(user, otherUser.StoreId));
}
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanChangeNetworkFeeMode()
@ -1789,6 +1892,45 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanExportInvoicesCsv()
{
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
await user.GrantAccessAsync();
user.RegisterDerivationScheme("BTC");
await user.SetNetworkFeeMode(NetworkFeeMode.Always);
var invoice = user.BitPay.CreateInvoice(
new Invoice
{
Price = 500,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some \", description",
FullNotifications = true
}, Facade.Merchant);
var cashCow = tester.ExplorerNode;
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
var firstPayment = invoice.CryptoInfo[0].TotalDue - Money.Coins(0.001m);
cashCow.SendToAddress(invoiceAddress, firstPayment);
TestUtils.Eventually(() =>
{
var exportResultPaid =
user.GetController<UIInvoiceController>().Export("csv").GetAwaiter().GetResult();
var paidresult = Assert.IsType<ContentResult>(exportResultPaid);
Assert.Equal("application/csv", paidresult.ContentType);
Assert.Contains($",orderId,{invoice.Id},", paidresult.Content);
Assert.Contains($",On-Chain,BTC,0.0991,0.0001,5000.0", paidresult.Content);
Assert.Contains($",USD,5.00", paidresult.Content); // Seems hacky but some plateform does not render this decimal the same
Assert.Contains("0,,\"Some \"\", description\",New (paidPartial),new,paidPartial",
paidresult.Content);
});
}
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanCreateAndDeleteApps()
@ -2717,7 +2859,7 @@ namespace BTCPayServer.Tests
using var tester = CreateServerTester();
await tester.StartAsync();
var user = tester.NewAccount();
await user.GrantAccessAsync();
user.GrantAccess();
var controller = tester.PayTester.GetController<UIServerController>(user.UserId, user.StoreId);
var fileSystemStorageConfiguration = Assert.IsType<FileSystemStorageConfiguration>(Assert
@ -2732,6 +2874,7 @@ namespace BTCPayServer.Tests
Assert.Equal(StorageProvider.FileSystem,
shouldBeRedirectingToLocalStorageConfigPage.RouteValues["provider"]);
await CanUploadRemoveFiles(controller);
}
@ -2763,7 +2906,7 @@ namespace BTCPayServer.Tests
//create a temporary link to file
var tmpLinkGenerate = Assert.IsType<RedirectToActionResult>(await controller.CreateTemporaryFileUrl(fileId,
new UIServerController.CreateTemporaryFileUrlViewModel
new UIServerController.CreateTemporaryFileUrlViewModel()
{
IsDownload = true,
TimeAmount = 1,
@ -2793,124 +2936,5 @@ namespace BTCPayServer.Tests
Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files(new string[] { fileId })).Model);
Assert.Null(viewFilesViewModel.DirectUrlByFiles);
}
[Fact]
[Trait("Selenium", "Selenium")]
public async Task CanCreateReports()
{
using var tester = CreateServerTester();
tester.ActivateLightning();
tester.DeleteStore = false;
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var acc = tester.NewAccount();
await acc.GrantAccessAsync();
await acc.MakeAdmin();
acc.RegisterDerivationScheme("BTC", importKeysToNBX: true);
acc.RegisterLightningNode("BTC");
await acc.ReceiveUTXO(Money.Coins(1.0m));
var client = await acc.CreateClient();
var posController = acc.GetController<UIPointOfSaleController>();
var app = await client.CreatePointOfSaleApp(acc.StoreId, new CreatePointOfSaleAppRequest()
{
AppName = "Static",
DefaultView = Client.Models.PosViewType.Static,
Template = new PointOfSaleSettings().Template
});
var resp = await posController.ViewPointOfSale(app.Id, choiceKey: "green-tea");
var invoiceId = GetInvoiceId(resp);
await acc.PayOnChain(invoiceId);
app = await client.CreatePointOfSaleApp(acc.StoreId, new CreatePointOfSaleAppRequest()
{
AppName = "Cart",
DefaultView = Client.Models.PosViewType.Cart,
Template = new PointOfSaleSettings().Template
});
resp = await posController.ViewPointOfSale(app.Id, posData: new JObject()
{
["cart"] = new JArray()
{
new JObject()
{
["id"] = "green-tea",
["count"] = 2
},
new JObject()
{
["id"] = "black-tea",
["count"] = 1
},
}
}.ToString());
invoiceId = GetInvoiceId(resp);
await acc.PayOnBOLT11(invoiceId);
resp = await posController.ViewPointOfSale(app.Id, posData: new JObject()
{
["cart"] = new JArray()
{
new JObject()
{
["id"] = "green-tea",
["count"] = 5
}
}
}.ToString());
invoiceId = GetInvoiceId(resp);
await acc.PayOnLNUrl(invoiceId);
await acc.CreateLNAddress();
await acc.PayOnLNAddress();
var report = await GetReport(acc, new() { ViewName = "Payments" });
// 1 payment on LN Address
// 1 payment on LNURL
// 1 payment on BOLT11
// 1 payment on chain
Assert.Equal(4, report.Data.Count);
var lnAddressIndex = report.GetIndex("LightningAddress");
var paymentTypeIndex = report.GetIndex("PaymentType");
Assert.Contains(report.Data, d => d[lnAddressIndex]?.Value<string>()?.Contains(acc.LNAddress) is true);
var paymentTypes = report.Data
.GroupBy(d => d[paymentTypeIndex].Value<string>())
.ToDictionary(d => d.Key);
Assert.Equal(3, paymentTypes["Lightning"].Count());
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" });
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]["v"].Value<decimal>() == 1.0m);
// Items sold
report = await GetReport(acc, new() { ViewName = "Products sold" });
var itemIndex = report.GetIndex("Product");
var countIndex = report.GetIndex("Quantity");
var itemsCount = report.Data.GroupBy(d => d[itemIndex].Value<string>())
.ToDictionary(d => d.Key, r => r.Sum(d => d[countIndex].Value<int>()));
Assert.Equal(8, itemsCount["green-tea"]);
Assert.Equal(1, itemsCount["black-tea"]);
}
private async Task<StoreReportResponse> GetReport(TestAccount acc, StoreReportRequest req)
{
var controller = acc.GetController<UIReportsController>();
return (await controller.StoreReportsJson(acc.StoreId, req)).AssertType<JsonResult>()
.Value
.AssertType<StoreReportResponse>();
}
private static string GetInvoiceId(IActionResult resp)
{
var redirect = resp.AssertType<RedirectToActionResult>();
Assert.Equal("Checkout", redirect.ActionName);
return (string)redirect.RouteValues["invoiceId"];
}
}
}

@ -99,7 +99,7 @@ services:
custom:
nbxplorer:
image: nicolasdorier/nbxplorer:2.3.66
image: nicolasdorier/nbxplorer:2.3.63
restart: unless-stopped
ports:
- "32838:32838"
@ -163,7 +163,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: btcpayserver/lightning:v23.08-dev
image: btcpayserver/lightning:v23.05-dev
stop_signal: SIGKILL
restart: unless-stopped
environment:
@ -190,7 +190,7 @@ services:
- bitcoind
merchant_lightningd:
image: btcpayserver/lightning:v23.08-dev
image: btcpayserver/lightning:v23.05-dev
stop_signal: SIGKILL
environment:
EXPOSE_TCP: "true"
@ -224,7 +224,7 @@ services:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:v0.16.4-beta-1
image: btcpayserver/lnd:v0.16.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.16.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.18.2.2-5
image: btcpayserver/monero:0.17.0.0-amd64
restart: unless-stopped
container_name: xmr_monerod
entrypoint: sleep 999999
@ -317,7 +317,7 @@ services:
ports:
- "18081:18081"
monero_wallet:
image: btcpayserver/monero:0.18.2.2-5
image: btcpayserver/monero:0.17.0.0-amd64
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.2-4
image: btcpayserver/elements:0.21.0.1
environment:
ELEMENTS_CHAIN: elementsregtest
ELEMENTS_EXTRA_ARGS: |
@ -364,9 +364,11 @@ services:
whitelist=0.0.0.0/0
rpcallowip=0.0.0.0/0
validatepegin=0
initialfreecoins=2100000000000000
initialfreecoins=210000000000000
con_dyna_deploy_signal=1
con_dyna_deploy_start=10
con_dyna_deploy_start=0
con_nminerconfirmationwindow=1
con_nrulechangeactivationthreshold=1
expose:
- "19332"
- "19444"

@ -96,7 +96,7 @@ services:
custom:
nbxplorer:
image: nicolasdorier/nbxplorer:2.3.66
image: nicolasdorier/nbxplorer:2.3.63
restart: unless-stopped
ports:
- "32838:32838"
@ -149,7 +149,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: btcpayserver/lightning:v23.08-dev
image: btcpayserver/lightning:v23.05-dev
stop_signal: SIGKILL
restart: unless-stopped
environment:
@ -176,7 +176,7 @@ services:
- bitcoind
merchant_lightningd:
image: btcpayserver/lightning:v23.08-dev
image: btcpayserver/lightning:v23.05-dev
stop_signal: SIGKILL
environment:
EXPOSE_TCP: "true"
@ -211,7 +211,7 @@ services:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:v0.16.4-beta-1
image: btcpayserver/lnd:v0.16.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.16.2-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"

@ -48,18 +48,19 @@
<PackageReference Include="YamlDotNet" Version="8.0.0" />
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.4.31" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.4.28" />
<PackageReference Include="CsvHelper" Version="15.0.5" />
<PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="Fido2" Version="2.0.2" />
<PackageReference Include="Fido2.AspNet" Version="2.0.2" />
<PackageReference Include="LNURL" Version="0.0.34" />
<PackageReference Include="HtmlSanitizer" Version="5.0.372" />
<PackageReference Include="LNURL" Version="0.0.29" />
<PackageReference Include="MailKit" Version="3.3.0" />
<PackageReference Include="BTCPayServer.NETCore.Plugins.Mvc" Version="1.4.4" />
<PackageReference Include="QRCoder" Version="1.4.3" />
<PackageReference Include="System.IO.Pipelines" Version="6.0.3" />
<PackageReference Include="NBitpayClient" Version="1.0.0.39" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NicolasDorier.CommandLine" Version="2.0.0" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="2.0.0" />
<PackageReference Include="NicolasDorier.RateLimits" Version="1.2.3" />
@ -80,7 +81,6 @@
</ItemGroup>
<ItemGroup>
<None Include="Views\UIReports\StoreReports.cshtml" />
<None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.svg" />
<None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.woff2" />
<None Include="wwwroot\vendor\font-awesome\less\animated.less" />
@ -119,7 +119,6 @@
<Folder Include="wwwroot\vendor\bootstrap" />
<Folder Include="wwwroot\vendor\clipboard.js\" />
<Folder Include="wwwroot\vendor\highlightjs\" />
<Folder Include="wwwroot\vendor\pivottable\" />
<Folder Include="wwwroot\vendor\summernote" />
<Folder Include="wwwroot\vendor\tom-select" />
<Folder Include="wwwroot\vendor\ur-registry" />
@ -137,7 +136,6 @@
<ItemGroup>
<Watch Include="Views\**\*.*"></Watch>
<Watch Remove="Views\UIAccount\CheatPermissions.cshtml" />
<Watch Remove="Views\UIReports\StoreReports.cshtml" />
<Content Update="Views\UIApps\_ViewImports.cshtml">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<Pack>$(IncludeRazorContentInPack)</Pack>

@ -1,14 +0,0 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.JSInterop;
namespace BTCPayServer.Blazor
{
public static class BlazorExtensions
{
public static bool IsPreRendering(this IJSRuntime runtime)
{
// The peculiar thing in prerender is that Blazor circuit isn't yet created, so we can't use JSInterop
return !(bool)runtime.GetType().GetProperty("IsInitialized").GetValue(runtime);
}
}
}

@ -1,22 +0,0 @@
@using BTCPayServer.Abstractions.Extensions;
@using BTCPayServer.Configuration;
@using Microsoft.AspNetCore.Hosting;
@using Microsoft.AspNetCore.Mvc.Routing;
@using Microsoft.AspNetCore.Mvc.ViewFeatures;
@using Microsoft.AspNetCore.Mvc;
@inject IFileVersionProvider FileVersionProvider
@inject BTCPayServerOptions BTCPayServerOptions
<svg role="img" class="icon icon-@Symbol">
<use href="@GetPathTo(Symbol)"></use>
</svg>
@code {
public string GetPathTo(string symbol)
{
var versioned = FileVersionProvider.AddFileVersionToPath(default, "img/icon-sprite.svg");
var rootPath = (BTCPayServerOptions.RootPath ?? "/").WithTrailingSlash();
return $"{rootPath}{versioned}#{Symbol}";
}
[Parameter]
public string Symbol { get; set; }
}

@ -1,152 +0,0 @@
@using System.Security.Claims
@using BTCPayServer.Abstractions.Contracts;
@using BTCPayServer.Configuration;
@using BTCPayServer.Data;
@using BTCPayServer.Services.Notifications;
@using Microsoft.AspNetCore.Identity;
@using Microsoft.AspNetCore.Routing;
@implements IDisposable
@inject AuthenticationStateProvider _AuthenticationStateProvider
@inject NotificationManager _NotificationManager
@inject UserManager<ApplicationUser> _UserManager
@inject IJSRuntime _JSRuntime
@inject LinkGenerator _LinkGenerator
@inject BTCPayServerOptions _BTCPayServerOptions
@inject EventAggregator _EventAggregator
<div id="Notifications">
@if (UnseenCount == "0")
{
<a href="@NotificationsUrl" id="NotificationsHandle" class="mainMenuButton" title="Notifications">
<Icon Symbol="notifications" />
</a>
}
else
{
<button id="NotificationsHandle" class="mainMenuButton" title="Notifications" type="button" data-bs-toggle="dropdown">
<Icon Symbol="notifications" />
<span class="badge rounded-pill bg-danger p-1 ms-1" id="NotificationsBadge">@UnseenCount</span>
</button>
}
@if (UnseenCount != "0" && Last5 is not null)
{
<div class="dropdown-menu text-center" id="NotificationsDropdown" aria-labelledby="NotificationsHandle">
<div class="d-flex gap-3 align-items-center justify-content-between py-3 px-4 border-bottom border-light">
<h5 class="m-0">Notifications</h5>
<a class="btn btn-link p-0" @onclick="MarkAllAsSeen" id="NotificationsMarkAllAsSeen">Mark all as seen</a>
</div>
<div id="NotificationsList" v-pre>
@foreach (var n in Last5)
{
<a href="@NotificationUrl(n.Id)" class="notification d-flex align-items-center dropdown-item border-bottom border-light py-3 px-4">
<div class="me-3">
<Icon Symbol="@NotificationIcon(n.Identifier)" />
</div>
<div class="notification-item__content">
<div class="text-start text-wrap">
@n.Body
</div>
<div class="text-start d-flex">
<small class="text-muted" data-timeago-unixms="@n.Created.ToUnixTimeMilliseconds()">@n.Created.ToTimeAgo()</small>
</div>
</div>
</a>
}
</div>
<div class="p-3">
<a href="@NotificationsUrl">View all</a>
</div>
</div>
}
</div>
@code {
string NotificationsUrl => _LinkGenerator.GetPathByAction("Index", "UINotifications", pathBase: _BTCPayServerOptions.RootPath);
string NotificationUrl(string notificationId) => _LinkGenerator.GetPathByAction("NotificationPassThrough", "UINotifications", values: new { id = notificationId }, pathBase: _BTCPayServerOptions.RootPath);
string UnseenCount;
List<NotificationViewModel> Last5;
IDisposable _EventAggregatorListener;
protected override void OnInitialized()
{
if (_JSRuntime.IsPreRendering())
return;
_EventAggregatorListener = _EventAggregator.Subscribe<UserNotificationsUpdatedEvent>((s, evt) =>
{
_ = InvokeAsync(async () =>
{
if (await GetUserId() is string userId)
{
var res = await _NotificationManager.GetSummaryNotifications(userId, cachedOnly: false);
UpdateState(res);
StateHasChanged();
}
});
});
}
public void Dispose() => _EventAggregatorListener?.Dispose();
string SeenCount(int? count)
{
if (count is not int c)
return "0";
if (c >= NotificationManager.MaxUnseen)
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()
{
if (await GetUserId() is string userId)
{
// For prerendering and first rendering, always use the cached value
var res = await _NotificationManager.GetSummaryNotifications(userId, cachedOnly: true);
// If we forget to update the state here, the UI will flicker.
// Because the first rendering will think there is 0 events, until the DB call ends and the second rendering happens.
// By updating the state here, the first rendering will show the cached value until the second rendering happens
UpdateState(res);
// We don't want to block the pre-rendering, so we will render again when the costly request is over
if (!_JSRuntime.IsPreRendering())
{
res = await _NotificationManager.GetSummaryNotifications(userId, cachedOnly: false);
UpdateState(res);
}
}
}
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()
{
if (await GetUserId() is string userId)
{
await _NotificationManager.ToggleSeen(new NotificationsQuery() { Seen = false, UserId = userId }, true);
UnseenCount = "0";
}
}
private static string NotificationIcon(string type)
{
return type switch
{
"invoice_expired" => "notifications-invoice-failure",
"invoice_expiredpaidpartial" => "notifications-invoice-failure",
"invoice_failedtoconfirm" => "notifications-invoice-failure",
"invoice_confirmed" => "notifications-invoice-settled",
"invoice_paidafterexpiration" => "notifications-invoice-settled",
"external-payout-transaction" => "notifications-payout",
"payout_awaitingapproval" => "notifications-payout",
"payout_awaitingpayment" => "notifications-payout-approved",
"newversion" => "notifications-new-version",
_ => "note"
};
}
}

@ -1,9 +0,0 @@
@using System.Net.Http
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.JSInterop
@using BTCPayServer.Blazor
@using BTCPayServer.Abstractions.Extensions

@ -1,24 +1,27 @@
if (!window.appSales) {
window.appSales = {
dataLoaded (model) {
const id = `AppSales-${model.id}`;
window.appSales =
{
dataLoaded: function (model) {
const id = "AppSales-" + model.id;
const appId = model.id;
const period = model.period;
const baseUrl = model.dataUrl;
const baseUrl = model.url;
const data = model;
const render = (data, period) => {
const series = data.series.map(s => s.salesCount);
const labels = data.series.map((s, i) => period === 'Month' ? (i % 5 === 0 ? s.label : '') : s.label);
const labels = data.series.map((s, i) => period === model.period ? s.label : (i % 5 === 0 ? s.label : ''));
const min = Math.min(...series);
const max = Math.max(...series);
const low = min === max ? 0 : Math.max(min - ((max - min) / 5), 0);
document.querySelectorAll(`#${id} .sales-count`).innerText = data.salesCount;
new Chartist.Bar(`#${id} .ct-chart`, {
labels,
series: [series]
}, {
low
low,
});
};

@ -24,7 +24,7 @@ public class AppTopItems : ViewComponent
public async Task<IViewComponentResult> InvokeAsync(string appId, string appType)
{
var type = _appService.GetAppType(appType);
if (type is not (IHasItemStatsAppType and AppBaseType appBaseType))
if (type is not IHasItemStatsAppType salesAppType || type is not AppBaseType appBaseType)
return new HtmlContentViewComponentResult(new StringHtmlContent(string.Empty));
var vm = new AppTopItemsViewModel

@ -27,8 +27,8 @@
const response = await fetch(url);
if (response.ok) {
document.getElementById(`AppTopItems-${appId}`).outerHTML = await response.text();
const data = document.querySelector(`#AppTopItems-${appId} template`);
if (data) window.appTopItems.dataLoaded(JSON.parse(data.innerHTML));
const data = document.querySelector(`#AppSales-${appId} template`);
if (data) window.appSales.dataLoaded(JSON.parse(data.innerHTML));
}
})();
</script>

@ -1,7 +1,8 @@
if (!window.appTopItems) {
window.appTopItems = {
dataLoaded (model) {
const id = `AppTopItems-${model.id}`;
window.appTopItems =
{
dataLoaded: function (model) {
const id = "AppTopItems-" + model.id;
const series = model.salesCount;
new Chartist.Bar(`#${id} .ct-chart`, { series }, {
distributeSeries: true,

@ -1,67 +0,0 @@
@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>

@ -1,22 +0,0 @@
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);
}
}
}

@ -1,14 +0,0 @@
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; }
}
}

@ -3,7 +3,7 @@
@model BTCPayServer.Components.LabelManager.LabelViewModel
@{
var elementId = "a" + Encoders.Base58.EncodeData(RandomUtils.GetBytes(16));
var fetchUrl = Url.Action("LabelsJson", "UIWallets", new {
var fetchUrl = Url.Action("GetLabels", "UIWallets", new {
walletId = Model.WalletObjectId.WalletId,
excludeTypes = Safe.Json(Model.ExcludeTypes)
});

@ -6,10 +6,7 @@
@using BTCPayServer.Views.Wallets
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Client
@using BTCPayServer.Components.ThemeSwitch
@using BTCPayServer.Components.UIExtensionPoint
@using BTCPayServer.Services
@using BTCPayServer.Views.Apps
@using BTCPayServer.Views.CustodianAccounts
@inject Microsoft.AspNetCore.Http.IHttpContextAccessor HttpContext;
@inject BTCPayServerEnvironment Env
@ -93,7 +90,7 @@
</li>
}
<vc:ui-extension-point location="store-wallets-nav" model="@Model"/>
<vc:ui-extension-point location="store-wallets-nav" model="@Model"/>
@if (PoliciesSettings.Experimental)
{
@foreach (var custodianAccount in Model.CustodianAccounts)
@ -134,12 +131,6 @@
<span>Invoices</span>
</a>
</li>
<li class="nav-item" permission="@Policies.CanViewInvoices">
<a asp-area="" asp-controller="UIReports" asp-action="StoreReports" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActivePage(StoreNavPages.Reporting)" id="SectionNav-Reporting">
<vc:icon symbol="invoice" />
<span>Reporting</span>
</a>
</li>
<li class="nav-item" permission="@Policies.CanModifyStoreSettings">
<a asp-area="" asp-controller="UIPaymentRequest" asp-action="GetPaymentRequests" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActiveCategory(typeof(PaymentRequestsNavPages))" id="StoreNav-PaymentRequests">
<vc:icon symbol="payment-requests"/>
@ -190,14 +181,6 @@
<span>Manage Plugins</span>
</a>
</li>
@if (Model.Store != null && Model.ArchivedAppsCount > 0)
{
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyStoreSettings">
<a asp-area="" asp-controller="UIApps" asp-action="ListApps" asp-route-storeId="@Model.Store.Id" asp-route-archived="true" class="nav-link @ViewData.IsActivePage(AppsNavPages.Index)" id="Nav-ArchivedApps">
@Model.ArchivedAppsCount Archived App@(Model.ArchivedAppsCount == 1 ? "" : "s")
</a>
</li>
}
</ul>
</div>
</div>

@ -68,17 +68,13 @@ namespace BTCPayServer.Components.MainNav
vm.LightningNodes = lightningNodes;
// Apps
var apps = await _appService.GetAllApps(UserId, false, store.Id, true);
vm.Apps = apps
.Where(a => !a.Archived)
.Select(a => new StoreApp
{
Id = a.Id,
AppName = a.AppName,
AppType = a.AppType
}).ToList();
vm.ArchivedAppsCount = apps.Count(a => a.Archived);
var apps = await _appService.GetAllApps(UserId, false, store.Id);
vm.Apps = apps.Select(a => new StoreApp
{
Id = a.Id,
AppName = a.AppName,
AppType = a.AppType
}).ToList();
if (PoliciesSettings.Experimental)
{

@ -1,6 +1,7 @@
using System.Collections.Generic;
using BTCPayServer.Data;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Services.Apps;
namespace BTCPayServer.Components.MainNav
{
@ -12,7 +13,6 @@ namespace BTCPayServer.Components.MainNav
public List<StoreApp> Apps { get; set; }
public CustodianAccountData[] CustodianAccounts { get; set; }
public bool AltcoinsBuild { get; set; }
public int ArchivedAppsCount { get; set; }
}
public class StoreApp

@ -0,0 +1,31 @@
@using BTCPayServer.Views.Notifications
@using BTCPayServer.Abstractions.Extensions
@model BTCPayServer.Components.Notifications.NotificationsViewModel
<div id="Notifications">
@if (Model.UnseenCount > 0)
{
<button id="NotificationsHandle" class="mainMenuButton @ViewData.IsActiveCategory(typeof(NotificationsNavPages))" title="Notifications" type="button" data-bs-toggle="dropdown">
<vc:icon symbol="notifications" />
<span class="badge rounded-pill bg-danger p-1 ms-1" id="NotificationsBadge">@Model.UnseenCount</span>
</button>
<div class="dropdown-menu text-center" id="NotificationsDropdown" aria-labelledby="NotificationsHandle">
<div class="d-flex gap-3 align-items-center justify-content-between py-3 px-4 border-bottom border-light">
<h5 class="m-0">Notifications</h5>
<form id="notificationsForm" asp-controller="UINotifications" asp-action="MarkAllAsSeen" asp-route-returnUrl="@Model.ReturnUrl" method="post">
<button class="btn btn-link p-0" type="submit">Mark all as seen</button>
</form>
</div>
<partial name="Components/Notifications/List" model="Model"/>
<div class="p-3">
<a asp-controller="UINotifications" asp-action="Index">View all</a>
</div>
</div>
}
else
{
<a asp-controller="UINotifications" asp-action="Index" id="NotificationsHandle" class="mainMenuButton @ViewData.IsActiveCategory(typeof(NotificationsNavPages))" title="Notifications">
<vc:icon symbol="notifications" />
</a>
}
</div>

@ -0,0 +1,38 @@
@using BTCPayServer.Abstractions.Extensions
@model BTCPayServer.Components.Notifications.NotificationsViewModel
@functions {
private static string NotificationIcon(string type)
{
return type switch
{
"invoice_expired" => "notifications-invoice-failure",
"invoice_expiredpaidpartial" => "notifications-invoice-failure",
"invoice_failedtoconfirm" => "notifications-invoice-failure",
"invoice_confirmed" => "notifications-invoice-settled",
"invoice_paidafterexpiration" => "notifications-invoice-settled",
"external-payout-transaction" => "notifications-payout",
"payout_awaitingapproval" => "notifications-payout",
"payout_awaitingpayment" => "notifications-payout-approved",
"newversion" => "notifications-new-version",
_ => "note"
};
}
}
<div id="NotificationsList">
@foreach (var n in Model.Last5)
{
<a asp-action="NotificationPassThrough" asp-controller="UINotifications" asp-route-id="@n.Id" class="notification d-flex align-items-center dropdown-item border-bottom border-light py-3 px-4">
<div class="me-3">
<vc:icon symbol="@NotificationIcon(n.Identifier)" />
</div>
<div class="notification-item__content">
<div class="text-start text-wrap">
@n.Body
</div>
<div class="text-start d-flex">
<small class="text-muted" data-timeago-unixms="@n.Created.ToUnixTimeMilliseconds()">@n.Created.ToTimeAgo()</small>
</div>
</div>
</a>
}
</div>

@ -0,0 +1,27 @@
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Notifications;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Components.Notifications
{
public class Notifications : ViewComponent
{
private readonly NotificationManager _notificationManager;
private static readonly string[] _views = { "List", "Dropdown", "Recent" };
public Notifications(NotificationManager notificationManager)
{
_notificationManager = notificationManager;
}
public async Task<IViewComponentResult> InvokeAsync(string appearance, string returnUrl)
{
var vm = await _notificationManager.GetSummaryNotifications(UserClaimsPrincipal);
vm.ReturnUrl = returnUrl;
var viewName = _views.Contains(appearance) ? appearance : _views[0];
return View(viewName, vm);
}
}
}

@ -0,0 +1,12 @@
using System.Collections.Generic;
using BTCPayServer.Abstractions.Contracts;
namespace BTCPayServer.Components.Notifications
{
public class NotificationsViewModel
{
public string ReturnUrl { get; set; }
public int UnseenCount { get; set; }
public List<NotificationViewModel> Last5 { get; set; }
}
}

@ -0,0 +1,19 @@
@model BTCPayServer.Components.Notifications.NotificationsViewModel
<div id="NotificationsRecent">
@if (Model.Last5.Any())
{
<div class="d-flex align-items-center justify-content-between mb-3">
<h4 class="mb-0">Recent Notifications</h4>
<a asp-controller="UINotifications" asp-action="Index">View all</a>
</div>
<partial name="Components/Notifications/List" model="Model"/>
}
else
{
<h4 class="mb-3">Notifications</h4>
<p class="text-secondary mt-3">
There are no recent unseen notifications.
</p>
}
</div>

@ -1,4 +1,3 @@
@using BTCPayServer.Client
@model BTCPayServer.Components.StoreNumbers.StoreNumbersViewModel
<div class="widget store-numbers" id="StoreNumbers-@Model.Store.Id">
@ -22,23 +21,26 @@
}
else
{
<div class="store-number">
<header>
<h6>Paid invoices in the last @Model.TimeframeDays days</h6>
@if (Model.PaidInvoices > 0)
{
<a asp-controller="UIInvoice" asp-action="ListInvoices" asp-route-storeId="@Model.Store.Id" permission="@Policies.CanViewInvoices">View All</a>
}
</header>
<div class="h3">@Model.PaidInvoices</div>
</div>
<div class="store-number">
<header>
<h6>Payouts Pending</h6>
<a asp-controller="UIStorePullPayments" asp-action="Payouts" asp-route-storeId="@Model.Store.Id" permission="@Policies.CanManagePullPayments">Manage</a>
<a asp-controller="UIStorePullPayments" asp-action="Payouts" asp-route-storeId="@Model.Store.Id">Manage</a>
</header>
<div class="h3">@Model.PayoutsPending</div>
</div>
@if (Model.Transactions is not null)
{
<div class="store-number">
<header>
<h6>TXs in the last @Model.TransactionDays days</h6>
@if (Model.Transactions.Value > 0)
{
<a asp-controller="UIWallets" asp-action="WalletTransactions" asp-route-walletId="@Model.WalletId">View All</a>
}
</header>
<div class="h3">@Model.Transactions.Value</div>
</div>
}
<div class="store-number">
<header>
<h6>Refunds Issued</h6>

@ -6,7 +6,6 @@ using BTCPayServer.Client.Models;
using BTCPayServer.Components.StoreRecentTransactions;
using BTCPayServer.Data;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using Dapper;
@ -22,16 +21,22 @@ public class StoreNumbers : ViewComponent
{
private readonly StoreRepository _storeRepo;
private readonly ApplicationDbContextFactory _dbContextFactory;
private readonly InvoiceRepository _invoiceRepository;
private readonly BTCPayWalletProvider _walletProvider;
private readonly NBXplorerConnectionFactory _nbxConnectionFactory;
private readonly BTCPayNetworkProvider _networkProvider;
public StoreNumbers(
StoreRepository storeRepo,
ApplicationDbContextFactory dbContextFactory,
InvoiceRepository invoiceRepository)
BTCPayNetworkProvider networkProvider,
BTCPayWalletProvider walletProvider,
NBXplorerConnectionFactory nbxConnectionFactory)
{
_storeRepo = storeRepo;
_walletProvider = walletProvider;
_nbxConnectionFactory = nbxConnectionFactory;
_networkProvider = networkProvider;
_dbContextFactory = dbContextFactory;
_invoiceRepository = invoiceRepository;
}
public async Task<IViewComponentResult> InvokeAsync(StoreNumbersViewModel vm)
@ -47,17 +52,28 @@ public class StoreNumbers : ViewComponent
return View(vm);
await using var ctx = _dbContextFactory.CreateContext();
var offset = DateTimeOffset.Now.AddDays(-vm.TimeframeDays).ToUniversalTime();
vm.PaidInvoices = await _invoiceRepository.GetInvoiceCount(
new InvoiceQuery { StoreId = new [] { vm.Store.Id }, StartDate = offset, Status = new [] { "paid", "confirmed" } });
vm.PayoutsPending = await ctx.Payouts
var payoutsCount = await ctx.Payouts
.Where(p => p.PullPaymentData.StoreId == vm.Store.Id && !p.PullPaymentData.Archived && p.State == PayoutState.AwaitingApproval)
.CountAsync();
vm.RefundsIssued = await ctx.Invoices
.Where(i => i.StoreData.Id == vm.Store.Id && !i.Archived && i.CurrentRefundId != null && i.Created >= offset)
var refundsCount = await ctx.Invoices
.Where(i => i.StoreData.Id == vm.Store.Id && !i.Archived && i.CurrentRefundId != null)
.CountAsync();
var derivation = vm.Store.GetDerivationSchemeSettings(_networkProvider, vm.CryptoCode);
int? transactionsCount = null;
if (derivation != null && _nbxConnectionFactory.Available)
{
await using var conn = await _nbxConnectionFactory.OpenConnection();
var wid = NBXplorer.Client.DBUtils.nbxv1_get_wallet_id(derivation.Network.CryptoCode, derivation.AccountDerivation.ToString());
var afterDate = DateTimeOffset.UtcNow - TimeSpan.FromDays(vm.TransactionDays);
var count = await conn.ExecuteScalarAsync<long>("SELECT COUNT(*) FROM wallets_history WHERE code=@code AND wallet_id=@wid AND seen_at > @afterDate", new { code = derivation.Network.CryptoCode, wid, afterDate });
transactionsCount = (int)count;
}
vm.PayoutsPending = payoutsCount;
vm.Transactions = transactionsCount;
vm.RefundsIssued = refundsCount;
return View(vm);
}
}

@ -7,9 +7,9 @@ public class StoreNumbersViewModel
public StoreData Store { get; set; }
public WalletId WalletId { get; set; }
public int PayoutsPending { get; set; }
public int TimeframeDays { get; set; } = 7;
public int? PaidInvoices { get; set; }
public int? Transactions { get; set; }
public int RefundsIssued { get; set; }
public int TransactionDays { get; set; } = 7;
public bool InitialRendering { get; set; }
public string CryptoCode { get; set; }
}

@ -3,7 +3,6 @@
@using BTCPayServer.Services
@using BTCPayServer.Services.Invoices
@inject DisplayFormatter DisplayFormatter
@inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary
@model BTCPayServer.Components.StoreRecentInvoices.StoreRecentInvoicesViewModel
<div class="widget store-recent-invoices" id="StoreRecentInvoices-@Model.Store.Id">
@ -52,8 +51,27 @@
<a asp-controller="UIInvoice" asp-action="Invoice" asp-route-invoiceId="@invoice.InvoiceId" class="text-break">@invoice.InvoiceId</a>
</td>
<td>
<vc:invoice-status state="invoice.Status" payments="invoice.Details.Payments" invoice-id="@invoice.InvoiceId"
is-archived="invoice.Details.Archived" has-refund="invoice.HasRefund" />
@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 paymentType in invoice.Details.Payments.Select(payment => payment.GetPaymentMethodId()?.PaymentType).Distinct().Where(type => type != null && !string.IsNullOrEmpty(type.GetBadge())))
{
<span class="badge">@paymentType.GetBadge()</span>
}
@if (invoice.HasRefund)
{
<span class="badge bg-warning">
Refund
</span>
}
</td>
<td class="text-end">
<span data-sensitive>@DisplayFormatter.Currency(invoice.Amount, invoice.Currency)</span>

@ -12,6 +12,7 @@ public class StoreRecentInvoiceViewModel
public string Currency { get; set; }
public InvoiceState Status { get; set; }
public DateTimeOffset Date { get; set; }
public InvoiceDetailsModel Details { get; set; }
public bool HasRefund { get; set; }
}

@ -1,9 +1,8 @@
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Abstractions.Contracts
@using BTCPayServer.Client
@using BTCPayServer.Components.MainLogo
@using BTCPayServer.Services
@using BTCPayServer.Views.Stores
@inject BTCPayServerEnvironment Env
@inject IFileService FileService
@model BTCPayServer.Components.StoreSelector.StoreSelectorViewModel
@ -35,7 +34,7 @@ else
<a asp-controller="UIStores" asp-action="Dashboard" permission="@Policies.CanModifyStoreSettings" asp-route-storeId="@Model.CurrentStoreId" id="StoreSelectorHome" class="navbar-brand py-2">@{await LogoContent();}</a>
<a asp-controller="UIInvoice" asp-action="ListInvoices" not-permission="@Policies.CanModifyStoreSettings" asp-route-storeId="@Model.CurrentStoreId" id="StoreSelectorHome" class="navbar-brand py-2">@{await LogoContent();}</a>
}
@if (Model.Options.Any() || Model.ArchivedCount > 0)
@if (Model.Options.Any())
{
<div id="StoreSelector">
<div id="StoreSelectorDropdown" class="dropdown only-for-js">
@ -65,16 +64,8 @@ else
}
</li>
}
@if (Model.Options.Any())
{
<li><hr class="dropdown-divider"></li>
}
<li><a asp-controller="UIUserStores" asp-action="CreateStore" class="dropdown-item @ViewData.IsActivePage(StoreNavPages.Create)" id="StoreSelectorCreate">Create Store</a></li>
@if (Model.ArchivedCount > 0)
{
<li><hr class="dropdown-divider"></li>
<li><a asp-controller="UIUserStores" asp-action="ListStores" asp-route-archived="true" class="dropdown-item @ViewData.IsActivePage(StoreNavPages.Index)" id="StoreSelectorArchived">@Model.ArchivedCount Archived Store@(Model.ArchivedCount == 1 ? "" : "s")</a></li>
}
<li><hr class="dropdown-divider"></li>
<li><a asp-controller="UIUserStores" asp-action="CreateStore" class="dropdown-item" id="StoreSelectorCreate">Create Store</a></li>
</ul>
</div>
</div>

@ -30,9 +30,7 @@ namespace BTCPayServer.Components.StoreSelector
var userId = _userManager.GetUserId(UserClaimsPrincipal);
var stores = await _storeRepo.GetStoresByUserId(userId);
var currentStore = ViewContext.HttpContext.GetStoreData();
var archivedCount = stores.Count(s => s.Archived);
var options = stores
.Where(store => !store.Archived)
.Select(store =>
{
var cryptoCode = store
@ -61,8 +59,7 @@ namespace BTCPayServer.Components.StoreSelector
Options = options,
CurrentStoreId = currentStore?.Id,
CurrentDisplayName = currentStore?.StoreName,
CurrentStoreLogoFileId = blob?.LogoFileId,
ArchivedCount = archivedCount
CurrentStoreLogoFileId = blob?.LogoFileId
};
return View(vm);

@ -8,7 +8,6 @@ namespace BTCPayServer.Components.StoreSelector
public string CurrentStoreId { get; set; }
public string CurrentStoreLogoFileId { get; set; }
public string CurrentDisplayName { get; set; }
public int ArchivedCount { get; set; }
}
public class StoreSelectorOption

@ -1,7 +1,6 @@
@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>
@ -39,12 +38,6 @@
{
<div class="ct-chart"></div>
}
else if (!Model.Store.GetSupportedPaymentMethods(NetworkProvider).Any(method => method.PaymentId.PaymentType == BitcoinPaymentType.Instance && method.PaymentId.CryptoCode == Model.CryptoCode))
{
<p>
We would like to show you a chart of your balance but you have not yet <a href="@Url.Action("SetupWallet", "UIStores", new {storeId = Model.Store.Id, cryptoCode = Model.CryptoCode})">configured a wallet</a>.
</p>
}
else
{
<p>

@ -75,7 +75,7 @@ public class StoreWalletBalance : ViewComponent
if (derivation is not null)
{
var balance = await wallet.GetBalance(derivation.AccountDerivation, cts.Token);
vm.Balance = balance.Available.GetValue(derivation.Network);
vm.Balance = balance.Available.GetValue();
}
}

@ -1,11 +1,10 @@
@model BTCPayServer.Components.TruncateCenter.TruncateCenterViewModel
@{
var classes = string.IsNullOrEmpty(Model.Classes) ? string.Empty : Model.Classes.Trim();
var isTruncated = !string.IsNullOrEmpty(Model.Start) && !string.IsNullOrEmpty(Model.End);
@if (Model.Copy) classes += " truncate-center--copy";
@if (Model.Elastic) classes += " truncate-center--elastic";
}
<span class="truncate-center @classes"@(!string.IsNullOrEmpty(Model.Id) ? $"id={Model.Id}" : null) data-text=@Safe.Json(Model.Text)>
<span class="truncate-center @classes">
@if (Model.IsVue)
{
<span class="truncate-center-truncated" data-bs-toggle="tooltip" :title=@Safe.Json(Model.Text)>
@ -16,12 +15,9 @@
}
else
{
<span class="truncate-center-truncated" @(isTruncated ? $"data-bs-toggle=tooltip title={Model.Text}" : "")>
<span class="truncate-center-start">@(Model.Elastic || !isTruncated ? Model.Text : $"{Model.Start}…")</span>
@if (isTruncated)
{
<span class="truncate-center-end">@Model.End</span>
}
<span class="truncate-center-truncated" @(!string.IsNullOrEmpty(Model.Start) ? $"data-bs-toggle=tooltip title={Model.Text}" : "")>
<span class="truncate-center-start">@(Model.Elastic ? Model.Text : $"{Model.Start}…")</span>
<span class="truncate-center-end">@Model.End</span>
</span>
<span class="truncate-center-text">@Model.Text</span>
}

@ -15,7 +15,7 @@ namespace BTCPayServer.Components.TruncateCenter;
/// <returns>HTML with truncated string</returns>
public class TruncateCenter : ViewComponent
{
public IViewComponentResult Invoke(string text, string link = null, string classes = null, int padding = 7, bool copy = true, bool elastic = false, bool isVue = false, string id = null)
public IViewComponentResult Invoke(string text, string link = null, string classes = null, int padding = 7, bool copy = true, bool elastic = false, bool isVue = false)
{
if (string.IsNullOrEmpty(text))
return new HtmlContentViewComponentResult(new StringHtmlContent(string.Empty));
@ -27,8 +27,7 @@ public class TruncateCenter : ViewComponent
IsVue = isVue,
Copy = copy,
Text = text,
Link = link,
Id = id
Link = link
};
if (!isVue && text.Length > 2 * padding)
{

@ -5,7 +5,6 @@ namespace BTCPayServer.Components.TruncateCenter
public string Text { get; set; }
public string Start { get; set; }
public string End { get; set; }
public string Id { get; set; }
public string Classes { get; set; }
public string Link { get; set; }
public int Padding { get; set; }

@ -1,20 +1,15 @@
@using BTCPayServer.Services;
@using BTCPayServer.Views.Stores
@using BTCPayServer.Client
@using BTCPayServer.Views.Wallets
@using BTCPayServer.Abstractions.Extensions
@inject DisplayFormatter DisplayFormatter
@model BTCPayServer.Components.WalletNav.WalletNavViewModel
<div class="d-sm-flex align-items-center justify-content-between">
<a asp-controller="UIWallets" asp-action="WalletTransactions" asp-route-walletId="@Model.WalletId" class="unobtrusive-link">
<h2 class="mb-1">@Model.Label</h2>
<div class="text-muted fw-semibold" data-sensitive>
@DisplayFormatter.Currency(Model.Balance, Model.Network.CryptoCode)
@if (!string.IsNullOrEmpty(Model.BalanceDefaultCurrency))
{
<span>(@DisplayFormatter.Currency(Model.BalanceDefaultCurrency, Model.DefaultCurrency))</span>
}
@Model.Balance @Model.Network.CryptoCode
</div>
</a>
<div class="d-flex gap-3 mt-3 mt-sm-0" permission="@Policies.CanModifyStoreSettings">

@ -1,18 +1,14 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using Microsoft.AspNetCore.Identity;
@ -27,22 +23,16 @@ namespace BTCPayServer.Components.WalletNav
{
private readonly BTCPayWalletProvider _walletProvider;
private readonly UIWalletsController _walletsController;
private readonly CurrencyNameTable _currencies;
private readonly BTCPayNetworkProvider _networkProvider;
private readonly RateFetcher _rateFetcher;
public WalletNav(
BTCPayWalletProvider walletProvider,
BTCPayNetworkProvider networkProvider,
UIWalletsController walletsController,
CurrencyNameTable currencies,
RateFetcher rateFetcher)
UIWalletsController walletsController)
{
_walletProvider = walletProvider;
_networkProvider = networkProvider;
_walletsController = walletsController;
_currencies = currencies;
_rateFetcher = rateFetcher;
}
public async Task<IViewComponentResult> InvokeAsync(WalletId walletId)
@ -50,34 +40,17 @@ namespace BTCPayServer.Components.WalletNav
var store = ViewContext.HttpContext.GetStoreData();
var network = _networkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
var wallet = _walletProvider.GetWallet(network);
var defaultCurrency = store.GetStoreBlob().DefaultCurrency;
var derivation = store.GetDerivationSchemeSettings(_networkProvider, walletId.CryptoCode);
var balance = await wallet.GetBalance(derivation?.AccountDerivation) switch
{
{ Available: null, Total: var total } => total,
{ Available: var available } => available
};
var balance = await _walletsController.GetBalanceString(wallet, derivation?.AccountDerivation);
var vm = new WalletNavViewModel
{
WalletId = walletId,
Network = network,
Balance = balance.ShowMoney(network),
DefaultCurrency = defaultCurrency,
Balance = balance,
Label = derivation?.Label ?? $"{store.StoreName} {walletId.CryptoCode} Wallet"
};
if (defaultCurrency != network.CryptoCode)
{
var rule = store.GetStoreBlob().GetRateRules(_networkProvider)?.GetRuleFor(new Rating.CurrencyPair(network.CryptoCode, defaultCurrency));
var bid = rule is null ? null : (await _rateFetcher.FetchRate(rule, HttpContext.RequestAborted)).BidAsk?.Bid;
if (bid is decimal b)
{
var currencyData = _currencies.GetCurrencyData(defaultCurrency, true);
vm.BalanceDefaultCurrency = (balance.GetValue(network) * b).ShowMoney(currencyData.Divisibility);
}
}
return View(vm);
}
}

@ -6,7 +6,5 @@ namespace BTCPayServer.Components.WalletNav
public BTCPayNetwork Network { get; set; }
public string Label { get; set; }
public string Balance { get; set; }
public string BalanceDefaultCurrency { get; set; }
public string DefaultCurrency { get; set; }
}
}

@ -1,23 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.ModelBinders;
using BTCPayServer.Models;
using BTCPayServer.Payments;
using BTCPayServer.Security.Greenfield;
using BTCPayServer.Security;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NBitpayClient;
using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Controllers
{
@ -42,7 +36,7 @@ namespace BTCPayServer.Controllers
{
if (invoice == null)
throw new BitpayHttpException(400, "Invalid invoice");
return await CreateInvoiceCore(invoice, HttpContext.GetStoreData(), HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
return await _InvoiceController.CreateInvoiceCore(invoice, HttpContext.GetStoreData(), HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
}
[HttpGet]
@ -72,7 +66,7 @@ namespace BTCPayServer.Controllers
int? limit = null,
int? offset = null)
{
if (User.Identity?.AuthenticationType == Security.Bitpay.BitpayAuthenticationTypes.Anonymous)
if (User.Identity.AuthenticationType == Security.Bitpay.BitpayAuthenticationTypes.Anonymous)
return Forbid(Security.Bitpay.BitpayAuthenticationTypes.Anonymous);
if (dateEnd != null)
dateEnd = dateEnd.Value + TimeSpan.FromDays(1); //Should include the end day
@ -94,133 +88,5 @@ namespace BTCPayServer.Controllers
return Json(DataWrapper.Create(entities));
}
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(BitpayCreateInvoiceRequest invoice,
StoreData store, string serverUrl, List<string> additionalTags = null,
CancellationToken cancellationToken = default, Action<InvoiceEntity> entityManipulator = null)
{
var entity = await CreateInvoiceCoreRaw(invoice, store, serverUrl, additionalTags, cancellationToken, entityManipulator);
var resp = entity.EntityToDTO();
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
}
internal async Task<InvoiceEntity> CreateInvoiceCoreRaw(BitpayCreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string> additionalTags = null, CancellationToken cancellationToken = default, Action<InvoiceEntity> entityManipulator = null)
{
var storeBlob = store.GetStoreBlob();
var entity = _InvoiceRepository.CreateNewInvoice(store.Id);
entity.ExpirationTime = invoice.ExpirationTime is { } v ? v : entity.InvoiceTime + storeBlob.InvoiceExpiration;
entity.MonitoringExpiration = entity.ExpirationTime + storeBlob.MonitoringExpiration;
if (entity.ExpirationTime - TimeSpan.FromSeconds(30.0) < entity.InvoiceTime)
{
throw new BitpayHttpException(400, "The expirationTime is set too soon");
}
if (entity.Price < 0.0m)
{
throw new BitpayHttpException(400, "The price should be 0 or more.");
}
if (entity.Price > GreenfieldConstants.MaxAmount)
{
throw new BitpayHttpException(400, $"The price should less than {GreenfieldConstants.MaxAmount}.");
}
entity.Metadata.OrderId = invoice.OrderId;
entity.Metadata.PosDataLegacy = invoice.PosData;
entity.ServerUrl = serverUrl;
entity.FullNotifications = invoice.FullNotifications || invoice.ExtendedNotifications;
entity.ExtendedNotifications = invoice.ExtendedNotifications;
entity.NotificationURLTemplate = invoice.NotificationURL;
entity.NotificationEmail = invoice.NotificationEmail;
if (additionalTags != null)
entity.InternalTags.AddRange(additionalTags);
FillBuyerInfo(invoice, entity);
var price = invoice.Price;
entity.Metadata.ItemCode = invoice.ItemCode;
entity.Metadata.ItemDesc = invoice.ItemDesc;
entity.Metadata.Physical = invoice.Physical;
entity.Metadata.TaxIncluded = invoice.TaxIncluded;
entity.Currency = invoice.Currency;
if (price is { } vv)
{
entity.Price = vv;
entity.Type = InvoiceType.Standard;
}
else
{
entity.Price = 0m;
entity.Type = InvoiceType.TopUp;
}
entity.StoreSupportUrl = storeBlob.StoreSupportUrl;
entity.RedirectURLTemplate = invoice.RedirectURL ?? store.StoreWebsite;
entity.RedirectAutomatically =
invoice.RedirectAutomatically.GetValueOrDefault(storeBlob.RedirectAutomatically);
entity.RequiresRefundEmail = invoice.RequiresRefundEmail;
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
IPaymentFilter excludeFilter = null;
if (invoice.PaymentCurrencies?.Any() is true)
{
invoice.SupportedTransactionCurrencies ??=
new Dictionary<string, InvoiceSupportedTransactionCurrency>();
foreach (string paymentCurrency in invoice.PaymentCurrencies)
{
invoice.SupportedTransactionCurrencies.TryAdd(paymentCurrency,
new InvoiceSupportedTransactionCurrency() { Enabled = true });
}
}
if (invoice.SupportedTransactionCurrencies != null && invoice.SupportedTransactionCurrencies.Count != 0)
{
var supportedTransactionCurrencies = invoice.SupportedTransactionCurrencies
.Where(c => c.Value.Enabled)
.Select(c => PaymentMethodId.TryParse(c.Key, out var p) ? p : null)
.Where(c => c != null)
.ToHashSet();
excludeFilter = PaymentFilter.Where(p => !supportedTransactionCurrencies.Contains(p));
}
entity.PaymentTolerance = storeBlob.PaymentTolerance;
entity.DefaultPaymentMethod = invoice.DefaultPaymentMethod;
entity.RequiresRefundEmail = invoice.RequiresRefundEmail;
return await _InvoiceController.CreateInvoiceCoreRaw(entity, store, excludeFilter, null, cancellationToken, entityManipulator);
}
private void FillBuyerInfo(BitpayCreateInvoiceRequest req, InvoiceEntity invoiceEntity)
{
var buyerInformation = invoiceEntity.Metadata;
buyerInformation.BuyerAddress1 = req.BuyerAddress1;
buyerInformation.BuyerAddress2 = req.BuyerAddress2;
buyerInformation.BuyerCity = req.BuyerCity;
buyerInformation.BuyerCountry = req.BuyerCountry;
buyerInformation.BuyerEmail = req.BuyerEmail;
buyerInformation.BuyerName = req.BuyerName;
buyerInformation.BuyerPhone = req.BuyerPhone;
buyerInformation.BuyerState = req.BuyerState;
buyerInformation.BuyerZip = req.BuyerZip;
var buyer = req.Buyer;
if (buyer == null)
return;
buyerInformation.BuyerAddress1 ??= buyer.Address1;
buyerInformation.BuyerAddress2 ??= buyer.Address2;
buyerInformation.BuyerCity ??= buyer.City;
buyerInformation.BuyerCountry ??= buyer.country;
buyerInformation.BuyerEmail ??= buyer.email;
buyerInformation.BuyerName ??= buyer.Name;
buyerInformation.BuyerPhone ??= buyer.phone;
buyerInformation.BuyerState ??= buyer.State;
buyerInformation.BuyerZip ??= buyer.zip;
}
private SpeedPolicy ParseSpeedPolicy(string transactionSpeed, SpeedPolicy defaultPolicy)
{
if (transactionSpeed == null)
return defaultPolicy;
var mappings = new Dictionary<string, SpeedPolicy>();
mappings.Add("low", SpeedPolicy.LowSpeed);
mappings.Add("low-medium", SpeedPolicy.LowMediumSpeed);
mappings.Add("medium", SpeedPolicy.MediumSpeed);
mappings.Add("high", SpeedPolicy.HighSpeed);
if (!mappings.TryGetValue(transactionSpeed, out SpeedPolicy policy))
policy = defaultPolicy;
return policy;
}
}
}

@ -66,8 +66,7 @@ namespace BTCPayServer.Controllers.Greenfield
{
StoreDataId = storeId,
Name = request.AppName,
AppType = CrowdfundAppType.AppType,
Archived = request.Archived ?? false
AppType = CrowdfundAppType.AppType
};
appData.SetSettings(ToCrowdfundSettings(request));
@ -98,8 +97,7 @@ namespace BTCPayServer.Controllers.Greenfield
{
StoreDataId = storeId,
Name = request.AppName,
AppType = PointOfSaleAppType.AppType,
Archived = request.Archived ?? false
AppType = PointOfSaleAppType.AppType
};
appData.SetSettings(ToPointOfSaleSettings(request));
@ -113,7 +111,7 @@ namespace BTCPayServer.Controllers.Greenfield
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> UpdatePointOfSaleApp(string appId, CreatePointOfSaleAppRequest request)
{
var app = await _appService.GetApp(appId, PointOfSaleAppType.AppType, includeArchived: true);
var app = await _appService.GetApp(appId, PointOfSaleAppType.AppType);
if (app == null)
{
return AppNotFound();
@ -131,10 +129,6 @@ namespace BTCPayServer.Controllers.Greenfield
}
app.Name = request.AppName;
if (request.Archived != null)
{
app.Archived = request.Archived.Value;
}
app.SetSettings(ToPointOfSaleSettings(request));
await _appService.UpdateOrCreateApp(app);
@ -159,7 +153,7 @@ namespace BTCPayServer.Controllers.Greenfield
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> GetAllApps()
{
var apps = await _appService.GetAllApps(_userManager.GetUserId(User), includeArchived: true);
var apps = await _appService.GetAllApps(_userManager.GetUserId(User));
return Ok(apps.Select(ToModel).ToArray());
}
@ -168,7 +162,7 @@ namespace BTCPayServer.Controllers.Greenfield
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> GetAllApps(string storeId)
{
var apps = await _appService.GetAllApps(_userManager.GetUserId(User), false, storeId, true);
var apps = await _appService.GetAllApps(_userManager.GetUserId(User), allowNoUser: false, storeId);
return Ok(apps.Select(ToModel).ToArray());
}
@ -177,7 +171,7 @@ namespace BTCPayServer.Controllers.Greenfield
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> GetApp(string appId)
{
var app = await _appService.GetApp(appId, null, includeArchived: true);
var app = await _appService.GetApp(appId, null);
if (app == null)
{
return AppNotFound();
@ -190,7 +184,7 @@ namespace BTCPayServer.Controllers.Greenfield
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> GetPosApp(string appId)
{
var app = await _appService.GetApp(appId, PointOfSaleAppType.AppType, includeArchived: true);
var app = await _appService.GetApp(appId, PointOfSaleAppType.AppType);
if (app == null)
{
return AppNotFound();
@ -203,7 +197,7 @@ namespace BTCPayServer.Controllers.Greenfield
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> GetCrowdfundApp(string appId)
{
var app = await _appService.GetApp(appId, CrowdfundAppType.AppType, includeArchived: true);
var app = await _appService.GetApp(appId, CrowdfundAppType.AppType);
if (app == null)
{
return AppNotFound();
@ -215,7 +209,7 @@ namespace BTCPayServer.Controllers.Greenfield
[HttpDelete("~/api/v1/apps/{appId}")]
public async Task<IActionResult> DeleteApp(string appId)
{
var app = await _appService.GetApp(appId, null, includeArchived: true);
var app = await _appService.GetApp(appId, null);
if (app == null)
{
return AppNotFound();
@ -299,7 +293,6 @@ namespace BTCPayServer.Controllers.Greenfield
return new AppDataBase
{
Id = appData.Id,
Archived = appData.Archived,
AppType = appData.AppType,
Name = appData.Name,
StoreId = appData.StoreDataId,
@ -312,7 +305,6 @@ namespace BTCPayServer.Controllers.Greenfield
return new AppDataBase
{
Id = appData.Id,
Archived = appData.Archived,
AppType = appData.AppType,
Name = appData.AppName,
StoreId = appData.StoreId,
@ -327,7 +319,6 @@ namespace BTCPayServer.Controllers.Greenfield
return new PointOfSaleAppData
{
Id = appData.Id,
Archived = appData.Archived,
AppType = appData.AppType,
Name = appData.Name,
StoreId = appData.StoreDataId,
@ -396,7 +387,6 @@ namespace BTCPayServer.Controllers.Greenfield
return new CrowdfundAppData
{
Id = appData.Id,
Archived = appData.Archived,
AppType = appData.AppType,
Name = appData.Name,
StoreId = appData.StoreDataId,

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