Compare commits

..

1 Commits

Author SHA1 Message Date
b9896037ba fix sln csproj ref
fixes #2262
2021-02-15 07:38:32 +01:00
662 changed files with 50877 additions and 54039 deletions

View File

@ -36,7 +36,4 @@
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.4" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.1.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BTCPayServer.Client\BTCPayServer.Client.csproj" />
</ItemGroup>
</Project>

View File

@ -1,10 +0,0 @@
using System.Threading.Tasks;
using BTCPayServer.Client;
namespace BTCPayServer.Abstractions.Contracts
{
public interface IBTCPayServerClientFactory
{
Task<BTCPayServerClient> Create(string userId, params string[] storeIds);
}
}

View File

@ -1,4 +1,3 @@
#nullable enable
using System.Threading;
using System.Threading.Tasks;
@ -6,8 +5,8 @@ namespace BTCPayServer.Abstractions.Contracts
{
public interface ISettingsRepository
{
Task<T?> GetSettingAsync<T>(string? name = null) where T : class;
Task UpdateSetting<T>(T obj, string? name = null) where T : class;
Task<T> WaitSettingsChanged<T>(CancellationToken cancellationToken = default) where T : class;
Task<T> GetSettingAsync<T>(string name = null);
Task UpdateSetting<T>(T obj, string name = null);
Task<T> WaitSettingsChanged<T>(CancellationToken cancellationToken = default);
}
}

View File

@ -1,5 +1,3 @@
using System.Collections.Generic;
namespace BTCPayServer.Abstractions.Contracts
{
public interface ISyncSummaryProvider
@ -7,12 +5,5 @@ namespace BTCPayServer.Abstractions.Contracts
bool AllAvailable();
string Partial { get; }
IEnumerable<ISyncStatus> GetStatuses();
}
public interface ISyncStatus
{
public string CryptoCode { get; set; }
public bool Available { get; }
}
}

View File

@ -13,7 +13,7 @@
<RepositoryType>git</RepositoryType>
</PropertyGroup>
<PropertyGroup>
<Version Condition=" '$(Version)' == '' ">1.4.0</Version>
<Version Condition=" '$(Version)' == '' ">1.2.0</Version>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<PublishRepositoryUrl>true</PublishRepositoryUrl>
@ -27,8 +27,8 @@
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="NBitcoin" Version="6.0.7" />
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.2.4" />
<PackageReference Include="NBitcoin" Version="5.0.73" />
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
</ItemGroup>
<ItemGroup>

View File

@ -1,44 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using NBitcoin;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<IEnumerable<InvoiceData>> GetInvoices(string storeId, string[] orderId = null,
InvoiceStatus[] status = null,
DateTimeOffset? startDate = null,
DateTimeOffset? endDate = null,
string textSearch = null,
bool includeArchived = false,
public virtual async Task<IEnumerable<InvoiceData>> GetInvoices(string storeId, bool includeArchived = false,
CancellationToken token = default)
{
Dictionary<string, object> queryPayload = new Dictionary<string, object>();
queryPayload.Add(nameof(includeArchived), includeArchived);
if (startDate is DateTimeOffset s)
queryPayload.Add(nameof(startDate), Utils.DateTimeToUnixTime(s));
if (endDate is DateTimeOffset e)
queryPayload.Add(nameof(endDate), Utils.DateTimeToUnixTime(e));
if (orderId != null)
queryPayload.Add(nameof(orderId), orderId);
if (textSearch != null)
queryPayload.Add(nameof(textSearch), textSearch);
if (status != null)
queryPayload.Add(nameof(status), status.Select(s=> s.ToString().ToLower()).ToArray());
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/invoices",
queryPayload), token);
new Dictionary<string, object>() {{nameof(includeArchived), includeArchived}}), token);
return await HandleResponse<IEnumerable<InvoiceData>>(response);
}
@ -93,7 +70,7 @@ namespace BTCPayServer.Client
{
if (request == null)
throw new ArgumentNullException(nameof(request));
if (request.Status != InvoiceStatus.Settled && request.Status != InvoiceStatus.Invalid)
if (request.Status!= InvoiceStatus.Settled && request.Status!= InvoiceStatus.Invalid)
throw new ArgumentOutOfRangeException(nameof(request.Status), "Status can only be Invalid or Complete");
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/invoices/{invoiceId}/status", bodyPayload: request,
@ -108,13 +85,5 @@ namespace BTCPayServer.Client
method: HttpMethod.Post), token);
return await HandleResponse<InvoiceData>(response);
}
public virtual async Task ActivateInvoicePaymentMethod(string storeId, string invoiceId, string paymentMethod, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods/{paymentMethod}/activate",
method: HttpMethod.Post), token);
await HandleResponse(response);
}
}
}

View File

@ -9,7 +9,7 @@ namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<LightningNodeInformationData> GetLightningNodeInfo(string cryptoCode,
public async Task<LightningNodeInformationData> GetLightningNodeInfo(string cryptoCode,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
@ -18,7 +18,7 @@ namespace BTCPayServer.Client
return await HandleResponse<LightningNodeInformationData>(response);
}
public virtual async Task ConnectToLightningNode(string cryptoCode, ConnectToNodeRequest request,
public async Task ConnectToLightningNode(string cryptoCode, ConnectToNodeRequest request,
CancellationToken token = default)
{
if (request == null)
@ -29,7 +29,7 @@ namespace BTCPayServer.Client
await HandleResponse(response);
}
public virtual async Task<IEnumerable<LightningChannelData>> GetLightningNodeChannels(string cryptoCode,
public async Task<IEnumerable<LightningChannelData>> GetLightningNodeChannels(string cryptoCode,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
@ -38,16 +38,16 @@ namespace BTCPayServer.Client
return await HandleResponse<IEnumerable<LightningChannelData>>(response);
}
public virtual async Task OpenLightningChannel(string cryptoCode, OpenLightningChannelRequest request,
public async Task<string> OpenLightningChannel(string cryptoCode, OpenLightningChannelRequest request,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/channels", bodyPayload: request,
method: HttpMethod.Post), token);
await HandleResponse(response);
return await HandleResponse<string>(response);
}
public virtual async Task<string> GetLightningDepositAddress(string cryptoCode, CancellationToken token = default)
public async Task<string> GetLightningDepositAddress(string cryptoCode, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/address", method: HttpMethod.Post), token);
@ -55,7 +55,7 @@ namespace BTCPayServer.Client
}
public virtual async Task PayLightningInvoice(string cryptoCode, PayLightningInvoiceRequest request,
public async Task PayLightningInvoice(string cryptoCode, PayLightningInvoiceRequest request,
CancellationToken token = default)
{
if (request == null)
@ -66,7 +66,7 @@ namespace BTCPayServer.Client
await HandleResponse(response);
}
public virtual async Task<LightningInvoiceData> GetLightningInvoice(string cryptoCode,
public async Task<LightningInvoiceData> GetLightningInvoice(string cryptoCode,
string invoiceId, CancellationToken token = default)
{
if (invoiceId == null)
@ -77,7 +77,7 @@ namespace BTCPayServer.Client
return await HandleResponse<LightningInvoiceData>(response);
}
public virtual async Task<LightningInvoiceData> CreateLightningInvoice(string cryptoCode, CreateLightningInvoiceRequest request,
public async Task<LightningInvoiceData> CreateLightningInvoice(string cryptoCode, CreateLightningInvoiceRequest request,
CancellationToken token = default)
{
if (request == null)

View File

@ -9,7 +9,7 @@ namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<LightningNodeInformationData> GetLightningNodeInfo(string storeId, string cryptoCode,
public async Task<LightningNodeInformationData> GetLightningNodeInfo(string storeId, string cryptoCode,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
@ -18,7 +18,7 @@ namespace BTCPayServer.Client
return await HandleResponse<LightningNodeInformationData>(response);
}
public virtual async Task ConnectToLightningNode(string storeId, string cryptoCode, ConnectToNodeRequest request,
public async Task ConnectToLightningNode(string storeId, string cryptoCode, ConnectToNodeRequest request,
CancellationToken token = default)
{
if (request == null)
@ -29,7 +29,7 @@ namespace BTCPayServer.Client
await HandleResponse(response);
}
public virtual async Task<IEnumerable<LightningChannelData>> GetLightningNodeChannels(string storeId, string cryptoCode,
public async Task<IEnumerable<LightningChannelData>> GetLightningNodeChannels(string storeId, string cryptoCode,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
@ -38,16 +38,16 @@ namespace BTCPayServer.Client
return await HandleResponse<IEnumerable<LightningChannelData>>(response);
}
public virtual async Task OpenLightningChannel(string storeId, string cryptoCode, OpenLightningChannelRequest request,
public async Task<string> OpenLightningChannel(string storeId, string cryptoCode, OpenLightningChannelRequest request,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/channels", bodyPayload: request,
method: HttpMethod.Post), token);
await HandleResponse(response);
return await HandleResponse<string>(response);
}
public virtual async Task<string> GetLightningDepositAddress(string storeId, string cryptoCode,
public async Task<string> GetLightningDepositAddress(string storeId, string cryptoCode,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
@ -56,7 +56,7 @@ namespace BTCPayServer.Client
return await HandleResponse<string>(response);
}
public virtual async Task PayLightningInvoice(string storeId, string cryptoCode, PayLightningInvoiceRequest request,
public async Task PayLightningInvoice(string storeId, string cryptoCode, PayLightningInvoiceRequest request,
CancellationToken token = default)
{
if (request == null)
@ -67,7 +67,7 @@ namespace BTCPayServer.Client
await HandleResponse(response);
}
public virtual async Task<LightningInvoiceData> GetLightningInvoice(string storeId, string cryptoCode,
public async Task<LightningInvoiceData> GetLightningInvoice(string storeId, string cryptoCode,
string invoiceId, CancellationToken token = default)
{
if (invoiceId == null)
@ -78,7 +78,7 @@ namespace BTCPayServer.Client
return await HandleResponse<LightningInvoiceData>(response);
}
public virtual async Task<LightningInvoiceData> CreateLightningInvoice(string storeId, string cryptoCode,
public async Task<LightningInvoiceData> CreateLightningInvoice(string storeId, string cryptoCode,
CreateLightningInvoiceRequest request, CancellationToken token = default)
{
if (request == null)

View File

@ -1,64 +0,0 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<IEnumerable<LightningNetworkPaymentMethodData>>
GetStoreLightningNetworkPaymentMethods(string storeId, bool? enabled = null,
CancellationToken token = default)
{
var query = new Dictionary<string, object>();
if (enabled != null)
{
query.Add(nameof(enabled), enabled);
}
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/LightningNetwork",
query), token);
return await HandleResponse<IEnumerable<LightningNetworkPaymentMethodData>>(response);
}
public virtual async Task<LightningNetworkPaymentMethodData> GetStoreLightningNetworkPaymentMethod(
string storeId,
string cryptoCode, CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/LightningNetwork/{cryptoCode}"), token);
return await HandleResponse<LightningNetworkPaymentMethodData>(response);
}
public virtual async Task RemoveStoreLightningNetworkPaymentMethod(string storeId,
string cryptoCode, CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/LightningNetwork/{cryptoCode}",
method: HttpMethod.Delete), token);
await HandleResponse(response);
}
public virtual async Task<LightningNetworkPaymentMethodData> UpdateStoreLightningNetworkPaymentMethod(
string storeId,
string cryptoCode, LightningNetworkPaymentMethodData paymentMethod,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/LightningNetwork/{cryptoCode}",
bodyPayload: paymentMethod, method: HttpMethod.Put), token);
return await HandleResponse<LightningNetworkPaymentMethodData>(response);
}
public virtual Task<LightningNetworkPaymentMethodData>
UpdateStoreLightningNetworkPaymentMethodToInternalNode(string storeId,
string cryptoCode, CancellationToken token = default) => UpdateStoreLightningNetworkPaymentMethod(
storeId, cryptoCode, new LightningNetworkPaymentMethodData(cryptoCode, "Internal Node", true), token);
}
}

View File

@ -1,23 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<PermissionMetadata[]> GetPermissionMetadata(CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest("misc/permissions"), token);
return await HandleResponse<PermissionMetadata[]>(response);
}
public virtual async Task<Language[]> GetAvailableLanguages(CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest("misc/lang"), token);
return await HandleResponse<Language[]>(response);
}
}
}

View File

@ -9,19 +9,11 @@ namespace BTCPayServer.Client
public partial class BTCPayServerClient
{
public virtual async Task<IEnumerable<OnChainPaymentMethodData>> GetStoreOnChainPaymentMethods(string storeId,
bool? enabled = null,
CancellationToken token = default)
{
var query = new Dictionary<string, object>();
if (enabled != null)
{
query.Add(nameof(enabled), enabled);
}
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain",
query), token);
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain"), token);
return await HandleResponse<IEnumerable<OnChainPaymentMethodData>>(response);
}
@ -78,17 +70,5 @@ namespace BTCPayServer.Client
method: HttpMethod.Get), token);
return await HandleResponse<OnChainPaymentMethodPreviewResultData>(response);
}
public virtual async Task<OnChainPaymentMethodDataWithSensitiveData> GenerateOnChainWallet(string storeId,
string cryptoCode, GenerateOnChainWalletRequest request,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/generate",
bodyPayload: request,
method: HttpMethod.Post), token);
return await HandleResponse<OnChainPaymentMethodDataWithSensitiveData>(response);
}
}
}

View File

@ -1,122 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using NBitcoin;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<OnChainWalletOverviewData> ShowOnChainWalletOverview(string storeId, string cryptoCode,
CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet"), token);
return await HandleResponse<OnChainWalletOverviewData>(response);
}
public virtual async Task<OnChainWalletFeeRateData> GetOnChainFeeRate(string storeId, string cryptoCode, int? blockTarget = null,
CancellationToken token = default)
{
Dictionary<string, object> queryParams = new Dictionary<string, object>();
if (blockTarget != null)
{
queryParams.Add("blockTarget",blockTarget);
}
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/feeRate", queryParams), token);
return await HandleResponse<OnChainWalletFeeRateData>(response);
}
public virtual async Task<OnChainWalletAddressData> GetOnChainWalletReceiveAddress(string storeId, string cryptoCode, bool forceGenerate = false,
CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/address", new Dictionary<string, object>()
{
{"forceGenerate", forceGenerate}
}), token);
return await HandleResponse<OnChainWalletAddressData>(response);
}
public virtual async Task UnReserveOnChainWalletReceiveAddress(string storeId, string cryptoCode,
CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/address",method:HttpMethod.Delete), token);
await HandleResponse(response);
}
public virtual async Task<IEnumerable<OnChainWalletTransactionData>> ShowOnChainWalletTransactions(
string storeId, string cryptoCode, TransactionStatus[] statusFilter = null,
CancellationToken token = default)
{
var query = new Dictionary<string, object>();
if (statusFilter?.Any() is true)
{
query.Add(nameof(statusFilter), statusFilter);
}
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/transactions", query), token);
return await HandleResponse<IEnumerable<OnChainWalletTransactionData>>(response);
}
public virtual async Task<OnChainWalletTransactionData> GetOnChainWalletTransaction(
string storeId, string cryptoCode, string transactionId,
CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/transactions/{transactionId}"), token);
return await HandleResponse<OnChainWalletTransactionData>(response);
}
public virtual async Task<IEnumerable<OnChainWalletUTXOData>> GetOnChainWalletUTXOs(string storeId,
string cryptoCode,
CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/utxos"), token);
return await HandleResponse<IEnumerable<OnChainWalletUTXOData>>(response);
}
public virtual async Task<OnChainWalletTransactionData> CreateOnChainTransaction(string storeId,
string cryptoCode, CreateOnChainTransactionRequest request,
CancellationToken token = default)
{
if (!request.ProceedWithBroadcast)
{
throw new ArgumentOutOfRangeException(nameof(request.ProceedWithBroadcast),
"Please use CreateOnChainTransactionButDoNotBroadcast when wanting to only create the transaction");
}
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/transactions", null, request, HttpMethod.Post), token);
return await HandleResponse<OnChainWalletTransactionData>(response);
}
public virtual async Task<Transaction> CreateOnChainTransactionButDoNotBroadcast(string storeId,
string cryptoCode, CreateOnChainTransactionRequest request, Network network,
CancellationToken token = default)
{
if (request.ProceedWithBroadcast)
{
throw new ArgumentOutOfRangeException(nameof(request.ProceedWithBroadcast),
"Please use CreateOnChainTransaction when wanting to also broadcast the transaction");
}
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/Onchain/{cryptoCode}/wallet/transactions", null, request, HttpMethod.Post), token);
return Transaction.Parse(await HandleResponse<string>(response), network);
}
}
}

View File

@ -9,18 +9,18 @@ namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<PullPaymentData> CreatePullPayment(string storeId, CreatePullPaymentRequest request, CancellationToken cancellationToken = default)
public async Task<PullPaymentData> CreatePullPayment(string storeId, CreatePullPaymentRequest request, CancellationToken cancellationToken = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{HttpUtility.UrlEncode(storeId)}/pull-payments", bodyPayload: request, method: HttpMethod.Post), cancellationToken);
return await HandleResponse<PullPaymentData>(response);
}
public virtual async Task<PullPaymentData> GetPullPayment(string pullPaymentId, CancellationToken cancellationToken = default)
public async Task<PullPaymentData> GetPullPayment(string pullPaymentId, CancellationToken cancellationToken = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/pull-payments/{HttpUtility.UrlEncode(pullPaymentId)}", method: HttpMethod.Get), cancellationToken);
return await HandleResponse<PullPaymentData>(response);
}
public virtual async Task<PullPaymentData[]> GetPullPayments(string storeId, bool includeArchived = false, CancellationToken cancellationToken = default)
public async Task<PullPaymentData[]> GetPullPayments(string storeId, bool includeArchived = false, CancellationToken cancellationToken = default)
{
Dictionary<string, object> query = new Dictionary<string, object>();
query.Add("includeArchived", includeArchived);
@ -28,44 +28,34 @@ namespace BTCPayServer.Client
return await HandleResponse<PullPaymentData[]>(response);
}
public virtual async Task ArchivePullPayment(string storeId, string pullPaymentId, CancellationToken cancellationToken = default)
public async Task ArchivePullPayment(string storeId, string pullPaymentId, CancellationToken cancellationToken = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{HttpUtility.UrlEncode(storeId)}/pull-payments/{HttpUtility.UrlEncode(pullPaymentId)}", method: HttpMethod.Delete), cancellationToken);
await HandleResponse(response);
}
public virtual async Task<PayoutData[]> GetPayouts(string pullPaymentId, bool includeCancelled = false, CancellationToken cancellationToken = default)
public async Task<PayoutData[]> GetPayouts(string pullPaymentId, bool includeCancelled = false, CancellationToken cancellationToken = default)
{
Dictionary<string, object> query = new Dictionary<string, object>();
query.Add("includeCancelled", includeCancelled);
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/pull-payments/{HttpUtility.UrlEncode(pullPaymentId)}/payouts", queryPayload: query, method: HttpMethod.Get), cancellationToken);
return await HandleResponse<PayoutData[]>(response);
}
public virtual async Task<PayoutData> CreatePayout(string pullPaymentId, CreatePayoutRequest payoutRequest, CancellationToken cancellationToken = default)
public async Task<PayoutData> CreatePayout(string pullPaymentId, CreatePayoutRequest payoutRequest, CancellationToken cancellationToken = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/pull-payments/{HttpUtility.UrlEncode(pullPaymentId)}/payouts", bodyPayload: payoutRequest, method: HttpMethod.Post), cancellationToken);
return await HandleResponse<PayoutData>(response);
}
public virtual async Task CancelPayout(string storeId, string payoutId, CancellationToken cancellationToken = default)
public async Task CancelPayout(string storeId, string payoutId, CancellationToken cancellationToken = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{HttpUtility.UrlEncode(storeId)}/payouts/{HttpUtility.UrlEncode(payoutId)}", method: HttpMethod.Delete), cancellationToken);
await HandleResponse(response);
}
public virtual async Task<PayoutData> ApprovePayout(string storeId, string payoutId, ApprovePayoutRequest request, CancellationToken cancellationToken = default)
public async Task<PayoutData> ApprovePayout(string storeId, string payoutId, ApprovePayoutRequest request, CancellationToken cancellationToken = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{HttpUtility.UrlEncode(storeId)}/payouts/{HttpUtility.UrlEncode(payoutId)}", bodyPayload: request, method: HttpMethod.Post), cancellationToken);
return await HandleResponse<PayoutData>(response);
}
public async Task MarkPayoutPaid(string storeId, string payoutId,
CancellationToken cancellationToken = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest(
$"api/v1/stores/{HttpUtility.UrlEncode(storeId)}/payouts/{HttpUtility.UrlEncode(payoutId)}/mark-paid",
method: HttpMethod.Post), cancellationToken);
await HandleResponse(response);
}
}
}

View File

@ -1,27 +0,0 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<Dictionary<string, GenericPaymentMethodData>> GetStorePaymentMethods(string storeId,
bool? enabled = null,
CancellationToken token = default)
{
var query = new Dictionary<string, object>();
if (enabled != null)
{
query.Add(nameof(enabled), enabled);
}
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods",
query), token);
return await HandleResponse<Dictionary<string, GenericPaymentMethodData>>(response);
}
}
}

View File

@ -1,4 +1,3 @@
#nullable enable
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
@ -20,16 +19,5 @@ namespace BTCPayServer.Client
var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/users", null, request, HttpMethod.Post), token);
return await HandleResponse<ApplicationUserData>(response);
}
public virtual async Task DeleteUser(string userId, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/users/{userId}", null, HttpMethod.Delete), token);
await HandleResponse(response);
}
public virtual async Task DeleteCurrentUser(CancellationToken token = default)
{
await DeleteUser("me", token);
}
}
}

View File

@ -1,56 +1,60 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client
{
public partial class BTCPayServerClient
{
public virtual async Task<StoreWebhookData> CreateWebhook(string storeId, Client.Models.CreateStoreWebhookRequest create, CancellationToken token = default)
public async Task<StoreWebhookData> CreateWebhook(string storeId, Client.Models.CreateStoreWebhookRequest create, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/webhooks", bodyPayload: create, method: HttpMethod.Post), token);
return await HandleResponse<StoreWebhookData>(response);
}
public virtual async Task<StoreWebhookData> GetWebhook(string storeId, string webhookId, CancellationToken token = default)
public async Task<StoreWebhookData> GetWebhook(string storeId, string webhookId, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/webhooks/{webhookId}"), token);
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
return null;
return await HandleResponse<StoreWebhookData>(response);
}
public virtual async Task<StoreWebhookData> UpdateWebhook(string storeId, string webhookId, Models.UpdateStoreWebhookRequest update, CancellationToken token = default)
public async Task<StoreWebhookData> UpdateWebhook(string storeId, string webhookId, Models.UpdateStoreWebhookRequest update, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/webhooks/{webhookId}", bodyPayload: update, method: HttpMethod.Put), token);
return await HandleResponse<StoreWebhookData>(response);
}
public virtual async Task<bool> DeleteWebhook(string storeId, string webhookId, CancellationToken token = default)
public async Task<bool> DeleteWebhook(string storeId, string webhookId, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/webhooks/{webhookId}", method: HttpMethod.Delete), token);
return response.IsSuccessStatusCode;
}
public virtual async Task<StoreWebhookData[]> GetWebhooks(string storeId, CancellationToken token = default)
public async Task<StoreWebhookData[]> GetWebhooks(string storeId, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/webhooks"), token);
return await HandleResponse<StoreWebhookData[]>(response);
}
public virtual async Task<WebhookDeliveryData[]> GetWebhookDeliveries(string storeId, string webhookId, CancellationToken token = default)
public async Task<WebhookDeliveryData[]> GetWebhookDeliveries(string storeId, string webhookId, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/webhooks/{webhookId}/deliveries"), token);
return await HandleResponse<WebhookDeliveryData[]>(response);
}
public virtual async Task<WebhookDeliveryData> GetWebhookDelivery(string storeId, string webhookId, string deliveryId, CancellationToken token = default)
public async Task<WebhookDeliveryData> GetWebhookDelivery(string storeId, string webhookId, string deliveryId, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}"), token);
return await HandleResponse<WebhookDeliveryData>(response);
}
public virtual async Task<string> RedeliverWebhook(string storeId, string webhookId, string deliveryId, CancellationToken token = default)
public async Task<string> RedeliverWebhook(string storeId, string webhookId, string deliveryId, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/redeliver", null, HttpMethod.Post), token);
return await HandleResponse<string>(response);
}
public virtual async Task<WebhookEvent> GetWebhookDeliveryRequest(string storeId, string webhookId, string deliveryId, CancellationToken token = default)
public async Task<WebhookEvent> GetWebhookDeliveryRequest(string storeId, string webhookId, string deliveryId, CancellationToken token = default)
{
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/request"), token);
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)

View File

@ -5,7 +5,6 @@ using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
@ -70,13 +69,6 @@ namespace BTCPayServer.Client
return JsonConvert.DeserializeObject<T>(str);
}
public async Task<T> SendHttpRequest<T>(string path,
Dictionary<string, object> queryPayload = null,
HttpMethod method = null, CancellationToken cancellationToken = default)
{
using var resp = await _httpClient.SendAsync(CreateHttpRequest(path, queryPayload, method), cancellationToken);
return await HandleResponse<T>(resp);
}
protected virtual HttpRequestMessage CreateHttpRequest(string path,
Dictionary<string, object> queryPayload = null,
HttpMethod method = null)

View File

@ -1,31 +0,0 @@
using System;
using NBitcoin;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.JsonConverters
{
public class MnemonicJsonConverter : JsonConverter<Mnemonic>
{
public override Mnemonic ReadJson(JsonReader reader, Type objectType, Mnemonic existingValue, bool hasExistingValue,
JsonSerializer serializer)
{
return reader.TokenType switch
{
JsonToken.String => new Mnemonic((string)reader.Value),
JsonToken.Null => null,
_ => throw new JsonObjectException(reader.Path, "Mnemonic must be a json string")
};
}
public override void WriteJson(JsonWriter writer, Mnemonic value, JsonSerializer serializer)
{
if (value != null)
writer.WriteValue(value.ToString());
else
{
writer.WriteNull();
}
}
}
}

View File

@ -1,59 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using NBitcoin;
using Newtonsoft.Json;
public class WordcountJsonConverter : JsonConverter
{
static WordcountJsonConverter()
{
_Wordcount = new Dictionary<long, WordCount>()
{
{18, WordCount.Eighteen},
{15, WordCount.Fifteen},
{12, WordCount.Twelve},
{24, WordCount.TwentyFour},
{21, WordCount.TwentyOne}
};
_WordcountReverse = _Wordcount.ToDictionary(kv => kv.Value, kv => kv.Key);
}
public override bool CanConvert(Type objectType)
{
return typeof(NBitcoin.WordCount).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo()) ||
typeof(NBitcoin.WordCount?).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return default;
if (reader.TokenType != JsonToken.Integer)
throw new NBitcoin.JsonConverters.JsonObjectException(
$"Unexpected json token type, expected Integer, actual {reader.TokenType}", reader);
if (!_Wordcount.TryGetValue((long)reader.Value, out var result))
throw new NBitcoin.JsonConverters.JsonObjectException(
$"Invalid WordCount, possible values {string.Join(", ", _Wordcount.Keys.ToArray())} (default: 12)",
reader);
return result;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value is WordCount wc)
writer.WriteValue(_WordcountReverse[wc]);
}
readonly static Dictionary<long, WordCount> _Wordcount = new Dictionary<long, WordCount>()
{
{18, WordCount.Eighteen},
{15, WordCount.Fifteen},
{12, WordCount.Twelve},
{24, WordCount.TwentyFour},
{21, WordCount.TwentyOne}
};
readonly static Dictionary<WordCount, long> _WordcountReverse;
}

View File

@ -1,55 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using NBitcoin;
using Newtonsoft.Json;
public class WordlistJsonConverter : JsonConverter
{
static WordlistJsonConverter()
{
_Wordlists = new Dictionary<string, Wordlist>(StringComparer.OrdinalIgnoreCase)
{
{"English", Wordlist.English},
{"Japanese", Wordlist.Japanese},
{"Spanish", Wordlist.Spanish},
{"ChineseSimplified", Wordlist.ChineseSimplified},
{"ChineseTraditional", Wordlist.ChineseTraditional},
{"French", Wordlist.French},
{"PortugueseBrazil", Wordlist.PortugueseBrazil},
{"Czech", Wordlist.Czech}
};
_WordlistsReverse = _Wordlists.ToDictionary(kv => kv.Value, kv => kv.Key);
}
public override bool CanConvert(Type objectType)
{
return typeof(Wordlist).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
if (reader.TokenType != JsonToken.String)
throw new NBitcoin.JsonConverters.JsonObjectException(
$"Unexpected json token type, expected String, actual {reader.TokenType}", reader);
if (!_Wordlists.TryGetValue((string)reader.Value, out var result))
throw new NBitcoin.JsonConverters.JsonObjectException(
$"Invalid wordlist, possible values {string.Join(", ", _Wordlists.Keys.ToArray())} (default: English)",
reader);
return result;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value is Wordlist wl)
writer.WriteValue(_WordlistsReverse[wl]);
}
readonly static Dictionary<string, Wordlist> _Wordlists;
readonly static Dictionary<Wordlist, string> _WordlistsReverse;
}

View File

@ -7,10 +7,33 @@ using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models
{
public class CreateInvoiceRequest : InvoiceDataBase
public class CreateInvoiceRequest
{
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal? Amount { get; set; }
public string[] AdditionalSearchTerms { get; set; }
[JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))]
public decimal Amount { get; set; }
public string Currency { get; set; }
public JObject Metadata { get; set; }
public CheckoutOptions Checkout { get; set; } = new CheckoutOptions();
public class CheckoutOptions
{
[JsonConverter(typeof(StringEnumConverter))]
public SpeedPolicy? SpeedPolicy { get; set; }
public string[] PaymentMethods { get; set; }
[JsonConverter(typeof(TimeSpanJsonConverter.Minutes))]
[JsonProperty("expirationMinutes")]
public TimeSpan? Expiration { get; set; }
[JsonConverter(typeof(TimeSpanJsonConverter.Minutes))]
[JsonProperty("monitoringMinutes")]
public TimeSpan? Monitoring { get; set; }
public double? PaymentTolerance { get; set; }
[JsonProperty("redirectURL")]
public string RedirectURL { get; set; }
public string DefaultLanguage { get; set; }
}
}
}

View File

@ -1,30 +0,0 @@
using System.Collections.Generic;
using BTCPayServer.JsonConverters;
using NBitcoin;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class CreateOnChainTransactionRequest
{
public class CreateOnChainTransactionRequestDestination
{
public string Destination { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal? Amount { get; set; }
public bool SubtractFromAmount { get; set; }
}
[JsonConverter(typeof(FeeRateJsonConverter))]
public FeeRate FeeRate { get; set; }
public bool ProceedWithPayjoin { get; set; }= true;
public bool ProceedWithBroadcast { get; set; } = true;
public bool NoChange { get; set; } = false;
[JsonProperty(ItemConverterType = typeof(OutpointJsonConverter))]
public List<OutPoint> SelectedInputs { get; set; } = null;
public List<CreateOnChainTransactionRequestDestination> Destinations { get; set; }
[JsonProperty("rbf")]
public bool? RBF { get; set; } = null;
}
}

View File

@ -1,25 +0,0 @@
using BTCPayServer.Client.JsonConverters;
using NBitcoin;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace BTCPayServer.Client
{
public class GenerateOnChainWalletRequest
{
public int AccountNumber { get; set; } = 0;
[JsonConverter(typeof(MnemonicJsonConverter))]
public Mnemonic ExistingMnemonic { get; set; }
[JsonConverter(typeof(WordlistJsonConverter))]
public NBitcoin.Wordlist WordList { get; set; }
[JsonConverter(typeof(WordcountJsonConverter))]
public NBitcoin.WordCount? WordCount { get; set; } = NBitcoin.WordCount.Twelve;
[JsonConverter(typeof(StringEnumConverter))]
public NBitcoin.ScriptPubKeyType ScriptPubKeyType { get; set; } = ScriptPubKeyType.Segwit;
public string Passphrase { get; set; }
public bool ImportKeysToRPC { get; set; }
public bool SavePrivateKeys { get; set; }
}
}

View File

@ -1,8 +0,0 @@
namespace BTCPayServer.Client.Models
{
public class GenericPaymentMethodData
{
public bool Enabled { get; set; }
public object Data { get; set; }
}
}

View File

@ -1,53 +1,12 @@
using System;
using BTCPayServer.Client.JsonConverters;
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models
{
public enum InvoiceType
{
Standard,
TopUp
}
public class InvoiceDataBase
{
[JsonConverter(typeof(StringEnumConverter))]
public InvoiceType Type { get; set; }
public string Currency { get; set; }
public JObject Metadata { get; set; }
public CheckoutOptions Checkout { get; set; } = new CheckoutOptions();
public class CheckoutOptions
{
[JsonConverter(typeof(StringEnumConverter))]
public SpeedPolicy? SpeedPolicy { get; set; }
public string[] PaymentMethods { get; set; }
[JsonConverter(typeof(TimeSpanJsonConverter.Minutes))]
[JsonProperty("expirationMinutes")]
public TimeSpan? Expiration { get; set; }
[JsonConverter(typeof(TimeSpanJsonConverter.Minutes))]
[JsonProperty("monitoringMinutes")]
public TimeSpan? Monitoring { get; set; }
public double? PaymentTolerance { get; set; }
[JsonProperty("redirectURL")]
public string RedirectURL { get; set; }
public bool? RedirectAutomatically { get; set; }
public string DefaultLanguage { get; set; }
}
}
public class InvoiceData : InvoiceDataBase
public class InvoiceData : CreateInvoiceRequest
{
public string Id { get; set; }
public string StoreId { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Amount { get; set; }
public string CheckoutLink { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
public InvoiceStatus Status { get; set; }

View File

@ -8,7 +8,6 @@ namespace BTCPayServer.Client.Models
{
public class InvoicePaymentMethodDataModel
{
public bool Activated { get; set; }
public string Destination { get; set; }
public string PaymentLink { get; set; }

View File

@ -1,14 +0,0 @@
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Client.Models
{
public class LabelData
{
public string Type { get; set; }
public string Text { get; set; }
[JsonExtensionData] public Dictionary<string, JToken> AdditionalData { get; set; }
}
}

View File

@ -1,21 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class Language
{
public Language(string code, string displayName)
{
DisplayName = displayName;
Code = code;
}
[JsonProperty("code")]
public string Code { get; set; }
[JsonProperty("currentLanguage")]
public string DisplayName { get; set; }
}
}

View File

@ -1,12 +0,0 @@
namespace BTCPayServer.Client.Models
{
public class LightningNetworkPaymentMethodBaseData
{
public string ConnectionString { get; set; }
public LightningNetworkPaymentMethodBaseData()
{
}
}
}

View File

@ -1,26 +0,0 @@
namespace BTCPayServer.Client.Models
{
public class LightningNetworkPaymentMethodData: LightningNetworkPaymentMethodBaseData
{
/// <summary>
/// Whether the payment method is enabled
/// </summary>
public bool Enabled { get; set; }
/// <summary>
/// Crypto code of the payment method
/// </summary>
public string CryptoCode { get; set; }
public LightningNetworkPaymentMethodData()
{
}
public LightningNetworkPaymentMethodData(string cryptoCode, string connectionString, bool enabled)
{
Enabled = enabled;
CryptoCode = cryptoCode;
ConnectionString = connectionString;
}
}
}

View File

@ -1,24 +0,0 @@
using NBitcoin;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class OnChainPaymentMethodBaseData
{
/// <summary>
/// The derivation scheme
/// </summary>
public string DerivationScheme { get; set; }
public string Label { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.KeyPathJsonConverter))]
public RootedKeyPath AccountKeyPath { get; set; }
public OnChainPaymentMethodBaseData()
{
}
}
}

View File

@ -1,8 +1,9 @@
using NBitcoin;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class OnChainPaymentMethodData : OnChainPaymentMethodBaseData
public class OnChainPaymentMethodData
{
/// <summary>
/// Whether the payment method is enabled
@ -14,16 +15,23 @@ namespace BTCPayServer.Client.Models
/// </summary>
public string CryptoCode { get; set; }
/// <summary>
/// The derivation scheme
/// </summary>
public string DerivationScheme { get; set; }
public string Label { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.KeyPathJsonConverter))]
public RootedKeyPath AccountKeyPath { get; set; }
public OnChainPaymentMethodData()
{
}
public OnChainPaymentMethodData(string cryptoCode, string derivationScheme, bool enabled, string label, RootedKeyPath accountKeyPath)
public OnChainPaymentMethodData(string cryptoCode, string derivationScheme, bool enabled)
{
Enabled = enabled;
Label = label;
AccountKeyPath = accountKeyPath;
CryptoCode = cryptoCode;
DerivationScheme = derivationScheme;
}

View File

@ -1,23 +0,0 @@
using BTCPayServer.Client.JsonConverters;
using NBitcoin;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class OnChainPaymentMethodDataWithSensitiveData : OnChainPaymentMethodData
{
public OnChainPaymentMethodDataWithSensitiveData()
{
}
public OnChainPaymentMethodDataWithSensitiveData(string cryptoCode, string derivationScheme, bool enabled,
string label, RootedKeyPath accountKeyPath, Mnemonic mnemonic) : base(cryptoCode, derivationScheme, enabled,
label, accountKeyPath)
{
Mnemonic = mnemonic;
}
[JsonConverter(typeof(MnemonicJsonConverter))]
public Mnemonic Mnemonic { get; set; }
}
}

View File

@ -1,15 +0,0 @@
using NBitcoin;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class OnChainWalletAddressData
{
public string Address { get; set; }
[JsonConverter(typeof(KeyPathJsonConverter))]
public KeyPath KeyPath { get; set; }
public string PaymentLink { get; set; }
}
}

View File

@ -1,12 +0,0 @@
using NBitcoin;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class OnChainWalletFeeRateData
{
[JsonConverter(typeof(FeeRateJsonConverter))]
public FeeRate FeeRate { get; set; }
}
}

View File

@ -1,17 +0,0 @@
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class OnChainWalletOverviewData
{
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Balance { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal UnconfirmedBalance { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal ConfirmedBalance { get; set; }
public string Label { get; set; }
}
}

View File

@ -1,34 +0,0 @@
using System;
using System.Collections.Generic;
using BTCPayServer.JsonConverters;
using NBitcoin;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class OnChainWalletTransactionData
{
[JsonConverter(typeof(UInt256JsonConverter))]
public uint256 TransactionHash { get; set; }
public string Comment { get; set; }
public Dictionary<string, LabelData> Labels { get; set; } = new Dictionary<string, LabelData>();
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Amount { get; set; }
[JsonConverter(typeof(UInt256JsonConverter))]
public uint256 BlockHash { get; set; }
public int? BlockHeight { get; set; }
public int Confirmations { get; set; }
[JsonConverter(typeof(DateTimeToUnixTimeConverter))]
public DateTimeOffset Timestamp { get; set; }
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
public TransactionStatus Status { get; set; }
}
}

View File

@ -1,26 +0,0 @@
using System;
using System.Collections.Generic;
using BTCPayServer.JsonConverters;
using NBitcoin;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class OnChainWalletUTXOData
{
public string Comment { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Amount { get; set; }
[JsonConverter(typeof(OutpointJsonConverter))]
public OutPoint Outpoint { get; set; }
public string Link { get; set; }
public Dictionary<string, LabelData> Labels { get; set; }
[JsonConverter(typeof(DateTimeToUnixTimeConverter))]
public DateTimeOffset Timestamp { get; set; }
[JsonConverter(typeof(KeyPathJsonConverter))]
public KeyPath KeyPath { get; set; }
public string Address { get; set; }
public int Confirmations { get; set; }
}
}

View File

@ -1,40 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Newtonsoft.Json;
namespace BTCPayServer.Client.Models
{
public class PermissionMetadata
{
static PermissionMetadata()
{
Dictionary<string, PermissionMetadata> nodes = new Dictionary<string, PermissionMetadata>();
foreach (var policy in Client.Policies.AllPolicies)
{
nodes.Add(policy, new PermissionMetadata() { PermissionName = policy });
}
foreach (var n in nodes)
{
foreach (var policy in Client.Policies.AllPolicies)
{
if (policy.Equals(n.Key, StringComparison.OrdinalIgnoreCase))
continue;
if (Client.Permission.Create(n.Key).Contains(Client.Permission.Create(policy)))
n.Value.SubPermissions.Add(policy);
}
}
foreach (var n in nodes)
{
n.Value.SubPermissions.Sort();
}
PermissionNodes = nodes.Values.OrderBy(v => v.PermissionName).ToArray();
}
public readonly static PermissionMetadata[] PermissionNodes;
[JsonProperty("name")]
public string PermissionName { get; set; }
[JsonProperty("included")]
public List<string> SubPermissions { get; set; } = new List<string>();
}
}

View File

@ -27,17 +27,12 @@ namespace BTCPayServer.Client.Models
/// <summary>
/// detailed sync information per chain
/// </summary>
public IEnumerable<SyncStatus> SyncStatus { get; set; }
}
public class SyncStatus
{
public string CryptoCode { get; set; }
public virtual bool Available { get; set; }
public IEnumerable<ServerInfoSyncStatusData> SyncStatus { get; set; }
}
public class ServerInfoSyncStatusData: SyncStatus
public class ServerInfoSyncStatusData
{
public string CryptoCode { get; set; }
public int ChainHeight { get; set; }
public int? SyncHeight { get; set; }
public ServerInfoNodeData NodeInformation { get; set; }

View File

@ -35,7 +35,6 @@ namespace BTCPayServer.Client.Models
public bool LightningAmountInSatoshi { get; set; }
public bool LightningPrivateRouteHints { get; set; }
public bool OnChainWithLnInvoiceFallback { get; set; }
public bool LazyPaymentMethods { get; set; }
public bool RedirectAutomatically { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
@ -54,6 +53,8 @@ namespace BTCPayServer.Client.Models
public string HtmlTitle { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public NetworkFeeMode NetworkFeeMode { get; set; } = NetworkFeeMode.Never;

View File

@ -1,9 +0,0 @@
namespace BTCPayServer.Client.Models
{
public enum TransactionStatus
{
Unconfirmed,
Confirmed,
Replaced
}
}

View File

@ -19,31 +19,7 @@ namespace BTCPayServer.Client.Models
}
public string DeliveryId { get; set; }
public string WebhookId { get; set; }
string _OriginalDeliveryId;
public string OriginalDeliveryId
{
get
{
if (_OriginalDeliveryId is null)
{
// Due to a typo in old version, we serialized `orignalDeliveryId` rather than `orignalDeliveryId`
// We silently fix that here.
// Note we can remove this code later on, as old webhook event are unlikely to be useful to anyone,
// and having a null orignalDeliveryId is not end of the world
if (AdditionalData != null &&
AdditionalData.TryGetValue("orignalDeliveryId", out var tok))
{
_OriginalDeliveryId = tok.Value<string>();
AdditionalData.Remove("orignalDeliveryId");
}
}
return _OriginalDeliveryId;
}
set
{
_OriginalDeliveryId = value;
}
}
public string OrignalDeliveryId { get; set; }
public bool IsRedelivery { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
public WebhookEventType Type { get; set; }

View File

@ -17,7 +17,6 @@ namespace BTCPayServer.Client
public const string CanViewStoreSettings = "btcpay.store.canviewstoresettings";
public const string CanViewInvoices = "btcpay.store.canviewinvoices";
public const string CanCreateInvoice = "btcpay.store.cancreateinvoice";
public const string CanModifyInvoices = "btcpay.store.canmodifyinvoices";
public const string CanViewPaymentRequests = "btcpay.store.canviewpaymentrequests";
public const string CanModifyPaymentRequests = "btcpay.store.canmodifypaymentrequests";
public const string CanModifyProfile = "btcpay.user.canmodifyprofile";
@ -25,7 +24,6 @@ namespace BTCPayServer.Client
public const string CanManageNotificationsForUser = "btcpay.user.canmanagenotificationsforuser";
public const string CanViewNotificationsForUser = "btcpay.user.canviewnotificationsforuser";
public const string CanCreateUser = "btcpay.server.cancreateuser";
public const string CanDeleteUser = "btcpay.user.candeleteuser";
public const string CanManagePullPayments = "btcpay.store.canmanagepullpayments";
public const string Unrestricted = "unrestricted";
public static IEnumerable<string> AllPolicies
@ -34,7 +32,6 @@ namespace BTCPayServer.Client
{
yield return CanViewInvoices;
yield return CanCreateInvoice;
yield return CanModifyInvoices;
yield return CanModifyStoreWebhooks;
yield return CanModifyServerSettings;
yield return CanModifyStoreSettings;
@ -44,7 +41,6 @@ namespace BTCPayServer.Client
yield return CanModifyProfile;
yield return CanViewProfile;
yield return CanCreateUser;
yield return CanDeleteUser;
yield return CanManageNotificationsForUser;
yield return CanViewNotificationsForUser;
yield return Unrestricted;
@ -166,12 +162,10 @@ namespace BTCPayServer.Client
switch (subpolicy)
{
case Policies.CanViewInvoices when this.Policy == Policies.CanModifyStoreSettings:
case Policies.CanViewInvoices when this.Policy == Policies.CanModifyInvoices:
case Policies.CanModifyStoreWebhooks when this.Policy == Policies.CanModifyStoreSettings:
case Policies.CanViewInvoices when this.Policy == Policies.CanViewStoreSettings:
case Policies.CanViewStoreSettings when this.Policy == Policies.CanModifyStoreSettings:
case Policies.CanCreateInvoice when this.Policy == Policies.CanModifyStoreSettings:
case Policies.CanModifyInvoices when this.Policy == Policies.CanModifyStoreSettings:
case Policies.CanViewProfile when this.Policy == Policies.CanModifyProfile:
case Policies.CanModifyPaymentRequests when this.Policy == Policies.CanModifyStoreSettings:
case Policies.CanViewPaymentRequests when this.Policy == Policies.CanModifyStoreSettings:
@ -179,7 +173,6 @@ namespace BTCPayServer.Client
case Policies.CanCreateLightningInvoiceInternalNode when this.Policy == Policies.CanUseInternalLightningNode:
case Policies.CanCreateLightningInvoiceInStore when this.Policy == Policies.CanUseLightningNodeInStore:
case Policies.CanViewNotificationsForUser when this.Policy == Policies.CanManageNotificationsForUser:
case Policies.CanUseInternalLightningNode when this.Policy == Policies.CanModifyServerSettings:
return true;
default:
return false;

View File

@ -1,29 +0,0 @@
using NBitcoin;
using NBXplorer;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitAlthash()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("HTML");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Althash",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://explorer.htmlcoin.com/api/tx/{0}" : "https://explorer.htmlcoin.com/api/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "htmlcoin",
DefaultRateRules = new[]
{
"HTML_X = HTML_USD",
"HTML_USD = hitbtc(HTML_USD)"
},
CryptoImagePath = "imlegacy/althash.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("88'") : new KeyPath("1'")
});
}
}
}

View File

@ -11,8 +11,8 @@ namespace BTCPayServer
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "BitCore",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://explorer.bitcore.cc/tx/{0}" : "https://explorer.bitcore.cc/tx/{0}",
DisplayName = "Bitcore",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://insight.bitcore.cc/tx/{0}" : "https://insight.bitcore.cc/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "bitcore",
DefaultRateRules = new[]

View File

@ -20,7 +20,7 @@ namespace BTCPayServer
DefaultRateRules = new[]
{
"DASH_X = DASH_BTC * BTC_X",
"DASH_BTC = bitfinex(DSH_BTC)"
"DASH_BTC = bittrex(DASH_BTC)"
},
CryptoImagePath = "imlegacy/dash.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),

View File

@ -26,8 +26,7 @@ namespace BTCPayServer
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("17'") : new KeyPath("1'"),
SupportRBF = true,
SupportPayJoin = true,
VaultSupported = true
SupportPayJoin = true
});
}
}

View File

@ -1,7 +1,6 @@
#if ALTCOINS
using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Common;
using NBitcoin;
using NBXplorer;
using NBXplorer.Models;
@ -25,24 +24,38 @@ namespace BTCPayServer
});
}
public override List<TransactionInformation> FilterValidTransactions(List<TransactionInformation> transactionInformationSet)
public override GetTransactionsResponse FilterValidTransactions(GetTransactionsResponse response)
{
return transactionInformationSet.FindAll(information =>
information.Outputs.Any(output =>
output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId) ||
information.Inputs.Any(output =>
output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId));
TransactionInformationSet Filter(TransactionInformationSet transactionInformationSet)
{
return new TransactionInformationSet()
{
Transactions =
transactionInformationSet.Transactions.FindAll(information =>
information.Outputs.Any(output =>
output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId) ||
information.Inputs.Any(output =>
output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId))
};
}
return new GetTransactionsResponse()
{
Height = response.Height,
ConfirmedTransactions = Filter(response.ConfirmedTransactions),
ReplacedTransactions = Filter(response.ReplacedTransactions),
UnconfirmedTransactions = Filter(response.UnconfirmedTransactions)
};
}
public override PaymentUrlBuilder GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue)
public override string GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue)
{
//precision 0: 10 = 0.00000010
//precision 2: 10 = 0.00001000
//precision 8: 10 = 10
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;
var money = new Money(cryptoInfoDue.ToDecimal(MoneyUnit.BTC) / decimal.Parse("1".PadRight(1 + 8 - Divisibility, '0')), MoneyUnit.BTC);
return $"{base.GenerateBIP21(cryptoInfoAddress, money)}&assetid={AssetId}";
}
}
}

View File

@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using BTCPayServer.Common;
using NBitcoin;
using NBXplorer;
using NBXplorer.Models;
@ -55,7 +54,7 @@ namespace BTCPayServer
{
public Network NBitcoinNetwork { get { return NBXplorerNetwork?.NBitcoinNetwork; } }
public NBXplorer.NBXplorerNetwork NBXplorerNetwork { get; set; }
public bool SupportRBF { get; set; }
public bool SupportRBF { get; internal set; }
public string LightningImagePath { get; set; }
public BTCPayDefaultSettings DefaultSettings { get; set; }
public KeyPath CoinType { get; set; }
@ -64,9 +63,9 @@ namespace BTCPayServer
public virtual bool WalletSupported { get; set; } = true;
public virtual bool ReadonlyWallet { get; set; } = false;
public virtual bool VaultSupported { get; set; } = false;
public int MaxTrackedConfirmation { get; set; } = 6;
public string UriScheme { get; set; }
public int MaxTrackedConfirmation { get; internal set; } = 6;
public string UriScheme { get; internal set; }
public bool SupportPayJoin { get; set; } = false;
public bool SupportLightning { get; set; } = true;
@ -122,20 +121,14 @@ namespace BTCPayServer
});
}
public virtual PaymentUrlBuilder GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue)
public virtual string GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue)
{
var builder = new PaymentUrlBuilder(UriScheme);
builder.Host = cryptoInfoAddress;
if (cryptoInfoDue != null && cryptoInfoDue != Money.Zero)
{
builder.QueryParams.Add("amount", cryptoInfoDue.ToString(false, true));
}
return builder;
return $"{UriScheme}:{cryptoInfoAddress}?amount={cryptoInfoDue.ToString(false, true)}";
}
public virtual List<TransactionInformation> FilterValidTransactions(List<TransactionInformation> transactionInformationSet)
public virtual GetTransactionsResponse FilterValidTransactions(GetTransactionsResponse response)
{
return transactionInformationSet;
return response;
}
}
@ -159,7 +152,7 @@ namespace BTCPayServer
}
}
public string BlockExplorerLinkDefault { get; set; }
public string BlockExplorerLinkDefault { get; internal set; }
public string DisplayName { get; set; }
public int Divisibility { get; set; } = 8;
[Obsolete("Should not be needed")]

View File

@ -14,7 +14,7 @@ namespace BTCPayServer
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Bitcoin",
BlockExplorerLink = NetworkType == ChainName.Mainnet ? "https://blockstream.info/tx/{0}" :
NetworkType == Bitcoin.Instance.Signet.ChainName ? "https://explorer.bc-2.jp/tx/{0}"
NetworkType == Bitcoin.Instance.Signet.ChainName ? "https://explorer.bc-2.jp/"
: "https://blockstream.info/testnet/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "bitcoin",
@ -24,7 +24,6 @@ namespace BTCPayServer
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("0'") : new KeyPath("1'"),
SupportRBF = true,
SupportPayJoin = true,
VaultSupported = true,
//https://github.com/spesmilo/electrum/blob/11733d6bc271646a00b69ff07657119598874da4/electrum/constants.py
ElectrumMapping = NetworkType == ChainName.Mainnet
? new Dictionary<uint, DerivationType>()

View File

@ -51,7 +51,6 @@ namespace BTCPayServer
InitMonacoin();
InitDash();
InitFeathercoin();
InitAlthash();
InitGroestlcoin();
InitViacoin();
InitMonero();
@ -132,10 +131,5 @@ namespace BTCPayServer
}
return network as T;
}
public bool TryGetNetwork<T>(string cryptoCode, out T network) where T : BTCPayNetworkBase
{
network = GetNetwork<T>(cryptoCode);
return network != null;
}
}
}

View File

@ -4,9 +4,9 @@
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="NBXplorer.Client" Version="4.0.3" />
<PackageReference Include="NBXplorer.Client" Version="3.0.20" />
</ItemGroup>
<ItemGroup Condition="'$(Altcoins)' != 'true'">
<Compile Remove="Altcoins\**\*.cs"></Compile>
</ItemGroup>
</Project>
</Project>

View File

@ -1,6 +1,4 @@
using System.Collections.Generic;
using NBitcoin;
using NBXplorer.DerivationStrategy;
namespace BTCPayServer
{
@ -13,23 +11,5 @@ namespace BTCPayServer
hashSet.Add(item);
}
}
public static ScriptPubKeyType ScriptPubKeyType(this DerivationStrategyBase derivationStrategyBase)
{
if (IsSegwitCore(derivationStrategyBase))
{
return NBitcoin.ScriptPubKeyType.Segwit;
}
return (derivationStrategyBase is P2SHDerivationStrategy p2shStrat && IsSegwitCore(p2shStrat.Inner))
? NBitcoin.ScriptPubKeyType.SegwitP2SH
: NBitcoin.ScriptPubKeyType.Legacy;
}
private static bool IsSegwitCore(DerivationStrategyBase derivationStrategyBase)
{
return (derivationStrategyBase is P2WSHDerivationStrategy) ||
(derivationStrategyBase is DirectDerivationStrategy direct) && direct.Segwit;
}
}
}

View File

@ -1,10 +0,0 @@
using NBXplorer;
namespace BTCPayServer.Common
{
public interface IExplorerClientProvider
{
ExplorerClient GetExplorerClient(string cryptoCode);
ExplorerClient GetExplorerClient(BTCPayNetworkBase network);
}
}

View File

@ -1,30 +0,0 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace BTCPayServer.Common
{
public class PaymentUrlBuilder
{
public PaymentUrlBuilder(string uriScheme)
{
UriScheme = uriScheme;
}
public string UriScheme { get; set; }
public Dictionary<string, string> QueryParams { get; set; } = new Dictionary<string, string>();
public string? Host { get; set; }
public override string ToString()
{
StringBuilder builder = new StringBuilder($"{UriScheme}:{Host}");
if (QueryParams.Count != 0)
{
var parts = QueryParams.Select(q => Uri.EscapeDataString(q.Key) + "=" + System.Web.NBitcoin.HttpUtility.UrlEncode(q.Value))
.ToArray();
builder.Append($"?{string.Join('&', parts)}");
}
return builder.ToString();
}
}
}

View File

@ -55,7 +55,6 @@ namespace BTCPayServer.Data
public DbSet<StoreWebhookData> StoreWebhooks { get; set; }
public DbSet<StoreData> Stores { get; set; }
public DbSet<U2FDevice> U2FDevices { get; set; }
public DbSet<Fido2Credential> Fido2Credentials { get; set; }
public DbSet<UserStore> UserStore { get; set; }
public DbSet<WalletData> Wallets { get; set; }
public DbSet<WalletTransactionData> WalletTransactions { get; set; }
@ -100,7 +99,6 @@ namespace BTCPayServer.Data
StoreWebhookData.OnModelCreating(builder);
//StoreData.OnModelCreating(builder);
U2FDevice.OnModelCreating(builder);
Fido2Credential.OnModelCreating(builder);
Data.UserStore.OnModelCreating(builder);
//WalletData.OnModelCreating(builder);
WalletTransactionData.OnModelCreating(builder);

View File

@ -9,7 +9,6 @@ namespace BTCPayServer.Data
{
public bool RequiresEmailConfirmation { get; set; }
public List<StoredFile> StoredFiles { get; set; }
[Obsolete("U2F support has been replace with FIDO2")]
public List<U2FDevice> U2FDevices { get; set; }
public List<APIKeyData> APIKeys { get; set; }
public DateTimeOffset? Created { get; set; }
@ -17,6 +16,5 @@ namespace BTCPayServer.Data
public List<NotificationData> Notifications { get; set; }
public List<UserStore> UserStores { get; set; }
public List<Fido2Credential> Fido2Credentials { get; set; }
}
}

View File

@ -1,32 +0,0 @@
using System;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace BTCPayServer.Data
{
public class Fido2Credential
{
public string Name { get; set; }
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public string Id { get; set; }
public string ApplicationUserId { get; set; }
public byte[] Blob { get; set; }
public CredentialType Type { get; set; }
public enum CredentialType
{
FIDO2
}
public static void OnModelCreating(ModelBuilder builder)
{
builder.Entity<Fido2Credential>()
.HasOne(o => o.ApplicationUser)
.WithMany(i => i.Fido2Credentials)
.HasForeignKey(i => i.ApplicationUserId).OnDelete(DeleteBehavior.Cascade);
}
public ApplicationUser ApplicationUser { get; set; }
}
}

View File

@ -1,6 +1,5 @@
using System;
using System.ComponentModel.DataAnnotations;
using BTCPayServer.Client.Models;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
@ -54,4 +53,13 @@ namespace BTCPayServer.Data
}
}
}
public enum PayoutState
{
AwaitingApproval,
AwaitingPayment,
InProgress,
Completed,
Cancelled
}
}

View File

@ -3,13 +3,11 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using BTCPayServer.Client.Models;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
namespace BTCPayServer.Data
{
public class PullPaymentData
{
[Key]
@ -88,6 +86,7 @@ namespace BTCPayServer.Data
}
}
public static class PayoutExtensions
{
public static IQueryable<PayoutData> GetPayoutInPeriod(this IQueryable<PayoutData> payouts, PullPaymentData pp)

View File

@ -23,12 +23,11 @@ namespace BTCPayServer.Data
internal static void OnModelCreating(ModelBuilder builder)
{
#pragma warning disable CS0618 // Type or member is obsolete
builder.Entity<U2FDevice>()
.HasOne(o => o.ApplicationUser)
.WithMany(i => i.U2FDevices)
.HasForeignKey(i => i.ApplicationUserId).OnDelete(DeleteBehavior.Cascade);
#pragma warning restore CS0618 // Type or member is obsolete
}
}
}

View File

@ -1,48 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20210314092253_Fido2Credentials")]
public partial class Fido2Credentials : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
int? maxLength = this.IsMySql(migrationBuilder.ActiveProvider) ? (int?)255 : null;
migrationBuilder.CreateTable(
name: "Fido2Credentials",
columns: table => new
{
Id = table.Column<string>(nullable: false, maxLength: maxLength),
Name = table.Column<string>(nullable: true),
ApplicationUserId = table.Column<string>(nullable: true, maxLength: maxLength),
Blob = table.Column<byte[]>(nullable: true),
Type = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Fido2Credentials", x => x.Id);
table.ForeignKey(
name: "FK_Fido2Credentials_AspNetUsers_ApplicationUserId",
column: x => x.ApplicationUserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Fido2Credentials_ApplicationUserId",
table: "Fido2Credentials",
column: "ApplicationUserId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Fido2Credentials");
}
}
}

View File

@ -170,31 +170,6 @@ namespace BTCPayServer.Migrations
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("BTCPayServer.Data.Fido2Credential", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("ApplicationUserId")
.HasColumnType("TEXT");
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ApplicationUserId");
b.ToTable("Fido2Credentials");
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{
b.Property<string>("InvoiceDataId")
@ -983,14 +958,6 @@ namespace BTCPayServer.Migrations
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.Fido2Credential", b =>
{
b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser")
.WithMany("Fido2Credentials")
.HasForeignKey("ApplicationUserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")

View File

@ -20,7 +20,6 @@ namespace BTCPayServer.Plugins.Test.Services
await using var context = _testPluginDbContextFactory.CreateContext();
await context.TestPluginRecords.AddAsync(new TestPluginData() {Timestamp = DateTimeOffset.UtcNow});
await context.SaveChangesAsync();
}

View File

@ -9,8 +9,8 @@
<div class="row">
<h2>Persisted Data</h2>
<p>The following is data persisted to the configured database but in an isolated DbContext. Every time you start BTCPay Server with this plugin enabled, a timestamp is logged.</p>
<ul class="list-group">
<p>The following is data persisted to the configured database but in an isolated DbContext. Every time you start BTCPayw with this plugin enabled, a timestamp is logged.</p>
<ul class="list-group">>
@foreach (var item in Model.Data)
{
<li class="list-group-item">@item.Id at @item.Timestamp.ToString("F")</li>

View File

@ -6,7 +6,7 @@
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.6.0" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
<PackageReference Include="NBitcoin" Version="6.0.7" />
<PackageReference Include="NBitcoin" Version="5.0.73" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.6.3" />
</ItemGroup>

View File

@ -1266,13 +1266,6 @@
"symbol":null,
"crypto":true
},
{
"name":"Althash",
"code":"HTML",
"divisibility":8,
"symbol":null,
"crypto":true
},
{
"name":"CHC",
"code":"CHC",

View File

@ -28,6 +28,7 @@ namespace BTCPayServer.Services.Rates
}
static readonly Dictionary<string, IFormatProvider> _CurrencyProviders = new Dictionary<string, IFormatProvider>();
public string FormatCurrency(string price, string currency)
{
return FormatCurrency(decimal.Parse(price, CultureInfo.InvariantCulture), currency);
@ -109,8 +110,9 @@ namespace BTCPayServer.Services.Rates
/// </summary>
/// <param name="value">The value</param>
/// <param name="currency">Currency code</param>
/// <param name="threeLetterSuffix">Add three letter suffix (like USD)</param>
/// <returns></returns>
public string DisplayFormatCurrency(decimal value, string currency)
public string DisplayFormatCurrency(decimal value, string currency, bool threeLetterSuffix = true)
{
var provider = GetNumberFormatInfo(currency, true);
var currencyData = GetCurrencyData(currency, true);

File diff suppressed because one or more lines are too long

View File

@ -1,31 +1,61 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Security;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Configuration;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.HostedServices;
using BTCPayServer.Lightning;
using BTCPayServer.Models;
using BTCPayServer.Models.AccountViewModels;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Models.ServerViewModels;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Models.WalletViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Rating;
using BTCPayServer.Security.Bitpay;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Tests.Logging;
using BTCPayServer.U2F.Models;
using BTCPayServer.Validation;
using ExchangeSharp;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitcoin.Payment;
using NBitcoin.Scripting.Parser;
using NBitpayClient;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Schema;
using OpenQA.Selenium;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
using RatesViewModel = BTCPayServer.Models.StoreViewModels.RatesViewModel;
namespace BTCPayServer.Tests
{
@ -42,7 +72,7 @@ namespace BTCPayServer.Tests
[Trait("Integration", "Integration")]
[Trait("Altcoins", "Altcoins")]
[Trait("Lightning", "Lightning")]
public async Task CanSetupWallet()
public async Task CanAddDerivationSchemes()
{
using (var tester = ServerTester.Create())
{
@ -50,14 +80,13 @@ namespace BTCPayServer.Tests
tester.ActivateLightning();
await tester.StartAsync();
var user = tester.NewAccount();
var cryptoCode = "BTC";
await user.GrantAccessAsync(true);
user.RegisterDerivationScheme(cryptoCode);
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
user.RegisterDerivationScheme("LTC");
user.RegisterLightningNode(cryptoCode, LightningConnectionType.CLightning);
var btcNetwork = tester.PayTester.Networks.GetNetwork<BTCPayNetwork>(cryptoCode);
var invoice = await user.BitPay.CreateInvoiceAsync(
new Invoice
user.RegisterLightningNode("BTC", LightningConnectionType.CLightning);
var btcNetwork = tester.PayTester.Networks.GetNetwork<BTCPayNetwork>("BTC");
var invoice = user.BitPay.CreateInvoice(
new Invoice()
{
Price = 1.5m,
Currency = "USD",
@ -70,44 +99,37 @@ namespace BTCPayServer.Tests
Assert.Equal(3, invoice.CryptoInfo.Length);
var controller = user.GetController<StoresController>();
var lightningVm = (LightningNodeViewModel)Assert.IsType<ViewResult>(await controller.SetupLightningNode(user.StoreId, cryptoCode)).Model;
Assert.True(lightningVm.Enabled);
var response = await controller.SetLightningNodeEnabled(user.StoreId, cryptoCode, false);
Assert.IsType<RedirectToActionResult>(response);
var lightningVM =
(LightningNodeViewModel)Assert.IsType<ViewResult>(controller.AddLightningNode(user.StoreId, "BTC"))
.Model;
Assert.True(lightningVM.Enabled);
lightningVM.Enabled = false;
controller.AddLightningNode(user.StoreId, lightningVM, "save", "BTC").GetAwaiter().GetResult();
lightningVM =
(LightningNodeViewModel)Assert.IsType<ViewResult>(controller.AddLightningNode(user.StoreId, "BTC"))
.Model;
Assert.False(lightningVM.Enabled);
// Get enabled state from overview action
StoreViewModel storeModel;
response = await controller.UpdateStore();
storeModel = (StoreViewModel)Assert.IsType<ViewResult>(response).Model;
var lnNode = storeModel.LightningNodes.Find(node => node.CryptoCode == cryptoCode);
Assert.NotNull(lnNode);
Assert.False(lnNode.Enabled);
// Only Enabling/Disabling the payment method must redirect to store page
var derivationVM = (DerivationSchemeViewModel)Assert
.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
Assert.True(derivationVM.Enabled);
derivationVM.Enabled = false;
Assert.IsType<RedirectToActionResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC")
.GetAwaiter().GetResult());
derivationVM = (DerivationSchemeViewModel)Assert
.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
Assert.False(derivationVM.Enabled);
WalletSetupViewModel setupVm;
var storeId = user.StoreId;
response = await controller.GenerateWallet(storeId, cryptoCode, WalletSetupMethod.GenerateOptions, new WalletSetupRequest());
Assert.IsType<ViewResult>(response);
// Clicking next without changing anything should send to the confirmation screen
derivationVM = (DerivationSchemeViewModel)Assert
.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller
.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
Assert.True(derivationVM.Confirmation);
// Get enabled state from overview action
response = await controller.UpdateStore();
storeModel = (StoreViewModel)Assert.IsType<ViewResult>(response).Model;
var derivationScheme = storeModel.DerivationSchemes.Find(scheme => scheme.Crypto == cryptoCode);
Assert.NotNull(derivationScheme);
Assert.True(derivationScheme.Enabled);
// Disable wallet
response = controller.SetWalletEnabled(storeId, cryptoCode, false).GetAwaiter().GetResult();
Assert.IsType<RedirectToActionResult>(response);
response = await controller.UpdateStore();
storeModel = (StoreViewModel)Assert.IsType<ViewResult>(response).Model;
derivationScheme = storeModel.DerivationSchemes.Find(scheme => scheme.Crypto == cryptoCode);
Assert.NotNull(derivationScheme);
Assert.False(derivationScheme.Enabled);
var oldScheme = derivationScheme.Value;
invoice = await user.BitPay.CreateInvoiceAsync(
new Invoice
invoice = user.BitPay.CreateInvoice(
new Invoice()
{
Price = 1.5m,
Currency = "USD",
@ -121,57 +143,76 @@ namespace BTCPayServer.Tests
Assert.Equal("LTC", invoice.CryptoInfo[0].CryptoCode);
// Removing the derivation scheme, should redirect to store page
response = controller.ConfirmDeleteWallet(user.StoreId, cryptoCode).GetAwaiter().GetResult();
Assert.IsType<RedirectToActionResult>(response);
var oldScheme = derivationVM.DerivationScheme;
derivationVM = (DerivationSchemeViewModel)Assert
.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
derivationVM.DerivationScheme = null;
Assert.IsType<RedirectToActionResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC")
.GetAwaiter().GetResult());
// Setting it again should show the confirmation page
response = await controller.UpdateWallet(new WalletSetupViewModel {StoreId = storeId, CryptoCode = cryptoCode, DerivationScheme = oldScheme });
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.True(setupVm.Confirmation);
// Setting it again should redirect to the confirmation page
derivationVM = (DerivationSchemeViewModel)Assert
.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
derivationVM.DerivationScheme = oldScheme;
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller
.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
Assert.True(derivationVM.Confirmation);
// The following part posts a wallet update, confirms it and checks the result
// cobo vault file
//cobo vault file
var content = "{\"ExtPubKey\":\"xpub6CEqRFZ7yZxCFXuEWZBAdnC8bdvu9SRHevaoU2SsW9ZmKhrCShmbpGZWwaR15hdLURf8hg47g4TpPGaqEU8hw5LEJCE35AUhne67XNyFGBk\",\"MasterFingerprint\":\"7a7563b5\",\"DerivationPath\":\"M\\/84'\\/0'\\/0'\",\"CoboVaultFirmwareVersion\":\"1.2.0(BTC-Only)\"}";
response = await controller.UpdateWallet(new WalletSetupViewModel {StoreId = storeId, CryptoCode = cryptoCode, WalletFile = TestUtils.GetFormFile("cobovault.json", content)});
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.True(setupVm.Confirmation);
response = await controller.UpdateWallet(setupVm);
Assert.IsType<RedirectToActionResult>(response);
response = await controller.ModifyWallet(new WalletSetupViewModel { StoreId = storeId, CryptoCode = cryptoCode });
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.Equal("CoboVault", setupVm.Source);
derivationVM = (DerivationSchemeViewModel)Assert
.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
derivationVM.WalletFile = TestUtils.GetFormFile("wallet3.json", content);
derivationVM.Enabled = true;
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller
.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
Assert.True(derivationVM.Confirmation);
Assert.IsType<RedirectToActionResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC")
.GetAwaiter().GetResult());
//wasabi wallet file
content =
"{\r\n \"EncryptedSecret\": \"6PYWBQ1zsukowsnTNA57UUx791aBuJusm7E4egXUmF5WGw3tcdG3cmTL57\",\r\n \"ChainCode\": \"waSIVbn8HaoovoQg/0t8IS1+ZCxGsJRGFT21i06nWnc=\",\r\n \"MasterFingerprint\": \"7a7563b5\",\r\n \"ExtPubKey\": \"xpub6CEqRFZ7yZxCFXuEWZBAdnC8bdvu9SRHevaoU2SsW9ZmKhrCShmbpGZWwaR15hdLURf8hg47g4TpPGaqEU8hw5LEJCE35AUhne67XNyFGBk\",\r\n \"PasswordVerified\": false,\r\n \"MinGapLimit\": 21,\r\n \"AccountKeyPath\": \"84'/0'/0'\",\r\n \"BlockchainState\": {\r\n \"Network\": \"RegTest\",\r\n \"Height\": \"0\"\r\n },\r\n \"HdPubKeys\": []\r\n}";
derivationVM = (DerivationSchemeViewModel)Assert
.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
derivationVM.WalletFile = TestUtils.GetFormFile("wallet4.json", content);
derivationVM.Enabled = true;
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller
.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
Assert.True(derivationVM.Confirmation);
Assert.IsType<RedirectToActionResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC")
.GetAwaiter().GetResult());
// wasabi wallet file
content = "{\r\n \"EncryptedSecret\": \"6PYWBQ1zsukowsnTNA57UUx791aBuJusm7E4egXUmF5WGw3tcdG3cmTL57\",\r\n \"ChainCode\": \"waSIVbn8HaoovoQg/0t8IS1+ZCxGsJRGFT21i06nWnc=\",\r\n \"MasterFingerprint\": \"7a7563b5\",\r\n \"ExtPubKey\": \"xpub6CEqRFZ7yZxCFXuEWZBAdnC8bdvu9SRHevaoU2SsW9ZmKhrCShmbpGZWwaR15hdLURf8hg47g4TpPGaqEU8hw5LEJCE35AUhne67XNyFGBk\",\r\n \"PasswordVerified\": false,\r\n \"MinGapLimit\": 21,\r\n \"AccountKeyPath\": \"84'/0'/0'\",\r\n \"BlockchainState\": {\r\n \"Network\": \"RegTest\",\r\n \"Height\": \"0\"\r\n },\r\n \"HdPubKeys\": []\r\n}";
response = await controller.UpdateWallet(new WalletSetupViewModel {StoreId = storeId, CryptoCode = cryptoCode, WalletFile = TestUtils.GetFormFile("wasabi.json", content)});
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.True(setupVm.Confirmation);
response = await controller.UpdateWallet(setupVm);
Assert.IsType<RedirectToActionResult>(response);
response = await controller.ModifyWallet(new WalletSetupViewModel { StoreId = storeId, CryptoCode = cryptoCode });
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.Equal("WasabiFile", setupVm.Source);
// Can we upload coldcard settings? (Should fail, we are giving a mainnet file to a testnet network)
content = "{\"keystore\": {\"ckcc_xpub\": \"xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw\", \"xpub\": \"ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}";
response = await controller.UpdateWallet(new WalletSetupViewModel {StoreId = storeId, CryptoCode = cryptoCode, WalletFile = TestUtils.GetFormFile("coldcard-ypub.json", content)});
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.False(setupVm.Confirmation); // Should fail, we are giving a mainnet file to a testnet network
derivationVM = (DerivationSchemeViewModel)Assert
.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
content =
"{\"keystore\": {\"ckcc_xpub\": \"xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw\", \"xpub\": \"ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}";
derivationVM.WalletFile = TestUtils.GetFormFile("wallet.json", content);
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller
.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
Assert.False(derivationVM
.Confirmation); // Should fail, we are giving a mainnet file to a testnet network
// And with a good file? (upub)
content = "{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"upub5DBYp1qGgsTrkzCptMGZc2x18pquLwGrBw6nS59T4NViZ4cni1mGowQzziy85K8vzkp1jVtWrSkLhqk9KDfvrGeB369wGNYf39kX8rQfiLn\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}";
response = await controller.UpdateWallet(new WalletSetupViewModel {StoreId = storeId, CryptoCode = cryptoCode, WalletFile = TestUtils.GetFormFile("coldcard-upub.json", content)});
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.True(setupVm.Confirmation);
response = await controller.UpdateWallet(setupVm);
Assert.IsType<RedirectToActionResult>(response);
response = await controller.ModifyWallet(new WalletSetupViewModel { StoreId = storeId, CryptoCode = cryptoCode });
setupVm = (WalletSetupViewModel)Assert.IsType<ViewResult>(response).Model;
Assert.Equal("ElectrumFile", setupVm.Source);
content =
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"upub5DBYp1qGgsTrkzCptMGZc2x18pquLwGrBw6nS59T4NViZ4cni1mGowQzziy85K8vzkp1jVtWrSkLhqk9KDfvrGeB369wGNYf39kX8rQfiLn\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}";
derivationVM = (DerivationSchemeViewModel)Assert
.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
derivationVM.WalletFile = TestUtils.GetFormFile("wallet2.json", content);
derivationVM.Enabled = true;
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller
.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
Assert.True(derivationVM.Confirmation);
Assert.IsType<RedirectToActionResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC")
.GetAwaiter().GetResult());
// Now let's check that no data has been lost in the process
var store = tester.PayTester.StoreRepository.FindStore(storeId).GetAwaiter().GetResult();
var store = tester.PayTester.StoreRepository.FindStore(user.StoreId).GetAwaiter().GetResult();
var onchainBTC = store.GetSupportedPaymentMethods(tester.PayTester.Networks)
#pragma warning disable CS0618 // Type or member is obsolete
.OfType<DerivationSchemeSettings>().First(o => o.PaymentId.IsBTCOnChain);
@ -180,8 +221,8 @@ namespace BTCPayServer.Tests
Assert.Equal(expected.ToJson(), onchainBTC.ToJson());
// Let's check that the root hdkey and account key path are taken into account when making a PSBT
invoice = await user.BitPay.CreateInvoiceAsync(
new Invoice
invoice = user.BitPay.CreateInvoice(
new Invoice()
{
Price = 1.5m,
Currency = "USD",
@ -192,7 +233,7 @@ namespace BTCPayServer.Tests
}, Facade.Merchant);
tester.ExplorerNode.Generate(1);
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo.First(c => c.CryptoCode == cryptoCode).Address,
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo.First(c => c.CryptoCode == "BTC").Address,
tester.ExplorerNode.Network);
tester.ExplorerNode.SendToAddress(invoiceAddress, Money.Coins(1m));
TestUtils.Eventually(() =>
@ -204,9 +245,9 @@ namespace BTCPayServer.Tests
var psbt = wallet.CreatePSBT(btcNetwork, onchainBTC,
new WalletSendModel()
{
Outputs = new List<WalletSendModel.TransactionOutput>
Outputs = new List<WalletSendModel.TransactionOutput>()
{
new WalletSendModel.TransactionOutput
new WalletSendModel.TransactionOutput()
{
Amount = 0.5m,
DestinationAddress = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, btcNetwork.NBitcoinNetwork)
@ -246,7 +287,7 @@ namespace BTCPayServer.Tests
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var user = tester.NewAccount();
user.GrantAccess(true);
user.GrantAccess();
user.RegisterLightningNode("BTC", LightningConnectionType.Charge);
user.RegisterDerivationScheme("BTC");
user.RegisterDerivationScheme("LTC");
@ -254,6 +295,7 @@ namespace BTCPayServer.Tests
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(100, "BTC"));
Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count);
invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(100, "BTC")
{
SupportedTransactionCurrencies = new Dictionary<string, InvoiceSupportedTransactionCurrency>()
@ -312,7 +354,7 @@ namespace BTCPayServer.Tests
var controller = tester.PayTester.GetController<InvoiceController>(null);
var checkout =
(Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id)
(Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, null)
.GetAwaiter().GetResult()).Value;
Assert.Single(checkout.AvailableCryptos);
Assert.Equal("LTC", checkout.CryptoCode);
@ -329,7 +371,7 @@ namespace BTCPayServer.Tests
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal("paid", invoice.Status);
checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id)
checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, null)
.GetAwaiter().GetResult()).Value;
Assert.Equal("paid", checkout.Status);
});
@ -834,7 +876,7 @@ normal:
var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[]
{
new BitcoinLikePaymentHandler(null, networkProvider, null, null, null),
new LightningLikePaymentHandler(null, null, networkProvider, null, null, null),
new LightningLikePaymentHandler(null, null, networkProvider, null, null),
});
var networkBTC = networkProvider.GetNetwork("BTC");
var networkLTC = networkProvider.GetNetwork("LTC");
@ -954,6 +996,28 @@ normal:
result = testnetParser.Parse(tpub);
Assert.Equal(tpub, result.ToString());
testnetParser.HintScriptPubKey = BitcoinAddress
.Create("tb1q4s33amqm8l7a07zdxcunqnn3gcsjcfz3xc573l", testnetParser.Network).ScriptPubKey;
result = testnetParser.Parse(tpub);
Assert.Equal(tpub, result.ToString());
testnetParser.HintScriptPubKey = BitcoinAddress
.Create("2N2humNio3YTApSfY6VztQ9hQwDnhDvaqFQ", testnetParser.Network).ScriptPubKey;
result = testnetParser.Parse(tpub);
Assert.Equal($"{tpub}-[p2sh]", result.ToString());
testnetParser.HintScriptPubKey = BitcoinAddress
.Create("mwD8bHS65cdgUf6rZUUSoVhi3wNQFu1Nfi", testnetParser.Network).ScriptPubKey;
result = testnetParser.Parse(tpub);
Assert.Equal($"{tpub}-[legacy]", result.ToString());
testnetParser.HintScriptPubKey = BitcoinAddress
.Create("2N2humNio3YTApSfY6VztQ9hQwDnhDvaqFQ", testnetParser.Network).ScriptPubKey;
result = testnetParser.Parse($"{tpub}-[legacy]");
Assert.Equal($"{tpub}-[p2sh]", result.ToString());
result = testnetParser.Parse(tpub);
Assert.Equal($"{tpub}-[p2sh]", result.ToString());
var regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork<BTCPayNetwork>("BTC"));
var parsed =

View File

@ -73,14 +73,14 @@ namespace BTCPayServer.Tests
var seed = new Mnemonic(Wordlist.English);
s.Driver.FindElement(By.Id("ModifyETH")).Click();
s.Driver.FindElement(By.Id("Seed")).SendKeys(seed.ToString());
s.Driver.SetCheckbox(By.Id("StoreSeed"), true);
s.Driver.SetCheckbox(By.Id("Enabled"), true);
s.SetCheckbox(s.Driver.FindElement(By.Id("StoreSeed")), true);
s.SetCheckbox(s.Driver.FindElement(By.Id("Enabled")), true);
s.Driver.FindElement(By.Id("SaveButton")).Click();
s.FindAlertMessage();
s.Driver.FindElement(By.Id("ModifyUSDT20")).Click();
s.Driver.FindElement(By.Id("Seed")).SendKeys(seed.ToString());
s.Driver.SetCheckbox(By.Id("StoreSeed"), true);
s.Driver.SetCheckbox(By.Id("Enabled"), true);
s.SetCheckbox(s.Driver.FindElement(By.Id("StoreSeed")), true);
s.SetCheckbox(s.Driver.FindElement(By.Id("Enabled")), true);
s.Driver.FindElement(By.Id("SaveButton")).Click();
s.FindAlertMessage();

View File

@ -61,9 +61,9 @@ namespace BTCPayServer.Tests
Assert.Contains("btcpay.server.canmodifyserversettings", s.Driver.PageSource);
//server management should show now
s.Driver.SetCheckbox(By.Id("btcpay.server.canmodifyserversettings"), true);
s.Driver.SetCheckbox(By.Id("btcpay.store.canmodifystoresettings"), true);
s.Driver.SetCheckbox(By.Id("btcpay.user.canviewprofile"), true);
s.SetCheckbox(s, "btcpay.server.canmodifyserversettings", true);
s.SetCheckbox(s, "btcpay.store.canmodifystoresettings", true);
s.SetCheckbox(s, "btcpay.user.canviewprofile", true);
s.Driver.FindElement(By.Id("Generate")).Click();
var superApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
@ -72,7 +72,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("AddApiKey")).Click();
s.Driver.SetCheckbox(By.Id("btcpay.server.canmodifyserversettings"), true);
s.SetCheckbox(s, "btcpay.server.canmodifyserversettings", true);
s.Driver.FindElement(By.Id("Generate")).Click();
var serverOnlyApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
await TestApiAgainstAccessToken(serverOnlyApiKey, tester, user,
@ -80,7 +80,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("AddApiKey")).Click();
s.Driver.SetCheckbox(By.Id("btcpay.store.canmodifystoresettings"), true);
s.SetCheckbox(s, "btcpay.store.canmodifystoresettings", true);
s.Driver.FindElement(By.Id("Generate")).Click();
var allStoreOnlyApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
await TestApiAgainstAccessToken(allStoreOnlyApiKey, tester, user,
@ -89,12 +89,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("AddApiKey")).Click();
s.Driver.FindElement(By.CssSelector("button[value='btcpay.store.canmodifystoresettings:change-store-mode']")).Click();
//there should be a store already by default in the dropdown
var src = s.Driver.PageSource;
var getPermissionValueIndex =
s.Driver.FindElement(By.CssSelector("input[value='btcpay.store.canmodifystoresettings']"))
.GetAttribute("name")
.Replace(".Permission", ".SpecificStores[0]");
var dropdown = s.Driver.FindElement(By.Name(getPermissionValueIndex));
var dropdown = s.Driver.FindElement(By.Name("PermissionValues[4].SpecificStores[0]"));
var option = dropdown.FindElement(By.TagName("option"));
var storeId = option.GetAttribute("value");
option.Click();
@ -154,7 +149,7 @@ namespace BTCPayServer.Tests
Assert.Equal("checkbox", s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("type").ToLowerInvariant());
Assert.Equal("true", s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("value").ToLowerInvariant());
s.Driver.SetCheckbox(By.Id("btcpay.server.canmodifyserversettings"), false);
s.SetCheckbox(s, "btcpay.server.canmodifyserversettings", false);
Assert.Contains("change-store-mode", s.Driver.PageSource);
s.Driver.FindElement(By.Id("consent-yes")).Click();
Assert.Equal(callbackUrl, s.Driver.Url);

View File

@ -23,7 +23,7 @@
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.13" />
<PackageReference Include="Selenium.Support" Version="3.141.0" />
<PackageReference Include="Selenium.WebDriver" Version="3.141.0" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="90.0.4430.2400" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="88.0.4324.9600" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.2">
<PrivateAssets>all</PrivateAssets>

View File

@ -112,7 +112,7 @@ namespace BTCPayServer.Tests
if (UseLightning)
{
config.AppendLine($"btc.lightning={IntegratedLightning}");
config.AppendLine($"btc.lightning={IntegratedLightning.AbsoluteUri}");
var localLndBackupFile = Path.Combine(_Directory, "walletunlock.json");
File.Copy(TestUtils.GetTestDataFullPath("LndSeedBackup/walletunlock.json"), localLndBackupFile, true);
config.AppendLine($"btc.external.lndseedbackup={localLndBackupFile}");
@ -167,7 +167,6 @@ namespace BTCPayServer.Tests
l.SetMinimumLevel(LogLevel.Information)
.AddFilter("Microsoft", LogLevel.Error)
.AddFilter("Hangfire", LogLevel.Error)
.AddFilter("Fido2NetLib.DistributedCacheMetadataService", LogLevel.Error)
.AddProvider(Logs.LogProvider);
});
})
@ -270,7 +269,7 @@ namespace BTCPayServer.Tests
public InvoiceRepository InvoiceRepository { get; private set; }
public StoreRepository StoreRepository { get; private set; }
public BTCPayNetworkProvider Networks { get; private set; }
public string IntegratedLightning { get; internal set; }
public Uri IntegratedLightning { get; internal set; }
public bool InContainer { get; internal set; }
public T GetService<T>()

View File

@ -1,13 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Payments;
using BTCPayServer.Tests.Logging;
using BTCPayServer.Views.Stores;
using NBitcoin;
using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;
using Xunit;
using Xunit.Abstractions;
@ -112,11 +109,11 @@ namespace BTCPayServer.Tests
s.Server.ActivateLightning();
await s.StartAsync();
s.GoToRegister();
s.RegisterNewUser(true);
s.RegisterNewUser();
var store = s.CreateNewStore();
s.AddLightningNode();
s.AddInternalLightningNode("BTC");
s.GoToStore(store.storeId, StoreNavPages.Checkout);
s.Driver.SetCheckbox(By.Id("LightningAmountInSatoshi"), true);
s.SetCheckbox(s, "LightningAmountInSatoshi", true);
var command = s.Driver.FindElement(By.Name("command"));
command.Click();

View File

@ -0,0 +1,91 @@
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments.CoinSwitch;
using BTCPayServer.Tests.Logging;
using Microsoft.AspNetCore.Mvc;
using Xunit;
using Xunit.Abstractions;
namespace BTCPayServer.Tests
{
public class CoinSwitchTests
{
public CoinSwitchTests(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) { Name = "Tests" };
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanSetCoinSwitchPaymentMethod()
{
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
var controller = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
var storeBlob = controller.CurrentStore.GetStoreBlob();
Assert.Null(storeBlob.CoinSwitchSettings);
var updateModel = new UpdateCoinSwitchSettingsViewModel()
{
MerchantId = "aaa",
};
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
await controller.UpdateCoinSwitchSettings(user.StoreId, updateModel, "save")).ActionName);
var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
storeBlob = controller.CurrentStore.GetStoreBlob();
Assert.NotNull(storeBlob.CoinSwitchSettings);
Assert.NotNull(storeBlob.CoinSwitchSettings);
Assert.IsType<CoinSwitchSettings>(storeBlob.CoinSwitchSettings);
Assert.Equal(storeBlob.CoinSwitchSettings.MerchantId,
updateModel.MerchantId);
}
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanToggleCoinSwitchPaymentMethod()
{
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
var controller = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
var updateModel = new UpdateCoinSwitchSettingsViewModel()
{
MerchantId = "aaa",
Enabled = true
};
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
await controller.UpdateCoinSwitchSettings(user.StoreId, updateModel, "save")).ActionName);
var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
Assert.True(store.GetStoreBlob().CoinSwitchSettings.Enabled);
updateModel.Enabled = false;
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
await controller.UpdateCoinSwitchSettings(user.StoreId, updateModel, "save")).ActionName);
store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
Assert.False(store.GetStoreBlob().CoinSwitchSettings.Enabled);
}
}
}
}

View File

@ -23,7 +23,7 @@ namespace BTCPayServer.Tests
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanCreateAndDeleteCrowdfundApp()
{
@ -63,7 +63,7 @@ namespace BTCPayServer.Tests
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanContributeOnlyWhenAllowed()
{
@ -155,7 +155,7 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = LongRunningTestTimeout)]
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanComputeCrowdfundModel()
{
@ -165,7 +165,7 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
await user.ModifyStore(s => s.NetworkFeeMode = NetworkFeeMode.Never);
user.ModifyStore(s => s.NetworkFeeMode = NetworkFeeMode.Never);
var apps = user.GetController<AppsController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
vm.Name = "test";

View File

@ -7,8 +7,6 @@ using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using OpenQA.Selenium;
using OpenQA.Selenium.Support.Extensions;
using OpenQA.Selenium.Support.UI;
using Xunit;
namespace BTCPayServer.Tests
@ -28,12 +26,46 @@ namespace BTCPayServer.Tests
public static void AssertNoError(this IWebDriver driver)
{
Assert.NotEmpty(driver.FindElements(By.ClassName("navbar-brand")));
if (!driver.PageSource.Contains("alert-danger")) return;
foreach (var dangerAlert in driver.FindElements(By.ClassName("alert-danger")))
Assert.False(dangerAlert.Displayed, $"No alert should be displayed, but found this on {driver.Url}: {dangerAlert.Text}");
try
{
Assert.NotEmpty(driver.FindElements(By.ClassName("navbar-brand")));
if (driver.PageSource.Contains("alert-danger"))
{
foreach (var dangerAlert in driver.FindElements(By.ClassName("alert-danger")))
Assert.False(dangerAlert.Displayed, "No alert should be displayed");
}
}
catch
{
StringBuilder builder = new StringBuilder();
builder.AppendLine();
foreach (var logKind in new[] { LogType.Browser, LogType.Client, LogType.Driver, LogType.Server })
{
try
{
var logs = driver.Manage().Logs.GetLog(logKind);
builder.AppendLine($"Selenium [{logKind}]:");
foreach (var entry in logs)
{
builder.AppendLine($"[{entry.Level}]: {entry.Message}");
}
}
catch
{
// ignored
}
builder.AppendLine("---------");
}
Logs.Tester.LogInformation(builder.ToString());
builder = new StringBuilder();
builder.AppendLine("Selenium [Sources]:");
builder.AppendLine(driver.PageSource);
builder.AppendLine("---------");
Logs.Tester.LogInformation(builder.ToString());
throw;
}
}
public static T AssertViewModel<T>(this IActionResult result)
{
Assert.NotNull(result);
@ -73,56 +105,5 @@ namespace BTCPayServer.Tests
}
Assert.False(true, "Elements was found");
}
public static void UntilJsIsReady(this WebDriverWait wait)
{
wait.Until(d=>((IJavaScriptExecutor)d).ExecuteScript("return document.readyState").Equals("complete"));
wait.Until(d=>((IJavaScriptExecutor)d).ExecuteScript("return typeof(jQuery) === 'undefined' || jQuery.active === 0").Equals(true));
}
// Open collapse via JS, because if we click the link it triggers the toggle animation.
// This leads to Selenium trying to click the button while it is moving resulting in an error.
public static void ToggleCollapse(this IWebDriver driver, string collapseId)
{
driver.ExecuteJavaScript($"document.getElementById('{collapseId}').classList.add('show')");
}
public static IWebElement WaitForElement(this IWebDriver driver, By selector)
{
var wait = new WebDriverWait(driver, SeleniumTester.ImplicitWait);
wait.UntilJsIsReady();
var el = driver.FindElement(selector);
wait.Until(d => el.Displayed);
return el;
}
public static void WaitForAndClick(this IWebDriver driver, By selector)
{
var wait = new WebDriverWait(driver, SeleniumTester.ImplicitWait);
wait.UntilJsIsReady();
var el = driver.FindElement(selector);
wait.Until(d => el.Displayed && el.Enabled);
el.Click();
wait.UntilJsIsReady();
}
public static void SetCheckbox(this IWebDriver driver, By selector, bool value)
{
var element = driver.FindElement(selector);
if ((value && !element.Selected) || (!value && element.Selected))
{
driver.WaitForAndClick(selector);
}
if (value != element.Selected)
{
Logs.Tester.LogInformation("SetCheckbox recursion, trying to click again");
driver.SetCheckbox(selector, value);
}
}
}
}

View File

@ -1,11 +1,9 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Controllers;
@ -13,7 +11,6 @@ using BTCPayServer.Events;
using BTCPayServer.JsonConverters;
using BTCPayServer.Lightning;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Payments;
using BTCPayServer.Services;
using BTCPayServer.Services.Notifications;
using BTCPayServer.Services.Notifications.Blobs;
@ -21,6 +18,7 @@ using BTCPayServer.Tests.Logging;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using NBitcoin.OpenAsset;
using NBitpayClient;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@ -40,23 +38,6 @@ namespace BTCPayServer.Tests
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task LocalClientTests()
{
using var tester = ServerTester.Create();
await tester.StartAsync();
var user = tester.NewAccount();
await user.GrantAccessAsync();
await user.MakeAdmin();
var factory = tester.PayTester.GetService<IBTCPayServerClientFactory>();
Assert.NotNull(factory);
var client = await factory.Create(user.UserId);
var u = await client.GetCurrentUser();
var s = await client.GetStores();
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task ApiKeysControllerTests()
@ -86,27 +67,6 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanUseMiscAPIs()
{
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
var acc = tester.NewAccount();
await acc.GrantAccessAsync();
var unrestricted = await acc.CreateClient();
var langs = await unrestricted.GetAvailableLanguages();
Assert.NotEmpty(langs);
Assert.NotNull(langs[0].Code);
Assert.NotNull(langs[0].DisplayName);
var perms = await unrestricted.GetPermissionMetadata();
Assert.NotEmpty(perms);
var p = perms.First(p => p.PermissionName == "unrestricted");
Assert.True(p.SubPermissions.Count > 6);
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
@ -165,45 +125,6 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanDeleteUsersViaApi()
{
using var tester = ServerTester.Create(newDb: true);
await tester.StartAsync();
var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri);
// Should not be authorized to perform this action
await AssertHttpError(401,
async () => await unauthClient.DeleteUser("lol user id"));
var user = tester.NewAccount();
await user.GrantAccessAsync();
await user.MakeAdmin();
var adminClient = await user.CreateClient(Policies.Unrestricted);
//can't delete if the only admin
await AssertHttpError(403,
async () => await adminClient.DeleteCurrentUser());
// Should 404 if user doesn't exist
await AssertHttpError(404,
async () => await adminClient.DeleteUser("lol user id"));
user = tester.NewAccount();
await user.GrantAccessAsync();
var badClient = await user.CreateClient(Policies.CanCreateInvoice);
await AssertHttpError(403,
async () => await badClient.DeleteCurrentUser());
var goodClient = await user.CreateClient(Policies.CanDeleteUser, Policies.CanViewProfile);
await goodClient.DeleteCurrentUser();
await AssertHttpError(404,
async () => await adminClient.DeleteUser(user.UserId));
tester.Stores.Remove(user.StoreId);
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanCreateUsersViaAPI()
@ -548,30 +469,6 @@ namespace BTCPayServer.Tests
{
Revision = payout.Revision
}));
// Create one pull payment with an amount of 9 decimals
var test3 = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest()
{
Name = "Test 2",
Amount = 12.303228134m,
Currency = "BTC",
PaymentMethods = new[] { "BTC" }
});
destination = (await tester.ExplorerNode.GetNewAddressAsync()).ToString();
payout = await unauthenticated.CreatePayout(test3.Id, new CreatePayoutRequest()
{
Destination = destination,
PaymentMethod = "BTC"
});
payout = await client.ApprovePayout(storeId, payout.Id, new ApprovePayoutRequest());
// The payout should round the value of the payment down to the network of the payment method
Assert.Equal(12.30322814m, payout.PaymentMethodAmount);
Assert.Equal(12.303228134m, payout.Amount);
await client.MarkPayoutPaid(storeId, payout.Id);
payout = (await client.GetPayouts(payout.PullPaymentId)).First(data => data.Id == payout.Id);
Assert.Equal(PayoutState.Completed, payout.State);
await AssertAPIError("invalid-state", async () => await client.MarkPayoutPaid(storeId, payout.Id));
}
}
@ -643,7 +540,7 @@ namespace BTCPayServer.Tests
}
}
private async Task<GreenFieldValidationException> AssertValidationError(string[] fields, Func<Task> act)
private async Task AssertValidationError(string[] fields, Func<Task> act)
{
var remainingFields = fields.ToHashSet();
var ex = await Assert.ThrowsAsync<GreenFieldValidationException>(act);
@ -653,7 +550,6 @@ namespace BTCPayServer.Tests
remainingFields.Remove(field);
}
Assert.Empty(remainingFields);
return ex;
}
private async Task AssertHttpError(int code, Func<Task> act)
@ -788,7 +684,7 @@ namespace BTCPayServer.Tests
Assert.NotNull(newDelivery);
Assert.Equal(404, newDelivery.HttpCode);
var req = await clientProfile.GetWebhookDeliveryRequest(user.StoreId, hook.Id, newDeliveryId);
Assert.Equal(delivery.Id, req.OriginalDeliveryId);
Assert.Equal(delivery.Id, req.OrignalDeliveryId);
Assert.True(req.IsRedelivery);
Assert.Equal(WebhookDeliveryStatus.HttpError, newDelivery.Status);
});
@ -1052,19 +948,8 @@ namespace BTCPayServer.Tests
});
await user.RegisterDerivationSchemeAsync("BTC");
var newInvoice = await client.CreateInvoice(user.StoreId,
new CreateInvoiceRequest()
{
Currency = "USD",
Amount = 1,
Metadata = JObject.Parse("{\"itemCode\": \"testitem\", \"orderId\": \"testOrder\"}"),
Checkout = new CreateInvoiceRequest.CheckoutOptions()
{
RedirectAutomatically = true
},
AdditionalSearchTerms = new string[] { "Banana" }
});
Assert.True(newInvoice.Checkout.RedirectAutomatically);
Assert.Equal(user.StoreId, newInvoice.StoreId);
new CreateInvoiceRequest() { Currency = "USD", Amount = 1, Metadata = JObject.Parse("{\"itemCode\": \"testitem\"}") });
//list
var invoices = await viewOnly.GetInvoices(user.StoreId);
@ -1072,76 +957,6 @@ namespace BTCPayServer.Tests
Assert.Single(invoices);
Assert.Equal(newInvoice.Id, invoices.First().Id);
invoices = await viewOnly.GetInvoices(user.StoreId, textSearch: "Banana");
Assert.NotNull(invoices);
Assert.Single(invoices);
Assert.Equal(newInvoice.Id, invoices.First().Id);
invoices = await viewOnly.GetInvoices(user.StoreId, textSearch: "apples");
Assert.NotNull(invoices);
Assert.Empty(invoices);
//list Filtered
var invoicesFiltered = await viewOnly.GetInvoices(user.StoreId,
orderId: null, status: null, startDate: DateTimeOffset.Now.AddHours(-1),
endDate: DateTimeOffset.Now.AddHours(1));
Assert.NotNull(invoicesFiltered);
Assert.Single(invoicesFiltered);
Assert.Equal(newInvoice.Id, invoicesFiltered.First().Id);
Assert.NotNull(invoicesFiltered);
Assert.Single(invoicesFiltered);
Assert.Equal(newInvoice.Id, invoicesFiltered.First().Id);
//list Yesterday
var invoicesYesterday = await viewOnly.GetInvoices(user.StoreId,
orderId: null, status: null, startDate: DateTimeOffset.Now.AddDays(-2),
endDate: DateTimeOffset.Now.AddDays(-1));
Assert.NotNull(invoicesYesterday);
Assert.Empty(invoicesYesterday);
// Error, startDate and endDate inverted
await AssertValidationError(new[] { "startDate", "endDate" },
() => viewOnly.GetInvoices(user.StoreId,
orderId: null, status: null, startDate: DateTimeOffset.Now.AddDays(-1),
endDate: DateTimeOffset.Now.AddDays(-2)));
await AssertValidationError(new[] { "startDate" },
() => viewOnly.SendHttpRequest<Client.Models.InvoiceData[]>($"api/v1/stores/{user.StoreId}/invoices", new Dictionary<string, object>()
{
{ "startDate", "blah" }
}));
//list Existing OrderId
var invoicesExistingOrderId =
await viewOnly.GetInvoices(user.StoreId, orderId: new []{newInvoice.Metadata["orderId"].ToString()});
Assert.NotNull(invoicesExistingOrderId);
Assert.Single(invoicesFiltered);
Assert.Equal(newInvoice.Id, invoicesFiltered.First().Id);
//list NonExisting OrderId
var invoicesNonExistingOrderId =
await viewOnly.GetInvoices(user.StoreId, orderId: new []{"NonExistingOrderId"});
Assert.NotNull(invoicesNonExistingOrderId);
Assert.Empty(invoicesNonExistingOrderId);
//list Existing Status
var invoicesExistingStatus =
await viewOnly.GetInvoices(user.StoreId, status: new []{newInvoice.Status});
Assert.NotNull(invoicesExistingStatus);
Assert.Single(invoicesExistingStatus);
Assert.Equal(newInvoice.Id, invoicesExistingStatus.First().Id);
//list NonExisting Status
var invoicesNonExistingStatus = await viewOnly.GetInvoices(user.StoreId,
status: new []{BTCPayServer.Client.Models.InvoiceStatus.Invalid});
Assert.NotNull(invoicesNonExistingStatus);
Assert.Empty(invoicesNonExistingStatus);
//get
var invoice = await viewOnly.GetInvoice(user.StoreId, newInvoice.Id);
Assert.Equal(newInvoice.Metadata, invoice.Metadata);
@ -1153,41 +968,38 @@ namespace BTCPayServer.Tests
//update
newInvoice = await client.CreateInvoice(user.StoreId,
new CreateInvoiceRequest() { Currency = "USD", Amount = 1 });
await client.MarkInvoiceStatus(user.StoreId, newInvoice.Id, new MarkInvoiceStatusRequest()
invoice = await viewOnly.GetInvoice(user.StoreId, newInvoice.Id);
await AssertValidationError(new[] { nameof(MarkInvoiceStatusRequest.Status) }, async () =>
{
Status = InvoiceStatus.Settled
});
newInvoice = await client.CreateInvoice(user.StoreId,
new CreateInvoiceRequest() { Currency = "USD", Amount = 1 });
await client.MarkInvoiceStatus(user.StoreId, newInvoice.Id, new MarkInvoiceStatusRequest()
{
Status = InvoiceStatus.Invalid
await client.MarkInvoiceStatus(user.StoreId, invoice.Id, new MarkInvoiceStatusRequest()
{
Status = InvoiceStatus.Settled
});
});
await AssertHttpError(403, async () =>
{
await viewOnly.UpdateInvoice(user.StoreId, invoice.Id,
await viewOnly.UpdateInvoice(user.StoreId, newInvoice.Id,
new UpdateInvoiceRequest()
{
Metadata = JObject.Parse("{\"itemCode\": \"updated\", newstuff: [1,2,3,4,5]}")
});
});
invoice = await client.UpdateInvoice(user.StoreId, invoice.Id,
invoice = await client.UpdateInvoice(user.StoreId, newInvoice.Id,
new UpdateInvoiceRequest()
{
Metadata = JObject.Parse("{\"itemCode\": \"updated\", newstuff: [1,2,3,4,5]}")
});
Assert.Equal("updated", invoice.Metadata["itemCode"].Value<string>());
Assert.Equal(15, ((JArray)invoice.Metadata["newstuff"]).Values<int>().Sum());
Assert.Equal("updated",invoice.Metadata["itemCode"].Value<string>());
Assert.Equal(15,((JArray) invoice.Metadata["newstuff"]).Values<int>().Sum());
//also test the the metadata actually got saved
invoice = await client.GetInvoice(user.StoreId, invoice.Id);
Assert.Equal("updated", invoice.Metadata["itemCode"].Value<string>());
Assert.Equal(15, ((JArray)invoice.Metadata["newstuff"]).Values<int>().Sum());
invoice = await client.GetInvoice(user.StoreId, newInvoice.Id);
Assert.Equal("updated",invoice.Metadata["itemCode"].Value<string>());
Assert.Equal(15,((JArray) invoice.Metadata["newstuff"]).Values<int>().Sum());
//archive
await AssertHttpError(403, async () =>
{
@ -1257,38 +1069,20 @@ namespace BTCPayServer.Tests
var langs = tester.PayTester.GetService<LanguageService>();
foreach (var match in new[] { "it", "it-IT", "it-LOL" })
{
Assert.Equal("it-IT", langs.FindLanguage(match).Code);
Assert.Equal("it-IT", langs.FindBestMatch(match).Code);
}
foreach (var match in new[] { "pt-BR" })
{
Assert.Equal("pt-BR", langs.FindLanguage(match).Code);
Assert.Equal("pt-BR", langs.FindBestMatch(match).Code);
}
foreach (var match in new[] { "en", "en-US" })
{
Assert.Equal("en", langs.FindLanguage(match).Code);
Assert.Equal("en", langs.FindBestMatch(match).Code);
}
foreach (var match in new[] { "pt", "pt-pt", "pt-PT" })
{
Assert.Equal("pt-PT", langs.FindLanguage(match).Code);
Assert.Equal("pt-PT", langs.FindBestMatch(match).Code);
}
//payment method activation tests
var store = await client.GetStore(user.StoreId);
Assert.False(store.LazyPaymentMethods);
store.LazyPaymentMethods = true;
store = await client.UpdateStore(store.Id,
JObject.FromObject(store).ToObject<UpdateStoreRequest>());
Assert.True(store.LazyPaymentMethods);
invoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest() { Amount = 1, Currency = "USD" });
paymentMethods = await client.GetInvoicePaymentMethods(store.Id, invoice.Id);
Assert.Single(paymentMethods);
Assert.False(paymentMethods.First().Activated);
await client.ActivateInvoicePaymentMethod(user.StoreId, invoice.Id,
paymentMethods.First().PaymentMethod);
paymentMethods = await client.GetInvoicePaymentMethods(store.Id, invoice.Id);
Assert.Single(paymentMethods);
Assert.True(paymentMethods.First().Activated);
}
}
@ -1311,6 +1105,7 @@ namespace BTCPayServer.Tests
merchant.RegisterLightningNode("BTC", LightningConnectionType.LndREST);
var merchantClient = await merchant.CreateClient($"{Policies.CanUseLightningNodeInStore}:{merchant.StoreId}");
var merchantInvoice = await merchantClient.CreateLightningInvoice(merchant.StoreId, "BTC", new CreateLightningInvoiceRequest(LightMoney.Satoshis(1_000), "hey", TimeSpan.FromSeconds(60)));
tester.PayTester.GetService<BTCPayServerEnvironment>().DevelopmentOverride = false;
// The default client is using charge, so we should not be able to query channels
var client = await user.CreateClient(Policies.CanUseInternalLightningNode);
@ -1380,7 +1175,8 @@ namespace BTCPayServer.Tests
Assert.NotEqual(0, info.BlockHeight);
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task NotificationAPITests()
@ -1419,7 +1215,7 @@ namespace BTCPayServer.Tests
Assert.Empty(await viewOnlyClient.GetNotifications(true));
Assert.Empty(await viewOnlyClient.GetNotifications(false));
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task OnChainPaymentMethodAPITests()
@ -1427,448 +1223,58 @@ namespace BTCPayServer.Tests
using var tester = ServerTester.Create();
await tester.StartAsync();
var user = tester.NewAccount();
var user2 = tester.NewAccount();
await user.GrantAccessAsync(true);
await user2.GrantAccessAsync(false);
var client = await user.CreateClient(Policies.CanModifyStoreSettings);
var client2 = await user2.CreateClient(Policies.CanModifyStoreSettings);
var viewOnlyClient = await user.CreateClient(Policies.CanViewStoreSettings);
var store = await client.CreateStore(new CreateStoreRequest() { Name = "test store" });
var store = await client.CreateStore(new CreateStoreRequest() {Name = "test store"});
Assert.Empty(await client.GetStoreOnChainPaymentMethods(store.Id));
await AssertHttpError(403, async () =>
{
await viewOnlyClient.UpdateStoreOnChainPaymentMethod(store.Id, "BTC", new OnChainPaymentMethodData() { });
});
});
var xpriv = new Mnemonic("all all all all all all all all all all all all").DeriveExtKey()
.Derive(KeyPath.Parse("m/84'/1'/0'"));
.Derive(KeyPath.Parse("m/84'/0'/0'"));
var xpub = xpriv.Neuter().ToString(Network.RegTest);
var firstAddress = xpriv.Derive(KeyPath.Parse("0/0")).Neuter().GetPublicKey().GetAddress(ScriptPubKeyType.Segwit, Network.RegTest).ToString();
await AssertHttpError(404, async () =>
{
await client.PreviewStoreOnChainPaymentMethodAddresses(store.Id, "BTC");
});
Assert.Equal(firstAddress, (await viewOnlyClient.PreviewProposedStoreOnChainPaymentMethodAddresses(store.Id, "BTC",
new OnChainPaymentMethodData() { Enabled = true, DerivationScheme = xpub })).Addresses.First().Address);
new OnChainPaymentMethodData() {Enabled = true, DerivationScheme = xpub})).Addresses.First().Address);
var method = await client.UpdateStoreOnChainPaymentMethod(store.Id, "BTC",
new OnChainPaymentMethodData() { Enabled = true, DerivationScheme = xpub });
Assert.Equal(xpub, method.DerivationScheme);
new OnChainPaymentMethodData() { Enabled = true, DerivationScheme = xpub});
Assert.Equal(xpub,method.DerivationScheme);
method = await client.UpdateStoreOnChainPaymentMethod(store.Id, "BTC",
new OnChainPaymentMethodData() { Enabled = true, DerivationScheme = xpub, Label = "lol", AccountKeyPath = RootedKeyPath.Parse("01020304/1/2/3") });
new OnChainPaymentMethodData() { Enabled = true, DerivationScheme = xpub, Label = "lol", AccountKeyPath = RootedKeyPath.Parse("01020304/1/2/3") });
method = await client.GetStoreOnChainPaymentMethod(store.Id, "BTC");
Assert.Equal("lol", method.Label);
Assert.Equal(RootedKeyPath.Parse("01020304/1/2/3"), method.AccountKeyPath);
Assert.Equal(xpub, method.DerivationScheme);
Assert.Equal(xpub,method.DerivationScheme);
Assert.Equal(firstAddress, (await viewOnlyClient.PreviewStoreOnChainPaymentMethodAddresses(store.Id, "BTC")).Addresses.First().Address);
await AssertHttpError(403, async () =>
{
await viewOnlyClient.RemoveStoreOnChainPaymentMethod(store.Id, "BTC");
});
await client.RemoveStoreOnChainPaymentMethod(store.Id, "BTC");
await AssertHttpError(404, async () =>
{
await client.GetStoreOnChainPaymentMethod(store.Id, "BTC");
});
await AssertHttpError(403, async () =>
{
await viewOnlyClient.GenerateOnChainWallet(store.Id, "BTC", new GenerateOnChainWalletRequest() { });
});
await AssertValidationError(new []{"SavePrivateKeys", "ImportKeysToRPC"}, async () =>
{
await client2.GenerateOnChainWallet(user2.StoreId, "BTC", new GenerateOnChainWalletRequest()
{
SavePrivateKeys = true,
ImportKeysToRPC = true
});
});
var allMnemonic = new Mnemonic("all all all all all all all all all all all all");
await client.RemoveStoreOnChainPaymentMethod(store.Id, "BTC");
var generateResponse = await client.GenerateOnChainWallet(store.Id, "BTC",
new GenerateOnChainWalletRequest() {ExistingMnemonic = allMnemonic,});
Assert.Equal(generateResponse.Mnemonic.ToString(), allMnemonic.ToString());
Assert.Equal(generateResponse.DerivationScheme, xpub);
await AssertAPIError("already-configured", async () =>
{
await client.GenerateOnChainWallet(store.Id, "BTC",
new GenerateOnChainWalletRequest() {ExistingMnemonic = allMnemonic,});
});
await client.RemoveStoreOnChainPaymentMethod(store.Id, "BTC");
generateResponse = await client.GenerateOnChainWallet(store.Id, "BTC",
new GenerateOnChainWalletRequest() {});
Assert.NotEqual(generateResponse.Mnemonic.ToString(), allMnemonic.ToString());
Assert.Equal(generateResponse.Mnemonic.DeriveExtKey().Derive(KeyPath.Parse("m/84'/1'/0'")).Neuter().ToString(Network.RegTest), generateResponse.DerivationScheme);
await client.RemoveStoreOnChainPaymentMethod(store.Id, "BTC");
generateResponse = await client.GenerateOnChainWallet(store.Id, "BTC",
new GenerateOnChainWalletRequest() { ExistingMnemonic = allMnemonic, AccountNumber = 1});
Assert.Equal(generateResponse.Mnemonic.ToString(), allMnemonic.ToString());
Assert.Equal(new Mnemonic("all all all all all all all all all all all all").DeriveExtKey()
.Derive(KeyPath.Parse("m/84'/1'/1'")).Neuter().ToString(Network.RegTest), generateResponse.DerivationScheme);
await client.RemoveStoreOnChainPaymentMethod(store.Id, "BTC");
generateResponse = await client.GenerateOnChainWallet(store.Id, "BTC",
new GenerateOnChainWalletRequest() { WordList = Wordlist.Japanese, WordCount = WordCount.TwentyFour});
Assert.Equal(24,generateResponse.Mnemonic.Words.Length);
Assert.Equal(Wordlist.Japanese,generateResponse.Mnemonic.WordList);
await client.RemoveStoreOnChainPaymentMethod(store.Id, "BTC");
await AssertHttpError(404, async () =>
{
await client.GetStoreOnChainPaymentMethod(store.Id, "BTC");
});
}
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Lightning", "Lightning")]
[Trait("Integration", "Integration")]
public async Task LightningNetworkPaymentMethodAPITests()
{
using var tester = ServerTester.Create();
tester.ActivateLightning();
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var admin = tester.NewAccount();
await admin.GrantAccessAsync(true);
var admin2 = tester.NewAccount();
await admin2.GrantAccessAsync(true);
var adminClient = await admin.CreateClient(Policies.CanModifyStoreSettings);
var admin2Client = await admin2.CreateClient(Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings);
var viewOnlyClient = await admin.CreateClient(Policies.CanViewStoreSettings);
var store = await adminClient.GetStore(admin.StoreId);
Assert.Empty(await adminClient.GetStoreLightningNetworkPaymentMethods(store.Id));
await AssertHttpError(403, async () =>
{
await viewOnlyClient.UpdateStoreLightningNetworkPaymentMethod(store.Id, "BTC", new LightningNetworkPaymentMethodData() { });
});
await AssertHttpError(404, async () =>
{
await adminClient.GetStoreLightningNetworkPaymentMethod(store.Id, "BTC");
});
await admin.RegisterLightningNodeAsync("BTC", false);
var method = await adminClient.GetStoreLightningNetworkPaymentMethod(store.Id, "BTC");
await AssertHttpError(403, async () =>
{
await viewOnlyClient.RemoveStoreOnChainPaymentMethod(store.Id, "BTC");
});
await adminClient.RemoveStoreOnChainPaymentMethod(store.Id, "BTC");
await AssertHttpError(404, async () =>
{
await adminClient.GetStoreOnChainPaymentMethod(store.Id, "BTC");
});
// Let's verify that the admin client can't change LN to unsafe connection strings without modify server settings rights
foreach (var forbidden in new string[]
{
"type=clightning;server=tcp://127.0.0.1",
"type=clightning;server=tcp://test",
"type=clightning;server=tcp://test.lan",
"type=clightning;server=tcp://test.local",
"type=clightning;server=tcp://192.168.1.2",
"type=clightning;server=unix://8.8.8.8",
"type=clightning;server=unix://[::1]",
"type=clightning;server=unix://[0:0:0:0:0:0:0:1]",
})
{
var ex = await AssertValidationError(new[] { "ConnectionString" }, async () =>
{
await adminClient.UpdateStoreLightningNetworkPaymentMethod(store.Id, "BTC", new LightningNetworkPaymentMethodData()
{
ConnectionString = forbidden,
CryptoCode = "BTC",
Enabled = true
});
});
Assert.Contains("btcpay.server.canmodifyserversettings", ex.Message);
// However, the other client should work because he has `btcpay.server.canmodifyserversettings`
await admin2Client.UpdateStoreLightningNetworkPaymentMethod(admin2.StoreId, "BTC", new LightningNetworkPaymentMethodData()
{
ConnectionString = forbidden,
CryptoCode = "BTC",
Enabled = true
});
}
// Allowed ip should be ok
await adminClient.UpdateStoreLightningNetworkPaymentMethod(store.Id, "BTC", new LightningNetworkPaymentMethodData()
{
ConnectionString = "type=clightning;server=tcp://8.8.8.8",
CryptoCode = "BTC",
Enabled = true
});
// If we strip the admin's right, he should not be able to set unsafe anymore, even if the API key is still valid
await admin2.MakeAdmin(false);
await AssertValidationError(new[] { "ConnectionString" }, async () =>
{
await admin2Client.UpdateStoreLightningNetworkPaymentMethod(admin2.StoreId, "BTC", new LightningNetworkPaymentMethodData()
{
ConnectionString = "type=clightning;server=tcp://127.0.0.1",
CryptoCode = "BTC",
Enabled = true
});
});
var settings = (await tester.PayTester.GetService<SettingsRepository>().GetSettingAsync<PoliciesSettings>()) ?? new PoliciesSettings();
settings.AllowLightningInternalNodeForAll = false;
await tester.PayTester.GetService<SettingsRepository>().UpdateSetting(settings);
var nonAdminUser = tester.NewAccount();
await nonAdminUser.GrantAccessAsync(false);
var nonAdminUserClient = await nonAdminUser.CreateClient(Policies.CanModifyStoreSettings);
await AssertHttpError(404, async () =>
{
await nonAdminUserClient.GetStoreLightningNetworkPaymentMethod(nonAdminUser.StoreId, "BTC");
});
await Assert.ThrowsAsync<GreenFieldValidationException>(async () =>
{
await nonAdminUserClient.UpdateStoreLightningNetworkPaymentMethod(nonAdminUser.StoreId, "BTC", method);
});
settings = await tester.PayTester.GetService<SettingsRepository>().GetSettingAsync<PoliciesSettings>();
settings.AllowLightningInternalNodeForAll = true;
await tester.PayTester.GetService<SettingsRepository>().UpdateSetting(settings);
await nonAdminUserClient.UpdateStoreLightningNetworkPaymentMethod(nonAdminUser.StoreId, "BTC", method);
}
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
public async Task WalletAPITests()
{
using var tester = ServerTester.Create();
await tester.StartAsync();
var user = tester.NewAccount();
await user.GrantAccessAsync(true);
var client = await user.CreateClient(Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings);
var viewOnlyClient = await user.CreateClient(Policies.CanViewStoreSettings);
var walletId = await user.RegisterDerivationSchemeAsync("BTC", ScriptPubKeyType.Segwit, true);
//view only clients can't do jack shit with this API
await AssertHttpError(403, async () =>
{
await viewOnlyClient.ShowOnChainWalletOverview(walletId.StoreId, walletId.CryptoCode);
});
var overview = await client.ShowOnChainWalletOverview(walletId.StoreId, walletId.CryptoCode);
Assert.Equal(0m, overview.Balance);
var fee = await client.GetOnChainFeeRate(walletId.StoreId, walletId.CryptoCode);
Assert.NotNull(fee.FeeRate);
await AssertHttpError(403, async () =>
{
await viewOnlyClient.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode);
});
var address = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode);
var address2 = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode);
var address3 = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode, true);
Assert.Equal(address.Address, address2.Address);
Assert.NotEqual(address.Address, address3.Address);
await AssertHttpError(403, async () =>
{
await viewOnlyClient.GetOnChainWalletUTXOs(walletId.StoreId, walletId.CryptoCode);
});
Assert.Empty(await client.GetOnChainWalletUTXOs(walletId.StoreId, walletId.CryptoCode));
uint256 txhash = null;
await tester.WaitForEvent<NewOnChainTransactionEvent>(async () =>
{
txhash = await tester.ExplorerNode.SendToAddressAsync(
BitcoinAddress.Create(address3.Address, tester.ExplorerClient.Network.NBitcoinNetwork),
new Money(0.01m, MoneyUnit.BTC));
});
await tester.ExplorerNode.GenerateAsync(1);
var address4 = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode, false);
Assert.NotEqual(address3.Address, address4.Address);
await client.UnReserveOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode);
var address5 = await client.GetOnChainWalletReceiveAddress(walletId.StoreId, walletId.CryptoCode, true);
Assert.Equal(address5.Address, address4.Address);
var utxo = Assert.Single(await client.GetOnChainWalletUTXOs(walletId.StoreId, walletId.CryptoCode));
Assert.Equal(0.01m, utxo.Amount);
Assert.Equal(txhash, utxo.Outpoint.Hash);
overview = await client.ShowOnChainWalletOverview(walletId.StoreId, walletId.CryptoCode);
Assert.Equal(0.01m, overview.Balance);
//the simplest request:
var nodeAddress = await tester.ExplorerNode.GetNewAddressAsync();
var createTxRequest = new CreateOnChainTransactionRequest()
{
Destinations =
new List<CreateOnChainTransactionRequest.CreateOnChainTransactionRequestDestination>()
{
new CreateOnChainTransactionRequest.CreateOnChainTransactionRequestDestination()
{
Destination = nodeAddress.ToString(), Amount = 0.001m
}
},
FeeRate = new FeeRate(5m) //only because regtest may fail but not required
};
await AssertHttpError(403, async () =>
{
await viewOnlyClient.CreateOnChainTransaction(walletId.StoreId, walletId.CryptoCode, createTxRequest);
});
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () =>
{
await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
});
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () =>
{
createTxRequest.ProceedWithBroadcast = false;
await client.CreateOnChainTransaction(walletId.StoreId, walletId.CryptoCode,
createTxRequest);
});
Transaction tx;
tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
Assert.NotNull(tx);
Assert.Contains(tx.Outputs, txout => txout.IsTo(nodeAddress) && txout.Value.ToDecimal(MoneyUnit.BTC) == 0.001m);
Assert.True((await tester.ExplorerNode.TestMempoolAcceptAsync(tx)).IsAllowed);
// no change test
createTxRequest.NoChange = true;
tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
Assert.NotNull(tx);
Assert.True(Assert.Single(tx.Outputs).IsTo(nodeAddress));
Assert.True((await tester.ExplorerNode.TestMempoolAcceptAsync(tx)).IsAllowed);
createTxRequest.NoChange = false;
//coin selection
await AssertValidationError(new[] { nameof(createTxRequest.SelectedInputs) }, async () =>
{
createTxRequest.SelectedInputs = new List<OutPoint>();
tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
});
createTxRequest.SelectedInputs = new List<OutPoint>()
{
utxo.Outpoint
};
tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
createTxRequest.SelectedInputs = null;
//destination testing
await AssertValidationError(new[] { "Destinations" }, async () =>
{
createTxRequest.Destinations[0].Amount = utxo.Amount;
tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
});
createTxRequest.Destinations[0].SubtractFromAmount = true;
tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
await AssertValidationError(new[] { "Destinations[0]" }, async () =>
{
createTxRequest.Destinations[0].Amount = 0m;
tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
});
//dest can be a bip21
//cant use bip with subtractfromamount
createTxRequest.Destinations[0].Amount = null;
createTxRequest.Destinations[0].Destination = $"bitcoin:{nodeAddress}?amount=0.001";
await AssertValidationError(new[] { "Destinations[0]" }, async () =>
{
tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
});
//if amt specified, it overrides bip21 amount
createTxRequest.Destinations[0].Amount = 0.0001m;
createTxRequest.Destinations[0].SubtractFromAmount = false;
tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
Assert.Contains(tx.Outputs, txout => txout.Value.GetValue(tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC")) == 0.0001m);
//fee rate test
createTxRequest.FeeRate = FeeRate.Zero;
await AssertValidationError(new[] { "FeeRate" }, async () =>
{
tx = await client.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
});
createTxRequest.FeeRate = new FeeRate(5.0m);
createTxRequest.Destinations[0].Amount = 0.001m;
createTxRequest.Destinations[0].Destination = nodeAddress.ToString();
createTxRequest.Destinations[0].SubtractFromAmount = false;
await AssertHttpError(403, async () =>
{
await viewOnlyClient.CreateOnChainTransactionButDoNotBroadcast(walletId.StoreId, walletId.CryptoCode,
createTxRequest, tester.ExplorerClient.Network.NBitcoinNetwork);
});
createTxRequest.ProceedWithBroadcast = true;
var txdata =
await client.CreateOnChainTransaction(walletId.StoreId, walletId.CryptoCode,
createTxRequest);
Assert.Equal(TransactionStatus.Unconfirmed, txdata.Status);
Assert.Null(txdata.BlockHeight);
Assert.Null(txdata.BlockHash);
Assert.NotNull(await tester.ExplorerClient.GetTransactionAsync(txdata.TransactionHash));
await AssertHttpError(403, async () =>
{
await viewOnlyClient.GetOnChainWalletTransaction(walletId.StoreId, walletId.CryptoCode, txdata.TransactionHash.ToString());
});
await client.GetOnChainWalletTransaction(walletId.StoreId, walletId.CryptoCode, txdata.TransactionHash.ToString());
await AssertHttpError(403, async () =>
{
await viewOnlyClient.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode);
});
Assert.True(Assert.Single(
await client.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode,
new[] { TransactionStatus.Confirmed })).TransactionHash == utxo.Outpoint.Hash);
Assert.Contains(
await client.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode,
new[] { TransactionStatus.Unconfirmed }), data => data.TransactionHash == txdata.TransactionHash);
Assert.Contains(
await client.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode), data => data.TransactionHash == txdata.TransactionHash);
await tester.WaitForEvent<NewBlockEvent>(async () =>
{
await tester.ExplorerNode.GenerateAsync(1);
}, bevent => bevent.CryptoCode.Equals("BTC", StringComparison.Ordinal));
Assert.Contains(
await client.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode,
new[] { TransactionStatus.Confirmed }), data => data.TransactionHash == txdata.TransactionHash);
}
[Fact(Timeout = TestTimeout)]
[Trait("Fast", "Fast")]
@ -1885,6 +1291,7 @@ namespace BTCPayServer.Tests
Assert.True(jsonConverter.CanConvert(typeof(double)));
Assert.True(jsonConverter.CanConvert(typeof(double?)));
Assert.False(jsonConverter.CanConvert(typeof(float)));
Assert.False(jsonConverter.CanConvert(typeof(int)));
Assert.False(jsonConverter.CanConvert(typeof(string)));
var numberJson = "1";
@ -1909,55 +1316,6 @@ namespace BTCPayServer.Tests
Assert.Equal(1.2, jsonConverter.ReadJson(Get(stringJson), typeof(double), null, null));
Assert.Equal(1.2, jsonConverter.ReadJson(Get(stringJson), typeof(double?), null, null));
}
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Lightning", "Lightning")]
[Trait("Integration", "Integration")]
public async Task StorePaymentMethodsAPITests()
{
using var tester = ServerTester.Create();
tester.ActivateLightning();
await tester.StartAsync();
await tester.EnsureChannelsSetup();
var admin = tester.NewAccount();
await admin.GrantAccessAsync(true);
var adminClient = await admin.CreateClient(Policies.Unrestricted);
var store = await adminClient.GetStore(admin.StoreId);
Assert.Empty(await adminClient.GetStorePaymentMethods(store.Id));
await adminClient.UpdateStoreLightningNetworkPaymentMethodToInternalNode(admin.StoreId, "BTC");
void VerifyLightning(Dictionary<string, GenericPaymentMethodData> dictionary)
{
Assert.True(dictionary.TryGetValue(new PaymentMethodId("BTC", PaymentTypes.LightningLike).ToStringNormalized(), out var item));
var lightningNetworkPaymentMethodBaseData =Assert.IsType<JObject>(item.Data).ToObject<LightningNetworkPaymentMethodBaseData>();
Assert.Equal("Internal Node", lightningNetworkPaymentMethodBaseData.ConnectionString);
}
var methods = await adminClient.GetStorePaymentMethods(store.Id);
Assert.Single(methods);
VerifyLightning(methods);
var randK = new Mnemonic(Wordlist.English, WordCount.Twelve).DeriveExtKey().Neuter().ToString(Network.RegTest);
await adminClient.UpdateStoreOnChainPaymentMethod(admin.StoreId, "BTC",
new OnChainPaymentMethodData("BTC", randK, true, "testing", null));
void VerifyOnChain(Dictionary<string, GenericPaymentMethodData> dictionary)
{
Assert.True(dictionary.TryGetValue(new PaymentMethodId("BTC", PaymentTypes.BTCLike).ToStringNormalized(), out var item));
var paymentMethodBaseData =Assert.IsType<JObject>(item.Data).ToObject<OnChainPaymentMethodBaseData>();
Assert.Equal(randK, paymentMethodBaseData.DerivationScheme);
}
methods = await adminClient.GetStorePaymentMethods(store.Id);
Assert.Equal(2, methods.Count);
VerifyLightning(methods);
VerifyOnChain(methods);
}
}
}

View File

@ -1,52 +0,0 @@
using System.Threading.Tasks;
using BTCPayServer.Services;
using BTCPayServer.Tests.Logging;
using Xunit;
using Xunit.Abstractions;
namespace BTCPayServer.Tests
{
public class LanguageServiceTests
{
public const int TestTimeout = TestUtils.TestTimeout;
public LanguageServiceTests(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) { Name = "Tests" };
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanAutoDetectLanguage()
{
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
var languageService = tester.PayTester.GetService<LanguageService>();
// Most common format. First option does not have a quality score. Others do in descending order.
// Result should be nl-NL (because the default weight is 1 for nl)
var lang1 = languageService.FindLanguageInAcceptLanguageHeader("nl,fr;q=0.7,en;q=0.5");
Assert.NotNull(lang1);
Assert.Equal("nl-NL", lang1?.Code);
// Most common format. First option does not have a quality score. Others do in descending order. This time the first option includes a country.
// Result should be nl-NL (because the default weight is 1 for nl-BE and it does not exist in BTCPay Server, but nl-NL does and applies too for language "nl")
var lang2 = languageService.FindLanguageInAcceptLanguageHeader("nl-BE,fr;q=0.7,en;q=0.5");
Assert.NotNull(lang2);
Assert.Equal("nl-NL", lang2?.Code);
// Unusual format, but still valid. All values have a quality score and not ordered.
// Result should be fr-FR (because 0.7 is the highest quality score)
var lang3 = languageService.FindLanguageInAcceptLanguageHeader("nl;q=0.1,fr;q=0.7,en;q=0.5");
Assert.NotNull(lang3);
Assert.Equal("fr-FR", lang3?.Code);
// Unusual format, but still valid. Some language is given that we don't have and a wildcard for everything else.
// Result should be NULL, because "xx" does not exist and * is a wildcard and has no meaning.
var lang4 = languageService.FindLanguageInAcceptLanguageHeader("xx,*;q=0.5");
Assert.Null(lang4);
}
}
}
}

View File

@ -86,9 +86,9 @@ namespace BTCPayServer.Tests
var signedPSBT = unsignedPSBT.Clone();
signedPSBT.SignAll(user.DerivationScheme, user.GenerateWalletResponseV.AccountHDKey, user.GenerateWalletResponseV.AccountKeyPath);
vmPSBT.PSBT = signedPSBT.ToBase64();
var psbtReady = await walletController.WalletPSBTReady(walletId, new WalletPSBTReadyViewModel
var psbtReady = await walletController.WalletPSBTReady(walletId, new WalletPSBTReadyViewModel()
{
SigningContext = new SigningContextModel
SigningContext = new SigningContextModel()
{
PSBT = AssertRedirectedPSBT(await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"), nameof(walletController.WalletPSBTReady))
}
@ -96,7 +96,9 @@ namespace BTCPayServer.Tests
Assert.Equal(2 + 1, psbtReady.Destinations.Count); // The fee is a destination
Assert.Contains(psbtReady.Destinations, d => d.Destination == sendDestination && !d.Positive);
Assert.Contains(psbtReady.Destinations, d => d.Positive);
var redirect = Assert.IsType<RedirectToActionResult>(await walletController.WalletPSBTReady(walletId, psbtReady, command: "broadcast"));
Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName);
vmPSBT.PSBT = unsignedPSBT.ToBase64();
var combineVM = await walletController.WalletPSBT(walletId, vmPSBT, "combine").AssertViewModelAsync<WalletPSBTCombineViewModel>();
Assert.Equal(vmPSBT.PSBT, combineVM.OtherPSBT);
@ -117,14 +119,14 @@ namespace BTCPayServer.Tests
Assert.True(signedPSBT2.TryFinalize(out _));
Assert.Equal(signedPSBT, signedPSBT2);
var ready = (await walletController.WalletPSBTReady(walletId, new WalletPSBTReadyViewModel
var ready = (await walletController.WalletPSBTReady(walletId, new WalletPSBTReadyViewModel()
{
SigningContext = new SigningContextModel(signedPSBT)
})).AssertViewModel<WalletPSBTReadyViewModel>();
Assert.Equal(signedPSBT.ToBase64(), ready.SigningContext.PSBT);
psbt = AssertRedirectedPSBT(await walletController.WalletPSBTReady(walletId, ready, command: "analyze-psbt"), nameof(walletController.WalletPSBT));
Assert.Equal(signedPSBT.ToBase64(), psbt);
var redirect = Assert.IsType<RedirectToActionResult>(await walletController.WalletPSBTReady(walletId, ready, command: "broadcast"));
redirect = Assert.IsType<RedirectToActionResult>(await walletController.WalletPSBTReady(walletId, ready, command: "broadcast"));
Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName);
//test base64 psbt file

View File

@ -8,10 +8,10 @@ using BTCPayServer.Client.Models;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Models;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.PayJoin;
using BTCPayServer.Payments.PayJoin.Sender;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Wallets;
@ -19,7 +19,6 @@ using BTCPayServer.Tests.Logging;
using BTCPayServer.Views.Wallets;
using Microsoft.AspNetCore.Http;
using NBitcoin;
using BTCPayServer.BIP78.Sender;
using NBitcoin.Payment;
using NBitpayClient;
using NBXplorer.DerivationStrategy;
@ -87,19 +86,6 @@ namespace BTCPayServer.Tests
Assert.True(await repo.TryLock(outpoint));
Assert.True(await repo.TryUnlock(outpoint));
Assert.False(await repo.TryUnlock(outpoint));
// Make sure that if any can't be locked, all are not locked
var outpoint1 = RandomOutpoint();
var outpoint2 = RandomOutpoint();
Assert.True(await repo.TryLockInputs(new[] { outpoint1 }));
Assert.False(await repo.TryLockInputs(new[] { outpoint1, outpoint2 }));
Assert.True(await repo.TryLockInputs(new[] { outpoint2 }));
outpoint1 = RandomOutpoint();
outpoint2 = RandomOutpoint();
Assert.True(await repo.TryLockInputs(new[] { outpoint1 }));
Assert.False(await repo.TryLockInputs(new[] { outpoint2, outpoint1 }));
Assert.True(await repo.TryLockInputs(new[] { outpoint2 }));
}
}
@ -146,7 +132,7 @@ namespace BTCPayServer.Tests
{
var tx = network.NBitcoinNetwork.CreateTransaction();
tx.Inputs.Add(new OutPoint(RandomUtils.GetUInt256(), 0), Script.Empty);
tx.Outputs.Add(Money.Coins(1.0m), new Key().GetScriptPubKey(ScriptPubKeyType.Legacy));
tx.Outputs.Add(Money.Coins(1.0m), new Key().ScriptPubKey);
return tx;
}
@ -179,21 +165,19 @@ namespace BTCPayServer.Tests
var cashCow = tester.ExplorerNode;
cashCow.Generate(2); // get some money in case
var unsupportedFormats = new[] {ScriptPubKeyType.Legacy};
var unsupportedFormats = Enum.GetValues(typeof(ScriptPubKeyType))
.AssertType<ScriptPubKeyType[]>()
.Where(type => !PayjoinClient.SupportedFormats.Contains(type));
foreach (ScriptPubKeyType senderAddressType in Enum.GetValues(typeof(ScriptPubKeyType)))
{
if (senderAddressType == ScriptPubKeyType.TaprootBIP86)
continue;
var senderUser = tester.NewAccount();
senderUser.GrantAccess(true);
senderUser.RegisterDerivationScheme("BTC", senderAddressType);
foreach (ScriptPubKeyType receiverAddressType in Enum.GetValues(typeof(ScriptPubKeyType)))
{
if (receiverAddressType == ScriptPubKeyType.TaprootBIP86)
continue;
var senderCoin = await senderUser.ReceiveUTXO(Money.Satoshis(100000), network);
Logs.Tester.LogInformation($"Testing payjoin with sender: {senderAddressType} receiver: {receiverAddressType}");
@ -226,57 +210,6 @@ namespace BTCPayServer.Tests
}
}
[Fact]
[Trait("Selenium", "Selenium")]
public async Task CanUsePayjoinForTopUp()
{
using (var s = SeleniumTester.Create())
{
await s.StartAsync();
s.RegisterNewUser(true);
var receiver = s.CreateNewStore();
var receiverSeed = s.GenerateWallet("BTC", "", true, true, ScriptPubKeyType.Segwit);
var receiverWalletId = new WalletId(receiver.storeId, "BTC");
var sender = s.CreateNewStore();
var senderSeed = s.GenerateWallet("BTC", "", true, true, ScriptPubKeyType.Segwit);
var senderWalletId = new WalletId(sender.storeId, "BTC");
await s.Server.ExplorerNode.GenerateAsync(1);
await s.FundStoreWallet(senderWalletId);
await s.FundStoreWallet(receiverWalletId);
var invoiceId = s.CreateInvoice(receiver.storeName, null, "BTC");
s.GoToInvoiceCheckout(invoiceId);
var bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn"))
.GetAttribute("href");
Assert.Contains($"{PayjoinClient.BIP21EndpointKey}=", bip21);
s.GoToWallet(senderWalletId, WalletsNavPages.Send);
s.Driver.FindElement(By.Id("bip21parse")).Click();
s.Driver.SwitchTo().Alert().SendKeys(bip21);
s.Driver.SwitchTo().Alert().Accept();
s.Driver.FindElement(By.Id("Outputs_0__Amount")).Clear();
s.Driver.FindElement(By.Id("Outputs_0__Amount")).SendKeys("0.023");
s.Driver.FindElement(By.Id("SignTransaction")).Click();
await s.Server.WaitForEvent<NewOnChainTransactionEvent>(() =>
{
s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).Click();
return Task.CompletedTask;
});
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
var invoiceRepository = s.Server.PayTester.GetService<InvoiceRepository>();
await TestUtils.EventuallyAsync(async () =>
{
var invoice = await invoiceRepository.GetInvoice(invoiceId);
Assert.Equal(InvoiceStatusLegacy.Paid, invoice.Status);
Assert.Equal(0.023m, invoice.Price);
});
}
}
[Fact]
[Trait("Selenium", "Selenium")]
public async Task CanUsePayjoinViaUI()
@ -287,21 +220,25 @@ namespace BTCPayServer.Tests
var invoiceRepository = s.Server.PayTester.GetService<InvoiceRepository>();
s.RegisterNewUser(true);
foreach (var format in new []{ScriptPubKeyType.Segwit, ScriptPubKeyType.SegwitP2SH})
foreach (var format in PayjoinClient.SupportedFormats)
{
var receiver = s.CreateNewStore();
var receiverSeed = s.GenerateWallet("BTC", "", true, true, format);
var receiverWalletId = new WalletId(receiver.storeId, "BTC");
//payjoin is enabled by default.
//payjoin is not enabled by default.
var invoiceId = s.CreateInvoice(receiver.storeName);
s.GoToInvoiceCheckout(invoiceId);
var bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn"))
.GetAttribute("href");
Assert.Contains($"{PayjoinClient.BIP21EndpointKey}=", bip21);
Assert.DoesNotContain($"{PayjoinClient.BIP21EndpointKey}=", bip21);
s.GoToHome();
s.GoToStore(receiver.storeId);
//payjoin is not enabled by default.
Assert.False(s.Driver.FindElement(By.Id("PayJoinEnabled")).Selected);
s.SetCheckbox(s, "PayJoinEnabled", true);
s.Driver.FindElement(By.Id("Save")).Click();
Assert.True(s.Driver.FindElement(By.Id("PayJoinEnabled")).Selected);
var sender = s.CreateNewStore();
@ -322,7 +259,10 @@ namespace BTCPayServer.Tests
s.Driver.SwitchTo().Alert().Accept();
Assert.False(string.IsNullOrEmpty(s.Driver.FindElement(By.Id("PayJoinBIP21"))
.GetAttribute("value")));
s.Driver.FindElement(By.Id("SignTransaction")).Click();
s.Driver.FindElement(By.Id("SendMenu")).Click();
var nbxSeedButton = s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]"));
new WebDriverWait(s.Driver, SeleniumTester.ImplicitWait).Until(d=> nbxSeedButton.Enabled);
nbxSeedButton.Click();
await s.Server.WaitForEvent<NewOnChainTransactionEvent>(() =>
{
s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).Click();
@ -357,7 +297,8 @@ namespace BTCPayServer.Tests
.GetAttribute("value")));
s.Driver.FindElement(By.Id("FeeSatoshiPerByte")).Clear();
s.Driver.FindElement(By.Id("FeeSatoshiPerByte")).SendKeys("2");
s.Driver.FindElement(By.Id("SignTransaction")).Click();
s.Driver.FindElement(By.Id("SendMenu")).Click();
s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click();
var txId = await s.Server.WaitForEvent<NewOnChainTransactionEvent>(() =>
{
s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).Click();
@ -367,7 +308,7 @@ namespace BTCPayServer.Tests
await TestUtils.EventuallyAsync(async () =>
{
var invoice = await invoiceRepository.GetInvoice(invoiceId);
var payments = invoice.GetPayments(false);
var payments = invoice.GetPayments();
Assert.Equal(2, payments.Count);
var originalPayment = payments[0];
var coinjoinPayment = payments[1];
@ -424,15 +365,10 @@ namespace BTCPayServer.Tests
var alice = tester.NewAccount();
await alice.RegisterDerivationSchemeAsync("BTC", ScriptPubKeyType.Segwit, true);
await notifications.ListenDerivationSchemesAsync(new[] { alice.DerivationScheme });
BitcoinAddress address = null;
for (int i = 0; i < 5; i++)
{
address = (await nbx.GetUnusedAsync(alice.DerivationScheme, DerivationFeature.Deposit)).Address;
await tester.ExplorerNode.GenerateAsync(1);
tester.ExplorerNode.SendToAddress(address, Money.Coins(1.0m));
await notifications.NextEventAsync();
}
var address = (await nbx.GetUnusedAsync(alice.DerivationScheme, DerivationFeature.Deposit)).Address;
await tester.ExplorerNode.GenerateAsync(1);
tester.ExplorerNode.SendToAddress(address, Money.Coins(1.0m));
await notifications.NextEventAsync();
var paymentAddress = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, Network.RegTest);
var otherAddress = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, Network.RegTest);
var psbt = (await nbx.CreatePSBTAsync(alice.DerivationScheme, new CreatePSBTRequest()
@ -474,7 +410,7 @@ namespace BTCPayServer.Tests
using var fakeServer = new FakeServer();
await fakeServer.Start();
var bip21 = new BitcoinUrlBuilder($"bitcoin:{paymentAddress}?pj={fakeServer.ServerUri}", Network.RegTest);
var requesting = pjClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, default);
var requesting = pjClient.RequestPayjoin(bip21, derivationSchemeSettings, psbt, default);
var request = await fakeServer.GetNextRequest();
Assert.Equal("1", request.Request.Query["v"][0]);
Assert.Equal(changeIndex.ToString(), request.Request.Query["additionalfeeoutputindex"][0]);
@ -490,7 +426,7 @@ namespace BTCPayServer.Tests
Assert.Contains("contribution is more than maxadditionalfeecontribution", ex.Message);
Logs.Tester.LogInformation("The payjoin receiver tries to change one of our output");
requesting = pjClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, default);
requesting = pjClient.RequestPayjoin(bip21, derivationSchemeSettings, psbt, default);
request = await fakeServer.GetNextRequest();
originalPSBT = await ParsePSBT(request);
proposalTx = originalPSBT.GetGlobalTransaction();
@ -499,8 +435,9 @@ namespace BTCPayServer.Tests
fakeServer.Done();
ex = await Assert.ThrowsAsync<PayjoinSenderException>(async () => await requesting);
Assert.Contains("The receiver decreased the value of one", ex.Message);
Logs.Tester.LogInformation("The payjoin receiver tries to pocket the fee");
requesting = pjClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, default);
requesting = pjClient.RequestPayjoin(bip21, derivationSchemeSettings, psbt, default);
request = await fakeServer.GetNextRequest();
originalPSBT = await ParsePSBT(request);
proposalTx = originalPSBT.GetGlobalTransaction();
@ -511,7 +448,7 @@ namespace BTCPayServer.Tests
Assert.Contains("The receiver decreased absolute fee", ex.Message);
Logs.Tester.LogInformation("The payjoin receiver tries to remove one of our output");
requesting = pjClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, default);
requesting = pjClient.RequestPayjoin(bip21, derivationSchemeSettings, psbt, default);
request = await fakeServer.GetNextRequest();
originalPSBT = await ParsePSBT(request);
proposalTx = originalPSBT.GetGlobalTransaction();
@ -523,7 +460,7 @@ namespace BTCPayServer.Tests
Assert.Contains("Some of our outputs are not included in the proposal", ex.Message);
Logs.Tester.LogInformation("The payjoin receiver tries to change their own output");
requesting = pjClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, default);
requesting = pjClient.RequestPayjoin(bip21, derivationSchemeSettings, psbt, default);
request = await fakeServer.GetNextRequest();
originalPSBT = await ParsePSBT(request);
proposalTx = originalPSBT.GetGlobalTransaction();
@ -535,7 +472,7 @@ namespace BTCPayServer.Tests
Logs.Tester.LogInformation("The payjoin receiver tries to send money to himself");
pjClient.MaxFeeBumpContribution = Money.Satoshis(1);
requesting = pjClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, default);
requesting = pjClient.RequestPayjoin(bip21, derivationSchemeSettings, psbt, default);
request = await fakeServer.GetNextRequest();
originalPSBT = await ParsePSBT(request);
proposalTx = originalPSBT.GetGlobalTransaction();
@ -546,9 +483,10 @@ namespace BTCPayServer.Tests
ex = await Assert.ThrowsAsync<PayjoinSenderException>(async () => await requesting);
Assert.Contains("is not only paying fee", ex.Message);
pjClient.MaxFeeBumpContribution = null;
Logs.Tester.LogInformation("The payjoin receiver can't use additional fee without adding inputs");
pjClient.MinimumFeeRate = new FeeRate(50m);
requesting = pjClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, default);
requesting = pjClient.RequestPayjoin(bip21, derivationSchemeSettings, psbt, default);
request = await fakeServer.GetNextRequest();
originalPSBT = await ParsePSBT(request);
proposalTx = originalPSBT.GetGlobalTransaction();
@ -563,19 +501,15 @@ namespace BTCPayServer.Tests
var bob = tester.NewAccount();
await bob.GrantAccessAsync();
await bob.RegisterDerivationSchemeAsync("BTC", ScriptPubKeyType.Segwit, true);
await notifications.DisposeAsync();
notifications = await nbx.CreateWebsocketNotificationSessionAsync();
await notifications.ListenDerivationSchemesAsync(new[] { bob.DerivationScheme });
address = (await nbx.GetUnusedAsync(bob.DerivationScheme, DerivationFeature.Deposit)).Address;
tester.ExplorerNode.SendToAddress(address, Money.Coins(1.1m));
await notifications.NextEventAsync();
await bob.ModifyStore(s => s.PayJoinEnabled = true);
bob.ModifyStore(s => s.PayJoinEnabled = true);
var invoice = bob.BitPay.CreateInvoice(
new Invoice() { Price = 0.1m, Currency = "BTC", FullNotifications = true });
var invoiceBIP21 = new BitcoinUrlBuilder(invoice.CryptoInfo.First().PaymentUrls.BIP21,
tester.ExplorerClient.Network.NBitcoinNetwork);
psbt = (await nbx.CreatePSBTAsync(alice.DerivationScheme, new CreatePSBTRequest()
{
Destinations =
@ -594,7 +528,7 @@ namespace BTCPayServer.Tests
psbt.SignAll(derivationSchemeSettings.AccountDerivation, alice.GenerateWalletResponseV.AccountHDKey, signingAccount.GetRootedKeyPath());
var endpoint = TestAccount.GetPayjoinBitcoinUrl(invoice, Network.RegTest);
pjClient.MaxFeeBumpContribution = Money.Satoshis(50);
var proposal = await pjClient.RequestPayjoin(endpoint, new PayjoinWallet(derivationSchemeSettings), psbt, default);
var proposal = await pjClient.RequestPayjoin(endpoint, derivationSchemeSettings, psbt, default);
Assert.True(proposal.TryGetFee(out var newFee));
Assert.Equal(Money.Satoshis(3001 + 50), newFee);
proposal = proposal.SignAll(derivationSchemeSettings.AccountDerivation, alice.GenerateWalletResponseV.AccountHDKey, signingAccount.GetRootedKeyPath());
@ -625,7 +559,7 @@ namespace BTCPayServer.Tests
psbt.SignAll(derivationSchemeSettings.AccountDerivation, alice.GenerateWalletResponseV.AccountHDKey, signingAccount.GetRootedKeyPath());
endpoint = TestAccount.GetPayjoinBitcoinUrl(invoice, Network.RegTest);
pjClient.MinimumFeeRate = new FeeRate(100_000_000.2m);
var ex2 = await Assert.ThrowsAsync<PayjoinReceiverException>(async () => await pjClient.RequestPayjoin(endpoint, new PayjoinWallet(derivationSchemeSettings), psbt, default));
var ex2 = await Assert.ThrowsAsync<PayjoinReceiverException>(async () => await pjClient.RequestPayjoin(endpoint, derivationSchemeSettings, psbt, default));
Assert.Equal(PayjoinReceiverWellknownErrors.NotEnoughMoney, ex2.WellknownError);
}
}
@ -867,32 +801,16 @@ retry:
//give the cow some cash
await cashCow.GenerateAsync(1);
//let's get some more utxos first
foreach (var m in new []
{
Money.Coins(0.011m),
Money.Coins(0.012m),
Money.Coins(0.013m),
Money.Coins(0.014m),
Money.Coins(0.015m),
Money.Coins(0.016m)
})
{
await receiverUser.ReceiveUTXO(m, btcPayNetwork);
}
foreach (var m in new[]
{
Money.Coins(0.021m),
Money.Coins(0.022m),
Money.Coins(0.023m),
Money.Coins(0.024m),
Money.Coins(0.025m),
Money.Coins(0.026m)
})
{
await senderUser.ReceiveUTXO(m, btcPayNetwork);
}
await receiverUser.ReceiveUTXO(Money.Coins(0.011m), btcPayNetwork);
await receiverUser.ReceiveUTXO(Money.Coins(0.012m), btcPayNetwork);
await receiverUser.ReceiveUTXO(Money.Coins(0.013m), btcPayNetwork);
await receiverUser.ReceiveUTXO(Money.Coins(0.014m), btcPayNetwork);
await senderUser.ReceiveUTXO(Money.Coins(0.021m), btcPayNetwork);
await senderUser.ReceiveUTXO(Money.Coins(0.022m), btcPayNetwork);
await senderUser.ReceiveUTXO(Money.Coins(0.023m), btcPayNetwork);
await senderUser.ReceiveUTXO(Money.Coins(0.024m), btcPayNetwork);
await senderUser.ReceiveUTXO(Money.Coins(0.025m), btcPayNetwork);
await senderUser.ReceiveUTXO(Money.Coins(0.026m), btcPayNetwork);
var senderChange = await senderUser.GetNewAddress(btcPayNetwork);
//Let's start the harassment
@ -923,24 +841,17 @@ retry:
settings.PaymentId == paymentMethodId);
ReceivedCoin[] senderCoins = null;
ReceivedCoin coin = null;
ReceivedCoin coin2 = null;
ReceivedCoin coin3 = null;
ReceivedCoin coin4 = null;
ReceivedCoin coin5 = null;
ReceivedCoin coin6 = null;
await TestUtils.EventuallyAsync(async () =>
{
senderCoins = await btcPayWallet.GetUnspentCoins(senderUser.DerivationScheme);
Assert.Contains(senderCoins, coin => coin.Value.GetValue(btcPayNetwork) == 0.026m);
coin = Assert.Single(senderCoins, coin => coin.Value.GetValue(btcPayNetwork) == 0.021m);
coin2 = Assert.Single(senderCoins, coin => coin.Value.GetValue(btcPayNetwork) == 0.022m);
coin3 = Assert.Single(senderCoins, coin => coin.Value.GetValue(btcPayNetwork) == 0.023m);
coin4 = Assert.Single(senderCoins, coin => coin.Value.GetValue(btcPayNetwork) == 0.024m);
coin5 = Assert.Single(senderCoins, coin => coin.Value.GetValue(btcPayNetwork) == 0.025m);
coin6 = Assert.Single(senderCoins, coin => coin.Value.GetValue(btcPayNetwork) == 0.026m);
});
var coin = senderCoins.Single(coin => coin.Value.GetValue(btcPayNetwork) == 0.021m);
var coin2 = senderCoins.Single(coin => coin.Value.GetValue(btcPayNetwork) == 0.022m);
var coin3 = senderCoins.Single(coin => coin.Value.GetValue(btcPayNetwork) == 0.023m);
var coin4 = senderCoins.Single(coin => coin.Value.GetValue(btcPayNetwork) == 0.024m);
var coin5 = senderCoins.Single(coin => coin.Value.GetValue(btcPayNetwork) == 0.025m);
var coin6 = senderCoins.Single(coin => coin.Value.GetValue(btcPayNetwork) == 0.026m);
var signingKeySettings = derivationSchemeSettings.GetSigningAccountKeySettings();
signingKeySettings.RootFingerprint =
@ -984,6 +895,10 @@ retry:
.SendEstimatedFees(new FeeRate(100m))
.BuildTransaction(true);
//Attempt 1: Send a signed tx to invoice 1 that does not pay the invoice at all
//Result: reject
// Assert.False((await tester.PayTester.HttpClient.PostAsync(endpoint,
// new StringContent(Invoice2Coin1.ToHex(), Encoding.UTF8, "text/plain"))).IsSuccessStatusCode);
//Attempt 2: Create two transactions using different inputs and send them to the same invoice.
//Result: Second Tx should be rejected.
@ -1135,7 +1050,7 @@ retry:
{
var invoiceEntity = await tester.PayTester.GetService<InvoiceRepository>().GetInvoice(invoice7.Id);
Assert.Equal(InvoiceStatusLegacy.Paid, invoiceEntity.Status);
Assert.Contains(invoiceEntity.GetPayments(false), p => p.Accounted &&
Assert.Contains(invoiceEntity.GetPayments(), p => p.Accounted &&
((BitcoinLikePaymentData)p.GetCryptoPaymentData()).PayjoinInformation is null);
});
////Assert.Contains(receiverWalletPayJoinState.GetRecords(), item => item.InvoiceId == invoice7.Id && item.TxSeen);
@ -1164,8 +1079,8 @@ retry:
{
var invoiceEntity = await tester.PayTester.GetService<InvoiceRepository>().GetInvoice(invoice7.Id);
Assert.Equal(InvoiceStatusLegacy.New, invoiceEntity.Status);
Assert.True(invoiceEntity.GetPayments(false).All(p => !p.Accounted));
ourOutpoint = invoiceEntity.GetAllBitcoinPaymentData(false).First().PayjoinInformation.ContributedOutPoints[0];
Assert.True(invoiceEntity.GetPayments().All(p => !p.Accounted));
ourOutpoint = invoiceEntity.GetAllBitcoinPaymentData().First().PayjoinInformation.ContributedOutPoints[0];
});
var payjoinRepository = tester.PayTester.GetService<PayJoinRepository>();
// The outpoint should now be available for next pj selection

View File

@ -26,21 +26,6 @@ You can also generate blocks:
.\docker-bitcoin-generate.ps1 3
```
### Using Polar to test Lightning payments
- Install and run [Polar](https://lightningpolar.com/). Setup a small network of nodes.
- Go to your store's General Settings and enable Lightning.
- Build your connection string using the Connect infomation in the Polar app.
LND Connection string example:
type=lnd-rest;server=https://127.0.0.1:8084/;macaroonfilepath="local path to admin.macaroon on your computer, without these quotes";allowinsecure=true
Now you can create a Lightning invoice on BTCPay Server regtest and make a payment through Polar.
PLEASE NOTE: You may get an exception break in Visual Studio. You must quickly click "Continue" in VS so the invoice is generated.
Or, uncheck the box that says, "Break when this exceptiontype is thrown".
### Using the test litecoin-cli
Same as bitcoin-cli, but with `.\docker-litecoin-cli.ps1` and `.\docker-litecoin-cli.sh` instead.
@ -70,20 +55,6 @@ Please, run the test `CanSetLightningServer`, this will establish a channel betw
Alternatively you can run the `./docker-lightning-channel-setup.sh` script to establish the channel connection.
The `./docker-lightning-channel-teardown.sh` script closes any existing lightning channels.
### Alternative Lightning testing: Using Polar to test Lightning payments
- Install and run [Polar](https://lightningpolar.com/). Setup a small network of nodes.
- Go to your store's General Settings and enable Lightning.
- Build your connection string using the Connect information in the Polar app.
LND Connection string example:
type=lnd-rest;server=https://127.0.0.1:8084/;macaroonfilepath="local path to admin.macaroon on your computer, without these quotes";allowinsecure=true
Now you can create a lightning invoice on BTCPay Server regtest and make a payment through Polar.
PLEASE NOTE: You may get an exception break in Visual Studio. You must quickly click "Continue" in VS so the invoice is generated.
Or, uncheck the box that says, "Break when this exception type is thrown".
## FAQ
### `docker-compose up dev` failed or tests are not passing, what should I do?

View File

@ -15,10 +15,8 @@ using BTCPayServer.Views.Stores;
using BTCPayServer.Views.Wallets;
using Microsoft.Extensions.Configuration;
using NBitcoin;
using BTCPayServer.BIP78.Sender;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Support.Extensions;
using Xunit;
namespace BTCPayServer.Tests
@ -34,7 +32,6 @@ namespace BTCPayServer.Tests
public static SeleniumTester Create([CallerMemberNameAttribute] string scope = null, bool newDb = false) =>
new SeleniumTester { Server = ServerTester.Create(scope, newDb) };
public static readonly TimeSpan ImplicitWait = TimeSpan.FromSeconds(5);
public async Task StartAsync()
{
@ -49,7 +46,9 @@ namespace BTCPayServer.Tests
var runInBrowser = config["RunSeleniumInBrowser"] == "true";
// Reset this using `dotnet user-secrets remove RunSeleniumInBrowser`
var chromeDriverPath = config["ChromeDriverDirectory"] ?? (Server.PayTester.InContainer ? "/usr/bin" : Directory.GetCurrentDirectory());
var chromeDriverPath = config["ChromeDriverDirectory"] ??
(Server.PayTester.InContainer ? "/usr/bin" : Directory.GetCurrentDirectory());
var options = new ChromeOptions();
if (Server.PayTester.InContainer)
@ -63,34 +62,31 @@ namespace BTCPayServer.Tests
}
options.AddArguments($"window-size={windowSize.Width}x{windowSize.Height}");
options.AddArgument("shm-size=2g");
options.AddArgument("start-maximized");
Driver = new ChromeDriver(chromeDriverPath, options);
var cds = ChromeDriverService.CreateDefaultService(chromeDriverPath);
cds.EnableVerboseLogging = true;
cds.Port = Utils.FreeTcpPort();
cds.HostName = "127.0.0.1";
cds.Start();
Driver = new ChromeDriver(cds, options,
// A bit less than test timeout
TimeSpan.FromSeconds(50));
Driver.Manage().Window.Maximize();
if (runInBrowser)
{
// ensure maximized window size
Driver.Manage().Window.Maximize();
}
Logs.Tester.LogInformation($"Selenium: Using {Driver.GetType()}");
Logs.Tester.LogInformation($"Selenium: Browsing to {Server.PayTester.ServerUri}");
Logs.Tester.LogInformation($"Selenium: Resolution {Driver.Manage().Window.Size}");
Driver.Manage().Timeouts().ImplicitWait = ImplicitWait;
GoToRegister();
Driver.AssertNoError();
}
internal IWebElement FindAlertMessage(StatusMessageModel.StatusSeverity severity = StatusMessageModel.StatusSeverity.Success)
{
var className = $"alert-{StatusMessageModel.ToString(severity)}";
var el = Driver.FindElement(By.ClassName(className)) ?? Driver.WaitForElement(By.ClassName(className));
var el = Driver.FindElements(By.ClassName($"alert-{StatusMessageModel.ToString(severity)}")).FirstOrDefault(e => e.Displayed);
if (el is null)
throw new NoSuchElementException($"Unable to find {className}");
throw new NoSuchElementException($"Unable to find alert-{StatusMessageModel.ToString(severity)}");
return el;
}
public static readonly TimeSpan ImplicitWait = TimeSpan.FromSeconds(5);
public string Link(string relativeLink)
{
return Server.PayTester.ServerUri.AbsoluteUri.WithoutEndingSlash() + relativeLink.WithStartingSlash();
@ -117,69 +113,59 @@ namespace BTCPayServer.Tests
public (string storeName, string storeId) CreateNewStore()
{
Driver.WaitForElement(By.Id("Stores")).Click();
Driver.WaitForElement(By.Id("CreateStore")).Click();
Driver.FindElement(By.Id("Stores")).Click();
Driver.FindElement(By.Id("CreateStore")).Click();
var name = "Store" + RandomUtils.GetUInt64();
Driver.WaitForElement(By.Id("Name")).SendKeys(name);
Driver.WaitForElement(By.Id("Create")).Click();
StoreId = Driver.WaitForElement(By.Id("Id")).GetAttribute("value");
Driver.FindElement(By.Id("Name")).SendKeys(name);
Driver.FindElement(By.Id("Create")).Click();
StoreId = Driver.FindElement(By.Id("Id")).GetAttribute("value");
return (name, StoreId);
}
public Mnemonic GenerateWallet(string cryptoCode = "BTC", string seed = "", bool importkeys = false, bool privkeys = false, ScriptPubKeyType format = ScriptPubKeyType.Segwit)
{
var isImport = !string.IsNullOrEmpty(seed);
Driver.FindElement(By.Id($"Modify{cryptoCode}")).Click();
// Replace previous wallet case
if (Driver.PageSource.Contains("id=\"ChangeWalletLink\""))
// Modify case
if (Driver.PageSource.Contains("id=\"change-wallet-link\""))
{
Driver.FindElement(By.Id("ChangeWalletLink")).Click();
Driver.FindElement(By.Id("continue")).Click();
Driver.FindElement(By.Id("change-wallet-link")).Click();
}
if (isImport)
if (string.IsNullOrEmpty(seed))
{
Logs.Tester.LogInformation("Progressing with existing seed");
Driver.FindElement(By.Id("ImportWalletOptionsLink")).Click();
Driver.FindElement(By.Id("ImportSeedLink")).Click();
Driver.FindElement(By.Id("ExistingMnemonic")).SendKeys(seed);
Driver.SetCheckbox(By.Id("SavePrivateKeys"), privkeys);
var option = privkeys ? "hotwallet" : "watchonly";
Logs.Tester.LogInformation($"Generating new seed ({option})");
Driver.FindElement(By.Id("generate-wallet-link")).Click();
Driver.FindElement(By.Id($"generate-{option}-link")).Click();
}
else
{
var option = privkeys ? "Hotwallet" : "Watchonly";
Logs.Tester.LogInformation($"Generating new seed ({option})");
Driver.FindElement(By.Id("GenerateWalletLink")).Click();
Driver.FindElement(By.Id($"Generate{option}Link")).Click();
Logs.Tester.LogInformation("Progressing with existing seed");
Driver.FindElement(By.Id("import-wallet-options-link")).Click();
Driver.FindElement(By.Id("import-seed-link")).Click();
Driver.FindElement(By.Id("ExistingMnemonic")).SendKeys(seed);
SetCheckbox(Driver.FindElement(By.Id("SavePrivateKeys")), privkeys);
}
Driver.FindElement(By.Id("ScriptPubKeyType")).Click();
Driver.FindElement(By.CssSelector($"#ScriptPubKeyType option[value={format}]")).Click();
Driver.ToggleCollapse("AdvancedSettings");
Driver.SetCheckbox(By.Id("ImportKeysToRPC"), importkeys);
Driver.FindElement(By.Id("advanced-settings-button")).Click();
SetCheckbox(Driver.FindElement(By.Id("ImportKeysToRPC")), importkeys);
Driver.FindElement(By.Id("advanced-settings-button")).Click(); // close settings again , otherwise the button might not be clickable for Selenium
Logs.Tester.LogInformation("Trying to click Continue button");
Driver.FindElement(By.Id("Continue")).Click();
if (isImport)
// Seed backup page
FindAlertMessage();
if (string.IsNullOrEmpty(seed))
{
// Confirm addresses
Driver.FindElement(By.Id("Confirm")).Click();
seed = Driver.FindElements(By.Id("recovery-phrase")).First().GetAttribute("data-mnemonic");
}
else
{
// Seed backup
FindAlertMessage();
if (string.IsNullOrEmpty(seed))
{
seed = Driver.FindElements(By.Id("RecoveryPhrase")).First().GetAttribute("data-mnemonic");
}
// Confirm seed backup
Driver.FindElement(By.Id("confirm")).Click();
Driver.FindElement(By.Id("submit")).Click();
}
// Confirm seed backup
Driver.FindElement(By.Id("confirm")).Click();
Driver.FindElement(By.Id("submit")).Click();
WalletId = new WalletId(StoreId, cryptoCode);
return new Mnemonic(seed);
}
@ -187,57 +173,41 @@ namespace BTCPayServer.Tests
public void AddDerivationScheme(string cryptoCode = "BTC", string derivationScheme = "xpub661MyMwAqRbcGABgHMUXDzPzH1tU7eZaAaJQXhDXsSxsqyQzQeU6kznNfSuAyqAK9UaWSaZaMFdNiY5BCF4zBPAzSnwfUAwUhwttuAKwfRX-[legacy]")
{
Driver.FindElement(By.Id($"Modify{cryptoCode}")).Click();
Driver.FindElement(By.Id("ImportWalletOptionsLink")).Click();
Driver.FindElement(By.Id("ImportXpubLink")).Click();
Driver.FindElement(By.Id("import-wallet-options-link")).Click();
Driver.FindElement(By.Id("import-xpub-link")).Click();
Driver.FindElement(By.Id("DerivationScheme")).SendKeys(derivationScheme);
Driver.FindElement(By.Id("Continue")).Click();
Driver.FindElement(By.Id("Confirm")).Click();
FindAlertMessage();
}
public void AddLightningNode(string cryptoCode = "BTC", LightningConnectionType? connectionType = null)
public void AddLightningNode(string cryptoCode, LightningConnectionType connectionType)
{
string connectionString;
if (connectionType == LightningConnectionType.Charge)
connectionString = $"type=charge;server={Server.MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true";
else if (connectionType == LightningConnectionType.CLightning)
connectionString = "type=clightning;server=" + ((CLightningClient)Server.MerchantLightningD).Address.AbsoluteUri;
else if (connectionType == LightningConnectionType.LndREST)
connectionString = $"type=lnd-rest;server={Server.MerchantLnd.Swagger.BaseUrl};allowinsecure=true";
else
throw new NotSupportedException(connectionType.ToString());
Driver.FindElement(By.Id($"Modify-Lightning{cryptoCode}")).Click();
Driver.FindElement(By.Name($"ConnectionString")).SendKeys(connectionString);
Driver.FindElement(By.Id($"save")).Click();
}
public void AddInternalLightningNode(string cryptoCode)
{
Driver.FindElement(By.Id($"Modify-Lightning{cryptoCode}")).Click();
var connectionString = connectionType switch
{
LightningConnectionType.Charge =>
$"type=charge;server={Server.MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true",
LightningConnectionType.CLightning =>
$"type=clightning;server={((CLightningClient) Server.MerchantLightningD).Address.AbsoluteUri}",
LightningConnectionType.LndREST =>
$"type=lnd-rest;server={Server.MerchantLnd.Swagger.BaseUrl};allowinsecure=true",
_ => null
};
if (connectionString == null)
{
Assert.True(Driver.FindElement(By.Id("LightningNodeType-Internal")).Enabled, "Usage of the internal Lightning node is disabled.");
Driver.FindElement(By.CssSelector("label[for=\"LightningNodeType-Internal\"]")).Click();
}
else
{
Driver.FindElement(By.CssSelector("label[for=\"LightningNodeType-Custom\"]")).Click();
Driver.FindElement(By.Id("ConnectionString")).SendKeys(connectionString);
Driver.FindElement(By.Id("test")).Click();
Assert.Contains("Connection to the Lightning node successful.", FindAlertMessage().Text);
}
Driver.FindElement(By.Id("save")).Click();
Assert.Contains($"{cryptoCode} Lightning node updated.", FindAlertMessage().Text);
var enabled = Driver.FindElement(By.Id($"{cryptoCode}LightningEnabled"));
if (enabled.Text == "Enable")
{
enabled.Click();
Assert.Contains($"{cryptoCode} Lightning payments are now enabled for this store.", FindAlertMessage().Text);
}
Driver.FindElement(By.Id($"internal-ln-node-setter")).Click();
Driver.FindElement(By.Id($"save")).Click();
}
public void ClickOnAllSideMenus()
{
var links = Driver.FindElements(By.CssSelector(".nav .nav-link")).Select(c => c.GetAttribute("href")).ToList();
var links = Driver.FindElements(By.CssSelector(".nav-pills .nav-link")).Select(c => c.GetAttribute("href")).ToList();
Driver.AssertNoError();
Assert.NotEmpty(links);
foreach (var l in links)
@ -312,6 +282,26 @@ namespace BTCPayServer.Tests
CheckForJSErrors();
}
public void SetCheckbox(IWebElement element, bool value)
{
if ((value && !element.Selected) || (!value && element.Selected))
{
element.Click();
}
if (value != element.Selected)
{
Logs.Tester.LogInformation("SetCheckbox recursion, trying to click again");
SetCheckbox(element, value);
}
}
public void SetCheckbox(SeleniumTester s, string checkboxId, bool value)
{
SetCheckbox(s.Driver.FindElement(By.Id(checkboxId)), value);
}
public void GoToInvoices()
{
Driver.FindElement(By.Id("Invoices")).Click();
@ -331,12 +321,11 @@ namespace BTCPayServer.Tests
Driver.Navigate().GoToUrl(new Uri(Server.PayTester.ServerUri, "/login"));
}
public string CreateInvoice(string storeName, decimal? amount = 100, string currency = "USD", string refundEmail = "")
public string CreateInvoice(string storeName, decimal amount = 100, string currency = "USD", string refundEmail = "")
{
GoToInvoices();
Driver.FindElement(By.Id("CreateNewInvoice")).Click();
if (amount is decimal v)
Driver.FindElement(By.Id("Amount")).SendKeys(v.ToString(CultureInfo.InvariantCulture));
Driver.FindElement(By.Id("Amount")).SendKeys(amount.ToString(CultureInfo.InvariantCulture));
var currencyEl = Driver.FindElement(By.Id("Currency"));
currencyEl.Clear();
currencyEl.SendKeys(currency);
@ -344,7 +333,8 @@ namespace BTCPayServer.Tests
Driver.FindElement(By.Name("StoreId")).SendKeys(storeName);
Driver.FindElement(By.Id("Create")).Click();
var statusElement = FindAlertMessage();
FindAlertMessage();
var statusElement = Driver.FindElement(By.ClassName("alert-success"));
var id = statusElement.Text.Split(" ")[1];
return id;
}
@ -373,22 +363,22 @@ namespace BTCPayServer.Tests
Driver.FindElement(By.Id("bip21parse")).Click();
Driver.SwitchTo().Alert().SendKeys(bip21);
Driver.SwitchTo().Alert().Accept();
Driver.FindElement(By.Id("SignTransaction")).Click();
Driver.FindElement(By.Id("SignWithSeed")).Click();
Driver.FindElement(By.Id("SendMenu")).Click();
Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click();
Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
}
private void CheckForJSErrors()
{
//wait for seleniun update: https://stackoverflow.com/questions/57520296/selenium-webdriver-3-141-0-driver-manage-logs-availablelogtypes-throwing-syste
// var errorStrings = new List<string>
// {
// "SyntaxError",
// "EvalError",
// "ReferenceError",
// "RangeError",
// "TypeError",
// "URIError"
// var errorStrings = new List<string>
// {
// "SyntaxError",
// "EvalError",
// "ReferenceError",
// "RangeError",
// "TypeError",
// "URIError"
// };
//
// var jsErrors = Driver.Manage().Logs.GetLog(LogType.Browser).Where(x => errorStrings.Any(e => x.Message.Contains(e)));
@ -415,7 +405,7 @@ namespace BTCPayServer.Tests
{
Driver.Navigate().GoToUrl(new Uri(Server.PayTester.ServerUri, relativeUrl));
}
public void GoToServer(ServerNavPages navPages = ServerNavPages.Index)
{
Driver.FindElement(By.Id("ServerSettings")).Click();

View File

@ -1,5 +1,4 @@
using System;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Text;
@ -8,12 +7,10 @@ using System.Threading.Tasks;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Services;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Tests.Logging;
using BTCPayServer.Views.Manage;
using BTCPayServer.Views.Server;
using BTCPayServer.Views.Stores;
using BTCPayServer.Views.Wallets;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
@ -78,16 +75,16 @@ namespace BTCPayServer.Tests
Assert.Contains("server/services/lndseedbackup/BTC", s.Driver.PageSource);
s.Driver.Navigate().GoToUrl(s.Link("/server/services/lndseedbackup/BTC"));
s.Driver.FindElement(By.Id("details")).Click();
var seedEl = s.Driver.FindElement(By.Id("Seed"));
var seedEl = s.Driver.FindElement(By.Id("SeedTextArea"));
Assert.True(seedEl.Displayed);
Assert.Contains("about over million", seedEl.Text, StringComparison.OrdinalIgnoreCase);
var passEl = s.Driver.FindElement(By.Id("WalletPassword"));
var passEl = s.Driver.FindElement(By.Id("PasswordInput"));
Assert.True(passEl.Displayed);
Assert.Contains(passEl.Text, "hellorockstar", StringComparison.OrdinalIgnoreCase);
s.Driver.FindElement(By.Id("delete")).Click();
s.Driver.FindElement(By.Id("continue")).Click();
s.FindAlertMessage();
seedEl = s.Driver.FindElement(By.Id("Seed"));
seedEl = s.Driver.FindElement(By.Id("SeedTextArea"));
Assert.Contains("Seed removed", seedEl.Text, StringComparison.OrdinalIgnoreCase);
}
}
@ -116,7 +113,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("Email")).SendKeys(u2.RegisterDetails.Email);
s.Driver.FindElement(By.Id("save")).Click();
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Error);
s.FindAlertMessage(Abstractions.Models.StatusMessageModel.StatusSeverity.Error);
s.GoToProfile(ManageNavPages.Index);
s.Driver.FindElement(By.Id("Email")).Clear();
@ -214,10 +211,6 @@ namespace BTCPayServer.Tests
using (var s = SeleniumTester.Create())
{
await s.StartAsync();
var settings = s.Server.PayTester.GetService<SettingsRepository>();
var policies = await settings.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
policies.DisableSSHService = false;
await settings.UpdateSetting(policies);
s.RegisterNewUser(isAdmin: true);
s.Driver.Navigate().GoToUrl(s.Link("/server/services"));
Assert.Contains("server/services/ssh", s.Driver.PageSource);
@ -246,17 +239,6 @@ namespace BTCPayServer.Tests
text = s.Driver.FindElement(By.Id("SSHKeyFileContent")).Text;
Assert.DoesNotContain("test2", text);
// Let's try to disable it now
s.Driver.FindElement(By.Id("disable")).Click();
s.Driver.FindElement(By.Id("continue")).Click();
policies = await settings.GetSettingAsync<PoliciesSettings>();
Assert.True(policies.DisableSSHService);
s.Driver.Navigate().GoToUrl(s.Link("/server/services/ssh"));
Assert.True(s.Driver.PageSource.Contains("404 - Page not found", StringComparison.OrdinalIgnoreCase));
policies.DisableSSHService = false;
await settings.UpdateSetting(policies);
}
}
@ -331,15 +313,13 @@ namespace BTCPayServer.Tests
}
[Fact(Timeout = TestTimeout)]
[Trait("Lightning", "Lightning")]
public async Task CanCreateStores()
{
using (var s = SeleniumTester.Create())
{
s.Server.ActivateLightning();
await s.StartAsync();
var alice = s.RegisterNewUser(true);
var (storeName, storeId) = s.CreateNewStore();
var alice = s.RegisterNewUser();
var storeData = s.CreateNewStore();
var onchainHint = "Set up your wallet to receive payments at your store.";
var offchainHint = "A connection to a Lightning node is required to receive Lightning payments.";
@ -348,31 +328,23 @@ namespace BTCPayServer.Tests
Assert.True(s.Driver.PageSource.Contains(offchainHint), "Lightning hint not present");
s.GoToStores();
Assert.True(s.Driver.PageSource.Contains($"warninghint_{storeId}"), "Warning hint on list not present");
Assert.True(s.Driver.PageSource.Contains("warninghint_" + storeData.storeId),
"Warning hint on list not present");
s.GoToStore(storeId);
Assert.Contains(storeName, s.Driver.PageSource);
s.GoToStore(storeData.storeId);
Assert.True(s.Driver.PageSource.Contains(onchainHint), "Wallet hint should be present at this point");
Assert.True(s.Driver.PageSource.Contains(offchainHint), "Lightning hint should be present at this point");
// setup onchain wallet
s.GoToStore(storeId);
s.AddDerivationScheme();
s.AddDerivationScheme(); // wallet hint should be dismissed
s.Driver.AssertNoError();
Assert.False(s.Driver.PageSource.Contains(onchainHint), "Wallet hint not dismissed on derivation scheme add");
// setup offchain wallet
s.GoToStore(storeId);
s.AddLightningNode();
s.Driver.AssertNoError();
var successAlert = s.FindAlertMessage();
Assert.Contains("BTC Lightning node updated.", successAlert.Text);
Assert.False(s.Driver.PageSource.Contains(offchainHint), "Lightning hint should be dismissed at this point");
Assert.False(s.Driver.PageSource.Contains(onchainHint),
"Wallet hint not dismissed on derivation scheme add");// dismiss lightning hint
Assert.Contains(storeData.storeName, s.Driver.PageSource);
var storeUrl = s.Driver.Url;
s.ClickOnAllSideMenus();
s.GoToInvoices();
var invoiceId = s.CreateInvoice(storeName);
var invoiceId = s.CreateInvoice(storeData.storeName);
s.FindAlertMessage();
s.Driver.FindElement(By.ClassName("invoice-details-link")).Click();
var invoiceUrl = s.Driver.Url;
@ -427,6 +399,10 @@ namespace BTCPayServer.Tests
s.Logout();
s.LogIn(alice);
s.Driver.FindElement(By.Id("Stores")).Click();
// there shouldn't be any hints now
Assert.False(s.Driver.PageSource.Contains(offchainHint), "Lightning hint should be dismissed at this point");
s.Driver.FindElement(By.LinkText("Remove")).Click();
s.Driver.FindElement(By.Id("continue")).Click();
s.Driver.FindElement(By.Id("Stores")).Click();
@ -498,25 +474,16 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("Apps")).Click();
s.Driver.FindElement(By.Id("CreateNewApp")).Click();
s.Driver.FindElement(By.Name("Name")).SendKeys("PoS" + Guid.NewGuid());
s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("Point of Sale");
s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("PointOfSale");
s.Driver.FindElement(By.Id("SelectedStore")).SendKeys(storeName);
s.Driver.FindElement(By.Id("Create")).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("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);
s.Driver.FindElement(By.Id("DefaultView")).SendKeys("Item list and cart");
s.Driver.FindElement(By.Id("DefaultView")).SendKeys("Cart");
s.Driver.FindElement(By.Id("SaveSettings")).Click();
s.Driver.FindElement(By.Id("ViewApp")).Click();
var posBaseUrl = s.Driver.Url.Replace("/Cart", "");
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");
s.Driver.Url = posBaseUrl + "/static";
Assert.False(s.Driver.PageSource.Contains("Cart"), "Static PoS not showing correct view");
@ -574,6 +541,7 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = TestTimeout)]
public async Task CanUseCoinSelection()
{
@ -602,16 +570,15 @@ namespace BTCPayServer.Tests
var x = store.GetSupportedPaymentMethods(s.Server.NetworkProvider)
.OfType<DerivationSchemeSettings>()
.Single(settings => settings.PaymentId.CryptoCode == walletId.CryptoCode);
var wallet = s.Server.PayTester.GetService<BTCPayWalletProvider>().GetWallet(walletId.CryptoCode);
wallet.InvalidateCache(x.AccountDerivation);
Assert.Contains(
await wallet.GetUnspentCoins(x.AccountDerivation),
await s.Server.PayTester.GetService<BTCPayWalletProvider>().GetWallet(walletId.CryptoCode)
.GetUnspentCoins(x.AccountDerivation),
coin => coin.OutPoint == spentOutpoint);
});
await s.Server.ExplorerNode.GenerateAsync(1);
s.GoToWallet(walletId);
s.Driver.ToggleCollapse("AdvancedSettings");
s.Driver.WaitForAndClick(By.Id("toggleInputSelection"));
s.Driver.FindElement(By.Id("advancedSettings")).Click();
s.Driver.FindElement(By.Id("toggleInputSelection")).Click();
s.Driver.FindElement(By.Id(spentOutpoint.ToString()));
Assert.Equal("true", s.Driver.FindElement(By.Name("InputSelection")).GetAttribute("value").ToLowerInvariant());
var el = s.Driver.FindElement(By.Id(spentOutpoint.ToString()));
@ -621,7 +588,8 @@ namespace BTCPayServer.Tests
var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest);
SetTransactionOutput(s, 0, bob, 0.3m);
s.Driver.FindElement(By.Id("SignTransaction")).Click();
s.Driver.FindElement(By.Id("SendMenu")).Click();
s.Driver.FindElement(By.Id("spendWithNBxplorer")).Click();
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
var happyElement = s.FindAlertMessage();
var happyText = happyElement.Text;
@ -641,14 +609,15 @@ namespace BTCPayServer.Tests
await s.StartAsync();
s.RegisterNewUser(true);
var (storeName, storeId) = s.CreateNewStore();
s.GoToStore(storeId, StoreNavPages.Webhooks);
s.GoToStore(storeId, Views.Stores.StoreNavPages.Webhooks);
Logs.Tester.LogInformation("Let's create two webhooks");
for (var i = 0; i < 2; i++)
{
s.Driver.FindElement(By.Id("CreateWebhook")).Click();
s.Driver.FindElement(By.Name("PayloadUrl")).SendKeys($"http://127.0.0.1/callback{i}");
new SelectElement(s.Driver.FindElement(By.Id("Everything"))).SelectByValue("false");
new SelectElement(s.Driver.FindElement(By.Name("Everything")))
.SelectByValue("false");
s.Driver.FindElement(By.Id("InvoiceCreated")).Click();
s.Driver.FindElement(By.Id("InvoiceProcessing")).Click();
s.Driver.FindElement(By.Name("add")).Click();
@ -742,8 +711,8 @@ namespace BTCPayServer.Tests
Logs.Tester.LogInformation("Let's see if we can delete store with some webhooks inside");
s.GoToStore(storeId);
s.Driver.ToggleCollapse("danger-zone");
s.Driver.ExecuteJavaScript("window.scrollBy(0,1000);");
s.Driver.FindElement(By.Id("danger-zone-expander")).Click();
s.Driver.FindElement(By.Id("delete-store")).Click();
s.Driver.FindElement(By.Id("continue")).Click();
s.FindAlertMessage();
@ -767,7 +736,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("Wallets")).Click();
s.Driver.FindElement(By.LinkText("Manage")).Click();
s.Driver.FindElement(By.Id("WalletSend")).Click();
s.Driver.FindElement(By.Id("SignTransaction")).Click();
s.Driver.FindElement(By.Id("SendMenu")).Click();
//you cannot use the Sign with NBX option without saving private keys when generating the wallet.
Assert.DoesNotContain("nbx-seed", s.Driver.PageSource);
@ -831,11 +800,6 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.LinkText("Manage")).Click();
s.ClickOnAllSideMenus();
// Make sure wallet info is correct
s.Driver.FindElement(By.Id("WalletSettings")).Click();
Assert.Contains(mnemonic.DeriveExtKey().GetPublicKey().GetHDFingerPrint().ToString(), s.Driver.FindElement(By.Id("AccountKeys_0__MasterFingerprint")).GetAttribute("value"));
Assert.Contains("m/84'/1'/0'", s.Driver.FindElement(By.Id("AccountKeys_0__AccountKeyPath")).GetAttribute("value"));
// Make sure we can rescan, because we are admin!
s.Driver.FindElement(By.Id("WalletRescan")).Click();
@ -843,24 +807,36 @@ namespace BTCPayServer.Tests
// We setup the fingerprint and the account key path
s.Driver.FindElement(By.Id("WalletSettings")).Click();
// s.Driver.FindElement(By.Id("AccountKeys_0__MasterFingerprint")).SendKeys("8bafd160");
// s.Driver.FindElement(By.Id("AccountKeys_0__AccountKeyPath")).SendKeys("m/49'/0'/0'" + Keys.Enter);
// Check the tx sent earlier arrived
s.Driver.FindElement(By.Id("WalletTransactions")).Click();
var walletTransactionLink = s.Driver.Url;
Assert.Contains(tx.ToString(), s.Driver.PageSource);
// Send to bob
s.Driver.FindElement(By.Id("WalletSend")).Click();
var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest);
SetTransactionOutput(s, 0, bob, 1);
s.Driver.FindElement(By.Id("SignTransaction")).Click();
// Broadcast
Assert.Contains(bob.ToString(), s.Driver.PageSource);
Assert.Contains("1.00000000", s.Driver.PageSource);
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
Assert.Equal(walletTransactionLink, s.Driver.Url);
void SignWith(Mnemonic signingSource)
{
// Send to bob
s.Driver.FindElement(By.Id("WalletSend")).Click();
var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest);
SetTransactionOutput(s, 0, bob, 1);
s.Driver.FindElement(By.Id("SendMenu")).Click();
s.Driver.FindElement(By.CssSelector("button[value=seed]")).Click();
// Input the seed
s.Driver.FindElement(By.Id("SeedOrKey")).SendKeys(signingSource + Keys.Enter);
// Broadcast
Assert.Contains(bob.ToString(), s.Driver.PageSource);
Assert.Contains("1.00000000", s.Driver.PageSource);
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
Assert.Equal(walletTransactionLink, s.Driver.Url);
}
SignWith(mnemonic);
s.Driver.FindElement(By.Id("Wallets")).Click();
s.Driver.FindElement(By.LinkText("Manage")).Click();
@ -868,16 +844,15 @@ namespace BTCPayServer.Tests
var jack = new Key().PubKey.Hash.GetAddress(Network.RegTest);
SetTransactionOutput(s, 0, jack, 0.01m);
s.Driver.FindElement(By.Id("SignTransaction")).Click();
s.Driver.FindElement(By.Id("SendMenu")).Click();
s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click();
Assert.Contains(jack.ToString(), s.Driver.PageSource);
Assert.Contains("0.01000000", s.Driver.PageSource);
s.Driver.FindElement(By.CssSelector("button[value=analyze-psbt]")).Click();
Assert.EndsWith("psbt", s.Driver.Url);
s.Driver.FindElement(By.Id("OtherActionsDropdownToggle")).Click();
s.Driver.FindElement(By.CssSelector("#OtherActions")).Click();
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
Assert.EndsWith("psbt/ready", s.Driver.Url);
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
Assert.Equal(walletTransactionLink, s.Driver.Url);
@ -893,16 +868,17 @@ namespace BTCPayServer.Tests
s.Driver.SwitchTo().Alert().SendKeys(bip21);
s.Driver.SwitchTo().Alert().Accept();
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Info);
Assert.Equal(parsedBip21.Amount.ToString(false), s.Driver.FindElement(By.Id("Outputs_0__Amount")).GetAttribute("value"));
Assert.Equal(parsedBip21.Address.ToString(), s.Driver.FindElement(By.Id("Outputs_0__DestinationAddress")).GetAttribute("value"));
Assert.Equal(parsedBip21.Amount.ToString(false), s.Driver.FindElement(By.Id($"Outputs_0__Amount")).GetAttribute("value"));
Assert.Equal(parsedBip21.Address.ToString(), s.Driver.FindElement(By.Id($"Outputs_0__DestinationAddress")).GetAttribute("value"));
s.GoToWallet(new WalletId(storeId, "BTC"), WalletsNavPages.Settings);
var walletUrl = s.Driver.Url;
s.Driver.FindElement(By.Id("OtherActionsDropdownToggle")).Click();
s.Driver.FindElement(By.Id("SettingsMenu")).Click();
s.Driver.FindElement(By.CssSelector("button[value=view-seed]")).Click();
// Seed backup page
var recoveryPhrase = s.Driver.FindElements(By.Id("RecoveryPhrase")).First().GetAttribute("data-mnemonic");
var recoveryPhrase = s.Driver.FindElements(By.Id("recovery-phrase")).First().GetAttribute("data-mnemonic");
Assert.Equal(mnemonic.ToString(), recoveryPhrase);
Assert.Contains("The recovery phrase will also be stored on the server as a hot wallet.", s.Driver.PageSource);
@ -913,179 +889,116 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = TestTimeout)]
public async Task CanImportWallet()
[Fact]
[Trait("Selenium", "Selenium")]
public async Task CanUsePullPaymentsViaUI()
{
using (var s = SeleniumTester.Create())
{
await s.StartAsync();
s.RegisterNewUser(true);
var (_, storeId) = s.CreateNewStore();
var mnemonic = s.GenerateWallet("BTC", "click chunk owner kingdom faint steak safe evidence bicycle repeat bulb wheel");
// Make sure wallet info is correct
s.GoToWallet(new WalletId(storeId, "BTC"), WalletsNavPages.Settings);
Assert.Contains(mnemonic.DeriveExtKey().GetPublicKey().GetHDFingerPrint().ToString(), s.Driver.FindElement(By.Id("AccountKeys_0__MasterFingerprint")).GetAttribute("value"));
Assert.Contains( "m/84'/1'/0'", s.Driver.FindElement(By.Id("AccountKeys_0__AccountKeyPath")).GetAttribute("value"));
}
}
s.CreateNewStore();
s.GenerateWallet("BTC", "", true, true);
[Fact]
[Trait("Selenium", "Selenium")]
public async Task CanUsePullPaymentsViaUI()
{
using var s = SeleniumTester.Create();
await s.StartAsync();
s.RegisterNewUser(true);
s.CreateNewStore();
s.GenerateWallet("BTC", "", true, true);
await s.Server.ExplorerNode.GenerateAsync(1);
await s.FundStoreWallet(denomination: 50.0m);
s.GoToWallet(navPages: WalletsNavPages.PullPayments);
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
s.Driver.FindElement(By.Id("Name")).SendKeys("PP1");
s.Driver.FindElement(By.Id("Amount")).Clear();
s.Driver.FindElement(By.Id("Amount")).SendKeys("99.0");;
s.Driver.FindElement(By.Id("Create")).Click();
s.Driver.FindElement(By.LinkText("View")).Click();
await s.Server.ExplorerNode.GenerateAsync(1);
await s.FundStoreWallet(denomination: 50.0m);
s.GoToWallet(navPages: WalletsNavPages.PullPayments);
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
s.Driver.FindElement(By.Id("Name")).SendKeys("PP1");
s.Driver.FindElement(By.Id("Amount")).Clear();
s.Driver.FindElement(By.Id("Amount")).SendKeys("99.0");;
s.Driver.FindElement(By.Id("Create")).Click();
s.Driver.FindElement(By.LinkText("View")).Click();
s.GoToWallet(navPages: WalletsNavPages.PullPayments);
s.GoToWallet(navPages: WalletsNavPages.PullPayments);
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
s.Driver.FindElement(By.Id("Name")).SendKeys("PP2");
s.Driver.FindElement(By.Id("Amount")).Clear();
s.Driver.FindElement(By.Id("Amount")).SendKeys("100.0");
s.Driver.FindElement(By.Id("Create")).Click();
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
s.Driver.FindElement(By.Id("Name")).SendKeys("PP2");
s.Driver.FindElement(By.Id("Amount")).Clear();
s.Driver.FindElement(By.Id("Amount")).SendKeys("100.0");
s.Driver.FindElement(By.Id("Create")).Click();
// This should select the first View, ie, the last one PP2
s.Driver.FindElement(By.LinkText("View")).Click();
var address = await s.Server.ExplorerNode.GetNewAddressAsync();
s.Driver.FindElement(By.Id("Destination")).SendKeys(address.ToString());
s.Driver.FindElement(By.Id("ClaimedAmount")).Clear();
s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys("15" + Keys.Enter);
s.FindAlertMessage();
// This should select the first View, ie, the last one PP2
s.Driver.FindElement(By.LinkText("View")).Click();
var address = await s.Server.ExplorerNode.GetNewAddressAsync();
s.Driver.FindElement(By.Id("Destination")).SendKeys(address.ToString());
s.Driver.FindElement(By.Id("ClaimedAmount")).Clear();
s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys("15" + Keys.Enter);
s.FindAlertMessage();
// We should not be able to use an address already used
s.Driver.FindElement(By.Id("Destination")).SendKeys(address.ToString());
s.Driver.FindElement(By.Id("ClaimedAmount")).Clear();
s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys("20" + Keys.Enter);
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Error);
// We should not be able to use an address already used
s.Driver.FindElement(By.Id("Destination")).SendKeys(address.ToString());
s.Driver.FindElement(By.Id("ClaimedAmount")).Clear();
s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys("20" + Keys.Enter);
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Error);
address = await s.Server.ExplorerNode.GetNewAddressAsync();
s.Driver.FindElement(By.Id("Destination")).Clear();
s.Driver.FindElement(By.Id("Destination")).SendKeys(address.ToString());
s.Driver.FindElement(By.Id("ClaimedAmount")).Clear();
s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys("20" + Keys.Enter);
s.FindAlertMessage();
Assert.Contains("Awaiting Approval", s.Driver.PageSource);
address = await s.Server.ExplorerNode.GetNewAddressAsync();
s.Driver.FindElement(By.Id("Destination")).Clear();
s.Driver.FindElement(By.Id("Destination")).SendKeys(address.ToString());
s.Driver.FindElement(By.Id("ClaimedAmount")).Clear();
s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys("20" + Keys.Enter);
s.FindAlertMessage();
Assert.Contains("Awaiting Approval", s.Driver.PageSource);
var viewPullPaymentUrl = s.Driver.Url;
// This one should have nothing
s.GoToWallet(navPages: WalletsNavPages.PullPayments);
var payouts = s.Driver.FindElements(By.ClassName("pp-payout"));
Assert.Equal(2, payouts.Count);
payouts[1].Click();
Assert.Contains("No payout waiting for approval", s.Driver.PageSource);
var viewPullPaymentUrl = s.Driver.Url;
// This one should have nothing
s.GoToWallet(navPages: WalletsNavPages.PullPayments);
var payouts = s.Driver.FindElements(By.ClassName("pp-payout"));
Assert.Equal(2, payouts.Count);
payouts[1].Click();
Assert.Empty(s.Driver.FindElements(By.ClassName("payout")));
// PP2 should have payouts
s.GoToWallet(navPages: WalletsNavPages.PullPayments);
payouts = s.Driver.FindElements(By.ClassName("pp-payout"));
payouts[0].Click();
Assert.NotEmpty(s.Driver.FindElements(By.ClassName("payout")));
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-selectAllCheckbox")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-actions")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-approve-pay")).Click();
// PP2 should have payouts
s.GoToWallet(navPages: WalletsNavPages.PullPayments);
payouts = s.Driver.FindElements(By.ClassName("pp-payout"));
payouts[0].Click();
Assert.DoesNotContain("No payout waiting for approval", s.Driver.PageSource);
s.Driver.FindElement(By.Id("selectAllCheckbox")).Click();
s.Driver.FindElement(By.Id("payCommand")).Click();
s.Driver.FindElement(By.Id("SendMenu")).Click();
s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click();
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
s.Driver.FindElement(By.Id("SignTransaction")).Click();
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
s.FindAlertMessage();
s.FindAlertMessage();
TestUtils.Eventually(() =>
{
s.Driver.Navigate().Refresh();
Assert.Contains("badge transactionLabel", s.Driver.PageSource);
});
Assert.Equal("payout", s.Driver.FindElement(By.ClassName("transactionLabel")).Text);
TestUtils.Eventually(() =>
{
s.Driver.Navigate().Refresh();
Assert.Contains("badge transactionLabel", s.Driver.PageSource);
});
Assert.Equal("payout", s.Driver.FindElement(By.ClassName("transactionLabel")).Text);
s.GoToWallet(navPages: WalletsNavPages.Payouts);
var x = s.Driver.PageSource;
s.Driver.FindElement(By.Id($"{PayoutState.InProgress}-view")).Click();
ReadOnlyCollection<IWebElement> txs;
TestUtils.Eventually(() =>
{
s.Driver.Navigate().Refresh();
s.GoToWallet(navPages: WalletsNavPages.Payouts);
TestUtils.Eventually(() =>
{
s.Driver.Navigate().Refresh();
Assert.Contains("No payout waiting for approval", s.Driver.PageSource);
});
var txs = s.Driver.FindElements(By.ClassName("transaction-link"));
Assert.Equal(2, txs.Count);
s.Driver.Navigate().GoToUrl(viewPullPaymentUrl);
txs = s.Driver.FindElements(By.ClassName("transaction-link"));
Assert.Equal(2, txs.Count);
});
Assert.Contains("In Progress", s.Driver.PageSource);
s.Driver.Navigate().GoToUrl(viewPullPaymentUrl);
txs = s.Driver.FindElements(By.ClassName("transaction-link"));
Assert.Equal(2, txs.Count);
Assert.Contains(PayoutState.InProgress.GetStateString(), s.Driver.PageSource);
await s.Server.ExplorerNode.GenerateAsync(1);
await s.Server.ExplorerNode.GenerateAsync(1);
TestUtils.Eventually(() =>
{
s.Driver.Navigate().Refresh();
Assert.Contains("Completed", s.Driver.PageSource);
});
await s.Server.ExplorerNode.GenerateAsync(10);
var pullPaymentId = viewPullPaymentUrl.Split('/').Last();
TestUtils.Eventually(() =>
{
s.Driver.Navigate().Refresh();
Assert.Contains(PayoutState.Completed.GetStateString(), s.Driver.PageSource);
});
await s.Server.ExplorerNode.GenerateAsync(10);
var pullPaymentId = viewPullPaymentUrl.Split('/').Last();
await TestUtils.EventuallyAsync(async () =>
{
using var ctx = s.Server.PayTester.GetService<ApplicationDbContextFactory>().CreateContext();
var payoutsData = await ctx.Payouts.Where(p => p.PullPaymentDataId == pullPaymentId).ToListAsync();
Assert.True(payoutsData.All(p => p.State == PayoutState.Completed));
});
s.GoToHome();
//offline/external payout test
s.Driver.FindElement(By.Id("NotificationsDropdownToggle")).Click();
s.Driver.FindElement(By.CssSelector("#notificationsForm button")).Click();
var newStore = s.CreateNewStore();
s.GenerateWallet("BTC", "", true, true);
var newWalletId = new WalletId(newStore.storeId, "BTC");
s.GoToWallet(newWalletId, WalletsNavPages.PullPayments);
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
s.Driver.FindElement(By.Id("Name")).SendKeys("External Test");
s.Driver.FindElement(By.Id("Amount")).Clear();
s.Driver.FindElement(By.Id("Amount")).SendKeys("0.001");
s.Driver.FindElement(By.Id("Currency")).Clear();
s.Driver.FindElement(By.Id("Currency")).SendKeys("BTC");
s.Driver.FindElement(By.Id("Create")).Click();
s.Driver.FindElement(By.LinkText("View")).Click();
address = await s.Server.ExplorerNode.GetNewAddressAsync();
s.Driver.FindElement(By.Id("Destination")).SendKeys(address.ToString());
s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys(Keys.Enter);
s.FindAlertMessage();
Assert.Contains(PayoutState.AwaitingApproval.GetStateString(), s.Driver.PageSource);
s.GoToWallet(newWalletId, WalletsNavPages.Payouts);
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-view")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-selectAllCheckbox")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-actions")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingApproval}-approve")).Click();
s.FindAlertMessage();
var tx =await s.Server.ExplorerNode.SendToAddressAsync(address, Money.FromUnit(0.001m, MoneyUnit.BTC));
s.GoToWallet(newWalletId, WalletsNavPages.Payouts);
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-view")).Click();
Assert.Contains(PayoutState.AwaitingPayment.GetStateString(), s.Driver.PageSource);
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-selectAllCheckbox")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-actions")).Click();
s.Driver.FindElement(By.Id($"{PayoutState.AwaitingPayment}-mark-paid")).Click();
s.FindAlertMessage();
s.Driver.FindElement(By.Id("InProgress-view")).Click();
Assert.Contains(tx.ToString(), s.Driver.PageSource);
await TestUtils.EventuallyAsync(async () =>
{
using var ctx = s.Server.PayTester.GetService<ApplicationDbContextFactory>().CreateContext();
var payoutsData = await ctx.Payouts.Where(p => p.PullPaymentDataId == pullPaymentId).ToListAsync();
Assert.True(payoutsData.All(p => p.State == Data.PayoutState.Completed));
});
}
}
private static void CanBrowseContent(SeleniumTester s)
@ -1101,9 +1014,8 @@ namespace BTCPayServer.Tests
private static void CanSetupEmailCore(SeleniumTester s)
{
s.Driver.FindElement(By.Id("QuickFillDropdownToggle")).Click();
s.Driver.FindElement(By.ClassName("dropdown-toggle")).Click();
s.Driver.FindElement(By.ClassName("dropdown-item")).Click();
s.Driver.FindElement(By.Id("Settings_Login")).SendKeys("test@gmail.com");
s.Driver.FindElement(By.CssSelector("button[value=\"Save\"]")).Submit();
s.FindAlertMessage();

View File

@ -7,8 +7,6 @@ using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Tests.Lnd;
using BTCPayServer.Tests.Logging;
using NBitcoin;
@ -88,10 +86,6 @@ namespace BTCPayServer.Tests
#endif
public void ActivateLightning()
{
ActivateLightning(LightningConnectionType.Charge);
}
public void ActivateLightning(LightningConnectionType internalNode)
{
var btc = NetworkProvider.GetNetwork<BTCPayNetwork>("BTC").NBitcoinNetwork;
CustomerLightningD = LightningClientFactory.CreateClient(GetEnvironment("TEST_CUSTOMERLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30992/"), btc);
@ -99,39 +93,7 @@ namespace BTCPayServer.Tests
MerchantCharge = new ChargeTester(this, "TEST_MERCHANTCHARGE", "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify;allowinsecure=true", "merchant_lightningd", btc);
MerchantLnd = new LndMockTester(this, "TEST_MERCHANTLND", "http://lnd:lnd@127.0.0.1:35531/", "merchant_lnd", btc);
PayTester.UseLightning = true;
PayTester.IntegratedLightning = GetLightningConnectionString(internalNode, true);
}
public string GetLightningConnectionString(LightningConnectionType? connectionType, bool isMerchant)
{
string connectionString = null;
if (connectionType is null)
return LightningSupportedPaymentMethod.InternalNode;
if (connectionType == LightningConnectionType.Charge)
{
if (isMerchant)
connectionString = $"type=charge;server={MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true";
else
throw new NotSupportedException();
}
else if (connectionType == LightningConnectionType.CLightning)
{
if (isMerchant)
connectionString = "type=clightning;server=" +
((CLightningClient)MerchantLightningD).Address.AbsoluteUri;
else
connectionString = "type=clightning;server=" +
((CLightningClient)CustomerLightningD).Address.AbsoluteUri;
}
else if (connectionType == LightningConnectionType.LndREST)
{
if (isMerchant)
connectionString = $"type=lnd-rest;server={MerchantLnd.Swagger.BaseUrl};allowinsecure=true";
else
throw new NotSupportedException();
}
else
throw new NotSupportedException(connectionType.ToString());
return connectionString;
PayTester.IntegratedLightning = MerchantCharge.Client.Uri;
}
public bool Dockerized

View File

@ -10,10 +10,8 @@ using BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration;
using BTCPayServer.Storage.Services.Providers.FileSystemStorage.Configuration;
using BTCPayServer.Storage.ViewModels;
using BTCPayServer.Tests.Logging;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using Xunit;
using Xunit.Abstractions;
@ -182,27 +180,23 @@ namespace BTCPayServer.Tests
private async Task CanUploadRemoveFiles(ServerController controller)
{
var fileContent = "content";
List<IFormFile> fileList = new List<IFormFile>();
fileList.Add(TestUtils.GetFormFile("uploadtestfile1.txt", fileContent));
var uploadFormFileResult = Assert.IsType<RedirectToActionResult>(await controller.CreateFiles(fileList));
Assert.True(uploadFormFileResult.RouteValues.ContainsKey("fileIds"));
string[] uploadFileList = (string[])uploadFormFileResult.RouteValues["fileIds"];
var fileId = uploadFileList[0];
var uploadFormFileResult = Assert.IsType<RedirectToActionResult>(await controller.CreateFile(TestUtils.GetFormFile("uploadtestfile.txt", fileContent)));
Assert.True(uploadFormFileResult.RouteValues.ContainsKey("fileId"));
var fileId = uploadFormFileResult.RouteValues["fileId"].ToString();
Assert.Equal("Files", uploadFormFileResult.ActionName);
//check if file was uploaded and saved in db
var viewFilesViewModel =
Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files(new string[] { fileId })).Model);
Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files(fileId)).Model);
Assert.NotEmpty(viewFilesViewModel.Files);
Assert.True(viewFilesViewModel.DirectUrlByFiles.ContainsKey(fileId));
Assert.NotEmpty(viewFilesViewModel.DirectUrlByFiles[fileId]);
Assert.Equal(fileId, viewFilesViewModel.SelectedFileId);
Assert.NotEmpty(viewFilesViewModel.DirectFileUrl);
//verify file is available and the same
var net = new System.Net.WebClient();
var data = await net.DownloadStringTaskAsync(new Uri(viewFilesViewModel.DirectUrlByFiles[fileId]));
var data = await net.DownloadStringTaskAsync(new Uri(viewFilesViewModel.DirectFileUrl));
Assert.Equal(fileContent, data);
//create a temporary link to file
@ -234,8 +228,10 @@ namespace BTCPayServer.Tests
//attempt to fetch deleted file
viewFilesViewModel =
Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files(new string[] { fileId })).Model);
Assert.Null(viewFilesViewModel.DirectUrlByFiles);
Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files(fileId)).Model);
Assert.Null(viewFilesViewModel.DirectFileUrl);
Assert.Null(viewFilesViewModel.SelectedFileId);
}

View File

@ -14,7 +14,6 @@ using BTCPayServer.Lightning;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Models.AccountViewModels;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments.PayJoin.Sender;
using BTCPayServer.Services;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
@ -23,8 +22,6 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.CodeAnalysis.Operations;
using NBitcoin;
using BTCPayServer.BIP78.Sender;
using BTCPayServer.Payments.Lightning;
using NBitcoin.Payment;
using NBitpayClient;
using NBXplorer.DerivationStrategy;
@ -125,22 +122,28 @@ namespace BTCPayServer.Tests
CreateStoreAsync().GetAwaiter().GetResult();
}
public async Task SetNetworkFeeMode(NetworkFeeMode mode)
public void SetNetworkFeeMode(NetworkFeeMode mode)
{
await ModifyStore(store =>
ModifyStore((store) =>
{
store.NetworkFeeMode = mode;
});
}
public async Task ModifyStore(Action<StoreViewModel> modify)
public void ModifyStore(Action<StoreViewModel> modify)
{
var storeController = GetController<StoresController>();
var response = await storeController.UpdateStore();
StoreViewModel store = (StoreViewModel)((ViewResult)response).Model;
StoreViewModel store = (StoreViewModel)((ViewResult)storeController.UpdateStore()).Model;
modify(store);
storeController.UpdateStore(store).GetAwaiter().GetResult();
}
public Task ModifyStoreAsync(Action<StoreViewModel> modify)
{
var storeController = GetController<StoresController>();
StoreViewModel store = (StoreViewModel)((ViewResult)storeController.UpdateStore()).Model;
modify(store);
return storeController.UpdateStore(store);
}
public T GetController<T>(bool setImplicitStore = true) where T : Controller
{
@ -168,29 +171,38 @@ namespace BTCPayServer.Tests
}
public async Task<WalletId> RegisterDerivationSchemeAsync(string cryptoCode, ScriptPubKeyType segwit = ScriptPubKeyType.Legacy,
bool importKeysToNBX = false, bool importsKeysToBitcoinCore = false)
bool importKeysToNBX = false)
{
if (StoreId is null)
await CreateStoreAsync();
SupportedNetwork = parent.NetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var store = parent.PayTester.GetController<StoresController>(UserId, StoreId, true);
var generateRequest = new WalletSetupRequest
var store = parent.PayTester.GetController<StoresController>(UserId, StoreId);
GenerateWalletResponseV = await parent.ExplorerClient.GenerateWalletAsync(new GenerateWalletRequest()
{
ScriptPubKeyType = segwit,
SavePrivateKeys = importKeysToNBX,
ImportKeysToRPC = importsKeysToBitcoinCore
};
await store.GenerateWallet(StoreId, cryptoCode, WalletSetupMethod.HotWallet, generateRequest);
Assert.NotNull(store.GenerateWalletResponse);
GenerateWalletResponseV = store.GenerateWalletResponse;
});
await store.AddDerivationScheme(StoreId,
new DerivationSchemeViewModel()
{
Enabled = true,
CryptoCode = cryptoCode,
Network = SupportedNetwork,
RootFingerprint = GenerateWalletResponseV.AccountKeyPath.MasterFingerprint.ToString(),
RootKeyPath = SupportedNetwork.GetRootKeyPath(),
Source = "NBXplorer",
AccountKey = GenerateWalletResponseV.AccountHDKey.Neuter().ToWif(),
DerivationSchemeFormat = "BTCPay",
KeyPath = GenerateWalletResponseV.AccountKeyPath.KeyPath.ToString(),
DerivationScheme = DerivationScheme.ToString(),
Confirmation = true
}, cryptoCode);
return new WalletId(StoreId, cryptoCode);
}
public Task EnablePayJoin()
{
return ModifyStore(s => s.PayJoinEnabled = true);
return ModifyStoreAsync(s => s.PayJoinEnabled = true);
}
public GenerateWalletResponse GenerateWalletResponseV { get; set; }
@ -244,19 +256,40 @@ namespace BTCPayServer.Tests
{
RegisterLightningNodeAsync(cryptoCode, connectionType, isMerchant).GetAwaiter().GetResult();
}
public Task RegisterLightningNodeAsync(string cryptoCode, bool isMerchant = true, string storeId = null)
{
return RegisterLightningNodeAsync(cryptoCode, null, isMerchant, storeId);
}
public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType? connectionType, bool isMerchant = true, string storeId = null)
{
var storeController = GetController<StoresController>();
var connectionString = parent.GetLightningConnectionString(connectionType, isMerchant);
var nodeType = connectionString == LightningSupportedPaymentMethod.InternalNode ? LightningNodeType.Internal : LightningNodeType.Custom;
public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType connectionType, bool isMerchant = true)
{
var storeController = this.GetController<StoresController>();
await storeController.SetupLightningNode(storeId ?? StoreId,
new LightningNodeViewModel { ConnectionString = connectionString, LightningNodeType = nodeType, SkipPortTest = true }, "save", cryptoCode);
string connectionString = null;
if (connectionType == LightningConnectionType.Charge)
{
if (isMerchant)
connectionString = $"type=charge;server={parent.MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true";
else
throw new NotSupportedException();
}
else if (connectionType == LightningConnectionType.CLightning)
{
if (isMerchant)
connectionString = "type=clightning;server=" +
((CLightningClient)parent.MerchantLightningD).Address.AbsoluteUri;
else
connectionString = "type=clightning;server=" +
((CLightningClient)parent.CustomerLightningD).Address.AbsoluteUri;
}
else if (connectionType == LightningConnectionType.LndREST)
{
if (isMerchant)
connectionString = $"type=lnd-rest;server={parent.MerchantLnd.Swagger.BaseUrl};allowinsecure=true";
else
throw new NotSupportedException();
}
else
throw new NotSupportedException(connectionType.ToString());
await storeController.AddLightningNode(StoreId,
new LightningNodeViewModel() { ConnectionString = connectionString, SkipPortTest = true }, "save", "BTC");
if (storeController.ModelState.ErrorCount != 0)
Assert.False(true, storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage);
}
@ -325,7 +358,7 @@ namespace BTCPayServer.Tests
Logs.Tester.LogInformation($"Proposing {psbt.GetGlobalTransaction().GetHash()}");
if (expectedError is null && !senderError)
{
var proposed = await pjClient.RequestPayjoin(endpoint, new PayjoinWallet(settings), psbt, default);
var proposed = await pjClient.RequestPayjoin(endpoint, settings, psbt, default);
Logs.Tester.LogInformation($"Proposed payjoin is {proposed.GetGlobalTransaction().GetHash()}");
Assert.NotNull(proposed);
return proposed;
@ -334,11 +367,11 @@ namespace BTCPayServer.Tests
{
if (senderError)
{
await Assert.ThrowsAsync<PayjoinSenderException>(async () => await pjClient.RequestPayjoin(endpoint, new PayjoinWallet(settings), psbt, default));
await Assert.ThrowsAsync<PayjoinSenderException>(async () => await pjClient.RequestPayjoin(endpoint, settings, psbt, default));
}
else
{
var ex = await Assert.ThrowsAsync<PayjoinReceiverException>(async () => await pjClient.RequestPayjoin(endpoint, new PayjoinWallet(settings), psbt, default));
var ex = await Assert.ThrowsAsync<PayjoinReceiverException>(async () => await pjClient.RequestPayjoin(endpoint, settings, psbt, default));
var split = expectedError.Split('|');
Assert.Equal(split[0], ex.ErrorCode);
if (split.Length > 1)

View File

@ -0,0 +1,132 @@
using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Models.AccountViewModels;
using BTCPayServer.Tests.Logging;
using BTCPayServer.U2F;
using BTCPayServer.U2F.Models;
using Microsoft.AspNetCore.Mvc;
using U2F.Core.Models;
using U2F.Core.Utils;
using Xunit;
using Xunit.Abstractions;
namespace BTCPayServer.Tests
{
public class U2FTests
{
public const int TestTimeout = 60_000;
public U2FTests(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) { Name = "Tests" };
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task U2ftest()
{
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var accountController = tester.PayTester.GetController<AccountController>();
var manageController = user.GetController<ManageController>();
var mock = new MockU2FService(tester.PayTester.GetService<ApplicationDbContextFactory>());
manageController._u2FService = mock;
accountController._u2FService = mock;
Assert
.IsType<RedirectToActionResult>(await accountController.Login(new LoginViewModel()
{
Email = user.RegisterDetails.Email,
Password = user.RegisterDetails.Password
}));
Assert.Empty(Assert.IsType<U2FAuthenticationViewModel>(Assert
.IsType<ViewResult>(await manageController.U2FAuthentication()).Model).Devices);
var addDeviceVM = Assert.IsType<AddU2FDeviceViewModel>(Assert
.IsType<ViewResult>(manageController.AddU2FDevice("testdevice")).Model);
Assert.NotEmpty(addDeviceVM.Challenge);
Assert.Equal("testdevice", addDeviceVM.Name);
Assert.NotEmpty(addDeviceVM.Version);
Assert.Null(addDeviceVM.DeviceResponse);
var devReg = new DeviceRegistration(Guid.NewGuid().ToByteArray(), Guid.NewGuid().ToByteArray(),
Guid.NewGuid().ToByteArray(), 1);
mock.GetDevReg = () => devReg;
mock.StartedAuthentication = () =>
new StartedAuthentication("chocolate", addDeviceVM.AppId,
devReg.KeyHandle.ByteArrayToBase64String());
addDeviceVM.DeviceResponse = new RegisterResponse("ss",
Convert.ToBase64String(Encoding.UTF8.GetBytes("{typ:'x', challenge: 'fff'}"))).ToJson();
Assert
.IsType<RedirectToActionResult>(await manageController.AddU2FDevice(addDeviceVM));
Assert.Single(Assert.IsType<U2FAuthenticationViewModel>(Assert
.IsType<ViewResult>(await manageController.U2FAuthentication()).Model).Devices);
var secondaryLoginViewModel = Assert.IsType<SecondaryLoginViewModel>(Assert
.IsType<ViewResult>(await accountController.Login(new LoginViewModel()
{
Email = user.RegisterDetails.Email,
Password = user.RegisterDetails.Password
})).Model);
Assert.NotNull(secondaryLoginViewModel.LoginWithU2FViewModel);
Assert.Single(secondaryLoginViewModel.LoginWithU2FViewModel.Challenges);
Assert.Equal(secondaryLoginViewModel.LoginWithU2FViewModel.Challenge,
secondaryLoginViewModel.LoginWithU2FViewModel.Challenges.First().challenge);
secondaryLoginViewModel.LoginWithU2FViewModel.DeviceResponse = new AuthenticateResponse(
Convert.ToBase64String(Encoding.UTF8.GetBytes(
"{typ:'x', challenge: '" + secondaryLoginViewModel.LoginWithU2FViewModel.Challenge + "'}")),
"dd", devReg.KeyHandle.ByteArrayToBase64String()).ToJson();
Assert
.IsType<RedirectToActionResult>(
await accountController.LoginWithU2F(secondaryLoginViewModel.LoginWithU2FViewModel));
}
}
public class MockU2FService : U2FService
{
public Func<DeviceRegistration> GetDevReg;
public Func<StartedAuthentication> StartedAuthentication;
public MockU2FService(ApplicationDbContextFactory contextFactory) : base(contextFactory)
{
}
protected override StartedRegistration StartDeviceRegistrationCore(string appId)
{
return global::U2F.Core.Crypto.U2F.StartRegistration(appId);
}
protected override DeviceRegistration FinishRegistrationCore(StartedRegistration startedRegistration,
RegisterResponse registerResponse)
{
return GetDevReg();
}
protected override StartedAuthentication StartAuthenticationCore(string appId, U2FDevice registeredDevice)
{
return StartedAuthentication();
}
protected override void FinishAuthenticationCore(StartedAuthentication authentication,
AuthenticateResponse authenticateResponse, DeviceRegistration registration)
{
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,5 @@
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading;

View File

@ -19,7 +19,7 @@ services:
TESTS_DB: "Postgres"
TESTS_POSTGRES: User ID=postgres;Host=postgres;Port=5432;Database=btcpayserver
TESTS_HOSTNAME: tests
TESTS_RUN_EXTERNAL_INTEGRATION: ${TESTS_RUN_EXTERNAL_INTEGRATION:-"false"}
TESTS_RUN_EXTERNAL_INTEGRATION: ${TESTS_RUN_EXTERNAL_INTEGRATION:-false}
TESTS_AzureBlobStorageConnectionString: ${TESTS_AzureBlobStorageConnectionString:-none}
TEST_MERCHANTLIGHTNINGD: "type=clightning;server=unix://etc/merchant_lightningd_datadir/lightning-rpc"
TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=unix://etc/customer_lightningd_datadir/lightning-rpc"
@ -69,7 +69,7 @@ services:
- "sshd_datadir:/root/.ssh"
devlnd:
image: btcpayserver/bitcoin:0.21.1
image: btcpayserver/bitcoin:0.21.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"
@ -84,7 +84,7 @@ services:
- customer_lnd
- merchant_lnd
nbxplorer:
image: nicolasdorier/nbxplorer:2.1.56
image: nicolasdorier/nbxplorer:2.1.49
restart: unless-stopped
ports:
- "32838:32838"
@ -118,7 +118,7 @@ services:
bitcoind:
restart: unless-stopped
image: btcpayserver/bitcoin:0.21.1
image: btcpayserver/bitcoin:0.21.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"
@ -146,7 +146,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: btcpayserver/lightning:v0.10.1-dev
image: btcpayserver/lightning:v0.9.3-1-dev
stop_signal: SIGKILL
restart: unless-stopped
environment:
@ -173,14 +173,12 @@ services:
- bitcoind
lightning-charged:
image: shesek/lightning-charge:0.4.23-1-standalone
image: shesek/lightning-charge:0.4.19-standalone
restart: unless-stopped
environment:
NETWORK: regtest
API_TOKEN: foiewnccewuify
BITCOIND_RPCCONNECT: bitcoind
LN_NET_PATH: /etc/lightning
LN_NET: /etc/lightning
volumes:
- "bitcoin_datadir:/etc/bitcoin"
- "lightning_charge_datadir:/data"
@ -195,7 +193,7 @@ services:
- merchant_lightningd
merchant_lightningd:
image: btcpayserver/lightning:v0.10.1-dev
image: btcpayserver/lightning:v0.9.3-1-dev
stop_signal: SIGKILL
environment:
EXPOSE_TCP: "true"
@ -227,7 +225,7 @@ services:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:v0.13.1-beta-withloop
image: btcpayserver/lnd:v0.12.0-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -261,7 +259,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.13.1-beta-withloop
image: btcpayserver/lnd:v0.12.0-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"

View File

@ -17,7 +17,7 @@ services:
TESTS_DB: "Postgres"
TESTS_POSTGRES: User ID=postgres;Host=postgres;Port=5432;Database=btcpayserver
TESTS_HOSTNAME: tests
TESTS_RUN_EXTERNAL_INTEGRATION: ${TESTS_RUN_EXTERNAL_INTEGRATION:-"false"}
TESTS_RUN_EXTERNAL_INTEGRATION: ${TESTS_RUN_EXTERNAL_INTEGRATION:-false}
TESTS_AzureBlobStorageConnectionString: ${TESTS_AzureBlobStorageConnectionString:-none}
TEST_MERCHANTLIGHTNINGD: "type=clightning;server=unix://etc/merchant_lightningd_datadir/lightning-rpc"
TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=unix://etc/customer_lightningd_datadir/lightning-rpc"
@ -66,7 +66,7 @@ services:
- "sshd_datadir:/root/.ssh"
devlnd:
image: btcpayserver/bitcoin:0.21.1
image: btcpayserver/bitcoin:0.21.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"
@ -81,7 +81,7 @@ services:
- customer_lnd
- merchant_lnd
nbxplorer:
image: nicolasdorier/nbxplorer:2.1.56
image: nicolasdorier/nbxplorer:2.1.49
restart: unless-stopped
ports:
- "32838:32838"
@ -105,7 +105,7 @@ services:
bitcoind:
restart: unless-stopped
image: btcpayserver/bitcoin:0.21.1
image: btcpayserver/bitcoin:0.21.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"
@ -133,7 +133,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: btcpayserver/lightning:v0.10.1-dev
image: btcpayserver/lightning:v0.9.3-1-dev
stop_signal: SIGKILL
restart: unless-stopped
environment:
@ -160,14 +160,12 @@ services:
- bitcoind
lightning-charged:
image: shesek/lightning-charge:0.4.23-1-standalone
image: shesek/lightning-charge:0.4.19-standalone
restart: unless-stopped
environment:
NETWORK: regtest
API_TOKEN: foiewnccewuify
BITCOIND_RPCCONNECT: bitcoind
LN_NET_PATH: /etc/lightning
LN_NET: /etc/lightning
volumes:
- "bitcoin_datadir:/etc/bitcoin"
- "lightning_charge_datadir:/data"
@ -182,7 +180,7 @@ services:
- merchant_lightningd
merchant_lightningd:
image: btcpayserver/lightning:v0.10.1-dev
image: btcpayserver/lightning:v0.9.3-1-dev
stop_signal: SIGKILL
environment:
EXPOSE_TCP: "true"
@ -215,7 +213,7 @@ services:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:v0.13.1-beta-withloop
image: btcpayserver/lnd:v0.12.0-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -249,7 +247,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.13.1-beta-withloop
image: btcpayserver/lnd:v0.12.0-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"

View File

@ -44,15 +44,12 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="BIP78.Sender" Version="0.2.0" />
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.1" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.2.10" />
<PackageReference Include="BuildBundlerMinifier" Version="3.2.449" />
<PackageReference Include="BTCPayServer.Hwi" Version="1.1.3" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.2.7" />
<PackageReference Include="BuildBundlerMinifier" Version="3.2.435" />
<PackageReference Include="BundlerMinifier.Core" Version="3.2.435" />
<PackageReference Include="BundlerMinifier.TagHelpers" Version="3.2.435" />
<PackageReference Include="CsvHelper" Version="15.0.5" />
<PackageReference Include="Fido2" Version="2.0.1" />
<PackageReference Include="Fido2.AspNet" Version="2.0.1" />
<PackageReference Include="HtmlSanitizer" Version="5.0.372" />
<PackageReference Include="McMaster.NETCore.Plugins.Mvc" Version="1.3.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
@ -84,12 +81,14 @@
<PackageReference Include="TwentyTwenty.Storage.Azure" Version="2.12.1" />
<PackageReference Include="TwentyTwenty.Storage.Google" Version="2.12.1" />
<PackageReference Include="TwentyTwenty.Storage.Local" Version="2.12.1" />
<PackageReference Include="U2F.Core" Version="1.0.4" />
<PackageReference Include="YamlDotNet" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.1.1" Condition="'$(RazorCompileOnBuild)' != 'true'" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.1" />
</ItemGroup>
<ItemGroup>
<None Include="wwwroot\main\bootstrap4-creativestart\creative.js" />
<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" />
@ -125,13 +124,19 @@
<None Include="wwwroot\vendor\jquery-easing\jquery.easing.min.js" />
<None Include="wwwroot\vendor\jquery\jquery.js" />
<None Include="wwwroot\vendor\jquery\jquery.min.js" />
<None Include="wwwroot\vendor\magnific-popup\jquery.magnific-popup.js" />
<None Include="wwwroot\vendor\magnific-popup\jquery.magnific-popup.min.js" />
<None Include="wwwroot\vendor\popper\popper.js" />
<None Include="wwwroot\vendor\popper\popper.min.js" />
<None Include="wwwroot\vendor\scrollreveal\scrollreveal.js" />
<None Include="wwwroot\vendor\scrollreveal\scrollreveal.min.js" />
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\vendor\bootstrap" />
<Folder Include="wwwroot\vendor\clipboard.js\" />
<Folder Include="wwwroot\vendor\highlightjs\" />
<Folder Include="wwwroot\vendor\summernote" />
<Folder Include="wwwroot\vendor\u2f" />
<Folder Include="wwwroot\vendor\vue-qrcode-reader" />
</ItemGroup>

View File

@ -1,49 +1,44 @@
@inject LinkGenerator linkGenerator
@inject UserManager<ApplicationUser> UserManager
@inject ISettingsRepository SettingsRepository
@inject CssThemeManager CssThemeManager
@using BTCPayServer.HostedServices
@using BTCPayServer.Views.Notifications
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Abstractions.Contracts
@using BTCPayServer.Client
@using BTCPayServer.Services
@using Microsoft.AspNetCore.Http.Extensions
@model BTCPayServer.Components.NotificationsDropdown.NotificationSummaryViewModel
@addTagHelper *, BundlerMinifier.TagHelpers
@if (Model.UnseenCount > 0)
{
<li class="nav-item dropdown" id="notifications-nav-item">
<a class="nav-link js-scroll-trigger @ViewData.IsActiveCategory(typeof(NotificationsNavPages))" href="#" id="NotificationsDropdownToggle" role="button" data-bs-toggle="dropdown">
<span class="d-inline-block d-lg-none">Notifications</span>
<i class="fa fa-bell d-lg-inline-block d-none"></i>
<span class="notification-badge badge rounded-pill bg-danger">@Model.UnseenCount</span>
<a class="nav-link js-scroll-trigger border-bottom-0" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" id="Notifications">
<i class="fa fa-bell"></i>
</a>
<div class="dropdown-menu dropdown-menu-end text-center notification-dropdown" aria-labelledby="NotificationsDropdownToggle">
<span class="alerts-badge badge badge-pill badge-danger">@Model.UnseenCount</span>
<div class="dropdown-menu dropdown-menu-right text-center notification-dropdown" aria-labelledby="navbarDropdown">
<div class="d-flex 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="Notifications" asp-action="MarkAllAsSeen" asp-route-returnUrl="@Context.Request.GetCurrentPathWithQueryString()" method="post">
<button class="btn btn-link p-0" type="submit">Mark all as seen</button>
<form asp-controller="Notifications" asp-action="MarkAllAsSeen" asp-route-returnUrl="@Context.Request.GetCurrentPathWithQueryString()" method="post">
<button class="btn btn-link p-0 font-weight-semibold" type="submit">Mark all as seen</button>
</form>
</div>
@foreach (var notif in Model.Last5)
{
<a asp-action="NotificationPassThrough" asp-controller="Notifications" asp-route-id="@notif.Id" class="notification d-flex align-items-center dropdown-item border-bottom border-light py-3 px-4">
<div class="me-3">
<div class="mr-3">
<vc:icon symbol="note" />
</div>
<div class="notification-item__content">
<div class="text-start text-wrap">
<div class="text-left text-wrap font-weight-semibold">
@notif.Body
</div>
<div class="text-start d-flex">
<div class="text-left d-flex">
<small class="text-muted" data-timeago-unixms="@notif.Created.ToUnixTimeMilliseconds()">@notif.Created.ToTimeAgo()</small>
</div>
</div>
</a>
}
<div class="p-3">
<a asp-controller="Notifications" asp-action="Index">View all</a>
<a class="font-weight-semibold" asp-controller="Notifications" asp-action="Index">View all</a>
</div>
</div>
</li>
@ -51,17 +46,17 @@
else
{
<li class="nav-item" id="notifications-nav-item">
<a asp-controller="Notifications" asp-action="Index" title="Notifications" class="nav-link js-scroll-trigger @ViewData.IsActiveCategory(typeof(NotificationsNavPages))" id="Notifications">
<span class="d-lg-none d-sm-block">Notifications</span><i class="fa fa-bell d-lg-inline-block d-none"></i>
<a asp-controller="Notifications" asp-action="Index" title="Notifications" class="nav-link js-scroll-trigger" id="Notifications">
<i class="fa fa-bell"></i>
</a>
</li>
}
@{
var disabled = (await SettingsRepository.GetPolicies()).DisableInstantNotifications;
var disabled = CssThemeManager.Policies.DisableInstantNotifications;
if (!disabled)
{
var user = await UserManager.GetUserAsync(User);
disabled = user?.DisabledNotifications == "all";
disabled = user.DisabledNotifications == "all";
}
}
@if (!disabled)

View File

@ -9,7 +9,7 @@
<nav aria-label="..." class="w-100">
@if (Model.Total > Model.Count)
{
<ul class="pagination float-start">
<ul class="pagination float-left">
<li class="page-item @(Model.Skip == 0 ? "disabled" : null)">
<a class="page-link" tabindex="-1" href="@NavigatePages(-1, Model.Count)">&laquo;</a>
</li>
@ -35,7 +35,7 @@
@if (Model.Total >= pageSizeOptions.Min())
{
<ul class="pagination float-end">
<ul class="pagination float-right">
<li class="page-item disabled">
<span class="page-link">Page Size:</span>
</li>

View File

@ -69,12 +69,7 @@ namespace BTCPayServer.Configuration
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
DockerDeployment = conf.GetOrDefault<bool>("dockerdeployment", true);
AllowAdminRegistration = conf.GetOrDefault<bool>("allow-admin-registration", false);
TorrcFile = conf.GetOrDefault<string>("torrcfile", null);
TorServices = conf.GetOrDefault<string>("torservices", null)
?.Split(new[] {';', ','}, StringSplitOptions.RemoveEmptyEntries);
if (!string.IsNullOrEmpty(TorrcFile) && TorServices != null)
throw new ConfigException($"torrcfile or torservices should be provided, but not both");
var socksEndpointString = conf.GetOrDefault<string>("socksendpoint", null);
if (!string.IsNullOrEmpty(socksEndpointString))
@ -200,7 +195,6 @@ namespace BTCPayServer.Configuration
set;
}
public string TorrcFile { get; set; }
public string[] TorServices { get; set; }
public Uri UpdateUrl { get; set; }
}
}

View File

@ -39,7 +39,6 @@ namespace BTCPayServer.Configuration
app.Option("--sshauthorizedkeys", "Path to a authorized_keys file that BTCPayServer can modify from the website (default: empty)", CommandOptionType.SingleValue);
app.Option("--sshtrustedfingerprints", "SSH Host public key fingerprint or sha256 (default: empty, it will allow untrusted connections)", CommandOptionType.SingleValue);
app.Option("--torrcfile", "Path to torrc file containing hidden services directories (default: empty)", CommandOptionType.SingleValue);
app.Option("--torservices", "Tor hostnames of available services added to Server Settings (and sets onion header for btcpay). Format: btcpayserver:host.onion:80;btc-p2p:host2.onion:81,BTC-RPC:host3.onion:82,UNKNOWN:host4.onion:83. (default: empty)", CommandOptionType.SingleValue);
app.Option("--socksendpoint", "Socks endpoint to connect to onion urls (default: empty)", CommandOptionType.SingleValue);
app.Option("--updateurl", $"Url used for once a day new release version check. Check performed only if value is not empty (default: empty)", CommandOptionType.SingleValue);
app.Option("--debuglog", "A rolling log file for debug messages.", CommandOptionType.SingleValue);

View File

@ -6,19 +6,20 @@ using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Fido2;
using BTCPayServer.Fido2.Models;
using BTCPayServer.Logging;
using BTCPayServer.Models;
using BTCPayServer.Models.AccountViewModels;
using BTCPayServer.Security;
using BTCPayServer.Services;
using Fido2NetLib;
using BTCPayServer.U2F;
using BTCPayServer.U2F.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using NicolasDorier.RateLimits;
using U2F.Core.Exceptions;
namespace BTCPayServer.Controllers
{
@ -32,7 +33,7 @@ namespace BTCPayServer.Controllers
readonly SettingsRepository _SettingsRepository;
readonly Configuration.BTCPayServerOptions _Options;
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
private readonly Fido2Service _fido2Service;
public U2FService _u2FService;
private readonly EventAggregator _eventAggregator;
readonly ILogger _logger;
@ -43,8 +44,8 @@ namespace BTCPayServer.Controllers
SettingsRepository settingsRepository,
Configuration.BTCPayServerOptions options,
BTCPayServerEnvironment btcPayServerEnvironment,
EventAggregator eventAggregator,
Fido2Service fido2Service)
U2FService u2FService,
EventAggregator eventAggregator)
{
_userManager = userManager;
_signInManager = signInManager;
@ -52,7 +53,7 @@ namespace BTCPayServer.Controllers
_SettingsRepository = settingsRepository;
_Options = options;
_btcPayServerEnvironment = btcPayServerEnvironment;
_fido2Service = fido2Service;
_u2FService = u2FService;
_eventAggregator = eventAggregator;
_logger = Logs.PayServer;
}
@ -120,8 +121,7 @@ namespace BTCPayServer.Controllers
return View(model);
}
var fido2Devices = await _fido2Service.HasCredentials(user.Id);
if (!await _userManager.IsLockedOutAsync(user) && fido2Devices)
if (!await _userManager.IsLockedOutAsync(user) && await _u2FService.HasDevices(user.Id))
{
if (await _userManager.CheckPasswordAsync(user, model.Password))
{
@ -140,7 +140,7 @@ namespace BTCPayServer.Controllers
return View("SecondaryLogin", new SecondaryLoginViewModel()
{
LoginWith2FaViewModel = twoFModel,
LoginWithFido2ViewModel = fido2Devices? await BuildFido2ViewModel(model.RememberMe, user): null,
LoginWithU2FViewModel = await BuildU2FViewModel(model.RememberMe, user)
});
}
else
@ -156,7 +156,7 @@ namespace BTCPayServer.Controllers
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: true);
if (result.Succeeded)
{
_logger.LogInformation($"User '{user.Id}' logged in.");
_logger.LogInformation("User logged in.");
return RedirectToLocal(returnUrl);
}
if (result.RequiresTwoFactor)
@ -171,7 +171,7 @@ namespace BTCPayServer.Controllers
}
if (result.IsLockedOut)
{
_logger.LogWarning($"User '{user.Id}' account locked out.");
_logger.LogWarning("User account locked out.");
return RedirectToAction(nameof(Lockout));
}
else
@ -185,29 +185,31 @@ namespace BTCPayServer.Controllers
return View(model);
}
private async Task<LoginWithFido2ViewModel> BuildFido2ViewModel(bool rememberMe, ApplicationUser user)
private async Task<LoginWithU2FViewModel> BuildU2FViewModel(bool rememberMe, ApplicationUser user)
{
if (_btcPayServerEnvironment.IsSecure)
{
var r = await _fido2Service.RequestLogin(user.Id);
if (r is null)
var u2fChallenge = await _u2FService.GenerateDeviceChallenges(user.Id,
Request.GetAbsoluteUriNoPathBase().ToString().TrimEnd('/'));
return new LoginWithU2FViewModel()
{
return null;
}
return new LoginWithFido2ViewModel()
{
Data = r,
Version = u2fChallenge[0].version,
Challenge = u2fChallenge[0].challenge,
Challenges = u2fChallenge,
AppId = u2fChallenge[0].appId,
UserId = user.Id,
RememberMe = rememberMe
};
}
return null;
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> LoginWithFido2(LoginWithFido2ViewModel viewModel, string returnUrl = null)
public async Task<IActionResult> LoginWithU2F(LoginWithU2FViewModel viewModel, string returnUrl = null)
{
if (!CanLoginOrRegister())
{
@ -225,25 +227,24 @@ namespace BTCPayServer.Controllers
var errorMessage = string.Empty;
try
{
if (await _fido2Service.CompleteLogin(viewModel.UserId, JObject.Parse(viewModel.Response).ToObject<AuthenticatorAssertionRawResponse>()))
if (await _u2FService.AuthenticateUser(viewModel.UserId, viewModel.DeviceResponse))
{
await _signInManager.SignInAsync(user, viewModel.RememberMe, "FIDO2");
await _signInManager.SignInAsync(user, viewModel.RememberMe, "U2F");
_logger.LogInformation("User logged in.");
return RedirectToLocal(returnUrl);
}
errorMessage = "Invalid login attempt.";
}
catch (Fido2VerificationException e)
catch (U2fException e)
{
errorMessage = e.Message;
}
ModelState.AddModelError(string.Empty, errorMessage);
viewModel.Response = null;
return View("SecondaryLogin", new SecondaryLoginViewModel()
{
LoginWithFido2ViewModel = viewModel,
LoginWithU2FViewModel = viewModel,
LoginWith2FaViewModel = !user.TwoFactorEnabled
? null
: new LoginWith2faViewModel()
@ -252,6 +253,7 @@ namespace BTCPayServer.Controllers
}
});
}
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> LoginWith2fa(bool rememberMe, string returnUrl = null)
@ -274,7 +276,7 @@ namespace BTCPayServer.Controllers
return View("SecondaryLogin", new SecondaryLoginViewModel()
{
LoginWith2FaViewModel = new LoginWith2faViewModel { RememberMe = rememberMe },
LoginWithFido2ViewModel = (await _fido2Service.HasCredentials(user.Id)) ? await BuildFido2ViewModel(rememberMe, user) : null,
LoginWithU2FViewModel = (await _u2FService.HasDevices(user.Id)) ? await BuildU2FViewModel(rememberMe, user) : null
});
}
@ -320,7 +322,7 @@ namespace BTCPayServer.Controllers
return View("SecondaryLogin", new SecondaryLoginViewModel()
{
LoginWith2FaViewModel = model,
LoginWithFido2ViewModel = (await _fido2Service.HasCredentials(user.Id)) ? await BuildFido2ViewModel(rememberMe, user) : null,
LoginWithU2FViewModel = (await _u2FService.HasDevices(user.Id)) ? await BuildU2FViewModel(rememberMe, user) : null
});
}
}
@ -541,7 +543,6 @@ namespace BTCPayServer.Controllers
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
[RateLimitsFilter(ZoneLimits.ForgotPassword, Scope = RateLimitsScope.RemoteAddress)]
public async Task<IActionResult> ForgotPassword(ForgotPasswordViewModel model)
{
if (ModelState.IsValid)

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