Compare commits
1 Commits
v1.1.1
...
fix-sln-is
Author | SHA1 | Date | |
---|---|---|---|
b9896037ba |
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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,7 +27,7 @@
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NBitcoin" Version="5.0.77" />
|
||||
<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>
|
||||
|
@ -1,41 +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,
|
||||
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 (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);
|
||||
}
|
||||
|
||||
@ -105,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,63 +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,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var response =
|
||||
await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/LightningNetwork"), 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 async Task<LightningNetworkPaymentMethodData>
|
||||
UpdateStoreLightningNetworkPaymentMethodToInternalNode(string storeId,
|
||||
string cryptoCode, LightningNetworkPaymentMethodData paymentMethod,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/LightningNetwork/{cryptoCode}/internal",
|
||||
method: HttpMethod.Put), token);
|
||||
return await HandleResponse<LightningNetworkPaymentMethodData>(response);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -9,7 +9,7 @@ namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class CreateInvoiceRequest
|
||||
{
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
[JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))]
|
||||
public decimal Amount { get; set; }
|
||||
public string Currency { get; set; }
|
||||
public JObject Metadata { get; set; }
|
||||
@ -33,8 +33,6 @@ namespace BTCPayServer.Client.Models
|
||||
public double? PaymentTolerance { get; set; }
|
||||
[JsonProperty("redirectURL")]
|
||||
public string RedirectURL { get; set; }
|
||||
|
||||
public bool? RedirectAutomatically { get; set; }
|
||||
public string DefaultLanguage { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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; }
|
||||
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class LightningNetworkPaymentMethodData
|
||||
{
|
||||
/// <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 string ConnectionString { get; set; }
|
||||
|
||||
public LightningNetworkPaymentMethodData()
|
||||
{
|
||||
}
|
||||
|
||||
public LightningNetworkPaymentMethodData(string cryptoCode, string connectionString, bool enabled)
|
||||
{
|
||||
Enabled = enabled;
|
||||
CryptoCode = cryptoCode;
|
||||
ConnectionString = connectionString;
|
||||
}
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -1,9 +0,0 @@
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public enum TransactionStatus
|
||||
{
|
||||
Unconfirmed,
|
||||
Confirmed,
|
||||
Replaced
|
||||
}
|
||||
}
|
@ -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; }
|
||||
|
@ -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'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -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[]
|
||||
|
@ -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),
|
||||
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -24,22 +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 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);
|
||||
return $"{base.GenerateBIP21(cryptoInfoAddress, money)}{(money is null? "?": "&")}assetid={AssetId}";
|
||||
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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -54,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; }
|
||||
@ -63,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;
|
||||
|
||||
@ -123,12 +123,12 @@ namespace BTCPayServer
|
||||
|
||||
public virtual string GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue)
|
||||
{
|
||||
return $"{UriScheme}:{cryptoInfoAddress}{(cryptoInfoDue is null? string.Empty: $"?amount={cryptoInfoDue.ToString(false, true)}")}";
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -152,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")]
|
||||
|
@ -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>()
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,9 +4,9 @@
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="3.0.21" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="3.0.20" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(Altcoins)' != 'true'">
|
||||
<Compile Remove="Altcoins\**\*.cs"></Compile>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
@ -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);
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,47 +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)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Fido2Credentials",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(nullable: false),
|
||||
Name = table.Column<string>(nullable: true),
|
||||
ApplicationUserId = table.Column<string>(nullable: true),
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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")
|
||||
|
@ -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="5.0.77" />
|
||||
<PackageReference Include="NBitcoin" Version="5.0.73" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.6.3" />
|
||||
</ItemGroup>
|
||||
|
@ -1266,13 +1266,6 @@
|
||||
"symbol":null,
|
||||
"crypto":true
|
||||
},
|
||||
{
|
||||
"name":"Althash",
|
||||
"code":"HTML",
|
||||
"divisibility":8,
|
||||
"symbol":null,
|
||||
"crypto":true
|
||||
},
|
||||
{
|
||||
"name":"CHC",
|
||||
"code":"CHC",
|
||||
|
File diff suppressed because one or more lines are too long
@ -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>(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 = 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 GenerateWalletRequest());
|
||||
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 = 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 = 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");
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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,
|
||||
@ -149,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);
|
||||
|
@ -53,8 +53,4 @@
|
||||
<ProjectReference Include="..\BTCPayServer\BTCPayServer.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Pages" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -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>()
|
||||
|
@ -109,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();
|
||||
|
91
BTCPayServer.Tests/CoinSwitchTests.cs
Normal file
91
BTCPayServer.Tests/CoinSwitchTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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()
|
||||
{
|
||||
|
@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Support.UI;
|
||||
using Xunit;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
@ -106,49 +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));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
@ -20,7 +19,6 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
using NBitcoin.OpenAsset;
|
||||
using NBitcoin.Payment;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
@ -471,25 +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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -561,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);
|
||||
@ -571,7 +550,6 @@ namespace BTCPayServer.Tests
|
||||
remainingFields.Remove(field);
|
||||
}
|
||||
Assert.Empty(remainingFields);
|
||||
return ex;
|
||||
}
|
||||
|
||||
private async Task AssertHttpError(int code, Func<Task> act)
|
||||
@ -706,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);
|
||||
});
|
||||
@ -970,12 +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
|
||||
}});
|
||||
Assert.True(newInvoice.Checkout.RedirectAutomatically);
|
||||
|
||||
new CreateInvoiceRequest() { Currency = "USD", Amount = 1, Metadata = JObject.Parse("{\"itemCode\": \"testitem\"}") });
|
||||
|
||||
//list
|
||||
var invoices = await viewOnly.GetInvoices(user.StoreId);
|
||||
|
||||
@ -983,62 +957,6 @@ namespace BTCPayServer.Tests
|
||||
Assert.Single(invoices);
|
||||
Assert.Equal(newInvoice.Id, invoices.First().Id);
|
||||
|
||||
//list Filtered
|
||||
var invoicesFiltered = await viewOnly.GetInvoices(user.StoreId,
|
||||
orderId: null, status: null, DateTimeOffset.Now.AddHours(-1),
|
||||
DateTimeOffset.Now.AddHours(1));
|
||||
|
||||
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, DateTimeOffset.Now.AddDays(-2),
|
||||
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, DateTimeOffset.Now.AddDays(-1),
|
||||
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: 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: "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);
|
||||
@ -1050,28 +968,25 @@ 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
|
||||
await client.MarkInvoiceStatus(user.StoreId, invoice.Id, new MarkInvoiceStatusRequest()
|
||||
{
|
||||
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 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]}")
|
||||
@ -1081,7 +996,7 @@ namespace BTCPayServer.Tests
|
||||
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);
|
||||
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());
|
||||
|
||||
@ -1168,24 +1083,6 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1208,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);
|
||||
|
||||
@ -1278,6 +1176,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task NotificationAPITests()
|
||||
@ -1373,340 +1272,9 @@ namespace BTCPayServer.Tests
|
||||
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")]
|
||||
@ -1723,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";
|
||||
|
@ -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 }));
|
||||
}
|
||||
}
|
||||
|
||||
@ -179,7 +165,9 @@ 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)))
|
||||
@ -232,7 +220,7 @@ 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);
|
||||
@ -249,7 +237,7 @@ namespace BTCPayServer.Tests
|
||||
s.GoToStore(receiver.storeId);
|
||||
//payjoin is not enabled by default.
|
||||
Assert.False(s.Driver.FindElement(By.Id("PayJoinEnabled")).Selected);
|
||||
s.Driver.SetCheckbox(By.Id("PayJoinEnabled"), true);
|
||||
s.SetCheckbox(s, "PayJoinEnabled", true);
|
||||
s.Driver.FindElement(By.Id("Save")).Click();
|
||||
Assert.True(s.Driver.FindElement(By.Id("PayJoinEnabled")).Selected);
|
||||
|
||||
@ -320,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];
|
||||
@ -377,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()
|
||||
@ -427,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]);
|
||||
@ -443,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();
|
||||
@ -452,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();
|
||||
@ -464,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();
|
||||
@ -476,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();
|
||||
@ -488,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();
|
||||
@ -499,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();
|
||||
@ -516,9 +501,6 @@ 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));
|
||||
@ -528,7 +510,6 @@ namespace BTCPayServer.Tests
|
||||
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 =
|
||||
@ -547,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());
|
||||
@ -578,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);
|
||||
}
|
||||
}
|
||||
@ -820,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
|
||||
@ -876,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 =
|
||||
@ -937,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.
|
||||
@ -1088,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);
|
||||
@ -1117,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
|
||||
|
@ -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?
|
||||
|
@ -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,38 +62,31 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
options.AddArguments($"window-size={windowSize.Width}x{windowSize.Height}");
|
||||
options.AddArgument("shm-size=2g");
|
||||
|
||||
var cds = ChromeDriverService.CreateDefaultService(chromeDriverPath);
|
||||
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 = new ChromeDriver(chromeDriverPath, options);
|
||||
|
||||
if (runInBrowser)
|
||||
{
|
||||
// ensure maximized window size
|
||||
// otherwise TESTS WILL FAIL because of different hierarchy in navigation menu
|
||||
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();
|
||||
@ -121,57 +113,53 @@ 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)
|
||||
{
|
||||
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 (string.IsNullOrEmpty(seed))
|
||||
{
|
||||
var option = privkeys ? "Hotwallet" : "Watchonly";
|
||||
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();
|
||||
Driver.FindElement(By.Id("generate-wallet-link")).Click();
|
||||
Driver.FindElement(By.Id($"generate-{option}-link")).Click();
|
||||
}
|
||||
else
|
||||
{
|
||||
Logs.Tester.LogInformation("Progressing with existing seed");
|
||||
Driver.FindElement(By.Id("ImportWalletOptionsLink")).Click();
|
||||
Driver.FindElement(By.Id("ImportSeedLink")).Click();
|
||||
Driver.FindElement(By.Id("import-wallet-options-link")).Click();
|
||||
Driver.FindElement(By.Id("import-seed-link")).Click();
|
||||
Driver.FindElement(By.Id("ExistingMnemonic")).SendKeys(seed);
|
||||
Driver.SetCheckbox(By.Id("SavePrivateKeys"), privkeys);
|
||||
SetCheckbox(Driver.FindElement(By.Id("SavePrivateKeys")), privkeys);
|
||||
}
|
||||
|
||||
Driver.FindElement(By.Id("ScriptPubKeyType")).Click();
|
||||
Driver.FindElement(By.CssSelector($"#ScriptPubKeyType option[value={format}]")).Click();
|
||||
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
|
||||
|
||||
// Open advanced settings 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.
|
||||
Driver.ExecuteJavaScript("document.getElementById('AdvancedSettings').classList.add('show')");
|
||||
|
||||
Driver.SetCheckbox(By.Id("ImportKeysToRPC"), importkeys);
|
||||
Logs.Tester.LogInformation("Trying to click Continue button");
|
||||
Driver.FindElement(By.Id("Continue")).Click();
|
||||
|
||||
// Seed backup page
|
||||
FindAlertMessage();
|
||||
if (string.IsNullOrEmpty(seed))
|
||||
{
|
||||
seed = Driver.FindElements(By.Id("RecoveryPhrase")).First().GetAttribute("data-mnemonic");
|
||||
seed = Driver.FindElements(By.Id("recovery-phrase")).First().GetAttribute("data-mnemonic");
|
||||
}
|
||||
|
||||
// Confirm seed backup
|
||||
@ -185,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)
|
||||
@ -310,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();
|
||||
@ -341,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;
|
||||
}
|
||||
@ -378,14 +371,14 @@ namespace BTCPayServer.Tests
|
||||
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)));
|
||||
@ -412,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();
|
||||
|
@ -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;
|
||||
@ -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();
|
||||
@ -502,21 +478,12 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("SelectedStore")).SendKeys(storeName);
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
s.Driver.FindElement(By.Id("DefaultView")).SendKeys("Cart");
|
||||
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("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");
|
||||
@ -603,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.FindElement(By.Id("advancedSettings")).Click();
|
||||
s.Driver.WaitForAndClick(By.Id("toggleInputSelection"));
|
||||
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()));
|
||||
@ -745,9 +711,8 @@ namespace BTCPayServer.Tests
|
||||
|
||||
Logs.Tester.LogInformation("Let's see if we can delete store with some webhooks inside");
|
||||
s.GoToStore(storeId);
|
||||
// Open danger zone 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.
|
||||
s.Driver.ExecuteJavaScript("document.getElementById('danger-zone').classList.add('show')");
|
||||
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();
|
||||
@ -913,7 +878,7 @@ namespace BTCPayServer.Tests
|
||||
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);
|
||||
|
||||
@ -981,18 +946,15 @@ namespace BTCPayServer.Tests
|
||||
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")));
|
||||
Assert.Contains("No payout waiting for approval", s.Driver.PageSource);
|
||||
|
||||
// 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();
|
||||
|
||||
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();
|
||||
@ -1007,14 +969,13 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal("payout", s.Driver.FindElement(By.ClassName("transactionLabel")).Text);
|
||||
|
||||
s.GoToWallet(navPages: WalletsNavPages.Payouts);
|
||||
ReadOnlyCollection<IWebElement> txs;
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
s.Driver.Navigate().Refresh();
|
||||
|
||||
txs = s.Driver.FindElements(By.ClassName("transaction-link"));
|
||||
Assert.Equal(2, txs.Count);
|
||||
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"));
|
||||
@ -1035,7 +996,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
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));
|
||||
Assert.True(payoutsData.All(p => p.State == Data.PayoutState.Completed));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
@ -174,7 +171,7 @@ 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();
|
||||
@ -184,13 +181,10 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
ScriptPubKeyType = segwit,
|
||||
SavePrivateKeys = importKeysToNBX,
|
||||
ImportKeysToRPC = importsKeysToBitcoinCore
|
||||
});
|
||||
await store.UpdateWallet(
|
||||
new WalletSetupViewModel
|
||||
await store.AddDerivationScheme(StoreId,
|
||||
new DerivationSchemeViewModel()
|
||||
{
|
||||
StoreId = StoreId,
|
||||
Method = importKeysToNBX ? WalletSetupMethod.HotWallet : WalletSetupMethod.WatchOnly,
|
||||
Enabled = true,
|
||||
CryptoCode = cryptoCode,
|
||||
Network = SupportedNetwork,
|
||||
@ -202,7 +196,7 @@ namespace BTCPayServer.Tests
|
||||
KeyPath = GenerateWalletResponseV.AccountKeyPath.KeyPath.ToString(),
|
||||
DerivationScheme = DerivationScheme.ToString(),
|
||||
Confirmation = true
|
||||
});
|
||||
}, cryptoCode);
|
||||
return new WalletId(StoreId, cryptoCode);
|
||||
}
|
||||
|
||||
@ -262,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);
|
||||
}
|
||||
@ -343,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;
|
||||
@ -352,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)
|
||||
|
132
BTCPayServer.Tests/U2FTests.cs
Normal file
132
BTCPayServer.Tests/U2FTests.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -19,8 +19,6 @@ using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Fido2;
|
||||
using BTCPayServer.Fido2.Models;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Hosting;
|
||||
using BTCPayServer.Lightning;
|
||||
@ -34,7 +32,6 @@ using BTCPayServer.Models.WalletViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Payments.PayJoin.Sender;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security.Bitpay;
|
||||
using BTCPayServer.Services;
|
||||
@ -44,15 +41,12 @@ using BTCPayServer.Services.Labels;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using BTCPayServer.U2F.Models;
|
||||
using BTCPayServer.Validation;
|
||||
using ExchangeSharp;
|
||||
using Fido2NetLib;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Configuration.Memory;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitcoin.Payment;
|
||||
@ -72,7 +66,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
public class UnitTest1
|
||||
{
|
||||
public const int LongRunningTestTimeout = 60_000; // 60s
|
||||
public const int TestTimeout = 60_000;
|
||||
|
||||
public UnitTest1(ITestOutputHelper helper)
|
||||
{
|
||||
@ -417,7 +411,7 @@ namespace BTCPayServer.Tests
|
||||
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 entity = new InvoiceEntity();
|
||||
entity.Networks = networkProvider;
|
||||
@ -705,7 +699,7 @@ namespace BTCPayServer.Tests
|
||||
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 entity = new InvoiceEntity();
|
||||
entity.Networks = networkProvider;
|
||||
@ -875,55 +869,18 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public async Task CanEnumerateTorServices()
|
||||
{
|
||||
var tor = new TorServices(new BTCPayNetworkProvider(ChainName.Regtest),
|
||||
new OptionsWrapper<BTCPayServerOptions>(new BTCPayServerOptions()
|
||||
{
|
||||
TorrcFile = TestUtils.GetTestDataFullPath("Tor/torrc")
|
||||
}));
|
||||
new BTCPayServerOptions() { TorrcFile = TestUtils.GetTestDataFullPath("Tor/torrc") });
|
||||
await tor.Refresh();
|
||||
|
||||
Assert.Single(tor.Services.Where(t => t.ServiceType == TorServiceType.BTCPayServer));
|
||||
Assert.Single(tor.Services.Where(t => t.ServiceType == TorServiceType.P2P));
|
||||
Assert.Single(tor.Services.Where(t => t.ServiceType == TorServiceType.RPC));
|
||||
Assert.True(tor.Services.Count(t => t.ServiceType == TorServiceType.Other) > 1);
|
||||
|
||||
tor = new TorServices(new BTCPayNetworkProvider(ChainName.Regtest),
|
||||
new OptionsWrapper<BTCPayServerOptions>(new BTCPayServerOptions()
|
||||
{
|
||||
TorrcFile = null,
|
||||
TorServices = "btcpayserver:host.onion:80;btc-p2p:host2.onion:81,BTC-RPC:host3.onion:82,UNKNOWN:host4.onion:83,INVALID:ddd".Split(new[] {';', ','}, StringSplitOptions.RemoveEmptyEntries)
|
||||
}));
|
||||
await Task.WhenAll(tor.StartAsync(CancellationToken.None));
|
||||
|
||||
var btcpayS = Assert.Single(tor.Services.Where(t => t.ServiceType == TorServiceType.BTCPayServer));
|
||||
Assert.Null(btcpayS.Network);
|
||||
Assert.Equal("host.onion", btcpayS.OnionHost);
|
||||
Assert.Equal(80, btcpayS.VirtualPort);
|
||||
|
||||
var p2p = Assert.Single(tor.Services.Where(t => t.ServiceType == TorServiceType.P2P));
|
||||
Assert.NotNull(p2p.Network);
|
||||
Assert.Equal("BTC", p2p.Network.CryptoCode);
|
||||
Assert.Equal("host2.onion", p2p.OnionHost);
|
||||
Assert.Equal(81, p2p.VirtualPort);
|
||||
|
||||
var rpc = Assert.Single(tor.Services.Where(t => t.ServiceType == TorServiceType.RPC));
|
||||
Assert.NotNull(p2p.Network);
|
||||
Assert.Equal("BTC", rpc.Network.CryptoCode);
|
||||
Assert.Equal("host3.onion", rpc.OnionHost);
|
||||
Assert.Equal(82, rpc.VirtualPort);
|
||||
|
||||
var unknown = Assert.Single(tor.Services.Where(t => t.ServiceType == TorServiceType.Other));
|
||||
Assert.Null(unknown.Network);
|
||||
Assert.Equal("host4.onion", unknown.OnionHost);
|
||||
Assert.Equal(83, unknown.VirtualPort);
|
||||
Assert.Equal("UNKNOWN", unknown.Name);
|
||||
|
||||
Assert.Equal(4, tor.Services.Length);
|
||||
|
||||
Assert.True(tor.Services.Where(t => t.ServiceType == TorServiceType.Other).Count() > 1);
|
||||
}
|
||||
|
||||
|
||||
@ -937,7 +894,7 @@ namespace BTCPayServer.Tests
|
||||
await tester.StartAsync();
|
||||
await tester.EnsureChannelsSetup();
|
||||
var user = tester.NewAccount();
|
||||
await user.GrantAccessAsync(true);
|
||||
await user.GrantAccessAsync();
|
||||
await user.RegisterDerivationSchemeAsync("BTC");
|
||||
await user.RegisterLightningNodeAsync("BTC", LightningConnectionType.CLightning);
|
||||
user.SetNetworkFeeMode(NetworkFeeMode.Never);
|
||||
@ -987,12 +944,12 @@ namespace BTCPayServer.Tests
|
||||
await tester.StartAsync();
|
||||
await tester.EnsureChannelsSetup();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess(true);
|
||||
user.GrantAccess();
|
||||
var storeController = user.GetController<StoresController>();
|
||||
Assert.IsType<ViewResult>(storeController.UpdateStore());
|
||||
Assert.IsType<ViewResult>(storeController.SetupLightningNode(user.StoreId, "BTC"));
|
||||
Assert.IsType<ViewResult>(storeController.AddLightningNode(user.StoreId, "BTC"));
|
||||
|
||||
var testResult = storeController.SetupLightningNode(user.StoreId, new LightningNodeViewModel
|
||||
var testResult = storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel()
|
||||
{
|
||||
ConnectionString = $"type=charge;server={tester.MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true",
|
||||
SkipPortTest = true // We can't test this as the IP can't be resolved by the test host :(
|
||||
@ -1001,19 +958,19 @@ namespace BTCPayServer.Tests
|
||||
storeController.TempData.Clear();
|
||||
Assert.True(storeController.ModelState.IsValid);
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(storeController.SetupLightningNode(user.StoreId,
|
||||
new LightningNodeViewModel
|
||||
Assert.IsType<RedirectToActionResult>(storeController.AddLightningNode(user.StoreId,
|
||||
new LightningNodeViewModel()
|
||||
{
|
||||
ConnectionString = $"type=charge;server={tester.MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true"
|
||||
}, "save", "BTC").GetAwaiter().GetResult());
|
||||
|
||||
// Make sure old connection string format does not work
|
||||
Assert.IsType<RedirectToActionResult>(storeController.SetupLightningNode(user.StoreId,
|
||||
new LightningNodeViewModel { ConnectionString = tester.MerchantCharge.Client.Uri.AbsoluteUri },
|
||||
Assert.IsType<ViewResult>(storeController.AddLightningNode(user.StoreId,
|
||||
new LightningNodeViewModel() { ConnectionString = tester.MerchantCharge.Client.Uri.AbsoluteUri },
|
||||
"save", "BTC").GetAwaiter().GetResult());
|
||||
|
||||
var storeVm =
|
||||
Assert.IsType<StoreViewModel>(Assert
|
||||
Assert.IsType<Models.StoreViewModels.StoreViewModel>(Assert
|
||||
.IsType<ViewResult>(storeController.UpdateStore()).Model);
|
||||
Assert.Single(storeVm.LightningNodes.Where(l => !string.IsNullOrEmpty(l.Address)));
|
||||
}
|
||||
@ -1054,7 +1011,7 @@ namespace BTCPayServer.Tests
|
||||
await tester.StartAsync();
|
||||
await tester.EnsureChannelsSetup();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess(true);
|
||||
user.GrantAccess();
|
||||
user.RegisterLightningNode("BTC", type);
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
@ -1088,7 +1045,7 @@ namespace BTCPayServer.Tests
|
||||
});
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanUseServerInitiatedPairingCode()
|
||||
{
|
||||
@ -1115,7 +1072,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanSendIPN()
|
||||
{
|
||||
@ -1185,7 +1142,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CantPairTwiceWithSamePubkey()
|
||||
{
|
||||
@ -1209,7 +1166,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanSolveTheDogesRatesOnKraken()
|
||||
{
|
||||
@ -1227,7 +1184,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanUseTorClient()
|
||||
{
|
||||
@ -1244,7 +1201,7 @@ namespace BTCPayServer.Tests
|
||||
});
|
||||
}
|
||||
var httpFactory = tester.PayTester.GetService<IHttpClientFactory>();
|
||||
var client = httpFactory.CreateClient(PayjoinServerCommunicator.PayjoinOnionNamedClient);
|
||||
var client = httpFactory.CreateClient(PayjoinClient.PayjoinOnionNamedClient);
|
||||
Assert.NotNull(client);
|
||||
var response = await client.GetAsync("https://check.torproject.org/");
|
||||
response.EnsureSuccessStatusCode();
|
||||
@ -1263,7 +1220,7 @@ namespace BTCPayServer.Tests
|
||||
AssertConnectionDropped();
|
||||
client.Dispose();
|
||||
AssertConnectionDropped();
|
||||
client = httpFactory.CreateClient(PayjoinServerCommunicator.PayjoinOnionNamedClient);
|
||||
client = httpFactory.CreateClient(PayjoinClient.PayjoinOnionNamedClient);
|
||||
response = await client.GetAsync("http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/");
|
||||
response.EnsureSuccessStatusCode();
|
||||
AssertConnectionDropped();
|
||||
@ -1280,7 +1237,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanRescanWallet()
|
||||
{
|
||||
@ -1382,7 +1339,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanListInvoices()
|
||||
{
|
||||
@ -1433,7 +1390,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanListNotifications()
|
||||
{
|
||||
@ -1639,8 +1596,8 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
var i = await tester.PayTester.InvoiceRepository.GetInvoice(invoice2.Id);
|
||||
Assert.Equal(InvoiceStatusLegacy.New, i.Status);
|
||||
Assert.Single(i.GetPayments(false));
|
||||
Assert.False(i.GetPayments(false).First().Accounted);
|
||||
Assert.Single(i.GetPayments());
|
||||
Assert.False(i.GetPayments().First().Accounted);
|
||||
});
|
||||
|
||||
Logs.Tester.LogInformation(
|
||||
@ -1672,8 +1629,8 @@ namespace BTCPayServer.Tests
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
var invoiceEntity = await tester.PayTester.InvoiceRepository.GetInvoice(invoice.Id);
|
||||
var btcPayments = invoiceEntity.GetAllBitcoinPaymentData(false).ToArray();
|
||||
var payments = invoiceEntity.GetPayments(false).ToArray();
|
||||
var btcPayments = invoiceEntity.GetAllBitcoinPaymentData().ToArray();
|
||||
var payments = invoiceEntity.GetPayments().ToArray();
|
||||
Assert.Equal(tx1, btcPayments[0].Outpoint.Hash);
|
||||
Assert.False(payments[0].Accounted);
|
||||
Assert.Equal(tx1Bump, payments[1].Outpoint.Hash);
|
||||
@ -1717,7 +1674,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.NotNull(paymentData.KeyPath);
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanParseFilter()
|
||||
{
|
||||
@ -1745,7 +1702,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal("hekki", search.TextSearch);
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanParseFingerprint()
|
||||
{
|
||||
@ -1762,7 +1719,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(f1.ToString(), f2.ToString());
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async void CheckCORSSetOnBitpayAPI()
|
||||
{
|
||||
@ -1797,7 +1754,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task TestAccessBitpayAPI()
|
||||
{
|
||||
@ -1876,7 +1833,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanUseExchangeSpecificRate()
|
||||
{
|
||||
@ -1921,7 +1878,7 @@ namespace BTCPayServer.Tests
|
||||
return invoice2.CryptoInfo[0].Rate;
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanUseAnyoneCanCreateInvoice()
|
||||
{
|
||||
@ -1973,7 +1930,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanTweakRate()
|
||||
{
|
||||
@ -2020,7 +1977,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanModifyRates()
|
||||
{
|
||||
@ -2168,7 +2125,7 @@ namespace BTCPayServer.Tests
|
||||
await tester.StartAsync();
|
||||
await tester.EnsureChannelsSetup();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess(true);
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC", ScriptPubKeyType.Segwit);
|
||||
user.RegisterLightningNode("BTC", LightningConnectionType.CLightning);
|
||||
|
||||
@ -2208,9 +2165,10 @@ namespace BTCPayServer.Tests
|
||||
Assert.StartsWith("bitcoin:", paymentMethodSecond.InvoiceBitcoinUrlQR);
|
||||
var split = paymentMethodSecond.InvoiceBitcoinUrlQR.Split('?')[0];
|
||||
|
||||
// Standard for all uppercase characters in QR codes is still not implemented in all wallets
|
||||
// But we're proceeding with BECH32 being uppercase
|
||||
Assert.True($"bitcoin:{paymentMethodSecond.BtcAddress.ToUpperInvariant()}" == split);
|
||||
// Standard for uppercase Bech32 addresses in QR codes is still not implemented in all wallets
|
||||
// When it is widely propagated consider uncommenting these lines
|
||||
//Assert.True($"BITCOIN:{paymentMethodSecond.BtcAddress.ToUpperInvariant()}" == split);
|
||||
Assert.True($"bitcoin:{paymentMethodSecond.BtcAddress}" == split);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2225,7 +2183,7 @@ namespace BTCPayServer.Tests
|
||||
await tester.StartAsync();
|
||||
await tester.EnsureChannelsSetup();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess(true);
|
||||
user.GrantAccess();
|
||||
user.RegisterLightningNode("BTC", LightningConnectionType.Charge);
|
||||
var vm = Assert.IsType<CheckoutExperienceViewModel>(Assert
|
||||
.IsType<ViewResult>(user.GetController<StoresController>().CheckoutExperience()).Model);
|
||||
@ -2349,7 +2307,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.True(client.WaitAllRunning(default).Wait(100));
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void PosDataParser_ParsesCorrectly()
|
||||
{
|
||||
@ -2366,9 +2324,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
("{ invalidjson file here}",
|
||||
new Dictionary<string, object>() {{String.Empty, "{ invalidjson file here}"}})
|
||||
},
|
||||
// Duplicate keys should not crash things
|
||||
{("{ \"key\": true, \"key\": true}", new Dictionary<string, object>() {{"key", "True"}})}
|
||||
}
|
||||
};
|
||||
|
||||
testCases.ForEach(tuple =>
|
||||
@ -2377,7 +2333,7 @@ namespace BTCPayServer.Tests
|
||||
});
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task PosDataParser_ParsesCorrectly_Slower()
|
||||
{
|
||||
@ -2427,7 +2383,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanExportInvoicesJson()
|
||||
{
|
||||
@ -2506,7 +2462,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanChangeNetworkFeeMode()
|
||||
{
|
||||
@ -2597,7 +2553,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanExportInvoicesCsv()
|
||||
{
|
||||
@ -2639,7 +2595,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanCreateAndDeleteApps()
|
||||
{
|
||||
@ -2677,7 +2633,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanCreateStrangeInvoice()
|
||||
{
|
||||
@ -2723,7 +2679,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task InvoiceFlowThroughDifferentStatesCorrectly()
|
||||
{
|
||||
@ -2913,7 +2869,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanQueryDirectProviders()
|
||||
{
|
||||
@ -2984,7 +2940,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanExportBackgroundFetcherState()
|
||||
{
|
||||
@ -3026,7 +2982,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanGetRateCryptoCurrenciesByDefault()
|
||||
{
|
||||
@ -3078,7 +3034,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CheckLogsRoute()
|
||||
{
|
||||
@ -3161,37 +3117,7 @@ namespace BTCPayServer.Tests
|
||||
return name;
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanCheckFileNameValid()
|
||||
{
|
||||
var tests = new[]
|
||||
{
|
||||
("test.com", true),
|
||||
("/test.com", false),
|
||||
("te/st.com", false),
|
||||
("\\test.com", false),
|
||||
("te\\st.com", false)
|
||||
};
|
||||
foreach(var t in tests)
|
||||
{
|
||||
Assert.Equal(t.Item2, t.Item1.IsValidFileName());
|
||||
}
|
||||
}
|
||||
|
||||
[Trait("Fast", "Fast")]
|
||||
[Fact]
|
||||
public void CanFixupWebhookEventPropertyName()
|
||||
{
|
||||
string legacy = "{\"orignalDeliveryId\":\"blahblah\"}";
|
||||
var obj = JsonConvert.DeserializeObject<WebhookEvent>(legacy, WebhookEvent.DefaultSerializerSettings);
|
||||
Assert.Equal("blahblah", obj.OriginalDeliveryId);
|
||||
var serialized = JsonConvert.SerializeObject(obj, WebhookEvent.DefaultSerializerSettings);
|
||||
Assert.DoesNotContain("orignalDeliveryId", serialized);
|
||||
Assert.Contains("originalDeliveryId", serialized);
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Fast", "Fast")]
|
||||
public async Task CanCreateSqlitedb()
|
||||
{
|
||||
@ -3203,7 +3129,7 @@ namespace BTCPayServer.Tests
|
||||
await new ApplicationDbContext(builder.Options).Database.MigrateAsync();
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanUsePermission()
|
||||
{
|
||||
@ -3228,7 +3154,7 @@ namespace BTCPayServer.Tests
|
||||
.Contains(Permission.Create(Policies.CanModifyStoreSettings)));
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CheckRatesProvider()
|
||||
{
|
||||
@ -3316,7 +3242,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanLoginWithNoSecondaryAuthSystemsOrRequestItWhenAdded()
|
||||
{
|
||||
@ -3328,7 +3254,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
var accountController = tester.PayTester.GetController<AccountController>();
|
||||
|
||||
//no 2fa or fido2 enabled, login should work
|
||||
//no 2fa or u2f enabled, login should work
|
||||
Assert.Equal(nameof(HomeController.Index),
|
||||
Assert.IsType<RedirectToActionResult>(await accountController.Login(new LoginViewModel()
|
||||
{
|
||||
@ -3336,46 +3262,48 @@ namespace BTCPayServer.Tests
|
||||
Password = user.RegisterDetails.Password
|
||||
})).ActionName);
|
||||
|
||||
var manageController = user.GetController<Fido2Controller>();
|
||||
var manageController = user.GetController<ManageController>();
|
||||
|
||||
//by default no fido2 devices available
|
||||
//by default no u2f devices available
|
||||
Assert.Empty(Assert
|
||||
.IsType<Fido2AuthenticationViewModel>(Assert
|
||||
.IsType<ViewResult>(await manageController.List()).Model).Credentials);
|
||||
Assert.IsType<CredentialCreateOptions>(Assert
|
||||
.IsType<ViewResult>(await manageController.Create(new AddFido2CredentialViewModel()
|
||||
{
|
||||
Name = "label"
|
||||
})).Model);
|
||||
.IsType<U2FAuthenticationViewModel>(Assert
|
||||
.IsType<ViewResult>(await manageController.U2FAuthentication()).Model).Devices);
|
||||
var addRequest =
|
||||
Assert.IsType<AddU2FDeviceViewModel>(Assert
|
||||
.IsType<ViewResult>(manageController.AddU2FDevice("label")).Model);
|
||||
//name should match the one provided in beginning
|
||||
Assert.Equal("label", addRequest.Name);
|
||||
|
||||
//sending an invalid response model back to server, should error out
|
||||
Assert.IsType<RedirectToActionResult>(await manageController.CreateResponse("sdsdsa", "sds"));
|
||||
Assert.IsType<RedirectToActionResult>(await manageController.AddU2FDevice(addRequest));
|
||||
var statusModel = manageController.TempData.GetStatusMessageModel();
|
||||
Assert.Equal(StatusMessageModel.StatusSeverity.Error, statusModel.Severity);
|
||||
|
||||
var contextFactory = tester.PayTester.GetService<ApplicationDbContextFactory>();
|
||||
|
||||
//add a fake fido2 device in db directly since emulating a fido2 device is hard and annoying
|
||||
//add a fake u2f device in db directly since emulating a u2f device is hard and annoying
|
||||
using (var context = contextFactory.CreateContext())
|
||||
{
|
||||
var newDevice = new Fido2Credential()
|
||||
var newDevice = new U2FDevice()
|
||||
{
|
||||
Id = Guid.NewGuid().ToString(),
|
||||
Name = "fake",
|
||||
Type = Fido2Credential.CredentialType.FIDO2,
|
||||
Counter = 0,
|
||||
KeyHandle = UTF8Encoding.UTF8.GetBytes("fake"),
|
||||
PublicKey = UTF8Encoding.UTF8.GetBytes("fake"),
|
||||
AttestationCert = UTF8Encoding.UTF8.GetBytes("fake"),
|
||||
ApplicationUserId = user.UserId
|
||||
};
|
||||
newDevice.SetBlob(new Fido2CredentialBlob() { });
|
||||
await context.Fido2Credentials.AddAsync(newDevice);
|
||||
await context.U2FDevices.AddAsync(newDevice);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
Assert.NotNull(newDevice.Id);
|
||||
Assert.NotEmpty(Assert
|
||||
.IsType<Fido2AuthenticationViewModel>(Assert
|
||||
.IsType<ViewResult>(await manageController.List()).Model).Credentials);
|
||||
.IsType<U2FAuthenticationViewModel>(Assert
|
||||
.IsType<ViewResult>(await manageController.U2FAuthentication()).Model).Devices);
|
||||
}
|
||||
|
||||
//check if we are showing the fido2 login screen now
|
||||
//check if we are showing the u2f login screen now
|
||||
var secondLoginResult = Assert.IsType<ViewResult>(await accountController.Login(new LoginViewModel()
|
||||
{
|
||||
Email = user.RegisterDetails.Email,
|
||||
@ -3386,11 +3314,11 @@ namespace BTCPayServer.Tests
|
||||
var vm = Assert.IsType<SecondaryLoginViewModel>(secondLoginResult.Model);
|
||||
//2fa was never enabled for user so this should be empty
|
||||
Assert.Null(vm.LoginWith2FaViewModel);
|
||||
Assert.NotNull(vm.LoginWithFido2ViewModel);
|
||||
Assert.NotNull(vm.LoginWithU2FViewModel);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async void CheckOnionlocationForNonOnionHtmlRequests()
|
||||
{
|
||||
@ -3436,7 +3364,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanCheckForNewVersion()
|
||||
{
|
||||
@ -3481,87 +3409,8 @@ namespace BTCPayServer.Tests
|
||||
Assert.False(fn.Seen);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanDoLightningInternalNodeMigration()
|
||||
{
|
||||
using (var tester = ServerTester.Create(newDb: true))
|
||||
{
|
||||
tester.ActivateLightning(LightningConnectionType.CLightning);
|
||||
await tester.StartAsync();
|
||||
var acc = tester.NewAccount();
|
||||
await acc.GrantAccessAsync(true);
|
||||
await acc.CreateStoreAsync();
|
||||
|
||||
// Test if legacy DerivationStrategy column is converted to DerivationStrategies
|
||||
var store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
|
||||
var xpub = "tpubDDmH1briYfZcTDMEc7uMEA5hinzjUTzR9yMC1drxTMeiWyw1VyCqTuzBke6df2sqbfw9QG6wbgTLF5yLjcXsZNaXvJMZLwNEwyvmiFWcLav";
|
||||
var derivation = $"{xpub}-[legacy]";
|
||||
store.DerivationStrategy = derivation;
|
||||
await tester.PayTester.StoreRepository.UpdateStore(store);
|
||||
await RestartMigration(tester);
|
||||
store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
|
||||
Assert.True(string.IsNullOrEmpty(store.DerivationStrategy));
|
||||
var v = (DerivationSchemeSettings)store.GetSupportedPaymentMethods(tester.NetworkProvider).First();
|
||||
Assert.Equal(derivation, v.AccountDerivation.ToString());
|
||||
Assert.Equal(derivation, v.AccountOriginal.ToString());
|
||||
Assert.Equal(xpub, v.SigningKey.ToString());
|
||||
Assert.Equal(xpub, v.GetSigningAccountKeySettings().AccountKey.ToString());
|
||||
|
||||
await acc.RegisterLightningNodeAsync("BTC", LightningConnectionType.CLightning, true);
|
||||
store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
|
||||
var lnMethod = store.GetSupportedPaymentMethods(tester.NetworkProvider).OfType<LightningSupportedPaymentMethod>().First();
|
||||
Assert.NotNull(lnMethod.GetExternalLightningUrl());
|
||||
await RestartMigration(tester);
|
||||
|
||||
store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
|
||||
lnMethod = store.GetSupportedPaymentMethods(tester.NetworkProvider).OfType<LightningSupportedPaymentMethod>().First();
|
||||
Assert.Null(lnMethod.GetExternalLightningUrl());
|
||||
|
||||
// Test if legacy lightning charge settings are converted to LightningConnectionString
|
||||
store.DerivationStrategies = new JObject()
|
||||
{
|
||||
new JProperty("BTC_LightningLike", new JObject()
|
||||
{
|
||||
new JProperty("LightningChargeUrl", "http://mycharge.com/"),
|
||||
new JProperty("Username", "usr"),
|
||||
new JProperty("Password", "pass"),
|
||||
new JProperty("CryptoCode", "BTC"),
|
||||
new JProperty("PaymentId", "someshit"),
|
||||
})
|
||||
}.ToString();
|
||||
await tester.PayTester.StoreRepository.UpdateStore(store);
|
||||
await RestartMigration(tester);
|
||||
|
||||
store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
|
||||
lnMethod = store.GetSupportedPaymentMethods(tester.NetworkProvider).OfType<LightningSupportedPaymentMethod>().First();
|
||||
Assert.NotNull(lnMethod.GetExternalLightningUrl());
|
||||
|
||||
var url = lnMethod.GetExternalLightningUrl();
|
||||
Assert.Equal(LightningConnectionType.Charge, url.ConnectionType);
|
||||
Assert.Equal("pass", url.Password);
|
||||
Assert.Equal("usr", url.Username);
|
||||
|
||||
// Test if lightning connection strings get migrated to internal
|
||||
store.DerivationStrategies = new JObject()
|
||||
{
|
||||
new JProperty("BTC_LightningLike", new JObject()
|
||||
{
|
||||
new JProperty("CryptoCode", "BTC"),
|
||||
new JProperty("LightningConnectionString", tester.PayTester.IntegratedLightning),
|
||||
})
|
||||
}.ToString();
|
||||
await tester.PayTester.StoreRepository.UpdateStore(store);
|
||||
await RestartMigration(tester);
|
||||
store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
|
||||
lnMethod = store.GetSupportedPaymentMethods(tester.NetworkProvider).OfType<LightningSupportedPaymentMethod>().First();
|
||||
Assert.True(lnMethod.IsInternalNode);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanDoInvoiceMigrations()
|
||||
{
|
||||
@ -3574,7 +3423,7 @@ namespace BTCPayServer.Tests
|
||||
await acc.CreateStoreAsync();
|
||||
await acc.RegisterDerivationSchemeAsync("BTC");
|
||||
var store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
|
||||
|
||||
|
||||
var blob = store.GetStoreBlob();
|
||||
var serializer = new Serializer(null);
|
||||
|
||||
@ -3595,10 +3444,10 @@ namespace BTCPayServer.Tests
|
||||
new KeyPath("44'/0'/0'").ToString()
|
||||
}
|
||||
})));
|
||||
|
||||
|
||||
blob.AdditionalData.Add("networkFeeDisabled", JToken.Parse(
|
||||
serializer.ToString((bool?)true)));
|
||||
|
||||
|
||||
blob.AdditionalData.Add("onChainMinValue", JToken.Parse(
|
||||
serializer.ToString(new CurrencyValue()
|
||||
{
|
||||
@ -3611,13 +3460,18 @@ namespace BTCPayServer.Tests
|
||||
Currency = "USD",
|
||||
Value = 5m
|
||||
}.ToString())));
|
||||
|
||||
|
||||
store.SetStoreBlob(blob);
|
||||
await tester.PayTester.StoreRepository.UpdateStore(store);
|
||||
await RestartMigration(tester);
|
||||
|
||||
var settings = tester.PayTester.GetService<SettingsRepository>();
|
||||
await settings.UpdateSetting<MigrationSettings>(new MigrationSettings());
|
||||
var migrationStartupTask = tester.PayTester.GetService<IServiceProvider>().GetServices<IStartupTask>()
|
||||
.Single(task => task is MigrationStartupTask);
|
||||
await migrationStartupTask.ExecuteAsync();
|
||||
|
||||
|
||||
store = await tester.PayTester.StoreRepository.FindStore(acc.StoreId);
|
||||
|
||||
|
||||
blob = store.GetStoreBlob();
|
||||
Assert.Empty(blob.AdditionalData);
|
||||
Assert.Single(blob.PaymentMethodCriteria);
|
||||
@ -3632,17 +3486,8 @@ namespace BTCPayServer.Tests
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task RestartMigration(ServerTester tester)
|
||||
{
|
||||
var settings = tester.PayTester.GetService<SettingsRepository>();
|
||||
await settings.UpdateSetting<MigrationSettings>(new MigrationSettings());
|
||||
var migrationStartupTask = tester.PayTester.GetService<IServiceProvider>().GetServices<IStartupTask>()
|
||||
.Single(task => task is MigrationStartupTask);
|
||||
await migrationStartupTask.ExecuteAsync();
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task EmailSenderTests()
|
||||
{
|
||||
|
@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
|
@ -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"
|
||||
@ -227,7 +225,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.12.1-beta
|
||||
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.12.1-beta
|
||||
image: btcpayserver/lnd:v0.12.0-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
|
@ -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"
|
||||
@ -215,7 +213,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.12.1-beta
|
||||
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.12.1-beta
|
||||
image: btcpayserver/lnd:v0.12.0-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
|
@ -44,15 +44,12 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BIP78.Sender" Version="0.2.0" />
|
||||
<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,6 +81,7 @@
|
||||
<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" />
|
||||
@ -138,6 +136,7 @@
|
||||
<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>
|
||||
|
||||
|
@ -2,23 +2,22 @@
|
||||
@inject UserManager<ApplicationUser> UserManager
|
||||
@inject CssThemeManager CssThemeManager
|
||||
@using BTCPayServer.HostedServices
|
||||
@using BTCPayServer.Views.Notifications
|
||||
@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="navbarDropdown" role="button" data-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 badge-pill badge-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>
|
||||
<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 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>
|
||||
<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)
|
||||
@ -29,7 +28,7 @@
|
||||
</div>
|
||||
|
||||
<div class="notification-item__content">
|
||||
<div class="text-left text-wrap">
|
||||
<div class="text-left text-wrap font-weight-semibold">
|
||||
@notif.Body
|
||||
</div>
|
||||
<div class="text-left d-flex">
|
||||
@ -39,7 +38,7 @@
|
||||
</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>
|
||||
@ -47,8 +46,8 @@
|
||||
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>
|
||||
}
|
||||
@ -57,7 +56,7 @@ else
|
||||
if (!disabled)
|
||||
{
|
||||
var user = await UserManager.GetUserAsync(User);
|
||||
disabled = user?.DisabledNotifications == "all";
|
||||
disabled = user.DisabledNotifications == "all";
|
||||
}
|
||||
}
|
||||
@if (!disabled)
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
|
@ -16,22 +16,23 @@ namespace BTCPayServer.Controllers
|
||||
public string StoreId { get; set; }
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Empty;
|
||||
return String.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("{appId}/settings/crowdfund")]
|
||||
|
||||
[HttpGet]
|
||||
[Route("{appId}/settings/crowdfund")]
|
||||
public async Task<IActionResult> UpdateCrowdfund(string appId)
|
||||
{
|
||||
var app = await GetOwnedApp(appId, AppType.Crowdfund);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<CrowdfundSettings>();
|
||||
var vm = new UpdateCrowdfundViewModel
|
||||
var vm = new UpdateCrowdfundViewModel()
|
||||
{
|
||||
Title = settings.Title,
|
||||
StoreId = app.StoreDataId,
|
||||
StoreName = app.StoreData?.StoreName,
|
||||
Enabled = settings.Enabled,
|
||||
EnforceTargetAmount = settings.EnforceTargetAmount,
|
||||
StartDate = settings.StartDate,
|
||||
@ -70,7 +71,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
try
|
||||
{
|
||||
vm.PerksTemplate = _AppService.SerializeTemplate(_AppService.Parse(vm.PerksTemplate, vm.TargetCurrency));
|
||||
_AppService.Parse(vm.PerksTemplate, vm.TargetCurrency).ToString();
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
@ -97,11 +97,10 @@ namespace BTCPayServer.Controllers
|
||||
settings.DefaultView = settings.EnableShoppingCart ? PosViewType.Cart : settings.DefaultView;
|
||||
settings.EnableShoppingCart = false;
|
||||
|
||||
var vm = new UpdatePointOfSaleViewModel
|
||||
var vm = new UpdatePointOfSaleViewModel()
|
||||
{
|
||||
Id = appId,
|
||||
StoreId = app.StoreDataId,
|
||||
StoreName = app.StoreData?.StoreName,
|
||||
Title = settings.Title,
|
||||
DefaultView = settings.DefaultView,
|
||||
ShowCustomAmount = settings.ShowCustomAmount,
|
||||
@ -162,16 +161,11 @@ namespace BTCPayServer.Controllers
|
||||
[Route("{appId}/settings/pos")]
|
||||
public async Task<IActionResult> UpdatePointOfSale(string appId, UpdatePointOfSaleViewModel vm)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
if (_currencies.GetCurrencyData(vm.Currency, false) == null)
|
||||
ModelState.AddModelError(nameof(vm.Currency), "Invalid currency");
|
||||
try
|
||||
{
|
||||
vm.Template = _AppService.SerializeTemplate(_AppService.Parse(vm.Template, vm.Currency));
|
||||
_AppService.Parse(vm.Template, vm.Currency);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@ -184,7 +178,7 @@ namespace BTCPayServer.Controllers
|
||||
var app = await GetOwnedApp(appId, AppType.PointOfSale);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
app.SetSettings(new PointOfSaleSettings
|
||||
app.SetSettings(new PointOfSaleSettings()
|
||||
{
|
||||
Title = vm.Title,
|
||||
DefaultView = vm.DefaultView,
|
||||
|
@ -57,10 +57,8 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("/")]
|
||||
[Route("/apps/{appId}/pos/{viewType?}")]
|
||||
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
|
||||
[DomainMappingConstraint(AppType.PointOfSale)]
|
||||
public async Task<IActionResult> ViewPointOfSale(string appId, PosViewType? viewType = null)
|
||||
{
|
||||
var app = await _AppService.GetApp(appId, AppType.PointOfSale);
|
||||
@ -106,12 +104,10 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("/")]
|
||||
[Route("/apps/{appId}/pos/{viewType?}")]
|
||||
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
|
||||
[IgnoreAntiforgeryToken]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
[DomainMappingConstraint(AppType.PointOfSale)]
|
||||
public async Task<IActionResult> ViewPointOfSale(string appId,
|
||||
PosViewType viewType,
|
||||
[ModelBinder(typeof(InvariantDecimalModelBinder))] decimal amount,
|
||||
@ -236,10 +232,8 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("/")]
|
||||
[Route("/apps/{appId}/crowdfund")]
|
||||
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
|
||||
[DomainMappingConstraintAttribute(AppType.Crowdfund)]
|
||||
public async Task<IActionResult> ViewCrowdfund(string appId, string statusMessage)
|
||||
{
|
||||
var app = await _AppService.GetApp(appId, AppType.Crowdfund, true);
|
||||
@ -269,12 +263,10 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("/")]
|
||||
[Route("/apps/{appId}/crowdfund")]
|
||||
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
|
||||
[IgnoreAntiforgeryToken]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
[DomainMappingConstraintAttribute(AppType.Crowdfund)]
|
||||
public async Task<IActionResult> ContributeToCrowdfund(string appId, ContributeToCrowdfund request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (request.Amount <= 0)
|
||||
|
@ -8,11 +8,6 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
public static class GreenFieldUtils
|
||||
{
|
||||
public static IActionResult CreateValidationError(this ControllerBase controller, ModelStateDictionary modelState)
|
||||
{
|
||||
return controller.UnprocessableEntity(modelState.ToGreenfieldValidationError());
|
||||
}
|
||||
|
||||
public static List<GreenfieldValidationError> ToGreenfieldValidationError(this ModelStateDictionary modelState)
|
||||
{
|
||||
List<GreenfieldValidationError> errors = new List<GreenfieldValidationError>();
|
||||
foreach (var error in modelState)
|
||||
@ -22,17 +17,11 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
errors.Add(new GreenfieldValidationError(error.Key, errorMessage.ErrorMessage));
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
return controller.UnprocessableEntity(errors.ToArray());
|
||||
}
|
||||
|
||||
public static IActionResult CreateAPIError(this ControllerBase controller, string errorCode, string errorMessage)
|
||||
{
|
||||
return controller.BadRequest(new GreenfieldAPIError(errorCode, errorMessage));
|
||||
}
|
||||
public static IActionResult CreateAPIError(this ControllerBase controller, int httpCode, string errorCode, string errorMessage)
|
||||
{
|
||||
return controller.StatusCode(httpCode, new GreenfieldAPIError(errorCode, errorMessage));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,18 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Conventions;
|
||||
using NBitcoin;
|
||||
using CreateInvoiceRequest = BTCPayServer.Client.Models.CreateInvoiceRequest;
|
||||
using InvoiceData = BTCPayServer.Client.Models.InvoiceData;
|
||||
@ -26,65 +27,39 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
private readonly InvoiceController _invoiceController;
|
||||
private readonly InvoiceRepository _invoiceRepository;
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary;
|
||||
|
||||
public LanguageService LanguageService { get; }
|
||||
|
||||
public GreenFieldInvoiceController(InvoiceController invoiceController, InvoiceRepository invoiceRepository,
|
||||
LinkGenerator linkGenerator, LanguageService languageService, BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
EventAggregator eventAggregator, PaymentMethodHandlerDictionary paymentMethodHandlerDictionary)
|
||||
public GreenFieldInvoiceController(InvoiceController invoiceController, InvoiceRepository invoiceRepository, LinkGenerator linkGenerator, LanguageService languageService)
|
||||
{
|
||||
_invoiceController = invoiceController;
|
||||
_invoiceRepository = invoiceRepository;
|
||||
_linkGenerator = linkGenerator;
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_eventAggregator = eventAggregator;
|
||||
_paymentMethodHandlerDictionary = paymentMethodHandlerDictionary;
|
||||
LanguageService = languageService;
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanViewInvoices,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/invoices")]
|
||||
public async Task<IActionResult> GetInvoices(string storeId, [FromQuery] string[] orderId = null, [FromQuery] string[] status = null,
|
||||
[FromQuery]
|
||||
[ModelBinder(typeof(ModelBinders.DateTimeOffsetModelBinder))]
|
||||
DateTimeOffset? startDate = null,
|
||||
[FromQuery]
|
||||
[ModelBinder(typeof(ModelBinders.DateTimeOffsetModelBinder))]
|
||||
DateTimeOffset? endDate = null, [FromQuery] bool includeArchived = false)
|
||||
public async Task<IActionResult> GetInvoices(string storeId, bool includeArchived = false)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return StoreNotFound();
|
||||
}
|
||||
if (startDate is DateTimeOffset s &&
|
||||
endDate is DateTimeOffset e &&
|
||||
s > e)
|
||||
{
|
||||
this.ModelState.AddModelError(nameof(startDate), "startDate should not be above endDate");
|
||||
this.ModelState.AddModelError(nameof(endDate), "endDate should not be below startDate");
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
var invoices =
|
||||
await _invoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
StoreId = new[] {store.Id},
|
||||
IncludeArchived = includeArchived,
|
||||
StartDate = startDate,
|
||||
EndDate = endDate,
|
||||
OrderId = orderId,
|
||||
Status = status
|
||||
StoreId = new[] { store.Id },
|
||||
IncludeArchived = includeArchived
|
||||
});
|
||||
|
||||
return Ok(invoices.Select(ToModel));
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Authorize(Policy = Policies.CanViewInvoices,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/invoices/{invoiceId}")]
|
||||
@ -93,13 +68,13 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return InvoiceNotFound();
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
|
||||
if (invoice?.StoreId != store.Id)
|
||||
if (invoice.StoreId != store.Id)
|
||||
{
|
||||
return InvoiceNotFound();
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Ok(ToModel(invoice));
|
||||
@ -113,13 +88,9 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return InvoiceNotFound();
|
||||
}
|
||||
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
|
||||
if (invoice?.StoreId != store.Id)
|
||||
{
|
||||
return InvoiceNotFound();
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
await _invoiceRepository.ToggleInvoiceArchival(invoiceId, true, storeId);
|
||||
return Ok();
|
||||
}
|
||||
@ -132,7 +103,7 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return InvoiceNotFound();
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var result = await _invoiceRepository.UpdateInvoiceMetadata(invoiceId, storeId, request.Metadata);
|
||||
@ -141,7 +112,7 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
return Ok(ToModel(result));
|
||||
}
|
||||
|
||||
return InvoiceNotFound();
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanCreateInvoice,
|
||||
@ -152,7 +123,7 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return StoreNotFound();
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (request.Amount < 0.0m)
|
||||
@ -229,19 +200,19 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return InvoiceNotFound();
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
|
||||
if (invoice.StoreId != store.Id)
|
||||
{
|
||||
return InvoiceNotFound();
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (!await _invoiceRepository.MarkInvoiceStatus(invoice.Id, request.Status))
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.Status),
|
||||
"Status can only be marked to invalid or settled within certain conditions.");
|
||||
"Status can only be marked to invalid or complete within certain conditions.");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
@ -258,13 +229,13 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return InvoiceNotFound();
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
|
||||
if (invoice.StoreId != store.Id)
|
||||
{
|
||||
return InvoiceNotFound();
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (!invoice.Archived)
|
||||
@ -283,72 +254,35 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
[Authorize(Policy = Policies.CanViewInvoices,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods")]
|
||||
public async Task<IActionResult> GetInvoicePaymentMethods(string storeId, string invoiceId, bool onlyAccountedPayments = true)
|
||||
public async Task<IActionResult> GetInvoicePaymentMethods(string storeId, string invoiceId)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return InvoiceNotFound();
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
|
||||
if (invoice?.StoreId != store.Id)
|
||||
{
|
||||
return InvoiceNotFound();
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Ok(ToPaymentMethodModels(invoice, onlyAccountedPayments));
|
||||
return Ok(ToPaymentMethodModels(invoice));
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanViewInvoices,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods/{paymentMethod}/activate")]
|
||||
public async Task<IActionResult> ActivateInvoicePaymentMethod(string storeId, string invoiceId, string paymentMethod)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return InvoiceNotFound();
|
||||
}
|
||||
|
||||
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
|
||||
if (invoice?.StoreId != store.Id)
|
||||
{
|
||||
return InvoiceNotFound();
|
||||
}
|
||||
|
||||
if (PaymentMethodId.TryParse(paymentMethod, out var paymentMethodId))
|
||||
{
|
||||
await _invoiceRepository.ActivateInvoicePaymentMethod(_eventAggregator, _btcPayNetworkProvider,
|
||||
_paymentMethodHandlerDictionary, store, invoice, paymentMethodId);
|
||||
return Ok();
|
||||
}
|
||||
ModelState.AddModelError(nameof(paymentMethod), "Invalid payment method");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
private IActionResult InvoiceNotFound()
|
||||
{
|
||||
return this.CreateAPIError(404, "invoice-not-found", "The invoice was not found");
|
||||
}
|
||||
private IActionResult StoreNotFound()
|
||||
{
|
||||
return this.CreateAPIError(404, "store-not-found", "The store was not found");
|
||||
}
|
||||
|
||||
private InvoicePaymentMethodDataModel[] ToPaymentMethodModels(InvoiceEntity entity, bool includeAccountedPaymentOnly)
|
||||
|
||||
private InvoicePaymentMethodDataModel[] ToPaymentMethodModels(InvoiceEntity entity)
|
||||
{
|
||||
return entity.GetPaymentMethods().Select(
|
||||
method =>
|
||||
{
|
||||
var accounting = method.Calculate();
|
||||
var details = method.GetPaymentMethodDetails();
|
||||
var payments = method.ParentEntity.GetPayments(includeAccountedPaymentOnly).Where(paymentEntity =>
|
||||
var payments = method.ParentEntity.GetPayments().Where(paymentEntity =>
|
||||
paymentEntity.GetPaymentMethodId() == method.GetId());
|
||||
|
||||
return new InvoicePaymentMethodDataModel()
|
||||
{
|
||||
Activated = details.Activated,
|
||||
PaymentMethod = method.GetId().ToStringNormalized(),
|
||||
Destination = details.GetPaymentDestination(),
|
||||
Rate = method.Rate,
|
||||
@ -403,9 +337,7 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
PaymentMethods =
|
||||
entity.GetPaymentMethods().Select(method => method.GetId().ToStringNormalized()).ToArray(),
|
||||
SpeedPolicy = entity.SpeedPolicy,
|
||||
DefaultLanguage = entity.DefaultLanguage,
|
||||
RedirectAutomatically = entity.RedirectAutomatically,
|
||||
RedirectURL = entity.RedirectURLTemplate
|
||||
DefaultLanguage = entity.DefaultLanguage
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -28,9 +28,8 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
public InternalLightningNodeApiController(
|
||||
BTCPayNetworkProvider btcPayNetworkProvider, BTCPayServerEnvironment btcPayServerEnvironment,
|
||||
CssThemeManager cssThemeManager, LightningClientFactoryService lightningClientFactory,
|
||||
IOptions<LightningNetworkOptions> lightningNetworkOptions,
|
||||
IAuthorizationService authorizationService) : base(
|
||||
btcPayNetworkProvider, btcPayServerEnvironment, cssThemeManager, authorizationService)
|
||||
IOptions<LightningNetworkOptions> lightningNetworkOptions ) : base(
|
||||
btcPayNetworkProvider, btcPayServerEnvironment, cssThemeManager)
|
||||
{
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_lightningClientFactory = lightningClientFactory;
|
||||
@ -101,17 +100,17 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
return base.CreateInvoice(cryptoCode, request);
|
||||
}
|
||||
|
||||
protected override async Task<ILightningClient> GetLightningClient(string cryptoCode, bool doingAdminThings)
|
||||
protected override Task<ILightningClient> GetLightningClient(string cryptoCode, bool doingAdminThings)
|
||||
{
|
||||
_lightningNetworkOptions.Value.InternalLightningByCryptoCode.TryGetValue(cryptoCode,
|
||||
out var internalLightningNode);
|
||||
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
||||
if (network == null ||
|
||||
!_lightningNetworkOptions.Value.InternalLightningByCryptoCode.TryGetValue(network.CryptoCode,
|
||||
out var internalLightningNode) ||
|
||||
!await CanUseInternalLightning(doingAdminThings))
|
||||
if (network == null || !CanUseInternalLightning(doingAdminThings) || internalLightningNode == null)
|
||||
{
|
||||
return null;
|
||||
return Task.FromResult<ILightningClient>(null);
|
||||
}
|
||||
return _lightningClientFactory.Create(internalLightningNode, network);
|
||||
|
||||
return Task.FromResult(_lightningClientFactory.Create(internalLightningNode, network));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,9 +31,8 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
public StoreLightningNodeApiController(
|
||||
IOptions<LightningNetworkOptions> lightningNetworkOptions,
|
||||
LightningClientFactoryService lightningClientFactory, BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
BTCPayServerEnvironment btcPayServerEnvironment, CssThemeManager cssThemeManager,
|
||||
IAuthorizationService authorizationService) : base(
|
||||
btcPayNetworkProvider, btcPayServerEnvironment, cssThemeManager, authorizationService)
|
||||
BTCPayServerEnvironment btcPayServerEnvironment, CssThemeManager cssThemeManager) : base(
|
||||
btcPayNetworkProvider, btcPayServerEnvironment, cssThemeManager)
|
||||
{
|
||||
_lightningNetworkOptions = lightningNetworkOptions;
|
||||
_lightningClientFactory = lightningClientFactory;
|
||||
@ -101,10 +100,11 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
return base.CreateInvoice(cryptoCode, request);
|
||||
}
|
||||
|
||||
protected override async Task<ILightningClient> GetLightningClient(string cryptoCode,
|
||||
protected override Task<ILightningClient> GetLightningClient(string cryptoCode,
|
||||
bool doingAdminThings)
|
||||
{
|
||||
|
||||
_lightningNetworkOptions.Value.InternalLightningByCryptoCode.TryGetValue(cryptoCode,
|
||||
out var internalLightningNode);
|
||||
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
||||
|
||||
var store = HttpContext.GetStoreData();
|
||||
@ -117,20 +117,13 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
var existing = store.GetSupportedPaymentMethods(_btcPayNetworkProvider)
|
||||
.OfType<LightningSupportedPaymentMethod>()
|
||||
.FirstOrDefault(d => d.PaymentId == id);
|
||||
if (existing == null)
|
||||
return null;
|
||||
if (existing.GetExternalLightningUrl() is LightningConnectionString connectionString)
|
||||
if (existing == null || (existing.GetLightningUrl().IsInternalNode(internalLightningNode) &&
|
||||
!CanUseInternalLightning(doingAdminThings)))
|
||||
{
|
||||
return _lightningClientFactory.Create(connectionString, network);
|
||||
return Task.FromResult<ILightningClient>(null);
|
||||
}
|
||||
else if (
|
||||
await CanUseInternalLightning(doingAdminThings) &&
|
||||
_lightningNetworkOptions.Value.InternalLightningByCryptoCode.TryGetValue(network.CryptoCode,
|
||||
out var internalLightningNode))
|
||||
{
|
||||
return _lightningClientFactory.Create(internalLightningNode, network);
|
||||
}
|
||||
return null;
|
||||
|
||||
return Task.FromResult(_lightningClientFactory.Create(existing.GetLightningUrl(), network));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,13 +2,10 @@ using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Newtonsoft.Json.Linq;
|
||||
@ -35,16 +32,13 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
|
||||
private readonly CssThemeManager _cssThemeManager;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
|
||||
protected LightningNodeApiController(BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
BTCPayServerEnvironment btcPayServerEnvironment, CssThemeManager cssThemeManager,
|
||||
IAuthorizationService authorizationService)
|
||||
BTCPayServerEnvironment btcPayServerEnvironment, CssThemeManager cssThemeManager)
|
||||
{
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_btcPayServerEnvironment = btcPayServerEnvironment;
|
||||
_cssThemeManager = cssThemeManager;
|
||||
_authorizationService = authorizationService;
|
||||
}
|
||||
|
||||
public virtual async Task<IActionResult> GetInfo(string cryptoCode)
|
||||
@ -300,11 +294,10 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
};
|
||||
}
|
||||
|
||||
protected async Task<bool> CanUseInternalLightning(bool doingAdminThings)
|
||||
protected bool CanUseInternalLightning(bool doingAdminThings)
|
||||
{
|
||||
return (!doingAdminThings && _cssThemeManager.AllowLightningInternalNodeForAll) ||
|
||||
(await _authorizationService.AuthorizeAsync(User, null,
|
||||
new PolicyRequirement(Policies.CanUseInternalLightningNode))).Succeeded;
|
||||
return (_btcPayServerEnvironment.IsDeveloping || User.IsInRole(Roles.ServerAdmin) ||
|
||||
(_cssThemeManager.AllowLightningInternalNodeForAll && !doingAdminThings));
|
||||
}
|
||||
|
||||
protected abstract Task<ILightningClient> GetLightningClient(string cryptoCode, bool doingAdminThings);
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@ -32,15 +31,13 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
private readonly CurrencyNameTable _currencyNameTable;
|
||||
private readonly BTCPayNetworkJsonSerializerSettings _serializerSettings;
|
||||
private readonly BTCPayNetworkProvider _networkProvider;
|
||||
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
|
||||
|
||||
public GreenfieldPullPaymentController(PullPaymentHostedService pullPaymentService,
|
||||
LinkGenerator linkGenerator,
|
||||
ApplicationDbContextFactory dbContextFactory,
|
||||
CurrencyNameTable currencyNameTable,
|
||||
Services.BTCPayNetworkJsonSerializerSettings serializerSettings,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
IEnumerable<IPayoutHandler> payoutHandlers)
|
||||
BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
_pullPaymentService = pullPaymentService;
|
||||
_linkGenerator = linkGenerator;
|
||||
@ -48,7 +45,6 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
_currencyNameTable = currencyNameTable;
|
||||
_serializerSettings = serializerSettings;
|
||||
_networkProvider = networkProvider;
|
||||
_payoutHandlers = payoutHandlers;
|
||||
}
|
||||
|
||||
[HttpGet("~/api/v1/stores/{storeId}/pull-payments")]
|
||||
@ -182,7 +178,7 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
return NotFound();
|
||||
using var ctx = _dbContextFactory.CreateContext();
|
||||
var payouts = await ctx.Payouts.Where(p => p.PullPaymentDataId == pullPaymentId)
|
||||
.Where(p => p.State != PayoutState.Cancelled || includeCancelled)
|
||||
.Where(p => p.State != Data.PayoutState.Cancelled || includeCancelled)
|
||||
.ToListAsync();
|
||||
var cd = _currencyNameTable.GetCurrencyData(pp.GetBlob().Currency, false);
|
||||
return base.Ok(payouts
|
||||
@ -200,9 +196,14 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
Amount = blob.Amount,
|
||||
PaymentMethodAmount = blob.CryptoAmount,
|
||||
Revision = blob.Revision,
|
||||
State = p.State
|
||||
State = p.State == Data.PayoutState.AwaitingPayment ? Client.Models.PayoutState.AwaitingPayment :
|
||||
p.State == Data.PayoutState.AwaitingApproval ? Client.Models.PayoutState.AwaitingApproval :
|
||||
p.State == Data.PayoutState.Cancelled ? Client.Models.PayoutState.Cancelled :
|
||||
p.State == Data.PayoutState.Completed ? Client.Models.PayoutState.Completed :
|
||||
p.State == Data.PayoutState.InProgress ? Client.Models.PayoutState.InProgress :
|
||||
throw new NotSupportedException(),
|
||||
};
|
||||
model.Destination = blob.Destination;
|
||||
model.Destination = blob.Destination.ToString();
|
||||
model.PaymentMethod = p.PaymentMethodId;
|
||||
return model;
|
||||
}
|
||||
@ -213,14 +214,10 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
{
|
||||
if (request is null)
|
||||
return NotFound();
|
||||
if (!PaymentMethodId.TryParse(request?.PaymentMethod, out var paymentMethodId))
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.PaymentMethod), "Invalid payment method");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
var payoutHandler = _payoutHandlers.FirstOrDefault(handler => handler.CanHandle(paymentMethodId));
|
||||
if (payoutHandler is null)
|
||||
|
||||
var network = request?.PaymentMethod is string paymentMethod ?
|
||||
this._networkProvider.GetNetwork<BTCPayNetwork>(paymentMethod) : null;
|
||||
if (network is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.PaymentMethod), "Invalid payment method");
|
||||
return this.CreateValidationError(ModelState);
|
||||
@ -231,8 +228,7 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
if (pp is null)
|
||||
return NotFound();
|
||||
var ppBlob = pp.GetBlob();
|
||||
IClaimDestination destination = await payoutHandler.ParseClaimDestination(paymentMethodId,request.Destination);
|
||||
if (destination is null)
|
||||
if (request.Destination is null || !ClaimDestination.TryParse(request.Destination, network, out var destination))
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.Destination), "The destination must be an address or a BIP21 URI");
|
||||
return this.CreateValidationError(ModelState);
|
||||
@ -249,7 +245,7 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
Destination = destination,
|
||||
PullPaymentId = pullPaymentId,
|
||||
Value = request.Amount,
|
||||
PaymentMethodId = paymentMethodId
|
||||
PaymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike)
|
||||
});
|
||||
switch (result.Result)
|
||||
{
|
||||
|
@ -1,211 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
namespace BTCPayServer.Controllers.GreenField
|
||||
{
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public class StoreLightningNetworkPaymentMethodsController : ControllerBase
|
||||
{
|
||||
private StoreData Store => HttpContext.GetStoreData();
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||
private readonly IOptions<LightningNetworkOptions> _lightningNetworkOptions;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly CssThemeManager _cssThemeManager;
|
||||
|
||||
public StoreLightningNetworkPaymentMethodsController(
|
||||
StoreRepository storeRepository,
|
||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
IOptions<LightningNetworkOptions> lightningNetworkOptions,
|
||||
IAuthorizationService authorizationService,
|
||||
CssThemeManager cssThemeManager)
|
||||
{
|
||||
_storeRepository = storeRepository;
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_lightningNetworkOptions = lightningNetworkOptions;
|
||||
_authorizationService = authorizationService;
|
||||
_cssThemeManager = cssThemeManager;
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/LightningNetwork")]
|
||||
public ActionResult<IEnumerable<LightningNetworkPaymentMethodData>> GetLightningPaymentMethods(
|
||||
[FromQuery] bool enabledOnly = false)
|
||||
{
|
||||
var blob = Store.GetStoreBlob();
|
||||
var excludedPaymentMethods = blob.GetExcludedPaymentMethods();
|
||||
return Ok(Store.GetSupportedPaymentMethods(_btcPayNetworkProvider)
|
||||
.Where((method) => method.PaymentId.PaymentType == PaymentTypes.LightningLike)
|
||||
.OfType<LightningSupportedPaymentMethod>()
|
||||
.Select(paymentMethod =>
|
||||
new LightningNetworkPaymentMethodData(paymentMethod.PaymentId.CryptoCode,
|
||||
paymentMethod.GetExternalLightningUrl().ToString(), !excludedPaymentMethods.Match(paymentMethod.PaymentId)))
|
||||
.Where((result) => !enabledOnly || result.Enabled)
|
||||
.ToList()
|
||||
);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/LightningNetwork/{cryptoCode}")]
|
||||
public ActionResult<LightningNetworkPaymentMethodData> GetLightningNetworkPaymentMethod(string cryptoCode)
|
||||
{
|
||||
if (!GetNetwork(cryptoCode, out BTCPayNetwork _))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var method = GetExistingLightningLikePaymentMethod(cryptoCode);
|
||||
if (method is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
return Ok(method);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpDelete("~/api/v1/stores/{storeId}/payment-methods/LightningNetwork/{cryptoCode}")]
|
||||
public async Task<IActionResult> RemoveLightningNetworkPaymentMethod(
|
||||
string cryptoCode,
|
||||
int offset = 0, int amount = 10)
|
||||
{
|
||||
if (!GetNetwork(cryptoCode, out BTCPayNetwork _))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
|
||||
var store = Store;
|
||||
store.SetSupportedPaymentMethod(id, null);
|
||||
await _storeRepository.UpdateStore(store);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPut("~/api/v1/stores/{storeId}/payment-methods/LightningNetwork/{cryptoCode}")]
|
||||
public async Task<IActionResult> UpdateLightningNetworkPaymentMethod(string cryptoCode,
|
||||
[FromBody] LightningNetworkPaymentMethodData paymentMethodData)
|
||||
{
|
||||
var paymentMethodId = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
|
||||
|
||||
if (!GetNetwork(cryptoCode, out var network))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(paymentMethodData?.ConnectionString))
|
||||
{
|
||||
ModelState.AddModelError(nameof(LightningNetworkPaymentMethodData.ConnectionString),
|
||||
"Missing connectionString");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
|
||||
LightningSupportedPaymentMethod paymentMethod = null;
|
||||
if (!string.IsNullOrEmpty(paymentMethodData.ConnectionString))
|
||||
{
|
||||
if (paymentMethodData.ConnectionString == LightningSupportedPaymentMethod.InternalNode)
|
||||
{
|
||||
if (!await CanUseInternalLightning())
|
||||
{
|
||||
ModelState.AddModelError(nameof(paymentMethodData.ConnectionString), $"You are not authorized to use the internal lightning node");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
paymentMethod = new Payments.Lightning.LightningSupportedPaymentMethod()
|
||||
{
|
||||
CryptoCode = paymentMethodId.CryptoCode
|
||||
};
|
||||
paymentMethod.SetInternalNode();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!LightningConnectionString.TryParse(paymentMethodData.ConnectionString, false,
|
||||
out var connectionString, out var error))
|
||||
{
|
||||
ModelState.AddModelError(nameof(paymentMethodData.ConnectionString), $"Invalid URL ({error})");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
if (connectionString.ConnectionType == LightningConnectionType.LndGRPC)
|
||||
{
|
||||
ModelState.AddModelError(nameof(paymentMethodData.ConnectionString),
|
||||
$"BTCPay does not support gRPC connections");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
if (!await CanManageServer() && !connectionString.IsSafe())
|
||||
{
|
||||
ModelState.AddModelError(nameof(paymentMethodData.ConnectionString), $"You do not have 'btcpay.server.canmodifyserversettings' rights, so the connection string should not contain 'cookiefilepath', 'macaroondirectorypath', 'macaroonfilepath', and should not point to a local ip or to a dns name ending with '.internal', '.local', '.lan' or '.'.");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
paymentMethod = new Payments.Lightning.LightningSupportedPaymentMethod()
|
||||
{
|
||||
CryptoCode = paymentMethodId.CryptoCode
|
||||
};
|
||||
paymentMethod.SetLightningUrl(connectionString);
|
||||
}
|
||||
}
|
||||
|
||||
var store = Store;
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
store.SetSupportedPaymentMethod(paymentMethodId, paymentMethod);
|
||||
storeBlob.SetExcluded(paymentMethodId, !paymentMethodData.Enabled);
|
||||
store.SetStoreBlob(storeBlob);
|
||||
await _storeRepository.UpdateStore(store);
|
||||
return Ok(GetExistingLightningLikePaymentMethod(cryptoCode, store));
|
||||
}
|
||||
|
||||
private LightningNetworkPaymentMethodData GetExistingLightningLikePaymentMethod(string cryptoCode, StoreData store = null)
|
||||
{
|
||||
store ??= Store;
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
|
||||
var paymentMethod = store
|
||||
.GetSupportedPaymentMethods(_btcPayNetworkProvider)
|
||||
.OfType<LightningSupportedPaymentMethod>()
|
||||
.FirstOrDefault(method => method.PaymentId == id);
|
||||
|
||||
var excluded = storeBlob.IsExcluded(id);
|
||||
return paymentMethod == null
|
||||
? null
|
||||
: new LightningNetworkPaymentMethodData(paymentMethod.PaymentId.CryptoCode,
|
||||
paymentMethod.GetDisplayableConnectionString(), !excluded);
|
||||
}
|
||||
|
||||
private bool GetNetwork(string cryptoCode, out BTCPayNetwork network)
|
||||
{
|
||||
network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
||||
network = network?.SupportLightning is true ? network : null;
|
||||
return network != null;
|
||||
}
|
||||
|
||||
private async Task<bool> CanUseInternalLightning()
|
||||
{
|
||||
return _cssThemeManager.AllowLightningInternalNodeForAll ||
|
||||
(await _authorizationService.AuthorizeAsync(User, null,
|
||||
new PolicyRequirement(Policies.CanUseInternalLightningNode))).Succeeded;
|
||||
}
|
||||
private async Task<bool> CanManageServer()
|
||||
{
|
||||
return
|
||||
(await _authorizationService.AuthorizeAsync(User, null,
|
||||
new PolicyRequirement(Policies.CanModifyServerSettings))).Succeeded;
|
||||
}
|
||||
}
|
||||
}
|
@ -165,12 +165,12 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(result);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpDelete("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}")]
|
||||
public async Task<IActionResult> RemoveOnChainPaymentMethod(
|
||||
public async Task<ActionResult<OnChainPaymentMethodPreviewResultData>> RemoveOnChainPaymentMethod(
|
||||
string cryptoCode,
|
||||
int offset = 0, int amount = 10)
|
||||
{
|
||||
|
@ -1,564 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.BIP78.Sender;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Models.WalletViewModels;
|
||||
using BTCPayServer.Payments.PayJoin;
|
||||
using BTCPayServer.Payments.PayJoin.Sender;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Payment;
|
||||
using NBXplorer;
|
||||
using NBXplorer.Models;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
namespace BTCPayServer.Controllers.GreenField
|
||||
{
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public class StoreOnChainWalletsController : Controller
|
||||
{
|
||||
private StoreData Store => HttpContext.GetStoreData();
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly BTCPayWalletProvider _btcPayWalletProvider;
|
||||
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||
private readonly WalletRepository _walletRepository;
|
||||
private readonly ExplorerClientProvider _explorerClientProvider;
|
||||
private readonly CssThemeManager _cssThemeManager;
|
||||
private readonly NBXplorerDashboard _nbXplorerDashboard;
|
||||
private readonly WalletsController _walletsController;
|
||||
private readonly PayjoinClient _payjoinClient;
|
||||
private readonly DelayedTransactionBroadcaster _delayedTransactionBroadcaster;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
private readonly WalletReceiveService _walletReceiveService;
|
||||
private readonly IFeeProviderFactory _feeProviderFactory;
|
||||
|
||||
public StoreOnChainWalletsController(
|
||||
IAuthorizationService authorizationService,
|
||||
BTCPayWalletProvider btcPayWalletProvider,
|
||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
WalletRepository walletRepository,
|
||||
ExplorerClientProvider explorerClientProvider,
|
||||
CssThemeManager cssThemeManager,
|
||||
NBXplorerDashboard nbXplorerDashboard,
|
||||
WalletsController walletsController,
|
||||
PayjoinClient payjoinClient,
|
||||
DelayedTransactionBroadcaster delayedTransactionBroadcaster,
|
||||
EventAggregator eventAggregator,
|
||||
WalletReceiveService walletReceiveService,
|
||||
IFeeProviderFactory feeProviderFactory)
|
||||
{
|
||||
_authorizationService = authorizationService;
|
||||
_btcPayWalletProvider = btcPayWalletProvider;
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_walletRepository = walletRepository;
|
||||
_explorerClientProvider = explorerClientProvider;
|
||||
_cssThemeManager = cssThemeManager;
|
||||
_nbXplorerDashboard = nbXplorerDashboard;
|
||||
_walletsController = walletsController;
|
||||
_payjoinClient = payjoinClient;
|
||||
_delayedTransactionBroadcaster = delayedTransactionBroadcaster;
|
||||
_eventAggregator = eventAggregator;
|
||||
_walletReceiveService = walletReceiveService;
|
||||
_feeProviderFactory = feeProviderFactory;
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet")]
|
||||
public async Task<IActionResult> ShowOnChainWalletOverview(string storeId, string cryptoCode)
|
||||
{
|
||||
if (IsInvalidWalletRequest(cryptoCode, out BTCPayNetwork network,
|
||||
out DerivationSchemeSettings derivationScheme, out IActionResult actionResult)) return actionResult;
|
||||
|
||||
var wallet = _btcPayWalletProvider.GetWallet(network);
|
||||
var balance = await wallet.GetBalance(derivationScheme.AccountDerivation);
|
||||
|
||||
return Ok(new OnChainWalletOverviewData()
|
||||
{
|
||||
Label = derivationScheme.ToPrettyString(),
|
||||
Balance = balance.Total.GetValue(network),
|
||||
UnconfirmedBalance= balance.Unconfirmed.GetValue(network),
|
||||
ConfirmedBalance= balance.Confirmed.GetValue(network),
|
||||
});
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/feerate")]
|
||||
public async Task<IActionResult> GetOnChainFeeRate(string storeId, string cryptoCode, int? blockTarget = null)
|
||||
{
|
||||
if (IsInvalidWalletRequest(cryptoCode, out BTCPayNetwork network,
|
||||
out DerivationSchemeSettings derivationScheme, out IActionResult actionResult)) return actionResult;
|
||||
|
||||
var feeRateTarget = blockTarget?? Store.GetStoreBlob().RecommendedFeeBlockTarget;
|
||||
return Ok(new OnChainWalletFeeRateData()
|
||||
{
|
||||
FeeRate = await _feeProviderFactory.CreateFeeProvider(network)
|
||||
.GetFeeRateAsync(feeRateTarget),
|
||||
});
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/address")]
|
||||
public async Task<IActionResult> GetOnChainWalletReceiveAddress(string storeId, string cryptoCode, bool forceGenerate = false)
|
||||
{
|
||||
if (IsInvalidWalletRequest(cryptoCode, out BTCPayNetwork network,
|
||||
out DerivationSchemeSettings derivationScheme, out IActionResult actionResult)) return actionResult;
|
||||
|
||||
var kpi = await _walletReceiveService.GetOrGenerate(new WalletId(storeId, cryptoCode), forceGenerate);
|
||||
if (kpi is null)
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
var bip21 = network.GenerateBIP21(kpi.Address.ToString(), null);
|
||||
var allowedPayjoin = derivationScheme.IsHotWallet && Store.GetStoreBlob().PayJoinEnabled;
|
||||
if (allowedPayjoin)
|
||||
{
|
||||
bip21 +=
|
||||
$"?{PayjoinClient.BIP21EndpointKey}={Request.GetAbsoluteUri(Url.Action(nameof(PayJoinEndpointController.Submit), "PayJoinEndpoint", new {cryptoCode}))}";
|
||||
}
|
||||
return Ok(new OnChainWalletAddressData()
|
||||
{
|
||||
Address = kpi.Address.ToString(),
|
||||
PaymentLink = bip21,
|
||||
KeyPath = kpi.KeyPath
|
||||
});
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpDelete("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/address")]
|
||||
public async Task<IActionResult> UnReserveOnChainWalletReceiveAddress(string storeId, string cryptoCode)
|
||||
{
|
||||
if (IsInvalidWalletRequest(cryptoCode, out BTCPayNetwork network,
|
||||
out DerivationSchemeSettings derivationScheme, out IActionResult actionResult)) return actionResult;
|
||||
|
||||
var addr = await _walletReceiveService.UnReserveAddress(new WalletId(storeId, cryptoCode));
|
||||
if (addr is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions")]
|
||||
public async Task<IActionResult> ShowOnChainWalletTransactions(string storeId, string cryptoCode,
|
||||
[FromQuery]TransactionStatus[] statusFilter = null)
|
||||
{
|
||||
if (IsInvalidWalletRequest(cryptoCode, out BTCPayNetwork network,
|
||||
out DerivationSchemeSettings derivationScheme, out IActionResult actionResult)) return actionResult;
|
||||
|
||||
var wallet = _btcPayWalletProvider.GetWallet(network);
|
||||
var walletId = new WalletId(storeId, cryptoCode);
|
||||
var walletTransactionsInfoAsync = await _walletRepository.GetWalletTransactionsInfo(walletId);
|
||||
|
||||
var txs = await wallet.FetchTransactions(derivationScheme.AccountDerivation);
|
||||
var filteredFlatList = new List<TransactionInformation>();
|
||||
if (statusFilter is null || !statusFilter.Any() || statusFilter.Contains(TransactionStatus.Confirmed))
|
||||
{
|
||||
filteredFlatList.AddRange(txs.ConfirmedTransactions.Transactions);
|
||||
}
|
||||
|
||||
if (statusFilter is null || !statusFilter.Any() || statusFilter.Contains(TransactionStatus.Unconfirmed))
|
||||
{
|
||||
filteredFlatList.AddRange(txs.UnconfirmedTransactions.Transactions);
|
||||
}
|
||||
|
||||
if (statusFilter is null || !statusFilter.Any() ||statusFilter.Contains(TransactionStatus.Replaced))
|
||||
{
|
||||
filteredFlatList.AddRange(txs.ReplacedTransactions.Transactions);
|
||||
}
|
||||
|
||||
var result = filteredFlatList.Select(information =>
|
||||
{
|
||||
walletTransactionsInfoAsync.TryGetValue(information.TransactionId.ToString(), out var transactionInfo);
|
||||
return ToModel(transactionInfo, information, wallet);
|
||||
}).ToList();
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions/{transactionId}")]
|
||||
public async Task<IActionResult> GetOnChainWalletTransaction(string storeId, string cryptoCode,
|
||||
string transactionId)
|
||||
{
|
||||
if (IsInvalidWalletRequest(cryptoCode, out BTCPayNetwork network,
|
||||
out DerivationSchemeSettings derivationScheme, out IActionResult actionResult)) return actionResult;
|
||||
|
||||
var wallet = _btcPayWalletProvider.GetWallet(network);
|
||||
var tx = await wallet.FetchTransaction(derivationScheme.AccountDerivation, uint256.Parse(transactionId));
|
||||
if (tx is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var walletId = new WalletId(storeId, cryptoCode);
|
||||
var walletTransactionsInfoAsync =
|
||||
(await _walletRepository.GetWalletTransactionsInfo(walletId, new[] {transactionId})).Values
|
||||
.FirstOrDefault();
|
||||
|
||||
return Ok(ToModel(walletTransactionsInfoAsync, tx, wallet));
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/utxos")]
|
||||
public async Task<IActionResult> GetOnChainWalletUTXOs(string storeId, string cryptoCode)
|
||||
{
|
||||
if (IsInvalidWalletRequest(cryptoCode, out BTCPayNetwork network,
|
||||
out DerivationSchemeSettings derivationScheme, out IActionResult actionResult)) return actionResult;
|
||||
|
||||
var wallet = _btcPayWalletProvider.GetWallet(network);
|
||||
|
||||
var walletId = new WalletId(storeId, cryptoCode);
|
||||
var walletTransactionsInfoAsync = await _walletRepository.GetWalletTransactionsInfo(walletId);
|
||||
var utxos = await wallet.GetUnspentCoins(derivationScheme.AccountDerivation);
|
||||
return Ok(utxos.Select(coin =>
|
||||
{
|
||||
walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.Hash.ToString(), out var info);
|
||||
return new OnChainWalletUTXOData()
|
||||
{
|
||||
Outpoint = coin.OutPoint,
|
||||
Amount = coin.Value.GetValue(network),
|
||||
Comment = info?.Comment,
|
||||
Labels = info?.Labels,
|
||||
Link = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink,
|
||||
coin.OutPoint.Hash.ToString()),
|
||||
Timestamp = coin.Timestamp,
|
||||
KeyPath = coin.KeyPath,
|
||||
Confirmations = coin.Confirmations,
|
||||
Address = network.NBXplorerNetwork.CreateAddress(derivationScheme.AccountDerivation, coin.KeyPath, coin.ScriptPubKey).ToString()
|
||||
};
|
||||
}).ToList()
|
||||
);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions")]
|
||||
public async Task<IActionResult> CreateOnChainTransaction(string storeId, string cryptoCode,
|
||||
[FromBody] CreateOnChainTransactionRequest request)
|
||||
{
|
||||
if (IsInvalidWalletRequest(cryptoCode, out BTCPayNetwork network,
|
||||
out DerivationSchemeSettings derivationScheme, out IActionResult actionResult)) return actionResult;
|
||||
if (network.ReadonlyWallet)
|
||||
{
|
||||
return this.CreateAPIError("not-available",
|
||||
$"{cryptoCode} sending services are not currently available");
|
||||
}
|
||||
|
||||
//This API is only meant for hot wallet usage for now. We can expand later when we allow PSBT manipulation.
|
||||
if (!(await CanUseHotWallet()).HotWallet)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var explorerClient = _explorerClientProvider.GetExplorerClient(cryptoCode);
|
||||
var wallet = _btcPayWalletProvider.GetWallet(network);
|
||||
|
||||
var utxos = await wallet.GetUnspentCoins(derivationScheme.AccountDerivation);
|
||||
if (request.SelectedInputs != null || !utxos.Any())
|
||||
{
|
||||
utxos = utxos.Where(coin => request.SelectedInputs?.Contains(coin.OutPoint) ?? true)
|
||||
.ToArray();
|
||||
if (utxos.Any() is false)
|
||||
{
|
||||
//no valid utxos selected
|
||||
request.AddModelError(transactionRequest => transactionRequest.SelectedInputs,
|
||||
"There are no available utxos based on your request", this);
|
||||
}
|
||||
}
|
||||
|
||||
var balanceAvailable = utxos.Sum(coin => coin.Value.GetValue(network));
|
||||
|
||||
var subtractFeesOutputsCount = new List<int>();
|
||||
var subtractFees = request.Destinations.Any(o => o.SubtractFromAmount);
|
||||
int? payjoinOutputIndex = null;
|
||||
var sum = 0m;
|
||||
var outputs = new List<WalletSendModel.TransactionOutput>();
|
||||
for (var index = 0; index < request.Destinations.Count; index++)
|
||||
{
|
||||
var destination = request.Destinations[index];
|
||||
|
||||
if (destination.SubtractFromAmount)
|
||||
{
|
||||
subtractFeesOutputsCount.Add(index);
|
||||
}
|
||||
|
||||
BitcoinUrlBuilder bip21 = null;
|
||||
var amount = destination.Amount;
|
||||
if (amount.GetValueOrDefault(0) <= 0)
|
||||
{
|
||||
amount = null;
|
||||
}
|
||||
var address = string.Empty;
|
||||
try
|
||||
{
|
||||
destination.Destination = destination.Destination.Replace(network.UriScheme+":", "bitcoin:", StringComparison.InvariantCultureIgnoreCase);
|
||||
bip21 = new BitcoinUrlBuilder(destination.Destination, network.NBitcoinNetwork);
|
||||
amount ??= bip21.Amount.GetValue(network);
|
||||
address = bip21.Address.ToString();
|
||||
if (destination.SubtractFromAmount)
|
||||
{
|
||||
request.AddModelError(transactionRequest => transactionRequest.Destinations[index],
|
||||
"You cannot use a BIP21 destination along with SubtractFromAmount", this);
|
||||
}
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
try
|
||||
{
|
||||
address = BitcoinAddress.Create(destination.Destination, network.NBitcoinNetwork).ToString();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
request.AddModelError(transactionRequest => transactionRequest.Destinations[index],
|
||||
"Destination must be a BIP21 payment link or an address", this);
|
||||
}
|
||||
}
|
||||
|
||||
if (amount is null || amount <= 0)
|
||||
{
|
||||
request.AddModelError(transactionRequest => transactionRequest.Destinations[index],
|
||||
"Amount must be specified or destination must be a BIP21 payment link, and greater than 0", this);
|
||||
}
|
||||
if (request.ProceedWithPayjoin && bip21?.UnknowParameters?.ContainsKey(PayjoinClient.BIP21EndpointKey) is true)
|
||||
{
|
||||
payjoinOutputIndex = index;
|
||||
}
|
||||
|
||||
outputs.Add(new WalletSendModel.TransactionOutput()
|
||||
{
|
||||
DestinationAddress = address,
|
||||
Amount = amount,
|
||||
SubtractFeesFromOutput = destination.SubtractFromAmount
|
||||
});
|
||||
sum += destination.Amount ?? 0;
|
||||
}
|
||||
|
||||
if (subtractFeesOutputsCount.Count > 1)
|
||||
{
|
||||
foreach (var subtractFeesOutput in subtractFeesOutputsCount)
|
||||
{
|
||||
request.AddModelError(model => model.Destinations[subtractFeesOutput].SubtractFromAmount,
|
||||
"You can only subtract fees from one destination", this);
|
||||
}
|
||||
}
|
||||
|
||||
if (balanceAvailable < sum)
|
||||
{
|
||||
request.AddModelError(transactionRequest => transactionRequest.Destinations,
|
||||
"You are attempting to send more than is available", this);
|
||||
}
|
||||
else if (balanceAvailable == sum && !subtractFees)
|
||||
{
|
||||
request.AddModelError(transactionRequest => transactionRequest.Destinations,
|
||||
"You are sending your entire balance, you should subtract the fees from a destination", this);
|
||||
}
|
||||
|
||||
var minRelayFee = _nbXplorerDashboard.Get(network.CryptoCode).Status.BitcoinStatus?.MinRelayTxFee ??
|
||||
new FeeRate(1.0m);
|
||||
if (request.FeeRate != null && request.FeeRate < minRelayFee)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.FeeRate),
|
||||
"The fee rate specified is lower than the current minimum relay fee");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
CreatePSBTResponse psbt;
|
||||
try
|
||||
{
|
||||
psbt = await _walletsController.CreatePSBT(network, derivationScheme,
|
||||
new WalletSendModel()
|
||||
{
|
||||
SelectedInputs = request.SelectedInputs?.Select(point => point.ToString()),
|
||||
Outputs = outputs,
|
||||
AlwaysIncludeNonWitnessUTXO = true,
|
||||
InputSelection = request.SelectedInputs?.Any() is true,
|
||||
AllowFeeBump =
|
||||
!request.RBF.HasValue ? WalletSendModel.ThreeStateBool.Maybe :
|
||||
request.RBF.Value ? WalletSendModel.ThreeStateBool.Yes :
|
||||
WalletSendModel.ThreeStateBool.No,
|
||||
FeeSatoshiPerByte = request.FeeRate?.SatoshiPerByte,
|
||||
NoChange = request.NoChange
|
||||
},
|
||||
CancellationToken.None);
|
||||
}
|
||||
catch (NBXplorerException ex)
|
||||
{
|
||||
return this.CreateAPIError(ex.Error.Code, ex.Error.Message);
|
||||
}
|
||||
catch (NotSupportedException)
|
||||
{
|
||||
return this.CreateAPIError("not-available", "You need to update your version of NBXplorer");
|
||||
}
|
||||
|
||||
derivationScheme.RebaseKeyPaths(psbt.PSBT);
|
||||
|
||||
var signingContext = new SigningContextModel()
|
||||
{
|
||||
PayJoinBIP21 =
|
||||
payjoinOutputIndex is null
|
||||
? null
|
||||
: request.Destinations.ElementAt(payjoinOutputIndex.Value).Destination,
|
||||
EnforceLowR = psbt.Suggestions?.ShouldEnforceLowR,
|
||||
ChangeAddress = psbt.ChangeAddress?.ToString()
|
||||
};
|
||||
|
||||
var signingKeyStr = await explorerClient
|
||||
.GetMetadataAsync<string>(derivationScheme.AccountDerivation,
|
||||
WellknownMetadataKeys.MasterHDKey);
|
||||
if (!derivationScheme.IsHotWallet || signingKeyStr is null)
|
||||
{
|
||||
return this.CreateAPIError("not-available",
|
||||
$"{cryptoCode} sending services are not currently available");
|
||||
}
|
||||
|
||||
var signingKey = ExtKey.Parse(signingKeyStr, network.NBitcoinNetwork);
|
||||
|
||||
var signingKeySettings = derivationScheme.GetSigningAccountKeySettings();
|
||||
signingKeySettings.RootFingerprint ??= signingKey.GetPublicKey().GetHDFingerPrint();
|
||||
RootedKeyPath rootedKeyPath = signingKeySettings.GetRootedKeyPath();
|
||||
psbt.PSBT.RebaseKeyPaths(signingKeySettings.AccountKey, rootedKeyPath);
|
||||
var accountKey = signingKey.Derive(rootedKeyPath.KeyPath);
|
||||
|
||||
var changed = psbt.PSBT.PSBTChanged(() => psbt.PSBT.SignAll(derivationScheme.AccountDerivation, accountKey,
|
||||
rootedKeyPath, new SigningOptions() {EnforceLowR = signingContext?.EnforceLowR is bool v ? v : psbt.Suggestions.ShouldEnforceLowR }));
|
||||
|
||||
if (!changed)
|
||||
{
|
||||
return this.CreateAPIError("psbt-signing-error",
|
||||
"Impossible to sign the transaction. Probable cause: Incorrect account key path in wallet settings, PSBT already signed.");
|
||||
}
|
||||
|
||||
psbt.PSBT.Finalize();
|
||||
var transaction = psbt.PSBT.ExtractTransaction();
|
||||
var transactionHash = transaction.GetHash();
|
||||
BroadcastResult broadcastResult;
|
||||
if (!string.IsNullOrEmpty(signingContext.PayJoinBIP21))
|
||||
{
|
||||
signingContext.OriginalPSBT = psbt.PSBT.ToBase64();
|
||||
try
|
||||
{
|
||||
await _delayedTransactionBroadcaster.Schedule(DateTimeOffset.UtcNow + TimeSpan.FromMinutes(2.0),
|
||||
transaction, network);
|
||||
var payjoinPSBT = await _payjoinClient.RequestPayjoin(
|
||||
new BitcoinUrlBuilder(signingContext.PayJoinBIP21, network.NBitcoinNetwork), new PayjoinWallet(derivationScheme),
|
||||
psbt.PSBT, CancellationToken.None);
|
||||
payjoinPSBT = psbt.PSBT.SignAll(derivationScheme.AccountDerivation, accountKey, rootedKeyPath,
|
||||
new SigningOptions() {EnforceLowR = !(signingContext?.EnforceLowR is false)});
|
||||
payjoinPSBT.Finalize();
|
||||
var payjoinTransaction = payjoinPSBT.ExtractTransaction();
|
||||
var hash = payjoinTransaction.GetHash();
|
||||
_eventAggregator.Publish(new UpdateTransactionLabel(new WalletId(Store.Id, cryptoCode), hash,
|
||||
UpdateTransactionLabel.PayjoinLabelTemplate()));
|
||||
broadcastResult = await explorerClient.BroadcastAsync(payjoinTransaction);
|
||||
if (broadcastResult.Success)
|
||||
{
|
||||
return await GetOnChainWalletTransaction(storeId, cryptoCode, hash.ToString());
|
||||
}
|
||||
}
|
||||
catch (PayjoinException)
|
||||
{
|
||||
//not a critical thing, payjoin is great if possible, fine if not
|
||||
}
|
||||
}
|
||||
|
||||
if (!request.ProceedWithBroadcast)
|
||||
{
|
||||
return Ok(new JValue(transaction.ToHex()));
|
||||
}
|
||||
|
||||
broadcastResult = await explorerClient.BroadcastAsync(transaction);
|
||||
if (broadcastResult.Success)
|
||||
{
|
||||
return await GetOnChainWalletTransaction(storeId, cryptoCode, transactionHash.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
return this.CreateAPIError("broadcast-error", broadcastResult.RPCMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<(bool HotWallet, bool RPCImport)> CanUseHotWallet()
|
||||
{
|
||||
return await _authorizationService.CanUseHotWallet(_cssThemeManager.Policies, User);
|
||||
}
|
||||
|
||||
private bool IsInvalidWalletRequest(string cryptoCode, out BTCPayNetwork network,
|
||||
out DerivationSchemeSettings derivationScheme, out IActionResult actionResult)
|
||||
{
|
||||
derivationScheme = null;
|
||||
actionResult = null;
|
||||
network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
||||
if (network is null)
|
||||
{
|
||||
actionResult = NotFound();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
if (!network.WalletSupported || !_btcPayWalletProvider.IsAvailable(network))
|
||||
{
|
||||
actionResult = this.CreateAPIError("not-available",
|
||||
$"{cryptoCode} services are not currently available");
|
||||
return true;
|
||||
}
|
||||
|
||||
derivationScheme = GetDerivationSchemeSettings(cryptoCode);
|
||||
if (derivationScheme?.AccountDerivation is null)
|
||||
{
|
||||
actionResult = NotFound();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private DerivationSchemeSettings GetDerivationSchemeSettings(string cryptoCode)
|
||||
{
|
||||
var paymentMethod = Store
|
||||
.GetSupportedPaymentMethods(_btcPayNetworkProvider)
|
||||
.OfType<DerivationSchemeSettings>()
|
||||
.FirstOrDefault(p =>
|
||||
p.PaymentId.PaymentType == Payments.PaymentTypes.BTCLike &&
|
||||
p.PaymentId.CryptoCode.Equals(cryptoCode, StringComparison.InvariantCultureIgnoreCase));
|
||||
return paymentMethod;
|
||||
}
|
||||
|
||||
private OnChainWalletTransactionData ToModel(WalletTransactionInfo walletTransactionsInfoAsync,
|
||||
TransactionInformation tx,
|
||||
BTCPayWallet wallet)
|
||||
{
|
||||
return new OnChainWalletTransactionData()
|
||||
{
|
||||
TransactionHash = tx.TransactionId,
|
||||
Comment = walletTransactionsInfoAsync?.Comment?? string.Empty,
|
||||
Labels = walletTransactionsInfoAsync?.Labels?? new Dictionary<string, LabelData>(),
|
||||
Amount = tx.BalanceChange.GetValue(wallet.Network),
|
||||
BlockHash = tx.BlockHash,
|
||||
BlockHeight = tx.Height,
|
||||
Confirmations = tx.Confirmations,
|
||||
Timestamp = tx.Timestamp,
|
||||
Status = tx.Confirmations > 0 ? TransactionStatus.Confirmed :
|
||||
tx.ReplacedBy != null ? TransactionStatus.Replaced : TransactionStatus.Unconfirmed
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -48,7 +48,7 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
{
|
||||
var w = await StoreRepository.GetWebhook(CurrentStoreId, webhookId);
|
||||
if (w is null)
|
||||
return WebhookNotFound();
|
||||
return NotFound();
|
||||
return Ok(FromModel(w, false));
|
||||
}
|
||||
}
|
||||
@ -70,7 +70,7 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
var webhookId = await StoreRepository.CreateWebhook(CurrentStoreId, ToModel(create));
|
||||
var w = await StoreRepository.GetWebhook(CurrentStoreId, webhookId);
|
||||
if (w is null)
|
||||
return WebhookNotFound();
|
||||
return NotFound();
|
||||
return Ok(FromModel(w, true));
|
||||
}
|
||||
|
||||
@ -88,7 +88,7 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
return this.CreateValidationError(ModelState);
|
||||
var w = await StoreRepository.GetWebhook(CurrentStoreId, webhookId);
|
||||
if (w is null)
|
||||
return WebhookNotFound();
|
||||
return NotFound();
|
||||
await StoreRepository.UpdateWebhook(storeId, webhookId, ToModel(update));
|
||||
return await ListWebhooks(webhookId);
|
||||
}
|
||||
@ -97,19 +97,10 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
{
|
||||
var w = await StoreRepository.GetWebhook(CurrentStoreId, webhookId);
|
||||
if (w is null)
|
||||
return WebhookNotFound();
|
||||
return NotFound();
|
||||
await StoreRepository.DeleteWebhook(CurrentStoreId, webhookId);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
IActionResult WebhookNotFound()
|
||||
{
|
||||
return this.CreateAPIError(404, "webhook-not-found", "The webhook was not found");
|
||||
}
|
||||
IActionResult WebhookDeliveryNotFound()
|
||||
{
|
||||
return this.CreateAPIError(404, "webhookdelivery-not-found", "The webhook delivery was not found");
|
||||
}
|
||||
private WebhookBlob ToModel(StoreWebhookBaseData create)
|
||||
{
|
||||
return new WebhookBlob()
|
||||
@ -142,7 +133,7 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
{
|
||||
var delivery = await StoreRepository.GetWebhookDelivery(CurrentStoreId, webhookId, deliveryId);
|
||||
if (delivery is null)
|
||||
return WebhookDeliveryNotFound();
|
||||
return NotFound();
|
||||
return Ok(FromModel(delivery));
|
||||
}
|
||||
}
|
||||
@ -151,7 +142,7 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
{
|
||||
var delivery = await StoreRepository.GetWebhookDelivery(CurrentStoreId, webhookId, deliveryId);
|
||||
if (delivery is null)
|
||||
return WebhookDeliveryNotFound();
|
||||
return NotFound();
|
||||
return this.Ok(new JValue(await WebhookNotificationManager.Redeliver(deliveryId)));
|
||||
}
|
||||
|
||||
@ -160,7 +151,7 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
{
|
||||
var delivery = await StoreRepository.GetWebhookDelivery(CurrentStoreId, webhookId, deliveryId);
|
||||
if (delivery is null)
|
||||
return WebhookDeliveryNotFound();
|
||||
return NotFound();
|
||||
return File(delivery.GetBlob().Request, "application/json");
|
||||
}
|
||||
|
||||
|
@ -131,7 +131,6 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints,
|
||||
OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback,
|
||||
RedirectAutomatically = storeBlob.RedirectAutomatically,
|
||||
LazyPaymentMethods = storeBlob.LazyPaymentMethods,
|
||||
ShowRecommendedFee = storeBlob.ShowRecommendedFee,
|
||||
RecommendedFeeBlockTarget = storeBlob.RecommendedFeeBlockTarget,
|
||||
DefaultLang = storeBlob.DefaultLang,
|
||||
@ -168,7 +167,6 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
blob.LightningAmountInSatoshi = restModel.LightningAmountInSatoshi;
|
||||
blob.LightningPrivateRouteHints = restModel.LightningPrivateRouteHints;
|
||||
blob.OnChainWithLnInvoiceFallback = restModel.OnChainWithLnInvoiceFallback;
|
||||
blob.LazyPaymentMethods = restModel.LazyPaymentMethods;
|
||||
blob.RedirectAutomatically = restModel.RedirectAutomatically;
|
||||
blob.ShowRecommendedFee = restModel.ShowRecommendedFee;
|
||||
blob.RecommendedFeeBlockTarget = restModel.RecommendedFeeBlockTarget;
|
||||
|
@ -7,7 +7,6 @@ using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
@ -49,22 +48,75 @@ namespace BTCPayServer.Controllers
|
||||
SignInManager = signInManager;
|
||||
}
|
||||
|
||||
[Route("")]
|
||||
[DomainMappingConstraint()]
|
||||
public IActionResult Index()
|
||||
private async Task<ViewResult> GoToApp(string appId, AppType? appType)
|
||||
{
|
||||
if (appType.HasValue && !string.IsNullOrEmpty(appId))
|
||||
{
|
||||
this.HttpContext.Response.Headers.Remove("Onion-Location");
|
||||
switch (appType.Value)
|
||||
{
|
||||
case AppType.Crowdfund:
|
||||
{
|
||||
var serviceProvider = HttpContext.RequestServices;
|
||||
var controller = (AppsPublicController)serviceProvider.GetService(typeof(AppsPublicController));
|
||||
controller.Url = Url;
|
||||
controller.ControllerContext = ControllerContext;
|
||||
var res = await controller.ViewCrowdfund(appId, null) as ViewResult;
|
||||
if (res != null)
|
||||
{
|
||||
res.ViewName = $"/Views/AppsPublic/ViewCrowdfund.cshtml";
|
||||
return res; // return
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case AppType.PointOfSale:
|
||||
{
|
||||
var serviceProvider = HttpContext.RequestServices;
|
||||
var controller = (AppsPublicController)serviceProvider.GetService(typeof(AppsPublicController));
|
||||
controller.Url = Url;
|
||||
controller.ControllerContext = ControllerContext;
|
||||
var res = await controller.ViewPointOfSale(appId) as ViewResult;
|
||||
if (res != null)
|
||||
{
|
||||
res.ViewName = $"/Views/AppsPublic/{res.ViewName}.cshtml";
|
||||
return res; // return
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Index()
|
||||
{
|
||||
if (_cachedServerSettings.FirstRun)
|
||||
{
|
||||
return RedirectToAction(nameof(AccountController.Register), "Account");
|
||||
}
|
||||
var matchedDomainMapping = _cachedServerSettings.DomainToAppMapping.FirstOrDefault(item =>
|
||||
item.Domain.Equals(Request.Host.Host, StringComparison.InvariantCultureIgnoreCase));
|
||||
if (matchedDomainMapping != null)
|
||||
{
|
||||
return await GoToApp(matchedDomainMapping.AppId, matchedDomainMapping.AppType) ?? GoToHome();
|
||||
}
|
||||
|
||||
return await GoToApp(_cachedServerSettings.RootAppId, _cachedServerSettings.RootAppType) ?? GoToHome();
|
||||
}
|
||||
|
||||
private IActionResult GoToHome()
|
||||
{
|
||||
if (SignInManager.IsSignedIn(User))
|
||||
return View("Home");
|
||||
else
|
||||
return Challenge();
|
||||
return RedirectToAction(nameof(AccountController.Login), "Account");
|
||||
}
|
||||
|
||||
[Route("misc/lang")]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie + "," + AuthenticationSchemes.Greenfield)]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult Languages()
|
||||
{
|
||||
return Json(LanguageService.GetLanguages(), new JsonSerializerSettings() { Formatting = Formatting.Indented });
|
||||
|
@ -14,13 +14,12 @@ using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.CoinSwitch;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Plugins.CoinSwitch;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
@ -359,7 +358,7 @@ namespace BTCPayServer.Controllers
|
||||
return new InvoiceDetailsModel
|
||||
{
|
||||
Archived = invoice.Archived,
|
||||
Payments = invoice.GetPayments(false),
|
||||
Payments = invoice.GetPayments(),
|
||||
CryptoPayments = invoice.GetPaymentMethods().Select(
|
||||
data =>
|
||||
{
|
||||
@ -504,9 +503,9 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (!isDefaultPaymentId)
|
||||
return null;
|
||||
var paymentMethodTemp = invoice
|
||||
.GetPaymentMethods()
|
||||
.FirstOrDefault(c => paymentMethodId.CryptoCode == c.GetId().CryptoCode);
|
||||
var paymentMethodTemp = invoice.GetPaymentMethods()
|
||||
.Where(c => paymentMethodId.CryptoCode == c.GetId().CryptoCode)
|
||||
.FirstOrDefault();
|
||||
if (paymentMethodTemp == null)
|
||||
paymentMethodTemp = invoice.GetPaymentMethods().First();
|
||||
network = paymentMethodTemp.Network;
|
||||
@ -515,22 +514,21 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId);
|
||||
var paymentMethodDetails = paymentMethod.GetPaymentMethodDetails();
|
||||
if (!paymentMethodDetails.Activated)
|
||||
{
|
||||
await _InvoiceRepository.ActivateInvoicePaymentMethod(_EventAggregator, _NetworkProvider,
|
||||
_paymentMethodHandlerDictionary, store, invoice, paymentMethod.GetId());
|
||||
return await GetInvoiceModel(invoiceId, paymentMethodId, lang);
|
||||
}
|
||||
var dto = invoice.EntityToDTO();
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var accounting = paymentMethod.Calculate();
|
||||
|
||||
CoinSwitchSettings coinswitch = (storeBlob.CoinSwitchSettings != null && storeBlob.CoinSwitchSettings.Enabled &&
|
||||
storeBlob.CoinSwitchSettings.IsConfigured())
|
||||
? storeBlob.CoinSwitchSettings
|
||||
: null;
|
||||
|
||||
|
||||
var paymentMethodHandler = _paymentMethodHandlerDictionary[paymentMethodId];
|
||||
|
||||
var divisibility = _CurrencyNameTable.GetNumberFormatInfo(paymentMethod.GetId().CryptoCode, false)?.CurrencyDecimalDigits;
|
||||
var model = new PaymentModel()
|
||||
{
|
||||
Activated = paymentMethodDetails.Activated,
|
||||
CryptoCode = network.CryptoCode,
|
||||
RootPath = this.Request.PathBase.Value.WithTrailingSlash(),
|
||||
OrderId = invoice.Metadata.OrderId,
|
||||
@ -561,7 +559,11 @@ namespace BTCPayServer.Controllers
|
||||
Status = invoice.StatusString,
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
NetworkFee = paymentMethodDetails.GetNextNetworkFee(),
|
||||
IsMultiCurrency = invoice.GetPayments(false).Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1,
|
||||
IsMultiCurrency = invoice.GetPayments().Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1,
|
||||
CoinSwitchEnabled = coinswitch != null,
|
||||
CoinSwitchAmountMarkupPercentage = coinswitch?.AmountMarkupPercentage ?? 0,
|
||||
CoinSwitchMerchantId = coinswitch?.MerchantId,
|
||||
CoinSwitchMode = coinswitch?.Mode,
|
||||
StoreId = store.Id,
|
||||
AvailableCryptos = invoice.GetPaymentMethods()
|
||||
.Where(i => i.Network != null)
|
||||
@ -903,20 +905,21 @@ namespace BTCPayServer.Controllers
|
||||
var jObject = JObject.Parse(posData);
|
||||
foreach (var item in jObject)
|
||||
{
|
||||
|
||||
switch (item.Value.Type)
|
||||
{
|
||||
case JTokenType.Array:
|
||||
var items = item.Value.AsEnumerable().ToList();
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
result.TryAdd($"{item.Key}[{i}]", ParsePosData(items[i].ToString()));
|
||||
result.Add($"{item.Key}[{i}]", ParsePosData(items[i].ToString()));
|
||||
}
|
||||
break;
|
||||
case JTokenType.Object:
|
||||
result.TryAdd(item.Key, ParsePosData(item.Value.ToString()));
|
||||
result.Add(item.Key, ParsePosData(item.Value.ToString()));
|
||||
break;
|
||||
default:
|
||||
result.TryAdd(item.Key, item.Value.ToString());
|
||||
result.Add(item.Key, item.Value.ToString());
|
||||
break;
|
||||
}
|
||||
|
||||
@ -924,7 +927,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
catch
|
||||
{
|
||||
result.TryAdd(string.Empty, posData);
|
||||
result.Add(string.Empty, posData);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
@ -44,10 +44,12 @@ namespace BTCPayServer.Controllers
|
||||
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary;
|
||||
private readonly ApplicationDbContextFactory _dbContextFactory;
|
||||
private readonly PullPaymentHostedService _paymentHostedService;
|
||||
readonly IServiceProvider _ServiceProvider;
|
||||
|
||||
public WebhookNotificationManager WebhookNotificationManager { get; }
|
||||
|
||||
public InvoiceController(
|
||||
IServiceProvider serviceProvider,
|
||||
InvoiceRepository invoiceRepository,
|
||||
CurrencyNameTable currencyNameTable,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
@ -61,6 +63,7 @@ namespace BTCPayServer.Controllers
|
||||
PullPaymentHostedService paymentHostedService,
|
||||
WebhookNotificationManager webhookNotificationManager)
|
||||
{
|
||||
_ServiceProvider = serviceProvider;
|
||||
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
|
||||
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
|
||||
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
|
||||
@ -170,7 +173,6 @@ namespace BTCPayServer.Controllers
|
||||
entity.Price = invoice.Amount;
|
||||
entity.SpeedPolicy = invoice.Checkout.SpeedPolicy ?? store.SpeedPolicy;
|
||||
entity.DefaultLanguage = invoice.Checkout.DefaultLanguage;
|
||||
entity.RedirectAutomatically = invoice.Checkout.RedirectAutomatically ?? storeBlob.RedirectAutomatically;
|
||||
IPaymentFilter excludeFilter = null;
|
||||
if (invoice.Checkout.PaymentMethods != null)
|
||||
{
|
||||
@ -318,16 +320,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
var logPrefix = $"{supportedPaymentMethod.PaymentId.ToPrettyString()}:";
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
|
||||
object preparePayment;
|
||||
if (storeBlob.LazyPaymentMethods)
|
||||
{
|
||||
preparePayment = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
preparePayment = handler.PreparePayment(supportedPaymentMethod, store, network);
|
||||
}
|
||||
var preparePayment = handler.PreparePayment(supportedPaymentMethod, store, network);
|
||||
var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.Currency)];
|
||||
if (rate.BidAsk == null)
|
||||
{
|
||||
|
@ -472,8 +472,8 @@ namespace BTCPayServer.Controllers
|
||||
{BTCPayServer.Client.Policies.CanCreateUser, ("Create new users", "The app will be able to create new users on this server.")},
|
||||
{BTCPayServer.Client.Policies.CanModifyStoreSettings, ("Modify your stores", "The app will be able to view, modify, delete and create new invoices on all your stores.")},
|
||||
{$"{BTCPayServer.Client.Policies.CanModifyStoreSettings}:", ("Manage selected stores", "The app will be able to view, modify, delete and create new invoices on the selected stores.")},
|
||||
{BTCPayServer.Client.Policies.CanModifyStoreWebhooks, ("Modify stores webhooks", "The app will modify the webhooks of all your stores.")},
|
||||
{$"{BTCPayServer.Client.Policies.CanModifyStoreWebhooks}:", ("Modify selected stores' webhooks", "The app will modify the webhooks of the selected stores.")},
|
||||
{BTCPayServer.Client.Policies.CanModifyStoreWebhooks, ("Modify stores webhooks", "The app will be mofidy the webhooks of all your stores.")},
|
||||
{$"{BTCPayServer.Client.Policies.CanModifyStoreWebhooks}:", ("Modify selected stores' webhooks", "The app will be mofidy the webhooks of the selected stores.")},
|
||||
{BTCPayServer.Client.Policies.CanViewStoreSettings, ("View your stores", "The app will be able to view stores settings.")},
|
||||
{$"{BTCPayServer.Client.Policies.CanViewStoreSettings}:", ("View your stores", "The app will be able to view the selected stores' settings.")},
|
||||
{BTCPayServer.Client.Policies.CanModifyServerSettings, ("Manage your server", "The app will have total control on the server settings of your server")},
|
||||
|
83
BTCPayServer/Controllers/ManageController.U2F.cs
Normal file
83
BTCPayServer/Controllers/ManageController.U2F.cs
Normal file
@ -0,0 +1,83 @@
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.U2F.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using U2F.Core.Exceptions;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class ManageController
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> U2FAuthentication()
|
||||
{
|
||||
return View(new U2FAuthenticationViewModel()
|
||||
{
|
||||
Devices = await _u2FService.GetDevices(_userManager.GetUserId(User))
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> RemoveU2FDevice(string id)
|
||||
{
|
||||
await _u2FService.RemoveDevice(id, _userManager.GetUserId(User));
|
||||
return RedirectToAction("U2FAuthentication", new
|
||||
{
|
||||
StatusMessage = "Device removed"
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult AddU2FDevice(string name)
|
||||
{
|
||||
if (!_btcPayServerEnvironment.IsSecure)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = "Cannot register U2F device while not on https or tor"
|
||||
});
|
||||
return RedirectToAction("U2FAuthentication");
|
||||
}
|
||||
|
||||
var serverRegisterResponse = _u2FService.StartDeviceRegistration(_userManager.GetUserId(User),
|
||||
Request.GetAbsoluteUriNoPathBase().ToString().TrimEnd('/'));
|
||||
|
||||
return View(new AddU2FDeviceViewModel()
|
||||
{
|
||||
AppId = serverRegisterResponse.AppId,
|
||||
Challenge = serverRegisterResponse.Challenge,
|
||||
Version = serverRegisterResponse.Version,
|
||||
Name = name
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> AddU2FDevice(AddU2FDeviceViewModel viewModel)
|
||||
{
|
||||
var errorMessage = string.Empty;
|
||||
try
|
||||
{
|
||||
if (await _u2FService.CompleteRegistration(_userManager.GetUserId(User), viewModel.DeviceResponse,
|
||||
string.IsNullOrEmpty(viewModel.Name) ? "Unlabelled U2F Device" : viewModel.Name))
|
||||
{
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Device added!";
|
||||
return RedirectToAction("U2FAuthentication");
|
||||
}
|
||||
}
|
||||
catch (U2fException e)
|
||||
{
|
||||
errorMessage = e.Message;
|
||||
}
|
||||
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = string.IsNullOrEmpty(errorMessage) ? "Could not add device." : errorMessage
|
||||
});
|
||||
return RedirectToAction("U2FAuthentication");
|
||||
}
|
||||
}
|
||||
}
|
@ -4,11 +4,13 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.ManageViewModels;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Security.GreenField;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using BTCPayServer.U2F;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
@ -27,6 +29,8 @@ namespace BTCPayServer.Controllers
|
||||
private readonly EmailSenderFactory _EmailSenderFactory;
|
||||
private readonly ILogger _logger;
|
||||
private readonly UrlEncoder _urlEncoder;
|
||||
readonly IWebHostEnvironment _Env;
|
||||
public U2FService _u2FService;
|
||||
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
|
||||
private readonly APIKeyRepository _apiKeyRepository;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
@ -44,6 +48,7 @@ namespace BTCPayServer.Controllers
|
||||
BTCPayWalletProvider walletProvider,
|
||||
StoreRepository storeRepository,
|
||||
IWebHostEnvironment env,
|
||||
U2FService u2FService,
|
||||
BTCPayServerEnvironment btcPayServerEnvironment,
|
||||
APIKeyRepository apiKeyRepository,
|
||||
IAuthorizationService authorizationService,
|
||||
@ -55,6 +60,8 @@ namespace BTCPayServer.Controllers
|
||||
_EmailSenderFactory = emailSenderFactory;
|
||||
_logger = logger;
|
||||
_urlEncoder = urlEncoder;
|
||||
_Env = env;
|
||||
_u2FService = u2FService;
|
||||
_btcPayServerEnvironment = btcPayServerEnvironment;
|
||||
_apiKeyRepository = apiKeyRepository;
|
||||
_authorizationService = authorizationService;
|
||||
|
@ -1,11 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Models;
|
||||
@ -26,21 +26,18 @@ namespace BTCPayServer.Controllers
|
||||
private readonly CurrencyNameTable _currencyNameTable;
|
||||
private readonly PullPaymentHostedService _pullPaymentHostedService;
|
||||
private readonly BTCPayNetworkJsonSerializerSettings _serializerSettings;
|
||||
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
|
||||
|
||||
public PullPaymentController(ApplicationDbContextFactory dbContextFactory,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
CurrencyNameTable currencyNameTable,
|
||||
PullPaymentHostedService pullPaymentHostedService,
|
||||
BTCPayNetworkJsonSerializerSettings serializerSettings,
|
||||
IEnumerable<IPayoutHandler> payoutHandlers)
|
||||
BTCPayServer.Services.BTCPayNetworkJsonSerializerSettings serializerSettings)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_networkProvider = networkProvider;
|
||||
_currencyNameTable = currencyNameTable;
|
||||
_pullPaymentHostedService = pullPaymentHostedService;
|
||||
_serializerSettings = serializerSettings;
|
||||
_payoutHandlers = payoutHandlers;
|
||||
}
|
||||
[Route("pull-payments/{pullPaymentId}")]
|
||||
public async Task<IActionResult> ViewPullPayment(string pullPaymentId)
|
||||
@ -58,7 +55,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
Entity = o,
|
||||
Blob = o.GetBlob(_serializerSettings),
|
||||
ProofBlob = _payoutHandlers.FirstOrDefault(handler => handler.CanHandle(o.GetPaymentMethodId()))?.ParseProof(o)
|
||||
TransactionId = o.GetProofBlob(_serializerSettings)?.TransactionId?.ToString()
|
||||
});
|
||||
var cd = _currencyNameTable.GetCurrencyData(blob.Currency, false);
|
||||
var totalPaid = payouts.Where(p => p.Entity.State != PayoutState.Cancelled).Select(p => p.Blob.Amount).Sum();
|
||||
@ -73,8 +70,7 @@ namespace BTCPayServer.Controllers
|
||||
ClaimedAmount = amountDue,
|
||||
AmountDueFormatted = _currencyNameTable.FormatCurrency(amountDue, blob.Currency),
|
||||
CurrencyData = cd,
|
||||
StartDate = pp.StartDate,
|
||||
LastRefreshed = DateTime.Now,
|
||||
LastUpdated = DateTime.Now,
|
||||
Payouts = payouts
|
||||
.Select(entity => new ViewPullPaymentModel.PayoutLine
|
||||
{
|
||||
@ -82,10 +78,10 @@ namespace BTCPayServer.Controllers
|
||||
Amount = entity.Blob.Amount,
|
||||
AmountFormatted = _currencyNameTable.FormatCurrency(entity.Blob.Amount, blob.Currency),
|
||||
Currency = blob.Currency,
|
||||
Status = entity.Entity.State,
|
||||
Destination = entity.Blob.Destination,
|
||||
Link = entity.ProofBlob?.Link,
|
||||
TransactionId = entity.ProofBlob?.Id
|
||||
Status = entity.Entity.State.GetStateString(),
|
||||
Destination = entity.Blob.Destination.Address.ToString(),
|
||||
Link = GetTransactionLink(_networkProvider.GetNetwork<BTCPayNetwork>(entity.Entity.GetPaymentMethodId().CryptoCode), entity.TransactionId),
|
||||
TransactionId = entity.TransactionId
|
||||
}).ToList()
|
||||
};
|
||||
vm.IsPending &= vm.AmountDue > 0.0m;
|
||||
@ -102,14 +98,11 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
ModelState.AddModelError(nameof(pullPaymentId), "This pull payment does not exists");
|
||||
}
|
||||
|
||||
var ppBlob = pp.GetBlob();
|
||||
var network = _networkProvider.GetNetwork<BTCPayNetwork>(ppBlob.SupportedPaymentMethods.Single().CryptoCode);
|
||||
|
||||
var paymentMethodId = ppBlob.SupportedPaymentMethods.Single();
|
||||
var payoutHandler = _payoutHandlers.FirstOrDefault(handler => handler.CanHandle(paymentMethodId));
|
||||
IClaimDestination destination = await payoutHandler?.ParseClaimDestination(paymentMethodId, vm.Destination);
|
||||
if (destination is null)
|
||||
IClaimDestination destination = null;
|
||||
if (network != null &&
|
||||
(!ClaimDestination.TryParse(vm.Destination, network, out destination) || destination is null))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Destination), $"Invalid destination");
|
||||
}
|
||||
|
@ -39,7 +39,6 @@ namespace BTCPayServer.Controllers
|
||||
Installed = pluginService.LoadedPlugins,
|
||||
Available = availablePlugins,
|
||||
Commands = pluginService.GetPendingCommands(),
|
||||
Disabled = pluginService.GetDisabledPlugins(),
|
||||
CanShowRestart = btcPayServerOptions.DockerDeployment
|
||||
};
|
||||
return View(res);
|
||||
@ -51,7 +50,6 @@ namespace BTCPayServer.Controllers
|
||||
public IEnumerable<PluginService.AvailablePlugin> Available { get; set; }
|
||||
public (string command, string plugin)[] Commands { get; set; }
|
||||
public bool CanShowRestart { get; set; }
|
||||
public string[] Disabled { get; set; }
|
||||
}
|
||||
|
||||
[HttpPost("server/plugins/uninstall")]
|
||||
@ -119,7 +117,7 @@ namespace BTCPayServer.Controllers
|
||||
public async Task<IActionResult> UploadPlugin([FromServices] PluginService pluginService,
|
||||
List<IFormFile> files)
|
||||
{
|
||||
foreach (var formFile in files.Where(file => file.Length > 0).Where(file => file.FileName.IsValidFileName()))
|
||||
foreach (var formFile in files.Where(file => file.Length > 0))
|
||||
{
|
||||
await pluginService.UploadPlugin(formFile);
|
||||
pluginService.InstallPlugin(formFile.FileName.TrimEnd(PluginManager.BTCPayPluginSuffix,
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
@ -20,8 +19,6 @@ using BTCPayServer.Storage.ViewModels;
|
||||
using BTCPayServer.Views;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
@ -33,14 +30,13 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
var fileUrl = string.IsNullOrEmpty(fileId) ? null : await _FileService.GetFileUrl(Request.GetAbsoluteRootUri(), fileId);
|
||||
|
||||
var model = new ViewFilesViewModel()
|
||||
return View(new ViewFilesViewModel()
|
||||
{
|
||||
Files = await _StoredFileRepository.GetFiles(),
|
||||
SelectedFileId = string.IsNullOrEmpty(fileUrl) ? null : fileId,
|
||||
DirectFileUrl = fileUrl,
|
||||
StorageConfigured = (await _SettingsRepository.GetSettingAsync<StorageSettings>()) != null
|
||||
};
|
||||
return View(model);
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("server/files/{fileId}/delete")]
|
||||
@ -150,15 +146,6 @@ namespace BTCPayServer.Controllers
|
||||
[HttpPost("server/files/upload")]
|
||||
public async Task<IActionResult> CreateFile(IFormFile file)
|
||||
{
|
||||
if (!file.FileName.IsValidFileName())
|
||||
{
|
||||
this.TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Message = "Invalid file name",
|
||||
Severity = StatusMessageModel.StatusSeverity.Error
|
||||
});
|
||||
return RedirectToAction(nameof(Files));
|
||||
}
|
||||
var newFile = await _FileService.AddFile(file, GetUserId());
|
||||
return RedirectToAction(nameof(Files), new
|
||||
{
|
||||
@ -178,13 +165,8 @@ namespace BTCPayServer.Controllers
|
||||
var savedSettings = await _SettingsRepository.GetSettingAsync<StorageSettings>();
|
||||
if (forceChoice || savedSettings == null)
|
||||
{
|
||||
var providersList = _StorageProviderServices.Select(a =>
|
||||
new SelectListItem(a.StorageProvider().ToString(), a.StorageProvider().ToString())
|
||||
);
|
||||
|
||||
return View(new ChooseStorageViewModel()
|
||||
{
|
||||
ProvidersList = providersList,
|
||||
ShowChangeWarning = savedSettings != null,
|
||||
Provider = savedSettings?.Provider ?? BTCPayServer.Storage.Models.StorageProvider.FileSystem
|
||||
});
|
||||
|
@ -19,40 +19,13 @@ namespace BTCPayServer.Controllers
|
||||
public partial class ServerController
|
||||
{
|
||||
[Route("server/users")]
|
||||
public async Task<IActionResult> ListUsers(
|
||||
UsersViewModel model,
|
||||
string sortOrder = null
|
||||
)
|
||||
public async Task<IActionResult> ListUsers(UsersViewModel model)
|
||||
{
|
||||
model = this.ParseListQuery(model ?? new UsersViewModel());
|
||||
|
||||
var usersQuery = _UserManager.Users;
|
||||
if (!string.IsNullOrWhiteSpace(model.SearchTerm))
|
||||
{
|
||||
#pragma warning disable CA1307 // Specify StringComparison
|
||||
// Entity Framework don't support StringComparison
|
||||
usersQuery = usersQuery.Where(u => u.Email.Contains(model.SearchTerm));
|
||||
#pragma warning restore CA1307 // Specify StringComparison
|
||||
}
|
||||
|
||||
if (sortOrder != null)
|
||||
{
|
||||
switch (sortOrder)
|
||||
{
|
||||
case "desc":
|
||||
ViewData["NextUserEmailSortOrder"] = "asc";
|
||||
usersQuery = usersQuery.OrderByDescending(user => user.Email);
|
||||
break;
|
||||
case "asc":
|
||||
usersQuery = usersQuery.OrderBy(user => user.Email);
|
||||
ViewData["NextUserEmailSortOrder"] = "desc";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
model.Users = await usersQuery
|
||||
.Skip(model.Skip)
|
||||
.Take(model.Count)
|
||||
var users = _UserManager.Users;
|
||||
model.Total = await users.CountAsync();
|
||||
model.Users = await users
|
||||
.Skip(model.Skip).Take(model.Count)
|
||||
.Select(u => new UsersViewModel.UserViewModel
|
||||
{
|
||||
Name = u.UserName,
|
||||
@ -60,9 +33,7 @@ namespace BTCPayServer.Controllers
|
||||
Id = u.Id,
|
||||
Verified = u.EmailConfirmed || !u.RequiresEmailConfirmation,
|
||||
Created = u.Created
|
||||
})
|
||||
.ToListAsync();
|
||||
model.Total = await usersQuery.CountAsync();
|
||||
}).ToListAsync();
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
@ -31,7 +31,6 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion.Internal;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NBitcoin;
|
||||
@ -76,7 +75,7 @@ namespace BTCPayServer.Controllers
|
||||
CheckConfigurationHostedService sshState,
|
||||
EventAggregator eventAggregator,
|
||||
CssThemeManager cssThemeManager,
|
||||
IOptions<ExternalServicesOptions> externalServiceOptions)
|
||||
IOptions<ExternalServicesOptions> externalServiceOptions)
|
||||
{
|
||||
_Options = options;
|
||||
_StoredFileRepository = storedFileRepository;
|
||||
@ -268,7 +267,7 @@ namespace BTCPayServer.Controllers
|
||||
sshClient.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public IHttpClientFactory HttpClientFactory { get; }
|
||||
|
||||
[Route("server/policies")]
|
||||
@ -282,9 +281,9 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[Route("server/policies")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Policies([FromServices] BTCPayNetworkProvider btcPayNetworkProvider, PoliciesSettings settings, string command = "")
|
||||
public async Task<IActionResult> Policies([FromServices] BTCPayNetworkProvider btcPayNetworkProvider,PoliciesSettings settings, string command = "")
|
||||
{
|
||||
|
||||
|
||||
ViewBag.UpdateUrlPresent = _Options.UpdateUrl != null;
|
||||
ViewBag.AppsList = await GetAppSelectList();
|
||||
if (command == "add-domain")
|
||||
@ -302,7 +301,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
settings.BlockExplorerLinks = settings.BlockExplorerLinks.Where(tuple => btcPayNetworkProvider.GetNetwork(tuple.CryptoCode).BlockExplorerLinkDefault != tuple.Link).ToList();
|
||||
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(settings);
|
||||
@ -354,7 +353,7 @@ namespace BTCPayServer.Controllers
|
||||
Link = this.Request.GetAbsoluteUriNoPathBase(externalService.Value).AbsoluteUri
|
||||
});
|
||||
}
|
||||
if (await CanShowSSHService())
|
||||
if (CanShowSSHService())
|
||||
{
|
||||
result.OtherExternalServices.Add(new ServicesViewModel.OtherExternalService()
|
||||
{
|
||||
@ -851,7 +850,7 @@ namespace BTCPayServer.Controllers
|
||||
[Route("server/services/ssh")]
|
||||
public async Task<IActionResult> SSHService()
|
||||
{
|
||||
if (!await CanShowSSHService())
|
||||
if (!CanShowSSHService())
|
||||
return NotFound();
|
||||
|
||||
var settings = _Options.SSHSettings;
|
||||
@ -889,11 +888,9 @@ namespace BTCPayServer.Controllers
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
async Task<bool> CanShowSSHService()
|
||||
bool CanShowSSHService()
|
||||
{
|
||||
var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>();
|
||||
return !(policies?.DisableSSHService is true) &&
|
||||
_Options.SSHSettings != null && (_sshState.CanUseSSH || CanAccessAuthorizedKeyFile());
|
||||
return _Options.SSHSettings != null && (_sshState.CanUseSSH || CanAccessAuthorizedKeyFile());
|
||||
}
|
||||
|
||||
private bool CanAccessAuthorizedKeyFile()
|
||||
@ -903,88 +900,55 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpPost]
|
||||
[Route("server/services/ssh")]
|
||||
public async Task<IActionResult> SSHService(SSHServiceViewModel viewModel, string command = null)
|
||||
public async Task<IActionResult> SSHService(SSHServiceViewModel viewModel)
|
||||
{
|
||||
if (!await CanShowSSHService())
|
||||
return NotFound();
|
||||
string newContent = viewModel?.SSHKeyFileContent ?? string.Empty;
|
||||
newContent = newContent.Replace("\r\n", "\n", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (command is "Save")
|
||||
bool updated = false;
|
||||
Exception exception = null;
|
||||
// Let's try to just write the file
|
||||
if (CanAccessAuthorizedKeyFile())
|
||||
{
|
||||
string newContent = viewModel?.SSHKeyFileContent ?? string.Empty;
|
||||
newContent = newContent.Replace("\r\n", "\n", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
bool updated = false;
|
||||
Exception exception = null;
|
||||
// Let's try to just write the file
|
||||
if (CanAccessAuthorizedKeyFile())
|
||||
{
|
||||
try
|
||||
{
|
||||
await System.IO.File.WriteAllTextAsync(_Options.SSHSettings.AuthorizedKeysFile, newContent);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "authorized_keys has been updated";
|
||||
updated = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exception = ex;
|
||||
}
|
||||
}
|
||||
|
||||
// If that fail, fallback to ssh
|
||||
if (!updated && _sshState.CanUseSSH)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var sshClient = await _Options.SSHSettings.ConnectAsync())
|
||||
{
|
||||
await sshClient.RunBash($"mkdir -p ~/.ssh && echo '{newContent.EscapeSingleQuotes()}' > ~/.ssh/authorized_keys", TimeSpan.FromSeconds(10));
|
||||
}
|
||||
updated = true;
|
||||
exception = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exception = ex;
|
||||
}
|
||||
}
|
||||
|
||||
if (exception is null)
|
||||
try
|
||||
{
|
||||
await System.IO.File.WriteAllTextAsync(_Options.SSHSettings.AuthorizedKeysFile, newContent);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "authorized_keys has been updated";
|
||||
updated = true;
|
||||
}
|
||||
else
|
||||
catch (Exception ex)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = exception.Message;
|
||||
exception = ex;
|
||||
}
|
||||
return RedirectToAction(nameof(SSHService));
|
||||
}
|
||||
else if (command is "disable")
|
||||
{
|
||||
return RedirectToAction(nameof(SSHServiceDisable));
|
||||
}
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
[Route("server/services/ssh/disable")]
|
||||
public IActionResult SSHServiceDisable()
|
||||
{
|
||||
return View("Confirm", new ConfirmModel()
|
||||
// If that fail, fallback to ssh
|
||||
if (!updated && _sshState.CanUseSSH)
|
||||
{
|
||||
Action = "Disable",
|
||||
Title = "Disable modification of SSH settings",
|
||||
Description = "This action is permanent and will remove the ability to change the SSH settings via the BTCPay Server user interface.",
|
||||
ButtonClass = "btn-danger"
|
||||
});
|
||||
}
|
||||
[Route("server/services/ssh/disable")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> SSHServiceDisablePost()
|
||||
{
|
||||
var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
|
||||
policies.DisableSSHService = true;
|
||||
await _SettingsRepository.UpdateSetting(policies);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Changes to the SSH settings are now permanently disabled in the BTCPay Server user interface";
|
||||
return RedirectToAction(nameof(Services));
|
||||
try
|
||||
{
|
||||
using (var sshClient = await _Options.SSHSettings.ConnectAsync())
|
||||
{
|
||||
await sshClient.RunBash($"mkdir -p ~/.ssh && echo '{newContent.EscapeSingleQuotes()}' > ~/.ssh/authorized_keys", TimeSpan.FromSeconds(10));
|
||||
}
|
||||
updated = true;
|
||||
exception = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exception = ex;
|
||||
}
|
||||
}
|
||||
|
||||
if (exception is null)
|
||||
{
|
||||
TempData[WellKnownTempData.SuccessMessage] = "authorized_keys has been updated";
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = exception.Message;
|
||||
}
|
||||
return RedirectToAction(nameof(SSHService));
|
||||
}
|
||||
|
||||
[Route("server/theme")]
|
||||
@ -1014,7 +978,7 @@ namespace BTCPayServer.Controllers
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Emails(EmailsViewModel model, string command)
|
||||
{
|
||||
|
||||
|
||||
if (command == "Test")
|
||||
{
|
||||
try
|
||||
|
400
BTCPayServer/Controllers/StoresController.BTCLike.cs
Normal file
400
BTCPayServer/Controllers/StoresController.BTCLike.cs
Normal file
@ -0,0 +1,400 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Models;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class StoresController
|
||||
{
|
||||
[HttpGet]
|
||||
[Route("{storeId}/derivations/{cryptoCode}")]
|
||||
public async Task<IActionResult> AddDerivationScheme(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
var network = cryptoCode == null ? null : _ExplorerProvider.GetNetwork(cryptoCode);
|
||||
if (network == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
DerivationSchemeViewModel vm = new DerivationSchemeViewModel();
|
||||
vm.CryptoCode = cryptoCode;
|
||||
vm.RootKeyPath = network.GetRootKeyPath();
|
||||
vm.Network = network;
|
||||
var derivation = GetExistingDerivationStrategy(vm.CryptoCode, store);
|
||||
if (derivation != null)
|
||||
{
|
||||
vm.DerivationScheme = derivation.AccountDerivation.ToString();
|
||||
vm.Config = derivation.ToJson();
|
||||
}
|
||||
vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.BTCLike));
|
||||
var hotWallet = await CanUseHotWallet();
|
||||
vm.CanUseHotWallet = hotWallet.HotWallet;
|
||||
vm.CanUseRPCImport = hotWallet.RPCImport;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private DerivationSchemeSettings GetExistingDerivationStrategy(string cryptoCode, StoreData store)
|
||||
{
|
||||
var id = new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike);
|
||||
var existing = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.OfType<DerivationSchemeSettings>()
|
||||
.FirstOrDefault(d => d.PaymentId == id);
|
||||
return existing;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/derivations/{cryptoCode}")]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public async Task<IActionResult> AddDerivationScheme(string storeId, [FromForm] DerivationSchemeViewModel vm,
|
||||
string cryptoCode)
|
||||
{
|
||||
vm.CryptoCode = cryptoCode;
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
var network = cryptoCode == null ? null : _ExplorerProvider.GetNetwork(cryptoCode);
|
||||
if (network == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
vm.Network = network;
|
||||
vm.RootKeyPath = network.GetRootKeyPath();
|
||||
DerivationSchemeSettings strategy = null;
|
||||
|
||||
var wallet = _WalletProvider.GetWallet(network);
|
||||
if (wallet == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(vm.Config))
|
||||
{
|
||||
if (!DerivationSchemeSettings.TryParseFromJson(vm.Config, network, out strategy))
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = "Config file was not in the correct format"
|
||||
});
|
||||
vm.Confirmation = false;
|
||||
return View(nameof(AddDerivationScheme), vm);
|
||||
}
|
||||
}
|
||||
|
||||
if (vm.WalletFile != null)
|
||||
{
|
||||
if (!DerivationSchemeSettings.TryParseFromWalletFile(await ReadAllText(vm.WalletFile), network,
|
||||
out strategy))
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = "Wallet file was not in the correct format"
|
||||
});
|
||||
vm.Confirmation = false;
|
||||
return View(nameof(AddDerivationScheme), vm);
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(vm.WalletFileContent))
|
||||
{
|
||||
if (!DerivationSchemeSettings.TryParseFromWalletFile(vm.WalletFileContent, network, out strategy))
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = "QR import was not in the correct format"
|
||||
});
|
||||
vm.Confirmation = false;
|
||||
return View(nameof(AddDerivationScheme), vm);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(vm.DerivationScheme))
|
||||
{
|
||||
var newStrategy = ParseDerivationStrategy(vm.DerivationScheme, null, network);
|
||||
if (newStrategy.AccountDerivation != strategy?.AccountDerivation)
|
||||
{
|
||||
var accountKey = string.IsNullOrEmpty(vm.AccountKey)
|
||||
? null
|
||||
: new BitcoinExtPubKey(vm.AccountKey, network.NBitcoinNetwork);
|
||||
if (accountKey != null)
|
||||
{
|
||||
var accountSettings =
|
||||
newStrategy.AccountKeySettings.FirstOrDefault(a => a.AccountKey == accountKey);
|
||||
if (accountSettings != null)
|
||||
{
|
||||
accountSettings.AccountKeyPath =
|
||||
vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath);
|
||||
accountSettings.RootFingerprint = string.IsNullOrEmpty(vm.RootFingerprint)
|
||||
? (HDFingerprint?)null
|
||||
: new HDFingerprint(
|
||||
NBitcoin.DataEncoders.Encoders.Hex.DecodeData(vm.RootFingerprint));
|
||||
}
|
||||
}
|
||||
|
||||
strategy = newStrategy;
|
||||
strategy.Source = vm.Source;
|
||||
vm.DerivationScheme = strategy.AccountDerivation.ToString();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
strategy = null;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
|
||||
vm.Confirmation = false;
|
||||
return View(nameof(AddDerivationScheme), vm);
|
||||
}
|
||||
}
|
||||
|
||||
var oldConfig = vm.Config;
|
||||
vm.Config = strategy == null ? null : strategy.ToJson();
|
||||
|
||||
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
|
||||
var exisingStrategy = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.Where(c => c.PaymentId == paymentMethodId)
|
||||
.OfType<DerivationSchemeSettings>()
|
||||
.FirstOrDefault();
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var wasExcluded = storeBlob.GetExcludedPaymentMethods().Match(paymentMethodId);
|
||||
var willBeExcluded = !vm.Enabled;
|
||||
|
||||
var showAddress = // Show addresses if:
|
||||
// - If the user is testing the hint address in confirmation screen
|
||||
(vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress)) ||
|
||||
// - The user is clicking on continue after changing the config
|
||||
(!vm.Confirmation && oldConfig != vm.Config) ||
|
||||
// - The user is clicking on continue without changing config nor enabling/disabling
|
||||
(!vm.Confirmation && oldConfig == vm.Config && willBeExcluded == wasExcluded);
|
||||
|
||||
showAddress = showAddress && strategy != null;
|
||||
if (!showAddress)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (strategy != null)
|
||||
await wallet.TrackAsync(strategy.AccountDerivation);
|
||||
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
|
||||
storeBlob.SetExcluded(paymentMethodId, willBeExcluded);
|
||||
storeBlob.Hints.Wallet = false;
|
||||
store.SetStoreBlob(storeBlob);
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
await _Repo.UpdateStore(store);
|
||||
_EventAggregator.Publish(new WalletChangedEvent() {WalletId = new WalletId(storeId, cryptoCode)});
|
||||
|
||||
if (willBeExcluded != wasExcluded)
|
||||
{
|
||||
var label = willBeExcluded ? "disabled" : "enabled";
|
||||
TempData[WellKnownTempData.SuccessMessage] =
|
||||
$"On-Chain payments for {network.CryptoCode} has been {label}.";
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData[WellKnownTempData.SuccessMessage] =
|
||||
$"Derivation settings for {network.CryptoCode} has been modified.";
|
||||
}
|
||||
|
||||
// This is success case when derivation scheme is added to the store
|
||||
return RedirectToAction(nameof(UpdateStore), new {storeId = storeId});
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(vm.HintAddress))
|
||||
{
|
||||
BitcoinAddress address = null;
|
||||
try
|
||||
{
|
||||
address = BitcoinAddress.Create(vm.HintAddress, network.NBitcoinNetwork);
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.HintAddress), "Invalid hint address");
|
||||
return ShowAddresses(vm, strategy);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var newStrategy = ParseDerivationStrategy(vm.DerivationScheme, address.ScriptPubKey, network);
|
||||
if (newStrategy.AccountDerivation != strategy.AccountDerivation)
|
||||
{
|
||||
strategy.AccountDerivation = newStrategy.AccountDerivation;
|
||||
strategy.AccountOriginal = null;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.HintAddress), "Impossible to find a match with this address");
|
||||
return ShowAddresses(vm, strategy);
|
||||
}
|
||||
|
||||
vm.HintAddress = "";
|
||||
TempData[WellKnownTempData.SuccessMessage] =
|
||||
"Address successfully found, please verify that the rest is correct and click on \"Confirm\"";
|
||||
ModelState.Remove(nameof(vm.HintAddress));
|
||||
ModelState.Remove(nameof(vm.DerivationScheme));
|
||||
}
|
||||
|
||||
return ShowAddresses(vm, strategy);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/derivations/{cryptoCode}/generatenbxwallet")]
|
||||
public async Task<IActionResult> GenerateNBXWallet(string storeId, string cryptoCode,
|
||||
GenerateWalletRequest request)
|
||||
{
|
||||
var hotWallet = await CanUseHotWallet();
|
||||
if (!hotWallet.HotWallet || (!hotWallet.RPCImport && request.ImportKeysToRPC))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var network = _NetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
||||
var client = _ExplorerProvider.GetExplorerClient(cryptoCode);
|
||||
GenerateWalletResponse response;
|
||||
try
|
||||
{
|
||||
response = await client.GenerateWalletAsync(request);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Html = $"There was an error generating your wallet: {e.Message}"
|
||||
});
|
||||
return RedirectToAction(nameof(AddDerivationScheme), new {storeId, cryptoCode});
|
||||
}
|
||||
|
||||
if (response == null)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Html = "There was an error generating your wallet. Is your node available?"
|
||||
});
|
||||
return RedirectToAction(nameof(AddDerivationScheme), new {storeId, cryptoCode});
|
||||
}
|
||||
|
||||
var store = HttpContext.GetStoreData();
|
||||
var result = await AddDerivationScheme(storeId,
|
||||
new DerivationSchemeViewModel()
|
||||
{
|
||||
Confirmation = string.IsNullOrEmpty(request.ExistingMnemonic),
|
||||
Network = network,
|
||||
RootFingerprint = response.AccountKeyPath.MasterFingerprint.ToString(),
|
||||
RootKeyPath = network.GetRootKeyPath(),
|
||||
CryptoCode = cryptoCode,
|
||||
DerivationScheme = response.DerivationScheme.ToString(),
|
||||
Source = "NBXplorer",
|
||||
AccountKey = response.AccountHDKey.Neuter().ToWif(),
|
||||
DerivationSchemeFormat = "BTCPay",
|
||||
KeyPath = response.AccountKeyPath.KeyPath.ToString(),
|
||||
Enabled = !store.GetStoreBlob()
|
||||
.IsExcluded(new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike))
|
||||
}, cryptoCode);
|
||||
if (!ModelState.IsValid || !(result is RedirectToActionResult))
|
||||
return result;
|
||||
TempData.Clear();
|
||||
if (string.IsNullOrEmpty(request.ExistingMnemonic))
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Html = $"<span class='text-centered'>Your wallet has been generated.</span>"
|
||||
});
|
||||
var vm = new RecoverySeedBackupViewModel()
|
||||
{
|
||||
CryptoCode = cryptoCode,
|
||||
Mnemonic = response.Mnemonic,
|
||||
Passphrase = response.Passphrase,
|
||||
IsStored = request.SavePrivateKeys,
|
||||
ReturnUrl = Url.Action(nameof(UpdateStore), new {storeId})
|
||||
};
|
||||
return this.RedirectToRecoverySeedBackup(vm);
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Warning,
|
||||
Html = "Please check your addresses and confirm"
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<(bool HotWallet, bool RPCImport)> CanUseHotWallet()
|
||||
{
|
||||
var isAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings))
|
||||
.Succeeded;
|
||||
if (isAdmin)
|
||||
return (true, true);
|
||||
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>();
|
||||
var hotWallet = policies?.AllowHotWalletForAll is true;
|
||||
return (hotWallet, hotWallet && policies?.AllowHotWalletRPCImportForAll is true);
|
||||
}
|
||||
|
||||
private async Task<string> ReadAllText(IFormFile file)
|
||||
{
|
||||
using (var stream = new StreamReader(file.OpenReadStream()))
|
||||
{
|
||||
return await stream.ReadToEndAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private IActionResult
|
||||
ShowAddresses(DerivationSchemeViewModel vm, DerivationSchemeSettings strategy)
|
||||
{
|
||||
vm.DerivationScheme = strategy.AccountDerivation.ToString();
|
||||
var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
|
||||
if (!string.IsNullOrEmpty(vm.DerivationScheme))
|
||||
{
|
||||
var line = strategy.AccountDerivation.GetLineFor(deposit);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var keyPath = deposit.GetKeyPath((uint)i);
|
||||
var rootedKeyPath = vm.GetAccountKeypath()?.Derive(keyPath);
|
||||
var derivation = line.Derive((uint)i);
|
||||
var address = strategy.Network.NBXplorerNetwork.CreateAddress(strategy.AccountDerivation,
|
||||
line.KeyPathTemplate.GetKeyPath((uint)i),
|
||||
derivation.ScriptPubKey).ToString();
|
||||
vm.AddressSamples.Add((keyPath.ToString(), address, rootedKeyPath));
|
||||
}
|
||||
}
|
||||
vm.Confirmation = true;
|
||||
ModelState.Remove(nameof(vm.Config)); // Remove the cached value
|
||||
return View(nameof(AddDerivationScheme), vm);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,27 +1,15 @@
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using BTCPayServer.Payments.CoinSwitch;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Plugins.CoinSwitch
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[Route("plugins/{storeId}/coinswitch")]
|
||||
public class CoinSwitchController : Controller
|
||||
public partial class StoresController
|
||||
{
|
||||
private readonly StoreRepository _storeRepository;
|
||||
|
||||
public CoinSwitchController(StoreRepository storeRepository)
|
||||
{
|
||||
_storeRepository = storeRepository;
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
[HttpGet]
|
||||
[Route("{storeId}/coinswitch")]
|
||||
public IActionResult UpdateCoinSwitchSettings(string storeId)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
@ -34,7 +22,8 @@ namespace BTCPayServer.Plugins.CoinSwitch
|
||||
|
||||
private void SetExistingValues(StoreData store, UpdateCoinSwitchSettingsViewModel vm)
|
||||
{
|
||||
var existing = store.GetStoreBlob().GetCoinSwitchSettings();
|
||||
|
||||
var existing = store.GetStoreBlob().CoinSwitchSettings;
|
||||
if (existing == null)
|
||||
return;
|
||||
vm.MerchantId = existing.MerchantId;
|
||||
@ -43,7 +32,8 @@ namespace BTCPayServer.Plugins.CoinSwitch
|
||||
vm.AmountMarkupPercentage = existing.AmountMarkupPercentage;
|
||||
}
|
||||
|
||||
[HttpPost("")]
|
||||
[HttpPost]
|
||||
[Route("{storeId}/coinswitch")]
|
||||
public async Task<IActionResult> UpdateCoinSwitchSettings(string storeId, UpdateCoinSwitchSettingsViewModel vm,
|
||||
string command)
|
||||
{
|
||||
@ -70,11 +60,14 @@ namespace BTCPayServer.Plugins.CoinSwitch
|
||||
{
|
||||
case "save":
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
storeBlob.SetCoinSwitchSettings(coinSwitchSettings);
|
||||
storeBlob.CoinSwitchSettings = coinSwitchSettings;
|
||||
store.SetStoreBlob(storeBlob);
|
||||
await _storeRepository.UpdateStore(store);
|
||||
await _Repo.UpdateStore(store);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "CoinSwitch settings modified";
|
||||
return RedirectToAction(nameof(UpdateCoinSwitchSettings), new {storeId});
|
||||
return RedirectToAction(nameof(UpdateStore), new
|
||||
{
|
||||
storeId
|
||||
});
|
||||
|
||||
default:
|
||||
return View(vm);
|
@ -1,26 +1,189 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Shopify;
|
||||
using BTCPayServer.Services.Shopify.ApiModels;
|
||||
using BTCPayServer.Services.Shopify.Models;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NicolasDorier.RateLimits;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class StoresController
|
||||
{
|
||||
[HttpGet("{storeId}/integrations")]
|
||||
public IActionResult Integrations()
|
||||
{
|
||||
return View("Integrations",new IntegrationsViewModel());
|
||||
private static string _cachedShopifyJavascript;
|
||||
|
||||
private async Task<string> GetJavascript()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_cachedShopifyJavascript) && !_BTCPayEnv.IsDeveloping)
|
||||
{
|
||||
return _cachedShopifyJavascript;
|
||||
}
|
||||
|
||||
string[] fileList = _BtcpayServerOptions.BundleJsCss
|
||||
? new[] {"bundles/shopify-bundle.min.js"}
|
||||
: new[] {"modal/btcpay.js", "shopify/btcpay-shopify.js"};
|
||||
|
||||
|
||||
foreach (var file in fileList)
|
||||
{
|
||||
await using var stream = _webHostEnvironment.WebRootFileProvider
|
||||
.GetFileInfo(file).CreateReadStream();
|
||||
using var reader = new StreamReader(stream);
|
||||
_cachedShopifyJavascript += Environment.NewLine + await reader.ReadToEndAsync();
|
||||
}
|
||||
|
||||
return _cachedShopifyJavascript;
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/webhooks")]
|
||||
[AllowAnonymous]
|
||||
[HttpGet("{storeId}/integrations/shopify/shopify.js")]
|
||||
public async Task<IActionResult> ShopifyJavascript(string storeId)
|
||||
{
|
||||
var jsFile =
|
||||
$"var BTCPAYSERVER_URL = \"{Request.GetAbsoluteRoot()}\"; var STORE_ID = \"{storeId}\"; {await GetJavascript()}";
|
||||
return Content(jsFile, "text/javascript");
|
||||
}
|
||||
|
||||
[RateLimitsFilter(ZoneLimits.Shopify, Scope = RateLimitsScope.RemoteAddress)]
|
||||
[AllowAnonymous]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
[HttpGet("{storeId}/integrations/shopify/{orderId}")]
|
||||
public async Task<IActionResult> ShopifyInvoiceEndpoint(
|
||||
[FromServices] InvoiceRepository invoiceRepository,
|
||||
[FromServices] InvoiceController invoiceController,
|
||||
[FromServices] IHttpClientFactory httpClientFactory,
|
||||
string storeId, string orderId, decimal amount, bool checkOnly = false)
|
||||
{
|
||||
var invoiceOrderId = $"{ShopifyOrderMarkerHostedService.SHOPIFY_ORDER_ID_PREFIX}{orderId}";
|
||||
var matchedExistingInvoices = await invoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
OrderId = new[] {invoiceOrderId}, StoreId = new[] {storeId}
|
||||
});
|
||||
matchedExistingInvoices = matchedExistingInvoices.Where(entity =>
|
||||
entity.GetInternalTags(ShopifyOrderMarkerHostedService.SHOPIFY_ORDER_ID_PREFIX)
|
||||
.Any(s => s == orderId))
|
||||
.ToArray();
|
||||
|
||||
var firstInvoiceStillPending =
|
||||
matchedExistingInvoices.FirstOrDefault(entity => entity.GetInvoiceState().Status == InvoiceStatusLegacy.New);
|
||||
if (firstInvoiceStillPending != null)
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
invoiceId = firstInvoiceStillPending.Id,
|
||||
status = firstInvoiceStillPending.Status.ToString().ToLowerInvariant()
|
||||
});
|
||||
}
|
||||
|
||||
var firstInvoiceSettled =
|
||||
matchedExistingInvoices.LastOrDefault(entity =>
|
||||
new[] {InvoiceStatusLegacy.Paid, InvoiceStatusLegacy.Complete, InvoiceStatusLegacy.Confirmed}.Contains(
|
||||
entity.GetInvoiceState().Status));
|
||||
|
||||
var store = await _Repo.FindStore(storeId);
|
||||
var shopify = store?.GetStoreBlob()?.Shopify;
|
||||
ShopifyApiClient client = null;
|
||||
ShopifyOrder order = null;
|
||||
if (shopify?.IntegratedAt.HasValue is true)
|
||||
{
|
||||
client = new ShopifyApiClient(httpClientFactory, shopify.CreateShopifyApiCredentials());
|
||||
order = await client.GetOrder(orderId);
|
||||
if (string.IsNullOrEmpty(order?.Id))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
}
|
||||
|
||||
if (firstInvoiceSettled != null)
|
||||
{
|
||||
//if BTCPay was shut down before the tx managed to get registered on shopify, this will fix it on the next UI load in shopify
|
||||
if (client != null && order?.FinancialStatus == "pending" &&
|
||||
firstInvoiceSettled.Status != InvoiceStatusLegacy.Paid)
|
||||
{
|
||||
await new OrderTransactionRegisterLogic(client).Process(orderId, firstInvoiceSettled.Id,
|
||||
firstInvoiceSettled.Currency,
|
||||
firstInvoiceSettled.Price.ToString(CultureInfo.InvariantCulture), true);
|
||||
order = await client.GetOrder(orderId);
|
||||
}
|
||||
|
||||
if (order?.FinancialStatus != "pending" && order?.FinancialStatus != "partially_paid")
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
invoiceId = firstInvoiceSettled.Id,
|
||||
status = firstInvoiceSettled.Status.ToString().ToLowerInvariant()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (checkOnly)
|
||||
{
|
||||
return Ok();
|
||||
}
|
||||
|
||||
if (shopify?.IntegratedAt.HasValue is true)
|
||||
{
|
||||
if (string.IsNullOrEmpty(order?.Id) ||
|
||||
!new[] {"pending", "partially_paid"}.Contains(order.FinancialStatus))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
//we create the invoice at due amount provided from order page or full amount if due amount is bigger than order amount
|
||||
var invoice = await invoiceController.CreateInvoiceCoreRaw(
|
||||
new CreateInvoiceRequest()
|
||||
{
|
||||
Amount = amount < order.TotalPrice ? amount : order.TotalPrice,
|
||||
Currency = order.Currency,
|
||||
Metadata = new JObject {["orderId"] = invoiceOrderId}
|
||||
}, store,
|
||||
Request.GetAbsoluteUri(""), new List<string>() {invoiceOrderId});
|
||||
|
||||
return Ok(new {invoiceId = invoice.Id, status = invoice.Status.ToString().ToLowerInvariant()});
|
||||
}
|
||||
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/integrations")]
|
||||
[Route("{storeId}/integrations/shopify")]
|
||||
public IActionResult Integrations()
|
||||
{
|
||||
var blob = CurrentStore.GetStoreBlob();
|
||||
|
||||
var vm = new IntegrationsViewModel {Shopify = blob.Shopify};
|
||||
|
||||
return View("Integrations", vm);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/webhooks")]
|
||||
public async Task<IActionResult> Webhooks()
|
||||
{
|
||||
var webhooks = await _Repo.GetWebhooks(CurrentStore.Id);
|
||||
var webhooks = await this._Repo.GetWebhooks(CurrentStore.Id);
|
||||
return View(nameof(Webhooks), new WebhooksViewModel()
|
||||
{
|
||||
Webhooks = webhooks.Select(w => new WebhooksViewModel.WebhookViewModel()
|
||||
@ -30,11 +193,11 @@ namespace BTCPayServer.Controllers
|
||||
}).ToArray()
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/webhooks/new")]
|
||||
[HttpGet]
|
||||
[Route("{storeId}/webhooks/new")]
|
||||
public IActionResult NewWebhook()
|
||||
{
|
||||
return View(nameof(ModifyWebhook), new EditWebhookViewModel
|
||||
return View(nameof(ModifyWebhook), new EditWebhookViewModel()
|
||||
{
|
||||
Active = true,
|
||||
Everything = true,
|
||||
@ -43,14 +206,14 @@ namespace BTCPayServer.Controllers
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/webhooks/{webhookId}/remove")]
|
||||
[HttpGet]
|
||||
[Route("{storeId}/webhooks/{webhookId}/remove")]
|
||||
public async Task<IActionResult> DeleteWebhook(string webhookId)
|
||||
{
|
||||
var webhook = await _Repo.GetWebhook(CurrentStore.Id, webhookId);
|
||||
if (webhook is null)
|
||||
return NotFound();
|
||||
|
||||
return View("Confirm", new ConfirmModel
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Title = $"Delete a webhook",
|
||||
Description = "This webhook will be removed from this store, do you wish to continue?",
|
||||
@ -58,36 +221,36 @@ namespace BTCPayServer.Controllers
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/webhooks/{webhookId}/remove")]
|
||||
[HttpPost]
|
||||
[Route("{storeId}/webhooks/{webhookId}/remove")]
|
||||
public async Task<IActionResult> DeleteWebhookPost(string webhookId)
|
||||
{
|
||||
var webhook = await _Repo.GetWebhook(CurrentStore.Id, webhookId);
|
||||
if (webhook is null)
|
||||
return NotFound();
|
||||
|
||||
await _Repo.DeleteWebhook(CurrentStore.Id, webhookId);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Webhook successfully deleted";
|
||||
return RedirectToAction(nameof(Webhooks), new { storeId = CurrentStore.Id });
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/webhooks/new")]
|
||||
[HttpPost]
|
||||
[Route("{storeId}/webhooks/new")]
|
||||
public async Task<IActionResult> NewWebhook(string storeId, EditWebhookViewModel viewModel)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
return View(nameof(ModifyWebhook), viewModel);
|
||||
return View(viewModel);
|
||||
|
||||
await _Repo.CreateWebhook(CurrentStore.Id, viewModel.CreateBlob());
|
||||
var webhookId = await _Repo.CreateWebhook(CurrentStore.Id, viewModel.CreateBlob());
|
||||
TempData[WellKnownTempData.SuccessMessage] = "The webhook has been created";
|
||||
return RedirectToAction(nameof(Webhooks), new { storeId });
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/webhooks/{webhookId}")]
|
||||
[HttpGet]
|
||||
[Route("{storeId}/webhooks/{webhookId}")]
|
||||
public async Task<IActionResult> ModifyWebhook(string webhookId)
|
||||
{
|
||||
var webhook = await _Repo.GetWebhook(CurrentStore.Id, webhookId);
|
||||
if (webhook is null)
|
||||
return NotFound();
|
||||
|
||||
var blob = webhook.GetBlob();
|
||||
var deliveries = await _Repo.GetWebhookDeliveries(CurrentStore.Id, webhookId, 20);
|
||||
return View(nameof(ModifyWebhook), new EditWebhookViewModel(blob)
|
||||
@ -96,8 +259,8 @@ namespace BTCPayServer.Controllers
|
||||
.Select(s => new DeliveryViewModel(s)).ToList()
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/webhooks/{webhookId}")]
|
||||
[HttpPost]
|
||||
[Route("{storeId}/webhooks/{webhookId}")]
|
||||
public async Task<IActionResult> ModifyWebhook(string webhookId, EditWebhookViewModel viewModel)
|
||||
{
|
||||
var webhook = await _Repo.GetWebhook(CurrentStore.Id, webhookId);
|
||||
@ -109,17 +272,16 @@ namespace BTCPayServer.Controllers
|
||||
return RedirectToAction(nameof(Webhooks), new { storeId = CurrentStore.Id });
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/redeliver")]
|
||||
[HttpPost]
|
||||
[Route("{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/redeliver")]
|
||||
public async Task<IActionResult> RedeliverWebhook(string webhookId, string deliveryId)
|
||||
{
|
||||
var delivery = await _Repo.GetWebhookDelivery(CurrentStore.Id, webhookId, deliveryId);
|
||||
if (delivery is null)
|
||||
return NotFound();
|
||||
|
||||
var newDeliveryId = await WebhookNotificationManager.Redeliver(deliveryId);
|
||||
if (newDeliveryId is null)
|
||||
return NotFound();
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Successfully planned a redelivery";
|
||||
return RedirectToAction(nameof(ModifyWebhook),
|
||||
new
|
||||
@ -128,15 +290,104 @@ namespace BTCPayServer.Controllers
|
||||
webhookId
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/request")]
|
||||
[HttpGet]
|
||||
[Route("{storeId}/webhooks/{webhookId}/deliveries/{deliveryId}/request")]
|
||||
public async Task<IActionResult> WebhookDelivery(string webhookId, string deliveryId)
|
||||
{
|
||||
var delivery = await _Repo.GetWebhookDelivery(CurrentStore.Id, webhookId, deliveryId);
|
||||
if (delivery is null)
|
||||
return NotFound();
|
||||
return this.File(delivery.GetBlob().Request, "application/json");
|
||||
}
|
||||
|
||||
return File(delivery.GetBlob().Request, "application/json");
|
||||
[HttpPost]
|
||||
[Route("{storeId}/integrations/shopify")]
|
||||
public async Task<IActionResult> Integrations([FromServices] IHttpClientFactory clientFactory,
|
||||
IntegrationsViewModel vm, string command = "", string exampleUrl = "")
|
||||
{
|
||||
if (!string.IsNullOrEmpty(exampleUrl))
|
||||
{
|
||||
try
|
||||
{
|
||||
//https://{apikey}:{password}@{hostname}/admin/api/{version}/{resource}.json
|
||||
var parsedUrl = new Uri(exampleUrl);
|
||||
var userInfo = parsedUrl.UserInfo.Split(":");
|
||||
vm.Shopify = new ShopifySettings()
|
||||
{
|
||||
ApiKey = userInfo[0],
|
||||
Password = userInfo[1],
|
||||
ShopName = parsedUrl.Host.Replace(".myshopify.com", "",
|
||||
StringComparison.InvariantCultureIgnoreCase)
|
||||
};
|
||||
command = "ShopifySaveCredentials";
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "The provided Example Url was invalid.";
|
||||
return View("Integrations", vm);
|
||||
}
|
||||
}
|
||||
|
||||
switch (command)
|
||||
{
|
||||
case "ShopifySaveCredentials":
|
||||
{
|
||||
var shopify = vm.Shopify;
|
||||
var validCreds = shopify != null && shopify?.CredentialsPopulated() == true;
|
||||
if (!validCreds)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "Please provide valid Shopify credentials";
|
||||
return View("Integrations", vm);
|
||||
}
|
||||
|
||||
var apiClient = new ShopifyApiClient(clientFactory, shopify.CreateShopifyApiCredentials());
|
||||
try
|
||||
{
|
||||
await apiClient.OrdersCount();
|
||||
}
|
||||
catch (ShopifyApiException)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] =
|
||||
"Shopify rejected provided credentials, please correct values and try again";
|
||||
return View("Integrations", vm);
|
||||
}
|
||||
|
||||
var scopesGranted = await apiClient.CheckScopes();
|
||||
if (!scopesGranted.Contains("read_orders") || !scopesGranted.Contains("write_orders"))
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] =
|
||||
"Please grant the private app permissions for read_orders, write_orders";
|
||||
return View("Integrations", vm);
|
||||
}
|
||||
|
||||
// everything ready, proceed with saving Shopify integration credentials
|
||||
shopify.IntegratedAt = DateTimeOffset.Now;
|
||||
|
||||
var blob = CurrentStore.GetStoreBlob();
|
||||
blob.Shopify = shopify;
|
||||
if (CurrentStore.SetStoreBlob(blob))
|
||||
{
|
||||
await _Repo.UpdateStore(CurrentStore);
|
||||
}
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Shopify integration successfully updated";
|
||||
break;
|
||||
}
|
||||
case "ShopifyClearCredentials":
|
||||
{
|
||||
var blob = CurrentStore.GetStoreBlob();
|
||||
blob.Shopify = null;
|
||||
if (CurrentStore.SetStoreBlob(blob))
|
||||
{
|
||||
await _Repo.UpdateStore(CurrentStore);
|
||||
}
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Shopify integration credentials cleared";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Integrations), new {storeId = CurrentStore.Id});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,80 +14,118 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class StoresController
|
||||
{
|
||||
[HttpGet("{storeId}/lightning/{cryptoCode}")]
|
||||
public IActionResult SetupLightningNode(string storeId, string cryptoCode)
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/lightning/{cryptoCode}")]
|
||||
public IActionResult AddLightningNode(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
var vm = new LightningNodeViewModel
|
||||
LightningNodeViewModel vm = new LightningNodeViewModel
|
||||
{
|
||||
CryptoCode = cryptoCode,
|
||||
InternalLightningNode = GetInternalLighningNode(cryptoCode)?.ToString(),
|
||||
StoreId = storeId
|
||||
};
|
||||
SetExistingValues(store, vm);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/lightning/{cryptoCode}")]
|
||||
public async Task<IActionResult> SetupLightningNode(string storeId, LightningNodeViewModel vm, string command, string cryptoCode)
|
||||
private void SetExistingValues(StoreData store, LightningNodeViewModel vm)
|
||||
{
|
||||
vm.ConnectionString = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store)?.GetLightningUrl()?.ToString();
|
||||
vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.LightningLike));
|
||||
}
|
||||
private LightningSupportedPaymentMethod GetExistingLightningSupportedPaymentMethod(string cryptoCode, StoreData store)
|
||||
{
|
||||
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
|
||||
var existing = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.OfType<LightningSupportedPaymentMethod>()
|
||||
.FirstOrDefault(d => d.PaymentId == id);
|
||||
return existing;
|
||||
}
|
||||
|
||||
private LightningConnectionString GetInternalLighningNode(string cryptoCode)
|
||||
{
|
||||
if (_lightningNetworkOptions.Value.InternalLightningByCryptoCode.TryGetValue(cryptoCode, out var connectionString))
|
||||
{
|
||||
return CanUseInternalLightning() ? connectionString : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/lightning/{cryptoCode}")]
|
||||
public async Task<IActionResult> AddLightningNode(string storeId, LightningNodeViewModel vm, string command, string cryptoCode)
|
||||
{
|
||||
vm.CryptoCode = cryptoCode;
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
vm.CanUseInternalNode = CanUseInternalLightning();
|
||||
|
||||
var network = vm.CryptoCode == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCode);
|
||||
|
||||
var internalLightning = GetInternalLighningNode(network.CryptoCode);
|
||||
vm.InternalLightningNode = internalLightning?.ToString();
|
||||
if (network == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.CryptoCode), "Invalid network");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
var paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike);
|
||||
var lightning = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store);
|
||||
|
||||
LightningSupportedPaymentMethod paymentMethod = null;
|
||||
if (vm.LightningNodeType == LightningNodeType.Internal)
|
||||
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike);
|
||||
Payments.Lightning.LightningSupportedPaymentMethod paymentMethod = null;
|
||||
if (!string.IsNullOrEmpty(vm.ConnectionString))
|
||||
{
|
||||
if (!CanUseInternalLightning())
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "You are not authorized to use the internal lightning node");
|
||||
return View(vm);
|
||||
}
|
||||
paymentMethod = new LightningSupportedPaymentMethod
|
||||
{
|
||||
CryptoCode = paymentMethodId.CryptoCode
|
||||
};
|
||||
paymentMethod.SetInternalNode();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (string.IsNullOrEmpty(vm.ConnectionString))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "Please provide a connection string");
|
||||
return View(vm);
|
||||
}
|
||||
if (!LightningConnectionString.TryParse(vm.ConnectionString, false, out var connectionString, out var error))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), $"Invalid URL ({error})");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
if (connectionString.ConnectionType == LightningConnectionType.LndGRPC)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), $"BTCPay does not support gRPC connections");
|
||||
return View(vm);
|
||||
}
|
||||
if (!User.IsInRole(Roles.ServerAdmin) && !connectionString.IsSafe())
|
||||
|
||||
bool isInternalNode = connectionString.IsInternalNode(internalLightning);
|
||||
|
||||
if (connectionString.BaseUri.Scheme == "http")
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "You are not a server admin, so the connection string should not contain 'cookiefilepath', 'macaroondirectorypath', 'macaroonfilepath', and should not point to a local ip or to a dns name ending with '.internal', '.local', '.lan' or '.'.");
|
||||
if (!isInternalNode && !connectionString.AllowInsecure)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "The url must be HTTPS");
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
if (connectionString.MacaroonFilePath != null)
|
||||
{
|
||||
if (!CanUseInternalLightning())
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "You are not authorized to use macaroonfilepath");
|
||||
return View(vm);
|
||||
}
|
||||
if (!System.IO.File.Exists(connectionString.MacaroonFilePath))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "The macaroonfilepath file does not exist");
|
||||
return View(vm);
|
||||
}
|
||||
if (!System.IO.Path.IsPathRooted(connectionString.MacaroonFilePath))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "The macaroonfilepath should be fully rooted");
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
if (isInternalNode && !CanUseInternalLightning())
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "Unauthorized url");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
paymentMethod = new LightningSupportedPaymentMethod
|
||||
paymentMethod = new Payments.Lightning.LightningSupportedPaymentMethod()
|
||||
{
|
||||
CryptoCode = paymentMethodId.CryptoCode
|
||||
};
|
||||
@ -98,24 +136,29 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
case "save":
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
storeBlob.SetExcluded(paymentMethodId, !vm.Enabled);
|
||||
storeBlob.Hints.Lightning = false;
|
||||
store.SetStoreBlob(storeBlob);
|
||||
store.SetSupportedPaymentMethod(paymentMethodId, paymentMethod);
|
||||
await _Repo.UpdateStore(store);
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"{network.CryptoCode} Lightning node updated.";
|
||||
return RedirectToAction(nameof(UpdateStore), new { storeId });
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"Lightning node modified ({network.CryptoCode})";
|
||||
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
|
||||
case "test" when paymentMethod == null:
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "Missing url parameter");
|
||||
return View(vm);
|
||||
case "test":
|
||||
var handler = _ServiceProvider.GetRequiredService<LightningLikePaymentHandler>();
|
||||
try
|
||||
{
|
||||
var info = await handler.GetNodeInfo(Request.IsOnion(), paymentMethod, network);
|
||||
var info = await handler.GetNodeInfo(this.Request.IsOnion(), paymentMethod, network);
|
||||
if (!vm.SkipPortTest)
|
||||
{
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20));
|
||||
await handler.TestConnection(info, cts.Token);
|
||||
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)))
|
||||
{
|
||||
await handler.TestConnection(info, cts.Token);
|
||||
}
|
||||
}
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"Connection to the Lightning node successful. Your node address: {info}";
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"Connection to the lightning node succeeded. Your node address: {info}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -123,64 +166,14 @@ namespace BTCPayServer.Controllers
|
||||
return View(vm);
|
||||
}
|
||||
return View(vm);
|
||||
|
||||
default:
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/lightning/{cryptoCode}/status")]
|
||||
public async Task<IActionResult> SetLightningNodeEnabled(string storeId, string cryptoCode, bool enabled)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
var network = cryptoCode == null ? null : _ExplorerProvider.GetNetwork(cryptoCode);
|
||||
if (network == null)
|
||||
return NotFound();
|
||||
|
||||
var lightning = GetExistingLightningSupportedPaymentMethod(cryptoCode, store);
|
||||
if (lightning == null)
|
||||
return NotFound();
|
||||
|
||||
var paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike);
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
storeBlob.SetExcluded(paymentMethodId, !enabled);
|
||||
store.SetStoreBlob(storeBlob);
|
||||
await _Repo.UpdateStore(store);
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"{network.CryptoCode} Lightning payments are now {(enabled ? "enabled" : "disabled")} for this store.";
|
||||
|
||||
return RedirectToAction(nameof(UpdateStore), new { storeId });
|
||||
}
|
||||
|
||||
private bool CanUseInternalLightning()
|
||||
{
|
||||
return User.IsInRole(Roles.ServerAdmin) || _CssThemeManager.AllowLightningInternalNodeForAll;
|
||||
}
|
||||
|
||||
private void SetExistingValues(StoreData store, LightningNodeViewModel vm)
|
||||
{
|
||||
vm.CanUseInternalNode = CanUseInternalLightning();
|
||||
var lightning = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store);
|
||||
if (lightning != null)
|
||||
{
|
||||
vm.LightningNodeType = lightning.IsInternalNode ? LightningNodeType.Internal : LightningNodeType.Custom;
|
||||
vm.ConnectionString = lightning.GetDisplayableConnectionString();
|
||||
}
|
||||
else
|
||||
{
|
||||
vm.LightningNodeType = vm.CanUseInternalNode ? LightningNodeType.Internal : LightningNodeType.Custom;
|
||||
}
|
||||
}
|
||||
|
||||
private LightningSupportedPaymentMethod GetExistingLightningSupportedPaymentMethod(string cryptoCode, StoreData store)
|
||||
{
|
||||
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
|
||||
var existing = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.OfType<LightningSupportedPaymentMethod>()
|
||||
.FirstOrDefault(d => d.PaymentId == id);
|
||||
return existing;
|
||||
return (_BTCPayEnv.IsDeveloping || User.IsInRole(Roles.ServerAdmin) || _CssThemeManager.AllowLightningInternalNodeForAll);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,21 +1,16 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Models;
|
||||
|
||||
@ -110,34 +105,42 @@ namespace BTCPayServer.Controllers
|
||||
return View(vm.ViewName, vm);
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(vm.DerivationScheme))
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
var newStrategy = ParseDerivationStrategy(vm.DerivationScheme, null, network);
|
||||
if (newStrategy.AccountDerivation != strategy?.AccountDerivation)
|
||||
if (!string.IsNullOrEmpty(vm.DerivationScheme))
|
||||
{
|
||||
var accountKey = string.IsNullOrEmpty(vm.AccountKey)
|
||||
? null
|
||||
: new BitcoinExtPubKey(vm.AccountKey, network.NBitcoinNetwork);
|
||||
if (accountKey != null)
|
||||
var newStrategy = ParseDerivationStrategy(vm.DerivationScheme, null, network);
|
||||
if (newStrategy.AccountDerivation != strategy?.AccountDerivation)
|
||||
{
|
||||
var accountSettings =
|
||||
newStrategy.AccountKeySettings.FirstOrDefault(a => a.AccountKey == accountKey);
|
||||
if (accountSettings != null)
|
||||
var accountKey = string.IsNullOrEmpty(vm.AccountKey)
|
||||
? null
|
||||
: new BitcoinExtPubKey(vm.AccountKey, network.NBitcoinNetwork);
|
||||
if (accountKey != null)
|
||||
{
|
||||
accountSettings.AccountKeyPath =
|
||||
vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath);
|
||||
accountSettings.RootFingerprint = string.IsNullOrEmpty(vm.RootFingerprint)
|
||||
? (HDFingerprint?)null
|
||||
: new HDFingerprint(
|
||||
NBitcoin.DataEncoders.Encoders.Hex.DecodeData(vm.RootFingerprint));
|
||||
var accountSettings =
|
||||
newStrategy.AccountKeySettings.FirstOrDefault(a => a.AccountKey == accountKey);
|
||||
if (accountSettings != null)
|
||||
{
|
||||
accountSettings.AccountKeyPath =
|
||||
vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath);
|
||||
accountSettings.RootFingerprint = string.IsNullOrEmpty(vm.RootFingerprint)
|
||||
? (HDFingerprint?)null
|
||||
: new HDFingerprint(
|
||||
NBitcoin.DataEncoders.Encoders.Hex.DecodeData(vm.RootFingerprint));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
strategy = newStrategy;
|
||||
strategy.Source = vm.Source;
|
||||
vm.DerivationScheme = strategy.AccountDerivation.ToString();
|
||||
strategy = newStrategy;
|
||||
strategy.Source = vm.Source;
|
||||
vm.DerivationScheme = strategy.AccountDerivation.ToString();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DerivationScheme), "Please provide your extended public key");
|
||||
return View(vm.ViewName, vm);
|
||||
}
|
||||
}
|
||||
catch
|
||||
@ -146,18 +149,15 @@ namespace BTCPayServer.Controllers
|
||||
return View(vm.ViewName, vm);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DerivationScheme), "Please provide your extended public key");
|
||||
return View(vm.ViewName, vm);
|
||||
}
|
||||
|
||||
var oldConfig = vm.Config;
|
||||
vm.Config = strategy?.ToJson();
|
||||
var configChanged = oldConfig != vm.Config;
|
||||
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var wasExcluded = storeBlob.GetExcludedPaymentMethods().Match(paymentMethodId);
|
||||
var willBeExcluded = !vm.Enabled;
|
||||
var excludedChanged = willBeExcluded != wasExcluded;
|
||||
|
||||
var showAddress = // Show addresses if:
|
||||
// - If the user is testing the hint address in confirmation screen
|
||||
@ -186,7 +186,17 @@ namespace BTCPayServer.Controllers
|
||||
await _Repo.UpdateStore(store);
|
||||
_EventAggregator.Publish(new WalletChangedEvent {WalletId = new WalletId(vm.StoreId, vm.CryptoCode)});
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"Derivation settings for {network.CryptoCode} have been updated.";
|
||||
if (excludedChanged)
|
||||
{
|
||||
var label = willBeExcluded ? "disabled" : "enabled";
|
||||
TempData[WellKnownTempData.SuccessMessage] =
|
||||
$"On-Chain payments for {network.CryptoCode} have been {label}.";
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData[WellKnownTempData.SuccessMessage] =
|
||||
$"Derivation settings for {network.CryptoCode} have been modified.";
|
||||
}
|
||||
|
||||
// This is success case when derivation scheme is added to the store
|
||||
return RedirectToAction(nameof(UpdateStore), new {storeId = vm.StoreId});
|
||||
@ -377,7 +387,8 @@ namespace BTCPayServer.Controllers
|
||||
return checkResult;
|
||||
}
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"Derivation settings for {network.CryptoCode} have been updated.";
|
||||
TempData[WellKnownTempData.SuccessMessage] =
|
||||
$"Derivation settings for {network.CryptoCode} have been modified.";
|
||||
|
||||
return RedirectToAction(nameof(UpdateStore), new {storeId});
|
||||
}
|
||||
@ -398,8 +409,6 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
var (hotWallet, rpcImport) = await CanUseHotWallet();
|
||||
var isHotWallet = await IsHotWallet(vm.CryptoCode, derivation);
|
||||
|
||||
vm.CanUseHotWallet = hotWallet;
|
||||
vm.CanUseRPCImport = rpcImport;
|
||||
vm.RootKeyPath = network.GetRootKeyPath();
|
||||
@ -410,61 +419,12 @@ namespace BTCPayServer.Controllers
|
||||
vm.KeyPath = derivation.GetSigningAccountKeySettings().AccountKeyPath?.ToString();
|
||||
vm.Config = derivation.ToJson();
|
||||
vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.BTCLike));
|
||||
vm.IsHotWallet = isHotWallet;
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/onchain/{cryptoCode}/replace")]
|
||||
public async Task<IActionResult> ReplaceWallet(string storeId, string cryptoCode)
|
||||
{
|
||||
var checkResult = IsAvailable(cryptoCode, out var store, out var network);
|
||||
if (checkResult != null)
|
||||
{
|
||||
return checkResult;
|
||||
}
|
||||
|
||||
var derivation = GetExistingDerivationStrategy(cryptoCode, store);
|
||||
var isHotWallet = await IsHotWallet(cryptoCode, derivation);
|
||||
var walletType = isHotWallet ? "hot" : "watch-only";
|
||||
var additionalText = isHotWallet
|
||||
? ""
|
||||
: " or imported into an external wallet. If you no longer have access to your private key (recovery seed), immediately replace the wallet";
|
||||
var description =
|
||||
$"<p class=\"text-danger font-weight-bold\">Please note that this is a {walletType} wallet!</p>" +
|
||||
$"<p class=\"text-danger font-weight-bold\">Do not replace the wallet if you have not backed it up{additionalText}.</p>" +
|
||||
"<p class=\"text-left mb-0\">Replacing the wallet will erase the current wallet data from the server. " +
|
||||
"The current wallet will be replaced once you finish the setup of the new wallet. If you cancel the setup, the current wallet will stay active .</p>";
|
||||
|
||||
return View("Confirm", new ConfirmModel
|
||||
{
|
||||
Title = $"Replace {network.CryptoCode} wallet",
|
||||
Description = description,
|
||||
DescriptionHtml = true,
|
||||
Action = "Setup new wallet"
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/onchain/{cryptoCode}/replace")]
|
||||
public IActionResult ConfirmReplaceWallet(string storeId, string cryptoCode)
|
||||
{
|
||||
var checkResult = IsAvailable(cryptoCode, out var store, out _);
|
||||
if (checkResult != null)
|
||||
{
|
||||
return checkResult;
|
||||
}
|
||||
|
||||
var derivation = GetExistingDerivationStrategy(cryptoCode, store);
|
||||
if (derivation == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(SetupWallet), new {storeId, cryptoCode});
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/onchain/{cryptoCode}/delete")]
|
||||
public async Task<IActionResult> DeleteWallet(string storeId, string cryptoCode)
|
||||
public IActionResult DeleteWallet(string storeId, string cryptoCode)
|
||||
{
|
||||
var checkResult = IsAvailable(cryptoCode, out var store, out var network);
|
||||
if (checkResult != null)
|
||||
@ -473,14 +433,9 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
var derivation = GetExistingDerivationStrategy(cryptoCode, store);
|
||||
var isHotWallet = await IsHotWallet(cryptoCode, derivation);
|
||||
var walletType = isHotWallet ? "hot" : "watch-only";
|
||||
var additionalText = isHotWallet
|
||||
? ""
|
||||
: " or imported into an external wallet. If you no longer have access to your private key (recovery seed), immediately replace the wallet";
|
||||
var description =
|
||||
$"<p class=\"text-danger font-weight-bold\">Please note that this is a {walletType} wallet!</p>" +
|
||||
$"<p class=\"text-danger font-weight-bold\">Do not remove the wallet if you have not backed it up{additionalText}.</p>" +
|
||||
(derivation.IsHotWallet ? "<p class=\"text-danger font-weight-bold\">Please note that this is a hot wallet!</p> " : "") +
|
||||
"<p class=\"text-danger font-weight-bold\">Do not remove the wallet if you have not backed it up!</p>" +
|
||||
"<p class=\"text-left mb-0\">Removing the wallet will erase the wallet data from the server. " +
|
||||
$"The store won't be able to receive {network.CryptoCode} onchain payments until a new wallet is set up.</p>";
|
||||
|
||||
@ -493,40 +448,6 @@ namespace BTCPayServer.Controllers
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/onchain/{cryptoCode}/status")]
|
||||
public async Task<IActionResult> SetWalletEnabled(string storeId, string cryptoCode, bool enabled)
|
||||
{
|
||||
var checkResult = IsAvailable(cryptoCode, out var store, out var network);
|
||||
if (checkResult != null)
|
||||
{
|
||||
return checkResult;
|
||||
}
|
||||
|
||||
var derivation = GetExistingDerivationStrategy(cryptoCode, store);
|
||||
if (derivation == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var wallet = _WalletProvider.GetWallet(network);
|
||||
if (wallet == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
storeBlob.SetExcluded(paymentMethodId, !enabled);
|
||||
store.SetStoreBlob(storeBlob);
|
||||
await _Repo.UpdateStore(store);
|
||||
_EventAggregator.Publish(new WalletChangedEvent {WalletId = new WalletId(storeId, cryptoCode)});
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] =
|
||||
$"{network.CryptoCode} on-chain payments are now {(enabled ? "enabled" : "disabled")} for this store.";
|
||||
|
||||
return RedirectToAction(nameof(UpdateStore), new {storeId});
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/onchain/{cryptoCode}/delete")]
|
||||
public async Task<IActionResult> ConfirmDeleteWallet(string storeId, string cryptoCode)
|
||||
{
|
||||
@ -557,7 +478,7 @@ namespace BTCPayServer.Controllers
|
||||
private IActionResult ConfirmAddresses(WalletSetupViewModel vm, DerivationSchemeSettings strategy)
|
||||
{
|
||||
vm.DerivationScheme = strategy.AccountDerivation.ToString();
|
||||
var deposit = new KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
|
||||
var deposit = new NBXplorer.KeyPathTemplates(null).GetKeyPathTemplate(DerivationFeature.Deposit);
|
||||
|
||||
if (!string.IsNullOrEmpty(vm.DerivationScheme))
|
||||
{
|
||||
@ -588,34 +509,5 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
return store == null || network == null ? NotFound() : null;
|
||||
}
|
||||
|
||||
private DerivationSchemeSettings GetExistingDerivationStrategy(string cryptoCode, StoreData store)
|
||||
{
|
||||
var id = new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike);
|
||||
var existing = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.OfType<DerivationSchemeSettings>()
|
||||
.FirstOrDefault(d => d.PaymentId == id);
|
||||
return existing;
|
||||
}
|
||||
|
||||
private async Task<(bool HotWallet, bool RPCImport)> CanUseHotWallet()
|
||||
{
|
||||
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>();
|
||||
return await _authorizationService.CanUseHotWallet(policies, User);
|
||||
}
|
||||
|
||||
private async Task<string> ReadAllText(IFormFile file)
|
||||
{
|
||||
using (var stream = new StreamReader(file.OpenReadStream()))
|
||||
{
|
||||
return await stream.ReadToEndAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> IsHotWallet(string cryptoCode, DerivationSchemeSettings derivation)
|
||||
{
|
||||
return derivation.IsHotWallet && await _ExplorerProvider.GetExplorerClient(cryptoCode)
|
||||
.GetMetadataAsync<string>(derivation.AccountDerivation, WellknownMetadataKeys.MasterHDKey) != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Shopify;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using BundlerMinifier.TagHelpers;
|
||||
@ -405,7 +406,6 @@ namespace BTCPayServer.Controllers
|
||||
vm.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi;
|
||||
vm.LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints;
|
||||
vm.OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback;
|
||||
vm.LazyPaymentMethods = storeBlob.LazyPaymentMethods;
|
||||
vm.RedirectAutomatically = storeBlob.RedirectAutomatically;
|
||||
vm.ShowRecommendedFee = storeBlob.ShowRecommendedFee;
|
||||
vm.RecommendedFeeBlockTarget = storeBlob.RecommendedFeeBlockTarget;
|
||||
@ -477,7 +477,6 @@ namespace BTCPayServer.Controllers
|
||||
}).ToList();
|
||||
|
||||
blob.RequiresRefundEmail = model.RequiresRefundEmail;
|
||||
blob.LazyPaymentMethods = model.LazyPaymentMethods;
|
||||
blob.LightningAmountInSatoshi = model.LightningAmountInSatoshi;
|
||||
blob.LightningPrivateRouteHints = model.LightningPrivateRouteHints;
|
||||
blob.OnChainWithLnInvoiceFallback = model.OnChainWithLnInvoiceFallback;
|
||||
@ -544,16 +543,24 @@ namespace BTCPayServer.Controllers
|
||||
break;
|
||||
case LightningPaymentType _:
|
||||
var lightning = lightningByCryptoCode.TryGet(paymentMethodId.CryptoCode);
|
||||
var isEnabled = !excludeFilters.Match(paymentMethodId) && lightning != null;
|
||||
vm.LightningNodes.Add(new StoreViewModel.LightningNode
|
||||
vm.LightningNodes.Add(new StoreViewModel.LightningNode()
|
||||
{
|
||||
CryptoCode = paymentMethodId.CryptoCode,
|
||||
Address = lightning?.GetDisplayableConnectionString(),
|
||||
Enabled = isEnabled
|
||||
Address = lightning?.GetLightningUrl()?.BaseUri.AbsoluteUri ?? string.Empty,
|
||||
Enabled = !excludeFilters.Match(paymentMethodId) && lightning?.GetLightningUrl() != null
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var coinSwitchEnabled = storeBlob.CoinSwitchSettings != null && storeBlob.CoinSwitchSettings.Enabled;
|
||||
vm.ThirdPartyPaymentMethods.Add(new StoreViewModel.AdditionalPaymentMethod()
|
||||
{
|
||||
Enabled = coinSwitchEnabled,
|
||||
Action = nameof(UpdateCoinSwitchSettings),
|
||||
Provider = "CoinSwitch"
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -849,12 +856,6 @@ namespace BTCPayServer.Controllers
|
||||
if (string.IsNullOrWhiteSpace(userId))
|
||||
return Challenge(AuthenticationSchemes.Cookie);
|
||||
var storeId = CurrentStore?.Id;
|
||||
if (storeId != null)
|
||||
{
|
||||
var store = await _Repo.FindStore(storeId, userId);
|
||||
if (store != null)
|
||||
HttpContext.SetStoreData(store);
|
||||
}
|
||||
var model = new CreateTokenViewModel();
|
||||
ViewBag.HidePublicKey = true;
|
||||
ViewBag.ShowStores = true;
|
||||
@ -911,14 +912,6 @@ namespace BTCPayServer.Controllers
|
||||
return Challenge(AuthenticationSchemes.Cookie);
|
||||
if (pairingCode == null)
|
||||
return NotFound();
|
||||
if (selectedStore != null)
|
||||
{
|
||||
var store = await _Repo.FindStore(selectedStore, userId);
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
HttpContext.SetStoreData(store);
|
||||
ViewBag.ShowStores = false;
|
||||
}
|
||||
var pairing = await _TokenRepository.GetPairingAsync(pairingCode);
|
||||
if (pairing == null)
|
||||
{
|
||||
@ -928,7 +921,7 @@ namespace BTCPayServer.Controllers
|
||||
else
|
||||
{
|
||||
var stores = await _Repo.GetStoresByUserId(userId);
|
||||
return View(new PairingModel
|
||||
return View(new PairingModel()
|
||||
{
|
||||
Id = pairing.Id,
|
||||
Label = pairing.Label,
|
||||
@ -987,6 +980,8 @@ namespace BTCPayServer.Controllers
|
||||
return _UserManager.GetUserId(User);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// TODO: Need to have talk about how architect default currency implementation
|
||||
// For now we have also hardcoded USD for Store creation and then Invoice creation
|
||||
const string DEFAULT_CURRENCY = "USD";
|
||||
@ -1015,7 +1010,7 @@ namespace BTCPayServer.Controllers
|
||||
ButtonType = 0,
|
||||
Min = 1,
|
||||
Max = 20,
|
||||
Step = "1",
|
||||
Step = 1,
|
||||
Apps = apps
|
||||
};
|
||||
return View(model);
|
||||
|
@ -9,11 +9,9 @@ using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.ModelBinders;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.WalletViewModels;
|
||||
using BTCPayServer.Payments.PayJoin.Sender;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using BTCPayServer.BIP78.Sender;
|
||||
using NBitcoin.Payment;
|
||||
using NBXplorer;
|
||||
using NBXplorer.Models;
|
||||
@ -173,7 +171,7 @@ namespace BTCPayServer.Controllers
|
||||
var cloned = psbt.Clone();
|
||||
cloned = cloned.Finalize();
|
||||
await _broadcaster.Schedule(DateTimeOffset.UtcNow + TimeSpan.FromMinutes(2.0), cloned.ExtractTransaction(), btcPayNetwork);
|
||||
return await _payjoinClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, cancellationToken);
|
||||
return await _payjoinClient.RequestPayjoin(bip21, derivationSchemeSettings, psbt, cancellationToken);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
|
@ -6,7 +6,6 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.ModelBinders;
|
||||
@ -18,7 +17,6 @@ using BTCPayServer.Views;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
using PayoutData = BTCPayServer.Data.PayoutData;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -191,9 +189,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var storeId = walletId.StoreId;
|
||||
var paymentMethodId = new PaymentMethodId(walletId.CryptoCode, PaymentTypes.BTCLike);
|
||||
|
||||
var commandState = Enum.Parse<PayoutState>(vm.Command.Split("-").First());
|
||||
var payoutIds = vm.GetSelectedPayouts(commandState);
|
||||
var payoutIds = vm.WaitingForApproval.Where(p => p.Selected).Select(p => p.PayoutId).ToArray();
|
||||
if (payoutIds.Length == 0)
|
||||
{
|
||||
this.TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
@ -207,121 +203,93 @@ namespace BTCPayServer.Controllers
|
||||
pullPaymentId = vm.PullPaymentId
|
||||
});
|
||||
}
|
||||
|
||||
var command = vm.Command.Substring(vm.Command.IndexOf('-', StringComparison.InvariantCulture) + 1);
|
||||
|
||||
switch (command)
|
||||
if (vm.Command == "pay")
|
||||
{
|
||||
|
||||
case "approve-pay":
|
||||
case "approve":
|
||||
{
|
||||
await using var ctx = this._dbContextFactory.CreateContext();
|
||||
ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||
var payouts = await GetPayoutsForPaymentMethod(walletId.GetPaymentMethodId(), ctx, payoutIds, storeId, cancellationToken);
|
||||
|
||||
for (int i = 0; i < payouts.Count; i++)
|
||||
{
|
||||
var payout = payouts[i];
|
||||
if (payout.State != PayoutState.AwaitingApproval)
|
||||
continue;
|
||||
var rateResult = await _pullPaymentService.GetRate(payout, null, cancellationToken);
|
||||
if (rateResult.BidAsk == null)
|
||||
{
|
||||
this.TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Message = $"Rate unavailable: {rateResult.EvaluatedRule}",
|
||||
Severity = StatusMessageModel.StatusSeverity.Error
|
||||
});
|
||||
return RedirectToAction(nameof(Payouts), new
|
||||
{
|
||||
walletId = walletId.ToString(),
|
||||
pullPaymentId = vm.PullPaymentId
|
||||
});
|
||||
}
|
||||
var approveResult = await _pullPaymentService.Approve(new HostedServices.PullPaymentHostedService.PayoutApproval()
|
||||
{
|
||||
PayoutId = payout.Id,
|
||||
Revision = payout.GetBlob(_jsonSerializerSettings).Revision,
|
||||
Rate = rateResult.BidAsk.Ask
|
||||
});
|
||||
if (approveResult != HostedServices.PullPaymentHostedService.PayoutApproval.Result.Ok)
|
||||
{
|
||||
this.TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Message = PullPaymentHostedService.PayoutApproval.GetErrorMessage(approveResult),
|
||||
Severity = StatusMessageModel.StatusSeverity.Error
|
||||
});
|
||||
return RedirectToAction(nameof(Payouts), new
|
||||
{
|
||||
walletId = walletId.ToString(),
|
||||
pullPaymentId = vm.PullPaymentId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (command == "approve-pay")
|
||||
{
|
||||
goto case "pay";
|
||||
}
|
||||
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Message = "Payouts approved", Severity = StatusMessageModel.StatusSeverity.Success
|
||||
});
|
||||
return RedirectToAction(nameof(Payouts),
|
||||
new {walletId = walletId.ToString(), pullPaymentId = vm.PullPaymentId});
|
||||
}
|
||||
|
||||
case "pay":
|
||||
{
|
||||
await using var ctx = this._dbContextFactory.CreateContext();
|
||||
ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||
var payouts = await GetPayoutsForPaymentMethod(walletId.GetPaymentMethodId(), ctx, payoutIds, storeId, cancellationToken);
|
||||
|
||||
var walletSend = (WalletSendModel)((ViewResult)(await this.WalletSend(walletId))).Model;
|
||||
walletSend.Outputs.Clear();
|
||||
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
|
||||
List<string> bip21 = new List<string>();
|
||||
foreach (var payout in payouts)
|
||||
{
|
||||
var blob = payout.GetBlob(_jsonSerializerSettings);
|
||||
if (payout.GetPaymentMethodId() != paymentMethodId)
|
||||
continue;
|
||||
bip21.Add(network.GenerateBIP21(payout.Destination, new Money(blob.CryptoAmount.Value, MoneyUnit.BTC)));
|
||||
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(WalletSend), new {walletId, bip21});
|
||||
}
|
||||
|
||||
case "cancel":
|
||||
await _pullPaymentService.Cancel(
|
||||
new HostedServices.PullPaymentHostedService.CancelRequest(payoutIds));
|
||||
this.TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Message = "Payouts archived", Severity = StatusMessageModel.StatusSeverity.Success
|
||||
});
|
||||
return RedirectToAction(nameof(Payouts),
|
||||
new {walletId = walletId.ToString(), pullPaymentId = vm.PullPaymentId});
|
||||
}
|
||||
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
private static async Task<List<PayoutData>> GetPayoutsForPaymentMethod(PaymentMethodId paymentMethodId,
|
||||
ApplicationDbContext ctx, string[] payoutIds,
|
||||
string storeId, CancellationToken cancellationToken)
|
||||
{
|
||||
var payouts = (await ctx.Payouts
|
||||
using var ctx = this._dbContextFactory.CreateContext();
|
||||
ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||
var payouts = (await ctx.Payouts
|
||||
.Include(p => p.PullPaymentData)
|
||||
.Include(p => p.PullPaymentData.StoreData)
|
||||
.Where(p => payoutIds.Contains(p.Id))
|
||||
.Where(p => p.PullPaymentData.StoreId == storeId && !p.PullPaymentData.Archived)
|
||||
.ToListAsync(cancellationToken))
|
||||
.Where(p => p.GetPaymentMethodId() == paymentMethodId)
|
||||
.ToList();
|
||||
return payouts;
|
||||
.ToListAsync())
|
||||
.Where(p => p.GetPaymentMethodId() == walletId.GetPaymentMethodId())
|
||||
.ToList();
|
||||
|
||||
for (int i = 0; i < payouts.Count; i++)
|
||||
{
|
||||
var payout = payouts[i];
|
||||
if (payout.State != PayoutState.AwaitingApproval)
|
||||
continue;
|
||||
var rateResult = await _pullPaymentService.GetRate(payout, null, cancellationToken);
|
||||
if (rateResult.BidAsk == null)
|
||||
{
|
||||
this.TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Message = $"Rate unavailable: {rateResult.EvaluatedRule}",
|
||||
Severity = StatusMessageModel.StatusSeverity.Error
|
||||
});
|
||||
return RedirectToAction(nameof(Payouts), new
|
||||
{
|
||||
walletId = walletId.ToString(),
|
||||
pullPaymentId = vm.PullPaymentId
|
||||
});
|
||||
}
|
||||
var approveResult = await _pullPaymentService.Approve(new HostedServices.PullPaymentHostedService.PayoutApproval()
|
||||
{
|
||||
PayoutId = payout.Id,
|
||||
Revision = payout.GetBlob(_jsonSerializerSettings).Revision,
|
||||
Rate = rateResult.BidAsk.Ask
|
||||
});
|
||||
if (approveResult != HostedServices.PullPaymentHostedService.PayoutApproval.Result.Ok)
|
||||
{
|
||||
this.TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Message = PullPaymentHostedService.PayoutApproval.GetErrorMessage(approveResult),
|
||||
Severity = StatusMessageModel.StatusSeverity.Error
|
||||
});
|
||||
return RedirectToAction(nameof(Payouts), new
|
||||
{
|
||||
walletId = walletId.ToString(),
|
||||
pullPaymentId = vm.PullPaymentId
|
||||
});
|
||||
}
|
||||
payouts[i] = await ctx.Payouts.FindAsync(payouts[i].Id);
|
||||
}
|
||||
var walletSend = (WalletSendModel)((ViewResult)(await this.WalletSend(walletId))).Model;
|
||||
walletSend.Outputs.Clear();
|
||||
foreach (var payout in payouts)
|
||||
{
|
||||
var blob = payout.GetBlob(_jsonSerializerSettings);
|
||||
if (payout.GetPaymentMethodId() != paymentMethodId)
|
||||
continue;
|
||||
var output = new WalletSendModel.TransactionOutput()
|
||||
{
|
||||
Amount = blob.CryptoAmount,
|
||||
DestinationAddress = blob.Destination.Address.ToString()
|
||||
};
|
||||
walletSend.Outputs.Add(output);
|
||||
}
|
||||
return View(nameof(walletSend), walletSend);
|
||||
}
|
||||
else if (vm.Command == "cancel")
|
||||
{
|
||||
await _pullPaymentService.Cancel(new HostedServices.PullPaymentHostedService.CancelRequest(payoutIds));
|
||||
this.TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Message = "Payouts archived",
|
||||
Severity = StatusMessageModel.StatusSeverity.Success
|
||||
});
|
||||
return RedirectToAction(nameof(Payouts), new
|
||||
{
|
||||
walletId = walletId.ToString(),
|
||||
pullPaymentId = vm.PullPaymentId
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -331,11 +299,9 @@ namespace BTCPayServer.Controllers
|
||||
WalletId walletId, PayoutsModel vm = null)
|
||||
{
|
||||
vm ??= new PayoutsModel();
|
||||
vm.PayoutStateSets ??= ((PayoutState[]) Enum.GetValues(typeof(PayoutState))).Select(state =>
|
||||
new PayoutsModel.PayoutStateSet() {State = state, Payouts = new List<PayoutsModel.PayoutModel>()}).ToList();
|
||||
using var ctx = this._dbContextFactory.CreateContext();
|
||||
var storeId = walletId.StoreId;
|
||||
vm.PaymentMethodId = new PaymentMethodId(walletId.CryptoCode, PaymentTypes.BTCLike);
|
||||
var paymentMethodId = new PaymentMethodId(walletId.CryptoCode, PaymentTypes.BTCLike);
|
||||
var payoutRequest = ctx.Payouts.Where(p => p.PullPaymentData.StoreId == storeId && !p.PullPaymentData.Archived);
|
||||
if (vm.PullPaymentId != null)
|
||||
{
|
||||
@ -347,42 +313,34 @@ namespace BTCPayServer.Controllers
|
||||
Payout = o,
|
||||
PullPayment = o.PullPaymentData
|
||||
}).ToListAsync();
|
||||
foreach (var stateSet in payouts.GroupBy(arg => arg.Payout.State))
|
||||
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
|
||||
vm.WaitingForApproval = new List<PayoutsModel.PayoutModel>();
|
||||
vm.Other = new List<PayoutsModel.PayoutModel>();
|
||||
foreach (var item in payouts)
|
||||
{
|
||||
var state = vm.PayoutStateSets.SingleOrDefault(set => set.State == stateSet.Key);
|
||||
if (state == null)
|
||||
if (item.Payout.GetPaymentMethodId() != paymentMethodId)
|
||||
continue;
|
||||
var ppBlob = item.PullPayment.GetBlob();
|
||||
var payoutBlob = item.Payout.GetBlob(_jsonSerializerSettings);
|
||||
var m = new PayoutsModel.PayoutModel();
|
||||
m.PullPaymentId = item.PullPayment.Id;
|
||||
m.PullPaymentName = ppBlob.Name ?? item.PullPayment.Id;
|
||||
m.Date = item.Payout.Date;
|
||||
m.PayoutId = item.Payout.Id;
|
||||
m.Amount = _currencyTable.DisplayFormatCurrency(payoutBlob.Amount, ppBlob.Currency);
|
||||
m.Destination = payoutBlob.Destination.Address.ToString();
|
||||
if (item.Payout.State == PayoutState.AwaitingPayment || item.Payout.State == PayoutState.AwaitingApproval)
|
||||
{
|
||||
state = new PayoutsModel.PayoutStateSet()
|
||||
{
|
||||
Payouts = new List<PayoutsModel.PayoutModel>(), State = stateSet.Key
|
||||
};
|
||||
vm.PayoutStateSets.Add(state);
|
||||
vm.WaitingForApproval.Add(m);
|
||||
}
|
||||
|
||||
foreach (var item in stateSet)
|
||||
else
|
||||
{
|
||||
|
||||
if (item.Payout.GetPaymentMethodId() != vm.PaymentMethodId)
|
||||
continue;
|
||||
var ppBlob = item.PullPayment.GetBlob();
|
||||
var payoutBlob = item.Payout.GetBlob(_jsonSerializerSettings);
|
||||
var m = new PayoutsModel.PayoutModel();
|
||||
m.PullPaymentId = item.PullPayment.Id;
|
||||
m.PullPaymentName = ppBlob.Name ?? item.PullPayment.Id;
|
||||
m.Date = item.Payout.Date;
|
||||
m.PayoutId = item.Payout.Id;
|
||||
m.Amount = _currencyTable.DisplayFormatCurrency(payoutBlob.Amount, ppBlob.Currency);
|
||||
m.Destination = payoutBlob.Destination;
|
||||
var handler = _payoutHandlers
|
||||
.FirstOrDefault(handler => handler.CanHandle(item.Payout.GetPaymentMethodId()));
|
||||
var proofBlob = handler?.ParseProof(item.Payout);
|
||||
m.TransactionLink = proofBlob?.Link;
|
||||
state.Payouts.Add(m);
|
||||
|
||||
if (item.Payout.GetPaymentMethodId().PaymentType == PaymentTypes.BTCLike &&
|
||||
item.Payout.GetProofBlob(this._jsonSerializerSettings)?.TransactionId is uint256 txId)
|
||||
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, txId);
|
||||
vm.Other.Add(m);
|
||||
}
|
||||
}
|
||||
|
||||
vm.PayoutStateSets = vm.PayoutStateSets.Where(set => set.Payouts?.Any() is true).ToList();
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Models.WalletViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Labels;
|
||||
using BTCPayServer.Services.Rates;
|
||||
@ -24,14 +25,11 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using BTCPayServer.BIP78.Sender;
|
||||
using BTCPayServer.Payments.PayJoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBXplorer;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Models;
|
||||
using Newtonsoft.Json;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -51,7 +49,7 @@ namespace BTCPayServer.Controllers
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly IFeeProviderFactory _feeRateProvider;
|
||||
private readonly BTCPayWalletProvider _walletProvider;
|
||||
private readonly WalletReceiveService _walletReceiveService;
|
||||
private readonly WalletReceiveStateService _WalletReceiveStateService;
|
||||
private readonly EventAggregator _EventAggregator;
|
||||
private readonly SettingsRepository _settingsRepository;
|
||||
private readonly DelayedTransactionBroadcaster _broadcaster;
|
||||
@ -60,7 +58,6 @@ namespace BTCPayServer.Controllers
|
||||
private readonly ApplicationDbContextFactory _dbContextFactory;
|
||||
private readonly BTCPayNetworkJsonSerializerSettings _jsonSerializerSettings;
|
||||
private readonly PullPaymentHostedService _pullPaymentService;
|
||||
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
|
||||
|
||||
public RateFetcher RateFetcher { get; }
|
||||
|
||||
@ -77,7 +74,7 @@ namespace BTCPayServer.Controllers
|
||||
ExplorerClientProvider explorerProvider,
|
||||
IFeeProviderFactory feeRateProvider,
|
||||
BTCPayWalletProvider walletProvider,
|
||||
WalletReceiveService walletReceiveService,
|
||||
WalletReceiveStateService walletReceiveStateService,
|
||||
EventAggregator eventAggregator,
|
||||
SettingsRepository settingsRepository,
|
||||
DelayedTransactionBroadcaster broadcaster,
|
||||
@ -85,8 +82,7 @@ namespace BTCPayServer.Controllers
|
||||
LabelFactory labelFactory,
|
||||
ApplicationDbContextFactory dbContextFactory,
|
||||
BTCPayNetworkJsonSerializerSettings jsonSerializerSettings,
|
||||
HostedServices.PullPaymentHostedService pullPaymentService,
|
||||
IEnumerable<IPayoutHandler> payoutHandlers)
|
||||
HostedServices.PullPaymentHostedService pullPaymentService)
|
||||
{
|
||||
_currencyTable = currencyTable;
|
||||
Repository = repo;
|
||||
@ -100,7 +96,7 @@ namespace BTCPayServer.Controllers
|
||||
ExplorerClientProvider = explorerProvider;
|
||||
_feeRateProvider = feeRateProvider;
|
||||
_walletProvider = walletProvider;
|
||||
_walletReceiveService = walletReceiveService;
|
||||
_WalletReceiveStateService = walletReceiveStateService;
|
||||
_EventAggregator = eventAggregator;
|
||||
_settingsRepository = settingsRepository;
|
||||
_broadcaster = broadcaster;
|
||||
@ -109,7 +105,6 @@ namespace BTCPayServer.Controllers
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_jsonSerializerSettings = jsonSerializerSettings;
|
||||
_pullPaymentService = pullPaymentService;
|
||||
_payoutHandlers = payoutHandlers;
|
||||
}
|
||||
|
||||
// Borrowed from https://github.com/ManageIQ/guides/blob/master/labels.md
|
||||
@ -343,8 +338,6 @@ namespace BTCPayServer.Controllers
|
||||
model.Transactions = model.Transactions.OrderByDescending(t => t.Timestamp).Skip(skip).Take(count).ToList();
|
||||
}
|
||||
|
||||
model.CryptoCode = walletId.CryptoCode;
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
@ -366,20 +359,13 @@ namespace BTCPayServer.Controllers
|
||||
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId?.CryptoCode);
|
||||
if (network == null)
|
||||
return NotFound();
|
||||
var address = _walletReceiveService.Get(walletId)?.Address;
|
||||
var allowedPayjoin = paymentMethod.IsHotWallet && CurrentStore.GetStoreBlob().PayJoinEnabled;
|
||||
var bip21 = address is null ? null : network.GenerateBIP21(address.ToString(), null);
|
||||
if (allowedPayjoin)
|
||||
{
|
||||
bip21 +=
|
||||
$"?{PayjoinClient.BIP21EndpointKey}={Request.GetAbsoluteUri(Url.Action(nameof(PayJoinEndpointController.Submit), "PayJoinEndpoint", new {walletId.CryptoCode}))}";
|
||||
}
|
||||
|
||||
var address = _WalletReceiveStateService.Get(walletId)?.Address;
|
||||
return View(new WalletReceiveViewModel()
|
||||
{
|
||||
CryptoCode = walletId.CryptoCode,
|
||||
Address = address?.ToString(),
|
||||
CryptoImage = GetImage(paymentMethod.PaymentId, network),
|
||||
PaymentLink = bip21
|
||||
CryptoImage = GetImage(paymentMethod.PaymentId, network)
|
||||
});
|
||||
}
|
||||
|
||||
@ -396,22 +382,29 @@ namespace BTCPayServer.Controllers
|
||||
var network = this.NetworkProvider.GetNetwork<BTCPayNetwork>(walletId?.CryptoCode);
|
||||
if (network == null)
|
||||
return NotFound();
|
||||
var wallet = _walletProvider.GetWallet(network);
|
||||
switch (command)
|
||||
{
|
||||
case "unreserve-current-address":
|
||||
var address = await _walletReceiveService.UnReserveAddress(walletId);
|
||||
if (!string.IsNullOrEmpty(address))
|
||||
KeyPathInformation cachedAddress = _WalletReceiveStateService.Get(walletId);
|
||||
if (cachedAddress == null)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
AllowDismiss = true,
|
||||
Message = $"Address {address} was unreserved.",
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
});
|
||||
break;
|
||||
}
|
||||
var address = cachedAddress.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork);
|
||||
ExplorerClientProvider.GetExplorerClient(network)
|
||||
.CancelReservation(cachedAddress.DerivationStrategy, new[] { cachedAddress.KeyPath });
|
||||
this.TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
AllowDismiss = true,
|
||||
Message = $"Address {address} was unreserved.",
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
});
|
||||
_WalletReceiveStateService.Remove(walletId);
|
||||
break;
|
||||
case "generate-new-address":
|
||||
await _walletReceiveService.GetOrGenerate(walletId, true);
|
||||
var reserve = (await wallet.ReserveAddressAsync(paymentMethod.AccountDerivation));
|
||||
_WalletReceiveStateService.Set(walletId, reserve);
|
||||
break;
|
||||
}
|
||||
return RedirectToAction(nameof(WalletReceive), new { walletId });
|
||||
@ -419,15 +412,18 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
private async Task<bool> CanUseHotWallet()
|
||||
{
|
||||
var isAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings)).Succeeded;
|
||||
if (isAdmin)
|
||||
return true;
|
||||
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>();
|
||||
return (await _authorizationService.CanUseHotWallet(policies, User)).HotWallet;
|
||||
return policies?.AllowHotWalletForAll is true;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{walletId}/send")]
|
||||
public async Task<IActionResult> WalletSend(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, string defaultDestination = null, string defaultAmount = null, string[] bip21 = null)
|
||||
WalletId walletId, string defaultDestination = null, string defaultAmount = null, string bip21 = null)
|
||||
{
|
||||
if (walletId?.StoreId == null)
|
||||
return NotFound();
|
||||
@ -445,29 +441,19 @@ namespace BTCPayServer.Controllers
|
||||
double.TryParse(defaultAmount, out var amount);
|
||||
var model = new WalletSendModel()
|
||||
{
|
||||
CryptoCode = walletId.CryptoCode
|
||||
};
|
||||
if (bip21?.Any() is true)
|
||||
{
|
||||
foreach (var link in bip21)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(link))
|
||||
{
|
||||
|
||||
LoadFromBIP21(model, link, network);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!(model.Outputs?.Any() is true))
|
||||
{
|
||||
model.Outputs = new List<WalletSendModel.TransactionOutput>()
|
||||
Outputs = new List<WalletSendModel.TransactionOutput>()
|
||||
{
|
||||
new WalletSendModel.TransactionOutput()
|
||||
{
|
||||
Amount = Convert.ToDecimal(amount), DestinationAddress = defaultDestination
|
||||
Amount = Convert.ToDecimal(amount),
|
||||
DestinationAddress = defaultDestination
|
||||
}
|
||||
};
|
||||
},
|
||||
CryptoCode = walletId.CryptoCode
|
||||
};
|
||||
if (!string.IsNullOrEmpty(bip21))
|
||||
{
|
||||
LoadFromBIP21(model, bip21, network);
|
||||
}
|
||||
var feeProvider = _feeRateProvider.CreateFeeProvider(network);
|
||||
var recommendedFees =
|
||||
@ -490,8 +476,10 @@ namespace BTCPayServer.Controllers
|
||||
})
|
||||
.ToArray();
|
||||
var balance = _walletProvider.GetWallet(network).GetBalance(paymentMethod.AccountDerivation);
|
||||
model.NBXSeedAvailable = await GetSeed(walletId, network) != null;
|
||||
model.CurrentBalance = (await balance).Total.GetValue(network);
|
||||
model.NBXSeedAvailable = await CanUseHotWallet() && !string.IsNullOrEmpty(await ExplorerClientProvider.GetExplorerClient(network)
|
||||
.GetMetadataAsync<string>(GetDerivationSchemeSettings(walletId).AccountDerivation,
|
||||
WellknownMetadataKeys.MasterHDKey));
|
||||
model.CurrentBalance = await balance;
|
||||
|
||||
await Task.WhenAll(recommendedFees);
|
||||
model.RecommendedSatoshiPerByte =
|
||||
@ -523,15 +511,6 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private async Task<string> GetSeed(WalletId walletId, BTCPayNetwork network)
|
||||
{
|
||||
return await CanUseHotWallet() &&
|
||||
GetDerivationSchemeSettings(walletId) is DerivationSchemeSettings s &&
|
||||
s.IsHotWallet &&
|
||||
ExplorerClientProvider.GetExplorerClient(network) is ExplorerClient client &&
|
||||
await client.GetMetadataAsync<string>(s.AccountDerivation, WellknownMetadataKeys.MasterHDKey) is string seed &&
|
||||
!string.IsNullOrEmpty(seed) ? seed : null;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{walletId}/send")]
|
||||
@ -548,10 +527,11 @@ namespace BTCPayServer.Controllers
|
||||
if (network == null || network.ReadonlyWallet)
|
||||
return NotFound();
|
||||
vm.SupportRBF = network.SupportRBF;
|
||||
vm.NBXSeedAvailable = await GetSeed(walletId, network) != null;
|
||||
vm.NBXSeedAvailable = await CanUseHotWallet() && !string.IsNullOrEmpty(await ExplorerClientProvider.GetExplorerClient(network)
|
||||
.GetMetadataAsync<string>(GetDerivationSchemeSettings(walletId).AccountDerivation,
|
||||
WellknownMetadataKeys.MasterHDKey, cancellation));
|
||||
if (!string.IsNullOrEmpty(bip21))
|
||||
{
|
||||
vm.Outputs?.Clear();
|
||||
LoadFromBIP21(vm, bip21, network);
|
||||
}
|
||||
|
||||
@ -576,8 +556,7 @@ namespace BTCPayServer.Controllers
|
||||
Amount = coin.Value.GetValue(network),
|
||||
Comment = info?.Comment,
|
||||
Labels = info == null ? null : _labelFactory.ColorizeTransactionLabels(walletBlobAsync, info, Request),
|
||||
Link = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, coin.OutPoint.Hash.ToString()),
|
||||
Confirmations = coin.Confirmations
|
||||
Link = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, coin.OutPoint.Hash.ToString())
|
||||
};
|
||||
}).ToArray();
|
||||
}
|
||||
@ -590,10 +569,6 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
if (!string.IsNullOrEmpty(bip21))
|
||||
{
|
||||
if (!vm.Outputs.Any())
|
||||
{
|
||||
vm.Outputs.Add(new WalletSendModel.TransactionOutput());
|
||||
}
|
||||
return View(vm);
|
||||
}
|
||||
if (command == "add-output")
|
||||
@ -686,7 +661,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
return View(vm);
|
||||
|
||||
|
||||
DerivationSchemeSettings derivationScheme = GetDerivationSchemeSettings(walletId);
|
||||
|
||||
CreatePSBTResponse psbt = null;
|
||||
@ -737,7 +712,6 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
private void LoadFromBIP21(WalletSendModel vm, string bip21, BTCPayNetwork network)
|
||||
{
|
||||
vm.Outputs ??= new List<WalletSendModel.TransactionOutput>();
|
||||
try
|
||||
{
|
||||
if (bip21.StartsWith(network.UriScheme, StringComparison.InvariantCultureIgnoreCase))
|
||||
@ -746,13 +720,15 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
var uriBuilder = new NBitcoin.Payment.BitcoinUrlBuilder(bip21, network.NBitcoinNetwork);
|
||||
|
||||
vm.Outputs.Add(new WalletSendModel.TransactionOutput()
|
||||
vm.Outputs = new List<WalletSendModel.TransactionOutput>()
|
||||
{
|
||||
Amount = uriBuilder.Amount?.ToDecimal(MoneyUnit.BTC),
|
||||
DestinationAddress = uriBuilder.Address.ToString(),
|
||||
SubtractFeesFromOutput = false
|
||||
});
|
||||
new WalletSendModel.TransactionOutput()
|
||||
{
|
||||
Amount = uriBuilder.Amount.ToDecimal(MoneyUnit.BTC),
|
||||
DestinationAddress = uriBuilder.Address.ToString(),
|
||||
SubtractFeesFromOutput = false
|
||||
}
|
||||
};
|
||||
if (!string.IsNullOrEmpty(uriBuilder.Label) || !string.IsNullOrEmpty(uriBuilder.Message))
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
@ -770,11 +746,13 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
try
|
||||
{
|
||||
vm.Outputs.Add(new WalletSendModel.TransactionOutput()
|
||||
vm.Outputs = new List<WalletSendModel.TransactionOutput>()
|
||||
{
|
||||
new WalletSendModel.TransactionOutput()
|
||||
{
|
||||
DestinationAddress = BitcoinAddress.Create(bip21, network.NBitcoinNetwork).ToString()
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
@ -919,17 +897,17 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError(nameof(viewModel.SeedOrKey), "The master fingerprint does not match the one set in your wallet settings. Probable causes are: wrong seed, wrong passphrase or wrong fingerprint in your wallet settings.");
|
||||
ModelState.AddModelError(nameof(viewModel.SeedOrKey), "The master fingerprint does not match the one set in your wallet settings. Probable cause are: wrong seed, wrong passphrase or wrong fingerprint in your wallet settings.");
|
||||
return View(nameof(SignWithSeed), viewModel);
|
||||
}
|
||||
|
||||
var changed = psbt.PSBTChanged( () => psbt.SignAll(settings.AccountDerivation, signingKey, rootedKeyPath, new SigningOptions()
|
||||
var changed = PSBTChanged(psbt, () => psbt.SignAll(settings.AccountDerivation, signingKey, rootedKeyPath, new SigningOptions()
|
||||
{
|
||||
EnforceLowR = !(viewModel.SigningContext?.EnforceLowR is false)
|
||||
}));
|
||||
if (!changed)
|
||||
{
|
||||
ModelState.AddModelError(nameof(viewModel.SeedOrKey), "Impossible to sign the transaction. Probable causes: Incorrect account key path in wallet settings or PSBT already signed.");
|
||||
ModelState.AddModelError(nameof(viewModel.SeedOrKey), "Impossible to sign the transaction. Probable cause: Incorrect account key path in wallet settings, PSBT already signed.");
|
||||
return View(nameof(SignWithSeed), viewModel);
|
||||
}
|
||||
ModelState.Remove(nameof(viewModel.SigningContext.PSBT));
|
||||
@ -942,6 +920,15 @@ namespace BTCPayServer.Controllers
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private bool PSBTChanged(PSBT psbt, Action act)
|
||||
{
|
||||
var before = psbt.ToBase64();
|
||||
act();
|
||||
var after = psbt.ToBase64();
|
||||
return before != after;
|
||||
}
|
||||
|
||||
private string ValueToString(Money v, BTCPayNetworkBase network)
|
||||
{
|
||||
return v.ToString() + " " + network.CryptoCode;
|
||||
@ -1048,19 +1035,26 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
internal DerivationSchemeSettings GetDerivationSchemeSettings(WalletId walletId)
|
||||
{
|
||||
return CurrentStore.GetDerivationSchemeSettings(NetworkProvider, walletId.CryptoCode);
|
||||
var paymentMethod = CurrentStore
|
||||
.GetSupportedPaymentMethods(NetworkProvider)
|
||||
.OfType<DerivationSchemeSettings>()
|
||||
.FirstOrDefault(p => p.PaymentId.PaymentType == Payments.PaymentTypes.BTCLike && p.PaymentId.CryptoCode == walletId.CryptoCode);
|
||||
return paymentMethod;
|
||||
}
|
||||
|
||||
private static async Task<string> GetBalanceString(BTCPayWallet wallet, DerivationStrategyBase derivationStrategy)
|
||||
{
|
||||
using CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||
try
|
||||
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
|
||||
{
|
||||
return (await wallet.GetBalance(derivationStrategy, cts.Token)).Total.ShowMoney(wallet.Network);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "--";
|
||||
try
|
||||
{
|
||||
return (await wallet.GetBalance(derivationStrategy, cts.Token)).ShowMoney(wallet.Network
|
||||
.Divisibility);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "--";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1155,22 +1149,10 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
else if (command == "view-seed" && await CanUseHotWallet())
|
||||
{
|
||||
if (await GetSeed(walletId, derivationScheme.Network) != null)
|
||||
{
|
||||
var mnemonic = await ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode)
|
||||
var seed = await ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode)
|
||||
.GetMetadataAsync<string>(derivationScheme.AccountDerivation,
|
||||
WellknownMetadataKeys.Mnemonic, cancellationToken);
|
||||
var recoveryVm = new RecoverySeedBackupViewModel()
|
||||
{
|
||||
CryptoCode = walletId.CryptoCode,
|
||||
Mnemonic = mnemonic,
|
||||
IsStored = true,
|
||||
RequireConfirm = false,
|
||||
ReturnUrl = Url.Action(nameof(WalletSettings), new { walletId })
|
||||
};
|
||||
return this.RedirectToRecoverySeedBackup(recoveryVm);
|
||||
}
|
||||
else
|
||||
if (string.IsNullOrEmpty(seed))
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
@ -1178,6 +1160,18 @@ namespace BTCPayServer.Controllers
|
||||
Message = "The seed was not found"
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
var recoveryVm = new RecoverySeedBackupViewModel()
|
||||
{
|
||||
CryptoCode = walletId.CryptoCode,
|
||||
Mnemonic = seed,
|
||||
IsStored = true,
|
||||
RequireConfirm = false,
|
||||
ReturnUrl = Url.Action(nameof(WalletSettings), new { walletId })
|
||||
};
|
||||
return this.RedirectToRecoverySeedBackup(recoveryVm);
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(WalletSettings));
|
||||
}
|
||||
@ -1201,7 +1195,6 @@ namespace BTCPayServer.Controllers
|
||||
public string CryptoImage { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
public string Address { get; set; }
|
||||
public string PaymentLink { get; set; }
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,22 +0,0 @@
|
||||
using System;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class AddressClaimDestination : IBitcoinLikeClaimDestination
|
||||
{
|
||||
public BitcoinAddress _bitcoinAddress;
|
||||
|
||||
public AddressClaimDestination(BitcoinAddress bitcoinAddress)
|
||||
{
|
||||
if (bitcoinAddress == null)
|
||||
throw new ArgumentNullException(nameof(bitcoinAddress));
|
||||
_bitcoinAddress = bitcoinAddress;
|
||||
}
|
||||
public BitcoinAddress Address => _bitcoinAddress;
|
||||
public override string ToString()
|
||||
{
|
||||
return _bitcoinAddress.ToString();
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user